easytorch
使用 Python 的 numpy 实现的简易深度学习框架,API 与 pytorch 基本相同,实现了自动求导、基础优化器、layer 等。
功能实现:
一、自动求导基础运算实现
这部分是自动求导的基础运算实现,注意下面的推导都是使用
微分
进行推导,直接使用导数推导会出现很多问题,比如矩阵求导时链式法则不成立、求得的导数结果为四维张量等等,使用微分就比较合适。具体实现代码见
tensor.py
。
1. 基础运算实现
Add,Sub,Mul,Divide, Pow
最基础的四则运算,共同特点是都是逐元素运算,所以求导都十分简单,与标量形式基本相同,麻烦的是要注意考虑 broadcast 的问题。
- $Z = X + Y$,则$dZ = dX + dY$,数学上梯度为单位矩阵,十分简单。比较复杂的是计算机实现中要考虑到向量的 broadcast 的问题,broadcast 分为两种情况:第一种是其中一个矩阵的维度小于另一个,比如$X=[[1, 2], [3, 4]]$,$Y = [1, 2]$,那么在对$Y$求微分时,需要将多余的维度进行$sum$操作;另一种情况是两个的维度相等,但其中一个的某些维度形状为 1,比如$X=[[1, 2], [3, 4]]$,$Y = [[1, 2]]$中,$shape(X) = (2, 2)$,$shape(Y)=(1, 2)$,需要对$shape$为 1 的维度做$sum$操作。
- Sub 可以直接使用 Add 实现。
- 逐元素乘法,$Z = X\odot Y$。
- 逐元素除法,$Z = X / Y$。
- Pow 操作也是逐元素操作,求导比较简单。
Sum, Mean
都是将矩阵的某一个维度压缩,所以梯度是将当前的梯度广播到原来的 size,二者的差别只在于是否需要乘一个常数项。
Matmul
矩阵乘法或叫矩阵内积,$Z = XY$,对$X$的微分为$dZ = dXY$,所以梯度为$Y^T$,而且要注意左乘右乘的顺序。
一种特殊情况是两个行向量做内积操作时,会自动将第二个行向量转化为列向量,得到一个标量的结果。比如下面的操作也是合法的,会得到 5。但目前我还没有想到怎么处理。
python
a = Tensor([1., 2.], requires_grad=True)
b = Tensor([1., 2.], requires_grad=True)
c = a @ b
reshape, getitem
这两个操作都是对矩阵的元素的重新排列,在数学上是完全无法求梯度的,反向传播的实现中只需要记录梯度,保持梯度与原数据的位置对应。
另外实现要注意这两个操作生成的新向量的数据与原向量的数据是相同的,是浅拷贝。
Tanh, Relu
这两个激活函数是逐元素函数$\sigma$,设为$y = \sigma(x)$,$dy = \sigma '(x) \odot dx = diag(\sigma '(x)) dx$,第一个等号是通过逐元素操作计算,实现更加简单,但这种操作数学上好像是不存在的,第二个等号是通过矩阵计算,是数学上的正确的形式,但计算机实现会稍微麻烦一点。
Tanh 求导:$tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}$,然后进行求导。
Relu 求导:relu 的问题是在$x=0$处不可导,此时需要使用次梯度$c \leq \frac{f(y) - f(x)}{y - x}$,通常取$c=0$(好像一般倾向取 0,使得计算方便、带来更多的稀疏性)。
Softmax
Softmax 的公式为$\frac{e^x}{\sum e^x}$,但这个公式在$x$非常小时,会出现下溢的情况,导致分母为 0,所以在实现时,通常会减去最大值$m$,即$\frac{e^{x - m}}{\sum e^{x - m}}$,这样在$x_i=m$时,$e^{x_i-m}$为 1,分母一定大于等于 1,从而避免了下溢出的问题。
求导暂时没有解决,和 pytorch 的梯度不同。
Abs
求导为 sign 函数,在$x=0$处不可导,需要使用次梯度。
2. 实现总结
这部分主要是总结理论上不需要考虑,但实际实现时需要考虑的问题。
- Broadcast 问题(详见 Add、Sub、Mul、Divide 的实现)
-
两个操作数为同一个对象时,该怎么处理(见
tensor.py:Tensor/backward
的实现)
3. 测试
代码中加入 assert
backward
操作后,得到的
grad
和
data
的 shape 是相同的。
对拍测试
未测试的代码永远是错的
。因为 API 和 pytorch 完全相同,所以采用和 pytorch 对拍的方式测试正确性,在 backward 后比较叶节点的
grad
。
二、优化器
迭代优化算法的基本框架如下:
- 计算目标函数对当前参数的梯度$g_t = \nabla f(\omega _t)$
- 更新历史的一阶动量和二阶动量$m_t$, $V_t$
- 使用$m_t$控制更新的方向,用$V_t$控制更新的步长,计算当前的下降梯度$\eta_t = \alpha \frac{m_t}{\sqrt{V_t}}$
- 使用梯度更新$\omega_{t+1} = \omega_t - \eta_t$
不同的优化器就是在第二步中不同。
具体代码见
optim.py
。
SGD
SGD 第二行更新为$m_{t} = g_t$,$V_t = 1$。
优化公式为$\omega_{t+1} = \omega_t - \alpha * g_t$
Adagrad(Adaptive gradient)
Adagrad 第二行更新为$m_{t} = g_{t}$,$V_{t} = V_{t-1} + g_t \odot g_t$加入了自适应的步长,通过累加$V_t$的方式,使得更新梯度$g_t$较大的,更新减慢,而梯度较小的$g_t$,更新加速。
优化公式为$\omega_{t+1} = \omega_t - \frac{\alpha}{\sqrt{V_{t}} + \epsilon} \odot g_t$
Adagrad 的问题是在训练后期,由于$V_t$一直在累加,所以分母会过大,导致后期的学习率过小,基本没有变化。
Moment
Moment 引入了动量,第二行的更新为$m_t = \beta m_{t-1} + (1 - \beta) g_t$,通过当前梯度和历史梯度的平均,使得在震荡的方向学习减慢,在稳定下降的方向学习加快。
优化公式为$\omega_{t+1} = \omega_t + \alpha * m_t$
RMSprop
RMSprop 一定程度上解决了 Adagrad 学习率消失的问题,对二阶动量的更新方式$V_{t} = \beta V_{t-1} + (1 - \beta)g_t \odot g_t$。
优化公式为$\omega_{t+1} = \omega_t - \frac{\alpha}{\sqrt{V_{t}} + \epsilon} \odot g_t$
Adam
Adam 是 Adaptive moment,将上面两种的思想结合,第二行的更新公式为$m_t = \beta_0 m_{t-1} + (1 - \beta_0) g_t$,$V_{t} = \beta_1 V_{t-1} + (1-\beta_1)g_t \odot g_t$,然后进行 bias correction,$m_t = \frac{m_t}{1 - \beta_0^t}$,$V_t = \frac{V_t}{1 - \beta_1^t}$。
优化公式为$\omega_{t+1} = \omega_t + \alpha * \frac{m_t}{\sqrt{V_t} + \epsilon}$
思考
优化器中已经加入了对学习率的衰减,那么再增加学习率衰减还有没有用。
三、损失函数
L2 损失函数
实现简单,不做说明。
L2 loss 的问题是梯度的值与 x 的值有关,在 x 特别大时,会有很大的梯度,训练不稳定。
L1 损失函数
L1 损失函数的形式为$loss = \sum_i |y_i - pred_i|$,导数为$sign(x)$,在$x = 0$处不可导,可以使用次梯度,取 0。
L1 loss 的问题与 L2 相反,梯度是常数,在 x 值很小时,梯度依然是 1,如果学习率不变的话,很容易发生震荡,难以收敛到更高的精度。
四、layer 的实现
具体代码见
layer.py
,先构建抽象基类 Layer,规定实现接口,然后由子类实现
forward
方法。
目前实现过于简单,之后希望可以按照 pytorch 的逻辑结构实现一个稍微复杂亿点点的版本。
Linear
Linear 层完成的操作是$x @ W + b$,其中的$W$和$b$为可训练参数。实现时只需要完成上面的正向传播操作,反向传播由自动求导完成,十分简单。
激活层
激活层的特点在于没有可训练参数,实现时与自动求导中相应函数的实现相同。
Sequential
Sequential 层在初始化时会保存所有的层,在正向传播时,按顺序传递数据。
快速启动项目
```python from easytorch.layer import Linear, Tanh, Sequential from easytorch.optim import SGD import easytorch.functional as F
Create a model, optimizer, loss function
model = Sequential( Linear(1, 5), Tanh(), Linear(5, 1) ) opt = SGD(model.parameters(), lr=3e-4) loss_fn = F.mse_loss
train the model
for epoch in range(epochs): pred = model(x) loss = loss_fn(pred, y) opt.zero_grad() loss.backward() opt.step() ```
案例
参考文献
- 基于IDEFO模型的云计算服务安全监测平台研究(华北电力大学(北京)·汪澄)
- 基于动静态结合的网页恶意脚本检测技术研究(上海交通大学·宋寅)
- 基于深度学习的网络入侵检测系统的设计与实现(华中科技大学·晏子骏)
- 构建跨平台服务器管理系统(华南理工大学·彭俊)
- Web方式DoS渗透测试平台的设计与实现(太原理工大学·闫天杰)
- Web方式DoS渗透测试平台的设计与实现(太原理工大学·闫天杰)
- 云环境下的入侵检测系统设计与实现(西安电子科技大学·刘辰烁)
- 基于静态分析的Go语言内存安全检测研究(西安石油大学·高潮)
- 基于PaaS的网络探测系统的设计与实现(山东大学·申小军)
- 互联网系统漏洞检测技术集成应用研究(贵州大学·庞晓康)
- 基于网络爬虫的网络漏洞扫描检测系统的设计与实现(吉林大学·肖征)
- 基于JUnit框架的接口测试系统研究与实现(中国科学院大学(工程管理与信息技术学院)·左刚)
- Web应用漏洞扫描系统的设计与实现(东华大学·周康成)
- 基于网络爬虫的XSS漏洞检测系统的研究与设计(北京邮电大学·牛皓)
- 基于IDEFO模型的云计算服务安全监测平台研究(华北电力大学(北京)·汪澄)
本文内容包括但不限于文字、数据、图表及超链接等)均来源于该信息及资料的相关主题。发布者:代码客栈 ,原文地址:https://bishedaima.com/yuanma/35928.html