目录
第2章 神经网络的数学基础
2.1 初识神经网络
# 加载Keras中的MNIST数据集
from keras.datasets import mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
print(train_images.shape)
print(len(train_labels))
print(train_labels)
print(test_images.shape)
print(len(test_labels))
print(test_labels)
(60000, 28, 28)
60000
[5 0 4 ... 5 6 8]
(10000, 28, 28)
10000
[7 2 1 ... 4 5 6]
接下来的工作流程如下:首先,将训练数据(train_images和train_labels)输入神经网络;其次,网络学习将图像和标签关联在一起;最后,网络对test_images生成预测,而我们将验证这些预测与test_labels中的标签是否匹配。
神经网络的核心组件是层,它是一种数据处理模块,你可以将它看成数据过滤器,进去一些数据,出来的数据变得更加有用。具体来说,层从数据中提取表示——我们期望这种表示有助于解决手头的问题。大多数深度学习都是将简单的层链接起来,从而实现渐进式的数据蒸馏。深度学习模型就像是数据处理的筛子,包含一系列越来越精细的数据过滤器(即层)。
想要训练网络,我们还需要选择编译步骤的三个参数。
1.损失函数:网络如何衡量在训练数据上的性能,即网络如何朝着正确的方向前进;
2.优化器:基于训练数据和损失函数来更新网络的机制;
3.在训练和测试过程中需要监控的指标:本例只关心精度,即正确分类的图像所占的比例。
from keras import models
from keras import layers
network = models.Sequential()
network.add(layers.Dense(512, activation='relu', input_shape=(28 * 28,)))
network.add(layers.Dense(10, activation='softmax'))
network.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])
在开始训练之前,我们将对数据进行预处理,将其变换为网络要求的形状,并缩放到所有值都在[0,1]区间。
train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype('float32') / 255
test_images = test_images.reshape((10000, 28 * 28))
test_images = test_images.astype('float32') / 255
我们还需要对标签进行分类编码。
from keras.utils.np.utils import to_categorical
train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)
现在我们准备开始训练网络,在Keras中这一步是通过调用网络的fit方法来完成的——我们在训练数据上拟合模型。
print(network.fit(train_images, train_labels, epochs=5, batch_size=128))
Epoch 1/5
469/469 [==============================] - 3s 4ms/step - loss: 0.2583 - accuracy: 0.9255
Epoch 2/5
469/469 [==============================] - 2s 4ms/step - loss: 0.1044 - accuracy: 0.9688
Epoch 3/5
469/469 [==============================] - 2s 4ms/step - loss: 0.0691 - accuracy: 0.9796
Epoch 4/5
469/469 [==============================] - 2s 4ms/step - loss: 0.0500 - accuracy: 0.9848
Epoch 5/5
469/469 [==============================] - 2s 4ms/step - loss: 0.0379 - accuracy: 0.9884
训练过程中显示了两个数字:一个是网络在训练数据上的损失(loss),另一个是网络在训练数据上的精度(acc)。我们很快就在训练数据上达到了98.8%的精度,现在我们来检查一下模型在测试集上的性能。
test_loss, test_acc = network.evaluate(test_images, test_labels)
print('test_acc:', test_acc)
test_acc: 0.9794999957084656
测试集精度为97.9%,比训练集精度低不少。训练精度和测试精度之间的这种差距是过拟合造成的,过拟合是指机器学习模型在新数据上的性能往往比在训练数据上要差。
2.2 神经网络的数据表示
前面例子使用的数据存储在多维Numpy数组中,也叫张量。一般来说,当前所有机器学习系统都使用张量作为基本数据结构。张量这一概念的核心在于,它是一个数据容器,它包含的数据几乎总是数值数据,因此它是数字的容器。矩阵是二维张量,张量是矩阵向任意维度的推广,张量的维度通常叫作轴。
2.2.1 标量(0D张量)
仅包含一个数字的张量叫作标量(scalar,也叫标量张量、零维张量,0D张量)。在Numpy中,一个float32或float64的数字就是一个标量张量(或标量数组)。标量张量有0个轴(ndim == 0),张量轴的个数也叫作阶。
import numpy as np
x = np.array(12)
print(x)
print(x.ndim)
12
0
2.2.2 向量(1D张量)
数字组成的数组叫作向量或一维张量(1D张量)。一维张量只有一个轴。
x = np.array([12, 3, 6, 14, 7])
print(x)
print(x.ndim)
[12 3 6 14 7]
1
2.2.3 矩阵(2D张量)
向量组成的数组叫作矩阵或二维张量(2D张量)。矩阵有两个轴(通常叫作行和列),你可以将矩阵直观地理解为数字组成的矩形网格。
x = np.array([[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]])
print(x)
print(x.ndim)
[[ 5 78 2 34 0]
[ 6 79 3 35 1]
[ 7 80 4 36 2]]
2
第一个轴上的元素叫作行,第二个轴上的元素叫作列。
2.2.4 3D张量与更高维张量
将多个矩阵组合成一个新的数组,可以得到一个3D张量,你可以将其直观地理解为数字组成的立方体。
x = np.array([[[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]],
[[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]],
[[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]]])
print(x)
print(x.ndim)
[[[ 5 78 2 34 0]
[ 6 79 3 35 1]
[ 7 80 4 36 2]]
[[ 5 78 2 34 0]
[ 6 79 3 35 1]
[ 7 80 4 36 2]]
[[ 5 78 2 34 0]
[ 6 79 3 35 1]
[ 7 80 4 36 2]]]
3
将多个3D张量组合成一个数组,可以创建一个4D张量,以此类推。深度学习处理的一般是0D到4D的张量,但处理视频数据时可能会遇到5D张量。
2.2.5 关键属性
张量是由以下三个关键属性来定义的。
1.轴的个数(阶)。3D张量有3个轴,矩阵有2个轴。
2.形状。这是一个整数元组,表示张量沿每个轴的维度大小(元素个数)。
3.数据类型。这是张量中所包含数据的类型。例如,张量的类型可以是float32、unit8、float64等。在极少数情况下,你可能会遇到字符张量。Numpy(以及大多数其他库)中不存在字符串张量,因为张量存储在预先分配的连续内存段中,而字符串的长度是可变的,无法用这种方式存储。
from keras.datasets import mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
print(train_images.ndim)
print(train_images.shape)
print(train_images.dtype)
3
(60000, 28, 28)
uint8
所以,这里train_images是一个由8位整数组成的3D张量,更确切地说,它是60000个矩阵组成的数组,每个矩阵由28x28个整数组成,每个这样的矩阵都是一张灰度图像,元素取值范围为0~255。
import matplotlib.pyplot as plt
digit = train_image[3]
plt.imshow(digit, cmap=plt.cm.binary)
plt.show()
上图显示的是数据集中的第4个样本。
2.2.6 在Numpy中操作张量
在前面的例子中,我们使用语法train_images[i]来选择沿着第一个轴的特定数字,选择张量的特定元素叫作张量切片。
my_slice = train_images[9:99]
print(my_slice.shape)
(90, 28, 28)
上面这个例子选择第10~100个数字(不包括第100个),并将其放在形状为(90, 28, 28)的数组中。
它等同于下面这个更复杂的写法,给出了切片沿着每个张量轴的起始索引和结束索引。:等同于选择整个轴。
my_slice = train_images[9:99, :, :]
print(my_slice.shape)
my_slice = train_images[9:99, 0:28, 0:28]
print(my_slice.shape)
2.2.7 数据批量的概念
通常来说,深度学习中所有数据张量的第一个轴(0轴,因为索引从0开始)都是样本轴(有时也叫样本维度)。
此外,深度学习模型不会同时处理整个数据集,而是将数据拆分成小批量。
batch = train_images[:128]
batch = train_images[128:256]
batch = train_images[128 * n:128 * (n + 1)]
对于这种批量张量,第一个轴(0轴)叫做批量轴或批量维度。
2.2.8 现实世界中的数据张量
1.向量数据:2D张量;
2.时间序列数据或序列数据:3D张量;
3.图像:4D张量;
4.视频:5D张量。
2.2.9 向量数据
这是最常见的数据。对于这种数据集,每个数据点都被编码为一个向量,因此一个数据批量就被编码为2D张量(即向量组成的数组),其中第一个轴是样本轴,第二个轴是特征轴。
2.2.10 时间序列数据或序列数据
当时间(或序列顺序)对于数据很重要时,应该将数据存储在带有时间轴地3D张量中。每个样本可以被编码为一个向量序列(即2D张量),因此一个数据批量就被编码为一个3D张量。
2.2.11 图像数据
图像通常具有三个维度:高度、宽度和颜色深度。虽然灰度图像只有一个颜色通道,因此可以保存在2D张量中,但按照惯例,图像张量始终都是3D张量,灰度图像的彩色通道只有一维。
图像张量的形状有两种约定:通道在后的约定和通道在前的约定。
2.2.12 视频数据
视频数据是现实生活中需要用到5D张量的少数数据类型之一。视频可以看作一系列帧,每一帧都是一张彩色图像。由于每一帧都可以保存在一个3D张量中,因此一系列帧可以保存在一个4D张量中,而不同视频组成的批量帧可以保存在一个5D张量中。
2.3 神经网络的“齿轮”:张量运算
所有计算机程序最终都可以简化为二进制输入上的一些二进制运算,与此类似,深度神经网络学到的所有变换也都可以简化为数值数据张量上的一些张量运算。
在最开始的例子中,我们通过叠加Dense层来构建网络。
2.3.1 逐元素运算
relu运算和加法都是逐元素的运算,即该运算独立地应用于张量中的每个元素,也就是说,这些运算非常适合大规模并行实现(向量化实现)
def naive_relu(x):
assert len(x.shape) == 2
x = x.copy()
for i in range(x.shape[0]):
for j in range(x.shape[1]):
x[i, j] = max(x[i, j], 0)
return x
def naive_add(x, y):
assert len(x.shape) == 2
assert x.shape == y.shape
x = x.copy()
for i in range(x.shape[0]):
for j in range(x.shape[1]):
x[i, j] += y[i, j]
return x
2.3.2 广播
如果没有歧义的话,较小的张量会被广播,以匹配较大张量的形状,广播包含以下两步:
1.向较小的张量添加轴(叫做广播轴),使其ndim与较大的张量相同;
2.将较小的张量沿着新轴重复,使其形状与较大的张量相同。
def naive_add_matrix_and_vector(x, y):
assert len(x.shape) == 2
assert len(y.shape) == 1
assert x.shape[1] == y.shape[0]
x = x.copy()
for i in range(x.shape[0]):
for i in range(x.shape[1]):
x[i, j] += y[j]
return x
下面这个例子利用广播将逐元素的maximum运算应用于两个形状不同的张量。
import numpy as np
x = np.random.random((64, 3, 32, 10))
y = np.random.random((32, 10))
z = np.maximum(x, y)
2.3.3 张量点积
点积运算,也叫张量积(不要与逐元素的乘积弄混),是最常见也最有用的张量运算。与逐元素的运算不同,它将输入张量的元素合并在一起。
我们首先看一下两个向量x和y的点积,其计算过程如下:
def naive_vector_dot(x, y):
assert len(x.shape) == 1
assert len(y.shape) == 1
assert x.shape[0] == y.shape[0]
z = 0
for i in range(x.shape[0]):
z += x[i] * y[i]
return z
两个向量之间的点积是一个标量,而且只有元素个数相同的向量之间才能做点积。
一个矩阵x和一个向量y做点积,返回值是一个向量,其中每个元素是y和x的每一行之间的点积。
import numpy as np
def naive_matrix_vector_dot(x, y):
assert len(x.shape) == 2
assert len(y.shape) == 1
assert x.shape[1] == y.shape[0]
z = np.zeros(x.shape[0])
for i in range(x.shape[0]):
for j in range(x.shape[1]):
z[i] += x[i, j] * y[j]
return z
def naive_matrix_vector_dot(x, y):
z = np.zeros(x.shape[0])
for i in range(x.shape[0]):
z[i] = naive_vector_dot(x[i, :], y)
return z
如果两个张量中有一个的ndim大于1,那么dot运算就不再是对称的,也就是说,dot(x, y)不等于dot(y, x)。
点积可以推广到具有任意个轴的张量,最常见的应用可能就是两个矩阵之间的点积,对于两个矩阵x和y,当且仅当x.shape[1] == y.shape[0]时,你才可以对它们做点积(dot(x, y))。得到的结果是一个形状为(x.shape[0], y.shape[1])的矩阵,其元素为x的行与y的列之间的点积。
def naive_matrix_dot(x, y):
assert len(x.shape) == 2
assert len(y.shape) == 2
assert x.shape[1] == y.shape[0]
z = np.zeros((x.shape[0], y.shape[1]))
for i in range(x.shape[0]):
for j in range(y.shape[1]):
row_x = x[i, :]
column_y = y[:, j]
z[i, j] = naive_vector_dot(row_x, column_y)
return z
如图所示,x、y、z都用矩形表示(元素按矩形排列)。x的行和y的列必须大小相同,因此x的宽度一定等于y的高度。
2.3.4 张量变形
第三个重要的张量运算是张量变形,在将图像数据输入神经网络之前,我们在预处理时用到了这个运算。
张量变形是指改变张量的行和列,以得到想要的形状。变形后的张量的元素总个数与初始张量相同。
import numpy as np
x = np.array([[0., 1.],
[2., 3.],
[4., 5.]])
print(x)
x = x.reshape((6, 1))
print(x)
x = x.reshape((2, 3))
print(x)
[[0. 1.]
[2. 3.]
[4. 5.]]
[[0.]
[1.]
[2.]
[3.]
[4.]
[5.]]
[[0. 1. 2.]
[3. 4. 5.]]
经常遇到的一种特殊的张量变形是转置,对矩阵做转置是指将行和列互换,使x[i, :]变为x[:, i]。
x = np.zeros((300, 20))
x = np.transpose(x)
print(x.shape)
(20, 300)
2.3.5 张量运算的几何解释
对于张量运算所操作的张量,其元素可以被解释为某种几何空间内点的坐标,因此所有的张量运算都有几何解释。
通常来说,仿射变换、旋转、缩放等基本的几何操作都可以表示为张量运算。
2.3.6 深度学习的几何解释
神经网络完全由一系列张量运算组成,而这些张量运算都只是输入数据的几何变换。因此,你可以将神经网络解释为高维空间中非常复杂的几何变换。
神经网络(或者任何机器学习模型)要做的就是找到可以让纸球恢复平整的变换,从而能够再次让两个类别明确可分。通过深度学习,这一过程可以用三维空间中一系列简单的变换来实现。
让纸球恢复平整就是机器学习的内容:为复杂的、高度折叠的数据流形找到简洁的表示。深度学习将复杂的几何变换逐步分解为一长串基本的几何变换,这与人类展开纸球所采取的策略大致相同。深度网络的每一层都通过变换使数据解开一点点——许多层堆叠在一起,可以实现非常复杂的解开过程。
2.4 神经网络的“引擎” :基于梯度的优化
output = relu(dot(W, input) + b)
在这个表达式中,W和b都是张量,均为该层的属性。它们被称为该层的权重或可训练参数,分别对应kernel和bias属性,这些权重包含网络从观察训练数据中学到的信息。
一开始,这些权重矩阵取较小的随机值,这一步叫做随机初始化,下一步则是根据反馈信号逐渐调节这些权重。这个逐渐调节的过程叫做训练,也就是机器学习中的学习。
上述过程发生在一个训练循环内,具体过程如下,必要时一直重复这些步骤:
1.抽取训练样本x和对应目标y组成的数据批量;
2.在x上运行网络[这一步叫作前向传播],得到预测值y_pred;
3.计算网络在这批数据上的损失,用于衡量y_pred和y之间的距离;
4.更新网络的所有权重,使网络在这批数据上的损失略微下降。
最终得到的网络在训练数据上的损失非常小,即预测值y_pred和预期目标y之间的距离非常小。网络“学会”将输入映射到正确的目标。
难点在于第四步:更新网络的权重。考虑网络中某个权重系数,你怎么知道这个系数应该增大还是减小,以及变化多少?
一种简单的解决方案是,保持网络中其他权重不变,只考虑某个标量系数,让其尝试不同的取值。
一种更好的方法是利用网络中所有运算都是可微的这一事实,计算损失相对于网络系数的梯度,然后向梯度的反方向改变系数,从而使损失降低。
2.4.1 什么是导数
导数完全描述了改变x后f(x)会如何变化,如果你希望减小f(x)的值,只需将x沿着导数的反方向移动一小步。
2.4.2 张量运算的导数:梯度
梯度是张量运算的导数,它是导数这一概念的多元函数导数的推广,多元函数是以张量作为输入的函数。
前面已经看到,单变量函数f(x)的导数可以看作函数f曲线的斜率,同样,gradient(f)(W0)也可以看作表示f(W)在W0附近曲率的张量。
对于一个函数f(x),你可以通过将x向导数的反方向移动一小步来减小f(x)的值。同样,对于张量的函数f(W),你也可以通过将W向梯度的反方向移动来减小f(W)。也就是说,沿着曲率的反方向移动,直观上来看在曲线上的位置会更低。
2.4.3 随机梯度下降
给定一个可微函数,理论上可以用解析法找到它的最小值:函数的最小值是导数为0的点,因此你只需找到所有导数为0的点,然后计算函数在其中哪个点具有最小值。
将这一方法应用于神经网络,就是用解析法求出最小损失函数对应的所有权重值。
沿着梯度的反方向更新权重,损失每次都会变小一点。
1.抽取训练样本x和对应目标y组成的数据批量;
2.在x上运行网络,得到预测值y_pred;
3.计算网络在这批数据上的损失;
4.将参数沿着梯度的反方向移动一点,比如W -= step * gradient,从而使这批数据上的损失减小一点。
这种方法叫做小批量随机梯度下降,又称为小批量SDG。术语随机是指每批数据都是随机抽取的。
小批量SGD算法的一个变体是每次迭代时只抽取一个样本和目标,而不是抽取一批数据,这叫作真SGD(有别于小批量SGD) 。还有一种极端,每一次迭代都在所有数据上运行,这叫作批量SGD。这样做的话,每次更新都更加准确,但计算代价也高得多,这两个极端之间的有效折中则是选择合理的批量大小。
神经网络的每一个权重参数都是空间中的一个自由维度,网络中可能包含数万个甚至上百万个参数维度。你不可能将神经网络的实际训练过程可视化,因为你无法用人类可以理解的方式来可视化1000000维空间。因此最好记住,在这些低维表示中形成的直觉在实践中不一定总是准确的,这在历史上一直是深度学习研究的问题来源。
此外,SGD还有多种变体,其区别在于计算下一次权重更新时还要考虑上一次权重更新,而不仅仅考虑当前梯度值,这些变体被称为优化方法或优化器。
如图,在某个参数值附近,有一个局部极小点:在这个点附近,向左移动和向右移动都会导致损失值增大,如果使用小学习率的SGD进行优化,那么优化过程可能会陷入局部极小点,导致无法找到全局最小点,使用动量方法可以避免这样的问题。
past_velocity = 0
momentum = 0.1
while loss > 0.01:
w, loss, gradient = get_current_parameters()
velocity = past_velocity * momentum - learning_rate * gradient
w = w + momentum * veloctiy - learning_rate * gradient
past_velocity = velocity
update_parameter(w)
2.4.4 链式求导:反向传播算法
将链式法则应用于神经网络梯度值的计算,得到的算法叫做反向传播,有时也叫反式微分。反向传播从最终损失值开始,从最顶层反向作用至最底层,利用链式法则计算每个参数对损失值的贡献大小。
现在以及未来数年,人们将使用能够进行符号微分的现代框架来实现神经网络,比如TensorFlow,也就是说,给定一个运算链,并且已知每个运算的导数,这些框架就可以利用链式法则来计算这个运算链的梯度函数,将网络参数值映射为梯度值。对于这样的函数,反向传播就简化为调用这个梯度函数。由于符号微分的出现,你无需手动实现反向传播算法。
2.5 回顾第一个例子
from keras.datasets import mnist
from keras import models
from keras import layers
import matplotlib.pyplot as plt
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype('float32') / 255
test_images = test_images.reshape((10000, 28 * 28))
test_images = test_images.astype('float32') / 255
network = models.Sequential()
network.add(layers.Dense(512, activation='relu', input_shape=(28 * 28)))
network.add(layers.Dense(10, activation='softmax'))
network.compile(optimizer='rmsprop',
loss = 'categorical_crossentropy',
metrics=['accuracy'])
network.fit(train_images, train_labels, epochs=5, batch_size=128)
网络开始在训练数据上进行迭代,共迭代5(在所有训练数据上迭代一次叫作一个轮次)。在每次迭代过程中,网络会计算批量损失相对于权重的梯度,并相应地更新权重。5轮之后,网络进行了2345次梯度更新,网络损失值将变得足够小,使得网络能够以很高的精度对手写数字进行分类。
本章小结
1.学习是指找到一组模型参数,使得在给定的训练数据样本和对应目标值上的损失函数最小化。
2.学习的过程:随机选取包含数据样本及其目标值的批量,并计算批量损失对于网络参数的梯度,随后将网络参数沿着梯度的反方向稍稍移动(移动距离由学习率指定) 。
3.整个学习过程之所以能够实现,是因为神经网络是一系列可微分的张量运算,因此可以利用求导的链式法则来得到梯度函数,这个函数将当前参数和当前数据批量映射为一个梯度值。
4.将数据输入网络之前,需要先定义损失和优化器。
5.损失是在训练过程中需要最小化的量,因此,它应该能够衡量当前任务是否已经成功解决。
6.优化器是使用损失梯度更新参数的具体方式,比如RMSProp优化器、带动量的随机梯度下降(SGD)等。