[深度学习基础] 5. 实现细节

来源:互联网 发布:c盘中的windows文件夹 编辑:程序博客网 时间:2024/06/01 21:08

在做工程实现时, 除了理论推导要正确外, 还要考虑诸如计算机的数值稳定性, 计算速度, 收敛快慢等问题. 本章将就一些实现细节问题加以讨论.


1 Softmax 的数值稳定性问题

在计算 softmax 函数


时, 如果不注意细节, 很容易造成数值稳定性问题.


当 s_j = c, ∀j 时, 理论上


但在实际中考虑两种情况:


(1). c 是一个非常大的负数. exp(c) 会是一个非常接近 0 的数. 由于计算机能表示的浮点数精度是有限的, 非常接近 0 的数会近似为 0, 这是数值下溢 (underflow). 因此



(2). c 是一个非常大的正数. exp(c) 会是一个非常大的数. 由于计算机的有限的计算精度, 非常大的数字会被近似为 ∞, 这是数值上溢 (overflow). 因此



注意到 softmax 函数满足性质


即在分子分母同乘以一个常数不会影响 softmax 的结果, 但是可以利用这个性质来解决数值稳定性问题.




即在分子分母的指数上同时减去 s 最大分量, 使得在指数上最大的一项为 0, 这去掉了发生上溢的可能性. 同样的, 分母中至少有一项为 1, 这去掉了因为下溢而导致分子分母同时为 0 的可能性.


2 卷积操作的实现

在卷积层中需要完成操作


如果直接按上式在计算机中实现, 上式 3 重求和对应 3 重 for 循环, 对 i_l , j_l , d_l 遍历又需要 3 重 for 循环, 而且通常我们进行批梯度下降, 对批数据中每个训练示例遍历也需要 1 重 for 循环, 共计需要 7 重 for 循环. 7 重for 循环的计算效率很低, 即使使用向量化技巧去掉 3 重求和对应的 3 重for 循环, 还需要 4 重 for 循环, 这个计算代价仍然很高. 因此, 需要更加高效的方法计算卷积. 大致有以下两种方法.


2.1 傅里叶变换

根据傅里叶变换 (Fourier transform) 的性质, f ∗ g 的傅里叶变换等于 f 的傅里叶变换乘 g 的傅里叶变换


这样可以将卷积运算转化为普通的乘法运算.


在卷积层内实现的卷积操作是


利用傅里叶变换, 可以写成



通过快速傅里叶变换 (fast Fourier transform, FFT), 可以在 O(N lg N )的时间内计算完长 N 向量的傅里叶变换, FFT 也可以扩展到 2 维矩阵中. 但是利用 FFT 计算卷积在滤波器尺寸大的时候有很大加速, 对于通常使用的 3 × 3 滤波器加速不明显. 而且 FFT 无法很高效的处理步幅 S > 1 的情况, 因此在实际上使用的并不多.


2.2 im2col

考虑到矩阵的乘法运算在计算机中已经有非常高效的实现方法, im2col 的思路是将卷积运算转化为普通的矩阵乘法运算. 方法如下: 


第一步将和每个输出神经元相连的输入张量中的局部区域展成一个行向量, 将所有的向量拼接成一个矩阵 X̃. 每个局部区域的维度是 F 1 × F 2 × D l−1 , 共有 H l × W l 个这样的区域, 因此 X̃ 的维度是 (H l W l ) × (F 1 F 2 D l−1 ).


X 和 X̃ 之间的对应关系可由如下方法算出 [35]: 用 (p, q) 索引 X̃ 中 的元素, 用 (i, j, d, d l ) 索引 W 中的元素, 用 (i l , j l , d l ) 索引 A 中的元素, 用 (i l−1 , j l−1 , d l−1 ) 索引 X 中的元素, 则有关系



p 决定了对应 A 中的空间位置 (i_l , j_l ), q 决定了对应 X 中的深度位置 d l−1和空间位移 (i, j), . 将 (i, j) 和 (i_l , j_l ) 结合起来, 就可以得到对应 X 中的空间位置 (i_{l−1} , j_{l−1} ).


第二步将每个滤波器 W_{d_l} 展成列向量, 将所有的列向量拼接成一个矩阵 W̃ . 每个滤波器的维度是 F_1 × F_2 × D_{l−1} , 共有 D_l 个滤波器, 因此 W̃ 的维度是 (F_1 F_2 D_{l−1} ) × D_l .


第三步将 X̃ 和 W̃ 相乘, 再加上 ⃗b.


à 的维度将是 (H_l W_l ) × D_l .


第四步将 Ã 转为张量, 即可得到 A ∈ R^{H_l ×W_l ×D_l }. 这种方法的弊端是 X̃ 会占用很大的内存, 因为 X 中的每个元素会在 X̃ 中多次出现. 但是 im2col 利用了矩阵运算的高效实现, 因此实际中使用的比较多. 卷积层的前向, 反向, 汇合操作也可以用 im2col 实现.


3 参数更新

前面讨论的参数更新方法都是梯度下降


事实上, 还有很多基于梯度下降的方法, 但是在深度网络中有着更好的收敛速率.


在梯度下降方法中, 确定合适的超参数 (hyperparameter)α 非常困难, 学习速率 α 选择的好坏直接影响到网络收敛情况及网络的性能. 我们希望有一种方法可以自适应的调整学习速率, 甚至对 θ ⃗ 中的每个参数 θ_j 有各自的学习速率. RMSprop[33] 就是这样一种方法, 它的参数更新方法如下


⃗v 是缓存变量, 和 θ ⃗ 的大小相同, 用来记录导数的平方. 通过 ⃗v 调整各参数的学习速率. 如果某个参数持续的得到高的梯度, 它的学习速率将降低; 如果某个参数持续得到低的梯度, 它的学习速率将增大. β 通常被设为 β ∈ {0.9, 0.99, 0.999}. ε 通常设定为 ε ∈ [10 −8 , 10 −4 ], 用于避免除 0 错误.


除 RMSprop 外, 还有很多常用基于梯度下降的参数更新规则, 比如Momentum[27], Nesterov Momentum[24], Adagrad[4], Adam[16].


4 退出

退出 (dropout)[30] 是一种非常简单而高效的正则化方法. 它以 p 的概率随机保留一些神经元, 其余的神经元输出将被设为 0, 见图. 这可以认为是在训练过程中从原始网络中提取一个小的网络, 参数更新只在这个小的网络中进行. 在测试过程中不适用退出, 这可以认为是无数小的网络所做的预测的集成 (ensemble).



5 数据初始化

一个常用的数据初始化策略是, 对训练集中的每个特征, 减去它在训练集中的均值.



6 参数初始化
输入数据在初始化后是以 0 为中心的, 因此我们希望参数一部分大于 0, 一部分小于 0.


但是如果我们将全部的参数初始化为 0, 那么一层中所有的神经元将计算得到相同的输出, 在反向传播时将得到相同的导数, 参数将进行相同的更新, 也就是说, 这会导致一层中所有的神经元是冗余的.


因此, 我们将参数初始为很接近 0 但不全是 0 的数, 比如从高斯分布N (0, 0.01 2 ) 中采样作为初始值.

7 随机裁剪
很多情况下, 训练数据越多, 模型的性能越好. 随机裁剪就是一种数据扩充 (data augmentation) 的方法. 在训练时, 随机在原图中裁剪一些区域, 将这些区域送给神经网络去训练. 在测试时, 取图像中的 5 个固定位置 (4 个角落 + 中央) 让神经网络预测, 将预测的结果平均之后作为最终的输出.