はじめに
この記事では、ミニマルな GPT 実装(microGPT)を題材に、
- ひらがな名前データでの学習
- Weights & Biases(W&B)による学習曲線の可視化
- 学習済みモデルを使った名前生成
までを一通り回し、その実行結果から見えてきたことをまとめます。
コードは、以下の 2 本を前提にしています。
- 学習用:
microgpt_train.py - 生成用:
microgpt_generate.py
どちらも外部フレームワーク(PyTorch 等)には依存せず、標準ライブラリと W&B だけで動く「自作 Transformer」です。
※また 2WINS では隔週で社内勉強会を開催しており、本稿は第15回勉強会「TransformerとGPTの仕組み」で扱った内容をベースに、GPT の内部構造理解を目的として microgpt を題材に学習した内容を発展させたものです。
1.1 実行方法のおさらい
まずは、このリポジトリでよく使う実行コマンドを整理します。
# 学習(デフォルト設定でモデルを学習)
uv run microgpt_train.py
# ハイパーパラメータを変えて学習
# 埋め込み次元とヘッド数を増やして、少し大きめのモデルを試す
uv run microgpt_train.py --n_embd 64 --n_head 8
# 学習ステップ数と学習率を変えて、収束の仕方の違いを観察する
uv run microgpt_train.py --num_steps 2000 --lr 0.005
# 生成(学習済みの最新の重みからランダムに名前を生成)
uv run microgpt_generate.py
# 「あ」から始まる名前だけに絞って 10 個生成する
uv run microgpt_generate.py --start あ -n 10
# temperature を上げて、より多様でランダム性の高い名前を生成する
uv run microgpt_generate.py --temperature 0.8
この記事では、この 3 パターンの学習条件と、対応する生成結果を見ながら考察していきます。また、ここでmicrogpt_train.py,microgpt_generate.pyを掲示しておきます。
1.2 環境構築
W&B 公式だと、まずはこのあたりを見るのがおすすめです。
W&B クイックスタート(インストール〜最初の実行)
https://docs.wandb.ai/ja/models/quickstart
→ pip install wandb、wandb login、wandb.init() の基本的な使い方がまとまっています。
ガイド一覧ページ(導入ガイドへの入口)
https://docs.wandb.ai/ja/guides
→ 「W&B のインストール方法と W&B をコードに追加する方法を学ぶために、quickstart を試してみてください」と案内されているページです。
もし APIキーを環境変数で持たせたい / サーバ環境で使いたい 場合は、以下も公式ドキュメントです。
環境変数の設定(W&B SDK 用)
https://docs.wandb.ai/ja/models/track/environment-variables
→ WANDB_API_KEY などの環境変数一覧と設定方法が載っています。
2. 学習条件と W&B の設定
デフォルト設定
学習スクリプトのデフォルト値は次のとおりです。
- 層数:
n_layer = 1 - 埋め込み次元:
n_embd = 48 - ヘッド数:
n_head = 4 - 最大コンテキスト長:
block_size = 16 - 学習ステップ数:
num_steps = 1000 - 学習率:
lr = 0.01 - データセット:ひらがな名前(約 220 件)
W&B には、以下のように run を記録しています。
- project 名:
workshop-microgpt - run 名:
jp-names_L{n_layer}_E{n_embd}_H{n_head} - config:
-
n_layer,n_embd,n_head,head_dim -
block_size,vocab_size -
learning_rate,num_steps -
dataset,num_docsなど
-
学習ループ内では、各 step ごとに次の指標をログしています。
-
train/loss:クロスエントロピー損失 -
train/perplexity:exp(loss)として計算したパープレキシティ -
lr:線形減衰させた学習率
3. W&B 上の学習曲線(3 条件比較)
今回は次の 3 条件で学習を回し、それぞれの train/loss と train/perplexity の曲線を比較しました。
-
デフォルト設定
uv run microgpt_train.pyn_layer=1, n_embd=48, n_head=4, num_steps=1000, lr=0.01
-
モデルを大きくした設定
uv run microgpt_train.py --n_embd 64 --n_head 8n_layer=1, n_embd=64, n_head=8, num_steps=1000, lr=0.01
-
step 数と学習率を変えた設定
uv run microgpt_train.py --num_steps 2000 --lr 0.005n_layer=1, n_embd=48, n_head=4, num_steps=2000, lr=0.005
W&B 上では、それぞれ色を変えてプロットしました(ここでは便宜上、以下のように呼びます)。
- 緑:ステップ数 2000、学習率 0.005
- 赤:埋め込み次元 64 / ヘッド数 8
- 青:デフォルト設定(48 次元 / 4 ヘッド)
3.1 共通して言えること
| 観察 | 原因 |
|---|---|
| train/loss は全体的に右下がり | 学習が進行している |
| ステップごとにギザギザしている | データが 1 件ずつの小規模セットであり、シャッフルにより「簡単/難しい」サンプルが偏ってくるため |
これらより、学習自体はうまく進んでいると言えます。
一方で、グラフはどれもかなりギザギザしています。 これは、
- データが名前 1 件ずつの小規模セット
- 各 step で 1 サンプル単位の loss をそのまま記録
-
docsをシャッフルしながら学習
しているため、step ごとに「たまたま難しいサンプル」「簡単なサンプル」が混ざりやすく、そのノイズが loss / perplexity に強く乗っているためです。ここでの「簡単/難しい」は、理論的な定義ではなく、今回の名前データに対して loss が下がりやすいかどうか、という実験的な感覚に基づいた分類です。少し補足説明しておきます。
| 分類 | 特徴 | 具体例 | loss への影響 |
|---|---|---|---|
| 簡単 | 頻出パターン。次トークンがほぼ 1 通りに絞れる | あや→か, しょう→す/た | 小さくなりやすい |
| 難しい | 希少・分散パターン。次候補が多いか長さが外れ値 | あ→きこ/や/い, 6〜7文字の名前 | 跳ね上がりやすい |
こういう名前は、モデルから見ると「次の文字を当てづらい」ので、その step の loss が跳ね上がりやすくなります。下図をイメージすると分かりやすいかもしれません。
3.2 モデルを大きくした場合(赤)
--n_embd 64 --n_head 8 でモデルを少し大きくした run は、デフォルトと比べて loss / perplexity の曲線の形が大きく変わる、ということはありませんでした。
| 条件 | 学習曲線の特徴 | 示唆 |
|---|---|---|
| 赤:モデル拡大 | デフォルトと大差なし。最終 loss/perplexity も同等 | 小規模データ+短い学習では、わずかな容量増加だけでは決定的な差が出にくい |
| 緑:長め+低 LR | 1000 step 以降も継続的に低下し、最終値が最も低い | モデル容量よりも「学習率+ステップ数」の調整が、小規模設定では効きやすい |
つまり今回の規模(データ 220 件・step 数 1000)では、
「モデルを少し大きくする」こと自体は、短い学習予算の範囲では決定的な差にはならなかった
と言えそうです。
もちろん、もっと長く回したり、別の初期値・シードを使ったりすれば差が出る可能性はありますが、「手軽に 1 回ずつ回した程度」の比較では、モデルサイズ増加のインパクトはあまり見られませんでした。
3.3 step 数と学習率を変えた場合(緑)
--num_steps 2000 --lr 0.005 の run は、
-
num_stepsを倍(1000 → 2000) -
lrを半分(0.01 → 0.005)
にした条件です。
この run の学習曲線をみると:
- 初期のスパイクはあるものの、1000 step 以降も loss / perplexity がさらに下がり続ける
- 最終的な loss / perplexity は、他の 2 条件よりも明らかに低い位置に落ち着く
という挙動を示しました。
ここから分かることは、
今回の microGPT 設定では、「学習率をやや抑えつつ、step 数を増やして長めに回す」方が、短時間で大きなモデルを試すよりも収束の改善がはっきり見えた
ということです。
この結果は、特に小規模データ+シンプルなモデルでは、
- モデル容量をむやみに増やすより、
- optimizer 側(lr やスケジュール)と学習時間の調整の方が効きやすい
という、典型的な傾向とも一致しています。
4. 名前生成の結果
学習済みの重み L1_E48_H4_S2000.pkl(1 層・48 次元・4 ヘッド・2000 step 学習)を使って、実際に名前を生成してみました。
4.1 「あ」から始まる名前を 10 個生成
uv run microgpt_generate.py --start あ -n 10
出力:
重み: L1_E48_H4_S2000.pkl (vocab=60, embd=48, head=4)
--- 「あ」から始まる名前を 10 個生成 ---
1: あお
2: あきこ
3: あみ
4: あや
5: あきら
6: あいな
7: あきこ
8: あおり
9: あきと
10: あみ
- 「あきこ」「あきら」「あきと」「あや」「あみ」など、日本語として自然な名前が多数生成されています。
- 「あお」「あおり」など、既存の名前としても見かける形が出ている一方で、完全には見慣れない形も混じるあたりが、いい意味でのランダム性です。
- 同じ名前(例:
あきこ,あみ)が複数回出ているのは、学習データ中の頻度や、モデルが覚えたパターンの「確率の高さ」を反映していると考えられます。
4.2 完全ランダムに 20 個生成
uv run microgpt_generate.py
出力:
重み: L1_E48_H4_S2000.pkl (vocab=60, embd=48, head=4)
--- ランダムに名前を 20 個生成 ---
1: あおり
2: あい
3: けんと
4: あきと
5: みどり
6: ゆうた
7: まさひろ
8: かずき
9: えみ
10: あやか
11: あみ
12: まさこ
13: まさひろ
14: しずひ
15: ゆづき
16: しょうすけ
17: あお
18: ゆうか
19: そうすけ
20: えい
ここでも、
- 「けんと」「ゆうた」「かずき」「あやか」「ゆづき」「しょうすけ」「そうすけ」など、よくある名前が多く出力されている
- 「しずひ」「えい」など、若干不思議な形も混ざっている
という結果になりました。
「完全にデタラメ」というよりも、「聞いたことのある名前と、ギリギリ名前っぽいものの中間」に収まっているのが分かります。
これは、モデルが「ひらがな列としてのパターン」を学習していることの良い証拠です。
4.3 temperature を 0.8 に上げて 20 個生成
uv run microgpt_generate.py --temperature 0.8
出力:
重み: L1_E48_H4_S2000.pkl (vocab=60, embd=48, head=4)
--- ランダムに名前を 20 個生成 ---
1: しのぶ
2: あやか
3: しんじ
4: なつき
5: かなで
6: えな
7: まゆ
8: ゆめ
9: さちこ
10: たくみ
11: ゆめ
12: かずお
13: いつき
14: くみ
15: まなみ
16: なおゆき
17: あい
18: たくみ
19: さちこ
20: ゆうせい
ちなみに、ここで使っている temperatureとは、ChatGPT や各種 API で見かけるものと同じで、「確率分布をどれだけ尖らせる/広げるか」のパラメータです。このパラメータを出力のランダム性と多様性を調整する際に使います。0~1か2の範囲で指定でき、temperatureの値が高くなればなるほどランダム性の高い表現が生成され、低いほどより正確な解答が返ってきます。下図でイメージすると分かりやすいかもしれません。
引用:https://www.mdrk.io/temperature-samplig-in-ai/ Temperature Sampling and Scaling in AI and LLMs
この図では、LLM が次のトークンを選ぶ状況を、アイスクリームのフレーバー選択に見立てて可視化しています。「Vanilla」「Strawberry」などの各フレーバーが 1つのトークン候補 に対応しており、縦軸の値はそのトークンが次に選ばれる確率を表しています。実際の LLM では語彙数が数万〜数十万に及びますが、ここでは概念の理解を優先し、候補を少数に絞った簡略モデルを用いています。
話を戻して、temperature を 0.8 にすると
- 「しのぶ」「しんじ」「なつき」「かなで」「ゆめ」「さちこ」「たくみ」「いつき」「まなみ」「なおゆき」「ゆうせい」など、現実にありそうな名前のバリエーションが一気に増えました。
- 一方で、破綻した文字列(明らかに名前になっていないもの)はほとんど出ていません。
今回の例の範囲では、
-
temperature = 0.5(デフォルト)
→ わりと「安定寄り」で、よく見る名前に寄りがち -
temperature = 0.8
→ 少しランダム性が増しつつ、まだ「名前として十分読める」範囲に収まっている
という印象を受けました。
5. 学習曲線と生成結果から分かること
今回の 3 条件の学習と生成結果から、ざっくり次のようなことが言えます。
-
microGPT でも、ひらがな名前のパターンはしっかり学習できる
- loss / perplexity は右下がりで、学習が進んでいる
- 生成される名前も、人間が読んで「名前だ」と感じるレベルになっている
-
モデルを少し大きくしても、短い学習では劇的な差は出なかった
-
n_embd=64, n_head=8の run は、n_embd=48, n_head=4と比べて学習曲線が大きく改善したとは言いにくい - 小規模データ+短い学習では、「モデルを大きくするだけ」では性能差が出にくいケースを確認できた
-
-
学習率と step 数の調整は効きやすい
-
num_steps=2000, lr=0.005の run は、他の条件より最終 loss / perplexity が低くなった - 生成結果も自然さ・多様性ともに良好で、「学習を少し長めに回し、学習率を抑える」ことの効果が見て取れた
-
-
temperature で「名前の攻め方」をコントロールできる
- 低め(0.5)では無難でよくある名前が多く、高め(0.8)ではやや攻めた名前が増える
- どちらも破綻しない範囲なので、好みや用途に応じて変えられる
6. おわりに
microGPT のような小さな実装でも、W&B を使って loss / perplexity を可視化し、ハイパーパラメータ(モデルサイズ・学習率・step 数)を変え、生成結果を見ながら手触りを確認するという一連の「実験サイクル」を回すことで、「モデルをどういじると学習と生成がどう変わるか」 をかなり感覚的に掴むことができます。
本番用の巨大モデルとはスケールが違いますが、
- 自動微分
- Transformer ブロック
- 次トークン予測
- 学習率スケジュール
- temperature によるサンプリング制御
といった、LLM の核になる要素はすべて同じです。
大きなモデルを触る前に、こういったミニマル実装で「学習と推論のフロー」を体験しておくと、後からフレームワークのコードを読んだときにも理解がスムーズになるはずです。




