第2章 神经网络的数学基础
2.1 初识神经网络
- 需要解决的问题:将手写数字的灰度图像(28像素x28像素)划分到10个类别中(0~9)。
- MNIST数据集包含60000张训练图像和10000张测试图像,由美国国家标准与技术研究院(National Institute of Standards and Technology)在20世纪80年代收集得到。
- MNIST数据集预先加载在Keras库中,其中包括4个Numpy数组。
from keras.datasets import mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
- train_images和train_labels组成了训练集(training set),模型将从这些数据中进行学习。然后在测试集(test set,即test_iamges和test_labels)上对模型进行测试。
- 图像被编码为Numpy数组,而标签是数字数组,取值范围为0~9。图像和标签一一对应。
- 接下来的工作流程如下:首先,将训练数据(train_images和train_labels)输入神经网络;其次,网络学习将图像和标签关联在一起;最后,网络对test_images生成预测,而我们将验证这些预测与test_labels中的标签是否匹配。
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'))
- 神经网络的核心组件是层(layer),它是一种数据处理模块,你可以将它看成数据过滤器。进去一些数据,出来的数据变得更加有用。具体来说,层从输入数据中提取表示–我们期望这种表示有助于解决手头的问题。大多数深度学习都是将简单的层链接起来,从而实现渐进式的数据蒸馏(data distillation)。深度学习模型就像是数据处理的筛子,包含一系列越来越精细的数据过滤器(即层)。
- 本例中的网络包含两个Dense层,它们是密集连接(也叫全连接)的神经层。第二层是一个10路softmax层,它将返回一个由10个概率值(总和为1)组成的数组。每个概率值表示当前数字图像属于10个数字类别中某一个的概率。
- 要训练网络,我们还需要选择编译(compile)步骤的三个参数:1、损失函数(loss function),网络如何衡量在训练数据上的性能,即网络如何朝着正确的方向前进。2、优化器(optimizer),基于训练数据和损失函数来更新网络的机制。3、在训练和测试过程中需要监控的指标(metric),本例只关心精度,即正确分类的图像所占的比例。
network.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])
- 在开始训练之前,我们需要对数据进行预处理,将其变化为网络要求的形状,并将取值缩放到[0, 1]区间。比如,之前训练图像保存在一个unit8类型的数组中,其形状为(60000,28,28),取值区间为[0, 255]。我们需要将其变换为一个float32数组,其形状为(60000, 28*28),取值范围为0~1。
train_images = train_images.reshape((60000, 28*28))
train_iamges = train_images.astype('float32') / 255
test_images = test_images.reshape((10000, 28*28))
test_images = test_images.astype('float32') / 255
from keras.utils import to_categorical
train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)
network.fit(train_images, train_labels, epochs=5, batch_size=128)
test_loss, test_acc = network.evaluate(test_images, test_labels)
print('test_acc', test_acc)
- 训练精度和测试精度之间的差距是过拟合(overfit)造成的。过拟合是指机器学习模型在新数据上的性能往往比在训练数据上要差。
- 张量(输入网络的数据存储对象)、张量运算(层的组成要素)、梯度下降(让网络从训练样本中进行学习)。
2.2 神经网络的数据表示
- 前面例子使用的数据存储在多维Numpy数组中,也叫张量(tensor)。一般来说,当前所有机器学习系统都使用张量作为基本数据结构。张量这一概念的核心在于,它是一个数据容器。它包含的数据几乎总是数值数据,因此它是数字的容器。矩阵是二维张量,张量是矩阵向任意维度的推广。张量的维度(dimension)通常叫作轴(axis)。
2.2.1 标量(0D张量)
- 仅包含一个数字的张量叫作标量(scala,也叫标量张量、零维张量、0D张量)。在Numpy中,一个float32或float64的数字就是一个标量张量(或标量数组)。可以使用ndim属性来查看一个Numpy张量轴的个数。标量张量有0个轴(ndim == 0)。张量轴的个数也叫作阶(rank)。下面是一个numpy标量。
import numpy as np
x = np.array(12)
print(x)
print(x.ndim)
2.2.2 向量(1D张量)
- 数字组成的数组叫作向量(vector)或一维张量(1D张量)。一维张量只有一个轴。下面是一个Numpy向量。下面是一个Numpy向量。
x = np.array([1, 2, 3, 4, 5])
print(x)
print(x.ndim)
2.2.3 矩阵(2D张量)
- 向量组成的数组叫作矩阵(matrix)或二维张量(2D张量)。矩阵有2个轴(通常叫作行和列)。你可以将矩阵直观地理解为数字组成的矩形网络。下面是一个Numpy矩阵。
x = np.array([[1,2,3],[2,3,4],[4,5,6]])
print(x)
print(x.ndim)
- 第一个轴上的元素叫作行(row),第二个轴上的元素叫作列(column)。
2.2.4 3D张量与更高维张量
- 将多个矩阵组合成一个新的数组,可以得到一个3D张量,可以直观地理解为数字组成的立方体。下面是一个Numpy的3D张量。
x = np.array([[[1, 2, 3], [2, 3, 4], [3, 4, 5]]])
print(x)
print(x.ndim)
- 将多个3D张量组合成一个数组,可以创建一个4D张量。深度学习处理的一般是0D到4D的张量,但处理视频数据时可能会遇到5D张量。
2.2.5 关键属性
- 轴的个数(阶)。例如,3D张量有3个轴,矩阵有2个轴。这在Numpy等Python库中也叫张量的ndim。
- 形状。这是一个整数元组,表示张量沿每个轴的维度大小(元素个数)。
- 数据类型(在Python库中通常叫作dtype)。这是张量中所包含数据的类型,例如,张量的类型可以是float32、unit8、float64等。在极少数情况下,你可能会遇到字符(char)张量。注意,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)
- train_images是一个由8位整数组成的3D张量。更确切地说,它是60000个矩阵组成的数组,每个矩阵由28x28个整数组成。每个这样的矩阵都是一张灰度图像,元素的取值范围0~255。
digit = train_images[4]
import matplotlib.pyplot as plt
plt.imshow(digit, cmap=plt.cm.binary)
plt.show()
2.2.6 在Numpy中操作张量
- 在前面的例子中,我们使用语法train_images[i]来选择沿着第一个轴的特定数字。选择张量的特定元素叫作张量切片(tensor slicing)。
my_slice = train_images[10:100]
print(my_slice.shape)
my_slice = train_images[10:100, :, :]
print(my_slice.shape)
my_slice = train_images[10:100, 0:28, 0:28]
print(my_slice.shape)
2.2.7 数据批量的概念
- 通常来说,深度学习中所有数据张量的第一个轴(0轴,因为索引从0开始)都是样本轴(sample axis,有时也叫样本维度)。
- 此外,深度学习模型不会同时处理整个数据集,而是将数据拆分成小批量。具体来看,下面是MNIST数据集的一个批量,批量大小为128。
batch = train_images[:128]
batch = train_images[128:256]
batch = train_images[128*n:128*(n+1)]
- 对于这种批量张量,第一个轴(0轴)叫作批量轴(bacth axis)或批量维度(batch dimension)。
2.28 现实世界中的数据张量
- 向量数据:2D张量,形状为(samples,features)。
- 时间序列数据或序列数据:3D张量,形状为(samples, timesteps, features)。
- 图像:4D张量,形状为(samples, height, width, channels)或(samples, channels, height, width)。
- 视频:5D张量,形状为(samples, frames, height, width, channels)或(samples, frames, channels, height, width)。
2.3 神经网络的“齿轮”:张量运算
- 所有计算机程序最终都可以简化为二进制输入上的一些二进制运算(AND、OR、NOR等),与此类似,深度神经网络学到的所有变换也都可以简化为数值数据张量上的一些张量运算(tensor operation),例如加上张量、乘以张量等。
- 在最开始的例子中,我们通过叠加Dense层来构建网络。Keras层的实例如下所示。
keras.layers.Dense(512, activation='relu')
- 这个层可以理解为一个函数,输入一个2D张量,返回另一个2D张量,即输入张量的新表示。具体而言,这个函数如下所示(其中W是一个2D张量,b是一个向量,二者都是该层的属性)。
output = relu(dot(W, input) + b)
- 上式有三个张量运算:输入张量和张量W之间的点积运算(dot)、得到的2D张量与向量b之间的加法运算(+)、最后的relu运算。relu(x)是max(x, 0)。
2.3.1 逐元素运算
- relu运算和加法都是逐元素(element-wise)的运算,即该运算独立地应用于张量中的每个元素,也就是说,这些运算非常适合大规模并行实现(向量化实现)。
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
- 在实践中处理Numpy数组时,这些运算都是优化好的Numpy内置函数,这些函数将大量运算交给安装好的基础线性代数子程序(BLAS,basic linear algebra subprograms)。BLAS是低层次的、高度并行的、高效的张量操作程序,通常用Fortran或C语言来实现。
- 因此,在Numpy中可以直接进行下列逐元素运算,速度非常快。
import numpy as np
z = x + y
z = np.maximum(z, 0.)
2.3.2 广播
- 在没有歧义的条件下,两个形状不同的张量进行运算时,较小的张量会被广播(broadcast),以匹配较大张量的形状。广播包含以下两步:(1)向较小的张量添加轴(叫作广播轴),使其ndim与较大的张量相同。(2)将较小的张量沿着新轴重复,使其形状与较大的张量相同。
- 在实际的实现过程中并不会创建新的2D张量,因为那样做非常低效。重复的操作完全是虚拟的,它只出现在算法中,而没有发生在内存中。
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
- 如果一个张量的形状是(a, b, …, n, n+1, …, m),另一个张量的形状是(n, n+1, …, m),那么你通常可以利用广播对它们做两个张量之间逐元素运算。广播操作会自动应用于从a到n-1的轴。
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 张量点积
- 点积运算,也叫张量积(tensor product,不要与逐元素的乘积弄混),是最常见也最有用的张量运算。与逐元素的运算不同,它将输入张量的元素合并在一起。
- 在Numpy、keras、Theano和Tensorflow中,都是用*实现逐元素乘积。Tensorflow中的点积使用了不同的语法,但在Numpy和Keras中,都是用标准的dot运算符来实现点积。
import numpy as np
z = np.dot(x, y)
z = 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
- 两个向量之间的点积是一个标量,而且只有元素个数相同的向量之间才能做点积。
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_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(x.shape[1]):
row_x = x[i, :]
column_y = y[:, j]
z[i, j] =naive_vector_dot(row_x, column_y)
return z
2.3.4 张量变形
- 张量变形是指改变张量的行和列,以得到想要的形状。变形后的张量的元素总个数与初始张量相同。
import numpy as np
x = np.array([[0., 1.], [2., 3.], [4., 5.]])
print(x.shape)
x = x.reshape((6, 1))
print(x)
x = x.reshape((2, 3))
print(x)
- 转置(transposition):行和列互换,使x[i, ;]变为x[:, i]。
x = np.zeros((300, 20))
x = np.transpose(x)
print(x.shape)
2.3.6 深度学习的几何解释
- 神经网络完全由一系列张量运算组成,而这些张量运算都只是输入数据的几何变换。因此,可以将神经网络解释为高维空间中非常复杂的几何变换,这种变换可以通过许多简单的步骤来实现。
- 想象有两张彩纸:一张红色,一张蓝色。将其中一张纸放在另一张纸上。现在将两张纸一起揉成小球。这个皱巴巴的纸球就是你的输入数据,每张纸对应于分类问题中的一个类别。神经网络(或者任何机器学习模型)要做的就是找到可以让纸球恢复平整的变换,从而能够再次让两个类别明确可分。通过深度学习,这一过程可以用三维空间中一系列简答的变换来实现,比如你用手指对纸球做的变换,每次一个动作。
- 让纸球恢复平整就是机器学习的内容:为复杂的、高度折叠的数据流形找到简洁的表示。深度学习将复杂的几何变换逐步分解为一长串基本的几何变换,这与人类展开纸球所采取的策略大致相同。深度网络的每一层都通过变换使数据解开一点点–许多层堆叠在一起,可以实现非常复杂的解开过程。
2.4 神经网络的“引擎”:基于梯度的优化
- 每个全连接神经层都用下述方法对输入数据进行变换:output = relu(dot(W, input) + b)。
- 在这个表达式中,W和b都是张量,均为该层的属性。它们被称为该层的权重(weight)或可训练参数(trainable parameter),分别对应kernel和bias数学。这些权重包含网络从观察训练数据中学到的信息。
- 一开始,这些权重矩阵取较小的随机值,这一步叫作随机初始化(random initialization)。当然,W和b都是随机的,relu(dot(W, input) + b)肯定不会得到任何有用的表示。虽然得到的表示是没有意义的,但这是一个起点。下一步则是根据反馈信号逐渐调节这些权重。这个逐渐调节的过程叫作训练,也就是机器学习中的学习。
- 上述过程发生在一个训练循环(training loop)内,必要时一直重复下述步骤:(1)抽取训练样本x和对应目标y组成的数据批量。(2)在x上运行网络(这一步叫作前向传播(forward pass)),得到预测值y_pred。(3)计算网络在这批数据上的损失,用于衡量y_pred和y之间的距离。(4)更新网络的所有权证,使网络在这批数据上的损失略微下降。最终得到的网络在训练数据上的损失非常小,即预测值y_pred和预期目标y之间的距离非常小。网络“学会”将输入映射到正确的目标。利用网络中所有运算都是可微(differentiable)这一事实,计算损失相对于网络系数的梯度(gradient),然后向梯度的反方向改变系数,从而使损失降低。
2.4.1 什么是导数
- 假设有一个连续的光滑函数 f ( x ) = y f(x) = y f(x)=y,将实数 x x x映射为另一个实数 y y y。由于函数是连续的, x x x的微小变化只能导致 y y y的微小变化。假设 x x x增大了一个很小的因子 e p s i l o n x epsilon_x epsilonx,这导致 y y y也发送了很小的变换,即 e p s i l o n y epsilon_y epsilony: f ( x + e p s i l o n x ) = y + e p s i l o n y f(x + epsilon_x) = y + epsilon_y f(x+epsilonx)=y+epsilony。
- 此外,由于函数式光滑的(即函数曲线没有突变的角度),在某个点 p p p附近,如果 e p s i l o n x epsilon_x epsilonx足够小,就可以将 f f f近似为斜率为 a a a的线性函数,这样 e p s i l o n y epsilon_y epsilony就变成了 a ∗ e p s i l o n x a * epsilon_x a∗epsilonx: f ( x + e p s i l o n x ) = y + a ∗ e p s i l o n x f(x + epsilon_x) = y + a * epsilon_x f(x+epsilonx)=y+a∗epsilonx。显然,只有在x足够接近 p p p时,这个线性近似才有效。斜率 a a a被称为 f f f在 p p p点的导数(derivative)。如果 a a a是负的,说明 x x x在 p p p点附近的微小变化将导致 f ( x ) f(x) f(x)减小;如果 a a a是正的,那么x的微小变化将导致 f ( x ) f(x) f(x)增大。此外 a a a的绝对值(导数的大小)表示增大或减小的快慢。
2.4.2 张量运算的导数:梯度
- 梯度(gradient)是张量运算的导数。它是导数这一概念向多元函数导数的推广。多元函数是以张量作为输入的函数。
- g r a d i e n t ( f ) ( W 0 ) gradient(f)(W0) gradient(f)(W0)也可以看作表示 f ( W ) f(W) f(W)在 w 0 w0 w0附近曲率(curvature)的张量。
2.4.3 随机梯度下降
- 给定一个可微函数,理论上可以用解析法找到它的最小值:函数的最小值是导数为0的点,因此你只需找到所有导数为0的点,然后计算函数在其中哪个点具有最小值。
- 将这一方法应用于神经网络,就是用解析法求出最小损失函数对应的所有权重值。可以通过对方程 g r a d i e n t ( f ) ( W ) = 0 gradient(f)(W) = 0 gradient(f)(W)=0求解W来实现这一方法。这是包含 N N N个变量的多项式方程,其中 N N N是网络中系数的个数。 N = 2 N=2 N=2或 N = 3 N=3 N=3时可以对这样的方程求解,但对于实际的神经网络是无法求解的,因为参数的个数不会少于几千个,而且经常有上千万个。
- (1)抽取训练样本 x x x和对应目标 y y y组成的数据批量。(2)在 x x x上运行网络,得到预测值 y p r e d y_pred ypred。(3)计算网络在这批数据上的损失,用于平衡 y p r e d y_pred ypred和 y y y之间的距离。(4)计算损失相对于网络参数的梯度(一次反向传播(backward pass))。(5)将参数沿着梯度的反方向移动一点,比如 W − = s t e p ∗ g r a d i e n t W - = step * gradient W−=step∗gradient,从而使这批数据上的损失减小一点。
- 动量方法的实现过程是每一步都移动小球,不仅要考虑当前的斜率值,还要考虑当前的速度(来自于之前的加速度)。这在实践中是指,更新参数w不仅要考虑当前的梯度值,还要考虑上一次的参数更新。
- 将链式法则应用于神经网络梯度值的计算,得到的算法叫作反向传播(backpropagation,有时也叫反式微分,reverse-mode differentiation)。反向传播从最终损失值开始,从最顶层反向作用至最底层,利用链式法则计算每个参数对损失值的贡献大小。