神经网络python简单实现

最近团队在搞DNN,用的都是别人搭好的框架,因此想着自己来实现一下简单的神经网络模型,更好的理解其中foward和back propagation的过程。

原理

最简单的神经网络基本上可以分为两个步骤:前向传播和后向误差传递。

前向传播

定义input layer与hidden layer之间的weights为$w_1$,hidden layer与output layer之间的weights为$w_2$。输入向量$x$与$w_1$相乘累加后通过一个非线性变换函数activation function (sigmoid, tanh, relu等)得到hidden layer中neuron的输出结果,然后hidden layer与$w_2$相乘累加后再通过activation function得到output layer中nueron的输出结果。如果中间有多个hidden layer,就不断重复这一过程(这里为了简化处理省略了bias项)。

其中$\sigma$即为activation function,如果为sigmoid函数,

其导数具有良好的性质,

而最终的output就是我们期待的输出y。

后向误差传递

后向传播的目地在于将预测结果与真实结果进行对比,并根据预测误差来对每层的weights进行更新,最简单的更新方法就是梯度下降,也就是每次迭代时在误差梯度方向移动一小步,这样在经过若干步后参数逐渐更新,误差逐渐收敛,以达到训练效果。如果我们如果将问题视为一个regression问题来处理,那么一种常见的平方误差损失函数可以表示为

求梯度,简单来讲就是求导数,在参数更新时,由于误差是在output layer才得到的,如果想对前若干层的weights求导,我们需要采取的是链式求导方法,也就是

这样误差一层一层向前传递,最终每一层的weights都会得到相对于loss的变化$\Delta$,然后再减掉$\Delta * \eta$ (学习率),这样就完成了一次所有参数的更新

针对$w_2$而言,运用链式求导法则,则有

$w_1$由于需要再向后反向传播一层,求导显得更麻烦一点,不过依然是运用链式求导法则,

如果中间隐层增加,依然遵循这一法则向后传播误差,可以看到每向后跨越一层,就需要计算一次activation fuction的导数,这个导数最大值在0处为0.25,也就是说如果隐层为N层,最后误差向后传播到第一个隐层,误差信息最多为$\frac{\partial loss} {\partial output}*0.25^N$,已经所剩无几,这也解释了为什么sigmoid函数容易出现gradient vanishing(梯度消失现象),因此在实际应用中我们大多数是使用ReLU这种激活函数的。

值得注意的是这里也要遵循维数相容原则,简单来讲就是链式求导时的前后相乘项需要维度互相匹配,并最终相乘后维度等于输出空间的维度。

  • “将他们当做一维实数然后使用链式法则,最后做维数相容的调整,使之符合矩阵乘法原则并且维数相容即可”,这种方式是一种最快速准确的策略
  • “对单个元素求导,再整理成矩阵形式”这种方式是困难、过程缓慢的

完成一次向前预测和向后传递误差的过程称为一次epoch,如此反复的进行多轮迭代到一定次数或误差小于一定的阈值,即可完成模型的训练。

实现

假设现在测试数据共4个样本,每个样本有3维特征,输出为1维,因此input layer为(4, 3),output layer为(4, 1)。设置1层4个neuron的hidden layer,其中$w_1$为(3, 4),$w_2$为(4, 1),代码十分简洁。

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
import numpy as np
def sigmoid(x):
return 1/(1+np.exp(-x))
def sigmoid_derivative(x):
return x * (1 - x)
def loss(y, y_hat):
return np.sum(np.square(y_hat - y))
#load data
X = np.array([ [0,0,1],[0,1,1],[1,0,1],[1,1,1] ])
y = np.array([[0,1,1,0]]).T
# weight initialization and learning rate
w_l1 = 2*np.random.random((3,4)) - 1
w_l2 = 2*np.random.random((4,1)) - 1
lr = 0.1
for j in range(10000):
#forward propagation
layer_1 = sigmoid(np.dot(X,w_l1))
layer_2 = sigmoid(np.dot(layer_1,w_l2))
#back propagation
delta_2 = (y - layer_2) * sigmoid_derivative(layer_2)
delta_1 = delta_2.dot(w_l2.T) * sigmoid_derivative(layer_1)
#update weights
w_l2 += lr * layer_1.T.dot(delta_2)
w_l1 += lr * X.T.dot(delta_1)
#print loss
if j % 2000 == 0:
print("Iteration ", j, ": ", loss(y, layer_2))

Loss随着epoch不断增加而减小,输出结果:

1
2
3
4
5
Iteration 0 : 1.00255859758
Iteration 2000 : 0.875218177244
Iteration 4000 : 0.111818055322
Iteration 6000 : 0.028557004711
Iteration 8000 : 0.0148779596401

加了nueron上的bias项、ReLU和learning rate,更加通用

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import numpy as np
from sklearn import datasets
def sigmoid(x):
return 1/(1+np.exp(-x))
def sigmoid_derivative(x):
return x * (1 - x)
def relu(x):
x[x <= 0] = 0
return x
def relu_derivative(x):
x[x <= 0] = 0
x[x > 0] = 1
return x
def mean_square(y, y_hat):
return np.mean(np.square(y_hat - y))
def cross_entropy(y, y_hat):
epsilon = 1e-15
return -np.mean(y * np.log(y_hat + epsilon) + (1 - y) * np.log(1 - y_hat + epsilon))
def train(X, y, activation = "sigmoid", hidden_units = 128):
# weight initialization
w_l1 = 2*np.random.random((X.shape[1],hidden_units)) - 1
w_l2 = 2*np.random.random((hidden_units,y.shape[1])) - 1
# bias initilization
b_l1 = np.zeros((1, hidden_units))
b_l2 = np.zeros((1, y.shape[1]))
# learning rate
lr = 0.1
for j in range(10000):
if activation == "relu":
layer_1 = relu(np.dot(X,w_l1) + b_l1)
layer_2 = sigmoid(np.dot(layer_1,w_l2) + b_l2)
delta_2 = (y - layer_2) * sigmoid_derivative(layer_2)
delta_1 = delta_2.dot(w_l2.T) * relu_derivative(layer_1)
else:
layer_1 = sigmoid(np.dot(X,w_l1) + b_l1)
layer_2 = sigmoid(np.dot(layer_1,w_l2) + b_l2)
delta_2 = (y - layer_2) * sigmoid_derivative(layer_2)
delta_1 = delta_2.dot(w_l2.T) * sigmoid_derivative(layer_1)
#update weights and biases
w_l2 += lr * layer_1.T.dot(delta_2)
w_l1 += lr * X.T.dot(delta_1)
b_l2 += lr * np.sum(delta_2, axis = 0, keepdims = True)
b_l1 += lr * np.sum(delta_1, axis = 0, keepdims = True)
#print loss
if j % 2000 == 0:
print("Iteration ", j, ": ", cross_entropy(y, layer_2))
#print(layer_2)
if __name__ == "__main__":
data = datasets.make_classification()
X = data[0]
y = data[1].reshape(-1, 1)
train(X, y,'relu')