ソフトウェアの品質を学びまくる

ソフトウェアの品質、ソフトウェアテストなどについて学んだことを記録するブログです。

猿とシェイクスピアと失敗とChatGPT

 「猿とシェイクスピア」の話は、いろんな文脈で出てくるアナロジーです。

  • 猿がランダムにキーボードをたたき続けると、いずれはシェイクスピアの戯曲と同じテキストが生成される。
     あるいはその逆に、
  • 猿がランダムにキーボードをたたき続けてシェイクスピアの戯曲を生成するには、宇宙の寿命があっても足りない

といったように言及されます*1

 一方『失敗の科学』では、この話に別の観点を与えています。

失敗の科学

失敗の科学

Amazon

 アルファベットと空白からなる27種類の文字からランダムに生成した28文字の文字列が、シェイクスピアの一節28文字と一致する確率は、わずか10の40乗分の1。
 しかし、生成した文章をチェックし、目標の一節に少しでも近いものを選択する。その文字列にランダムな変化を加えて次世代とし、これを繰り返すと、43世代目で目標に到達した、という実験があるそうです。これを「累積淘汰のメカニズム」と呼んでいます。

 「小さく失敗し、その失敗から学んで改善することで、成功に近づくことができる」「成功のためには失敗が必要である」という主張を、「猿とシェイクスピア」のアナロジーで説明しているわけです。お察しの通りこの話は、リーン開発、アジャイル開発の話にもつながっています。
 「確率がどれだけ低くても、決してゼロではない」か、「確率が低すぎれば、それはゼロと変わりない」のアナロジーとしてしか聞いたことがなかったため、とても新鮮な発想でした。

ChatGPTで、累積淘汰のメカニズムを実装してみる

 さて、ここからは話が変わります。この「累積淘汰のメカニズム」、実感してみたい!

  だけどぼくにはプログラミング能力がない

  きみに聴かせる腕もない

DREAM PRICE 1000 西田敏行 もしもピアノが弾けたなら

DREAM PRICE 1000 西田敏行 もしもピアノが弾けたなら

Amazon

 ので、ChatGPTの練習を兼ねて、このメカニズムを実験するコードを書いてみることにしました!
 もっといいプロンプトあるよ! こういうコードの方がいいよ! というか意図したコードになっていないよ!などあれば、教えてください。

スコアリングの仕組みを考える

生成した文章をチェックし、目標の一節に少しでも近いものを選択

 まずこの部分。2つの文字列がどのくらい一致しているかをスコアリングするかを考えます。

 そこで思い出すのが、日本でも流行したWordle。
 あのゲームでは、「文字も位置も一致していれば緑、文字は合っているが位置は合っていない場合は黄色」で表示されましたね。あの色を見れば、どのくらい目標に近づいているかがわかるわけです。HIT、BLOWとも呼ばれるものです。

www.nytimes.com

 このコンセプトを、スコアリングのルールに使います。
 指定したプロンプトは以下です。ルール4を自然言語で書くのが難しかったですが、普通に通じていました。

見本となる文字列Sに対し、与えられた別の文字列xがどのくらい一致しているか点数をつけるPythonのコードを書いてください。ルール*2は以下の通りです。

  • ルール1. Sとxは、大文字アルファベット26種類の文字から構成される。
  • ルール2. Sとxは、ともに10文字である。
  • ルール3. xに指定した10文字それぞれについて、文字も位置もSと一致している場合は、1つにつき5点を与える。
  • ルール4. xに指定した10文字それぞれについて、位置は一致しないが同じ文字がSにも含まれている場合は、1つにつき2点を与える。
  • ルール5. ルール3とルール4の点数を合計した数値が、文字列xと点数となる。

 生成されたコードは、以下。
 特に問題なさそうです。ルール3とルール4で2重計上していることもなさそう。一番最後のコメントの足し算を間違っているけど・・・。

def calculate_score(S, x):
    # 初期スコアを0に設定
    score = 0
    
    # ルール3に基づいてスコアを計算
    for i in range(10):
        if S[i] == x[i]:
            score += 5
    
    # ルール4に基づいてスコアを計算
    for i in range(10):
        if x[i] in S and S[i] != x[i]:
            score += 2
    
    return score

# 例
S = "ABCDEFGHIJ"
x = "ABCDWXYZIJ"
print(calculate_score(S, x))  # この例では、スコアは5*6 + 2*2 = 32 となります。

 なんとなく関数名が気に食わなかったので、別の名前を10個提案してもらい、rate_string_similarity に変更しました。

文字列にランダムな変化を加える

その文字列にランダムな変化を加えて次世代とし

 これについて、以下のようなプロンプトを指定しました。

では次のように、文字列中の文字をランダムに置き換える関数をpythonで作成してください。 ルールは以下の通りです。

  • ルール1: 与えられた文字列xのすべての文字をランダムに置換する。
  • ルール2: 置換後の文字は、大文字アルファベット26文字から選ぶ。
  • ルール3: 置換後の文字が、置換前の文字で合ってもよい。

生成されたコードは、以下の通りです。
わたしは .join(random.choice(alphabet) for _ in x) みたいな書き方はできないですね。。

import random

def random_replace(x):
    # 大文字アルファベット26文字を定義
    alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    
    # 文字列xの各文字をランダムに置換
    replaced = ''.join(random.choice(alphabet) for _ in x)
    
    return replaced

### 例
x = "HELLOWORLD"
print(random_replace(x))

ある文字列と、ランダム置換後の文字列のスコアを比較する

目標の一節に少しでも近いものを選択する。

 使いたい関数はもうできているので、組み合わせるだけです。プロンプトは以下。

ある文字列と、それをランダムに置換した文字列のスコアを比較するPythonのコードを書いてください。
ルールは以下の通りです。

  • ルール1: 文字列xを受け取る。
  • ルール2: 文字列xを、関数random_replaceで置換する。これを文字列yと呼ぶ。
  • ルール3: 文字列xと文字列Sの類似度を、関数rate_string_similarityで点数化する。
  • ルール4: 文字列yと文字列Sの類似度を、関数rate_string_similarityで点数化する。
  • ルール5: 点数が高かった方の文字列を返す。

 生成されたコードは以下。
 「返す」と書くと、print で表現されるのか。

import random

def random_replace(x):
    # 大文字アルファベット26文字を定義
    alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    
    # 文字列xの各文字をランダムに置換
    replaced = ''.join(random.choice(alphabet) for _ in x)
    
    return replaced

def rate_string_similarity(S, x):
    # 初期スコアを0に設定
    score = 0
    
    # ルール3に基づいてスコアを計算
    for i in range(10):
        if S[i] == x[i]:
            score += 5
    
    # ルール4に基づいてスコアを計算
    for i in range(10):
        if x[i] in S and S[i] != x[i]:
            score += 2
    
    return score

def compare_strings(S, x):
    # ルール2に基づいて文字列xをランダムに置換
    y = random_replace(x)
    
    # ルール3とルール4に基づいてスコアを計算
    score_x = rate_string_similarity(S, x)
    score_y = rate_string_similarity(S, y)
    
    # ルール5に基づいて点数が高かった方の文字列を返す
    return x if score_x >= score_y else y

# 例
S = "ABCDEFGHIJ"
x = "ABCDWXYZIJ"
print(compare_strings(S, x))

試行を繰り返させる

 文字列をランダムに変化させ、変化前と後を比較して、スコアの高い方を残すことができました。これを繰り返していきましょう。
 プロンプトは以下*3

ランダムに指定した文字列に対して置換を繰り返すことで、目的とする文字列に近づけるかどうかを確認します。 次のような操作を繰り返してください。

  • 手順1: 目的とする文字列Sを "ABCDEFJHIJ" とする。
  • 手順2: 初期文字列xをランダムに指定する。xは大文字アルファベット26種類からなる、10文字の文字列である。
  • 手順3: 文字列xに対し、関数compare_stringsを適用し、よりスコアの高い文字列xと、その時のスコアを出力する。
  • 手順4: 以下のいずれかの条件を満たすまで、手順3を繰り返す。
      条件1: xがSに一致する。
     
    条件2: 手順3の実行回数が100回を超える。

 生成されたコードは以下。

import random

def random_replace(x):
    alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    replaced = ''.join(random.choice(alphabet) for _ in x)
    return replaced

def rate_string_similarity(S, x):
    score = 0
    for i in range(10):
        if S[i] == x[i]:
            score += 5
    for i in range(10):
        if x[i] in S and S[i] != x[i]:
            score += 2
    return score

def compare_strings(S, x):
    y = random_replace(x)
    score_x = rate_string_similarity(S, x)
    score_y = rate_string_similarity(S, y)
    return (x, score_x) if score_x >= score_y else (y, score_y)

def main():
    # 手順1
    S = "ABCDEFJHIJ"
    
    # 手順2
    x = ''.join(random.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ") for _ in range(10))
    
    iteration = 0
    while True:
        # 手順3
        x, score = compare_strings(S, x)
        print(f"Iteration {iteration + 1}: x = {x}, Score = {score}")
        
        # 手順4
        iteration += 1
        if x == S or iteration > 100:
            break

main()

 でけた!

しかしうまくいかない・・・

 このコードをそのままChatGPTで実行させることができそうなのですが、やり方を知らないのでVSCode上で実行してみました。

 その結果・・・100回試行で打ち切りになってしまう。
 最大試行回数を10,000にしても、

Iteration 10001: x = ACZLZAJJIJ, Score = 26

 全文字一致となる50点(位置文字一致5点×10文字)に遠く及ばないし、そもそも試行中に文字列がほとんど変化していない。

ChatGPTに頼り切る

 うーん、コードが悪いのか、そもそもアルゴリズムが間違っているのか・・・と悩むこと数秒。
 「ChatGPTに聞けばいいのか? いやでもそんなこと理解できる?」

 とりあえず聞いてみました。

実行したところ、100回繰り返しても文字列がほとんど変化しません。試行を繰り返すことで、スコアが徐々によくなっていくと思ったのですが、なぜこんなにも変わらないのでしょうか?

 すると・・・

問題の原因は、compare_strings関数内でrandom_replace関数を使用しているためです。random_replace関数は、文字列xのすべての文字をランダムに置き換えるため、毎回完全に新しいランダムな文字列が生成されます。そのため、前回の試行で得られたスコアが良かったとしても、次の試行ではそれを利用することなく、完全に新しいランダムな文字列が評価されることになります。

この問題を解決するためには、文字列xの一部の文字だけをランダムに置き換えるように変更する必要があります。これにより、前回の試行で得られた良い結果を部分的に維持しつつ、新しい試行でさらに良い結果を得ることが期待できます。

以下に、文字列xの一部の文字だけをランダムに置き換える修正を加えたコードを示します。

 ひええええ頭がよすぎるううう!というか自分の頭が悪すぎるううう!
 毎回ランダムなんだから、高いスコアが偶然出る可能性はどんどん下がっていくわけですね。なので、すべての文字ではなく、一部の文字だけを置換する必要があったわけです。

import random

def partial_random_replace(x):
    # 1〜3文字の位置をランダムに選択
    num_replacements = random.randint(1, 3)
    positions = random.sample(range(10), num_replacements)
    
    # 選択された位置の文字をランダムに置き換え
    x_list = list(x)
    alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    for pos in positions:
        x_list[pos] = random.choice(alphabet)
    
    return ''.join(x_list)

def rate_string_similarity(S, x):
    score = 0
    for i in range(10):
        if S[i] == x[i]:
            score += 5
    for i in range(10):
        if x[i] in S and S[i] != x[i]:
            score += 2
    return score

def compare_strings(S, x):
    y = partial_random_replace(x)
    score_x = rate_string_similarity(S, x)
    score_y = rate_string_similarity(S, y)
    return (x, score_x) if score_x >= score_y else (y, score_y)

def main():
    S = "ABCDEFJHIJ"
    x = ''.join(random.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ") for _ in range(10))
    
    iteration = 0
    while True:
        x, score = compare_strings(S, x)
        print(f"Iteration {iteration + 1}: x = {x}, Score = {score}")
        
        iteration += 1
        if x == S or iteration > 100:
            break

main()

 新しく作られた関数 partial_random_replace では、まず何文字置換するかを決め、文字列中の置換位置と置換文字をランダムに決めて、置換しています。

 これを実行したところ、「VTRCOBETEU」からスタートした文字列が、731回目の試行で「ABCDEFJHIJ」に変化しました。

まとめ

 実装してほしい仕様を自然言語で書いて、ChatGPTにコードにしてもらい、実行してみて、うまくいかなかったら直してもらう、という過程は、とても楽しいです!

 以下、まとめです。全然関係のなさそうな主張2つになってしまった。

  • 小さい失敗を繰り返して改善・軌道修正していくことで、成功に近づく可能性が高くなる
  • 「これは言っても伝わらなくない?」ということでも、ChatGPTはけっこう理解してくれる

キーボードを叩く猿

おまけ

 目標文字列を「生きるべきか、・・・」にした場合の変化の過程は以下の通りです。

Iteration 2: x = BNKMOJKCHMMOITWILJ AJDOAZEWMIXOZYCSJMON, Score = 51
Iteration 4: x = BNKMOJKCHMMOITWILJ AHDOAZEWMIXOZYYSJMON, Score = 56
Iteration 7: x = BNKMOGKCHMMOITWILP AHDOAZEWMIXOZYRSJMON, Score = 58
Iteration 10: x = BNTMOGKEHMMOITWILP AHDOAZEWMIXOZYRSJMON, Score = 62
Iteration 11: x = BNTMOGKEHMMOITWILP AHDOAZE MIXOZYRSJMON, Score = 67
Iteration 16: x = BNTMOGKEHMMOITWILR AHDOAZE MIXOZYRSJMON, Score = 69
Iteration 21: x = TNTMOGKEHMMOITWILR AHDOAFE OIXOZYRSJMON, Score = 74
Iteration 33: x = TNTMOGKEHUMOITWILR AHDOAFE OIQOZYCSJMON, Score = 76
Iteration 39: x = TNTMOGKEHUMOITWILR AHDOAFE OIQ ZYCSJMON, Score = 79
Iteration 42: x = TNTMOGKEHUMOITEILR AHDOAFE OIQ ZYCSJMON, Score = 81
Iteration 45: x = TNTMOGKEHUMOITEILR AHDOAHE OIQ ZYCSJMON, Score = 83
Iteration 65: x = TNTMOGKEHVMOITEILR AHDOAHE OIQ ZYCSTMON, Score = 86
Iteration 67: x = TNTBOGKEHVMOITEILR AHDOAHE OIQ ZYCSTMON, Score = 91
Iteration 69: x = TNTBOGKEHVMOITEILR AH OAHR OIQ ZRCSTMON, Score = 95
Iteration 70: x = TNTBOUQEHVMOITEILR AH OZHR OIQ ZRCSTMON, Score = 97
Iteration 77: x = TNTBOUQEHVMOITEILR AH OZHR OIQ ZRNSTMON, Score = 99
Iteration 82: x = TNTBOUQEHVMOITEILR AH OOHR OIQ ZRNSTMON, Score = 101
Iteration 92: x = TNTBAOQEHVMOITEILR AH OOIR OIQ ZRNSTMON, Score = 104
Iteration 120: x = TNTBAOQEHSMOITEILR AH OOIR OIQ ZRNSTMON, Score = 106
Iteration 145: x = TFTBAOQEHSMOITEILR AH O IR OIQ ZRNSTMON, Score = 107
Iteration 177: x = TFTBAOQEHSMOITEIHR AH O IR OIQ ZRNSTMON, Score = 109
Iteration 196: x = TFTBEOQEHSMOITEIHR AH O IR OIQ ZRNSTMON, Score = 112
Iteration 199: x = TUTBEOQEHSBOITEIHR AH O IL OIQ ZRNSTMON, Score = 114
Iteration 202: x = TUTBEOQEQSBOITO HR AH O IL OIQ ZRNSTMON, Score = 120
Iteration 214: x = TUTBEOQEQSBOTTO HR AH O I  OIQ ZRNSTMON, Score = 122
Iteration 230: x = TUTBEOQEQSBOTTO HH AH R I  OIQ ZRNSTNON, Score = 124
Iteration 251: x = TOTBEOEECSBOTTO HH AH R I  OIQ ZRNSTNON, Score = 125
Iteration 255: x = TOTBEOEECSOOTTO HH AH R I  OQQ XRNSTNON, Score = 128
Iteration 273: x = TOTBEOEEBSOOTTO HH AH R I  OQH XRNSTNON, Score = 130
Iteration 281: x = TOTBEOEEBSOOTTO BH AH R I  OQH XRNSTNON, Score = 133
Iteration 313: x = TOTBEOEEBSOOTTO BH AH R I  TQH XRNSTNON, Score = 136
Iteration 332: x = TO BEOEEBSOOTTO BH AH R I  TQH XRNSTNON, Score = 139
Iteration 379: x = TO BEOEEBSOOTTO BH AH R I  TQH XRNSTION, Score = 142
Iteration 553: x = TO BEOEEBSOOTTO BH AH R I  TQH HRNSTION, Score = 144
Iteration 646: x = TO BEOEEBSOOTTO BH AH T I  TQH HRNSTION, Score = 147
Iteration 685: x = TO BEOEEBSOOTTO BH AH T I  TQE HRNSTION, Score = 150
Iteration 736: x = TO BEOEEBSOOTTO BH AH T I  TQE HRESTION, Score = 153
Iteration 822: x = TO BEMEIBSOOTTO BH AH T IS TQE HRESTION, Score = 154
Iteration 985: x = TO BEMEIBMOOTTO BE AH T IS TQE HRESTION, Score = 155
Iteration 989: x = TO BEMEIBMOOTTO BE AH T IS THE HRESTION, Score = 158
Iteration 1075: x = TO BEMEI MOOTTO BE AH T IS THE HRESTION, Score = 161
Iteration 1212: x = TO BEAEI MOOTTO BE AH T IS THE HRESTION, Score = 163
Iteration 1842: x = TO BEAEI  OOTTO BE AH T IS THE HRESTION, Score = 165
Iteration 1891: x = TO BEAEI  OOTTO BE AHAT IS THE HRESTION, Score = 168
Iteration 2008: x = TO BEAEI  OTTTO BE AHAT IS THE HRESTION, Score = 171
Iteration 2146: x = TO BEAOI  OTTPO BE AHAT IS THE HUESTION, Score = 172
Iteration 2174: x = TO BEAOI  OTTTO BE AHAT IS THE HUESTION, Score = 177
Iteration 2367: x = TO BE OI  OTTTO BE AHAT IS THE HUESTION, Score = 180
Iteration 3137: x = TO BE OI NOTTTO BE AHAT IS THE HUESTION, Score = 183
Iteration 7172: x = TO BE OR NOTVTO BE AHAT IS THE HUESTION, Score = 184
Iteration 7503: x = TO BE OR NOT TOBBE AHAT IS THE HUESTION, Score = 186
Iteration 7994: x = TO BE OR NOT TO BE AHAT IS THE HUESTION, Score = 189
Iteration 8524: x = TO BE OR NOT TO BE AHAT IS THE QUESTION, Score = 192
Iteration 8581: x = TO BE OR NOT TO BE THAT IS THE QUESTION, Score = 195

 IterationとScoreの関係は以下の通り。これもChatGPTに描いてもらえたかな?

IterationとScoreの関係

*1:Wikipediaでは、「無限の猿定理」(infinite monkey theorem)として説明されています。これを読むと、もっと深い意味のある内容であることがわかります。

*2:変数名Sとかxって何だよって言われるやつかしら・・・

*3:実際には、最初の2行を指定するのを忘れていました。なのに、手順だけでちゃんと回答してくれました。