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

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-5 到 1e-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^{2}_{i}$
  • $\frac{\partial \hat{y_i}}{\partial d}=x^{3}_{i}$

故 $\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]}|^{2}_{F}$$ 注意:这个损失函数只针对第 $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 之后,cost^{t} 与 t(mini batch #) 的示意图不再如理想的梯度下降中 cost 与 t(# iteration) 那般,一直下降,它可能是震荡的,但趋势是持续下降的。震荡的原因,是因为每个小 batch 的拟合难度不同,所以他们对应的 cost 有所不同。

对于不同的 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.SDG 不是同一个东西。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:$\lVert A \rVert_{\mathrm{F}}=\sqrt{\sum_{i=1}^{m}\sum_{j=1}^{n}|a_{ij}|^{2}}$,是矩阵所有元素的平方根。灵感来源于 RMS7,乍一看很像 L2 norm 或者 2-norm,但其实并不能完全认为二者等价 8

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

$$ \frac{\lambda}{2} \lVert W \rVert_{\mathrm{F}}^2 = \frac{\lambda}{2} \sum_{i,j} w_{ij}^2 $$

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

所以,带 L2 regularization 的损失函数就是原来的损失函数加上 $\frac{\lambda}{2} \lVert W \rVert_{\mathrm{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