見出し画像

初心者向けLLM学習:名前生成の裏側③

この記事の対象者

  • PythonやGitをちょっとやったことある

  • LLM(大規模言語モデル)について興味がある

  • プログラミング初心者


前回の記事

前回は、トレーニングのコード部分について解説しました。今回は、名前を生成する部分と実際にそれらを使用するコードについて解説していきます。


namegen.pyの概要

namegen.pyは100行未満のコードで構成されています。

  1. クラスの定義: NameGenというクラスを作っていて、これが名前を生成するための設定や機能を持っているよ。

  2. 初期化 (__init__メソッド): クラスが最初にどんなデータを持っているかを設定している。文字や文字と番号の対応表(辞書)などがこれに当たるね。

  3. トレーニング (trainメソッド): この部分は、名前を生成するために必要な情報を学習するためのもの。ファイルから名前を読み込んで、どんな文字がどのくらいの頻度で使われるかを記録しているよ。

  4. 名前の生成 (generate_namesメソッド): 学習した情報をもとに、新しい名前をランダムに生成する部分。ルールに基づいて、一文字ずつ名前を組み立てていくよ。


4.名前の生成 (generate_namesメソッド)

学習した情報をもとに、新しい名前をランダムに生成する部分。こちらも長いので部分ごとに解説。

def generate_names(self, num_names=1):
        
        for _ in range(num_names):
            # 空のリストnameを作成
            name = []
            # 3つのインデックスを0で初期化
            ix1 = ix2 = ix3 = 0
            
            
            while True:
                # 次に来る文字の確率を計算して正規化
                p = self.fourgrams[ix1, ix2, ix3].float()
                p = p / p.sum()
                
                # 多次元テンソルpのすべての要素を1次元配列に変形
                probs_flat = p.view(-1)
                
                # 次に来る文字のインデックスをランダムに選択
                adjusted_ix = torch.multinomial(probs_flat, num_samples=1)
                
                # ランダムにより選ばれたインデックスに対応する文字を取得
                out = self.ind_to_char[adjusted_ix.item()]
                
                # サンプリングされた文字を名前に追加
                name.append(out)
                
               # 次の文字を選択するためのインデックスを更新
                ix1 = ix2
                ix2 = ix3
                ix3 = adjusted_ix
                
                # ランダムに選ばれた文字がピリオドの場合、名前の生成を終了
                if adjusted_ix == 0:
                    break
                
            # 生成された名前から最後の文字(ピリオド)を取り除く
            name = ''.join(name[:-1])
            
            # 生成された名前の最初の文字を大文字に変換
            name_capitalized = name[0].upper() + name[1:]
            print(name_capitalized)
            


初期化の部分は省略します。指定された数(num_names)の新しい名前を生成するための準備です。

for _ in range(num_names):
            name = []
            ix1 = ix2 = ix3 = 0


①次に来る文字の確率を計算して正規化

文字の組み合わせから次に来る文字を予想するルールを作っている。正規化とは、合計が1になるように調整すること。

while True:
    p = self.fourgrams[ix1, ix2, ix3].float()
    p = p / p.sum()
  1. 「self.fourgrams[ix1, ix2, ix3]」で、今までに見た文字の組み合わせがどれだけあるかをチェックするよ。これは、ある文字の組み合わせの後に、次にどんな文字が来るかの情報を持っている箱みたいなものだよ。

  2. 「.float()」っていうのは、この情報をもっと簡単に扱えるように変換する魔法だね。

  3. 「p = p / p.sum()」は、これまでに見た文字の組み合わせの中から、次に来る文字を選ぶ時のルールを作るんだ。このルールは、どの文字がどれだけのチャンスで来るかを決めるよ。たとえば、「さくら」の「ら」の後には何が来やすいかな?ということを決めるんだよ。


②多次元テンソルpのすべての要素を1次元配列に変形

要するに、いろんな箱(次元って言うんだけど)に分けられたアメ玉を、全部一つの長〜い箱に入れ直すようなもの。探しやすくするため。

probs_flat = p.view(-1)

view(-1)メソッドを使用することで、テンソル内の全ての値を1次元に「折り畳む」ことができるよ。こうすることで、計算された確率分布から特定の文字をランダムに、ただし計算された分布に従って、より効率的に抽出することが可能になるよ。


③次に来る文字のインデックスをランダムに選択

生成される名前に多様性を持たせるため。

adjusted_ix = torch.multinomial(probs_flat, num_samples=1)

お祭りのくじ引きみたいなものだよ。たくさんのくじがあって、それぞれのくじには文字が書いてあるんだ。でも、このくじ引きでは、くじを引くチャンスが全部同じじゃないの。いくつかのくじはよく引かれやすくて、いくつかはあんまり引かれないんだ。

  • probs_flat:それぞれのくじが引かれるチャンスが書かれたリストみたいなもの。

  • torch.multinomial:上のリストを使って、ランダムにくじを引くんだけど、各くじの引かれるチャンスに従う。

  • num_samples=1:1回だけくじを引くって意味。


④ランダムにより選ばれたインデックスに対応する文字を取得


 out = self.ind_to_char[adjusted_ix.item()]

ind_to_charを使って、ランダムサンプリングにより選ばれたインデックス(adjusted_ix.item())に対応する文字を取得するよ。つまり、選ばれた数字(インデックス)をもとに、その数字が示す特定の文字を見つけ出すよ。このプロセスにより、次に追加する文字が決定されるんだ。

item()っていうのは、Pythonのプログラミングで使う特別な魔法の呪文みたいなものだよ。特に、PyTorchという魔法の道具を使っている時によく出てくるんだ。この呪文を使うと、テンソル(数やデータがたくさん詰まった箱)から、中の一つの数やデータを取り出して、普通の数字や情報に変えることができるんだ。


⑤サンプリングされた文字を名前に追加

新しい名前を作る時に、選んだ文字を名前のリストに追加する。


name.append(out)

たとえば、「さ」って文字を選んだら、その「さ」を名前の最初に置くんだ。次に「く」って文字を選んだら、「さ」の後ろに「く」を追加して、「さく」にする。これを繰り返して、一つずつ文字を名前のリストに追加していくことで、新しい名前を作っていくんだよ。


⑥次の文字を選択するためのインデックスを更新

この更新プロセスにより、次の文字の選択に必要な前の3文字の情報が保持され、名前生成の各ステップで次に来るべき文字の予測が可能になる。

ix1 = ix2
ix2 = ix3
ix3 = adjusted_ix

ix1にix2の値を、ix2にix3の値を、そしてix3には新しく選ばれた文字のインデックスadjusted_ixの値をそれぞれ代入している。


⑦ランダムに選ばれた文字がピリオドの場合、名前の生成を終了

名前の生成を適切な長さで停止させるため。

if adjusted_ix == 0:
   break

ンダムに選ばれた文字がピリオド(.、インデックス0に対応)の場合、名前の生成を終了するための条件を設定しているよ。ピリオドが選ばれたことは、名前の終了を意味するため、breakを使って、さっきの文字を追加するループから抜け出すよ。


⑧生成された名前から最後の文字(ピリオド)を取り除く

ピリオドなしのきれいな名前にするため。

name = ''.join(name[:-1])

生成された名前から最後の文字(ピリオド)を取り除き、残りの文字を結合して文字列にする処理を行うよ。name[:-1]で最後の文字を除外したリストを取得し、''.join()を使ってこれらの文字を結合しているよ。


⑨生成された名前の最初の文字を大文字に変換

名前が文化的に一般的な形式(先頭文字が大文字)で出力するよ。

name_capitalized = name[0].upper() + name[1:]
print(name_capitalized)

name[0].upper()で最初の文字を大文字にし、name[1:]で最初の文字以外の部分を取得しているよ。


定義したものを実際に動かす

これまでに定義されたクラスとメソッドを実際に使う部分。

model = NameGen()
model.train('names.txt')
model.generate_names(num_names=10)
  • model = NameGen():NameGenクラスの新しいインスタンスを作成

  • model.train('names.txt'):そのインスタンスに対してtrainメソッドを呼び出し、'names.txt'というファイルから名前を学習させている。

  • model.generate_names(num_names=10):最後に、そのインスタンスに対してgenerate_namesメソッドを呼び出し、学習したデータを元に10個の新しい名前を生成する。



おまけ

日本語で名前生成する場合、そのままではエラーになるので以下の部分に「encoding="utf-8"」を追加する。

with open(filename, 'r', encoding="utf-8") as f:
    words = f.read().splitlines()


names.txtファイルを使用するか新しいテキストファイルを作成してデータを作成する。データは以下の名前メーカーをお借りして作成しました。

新しいテキストファイルを指定する場合は以下のコードを修正します。(例:kana.txt)

model = NameGen()
model.train('kana.txt')
model.generate_names(num_names=10)


実行結果

Vocab Tokens:
['ァ', 'ア', 'ィ', 'イ', 'ウ', 'ェ', 'エ', 'オ', 'カ', 'ガ', 'グ', 'ケ', 'ゴ', 'サ', 'シ', 'ジ', 'ス', 'セ', 'ゼ', 'ソ', 'タ', 'ダ', 'チ', 'ッ', 'ツ', 'テ', 'デ', 'ト', 'ド', 'ナ', 'ニ', 'ヌ', 'ネ', 'ノ', 'ハ', 'バ', 'パ', 'ヒ', 'ビ', 'フ', 'プ', 'ヘ', 'ベ', 'ペ', 'ホ', 'ボ', 'ポ', 'マ', 'ミ', 'ム', 'モ', 'ャ', 'ヨ', 'ラ', 'リ', 'ル', 'レ', 'ロ', 'ワ', 'ン', 'ヴ', 'ー']
Vocab Length: 63
Character-to-Index mapping:
{'ァ': 1, 'ア': 2, 'ィ': 3, 'イ': 4, 'ウ': 5, 'ェ': 6, 'エ': 7, 'オ': 8, 'カ': 9, 'ガ': 10, 'グ': 11, 'ケ': 12, 'ゴ': 13, 'サ': 14, 'シ': 15, 'ジ': 16, 'ス': 17, 'セ': 18, 'ゼ': 19, 'ソ': 20, 'タ': 21, 'ダ': 22, 'チ': 23, 'ッ': 24, 'ツ': 25, 'テ': 26, 'デ': 27, 'ト': 28, 'ド': 29, 'ナ': 30, 'ニ': 31, 'ヌ': 32, 'ネ': 33, 'ノ': 34, 'ハ': 35, 'バ': 36, 'パ': 37, 'ヒ': 38, 'ビ': 39, 'フ': 40, 'プ': 41, 'ヘ': 42, 'ベ': 43, 'ペ': 44, 'ホ': 45, 'ボ': 46, 'ポ': 47, 'マ': 48, 'ミ': 49, 'ム': 50, 'モ': 51, 'ャ': 52, 'ヨ': 53, 'ラ': 54, 'リ': 55, 'ル': 56, 'レ': 57, 'ロ': 58, 'ワ': 59, 'ン': 60, 'ヴ': 61, 'ー': 62, '.': 0}
Trarining starts ...
Training finished!
イウールス
ビハダドリ
ネダニス
ヘンスロー
ペータル
ゼヒール
ディビッド
フラリル
ルディ
プルゼンシオ


せっかくなので生成された名前でDALL-Eにキャラクターを描いてもらった。(なぜか11人いる…)

ファンタジー風のキャラ

この記事が気に入ったらサポートをしてみませんか?