深度学习Step by Step
写在前面
本教程是有关深度学习的入门教程,基于一个国产的人工智能引擎,mindspore,主要参考了《深度学习》以及《动手学深度学习》,使用MindSpore实现了《动手学深度学习》中很多有用的练习,可以作为深度学习以及MindSpore的入门教程。
阅读本教程需要有一定的Python基础。
你可以访问这个连接进行下载MindSpore:https://www.mindspore.cn/install。
所有代码可以在仓库中获取。
线性神经网络
在历史上,神经网络有三次浪潮,其一是随着控制论的发展,奠定了神经网络的基本结构(感知机);第二次是联结主义的发展,使用了反向传播;深度学习则是近十几年发展而来,主要的代表有卷积神经网络等。
下面通过numpy来实现线性回归,以介绍深度学习的基本知识。
产生数据
假设有一个线性生成器,以下式产生数值,其实W与B是未知的矩阵,N是高斯分布的噪声
Y = W X T + B + N Y = W X^T + B + N Y=WXT+B+N
我们通过下面这个程序来生成n个数据,并添加均值为0,方差为0.01的噪声。
def synthetic_data(w, b, n):
x = np.random.normal(0, 1, (n, 1))
y = np.matmul(x, w) + b
# add noise
y += np.random.normal(0, 0.01, y.shape)
return x, y
线性网络
我们要通过数据来估计出W与B的取值,使用线性网络
y ^ = w ^ x T + b ^ \hat y = \hat w x^T + \hat b y^=w^xT+b^
可以通过下面这个式子来实现
def linreg(x, w, b):
return np.matmul(w, x.T) + b
损失函数
我们需要评估我们与目标函数之间的误差,并称这个函数为损失函数。在这里,我们采用平方误差(也叫L2损失)来衡量我们的预测函数与目标函数之间的差距。注意到我们对目标函数的 X X X与 B B B都是未知的,所以我们只能在给定 X X X的条件下,确定预测函数输出的 y ^ \hat y y^与目标函数输出的真实 y y y之间的差距,即
L o s s ( y , y ^ ) = ( y ^ − y ) 2 2 Loss(y, \hat y) = \frac{(\hat y - y)^2}2 Loss(y,y^)=2(y^−y)2
上式需要除二是因为求导后的表达式更简洁。我们通过下面的程序实现:
def squared_loss(y_hat, y):
return np.square(y_hat - y)/2
批量计算
我们通过来打乱数据,提高计算速度。
以下是实现的代码,首先,我们要产生一组索引,通过np.random.shuffle
将其打乱,然后以batch_size
为一个批次,用np.mat
生成将一系列矩阵作为索引,最后通过yield
返回数据。
def data_iter(batch_size, features, labels):
num_examples = len(features)
indices = list(range(num_examples))
np.random.shuffle(indices)
for i in range(0, num_examples, batch_size):
batch_indices = np.mat(indices[i:min(i + batch_size, num_examples)])
yield features[batch_indices, :], labels[:, batch_indices]
优化器
梯度
我们初始化设置的 W W W是高斯分布的, B B B为0,可以想到,当我们输入一个 X X X,此时线性网络的输出结果 y ^ \hat y y^与真实值之间存在有较大的差距,所以我们需要通过某种方法来更新 W W W与 B B B。
作为线性函数时,我们很容易想到使用最小二乘法来计算最佳的 W W W与 B B B,但最小二乘法并不是什么时候都能够使用的。在深度学习中,我们由远及近,通过计算梯度的方式,来更新网络的参数。
什么是梯度呢?用一个简单的例子,比如有个函数, y = 1 2 x 2 y=\frac 1 2 x^2 y=21x2,我们要寻找x值的最小值,虽然从数学上我们能很容易地知道该函数的最小值是0,但假设我们现在还不知道,如何寻找该函数的最小值呢?根据微积分,加入我们有一个点 x 0 x_0 x0,我们将其带入函数中求导,得到 y ′ = x 0 y'=x_0 y′=x0,如果 x 0 > 0 x_0>0 x0>0,则 y ′ > 0 y'>0 y′>0,此时往 y ′ y' y′减小的方向(即 − y ′ -y' −y′方向)更新 x 0 x_0 x0,就有希望达到极小值;如果 x 0 < 0 x_0<0 x0<0,则 y ′ < 0 y'<0 y′<0,此时往 y ′ y' y′增大的方向(即 − y ′ -y' −y′方向)更新 x 0 x_0 x0,就有希望达到极小值。从上面的例子看出,不管如何,往导数方向相反的方向,就有可能找到函数的极小值。
梯度,则是一元函数导数的推广,即对多元函数来说,梯度方向是函数值增长的方向;而梯度的反方向,有可能能够找到函数的极小值。
链式法则
对多元函数来说,可以链式法则进行求导。
反向传播
我们将由数据 x x x得到 y ^ \hat y y^的过程,称为正向传播,此时我们可以计算出梯度。根据得到的梯度,反向更新网络的参数,该过程被称为反向传播。
随机梯度下降(SGD)
我们要使 y ^ → y \hat y \to y y^→y,等价于使损失函数最小化,也就是对损失函数求 W W W与 B B B的梯度,来更新 W W W与 B B B的权重。
所以,我们根据以下式子来更新线性函数的 W W W与 B B B
w ^ − l r ∗ ∂ L o s s ∂ w ^ → w ^ \hat w - lr * \frac{\partial Loss}{\partial \hat w} \to \hat w w^−lr∗∂w^∂Loss→w^
b ^ − l r ∗ ∂ L o s s ∂ b ^ → b ^ \hat b - lr * \frac{\partial Loss}{\partial \hat b} \to \hat b b^−lr∗∂b^∂Loss→b^
式中的 l r lr lr是学习率,目的是将梯度的大小进行缩放,避免以太大的范围更新权限。
首先我们来计算一下 W W W与 B B B的梯度,根据损失函数,将 y ^ \hat y y^代入,有
L o s s ( y , x , b ) = ( w ^ × x + b ^ − y ) 2 2 Loss(y, x, b) = \frac{(\hat w \times x + \hat b - y)^2}2 Loss(y,x,b)=2(w^×x+b^−y)2
如果我们对他求导,可以得到以下两个式子
∂ L o s s ∂ w ^ = ( w ^ × x + b ^ − y ) × x = ( y ^ − y ) × x \frac{\partial Loss}{\partial \hat w}= (\hat w \times x + \hat b - y)\times x = (\hat y - y)\times x ∂w^∂Loss=(w^×x+b^−y)×x=(y^−y)×x
∂ L o s s ∂ b ^ = w ^ × x + b ^ − y ) = ( y ^ − y ) \frac{\partial Loss}{\partial \hat b} = \hat w \times x + \hat b - y) = (\hat y - y) ∂b^∂Loss=w^×x+b^−y)=(y^−y)
我们通过下面的代码实现上述的过程。
def linreg_sgd(w, b, x, y, lr, batch_size):
# manual calculate the gradient of squared loss
y_hat = linreg(x.squeeze(axis=0), w, b)
grad_w = (y_hat - y.squeeze(axis=0))*x.squeeze(axis=0)
grad_b = (y_hat - y.squeeze(axis=0))
grad_b = grad_b.sum(axis=1)
new_w = w-lr*grad_w/batch_size
new_b = b-lr*grad_b/batch_size
return new_w, new_b
原理上,我们要在所有的样本中计算出梯度再进行更新,但现在我们只计算了一个batch size样本的梯度就对网络参数进行了更新,这个batch size样本的选择具有一定的随机性,所以称为随机梯度下降。
程序逻辑
首先,我们设置真实的分布
true_w = np.mat([2, -3.4])
true_b = np.array([4.2])
然后,生成1000个样本数据
features, labels = synthetic_data(true_w, true_b, 1000)
设置批量为10,学习率0.03,训练三轮,
batch_size = 10
lr = 0.03
epochs = 3
以均值为0,方差为0.01设置需要训练的w,以0设置b。
w = np.random.normal(0, 0.01, (1, 2))
b = np.zeros((1, 1))
print(f'Initial w={w}, b={b}')
设置网络为线性网络,损失函数为平方差函数。
net = linreg
loss = squared_loss
最后一步,输入数据,获取梯度,更新 w w w, b b b的值,计算损失函数。
for epoch in range(epochs):
for x, y in data_iter(batch_size, features, labels):
w, b = linreg_sgd(w, b, x, y, lr, batch_size)
train_loss = loss(net(features, w, b), labels)
print(f'In epoch {epoch + 1}, loss is {float(train_loss.mean()):f}')
最后我们可以得到估计的 w w w与 b b b。
print(f'estimate w is {w}, b is {b}')
print(f'estimate error is that w is {w-true_w} and b is {b-true_b}')
实现结果
以下是程序的输出结果
Initial w=[[-0.0204044 -0.0061789]], b=[[0.]]
In epoch 1, loss is 0.035662
In epoch 2, loss is 0.000127
In epoch 3, loss is 0.000049
estimate w is [[ 1.9999425 -3.39916696]], b is [[4.19945344]]
estimate error is that w is [[-5.74996689e-05 8.33035609e-04]] and b is [[-0.00054656]]
(2022年10月15日更新)