深度学习入门 1 一个简单的反向传播
作者简介
梳影揽溪柔,来自浙江杭州,西北大学大一学生,非计算机专业。
博客简介
主要记录自学CL/NLP的日常。
前言
MNIST是深度学习入门时的一个经典的数据集,本文介绍一个基于MNIST的反向传播代码实例。
引库
import numpy as np
深度学习中的数据,很多情况下用numpy运算。
numpy的底层用C写成,运算速度比较快。
numpy的二维数组可以存放矩阵,适合描述神经网络。
from torchvision.datasets import MNIST
用Pytorch下载MNIST数据集比较方便。
下载和预处理
def load_mnist(normalize=True, one_hot_label=True):train_set = MNIST(root='./data', train=True, download=True)test_set = MNIST(root='./data', train=False, download=True)
MNIST有训练集(60000张)和数据集(10000张)两个子数据集,通过train控制下载。
root是数据集存放的文件夹,这里‘./data’是博主Pycharm里的相对路径。
x_train = train_set.data.numpy().astype(np.float32)
t_train = train_set.targets.numpy()
x_test = test_set.data.numpy().astype(np.float32)
t_test = test_set.targets.numpy()
这里用train_set.data获取Pytorch张量,.numpy()准备用numpy进行操作。
转换后张量的形状和数值都不变。训练集张量的形状是(60000,28,28),每行每列都是28个像素。
而数值是[0,255] 之间的整数,0纯黑色,255纯白色。
因为下一步要归一化,所以把整数强制类型转换为32位浮点数。
targets是一维张量(向量),存放每张首写字母的标签。
if normalize:x_train/=255x_test/=255x_train = x_train.reshape(-1, 784)x_test = x_test.reshape(-1, 784)
神经网络要避免数值溢出,所以要/255“归一化”到[0,1]之间。
为便于numpy神经网络处理,将三维张量转为二维张量(矩阵)。
这里reshape里第一个参数-1,可以根据总元素一致,自动计算这个值。
if one_hot_label:def to_one_hot(y, num_class=10):return np.eye(num_class)[y]t_train = to_one_hot(t_train)t_test = to_one_hot(t_test)
用one-hot编码作为分类标签,便于进行后续计算。
np.eye(x)会生成 x 阶的单位矩阵,再通过[y]取出对应的行,得到one-hot编码。
所以targets被批量转成了二维张量,训练集标签形状是(60000,10),测试集是(10000,10)。
return (x_train, t_train), (x_test, t_test)
神经网络
class TwoLayerNet:def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):self.params = {}self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)self.params['b1'] = np.zeros(hidden_size)self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)self.params['b2'] = np.zeros(output_size)
构建一个两层的神经网络的类,层的神经元个数可设置。用字典prams存放参数。
其中 randn 是 random normal(随机正态分布),固定生成若干随机小数。
这些小数需要满足,均值为0,方差为1。它们大多数都在(-3,3)之间。
层间矩阵W的权重都乘0.01,使得初始权重较小,但不过小。
过大会导致进入sigmoid的值过大,区域导数变化很小,不利于反向传播更新梯度。
过小或者直接设置为0,会导致神经元同质化,学习速度会很慢且效果下降。
def sigmoid(self, x):return 1 / (1 + np.exp(-x))
用sigmoid函数,可以把Wa+b算出的矩阵里的元素压在(0,1)内。
def softmax(self, x):if x.ndim == 2:x = x - np.max(x, axis=1, keepdims=True)return np.exp(x) / np.sum(np.exp(x), axis=1, keepdims=True)
用softmax函数实现最终输出,softmax函数可以把一组数转换成总和为1的概率分布。
由于训练集的数据量多达60000,一张张处理太慢了,所以引入批处理的概念。
比如100张照片为一个batch,最终输入给softmax的就会是一个二维张量(100,10)。
为防止e的x次方溢出,可以令x减去行内最大的那个值,对softmax函数简单变形可知成立。
因为np.max是找每行最大,会返回一个一维的张量。所以用keepdims控制为(100,1)。
def cross_entropy_error(self, y, t):batch_size = y.shape[0]return -np.sum(t * np.log(y + 1e-7)) / batch_size
损失函数这里用交叉熵,对应的t和logy相乘求和。
注意这里不是点乘,t和y都是(100,10)的二维张量,直接对应元素相乘。
必须要加一个小的数避免log(0)。数学上不可能,但计算机上有限精度的浮点数有可能。
def predict(self, x):W1, W2 = self.params['W1'], self.params['W2']b1, b2 = self.params['b1'], self.params['b2']a1 = np.dot(x, W1) + b1z1 = self.sigmoid(a1)a2 = np.dot(z1, W2) + b2y = self.softmax(a2)return y
前向传播,这里不具体解释了。
def loss(self, x, t):y = self.predict(x)return self.cross_entropy_error(y, t)
定义损失函数。
def accuracy(self, x, t):y = self.predict(x)y = np.argmax(y, axis=1)t = np.argmax(t, axis=1)return np.sum(y == t) / float(x.shape[0])
精度函数。找y概率最大的索引,和标签所在位置的索引是否匹配。
这里np.argmax返回数组中最大数的索引。
def gradient(self, x, t):W1, W2 = self.params['W1'], self.params['W2']b1, b2 = self.params['b1'], self.params['b2']batch_num = x.shape[0]a1 = np.dot(x, W1) + b1z1 = self.sigmoid(a1)a2 = np.dot(z1, W2) + b2y = self.softmax(a2)dy = (y - t) / batch_numgrads = {}grads['W2'] = np.dot(z1.T, dy)grads['b2'] = np.sum(dy, axis=0)dz1 = np.dot(dy, W2.T)da1 = dz1 * z1 * (1 - z1)grads['W1'] = np.dot(x.T, da1)grads['b1'] = np.sum(da1, axis=0)return grads
反向传播。博主刚接触博客园,还没学会怎么在这里打LaTeX。
没有数学公式无法讲,下一篇文章大概是以这段代码为样例,详解反向传播的数学原理。
主程序
if __name__ == '__main__':(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)iters_num = 10000train_size = x_train.shape[0]batch_size = 100learning_rate = 0.1train_loss_list = []train_acc_list = []test_acc_list = []iter_per_epoch =600network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)for i in range(iters_num):batch_mask = np.random.choice(train_size, batch_size)x_batch = x_train[batch_mask]t_batch = t_train[batch_mask]grad = network.gradient(x_batch, t_batch)for key in ('W1', 'b1', 'W2', 'b2'):network.params[key] -= learning_rate * grad[key]loss = network.loss(x_batch, t_batch)train_loss_list.append(loss)if i % iter_per_epoch == 0:train_acc = network.accuracy(x_train, t_train)test_acc = network.accuracy(x_test, t_test)train_acc_list.append(train_acc)test_acc_list.append(test_acc)print(f"Epoch {int(i/iter_per_epoch)+1} | 训练精度:{train_acc:.4f} | 测试精度:{test_acc:.4f} | loss:{loss:.4f}")
下载数据,设置各种参数和用于存放得到数据的列表。
用训练集训练,随机选择batch个数据,反向传播更新梯度,记录损失函数变化,迭代10000次。
跑完一次整个数据集称为一个Epoch,每跑完一个Epoch用训练集和测试集的数据评估情况。
结果
Epoch 1 | 训练精度:0.0986 | 测试精度:0.0958 | loss:2.2925
Epoch 2 | 训练精度:0.7843 | 测试精度:0.7872 | loss:0.8092
Epoch 3 | 训练精度:0.8781 | 测试精度:0.8817 | loss:0.4142
Epoch 4 | 训练精度:0.8987 | 测试精度:0.9006 | loss:0.3482
Epoch 5 | 训练精度:0.9074 | 测试精度:0.9095 | loss:0.3546
Epoch 6 | 训练精度:0.9135 | 测试精度:0.9148 | loss:0.2072
Epoch 7 | 训练精度:0.9186 | 测试精度:0.9197 | loss:0.1746
Epoch 8 | 训练精度:0.9238 | 测试精度:0.9242 | loss:0.3122
Epoch 9 | 训练精度:0.9265 | 测试精度:0.9264 | loss:0.2824
Epoch 10 | 训练精度:0.9302 | 测试精度:0.9299 | loss:0.2427
Epoch 11 | 训练精度:0.9328 | 测试精度:0.9335 | loss:0.1280
Epoch 12 | 训练精度:0.9364 | 测试精度:0.9366 | loss:0.1714
Epoch 13 | 训练精度:0.9388 | 测试精度:0.9389 | loss:0.2182
Epoch 14 | 训练精度:0.9412 | 测试精度:0.9410 | loss:0.1086
Epoch 15 | 训练精度:0.9427 | 测试精度:0.9422 | loss:0.1967
Epoch 16 | 训练精度:0.9448 | 测试精度:0.9446 | loss:0.0969
Epoch 17 | 训练精度:0.9465 | 测试精度:0.9461 | loss:0.2837
随机跑的一次,可以看到损失函数并不是一直下降的。
这和我们设置的learning rate(学习率)是固定的有关。
