简单粗暴 TensorFlow(Xihan Li(雪麒))

简单粗暴TensorFlow | A Concise Handbook of TensorFlow

基于Eager Execution | Based on Eager Execution

在线阅读 | Read online : https://tf.wiki

备用地址 | Alternative URL:https://snowkylin.github.io/TensorFlow-cn/

作者 | Author: Xihan Li (snowkylin)

英文版译者 | Translators of English version: Zida Jin, Ming, Ji-An Li, Xihan Li

本手册是一篇精简的TensorFlow入门指导,基于TensorFlow的Eager Execution(动态图)模式,力图让具备一定机器学习及Python基础的开发者们快速上手TensorFlow。

This handbook is a concise introduction to TensorFlow based on Eager Execution mode, trying to help developers get started with TensorFlow quickly with some basic machine learning and Python knowledge.

PDF下载 | PDF download :

(中文版 | Chinese): https://www.tensorflowers.cn/t/6230
(英文版 | English): https://github.com/snowkylin/TensorFlow-cn/releases
在线答疑区 | Online Q&A area :

(中文 | Chinese): https://www.tensorflowers.cn/b/48
(英文 | English): https://github.com/snowkylin/TensorFlow-cn/issues

开始

基于 Eager Execution | Based on Eager Execution

扫描二维码关注公众号,回复: 9035738 查看本文章

本手册是一篇精简的 TensorFlow 入门指导,基于 TensorFlow 的 Eager Execution(动态图)模式,力图让 具备一定机器学习及 Python 基础的开发者们快速上手 TensorFlow。

友情提醒:如果发现阅读中有难以理解的部分,请检查自己对每章的“前置知识”部分是否有清楚的理解。

答疑区 - TensorFlow 中文社区“简单粗暴 TensorFlow”版面:https://www.tensorflowers.cn/b/48 (如果 您对本教程有任何疑问,请至 TensorFlow 中文社区的该版面发问) PDF 下载:https://www.tensorflowers.cn/t/6230 GitHub: https://github.com/snowkylin/TensorFlow-cn

This handbook is a concise introduction to TensorFlow based on TensorFlow’s Eager Execution mode, trying to help developers get started with TensorFlow quickly with some basic machine learning and Python knowledge.

Friendly reminder: If you find something difficult to understand in reading, please check if you have a clear understanding of the “Prerequisites”part of each chapter.

Q&A area - TensorFlow Chinese community “A Concise Handbook of TensorFlow”forum: https://www. tensorflowers.cn/b/48 (If you have any questions about this tutorial, please ask in this forum of the TensorFlow Chinese community) PDF download: https://www.tensorflowers.cn/t/6230

GitHub: https://github.com/snowkylin/TensorFlow-cn

1. 前言

2018 年 3 月 30 日,Google 在加州山景城举行了第二届 TensorFlow Dev Summit 开发者峰会,并宣布正 式发布 TensorFlow 1.8 版本。笔者有幸获得 Google 的资助亲临峰会现场,见证了这一具有里程碑式意义的 新版本发布。众多新功能的加入和支持展示了 TensorFlow 的雄心壮志,同时早在 2017 年秋就开始测试的 Eager Execution(动态图机制)在这一版本中终于正式加入,并成为了入门 TensorFlow 的官方推荐模式。

The easiest way to get started with TensorFlow is using Eager Execution. ——https://www.tensorflow.org/get_started/ 在此之前,TensorFlow 所基于的传统 Graph Execution 的弊端,如入门门槛高、调试困难、灵活性差、无法使 用 Python 原生控制语句等早已被开发者诟病许久。一些新的基于动态图机制的深度学习框架(如 PyTorch) 也横空出世,并以其易用性和快速开发的特性而占据了一席之地。尤其是在学术研究等需要快速迭代模型的 领域,PyTorch 等新兴深度学习框架已经成为主流。**笔者所在的数十人的机器学习实验室中,竟只有笔者一 人“守旧”地使用 TensorFlow。然而,直到目前,市面上相关的 TensorFlow 相关的中文技术书籍及资料仍 然基于传统的 Graph Execution 模式,让不少初学者(尤其是刚学过机器学习课程的大学生)望而却步。**由 此,在 TensorFlow 正式支持 Eager Execution 之际,有必要出现一本全新的技术手册,帮助初学者及需要 快速迭代模型的研究者,以一个全新的角度快速入门 TensorFlow。

同时,本手册还有第二个任务。市面上与 TensorFlow 相关的中文技术书籍大部分都以深度学习为主线,而 将 TensorFlow 作为这些深度学习模型的实现方式。这样固然有体系完整的优点,然而对于已经对机器学 习或深度学习理论有所了解,希望侧重于学习 TensorFlow 本身的读者而言,就显得不够友好。同时,虽然 TensorFlow 有官方的教学文档(https://tensorflow.google.cn/tutorials),然而在体例上显得逻辑性不足,缺 乏一般教学文档从浅入深,层次递进的特性,而更类似于一系列技术文档的罗列。于是,笔者希望编写一本 手册,以尽量精简的篇幅展示 TensorFlow 作为一个计算框架的主要特性,并弥补官方手册的不足,力图能 让已经有一定机器学习/深度学习知识及编程能力的读者迅速上手 TensorFlow,并在实际编程过程中可以随时查阅并解决实际问题。

本手册的主要特征有:

• 主要基于 TensorFlow 最新的 Eager Execution(动态图)模式,以便于模型的快速迭代开发。但依然 会包含传统的 Graph Execution 模式,代码上尽可能兼容两者;

• 定位以教学及工具书为主,编排以 TensorFlow 的各项概念和功能为核心,力求能够让 TensorFlow 开 发者快速查阅。各章相对独立,不一定需要按顺序阅读。正文中不会出现太多关于深度学习和机器学 习的理论介绍,但会提供若干阅读推荐以便初学者掌握相关基础知识;

• 代码实现均进行仔细推敲,力图简洁高效和表意清晰。模型实现均统一使用 TensorFlow 官方文档 最新 提出的继承 tf.keras.Model 和 tf.keras.layers.Layer 的方式(在其他技术文档中鲜少介绍),保 证代码的高度可复用性。每个完整项目的代码总行数均不过百行,让读者可以快速理解并举一反三;

• 注重详略,少即是多,不追求巨细靡遗和面面俱到,不进行大篇幅的细节论述。

在整本手册中,带“*”的部分均为选读。

A 本手册的暂定名称《简单粗暴 TensorFlow》是向我的好友兼同学 Chris Wu 编写的《简单粗暴 L T E X 》(https: A //github.com/wklchris/Note-by-LaTeX)致敬。该手册清晰精炼,是 L T E X 领域不可多得的中文资料,也是 我在编写这一技术文档时所学习的对象。本手册最初是在我的好友 Ji-An Li 所组织的深度学习研讨小组中, 由我作为预备知识的讲义而编写和使用。好友们的才学卓著与无私分享的品格也是编写此拙作的重要助力。

本手册的英文版由我的好友 Zida Jin(1-4 章)和 Ming(5-6 章)翻译,并由 Ji-An Li 和笔者审校。三位朋 友牺牲了自己的大量宝贵时间翻译和校对本手册,同时 Ji-An Li 亦对本手册的教学内容和代码细节提供了 诸多宝贵意见。我谨向好友们为本手册的辛勤付出致以衷心的感谢。

衷心感谢 Google 中国开发者关系团队和 TensorFlow 工程团队的成员们对本手册编写所提供的帮助。其中 包括开发者关系团队的 Luke Cheng 在本手册写作全程提供的思路启发和持续鼓励,开发者关系团队的 Rui Li, Pryce Mu 和 TensorFlow 社群维护的小伙伴们在本手册宣发及推广上提供的大力支持,以及 TensorFlow 团队的 Tiezhen Wang 在本手册工程细节方面提供的诸多建议和补充。

关于本手册的意见和建议,欢迎在 https://github.com/snowkylin/TensorFlow-cn/issues 提交。这是一个开 源项目,您的宝贵意见将促进本手册的持续更新。

2. TensorFlow 安装

TensorFlow 的最新安装步骤可参考官方网站上的说明(https://tensorflow.google.cn/install)。TensorFlow 支持 Python、Java、Go、C 等多种编程语言以及 Windows、OSX、Linux 等多种操作系统,此处及后文均 以主流的 Python 语言为准。 以下提供简易安装和正式安装两种途径,供不同层级的读者选用。

2.1 简易安装

如果只是安装一个运行在自己电脑上的,无需 GPU 的简易环境,不希望在环境配置上花费太多精力,建议按以下步骤安装(以 Windows 系统为例):

• 下载并安装 Python 集成环境 Anaconda (Python 3.6 版本);

• 下载并安装 Python 的 IDE PyCharm (Community 版本,或学生可申请 Professional 版本的免费授 权);

• 打开开始菜单中的“Anaconda Prompt”,输入 pip install tensorflow。 完毕。

2.1.1 Test

安装完毕后,我们来编写一个简单的程序来验证安装。

在命令行下输入 activate tensorflow 进入之前建立的安装有 TensorFlow 的 conda 环境,再输入 python 进入 Python 环境,逐行输入以下代码:

import tensorflow as tf

tf.enable_eager_execution()

A = tf.constant([[1, 2], [3, 4]]) B = tf.constant([[5, 6], [7, 8]]) C = tf.matmul(A, B)

print(C)

如果能够最终输出:

tf.Tensor( [[19 22] [43 50]], shape=(2, 2), dtype=int32)

说明 TensorFlow 已安装成功。运行途中可能会输出一些 TensorFlow 的提示信息,属于正常现象。

此处使用的是 Python 语言, 关于 Python 语言的入门教程可以参考 http://www.runoob.com/python3/ python3-tutorial.html 或 https://www.liaoxuefeng.com ,本手册之后将默认读者拥有 Python 语言的基本知识。不用紧张,Python 语言易于上手,而 TensorFlow 本身也不会用到 Python 语言的太多高级特性。关于 Python 的 IDE,建议使用 PyCharm 。如果你是学生并有.edu 结尾的邮箱的话,可以在 这里 申请免费 的授权。如果没有,也可以下载社区版本的 PyCharm,主要功能差别不大。

3. TensorFlow 基础

本章介绍 TensorFlow 的基本操作。 前置知识:

• Python 基本操作 (赋值、分支及循环语句、使用 import 导入库);

• Python 的 With 语句 ;

• NumPy ,Python 下常用的科学计算库。TensorFlow 与之结合紧密;

• 向量 和 矩阵 运算(矩阵的加减法、矩阵与向量相乘、矩阵与矩阵相乘、矩阵的转置等。

• 函数的导数 ,多元函数求导 (测试题:f(x, y) = x 2 + xy + y 2 , ∂f ∂x =?, ∂f ∂y =?);

• 线性回归 ;

• 梯度下降方法 求函数的局部最小值。

import tensorflow as tf

tf.enable_eager_execution()

a = tf.constant(1) b = tf.constant(1) c = tf.add(a, b)

print(c)

# 也可以直接写 c = a + b,两者等价

A = tf.constant([[1, 2], [3, 4]]) B = tf.constant([[5, 6], [7, 8]]) C = tf.matmul(A, B)

print(C)

输出:

tf.Tensor(2, shape=(), dtype=int32) 
tf.Tensor( [[19 22] [43 50]], shape=(2, 2), dtype=int32)

使用常规的科学计算库实现机器学习模型有两个痛点:

• 经常需要手工求函数关于参数的偏导数。如果是简单的函数或许还好,但一旦函数的形式变得复杂(尤 其是深度学习模型),手工求导的过程将变得非常痛苦,甚至不可行。

• 经常需要手工根据求导的结果更新参数。这里使用了最基础的梯度下降方法,因此参数的更新还较为 容易。但如果使用更加复杂的参数更新方法(例如 Adam 或者 Adagrad),这个更新过程的编写同样会 非常繁杂。

而 TensorFlow 等深度学习框架的出现很大程度上解决了这些痛点,为机器学习模型的实现带来了很大的便 利。

TensorFlow 的 Eager Execution(动态图)模式 与上述 NumPy 的运行方式十分类似,然而提供了更快速的运算(GPU 支持)、自动求导、优化器等一系列对深度学习非常重要的功能。以下展示了如何使用 TensorFlow 计算线性回归。可以注意到,程序的结构和前述 NumPy 的实现非常类似。这里,TensorFlow 帮助我们做了两件重要的工作:

• 使用 tape.gradient(ys, xs) 自动计算梯度;

• 使用 optimizer.apply_gradients(grads_and_vars) 自动更新模型参数。

a = tf.get_variable('a', dtype=tf.float32, shape=[], initializer=tf.zeros_initializer) b = tf.get_variable('b', dtype=tf.float32, shape=[], initializer=tf.zeros_initializer) variables = [a, b]

num_epoch = 10000 optimizer = tf.train.GradientDescentOptimizer(learning_rate=1e-3) for e in range(num_epoch):

# 使用 tf.GradientTape() 记录损失函数的梯度信息 with tf.GradientTape() as tape:

y_pred = a * X + b

loss = 0.5 * tf.reduce_sum(tf.square(y_pred - y)) # TensorFlow 自动计算损失函数关于自变量(模型参数)的梯度 grads = tape.gradient(loss, variables) # TensorFlow 自动根据梯度更新参数 optimizer.apply_gradients(grads_and_vars=zip(grads, variables))

在 这 里, 我 们 使 用 了 前 文 的 方 式 计 算 了 损 失 函 数 关 于 参 数 的 偏 导 数。 同 时, 使 用 tf.train. GradientDescentOptimizer(learning_rate=1e-3) 声明了一个梯度下降 优化器(Optimizer), 其学习率为 1e-3。优化器可以帮助我们根据计算出的求导结果更新模型参数,从而最小化某个特定的损失函数, 具体使用方式是调用其 apply_gradients() 方法。

注意到这里,更新模型参数的方法 optimizer.apply_gradients() 需要提供参数 grads_and_vars,即待更新的变量(如上述代码中的 variables )及损失函数关于这些变量的偏导数(如上述代码中的 grads )。 具体而言,这里需要传入一个 Python 列表(List),列表中的每个元素是一个(变量的偏导数,变量)对。 比如这里是 [(grad_w, w), (grad_b, b)] 。我们通过 grads = tape.gradient(loss, variables) 求出 tape 中记录的 loss 关于 variables = [w, b] 中每个变量的偏导数,也就是 grads = [grad_w, grad_b], 再使用 Python 的 zip() 函数将 grads = [grad_w, grad_b] 和 vars = [w, b] 拼装在一起,就可以组合出所需的参数了。

在实际应用中,我们编写的模型往往比这里一行就能写完的线性模型 y_pred = tf.matmul(X, w) + b 要 复杂得多。所以,我们往往会编写一个模型类,然后在需要调用的时候使用 y_pred = model(X) 进行调用。

4. TensorFlow 模型

关于模型类的编写方式可见下章。

本章介绍如何使用 TensorFlow 快速搭建动态模型。 前置知识:

• Python 面向对象 (在 Python 内定义类和方法、类的继承、构造和析构函数,使用 super() 函数调用 父类方法 ,使用 call() 方法对实例进行调用 等);

• 多层感知机、卷积神经网络、循环神经网络和强化学习(每节之前给出参考资料)。

4.1 模型(Model)与层(Layer)

如上一章所述,为了增强代码的可复用性,我们往往会将模型编写为类,然后在模型调用的地方使用 y_pred = model(X) 的形式进行调用。模型类的形式非常简单, 主要包含 init() (构造函数, 初始化)和 1 call(input) (模型调用)两个方法,但也可以根据需要增加自定义的方法。

class MyModel(tf.keras.Model):

def __init__(self):

super().__init__() # Python 2 下使用 super(MyModel, self).__init__() # 此处添加初始化代码(包含 call 方法中会用到的层)

def call(self, inputs):
# 此处添加模型调用的代码(处理输入并返回输出)

return output

在 Python 类中,对类的实例 myClass 进行形如 myClass() 的调用等价于 myClass.call() 。在这里,我们的模型继承了 tf.keras.Model 这一父类。该父类中包含 call() 的定义,其中调用了 call() 方法,同时进行了一些 keras 的内部操作。这里, 我们通过继承 tf.keras.Model 并重载 call() 方法,即可在保持 keras 结构的同时加入模型调用的代码。具体请见本章初“前置知 识”的 call() 部分。

在这里,我们的模型类继承了 tf.keras.Model 。Keras 是一个用 Python 编写的高级神经网络 API,现已得到 TensorFlow 的官方支持和内置。继承 tf.keras.Model 的一个好处在于我们可以使用父类的若干方法 和属性,例如在实例化类后可以通过 model.variables 这一属性直接获得模型中的所有变量,免去我们一 个个显式指定变量的麻烦。

同时,我们引入 “层”(Layer) 的概念,层可以视为比模型粒度更细的组件单位,将计算流程和变量进行 了封装。我们可以使用层来快速搭建模型。

上一章中简单的线性模型 y_pred = tf.matmul(X, w) + b ,我们可以通过模型类的方式编写如下:

import tensorflow as tf tf.enable_eager_execution()

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):

def __init__(self):

super().__init__() self.dense = tf.keras.layers.Dense(units=1, kernel_initializer=tf.zeros_initializer(), bias_initializer=tf.zeros_initializer())

def call(self, input):

output = self.dense(input) return output

# 以下代码结构与前节类似

model = Linear() optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.01) for i in range(100):

with tf.GradientTape() as tape:

y_pred = model(X) # 调用模型 loss = tf.reduce_mean(tf.square(y_pred - y)) grads = tape.gradient(loss, model.variables) optimizer.apply_gradients(grads_and_vars=zip(grads, model.variables)) print(model.variables)

这里,我们没有显式地声明 w 和 b 两个变量并写出 y_pred = tf.matmul(X, w) + b 这一线性变换,而是 在初始化部分实例化了一个全连接层(tf.keras.layers.Dense ),并在 call 方法中对这个层进行调用。全 连接层封装了 output = activation(tf.matmul(input, kernel) + bias) 这一线性变换 + 激活函数的计算操作,以及 kernel 和 bias 两个变量。当不指定激活函数时(即 activation(x) = x ),这个全连接 层就等价于我们上述的线性变换。顺便一提,全连接层可能是我们编写模型时使用最频繁的层。

如果我们需要显式地声明自己的变量并使用变量进行自定义运算,请参考自定义层。

4.2 基础示例:多层感知机(MLP)

我们从编写一个最简单的 多层感知机 (Multilayer Perceptron, MLP)开始,介绍 TensorFlow 的模型编写 方式。这里,我们使用多层感知机完成 MNIST 手写体数字图片数据集 [LeCun1998] 的分类任务。

在这里插入图片描述

先进行预备工作,实现一个简单的 DataLoader 类来读取 MNIST 数据集数据。

class DataLoader():

def __init__(self):

mnist = tf.contrib.learn.datasets.load_dataset("mnist") self.train_data = mnist.train.images ,784] self.train_labels = np.asarray(mnist.train.labels, dtype=np.int32) ,→int32 self.eval_data = mnist.test.images ,784] self.eval_labels = np.asarray(mnist.test.labels, dtype=np.int32) ,→int32

def get_batch(self, batch_size):

# np.array [55000,␣

# np.array [55000] of␣

# np.array [10000,␣

# np.array [10000] of␣

index = np.random.randint(0, np.shape(self.train_data)[0], batch_size) return self.train_data[index, :], self.train_labels[index]

多层感知机的模型类实现与上面的线性模型类似,所不同的地方在于层数增加了(顾名思义,“多层”感知 机),以及引入了非线性激活函数(这里使用了 ReLU 函数 ,即下方的 activation=tf.nn.relu )。该模型 输入一个向量(比如这里是拉直的 1×784 手写体数字图片),输出 10 维的信号,分别代表这张图片属于 0 到 9 的概率。这里我们加入了一个 predict 方法,对图片对应的数字进行预测。在预测的时候,选择概率最 大的数字进行预测输出。

class MLP(tf.keras.Model):

def __init__(self):

super().__init__()
self.dense1 = tf.keras.layers.Dense(units=100,activation=tf.nn.relu) 
self.dense2 = tf.keras.layers.Dense(units=10)

def call(self, inputs):

x = self.dense1(inputs) 
x = self.dense2(x) 
return x

def predict(self, inputs):
logits = self(inputs) 
return tf.argmax(logits, axis=-1)

定义一些模型超参数:

num_batches = 1000 batch_size = 50 learning_rate = 0.001

实例化模型,数据读取类和优化器:

model = MLP() 
data_loader = DataLoader() 
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)

然后迭代进行以下步骤:

• 从 DataLoader 中随机取一批训练数据;

• 将这批数据送入模型,计算出模型的预测值;

• 将模型预测值与真实值进行比较,计算损失函数(loss);

• 计算损失函数关于模型变量的导数;

• 使用优化器更新模型参数以最小化损失函数。 具体代码实现如下:

for batch_index in range(num_batches):

X, y = data_loader.get_batch(batch_size) with tf.GradientTape() as tape:

y_logit_pred = model(tf.convert_to_tensor(X))

loss = tf.losses.sparse_softmax_cross_entropy(labels=y, logits=y_logit_pred)

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))

接下来,我们使用验证集测试模型性能。具体而言,比较验证集上模型预测的结果与真实结果,输出预测正 确的样本数占总样本数的比例:

num_eval_samples = np.shape(data_loader.eval_labels)[0] y_pred = model.predict(data_loader.eval_data).numpy() print("test accuracy: %f" % (sum(y_pred == data_loader.eval_labels) / num_eval_samples))

输出结果:
test accuracy: 0.947900

可以注意到,使用这样简单的模型,已经可以达到 95% 左右的准确率。

4.3 卷积神经网络(CNN)

卷积神经网络 (Convolutional Neural Network, CNN)是一种结构类似于人类或动物的 视觉系统 的人工神 经网络,包含一个或多个卷积层(Convolutional Layer)、池化层(Pooling Layer)和全连接层(Dense Layer)。 具体原理建议可以参考台湾大学李宏毅教授的《机器学习》课程的 Convolutional Neural Network 一章。

具体的实现见下,和 MLP 很类似,只是新加入了一些卷积层和池化层。
在这里插入图片描述

class CNN(tf.keras.Model):

def __init__(self):

super().__init__() self.conv1 = tf.keras.layers.Conv2D( filters=32, # 卷积核数目 kernel_size=[5, 5], # 感受野大小 padding="same", # padding 策略 activation=tf.nn.relu # 激活函数 ) self.pool1 = tf.keras.layers.MaxPool2D(pool_size=[2, 2], strides=2) self.conv2 = tf.keras.layers.Conv2D( filters=64,
kernel_size=[5, 5], padding="same", activation=tf.nn.relu

) self.pool2 = tf.keras.layers.MaxPool2D(pool_size=[2, 2], strides=2) self.flatten = tf.keras.layers.Reshape(target_shape=(7 * 7 * 64,)) self.dense1 = tf.keras.layers.Dense(units=1024, activation=tf.nn.relu) self.dense2 = tf.keras.layers.Dense(units=10)

def call(self, inputs):

inputs = tf.reshape(inputs, [-1, 28, 28, 1]) x = self.conv1(inputs) # [batch_size, 28, 28, 32] x = self.pool1(x) # [batch_size, 14, 14, 32] x = self.conv2(x) # [batch_size, 14, 14, 64] x = self.pool2(x) # [batch_size, 7, 7, 64] x = self.flatten(x) # [batch_size, 7 * 7 * 64] x = self.dense1(x) # [batch_size, 1024] x = self.dense2(x) # [batch_size, 10] return x

def predict(self, inputs):

logits = self(inputs)

将前节的 model = MLP() 更换成 model = CNN() ,输出如下:

test accuracy: 0.988100

可以发现准确率有非常显著的提高。事实上,通过改变模型的网络结构(比如加入 Dropout 层防止过拟合), 准确率还有进一步提升的空间。

4.4 循环神经网络(RNN)

循环神经网络(Recurrent Neural Network, RNN)是一种适宜于处理序列数据的神经网络,被广泛用于语言 模型、文本生成、机器翻译等。关于 RNN 的原理,可以参考:

• Recurrent Neural Networks Tutorial, Part 1 – Introduction to RNNs

• 台湾大学李宏毅教授的《机器学习》课程的 Recurrent Neural Network (part 1) Recurrent Neural Network (part 2) 两部分。

• LSTM 原理:Understanding LSTM Networks

• RNN 序列生成:[Graves2013]

这里,我们使用 RNN 来进行尼采风格文本的自动生成。

这个任务的本质其实预测一段英文文本的接续字母的概率分布。比如,我们有以下句子:

I am a studen

这个句子(序列)一共有 13 个字符(包含空格)。当我们阅读到这个由 13 个字符组成的序列后,根据我 们的经验,我们可以预测出下一个字符很大概率是“t”。我们希望建立这样一个模型,输入 num_batch 个 由编码后字符组成的,长为 seq_length 的序列,输入张量形状为 [num_batch, seq_length],输出这些序列 接续的下一个字符的概率分布,概率分布的维度为字符种类数 num_chars,输出张量形状为 [num_batch, num_chars]。我们从下一个字符的概率分布中采样作为预测值,然后滚雪球式地生成下两个字符,下三个字 符等等,即可完成文本的生成任务。

首先,还是实现一个简单的 DataLoader 类来读取文本,并以字符为单位进行编码。

class DataLoader():

def __init__(self):

path = tf.keras.utils.get_file('nietzsche.txt', origin='https://s3.amazonaws.com/text-datasets/nietzsche.txt') with open(path, encoding='utf-8') as f:

self.raw_text = f.read().lower() self.chars = sorted(list(set(self.raw_text))) self.char_indices = dict((c, i) for i, c in enumerate(self.chars)) self.indices_char = dict((i, c) for i, c in enumerate(self.chars)) self.text = [self.char_indices[c] for c in self.raw_text]

def get_batch(self, seq_length, batch_size):

seq = [] 
next_char = [] 
for i in range(batch_size):
index = np.random.randint(0, len(self.text) - seq_length)
seq.append(self.text[index:index+seq_length])
next_char.append(self.text[index+seq_length]) 
return np.array(seq), np.array(next_char) # [num_batch, seq_length], [num_batch]

接下来进行模型的实现。在 init 方法中我们实例化一个常用的 BasicLSTMCell 单元,以及一个线性变 换用的全连接层,我们首先对序列进行 One Hot 操作,即将编码 i 变换为一个 n 维向量,其第 i 位为 1,其余 均为 0。这里 n 为字符种类数 num_char。变换后的序列张量形状为 [num_batch, seq_length, num_chars]。 接下来,我们将序列从头到尾依序送入 RNN 单元,即将当前时间 t 的 RNN 单元状态 state 和 t 时刻的序 列 inputs[:, t, :] 送入 RNN 单元,得到当前时间的输出 output 和下一个时间 t+1 的 RNN 单元状态。 取 RNN 单元最后一次的输出,通过全连接层变换到 num_chars 维,即作为模型的输出。

具体实现如下:
在这里插入图片描述
在这里插入图片描述

class RNN(tf.keras.Model):

def __init__(self, num_chars):

super().__init__() self.num_chars = num_chars self.cell = tf.nn.rnn_cell.BasicLSTMCell(num_units=256) self.dense = tf.keras.layers.Dense(units=self.num_chars)

def call(self, inputs):

batch_size, seq_length = tf.shape(inputs) inputs = tf.one_hot(inputs, depth=self.num_chars) ,→chars] state = self.cell.zero_state(batch_size=batch_size, dtype=tf.float32) for t in range(seq_length.numpy()):

# [batch_size, seq_length, num_

output, state = self.cell(inputs[:, t, :], state) output = self.dense(output) return output

训练过程与前节基本一致,在此复述:

• 从 DataLoader 中随机取一批训练数据;

• 将这批数据送入模型,计算出模型的预测值;

• 将模型预测值与真实值进行比较,计算损失函数(loss);

• 计算损失函数关于模型变量的导数;

• 使用优化器更新模型参数以最小化损失函数。

data_loader = DataLoader() 
model = RNN(len(data_loader.chars)) optimizer = tf.train.Adam
Optimizer(learning_rate=learning_rate) 
for batch_index in range(num_batches):

X, y = data_loader.get_batch(seq_length, batch_size) 
with tf.GradientTape() as tape:

y_logit_pred = model(X)

loss = tf.losses.sparse_softmax_cross_entropy(labels=y, logits=y_logit_pred)

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))

关于文本生成的过程有一点需要特别注意。之前,我们一直使用 tf.argmax() 函数,将对应概率最大的值作 为预测值。然而对于文本生成而言,这样的预测方式过于绝对,会使得生成的文本失去丰富性。于是,我们 使用 np.random.choice() 函数按照生成的概率分布取样。这样,即使是对应概率较小的字符,也有机会被 取样到。同时,我们加入一个 temperature 参数控制分布的形状,参数值越大则分布越平缓(最大值和最小 值的差值越小),生成文本的丰富度越高;参数值越小则分布越陡峭,生成文本的丰富度越低。

def predict(self, inputs, temperature=1.):

batch_size, _ = tf.shape(inputs) 
logits = self(inputs) 
prob = tf.nn.softmax(logits / temperature).numpy() 
return np.array([np.random.choice(self.num_chars, p=prob[i, :]) 
for i in range(batch_size.numpy())])

通过这种方式进行“滚雪球”式的连续预测,即可得到生成文本。

X_, _ = data_loader.get_batch(seq_length, 1) 
for diversity in [0.2, 0.5, 1.0, 1.2]:
X = X_ 
print("diversity %f:" % diversity) 
for t in range(400):
y_pred = model.predict(X, diversity)
print(data_loader.indices_char[y_pred[0]], end='', flush=True)
X = np.concatenate([X[:, 1:], np.expand_dims(y_pred, axis=1)], axis=-1)

生成的文本如下:
diversity 0.200000:

conserted and conseive to the conterned to it is a self–and seast and the selfes as a seast the␣ ,→expecience and and and the self–and the sered is a the enderself and the sersed and as a the␣ ,→concertion of the series of the self in the self–and the serse and and the seried enes and␣ ,→seast and the sense and the eadure to the self and the present and as a to the self–and the␣ ,→seligious and the enders

diversity 0.500000:

can is reast to as a seligut and the complesed has fool which the self as it is a the beasing and us immery and seese for entoured underself of␣

,→the seless and the sired a mears and everyther to out every sone thes and reapres and seralise␣ ,→as a streed liees of the serse to pease the cersess of the selung the elie one of the were as we␣ ,→and man one were perser has persines and conceity of all self-el

diversity 1.000000:

entoles by their lisevers de weltaale, arh pesylmered, and so jejurted count have foursies as is descinty iamo; to semplization refold, we dancey or theicks-welf–atolitious on his such which here oth idey of pire master, ie gerw their endwit in ids, is an trees constenved mase commars is leed␣ ,→mad decemshime to the mor the elige. the fedies (byun their ope wopperfitious–antile and the it␣ ,→as the f

diversity 1.200000:
cain, elvotidue, madehoublesily inselfy!–ie the rads incults of to prusely le]enfes patuateded:.–a coud–theiritibaior ,→"nrallysengleswout peessparify oonsgoscess teemind thenry ansken suprerial mus, cigitioum: 4reas. ,→ whouph: who eved arn inneves to sya" natorne. hag open reals whicame oderedte,[fingo is zisternethta simalfule dereeg hesls lang-lyes thas quiin turjentimy; periaspedey tomm–whach

4.5 深度强化学习(DRL)

强化学习 (Reinforcement learning,RL)强调如何基于环境而行动,以取得最大化的预期利益。结合了深 度学习技术后的强化学习更是如虎添翼。这两年广为人知的 AlphaGo 即是深度强化学习的典型应用。深度 强化学习的基础知识可参考:

• Demystifying Deep Reinforcement Learning (中文编译)

• [Mnih2013]

这里,我们使用深度强化学习玩 CartPole(平衡杆)游戏。简单说,我们需要让模型控制杆的左右运动,以让其一直保持竖直平衡状态。
在这里插入图片描述
我们使用 OpenAI 推出的 Gym 环境库 中的 CartPole 游戏环境,具体安装步骤和教程可参考 官方文档 和 这里 。Gym 的基本调用方法如下:

import gym
env = gym.make('CartPole-v1') state = env.reset() while True:

# 实例化一个游戏环境,参数为游戏名称 # 初始化环境,获得初始状态

env.render()

# 对当前帧进行渲染,绘图到屏幕 # 假设我们有一个训练好的模型,能够通过当前状态预测出这时应该进行的

action = model.predict(state) 动作

next_state, reward, done, info = env.step(action) 动作的奖励,游戏是否已结束以及额外信息

# 让环境执行动作,获得执行完动作的下一个状态,

if done: # 如果游戏结束则退出循环

break

那么,我们的任务就是训练出一个模型,能够根据当前的状态预测出应该进行的一个好的动作。粗略地说, 一个好的动作应当能够最大化整个游戏过程中获得的奖励之和,这也是强化学习的目标。 以下代码展示了如何使用深度强化学习中的 Deep Q-Learning 方法来训练模型。

import tensorflow as tf 
import numpy as np 
import gym 
import random 
from collections import deque

tf.enable_eager_execution() 
num_episodes = 500 
num_exploration_episodes = 100 
max_len_episode = 1000 
batch_size = 32 
learning_rate = 1e-3 
gamma = 1.
initial_epsilon = 1. 
final_epsilon = 0.01

# Q-network 用于拟合 Q 函数,和前节的多层感知机类似。输入 state,输出各个 action 下的 Q-value(CartPole 下为 2 维)。 class QNetwork(tf.keras.Model):

def __init__(self):

super().__init__() self.dense1 = tf.keras.layers.Dense(units=24, activation=tf.nn.relu) self.dense2 = tf.keras.layers.Dense(units=24, activation=tf.nn.relu) self.dense3 = tf.keras.layers.Dense(units=2)

def call(self, inputs):

x = self.dense1(inputs) x = self.dense2(x)
x = self.dense3(x) return x

def predict(self, inputs):

q_values = self(inputs) return tf.argmax(q_values, axis=-1)

# 实例化一个游戏环境,参数为游戏名称

env = gym.make('CartPole-v1') model = QNetwork() optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate) replay_buffer = deque(maxlen=10000) epsilon = initial_epsilon for episode_id in range(num_episodes):

state = env.reset() # 初始化环境,获得初始状态

epsilon = max(

initial_epsilon * (num_exploration_episodes - episode_id) / num_exploration_episodes,

final_epsilon)

for t in range(max_len_episode):

env.render() # 对当前帧进行渲染,绘图到屏幕

if random.random() < epsilon: # epsilon-greedy 探索策略 action = env.action_space.sample() # 以 epsilon 的概率选择随机动作

else:

action = model.predict( tf.constant(np.expand_dims(state, axis=0), dtype=tf.float32)).numpy() action = action[0]

next_state, reward, done, info = env.step(action) # 让环境执行动作,获得执行完 动作的下一个状态,动作的奖励,游戏是否已结束以及额外信息

reward = -10. if done else reward # 如果游戏 Game Over,给予大 的负奖励

replay_buffer.append((state, action, reward, next_state, done)) # 将 (state, action,␣ ,→reward, next_state) 的四元组(外加 done 标签表示是否结束)放入经验重放池

state = next_state

if done:

行下一个 episode

# 游戏结束则退出本轮循环,进

print("episode %d, epsilon %f, score %d" % (episode_id, epsilon, t)) break

if len(replay_buffer) >= batch_size:

batch_state, batch_action, batch_reward, batch_next_state, batch_done = \ [np.array(a, dtype=np.float32) for a in zip(*random.sample(replay_buffer, batch_ ,→size))] # 从经验回放池中随机取一个 batch 的四元组 q_value = model(tf.constant(batch_next_state, dtype=tf.float32)) y = batch_reward + (gamma * tf.reduce_max(q_value, axis=1)) * (1 - batch_done) # 按照论文计算 y 值
with tf.GradientTape() as tape:

# 最小化 y 和 Q-value 的距离

loss = tf.losses.mean_squared_error( labels=y, predictions=tf.reduce_sum(model(tf.constant(batch_state)) * tf.one_hot(batch_action, depth=2), axis=1)

) grads = tape.gradient(loss, model.variables) optimizer.apply_gradients(grads_and_vars=zip(grads, model.variables))

# 计算梯度

并更新参数

4.6 自定义层 *

可能你还会问,如果现有的这些层无法满足我的要求,我需要定义自己的层怎么办?

事实上,我们不仅可以继承 tf.keras.Model 编写自己的模型类,也可以继承 tf.keras.layers.Layer 编 写自己的层。

class MyLayer(tf.keras.layers.Layer):

def __init__(self):

super().__init__() # 初始化代码

def build(self, input_shape): # input_shape 是一个 TensorShape 类型对象,提供输入的形状 # 在第一次使用该层的时候调用该部分代码,在这里创建变量可以使得变量的形状自适应输入的形状 # 而不需要使用者额外指定变量形状。 # 如果已经可以完全确定变量的形状,也可以在__init__ 部分创建变量 self.variable_0 = self.add_variable(...)

self.variable_1 = self.add_variable(...)

def call(self, input):

# 模型调用的代码(处理输入并返回输出) return output

例如,如果我们要自己实现一个本章第一节 中的全连接层,但指定输出维度为 1,可以按如下方式编写,在 build 方法中创建两个变量,并在 call 方法中使用创建的变量进行运算:

class LinearLayer(tf.keras.layers.Layer):

def __init__(self):

super().__init__()

def build(self, input_shape): # here input_shape is a TensorShape

self.w = self.add_variable(name='w', shape=[input_shape[-1], 1], initializer=tf.zeros_initializer())
self.b = self.add_variable(name='b', shape=[1], initializer=tf.zeros_initializer())

def call(self, X):

y_pred = tf.matmul(X, self.w) + self.b return y_pred

使用相同的方式,可以调用我们自定义的层 LinearLayer:

class Linear(tf.keras.Model):

def __init__(self):

super().__init__() self.layer = LinearLayer()

def call(self, input):

output = self.layer(input) return output
发布了71 篇原创文章 · 获赞 19 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Theo93/article/details/104232395