第10章 训练与调参:交叉验证、优化器、损失与正则
读完你能: 设计互不泄漏的训练、验证与测试划分;手算交叉熵损失以及 SGD、RMSProp、Adam 的参数更新;区分按值与按范数梯度裁剪;解释批大小、学习率、训练轮数、early stopping、dropout 与权重衰减的作用机制。
1. 模型参数与超参数
在统计与神经网络模型中,有两类量需要严格分开:
- 模型参数(Parameters):由优化算法在训练数据上直接拟合出的内部变量,例如线性分类器的权重 和偏置 、词嵌入矩阵 、LSTM 的门控权重矩阵。
- 超参数(Hyperparameters):在启动训练前由外部设定的配置,用于控制模型结构或训练过程,例如折数 、嵌入维度 、隐藏层宽度、学习率 、批大小(batch size)、训练轮数(epochs)、正则化系数 、梯度裁剪阈值以及 dropout 比例。
超参数不能放到训练集损失里和模型参数一起直接求导最小化——如果按训练集分数挑选超参数,模型总是倾向于容量最大、正则最弱的配置,最终导致过拟合。因此,超参数必须依靠独立的验证集或交叉验证来选择。
2. K 折交叉验证与防泄漏边界
评估模型与挑选超参数时,第一步是先切出最终测试集(test set)锁存起来,在剩余的开发集上做 折交叉验证(-fold cross-validation):
- 将开发集随机打乱并均分为 个互斥子集(折,folds);
- 每次留出其中 1 折作为验证集,其余 折合起来作为训练集;
- 在训练折上从头拟合预处理与模型,在验证折上计算评估指标;
- 轮流对全部 折重复上述过程,用 次验证分数的均值和波动评估当前超参数配置;
- 选出最优超参数后,用全部开发集样本重新训练一次最终模型,最后在测试集上做唯一一次评估。
折数 的常见选择有 或 (兼顾偏差、方差与训练开销);极端情形 (每次只留 1 个样本验证)称为留一法(Leave-One-Out, LOO),几乎无偏但需要训练 次模型。
例1: 共有 1000 条标注文本,拟比较 6 组超参数配置。
- 先留出 20%(200 条)作为最终测试集;
- 剩余 800 条做 5 折交叉验证,每轮用 条训练、 条验证;
- 6 组配置 5 折共需训练 次;
- 选定平均验证分数最高的一组配置后,在全部 800 条开发集上重训 1 个模型,最后在 200 条测试集上给出最终指标。
划分数据时还要注意三条防泄漏红线:
- 所有依赖语料统计的预处理必须在训练折内部拟合:词表、IDF 权重、特征选择、均值方差标准化以及 SVD/PCA 投影矩阵,都只能用当前训练折的 640 条计算,再把学到的变换直接应用到验证折。如果先拿全部 1000 条文本算好 TF-IDF 再分折,验证集和测试集的词频分布就已经泄漏进了训练特征。
- 类别不平衡时用分层划分(Stratified K-Fold):保持每一折的类别比例与总体一致(前提是最小类别的样本数不少于折数 )。
- 同源片段或时序数据不能随机打散:若同一篇长文档被切成多段,或同一位用户的多条评论并存,必须按文档/用户 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) 用于二分类任务。设样本 的真实标签为 ,模型预测其为正类的概率为 , 个样本的平均损失为:
例2: 两个样本的真实标签与预测概率分别为 、。第二个样本真实为 0,它分到真实类别的概率是 ,因此:
2. 分类交叉熵(Categorical Cross-Entropy, CCE) 用于单标签多分类(共 类,通常配合输出层 softmax)。设 为样本 的 one-hot 标签, 为预测属于第 类的概率:
因为每个样本只有一个真实类别 满足 ,内层求和只剩 一项。例如某样本在真实类别上的预测概率为 ,则该样本的损失为 。若是多标签分类(一篇文档可同时打上多个不互斥标签),则应对每个标签独立输出 sigmoid 并按二元交叉熵求和,而不是用 softmax 强行让所有标签概率加和为 1。
工程实现中,框架通常提供将线性层 logits 与对数 softmax / sigmoid 合并计算的融合损失(如 BCEWithLogitsLoss、CrossEntropyLoss),既省去显式取指数再取对数的冗余,又能防止 时出现 数值溢出。
5. 梯度下降、批大小(Batch Size)与训练轮数(Epochs)
设第 步参数为 ,目标函数对参数的梯度为 ,学习率(步长)为 ,基本梯度下降更新为:
- 当 时,增大 会使损失上升,因此减去 向左移动;
- 当 时,增大 会使损失下降,减去负数正好向右移动。
例3: 设单参数损失 ,初始值 ,学习率 。在 处导数为 ,一步更新后:
损失从 下降到 。若学习率 设得过大(例如 ),一步更新就会跨过谷底 跳到对面更高处,引发震荡甚至发散;若 设得过小,则每步移动极慢,需要耗费大量迭代。
按每次计算梯度所用的样本数量,梯度下降分为三种模式:
- 全批量梯度下降(Batch GD):每步遍历全部训练集算精确梯度,方向稳但数据集大时单步极其耗时;
- 单样本随机梯度下降(狭义 SGD):每步只抽 1 个样本,更新快但梯度方差大;
- 小批量梯度下降(Mini-batch SGD):每步抽取
batch_size个样本(如 32、64、128)算平均梯度,兼顾 GPU 并行吞吐率与收敛稳定性。
一个 epoch(训练轮数) 指把整个训练集完整遍历一遍。设训练集有 个样本,取 batch_size = 64 且保留最后不足 64 的尾批,则每 1 个 epoch 包含 次参数更新(steps / iterations)。因此,把 batch_size 调大一倍而保持 epoch 数不变时,实际发生的参数更新步数会减少一半,对比实验时要同时留意总更新步数。
6. 自适应优化器:RMSProp 与 Adam
在词表特征或深层序列网络中,不同参数维度的梯度量级往往相差悬殊:高频词和浅层权重的梯度经常很大,低频词或深层权重的梯度却很小。统一使用同一个固定学习率 很难兼顾所有维度。
1. RMSProp RMSProp 为每个参数分量维护梯度平方的指数滑动平均 (衰减率 ,常写作 ),并用 对当前梯度做逐坐标缩放:
其中 (如 )用于防止除以 0。近期梯度平方越大的坐标, 越大,有效步长 就自动缩小,从而抑制该方向上的震荡。
例4: 设 ,第一步梯度 (忽略极小的 ):
2. Adam(Adaptive Moment Estimation) Adam 同时维护梯度的一阶动量(均值估计 )和二阶原点矩(未中心化的平方梯度估计 )。由于 ,在训练刚开始的前几步, 和 都会严重偏向 0,因此 Adam 引入了针对时间步 的偏差校正(bias correction):
例5: 设初始参数 ,,取常用超参数 ,第一步梯度 (忽略 ):
- 未校正矩:,;
- 偏差校正():,;
- 更新参数:。
注意看:在 时,偏差校正恰好把 和 约掉,使得 ,在梯度非零、忽略 且没有其他更新项的这个例子中,第一步更新量就是 。如果不做偏差校正,由于 远小于 ,直接算 ,第一步步长会无端放大 3 倍以上。另外, 是梯度平方的滑动平均(二阶原点矩),并没有减去均值 。
7. 梯度裁剪:按值裁剪与按范数裁剪
训练 RNN/LSTM 等序列模型时,偶发的陡峭损失面可能让某一步梯度突然暴涨。梯度裁剪(Gradient Clipping)在优化器执行更新前对梯度设置上限,常见有两种方式:
- 按值裁剪(Clipping by Value):指定阈值 ,将每个梯度分量独立截断到区间 内,即 。
- 按范数裁剪(Clipping by Norm):指定范数上限 ,当整个梯度向量的欧氏范数 时,将整个向量按比例缩小到长度 ,即 。
例6: 设梯度向量 ,其欧氏范数为 ,取裁剪阈值 :
- 按范数裁剪:因为 ,整体乘以 ,得到 。裁剪后范数恰好为 ,且两个分量的比值仍为 ,完全保留了原始下降方向。
- 按值裁剪:两个分量都超过 ,被双双截成 。此时不仅范数变成了 ,而且方向变成了 ,改变了梯度方向。
8. 过拟合与正则化
当训练轮数过多或模型容量远超数据规模时,常见现象是:训练损失持续下降,而验证集损失见底后开始反弹恶化。除了扩充数据、简化特征外,常用的三类正则化手段是:
- 早停(Early Stopping):每个 epoch 结束时监控验证集指标;若验证集表现在连续若干轮(patience)内不再提升,就提前终止训练,并恢复验证集表现最好那一轮的模型权重。注意早停的判断依据必须是验证集,决不能用最终测试集来决定何时停下。
- Dropout:训练期间每次前向传播以概率 将神经元的激活随机置 0,迫使网络不依赖少数神经元的固定协同(co-adaptation)。现代框架普遍采用 inverted dropout:训练时对保留下来的神经元除以 放大,推断(测试)时关闭随机丢弃、直接原样通过,从而保证训练与推断的激活期望一致。
- 例7: 某神经元原始激活为 ,丢弃率 (保留率 )。训练时它以 的概率输出 ,以 的概率输出 ,期望为 ;测试时直接输出 。
- 权重衰减(Weight Decay / L2 正则化):在损失函数后加上参数平方和惩罚 ,求导后梯度多出一项 ,抑制过大的权重。在标准 SGD 中,这等价于每次更新前先把权重乘以 做线性衰减,即 。但在 Adam 等自适应优化器中,直接往损失里加 L2 项会让 也一起被 缩放;若想保持纯粹的权重衰减,应使用解耦权重衰减的 AdamW。
自测
1. 先在整个数据集上调用 TfidfVectorizer().fit_transform(all_texts),再把得到的矩阵传给 5 折交叉验证,为什么会造成数据泄漏?
fit_transform(all_texts) 已经把验证折甚至测试折里的词表和文档频率(DF/IDF)统计进了全套特征向量。每一折训练时,模型通过 IDF 权重间接获知了未见样本的词分布,往往导致验证分数虚高。正确做法是用 Pipeline 在每一折的训练集内部独立调用 fit。
2. 深层 RNN 出现梯度消失、早期时间步梯度接近 0 时,把梯度裁剪阈值调大能解决问题吗?
不能。梯度裁剪只会在梯度超过阈值时将其缩小(防爆炸),当梯度本身小于阈值甚至趋近 0 时,裁剪操作原样放行,不会放大已经消失的信号。解决梯度消失需要改用 LSTM/GRU、残差连接或调整序列截断与初始化。
- 感谢你赐予我前进的力量

