Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lab 1 · 深度学习入门与音乐生成

本实验对应第 1、2 讲:先用一层网络把「前向 → 损失 → 反向 → 更新」跑通,建立张量与自动微分的手感;再用字符级 RNN 学习一段旋律的序列规律,并从初始片段续写出新乐句。全程不需要大数据集。

1. 环境与数据

  • 运行环境:Python 3.9+,TensorFlow 或 PyTorch 任选其一(本书示例给出 PyTorch 版本,思路与框架无关)。
  • 数据:一小段符号化的旋律(如 ABC 记谱或 MIDI 转成的音符事件序列)。把它当作一串 token——这正是第 2 讲把序列当作离散符号序列的翻版。
数据很小,所以别期待"作曲"

本实验的目的是看清训练循环与序列模型的行为,不是训练一个能谱曲的模型。数据集只有几 KB 时,模型能学会局部的音符搭配与节奏模式就已经达到目标。判断标准是"续写像训练数据",不是"续写好不好听"。

2. 第一部分:用自动微分训练一层网络

把一个简单函数(如 y=sin⁡(3x)y=\sin(3x) 在若干采样点上)当作回归问题,用一层网络拟合。每一步都对应第 1 讲的四个动作:

import torch, torch.nn as nn

model = nn.Sequential(nn.Linear(1, 64), nn.ReLU(), nn.Linear(64, 1))
opt   = torch.optim.Adam(model.parameters(), lr=1e-2)
loss_fn = nn.MSELoss()

for step in range(2000):
    pred = model(x)                 # ① 前向:得到预测
    loss = loss_fn(pred, y)         # ② 损失:MSE(回归用 MSE,见第 1 讲 1.4)
    opt.zero_grad()                 #    清空上一轮梯度
    loss.backward()                 # ③ 反向:自动微分求梯度(定理 1.2)
    opt.step()                      # ④ 更新:θ ← θ − η∇L
逐行对应的讲次

model(x) = 定义 1.1 的前向;loss_fn = 定义 1.2 的 MSE;loss.backward() = 定理 1.2 的反向传播;opt.step() = 定义 1.3 的梯度下降。把这条对应关系记住,你就掌握了所有深度学习的骨架。

观察题 73.1 · 去掉激活会怎样

把 nn.ReLU() 删掉再训一次,拟合效果如何?为什么?(提示:见第 1 讲 1.2的"线性变换 + 非线性"。)

参考结论

拟合会退化——两层线性等价于一层线性,无法表达 sin⁡\sin 的弯曲。损失停留在一个较高的平台,加大训练步数也无济于事。这从反面印证了激活函数的作用。

∎

3. 第二部分:字符级 RNN 生成旋律

建模思路(第 2 讲):把旋律表示成 token 序列 x1,x2,…,xTx_1,x_2,\dots,x_T,模型在每个时刻读入当前 token 的 one-hot,输出下一个 token 的概率分布 p^(xt+1∣x≤t)\hat p(x_{t+1}\mid x_{\le t})。

class RNN(nn.Module):
    def __init__(self, vocab, hidden=128):
        super().__init__()
        self.emb = nn.Embedding(vocab, 32)
        self.rnn = nn.GRU(32, hidden, batch_first=True)   # 第 2 讲 2.3
        self.out = nn.Linear(hidden, vocab)
    def forward(self, x, h=None):
        z, h = self.rnn(self.emb(x), h)
        return self.out(z), h          # 每个时刻的下一 token logits

# 训练目标:交叉熵(分类损失,第 1 讲 1.4)
loss = nn.functional.cross_entropy(logits[:, :-1].reshape(-1, V), tokens[:, 1:].reshape(-1))

训练方式叫 teacher forcing:每个时刻用真实的上一 token 作为输入,预测下一个 token,与真值算交叉熵。

4. 生成:温度采样

生成时从初始片段开始,把模型输出的分布采样出下一个 token,再把采样的 token 作为下一步输入(自回归)。采样用温度 τ\tau 调节:

p^i=exp⁡(zi/τ)∑jexp⁡(zj/τ).\hat p_i=\frac{\exp(z_i/\tau)}{\sum_j\exp(z_j/\tau)}.

温度分布形态生成风格
τ→0\tau\to0趋近 one-hot(argmax)保守:重复、单调,但不易出错
τ=1\tau=1softmax 原分布均衡
τ>1\tau\gt1更平坦冒险:多样、惊喜,也更容易跑调
温度在调"胆量"

低温度下模型只敢选概率最高的音符,于是容易陷入循环("总在同一个音上打转");高温度下给低概率音符更多机会,旋律更多变,但也可能蹦出训练数据里没出现过的怪组合。温度不是一个要"调对"的参数,而是一个风格旋钮。

温度对采样分布的影响 同一组 logits 在三个温度下的 softmax 分布。低温 0.5 时概率集中到最优音;温度 1 为原分布;高温 2 时分布被摊平,低概率音获得可观的质量。 024601 音0 0.02 音1 0.07 音2 0.88 音3 0.00 音4 0.00 音5 0.00 音6 0.03 τ = 0.5(保守) 次概率高 → 分布尖 02460 音0 0.09 音1 0.17 音2 0.59 音3 0.01 音4 0.03 音5 0.01 音6 0.10 τ = 1.0(原分布) softmax 原样 02460 音0 0.14 音1 0.19 音2 0.35 音3 0.05 音4 0.08 音5 0.05 音6 0.15 τ = 2.0(冒险) 分布被摊平
温度是「风格旋钮」不是「待调参数」(本教程计算绘制;同一组 logits,柱上数字为概率)。τ→0 分布趋近 one-hot——保守、易复读;τ>1 分布摊平——多样、易跑调。生成音乐时通常在 0.7~1.2 之间试。

5. 排错清单(训练不收敛时按序查)

  1. 数据编码:token→id 的映射有没有错位(差一位的 off-by-one 是序列任务最常见的 bug)?
  2. 损失配对:预测 logits[:, :-1] 对真值 tokens[:, 1:]——即“用 xtx_t 预测 xt+1x_{t+1}“了吗?
  3. 学习率:太大→损失震荡/NaN;太小→几千步不动。先试 10−310^{-3} 与 10−410^{-4}。
  4. 隐状态:RNN 的 h0h_0 是否正确初始化、是否在 epoch 之间断开(detach)?
  5. 序列长度:截断太短→学不到长程模式;太长→显存爆。

6. 提交与自检

  • 一张训练损失曲线(应单调下降);
  • 一段续写样例(给定初始 8 个 token,续写 ≥32 个 token);
  • 一段温度对比(τ=0.5\tau=0.5 与 τ=1.5\tau=1.5 各生成一段),并用一两句话解释差异。

自检:能说清「前向 / 损失 / 反向 / 更新」在代码的哪几行;能解释为什么回归用 MSE、分类用交叉熵;能说明温度如何改变采样分布。做到这三条,本实验达标。

7. 代码 ↔ 讲次对照表

代码对应结论
nn.Linear(1,64) → ReLU → Linear(64,1)1.2 多层感知机与通用逼近:非线性让深度有意义
loss_fn = nn.MSELoss()1.4 损失函数:回归用 MSE、分类用交叉熵
loss.backward()1.5 反向传播:链式法则的线性时间实现
opt.step() + zero_grad()1.6 优化器:Adam 的自适应步长;梯度默认累积
nn.GRU(32, 128)2.3 门控:门控与细胞/隐状态的作用
logits[:, :-1] 对 tokens[:, 1:]2.2 BPTT 与教师强制:用 xtx_t 预测 xt+1x_{t+1}
温度采样2.8 应用导读:τ\tau 是风格旋钮

8. 扩展任务(选做 1–2 项)

  1. 换结构对比:把 GRU 换成 LSTM、纯 RNN 与单层 Transformer(2.5),在同参数量下比收敛速度与生成质量——你会发现「结构选择」不是免费的。
  2. 采样策略对比:实现 top-k 与 top-p(核采样),与温度采样对照(2.8):哪种策略在「保持音乐性」上更稳?
  3. 序列长度实验:把 BPTT 的截断长度从 16 改到 64,观察损失与生成质量;再打开梯度裁剪(2.2),记录梯度范数曲线。
  4. 困惑度报告:计算验证集困惑度 PPL=exp⁡(cross-entropy)\mathrm{PPL}=\exp(\text{cross-entropy})(6.1),并解释它与损失的关系;对比不同温度的 PPL。
  5. 从零写一遍反向传播:用 numpy 手写这一层网络的 forward/backward,与 PyTorch 的 x.grad 逐元素对照(误差 < 1e-6)——这是最有效的一次「1.5 验收测试」。