Python 自然语言处理秘籍(三)
原文:
annas-archive.org/md5/209bb9c5cbc97ace0712ae52223b18f3译者:飞龙
第八章:高级 NLP 示例
本章我们将讨论以下几种方法:
-
创建一个 NLP 管道
-
解决文本相似性问题
-
识别话题
-
摘要文本
-
解决指代问题
-
解决词义歧义
-
执行情感分析
-
探索高级情感分析
-
创建一个对话助手或聊天机器人
简介
到目前为止,我们已经了解了如何处理输入文本、识别词性以及提取重要信息(命名实体)。我们还学到了一些计算机科学的概念,如语法、解析器等等。在本章中,我们将深入探讨自然语言处理(NLP)中的高级话题,这些话题需要多种技术才能正确理解和解决。
创建一个 NLP 管道
在计算中,管道可以被看作是一个多阶段的数据流系统,其中一个组件的输出作为另一个组件的输入。
以下是管道中发生的事情:
-
数据始终在一个组件到另一个组件之间流动
-
组件是一个黑箱,应该关注输入数据和输出数据
一个明确的管道需要处理以下几件事:
-
每个组件中流动的数据的输入格式
-
每个组件输出的数据格式
-
确保通过调整数据流入和流出速度来控制组件之间的数据流动
例如,如果你熟悉 Unix/Linux 系统,并且对在 shell 中工作有一些接触,你会看到|运算符,它是 shell 的数据管道抽象。我们可以利用|运算符在 Unix shell 中构建管道。
让我们通过一个 Unix 的例子来快速理解:如何在给定目录中查找文件数量?
为了解决这个问题,我们需要以下几件事:
-
我们需要一个组件(或在 Unix 中是一个命令),它读取目录并列出其中的所有文件
-
我们需要另一个组件(或在 Unix 中是一个命令),它读取行并打印行数
所以,我们已经解决了这两个需求。它们是:
-
ls命令 -
wc命令
如果我们能构建一个管道,将ls的输出传递给wc,那么我们就完成了。
在 Unix 命令中,ls -l | wc -l 是一个简单的管道,用来计算目录中的文件数量。
有了这些知识,让我们回到 NLP 管道的需求:
-
输入数据获取
-
将输入数据分割成单词
-
识别输入数据中单词的词性
-
从单词中提取命名实体
-
识别命名实体之间的关系
在这个例子中,我们将尝试构建一个最简单的管道;它从远程 RSS 源获取数据,并打印每个文档中识别出的命名实体。
准备工作
你应该安装 Python,并且安装nltk、queue、feedparser和uuid库。
如何实现...
-
打开 Atom 编辑器(或你喜欢的编程编辑器)。
-
创建一个名为
PipelineQ.py的新文件。 -
输入以下源代码:
-
保存文件。
-
使用 Python 解释器运行程序。
-
你将看到这个输出:
它是如何工作的...
让我们看看如何构建这个管道:
import nltk
import threading
import queue
import feedparser
import uuid
这五个指令将五个 Python 库导入当前程序:
-
nltk: 自然语言工具包 -
threading: 用于在单个程序中创建轻量级任务的线程库 -
queue: 可在多线程程序中使用的队列库 -
feedparser: 一个 RSS 源解析库 -
uuid: 基于 RFC-4122 的 uuid 版本 1、3、4、5 生成库
threads = []
创建一个新的空列表,用于跟踪程序中的所有线程:
queues = [queue.Queue(), queue.Queue()]
该指令在变量queue中创建一个包含两个队列的列表?
为什么我们需要两个队列:
-
第一个队列用于存储分词后的句子
-
第二个队列用于存储所有已分析的词性(POS)词语
该指令定义了一个新函数extractWords(),该函数从互联网读取一个示例 RSS 源,并存储这些单词以及该文本的唯一标识符:
def extractWords():
我们正在定义一个来自印度时报网站的示例 URL(娱乐新闻):
url = 'https://timesofindia.indiatimes.com/rssfeeds/1081479906.cms'
该指令调用了feedparser库的parse()函数。这个parse()函数下载 URL 的内容并将其转换为新闻条目列表。每个新闻条目是一个包含标题和摘要键的字典:
feed = feedparser.parse(url)
我们从 RSS 源中取出前五条条目,并将当前条目存储在一个名为entry的变量中:
for entry in feed['entries'][:5]:
当前 RSS 源条目的标题存储在一个名为text的变量中:
text = entry['title']
该指令跳过包含敏感词的标题。由于我们从互联网上读取数据,因此我们必须确保数据已正确清理:
if 'ex' in text:
continue
使用word_tokenize()函数将输入文本分解为单词,并将结果存储在一个名为words的变量中:
words = nltk.word_tokenize(text)
创建一个名为data的字典,其中包含两个键值对,我们分别将 UUID 和输入词存储在 UUID 和 input 键下:
data = {'uuid': uuid.uuid4(), 'input': words}
该指令将字典存储在第一个队列queues[0]中。第二个参数设置为 true,这表示如果队列已满,则暂停线程:
queues[0].put(data, True)
一个设计良好的管道应该根据组件的计算能力来控制数据的流入和流出。如果不这样,整个管道会崩溃。此指令打印出我们正在处理的当前 RSS 项以及其唯一 ID:
print(">> {} : {}".format(data['uuid'], text))
该指令定义了一个名为extractPOS()的新函数,该函数从第一个队列读取数据,处理数据,并将词性的词存储在第二个队列中:
def extractPOS():
这是一个无限循环:
while True:
这些指令检查第一个队列是否为空。当队列为空时,我们停止处理:
if queues[0].empty():
break
为了使程序更健壮,传递来自第一个队列的反馈。这个部分留给读者练习。这是else部分,表示第一个队列中有数据:
else:
从队列中取出第一个项目(按 FIFO 顺序):
data = queues[0].get()
识别单词中的词性:
words = data['input']
postags = nltk.pos_tag(words)
更新第一个队列,表明我们已完成处理刚刚由该线程提取的项目:
queues[0].task_done()
将带有词性标注的单词列表存储到第二个队列中,以便管道的下一阶段执行。在这里,我们也使用了true作为第二个参数,这将确保如果队列中没有空闲空间,线程会等待:
queues[1].put({'uuid': data['uuid'], 'input': postags}, True)
该指令定义了一个新函数,extractNE(),它从第二个队列读取,处理带有词性标注的单词,并在屏幕上打印命名实体:
def extractNE():
这是一个无限循环指令:
while True:
如果第二个队列为空,则退出无限循环:
if queues[1].empty():
break
该指令从第二个队列中选取一个元素,并将其存储在一个数据变量中:
else:
data = queues[1].get()
该指令标记从第二个队列刚刚选取的元素的数据处理完成:
postags = data['input']
queues[1].task_done()
该指令从postags变量中提取命名实体,并将其存储在名为chunks的变量中:
chunks = nltk.ne_chunk(postags, binary=False)
print(" << {} : ".format(data['uuid']), end = '')
for path in chunks:
try:
label = path.label()
print(path, end=', ')
except:
pass
print()
这些指令执行以下操作:
-
打印数据字典中的 UUID
-
遍历所有已识别的语法块:
-
我们使用了一个
try/except块,因为树中的并非所有元素都有label()函数(当没有找到命名实体时,它们是元组): -
最后,我们调用一个
print()函数,它在屏幕上打印一个换行符:
该指令定义了一个新函数,runProgram,它使用线程进行管道设置:
def runProgram():
这三条指令使用extractWords()作为函数创建一个新线程,启动该线程并将线程对象(e)添加到名为threads的列表中:
e = threading.Thread(target=extractWords())
e.start()
threads.append(e)
这些指令使用extractPOS()作为函数创建一个新线程,启动该线程,并将线程对象(p)添加到列表变量threads中:
p = threading.Thread(target=extractPOS())
p.start()
threads.append(p)
这些指令使用extractNE()为代码创建一个新线程,启动该线程,并将线程对象(n)添加到列表threads中:
n = threading.Thread(target=extractNE())
n.start()
threads.append(n)
这两条指令在所有处理完成后释放分配给队列的资源:
queues[0].join()
queues[1].join()
这两条指令遍历线程列表,将当前线程对象存储在变量t中,调用join()函数以标记线程完成,并释放分配给该线程的资源:
for t in threads:
t.join()
这是程序在主线程运行时调用的代码部分。调用runProgram(),它模拟整个管道:
if __name__ == '__main__':
runProgram()
解决文本相似性问题
文本相似性问题涉及到找到给定文本文件之间的相似度。现在,当我们说它们相似时,我们可以从多个维度来判断它们是更接近还是更远:
-
情感/情绪维度
-
感知维度
-
某些单词的存在
有许多可用的算法来解决这个问题;它们在复杂度、所需资源以及我们处理的数据量上各不相同。
在这个配方中,我们将使用 TF-IDF 算法来解决相似度问题。所以首先,让我们理解基础知识:
- 词频(TF):该技术试图找到一个单词在给定文档中的相对重要性(或频率)
由于我们讨论的是相对重要性,通常会将词频归一化,以便与文档中出现的总词数相比,从而计算出单词的 TF 值。
- 逆文档频率(IDF):该技术确保频繁使用的词(如 a、the 等)相对于那些很少使用的词,其权重应更低。
由于 TF 和 IDF 值都被分解为数字(分数),我们将对每个词和每个文档的这两个值进行相乘,并构建M个N维度的向量(其中N是文档的总数,M是所有文档中的唯一词汇)。
一旦我们有了这些向量,我们需要使用以下公式计算这些向量之间的余弦相似度:
准备工作
你应该安装 Python,并且拥有nltk和scikit库。对数学有一定了解会更有帮助。
如何操作...
-
打开 Atom 编辑器(或你最喜欢的编程编辑器)。
-
创建一个名为
Similarity.py的新文件。 -
输入以下源代码:
-
保存文件。
-
使用 Python 解释器运行程序。
-
你将看到以下输出:
它是如何工作的...
让我们看看如何解决文本相似度问题。这四个指令导入了程序中使用的必要库:
import nltk
import math
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
我们正在定义一个新的类,TextSimilarityExample:
class TextSimilarityExample:
该指令为类定义了一个新的构造函数:
def __init__(self):
该指令定义了我们想要查找相似度的示例句子。
self.statements = [
'ruled india',
'Chalukyas ruled Badami',
'So many kingdoms ruled India',
'Lalbagh is a botanical garden in India'
]
我们正在定义给定句子中所有单词的 TF:
def TF(self, sentence):
words = nltk.word_tokenize(sentence.lower())
freq = nltk.FreqDist(words)
dictionary = {}
for key in freq.keys():
norm = freq[key]/float(len(words))
dictionary[key] = norm
return dictionary
该函数执行以下操作:
-
将句子转换为小写,并提取所有单词
-
使用 nltk 的
FreqDist函数查找这些单词的频率分布 -
遍历所有字典键,构建归一化的浮动值,并将它们存储在字典中
-
返回包含句子中每个单词归一化分数的字典。
我们定义了一个 IDF,来查找所有文档中所有单词的 IDF 值:
def IDF(self):
def idf(TotalNumberOfDocuments, NumberOfDocumentsWithThisWord):
return 1.0 + math.log(TotalNumberOfDocuments/NumberOfDocumentsWithThisWord)
numDocuments = len(self.statements)
uniqueWords = {}
idfValues = {}
for sentence in self.statements:
for word in nltk.word_tokenize(sentence.lower()):
if word not in uniqueWords:
uniqueWords[word] = 1
else:
uniqueWords[word] += 1
for word in uniqueWords:
idfValues[word] = idf(numDocuments, uniqueWords[word])
return idfValues
该函数执行以下操作:
-
我们定义了一个名为
idf()的局部函数,它是计算给定单词 IDF 值的公式。 -
我们遍历所有语句并将它们转换为小写
-
查找每个单词在所有文档中出现的次数
-
为所有单词构建 IDF 值,并返回包含这些 IDF 值的字典
我们现在正在定义一个TF_IDF(TF 乘以 IDF),用于所有文档和给定的搜索字符串。
def TF_IDF(self, query):
words = nltk.word_tokenize(query.lower())
idf = self.IDF()
vectors = {}
for sentence in self.statements:
tf = self.TF(sentence)
for word in words:
tfv = tf[word] if word in tf else 0.0
idfv = idf[word] if word in idf else 0.0
mul = tfv * idfv
if word not in vectors:
vectors[word] = []
vectors[word].append(mul)
return vectors
让我们看看我们在这里做了什么:
-
将搜索字符串拆分为词元
-
为
self.statements变量中的所有句子构建IDF()。 -
遍历所有句子,找到该句子中所有单词的 TF
-
过滤并只使用在输入搜索字符串中出现的单词,并构建包含tfidf*值的向量,针对每个文档
-
返回搜索查询中每个单词的向量列表
该函数在屏幕上显示向量的内容:
def displayVectors(self, vectors):
print(self.statements)
for word in vectors:
print("{} -> {}".format(word, vectors[word]))
现在,为了找到相似度,正如我们最初讨论的,我们需要在所有输入向量上找到余弦相似度。我们可以自己做所有的数学运算。但这一次,让我们尝试使用 scikit 来为我们做所有的计算。
def cosineSimilarity(self):
vec = TfidfVectorizer()
matrix = vec.fit_transform(self.statements)
for j in range(1, 5):
i = j - 1
print("\tsimilarity of document {} with others".format(i))
similarity = cosine_similarity(matrix[i:j], matrix)
print(similarity)
在之前的函数中,我们学习了如何构建 TF 和 IDF 值,并最终得到所有文档的 TF x IDF 值。
让我们看看我们在这里做了什么:
-
定义一个新函数:
cosineSimilarity() -
创建一个新的向量化对象
-
使用
fit_transform()函数构建我们感兴趣的所有文档的 TF-IDF 矩阵 -
然后我们将每个文档与其他所有文档进行比较,看看它们之间的相似度如何
这是demo()函数,它运行我们之前定义的所有其他函数:
def demo(self):
inputQuery = self.statements[0]
vectors = self.TF_IDF(inputQuery)
self.displayVectors(vectors)
self.cosineSimilarity()
让我们看看我们在这里做了什么
-
我们将第一句话作为输入查询。
-
我们使用自己手写的
TF_IDF()函数来构建向量。 -
我们在屏幕上显示所有句子的 TF x IDF 向量。
-
我们使用
scikit库通过调用cosineSimilarity()函数计算并打印所有句子的余弦相似度。
我们正在创建一个新的TextSimilarityExample()类对象,然后调用demo()函数。
similarity = TextSimilarityExample()
similarity.demo()
识别主题
在上一章中,我们学习了如何进行文档分类。初学者可能认为文档分类和主题识别是相同的,但实际上它们有些微小的差别。
主题识别是发现输入文档集中存在的主题的过程。这些主题可以是出现在给定文本中的多个唯一单词。
让我们举个例子。当我们阅读包含 Sachin Tendulkar、score、win 等词汇的任意文本时,我们可以理解这句话是在描述板球。然而,我们也可能错了。
为了在给定的输入文本中找到所有这些类型的主题,我们使用了潜在狄利克雷分配(Latent Dirichlet Allocation, LDA)算法(我们也可以使用 TF-IDF,但由于我们在前面的例子中已经探索过它,让我们看看 LDA 在识别主题中的作用)。
准备中
你应该已经安装了 Python,以及nltk、gensim和feedparser库。
如何做...
-
打开 Atom 编辑器(或你喜欢的编程编辑器)。
-
创建一个名为
IdentifyingTopic.py的新文件。 -
输入以下源代码:
-
保存文件。
-
使用 Python 解释器运行程序。
-
你将看到以下输出:
它是如何工作的……
让我们看看主题识别程序是如何工作的。这五条指令将必要的库导入到当前程序中。
from nltk.tokenize import RegexpTokenizer
from nltk.corpus import stopwords
from gensim import corpora, models
import nltk
import feedparser
该指令定义了一个新类IdentifyingTopicExample:
class IdentifyingTopicExample:
该指令定义了一个新函数getDocuments(),其责任是使用feedparser从互联网上下载少量文档:
def getDocuments(self):
下载 URL 中提到的所有文档,并将字典列表存储在名为feed的变量中:
url = 'https://sports.yahoo.com/mlb/rss.xml'
feed = feedparser.parse(url)
清空列表,以跟踪我们将进一步分析的所有文档:
self.documents = []
从feed变量中获取前五篇文档,并将当前新闻条目存储在名为entry的变量中:
for entry in feed['entries'][:5]:
将新闻摘要存储在名为text的变量中:
text = entry['summary']
如果新闻文章包含任何敏感词,则跳过这些词:
if 'ex' in text:
continue
将文档存储在documents变量中:
self.documents.append(text)
在屏幕上显示当前文档:
print("-- {}".format(text))
向用户显示一条信息,告知我们已从给定的url收集了N个文档:
print("INFO: Fetching documents from {} completed".format(url))
该指令定义了一个新函数cleanDocuments(),其责任是清理输入文本(因为我们从互联网上下载它,可能包含任何类型的数据)。
def cleanDocuments(self):
我们感兴趣的是提取那些属于英语字母表的单词。因此,定义了这个分词器,将文本分解为标记,每个标记由 a 到 z 和 A-Z 的字母组成。通过这种方式,我们可以确保标点符号和其他不良数据不会进入处理。
tokenizer = RegexpTokenizer(r'[a-zA-Z]+')
将英语停用词存储在变量en_stop中:
en_stop = set(stopwords.words('english'))
定义一个空列表cleaned,用于存储所有已清理和分词的文档:
self.cleaned = []
使用getDocuments()函数遍历我们收集的所有文档:
for doc in self.documents:
将文档转换为小写字母,以避免由于大小写敏感而对相同的单词进行不同的处理:
lowercase_doc = doc.lower()
将句子分解为单词。输出是一个存储在words变量中的单词列表:
words = tokenizer.tokenize(lowercase_doc)
如果句子中的词属于英语停用词类别,则忽略所有这些词,并将它们存储在non_stopped_words变量中:
non_stopped_words = [i for i in words if not i in en_stop]
将分词和清理后的句子存储在名为self.cleaned(类成员)的变量中。
self.cleaned.append(non_stopped_words)
向用户显示诊断消息,告知我们已完成文档清理:
print("INFO: Cleaning {} documents completed".format(len(self.documents)))
该指令定义了一个新函数doLDA,该函数在清理后的文档上运行 LDA 分析:
def doLDA(self):
在直接处理已清理的文档之前,我们从这些文档创建一个字典:
dictionary = corpora.Dictionary(self.cleaned)
输入语料库被定义为每个清理过的句子的词袋:
corpus = [dictionary.doc2bow(cleandoc) for cleandoc in self.cleaned]
在语料库上创建一个模型,定义主题数量为2,并使用id2word参数设置词汇大小/映射:
ldamodel = models.ldamodel.LdaModel(corpus, num_topics=2, id2word = dictionary)
在屏幕上打印两个主题,每个主题应包含四个单词:
print(ldamodel.print_topics(num_topics=2, num_words=4))
这是执行所有步骤的函数:
def run(self):
self.getDocuments()
self.cleanDocuments()
self.doLDA()
当当前程序作为主程序被调用时,创建一个名为topicExample的新对象,该对象来自IdentifyingTopicExample()类,并在该对象上调用run()函数。
if __name__ == '__main__':
topicExample = IdentifyingTopicExample()
topicExample.run()
文本总结
在这个信息过载的时代,信息的形式多种多样,且以印刷/文本的形式存在。对我们来说,想要全部消化这些信息几乎是不可能的。为了让我们更容易地消费这些数据,我们一直在尝试发明一些算法,能够将大量的文本简化成一个我们能轻松消化的摘要(或要点)。
这样做,我们既节省时间,又能让网络变得更简单。
在这个教程中,我们将使用 gensim 库,它内建对基于 TextRank 算法的总结功能支持(web.eecs.umich.edu/~mihalcea/papers/mihalcea.emnlp04.pdf)。
正在准备中
你应该已经安装了 Python,并且安装了bs4和gensim库。
如何操作...
-
打开 atom 编辑器(或你最喜欢的编程编辑器)。
-
创建一个名为
Summarize.py的新文件。 -
输入以下源代码:
-
保存文件。
-
使用 Python 解释器运行程序。
-
你将看到以下输出:
它是如何工作的...
让我们看看我们的总结程序是如何工作的。
from gensim.summarization import summarize
from bs4 import BeautifulSoup
import requests
这三条指令将必要的库导入到当前程序中:
-
gensim.summarization.summarize:基于 TextRank 算法的文本总结功能 -
bs4:一个用于解析 HTML 文档的BeautifulSoup库 -
requests:一个用于下载 HTTP 资源的库
我们定义了一个名为 URLs 的字典,其键是自动生成的论文标题,值是论文的 URL:
urls = {
'Daff: Unproven Unification of Suffix Trees and Redundancy': 'http://scigen.csail.mit.edu/scicache/610/scimakelatex.21945.none.html',
'CausticIslet: Exploration of Rasterization': 'http://scigen.csail.mit.edu/scicache/790/scimakelatex.1499.none.html'
}
遍历字典的所有键:
for key in urls.keys():
将当前论文的 URL 存储在一个名为url的变量中:
url = urls[key]
使用requests库的get()方法下载 URL 的内容,并将响应对象存储到变量r中:
r = requests.get(url)
使用BeautifulSoup()函数,通过 HTML 解析器解析r对象中的文本,并将返回的对象存储在一个名为soup的变量中:
soup = BeautifulSoup(r.text, 'html.parser')
去除所有 HTML 标签,仅将文档中的文本提取到变量data中:
data = soup.get_text()
找到文本Introduction的位置,并跳过直到字符串末尾,将其标记为我们提取子字符串的起始偏移量。
pos1 = data.find("1 Introduction") + len("1 Introduction")
找到文档中的第二个位置,准确位于相关工作部分的开始:
pos2 = data.find("2 Related Work")
现在,提取论文的介绍部分,内容位于这两个偏移量之间:
text = data[pos1:pos2].strip()
在屏幕上显示论文的 URL 和标题:
print("PAPER URL: {}".format(url))
print("TITLE: {}".format(key))
在文本上调用summarize()函数,该函数根据文本排序算法返回简短的文本:
print("GENERATED SUMMARY: {}".format(summarize(text)))
打印额外的换行符,以便提高屏幕输出的可读性。
print()
解决指代问题
在许多自然语言中,在构造句子时,我们避免重复使用某些名词,而是用代词来简化句子的结构。
例如:
Ravi 是个男孩。他经常为穷人捐钱。
在这个例子中,有两个句子:
-
Ravi 是个男孩。
-
他经常为穷人捐钱。
当我们开始分析第二句话时,如果不知道第一句话,我们无法判断是谁在捐钱。因此,我们应该将“He”与 Ravi 关联起来,以获得完整的句子含义。所有这些引用消解在我们的大脑中自然发生。
如果我们仔细观察前面的例子,首先是主语出现;然后是代词出现。所以,流动的方向是从左到右。根据这个流动,我们可以称这些句子为指代句(anaphora)。
让我们再看一个例子:
他已经在前往机场的路上。Ravi 意识到这一点。
这是另一类例子,其中表达的方向是反向顺序(先是代词,再是名词)。在这里,He 与 Ravi 相关联。这类句子被称为前指(Cataphora)。
这个最早可用的指代消解算法可以追溯到 1970 年;Hobbs 曾发表过一篇相关论文。该论文的在线版本可以在这里查看:www.isi.edu/~hobbs/pronoun-papers.html。
在这个教程中,我们将尝试使用我们刚学到的知识编写一个非常简单的指代消解算法。
准备工作
你应该安装 Python,并配备nltk库和gender数据集。
你可以使用nltk.download()来下载语料库。
如何实现它…
-
打开 atom 编辑器(或你喜欢的编程编辑器)。
-
创建一个名为
Anaphora.py的新文件。 -
输入以下源代码:
-
保存文件。
-
使用 Python 解释器运行程序。
-
你将看到以下输出:
它是如何工作的…
让我们看看我们简单的指代消解算法是如何工作的。
import nltk
from nltk.chunk import tree2conlltags
from nltk.corpus import names
import random
这四条指令导入了程序中所需的必要模块和函数。我们正在定义一个名为AnaphoraExample的新类:
class AnaphoraExample:
我们为这个类定义了一个新的构造函数,该函数不接受任何参数:
def __init__(self):
这两条指令从nltk.names语料库中加载所有的男性和女性名字,并在将它们存储在两个名为 male/female 的列表之前对其进行标记。
males = [(name, 'male') for name in names.words('male.txt')]
females = [(name, 'female') for name in names.words('female.txt')]
这条指令创建了一个独特的男性和女性名字列表。random.shuffle()确保列表中的所有数据都是随机的:
combined = males + females
random.shuffle(combined)
这条指令在gender上调用feature()函数,并将所有名字存储在一个名为training的变量中:
training = [(self.feature(name), gender) for (name, gender) in combined]
我们正在使用存储在名为training的变量中的男性和女性特征,创建一个名为_classifier的NaiveBayesClassifier对象:
self._classifier = nltk.NaiveBayesClassifier.train(training)
这个函数定义了最简单的特征,只通过查看名字的最后一个字母就能将给定的名字分类为男性或女性:
def feature(self, word):
return {'last(1)' : word[-1]}
这个函数接受一个单词作为参数,并尝试通过我们构建的分类器来检测该单词的性别是男性还是女性:
def gender(self, word):
return self._classifier.classify(self.feature(word))
这是我们感兴趣的主要函数,因为我们将对示例句子进行指代检测:
def learnAnaphora(self):
这是四个具有不同复杂性的例子,以指代形式表达:
sentences = [
"John is a man. He walks",
"John and Mary are married. They have two kids",
"In order for Ravi to be successful, he should follow John",
"John met Mary in Barista. She asked him to order a Pizza"
]
该指令通过一次处理一个句子,将每个句子存储到一个名为sent的局部变量中:
for sent in sentences:
该指令对句子进行分词、赋予词性、提取块(命名实体),并将块树返回给一个名为chunks的变量:
chunks = nltk.ne_chunk(nltk.pos_tag(nltk.word_tokenize(sent)), binary=False)
这个变量用于存储所有帮助我们解决指代问题的名字和代词:
stack = []
该指令将在用户屏幕上显示当前正在处理的句子:
print(sent)
该指令将树结构的块展平为 IOB 格式表达的项目列表:
items = tree2conlltags(chunks)
我们正在遍历所有以 IOB 格式表示的分块句子(每个元组包含三个元素):
for item in items:
如果单词的词性(POS)是NNP,并且该单词的 IOB 标签是B-PERSON或O,那么我们将此单词标记为Name:
if item[1] == 'NNP' and (item[2] == 'B-PERSON' or item[2] == 'O'):
stack.append((item[0], self.gender(item[0])))
如果单词的词性是CC,我们也会将其添加到stack变量中:
elif item[1] == 'CC':
stack.append(item[0])
如果单词的词性是PRP,我们将把它添加到stack变量中:
elif item[1] == 'PRP':
stack.append(item[0])
最后,我们在屏幕上打印栈:
print("\t {}".format(stack))
我们正在从AnaphoraExample()创建一个新的对象,调用learnAnaphora()函数,并在指代对象上执行此函数。一旦函数执行完毕,我们就可以看到每个句子的单词列表。
anaphora = AnaphoraExample()
anaphora.learnAnaphora()
词义歧义消解
在之前的章节中,我们学习了如何识别单词的词性、找到命名实体等等。就像英语中的单词既可以是名词也可以是动词一样,计算机程序很难准确找出一个单词在特定上下文中的语义。
让我们通过几个例子来理解这个语义部分:
| 句子 | 描述 |
|---|---|
| 她是我的约会对象 | 这里单词date的意思不是日历日期,而是表达一种人际关系。 |
| 你已经休息得太多,以至于忘记了清理花园里的叶子 | 这里单词leaves有多重含义:
-
第一个单词leave的意思是休息。
-
第二个意思实际上指的是树叶。
|
就像这样,句子中可能有多种语义组合。
我们在进行语义识别时面临的挑战之一是找到一种合适的命名法来描述这些语义。市面上有很多英语词典可以描述单词的行为以及所有可能的组合。在所有这些词典中,WordNet 是结构化最强、最受欢迎且被广泛接受的语义使用来源。
在这个食谱中,我们将展示来自 WordNet 库的语义示例,并使用内置的nltk库来找出单词的语义。
Lesk 是最早提出用于处理语义检测的算法之一,然而你会发现,这个算法在某些情况下也不够准确。
准备工作
你应该安装 Python,并安装nltk库。
如何做到这一点...
-
打开 Atom 编辑器(或你喜欢的编程编辑器)。
-
创建一个名为
WordSense.py的新文件。 -
输入以下源代码:
-
保存文件。
-
使用 Python 解释器运行程序。
-
你将看到以下输出:
它是如何工作的……
让我们来看一下我们的程序是如何工作的。这个指令将nltk库导入到程序中:
import nltk
我们定义了一个名为understandWordSenseExamples()的函数,它使用 WordNet 语料库展示我们感兴趣的单词可能的意义。
def understandWordSenseExamples():
这些是三个具有不同表达意义的单词。它们作为一个列表存储在一个名为words的变量中。
words = ['wind', 'date', 'left']
print("-- examples --")
for word in words:
syns = nltk.corpus.wordnet.synsets(word)
for syn in syns[:2]:
for example in syn.examples()[:2]:
print("{} -> {} -> {}".format(word, syn.name(), example))
这些指令执行以下操作:
-
遍历列表中的所有单词,将当前单词存储在名为
word的变量中。 -
从
wordnet模块调用synsets()函数,并将结果存储在syns变量中。 -
从列表中取出前三个同义词集,遍历它们,并将当前同义词集存入名为
syn的变量中。 -
在
syn对象上调用examples()函数,并将前两个示例作为迭代器。迭代器的当前值可以通过变量example获得。 -
最后,打印出单词、同义词集的名称和示例句子。
定义一个新函数understandBuiltinWSD(),以探索 NLTK 内置 lesk 算法在示例句子上的表现。
def understandBuiltinWSD():
定义一个名为maps的新变量,一个包含元组的列表。
print("-- built-in wsd --")
maps = [
('Is it the fish net that you are using to catch fish ?', 'fish', 'n'),
('Please dont point your finger at others.', 'point', 'n'),
('I went to the river bank to see the sun rise', 'bank', 'n'),
]
每个元组由三个元素组成:
-
我们想要分析的句子
-
我们想要找出其意义的句子中的单词
-
单词的词性(POS)
在这两个指令中,我们正在遍历maps变量,将当前元组存入变量m,调用nltk.wsd.lesk()函数,并在屏幕上显示格式化的结果。
for m in maps:
print("Sense '{}' for '{}' -> '{}'".format(m[0], m[1], nltk.wsd.lesk(m[0], m[1], m[2])))
当程序运行时,调用这两个函数将结果显示在用户的屏幕上。
if __name__ == '__main__':
understandWordSenseExamples()
understandBuiltinWSD()
进行情感分析
反馈是理解关系的最强有力手段之一。人类非常擅长理解口头交流中的反馈,因为分析过程是无意识进行的。为了编写能够衡量并找到情感商的计算机程序,我们应该对这些情感在自然语言中的表达方式有一定的理解。
让我们举几个例子:
| 句子 | 描述 |
|---|---|
| 我很开心 | 表示一种快乐的情感 |
| 她好难过 :( | 我们知道这里有一个经典的悲伤表情 |
随着文本、图标和表情符号在书面自然语言交流中的使用增加,计算机程序越来越难以理解句子的情感含义。
让我们尝试编写一个程序,以理解 nltk 提供的功能来构建我们自己的算法。
准备开始
你应该已经安装了 Python,并且安装了nltk库。
如何做……
-
打开 Atom 编辑器(或你最喜欢的编程编辑器)。
-
创建一个名为
Sentiment.py的新文件。 -
输入以下源代码:
-
保存文件。
-
使用 Python 解释器运行程序。
-
你将看到以下输出:
它是如何工作的……
让我们看看我们的情感分析程序是如何工作的。这些指令分别导入了nltk模块和sentiment_analyzer模块。
import nltk
import nltk.sentiment.sentiment_analyzer
我们定义了一个新函数,wordBasedSentiment(),我们将用它来学习如何基于我们已经知道且对我们有意义的单词进行情感分析。
def wordBasedSentiment():
我们定义了一个包含三个特殊单词的列表,这些单词代表某种形式的幸福。这些单词存储在positive_words变量中。
positive_words = ['love', 'hope', 'joy']
这是我们将要分析的示例文本;文本被存储在一个名为text的变量中。
text = 'Rainfall this year brings lot of hope and joy to Farmers.'.split()
我们在文本上调用了extract_unigram_feats()函数,使用我们已定义的单词。结果是一个字典,显示这些单词是否出现在文本中。
analysis = nltk.sentiment.util.extract_unigram_feats(text, positive_words)
该指令将在用户的屏幕上显示字典。
print(' -- single word sentiment --')
print(analysis)
该指令定义了一个新函数,我们将用它来判断某些单词对是否出现在句子中。
def multiWordBasedSentiment():
该指令定义了一个包含双词元组的列表。我们感兴趣的是判断这些单词对是否一起出现在句子中。
word_sets = [('heavy', 'rains'), ('flood', 'bengaluru')]
这是我们感兴趣的句子,旨在处理并找出其特征。
text = 'heavy rains cause flash flooding in bengaluru'.split()
我们在输入句子上调用了extract_bigram_feats()函数,对照word_sets变量中的单词集。结果是一个字典,告诉我们这些单词对是否出现在句子中。
analysis = nltk.sentiment.util.extract_bigram_feats(text, word_sets)
该指令将在屏幕上显示字典。
print(' -- multi word sentiment --')
print(analysis)
我们正在定义一个新函数,markNegativity(),它帮助我们理解如何在句子中找出否定性。
def markNegativity():
接下来是我们希望进行否定性分析的句子,它存储在一个变量text中。
text = 'Rainfall last year did not bring joy to Farmers'.split()
我们在文本上调用了mark_negation()函数。此函数返回句子中所有单词的列表,并且对于所有具有否定意义的单词,会加上一个特殊后缀_NEG。结果存储在negation变量中。
negation = nltk.sentiment.util.mark_negation(text)
该指令在屏幕上显示列表的否定。
print(' -- negativity --')
print(negation)
当程序运行时,这些函数会被调用,我们将看到按执行顺序(自上而下)输出的三个函数结果。
if __name__ == '__main__':
wordBasedSentiment()
multiWordBasedSentiment()
markNegativity()
探索高级情感分析
我们看到越来越多的企业走向线上,以增加目标客户群,并且顾客可以通过各种渠道留下反馈。企业越来越需要理解顾客对于其经营活动的情感反应。
在这个示例中,我们将根据前面学到的内容编写自己的情感分析程序。我们还将探索内置的 vader 情感分析算法,这有助于评估复杂句子的情感。
准备就绪
您应该安装了 Python,以及nltk库。
如何操作...
-
打开 Atom 编辑器(或您喜欢的编程编辑器)。
-
创建一个名为
AdvSentiment.py的新文件。 -
输入以下源代码:
-
保存文件。
-
使用 Python 解释器运行程序。
-
您将看到以下输出:
它是如何工作的...
现在,让我们看看我们的情感分析程序如何工作。这四条指令导入了我们在此程序中要使用的必要模块。
import nltk
import nltk.sentiment.util
import nltk.sentiment.sentiment_analyzer
from nltk.sentiment.vader import SentimentIntensityAnalyzer
定义一个新函数,mySentimentAnalyzer():
def mySentimentAnalyzer():
此指令定义了一个新的子函数,score_feedback(),它接受一个句子作为输入,并根据-1负面、0中性和1积极返回句子的分数。
def score_feedback(text):
由于我们只是在做实验,所以我们定义了三个词语,用于找到情感。在实际应用中,我们可能会从更大的词典语料库中选择这些词语。
positive_words = ['love', 'genuine', 'liked']
此指令将输入的句子分解为单词。将单词列表提供给mark_negation()函数以识别句子中是否存在消极情绪。将来自mark_negation()的结果与字符串连接,并查看是否存在_NEG后缀;然后将分数设置为-1。
if '_NEG' in ' '.join(nltk.sentiment.util.mark_negation(text.split())):
score = -1
在这里,我们正在对输入文本使用extract_unigram_feats(),并根据positive_words进行分析,并将字典存储到名为analysis的变量中:
else:
analysis = nltk.sentiment.util.extract_unigram_feats(text.split(), positive_words)
如果输入文本中存在积极词汇,则分数的值决定为1。
if True in analysis.values():
score = 1
else:
score = 0
最后,这个score_feedback()函数会返回计算出的分数:
return score
这些是我们有兴趣使用我们的算法处理的四篇评论,以打印分数。
feedback = """I love the items in this shop, very genuine and quality is well maintained.
I have visited this shop and had samosa, my friends liked it very much.
ok average food in this shop.
Fridays are very busy in this shop, do not place orders during this day."""
这些指令通过在换行符(\n)上分割并调用score_feedback()函数来从变量 feedback 中提取句子。
print(' -- custom scorer --')
for text in feedback.split("\n"):
print("score = {} for >> {}".format(score_feedback(text), text))
结果将显示在屏幕上的分数和句子。此指令定义了advancedSentimentAnalyzer()函数,用于理解 NLTK 情感分析的内置特性。
def advancedSentimentAnalyzer():
我们定义了五个句子来进行分析。请注意,我们还使用了表情符号(图标)来查看算法的工作原理。
sentences = [
':)',
':(',
'She is so :(',
'I love the way cricket is played by the champions',
'She neither likes coffee nor tea',
]
此指令为SentimentIntensityAnalyzer()创建了一个新对象,并将对象存储在变量senti中。
senti = SentimentIntensityAnalyzer()
print(' -- built-in intensity analyser --')
for sentence in sentences:
print('[{}]'.format(sentence), end=' --> ')
kvp = senti.polarity_scores(sentence)
for k in kvp:
print('{} = {}, '.format(k, kvp[k]), end='')
print()
这些指令执行以下操作:
-
迭代所有句子,并将当前句子存储在变量
sentence中。 -
在屏幕上显示当前处理的句子
-
在这个句子上调用
polarity_scores()函数;将结果存储在名为kvp的变量中。 -
遍历字典
kvp,并打印出这些类型的键(消极、中立、积极或复合类型)及其计算出的分数。
当当前程序被调用时,调用这两个函数以在屏幕上显示结果。
if __name__ == '__main__':
advancedSentimentAnalyzer()
mySentimentAnalyzer()
创建一个对话助手或聊天机器人
对话助手或聊天机器人并不算新颖。这个领域最早的代表之一是 ELIZA,它是在 1960 年代初期创建的,值得一探。
为了成功地构建一个对话引擎,它应该处理以下几个方面:
-
了解目标受众
-
理解自然语言中的交流
-
理解用户的意图
-
提供能回答用户问题并进一步提示的回应
NLTK 有一个模块nltk.chat,它通过提供一个通用框架来简化构建这些引擎的过程。
让我们来看一下 NLTK 中可用的引擎:
| 引擎 | 模块 |
|---|---|
| Eliza | nltk.chat.eliza Python 模块 |
| Iesha | nltk.chat.iesha Python 模块 |
| 粗鲁 | nltk.chat.rudep Python 模块 |
| Suntsu | nltk.chat.suntsu模块 |
| Zen | nltk.chat.zen模块 |
为了与这些引擎交互,我们只需在 Python 程序中加载这些模块并调用demo()函数。
这个教程将展示如何使用内置引擎,并且还会教我们如何使用nltk.chat模块提供的框架编写我们自己的简单对话引擎。
做好准备
你应该已经安装了 Python,以及nltk库。了解正则表达式也很有帮助。
如何做到这一点...
-
打开 Atom 编辑器(或你喜欢的编程编辑器)。
-
创建一个名为
Conversational.py的新文件。 -
输入以下源代码:
-
保存文件。
-
使用 Python 解释器运行程序。
-
你将看到如下输出:
它是如何工作的...
让我们尝试理解我们在这里想要实现的目标。这个指令将nltk库导入当前程序中。
import nltk
这个指令定义了一个名为builtinEngines的新函数,它接收一个字符串参数whichOne:
def builtinEngines(whichOne):
这些 if, elif, else 指令是典型的分支指令,它们根据whichOne变量中提供的参数来决定调用哪个聊天引擎的demo()函数。当用户传递一个未知的引擎名称时,它会显示一条信息,告诉用户该引擎并不在其了解范围内。
if whichOne == 'eliza':
nltk.chat.eliza.demo()
elif whichOne == 'iesha':
nltk.chat.iesha.demo()
elif whichOne == 'rude':
nltk.chat.rude.demo()
elif whichOne == 'suntsu':
nltk.chat.suntsu.demo()
elif whichOne == 'zen':
nltk.chat.zen.demo()
else:
print("unknown built-in chat engine {}".format(whichOne))
处理所有已知和未知情况是一个好习惯;这能让我们的程序在处理未知情况时更加稳健。
这个指令定义了一个名为myEngine()的新函数;这个函数不接收任何参数。
def myEngine():
这是一个单一的指令,我们在其中定义了一个嵌套的元组数据结构,并将其分配给聊天对。
chatpairs = (
(r"(.*?)Stock price(.*)",
("Today stock price is 100",
"I am unable to find out the stock price.")),
(r"(.*?)not well(.*)",
("Oh, take care. May be you should visit a doctor",
"Did you take some medicine ?")),
(r"(.*?)raining(.*)",
("Its monsoon season, what more do you expect ?",
"Yes, its good for farmers")),
(r"How(.*?)health(.*)",
("I am always healthy.",
"I am a program, super healthy!")),
(r".*",
("I am good. How are you today ?",
"What brings you here ?"))
)
让我们仔细关注这个数据结构:
-
我们正在定义一个元组的元组
-
每个子元组由两个元素组成:
-
第一个成员是一个正则表达式(这是用户的问题,采用正则表达式格式)
-
元组的第二个成员是另一组元组(这些是答案)
-
我们在myEngine()函数内部定义了一个名为chat()的子函数。在 Python 中这是允许的。这个chat()函数在屏幕上向用户显示一些信息,并调用 nltk 内置的nltk.chat.util.Chat()类,传入chatpairs变量。它将nltk.chat.util.reflections作为第二个参数。最后,我们在使用chat()类创建的对象上调用chatbot.converse()函数。
def chat():
print("!"*80)
print(" >> my Engine << ")
print("Talk to the program using normal english")
print("="*80)
print("Enter 'quit' when done")
chatbot = nltk.chat.util.Chat(chatpairs, nltk.chat.util.reflections)
chatbot.converse()
这个指令调用了chat()函数,它在屏幕上显示一个提示,并接受用户的请求。根据我们之前构建的正则表达式,它会显示响应:
chat()
当程序作为独立程序被调用时(不是通过导入),这些指令将被调用。
if __name__ == '__main__':
for engine in ['eliza', 'iesha', 'rude', 'suntsu', 'zen']:
print("=== demo of {} ===".format(engine))
builtinEngines(engine)
print()
myEngine()
它们完成了这两件事:
-
依次调用内置引擎(以便我们可以体验它们)
-
一旦所有五个内置引擎被激活,它们会调用我们的
myEngine(),这时我们的客户引擎开始发挥作用
第九章:深度学习在自然语言处理中的应用
本章将涵盖以下内容:
-
在生成 TF-IDF 后,使用深度神经网络进行电子邮件分类
-
使用卷积神经网络 CNN 1D 进行 IMDB 情感分类
-
使用双向 LSTM 进行 IMDB 情感分类
-
使用神经词向量可视化在二维空间中可视化高维词汇
引言
最近,深度学习在文本、语音和图像数据的应用中变得非常突出,获得了最先进的结果,这些结果主要用于人工智能领域应用的创建。然而,这些模型证明在所有应用领域都能产生这样的结果。本章将涵盖自然语言处理(NLP)/文本处理中的各种应用。
卷积神经网络和循环神经网络是深度学习中的核心主题,您将在整个领域中不断遇到。
卷积神经网络
卷积神经网络(CNN)主要用于图像处理,将图像分类为固定类别等。CNN 的工作原理已在下图中描述,其中一个 3x3 的滤波器对一个 5x5 的原始矩阵进行卷积,产生一个 3x3 的输出。滤波器可以按步长 1 或大于 1 的值水平滑动。对于单元(1,1),得到的值是 3,它是底层矩阵值和滤波器值的乘积。通过这种方式,滤波器将遍历原始 5x5 矩阵,生成 3x3 的卷积特征,也称为激活图:
使用卷积的优点:
-
与固定大小不同,完全连接的层节省了神经元的数量,从而减少了机器的计算能力需求。
-
只使用小尺寸的滤波器权重在矩阵上滑动,而不是每个像素连接到下一个层。因此,这是一种更好的方式,将输入图像摘要到下一个层。
-
在反向传播过程中,只需根据反向传播的误差更新滤波器的权重,因此效率较高。
CNN 执行空间/时间分布数组之间的映射,适用于时间序列、图像或视频等应用。CNN 的特点包括:
-
平移不变性(神经权重在空间平移方面是固定的)
-
局部连接(神经连接仅存在于空间上局部的区域之间)
-
空间分辨率的可选逐渐降低(随着特征数量的逐步增加)
卷积后,卷积特征/激活图需要根据最重要的特征进行降维,因为相同的操作减少了点数并提高了计算效率。池化是通常用来减少不必要表示的操作。关于池化操作的简要说明如下:
- 池化(Pooling):池化使得激活表示(从在输入组合和权重值上进行卷积的滤波器获得)变小且更易管理。它独立地作用于每个激活映射。在池化阶段,宽度和高度将被应用,而深度在此过程中将保持不变。在下图中,解释了一个 2 x 2 的池化操作。每个原始的 4 x 4 矩阵已经缩小了一半。在前四个单元格值 2、4、5 和 8 中,提取了最大值,即 8:
由于卷积的操作,像素/输入数据大小在各个阶段自然会减少。但在某些情况下,我们确实希望在操作中保持大小。一种可行的方法是在顶层相应地用零填充。
- 填充(Padding):以下图(其宽度和深度)将依次缩小;这在深度网络中是不希望的,填充可以保持图片大小在整个网络中恒定或可控。
基于给定的输入宽度、滤波器大小、填充和步长的简单方程如下所示。这个方程给出了需要多少计算能力的概念,等等。
- 激活映射大小的计算:在下面的公式中,从卷积层获得的激活映射的大小是:
其中,W是原始图像的宽度,F是滤波器大小,P是填充大小(单层填充为1,双层填充为2,依此类推),S是步长长度
例如,考虑一个大小为 224 x 224 x 3 的输入图像(3 表示红、绿和蓝通道),滤波器大小为 11 x 11,滤波器数量为 96。步长为 4,没有填充。这些滤波器生成的激活映射大小是多少?
激活映射的维度将是 55 x 55 x 96。使用前述公式,只能计算宽度和深度,但深度取决于使用的滤波器数量。事实上,在 AlexNet 的卷积阶段第一步骤中得到了这个结果,我们现在将进行描述。
- AlexNet 在 2012 年 ImageNet 竞赛中的应用:以下图描述了 AlexNet,在 2012 年的 ImageNet 竞赛中获胜。它相比其他竞争者显著提高了准确性。
在 AlexNet 中,所有技术如卷积、池化和填充都被使用,并最终与全连接层连接。
CNN 的应用
CNNs 在各种应用中被使用,以下是其中的几个例子:
-
图像分类:与其他方法相比,CNN 在大规模图像数据集上具有更高的准确性。在图像分类中,CNN 在初始阶段被使用,一旦通过池化层提取了足够的特征,接着使用其他 CNN 层,最后通过全连接层将它们分类到指定类别中。
-
人脸识别:CNN 对位置、亮度等不变,这使得它能够从图像中识别人脸,并在光线不佳、人脸侧面朝向等情况下依然能够处理图像。
-
场景标注:在场景标注中,每个像素都会被标记为它所属的物体类别。这里使用卷积神经网络(CNN)以层次化的方式将像素组合在一起。
-
自然语言处理(NLP):在 NLP 中,CNN 与词袋模型类似使用,其中词语的顺序在识别电子邮件/文本等的最终类别时并不起关键作用。CNN 被应用于矩阵,这些矩阵由句子以向量形式表示。随后,滤波器会应用于其中,但 CNN 是单维的,宽度保持不变,滤波器仅在高度(对于二元组,高度为 2;对于三元组,高度为 3,依此类推)上进行遍历。
循环神经网络
循环神经网络用于处理一系列向量 X,通过在每个时间步应用递归公式。在卷积神经网络中,我们假设所有输入是彼此独立的。但在某些任务中,输入是相互依赖的,例如时间序列预测数据,或根据过去的词预测句子的下一个词等,这些都需要通过考虑过去序列的依赖关系来建模。这类问题通过 RNN 进行建模,能够提供更高的准确性。理论上,RNN 能够利用任意长序列中的信息,但实际上,它们仅限于回顾过去的几个步骤。下述公式解释了 RNN 的功能:
-
RNN 中的梯度消失或爆炸问题:随着层数的增加,梯度会迅速消失,这个问题在 RNN 中尤为严重,因为在每一层都会有很多时间步,而循环权重本质上是乘法性的,因此梯度要么爆炸,要么迅速消失,这使得神经网络无法训练。通过使用梯度裁剪技术,可以限制梯度爆炸,设定一个上限来限制梯度的爆炸,但消失梯度问题仍然存在。这个问题可以通过使用长短期记忆(LSTM)网络来克服。
-
LSTM:LSTM 是一种人工神经网络,除了常规的网络单元外,还包含 LSTM 块。LSTM 块包含门控机制,用于决定何时输入的内容足够重要以被记住,何时需要继续记住,何时需要忘记该值,以及何时输出该值。
LSTM 不会发生梯度消失和爆炸问题,因为它是加性模型,而不是 RNN 中的乘法模型。
RNN 在 NLP 中的应用
RNN 在许多 NLP 任务中取得了巨大成功。RNN 最常用的变体是 LSTM,因为它克服了梯度消失/爆炸的问题。
-
语言建模:给定一系列单词,任务是预测下一个可能的单词
-
文本生成:从某些作者的作品中生成文本
-
机器翻译:将一种语言转换为另一种语言(如英语到中文等)
-
聊天机器人:这个应用非常类似于机器翻译;然而,问答对被用来训练模型
-
生成图像描述:通过与 CNN 一起训练,RNN 可用于生成图像的标题/描述
在生成 TF-IDF 后使用深度神经网络对电子邮件进行分类
在这个教程中,我们将使用深度神经网络根据每封电子邮件中出现的单词将电子邮件分类到 20 个预训练类别之一。这是一个简单的模型,可以帮助我们理解深度学习的主题及其在 NLP 中的应用。
准备工作
使用来自 scikit-learn 的 20 个新闻组数据集来说明该概念。用于分析的观察/电子邮件数量为 18,846(训练数据 - 11,314,测试数据 - 7,532),其对应的类别/类为 20,如下所示:
>>> from sklearn.datasets import fetch_20newsgroups
>>> newsgroups_train = fetch_20newsgroups(subset='train')
>>> newsgroups_test = fetch_20newsgroups(subset='test')
>>> x_train = newsgroups_train.data
>>> x_test = newsgroups_test.data
>>> y_train = newsgroups_train.target
>>> y_test = newsgroups_test.target
>>> print ("List of all 20 categories:")
>>> print (newsgroups_train.target_names)
>>> print ("\n")
>>> print ("Sample Email:")
>>> print (x_train[0])
>>> print ("Sample Target Category:")
>>> print (y_train[0])
>>> print (newsgroups_train.target_names[y_train[0]])
在以下屏幕截图中,展示了一个样本的首个数据观察及目标类别。通过第一个观察或电子邮件,我们可以推断出该电子邮件是在谈论一辆双门跑车,我们可以将其手动分类为汽车类别 8。
目标值是 7(由于索引从 0 开始),这验证了我们对实际目标类别 7 的理解。
如何实现...
使用 NLP 技术,我们已经对数据进行了预处理,以获得最终的单词向量,并与最终结果(垃圾邮件或正常邮件)进行映射。主要步骤包括:
-
预处理。
-
移除标点符号。
-
单词分词。
-
将单词转换为小写字母。
-
停用词移除。
-
保持单词长度至少为 3。
-
词干提取。
-
词性标注。
-
词形还原:
-
TF-IDF 向量转换。
-
深度学习模型训练与测试。
-
模型评估和结果讨论。
-
它是如何工作的...
已经使用 NLTK 包处理了所有的预处理步骤,因为它包含了所有必要的 NLP 功能,集中在一个平台上:
# Used for pre-processing data
>>> import nltk
>>> from nltk.corpus import stopwords
>>> from nltk.stem import WordNetLemmatizer
>>> import string
>>> import pandas as pd
>>> from nltk import pos_tag
>>> from nltk.stem import PorterStemmer
编写的函数(预处理)包含了所有步骤,便于操作。然而,我们将在每个章节中解释每一步:
>>> def preprocessing(text):
以下代码行将单词拆分,并检查每个字符是否包含任何标准标点符号,如果包含则替换为空格,否则保持不变:
... text2 = " ".join("".join([" " if ch in string.punctuation else ch for ch in text]).split())
以下代码将句子根据空格分词,并将它们组合成一个列表以应用进一步步骤:
... tokens = [word for sent in nltk.sent_tokenize(text2) for word in nltk.word_tokenize(sent)]
将所有大小写(大写、小写和专有名词)转为小写,可以减少语料库中的重复:
... tokens = [word.lower() for word in tokens]
如前所述,停用词是那些在理解句子时没有太多意义的词,它们用于连接词等。我们已通过以下代码将它们移除:
... stopwds = stopwords.words('english')
... tokens = [token for token in tokens if token not in stopwds]
在下面的代码中,仅保留长度大于3的单词,用于去除那些几乎没有意义的小词;
... tokens = [word for word in tokens if len(word)>=3]
使用 Porter 词干提取器对单词进行词干化,从单词中去除多余的后缀:
... stemmer = PorterStemmer()
... tokens = [stemmer.stem(word) for word in tokens]
词性标注是词形还原的前提,基于词汇是名词、动词等,它将词汇简化为词根。
... tagged_corpus = pos_tag(tokens)
pos_tag函数返回四种名词格式和六种动词格式。NN -(名词,普通,单数),NNP -(名词,专有,单数),NNPS -(名词,专有,复数),NNS -(名词,普通,复数),VB -(动词,原形),VBD -(动词,过去式),VBG -(动词,现在分词),VBN -(动词,过去分词),VBP -(动词,现在时,非第三人称单数),VBZ -(动词,现在时,第三人称单数)
... Noun_tags = ['NN','NNP','NNPS','NNS']
... Verb_tags = ['VB','VBD','VBG','VBN','VBP','VBZ']
... lemmatizer = WordNetLemmatizer()
以下函数prat_lemmatize仅为了解决pos_tag函数与lemmatize函数输入值不匹配的问题而创建。如果某个单词的标签属于名词或动词类别,则lemmatize函数将分别应用n或v标签:
... def prat_lemmatize(token,tag):
... if tag in Noun_tags:
... return lemmatizer.lemmatize(token,'n')
... elif tag in Verb_tags:
... return lemmatizer.lemmatize(token,'v')
... else:
... return lemmatizer.lemmatize(token,'n')
在进行分词并应用所有操作后,我们需要将其重新组合成字符串,以下函数执行了此操作:
... pre_proc_text = " ".join([prat_lemmatize(token,tag) for token,tag in tagged_corpus])
... return pre_proc_text
对训练数据和测试数据应用预处理:
>>> x_train_preprocessed = []
>>> for i in x_train:
... x_train_preprocessed.append(preprocessing(i))
>>> x_test_preprocessed = []
>>> for i in x_test:
... x_test_preprocessed.append(preprocessing(i))
# building TFIDF vectorizer
>>> from sklearn.feature_extraction.text import TfidfVectorizer
>>> vectorizer = TfidfVectorizer(min_df=2, ngram_range=(1, 2), stop_words='english', max_features= 10000,strip_accents='unicode', norm='l2')
>>> x_train_2 = vectorizer.fit_transform(x_train_preprocessed).todense()
>>> x_test_2 = vectorizer.transform(x_test_preprocessed).todense()
预处理步骤完成后,处理后的 TF-IDF 向量需要传入以下深度学习代码:
# Deep Learning modules
>>> import numpy as np
>>> from keras.models import Sequential
>>> from keras.layers.core import Dense, Dropout, Activation
>>> from keras.optimizers import Adadelta,Adam,RMSprop
>>> from keras.utils import np_utils
下图展示了运行前面 Keras 代码后产生的输出。Keras 已经在 Theano 上安装,并且最终在 Python 上运行。安装了一块 6GB 内存的 GPU,并添加了额外的库(CuDNN 和 CNMeM),使执行速度提高了四到五倍,同时内存占用约为 20%,因此仅有 80%的 6GB 内存可用;
以下代码解释了深度学习模型的核心部分。代码是自解释的,考虑到的类别数为20,批次大小为64,训练的 epoch 数为20:
# Definition hyper parameters
>>> np.random.seed(1337)
>>> nb_classes = 20
>>> batch_size = 64
>>> nb_epochs = 20
以下代码将20个类别转换为一热编码向量,其中创建了20列,并且对应类别的值为1。所有其他类别的值为0:
>>> Y_train = np_utils.to_categorical(y_train, nb_classes)
在以下的 Keras 代码构建模块中,使用了三个隐藏层(每层分别有1000、500和50个神经元),每层的 dropout 为 50%,并使用 Adam 优化器:
#Deep Layer Model building in Keras
#del model
>>> model = Sequential()
>>> model.add(Dense(1000,input_shape= (10000,)))
>>> model.add(Activation('relu'))
>>> model.add(Dropout(0.5))
>>> model.add(Dense(500))
>>> model.add(Activation('relu'))
>>> model.add(Dropout(0.5))
>>> model.add(Dense(50))
>>> model.add(Activation('relu'))
>>> model.add(Dropout(0.5))
>>> model.add(Dense(nb_classes))
>>> model.add(Activation('softmax'))
>>> model.compile(loss='categorical_crossentropy', optimizer='adam')
>>> print (model.summary())
架构如下所示,描述了从输入开始为 10,000 的数据流。然后有1000、500、50和20个神经元,将给定的电子邮件分类到20个类别中的一个:
模型是根据给定的指标进行训练的:
# Model Training
>>> model.fit(x_train_2, Y_train, batch_size=batch_size, epochs=nb_epochs,verbose=1)
模型已经在 20 个 epoch 中进行了拟合,每个 epoch 大约需要 2 秒钟。损失从1.9281减少到0.0241。使用 CPU 硬件时,每个 epoch 所需的时间可能会增加,因为 GPU 通过数千个线程/核心并行计算大大加速了计算:
最后,在训练集和测试集上进行预测,以确定准确率、精度和召回值:
#Model Prediction
>>> y_train_predclass = model.predict_classes(x_train_2,batch_size=batch_size)
>>> y_test_predclass = model.predict_classes(x_test_2,batch_size=batch_size)
>>> from sklearn.metrics import accuracy_score,classification_report
>>> print ("\n\nDeep Neural Network - Train accuracy:"),(round(accuracy_score( y_train, y_train_predclass),3))
>>> print ("\nDeep Neural Network - Test accuracy:"),(round(accuracy_score( y_test,y_test_predclass),3))
>>> print ("\nDeep Neural Network - Train Classification Report")
>>> print (classification_report(y_train,y_train_predclass))
>>> print ("\nDeep Neural Network - Test Classification Report")
>>> print (classification_report(y_test,y_test_predclass))
看起来分类器在训练数据集上提供了 99.9%的准确率,在测试数据集上则为 80.7%。
使用卷积神经网络 CNN 1D 进行 IMDB 情感分类
在这个示例中,我们将使用 Keras IMDB 电影评论情感数据,该数据已经标注了情感(正面/负面)。评论已经过预处理,每个评论已经被编码为一个单词索引序列(整数)。然而,我们已将其解码,以下代码中展示了一个示例。
准备开始
Keras 中的 IMDB 数据集包含一组单词及其相应的情感。以下是数据的预处理过程:
>>> import pandas as pd
>>> from keras.preprocessing import sequence
>>> from keras.models import Sequential
>>> from keras.layers import Dense, Dropout, Activation
>>> from keras.layers import Embedding
>>> from keras.layers import Conv1D, GlobalMaxPooling1D
>>> from keras.datasets import imdb
>>> from sklearn.metrics import accuracy_score,classification_report
在这一组参数中,我们设置了最大特征或要提取的单词数为 6,000,并且单个句子的最大长度为 400 个单词:
# set parameters:
>>> max_features = 6000
>>> max_length = 400
>>> (x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)
>>> print(len(x_train), 'train observations')
>>> print(len(x_test), 'test observations')
数据集具有相同数量的训练和测试观察值,我们将在 25,000 个观察值上构建模型,并在测试数据的 25,000 个数据观察值上测试训练好的模型。以下截图展示了数据的一个示例:
以下代码用于创建单词及其相应整数索引值的字典映射:
# Creating numbers to word mapping
>>> wind = imdb.get_word_index()
>>> revind = dict((v,k) for k,v in wind.iteritems())
>>> print (x_train[0])
>>> print (y_train[0])
我们看到的第一个观察值是数字的集合,而不是任何英文单词,因为计算机只能理解并处理数字,而不是字符、单词等:
使用创建的逆映射字典进行解码,如下所示:
>>> def decode(sent_list):
... new_words = []
... for i in sent_list:
... new_words.append(revind[i])
... comb_words = " ".join(new_words)
... return comb_words
>>> print (decode(x_train[0]))
以下截图描述了将数字映射转换为文本格式后的阶段。在这里,字典被用来反转从整数格式到文本格式的映射:
如何做...
主要步骤如下:
-
预处理:在此阶段,我们对序列进行填充,使所有观察值具有相同的固定维度,这有助于提高速度并实现计算。
-
CNN 1D 模型的开发与验证。
-
模型评估。
它是如何工作的...
以下代码执行填充操作,添加额外的句子,使其达到最大长度 400 个单词。通过这样做,数据将变得均匀,并且更容易通过神经网络进行计算:
#Pad sequences for computational efficiency
>>> x_train = sequence.pad_sequences(x_train, maxlen=max_length)
>>> x_test = sequence.pad_sequences(x_test, maxlen=max_length)
>>> print('x_train shape:', x_train.shape)
>>> print('x_test shape:', x_test.shape)
以下深度学习代码描述了应用 Keras 代码创建 CNN 1D 模型:
# Deep Learning architecture parameters
>>> batch_size = 32
>>> embedding_dims = 60
>>> num_kernels = 260
>>> kernel_size = 3
>>> hidden_dims = 300
>>> epochs = 3
# Building the model
>>> model = Sequential()
>>> model.add(Embedding(max_features,embedding_dims, input_length= max_length))
>>> model.add(Dropout(0.2))
>>> model.add(Conv1D(num_kernels,kernel_size, padding='valid', activation='relu', strides=1))
>>> model.add(GlobalMaxPooling1D())
>>> model.add(Dense(hidden_dims))
>>> model.add(Dropout(0.5))
>>> model.add(Activation('relu'))
>>> model.add(Dense(1))
>>> model.add(Activation('sigmoid'))
>>> model.compile(loss='binary_crossentropy',optimizer='adam', metrics=['accuracy'])
>>> print (model.summary())
在以下截图中,显示了整个模型的总结,指出了维度数量及其各自的神经元数量。这些直接影响将用于计算的参数数量,从输入数据到最终目标变量(无论是0还是1)。因此,在网络的最后一层使用了一个全连接层:
以下代码执行训练数据的模型拟合操作,其中X和Y变量用于按批次训练数据:
>>> model.fit(x_train, y_train,batch_size=batch_size,epochs=epochs, validation_split=0.2)
模型已训练三个周期,每个周期在 GPU 上消耗 5 秒。但如果我们观察以下迭代,尽管训练准确率在上升,验证准确率却在下降。这个现象可以被识别为模型过拟合。这表明我们需要尝试其他方法来提高模型准确率,而不仅仅是增加训练周期的次数。我们可能应该关注的方法还包括增加架构的规模等。鼓励读者尝试不同的组合。
以下代码用于预测训练数据和测试数据的类别:
#Model Prediction
>>> y_train_predclass = model.predict_classes(x_train,batch_size=batch_size)
>>> y_test_predclass = model.predict_classes(x_test,batch_size=batch_size)
>>> y_train_predclass.shape = y_train.shape
>>> y_test_predclass.shape = y_test.shape
# Model accuracies and metrics calculation
>>> print (("\n\nCNN 1D - Train accuracy:"),(round(accuracy_score(y_train, y_train_predclass),3)))
>>> print ("\nCNN 1D of Training data\n",classification_report(y_train, y_train_predclass))
>>> print ("\nCNN 1D - Train Confusion Matrix\n\n",pd.crosstab(y_train, y_train_predclass,rownames = ["Actuall"],colnames = ["Predicted"]))
>>> print (("\nCNN 1D - Test accuracy:"),(round(accuracy_score(y_test, y_test_predclass),3)))
>>> print ("\nCNN 1D of Test data\n",classification_report(y_test, y_test_predclass))
>>> print ("\nCNN 1D - Test Confusion Matrix\n\n",pd.crosstab(y_test, y_test_predclass,rownames = ["Actuall"],colnames = ["Predicted"]))
以下截图描述了用于判断模型性能的各种可度量指标。从结果来看,训练准确率高达 96%,然而测试准确率为 88.2%,略低。这可能是由于模型过拟合:
使用双向 LSTM 进行 IMDB 情感分类
在本教程中,我们使用相同的 IMDB 情感数据,展示 CNN 和 RNN 方法在准确率等方面的差异。数据预处理步骤保持不变,只有模型架构不同。
准备就绪
Keras 中的 IMDB 数据集包含了一组词汇及其对应的情感。以下是数据的预处理过程:
>>> from __future__ import print_function
>>> import numpy as np
>>> import pandas as pd
>>> from keras.preprocessing import sequence
>>> from keras.models import Sequential
>>> from keras.layers import Dense, Dropout, Embedding, LSTM, Bidirectional
>>> from keras.datasets import imdb
>>> from sklearn.metrics import accuracy_score,classification_report
# Max features are limited
>>> max_features = 15000
>>> max_len = 300
>>> batch_size = 64
# Loading data
>>> (x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)
>>> print(len(x_train), 'train observations')
>>> print(len(x_test), 'test observations')
如何做...
主要步骤如下:
-
预处理,在此阶段,我们对序列进行填充,使所有观测值都进入一个固定的维度,从而提高速度并实现计算。
-
LSTM 模型的开发与验证。
-
模型评估。
它是如何工作的...
# Pad sequences for computational efficiently
>>> x_train_2 = sequence.pad_sequences(x_train, maxlen=max_len)
>>> x_test_2 = sequence.pad_sequences(x_test, maxlen=max_len)
>>> print('x_train shape:', x_train_2.shape)
>>> print('x_test shape:', x_test_2.shape)
>>> y_train = np.array(y_train)
>>> y_test = np.array(y_test)
以下深度学习代码描述了 Keras 代码的应用,用于创建一个双向 LSTM 模型:
双向 LSTM 具有来自前向和后向的连接,这使得它们能够填补中间单词,与左右单词更好地连接:
# Model Building
>>> model = Sequential()
>>> model.add(Embedding(max_features, 128, input_length=max_len))
>>> model.add(Bidirectional(LSTM(64)))
>>> model.add(Dropout(0.5))
>>> model.add(Dense(1, activation='sigmoid'))
>>> model.compile('adam', 'binary_crossentropy', metrics=['accuracy'])
# Print model architecture
>>> print (model.summary())
这里是模型的架构。嵌入层被用来将维度减少到128,接着是双向 LSTM,最后用一个全连接层来建模情感(0 或 1):
以下代码用于训练数据:
#Train the model
>>> model.fit(x_train_2, y_train,batch_size=batch_size,epochs=4, validation_split=0.2)
LSTM 模型的训练时间比 CNN 长,因为 LSTM 不容易在 GPU 上进行并行化(比 CNN 慢 4 到 5 倍),而 CNN(100 倍)是大规模并行化的。一个重要的观察是:即使训练准确率提高,验证准确率却在下降。这种情况表明出现了过拟合。
以下代码用于预测训练和测试数据的类别:
#Model Prediction
>>> y_train_predclass = model.predict_classes(x_train_2,batch_size=1000)
>>> y_test_predclass = model.predict_classes(x_test_2,batch_size=1000)
>>> y_train_predclass.shape = y_train.shape
>>> y_test_predclass.shape = y_test.shape
# Model accuracies and metrics calculation
>>> print (("\n\nLSTM Bidirectional Sentiment Classification - Train accuracy:"),(round(accuracy_score(y_train,y_train_predclass),3)))
>>> print ("\nLSTM Bidirectional Sentiment Classification of Training data\n",classification_report(y_train, y_train_predclass))
>>> print ("\nLSTM Bidirectional Sentiment Classification - Train Confusion Matrix\n\n",pd.crosstab(y_train, y_train_predclass,rownames = ["Actuall"],colnames = ["Predicted"]))
>>> print (("\nLSTM Bidirectional Sentiment Classification - Test accuracy:"),(round(accuracy_score(y_test,y_test_predclass),3)))
>>> print ("\nLSTM Bidirectional Sentiment Classification of Test data\n",classification_report(y_test, y_test_predclass))
>>> print ("\nLSTM Bidirectional Sentiment Classification - Test Confusion Matrix\n\n",pd.crosstab(y_test, y_test_predclass,rownames = ["Actuall"],colnames = ["Predicted"]))
看起来 LSTM 的测试准确率相较于 CNN 稍微低一些;然而,通过精心调整模型参数,我们可以在 RNN 中获得比 CNN 更好的准确率。
将高维单词在二维中可视化,使用神经网络词向量进行可视化。
在这个方案中,我们将使用深度神经网络,将高维空间中的单词可视化到二维空间中。
准备工作
爱丽丝梦游仙境数据集已被用来提取单词并使用密集网络创建可视化,类似于编码器-解码器架构:
>>> from __future__ import print_function
>>> import os
""" First change the following directory link to where all input files do exist """
>>> os.chdir("C:\\Users\\prata\\Documents\\book_codes\\NLP_DL")
>>> import nltk
>>> from nltk.corpus import stopwords
>>> from nltk.stem import WordNetLemmatizer
>>> from nltk import pos_tag
>>> from nltk.stem import PorterStemmer
>>> import string
>>> import numpy as np
>>> import pandas as pd
>>> import random
>>> from sklearn.model_selection import train_test_split
>>> from sklearn.preprocessing import OneHotEncoder
>>> import matplotlib.pyplot as plt
>>> def preprocessing(text):
... text2 = " ".join("".join([" " if ch in string.punctuation else ch for ch in text]).split())
... tokens = [word for sent in nltk.sent_tokenize(text2) for word in
nltk.word_tokenize(sent)]
... tokens = [word.lower() for word in tokens]
... stopwds = stopwords.words('english')
... tokens = [token for token in tokens if token not in stopwds]
... tokens = [word for word in tokens if len(word)>=3]
... stemmer = PorterStemmer()
... tokens = [stemmer.stem(word) for word in tokens]
... tagged_corpus = pos_tag(tokens)
... Noun_tags = ['NN','NNP','NNPS','NNS']
... Verb_tags = ['VB','VBD','VBG','VBN','VBP','VBZ']
... lemmatizer = WordNetLemmatizer()
... def prat_lemmatize(token,tag):
... if tag in Noun_tags:
... return lemmatizer.lemmatize(token,'n')
... elif tag in Verb_tags:
... return lemmatizer.lemmatize(token,'v')
... else:
... return lemmatizer.lemmatize(token,'n')
... pre_proc_text = " ".join([prat_lemmatize(token,tag) for token,tag in tagged_corpus])
... return pre_proc_text
>>> lines = []
>>> fin = open("alice_in_wonderland.txt", "rb")
>>> for line in fin:
... line = line.strip().decode("ascii", "ignore").encode("utf-8")
... if len(line) == 0:
... continue
... lines.append(preprocessing(line))
>>> fin.close()
如何实现...
主要步骤如下所示:
-
预处理,创建跳字模型,并使用中间单词预测左侧或右侧的单词。
-
对特征工程应用独热编码。
-
使用编码器-解码器架构构建模型。
-
提取编码器架构,从测试数据中创建用于可视化的二维特征。
它是如何工作的...
以下代码创建了一个字典,它是单词与索引、索引与单词(反向)的映射。正如我们所知道的,模型不能直接处理字符/单词输入。因此,我们将把单词转换成数字等价物(特别是整数映射),一旦通过神经网络模型完成计算,反向映射(索引到单词)将被应用以进行可视化。collections库中的计数器用于高效创建字典:
>>> import collections
>>> counter = collections.Counter()
>>> for line in lines:
... for word in nltk.word_tokenize(line):
... counter[word.lower()]+=1
>>> word2idx = {w:(i+1) for i,(w,_) in enumerate(counter.most_common())}
>>> idx2word = {v:k for k,v in word2idx.items()}
以下代码应用了词到整数的映射,并从嵌入中提取三元组。Skip-gram 是一种方法,其中中心单词与左侧和右侧相邻的单词连接进行训练,如果在测试阶段预测正确:
>>> xs = []
>>> ys = []
>>> for line in lines:
... embedding = [word2idx[w.lower()] for w in nltk.word_tokenize(line)]
... triples = list(nltk.trigrams(embedding))
... w_lefts = [x[0] for x in triples]
... w_centers = [x[1] for x in triples]
... w_rights = [x[2] for x in triples]
... xs.extend(w_centers)
... ys.extend(w_lefts)
... xs.extend(w_centers)
... ys.extend(w_rights)
以下代码描述了字典的长度就是词汇表的大小。不过,根据用户的指定,可以选择任何自定义的词汇表大小。在这里,我们考虑了所有的单词!
>>> print (len(word2idx))
>>> vocab_size = len(word2idx)+1
基于词汇表的大小,所有独立和依赖变量都被转换为向量表示,使用以下代码,其中行数为单词数量,列数为词汇表的大小。神经网络模型基本上是在向量空间中映射输入和输出变量:
>>> ohe = OneHotEncoder(n_values=vocab_size)
>>> X = ohe.fit_transform(np.array(xs).reshape(-1, 1)).todense()
>>> Y = ohe.fit_transform(np.array(ys).reshape(-1, 1)).todense()
>>> Xtrain, Xtest, Ytrain, Ytest,xstr,xsts = train_test_split(X, Y,xs, test_size=0.3, random_state=42)
>>> print(Xtrain.shape, Xtest.shape, Ytrain.shape, Ytest.shape)
在总共 13,868 个观测值中,训练集和测试集被分为 70% 和 30%,分别为 9,707 和 4,161:
模型的核心部分通过以下几行使用 Keras 软件编写的深度学习代码进行描述。这是一个收敛-发散代码,最初将所有输入单词的维度压缩,以达到输出格式。
在此过程中,第二层将维度降低到二维。训练完模型后,我们将提取到第二层以对测试数据进行预测。这的工作原理与传统的编码器-解码器架构类似:
>>> from keras.layers import Input,Dense,Dropout
>>> from keras.models import Model
>>> np.random.seed(42)
>>> BATCH_SIZE = 128
>>> NUM_EPOCHS = 20
>>> input_layer = Input(shape = (Xtrain.shape[1],),name="input")
>>> first_layer = Dense(300,activation='relu',name = "first")(input_layer)
>>> first_dropout = Dropout(0.5,name="firstdout")(first_layer)
>>> second_layer = Dense(2,activation='relu',name="second") (first_dropout)
>>> third_layer = Dense(300,activation='relu',name="third") (second_layer)
>>> third_dropout = Dropout(0.5,name="thirdout")(third_layer)
>>> fourth_layer = Dense(Ytrain.shape[1],activation='softmax',name = "fourth")(third_dropout)
>>> history = Model(input_layer,fourth_layer)
>>> history.compile(optimizer = "rmsprop",loss= "categorical_crossentropy", metrics=["accuracy"])
以下代码用于训练模型:
>>> history.fit(Xtrain, Ytrain, batch_size=BATCH_SIZE,epochs=NUM_EPOCHS, verbose=1,validation_split = 0.2)
通过仔细观察训练和验证数据集的准确性,我们可以发现最佳准确率值甚至没有超过 6%。这主要是由于数据量有限以及深度学习模型的架构问题。为了使其真正有效,我们需要至少几千兆字节的数据和大型架构。模型也需要训练很长时间。由于实际限制和示范目的,我们仅训练了 20 次迭代。然而,鼓励读者尝试各种组合以提高准确性。
# Extracting Encoder section of the Model for prediction of latent variables
>>> encoder = Model(history.input,history.get_layer("second").output)
# Predicting latent variables with extracted Encoder model
>>> reduced_X = encoder.predict(Xtest)
Converting the outputs into Pandas data frame structure for better representation
>>> final_pdframe = pd.DataFrame(reduced_X)
>>> final_pdframe.columns = ["xaxis","yaxis"]
>>> final_pdframe["word_indx"] = xsts
>>> final_pdframe["word"] = final_pdframe["word_indx"].map(idx2word)
>>> rows = random.sample(final_pdframe.index, 100)
>>> vis_df = final_pdframe.ix[rows]
>>> labels = list(vis_df["word"]);xvals = list(vis_df["xaxis"])
>>> yvals = list(vis_df["yaxis"])
#in inches
>>> plt.figure(figsize=(8, 8))
>>> for i, label in enumerate(labels):
... x = xvals[i]
... y = yvals[i]
... plt.scatter(x, y)
... plt.annotate(label,xy=(x, y),xytext=(5, 2),textcoords='offset points', ha='right',va='bottom')
>>> plt.xlabel("Dimension 1")
>>> plt.ylabel("Dimension 2")
>>> plt.show()
以下图像描述了在二维空间中单词的可视化。某些单词彼此靠得比其他单词近,这表示它们与附近单词的接近性和关系。例如,never、ever 和 ask 这些单词彼此非常接近。