手把手教学 - MLP算法实现

手把手教学 M L P MLP MLP 算法实现

在上一次的推送 M L P MLP MLP算法推导小记中对反向传播进行了详细的推导, 本文将详细讲解该算法如何用 n u m p y numpy numpy来实现。

本次实验中输入层为4个结点,隐藏层为20个结点,再加上偏置,激活函数为 t a n h tanh tanh,输出层为3个结点,损失函数为均方误差 ,上次的推导也是基于此进行的。

对于不同的激活函数或者损失函数的推导都类似,可以基于上次的推送重新推导出网络权值更新公式即可。本文的激活函数和损失函数都是固定的,是小鱼我之前一次实验的精简版本,后面如果大家需要,可以推出能够自定义激活函数和损失函数的版本。

M L P MLP MLP也可以称为全连接前馈神经网络,理解了 M L P MLP MLP也可以帮助我们更好的理解深度学习框架中的全连接层。

1. M L P MLP MLP 理解

对于 M L P MLP MLP网络已经很熟悉的就可以跳过这部分, 这里主要是可以让我们对多层感知器的工作流程有一个直观的认识。

首先要明白这次实验是要对鸢尾花数据集进行分类, 网络可以简单的看为一个函数,函数中有参数可以调整,从而调整函数的输出值。

  • 输入有四个数据, 分别为四个特征

  • 输出有三个数据, 形式为[1,0,0](类别1), [ 1 , 0 , 0 ] , [ 0 , 1 , 0 ] , [ 0 , 0 , 1 ] [1,0,0],[0,1,0],[0,0,1] [1,0,0],[0,1,0],[0,0,1]代表三个类别.

对于训练过程就是给一组四个特征,通过调整函数里的参数,使得函数的输出为对应的类别,如[1,0,0]。本次实验中的batch大小为1,即每次采用一组数据进行训练。将训练集中的所有数据都训练一遍就称为一个epoch。

当通过函数里调整过的参数计算得出的输出,能够让我们判断该组特征是属于鸢尾花的什么类别,就可以认为训练完成。

测试时,我们只需要将测试数据输入函数,取输出三个数据中最大的下标,就可以得到该特征对应的鸢尾花预测类别。

2. 网络工作流程

为了对网络工作的流程说明清楚, 本文取数据集中的第一组数据为例, 在实验中将所有数据一组一组的送入网络进行训练即可

sepal length sepal width petal length petal width class
5.1cm​ 3.5cm​ 1.4cm​ 0.2cm​ Iris-setosa​

那么对于 I r i s − s e t o s a Iris-setosa Iris−setosa 这一类别的label表示为

y_{1}​ y_{2}​ y_{3}​
1 0 0

手把手教学 - MLP算法实现

首先要初始化网络, 包括输入结点数为4, 隐藏层结点数为20, 输出层结点数为3, 网络权值 w i j [ 5 , 20 ] w_{i j}[5 ,20] wij​[5,20] (其中第一行1*20为隐藏层的偏置), w j k [ 20 , 3 ] w_{j k}[20,3] wjk​[20,3]的随机初始化 ,学习率 η \eta η 为0.001

对于训练过程

正向传播

  • 输入层:输入大小为1*4的矩阵 [ 5.1 , 3.5 , 1.4 , 0.2 ] [5.1, 3.5, 1.4, 0.2] [5.1,3.5,1.4,0.2]

  • 隐藏层:输入的1*4的矩阵加上偏置项,相当于输入(1,5)的矩阵 [ 1 , 5.1 , 3.5 , 1.4 , 0.2 ] [1,5.1, 3.5, 1.4, 0.2] [1,5.1,3.5,1.4,0.2] 与隐藏层的网络权值 w i j [ 5 , 20 ] w_{i j}[5 ,20] wij​[5,20] 内积得到(1,20)的矩阵

  • 输出层:输入(1,20)的矩阵与输出层的网络权值 w j k [ 20 , 3 ] w_{j k}[20,3] wjk​[20,3] 内积得到(1,3)的矩阵

  • (1,3)的矩阵即为 y k y_{k} yk​ ,但因为网络权值在一定范围内随机初始化的,因此现在的输出肯定是不满足要求 [ 1 , 0 , 0 ] [1,0,0] [1,0,0]

反向传播

  • 由于此时的输出不满足要求,因此要去更新网络权值, 根据前面推出的公式就可以计算出 ∂ E n ∂ w k j \frac{\partial E_{n}}{\partial w_{k j}} ∂wkj​∂En​​以及 ∂ E n ∂ w j i \frac{\partial E_{n}}{\partial w_{j i}} ∂wji​∂En​​, 对于梯度的计算,小鱼我认为主要在于推导,当推导出公式后,其计算过程就是将公式用代码表示出来就可以了。

  • 接着分别去更新 w i j [ 5 , 20 ] w_{i j}[5 ,20] wij​[5,20], w j k [ 20 , 3 ] w_{j k}[20,3] wjk​[20,3] 即可

就这样进行迭代训练, 在一次又一次的训练中, 正向传播的输出就被慢慢纠正, 最后趋近于 [ 1 , 0 , 0 ] [1,0,0] [1,0,0] 了

对于测试过程, 那就是只要用到正向传播的过程, 得到网络的输出, 判断是哪一类别, 就可以得知分类是否正确

3. 正向传播过程

3.1 输入层

输入层为4个特征结点, 表示为 x i x_{i} xi​, 其中 i {i} i 代表第 i {i} i 个特征, 其中有一个常数1的输入为偏置。
x i ′ = [ 1 , x i ] x_{i}' =\left[\begin{array}{ll} {1},&x_{i} \end{array}\right] xi′​=[1,​xi​​]

3.2 隐藏层

隐藏层为20个结点, 因此隐藏层网络的权值为 w i j w_{i j} wij​, 即一个大小为(5, 20)的矩阵, 其转置为 w j i w_{j i} wji​

进入隐藏层第一部分后输出为 a j a_{j} aj​ , 输出为一个大小(1,20)的向量其为
a j = ∑ i = 1 d w j i [ x 1 , . . . , x i ] + w j 0 = ∑ i = 0 d w j i ( 1 ) x i ′ \begin{aligned} a_{j} &=\sum_{i=1}^{d} w_{j i} [x_{1},...,x_{i}] + w_{j 0}\\&=\sum_{i=0}^{d} w_{j i}^{(1)} x'_{i} \end{aligned} aj​​=i=1∑d​wji​[x1​,...,xi​]+wj0​=i=0∑d​wji(1)​xi′​​
d d d 为输入结点的个数

经过隐藏层的激活函数 t a n h ( x ) tanh \left(x \right) tanh(x) , 输出为 z j z_{j} zj​ , 仍然为一个(1,20)的向量
z j = tanh ⁡ ( a j ) \begin{aligned} z_{j} &=\tanh \left(a_{j}\right) \end{aligned} zj​​=tanh(aj​)​

隐藏层的 t a n h tanh tanh 函数的实现如下

		def tanh(x):
    		return (np.exp(x)-np.exp(-x))/(np.exp(x)+np.exp(-x))

由于输入的为(5,1)的数组, 因此需要转置, 再与 w j i w_{j i} wji​ 进行矩阵点乘, 输出 z j z_{j} zj​ 为(1,20)的数组

        # Calculate hidden layer output
        # input: (1,4),  W_ij:{Bias(0,20),W_ij(1:,20)},  output: (1,20) 
        a_j = np.dot(X,self.W_ij[1:,:]) + self.W_ij[0,:]   
        z_j = self.tanh(a_j)
3.3 输出层

输出层为3个结点, 因为采用独热编码表示, 并且本文中一共有三个分类, 因此输出层的网络权值为 w j k w_{j k} wjk​ , 即一个大小为(20, 3)的矩阵, 其转置矩阵为 w k j w_{k j} wkj​ , 输出为
y k = ∑ j = 0 K w k j ( 2 ) z j \begin{aligned} y_{k} &=\sum_{j=0}^{K} w_{k j}^{(2)} z_{j} \end{aligned} yk​​=j=0∑K​wkj(2)​zj​​

输入的为(1,20)的数组, 再与 w j k w_{j k} wjk​ 进行矩阵点乘, 输出为(1,3)的数组

        # Calculate the output layer output
        # input (1,20) W_kj(20,3) output (1,3)    
        y_k = np.dot(z_j, self.W_jk)

4. 损失函数

对于输出的(1,3)的标签 y k y_{k} yk​ , 采用均方差误差计算损失,
E n = 1 2 ∑ k = 1 L ( y k − t k ) 2 E_{n} = \frac{1}{2} \sum_{k=1}^{L}\left(y_{k}-t_{k}\right)^{2} En​=21​k=1∑L​(yk​−tk​)2

输出层输出的 y k y_{k} yk​ 与该组特征对应的标签 y l a b e l y_{label} ylabel​ 相减, 对数组中每个数据分别求平方, 再求和除以 2 2 2 , 最终得到 l o s s loss loss

    	def loss_function(self, y_k, y_label):       
       	 	loss = np.sum(np.square(y_k - y_label))/ 2  
       	 	return loss

5. 反向传播过程

5.1 损失函数对输出层输出 y k y_{k} yk​ 的偏导

损失函数对输出层输出 y k y_{k} yk​ 的偏导 ∂ E n ∂ y k \frac{\partial E_{n}}{\partial y_{k}} ∂yk​∂En​​ , 即 δ k \delta_{k} δk​
δ k = y k − t k \begin{aligned} \delta_{k}=y_{k}-t_{k} \end{aligned} δk​=yk​−tk​​

        # the gradient of the output 
        # input: y_k(1,3) y_label(1,3)  ,output: gradient_y_k (1,3)   
        gradient_y_k = y_k - y_label
5.2 损失函数对隐藏层输出 a j a_{j} aj​的偏导

损失函数对隐藏层输出 a j a_{j} aj​的偏导为 ∂ E n ∂ a j = ∂ E n ∂ z j ∂ z j ∂ a j \frac{\partial E_{n}}{\partial a_{j}} = \frac{\partial E_{n}}{\partial z_{j}} \frac{\partial z_{j}}{\partial a_{j}} ∂aj​∂En​​=∂zj​∂En​​∂aj​∂zj​​ ,即 δ j \delta_{j } δj​

其中 ∂ E n ∂ z j \frac{\partial E_{n}}{\partial z_{j}} ∂zj​∂En​​ 的偏导求解为
∂ E n ∂ z j = ∂ E y 1 ∂ z j + . . . + ∂ E y k ∂ z j = ∑ k w k j δ k \begin{aligned} \frac{\partial E_{n}}{\partial z_{j}} &= \frac{\partial E_{y_1}}{\partial z_{j}} +...+ \frac{\partial E_{y_k}}{\partial z_{j}} \\ &= \sum_{k} w_{k j} \delta_{k} \end{aligned} ∂zj​∂En​​​=∂zj​∂Ey1​​​+...+∂zj​∂Eyk​​​=k∑​wkj​δk​​
∂ z j ∂ a j \frac{\partial z_{j}}{\partial a_{j}} ∂aj​∂zj​​ 的偏导, 即为对激活函数的偏导, 求解为
∂ z j ∂ a j = tanh ⁡ ( a j ) ′ = ( 1 − z j 2 ) \begin{aligned} \frac{\partial z_{j}}{\partial a_{j}} &= \tanh \left(a_{j}\right)^{\prime} \\ &= \left(1-z_{j}^{2}\right) \end{aligned} ∂aj​∂zj​​​=tanh(aj​)′=(1−zj2​)​
因此可以得到
δ j = ( 1 − z j 2 ) ∗ ∑ k w k j δ k \delta_{j } = \left(1-z_{j}^{2}\right) * \sum_{k} w_{k j} \delta_{k} δj​=(1−zj2​)∗k∑​wkj​δk​

		# the gradient of the tanh 
        # input: z_j(1,20) , output: d_tanh(1,20)
        d_tanh = 1 - np.square(z_j) 
        
        # the gradient of the z_j input: gradient_y_k(1,3) W_jk(20,3), output:(1,20)
        gradient_z_j = np.dot(gradient_y_k, np.transpose(self.W_jk))   
        
        # the gradient of the a_j input: gradient_z_j(1,20) d_tanh(1,20), output:(1,20)
        gradient_a_j = gradient_z_j* d_tanh
5.3 损失函数对网络权值$w_{j i} $的梯度

损失函数对网络权值$w_{j i} 的 梯 度 为 的梯度为 的梯度为\frac{\partial E_{n}}{\partial w_{j i}^{(1)} }$ ,
∂ E n ∂ w j i = ∂ E n ∂ z j ∂ z j ∂ a j ∂ a j ∂ w j i \begin{aligned} \frac{\partial E_{n}}{\partial w_{j i}}= \frac{\partial E_{n}}{\partial z_{j}} \frac{\partial z_{j}}{\partial a_{j}} \frac{\partial a_{j}}{\partial w_{j i}} \end{aligned} ∂wji​∂En​​=∂zj​∂En​​∂aj​∂zj​​∂wji​∂aj​​​
∂ a j ∂ w j i ( 1 ) \frac{\partial a_{j}}{\partial w_{j i}^{(1)}} ∂wji(1)​∂aj​​ 的偏导求解为
∂ a j ∂ w j i = x i \frac{\partial a_{j}}{\partial w_{j i}} = x_{i} ∂wji​∂aj​​=xi​
由于 δ j = ( 1 − z j 2 ) ∗ ∑ k w k j δ k \delta_{j} =\left(1-z_{j}^{2}\right)* \sum_{k} w_{k j} \delta_{k} δj​=(1−zj2​)∗∑k​wkj​δk​ ,则可将公式简化为
∂ E n ∂ w j i = δ j ⊗ x i \begin{aligned} \frac{\partial E_{n}}{\partial w_{j i}}=\delta_{j} \otimes x_{i} \end{aligned} ∂wji​∂En​​=δj​⊗xi​​
对于隐藏层的偏置,其实就相当于上式子中的 x 0 = 1 x_{0}=1 x0​=1,
∂ E n ∂ w j 0 = δ j \begin{aligned} \frac{\partial E_{n}}{\partial w_{j 0}}=\delta_{j} \end{aligned} ∂wj0​∂En​​=δj​​

        # the gradient of the w_ij  input: x(1,4) gradient_a_j(1,20), output:(4,20)
        gradient_w_ij = np.outer(x, gradient_a_j)
        # the gradient of the W_i_j_1(Bias), input: gradient_a_j(1,20), output:(1,20)
        gradient_W_i_j_1 = gradient_a_j 
5.4 损失函数对网络权值 w k j w_{k j} wkj​的梯度

根据损失函数对网络权值 w k j w_{k j} wkj​的梯度为 ∂ E n ∂ w k j ( 2 ) \frac{\partial E_{n}}{\partial w_{k j}^{(2)} } ∂wkj(2)​∂En​​ ,
∂ E n ∂ w k j ( 2 ) = ∂ E n ∂ y k ∂ y k ∂ w k j ( 2 ) \begin{aligned} \frac{\partial E_{n}}{\partial w_{k j}^{(2)}}= \frac{\partial E_{n}}{\partial y_{k}} \frac{\partial y_{k}}{\partial w_{k j}^{(2)}} \end{aligned} ∂wkj(2)​∂En​​=∂yk​∂En​​∂wkj(2)​∂yk​​​
y k y_{k} yk​ 对网络权值 w k j ( 2 ) w_{k j}^{(2)} wkj(2)​ 的偏导 ∂ y k ∂ w k j ( 2 ) \frac{\partial y_{k}}{\partial w_{k j}^{(2)}} ∂wkj(2)​∂yk​​ ,

∂ y k ∂ w k j ( 2 ) = z j \begin{aligned} \frac{\partial y_{k}}{\partial w_{k j}^{(2)}}= z_{j} \end{aligned} ∂wkj(2)​∂yk​​=zj​​
因此损失函数对网络权值 w k j w_{k j} wkj​的梯度为 ,
∂ E n ∂ w k j ( 2 ) = δ k ⊗ z j \frac{\partial E_{n}}{\partial w_{k j}^{(2)} } = \delta_{k} \otimes z_{j} ∂wkj(2)​∂En​​=δk​⊗zj​

        # the gradient of the w_jk  input: z_j(1,20) gradient_y_k(1,3), output:(20,3)
        gradient_w_jk = np.outer(z_j , gradient_y_k ) 

6. 梯度更新公式

对于网络权值 w k j w_{k j} wkj​的参数更新公式为
w j k = w j k − η ∂ E n ∂ w k j ( 2 ) \begin{aligned} w_{j k}=w_{j k}-\eta \frac{\partial E_{n}}{\partial w_{k j}^{(2)}} \end{aligned} wjk​=wjk​−η∂wkj(2)​∂En​​​
对于网络权值 w j i w_{j i} wji​的参数更新公式为
w i j = w i j − η ∂ E n ∂ w j i ( 1 ) \begin{aligned} w_{i j}=w_{i j}-\eta \frac{\partial E_{n}}{\partial w_{j i}^{(1)}} \end{aligned} wij​=wij​−η∂wji(1)​∂En​​​

梯度更新公式的代码实现, 返回网络当前计算的损失, 其中learning_rate = 0.001

        # weight update
        self.W_jk = self.W_jk - self.learning_rate*gradient_w_jk

        self.W_ij[1:,:] = self.W_ij[1:,:] - self.learning_rate*gradient_w_ij
        self.W_ij[0,:] = self.W_ij[0,:] - self.learning_rate*gradient_W_i_j_1

7. 模型的使用

本次模型采用类实现,其调用过程如下,当想要训练其他的数据时,只需要将对应的数据按相应的格式读取,初始化对应大小的网络,然后将数据输入网络中即可。需要注意的是train方法只是使用一组数据更新一次,因此需要使用循环将所有数据进行训练,具体可参照完整源代码的实现。

# Initialize w ,learning_rate
model = MLP_model.MLP_Classification(n_features, n_hidden,
                     n_classes, learning_rate)

# Train the model x(1,4) label(1,3)
loss_ = model.train( x, label)

# Calculate the accuracy of the data  x(n,4) label(n,3)
accuracy = model.accuracy(x, label)

对于模型的保存和载入,该模型较为简单,因为网络参数只有 w k j , w j i w_{k j},w_{j i} wkj​,wji​,因此我们只需要保存这两个参数就可以了。

# save model
path = os.path.join('MLP_Parameters/model%s/' % (crossValidation))
folder = os.getcwd() +'/'+ path
if not os.path.exists(folder):
    os.makedirs(folder)
model.save(path)

# load model
model = MLP_model.MLP_Classification(n_features, n_hidden,
                                         n_classes, learning_rate)
model.load(path)

ref

  • D. Dua and C. Graff, “UCI machine learning repository,” 2017. [Online]. Available:http://archive.ics.uci.edu/ml

本文仅供参考, 若有错误, 欢迎指出, 共同学习

关注 钰哥lab 公众号,发送 MLP,即可获取完整源码

上一篇:使用Actuator监控


下一篇:使程序在用户长时间不操作时退出