モジュール 09 · 第 5 回

学習させる:モデルに唐詩を書かせる

3 万 4 千首の唐詩で前の課の GPT を学習させ、ノートパソコンの CPU で 7 分動かして、でたらめな文字から一歩ずつ五言や七言を身につけていく様子を見ます。さらに 300 首だけを与え、詩の作り方を学ぶのではなく詩を丸暗記してしまう様子を見ます。

  • 約 50 分
  • 難易度:上級
  • 検証:2026-09-15 torch 2.14、Apple M4 CPU、乱数シード固定

コードと実行結果は実際に動かしたときのまま載せているため、コメントと出力は中国語です。

モデルは組み上がったので、今度は学習させます。この課のコード train.py は百数十行だけで、構造はモジュール 08 の学習ループと同じです。データを一束取り、損失を計算し、逆伝播し、パラメータを更新する。

python train.py

データ:一文字ずらす

言語モデルの学習データには、人手によるラベル付けが要りません。文章そのものが問題であり答えでもあるのです。

def get_batch(ids, generator=None):
    """随机截取 BATCH 段长度为 block_size 的文字。目标 y 就是 x 往后错一位:每个位置都要预测下一个字。"""
    starts = torch.randint(len(ids) - cfg.block_size - 1, (BATCH,), generator=generator)
    x = torch.stack([ids[s:s + cfg.block_size] for s in starts])
    y = torch.stack([ids[s + 1:s + cfg.block_size + 1] for s in starts])
    return x, y

すべての詩を改行でつないで一本の長い文字列にし、ランダムに 32 か所を切り出して、それぞれ 128 文字を入力 x にします。それぞれを一文字後ろにずらしたものが、答え y です。たとえば入力が「白日依山尽,」なら、答えは「日依山尽,黄」です。1 番目の位置は「白」を見て「日」を当て、2 番目の位置は「白日」を見て「依」を当てる。前の課で言ったとおり、因果マスクがあるので、どの位置も答えを覗けません。

改行はここで特別な意味を持ちます。一首が終わり、次の一首が始まることを表しているのです。モデルは「句点の後には改行が来る」ことを学び、生成のときは改行が出たら止めればよく、それで一首が書き終わります。

最後の 1000 首は検証セットとして取っておき、学習中は見せません。そうすれば、モジュール 08 第 6 課の方法で、モデルが学んでいるのか暗唱しているのかを確かめられます。

训练集 34135 首诗,1547255 个词元;验证集 1000 首;词表 6289 个字符
模型:4 层,4 个头,向量维度 128,共 1,614,720 个参数

学習ループのいくつかの細部

optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.1)
for step in range(1, args.steps + 1):
    for group in optimizer.param_groups:
        group["lr"] = lr_at(step - 1)
    x, y = get_batch(train_ids)
    _, loss = model(x, y)
    optimizer.zero_grad()
    loss.backward()
    torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)  # 梯度裁剪:防止偶尔一步梯度太大把训练带飞
    optimizer.step()

モジュール 08 と比べて、二つのものが増えています。

学習率が変化する。最初の 100 ステップで 0 から 0.001 までゆっくり上げます。これをウォームアップと呼びます。学習を始めたばかりのときはパラメータがランダムで、勾配の向きが当てにならないので、歩幅が大きすぎると道を外れやすいのです。その後、学習率はコサイン曲線に沿って 0.0001 までゆっくり下がります。学習の後半は歩幅を小さくして、損失の低いところをより細かく探せるようにします。

def lr_at(step, peak=1e-3, warmup=100):
    """学习率:前 100 步从 0 慢慢升上去(预热),然后按余弦曲线降到峰值的十分之一。"""
    if step < warmup:
        return peak * (step + 1) / warmup
    progress = (step - warmup) / max(1, args.steps - warmup)
    return peak * (0.1 + 0.9 * 0.5 * (1 + math.cos(math.pi * progress)))

勾配クリッピング。あるステップで計算された勾配が特に大きかったら(特殊なデータの束に当たったとき)、それを比例的に縮めて、全体の大きさが 1 を超えないようにします。モジュール 08 第 2 課で見たとおり、一歩踏み出しすぎると、学習がそのまま発散してしまうことがあります。

「ウォームアップ + コサイン減衰 + 勾配クリッピング + AdamW」は Transformer の学習で最もよく使われる組み合わせで、LLM の学習も基本的にはこうで、具体的な数値が違うだけです。

詩を書けるようになる様子を見る

3000 ステップ学習させ、一定の間隔ごとに損失を見て、詩を 2 首書かせます。

训练前:训练损失 8.779,验证损失 8.778(随便猜的话是 ln(6289) = 8.747)
  训练前随便写的: 耦呌同捐睢寮办猬囊斵樬劫鞑顷饼遁瞩赴冈譀嗾雅踯牧写禧氛迹樯毁尤平螵蔻郓祐菼灺嵊匦

第 300 步(27 秒):训练损失 5.778,验证损失 5.818
  将南。
  门不独。到李斜,功觅青。谁将风花不可碧,不和山。

第 1000 步(97 秒):训练损失 4.777,验证损失 4.879
  山里怅望两间道,千里长亭寺断肠。曾梳白兰草,试向玉关鱼。
  此日醉前年,相思高至兹。云如汉陵子,暮水九重宫。

第 2000 步(249 秒):训练损失 4.410,验证损失 4.583
  秋江野鸟过高楼,野树猿声怨见人。处处秋风满孤照,沧海无端行处闻。
  春山度滟月,多少漫为秋。药罢已开葬,松阴不道开。

第 3000 步(407 秒):训练损失 4.280,验证损失 4.488
  江山古馆响幽幽,山鸟无人见白头。此时猩猩争得语,又将杯酒醉参差。
  分明人在泪,明月更经过。小谷闲烟树,红潭古石床。归来扶白首,立向卧青山。独有安行处,如何却得还。

训练用了 408 秒,模型存到 .cache/gpt.pt

私のパソコン(Apple M4、CPU のみ)では、3000 ステップに 7 分もかかりませんでした。1 ステップで 32×128 = 4096 文字を処理するので、3000 ステップで約 1200 万文字を見たことになり、学習セットをおよそ 8 周したのに相当します。

身につけたことは段階的に現れます。

  • 学習前:ランダムな文字の並びで、損失は 8.78、でたらめに当てているだけです。
  • 300 ステップ:句読点を使えるようになり、書くのは常用字ばかりですが、句の長さはばらばらです。
  • 1000 ステップ:ほとんどの句が 5 文字か 7 文字で、読点と句点が交互に出てきます。しかし一首目は前の 2 句が七言、後の 2 句が五言で、一首の中でそろえるべきだとまだわかっていません。
  • 2000 ステップ:形式はほぼ正しくなり、「秋江野鸟过高楼,野树猿声怨见人」(秋の川で野鳥は高楼を過ぎ、野の木で猿の声は人を見て恨む)のような、イメージのつながった句が出始めます。
  • 3000 ステップ:一首目は完全な七言絶句、「江山古馆响幽幽,山鸟无人见白头」(川と山、古い館はかすかに響き、山の鳥、白髪を見る人はない)です。二首目は 8 句そろった完全な五言律詩です。

身につけていないこともはっきりしています。意味がよくつながらず(「此时猩猩争得语」、このときオランウータンが競って話す)、平仄も韻もかまっていません。パラメータ 161 万個、学習 7 分で、ここまでできれば上出来です。

学習の過程全体を通して、学習の損失も検証の損失も下がり続け、しかもずっと近い値でした(最後は 4.28 と 4.49)。モジュール 08 第 6 課の考え方に従えば、目立った過学習はないということです。見たことのない詩でも、ほぼ同じくらいよくできています。

詩を作っているのか、暗唱しているのか

このコースのこれまでのモジュールで繰り返し言ってきたとおり、生成したものがよく見えても、モデルが本当に身につけたとは限らず、学習データを丸暗記しただけかもしれません。そこで学習の後、スクリプトはチェックを一つします。詩を 100 首生成し、学習セットのどれかの句とまったく同じ句がいくつあるかを見るのです。

train_sentences = {s for p in train_poems for s in p.replace("。", ",").split(",") if s}
torch.manual_seed(1)
generated = sample(100)
sentences = [s for p in generated for s in p.replace("。", ",").split(",") if s]
copied = sum(s in train_sentences for s in sentences)
生成 100 首诗,共 576 句,其中 0 句(0%)和训练集里的某一句一模一样
整首和训练集里某一首一模一样的:0 首

576 句のうち、写したものは一句もありません。学んだのは唐詩の「書き方」です。形式、よく使う字や語、どの字がよく一緒に現れるか。そしてそれらを組み合わせて新しい句を作っています。

300 首だけを与える

モジュール 08 第 6 課で、データが少なすぎるとモデルは暗唱すると言いました。今度は同じモデルに、300 首だけを与えて試してみます。

python train.py --poems 300 --steps 1500 --out small.pt
训练集 300 首诗,13612 个词元;验证集 1000 首;词表 6289 个字符

第 150 步(22 秒):训练损失 5.079,验证损失 6.584
第 500 步(70 秒):训练损失 0.421,验证损失 8.864
第 1000 步(145 秒):训练损失 0.103,验证损失 9.817
  雕鹗途程在碧天,彩衣东去复何言。二千宾客旧知己,十二山河新故园。吟看桂生溪月上,醉听鲲化海涛翻。好期圣代重相见,莫学袁生老竹轩。
第 1500 步(230 秒):训练损失 0.071,验证损失 9.984

生成 100 首诗,共 593 句,其中 454 句(77%)和训练集里的某一句一模一样
整首和训练集里某一首一模一样的:40 首

学習の損失は 0.07 まで下がり、全データで学習したときの 4.28 よりずっと低くなりました。ところが検証の損失は 9.98 まで上がり続け、学習前のでたらめな当て推量の 8.78 よりも高くなっています。

1000 ステップ目に書いたあの七言律詩はきれいに整っていますが、それは学習セットから一字違わず暗唱したものだからです。最後のチェックもそれを裏付けています。77% の句が写したもので、100 首のうち 40 首は一首丸ごと写していました。

161 万個のパラメータに対して学ぶべき文字は 1.3 万しかなく、モデルにはそれを全部覚える力が十分にあります。覚えてしまうのが、学習の損失を下げる最も手っ取り早い方法です。見たことのない詩に対しては、何も学んでいないときよりかえって悪くなります。暗唱した内容に「自信」を持ちすぎていて、ほかの詩に出会うと、正解に与える確率が哀れなほど低いのです。

同じモデル、同じコードで、データが 3.4 万首から 300 首に変わっただけで、結果は「詩の書き方を身につけた」から「詩を暗唱した」に変わりました。これはモジュール 08 第 6 課の結論が言語モデルで再現されたもので、LLM に膨大なデータが必要な理由でもあります。

損失 4.49 は何を意味するか

最後の検証の損失は 4.49 です。交差エントロピーは -log(正しい文字の確率) なので、平均すると、モデルが正しい次の文字に与える確率は e の -4.49 乗、約 1.1% です。

とても低く聞こえますが、詩を書くときには、次の文字にはもともと多くの妥当な選択肢があることを知っておいてください。「白日依山尽,黄河入海」(白い日は山に寄り添って沈み、黄河は海に入って)の後に、モデルは「无」「间」「多」「深」のどれにもある程度の確率を与えています(具体的な数字は次の課で見ます)。原詩は「流」(流れる)ですが、ほかの字にしたからといって間違いとは限りません。損失が 0 まで下がることはありえず、これはモジュール 08 第 1 課の「データにはランダムな揺らぎがあるので、損失は決して 0 まで下がらない」と同じ理屈です。

一つの物差しは、でたらめに当てる場合と比べることです。でたらめに当てるとは 6289 文字から均等に選ぶことで、損失は 8.75 です。4.49 は、選択の範囲を 6289 文字から e の 4.49 乗、約 89 文字にまで絞り込んだのに相当します。

練習問題

  1. --steps を 6000 に変えてください。損失はさらに下がり続けますか。生成した詩はよくなりましたか。
  2. 学習させる詩をそれぞれ 1000 首、3000 首、10000 首にして(--poems)、それぞれ 1500 ステップ学習させ、検証の損失と「写した句」の割合を記録して、表にしてください。
  3. 学習率のウォームアップと減衰を取り除いて(lr_at が常に 0.001 を返すようにする)、同じく 3000 ステップ学習させ、最後の検証の損失を比べてください。

確認テスト

1. 言語モデルを学習させるとき、入力と答えはそれぞれ何ですか?

文章から一区間を切り出して入力にし、それを一文字後ろにずらしたものを答えにします。各位置は、自分と前の文字をもとに次の文字を予測しなければなりません。128 文字の文章は、予測の問題を同時に 128 問提供します。

2. 学習率のウォームアップと勾配クリッピングは、それぞれ何の問題を解決するためのものですか?

ウォームアップ:学習を始めたばかりのときはパラメータがランダムで勾配の向きが当てにならず、最初から大きな学習率を使うと道を外れやすいので、まず小さな学習率を使い、ゆっくり上げていきます。勾配クリッピング:ときどきあるステップの勾配が特に大きくなり、それでそのまま更新するとパラメータが遠くへ飛びすぎて学習が発散するかもしれないので、大きすぎる勾配を比例的に縮めます。

3. 300 首だけで学習させたとき、学習の損失はとても低いのに、検証の損失がでたらめに当てるよりも高くなったのはなぜですか?

モデルのパラメータは 300 首すべてを暗記するのに十分な数があり、そのため学習の損失はとても低くなります。しかし学んだのはこの 300 首そのもので、詩を作る規則ではありません。暗唱した内容にとても自信を持っているので、見たことのない詩に出会うと、正解に与える確率がとても低く、交差エントロピーはでたらめに当てるよりも高くなるのです。生成した詩の 77% の句が写したものだったことも、これを裏付けています。

質問と議論

このレッスンでつまずいたところは、ここで質問してください。他の人の質問に答えるのも歓迎です。

質問で 3 ポイント、回答で 6 ポイント。審査を通過すると公開されます。

議論を読み込んでいます…