一步步手写神经网络
这篇博客主要是一步步的手撸一个神经网络出来,相信看完这篇博客,大家会对神经网络有一个比较深刻的认识。看完后,你可能会发现只要会基本的求导,其实bp(backward propagation)并没有想象中的那么难。下面就来一步步手写一个神经网络。我会先拿一个简单的3层神经网络做demo展示下求解神经网络的整体过程,其实主要就是forward propagation(前向传播)和backward propagation(后向传播/逆向传播)这两个过程。
一个简单的神经网络:
这是一个简单的只有三层的神经网络,先来定义一些notation(这个notation沿用ng的):
具体到上面给出的神经网络demo图,这里有:
其实这其中最难确定的是参数w,b,a 的维度,其实看上面图中参数的维度,也能够发现参数w,b,a 的维度:
还有就是这个demo中隐藏层激活函数使用的是现在最常用的ReLU当做激活函数,输出层因为是个二元分类就使用sigmoid函数作为激活函数(如果是多分类现在最常用的是softmax函数),关于几种激活函数,详见本人另一篇博客:几种常见的激活函数,代价函数则是使用的交叉熵函数,神经网络的交叉熵代价函数为:
接下来正式进入到forward propagation阶段,前向传播的计算过程为:
总结下上面的公式其实能够发现:
下面是backward propagation(bp)阶段,其实就是求导(得到梯度),我会详细的写出求导过程:
对 W [ 3 ] W^{[3]} W[3] 求导:
在求出 W [ 3 ] W^{[3]} W[3] 的同时其实也得到了 d b [ 3 ] db^{[3]} db[3]:
对 W [ 2 ] W^{[2]} W[2]求导:
同样能够得到 d b [ 2 ] db^{[2]} db[2]:
对 W [ 1 ] W^{[1]} W[1]求导,得:
则 d b [ 1 ] db^{[1]} db[1]为:
其实bp的过程也是有规律可循的,根据链式法则,一个变量的导数依赖它的“上家”,通过上面我的 “总结”,应该发现:
以上就是这个3层神经网络Foward propagation和backward propagation的详细具体过程。并且我们能够发现一些规律,很容易的推广到L层。下面就来一步步手撸一个神经网络,我们在实现的时候一定要注意矩阵的维度,而且要搞清楚矩阵之间到底是乘(线代里矩阵相乘的定义)还是点乘(对应元素相乘)。前向传播的还倒好,bp的向量化实现如下:
其实bp过程就是逆过来,一步接着一步。
下面进入到代码实现阶段:
**|| **forward propagation阶段
其实整个fp的过程就是:linear_forward->ReLU_forward->linear_forward->…
1.首先初始化参数w,b,对于参数w,b的初始化要随机初始化,至于为什么要随机初始化,请参见本人博客:神经网络(neural network)。关于对权重的初始化方法,详见本人另一篇博客:深度学习中神经网络的几种权重初始化方法。下面代码中随机初始化参数w时之所以*0.01,目的是:如果我们不乘的话,有可能会把参数初始化为一个比较大的数,这样会导致z=w.T * X + b,z的值有时候会比较大,这样的话,如果你使用sigmoid或者tanH作为激活函数,得到的导数(梯度)会非常的小,这样参数更新的速度会比较慢。
#initialize parameters(w,b)
def initialize_parameters(layer_dims):
"""
:param layer_dims: list,每一层单元的个数(维度)
:return:dictionary,存储参数w1,w2,...,wL,b1,...,bL
"""
np.random.seed(3)
L = len(layer_dims)#the number of layers in the network
parameters = {}
for l in range(1,L):
parameters["W" + str(l)] = np.random.randn(layer_dims[l],layer_dims[l-1])*0.1
parameters["b" + str(l)] = np.zeros((layer_dims[l],1))
return parameters
2.实现线性函数部分:
def linear_forward(x, w, b):
"""
:param x:
:param w:
:param b:
:return:
"""
z = np.dot(w, x) + b # 计算z = wx + b
return z
3.实现激活函数(ReLU和sigmoid):
#implement the activation function(ReLU and sigmoid)
def relu_forward(Z):
"""
:param Z: Output of the activation layer
:return:
A: output of activation
"""
A = np.maximum(0,Z)
return A
def sigmoid(Z):
"""
:param Z: Output of the linear layer
:return:
"""
A = 1 / (1 + np.exp(-Z))
return A
4.整个Foward propagation的过程:
def forward_propagation(X, parameters):
"""
X -- input dataset, of shape (input size, number of examples)
parameters -- python dictionary containing your parameters "W1", "b1", "W2", "b2",...,"WL", "bL"
W -- weight matrix of shape (size of current layer, size of previous layer)
b -- bias vector of shape (size of current layer,1)
:return:
AL: the output of the last Layer(y_predict)
caches: list, every element is a tuple:(W,b,z,A_pre)
"""
L = len(parameters) // 2 # number of layer
A = X
caches = []
# calculate from 1 to L-1 layer
for l in range(1,L):
W = parameters["W" + str(l)]
b = parameters["b" + str(l)]
#linear forward -> relu forward ->linear forward....
z = linear_forward(A, W, b)
caches.append((A, W, b, z)) # 以激活函数为分割,到z认为是这一层的,激活函数的输出值A认为是下一层的输入,划归到下一层。注意cache的位置,要放在relu前面。
A = relu_forward(z) #relu activation function
# calculate Lth layer
WL = parameters["W" + str(L)]
bL = parameters["b" + str(L)]
zL = linear_forward(A, WL, bL)
caches.append((A, WL, bL, zL))
AL = sigmoid(zL)
return AL, caches
5.整个forward propagation完后,开始计算代价函数:
#calculate cost function
def compute_cost(AL,Y):
"""
:param AL: 最后一层的激活值,即预测值,shape:(1,number of examples)
:param Y:真实值,shape:(1, number of examples)
:return:
"""
m = Y.shape[1]
cost = 1. / m * np.nansum(np.multiply(-np.log(AL), Y) +
np.multiply(-np.log(1 - AL), 1 - Y))
#从数组的形状中删除单维条目,即把shape中为1的维度去掉,比如把[[[2]]]变成2
cost = np.squeeze(cost)
return cost
**|| **backward propagation阶段
其实BP过程就是从后往前,一层接着一层,具体过程为 sigmoid_backward(输出层比较特殊一般特殊考虑):ReLU_backward->linear_backward->ReLU_backward->…
1、relu导数:
#derivation of relu
def relu_backward(dA, Z):
"""
:param Z: the input of activation function
:param dA:
:return:
"""
dout = np.multiply(dA, np.int64(Z > 0)) #J对z的求导
return dout
2、线性部分导数
#derivation of linear
def linear_backward(dZ, cache):
"""
:param dZ: Upstream derivative, the shape (n^[l+1],m)
:param A: input of this layer
:return:
"""
A, W, b, z = cache
dW = np.dot(dZ, A.T)
db = np.sum(dZ, axis=1, keepdims=True)
da = np.dot(W.T, dZ)
return da, dW, db
3、下面是bp过程:
def backward_propagation(AL, Y, caches):
"""
Implement the backward propagation presented in figure 2.
Arguments:
X -- input dataset, of shape (input size, number of examples)
Y -- true "label" vector (containing 0 if cat, 1 if non-cat)
caches -- caches output from forward_propagation(),(W,b,z,pre_A)
Returns:
gradients -- A dictionary with the gradients with respect to dW,db
"""
m = Y.shape[1]
L = len(caches) - 1
#calculate the Lth layer gradients
dz = 1. / m * (AL - Y)
da, dWL, dbL = linear_backward(dz, caches[L])
gradients = {"dW" + str(L + 1): dWL, "db" + str(L + 1): dbL}
#calculate from L-1 to 1 layer gradients
for l in reversed(range(0,L)): # L-1,L-3,....,0
A, W, b, z = caches[l]
#ReLu backward -> linear backward
#relu backward
dout = relu_backward(da, z)
#linear backward
da, dW, db = linear_backward(dout, caches[l])
# print("========dW" + str(l+1) + "================")
# print(dW.shape)
gradients["dW" + str(l+1)] = dW
gradients["db" + str(l+1)] = db
return gradients
5、下面就是更新参数w,b:
def update_parameters(parameters, grads, learning_rate):
"""
:param parameters: dictionary, W,b
:param grads: dW,db
:param learning_rate: alpha
:return:
"""
L = len(parameters) // 2
for l in range(L):
parameters["W" + str(l + 1)] = parameters["W" + str(l + 1)] - learning_rate * grads["dW" + str(l+1)]
parameters["b" + str(l + 1)] = parameters["b" + str(l + 1)] - learning_rate * grads["db" + str(l+1)]
return parameters
以上就是整个bp的过程,至此,fp和bp整个流程已经全部写完了,下面再写个主函数,把fp和bp串联下,整个dnn网络就完成了:
def L_layer_model(X, Y, layer_dims, learning_rate, num_iterations):
"""
:param X:
:param Y:
:param layer_dims:list containing the input size and each layer size
:param learning_rate:
:param num_iterations:
:return:
parameters:final parameters:(W,b)
"""
costs = []
# initialize parameters
parameters = initialize_parameters(layer_dims)
for i in range(0, num_iterations):
#foward propagation
AL,caches = forward_propagation(X, parameters)
# calculate the cost
cost = compute_cost(AL, Y)
if i % 1000 == 0:
print("Cost after iteration {}: {}".format(i, cost))
costs.append(cost)
#backward propagation
grads = backward_propagation(AL, Y, caches)
#update parameters
parameters = update_parameters(parameters, grads, learning_rate)
print('length of cost')
print(len(costs))
plt.clf()
plt.plot(costs) # o-:圆形
plt.xlabel("iterations(thousand)") # 横坐标名字
plt.ylabel("cost") # 纵坐标名字
plt.show()
return parameters
这样就得到了,这个网络的参数W,b,这样我们就可以用这个参数去做分类了,下面顺手在撸个predict函数吧:
#predict function
def predict(X_test,y_test,parameters):
"""
:param X:
:param y:
:param parameters:
:return:
"""
m = y_test.shape[1]
Y_prediction = np.zeros((1, m))
prob, caches = forward_propagation(X_test,parameters)
for i in range(prob.shape[1]):
# Convert probabilities A[0,i] to actual predictions p[0,i]
if prob[0, i] > 0.5:
Y_prediction[0, i] = 1
else:
Y_prediction[0, i] = 0
accuracy = 1- np.mean(np.abs(Y_prediction - y_test))
return accuracy
写个入口函数:
#DNN model
def DNN(X_train, y_train, X_test, y_test, layer_dims, learning_rate= 0.001, num_iterations=30000):
parameters = L_layer_model(X_train, y_train, layer_dims, learning_rate, num_iterations)
accuracy = predict(X_test,y_test,parameters)
return accuracy
至此,整个deep neural network已经写完了,虽然简单,可能代码也写的不够严谨。但对于理解neural network,即forward propagation和backward propagation非常有帮助。拿sklearn中的load_breast_cancer数据集测了下,当隐藏层单元设置为[10,5,1]时,准去率达到了92%,代价函数示意图如下:
p.s:这里另外提一下,ng的在Coursera上的课把每个步骤划分的更加细致,但是我个人认为那样不太好,有点打乱了fp和bp的整体性,这个每个人的口味都不一样。最重要的一点。。。是我觉得ng的relu求导写错了,现在把ng的relu求导代码贴出来,然后把我改正的(我认为是对的)也贴出来,供路过的大侠鉴定下,欢迎留言交流。
ng的relu_backward:
我改正后的:
博客完整的代码已经放到github上了:deep_neural_network
改正版的ngCoursera作业上的代码:deep_neural_network_ng
- 参考资料:Coursera上ng的《neural network and deep learning》课