自分のブログ49記事でミニGPTを学習させたら、「位置エンコーディング」と「.envを呼ぶ」を書き出した
約8分で読めます

自分のブログ49記事でミニGPTを学習させたら、「位置エンコーディング」と「.envを呼ぶ」を書き出した


Self-Attentionを実装し、位置エンコーディングを実装し、手書きのMLPとAdamを拡散モデルGANFlowで何度も書いた。部品は全部揃っている。

組み立てたら、GPTになるはずだ。

というわけで、2層Transformer(d=128、4ヘッド)の文字レベルGPTをnumpyだけで実装し、**このブログ自身の全49記事(12万字)**で学習させた。自分の書いたものを食べさせたら、何を書き出すのか。

まず、実装が正しいことを確認する

Transformerの逆伝播は手で書くと間違えやすい。学習が進まないとき「アルゴリズムのせい」なのか「バグのせい」なのか分からなくなるので、先に数値微分と突き合わせた

Wq0 : 数値 -3.850818e-04  解析 -3.850818e-04  相対誤差 8.33e-09
W11 : 数値 -6.456322e-04  解析 -6.456322e-04  相対誤差 8.05e-09
Wout: 数値  1.003423e-03  解析  1.003423e-03  相対誤差 3.58e-08

パラメータを微小量ずらして損失の差から求めた勾配と、自分の書いた逆伝播が出す勾配が相対誤差1e-8で一致。これで「以降の結果はバグではなく学習の性質だ」と言える土台ができた。この確認に30秒かけるかどうかで、後のデバッグ時間が何時間も変わる。

生成文は、こう育った

学習中の6時点で、同じ「この実験では」という書き出しから続きを生成させた。

学習ステップごとの生成文。step 1は「校跨紫ユ像跡%蓄縮弱処傷任岸恒掛魚縞洗駆争」のような完全にランダムな漢字の羅列。step 200で助詞や句読点が正しい位置に現れ始める。step 600で「変わりにす」「考えまでは」など単語らしき断片が出る。step 1500で「低周波」「ノイズ」「次元」など技術用語が現れる。step 3000で「平面になった」「カスゲーナ」など文らしい構造になる。step 6000で「位置エンコーディング」「パーセプトロン学習則」「.envを呼ぶ」など、このブログ特有の語彙とmarkdown記法が再現される

段階がはっきり分かれていて面白かった。

  • step 1: 完全なランダム。1372種類の文字から一様に選んでいる
  • step 200: 助詞(「の」「で」「は」)と句読点が正しい位置に現れ始める。頻度の高い文字とその並びを先に覚える
  • step 600: 「変わりにす」「考えまでは」など、日本語の単語らしき断片
  • step 1500: 「低周波」「ノイズ」「次元」——技術用語が出てくる。このブログのコーパスを食べていることが分かる
  • step 3000: 文の構造ができる。「結果は見えなった(データはどちら…」
  • step 6000: 「位置エンコーディング」「パーセプトロン学習則」「.envを呼ぶ」。さらに ** (markdownの太字)や - (箇条書き)まで再現している

最後の生成文を全文で見ると、こうだ。

この実験ではなくなったの、位置エンコーディングアプ競約1パックセストロン学習則**。「.envを呼ぶの分布図」をアニメーションとしても、何もっきりしていない状態が認知りやすい相手が求めると、こうしてよく聞いた。 - **自分の考え方を計算し、入力させる

意味は全く通っていない。 でも「アニメーションとしても」「〜という状態が」「自分の考え方を」あたりの言い回しは、確かに自分が書きそうな文だ。7万パラメータ程度の小さなモデルが12万字から学べるのは、意味ではなく文体なのだと分かる。

損失の下がり方

損失曲線。初期値7.2(ランダムなら ln(1372)=7.22)から急速に下がり、6000ステップ後には2.5前後で安定する。図内注釈「完全なランダムなら ln(語彙1372) = 7.22」

損失は7.2から2.5へ。1372種類の文字から完全にランダムに選ぶと ln(1372)=7.22\ln(1372) = 7.22 なので、初期値はぴったり理論値だ(実装が正しい傍証にもなる)。最終的な2.5は、次の1文字の候補が実質12個くらいに絞れている状態を意味する。

attentionは何を見ていたか

学習後のモデルに「拡散モデルを実装した結果、」を読ませて、attentionの重みを覗いた。

3つのヘッドのattention行列。層1ヘッド1は対角線のすぐ下(直前の文字)に注目が集中。層2ヘッド1は文頭の「拡」「散」に強く注目。層2ヘッド3は「モデル」「実装」など2〜3文字前のまとまりに注目している

ヘッドごとに役割が分かれていた。

  • 層1ヘッド1: 対角線のすぐ下——つまり直前の1文字への注目。日本語では直前の文字が次の文字を強く決める(「拡」の次は「散」)ので、これは合理的
  • 層2ヘッド1: 文頭の「拡」「散」へ遠距離の注目。話題を保持する役割に見える
  • 層2ヘッド3: 2〜3文字前の「モデル」「実装」といったまとまりへの注目

誰も「そう分業しなさい」と指示していない。損失を下げるという1つの目的だけから、役割分担が自然に生まれたSelf-Attentionの記事で「学習済みの射影行列がないと『it』は『animal』に注目しない」と確かめたが、今回はその学習済みの射影行列を自分で作ったことになる。

温度を変えると性格が変わる

生成時の温度(確率分布の尖らせ方)を振ってみた。

  • 温度0.4(慎重): 「この実験では、「このデータ」の値を持ち上げるほど、ここまではなく…」——無難な言い回しに寄る
  • 温度0.8(標準): 「この抑えられた次元になっているがかりましています。…こここで人生の使うのかもののは、実際に自分のブログ」
  • 温度1.2(大胆): 「とも代わりにせる。1つの小法起きやすい 原因は絶対ればuけないメートを作り出して…」——語彙は豊かになるが崩壊も増える

ε-greedyの記事の探索率と同じ構図だ。安全に行くほど平凡になり、冒険するほど当たりも外れも増える。

手を動かして意外だったこと

一番面白かったのは、意味より先に文体を覚えることだった。step 6000のモデルは何も理解していない。それでも「アニメーションとしても」「という状態が」といった、自分の手癖のような言い回しを再現する。文体とは、意味を組み立てる能力とは独立した、文字の並び方の統計にすぎないのかもしれない。文章の個性だと思っていたものの結構な部分が、実は12万字から抽出できる表層のパターンだったわけだ。

もう1つは、この実験が過去記事の総決算になったこと。Self-Attentionも位置エンコーディングもAdamも、それぞれの記事では単体で動かしただけだった。それを全部繋いだら、(おもちゃとはいえ)言語モデルとして動いた。部品を1つずつ手で書いてきた蓄積が、最後に1つの動くものになる——1ヶ月半かけて積んだブロックが噛み合った瞬間で、この連載でいちばん嬉しかった実験だった。

まとめ

  • Self-Attention・位置エンコーディング・手書きMLP・Adamという過去記事の実装を全部組み立て、2層Transformer(d=128、4ヘッド)の文字レベルGPTをnumpyだけで構築
  • 数値微分との突き合わせで相対誤差1e-8を確認してから学習(実装の正しさを先に保証する)
  • このブログ自身の全49記事12万字で6000ステップ学習。損失7.22(理論上のランダム値)→2.5
  • 生成文の進化: ランダムな漢字 → 助詞と句読点 → 単語断片 → 技術用語 → 文構造 → ブログ特有の語彙とmarkdown記法
  • attentionはヘッドごとに「直前の文字」「文頭への遠距離注目」「2〜3文字前のまとまり」と自然に役割分担していた
  • 意味は最後まで通らない。それでも文体は再現される——文体の相当部分は、意味とは独立した文字並びの統計である