0%

深度学习框架

网络结构

  • one-hot: 对离散的类别数据进行编码, 如词元、标签等。通过编码可以将所有类别一视同仁, 而避免造成label=5的标签比label=1的标签更重要。
  • embedding: 对词元进行one-hot编码之后,通常维度都会成千上万,并且除了一个维度是1以为,其他维度全都是0,编码很稀疏,效率很低。为了压缩维度使用embedding层进行维度压缩。
  • 残差层: 用于在网络F的基础上构造残差连接, 如ReLU、GELU。在激活函数之前。
  • Batch Normalization: 每层网络运算之后的归一化。在网络计算与激活层之间。
  • 激活层: 在两层神经网络之间,切断两层的线性联系,使网络组合可以变成非线性网络。

激活函数

原理讲解

Sigmoid(Logistic/Tanh)

  • 有输出概率的实际意义, 导数易求。
  • 但是计算需要指数运算, 效率低。而且梯度最大值≤0.25, 在链式传播乘起来后会越来越小, 最终网络一多就会造成梯度消失。另外Logistic均值不为0, 会影响一点输入数据的均值分布, Tanh则不会。

ReLU/Leaky ReLU

\[ ReLU(z) = max(0,z) = \begin{cases}z, & z \geq 0 \\\\0, & z < 0\end{cases} \]

  • 正常的反向导数恒等于1, 收敛快。无论链式传播多少层梯度都为1, 不存在梯度消失。且计算简单, 并且会造成神经元的稀疏性, 降低计算成本。
  • 输出无界==>输出可能很大==>损失很大==>梯度很大==>梯度下降造成神经元参数W和b小于0==>激活前的神经元输入小于0, 激活时ReLU梯度恒为0, 即ReLU神经元死亡。
  • 在RNN中难以使用ReLU==RNN时间步中共享W参数矩阵, 相当于会对W做连乘, 且ReLU不抑制输出, 最终输出结果很大。除非W初始化为I。

ReLU不能接受小于0的输入, 不代表原始输入数据不能小于0, 因为原始数据起码也在一层WX+b之后才会进入ReLU。而初始化的W是0对称分布, b则是恒为0, 因此会将输入数据变成有正有负, 不用担心。

Leaky ReLU: 在ReLU的基础上, 给小于0的部分泄漏了一点点梯度,避免神经元死亡

\[ LReLU(z) = \begin{cases} z & z \geq 0 \\\\ \alpha \cdot z & z < 0 \end{cases} \]

Batch Normalization

网络每一层的计算都会使输入数据的分布发生一点变化,变化随着层数放大。因此最后训练中间层数据分布可能已经不是原始数据分布了,BN(BatchNormalization)就是为了解决这种分布变化,从而获得以下优点:

  • 可以选择比较大的初始学习率,让你的训练速度提高
  • 减少对初始化的依赖
  • 减少对正则的依赖

使用方法:nn.BatchNorm1d/2d/3d(num_features)

  • 全连接层:置于全连接计算和激活层之间
  • 卷积层:分通道处理,一个通道一个批量归一化
  • 测试和预测时:由于只有一个样本不能批量归一化,因此可以采用训练时的全局均值来做归一化。在pytorch中把模型置为eval会自动做好。

详细原理:BN即是在每一层都把数据分布归一化到0点附近,使得每一层网络都有着相同分布的数据。具体做法:

  1. 先对方差、均值归一化,将数据分布移动0点附近:仅这一步还不够,由于区间在[-1,1],可能会减弱激活函数效果,另外压缩了网络的学习空间,可能会破坏网络学习效果。
  2. 通过可学习的参数 γ,β,对数据进行可学习的线性变换来适当拉宽数据分布,解决上述问题。

\[ \mu_B = \frac{1}{m}\sum_1^m x_i\\ \sigma^2_B = \frac{1}{m} \sum_1^m (x_i-\mu_B)^2\\ n_i = \frac{x_i-\mu_B}{\sqrt{\sigma^2_B + \epsilon}} \\ z_i = \gamma n_i + \beta \]

优化

网络参数

参数初始化

在自己的init_func中利用nn.init模块对不同的层定义不同的初始化方式。然后用net.apply(init_func)将函数应用到每一层。或者net[i].apply(init_func)单独应用:

  • 线性激活:Xavier,前提激活函数在零点附近线性,如tanh等。而ReLU不满足。
  • ReLU:He,解决ReLU不能使用Xavier的问题。

详细介绍初始化方法

参数更新器

SGD:随机梯度下降

  • \(\eta\):全局学习率
  • 收敛慢,在终点容易错左右跳,可能陷入局部最优。

Momentum:更新时在一定程度上保留之前的方向,再通过当前梯度进行微调方向,即带有一定的惯性

  • \(\eta\):全局学习率;\(\alpha\):动量系数;\(v_t\):初始方向
  • 增加稳定性学习更快,有一定的摆脱局部最优能力。
Momentum

NAG:Nesterov Momentum/Nesterov Accelerated Gradient 梯度加速算法。同Momentum,已知上一步的更新方向,那么在这次确定方向之前,先按之前的方向走一步,然后在新位置上求梯度,再用这个 未来的梯度 在当前位置进行Momentum的方向计算。

换句话说,就是把Momentum中计算当前梯度的步骤延后,先走一步看看,计算梯度,看看路顺不顺,再回到原位置决定要怎么走。

  • \(\eta\):全局学习率;\(\alpha\):动量系数;\(v_t\):初始方向
  • 相比于在原位置做方向的微调,NAG是先探好前方的路,再决定现在怎么走。因此更具有预见能力,未来的路更顺畅则加快速度,未来的路不好走则提前改变方向。
Nesterov Momentum

以下是自适应更新器:在同一步更新中,不同参数的更新步长不一样,自适应针对性调整,每个参数有自己的学习率。

AdaGrad: 学习中会累计之前所有梯度平方之和作为学习率的分母,也就是更新步长会越来越小。同一步更新中,小梯度的参数更新会较大。

  • \(\eta\):全局学习率;\(\epsilon=1e-6\):数值稳定的小常数;\(r=0\):累积辅助系数,不用管

AdaDelta:解决AdaGrad更新步长单调减小的问题,过去累计的梯度会不断衰减,减小影响。

  • \(\eta\):全局学习率;\(\alpha=0.9 \in [0,1)\):衰减比率;\(s=0\): 平方梯度累计,不用管;\(r=0\):累积辅助系数,不用管。

RMSProp:Root Mean Square Prop 均方根反向传播。优化了AdaGrad在更新中摆幅过大的问题,学习率衰减不会太快,加快收敛速度,将梯度平方累计改成了加权平均累计。

  • \(\eta=0.01\):全局学习率;\(\epsilon=1e-6\):数值稳定的小常数;\(\alpha=0.9 \in [0,1)\):加权比例;\(r=0\):累积辅助系数,\(r=\alpha r + (1-\alpha)(g_t \odot g_t )\)
  • 初始学习率不能太大,容易不收敛。

Adam:Adaptive Moment Estimation 主流选择。相当于RMSProp+Momentum。Adam在RMSProp算法基础上对小批量随机梯度也做了指数加权移动平均。

  • \(\eta=0.01\):全局学习率,\(\epsilon\):数值稳定小常数;\(\beta_1=0.9,\beta_2=0.999\);矩估计指数衰减比率;
主流更新算法比较:Momentum有惯性,比较平滑;RMSProp波动幅度小,后期步长越来越短;Adam结合两者优势,比Momentum幅度小,比RMSProp平滑

AIEDU-自适应更新算法

学习率调度器

学习率调度器用于随着epoch改变学习率,通常我们希望lr随着epoch越来越小。通常的参数有:

  • optimizer: 要修改学习率的更新器
  • step_size: 改变学习率的step周期。通常每个epoch之后调度器step一次,达到step_size则会改变学习率
  • last_epoch: 中断训练再继续的时候使用,告诉调度器上一次到达了多少epoch。

StepLR: 等间隔调整

CosineAnnealingLR:类似余弦的调度,学习率衰减先慢后快。torch.optim.lr_scheduler.

余弦学习率变化

另外学习率应该匹配批大小。通常我们希望经过同样数量的样本后,更新的步伐是一样的。因此增大了batch理论上应该增加lr。换个思路来说,衰减学习率也可以通过增加batch size来实现。

  • 增加 batch size,需要增加学习率来适应,可以用线性缩放的规则,成比例放大
  • 尽量使用大的学习率,因为很多研究都表明更大的学习率有利于提高泛化能力。如果真的要衰减,可以尝试其他办法,比如增加batch size,学习率对模型的收敛影响真的很大,慎重调整。
  • batch size增加比较自由,但是大到一定程度的时候,学习率不能随之增加了,因为学习率有上限限制。

在SGD中,学习率不能大于1,这是收敛必要条件。

学习率与批大小关系

过拟合

为了减轻过拟合,一定程度上限制权重的学习。weight_decay一般可以取0.005

L2正则

限制权重L2范数大小,通常小的模型适应能力较强。一般不会正则bias,bias对学习影响不大,没有什么明显效果。

\[ J(w,b)= J_{old}(w,b)+\frac{\lambda}{2m}\sum_{j=1}^n{w_j^2} \]

L1正则

使权重稀疏(即趋近于0的权重很多)。L0和L1都可以实现稀疏,但L0无法优化求解。权重稀疏的好处是可以实现 特征选择可解释性

\[ J(w,b)= J_{old}(w,b)+\lambda\sum_{j}^m{|w_j|} \]

Dropout=0.5 / 0.7

避免过分依赖某一层中某些神经元发生过拟合,提高泛化。通过mask随机丢弃即可。

预测和测试时不能dropout,要不然会造成结果的不稳定,无法解释。

DropAttention

用于正则化Transformer网络的注意力权重,(Zehui et al. 2019)

Utils

Pytorch模型管理

save(obj,path) load(path) :可以存储大多数类型,甚至包括字典。对于tensor之类的可以直接save,对于网络模型,一般save(net.state_dict()),即状态参数信息。

  • 保存模型: torch.save(myNet.state_dict(),save_path)
  • 读取模型: myNet.load_state_dict(torch.load(save_path))
  • 模型状态: myNet.train(),myNet.eval()。使用不同的模型状态。主要改变模型中BatchNormalization和Dropout的作用方式。和梯度记录无关,那是另一回事。
  • 梯度记录管理: 使用上下文with torch.no_grad(),或者使用装饰器@torch.no_grad()装饰需要禁用梯度的函数,影响范围内代码都不会记录梯度状态,不影响后续梯度更新。

也可以直接save(myNet),但是网络结构变化的时候会出问题

Pytorch 设备管理

假如要对多个项同时操作,它们必须在同样的设备上。但是跨设备传输比计算慢得多,因此要谨慎操作。

且打印张量或将张量转换为NumPy格式时,如果数据不在内存中,框架会首先将其复制到内存中。

一个典型的错误如下:计算GPU上每个小批量的损失,并在命令行中将其报告给用户(或将其记录在NumPy ndarray中)时,将触发全局解释器锁,从而使所有GPU阻塞。

  • 返回可用设备: torch.device('cpu'), torch.cuda.device('cuda'), torch.cuda.device('cuda:1')
  • 查询可用gpu的数量: torch.cuda.device_count()
  • 查询张量所在设备: x.device
  • 转移设备: MyNet.to(device),MyTensor.to(device)

Hydra 程序参数管理

Hydra通过yaml文件管理python程序的参数。比如argparse方便的是, 它可以创建多个yaml文件, 并且自由组合生成一套最终配置。例如在深度学习里面, 可以把模型参数, 数据集参数, 超参数分成三个yaml文件, 然后可以混搭使用, 省心省事炼丹。

基础使用方法参考官方小教程。值得注意的关键字:

  • # @package _global_ : 放在yaml文件头, 可以使该文件所有配置放在最外层
  • subpackage@newname : defaults列表中使用@可以重命名
  • subpackage@_here_ : defaults列表中使用_here_可以原地展开subpackage的配置, 不再嵌套在subpackage.somekeyword里面

注意Hydra会修改程序的执行路径至hydra的output路径,因此你程序里所有关于相对路径的设置都可能出现问题。参考官方说明

Profile 性能分析

pytorch自带的性能分析工具, 可以分析内存使用, 耗时情况等性能数据。并且能够集成在tensorboard中查看, 可以说是很方便的模型性能分析工具了。官方文档

使用方法如下, 需要将被分析的代码放在profile的上下文中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
with torch.profiler.profile(
schedule=torch.profiler.schedule(
skip_first=1,
wait=1,
warmup=1,
active=2
),
on_trace_ready=torch.profiler.tensorboard_trace_handler('./logs'),
profile_memory=True,
record_shapes=True,
with_stack=True
) as prof:
for X,y in data:
your code

prof.step()

需要注意的参数如下:

  • schedule : torch.profiler.schedule类型。按时间步长去规定profiler做什么事。具体根据参数情况profiler将跳过skip_first个步长,然后等待 wait 个步长, 在接下来的 warmup 个步长中进行warmup以便分析更准确。最后真正开始分析接下来的 active 个步长的性能, 这是一轮分析的结束。之后回到wait处进行上述循环, 循环次数由 repeat 决定, 为0则意味着持续循环。

  • on_trace_ready: 当一轮分析完毕的回调函数, 默认传递prof变量。

    例如回调函数里可以写下面这句用于打印分析表格

    1
    print(prof.key_averages().table(sort_by="self_cuda_time_total", row_limit=-1))

    或者可以直接传递内置函数, 用于生成tensorboard文件用于可视化分析。

    1
    torch.profiler.tensorboard_trace_handler('./logs')

  • step(): 用于迭代profiler的时间步长(需要手动调用)。

  • with_stack: 记录某个操作的文件名和行号。

坑位排除

  1. 网络中使用了softmax而不是log_softmax:

softmax存在数值上的问题,可能导致更新极度缓慢,参见

训练时应该使用数值稳定的log_softmax代替,torch中之所以还保留softmax函数,是为了方便我们想要查看真正的softmax概率时使用。

  1. 网络计算中tensor不能使用原地操作,只能创建新对象然后赋值.

由于梯度更新的要求,例如x+=1这样的原地操作是不行的。同时tensor的切片赋值也是不行的。

例如形状为2,3的x,希望使x[1,:]=0,x[2,:]=0。此时需要创建一个形状为[2,3]的y,然后进行赋值x=y


主函数代码框架见github

参考资料

微软:神经网络基本原理