使用 MNIST数据集将将手写数字的灰度图像(28 像素×28 像素)划分到 10 个类别中(0~9),它是机器学习领域的一个经典数据集,包含60000张训练图像和10000 张测试图像。
[warningbox title="关于类和标签的说明"]
在机器学习中,分类问题中的某个类别叫作类(class)。数据点叫作样本(sample)。某个样本对应的类叫作标签(label)。[/warningbox]
MNIST 数据集预先加载在 Keras 库中,其中包括 4 个Numpy数组。
[warningbox title="数据导入及格式预览"]
# MNIST 数据集导入
from keras.datasets import mnist
# print(type(mnist.load_data()))
# print(type(tuple(mnist.load_data())))
'图像被编码为Numpy数组,标签为数字数组'
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
print(train_images.shape)
print(len(train_labels))
print(type(test_images))
print(len(test_labels))
[/warningbox]
from keras.datasets import mnist
from keras import models
'''
神经网络的核心组件是层(layer),它是一种数据处理模块,
你可以将它看成数据过滤器。进去一些数据,出来的数据变得更加有用。
具体来说,层从输入数据中提取表示——我们期望这种表示有助于解决手头的问题。
大多数深度学习都是将简单的层链接起来,从而实现渐进式的数据蒸馏(data distillation)。
深度学习模型就像是数据处理的筛子,包含一系列越来越精细的数据过滤器(即层)。
'''
from keras import layers
from keras.utils import to_categorical
'''
首先,将训练数据(train_images 和 train_labels)输入神经网络;其次,网络学习将图像和标签关联在一起;
最后,网络对 test_images 生成预测, 而我们将验证这些预测与 test_labels 中的标签是否匹配。
本例中的网络包含 2 个 Dense 层,它们是密集连接(也叫全连接)的神经层。
第二层(也是最后一层)是一个 10 路 softmax 层,它将返回一个由 10 个概率值(总和为 1)组成的数组。
每个概率值表示当前数字图像属于 10 个数字类别中某一个的概率。
要想训练网络,我们还需要选择编译(compile)步骤的三个参数。
损失函数(loss function):网络如何衡量在训练数据上的性能,即网络如何朝着正确的方向前进。
优化器(optimizer):基于训练数据和损失函数来更新网络的机制。
在训练和测试过程中需要监控的指标(metric):本例只关心精度,即正确分类的图像所占的比例。
'''
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
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'])
# 图像数据为uint8类型的数组,取值区间为 [0, 255],需转换为float32数组,取值范围为 0~1,(60000,28,28) => (60000,28*28)
# reshape 在不改变数据内容的情况下,改变一个数组的格式,参数及返回值
train_images = train_images.reshape((60000, 28 * 28))
# astype 改变np.array中所有数据元素的数据类型。
train_images = train_images.astype('float32') / 255
test_images = test_images.reshape((10000, 28 * 28))
test_images = test_images.astype('float32') / 255
# to_categorical 将类别向量转换为二进制(只有0和1)的矩阵类型表示。其表现为将原有的类别向量转换为独热编码onehot的形式。
# 也就是说它是对于一个类型的容器(整型)的转化为二元类型矩阵。比如用来计算多类别交叉熵来使用的。
train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)
# 准备开始训练网络,在 Keras 中这一步是通过调用网络的 fit 方法来完成的
'''
为了获得性能良好的神经网络,网络定型过程中需要进行许多关于所用设置(超参数)的决策。超参数之一是定型周期(epoch)的数量.
epochs被定义为向前和向后传播中所有批次的单次训练迭代。这意味着1个epoch是整个输入数据的单次向前和向后传递。简单说,epochs指的就是训练过程中数据将被“轮”多少次。 亦即应当完整遍历数据集多少次(一次为一个epoch)。
Epoch(回合)代表样本集内所有的数据经过了一次训练。
如果epoch数量太少,网络有可能发生欠拟合(即对于定型数据的学习不够充分);如果epoch数量太多,则有可能发生过拟合(即网络对定型数据中的“噪声”而非信号拟合)。
'''
'''
Batch_Size(批尺寸)是机器学习中一个重要参数
计算梯度所需的样本数量,太小会导致效率低下,无法收敛。
太大会导致内存撑不住,Batch Size增大到一定程度后,其下降方向变化很小了,所以Batch Size是一个很重要的参数。
'''
network.fit(train_images, train_labels, epochs=5, batch_size=128)
test_loss, test_acc = network.evaluate(test_images, test_labels)
'''
60000/60000 [==============================] - 5s 76us/step - loss: 0.0376 - accuracy: 0.9887
训练过程中的两个数字 网络在训练数据上的损失(loss)网络在训练数据上的精度(acc)
'''
print(test_loss, test_acc)
# 0.06532313829974737 0.9797000288963318
# 测试集精度为 97.8%,比训练集精度低不少。训练精度和测试精度之间的这种差距是过拟合(overfit)造成的。
# 过拟合是指机器学习模型在新数据上的性能往往比在训练数据上要差
[successbox title="张量"]
上一案例使用的数据存储在多维 Numpy 数组中,也叫张量(tensor)。
一般来说,当前所有机器学习系统都使用张量作为基本数据结构。张量对这个领域非常重要,
张量这一概念的核心在于,它是一个数据容器。它包含的数据几乎总是数值数据,因此它是数字的容器。
你可能对矩阵很熟悉,它是二维张量。张量是矩阵向任意维度的推广[注意, 张量的维度(dimension)通常叫作轴(axis)]。
[/successbox]
[successbox title="标量(0D 张量)"]
仅包含一个数字的张量叫作标量(scalar,也叫标量张量、零维张量、0D 张量)。在 Numpy
中,一个 float32 或 float64 的数字就是一个标量张量(或标量数组)。你可以用 ndim 属性
来查看一个 Numpy 张量的轴的个数。标量张量有 0 个轴(ndim == 0)。张量轴的个数也叫作
阶(rank)。
import numpy as np
x = np.array(12)
print(x)
print(x.ndim)
[/successbox]
[successbox title="向量(1D 张量)"]
数字组成的数组叫作向量(vector)或一维张量(1D 张量)。一维张量只有一个轴。下面是一个 Numpy 向量。
这个向量有 5 个元素,所以被称为 5D 向量。不要把 5D 向量和 5D 张量弄混! 5D 向量只有一个轴,沿着轴有 5 个维度,而 5D 张量有 5 个轴(沿着每个轴可能有任意个维度)。维度
(dimensionality)可以表示沿着某个轴上的元素个数(比如 5D 向量),也可以表示张量中轴的个数(比如 5D 张量),这有时会令人感到混乱。对于后一种情况,技术上更准确的说法是 5 阶张量
(张量的阶数即轴的个数),但 5D 张量这种模糊的写法更常见。
import numpy as np
x = np.array([12,11,10,9,8])
print(x)
print(x.ndim)
[/successbox]
[successbox title="矩阵(2D 张量)"]
向量组成的数组叫作矩阵(matrix)或二维张量(2D 张量)。矩阵有 2 个轴(通常叫作行和列)。
你可以将矩阵直观地理解为数字组成的矩形网格。下面是一个 Numpy 矩阵。
import numpy as np
x = np.array([[12, 11, 10, 9, 8],
[7, 6, 5, 4, 3],
[2, 1, 0, 13, 14]])
print(x)
print(x.ndim)
第一个轴上的元素叫作行(row),第二个轴上的元素叫作列(column)。在上面的例子中,
[12, 11, 10, 9, 8] 是 x 的第一行,[12, 7, 2] 是第一列。
[/successbox]
[successbox title="3D 张量与更高维张量"]
import numpy as np
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)
将多个矩阵组合成一个新的数组,可以得到一个 3D 张量,你可以将其直观地理解为数字组成的立方体。下面是一个 Numpy 的 3D 张量。
将多个 3D 张量组合成一个数组,可以创建一个 4D 张量,以此类推。深度学习处理的一般是 0D 到 4D 的张量,但处理视频数据时可能会遇到 5D 张量。
[/successbox]
[successbox title="关键属性"]
张量是由以下三个关键属性来定义的。
轴的个数(阶)。例如,3D 张量有 3 个轴,矩阵有 2 个轴。这在 Numpy 等 Python 库中也叫张量的 ndim。
形状。这是一个整数元组,表示张量沿每个轴的维度大小(元素个数)。例如,前面矩阵示例的形状为 (3, 5),3D 张量示例的形状为 (3, 3, 5)。向量的形状只包含一个元素,比如 (5,),而标量的形状为空,即 ()。
数据类型(在 Python 库中通常叫作 dtype)。这是张量中所包含数据的类型,例如,张量的类型可以是 float32、uint8、float64 等。在极少数情况下,你可能会遇到字符(char)张量。注意,Numpy(以及大多数其他库)中不存在字符串张量,因为张量存储在预先分配的连续内存段中,而字符串的长度是可变的,无法用这种方式存储
from keras.datasets import mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
# 张量 train_images 的轴的个数,即 ndim 属性
print(train_images.ndim)
# 形状
print(train_images.shape)
# 数据类型,即 dtype 属性
print(train_images.dtype)
'''
train_images 是一个由 8 位整数组成的 3D 张量。更确切地说,它是60000
个矩阵组成的数组,每个矩阵由 28×28 个整数组成。每个这样的矩阵都是一张灰度图像,元素取值范围为 0~255
'''
# 通过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)。
[/successbox]
[successbox title="在 Numpy 中操作张量"]
import numpy as np
from keras.datasets import mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
'Numpy 数组上的张量切片运'
# 选择第 10~100 个数字(不包括第 100 个),并将其放在形状为 (90, 28, 28) 的数组中。
my_slice = train_images[10:100]
print(my_slice.shape)
print(len(my_slice))
# 等同于第一个的写法,给出了切片沿着每个张量轴的起始索引和结束索引。注意,: 等同于选择整个轴。
my_slice = train_images[10:100, :, :]
print(my_slice.shape)
print(len(my_slice))
# 等同于第一个的写法,给出了切片沿着每个张量轴的起始索引和结束索引。注意,: 等同于选择整个轴。
my_slice = train_images[10:100, 0:28, 0:28]
print(my_slice.shape)
print(len(my_slice))
# 沿着每个张量轴在任意两个索引之间进行选择。例如,你可以在所有图像的右下角选出 14 像素×14 像素的区域
my_slice = train_images[:, 14:, 14:]
print(my_slice.shape)
print(len(my_slice))
# 使用负数索引。与 Python 列表中的负数索引类似,它表示与当前轴终点的相对位置。在图像中心裁剪出 14 像素×14 像素的区域
my_slice = train_images[:, 7:-7, 7:-7]
print(my_slice.shape)
print(len(my_slice))
[/successbox]
[successbox title="数据批量的概念"]
from keras.datasets import mnist
'''
深度学习中所有数据张量的第一个轴(0 轴,因为索引从 0 开始)都是样本轴
(samples axis,有时也叫样本维度)。在 MNIST 的例子中,样本就是数字图像。
深度学习模型不会同时处理整个数据集,而是将数据拆分成小批量
'''
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
# MNIST 数据集的一个批量,批量大小为 128。
batch = train_images[:128]
print(batch.shape)
batch = train_images[128:256]
print(batch.shape)
batch = train_images[256:384]
print(batch.shape)
# 第 n 个批量
# batch = train_images[n * 128:(n + 1) * 128]
[/successbox]
[successbox title="现实世界中的数据张量"]
向量数据: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)。
[/successbox]
[successbox title="向量数据"]
这是最常见的数据。对于这种数据集,每个数据点都被编码为一个向量,因此一个数据批量就被编码为 2D 张量(即向量组成的数组),其中第一个轴是样本轴,第二个轴是特征轴。
人口统计数据集,其中包括每个人的年龄、邮编和收入。每个人可以表示为包含 3 个值的向量,而整个数据集包含 100 000 个人,因此可以存储在形状为 (100000, 3) 的 2D 张量中。
文本文档数据集,我们将每个文档表示为每个单词在其中出现的次数(字典中包含20 000 个常见单词)。每个文档可以被编码为包含 20 000 个值的向量(每个值对应于
字典中每个单词的出现次数),整个数据集包含 500 个文档,因此可以存储在形状为(500, 20000) 的张量中。
[/successbox]
[successbox title="时间序列数据或序列数据"]
当时间(或序列顺序)对于数据很重要时,应该将数据存储在带有时间轴的 3D 张量中。每个样本可以被编码为一个向量序列(即 2D 张量),因此一个数据批量就被编码为一个 3D 张量
根据惯例,时间轴始终是第 2 个轴(索引为 1 的轴)。
股票价格数据集。每一分钟,我们将股票的当前价格、前一分钟的最高价格和前一分钟的最低价格保存下来。因此每分钟被编码为一个 3D 向量,整个交易日被编码为一个形状为 (390, 3) 的 2D 张量(一个交易日有 390 分钟),而 250 天的数据则可以保存在一个形状为 (250, 390, 3) 的 3D 张量中。这里每个样本是一天的股票数据。
推文数据集。我们将每条推文编码为 280 个字符组成的序列,而每个字符又来自于 128个字符组成的字母表。在这种情况下,每个字符可以被编码为大小为 128 的二进制向量(只有在该字符对应的索引位置取值为 1,其他元素都为 0)。那么每条推文可以被编码为一个形状为 (280, 128) 的 2D 张量,而包含 100 万条推文的数据集则可以存储在一个形状为 (1000000, 280, 128) 的张量中。
[/successbox]
[successbox title="图像数据"]
图像通常具有三个维度:高度、宽度和颜色深度。虽然灰度图像(比如 MNIST 数字图像) 只有一个颜色通道,因此可以保存在 2D 张量中,但按照惯例,图像张量始终都是 3D 张量,灰度图像的彩色通道只有一维。因此,如果图像大小为 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 框架同时支持这两种格式。
[/successbox]
[successbox title="视频数据"]
视频数据是现实生活中需要用到 5D 张量的少数数据类型之一。视频可以看作一系列帧, 每一帧都是一张彩色图像。由于每一帧都可以保存在一个形状为 (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)
的张量中。总共有 106 168 320 个值!如果张量的数据类型(dtype)是 float32,每个值都是
32 位,那么这个张量共有 405MB。好大!你在现实生活中遇到的视频要小得多,因为它们不以
float32 格式存储,而且通常被大大压缩,比如 MPEG 格式。
[/successbox]
所有计算机程序最终都可以简化为二进制输入上的一些二进制运算(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)。
[successbox title="逐元素运算"]
relu 运算和加法都是逐元素(element-wise)的运算,即该运算独立地应用于张量中的每个元素,也就是说,这些运算非常适合大规模并行实现(向量化实现,这一术语来自于 1970—1990 年间向量处理器超级计算机架构)。如果你想对逐元素运算编写简单的 Python 实现,那么可以用 for 循环。下列代码是对逐元素 relu 运算的简单实现
# x 为 Numpy 的 2D 张量
def native_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 native_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.) #逐元素的 relu
[/successbox]
[successbox title="广播"]
在前面介绍的Dense 层中,将一个 2D 张量与一个向量相加。如果将两个形状不同的张量相加,会发生什么?
如果没有歧义的话,较小的张量会被广播(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 相加,因为它们的形状相同。
在实际的实现过程中并不会创建新的 2D 张量,因为那样做非常低效。重复的操作完全是虚拟的,它只出现在算法中,而没有发生在内存中。但想象将向量沿着新轴重复 10 次,是一种很有用的思维模型。下面是一种简单的实现。
import numpy as np
def naive_add_matrix_and_vector(x, y):
# Python assert(断言)用于判断一个表达式,在表达式条件为 false 的时候触发异常。
# 断言可以在条件不满足程序运行的情况下直接返回错误,而不必等待程序运行后出现崩溃的情况,
# 例如我们的代码只能在 Linux 系统下运行,可以先判断当前系统是否符合条件。
print(x.shape)
print(y.shape)
# x 是一个 Numpy 的 2D 张量
assert len(x.shape) == 2
# y 是一个 Numpy 向量
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
x = np.array([[1, 2], [3, 4]])
y = np.array([3, 4])
print(naive_add_matrix_and_vector(x, y))
如果一个张量的形状是 (a, b, ... n, n+1, ... m),另一个张量的形状是 (n, n+1,
... m),那么你通常可以利用广播对它们做两个张量之间的逐元素运算。广播操作会自动应用于从 a 到 n-1 的轴。
下面这个例子利用广播将逐元素的 maximum 运算应用于两个形状不同的张量。
import numpy as np
# x 是形状为 (64, 3, 32, 10) 的随机张量
x = np.random.random((64,3,32,10))
# y 是形状为 (32, 10) 的随机张量
y = np.random.random((32,10))
# 输出 z 的形状是与 x 相同
z = np.maximum(x, y)
print(x.shape)
print(y.shape)
print(z.shape)
[/successbox]
[successbox title="张量点积"]
点积运算,也叫张量积(tensor product,不要与逐元素的乘积弄混),是最常见也最有用的张量运算。与逐元素的运算不同,它将输入张量的元素合并在一起。
在 Numpy、Keras、Theano 和 TensorFlow 中,都是用 * 实现逐元素乘积。TensorFlow 中的点积使用了不同的语法,但在 Numpy 和 Keras 中,都是用标准的 dot 运算符来实现点积。
import numpy as np
# 两个向量 x 和 y 的点积。其计算过程如下
def naive_vector_dot(x, y):
assert len(x.shape) == 1
assert len(y.shape) == 1
# x 和 y 都是 Numpy 向量
assert x.shape[0] == y.shape[0]
z = 0.
for i in range(x.shape[0]):
z += x[i] * y[i]
return z
x = np.array([13,14])
y = np.array([11,12])
# 数学符号中的点(.)表示点积运算。
z = np.dot(x, y)
h = naive_vector_dot(x,y)
print(x.shape)
print(y.shape)
print(z.shape)
print(z)
print(h)
import numpy as np
'''
两个向量之间的点积是一个标量,而且只有元素个数相同的向量之间才能做点积。
还可以对一个矩阵 x 和一个向量 y 做点积,返回值是一个向量,其中每个元素是 y 和 x
的每一行之间的点积。其实现过程如下。
'''
def naive_matrix_vector_dot(x, y):
# x 是一个 Numpy 矩阵
assert len(x.shape) == 2
# y 是一个 Numpy 向量
assert len(y.shape) == 1
# x 的第 1 维和 y 的第 0 维大小必须相同
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
x = np.array([[1,2],[3,4]])
y = np.array([5,6])
z = naive_matrix_vector_dot(x, y)
print(z.shape)
print(z)
注意,如果两个张量中有一个的 ndim 大于 1,那么 dot 运算就不再是对称的,也就是说,ot(x, y) 不等于 dot(y, x)。
当然,点积可以推广到具有任意个轴的张量。最常见的应用可能就是两个矩阵之间的点积。对于两个矩阵 x 和 y,当且仅当 x.shape[1] == y.shape[0] 时,你才可以对它们做点积(dot(x, y))。得到的结果是一个形状为 (x.shape[0], y.shape[1]) 的矩阵,其元素为x的行与y的列之间的点积。其简单实现如下。
import numpy as np
# 两个向量 x 和 y 的点积
def naive_vector_dot(x, y):
assert len(x.shape) == 1
assert len(y.shape) == 1
# x 和 y 都是 Numpy 向量
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_dot(x, y):
# x 和 y 都是Numpy 矩阵
assert len(x.shape) == 2
assert len(y.shape) == 2
# x 的第 1 维和 y 的第 0 维大小必须相同
assert x.shape[1] == y.shape[0]
# 这个运算返回特定形状的零矩阵 一个给定形状和类型的用0填充的数组
z = np.zeros((x.shape[0], y.shape[1]))
# 遍历 x 的所有行
for i in range(x.shape[0]):
# 遍历 y 的所有列
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 = np.array([[1, 2], [3, 4]])
y = np.array([[5, 6], [7, 8]])
z = naive_matrix_dot(x, y)
print(z.shape)
print(z)
print(np.dot(x,y))
x、y 和 z 都用矩形表示(元素按矩形排列)。x 的行和 y 的列必须大小相同,因
此 x 的宽度一定等于 y 的高度。如果你打算开发新的机器学习算法,可能经常要画这种图。
更一般地说,你可以对更高维的张量做点积,只要其形状匹配遵循与前面 2D 张量相同的
原则:
(a, b, c, d) . (d,) -> (a, b, c)
(a, b, c, d) . (d, e) -> (a, b, c, e)
以此类推。
[/successbox]
[successbox title="张量变形"]
第三个重要的张量运算是张量变形(tensor reshaping)。虽然前面神经网络第一个例子的
Dense 层中没有用到它,但在将图像数据输入神经网络之前,我们在预处理时用到了这个运算。
train_images = train_images.reshape((60000, 28 * 28))
张量变形是指改变张量的行和列,以得到想要的形状。变形后的张量的元素总个数与初始
张量相同。简单的例子可以帮助我们理解张量变形。
import numpy as np
x = np.array([[0., 1.],[2., 3.],[4., 5.]])
print(x.shape)
print("___")
x = x.reshape((6, 1))
print(x.shape)
print(x)
print("___")
x = x.reshape((2, 3))
print(x.shape)
print(x)
import numpy as np
'''
经常遇到的一种特殊的张量变形是转置(transposition)。对矩阵做转置是指将行和列互换,
使 x[i, :] 变为 x[:, i]。
'''
# 创建一个形状为 (300, 20) 的零矩阵
x = np.zeros((300,200))
# transpose 二维数组 调换数组的行列值的索引值
# 三维数组 调换x,y,z的位置 (x,y,z)=>(y,x,z)
x = np.transpose(x)
print(x.shape)
[/successbox]
[successbox title="张量运算的几何解释"]
对于张量运算所操作的张量,其元素可以被解释为某种几何空间内点的坐标,因此所有的张量运算都有几何解释。举个例子,我们来看加法。首先有这样一个向量:
A = [0.5, 1]
它是二维空间中的一个点。常见的做法是将向量描绘成原点到这个点的箭头,
假设又有一个点:B = [1, 0.25],将它与前面的 A 相加。从几何上来看,这相当于将两
个向量箭头连在一起,得到的位置表示两个向量之和对应的向量。
通常来说,仿射变换、旋转、缩放等基本的几何操作都可以表示为张量运算。举个例子,要将
一个二维向量旋转 theta 角,可以通过与一个 2×2 矩阵做点积来实现,这个矩阵为 R = [u, v],其
中u和v都是平面向量:u = [cos(theta), sin(theta)],v = [-sin(theta), cos(theta)]。
[/successbox]
[successbox title="深度学习的几何解释"]
前面讲过,神经网络完全由一系列张量运算组成,而这些张量运算都只是输入数据的几何变换。因此,你可以将神经网络解释为高维空间中非常复杂的几何变换,这种变换可以通过许多简单的步骤来实现。对于三维的情况,下面这个思维图像是很有用的。想象有两张彩纸:一张红色,一张蓝色。
将其中一张纸放在另一张上。现在将两张纸一起揉成小球。这个皱巴巴的纸球就是你的输入数据,每张纸对应于分类问题中的一个类别。神经网络(或者任何机器学习模型)要做的就是找到可以让纸球恢复平整的变换,从而能够再次让两个类别明确可分。通过深度学习,这一过程可以用三维空间中一系列简单的变换来实现,比如你用手指对纸球做的变换,每次做一个动作,
让纸球恢复平整就是机器学习的内容:为复杂的、高度折叠的数据流形找到简洁的表示。
现在你应该能够很好地理解,为什么深度学习特别擅长这一点:它将复杂的几何变换逐步分解
为一长串基本的几何变换,这与人类展开纸球所采取的策略大致相同。深度网络的每一层都通
过变换使数据解开一点点——许多层堆叠在一起,可以实现非常复杂的解开过程。
[/successbox]
我们的第一个神经网络示例中,每个神经层都用下述方法对输入数据进行变换。
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 之间的距离非常小。网络“学会”将输入映射到正确的目标。乍一看可能像魔法一样,但如果你将其简化为基本步骤,那么会变得非常简单。第一步看起来非常简单,只是输入 / 输出(I/O)的代码。第二步和第三步仅仅是一些张量运算的应用,所以你完全可以利用上一节学到的知识来实现这两步。难点在于第四步:更新网络的权重。考虑网络中某个权重系数,你怎么知道这个系数应该增大还是减小,以及变化多少?
一种简单的解决方案是,保持网络中其他权重不变,只考虑某个标量系数,让其尝试不同的取值。假设这个系数的初始值为 0.3。对一批数据做完前向传播后,网络在这批数据上的损失是 0.5。如果你将这个系数的值改为 0.35 并重新运行前向传播,损失会增大到 0.6。但如果你将这个系数减小到 0.25,损失会减小到 0.4。在这个例子中,将这个系数减小 0.05 似乎有助于使损失最小化。对于网络中的所有系数都要重复这一过程。但这种方法是非常低效的,因为对每个系数(系数很多,通常有上千个,有时甚至多达上百万个)都需要计算两次前向传播(计算代价很大)。一种更好的方法是利用网络中所有运算都是可微(differentiable)的这一事实,计算损失相对于网络系数的梯度(gradient),然后向梯度的反方向改变系数,从而使损失降低。
[successbox title="什么是导数"]
假设有一个连续的光滑函数 f(x) = y,将实数 x 映射为另一个实数 y。由于函数是连续的, x 的微小变化只能导致 y 的微小变化——这就是函数连续性的直观解释。假设 x 增大了一个很小的因子 epsilon_x,这导致 y 也发生了很小的变化,即 epsilon_y:
f(x + epsilon_x) = y + epsilon_y
此外,由于函数是光滑的(即函数曲线没有突变的角度),在某个点 p 附近,如果 epsilon_x足够小,就可以将 f 近似为斜率为 a 的线性函数,这样 epsilon_y 就变成了 a * epsilon_x:
f(x + epsilon_x) = y + a * epsilon_x
显然,只有在 x 足够接近 p 时,这个线性近似才有效。
斜率 a 被称为 f 在 p 点的导数(derivative)。如果 a 是负的,说明 x 在 p 点附近的微小变化将导致 f(x) 减小(如图 2-10 所示);如果 a 是正的,那么 x 的微小变化将导致 f(x) 增大。此外,a 的绝对值(导数大小)表示增大或减小的速度快慢。
对于每个可微函数 f(x)(可微的意思是“可以被求导”。例如,光滑的连续函数可以被求导),都存在一个导数函数 f'(x),将 x 的值映射为 f 在该点的局部线性近似的斜率。例如,cos(x) 的导数是 -sin(x),f(x) = a * x 的导数是 f'(x) = a,等等。
如果你想要将 x 改变一个小因子 epsilon_x,目的是将 f(x) 最小化,并且知道 f 的导数, 那么问题解决了:导数完全描述了改变 x 后 f(x) 会如何变化。如果你希望减小 f(x) 的值,只需将 x 沿着导数的反方向移动一小步。
[/successbox]
[successbox title="张量运算的导数:梯度"]
梯度(gradient)是张量运算的导数。它是导数这一概念向多元函数导数的推广。多元函数是以张量作为输入的函数。
假设有一个输入向量 x、一个矩阵 W、一个目标 y 和一个损失函数 loss。你可以用 W 来计算预测值 y_pred,然后计算损失,或者说预测值 y_pred 和目标 y 之间的距离。
y_pred = dot(W, x)
loss_value = loss(y_pred, y)
如果输入数据 x 和 y 保持不变,那么这可以看作将 W 映射到损失值的函数。
loss_value = f(W)
假设W 的当前值为W0。f 在W0 点的导数是一个张量gradient(f)(W0),其形状与 W 相同,每个系数 gradient(f)(W0)[i, j] 表示改变 W0[i, j] 时 loss_value 变化的方向和大小。张量 gradient(f)(W0) 是函数 f(W) = loss_value 在 W0 的导数。
前面已经看到,单变量函数 f(x) 的导数可以看作函数 f 曲线的斜率。同样,gradient(f) (W0) 也可以看作表示 f(W) 在 W0 附近曲率(curvature)的张量。
对于一个函数 f(x),你可以通过将 x 向导数的反方向移动一小步来减小 f(x) 的值。同样,对于张量的函数 f(W),你也可以通过将 W 向梯度的反方向移动来减小 f(W),比如 W1 = W0 - step * gradient(f)(W0),其中 step 是一个很小的比例因子。也就是说,沿着曲率的反方向移动,直观上来看在曲线上的位置会更低。注意,比例因子 step 是必需的,因为gradient(f)(W0) 只是 W0 附近曲率的近似值,不能离 W0 太远。
[/successbox]
[successbox title="随机梯度下降"]
给定一个可微函数,理论上可以用解析法找到它的最小值:函数的最小值是导数为 0 的点,因此你只需找到所有导数为 0 的点,然后计算函数在其中哪个点具有最小值。
将这一方法应用于神经网络,就是用解析法求出最小损失函数对应的所有权重值。可以通过对方程 gradient(f)(W) = 0 求解 W 来实现这一方法。这是包含 N 个变量的多项式方程,其中 N 是网络中系数的个数。N=2 或 N=3 时可以对这样的方程求解,但对于实际的神经网络是无法求解的,因为参数的个数不会少于几千个,而且经常有上千万个。
相反,你可以使用 "神经网络的“引擎”:基于梯度的优化" 开头总结的四步算法:基于当前在随机数据批量上的损失,一点一点地对参数进行调节。由于处理的是一个可微函数,你可以计算出它的梯度,从而有效地实现第四步。沿着梯度的反方向更新权重,损失每次都会变小一点。
(1) 抽取训练样本 x 和对应目标 y 组成的数据批量。
(2) 在 x 上运行网络,得到预测值 y_pred。
(3) 计算网络在这批数据上的损失,用于衡量 y_pred 和 y 之间的距离。
(4) 计算损失相对于网络参数的梯度[一次反向传播(backward pass)]。
(5) 将参数沿着梯度的反方向移动一点,比如 W -= step * gradient,从而使这批数据上的损失减小一点。
刚刚描述的方法叫作小批量随机梯度下降(mini-batch stochastic gradient descent,又称为小批量 SGD)。术语随机(stochastic)是指每批数据都是随机抽取的(stochastic 是random在科学上的同义词 a)。下图给出了一维的情况,网络只有一个参数,并且只有一个训练样本。
直观上来看,为 step 因子选取合适的值是很重要的。如果取值太小,则沿着曲线的下降需要很多次迭代,而且可能会陷入局部极小点。如果取值太大,则更新权重值之后可能会出现在曲线上完全随机的位置。
注意,小批量 SGD 算法的一个变体是每次迭代时只抽取一个样本和目标,而不是抽取一批数据。这叫作真 SGD(有别于小批量 SGD)。还有另一种极端,每一次迭代都在所有数据上运行,这叫作批量 SGD。这样做的话,每次更新都更加准确,但计算代价也高得多。这两个极端之间的有效折中则是选择合理的批量大小。
上图描述的是一维参数空间中的梯度下降,但在实践中需要在高维空间中使用梯度下降。神经网络的每一个权重参数都是空间中的一个自由维度,网络中可能包含数万个甚至上百万个参数维度。为了让你对损失曲面有更直观的认识,你还可以将梯度下降沿着二维损失曲面可视化,如下图所示。但你不可能将神经网络的实际训练过程可视化,因为你无法用人类可以理解的方式来可视化 1 000 000 维空间。因此最好记住,在这些低维表示中形成的直觉在实践中不一定总是准确的。这在历史上一直是深度学习研究的问题来源。
此外,SGD 还有多种变体,其区别在于计算下一次权重更新时还要考虑上一次权重更新,而不是仅仅考虑当前梯度值,比如带动量的 SGD、Adagrad、RMSProp 等变体。这些变体被称为优化方法(optimization method)或优化器(optimizer)。其中动量的概念尤其值得关注,它在许多变体中都有应用。动量解决了 SGD 的两个问题:收敛速度和局部极小点。下图给出了损失作为网络参数的函数的曲线。
如你所见,在某个参数值附近,有一个局部极小点(local minimum):在这个点附近,向左移动和向右移动都会导致损失值增大。如果使用小学习率的 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)
[/successbox]
[successbox title="链式求导:反向传播算法"]
在前面的算法中,我们假设函数是可微的,因此可以明确计算其导数。在实践中,神经网络函数包含许多连接在一起的张量运算,每个运算都有简单的、已知的导数。例如,下面这个网络 f 包含 3 个张量运算 a、b 和 c,还有 3 个权重矩阵 W1、W2 和 W3。
f(W1, W2, W3) = a(W1, b(W2, c(W3)))
根据微积分的知识,这种函数链可以利用下面这个恒等式进行求导,它称为链式法则(chain rule):
(f(g(x)))' = f'(g(x)) * g'(x)
将链式法则应用于神经网络梯度值的计算,得到的算法叫作反向传播(backpropagation,有时也叫反式微分,reverse-mode differentiation)。反向传播从最终损失值开始,从最顶层反向作用至最底层,利用链式法则计算每个参数对损失值的贡献大小。
现在以及未来数年,人们将使用能够进行符号微分(symbolic differentiation)的现代框架来实现神经网络,比如 TensorFlow。也就是说,给定一个运算链,并且已知每个运算的导数,这些框架就可以利用链式法则来计算这个运算链的梯度函数,将网络参数值映射为梯度值。对于这样的函数,反向传播就简化为调用这个梯度函数。由于符号微分的出现,你无须手动实现反向传播算法。因此,我们不会在本节浪费你的时间和精力来推导反向传播的具体公式。你只需充分理解基于梯度的优化方法的工作原理。
[/successbox]
from keras.datasets import mnist
from keras import models
from keras import layers
from keras.utils import to_categorical
(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
# 输入图像保存在 float32 格式的 Numpy 张量中,形状分别为 (60000, 784)(训练数据)和 (10000, 784)(测试数据)。
# 构建网络
network = models.Sequential()
network.add(layers.Dense(512, activation='relu', input_shape=(28 * 28,)))
network.add(layers.Dense(10, activation='softmax'))
'''
这个网络包含两个 Dense 层,每层都对输入数据进行一些简单的张量运算,
这些运算都包含权重张量。权重张量是该层的属性,里面保存了网络所学到的知识(knowledge)。
下面是网络的编译。
'''
network.compile(optimizer='rmsprop',loss='categorical_crossentropy',metrics=['accuracy'])
'''
categorical_crossentropy 是损失函数,是用于学习权重张量的反馈信号,在训练阶段应使它最小化。你还知道,减小损失是通过小批量随机梯度下降来实现的。
梯度下降的具体方法由第一个参数给定,即 rmsprop 优化器。最后,下面是训练循环。
'''
network.fit(train_images, train_labels, epochs=5, batch_size=128)
'''
在调用 fit 时发,网络开始在训练数据上进行迭代(每个小批量包含128 个样本),共迭代 5 次[在所有训练数据上迭代一次叫作一个轮次(epoch)]。在每次迭代
过程中,网络会计算批量损失相对于权重的梯度,并相应地更新权重。5 轮之后,网络进行了2345 次梯度更新(每轮 469 次),
网络损失值将变得足够小,使得网络能够以很高的精度对手写数字进行分类。
'''
学习是指找到一组模型参数,使得在给定的训练数据样本和对应目标值上的损失函数最小化。
学习的过程:随机选取包含数据样本及其目标值的批量,并计算批量损失相对于网络参数的梯度。随后将网络参数沿着梯度的反方向稍稍移动(移动距离由学习率指定)。
整个学习过程之所以能够实现,是因为神经网络是一系列可微分的张量运算,因此可以利用求导的链式法则来得到梯度函数,这个函数将当前参数和当前数据批量映射为一个梯度值。
两个关键的概念:损失和优化器。将数据输入网络之前,你需要先定义这二者。
损失是在训练过程中需要最小化的量,因此,它应该能够衡量当前任务是否已成功解决。
优化器是使用损失梯度更新参数的具体方式,比如 RMSProp 优化器、带动量的随机梯度下降(SGD)等。
Ps:本章内容来自对《Deep Learning with Python》学习过程的记录
文章评论
阿巴阿巴
阿巴阿巴