2.8 应用导读:Lab 1 的音乐生成
前面七节把序列建模的技术史走完了:递推给出与长度无关的参数共享,代价是沿时间的连乘雅可比(2.2);门控用加法通道把长程依赖推到上百步(2.3);注意力不再压缩历史,把任意两点的路径长度压到 (2.4),Transformer 把它堆成主干(2.5);位置编码与高效注意力处理顺序与平方复杂度(2.6),状态空间、线性注意力与稀疏专家给出三条现代替代路线(2.7)。但结论落到能跑的代码上,中间隔着几件极具体的工程事:数据怎么变成 token、损失怎么「错位一位」、生成时怎么从分布里取出一个 token、结果不好看时先怀疑什么。
这一节就是那座桥:它不引入新理论,而是把 Lab 1 的第二部分(字符级 RNN 音乐生成)拆成可逐步对照的动作,并说清每一步「对应讲里哪一条结论、写错了会看到什么现象」。例题选音乐,是因为它把自回归任务的骨架暴露得最干净:旋律是一串离散符号,既有很强的局部语法(音程、节奏),又有真实的长程结构(主题再现、段落反复),而数据只有几 KB——小数据的好处是「没学会」的现象非常直白,你不会把它错怪在算力上。读法建议:先按 Lab 1 跑起来,再回来对照小节;2.8.8 给出「理论 → 代码」的总表。
2.8.1 任务定义:从旋律到 token 序列
符号音乐(symbolic music)指以离散事件而非波形描述的音乐。两种常见表示:
- ABC 记谱的字符流:一个音符写成一到几个 ASCII 字符(音名
C D E F G A B、升 / 降号^_、时值/2/4、小节线|)。做字符级建模时,一个字符 = 一个 token,词表 就是出现过的字符集合,整段旋律是一条字符序列。 - MIDI 的事件序列:一串带时刻的事件(
note_on/note_off、音高 0–127、力度、通道)。喂给序列模型前通常先把时间离散化(如每 拍一格),再表示成「音高 + 时长组合的事件」序列。
形式化地,给定 token 序列 (),自回归模型建模的是联合分布
即把「整段旋律的概率」分解为 个「给定前缀预测下一个符号」的条件概率之积。这与文本语言模型的对象逐字相同:两者都是离散符号序列,因此同一个递推 / 注意力结构、同一套交叉熵损失可以直接搬过来——差别只在词表从哪来。
音乐在两种尺度上都有规律,而且都能被耳朵快速验证:
- 局部语法(几拍之内):音程倾向(大跳之后常反向级进)、惯用的时值组合(附点、切分)、调内音级分布。这类规律统计上很强,几百个 token 就能学到,学到与否的证据是续写「不走音」。
- 长程结构(几十到几百拍):主题再现、段落反复(AABA)、模仿与卡农。这类规律要求模型在很久之后仍「记得」前面的素材,正好落在 2.2 的连乘雅可比与 2.3 的门控要处理的区域上。
于是音乐成了一个分层可控的实验台:想考察短程统计,看局部是否像样;想考察记忆,看主题有没有回来。文本任务同样能做这两件事,但「像不像」要人来判;音乐里跑调几乎立刻能听出来,反馈回路短得多。
2.8.2 数据表示与词表
记词表大小 、批大小 、时间步 、嵌入维 、隐状态维 。按 batch_first=True 的约定(与 Lab 1 的代码一致),一次前向的全部形状如下:
| 量 | 形状 | 含义 |
|---|---|---|
tokens |
一批 token id(整数张量),第 1 维 batch、第 2 维时间 | |
emb(tokens) |
Embedding 查表:每个 id 换成一行向量 | |
| 逐步更新的状态(定义 2.2.1) | ||
logits |
每个位置对下一个 token 的未归一化分数 |
三条实现语义必须记牢:
- Embedding 就是一次查表:
nn.Embedding(V, d_e)内部是一张 的矩阵,输入 id 就取出第 行。它数学上等价于先把 id 变成 one-hot 再做一次线性变换 ;两种写法参数量相同(),但查表只做一次索引,不做 维的稀疏乘。 - 输出维度必须是 :
nn.Linear(d_h, V)把每个位置的隐状态映射成 个 logits,softmax 之后才是分布 。 - 损失按位置独立:把 logits 与目标 id 各自展平成 个位置,一次
cross_entropy算完(见 2.8.4)。
词表设计决定模型要学什么(本节最容易跳过、却最影响结果的一步):
- 一个音 = 一个字符:词表小(几十),序列长(每音符 1–3 个 token)。模型必须自己从字符组合里拼出「音高与时长」的耦合;好处是数据需求低、可直接套用 char-RNN 的代码,代价是时值信息被切碎——一个长音符的音高字符与它的时值字符相隔若干 token。
- 音高 + 时长合成一个事件 token:词表变大(音高数 时长档数),序列变短(每音符 1 个 token)。时序结构更干净,但组合爆炸摊薄了每个 token 的样本数: 从 30 涨到 300,平均每 token 的观测数掉 10 倍,而参数量里 与 两项反而变大。
- 折中:音高与时长各占一个位置、但成对出现(把时长字符限制在一个很小的集合,如
1/2/4)。多数入门实现默认走这条。
这里有一条与 2.1 一脉相承的账:词表大小 与序列长度 要一起算。 大、 小,序列短但每个 token 的样本少; 小、 大,样本多但单步预测更「碎」。几 KB 的数据上,先把 压小几乎总是更稳的选择——这也是 Lab 1 直接采用字符流的原因。
2.8.3 模型:字符级 RNN 与一个更小的 Transformer
Lab 1 的默认实现是一层 Embedding + 一层 GRU + 一层线性输出。把它写清楚(与 Lab 1 的实现一致,这里补上形状注释):
import torch, torch.nn as nn
class RNN(nn.Module):
def __init__(self, vocab, hidden=128):
super().__init__()
self.emb = nn.Embedding(vocab, 32) # (B,T) -> (B,T,32):一次查表
self.rnn = nn.GRU(32, hidden, batch_first=True) # 门控递推(2.3):内部逐步更新 h_t
self.out = nn.Linear(hidden, vocab) # (B,T,d_h) -> (B,T,V):每个位置的下一 token logits
def forward(self, x, h=None):
z, h = self.rnn(self.emb(x), h) # h: (B,d_h),可在 batch 间复用、必须按需断开
return self.out(z), h # 返回未归一化的分数,softmax 留给采样或损失
三点实现问答(每条都对应讲里的结论):
- 为什么用门控单元而不是朴素 RNN:字符流里的长程依赖要靠状态通道传下去,而朴素 RNN 的梯度是 的连乘(定理 2.2.2),几百个 token 的片段上很容易衰减到不起作用;GRU 的更新门提供加法通道(2.3.1)——这是「2.2 的代价 → 2.3 的解法」第一次在实验里见效。
- 为什么
hidden=128这种小模型就够:数据只有几 KB、词表几十,瓶颈在数据里的统计规律而不在参数量。参数远超数据量只会换来训练集损失漂亮、自由生成崩坏。 - 为什么
out不接 softmax:训练用F.cross_entropy(内部含 log-softmax,数值更稳),推理时再手动 softmax 或直接在 logits 上做截断与采样(2.8.5)。
把 GRU 换成 Transformer 也是可选路线(Lab 1 扩展任务 1),但规模要跟着数据缩小。下面是一个 2 层、、4 头的小解码器,直接复用 2.5.7 的 PreLNBlock(Pre-LN + 因果掩码):
class TinyTransformer(nn.Module):
"""可选路线:Pre-LN 块 + 因果掩码 + 学习式位置编码(2.5.3 / 2.4.5 / 2.6)"""
def __init__(self, vocab, d=96, H=4, layers=2, d_ff=256, max_T=64):
super().__init__()
self.emb = nn.Embedding(vocab, d) # (B,T) -> (B,T,d)
self.pos = nn.Embedding(max_T, d) # 注意力置换等变,顺序必须显式注入
self.blocks = nn.ModuleList([PreLNBlock(d, H, d_ff) for _ in range(layers)])
self.norm = RMSNorm(d) # Pre-LN 主干尺度随深度累积,收尾必须归一化
self.out = nn.Linear(d, vocab, bias=False) # (B,T,d) -> (B,T,V)
def forward(self, x):
h = self.emb(x) + self.pos(torch.arange(x.shape[1], device=x.device)) # (B,T,d)
for blk in self.blocks:
h, _ = blk(h) # blk 内 causal=True:一次算完全部位置
return self.out(self.norm(h)) # (B,T,V)
同一个小数据集上的对比(GRU 侧 、、;Transformer 侧 、2 层、4 头、):
| 项目 | 字符级 GRU | 小 Transformer |
|---|---|---|
| 参数量 | 约 :嵌入 ;GRU ;输出 | 约 :每块 ,2 层 ;嵌入 ;位置 ;输出 ;收尾归一化 |
| 每步训练开销 | 更小(一次门控矩阵乘) | 更大,但沿 维可并行 |
| 典型现象 | 几百步内损失稳定下降,续写很快「像」训练数据的局部搭配 | 同样步数下损失往往还很高,需要更多步才可比,且更容易先过拟合 |
| 长程行为 | 受状态容量与 BPTT 连乘限制,主题再现常失败 | 路径长度 ,数据足够时主题再现更容易出现 |
标注:「典型现象」一列属于工程经验 / 本书判断,不是论文事实——它依赖数据、超参与随机种子。正确的用法是当作预期管理(几 KB 数据上先跑 GRU),而不是当作可引用的结论;要得到自己的结论,就按 Lab 1 扩展任务 1 在同参数量下比较收敛速度与生成质量。
2.8.4 训练:teacher forcing 与逐位置交叉熵
给定训练片段 ( 个 token,共 个预测位置),teacher forcing 指:第 个位置以数据中的真实前缀 为条件预测下一个 token,损失为所有位置的负对数似然之和
条件里的 全部来自数据,不含模型自己的输出;因此训练可以「一次前向、 个位置同时算」。这一条与 定义 2.2.3 是同一件事,实现上它等价于错位一位:
logits = model(tokens)[0] # (B,T,V)
loss = F.cross_entropy(logits[:, :-1].reshape(-1, V), # 用 x_t 处的输出
tokens[:, 1:].reshape(-1)) # 预测真值 x_{t+1}(右移一位)
对应的自由跑(free-running)模式则是把模型上一步采样出的 token 接回输入——两个模式的输入分布不同,这个偏移就是 定义 2.2.3 的 exposure bias。
语言模型训练不需要按时间写循环,只需要把同一段序列右移一位当标签:第 个位置的输出对应第 个 token。代价是这行代码的两种典型错误都不会报错、只会让损失偏高或偏低:写成 logits[:, :-1] 对 tokens[:, :-1] 变成「预测自己」(损失会降得异常快,生成却只会复读);写成对 tokens[:, 2:] 则是跳过一格(损失停在较高水平)。因此 Lab 1 的排错清单把「损失配对」放在第 2 条——先确认标签,再调学习率。
设模型在 个预测位置上的平均交叉熵为 (单位为 nat,即自然对数下的每 token 平均负对数似然),定义困惑度(perplexity, PPL)
读法是:模型在每个位置上相当于在多少个「等概率候选」之间犹豫。若分布是在 个候选上的均匀分布,则 、;因此 PPL 的取值范围是 ,下界 1 表示把所有概率都压给正确 token。
| 平均交叉熵 | 直觉读法 | |
|---|---|---|
| 约在 2.7 个候选间犹豫——相当确信 | ||
| 约 7 个候选 | ||
| 约 20 个候选 | ||
| 比 的均匀分布()还差 |
未经训练或训练失败的模型会输出接近均匀的分布,于是损失停在 附近: 时是 。若损失高于这条线,说明模型不是「不会」而是在系统性地给错答案——最常见的原因是标签错位、padding 位置被计入了损失、或 token 映射表与数据预处理不一致。把 当作一条水平线画在损失曲线上,是这个实验里成本最低、信息量最大的一次对照。
同时要记住:困惑度是「猜得准不准」,与「好不好听」没有直接关系(见 2.8.6)。
训练循环还差三个动作,它们都可以在 Lab 1 的排错清单里找到对应位置:
for tokens in loader: # tokens: (B,T)
logits, h = model(tokens, h) # h 每段从零开始(①),段末 h = h.detach()(③)
loss = F.cross_entropy(logits[:, :-1].reshape(-1, V), tokens[:, 1:].reshape(-1))
opt.zero_grad(); loss.backward()
nn.utils.clip_grad_norm_(model.parameters(), 1.0); opt.step() # ② 裁剪必须在 backward 与 step 之间
三点说明:① 隐状态不跨片段(更不必说跨 epoch)复用,这是「片段之间没有真实连续性」的直接后果(强行传下去会让 BPTT 图跨越整个训练集);② 裁剪是序列任务的安全网,治爆炸不治消失(定理 2.2.3);③ detach() 忘记写时,症状是显存随训练步数缓慢增长、最后 OOM——与「模型不学习」是两回事。
2.8.5 采样:温度、top-k 与 top-p
设模型在某一步给出的 logits 为 ,温度 下的采样分布为
等价说法:温度就是把 logits 除以 再做 softmax。三条边界行为: 时 趋近 one-hot,即趋近贪心(argmax)解码; 时是原分布,不做任何修改; 时分布被摊平,低概率候选获得可观的概率质量。
对任意两个候选有 ——温度不改变大小关系,只改变「分数差」被放大的倍数。
低温度下模型只敢选概率最高的音符,于是容易陷入循环(「总在同一个音上打转」);高温度下低概率音符获得更多机会,旋律更多变,也更容易蹦出训练数据里没见过的组合。所以温度没有「正确值」,只有风格:本实验常用 0.5 / 1.0 / 1.5 各生成一段来对比(Lab 1 提交要求第 3 条;Lab 1 的温度图 画出了同一组 logits 在 下的分布差异)。
一个容易忽略的推论: 与 的 argmax 完全相同(softmax 保序),所以「把温度调到 0 更安全」是错的——它只是把多样性换成确定性与复读。
两种截断采样(truncation sampling),都先按概率从大到小排序、只保留一个子集 ,再把 上的概率重新归一化:
- top-k:固定候选个数, 概率最大的 个 token, 是超参;
- top-p(核采样,nucleus sampling):固定概率质量,取最小的 使 (即把概率降序累计到刚跨过 为止,跨过阈值的那个 token 保留), 是超参;然后用 采样。
适用差异:当分布很尖(模型确信)时,top-p 会自动缩小 ;当分布很平(模型犹豫)时它自动扩大 ——候选数随模型的不确定性自适应。top-k 的候选数恒定,实现更简单、行为更可预测,但在「该确信」的位置上会强行保留一批垃圾候选,在「该发散」的位置上又可能砍掉合理选项。因此实践上常以 top-p 为主,top-k 作为上界备用(两者可以叠加使用)。
温度改变的是整个分布的形状,top-k / top-p 改变的是分布的支撑集:它们把「累积概率很小的一大堆候选」直接删掉,再把删掉的质量摊给留下来的候选。于是两者的作用可以叠加理解:温度决定分布的平坦程度,截断决定长尾里有没有机会。具体的组合效果在本节的 例 2.8.1 里手算。
一段可运行的采样实现(自回归、逐步把新 token 接回输入):
@torch.no_grad()
def generate(model, idx, n_new, tau=1.0, top_p=None, top_k=None):
"""idx: (B,n0) 初始片段;每步采样一个 token 再把它接回输入(自由跑)"""
for _ in range(n_new):
logits, _ = model(idx) # (B,T,V),只用最后一个位置
probs = (logits[:, -1, :] / tau).softmax(-1) # 温度 = 把 logits 除以 τ(定义 2.8.3)
if top_p is not None: # 核采样:按概率质量截断
sp, si = probs.sort(-1, descending=True) # sp:降序概率,si:对应 token id
keep = (sp.cumsum(-1) - sp) <= top_p # 保留「前缀质量未超阈值」的候选(含跨过阈值者)
sp = torch.where(keep, sp, torch.zeros_like(sp))
probs = torch.zeros_like(probs).scatter(-1, si, sp)
probs = probs / probs.sum(-1, keepdim=True) # 重新归一化
if top_k is not None: # top-k:按候选个数截断
kth = probs.topk(top_k, -1).values[:, -1:]
probs = torch.where(probs >= kth, probs, torch.zeros_like(probs))
probs = probs / probs.sum(-1, keepdim=True)
idx = torch.cat([idx, torch.multinomial(probs, 1)], dim=1) # 采样:不是 argmax
return idx
某一步模型给出四个候选 token 的 logits 。计算(保留三位小数):(1) 的 softmax 分布;(2) 的分布;(3) 的分布;(4) 下取 top- 的截断集合与重新归一化概率,被丢弃的质量是多少?
(1)(2)(3) 按 逐个算,指数项与分母都列在表里( 越小,指数项的跨度越大、概率越集中于少数候选):
| 温度 | 指数项 | 分母 | ||
|---|---|---|---|---|
(4) top-()。 把 降序累积:候选 1 为 、候选 2 为 、候选 3 为 (首次跨过 ,该候选保留)、候选 4 为 (其前缀质量 已超阈值,丢弃)。于是
三句可以带走的观察。 ① 温度只改变「分数差 → 概率比」的放大倍数: 在三个温度下分别是 、、,与表中数值一致;② 三个温度下 argmax 都是候选 1,但「候选 1 有多确信」从 变到 ——这正是温度改变生成风格、却不改变贪心结果的原因;③ 温度与截断作用在不同层面,并且互相耦合:把阈值降到 top-, 下候选 3 的前缀质量 已超阈值,核只有 ;换成 后前缀质量被压到 、,两者都在阈值内,核扩大到 ——「分布变平 → 核变大」正是核采样的自适应行为。
∎2.8.6 评估:生成质量怎么看
生成质量没有单一指标。可行的做法是「一个主指标 + 三个体检项 + 一次人工试听」,每项都要知道它能发现什么 / 不能发现什么:
| 指标 | 怎么算 | 能发现什么 | 不能发现什么 |
|---|---|---|---|
| 验证集困惑度 | 在留出的片段上算平均负对数似然再取指数(定理 2.8.1) | 模型对训练分布内的下一 token 预测是否变准;词表 / 标签错位这类硬 bug(明显高于 就说明有问题) | 音乐质量;自由跑时的误差累积;模型自己采样出的序列有多好 |
| n-gram 重复率 | 统计生成片段里重复出现的 (如 3、4)元组占比 | 低温度 / 过训练导致的复读与「卡在一个音上打转」 | 复读有时是训练数据的真实结构(反复记号),需要对照训练集基线 |
| 音高 / 时长的分布距离 | 生成片段的音高直方图、时值直方图与训练集的距离(如总变差距离、KL) | 系统性偏差:整体跑调、时值全变成同一个值、只敢用少数几个音 | 顺序信息——分布完全一致但先后顺序错乱的旋律仍然难听 |
| 训练集外 token 比例 | 生成序列中出现在训练集里的 token / 组合占比 | token 映射表错误、词表与生成器不一致、截断后采样到越界 id | 组合层面的怪:单个 token 合法但搭配从未出现过 |
| 人工试听 A/B | 同一初始片段、不同设置各生成一段,盲听后投票 | 真正关心的东西:像不像训练数据、有没有主题再现、是否复读 | 不可复现、成本高;样本量小时结论不稳 |
三条使用纪律:① 用留出集算困惑度,用自由跑的产物看质量——两者衡量的是不同的事;② 报告困惑度时必须同时给出「均匀分布基准 」,否则数字无法解释;③ 音高 / 时长分布这类指标先算训练集自身的基线,否则不知道多远算远。
必须写清的一条:困惑度不是音乐质量指标。它只回答「给定真实前缀,模型把多少概率分给了正确的下一个 token」——一个把训练片段背下来的模型可以有极低困惑度,而它在自由跑时可能三步之内就开始复读;反过来,一个被温度调得更「敢猜」的模型在采样时更好听,它的困惑度却完全没变(困惑度是模型分布与数据之间的量,与采样策略无关)。
顺带把一个术语陷阱说清楚:Lab 1 扩展任务 4 提到「对比不同温度的 PPL」,严格的说法是必须先固定评测协议——常见两种口径:(i) 用温度为 的重标定分布 在留出片段上算平均 NLL(衡量「这个被重新塑形的分布还给正确 token 留了多少质量」, 变小会让它变差,因为分布偏离了数据的真实统计);(ii) 用温度为 生成一段序列,再计算这段自己生成的序列在其模型下的平均 NLL(衡量生成结果的「自信程度」, 越小这值越小)。两者都叫「温度的 PPL」,结论却相反——报告时务必写明口径。
2.8.7 常见失败模式与排查顺序
前五条与 Lab 1 的排错清单逐条对应,后四条是序列生成任务特有的。按序查,每一步都要求一个可观测的判据,不要同时改三个超参。
- 数据编码(最优先):
token -> id映射有没有错位?判据:把 id 反解回字符,与原始片段逐字符比对;检查词表里是否混入了不该有的字符(换行、空格、文件头)。 - 损失配对:是否严格「用 的输出预测 」?判据:写一个单样本小例子,让模型过拟合 8 个 token 的片段——若损失不能降到接近 0,标签或掩码一定有问题。同时检查 padding 位置是否被计入了损失(它们必须被 mask 掉)。
- 学习率:太大 → 损失震荡或出现
NaN;太小 → 几千步不动。判据:先试 与 各跑 200 步比较曲线形状。 - 隐状态: 是否每次从零开始?是否在段 / epoch 之间
detach()?判据:打印h.shape与显存占用随时间的变化——忘记 detach 的典型症状是显存缓慢增长直至 OOM。 - 序列长度:截断太短学不到长程结构(主题再现永远不会出现);太长则显存与梯度连乘长度一起爆。判据:固定其它设置,把窗口从 16 改成 64 看损失与生成结果。
- teacher forcing 与推理不一致(exposure bias,定义 2.2.3):训练损失已经很低,但自由跑几步就散。判据:同时报告 teacher-forced 损失与自由跑的损失;两者的差距就是这一项的大小。缓解:课程式地混入模型自己的预测、对输入加噪、或把单步错误率压得更低。
- 小数据过拟合的判据:「训练集困惑度很低、自由跑立刻发散」几乎总是这一条。定量的判据是:留出集 PPL 先降后升(或远高于训练集),而生成结果的重复率 / 分布距离同时变差。此时减小模型(、层数)或加 dropout 比继续训练更有效。
- 采样参数组合:温度与 top-p 一起放大时(如 且 top-),截断几乎不起作用,生成会变成随机拼接;两者都收得极紧( 且 top-)则必然复读。判据:固定初始片段,只改一个参数各生成三段对比。
- 评估协议本身:是否用训练集算的 PPL?是否把「自由跑的差」当成模型缺陷,而实际上只是 exposure bias?是否忘了对照 基准与训练集自身的 n-gram 基线?判据:把评估脚本逐项写在报告里,让每一步可被别人重跑。
2.8.8 从本讲理论到 Lab 的对应表
把 2.1–2.7 的结论按「实验里落在哪一步」排一遍。左列是理论,右列是 Lab 1 中的具体位置;跨页引用指向对应小节。
| 理论 | Lab 1 里的落点 | 做错时的可观测现象 |
|---|---|---|
| 定义 2.1.1 序列建模的三个要求(变长 / 保序 / 参数共享) | 把旋律当作一串 token 建模,而不是把定长向量喂给 MLP(Lab 1 §3 的建模思路) | 用 MLP 摊平输入 → 换个长度就报形状错误,且不能外推 |
| 定义 2.2.1 RNN 的隐状态递推 | self.rnn = nn.GRU(...) 那一行——GRU 是 2.3 的门控单元(定义 2.3.2),骨架仍是递推;换 nn.LSTM 可做同参数量对比(扩展任务 1) | 逐步循环里漏掉 的传递 → 模型退化成逐 token 的独立分类器 |
| 定理 2.2.2/定理 2.2.3 BPTT 连乘与消失 / 爆炸判据 | 损失不降 / 出现 NaN 的诊断;裁剪夹在 backward() 与 step() 之间(按全局范数裁剪) | 把学习率一再调小治 NaN(无效);或不写裁剪直接训长序列 |
| 定义 2.2.3 teacher forcing 与 exposure bias | 训练循环里用真实前缀(logits[:, :-1] 对 tokens[:, 1:]);「训练好、自由跑崩」的诊断 | 只报 teacher-forced 损失 → 误判模型可用 |
| 定义 2.4.4 缩放点积注意力 | 换 Transformer 时的注意力层(2.5.7 的 MHA) | 漏掉除以 → 训练初期损失平台期更长 |
| 定义 2.4.5 因果掩码 | PreLNBlock 内的 causal=True:训练时一次算完所有位置 | 漏掩码 → 训练损失漂亮、生成立刻崩坏(最隐蔽的 bug) |
| 2.5.1 多头注意力 | TinyTransformer 的 MHA;同参数量下与 GRU 对比 | 不加位置编码 → 输出与输入顺序几乎无关 |
| 2.5.3 Pre-LN 与 RMSNorm | x = x + self.attn(self.n1(x)) 与收尾的 RMSNorm | 把 LN 放错位置 → 深层训练变差,误判为学习率问题 |
| 2.6.4 位置编码(含 RoPE) | 若改用 RoPE 替掉 self.pos:分数只依赖相对位移 | 学得更长上下文时外推失败(位置表长度 max_T 用尽) |
| 定义 2.8.3 温度采样 | z = logits[:, -1, :] / tau 与最后的 torch.multinomial | 用 argmax 生成 → 复读、越多人听越单调 |
| 定理 2.8.1 困惑度 | 留出片段上的平均 NLL 取指数(扩展任务 4);必须给 基准 | 用训练集 PPL 选模型 → 选出过拟合最严重的那个 |
| 2.7 状态空间 / 线性注意力 / MoE | 本节不展开:想进一步压长序列或算力时的替代方向 | 在小数据上直接上大结构 → 参数量暴涨而质量无提升 |
2.8.9 陷阱与勘误
- 「用 argmax 生成最安全」。argmax 是 的极限(定义 2.8.3),它把分布整段丢掉只留一条路径,结果高度确定、极易复读;而且它与 的 argmax 完全相同(例 2.8.1(1)(2) 的候选 1 都是 argmax,但「有多确信」从 变成 )。采样不是「引入噪声」,而是按模型自己的概率行事。
- 「训练损失下降 = 生成质量提升」。损失是 teacher forcing 下的一步预测精度(定义 2.8.2),生成是自由跑下的多步决策;两者之间隔着 exposure bias 与采样策略。只看损失下降就宣布成功,是这个实验里最常见的误判(对应 2.8.7 第 6 条)。
- 「温度是唯一要调的超参」。温度只改变分布形状;top-k / top-p 改变支撑集(定义 2.8.4);数据窗口长度、隐蔽的
detach、截断与评估口径各自影响完全不同的环节。把三个变量同时改,等于没有任何可归因的结论。 - 「忘了掩码 / 忘了错位一位」。这两个 bug 的共同特征是不报错:漏因果掩码时训练损失异常低而生成崩坏(2.8.8 表里「定义 2.4.5 因果掩码」那一行);错位一位写成「预测自己」时损失降得比正常快、生成只会复读(定义 2.8.2)。
- 「把音乐当 NLP 词表随意切分」。音乐的 token 由记谱格式决定,而不是由统计分词器决定(定义 2.8.1):把音高与时长拆开、或把时值写成不同长度的字符串,都会直接改变「模型要学什么」以及每类 token 的样本数(词表与形状约定)。词表是设计决定,不是预处理细节。
Q1 写出字符级音乐模型的训练损失,并说明「错位一位」具体指哪两列的配对;若把它写成「用 预测 」,会观察到什么现象?
Q2 某模型在留出集上的平均交叉熵为 。它的困惑度是多少(保留两位小数)?若词表 ,均匀分布基准是多少?这个模型的 PPL 说明什么、不说明什么?
Q3 同一组 logits,把 从 改成 、再改成 ,生成结果分别会怎样变化?为什么在 top- 截断下,把分布调平往往会使保留的候选数变多?(用 例 2.8.1 里 的情形验算。)
Q4 一个模型训练集困惑度 、自由跑生成的旋律三步之内开始复读,且重复率随温度升高反而下降。给出至少三条排查动作,并说明每条期望看到什么。
参考要点
Q1 ;配对是 logits[:, :-1](第 个位置的输出)对 tokens[:, 1:](第 个 token)。若写成 tokens[:, :-1],任务变成「复制输入」:损失会下降得比正常更快、更低(模型学会输出当前 token 的特征),但推理时它没有「当前 token」可看,只能重复刚刚生成的那个符号——复读。
Q2 。均匀分布基准 (PPL )。它说明模型在留出片段上平均相当于在约 12 个候选之间犹豫,明显好于均匀(12 远小于 40);它不说明生成的旋律好听、也不说明自由跑时误差不会累积——困惑度只与「给定真实前缀的下一个 token 预测」有关。
Q3 :分布更尖,输出更保守、更确定,重复增多、更少「惊喜」;:分布更平,更多变、也更容易跑调或出现训练数据里没有的怪组合。叠加 top- 时核的大小随分布自适应:分布被摊平后,需要更多候选才能凑够同样的概率质量(例 2.8.1(3) 里把阈值取成 : 时核是 , 时扩大到 ),反之分布很尖时核会自动收缩到一两个候选。
Q4 ① 先分离两种模式:同时报告 teacher-forced 损失与自由跑的损失,差值大小就是 exposure bias 的量级(期望看到自由跑损失远高于训练集损失);② 检查是否过拟合:比较训练集与留出集 PPL,若留出集明显更差,减小模型规模或加 dropout(期望留出集 PPL 下降、重复率下降);③ 检查采样组合: 的强截断会直接制造复读,把它调到 并只保留 top- 作为上界(期望重复率下降、但音高分布仍接近训练集);④ 检查窗口长度与数据本身:窗口太短会让模型无法学到「段落」级结构,只能学局部 n-gram——把窗口从 16 调到 64 后主题再现开始出现,是这类问题的典型判据。注意:最后一条才是「模型学不会长程依赖」与「训练已经饱和」的分界线。
- 本书 Lab 1(音乐生成):本节每一条都能在实验里找到对应的代码与排错步骤,建议与 2.8.8 的对照表一起读。
- MIT 6.S191 Introduction to Deep Learning(官网
introtodeeplearning.com,Lab 仓库github.com/aamini/introtodeeplearning):母本课程与配套的音乐生成实验。 - Cho et al., Learning Phrase Representations using RNN Encoder–Decoder(2014, arXiv:1406.1078)——GRU 的原始论文,Lab 1 默认使用的循环单元。
- Jozefowicz, Zaremba & Sutskever, An Empirical Exploration of Recurrent Network Architectures(2015, ICML)——在大规模对比中 GRU 与 LSTM 表现相近、门控结构的选择并非决定性因素这一经验结论的来源。
- Olah, Understanding LSTM Networks(2015, colah.github.io)——用图解讲清门控与状态通道,配合 2.3 读。
- Vaswani et al., Attention Is All You Need(2017, arXiv:1706.03762)——把 Lab 1 的扩展任务 1(换小 Transformer)接回 2.5 的原始论文。
本节小结:Lab 1 的音乐生成把整讲的主线收成一条可执行的流水线——旋律 → token 序列 → 门控递推(或小 Transformer)→ teacher forcing 的逐位置交叉熵 → 温度 / top-k / top-p 采样 → 用困惑度、重复率、分布距离与人工试听共同判断质量。它同时演示了整讲的主线逻辑:递推给出参数共享与 推理状态(2.2),门控用加法通道把梯度从连乘里救出来(2.3),注意力用 路径长度与训练并行换取 开销(2.4、2.5),而位置编码、高效注意力、状态空间与稀疏专家(2.6、2.7)是在同一批约束上继续做交换。最后请记住三件事:困惑度不是音乐质量指标;温度是风格旋钮而不是待调参数;「训练好、自由跑崩」几乎总是 exposure bias 或漏掩码。下一讲把同样的自回归与采样框架搬到像素上,从卷积骨干走到视觉中的注意力混合(第 3 讲);再下一讲(第 4 讲)系统展开生成建模,自回归、扩散与流匹配会在同一个「逐步生成」的框架里被并列比较。