此处以手写数字图像分类为例,使用 MNIST 数据集,包含 60 000 张训练图像和 10 000 张测试图像,图像大小为 28*28。
1、加载 Keras 中的 MNIST 数据集
from keras.datasets import mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
训练集 包括:6万张存储在变量train_images
中的图像样本(大小为(60000, 28, 28)
)和6万个存储在变量train_labels
中的样本标签(大小为(60000,)
)。
测试集 包括:1万张存储在变量test_images
中的图像样本(大小为(10000, 28, 28)
)和6万个存储在变量test_labels
中的样本标签(大小为(10000, )
)。
2、网络架构
神经网络的核心组件是 层(layer),它是一种数据处理模块,你可以将它看成数据过滤器。进去一些数据,出来的数据变得更加有用。具体来说,层从输入数据中提取 表示——我们期望这种表示有助于解决手头的问题。
(个人理解:通过改变数据的表达形式,使得混杂在一起的数据分离,易于区分。同时,通过层层变化,使得抽象的内容变得简单清晰,方便电脑理解识别。)
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'))
上述代码中的网络包含 2 个 Dense 层,它们是 密集连接(也叫 全连接)的神经层。第二层(也是最后一层)是一个 10 路 softmax 层,它将返回一个由 10 个概率值(总和为 1)组成的数组。每个概率值表示当前数字图像属于 10 个数字类别中某一个的概率。
3、编译步骤
network.compile(optimizer='rmsprop',
loss='categorical_crossentropy',
metrics=['accuracy'])
编译(compile) 包括三个重要参数:损失函数(loss function),优化器(optimizer) 和 指标(metric)。
此处选用多分类交叉熵损失函数和RMSprop优化器(可添加动量),以变量accuracy
中存储的精确度为指标。
4、准备图像数据
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
在开始训练之前,要对数据进行 预处理,将其变换为网络要求的形状,并缩放到所有值都在 [0, 1] 区间。
因为数据将输入到大小512的全连接层中,所以需要将二维图像变换为一维数列,同时对数据 标准化。原始数据为unit 8数组,为确保标准化后的数据精度,此处将数据存储在float 32数组中。
5、准备标签
from keras.utils import to_categorical
train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)
6、训练
最后调用网络中的fit()
方法,拟合(fit) 模型。
>>> network.fit(train_images, train_labels, epochs=5, batch_size=128)
Epoch 1/5
60000/60000 [=============================] - 9s - loss: 0.2524 - acc: 0.9273
Epoch 2/5
51328/60000 [=======================>.....] - ETA: 1s - loss: 0.1035 - acc: 0.9692
训练过程中显示了两个数字:一个是网络在训练数据上的 损失(loss),另一个是网络在训练数据上的 精度(acc)。本例中,训练集精度可以达到0.989(98.9%),预测集精度只有 97.8%,这是由于 过拟合(overfit) 造成的。
(就像做题,同样的考点,盯着几种题型多刷几遍,准确率就上去了,换一个题型考法,准确率就会低一些。)
前面例子使用的数据存储在多维Numpy
数组中,也叫 张量(tensor)。张量是矩阵向任意维度的推广[注意,张量的 维度(dimension) 通常叫作 轴(axis)]。
仅包含一个数字的张量叫作 标量(scalar,也叫标量张量、零维张量、0D 张量)。在 Numpy中,一个float32
或float64
的数字就是一个标量张量(或标量数组)。可以用ndim
属性 来查看一个 Numpy 张量的轴的个数。标量张量有 0 个轴(ndim == 0)。张量轴的个数也叫作 阶(rank)。
>>> import numpy as np
>>> x = np.array(12)
>>> x
array(12)
>>> x.ndim
0
数字组成的数组叫作 向量(vector) 或一维张量(1D 张量)。一维张量只有一个轴。
>>> x = np.array([12, 3, 6, 14, 7])
>>> x
array([12, 3, 6, 14, 7])
>>> x.ndim
1
向量组成的数组叫作 矩阵(matrix) 或 二维张量(2D 张量)。矩阵有 2 个轴(通常叫作行和列)。
>>> x = np.array([[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]])
>>> x.ndim
2
第一个轴(0 轴)上的元素叫作 行(row),第二个轴(1 轴)上的元素叫作 列(column)。
(0轴方向是由5指向7,1轴方向是由5指向0。行元素为 [5, 78, 2, 34, 0])。
3D 张量由多个2D 张量沿第三个轴(2 轴,深度)堆叠形成,可以想象成一个立方体。
>>> 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]]])
张量是由三个关键 属性 来定义的:轴的个数,形状 和 数据类型。
利用 Matplotlib 库(Python 标准科学套件的一部分)来显示这个 3D 张量中的第 4 个数字:
digit = train_images[4]
import matplotlib.pyplot as plt
plt.imshow(digit, cmap=plt.cm.binary)
plt.show()
在前面的例子中,使用语法train_images[i]
来选择沿着第一个轴的特定数字。选
择张量的特定元素叫作 张量切片(tensor slicing)。下面代码在所有图像的右下角选出 14 像素×14 像素的区域:
my_slice = train_images[:, 14:, 14:]
通常来说,深度学习中所有数据张量的第一个轴(0 轴,因为索引从 0 开始)都是 样本轴(samples axis,有时也叫 样本维度)。
此外,深度学习模型不会同时处理整个数据集,而是将数据拆分成小批量。之前的例子的批量大小batch_size=128
。对于这种批量张量,第一个轴(0 轴)叫作 批量轴(batch axis)或 批量维度(batch dimension)
常用的张量类型有:
1、向量数据:2D 张量,形状为 (samples, features)。
2、时间序列数据或序列数据:3D 张量,形状为 (samples, timesteps, features)。
3、图像:4D 张量,形状为 (samples, height, width, channels) 或 (samples, channels, height, width)。
4、视频:5D 张量,形状为 (samples, frames, height, width, channels) 或 (samples, frames, channels, height, width)。
Relu 运算 和 加法 都是逐元素(element-wise)的运算,即该运算独立地应用于张量中的每个元素,也就是说,这些运算非常适合大规模并行实现(向量化实现,这一术语来自于 1970—1990 年间向量处理器超级计算机架构)。
Relu 运算:
def naive_relu(x):
assert len(x.shape) == 2 #'断言' x是一个2D张量
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
当两个形状不同的张量相加时,如果没有歧义的话,较小的张量会被 广播(broadcast),以匹配较大张量的形状。具体步骤:
(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 j in range(x.shape[1]):
x[i, j] += y[j]
return x
张量点积运算和矩阵电机运算类似,用函数np.dot()
或者点积运算符.
表示。
利用方法reshape()
可以改变张量的形状。
train_images = train_images.reshape((60000, 28 * 28))
使用方法transpose()
对张量进行 转置变换,这个比较复杂,需要添加转置轴的相关参数。
对于张量运算所操作的张量,其元素可以被解释为某种几何空间内点的坐标。张量运算改变了张量在几何空间中的形状和位置。仿射变换、旋转、缩放等基本的几何操作都可以表示为张量运算。
神经网络完全由一系列张量运算组成,而这些张量运算都只是输入数据的几何变换。因此,你可以将神经网络解释为高维空间中非常复杂的几何变换,这种变换可以通过许多简单的步骤来实现。
对于三维的情况,下面这个思维图像是很有用的。想象有两张彩纸:一张红色,一张蓝色。将其中一张纸放在另一张上。现在将两张纸一起揉成小球。这个皱巴巴的纸球就是你的 输入数据,每张纸对应于分类问题中的一个 类别。神经网络(或者任何机器学习模型)要做的就是找到可以让纸球恢复平整的变换,从而能够再次让两个类别明确可分。 通过深度学习,这一过程可以用三维空间中一系列简单的变换来实现,比如你用手指对纸球做的变换,每次做一个动作。
在之前的代码中,第一层对输入做了如下操作
output = relu(dot(w, input) + b)
其中,w
和b
都是张量,均为该层的属性。它们被称为该层的 权重(weight)或 可训练参数(trainable parameter),分别对应 kernel 和 bias 属性(卷积核和偏置)。这些权重包含网络从观察训练数据中学到的信息。
一开始,这些权重矩阵取较小的随机值,这一步叫作 随机初始化(random initialization)下一步则是根据反馈信号逐渐调节这些权重。这个逐渐调节的过程叫作 训练,也就是机器学习中的学习。
上述过程发生在一个 训练循环(training loop)内,其具体过程如下:
(1) 抽取训练样本 x 和对应目标 y 组成的数据批量。
(2) 在 x 上运行网络[这一步叫作 前向传播(forward pass)],得到预测值y_pred。
(3) 计算网络在这批数据上的损失,用于衡量 y_pred 和 y 之间的距离。
(4) 更新网络的所有权重,使网络在这批数据上的损失略微下降。
梯度(gradient)是张量运算的导数。它是导数这一概念向多元函数导数的推广。多元函数是以张量作为输入的函数。
给定一个可微函数,理论上可以用解析法找到它的最小值,但对于实际的神经网络是无法求解的,因为参数的个数不会少于几千个,而且经常有上千万个。
之前描述的方法叫作 小批量随机梯度下降(mini-batch stochastic gradient descent,又称为 小批量 SGD)。具体是:
(1) 抽取训练样本 x 和对应目标 y 组成的数据批量。
(2) 在 x 上运行网络,得到预测值 y_pred。
(3) 计算网络在这批数据上的损失,用于衡量 y_pred 和 y 之间的距离。
(4) 计算损失相对于网络参数的梯度[一次反向传播(backward pass)]。
(5) 将参数沿着梯度的反方向移动一点,比如 w -= step * gradient
,从而使这批数据上的损失减小一点。
小批量 SGD 算法的一个变体是每次迭代时只抽取一个样本和目标,而不是抽取一批数据。这叫作 真 SGD(有别于小批量 SGD)。还有另一种极端,每一次迭代都在所有数据上运行,这叫作 批量 SGD。这样做的话,每次更新都更加准确,但计算代价也高得多。这两个极端之间的有效折中则是选择合理的批量大小。
此外,SGD 还有多种变体,其区别在于计算下一次权重更新时还要考虑上一次权重更新,而不是仅仅考虑当前梯度值,比如 带动量的 SGD、Adagrad、RMSProp 等变体。这些变体被称为 优化方法(optimization method)或 优化器(optimizer)。其中动量的概念尤其值得关注,它在许多变体中都有应用。动量 解决了 SGD 的两个问题:收敛速度 和 局部极小点。
使用 动量 方法可以避免这样的问题,这一方法的灵感来源于物理学。有一种有用的思维图像,就是将优化过程想象成一个小球从损失函数曲线上滚下来。如果小球的动量足够大,那么它不会卡在峡谷里,最终会到达全局最小点。动量方法的实现过程是每一步都移动小球,不仅要考虑当前的斜率值(当前的加速度),还要考虑当前的速度(来自于之前的加速度)。这在实践中的是指,更新参数 w 不仅要考虑 当前的梯度值,还要考虑 上一次的参数更新,其简单实现如下所示。
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 * velocity - learning_rate * gradient
past_velocity = velocity
update_parameter(w)
根据微积分的知识,可以推导(f(g(x)))' = f'(g(x)) * g'(x)
,这称为 链式法则(chain rule)。将链式法则应用于神经网络梯度值的计算,得到的算法叫作 反向传播(backpropagation,有时也叫 反式微分,reverse-mode differentiation)。反向传播从最终损失值开始,从最顶层反向作用至最底层,利用链式法则计算每个参数对损失值的贡献大小。
回顾之前的例子,我们会明白:
1、张量的形状。
2、权重张量 是层的属性,里面保存了网络所学到的 知识(knowledge)。
3、categorical_crossentropy
是损失函数,是用于学习权重张量的反馈
信号,在训练阶段应使它最小化。
4、减小损失是通过小批量随机梯度下降来实现的,具体在于选择什么样的优化器。
5、明白在调用fit
时发生了什么:网络开始在训练数据上进行 迭代(每个小 批量包含128 个样本),共迭代 5 次[在所有训练数据上迭代一次叫作一个 轮次(epoch)]。在每次迭代过程中,网络会计算批量损失相对于权重的梯度,并相应地 更新权重。5 轮之后,网络进行了2345 次梯度更新(每轮 469 次,60000/128=468.75),网络损失值将变得足够小,使得网络能够以很高的精度对手写数字进行分类。