見出し画像

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

この記事の対象者

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

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

  • プログラミング初心者


前回の記事

https://note.com/toki_mwc/n/nc12eab405d43


前回は、namegenを使って名前を生成するまでの操作とクラスの定義、初期化について説明しました。今回は、トレーニングのコード部分について解説します。


namegen.pyの概要

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

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

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

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

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



3.トレーニング (trainメソッド)

データセットをロードしてモデルをトレーニングする機能。長いので部分ごとに解説します。

def train(self, filename):
        
        # ファイルを開いて中身を読み込む操作
        with open(filename, 'r') as f:
            words = f.read().splitlines()
        
        # データセット内の一意な文字を取得し、それらをソートして「アルファベット」のような語彙(vocabulary)を作成
        self.characters = sorted(list(set(''.join(words))))
        print(f"Vocab Tokens:\n {self.characters}")
        
        # 作成した語彙(文字のリスト)の長さを計算し、その長さを表示
        vocab_len = len(self.characters) + 1
        print(f"Vocab Length: {vocab_len}")
        
    # 文字とインデックス(位置)の対応関係を作成
        self.char_to_ind = {}
        for i, s in enumerate(self.characters):
            self.char_to_ind[s] = i +  1

        # 文字とインデックスのマッピングにピリオド「.」を追加        
        self.char_to_ind['.'] = 0
        print(f"Character-to-Index mapping:\n {self.char_to_ind}")

        # インデックスから文字へのマッピングを作成
        self.ind_to_char = {}
        for s, i in self.char_to_ind.items():
            self.ind_to_char[i] = s
        
        # 4次元テンソルself.fourgramsを初期化
        self.fourgrams = torch.zeros((vocab_len, vocab_len, vocab_len, vocab_len), dtype=torch.int32)

        # トレーニングプロセスを開始を出力
        print("Trarining starts ...")

    # 各単語にパディングとしてピリオド(.)を3つ追加
        for word in words:
            chs = ['.', '.', '.'] + list(word) + ['.', '.', '.']
      
            # 連続する4文字の出現頻度を計算してself.fourgramsテンソルに記録
            for ch1, ch2, ch3, ch4 in zip(chs, chs[1:], chs[2:], chs[3:]):
                ix1 = self.char_to_ind[ch1]
                ix2 = self.char_to_ind[ch2]
                ix3 = self.char_to_ind[ch3]
                ix4 = self.char_to_ind[ch4]
                self.fourgrams[ix1, ix2, ix3, ix4] += 1

     
        # トレーニングプロセスの終了を出力
        print("Training finished!")

        # トレーニングの重みを保存するオプション
        # torch.save(self.fourgrams, "namegen_weights.pt")


それぞれの機能について説明していきます。

①ファイルを開いて中身を読み込む操作
names.txtから名前などの単語リストを得るため。

with open(filename, 'r') as f:
    words = f.read().splitlines()

「filename」という名前のファイルを読み込みモードで開き、その中身を一行ずつ読んでリストにする。splitlines()メソッドを使うことで、ファイルの内容を行ごとに分割。今回は、names.txt というファイルを読み込んでいる。以下のようにファイル名を指定すると思うが、このコードではそのような指定をしなくても動作した。
コードの一番最後にあるmodel.train('names.txt')でnames.txtファイルを読み込んでいる。



②データセット内の一意な文字を取得し、それらをソートして「アルファベット」のような語彙(vocabulary)を作成

self.characters = sorted(list(set(''.join(words))))
print(f"Vocab Tokens:\n {self.characters}")

まず与えられた単語のリスト(words)を連結して、その中の一意な文字※だけを抽出します。これにより、重複を排除した文字のセットが得られます。その後、このセットをリストに変換し、sorted関数でアルファベット順に並べ替える。

※一意な文字とは、同じ文字が2回以上出現しない文字 です。つまり、すべての文字が異なる文字 になります。

  • "abc"

  • "hello"

  • "12345"


前回の記事でも紹介したが、実行結果で以下のように表示される。

Vocab Tokens:
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']



③作成した語彙の長さを計算し、その長さを表示

名前を生成する際の選択肢の範囲を明確にするため。

vocab_len = len(self.characters) + 1
print(f"Vocab Length: {vocab_len}")

語彙に含まれる一意な文字の数に1を足すことで、名前生成時に使用可能な全ての文字(ドットを含む)の総数を求めている。


④文字とインデックス(位置)の対応関係を作成



self.char_to_ind = {}
for i, s in enumerate(self.characters):
    self.char_to_ind[s] = i +  1

self.charactersに含まれる各文字に対して、その文字がリスト内でどの位置にあるかを示す数値を割り当て、それをself.char_to_indという辞書に保存しています。ここで、i + 1としているのは、インデックスを1から始めるためです。後で文字に基づいてデータを参照する際に、どの文字がどの位置に対応するかを簡単に見つけるため。


⑤文字とインデックスのマッピングにピリオド「.」を追加


self.char_to_ind['.'] = 0
print(f"Character-to-Index mapping:\n {self.char_to_ind}") 

ピリオドは名前の開始と終了を示す目印にしたいので、インデックス番号の0に割り当てている。これにより、名前の始まりと終わりを簡単に識別できるようになるね。そして、print関数を使って、この文字とインデックスの対応関係(マッピング)を表示しているよ。これは、各文字がどの数字に対応するかを確認するためだよ。

ちなみに、実行結果では以下のように表示される。

Character-to-Index mapping:{'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6, 'g': 7, 'h': 8, 'i': 9, 'j': 10, 'k': 11, 'l': 12, 'm': 13, 'n': 14, 'o': 15, 'p': 16, 'q': 17, 'r': 18, 's': 19, 't': 20, 'u': 21, 'v': 22, 'w': 23, 'x': 24, 'y': 25, 'z': 26, '.': 0}


⑥インデックスから文字へのマッピングを作成

先ほどのchar_to_indの逆をやるよ。今度は、インデックスから文字へのマッピング(キーと値のペア)を作成する。


 self.ind_to_char = {}
 for s, i in self.char_to_ind.items():
     self.ind_to_char[i] = s

self.ind_to_charという辞書を使って、各インデックス(数字)に対応する文字を保存している。これにより、数字から文字を簡単に引き出すことができ、名前を生成するプロセスでこの逆マッピングが役立つ。


⑦4次元テンソルself.fourgramsを初期化

4文字の連続の出現頻度を記録するための4次元テンソル(self.fourgrams)※を初期化している。(この辺、もうよくわかんないね…)

※4次元テンソルとは、4つのインデックスを持つ多次元配列です。つまり、4つの異なる方向に沿って値を格納することができる。機械学習とかに使用されるやつ。

self.fourgrams = torch.zeros((vocab_len, vocab_len, vocab_len, vocab_len), dtype=torch.int32)

テンソルの各軸のサイズは語彙の長さに基づいており、torch.int32を使うことで、要素ごとに32ビット整数を格納します。

元のコードには、PyTorchがない場合の実装例として、4重のリスト内包表記を使って同様の構造を0で初期化する方法も示されている。これにより、特定の4文字の組み合わせがどれだけ頻繁に登場するかを追跡できる。

# PyTorchがない場合の実装例
self.fourgrams = [[[[0 for _ in range(vocab_len)] for _ in range(vocab_len)] for _ in range(vocab_len)] for _ in range(vocab_len)]


さぁ、トレーニング開始だ!

print("Trarining starts ...")



⑧各単語にパディングとしてピリオド(.)を3つ追加

単語の開始と終了を示すマーカーとして機能


for word in words:
    chs = ['.', '.', '.'] + list(word) + ['.', '.', '.']


⑨連続する4文字の出現頻度を計算してself.fourgramsテンソルに記録

名前の中で隣り合う4つの文字がどのように組み合わされているかを調べている。

for ch1, ch2, ch3, ch4 in zip(chs, chs[1:], chs[2:], chs[3:]):
    ix1 = self.char_to_ind[ch1]
    ix2 = self.char_to_ind[ch2]
    ix3 = self.char_to_ind[ch3]
    ix4 = self.char_to_ind[ch4]
    self.fourgrams[ix1, ix2, ix3, ix4] += 1
  1. chs:名前の文字のリストだよ。例えば、「さくら」って名前があったら、その文字一つ一つがリストに入っているの。

  2. zip(chs, chs[1:], chs[2:], chs[3:]):このリストから4つの文字を一緒に見ていく方法。例えば「さ」「く」「ら」「ん」と次々に文字を見ていくんだ。zip関数。

  3. ix1, ix2, ix3, ix4:それぞれの文字がどの位置(インデックス)にあるかを保存しているの。

  4. self.fourgrams[ix1, ix2, ix3, ix4] += 1:「さくらん」って組み合わせが出た回数を1つ増やしているんだ。これを全部の名前でやって、どんな文字の組み合わせがよく出るかを調べているんだよ。


zip関数は、いくつかのリスト(物のリストみたいなもの)を一緒に見るための魔法の道具だよ。たとえば、お友達の名前が書かれたリストと、そのお友達の好きなアイスクリームの味が書かれたリストがあったとしよう。

名前リスト:[さくら、たろう、はなこ]
好きなアイスクリームリスト:[チョコ、バニラ、ストロベリー]

zip関数を使うと、これらのリストをペアにできるんだ。「さくらはチョコが好き、たろうはバニラが好き、はなこはストロベリーが好き」というように、一緒に見ることができる。


トレーニング終了!


⑩トレーニングの重みを保存するオプション

ここでは、実行されずにコメントアウトされているけど、トレーニングしたモデルを保存することができるよ。

torch.save(self.fourgrams, "namegen_weights.pt") 

トレーニングした重みを保存するメリットは、長時間かかるトレーニングプロセスを再度実行する必要がなくなることです。一度学習したモデルを保存しておくことで、将来的に同じモデルを使用したい場合にすぐに読み込んで利用できます。これにより、計算リソースと時間を節約でき、効率的にモデルを再利用することが可能になります。

つまり、時間がかかるような大きいモデルは保存しておいた方がいい。


今回はここまで。

次回は、4.名前の生成 (generate_namesメソッド)について解説します。



おまけ

トレーニングモデルを保存するのを忘れて発狂している人。


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