第2章 神经网络的数学基础
张量(tensor)
一般来说,当前所有机器学习系统都使用张量作为基本数据结构。张量是数字的容器,矩阵就是二维张量。张量是矩阵向任意维度的推广。张量的维度通常称作轴。
仅包含一个数字的张量叫做标量(也叫 0D张量)
在 Numpy 中,一个 float32 或 float64 的数字就是一个标量张量(或标量数组)。你可以用 ndim 属性来查看一个 Numpy 张量的轴的个数。标量张量有 0 个轴(ndim == 0)。张量轴的个数也叫作阶(rank)。
例程如下:
>>> import numpy as np >>> x = np.array(12) >>> x array(12) >>> x.ndim 0
数组(也叫向量)是一维张量(1D张量),例程如下:
>>> x = np.array([12, 3, 6, 14, 7]) >>> x array([12, 3, 6, 14, 7]) >>> x.ndim 1
这个向量有5个元素,也被称为5D向量(注意不是5D张量)
矩阵是2D张量,3D张量可以想象成立方体,以此类推
深度学习处理的一般是0D到4D张量,但处理视频数据时可能会遇到5D张量
张量的属性有:轴数(ndim属性)、形状(shape属性)、数据类型(dtype属性)
向量数据
向量数据是最常见的数据,对于这种数据集,每个数据点都被编码为一个向量,因此一个数据批量就被编码为2D张量(即向量组成的数组),其中第一个轴是样本轴,第二个轴是特征轴。举两个例子:
人口统计数据集,其中包括每个人的年龄、邮编和收入。每个人可以表示为包含 3 个值 的向量,而整个数据集包含100 000 个人,因此可以存储在形状为 (100000, 3) 的 2D 张量中。
文本文档数据集,我们将每个文档表示为每个单词在其中出现的次数(字典中包含 20 000 个常见单词)。每个文档可以被编码为包含20 000 个值的向量(每个值对应于 字典中每个单词的出现次数),整个数据集包含500 个文档,因此可以存储在形状为 (500, 20000) 的张量中。
时间序列数据或序列数据
当时间(或序列顺序)对于数据很重要时,应该将数据存储在带有时间轴的3D 张量中。 每个样本可以被编码为一个向量序列(即2D 张量),因此一个数据批量就被编码为一个3D 张量,根据管理,我们将时间轴作为第二个轴,例子如下:
股票价格数据集。每一分钟,我们将股票的当前价格、前一分钟的最高价格和前一分钟的最低价格保存下来。因此每分钟被编码为一个3D 向量,整个交易日被编码为一个形状为 (390, 3) 的 2D 张量(一个交易日有 390 分钟),而 250 天的数据则可以保存在一 个形状为 (250, 390, 3) 的 3D 张量中。这里每个样本是一天的股票数据。
推文数据集。我们将每条推文编码为280个字符组成的序列,而每个字符又来自于128 个字符组成的字母表。在这种情况下,每个字符可以被编码为大小为 128 的二进制向量 (只有在该字符对应的索引位置取值为1,其他元素都为0)。那么每条推文可以被编码为一个形状为 (280, 128) 的 2D 张量,而包含100 万条推文的数据集则可以存储在一 个形状为 (1000000, 280, 128) 的张量中。
图像数据
如果图像大小为256×256,那么128 张灰度图像组成的批 量可以保存在一个形状为 (128, 256, 256, 1) 的张量中,而128 张彩色图像组成的批量则可以保存在一个形状为 (128, 256, 256, 3) 的张量中
图像张量的形状有两种约定:通道在后(channels-last)的约定(在 TensorFlow 中使用)和 通道在前(channels-first)的约定(在Theano 中使用)。Google 的 TensorFlow 机器学习框架将 颜色深度轴放在最后:(samples, height, width, color_depth)。与此相反,Theano 将图像深度轴放在批量轴之后:(samples, color_depth, height, width)。如果采 用 Theano 约定,前面的两个例子将变成 (128, 1, 256, 256) 和 (128, 3, 256, 256)。 Keras 框架同时支持这两种格式。
视频数据
视频可以看作一系列帧, 每一帧都是一张彩色图像。由于每一帧都可以保存在一个形状为 (height, width, color_ depth) 的 3D 张量中,因此一系列帧可以保存在一个形状为(frames, height, width, color_depth) 的 4D 张量中,而不同视频组成的批量则可以保存在一个5D 张量中,其形状为 (samples, frames, height, width, color_depth)。 例子如下:
一个以每秒4 帧采样的60 秒 YouTube 视频片段,视频尺寸为144×256,这个 视频共有 240 帧。4 个这样的视频片段组成的批量将保存在形状为 (4, 240, 144, 256, 3) 的张量中。
张量运算
relu运算
relu(x) = max(x, 0)
relu运算是一个逐元素运算,即该元素独立地应用于张量中的每个元素,因此这种运算非常适合大规模并行运算。
add运算
add运算也是一个逐元素运算
在实践中处理Numpy 数组时,这些逐元素运算(relu、add、逐元素乘法除法)都是优化好的Numpy 内置函数:
import numpy as np z = x + y #逐元素加法 z = np.maximum(z, 0.) #逐元素relu
广播
如果将两个形状不同的张量相加,较小的张量会被广播(broadcast),以匹配较大张量的形状。广播包含以下两步。
(1) 向较小的张量添加轴(叫作广播轴),使其 ndim 与较大的张量相同。
(2) 将较小的张量沿着新轴重复,使其形状与较大的张量相同。
来看一个具体的例子。假设 X 的形状是 (32, 10),y 的形状是 (10,)。首先,我们给 y 添加空的第一个轴,这样 y 的形状变为 (1, 10)。然后,我们将 y 沿着新轴重复32 次,这样得到的张量 Y 的形状为 (32, 10),并且 Y[i, :] == y for i in range(0, 32)。现在, 我们可以将 X 和 Y 相加,因为它们的形状相同。
在Python中,numpy会自动完成广播
import numpy as np x = np.random.random((64, 3, 32, 10)) #x是形状为(64,3,32,10)的随机张量 y = np.random.random((32, 10)) #y是形状为(32,10)的随机张量 z = np.maximum(x, y) #z是形状为(64,3,32,10)的张量,与x相同
张量点积(张量积)
在 Numpy、Keras、Theano 和 TensorFlow 中,都是用 * 实现逐元素乘积。TensorFlow 中的 点积使用了不同的语法,但在 Numpy 和 Keras 中,都是用标准的 dot 运算符来实现点积。
import numpy as np z = np.dot(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的每一行之间的点击,可以用这个程序来说明一下矩阵与向量点积的含义:
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
两个矩阵的点积即是线代中的矩阵乘法,也就是result[i][j] = x的第i行和y的第j列之间的点积
张量变形(reshape)
张量变形是改变张量的行和列,以得到想要的形状,变形后张量的元素总个数是没有发生改变的,相对的顺序也没有发生改变,例子如下:
>>> x = np.array([[0., 1.], [2., 3.], [4., 5.]]) >>> print(x.shape) (3, 2) >>> x = x.reshape((6, 1)) >>> x array([[ 0.], [ 1.], [ 2.], [ 3.], [ 4.], [ 5.]]) >>> x = x.reshape((2, 3)) >>> x array([[ 0., 1., 2.], [ 3., 4., 5.]])
张量转置(transpose)
转置就是线代里面的转置,比如x = np.transpose(x),结果就会把x从(20,300)转置成(300,20)
神经网络的引擎:基于梯度的优化
是利用网络中所有运算都是可微(differentiable)的这一事实,计算损失相对于网络系数的梯度(gradient),然后向梯度的反方向改变系数,从而使损失降低。
梯度可以理解成张量运算的导数,单变量函数 f(x) 的导数可以看作函数 f 曲线的斜率。同样,gradient(f) (W0) 也可以看作表示 f(W) 在 W0 附近曲率(curvature)的张量
y_pred = dot(W, x)
loss_value = loss(y_pred, y)
这也就是说,如果输入数据 x 和 y 保持不变,那么这可以看作将 W 映射到损失值的函数。即我们可以假设 f(W) = loss_value
对于一个函数 f(x),你可以通过将 x 向导数的反方向移动一小步来减小 f(x) 的值。同 样,对于张量的函数 f(W),你也可以通过将 W 向梯度的反方向移动来减小 f(W),比如 W1 = W0 - step * gradient(f)(W0),其中 step 是一个很小的比例因子。也就是说,沿着曲 率的反方向移动,直观上来看在曲线上的位置会更低(也就是loss_value更小了)。
一个基于梯度优化的方案是小批量随机梯度下降(又称为小批量SGD)
(1) 抽取训练样本 x 和对应目标 y 组成的数据批量。
(2) 在 x 上运行网络,得到预测值 y_pred。
(3) 计算网络在这批数据上的损失,用于衡量 y_pred 和 y 之间的距离。
(4) 计算损失相对于网络参数的梯度[一次反向传播(backward pass)]。
(5) 将参数沿着梯度的反方向移动一点,比如 W -= step * gradient,从而使这批数据上的损失减小一点。
如果使用小学习率(也叫步长)的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)
一个例子
构建深度学习网络来识别手写数字
(train_images, train_labels), (test_images, test_labels) = mnist.load_data() # 把输入图像保存在28*28的float32的Numpy张量中 train_images = train_images.reshape((60000, 28 * 28)) train_images = train_images.astype('float32') / 255 # 把输出图像保存在28*28的float32的Numpy张量中 test_images = test_images.reshape((10000, 28 * 28)) test_images = test_images.astype('float32') / 255 # 开始构建网络(这个网络包含了两个Dense层) network = models.Sequential() network.add(layers.Dense(512, activation='relu', input_shape=(28 * 28,)))
network.add(layers.Dense(10, activation='softmax')) # 进行网络编译(categorical_crossentropy 是损失函数,是用于学习权重张量的反馈信号,在训练阶段应使它最小化。
# 减小损失是通过小批量随机梯度下降来实现的。 梯度下降的具体方法由第一个参数给定,即 rmsprop 优化器) network.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy']) # 开始训练网络
# 在调用 fit 时:网络开始在训练数据上进行迭代(每个小批量包含 128 个样本),共迭代5次[在所有训练数据上迭代一次叫作一个轮次(epoch)]。 network.fit(train_images, train_labels, epochs=5, batch_size=128)
本章总结
- 学习是指找到一组模型参数,使得在给定的训练数据样本和对应目标值上的损失函数最小化。
- 学习的过程:随机选取包含数据样本及其目标值的批量,并计算批量损失相对于网络参数的梯度。随后将网络参数沿着梯度的反方向稍稍移动(移动距离由学习率指定)。
- 整个学习过程之所以能够实现,是因为神经网络是一系列可微分的张量运算,因此可以利用求导的链式法则来得到梯度函数,这个函数将当前参数和当前数据批量映射为一个梯度值。
- 将数据输入网络之前,需要先定义损失和优化器。
- 损失是在训练过程中需要最小化的量,因此,它应该能够衡量当前任务是否已成功解决。
- 优化器是使用损失梯度更新参数的具体方式,比如RMSProp 优化器、带动量的随机梯度下降(SGD)等。
一些疑惑
关于链式求导(反向传播算法),没有太理解,回头学懂了再来补充