本篇记录了 Hylee 2023 年 HW01 回归任务的实验过程与调参心得,内容包括神经网络结构设计、特征变量选择、优化器与正则化方法的尝试,以及相关的学习笔记和思考,适合关注深度学习实战与模型调优的同学参考。

AI总结

本文记录了一次完整的深度学习回归任务实验过程,涵盖了从网络架构设计到理论补充的全方位实践。

核心实验发现:

  • 网络架构选择: 测试了从浅层到深层的多种架构,发现 (32, 16) -> (16, 8) -> (8, 1) 效果最佳,验证了合适的网络深度比盲目加深更重要
  • 特征工程影响: 特征数量从64→32→16的实验揭示了验证集表现与最终测试集表现可能存在差异的重要现象
  • 优化器调参: SGD到Adam的切换需要配合学习率调整(从 1e-51e-3),early stopping参数从400到2000会导致过拟合

关键技术难点:

  • 过拟合识别: 多个实验中出现训练表现良好但排行榜分数差的情况,说明在整体训练数据上过拟合
  • 调参策略: 超参数调优是"不间断的、反复的"迭代过程,需要系统化记录和备份

理论知识整合:

  • Backpropagation: 提供了多项式回归的完整梯度推导过程,从 $\frac{\partial \text{loss}}{\partial a} = \sum_{i=1}^{n} 2(\hat{y_i} - y_i)$ 到参数更新公式
  • Mini-batch Gradient Descent: 详细阐述了算法流程,包括前向传播、损失计算(含L2正则化)和反向传播的数学表达
  • L2正则化: 解释了Frobenius范数 $\|W\|_F^2$ 的作用机制和 weight_decay 参数的实际效果
  • 学习率调度: 介绍了 CosineAnnealingWarmRestarts 的参数设置策略

实践经验总结:

  • 模型备份的关键性(“丢了一个效果特别好的模型"的教训)
  • 训练监控技巧(tqdm输出频率对训练速度的影响)
  • 基于单变量统计的特征选择方法在实际应用中的有效性

这份记录的价值在于展现了真实的机器学习实验过程:不仅包含成功的尝试,更重要的是记录了失败的探索和从中获得的洞察,为深度学习实践者提供了宝贵的调参经验和理论理解。

2023 年 HW011,之所以做 2023 年,是因为找到了参考 2

作业的目标,是个看似简单的回归问题。但难点在于,它给出的 baseline 很高,不是随便改两笔就能达到的。

代码中明确提示可以更改的部分,以 TODO 标出有:

  • 神经网络的具体结构/层数
  • 特征变量选择
  • 优化算法
  • L2 正则化

用到了很多没有讲到的技术,先记录使用和调参,具体的技术细节等学到的时候,再进行补充。

实验记录

大部分的目的是为了手动记录各种参数的效果,不太具有泛化参考意义。

神经网络尺寸

  • (input, 128) -> (128, 64) -> (64, 16) -> (16, 1),其他不变
  • (input, 128) -> (128, 32) -> (32, 1)
    • 差,难以收敛
  • (input, 128) -> (128, 64) -> (64, 32) -> (32, 8) -> (8,1)
    • 差,虽然收敛,但是效果不如 base
  • (input, 64) -> (64, 32) -> (32, 8) -> (8, 1),其他不变
    • 差,难以收敛
  • (input, 64) -> (64, 32) -> (32, 16) -> (16, 8) -> (8, 1),其他不变
    • 虽然没有提交,但是相比上一个层数比较浅的方案,验证集上出现了明显的过拟合 - 测试集误差更低但是预测集上误差反倒更高
  • (input, 64) -> (64, 32) -> (32, 16) -> (16, 1),其他不变
    • 训练和测试的结果都很差
  • (input, 64) -> (64, 16) -> (16, 8) -> (8, 1),其他不变
    • 这个模型,从数据集的训练上看,无论是训练过程还是测试过程,都表现很好。
    • 但在结果提交之后,排行榜的表现都很差,可以下结论,是在整体训练数据上过拟合了。
  • (input, 64) -> (64, 16) -> (16, 1)
    • 从别人的作业 2 学来的
    • private - 2.308,public - 2.405
    • 远不如我丢的那个!!!
  • (input, 32) -> (32, 16) -> (16, 8) -> (8, 1),epochs - 10000
    • 大概在 3100 停止,效果很差很差!

选择了 64 之后,进行了特征数量的尝试,发现 16 是个不错的特征数量 (超参数),进而尝试了 (32, 16) -> (16, 8) -> (8, 1),比 64 的情景好一些。

超参数的迭代是不间断的、反复的。

特征选择数量

  • 特征数量从 64 - 32 看到了训练和验证误差的下降,但是从 32 - 16 时,训练和验证误差都有所上升。如果从这个结果上看,似乎是特征 32 更加合适,可是当把 pred 上传测试发现,分数仍然还在下降!

优化器

  • 最开始从 SGD 换到 Adam,共享同一个 lr 的时候,训练效果甚至下降
    • 从曲线上看,收敛速度太慢
  • 调快了 lr 从 1e-51e-3 之后,训练效果有显著提升,达到新高
  • early stop 参数,从 400 到 2000 之后,明显观察到训练误差继续降低,但是验证误差有所反弹,pred 的分数也确实证明效果有所下降,这应该就是某种过拟合
    • 1000 也试了一下,还是过拟合

一些思考

  • 其实 epochs 的数目不是那么重要,尤其是设置了停止条件的时候
  • 丢了一个效果特别好的模型,得做好记录和备份!!!
  • 当看到训练因为达到最大 epoch 而停止时,应该加大 epoch,因为他没训练完,还能继续优化!
  • 花里胡哨的 tqdm 每 500 或者更多 epoch 输出一次就挺好,还能给整个程序提提速,明显 500 epoch 每输出的,要比以前每次都输出的快的多。

补充学习

Backpropagation

在学习 torch 入门文档 时,代码用 numpy 显式计算了梯度并更新参数,是个很好的学习梯度计算细节的例子。

有 $y = a + bx + cx^2 + dx^3$,当 $\text{loss} = \sum_{i=1}^{n}(\hat{y_i} - y_i)^2$ 时,计算每个参数的梯度来进行参数更新。

对于每个数据点 $\hat{y_i}$,分别计算 $\frac{\partial \text{loss}}{\partial a}$, $\frac{\partial \text{loss}}{\partial b}$, $\ldots$ 根据链式法则有:

$$\frac{\partial \text{loss}}{\partial a} = \sum_{i=1}^{n}\frac{\partial \text{loss}}{\partial \hat{y_i}}\frac{\partial \hat{y_i}}{\partial a}$$

其中 $\frac{\partial \text{loss}}{\partial \hat{y_i}} = 2(\hat{y_i} - y_i)$。

进一步有:$\frac{\partial \hat{y_i}}{\partial a} = 1$,所以:

$$\frac{\partial \text{loss}}{\partial a} = \sum_{i=1}^{n} 2(\hat{y_i} - y_i) \times 1$$

即 $\text{grad}_a$,那么参数更新为:$a^* = a - \text{learning rate} \times \text{grad}_a$

类似地,我们有:

  • $\frac{\partial \hat{y_i}}{\partial b} = x_i$
  • $\frac{\partial \hat{y_i}}{\partial c} = x_i^2$
  • $\frac{\partial \hat{y_i}}{\partial d} = x_i^3$

故 $\frac{\partial \text{loss}}{\partial d} = \sum_{i=1}^{n} 2(\hat{y_i} - y_i) x_i^3$,其他参数更新类似。

特征选择

sk-learn 的关于特征选择的 文档 提及了很多内容。

按照参考 2 的方向,选择使用 基于单变量统计方法的特征选择。学过统计学之后,非常的好懂。根据分数最高的进行选择,分数的计算对于回归和分类问题有所不同,通过 K 个最高分、分位数亦或者其他正确率指标,也有问题本身有所关联。

mini-batch gradient descent 3

吴恩达讲的真好,以前真没看出来

Mini batch 是区别于 batch 的,其核心在于,不一次性考虑全部样本,而只是考虑一个 batch 的 gradient 进行更新参数。

假设我们有 5,000,000 个样本,其中 $X \in \mathbb{R}^{n_x \times 5000000}$,$Y \in \mathbb{R}^{1 \times 5000000}$,具体来说就是 $X = [X^{(1)}, X^{(2)}, \ldots, X^{(5000000)}]$,取 1000 个样本作为一个 batch,那么就有:

$$X^{\{1\}} = [X^{(1)}, X^{(2)}, \ldots, X^{(1000)}] \in \mathbb{R}^{n_x \times 1000}$$

$Y^{\{1\}}$ 同理,共有 5,000 对 $(X^{\{t\}}, Y^{\{t\}})$。

其中

  • $x^{(i)}$ 表示第 $i$ 个训练样本
  • $z^{[l]}$ 表示神经网络第 $l$ 层的输出
  • $X^{\{t\}}, Y^{\{t\}}$ 表示第 $t$ 个 batch

整个算法的流程如下:

for $t = 1, 2, \ldots, 5000$:

  1. 前向传播 $X^{\{t\}}$:

    • $Z^{[l]} = W^{[l]} X^{\{t\}} + b^{[l]}$
    • $A^{[l]} = g^{[l]}(Z^{[l]})$,其中 $g^{[l]}$ 为对应层的激活函数
    • 这个计算过程应该是向量化的,同时计算 1000 个样本
  2. 计算损失函数(考虑 L2 正则化):

    $$J^{\{t\}} = \frac{1}{1000} \sum_{i=1}^{1000} l(\hat{y}^{(i)}, y^{(i)}) + \frac{\lambda}{2 \times 1000} \sum_{l} \|W^{[l]}\|_F^2$$

    注意:这个损失函数只针对第 $t$ 个 batch

  3. 反向传播计算 $J^{\{t\}}$ 的梯度并更新参数:

    • $W^{[l]} = W^{[l]} - \alpha \frac{\partial J^{\{t\}}}{\partial W^{[l]}}$
    • $b^{[l]} = b^{[l]} - \alpha \frac{\partial J^{\{t\}}}{\partial b^{[l]}}$

对所有 batch 都更新一遍,这叫一个 epoch(对训练集的单次完整遍历)。

分成 mini-batch 之后,$\text{loss}^{\{t\}}$ 与 $t$ (mini batch #) 的示意图不再如理想的梯度下降中 loss 与 $t$ (# iteration) 那般,一直下降,它可能是震荡的,但趋势是持续下降的。震荡的原因,是因为每个小 batch 的拟合难度不同,所以他们对应的 loss 有所不同。

对于不同的 batch size,形成了不同的更新参数的思路 4。当 batch size 为 m,即所有训练样本都放入一个 batch 时,这就是 batch/ full gradient descent;当 batch size 为 1 时,即每次只看到一个训练样本,根据这一个样本的情况去更新参数,这就是 stochastic gradient descent;当 batch size 在 1 与 m 之间时,就是我们所常用的、讨论的 mini-batch gradient descent。

需要额外声明,这种 full、mini 与 stochastic 的区别,是一种关于 batch size 不同设置结果的讨论,与 torch.optim.SGD 不是同一个东西。SGD 的思路 5 是用一个 batch 里随机的一个损失函数 $f_i$ 求梯度作为参考进行更新参数,而不是 gradient descent 时参考全部样本的损失函数 $\frac{1}{n}\sum_{i=1}^{n}f_i$ 求梯度进行参数更新。当 mini-batch 与 SGD 结合时,即最常见的情况,是 mini batch stochastic gradient descent 6

L2 正则化

L2 regularization / weight decay

Frobenius 范数/norm:$\|A\|_F = \sqrt{\sum_{i=1}^{m}\sum_{j=1}^{n}|a_{ij}|^2}$,是矩阵所有元素的平方根。灵感来源于 RMS7,乍一看很像 L2 norm 或者 2-norm,但其实并不能完全认为二者等价 8

L2 正则化就是在损失函数中添加惩罚项去抑制大的权重,其通常的格式为:

$$\frac{\lambda}{2} \|W\|_F^2 = \frac{\lambda}{2} \sum_{i,j} w_{ij}^2$$

其中 $\lambda$ 为控制正则化强度的超参数。

所以,带 L2 regularization 的损失函数就是原来的损失函数加上 $\frac{\lambda}{2} \|W\|_F^2$。

在 Adam 中只要设置 weight_decay,添加了正则项之后,发现预测和测试误差有所减小的同时,训练所需轮次增加。

Learning Rate Schedulers

从大哥的答案 2 中发现的新东西

利用 CosineAnnealingWarmRestarts 余弦退火调节学习率,其思路就是像余弦曲线一样控制学习率的上下波动,通过手动设置最小值 lr、最大值 lr、首个周期长度和重启周期的乘子来进行 lr 的自动调节。

参数设置:

  • $T_0 \approx$ 总训练 epoch 的 10% – 20%
  • $T_{\text{mult}}$ 可以为 2 ➔ 周期指数增长:2, 4, 8, …(最常用)
  • $\eta_{\text{min}}$ - 周期最低学习率,设为 $\text{base lr} \times 10^{-2}$ 到 $10^{-3}$ 最常见

Reference