1、什么是深度学习
1.1、机器学习
图1:计算机有效工作的常用方法:程序员编写规则(程序),计算机遵循这些规则将输入数据转换为适当的答案。这一方法被称为符号主义人工智能,适合用来解决定义明确的逻辑问题,比如早期的PC小游戏:五子棋等,但是像图像分类、语音识别或自然语言翻译等更复杂、更模糊的任务,难以给出明确的规则。
图2:机器学习把这个过程反了过来:机器读取输入数据和相应的答案,然后找出应有的规则。机器学习系统是训练出来的,而不是明确的用程序编写出来。举个例子,如果你想为度假照片添加标签,并希望将这项任务自动化,那么你可以将许多人工打好标签的照片输人机器学习系统,系统将学会把特定照片与特定标签联系在一起的统计规则。
定义:机器学习就是在预定义的可能性空间中,利用反馈信号的指引,在输入数据中寻找有用的表示和规则。
1.2、深度学习
深度学习是机器学习的一个分支领域,强调从一系列连续的表示层中学习。现代的深度学习模型通常包含数十个甚至上百个连续的表示层,它们都是从训练数据中自动学习而来。与之对应,机器学习有时也被称为浅层学习。
在深度学习中,这些分层表示是通过叫作神经网络的模型学习得到的。深度神经网络可以看作多级信息蒸馏过程:信息穿过连续的过滤器,其纯度越来越高。
技术定义:一种多层的学习数据表示的方法。
1.3、深度学习工作原理
a. 对神经网络的权重(有时也被称为该层的参数)进行随机赋值
b. 经过一系列随机变换,得到预测值Y'
c. 通过损失函数(有时也被称为目标函数或代价函数),得到预测值Y'与真实值Y之间的损失值
d. 将损失值作为反馈信号,通过优化器来对权重值进行微调,以降低当前示例对应的损失值
e. 循环重复足够做的次数(b-d),得到具有最小损失值的神经网络,就是一个训练好的神经网络
2、神经网络数学基础
2.1、神经网络的数据表示
目前所有机器学习系统都使用张量(tensor)作为基本数据结构,张量对这个领域非常重要,TensorFlow就是以它来命名。
张量这一概念的核心在于,它是一个数据容器。它包含的数据通常是数值数据,因此它是一个数字容器。你可能对矩阵很熟悉,它是2阶张量。张量是矩阵向任意维度的推广,张量的维度通常叫做轴。
张量是由以下3个关键属性来定义的。
•轴:轴的个数
•形状:表示张量沿每个轴的维度大小(元素个数)
•数据类型(dtype):数据的类型,可以是float16、float32、float64、unit8、string等
2.1.1、标量(0阶张量)
仅包含一个数字的张量叫做标量(SCALAR),也叫0阶张量或0维张量。
下面是一个NumPy标量
import numpy as np
x = np.array(3)
x.ndim // 轴:0, 形状:()
2.1.2、向量(1阶张量)
数字组成的数组叫做向量(VECTOR),也叫1阶张量或1维张量。
下面是一个NumPy向量
x = np.array([4, 1, 5])
x.ndim // 轴:1, 形状:(3,)
这个向量包含3个元素,所以也叫3维向量。不要把3维向量和3维张量混为一谈,3维向量只有一个轴,沿着这个轴有3个维度。
2.1.3、矩阵(2阶张量)
向量组成的数组叫做矩阵(MATRIX),也2阶张量或2维张量。矩阵有2个轴:行和列。
下面是一个NumPy矩阵
x = np.array([
[4, 6, 7],
[7, 3, 9],
[1, 2, 5]
])
x.ndim // 轴:2, 形状:(3, 3)
现实世界中的向量实例:
向量数据:形状为(samples, features)的2阶张量,每个样本都是一个数值(特征)向量,向量数据库存储的基本单位。
2.1.4、3阶张量与更高阶的张量
将多个矩阵打包成一个新的数组,就可以得到一个3阶张量(或3维张量)
下面是一个3阶NumPy张量
x = np.array([
[[4, 6, 7],
[7, 3, 9],
[1, 2, 5]],
[[5, 7, 1],
[9, 4, 3],
[3, 5, 2]]
])
x.ndim // 轴:3, 形状:(2, 3, 3)
将多个3阶张量打包成一个数组,就可以创建一个4阶张量。
现实世界中的实例:
时间序列数据或序列数据:形状为(samples, timesteps, features)的3阶张量,每个样本都是特征向量组成的序列(序列长度为timesteps)
图像数据:形状为(samples, height, width, channels)的4阶张量,每个样本都是一个二维像素网格,每个像素则由一个“通道”(channel)向量表示。
视频数据:形状为(samples, frames, height, width, channels)的5阶张量,每个样本都是由图像组成的序列(序列长度为frames)。
2.2、神经网络的“齿轮”:张量运算
所有计算机程序最终都可以简化为对二进制输入的一些二进制运算,与此类似,深度神经网络学到的所有变换也都可以简化为对数值数据张量的一些张量运算或张量函数。
2.2.1、逐元素运算
逐元素运算,即该运算分别应用于张量的每个元素。参与运算的张量的形状必须相同。
import numpy as np
z = x + y // 逐元素加法
z = x - y // 逐元素加法
z = x * y // 逐元素乘积
z = x / y // 逐元素除法
z = np.maximum(z, 0.) //逐元素relu,大于0输出等于输入,小于0则输出为0
rule运算是一种常用的激活函数,rule(x)就是max(x, 0):如果输入x大于0,则输出等于输入值;如果输入x小于等于0,则输出为0。
2.2.2、张量积
张量积或点积是最常见且最有用的张量运算之一。注意,不要将其与逐元素乘积弄混。
在NumPy中使用np.dot函数来实现张量积:z = np.dot(x, y)
数学符号中的(·)表示点积运算:z = x · y
•两个向量的点积是一个标量,而且只有元素个数相同的向量才能进行点积运算。
•一个矩阵x和一个向量y做点积运算,其返回值是一个向量,其中每个元素是y和x每一行的点积。
•对于矩阵x和y,当且仅当x.shape[1] == y.shape[0]时,才可以计算点积,其结果是一个形状为(x.shape[0], y.shape[1])的矩阵,其元素是x的行与y的列之间的向量点积。
2.2.3、张量变形
张量变形是指重新排列张量的行和列,以得到想要的形状。变形后,张量的元素个数与初始张量相同。
import numpy as np
x = np.array([[0, 1],
[2, 3]
[4, 5]])
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]])
常见的一种特殊的张量变形是转置。矩阵转置是指将矩阵的行和列互换,即x[i, :]变为x[:, i]
x = np.zeros((300, 20)) //创建一个形状为(300, 20)的零矩阵
x = np.transpose(x)
>>> x.shape
(20, 300)
2.2.4、张量运算的几何解释
平移、旋转、缩放、倾斜等基本的几何操作都可以表示为张量运算。
•线性变换:与任意矩阵做点积运算,都可以实现一次线性变换。缩放和旋转,都属于线性变换。
•仿射变换:一次线性变换与一次平移的组合。
•带有rule激活函数的仿射变换:多次仿射变换相当于一次仿射变换,因此一个完全没有激活函数的多层神经网络等同于一层,这种“深度”神经网络其实就是一个线性模型。
2.2.5、深度学习的几何解释
神经网络完全由一系列张量运算组成,而这些张量运算只是输入数据的简单几何变换。因此,你可以将神经网络解释为高维空间中非常复杂的几何变换,这种变换通过一系列简单步骤来实现。
机器学习的目的:为高维空间中复杂、高度折叠的数据流行(一个连续的表面)找到简洁的表示。深度学习可以将复杂的几何变换逐步分解为一系列基本变换。
2.3、神经网络的“引擎”:基于梯度的优化
回顾1.3章节【深度学习工作原理】,步骤a看起来很简单,只是输入/输出(I/O)的代码。步骤b、c仅仅是应用了一些张量运算。难点在于步骤d:更新模型权重。对于模型的某个权重系数,你怎么知道这个系数应该增大还是减小,以及变化多少?
一种简单的解决方案是,保持模型的其他权重不变,只考虑一个标量系数,让其尝试不同的取值。对于模型的所有系数都要重复这一过程。但这种方法非常低效,因为系数有很多(通常有上千个,甚至多达百万个)。幸运的是,有一种更好的方法:梯度下降法。
2.3.1、导数
假设有一个光滑连续的函数f(x) = y,由于函数是连续的,因此x的微小变化只会导致y的微小变化。因此在某个点p附近,如果x变化足够小,就可以将f近似看作斜率为a的线性函数。
斜率a被称为f在p点的导数。如果a < 0,说明x在p点附近的微增将导致f(x)减小;如果a > 0,那么x在p点附近的微增将导致f(x)增大;
2.3.2、梯度
导数这一概念可以应用于任何函数,只要函数所对应的表面是连续且光滑的。张量运算的导数叫做梯度。对于一个标量函数来说,导数是表示函数曲线的局部斜率,张量函数的梯度表示该函数所对应多维表面的曲率。
举例来说,物体位置相对于时间的梯度是这个物体的速度,二阶梯度则是它的加速度。
2.3.3、随机梯度下降
步骤d中更新模型权重,假设我们要处理的是一个可微函数,可以计算出它的梯度,沿着梯度的反方向更新权重,每次损失都会减小一点。
(1)抽取训练样本x和对应目标y_true组成的一个数据批量
(2)在x上运行模型,得到预测值y_pred(前向传播)
(3)计算模型在这批数据上的损失值
(4)计算损失相对于模型参数的梯度(反向传播)
(5)将参数沿着梯度的反方向移动一小步,从而减小损失值
这个方法叫做小批量随机梯度下降(SGD),随机是指每批数据都是随机抽取的;如果每次迭代都在所有数据上运行,这叫做批量梯度下降,但是计算成本高得多,折中办法是选择合理的小批量大小。
神经网络的每一个权重系数都是空间中的一个自由维度,为了对损失表面有更直观的认识,可以将沿着二维损失表面的梯度下降可视化,但你不可能将神经网络的真实训练过程可视化,因为无法用人类可以理解的方式来可视化1 000 000维空间。这些低维表示中建立的直觉,实践中不一定总是准确的。
2.3.4、链式求导:反向传播
在前面的算法中,我们假设函数是可微(可以被求导)的,所以很容易计算其梯度。但是在实践中如何计算复杂表达式的梯度?这时就需要用到反向传播算法。
(1)链式法则
利用简单运算(如加法、rule或张量积)的导数,可以轻松计算出这些基本运算的任意复杂组合的梯度。链式法则规定:grad(y, x) == grad(y, x1) * grad(x1, x),因此只要知道f和g的导数,就可以求出fg的导数。如果添加更多的中间函数,看起来就像是一条链。将链式法则应用于神经网络梯度值的计算,就得到了一种叫做反向传播的算法。
(2)用计算图进行自动微分
思考反向传播的一种有用方法是利用计算图。计算图是TensorFlow和深度学习革命的核心数据结构。它是一种由运算构成的有向无环图。如今,现代框架比如TensorFlow,支持基于计算图的自动微分,可以计算任意可维张量运算组合的梯度,只需写出前向传播,而无需做任何额外工作。
GradientTape是一个API,让你可以充分利用TensorFlow强大的自动微分能力。它是一个Python作用域,能够以计算图(tape)的形式记录在其中运行的张量运算。
3、实践:使用Python的Kears库识别手写数字
在这个例子中,我们要解决的问题是,将手写数字的灰度图像(28像素 * 28像素)划分到10个类别(从0到9)中,我们将使用MNIST数据集,它是机器学习领域的一个经典数据集。你可以将解决MNIST问题看作深度学习的“Hello World”。
3.1 加载Kears中的MNIST数据集
from tensorflow.keras.datasets import mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images, train_labels组成了训练集,模型将从这些数据中进行学习。我们会在测试集test_images, test_labels上对模型进行测试。
查看数据集形状:
>>> train_images.shape
(60000, 28, 28) //训练集为60000张图片,每张图片中28*28像素点数据
>>> test_images.shape
(10000, 28, 28) //测试集为10000张图片,每张图片中28*28像素点数据
3.2 神经网络架构模型
from tensorflow import keras
from tensorflow.keras import layers
model = keras.Sequential([
layers.Dense(512, activation="relu"),
layers.Dense(10, activation="softmax")
])
神经网络的核心组件是层(layer),大多数深度学习工作设计将简单的层链接起来,从而实现渐进式的数据蒸馏,从输入数据中提取表示。
本例中的模型包含2个Dense层,每层都对输入数据做一些简单的张量运算(relu、softmax),这些运算都涉及权重张量,权重张量是该层的属性或参数,里面保存了模型所学到的知识。
3.3 模型编译
model.compile(
optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"]
)
这里指定了编译的3个步骤:优化器、损失函数以及监控的指标。其中sparse_categorical_crossentropy是损失函数,用于学习权重张量的反馈信号;使用rmsprop优化器,通过小批量随机梯度下降(SGD)降低损失值。
3.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]区间。
3.5 拟合模型
model.fit(train_images, train_labels, epochs=5, batch_size=128)
在Keras中通过调用模型的fit方法来完成训练数据上的拟合模型:模型开始在训练数据上进行迭代(每个小批量包含128个样本),共迭代5轮。对于每批数据,模型会计算损失相对于权重的梯度,并将权重沿着减小该批量对应损失值的方向移动,5轮迭代后训练精度到达了98.9%。
3.6 利用模型预测
>>> test_digits = test_images[0:10]
>>> predictions = model.predict(test_digits)
>>> predictions[0]
//为了方面阅读,以下数据均为示例
array([1.07, 1.69, 6.13, 8.41, 2.99, 3.03, 8.36, 9.99, 2.66, 3.81], dtype=float32)
这个数组中的每个值,为对应数字图像test_digits[0]属于0-9类别的概率,可以看到第7个概率最大,因此这个数字一定是7。检查测试标签是否与之一致:
>>> test_lables[0]
7
3.7 在新数据上评估模型
>>> test_loss, test_acc = model.evaluate(test_images, test_lables)
>>> print(f"test_acc: {test_acc}")
test_acc: 0.9785
测试精度约为97.8%,比训练精度98.9%低不少。训练精度和测试精度之间的这种差距是过拟合造成的。
4、参考资料
图书:Python深度学习(第2版)
作者:[美]弗朗索瓦·肖莱 著 张亮 译