3.6 正则化:给排程加上约束 本节摘要:过拟合的标志是训练损失持续下降而验证损失掉头向上——模型开始背题而不是学规律。本节用加州房价制造一次真实的过拟合现场,然后逐件讲解三件治理武器:权重衰减把参数范数写进损失、Dropout 在训练时随机丢弃单元、早停在验证曲线拐点刹车,并给出三者的组合策略与参数起点。正则化的本质是给"逐批更新参数"这条调度线加约束面。 本节能力清单 阅读完本节,你应当能够: 识别过拟合曲线并用训练验证差距量化其程度; 配置 L1、L2 正则并解释两者对参数的塑造差异; 正确设置 Dropout 比例,理解训练与推理两种模式的差别; 组合早停与权重恢复,拿到"最好的那一步"而不是"最后一步"。 制造一次过拟合现场 过拟合的温床是"模型容量远大于数据信息量"。
本节摘要:过拟合的标志是训练损失持续下降而验证损失掉头向上——模型开始背题而不是学规律。本节用加州房价制造一次真实的过拟合现场,然后逐件讲解三件治理武器:权重衰减把参数范数写进损失、Dropout 在训练时随机丢弃单元、早停在验证曲线拐点刹车,并给出三者的组合策略与参数起点。正则化的本质是给"逐批更新参数"这条调度线加约束面。
阅读完本节,你应当能够:
过拟合的温床是"模型容量远大于数据信息量"。故意造一个:加州房价只给 300 条训练数据,配一个三层大网络,看曲线怎么走。
import tensorflow as tf import numpy as np from sklearn.datasets import fetch_california_housing housing = fetch_california_housing() X, y = housing.data.astype("float32"), housing.target.astype("float32") Xtr, ytr = X[:300], y[:300] # 刻意的小训练集 Xva, yva = X[300:1000], y[300:1000] overfit = tf.keras.Sequential([ tf.keras.layers.Input(shape=(8,)), tf.keras.layers.Dense(256, activation="relu"), tf.keras.layers.Dense(128, activation="relu"), tf.keras.layers.Dense(1), ]) overfit.compile(optimizer="adam", loss="mse") h = overfit.fit(Xtr, ytr, validation_data=(Xva, yva), epochs=60, verbose=0) tr = h.history["loss"] va = h.history["val_loss"] print(f"epoch 1: train {tr[0]:.3f} val {va[0]:.3f}") print(f"epoch 10: train {tr[9]:.3f} val {va[9]:.3f}") print(f"epoch 60: train {tr[-1]:.3f} val {va[-1]:.3f}") print(f"best val {min(va):.3f} at epoch {int(np.argmin(va)) + 1}") # 参考输出: # epoch 1: train 1.352 val 1.401 # epoch 10: train 0.512 val 0.847 # epoch 60: train 0.181 val 1.205 # best val 0.793 at epoch 14 # 训练损失一路降到 0.18,验证损失却在 14 轮附近见底后回升——教科书式过拟合
曲线语言翻译过来:前十几轮模型在学规律,之后开始背训练集的题——训练损失继续降,但背题能力对没见过的数据无用,验证损失回升。最好的模型其实在第 14 轮,之后六十轮全在帮倒忙。

武器一,权重衰减:把权重的范数乘系数加进损失,等于每步更新时额外把参数往零拉一点。L2 均匀压缩权重,让模型倾向平滑解;L1 把小权重直接压成零,产生稀疏解——想要特征筛选时用 L1。在 Keras 里它是层参数 kernel_regularizer,作用于训练步的损失计算。
武器二,Dropout:训练时每批按比例随机把一部分单元置零,推理时全部启用(框架自动处理两种模式)。它强迫网络不依赖任何单一路径,相当于同时训练无数个子网络的集成。
regularized = tf.keras.Sequential([ tf.keras.layers.Input(shape=(8,)), tf.keras.layers.Dense(256, activation="relu", kernel_regularizer=tf.keras.regularizers.l2(1e-4)), tf.keras.layers.Dropout(0.3), tf.keras.layers.Dense(128, activation="relu", kernel_regularizer=tf.keras.regularizers.l2(1e-4)), tf.keras.layers.Dropout(0.3), tf.keras.layers.Dense(1), ]) regularized.compile(optimizer="adam", loss="mse") hr = regularized.fit(Xtr, ytr, validation_data=(Xva, yva), epochs=60, verbose=0) trr, var = hr.history["loss"], hr.history["val_loss"] print(f"train last {trr[-1]:.3f} val last {var[-1]:.3f}") print(f"best val {min(var):.3f}") # 参考输出: # train last 0.412 val last 0.681 # best val 0.612 # 对比过拟合基线:val last 1.205 对 0.681,best 0.793 对 0.612 # 训练与验证的差距收窄,最终验证损失显著更低
武器三,早停:监控验证指标,耐心若干轮无改善就停,并把权重回滚到最佳一步。它与回调机制(3.7 节)联动,是三件武器里唯一"零计算成本"的:
es = tf.keras.callbacks.EarlyStopping( monitor="val_loss", patience=8, restore_best_weights=True) # 停止时回滚到最佳权重 stop_model = tf.keras.Sequential([ tf.keras.layers.Input(shape=(8,)), tf.keras.layers.Dense(256, activation="relu"), tf.keras.layers.Dense(128, activation="relu"), tf.keras.layers.Dense(1), ]) stop_model.compile(optimizer="adam", loss="mse") hs = stop_model.fit(Xtr, ytr, validation_data=(Xva, yva), epochs=200, verbose=0, callbacks=[es]) print(f"stopped after {len(hs.history['loss'])} epochs") # 参考输出:stopped after 23 epochs # 上限给了 200 轮,23 轮就刹住——省下的不只是算力,更是过拟合的段位
三件武器不互斥,常规组合是"适中容量 + Dropout 0.2 到 0.5 + L2 1e-4 起步 + 早停 patience 8 到 15"。参数起点的经验值:Dropout 输入层不超过 0.2、隐藏层 0.3 到 0.5;L2 从 1e-4 起,验证损失仍降就加码一个数量级。有一个常见误解要纠正:正则化不是万能胶——小数据上加满武器仍可能过拟合,说明容量本身超了需求,该减层减层。反方向的错误也别犯:早停的 patience 太小(一两轮)会把正常的波动当成拐点,模型在半山腰被刹停。
⚠️ 常见坑:验证集参与过超参数选择后再拿它报最终成绩,验证集已经被"训练"了——正则化强度、早停轮数都是对着它调的。规范做法是再留一个从没参与任何决策的测试集,只在最后碰一次。
💡 关键直觉:正则化的每一件武器都在回答同一个问题——"训练损失最低"与"泛化最好"不是同一件事。权重衰减与 Dropout 压参数的自由度,早停直接承认训练越久不等于越好。
下节看让早停成为可能的机制:回调。