本篇记录了 Hylee 2023 年 HW01 回归任务的实验过程与调参心得,内容包括神经网络结构设计、特征变量选择、优化器与正则化方法的尝试,以及相关的学习笔记和思考,适合关注深度学习实战与模型调优的同学参考。
作业的目标,是个看似简单的回归问题。但难点在于,它给出的 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$:
前向传播 $X^{{t}}$:
- $Z^{[l]} = W^{[l]}X^{{t}} + b^{[l]}$
- $A^{[l]} = g^{[l]}(Z^{[l]})$,其中 $g^{[l]}$ 为对应层的激活函数
- 这个计算过程应该是向量化的,同时计算 1000 个样本
计算损失函数(考虑 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
反向传播计算 $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
https://colab.research.google.com/drive/1Qi4-BRqZ3qI3x_Jtr5ci_oRvHDMQpdiW?usp=sharing ↩︎
https://github.com/sotaBrewer824/LHY_MLDL/blob/main/hw01/hw01.ipynb ↩︎ ↩︎ ↩︎ ↩︎
https://www.youtube.com/watch?v=4qJaSmvhxi8&list=PLkDaE6sCZn6Hn0vK8co82zjQtt3T2Nkqc&index=15 ↩︎
https://www.youtube.com/watch?v=-_4Zi8fCZO4&list=PLkDaE6sCZn6Hn0vK8co82zjQtt3T2Nkqc&index=16 ↩︎
https://machinelearningreference.com/10-foundations/03-linear-algebra/30-operations/Frobenius-norm ↩︎