为了理解深度学习框架的大致机理,决定使用numpy实现一个简单的神经网络框架
深度学习框架我觉得最重要的是实现了链式求导法则,而计算图就是建立在链式求导法则之上的,目前大多数深度学习是基于反向传播思想的,如何在链式计算图中进行前向传播和反向传播是深度学习框架重点要考虑的
计算图
目前主流的深度学习框架有两种计算图实现方式:静态图:先构建好图再让数据流入,优点是结构清晰,理论上速度占优(实际不一定);缺点是debug困难,对初学者学习不友好(我认为主要是对计算图的理解不深刻导致的)。典型的框架代表:Tensorflow
动态图:程序按照编写的顺序动态执行,优点是所见所得,可以随时输出动态结果,调试方便,典型的框架代表:PyTorch
个人现在更倾向于使用tensorflow,一方面是tensorflow生态较成熟,另一方面个人感觉静态图更数学化,而动态图更工程化
不过本文实现的是一个类似pytorch风格的动态图框架
知识储备
链式求导法则
[begin{align*}
& f{'} = f(x) \\
& g{'} = g(f{'}) \\
& y{'} = k(g{'}) \\
& loss = L(y, y{'})
end{align*}]
用链式求导法则计算可得
[begin{align*}
frac{d(loss)}{d(x)} = frac{d(f{'})}{d(x)} times frac{d(g{'})}{d(f{'})} times frac{d(y{'})}{d(g{'})} times frac{d(loss)}{y{'}}\\
tag{1}
end{align*}
]
常见的激活函数
激活函数几乎都是非线性且尽可能全局可导的,激活函数的选择是很重要的,sigmoid, tanh 由于自身函数的特点,只有很小的区间梯度变化比较明显,大部分的区间梯度变化很小很容易造成梯度消失,本人更倾向于使用relu,或者是 batch normalization和tanh搭配使用
sigmoid
[sigma (z) = frac{1}{1+e^{-z}} tag{2}]
用matplotlib画出它的图像1
2
3
4
5
6
7
8
9
10
11
12
13import numpy as np
import matplotlib.pyplot as plt
def (x):
return 1. / (1. + np.exp(-x))
x = np.arange(-10, 10, 0.2)
y = sigmoid(x)
plt.plot(x, y, label="sigmoid", color="blue")
plt.show()
tanh
[tanh(z) = frac{e^{z} - e^{-z}}{e^{z} + e^{-z}} tag{3}]
用matplotlib画出它的图像1
2
3
4
5
6
7
8
9
10
11
12
13import numpy as np
import matplotlib.pyplot as plt
def tanh(x):
return (np.exp(x) - np.exp(-x)) / (np.exp(x) + np.exp(-x))
x = np.arange(-10, 10, 0.2)
y = tanh(x)
plt.plot(x, y, label="tanh", color="blue")
plt.show()
relu
[relu(z) = max(0, z) = relu(z) = begin{cases}
0 & text{ if } x leqslant 0 \
z & text{ if } x > 0
end{cases} tag{4}]
用matplotlib画出它的图像1
2
3
4
5
6
7
8
9
10
11
12
13import numpy as np
import matplotlib.pyplot as plt
def relu(x):
return np.maximum(0, x)
x = np.arange(-10, 10, 0.2)
y = relu(x)
plt.plot(x, y, label="relu", color="blue")
plt.show()
常见的优化器
SGD、Adam等,推荐阅读本人的另一篇文章 常用的梯度下降优化算法
常见的损失函数
MSE
[L(x_{i}, y_{i}) = (x_{i} - y_{i})^{2} tag{5}]
NLL 负对数似然
[L(x, label) = -x_{label} tag{6}]
BCE
BinCrossEntropy 是二分类用的交叉熵
[L(x_{i}, y_{i}) = -w_{i}[y_{i} logx_{i} + (1-y_{i})log(1-x_{i})] tag{7}]
CrossEntropy 交叉熵
[begin{align*}
L(x, label) & = -w_{label} logfrac{e^{x_label}}{sum_{j=1}^{N} e^{x_{j}}} \\
& = w_{label} [-x_{label} + logsum_{j=1}^{N} e^{x_{j}}]
end{align*} tag{8}]
目录结构nn:核心的网络包NN: 基础网络类
Linear:全连接层
Variable:基础参数类
ReLU / Sigmoid / Tanh:激活函数
optim:优化器SGD
Adam
loss:损失函数MSE
CrossEntropy
init:初始化器Normal:高斯分布
TruncatedNormal:截断高斯分布
Uniform:均匀分布
fn.py:激活函数
pipe.py:pipeline
实现细节
NN
NN 是最基础的网络基类,所有定义的网络都要继承该类1
2
3
4
5
6
7
8
9
10
11class NN(object):
def __init__(self):
pass
def forward(self, *args):
pass
def backward(self, grad):
pass
def params(self):
pass
def __call__(self, *args):
return self.forward(*args)
Variable
用于保存可导变量,求导时会用到此类封装的参数1
2
3
4
5
6class Variable(object):
def __init__(self, wt, dw, b, db):
self.wt = wt
self.dw = dw
self.b = b
self.db = db
Linear
定义全连接层, 全连接层可以实现网络空间大小的放缩,全连接层是实现深网的一种方式(但不推荐,深网的构建可以使用残差网络或者高速网络来构建)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36class Linear(NN):
def __init__(self,
dim_in,
dim_out,
init=None,
pretrained=None,
zero_bias=False):
super(Linear, self).__init__()
if isinstance(pretrained, tuple):
self.wt, self.b = pretrained
else:
if not isinstance(init, Init):
init = ini.Random([dim_in, dim_out])
self.wt = init()
if zero_bias:
self.b = ini.Zero([dim_out])()
else:
self.b = ini.Random([dim_out])()
self.input = None
self.output = None
self.dw = ini.Zero(self.wt.shape)()
self.db = ini.Zero([dim_out])()
self.variable = Variable(self.wt, self.dw, self.b, self.db)
def params(self):
return self.variable
def forward(self, *args):
self.input = args[0]
self.output = np.dot(self.input, self.wt) + self.b
return self.output
def backward(self, grad):
self.db = grad
self.dw += np.dot(self.input.T, grad)
grad = np.dot(grad, self.wt.T)
return grad
Sigmoid的实现
Sigmoid的前向推导很容易实现,也就是将输入放入sigmoid函数即可,难点在Sigmoid的求导上,以下是sigmoid的求导过程
[begin{align*}
f{'}(z) & = (frac{1}{1+e^{-z}}){'} \\
& = frac{e^{-z}}{(1+e^{-z})^{2}} \\
& = frac{1+e^{-z}-1}{(1+e^{-z})^{2}} \\
& = frac{1}{1+e^{-z}}(1-frac{1}{1+e^{-z}}) \\
& = f(z)(1-f(z))
end{align*} tag{9}]
知道了前向推导和反向推导的结果就可以用代码实现了1
2
3
4
5
6
7
8
9
10
11
12
13
14class Sigmoid(NN):
def __init__(self):
super(Sigmoid, self).__init__()
self.input = None
self.output = None
def forward(self, *args):
self.input = args[0]
self.output = 1.0 / (1.0 + np.exp(-self.input))
return self.output
def backward(self, grad):
grad *= self.output*(1.0-self.output)
return grad
tanh的实现
tanh的实现和sigmoid的类似,只要计算出tanh的导函数就可以很容易实现反向传播部分,tanh的求导结果是
[f(z){'} = 1-(f(z))^{2} tag{10}]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Tanh(NN):
def __init__(self):
super(Tanh, self).__init__()
self.input = None
self.output = None
def forward(self, *args):
self.input = args[0]
self.output = ((np.exp(self.input) - np.exp(-self.input)) /
np.exp(self.input) + np.exp(-self.input))
return self.output
def backward(self, grad):
grad *= 1.0 - np.power(self.output, 2)
return grad
同理其他模块的实现也类似,主要包括前向传播和反向传播两部分
实例
以下构造了一个简单的BP网络1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class BPNet(nn.NN):
def __init__(self,D_in, D_hidden, D_out):
super(Mnistnet,self).__init__()
self.layers = Pipe(
nn.Linear(D_in, D_hidden),
nn.ReLU(),
nn.Linear(D_hidden, D_out)
)
self.criterion = loss.MSE()
def forward(self,*args):
x = args[0]
return self.layers.forward(x)
def backward(self,grad=None):
grad=self.criterion.backward(grad)
self.layers.backward(grad)
可视化训练
使用simnet实现了一个简单的BP网络,数据集是 mnist,做了可视化训练demo , 效果如下图
refrence