使用numpy实现一个深度学习框架

为了理解深度学习框架的大致机理,决定使用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 normalizationtanh搭配使用

sigmoid

\[\sigma (z) = \frac{1}{1+e^{-z}} \tag{2}\]

用matplotlib画出它的图像

1
2
3
4
5
6
7
8
9
10
11
12
13
# -*- coding: utf-8 -*-

import numpy as np
import matplotlib.pyplot as plt

def sigmoid(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
13
# -*- coding: utf-8 -*-

import 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
13
# -*- coding: utf-8 -*-

import 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} log\frac{e^{x_label}}{\sum_{j=1}^{N} e^{x_{j}}} \\\\ & = w_{label} [-x_{label} + log\sum_{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
11
class 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
6
class 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
36
class 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
14
class 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
15
class 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
18
class 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 , 效果如下图

详细的demo地址 examples/mnist

本项目地址simnet

refrence

sean lee wechat
欢迎关注我的公众号!
感谢支持!