自然语言处理的深度学习(三)
原文:
annas-archive.org/md5/79edb699aa9642b682a3d7113b128c41译者:飞龙
第十章:第九章
组织中的实用 NLP 项目工作流
学习目标
本章结束时,你将能够:
-
确定自然语言处理项目的需求
-
了解组织中不同团队的参与方式
-
使用 Google Colab 笔记本,通过 GPU 训练深度学习模型
-
在 AWS 上部署一个模型,作为软件即服务(SaaS)使用
-
熟悉一个简单的技术栈用于部署
本章我们将关注一个实时 NLP 项目及其在组织中的流程,一直到最终阶段,贯穿整章。
引言
到目前为止,我们已经学习了几种深度学习技术,这些技术可以应用于解决 NLP 领域的特定问题。掌握这些技术使我们能够构建出优秀的模型并提供高质量的性能。然而,当涉及到在组织中交付一个可用的机器学习产品时,还有许多其他方面需要考虑。
本章将介绍在组织中交付一个可工作的深度学习系统时的实际项目工作流。具体来说,你将了解组织内不同团队的角色,构建深度学习管道,并最终将产品交付为 SaaS 形式。
机器学习产品开发的一般工作流
如今,在组织中有多种与数据科学合作的方式。大多数组织都有一个特定于其环境的工作流。以下是一些示例工作流:
图 9.1:机器学习产品开发的一般工作流
展示工作流:
图 9.2:一般的展示工作流
展示工作流可以如下详细说明:
-
数据科学团队收到使用机器学习解决问题的请求。请求者可以是组织内的其他团队,或者是雇佣你作为顾问的其他公司。
-
获取相关数据并应用特定的机器学习技术。
-
你以报告/演示的形式向相关方展示结果和见解。这也可能是项目概念验证(PoC)阶段的一种潜在方式。
研究工作流:
图 9.3:研究工作流
这种方法的主要重点是进行研究以解决一个特定的问题,且该问题迎合了一个实际应用场景。该解决方案既可以被组织使用,也可以为整个社区所利用。其他将这种工作流与展示工作流区分开来的因素如下:
-
这类项目的时间线通常比呈现工作流的时间线要长。
-
可交付成果是以研究论文和/或工具包的形式呈现的。
工作流可以分解如下:
-
你的组织有一个研究部门,希望增强社区内现有的机器学习状态,同时允许你的公司利用结果。
-
你的团队研究现有研究,以解决提出的问题。这包括详细阅读研究论文,并实施它们以在研究论文中建议的某些数据集上建立基准性能。
-
然后你要么尝试调整现有研究以解决你的问题,要么自己提出新的解决方案。
-
最终产品可以是研究论文和/或工具箱。
面向生产的工作流程
图 9.4:面向生产的工作流程
该工作流程可以详细阐述如下:
-
数据科学团队接到了使用机器学习解决问题的请求。请求者可能是组织内的其他团队,也可能是雇佣你们作为顾问的另一家公司。也可能是数据科学团队希望构建他们认为将为组织带来价值的产品。
-
你获取数据,进行必要的研究,并构建机器学习模型。数据可以来自组织内部,也可以是开源数据集(例如:语言翻译)。因此构建的模型可以作为PoC展示给利益相关者。
-
你定义了一个最小可行产品(MVP):例如,以 SaaS 形式的机器学习模型。
一旦达到 MVP,你可以逐步添加其他方面,例如数据获取管道,持续集成,监控等。
你会注意到,即使示例工作流程也共享组件。在本章中,我们的重点将放在生产工作流程的一部分。我们将为一个特定问题建立一个最小可行产品。
问题定义
假设你在一个电子商务平台工作,通过这个平台,客户可以购买各种产品。你公司的商品部门提出在网站上添加一个功能的请求 – '增加一个滑块,显示在特定日历周内获得最多正面评价的 5 个商品'。
首先将此请求提交给网页开发部门,因为最终他们负责显示网站内容。网页开发部门意识到,为了获得评价等级,需要数据科学团队参与。数据科学团队从网页开发团队接到请求 – '我们需要一个 Web 服务,接受文本字符串作为输入,并返回表示文本表达积极情绪程度的评分'。
然后数据科学团队细化需求,并与网页开发团队达成关于最小可行产品(MVP)定义的协议:
-
交付物将是部署在 AWS EC2 实例上的 Web 服务。
-
Web 服务的输入将是一个包含四条评论的 POST 请求(即单个 POST 请求包含四条评论)。
-
Web 服务的输出将是与每个输入文本对应的四个评分。
-
输出评分将以 1 到 5 的尺度表示,1 表示最不积极的评论,5 表示最积极的评论。
数据采集
任何机器学习模型性能的一个重要决定因素是数据的质量和数量。
通常,数据仓库团队/基础设施团队(DWH)负责公司内与数据相关的基础设施维护。该团队确保数据不会丢失,底层基础设施稳定,并且数据始终可供任何可能需要使用的团队使用。数据科学团队作为数据的消费者之一,会联系 DWH 团队,由其授予访问权限,访问包含公司产品目录中各种商品评论的数据库。
通常,数据库中有多个数据字段/表格,其中一些可能对机器学习模型的开发并不重要。
数据工程师(DWH 团队的一部分、其他团队成员或你团队的成员)随后连接到数据库,将数据处理为表格格式,并生成csv格式的平面文件。在这一过程中,数据科学家与数据工程师的讨论最终决定只保留数据库表中的三列:
-
'Rating':1 到 5 的评分,表示积极情感的程度
-
'Review Title':评论的简单标题
-
'Review':实际的评论文本
请注意,这三个字段都是来自客户(即你电商平台的用户)的输入。此外,像‘item id’这样的字段并未保留,因为它们不需要用于构建这个情感分类的机器学习模型。这些信息的保留与删除也是数据科学团队、数据工程师和 DWH 团队之间讨论的结果。
目前的数据可能没有情感评分。在这种情况下,一种常见的解决方案是手动浏览每个评论并为其分配情感分数,以便为模型获得训练数据。然而,正如你所想的那样,为数百万条评论做这项工作是一个令人望而却步的任务。因此,可以利用众包服务,如Amazon Mechanical Turk,来标注数据并为其获取训练标签。
注意
欲了解更多关于 Amazon Mechanical Turk 的信息,请参见 www.mturk.com/。
Google Colab
你一定熟悉深度学习模型的强大计算需求。在 CPU 上,训练一个含有大量训练数据的深度学习模型会花费非常长的时间。因此,为了让训练时间保持在可接受范围内,通常会使用提供图形处理单元(GPU)加速计算的云服务。与在 CPU 上运行训练过程相比,你可以期待加速 10 到 30 倍。当然,具体的加速效果取决于 GPU 的性能、数据量和处理步骤。
有许多供应商提供此类云服务,如亚马逊 Web 服务(AWS)、微软 Azure等。Google 提供了一个名为Google Colab的环境/IDE,任何想训练深度学习模型的人都可以每天免费使用最多 12 小时的 GPU。此外,代码是在类似Jupyter的笔记本上运行的。在本章中,我们将利用 Google Colab 的强大功能来开发我们的深度学习情感分类器。
为了熟悉 Google Colab,建议你完成一个 Google Colab 教程。
注意
在继续之前,请参考 colab.research.google.com/notebooks/w… 上的教程
以下步骤将帮助你更好地了解 Google Colab:
-
若要打开一个新的空白Colab笔记本,请访问 colab.research.google.com/notebooks/w… Python 3 笔记本**”选项,如截图所示:
图 9.5:Google Colab 上的新 Python 笔记本
-
接下来,给笔记本重命名为你选择的名称。然后,为了使用GPU进行训练,我们需要选择GPU作为运行时环境。为此,从菜单中选择“编辑”选项,然后选择“笔记本设置”。
图 9.6:Google Colab 中的编辑下拉菜单
-
一个菜单将弹出,其中有一个“硬件加速器”字段,默认设置为“无”:
图 9.7:Google Colab 的笔记本设置
-
此时,可以使用下拉菜单选择“GPU”作为选项:
图 9.8:GPU 硬件加速器
-
为了检查 GPU 是否已经分配给你的笔记本,请运行以下代码:
# Check if GPU is detected import tensorflow as tf tf.test.gpu_device_name()运行此代码后的输出应显示 GPU 的可用性:
图 9.9:GPU 设备名称的截图
输出结果是 GPU 的设备名称。
-
接下来,数据需要在笔记本中可访问。有多种方式可以做到这一点。一种方法是将数据移动到个人的 Google Drive 位置。为了避免占用过多的空间,最好将数据以压缩格式移动。首先,在 Google Drive 上创建一个新文件夹,并将压缩的 CSV 数据文件移入该文件夹。然后,我们将 Google Drive 挂载到 Colab 笔记本机器上,使得驱动器中的数据可以在 Colab 笔记本中使用:
from google.colab import drive drive.mount('/content/gdrive')我们刚才提到的代码片段会返回一个用于授权的网页链接。点击该链接后,会打开一个新的浏览器标签页,显示一个授权码,复制并粘贴到笔记本提示框中:
图 9.10:从 Google Drive 导入数据的截图
此时,Google Drive 中的所有数据都可以在 Colab 笔记本中使用了。
-
接下来,导航到包含压缩数据的文件夹位置:
cd "/content/gdrive/My Drive/Lesson-9/" -
通过在笔记本单元中输入 '
pwd' 命令来确认你已经导航到所需位置:图 9.11:从 Google Drive 导入的数据截图
-
接下来,使用
unzip命令解压缩数据文件:!unzip data.csv.zip这将产生以下输出:
图 9.12:在 Colab 笔记本上解压数据文件
'MACOSX' 输出行是操作系统特定的,可能并不适用于每个人。无论如何,一个解压后的数据文件 'data.csv' 现在可以在 Colab 笔记本中使用了。
-
现在我们已经有了数据,并且设置好了可以使用 GPU 的环境,我们可以开始编写模型代码了。首先,我们将导入所需的包:
import os import re import pandas as pd from keras.preprocessing.text import Tokenizer from keras.preprocessing.sequence import pad_sequences from keras.models import Sequential from keras.layers import Dense, Embedding, LSTM -
接下来,我们将编写一个预处理函数,将所有文本转为小写并移除任何数字:
def preprocess_data(data_file_path): data = pd.read_csv(data_file_path, header=None) # read the csv data.columns = ['rating', 'title', 'review'] # add column names data['review'] = data['review'].apply(lambda x: x.lower()) # change all text to lower data['review'] = data['review'].apply((lambda x: re.sub('[^a-zA-z0-9\s]','',x))) # remove all numbers return data -
请注意,我们使用 pandas 来读取和处理文本。让我们运行这个函数并提供 CSV 文件的路径:
df = preprocess_data('data.csv') -
现在,我们可以检查数据框的内容:
图 9.13:数据框内容截图
-
正如预期的那样,我们有三个字段。此外,我们可以看到 'review' 列的文本比 'title' 列多得多。因此,我们选择仅使用 'review' 列来开发模型。接下来,我们将进行文本分词:
# initialize tokenization max_features = 2000 maxlength = 250 tokenizer = Tokenizer(num_words=max_features, split=' ') # fit tokenizer tokenizer.fit_on_texts(df['review'].values) X = tokenizer.texts_to_sequences(df['review'].values) # pad sequences X = pad_sequences(X, maxlen=maxlength)在这里,我们将特征数限制为 2,000 个词。然后,我们使用最大特征数的分词器应用于数据的 'review' 列。我们还将序列长度填充到 250 个词。
X变量如下所示:图 9.14:X 变量数组的截图
X 变量是一个
NumPy数组,包含 3,000,000 行和 250 列。因为有 3,000,000 条评论,每条评论在填充后都有固定长度 250 个词。 -
我们现在准备目标变量以进行训练。我们将问题定义为一个五分类问题,每个类别对应一个评分。由于评分(情感分数)是 1 到 5 的范围,因此分类器有 5 个输出。(你也可以将其建模为回归问题)。我们使用 pandas 的
get_dummies函数来获得这五个输出:# get target variable y_train = pd.get_dummies(df.rating).valuesy_train变量是一个NumPy数组,包含 3,000,000 行和 5 列,值如下所示:](tos-cn-i-73owjymdk6/3654fd198c7d410092f65ae16c598a67)
图 9.15:y_train 输出
-
我们现在已经预处理了文本并准备好了目标变量。接下来,我们定义模型:
embed_dim = 128 hidden_units = 100 n_classes = 5 model = Sequential() model.add(Embedding(max_features, embed_dim, input_length = X.shape[1])) model.add(LSTM(hidden_units)) model.add(Dense(n_classes, activation='softmax')) model.compile(loss = 'categorical_crossentropy', optimizer='adam',metrics = ['accuracy']) print(model.summary())我们选择 128 的嵌入维度作为输入。我们还选择了 LSTM 作为 RNN 单元,隐藏维度为 100。模型摘要如下所示:
图 9.16:模型摘要的截图
-
我们现在可以拟合模型:
# fit the model model.fit(X[:100000, :], y_train[:100000, :], batch_size = 128, epochs=15, validation_split=0.2)注意,我们训练了 100,000 条评论而不是 3,000,000 条。使用这个配置运行训练会话大约需要 90 分钟。如果使用完整的数据集,将需要更长时间:
](tos-cn-i-73owjymdk6/3aed7419cae7499dae6c40ce66b9b51e)
图 9.17:训练会话的截图
这个五分类问题的验证准确率为 48%。这个结果不是很好,但为了演示的目的,我们可以继续进行部署。
-
我们现在拥有了想要部署的模型。接下来,我们需要保存模型文件和分词器,以便在生产环境中用于获取新的评论预测:
# save model and tokenizer model.save('trained_model.h5') # creates a HDF5 file 'trained_model.h5' with open('trained_tokenizer.pkl', 'wb') as f: # creates a pickle file 'trained_tokenizer.pkl' pickle.dump(tokenizer, f) -
现在这些文件需要从 Google Colab 环境下载到本地硬盘:
from google.colab import files files.download('trained_model.h5') files.download('trained_tokenizer.pkl')这个代码片段将下载分词器和模型文件到本地计算机。现在我们已经准备好使用模型进行预测。
Flask
在本节中,我们将使用 Python 提供的 Flask 微服务框架,制作一个提供预测的 Web 应用程序。我们将获得一个可以查询的 RESTful API 以获取结果。在开始之前,我们需要安装 Flask(使用 pip):
-
让我们开始导入必要的包:
import re import pickle import numpy as np from flask import Flask, request, jsonify from keras.models import load_model from keras.preprocessing.sequence import pad_sequences -
现在,让我们编写一个加载训练好的模型和
tokenizer的函数:def load_variables(): global model, tokenizer model = load_model('trained_model.h5') model._make_predict_function() #https://github.com/keras-team/keras/issues/6462 with open('trained_tokenizer.pkl', 'rb') as f: tokenizer = pickle.load(f)make_predict_function()是一个技巧,使得 Flask 可以使用keras模型。 -
现在,我们将定义类似于训练代码的预处理函数:
def do_preprocessing(reviews): processed_reviews = [] for review in reviews: review = review.lower() processed_reviews.append(re.sub('[^a-zA-z0-9\s]', '', review)) processed_reviews = tokenizer.texts_to_sequences(np.array(processed_reviews)) processed_reviews = pad_sequences(processed_reviews, maxlen=250) return processed_reviews与训练阶段类似,评论首先会被转换为小写。然后,数字会被空白替换。接着,加载的分词器将被应用,并且序列会被填充为固定长度 250,以便与训练输入一致。
-
我们现在定义一个 Flask 应用实例:
app = Flask(__name__) -
现在我们定义一个端点,显示固定的消息:
@app.route('/') def home_routine(): return 'Hello World!'良好的实践是在根端点处检查 Web 服务是否可用。
-
接下来,我们将设置一个预测端点,向该端点发送我们的评论字符串。我们将使用的 HTTP 请求类型是‘
POST’请求:@app.route('/prediction', methods=['POST']) def get_prediction(): # get incoming text # run the model if request.method == 'POST': data = request.get_json() data = do_preprocessing(data) predicted_sentiment_prob = model.predict(data) predicted_sentiment = np.argmax(predicted_sentiment_prob, axis=-1) return str(predicted_sentiment) -
现在我们可以启动网页服务器:
if __name__ == '__main__': # load model load_variables() app.run(debug=True) -
我们可以将此文件保存为
app.py(可以使用任何名称)。从终端运行此代码,使用app.py:python app.py在终端窗口中将产生如下所示的输出:
图 9.18:Flask 输出
-
这时,请打开浏览器并输入
http://127.0.0.1:5000/地址。屏幕上将显示“Hello World!”消息。输出内容对应于我们在代码中设置的根端点。现在,我们将评论文本发送到 Flask 网络服务的“预测”端点。让我们发送以下四条评论: -
“这本书非常差”
-
“非常好!”
-
“作者本可以做得更多”
-
“令人惊叹的产品!”
-
我们可以使用
curl请求向 Web 服务发送 POST 请求。对于提到的四条评论,可以通过终端发送curl请求,方法如下:curl -X POST \ 127.0.0.1:5000/prediction \ -H 'Content-Type: application/json' \ -d '["The book was very poor", "Very nice!", "The author could have done more", "Amazing product!"]'四条评论将被发布到网络服务的预测端点。
Web 服务返回四个评分的列表:
[0 4 2 4]因此,情感评分如下:
-
“这本书非常差”- 0
-
“非常好!”- 4
-
“作者本可以做得更多” - 2
-
“令人惊叹的产品!” - 4
评分实际上很有意义!
部署
到目前为止,数据科学团队已经拥有了一个在本地系统上运行的 Flask Web 服务。然而,网页开发团队仍然无法使用该服务,因为它只在本地系统上运行。因此,我们需要将这个 Web 服务托管在某个云平台上,以便网页开发团队也能使用。本节提供了一个基本的部署管道,分为以下几个步骤:
-
对 Flask 网页应用进行更改,以便可以部署。
-
使用 Docker 将 Flask 网页应用打包成容器。
-
将容器托管在亚马逊 Web 服务(AWS)EC2 实例上。
让我们详细查看每一步。
对 Flask 网页应用进行更改
在 FLASK 部分编码的 Flask 应用程序运行在本地 Web 地址:http://127.0.0.1:5000。由于我们的目标是将其托管在互联网上,因此该地址需要更改为:0.0.0.0。 此外,由于默认的 HTTP 端口是 80,因此端口也需要从 5000 更改为 80。所以,现在需要查询的地址变成了:0.0.0.0:80。
在代码片段中,可以通过修改对 app.run 函数的调用来简单地完成此更改,如下所示:
app.run(host=0.0.0.0, port=80)
请注意,‘debug’标志也消失了(‘debug’标志的默认值是‘False’)。这是因为应用程序已经过了调试阶段,准备部署到生产环境。
注意
其余的代码与之前完全相同。
应用程序应使用与之前相同的命令再次运行,并验证是否收到了与之前相同的响应。curl 请求中的地址需要更改为反映更新后的网址:
curl -X POST \
0.0.0.0:80/prediction \
-H 'Content-Type: application/json' \
-d '["The book was very poor", "Very nice!", "The author could have done more", "Amazing product!"]'
注意
如果此时收到权限错误,请在 app.py 中的 app.run() 命令中将端口号更改为 5000。(端口 80 是特权端口,因此将其更改为非特权端口,例如 5000)。但是,请确保在验证代码正常工作后将端口改回 80。
使用 Docker 将 Flask Web 应用程序打包成容器
DS 团队打算在云平台(即 AWS EC2)上托管的虚拟机上运行 Web 服务。为了将 EC2 操作系统与代码环境隔离,Docker 提供了容器化作为解决方案。我们将在这里使用它。
注意
要了解 Docker 的基础知识以及如何安装和使用它,可以参考 docker-curriculum.com/。
按照以下步骤将应用程序部署到容器中:
-
我们首先需要一个 requirements.txt 文件,列出运行 Python 代码所需的特定包:
Flask==1.0.2 numpy==1.14.1 keras==2.2.4 tensorflow==1.10.0 -
我们需要一个包含指令的
Dockerfile,以便 Docker 守护进程可以构建 Docker 镜像:FROM python:3.6-slim COPY ./app.py /deploy/ COPY ./requirements.txt /deploy/ COPY ./trained_model.h5 /deploy/ COPY ./trained_tokenizer.pkl /deploy/ WORKDIR /deploy/ RUN pip install -r requirements.txt EXPOSE 80 ENTRYPOINT ["python", "app.py"]Docker 镜像是从 Python dockerhub 仓库拉取的。在这里,执行了 Dockerfile。app.py、requirements.txt、tokenizer pickle 文件和 trained model 被使用 COPY 命令复制到 Docker 镜像中。为了将工作目录更改为“deploy”目录(文件已复制到该目录中),使用 WORKDIR 命令。接着,
RUN命令安装了 Dockerfile 中提到的 Python 包。由于端口 80 需要在容器外部进行访问,因此使用了EXPOSE命令。注意
Docker Hub 链接可以在 hub.docker.com/_/python 找到。
-
接下来,应使用
docker build命令构建 Docker 镜像:docker build -f Dockerfile -t app-packt .别忘了命令中的句点。命令的输出如下:
图 9.19:docker build 的输出截图
'
app-packt' 是生成的 Docker 镜像的名称。 -
现在,可以通过发出
docker run命令来将 Docker 镜像作为容器运行:docker run -p 80:80 app-packtp 标志用于在本地系统的端口 80 和 Docker 容器的端口 80 之间进行端口映射。(如果本地使用 5000 端口,请将命令中的端口映射部分改为 5000:80。验证 Docker 容器正常工作后,请按照说明将映射更改回 80:80。)以下截图展示了
docker run命令的输出:
图 9.20:docker run 命令的输出截图
现在可以发出与上一部分完全相同的 curl 请求来验证应用程序是否正常工作。
现在,应用程序代码已经准备好部署到 AWS EC2 上。
在亚马逊 Web 服务(AWS)EC2 实例上托管容器
DS 团队现在拥有一个在本地系统上运行的容器化应用程序。由于该应用程序仍在本地,Web 开发团队仍然无法使用它。根据最初的 MVP 定义,DS 团队现在开始使用 AWS EC2 实例来部署该应用程序。部署将确保 Web 服务可供 Web 开发团队使用。
作为前提条件,您需要有一个 AWS 账户才能使用 EC2 实例。为了演示,我们将使用一个't2.small' EC2 实例类型。此实例在撰写时每小时约收费 2 美分(USD)。请注意,此实例不属于免费套餐。默认情况下,您的 AWS 区域内将无法使用此实例,您需要提出请求将此实例添加到您的账户中。通常需要几个小时。或者,检查您 AWS 区域的实例限制,并选择另一个至少有 2GB 内存的实例。一个简单的't2.micro'实例无法满足我们的需求,因为它只有 1GB 内存。
注意
AWS 账户的链接可以在 aws.amazon.com/premiumsupp…
要添加实例并检查实例限制,请参阅 docs.aws.amazon.com/AWSEC2/late…
让我们从部署过程开始:
-
登录到 AWS 管理控制台后,在搜索栏中搜索'ec2'。这将带您进入 EC2 仪表板,如下图所示:
图 9.21:AWS 管理控制台中的 AWS 服务
-
需要创建一个密钥对来访问 AWS 资源。要创建一个,请查找以下面板并选择'密钥对'。这将允许您创建一个新的密钥对:
图 9.22:AWS 控制台上的网络与安全
-
下载了一个'
.pem'文件,这是密钥文件。请确保安全保存pem文件,并使用以下命令更改其模式:chmod 400 key-file-name.pem需要更改文件权限为私有。
-
要配置实例,请在 EC2 仪表板上选择'启动实例':
图 9.23:AWS 控制台上的资源
-
接下来,选择亚马逊机器实例(AMI),它会选择 EC2 实例运行的操作系统。我们将使用'Amazon Linux 2 AMI':
注意
要了解有关 Amazon Linux 2 AMI 的更多信息,请参阅 aws.amazon.com/amazon-linu…
图 9.24:亚马逊机器实例(AMI)
-
现在,我们选择 EC2 的硬件部分,即't2.small'实例:
图 9.25:选择 AMI 上的实例类型
-
点击 'Review and Launch' 将进入第 7 步——审查实例启动屏幕:
图 9.26:审查实例启动屏幕
-
现在,为了使 Web 服务可达,需要修改安全组。为此,需要创建一条规则。最后,你应该看到以下屏幕:
图 9.27:配置安全组
注意
可以通过 AWS 文档进一步了解安全组和配置:docs.aws.amazon.com/AWSEC2/late…
-
接下来,点击 'Launch' 图标将触发重定向到 Launch 屏幕:
图 9.28:AWS 实例上的启动状态
'View Instance' 按钮用于导航到显示正在启动的 EC2 实例的屏幕,当实例状态变为“running”时,表示实例已准备好使用。
-
接下来,通过以下命令从本地系统终端访问 EC2,替换 '
public-dns-name' 字段为你的 EC2 实例名称(格式为:ec2–x–x–x–x.compute-1.amazonaws.com)和之前保存的密钥对pem文件的路径:ssh -i /path/my-key-pair.pem ec2-user@public-dns-name此命令将带你进入 EC2 实例的提示符,首先需要在 EC2 实例中安装 Docker。由于 Docker 镜像将在 EC2 实例中构建,因此安装 Docker 是工作流所必需的。
-
对于 Amazon Linux 2 AMI,应使用以下命令来完成此操作:
sudo amazon-linux-extras install docker sudo yum install docker sudo service docker start sudo usermod -a -G docker ec2-user注意
有关命令的解释,请查阅文档 docs.aws.amazon.com/AmazonECS/l…
-
应该使用 '
exit' 命令退出实例。接下来,使用之前使用的ssh命令重新登录。通过执行 'docker info' 命令验证 Docker 是否正常工作。为接下来的步骤打开另一个本地终端窗口。 -
现在,将构建 Docker 镜像所需的文件复制到 EC2 实例内。从本地终端(不是 EC2 内部!)发出以下命令:
scp -i /path/my-key-pair.pem file-to-copy ec2-user@public-dns-name:/home/ec2-user -
应复制以下文件以构建 Docker 镜像,如之前所做:requirements.txt,app.py,trained_model.h5,trained_tokenizer.pkl 和 Dockerfile。
-
接下来,登录到 EC2 实例,执行 '
ls' 命令查看复制的文件是否存在,然后使用与本地系统相同的命令构建并运行 Docker 镜像(确保在代码/命令中的所有位置使用端口 80)。 -
从本地浏览器通过公共 DNS 名称进入首页端点,查看 'Hello World!' 消息:
图 9.29:首页端点截图
-
现在,你可以从本地终端发送 curl 请求到 Web 服务,测试示例数据,并将
public-dns-name替换为你的值:curl -X POST \ public-dns-name:80/predict \ -H 'Content-Type: application/json' \ -d '["The book was very poor", "Very nice!", "The author could have done more", "Amazing product!"]' -
这应该返回与本地获得的相同的评论评分。
这就结束了简单的部署过程。
DS 团队现在将此 curl 请求与 Web 开发团队共享,后者可以使用他们的测试样本来调用该 Web 服务。
注意
当不再需要 Web 服务时,停止或终止 EC2 实例,以避免产生费用。
图 9.30:停止 AWS EC2 实例
从 MVP 的角度来看,交付物现在已经完成!
改进
本章描述的工作流程仅仅是为了介绍一个使用特定工具(Flask、Colab、Docker 和 AWS EC2)的基本工作流程,并为组织中的深度学习项目提供一个示范计划。然而,这仅仅是一个 MVP(最小可行产品),未来的迭代可以在许多方面进行改进。
总结
在本章中,我们看到了深度学习项目在组织中的发展历程。我们还学习了如何使用 Google Colab 笔记本来利用 GPU 加速训练。此外,我们开发了一个基于 Flask 的 Web 服务,使用 Docker 部署到云环境,从而使利益相关者能够为给定输入获取预测结果。
本章总结了我们如何利用深度学习技术解决自然语言处理领域问题的学习过程。本章以及前几章讨论的几乎每个方面都是研究课题,并且正在不断改进。保持信息更新的唯一方法是不断学习新的、令人兴奋的解决问题的方式。一些常见的做法是通过社交媒体跟踪讨论,关注顶尖研究人员/深度学习从业者的工作,并时刻留意那些在该领域进行前沿研究的组织。
第十一章:附录
关于
本节内容是为了帮助学习者完成书中的活动,内容包括学习者需要执行的详细步骤,以完成并实现书本中的目标。
第一章:自然语言处理简介
活动 1:使用 Word2Vec 从语料库生成词嵌入。
解决方案:
-
从上述链接上传文本语料。
-
从 gensim 模型中导入 word2vec
from gensim.models import word2vec -
将语料库存储在一个变量中。
sentences = word2vec.Text8Corpus('text8') -
在语料库上拟合 word2vec 模型。
model = word2vec.Word2Vec(sentences, size = 200) -
找到与‘man’最相似的词。
model.most_similar(['man'])输出如下:
图 1.29:相似词嵌入的输出
-
‘Father’对应‘girl’,‘x’对应 boy。找出 x 的前三个词。
model.most_similar(['girl', 'father'], ['boy'], topn=3)输出如下:
图 1.30:‘x’的前三个词的输出
第二章:自然语言处理的应用
活动 2:构建并训练你自己的词性标注器
解决方案:
-
第一件事是选择我们想要训练标注器的语料。导入必要的 Python 包。在这里,我们使用
nltktreebank语料库进行操作:import nltk nltk.download('treebank') tagged_sentences = nltk.corpus.treebank.tagged_sents() print(tagged_sentences[0]) print("Tagged sentences: ", len(tagged_sentences)) print ("Tagged words:", len(nltk.corpus.treebank.tagged_words())) -
接下来,我们需要确定我们的标注器在为一个词分配标签时会考虑哪些特征。这些特征可以包括该词是否全大写、是否小写或是否有一个大写字母:
def features(sentence, index): """ sentence: [w1, w2, ...], index: the index of the word """ return { 'word': sentence[index], 'is_first': index == 0, 'is_last': index == len(sentence) - 1, 'is_capitalized': sentence[index][0].upper() == sentence[index][0], 'is_all_caps': sentence[index].upper() == sentence[index], 'is_all_lower': sentence[index].lower() == sentence[index], 'prefix-1': sentence[index][0], 'prefix-2': sentence[index][:2], 'prefix-3': sentence[index][:3], 'suffix-1': sentence[index][-1], 'suffix-2': sentence[index][-2:], 'suffix-3': sentence[index][-3:], 'prev_word': '' if index == 0 else sentence[index - 1], 'next_word': '' if index == len(sentence) - 1 else sentence[index + 1], 'has_hyphen': '-' in sentence[index], 'is_numeric': sentence[index].isdigit(), 'capitals_inside': sentence[index][1:].lower() != sentence[index][1:] } import pprint pprint.pprint(features(['This', 'is', 'a', 'sentence'], 2)) {'capitals_inside': False, 'has_hyphen': False, 'is_all_caps': False, 'is_all_lower': True, 'is_capitalized': False, 'is_first': False, 'is_last': False, 'is_numeric': False, 'next_word': 'sentence', 'prefix-1': 'a', 'prefix-2': 'a', 'prefix-3': 'a', 'prev_word': 'is', 'suffix-1': 'a', 'suffix-2': 'a', 'suffix-3': 'a', 'word': 'a'} -
创建一个函数来剥离标注词的标签,以便我们可以将它们输入到标注器中:
def untag(tagged_sentence): return [w for w, t in tagged_sentence] -
现在我们需要构建我们的训练集。我们的标注器需要为每个词单独提取特征,但我们的语料库实际上是句子的形式,所以我们需要做一些转换。将数据拆分为训练集和测试集。对训练集应用此函数。
# Split the dataset for training and testing cutoff = int(.75 * len(tagged_sentences)) training_sentences = tagged_sentences[:cutoff] test_sentences = tagged_sentences[cutoff:] print(len(training_sentences)) # 2935 print(len(test_sentences)) # 979 and create a function to assign the features to 'X' and append the POS tags to 'Y'. def transform_to_dataset(tagged_sentences): X, y = [], [] for tagged in tagged_sentences: for index in range(len(tagged)): X.append(features(untag(tagged), index)) y.append(tagged[index][1]) return X, y X, y = transform_to_dataset(training_sentences) from sklearn.tree import DecisionTreeClassifier from sklearn.feature_extraction import DictVectorizer from sklearn.pipeline import Pipeline -
在训练集上应用此函数。现在我们可以训练我们的标注器。它本质上是一个分类器,因为它将词分类到不同的类别中,所以我们可以使用分类算法。你可以使用任何你喜欢的算法,或者尝试多个看看哪个效果最好。这里,我们将使用决策树分类器。导入分类器,初始化它,并将模型拟合到训练数据上。然后打印准确率分数。
clf = Pipeline([ ('vectorizer', DictVectorizer(sparse=False)), ('classifier', DecisionTreeClassifier(criterion='entropy')) ]) clf.fit(X[:10000], y[:10000]) # Use only the first 10K samples if you're running it multiple times. It takes a fair bit :) print('Training completed') X_test, y_test = transform_to_dataset(test_sentences) print("Accuracy:", clf.score(X_test, y_test))输出如下:
图 2.19:准确率分数
活动 3:在标注语料上执行命名实体识别(NER)
解决方案:
-
导入必要的 Python 包和类。
import nltk nltk.download('treebank') nltk.download('maxent_ne_chunker') nltk.download('words') -
打印
nltk.corpus.treebank.tagged_sents()查看你需要从中提取命名实体的标注语料。nltk.corpus.treebank.tagged_sents() sent = nltk.corpus.treebank.tagged_sents()[0] print(nltk.ne_chunk(sent, binary=True)) -
将标注句子的第一句存储在一个变量中。
sent = nltk.corpus.treebank.tagged_sents()[1] -
使用
nltk.ne_chunk对句子执行命名实体识别(NER)。将 binary 设置为 True 并打印命名实体。print(nltk.ne_chunk(sent, binary=False)) sent = nltk.corpus.treebank.tagged_sents()[2] rint(nltk.ne_chunk(sent))输出如下:
图 2.20:在标注语料上的命名实体识别(NER)
第三章:神经网络简介
活动 4:评论的情感分析
解决方案:
-
打开一个新的
Jupyter笔记本。导入numpy、pandas和matplotlib.pyplot。将数据集加载到数据框中。import numpy as np import matplotlib.pyplot as plt import pandas as pd dataset = pd.read_csv('train_comment_small_100.csv', sep=',') -
下一步是清理和准备数据。导入
re和nltk。从nltk.corpus导入stopwords。从nltk.stem.porter导入PorterStemmer。创建一个数组来存储清理后的文本。import re import nltk nltk.download('stopwords') from nltk.corpus import stopwords from nltk.stem.porter import PorterStemmer corpus = [] -
使用 for 循环,遍历每个实例(每个评论)。将所有非字母字符替换为
' '(空格)。将所有字母转换为小写。将每条评论拆分成单个单词。初始化PorterStemmer。如果该单词不是停用词,则对该单词进行词干提取。将所有单个单词重新组合成一条清理后的评论。将这条清理后的评论附加到你创建的数组中。for i in range(0, dataset.shape[0]-1): review = re.sub('[^a-zA-Z]', ' ', dataset['comment_text'][i]) review = review.lower() review = review.split() ps = PorterStemmer() review = [ps.stem(word) for word in review if not word in set(stopwords.words('english'))] review = ' '.join(review) corpus.append(review) -
导入
CountVectorizer。使用CountVectorizer将评论转换为词频向量。from sklearn.feature_extraction.text import CountVectorizer cv = CountVectorizer(max_features = 20) -
创建一个数组来存储每个唯一的单词作为独立的列,从而使它们成为独立变量。
X = cv.fit_transform(corpus).toarray() y = dataset.iloc[:,0] y1 = y[:99] y1 -
从
sklearn.preprocessing导入LabelEncoder。对目标输出(y)使用LabelEncoder。from sklearn import preprocessing labelencoder_y = preprocessing.LabelEncoder() y = labelencoder_y.fit_transform(y1) -
导入
train_test_split。将数据集划分为训练集和验证集。from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.20, random_state = 0) -
从
sklearn.preprocessing导入StandardScaler。对训练集和验证集(X)的特征使用StandardScaler。from sklearn.preprocessing import StandardScaler sc = StandardScaler() X_train = sc.fit_transform(X_train) X_test = sc.transform(X_test) -
现在下一个任务是创建神经网络。导入
keras。从keras.models导入Sequential,从 Keras 层导入Dense。import tensorflow import keras from keras.models import Sequential from keras.layers import Dense -
初始化神经网络。添加第一个隐藏层,激活函数使用
'relu'。对第二个隐藏层重复此步骤。添加输出层,激活函数使用'softmax'。编译神经网络,使用'adam'作为优化器,'binary_crossentropy'作为损失函数,'accuracy'作为性能评估标准。classifier = Sequential() classifier.add(Dense(output_dim = 20, init = 'uniform', activation = 'relu', input_dim = 20)) classifier.add(Dense(output_dim =20, init = 'uniform', activation = 'relu')) classifier.add(Dense(output_dim = 1, init = 'uniform', activation = 'softmax')) classifier.compile(optimizer = 'adam', loss = 'binary_crossentropy', metrics = ['accuracy']) -
现在我们需要训练模型。使用
batch_size为 3 和nb_epoch为 5,在训练数据集上拟合神经网络。classifier.fit(X_train, y_train, batch_size = 3, nb_epoch = 5) X_test -
验证模型。评估神经网络并打印准确度得分,以查看其表现如何。
y_pred = classifier.predict(X_test) scores = classifier.evaluate(X_test, y_pred, verbose=1) print("Accuracy:", scores[1]) -
(可选)通过从
sklearn.metrics导入confusion_matrix打印混淆矩阵。from sklearn.metrics import confusion_matrix cm = confusion_matrix(y_test, y_pred) scores你的输出应类似于此:
图 3.21:情感分析的准确度得分
第四章:卷积网络介绍
活动 5:对真实数据集进行情感分析
解决方案:
-
导入必要的类
from keras.preprocessing.text import Tokenizer from keras.models import Sequential from keras import layers from keras.preprocessing.sequence import pad_sequences import numpy as np import pandas as pd -
定义你的变量和参数。
epochs = 20 maxlen = 100 embedding_dim = 50 num_filters = 64 kernel_size = 5 batch_size = 32 -
导入数据。
data = pd.read_csv('data/sentiment labelled sentences/yelp_labelled.txt',names=['sentence', 'label'], sep='\t') data.head()在
Jupyter笔记本中打印此内容应显示:![图 4.27:带标签的数据集
图 4.27:带标签的数据集
-
选择
'sentence'和'label'列sentences=data['sentence'].values labels=data['label'].values -
将数据拆分为训练集和测试集
from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( sentences, labels, test_size=0.30, random_state=1000) -
分词
tokenizer = Tokenizer(num_words=5000) tokenizer.fit_on_texts(X_train) X_train = tokenizer.texts_to_sequences(X_train) X_test = tokenizer.texts_to_sequences(X_test) vocab_size = len(tokenizer.word_index) + 1 #The vocabulary size has an additional 1 due to the 0 reserved index -
填充数据以确保所有序列的长度相同
X_train = pad_sequences(X_train, padding='post', maxlen=maxlen) X_test = pad_sequences(X_test, padding='post', maxlen=maxlen) -
创建模型。注意,我们在最后一层使用 sigmoid 激活函数,并使用二元交叉熵来计算损失。这是因为我们正在进行二分类。
model = Sequential() model.add(layers.Embedding(vocab_size, embedding_dim, input_length=maxlen)) model.add(layers.Conv1D(num_filters, kernel_size, activation='relu')) model.add(layers.GlobalMaxPooling1D()) model.add(layers.Dense(10, activation='relu')) model.add(layers.Dense(1, activation='sigmoid')) model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy']) model.summary()上述代码应该输出
图 4.28:模型总结
模型也可以如下所示进行可视化:
图 4.29:模型可视化
-
训练并测试模型。
model.fit(X_train, y_train, epochs=epochs, verbose=False, validation_data=(X_test, y_test), batch_size=batch_size) loss, accuracy = model.evaluate(X_train, y_train, verbose=False) print("Training Accuracy: {:.4f}".format(accuracy)) loss, accuracy = model.evaluate(X_test, y_test, verbose=False) print("Testing Accuracy: {:.4f}".format(accuracy))准确度输出应如下所示:
图 4.30:准确度评分
第五章:循环神经网络基础
活动 6:用 RNN 解决问题——作者归属
解决方案:
准备数据
我们首先设置数据预处理管道。对于每个作者,我们将所有已知的论文聚合为一篇长文。我们假设风格在不同论文中没有变化,因此一篇长文等同于多篇短文,而且从程序角度处理起来要更容易。
对于每个作者的每篇论文,我们执行以下步骤:
-
将所有文本转换为小写字母(忽略大小写可能是风格属性这一事实)
-
将所有换行符和多个空格转换为单个空格
-
删除任何涉及作者姓名的内容,否则我们可能会面临数据泄漏的风险(作者的名字是 hamilton 和 madison)
-
将上述步骤封装成一个函数,因为它在预测未知论文时是必需的。
import numpy as np import os from sklearn.model_selection import train_test_split # Classes for A/B/Unknown A = 0 B = 1 UNKNOWN = -1 def preprocess_text(file_path): with open(file_path, 'r') as f: lines = f.readlines() text = ' '.join(lines[1:]).replace("\n", ' ').replace(' ',' ').lower().replace('hamilton','').replace('madison', '') text = ' '.join(text.split()) return text # Concatenate all the papers known to be written by A/B into a single long text all_authorA, all_authorB = '','' for x in os.listdir('./papers/A/'): all_authorA += preprocess_text('./papers/A/' + x) for x in os.listdir('./papers/B/'): all_authorB += preprocess_text('./papers/B/' + x) # Print lengths of the large texts print("AuthorA text length: {}".format(len(all_authorA))) print("AuthorB text length: {}".format(len(all_authorB)))该输出应如下所示:
图 5.34:文本长度计数
下一步是将每个作者的长文本拆分为多个小序列。如上所述,我们经验性地选择一个序列长度,并在模型生命周期内始终使用该长度。我们通过为每个序列标注其作者来获取完整的数据集。
为了将长文本拆分为较小的序列,我们使用
keras框架中的Tokenizer类。特别地,注意我们将其设置为按字符而非按词进行标记。 -
选择
SEQ_LEN超参数,如果模型与训练数据不匹配,可能需要更改此参数。 -
编写一个函数
make_subsequences,将每个文档转化为长度为 SEQ_LEN 的序列,并赋予正确的标签。 -
使用 Keras
Tokenizer并设置char_level=True -
对所有文本进行分词器拟合
-
使用这个分词器将所有文本转换为序列,方法是使用
texts_to_sequences() -
使用
make_subsequences()将这些序列转化为合适的形状和长度from keras.preprocessing.text import Tokenizer # Hyperparameter - sequence length to use for the model SEQ_LEN = 30 def make_subsequences(long_sequence, label, sequence_length=SEQ_LEN): len_sequences = len(long_sequence) X = np.zeros(((len_sequences - sequence_length)+1, sequence_length)) y = np.zeros((X.shape[0], 1)) for i in range(X.shape[0]): X[i] = long_sequence[i:i+sequence_length] y[i] = label return X,y # We use the Tokenizer class from Keras to convert the long texts into a sequence of characters (not words) tokenizer = Tokenizer(char_level=True) # Make sure to fit all characters in texts from both authors tokenizer.fit_on_texts(all_authorA + all_authorB) authorA_long_sequence = tokenizer.texts_to_sequences([all_authorA])[0] authorB_long_sequence = tokenizer.texts_to_sequences([all_authorB])[0] # Convert the long sequences into sequence and label pairs X_authorA, y_authorA = make_subsequences(authorA_long_sequence, A) X_authorB, y_authorB = make_subsequences(authorB_long_sequence, B) # Print sizes of available data print("Number of characters: {}".format(len(tokenizer.word_index))) print('author A sequences: {}'.format(X_authorA.shape)) print('author B sequences: {}'.format(X_authorB.shape))输出应如下所示:
图 5.35:序列的字符计数
-
比较每个作者的原始字符数与标注序列数。深度学习需要大量的每种输入的示例。以下代码计算文本中的总词数和唯一词数。
# Calculate the number of unique words in the text word_tokenizer = Tokenizer() word_tokenizer.fit_on_texts([all_authorA, all_authorB]) print("Total word count: ", len((all_authorA + ' ' + all_authorB).split(' '))) print("Total number of unique words: ", len(word_tokenizer.word_index))输出应如下所示:
图 5.36:总词数和唯一词数
我们现在开始创建我们的训练集和验证集。
-
将
x数据堆叠在一起,将y数据堆叠在一起。 -
使用
train_test_split将数据集分割为 80% 的训练集和 20% 的验证集。 -
重塑数据,确保它们是正确长度的序列。
# Take equal amounts of sequences from both authors X = np.vstack((X_authorA, X_authorB)) y = np.vstack((y_authorA, y_authorB)) # Break data into train and test sets X_train, X_val, y_train, y_val = train_test_split(X,y, train_size=0.8) # Data is to be fed into RNN - ensure that the actual data is of size [batch size, sequence length] X_train = X_train.reshape(-1, SEQ_LEN) X_val = X_val.reshape(-1, SEQ_LEN) # Print the shapes of the train, validation and test sets print("X_train shape: {}".format(X_train.shape)) print("y_train shape: {}".format(y_train.shape)) print("X_validate shape: {}".format(X_val.shape)) print("y_validate shape: {}".format(y_val.shape))输出如下:
图 5.37: 测试集和训练集
最后,我们构建模型图并执行训练过程。
-
使用
RNN和Dense层创建模型。 -
由于这是一个二分类问题,输出层应为
Dense,并使用sigmoid激活函数。 -
使用
optimizer,适当的损失函数和指标编译模型。 -
打印模型摘要。
from keras.layers import SimpleRNN, Embedding, Dense from keras.models import Sequential from keras.optimizers import SGD, Adadelta, Adam Embedding_size = 100 RNN_size = 256 model = Sequential() model.add(Embedding(len(tokenizer.word_index)+1, Embedding_size, input_length=30)) model.add(SimpleRNN(RNN_size, return_sequences=False)) model.add(Dense(1, activation='sigmoid')) model.compile(optimizer='adam', loss='binary_crossentropy', metrics = ['accuracy']) model.summary()输出如下:
图 5.38: 模型摘要
-
确定批量大小、训练周期,并使用训练数据训练模型,使用验证数据进行验证。
-
根据结果,返回上述模型,必要时进行更改(使用更多层,使用正则化,dropout 等,使用不同的优化器,或不同的学习率等)。
-
如有需要,修改
Batch_size、epochs。Batch_size = 4096 Epochs = 20 model.fit(X_train, y_train, batch_size=Batch_size, epochs=Epochs, validation_data=(X_val, y_val))输出如下:
图 5.39: 训练周期
将模型应用于未知论文
对“Unknown”文件夹中的所有论文进行此操作。
-
以与训练集相同的方式预处理它们(小写,去除空白行等)。
-
使用
tokenizer和上述make_subsequences函数将它们转换为所需大小的序列。 -
使用模型对这些序列进行预测。
-
计算分配给作者 A 和作者 B 的序列数。
-
根据计数,选择具有最高票数/计数的作者。
for x in os.listdir('./papers/Unknown/'): unknown = preprocess_text('./papers/Unknown/' + x) unknown_long_sequences = tokenizer.texts_to_sequences([unknown])[0] X_sequences, _ = make_subsequences(unknown_long_sequences, UNKNOWN) X_sequences = X_sequences.reshape((-1,SEQ_LEN)) votes_for_authorA = 0 votes_for_authorB = 0 y = model.predict(X_sequences) y = y>0.5 votes_for_authorA = np.sum(y==0) votes_for_authorB = np.sum(y==1) print("Paper {} is predicted to have been written by {}, {} to {}".format( x.replace('paper_','').replace('.txt',''), ("Author A" if votes_for_authorA > votes_for_authorB else "Author B"), max(votes_for_authorA, votes_for_authorB), min(votes_for_authorA, votes_for_authorB)))输出如下:
图 5.40: 作者归属输出
第六章:GRU 基础
活动 7:使用简单 RNN 开发情感分类模型
解决方案:
-
加载数据集。
from keras.datasets import imdb max_features = 10000 maxlen = 500 (train_data, y_train), (test_data, y_test) = imdb.load_data(num_words=max_features) print('Number of train sequences: ', len(train_data)) print('Number of test sequences: ', len(test_data)) -
填充序列,以确保每个序列具有相同数量的字符。
from keras.preprocessing import sequence train_data = sequence.pad_sequences(train_data, maxlen=maxlen) test_data = sequence.pad_sequences(test_data, maxlen=maxlen) -
使用 32 个隐藏单元的
SimpleRNN定义并编译模型。from keras.models import Sequential from keras.layers import Embedding from keras.layers import Dense from keras.layers import GRU from keras.layers import SimpleRNN model = Sequential() model.add(Embedding(max_features, 32)) model.add(SimpleRNN(32)) model.add(Dense(1, activation='sigmoid')) model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc']) history = model.fit(train_data, y_train, epochs=10, batch_size=128, validation_split=0.2) -
绘制验证和训练准确度及损失。
import matplotlib.pyplot as plt def plot_results(history): acc = history.history['acc'] val_acc = history.history['val_acc'] loss = history.history['loss'] val_loss = history.history['val_loss'] epochs = range(1, len(acc) + 1) plt.plot(epochs, acc, 'bo', label='Training Accuracy') plt.plot(epochs, val_acc, 'b', label='Validation Accuracy') plt.title('Training and validation Accuracy') plt.legend() plt.figure() plt.plot(epochs, loss, 'bo', label='Training Loss') plt.plot(epochs, val_loss, 'b', label='Validation Loss') plt.title('Training and validation Loss') plt.legend() plt.show() -
绘制模型图。
plot_results(history)输出如下:
](tos-cn-i-73owjymdk6/f68b461bd93b48b98fb4512e519ca530)
图 6.29: 训练和验证准确度损失
活动 8:使用您选择的数据集训练自己的字符生成模型。
解决方案:
-
加载文本文件,并导入必要的 Python 包和类。
import sys import random import string import numpy as np from keras.models import Sequential from keras.layers import Dense from keras.layers import LSTM, GRU from keras.optimizers import RMSprop from keras.models import load_model # load text def load_text(filename): with open(filename, 'r') as f: text = f.read() return text in_filename = 'drive/shakespeare_poems.txt' # Add your own text file here text = load_text(in_filename) print(text[:200])输出如下:
图 6.30: 莎士比亚的十四行诗
-
创建字典,将字符映射到索引,并反向映射。
chars = sorted(list(set(text))) print('Number of distinct characters:', len(chars)) char_indices = dict((c, i) for i, c in enumerate(chars)) indices_char = dict((i, c) for i, c in enumerate(chars))输出如下:
图 6.31: 不同字符计数
-
从文本中创建序列。
max_len_chars = 40 step = 3 sentences = [] next_chars = [] for i in range(0, len(text) - max_len_chars, step): sentences.append(text[i: i + max_len_chars]) next_chars.append(text[i + max_len_chars]) print('nb sequences:', len(sentences))输出如下:
图 6.32: 序列计数
-
创建输入和输出数组,以供模型使用。
x = np.zeros((len(sentences), max_len_chars, len(chars)), dtype=np.bool) y = np.zeros((len(sentences), len(chars)), dtype=np.bool) for i, sentence in enumerate(sentences): for t, char in enumerate(sentence): x[i, t, char_indices[char]] = 1 y[i, char_indices[next_chars[i]]] = 1 -
使用 GRU 构建并训练模型,并保存该模型。
print('Build model...') model = Sequential() model.add(GRU(128, input_shape=(max_len_chars, len(chars)))) model.add(Dense(len(chars), activation='softmax')) optimizer = RMSprop(lr=0.01) model.compile(loss='categorical_crossentropy', optimizer=optimizer) model.fit(x, y,batch_size=128,epochs=10) model.save("poem_gen_model.h5") -
定义采样和生成函数。
def sample(preds, temperature=1.0): # helper function to sample an index from a probability array preds = np.asarray(preds).astype('float64') preds = np.log(preds) / temperature exp_preds = np.exp(preds) preds = exp_preds / np.sum(exp_preds) probas = np.random.multinomial(1, preds, 1) return np.argmax(probas) -
生成文本。
from keras.models import load_model model_loaded = load_model('poem_gen_model.h5') def generate_poem(model, num_chars_to_generate=400): start_index = random.randint(0, len(text) - max_len_chars - 1) generated = '' sentence = text[start_index: start_index + max_len_chars] generated += sentence print("Seed sentence: {}".format(generated)) for i in range(num_chars_to_generate): x_pred = np.zeros((1, max_len_chars, len(chars))) for t, char in enumerate(sentence): x_pred[0, t, char_indices[char]] = 1. preds = model.predict(x_pred, verbose=0)[0] next_index = sample(preds, 1) next_char = indices_char[next_index] generated += next_char sentence = sentence[1:] + next_char return generated generate_poem(model_loaded, 100)输出如下:
](tos-cn-i-73owjymdk6/fe0dac2a9a274766b1fafc9eb971ab76)
图 6.33:生成的文本输出
第七章:LSTM 基础
活动 9:使用简单的 RNN 构建垃圾邮件或正常邮件分类器。
解决方案:
-
导入所需的 Python 包。
import pandas as pd import numpy as np from keras.models import Model, Sequential from keras.layers import SimpleRNN, Dense,Embedding from keras.preprocessing.text import Tokenizer from keras.preprocessing import sequence -
读取输入文件,文件中包含一列文本和另一列标签,标签表示该文本是否为垃圾邮件。
df = pd.read_csv("drive/spam.csv", encoding="latin") df.head()输出如下:
图 7.35:输入数据文件
-
标注输入数据中的列。
df = df[["v1","v2"]] df.head()输出如下:
图 7.36:标注的输入数据
-
计算
v1列中垃圾邮件和正常邮件字符的数量。df["v1"].value_counts()输出如下:
图 7.37:垃圾邮件或正常邮件的值计数
-
获取
X作为特征,Y作为目标。lab_map = {"ham":0, "spam":1} X = df["v2"].values Y = df["v1"].map(lab_map).values -
转换为序列并填充序列。
max_words = 100 mytokenizer = Tokenizer(nb_words=max_words,lower=True, split=" ") mytokenizer.fit_on_texts(X) text_tokenized = mytokenizer.texts_to_sequences(X) text_tokenized输出如下:
图 7.38:分词数据
-
训练序列。
max_len = 50 sequences = sequence.pad_sequences(text_tokenized,maxlen=max_len) sequences -
构建模型。
model = Sequential() model.add(Embedding(max_words, 20, input_length=max_len)) model.add(SimpleRNN(64)) model.add(Dense(1, activation="sigmoid")) model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) model.fit(sequences,Y,batch_size=128,epochs=10, validation_split=0.2) -
对新测试数据预测邮件类别。
inp_test_seq = "WINNER! U win a 500 prize reward & free entry to FA cup final tickets! Text FA to 34212 to receive award" test_sequences = mytokenizer.texts_to_sequences(np.array([inp_test_seq])) test_sequences_matrix = sequence.pad_sequences(test_sequences,maxlen=max_len) model.predict(test_sequences_matrix)输出如下:
图 7.39:新测试数据的输出
活动 10:创建法语到英语的翻译模型
解决方案:
-
导入必要的 Python 包和类。
import os import re import numpy as np -
以句子对的形式读取文件。
with open("fra.txt", 'r', encoding='utf-8') as f: lines = f.read().split('\n') num_samples = 20000 # Using only 20000 pairs for this example lines_to_use = lines[: min(num_samples, len(lines) - 1)] -
移除
\u202f字符。for l in range(len(lines_to_use)): lines_to_use[l] = re.sub("\u202f", "", lines_to_use[l]) for l in range(len(lines_to_use)): lines_to_use[l] = re.sub("\d", " NUMBER_PRESENT ", lines_to_use[l]) -
在目标序列中附加
**BEGIN_**和**_END**词,将词映射到整数。input_texts = [] target_texts = [] input_words = set() target_words = set() for line in lines_to_use: target_text, input_text = line.split('\t') target_text = 'BEGIN_ ' + target_text + ' _END' input_texts.append(input_text) target_texts.append(target_text) for word in input_text.split(): if word not in input_words: input_words.add(word) for word in target_text.split(): if word not in target_words: target_words.add(word) max_input_seq_length = max([len(i.split()) for i in input_texts]) max_target_seq_length = max([len(i.split()) for i in target_texts]) input_words = sorted(list(input_words)) target_words = sorted(list(target_words)) num_encoder_tokens = len(input_words) num_decoder_tokens = len(target_words) -
定义编码器-解码器输入。
input_token_index = dict( [(word, i) for i, word in enumerate(input_words)]) target_token_index = dict( [(word, i) for i, word in enumerate(target_words)]) encoder_input_data = np.zeros( (len(input_texts), max_input_seq_length), dtype='float32') decoder_input_data = np.zeros( (len(target_texts), max_target_seq_length), dtype='float32') decoder_target_data = np.zeros( (len(target_texts), max_target_seq_length, num_decoder_tokens), dtype='float32') for i, (input_text, target_text) in enumerate(zip(input_texts, target_texts)): for t, word in enumerate(input_text.split()): encoder_input_data[i, t] = input_token_index[word] for t, word in enumerate(target_text.split()): decoder_input_data[i, t] = target_token_index[word] if t > 0: # decoder_target_data is ahead of decoder_input_data #by one timestep decoder_target_data[i, t - 1, target_token_index[word]] = 1. -
构建模型。
from keras.layers import Input, LSTM, Embedding, Dense from keras.models import Model embedding_size = 50 -
初始化编码器训练。
encoder_inputs = Input(shape=(None,)) encoder_after_embedding = Embedding(num_encoder_tokens, embedding_size)(encoder_inputs) encoder_lstm = LSTM(50, return_state=True)_, state_h, state_c = encoder_lstm(encoder_after_embedding) encoder_states = [state_h, state_c] -
初始化解码器训练。
decoder_inputs = Input(shape=(None,)) decoder_after_embedding = Embedding(num_decoder_tokens, embedding_size)(decoder_inputs) decoder_lstm = LSTM(50, return_sequences=True, return_state=True) decoder_outputs, _, _ = decoder_lstm(decoder_after_embedding, initial_state=encoder_states) decoder_dense = Dense(num_decoder_tokens, activation='softmax') decoder_outputs = decoder_dense(decoder_outputs) -
定义最终模型。
model = Model([encoder_inputs, decoder_inputs], decoder_outputs) model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['acc']) model.fit([encoder_input_data, decoder_input_data], decoder_target_data, batch_size=128, epochs=20, validation_split=0.05) -
将推理提供给编码器和解码器。
# encoder part encoder_model = Model(encoder_inputs, encoder_states) # decoder part decoder_state_input_h = Input(shape=(50,)) decoder_state_input_c = Input(shape=(50,)) decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c] decoder_outputs_inf, state_h_inf, state_c_inf = decoder_lstm(decoder_after_embedding, initial_state=decoder_states_inputs) decoder_states_inf = [state_h_inf, state_c_inf] decoder_outputs_inf = decoder_dense(decoder_outputs_inf) decoder_model = Model( [decoder_inputs] + decoder_states_inputs, [decoder_outputs_inf] + decoder_states_inf) -
使用反向查找标记索引解码序列。
reverse_input_word_index = dict( (i, word) for word, i in input_token_index.items()) reverse_target_word_index = dict( (i, word) for word, i in target_token_index.items()) def decode_sequence(input_seq): -
将输入编码为状态向量。
states_value = encoder_model.predict(input_seq) -
生成长度为 1 的空目标序列。
target_seq = np.zeros((1,1)) -
用开始字符填充目标序列的第一个字符。
target_seq[0, 0] = target_token_index['BEGIN_'] -
为一批序列进行采样循环。
stop_condition = False decoded_sentence = '' while not stop_condition: output_tokens, h, c = decoder_model.predict( [target_seq] + states_value) -
采样一个标记。
sampled_token_index = np.argmax(output_tokens) sampled_word = reverse_target_word_index[sampled_token_index] decoded_sentence += ' ' + sampled_word -
退出条件:达到最大长度或找到停止字符。
if (sampled_word == '_END' or len(decoded_sentence) > 60): stop_condition = True -
更新目标序列(长度为 1)。
target_seq = np.zeros((1,1)) target_seq[0, 0] = sampled_token_index -
更新状态。
states_value = [h, c] return decoded_sentence -
用户输入推理:接受一个词序列,逐个词地将序列转换为编码。
text_to_translate = "Où est ma voiture??" encoder_input_to_translate = np.zeros( (1, max_input_seq_length), dtype='float32') for t, word in enumerate(text_to_translate.split()): encoder_input_to_translate[0, t] = input_token_index[word] decode_sequence(encoder_input_to_translate)输出如下:
图 7.47:法语到英语翻译器
第八章:自然语言处理的最新进展
活动 11:构建文本摘要模型
解决方案:
-
导入必要的 Python 包和类。
import os import re import pdb import string import numpy as np import pandas as pd from keras.utils import to_categorical import matplotlib.pyplot as plt %matplotlib inline -
加载数据集并读取文件。
path_data = "news_summary_small.csv" df_text_file = pd.read_csv(path_data) df_text_file.headlines = df_text_file.headlines.str.lower() df_text_file.text = df_text_file.text.str.lower() lengths_text = df_text_file.text.apply(len) dataset = list(zip(df_text_file.text.values, df_text_file.headlines.values)) -
创建词汇字典。
input_texts = [] target_texts = [] input_chars = set() target_chars = set() for line in dataset: input_text, target_text = list(line[0]), list(line[1]) target_text = ['BEGIN_'] + target_text + ['_END'] input_texts.append(input_text) target_texts.append(target_text) for character in input_text: if character not in input_chars: input_chars.add(character) for character in target_text: if character not in target_chars: target_chars.add(character) input_chars.add("<unk>") input_chars.add("<pad>") target_chars.add("<pad>") input_chars = sorted(input_chars) target_chars = sorted(target_chars) human_vocab = dict(zip(input_chars, range(len(input_chars)))) machine_vocab = dict(zip(target_chars, range(len(target_chars)))) inv_machine_vocab = dict(enumerate(sorted(machine_vocab))) def string_to_int(string_in, length, vocab): """ Converts all strings in the vocabulary into a list of integers representing the positions of the input string's characters in the "vocab" Arguments: string -- input string length -- the number of time steps you'd like, determines if the output will be padded or cut vocab -- vocabulary, dictionary used to index every character of your "string" Returns: rep -- list of integers (or '<unk>') (size = length) representing the position of the string's character in the vocabulary """ -
转换为小写字母以标准化。
string_in = string_in.lower() string_in = string_in.replace(',','') if len(string_in) > length: string_in = string_in[:length] rep = list(map(lambda x: vocab.get(x, '<unk>'), string_in)) if len(string_in) < length: rep += [vocab['<pad>']] * (length - len(string_in)) return rep def preprocess_data(dataset, human_vocab, machine_vocab, Tx, Ty): X, Y = zip(*dataset) X = np.array([string_to_int(i, Tx, human_vocab) for i in X]) Y = [string_to_int(t, Ty, machine_vocab) for t in Y] print("X shape from preprocess: {}".format(X.shape)) Xoh = np.array(list(map(lambda x: to_categorical(x, num_classes=len(human_vocab)), X))) Yoh = np.array(list(map(lambda x: to_categorical(x, num_classes=len(machine_vocab)), Y))) return X, np.array(Y), Xoh, Yoh def softmax(x, axis=1): """Softmax activation function. # Arguments x : Tensor. axis: Integer, axis along which the softmax normalization is applied. # Returns Tensor, output of softmax transformation. # Raises ValueError: In case 'dim(x) == 1'. """ ndim = K.ndim(x) if ndim == 2: return K.softmax(x) elif ndim > 2: e = K.exp(x - K.max(x, axis=axis, keepdims=True)) s = K.sum(e, axis=axis, keepdims=True) return e / s else: raise ValueError('Cannot apply softmax to a tensor that is 1D') -
运行之前的代码片段以加载数据,获取词汇字典并定义一些稍后使用的工具函数。定义输入字符和输出字符的长度。
Tx = 460 Ty = 75 X, Y, Xoh, Yoh = preprocess_data(dataset, human_vocab, machine_vocab, Tx, Ty) Define the model functions (Repeator, Concatenate, Densors, Dotor) # Defined shared layers as global variables repeator = RepeatVector(Tx) concatenator = Concatenate(axis=-1) densor1 = Dense(10, activation = "tanh") densor2 = Dense(1, activation = "relu") activator = Activation(softmax, name='attention_weights') dotor = Dot(axes = 1) Define one-step-attention function: def one_step_attention(h, s_prev): """ Performs one step of attention: Outputs a context vector computed as a dot product of the attention weights "alphas" and the hidden states "h" of the Bi-LSTM. Arguments: h -- hidden state output of the Bi-LSTM, numpy-array of shape (m, Tx, 2*n_h) s_prev -- previous hidden state of the (post-attention) LSTM, numpy-array of shape (m, n_s) Returns: context -- context vector, input of the next (post-attetion) LSTM cell """ -
使用
repeator将s_prev重复为形状(m,Tx,n_s),以便可以将其与所有隐藏状态“a”连接。s_prev = repeator(s_prev) -
使用连接器在最后一个轴上连接
a和s_prev(≈ 1 行)concat = concatenator([h, s_prev]) -
使用
densor1通过一个小型全连接神经网络传播concat,以计算“中间能量”变量 e。e = densor1(concat) -
使用
densor2通过一个小型全连接神经网络传播 e,计算“能量”变量 energies。energies = densor2(e) -
使用“
activator”对“能量”计算注意力权重“alphas”alphas = activator(energies) -
使用
dotor与“alphas”和“a”一起计算要传递给下一个(后注意力)LSTM 单元的上下文向量context = dotor([alphas, h]) return context Define the number of hidden states for decoder and encoder. n_h = 32 n_s = 64 post_activation_LSTM_cell = LSTM(n_s, return_state = True) output_layer = Dense(len(machine_vocab), activation=softmax) Define the model architecture and run it to obtain a model. def model(Tx, Ty, n_h, n_s, human_vocab_size, machine_vocab_size): """ Arguments: Tx -- length of the input sequence Ty -- length of the output sequence n_h -- hidden state size of the Bi-LSTM n_s -- hidden state size of the post-attention LSTM human_vocab_size -- size of the python dictionary "human_vocab" machine_vocab_size -- size of the python dictionary "machine_vocab" Returns: model -- Keras model instance """ -
定义模型的输入,形状为(
Tx,) -
定义
s0和c0,解码器 LSTM 的初始隐藏状态,形状为(n_s,)X = Input(shape=(Tx, human_vocab_size), name="input_first") s0 = Input(shape=(n_s,), name='s0') c0 = Input(shape=(n_s,), name='c0') s = s0 c = c0 -
初始化空的输出列表
outputs = [] -
定义你的前注意力 Bi-LSTM。记得使用 return_sequences=True。
a = Bidirectional(LSTM(n_h, return_sequences=True))(X) # Iterate for Ty steps for t in range(Ty): # Perform one step of the attention mechanism to get back the context vector at step t context = one_step_attention(h, s) -
将后注意力 LSTM 单元应用于“
上下文”向量。# Pass: initial_state = [hidden state, cell state] s, _, c = post_activation_LSTM_cell(context, initial_state = [s,c]) -
将
Dense层应用于后注意力 LSTM 的隐藏状态输出out = output_layer(s) -
将“out”附加到“outputs”列表中
outputs.append(out) -
创建模型实例,接受三个输入并返回输出列表。
model = Model(inputs=[X, s0, c0], outputs=outputs) return model model = model(Tx, Ty, n_h, n_s, len(human_vocab), len(machine_vocab)) #Define model loss functions and other hyperparameters. Also #initialize decoder state vectors. opt = Adam(lr = 0.005, beta_1=0.9, beta_2=0.999, decay = 0.01) model.compile(loss='categorical_crossentropy', optimizer=opt, metrics=['accuracy']) s0 = np.zeros((10000, n_s)) c0 = np.zeros((10000, n_s)) outputs = list(Yoh.swapaxes(0,1)) Fit the model to our data: model.fit([Xoh, s0, c0], outputs, epochs=1, batch_size=100) #Run inference step for the new text. EXAMPLES = ["Last night a meteorite was seen flying near the earth's moon."] for example in EXAMPLES: source = string_to_int(example, Tx, human_vocab) source = np.array(list(map(lambda x: to_categorical(x, num_classes=len(human_vocab)), source))) source = source[np.newaxis, :] prediction = model.predict([source, s0, c0]) prediction = np.argmax(prediction, axis = -1) output = [inv_machine_vocab[int(i)] for i in prediction] print("source:", example) print("output:", ''.join(output))输出如下:
](tos-cn-i-73owjymdk6/04682d4330ff44ff972ca50ff9b85f93)
图 8.18:文本摘要模型输出
第九章:组织中的实际 NLP 项目工作流程
LSTM 模型的代码
-
检查是否检测到 GPU
import tensorflow as tf tf.test.gpu_device_name() -
设置领口笔记本
from google.colab import drive drive.mount('/content/gdrive') # Run the below command in a new cell cd /content/gdrive/My Drive/Lesson-9/ # Run the below command in a new cell !unzip data.csv.zip -
导入必要的 Python 包和类。
import os import re import pickle import pandas as pd from keras.preprocessing.text import Tokenizer from keras.preprocessing.sequence import pad_sequences from keras.models import Sequential from keras.layers import Dense, Embedding, LSTM -
加载数据文件。
def preprocess_data(data_file_path): data = pd.read_csv(data_file_path, header=None) # read the csv data.columns = ['rating', 'title', 'review'] # add column names data['review'] = data['review'].apply(lambda x: x.lower()) # change all text to lower data['review'] = data['review'].apply((lambda x: re.sub('[^a-zA-z0-9\s]','',x))) # remove all numbers return data df = preprocess_data('data.csv') -
初始化分词。
max_features = 2000 maxlength = 250 tokenizer = Tokenizer(num_words=max_features, split=' ') -
拟合分词器。
tokenizer.fit_on_texts(df['review'].values) X = tokenizer.texts_to_sequences(df['review'].values) -
填充序列。
X = pad_sequences(X, maxlen=maxlength) -
获取目标变量
y_train = pd.get_dummies(df.rating).values embed_dim = 128 hidden_units = 100 n_classes = 5 model = Sequential() model.add(Embedding(max_features, embed_dim, input_length = X.shape[1])) model.add(LSTM(hidden_units)) model.add(Dense(n_classes, activation='softmax')) model.compile(loss = 'categorical_crossentropy', optimizer='adam',metrics = ['accuracy']) print(model.summary()) -
拟合模型。
model.fit(X[:100000, :], y_train[:100000, :], batch_size = 128, epochs=15, validation_split=0.2) -
保存模型和分词器。
model.save('trained_model.h5') # creates a HDF5 file 'trained_model.h5' with open('trained_tokenizer.pkl', 'wb') as f: # creates a pickle file 'trained_tokenizer.pkl' pickle.dump(tokenizer, f) from google.colab import files files.download('trained_model.h5') files.download('trained_tokenizer.pkl')
Flask 的代码
-
导入必要的 Python 包和类。
import re import pickle import numpy as np from flask import Flask, request, jsonify from keras.models import load_model from keras.preprocessing.sequence import pad_sequences -
定义输入文件并加载到数据框中
def load_variables(): global model, tokenizer model = load_model('trained_model.h5') model._make_predict_function() # https://github.com/keras-team/keras/issues/6462 with open('trained_tokenizer.pkl', 'rb') as f: tokenizer = pickle.load(f) -
定义类似于训练代码的预处理函数:
def do_preprocessing(reviews): processed_reviews = [] for review in reviews: review = review.lower() processed_reviews.append(re.sub('[^a-zA-z0-9\s]', '', review)) processed_reviews = tokenizer.texts_to_sequences(np.array(processed_reviews)) processed_reviews = pad_sequences(processed_reviews, maxlen=250) return processed_reviews -
定义 Flask 应用实例:
app = Flask(__name__) -
定义一个显示固定消息的端点:
@app.route('/') def home_routine(): return 'Hello World!' -
我们将拥有一个预测端点,通过它可以发送我们的评论字符串。我们将使用的 HTTP 请求类型是'
POST'请求:@app.route('/prediction', methods=['POST']) def get_prediction(): # get incoming text # run the model if request.method == 'POST': data = request.get_json() data = do_preprocessing(data) predicted_sentiment_prob = model.predict(data) predicted_sentiment = np.argmax(predicted_sentiment_prob, axis=-1) return str(predicted_sentiment) -
启动 Web 服务器。
if __name__ == '__main__': # load model load_variables() app.run(debug=True) -
将此文件保存为
app.py(可以使用任何名称)。通过终端使用app.py运行此代码:python app.py输出如下: