深度学习课程 DAY 3 - 构建神经网络模型(二)
Chapter 2 构建神经网络模型
2.2 代码实现-构建神经网络模型
(4)训练过程
上述计算过程描述了如何构建神经网络,通过神经网络完成预测值和损失函数的计算。接下来介绍如何求解参数w和b的数值,这个过程也称为模型训练过程。训练过程是深度学习模型的关键要素之一,其目标是让定义的损失函数Loss尽可能的小,也就是说找到一个参数解w和b,使得损失函数取得极小值。
总结来说,计算损失函数由三个部分组成:真实样本的特征x、真实样本的输出y和参数。由于真实样本是确定的,因此损失函数为以参数为自变量的一个函数。
右图假设横轴自变量为参数,纵轴因变量为y,当曲线达到最低点时斜率为0,即导数为0。导数为0的点为函数的极值点。
理论上,Loss函数对参数w和参数b分别取导数,令导数为0,将样本数据(x,y)带入上面的方程组中即可求解出w和b的值,能求解出参数w和b。
但实际上应用,导函数很可能为不可逆的函数或有不可逆的倾向,即由x求y容易,由y求x难。因此可以采用摸索的形式找到最低点,这种方法为梯度下降法。
1)梯度下降法
训练的关键是找到一组(w,b),使得损失函数L取极小值。我们先看一下损失函数LLL只随两个参数w5、w9变化时的简单情形,方便展示3D模型,启发寻解的思路。
L=L(w5,w9)
这里我们将w0,w1,…,w12中除w5、w9之外的参数和b都固定下来,可以用图画出L(w5,w9)的形式。
net = Network(13)
losses = []
#设定取值范围,只画出参数w5和w9在区间[-160, 160]的曲线部分,以及包含损失函数的极值
w5 = (-160.0, 160.0, 1.0) #np.arange()函数,返回一个有终点和起点的固定步长的排列
w9 = np.arange(-160.0, 160.0, 1.0)
losses = np.zeros([len(w5), len(w9)]) #返回来一个给定形状和类型的用0填充的数组
##双层循环计算设定区域内每个参数取值所对应的Loss
for i in range(len(w5)):
for j in range(len(w9)):
net.w[5] = w5[i]
net.w[9] = w9[j]
z = net.forward(x)
loss = net.loss(z, y)
losses[i, j] = loss
#使用matplotlib将两个变量和对应的Loss作3D图
#x轴y轴为w5和w9,z轴为loss值
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure() #创建画板
ax = Axes3D(fig) #绘制3D图形
w5, w9 = np.meshgrid(w5, w9) #x轴y轴每隔一个点进行打点,
ax.plot_surface(w5, w9, losses, rstride=1, cstride=1, cmap='rainbow') #用彩虹图绘制,指定xyz轴,指定行列跨度,设置颜色映射
plt.show()
从图中可以看出有些区域的函数值明显比周围的点小。选择w5和w9来画图,因为选择这两个参数的时候,可比较直观的从损失函数的曲面图上发现极值点的存在。
观察上述曲线呈现出“圆滑”的坡度,这正是我们选择以均方误差作为损失函数的原因之一。下图呈现了只有一个参数维度时,均方误差和绝对值误差(只将每个样本的误差累加,不做平方处理)的损失函数曲线图。
由此可见,均方误差表现的“圆滑”的坡度有两个好处:
- 曲线的最低点是可导的。
- 越接近最低点,曲线的坡度逐渐放缓,有助于通过当前的梯度来判断接近最低点的程度(是否逐渐减少步长,以免错过最低点)。
而绝对值误差是不具备这两个特性的,这也是损失函数的设计不仅仅要考虑“合理性”,还要追求“易解性”的原因。
现在我们要找出一组[w5,w9] 的值,使得损失函数最小,实现梯度下降法的方案如下:
- 步骤1:随机的选一组初始值,例如:[w5,w9] =[−100.0,−100.0]
- 步骤2:选取下一个点[w5’,w9’],使得L(w5’,w9’)<L(w5,w9)
- 步骤3:重复步骤2,直到损失函数几乎不再下降。
如何选择[w5’,w9’]是至关重要的,第一要保证L是下降的,第二要使得下降的趋势尽可能的快。
微积分的基础知识告诉我们,沿着梯度的反方向,是函数值下降最快的方向。函数在某一个点的梯度方向是曲线切线的方向,即曲线斜率最大的方向;但梯度方向是向上的,所以下降最快的是梯度的反方向。
2)计算梯度
改写损失函数的计算方法。为了使梯度计算更加简洁,引入因子 1 2 \frac{1}{2} 21,定义损失函数如下:
其中zi是网络对第i个样本的预测值:
由梯度的定义,就是对变量取偏导:
可以计算出L对w和b的偏导数:
从导数的计算过程可以看出,因子 1 2 \frac{1}{2} 21被消掉了,这是因为二次函数求导的时候会产生因子2,这也是我们将损失函数改写的原因。
下面我们考虑只有一个样本的情况下,计算梯度:
可以计算出:
可以计算出L对w和b的偏导数:
只有一个样本时,通过具体的程序查看每个变量的数据和维度。
x1 = x[0] #x特征变量值
y1 = y[0] #y真实值
z1 = net.forward(x1) #z预测值
print('x1 {}, shape {}'.format(x1, x1.shape))
print('y1 {}, shape {}'.format(y1, y1.shape))
print('z1 {}, shape {}'.format(z1, z1.shape))
输出
x1 [-0.02146321 0.03767327 -0.28552309 -0.08663366 0.01289726 0.04634817
0.00795597 -0.00765794 -0.25172191 -0.11881188 -0.29002528 0.0519112
-0.17590923], shape (13,)
y1 [-0.00390539], shape (1,)
z1 [-12.05947643], shape (1,)
按上面的公式,当只有一个样本时,可以计算某个wj,比如w0的梯度。
gradient_w0 = (z1 - y1) * x1[0]
print('gradient_w0 {}'.format(gradient_w0))
gradient_w0 [0.25875126]
同样我们可以计算w1的梯度。
gradient_w1 = (z1 - y1) * x1[1]
print('gradient_w1 {}'.format(gradient_w1))
gradient_w1 [-0.45417275]
依次计算w2的梯度。
gradient_w2= (z1 - y1) * x1[2]
print('gradient_w1 {}'.format(gradient_w2))
gradient_w2 [3.44214394]
3)使用Numpy进行梯度计算
基于Numpy广播机制(对向量和矩阵计算如同对1个单一变量计算一样),可以更快速的实现梯度计算。
- step1 计算1个样本13个参数的梯度
计算梯度的代码中直接用(z1−y1)⋅x1,得到的是一个13维的向量,每个分量分别代表该维度的梯度。
gradient_w = (z1 - y1) * x1 #这里x1是13维的向量,梯度也是向量
print('gradient_w_by_sample1 {}, gradient.shape {}'.format(gradient_w, gradient_w.shape))
输出
gradient_w_by_sample1 [ 0.25875126 -0.45417275 3.44214394 1.04441828 -0.15548386 -0.55875363
-0.09591377 0.09232085 3.03465138 1.43234507 3.49642036 -0.62581917
2.12068622], gradient.shape (13,)
输入数据中有多个样本,每个样本都对梯度有贡献。如上代码计算了只有样本1时的梯度值,同样的计算方法也可以计算样本2和样本3对梯度的贡献。
x2 = x[1]
y2 = y[1]
z2 = net.forward(x2)
gradient_w = (z2 - y2) * x2
print('gradient_w_by_sample2 {}, gradient.shape {}'.format(gradient_w, gradient_w.shape))
输出
gradient_w_by_sample2 [ 0.7329239 4.91417754 3.33394253 2.9912385 4.45673435 -0.58146277
-5.14623287 -2.4894594 7.19011988 7.99471607 0.83100061 -1.79236081
2.11028056], gradient.shape (13,)
x3 = x[2]
y3 = y[2]
z3 = net.forward(x3)
gradient_w = (z3 - y3) * x3
print('gradient_w_by_sample3 {}, gradient.shape {}'.format(gradient_w, gradient_w.shape))
输出
gradient_w_by_sample3 [ 0.25138584 1.68549775 1.14349809 1.02595515 1.5286008 -1.93302947
0.4058236 -0.85385157 2.46611579 2.74208162 0.28502219 -0.46695229
2.39363651], gradient.shape (13,)
有404个样本,计算13个参数的梯度,可以写双层for循环计算一个样本所有参数的梯度和所有样本对参数的梯度,然后再作平均。实际上可以用Numpy矩阵操作来简化运算。
# 注意这里是一次取出3个样本的数据,不是取出第3个样本
x3samples = x[0:3]
y3samples = y[0:3]
z3samples = net.forward(x3samples)
print('x {}, shape {}'.format(x3samples, x3samples.shape))
print('y {}, shape {}'.format(y3samples, y3samples.shape))
print('z {}, shape {}'.format(z3samples, z3samples.shape))
输出
x [[-0.02146321 0.03767327 -0.28552309 -0.08663366 0.01289726 0.04634817
0.00795597 -0.00765794 -0.25172191 -0.11881188 -0.29002528 0.0519112
-0.17590923]
[-0.02122729 -0.14232673 -0.09655922 -0.08663366 -0.12907805 0.0168406
0.14904763 0.0721009 -0.20824365 -0.23154675 -0.02406783 0.0519112
-0.06111894]
[-0.02122751 -0.14232673 -0.09655922 -0.08663366 -0.12907805 0.1632288
-0.03426854 0.0721009 -0.20824365 -0.23154675 -0.02406783 0.03943037
-0.20212336]], shape (3, 13)
y [[-0.00390539]
[-0.05723872]
[ 0.23387239]], shape (3, 1)
z [[-12.05947643]
[-34.58467747]
[-11.60858134]], shape (3, 1)
上面的x3samples, y3samples, z3samples的第一维大小均为3,表示有3个样本,为3×13的矩阵。下面计算这3个样本对梯度的贡献。
radient_w = (z3samples - y3samples) * x3samples
print('gradient_w {}, gradient.shape {}'.format(gradient_w, gradient_w.shape))
输出
gradient_w [[ 0.25875126 -0.45417275 3.44214394 1.04441828 -0.15548386 -0.55875363
-0.09591377 0.09232085 3.03465138 1.43234507 3.49642036 -0.62581917
2.12068622]
[ 0.7329239 4.91417754 3.33394253 2.9912385 4.45673435 -0.58146277
-5.14623287 -2.4894594 7.19011988 7.99471607 0.83100061 -1.79236081
2.11028056]
[ 0.25138584 1.68549775 1.14349809 1.02595515 1.5286008 -1.93302947
0.4058236 -0.85385157 2.46611579 2.74208162 0.28502219 -0.46695229
2.39363651]], gradient.shape (3, 13)
此处可见,计算梯度gradient_w的维度是3×133 ,并且其第1行与上面第1个样本计算的梯度gradient_w_by_sample1一致,第2行与上面第2个样本计算的梯度gradient_w_by_sample2一致,第3行与上面第3个样本计算的梯度gradient_w_by_sample3一致。这里使用矩阵操作,可以更加方便的对3个样本分别计算各自对梯度的贡献。
那么对于有N个样本的情形,我们可以直接使用如下方式计算出所有样本对梯度的贡献,这就是使用Numpy库广播功能带来的便捷。 小结一下这里使用Numpy库的广播功能:
- 一方面可以扩展参数的维度,代替for循环来计算1个样本对从w0到w12的所有参数的梯度。
- 另一方面可以扩展样本的维度,代替for循环来计算样本0到样本403对参数的梯度。
因此,利用Numpy库的广播功能对梯度的计算如下:
z = net.forward(x)
gradient_w = (z - y) * x
print('gradient_w shape {}'.format(gradient_w.shape))
print(gradient_w)
输出
gradient_w shape (404, 13)
[[ 0.25875126 -0.45417275 3.44214394 ... 3.49642036 -0.62581917
2.12068622]
[ 0.7329239 4.91417754 3.33394253 ... 0.83100061 -1.79236081
2.11028056]
[ 0.25138584 1.68549775 1.14349809 ... 0.28502219 -0.46695229
2.39363651]
...
[ 14.70025543 -15.10890735 36.23258734 ... 24.54882966 5.51071122
26.26098922]
[ 9.29832217 -15.33146159 36.76629344 ... 24.91043398 -1.27564923
26.61808955]
[ 19.55115919 -10.8177237 25.94192351 ... 17.5765494 3.94557661
17.64891012]]
上面gradient_w的每一行代表了一个样本对梯度的贡献。根据梯度的计算公式,总梯度是对每个样本对梯度贡献的平均值。
我们也可以使用Numpy的均值函数来完成此过程:
# axis = 0 表示把每一行做相加然后再除以总的行数
gradient_w = np.mean(gradient_w, axis=0)
print('gradient_w ', gradient_w.shape)
print('w ', net.w.shape)
print(gradient_w)
print(net.w)
gradient_w (13,)
w (13, 1)
[ 1.59697064 -0.92928123 4.72726926 1.65712204 4.96176389 1.18068454
4.55846519 -3.37770889 9.57465893 10.29870662 1.3900257 -0.30152215
1.09276043]
[[ 1.76405235e+00]
[ 4.00157208e-01]
[ 9.78737984e-01]
[ 2.24089320e+00]
[ 1.86755799e+00]
[ 1.59000000e+02]
[ 9.50088418e-01]
[-1.51357208e-01]
[-1.03218852e-01]
[ 1.59000000e+02]
[ 1.44043571e-01]
[ 1.45427351e+00]
[ 7.61037725e-01]]
我们使用Numpy的矩阵操作方便地完成了gradient的计算,但引入了一个问题,gradient_w的形状是(13,),而www的维度是(13, 1)。导致该问题的原因是使用np.mean函数时消除了第0维。为了加减乘除等计算方便,gradient_w和w必须保持一致的形状。因此我们将gradient_w的维度也设置为(13,1),代码如下:
gradient_w = gradient_w[:, np.newaxis] #np.newaxis的作用是在这一位置增加一个一维
print('gradient_w shape', gradient_w.shape)
综合上面的剖析,计算w的梯度的代码如下所示。
z = net.forward(x)
gradient_w = (z - y) * x
gradient_w = np.mean(gradient_w, axis=0)
gradient_w = gradient_w[:, np.newaxis]
gradient_w
输出
array([[ 1.59697064],
[-0.92928123],
[ 4.72726926],
[ 1.65712204],
[ 4.96176389],
[ 1.18068454],
[ 4.55846519],
[-3.37770889],
[ 9.57465893],
[10.29870662],
[ 1.3900257 ],
[-0.30152215],
[ 1.09276043]])
上述代码非常简洁地完成了w的梯度计算。同样,计算b的梯度的代码也是类似的原理。
gradient_b = (z - y)
gradient_b = np.mean(gradient_b)
# 此处b是一个数值,所以可以直接用np.mean得到一个标量
gradient_b
输出
-1.0918438870293816e-13
将上面计算w和b的梯度的过程,写成Network类的gradient
函数,实现方法如下所示。
定义:
class Network(object):
def __init__(self, num_of_weights):
# 随机产生w的初始值
# 为了保持程序每次运行结果的一致性,此处设置固定的随机数种子
np.random.seed(0)
self.w = np.random.randn(num