约 35 分钟 · 需要梯度、期望与前面的分类及序列模型概念

读完你能: 设计互不泄漏的训练、验证与测试划分;手算交叉熵损失以及 SGD、RMSProp、Adam 的参数更新;区分按值与按范数梯度裁剪;解释批大小、学习率、训练轮数、early stopping、dropout 与权重衰减的作用机制。

1. 模型参数与超参数

在统计与神经网络模型中,有两类量需要严格分开:

  • 模型参数(Parameters):由优化算法在训练数据上直接拟合出的内部变量,例如线性分类器的权重 ww 和偏置 bb、词嵌入矩阵 EE、LSTM 的门控权重矩阵。
  • 超参数(Hyperparameters):在启动训练前由外部设定的配置,用于控制模型结构或训练过程,例如折数 KK、嵌入维度 dd、隐藏层宽度、学习率 η\eta、批大小(batch size)、训练轮数(epochs)、正则化系数 λ\lambda、梯度裁剪阈值以及 dropout 比例。

超参数不能放到训练集损失里和模型参数一起直接求导最小化——如果按训练集分数挑选超参数,模型总是倾向于容量最大、正则最弱的配置,最终导致过拟合。因此,超参数必须依靠独立的验证集或交叉验证来选择。

2. K 折交叉验证与防泄漏边界

评估模型与挑选超参数时,第一步是先切出最终测试集(test set)锁存起来,在剩余的开发集上做 KK 折交叉验证(KK-fold cross-validation):

  1. 将开发集随机打乱并均分为 KK 个互斥子集(折,folds);
  2. 每次留出其中 1 折作为验证集,其余 K−1K-1 折合起来作为训练集;
  3. 在训练折上从头拟合预处理与模型,在验证折上计算评估指标;
  4. 轮流对全部 KK 折重复上述过程,用 KK 次验证分数的均值和波动评估当前超参数配置;
  5. 选出最优超参数后,用全部开发集样本重新训练一次最终模型,最后在测试集上做唯一一次评估。

折数 KK 的常见选择有 K=5K=5 或 K=10K=10(兼顾偏差、方差与训练开销);极端情形 K=nK=n(每次只留 1 个样本验证)称为留一法(Leave-One-Out, LOO),几乎无偏但需要训练 nn 次模型。

例1: 共有 1000 条标注文本,拟比较 6 组超参数配置。

  • 先留出 20%(200 条)作为最终测试集;
  • 剩余 800 条做 5 折交叉验证,每轮用 800×45=640800\times\frac45=640 条训练、160160 条验证;
  • 6 组配置 ×\times 5 折共需训练 3030 次;
  • 选定平均验证分数最高的一组配置后,在全部 800 条开发集上重训 1 个模型,最后在 200 条测试集上给出最终指标。

划分数据时还要注意三条防泄漏红线:

  • 所有依赖语料统计的预处理必须在训练折内部拟合:词表、IDF 权重、特征选择、均值方差标准化以及 SVD/PCA 投影矩阵,都只能用当前训练折的 640 条计算,再把学到的变换直接应用到验证折。如果先拿全部 1000 条文本算好 TF-IDF 再分折,验证集和测试集的词频分布就已经泄漏进了训练特征。
  • 类别不平衡时用分层划分(Stratified K-Fold):保持每一折的类别比例与总体一致(前提是最小类别的样本数不少于折数 KK)。
  • 同源片段或时序数据不能随机打散:若同一篇长文档被切成多段,或同一位用户的多条评论并存,必须按文档/用户 ID 做分组划分(Group K-Fold),防止近似重复片段同时出现在训练折和验证折;若任务是用历史数据预测未来趋势,则必须按时间先后切分。

关于数据泄漏的常见陷阱可进一步参考 scikit-learn 常见陷阱与防泄漏文档。

3. 用 Pipeline 锁住每一折的拟合边界

在代码中防止预处理泄漏最稳妥的做法,是把向量化器与分类器封装进同一个 Pipeline。这样 GridSearchCV 在每一折内部调用 fit 时,都会只用当前训练折重新统计词表和 IDF:

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.svm import LinearSVC
from sklearn.pipeline import Pipeline
from sklearn.model_selection import train_test_split, StratifiedKFold, GridSearchCV

def train_and_evaluate(texts, labels):
    x_train, x_test, y_train, y_test = train_test_split(
        texts, labels, test_size=0.2, stratify=labels, random_state=7)
    pipe = Pipeline([("tfidf", TfidfVectorizer()), ("clf", LinearSVC())])
    cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=7)
    search = GridSearchCV(pipe, {
        "tfidf__ngram_range": [(1, 1), (1, 2)],
        "clf__C": [0.1, 1.0, 10.0],
    }, scoring="f1_macro", cv=cv, n_jobs=1)
    search.fit(x_train, y_train)
    return search.best_params_, search.predict(x_test), y_test

注意:上面示例直接使用了默认的 TfidfVectorizer(),其默认词级正则提取长度至少为两个字符的 Unicode 词字符片段,不会自动为连续中文做词语切分;这里用于英文文本;**如果处理中文语料,切勿直接把未分词的中文整句原样喂给默认 TfidfVectorizer**(否则整句连写汉字会被当成一个长词,单字还会被默认规则过滤),而应先接中文分词器传入自定义 tokenizer,或改设 analyzer="char" / "char_wb" 提取字符 n-gram。此外,由于使用了 5 折分层划分,训练集中每个类别至少要有 5 条样本。

4. 损失函数:二元交叉熵与多类交叉熵

评估指标(如 F1、准确率)往往因为包含离散阈值与取整而不可微,模型训练时真正通过梯度最小化的是可微的损失函数(loss function)。

1. 二元交叉熵(Binary Cross-Entropy, BCE / Log Loss) 用于二分类任务。设样本 ii 的真实标签为 yi∈{0,1}y_i\in\{0,1\},模型预测其为正类的概率为 pi∈(0,1)p_i\in(0,1),NN 个样本的平均损失为:

LBCE=−1N∑i=1N[yiln⁡pi+(1−yi)ln⁡(1−pi)].L_{\text{BCE}}=-\frac1N\sum_{i=1}^N\big[y_i\ln p_i+(1-y_i)\ln(1-p_i)\big].

例2: 两个样本的真实标签与预测概率分别为 (y1,p1)=(1,0.8)(y_1,p_1)=(1,0.8)、(y2,p2)=(0,0.6)(y_2,p_2)=(0,0.6)。第二个样本真实为 0,它分到真实类别的概率是 1−0.6=0.41-0.6=0.4,因此:

LBCE=−12(ln⁡0.8+ln⁡0.4)≈−12(−0.2231−0.9163)=0.5697.L_{\text{BCE}}=-\frac12(\ln0.8+\ln0.4)\approx-\frac12(-0.2231-0.9163)=0.5697.

2. 分类交叉熵(Categorical Cross-Entropy, CCE) 用于单标签多分类(共 CC 类,通常配合输出层 softmax)。设 yic∈{0,1}y_{ic}\in\{0,1\} 为样本 ii 的 one-hot 标签,picp_{ic} 为预测属于第 cc 类的概率:

LCCE=−1N∑i=1N∑c=1Cyicln⁡pic.L_{\text{CCE}}=-\frac1N\sum_{i=1}^N\sum_{c=1}^C y_{ic}\ln p_{ic}.

因为每个样本只有一个真实类别 c∗c^* 满足 yic∗=1y_{ic^*}=1,内层求和只剩 −ln⁡pic∗-\ln p_{ic^*} 一项。例如某样本在真实类别上的预测概率为 0.70.7,则该样本的损失为 −ln⁡0.7≈0.3567-\ln0.7\approx0.3567。若是多标签分类(一篇文档可同时打上多个不互斥标签),则应对每个标签独立输出 sigmoid 并按二元交叉熵求和,而不是用 softmax 强行让所有标签概率加和为 1。

工程实现中,框架通常提供将线性层 logits 与对数 softmax / sigmoid 合并计算的融合损失(如 BCEWithLogitsLoss、CrossEntropyLoss),既省去显式取指数再取对数的冗余,又能防止 pi→0p_i\to 0 时出现 ln⁡0\ln 0 数值溢出。

5. 梯度下降、批大小(Batch Size)与训练轮数(Epochs)

设第 tt 步参数为 θt\theta_t,目标函数对参数的梯度为 gt=∇θL(θt)g_t=\nabla_\theta L(\theta_t),学习率(步长)为 η>0\eta>0,基本梯度下降更新为:

θt+1=θt−ηgt.\theta_{t+1}=\theta_t-\eta g_t.
  • 当 ∂L∂θj>0\dfrac{\partial L}{\partial\theta_j}>0 时,增大 θj\theta_j 会使损失上升,因此减去 ηgt,j\eta g_{t,j} 向左移动;
  • 当 ∂L∂θj<0\dfrac{\partial L}{\partial\theta_j}<0 时,增大 θj\theta_j 会使损失下降,减去负数正好向右移动。

例3: 设单参数损失 L(θ)=(θ−3)2L(\theta)=(\theta-3)^2,初始值 θ0=1\theta_0=1,学习率 η=0.1\eta=0.1。在 θ0=1\theta_0=1 处导数为 g0=2(1−3)=−4g_0=2(1-3)=-4,一步更新后:

θ1=1−0.1×(−4)=1.4,\theta_1=1-0.1\times(-4)=1.4,

损失从 (1−3)2=4(1-3)^2=4 下降到 (1.4−3)2=2.56(1.4-3)^2=2.56。若学习率 η\eta 设得过大(例如 η>1\eta>1),一步更新就会跨过谷底 θ=3\theta=3 跳到对面更高处,引发震荡甚至发散;若 η\eta 设得过小,则每步移动极慢,需要耗费大量迭代。

按每次计算梯度所用的样本数量,梯度下降分为三种模式:

  • 全批量梯度下降(Batch GD):每步遍历全部训练集算精确梯度,方向稳但数据集大时单步极其耗时;
  • 单样本随机梯度下降(狭义 SGD):每步只抽 1 个样本,更新快但梯度方差大;
  • 小批量梯度下降(Mini-batch SGD):每步抽取 batch_size 个样本(如 32、64、128)算平均梯度,兼顾 GPU 并行吞吐率与收敛稳定性。

一个 epoch(训练轮数) 指把整个训练集完整遍历一遍。设训练集有 10001000 个样本,取 batch_size = 64 且保留最后不足 64 的尾批,则每 1 个 epoch 包含 ⌈1000/64⌉=16\lceil 1000/64\rceil=16 次参数更新(steps / iterations)。因此,把 batch_size 调大一倍而保持 epoch 数不变时,实际发生的参数更新步数会减少一半,对比实验时要同时留意总更新步数。

6. 自适应优化器:RMSProp 与 Adam

在词表特征或深层序列网络中,不同参数维度的梯度量级往往相差悬殊:高频词和浅层权重的梯度经常很大,低频词或深层权重的梯度却很小。统一使用同一个固定学习率 η\eta 很难兼顾所有维度。

1. RMSProp RMSProp 为每个参数分量维护梯度平方的指数滑动平均 vtv_t(衰减率 β∈(0,1)\beta\in(0,1),常写作 γ\gamma),并用 vt\sqrt{v_t} 对当前梯度做逐坐标缩放:

vt=βvt−1+(1−β)gt2,θt+1=θt−ηgtvt+ϵ.v_t=\beta v_{t-1}+(1-\beta)g_t^2,\qquad \theta_{t+1}=\theta_t-\eta\frac{g_t}{\sqrt{v_t}+\epsilon}.

其中 ϵ\epsilon(如 10−810^{-8})用于防止除以 0。近期梯度平方越大的坐标,vtv_t 越大,有效步长 η/vt\eta/\sqrt{v_t} 就自动缩小,从而抑制该方向上的震荡。

例4: 设 v0=0, β=0.9, η=0.1v_0=0,\ \beta=0.9,\ \eta=0.1,第一步梯度 g1=2g_1=2(忽略极小的 ϵ\epsilon):

v1=0.9×0+0.1×22=0.4,Δθ1=−ηg1v1=−0.20.4≈−0.3162.v_1=0.9\times0+0.1\times2^2=0.4,\qquad \Delta\theta_1=-\eta\frac{g_1}{\sqrt{v_1}}=-\frac{0.2}{\sqrt{0.4}}\approx-0.3162.

2. Adam(Adaptive Moment Estimation) Adam 同时维护梯度的一阶动量(均值估计 mtm_t)和二阶原点矩(未中心化的平方梯度估计 vtv_t)。由于 m0=0,v0=0m_0=0,v_0=0,在训练刚开始的前几步,mtm_t 和 vtv_t 都会严重偏向 0,因此 Adam 引入了针对时间步 tt 的偏差校正(bias correction):

mt=β1mt−1+(1−β1)gt,vt=β2vt−1+(1−β2)gt2,m_t=\beta_1m_{t-1}+(1-\beta_1)g_t,\qquad v_t=\beta_2v_{t-1}+(1-\beta_2)g_t^2,
m^t=mt1−β1t,v^t=vt1−β2t,θt+1=θt−ηm^tv^t+ϵ.\widehat m_t=\frac{m_t}{1-\beta_1^t},\qquad \widehat v_t=\frac{v_t}{1-\beta_2^t},\qquad \theta_{t+1}=\theta_t-\eta\frac{\widehat m_t}{\sqrt{\widehat v_t}+\epsilon}.

例5: 设初始参数 θ0=2\theta_0=2,m0=v0=0m_0=v_0=0,取常用超参数 β1=0.9, β2=0.999, η=0.1\beta_1=0.9,\ \beta_2=0.999,\ \eta=0.1,第一步梯度 g1=2g_1=2(忽略 ϵ\epsilon):

  1. 未校正矩:m1=(1−0.9)×2=0.2m_1=(1-0.9)\times2=0.2,v1=(1−0.999)×22=0.004v_1=(1-0.999)\times2^2=0.004;
  2. 偏差校正(t=1t=1):m^1=0.21−0.91=2\widehat m_1=\dfrac{0.2}{1-0.9^1}=2,v^1=0.0041−0.9991=4\widehat v_1=\dfrac{0.004}{1-0.999^1}=4;
  3. 更新参数:θ1=2−0.1×24=2−0.1=1.9\theta_1=2-0.1\times\dfrac{2}{\sqrt4}=2-0.1=1.9。

注意看:在 t=1t=1 时,偏差校正恰好把 1−β11-\beta_1 和 1−β21-\beta_2 约掉,使得 m^1=g1, v^1=g12\widehat m_1=g_1,\ \widehat v_1=g_1^2,在梯度非零、忽略 ϵ\epsilon 且没有其他更新项的这个例子中,第一步更新量就是 −ηsign⁡(g1)-\eta\operatorname{sign}(g_1)。如果不做偏差校正,由于 1−β2=0.0011-\beta_2=0.001 远小于 1−β1=0.11-\beta_1=0.1,直接算 m1/v1=0.2/0.004≈3.162m_1/\sqrt{v_1}=0.2/\sqrt{0.004}\approx3.162,第一步步长会无端放大 3 倍以上。另外,vtv_t 是梯度平方的滑动平均(二阶原点矩),并没有减去均值 mt2m_t^2。

7. 梯度裁剪:按值裁剪与按范数裁剪

训练 RNN/LSTM 等序列模型时,偶发的陡峭损失面可能让某一步梯度突然暴涨。梯度裁剪(Gradient Clipping)在优化器执行更新前对梯度设置上限,常见有两种方式:

  • 按值裁剪(Clipping by Value):指定阈值 c>0c>0,将每个梯度分量独立截断到区间 [−c,c][-c,c] 内,即 gj←min⁡(max⁡(gj,−c), c)g_j\leftarrow\min(\max(g_j,-c),\,c)。
  • 按范数裁剪(Clipping by Norm):指定范数上限 c>0c>0,当整个梯度向量的欧氏范数 ∥g∥2>c\lVert g\rVert_2>c 时,将整个向量按比例缩小到长度 cc,即 g←g⋅min⁡(1, c∥g∥2)g\leftarrow g\cdot\min\left(1,\,\dfrac{c}{\lVert g\rVert_2}\right)。

例6: 设梯度向量 g=(3,4)Tg=(3,4)^T,其欧氏范数为 ∥g∥2=32+42=5\lVert g\rVert_2=\sqrt{3^2+4^2}=5,取裁剪阈值 c=2c=2:

  • 按范数裁剪:因为 5>25>2,整体乘以 2/52/5,得到 gnorm=(1.2,1.6)Tg_{\text{norm}}=(1.2,1.6)^T。裁剪后范数恰好为 22,且两个分量的比值仍为 3:43:4,完全保留了原始下降方向。
  • 按值裁剪:两个分量都超过 22,被双双截成 gval=(2,2)Tg_{\text{val}}=(2,2)^T。此时不仅范数变成了 22+22=8≈2.83>2\sqrt{2^2+2^2}=\sqrt8\approx2.83>2,而且方向变成了 1:11:1,改变了梯度方向。

8. 过拟合与正则化

当训练轮数过多或模型容量远超数据规模时,常见现象是:训练损失持续下降,而验证集损失见底后开始反弹恶化。除了扩充数据、简化特征外,常用的三类正则化手段是:

  1. 早停(Early Stopping):每个 epoch 结束时监控验证集指标;若验证集表现在连续若干轮(patience)内不再提升,就提前终止训练,并恢复验证集表现最好那一轮的模型权重。注意早停的判断依据必须是验证集,决不能用最终测试集来决定何时停下。
  2. Dropout:训练期间每次前向传播以概率 pp 将神经元的激活随机置 0,迫使网络不依赖少数神经元的固定协同(co-adaptation)。现代框架普遍采用 inverted dropout:训练时对保留下来的神经元除以 1−p1-p 放大,推断(测试)时关闭随机丢弃、直接原样通过,从而保证训练与推断的激活期望一致。
    • 例7: 某神经元原始激活为 22,丢弃率 p=0.25p=0.25(保留率 1−p=0.751-p=0.75)。训练时它以 0.250.25 的概率输出 00,以 0.750.75 的概率输出 2/0.75=8/32/0.75=8/3,期望为 0.25×0+0.75×83=20.25\times0+0.75\times\frac83=2;测试时直接输出 22。
  3. 权重衰减(Weight Decay / L2 正则化):在损失函数后加上参数平方和惩罚 λ2∥θ∥22\frac{\lambda}{2}\lVert\theta\rVert_2^2,求导后梯度多出一项 λθ\lambda\theta,抑制过大的权重。在标准 SGD 中,这等价于每次更新前先把权重乘以 (1−ηλ)(1-\eta\lambda) 做线性衰减,即 θt+1=θt−η(gt+λθt)=(1−ηλ)θt−ηgt\theta_{t+1}=\theta_t-\eta(g_t+\lambda\theta_t)=(1-\eta\lambda)\theta_t-\eta g_t。但在 Adam 等自适应优化器中,直接往损失里加 L2 项会让 λθt\lambda\theta_t 也一起被 v^t\sqrt{\widehat v_t} 缩放;若想保持纯粹的权重衰减,应使用解耦权重衰减的 AdamW。

自测

1. 先在整个数据集上调用 TfidfVectorizer().fit_transform(all_texts),再把得到的矩阵传给 5 折交叉验证,为什么会造成数据泄漏?

fit_transform(all_texts) 已经把验证折甚至测试折里的词表和文档频率(DF/IDF)统计进了全套特征向量。每一折训练时,模型通过 IDF 权重间接获知了未见样本的词分布,往往导致验证分数虚高。正确做法是用 Pipeline 在每一折的训练集内部独立调用 fit。

2. 深层 RNN 出现梯度消失、早期时间步梯度接近 0 时,把梯度裁剪阈值调大能解决问题吗?

不能。梯度裁剪只会在梯度超过阈值时将其缩小(防爆炸),当梯度本身小于阈值甚至趋近 0 时,裁剪操作原样放行,不会放大已经消失的信号。解决梯度消失需要改用 LSTM/GRU、残差连接或调整序列截断与初始化。