实现内容:
实现一个三层感知机
对手写数字数据集进行分类
绘制损失值变化曲线
完成kaggle MNIST手写数字分类任务,根据给定的超参数训练模型,完成表格的填写
实现 数据集使用手写数字集。并且40%作测试集,60%做训练集。
1 2 3 4 5 6 7 8 import matplotlib.pyplot as plt%matplotlib inline from time import timeimport numpy as npfrom sklearn.datasets import load_digitsfrom sklearn.model_selection import train_test_splittrainX, testX, trainY, testY = train_test_split(load_digits()['data' ], load_digits()['target' ], test_size = 0.4 , random_state = 32 )
接下来是数据预处理,神经网络的训练方法一般是基于梯度的优化算法,如梯度下降,为了让这类算法能更好的优化神经网络,我们往往需要对数据集进行归一化,这里我们选择对数据进行标准化。
减去均值可以让数据以0为中心,除以标准差可以让数据缩放到一个较小的范围内。这样可以使得梯度的下降方向更多样,同时缩小梯度的数量级,让学习变得稳定。
首先需要对训练集进行标准化,针对每个特征求出其均值和标准差,然后用训练集的每个样本减去均值除以标准差,就得到了新的训练集。然后用测试集的每个样本,减去训练集的均值,除以训练集的标准差,完成对测试集的标准化。
1 2 3 4 5 trainY_mat = np.zeros((len (trainY), 10 )) trainY_mat[np.arange(0 , len (trainY), 1 ), trainY] = 1 testY_mat = np.zeros((len (testY), 10 )) testY_mat[np.arange(0 , len (testY), 1 ), testY] = 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 def initialize (h, K ): ''' 参数初始化 Parameters ---------- h: int: 隐藏层单元个数 K: int: 输出层单元个数 Returns ---------- parameters: dict,参数,键是"W1", "b1", "W2", "b2" ''' np.random.seed(32 ) W_1 = np.random.normal(size = (trainX.shape[1 ], h)) * 0.01 b_1 = np.zeros((1 , h)) np.random.seed(32 ) W_2 = np.random.normal(size = (h, K)) * 0.01 b_2 = np.zeros((1 , K)) parameters = {'W1' : W_1, 'b1' : b_1, 'W2' : W_2, 'b2' : b_2} return parameters
向前传播,这里具体指的就是依据公式向前计算值。
这里有一点要注意,矩阵的点乘是使用np.dot()
进行的,否则py会默认为元素乘。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 def linear_combination (X, W, b ): ''' 计算Z,Z = XW + b Parameters ---------- X: np.ndarray, shape = (n, m),输入的数据 W: np.ndarray, shape = (m, h),权重 b: np.ndarray, shape = (1, h),偏置 Returns ---------- Z: np.ndarray, shape = (n, h),线性组合后的值 ''' Z = np.dot(X,W) + b return Z
每一线性层的输出都要经过一个activate,隐藏层的activate为ReLu。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def ReLU (X ): ''' ReLU激活函数 Parameters ---------- X: np.ndarray,待激活的矩阵 Returns ---------- activations: np.ndarray, 激活后的矩阵 ''' X[X < 0 ] = 0 activations = X return activations
输出层要经过softmax找到每一个label的概率大小。这里值得注意的是,O矩阵的求和是对每一行的各个元素求和,而不是对所有元素求和,所以要有axis=1
,对行进行sum操作,并保持维度。
前一个my_softmax(O)
会导致对于较小值的output,会导致分母为0的情况,所以要对其进行一些处理,让O的每一个元素减去该行的最大值,这样能保证取exp后至少一个元素为1,所以不会出现NaN的情况。
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 def my_softmax (O ): ''' softmax激活 ''' return np.exp(O) / np.sum (np.exp(O), axis = 1 , keepdims = True ) def softmax (O ): ''' softmax激活函数 Parameters ---------- O: np.ndarray,待激活的矩阵 Returns ---------- activations: np.ndarray, 激活后的矩阵 ''' O = O - np.max (O, axis=1 , keepdims=True ) activations = my_softmax(O) return activations
接下来是实现损失函数,交叉熵损失函数: 这里又会出一个问题,交叉熵损失函数中,我们需要对softmax的激活值取对数,也就是log\haty,这就要求我们的激活值全都是大于0的数,不能等于0,但是我们实现的softmax在有些时候确实会输出0。这就使得在计算loss的时候会出现问题,解决这个问题的方法是log softmax。所谓log softmax,就是将交叉熵中的对数运算与softmax结合起来,避开为0的情况。
这样我们再计算loss的时候就可以把输出层的输出直接放到log softmax中计算,不用先激活,再取对数了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def log_softmax (x ): ''' log softmax Parameters ---------- x: np.ndarray,待激活的矩阵 Returns ---------- log_activations: np.ndarray, 激活后取了对数的矩阵 ''' log_activations = x - np.max (x) - np.log( np.sum (np.exp(x - np.max (x)), axis = 1 , keepdims = True ) ) return log_activations
然后编写cross_entropy_with_softmax
。函数内容不再赘述。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 def cross_entropy_with_softmax (y_true, O ): ''' 求解交叉熵损失函数,这里需要使用log softmax,所以参数分别是真值和未经softmax激活的输出值 Parameters ---------- y_true: np.ndarray,shape = (n, K), 真值 O: np.ndarray, shape = (n, K),softmax激活前的输出层的输出值 Returns ---------- loss: float, 平均的交叉熵损失值 ''' loss = - 1 /len (y_true) * np.sum (np.sum (y_true * log_softmax(O))) return loss
正是因为softmax激活与交叉熵损失会有这样的问题,所以在很多深度学习框架中,交叉熵损失函数就直接带有了激活的功能,所以我们在实现前向传播计算的时候,就不要加softmax激活函数了。
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 def forward (X, parameters ): ''' 前向传播,从输入一直到输出层softmax激活前的值 Parameters ---------- X: np.ndarray, shape = (n, m),输入的数据 parameters: dict,参数 Returns ---------- O: np.ndarray, shape = (n, K),softmax激活前的输出层的输出值 ''' Z = np.dot(X, parameters['W1' ]) + parameters['b1' ] H = ReLU(Z) O = np.dot(H, parameters['W2' ]) + parameters['b2' ] return O
下面是反向传播,也是本篇blog的重点。首先是偏导的推导,细节不再赘述,使用链式求导法则认真推导即可。
forward公式:最后一层的输出,使用softmax函数激活,得到神经网络计算出的各类的概率值。
损失函数对参数W_2和b_2的偏导数:
求得loss对W_1和b_1的偏导数:
ReLu的偏导数:
从而:
描述完公式后下面来用代码实现,首先dW2和db2的代码是很显然的。对于dW1,这里涉及到的ReLu的偏导数,很显然如果hidden层的值小于零对应ReLu为0时,定义其偏导为0,那么如何确定dW1中的那些值是由该定义得到的呢。如果我们眼光狭窄只分析dW2公式的最后结果必然很难分析出来,因为最终的dW2是(hidden, output)维度的,而relu_regard是(n, hidden)维度的,直接对它们进行关联显然不现实。那么需要追根溯源,深入了解这个dW2的来由。
在dW2分段函数的前一步,它的结果是XT点积后面的一堆,其中H对Z的偏导其实就是ReLu的偏导,是在这里决定了dW2的值,再来分析一下维度1/n可以broadcast不用管,后面是(n, input)T · [(n, output) · (hidden, output)T * (n, hidden)]
这样的维度关系。这里尤其要注意最后一个运算,我一开始卡在这里好久,因为这里涉及到了元素乘,(n, hidden) * (n, hidden)
,这里决定了哪个计算位置的值来自于ReLu的0,元素乘后再与X的转置计算。
db2同理。
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 def compute_gradient (y_true, y_pred, H, Z, X, parameters ): ''' 计算梯度 Parameters ---------- y_true: np.ndarray,shape = (n, K), 真值 y_pred: np.ndarray, shape = (n, K),softmax激活后的输出层的输出值 H: np.ndarray, shape = (n, h),隐藏层激活后的值 Z: np.ndarray, shape = (n, h), 隐藏层激活前的值 X: np.ndarray, shape = (n, m),输入的原始数据 parameters: dict,参数 Returns ---------- grads: dict, 梯度 ''' dW2 = (1 /len (y_true)) * np.dot(np.transpose(H), (y_pred - y_true)) db2 = (1 /len (y_true)) * np.sum (y_pred - y_true, axis=0 ) relu_grad = Z.copy() relu_grad[relu_grad >= 0 ] = 1 relu_grad[relu_grad < 0 ] = 0 dW1 = 1 /len (y_true) * np.dot(np.transpose(X), np.dot((y_pred - y_true), np.transpose(parameters['W2' ])) * relu_grad ) db1 = 1 /len (y_true) * np.sum (np.dot((y_pred - y_true), np.transpose(parameters['W2' ])) * relu_grad, axis=0 ) grads = {'dW2' : dW2, 'db2' : db2, 'dW1' : dW1, 'db1' : db1} return grads
梯度下降,反向传播,参数更新。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def update (parameters, grads, learning_rate ): ''' 参数更新 Parameters ---------- parameters: dict,参数 grads: dict, 梯度 learning_rate: float, 学习率 ''' parameters['W2' ] -= learning_rate * grads['dW2' ] parameters['b2' ] -= learning_rate * grads['db2' ] parameters['W1' ] -= learning_rate * grads['dW1' ] parameters['b1' ] -= learning_rate * grads['db1' ]
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 def backward (y_true, y_pred, H, Z, X, parameters, learning_rate ): ''' 计算梯度,参数更新 Parameters ---------- y_true: np.ndarray,shape = (n, K), 真值 y_pred: np.ndarray, shape = (n, K),softmax激活后的输出层的输出值 H: np.ndarray, shape = (n, h),隐藏层激活后的值 Z: np.ndarray, shape = (n, h), 隐藏层激活前的值 X: np.ndarray, shape = (n, m),输入的原始数据 parameters: dict,参数 learning_rate: float, 学习率 ''' grads = compute_gradient(y_true, y_pred, H, Z, X, parameters) update(parameters, grads, 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 57 def train (trainX, trainY, testX, testY, parameters, epochs, learning_rate = 0.01 , verbose = False ): ''' 训练 Parameters ---------- Parameters ---------- trainX: np.ndarray, shape = (n, m), 训练集 trainY: np.ndarray, shape = (n, K), 训练集标记 testX: np.ndarray, shape = (n_test, m),测试集 testY: np.ndarray, shape = (n_test, K),测试集的标记 parameters: dict,参数 epochs: int, 要迭代的轮数 learning_rate: float, default 0.01,学习率 verbose: boolean, default False,是否打印损失值 Returns ---------- training_loss_list: list(float),每迭代一次之后,训练集上的损失值 testing_loss_list: list(float),每迭代一次之后,测试集上的损失值 ''' training_loss_list = [] testing_loss_list = [] for i in range (epochs): Z = linear_combination(trainX, parameters['W1' ], parameters['b1' ]) H = ReLU(Z) train_O = linear_combination(H, parameters['W2' ], parameters['b2' ]) train_y_pred = softmax(train_O) training_loss = cross_entropy_with_softmax(trainY, train_O) test_O = forward(testX, parameters) testing_loss = cross_entropy_with_softmax(testY, test_O) if verbose == True : print ('epoch %s, training loss:%s' %(i + 1 , training_loss)) print ('epoch %s, testing loss:%s' %(i + 1 , testing_loss)) print () training_loss_list.append(training_loss) testing_loss_list.append(testing_loss) backward(trainY, train_y_pred, H, Z, trainX, parameters, learning_rate) return training_loss_list, testing_loss_list
绘制loss随epoch的变化曲线。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def plot_loss_curve (training_loss_list, testing_loss_list ): ''' 绘制损失值变化曲线 Parameters ---------- training_loss_list: list(float),每迭代一次之后,训练集上的损失值 testing_loss_list: list(float),每迭代一次之后,测试集上的损失值 ''' plt.figure(figsize = (10 , 6 )) plt.plot(training_loss_list, label = 'training loss' ) plt.plot(testing_loss_list, label = 'testing loss' ) plt.xlabel('epoch' ) plt.ylabel('loss' ) plt.legend()
预测
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 def predict (X, parameters ): ''' 预测,调用forward函数完成神经网络对输入X的计算,然后完成类别的划分,取每行最大的那个数的下标作为标记 Parameters ---------- X: np.ndarray, shape = (n, m), 训练集 parameters: dict,参数 Returns ---------- prediction: np.ndarray, shape = (n, 1),预测的标记 ''' O = forward(X, parameters) y_pred = softmax(O) prediction = np.argmax(y_pred, axis=1 ) return prediction
训练一个不算特别优秀的3-layer-perceptron。
1 2 3 4 5 6 7 8 9 10 11 12 13 from sklearn.metrics import accuracy_scorestart_time = time() h = 50 K = 10 parameters = initialize(h, K) training_loss_list, testing_loss_list = train(trainX, trainY_mat, testX, testY_mat, parameters, 1000 , 0.03 , False ) end_time = time() print ('training time: %s s' %(end_time - start_time))prediction = predict(testX, parameters) print (accuracy_score(prediction, testY))plot_loss_curve(training_loss_list, testing_loss_list)
到这里就结束了,其实不算复杂,数值计算的细节比较重要,以前经常用pytorch来写BP、RNN之类的,但是很少从底层去实现过,这还是一个简单的感知机模型,较复杂的基础模型涉及到的内容可能更复杂。只能说我企图学会吧。