一字一诗
LESSON 17 / 26

控制学习的节奏与规模

这一课完成什么

学会调整训练设置,并知道每次改动在控制什么。一次只改一个变量,保留原配置和验证题,才容易判断变化来自哪里。本课先解释四个最常用设置:每批数量、调整幅度、训练步数、模型大小。

学习率安排,是让调整幅度随时间变化

先逐步增大到设定值,再缓慢减小。观察速度时也要留意第一次计算的准备开销,不能只拿第 1 步推算全程。

学习率安排,是让调整幅度随时间变化
用文字逐步读这张图
  1. 起点:第 1 次更新;调整幅度较小
  2. 预热:逐渐接近设定值;本实现最多 100 步
  3. 中段:逐步减小;变化保持连续
  4. 末段:最低为设定值的 15%;不会归零
  5. 延长总目标:后续计划也改变;记下改动再比较
  6. 检查依据:固定验证误差;不是只看当前一批
为什么恢复到更长目标后曲线可能反弹?

总目标影响调整幅度的计划。延长计划会改变后续更新,不能把两段当作完全相同设置。

每批数量与训练步数

batch_size 表示一次更新同时看多少首诗。默认完整配置为 48,6000 步共抽取 288,000 次诗文练习,但其中可能有重复抽样。它不表示见到了 288,000 首不同作品。每一首的关键词条件也会按固定随机序列变化。

批数量变大通常增加中间结果与内存占用;相同步数下也会处理更多材料,比较时应说明究竟固定了步数还是总练习量。内存不足时降低批数量是一项直接的办法,但不能把改后的曲线不加说明地当成同一个实验。

学习率决定每次改动的尺度

第 7 课的调整幅度在正式训练中叫学习率 learning_rate。完整配置峰值为 0.0006。它不是“学会的比例”,也不是准确率。过大可能不稳定,过小可能学得很慢。

本项目先用一小段预热,从较小幅度逐步升到峰值;后面再缓慢降低,末尾保留峰值的 15%。这种安排叫学习率计划。具体时间由总目标步数决定,不是另一个会训练的参数。

bash
./poet lesson 17

实验使用 1000 步的教学计划,观察第 1、10、100、300、600、1000 步的数值。它用于看形状,与正式 6000 步计划的后半段数值不同。

怎样做一项可比较的实验

bash
cp configs/tiny.json configs/my-tiny.json

打开副本,只把 learning_rate 改为原来的一半。分别在不同目录训练同样步数,使用同一份材料、同一随机种子和同一验证方式。

bash
./poet train --config configs/tiny.json --run-dir artifacts/runs/rate-a --steps 300
./poet train --config configs/my-tiny.json --run-dir artifacts/runs/rate-b --steps 300

依次运行这两个命令,记录验证误差、耗时和生成示例。300 步用于演示比较方法,并不保证足以看出最终质量差异。不要只挑某一批最低的学习误差作结论。

模型大小与内存

tiny 配置约 145 万参数,完整配置约 488 万参数。实测完整训练使用 M1 Pro 的图形处理器,但内存数字需区分:进程常驻内存与 MPS 驱动分配不是同一个指标,不能简单相加当作全机峰值。训练报告会分别列出。

若想租机器,可以沿用代码和材料,在支持的 NVIDIA 环境选择 --device cuda。先在本机完成短跑、确认大致工作量,再按第 26 课迁移。换设备会影响速度,也可能影响最后几位数值与随机轨迹。

验收与排错

真实曲线中,为什么会有反弹

下面将正式训练的全部 61 条基线与验证记录连起来,没有把波动删掉。先读横轴的步数,再读纵轴;右图放大了后半段,所以看起来波动更明显。

6000 步后我们把总目标延长到 18000 步,后续学习率计划随之改变。曲线随后短暂上升,再继续下降。它提示你要把误差和“改过什么设置”一起记录,不能只凭一次上升就判断程序坏了。

图 21 / 数据图按真实日志绘图
完整训练误差曲线:包括中间的波动

完整训练误差曲线:包括中间的波动

先看哪里
横轴是更新次数;纵轴是固定验证题的平均误差。右图放大后半段。
这说明什么
曲线直接使用 61 条基线及验证记录,没有平滑。6000 步后恢复并延长训练,调整幅度计划改变,随后出现短暂反弹,再继续下降。
你接着做
先比较同一口径的曲线;再看第 19 课另外保存的最终评估,不能把不同抽样的数字混在一起。

打开原图,放大阅读

查看来源

artifacts/runs/poet/metrics.jsonl

小练习与答案

把批数量翻倍、步数不变,然后说“模型更好完全因为大批训练”,结论充分吗?

查看答案

不充分,因为总练习量也翻倍了。需要区分是在固定更新次数、固定抽样数量还是固定时间下比较,并记录其他设置。课程对照实验至少应保存配置、材料摘要、运行设备、耗时和固定验证结果。

动手补全一小段

先运行上面的完整实验,再复制本课起始文件为自己的练习。starter 有意留空 solve 函数;补全后运行文件,底部检查会告诉你是否符合本课要求。卡住时打开参考答案,比较每一步。

bash
cp lessons/17/starter.py lessons/17/my_exercise.py
# 编辑 my_exercise.py 中的 solve 函数,然后运行:
.venv/bin/python lessons/17/my_exercise.py
# 对照完整答案:
.venv/bin/python lessons/17/solution.py

下载起始代码 · 下载参考答案

展开本课补全练习的完整参考答案
python
"""第 17 课补全练习:实现预热与逐渐减小。修改 solve,保持下方检查不变。"""
import math, io, argparse
import torch
from torch import nn
from torch.nn import functional as F
torch.set_num_threads(2)
torch.manual_seed(26)

def solve(step, total, warmup):
    if step < warmup: return step/warmup
    progress=(step-warmup)/max(1,total-warmup)
    return .15+.85*.5*(1+math.cos(math.pi*progress))

assert abs(solve(1,1000,100)-.01)<1e-9
assert abs(solve(100,1000,100)-1)<1e-9
assert abs(solve(1000,1000,100)-.15)<1e-9
print("本课补全练习通过。")
展开本课完整、可独立运行的实验代码
python
"""本课独立实验;在仓库根目录执行 .venv/bin/python lessons/17/experiment.py。"""
import sys, json, math
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
import torch
from torch import nn
from torch.nn import functional as F
from poetry_gpt.common import ROOT, DEFAULT_DATA, read_json, read_jsonl
from poetry_gpt.model import ModelConfig, PoetryGPT, CausalAttention, Block
from poetry_gpt.data import Tokenizer, SPECIAL, clean_record, keywords_for
from poetry_gpt.labs import show
torch.set_num_threads(2)
torch.manual_seed(26)

for step in [1, 10, 100, 300, 600, 1000]:
    progress = max(0, step - 100) / 900
    factor = step / 100 if step < 100 else 0.15 + 0.85 * 0.5 * (1 + math.cos(math.pi * progress))
    show(f'第{step}步调整幅度', round(0.0006 * factor, 8))
下载本课实验