Python 深度学习

1. 什么是深度学习

人工智能、机器学习和深度学习

人工智能

将通常由人类完成的智能任务,尽量实现自动化;

机器学习

  • 程序设计:输入数据和计算规则,输出计算结果;

  • 机器学习:输入数据和结果,输出计算规则;

机器学习系统是训练出来的,而不是通过程序编写出来的

从数据中学习表示

机器学习的三要素:输入数据点、预期输出的示例、衡量算法好坏的方法;

机器学习的核心:在预先定义的一组方法(即 hypothesis space 假设空间)中,找到一种有意义的变换数据的方法,使得数据转换成更加有用的表示 representation ;

深度学习之“深度”

深度 depth 是指学习的过程,涉及很多层级的堆叠,所以深度学习也叫 hierarchical representation learning 层级表示学习(或 layer representation learning 分层表示学习);

分层的做法,来源于神经网络模型(neural network)启发,但事实上它跟人类大脑的神经网络模型,并没有任何关系;只是恰好用这个启发来命名这个学习模型而已;

传统的机器学习因只注重1-2层的数据表示,因此有时也叫做浅层学习 shallow learning;

深度学习可以简单理解为:学习数据表示的多级方法;每一级的方法,就像是一个蒸馏的操作,虽然每经过一级数据变得越来越少,但纯度却越来越高,跟解决任务越来越相关;

用三张图理解深度学习的工作原理

权重 weight :神经网络中,某一层对数据所做的变换操作,存储于该层的权重中;权重是一组数字,它是该层操作的参数 parameter ;每层的变换,由权重来实现参数化 parameterize ;

学习的过程,即为神经网络中的所有层,找到最合适的一组权重值,使得输入的示例,能够与目标一一对应;

损失函数 loss function :用于计算神经网络的预测值与真实目标值之间的距离值;损失函数有时也叫目标函数 objective function;

深度学习技巧:根据损失函数计算出的距离值,作为反馈信号,对权重进行微调,以降低损失值;这种调节由优化器 optimizer 来完成,它实现了反向传播 backpropagation 的算法;

整个调节的过程,称为训练循环;通过对几千个示例,做几十次的循环后,就有可能得到损失最小的网络模型;

机器学习简史

概率建模

朴素贝叶斯算法:基于朴素贝叶斯定理的分类器;

logistic regression 逻辑回归(简称 logreg):名字虽然有回归两个字,其实是一种分类算法,而不是回归算法;

早期神经网络

Yann LeCun 使用卷积神经网络+反向传播算法,应用于美国邮政的手写邮政编码识别;早期的神经网络算法目前都被一些更现代的算法取代了;

核方法

核方法是一种分类算法,其中最有名的是 SVM 支持向量机 support vector machine;其致力于在两级不同类别的数据集合中,寻找一个良好的决策平面(边界),从而解决分类问题;

步骤:

  • 将数据映射到高维空间;
  • 在高维空间中找到一个超平面,该平面使得两个类别的数据点之间的距离最大化;

kernel trick 核技巧:由于映射高维空间很抽象,因此,可以通过核函数(kernel function)来简化这个过程;核函数的原理是抛弃高维空间,转而求数据点对之间的距离;之后根据求得的距离结果,来寻找超平面;但这也有缺点:当数据集很大时,或需要解决感知问题时,这一思路变得不那么可行;因为如果想将 SVM 应用于感知表示,则需要先提取有用的表示(即特征工程),但这个提取过程比较麻烦,而且也不太稳定,从而限制了 SVM 的使用场景;

决策树、随机森林与梯度提升机

decision tree 决策树:挑出一个待选特征,对输入数据点进行分类,如果分类的数据符合目标,则特征有意义,如果不符合,则没有意义;通过对这个过程的反复迭代,最终得到由特征判断组成的整个决策树;另外决策树也可用于给定输入预测输出;

random forest 随机森林:一种决策树学习算法;它首先构建很多决策树,然后再把这些决策集成起来;对于浅层学习任务,它几乎总是第二好的算法;

  • 步骤:
    • 数据随机抽取;
    • 待选特征随机抽取;
    • 对各子树的分类结果进行投票,量多者胜;
  • 思想:相对于决策树寻找最厉害的专家的策略,随机森林的策略为:三个臭皮匠,顶个诸葛亮;

gradient boosting machine 梯度提升机:将多个弱预测模型集成起来,并通过训练循环不断改进弱预测模型;最后与决策树方法进行结合得到模型,其性质与随机森林类似,但效果更好;对于非感知问题,基本上是目前最适用的算法;

深度学习的不同点

通过渐进的、逐层的方式,形成越来越复杂的表示;(貌似需要记录各层之间的依赖关系)

模型可以在同一时间共同学习所有表示层,而不是依次渐进的学习(基于前一步的不同层之间的依赖关系进行调整,但貌似计算量也很大)

决策树、随机森林和梯度提升机,都涉及到特征的提取(即特征工程),但深度学习则绕过了这个问题,它通过假设空间对每一层做简单变换,然后再根据反向传播不断微调,最终取得最好的权重值组合;(不过话说回来,怎么感觉假设空间与特征工程其实是一回事?差别在于后者没有记录依赖关系,学习的效率降低了);

机器学习现状

梯度提升机的常用框架:XGBoost;

深度学习的常用框架:Keras;

深度学习的两个核心思想卷积神经网络和反向传播,在70年代就已经提出了,但由于硬件和数据集的瓶颈,直到最近几年才开始发挥影响力;

  • 硬件:CPU 的设计面向复杂的计算场景,使用多种指令集;GPU 的设计面向单一的使用场景,所以在特定场景中,其计算效率要远远高于 CPU;Google 则研发 TPU 进行专用的运算;
  • 数据:由于互联网的普及,使得数据的收集变得非常容易;
  • 算法:神经需要足够多的层数,才能发挥作用;早期没有找到有效增加层数进行梯度传播的办法;最近几年,越来越多的算法被提出,得以实现足够多的层数;包括:更好的神经层激活函数 activation function,更好的权重初始化方案 weight initialization scheme;更好的优化方案 optimization scheme;2014年以后,又增加了更好的覆盖率传播方法,例如:批标准化、残差连接、深度可分离卷积等;

2. 神经网络的数学基础

初识神经网络

分类问题中的某个类别叫作类 class,数据点叫做样本 sample,标签 label 用来表示样本对应某个类;

训练集(trainng set) 一般由 train_images 和 train_labels 组成;测试集(test set) 一般用 test_images 和 test_labels 组成;

神经网络的核心组件是层 layer,它是一个数据处理模块,它从输入数据中提取表示,有点像是一个数据过滤器,或者数据蒸馏器;大多数深度学习是将多个层链接起来,实现渐进式的数据蒸馏 data distillation;

在训练和测试过程中,需要指定需要监控的指标 metric,以便网络可以根据指标进行改进,拟合(fit)模型;

过拟合:学习模型是测试集上面的表现比训练集差;

神经网络的数据表示

张量 tensor 是一种数据结构,用来存储输入网络的数据对象;张量是一种数字容器,可以看做是矩阵在任意维度的推广;

仅包含一个数字的张量,称为标量 scalar(也叫标量张量,零维张量,0D张量),标量张量的轴数为0;

数字组成的数组,叫做向量 vertor(也叫一维张量,1D张量),向量的轴数为1;向量(即数组)有几个元素,称为几D向量,例如5个元素称为5D向量;

向量组成的数组,叫做矩阵(也叫二维张量,2D张量),矩阵的轴数为2;

多个矩阵组成的数级,可以得到3D张量;多个3D张量组成的数组,可以得到 4D 张量,以此类推;多数深度学习使用 0D - 4D 张量的数据结构,视频处理则可能用 5D 张量;

张量的三个关键属性:轴数(即阶数,arr.ndim),形状(每个轴的维度大小, arr.shape)、数据类型(arr.dtype);

张量切片:选择张量的特定元素;所有张量的第一个轴(即0轴)用于做样本轴(sample axis,也叫样本维度);

深度学习模型为提高计算速度,会将数据集分成多个小批量,并行处理;每个小批量的第一个轴叫做批量轴或批量维度(其实本质和样本轴一样,只是数据量大小不同);

几种常见的数据张量类型:

  • 2D张量(即矩阵):samples, features
  • 时间序列:samples, timestamps, features
  • 图像:samples, height, width, channels
  • 视频:samples, frames, height, width, channels

张量运算

逐元素(element-wise)运算:该运算独立应用于张量中的每个元素(因此这种运算非常适合用来做并行计算);

广播:将轴数较小的张量,与轴数较大的张量进行运算时,小张量会在大张量的其他轴上进行广播;

张量点积 tensor product:其实它就是矩阵的乘法,背后的本质是求解多项式的应用;注意别跟逐元素的乘法弄混了;

张量变形 tensor reshaping:保持元素数量不变,但改变形状,也即 numpy 里面的 reshape,以及转置 np.transpose

张量运算可以视为几何空间中的运算;神经网络对输入数据在几何空间中做各种变换尝试,最终将原本复杂混合的数据,转换成清晰分类的数据(红纸蓝纸揉成一团后再解开的例子);

基于梯度的优化

output = relu(dot(W, input), b),其中 W,b 都是张量,属于该层的属性,分别对应 kernel 属性和 bias 属性;二者即该层的可训练参数 trainable parameter,或者叫权重 weight;一开始这些权重取很小的初始值,即随机初始化 random initialization;

抽取训练样本 x 和对应的目标样本 y 组成数据批量,将 x 输入网络运行得到预测值 y_pred;这一步叫做正向传播 forward pass;

由于网络中所有的运算都是可微的,因此可以计算损失相对于网络系数的梯度(张量运算的导数),之后按梯度的反方向改变网络系统大小;这一步叫做反向传播 backward pass;

随机梯度下降 stochastic gradient descent(SGD):将参数沿着梯度的反方向随机移动一点点,从而使得损失减少一点点;

为了避免局部最小值和收敛速度问题,引入动量的概念:根据动量的概念,每次移动参数的幅度,要同时考虑加速度(斜率值)和当前速度(来自于之前的加速度),这样可以跳过局部最小点,同时加快收敛的速度;

链式法则:基于求导恒等式 (f(g(x)) = f(g(x)) * g`(x),推导出反向传播算法 back-propagation(也叫反式微分 reverse-mode differentiation),即根据最终损失值,从最顶层开始到最低层,推导每个参数对损失值的贡献大小;此处引入了符号微分 symbolic differentiation 算法,该算法可以实现:给定一个运算链,并且已知每个运算环节的导数,则可以求得整个运算链的梯度函数;

3. 神经网络入门

神经网络剖析

层:深度学习的基础组件

不同的张量格式的不同的数据类型通常会使用到不同各类的层进行处理;

  • 向量数据(2D):密集层,也叫全连接层或密集连接层
  • 图像数据(4D):二维卷积层
  • 序列数据(3D):循环层

层兼容性:每一层只接收特定形状的输入,产生特定形状的输出;

模型:层构成的网络

它有很多种结构,常见的如线性堆叠、双分支(two-branch)、多头(multi-head)、Inception模块等;网络的拓扑结构定义了一个假设空间,也因此限定了一系列特定的张量运算;选择合适有效的网络结构,更像是一门艺术而科学;

损失函数与优化器:配置学习过程的关键

损失函数:选择正确的损失函数对解决问题至关重要,对于常见的问题,已经有一些现成的目标函数可以使用,例如二分类问题使用二元交叉熵(binary crossentropy),序列问题使用联结主义时序分类(connectionist temporal classification);多分类问题使用分类交叉熵(categorical crossentropy);回归问题使用均方误差(mean-squared error);只有真正面对全新问题的时候,才需要自主开发新的目标函数;

具有多个输出的神经网络,可能具有多个损失函数,但只能有一个损失标量值,因此需要将多个损失函数的结果取平均;

Keras 简介

Keras 是一模型库,因此它可以和张量库(如 TensorFlow, Theano, CNTK 等)配合使用,简化了用户的学习和上手成本;

使用 Keras 开发:概述

典型的工作流程:

  • 定义训练数据:输入张量和目标张量
  • 定义模型(即由层组成的网络),将输入映射到目标;
  • 配置学习过程:选择损失函数、优化器和过程中需要监控的指标;
  • 调用模型的 fit 方法在训练数据上进行迭代;

定义模型的两种方法

  • 使用 Sequential 类:用于层的线性堆叠,属于目前最常见的网络架构;
  • 使用 函数式 API:通过有向无环图,用于构建任何形式的架构;
1
2
3
4
5
6
7
8
9
10
11
12
13
from keras import models
from keras import layers

# 使用 Sequential 构建模型
model = models.Sequential()
model.add(layers.Dense(32, activation="relu", input_shape=(784,)))
model.add(layers.Dense(10, activation="softmax"))

# 使用函数式 API 构建模型
input_tensor = layers.Input(shape=(784,))
x = layers.Dense(32, activation="relu")(input_tensor)
output_tensor = layers.Dense(10, activation="softmax")(x)
model = models.Model(inputs=input_tensor, outputs=output_tensor)

配置学习过程

1
2
3
4
5
6
7
from keras import optimizers

model.compile(
optimizer=optimizers.RMSprop(lr=0.001),
loss="mse",
metrics=["accuracy"]
)

调用 fit 方法进行迭代训练

1
model.fit(input_tensor, target_tensor, batch_size=128, epoch=10)

深度学习的原理并不复杂,最难的部分可能是根据待解决的问题,找到最适合的模型;对于常见的问题,前人们已经找到和总结了很多高效的模型;但在实际业务过程中,有可能会遇到不完全相同的问题,此时便需要在前人模型的基础,进一步调整和测试;这一步才是最难的,搞不好整个过程中都需要有一定的运气成分;

建立深度学习工作站

推荐使用 Linux 系统 + GPU 机器;

虽然书上提供了在本地原生安装的方法,但其实更好的安装方式应该是使用 Docker 镜像,但可惜书上并没有提到;

电影评论分类:二分类问题

导入 IMDB 数据集

1
2
3
4
# 此处导入 keras 已经提前内置的 imdb 数据集
from keras.datasets import imdb
# imdb 对象的 load_data 方法可导入训练数据和测试数据,元组格式,每个元组由数据和标签两部分组成,一一对应
(train_data, train_labels), (test_data, test_labels) = imdb.load_data(num_words=10000)

准备数据

导入的数据只是列表,但 keras 只接收张量格式,因此需要将数据从列表格式转变成张量格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import numpy as np

# 将列表转成张量,若存在某个单词,则在对应的索引位置标记1
def vectorize_sequences(sequences, demension=10000):
results = np.zeros((len(sequences), demension))
for i, sequence in enumerate(sequences):
results[i, sequence] = 1.
return results

tensor_train_data = vectorize_sequences(train_data)
tensor_test_data = vectorize_sequences(test_data)

tensor_train_labels = np.asarray(train_labels).astype('float32')
tensor_test_labels = np.asarray(test_labels).astype('float32')

构建网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from tensorflow.keras import models
from tensorflow.keras import layers

# 开始构建网络
model = models.Sequential()
# 此处的16表示使用16的隐藏单元,用来表示结果空间,16即表示空间有16个维度
# 维度太高不一定好,一来计算量更大,二来有可能和训练数据过耦合,导致预测效果并不好
# 维度太低则有可能没有提到出最有用的特征,导致预测准确率下降
# 激活函数 relu 用来对计算结果中的负值归零,正值则保持不变
model.add(layers.Dense(16, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(16, activation='relu'))
# 由于最终的目标是一个标量,1表示正面评论,0表示负面评论
# 因此模型的最终输出的那层只需设置一个隐藏单元,这样就将计算结果映射到一个维度的标量中
# 激活函数 sigmoid 用来对计算结果进行归一处理,这样可以表示最终的概率
model.add(layers.Dense(1, activation="sigmoid"))
# 如果没有激活函数,则层的计算将只是 output = dot(W, input) + b 的矩阵点积计算,其结果
# 将只是对数据进行简单的线性仿射变换,并没有实质性的改变数据的空间映射;而通过引入激活函数
# 计算结果将不再是简单的线性变换,变成了非线性变换,因此空间映射发生了改变

# 此处的模型编译使用了默认内置的优化器、损失函数和指标器,但是,这三个东西也是可以
# 自定义的,即自定义优化函数、损失函数、衡量指标等;
# 此处使用的损失函数为二元交叉熵 binary_crossentropy,因为最终的结果是一个二元问题,即是或者否
# 因此特别适合使用二元交叉熵来做损失判断,它能够计算判断正确的概率
# 如果结果并不是一个二元问题,而是一个范围问题,则应使用其他损失函数,例如均方误差 MSE,mean squared error
# 它能够用来判断计算结果与预期目标的误差范围;
# 另外此处使用的度量指标是准确度 accuracy,表示计算结果是否准确等于目标值;
# 如果计算结果不需要准确等于目标值,而只需要控制在目标值一定范围内即可算是正确,则
# 应该使用平均绝对误差 MAE,mean absolube error;
model.compile(
optimizer="rmsprop",
loss="binary_crossentropy",
metrics=['accuracy']
)

# 根据问题场景的不同,内置的损失函数、度量指标、优化器不一定能够满足需求,此时
# 可以使用自定义的损失函数、度量指标、优化器
from keras import losses
from keras import metrics

model.compile(
optimizer=optimizers.RMSprop(1r=0.001),
loss=losses.binary_crossentropy,
metrics=[metrics.binary_accuracy]
)

验证模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 虽然在训练模型时将数据分为训练集和测试集,但在训练过程中,是分很多轮进行迭代训练的,这意味着每一轮都得对
# 训练结果进行测试;此时不能将测试集引入测试,因为它将直接测试集被耦合进模型;因此,需要从训练集中,再拆分
# 一部分数据出来,做为验证训练结果的测试数据,来训练模型;这样对最终的模型结果来说,测试集的数据仍然
# 保持是前所未见的数据
tensor_val_data = tensor_train_data[:10000]
partial_tensor_train_data = tensor_train_data[10000:]

tensor_val_labels = tensor_train_labels[:10000]
partial_tensor_train_labels = tensor_train_labels[10000:]

model.compile(
optimizer="rmsprop",
loss="binary_crossentropy",
metrics=['accuracy']
)

history = model.fit(
partial_tensor_train_data,
partial_tensor_train_labels,
epochs=20,
batch_size=512,
validation_data=(tensor_val_data, tensor_val_labels)
)

# 绘制图表,将训练结果可视化
import matplotlib.pyplot as plt
# 绘制预测损失的图表
history_dict = history.history
loss_values = history_dict.get("loss")
val_loss_values = history_dict.get("val_loss")
epochs = range(1, len(loss_values) + 1)

plt.plot(epochs, loss_values, 'bo', label="Training loss")
plt.plot(epochs, val_loss_values, 'b', label='Validation loss')
plt.title("Training and validation loss")
plt.xlabel("Epochs")
plt.ylabel("Loss")
plt.legend()
plt.show()

# 绘制预测精度的图表
plt.clf()
acc = history_dict.get("acc")
val_acc = history_dict.get("val_acc")

plt.plot(epochs, acc, 'bo', label="Training acc")
plt.plot(epochs, val_acc, 'b', label="Valication acc")
plt.title("Training and validation accuracy")
plt.xlabel("Epochs")
plt.ylabel('Accuracy')
plt.legend()
plt.show()

调整模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 调整轮次重新训练网络模型,这次使用全部的训练集,没有使用验证集
new_history = model.fit(
tensor_train_data,
tensor_train_labels,
epochs=4,
batch_size=512,
)

# 使用训练好的模型,使用测试集对其进行评估,看模型预测的准确性
results = model.evaluate(tensor_test_data, tensor_test_labels)
print(resutls)

# 查看模型在测试集上的预测结果
model.predict(tensor_test_data)

新闻分类:多分类问题

总共有46个主题标签,每条新闻只属于其中的一个主题,即只拥有一个标签;因此这是一个单标签、多种类别的问题;另外对于电影,则有可能是多标签、多种类别的问题;

此处联想到图片上的目标识别,可能也可以算是一个单标签多分类的问题;因为可以假设目标物体由多个部位组成,例如由头、手、脚组成;这些部位即是目标类别,然后图片上的每一个点,有且只有可能属于其中的某个类别,或者完全不属于任何一个部位的类别;

整理数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 此处导入 keras 已经提前内置的 reuters 数据集
from tensorflow.keras.datasets import reuters

(train_data, train_labels), (test_data, test_labels) = reuters.load_data(num_words=10000)

# 将数据张量化
import numpy as np

def vectorize_sequences(sequences, demension=10000):
results = np.zeros((len(sequences), demension))
for i, sequence in enumerate(sequences):
results[i, sequence] = 1.
return results

tensor_train_data = vectorize_sequences(train_data)
tensor_test_data = vectorize_sequences(test_data)


# 将标签向量化,有两种方法, 一种是将标签列表转换为整数张量,另一种是使用 one-hot 编码
# one-hot 的意思就是将 n 个标签中,被命中的那个标记为 1,其他的标记为 0
def to_one_hot(labels, dimension=46):
results = np.zeros((len(labels), dimension))
for i, label in enumerate(labels):
results[i, label] = 1.
return results

one_hot_train_labels = to_one_hot(train_labels)
one_hot_test_labels = to_one_hot(test_labels)

# 如果是转换为整数张量的话,则损失函数应该选择 sparse_categorical_crossentropy,即离散分类交叉熵
tensor_train_data = np.array(train_data)
tensor_train_labels = np.array(train_labels)

构建网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 构建网络
from tensorflow.keras import models
from tensorflow.keras import layers

# 开始构建网络
# 由于最后的结果需要将概率映射到46个标签的空间中,因此前面两层的空间不应该小于46
# 此处空间隐藏单元数量取值 64,以避免计算过程中的信息丢失
model = models.Sequential()
model.add(layers.Dense(64, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(64, activation='relu'))
# 由于最后的目标是从46个标签中选择一个,所以此处最后一层选择的激活函数为 softmax,
# 它用来计算某个样本在46种标签中,属于某一种标签的概率,46个概率的总共刚好等于 1
model.add(layers.Dense(46, activation="softmax"))

# 此处的损失函数不再使用二元分类问题的交叉熵,而是使用多元分类问题的交叉熵
# 度量指标仍然使用 accuracy,因为它本质上仍然是计算分类的准确率
model.compile(
optimizer="rmsprop",
loss="categorical_crossentropy",
metrics=['accuracy']
)

训练模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# 预留部分数据作为验证集
val_data = tensor_train_data[:1000]
partial_train_data = tensor_train_data[1000:]

val_labels = one_hot_train_labels[:1000]
partial_train_labels = one_hot_train_labels[1000:]

# 训练模型
history = model.fit(
partial_train_data,
partial_train_labels,
epochs=20,
batch_size=512,
validation_data=(val_data, val_labels)
)


# 绘制表格,将数据可视化
import matplotlib.pyplot as plt
# 绘制预测损失的图表
history_dict = history.history
loss_values = history_dict.get("loss")
val_loss_values = history_dict.get("val_loss")
epochs = range(1, len(loss_values) + 1)

plt.plot(epochs, loss_values, 'bo', label="Training loss")
plt.plot(epochs, val_loss_values, 'b', label='Validation loss')
plt.title("Training and validation loss")
plt.xlabel("Epochs")
plt.ylabel("Loss")
plt.legend()
plt.show()

# 绘制预测精度的图表
plt.clf()
acc = history_dict.get("acc")
val_acc = history_dict.get("val_acc")
epochs = range(1, len(loss_values) + 1)

plt.plot(epochs, acc, 'bo', label="Training acc")
plt.plot(epochs, val_acc, 'b', label="Valication acc")
plt.title("Training and validation accuracy")
plt.xlabel("Epochs")
plt.ylabel('Accuracy')
plt.legend()
plt.show()


# 重新训练模型,因为从第 9 轮开始就过拟合了
history = model.fit(
partial_train_data,
partial_train_labels,
epochs=9,
batch_size=512,
validation_data=(val_data, val_labels)
)

results = model.evaluate(tensor_test_data, one_hot_test_labels)

预测房价:回归问题

整理数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 此处导入 keras 已经提前内置的 boston housing 数据集
from tensorflow.keras.datasets import boston_housing

(train_data, train_targets), (test_data, test_targets) = boston_housing.load_data()

# 准备数据
# 如果数据过于离散,取值范围跨度很大,虽然模型仍然可以从中进行学习
# 但是这样会加大学习的难度,所以对于取值范围跨度很大的数据,最好一开始对其进行标准化操作
mean = train_data.mean(axis=0)
train_data -= mean
std = train_data.std(axis=0)
train_data /= std

test_data -= mean
test_data /= std

构建网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from tensorflow.keras import models
from tensorflow.keras import layers

def build_model():
model = models.Sequential()
model.add(layers.Dense(64, activation='relu', input_shape=(train_data.shape[1],)))
model.add(layers.Dense(64, activation='relu'))
# 最后一层没有使用激活函数,是因为现在要解决的是一个标量回归的问题
# 因此最后一层计算出来的结果,可以直接作为目标值使用
model.add(layers.Dense(1))
model.compile(
optimizer="rmsprop",
loss="mse",
metrics=['mae']
)
return model

训练模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import numpy as np 
# 使用 K 折验证,解决样本数过小的问题
k = 4
num_val_samples = len(train_data) // k
num_epochs = 500
all_mae_histories = []

for i in range(k):
print("processing fold: ", i)
val_data = train_data[i * num_val_samples : (i + 1) * num_val_samples]
val_targets = train_targets[i * num_val_samples : (i + 1) * num_val_samples]

partial_train_data = np.concatenate(
[train_data[: i * num_val_samples],
train_data[(i + 1) * num_val_samples:]],
axis=0
)
partial_train_targets = np.concatenate(
[train_targets[: i * num_val_samples],
train_targets[(i + 1) * num_val_samples:]],
axis=0
)

model = build_model()
history = model.fit(
partial_train_data,
partial_train_targets,
validation_data=(val_data, val_targets),
epochs=num_epochs, batch_size=1, verbose=0)
# print(history.history.keys())
# break
mae_history = history.history['val_mae']
all_mae_histories.append(mae_history)

绘制图表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
average_mae_history = [
np.mean([x[i] for x in all_mae_histories]) for i in range(num_epochs)]

# 绘制图表
import matplotlib.pyplot as plt

plt.plot(range(1, len(average_mae_history) + 1), average_mae_history)
plt.xlabel("Epochs")
plt.ylabel("Validation MAE")
plt.show()


# 去除无效值,平滑曲线
def smooth_curve(points, factor=0.9):
smoothed_points = []
for point in points:
if smoothed_points:
previous = smoothed_points[-1]
smoothed_points.append(previous * factor + point * (1 - factor))
else:
smoothed_points.append(point)
return smoothed_points

# 删除前10个数据点,因为它们跟其他点偏差过大
smooth_mae_history = smooth_curve(average_mae_history[10:])

plt.plot(range(1, len(smooth_mae_history) + 1), smooth_mae_history)
plt.xlabel('Epochs')
plt.ylabel('Validation MAE')
plt.show()

重新训练

1
2
3
4
5
6
7
8
9
10
11
12
13
# 训练最终的模型
model = build_model()
model.fit(
train_data,
train_targets,
epochs=80,
batch_size=16,
verbose=0
)

test_mse_score, test_mae_score = model.evaluate(test_data, test_targets)

print(test_mae_score)

小结

  • 回归问题与分类问题不同;对于分类问题,要么对,要么错,因此可以使用准确率作为预测结果的度量指标;但对于回归问题,它的结果跟目标之间是以差值多少出现的,而非对或者错,因此它需要使用平均绝对误差 MAE 作为度量指标;同时它的损失函数使用均方误差 MSE 来计算;
  • 由于回归问题的标签值是某个数值,每个值与值之间可能存在较大的取值范围,因此在使用模型学习之前,一般要对它们进行标准化处理,让它们的取值范围呈现标准化;
  • 当样本数量比较少时,可以考虑使用 K 折验证来降低偶然因素;
  • 如果样本数很少,模型的层数也应该尽量少一些,不然模型容易与数据产生过拟合;

4. 机器学习基础

机器学习的四个分支

1. 监督学习

目标:学会将输入数据映射到已知目标;

  • 分类问题:二分类、多分类;
  • 回归问题:根据打分预测房价;
  • 序列生成:给定一张图像,预测描述图像的文字(感觉像是多分类问题);
  • 语法树预测:给定一个句子,预测其生成的语法树;
  • 目标检测:给定一张图像,在特定目标的周围画一个框;
  • 图像分割:给定一张图像,在特定物体上画一个像素级的掩模;

2. 无监督学习

  • 定义:没有特定目标的情况下,寻找输入数据的有趣变换;
  • 目的:数据可视化、数据压缩、数据去噪,或者更好的理解数据的相关性;它常用于数据分析,在解决监督学习的问题之前,对数据进行分析通常是必要的,以便更好的了解数据集;
  • 常用方法:降维(dimension reducion)、聚类(clustering);

3. 自监督学习

  • 定义:它是监督学习的一个特例;初始的时候,并没有输入标签,而只是给了一个启发式的算法,让机器来自己生成标签,然后靠这些标签进行自监督学习;
  • 自监督学习的一个例子是自编码器,它用输入作为目标,来比对对数据所提取的抽象表征能否顺利的还原;

以前曾经用它来学习压缩算法,后来发现没有什么卵用,一个是压缩效率不高,二是跟输入数据强相关,在不同类型的数据上面,压缩效率急剧变差;目前研究到最有用的应用领域是图像去噪;另外一个应用是将数据降维,让其可视化,方便人类发现数据的一些有趣特征;

4. 强化学习

智能体接收环境的信息,然后选择某种可以使奖励最大化的行动;目前主要在游戏领域比较成功,其他方面的应用则仍处于研究阶段;

常见术语

  • 样本:也叫输入,进入模型的数据;
  • 预测:也叫输出,模型给出的结果;
  • 目标:真实准确的值,模型在理想情况下给出的结果应跟目标一致;
  • 预测误差:也叫损失值,预测与目标之间的距离;
  • 类别:分类问题中的一组分类标签;
  • 标签:分类问题中的单个类别标签;
  • 真值:也叫标注:数据集的所有目标;
  • 二分类:预测结果只有两个类型的分类任务;
  • 多分类:预测结果应分配到2个以上类型的分类任务;
  • 多标签分类:预测结果可以分配多个标签的任务;
  • 标量回归:目标是连续的标量值的任务,例如房价;
  • 向量回归:目标是一组连续值的任务,例如图像边框检测;
  • 小批量:模型同时进行处理的一小组样本;样本数量通常取2的幂,这样在 GPU 内存上比较好分配;
  • 特征图:feature map,其实就是 3D 张量(包含高度和宽度两个空间轴,和一个深度轴,深度轴也叫通道轴),它即可以是输入,也可以是输出(此处的通道很像 Dense 层里面的隐藏单元,用来存放计算结果);
  • 过滤器:filter,3D 张量深度轴的不同通道即是代表过滤器;通道值是过滤器对输入数据的某一方面进行编码的结果;

评估机器学习模型

可泛化的模型:在新数据上面表现良好的模型;泛化能力是评估一个模型优秀与否的指标;

模型的超参数:指模型的层数、每层大小(隐藏单元数量)这些参数;
模型的参数:指每层的权重值;

训练集、验证集和测试集

将数据分成三个集合是必要的,因为在训练过程中,模型反复根据验证集的验证结果进行参数的调整,这会导致模型与验证集的拟合性越来越好,但是在全新数据上面的性能却不一定更好;所以需要有一个测试集,做为全新的数据来对模型进行评估;

三种经典的模型评估方法
1. 简单留出验证

将数据分成三部分,其中的训练集、验证集用来训练模型,测试集用来评估模型;
缺点:当样本数很少时,这种方法很容易跟数据过拟合;过拟合可以通过随机打乱数据集来训练模型,看最后的结果是否波动很大;

2. K 折验证

将数据均分大小相同的 K 个分区,每次取其中一个分区作为验证集,余下做为训练集;最后取 K 个分数的平均值作为评分;

3. 重复 K 折验证

进行多次 K 折验证,每次都将数据先打乱;这种方式的计算成本比较高;需要计算 K * P 次

评估模型的注意事项
  • 数据代表性:一般通过随机打乱数据来实现;
  • 时间箭头:如果是解决用旧数据预测未来新数据的问题,则注意训练的数据与测试的数据有时间点的区隔,不可重叠;
  • 数据冗余:确保训练集和测试集没有任何交集,避免因为有数据冗余导致隐藏交集;

数据预处理、特征工程和特征学习

神经网络的数据预处理

  • 向量化:data vectorization,神经网络的输入和目标都必须是浮点数张量(少数特殊情况可接收整数);
  • 值标准化:让所有特征的均值为0,标准差为1;输入数据应满足同质性,即大致相同的取值范围;
  • 处理缺失值:一般使用 0 来代表缺失值;如果样本集中没有缺失值,但未来的新数据有可能有缺失值,那么训练出来的网络无法应对有缺失值的情况,此时需要人工生成一些缺失值的样本;

特征工程

特征工程的作用在于:用更简单的方式来表达问题,从而使得问题的解决变得更容易;

虽然现代的卷积神经网络可以自动学习特征,使得大部分特征工程变得没有必要,但是良好的特征工程仍然重要,原因有二:

  • 用更少的计算资源更优雅的解决问题
  • 用更少的数据样本即可解决问题;

过拟合和欠拟合

机器学习的根本问题是优化(optimization)和泛化(generalization)的对立;

防止模型从训练数据中学到错误或无关紧要的模式,方法有二:

  • 最优的方法:收集更多的数据用于训练;
  • 次优的方法:调节模型允许存储的信息量,或对允许存储的信息增加约束;原因:模型允许存储的信息量越少,模型越容易记住更关键的信息;

降低过拟合的方法:正则化 regularization

1. 减少网络大小

如果模型的容量足够大(由层数和每层单元数决定),模型将很容易实现样本和目标之间的映射关系,但这种映射却对泛化能力有害;

反之,如果容量不那么大,则无法轻松实现映射,此时模型就需要学会对目标具有很强预测能力的压缩表示,这样对泛化有利;但容量也不能太小,不然容易出现欠拟合问题;

暂时没有魔法公式可以确定最佳层数和每层最佳单元数,这需要使用验证集进行反复实验才能得到最佳结果;

2. 添加权重正则化

奥卡姆剃刀原则:如果一件事情有两种解释,那么最可能正确的是最简单的那个(即假设条件最少的那个);

给定一些训练数据和一种网络架构,很多组权重值(即很多模型)都可以解释这些数据,此时,简单的模型比复杂的模型更不容易过拟合;

这里的简单模型指参数分布的熵更小的模型,或参数更少的模型;
熵被用计算一个系统中的失序现象,即系统的混乱程度;熵越高 ,系统越混乱;

通过强制让模型权重取较小的值,从而限制模型的复杂度,使得权重值的分布更加规则(regular);这种方法叫权重正则化(weight regularization);实现方法:向网络的损失函数中添加与较大权重值相关的成本,Keras 中通过向层传递权重正则化项实例(weight regularizer);

  • L1 正则化:添加的成本与权重系数的绝对值成正比;
  • L2 正则化:添加的成本与权重系数的平方成正比;此方法也叫权重衰减(weight decay);

由于惩罚项只在训练时添加,测试没有添加,因此网络的训练损失会比测试损失大很多;

3. 添加 dropout 正则化

对某一层使用 dropout,就是在训练过程中随机将该层的一些输出特征舍弃(设置为 0);dropout 的比率通常在 0.2~0.5 范围内;

测试时没有单元被舍弃,而该层的输出值需要按 dropout 比率缩小,因为此时有更多的单元被激活,需要加以平衡;
但在实践中,一般这个平衡的动作是在训练时操作,即先 dropout,再将输出成比例放大;而最后测试时输出保持不变;
dropout 的思想在于在层的输出中引入一些噪声,从而避免模型学习到一些偶然的模式,从而降低过拟合的概率;

机器学习的通用工作流程

1. 定义问题,收集数据集

使用机器学习解决问题的关键在于以下两个假设成立:

  • 假设输出是可以根据输入进行预测的;(数据与答案有关联)

    现实中,有很多问题的答案,如果跟过去的历史并没有关系,则机器学习到的模型并不能用来很好的预测未来;

  • 假设可用数据包含足够多的信息,足以学习输入和输出之间的关系;(数据足够多)

    数据必须是在一个平稳的尺度上收集的;例如用夏天的服装销售数据预测冬天的销量并没有意义,因为机器学习无法解决非平稳问题(nonstationary problem);

2. 选择衡量成功的指标

制定衡量成功的指标,与损失函数的选择相关;不同类别的问题,选择不同的指标;

  • 平衡分类问题(每种类别的可能性相同):常用指标为精度和 ROC AUC(area under the receiver operating characteristis curve,接收者操作特征曲线下面积);
  • 不平衡的分类问题:常用指标为准确率和召回率(问:啥是召回率?答:所有为真值的样本,被正确识别出来的比例,而准确率表示被认为是真的那些样本,确实为真的比例);
  • 排序问题或多标签分类:常用指标为平均准确率均值(mean average precision);

其他更多的问题类型和对应的自定义指标,可以浏览 Kaggle 网站上的数据竞赛,上面有各式各样的问题和评估指标;

3. 确定评估方法

留出验证集、K 折验证、重复 K 折验证,三者选其一;一般情况下,第一种方法即可满足要求(除非样本数很小);

4. 准备数据

将数据格式化,转换成张量数据;

5. 开发比基准更好的模型

此阶段的目标在于先开发一个”小型“模型,它要能够打败纯随机的基准(dumb baseline),即获得统计功效(statistical power);

如果不能获得统计功效,那有可能答案并不在数据里,先前的两个假设可能是错误的;

构建模型需要选择的三个关键参数:

  • 最后一层的激活:它用来对网络的输出做有效的限制;
  • 损失函数:需要匹配问题类型;
  • 优化器:一般使用 rmsprop 即可;

衡量问题成功与否的指标,有时并不能用损失函数进行优化,因为损失函数有两个要求,一是即使小批量数据也可以计算,二是必须是可微的;此时的办法是使用替代指标,例如 ROC AUC 的替代指标为交叉熵;

6. 扩大模型规模:开发过拟合的模型

在有了统计功效的小模型之后,接下来要做的是扩大它,让它变成过拟合;因为理想的模型刚好处在欠拟合和过拟合的分界线上;所以需要先达到过拟合的状态,才能发现二者的分界线;

开发过拟合模型的办法:
  • 添加更多的层;
  • 每层变得更大;
  • 训练更多的轮次;

通过始终监控训练损失和验证损失,以及所关注指标的训练值和验证值,来发现是否出现过拟合;

7. 模型正则化与调节参数

此步的目标是反复对模型进行局部的调节优化,以便达到最佳的性能;

调节模型的方法:
  • 添加 dropout
  • 尝试不同的架构:增加或减少层数;
  • 添加 L1 和(或) L2 正则化(正则化:在损失函数中,给更大的权重值添加一些成本);
  • 尝试不同的超参数(比如每层的单元个数,或优化器的学习率),以找到最佳配置
  • (可选)反复做特征工程:添加新特征,或者删除没有信息量的特征;

一旦开发出满意的模型配置后,就可以在训练集和验证集上训练最终的生产模型,然后在测试集上最后评估一次;

如果测试集上的性能比验证集差很多,则说明验证流程并不可靠,或者模型在验证数据上出现了过拟合;此时,需要更换为更可靠的验证方法,如 K 折验证等;

5. 深度学习用于计算机视觉

卷积神经网络简介

卷积网络在处理图像时特别好用,原因在于它对应了图像的两种基本特征:

  • 平移不变性:在某个局部位置学习到的模式,可以适用于其他位置,即局部模式可以进行平移;密集连接网络学习到的模式是全局关系,因此它不具备平移不变性;(可移植)
  • 空间层次性:在某个层次学习到的模式,可以在下一个层次中进行组合,变成更大的模式;(可组合)

卷积运算过程

  • 按一定大小的窗口,例如 3 * 3,对图片进行某个局部位置做卷积运算,得到一个有深度的输出结果;在深度维度上的每一个值,代表在这个小窗口中学习到的一个小特征;深度可以自定义;
  • 平移小窗口,对整张图片进行卷积运算,就会得到由各种小特征组成的一个 3D 特征矩阵;矩阵的长宽分别代表一个窗口运算的结果,矩阵的深度则是该窗口的小特征集合;
  • 接下来使用最大池化技术,对上一步获取的特征矩阵,进行采样;使用 2 * 2 窗口按步幅 2 进行采样,而卷积层是使用 3 * 3 窗口按步幅 1 进行计算;

总结来说就是两步,第一步是找特征,第二步是对特征进行采样(采集明显与众不同的那些特征);

除了卷积计算外,还是一个反向卷积计算,叫 Deconvolution,也叫 transpose convolution;先使用正向卷积提取关键特征后,再用反向卷积可以提纯这些特征,去除最原始的噪声;在做反卷积计算时,由于输出比输入大,因此需要做一些 padding 的工作,然后才能够作常规的卷积核乘积计算;

反向卷积常用于图片分割任务,因为分割涉及像素级的操作,所以不能使用样本来代表整个图片,因此需要让最后的数据仍然保持和输入时一样,此时就可以先通过正向卷积获取关键特征,最后再通过反向卷积重新生成图片,用于分割;另外在 super-resolution,GAN,Surface depth estimation 任务中也会用到;可以说,凡是输出需要输入大的场景,都有可能会用到它;

用最大池化进行采样的目的

  • 一是可以减少需要处理的特征图的元素的个数;
  • 二是让观察窗口越来越大,覆盖原输入图的全部位置,从而可以学习到由局部图像组成的空间层次模式;
  • 观察不同特征的最大值,而非平均值,更容易发现一些特征信息,因为特征通常是突出表现,与众不同的;

在小型数据集上从头开始训练一个卷积神经网络

  • 卷积网络学习到的模式因为具有局部性和平移不变性的特点,相比其他网络模型,它可以在一个相对较小的数据集上学到较多的有用信息,取得还不错的效果;
  • 如果要处理问题和数据比较大比较复杂,则应相应增加一些层数和单元数,以便有足够的容量存储学习到特征信息,避免欠拟合;
  • Keras 有自带一个图像处理类,它能很好的完成图像处理的一些常见任务(以 python 生成器来实现);
  • 在较小的图片数据集上,可以使用数据增强(data augmentation)的技巧,来间接扩大数据集(它的本质上对图像做一些变形以生成新图片,例如旋转、翻转、缩放、拉伸等);但是由于数据增强的数据来源仍是原始数据,所以部分数据是高度相关的,为避免产生过拟合,一般配合使用 dropout 层添加一些噪声来平衡;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# 下载图片,准备数据
import os, shutil
# 原始数据解压后的存放目标
original_dataset_dir = "/downloads/kaggle_original_data"
# 较小数据集的保存目录
base_dir = '/downloads/cats_and_dogs_small'
os.mkdir(base_dir)

train_dir = os.path.join(base_dir, "train")
os.mkdir(train_dir)

validation_dir = os.path.join(base_dir, "validation")
os.mkdir(validation_dir)

test_dir = os.path.join(base_dir, "test")
os.mkdir(test_dir)

train_cats_dir = os.path.join(train_dir, "cats")
os.mkdir(train_cats_dir)

train_dogs_dir = os.path.join(train_dir, "dogs")
os.mkdir(train_dogs_dir)

validation_cats_dir = os.path.join(validation_dir, "cats")
os.mkdir(validation_cats_dir)

validation_dogs_dir = os.path.join(validation_dir, "dogs")
os.mkdir(validation_dogs_dir)

test_cats_dir = os.path.join(test_dir, "cats")
os.mkdir(test_cats_dir)

test_dogs_dir = os.path.join(test_dir, "dogs")
os.mkdir(test_dogs_dir)


fnames = ['cat.{}.jpg'.format(i) for i in range(1000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(train_cats_dir, fname)
shutil.copyfile(src, dst)

fnames = ['cat.{}.jpg'.format(i) for i in range(1000, 1500)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(validation_cats_dir, fname)
shutil.copyfile(src, dst)

fnames = ['cat.{}.jpg'.format(i) for i in range(1500, 2000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(test_cats_dir, fname)
shutil.copyfile(src, dst)

fnames = ['dog.{}.jpg'.format(i) for i in range(1000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(train_dogs_dir, fname)
shutil.copyfile(src, dst)

fnames = ['dog.{}.jpg'.format(i) for i in range(1000, 1500)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(validation_dogs_dir, fname)
shutil.copyfile(src, dst)

fnames = ['dog.{}.jpg'.format(i) for i in range(1500, 2000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(test_dogs_dir, fname)
shutil.copyfile(src, dst)

数据预处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 数据预处理
# 读取图片,将图片转换为像素风格;将像素网络转换为浮点数张量;将像素值缩放到[0, 1] 之间;
from keras.preprocessing.image import ImageDataGenerator

train_data_gen = ImageDataGenerator(rescale=1./255)
test_data_gen = ImageDataGenerator(rescale=1./255)

# generator 表示生成器,它会在每次被调用时,生成并返回一份数据
# 有点像迭代器,通常和 for...in... 配合使用
# 生成器跟迭代器不同的地方在于,它没有终点,只要一直被调用,就会不断生成数据
# 所以需要在某个时间点使用 break 进行终止
train_data_generator = train_data_gen.flow_from_director(
train_dir,
target_size=(150, 150),
batch_size=20,
# 此处使用二进制类模式,原因在于问题本身是一个二元分类问题,后续计算时
# 将使用二元交叉熵作为损失函数
class_mode='binary'
)

validation_data_generator = test_data_gen.flow_from_director(
valication_dir,
target_size=(150, 150),
batch_size=20,
class_mode='binary'
)

构建网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 构建网络
from tensorflow.keras import models
from tensorflow.keras import layers

model = model.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation="relu"), input_shape=(150, 150, 3))
model.add(layers.Maxpooling2D((2, 2)))

model.add(layers.Conv2D(64, (3, 3), activation="relu"))
model.add(layers.Maxpooling2D((2, 2)))

model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.Maxpooling2D((2, 2)))

model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.Maxpooling2D((2, 2)))

model.add(layers.Flatten())
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

开始训练

1
2
3
4
5
6
7
8
9
10
11
12
# 开始训练,此处使用了 fit_generator 方法,跟之前用的 fit 方法不同
# 它的不同之处在于,它接受生成器作为参数,而不是 numpy 数组
#
history = model.fit_generator(
train_generator,
steps_per_epoch=100,
epochs=30,
validation_data=validation_generator,
validation_steps=50
)
# 在训练完成后保存模型
model.save("path/to/model.h5")

数据增强

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 数据增强,可以通过对图片进行随机的变形,来增加训练的数据量
# 通过在实例化 Image 数据生成器时,引入更多参数来实现
# 之后通过这个实例化后的对象来处理图片时,会自动随机添加变形
datagen = ImageDataGenerator(
rotation_range=40,
width_shift_range=0.2,
height_shift_range=0.2,
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True,
fill_mode='nearest'
)

# 为了尽可能避免过拟合,还有一种方法是在展平层之后,添加 dropout 层,引入一些随机的噪音
# 强迫模型去学习噪音背后有用和真实存在的识别模式
model.add(layers.Flatten())
model.add(layers.Dropout(0.5))
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

使用预训练的卷积神经网络

深度学习的模型在本质天生具备高度的可复用性,这意味着,可以利用别人在大数据集上训练好的模型,做一些微调,来完成一些小数据集上面的任务;前提是该预训练的网络模型的原始数据集是足够大、足够通用的;而不是某种特定的任务;

1
2
# 使用预训练的模型
from keras.applications import VGG16

使用预训练网络的两种方法:特征提取、微调模型;

特征提取

  • 一般来说,一个训练好的卷积神经网络包含两个部分,一个是由卷积层和池化层组成的卷积基,一个是密集连接层组成的分类器;除非问题完全相同,不然一般只复用卷积基,而不复用分类器,因为分类器是面向特定问题的;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# 实例化模型,获得其卷积基(通过将 include_top 设置为 false 来实现,表示不复用顶层的分类器)
conv_base = VGG16(
weights='imagenet',
include_top=False,
input_shape=(150, 150, 3)
)

import os
import numpy as np
from keras.preprocessing.image import ImageDataGenerator
# 指定数据存储的目录
base_dir = '/downloads/cats_and_dogs_small'
train_dir = os.path.join(base_dir, "train")
validation_dir = os.path.join(base_dir, "validation")
test_dir = os.path.join(base_dir, "test")

# 此处实例化的数据生成器没有使用数据增强
datagen = ImageDataGenerator(rescale=1./255)
batch_size = 20

# 将图片做为输入,利用已训练好的模型的卷积基,获得计算后的特征(即输出)
def extract_features(directory, sample_count):
features = np.zeros(shape=(sample_count, 4, 4, 512))
# 现在处理的是一个二元分类问题,所以 labels 只有一维
labels = np.zeros(shape=(sample_count))
generator = datagen.flow_from_directory(
directory,
target_size=(150, 150),
batch_size=batch_size,
class_mode='binary'
)
i = 0
for inputs_batch, labels_batch in generator:
features_batch = conv_base.predict(inputs_batch)
features[i * batch_size : (i + 1) * batch_size] = features_batch
labels[i * batch_size : (i + 1) * batch_size] = labels_batch
i += 1
if i * batch_size >= sample_count:
break
return features, labels

# 从目录中提取图片,用卷积基进行计算,将结果保存下来
train_features, train_labels = extract_features(train_dir, 2000)
validation_features, validation_labels = extract_features(validation_dir, 1000)
test_features, test_labels = extract_features(test_dir, 1000)

# 以上获得的特征的形状是 (sample, 4, 4, 512),由于接下来要将这些
# 特征做为密集层的输入,因此需要将它们展开成二维的
train_features = np.reshape(train_features, (sample_count, (2000, 4 * 4 * 512)))
validation_features = np.reshape(validation_features, (sample_count, (2000, 4 * 4 * 512)))
test_features = np.reshape(test_features, (sample_count, (2000, 4 * 4 * 512)))

# 接下根据自身的业务场景,添加自己的密集层进行训练
model = models.Sequential()
model.add(layers.Dense(256, activation='relu', input_dim=4 * 4 * 512))
model.add(layers.Dropout(0.5))
model.add(layers.Dense(1, activation="sigmoid"))

model.compile(
optimizer=optimizers.RMSprop(1r=2e-5),
loss='binary_crossentropy',
metrics=["accuracy"]
)

history = model.fit(
train_features,
train_labels,
epochs=30,
batch_size=30,
validation_data=(validation_features, validation_labels)
)

如果特征提取想要使用数据增强(当样本数比较少时),则需要换一种方法:扩展卷积基;

这种方法的计算代价比较大,因为数据要流过整个卷积基,按模型训练的方式重新计算,而不是像前一种方法基于已有参数快速进行预测计算即可;

1
2
3
4
5
6
7
8
# 扩展卷积基
model = model.Sequential()
# 在将卷积基加上模型前,需要先对其进行冻结,避免训练过程中改变了它们的参数
conv_base.trainable = False
model.add(conv_base)
model.add(layers.Flatten())
model.add(layers.Dense(256, activation='relu')
model.add(layers.Dense(1, activation="sigmoid"))

微调模型

  • 同时,对于卷积基,越靠近输入端的那几层,其提取的特征通用性越好;越靠近输出的层,则越是面向特定分类的模式组成,越是定向化,通用性降低;因此,虽然也可以解决全部层进行重新训练,但更靠底部的层,训练回报越少;
微调步骤
  1. 复用预训练网络的整个卷积基,添加自己的分类器到模型中;
  2. 冻结卷积基,对分类器进行训练;
  3. 解冻顶部的一个卷积块,联合训练解决冻这些层和分类器;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 微调模型,解冻顶部的少数层
# 先将整个卷积基的 trainable 属性设置为 True
conv_base.trainable = True
# 指定将某个层的 trainable 属性设置为 True,其他仍为 Fasle
set_trainable = False
for layer in conv_base.layers:
if layer.name == 'block5_conv1':
set_trainable = True
if set_trainable:
layer.trainable = True
else:
layer.trainable = False

# 使用非常小的学习率开始训练模型
model.compile(
optimizer=optimizers.RMSprop(lr=1e-5),
loss="binary_crossentropy",
metrics=["accuracy"]
)

卷积神经网络的可视化

网络模型本质上是由层组成的,而每一层实际上又由多个过滤器组成;而过滤器本质上是一个有着特定参数的函数,它对输入数据进行计算,得到一个输出结果;该输出结果做出下一层的输入数据;

可视化网络模型,本质是就是可视化这些过滤器函数的功能;有三种观察它的方式:

  • 一种是给定输入,看它的输出(可视化中间激活)
  • 一种是看该函数得到最大值时的输入(可视化过滤器)
  • 一种是看涉及分类决策在原输入图中的部位(可视化类激活图)

可视化中间激活

层的输出一般称为激活(原因:层的输出即为激活函数的输出)

随着层数的增加,模型不断对输入图像进行特征提取并进行组合,因此,到了越高的层级,特征变得越来越抽象,越无法直观理解,但是与目标类别需的信息越来越接近;

实现方法:

  • 获取已训练好的模型的各层输出,组成一个输出列表
  • 创建一个新的模型实例,该实现以已训练好的模型的输入和输出列表为参数;
  • 用新模型对一张图片进行预测,得到输出结果列表;
  • 为每一层输出的每一个通道生成一张图像(为了让图片美观,此处会对数值进行标准化处理)

可视化卷积神经网络的过滤器

根据过滤器的参数,反向来计算让参数获得最大值的输入,从而知悉过滤器对什么样的模式产生响应;

实现方法:

  • 从模型中获取某一层的输出;
  • 使用 backend.mean 函数,计算该层输出的损失值;
  • 使用 backend.gradients 函数,计算损失相对模型原始输入的梯度;
  • 对梯度进行标准化(这样可以比较不同输入图像之间的计算结果);
  • 定义后端函数,它可以将输入的张量,转换为损失值张量和梯度值张量;
  • 初始化一张灰度图,并随机加入一些噪声;
  • 使用该灰度图做为初始输入值,用刚定义的后端函数进行计算损失值和梯度值张量;
  • 将梯度值添加到灰度图中,再重复上一个步骤,循环多次(例如40次),最后将得到一系列图像,该系列图像可最大化的激活对应通道的过滤器

可视化图像中类激活的热力图

图像上的不同部分,对最终分类决策重要程度不同,有些部分强相关,有些部分弱相关;假设已知输入图像对不同通道的激活强度,再加上每个通道对分类决策的重要程度,我们就可以求得输入图像的不同部分对分类决策的不同重要程度;

实现方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 选定一张待分类的图片,先进行预处理,以便可以做为模型的输入数据 
x = preprocess_input(image);
# 使用模型对图片进行预测分类,得到分类结果的输出;该输出是一个向量,由于每种类别的概率组成
preds = model.predict(x);
# 找到最大概率类型所在的下标
index = np.argmax(preds[0])
# 根据该下标,在模型预测向量中取得输入图像的相关输出
image_output = model.output(:, index);
# 从模型中取出最后一个卷积层
last_conv_layer = model.get_layer(layer_name);
# 计算图像的最终输出与最后一个卷积层的梯度
grads = K.gradients(image_output, last_conv_layer.ouput)[0]
# 计算梯度中每个通道的平均值
pooled_grads = K.mean(grads, axis=(0, 1, 2))
# 定义后端函数,它接受一个输入,给出 pooled_grads 和 last_conv_layer 的输出特征图
iterate = K.function([model.input], [pooled_grads, last_conv_layer.output[0])
# 计算输入的测试图像 x 的 pooled_grads_value 和 conv_layer_output_value
pooled_grads_value, conv_layer_output_value = iterate([x])
# 将特征数据的每个通道,乘以该通道对大象类型的重要程度
for i in range(512):
conv_layer_output_value[:, :, i] *= pooled_grads_value[i]
# 上一步得到的特征图,对每个通道求平均值,即可得到热力图
heatmap = np.mean(conv_layer_output_value, axis=-1)
# 为了方便查看,将热力图标准化处理
heatmap = np.maxmium(heatmap, 0)
heatmap /= np.max(heatmap)
# 将热力图叠加到原始图片上
import cv2
img = cv2.imread(img_path)
heatmap = cv2.resize(heatmap, img.shape[1], img.shape[0])
heatmap = np.uint8(255 * heatmap)
heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
superimposed_img = heatmap * 0.4 + img
cv2.imwrite(superimposed_img_path, superimposed_img)

Python 深度学习
https://ccw1078.github.io/2020/08/27/Python 深度学习/
作者
ccw
发布于
2020年8月27日
许可协议