aspcms网站地图,给自己的网站做镜像网站,网站seo优化是什么意思,重庆网站建站第2章 神经网络的数学基础
2.1 初识神经网络 我们来看一个具体的神经网络示例#xff0c;使用 Python 的 Keras 库 来学习手写数字分类。 我们这里要解决的问题是#xff0c; 将手写数字的灰度图像#xff08;28 像素28 像素#xff09;划分到 10 个类别 中#xff08;0… 第2章 神经网络的数学基础
2.1 初识神经网络 我们来看一个具体的神经网络示例使用 Python 的 Keras 库 来学习手写数字分类。 我们这里要解决的问题是 将手写数字的灰度图像28 像素×28 像素划分到 10 个类别 中0~9 。我们将使用 MNIST 数据集 它是机器学习领域的一个经典数据集其历史几乎和这 个领域一样长而且已被人们深入研究。 MNIST 数据集包含 60 000 张训练图像 和 10 000 张测试图 像 由美国国家标准与技术研究院 National Institute of Standards and Technology 即 MNIST 中 的 NIST 在 20 世纪 80 年代收集得到。你可以将“解决” MNIST 问题看作深度学习的 “Hello World ”正是用它来验证你的算法是否按预期运行。 当你成为机器学习从业者后会发现 MNIST 一次又一次地出现在科学论文、博客文章等中。图 2-1 给出了 MNIST 数据集的一些样本。 关于类和标签的说明 在机器学习中 分类问题中的某个类别叫作类class 。 数据点叫作样本sample 。 某个样本对应的类叫作标签label 。 MNIST 数据集预先加载在 Keras 库中其中包括 4 个 Numpy 数组。 代码清单 2-1 加载 Keras 中的 MNIST 数据集 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_images 和 test_labels上对模型进行测试 。 图像被编码为 Numpy 数组 而标签是数字数组取值范围为 0~9 。图像和标签一一对应。 # 代码清单 2-1 加载keras中的minst数据集
from keras.datasets import mnist
from keras.utils import to_categorical(train_images, train_labels), (test_images, test_labels) mnist.load_data()print(train_images.shape:, train_images.shape)
print(len(train_labels), len(train_labels))print(train_labels, train_labels)print(test_images.shape:, test_images.shape)
print( len(test_labels):, len(test_labels))
print(test_labels:, test_labels)运行结果 train_images.shape: (60000, 28, 28)
len(train_labels) 60000
train_labels [5 0 4 ... 5 6 8]
test_images.shape: (10000, 28, 28)len(test_labels): 10000
test_labels: [7 2 1 ... 4 5 6] 接下来的工作流程如下 首先将训练数据train_images 和 train_labels输入神经网络 其次网络学习将图像和标签关联在一起 最后网络对 test_images 生成预测 而我们将验证这些预测与 test_labels 中的标签是否匹配。 # 代码清单 2-2 网络架构
from keras import models
from keras import layersnetwork models.Sequential()
# Dense 也就是全连接的神经网络
network.add(layers.Dense(512, activationrelu, input_shape(28 * 28,)))
network.add(layers.Dense(10, activationsoftmax)) 神经网络的核心组件是层layer 它是一种数据处理模块你可以将它看成数据过滤器。 进去一些数据出来的数据变得更加有用。具体来说层从输入数据中提取表示——我们期望 这种表示有助于解决手头的问题。大多数深度学习都是将简单的层链接起来从而实现渐进式的 数据蒸馏data distillation 。深度学习模型就像是数据处理的筛子包含一系列越来越精细的 数据过滤器即层。 本例中的网络包含 2 个 Dense 层 它们是 密集连接也叫全连接的神经层 。 第二层也是最后一层是一个 10 路 softmax 层 它将返回一个由 10 个概率值总和为 1 组成的数组。 每个概率值表示当前数字图像属于 10 个数字类别中某一个的概率。 要想训练网络我们还需要选择 编译compile步骤的三个参数 。 损失函数loss function 网络如何衡量在训练数据上的性能即网络如何朝着正确的方向前进。 优化器optimizer 基于训练数据和损失函数来更新网络的机制。 在训练和测试过程中需要监控的指标metric 本例只关心 精度 即正确分类的图像所占的比例。 后续两章会详细解释损失函数和优化器的确切用途。 # 2.3 编译步骤
# 编译的三个参数损失函数、优化器、监控的指标精度
network.compile(optimizerrmsprop,losscategorical_crossentropy,metrics[accuracy]) 在开始训练之前我们将对数据进行预处理 将其变换为网络要求的形状并缩放到所有值都在 [0, 1] 区间。比如之前训练图像保存在一个 uint8 类型 的数组中其形状为 (60000, 28, 28) 取值区间为 [0, 255] 。我们需要将其变换为一个 float32 数组其形状为 (60000, 28 * 28) 取值范围为 0~1 。 # 2-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 我们还需要对标签进行分类编码第 3 章将会对这一步骤进行解释。 # 2-5准备标签
train_labels to_categorical(train_labels)
test_labels to_categorical(test_labels)network.fit(train_images, train_labels, epochs5, batch_size128)test_loss, test_acc network.evaluate(test_images, test_labels)
print(test_acc:, test_acc) 现在我们准备开始训练网络在 Keras 中这一步是通过调用网络的 fit 方法来完成的 —— 我们在训练数据上 拟合 fit 模型。 469/469 [] - 1s 2ms/step - loss: 0.2649 - accuracy: 0.9234
Epoch 2/5
469/469 [] - 1s 2ms/step - loss: 0.1077 - accuracy: 0.9678
Epoch 3/5
469/469 [] - 1s 2ms/step - loss: 0.0712 - accuracy: 0.9790
Epoch 4/5
469/469 [] - 1s 2ms/step - loss: 0.0508 - accuracy: 0.9847
Epoch 5/5
469/469 [] - 1s 2ms/step - loss: 0.0380 - accuracy: 0.9885
313/313 [] - 0s 923us/step - loss: 0.0592 - accuracy: 0.9812
test_acc: 0.9811999797821045 训练过程中显示了两个数字一个是网络在训练数据上的 损失loss 另一个是网络在训练数据上的 精度acc 。 我们很快就在训练数据上达到了 0.989 98.9%的精度。现在我们来检查一下模型在测试集上的性能。 测试集精度为 97.8% 比训练集精度低不少。 训练精度和测试精度之间的这种差距是 过拟合overfit 造成的。过拟合是指机器学习模型在新数据上的性能往往比在训练数据上要差它是第 3 章的核心主题。 第一个例子到这里就结束了。你刚刚看到了如何构建和训练一个神经网络用不到 20 行的 Python 代码对手写数字进行分类。 下一章会详细介绍这个例子中的每一个步骤并讲解其背后的原理。接下来你将要学到张量输入网络的数据存储对象、张量运算层的组成要素和梯度下降可以让网络从训练样本中进行学习。 2.2 神经网络的数据表示
前面例子使用的数据存储在多维 Numpy 数组中也叫张量tensor。一般来说当前所有机器学习系统都使用张量作为基本数据结构。张量对这个领域非常重要重要到 Google 的TensorFlow 都以它来命名。
张量这一概念的核心在于它是一个数据容器。它包含的数据几乎总是数值数据因此它是数字的容器。你可能对矩阵很熟悉它是二维张量。张量是矩阵向任意维度的推广注意张量的维度dimension通常叫作轴axis。
2.2.1 标量0D 张量
仅包含一个数字的张量叫作标量scalar也叫标量张量、零维张量、0D 张量。在 Numpy中一个 float32 或 float64 的数字就是一个标量张量或标量数组。你可以用 ndim 属性来查看一个 Numpy 张量的轴的个数。标量张量有 0 个轴ndim 0。张量轴的个数也叫作阶rank。下面是一个 Numpy 标量。 import numpy as npx np.array(12)x
array(12)x.ndim
0
2.2.2 向量1D 张量
数字组成的数组叫作向量vector或一维张量1D 张量。一维张量只有一个轴。下面是
一个 Numpy 向量。 x np.array([12, 3, 6, 14, 7])x
array([12, 3, 6, 14, 7])x.ndim
1
这个向量有 5 个元素所以被称为 5D 向量。
不要把 5D 向量和 5D 张量弄混
5D 向量只有一个轴沿着轴有 5 个维度而 5D 张量有 5 个轴沿着每个轴可能有任意个维度。
维度dimensionality可以表示沿着某个轴上的元素个数比如 5D 向量也可以表示张量中轴的个数比如 5D 张量这有时会令人感到混乱。
对于后一种情况技术上更准确的说法是 5 阶张量张量的阶数即轴的个数但 5D 张量这种模糊的写法更常见。
2.2.3 矩阵2D 张量
向量组成的数组叫作矩阵matrix或二维张量2D 张量。
矩阵有 2 个轴通常叫作行和列。你可以将矩阵直观地理解为数字组成的矩形网格。 x np.array([[5, 78, 2, 34, 0],[6, 79, 3, 35, 1],[7, 80, 4, 36, 2]])x.ndim
2
第一个轴上的元素叫作行row第二个轴上的元素叫作列column。
在上面的例子中[5, 78, 2, 34, 0] 是 x 的第一行[5, 6, 7] 是第一列。
2.2.4 3D 张量与更高维张量
将多个矩阵组合成一个新的数组可以得到一个 3D 张量你可以将其直观地理解为数字组成的立方体。下面是一个 Numpy 的 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]]])x.ndim
3
将多个 3D 张量组合成一个数组可以创建一个 4D 张量以此类推。深度学习处理的一般是 0D 到 4D 的张量但处理视频数据时可能会遇到 5D 张量。
2.2.5 关键属性
张量是由以下三个关键属性来定义的。
轴的个数阶。例如3D 张量有 3 个轴矩阵有 2 个轴。这在 Numpy 等 Python 库中也叫张量的 ndim。
形状。这是一个整数元组表示张量沿每个轴的维度大小元素个数。例如前面矩阵示例的形状为 (3, 5)3D 张量示例的形状为 (3, 3, 5)。向量的形状只包含一个元素比如 (5,)而标量的形状为空即 ()。
数据类型在 Python 库中通常叫作 dtype。这是张量中所包含数据的类型例如张量的类型可以是 float32、uint8、float64 等。
在极少数情况下你可能会遇到字符char张量。注意Numpy以及大多数其他库中不存在字符串张量因为张量存储在预先分配的连续内存段中而字符串的长度是可变的无法用这种方式存储。
为了具体说明我们回头看一下 MNIST 例子中处理的数据。首先加载 MNIST 数据集。
from keras.datasets import mnist(train_images, train_labels), (test_images, test_labels) mnist.load_data()print(train_images.ndim:,train_images.ndim)
print(train_images.shape:,train_images.shape)
print(train_images.dtype:,train_images.dtype) train_images.ndim: 3
train_images.shape: (60000, 28, 28)
train_images.dtype: uint8 所以这里 train_images 是一个由 8 位整数组成的 3D 张量。更确切地说它是 60 000个矩阵组成的数组每个矩阵由 28×28 个整数组成。每个这样的矩阵都是一张灰度图像元素取值范围为 0~255。 我们用 Matplotlib 库 Python 标准科学套件的一部分来显示这个 3D 张量中的第 4 个数字 如图 2-2 所示。 #2-8 显示第 4 个数字
digit train_images[4]
import matplotlib.pyplot as plt
plt.imshow(digit, cmapplt.cm.binary)
plt.show() train_images[4]就是train数据库中的第5张图片从0开始计数 2.2.6 在 Numpy 中操作张量 在前面的例子中我们使用语法 train_images[i] 来 选择 沿着第一个轴的特定数字。 选择张量的特定元素叫作张量切片tensor slicing 。 我们来看一下 Numpy 数组上的张量切片运算。 下面这个例子选择第 10~100 个数字不包括第 100 个并将其放在形状为 (90, 28, 28) 的数组中。 my_slice train_images[10:100]
print(my_slice.shape,my_slice.shape) 运行结果 my_slice.shape (90, 28, 28) 它等同于下面这个更复杂的写法给出了切片沿着每个张量轴的起始索引和结束索引。 注意 : 等同于选择整个轴。 my_slice train_images[10:100, :, :]
print(my_slice.shape2:,my_slice.shape)my_slice train_images[10:100, 0:28, 0:28]
print(my_slice.shape3:,my_slice.shape) my_slice.shape2: (90, 28, 28)
my_slice.shape3: (90, 28, 28) 一般来说你 可以沿着每个张量轴在任意两个索引之间进行选择 。例如你可以在所有图 像的右下角选出 14 像素× 14 像素的区域 my_slice train_images[:, 14:, 14:] 也可以使用负数索引。与 Python 列表中的负数索引类似它表示与当前轴终点的相对位置。 你可以在图像中心裁剪出 14 像素× 14 像素的区域 my_slice train_images[:, 7:-7, 7:-7] my_slice train_images[:, 14:, 14:]
print(my_slice.shape4:,my_slice.shape)my_slice train_images[:, 7:-7, 7:-7]
print(my_slice.shape5:,my_slice.shape) my_slice.shape4: (60000, 14, 14)
my_slice.shape5: (60000, 14, 14) 2.2.7 数据批量的概念 通常来说 深度学习中所有数据张量的第一个轴0 轴因为索引从 0 开始都是样本轴 samples axis 有时也叫 样本维度 。 在 MNIST 的例子中样本就是数字图像。 此外 深度学习模型不会同时处理整个数据集而是将数据拆分成小批量 。 具体来看下面是 MNIST 数据集的一个批量批量大小为 128 。 batch train_images[:128] 然后是下一个批量。 batch train_images[128:256] 然后是第 n 个批量。 batch train_images[128 * n:128 * (n 1)] 对于这种批量张量第一个轴0 轴叫作 批量轴batch axis 或 批量维度batch dimension 。 在使用 Keras 和其他深度学习库时你会经常遇到这个术语。 2.2.8 现实世界中的数据张量 我们用几个你未来会遇到的示例来具体介绍数据张量。你需要处理的数据几乎总是以下类别之一。 向量数据 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.2.9 向量数据 这是最常见的数据。对于这种数据集每个数据点都被编码为一个向量因此一个数据批量就被编码为 2D 张量即向量组成的数组其中第一个轴是 样本轴 第二个轴是 特征轴 。 2D 张量形状为 (samples, features) 我们来看两个例子。 人口统计数据集 其中包括每个人的年龄、邮编和收入。 每个人可以表示为包含 3 个值的向量而整个数据集包含 100 000 个人因此可以存储在形状为 (100000, 3) 的 2D 张量中。 文本文档数据集 我们将每个文档表示为每个单词在其中出现的次数字典中包含20 000 个常见单词。每个文档可以被编码为包含 20 000 个值的向量每个值对应于字典中每个单词的出现次数整个数据集包含 500 个文档因此可以存储在形状为 (500, 20000) 的张量中。 2.2.10 时间序列数据或序列数据 当时间或序列顺序对于数据很重要时应该将数据存储在带有时间轴的 3D 张量中。 每个样本可以被编码为一个向量序列即 2D 张量因此一个数据批量就被编码为一个 3D 张 量见图 2-3 。 3D 张量形状为 (samples, timesteps, features)。 根据惯例时间轴始终是第 2 个轴索引为 1 的轴 。我们来看几个例子。 股票价格数据集。 每一分钟我们将股票的当前价格、前一分钟的最高价格和前一分钟 的最低价格保存下来。因此每分钟被编码为一个 3D 向量整个交易日被编码为一个形 状为 (390, 3) 的 2D 张量一个交易日有 390 分钟而 250 天的数据则可以保存在一 个形状为 (250, 390, 3) 的 3D 张量中。这里每个样本是一天的股票数据。 推文数据集。 我们将每条推文编码为 280 个字符组成的序列而每个字符又来自于 128 个字符组成的字母表。在这种情况下每个字符可以被编码为 大小为 128 的二进制向量 只有在该字符对应的索引位置取值为 1 其他元素都为 0。那么每条推文可以被编码为一个形状为 (280, 128) 的 2D 张量而包含 100 万条推文的数据集则可以存储在一 个形状为 (1000000, 280, 128) 的张量中。 2.2.11 图像数据 图像通常具有三个维度 高度、宽度和颜色深度 。 虽然灰度图像比如 MNIST 数字图像 只有一个颜色通道 因此可以保存在 2D 张量中但按照惯例图像张量始终都是 3D 张量 灰度图像的彩色通道只有一维 。 因此如果图像大小为 256 × 256 那么 128 张 灰度图像组成的批量可以保存在一个形状为 ( 128 , 256, 256, 1 ) 的张量中而 128 张彩色图像组成的批量则可以保存在一个形状为 (128, 256, 256, 3 ) 的张量中见图 2-4 。 图像张量的形状有两种约定 通道在后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 框架同时支持这两种格式。 2.2.12 视频数据 视频数据是现实生活中需要用到 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 格式。 2.3 神经网络的“齿轮”张量运算 所有计算机程序最终都可以简化为二进制输入上的一些二进制运算 AND、OR、NOR 等 与此类似深度神经网络学到的所有变换也都可以简化为数值数据张量上的一些张量运算tensor operation 例如加上张量、乘以张量等。 在最开始的例子中我们通过叠加 Dense 层来构建网络。 Keras 层的实例如下所示。 keras.layers.Dense(512, activationrelu) 这个层可以理解为一个函数输入一个 2D 张量返回另一个 2D 张量即输入张量的新表示。具体而言这个函数如下所示其中 W 是一个 2D 张量 b 是一个向量二者都是该层的 属性。 output relu(dot(W, input) b) 我们将上式拆开来看。这里有三个张量运算输入张量和张量 W 之间的 点积运算dot 、 得到的 2D 张量与向量 b 之间的 加法运算 、最后的 relu 运算 。 relu(x) 是 max(x, 0) 。 注意 虽然本节的内容都是关于线性代数表达式但你却找不到任何数学符号。我发现对于 没有数学背景的程序员来说如果用简短的 Python 代码而不是数学方程来表达数学概念 他们将更容易掌握。所以我们自始至终将会使用 Numpy 代码。 2.3.1 逐元素运算 relu 运算和加法都是 逐元素 element-wise 的运算即该运算独立地应用于张量中的每 个元素也就是说这些运算非常适合大规模并行实现 向量化 实现这一术语来自于 1970 — 1990 年间 向量处理器 超级计算机架构。如果你想对逐元素运算编写简单的 Python 实现那么 可以用 for 循环。下列代码是对逐元素 relu 运算的简单实现。 def naive_relu(x):# x 是一个 Numpy 的 2D 张量assert len(x.shape) 2# 避免覆盖输入张量x x.copy()# x 是一个 Numpy 的 2D 张量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) 2assert x.shape y.shapex 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 广播 上一节 naive_add 的简单实现仅支持两个形状相同的 2D 张量相加。但在前面介绍的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 次是一种很有用的思维模型。下面是一种简单的实现。 def naive_add_matrix_and_vector(x, y):# x 是一个 Numpy 的 2D 张量assert len(x.shape) 2# y 是一个 Numpy 向量assert len(y.shape) 1assert 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, n1, ... m) 另一个张量的形状是 (n, n1, ... 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 的形状是 (64, 3, 32, 10)与 x 相同
z np.maximum(x, y) 2.3.3 张量点积 点积运算dot product也叫张量积 tensor product不要与逐元素的乘积弄混是最常见也最有用的张量运算。与逐元素的运算不同它将输入张量的元素合并在一起。 在 Numpy 、 Keras 、 Theano 和 TensorFlow 中都是用 * 实现逐元素乘积。 TensorFlow 中的 点积使用了不同的语法 但在 Numpy 和 Keras 中都是用标准的 dot 运算符来实现点积。 import numpy as np
z np.dot(x, y) 数学符号中的点 . 表示点积运算。 zx.y 从数学的角度来看点积运算做了什么我们首先看一下两个向量 x 和 y 的点积。其计算 过程如下。 def naive_vector_dot(x, y):# x 和 y 都是 Numpy 向量assert len(x.shape) 1assert len(y.shape) 1assert 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):# 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]# 这个运算返回一个全是 0 的向量# 其形状与 x.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):# x 和 y 都 是Numpy矩阵,二维张量assert len(x.shape) 2assert len(y.shape) 2# x 的第 1 维和 y 的第 0 维大小必须相同assert x.shape[1] y.shape[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 为了便于理解点积的形状匹配可以将输入张量和输出张量像图 2-5 中那样排列利用可 视化来帮助理解。 图 2-5 中 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) 以此类推。 2.3.4 张量变形 第三个重要的张量运算是 张量变形tensor reshaping 。虽然前面神经网络第一个例子的 Dense 层中没有用到它但在 将图像数据输入神经网络之前 我们在预处理时用到了这个运算。 train_images train_images.reshape((60000, 28 * 28)) 张量变形是指改变张量的行和列以得到想要的形状 。变形后的张量的元素总个数与初始张量相同。简单的例子可以帮助我们理解张量变形。 import numpy as npx np.array([[0., 1.],[2., 3.],[4., 5.]])
print(x, x)
print(x.shape)x x.reshape((6, 1))
print(x, x)x x.reshape((2, 3))
print(x, x) x [[0. 1.][2. 3.][4. 5.]]
(3, 2)
x [[0.][1.][2.][3.][4.][5.]]
x [[0. 1. 2.][3. 4. 5.]] 经常遇到的一种特殊的张量变形是 转置transposition 。对矩阵做 转置 是指将行和列互换 使 x[i, :] 变为 x[:, i] 。 x np.zeros((3, 4))
x np.transpose(x)
print(x.shape) (4, 3) 2.3.5 张量运算的几何解释 对于张量运算所操作的张量其元素可以被解释为某种几何空间内点的坐标因此 所有的张量运算都有几何解释 。举个例子我们来看加法。首先有这样一个向量 A [0.5, 1] 它是二维空间中的一个点见图 2-6 。常见的做法是将向量描绘成原点到这个点的箭头 如图 2-7 所示。 假设又有一个点 B [1, 0.25] 将它与前面的 A 相加。从几何上来看这相当于将两个向量箭头连在一起得到的位置表示两个向量之和对应的向量见图 2-8 。 通常来说 仿射变换、旋转、缩放 等基本的几何操作都可以表示为张量运算。 举个例子要将一个二维向量旋转 theta 角可以通过与一个 2 × 2 矩阵做点积来实现这个矩阵为 R [u, v]其中 u 和 v 都是平面向量 u [cos(theta), sin(theta)] v [-sin(theta), cos(theta)] 。 2.3.6 深度学习的几何解释 前面讲过神经网络完全由一系列张量运算组成而这些张量运算都只是输入数据的几何变换。 因此你可以将神经网络解释为高维空间中非常复杂的几何变换这种变换可以通过许多简单的步骤来实现。 对于三维的情况下面这个思维图像是很有用的。想象有两张彩纸一张红色一张蓝色。其中一张纸放在另一张上。现在将两张纸一起揉成小球。这个皱巴巴的纸球就是你的输入数据每张纸对应于分类问题中的一个类别。 神经网络或者任何机器学习模型要做的就是找 到可以让纸球恢复平整的变换从而能够再次让两个类别明确可分 。通过深度学习这一过程 可以用三维空间中一系列简单的变换来实现比如你用手指对纸球做的变换每次做一个动作 如图 2-9 所示。 让纸球恢复平整就是机器学习的内容为复杂的、高度折叠的数据流形找到简洁的表示 。 现在你应该能够很好地理解为什么深度学习特别擅长这一点它将复杂的几何变换逐步分解为一长串基本的几何变换这与人类展开纸球所采取的策略大致相同。深度网络的每一层都通过变换使数据解开一点点——许多层堆叠在一起可以实现非常复杂的解开过程。 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 之间的距离非常小。网络“学会”将输入映射到正确的目标。乍一看可能像魔法一样但如果你将其简化为基本步骤那么会变得非常简单。 第一步看起来非常简单只是输入 / 输出I/O的代码。第二步和第三步仅仅是一些张量运算的应用所以你完全可以利用上一节学到的知识来实现这两步。 难点在于第四步更新网络的权重 。考虑网络中某个权重系数你怎么知道这个系数应该增大还是减小以及变化多少 一种简单的解决方案是保持网络中其他权重不变只考虑某个标量系数让其尝试不同的取值。假设这个系数的初始值为 0.3。对一批数据做完前向传播后网络在这批数据上的损失是 0.5 。如果你将这个系数的值改为 0.35 并重新运行前向传播损失会增大到 0.6。但如果你将这个系数减小到 0.25 损失会减小到 0.4 。在这个例子中将这个系数减小 0.05 似乎有助于使损失最小化。对于网络中的所有系数都要重复这一过程。 但这种方法是非常低效的因为对每个系数系数很多通常有上千个有时甚至多达上百万个都需要计算两次前向传播计算代价很大。 一种更好的方法 是利用网络中所有运算都是 可微differentiable 的这一事实计算损失相对于网络系数的 梯度gradient 然后向梯度的反方向改变系数从而使损失降低。 如果你已经了解 可微 和 梯度 这两个概念可以直接跳到 2.4.3 节。如果不了解下面两小节有助于你理解这些概念。 2.4.1 什么是导数 设有一个连续的光滑函数 f(x) y 将实数 x 映射为另一个实数 y 。由于函数是 连续的微小变化只能导致 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 沿着导数的反方向移动一小步。 2.4.2 张量运算的导数梯度 梯度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 太远。 2.4.3 随机梯度下降 给定一个可微函数理论上可以用解析法找到它的最小值函数的最小值是导数为 0 的点 因此你只需找到所有导数为 0 的点然后计算函数在其中哪个点具有最小值。 将这一方法应用于神经网络就是用解析法求出最小损失函数对应的所有权重值。可以通 过对方程 gradient(f)(W) 0 求解 W 来实现这一方法。这是包含 N 个变量的多项式方程 其中 N 是网络中系数的个数。 N2 或 N3 时可以对这样的方程求解但对于实际的神经网络是 无法求解的因为参数的个数不会少于几千个而且经常有上千万个。 相反你可以使用 2.4 节开头总结的四步算法基于当前在随机数据批量上的损失一点一点地对参数进行调节。由于处理的是一个可微函数你可以计算出它的梯度从而有效地实现第四步。沿着梯度的反方向更新权重损失每次都会变小一点。 (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 。图 2-11 给出了一维的情况网络只有一个参数并且只有一个训练样本。 如你所见直观上来看为 step 因子选取合适的值是很重要的。如果取值太小则沿着 曲线的下降需要很多次迭代而且可能会陷入局部极小点。如果取值太大则更新权重值之后 可能会出现在曲线上完全随机的位置。 注意小批量 SGD 算法的一个变体是每次迭代时只抽取一个样本和目标而不是抽取一批 数据。这叫作 真 SGD 有别于 小批量 SGD 。还有另一种极端每一次迭代都在 所有 数据上 运行这叫作 批量 SGD 。这样做的话每次更新都更加准确但计算代价也高得多。这两个极 端之间的有效折中则是选择合理的批量大小。 图 2-11 描述的是一维参数空间中的梯度下降但在实践中需要在高维空间中使用梯度下降。神经网络的每一个权重参数都是空间中的一个自由维度网络中可能包含数万个甚至上百万个参数维度。为了让你对损失曲面有更直观的认识你还可以将梯度下降沿着二维损失曲面可视化 如图 2-12 所示 。但你不可能将神经网络的实际训练过程可视化因为你无法用人类可以理解的 方式来可视化 1 000 000 维空间。因此最好记住在这些低维表示中形成的直觉在实践中不一定 总是准确的。这在历史上一直是深度学习研究的问题来源。 2.4.4 链式求导反向传播算法 在前面的算法中我们假设函数是可微的因此可以明确计算其导数。在实践中神经网 络函数包含许多连接在一起的张量运算每个运算都有简单的、已知的导数。例如下面这个 网络 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 。反向传播从最终损失值开始从最顶层反向作用至最底层利用链式法则计算每个参数对损失值的贡献大小。 2.5 回顾第一个例子 你已经读到了本章最后一节现在应该对神经网络背后的原理有了大致的了解。我们回头 看一下第一个例子并根据前面三节学到的内容来重新阅读这个例子中的每一段代码。 下面是输入数据。 (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, activationrelu, input_shape(28 * 28,)))
network.add(layers.Dense(10, activationsoftmax)) 现在你明白了这个网络包含 两个 Dense 层 每层都对输入数据进行一些简单的张量运算这些运算都包含权重张量。权重张量是该层的属性里面保存了网络所学到的 知识 knowledge 。 下面是网络的编译。 network.compile(optimizerrmsprop,losscategorical_crossentropy,metrics[accuracy]) 现在你明白了 categorical_crossentropy 是损失函数 是用于学习权重张量的反馈信号在训练阶段应使它最小化。你还知道减小损失是通过小批量随机梯度下降来实现的。 梯度下降的具体方法由第一个参数给定即 rmsprop 优化器 。 最后下面是训练循环。 network.fit(train_images, train_labels, epochs5, batch_size128) 现在你明白在调用 fit 时发生了什么网络开始在训练数据上进行迭代 每个小批量包含128 个样本 共 迭代 5 次 在所有训练数据上迭代一次叫作一个 轮次 epoch。在每次迭代过程中网络会计算批量损失相对于权重的梯度并相应地更新权重。 5 轮之后网络进行了2345 次梯度更新每轮 469 次网络损失值将变得足够小使得网络能够以很高的精度对手 写数字进行分类。 到目前为止你已经了解了神经网络的大部分知识。 本章小结 学习是指找到一组模型参数使得在给定的训练数据样本和对应目标值上的损失函数最小化 。 学习的过程随机选取包含数据样本及其目标值的批量并计算批量损失相对于网络参 数的梯度。随后将网络参数沿着梯度的反方向稍稍移动移动距离由学习率指定。 整个学习过程之所以能够实现是因为神经网络是一系列可微分的张量运算因此可以利用求导的链式法则来得到梯度函数这个函数将当前参数和当前数据批量映射为一个梯度值。 后续几章你会经常遇到两个关键的概念 损失和优化器 。将数据输入网络之前你需要先定义这二者。 损失是在训练过程中需要最小化的量因此它应该能够衡量当前任务是否已成功解决。 优化器是使用损失梯度更新参数的具体方式 比如 RMSProp 优化器 、带动量的随机梯度下降 SGD 等。