原文:Applied Neural Networks with TensorFlow 2
协议:CC BY-NC-SA 4.0
六、前馈神经网络
在这一章中,我们将讨论神经网络最普通的版本,前馈神经网络。前馈神经网络是一组人工神经网络,其中神经元之间的连接不形成循环。神经元之间的连接是单向的,只从输入层通过隐藏层向前移动,然后输出。换句话说,这些网络之所以被称为前馈网络,是因为信息的流动是向前的。
Recurrent Neural Networks
我们将在第八章中讨论,是增加了双向功能的前馈神经网络的改进版本。因此,它们不再被视为前馈。
前馈神经网络主要用于监督学习任务。它们在也使用传统机器学习算法的分析应用和定量研究中特别有用。
前馈神经网络非常容易建立,但是它们在计算机视觉和自然语言处理(NLP)问题中不可扩展。此外,前馈神经网络没有对序列数据有用的记忆结构。为了解决可扩展性和内存问题,开发了替代的人工神经网络,如卷积神经网络和循环神经网络,这将在下一章中讨论。
你可能会遇到前馈神经网络的不同名称,如人工神经网络、常规神经网络、常规网络、多层感知器和其他一些网络。不幸的是,这里有一个歧义,但是在本书中,我们总是使用前馈神经网络这个术语。
深层和浅层前馈神经网络
每个前馈神经网络必须有两层:(I)输入层和(ii)输出层。前馈神经网络的主要目标是使用(I)输入层的输入值和(ii)输出层的最终输出值(通过将它们与标签值进行比较)来近似函数。
浅层前馈神经网络
当一个模型只有一个输入和一个输出层用于函数近似时,它被认为是一个浅层前馈神经网络。也称为单层感知器,如图 6-1 所示。
图 6-1
浅层前馈神经网络或单层感知器
浅前馈神经网络中的输出值直接从其权重与相应的输入值和一些偏差的乘积之和来计算。浅前馈神经网络对于近似非线性函数是无用的。为了解决这个问题,我们在输入层和输出层之间嵌入了隐藏层。
深度前馈神经网络
当前馈神经网络具有一个或多个隐藏层,使其能够近似更复杂的函数时,该模型被认为是深度前馈神经网络。也称为多层感知器,如图 6-2 所示。
图 6-2
深度前馈神经网络或多层感知器
一层中的每个神经元都连接到下一层中的神经元,并利用激活函数。
Universal Approximation Theory
表明前馈神经网络可以近似欧氏空间的紧致子集上的任何实值连续函数。该理论还暗示,当给定适当的权重时,神经网络可以代表所有潜在的功能。
由于深度前馈神经网络可以近似任何线性或非线性函数,因此它们被广泛用于现实世界的应用中,包括分类和回归问题。在本章的案例研究中,我们还构建了一个深度前馈神经网络,以获得可接受的结果。
前馈神经网络结构
在前向神经网络中,最左边的层称为输入层,由输入神经元组成。最右边的一层称为输出层,由一组输出神经元或单个输出神经元组成。中间的层称为隐藏层,具有几个神经元,确保非线性近似。
在前馈神经网络中,我们利用具有反向传播、激活函数、成本函数以及权重之上的附加偏差的优化器。这些术语已经在第三章中解释过,因此在此省略。更多细节请参见第三章。让我们更深入地看看前馈神经网络的层。
前馈神经网络中的层
如前所述,我们的通用前馈神经网络结构包括三种类型的层:
-
输入层
-
输出层
-
许多隐藏的层
输入层
输入层是前馈神经网络的第一层,用于将数据输入网络。输入层不利用激活功能,它的唯一目的是将数据输入系统。输入层中神经元的数量必须等于输入系统的特征(即解释变量)的数量。例如,如果我们使用五个不同的解释变量来预测一个响应变量,我们模型的输入层必须有五个神经元。
输出层
输出层是前馈神经网络的最后一层,用于输出预测。输出层中神经元的数量是根据问题的性质决定的。对于回归问题,我们的目标是预测单个值,因此,我们在输出层设置单个神经元。对于分类问题,神经元的数量等于类的数量。例如,对于二元分类,我们在输出层需要两个神经元,而对于具有五个不同类的多类分类,我们在输出层需要五个神经元。输出图层还根据问题的性质利用激活函数(例如,线性激活用于回归,softmax 用于分类问题)。
隐蔽层
创建隐藏层以确保非线性函数的近似。我们可以添加任意多的隐藏层,并且每层的神经元数量可以改变。因此,与输入和输出层相反,我们对隐藏层的使用要灵活得多。隐藏层是引入偏置项的合适层,偏置项不是神经元,而是添加到影响下一层中每个神经元的计算中的常数。隐藏层还利用了激活函数,如 Sigmoid、Tanh 和 ReLU。
在下一节中,我们将构建一个深度前馈神经网络来显示所有这些层的作用。由于 Keras 顺序 API,这个过程将非常容易。
案例研究|汽车 MPG 的燃油经济性
既然我们已经介绍了前馈神经网络的基础知识,我们可以构建一个深度前馈神经网络来预测一辆汽车用一加仑汽油可以行驶多少英里。这个术语通常指的是每加仑英里数(MPG)。对于这个案例研究,我们使用一个经典数据集:自动 MPG 数据集。自动 MPG 最初用于 1983 年美国统计协会博览会。该数据涉及城市循环油耗的预测,以英里/加仑为单位,包含三个多值离散属性和五个连续属性。对于这个案例研究,我们受益于 Keras 库的创建者 Franç ois Chollet 写的一个教程。 1
让我们深入研究代码。请通过 https://colab.research.google.com
创建一个新的 Colab 笔记本。
初始安装和导入
我们将利用 TensorFlow 文档库,它最初并不包含在 Google Colab 笔记本中。因此,我们从使用以下代码的库安装开始案例研究:
# Install tensorflow_docs
!pip install -q git+https://github.com/tensorflow/docs
- 1
- 2
- 3
在这个案例研究中,我们将使用许多库。让我们导入我们将在开始时使用的那些:
# Import the initial libraries to be used
import tensorflow as tf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
- 1
- 2
- 3
- 4
- 5
- 6
请注意,还会有一些其他的导入,会在它们对应的章节里分享。
下载自动 MPG 数据
尽管 Auto MPG 是一个非常受欢迎的数据集,但我们仍然无法通过 TensorFlow 的数据集模块访问该数据集。然而,有一种非常简单的方法(多亏了tf.keras.utils
模块的get_file()
函数)将外部数据加载到我们的 Google Colab 笔记本中,代码如下:
autompg = tf.keras.utils.get_file(
fname='auto-mpg', #filename for local directory
origin='http://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/auto-mpg.data',#URL address to retrieve the dataset
- 1
- 2
- 3
- 4
注意,我们从 UCI 机器学习知识库中检索数据集。加州大学欧文分校提供了一个基本的存储库,以及 Kaggle,在其中您可以访问大量流行的数据集。
数据准备
当我们查看加州大学欧文分校的自动 MPG 页面时,我们可以看到一个代表自动 MPG 数据集中所有变量的属性列表,共享如下:
属性信息:
-
mpg :连续(响应变量)
-
气缸:多值离散
-
位移:连续
-
马力:连续
-
重量:连续
-
加速度:连续
-
年款:多值离散
-
原点:多值离散
-
汽车名称:字符串(每个实例唯一)
数据帧创建
作为最佳实践,我们将使用这些属性名称命名数据集列,并从 Google Colab 目录导入,因为我们已经在上一节中保存了它:
column_names = ['mpg', 'cylinders', 'displacement', 'HP', 'weight', 'acceleration', 'modelyear', 'origin']
df = pd.read_csv(autompg, # name of the csv file
sep=" ", # separator in the csv file
comment=' ', #remove car name sep. with ' '
names=column_names,
na_values = '?', #NA values are coded as '?'
skipinitialspace=True)
df.head(2) #list the first two row of the dataset
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
这里是df.head(2)
的结果,如图 6-3 所示。
图 6-3
自动 MPG 数据集的前两行
删除空值
我们可以用下面的代码检查空值的数量:
df.isna().sum()
- 1
- 2
我们得到的输出如图 6-4 所示。
图 6-4
自动 MPG 数据集中的空值计数
我们在 HP 列中有六个空值。有几种方法可以处理空值。首先,我们可以放弃他们。其次,我们可以使用一种方法来填充它们,例如(a)用其他观测值的平均值填充,或者(b)使用回归方法来插值它们的值。为了简单起见,我们将使用以下代码删除它们:
df = df.dropna() # Drop null values
df = df.reset_index(drop=True) # Reset index to tidy up the dataset
df.show()
- 1
- 2
- 3
- 4
处理分类变量
让我们用 Pandas DataFrame 对象的 info 属性来查看我们的数据集:
df.info() # Get an overview of the dataset
- 1
- 2
如图 6-5 所示,我们可以看到 Auto MPG 数据集有 392 个汽车观察值,没有空值。变量气缸、年款和原点是我们应该考虑使用虚拟变量的分类变量。
图 6-5
自动 MPG 数据集概述
Dummy Variable
是一种特殊的变量类型,仅取值 0 或 1 来指示分类效果的存在与否。在机器学习研究中,分类变量的每个类别都被编码为虚拟变量。但是,省略其中一个类别作为虚拟变量是一个很好的做法,这可以防止多重共线性问题。
如果分类变量的值不表示数学关系,使用虚拟变量尤其重要。这对于原点变量绝对有效,因为值 1、2 和 3 代表美国、欧洲和日本。因此,我们需要为原点变量生成虚拟变量,删除第一个以防止多重共线性,并删除初始的原点变量(原点变量现在用生成的虚拟变量表示)。我们可以用下面几行代码来完成这些任务:
def one_hot_origin_encoder(df):
df_copy = df.copy()
df_copy['EU']=df_copy['origin'].map({1:0,2:1,3:0})
df_copy['Japan']=df_copy['origin'].map({1:0,2:0,3:1})
df_copy = df_copy.drop('origin',axis=1)
return df_copy
df_clean = one_hot_origin_encoder(df)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
这里是df_clean.head(2)
的结果,如图 6-6 所示。
图 6-6
带有虚拟变量的自动 MPG 数据集的前两行
为训练和测试拆分自动 MPG
既然我们已经清理了数据集,是时候将它们分成训练集和测试集了。训练集用于训练我们的神经网络(即优化神经元权重)以最小化误差。测试集被用作从未见过的观察值来测试我们训练的神经网络的性能。
因为我们的数据集是熊猫 DataFrame 对象的形式,所以我们可以使用 sample 属性。我们将 80%的观察值用于训练,20%用于测试。此外,我们还将标签从特性中分离出来,这样我们就可以将特性作为输入。然后,用标签检查结果。
这些任务可以通过下面几行代码来完成:
# Training Dataset and X&Y Split
# Test Dataset and X&Y Split
# For Training
train = df_clean.sample(frac=0.8,random_state=0)
train_x = train.drop('mpg',axis=1)
train_y = train['mpg']
# For Testing
test = df_clean.drop(train.index)
test_x = test.drop('mpg',axis=1)
test_y = test['mpg']
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
既然我们已经将数据集分成了训练集和测试集,那么是时候规范化我们的数据了。如第三章所述,特征缩放是数据准备的重要部分。如果没有特征缩放,特征会对我们的模型产生负面影响。
我们需要提取平均值和标准偏差,以便手动对数据进行标准化。我们可以使用以下代码轻松生成该数据集:
train_stats = train_x.describe().transpose()
- 1
- 2
运行train_stats
可以得到图 6-7 中的如下输出。
图 6-7
列车组统计的 train_stats 数据帧
既然我们已经有了训练集特征的平均值和标准偏差值,那么是时候规范化训练集和测试集了。自定义 规格化器(x) 功能可用于训练、测试和新观察集。
# Feature scaling with the mean
# and std. dev. values in train_stats
def normalizer(x):
return (x-train_stats['mean'])/train_stats['std']
train_x_scaled = normalizer(train_x)
test_x_scaled = normalizer(test_x)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
请注意,我们没有标准化标签(y)值,因为它们的大范围不会对我们的模型构成威胁。
模型构建和培训
现在,我们的数据被清理,并为我们的前馈神经网络流水线做准备。让我们建立我们的模型并训练它。
TensorFlow 导入
我们已经有了一些初始进口。在这一部分中,我们将导入剩余的模块和库来构建、训练和评估我们的前馈神经网络。
剩余的导入包括以下库:
# Importing the required Keras modules containing model and layers
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
# TensorFlow Docs Imports for Evaluation
import tensorflow_docs as tfdocs
import tensorflow_docs.plots
import tensorflow_docs.modeling
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
Sequential()
是我们用于建模的 API,而Dense()
是我们将在前馈神经网络中使用的层。tf.docs
模块将用于模型评估。
具有顺序 API 的模型
用Sequential
API 创建一个模型对象并命名为model
后,我们可以通过添加Dense()
层来塑造我们的空模型。除了最后一个–
之外,每个密集层–
都需要一个激活函数。在这个案例研究中,我们将使用 ReLU,但也可以随意设置其他激活函数,如 Tanh 或 Sigmoid。我们的input_shape
参数必须等于特征的数量,我们的输出层必须只有一个神经元,因为这是一个回归案例。
# Creating a Sequential Model and adding the layers
model = Sequential()
model.add(Dense(8,activation=tf.nn.relu, input_shape= [train_x.shape[1]])),
model.add(Dense(32,activation=tf.nn.relu)),
model.add(Dense(16,activation=tf.nn.relu)),
model.add(Dense(1))
- 1
- 2
- 3
- 4
- 5
- 6
- 7
我们可以用一行代码看到model
的流程图;见图 6-8 :
tf.keras.utils.plot_model(model, show_shapes=True)
- 1
- 2
图 6-8
汽车 MPG 前馈神经网络流程图
模型配置
既然我们已经构建了神经网络的主要网络结构,我们需要在开始训练之前配置优化器、成本函数和指标。我们将在神经网络中使用 Adam 优化器和均方误差(MSE)。此外,TensorFlow 将为我们提供平均绝对误差(MAE)值以及 MSE 值。我们可以用下面的代码配置我们的模型:
# Optimizer, Cost, and Metric Configuration
model.compile(optimizer='adam',
loss='mse',
metrics=['mse','mae']
)
- 1
- 2
- 3
- 4
- 5
- 6
正如在第三章中提到的,对抗过拟合的一个强有力的方法是尽早停止。使用下面的代码行,如果我们在 50 个纪元内没有看到有价值的改进,我们将设置一个早期停止器。
# Early Stop Configuration
early_stop=tf.keras.callbacks.EarlyStopping( monitor="val_loss", patience=50)
- 1
- 2
- 3
既然我们已经配置了我们的模型,我们可以用我们的model
对象的fit
属性来训练我们的模型:
# Fitting the Model and Saving the Callback Histories
history=model.fit(
x=train_x_scaled,
y=train_y,
epochs=1000,
validation_split = 0.2,
verbose=0,
callbacks=[early_stop,
tfdocs.modeling.EpochDots()
])
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
我们留出 20%的训练集进行验证。因此,我们的神经网络甚至会在测试集之前评估模型。我们将 epoch 值设置为 1000,但是如果它不能观察到验证损失/成本的有价值的改进,它将提前停止。最后,回调参数将为我们保存有价值的信息,以便用绘图和其他有用的工具来评估我们的模型。
评估结果
既然我们已经训练了模型,我们就可以评估结果了。我们的 TensorFlow 文档 库允许我们绘制每个时期的损失值。我们可以使用HistoryPlotter
创建一个新的对象,用下面的代码创建这个对象:
plot_obj=tfdocs.plots.HistoryPlotter(smoothing_std=2)
- 1
- 2
创建对象后,我们可以使用 plot 属性来创建绘图,并且我们可以像在 Matplotlib 中一样使用以下代码来设置ylim
和ylabel
值:
plot_obj.plot({'Auto MPG': history}, metric = "mae")
plt.ylim([0, 10])
plt.ylabel('MAE [mpg]')
- 1
- 2
- 3
- 4
图 6-9 显示了我们在每个时期的损失值的概况。
图 6-9
该线图显示了每个时期的平均绝对误差值
有了模型的 evaluate 属性,我们还可以使用测试集来评估我们的模型。如图 6-10 所示,下面几行将使用我们的测试集生成损耗、MAE 和 MSE 值:
loss,mae,mse=model.evaluate(test_x_scaled,
test_y,
verbose=2)
print("Testing set Mean Abs Error: {:5.2f} MPG".format(mae))
- 1
- 2
- 3
- 4
- 5
图 6-10
汽车 MPG 训练模型的评估结果
我们可以用单行代码使用测试集标签来生成预测:
test_preds = model.predict(test_x_scaled).flatten()
- 1
- 2
最后,我们可以使用以下代码行绘制测试集标签(实际值)与测试集特性生成的预测(见图 6-11 )的对比图:
evaluation_plot = plt.axes(aspect='equal')
plt.scatter(test_y, test_preds)#Scatter Plot
plt.ylabel('Predictions [mpg]')#Y for Predictions
plt.xlabel('Actual Values [mpg]')#X for Actual Values
plt.xlim([0, 50])
plt.ylim([0, 50])
plt.plot([0, 50], [0, 50]) #line plot for comparison
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
图 6-11
实际测试标签与其预测值的散点图
我们还可以生成一个直方图,显示误差项在零附近的分布(见图 6-12 ),这是我们模型中偏差的一个重要指标。下面几行代码生成了上述直方图:
error = test_preds - test_y
plt.hist(error, bins = 25)
plt.xlabel("Prediction Error [mpg]")
plt.ylabel("Count")
- 1
- 2
- 3
- 4
- 5
图 6-12
直方图显示了零附近模型的误差分布
用新的观察做预测
我们之前生成的散点图和直方图都表明我们的模型是健康的,我们的损失值也在可接受的范围内。因此,我们可以使用我们训练好的模型,使用我们自己的虚拟观察来进行新的预测。
我将使用以下代码行创建一辆虚拟汽车:
# Prediction for Single Observation
# What is the MPG of a car with the following info:
new_car = pd.DataFrame([[8, #cylinders
307.0, #displacement
130.0, #HP
5504.0, #weight
12.0, #acceleration
70, #modelyear
1 #origin
]], columns=column_names[1:])
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
该代码将创建以下带有单次观察的 Pandas 数据帧,如图 6-13 所示。
图 6-13
单次观测的熊猫数据框架
在输入训练模型之前,我们需要创建虚拟变量并使观察结果正常化。完成这些操作后,我们可以简单地使用模型的预测属性。我们可以用这些行来完成这些操作:
new_car = normalizer(one_hot_origin_encoder(new_car))
new_car_mpg = model.predict(new_car).flatten()
print('The predicted miles per gallon value for this car is:', new_car_mpg)
- 1
- 2
- 3
- 4
前面的代码给出了以下输出:
The prediction miles per gallon value for this car is: [14.727904]
- 1
- 2
结论
前馈神经网络是广泛用于分析应用和定量研究的人工神经网络。它们是最古老的人工神经网络,通常被称为多层感知器。它们被认为是人工神经网络家族的骨干。你可以在卷积神经网络的末端找到它们。循环神经网络是从前馈神经网络发展而来的,增加了双向性。
在下一章中,我们将深入探讨卷积神经网络,它是一组神经网络家族,广泛应用于计算机视觉、图像和视频处理等领域。
Footnotes 1
版权所有©2017 Fran ois chollet
特此免费授予获得本软件和相关文档文件(“软件”)副本的任何人不受限制地经营本软件的权利,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或出售本软件副本的权利,并允许获得本软件的人在遵守以下条件的情况下这样做:
上述版权声明和本许可声明应包含在软件的所有副本或重要部分中。
本软件按“原样”提供,不含任何明示或暗示的担保,包括但不限于对适销性、特定用途适用性和不侵权的担保。在任何情况下,作者或版权所有者都不对任何索赔、损害或其他责任负责,无论是在合同诉讼、侵权诉讼或其他诉讼中,还是在与软件或软件的使用或其他交易相关的诉讼中。
七、卷积神经网络
可以肯定地说,最强大的监督深度学习模型之一是卷积神经网络(缩写为 CNN 或 ConvNet)。CNN 是一类深度学习网络,多应用于图像数据。然而,CNN 结构可以用于各种现实世界的问题,包括但不限于,图像识别、自然语言处理、视频分析、异常检测、药物发现、健康风险评估、推荐系统和时间序列预测。
CNN 通过使用在训练数据中找到的更基本的模式来组合复杂的模式,实现了高水平的准确性。例如,从线条到眉毛,从两条眉毛到一张人脸,然后到一个完整的人的形象,CNN 可以通过使用纯粹的线条正确地检测图像中的人。为了组合这些模式,CNN 需要少量的数据准备,因为它们的算法自动执行这些操作。与用于图像处理的其他模型相比,CNN 的这一特性提供了优势。
今天,CNN 的整体架构已经简化。CNN 的最后一部分非常类似于前馈神经网络(正则网络,多层感知器),其中有带权重和偏差的完全连接的神经元层。就像在前馈神经网络中一样,在 CNN 中有一个损失函数(例如,交叉熵,MSE),多个激活函数和一个优化器(例如,SGD,Adam 优化器)。此外,尽管在 CNN 中,也有卷积层、汇集层和平坦层。
在下一节中,我们将看看为什么使用 CNN 进行图像处理是一个好主意。
Note
我通常会参考图像数据来举例说明 CNN 的概念。但是,请注意,这些示例仍然适用于不同类型的数据,如音频波或股票价格。
为什么选择卷积神经网络?
前馈神经网络的主要结构特征是所有神经元的层内连通性。例如,当我们有 28 x 28 像素的灰度图像时,我们最终在一个似乎易于管理的层中有 784 (28 x 28 x 1)个神经元。然而,大多数图像有更多的像素,而且它们不是灰度的。因此,当我们在 4K 超高清中有一组彩色图像时,我们最终在输入层中有 26,542,080 (4096 x 2160 x 3)个不同的神经元连接到下一层中的神经元,这是不可管理的。因此,我们可以说前馈神经网络对于图像分类是不可扩展的。然而,特别是当涉及到图像时,两个单独的像素之间似乎几乎没有相关性或关系,除非它们彼此靠近。这一重要发现引出了卷积层和池层的概念,在每个 CNN 架构中都可以找到。
CNN 架构
通常,在一个 CNN 架构中,开头有几个卷积层和池层,主要用于简化图像数据复杂度,减小其大小。此外,它们对于从图像中观察到的基本模式中提取复杂模式非常有用。在使用了几个卷积层和池层(由激活函数支持)之后,我们将二维或三维数组中的数据改造成一个带有扁平化层的一维数组。展平图层后,一组全连通图层以展平后的一维数组为输入,完成分类或回归任务。让我们分别来看看这几层。
CNN 中的层
我们能够在卷积神经网络中使用许多不同的层。然而,卷积层、池层和全连接层是最重要的层。因此,在我们的案例研究中实现这些层之前,让我们快速了解一下这些层。
卷积层
卷积层是我们从数据集的影像中提取特征的第一层。由于像素仅与相邻的和其他接近的像素相关,卷积允许我们保留图像不同部分之间的关系。卷积层的任务仅仅是用较小的像素滤波器对图像进行滤波,以减小图像的尺寸,而不丢失像素之间的关系。当我们使用步长为 1 x 1 的 3 x 3 像素滤波器对 5 x 5 像素图像进行卷积时(每步移动 1 像素),我们最终会得到 3 x 3 像素的输出(复杂度降低 64%),如图 7-1 所示。
图 7-1
5 x 5 像素图像与 3 x 3 像素过滤器的卷积(跨距= 1 x 1 像素)
过滤
通过将一部分图像数据中的每个值乘以相应的滤波器值来执行滤波。在图 7-2 中,第一次操作如下。(图 7-1 所示的所有卷积运算请参见表 7-1 )。
表 7-1
图 7-2 的计算表
|
行
|
计算
|
结果
|
| — | — | — |
| 第一排 | (1×0) + (0x0) + (0x0) + | |
| 第二排 | (0x0) + (1×1) + (1×1) + | = 5 |
| 第三排 | (1×1) + (0x0) + (1×1) | |
图 7-2
图 7-1 所示卷积的第一次滤波操作
使用过大的过滤器会降低复杂度,但也会导致重要模式的丢失。因此,我们应该设置一个最佳的过滤器大小来保持模式,并充分降低数据的复杂性。
大步
Stride 是一个参数,用于设置每次操作后滤镜将移动多少像素。对于前面的例子
-
如果我们选择 1 x 1 像素步距,我们最终将滤波器移动 9 次来处理所有数据。
-
如果我们选择 2×2 像素的步距,我们可以在 4 次滤波操作中处理整个 5×5 像素的图像。
使用大跨距值会减少滤波器计算的次数。大的步幅值将显著降低模型的复杂性,但是我们可能会在这个过程中丢失一些模式。因此,我们应该始终设置一个最佳步幅值–
不要太大,也不要太小。
汇集层
当构造 CNN 时,几乎标准的做法是在每个卷积层之后插入池层,以减小表示的空间大小,从而减少参数计数,这降低了计算复杂度。此外,合并层也有助于解决过拟合问题。
对于池操作,我们通过选择这些像素内的最大值、平均值或和值来选择池大小,以减少参数的数量。最大池化是最常见的池化技术之一,其演示如下。
图 7-3
最大池化 2 x 2
在合并层中,在设置 N×N 像素的合并大小后,我们将图像数据划分为 N×N 个像素部分,以选择这些划分部分的最大值、平均值或和值。
对于图 7-3 中的例子,我们将 4 x 4 像素的图像分割成 2 x 2 像素的部分,总共得到 4 个部分。由于我们使用最大池,我们选择这些部分中的最大值,并创建一个仍包含原始图像数据中的模式的简化图像。
选择 N x N 的最佳值对于保持数据中的模式同时实现足够的复杂度降低也是至关重要的。
一组完全连接的层
CNN 中的全连接网络是嵌入式前馈神经网络,其中一层中的每个神经元都链接到下一层中的神经元,以确定标签上每个参数的真实关系和效果。由于卷积和池层的存在,我们的时空复杂度大大降低,我们可以在 CNN 的末端构建一个完全连接的网络来对我们的图像进行分类。一组完全连接的层如图 7-4 所示:
图 7-4
具有两个隐藏层的完全连接的层
一个完整的 CNN 模型
现在你对 CNN 的各个层有了一些了解,是时候分享图 7-5 中完整卷积神经网络的概貌了:
图 7-5
卷积神经网络示例
虽然特征学习阶段是在卷积层和池层的帮助下执行的,但分类是使用完全连接的层集执行的。
案例研究| MNIST 影像分类
既然我们已经介绍了卷积神经网络的基础知识,我们可以构建一个用于图像分类的 CNN。在这个案例研究中,我们使用了用于图像分类的最老套的数据集:MNIST 数据集,代表修改后的国家标准与技术研究所数据库。这是一个广泛的手写数字数据库,通常用于训练各种图像处理系统。
下载 MNIST 数据
MNIST 数据集是用于影像分类的最常用数据集之一,可从许多不同的来源访问。Tensorflow 允许我们直接从其 API 导入和下载 MNIST 数据集。因此,我们从下面两行开始,在 Keras API 下导入 TensorFlow 和 MNIST 数据集。
import tensorflow as tf
import tensorflow_datasets as tfds
(x_train,y_train),(x_test,y_test)=tfds.as_numpy(tfds.load('mnist', #name of the dataset
split=['train', 'test'], #both train & test sets
batch_size=-1, #all data in single batch
as_supervised=True, #only input and label
shuffle_files=True #shuffle data to randomize
))
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
MNIST 数据库包含来自美国人口普查局员工和美国高中生的 60,000 幅训练图像和 10,000 幅测试图像。因此,在第二行中,我们将这两组分为训练组和测试组,还将标签和图像分开。x_train
和x_test
部分包含灰度 RGB 码(从 0 到 255),而y_train
和y_test
部分包含从 0 到 9 的标签,代表它们实际上是哪个数字。为了可视化这些数字,我们可以从 Matplotlib 获得帮助。
import matplotlib.pyplot as plt
img_index = 7777 #You may pick a number up to 60,000
print("The digit in the image:", y_train[img_index])
plt.imshow(x_train[img_index].reshape(28,28),cmap='Greys')
- 1
- 2
- 3
- 4
- 5
当我们运行前面的代码时,我们将得到图像的灰度可视化,如图 7-6 所示。
图 7-6
样本图像及其标签的可视化
我们还需要知道数据集的形状,以便将其导入卷积神经网络。因此,我们在下面的代码中使用 NumPy 数组的shape
属性:
x_train.shape
- 1
- 2
我们得到的输出是(60000,28,28,1)。您可能已经猜到,60000 代表训练数据集中的图像数量;(28,28)代表图像的大小,28 x 28 像素;1 表示我们的图像没有颜色。
重塑和标准化图像
使用 TensorFlow 的 dataset API,我们已经创建了一个用于训练的四维 NumPy 数组,这是所需的数组维数。另一方面,我们必须规范化我们的数据,因为这是神经网络模型中的最佳实践。我们可以通过将灰度 RGB 代码分为 255(最大灰度 RGB 代码减去最小灰度 RGB 代码)来实现这一点。这可以通过下面的代码来完成:
# Making sure that the values are float so that we can get decimal points after division
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
# Normalizing the grayscale RGB codes by dividing it to the "max minus min grayscale RGB value".
x_train /= 255
x_test /= 255
print('x_train shape:', x_train.shape)
print('Number of images in x_train', x_train.shape[0])
print('Number of images in x_test', x_test.shape[0])
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
构建卷积神经网络
我们通过使用高级 Keras 顺序 API 来简化开发过程,从而构建我们的模型。我想提到的是,还有其他高级 TensorFlow APIs 如 Estimators、Keras Functional API 和另一种 Keras Sequential API 方法,它们帮助我们创建具有高级知识的神经网络。这些不同的选项可能会导致混淆,因为它们的实现结构各不相同。所以,如果你看到同一个神经网络完全不同的代码,虽然都用 TensorFlow,这就是为什么。
我们使用最简单的 tensor flow API–
Keras Sequential API–
,因为我们不需要太多的灵活性。因此,我们从 Keras 导入Sequential
模型对象,并添加 Conv2D、MaxPooling、Flatten、Dropout 和 Dense 图层。我们已经介绍了 Conv2D、MaxPooling 和 Dense 层。此外,丢弃层通过在训练时忽略一些神经元来对抗过拟合,而展平层在构建完全连接的层之前将二维数组展平为一维数组。
#Importing the required Keras modules containing model and layers
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense,Conv2D, Dropout,Flatten,MaxPooling2D
#Creating a Sequential Model and adding the layers
model = Sequential()
model.add(Conv2D(28,kernel_size=(3,3), input_shape=(28,28,1)))
model.add(MaxPooling2D(pool_size=(2,2))
model.add(Flatten()) #Flattening the 2D arrays for fully connected layers
model.add(Dense(128,activation=tf.nn.relu))
model.add(Dropout(0.2))
model.add(Dense(10,activation=tf.nn.softmax))
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
对于第一致密层,我们可以用任何数目进行实验;然而,最终的密集层必须有 10 个神经元,因为我们有 10 个数字类(0,1,2,…,9)。你可以尝试内核大小、池大小、激活函数、退出率和第一密集层中的神经元数量,以获得更好的结果。
编译和拟合模型
使用前面的代码,我们创建了一个非优化的空 CNN。现在是时候为优化器设置一个使用度量标准的给定损失函数了。然后,我们可以通过使用我们的训练数据来拟合模型。我们将使用以下代码执行这些任务,并查看图 7-7 中所示的输出:
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
model.fit(x=x_train,y=y_train, epochs=10)
Output:
- 1
- 2
- 3
- 4
- 5
- 6
图 7-7
CNN 在 MNIST 数据集上训练的新纪元统计
您可以试验优化器、损失函数、指标和时期。然而,即使 Adam optimizer、分类交叉熵和准确性是合适的指标,也可以随意试验。
纪元编号可能看起来有点小。但是,您可以轻松达到 98–99%的测试准确度。由于 MNIST 数据集不需要强大的计算能力,您也可以尝试使用纪元编号。
评估模型
最后,您可以使用单行代码使用x_test
和y_test
来评估训练好的模型:
model.evaluate(x_test, y_test)
- 1
- 2
图 7-8 中的结果显示了基于测试集性能计算的 10 个时期的评估结果。
图 7-8
我们的 MNIST 训练的 CNN 模型的评估结果具有 98.5%的准确性
我们用这样一个基本模型达到了 98.5%的准确率。坦率地说,在大多数图像分类情况下(例如,对于自动驾驶汽车),我们甚至不能容忍 0.1%的误差。打个比方,如果我们建立一个自动驾驶系统,0.1%的误差很容易意味着 1000 起事故中的 1 起。然而,对于我们的第一个模型,我们可以说这个结果是杰出的。
我们还可以使用以下代码进行单独的预测:
img_pred_index = 1000
plt.imshow(x_test[img_pred_index].reshape(28,28),
cmap='Greys')
pred = model.predict(
x_test[img_pred_index].reshape(1,28,28,1))
print("Our CNN model predicts that the digit in the image is:", pred.argmax())
- 1
- 2
- 3
- 4
- 5
- 6
- 7
我们训练过的 CNN 模型会将图像归类为数字“5”(五,这里是图 7-9 中图像的视觉。
图 7-9
我们的模型正确地将该图像分类为数字 5(五)
请注意,由于我们打乱了数据集,您可能会看到索引 1000 的不同图像。但是你的模型仍然以大约 98%的准确率预测这个数字。
虽然图像没有数字 5 ( five )的好笔迹,但是我们的模型能够将其正确分类。
保存已训练的模型
在这个案例研究中,我们使用 Tensorflow 的 Keras Sequential API 构建了第一个卷积神经网络来分类手写数字。我们实现了超过 98%的准确率,现在我们甚至可以用下面几行代码来保存这个模型:
# Save the entire model as a SavedModel.
# Create a 'saved_model' folder under the 'content' folder of your Google Colab Directory.
!mkdir -p saved_model
# Save the full model with its variables, weights, and biases.
model.save('saved_model/digit_classifier')
- 1
- 2
- 3
- 4
- 5
- 6
使用 SavedModel,您可以重建训练过的 CNN,并使用它来创建不同的应用程序,如数字分类器游戏或图像到数字的转换器!
Note
有两种类型的保存选项——新颖别致的“SavedModel”和老式的“H5”格式。如果您想进一步了解这些格式之间的差异,请查看 TensorFlow 指南的保存和加载部分:
www.tensorflow.org/tutorials/keras/save_and_load
结论
卷积神经网络是非常重要和有用的神经网络模型,主要用于图像处理和分类。您可以对图像中的对象进行检测和分类,这可能会用于许多不同的领域,例如制造业中的异常检测、交通运输中的自动驾驶以及零售业中的库存管理。CNN 对于处理音频和视频以及金融数据也很有用。因此,利用 CNN 的应用类型比前面提到的更广泛。
CNN 由用于特征学习的卷积层和汇集层以及一组用于预测和分类的全连接层组成。CNN 降低了数据的复杂性,这是前馈神经网络无法单独做到的。
在下一节中,我们将介绍另一种基本的神经网络架构:循环神经网络(RNNs),它对于音频、视频、文本和时序数据等序列数据特别有用。
八、循环神经网络
在第六章中,我们介绍了前馈神经网络,这是最基本的人工神经网络类型。然后,我们在第七章中介绍了卷积神经网络作为人工神经网络架构的类型,它在图像数据上表现得非常好。现在,是时候介绍另一种类型的人工神经网络体系结构,即循环神经网络或 RNN,它是专为处理序列数据而设计的。
序列数据和时间序列数据
rnn 对于序列数据非常有用。如果您熟悉预测分析,您可能知道与横截面数据相比,使用时间序列数据进行预测需要不同的方法。
横断面数据是指在一个时间点记录的一组观察数据。今年年底许多不同股票的回报率百分比就是横截面数据的一个例子。
时间序列数据是指在一段时间内以相等的时间间隔记录的一组观察值。一只股票在过去 10 年中每年的回报率百分比就是时间序列数据的一个例子。
在时序数据集中,观察值是基于时间戳记录的,但这不能推广到序列数据。序列数据是一个更广泛的术语。序列数据是观察顺序很重要的任何数据。因此,时间序列是一种特殊类型的按时间戳排序的序列数据。例如,一个句子(由几个单词组成)的顺序对其含义至关重要。我们不能随意改变单词的顺序,并期望它有所指。然而,句子中的单词没有时间标记,所以它们不携带任何时间信息。因此,它们只是序列数据,而不是时间序列数据。序列数据(但不是时间序列数据)的另一个例子是 DNA 序列。DNA 序列的顺序是至关重要的,它们不是根据时间戳排序的。序列数据和时序数据的关系如图 8-1 所示。
图 8-1
序列数据和时间序列数据之间的关系
现在,您已经知道了时序数据和更广泛的术语–
序列数据之间的关系,您也知道当我们提到序列数据时,我们也指时序数据,除非另有说明。
与其他神经网络架构相比,RNNs 通常在序列数据问题上做得更好。因此,了解如何实现用于序列数据问题的循环神经网络是很重要的,例如股票价格预测、销售预测、DNA 序列建模和机器翻译。
RNNs 和顺序数据
前馈神经网络有三个主要限制,这使得它们不适合序列数据:
-
前馈神经网络不能考虑阶数。
-
前馈神经网络需要固定的输入大小。
-
前馈神经网络不能输出不同长度的预测。
序列数据的基本特征之一是其顺序的重要性。重新排列月销售额的顺序可以将我们从上升趋势引向下降趋势,我们对下个月销售额的预测将会发生巨大的变化。这就是前馈神经网络的局限性所在。在前馈神经网络中,由于这种限制,不能考虑数据的顺序。重新安排月销售额的顺序会得到完全相同的结果,这证明他们不能利用投入的顺序。
在序列数据研究中,问题的性质各不相同,如图 8-2 所示。虽然机器翻译任务本质上是多对多的问题,但是情感分析是多对一的任务。特别是在可能有许多输入的任务中,我们经常需要可变的输入大小。然而,前馈神经网络要求模型具有固定的输入大小,这使得它们不适用于许多序列数据问题。如果模型被训练为使用过去 7 天进行预测,则不能使用第 8 天。
图 8-2
深度学习中的潜在序列数据任务
最后,前馈神经网络不能输出不同的长度预测。特别是在机器翻译问题中,我们无法预测输出的大小。例如,英语中的一个长句可以很容易地用另一种语言中的三个单词的句子来表达。前馈神经网络不能提供这种灵活性。但是,rnn 提供了这种能力,因此,它们被广泛用于机器翻译等任务。
RNNs 的基础
让我们快速回顾一下 rnn 的历史,然后简单介绍一下 rnn 的真实使用案例及其运行机制。
RNNs 的历史
我们已经在前面的章节中介绍了一些 RNNs 的历史。开发 RNNs 的主要动机是消除前面提到的问题。多年来,研究人员基于他们特定的研究领域开发了不同的 RNN 架构。RNN 有许多变体,不同 RNN 架构的总数可以用几十来表示。第一个 RNN 是约翰·霍普菲尔德在 1982 年开发的霍普菲尔德网络。1997 年,Hochreiter 和 Schmidhuber 发明了长短期记忆(LSTM)网络,以解决当时现有 rnn 的问题。LSTM 网络在序列数据任务上表现非常好,它们是非常流行的 RNN 架构,今天被广泛使用。2014 年,Kyunghyun Cho 引入了循环门控单元(gru)来简化 LSTM 网络。GRUs 在许多任务上的表现也非常出色,其内部结构比 LSTMs 更加直观。在本章中,我们将更详细地介绍简单的 rnn、LSTMs 和 gru。
RNNs 的应用
rnn 有大量的实际应用,其中一些应用只能用 rnn 构建。如果没有 RNNs,我们将无法在许多领域提供有竞争力的解决方案,例如机器翻译或情感分析。以下是 RNN 潜在使用案例的非详尽列表:
-
语法学习
-
手写识别
-
人体动作识别
-
机器翻译
-
音乐创作
-
预测蛋白质的亚细胞定位
-
医疗保健路径中的预测
-
蛋白质同源性检测
-
节奏学习
-
机器人学
-
情感分析
-
语音识别和合成
-
时间序列异常检测
-
时间序列预测
RNNs 的机制
RNN 通过将以前的信息保存在内存中来利用它们,这些信息在 RNN 神经元中被保存为“状态”。
在深入 LSTMs 和 GRUs 的内部结构之前,我们先用一个基本的天气预报例子来了解一下内存结构。我们想通过使用序列中提供的信息来猜测是否会下雨。该数据序列可以从文本、语音或视频中获得。每有新的信息,我们就慢慢更新降雨的概率,最后得出结论。在图 8-3 中显示了这项任务。
图 8-3
一个简单的天气预报任务:会下雨吗?
在图 8-3 中,我们首先记录了多云天气。这一单一信息可能是降雨的指示,其计算为 50%(或 0.5)的降雨概率。然后,我们收到以下输入:拥挤的街道。拥挤的街道意味着人们在外面,这意味着降雨的可能性更小,因此,我们的估计下降到 30%(或 0.3)。然后,我们被提供了更多的信息:膝盖疼痛。人们认为风湿病患者在下雨前会感到膝盖疼痛。因此,我的估计上升到 70%(或 0.7)。最后,当我们的模型将闪电作为最新信息时,集体估计增加到 90%(或 0.9)。在每个时间间隔,我们的神经元使用其包含先前信息–
的存储器–
,并在该存储器上添加新信息,以计算降雨的可能性。可以在层级别以及单元级别设置存储器结构。图 8-4 显示了细胞级 RNN 机制,(I)左侧为折叠版本,(ii)右侧为展开版本。
图 8-4
基于细胞的循环神经网络活动
RNN 类型
如前所述,RNNs 有许多不同的变体。在本节中,我们将介绍我们经常遇到的三种 RNN:
-
简单的 RNN
-
长短期记忆(LSTM)网络
-
门控循环单元(GRU)网络
你可以在图 8-5 中找到这些可选 RNN 细胞的可视化。
图 8-5
简单 RNN、门控循环单位和长短期记忆细胞
如图 8-5 所示,所有这三种备选方案都具有共同的 RNN 特征:
-
它们都将 t-1 状态(存储器)作为先前值的表示带入计算中。
-
它们都应用某种激活函数并进行矩阵运算。
-
它们都计算时间 t 的当前状态
-
他们重复这个过程来完善他们的权重和偏差值。
让我们详细检查这三种选择。
简单 RNNs
简单的 rnn 是神经元节点的网络,其被设计在连接的层中。简单 RNN 单元的内部结构如图 8-6 所示。
图 8-6
一种简单的 RNN 单元结构
在一个简单的 RNN 单元中,有两个输入:(I)来自前一时间步(t-1)的状态和(ii)在时间 t 的观察值。在激活函数(通常为 Tanh)之后,输出作为时间 t 的状态传递给下一个单元。因此,前一个信息的效果会在每一步传递给下一个单元格。
简单的 RNNs 可以解决许多序列数据问题,并且它们不是计算密集型的。因此,在资源有限的情况下,这可能是最佳选择。有必要了解简单的 RNNs 然而,它容易出现几个技术问题,如消失梯度问题。因此,我们倾向于使用更复杂的 RNN 变体,如长短期记忆(LSTM)和门控循环单位(GRU)。
长短期记忆(LSTM)
长短期记忆(LSTM)网络是由 Hochreiter 和 Schmidhuber 在 1997 年发明的,在许多不同的应用中提高了最高的精度性能,旨在解决序列数据问题。
LSTM 单元由单元状态、输入门、输出门和遗忘门组成,如图 8-7 所示。这三个门控制进出 LSTM 单元的信息流。此外,LSTM 单元具有单元状态和隐藏状态。
图 8-7
一种长短期记忆单元结构
LSTM 网络非常适合于任何格式的序列数据问题,并且它们不容易出现消失梯度问题,这在简单的 RNN 网络中是常见的。另一方面,我们可能仍然会遇到爆炸梯度问题,梯度趋向于无穷。LSTM 网络的另一个缺点是计算密集型。使用 LSTM 训练模型可能需要大量的时间和处理能力,这是开发 gru 的主要原因。
门控循环单元
门控循环单元由 Kyunghyun Cho 于 2014 年推出。正如 LSTMs 一样,gru 也是 RNNs 中处理序列数据的门控机制。然而,为了简化计算过程,GRUs 使用两个门:(I)复位门和(ii)更新门。gru 也为隐藏状态和单元状态使用相同的值。图 8-8 显示了门控循环单元的内部结构。
图 8-8
一种门控循环单元结构
当计算资源有限时,gru 是有用的。即使 gru 在某些应用中优于 lstm,lstm 通常也优于 gru。在处理序列数据时,用 LSTM 和 GRU 训练两个模型并选择性能最好的一个模型是一个好策略,因为这两个备选选通机制的性能会因情况而异。
案例研究|带有 IMDB 评论的情感分析
既然我们已经讨论了循环神经网络的概念部分,现在是进行案例研究的时候了。一般来说,您不必记住简单 rnn、LSTMs 和 gru 的内部工作结构来构建循环神经网络。TensorFlow APIs 使得构建在几个任务上表现良好的 rnn 变得非常容易。在本节中,我们将使用 IMDB 评论数据库进行情感分析案例研究,这是受 TensorFlow 的官方教程“用 RNN 进行文本分类”的启发。 1
为 GPU 加速培训准备我们的 Colab
在深入研究我们的数据之前,有一个关键的环境调整:我们需要在我们的 Google Colab 笔记本中激活 GPU 培训。激活 GPU 训练是一项相当简单的任务,但如果不这样做,你将永远处于 CPU 训练模式。
请转到您的 Google Colab 笔记本,选择运行时➤的“更改运行时类型”菜单来启用 GPU 加速器,如图 8-9 所示。
如前几章所述,使用 GPU 或 TPU –
代替 CPU –
进行训练通常会加快训练速度。既然我们已经在模型中启用了 GPU,我们可以使用以下代码来确认是否为训练激活了 GPU:
import tensorflow as tf
print("Num GPUs Available: ", len(tf.config.experimental.list_physical_devices('GPU')))
Output: Num GPUs Available: 1
- 1
- 2
- 3
- 4
- 5
图 8-9
在 Google Colab 中启用 GPU 加速
IMDB 评论
IMDB 评论数据集是由 Andrew L. Maas 从流行的电影评级服务 IMDB 收集和准备的大型电影评论数据集。 2 IMDB reviews 用于二元情感分类,不管一个评论是正面还是负面。IMDB 评论包含 25,000 条用于训练的电影评论和 25,000 条用于测试的电影评论。所有这 50,000 条评论都是有标签的数据,可能用于有监督的深度学习。此外,还有另外 50,000 篇未标记的评论,我们不会在本案例研究中使用。
幸运的是,TensorFlow 已经处理了原始文本数据,并为我们准备了单词袋格式。此外,我们还可以访问原始文本。准备单词包是一项自然语言处理(NLP)任务,我们将在接下来的章节 9 中介绍。因此,在这个例子中,我们几乎不用任何 NLP 技术。相反,我们将使用经过处理的词袋版本,以便我们可以轻松地建立我们的 RNN 模型来预测评论是正面还是负面的。
用于数据集下载的 TensorFlow 导入
我们从两个初始导入开始,即主 TensorFlow 导入和 TensorFlow 数据集导入,以加载数据:
import tensorflow as tf
import tensorflow_datasets as tfds
- 1
- 2
- 3
从 TensorFlow 加载数据集
TensorFlow 提供了几个流行的数据集,可以直接从tensorflow_datasets
API 加载。tensorflow_datasets
API 的load()
函数返回两个对象:(I)包含训练、测试和未标记集的字典,以及(ii)关于 IMDB 评论数据集的信息和其他相关对象。我们可以用下面的代码将它们保存为变量:
# Dataset is a dictionary containing train, test, and unlabeled datasets
# Info contains relevant information about the dataset
dataset, info = tfds.load('imdb_reviews/subwords8k',
with_info=True,
as_supervised=True)
- 1
- 2
- 3
- 4
- 5
- 6
理解单词袋概念:文本编码和解码
单词包是描述单词在文档中出现的文本表示。这种表示是基于词汇创建的。在我们的数据集中,评论使用 8185 个单词的词汇表进行编码。我们可以通过之前创建的“info”对象访问编码器。
# Using info we can load the encoder which converts text to bag of words
encoder = info.features['text'].encoder
print('Vocabulary size: {}'.format(encoder.vocab_size))
output: Vocabulary size: 8185
- 1
- 2
- 3
- 4
- 5
通过使用这个编码器,我们可以对新的评论进行编码:
# You can also encode a brand new comment with encode function
review = 'Terrible Movie!.'
encoded_review = encoder.encode(review)
print('Encoded review is {}'.format(encoded_review))
output: Encoded review is [3585, 3194, 7785, 7962, 7975]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
我们也可以如下解码编码的评论:
# You can easily decode an encoded review with decode function
original_review = encoder.decode(encoded_review)
print('The original review is "{}"'.format(original_review))
output: The original review is "Terrible Movie!."
- 1
- 2
- 3
- 4
- 5
- 6
准备数据集
我们已经在“dataset”对象中保存了我们的评论,这是一个包含三个键的字典:(i) train,(ii) test,和(iii) unlabeled。通过使用这些键,我们将用下面的代码分割我们的训练集和测试集:
# We can easily split our dataset dictionary with the relevant keys
train_dataset, test_dataset = dataset['train'], dataset['test']
- 1
- 2
- 3
我们还需要调整我们的数据集以避免任何偏见,并填充我们的评论,以便所有的评论长度相同。我们需要选择一个大的缓冲区大小,以便我们可以有一个混合良好的训练数据集。此外,为了避免过多的计算负担,我们将序列长度限制为 64。
BUFFER_SIZE = 10000
BATCH_SIZE = 64
train_dataset = train_dataset.shuffle(BUFFER_SIZE)
train_dataset = train_dataset.padded_batch(BATCH_SIZE)
test_dataset = test_dataset.padded_batch(BATCH_SIZE)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
Padding
填充是将序列数据编码成连续批次的一种有用方法。为了能够将所有序列调整到一个定义的长度,我们必须填充或截断数据集中的一些序列。
构建循环神经网络
既然我们的训练和测试数据集已经准备好输入到模型中,我们就可以开始用 LSTM 单元构建我们的 RNN 模型了。
用于模型构建的导入
我们使用 Keras 顺序 API 来构建我们的模型。我们还需要密集、嵌入、双向、LSTM 和漏失层来构建我们的 RNN 模型。我们还需要引入二进制交叉熵作为损失函数,因为我们使用二进制分类来预测评论是正面还是负面。最后,我们使用 Adam 优化器通过反向传播来优化我们的权重。这些组件是通过以下代码行导入的:
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import (Dense,
Embedding,
Bidirectional,
Dropout,
LSTM)
from tensorflow.keras.losses import BinaryCrossentropy
from tensorflow.keras.optimizers import Adam
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
创建模型并用层填充它
我们使用一个编码层、两个双向包裹的 LSTM 层、两个密集层和一个分离层。我们从嵌入层开始,它将单词索引序列转换成向量序列。嵌入层为每个单词存储一个向量。然后,我们添加两个包裹在双向层中的 LSTM 层。双向层通过 LSTM 层来回传播输入,然后连接输出,这有助于了解长程相关性。然后,我们添加一个具有 64 个神经元的密集层来增加复杂性,添加一个丢弃层来防止过拟合。最后,我们添加一个最终的密集层来进行二元预测。以下代码行创建了一个顺序模型,并添加了所有提到的层:
model = Sequential([
Embedding(encoder.vocab_size, 64),
Bidirectional(LSTM(64, return_sequences=True)),
Bidirectional(LSTM(32)),
Dense(64, activation="relu"),
Dropout(0.5),
Dense(1)
])
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
如图 8-10 所示,我们还可以看到带有model.summary()
的车型概况。
图 8-10
RNN 模式综述
我们还可以创建我们的 RNN 模型的流程图,如图 8-11 所示,使用以下代码行:
tf.keras.utils.plot_model(model)
- 1
- 2
图 8-11
RNN 模式的流程图
编译和拟合模型
既然我们构建了一个空模型,现在是时候用下面的代码配置损失函数、优化器和性能指标了:
model.compile(
loss=BinaryCrossentropy(from_logits=True),
optimizer=Adam(1e-4),
metrics=['accuracy'])
- 1
- 2
- 3
- 4
- 5
我们的数据和模型已经可以进行训练了。我们可以使用model.fit()
函数来训练我们的模型。大约 10 个时期对于训练我们的情感分析模型来说是足够的,这可能需要大约 30 分钟。我们还将我们的训练过程保存为一个变量,以评估模型随时间的表现。
history = model.fit(train_dataset, epochs=10,
validation_data=test_dataset,
validation_steps=30)
- 1
- 2
- 3
- 4
图 8-12 显示了每个时期的主要性能指标。
图 8-12
模拟每个时期的训练表现
评估模型
在看到大约 85%的准确性性能后,我们可以放心地继续评估我们的模型。我们使用test_dataset
来计算我们的最终损耗和精度值:
test_loss, test_acc = model.evaluate(test_dataset)
print('Test Loss: {}'.format(test_loss))
print('Test Accuracy: {}'.format(test_acc))
- 1
- 2
- 3
- 4
- 5
运行上面的代码后,我们得到如下图 8-13 所示的输出:
图 8-13
培训后的模型评估
我们还可以使用 history 对象,通过下面的代码绘制一段时间内的性能指标:
import matplotlib.pyplot as plt
def plot_graphs(history, metric):
plt.plot(history.history[metric])
plt.plot(history.history['val_'+metric], '')
plt.xlabel("Epochs")
plt.ylabel(metric)
plt.legend([metric, 'val_'+metric])
plt.show()
plot_graphs(history, 'accuracy')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
图 8-14 显示了输出的图。
图 8-14
情绪分析 LSTM 模型的准确度与时间图
做出新的预测
既然我们已经训练了我们的 RNN 模型,我们可以从我们的模型从未见过的评论中做出新的情绪预测。由于我们对训练和测试集进行了编码和填充,我们必须以同样的方式处理新的评论。因此,我们需要一个 padder 和一个编码器。以下代码是我们的自定义填充函数:
def review_padding(encoded_review, padding_size):
zeros = [0] * (padding_size - len(encoded_review))
encoded_review.extend(zeros)
return encoded_review
- 1
- 2
- 3
- 4
- 5
我们还需要一个编码器功能,将编码和处理我们的审查,以馈入我们训练有素的模型。以下函数完成这些任务:
def review_encoder(review):
encoded_review = review_padding(encoder.encode( review ), 64)
encoded_review = tf.cast( encoded_review, tf.float32)
return tf.expand_dims( encoded_review, 0)
- 1
- 2
- 3
- 4
- 5
现在我们可以很容易地从以前看不到的评论中做出预测。为了这个任务,我访问了电影《搏击俱乐部》的 IMDB 评论页面,并选择了以下评论:
fight_club_review = 'It has some cliched moments, even for its time, but FIGHT CLUB is an awesome film. I have watched it about 100 times in the past 20 years. It never gets old. It is hard to discuss this film without giving things away but suffice it to say, it is a great thriller with some intriguing twists.'
- 1
- 2
评论人给了 8 星,给搏击俱乐部写了这个评论。所以,显然是正面评价。由于我们前面定义的自定义函数,进行新的预测非常容易,如下面一行所示:
model.predict(review_encoder(fight_club_review))
output: array([[1.5780725]], dtype=float32)
- 1
- 2
- 3
- 4
当输出大于 0.5 时,我们的模型将评论分类为正面,而如果低于 0.5,则分类为负面。由于我们的输出是 1.57,我们确认我们的模型成功地预测了评论的情绪。
虽然我们的模型有超过 85%的准确率,但我认识到的一个偏差是关于评论的长度。当我们选择一个非常短的评论时,不管它有多积极,我们总是得到一个消极的结果。这个问题可以通过微调来解决。尽管我们不会在这个案例研究中进行微调,但是您可以随意地对其进行工作,以进一步改进模型。
保存和加载模型
你已经成功训练了一个 RNN 模型,你可以完成这一章。但是我想再介绍一个话题:保存和加载训练好的模型。正如你所经历的,训练这个模型需要大约 30 分钟,谷歌 Colab 会在一段时间不活动后删除你所做的一切。所以,你必须保存你训练好的模型以备后用。此外,你不能简单地将它保存到 Google Colab 目录中,因为过一会儿它也会被删除。解决办法是把它保存到你的 Google Drive。为了能够通过云随时使用我们的模型,我们应该
-
让 Colab 访问我们的 Google Drive 来保存文件
-
将训练好的模型保存到指定路径
-
随时从 Google Drive 加载训练好的模型
-
使用 savemodel 对象进行预测
让 Colab 访问 Google Drive
为了能够访问 Colab,我们需要在我们的 Colab 笔记本中运行以下代码:
from google.colab import drive
drive.mount('/content/gdrive')
- 1
- 2
- 3
按照输出单元格中的说明完成此任务。
将训练好的模型保存到 Google Drive
现在我们可以从 Colab 笔记本中访问我们的 Google Drive 文件,我们可以创建一个名为saved_models
的新文件夹,并使用以下代码行将我们的SavedModel
对象保存到该文件夹中:
# This will create a 'saved_model' folder under the 'content' folder.
!mkdir -p "/content/gdrive/My Drive/saved_model"
# This will save the full model with its variables, weights, and biases.
model.save('/content/gdrive/My Drive/saved_model/sentiment_analysis')
# Also save the encoder for later use
encoder.save_to_file('/content/gdrive/My Drive/saved_model/sa_vocab')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
在这段代码之后,只要我们将保存的文件保存在 Google Drive 中,我们就可以加载训练好的模型。您还可以使用以下代码查看sentiment_analysis
文件夹下的文件夹和文件:
import os
os.listdir("/content/gdrive/My Drive/saved_model/sentiment_analysis")
output: ['variables', 'assets', 'saved_model.pb']
- 1
- 2
- 3
- 4
加载训练好的模型并进行预测
为了能够加载saved_model
,我们可以使用saved_model
对象的 load 属性。我们只需要传递我们的模型所在的确切路径(确保 Colab 可以访问您的 Google Drive ),一旦我们运行代码,我们的模型就可以使用了:
import tensorflow as tf
loaded = tf.keras.models.load_model("/content/gdrive/My Drive/saved_model/sentiment_analysis/")
- 1
- 2
- 3
我们还加载了之前保存的词汇表,用下面的代码进行编码和解码:
import tensorflow_datasets as tfds
vocab_path = '/content/gdrive/My Drive/saved_model/sa_vocab'
encoder = tfds.features.text.SubwordTextEncoder.load_from_file(vocab_path)
- 1
- 2
- 3
- 4
此外,如果重新启动运行时,请确保再次运行定义了review_padding()
和review_encoder()
函数(之前共享)的单元格。
请注意,加载的模型对象与我们之前的模型完全相同,它具有标准的模型函数,如fit()
、evaluate()
和predict()
。为了能够进行预测,我们需要使用我们加载的模型对象的predict()
函数。我们还需要通过我们作为embedding_input
参数的过程化审查。以下代码行完成了这些任务:
fight_club_review = 'It has some cliched moments, even for its time, but FIGHT CLUB is an awesome film. I have watched it about 100 times in the past 20 years. It never gets old. It is hard to discuss this film without giving things away but suffice it to say, it is a great thriller with some intriguing twists.'
loaded.predict(review_encoder(rev))
output: array([[1.5780725]], dtype=float32)
- 1
- 2
- 3
- 4
- 5
正如所料,我们得到相同的输出。因此,我们成功地保存了我们的模型,加载了它,并进行了预测。现在,您可以将这个经过训练的模型嵌入到 web 应用程序、REST API 或移动应用程序中,为全世界服务!
结论
在本章中,我们介绍了循环神经网络,这是一种人工神经网络,专门用于处理序列数据。我们涵盖了 RNNs 的基础知识和不同类型的 RNNs(基本 RNN,LSTM,GRU 神经元)。然后,我们使用 IMDB 评论数据集进行了一个案例研究。我们的 RNN 学会了通过使用超过 50,000 条评论来预测评论是正面还是负面(即情绪分析)。
在下一章,我们将讨论自然语言处理,它是人工智能的一个分支,处理文本数据。此外,我们将在下一章构建另一个 RNN 模型,但这一次,它将生成文本数据。
Footnotes 1
使用 RNN tensor flow 进行文本分类,可在www.tensorflow.org/tutorials/text/text_classification_rnn
获得
2
安德鲁·马斯、雷蒙德·戴利、彼得·范、黄丹、安德鲁·Ng 和克里斯托弗·波茨。(2011).学习用于情感分析的词向量。计算语言学协会第 49 届年会(ACL 2011)。
九、自然语言处理
自然语言处理(NLP)是一个跨学科的子领域,包括语言学、计算机科学和人工智能等主要领域。NLP 主要关注人和计算机之间的交互。NLP 的范围从计算理解和生成人类语言到处理和分析大量自然语言数据。自然语言处理的范围还包括文本、语音、认知以及它们之间的相互作用。在这一章中,我们简要介绍了自然语言处理的历史,基于规则的自然语言处理和基于统计的自然语言处理之间的区别,以及主要的自然语言处理方法和技术。最后,我们对 NLP 进行了一个案例研究,让你为现实世界中的问题做好准备。
自然语言处理的历史
NLP 的历史可以分为四个主要时代:
-
早期思想时代
-
基于规则的自然语言处理时代
-
监督学习时代的统计自然语言处理
-
无监督和半监督学习时代
请注意,这些时代是互补的,而不是破坏性的。因此,我们仍然利用早期引入的规则和方法。
早期的想法
自然语言处理的历史始于 17 世纪,由莱布尼茨和笛卡尔提出哲学建议,引入特殊代码将不同语言之间的单词连接起来。尽管这些建议总是停留在理论层面,但它们影响了未来几个世纪的科学家们实现自动机器翻译的想法。
基于规则的自然语言处理
这个时代的共同特点是大量使用复杂的手写规则来覆盖 NLP 任务的潜在结果。与自然语言处理相关的早期创新最早出现在 20 世纪 30 年代的“翻译机”专利中。这些早期专利包含自动双语词典和处理语言间语法规则的方法。
在第二次世界大战期间,机器翻译设备被开发出来翻译敌人之间的通信。然而,这些机器大多不成功。
正如在人工智能的所有其他领域一样,1950 年,图灵测试为智能设定了标准,其中包括理解自然语言的对话。
1957 年,诺姆·乔姆斯基公布了句法结构,这是一个基于规则的系统,用一个普遍的语法规则彻底改变了语言学研究。
1954 年,在乔治敦大学的实验中,60 个俄语句子被自动翻译成英语。这个成功的实验鼓励作者宣称这两种语言之间的机器翻译将在 3 到 5 年内完成。这种积极的方法与其他人工智能子领域一致,在这些子领域中,大多数乐观的承诺都未能实现。因此,在 20 世纪 60 年代末和 70 年代初,在人工智能冬天的时代,NLP 研究的资金被削减。
尽管资金有限,一些成功的 NLP 系统还是在 20 世纪 60 年代开发出来,在有限的环境中工作。20 世纪 70 年代是许多程序员开始编写“概念本体”的年代,概念本体将现实世界的对象结构化、分组并分类成二进制数据。
统计自然语言处理和监督学习
大约在 20 世纪 80 年代,计算能力的稳步增长和优先使用机器学习方法进行语言处理的语料库语言学的普及使得在 NLP 中使用统计模型成为可能。尽管统计 NLP 的早期研究与基于规则的 NLP 研究没有太大的不同,但是随着更复杂方法的引入,统计 NLP 变得更具概率性。这种从基于规则的模型到统计模型的转变提高了精度性能,尤其是对于不寻常的观察。
在这个时代,IBM Research 走在了前面,开发了 IBM Watson 等几个成功的 NLP 解决方案。此外,欧盟、联合国和加拿大议会制作的多语言官方文档也为成功的机器翻译系统的开发做出了贡献。
另一方面,对这些大型文本语料库的访问有限的较小参与者专注于开发可以从有限数量的数据中有效学习的方法。
无监督和半监督自然语言处理
如今,NLP 问题在现实世界中的应用越来越成功。然而,找到足够的标记数据是现代自然语言处理研究中的主要问题之一。因此,使用无监督和半监督学习算法来完成常见的 NLP 任务变得越来越流行。一般来说,使用无监督学习算法做出的预测不如有监督算法准确。然而,通过无监督模型,研究人员可以从大量数据中推断出结果,这对发现更复杂的模式非常有用。
自然语言处理的实际应用
随着机器学习领域的进步,NLP 现实世界应用的数量正在增加。随着计算能力的提高,大量可用的机器学习模型,以及大量文本语料库的可用性,每天都有新的 NLP 用例被发现。以下是最受欢迎的 NLP 应用程序列表:
-
机器翻译:将文本从一种语言翻译成另一种语言的任务(例如,谷歌翻译)
-
语音识别:识别人声以采取行动或转换成文本的任务
-
情感分析:理解文本片段中的情感的任务,例如评论
-
问题回答:开发能够准确给出给定问题答案的系统的任务(例如 Siri)
-
自动摘要(Automatic Summarization】:在不丢失要点的情况下,从全文中提取简短摘要的任务
-
聊天机器人(Chatbots):开发特殊系统的任务,这些系统能够完成几项自然语言处理任务,比如问答、语音识别等等
-
市场情报:利用多种 NLP 和其他统计方法分析客户行为的任务
-
文本分类:通过分析文本的内容、结构和其他相关特征,将文本分类到给定类别的任务
-
光学字符识别(OCR) :借助计算机视觉和图像处理方法,分析图像数据并将其转换为文本的任务
-
拼写检查:识别和纠正文本中拼写错误的任务(例如,语法上)
为了能够开发这些真实世界的应用程序,研究人员必须使用几种 NLP 方法,这些方法将在下一节中介绍。
主要评估、技术、方法和任务
自然语言数据的处理由几个小任务组成,这些小任务可以分成以下几组:
-
形态句法
-
语义学
-
话语
-
演讲
-
对话
-
认识
在下一节中,我们将在相应的组下讨论这些任务。
形态句法
形态句法是对基于形态和句法属性创造的语法范畴和语言单位的研究。在自然语言处理领域,有许多基本的形态句法任务,列举如下:
-
基本形式提取:有两种流行的方法来提取单词的基本形式。
-
词条化:通过使用一个实际的词典(例如通过去掉 -ing 后缀将游泳转换为游泳)来去掉单词的无关紧要的词尾,并返回它们的基本词典形式(即词条)。
-
词干化:一种将屈折词或派生词还原为其词根形式的方法。虽然词干化类似于词汇化,但是使用词干化生成的词根形式不必是真实的单词(例如,单词troubl、troubl和troubl被词干化为 troubl )。
-
-
语法归纳:生成描述其语法的全语言形式语法。
-
词法切分:将单词拆分成最小的有意义的单位(即语素,并识别这些单位的类别。
-
词性标注:确定每个单词的词性类型。常见的词类有名词、动词、形容词、副词、代词、介词、连词、感叹词、数词、冠词或限定词。
-
解析:确定给定字符串(如句子)的解析树。解析树是一个有序的、有根的树,它代表了字符串的语法结构。
-
断句:寻找句子边界。一些标点符号,如句号或感叹号,对这项任务很有用。
-
分词:将给定文本分割成单独的单词。这个过程通常用于创建一个单词包(BOW)和文本矢量化。
-
术语提取:从给定的语料库中提取相关术语。
语义学
语义学是语言学和逻辑学的交叉学科,研究意义。逻辑语义学关注的是意义、指称和蕴涵,而词汇语义学关注的是对词义及其关系的分析。与语义相关的主要问题、方法和任务如下:
-
机器翻译:如前所述,将文本从一种人类语言自动翻译成另一种语言。
-
命名实体识别(NER) :在给定的字符串中寻找人名和地名。尽管资本化对 NER 很有用,但肯定还有更多的工作要做。
-
自然语言生成:使用单词表示和统计模型生成自然语言文本。
-
光学字符识别:如前所述,从包含打印文本的图像中识别文本数据。
-
问题回答:如前所述,用自然语言给出一个问题,提供答案。
-
识别文本蕴涵:识别文本片段之间的方向关系,比死板的逻辑蕴涵更宽松。
两个字符串之间的正文本蕴涵的示例如下:
正文:努力就会成功。
假设:努力工作会有好的结果。
-
关系提取:从给定文本中识别现实世界的关系(例如,人 A 为 X 公司工作)。
-
情感分析:如前所述,从一组文档中提取主观信息(即情感)。
-
主题分割和识别:将一组文档或文本分类成单独的主题。虽然在某些情况下主题界限可能很明显,但通常需要更多的评估。
-
词义消歧:根据上下文确定一个有一个以上含义的单词的含义。
话语
-
自动摘要:如前所述,产生一个大文本的可读摘要。
-
共指消解:确定哪些词指代相同的对象,既包括名词也包括代词。当一个文本中有多个表达式引用同一个对象时,就会出现共指。
-
话语分析:研究书面语或口语与其社会背景的关系,旨在了解语言在现实生活中的使用情况。
演讲
-
语音识别:如前所述,将一个人说话的给定声音片段转换成文本
-
语音分割:语音识别的一个子任务,将识别的文本分割成单词
-
文本到语音:将给定的文本转换成它的音频表示
对话
开始并继续与人或机器进行有意义的书面或口头交流。对话需要同时完成几项任务,如回答问题、文本到语音转换、语音识别、情感分析等。
认识
通过思想、经验和感觉获得知识和理解。它被认为是最复杂的自然语言处理评估,通常被称为自然语言理解(NLU)。
自然语言工具包(NLTK)
NLTK 是一个为 NLP 任务设计的重要 Python 库。NLTK 支持基本的 NLP 任务,例如文本分类、词干提取和词汇化、标记、解析、标记化,甚至推理。
在由宾夕法尼亚大学的 Steven Bird 和 Edward Loper 开发之后,NLTK 被认为是 Python 的主要 NLP 库。
尽管您可以利用数据科学库,如 Pandas、scikit-learn、TensorFlow 和 NumPy,但这些库中可用的方法甚至无法与 NLTK 提供的方法相比。
这里列出了可用的 NLTK 模块。
|
app
|
parse
|
| — | — |
| ccg
| probability
|
| chat
| sem
|
| chunk
| sentiment
|
| classify
| stem
|
| cluster
| tag
|
| collections
| tbl
|
| corpus
| test
|
| data
| text
|
| downloader
| tokenize
|
| draw
| toolbox
|
| featstruct
| translate
|
| grammar
| tree
|
| help
| treetransform
|
| inference
| twitter
|
| lm
| util
|
| metrics
| wsd
|
| misc
| |
关于 NLTK 的有用信息
-
网站 :
www.nltk.org/
-
模块的文档 URL:
www.nltk.org/py-modindex
-
安装命令 :
pip install --user -U nltk
-
导入的首选别名:
import nltk
案例研究|深度自然语言处理的文本生成
请注意,NLP 的主题本身就是一个专业领域。有人可以花一生的时间来研究 NLP。在这一章中,我们只做一个介绍,现在我们已经讨论了自然语言处理中的主要主题,我们可以继续我们的案例研究:使用深度自然语言处理的文本生成。
NLP 项目中最重要的主题之一是文本矢量化。在本案例研究中,我们将参考 Andrej Karpathy 的博文《循环神经网络的不合理有效性》 1 ,以及 TensorFlow 团队对这篇博文的承担 2 。
研究表明,用于 NLP 的最有效的人工神经网络类型之一是循环神经网络(RNNs)。rnn 广泛用于 NLP 任务,如机器翻译、文本生成和图像字幕。在 NLP 任务中,通常,开发者使用 NLP 工具和方法来将文本数据处理成向量,然后将它们馈送到选定的人工神经网络,例如 RNN、CNN,或者甚至前馈神经网络,以完成任务。在我们的案例研究中,我们也遵循这两个标准化的步骤:(I)将文本处理成向量,(ii)用这些向量训练神经网络。
案例研究的目标
充分理解案例研究的目标至关重要。在这个案例研究中,我们的目标是训练一个 RNN,它能够使用字符生成有意义的文本。RNN 可以从单词和字符中生成文本,我们选择使用字符来生成这个案例研究的文本。问题是,当我们未经训练就建立一个新的 RNN 时,它组合了一堆毫无意义的字符,这没有任何意义。然而,如果我们给我们的 RNN 输入大量文本数据,它就会开始模仿这些文本的风格,并使用字符生成有意义的文本。所以,如果我们给模型输入大量说教性的文本,我们的模型就会生成教育材料。如果我们给我们的模型输入大量的诗歌,我们的模型将开始生成诗歌,所以我们将最终拥有一个人工诗人。这些都是可行的选择,但是我们将为我们的模型提供一些其他的东西:一个包含莎士比亚作品的长文本数据集。因此,我们将创造一个人造的莎士比亚。
莎士比亚文集
莎士比亚语料库是一个包含 40,000 行莎士比亚作品的文本文件,由 Karpathy 清理和准备,由 TensorFlow 团队托管于此 URL:
https://storage.googleapis.com/download.tensorflow.org/data/shakespeare.txt
我强烈建议您看看。txt 文件来理解我们正在处理的文本。文件包含对话内容,每个角色的名字放在相应部分的前面,如图 9-1 所示。
图 9-1
莎士比亚文集的一部分
初始进口
在本案例研究中,所需的库是 TensorFlow、NumPy 和 os,我们可以使用以下代码导入它们:
import tensorflow as tf
import numpy as np
import os
- 1
- 2
- 3
- 4
你注意到我没有提到 NLTK 库吗?原因是 TensorFlow 对 NLP 任务的支持也有限,在本案例研究中,结合 NumPy 操作,我们能够使用 TensorFlow 对数据集进行矢量化。这主要是因为我们的语料库非常标准化和干净。如果我们需要一个更复杂的 NLP 方法,我们将不得不在更大程度上依赖 NLTK、Pandas 和 NumPy 库。
加载语料库
为了能够从在线目录加载数据集,我们可以使用 TensorFlow 中 Keras API 的util
模块。对于这个任务,我们将使用get_file()
函数,如果文件不在缓存中,它将从 URL 下载文件,代码如下:
path_to_file = tf.keras.utils.get_file('shakespeare.txt', 'https://storage.googleapis.com/download.tensorflow.org/data/shakespeare.txt')
- 1
- 2
下载文件后,我们可以使用以下 Python 代码从缓存中打开文件:
text = open(path_to_file, 'rb').read()
text = text.decode(encoding='utf-8')
- 1
- 2
- 3
现在,我们成功地将整个语料库作为变量保存在了 Colab 笔记本的内存中。让我们看看语料库中有多少个字符,前 100 个字符是什么,代码如下:
print ('Total number of characters in the corpus is:', len(text))
print('The first 100 characters of the corpus are as follows:
', text[:100])
Output:
Total number of characters in the corpus is: 1115394
The first 100 characters of the corpus are as follows:
First Citizen:
Before we proceed any further, hear me speak.
All:
Speak, speak.
First Citizen:
You
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
我们的整个语料库可以通过一个名为 text 的 Python 变量来访问,现在我们可以开始对它进行矢量化了。
向量化文本
文本矢量化是一种基本的 NLP 方法,用于将文本数据转换为机器可以理解的有意义的数字向量。文本矢量化有多种方法。在本案例研究中,我们是这样一步步进行的:
-
给每个独特的字符一个索引号。
-
在语料库中运行一个 for 循环,并索引整个文本中的每个字符。
为了给每个独特的字符分配一个索引号,我们首先必须创建一个只包含文本中所有独特字符的单个副本的列表。使用内置的set()
函数很容易做到这一点,该函数将一个列表对象转换成一个只有唯一值的集合对象。
集合和列表数据结构的区别在于,列表是有序的,允许重复,而集合是无序的,不允许重复元素。因此,当我们运行set()
函数时,如下面的代码所示,它在文本文件中返回一组独特的字符:
vocab = sorted(set(text))
print ('The number of unique characters in the corpus is', len(vocab))
print('A slice of the unique characters set:
', vocab[:10])
Output:
The number of unique characters in the corpus is 65
A slice of the unique characters set:
['
', ' ', '!', '$', '&', "'", ',', '-', '.', '3']
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
我们还需要给每个字符一个索引号。以下代码为每个集合项目分配一个编号,然后使用以下代码创建一个集合项目字典,其中包含这些集合项目的给定编号:
char2idx = {u:i for i, u in enumerate(vocab)}
- 1
- 2
我们还复制了 NumPy 数组格式的唯一集合元素,供以后解码预测时使用:
idx2char = np.array(vocab)
- 1
- 2
现在,我们可以使用一个简单的 for 循环对文本进行矢量化处理,遍历文本中的每个字符,为它们分配相应的索引值,并将所有索引值保存为一个新列表,代码如下:
text_as_int = np.array([char2idx[c] for c in text])
- 1
- 2
创建数据集
此时,我们用char2idx
字典对文本进行矢量化,用idx2char
对矢量化后的文本进行去矢量化(即解码)。最后,我们将text_as_int
作为矢量化的 NumPy 数组。我们现在可以创建数据集了。
首先,我们将使用来自Dataset
模块的from_tensor_slices
方法从我们的text_as_int
对象创建一个 TensorFlow Dataset 对象,我们将把它们分成几批。数据集的每个输入的长度限制为 100 个字符。我们可以用下面的代码实现所有这些:
char_dataset = tf.data.Dataset.from_tensor_slices(text_as_int)
seq_length = 100 # The max. length for single input
sequences = char_dataset.batch(seq_length+1, drop_remainder=True)
- 1
- 2
- 3
- 4
我们的sequences
对象包含字符序列,但是我们必须创建一个这些序列的元组,以便输入到 RNN 模型中。我们可以通过如下自定义映射函数来实现这一点:
def split_input_target(chunk):
input_text = chunk[:-1]
target_text = chunk[1:]
return input_text, target_text
dataset = sequences.map(split_input_target)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
我们生成这些元组的原因是为了让 RNN 工作,我们需要创建一个流水线,如图 9-2 所示。
图 9-2
具有四维输入和输出层的 RNN 的例子。注意输入和输出字符之间的延迟
最后,我们重组数据集,分成 64 个句子批次,每行如下:
BUFFER_SIZE = 10000 # TF shuffles the data only within buffers
BATCH_SIZE = 64 # Batch size
dataset = dataset.shuffle(BUFFER_SIZE).batch(BATCH_SIZE, drop_remainder=True)
print(dataset)
Output:
<BatchDataset shapes: ((64, 100), (64, 100)), types: (tf.int64, tf.int64)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
构建模型
我们的数据已经准备好输入模型流水线。让我们创建我们的模型。我们希望训练我们的模型,然后做出新的预测。重要的是,我们的训练流水线将在每一批输入 64 个句子。因此,我们需要以一次接受 64 个输入句子的方式构建我们的模型。然而,在我们训练了我们的模型之后,我们想要输入单句来生成新的任务。所以,我们需要不同的训练前和训练后模型的批量大小。为了实现这一点,我们需要创建一个函数,它允许我们为不同的批量大小重现模型。下面的代码可以做到这一点:
def build_model(vocab_size, embedding_dim, rnn_units, batch_size):
model = tf.keras.Sequential([
tf.keras.layers.Embedding(
vocab_size,
embedding_dim,
batch_input_shape=[batch_size, None]),
tf.keras.layers.GRU(
rnn_units,
return_sequences=True,
stateful=True,
recurrent_initializer='glorot_uniform'),
tf.keras.layers.Dense(vocab_size)
])
return model
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
我们的模型有三层:
-
嵌入层:该层作为输入层,接受输入值(数字格式)并将其转换为矢量。
-
GRU 图层:填充了 1024 个梯度下降单元的 RNN 图层
-
密集层:输出结果,
vocab_size
输出。
现在,我们可以使用以下代码创建我们的培训模型:
model = build_model(
vocab_size = len(vocab), # no. of unique characters
embedding_dim=embedding_dim, # 256
rnn_units=rnn_units, # 1024
batch_size=BATCH_SIZE) # 64 for the training
- 1
- 2
- 3
- 4
- 5
- 6
图 9-3 总结了我们的模型。
图 9-3
培训模型的摘要视图。请注意输出形状中的 64,对于训练后的单个预测,它必须为 1
编译和训练模型
为了编译我们的模型,我们需要配置我们的优化器和损失函数。对于这个任务,我们选择“Adam”作为优化器,选择稀疏分类交叉熵函数作为损失函数。
由于我们的输出总是 65 个字符中的一个,这是一个多类分类问题。因此,我们必须选择一个分类交叉熵函数。然而,在这个例子中,我们选择了分类交叉熵的一个变体:稀疏分类交叉熵。我们使用稀疏分类交叉熵的原因是,即使它们使用相同的损失函数,它们的输出格式也是不同的。请记住,我们将文本矢量化为整数(例如,[0],[2],[1]),而不是一键编码格式(例如,[0,0,0],[0,1],[1,0,0])。为了能够输出整数,我们必须使用稀疏分类交叉熵函数。
为了能够设置我们自定义的损失函数,让我们创建一个包含稀疏分类交叉熵损失的基本 Python 函数:
def loss(labels, logits):
return tf.keras.losses.sparse_categorical_crossentropy( labels, logits, from_logits=True)
- 1
- 2
- 3
现在,我们可以使用以下代码设置损失函数和优化器:
model.compile(optimizer='adam', loss=loss)
- 1
- 2
为了能够加载我们的重量并保存我们的训练成绩,我们需要使用以下代码设置和配置一个检查点目录:
# Directory where the checkpoints will be saved
checkpoint_dir = './training_checkpoints'
# Name of the checkpoint files
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt_{epoch}")
checkpoint_callback=tf.keras.callbacks.ModelCheckpoint(
filepath=checkpoint_prefix,
save_weights_only=True)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
我们的模型和检查点目录已经配置好了。我们将为我们的模型训练 30 个时期,并将训练历史保存到名为 history 的变量中,代码如下:
EPOCHS = 30
history = model.fit(dataset, epochs=EPOCHS, callbacks=[checkpoint_callback])
- 1
- 2
- 3
在训练模型时,我们得到了如图 9-4 所示的以下输出:
图 9-4
模型训练的最后八个时期
由于模型的简单性以及我们对模型进行编码的方式,我们的训练并不需要太长时间(大约 3 –
4 分钟)。现在,我们可以使用保存的权重并构建一个自定义模型,该模型接受单个输入来生成文本。
用训练好的模型生成文本
为了能够查看我们最新检查点的位置,我们需要运行以下代码:
tf.train.latest_checkpoint(checkpoint_dir)
Output:
./training_checkpoints/ckpt_30
- 1
- 2
- 3
- 4
现在,我们可以使用之前创建的定制build_model()
函数,使用latest_checkpoint
中保存的权重,用batch_size=1
、load weights
构建一个新模型,并使用build()
函数基于接收到的输入形状(即[1, None]
)构建模型。我们可以用下面的代码实现所有这些和summarize()
关于我们新模型的信息:
model = build_model(vocab_size, embedding_dim, rnn_units, batch_size=1)
model.load_weights(tf.train.latest_checkpoint(checkpoint_dir))
model.build(tf.TensorShape([1, None]))
model.summary()
- 1
- 2
- 3
- 4
- 5
输出如图 9-5 所示:
Output:
图 9-5
新创建的模型的概要视图。现在它接受单一输入
我们的模型已经准备好进行预测,我们所需要的只是一个定制函数来为模型准备输入。我们必须设置以下内容:
-
要生成的字符数
-
向量化输入(从字符串到数字)
-
存储结果的空变量
-
手动调整预测可变性的温度值
-
对输出进行去因子化,并且还将输出再次馈送到模型,用于下一次预测
-
将所有生成的字符连接成一个最终字符串
以下自定义函数完成所有这些工作:
def generate_text(model, num_generate, temperature, start_string):
input_eval = [char2idx[s] for s in start_string] # string to numbers (vectorizing)
input_eval = tf.expand_dims(input_eval, 0) # dimension expansion
text_generated = [] # Empty string to store our results
model.reset_states() # Clears the hidden states in the RNN
for i in range(num_generate): #Run a loop for number of characters to generate
predictions = model(input_eval) # prediction for single character
predictions = tf.squeeze(predictions, 0) # remove the batch dimension
# using a categorical distribution to predict the character returned by the model
# higher temperature increases the probability of selecting a less likely character
# lower --> more predictable
predictions = predictions / temperature
predicted_id = tf.random.categorical(predictions, num_samples=1)[-1,0].numpy()
# The predicted character as the next input to the model
# along with the previous hidden state
# So the model makes the next prediction based on the previous character
input_eval = tf.expand_dims([predicted_id], 0)
# Also devectorize the number and add to the generated text
text_generated.append(idx2char[predicted_id])
return (start_string + ''.join(text_generated))
- 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
它返回我们的最终预测值,我们可以使用下面一行轻松地生成一个文本:
generated_text = generate_text(
model,
num_generate=500,
temperature=1,
start_string=u"ROMEO")
- 1
- 2
- 3
- 4
- 5
- 6
我们可以用内置的打印功能打印出来:
print(generated_text)
Output:
ROMEO:
Third Servingman:
This attemptue never long to smile
under garlands grass and enterhoand of death.
GREMIO:
Have I not fought for such a joy? can come to Spilet O, thy husband!
Go, sirs, confusion's cut off? princely Noboth, my any thing thee;
Whereto we will kiss thy lips.
ANTIGONUS:
It is your office: you have ta'en her relatants so many friends as they
or no man upon the market-play with thee!
GRUMIO:
First, know, my lord.
KING RICHARD II:
Then why.
CORIOLANUS:
How like a tinker? Was e
- 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
如您所见,我们的模型能够生成任意长度的文本。请注意:我们的模型使用字符,因此模型的奇迹在于它学会了从字符中创建有意义的单词。所以,不要认为它把一堆不相关的单词加在一起。它检查了数千个单词,学习了不同字符之间的关系,以及如何使用它们来创建有意义的单词。然后它复制这个,并返回给我们有意义的单词的句子。
请玩玩温度,看看如何将输出从更合适的单词变成更失真的单词。较高的温度值会增加我们的功能选择不太可能的字符的机会。当我们把它们都加起来时,我们得到的结果就没那么有意义了。另一方面,低温会导致函数生成更简单的文本,更像是原始语料库的副本。
结论
在这一章中,我们讨论了自然语言处理,它是人工智能的一个分支,处理文本数据。我们介绍了 NLP 研究中使用的主要方法和技术。我们也简单参观了 NLP 的时间线。我们最后进行了一个案例研究,使用循环神经网络生成类似莎士比亚的文本。
在下一章,我们将讨论推荐系统,它是我们今天所知的大型科技公司提供的许多服务的支柱。
Footnotes 1
循环神经网络的不合理有效性,可用 http://karpathy.github.io/2015/05/21/rnn-effectiveness
2
使用 RNN | TensorFlow 核心 TensorFlow 生成文本,可在 www.tensorflow.org/tutorials/text/text_generation
上使用