TensorFlow 2.0 模型:多层感知机
文 / 李锡涵,Google Developers Expert
本文节选自《简单粗暴 TensorFlow 2.0》
在 上一篇文章 里,我们简要介绍了 TensorFlow 2.0 中建立模型类的方法。本文即以多层感知机 (Multilayer Perceptron, MLP),或者说 “多层全连接神经网络” 为例,给出一个具体示例,详细介绍 TensorFlow 2.0 的模型构建、训练、评估的全流程。在这一部分,我们依次进行以下步骤:
使用
tf.keras.datasets
获得数据集并预处理使用
tf.keras.Model
和tf.keras.layers
构建模型构建模型训练流程,使用
tf.keras.losses
计算损失函数,并使用tf.keras.optimizer
优化模型构建模型评估流程,使用 tf.keras.metrics
计算评估指标
基础知识和原理
UFLDL 教程 | 神经网络 一节
斯坦福课程 CS231n: Convolutional Neural Networks for Visual Recognition 中的 “Neural Networks Part 1 ~ 3” 部分。
注:神经网络 链接
http://ufldl.stanford.edu/tutorial/supervised/MultiLayerNeuralNetworks/
CS231n 链接
http://cs231n.github.io/
MNIST 手写数字图片示例
数据获取及预处理:tf.keras.datasets
MNISTLoader
类来读取 MNIST 数据集数据。这里使用了 tf.keras.datasets
快速载入 MNIST 数据集。 1class MNISTLoader():
2 def __init__(self):
3 mnist = tf.keras.datasets.mnist
4 (self.train_data, self.train_label), (self.test_data, self.test_label) = mnist.load_data()
5 # MNIST中的图像默认为uint8(0-255的数字)。以下代码将其归一化到0-1之间的浮点数,并在最后增加一维作为颜色通道
6 self.train_data = np.expand_dims(self.train_data.astype(np.float32) / 255.0, axis=-1) # [60000, 28, 28, 1]
7 self.test_data = np.expand_dims(self.test_data.astype(np.float32) / 255.0, axis=-1) # [10000, 28, 28, 1]
8 self.train_label = self.train_label.astype(np.int32) # [60000]
9 self.test_label = self.test_label.astype(np.int32) # [10000]
10 self.num_train_data, self.num_test_data = self.train_data.shape[0], self.test_data.shape[0]
11
12 def get_batch(self, batch_size):
13 # 从数据集中随机取出batch_size个元素并返回
14 index = np.random.randint(0, np.shape(self.train_data)[0], batch_size)
15 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 的概率。
1class MLP(tf.keras.Model):
2 def __init__(self):
3 super().__init__()
4 self.flatten = tf.keras.layers.Flatten() # Flatten层将除第一维(batch_size)以外的维度展平
5 self.dense1 = tf.keras.layers.Dense(units=100, activation=tf.nn.relu)
6 self.dense2 = tf.keras.layers.Dense(units=10)
7
8 def call(self, inputs): # [batch_size, 28, 28, 1]
9 x = self.flatten(inputs) # [batch_size, 784]
10 x = self.dense1(x) # [batch_size, 100]
11 x = self.dense2(x) # [batch_size, 10]
12 output = tf.nn.softmax(x)
13 return output
softmax 函数
这里,因为我们希望输出 “输入图片分别属于 0 到 9 的概率”,也就是一个 10 维的离散概率分布,所以我们希望这个 10 维向量至少满足两个条件:
该向量中的每个元素均在
之间; 该向量的所有元素之和为 1。
为了使得模型的输出能始终满足这两个条件,我们使用 softmax 函数(归一化指数函数,
tf.nn.softmax
)对模型的原始输出进行归一化。其形式为。不仅如此,softmax 函数能够凸显原始向量中最大的值,并抑制远低于最大值的其他分量,这也是该函数被称作 softmax 函数的原因(即平滑化的 argmax 函数)。
模型的训练:tf.keras.losses 和 tf.keras.optimizer
定义一些模型超参数:
2batch_size = 50
3learning_rate = 0.001
tf.keras.optimizer
的优化器(这里使用常用的 Adam 优化器):1model = MLP()
2data_loader = MNISTLoader()
3optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate)
然后迭代进行以下步骤:
从 DataLoader 中随机取一批训练数据; 将这批数据送入模型,计算出模型的预测值;
将模型预测值与真实值进行比较,计算损失函数(loss)。这里使用
tf.keras.losses
中的交叉熵函数作为损失函数;计算损失函数关于模型变量的导数;
将求出的导数值传入优化器,使用优化器的
apply_gradients
方法更新模型参数以最小化损失函数(优化器的详细使用方法见 前文 )。
具体代码实现如下:
1 num_batches = int(data_loader.num_train_data // batch_size * num_epochs)
2 for batch_index in range(num_batches):
3 X, y = data_loader.get_batch(batch_size)
4 with tf.GradientTape() as tape:
5 y_pred = model(X)
6 loss = tf.keras.losses.sparse_categorical_crossentropy(y_true=y, y_pred=y_pred)
7 loss = tf.reduce_mean(loss)
8 print("batch %d: loss %f" % (batch_index, loss.numpy()))
9 grads = tape.gradient(loss, model.variables)
10 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 帮助我们计算损失函数的值。
交叉熵作为损失函数,在分类问题中被广泛应用。其离散形式为
,其 为真实概率分布, 为预测概率分布, 为分类任务的类别个数。预测概率分布与真实分布越接近,则交叉熵的值越小,反之则越大。更具体的介绍及其在机器学习中的应用可参考 博客文章 。 注:博客文章 链接
https://blog.csdn.net/tsyccnh/article/details/79163834
在
tf.keras
中,有两个交叉熵相关的损失函数tf.keras.losses.categorical_crossentropy
和tf.keras.losses.sparse_categorical_crossentropy
。其中 sparse 的含义是,真实的标签值y_true
可以直接传入 int 类型的标签类别。具体而言:
1loss = tf.keras.losses.sparse_categorical_crossentropy(y_true=y, y_pred=y_pred)
与1loss = tf.keras.losses.categorical_crossentropy(
2 y_true=tf.one_hot(y, depth=tf.shape(y_pred)[-1]),
3 y_pred=y_pred
4)的结果相同。
模型的评估:tf.keras.metrics
最后,我们使用测试集评估模型性能。这里,我们使用 tf.keras.metrics
中的 SparseCategoricalAccuracy
评估器来评估模型在测试集上的性能,该评估器能够对模型预测的结果与真实结果进行比较,并输出预测正确的样本数占总样本数的比例。我们迭代测试数据集,每次通过 update_state()
方法向评估器输入两个参数:y_pred
和 y_true
,即模型预测出的结果和真实结果。评估器具有内部变量来保存当前评估指标相关的参数数值(例如当前已传入的累计样本数和当前预测正确的样本数)。迭代结束后,我们使用 result()
方法输出最终的评估指标值(预测正确的样本数占总样本数的比例)。
在以下代码中,我们实例化了一个 tf.keras.metrics.SparseCategoricalAccuracy
评估器,并使用 For 循环迭代分批次传入了测试集数据的预测结果与真实结果,并输出训练后的模型在测试数据集上的准确率。
1 sparse_categorical_accuracy = tf.keras.metrics.SparseCategoricalAccuracy()
2 num_batches = int(data_loader.num_test_data // batch_size)
3 for batch_index in range(num_batches):
4 start_index, end_index = batch_index * batch_size, (batch_index + 1) * batch_size
5 y_pred = model.predict(data_loader.test_data[start_index: end_index])
6 sparse_categorical_accuracy.update_state(y_true=data_loader.test_label[start_index: end_index], y_pred=y_pred)
7 print("test accuracy: %f" % sparse_categorical_accuracy.result())
输出结果:
1test 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
的形状。仔细研究一下,你会发现,这里基于神经元建模的介绍与前文 基于矩阵计算的介绍 是等价的。
参考文献
[LeCun1998] LeCun, L. Bottou, Y. Bengio, and P. Haffner. “Gradient-based learning applied to document recognition.” Proceedings of the IEEE, 86(11):2278-2324, November 1998.
http://yann.lecun.com/exdb/mnist/
福利 | 问答环节
我们知道在入门一项新的技术时有许多挑战与困难需要克服。如果您有关于 TensorFlow 的相关问题,请在本文后留言,我们的工程师和 GDE 将挑选其中具有代表性的问题在下一期进行回答~
在上一篇文章《TensorFlow 2.0 模型:模型类的建立》中,我们对于部分具有代表性的问题回答如下:
Q1:有什么好的下载方式,访问不了官网啊。
A:关于 TensorFlow 的下载方式,在国内建议使用清华大学或其他国内的pypi镜像进行 pip 下 TensorFlow 的安装,详情可参考安装指南。官方网站和文档可以访问中文网站 https://tensorflow.google.cn (可以在右上角的菜单切换需要的语言)。
Q2:有一说一,太多的 API 以及版本更换废除一些已经习惯的 API,导致代码风格差异太大,确实带来了一些麻烦。
A:关于从 TensorFlow 1.X 至 2.0 的代码转换,TensorFlow 提供了自动转换脚本及指导文档,可参考迁移指南 。同时,如果无法做到完美的转换,也可以随时使用 tf.compat.v1 模块载入 1.X 风格的 API。最后,程序框架的更新换代往往伴随着阵痛。为了让框架变得更加现代、易用和与时俱进,摆脱一些历史包袱,一些兼容性上的牺牲也是难以避免的。我们一方面尽可能减少转换过程中的问题,另一方面也希望你能够拥抱新版本的 TensorFlow,我们相信全新的版本将为新旧开发者们均带来更好的体验。
Q3:看了《让计算机看懂你在摆什么Pose》,想问一下TensorFlow.js可以实现吗?
A:可以!请参考我们在 Medium 上的 Real-time Human Pose Estimation in the Browser with TensorFlow.js 一文,Demo可以直接在浏览器中(甚至手机上)运行哦!
文章
https://medium.com/tensorflow/real-time-human-pose-estimation-in-the-browser-with-tensorflow-js-7dd0bc881cd5
Demo
https://storage.googleapis.com/tfjs-models/demos/posenet/camera.html
Q4:请问《简单粗暴 TensorFlow 2.0》这本书可以下载PDF版本吗?
A:我们有计划发布 PDF 版本,不过目前本手册的部分章节尚未编写完成,待手册基本完工后我们将发布 PDF 版本。