Python 数据科学(六)
原文:
annas-archive.org/md5/b306e51c73948c57f772d5af5f61eb39译者:飞龙
第十五章:第五章
掌握结构化数据
学习目标
在本章结束时,你将能够:
-
使用结构化数据创建高精度模型
-
使用 XGBoost 库训练提升模型
-
使用 Keras 库训练神经网络模型
-
微调模型参数以获得最佳准确性
-
使用交叉验证
-
保存并加载你的训练模型
本章将介绍如何创建高精度的结构化数据模型的基础知识。
简介
数据主要分为两种类型:结构化数据和非结构化数据。结构化数据指的是具有定义格式的数据,通常以表格形式存储,例如存储在 Excel 表格或关系型数据库中的数据。非结构化数据没有预定义的模式,任何无法存储在表格中的数据都属于这一类。例如语音文件、图片和 PDF 文件等。
在本章中,我们将重点讨论结构化数据,并使用 XGBoost 和 Keras 创建机器学习模型。XGBoost 算法因其能够快速交付高精度模型以及其分布式特性而被业界专家和研究人员广泛使用。分布式特性指的是能够并行处理数据和训练模型;这使得训练速度更快,数据科学家的周转时间更短。另一方面,Keras 让我们能够创建神经网络模型。在某些情况下,神经网络的效果远胜于提升算法,但找到合适的网络和网络配置是非常困难的。以下主题将帮助你熟悉这两个库,确保你能应对数据科学旅程中的任何结构化数据。
提升算法
提升方法是一种提高任何学习算法准确性的方式。提升方法通过将粗略的、高层次的规则结合成一个比任何单一规则更准确的预测。它通过迭代地将训练数据集的一个子集输入到一个“弱”算法中来生成一个弱模型。这些弱模型随后被结合在一起,形成最终的预测。两种最有效的提升算法是梯度提升机和 XGBoost。
梯度提升机(GBM)
GBM 利用分类树作为弱算法。结果通过使用可微分的损失函数来改进这些弱模型的估算。该模型通过考虑前一棵树的净损失来拟合连续的树;因此,每棵树都部分地参与了最终的解决方案。因此,提升树会降低算法的速度,但它们提供的透明度能带来更好的结果。GBM 算法有很多参数,而且对噪声和极端值非常敏感。同时,GBM 会发生过拟合,因此需要一个合适的停止点,但它通常是最佳的模型。
XGBoost(极端梯度提升)
XGBoost 是全球研究人员在构建结构化数据模型时的首选算法。XGBoost 同样使用树作为弱学习算法。那么,为什么当数据科学家看到结构化数据时,XGBoost 是他们首先想到的算法呢?XGBoost 是可移植和分布式的,这意味着它可以在不同的架构中轻松使用,并且可以利用多个核心(单台机器)或多台机器(集群)。作为额外的好处,XGBoost 库是用 C++编写的,这使它非常快速。当处理大数据集时,它特别有用,因为它允许将数据存储在外部磁盘上,而不必将所有数据加载到内存中。
注意
你可以在这里阅读更多关于 XGBoost 的内容:arxiv.org/abs/1603.02754
练习 44:使用 XGBoost 库进行分类
在本练习中,我们将使用 Python 的 XGBoost 库对批发客户数据集进行分类(github.com/TrainingByPackt/Data-Science-with-Python/tree/master/Chapter05/data)。该数据集包含批发分销商客户的购买数据,包括在各种产品类别上的年度花费。我们将根据不同产品的年度支出预测渠道。这里的“渠道”描述了客户是酒店/餐厅/咖啡馆(horeca)客户还是零售客户。
-
打开你虚拟环境中的 Jupyter Notebook。
-
导入 XGBoost、Pandas 和 sklearn,这些是我们将用于计算准确度的函数。准确度对于理解我们的模型性能至关重要。
import pandas as pd import xgboost as xgb from sklearn.metrics import accuracy_score -
使用 pandas 读取批发客户数据集,并通过以下命令检查数据是否成功加载:
data = pd.read_csv("data/wholesale-data.csv") -
使用
head()命令检查数据集的前五个条目。输出如下面的截图所示:data.head()图 5.1:显示数据集前五个元素的截图
-
现在,“
data”数据框包含所有数据。它有目标变量,在我们的例子中是“Channel”,以及预测变量。因此,我们将数据分为特征(预测变量)和标签(目标变量)。X = data.copy()X.drop("Channel", inplace = True, axis = 1)Y = data.Channel -
按照前面章节的讨论,创建训练集和测试集。在这里,我们使用 80:20 的比例进行分割,因为数据集中的数据点较少。你可以尝试不同的分割比例。
X_train, X_test = X[:int(X.shape[0]*0.8)].values, X[int(X.shape[0]*0.8):].values Y_train, Y_test = Y[:int(Y.shape[0]*0.8)].values, Y[int(Y.shape[0]*0.8):].values -
将 pandas 数据框转换为 DMatrix,这是 XGBoost 用于存储训练和测试数据集的内部数据结构。
train = xgb.DMatrix(X_train, label=Y_train)test = xgb.DMatrix(X_test, label=Y_test) -
指定训练参数并训练模型。
注意
param = {'max_depth':6, 'eta':0.1, 'silent':1, 'objective':'multi:softmax', 'num_class': 3}num_round = 5 model = xgb.train(param, train, num_round)注意
默认情况下,XGBoost 会使用所有可用线程进行多线程处理。要限制线程数,你可以使用
nthread参数。有关更多信息,请参阅下一节。 -
使用我们刚刚创建的模型预测测试集的“
Channel”值。preds = model.predict(test) -
获取我们为测试数据集训练的模型的准确度。
acc = accuracy_score(Y_test, preds)print("Accuracy: %.2f%%" % (acc * 100.0))输出截图如下:
图 5.2:最终准确度
恭喜!你刚刚使用大约 90%的准确率创建了你的第一个 XGBoost 模型,而且几乎没有进行调优!
XGBoost 库
我们用于执行上述分类的库名为 XGBoost。该库通过许多参数提供了大量的自定义选项。在接下来的章节中,我们将深入了解 XGBoost 库的不同参数和功能。
注意
欲了解更多关于 XGBoost 的信息,请访问网站:xgboost.readthedocs.io
训练
影响任何 XGBoost 模型训练的参数如下所示。
-
booster: 尽管我们在介绍中提到 XGBoost 的基础学习器是回归树,但使用此库时,我们也可以将线性回归作为弱学习器使用。另一种弱学习器,DART 增强器,是一种新的树增强方法,它通过随机丢弃树来防止过拟合。使用树增强时,传递"gbtree"(默认);使用线性回归时,传递"gblinear";若要进行带有丢弃的树增强,传递"dart"。注意
你可以通过这篇论文了解更多关于 DART 的信息:www.jmlr.org/proceedings…
-
silent: 0 打印训练日志,而 1 则为静默模式。 -
nthread: 这表示要使用的并行线程数。默认情况下,它是系统中可用的最大线程数。注意
参数
silent已被弃用,现已被verbosity替代,verbosity可以取以下值:0(静默)、1(警告)、2(信息)、3(调试)。 -
seed: 这是随机数生成器的种子值。在此处设置一个常量值,可以得到可重复的结果。默认值是 0。 -
objective: 这是模型尝试最小化的函数。接下来的几个点将介绍目标函数。reg:linear: 线性回归应该用于连续型目标变量(回归问题)。 (默认)binary:logistic: 用于二分类的逻辑回归。它输出概率而非类别。binary:hinge: 这是二分类,它输出的是 0 或 1 的预测,而不是概率。当你不关心概率时,使用此方法。multi:softmax: 如果你想进行多类别分类,使用此方法来执行基于 softmax 目标的分类。必须将num_class参数设置为类别数。multi:softprob: 这与 softmax 的作用相同,但输出的是每个数据点的概率,而不是预测类别。 -
eval_metric: 需要在验证集上观察模型的表现(如在第一章,数据科学与数据预处理介绍中讨论)。此参数用于指定验证数据的评估指标。默认的评估指标根据目标函数选择(回归使用rmse,分类使用logloss)。你可以使用多个评估指标。rmse:均方根误差(RMSE)对较大误差的惩罚更重。因此,当误差为 1 比误差为 3 更为严重时,它是合适的。mae:平均绝对误差(MAE)适用于当误差为 1 和误差为 3 的情况类似时。以下图表显示了随着实际值与预测值之间差异的增加,误差也随之增加。图示如下:
图 5.3:实际值与预测值的差异
图 5.4:随着误差变化,惩罚的变化;|X| 为 mae,X² 为 rmse
logloss:模型的负对数似然(logloss)等同于最大化模型的准确度。它在数学上被定义为:
图 5.5:Logloss 方程示意图
这里,N 是数据点的数量,M 是类别的数量,1 或 0 取决于预测是否正确,表示的是预测数据点 i 为标签 j 的概率。
AUC:曲线下面积在二分类中广泛使用。如果你的数据集存在类别不平衡问题,你应该始终使用它。类别不平衡问题发生在数据未能均匀分配到不同类别中;例如,如果类别 A 占数据的 90%,而类别 B 占 10%。我们将在“处理不平衡数据集”一节中进一步讨论类别不平衡问题。
aucpr:精准率-召回率(PR)曲线下的面积与 AUC 曲线相同,但在处理高度不平衡的数据集时应优先使用。我们将在“处理不平衡数据集”一节中讨论这一点。
注意
在处理二分类数据集时,应作为经验法则使用 AUC 或 AUCPR。
树提升器
特定于基于树的模型的参数如下所示:
-
eta:这是学习率。修改此值以防止过拟合,正如我们在第一章,数据科学与数据预处理简介中讨论的那样。学习率决定了每一步更新权重的幅度。权重的梯度会与学习率相乘,并加到权重上。默认值为 0.3,最大值为 1,最小值为 0。 -
gamma:这是进行划分所需的最小损失减少量。gamma 值越大,算法越保守。更加保守有助于防止过拟合。该值依赖于数据集和使用的其他参数。其范围从 0 到无限大,默认值为 0。较小的值会导致树较浅,较大的值则会导致树更深。注意
gamma 值大于 1 通常不会得到好的结果。
-
max_depth:这是任何树的最大深度,如第三章《通过 Sklearn 介绍机器学习》中所讨论的。增加最大深度将使模型更容易发生过拟合。0 表示没有限制。默认值为 6。 -
subsample:将其设置为 0.5 将导致算法在生成树之前随机抽取一半的训练数据。这可以防止过拟合。每次提升迭代时会进行一次子采样,默认值为 1,这意味着模型会使用完整的数据集,而不是样本。 -
lambda:这是 L2 正则化项。L2 正则化将系数的平方值作为惩罚项添加到损失函数中。增大该值可以防止过拟合。其默认值为 1。 -
alpha:这是 L1 正则化项。L1 正则化将系数的绝对值作为惩罚项添加到损失函数中。增大该值可以防止过拟合。其默认值为 0。 -
scale_pos_weight:当类别极度不平衡时非常有用。我们将在接下来的章节中学习更多关于不平衡数据的内容。一个典型的考虑值是:负实例的总和 / 正实例的总和。其默认值为 1。 -
predictor:有两个预测器。cpu_predictor使用 CPU 进行预测,默认使用。gpu_predictor使用 GPU 进行预测。注意
获取所有参数的列表,请访问:xgboost.readthedocs.io/en/latest/p…
控制模型过拟合
如果你观察到训练数据集上的准确度很高,但在测试数据集上的准确度较低,那么你的模型已经对训练数据过拟合,如第一章《数据科学和数据预处理简介》中所示。XGBoost 中有两种主要方法可以限制过拟合:
-
在监控训练和测试指标的同时调整
max_depth、min_child_weight和gamma,以便获得最佳模型而不对训练数据集过拟合。你将在接下来的章节中学到更多相关内容。 -
colsample_bytree的作用与子采样相同,但它是对列进行采样,而不是对行进行采样。
为了更好地理解,请参见下图中的训练和准确度图表:
图 5.6:训练和准确度图表
要理解具有过拟合和适当拟合模型的数据集的概念化,请参阅下图:
图 5.7:具有过拟合和适当拟合模型的数据集示意图
注意
黑线表示模型适配良好,而红线表示的模型已经对数据集过拟合。
处理不平衡数据集
不平衡数据集给数据科学家带来了许多问题。一个不平衡数据集的例子是信用卡欺诈数据。在这种情况下,大约 95%的交易是合法的,只有 5%是欺诈性的。在这种情况下,一个将所有交易预测为正确的模型会得到 95%的准确率,但实际上这是一个非常糟糕的模型。为了查看类别的分布情况,你可以使用以下函数:
data['target_variable'].value_counts()
输出结果如下:
图 5.8:类别分布
为了处理不平衡数据集,你可以使用以下方法:
-
对记录数较多的类别进行欠采样:以信用卡欺诈为例,你可以通过随机抽取合法交易样本,使其记录数与欺诈记录相等。这样就能实现欺诈类别和合法类别的均衡分布。
-
对记录数较少的类别进行过采样:以信用卡欺诈为例,你可以通过添加新数据点或复制现有数据点来增加欺诈交易样本的数量。这样就能实现欺诈类别和合法类别的均衡分布,欺诈和合法。
-
使用 scale_pos_weight 来平衡正负权重:你可以通过该参数为数据点较少的类别分配更高的权重,从而人为地平衡各类别。该参数的值可以是:
图 5.9:值参数方程
你可以使用以下代码查看各类别的分布情况:
positive = sum(Y == 1)
negative = sum(Y == 0)
scale_pos_weight = negative/positive
- 使用 AUC 或 AUCPR 进行评估:如前所述,AUC 和 AUCPR 指标对于不平衡数据集非常敏感,不像准确率那样,它会给出一个高值,尽管模型通常预测的是多数类别。AUC 仅适用于二分类问题。它表示的是在不同阈值(0、0.01、0.02...1)下的真正例率与假正例率的关系。如下图所示:
图 5.10:TPR 和 FPR 方程
该指标是绘制TPR 和 FPR 后所得到的曲线下面积。在处理高度偏斜的数据集时,AUCPR 能提供更好的结果,因此它是首选。AUCPR 表示在不同阈值下的精确度和召回率。
图 5.11:精确度和召回率方程
作为经验法则,当处理不平衡类别时,应使用 AUC 或 AUCPR 作为评估指标,因为它能提供更清晰的模型表现。
注意
机器学习算法无法轻松处理作为字符串表示的字符串或类别变量,因此我们必须将它们转换为数字。
活动 14:训练并预测一个人的收入
在本次活动中,我们将尝试预测一个人的收入是否超过$50,000。成人收入数据集(链接)的数据来源于 1994 年人口普查数据集(链接),包含个人收入、教育资格和职业等信息。让我们来看一个场景:你在一家汽车公司工作,你需要创建一个系统,让公司的销售代表能够判断应该向哪个人推荐哪种汽车。
为此,你需要创建一个机器学习模型来预测潜在买家的收入,从而为销售人员提供正确的信息,以便销售正确的汽车。
-
使用 pandas 加载收入数据集(
adult-data.csv)。 -
数据应该如下所示:
图 5.12:显示人口普查数据集五个元素的截图
使用以下代码来指定列名:
data = pd.read_csv("../data/adult-data.csv", names=['age', 'workclass','education-num', 'occupation', 'capital-gain', 'capital-loss', 'hoursper-week', 'income']) -
使用 sklearn 将所有分类变量从字符串转换为整数。
-
使用 XGBoost 库进行预测,并进行参数调优,使准确率超过 80%。
我们成功地使用数据集预测了收入,准确率约为 83%。
注意
该活动的解决方案可以在第 360 页找到。
外部内存使用
当你有一个异常大的数据集,无法将其加载到内存中时,XGBoost 库的外部内存功能将帮助你。这个功能将训练 XGBoost 模型,而无需将整个数据集加载到内存中。
使用这个功能几乎不需要额外努力;你只需要在文件名的末尾添加缓存前缀。
train = xgb.DMatrix('data/wholesale-data.dat.train#train.cache')
该功能仅支持libsvm文件。因此,我们现在将把一个已加载到 pandas 的数据集转换为libsvm文件,以便使用外部内存功能。
注意
你可能需要根据数据集的大小分批进行处理。
from sklearn.datasets import dump_svmlight_file
dump_svmlight_file(X_train, Y_train, 'data/wholesale-data.dat.train', zero_based=True, multilabel=False)
这里,X_train和Y_train分别是预测变量和目标变量。libsvm文件将保存在数据文件夹中。
交叉验证
交叉验证是一种帮助数据科学家评估模型在未见数据上的表现的技术。当数据集不够大,无法创建三个划分(训练集、测试集和验证集)时,交叉验证特别有用。交叉验证通过给模型呈现同一数据集的不同划分,帮助模型避免过拟合。它通过在每次交叉验证的过程中,向模型提供不同的训练集和验证集来实现。10 折交叉验证是最常用的方式,数据集被分成 10 个完全不同的子集,并对每个子集进行训练,最后平均各项指标,以获得模型的准确预测性能。在每一轮交叉验证中,我们进行以下操作:
-
将数据集打乱并将其分成 k 个不同的组(k=10 用于 10 折交叉验证)。
-
在 k-1 个组上训练模型,并在 1 个组上进行测试。
-
评估模型并存储结果。
-
重复步骤 2 和 3,使用不同的组,直到所有 k 种组合都训练完毕。
-
最终指标是不同轮次生成的指标的平均值。
图 5.13:交叉验证数据集示意图
XGBoost 库内置了一个进行交叉验证的函数。本节将帮助你熟悉如何使用它。
练习 45:使用交叉验证找到最佳超参数
在本练习中,我们将使用 XGBoost 库在 Python 中为上一个活动中的成人数据集找到最佳超参数。为此,我们将利用该库的交叉验证功能。
-
从活动 14加载人口普查数据集,并执行所有预处理步骤。
import pandas as pd import numpy as np data = pd.read_csv("../data/adult-data.csv", names=['age', 'workclass', 'fnlwgt', 'education-num', 'occupation', 'capital-gain', 'capital-loss', 'hours-per-week', 'income'])使用 sklearn 中的 Label Encoder 对字符串进行编码。首先,导入 Label Encoder,然后逐个编码所有字符串类型的列。
from sklearn.preprocessing import LabelEncoder data['workclass'] = LabelEncoder().fit_transform(data['workclass']) data['occupation'] = LabelEncoder().fit_transform(data['occupation']) data['income'] = LabelEncoder().fit_transform(data['income']) -
从数据中创建训练集和测试集,并将数据转换为 D 矩阵。
import xgboost as xgb X = data.copy() X.drop("income", inplace = True, axis = 1) Y = data.income X_train, X_test = X[:int(X.shape[0]*0.8)].values, X[int(X.shape[0]*0.8):].values Y_train, Y_test = Y[:int(Y.shape[0]*0.8)].values, Y[int(Y.shape[0]*0.8):].values train = xgb.DMatrix(X_train, label=Y_train) test = xgb.DMatrix(X_test, label=Y_test) -
不使用 train 函数,而是使用以下代码进行 10 折交叉验证,并将结果存储在
model_metrics数据框中。for 循环遍历不同的树深度值,以找到最适合我们数据集的深度。test_error = {} for i in range(20): param = {'max_depth':i, 'eta':0.1, 'silent':1, 'objective':'binary:hinge'} num_round = 50 model_metrics = xgb.cv(param, train, num_round, nfold = 10) test_error[i] = model_metrics.iloc[-1]['test-error-mean'] -
使用 Matplotlib 可视化结果。
import matplotlib.pyplot as plt plt.scatter(test_error.keys(),test_error.values()) plt.xlabel('Max Depth') plt.ylabel('Test Error') plt.show()图 5.14:最大深度与测试误差的图表
从图中我们可以看到,9 的最大深度对于我们的数据集效果最好,因为它的测试误差最低。
-
找到最佳学习率。运行这段代码会花费一些时间,因为它会遍历多个学习率,每个学习率运行 500 轮。
for i in range(1,100,5): param = {'max_depth':9, 'eta':0.001*i, 'silent':1, 'objective':'binary:hinge'} num_round = 500 model_metrics = xgb.cv(param, train, num_round, nfold = 10) test_error[i] = model_metrics.iloc[-1]['test-error-mean'] -
可视化结果。
lr = [0.001*(i) for i in test_error.keys()] plt.scatter(temp,test_error.values()) plt.xlabel('Learning Rate') plt.ylabel('Error') plt.show()图 5.15:学习率与测试误差的图表
从图中可以看到,约 0.01 的学习率对我们的模型效果最好,因为它的误差最小。
-
让我们可视化学习率为 0.01 时每一轮的训练和测试误差。
param = {'max_depth':9, 'eta':0.01, 'silent':1, 'objective':'binary:hinge'} num_round = 500 model_metrics = xgb.cv(param, train, num_round, nfold = 10) plt.scatter(range(500),model_metrics['test-error-mean'], s = 0.7, label = 'Test Error') plt.scatter(range(500),model_metrics['train-error-mean'], s = 0.7, label = 'Train Error') plt.legend() plt.show()](tos-cn-i-73owjymdk6/a292ebbe76d343c8987dbd35ad87db79)
图 5.16:训练和测试误差随轮次变化的图表
注意
list(model_metrics['test-error-mean']).index(min(model_metrics['test-error-mean'])) -
要理解这一点,可以查看输出结果。
图 5.17:最小误差
注意
对于该数据集,效果最好的最终模型参数为:
最大深度 = 9
学习率 = 0.01
轮次数 = 496
保存和加载模型
掌握结构化数据的最后一步是能够保存和加载你已经训练和微调的模型。每次需要预测时都重新训练一个新模型会浪费大量时间,因此能够保存已训练的模型对于数据科学家来说至关重要。保存的模型使我们能够复制结果,并创建使用机器学习模型的应用程序和服务。步骤如下:
-
要保存 XGBoost 模型,你需要调用
save_model函数。model.save_model('wholesale-model.model') -
要加载之前保存的模型,您需要在初始化的 XGBoost 变量上调用
load_model。loaded_model = xgb.Booster({'nthread': 2}) loaded_model.load_model('wholesale-model.model')注意
如果您让 XGBoost 获取所有可用的线程,您的计算机在训练或预测时可能会变慢。
现在你已经准备好使用 XGBoost 库来开始建模你的结构化数据集了!
练习 46:创建一个基于实时输入进行预测的 Python 脚本
在这个练习中,我们将首先创建一个模型并保存它。然后,我们将创建一个 Python 脚本,利用这个保存的模型对用户输入的数据进行预测。
-
将活动 14 中的收入数据集加载为 pandas 数据框。
import pandas as pd import numpy as np data = pd.read_csv("../data/adult-data.csv", names=['age', 'workclass', 'education-num', 'occupation', 'capital-gain', 'capital-loss', 'hours-per-week', 'income']) -
去除所有尾随空格。
data[['workclass', 'occupation', 'income']] = data[['workclass', 'occupation', 'income']].apply(lambda x: x.str.strip()) -
使用 scikit 将所有分类变量从字符串转换为整数。
from sklearn.preprocessing import LabelEncoder from collections import defaultdict label_dict = defaultdict(LabelEncoder) data[['workclass', 'occupation', 'income']] = data[['workclass', 'occupation', 'income']].apply(lambda x: label_dict[x.name].fit_transform(x)) -
将标签编码器保存到一个 pickle 文件中,以供将来使用。pickle 文件存储 Python 对象,以便我们在需要时可以访问它们。
import pickle with open( 'income_labels.pkl', 'wb') as f: pickle.dump(label_dict, f, pickle.HIGHEST_PROTOCOL) -
将数据集拆分为训练集和测试集并创建模型。
-
将模型保存到文件。
model.save_model('income-model.model') -
在 Python 脚本中,加载模型和标签编码器。
import xgboost as xgb loaded_model = xgb.Booster({'nthread': 8}) loaded_model.load_model('income-model.model') def load_obj(file): with open(file + '.pkl', 'rb') as f: return pickle.load(f) label_dict = load_obj('income_labels') -
从用户那里读取输入。
age = input("Please enter age: ") workclass = input("Please enter workclass: ") education_num = input("Please enter education_num: ") occupation = input("Please enter occupation: ") capital_gain = input("Please enter capital_gain: ") capital_loss = input("Please enter capital_loss: ") hours_per_week = input("Please enter hours_per_week: ") -
创建一个数据框来存储这些数据。
data_list = [age, workclass, education_num, occupation, capital_gain, capital_loss, hours_per_week] data = pd.DataFrame([data_list]) data.columns = ['age', 'workclass', 'education-num', 'occupation', 'capital-gain', 'capital-loss', 'hours-per-week'] -
预处理数据。
data[['workclass', 'occupation']] = data[['workclass', 'occupation']].apply(lambda x: label_dict[x.name].transform(x)) -
转换为 Dmatrix 并使用模型进行预测。
data = data.astype(int) data_xgb = xgb.DMatrix(data) pred = loaded_model.predict(data_xgb) -
执行逆变换以获取结果。
income = label_dict['income'].inverse_transform([int(pred[0])])输出如下:
图 5.18:逆变换输出
注意
确保你输入的 workclass 和 occupation 值在训练数据中存在,否则脚本会抛出错误。当 LabelEncoder 遇到它之前未见过的新值时,就会发生此错误。
恭喜!你构建了一个使用用户输入数据进行预测的脚本。现在你可以将你的模型部署到任何你想要的地方。
活动 15:预测客户流失
在本活动中,我们将尝试预测一个客户是否会转移到另一个电信提供商。数据来自 IBM 示例数据集。让我们来看以下场景:你在一家电信公司工作,最近,许多用户开始转向其他提供商。现在,为了能够给流失的客户提供折扣,你需要预测哪些客户最有可能流失。为此,你需要创建一个机器学习模型,预测哪些客户会流失。
-
使用 pandas 加载电信流失(
telco-churn.csv)数据集(github.com/TrainingByP… www.ibm.com/communities…](p6-xtjj-sign.byteimg.com/tos-cn-i-73…)图 5.19:显示电信流失数据集前五个元素的截图
-
删除不必要的变量。
-
使用 scikit 将所有类别变量从字符串转换为整数。你可以使用以下代码:
data.TotalCharges = pd.to_numeric(data.TotalCharges, errors='coerce') -
修复使用 pandas 加载时的数据类型不匹配问题。
-
使用 XGBoost 库进行预测,并通过交叉验证进行参数调优,提升准确率至超过 80%。
-
保存你的模型以供将来使用。
注意
本活动的解决方案可以在第 361 页找到。
神经网络
神经网络是数据科学家使用的最流行的机器学习算法之一。它在需要图像或数字媒体来解决问题的场景中,始终优于传统的机器学习算法。在拥有足够数据的情况下,它在结构化数据问题中也超越了传统的机器学习算法。拥有超过两层的神经网络被称为深度神经网络,而利用这些“深度”网络解决问题的过程被称为深度学习。为了处理非结构化数据,神经网络有两种主要类型:卷积神经网络(CNN)可以用于处理图像,递归神经网络(RNN)可以用于处理时间序列和自然语言数据。我们将在第六章《解码图像》和第七章《处理人类语言》中深入讨论 CNN 和 RNN。接下来,让我们看看一个普通的神经网络是如何工作的。在这一部分,我们将简要讲解神经网络的不同部分,接下来的章节会详细解释每个话题。
什么是神经网络?
神经网络的基本单元是神经元。神经网络的灵感来源于生物大脑,因此神经元这一名称也由此得来。神经网络中的所有连接,就像大脑中的突触一样,可以将信息从一个神经元传递到另一个神经元。在神经网络中,输入信号的加权组合会被汇聚,然后经过一个函数处理后将输出信号传递出去。这个函数是一个非线性激活函数,代表着神经元的激活阈值。多个相互连接的神经元层组成了一个神经网络。神经网络中只有非输出层包含偏置单元。与每个神经元相关的权重和这些偏置共同决定了整个网络的输出;因此,这些就是我们在训练过程中为了拟合数据而修改的参数。
图 5.20:单层神经网络的表示
神经网络的第一层包含的节点数与数据集中的独立变量数量相等。这个层被称为输入层,接下来是多个隐藏层,最后是输出层。输入层的每个神经元接收数据集中的一个独立变量。输出层则输出最终的预测结果。如果是回归问题,这些输出可以是连续的(如 0.2、0.6、0.8);如果是分类问题,则输出为分类标签(如 2、4、5)。神经网络的训练通过调整网络的权重和偏置来最小化误差,这个误差是期望值与输出值之间的差异。权重与输入相乘,偏置值则与这些权重的组合相加,最终得到输出。
图 5.21:神经元输出
在这里,y 是神经元的输出,x 是输入,w 和 b 分别是权重和偏置,f 是激活函数,我们将在后面进一步学习它。
优化算法
为了最小化模型的误差,我们训练神经网络以最小化预定义的损失函数,使用优化算法。对于这个优化算法,有许多选择,具体选择哪个取决于你的数据和模型。在本书的大部分内容中,我们将使用随机梯度下降(SGD),它在大多数情况下表现良好,但我们会在需要时解释其他优化器。SGD 通过迭代地计算梯度来工作,梯度是权重相对于误差的变化。在数学上,它是相对于输入的偏导数。它找到的梯度有助于最小化给定的函数,在我们这个案例中就是损失函数。当我们接近解决方案时,梯度的大小会减小,从而防止我们超越最优解。
理解 SGD(随机梯度下降)最直观的方式是将其视为下坡的过程。最初,我们会采取陡峭的下降,然后当我们接近谷底时,坡度会变小。
图 5.22:梯度下降的直观理解(k 表示梯度的大小)
超参数
一个决定训练模型所需时间的重要参数叫做学习率,它本质上是我们进行下降时采取的步长。步长过小,模型需要很长时间才能找到最优解;步长过大,则会超越最优解。为了解决这个问题,我们从较大的学习率开始,并在进行几步后降低学习率。这有助于我们更快地达到最小值,并且由于步长的减小,防止模型超越解决方案。
接下来是权重的初始化。我们需要对神经网络的权重进行初始化,以便从一个起始点开始,然后调整权重以最小化误差。初始化在防止消失梯度和爆炸梯度问题中起着重要作用。
消失梯度问题指的是每经过一层,梯度逐渐变小,因为任何小于 1 的数相乘会变得更小,因此经过多层后,这个值会变为 0。
爆炸梯度问题发生在较大的误差梯度累加时,导致模型更新过大。如果模型的损失变为 NaN,这可能是一个问题。
使用Xavier 初始化,我们可以防止这些问题,因为它在初始化权重时考虑了网络的大小。Xavier 初始化通过从以 0 为中心的截断正态分布中抽取权重进行初始化,标准差为
图 5.23:Xavier 初始化使用的标准差。
其中 xi 是该层输入神经元的数量,yi 是输出神经元的数量。
这确保了即使网络中的层数非常大,输入和输出的方差仍然保持不变。
损失函数
另一个需要考虑的超参数是损失函数。根据问题的类型,分类或回归,使用不同的损失函数。对于分类,我们选择交叉熵、铰链等损失函数;对于回归,我们使用均方误差、平均绝对误差(MAE)和 Huber 损失函数。不同的损失函数适用于不同的数据集。我们将在使用过程中逐步介绍它们。
激活函数
在创建神经网络层时,您需要定义一个激活函数,这取决于该层是隐藏层还是输出层。对于隐藏层,我们将使用 ReLU 或 tanh 激活函数。激活函数帮助神经网络模型实现非线性函数。几乎没有实际问题可以通过线性模型来解决。除了这些,不同的激活函数具有不同的特点。Sigmoid 输出的值在 0 和 1 之间,而 tanh 则将输出围绕 0 进行中心化,有助于更好的学习。ReLU 则可以防止梯度消失问题,并且在计算上更为高效。下面是 ReLU 图形的表示。
图 5.24:ReLU 激活函数的表示
Softmax 输出的是概率,用于多分类问题,而 sigmoid 输出的是介于 0 和 1 之间的值,仅用于二分类问题。线性激活通常用于解决回归问题的模型。下图展示了 sigmoid 激活函数的表示:
图 5.25:Sigmoid 激活函数的表示
上一部分介绍了很多新信息;如果您感到困惑,不必担心。我们将在接下来的章节中实践所有这些概念,这将加强对这些主题的理解。
Keras
Keras 是一个用 Python 编写的开源高级神经网络 API,能够在 TensorFlow、微软认知工具包(CNTK)或 Theano 上运行。Keras 的开发旨在快速实验,从而帮助加速应用程序开发。使用 Keras,用户可以以最短的延迟从创意到结果。由于强大的社区支持,Keras 支持几乎所有最新的数据科学模型,尤其是与神经网络相关的模型。它包含了多种常用构建块的实现,如层、批量归一化、Dropout、目标函数、激活函数和优化器。同时,Keras 允许用户为智能手机(Android 和 iOS)、Web 或 Java 虚拟机 (JVM) 创建模型。使用 Keras,您可以在 GPU 上训练模型,而无需更改代码。
鉴于 Keras 的所有这些特性,数据科学家必须学习如何使用该库的各个方面。掌握 Keras 的使用将极大地帮助你成为一名优秀的数据科学家。为了展示 Keras 的强大功能,我们将安装它并创建一个单层神经网络模型。
注意
你可以在这里阅读更多关于 Keras 的信息:keras.io/
练习 47:为 Python 安装 Keras 库并使用它进行分类
在本练习中,我们将使用 Python 的 Keras 库对批发客户数据集(我们在练习 44中使用的那个)进行分类。
-
在虚拟环境中运行以下命令以安装 Keras。
pip3 install keras -
从你的虚拟环境中打开 Jupyter Notebook。
-
导入 Keras 和其他所需的库。
import pandas as pd from keras.models import Sequential from keras.layers import Dense import numpy as np from sklearn.metrics import accuracy_score -
使用 pandas 读取批发客户数据集,并使用以下命令检查数据是否成功加载:
data = pd.read_csv("data/wholesale-data.csv") data.head()输出应该是这样的:
图 5.26:显示数据集前五个元素的截图
-
将数据拆分为特征和标签。
X = data.copy()X.drop("Channel", inplace = True, axis = 1)Y = data.Channel -
创建训练集和测试集。
X_train, X_test = X[:int(X.shape[0]*0.8)].values, X[int(X.shape[0]*0.8):].values Y_train, Y_test = Y[:int(Y.shape[0]*0.8)].values, Y[int(Y.shape[0]*0.8):].values -
创建神经网络模型。
model = Sequential() model.add(Dense(units=8, activation='relu', input_dim=7)) model.add(Dense(units=16, activation='relu')) model.add(Dense(units=1, activation='sigmoid'))在这里,我们创建了一个四层网络,其中包含一个输入层、两个隐藏层和一个输出层。隐藏层使用 ReLU 激活函数,输出层使用 softmax 激活函数。
-
编译并训练模型。我们使用的是二元交叉熵损失函数,这与我们之前讨论的 logloss 相同;我们选择了随机梯度下降作为优化器。我们将训练运行五个 epoch,批量大小为八。
model.compile(loss='binary_crossentropy', optimizer='sgd', metrics=['accuracy']) model.fit(X_train, Y_train, epochs=5, batch_size=8)注意
你将看到模型训练日志。Epoch 表示训练的迭代次数,352 表示数据集的大小除以批量大小。在进度条之后,你可以看到一次迭代所用的时间。接下来,你会看到训练每个批次所需的平均时间。接下来是模型的损失,这里是二元交叉熵损失,然后是迭代后的准确度。这些术语中的一些是新的,但我们将在接下来的章节中逐一了解它们。
图 5.27:模型训练日志截图
-
预测测试集的值。
preds = model.predict(X_test, batch_size=128) -
获取模型的准确度。
accuracy = accuracy_score(Y_test, preds.astype(int)) print("Accuracy: %.2f%%" % (accuracy * 100.0))输出结果如下:
图 5.28:输出准确度
恭喜!你刚刚成功创建了第一个神经网络模型,准确率约为 81%,而且没有进行任何微调!你会注意到,与 XGBoost 相比,这个准确率相当低。在接下来的章节中,你将学习如何提高这个准确度。准确率较低的一个主要原因是数据的大小。为了让神经网络模型真正发挥作用,它必须有一个大型的数据集来进行训练,否则会出现过拟合。
Keras 库
Keras 使得模块化成为可能。所有的初始化器、损失函数、优化器、层、正则化器和激活函数都是独立的模块,可以用于任何类型的数据和网络架构。你会发现几乎所有最新的函数都已经在 Keras 中实现。这使得代码的可重用性和快速实验成为可能。作为数据科学家,你不会受到内建模块的限制;创建自己的自定义模块并与其他内建模块一起使用是非常简单的。这促进了研究并有助于不同的应用场景。例如,你可能需要编写一个自定义的损失函数来最大化汽车销量,并对利润更高的汽车赋予更大的权重,从而提高整体利润。
所有你需要创建神经网络的不同种类的层在 Keras 中都有定义。我们将在使用它们时进行探讨。在 Keras 中创建神经模型的主要方式有两种,顺序模型和功能性 API。
顺序模型:顺序模型是一个层的线性堆叠。这是使用 Keras 创建神经网络模型的最简单方式。下面给出了该模型的代码片段:
model = Sequential()model.add(Dense(128, input_dim=784))model.add(Activation('relu'))
model.add(Dense(10))model.add(Activation('softmax'))
功能性 API:功能性 API 是构建复杂模型的方式。由于顺序模型是线性的,无法创建复杂模型。功能性 API 让你能够创建模型的多个部分,然后将它们合并在一起。通过功能性 API 创建的相同模型如下所示:
inputs = Input(shape=(784,))
x = Dense(128, activation='relu')(inputs)prediction = Dense(10, activation='softmax')(x)model = Model(inputs=inputs, outputs=prediction)
Keras 的一个强大功能是回调函数。回调函数允许你在训练过程的任何阶段使用函数。这对于获取统计信息和在不同阶段保存模型非常有用。它还可以用来对学习率应用自定义衰减,并执行提前停止。
filepath="model-weights-{epoch:02d}-{val_loss:.2f}.hdf5"
model_ckpt = ModelCheckpoint(filepath, monitor='val_loss', verbose=1, save_best_only=True, mode='auto')
callbacks = [model_ckpt]
要保存你在 Keras 上训练的模型,你只需要使用以下代码:
model.save('Path to model')
要从文件中加载模型,使用以下代码:
keras.models.load_model('Path to model')
提前停止是一个可以通过回调函数实现的有用功能。提前停止有助于节省训练模型的时间。如果指定的指标变化小于设定的阈值,训练过程将停止。
EarlyStopping(monitor='val_loss', min_delta=0.01, patience=5, verbose=1, mode='auto')
上述回调函数将在验证损失的变化小于 0.01 且持续五个周期时停止训练。
注意
始终使用 ModelCheckpoint 来存储模型状态。对于大型数据集和更大的网络,这一点尤为重要。
练习 48:使用神经网络预测鳄梨价格
让我们应用本节所学的知识,创建一个优秀的神经网络模型,用于预测不同种类鳄梨的价格。数据集(github.com/TrainingByP…
注意
原始来源网站:www.hassavocadoboard.com/retail/volu…
-
导入鳄梨数据集并观察列。你将看到如下内容:
import pandas as pd import numpy as np data = pd.read_csv('data/avocado.csv') data.T图 5.29:展示鳄梨数据集的截图
-
浏览数据并将日期列拆分为天和月。这将帮助我们捕捉季节性变化,同时忽略年份。现在,删除日期和未命名的列。
data['Day'], data['Month'] = data.Date.str[:2], data.Date.str[3:5] data = data.drop(['Unnamed: 0', 'Date'], axis = 1) -
使用
LabelEncoder对分类变量进行编码,以便 Keras 可以使用它来训练模型。from sklearn.preprocessing import LabelEncoder from collections import defaultdict label_dict = defaultdict(LabelEncoder) data[['region', 'type', 'Day', 'Month', 'year']] = data[['region', 'type', 'Day', 'Month', 'year']].apply(lambda x: label_dict[x.name].fit_transform(x)) -
将数据分为训练集和测试集。
from sklearn.model_selection import train_test_split X = data y = X.pop('AveragePrice') X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=9) -
使用回调函数在损失改善时保存模型,并在模型表现不佳时进行早停。
from keras.callbacks import ModelCheckpoint, EarlyStopping filepath="avocado-{epoch:02d}-{val_loss:.2f}.hdf5" model_ckpt = ModelCheckpoint(filepath, monitor='val_loss', verbose=1, save_best_only=True, mode='auto') es = EarlyStopping(monitor='val_loss', min_delta=1, patience=5, verbose=1) callbacks = [model_ckpt, es] -
创建神经网络模型。这里,我们使用与之前相同的模型。
from keras.models import Sequential from keras.layers import Dense model = Sequential() model.add(Dense(units=16, activation='relu', input_dim=13)) model.add(Dense(units=8, activation='relu')) model.add(Dense(units=1, activation='linear')) model.compile(loss='mse', optimizer='adam') -
训练并评估模型,以获取模型的 MSE。
model.fit(X_train, y_train, validation_data = (X_test, y_test), epochs=40, batch_size=32) model.evaluate(X_test, y_test) -
查看下方截图中的最终输出:
图 5.30:模型的 MSE
恭喜!你刚刚训练了你的神经网络,得到了合理的误差值,应用于鳄梨数据集。上面显示的值是模型的均方误差。修改一些超参数并使用剩余数据,看看能否获得更好的误差分数。利用前面章节提供的信息。
注意
MSE(均方误差)下降是理想的。最优值将取决于具体情况。例如,在预测汽车速度时,低于 100 的值是理想的,而在预测一个国家的 GDP 时,1000 的 MSE 已经足够好。
分类变量
分类变量是其值可以表示为不同类别的变量。例如,球的颜色、狗的品种和邮政编码等。将这些分类变量映射到单一维度会导致它们之间相互依赖,这是不正确的。尽管这些分类变量没有顺序或依赖关系,但将它们作为单一特征输入神经网络时,神经网络会根据顺序在这些变量之间创建依赖关系,而实际上,顺序并不代表任何意义。在本节中,我们将学习如何解决这个问题并训练有效的模型。
独热编码
映射分类变量的最简单且最广泛使用的方法是使用独热编码。使用此方法,我们将一个分类特征转换为等于特征中类别数量的多个特征。
图 5.31:分类特征转换
使用以下步骤将分类变量转换为独热编码变量:
-
如果数据类型不是整数,则将数据转换为数字。有两种方法可以做到这一点。
-
你可以直接使用 sklearn 的
LabelEncoder方法。 -
创建箱子以减少类别的数量。类别的数量越高,模型的难度越大。你可以选择一个整数来表示每个箱子。请记住,进行这种操作会导致信息的丧失,并可能导致模型效果不佳。你可以使用以下规则进行直方图分箱:
如果分类列的数量少于 25,使用 5 个箱子。
如果分类列的数量在 25 到 100 之间,使用 n/5 个箱子,其中 n 是分类列的数量;如果超过 100,使用 10 * log (n)个箱子。
注意
你可以将频率小于 5%的类别合并为一个类别。
-
使用 pandas 的
get_dummies函数将步骤 1 中的数值数组转换为一热向量。
图 5.32:get_dummies 函数的输出
一热编码不是使用分类数据的最佳方法,主要有两个原因:
-
假设分类变量的不同值是完全独立的。这会导致它们之间关系信息的丧失。
-
拥有许多类别的分类变量会导致一个计算成本非常高的模型。数据集越宽广,生成有意义的模型所需的数据点就越多。这就是所谓的维度灾难。
为了解决这些问题,我们将使用实体嵌入。
实体嵌入
实体嵌入将分类特征表示在一个多维空间中。这确保了网络学习不同类别之间的正确关系。这个多维空间的维度没有特定的意义;它可以是模型认为合适学习的任何内容。例如,在一周的日子中,一维可以表示是否是工作日,另一维可以表示与工作日的距离。这种方法的灵感来源于自然语言处理中的词嵌入,用于学习词汇和短语之间的语义相似性。创建嵌入可以帮助教会神经网络如何区分星期五和星期三,或者小狗和狗的区别。例如,一周七天的四维嵌入矩阵可能如下所示:
](tos-cn-i-73owjymdk6/42fa56b57b354676b65349185c39e45a)
图 5.33:四维嵌入矩阵
从上面的矩阵中,你可以看到嵌入学习了类别之间的依赖关系:它知道周六和周日比周四和周五更相似,因为周六和周日的向量是相似的。当数据集中有大量分类变量时,实体嵌入会提供巨大的优势。要在 Keras 中创建实体嵌入,你可以使用嵌入层。
注意
总是尽量使用词嵌入,因为它能提供最佳的结果。
练习 49:使用实体嵌入预测鳄梨价格
让我们利用实体嵌入的知识,通过创建一个更好的神经网络模型来预测鳄梨价格。我们将使用之前的鳄梨数据集。
-
导入鳄梨价格数据集并检查是否有空值。将日期列拆分为月份和日期列。
import pandas as pd import numpy as np data = pd.read_csv('data/avocado.csv') data['Day'], data['Month'] = data.Date.str[:2], data.Date.str[3:5] data = data.drop(['Unnamed: 0', 'Date'], axis = 1) data = data.dropna() -
对分类变量进行编码。
from sklearn.preprocessing import LabelEncoder from collections import defaultdict label_dict = defaultdict(LabelEncoder) data[['region', 'type', 'Day', 'Month', 'year']] = data[['region', 'type', 'Day', 'Month', 'year']].apply(lambda x: label_dict[x.name].fit_transform(x)) -
将数据拆分为训练集和测试集。
from sklearn.model_selection import train_test_split X = data y = X.pop('AveragePrice') X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=9) -
创建一个字典,将分类列名映射到其中的唯一值。
cat_cols_dict = {col: list(data[col].unique()) for col in ['region', 'type', 'Day', 'Month', 'year'] } -
接下来,获取嵌入神经网络可以接受的输入数据格式。
train_input_list = [] test_input_list = [] for col in cat_cols_dict.keys(): raw_values = np.unique(data[col]) value_map = {} for i in range(len(raw_values)): value_map[raw_values[i]] = i train_input_list.append(X_train[col].map(value_map).values) test_input_list.append(X_test[col].map(value_map).fillna(0).values) other_cols = [col for col in data.columns if (not col in cat_cols_dict.keys())] train_input_list.append(X_train[other_cols].values) test_input_list.append(X_test[other_cols].values)在这里,我们要做的是创建一个包含所有变量的数组列表。
-
接下来,创建一个字典,用于存储嵌入层的输出维度。这是变量所表示的值的数量。你必须通过反复试验来确定正确的数字。
cols_out_dict = { 'region': 12, 'type': 1, 'Day': 10, 'Month': 3, 'year': 1 } -
现在,为分类变量创建嵌入层。在循环的每次迭代中,我们为分类变量创建一个嵌入层。
from keras.models import Model from keras.layers import Input, Dense, Concatenate, Reshape, Dropout from keras.layers.embeddings import Embedding inputs = [] embeddings = [] for col in cat_cols_dict.keys(): inp = Input(shape=(1,), name = 'input_' + col) embedding = Embedding(cat_cols_dict[col], cols_out_dict[col], input_length=1, name = 'embedding_' + col)(inp) embedding = Reshape(target_shape=(cols_out_dict[col],))(embedding) inputs.append(inp) embeddings.append(embedding) -
现在,将连续变量添加到网络中并完成模型。
input_numeric = Input(shape=(8,)) embedding_numeric = Dense(16)(input_numeric) inputs.append(input_numeric) embeddings.append(embedding_numeric) x = Concatenate()(embeddings) x = Dense(16, activation='relu')(x) x = Dense(4, activation='relu')(x) output = Dense(1, activation='linear')(x) model = Model(inputs, output) model.compile(loss='mse', optimizer='adam') -
用我们在第 5 步创建的 train_input_list 训练模型,训练 50 个 epoch。
model.fit(train_input_list, y_train, validation_data = (test_input_list, y_test), epochs=50, batch_size=32) -
现在,从嵌入层获取权重以可视化嵌入。
embedding_region = model.get_layer('embedding_region').get_weights()[0] -
执行 PCA,并使用区域标签绘制输出(你可以通过对我们之前创建的字典执行逆变换获得区域标签)。PCA 通过将维度降至二维,将相似的数据点聚集在一起。在这里,我们仅绘制前 25 个区域。
如果你想的话,可以绘制所有它们。
import matplotlib.pyplot as plt from sklearn.decomposition import PCA pca = PCA(n_components=2) Y = pca.fit_transform(embedding_region[:25]) plt.figure(figsize=(8,8)) plt.scatter(-Y[:, 0], -Y[:, 1]) for i, txt in enumerate((label_dict['region'].inverse_transform(cat_cols_dict['region']))[:25]): plt.annotate(txt, (-Y[i, 0],-Y[i, 1]), xytext = (-20, 8), textcoords = 'offset points') plt.show()
图 5.34:使用实体嵌入表示鳄梨生长区域的图形
恭喜!你通过使用实体嵌入提高了模型的准确性。从嵌入图中可以看出,模型能够识别出平均价格高和低的区域。你可以绘制其他变量的嵌入,看看网络从数据中得出了什么关系。另外,尝试通过超参数调优来提高模型的准确性。
活动 16:预测顾客的购买金额
在本活动中,我们将尝试预测顾客在某一产品类别上的消费金额。数据集(github.com/TrainingByP…
-
使用 pandas 加载 Black Friday 数据集。该数据集是一个零售店的交易记录集合。它包含的信息包括客户的年龄、城市、婚姻状况、购买商品的类别以及账单金额。前几行应该像这样:
图 5.35:显示 Black Friday 数据集前五个元素的截图
移除不必要的变量和空值。移除
Product_Category_2和Product_Category_3列。 -
对所有分类变量进行编码。
-
通过使用 Keras 库创建神经网络进行预测。利用实体嵌入并进行超参数调优。
-
保存你的模型以便日后使用。
注意
本活动的解决方案可以在第 364 页找到。
总结
在这一章中,我们学习了如何创建高精度的结构化数据模型,了解了 XGBoost 是什么,以及如何使用该库来训练模型。在开始之前,我们曾经想知道什么是神经网络,以及如何使用 Keras 库来训练模型。了解了神经网络后,我们开始处理分类数据。最后,我们学习了什么是交叉验证以及如何使用它。
现在你已经完成了这一章的内容,你可以处理任何类型的结构化数据,并用它创建机器学习模型。在下一章,你将学习如何为图像数据创建神经网络模型。
第十六章:第六章
解码图像
学习目标
到本章结束时,你将能够:
-
创建能够将图像分类为不同类别的模型
-
使用 Keras 库训练图像的神经网络模型
-
在不同的商业场景中利用图像增强的概念
-
从图像中提取有意义的信息
本章将涵盖如何读取和处理图像的各种概念。
介绍
到目前为止,我们只处理过数字和文本。在本章中,我们将学习如何使用机器学习解码图像并提取有意义的信息,比如图像中存在的物体类型或图像中写的数字。你有没有停下来思考过我们的大脑是如何解读它们从眼睛接收到的图像的?经过数百万年的进化,我们的大脑已经变得非常高效和准确,能够从眼睛接收到的图像中识别物体和模式。我们已经能够通过相机复制眼睛的功能,但让计算机识别图像中的模式和物体却是一项非常艰巨的任务。与理解图像中存在的内容相关的领域被称为计算机视觉。计算机视觉领域在过去几年里经历了巨大的研究和进展。卷积神经网络(CNN)和在 GPU 上训练神经网络的能力是其中最重大的突破之一。如今,CNN 被广泛应用于任何计算机视觉问题中,例如自动驾驶汽车、人脸识别、物体检测、物体追踪以及创建完全自主的机器人。在本章中,我们将学习这些 CNN 是如何工作的,并了解它们相比传统方法有哪些显著的改进。
图像
我们今天使用的数码相机将图像存储为一个巨大的数字矩阵,这些就是我们所说的数字图像。矩阵中的每个数字代表图像中的一个像素。每个数字代表该像素的颜色强度。对于灰度图像,这些值的范围是 0 到 255,其中 0 是黑色,255 是白色。对于彩色图像,这个矩阵是三维的,每个维度对应红色、绿色和蓝色的值。矩阵中的值代表各自颜色的强度。我们将这些值作为输入,用于我们的计算机视觉程序或数据科学模型,以进行预测和识别。
现在,有两种方法可以让我们使用这些像素来创建机器学习模型:
-
将单独的像素作为不同的输入变量输入神经网络
-
使用卷积神经网络
创建一个将单个像素值作为输入变量的全连接神经网络是目前最简单、最直观的方法,因此我们将从创建这个模型开始。在下一节中,我们将学习 CNN,并了解它们在处理图像时的优势。
练习 50:使用全连接神经网络分类 MNIST
在本次练习中,我们将对 修改后的国家标准与技术研究所数据库(MNIST)数据集进行分类。MNIST 是一个手写数字数据集,已被规范化以适应 28 x 28 像素的边界框。该数据集包含 60,000 张训练图像和 10,000 张测试图像。在完全连接的网络中,我们将单个像素作为特征输入到网络中,然后像训练第一个神经网络一样训练它,就像在 第五章 掌握结构化数据 中训练的第一个神经网络一样。
完成此练习,请执行以下步骤:
-
加载所需的库,如下所示:
import numpy as np import matplotlib.pyplot as plt from sklearn.preprocessing import LabelBinarizer from keras.datasets import mnist from keras.models import Sequential from keras.layers import Dense -
使用 Keras 库加载 MNIST 数据集:
(x_train, y_train), (x_test, y_test) = mnist.load_data() -
从数据集的形状,你可以推测数据是以 2D 格式呈现的。第一个元素是可用图像的数量,接下来的两个元素是图像的宽度和高度:
x_train.shape输出结果如下:
图 6.1:图像的宽度和高度
-
绘制第一张图像,查看你正在处理的数据类型:
plt.imshow(x_test[0], cmap=plt.get_cmap(‘gray')) plt.show()图 6.2:MNIST 数据集的样本图像
-
将 2D 数据转换为 1D 数据,以便我们的神经网络可以将其作为输入(28 x 28 像素 = 784):
x_train = x_train.reshape(60000, 784) x_test = x_test.reshape(10000, 784) -
将目标变量转换为 one-hot 向量,这样我们的网络就不会在不同的目标变量之间形成不必要的连接:
label_binarizer = LabelBinarizer() label_binarizer.fit(range(10)) y_train = label_binarizer.transform(y_train) y_test = label_binarizer.transform(y_test) -
创建模型。建立一个小型的两层网络;你可以尝试其他架构。接下来你将学习更多关于交叉熵损失的内容:
model = Sequential() model.add(Dense(units=32, activation='relu', input_dim=784)) model.add(Dense(units=32, activation='relu')) model.add(Dense(units=10, activation='softmax')) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics = [‘acc']) model.summary()图 6.3:稠密网络的模型架构
-
训练模型并检查最终准确率:
model.fit(x_train, y_train, validation_data = (x_test, y_test), epochs=40, batch_size=32) score = model.evaluate(x_test, y_test) print(“Accuracy: {0:.2f}%”.format(score[1]*100))输出结果如下:
图 6.4:模型准确率
恭喜!你现在已经创建了一个能够以 93.57% 的准确率预测图像上的数字的模型。你可以使用以下代码绘制不同的测试图像,并查看网络的结果。更改图像变量的值来获取不同的图像:
image = 6
plt.imshow(x_test[image].reshape(28,28),
cmap=plt.get_cmap(‘gray'))
plt.show()
y_pred = model.predict(x_test)
print(“Prediction: {0}”.format(np.argmax(y_pred[image])))
图 6.5:带有稠密网络预测的 MNIST 图像
你可以仅可视化错误的预测,以了解你的模型在哪些地方失败:
incorrect_indices = np.nonzero(np.argmax(y_pred,axis=1) != np.argmax(y_test,axis=1))[0]
image = 4
plt.imshow(x_test[incorrect_indices[image]].reshape(28,28),
cmap=plt.get_cmap(‘gray'))
plt.show()
print(“Prediction: {0}”.format(np.argmax(y_pred[incorrect_indices[image]])))
图 6.6:稠密网络错误分类的示例
如你在之前的截图中所见,模型失败了,因为我们预测的类别是 2,而正确的类别是 3。
卷积神经网络
卷积神经网络(CNN)是指具有卷积层的神经网络。这些卷积层借助卷积滤波器高效地处理原始图像的高维度。CNN 使我们能够识别图像中的复杂模式,这是简单神经网络无法做到的。CNN 还可以用于自然语言处理。
CNN 的前几层是卷积层,网络在这些层中应用不同的滤波器来寻找图像中的有用模式;接着是池化层,它们有助于下采样卷积层的输出。激活层控制信号从一层流向下一层,模拟我们大脑中的神经元。网络中的最后几层是全连接层;这些层与我们在之前练习中使用的层相同。
卷积层
卷积层由多个滤波器组成,它们在看到初始层中的特征、边缘或颜色时会激活,最终能够识别出面孔、蜂窝图案和车轮等。这些滤波器就像我们常用的 Instagram 滤镜一样。滤镜通过以某种方式改变像素来改变图像的外观。以一个检测水平边缘的滤波器为例。
图 6.7:水平边缘检测滤波器
如前面截图所示,滤波器将图像转换成另一幅图像,其中水平线被突出显示。为了得到这种转换,我们将图像的部分区域与滤波器逐一相乘。首先,我们取图像的左上角 3x3 区域,并与滤波器进行矩阵乘法,得到转换后的第一个左上角像素。然后,我们将滤波器向右移动一个像素,得到转换后的第二个像素,依此类推。转换后的图像是一幅只突出显示水平线部分的图像。滤波器参数的值(此处为 9)是卷积层在训练过程中学习的权重或参数。有些滤波器可能学会检测水平线,有些则是垂直线,或者是 45 度角的线。随后的层将学习到更复杂的结构,比如车轮或人脸的模式。
这里列出了一些卷积层的超参数:
-
滤波器:这是网络中每层的滤波器数量。这个数字也反映了转换的维度,因为每个滤波器将导致输出的一个维度。
-
滤波器大小:这是网络将学习的卷积滤波器的大小。这个超参数将决定输出转换的大小。
-
步长:在前述水平边缘示例中,我们每次通过都将滤波器移动一个像素。这就是步长。它指的是滤波器每次通过时移动的量。这个超参数还决定了输出转换的大小。
-
填充:这是一个超参数,使网络在图像的所有边缘填充零。在某些情况下,这有助于保留边缘信息,并确保输入和输出的大小相同。
注意
如果进行填充,则得到的图像大小与卷积操作的输出相同或更大。如果不进行填充,则图像大小将会减小。
池化层
池化层 将输入图像的大小减小,以减少网络中的计算量和参数。池化层周期性地插入到卷积层之间,以控制过拟合。最常见的池化变体是 2 x 2 最大池化,步长为 2。此变体通过下采样输入,保留输出中四个像素的最大值。深度维度保持不变。
图 6.8:最大池化操作
在过去,我们也进行过平均池化,但是现在更常见的是使用最大池化,因为在实践中已经证明其效果更好。许多数据科学家不喜欢使用池化层,仅仅是因为池化操作会伴随信息的丢失。关于这个主题已经有一些研究,发现在某些时候,简单的没有池化层的架构能够超越最先进的模型。为了减少输入的大小,建议偶尔在卷积层中使用更大的步长。
注意
研究论文 Striving for Simplicity: The All Convolutional Net 评估具有池化层的模型,发现在有足够数据可用时,池化层并不总是能够提高网络的性能。有关更多信息,请阅读 Striving for Simplicity: The All Convolutional Net 论文:arxiv.org/abs/1412.68…
Adam 优化器
优化器通过损失函数更新权重。选择错误的优化器或优化器的错误超参数可能会导致在找到问题的最优解时延迟。
Adam 的名称源自自适应矩估计。Adam 是专门设计用于训练深度神经网络的优化器。由于其快速接近最优解的速度,Adam 在数据科学社区中被广泛使用。因此,如果您想要快速收敛,请使用 Adam 优化器。但是,Adam 并不总是导致最优解;在这种情况下,带有动量的 SGD 有助于实现最先进的结果。以下是参数:
-
学习率:这是优化器的步长。较大的值(0.2)会导致更快的初始学习速度,而较小的值(0.00001)会在训练过程中减慢学习速度。
-
Beta 1:这是梯度均值估计的指数衰减率。
-
Beta 2:这是梯度的未中心化方差估计的指数衰减率。
-
Epsilon:这是一个非常小的数值,用于防止除零错误。
对于深度学习问题,一个好的起始点是学习率 = 0.001,Beta 1 = 0.9,Beta 2 = 0.999,Epsilon = 10^-8。
注意
欲了解更多信息,请阅读 Adam 论文:arxiv.org/abs/1412.6980v8
交叉熵损失
交叉熵损失用于分类问题中,其中每个类别的输出是介于 0 和 1 之间的概率值。这里的损失随着模型偏离实际值而增加;它遵循一个负对数图形。当模型预测的概率远离实际值时,这种损失尤为有效。例如,如果真实标签的概率是 0.05,我们会给模型一个很大的惩罚损失。另一方面,如果真实标签的概率是 0.40,我们则给予它较小的惩罚损失。
](tos-cn-i-73owjymdk6/e5e0c8f5113c416790bea75ceae146ce)
图 6.9:对数损失与概率的关系图
上面的图表显示,当预测值远离真实标签时,损失会呈指数增长。交叉熵损失遵循的公式如下:
](tos-cn-i-73owjymdk6/0928adc1891547a2bd7f9aa9c73e5718)
图 6.10:交叉熵损失公式
M是数据集中的类别数(对于 MNIST 来说是 10),y是真实标签,p是该类别的预测概率。我们偏好使用交叉熵损失来进行分类,因为随着我们接近真实值,权重更新会变得越来越小。交叉熵损失只会惩罚正确类别的概率。
练习 51:使用 CNN 对 MNIST 进行分类
在这个练习中,我们将使用 CNN 对**修改后的国家标准与技术研究院(MNIST)**数据集进行分类,而不是像练习 50中那样使用全连接层。我们将完整的图像作为输入,得到图像上的数字作为输出:
-
使用 Keras 库加载 MNIST 数据集:
from keras.datasets import mnist (x_train, y_train), (x_test, y_test) = mnist.load_data() -
将二维数据转换为三维数据,第三维只有一层,这是 Keras 要求的输入格式:
x_train = x_train.reshape(-1, 28, 28, 1) x_test = x_test.reshape(-1, 28, 28, 1) -
将目标变量转换为一个独热编码向量,这样我们的网络就不会在不同的目标变量之间形成不必要的连接:
from sklearn.preprocessing import LabelBinarizer label_binarizer = LabelBinarizer() label_binarizer.fit(range(10)) y_train = label_binarizer.transform(y_train) y_test = label_binarizer.transform(y_test) -
创建模型。这里,我们构建了一个小型 CNN。你可以尝试其他架构:
from keras.models import Model, Sequential from keras.layers import Dense, Conv2D, MaxPool2D, Flatten model = Sequential()添加卷积层:
model.add(Conv2D(32, kernel_size=3, padding=”same”,input_shape=(28, 28, 1), activation = ‘relu')) model.add(Conv2D(32, kernel_size=3, activation = ‘relu'))添加池化层:
model.add(MaxPool2D(pool_size=(2, 2))) -
将二维矩阵展平成一维向量:
model.add(Flatten()) -
使用全连接层作为模型的最后几层:
model.add(Dense(128, activation = “relu”)) model.add(Dense(10, activation = “softmax”)) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics = [‘acc']) model.summary()为了完全理解这一点,请查看模型输出的以下截图:
](tos-cn-i-73owjymdk6/fc8f02043ae44d108ec4e239a5678103)
图 6.11:CNN 的模型架构
-
训练模型并检查最终的准确度:
model.fit(x_train, y_train, validation_data = (x_test, y_test), epochs=10, batch_size=1024) score = model.evaluate(x_test, y_test) print(“Accuracy: {0:.2f}%”.format(score[1]*100))输出如下:
](tos-cn-i-73owjymdk6/0928adc1891547a2bd7f9aa9c73e5718)
图 6.12:最终模型准确率
恭喜你!现在你已经创建了一个能够以 98.62% 的准确率预测图像上数字的模型。你可以使用练习 50中提供的代码绘制不同的测试图像,并查看你的网络结果。还可以绘制错误预测,看看模型哪里出错:
import numpy as np
import matplotlib.pyplot as plt
incorrect_indices = np.nonzero(np.argmax(y_pred,axis=1) != np.argmax(y_test,axis=1))[0]
image = 4
plt.imshow(x_test[incorrect_indices[image]].reshape(28,28),
cmap=plt.get_cmap(‘gray'))
plt.show()
print(“Prediction: {0}”.format(np.argmax(y_pred[incorrect_indices[image]])))
图 6.13:模型的错误预测;真实标签为 2
如你所见,模型在预测模糊的图像时遇到了困难。你可以尝试调整层和超参数,看看是否能获得更好的准确率。尝试用更高步幅的卷积层替代池化层,正如前面一节中建议的那样。
正则化
正则化是一种通过修改学习算法帮助机器学习模型更好地泛化的技术。它有助于防止过拟合,并使我们的模型在训练过程中未见过的数据上表现得更好。在本节中,我们将学习可用的不同正则化方法。
丢弃层
**丢弃(Dropout)**是一种我们用来防止神经网络模型过拟合的正则化技术。在训练过程中,我们随机忽略网络中的神经元。这样可以防止这些神经元的激活信号继续传播下去,且在反向传播时这些神经元的权重更新不会被应用。神经元的权重被调节来识别特定的特征,而与它们相邻的神经元则变得依赖于这些特征,这可能会导致过拟合,因为这些神经元可能会过于专门化于训练数据。当神经元被随机丢弃时,相邻的神经元会介入并学习这些表示,从而使网络学习到多种不同的表示。这使得网络能更好地进行泛化,并防止模型过拟合。一个需要注意的重要事项是,当你进行预测或测试模型时,不应使用丢弃层。这会使模型失去宝贵的信息,并导致性能下降。Keras 会自动处理这个问题。
在使用丢弃层时,建议创建更大的网络,因为这能为模型提供更多学习的机会。我们通常使用 0.2 到 0.5 之间的丢弃概率。该概率指的是神经元在训练过程中被丢弃的概率。每层之后使用丢弃层通常能得到较好的效果,因此你可以从每层之后放置一个丢弃层,概率设置为 0.2,然后从那里进行微调。
要在 Keras 中创建一个丢弃层(dropout layer),且其概率为 0.5,你可以使用以下函数:
keras.layers.Dropout(0.5)
图 6.14:在密集神经网络中可视化丢弃
L1 和 L2 正则化
L2 是最常见的正则化类型,其次是 L1。这些正则化器通过向模型的损失中添加一个项来工作,以获得最终的代价函数。这一额外的项会导致模型的权重减少,从而使模型具有良好的泛化能力。
L1 正则化的代价函数如下所示:
图 6.15:L1 正则化的代价函数
这里,λ 是正则化参数。L1 正则化会导致权重非常接近零。这使得应用 L1 正则化的神经元仅依赖于最重要的输入,并忽略噪声输入。
L2 正则化的代价函数如下所示:
图 6.16:L2 正则化的代价函数
L2 正则化对高权重向量进行重罚,并偏好扩散的权重。L2 正则化也被称为 权重衰减,因为它迫使网络的权重衰减到接近零,但与 L1 正则化不同,L2 正则化并不会完全将权重压缩到零。我们可以将 L1 和 L2 正则化结合使用。要实现这些正则化器,您可以在 Keras 中使用以下函数:
keras.regularizers.l1(0.01)
keras.regularizers.l2(0.01)
keras.regularizers.l1_l2(l1=0.01, l2=0.01)
批量归一化
在第一章,数据科学与数据预处理简介中,我们学习了如何进行归一化,以及它如何帮助加速我们机器学习模型的训练。在这里,我们将对神经网络的每一层应用相同的归一化方法。批量归一化允许各层独立学习,而不受其他层的影响。它通过将层的输入标准化,使其具有固定的均值和方差来实现这一点;这可以防止前一层的参数变化对当前层的输入产生过大影响。它还有一定的正则化作用;类似于 dropout,它防止过拟合,但它是通过在小批量的值中引入噪声来实现的。在使用批量归一化时,请确保使用较低的 dropout,这样更好,因为 dropout 会导致信息丢失。然而,不要完全依赖批量归一化而去除 dropout,因为两者结合使用效果更好。使用批量归一化时,可以使用较高的学习率,因为它确保了不会有动作过大或过小。
图 6.17:批量归一化方程
这里,(xi) 是层的输入,y 是标准化后的输入。μ 是批量均值,σ2 是批量的标准差。批量归一化引入了两个新的(x_i)̂损失。
要在 Keras 中创建批量归一化层,您可以使用以下函数:
keras.layers.BatchNormalization()
练习 52:使用正则化改进图像分类,使用 CIFAR-10 图像
在本次练习中,我们将对加拿大高级研究院(CIFAR-10)数据集进行分类。该数据集包含 60,000 张 32 x 32 的彩色图像,分为 10 类。这 10 类分别是:鸟类、飞机、猫、汽车、青蛙、鹿、狗、卡车、船和马。它是机器学习研究中最广泛使用的数据集之一,主要用于卷积神经网络(CNN)领域。由于图像的分辨率较低,模型可以在这些图像上更快速地训练。我们将使用该数据集实现我们在上一节中学到的一些正则化技术:
注
若要获取原始 CIFAR-10 文件和 CIFAR-100 数据集,请访问 www.cs.toronto.edu/~kriz/cifar…
-
使用 Keras 库加载 CIFAR-10 数据集:
from keras.layers import Dense, Conv2D, MaxPool2D, Flatten, Dropout, BatchNormalization from keras.datasets import cifar10 (x_train, y_train), (x_test, y_test) = cifar10.load_data() -
检查数据的维度:
x_train.shape输出结果如下:
图 6.18:x 的维度
相似的维度,针对
y:y_train.shape输出结果如下:
图 6.19:y 的维度
由于这些是彩色图像,它们有三个通道。
-
将数据转换为 Keras 所需的格式:
x_train = x_train.reshape(-1, 32, 32, 3) x_test = x_test.reshape(-1, 32, 32, 3) -
将目标变量转换为 one-hot 向量,以确保网络在不同的目标变量之间不会形成不必要的连接:
from sklearn.preprocessing import LabelBinarizer label_binarizer = LabelBinarizer() label_binarizer.fit(range(10)) y_train = label_binarizer.transform(y_train) y_test = label_binarizer.transform(y_test) -
创建模型。在这里,我们首先创建一个不带正则化的小型 CNN:
from keras.models import Sequential model = Sequential()添加卷积层:
model.add(Conv2D(32, (3, 3), activation='relu', padding='same', input_shape=(32,32,3))) model.add(Conv2D(32, (3, 3), activation='relu'))添加池化层:
model.add(MaxPool2D(pool_size=(2, 2))) -
将 2D 矩阵展平为 1D 向量:
model.add(Flatten()) -
使用密集层作为模型的最终层并编译模型:
model.add(Dense(512, activation='relu')) model.add(Dense(10, activation='softmax')) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics = [‘acc']) -
训练模型并检查最终准确度:
model.fit(x_train, y_train, validation_data = (x_test, y_test), epochs=10, batch_size=512) -
现在检查模型的准确度:
score = model.evaluate(x_test, y_test) print(“Accuracy: {0:.2f}%”.format(score[1]*100))输出结果如下:
图 6.20:模型的准确度
-
现在创建相同的模型,但加入正则化。你也可以尝试其他架构:
model = Sequential()添加卷积层:
model.add(Conv2D(32, (3, 3), activation='relu', padding='same', input_shape=(32,32,3))) model.add(Conv2D(32, (3, 3), activation='relu'))添加池化层:
model.add(MaxPool2D(pool_size=(2, 2))) -
添加批量归一化层和 Dropout 层:
model.add(BatchNormalization()) model.add(Dropout(0.10)) -
将 2D 矩阵展平为 1D 向量:
model.add(Flatten()) -
使用密集层作为模型的最终层并编译模型:
model.add(Dense(512, activation='relu')) model.add(Dropout(0.5)) model.add(Dense(10, activation='softmax')) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics = [‘acc']) model.summary()图 6.21:带正则化的 CNN 架构
-
训练模型并检查最终准确度:
model.fit(x_train, y_train, validation_data = (x_test, y_test), epochs=10, batch_size=512) score = model.evaluate(x_test, y_test) print(“Accuracy: {0:.2f}%”.format(score[1]*100))输出结果如下:
图 6.22:最终准确度输出
恭喜!你通过使用正则化使得模型比以前表现得更好。如果你的模型没有看到改善,尝试将训练时间延长,增加更多的训练轮数。你也会发现,可以训练更多的轮次而不必担心过拟合。
你可以绘制不同的测试图像,并使用练习 50 中给出的代码查看网络的结果。同时,绘制错误预测,看看模型哪里出错:
import numpy as np
import matplotlib.pyplot as plt
y_pred = model.predict(x_test)
incorrect_indices = np.nonzero(np.argmax(y_pred,axis=1) != np.argmax(y_test,axis=1))[0]
labels = [‘airplane', ‘automobile', ‘bird', ‘cat', ‘deer', ‘dog', ‘frog', ‘horse', ‘ship', ‘truck']
image = 3
plt.imshow(x_test[incorrect_indices[image]].reshape(32,32,3))
plt.show()
print(“Prediction: {0}”.format(labels[np.argmax(y_pred[incorrect_indices[image]])]))
图 6.23:模型的错误预测
如你所见,模型在预测模糊图像时遇到了困难。真实标签是马。你可以尝试调整层和超参数,看看是否能提高准确率。尝试创建更复杂的模型并进行正则化,训练更长时间。
图像数据预处理
本节将介绍数据科学家可以用来预处理图像的一些技术。首先,我们将介绍图像归一化,然后学习如何将彩色图像转换为灰度图像。最后,我们将探讨如何将数据集中的所有图像调整为相同尺寸。预处理图像是必要的,因为数据集中的图像大小不同,我们需要将它们转换为标准大小,以便在其上训练机器学习模型。一些图像预处理技术通过简化模型识别重要特征或通过减少维度(如灰度图像的情况)来帮助减少模型的训练时间。
归一化
对于图像而言,像素的规模在同一量级,范围是 0 到 255。因此,这一步归一化是可选的,但它可能有助于加速学习过程。再重申一下,数据中心化并将其缩放到相同的量级,有助于确保梯度不会失控。神经网络共享参数(神经元)。如果输入数据没有缩放到相同的量级,那么网络学习将变得困难。
转换为灰度图像
根据数据集和问题的不同,你可以将图像从 RGB 转换为灰度图像。这有助于网络更快地工作,因为它需要学习的参数要少得多。根据问题类型,你可能不希望这样做,因为这会导致丢失图像颜色所提供的信息。要将 RGB 图像转换为灰度图像,可以使用Pillow库:
from PIL import Image
image = Image.open(‘rgb.png').convert(‘LA')
image.save(‘greyscale.png')
图 6.24:转换为灰度的汽车图像
将所有图像调整为相同大小
在处理现实生活中的数据集时,你会经常遇到一个主要的挑战,那就是数据集中的所有图像大小可能不相同。你可以根据情况执行以下步骤来解决这个问题:
resize函数是用于获取调整大小后新像素的算法。双三次插值算法速度较快,是上采样时最好的像素重采样算法之一。
图 6.25:上采样的汽车图像
resize函数是用于获取调整大小后新像素的算法,如前所述。抗锯齿算法有助于平滑像素化的图像。它比双三次插值算法效果更好,但速度较慢。抗锯齿是最适合下采样的像素重采样算法之一。
图 6.26:下采样的汽车图像
-
裁剪:将所有图像裁剪为相同大小的另一种方法是裁剪它们。如前所述,可以使用不同的中心来防止信息丢失。你可以使用以下代码裁剪图像:
area = (1000, 500, 2500, 2000) cropped_img = img.crop(area)
图 6.27:裁剪后的汽车图像
-
填充:填充是指在图像周围添加一层零或一的边界,以增加图像的大小。执行填充时,请使用以下代码:
size = (2000,2000) back = Image.new(“RGB”, size, “white”) offset = (250, 250) back.paste(cropped_img, offset)
图 6.28:裁剪后的填充汽车图像
其他有用的图像操作
Pillow 库提供了许多用于修改和创建新图像的功能。这些功能将帮助我们从现有的训练数据中创建新图像。
要翻转图像,可以使用以下代码:
img.transpose(Image.FLIP_LEFT_RIGHT)
图 6.29:翻转后的裁剪汽车图像
要将图像旋转 45 度,可以使用以下代码:
img.rotate(45)
图 6.30:旋转 45 度后的裁剪汽车图像
要将图像平移 1,000 像素,可以使用以下代码:
import PIL
width, height = img.size
image = PIL.ImageChops.offset(img, 1000, 0)
image.paste((0), (0, 0, 1000, height))
图 6.31:旋转后的裁剪汽车图像
活动 17:预测图像是猫还是狗
在本活动中,我们将尝试预测提供的图像是猫还是狗。微软提供的猫狗数据集(github.com/TrainingByP… 25,000 张猫和狗的彩色图像。假设你在一家兽医诊所工作,诊所里有两位兽医,一位专门治疗狗,另一位专门治疗猫。你希望通过判断下一位客户是狗还是猫,来自动安排兽医的预约。为此,你创建了一个 CNN 模型:
-
加载狗与猫数据集并预处理图像。
-
使用图像文件名找到每个图像的猫或狗标签。第一张图像应该是这样的:
图 6.32:狗与猫类别的第一张图像
-
获取形状正确的图像以进行训练。
-
创建一个使用正则化的 CNN。
注意
本活动的解决方案可以在第 369 页找到。
你应该发现该模型的测试集准确率为 70.4%。训练集准确率非常高,约为 96%。这意味着模型已经开始出现过拟合。改进模型以获得最佳准确率的任务留给你作为练习。你可以使用前面练习中的代码绘制错误预测的图像,从而了解模型的表现:
import matplotlib.pyplot as plt
y_pred = model.predict(x_test)
incorrect_indices = np.nonzero(np.argmax(y_pred,axis=1) != np.argmax(y_test,axis=1))[0]
labels = [‘dog', ‘cat']
image = 5
plt.imshow(x_test[incorrect_indices[image]].reshape(50,50), cmap=plt.get_cmap(‘gray'))
plt.show()
print(“Prediction: {0}”.format(labels[np.argmax(y_pred[incorrect_indices[image]])]))
图 6.33:常规 CNN 模型错误地预测为狗
数据增强
在训练机器学习模型时,我们数据科学家经常遇到类别不平衡和训练数据不足的问题。这导致模型性能不佳,在实际应用中表现差强人意。应对这些问题的一种简单方法是数据增强。数据增强有多种方式,例如旋转图像、平移物体、裁剪图像、剪切扭曲图像、放大图像的某部分,以及更复杂的方法,如使用生成对抗网络(GANs)生成新图像。GAN 只是两个相互竞争的神经网络。生成器网络试图生成与已有图像相似的图像,而判别器网络则尝试判断图像是生成的还是原始数据的一部分。训练完成后,生成器网络能够创造出并非原始数据的一部分,但与真实拍摄的图像相似,几乎可以误认为是摄像机拍摄的图像。
注意
你可以在这篇论文中了解更多关于 GAN 的信息:arxiv.org/abs/1406.26…
图 6.34:左侧是一个由生成对抗网络(GAN)生成的假图像,而右侧是一个真实人物的图像
注意
回到传统的图像增强方法,我们执行之前提到的操作,如翻转图像,然后在原始图像和变换后的图像上训练我们的模型。假设我们有以下左侧的翻转猫图像:
图 6.35:右侧是猫的正常图像,左侧是翻转后的图像
现在,一个在左侧图像上训练的机器学习模型可能会很难将右侧翻转后的图像识别为猫的图像,因为它朝向相反。这是因为卷积层被训练成只检测朝左看的猫图像。它已经对身体的不同特征位置建立了规则。
因此,我们在所有增强后的图像上训练我们的模型。数据增强是获得 CNN 模型最佳结果的关键。我们利用 Keras 中的ImageDataGenerator类轻松执行图像增强。你将在下一节中了解更多关于生成器的内容。
生成器
在上一章中,我们讨论了大数据集如何由于 RAM 的限制而导致训练问题。当处理图像时,这个问题会更严重。Keras 实现了生成器,帮助我们在训练时动态获取输入图像及其相应标签。这些生成器还帮助我们在训练前对图像进行数据增强。首先,我们将看看如何利用ImageDataGenerator类为我们的模型生成增强后的图像。
为了实现数据增强,我们只需要稍微修改我们的练习 3 代码。我们将用以下代码替代model.fit():
BATCH_SIZE = 32
aug = ImageDataGenerator(rotation_range=20,
width_shift_range=0.2, height_shift_range=0.2,
shear_range=0.15, zoom_range=0.15,
horizontal_flip=True, vertical_flip=True,
fill_mode=”nearest”)
log = model.fit_generator(
aug.flow(x_train, y_train, batch_size= BATCH_SIZE),
validation_data=( x_test, y_test), steps_per_epoch=len(x_train) // BATCH_SIZE, epochs=10)
现在我们来看一下ImageDataGenerator实际在做什么:
-
rotation_range:此参数定义图像可以旋转的最大角度。旋转是随机的,可以小于该值的任何数值。这确保了没有两张图像是相同的。 -
width_shift_range/height_shift_range:该值定义了图像可以移动的范围。如果值小于 1,则认为它是总宽度的一个比例;如果大于 1,则表示像素数。范围将在(-shift_range,+ shift_range)区间内。 -
shear_range:这是剪切角度,单位为度(逆时针方向)。 -
zoom_range:这里的值可以是[lower_range,upper_range],或者是浮动值,表示[1-zoom_range,1+zoom_range],这是随机缩放的范围。 -
horizontal_flip/vertical_flip:此处的布尔值为真时,生成器会随机水平或垂直翻转图像。 -
fill_mode:这帮助我们决定在旋转和剪切过程中产生的空白区域应填充什么内容。constant:此选项将用常数值填充空白区域,常数值需要通过cval参数定义。nearest:这会用最近的像素填充空白区域。reflect:这会产生反射效果,就像镜子一样。wrap:这会使图像环绕并填充空白区域。
生成器会随机应用前述操作到它遇到的所有图像上。这确保了模型不会看到相同的图像两次,从而减轻过拟合问题。在使用生成器时,我们需要使用fit_generator()函数,而不是fit()函数。我们根据训练时可用的内存大小,向生成器传递合适的批处理大小。
默认的 Keras 生成器有一些内存开销;为了去除这些开销,你可以创建自己的生成器。为此,你需要确保实现生成器的以下四个部分:
-
读取输入图像(或任何其他数据)。
-
读取或生成标签。
-
对图像进行预处理或增强。
注意
确保随机增强图像。
-
以 Keras 所期望的形式生成输出。
这里提供了一个示例代码,帮助你创建自己的生成器:
def custom_image_generator(images, labels, batch_size = 128):
while True:
# Randomly select images for the batch batch_images = np.random.choice(images,
size = batch_size) batch_input = [] batch_output = []
# Read image, perform preprocessing and get labels
for image in batch_images:
# Function that reads and returns the image
input = get_input(image)
# Function that gets the label of the image
output = get_output(image,labels =labels)
# Function that pre-processes and augments the image
input = preprocess_image(input)
batch_input += [input] batch_output += [output]
batch_x = np.array( batch_input ) batch_y = np.array( batch_output )
# Return a tuple of (images,labels) to feed the network yield(batch_x, batch_y)
实现 get_input、get_output 和 preprocess_image 被留作练习。
练习 53:使用图像增强对 CIFAR-10 图像进行分类
在本练习中,我们将对 CIFAR-10(加拿大高级研究院)数据集进行分类,类似于练习 52。在这里,我们将使用生成器来增强训练数据。我们将随机旋转、平移和翻转图像:
-
使用 Keras 库加载 CIFAR-10 数据集:
from keras.datasets import cifar10 (x_train, y_train), (x_test, y_test) = cifar10.load_data() -
将数据转换为 Keras 所需的格式:
x_train = x_train.reshape(-1, 32, 32, 3) x_test = x_test.reshape(-1, 32, 32, 3) -
将目标变量转换为 one-hot 向量,以便我们的网络不会在不同的目标变量之间形成不必要的连接:
from sklearn.preprocessing import LabelBinarizer label_binarizer = LabelBinarizer() label_binarizer.fit(range(10)) y_train = label_binarizer.transform(y_train) y_test = label_binarizer.transform(y_test) -
创建模型。我们将使用练习 3中的网络:
from keras.models import Sequential model = Sequential()添加卷积层:
from keras.layers import Dense, Dropout, Conv2D, MaxPool2D, Flatten, BatchNormalization model.add(Conv2D(32, (3, 3), activation='relu', padding='same', input_shape=(32,32,3))) model.add(Conv2D(32, (3, 3), activation='relu'))添加池化层:
model.add(MaxPool2D(pool_size=(2, 2)))添加批归一化层,并附加一个丢弃层:
model.add(BatchNormalization()) model.add(Dropout(0.10)) -
将 2D 矩阵展平为 1D 向量:
model.add(Flatten()) -
使用全连接层作为模型的最后一层:
model.add(Dense(512, activation='relu')) model.add(Dropout(0.5)) model.add(Dense(10, activation='softmax')) -
使用以下代码编译模型:
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics = [‘acc']) -
创建数据生成器并传递所需的增强方式:
from keras.preprocessing.image import ImageDataGenerator datagen = ImageDataGenerator( rotation_range=45, width_shift_range=0.2, height_shift_range=0.2, horizontal_flip=True) -
训练模型:
BATCH_SIZE = 128 model_details = model.flow(datagen.flow(x_train, y_train, batch_size = BATCH_SIZE), steps_per_epoch = len(x_train) // BATCH_SIZE, epochs = 10, validation_data= (x_test, y_test), verbose=1) -
检查模型的最终准确度:
score = model.evaluate(x_test, y_test) print(“Accuracy: {0:.2f}%”.format(score[1]*100))输出如下所示:
图 6.36:模型准确度输出
恭喜!你已经使用数据增强让你的模型识别更广泛的图像。你一定注意到模型的准确率下降了。这是因为我们训练模型的 epochs 数量较少。使用数据增强的模型需要更多的 epochs 来训练。你还会看到,即使训练更多 epochs 也不用担心过拟合。这是因为每个 epoch,模型看到的数据都是新图像,数据集中的图像很少重复,甚至几乎不重复。如果你训练更多 epochs,一定会看到进展。试着尝试更多的架构和增强方式。
这里你可以看到一个错误分类的图像。通过检查错误识别的图像,你可以评估模型的表现,并找出其表现不佳的地方。
y_pred = model.predict(x_test)
incorrect_indices = np.nonzero(np.argmax(y_pred,axis=1) != np.argmax(y_test,axis=1))[0]
labels = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']
image = 2
plt.imshow(x_test[incorrect_indices[image]].reshape(32,32,3))
plt.show()
print("Prediction: {0}".format(labels[np.argmax(y_pred[incorrect_indices[image]])]))
请查看以下截图以检查错误预测:
图 6.37:基于增强数据训练的 CNN 模型的错误预测
活动 18:识别和增强图像
在本活动中,我们将尝试预测图像是猫还是狗,类似于活动 17。不过这次我们将使用生成器来处理图像,并对它们进行数据增强,以获得更好的结果:
-
创建函数以获取每个图像和每个图像标签。然后,创建一个函数来预处理加载的图像并对其进行增强。最后,创建一个数据生成器(如生成器部分所示),利用上述函数在训练期间将数据传递给 Keras。
-
加载未增强的测试数据集。使用活动 17中的函数。
-
创建一个 CNN 模型,用于识别给定的图像是猫还是狗。确保使用正则化。
注意
本活动的解决方案可以在第 373 页找到。
你应该会发现该模型的测试集准确度大约为 72%,相比活动 17中的模型有所提升。你还会观察到训练集的准确度非常高,约为 98%。这意味着该模型开始出现过拟合,就像活动 17中的模型一样。这可能是由于数据增强不足造成的。尝试更改数据增强参数,看看准确度是否有所变化。或者,你可以修改神经网络的架构,以获得更好的结果。你可以绘制出错误预测的图像,了解模型的表现如何。
import matplotlib.pyplot as plt
y_pred = model.predict(validation_data[0])
incorrect_indices = np.nonzero(np.argmax(y_pred,axis=1) != np.argmax(validation_data[1],axis=1))[0]
labels = ['dog', 'cat']
image = 7
plt.imshow(validation_data[0][incorrect_indices[image]].reshape(50,50), cmap=plt.get_cmap('gray'))
plt.show()
print("Prediction: {0}".format(labels[np.argmax(y_pred[incorrect_indices[image]])]))
下图展示了一个例子:
图 6.38:数据增强 CNN 模型错误预测为猫
总结
本章中,我们学习了数字图像是什么以及如何使用它们创建机器学习模型。然后,我们讲解了如何使用 Keras 库训练图像的神经网络模型。我们还介绍了什么是正则化,如何在神经网络中使用正则化,什么是图像增强,以及如何使用它。我们探讨了 CNN 是什么以及如何实现 CNN。最后,我们讨论了各种图像预处理技术。
现在你已经完成了本章内容,你将能够处理任何类型的数据来创建机器学习模型。在下一章中,我们将学习如何处理人类语言。
第十七章:第七章
处理人类语言
学习目标
到本章结束时,您将能够:
-
为文本数据创建机器学习模型
-
使用 NLTK 库对文本进行预处理
-
使用正则表达式清洗和分析字符串
-
使用 Word2Vec 模型创建词向量
本章将介绍处理人类语言的相关概念。
介绍
人工智能(AI)最重要的目标之一是理解人类语言,以执行任务。拼写检查、情感分析、问答、聊天机器人和虚拟助手(如 Siri 和 Google 助手)都具有自然语言处理(NLP)模块。NLP 模块使虚拟助手能够处理人类语言并根据语言执行相应的操作。例如,当我们说“OK Google,设定一个早上 7 点的闹钟”时,语音首先被转换为文本,然后由 NLP 模块处理。处理完毕后,虚拟助手会调用闹钟/时钟应用程序的适当 API。处理人类语言有其自身的挑战,因为语言具有歧义性,词汇的意义取决于其所处的上下文。这是 AI 语言处理中的最大痛点。
另一个重要原因是完整信息的缺乏。在交流时,我们往往省略大部分信息;这些信息通常是常识,或者是普遍真实或错误的事情。例如,句子“I saw a man on a hill with a telescope”根据上下文信息的不同,可以有不同的含义。例如,它可能意味着“我看到一个手持望远镜的男人站在山上”,也可能意味着“我通过望远镜看到一个站在山上的男人”。计算机很难跟踪这些信息,因为大部分是上下文性的。由于深度学习的进步,今天的自然语言处理(NLP)比我们以前使用传统方法(如聚类和线性模型)时更为有效。这也是我们将使用深度学习来处理文本语料库解决 NLP 问题的原因。像其他任何机器学习问题一样,NLP 也有两个主要部分:数据处理和模型创建。在接下来的内容中,我们将学习如何处理文本数据,随后我们将学习如何使用这些处理过的数据来创建机器学习模型,以解决我们的实际问题。
文本数据处理
在我们开始为文本数据构建机器学习模型之前,我们需要对数据进行处理。首先,我们将学习不同的方法来理解数据的组成。这有助于我们了解数据的真正内容,并决定下一步要使用的预处理技术。接下来,我们将学习有助于预处理数据的技术。这一步有助于减少数据的大小,从而缩短训练时间,并帮助我们将数据转换为机器学习算法更易于提取信息的形式。最后,我们将学习如何将文本数据转换为数字,以便机器学习算法可以实际使用这些数据来创建模型。我们通过词嵌入来实现这一点,就像我们在第五章:“掌握结构化数据”中进行的实体嵌入一样。
正则表达式
在我们开始处理文本数据之前,我们需要了解正则表达式(RegEx)。正则表达式并不真正属于预处理技术,而是一串定义字符串中搜索模式的字符。正则表达式是处理文本数据时的强大工具,它帮助我们在文本集合中查找特定的序列。正则表达式由元字符和普通字符组成。
图 7.1:包含在正则表达式中使用的元字符的表格,以及一些示例
使用正则表达式,我们可以在文本中搜索复杂的模式。例如,我们可以用它来从文本中删除 URL。我们可以使用 Python 的re模块删除 URL,如下所示:
re.sub(r"https?\://\S+\s", '', "https://www.asfd.com hello world")
re.sub接受三个参数:第一个是正则表达式,第二个是你想要替换匹配模式的表达式,第三个是它应该搜索该模式的文本。
命令的输出如下:
图 7.2:输出命令
注意
记住所有正则表达式的约定很困难,因此在使用正则表达式时,参考备忘单是个不错的选择,例如: (www.pyregex.com/)。
练习 54:使用正则表达式进行字符串清理
在这个练习中,我们将使用 Python 的re模块来修改和分析字符串。在本练习中,我们将简单地学习如何使用正则表达式,接下来的部分我们将展示如何使用正则表达式来预处理数据。我们将使用 IMDB 电影评论数据集中的一条评论(github.com/TrainingByP…
-
在这个练习中,我们将使用来自 IMDB 的电影评论。将评论文本保存到一个变量中,如以下代码所示。你也可以使用任何其他段落的文本进行这个练习:
string = "first think another Disney movie, might good, it's kids movie. watch it, can't help enjoy it. ages love movie. first saw movie 10 8 years later still love it! Danny Glover superb could play part better. Christopher Lloyd hilarious perfect part. Tony Danza believable Mel Clark. can't help, enjoy movie! give 10/10!<br /><br />- review Jamie Robert Ward (http://www.invocus.net)" -
计算评论的长度,以确定我们需要减少多大的大小。我们将使用
len(string)并获得输出,如以下代码所示:len(string)输出长度如下:
图 7.3:字符串长度
-
有时,当你从网站抓取数据时,超链接也会被记录下来。大多数情况下,超链接不会为我们提供任何有用信息。通过使用复杂的正则表达式字符串(如"
https?\://\S+"),删除数据中的任何超链接。这将选择任何包含https://的子字符串:import re string = re.sub(r"https?\://\S+", '', string) string去除超链接后的字符串如下:
图 7.4:去除超链接后的字符串
-
接下来,我们将从文本中删除
brHTML 标签,这些标签是我们在读取字符串时观察到的。有时,这些 HTML 标签会被添加到抓取的数据中:string = re.sub(r'<br />', ' ', string) string去除
br标签后的字符串如下:图 7.5:去除 br 标签后的字符串
-
现在,我们将从文本中删除所有数字。当数字对我们没有意义时,这有助于减少数据集的大小:
string = re.sub('\d','', string) string去除数字后的字符串如下所示:
图 7.6:去除数字后的字符串
-
接下来,我们将删除所有特殊字符和标点符号。根据你的问题,这些可能只是占用空间,且不会为机器学习算法提供相关信息。所以,我们使用以下正则表达式模式将它们移除:
string = re.sub(r'[_"\-;%()|+&=*%.,!?:#$@\[\]/]', '', string) string去除特殊字符和标点符号后的字符串如下所示:
图 7.7:没有特殊字符的字符串
-
现在,我们将把
can't替换为cannot,并将it's替换为it is。这有助于减少训练时间,因为唯一单词的数量减少了:string = re.sub(r"can\'t", "cannot", string) string = re.sub(r"it\'s", "it is", string) string最终的字符串如下:
图 7.8:最终字符串
-
最后,我们将计算清理后字符串的长度:
len(string)字符串的输出大小如下:
图 7.9:清理后的字符串长度
我们将评论的大小减少了 14%。
-
现在,我们将使用正则表达式分析数据,并获取所有以大写字母开头的单词:
注意
re.findall(r"[A-Z][a-z]*", string)单词如下:
图 7.10:以大写字母开头的单词
-
要在文本中查找所有一字母和二字母的单词,请使用以下方法:
re.findall(r"\b[A-z]{1,2}\b", string)输出如下:
图 7.11:一字母和二字母单词
恭喜你!你已经成功使用 re 模块和正则表达式修改并分析了评论字符串。
基本特征提取
基本特征提取帮助我们了解数据的组成。这有助于我们选择进行数据预处理的步骤。基本特征提取包括计算平均词数和特殊字符计数等操作。我们将在本节中使用 IMDB 电影评论数据集作为示例:
data = pd.read_csv('movie_reviews.csv', encoding='latin-1')
让我们看看数据集包含了什么:
data.iloc[0]
输出如下:
图 7.12:SentimentText 数据
SentimentText 变量包含实际评论,Sentiment 变量包含评论的情绪。1 表示正面情绪,0 表示负面情绪。让我们打印出第一条评论,以便了解我们处理的数据:
data.SentimentText[0]
第一条评论如下:
图 7.13:第一条评论
现在,我们将尝试通过获取数据集的关键统计数据来了解我们正在处理的数据类型。
词数
我们可以使用以下代码获取每条评论中的词数:
data['word_count'] = data['SentimentText'].apply(lambda x: len(str(x).split(" ")))
现在,DataFrame 中的 word_count 变量包含评论中的总词数。apply 函数会将 split 函数逐行应用于数据集。现在,我们可以获取每一类评论的平均词数,看看正面评论是否比负面评论有更多的词汇。
mean() 函数计算 pandas 中某列的平均值。对于负面评论,使用以下代码:
data.loc[data.Sentiment == 0, 'word_count'].mean()
负面情绪的平均词数如下:
图 7.14:负面情绪的总词数
对于正面评论,使用以下代码:
data.loc[data.Sentiment == 1, 'word_count'].mean()
正面情绪的平均词数如下:
图 7.15:正面情绪的总词数
我们可以看到,正负情绪的平均词数差异不大。
停用词
停用词是语言中最常见的词汇——例如,“I”、“me”、“my”、“yours”和“the”。大多数时候,这些词汇不会提供句子的实际信息,因此我们将它们从数据集中移除,以减少数据的大小。nltk 库提供了一个可以访问的英文停用词列表。
from nltk.corpus import stopwords
stop = stopwords.words('english')
为了获取这些停用词的计数,我们可以使用以下代码:
data['stop_count'] = data['SentimentText'].apply(lambda x: len([x for x in x.split() if x in stop]))
然后,我们可以使用以下代码查看每个类别的平均停用词数量:
data.loc[data.Sentiment == 0, 'stop_count'].mean()
负面情绪的平均停用词数量如下:
图 7.16:负面情绪的平均停用词数
现在,为了获取正面情绪的停用词数量,我们使用以下代码:
data.loc[data.Sentiment == 1, 'stop_count'].mean()
这里显示的是正面情感的停用词平均数量:
图 7.17:正面情感的停用词平均数量
特殊字符数量
根据你处理的问题类型,你可能需要保留诸如 @、#、$ 和 * 等特殊字符,或者将它们移除。要做到这一点,你首先需要弄清楚数据集中出现了多少特殊字符。要获取数据集中 ^、&、*、$、@ 和 # 的数量,可以使用以下代码:
data['special_count'] = data['SentimentText'].apply(lambda x: len(re.sub('[^\^&*$@#]+' ,'', x)))
文本预处理
现在我们已经知道了数据的组成部分,我们需要对其进行预处理,以便机器学习算法能够轻松地在文本中找到模式。在本节中,我们将介绍一些用于清理和减少我们输入机器学习算法的数据维度的技术。
小写化
我们执行的第一个预处理步骤是将所有数据转换为小写字母。这可以防止出现同一个词的多个副本。你可以使用以下代码轻松将所有文本转换为小写:
data['SentimentText'] = data['SentimentText'].apply(lambda x: " ".join(x.lower() for x in x.split()))
apply 函数会将 lower 函数迭代地应用于数据集的每一行。
停用词移除
如前所述,停用词应从数据集中移除,因为它们提供的信息非常有限。停用词不会影响句子的情感。我们执行此步骤是为了去除停用词可能引入的偏差:
data['SentimentText'] = data['SentimentText'].apply(lambda x: " ".join(x for x in x.split() if x not in stop))
常见词移除
停用词是一些常见的词汇,如 'a'、'an' 和 'the'。然而,在这一步,你将移除数据集中最常见的词。例如,可以从推文数据集中移除的词包括 RT、@username 和 DM。首先,找出最常见的词汇:
word_freq = pd.Series(' '.join(data['SentimentText']).split()).value_counts()
word_freq.head()
最常见的词汇是:
图 7.18:推文数据集中最常见的词汇
从输出结果中,我们得到一个提示:文本包含 HTML 标签,这些标签可以去除,从而大大减少数据集的大小。因此,我们首先去除所有 <br /> HTML 标签,然后去除诸如 'movie' 和 'film' 这样的词,这些词对情感分析器的影响不大:
data['SentimentText'] = data['SentimentText'].str.replace(r'<br />','')
data['SentimentText'] = data['SentimentText'].apply(lambda x: " ".join(x for x in x.split() if x not in ['movie', 'film']))
标点符号和特殊字符移除
接下来,我们从文本中移除所有标点符号和特殊字符,因为它们对文本提供的信息很少。要移除标点符号和特殊字符,可以使用以下正则表达式:
punc_special = r"[^A-Za-z0-9\s]+"
data['SentimentText'] = data['SentimentText'].str.replace(punc_special,'')
正则表达式选择所有字母数字字符和空格。
拼写检查
有时,同一个词的拼写错误会导致我们拥有相同词汇的多个副本。通过使用自动纠正库进行拼写检查,可以纠正这种问题:
from autocorrect import spell
data['SentimentText'] = [' '.join([spell(i) for i in x.split()]) for x in data['SentimentText']]
词干提取
nltk 库:
from nltk.stem import PorterStemmer
stemmer = PorterStemmer()
data['SentimentText'] = data['SentimentText'].apply(lambda x: " ".join([stemmer.stem(word) for word in x.split()]))
注意
拼写检查、词干提取和词形还原的处理时间可能会很长,具体取决于数据集的大小,因此,在执行这些步骤之前,请先检查数据集,确保需要进行这些操作。
词形还原
提示
你应该更倾向于使用词形还原(lemmatization)而非词干提取(stemming),因为它更为有效。
nltk 库:
lemmatizer = nltk.stem.WordNetLemmatizer()
data['SentimentText'][:5].apply(lambda x: " ".join([lemmatizer.lemmatize(word) for word in x.split()]))
注意
我们正在减少数据集的维度,原因是“维度灾难”。随着维度(因变量)增加,数据集会变得稀疏。这会导致数据科学技术失效,因为很难对高维特征进行建模以得到正确的输出。随着数据集特征数量的增加,我们需要更多的数据点来进行建模。因此,为了克服高维数据的灾难,我们需要获取更多的数据,这样会增加处理这些数据所需的时间。
分词
nltk 库:
import nltk
nltk.word_tokenize("Hello Dr. Ajay. It's nice to meet you.")
标记后的列表如下:
图 7.19:分词后的列表
如你所见,它将标点符号与单词分开,并检测像“Dr.”这样的复杂词语。
练习 55:预处理 IMDB 电影评论数据集
在这个练习中,我们将预处理 IMDB 电影评论数据集,使其适用于任何机器学习算法。该数据集包含 25,000 条电影评论以及评论的情感(正面或负面)。我们希望通过评论预测情感,因此在进行预处理时需要考虑这一点。
-
使用 pandas 加载 IMDB 电影评论数据集:
import pandas as pd data = pd.read_csv('../../chapter 7/data/movie_reviews.csv', encoding='latin-1') -
首先,我们将数据集中的所有字符转换为小写字母:
data.SentimentText = data.SentimentText.str.lower() -
接下来,我们将编写一个
clean_str函数,在其中使用re模块清理评论:import re def clean_str(string): string = re.sub(r"https?\://\S+", '', string) string = re.sub(r'\<a href', ' ', string) string = re.sub(r'&', 'and', string) string = re.sub(r'<br />', ' ', string) string = re.sub(r'[_"\-;%()|+&=*%.,!?:#$@\[\]/]', ' ', string) string = re.sub('\d','', string) string = re.sub(r"can\'t", "cannot", string) string = re.sub(r"it\'s", "it is", string) return string注意
data.SentimentText = data.SentimentText.apply(lambda x: clean_str(str(x)))使用 pandas 的 apply 函数对整个数据集进行评论清理。
-
接下来,使用以下代码检查数据集中的词语分布:
pd.Series(' '.join(data['SentimentText']).split()).value_counts().head(10)排名前 10 的词汇出现频率如下:
图 7.20:排名前 10 的词语
-
从评论中移除停用词:
注意
这将通过首先对评论进行分词,然后移除从
nltk库加载的停用词来完成。 -
我们将'
movie'、'film' 和 'time' 加入停用词列表,因为它们在评论中出现频率很高,且对理解评论情感没有太大帮助:from nltk.corpus import stopwords from nltk.tokenize import word_tokenize,sent_tokenize stop_words = stopwords.words('english') + ['movie', 'film', 'time'] stop_words = set(stop_words) remove_stop_words = lambda r: [[word for word in word_tokenize(sente) if word not in stop_words] for sente in sent_tokenize(r)] data['SentimentText'] = data['SentimentText'].apply(remove_stop_words) -
接下来,我们将分词后的内容转回为句子,并去除那些全部由停用词组成的评论:
def combine_text(text): try: return ' '.join(text[0]) except: return np.nan data.SentimentText = data.SentimentText.apply(lambda x: combine_text(x)) data = data.dropna(how='any') -
下一步是将文本转换为词汇表中的标记,再转换为数字。我们将使用 Keras Tokenizer,因为它能同时完成这两个步骤:
from keras.preprocessing.text import Tokenizer tokenizer = Tokenizer(num_words=250) tokenizer.fit_on_texts(list(data['SentimentText'])) sequences = tokenizer.texts_to_sequences(data['SentimentText']) -
要获取词汇表的大小,使用以下代码:
word_index = tokenizer.word_index print('Found %s unique tokens.' % len(word_index))唯一标记的数量如下:
图 7.21:唯一词汇的数量
-
为了减少模型的训练时间,我们将把评论的长度限制在 200 个单词以内。你可以调整这个数字,以找到最适合的准确率。
注意
from keras.preprocessing.sequence import pad_sequences reviews = pad_sequences(sequences, maxlen=200) -
你应该保存分词器,以便在之后将评论转回为文本:
import pickle with open('tokenizer.pkl', 'wb') as handle: pickle.dump(tokenizer, handle, protocol=pickle.HIGHEST_PROTOCOL)要预览清理过的评论,运行以下命令:
data.SentimentText[124]一个清理过的评论如下所示:
图 7.22:已清理的评论
要获取下一步骤的实际输入,请运行以下命令:
reviews[124]
reviews 命令的下一步骤输入大致如下所示:
图 7.23:下一步骤的输入,已清理的评论
恭喜!你已经成功预处理了你的第一个文本数据集。评论数据现在是一个包含 25,000 行(即评论)和 200 列(即单词)的矩阵。接下来,我们将学习如何将这些数据转换为嵌入,以便更容易预测情感。
文本处理
现在我们已经清理好了数据集,将其转化为机器学习模型可以使用的形式。回顾一下第五章,结构化数据的掌握,我们讨论了神经网络无法处理单词,因此我们需要将单词表示为数字才能处理它们。因此,为了执行情感分析等任务,我们需要将文本转换为数字。
所以,我们首先讨论的方法是独热编码(one-hot encoding),它在处理单词时表现较差,因为单词之间存在某些关系,而独热编码却将单词当作彼此独立的来计算。例如,假设我们有三个单词:‘car’(车),‘truck’(卡车),和‘ship’(船)。现在,‘car’在相似度上更接近‘truck’,但它与‘ship’仍然有一定相似性。独热编码未能捕捉到这种关系。
词嵌入也是单词的向量表示,但它们捕捉了每个单词与其他单词之间的关系。获取词嵌入的不同方法将在以下部分中解释。
计数嵌入(Count Embedding)
计数嵌入(Count embedding) 是一种简单的词向量表示,依据单词在文本中出现的次数来构建。假设有一个数据集,其中包含 n 个唯一单词和 M 个不同记录。要获得计数嵌入,你需要创建一个 N x M 矩阵,其中每一行代表一个单词,每一列代表一个记录。矩阵中任何 (n,m) 位置的值将包含单词 n 在记录 m 中出现的次数。
TF-IDF 嵌入(TF-IDF Embedding)
TF-IDF 是一种衡量每个单词在一组单词或文档中重要性的方法。它代表词频-逆文档频率(term frequency-inverse document frequency)。在 TF-IDF 中,单词的重要性随着该单词的频率而增加,但这一重要性会被包含该单词的文档数量所抵消,从而有助于调整某些使用频率较高的单词。换句话说,单词的重要性是通过计算该单词在训练集中的一个数据点的频率来得出的。这一重要性会根据单词在训练集中的其他数据点的出现情况而增加或减少。
TF-IDF 生成的权重由两个术语组成:
- 词频(TF):单词在文档中出现的频率,如下图所示:
图 7.24:词频公式
其中 w 是单词。
- 逆文档频率(IDF):单词提供的信息量,如下图所示:
图 7.25:逆文档频率公式
权重是这两个项的乘积。在 TF-IDF 的情况下,我们用这个权重替代词频,在我们之前用于计数嵌入部分的 N x M 矩阵中。
连续词袋模型嵌入
连续词袋模型(CBOW)通过使用神经网络进行工作。当输入是某个单词的上下文单词时,它预测该单词。神经网络的输入是上下文单词的独热向量。输入单词的数量由窗口参数决定。网络只有一个隐藏层,输出层通过 softmax 激活函数来获取概率。层与层之间的激活函数是线性的,但更新梯度的方法与常规神经网络相同。
语料库的嵌入矩阵是隐藏层和输出层之间的权重。因此,这个嵌入矩阵的维度将是 N x H,其中 N 是语料库中唯一单词的数量,H 是隐藏层节点的数量。由于其概率性质和低内存需求,CBOW 比之前讨论的两种方法表现更好。
图 7.26:CBOW 网络的表示
Skip-gram 嵌入
使用神经网络,skip-gram 根据输入单词预测其周围的单词。这里的输入是单词的独热向量,输出是周围单词的概率。输出单词的数量由窗口参数决定。与 CBOW 类似,这种方法使用一个只有单层隐藏层的神经网络,且所有激活函数均为线性,除了输出层,我们使用 softmax 函数。一个重要的区别是误差的计算方式:为每个被预测的单词计算不同的误差,然后将所有误差加起来得到最终的误差。每个单词的误差是通过将输出概率向量与目标独热向量相减来计算的。
这里的嵌入矩阵是输入层和隐藏层之间的权重矩阵。因此,这个嵌入矩阵的维度将是 H x N,其中 N 是语料库中唯一单词的数量,H 是隐藏层节点的数量。对于较少频繁出现的单词,skip-gram 的表现远好于 CBOW,但通常较慢:
图 7.27:跳字模型的表示
提示
对于词汇较少但样本量大的数据集,使用 CBOW;对于词汇量较大且样本量较小的数据集,使用 skip-gram。
Word2Vec
gensim库:
model = gensim.models.Word2Vec(
tokens,
iter=5
size=100,
window=5,
min_count=5,
workers=10,
sg=0)
为了训练模型,我们需要将令牌化的句子作为参数传递给gensim的Word2Vec类。iter是训练的轮数,size指的是隐藏层节点数,也决定了嵌入层的大小。window是指在训练神经网络时,考虑的上下文单词数。min_count是指某个单词至少出现多少次才能被考虑。workers是训练时使用的线程数,sg是指使用的训练算法,0代表 CBOW,1代表 skip-gram。
要获取训练好的词向量中的唯一词汇数量,可以使用以下代码:
vocab = list(model.wv.vocab)
len(vocab)
在使用这些词向量之前,我们需要确保它们是正确的。为此,我们通过查找相似的单词来验证:
model.wv.most_similar('fun')
输出结果如下:
图 7.28:相似单词
要将你的词向量保存到文件中,请使用以下代码:
model.wv.save_word2vec_format('movie_embedding.txt', binary=False)
要加载预训练的词向量,可以使用这个函数:
def load_embedding(filename, word_index , num_words, embedding_dim):
embeddings_index = {}
file = open(filename, encoding="utf-8")
for line in file:
values = line.split()
word = values[0]
coef = np.asarray(values[1:])
embeddings_index[word] = coef
file.close()
embedding_matrix = np.zeros((num_words, embedding_dim))
for word, pos in word_index.items():
if pos >= num_words:
continue
print(num_words)
embedding_vector = embeddings_index.get(word)
if embedding_vector is not None:
embedding_matrix[pos] = embedding_vector
return embedding_matrix
该函数首先读取嵌入文件filename,并获取文件中所有的嵌入向量。然后,它会创建一个嵌入矩阵,将这些嵌入向量堆叠在一起。num_words参数限制了词汇表的大小,在 NLP 算法训练时间过长的情况下非常有用。word_index是一个字典,键是语料库中的唯一单词,值是该单词的索引。embedding_dim是训练时指定的嵌入向量的大小。
提示
有很多非常好的预训练词向量可供使用。一些流行的包括 GloVe: nlp.stanford.edu/projects/gl… 和 fastText: fasttext.cc/docs/en/eng…
练习 56:使用 Gensim 创建词向量
在本练习中,我们将使用gensim库创建我们自己的 Word2Vec 词向量。这个词向量将为我们正在使用的 IMDB 电影评论数据集创建。我们将从练习 55的结束部分继续。
-
评审变量包含的是以令牌形式表示的评论,但它们已经被转换成数字。
gensim的 Word2Vec 要求令牌为字符串形式,因此我们回溯到步骤 6 中将令牌重新转换为句子的部分,即练习 55。data['SentimentText'] [0]第一条评论的令牌如下:
图 7.29:第一条评论的令牌
-
现在,我们使用
pandas的apply函数将每一行的列表转换为单个列表,使用如下代码:data['SentimentText'] = data['SentimentText'].apply(lambda x: x[0]) -
现在,我们将这些预处理过的数据输入到 Word2Vec 中,以创建词向量:
from gensim.models import Word2Vec model = Word2Vec( data['SentimentText'], iter=50, size=100, window=5, min_count=5, workers=10) -
让我们通过查看一些相似的单词来检查模型的表现:
model.wv.most_similar('insight')数据集中与'
insight'最相似的词语是:图 7.30:与'insight'相似的词语
-
要获得两个词之间的相似度,请使用:
model.wv.similarity(w1='violent', w2='brutal') -
这里显示了相似度的输出结果:
图 7.31:相似度输出
相似度分数的范围是从
0到1,其中1表示两个词完全相同,0表示两个词完全不同,毫无关联。 -
将嵌入结果绘制在二维空间中,以理解哪些词语被发现彼此相似。
首先,使用 PCA 将嵌入转换为二维。我们将仅绘制前 200 个词。(如果愿意,你可以绘制更多的词。)
from sklearn.decomposition import PCA word_limit = 200 X = model[model.wv.vocab][: word_limit] pca = PCA(n_components=2) result = pca.fit_transform(X) -
现在,使用
matplotlib将结果绘制成散点图:import matplotlib.pyplot as plt plt.scatter(result[:, 0], result[:, 1]) words = list(model.wv.vocab)[: word_limit] for i, word in enumerate(words): plt.annotate(word, xy=(result[i, 0], result[i, 1])) plt.show()你的输出结果应该如下所示:
图 7.32:使用 PCA 表示前 200 个词的嵌入
注意
在词嵌入的表示中,坐标轴没有任何特定含义。表示仅显示不同词语之间的相似度。
-
将嵌入保存到文件中,以便稍后检索:
model.wv.save_word2vec_format('movie_embedding.txt', binary=False)
恭喜!你刚刚创建了你的第一个词嵌入。你可以玩转这些嵌入,查看不同词语之间的相似性。
活动 19:预测电影评论的情感
在本活动中,我们将尝试预测电影评论的情感。数据集(github.com/TrainingByP… 25,000 条来自 IMDB 的电影评论及其情感(正面或负面)。让我们看看以下情景:你在一家 DVD 租赁公司工作,必须根据评论者的评价预测某部电影需要制作的 DVD 数量。为此,你创建一个机器学习模型,能够分析评论并判断电影的受欢迎程度。
-
读取并预处理电影评论。
-
创建评论的词嵌入。
-
创建一个完全连接的神经网络来预测情感,这与我们在第五章中创建的神经网络模型类似:掌握结构化数据。输入将是评论的词嵌入,而模型的输出将是
1(正面情感)或0(负面情感)。注意
该活动的解决方案可以在第 378 页找到。
输出结果有些晦涩,因为停用词和标点符号已经被移除,但你仍然可以理解评论的大致意思。
恭喜你!你刚刚创建了你的第一个 NLP 模块。你应该会发现该模型的准确率大约是 76%,这是比较低的。这是因为它是基于单个词来预测情感的;它无法理解评论的上下文。例如,它会将“not good”预测为积极情感,因为它看到了“good”这个词。如果它能看到多个词,就能理解这是负面情感。在接下来的章节中,我们将学习如何创建能够保留过去信息的神经网络。
递归神经网络(RNN)
到目前为止,我们讨论的所有问题都没有时间依赖性,这意味着预测不仅依赖于当前输入,还依赖于过去的输入。例如,在狗与猫分类器的案例中,我们只需要一张狗的图片就能将其分类为狗。不需要其他信息或图片。而如果你想创建一个分类器,用于预测狗是走路还是站着,你将需要一系列的图片或视频,以确定狗的行为。RNNs 就像我们之前讨论的完全连接的网络。唯一的区别是,RNN 具有存储关于之前输入的信息作为状态的记忆。隐藏层的输出作为下一个输入的输入。
图 7.33:递归神经网络的表示
从图像中,你可以理解隐藏层的输出是如何作为下一个输入的输入。这在神经网络中充当记忆元素。另一个需要注意的事项是,普通神经网络的输出是输入和网络权重的函数。
这使得我们能够随机输入任何数据点以获得正确的输出。然而,RNNs(递归神经网络)却不是这样。在 RNN 的情况下,我们的输出取决于之前的输入,因此我们需要按照正确的顺序输入数据。
图 7.34:递归层的表示
在前面的图像中,你可以看到左边“折叠”模型中的单个 RNN 层。U 是输入权重,V 是输出权重,W 是与记忆输入相关的权重。RNN 的记忆也被称为状态。右边的“展开”模型展示了 RNN 如何处理输入序列[xt-1, xt, xt+1]。该模型会根据应用的不同而有所变化。例如,在情感分析中,输入序列最终只需要一个输出。这个问题的展开模型如下所示:
图 7.35:用于情感分析的递归层展开表示
LSTM(长短期记忆网络)
长短期记忆(LSTM)单元是一种特殊的 RNN 单元,能够在长时间段内保持信息。Hochreiter 和 Schmidhuber 在 1997 年引入了 LSTM。RNN 遭遇了梯度消失问题。它们在长时间段内会丢失所检测到的信息。例如,如果我们在对一篇文本进行情感分析,第一句话说“我今天很高兴”,然后接下来的文本没有任何情感,RNN 就无法有效地检测到文本的情感是高兴的。长短期记忆(LSTM)单元通过长时间存储某些输入而不忘记它们,克服了这个问题。大多数现实世界的递归机器学习实现都使用 LSTM。RNN 单元和 LSTM 单元的唯一区别在于记忆状态。每个 RNN 层接受一个记忆状态作为输入,并输出一个记忆状态,而每个 LSTM 层则接受长期记忆和短期记忆作为输入,并输出这两者。长期记忆使得网络能够保留信息更长时间。
LSTM 单元在 Keras 中已经实现,你可以轻松地将 LSTM 层添加到模型中:
model.add(keras.layers.LSTM(units, activation='tanh', dropout=0.0, recurrent_dropout=0.0, return_sequences=False))
在这里,units 是层中节点的数量,activation 是该层使用的激活函数。recurrent_dropout 和 dropout 分别是递归状态和输入的丢弃概率。return_sequences 指定输出是否应包含序列;当你计划在当前层后面使用另一个递归层时,这一选项设置为 True。
注意
LSTM 通常比 RNN 更有效。
练习 57:使用 LSTM 执行情感分析
在本次练习中,我们将修改之前为前一个活动创建的模型,使其使用 LSTM 单元。我们将继续使用之前处理过的 IMDB 电影评论数据集。大多数预处理步骤与活动 19中的步骤相似。
-
使用 pandas 在 Python 中读取 IMDB 电影评论数据集:
import pandas as pd data = pd.read_csv('../../chapter 7/data/movie_reviews.csv', encoding='latin-1') -
将推文转换为小写,以减少唯一词汇的数量:
data.text = data.text.str.lower() -
使用 RegEx 和
clean_str函数清理评论:import re def clean_str(string): string = re.sub(r"https?\://\S+", '', string) string = re.sub(r'\<a href', ' ', string) string = re.sub(r'&', '', string) string = re.sub(r'<br />', ' ', string) string = re.sub(r'[_"\-;%()|+&=*%.,!?:#$@\[\]/]', ' ', string) string = re.sub('\d','', string) string = re.sub(r"can\'t", "cannot", string) string = re.sub(r"it\'s", "it is", string) return string data.SentimentText = data.SentimentText.apply(lambda x: clean_str(str(x))) -
接下来,去除评论中的停用词和其他频繁出现的不必要的词语。此步骤将字符串转换为标记(这在下一步中会有所帮助):
from nltk.corpus import stopwords from nltk.tokenize import word_tokenize,sent_tokenize stop_words = stopwords.words('english') + ['movie', 'film', 'time'] stop_words = set(stop_words) remove_stop_words = lambda r: [[word for word in word_tokenize(sente) if word not in stop_words] for sente in sent_tokenize(r)] data['SentimentText'] = data['SentimentText'].apply(remove_stop_words) -
将这些标记组合成一个字符串,然后删除任何在去除停用词后内容为空的评论:
def combine_text(text): try: return ' '.join(text[0]) except: return np.nan data.SentimentText = data.SentimentText.apply(lambda x: combine_text(x)) data = data.dropna(how='any') -
使用 Keras Tokenizer 对评论进行标记化,并将它们转换为数字:
from keras.preprocessing.text import Tokenizer tokenizer = Tokenizer(num_words=5000) tokenizer.fit_on_texts(list(data['SentimentText'])) sequences = tokenizer.texts_to_sequences(data['SentimentText']) word_index = tokenizer.word_index -
最后,将推文填充为最多 100 个单词。如果单词数少于 100,将补充 0,超过 100 则会删除多余的单词:
from keras.preprocessing.sequence import pad_sequences reviews = pad_sequences(sequences, maxlen=100) -
使用
load_embedding函数加载先前创建的嵌入,获取嵌入矩阵,该函数在文本处理部分中有讨论,使用以下代码:import numpy as np def load_embedding(filename, word_index , num_words, embedding_dim): embeddings_index = {} file = open(filename, encoding="utf-8") for line in file: values = line.split() word = values[0] coef = np.asarray(values[1:]) embeddings_index[word] = coef file.close() embedding_matrix = np.zeros((num_words, embedding_dim)) for word, pos in word_index.items(): if pos >= num_words: continue embedding_vector = embeddings_index.get(word) if embedding_vector is not None: embedding_matrix[pos] = embedding_vector return embedding_matrix embedding_matrix = load_embedding('movie_embedding.txt', word_index, len(word_index), 16) -
将数据分为训练集和测试集,按 80:20 的比例划分。此比例可以调整,以找到最佳的划分方式:
from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(reviews, pd.get_dummies(data.Sentiment), test_size=0.2, random_state=9) -
创建并编译包含一个 LSTM 层的 Keras 模型。你可以尝试不同的层和超参数:
from keras.models import Model from keras.layers import Input, Dense, Dropout, BatchNormalization, Embedding, Flatten, LSTM inp = Input((100,)) embedding_layer = Embedding(len(word_index), 16, weights=[embedding_matrix], input_length=100, trainable=False)(inp) model = Dropout(0.10)(embedding_layer) model = LSTM(128, dropout=0.2)(model) model = Dense(units=256, activation='relu')(model) model = Dense(units=64, activation='relu')(model) model = Dropout(0.3)(model) predictions = Dense(units=2, activation='softmax')(model) model = Model(inputs = inp, outputs = predictions) model.compile(loss='binary_crossentropy', optimizer='sgd', metrics = ['acc']) -
使用以下代码训练模型 10 个 epoch,以查看其表现是否优于活动 1中的模型:
model.fit(X_train, y_train, validation_data = (X_test, y_test), epochs=10, batch_size=256) -
检查模型的准确度:
from sklearn.metrics import accuracy_score preds = model.predict(X_test) accuracy_score(np.argmax(preds, 1), np.argmax(y_test.values, 1))LSTM 模型的准确度为:
图 7.36:LSTM 模型准确度
-
绘制模型的混淆矩阵,以便更好地理解模型的预测:
y_actual = pd.Series(np.argmax(y_test.values, axis=1), name='Actual') y_pred = pd.Series(np.argmax(preds, axis=1), name='Predicted') pd.crosstab(y_actual, y_pred, margins=True)图 7.37:模型的混淆矩阵(0 = 负面情感,1 = 正面情感)
-
使用以下代码检查模型的表现,通过查看随机评论的情感预测结果:
review_num = 110 print("Review: \n"+tokenizer.sequences_to_texts([X_test[review_num]])[0]) sentiment = "Positive" if np.argmax(preds[review_num]) else "Negative" print("\nPredicted sentiment = "+ sentiment) sentiment = "Positive" if np.argmax(y_test.values[review_num]) else "Negative" print("\nActual sentiment = "+ sentiment)输出结果如下:
图 7.38:来自 IMDB 数据集的负面评论
恭喜!你刚刚实现了一个 RNN 来预测电影评论的情感。这个网络比我们之前创建的网络表现得稍微好一点。可以通过调整网络架构和超参数来提高模型的准确度。你还可以尝试使用来自 fastText 或 GloVe 的预训练词嵌入来提高模型的准确度。
活动 20:从推文中预测情感
在这个活动中,我们将尝试预测推文的情感。提供的数据集(github.com/TrainingByP… 150 万条推文及其情感(正面或负面)。让我们来看以下情境:你在一家大型消费者组织工作,该公司最近创建了一个 Twitter 账户。部分对公司有不满经历的顾客开始在 Twitter 上表达他们的情感,导致公司声誉下降。你被指派识别这些推文,以便公司可以与这些顾客取得联系,提供更好的支持。你通过创建一个情感预测器来完成这项任务,预测器可以判断推文的情感是正面还是负面。在将你的情感预测器应用到关于公司实际的推文之前,你将先在提供的推文数据集上进行测试。
-
读取数据并去除所有不必要的信息。
-
清理推文,进行分词,最后将其转换为数字。
-
加载 GloVe Twitter 嵌入并创建嵌入矩阵(nlp.stanford.edu/projects/gl…
-
创建一个 LSTM 模型来预测情感。
注意
该活动的解决方案可以在第 383 页找到。
恭喜!你刚刚创建了一个机器学习模块,用于从推文中预测情感。现在你可以使用 Twitter API 将其部署,用于实时推文情感分析。你可以尝试不同的 GloVe 和 fastText 嵌入,并查看模型能提高多少准确度。
总结
在本章中,我们学习了计算机如何理解人类语言。我们首先了解了什么是正则表达式(RegEx),以及它如何帮助数据科学家分析和清洗文本数据。接下来,我们了解了停用词,它们是什么,以及为什么要从数据中去除停用词以减少维度。接着,我们学习了句子切分及其重要性,然后是词嵌入。词嵌入是我们在第五章《掌握结构化数据》中讲解的主题;在这里,我们学习了如何创建词嵌入以提升我们的自然语言处理(NLP)模型的性能。为了创建更好的模型,我们研究了循环神经网络(RNN),这是一种特殊类型的神经网络,能够保留过去输入的记忆。最后,我们学习了 LSTM 单元及其为何优于普通 RNN 单元。
现在你已经完成了本章的学习,你已经能够处理文本数据并为自然语言处理创建机器学习模型。在下一章中,你将学习如何通过迁移学习和一些技巧加速模型的训练。