AIGC实战——深度学习
0. 前言
深度学习 (Deep Learning
, DL
) 是贯穿所有生成模型 (Generative Model
) 的共同特征,几乎所有复杂的生成模型都以深度神经网络为核心,深度神经网络能够学习数据结构中的复杂关系,而不需要预先提取数据特征。在本节中,我们将介绍深度学习基本概念,并利用 Keras
构建深度神经网络。
1. 深度学习基本概念
1.1 基本定义
深度学习 (Deep Learning
, DL
) 是一类机器学习算法,使用多个堆叠的处理单元层从非结构化数据中学习高级表示。
为了充分理解深度学习,尤其是理解深度学习在生成建模中的重要性,我们将首先介绍深度学习中常见的非结构化数据,然后探讨如何构建多个堆叠的处理单元层解决分类任务。
1.2 非结构化数据
多数类型的机器学习算法需要结构化的表格数据作为输入,表格数据中的每一列都用于描述观测数据的特征。例如,一个人的年龄、收入和上个月购物网站访问等特征有助于预测此人在下个月网络购物的金额,可以使用这些特征的结构化表格来训练逻辑回归、随机森林或 XGBoost
等模型,并使用这些模型预测连续空间的响应变量(模型预测输出结果),即此人的网购消费金额。在此示例中,每个特征都包含观测数据的一部分信息,模型将学习这些特征如何相互作用以影响响应变量。
非结构化数据指的是不能自然地将特征组织成列形式的数据,例如图像、音频、文本和视频等。其中,图像具有空间结构,音频或文本具有时间结构,而视频数据兼具空间和时间结构,但由于数据不是以特征列的形式组织,因此称为是非结构化数据,如下图所示。
在非结构化数据中,单个像素、频率或字符几乎不含任何信息。例如,知道一张图像的第 3
行、第 4
列的像素是白色并不能帮助我们确定这张图像是一只狗还是一只猫,知道一个句子的第 5
个单词是字母 that
也不能帮助我们预测这段文本是关于电影还是体育的。
实际上,像素或字符只是嵌入到更高级信息特征(例如小狗图像或 “football
” 单词)的一部分。无论图像中的小狗被放置屋子里或马路边,图像仍然包含小狗,只不过该信息将由不同的像素传递;无论文本中的 “football
” 稍早或稍晚出现,该文本仍然是关于足球的,只是该信息将由不同的字符位置提供。数据的细粒度和高度的空间依赖性破坏了像素或字符作为独立信息特征的意义。
因此,如果我们对原始像素值训练逻辑回归、随机森林或 XGBoost
模型,则训练后的模型通常只能处理最简单的分类任务,而在其他较复杂的任务中通常表现不佳。这些模型依赖于输入特征具有的信息量且不具有空间依赖性。而深度学习模型可以直接从非结构化数据中自行学习如何构建高级信息特征。
深度学习也可以应用于结构化数据,但其真正的作用(尤其在生成模型领域)来自于其处理非结构化数据的能力。通常,我们希望生成非结构化数据,比如新的图像或文本,这就是为什么深度学习对于生成模型领域有如此深远的影响的原因。
2. 深度神经网络
目前,大部分深度学习系统通过堆叠多个隐藏层构建人工神经网络 (Artificial Neural Networks
, ANNs
)。因此,深度学习现在几乎已经成为了深度神经网络的同义词。但需要注意的是,任何使用多个层来学习输入数据的高级表示的系统都是深度学习(例如深度信念网络和深度玻尔兹曼机)。
首先,我们详细介绍神经网络的基本概念,然后了解它们如何用于从非结构化数据中学习高级特征。
2.1 神经网络
神经网络是由一系列堆叠的层组成的,每一层包含多个单位,这些单元通过一组权重与前一层的单位相连接。有多种不同类型的层,其中最常见的是全连接层 (fully connected layer
),也称密集层 (dense layer
),它将该层的所有单位直接连接到前一层的每个单位。所有相邻层都是全连接层的神经网络被称为多层感知器 (Multilayer Perceptron
, MLP
)。
输入(例如图像)依次通过网络的每一层进行传递直到达到输出层,这一过程称为网络的前向传播过程。具体而言,每个单元对其输入的加权求和结果应用非线性变换,并将输出传递到下一层。前向传播过程的最终输出层是该过程的最终结果,其中每个单元输出原始输入属于特定类别(例如微笑)的概率。
深度神经网络通过寻找每个层的权重集,以产生最准确的预测,寻找最优权重的过程就是网络训练 (training
)。
在训练过程中,图像会按批 (batch
) 传递给神经网络,然后将预测输出与实际结果进行比较。例如,网络可能对一个微笑的人脸图像输出 80%
的概率,对一个不微笑的人脸的图像输出 23%
的概率,而实际结果应输出 100%
和 0%
,因此预测输出和实际结果存在误差。预测中的误差会向后传播到整个神经网络中,并沿着能够最显著改善预测的方向微调网络权重,这个过程称为反向传播 (backpropagation
)。每个神经网络单元都能够逐渐识别出一个特定的特征,最终帮助神经网络做出更准确的预测。
2.2 学习高级特征
神经网络的关键特性是能够在没有人工指导的情况下从输入数据中学习特征。换句话说,我们不需要进行任何特征工程,仅仅通过最小化预测误差的指导,模型就可以自动学习最优权重。
例如,在上一小节介绍的神经网络中,假设它已经完成训练能够准确地预测给定输入面部是否微笑:
- 单元
A
接收输入像素值 - 单元
B
将其输入值组合起来,当某个低级特征(如边缘)存在时,它会被激活 - 单元
C
与单元D
等将低级特征组合在一起,当图像中出现更高级别的特征(如眼睛)时,它会被激活 - 单元
E
将高级特征组合在一起,当图像中的人物出现微笑时,它会被激活
每个网络层中的单元能够通过组合前一层的低级特征表示学习到原始输入的复杂特征,这是在网络训练过程自动学习的,我们不需要告诉每个单元要学习什么,或者它要学习高级特征还是低级特征。
输入层和输出层之间的层被称为隐藏层 (hidden layers
),深度神经网络可以拥有任意数量的隐藏层。通过堆叠大量的网络层,神经网络可以逐渐从浅层的低级特征中捕捉信息,学习越来越高级别的特征。例如,ResNet
是设计用于图像识别的深度神经网络,包含 152
层。
接下来,我们将深入介绍深度学习中的常用技术,并使用 TensorFlow
和 Keras
库构建深度神经网络。
3. TensorFlow 和 Keras
TensorFlow
是由 Google
开发的用于机器学习的开源 Python
库。TensorFlow
是构建机器学习解决方案最常用的框架之一,其因张量 (tensor
) 操作而得名。在深度学习中,张量只是存储流经神经网络的数据的多维数组。它提供了训练神经网络所需的底层功能,例如高效计算任意可微表达式的梯度和执行张量操作。
Keras
是建立在 TensorFlow
上的高级 API
,它非常灵活且拥有非常易于使用的 API
,是入门深度学习的理想选择。此外,Keras
通过其函数式 API
提供了许多有用的构建模块,可以通过将它们连接在一起来创建复杂的深度学习架构。
使用 TensorFlow
和 Keras
组合能够在生产环境中构建复杂网络,可以快速实现所设计的网络模型。接下来,我们使用 Keras
构建多层感知器(Multilayer Perceptron
,MLP
)。
4. 多层感知器 (MLP)
在本节中,我们将使用监督学习训练一个 MLP
来对指定图像进行分类。监督学习是一种机器学习算法,通过带有标记的数据集进行训练,监督学习仍然在许多类型的生成模型中扮演重要角色。换句话说,用于训练的数据集包括带有输入数据相应的输出标签。算法的目标是学习输入数据和输出标签之间的映射关系,以便它可以对新的、未见过的数据进行预测。
4.1 准备数据
在本节中,我们将使用 CIFAR-10
数据集,这是一个 Keras
内置的数据集,其包含 60,000
张 32×32
像素的彩色图像,每张图片都被分类到其所属的类别(共有 10
个类别)。
默认情况下,图像数据中每个像素的默认值为在 0
到 255
之间的整数。我们首先需要通过将这些值缩放到 0
到 1
之间来预处理图像,因为神经网络在每个输入的绝对值小于 1
时效果最好。
我们还需要将图像的整数标签转换为独热编码 (one-hot
) 向量,因为神经网络的输出是图像属于每个类别的概率。如果图像的类别整数标签是 i
,则它的独热编码是长度为 10
(类别数量)的向量,除第 i
个元素值为 1
外,其他所有位置上元素值都为 0
。
(1) 导入所需库:
import numpy as np
import matplotlib.pyplot as plt
from keras import layers, models, optimizers, utils, datasets
NUM_CLASSES = 10
(2) 加载 CIFAR-10
数据集,其中 x_train
和 x_test
分别是形状为 [50000, 32, 32, 3]
和 [10000, 32, 32, 3]
的 numpy
数组,y_train
和 y_test
分别是形状为 [50000, 1]
和 [10000, 1]
的 numpy
数组,包含每个图像的类别标签,范围在 0
到 9
之间:
(x_train, y_train), (x_test, y_test) = datasets.cifar10.load_data()
(3) 对每张图像进行缩放,使得像素值介于 0
和 1
之间:
x_train = x_train.astype("float32") / 255.0
x_test = x_test.astype("float32") / 255.0
(4) 对标签进行独热编码,修改后的 y_train
和 y_test
的形状分别为 [50000, 10]
和 [10000, 10]
:
y_train = utils.to_categorical(y_train, NUM_CLASSES)
y_test = utils.to_categorical(y_test, NUM_CLASSES)
训练图像数据 (x_train
) 存储在形状为 [50000, 32, 32, 3]
的张量中。这个数据集没有列或行的概念,而是一个具有四个维度的张量,张量只是一个多维数组,它是矩阵超过两个维度的扩展。这个张量的第一个维度引用了数据集中图像的索引,第二和第三个维度与图像的尺寸相关,最后一个维度是通道(对于 RGB
图像而言是红色、绿色或蓝色通道)。使用以下方式可以获取图像中指定像素的通道值:
# 获取第100个图像中像素 (10,10) 的绿色通道值
print(x_train[100, 10, 10, 1])
4.2 构建模型
在 Keras
中,可以使用 Sequential
模型或函数 API
来定义神经网络的结构。
Sequential
模型用于快速定义一系列线性堆叠层(即一层直接紧随着下一层,没有任何分支)。我们可以使用 Sequential
类来定义 MLP
模型:
model = models.Sequential([
layers.Flatten(input_shape=(32,32,3)),
layers.Dense(200, activation='relu'),
layers.Dense(150, activation='relu'),
layers.Dense(10, activation='softmax'),
])
但许多模型需要从一个层传递输出到多个后续层,或者一个层接收其上多个层的输入。对于这些模型,Sequential
类就不再适用,我们需要使用更加灵活的函数 API
。随着神经网络的架构变得越来越复杂,函数 API
将能够更好的满足需求,给予我们随心所欲设计深度神经网络的自由。
接下来,我们使用使用函数 API
构建相同 MLP
,在使用函数 API
时,使用 Model
类来定义模型的整体输入和输出层:
input_layer = layers.Input((32, 32, 3))
x = layers.Flatten()(input_layer)
x = layers.Dense(200, activation="relu")(x)
x = layers.Dense(150, activation="relu")(x)
output_layer = layers.Dense(NUM_CLASSES, activation="softmax")(x)
model = models.Model(input_layer, output_layer)
使用以上两种方法可以得到相同的模型:
神经网络层
为了构建 MLP
,我们使用了三种不同类型的层:Input
、Flatten
和 Dense
。接下来,我们详细地介绍在 MLP
中使用的不同层和激活函数。
Input
层是神经网络的入口,以元组形式告诉网络每个数据元素的形状,不需要显式地指定批大小,因为我们可以同时传递任意数量的图像到 Input
层。
接下来,使用 Flatten
层将输入展平成一个向量,得到一个长度为 32 × 32 × 3=3,072
的向量。这是由于后续的 Dense
层要求其输入是一维的,而不是多维数组。而有些类型的层需要多维数组作为输入,因此需要了解每种类型的层所需的输入和输出形状,以理解何时需要使用 Flatten
。
Dense
层是神经网络中最基本的层类型之一。它包含给定数量的单元,该层中的每个单元与前一层中的每个单元通过一个权重连接。给定单元的输出是它从前一层接收到的输入的加权和,然后通过非线性激活函数传递给下一层。激活函数用于确保神经网络能够学习复杂函数,而不仅仅输出其输入的线性组合。
激活函数
有多种类型的激活函数,我们主要介绍以下三种激活函数:ReLU
、sigmoid
和 softmax
。
ReLU
(Rectified Linear Unit
) 激活函数在输入为负时定义为 0
,否则等于输入值。LeakyReLU
激活函数与 ReLU
非常相似,关键区别在于:ReLU
激活函数对于小于 0
的输入值返回 0
,而 LeakyReLU
函数返回与输入成比例的一个小的负数。如果 ReLU
单元始终输出 0
,则梯度同样为 0
,因此误差无法通过该单元反向传播。LeakyReLU
激活通过始终确保梯度非零来解决此问题。基于 ReLU
的函数是在深度网络的层之间使用的最可靠的激活函数之一,可以确保模型稳定的训练。
如果我们希望某一层的输出介于 0
到 1
之间,则可以使用 sigmoid
激活函数,例如用于具有一个输出单元的二分类问题。下图为 ReLU
、LeakyReLU
和 sigmoid
激活函数的函数图像:
如果我们希望某一网络层的输出总和等于 1
,则可以使用 softmax
激活函数,例如在多类别分类问题中,每个观测样本只属于一个类别,函数定义如下:
y
i
=
e
x
i
∑
j
=
1
J
e
x
j
y_i = \frac {e^{x_i}}{∑_{j=1}^J e^{x_j}}
yi=∑j=1Jexjexi
其中,
J
J
J 是层中的单元总数。在上述构建的神经网络中,我们在最后一层使用 softmax
激活函数,以确保输出包含 10
个概率值,这些概率值相加为 1
,可表示图像属于每个类别的可能性。
在 Keras
中,可以在层内定义激活函数或将其作为单独的层定义:
# 方法1
x = layers.Dense(units=200, activation = 'relu')(x)
# 方法2
x = layers.Dense(units=200)(x)
x = layers.Activation('relu')(x)
在以上代码中,我们将输入通过两个 Dense
层,第一层有 200
个单元,第二层有 150
个单元,两层均使用 ReLU
激活函数。
4.3 检查模型
我们可以使用 model.summary()
方法来检查神经网络每一层的形状:
print(model.summary())
可以看到,输入层 Input
的形状与 x_train
的形状相匹配,而输出层的形状与 y_train
的形状匹配。Keras
使用 None
作为标记,表示它尚不明确将传递给网络的观测样本数量。实际上,它也不需要知道,因为一次只传递 1
个观测样本对象或 1000
个观测样本对象给网络是一样的。这是由于张量操作使用线性代数在所有观测样本对象上同时进行。这也是为什么在 GPU
上训练深度神经网络比 CPU
上训练可以提高效率的原因之一,因为 GPU
针对大型张量操作进行了优化。
summary
方法还会给出每个层中需要进行训练的参数(权重)数量。如果模型训练速度过慢,可以使用 summary
查看是否有包含大量权重的层。如果有此类层,可以考虑减少层中的单元数量以加速训练。
最后,我们介绍如何计算每个层中的参数数量。默认情况下,给定层中的每个单元会连接到一个额外的偏置单元,用于确保即使来自前一层的所有输入为 0
,单元的输出仍然可以是非零值。因此,在一个包含 200
个单元的 Dense
层中(假设输入像素数为 3072
),参数数量为 200 * (3072 + 1) = 614,600
。
4.4 编译模型
使用优化器和损失函数编译模型:
opt = optimizers.Adam(learning_rate=0.0005)
model.compile(
loss="categorical_crossentropy", optimizer=opt, metrics=["accuracy"]
)
接下来,我们详细介绍损失函数和优化器。
损失函数
损失函数 (loss function
) 用于神经网络将其预测输出与真实值进行比较。它为每个观测样本对象返回一个值,值越大,表明神经网络针对该观测样本对象所得出的输出结果就越差。
Keras
提供了许多内置的损失函数可供选择,我们也可以创建自定义损失函数。常用的损失函数包括均方误差 (mean squared error
)、分类交叉熵 (categorical cross-entropy
) 和二元交叉熵 (binary cross-entropy
)。我们需要根据实际问题选择合适的损失函数。
如果神经网络旨在解决回归问题(即输出是连续值),那么可以使用均方误差损失。均方误差是每个输出单元的实际值
y
i
y_i
yi 与预测值
p
i
p_i
pi 之差的平方的均值,其中均值是在所有
n
n
n 个输出单元上进行计算:
M
S
E
=
1
n
Σ
i
=
1
n
(
y
i
−
p
i
)
2
MSE = \frac1 n Σ_{i=1}^n {(y_i - p_i)^2}
MSE=n1Σi=1n(yi−pi)2
如果需要处理分类问题,其中每个观测样本对象只属于一个类别,那么可以使用分类交叉熵是作为损失函数。其定义如下:
−
Σ
i
=
1
n
y
i
l
o
g
(
p
i
)
-Σ_{i=1}^n{y_ilog(p_i)}
−Σi=1nyilog(pi)
如果需要处理具有一个输出单元的二分类问题,或者每个观测样本可以同时属干多个类别的多分类问题,则应该使用二元交叉熵:
−
1
n
Σ
i
=
1
n
(
y
i
l
o
g
(
p
i
)
+
(
1
−
y
i
)
l
o
g
(
1
−
p
i
)
)
-\frac 1 n Σ_{i=1}^n {(y_ilog(p_i) + (1-y_i)log(1-p_i))}
−n1Σi=1n(yilog(pi)+(1−yi)log(1−pi))
优化器
优化器 (optimizer
) 是根据损失函数的梯度来更新神经网络权重的算法。Adam
是最常用且稳定的优化器之一,在大多数情况下,除了学习率 (learning rate
) 外,我们不需要调整 Adam
优化器的默认参数。学习率越大,每个训练步骤中权重的改变就越大。虽然使用较大的学习率在初始时训练速度更快,但缺点是可能导致训练不稳定,并且可能无法找到损失函数的全局最小值。因此需要在训练过程中调整该参数。
另一个常见的优化器是 RMSProp
,同样,我们不需要过多调整此优化器的默认参数。
我们将损失函数和优化器都传递给模型的 compile
方法,并通过 metrics
参数指定我们在训练期间希望监测的其他指标,例如准确率。
4.5 训练模型
到目前为止,我们还没有向模型传递任何数据,只是构建了模型架构,并使用损失函数和优化器编译了模型。要传递数据训练模型,只需调用 fit
方法:
model.fit(x_train, y_train, batch_size=32, epochs=20, shuffle=True)
其中,x_train
表示原始图像数据,y_train
表示 one-hot
编码的类别标签,batch_size
决定每个训练步骤中将传递给网络的观测样本数量,epochs
决定将完整的训练数据展示给网络的次数,shuffle = True
表示每个训练步骤中都会以无放回的方式从训练数据中随机抽取批数据。
训练深度神经网络,用于预测 CIFAR-10
数据集中的图像类别。训练过程如下:
首先,将神经网络的权重初始化为很小的随机值。然后,网络执行一系列的训练步骤。在每个训练步骤中,通过网络传递一批图像,并进行误差反向传播以更新权重。batch_size
决定每个训练步骤的批数据中包含多少图像。批大小越大,梯度计算越稳定,但每个训练步骤速度越慢。如果在每个训练步骤中都使用整个数据集计算梯度,将非常耗时且计算量过大,因此通常将批大小设置在 32
到 256
之间。
这个过程一直持续到数据集中的所有观测样本都被处理过 1
次为止,这样就完成了第一个 epoch
。然后在第二个 epoch
中,数据再次以批的形式通过网络进行传递。重复以上过程,直到完整指定的 epoch
次数。
在训练过程中,Keras
输出训练过程的进展情况,如下图所示。可以看到训练数据集被分成 1,563
个批(每批包含 32
个图像),并且它已经在网络中展示了 10
次(即经历了 10
个 epoch
),每批的处理速率约为 2
毫秒。分类交叉熵损失从 1.8534
下降到 1.2557
,准确率从 33.20%
增加到 55.20%
。
4.6 评估模型
该模型在训练集上的准确率为 55.50%
,为了评估模型在从未见过的数据上表现,可以使用 Keras
提供的 evaluate
方法:
model.evaluate(x_test, y_test)
# 313/313 [==============================] - 0s 826us/step - loss: 1.3732 - accuracy: 0.5118
输出是我们监测的指标列表:分类交叉熵和准确率。可以看到,即使对于从未见过的图像,该模型的准确率仍然为 51.18%
,考虑到我们仅仅使用了一个非常基本的神经网络,49.0%
可以说是一个很好的结果。
可以使用 predict
方法查看测试集上的预测结果:
CLASSES = np.array(
[
"airplane",
"automobile",
"bird",
"cat",
"deer",
"dog",
"frog",
"horse",
"ship",
"truck",
]
)
preds = model.predict(x_test)
preds_single = CLASSES[np.argmax(preds, axis=-1)]
actual_single = CLASSES[np.argmax(y_test, axis=-1)]
preds
是一个形状为 [10000, 10]
的数组,即每个观测样本对应一个包含 10
个类别概率的向量。
使用 numpy
的 argmax
函数将这个概率数组转换回单个预测结果。其中,使用 axis = -1
指定数组在最后一个维度(即类别维度)上压缩,因此 preds_single
的形状为 [10000, 1]
。
我们可以使用以下代码查看图片以及它们的标签和预测结果:
n_to_show = 10
indices = np.random.choice(range(len(x_test)), n_to_show)
fig = plt.figure(figsize=(15, 3))
fig.subplots_adjust(hspace=0.4, wspace=0.4)
for i, idx in enumerate(indices):
img = x_test[idx]
ax = fig.add_subplot(1, n_to_show, i + 1)
ax.axis("off")
ax.text(
0.5,
-0.35,
"pred = " + str(preds_single[idx]),
fontsize=10,
ha="center",
transform=ax.transAxes,
)
ax.text(
0.5,
-0.7,
"act = " + str(actual_single[idx]),
fontsize=10,
ha="center",
transform=ax.transAxes,
)
ax.imshow(img)
plt.show()
下图展示了模型的对随机选择数据的预测结果及其真实标签:
小结
深度学习是一种机器学习算法,它通过构建多层神经网络模型,自动地从输入数据中进行特征学习,并输出对应的预测结果。深度神经网络是构建生成模型的基础,几乎所有复杂的生成模型都以深度神经网络为核心。构建生成模型时,其核心思想在于正确组合损失函数、激活函数和层形状等。在本节中,介绍了生成模型与深度学习之间的联系,以及深度神经网络模型中的各种基本组件,并使用 Keras
构建了一个简单的多层感知器。
系列链接
AIGC实战——生成模型简介
AIGC实战——卷积神经网络(Convolutional Neural Network, CNN)
AIGC实战——自编码器(Autoencoder)
AIGC实战——变分自编码器(Variational Autoencoder, VAE)
AIGC实战——使用变分自编码器生成面部图像
AIGC实战——生成对抗网络(Generative Adversarial Network, GAN)
AIGC实战——WGAN(Wasserstein GAN)
AIGC实战——条件生成对抗网络(Conditional Generative Adversarial Net, CGAN)
AIGC实战——自回归模型(Autoregressive Model)
AIGC实战——改进循环神经网络
AIGC实战——像素卷积神经网络(PixelCNN)
AIGC实战——归一化流模型(Normalizing Flow Model)
AIGC实战——能量模型(Energy-Based Model)