Tensorflow模型构建与训练
本章介绍如何使用 TensorFlow 快速搭建动态模型。
- 模型的构建:
tf.keras.Model
和tf.keras.layers
- 模型的损失函数:
tf.keras.losses
- 模型的优化器:
tf.keras.optimizer
- 模型的评估:
tf.keras.metrics
前置知识
- Python -{zh-hant: 物件導向;zh-hans: 面向對象;}- 编程 (在 Python 内定义类和方法、类的继承、构造和析构函数,使用 super () 函数调用父类方法 ,使用call() 方法对实例进行调用 等);
- 多层感知机、卷积神经网络、循环神经网络和强化学习(每节之前给出参考资料)。
- Python 的函数装饰器 (非必须)
模型构建
Keras构建模型
Keras 是一个广为流行的高级神经网络 API,简单、快速而不失灵活性,现已得到 TensorFlow 的官方内置和全面支持,推荐使用。
模型和层是Keras里两个重要的概念:
- 层(Layer):将各种计算流程和变量进行了封装(例如基本的全连接层,CNN 的卷积层、池化层等)。
- 模型(Model):将各种层进行组织和连接,并封装成一个整体,描述了如何将输入数据通过各种层以及运算而得到输出。
使用 y_pred = model(x) 的形式进行模型调用,tf.keras.layers 内置了深度学习中大量常用的的预定义层,同时也允许我们自定义层。
Keras 模型以类的形式呈现,我们可以通过继承 tf.keras.Model
这个 Python 类来定义自己的模型。在继承类中,我们需要重写 __init__()
(构造函数,初始化)和 call(input)
(模型调用)两个方法,同时也可以根据需要增加自定义的方法。
class MyModel(tf.keras.Model):
def __init__(self):
super().__init__() # Python 2 下使用 super(MyModel, self).__init__()
# 此处添加初始化代码(包含 call 方法中会用到的层),例如
# layer1 = tf.keras.layers.BuiltInLayer(...)
# layer2 = MyCustomLayer(...)
def call(self, input):
# 此处添加模型调用的代码(处理输入并返回输出),例如
# x = layer1(input)
# output = layer2(x)
return output
# 还可以添加自定义的方法
继承 tf.keras.Model
后,我们同时可以使用父类的若干方法和属性,例如在实例化类 model = Model()
后,可以通过 model.variables
这一属性直接获得模型中的所有变量,免去我们一个个显式指定变量的麻烦。
线性模型 y_pred = a * X + b
,我们可以通过模型类的方式编写如下:
import tensorflow as tf
X = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
y = tf.constant([[10.0], [20.0]])
class Linear(tf.keras.Model): # 建立了一个继承了 tf.keras.Model 的模型类 Linear
def __init__(self):
super().__init__()
self.dense = tf.keras.layers.Dense( # 实例化了一个 全连接层
units=1, # 输出张量的维度
activation=None, # 激活函数,默认为无激活函数 a(x) = x
kernel_initializer=tf.zeros_initializer(), # 权重矩阵kernel的初始化器,默认为tf.glorot_uniform_initializer
bias_initializer=tf.zeros_initializer() # 偏置向量bias的初始化器,默认为tf.glorot_uniform_initializer
)
def call(self, input): # 对 全连接层 进行调用,实现了线性变换的计算
output = self.dense(input)
return output
model = Linear()
optimizer = tf.keras.optimizers.SGD(learning_rate=0.01)
for i in range(100):
with tf.GradientTape() as tape:
y_pred = model(X) # 调用模型 y_pred = model(X) 而不是显式写出 y_pred = a * X + b
loss = tf.reduce_mean(tf.square(y_pred - y))
grads = tape.gradient(loss, model.variables) # 使用 model.variables 这一属性直接获得模型中的所有变量
optimizer.apply_gradients(grads_and_vars=zip(grads, model.variables))
print(model.variables)
这里,我们没有显式地声明 a
和 b
两个变量并写出 y_pred = a * X + b
这一线性变换,而是建立了一个继承了 tf.keras.Model
的模型类 Linear
。这个类在初始化部分实例化了一个 全连接层 ( tf.keras.layers.Dense
),并在 call 方法中对这个层进行调用,实现了线性变换的计算。如果需要显式地声明自己的变量并使用变量进行自定义运算,或者希望了解 Keras 层的内部原理,请参考 自定义层。
Keras 的全连接层:线性变换 + 激活函数
全连接层 (Fully-connected Layer,
tf.keras.layers.Dense
)是 Keras 中最基础和常用的层之一,对输入矩阵进行
的线性变换 + 激活函数操作。如果不指定激活函数,即是纯粹的线性变换
。具体而言,给定输入张量
input = [batch_size, input_dim]
,该层对输入张量首先进行tf.matmul(input, kernel) + bias
的线性变换(kernel
和bias
是层中可训练的变量),然后对线性变换后张量的每个元素通过激活函数activation
,从而输出形状为[batch_size, units]
的二维张量。
其包含的主要参数如下:
units
:输出张量的维度;activation
:激活函数,对应于中的
,默认为无激活函数(
a(x) = x
)。常用的激活函数包括tf.nn.relu
、tf.nn.tanh
和tf.nn.sigmoid
;use_bias
:是否加入偏置向量bias
,即中的
。默认为
True
;kernel_initializer
、bias_initializer
:权重矩阵kernel
和偏置向量bias
两个变量的初始化器。默认为tf.glorot_uniform_initializer
1 。设置为tf.zeros_initializer
表示将两个变量均初始化为全 0;该层包含权重矩阵
kernel = [input_dim, units]
和偏置向量bias = [units]
2 两个可训练变量,对应于中的
和
。
这里着重从数学矩阵运算和线性变换的角度描述了全连接层。基于神经元建模的描述可参考 后文介绍 。
Keras 中的很多层都默认使用
tf.glorot_uniform_initializer
初始化变量,关于该初始化器可参考 https://www.tensorflow.org/api_docs/python/tf/glorot_uniform_initializer 。- 你可能会注意到,
tf.matmul(input, kernel)
的结果是一个形状为[batch_size, units]
的二维矩阵,这个二维矩阵要如何与形状为[units]
的一维偏置向量bias
相加呢?事实上,这里是 TensorFlow 的 Broadcasting 机制在起作用,该加法运算相当于将二维矩阵的每一行加上了Bias
。Broadcasting 机制的具体介绍可见 https://www.tensorflow.org/xla/broadcasting 。为什么模型类是重载
call()
方法而不是__call__()
方法?在 Python 中,对类的实例
myClass
进行形如myClass()
的调用等价于myClass.__call__()
(具体请见本章初 “前置知识” 的__call__()
部分)。那么看起来,为了使用y_pred = model(X)
的形式调用模型类,应该重写__call__()
方法才对呀?原因是 Keras 在模型调用的前后还需要有一些自己的内部操作,所以暴露出一个专门用于重载的call()
方法。tf.keras.Model
这一父类已经包含__call__()
的定义。__call__()
中主要调用了call()
方法,同时还需要在进行一些 keras 的内部操作。这里,我们通过继承tf.keras.Model
并重载call()
方法,即可在保持 keras 结构的同时加入模型调用的代码。
基础示例:多层感知机(MLP)
我们从编写一个最简单的 多层感知机 (Multilayer Perceptron, MLP),或者说 “多层全连接神经网络” 开始,介绍 TensorFlow 的模型编写方式。在这一部分,我们依次进行以下步骤:
-
使用
tf.keras.datasets
获得数据集并预处理 -
使用
tf.keras.Model
和tf.keras.layers
构建模型 -
构建模型训练流程,使用
tf.keras.losses
计算损失函数,并使用tf.keras.optimizer
优化模型 - 构建模型评估流程,使用
tf.keras.metrics
计算评估指标
基础知识和原理
UFLDL 教程 Multi-Layer Neural Network 一节;
- 斯坦福课程 CS231n: Convolutional Neural Networks for Visual Recognition 中的 “Neural Networks Part 1 ~ 3” 部分。
这里,我们使用多层感知机完成 MNIST 手写体数字图片数据集 [LeCun1998] 的分类任务。
MNIST 手写体数字图片示例:
数据获取及预处理
tf.keras.datasets
先进行预备工作,实现一个简单的 MNISTLoader
类来读取 MNIST 数据集数据。这里使用了 tf.keras.datasets
快速载入 MNIST 数据集。
class MNISTLoader():
def __init__(self):
mnist = tf.keras.datasets.mnist
(self.train_data, self.train_label), (self.test_data, self.test_label) = mnist.load_data()
# MNIST中的图像默认为uint8(0-255的数字)。以下代码将其归一化到0-1之间的浮点数,并在最后增加一维作为颜色通道
self.train_data = np.expand_dims(self.train_data.astype(np.float32) / 255.0, axis=-1) # [60000, 28, 28, 1]
self.test_data = np.expand_dims(self.test_data.astype(np.float32) / 255.0, axis=-1) # [10000, 28, 28, 1]
self.train_label = self.train_label.astype(np.int32) # [60000]
self.test_label = self.test_label.astype(np.int32) # [10000]
self.num_train_data, self.num_test_data = self.train_data.shape[0], self.test_data.shape[0]
def get_batch(self, batch_size):
# 从数据集中随机取出batch_size个元素并返回
index = np.random.randint(0, self.num_train_data, batch_size)
return self.train_data[index, :], self.train_label[index]
提示
mnist = tf.keras.datasets.mnist
将从网络上自动下载 MNIST 数据集并加载。如果运行时出现网络连接错误,可以从 https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz 或 https://s3.amazonaws.com/img-datasets/mnist.npz 下载 MNIST 数据集mnist.npz
文件,并放置于用户目录的.keras/dataset
目录下(Windows 下用户目录为C:\Users\用户名
,Linux 下用户目录为/home/用户名
)。TensorFlow 的图像数据表示
在 TensorFlow 中,图像数据集的一种典型表示是
[图像数目,长,宽,色彩通道数]
的四维张量。在上面的DataLoader
类中,self.train_data
和self.test_data
分别载入了 60,000 和 10,000 张大小为28*28
的手写体数字图片。由于这里读入的是灰度图片,色彩通道数为 1(彩色 RGB 图像色彩通道数为 3),所以我们使用np.expand_dims()
函数为图像数据手动在最后添加一维通道。
模型的构建
tf.keras.Model
和tf.keras.layers
多层感知机的模型类实现与上面的线性模型类似,使用 tf.keras.Model
和 tf.keras.layers
构建,所不同的地方在于层数增加了(顾名思义,“多层” 感知机),以及引入了非线性激活函数(这里使用了 ReLU 函数 , 即下方的 activation=tf.nn.relu
)。该模型输入一个向量(比如这里是拉直的 1×784
手写体数字图片),输出 10 维的向量,分别代表这张图片属于 0 到 9 的概率。
class MLP(tf.keras.Model):
def __init__(self):
super().__init__()
self.flatten = tf.keras.layers.Flatten() # Flatten层将除第一维(batch_size)以外的维度展平
self.dense1 = tf.keras.layers.Dense(units=100, activation=tf.nn.relu)
self.dense2 = tf.keras.layers.Dense(units=10)
def call(self, inputs): # [batch_size, 28, 28, 1]
x = self.flatten(inputs) # [batch_size, 784]
x = self.dense1(x) # [batch_size, 100]
x = self.dense2(x) # [batch_size, 10]
output = tf.nn.softmax(x)
return output
softmax 函数
这里,因为我们希望输出 “输入图片分别属于 0 到 9 的概率”,也就是一个 10 维的离散概率分布,所以我们希望这个 10 维向量至少满足两个条件:
- 该向量中的每个元素均在
之间;
- 该向量的所有元素之和为 1。
为了使得模型的输出能始终满足这两个条件,我们使用 Softmax 函数 (归一化指数函数,
tf.nn.softmax
)对模型的原始输出进行归一化。其形式为。不仅如此,softmax 函数能够凸显原始向量中最大的值,并抑制远低于最大值的其他分量,这也是该函数被称作 softmax 函数的原因(即平滑化的 argmax 函数)。
模型的训练
tf.keras.losses
和tf.keras.optimizer
定义一些模型超参数:
num_epochs = 5
batch_size = 50
learning_rate = 0.001
实例化模型和数据读取类,并实例化一个 tf.keras.optimizer
的优化器(这里使用常用的 Adam 优化器):
model = MLP()
data_loader = MNISTLoader()
optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate)
然后迭代进行以下步骤:
- 从 DataLoader 中随机取一批训练数据;
- 将这批数据送入模型,计算出模型的预测值;
- 将模型预测值与真实值进行比较,计算损失函数(loss)。这里使用
tf.keras.losses
中的交叉熵函数作为损失函数; - 计算损失函数关于模型变量的导数;
- 将求出的导数值传入优化器,使用优化器的
apply_gradients
方法更新模型参数以最小化损失函数(优化器的详细使用方法见 前章 )。
具体代码实现如下:
num_batches = int(data_loader.num_train_data // batch_size * num_epochs)
for batch_index in range(num_batches):
X, y = data_loader.get_batch(batch_size)
with tf.GradientTape() as tape:
y_pred = model(X)
loss = tf.keras.losses.sparse_categorical_crossentropy(y_true=y, y_pred=y_pred)
loss = tf.reduce_mean(loss)
print("batch %d: loss %f" % (batch_index, loss.numpy()))
grads = tape.gradient(loss, model.variables)
optimizer.apply_gradients(grads_and_vars=zip(grads, model.variables))
交叉熵(cross entropy)与
tf.keras.losses
你或许注意到了,在这里,我们没有显式地写出一个损失函数,而是使用了
tf.keras.losses
中的sparse_categorical_crossentropy
(交叉熵)函数,将模型的预测值y_pred
与真实的标签值y
作为函数参数传入,由 Keras 帮助我们计算损失函数的值。交叉熵作为损失函数,在分类问题中被广泛应用。其离散形式为
,其中
为真实概率分布,
为预测概率分布,
为分类任务的类别个数。预测概率分布与真实分布越接近,则交叉熵的值越小,反之则越大。更具体的介绍及其在机器学习中的应用可参考 这篇博客文章 。
在
tf.keras
中,有两个交叉熵相关的损失函数tf.keras.losses.categorical_crossentropy
和tf.keras.losses.sparse_categorical_crossentropy
。其中 sparse 的含义是,真实的标签值y_true
可以直接传入 int 类型的标签类别。具体而言:loss = tf.keras.losses.sparse_categorical_crossentropy(y_true=y, y_pred=y_pred)
与
loss = tf.keras.losses.categorical_crossentropy( y_true=tf.one_hot(y, depth=tf.shape(y_pred)[-1]), y_pred=y_pred )
的结果相同。
模型的评估
tf.keras.metrics
最后,我们使用测试集评估模型的性能。这里,我们使用 tf.keras.metrics
中的 SparseCategoricalAccuracy
评估器来评估模型在测试集上的性能,该评估器能够对模型预测的结果与真实结果进行比较,并输出预测正确的样本数占总样本数的比例。我们迭代测试数据集,每次通过 update_state()
方法向评估器输入两个参数: y_pred
和 y_true
,即模型预测出的结果和真实结果。评估器具有内部变量来保存当前评估指标相关的参数数值(例如当前已传入的累计样本数和当前预测正确的样本数)。迭代结束后,我们使用 result()
方法输出最终的评估指标值(预测正确的样本数占总样本数的比例)。
在以下代码中,我们实例化了一个 tf.keras.metrics.SparseCategoricalAccuracy
评估器,并使用 For 循环迭代分批次传入了测试集数据的预测结果与真实结果,并输出训练后的模型在测试数据集上的准确率。
sparse_categorical_accuracy = tf.keras.metrics.SparseCategoricalAccuracy()
num_batches = int(data_loader.num_test_data // batch_size)
for batch_index in range(num_batches):
start_index, end_index = batch_index * batch_size, (batch_index + 1) * batch_size
y_pred = model.predict(data_loader.test_data[start_index: end_index])
sparse_categorical_accuracy.update_state(y_true=data_loader.test_label[start_index: end_index], y_pred=y_pred)
print("test accuracy: %f" % sparse_categorical_accuracy.result())
输出结果:
test accuracy: 0.947900
可以注意到,使用这样简单的模型,已经可以达到 95% 左右的准确率。
神经网络的基本单位:神经元。
如果我们将上面的神经网络放大来看,详细研究计算过程,比如取第二层的第 k 个计算单元,可以得到示意图如下:
该计算单元
有 100 个权值参数
和 1 个偏置参数
。将第 1 层中所有的 100 个计算单元
的值作为输入,分别按权值
加和(即
),并加上偏置值
,然后送入激活函数
进行计算,即得到输出结果。
事实上,这种结构和真实的神经细胞(神经元)类似。神经元由树突、胞体和轴突构成。树突接受其他神经元传来的信号作为输入(一个神经元可以有数千甚至上万树突),胞体对电位信号进行整合,而产生的信号则通过轴突传到神经末梢的突触,传播到下一个(或多个)神经元。
神经细胞模式图(修改自 Quasar Jarosz at English Wikipedia [CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0)])
上面的计算单元,可以被视作对神经元结构的数学建模。在上面的例子里,第二层的每一个计算单元(人工神经元)有 100 个权值参数和 1 个偏置参数,而第二层计算单元的数目是 10 个,因此这一个全连接层的总参数量为 100*10 个权值参数和 10 个偏置参数。事实上,这正是该全连接层中的两个变量
kernel
和bias
的形状。仔细研究一下,你会发现,这里基于神经元建模的介绍与上文基于矩阵计算的介绍是等价的。事实上,应当是先有神经元建模的概念,再有基于人工神经元和层结构的人工神经网络。但由于本手册着重介绍 TensorFlow 的使用方法,所以调换了介绍顺序。