深度学习自然语言处理教程-全-
深度学习自然语言处理教程(全)
原文:Deep Learning for Natural Language Processing
协议:CC BY-NC-SA 4.0
一、自然语言处理和深度学习导论
自然语言处理是计算机科学中一项极其困难的任务。语言带来了各种各样的问题,这些问题因语言而异。如果方法正确,从自由文本中构造或提取有意义的信息是一个很好的解决方案。以前,计算机科学家将语言分解成语法形式,如词类、短语等。,使用复杂的算法。今天,深度学习是执行相同练习的关键。
自然语言处理深度学习的第一章为读者提供了 Python 语言、NLP 和深度学习的基础知识。首先,我们介绍了 Pandas、NumPy 和 SciPy 库中的初级代码。我们假设用户已经设置了初始 Python 环境(2.x 或 3.x ),并安装了这些库。我们还将简要讨论 NLP 中常用的库,并给出一些基本的例子。最后,我们将讨论深度学习背后的概念和一些常见的框架,如 TensorFlow 和 Keras。然后,在后面的章节中,我们将继续提供 NLP 的更高层次的概述。
根据机器和版本首选项,可以使用以下参考资料安装 Python:
www.python.org/downloads/
www.continuum.io/downloads
前面的链接和基础包安装将为用户提供深度学习所需的环境。
我们将使用下面的包开始。除了软件包名称供您参考之外,请参考以下链接:
Python 机器学习
- 熊猫(
http://pandas.pydata.org/pandas-docs/stable
- NumPy (
www.numpy.org
- scipy(“??””
Python 深度学习
- TensorFlow (
http://tensorflow.org/
) - 硬 (
https://keras.io/
)
Python 自然语言处理
- 空间(
https://spacy.io/
) - NLTK (
www.nltk.org/
- 文本块(
http://textblob.readthedocs.io/en/dev/
如果需要,我们可能会安装其他相关的软件包。如果您在安装的任何阶段遇到问题,请参考以下链接: https://packaging.python.org/tutorials/installing-packages/
。
Note
参考 Python 包索引 PyPI ( https://pypi.python.org/pypi
,搜索最新可用的包。
按照步骤通过 https://pip.pypa.io/en/stable/installing/
安装 pip。
Python 包
我们将涉及到 Pandas、NumPy 和 SciPy 包的安装步骤和初始编码的参考。目前,Python 提供了 2.x 和 3.x 版本,具有兼容机器学习的功能。我们将在需要的地方使用 Python2.7 和 Python3.5。3.5 版在本书的各个章节中被广泛使用。
NumPy
NumPy 特别用于 Python 中的科学计算。它被设计用来有效地操作任意记录的大型多维数组,而不会牺牲小型多维数组的太多速度。它还可以用作通用数据的多维容器。NumPy 能够创建任意类型的数组,这也使得 NumPy 适合于与通用数据库应用程序进行接口,这使得它成为您将在本书中或之后使用的最有用的库之一。
下面是使用 NumPy 包的代码。大多数代码行都附加了注释,以便用户更容易理解。
## Numpy
import numpy as np # Importing the Numpy package
a= np.array([1,4,5,8], float) # Creating Numpy array with Float variables
print(type(a)) #Type of variable
> <class 'numpy.ndarray'>
# Operations on the array
a[0] = 5 #Replacing the first element of the array
print(a)
> [ 5\. 4\. 5\. 8.]
b = np.array([[1,2,3],[4,5,6]], float) # Creating a 2-D numpy array
b[0,1] # Fetching second element of 1st array
> 2.0
print(b.shape) #Returns tuple with the shape of array
> (2, 3)
b.dtype #Returns the type of the value stored
> dtype('float64')
print(len(b)) #Returns length of the first axis
> 2
2 in b #'in' searches for the element in the array
> True
0 in b
> False
# Use of 'reshape' : transforms elements from 1-D to 2-D here
c = np.array(range(12), float)
print(c)
print(c.shape)
print('---')
c = c.reshape((2,6)) # reshape the array in the new form
print(c)
print(c.shape)
> [ 0\. 1\. 2\. 3\. 4\. 5\. 6\. 7\. 8\. 9\. 10\. 11.]
(12,)
---
[[ 0\. 1\. 2\. 3\. 4\. 5.] [ 6\. 7\. 8\. 9\. 10\. 11.]]
(2, 6)
c.fill(0) #Fills whole array with single value, done inplace
print(c)
> [[ 0\. 0\. 0\. 0\. 0\. 0.] [ 0\. 0\. 0\. 0\. 0\. 0.]]
c.transpose() #creates transpose of the array, not done inplace
> array([[ 0., 0.], [ 0., 0.], [ 0., 0.], [ 0., 0.], [ 0., 0.], [ 0., 0.]])
c.flatten() #flattens the whole array, not done inplace
> array([ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
# Concatenation of 2 or more arrays
m = np.array([1,2], float)
n = np.array([3,4,5,6], float)
p = np.concatenate((m,n))
print(p)
> [ 1\. 2\. 3\. 4\. 5\. 6.]
(6,)
print(p.shape)
# 'newaxis' : to increase the dimensonality of the array
q = np.array([1,2,3], float)
q[:, np.newaxis].shape
> (3, 1)
NumPy 还有其他函数,如zeros
、ones
、zeros_like
、ones_like
、identity
、eye
,用于创建给定维数的填充 0、1 或 0 和 1 的数组。
加法、减法和乘法发生在相同大小的数组上。NumPy 中的乘法是以元素方式提供的,而不是矩阵乘法。如果数组大小不匹配,则重复较小的数组来执行所需的操作。以下是这方面的一个例子:
a1 = np.array([[1,2],[3,4],[5,6]], float)
a2 = np.array([-1,3], float)
print(a1+a2)
> [[ 0\. 5.] [ 2\. 7.] [ 4\. 9.]]
Note
pi
和e
作为常量包含在 NumPy 包中。
关于 NumPy 的详细教程可以参考以下资料: www.numpy.org/
和 https://docs.scipy.org/doc/numpy-dev/user/quickstart.html
。
NumPy 提供了几个可直接应用于数组的函数:sum
(元素的总和)、prod
(元素的乘积)、mean
(元素的平均值)、var
(元素的方差)、std
(元素的标准差)、argmin
(数组中最小元素的索引)、argmax
(数组中最大元素的索引)、sort
(对元素排序)、unique
(数组中唯一的元素)。
a3 = np.array([[0,2],[3,-1],[3,5]], float)
print(a3.mean(axis=0)) # Mean of elements column-wise
> [ 2\. 2.]
print(a3.mean(axis=1)) # Mean of elements row-wise
> [ 1\. 1\. 4.]
Note
要在多维数组上执行上述操作,请在命令中包含可选参数axis
。
NumPy 提供了测试数组中出现的值的函数,比如nonzero
(检查非零元素)、isnan
(检查“非数字”元素)和isfinite
(检查有限元素)。where
函数返回一个数组,其中的元素满足以下条件:
a4 = np.array([1,3,0], float)
np.where(a!=0, 1/a ,a)
> array([ 0.2 , 0.25 , 0.2 , 0.125])
要生成不同长度的随机数,请使用 NumPy 的 random 函数。
np.random.rand(2,3)
> array([[ 0.41453991, 0.46230172, 0.78318915], [0.54716578, 0.84263735, 0.60796399]])
Note
可以通过numpy.random.seed (1234)
设置随机数种子。NumPy 使用 Mersenne Twister 算法来生成伪随机数。
熊猫
Pandas 是一个开源软件库。DataFrames 和 Series 是广泛用于数据分析目的的两种主要数据结构。Series 是一维索引数组,DataFrame 是具有列级和行级索引的表格数据结构。Pandas 是预处理数据集的一个很好的工具,并提供了高度优化的性能。
import pandas as pd
series_1 = pd.Series([2,9,0,1]) # Creating a series object
print(series_1.values) # Print values of the series object
> [2 9 0 1]
series_1.index # Default index of the series object
> RangeIndex(start=0, stop=4, step=1)
series_1.index = ['a','b','c','d'] #Settnig index of the series object
series_1['d'] # Fetching element using new index
> 1
# Creating dataframe using pandas
class_data = {'Names':['John','Ryan','Emily'],
'Standard': [7,5,8],
'Subject': ['English','Mathematics','Science']}
class_df = pd.DataFrame(class_data, index = ['Student1','Student2','Student3'],
columns = ['Names','Standard','Subject'])
print(class_df)
> Names Standard Subject
Student1 John 7 English
Student2 Ryan 5 Mathematics
Student3 Emily 8 Science
class_df.Names
>Student1 John
Student2 Ryan
Student3 Emily
Name: Names, dtype: object
# Add new entry to the dataframe
import numpy as np
class_df.ix['Student4'] = ['Robin', np.nan, 'History']
class_df.T # Take transpose of the dataframe
> Student1 Student2 Student3 Student4
Names John Ryan Emily Robin
Standard 7 5 8 NaN
Subject English Mathematics Science History
class_df.sort_values(by='Standard') # Sorting of rows by one column
> Names Standard Subject
Student1 John 7.0 English
Student2 Ryan 5.0 Mathematics
Student3 Emily 8.0 Science
Student4 Robin NaN History
# Adding one more column to the dataframe as Series object
col_entry = pd.Series(['A','B','A+','C'],
index=['Student1','Student2','Student3','Student4'])
class_df['Grade'] = col_entry
print(class_df)
> Names Standard Subject Grade
Student1 John 7.0 English A
Student2 Ryan 5.0 Mathematics B
Student3 Emily 8.0 Science A+
Student4 Robin NaN History C
# Filling the missing entries in the dataframe, inplace
class_df.fillna(10, inplace=True)
print(class_df)
> Names Standard Subject Grade
Student1 John 7.0 English A
Student2 Ryan 5.0 Mathematics B
Student3 Emily 8.0 Science A+
Student4 Robin 10.0 History C
# Concatenation of 2 dataframes
student_age = pd.DataFrame(data = {'Age': [13,10,15,18]} ,
index=['Student1','Student2','Student3','Student4'])
print(student_age)
> Age
Student1 13
Student2 10
Student3 15
Student4 18
class_data = pd.concat([class_df, student_age], axis = 1)
print(class_data)
> Names Standard Subject Grade Age
Student1 John 7.0 English A 13
Student2 Ryan 5.0 Mathematics B 10
Student3 Emily 8.0 Science A+ 15
Student4 Robin 10.0 History C 18
Note
使用map
功能对列/行中的每个元素单独执行任何功能,使用apply
功能对列/行中的所有元素同时执行任何功能。
# MAP Function
class_data['Subject'] = class_data['Subject'].map(lambda x : x + 'Sub')
class_data['Subject']
> Student1 EnglishSub
Student2 MathematicsSub
Student3 ScienceSub
Student4 HistorySub
Name: Subject, dtype: object
# APPLY Function
def age_add(x): # Defining a new function which will increment the age by 1
return(x+1)
print('-----Old values-----')
print(class_data['Age'])
print('-----New values-----')
print(class_data['Age'].apply(age_add)) # Applying the age function on top of the age column
> -----Old values-----
Student1 13
Student2 10
Student3 15
Student4 18
Name: Age, dtype: int64-----New values-----
Student1 14
Student2 11
Student3 16
Student4 19
Name: Age, dtype: int64
以下代码用于将列的数据类型更改为“category”类型:
# Changing datatype of the column
class_data['Grade'] = class_data['Grade'].astype('category')
class_data.Grade.dtypes
> category
下面将结果存储到一个.csv
文件中:
# Storing the results
class_data.to_csv('class_dataset.csv', index=False)
在熊猫图书馆提供的函数库中,合并函数(concat
、merge
、append
、groupby
和pivot_table
函数)在数据处理任务中有大量的应用。详细熊猫教程参考以下来源: http://pandas.pydata.org/
。
我的天啊
SciPy 提供了复杂的算法以及它们在 NumPy 中作为函数的用途。这分配了高级命令和各种类来操作和可视化数据。SciPy 是以多个小软件包的形式策划的,每个软件包针对单独的科学计算领域。一些子包是linalg
(线性代数)constants
(物理和数学常数),和sparse
(稀疏矩阵和相关例程)。
适用于数组的大多数 NumPy 包函数也包含在 SciPy 包中。SciPy 提供了预先测试的例程,从而在科学计算应用程序中节省了大量处理时间。
import scipy
import numpy as np
Note
SciPy 为表示随机变量的对象提供了内置的构造函数。
下面是来自 Linalg 和 SciPy 提供的多个子包的一些例子。由于子包是特定于领域的,这使得 SciPy 成为数据科学的最佳选择。
这里用于线性代数(scipy.linalg
)的 SciPy 子包应该以如下方式显式导入:
from scipy import linalg
mat_ = np.array([[2,3,1], [4,9,10], [10,5,6]]) # Matrix Creation
print(mat_)
> [[ 2 3 1] [ 4 9 10] [10 5 6]]
linalg.det(mat_) # Determinant of the matrix
inv_mat = linalg.inv(mat_) # Inverse of the matrix
print(inv_mat)
> [[ 0.02409639 -0.07831325 0.12650602] [ 0.45783133 0.01204819 -0.09638554] [-0.42168675 0.12048193 0.03614458]]
执行奇异值分解和存储各个分量的代码如下:
# Singular Value Decomposition
comp_1, comp_2, comp_3 = linalg.svd(mat_)
print(comp_1)
print(comp_2)
print(comp_3)
> [[-0.1854159 0.0294175 -0.98221971]
[-0.73602677 -0.66641413 0.11898237]
[-0.65106493 0.74500122 0.14521585]]
[ 18.34661713 5.73710697 1.57709968]
[[-0.53555313 -0.56881403 -0.62420625]
[ 0.84418693 -0.38076134 -0.37731848]
[-0.02304957 -0.72902085 0.6841033 ]]
Scipy.stats
是一个巨大的子包,有各种统计分布和函数,可以对不同种类的数据集进行操作。
# Scipy Stats module
from scipy import stats
# Generating a random sample of size 20 from normal distribution with mean 3 and standard deviation 5
rvs_20 = stats.norm.rvs(3,5 , size = 20)
print(rvs_20, '\n --- ')
# Computing the CDF of Beta distribution with a=100 and b=130 as shape parameters at random variable 0.41
cdf_ = scipy.stats.beta.cdf(0.41, a=100, b=130)
print(cdf_)
> [ -0.21654555 7.99621694 -0.89264767 10.89089263 2.63297827
-1.43167281 5.09490009 -2.0530585 -5.0128728 -0.54128795
2.76283347 8.30919378 4.67849196 -0.74481568 8.28278981
-3.57801485 -3.24949898 4.73948566 2.71580005 6.50054556]
---
0.225009574362
关于使用 SciPy 子包的深入示例,请参考 http://docs.scipy.org/doc/
。
自然语言处理导论
我们已经看到了 Python 中三个最有用和最常用的库。提供的例子和参考资料应该足以开始。现在,我们正在将我们的重点领域转移到自然语言处理上。
什么是自然语言处理?
简单来说,自然语言处理是计算机/系统真正理解人类语言并以与人类相同的方式处理语言的能力。
够好了,但有什么大不了的?
人类很容易理解其他人所说/表达的语言。例如,如果我说“美国遵循资本主义经济形式,这种经济形式很适合它”,很容易推断出这句话中使用的 which 与“资本主义经济形式”相关联,但计算机/系统如何理解这一点是个问题。
是什么让自然语言处理变得困难?
在人类之间的正常对话中,有些事情往往是没有说出来的,无论是以某种信号、表情还是只是沉默的形式。然而,作为人类,我们有能力理解对话的潜在意图,而这是计算机所缺乏的。
第二个困难是由于句子中的歧义。这可能是词的层面,可能是句子的层面,也可能是意思的层面。
词汇层面的歧义
想想“不会”这个词。这个词总是有歧义。系统会将缩写视为一个单词还是两个单词,在什么意义上(它的含义会是什么?).
判决层面的双重有罪
考虑下面的句子:
大多数时间旅行者担心他们的行李。
没有标点符号,很难从给定的句子中推断出“时间旅行者”是担心他们的行李还是仅仅是“旅行者”
时间像箭一样飞逝。
花费时间的速度被比作箭的速度,仅给出这句话而没有足够的关于所提到的两个实体的一般性质的信息,这是很难描绘的。
意义层面的歧义
考虑一下 tie 这个词。有三种方法可以处理(解释)这个词:作为参赛者之间的平等分数,作为一件衣服,作为一个动词。
图 1-1 展示了一个简单的谷歌翻译失败。它假设范的意思是一个崇拜者,而不是一个对象。
图 1-1
Example of Google Translate from English to Hindi
这些只是你在 NLP 工作时会遇到的无穷挑战中的一小部分。随着我们继续深入,我们将探索如何处理它们。
我们想通过自然语言处理达到什么目的?
通过 NLP 可以实现的目标是无限的。然而,NLP 有一些常见的应用,主要如下:
- 还记得你上学的时候,老师让全班同学总结一篇课文吗?使用 NLP 可以很好地完成这项任务。
- 文本标注 NLP 可以有效地用于查找一整串文本的上下文(主题标注)。
- 命名实体识别这可以确定一个单词或词组是否代表一个地方、组织或其他任何东西。
- 聊天机器人(Chatbot)人们谈论最多的 NLP 应用是聊天机器人(Chatbot)。它可以发现用户所提问题的意图,并发送适当的答复,这是通过训练过程实现的。
- 语音识别
这个应用程序可以识别口语,并将其转换成文本。
如前所述,NLP 有许多应用。我们的想法是不要被它们吓倒,而是自己学习和开发一个或多个这样的应用程序。
与语言处理相关的常用术语
随着我们越来越深入,有几个术语你会经常遇到。因此,尽快熟悉它们是一个好主意。
- 语音学/音系学研究语言声音及其与书面文字的关系
- 形态学对词的内部结构/词的构成的研究
- 句法研究句子中词与词之间的结构关系
- 语义学研究单词的意思以及这些意思如何组合形成句子的意思
- 语言句子的语用情景运用
- 话语
大于单个句子(上下文)的语言单位
自然语言处理库
以下是 Python 中一些最常用的 NLP 库的基本示例。
我是 NLTK
NLTK (
Note
以下是安装 NLTK 包的推荐方法:pip install nltk
。
您可以将给定的句子标记为单个单词,如下所示:
-
获取单词的同义词。使用 NLTK 可以获得一个单词的同义词列表。
# Make sure to install wordnet, if not done already so # import nltk # nltk.download('wordnet') # Synonyms from nltk.corpus import wordnet word_ = wordnet.synsets("spectacular") print(word_) >> [Synset('spectacular.n.01'), Synset('dramatic.s.02'), Synset('spectacular.s.02'), Synset('outstanding.s.02')] print(word_[0].definition()) # Printing the meaning along of each of the synonyms print(word_[1].definition()) print(word_[2].definition()) print(word_[3].definition()) >> a lavishly produced performance >> sensational in appearance or thrilling in effect >> characteristic of spectacles or drama >> having a quality that thrusts itself into attention
-
词干化和词汇化。词干是指从单词中去掉词缀,返回词根(可能不是真正的单词)。词干化和词干化类似,不同的是,词干化的结果是一个真实的词。
# Stemming from nltk.stem import PorterStemmer stemmer = PorterStemmer() # Create the stemmer object print(stemmer.stem("decreases")) >> decreas #Lemmatization from nltk.stem import WordNetLemmatizer lemmatizer = WordNetLemmatizer() # Create the Lemmatizer object print(lemmatizer.lemmatize("decreases")) >> decrease
import nltk
# Tokenization
sent_ = "I am almost dead this time"
tokens_ = nltk.word_tokenize(sent_)
tokens_
>> ['I', 'am', 'almost', 'dead', 'this', 'time']
文本 Blob
TextBlob ( http://textblob.readthedocs.io/en/dev/index.html
)是一个处理文本数据的 Python 库。它提供了一个简单的 API 来深入研究常见的 NLP 任务,如词性标注、名词短语提取、情感分析、分类等等。你可以用它来进行情感分析。感悟是指隐藏在句子中的一种感觉。极性定义了句子中的否定性或肯定性,而主观性意味着句子是模糊地还是完全确定地讨论某事。
from textblob import TextBlob
# Taking a statement as input
statement = TextBlob("My home is far away from my school.")
# Calculating the sentiment attached with the statement
statement.sentiment
Sentiment(polarity=0.1, subjectivity=1.0)
您还可以使用 TextBlob 进行标记。标记是将文本(语料库)中的单词表示为对应于特定词性的过程。
# Defining a sample text
text = '''How about you and I go together on a walk far away from this place, discussing the things we have never discussed on Deep Learning and Natural Language Processing.'''
blob_ = TextBlob(text) # Making it as Textblob object
blob_
>> TextBlob("How about you and I go together on a walk far away from this place, discussing the things we have never discussed on Deep Learning and Natural Language Processing.")
# This part internally makes use of the 'punkt' resource from the NLTK package, make sure to download it before running this
# import nltk
# nltk.download('punkt')
# nltk.download('averaged_perceptron_tagger')
# Running this separately : python3.6 -m textblob.download_corpora
blob_.tags
>>
[('How', 'WRB'),
('about', 'IN'),
('you', 'PRP'),
('and', 'CC'),
('I', 'PRP'),
('go', 'VBP'),
('together', 'RB'),
('on', 'IN'),
('a', 'DT'),
('walk', 'NN'),
('far', 'RB'),
('away', 'RB'),
('from', 'IN'),
('this', 'DT'),
('place', 'NN'),
('discussing', 'VBG'),
('the', 'DT'),
('things', 'NNS'),
('we', 'PRP'),
('have', 'VBP'),
('never', 'RB'),
('discussed', 'VBN'),
('on', 'IN'),
('Deep', 'NNP'),
('Learning', 'NNP'),
('and', 'CC'),
('Natural', 'NNP'),
('Language', 'NNP'),
('Processing', 'NNP')]
您可以使用 TextBlob 来处理拼写错误。
sample_ = TextBlob("I thinkk the model needs to be trained more!")
print(sample_.correct())
>> I think the model needs to be trained more!
此外,该软件包还提供了语言翻译模块。
# Language Translation
lang_ = TextBlob(u"Voulez-vous apprendre le français?")
lang_.translate(from_lang='fr', to="en")
>> TextBlob("Do you want to learn French?")
空间
SpaCy ( 它是用 Cython 语言编写的,包含各种各样的语言词汇、语法、单词到矢量转换和实体识别的训练模型。
Note
实体识别是用于将文本中发现的多个实体分类到预定义类别中的过程,例如人、对象、位置、组织、日期、事件等。单词向量指的是从词汇到实数向量的单词或短语的映射。
import spacy
# Run below command, if you are getting error
# python -m spacy download en
nlp = spacy.load("en")
william_wikidef = """William was the son of King William II and Anna Pavlovna of Russia. On the abdication of his grandfather William I in 1840, he became the Prince of Orange. On the death of his father in 1849, he succeeded as king of the Netherlands. William married his cousin Sophie of Württemberg in 1839 and they had three sons, William, Maurice, and Alexander, all of whom predeceased him. """
nlp_william = nlp(william_wikidef)
print([ (i, i.label_, i.label) for i in nlp_william.ents])
>> [(William, 'PERSON', 378), (William II, 'PERSON', 378), (Anna Pavlovna, 'PERSON', 378), (Russia, 'GPE', 382), (
, 'GPE', 382), (William, 'PERSON', 378), (1840, 'DATE', 388), (the Prince of Orange, 'LOC', 383), (1849, 'DATE', 388), (Netherlands, 'GPE', 382), (
, 'GPE', 382), (William, 'PERSON', 378), (Sophie, 'GPE', 382), (Württemberg, 'PERSON', 378), (1839, 'DATE', 388), (three, 'CARDINAL', 394), (William, 'PERSON', 378), (Maurice, 'PERSON', 378), (Alexander, 'GPE', 382), (
, 'GPE', 382)]
SpaCy 还提供依存解析,可以进一步用于从文本中提取名词短语,如下所示:
# Noun Phrase extraction
senten_ = nlp('The book deals with NLP')
for noun_ in senten_.noun_chunks:
print(noun_)
print(noun_.text)
print('---')
print(noun_.root.dep_)
print('---')
print(noun_.root.head.text)
>> The book
The book
---
nsubj
---
deals
NLP
NLP
---
pobj
---
with
玄诗
Gensim ( https://pypi.python.org/pypi/gensim
)是另一个重要的库。它主要用于主题建模和文档相似性。Gensim 对于获取单词的单词向量等任务最为有用。
from gensim.models import Word2Vec
min_count = 0
size = 50
window = 2
sentences= "bitcoin is an innovative payment network and a new kind of money."
sentences=sentences.split()
print(sentences)
>> ['bitcoin', 'is', 'an', 'innovative', 'payment', 'network', 'and', 'a', 'new', 'kind', 'of', 'money.']
model = Word2Vec(sentences, min_count=min_count, size=size, window=window)
model
>> <gensim.models.word2vec.Word2Vec at 0x7fd1d889e710>
model['a'] # Vector for the character 'a'
>> array([ 9.70041566e-03, -4.16209083e-03, 8.05089157e-03,
4.81479801e-03, 1.93488982e-03, -4.19071550e-03,
1.41675305e-03, -6.54719025e-03, 3.92444432e-03,
-7.05081783e-03, 7.69438222e-03, 3.89579940e-03,
-9.02676862e-03, -8.58401007e-04, -3.24096601e-03,
9.24982232e-05, 7.13059027e-03, 8.80233292e-03,
-2.46750680e-03, -5.17094415e-03, 2.74592242e-03,
4.08304436e-03, -7.59716751e-03, 8.94313212e-03,
-8.39354657e-03, 5.89343486e-03, 3.76902265e-03,
8.84669367e-04, 1.63217512e-04, 8.95449053e-03,
-3.24510527e-03, 3.52341868e-03, 6.98625855e-03,
-5.50296041e-04, -5.10712992e-03, -8.52414686e-03,
-3.00202984e-03, -5.32727176e-03, -8.02035537e-03,
-9.11156740e-03, -7.68519414e-04, -8.95629171e-03,
-1.65163784e-03, 9.59598401e-04, 9.03090648e-03,
5.31166652e-03, 5.59739536e-03, -4.49402537e-03,
-6.75261812e-03, -5.75679634e-03], dtype=float32)
人们可以从 Google 下载经过训练的向量集,并计算出所需文本的表示,如下所示:
model = gensim.models.KeyedVectors.load_word2vec_format('GoogleNews-vectors-negative300.bin', binary=True)
sentence = ["I", "hope", "it", "is", "going", "good", "for", "you"]
vectors = [model[w] for w in sentence]
(您可以使用以下链接下载示例模型: https://github.com/mmihaltz/word2vec-GoogleNews-vectors
,或者使用给定名称的.bin
文件进行常规搜索,并将其粘贴到您的工作目录中。)
Gensim 还提供 LDA(潜在狄利克雷分配——一种生成统计模型,允许通过未观察到的组来解释观察值集,从而解释为什么数据的某些部分是相似的)模块。这既允许从训练语料库进行 LDA 模型估计,又允许推断新的、看不见的文档上的主题分布。该模型还可以用在线培训的新文档进行更新。
模式
模式( https://pypi.python.org/pypi/Pattern
)对于各种 NLP 任务都很有用,比如词性标注、n-gram 搜索、情感分析,以及 WordNet 和机器学习,比如向量空间建模、k-means 聚类、朴素贝叶斯、K-NN 和 SVM 分类器。
import pattern
from pattern.en import tag
tweet_ = "I hope it is going good for you!"
tweet_l = tweet_.lower()
tweet_tags = tag(tweet_l)
print(tweet_tags)
>> [('i', 'JJ'), ('hope', 'NN'), ('it', 'PRP'), ('is', 'VBZ'), ('going', 'VBG'), ('good', 'JJ'), ('for', 'IN'), ('you', 'PRP'), ('!', '.')]
斯坦福·科伦普
Stanford CoreNLP ( https://stanfordnlp.github.io/CoreNLP/
)提供了单词的基本形式;他们的词类;无论是公司名称、人名等。;将日期、时间和数值正常化。根据短语和句法依赖性标记句子的结构;指示哪些名词短语指代相同的实体;表示情绪;提取实体提及之间的特定或开放类关系;得到人们所说的话;等等。
NLP 入门
在本章的这一部分,我们将获取一个简单的文本数据(比如一个句子)并执行一些基本操作来熟悉 NLP 是如何工作的。这一部分将为你在本书其余部分将要学习的内容提供基础。
使用正则表达式的文本搜索
正则表达式是从给定文本中搜索特定类型的设计或词集的一种非常有用的方法。正则表达式(RE)指定一组与之匹配的字符串。这个模块中的函数允许你检查一个给定的字符串是否匹配一个特定的 RE(或者一个给定的 RE 是否匹配一个特定的字符串,这归结为同样的事情)。
# Text search across the sentence using Regular expression
import re
words = ['very','nice','lecture','day','moon']
expression = '|'.join(words)
re.findall(expression, 'i attended a very nice lecture last year', re.M)
>> ['very', 'nice', 'lecture']
要列出的文本
你可以读取一个文本文件,并根据你的需要把它转换成单词列表或句子列表。
text_file = 'data.txt'
# Method-1 : Individual words as separate elements of the list
with open(text_file) as f:
words = f.read().split()
print(words)
>> ['Are', 'you', 'sure', 'moving', 'ahead', 'on', 'this', 'route', 'is', 'the', 'right', 'thing?']
# Method-2 : Whole text as single element of the list
f = open(text_file , 'r')
words_ = f.readlines()
print(words_)
>> ['Are you sure moving ahead on this route is the right thing?\n']
预处理文本
您可以做很多事情来预处理文本。例如,用一个单词替换另一个单词,删除或添加某些特定类型的单词等。
sentence = 'John has been selected for the trial phase this time. Congrats!!'
sentence=sentence.lower()
# defining the positive and negative words explicitly
positive_words=['awesome','good', 'nice', 'super', 'fun', 'delightful','congrats']
negative_words=['awful','lame','horrible','bad']
sentence=sentence.replace('!','')
sentence
>> 'john has been selected for the trial phase this time. congrats'
words= sentence.split(' ')
print(words)
>> ['john', 'has', 'been', 'selected', 'for', 'the', 'trial', 'phase', 'this', 'time.', 'congrats']
result= set(words)-set(positive_words)
print(result)
>> {'has', 'phase', 'for', 'time.', 'trial', 'been', 'john', 'the', 'this', 'selected'}
从 Web 上访问文本
可以使用urllib
访问来自 URL 的文本文件。
# Make sure both the packages are installed
import urllib3
from bs4 import BeautifulSoup
pool_object = urllib3.PoolManager()
target_url = 'http://www.gutenberg.org/files/2554/2554-h/2554-h.htm#link2HCH0008'
response_ = pool_object.request('GET', target_url)
final_html_txt = BeautifulSoup(response_.data)
print(final_html_txt)
停用词的删除
停用字词是搜索引擎被编程忽略的常用字词(例如)。
import nltk
from nltk import word_tokenize
sentence= "This book is about Deep Learning and Natural Language Processing!"
tokens = word_tokenize(sentence)
print(tokens)
>> ['This', 'book', 'is', 'about', 'Deep', 'Learning', 'and', 'Natural', 'Language', 'Processing', '!']
# nltk.download('stopwords')
from nltk.corpus import stopwords
stop_words = set(stopwords.words('english'))
new_tokens = [w for w in tokens if not w in stop_words]
new_tokens
>> ['This', 'book', 'Deep', 'Learning', 'Natural', 'Language', 'Processing', '!']
计数器矢量化
计数器矢量化是一个 SciKit-Learn 库工具,它可以获取任意数量的文本,并将每个唯一的单词作为一个特征返回,并计算特定单词在文本中出现的次数。
from sklearn.feature_extraction.text import CountVectorizer
texts=["Ramiess sings classic songs","he listens to old pop ","and rock music", ' and also listens to classical songs']
cv = CountVectorizer()
cv_fit=cv.fit_transform(texts)
print(cv.get_feature_names())
print(cv_fit.toarray())
>> ['also', 'and', 'classic', 'classical', 'he', 'listens', 'listens', 'music', 'old', 'pop', 'ramiess', 'rock', 'sings', 'songs', 'to']
>> [[0 0 1 0 0 0 0 0 0 0 1 0 1 1 0]
[0 0 0 0 1 1 0 0 1 1 0 0 0 0 1]
[0 1 0 0 0 0 0 1 0 0 0 1 0 0 0]
[1 1 0 1 0 0 1 0 0 0 0 0 0 1 0]]
TF-IDF 得分
TF-IDF 是两个术语的首字母缩写词:术语频率和逆文档频率。TF 是表示特定单词的计数与文档中单词总数的比率。假设一个文档包含 100 个单词,其中单词 happy 出现了五次。快乐的频率项(即 tf)则为(5/100) = 0.05。另一方面,IDF 是文档总数与包含特定单词的文档的对数比。假设我们有 1000 万个文档,其中 1000 个文档中出现了 happy 这个词。那么,逆文档频率(即 idf)将被计算为 log (10,000,000/1,000) = 4。因此,TF-IDF 重量是这些量的乘积:0.05 × 4 = 0.20。
Note
与 TF-IDF 类似的是 BM25,它用于根据文档与查询的关系对文档进行评分。BM25 使用每个文档的查询项对一组文档进行排序,而不考虑文档内查询的关键字之间的关系。
from sklearn.feature_extraction.text import TfidfVectorizer
texts=["Ramiess sings classic songs","he listens to old pop","and rock music", ' and also listens to classical songs']
vect = TfidfVectorizer()
X = vect.fit_transform(texts)
print(X.todense())
>> [[ 0\. 0\. 0.52547275 0\. 0\. 0\. 0.
0\. 0\. 0\. 0.52547275 0\. 0.52547275
0.41428875 0\. ]
[ 0\. 0\. 0\. 0\. 0.4472136 0.4472136
0\. 0\. 0.4472136 0.4472136 0\. 0\. 0.
0\. 0.4472136 ]
[ 0\. 0.48693426 0\. 0\. 0\. 0\. 0.
0.61761437 0\. 0\. 0\. 0.61761437 0\. 0.
0\. ]
[ 0.48546061 0.38274272 0\. 0.48546061 0\. 0.
0.48546061 0\. 0\. 0\. 0\. 0\. 0.
0.38274272 0\. ]]
文本分类器
文本可以分为各种类别,如正面和负面。TextBlob 提供了许多这样的架构。
from textblob import TextBlob
from textblob.classifiers import NaiveBayesClassifier
data = [
('I love my country.', 'pos'),
('This is an amazing place!', 'pos'),
('I do not like the smell of this place.', 'neg'),
('I do not like this restaurant', 'neg'),
('I am tired of hearing your nonsense.', 'neg'),
("I always aspire to be like him", 'pos'),
("It's a horrible performance.", "neg")
]
model = NaiveBayesClassifier(data)
model.classify("It's an awesome place!")
>> 'pos'
深度学习简介
深度学习是机器学习的扩展领域,已被证明主要在文本、图像和语音领域非常有用。在深度学习下实现的算法集合与人脑中刺激和神经元之间的关系有相似之处。深度学习在计算机视觉、语言翻译、语音识别、图像生成等方面有着广泛的应用。这些算法非常简单,可以在有人监督和无人监督的情况下学习。
大多数深度学习算法都是基于人工神经网络的概念,并且随着丰富数据和足够计算资源的可用性,在当今世界中训练这种算法已经变得更加容易。有了额外的数据,深度学习模型的性能就会不断提高。在图 1-2 中可以更好地看到这一点。
图 1-2
Scaling data science techniques to amount of data
深度学习中的深度一词是指人工神经网络架构的深度,学习代表通过人工神经网络本身进行学习。图 1-3 准确描述了深度和浅层网络之间的差异,以及深度学习一词流行的原因。
图 1-3
Representation of deep and shallow networks
深度神经网络能够从未标记和非结构化的数据中发现潜在的结构(或特征学习),例如图像(像素数据)、文档(文本数据)或文件(音频、视频数据)。
尽管人工神经网络和深度学习中的模型从根本上持有相似的结构,但这并不意味着两个人工神经网络的组合在训练使用数据时将与深度神经网络表现相似。
任何深度神经网络与普通人工神经网络的区别在于我们使用反向传播的方式。在普通的人工神经网络中,反向传播训练后面(或结束)的层比训练初始(或前面)的层更有效。因此,当我们回到网络中时,误差变得更小、更分散。
“深”有多深?
我们听到“深度”这个词,会立刻联想到它,但是浅层和深层神经网络之间并没有太大的区别。深度神经网络只是具有多个隐藏层的前馈神经网络。对,就是这么简单!
如果网络有很多层,那么我们说网络很深。你现在应该想到的问题是,一个网络必须有多少层才算深度?
在我们开始 NLP 空间中深度学习的实际旅程之前,回顾神经网络的基础知识及其不同类型将是有用的。
我们将介绍一个基本神经网络的基本结构,以及一些不同类型的神经网络,用于整个行业的应用。为了提供对这种技术的简明而实用的理解,本章的这一部分被细分为六个标题:
- 什么是神经网络?
- 神经网络的基本结构
- 神经网络的类型
- 多层感知器
- 随机梯度下降
- 反向传播
Note
详细的学术了解,可以参考Geoffrey hint on(www.cs.toronto.edu/~hinton/
)等人( http://deeplearning.net/
)发表的论文和文章。
什么是神经网络?
神经网络有着悠久的历史,可以追溯到马文·明斯基在人工智能(AI)方面的开创性工作,以及他(在)对解决异或(XOR)函数的挑战的著名引用。随着对越来越大的数据集的访问以及提供巨大计算能力的云计算和 GPU 的出现,神经网络已经变得越来越普遍。这种对数据和计算的便捷访问提高了建模和分析的准确性。
神经网络是一种受生物学启发的范式(模仿哺乳动物大脑的功能),它使计算机能够从观察数据中学习人类的能力。它们目前为许多问题提供解决方案:图像识别、手写识别、语音识别、语音分析和 NLP。
为了帮助我们发展直觉,我们一天中执行的不同任务可以分类如下:
- 代数或线性推理(例如,A × B = C,或一系列任务,如蛋糕的配方)
- 识别感知或非线性推理(例如,将名称与动物照片相关联,或减轻压力,或基于声音分析验证陈述)
- 通过观察学习任务(例如,在谷歌汽车中导航)
第一个任务可以通过算法来解决,即通过编程来描述,以从数字或成分中产生结果,而为后面的任务定义算法方法是困难的,如果不是不可能的话。后面的任务需要一个灵活的模型,它可以根据标记的例子自动调整自己的行为。
现在,统计或优化算法也努力提供与可能的输入相关的正确输出,尽管它们需要指定一个函数来模拟数据,从而产生最佳的一组系数。与优化技术相比,神经网络是一种灵活的功能,它自动调整其行为以尽可能满足输入和预期结果之间的关系,并被称为通用逼近器。
鉴于算法的普遍使用,所有流行的平台上都有可用的库(图 1-4 ),比如 R (knn,nnet 包),Scala(机器学习 ML 扩展),Python (TensorFlow,MXNet,Keras)。
图 1-4
Multiple open source platforms and libraries for deep learning
神经网络的基本结构
神经网络背后的基本原理是基本元素的集合,人工神经元或感知器,由 Frank Rosenblatt 在 20 世纪 50 年代首次开发。它们接受几个二进制输入,x 1 ,x 2 ,...,x N 并且如果总和大于激活电位,则产生单个二进制输出。每当激活电位被超过时,神经元就被称为“触发”,并表现为阶跃函数。触发的神经元将信号传递给连接到其树突的其他神经元,如果超过激活电位,这些神经元将依次触发,从而产生级联效应(图 1-5 )。
图 1-5
Sample neuron
由于并非所有的输入都具有相同的重点,因此权重被附加到每个输入,x i 以允许模型将更多的重要性分配给一些输入。因此,如果加权和大于激活电位或偏置,即,
输出=
实际上,由于阶跃函数的突变性质,这种简单形式很难实现(图 1-6 )。因此,创建了一个修改的形式来表现更可预测的行为,即权重和偏差的小变化仅引起输出的小变化。主要有两处修改。
- 输入可以取 0 到 1 之间的任何值,而不是二进制。
- 对于给定的输入,x 1 ,x 2 ,…,x N ,以及权重,为了使输出表现得更加平滑。w 1 ,w 2 ,…,w N ,以及 bias,b,使用以下 sigmoid 函数(图 1-7 ):
图 1-6
Step function
指数函数(或σ)的平滑度意味着权重和偏差的微小变化将产生神经元输出的微小变化(该变化可以是权重和偏差变化的线性函数)。
图 1-7
Neural network activation function: sigmoid
除了常见的 sigmoid 函数之外,其他更常用的非线性函数如下,每种非线性函数的输出范围可能相似,也可能不同,可以相应地使用。
-
ReLU: Rectified linear unit . This keeps the activation guarded at zero. It is computed using the following function:
where, x j , the j-th input value, and z j is its corresponding output value after the ReLU function f. Following is the graph (Figure 1-8) of the ReLU function, with ‘0’ value for all x <= 0, and with a linear slope of 1 for all x > 0:
图 1-8
ReLU function graph
ReLUs 经常面临死亡的问题,尤其是当学习速率被设置为更高的值时,因为这触发了不允许激活特定神经元的权重更新,从而使该神经元的梯度永远为零。ReLU 提供的另一个风险是激活函数的爆炸,因为输入值 x j 本身就是这里的输出。虽然 ReLU 也提供了其他好处,例如在 x j 小于 0 的情况下引入稀疏性,从而导致稀疏表示,并且当 ReLU 恒定的情况下梯度返回时,它导致更快的学习,同时降低了梯度消失的可能性。
- LReLUs(泄漏 relu):通过引入 x 值小于 0 的略微降低的斜率(~0.01),这些减轻了垂死 relu 的问题。LReLUs 确实提供了成功的场景,尽管并不总是如此。
- ELU(指数线性单位) :这些提供负值,推动平均单位激活接近零,从而通过将附近的梯度移动到单位自然梯度来加速学习过程。为了更好地解释 ELUs,请参考 Djork-Arné Clevert 的原始论文,可在
https://arxiv.org/abs/1511.07289
获得。 - Softmax:也称为归一化指数函数,它转换(0,1)范围内的一组给定实数值,使得组合和为 1。softmax 函数表示如下:
对于 j = 1,…,K
所有前面的函数都很容易微分,使得网络可以很容易地用梯度下降法训练(在下一节“神经网络的类型”中讨论)。
正如在哺乳动物的大脑中一样,单个神经元被组织成层,一层内和下一层之间有连接,从而创建了 ANN,即人工神经网络或多层感知器(MLP)。您可能已经猜到了,复杂度是基于元素的数量和连接的邻居的数量。
输入和输出之间的层称为隐藏层,层之间连接的密度和类型就是配置。例如,全连接配置将 L 层的所有神经元连接到 L + 1 层的神经元。对于更明显的定位,我们可以只将一个局部邻域,比如说九个神经元,连接到下一层。图 1-9 展示了两个具有密集连接的隐藏层。
图 1-9
Neural network architecture
神经网络的类型
到目前为止,我们一直在一般性地讨论人工神经网络;然而,基于结构和用途,有不同类型的神经网络。为了使神经网络以更快和更有效的方式学习,各种神经元以这样一种方式放置在网络中,以最大化网络对给定问题的学习。这种神经元的放置遵循了一种合理的方法,并导致了一种架构网络设计,其中不同的神经元消耗其他神经元的输出,或者不同的函数在其输入中从其他函数获取输出。如果神经元之间的连接以循环的形式放置,那么它们就形成了网络,如反馈、递归或循环神经网络。然而,如果神经元之间的连接是非循环的,它们就形成了网络,例如前馈神经网络。以下是对所引用网络的详细解释。
前馈神经网络
前馈神经网络构成了神经网络家族的基本单元。任何前馈神经网络中的数据移动都是通过当前隐藏层从输入层到输出层,从而限制任何类型的循环(图 1-10 )。一层的输出作为下一层的输入,对网络架构中的任何类型的环路都有限制。
图 1-10
A multilayer feedforward neural network
卷积神经网络
卷积神经网络非常适合于图像识别和手写识别。它们的结构基于对图像的窗口或部分进行采样,检测其特征,然后使用这些特征来构建表示。从这个描述中可以明显看出,这导致了几个层的使用,因此这些模型是第一批深度学习模型。
循环神经网络
循环神经网络(RNNs 当数据模式随时间变化时,使用图 1-11 。rnn 可以被假设为随时间展开。RNN 使用输出(即先前时间步长的状态作为输入)在每个时间步长对输入应用相同的图层。
rnn 具有反馈回路,其中来自前一次发射或时间索引 T 的输出作为时间索引 T + 1 处的输入之一被馈送。可能有这样的情况,神经元的输出作为输入反馈给它自己。因为它们非常适合于涉及序列的应用,所以它们广泛用于与视频相关的问题,视频是图像的时间序列,并且用于翻译目的,其中理解下一个单词是基于先前文本的上下文。以下是各种类型的 rnn:
-
Encoding recurrent neural networks : This set of RNNs enables the network to take an input of the sequence form (Figure 1-12).
图 1-12
Encoding RNNs
-
Generating recurrent neural networks: Such networks basically output a sequence of numbers or values, like words in a sentence (Figure 1-13).
图 1-13
Generating RNNs
-
General recurrent neural networks: These networks are a combination of the preceding two types of RNNs. General RNNs (Figure 1-14) are used to generate sequences and, thus, are widely used in NLG (natural language generation) tasks.
图 1-14
General RNNs
图 1-11
Recurrent neural network
编码器-解码器网络
编码器-解码器网络使用一个网络来创建输入的内部表示,或者对其进行“编码”,并且该表示被用作另一个网络的输入以产生输出。这对于超越输入的分类是有用的。基于概念,最终输出可以是相同的模态,即语言翻译,或者不同的模态,例如图像的文本标记。作为参考,人们可以参考谷歌( https://papers.nips.cc/paper/5346-sequence-to-sequence-learning-with-neural-networks.pdf
)团队发表的论文“用神经网络进行序列到序列的学习”。
循环神经网络
在循环神经网络(图 1-15 )中,一组固定的权重递归应用于网络结构,主要用于发现数据的层次或结构。RNN 是一条链,而循环神经网络采取树状结构的形式。这种网络在自然语言处理领域有很大的用途,例如破译句子的情感。整体情感不仅仅取决于单个作品,还取决于单词在句子中的句法组合顺序。
图 1-15
Recursive neural network
可以看出,网络有不同的类型,虽然有些网络可以应用于许多不同的环境,但就速度和质量而言,特定的网络更适合某些应用。
多层感知器
多层感知器(MLPs)属于前馈神经网络的范畴,由三种类型的层组成:输入层、一个或多个隐藏层和最终输出层。正常的 MLP 具有以下特性:
- 具有任意数量神经元的隐藏层
- 使用线性函数的输入图层
- 使用激活函数的隐藏层,如 sigmoid
- 给出任意数量输出的激活函数
- 输入层、隐藏层和输出层之间正确建立的连接
MLPs 也称为通用逼近器,因为它们可以通过在隐藏层中使用足够数量的神经元、改变权重或者通过使用额外的训练数据来逼近给定的函数达到任何精度水平,从而找到输入值和目标之间的关系。这甚至不需要大量关于输入和输出值之间映射的先验信息。通常,在给定 MLP 自由度的情况下,它可以通过引入更多的隐藏层、每个隐藏层中更少的神经元和最佳权重来胜过基本的 MLP 网络。这有助于模型的整体泛化过程。
以下是网络体系结构的一些对其性能有直接影响的功能
- 隐藏层:这些有助于网络的泛化因子。在大多数情况下,在足够数量的神经元的支持下,单个层足以包含任何期望函数的近似。
- 隐藏神经元:隐藏层中存在的神经元的数量,可以通过使用任何类型的公式进行选择。一个基本的经验法则是在一个和几个输入单元之间选择计数。另一种方法是使用交叉验证,然后检查隐藏层中神经元的数量与每个组合的平均均方误差(MSE)之间的关系,最后选择具有最小 MSE 值的组合。它还取决于非线性的程度或初始问题的维数。因此,增加/删除神经元更像是一个适应性过程。
- 输出节点:输出节点的计数通常等于我们想要分类目标值的类的数量。
- 激活函数:这些函数应用于各个节点的输入。一组非线性函数(在本章神经网络的基本结构一节中有详细描述)用于使输出落在所需的范围内,从而防止网络瘫痪。除了非线性之外,这些函数的连续可微性有助于防止对神经网络训练的抑制。
由于 MLP 给出的输出仅依赖于当前输入,而不依赖于过去或未来的输入,因此 MLP 被认为适于解决分类问题。
图 1-16 显示 MLP 共有(L + 2)层,输入层在第一个位置,接着是 L 个隐藏层,最后是第(L + 2)个位置的输出层。以下等式定义了 MLP 的不同单位,激活函数应用于网络的不同阶段。
W(k)表示第 k 个隐藏层和它之前的层、输入层或另一个隐藏层之间的权重连接。每个 W(k)由两个连接层的单元 I 和 j 之间的权重 W ij (k) 组成。b(k)是第 k 层的偏差。
下面的等式代表对于 k > 0 的隐藏层预激活:
对于第 k 个隐藏层中存在的任何第 I 个神经元,以下等式成立:
输出层(k = L + 1)的激活函数如下:
图 1-16
Multilayer neural network
随机梯度下降
几乎所有最优化问题的解决方案的主力是梯度下降算法。这是一种迭代算法,通过随后更新函数的参数来最小化损失函数。
正如我们从图 1-17 中看到的,我们首先把我们的功能想象成一种山谷。我们想象一个球滚下山谷的斜坡。我们的日常经验告诉我们,球最终会滚到谷底。也许我们可以用这个想法来寻找成本函数的最小值。
图 1-17
Ball rolling down the slope
这里我们使用的函数依赖于两个变量:v1 和 v2。这可能是显而易见的,因为我们的损失函数看起来和前面的一样。为了实现这样的平滑损失函数,我们取二次损失,如下:
同样,读者应该注意,二次成本函数只是一种方法,还有许多其他方法来定义损失。最终,选择不同损失函数的目的是得到
- 关于重量的平滑偏导数
- 一条好的凸曲线,实现全局最小。然而,在寻找全局最小值时,许多其他因素也起作用(学习速率、函数形状等。).
我们随机选择一个(假想的)球的起点,然后模拟球滚到谷底时的运动。打个类似的比方,想象我们在曲线上的某个任意点初始化网络的权重,或者一般来说,初始化函数的参数(就像在斜率的任意点上丢一个球一样),然后我们在附近检查斜率(导数)。
我们知道,由于重力的作用,球将向最大坡度的方向下落。类似地,我们在该点的导数方向上移动权重,并根据以下规则更新权重:
设 J(w) =成本是权重的函数
w =网络参数(v1 和 v2)
w i =初始权重集(随机初始化)
这里,dJ(w)/dw =重量 w 相对于 J(w)的偏导数
η =学习率。
学习率更多的是一个超参数,没有固定的方法找到最合适的学习率。然而,人们总是可以通过研究批次损失来找到它。
一种方法是看到损失,分析损失的模式。一般来说,不良的学习率会导致小批量的不稳定损失。它(亏损)可以递归上下,不稳定。
图 1-18 通过图表说明了更直观的解释。
图 1-18
Impact of small and large learning rates
在上图中,有两种情况:
- 小学习率
- 大学习率
目的是达到上图的最小值,我们必须到达谷底(就像球的类比一样)。现在,学习率与球滚下山时的跳跃有关。
首先考虑情况 1(图的左边部分),其中我们进行小跳跃,逐渐保持向下滚动,慢慢地,并最终达到最小值,有可能球会卡在沿途的一些小裂缝中,并且无法逃脱,因为它无法进行大跳跃。
在情况 2(图的右边部分)中,与曲率的斜率相比,有更大的学习速率。这是一个次优的策略,实际上可能会把我们从山谷中驱逐出去,在某些情况下,这可能是一个走出局部最小值的良好开端,但在跳过全局最小值的情况下,这一点都不令人满意。
在图中,我们正在实现局部最小值,但这只是一种情况。这意味着权重卡在了局部最小值,而我们错过了全局最小值。梯度下降或随机梯度下降不能保证神经网络收敛到全局最小值(假设隐藏单元不是线性的),因为成本函数是非凸的。
理想情况是步长不断变化,本质上更具适应性,开始时略高,然后在一段时间内逐渐减小,直到收敛。
反向传播
理解反向传播算法可能需要一些时间,如果您正在寻找神经网络的快速实现,那么您可以跳过这一部分,因为现代库具有自动区分和执行整个训练过程的能力。然而,理解这种算法肯定会让你深入了解与深度学习相关的问题(学习问题,学习缓慢,爆炸梯度,递减梯度)。
梯度下降是一种强大的算法,但当权重数量增加时,它是一种缓慢的方法。在神经网络具有数千个范围内的参数的情况下,相对于损失函数训练每个权重,或者更确切地说,将损失公式化为所有权重的函数,对于实际应用来说变得极其缓慢和复杂。
由于 Geoffrey Hinton 和他的同事在 1986 年发表了开创性的论文,我们有了一个非常快速和漂亮的算法,可以帮助我们找到损失相对于每个重量的偏导数。该算法是每个深度学习算法的训练过程的主力。更多详细信息可以在这里找到: www.cs.toronto.edu/~hinton/backprop.html
。
这是计算精确梯度的最有效的可能过程,并且其计算成本总是与计算损失本身具有相同的 O()复杂度。反向传播的证明超出了本书的范围;然而,对算法的直观解释可以让你很好地理解它的复杂工作。
为了使反向传播有效,关于Error
函数有两个基本假设。
- 总误差可以写成训练样本/小批量个体误差的总和,
- 误差可以写成网络输出的函数
反向传播由两部分组成:
- 正向传递,其中我们初始化权重,并建立一个前馈网络来存储所有值
- 向后传递,执行该传递以使存储的值更新权重
偏导数、链式法则、线性代数是处理反向传播所需的主要工具(图 1-19 )。
图 1-19
Backpropagation mechanism in an ANN
最初,所有的边权重都是随机分配的。对于训练数据集中的每个输入,激活人工神经网络,并观察其输出。将该输出与我们已知的期望输出进行比较,误差被“传播”回前一层。会记录此错误,并相应地“调整”权重。重复该过程,直到输出误差低于预定阈值。
一旦前面的算法终止,我们就有了一个“学习过的”人工神经网络,我们认为它可以处理“新的”输入。据说这个 ANN 已经从几个例子(标记的数据)和它的错误(错误传播)中学习了。
好奇的读者应该研究一下关于反向传播的原始论文。我们提供了一个资源和博客列表,以便更深入地理解该算法。然而,当涉及到实现时,您几乎不会编写自己的反向传播代码,因为大多数库都支持自动微分,并且您不会真的想要调整反向传播算法。
通俗地说,在反向传播中,我们尝试顺序更新权重,首先通过在网络上进行正向传递,之后我们首先使用标签和最后一层输出来更新最后一层的权重,然后随后在之前的层上递归地使用该信息并继续。
深度学习图书馆
本节包括对一些广泛使用的深度学习库的介绍,包括 Theano、TensorFlow 和 Keras,此外还有对其中每一个库的基本教程。
提亚诺
Theano 是一个开源项目,主要由蒙特利尔大学在 Yoshua Bengio 的监督下开发。它是 Python 的一个数值计算库,语法类似于 NumPy。它在用多维数组执行复杂的数学表达式时很有效。这使得它成为神经网络的完美选择。
链接 http://deeplearning.net/software/theano
将使用户对所涉及的各种操作有更好的了解。我们将演示在不同平台上安装 Theano 的步骤,然后是相关的基础教程。
Theano 是一个数学库,它提供了创建机器学习模型的方法,这些模型可以在以后用于多个数据集。在 ano 之上已经实现了许多工具。主要包括
- 积木
http://blocks.readthedocs.org/en/latest/
- 硬
http://keras.io/
- 千层面
http://lasagne.readthedocs.org/en/latest/
- pyr earn 2
http://deeplearning.net/software/pylearn2/
Note
应该注意的是,在撰写本书时,由于其他深度学习包的使用量大幅增加,社区成员已经停止了对 Theano 包的贡献。
Theano 安装
下面的命令对于在 Ubuntu 上安装 Theano 非常有用:
> sudo apt-get install python-numpy python-scipy python-dev python-pip python-nose g++ libopenblas-dev git
> sudo pip install Theano
关于在不同平台上安装 Theano 的详细说明,请参考以下链接: http://deeplearning.net/software/theano/install.html
。甚至连 CPU 和 GPU 兼容的 docker 镜像都有。
Note
建议在单独的虚拟环境中继续安装。
最新版本的 Theano 可从以下网站的开发版本安装
> git clone git://github.com/Theano/Theano.git
> cd Theano
> python setup.py install
对于 Windows 上的安装,采取以下步骤(来源于对堆栈溢出的回答):
- 安装 TDM GCC x64 (
http://tdm-gcc.tdragon.net/
)。 - 安装 Anaconda x64 (
www.continuum.io/downloads
,在C:/Anaconda
中说)。 - 安装 Anaconda 之后,运行以下命令:
conda update conda
conda update -all
conda install mingw libpython
- 在环境变量
PATH
中包含目的地'C:\Anaconda\Scripts'
。 - 安装旧版本或可用的最新版本。
-
旧版本:
> pip install Theano
-
最新版本:
> pip install --upgrade --no-deps git+git://github.com/Theano/Theano.git
-
没有例子
下一节介绍了 Theano 库中的基本代码。Theano 库的张量子包包含了大部分需要的符号。
下面的示例使用了张量子包,并对两个数执行运算(输出已包括在内以供参考):
> import theano
> import theano.tensor as T
> import numpy
> from theano import function
# Variables 'x' and 'y' are defined
> x = T.dscalar('x') # dscalar : Theano datatype
> y = T.dscalar('y')
# 'x' and 'y' are instances of TensorVariable, and are of dscalar theano type
> type(x)
<class 'theano.tensor.var.TensorVariable'>
> x.type
TensorType(float64, scalar)
> T.dscalar
TensorType(float64, scalar)
# 'z' represents the sum of 'x' and 'y' variables. Theano's pp function, pretty-print out, is used to display the computation of the variable 'z'
> z = x + y
> from theano import pp
> print(pp(z))
(x+y)
# 'f' is a numpy.ndarray of zero dimensions, which takes input as the first argument, and output as the second argument
# 'f' is being compiled in C code
> f = function([x, y], z)
可以按以下方式使用前面的函数来执行加法运算:
> f(6, 10)
array(16.0)
> numpy.allclose(f(10.3, 5.4), 15.7)
True
TensorFlow
TensorFlow 是 Google 为大规模机器学习实现提供的开源库。TensorFlow 在真正意义上是 dist faith 的继任者,dist faith 是谷歌发布的一个早期软件框架,能够利用具有数千台机器的计算集群来训练大型模型。
TensorFlow 是谷歌大脑团队的软件工程师和研究人员的创意,谷歌大脑团队是谷歌集团(现为 Alphabet)的一部分,主要专注于深度学习及其应用。它利用数据流图进行数值计算,下面将详细介绍。它的设计方式是通过单一的 API 来满足单一桌面或服务器或移动设备上的 CPU 或 GPU 系统的计算。
TensorFlow 将高度密集的计算任务从 CPU 转移到面向异构 GPU 的平台,只需对代码进行非常微小的更改。此外,在一台机器上训练的模型可以在另一台轻量级设备上使用,例如支持 Android 的移动设备,以达到最终实现的目的。
TensorFlow 是 DeepDream 和 RankBrain 等应用程序的实现基础,deep dream 是一种自动图像字幕软件,rank brain 帮助谷歌处理搜索结果,并向用户提供更相关的搜索结果。
为了更好地了解 TensorFlow 的工作和实现,可以在 http://download.tensorflow.org/paper/whitepaper2015.pdf
阅读相关白皮书。
数据流图表
TensorFlow 使用数据流图来表示以图形形式执行的数学计算。它利用了带有节点和边的有向图。这些节点表示数学运算,并充当数据输入、结果输出或持久变量读/写的终端。边处理节点之间的输入和输出关系。数据边在节点之间携带张量或动态大小的多维数据数组。这些张量单位在整个图形中的运动本身就导致了 TensorFlow 这个名称。图中的节点在接收到来自传入边的所有各自的张量后,异步且并行地执行。
数据流图中涵盖的整体设计和计算流程发生在一个会话中,然后在所需的机器上执行。TensorFlow 提供了 Python、C 和 C+API,依靠 C++进行优化计算。
凭借 TensorFlow 的以下特性,它是机器学习领域所需的大规模并行性和高可扩展性的最佳选择
- 深度灵活性:用户获得了在 TensorFlow 上编写自己的库的全部权限。人们只需要以图形的形式创建整个计算,其余的由 TensorFlow 处理。
- 真正的可移植性:TensorFlow 提供的可扩展性使得在笔记本电脑上编写的机器学习代码可以在 GPU 上进行训练,以实现更快的模型训练,而无需更改代码,并且可以作为云服务部署在移动设备、最终产品或 docker 上。
- 自动微分:TensorFlow 通过其自动微分功能来处理基于梯度的机器学习算法的导数计算。数值导数的计算有助于理解数值相对于彼此的扩展图。
- 语言选项:TensorFlow 提供 Python 和 C++接口来构建和执行计算图形。
- 性能最大化:TensorFlow 图中的计算元素可以分配给多个设备,TensorFlow 通过广泛支持线程、队列和异步计算来实现最大性能。
tensorflow 安装
TensorFlow 安装非常容易,就像任何其他 Python 包一样,可以通过使用一个 pip install 命令来实现。此外,如果需要,用户可以按照 TensorFlow 主站点上的详细说明进行安装(r0.10 版本为 www.tensorflow.org/versions/r0.10/get_started/os_setup.html
)。
通过 pip 安装之前,必须安装与平台相关的二进制包。有关 TensorFlow 软件包及其资源库 https://github.com/tensorflow/tensorflow
的更多详细信息,请参考以下链接。
要查看 TensorFlow 在 Windows 上的安装,请查看以下博客链接: www.hanselman.com/blog/PlayingWithTensorFlowOnWindows.aspx
。
TensorFlow 示例
运行和试验 TensorFlow 就像安装一样简单。官方网站上的教程 www.tensorflow.org/
,非常清晰,涵盖了基础到专家级的例子。
下面是一个这样的例子,带有 TensorFlow 的基础知识(输出已包括在内以供参考):
> import tensorflow as tf
> hello = tf.constant('Hello, Tensors!')
> sess = tf.Session()
> sess.run(hello)
Hello, Tensors!
# Mathematical computation
> a = tf.constant(10)
> b = tf.constant(32)
> sess.run(a+b)
42
run()
方法将计算的结果变量作为参数,并为此进行反向调用。
TensorFlow 图由不需要任何输入的节点形成,即源。然后,这些节点将它们的输出传递给其他节点,这些节点对产生的张量执行计算,整个过程以这种模式进行。
以下示例显示使用 Numpy 创建两个矩阵,然后使用 TensorFlow 将这些矩阵指定为 TensorFlow 中的对象,然后将两个矩阵相乘。第二个例子包括两个常数的加法和减法。TensorFlow 会话也被激活以执行操作,并在操作完成后被停用。
> import tensorflow as tf
> import numpy as np
> mat_1 = 10*np.random.random_sample((3, 4)) # Creating NumPy arrays
> mat_2 = 10*np.random.random_sample((4, 6))
# Creating a pair of constant ops, and including the above made matrices
> tf_mat_1 = tf.constant(mat_1)
> tf_mat_2 = tf.constant(mat_2)
# Multiplying TensorFlow matrices with matrix multiplication operation
> tf_mat_prod = tf.matmul(tf_mat_1 , tf_mat_2)
> sess = tf.Session() # Launching a session
# run() executes required ops and performs the request to store output in 'mult_matrix' variable
> mult_matrix = sess.run(tf_mat_prod)
> print(mult_matrix)
# Performing constant operations with the addition and subtraction of two constants
> a = tf.constant(10)
> a = tf.constant(20)
> print("Addition of constants 10 and 20 is %i " % sess.run(a+b))
Addition of constants 10 and 20 is 30
> print("Subtraction of constants 10 and 20 is %i " % sess.run(a-b))
Subtraction of constants 10 and 20 is -10
> sess.close() # Closing the session
Note
由于在前面的 TensorFlow 示例中没有指定图形,因此会话仅使用默认实例。
硬
Keras 是一个高度模块化的神经网络库,它运行在 ano 或 TensorFlow 之上。Keras 是同时支持 CNN 和 RNNs(我们将在后面的章节中详细讨论这两种神经网络)的库之一,可以毫不费力地在 GPU 和 CPU 上运行。
模型被理解为独立的、完全可配置的模块的序列或图形,这些模块可以以尽可能少的限制被插在一起。特别地,神经层、成本函数、优化器、初始化方案、激活函数、正则化方案都是独立的模块,可以组合起来创建新的模型。
硬安装
除了作为后端的 Theano 或 TensorFlow 之外,Keras 还使用了一些库作为依赖项。在安装 no 或 TensorFlow 之前安装这些组件可以简化安装过程。
> pip install numpy scipy
> pip install scikit-learn
> pip install pillow
> pip install h5py
Note
Keras 总是要求安装最新版本的 Theano(如前一节所述)。在整本书中,我们使用 TensorFlow 作为 Keras 的后端。
> pip install keras
克拉斯原则
Keras 提供了一个模型作为它的主要数据结构之一。每个模型都是可定制的实体,可以由不同的层、成本函数、激活函数和正则化方案组成。Keras 提供了广泛的预构建层来插入神经网络,其中一些包括卷积层、下降层、池层、局部连接层、递归层、噪声层和归一化层。网络的单个层被认为是下一层的输入对象。
Keras 中的代码片段主要是为实现神经网络和深度学习而构建的,除了它们相关的神经网络之外,它们也将包含在后面的章节中。
硬例子
Keras 的基本数据结构是模型类型,由网络的不同层组成。顺序模型是 Keras 中的主要模型类型,其中一层一层地添加,直到最终的输出层。
以下 Keras 示例使用了 UCI ML 知识库中的输血数据集。可以在这里找到关于数据的详细信息: https://archive.ics.uci.edu/ml/datasets/Blood+Transfusion+Service+Center
)。数据取自台湾的一个输血服务中心,除了目标变量之外,还有四个属性。这是一个二元分类的问题,'1'
代表献血者,'0'
代表拒绝献血者。关于属性的更多细节可以从提到的链接中获得。
将网站上共享的数据集保存在当前工作目录中(如果可能,删除标头)。我们首先加载数据集,在 Keras 中构建基本的 MLP 模型,然后在数据集上拟合模型。
Keras 中模型的基本类型是顺序的,这为模型提供了逐层增加的复杂性。多个层可以用它们各自的结构制造,并堆叠在初始基础模型上。
# Importing the required libraries and layers and model from Keras
> import keras
> from keras.layers import Dense
> from keras.models import Sequential
> import numpy as np
# Dataset Link : # https://archive.ics.uci.edu/ml/datasets/Blood+Transfusion+Service+Center
# Save the dataset as a .csv file :
tran_ = np.genfromtxt('transfusion.csv', delimiter=',')
X=tran_[:,0:4] # The dataset offers 4 input variables
Y=tran_[:,4] # Target variable with '1' and '0'
print(x)
由于输入数据有四个相应的变量,因此input_dim
(指不同输入变量的数量)被设置为四个。我们利用 Keras 中由密集层定义的完全连接的层来构建附加层。网络结构的选择是基于问题的复杂性。这里,第一个隐藏层由八个神经元组成,它们负责进一步捕捉非线性。该层已经用均匀分布的随机数和激活函数 ReLU 初始化,如本章前面所述。第二层有六个神经元,其结构与前一层相似。
# Creating our first MLP model with Keras
> mlp_keras = Sequential()
> mlp_keras.add(Dense(8, input_dim=4, init="uniform", activation="relu"))
> mlp_keras.add(Dense(6, init="uniform", activation="relu"))
在最后一层输出中,我们将激活设置为 sigmoid,如前所述,它负责生成一个介于0
和1
之间的值,并有助于二进制分类。
> mlp_keras.add(Dense(1, init="uniform", activation="sigmoid"))
为了编译网络,我们使用了具有对数损失的二进制分类,并选择 Adam 作为优化器的默认选择,选择 accuracy 作为要跟踪的期望指标。使用反向传播算法以及给定的优化算法和损失函数来训练网络。
> mlp_keras.compile(loss = 'binary_crossentropy', optimizer="adam",metrics=['accuracy'])
该模型已经在给定的数据集上用少量迭代(nb_epoch
)进行了训练,并且以可行的批量大小的实例(batch_size
)开始。可以根据以前处理这种数据集的经验来选择参数,或者甚至可以利用网格搜索来优化这种参数的选择。必要时,我们将在后面的章节中讨论相同的概念。
> mlp_keras.fit(X,Y, nb_epoch=200, batch_size=8, verbose=0)
下一步是最终评估已经构建的模型,并检查初始训练数据集的性能指标、损失和准确性。相同的操作可以在模型不熟悉的新测试数据集上执行,并且可以是模型性能的更好的度量。
> accuracy = mlp_keras.evaluate(X,Y)
> print("Accuracy : %.2f%% " % (accuracy[1]*100 ))
如果想要通过使用不同的参数组合和其他调整来进一步优化模型,可以通过在进行模型创建和验证时使用不同的参数和步骤来实现,尽管这不需要在所有情况下都产生更好的性能。
# Using a different set of optimizer
> from keras.optimizers import SGD
> opt = SGD(lr=0.01)
下面的示例创建了一个模型,其配置类似于早期模型中的配置,但具有不同的优化器,并且包括来自初始定型数据的验证数据集:
> mlp_optim = Sequential()
> mlp_optim.add(Dense(8, input_dim=4, init="uniform", activation="relu"))
> mlp_optim.add(Dense(6, init="uniform", activation="relu"))
> mlp_optim.add(Dense(1, init="uniform", activation="sigmoid"))
# Compiling the model with SGD
> mlp_optim.compile(loss = 'binary_crossentropy', optimizer=opt, metrics=['accuracy'])
# Fitting the model and checking accuracy
> mlp_optim.fit(X,Y, validation_split=0.3, nb_epoch=150, batch_size=10, verbose=0)
> results_optim = mlp_optim.evaluate(X,Y)
> print("Accuracy : %.2f%%" % (results_optim[1]*100 ) )
在继续下一步之前,请确保前面几节中提到的自然语言处理和深度学习的所有包都已安装。一旦你建立了这个系统,你就可以很好地使用本书提供的例子了。
后续步骤
第一章介绍了自然语言处理和深度学习领域,以及来自公共 Python 库的相关介绍性示例。我们将在接下来的章节中对此进行更深入的研究,介绍当前自然语言处理中的行业问题,以及深度学习的存在如何影响以有效方式解决这些问题的范式。
二、词向量表示法
当处理语言和单词时,我们可能会在数千个类别中对文本进行分类,以用于多种自然语言处理(NLP)任务。近年来,在这一领域进行了大量的研究,这导致了语言中的单词向可以在多组算法和过程中使用的向量格式的转换。本章深入解释了单词嵌入及其有效性。我们介绍了它们的起源,并比较了用于完成各种 NLP 任务的不同模型。
单词嵌入简介
语言项目之间语义相似性的分类和量化属于分布语义学的范畴,并且基于它们在语言使用中的分布。向量空间模型以向量的形式表示文本文档和查询,长期以来被用于分布式语义目的。由向量空间模型在 N 维向量空间中表示单词对于不同的 NLP 算法实现更好的结果是有用的,因为它导致在新的向量空间中相似文本的分组。
单词嵌入这个术语是 Yoshua Bengio 在他的论文《一种神经概率语言模型》( www.jmlr.org/papers/volume3/bengio03a/bengio03a.pdf
)中提出的。随后罗南·科洛伯特(Ronan Collobert)和杰森·韦斯顿(Jason Weston)在他们的论文《自然语言处理的统一架构》( https://ronan.collobert.com/pub/matos/2008_nlp_icml.pdf
)中,作者展示了多任务学习和半监督学习的使用如何提高共享任务的泛化能力。最后,Tomas Mikolov 等人创建了 word2vec,并将单词嵌入置于镜头之下,阐明了单词嵌入的训练以及预训练单词嵌入的使用。后来,Jeffrey Pennington 引入了 GloVe,这是另一组预训练的单词嵌入。
单词嵌入模型已被证明比最初使用的单词袋模型或一热编码方案更有效,该模型由大小与词汇表大小相等的稀疏向量组成。向量表示中存在的稀疏性是词汇表的巨大规模以及在索引位置标注其中的单词或文档的结果。单词嵌入通过使用所有单个单词的周围单词,使用来自给定文本的信息并将其传递给模型,取代了这个概念。这使得嵌入可以采用密集向量的形式,在连续的向量空间中,这种形式表示单个单词的投影。因此,嵌入指的是单词在新学习的向量空间中的坐标。
下面的例子给出了一个单词向量的创建过程,它对样本词汇表中的单词进行了一键编码,然后对单词向量进行了重组。它使用了一种分布式表示方法,并展示了如何使用最终的矢量组合来推断单词之间的关系。
假设我们的词汇表包含罗马、意大利、巴黎、法国和国家这几个词。我们可以利用这些单词中的每一个来创建一个表示,对所有的单词使用一键方案,如图 2-1 中的罗马所示。
图 2-1
A representation of Rome
使用前面的以向量形式呈现单词的方法,我们可以或多或少地通过比较单词的向量来测试单词之间的相等性。这种方法不会达到其他更高的目的。在一种更好的表示形式中,我们可以创建多个层次或分段,其中每个单词所显示的信息可以被分配不同的权重。我们可以选择这些片段或维度,并且每个单词将由这些片段上的权重分布来表示。因此,现在我们有了一种新的单词表示格式,对每个单词使用不同的标度(图 2-2 )。
图 2-2
Our representation
用于每个单词的前面的向量确实表示了该单词的实际意思,并且提供了更好的尺度来进行单词之间的比较。新形成的向量足以回答单词之间的这种关系。图 2-3 表示使用这种新方法形成的矢量。
图 2-3
Our vectors
不同单词的输出向量确实保留了语言规则和模式,并且这些模式的线性翻译证明了这一点。比如 vectors 和后面的单词 vector(法国)- vector(巴黎)+ vector(意大利)的差的结果,会接近 vector(罗马),如图 2-4 。
图 2-4
Comparing vectors
随着时间的推移,单词嵌入已经成为无监督学习领域最重要的应用之一。词向量提供的语义关系在神经机器翻译、信息检索和问答应用的 NLP 方法中有所帮助。
神经语言模型
Bengio 提出的前馈神经网络语言模型(FNNLM)引入了一个前馈神经网络,它由一个单独的隐藏层组成,预测序列的未来单词,在我们的例子中,只有一个单词。
训练神经网络语言模型以找到θ,这最大化了训练语料惩罚对数似然:
这里,f 是由与词汇表中存在的每个单词的分布式特征向量相关的参数和前馈或循环神经网络的参数组成的复合函数。R(θ)指的是正则化项,其将权重衰减惩罚应用于神经网络和特征向量矩阵的权重。函数 f 返回 softmax 函数使用前面的 n 个单词为第 t 个位置的单词计算的概率得分。
Bengio 引入的模型是同类模型中的第一个,为未来的单词嵌入模型奠定了基础。这些原始模型的组件仍然在当前的单词嵌入模型中使用。其中一些组件包括以下内容:
- 嵌入层:它记录了训练数据集中所有单词的表示。它用一组随机权重初始化。嵌入层由三部分组成,包括词汇表的大小、将嵌入所有单词的向量的输出大小以及模型的输入序列的长度。嵌入层的最终输出是一个二维向量,它对给定单词序列中存在的所有单词进行最终嵌入。
- 中间层:隐藏层,从初始层到最终层,计数为 1 或更多,通过将神经网络中的非线性函数应用于先前
n
单词的单词嵌入来产生输入文本数据的表示。 - Softmax 层:这是神经网络架构的最后一层,返回输入词汇表中所有单词的概率分布。
Bengio 的论文提到了 softmax 标准化中涉及的计算成本,并且它与词汇量成比例。这给在全词汇量上对神经语言模型和单词嵌入模型的新算法的试验带来了挑战。
神经网络语言模型有助于获得当前词汇表中不存在的单词的泛化,因为如果单词的组合与已经包含在句子中的单词相似,则从未见过的单词序列被给予更高的概率。
Word2vec
Word2vec 或单词到向量模型是由托马斯·米科洛夫等人( https://arxiv.org/pdf/1301.3781.pdf
)提出的,并且是最被采用的模型之一。它用于学习单词嵌入,或单词的矢量表示。通过检查单词组之间的相似性,将所提出的模型的性能与先前的模型进行比较。该论文中提出的技术产生了对于相似单词具有跨多个相似度的单词的向量表示。单词表示的相似性超出了简单的句法规则,简单的代数运算也在单词向量上执行。
Word2vec 模型在内部使用单一层的简单神经网络,并捕获隐藏层的权重。训练模型的目的是学习隐藏层的权重,它代表“单词嵌入”虽然 word2vec 使用神经网络架构,但该架构本身不够复杂,并且没有利用任何类型的非线性。暂时可以卸下深度学习的标签。
Word2vec 提供了一系列用于在 n 维空间中表示单词的模型,通过这种方式,相似的单词和表示更接近含义的单词被放置得彼此靠近。这证明了将单词放置在新的向量空间中的整个练习是正确的。我们将介绍两个最常用的模型,skip-gram 和 continuous bag-of-words (CBOW ),以及它们在 TensorFlow 中的实现。这两个模型在算法上是相似的,区别仅在于它们执行预测的方式。CBOW 模型利用上下文或周围的词来预测中心词,而 skip-gram 模型利用中心词来预测上下文词。
与 one-hot 编码相比,word2vec 有助于减少编码空间的大小,并将单词的表示压缩到向量所需的长度(图 2-5 )。Word2vec 根据单词出现的上下文来处理单词表示。例如,同义词、反义词、语义相似的概念和相似的词出现在整个文本的相似上下文中,因此以相似的方式嵌入,并且它们的最终嵌入彼此更接近。
图 2-5
Using the window size of 2 to pick the words from the sentence “Machines can now recognize objects and translate speech in real time” and training the model
跳格模型
跳格模型通过使用序列中的当前单词来预测周围的单词。周围单词的分类分数基于与中心单词的句法关系和出现次数。序列中出现的任何单词都被作为对数线性分类器的输入,对数线性分类器进而对出现在中心单词之前和之后的某个预先指定的单词范围内的单词进行预测。在单词范围的选择和结果单词向量的计算复杂度和质量之间有一个折衷。随着与相关单词的距离增加,与较近的单词相比,较远的单词与当前单词的相关程度较低。这是通过将权重分配为与中心单词的距离的函数,并从较高范围的单词中给予较小的权重或采样较少的单词来解决的(见图 2-6 )。
图 2-6
Skip-gram model architecture
跳格模型的训练不涉及密集矩阵乘法。再加上一点优化,它可以产生一个高效的模型训练过程。
模型构件:建筑
在本例中,网络用于训练模型,输入单词作为一个热码编码的向量,输出作为一个热码编码的向量,表示输出单词(图 2-7 )。
图 2-7
The model
模型构件:隐藏层
使用隐藏层来完成神经网络的训练,其中神经元的数量等于我们想要用来表示单词嵌入的特征或维度的数量。在下图中,我们用权重矩阵来表示隐藏层,该矩阵的列数为 300,等于神经元的数量(这将是单词嵌入的最终输出向量中的特征数),行数为 100,000,等于用于训练模型的词汇的大小。
神经元的数量被认为是模型的超参数,可以根据需要改变。谷歌训练的模型利用了 300 维特征向量,并且已经公开。对于那些不想训练单词嵌入模型的人来说,这可能是一个好的开始。你可以使用以下链接下载训练好的向量集: https://code.google.com/archive/p/word2vec/
。
由于作为词汇表中每个单词的输入而给出的输入向量是一次性编码的,所以在隐藏层阶段发生的计算将确保仅从权重矩阵中选择对应于相应单词的向量,并将其传递到输出层。如图 2-8 所示,在词汇量为 v 的情况下,对于任何一个单词,在输入向量中的期望索引处都会有“1”出现,将其乘以权重矩阵后,对于每一个单词,我们都会得到该单词对应的一行作为输出向量。因此,真正重要的不是输出,而是权重矩阵。图 2-8 清楚地表示了如何使用隐藏层的权重矩阵来计算单词向量查找表。
图 2-8
Weight matrix of the hidden layer and vector lookup table
即使独热编码向量完全由零组成,将 1 ×
100,000 维向量乘以 100,000 ×
300 权重矩阵仍将导致选择存在“1”的对应行。图 2-9 给出了这种计算的图示,隐含层的输出就是关注词的矢量表示。
图 2-9
The calculation
模型组件:输出层
我们计算单词的单词嵌入背后的主要意图是确保具有相似含义的单词在我们定义的向量空间中更接近。这个问题由模型自动处理,因为在大多数情况下,具有相似含义的单词被相似的上下文(即,围绕输入单词的单词)包围,这在训练过程中固有地以相似的方式进行权重调整(图 2-10 )。除了同义词和具有相似含义的单词之外,该模型还处理词干提取的情况,因为复数或单数单词(比如,car 和 cars)将具有相似的上下文。
图 2-10
The training process
CBOW 模型
连续词袋模型在架构上与 FNNLM 相似,如图 2-11 所示。单词的顺序不会影响投影层,重要的是哪些单词当前落入袋中以进行输出单词预测。
图 2-11
Continuous bag-of-words model architecture
输入层和投影层以类似于 FNNLM 中共享的方式共享所有字位置的权重矩阵。CBOW 模型利用了上下文的连续分布表示,因此是一个连续的单词包。
Note
在较小的数据集上使用 CBOW 导致分布信息的平滑,因为模型将整个上下文视为单个观察。
二次抽样常用词
在大多数处理文本数据的情况下,词汇表的大小可以增加到大量的唯一单词,并且可以由所有单词的不同频率大小组成。为了选择出于建模目的而保留的单词,单词在语料库中出现的频率被用于决定单词的移除,也通过检查总单词的计数。子采样方法是由 Mikolov 等人在他们的论文“单词和短语的分布式表示及其组合性”中引入的。通过包括子采样,在训练过程中获得了显著的速度,并且以更有规律的方式学习单词表示。
生存函数用于计算单词级别的概率得分,该得分可在以后用于决定从词汇表中保留或删除该单词。该函数考虑了相关单词的频率和子采样率,可以进行调整:
其中,w i 是相关作品,z(w i 是该单词在训练数据集或语料库中的频率,s 是子采样率。Note
Mikolov 等人在他们的论文中提到的原始函数不同于 word2vec 代码的实际实现中使用的函数,并且已经在前面的文本中提到过。论文中为二次抽样选择的公式是启发式选择的,它包括一个阈值 t,通常表示为 10 -5 ,作为语料库中单词的最小频率。论文中提到的用于子采样的公式为
其中,w i 为关注的词,f(w i 为该词在训练数据集或语料库中的出现频率,s 为使用的阈值。
子采样率对是否保留频繁词做出关键决定。较小的值意味着单词不太可能保留在语料库中用于模型训练。在大多数情况下,在生存函数的输出上设置一个首选阈值,以删除出现频率较低的单词。参数 s 的优选值是 0.001。所提到的二次采样方法有助于克服语料库中稀有词和频繁词之间的不平衡。
图 2-12
Distribution of the Survival function, P(x) = {(sqrt(x/0.001) + 1) * (0.001/x)} for a constant value of 0.001 for sampling rate (Credits : http://www.mccormickml.com
)
该图显示了单词的频率与通过子采样方法生成的最终概率得分之间的图表。由于语料库中存在的单词都不能占据更高的百分比,所以我们将考虑图中单词百分比范围较低的部分,即沿 x 轴的部分。从上面的图表中,我们可以得出一些关于单词的百分比及其与生成的分数的关系的观察结果,从而得出二次抽样对单词的影响:
- 当 z(w i ) < = 0.0026 时,P(w i ) =1。这意味着频率百分比小于 0.26%的单词将不会被考虑进行二次采样。
- 当 z(w i ) = 0.00746 时,P(w i ) = 0.5。因此,一个词有 50%的机会被保留或删除所需的百分比是当它有 0.746%的频率。
- P(w i ) = 0.033 出现在 z(w i ) =1 的情况下,即,即使整个语料库仅由单个单词组成,也有 96.7%的概率将其从语料库中移除,这在实践中没有任何意义。
负采样
负采样是噪声对比估计(NCE)方法的简化形式,因为它在选择噪声或负样本的计数及其分布时做出某些假设。它用作分级 softmax 函数的替代函数。虽然在训练模型时使用了负采样,但是在推断时,要计算完整的 softmax 值,以获得归一化的概率得分。
神经网络模型的隐藏层中的权重矩阵的大小取决于词汇的整体大小,词汇的整体大小是高阶的。这导致了大量的权重参数。所有权重参数在数百万和数十亿训练样本的多次迭代中被更新。对于每个训练样本,负采样会导致模型只更新很小百分比的权重。
给予模型的单词的输入表示是通过一个热编码向量。负采样随机选择给定数量的“负”词(比如 10 个),用“正”词(或中心词)的权重来更新这些词的权重。总的来说,对于 11 个单词(10 + 1),权重将被更新。参考前面给出的图,任何迭代都将导致更新权重矩阵中的 11 × 300 = 3,300 个值。然而,不管负采样的使用,在隐藏层中只更新“正”字的权重。
选择“阴性”样本的概率取决于该词在语料库中的频率。频率越高,“负面”单词被选中的概率就越高。正如在论文“单词和短语的分布式表示及其组成性”中提到的,对于小的训练数据集,负样本的计数在 5 到 20 之间,对于大的训练数据集,建议在 2 到 5 之间。
实际上,负样本是不应该确定输出的输入,只应该产生一个全为 0 的向量。
Note
子采样和负采样的组合在很大程度上减少了训练过程的负荷。
word2vec 模型通过在一系列句法和语义语言任务上使用模型的组合,帮助实现了更高质量的单词矢量表示。随着计算资源、更快的算法和文本数据可用性的进步,与早期提出的神经网络模型相比,有可能训练高质量的单词向量。
在下一节中,我们将研究如何使用 TensorFlow 实现 skip-gram 和 CBOW 模型。这些课程的结构归功于在线课程和写作时可用材料的结合。
Word2vec Code
TensorFlow 库通过引入在 word2vec 算法实现中使用的多个预定义函数,使我们的生活变得更加轻松。本节包括 word2vec 算法、skip-gram 和 CBOW 模型的实现。
本节开头的第一部分代码对于 skip-gram 和 CBOW 模型都是通用的,后面是 skip-gram 和 CBOW 代码小节中的相应实现。
Note
我们的练习使用的数据是 2006 年 3 月 3 日制作的英语维基百科转储的压缩格式。可从以下链接获得: http://mattmahoney.net/dc/textdata.html
。
导入 word2vec 实现所需的包,如下所示:
"""Importing the required packages"""
import random
import collections
import math
import os
import zipfile
import time
import re
import numpy as np
import tensorflow as tf
from matplotlib import pylab
%matplotlib inline
from six.moves import range
from six.moves.urllib.request import urlretrieve
"""Make sure the dataset link is copied correctly"""
dataset_link = 'http://mattmahoney.net/dc/'
zip_file = 'text8.zip'
函数data_download()
下载 Matt Mahoney 收集的维基百科文章的清理数据集,并将其作为一个单独的文件存储在当前工作目录下。
def data_download(zip_file):
"""Downloading the required file"""
if not os.path.exists(zip_file):
zip_file, _ = urlretrieve(dataset_link + zip_file, zip_file)
print('File downloaded successfully!')
return None
data_download(zip_file)
> File downloaded successfully!
压缩文本数据集在内部文件夹数据集中提取,稍后用于训练模型。
"""Extracting the dataset in separate folder"""
extracted_folder = 'dataset'
if not os.path.isdir(extracted_folder):
with zipfile.ZipFile(zip_file) as zf:
zf.extractall(extracted_folder)
with open('dataset/text8') as ft_ :
full_text = ft_.read()
由于输入数据在整个文本中具有多个标点和其他符号,因此相同的符号被替换为它们各自的标记,标记中具有标点和符号名称的类型。这有助于模型单独识别每个标点符号和其他符号,并生成一个向量。函数text_processing()
执行该操作。它将维基百科的文本数据作为输入。
def text_processing(ft8_text):
"""Replacing punctuation marks with tokens"""
ft8_text = ft8_text.lower()
ft8_text = ft8_text.replace('.', ' <period> ')
ft8_text = ft8_text.replace(',', ' <comma> ')
ft8_text = ft8_text.replace('"', ' <quotation> ')
ft8_text = ft8_text.replace(';', ' <semicolon> ')
ft8_text = ft8_text.replace('!', ' <exclamation> ')
ft8_text = ft8_text.replace('?', ' <question> ')
ft8_text = ft8_text.replace('(', ' <paren_l> ')
ft8_text = ft8_text.replace(')', ' <paren_r> ')
ft8_text = ft8_text.replace(' ', ' <hyphen> ')
ft8_text = ft8_text.replace(':', ' <colon> ')
ft8_text_tokens = ft8_text.split()
return ft8_text_tokens
ft_tokens = text_processing(full_text)
为了提高所产生的矢量表示的质量,建议去除与单词相关的噪声,即在输入数据集中频率小于 7 的单词,因为这些单词将不具有足够的信息来提供它们所存在的上下文。
可以通过检查数据集中单词计数和的分布来改变这个阈值。为了方便起见,我们在这里把它当作 7。
"""Shortlisting words with frequency more than 7"""
word_cnt = collections.Counter(ft_tokens)
shortlisted_words = [w for w in ft_tokens if word_cnt[w] > 7 ]
根据出现频率列出数据集中出现的前几个词,如下所示:
print(shortlisted_words[:15])
> ['anarchism', 'originated', 'as', 'a', 'term', 'of', 'abuse', 'first', 'used', 'against', 'early', 'working', 'class', 'radicals', 'including']
检查数据集中出现的所有单词的统计信息。
print("Total number of shortlisted words : ",len(shortlisted_words))
print("Unique number of shortlisted words : ",len(set(shortlisted_words)))
> Total number of shortlisted words : 16616688
> Unique number of shortlisted words : 53721
为了处理语料库中出现的独特单词,我们制作了一组单词,后跟它们在训练数据集中的频率。下面的函数创建一个字典,将单词转换成整数,反之,将整数转换成单词。最频繁出现的单词被赋予最小的值0
,并且以类似的方式,数字被赋予其他单词。单词到整数的转换存储在一个单独的列表中。
def dict_creation(shortlisted_words):
"""The function creates a dictionary of the words present in dataset along with their frequency order"""
counts = collections.Counter(shortlisted_words)
vocabulary = sorted(counts, key=counts.get, reverse=True)
rev_dictionary_ = {ii: word for ii, word in enumerate(vocabulary)}
dictionary_ = {word: ii for ii, word in rev_dictionary_.items()}
return dictionary_, rev_dictionary_
dictionary_, rev_dictionary_ = dict_creation(shortlisted_words)
words_cnt = [dictionary_[word] for word in shortlisted_words]
到目前为止创建的变量都是常见的,可以在 word2vec 模型的实现中使用。接下来的小节包括这两种架构的实现。
跳格码
在 skip-gram 模型中加入了二次抽样方法来处理文本中的停用词。通过对它们的频率设置阈值,去除所有具有较高频率并且在中心单词周围没有任何重要上下文的单词。这导致更快的训练和更好的单词向量表示。
Note
我们在这里的实现中使用了关于 skip-gram 的论文中给出的概率得分函数。对于训练集中的每个单词 w i ,我们将按照
给出的概率将其丢弃,其中 t 是阈值参数,f(w i )是单词 w i 在总数据集中的出现频率。
"""Creating the threshold and performing the subsampling"""
thresh = 0.00005
word_counts = collections.Counter(words_cnt)
total_count = len(words_cnt)
freqs = {word: count / total_count for word, count in word_counts.items()}
p_drop = {word: 1 - np.sqrt(thresh/freqs[word]) for word in word_counts}
train_words = [word for word in words_cnt if p_drop[word] < random.random()]
由于跳格模型采用中心单词并预测其周围的单词,skipG_target_set_generation()
函数以期望的格式为跳格模型创建输入:
def skipG_target_set_generation(batch_, batch_index, word_window):
"""The function combines the words of given word_window size next to the index, for the SkipGram model"""
random_num = np.random.randint(1, word_window+1)
words_start = batch_index - random_num if (batch_index - random_num) > 0 else 0
words_stop = batch_index + random_num
window_target = set(batch_[words_start:batch_index] + batch_[batch_index+1:words_stop+1])
return list(window_target)
skipG_batch_creation
()函数利用skipG_target_set_generation()
函数,创建一个中心单词及其周围单词的组合格式作为目标文本,并返回批处理输出,如下所示:
def skipG_batch_creation(short_words, batch_length, word_window):
"""The function internally makes use of the skipG_target_set_generation() function and combines each of the label
words in the shortlisted_words with the words of word_window size around"""
batch_cnt = len(short_words)//batch_length
short_words = short_words[:batch_cnt*batch_length]
for word_index in range(0, len(short_words), batch_length):
input_words, label_words = [], []
word_batch = short_words[word_index:word_index+batch_length]
for index_ in range(len(word_batch)):
batch_input = word_batch[index_]
batch_label = skipG_target_set_generation(word_batch, index_, word_window)
# Appending the label and inputs to the initial list. Replicating input to the size of labels in the window
label_words.extend(batch_label)
input_words.extend([batch_input]*len(batch_label))
yield input_words, label_words
以下代码注册了一个 TensorFlow 图以供 skip-gram 实现使用,声明了变量的inputs
和labels
占位符,这些占位符将用于根据中心词和周围词的组合,为输入词和不同大小的批次分配一个热编码向量:
tf_graph = tf.Graph()
with tf_graph.as_default():
input_ = tf.placeholder(tf.int32, [None], name="input_")
label_ = tf.placeholder(tf.int32, [None, None], name="label_")
下面的代码声明了嵌入矩阵的变量,嵌入矩阵的维数等于词汇表的大小和单词嵌入向量的维数:
with tf_graph.as_default():
word_embed = tf.Variable(tf.random_uniform((len(rev_dictionary_), 300), -1, 1))
embedding = tf.nn.embedding_lookup(word_embed, input_)
tf.train.AdamOptimizer
使用 Diederik P. Kingma 和 Jimmy Ba 的 Adam 算法( http://arxiv.org/pdf/1412.6980v8.pdf
)来控制学习速率。关于进一步的信息,另外参考本吉奥的以下论文: http://arxiv.org/pdf/1206.5533.pdf
。
"""The code includes the following :
# Initializing weights and bias to be used in the softmax layer
# Loss function calculation using the Negative Sampling
# Usage of Adam Optimizer
# Negative sampling on 100 words, to be included in the loss function
# 300 is the word embedding vector size
"""
vocabulary_size = len(rev_dictionary_)
with tf_graph.as_default():
sf_weights = tf.Variable(tf.truncated_normal((vocabulary_size, 300), stddev=0.1) )
sf_bias = tf.Variable(tf.zeros(vocabulary_size) )
loss_fn = tf.nn.sampled_softmax_loss(weights=sf_weights, biases=sf_bias,
labels=label_, inputs=embedding,
num_sampled=100, num_classes=vocabulary_size)
cost_fn = tf.reduce_mean(loss_fn)
optim = tf.train.AdamOptimizer().minimize(cost_fn)
为了确保单词向量表示保持单词之间的语义相似性,在下面的代码部分中生成了一个验证集。这将在语料库中选择常见和不常见单词的组合,并基于单词向量之间的余弦相似性返回与它们最接近的单词。
"""The below code performs the following operations :
# Performing validation here by making use of a random selection of 16 words from the dictionary of desired size
# Selecting 8 words randomly from range of 1000
# Using the cosine distance to calculate the similarity between the words
"""
with tf_graph.as_default():
validation_cnt = 16
validation_dict = 100
validation_words = np.array(random.sample(range(validation_dict), validation_cnt//2))
validation_words = np.append(validation_words, random.sample(range(1000,1000+validation_dict), validation_cnt//2))
validation_data = tf.constant(validation_words, dtype=tf.int32)
normalization_embed = word_embed / (tf.sqrt(tf.reduce_sum(tf.square(word_embed), 1, keep_dims=True)))
validation_embed = tf.nn.embedding_lookup(normalization_embed, validation_data)
word_similarity = tf.matmul(validation_embed, tf.transpose(normalization_embed))
在当前工作目录下创建一个文件夹model_checkpoint
来存储模型检查点。
"""Creating the model checkpoint directory"""
!mkdir model_checkpoint
epochs = 2 # Increase it as per computation resources. It has been kept low here for users to replicate the process, increase to 100 or more
batch_length = 1000
word_window = 10
with tf_graph.as_default():
saver = tf.train.Saver()
with tf.Session(graph=tf_graph) as sess:
iteration = 1
loss = 0
sess.run(tf.global_variables_initializer())
for e in range(1, epochs+1):
batches = skipG_batch_creation(train_words, batch_length, word_window)
start = time.time()
for x, y in batches:
train_loss, _ = sess.run([cost_fn, optim],
feed_dict={input_: x, label_: np.array(y)[:, None]})
loss += train_loss
if iteration % 100 == 0:
end = time.time()
print("Epoch {}/{}".format(e, epochs), ", Iteration: {}".format(iteration),
", Avg. Training loss: {:.4f}".format(loss/100),", Processing : {:.4f} sec/batch".format((end-start)/100))
loss = 0
start = time.time()
if iteration % 2000 == 0:
similarity_ = word_similarity.eval()
for i in range(validation_cnt):
validated_words = rev_dictionary_[validation_words[i]]
top_k = 8 # number of nearest neighbors
nearest = (-similarity_[i, :]).argsort()[1:top_k+1]
log = 'Nearest to %s:' % validated_words
for k in range(top_k):
close_word = rev_dictionary_[nearest[k]]
log = '%s %s,' % (log, close_word)
print(log)
iteration += 1
save_path = saver.save(sess, "model_checkpoint/skipGram_text8.ckpt")
embed_mat = sess.run(normalization_embed)
> Epoch 1/2 , Iteration: 100 , Avg. Training loss: 6.1494 , Processing : 0.3485 sec/batch
> Epoch 1/2 , Iteration: 200 , Avg. Training loss: 6.1851 , Processing : 0.3507 sec/batch
> Epoch 1/2 , Iteration: 300 , Avg. Training loss: 6.0753 , Processing : 0.3502 sec/batch
> Epoch 1/2 , Iteration: 400 , Avg. Training loss: 6.0025 , Processing : 0.3535 sec/batch
> Epoch 1/2 , Iteration: 500 , Avg. Training loss: 5.9307 , Processing : 0.3547 sec/batch
> Epoch 1/2 , Iteration: 600 , Avg. Training loss: 5.9997 , Processing : 0.3509 sec/batch
> Epoch 1/2 , Iteration: 700 , Avg. Training loss: 5.8420 , Processing : 0.3537 sec/batch
> Epoch 1/2 , Iteration: 800 , Avg. Training loss: 5.7162 , Processing : 0.3542 sec/batch
> Epoch 1/2 , Iteration: 900 , Avg. Training loss: 5.6495 , Processing : 0.3511 sec/batch
> Epoch 1/2 , Iteration: 1000 , Avg. Training loss: 5.5558 , Processing : 0.3560 sec/batch
> ..................
> Nearest to during: stress, shipping, bishoprics, accept, produce, color, buckley, victor,
> Nearest to six: article, incorporated, raced, interval, layouts, confused, spitz, masculinity,
> Nearest to all: cm, unprotected, fit, tom, opold, render, perth, temptation,
> Nearest to th: ponder, orchids, shor, polluted, firefighting, hammering, bonn, suited,
> Nearest to many: trenches, parentheses, essential, error, chalmers, philo, win, mba,
> ..................
对于所有其他迭代,将打印类似的输出,并且已训练的网络将被恢复以供进一步使用。
"""The Saver class adds ops to save and restore variables to and from checkpoints."""
with tf_graph.as_default():
saver = tf.train.Saver()
with tf.Session(graph=tf_graph) as sess:
"""Restoring the trained network"""
saver.restore(sess, tf.train.latest_checkpoint('model_checkpoint'))
embed_mat = sess.run(word_embed)
> INFO:tensorflow:Restoring parameters from model_checkpoint/skipGram_text8.ckpt
为了可视化的目的,我们使用了 t 分布随机邻居嵌入(t-SNE)(https://lvdmaaten.github.io/tsne/
)。250 个随机单词的高维 300 向量表示已经在二维向量空间中使用。t-SNE 确保向量的初始结构保留在新的维度中,即使在转换之后。
word_graph = 250
tsne = TSNE()
word_embedding_tsne = tsne.fit_transform(embed_mat[:word_graph, :])
正如我们在图 2-13 中所观察到的,在二维空间中,具有语义相似性的单词在它们的表示中被放置得彼此更接近,从而即使在维度被进一步降低之后,它们的相似性仍然保持。例如,像年、年和年龄这样的词被放置在彼此附近,而远离像国际和宗教这样的词。可以针对更高的迭代次数来训练该模型,以实现单词嵌入的更好表示,并且可以进一步改变阈值,以微调结果。
图 2-13
Two-dimensional representation of the word vectors obtained after training the Wikipedia corpus using a skip-gram model
CBOW 代码
CBOW 模型考虑周围的单词并预测中心单词。因此,使用cbow_batch_creation()
函数已经实现了批处理和标签生成,当期望的word_window
大小被传递给该函数时,该函数在label_
变量中分配目标单词,在batch
变量中分配上下文中的周围单词。
data_index = 0
def cbow_batch_creation(batch_length, word_window):
"""The function creates a batch with the list of the label words and list of their corresponding words in the context of
the label word."""
global data_index
"""Pulling out the centered label word, and its next word_window count of surrounding words
word_window : window of words on either side of the center word
relevant_words : length of the total words to be picked in a single batch, including the center word and the word_window words on both sides
Format : [ word_window ... target ... word_window ] """
relevant_words = 2 * word_window + 1
batch = np.ndarray(shape=(batch_length,relevant_words-1), dtype=np.int32)
label_ = np.ndarray(shape=(batch_length, 1), dtype=np.int32)
buffer = collections.deque(maxlen=relevant_words) # Queue to add/pop
#Selecting the words of length 'relevant_words' from the starting index
for _ in range(relevant_words):
buffer.append(words_cnt[data_index])
data_index = (data_index + 1) % len(words_cnt)
for i in range(batch_length):
target = word_window # Center word as the label
target_to_avoid = [ word_window ] # Excluding the label, and selecting only the surrounding words
# add selected target to avoid_list for next time
col_idx = 0
for j in range(relevant_words):
if j==relevant_words//2:
continue
batch[i,col_idx] = buffer[j] # Iterating till the middle element for window_size length
col_idx += 1
label_[i, 0] = buffer[target]
buffer.append(words_cnt[data_index])
data_index = (data_index + 1) % len(words_cnt)
assert batch.shape[0]==batch_length and batch.shape[1]== relevant_words-1
return batch, label_
确保cbow_batch_creation()
功能按照 CBOW 模型输入运行,已经对第一批标签及其周围窗口长度为 1 和 2 的单词进行了测试,并打印了结果。
for num_skips, word_window in [(2, 1), (4, 2)]:
data_index = 0
batch, label_ = cbow_batch_creation(batch_length=8, word_window=word_window)
print('\nwith num_skips = %d and word_window = %d:' % (num_skips, word_window))
print('batch:', [[rev_dictionary_[bii] for bii in bi] for bi in batch])
print('label_:', [rev_dictionary_[li] for li in label_.reshape(8)])
>>
> with num_skips = 2 and word_window = 1:
batch: [['anarchism', 'as'], ['originated', 'a'], ['as', 'term'], ['a', 'of'], ['term', 'abuse'], ['of', 'first'], ['abuse', 'used'], ['first', 'against']]
label_: ['originated', 'as', 'a', 'term', 'of', 'abuse', 'first', 'used']
> with num_skips = 4 and word_window = 2:
batch: [['anarchism', 'originated', 'a', 'term'], ['originated', 'as', 'term', 'of'], ['as', 'a', 'of', 'abuse'], ['a', 'term', 'abuse', 'first'], ['term', 'of', 'first', 'used'], ['of', 'abuse', 'used', 'against'], ['abuse', 'first', 'against', 'early'], ['first', 'used', 'early', 'working']]
label_: ['as', 'a', 'term', 'of', 'abuse', 'first', 'used', 'against']
以下代码声明了 CBOW 模型配置中使用的变量。单词嵌入向量的大小被指定为 128,并且在目标单词的任一侧,1 个单词被考虑用于预测,如下所示:
num_steps = 100001
"""Initializing :
# 128 is the length of the batch considered for CBOW
# 128 is the word embedding vector size
# Considering 1 word on both sides of the center label words
# Consider the center label word 2 times to create the batches
"""
batch_length = 128
embedding_size = 128
skip_window = 1
num_skips = 2
要注册一个用于 CBOW 实现的 TensorFlow 图,并计算生成的向量之间的余弦相似性,请使用以下代码:
Note
这是一个与 skip-gram 代码中使用的图不同的图,所以这两个代码可以在一个脚本中使用。
"""The below code performs the following operations :
# Performing validation here by making use of a random selection of 16 words from the dictionary of desired size
# Selecting 8 words randomly from range of 1000
# Using the cosine distance to calculate the similarity between the words
"""
tf_cbow_graph = tf.Graph()
with tf_cbow_graph.as_default():
validation_cnt = 16
validation_dict = 100
validation_words = np.array(random.sample(range(validation_dict), validation_cnt//2))
validation_words = np.append(validation_words,random.sample(range(1000,1000+validation_dict), validation_cnt//2))
train_dataset = tf.placeholder(tf.int32, shape=[batch_length,2*skip_window])
train_labels = tf.placeholder(tf.int32, shape=[batch_length, 1])
validation_data = tf.constant(validation_words, dtype=tf.int32)
"""
Embeddings for all the words present in the vocabulary
"""
with tf_cbow_graph.as_default() :
vocabulary_size = len(rev_dictionary_)
word_embed = tf.Variable(tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0))
# Averaging embeddings accross the full context into a single embedding layer
context_embeddings = []
for i in range(2*skip_window):
context_embeddings.append(tf.nn.embedding_lookup(word_embed, train_dataset[:,i]))
embedding = tf.reduce_mean(tf.stack(axis=0,values=context_embeddings),0,keep_dims=False)
以下代码部分使用 64 个单词的负采样计算 softmax loss,并进一步优化模型训练中产生的权重、偏差和单词嵌入。阿达格拉德优化器( www.jmlr.org/papers/volume12/duchi11a/duchi11a.pdf
)已用于此目的。
"""The code includes the following :
# Initializing weights and bias to be used in the softmax layer
# Loss function calculation using the Negative Sampling
# Usage of AdaGrad Optimizer
# Negative sampling on 64 words, to be included in the loss function
"""
with tf_cbow_graph.as_default() :
sf_weights = tf.Variable(tf.truncated_normal([vocabulary_size, embedding_size],
stddev=1.0 / math.sqrt(embedding_size)))
sf_bias = tf.Variable(tf.zeros([vocabulary_size]))
loss_fn = tf.nn.sampled_softmax_loss(weights=sf_weights, biases=sf_bias, inputs=embedding,
labels=train_labels, num_sampled=64, num_classes=vocabulary_size)
cost_fn = tf.reduce_mean(loss_fn)
"""Using AdaGrad as optimizer"""
optim = tf.train.AdagradOptimizer(1.0).minimize(cost_fn)
此外,计算余弦相似度以确保语义相似单词的接近度。
"""
Using the cosine distance to calculate the similarity between the batches and embeddings of other words
"""
with tf_cbow_graph.as_default() :
normalization_embed = word_embed / tf.sqrt(tf.reduce_sum(tf.square(word_embed), 1, keep_dims=True))
validation_embed = tf.nn.embedding_lookup(normalization_embed, validation_data)
word_similarity = tf.matmul(validation_embed, tf.transpose(normalization_embed))
with tf.Session(graph=tf_cbow_graph) as sess:
sess.run(tf.global_variables_initializer())
avg_loss = 0
for step in range(num_steps):
batch_words, batch_label_ = cbow_batch_creation(batch_length, skip_window)
_, l = sess.run([optim, loss_fn], feed_dict={train_dataset : batch_words, train_labels : batch_label_ })
avg_loss += l
if step % 2000 == 0 :
if step > 0 :
avg_loss = avg_loss / 2000
print('Average loss at step %d: %f' % (step, np.mean(avg_loss) ))
avg_loss = 0
if step % 10000 == 0:
sim = word_similarity.eval()
for i in range(validation_cnt):
valid_word = rev_dictionary_[validation_words[i]]
top_k = 8 # number of nearest neighbors
nearest = (-sim[i, :]).argsort()[1:top_k+1]
log = 'Nearest to %s:' % valid_word
for k in range(top_k):
close_word = rev_dictionary_[nearest[k]]
log = '%s %s,' % (log, close_word)
print(log)
final_embeddings = normalization_embed.eval()
> Average loss at step 0: 7.807584
> Nearest to can: ambients, darpa, herculaneum, chocolate, alloted, bards, coyote, analogy,
> Nearest to or: state, stopping, falls, markus, bellarmine, bitrates, snub, headless,
> Nearest to will: cosmologies, valdemar, feeding, synergies, fence, helps, zadok, neoplatonist,
> Nearest to known: rationale, fibres, nino, logging, motherboards, richelieu, invaded, fulfill,
> Nearest to no: rook, logitech, landscaping, melee, eisenman, ecuadorian, warrior, napoli,
> Nearest to these: swinging, zwicker, crusader, acuff, ivb, karakoram, mtu, egg,
> Nearest to not: battled, grieg, denominators, kyi, paragliding, loxodonta, ceases, expose,
> Nearest to one: inconsistencies, dada, ih, gallup, ayya, float, subsumed, aires,
> Nearest to woman: philibert, lug, breakthroughs, ric, raman, uzziah, cops, chalk,
> Nearest to alternative: kendo, tux, girls, filmmakers, cortes, akio, length, grayson,
> Nearest to versions: helvetii, moody, denning, latvijas, subscripts, unamended, anodes, unaccustomed,
> Nearest to road: bataan, widget, commune, culpa, pear, petrov, accrued, kennel,
> Nearest to behind: coahuila, writeup, exarchate, trinidad, temptation, fatimid, jurisdictional, dismissed,
> Nearest to universe: geocentric, achieving, amhr, hierarchy, beings, diabetics, providers, persistent,
> Nearest to institute: cafe, explainable, approached, punishable, optimisation, audacity, equinoxes, excelling,
> Nearest to san: viscount, neum, sociobiology, axes, barrington, tartarus, contraband, breslau,
> Average loss at step 2000: 3.899086
> Average loss at step 4000: 3.560563
> Average loss at step 6000: 3.362137
> Average loss at step 8000: 3.333601
> .. .. .. ..
为了可视化的目的,使用 t-SNE,250 个随机单词的高维、128 个矢量表示已经被用于显示整个二维空间的结果。
num_points = 250
tsne = TSNE(perplexity=30, n_components=2, init="pca", n_iter=5000)
embeddings_2d = tsne.fit_transform(final_embeddings[1:num_points+1, :])
cbow_plot()
函数绘制维度缩减的向量。
def cbow_plot(embeddings, labels):
assert embeddings.shape[0] >= len(labels), 'More labels than embeddings'
pylab.figure(figsize=(12,12))
for i, label in enumerate(labels):
x, y = embeddings[i,:]
pylab.scatter(x, y)
pylab.annotate(label, xy=(x, y), xytext=(5, 2), textcoords='offset points', ha="right", va="bottom")
pylab.show()
words = [rev_dictionary_[i] for i in range(1, num_points+1)]
cbow_plot(embeddings_2d, words)
图 2-14 还示出了具有语义相似性的单词在它们的二维空间表示中彼此放置得更近。例如,单词 right、left 和 end 被放在一起,远离单词 one、two、three 等。
在这里呈现的所有单词中,我们可以观察到,在图的左下方,那些与单个字母表相关的单词被放置得彼此更靠近。这有助于我们理解模型是如何工作的,以及如何将没有重要意义的单个字符分配给相似的单词嵌入。在该群集中不存在诸如 a 和 I 这样的单词表明与这两个单词相关的两个字母的单词嵌入与其他单个字母不相似,因为它们在英语中具有实际意义,并且比其他字母使用得更频繁,在其他字母中,它们仅仅是训练数据集中打字错误的标志。具有更高迭代的模型的进一步训练可以试图使这些字母表的向量更接近或更远离语言的实际有意义的单词。
Note
CBOW 和 skip-gram 方法都使用局部统计来学习单词向量嵌入。有时,通过探索单词对的全局统计可以学习更好的表示,GloVe 和 FastText 方法利用了这一点。关于有关算法的进一步细节,可以分别参考以下论文:GloVe ( https://nlp.stanford.edu/pubs/glove.pdf
)和 FastText ( https://arxiv.org/pdf/1607.04606.pdf
)。
图 2-14
Two-dimensional representation of the word vectors obtained after training the Wikipedia corpus using the CBOW model
后续步骤
本章介绍了在研究和工业领域中使用的单词表示模型。除了 word2vec,还可以探索 GloVe 和 FastText 作为单词嵌入的其他选项。我们尝试使用 CBOW 和 skip-gram 给出一个单词嵌入的可用方法的例子。在下一章中,我们将强调不同类型的可用神经网络,如 RNNs、LSTMs、Seq2Seq,以及它们对文本数据的用例。来自所有章节的知识将帮助读者执行任何结合深度学习和自然语言处理的项目的整个流程。
三、展开循环神经网络
本章介绍了跨文本的上下文信息的使用。对于任何形式的文本工作,即演讲、文本和印刷,以及任何语言,为了理解其中提供的信息,我们试图捕捉和联系现在和过去的上下文,并旨在从中获得一些有意义的东西。这是因为文本的结构创造了一个句子内和跨句子的链接,就像思想一样,贯穿始终。
传统的神经网络缺乏从以前的事件中获取知识并将其传递给未来事件和相关预测的能力。在这一章中,我们将介绍一个神经网络家族,它可以帮助我们长时间地保存信息。
在深度学习中,所有问题一般分为两类:
- 固定拓扑结构:用于具有静态数据的图像,使用案例如图像分类
- 顺序数据:用于带有动态数据的文本/音频,在与文本生成和语音识别相关的任务中
使用卷积神经网络(CNN)解决了静态数据的大多数问题,并且通过循环神经网络(RNNs),特别是通过长短期记忆(LSTM)方法处理了与序列数据相关的大多数问题。我们将在本章中详细讨论这两种类型的网络,并涵盖 rnn 的使用案例。
在正常的前馈网络中,在时间t
要分类的输出不一定与已经分类的先前输出有任何关系。换句话说,先前分类的输出在下面的分类问题中不起任何作用。
但是这是不实际的,因为在很少的情况下,我们必须用以前的输出来预测新的输出。例如,在阅读一本书时,我们必须知道并记住章节中提到的上下文以及整本书讨论的内容。另一个主要的用例是对大部分文本的情感分析。对于所有这些问题,RNNs 已经被证明是非常有用的资源。
无线网络和 LSTM 网络可应用于各种领域,包括
- 查特斯
- 顺序模式识别
- 图像/手写检测
- 视频和音频分类
- 情感分析
- 金融中的时间序列建模
循环神经网络
循环神经网络非常有效,能够执行几乎任何类型的计算。rnn 有各种各样的用例集,可以实现一组多个更小的程序,每个程序独立绘制一幅单独的图片,所有程序并行学习,最终揭示所有这些小程序协作的复杂效果。
rnn 能够执行这样的操作有两个主要原因:
- 隐藏状态本质上是分布式的,存储了大量过去的信息并有效地传递下去。
- 通过非线性方法更新隐藏状态。
什么是复发?
递归是一个递归过程,在此过程中,每一步都会调用一个递归函数来对时态数据集进行建模。
什么是时态数据?依赖于先前数据单元的任何数据单元,尤其是顺序数据。例如,一家公司的股价取决于前几天/几周/几月/几年的股价,因此,对前几天或前几个步骤的依赖性很重要,从而使这类模型非常有用。
所以,下一次你看到任何类型的数据有时间模式时,试着使用本章后面部分讨论的模型类型,但是要预先警告:有大量的数据!
前馈神经网络和循环神经网络的区别
在正常的前馈网络中,数据被离散地馈送给它,而不考虑时间关系。这种类型的网络对于离散预测任务非常有用,因为特征在时间上并不相互依赖。这代表了神经网络的最简单形式,信号沿一个方向流动,即从输入到输出。
例如,如果我们获取三个月的股票价格数据,并试图根据这些数据预测下个月的价格,前馈网络将立即获取前三个月的数据,就好像数据之间没有相互依赖关系一样,但事实可能并非如此。
然而,循环神经网络将一次获取每个月的数据,就像时间序列模型一样。
这一概念的类似功能驱动 RNNs 首先对过去间隔(比如说 ??)的信息执行一些计算,并将其与对当前间隔数据(比如说 t)完成的计算一起使用,并将两者组合以产生下一间隔的结果。
快速浏览一下前馈神经网络和 RNNs 之间的差异,可以发现前馈神经网络仅基于当前输入做出决策,而 RNN 基于当前和先前的输入做出决策,并确保连接也跨隐藏层建立。
以下是前馈神经网络的主要限制:
- 不适用于序列、时间序列数据、视频流、股票数据等。
- 不要在建模中引入记忆因素
图 3-1 说明了一种类型的 RNN 和前馈神经网络之间的区别。
图 3-1
Structural differentiation between a sample RNN and feedforward neural network
循环神经网络基础
在介绍 RNNs 的基础知识和它在 NLP 中的应用之前,我们将快速介绍一个完整的 RNNs 用例。让我们考虑一个例子,其中 RNN 学会了求和运算符的工作方式,并试图复制它。
rnn 属于具有非常强大的序列建模功能的算法家族,在这里,我们将看到,如果给定一个二进制输入序列,该模型如何能够将数字相加,并以近乎完美的精度向我们提供总和作为输出。
给定一个长度为 20 的二进制字符串(只有 0 和 1 的字符串),我们必须确定二进制字符串中 1 的计数。比如“01010010011011100110”有 11 个 1。因此,我们程序的输入将是一个包含 0 和 1 的长度为 20 的字符串,输出必须是一个介于 0 和 20 之间的数字,表示字符串中 1 的数量。
从普通编程的角度来看,这个任务似乎很容易,读者可能认为它类似于任何典型的“Hello World”问题。然而,如果我们从机器的角度来看,它是一个可以将数字相加的模型,一个采用顺序二进制输入进行求和的模型。这就是我们正在处理的事情!
让我们动手为 rnn 定义一些关键术语。在此之前,在执行任何深度学习模型时要记住的一件事是,作为输入输入到模型的张量的形状。当作为模型的输入时,张量可以是任何维度,3-D/4-D。我们可以把它想象成一个列表的列表。一开始理解起来有点复杂,但是我们将会看到如何将这个概念分解成更小更有意义的表示。
Note
[ [ [ ] ] ]是一个三维张量,具有三个分层放置的列表。
RNN 需要一个三维张量作为输入,它可以被完美地分解成图 3-2 所示的组件。
图 3-2
Component-wise detail of a 3-D tensor used as input for RNN Note
没有必要记住这些,当我们继续查看 rnn 的结构时,您将理解以这种方式考虑组件背后的原因。
在当前的问题中,我们采用 20 个时间步长,或长度为 20 的序列输入,并且每个时间步长用 1-D 表示,即,用 0 或 1 的值表示。根据手头的问题,输入时间步长可以是不同的维度。图 3-3 显示了我们将使用的模型的架构。
图 3-3
RNN model architecture to compute the number of 1s in a 20 length sequence of binary digits
在模型图中,我们可以看到,我们将每个二进制单元作为每个时间步长(即 20 个时间步长)的输入,并使它们通过一个隐藏层(在这种情况下是一个递归层),并将最终层的输出作为正常分类多层感知器。
因此,TensorFlow RNN 的输入形式如下
List = [ [ [0] [1] [1] [1] [0] [0] [1] [1] [0] [1] [1] [1] [0] [0] [1] [1] [0] [1] [1] [1] ],
[ [0] [1] [1] [1] [0] [0] [1] [1] [0] [1] [1] [1] [0] [0] [1] [1] [0] [1] [1] [1] ] ,
...., [ [0] [1] [1] [1] [0] [0] [1] [1] [0] [1] [1] [1] [0] [0] [1] [1] [0] [1] [1] [1] ] ]
我们建议不要把重点放在实际的训练部分,因为一旦你理解了数据流过程,训练部分就变得更容易理解,你可以训练多个相关的模型。就这一次,不要把你的注意力从图中显示的隐藏的 RNN 层上转移开,试着得到模型输入的要点。
随着我们的深入,我们将考虑一个稍微复杂一点的例子,并尝试使用循环神经网络进行情感分类(NLP 领域中最基本的任务之一)。
自然语言处理和循环神经网络
从前面的理论和解释中,我们很容易猜测 rnn 是为顺序任务量身定制的,更适合这种问题陈述的是语言。从童年开始,我们人类的大脑就接受了特殊的训练,以适应任何语言的结构。让我们假设英语是大多数人最常用的语言。当我们说话和写作时,我们知道这种语言的普遍结构,因为我们从小就被教导这种语言,而且我们能够毫不费力地破译它。
我们应该通过使用语法来使用适当的语言,语法构成了语言的基本规则。传统上,NLP 任务极其困难,因为不同语言的语法非常庞大。
针对每种语言的约束的硬编码有其自身的缺点。没有人想在世界上不同语言中存在的成百上千的语法规则中寻找答案,也没有人想按照定制的业务需求进一步学习或编写代码。
将我们从所有这些麻烦中拯救出来的是深度学习,它的目标是学习所有语言的复杂局部结构公式,并使用这种学习来破解问题集中存在的复杂性。
所以,最后,我们让我们的宝贝深度学习模型,属于 RNN 类别,自己学习。我们一个字一个字地给它输入英语句子的序列,让它在一些监督标签上训练,比方说,情感分类的积极或消极,或者暂时对文本进行星级评定的 1,2,3,4,5。
让我们通过考虑一个 n 元语言模型的例子来理解这一点。这里,如果我们有四个前面的单词,4-gram,我们的模型具有通过使用来自这种四个单词的组合类型的出现的过去信息来预测最可能的第五个单词的能力。这种类型的模型在诸如 Google 搜索自动完成建议的问题中有直接的用例。
Note
用于谷歌搜索的实际模型不仅仅是任何 n 元语法的直接实现,而是更复杂模型的组合。
让我们通过考虑一个基本的例子来理解这个概念。假设我们有一个正常的英语句子:“萨钦是一个伟大的板球运动员。”然后,我们可以根据我们的深度学习模型采用的输入来表示这句话,如图 3-4 所示。
图 3-4
Inputting the “Sachin is a great” sentence into the model
在这里,最后一个词,板球运动员,可以从前面四个词的顺序来判断沙钦是一个伟大的。我们可以判断“萨钦是一个伟大的”——什么?一个答案可能是“板球运动员”,因为我们对这样一个问题和背景的思考已经被这样建模了。同样,在某些情况下,我们希望模型考虑过去的历史事件,并对未来事件做出预测。这些事件也可能与我们能够从文本中提取的信息有关。
前馈网络一次性将整个句子作为输入,而 RNN 则一个接一个地提取每个单词,然后对给定文本进行分类。前面的图表会使它更清楚。
RNN 以单词嵌入的形式接受输入,这已在第二章中介绍过,有两种不同类型的模型,CBOW 和 Skip-gram。
word2vec 模型旨在为每个单词初始化随机向量,进一步学习这些向量以获得有意义的向量,从而执行特定的任务。词向量可以由任何给定的维度构成,并且能够相应地封装信息。
RNNs 机制
rnn 在不同的领域有创造性的应用,从音频和文本到图像,包括音乐生成、字符生成、机器翻译等。让我们尝试以一种对初学者更友好的方式来理解 RNNs 的功能过程,这样任何没有深度学习背景的人也可以理解它(图 3-5 )。
我们将使用 NumPy 库进行向量乘法,并描述内部数学。这个阶跃函数在每个时间步长被调用,即递归。
图 3-5
Unrolled recurrent neural network
首先,定义 RNN 类:
class RNN:
# ...
def step(self, x):
# Update the Hidden state
self.h = np.tanh(np.dot(self.W_hh, self.h) + np.dot(self.U_xh, x))
# Compute the Output vector
o = np.dot(self.V_hy, self.h)
return o
前面的伪代码指定了基本 RNN 的正向传递。函数step
在 RNN 类的每个时间步被调用。这个 RNN 的参数是三个矩阵(W_hh, U_xh, V_hy
)。
以下是来自前面伪码的每个权重矩阵的维数及其来自图 3-5 的等效实体:
- 在时间步长 t 输入 X t
- S t 是时间步长 t 时的隐藏状态。它是网络的“记忆”,是根据先前的隐藏状态和当前步长的输入计算的。
- U xh 是从输入(x)到隐藏层(h)的映射,因此,h
×
维度(x),其中 x 的维度是每个时间步长输入的维度(1,在二进制求和的情况下)。请参考上图中的 U 矩阵。 - W hh 在隐藏状态之间映射,因此,h
×
h。参考上图中的 W 矩阵。 - V hy 从最终隐藏层映射到输出 y。因此,h x dimension (y),其中 y 的维度是输出的维度(20,在之前考虑的二进制求和情况下)。请参考上图中的 V 矩阵。
- o t 是步骤 t 的输出。例如,如果我们想预测一个句子中的下一个单词,它将是我们词汇中的概率向量。
隐藏状态self.h
用零向量初始化。np.tanh
函数实现了将激活压缩到范围(-1, 1
)的非线性。
请简要注意这是如何工作的。在tanh
函数内部有两个术语:第一个是基于先前的隐藏状态,第二个是基于当前的输入。在 NumPy 中,np.dot
执行矩阵乘法。
这两个中间体与加法相互作用,然后被tanh
函数挤压到新的状态向量中。为了从数学符号的角度来推断隐藏状态更新,我们可以将其重写如下:
其中 f 1 通常被视为tanh
或sigmoid
,并按元素方式应用。
我们用随机数初始化 RNN 的矩阵,并且在训练阶段执行的大部分工作进入产生期望行为的理想矩阵的计算。这是用某种损失函数来衡量的,这种损失函数表达了我们对哪种输出o
的偏好,我们希望看到哪种输出响应于我们的输入序列x
。
我们可以用多种方式训练 RNN 模型。然而,不可知的任何具体方式,rnn 有一个非常特殊的问题,它面临的原因是,随着权重随着时间的传播,它们在前面的函数中递归地相乘,从而产生以下两种情况:
- 消失梯度:如果权重很小,后续值将不断变小,并趋于~0。
- 爆炸梯度:如果权重很大,最终值将接近无穷大。
这两个问题使得 RNNs 对时间步长或序列限制的数量非常敏感。我们可以通过考虑 RNN 的输出来更详细地理解这一点。RNN 网络的输出表示如下:
其中 U 和 V 分别是连接输入和递归输出的权重矩阵,f 2 是用于分类任务的 softmax,L2 范数(平方误差)用于回归任务。Softmax 在 h t 输出端。
然而,请注意,如果我们参考,比方说,我们的循环神经网络中的三个时间步长(在前面的部分中解释),我们有以下:
从前面的等式中,我们可以推断,随着网络通过添加更复杂的层而变得更深,并且随着时间的传播,它将导致梯度消失或爆炸问题。
当输入值接近 0 或 1 时,会出现sigmoid
函数的梯度问题。此时,梯度很小,趋于消失,如图 3-6 所示。
图 3-6
Logistic curve, at top, along with its first degree differentiation, below
图 3-7 说明了 RNN 中的消失梯度问题。
图 3-7
Example of vanishing gradient
如上图所示(h 0 ,h 1 ,h 2 ,h 3 ,均为隐藏状态),在每个时间步,当我们运行反向传播算法时,梯度变得越来越小,当我们回到句子的开头时,梯度变得如此之小,以至于实际上无法对必须更新的参数产生显著影响。之所以会出现这种效应,是因为除非 d h??/d ht恰好为 1,或者 d h??/d ht= 1,否则它将倾向于减小或放大梯度 d l/d h t ,当这种减小或其放大重复进行时,它将对损耗的梯度产生指数效应。
为了解决这个问题,使用了一种称为长短期记忆(LSTM)网络和门控整流单元(GRUs)的特定类型的隐藏层。后者是特殊的门控细胞,旨在本质上处理这种情况。我们将在本章的后面部分简要介绍这些内容。
培训注册护士
RNNs 最值得注意的一点是,它们在训练方面非常灵活,可以在监督和非监督领域的广泛问题上表现出色。在进入正题之前,让我们先了解一下隐藏态(LSTM/GRU/s 形神经元)的深层秘密。
好奇的人可能会想知道隐藏状态到底是什么。它像一个正常的前馈网络吗?还是本质上更复杂?
前面问题的答案是,任何隐藏状态的数学表示与任何正常前馈网络的数学表示相同,并且对于任何静态/无状态维度,它确实表示输入的隐藏特征。
然而,正如我们看到的 RNNs 的特殊递归属性,在任何时间间隔步长的 RNNs 的隐藏状态中,它以压缩密集的方式表示所有先前时间步长的上下文表示。它也包含密集向量中的语义序列信息。
例如,在时间 t,H(t)的隐藏状态包含时间间隔 X(t-1),X(t-2),,的一些噪声和一些真实信息。。。,X(0)。
考虑到 RNN 训练,对于监督学习的任何问题,我们必须找到一个Loss
函数,它有助于通过反向传播或梯度下降来更新随机初始化的权重。
Note
不熟悉反向传播实现的读者不应该太担心,因为像 TensorFlow 和 PyTorch 这样的现代库具有超快的自动微分过程,使这些任务变得容易得多。人们只需要定义网络架构和目标。然而,建议读者彻底了解反向传播技术,用神经网络进行更多的实验,因为这是任何神经网络训练的基础。
现在,让我们创建二进制序列求和的初始示例。以下是对网络如何运行和训练的逐步解释:
-
将隐藏状态初始化为一个随机数向量(隐藏层的大小是我们设置的自由参数)。
-
Feed the binary number, 0 or 1, at each sequence step. Hence, calculating and updating the hidden vector at each step according to the following equation:
where, ‘.’ represents the dot product between the two matrices, and H, X, U, V have the same references as before.
-
最后一个隐藏层(特别是在这种情况下)作为输出,并馈入另一个多层感知器(前馈网络)。
所以,基本上,最后一层是整个序列的表示,这最后一层(t 时刻的隐藏表示)是最重要的一层。然而,在更早的时间间隔{t-1,t-2,…,0}的其他隐藏状态也可以用于其他目的。
Note
与传统的反向传播不同,RNNs 有一种称为时间反向传播(BPTT)的特定算法。在 BPTT,在时间 t,层的梯度更新依赖于时间 t-1,t-2,…,0。因此,在其所有形式中,反向传播是通过连续的时间步骤完成的。然而,如果一个人理解 BPTT,很明显它只是正常反向传播的一个特例。
除了通过从最后一个隐藏层获取输出来进行训练之外,如果一个人具有好奇/直觉的头脑,他/她可能会想为什么我们没有获取所有的隐藏状态并将它们平均。的确,那是另一种手段。如果读者已经得出结论,那么很高兴知道他/她正在很好地掌握 RNNs!图 3-8 显示了利用模型输出的多种方式。
图 3-8
An RNN can be trained in multiple ways, as required. One can take output of just the last time step, or all the time steps, or take the average of all the time steps.
RNN 隐藏状态的元意义
RNN 中的隐藏状态非常重要。除了作为矩阵乘法的数学输出之外,RNN 隐藏状态还保存了关于数据的一些关键信息,即,特别是顺序信息。RNN 的最后隐藏状态能够完成各种高度创造性的任务。例如,有一个非常直观的模型叫做序列对序列(seq-to-seq 或 seq2seq)模型。这些模型用于机器翻译、图像字幕等。我们将在接下来的章节中简要概述它是如何工作的,但是编码和其他相关的细节已经超出了本书的范围。
假设我们有一个英语句子,我们想使用 seq2seq 模型将它自动转换/翻译成法语。直觉上,我们向 RNN 模型输入一个单词序列,一个英语句子,并且只考虑最后的隐藏输出。这个隐藏的输出似乎存储了句子最相关的信息。接下来,我们使用这个隐藏状态来初始化另一个将进行转换的 RNN。就这么简单,对吧?
调谐 rnn
rnn 对输入变量非常关键,本质上非常容易接受。在训练过程中起主要作用的 RNNs 中的一些重要参数包括:
- 隐藏层数
- 每层隐藏单元的数量(通常选择每层相同的数量)
- 优化器的学习速率
- 退出率(RNNs 中最初成功的退出仅适用于前馈连接,不适用于循环连接)
- 迭代次数
通常,我们可以用验证曲线和学习曲线来绘制输出,并检查过度拟合和欠拟合。训练和测试在每次分裂时的误差应该被绘制出来,根据我们检查的问题,如果它是一个过拟合,那么我们可以减少隐藏层或隐藏神经元的数量,或者增加辍学率,或者增加辍学率,反之亦然。
然而,除了这些考虑之外,另一个主要问题是权重,我们在 TensorFlow 库中有权重/梯度裁剪和多个初始化函数。
长短期记忆网络
LSTM 网络由 Sepp Hochreiter 和 J ü
rgen Schmidhuber 于 1997 年首次提出,解决了 rnn 在更长时间内( www.bioinf.jku.at/publications/older/2604.pdf
)保留信息的问题。
已经证明 RNNs 是处理与序列分类相关的问题的唯一选择,并且已经证明它们适合于保留来自先前输入数据的信息,并且使用该信息在任何时间步骤修改输出。然而,如果序列的长度足够长,则在 RNN 模型的训练过程中计算的梯度,特别是反向传播,或者由于 0 和 1 之间的值的累积乘法效应而消失,或者再次由于大值的累积乘法而爆炸,从而导致模型以相对较慢的方式训练。
LSTM 网络是这里的救星。正是这种类型的 RNN 体系结构有助于在冗长的序列中训练模型,并有助于保持输入到模型的先前时间步骤的记忆。理想情况下,它通过引入额外的门、输入和遗忘门来解决梯度消失或梯度爆炸问题,从而允许更好地控制梯度,允许保留什么信息和遗忘什么信息,从而控制信息对当前单元状态的访问,这使得能够更好地保持“长期依赖性”
即使我们可以尝试其他的激活函数,比如 ReLU,来减少问题,它们也不能完全解决问题。RNN 的这一缺点导致了 LSTM 网络的兴起,从而有效地解决了这一问题。
LSTM 的组成部分
LSTM 网络也具有链状结构,但是重复模块具有不同的结构。不是只有一个神经网络层,而是有四个,以一种非常特殊的方式相互作用。LSTM 电池的结构如图 3-9 所示。
图 3-9
LSTM module with four interacting layers
使用多个门来形成 LSTM,这是一个很好的选择,可以用来管理通过的信息。它们有一个 sigmoid 神经网络层,输出为[0,1]以衡量组件的通过限制,以及一个逐点乘法运算。
在上图中,C i 是细胞状态,它存在于所有的时间步中,并因每个时间步中的相互作用而改变。为了通过单元状态保留流经 LSTM 的信息,它有三种类型的门:
-
Input gate : To control the contribution from a new input to the memory
Here x t denotes the input at time step t, ht - 1 denotes the hidden state at time step t-1, i t denotes the input gate layer output at time step t, Ć t refers to candidate values to be added to input gate output at time step t, b i and b c denote the bias for the input gate layer and the candidate value computation, W i and W c denote the weights for the input gate layer and the candidate value computation.
Here, C i denotes the cell state after time step i, and f t denotes the forget state at time step t.
-
Forget gate: To control the limit up to which a value is pertained in the memory
Here, f t denotes the forget state at time step t and, W f and b f denote the weights and bias for the forget state at time step t.
-
Output gate: To control up to what limit memory contributes in the activation block of output
Here, o t denotes the output gate output at time step t, and W o and b o denote the weights and bias for the output gate at time step t.
今天,LSTM 网络已经成为比基本 rnn 更受欢迎的选择,因为它们已经被证明在各种问题上非常有效。与 RNNs 相比,最显著的结果是用 LSTM 网络实现的,并且现在这种现象已经扩展到,无论哪里引用 RNN,它通常仅指 LSTM 网络。
LSTM 如何帮助减少消失梯度问题
如我们之前提到的,在基本 RNN 中,在反向传播期间,即在计算梯度以更新权重时,出现消失梯度,因为它涉及偏导数的级联,并且每个偏导数涉及σ项,即 sigmoid 神经网络层。由于每个 sigmoid 导数的值可能变得小于 1,从而使整体梯度值变得足够小,以至于它们不能进一步更新权重,这意味着模型将停止学习!
现在,在一个 LSTM 网络中,遗忘门的输出是
所以,C 对其时间滞后值 C t -1 的偏导数将得到值 f t ,乘以偏导数的次数。现在,如果我们设置 f = 1 的输出,就不会有梯度的衰减,这意味着所有过去的输入都会被记忆在单元格中。在训练过程中,遗忘之门将决定哪些信息是重要的,保留哪些信息,删除哪些信息。
了解 GRUs
今天有许多 LSTM 的变体在使用。LSTM 的一个合理变化是门控循环单元,或 GRU(图 3-10 )。它通过组合遗忘门和输入门来形成更新门,还合并单元状态和隐藏状态,并改变生成输出的方式。与标准的 LSTM 模型相比,得到的模型通常具有较低的复杂性。
GRU 像 LSTM 单元一样控制信息流,但不需要使用存储单元。它只是暴露了完全隐藏的内容,没有任何控制。
据观察,LSTM 更适合较大的数据集,而 GRU 更适合较小的数据集。因此,没有硬性的规则,因为在某种程度上,效率取决于数据和模型的复杂性。
图 3-10
LSTM and GRU
LSTMs 的局限性
除了 LSTM 网络的复杂性,它们通常比其他典型模型要慢。通过仔细的初始化和训练,即使是 RNN 也能产生类似于 LSTM 的结果,而且计算复杂度更低。此外,当最近的信息比旧信息更重要时,毫无疑问,LSTM 模型总是更好的选择,但有些问题我们希望深入到过去来解决。在这种情况下,一种被称为注意力机制的新机制——这是一种稍微修改过的版本——越来越受欢迎。我们将在后面的小节“注意力评分”中介绍它
序列间模型
序列到序列(seq2seq)模型被用于从聊天机器人到语音到文本到对话系统到 QnA 到图像字幕的一切。seq2seq 模型的关键是序列保持了输入的顺序,而基本神经网络却不是这样。当然没有好的方法来表示时间和随时间变化的事物的概念,所以 seq2seq 模型允许我们处理带有时间或时间顺序元素的信息。它们允许我们保存普通神经网络无法保存的信息。
这是什么?
简单来说,seq2seq 模型由两个独立的 rnn 组成,即编码器和解码器。编码器将信息作为多个时间步长中的输入,并将输入序列编码成上下文向量。解码器获取该隐藏状态,并将其解码为所需的输出序列。对于这种类型的模型,需要大量的数据,比如难以置信的大量数据。
seq2seq 模型背后的关键任务是将序列转换成固定大小的特征向量,该向量只编码序列中的重要信息,而丢失不必要的信息。
让我们考虑一个基本问答系统的例子,其中的问题是“你好吗?”在这种情况下,模型将单词序列作为输入,因此我们将尝试将序列中的每个单词放入一个固定大小的特征向量中,然后该向量可用于预测模型的输出,以获得结构化答案。模型必须记住第一个序列中的重要事情,同时也要丢掉该序列中任何不必要的信息,以产生相关的答案。
图 3-11 显示了编码器和解码器的展开版本,以便更好地理解整个过程。
图 3-11
Sample seq2seq model with input and output sentence
在编码器阶段,我们向网络输入嵌入在问题“你好吗?”中的单词向量,连同一组权重分配给 LSTMs 序列。在解码器端,在顶部,我们有一个时间分布的密集网络(在代码部分解释),它用于预测当前文本词汇中的单词以获得答案。
相同的模型可以用于聊天机器人、语言翻译和其他相关目的。
双向编码器
在双向编码器中,我们有一个覆盖正向文本的 lstm 系列和另一个覆盖反向文本的 lstm 系列,就在前一个系列之上。因此,这种情况下的权重,即上图中的 A,基本上是隐藏状态,我们最终有两个隐藏状态:一个来自向前方向,一个来自向后方向。这允许网络从文本中学习并获得关于上下文的全部信息。
对于几乎所有的 NLP 任务来说,双向 LSTMs 通常比其他任何方法都要好(图 3-12 )。我们添加的双向 LSTMs 层越多,结果就越好。
图 3-12
Bidirectional encoder
堆叠双向编码器
对于堆叠式双向编码器,如下图所示,我们有两个双向 LSTMs 或四层。(对于更复杂的结构和实现更好的结果,可以达到六个双向 LSTMs。)
这些 LSTM 层中的每一层内部都有权重,这些权重在自我学习,同时也影响前面层中的权重。
随着网络相对于给定的输入在时间上向前移动,并且遇到来自传入文本的新信息,它产生一个隐藏状态,表示在整个文本中存在的所有有用的东西(图 3-13 )。
图 3-13
Stacked bidirectional encoder
解码器
编码器输出上下文向量,该向量提供之前发生的整个序列的快照。通过将上下文向量传递给解码器,上下文向量用于预测输出。
在解码器中,我们有一个使用 softmax 的密集层,就像在正常的神经网络中一样,并且它是时间分布的,这意味着每个时间步都有一个这样的层。
在图 3-14 中,顶部的圆圈代表整个词汇,得分最高的对应那个时间步的输出。这是有效的,如果我们正在处理文本,并试图只获得单词的结果,顶层将有一个神经元用于词汇表中的每个单词。随着词汇表大小的增加,顶层通常会变得非常大。
重要的是,为了开始预测,我们传入一个<GO>
令牌来启动预测过程。接下来,我们将<GO>
标记本身作为第一个单元格的输入,它现在预测我们答案的第一个单词,以及来自上下文向量的信息,然后我们从模型中获取预测的第一个单词,并将其作为输入输入到下一个时间步骤,以获得第二个单词的预测,依此类推。这将导致我们的答案的整个文本的创建。理论上,在理想情况下,当预测正确时,模型应该预测我们试图回答或翻译的任何内容。
图 3-14
Decoder
高级序列到序列模型
基本的 seq2seq 模型对于短句的正常任务很有效,但是对于长句就开始失效了。此外,正常的 LSTMs 可以记住大约 30 个时间步长,并且在 30 个时间步长之后开始非常快速地下降。如果他们没有得到足够的训练,他们甚至会更快地离开。
与基本的 seq2seq 模型相比,注意机制在短期长度序列上表现更好。此外,使用注意机制,我们可以达到大约 50 个时间步长的最大长度。目前 NLP 的一个主要限制是我们没有任何东西可以真正回到过去,甚至记住几个段落,更不用说整本书了。
有几个技巧可以解决这个问题。例如,我们可以翻转输入,并向后训练模型,即向后进入,向前出来。这通常会使结尾词更接近,并有助于更好地关联预测词。
序列到序列可以是 rnn、lstm(首选)或 gru,对于较低级别的任务,首选双向 lstm。我们将研究一些用于处理此类问题的高级模型。
注意力评分
注意力模型查看显示的整个内容,并找出方法来计算出哪个单词对文本中的每个单词最重要。所以,它会给你句子中的每个单词打分,这样,它就能感觉到某些单词对某些单词的依赖程度远远超过其他单词。
以前的文本生成方法包括生成语法非常好的句子,但这要么会弄错名称,要么会重复一些字符,如问号。理解注意力模型的最好方法是把它们想象成一种小小的记忆模块,它基本上位于网络之上,然后查看单词并挑选出最重要的。例如,在下面的句子中,显然不是所有的单词都同等重要:
上个月大家都去了俱乐部,但我呆在家里。
上个月大家都去了俱乐部,但我呆在家里。
与句子中的其他单词相比,第二个句子中的斜体单词是被注意到且得分较高的单词。这有助于翻译成不同的语言,也有助于保留上下文信息,例如“上个月”发生的事件,因为在执行 NLP 任务时需要这个时间信息。
增加注意力有助于获得固定长度的向量,每个单词的得分告诉我们每个单词和时间步长在给定序列中的重要性。这在翻译时变得很重要。当手动翻译一个长句子时,我们更关注特定的单词或短语,而不考虑它们在句子中的位置。注意力有助于为神经网络重建同样的机制。
如前所述,正常模型无法捕捉完整句子的关键,仅使用单个隐藏状态,随着长度的增加,情况会变得更糟。注意力向量(如图 3-15 所示)通过在解码器的每一步从整个输入句子中捕捉信息,有助于提高模型的性能。该步骤确保解码器不仅依赖于最后的解码器状态,还依赖于所有输入状态的组合权重。
最好的技术是在编码器中使用双向 LSTMs,同时注意它。
图 3-15
Attention scoring network
图 3-16 展示了一个用于语言翻译的注意力评分网络的用例。编码器获取输入令牌,直到它获得一个特殊的结束令牌,比如说<DONE>
,然后解码器接管并开始生成令牌,也以它自己的结束令牌<DONE>
结束。
随着英语句子标记的到来,编码器改变其内部状态,然后,一旦最后一个标记到达,就获取最终的编码器状态并将其传递给解码器,不变并重复。在解码器中,生成每一个德国令牌。解码器也有自己的动态内部状态。
图 3-16
Language translation using an attention scoring network
教师强迫
教师强制使用地面实况作为每个连续时间步长的输入,而不是网络的输出。
人们可以参考关于教师强迫的原始论文的摘要,“教授强迫:训练递归网络的新算法”,以获得对该技术的令人信服的解释( https://papers.nips.cc/paper/6099-professor-forcing-a-new-algorithm-for-training-recurrent-networks.pdf
)。
Teachers' mandatory algorithm trains recursive networks by providing observed sequence values as input in the training process and using the network's own one-step prediction to do multi-step sampling. We introduce the professor-forced algorithm, which uses the opposite domain adaptation to encourage the cyclic network to have the same dynamics when training the network and sampling from the network at multiple time steps.
为了更好地理解这一点,当我们训练教师强制模型时,在进行预测部分时,我们检查预测的每个单词是否正确,并在反向传播网络时使用该信息。但是,我们不会将预测的单词输入到下一个时间步骤。相反,在进行每个下一个单词预测时,我们使用上一个时间步的正确单词答案进行下一个时间步预测。这就是为什么这个过程被称为“教师强迫”我们基本上是强迫解码器部分不仅使用最后一个隐藏状态的输出,而且实际上使用正确的答案。这极大地改进了文本生成的训练过程。在对测试数据集进行实际评分时,不需要遵循此流程。将学习到的权重用于计分步骤。
教师强迫技术是作为训练 RNN 的时间反向传播的一种替代方法而开发的。图 3-17 显示了一个使用教师强制机制训练 RNN 的例子。
图 3-17
Teacher forcing approach
偷看
扫视包括通过 RNN 或 LSTM 的每一步直接输入上下文向量的隐藏状态。隐藏状态在每次通过权重时都会改变,我们利用这个更新的隐藏状态,并且还保留来自编码器的原始上下文向量,以便它检查发生的定期更新,从而找出提高准确性的方法。
Peeking 是由 Yoshua Bengio 等人在研究论文《使用 RNN 编码器学习短语表示——统计机器翻译的解码器》( https://arxiv.org/abs/1406.1078
)中提出的。
We propose a new neural network model called rnn encoder-decoder, which consists of two RNN. One RNN encodes the symbol sequence into a fixed-length vector representation, and the other decodes the representation into another symbol sequence. The encoder and decoder of this model are jointly trained to maximize the conditional probability of the target sequence given the source sequence. The proposed model learns the semantic and syntactic meaningful expressions of language phrases.
序列间用例
对于 seq2seq 模型的用例,我们采用了 H. Gurulingappa 的研究论文“支持从医学病例报告中自动提取药物相关不良反应的基准语料库的开发”( www.sciencedirect.com/science/article/pii/S1532046412000615
)中使用的注释语料库的文本内容。
The purpose of this paper is to generate a systematic annotated corpus, which can support the development and verification of methods for automatically extracting drug-related adverse reactions from medical case reports. In order to ensure the consistency of annotations, documents are systematically double annotated in different rounds. The annotations are finally coordinated to generate representative consistent annotations. We use the open source skip-gram model (
http://evexdb.org/pmresources/vec-space-models/wikipedia-pubmed-and-PMC-w2v.bin
) provided by NLPLab, which is trained on all PubMed abstracts and PMC full texts (4.08 million different words). The output of the skip model is a set of 200-dimensional word vectors.
像往常一样,首先导入所有必需的模块:
# Importing the required packages
import os
import re
import csv
import codecs
import numpy as np
import pandas as pd
import nltk
from nltk.corpus import stopwords
from nltk.stem import SnowballStemmer
from string import punctuation
from gensim.models import KeyedVectors
检查用于本练习的 Keras 和 TensorFlow 版本:
import keras
print(keras.__version__)
> 2.1.2
import tensorflow
print(tensorflow.__version__)
> 1.3.0
确保您已经从前面提到的链接下载并保存了 word 嵌入文件到当前的工作目录中。
EMBEDDING_FILE = 'wikipedia-pubmed-and-PMC-w2v.bin'
print('Indexing word vectors')
> Indexing word vectors
word2vec = KeyedVectors.load_word2vec_format(EMBEDDING_FILE, binary=True)
print('Found %s word vectors of word2vec' % len(word2vec.vocab))
> Found 5443656 word vectors of word2vec
import copy
from keras.preprocessing.sequence import pad_sequences
> Using TensorFlow backend.
Gurulingappa 在论文中使用的 ADE 语料库分为三个文件:DRUG-AE.rel
、DRUG-DOSE.rel
和ADE-NEG.txt
。我们正在利用DRUG-AE.rel
文件,该文件提供了药物和不良反应之间的关系。
以下是该文件中的文本示例:
10030778 | Intravenous azithromycin-induced ototoxicity. | ototoxicity | 43 | 54 | azithromycin | 22 | 34
10048291 | Immobilization, while Paget’s bone disease was present, and perhaps enhanced activation of dihydrotachysterol by rifampicin, could have led to increased calcium-release into the circulation. | increased calcium-release | 960 | 985 | dihydrotachysterol | 908 | 926
10048291 | Unaccountable severe hypercalcemia in a patient treated for hypoparathyroidism with dihydrotachysterol. | hypercalcemia | 31 | 44 | dihydrotachysterol | 94 | 112
10082597 | METHODS: We report two cases of pseudoporphyria
caused by naproxen and oxaprozin. | pseudoporphyria | 620 | 635 | naproxen | 646 | 654
10082597 | METHODS: We report two cases of pseudoporphyria caused by naproxen and oxaprozin. | pseudoporphyria | 620 | 635 | oxaprozin | 659 | 668
DRUG-AE.rel
文件的格式如下,字段由管道分隔符分隔:
第 1 列:PubMed-ID
第二栏:句子
第 3 栏:不利影响
第 4 列:在“文档级别”开始抵消不利影响
第 5 列:“文档级别”的不利影响的结束偏移量
第 6 栏:药物
第 7 列:药品在“文件级别”的起始偏移量
第 8 列:药品在“文档级别”的结束偏移量
Note
在注释过程中,使用了以下格式的文档:PubMed-ID \n \n Title \n \n Abstract。
# Reading the text file 'DRUG-AE.rel' which provides relations between drugs and adverse effects.
TEXT_FILE = 'DRUG-AE.rel'
接下来,我们想要为我们的模型创建输入。我们模型的输入是一个字符序列。目前,我们认为序列长度为 200,也就是说,我们将拥有一个大小为“原始字符数-序列长度”的数据集
对于每一个输入数据,即 200 个字符的序列,接下来,一个字符将以一键编码的格式输出。我们将在input_data_ae
和op_labels_ae
张量中添加输入数据字段及其相应的标签,如下所示:
f = open(TEXT_FILE, 'r')
for each_line in f.readlines():
sent_list = np.zeros([0,200])
labels = np.zeros([0,3])
tokens = each_line.split("|")
sent = tokens[1]
if sent in sentences:
continue
sentences.append(sent)
begin_offset = int(tokens[3])
end_offset = int(tokens[4])
mid_offset = range(begin_offset+1, end_offset)
word_tokens = nltk.word_tokenize(sent)
offset = 0
for each_token in word_tokens:
offset = sent.find(each_token, offset)
offset1 = copy.deepcopy(offset)
offset += len(each_token)
if each_token in punctuation or re.search(r'\d', each_token):
continue
each_token = each_token.lower()
each_token = re.sub("[^A-Za-z\-]+","", each_token)
if each_token in word2vec.vocab:
new_word = word2vec.word_vec(each_token)
if offset1 == begin_offset:
sent_list = np.append(sent_list, np.array([new_word]), axis=0)
labels = np.append(labels, np.array([[0,0,1]]), axis=0)
elif offset == end_offset or offset in mid_offset:
sent_list = np.append(sent_list, np.array([new_word]), axis=0)
labels = np.append(labels, np.array([[0,1,0]]), axis=0)
else:
sent_list = np.append(sent_list, np.array([new_word]), axis=0)
labels = np.append(labels, np.array([[1,0,0]]), axis=0)
input_data_ae.append(sent_list)
op_labels_ae.append(labels)
input_data_ae = np.array(input_data_ae)
op_labels_ae = np.array(op_labels_ae)
向输入文本添加填充,在任何时间步长输入的最大长度为 30(一个安全的赌注!).
input_data_ae = pad_sequences(input_data_ae, maxlen=30, dtype="float64", padding="post")
op_labels_ae = pad_sequences(op_labels_ae, maxlen=30, dtype="float64", padding="post")
检查输入数据中条目总数的长度及其对应的标签。
print(len(input_data_ae))
> 4271
print(len(op_labels_ae))
> 4271
从 Keras 导入所需模块。
from keras.preprocessing.text import Tokenizer
from keras.layers import Dense, Input, LSTM, Embedding, Dropout, Activation,Bidirectional, TimeDistributed
from keras.layers.merge import concatenate
from keras.models import Model, Sequential
from keras.layers.normalization import BatchNormalization
from keras.callbacks import EarlyStopping, ModelCheckpoint
创建训练和验证数据集,训练中有 4,000 个条目,其余 271 个在验证数据集中。
# Creating Train and Validation datasets, for 4271 entries, 4000 in train dataset, and 271 in validation dataset
x_train= input_data_ae[:4000]
x_test = input_data_ae[4000:]
y_train = op_labels_ae[:4000]
y_test =op_labels_ae[4000:]
现在我们有了标准格式的数据集,接下来是这个过程中最重要的部分:定义模型架构。我们将使用双向 LSTM 网络的一个隐藏层,有 300 个隐藏单元,丢失概率为 0.2。除此之外,我们还使用了一个 TimeDistributedDense 层,丢失概率为 0.2。
Dropout 是一种正则化技术,通过这种技术,当你更新神经网络的层时,你随机地不更新,或dropout
,某些层。也就是说,在更新你的神经网络层时,你用概率1-dropout
更新每个节点,用概率dropout
保持不变。
时间分布层用于 RNN(和 LSTMs)以保持输入和输出之间的一对一映射。假设我们有 30 个时间步长和 200 个数据样本,即 30 个×
200,我们想要使用输出为 3 的 RNN。如果我们不使用一个时间分布密度层,我们将得到一个 200 ×
30 ×
3 张量。因此,我们将输出平坦化,每个时间步长混合。如果我们应用 TimeDistributedDense 层,我们将在每个时间步长上应用完全连接的密集层,并按时间步长分别获得输出。
我们还使用categorical_crossentropy
作为损失函数,adam
作为优化器,softmax
作为激活函数。
您可以尝试所有这些东西,以便更好地了解 LSTM 网络是如何工作的。
batch = 1 # Making the batch size as 1, as showing model each of the instances one-by-one
# Adding Bidirectional LSTM with Dropout, and Time Distributed layer with Dropout
# Finally using Adam optimizer for training purpose
xin = Input(batch_shape=(batch,30,200), dtype="float")
seq = Bidirectional(LSTM(300, return_sequences=True),merge_mode='concat')(xin)
mlp1 = Dropout(0.2)(seq)
mlp2 = TimeDistributed(Dense(60, activation="softmax"))(mlp1)
mlp3 = Dropout(0.2)(mlp2)
mlp4 = TimeDistributed(Dense(3, activation="softmax"))(mlp3)
model = Model(inputs=xin, outputs=mlp4)
model.compile(optimizer='Adam', loss="categorical_crossentropy")
我们将用 50 个纪元和 1 的批量大小来训练我们的模型。只要模型不断改进,您总是可以增加纪元的数量。还可以创建检查点,以便以后可以检索和使用模型。创建检查点的想法是在训练时保存模型权重,以便以后不必再次经历相同的过程。这是留给读者的一个练习。
model.fit(x_train, y_train,
batch_size=batch,
epochs=50,
validation_data=(x_test, y_test))
> Train on 4000 samples, validate on 271 samples
> Epoch 1/50
4000/4000 [==============================] - 363s 91ms/step - loss: 0.1661 - val_loss: 0.1060
> Epoch 2/50
4000/4000 [==============================] - 363s 91ms/step - loss: 0.1066 - val_loss: 0.0894
> Epoch 3/50
4000/4000 [==============================] - 361s 90ms/step - loss: 0.0903 - val_loss: 0.0720
> Epoch 4/50
4000/4000 [==============================] - 364s 91ms/step - loss: 0.0787 - val_loss: 0.0692
> Epoch 5/50
4000/4000 [==============================] - 362s 91ms/step - loss: 0.0698 - val_loss: 0.0636
...
...
...
> Epoch 46/50
4000/4000 [==============================] - 344s 86ms/step - loss: 0.0033 - val_loss: 0.1596
> Epoch 47/50
4000/4000 [==============================] - 321s 80ms/step - loss: 0.0033 - val_loss: 0.1650
> Epoch 48/50
4000/4000 [==============================] - 322s 80ms/step - loss: 0.0036 - val_loss: 0.1684
> Epoch 49/50
4000/4000 [==============================] - 319s 80ms/step - loss: 0.0027 - val_loss: 0.1751
> Epoch 50/50
4000/4000 [==============================] - 319s 80ms/step - loss: 0.0035 - val_loss: 0.1666
<keras.callbacks.History at 0x7f48213a3b38>
在具有 271 个条目的验证数据集上验证模型结果。
val_pred = model.predict(x_test,batch_size=batch)
labels = []
for i in range(len(val_pred)):
b = np.zeros_like(val_pred[i])
b[np.arange(len(val_pred[i])), val_pred[i].argmax(1)] = 1
labels.append(b)
print(val_pred.shape)
> (271, 30, 3)
Note
val_pred
张量的大小为(271 × 30 × 3)。
使用 F1 分数以及精确度和召回率检查模型性能。从 scikit-learn 库中导入所需的模块。
from sklearn.metrics import f1_score
from sklearn.metrics import precision_score
from sklearn.metrics import recall_score
定义变量以记录模型性能。
score =[]
f1 = []
precision =[]
recall =[]
point = []
我们可以将验证数据集中 F1 值超过 0.6 的所有实例列入候选名单。这将使我们对验证数据的性能有一个公平的概念,使用我们设定的基准。
for i in range(len(y_test)):
if(f1_score(labels[i],y_test[i],average='weighted')>.6):
point.append(i)
score.append(f1_score(labels[i],y_test[i],average='weighted'))
precision.append(precision_score(labels[i],y_test[i],average='weighted'))
recall.append(recall_score(labels[i],y_test[i],average='weighted'))
print(len(point)/len(labels)*100)
> 69.37
print(np.mean(score))
> 0.686
print(np.mean(precision))
> 0.975
print(np.mean(recall))
> 0.576
虽然产生的结果不太令人满意,但它确实达到了接近最先进的结果。这些限制可以通过构建更密集的网络、增加历元的数量和数据集的长度来克服。
使用 CPU 训练大型数据集需要太多时间。这就是为什么使用 GPU 几乎是不可避免的,并且对于快速训练深度学习模型非常重要。
训练 RNN 是一项有趣的运动。相同的算法可以扩展到许多其他练习,例如音乐生成、语音生成等。它还可以有效地扩展到现实生活中的应用,如视频字幕和语言翻译。
我们鼓励读者在这个层次上为不同的应用程序创建自己的模型。我们将在接下来的章节中涉及更多这样的例子。
后续步骤
本章介绍的结构是最重要的部分,也是任何 RNN 类型的核心,无论是暹罗网络、seq2seq 模型、注意机制还是迁移学习。(建议读者进一步研究这些概念,以便更好地理解广泛可用的网络、其结构的变化以及它们各自的用例。)
此外,如果您能够直观地理解三维向量的维数和乘法在 TensorFlow 和 NumPy 中是如何工作的,您就能够实现最复杂的模型。所以,重点应该是尽可能多地掌握基础知识。旨在通过注意力/权重增加复杂性的模型只是为了提高模型准确性而进行的一些迭代/思考。这些进一步的改进更像是黑客,无论多么成功,但仍然需要一个结构化的思考过程。同样,最好的办法是不断尝试不同类型的模型及其广泛的应用,以便很好地掌握概念。
四、开发聊天机器人
在本章中,我们将创建一个聊天机器人。我们将以一种渐进的方式做这件事,并且将聊天机器人分成两层。本章的第一节介绍了聊天机器人的概念,接下来的一节介绍了如何实现一个基于规则的聊天机器人系统。最后一节讨论了在公开可用的数据集上训练序列对序列(seq2seq)循环神经网络(RNN)模型。最终的聊天机器人将能够回答针对数据集领域提出的特定问题,该模型已在该数据集领域中进行了训练。我们希望你喜欢前面的章节,这一章也会让你参与到深度学习和自然语言处理(NLP)的实现中。
聊天机器人简介
事实上,我们都在使用聊天机器人,甚至不知道如何定义它,这使得聊天机器人的定义变得无关紧要。
在我们的日常生活中,我们都在使用各种各样的应用程序,如果有人在阅读这一章时没有听说过“聊天机器人”,那将是令人惊讶的聊天机器人就像任何其他应用程序一样。聊天机器人与普通应用程序的唯一区别在于它们的用户界面。聊天机器人有一个聊天界面,用户可以与应用程序进行文字聊天,而不是发送消息,并以对话的方式操作它,而不是由按钮和图标组成的视觉界面。我们希望这个定义现在是清晰的,你可以深入到聊天机器人的奇妙世界。
聊天机器人的起源
就像我们讨厌起源的想法一样,我们喜欢起源的想法。不要仅仅成为事实的记录者,而是要试着去洞察它们起源的奥秘。——伊凡·巴甫洛夫
不探究聊天机器人的起源就去讨论它们是没有用的。你可能会觉得有趣的是,1950 年,当世界从第二次世界大战的冲击中恢复过来时,英国学者艾伦·图灵有先见之明,发明了一种测试,看一个人能否区分人和机器。这就是所谓的图灵测试 ( https://en.wikipedia.org/wiki/Turing_test
)。
16 年后的 1966 年,约瑟夫·韦岑鲍姆发明了一种叫做伊莱扎的计算机程序。它只用了 200 行代码就模仿了一位心理治疗师的语言。你还可以在这里跟它对话: http://psych.fullerton.edu/mbirnbaum/psych101/Eliza.htm
。
机器学习的最新发展为聊天机器人提供了前所未有的动力,解释自然语言,以便随着时间的推移更好地理解和学习。脸书、苹果、谷歌(Alphabet)和微软等大公司正在投入大量资源,研究如何模仿消费者和机器之间的真实对话,以及商业上可行的商业模式。
但是聊天机器人是如何工作的呢?
好了,介绍够了。让我们言归正传。
- “嘿,怎么了?”
- “你过得怎么样?”
- “你好!”
这些句子似乎很熟悉。不是吗?它们都是某种问候某人的信息。我们如何回应这些问候?通常,我们会回答“我很好。你呢?”
这正是聊天机器人的工作方式。典型的聊天机器人会找到所提问题的所谓上下文,在这种情况下,就是“问候”然后,机器人获得适当的响应,并将其发送回用户。它如何找到适当的响应,它能处理图像、音频和视频等附件吗?我们将在接下来的部分中处理这个问题。
为什么聊天机器人是如此大的机会?
Forrester ( https://go.forrester.com/data/consumer-technographics/
)进行的研究指出,我们在移动设备上大约 85%的时间花在主要应用程序上,如电子邮件和消息平台。借助深度学习和 NLP 提供的巨大优势,几乎每家公司都在尝试构建应用程序,以保持潜在消费者对其产品和服务的兴趣,聊天机器人是实现这一目的的独特工具。通过安装聊天机器人,可以轻松避免由传统客户服务处理的多种人为错误和客户请求。此外,聊天机器人可以允许客户和相关公司访问所有以前的聊天/问题记录。
虽然聊天机器人可以被认为是与最终客户进行对话的应用程序,但是聊天机器人执行的任务和少数相关应用程序可以在更高的级别上进行分类,分为以下类别:
- 问答:每个用户一轮;当出现带标签的答案时很有用
- 产品查询用例
- 提取用户信息
- 句子补全:在对话的下一句话中补上缺失的单词
- 将正确的产品映射到客户
- 面向目标的对话:以实现目标为任务的对话
- 给客户的建议
- 与顾客协商价格
- 聊天对话:没有明确目标的对话,更多的是讨论。现在没有这样的用例需要关注
- 可视对话:包含文本、图像和音频的任务
- 与顾客交换图像,并在此基础上建立推论
好吧,你现在可能在想,“我很兴奋。我怎么能造一个呢?”
构建聊天机器人听起来很吓人。是真的吗?
构建聊天机器人的困难不在于技术,而在于用户体验。市场上最流行的成功的机器人之一是那些用户希望定期回来,并为他们的日常任务和需求提供一致价值的机器人。—Matt Hartman,Betaworks 的种子投资总监
在构建聊天机器人之前,如果我们提前解决以下四个问题,然后决定如何推进项目,那会更有意义:
- 我们要用机器人解决什么问题?
- 我们的机器人将生活在哪个平台上(脸书,Slack 等)。)?
- 我们将使用什么服务器来托管机器人? Heroku (
www.heroku.com
)还是我们自己? - 我们是想从头开始还是使用现有的聊天机器人平台工具(如下)?
- Botsify (
https://botsify.com/
) - Pandorabots (
https://playground.pandorabots.com/en/
- Chattypeople (
www.chattypeople.com/
- Wit.ai (
https://wit.ai/
- Api.ai (
https://api.ai/
- Botsify (
要更深入地了解不同平台的工作方法以及业务用例的最佳匹配,可以参考以下文档,这些文档来自一些流行的聊天机器人平台的链接:
- 脸书信使(
https://developers.facebook.com/products/messenger/
) - 懈怠(
https://api.slack.com/bot-users
) - 不和(
https://blog.discordapp.com/the-robot-revolution-has-unofficially-begun/
) - 电报
(
https://core.telegram.org/bots/api
) - Kik (
https://dev.kik.com/#/home
对话机器人
对于我们的对话聊天机器人的第一个版本,我们将制作一个基于规则的机器人,它将帮助开发人员定义他/她对最终用户提出的特定类别问题的期望答案。创建这样一个机器人将有助于我们对使用机器人有一个基本的了解,然后我们再进入下一个层次,使用文本生成机器人。
我们将使用 Facebook Messenger 作为我们想要的平台,使用 Heroku 作为我们想要的服务器,来发布 chatbot 的基本版本。重要的事情先来。你一定有一个脸书的页面。如果没有,请创建一个。要与机器人通信,必须访问该页面并选择消息选项,以启动对话。
按照图 4-1 中的步骤在脸书上创建页面:
-
选择“创建页面”选项。
-
Select the desired category of the organization and choose a name to create the page. We have selected Insurance as the field of the organization, as later on, we will build test cases around it and use an Insurance-related conversation dataset to train our model.
图 4-1
Creating a Facebook page
-
根据需要,为页面添加个人资料和封面照片。
在执行了前面的步骤后,最终的页面 Dl4nlp_cb, www.facebook.com/dlnlpcb/
,将如图 4-2 所示。
图 4-2
Dl4nlp_cb Facebook page
下一步是创建一个脸书应用程序。使用您的官方脸书帐户登录,访问以下网址创建一个: https://developers.facebook.com/apps/
。该应用程序将订阅创建的页面,并代表该页面处理所有响应(图 4-3 )。
图 4-3
Creating a Facebook app
我们为该应用程序分配了与之前创建的脸书页面相同的显示名称,并使用所需的电子邮件 ID 注册了该应用程序。发布应用程序创建。应用仪表板将如图 4-4 所示。
图 4-4
Facebook App Dashboard
脸书提供了一系列可以添加到新创建的应用程序中的产品。对于聊天机器人,我们需要选择 Messenger 作为选项(上图中第二行中间的选项)。单击设置按钮。这将把用户重定向到设置页面(图 4-5 ),在这里,除了选择教程之外,我们还可以创建令牌并设置 webhooks(见下文)。
图 4-5
Facebook app Settings page
从“设置”页面,转到“令牌生成”部分,并选择在第一步中创建的页面。将弹出一个警告框,要求授予权限。点击继续并继续(图 4-6 )。
图 4-6
Facebook Token Generation Note
人们可以查看脸书正在访问的有关该应用程序的信息。单击查看您提供的信息链接进行检查。
选择 Continue 选项后,您将获得另一个窗口,显示授予该页面的权限。用户可以选择要授予的权限。出于当前目的,建议不要更改特权部分中之前选择的任何选项(图 4-7 )。
图 4-7
Privilege grant section
单击“选择您允许的内容”将显示授予该页面的权限。检查完毕后,点击确定并进入其他步骤(图 4-8 )。
图 4-8
Permissions granted
这将在应用程序设置页面上开始生成令牌(生成令牌可能需要几秒钟)。参见图 4-9 。
图 4-9
Final page access token generation
页面访问令牌是一个长字符串,是数字和字母的组合,我们稍后将使用它来用 Heroku 创建应用程序。它将被设置为 Heroku 应用程序中的配置参数。
令牌在每次生成时都是唯一的,并且对于每个应用程序、页面和用户组合都是独立的。生成后会像图 4-10 中的样子。
图 4-10
Page access token
创建脸书页面和应用程序后,在 Heroku ( www.heroku.com
)上注册并打开一个帐户,并在这里创建一个应用程序,选择 Python 语言。
在 Heroku 上创建一个应用程序将为我们提供一个 webhook,脸书应用程序将向其发送请求,以防事件被触发,例如聊天机器人,无论何时接收或发送一些消息。
Note
确保 Heroku 使用的密码是字母、数字和符号的组合——全部三个,而不仅仅是两个。
创建帐户后,Heroku 仪表板将如图 4-11 所示。
图 4-11
Heroku dashboard
点击创建新应用程序,在 Heroku 上创建应用程序。关于 Python 语言的教程,可以点击 Python 按钮 https://devcenter.heroku.com/articles/getting-started-with-python#introduction
访问共享教程。目前,保持默认选择“美国”,对于 pipeline,在创建应用程序时不要做任何选择(图 4-12 )。
Note
应用程序的名称不能包含数字、下划线或符号。应用程序名称中只允许使用小写字母。
图 4-12
Heroku app creation
Heroku 应用仪表板将如图 4-13 所示,默认情况下,在应用创建后选择部署选项卡。
图 4-13
Heroku app dashboard
现在,我们都准备好使用脸书应用程序、page 和 Heroku 应用程序了。下一步是创建代码并将其导入 Heroku 应用程序。
从以下 URL,访问 GitHub 存储库,并将其克隆到您的个人 GitHub 帐户,以访问为我们的 chatbot 第一版上的测试用例提供的示例代码: https://github.com/palashgoyal1/DL4NLP
。该存储库包含您需要开始使用的四个重要文件。
的。gitignore file 告诉 Git 应该忽略哪些文件(或模式)。它有以下内容:
> *.pyc
> .*
Procfile 用于声明各种流程类型,在我们的例子中,是一个 web 应用程序。
> web: gunicorn app:app --log-file=-
Requirements.txt 安装 Python 依赖项。
> Flask==0.11.1
> Jinja2==2.8
> MarkupSafe==0.23
> Werkzeug==0.11.10
> click==6.6
> gunicorn==19.6.0
> itsdangerous==0.24
> requests==2.10.0
> wsgiref==0.1.2
> chatterbot>=0.4.6
> urllib
> clarifai==2.0.30
> enum34
App.py 是包含 chatbot 应用程序主要代码的 Python 文件。由于文件很大,我们已经把它放在前面提到的 GitHub 存储库中。请读者访问,以供参考。这样,克隆存储库也将变得更加容易。
让我们设置 webhook。(webhook 是一个 HTTP 回调——一个在某件事情发生时发生的 HTTP POST,比如一个通过 HTTP POST 的简单事件通知。)我们使用了 Heroku,因为它提供了一个 webhook,脸书使用它来发送请求,并在发生任何事件时检索适当的结果。
访问您在 Heroku 中创建的应用程序,然后转到 Deploy 选项卡。有四种方法可以通过 Heroku Git、GitHub、Dropbox 和 Container Registry 部署应用程序(图 4-14 )。为了简单起见,我们将使用 GitHub 部署我们的代码。
图 4-14
Heroku deploy app section
一旦我们选择了 Connect to GitHub,它将询问放置代码的 GitHub 存储库。确保这里提到的名称是正确的,并且主目录是存储库。选择正确的存储库后,点击连接按钮(图 4-15 )。
图 4-15
Heroku deploy app via GitHub
代码将使用您的个人 GitHub 存储库的链接来部署,这个特定的应用程序的代码已经被放置在那里。在 Heroku 的 Settings 标签中,你可以在 Domains 和 Certificates 部分找到应用程序的域名,它看起来与https://*******.herokuapp.com/
的格式相似。对于之前创建的测试应用程序,它是 https://dlnlpcbapp.herokuapp.com/
。把它单独记下来,因为我们以后会需要它。
现在是整合脸书页面 Dl4nlp_cb 和 Heroku app dlnlpcbapp 的时候了。访问脸书应用仪表板,在显示页面访问令牌的 Messenger 设置选项卡下,转到 webhooks 以设置 webhook(图 4-16 )。
图 4-16
Setting the webhook
弹出窗口将要求输入以下三个字段:
图 4-17
Setting the webhook—adding relevant information
- 回调 URL:我们之前设置的 Heroku URL(我们在步骤 1 中生成的设置 URL)
- 验证令牌:一个将被发送到您的 bot 的秘密值,以验证请求来自脸书。无论您在这里设置了什么值,请确保将其添加到您的 Heroku 环境中。
- 订阅字段:这告诉脸书你关心什么消息事件,并希望它通知你的 webhook。如果你不确定,检查所有的方框(图 4-17 )。
Note
“回拨验证失败”是最常见的报告错误之一,当脸书在尝试将 Heroku 端点添加到脸书聊天应用程序时返回错误消息(图 4-18 )时会遇到这种情况。
如果脸书发送的令牌与使用 Heroku 配置变量设置的令牌不匹配,Flask 应用程序会故意返回 403 禁止错误。
如果遇到图 4-18 所示的错误,这意味着 Heroku 配置值没有正确设置。在应用程序中从命令行运行heroku config
并验证名为VERIFY_TOKEN
的键被设置为等于在脸书窗口中键入的值,这将纠正错误。
回调 URL 框中显示的 URL 将是 Heroku 应用程序的 URL。
图 4-18
Error: “Callback verification failed”
webhook 的成功配置将带您进入另一个显示完成信息的屏幕(图 4-19 )。
图 4-19
Successful webhook configuration
配置好 webhook 后,选择所需的脸书页面并点击订阅(图 4-20 )。
图 4-20
Subscribe webhook to desired Facebook page Dl4nlp_cb
现在再次回到 Heroku 应用程序。在“设置”选项卡下,您会发现“配置变量选项”您必须设置两个变量:PAGE_ACCESS_TOKEN
(从前面的步骤中选择)和VERIFY_TOKEN
(从在应用仪表板中设置 webhook 时使用的变量中选择)。除了前面的两个参数外,还要从应用页面的基本设置中获取应用 ID 和 Api Secret token(图 4-21 )。这两个也必须在 Heroku 配置参数中设置(单击 Show 按钮获得 Api Secret 令牌)。
图 4-21
Configuring Heroku settings
现在打开 Heroku 应用中的设置选项卡,将应用 ID 设置为api_key
,应用秘密设置为api_secret
,同时设置PAGE_ACCESS_TOKEN
和VERIFY_TOKEN
(图 4-22 )。
图 4-22
Adding configuration variables in Heroku settings
保存配置参数后,转到 Heroku 上的 Deploy 选项卡,向下滚动到 Manual Deploy 部分,然后单击 Deploy Branch 按钮。这将部署从存储库中选择的当前分支,并进行必要的编译。通过检查日志部分,确保没有错误。
现在转到已创建的脸书页面,单击页面顶部“喜欢”按钮旁边的“消息”按钮。这将打开一个消息窗格,显示页面的消息框。开始和你定制的聊天机器人聊天吧(图 4-23 )!
图 4-23
Enjoy your conversations with the chatbot!
聊天机器人:自动文本生成
在上一节中,我们使用不同的平台和库构建了一个简单的对话聊天机器人。它的问题是它只能处理一组固定的问题。如果我们能建造一个从现有的对话中学习的机器人,会怎么样?这就是自然语言生成派上用场的地方。我们将制作一个 seq2seq 模型,它可以处理任何类型的问题,也就是说,即使问题是由一些随机的单词组成的。这个答案在语法和语境上是否正确是一个完全不同的问题,取决于各种因素,如数据集的大小和质量。
在本节中,我们将尝试构建一个模型,该模型将一组问题和答案作为输入,并在被问及与输入数据相关的问题时预测答案。如果该问题与用于训练模型的问题集相匹配,则会以最佳方式回答该问题。
我们将使用序列到序列模型来解决所描述的问题。我们使用的数据集由从保险领域的客户服务站记录的问答组成。该数据集是从网站 www.insurancelibrary.com/
收集的,是保险行业首次发布的此类问答语料库。这些问题属于客户就保险公司提供的多种服务和产品提出的一系列问题,答案由对保险行业有深入了解的专业人士给出。
用于训练的数据集取自 URL https://github.com/shuzi/insuranceQA
,目前位于 https://github.com/palashgoyal1/InsuranceQnA
,此外还有用于问题、答案和词汇的所需文件。该数据集被 IBM 的几位员工用于论文“将深度学习应用于答案选择:一项研究和一项开放任务”( https://arxiv.org/pdf/1508.01585v2.pdf
),他们使用了 CNN 框架的多个变体。在所有的变体中,他们都让模型学习了给定问题及其对应答案的单词嵌入,然后使用余弦距离作为相似性度量来衡量匹配程度。
图 4-24 是本文中演示的多种架构的快照。对于架构 II、III 和 IV,问答部分对于隐藏层和 CNN 层具有相同的权重。CNN Q 和 CNN A 层分别用于提取问答双方的特征。
图 4-24
Architectures used in the research paper
GitHub 存储库中的原始数据集结合了问题的训练、验证和测试分区。我们结合了给定的问题和答案,并在最终选择用于建模目的的 qna 之前执行了一些处理步骤。此外,一组序列到序列模型已被用于生成用户所提问题的答案。如果使用适当的模型进行训练,并且经过足够的迭代,该模型也将能够回答以前看不到的问题。
为了准备模型要使用的数据,我们做了一些更改,并完成了对初始给定数据集的选择。随后,我们利用整个数据集的词汇,以及问答中使用的单词标记,以英语可理解的格式创建问题及其相应答案的完美组合。
Note
在开始执行代码之前,请确保您已经安装了 TensorFlow 1 . 0 . 0 版,并且没有安装其他版本,因为 tensor flow 的后续更新版本中已经发生了变化。
以编码格式导入所需的包和数据集。
import pandas as pd
import numpy as np
import tensorflow as tf
import re
import time
tf.__version__
> '1.0.0'
# Make sure the vocabulary.txt file and the encoded datasets for Question and Answer are present in the same folder
# reading vocabulary
lines = open('vocabulary.txt', encoding='utf-8', errors="ignore").read().split('\n')
# reading questions
conv_lines = open('InsuranceQAquestionanslabelraw.encoded', encoding='utf-8', errors="ignore").read().split('\n')
# reading answers
conv_lines1 = open('InsuranceQAlabel2answerraw.encoded', encoding='utf-8', errors="ignore").read().split('\n')
# The print command shows the token value associated with each of the words in the 3 datasets
print(" -- Vocabulary -- ")
print(lines[:2])
> -- Vocabulary –
> ['idx_17904\trating/result', 'idx_14300\tconsidered,']
print(" -- Questions -- ")
print(conv_lines[:2])
> -- Questions –
> ['medicare-insurance\tidx_1285 idx_1010 idx_467 idx_47610 idx_18488 idx_65760\t16696', 'long-term-care-insurance\tidx_3815 idx_604 idx_605 idx_891 idx_136 idx_5293 idx_65761\t10277']
print(" -- Answers -- ")
print(conv_lines1[:2])
> -- Answers –
> ['1\tidx_1 idx_2 idx_3 idx_4 idx_5 idx_6 idx_7 idx_8 idx_9 idx_10 idx_11 idx_12 idx_13 idx_14 idx_3 idx_12 idx_15 idx_16 idx_17 idx_8 idx_18 idx_19 idx_20 idx_21 idx_3 idx_12 idx_14 idx_22 idx_20 idx_23 idx_24 idx_25 idx_26 idx_27 idx_28 idx_29 idx_8 idx_30 idx_19 idx_11 idx_4 idx_31 idx_32 idx_22 idx_33 idx_34 idx_35 idx_36 idx_37 idx_30 idx_38 idx_39 idx_11 idx_40 idx_41 idx_42 idx_43 idx_44 idx_22 idx_45 idx_46 ...
在接下来的几行中,我们根据分配给问题和答案的 ID,将问题与其对应的答案组合在一起。
id2line = {}
for line in vocab_lines:
_line = line.split('\t')
if len(_line) == 2:
id2line[_line[0]] = _line[1]
# Creating the word tokens for both questions and answers, along with the mapping of the answers enlisted for questions
convs, ansid = [], []
for line in question_lines[:-1]:
_line = line.split('\t')
ansid.append(_line[2].split(' '))
convs.append(_line[1])
convs1 = [ ]
for line in answer_lines[:-1]:
_line = line.split('\t')
convs1.append(_line[1])
print(convs[:2]) # word tokens present in the question
> ['idx_1285 idx_1010 idx_467 idx_47610 idx_18488 idx_65760', 'idx_3815 idx_604 idx_605 idx_891 idx_136 idx_5293 idx_65761']
print(ansid[:2]) # answers IDs mapped to the questions
> [['16696'], ['10277']]
print(convs1[:2]) # word tokens present in the answer
> ['idx_1 idx_2 idx_3 idx_4 idx_5 idx_6 idx_7 idx_8 idx_9 idx_10 idx_11 idx_12 idx_13 idx_14 idx_3 idx_12 idx_15 idx_16 idx_17 idx_8 idx_18 idx_19 idx_20 idx_21 ...
# Creating matching pair between questions and answers on the basis of the ID allocated to each.
questions, answers = [], []
for a in range(len(ansid)):
for b in range(len(ansid[a])):
questions.append(convs[a])
for a in range(len(ansid)):
for b in range(len(ansid[a])):
answers.append(convs1[int(ansid[a][b])-1])
ques, ans =[], []
m=0
while m<len(questions):
i=0
a=[]
while i < (len(questions[m].split(' '))):
a.append(id2line[questions[m].split(' ')[i]])
i=i+1
ques.append(' '.join(a))
m=m+1
n=0
while n<len(answers):
j=0
b=[]
while j < (len(answers[n].split(' '))):
b.append(id2line[answers[n].split(' ')[j]])
j=j+1
ans.append(' '.join(b))
n=n+1
保险 QnA 数据集中前五个问题的以下输出将给出客户所提问题的类型以及专业人员给出的相应答案。在本练习的最后,我们的模型将尝试以类似于提问的方式提供答案。
# Printing top 5 questions along with their answers
limit = 0
for i in range(limit, limit+5):
print(ques[i])
print(ans[i])
print("---")
> What Does Medicare IME Stand For?
According to the Centers for Medicare and Medicaid Services website, cms.gov, IME stands for Indirect Medical Education and is in regards to payment calculation adjustments for a Medicare discharge of higher cost patients receiving care from teaching hospitals relative to non-teaching hospitals. I would recommend contacting CMS to get more information about IME
---
> Is Long Term Care Insurance Tax Free?
As a rule, if you buy a tax qualified long term care insurance policy (as nearly all are, these days), and if you are paying the premium yourself, there are tax advantages you will receive. If you are self employed, the entire premium is tax deductible. If working somewhere but paying your own premium for an individual or group policy, you can deduct the premium as a medical expense under the same IRS rules as apply to all medical expenses. In both situations, you also receive the benefits from the policy tax free, if they are ever needed.
---
> Can Husband Drop Wife From Health Insurance?
Can a spouse drop another spouse from health insurance? Usually not without the spouse's who is being dropped consent in writting. Most employers who have a quality HR department will require a paper trial for any changes in an employee's benefit plan. When changes are attempted that could come back to haunt the employer, steps are usually taken to comfirm something like this.
---
> Is Medicare Run By The Government?
Medicare Part A and Part B is provided by the Federal government for Americans who are 65 and older who have worked and paid Social Security taxes into the system. Medicare is also available to people under the age of 65 that have certain disabilities and people with End-Stage Renal Disease (ESRD).
---
> Is Medicare Run By The Government?
Definitely. It is ran by the Center for Medicare and Medicaid Services, a Government Agency given the responsibility of overseeing and administering Medicare and Medicaid. Even Medicare Advantage Plans, which are administered by private insurance companies are strongly regulated by CMMS. They work along with Social Security and Jobs and Family Services to insure that your benefits are available and properly administered.
---
虽然前面示例中的第四个和第五个问题是相同的,但它们有不同的答案,这取决于有多少专业人士回答了该问题。
# Checking the count of the total number of questions and answers
print(len(questions))
> 27987
print(len(answers))
> 27987
通过用实际的扩展单词替换单词的短形式来创建文本清理功能,以便单词可以在以后被它们的实际标记替换。
def clean_text(text):
"""Cleaning the text by replacing the abbreviated words with their proper full replacement, and converting all the characters to lower case"""
text = text.lower()
text = re.sub(r"i'm", "i am", text)
text = re.sub(r"he's", "he is", text)
text = re.sub(r"she's", "she is", text)
text = re.sub(r"it's", "it is", text)
text = re.sub(r"that's", "that is", text)
text = re.sub(r"what's", "that is", text)
text = re.sub(r"where's", "where is", text)
text = re.sub(r"how's", "how is", text)
text = re.sub(r"\'ll", " will", text)
text = re.sub(r"\'ve", " have", text)
text = re.sub(r"\'re", " are", text)
text = re.sub(r"\'d", " would", text)
text = re.sub(r"\'re", " are", text)
text = re.sub(r"won't", "will not", text)
text = re.sub(r"can't", "cannot", text)
text = re.sub(r"n't", " not", text)
text = re.sub(r"n'", "ng", text)
text = re.sub(r"'bout", "about", text)
text = re.sub(r"'til", "until", text)
text = re.sub(r"[-()\"#/@;:<>{}`+=~|.!?,']", "", text)
return text
# Applying the 'clean_text()' function on the set of Questions and Answers
clean_questions = []
for question in ques:
clean_questions.append(clean_text(question))
clean_answers = []
for answer in ans:
clean_answers.append(clean_text(answer))
看看在对问题和答案执行清理操作后数据集是如何出现的。这个清理后的数据集将作为输入提供给我们的模型,以确保提供给模型的输入在结构和格式上彼此同步:
limit = 0
for i in range(limit, limit+5):
print(clean_questions[i])
print(clean_answers[i])
print()
> what does medicare ime stand for
according to the centers for medicare and medicaid services website cmsgov ime stands for indirect medical education and is in regards to payment calculation adjustments for a medicare discharge of higher cost patients receiving care from teaching hospitals relative to nonteaching hospitals i would recommend contacting cms to get more information about ime
----
> is long term care insurance tax free
as a rule if you buy a tax qualified long term care insurance policy as nearly all are these days and if you are paying the premium yourself there are tax advantages you will receive if you are self employed the entire premium is tax deductible if working somewhere but paying your own premium for an individual or group policy you can deduct the premium as a medical expense under the same irs rules as apply to all medical expenses in both situations you also receive the benefits from the policy tax free if they are ever needed
----
> can husband drop wife from health insurance
can a spouse drop another spouse from health insurance usually not without the spouses who is being dropped consent in writting most employers who have a quality hr department will require a paper trial for any changes in an employees benefit plan when changes are attempted that could come back to haunt the employer steps are usually taken to comfirm something like this
----
> is medicare run by the government
medicare part a and part b is provided by the federal government for americans who are 65 and older who have worked and paid social security taxes into the system medicare is also available to people under the age of 65 that have certain disabilities and people with endstage renal disease esrd
----
> is medicare run by the government
definitely it is ran by the center for medicare and medicaid services a government agency given the responsibility of overseeing and administering medicare and medicaid even medicare advantage plans which are administered by private insurance companies are strongly regulated by cmms they work along with social security and jobs and family services to insure that your benefits are available and properly administered
----
根据两个问题和答案中出现的单词数分析问题和答案,并检查不同区间的百分位数。
lengths.describe(percentiles=[0,0.25,0.5,0.75,0.85,0.9,0.95,0.99])
> counts
count 55974.000000
mean 54.176725
std 67.638972
min 2.000000
0% 2.000000
25% 7.000000
50% 30.000000
75% 78.000000
85% 103.000000
90% 126.000000
95% 173.000000
99% 314.000000
max 1176.000000
由于提供给模型的数据需要所提问题的完整答案,而不是半生不熟的答案,因此我们必须确保我们为模型训练选择的问答组合在问题和答案中都有足够数量的单词,从而对单词数设置了最低限制。同时,我们希望该模型能够对问题产生简明扼要的答案,因此我们也对问题和答案中的字数设置了最大限制。
在这里,我们只列出最少两个单词、最多 100 个单词的文本。
# Remove questions and answers that are shorter than 1 words and longer than 100 words.
min_line_length, max_line_length = 2, 100
# Filter out the questions that are too short/long
short_questions_temp, short_answers_temp = [], []
i = 0
for question in clean_questions:
if len(question.split()) >= min_line_length and len(question.split()) <= max_line_length:
short_questions_temp.append(question)
short_answers_temp.append(clean_answers[i])
i += 1
# Filter out the answers that are too short/long
short_questions, short_answers = [], []
i = 0
for answer in short_answers_temp:
if len(answer.split()) >= min_line_length and len(answer.split()) <= max_line_length:
short_answers.append(answer)
short_questions.append(short_questions_temp[i])
i += 1
执行上述选择后的数据集统计如下:
print("# of questions:", len(short_questions))
> # of questions: 19108
print("# of answers:", len(short_answers))
> # of answers: 19108
print("% of data used: {}%".format(round(len(short_questions)/len(questions),4)*100))
> % of data used: 68.27%
直接输入文本的问题是模型不能处理可变长度的序列,下一个大问题是词汇量。解码器必须对大词汇量运行 softmax,比如说,对输出中的每个单词运行 20,000 个单词。这会减慢训练过程。那么,我们如何处理这个问题呢?填充。
填充是将可变长度序列转换为固定长度序列的一种方式。假设我们想要这个句子“你好吗?”为固定长度,比如说 10,在应用填充后,这一对被转换为[PAD,PAD,PAD,PAD,PAD,PAD,"?"、“你”、“是”、“怎么样”】。
def pad_sentence_batch(sentence_batch, vocab_to_int):
"""Including <PAD> token in sentence to make all batches of same length"""
max_sentence = max([len(sentence) for sentence in sentence_batch])
return [sentence + [vocab_to_int['<PAD>']] * (max_sentence - len(sentence)) for sentence in sentence_batch]
下面的代码映射新形成的训练数据集的词汇表中的单词,并为每个单词分配一个频率标记。
# Create a dictionary for the frequency of the vocabulary
vocab = {}
for question in short_questions:
for word in question.split():
if word not in vocab:
vocab[word] = 1
else:
vocab[word] += 1
for answer in short_answers:
for word in answer.split():
if word not in vocab:
vocab[word] = 1
else:
vocab[word] += 1
与第二章中执行的操作一样,我们将删除训练数据集中出现频率较低的单词,因为这些单词不会向模型引入任何重要信息。
# Remove rare words from the vocabulary.
threshold = 1
count = 0
for k,v in vocab.items():
if v >= threshold:
count += 1
print("Size of total vocab:", len(vocab))
> Size of total vocab: 18983
print("Size of vocab we will use:", count)
> Size of vocab we will use: 18983
# Create dictionaries to provide a unique integer for each word.
questions_vocab_to_int = {}
word_num = 0
for word, count in vocab.items():
if count >= threshold:
questions_vocab_to_int[word] = word_num
word_num += 1
answers_vocab_to_int = {}
word_num = 0
for word, count in vocab.items():
if count >= threshold:
answers_vocab_to_int[word] = word_num
word_num += 1
由于解码器生成了多个单词或定制符号,我们必须将新的标记添加到训练数据集的当前词汇中,并且也将这些标记包括在当前词典中。关于所包含的四个令牌的基本信息如下:
GO
:与<start>
令牌相同。它是馈送给解码器的第一个标记,与思想向量一起,开始为答案生成标记。EOS
:“句子结束”,与表示句子结束或回答完成的<end>
标记相同。我们不能用标点符号来代替它,因为它们在上下文中有完全不同的含义。解码器一生成EOS
令牌,它就表示答案的完成。UNK
:“未知”令牌。如果没有对单词的最小出现次数进行额外的检查/筛选,这用于替换词汇表中频率低得多的单词。例如,输入的句子Insurance is highly criticalll1090
将被转换为Insurance is highly <UNK>
。PAD
:由于训练数据是等长批量处理的,一批中的所有序列也是等长的,所以输入的句子将在句子所需的两边用PAD
标记填充。例如,对于允许最大长度的情况,输入句子Insurance is highly criticalll1090
将被转换为Insurance is highly criticalll1090 <PAD> <PAD> <PAD> <PAD>
。
图 4-25 显示用户自定义令牌在模型响应中的用法(来源: http://colah.github.io/
)。添加这些令牌的代码如下。
图 4-25
Sample encoder-decoder with usage of tokens
# Adding unique tokens to the present vocabulary
codes = ['<PAD>','<EOS>','<UNK>','<GO>']
for code in codes:
questions_vocab_to_int[code] = len(questions_vocab_to_int)+1
for code in codes:
answers_vocab_to_int[code] = len(answers_vocab_to_int)+1
# Creating dictionary so as to map the integers to their respective words, inverse of vocab_to_int
questions_int_to_vocab = {v_i: v for v, v_i in questions_vocab_to_int.items()}
answers_int_to_vocab = {v_i: v for v, v_i in answers_vocab_to_int.items()}
print(len(questions_vocab_to_int))
> 18987
print(len(questions_int_to_vocab))
> 18987
print(len(answers_vocab_to_int))
> 18987
print(len(answers_int_to_vocab))
> 18987
我们试图减少有效词汇表的大小,这将加快训练和测试步骤,方法是简单地将它限制在一个很小的数目,并用一个UNK
标签替换词汇表之外的单词。现在,训练和测试时间都可以显著减少,但这显然并不理想,因为我们可能会生成带有大量UNK
的输出,但现在,我们确保这些令牌的百分比足够低,我们不会面临任何严重的问题。
此外,在我们将数据输入模型之前,我们必须将句子中的每个单词转换为唯一的整数。这可以通过建立一个包含所有单词的词汇表并给它们分配唯一的编号来实现(一键编码向量)。
# Convert the text to integers, and replacing any of the words not present in the respective vocabulary with <UNK> token
questions_int = []
for question in short_questions:
ints = []
for word in question.split():
if word not in questions_vocab_to_int:
ints.append(questions_vocab_to_int['<UNK>'])
else:
ints.append(questions_vocab_to_int[word])
questions_int.append(ints)
answers_int = []
for answer in short_answers:
ints = []
for word in answer.split():
if word not in answers_vocab_to_int:
ints.append(answers_vocab_to_int['<UNK>'])
else:
ints.append(answers_vocab_to_int[word])
answers_int.append(ints)
进一步检查被替换为<UNK>
标记的单词数。由于我们已经完成了预处理步骤,删除了词汇表中出现频率较低的单词,因此没有一个单词会被替换为<UNK>
标记。但是,建议将它们包含在通用脚本中。
# Calculate what percentage of all words have been replaced with <UNK>
word_count = 0
unk_count = 0
for question in questions_int:
for word in question:
if word == questions_vocab_to_int["<UNK>"]:
unk_count += 1
word_count += 1
for answer in answers_int:
for word in answer:
if word == answers_vocab_to_int["<UNK>"]:
unk_count += 1
word_count += 1
unk_ratio = round(unk_count/word_count,4)*100
print("Total number of words:", word_count)
> Total number of words: 1450824
print("Number of times <UNK> is used:", unk_count)
> Number of times <UNK> is used: 0
print("Percent of words that are <UNK>: {}%".format(round(unk_ratio,3)))
> Percent of words that are <UNK>: 0.0%
根据问题中的单词数创建问题和答案的有序集合。以这种方式对文本进行排序将有助于我们稍后使用的填充方法。
# Next, sorting the questions and answers on basis of the length of the questions.
# This exercise will reduce the amount of padding being done during the training process.
# This will speed up the training process and reduce the training loss.
sorted_questions = []
short_questions1 = []
sorted_answers = []
short_answers1= []
for length in range(1, max_line_length+1):
for i in enumerate(questions_int):
if len(i[1]) == length:
sorted_questions.append(questions_int[i[0]])
short_questions1.append(short_questions[i[0]])
sorted_answers.append(answers_int[i[0]])
short_answers1.append(short_answers[i[0]])
print(len(sorted_questions))
> 19108
print(len(sorted_answers))
> 19108
print(len(short_questions1))
> 19108
print(len(short_answers1))
> 19108
print()
for i in range(3):
print(sorted_questions[i])
print(sorted_answers[i])
print(short_questions1[i])
print(short_answers1[i])
print()
> [219, 13]
[219, 13, 58, 2310, 3636, 1384, 3365... ]
why can
why can a simple question but yet so complex why can someone do this or why can someone do that i have often pondered for hours to come up with the answer and i believe after years of thoughtprovoking consultation with friends and relativesi have the answer to the question why can the answer why not
[133, 479, 56]
[242, 4123, 3646, 282, 306, 56, ... ]
who governs annuities
if youre asking about all annuities then here are two governing bodies for variable annuities finra and the department of insurance variable products like variable annuities are registered products and come under the oversight of finras jurisdiction but because it is an annuity insurance product as well it falls under the department of insurance non finra annuities are governed by the department of insurance in each state
[0, 201, 56]
[29, 202, 6, 29, 10, 3602, 58, 36, ... ]
what are annuities
an annuity is an insurance product a life insurance policy protects you from dying too soon an annuity protects you from living too long annuities are complex basically in exchange for a sum of money either immediate or in installments the company will pay the annuitant a specific amount normally monthly for the life of the annuitant there are many modifications of this basic form annuities are taxed differently from other programs
从排序对中检查一个随机问题答案。
print(sorted_questions[1547])
> [37, 6, 36, 10, 466]
print(short_questions1[1547])
> how is life insurance used
print(sorted_answers[1547])
> [8, 36, 10, 6, 466, 26, 626, 58, 199, 200, 1130, 58, 3512, 31, 105, 208, 601, 10, 6, 466, 26, 626, ...
print(short_answers1[1547])
> term life insurance is used to provide a death benefit during a specified period of time permanent insurance is used to provide a death benefit at any time the policy is in force in order to accomplish this and have level premiums policies accumulate extra funds these funds are designed to allow the policy to meet its lifelong obligations however these funds accumulate tax free and give the policy the potential of solving many problems from funding education to providing long term care
现在是时候定义 seq2seq 模型将使用的助手函数了。其中一些函数来自 GitHub 代码库( https://github.com/Currie32/Chatbot-from-Movie-Dialogue
),它有类似的应用。
定义函数来为我们的模型输入创建占位符。
def model_inputs():
input_data = tf.placeholder(tf.int32, [None, None], name="input")
targets = tf.placeholder(tf.int32, [None, None], name="targets")
lr = tf.placeholder(tf.float32, name="learning_rate")
keep_prob = tf.placeholder(tf.float32, name="keep_prob")
return input_data, targets, lr, keep_prob
删除每个批次中的最后一个单词 ID,并在每个批次的开头添加<GO>
标记。
def process_encoding_input(target_data, vocab_to_int, batch_size):
ending = tf.strided_slice(target_data, [0, 0], [batch_size, -1], [1, 1])
dec_input = tf.concat([tf.fill([batch_size, 1], vocab_to_int['<GO>']), ending], 1)
return dec_input
正常的 RNN 处理过去的状态(将它们保存在记忆中),但是如果你想以某种方式将未来也包含在上下文中呢?通过使用双向 RNNs,我们可以将两个方向相反的隐藏层连接到同一个输出。通过这种结构,输出层可以从过去和未来的状态中获取信息。
因此,我们用 LSTM 单元和双向编码器定义了 seq2seq 模型的编码层。编码器层的状态,即权重,被作为解码层的输入。
def encoding_layer(rnn_inputs, rnn_size, num_layers, keep_prob, sequence_length):
lstm = tf.contrib.rnn.BasicLSTMCell(rnn_size)
drop = tf.contrib.rnn.DropoutWrapper(lstm, input_keep_prob = keep_prob)
enc_cell = tf.contrib.rnn.MultiRNNCell([drop] * num_layers)
_, enc_state = tf.nn.bidirectional_dynamic_rnn(cell_fw = enc_cell, cell_bw = enc_cell, sequence_length = sequence_length, inputs = rnn_inputs, dtype=tf.float32)
return enc_state
已经使用了第三章中解释的注意机制。这将大大减少产生的损失。注意状态被设置为 0,以最大化模型性能,并且对于注意机制,使用较便宜的 Bahdanau 注意。关于 Luong 和 Bahdanau 注意力技术的比较,请参考论文“基于注意力的神经机器翻译的有效方法”( https://arxiv.org/pdf/1508.04025.pdf
)。
def decoding_layer_train(encoder_state, dec_cell, dec_embed_input, sequence_length, decoding_scope, output_fn, keep_prob, batch_size):
attention_states = tf.zeros([batch_size, 1, dec_cell.output_size])
att_keys, att_vals, att_score_fn, att_construct_fn = tf.contrib.seq2seq.prepare_attention(attention_states, attention_option="bahdanau", num_units=dec_cell.output_size)
train_decoder_fn = tf.contrib.seq2seq.attention_decoder_fn_train(encoder_state[0], att_keys, att_vals, att_score_fn, att_construct_fn, name = "attn_dec_train")
train_pred, _, _ = tf.contrib.seq2seq.dynamic_rnn_decoder(dec_cell, train_decoder_fn, dec_embed_input, sequence_length, scope=decoding_scope)
train_pred_drop = tf.nn.dropout(train_pred, keep_prob)
return output_fn(train_pred_drop)
decoding_layer_infer()
函数为查询的问题创建正确的响应。该函数利用额外的注意力参数来预测答案中的单词,并且它不像在最终评分阶段那样与任何漏失相关联。这里,在生成答案时,不考虑退出,以便利用网络中存在的所有神经元。
def decoding_layer_infer(encoder_state, dec_cell, dec_embeddings, start_of_sequence_id, end_of_sequence_id,
maximum_length, vocab_size, decoding_scope, output_fn, keep_prob, batch_size):
attention_states = tf.zeros([batch_size, 1, dec_cell.output_size])
att_keys, att_vals, att_score_fn, att_construct_fn = tf.contrib.seq2seq.prepare_attention(attention_states, attention_option="bahdanau", num_units=dec_cell.output_size)
infer_decoder_fn = tf.contrib.seq2seq.attention_decoder_fn_inference(output_fn, encoder_state[0], att_keys, att_vals, att_score_fn, att_construct_fn,
dec_embeddings, start_of_sequence_id, end_of_sequence_id, maximum_length, vocab_size, name = "attn_dec_inf")
infer_logits, _, _ = tf.contrib.seq2seq.dynamic_rnn_decoder(dec_cell, infer_decoder_fn, scope=decoding_scope)
return infer_logits
decoding_layer()
函数使用截尾正态分布创建推理和训练逻辑,并用给定的标准偏差初始化权重和偏差。
def decoding_layer(dec_embed_input, dec_embeddings, encoder_state, vocab_size, sequence_length, rnn_size,
num_layers, vocab_to_int, keep_prob, batch_size):
with tf.variable_scope("decoding") as decoding_scope:
lstm = tf.contrib.rnn.BasicLSTMCell(rnn_size)
drop = tf.contrib.rnn.DropoutWrapper(lstm, input_keep_prob = keep_prob)
dec_cell = tf.contrib.rnn.MultiRNNCell([drop] * num_layers)
weights = tf.truncated_normal_initializer(stddev=0.1)
biases = tf.zeros_initializer()
output_fn = lambda x: tf.contrib.layers.fully_connected(x, vocab_size, None, scope=decoding_scope, weights_initializer = weights, biases_initializer = biases)
train_logits = decoding_layer_train(encoder_state, dec_cell, dec_embed_input, sequence_length, decoding_scope, output_fn, keep_prob, batch_size)
decoding_scope.reuse_variables()
infer_logits = decoding_layer_infer(encoder_state, dec_cell, dec_embeddings, vocab_to_int['<GO>'], vocab_to_int['<EOS>'],
sequence_length - 1, vocab_size, decoding_scope, output_fn, keep_prob, batch_size)
return train_logits, infer_logits
seq2seq_model()
函数用于将所有之前定义的函数放在一起,并使用随机均匀分布初始化嵌入。该函数将在最终图形中用于计算训练和推理逻辑。
def seq2seq_model(input_data, target_data, keep_prob, batch_size, sequence_length, answers_vocab_size,
questions_vocab_size, enc_embedding_size, dec_embedding_size, rnn_size, num_layers,
questions_vocab_to_int):
enc_embed_input = tf.contrib.layers.embed_sequence(input_data, answers_vocab_size+1, enc_embedding_size, initializer = tf.random_uniform_initializer(0,1))
enc_state = encoding_layer(enc_embed_input, rnn_size, num_layers, keep_prob, sequence_length)
dec_input = process_encoding_input(target_data, questions_vocab_to_int, batch_size)
dec_embeddings = tf.Variable(tf.random_uniform([questions_vocab_size+1, dec_embedding_size], 0, 1))
dec_embed_input = tf.nn.embedding_lookup(dec_embeddings, dec_input)
train_logits, infer_logits = decoding_layer(dec_embed_input, dec_embeddings, enc_state, questions_vocab_size,
sequence_length, rnn_size, num_layers, questions_vocab_to_int, keep_prob, batch_size)
return train_logits, infer_logits
当训练实例总数(N)较大时,少量训练实例(B <
Note
使用整个训练数据一次需要 n (=N/B)次迭代。这构成了一个时代。因此,参数更新的总次数是(N/B)*E,其中 E 是历元数。
最后,我们定义了我们的 seq2seq 模型,它将接受编码和解码部分,并同时训练它们。现在,设置以下模型参数并启动会话进行优化。
- 历元:单次通过整个训练集
- 批量大小:输入中同时出现的句子数量
- Rnn_size:隐藏层中的节点数
- Num_layers:隐藏层数
- 嵌入大小:嵌入尺寸
- 学习率:一个网络抛弃旧的信念去追求新的信念的速度有多快
- 保持概率:用于控制掉线。辍学是一个简单的技术,以防止过度拟合。它本质上通过使它们为零来丢弃层中的一些单位激活。
# Setting the model parameters
epochs = 50
batch_size = 64
rnn_size = 512
num_layers = 2
encoding_embedding_size = 512
decoding_embedding_size = 512
learning_rate = 0.005
learning_rate_decay = 0.9
min_learning_rate = 0.0001
keep_probability = 0.75
tf.reset_default_graph()
# Starting the session
sess = tf.InteractiveSession()
# Loading the model inputs
input_data, targets, lr, keep_prob = model_inputs()
# Sequence length is max_line_length for each batch
sequence_length = tf.placeholder_with_default(max_line_length, None, name="sequence_length")
# Finding shape of the input data for sequence_loss
input_shape = tf.shape(input_data)
# Create the training and inference logits
train_logits, inference_logits = seq2seq_model( tf.reverse(input_data, [-1]), targets, keep_prob, batch_size, sequence_length, len(answers_vocab_to_int),
len(questions_vocab_to_int), encoding_embedding_size, decoding_embedding_size, rnn_size, num_layers, questions_vocab_to_int)
# Create inference logits tensor
tf.identity(inference_logits, 'logits')
with tf.name_scope("optimization"):
# Calculating Loss function
cost = tf.contrib.seq2seq.sequence_loss( train_logits, targets, tf.ones([input_shape[0], sequence_length]))
# Using Adam Optimizer
optimizer = tf.train.AdamOptimizer(learning_rate)
# Performing Gradient Clipping to handle the vanishing gradient problem
gradients = optimizer.compute_gradients(cost)
capped_gradients = [(tf.clip_by_value(grad, -5., 5.), var) for grad, var in gradients if grad is not None]
train_op = optimizer.apply_gradients(capped_gradients)
batch_data()
函数有助于为问题和答案创建批处理。
def batch_data(questions, answers, batch_size):
for batch_i in range(0, len(questions)//batch_size):
start_i = batch_i * batch_size
questions_batch = questions[start_i:start_i + batch_size]
answers_batch = answers[start_i:start_i + batch_size]
pad_questions_batch = np.array(pad_sentence_batch(questions_batch, questions_vocab_to_int))
pad_answers_batch = np.array(pad_sentence_batch(answers_batch, answers_vocab_to_int))
yield pad_questions_batch, pad_answers_batch
保留总数据集的 15%用于验证,其余 85%用于训练模型。
# Creating train and validation datasets for both questions and answers, with 15% to validation
train_valid_split = int(len(sorted_questions)*0.15)
train_questions = sorted_questions[train_valid_split:]
train_answers = sorted_answers[train_valid_split:]
valid_questions = sorted_questions[:train_valid_split]
valid_answers = sorted_answers[:train_valid_split]
print(len(train_questions))
print(len(valid_questions))
设置训练参数并初始化声明的变量。
display_step = 20 # Check training loss after every 20 batches
stop_early = 0
stop = 5 # If the validation loss decreases after 5 consecutive checks, stop training
validation_check = ((len(train_questions))//batch_size//2)-1 # Counter for checking validation loss
total_train_loss = 0 # Record the training loss for each display step
summary_valid_loss = [] # Record the validation loss for saving improvements in the model
checkpoint= "./best_model.ckpt" # creating the checkpoint file in the current directory
sess.run(tf.global_variables_initializer())
训练模型。
for epoch_i in range(1, epochs+1):
for batch_i, (questions_batch, answers_batch) in enumerate(
batch_data(train_questions, train_answers, batch_size)):
start_time = time.time()
_, loss = sess.run(
[train_op, cost],
{input_data: questions_batch, targets: answers_batch, lr: learning_rate,
sequence_length: answers_batch.shape[1], keep_prob: keep_probability})
total_train_loss += loss
end_time = time.time()
batch_time = end_time - start_time
if batch_i % display_step == 0:
print('Epoch {:>3}/{} Batch {:>4}/{} - Loss: {:>6.3f}, Seconds: {:>4.2f}'
.format(epoch_i, epochs, batch_i,
len(train_questions) // batch_size, total_train_loss / display_step,
batch_time*display_step))
total_train_loss = 0
if batch_i % validation_check == 0 and batch_i > 0:
total_valid_loss = 0
start_time = time.time()
for batch_ii, (questions_batch, answers_batch) in enumerate(batch_data(valid_questions, valid_answers, batch_size)):
valid_loss = sess.run(
cost, {input_data: questions_batch, targets: answers_batch, lr: learning_rate,
sequence_length: answers_batch.shape[1], keep_prob: 1})
total_valid_loss += valid_loss
end_time = time.time()
batch_time = end_time - start_time
avg_valid_loss = total_valid_loss / (len(valid_questions) / batch_size)
print('Valid Loss: {:>6.3f}, Seconds: {:>5.2f}'.format(avg_valid_loss, batch_time))
# Reduce learning rate, but not below its minimum value
learning_rate *= learning_rate_decay
if learning_rate < min_learning_rate:
learning_rate = min_learning_rate
summary_valid_loss.append(avg_valid_loss)
if avg_valid_loss <= min(summary_valid_loss):
print('New Record!')
stop_early = 0
saver = tf.train.Saver()
saver.save(sess, checkpoint)
else:
print("No Improvement.")
stop_early += 1
if stop_early == stop:
break
if stop_early == stop:
print("Stopping Training.")
break
> Epoch 1/50 Batch 0/253 - Loss: 0.494, Seconds: 1060.06
> Epoch 1/50 Batch 20/253 - Loss: 8.450, Seconds: 905.71
> Epoch 1/50 Batch 40/253 - Loss: 4.540, Seconds: 933.88
> Epoch 1/50 Batch 60/253 - Loss: 4.401, Seconds: 740.15
> Epoch 1/50 Batch 80/253 - Loss: 4.453, Seconds: 831.04
> Epoch 1/50 Batch 100/253 - Loss: 4.338, Seconds: 774.67
> Epoch 1/50 Batch 120/253 - Loss: 4.295, Seconds: 832.49
Valid Loss: 4.091, Seconds: 675.05
New Record!
> Epoch 1/50 Batch 140/253 - Loss: 4.255, Seconds: 822.40
> Epoch 1/50 Batch 160/253 - Loss: 4.232, Seconds: 888.85
> Epoch 1/50 Batch 180/253 - Loss: 4.168, Seconds: 858.95
> Epoch 1/50 Batch 200/253 - Loss: 4.093, Seconds: 849.23
> Epoch 1/50 Batch 220/253 - Loss: 4.034, Seconds: 846.77
> Epoch 1/50 Batch 240/253 - Loss: 4.005, Seconds: 809.77
Valid Loss: 3.903, Seconds: 509.83
New Record!
...
...
...
...
...
定义question_to_seq()
函数,从用户那里获取输入问题,或者从数据集中选取一个随机问题,并将其转换为模型使用的整数格式。
def question_to_seq(question, vocab_to_int):
"""Creating the question to be taken as input by the model"""
question = clean_text(question)
return [vocab_to_int.get(word, vocab_to_int['<UNK>']) for word in question.split()]
现在是从本节开始时种下的树上获得果实的时候了。因此,这里我们将通过给出一个随机问题作为输入来检查 seq2seq 模型的输出。答案将由经过训练的模型生成。
# Selecting a random question from the full lot
random = np.random.choice(len(short_questions))
input_question = short_questions[random]
print(input_question)
> what exactly does adjustable life insurance mean
# Transforming the selected question in the desired format of IDs and Words
input_question = question_to_seq(input_question, questions_vocab_to_int)
# Applying Padding to the question to reach the max_line_length
input_question = input_question + [questions_vocab_to_int["<PAD>"]] * (max_line_length - len(input_question))
# Correcting the shape of input_data, by adding the empty questions
batch_shell = np.zeros((batch_size, max_line_length))
# Setting the input question as the first question
batch_shell[0] = input_question
# Passing input question to the model
answer_logits = sess.run(inference_logits, {input_data: batch_shell, keep_prob: 1.0})[0]
# Removing padding from Question and Answer both
pad_q = questions_vocab_to_int["<PAD>"]
pad_a = answers_vocab_to_int["<PAD>"]
# Printing the final Answer output by the model
print('Question')
print('Word Ids: {}'.format([i for i in input_question if i != pad_q]))
print('Input Words: {}'.format([questions_int_to_vocab[i] for i in input_question if i != pad_q]))
print('\n')
> Question
> Word Ids: [17288, 16123, 9831, 13347, 1694, 11205, 7655]
> Input Words: ['what', 'exactly', 'does', 'adjustable', 'life', 'insurance', 'mean']
print('\nAnswer')
print('Word Ids: {}'.format([i for i in np.argmax(answer_logits, 1) if i != pad_a]))
print('Response Words: {}'.format([answers_int_to_vocab[i] for i in np.argmax(answer_logits, 1) if i != pad_a]))
print('\n')
print(' '.join(([questions_int_to_vocab[i] for i in input_question if i != pad_q])))
print(' '.join(([answers_int_to_vocab[i] for i in np.argmax(answer_logits, 1) if i != pad_a])))
> Answer
> Word Ids: [10130, 10344, 13123, 2313, 1133, 1694, 11205, 6968, 966, 10130, 3030, 2313, 5964, 10561, 10130, 9158, 17702, 13344, 13278, 10130, 7457, 14167, 17931, 14479, 10130, 6968, 9158, 8521, 10130, 9158, 17702, 12230, 10130, 6968, 8679, 1688, 10130, 7457, 14167, 17931, 9472, 10130, 9158, 12230, 10130, 6968, 8679, 1688, 10130, 7457, 14167, 17931, 18293, 10130, 16405, 16640, 6396, 3613, 2313, 10130, 6968, 10130, 6968, 8679, 1688, 10130, 7457, 14167, 17931, 18293, 10130, 16405, 16640, 6396, 3613, 10628, 13040, 10130, 6968]
> Response Words: ['the', 'face', 'value', 'of', 'a', 'life', 'insurance', 'policy', 'is', 'the', 'amount', 'of', 'time', 'that', 'the', 'insured', 'person', 'passes', 'with', 'the', 'death', 'benefit', 'proceeds', 'from', 'the', 'policy', 'insured', 'if', 'the', 'insured', 'person', 'dies', 'the', 'policy', 'will', 'pay', 'the', 'death', 'benefit', 'proceeds', 'whenever', 'the', 'insured', 'dies', 'the', 'policy', 'will', 'pay', 'the', 'death', 'benefit', 'proceeds', 'within', 'the', 'two', 'year', 'contestability', 'period', 'of', 'the', 'policy', 'the', 'policy', 'will', 'pay', 'the', 'death', 'benefit', 'proceeds', 'within', 'the', 'two', 'year', 'contestability', 'period', 'specified', 'in', 'the', 'policy']
> what exactly does adjustable life insurance mean
> the face value of a life insurance policy is the amount of time that the insured person passes with the death benefit proceeds from the policy insured if the insured person dies the policy will pay the death benefit proceeds whenever the insured dies the policy will pay the death benefit proceeds within the two year contestability period of the policy the policy will pay the death benefit proceeds within the two year contestability period specified in the policy
最后一段是问题“可调寿险到底是什么意思?”我们放入模型中。嗯,这听起来在语法上不正确,但这是一个完全不同的问题,可以通过用更多的数据集和精炼的嵌入来训练模型,以更好的方式来处理。
假设随着时间的推移,对话文本中没有发生重大更新,人们可以利用经过训练的模型对象,并在聊天机器人应用程序中吸收它,以对聊天机器人的最终用户提出的问题做出漂亮的回答。这是留给读者的一个练习。享受与您自己的聊天机器人交谈!为了增加乐趣,你可以试着在与朋友的个人聊天中训练这个模型,看看你的聊天机器人是否能够成功地模仿你所爱的人。现在你知道了,要创建一个功能齐全的聊天机器人,只需要两个人的对话文本文件。
后续步骤
本章利用了第三章中解释的概念,帮助制作了一个聊天机器人,并训练了一个可以进一步嵌入 Facebook Messenger 聊天机器人的文本生成模型。在第五章中,我们将展示从第五届学习表征国际会议(2017 年 ICLR)上发布的一篇论文中提取的情感分类的实现。我们建议读者复制本章中的示例,并在不同的可用公共数据集上探索文本生成技术的不同用例。
五、研究论文实现:情感分类
第五章以一篇研究论文中情感分析的实现结束了这本书。本章的第一节详细介绍了所提到的方法,接下来的第二节将使用 TensorFlow 专门介绍其实现。为了确保我们使用的实际论文和我们的结果之间存在差异,我们选择了不同的数据集进行测试,因此我们的结果的准确性可能与实际研究论文中的结果有所不同。
正在使用的数据集可供公众使用,并作为样本数据集包含在 Keras 库中。本章将第二章和第三章中分享的理论和实践范例联系起来,并使用研究论文中遵循的建模方法创建了一个附加层。
我们的实现工作的成功归功于论文“结构化的自我关注句子嵌入”( https://arxiv.org/pdf/1703.03130.pdf
),该论文由来自 IBM Watson 和蒙特利尔大学( Université de Montréal )的蒙特利尔学习算法研究所(MILA)的研究科学家团队在 ICLR 2017(第五届国际学习表示会议)上提交,并随后发表。
本文提出了一种新的建模技术,通过引入自我注意机制来提取可解释的句子嵌入。该模型使用二维矩阵代替向量来表示句子嵌入,其中每个矩阵表示句子的不同片段。此外,还提出了一种自关注机制和一个独特的正则项。所提出的嵌入方法可以容易地被可视化,以计算出句子的哪些特定部分最终被编码到句子嵌入中。所进行的研究分享了所提出的模型在三种不同类型的任务上的性能评估。
- 作者简介
- 情感分类
- 文本蕴涵
与目前的其他句子嵌入技术相比,该模型对于前面三种类型的任务都非常有前途。
自我注意句子嵌入
先前已经提出了各种有监督和无监督的句子嵌入模型,例如跳过思想向量、段落向量、递归自编码器、顺序去噪自编码器、FastSent 等。,但该论文中提出的方法使用了一种新的自我注意机制,允许它将句子的不同方面提取到多个向量表示中。带有惩罚项的矩阵结构赋予模型更大的能力来从输入句子中解开潜在信息。
此外,语言结构不用于指导句子表示模型。此外,使用这种方法,人们可以很容易地创建可视化,这有助于对所学表征的解释。
skip-thought vector 是一个通用分布式句子编码器的无监督学习。利用书籍中文本的连续性,训练一个编码器-解码器模型,试图重建一段编码文章的周围句子。因此,共享语义和句法属性的句子被映射到相似的向量表示。有关这方面的进一步信息,请参阅原文,可在 https://arxiv.org/abs/1506.06726
查阅。
段落向量是一种无监督算法,它从可变长度的文本片段(如句子、段落和文档)中学习固定长度的特征表示。该算法用密集向量来表示每个文档,该密集向量被训练来预测文档中的单词。论文中的实证结果表明,段落向量优于词袋模型以及其他文本表示技术。关于这一点的更详细的解释包含在原始研究论文中,可在 https://arxiv.org/abs/1405.4053
获得。
图 5-1 显示了一个样本模型结构,用于展示句子嵌入模型与全连接和 softmax 层结合进行情感分析时的情况。
Note
蓝色代表隐藏表示,红色代表权重、注释或输入/输出。
图 5-1
The sentence-embedding model is computed as multiple weighted sums of hidden states from a bidirectional long short-term memory (LSTM) (h1, …, hn)
提议的方法
这一节包括建议的自我注意句子嵌入模型和正则化项。这两个概念都在单独的小节中解释,就像实际论文中提到的那样。读者可以选择参考原始论文以获得更多信息,尽管本节中介绍的内容足以对建议的方法有一个总体的理解。
所提出的注意机制仅执行一次,并且它直接关注对于辨别目标有意义的语义。它不太关注单词之间的关系,而是更关注每个单词所构成的整个句子的语义。在计算方面,该方法随着句子长度的增加而增加,因为它不需要 LSTM 计算所有先前单词的注释向量。
模型
在“结构化自我注意句子嵌入”中提出的句子嵌入模型由两部分组成:
- 双向 LSTM
- 自我注意机制
自我关注机制为 LSTM 隐藏状态提供了一组求和权向量(图 5-2 )。
图 5-2
The summation weights (Ai1, …, Ain) are computed as illustrated
求和权重向量的集合点缀有 LSTM 隐藏状态,并且得到的加权 LSTM 隐藏状态被认为是句子的嵌入。例如,它可以与多层感知器(MLP)相结合,应用于下游应用。所示的图属于一个示例,其中所提出的句子嵌入模型被应用于情感分析,与全连接层和 softmax 层相结合。
Note
对于情感分析练习,上图中使用的数字足以描述所需的模型。
(可选)除了使用完全连接的层之外,在该论文中还提出了一种通过利用矩阵句子嵌入的二维结构来修剪权重连接的方法,并在其附录 a 中进行了详细描述
假设我们有一个有 n 个标记的句子,用单词嵌入序列表示。
这里 w i 是一个向量,代表嵌入句子中第 I 个单词的 d 维单词。因此,s 是一个表示为二维矩阵的序列,它将所有的单词嵌入连接在一起。s 的形状应该是 n 乘 d。
现在,序列 S 中的每个条目都是相互独立的。为了在单个句子中的相邻单词之间获得一些依赖性,我们使用双向 LSTM 来处理句子
,然后我们将每个与
连接起来,以获得隐藏状态 h t 。设每个单向 LSTM 的隐藏单元号为 u。为简单起见,我们将所有 n 个 h t s 记为 H,其大小为 n 乘 2u。
H = (h 1 ,h 2 ,…,h n
我们的目标是将一个变长的句子编码成一个固定大小的嵌入。我们通过选择 h 中的 n 个 LSTM 隐藏向量的线性组合来实现这一点。计算线性组合需要自我注意机制。注意机制将所有 LSTM 隐藏状态 H 作为输入,并输出权重 a 的向量,如下:
这里 W s1 是一个形状为 da-x-2u 的权重矩阵,W s2 是一个大小为 d a 的参数向量,其中 d a 是一个我们可以任意设置的超参数。因为 H 的大小为 n 乘 2u,所以注释向量 a 的大小为 softmax()确保所有计算的权重相加为 1。然后,我们根据 a 提供的权重将 LSTM 隐藏状态 H 相加,以获得输入句子的向量表示 m。
这种向量表示通常关注句子的特定组成部分,例如一组特殊的相关单词或短语。因此,它应该反映句子语义的一个方面或组成部分。然而,一个句子中可以有多个组成部分,它们共同构成句子的整体语义,特别是对于长句。(例如,两个分句由 and 连接在一起。)因此,为了表示句子的整体语义,我们需要多个 m 来关注句子的不同部分。因此,我们必须进行多次注意力跳跃。假设我们想从句子中提取 r 个不同的部分。为此,我们将 W s2 扩展成一个 r 乘 d a 矩阵,记为 W s2 ,得到的标注向量 a 成为标注矩阵 a
形式上
这里,softmax()沿着其输入的第二维度执行。我们可以把前面的方程看作是一个无偏差的两层 MLP,其隐单元数为 d a ,参数为{W s2 ,W s1 }。
嵌入向量 m 然后变成 r 乘 2u 的嵌入矩阵 m。我们通过将注释矩阵 A 和 LSTM 隐藏状态 h 相乘来计算 r 加权和。得到的矩阵是句子嵌入:
M = A H
惩罚条款
如果注意机制总是为所有 r 跳提供相似的求和权重,则嵌入矩阵 M 可能遭受冗余问题。因此,我们需要一个惩罚项,以鼓励不同注意力跳跃的加权向量总和的多样性。
评估差异的最佳方式无疑是任意两个总权重向量之间的 Kullback Leibler 散度(KL)。
KL 散度用于度量同一变量 x 上两个概率分布的差异,它与交叉熵和信息散度有关。对于给定的两个概率分布 p(x)和 q(x),KL 散度作为 q(x)与 p(x)的散度的非对称度量,被表示为 D KL (p(x),q(x)),并且是当 q(x)被用来逼近 p(x)时丢失的信息的度量。
对于一个离散的随机变量 x,若 p(x)和 q(x)是它的两个概率分布,则 p(x)和 q(x)之和均为 1,且 p(x) > 0 和 q(x) > 0 对于 x 中的任意一个 X.
其中,
,当且仅当,P = Q
当使用基于 q(x)的码而不是使用基于 p(x)的码时,KL 散度测量对来自 p(x)的样本进行编码所需的额外比特的预期数量。通常,p(x)代表观测值的“实际”数据分布,或精确计算的理论分布,q(x)代表理论,或模型,或 p(x)的近似值。与离散型相似,KL 散度也有连续型。
KL 散度不是距离度量,即使它度量两个分布之间的“距离”,因为它不是度量。此外,它本质上是不对称的,即,在大多数情况下,从 p(x)到 q(x)的 KL 散度值不同于从 q(x)到 p(x)的 KL 散度值。此外,它可能不满足三角不等式。
然而,在这种情况下,这不是非常稳定的,因为在这里,正试图最大化一组 KL 散度(而不是通常情况下仅最小化一个),并且当执行注释矩阵 A 的优化时,为了在不同的 softmax 输出单元处具有许多足够小的值或者甚至零值,大量的零使得训练不稳定。KL divergence 没有提供的另一个特性是当前需要的,即每一行只关注语义的一个方面。这要求注释 softmax 输出中的概率质量更加集中,但是使用 KL 散度惩罚,这将达不到目的。
因此,引入了一个新的惩罚项,它克服了前面提到的缺点。与 KL 散度惩罚相比,这一项只消耗三分之一的计算量。从单位矩阵中减去 A 及其转置的点积,作为冗余度的度量。
在上式中,代表矩阵的 Frobenius 范数。像添加 L2 正则化项一样,这个惩罚项 P 将乘以一个系数,我们将其与原始损失一起最小化,这取决于下游应用。
让我们考虑 A 中的两个不同的求和向量,a i 和 a j ,由于 softmax,A 中任何求和向量内的所有条目加起来应该是 1。因此,它们可以被视为离散概率分布中的概率质量。对于 A.A T 矩阵中的任何非对角元素 a ij (i ≠ j),它对应于两个分布的逐元素乘积的求和:
其中 a k i 和 a k j 是 a i 和 Aj 中的第 k 个元素在最极端的情况下,当两个概率分布 a i 和 a j 之间没有重叠时,相应的 a ij 将为 0,否则它将具有正值。在另一个极端,如果两个分布是相同的,并且都集中在一个单词上,那么它将具有最大值 1。我们从 A.A T 中减去一个单位矩阵,这迫使 A.A T 对角线上的元素近似为 1,这鼓励每个求和向量 a i 关注尽可能少的单词数,迫使每个向量关注单个方面,而所有其他元素为 0,这惩罚了不同求和向量之间的冗余。
形象化
一般情况可视化呈现作者分析任务的结果,并显示正在使用的两种类型的可视化。第二个案例是关于情感分析的,利用了第二种可视化手段,即 Yelp 上的评论热图。
一般情况
由于注释矩阵 a 的存在,句子嵌入的解释非常简单。对于句子嵌入矩阵 M 中的每一行,其对应的注释向量 a i 都存在。该向量中的每个元素对应于该位置上的令牌的 LSTM 隐藏状态的贡献大小。因此,可以为嵌入矩阵 m 的每一行绘制热图
这种可视化方法暗示了在嵌入的每个部分中编码了什么,增加了一个额外的解释层。图 5-3 显示了在 Twitter Age 数据集上训练的两个模型的热图( http://pan.webis.de/clef16/pan16-web/author-profiling.html
)。
图 5-3
Heatmaps of six random detailed attentions from 30 rows of matrix embedding, and for two models without and with 1.0 penalization
第二种可视化方法可以通过将所有注释向量相加,然后将得到的权重向量归一化为 1 来实现。因为它把一个句子的语义的所有方面都加了起来,所以它产生了一个嵌入主要关注什么的总体视图。人们可以计算出哪些单词是嵌入考虑最多的,哪些是嵌入跳过的。图 5-4 通过将所有 30 个注意力权重向量相加来表示整体注意力的概念,包括惩罚和不惩罚。
图 5-4
Overall attention without penalization and with 1.0 penalization
情感分析案例
对于研究论文,已经为情感分析任务选择了 Yelp 数据集( www.yelp.com/dataset_challenge
)。它由 270 万条 Yelp 评论组成,从中随机选择了 50 万条评论星对作为训练集,2000 条用于开发集,2000 条用于测试集。将评论作为输入,并且根据用户为对应于企业商店的每个评论实际写了什么来预测星级的数量。
使用 100 维的 word2vec 来初始化单词嵌入,并且在训练期间进一步调整嵌入。目标星的数量是在[1,2,3,4,5]的范围内的整数,包括 1,2,3,4,5,并且因此,该任务被视为分类任务,即,将评论文本分类为五个类别之一,并且分类精度被用于测量。对于两个基线模型,使用的批量大小为 32,输出 MLP 中的隐藏单位数选择为 3,000。
作为对已学习句子嵌入的解释,下面使用第二种可视化方式,为数据集中的一些评论绘制热图。随机选择三篇评论。如图 5-5 所示,该模型主要学习捕捉评论中的一些关键因素,这些因素强烈地表明了句子背后的情感。对于大多数短评论,该模型设法捕捉所有促成极端得分的关键因素,但是对于较长评论,该模型仍然不能捕捉所有相关因素。正如在第一篇评论中所反映的那样,很多注意力都放在了一个单一的因素上,“没什么特别的”,而很少注意到其他的关键点,如“令人讨厌的事情”,“这么硬/冷”等。
图 5-5
Attention of sentence embedding on three different Yelp reviews, trained without and with 1.0 penalization
研究成果
本文引入了一个固定大小的矩阵句子嵌入,具有自我注意机制,有助于解释该模型中的句子嵌入深度。引入注意力机制允许最终句子嵌入通过注意力总和直接访问先前的 LSTM 隐藏状态。因此,LSTM 不需要把每一条信息都带到它最后隐藏的状态。相反,每个 LSTM 隐藏状态仅被期望提供关于每个单词的短期上下文信息,而需要长期依赖性的高级语义可以被注意力机制直接拾取。这种设置减轻了 LSTM 继续长期依赖的负担。将注意力机制中的元素相加的概念非常原始。它可以比这更复杂,这将允许对 LSTM 的隐藏状态进行更多的操作。
该模型可以将任何可变长度的序列编码成固定大小的表示形式,而不会遇到长期依赖问题。这为模型带来了很大的可扩展性,无需任何重大修改,就可以直接应用于更长的内容,比如段落、文章等。
实现情感分类
我们利用互联网电影数据库,俗称 IMDb ( www.imdb.com
),为情感分类问题选择数据集。它提供了大量的数据集,包括图像和文本,这对深度学习和数据分析的多种研究活动非常有用。
对于情感分类,我们使用了一组 25,000 条电影评论,这些评论附有它们的正面和负面标签。公开可用的评论已经被预处理并被编码为单词索引序列,即整数。单词基于它们在数据集中的总频率进行排序,即,具有第二高频率的单词或单词被索引为 2,等等。将这样的索引附加到单词上将有助于根据单词的频率将单词列入候选名单,例如挑选出前 2000 个最常用的单词或删除前 10 个最常用的单词。下面是查看训练数据集示例的代码。
from keras.datasets import imdb
(X_train,y_train), (X_test,y_test) = imdb.load_data(num_words=1000, index_from=3)
# Getting the word index used for encoding the sequences
vocab_to_int = imdb.get_word_index()
vocab_to_int = {k:(v+3) for k,v in vocab_to_int.items()} # Starting from word index offset onward
# Creating indexes for the special characters : Padding, Start Token, Unknown words
vocab_to_int["<PAD>"] = 0
vocab_to_int["<GO>"] = 1
vocab_to_int["<UNK>"] = 2
int_to_vocab = {value:key for key,value in vocab_to_int.items()}
print(' '.join(int_to_vocab[id] for id in X_train[0] ))
>
<GO> this film was just brilliant casting <UNK> <UNK> story direction <UNK> really <UNK> the part they played and you could just imagine being there robert <UNK> is an amazing actor and now the same being director <UNK> father came from the same <UNK> <UNK> as myself so i loved the fact there was a real <UNK> with this film the <UNK> <UNK> throughout the film were great it was just brilliant so much that i <UNK> the film as soon as it was released for <UNK> and would recommend it to everyone to watch and the <UNK> <UNK> was amazing really <UNK> at the end it was so sad and you know what they say if you <UNK> at a film it must have been good and this definitely was also <UNK> to the two little <UNK> that played the <UNK> of <UNK> and paul they were just brilliant children are often left out of the <UNK> <UNK> i think because the stars that play them all <UNK> up are such a big <UNK> for the whole film but these children are amazing and should be <UNK> for what they have done don't you think the whole story was so <UNK> because it was true and was <UNK> life after all that was <UNK> with us all
情感分类代码
这本书的最后一节涵盖了在前面提到的论文中描述的概念的实现及其在所选 IMDb 数据集的情感分类中的使用。所需的 IMDb 数据集可通过以下代码自动下载。如果需要,也可以从以下网址下载数据集,并查看可用的评论集: https://s3.amazonaws.com/text-datasets/imdb_full.pkl
。
Note
在运行代码之前,请确保计算机上有打开的互联网连接,以启用数据集下载和 TensorFlow 版本 1.3.0。
“0”未用于编码任何单词,因为它用于编码词汇表中的未知单词。
导入所需的包,并在需要时检查包的版本。
# Importing TensorFlow and IMDb dataset from keras library
from keras.datasets import imdb
import tensorflow as tf
> Using TensorFlow backend.
# Checking TensorFlow version
print(tf.__version__)
> 1.3.0
from __future__ import print_function
from tensorflow.python.ops import rnn, rnn_cell
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
下一步是从 IMDb 的评论数据集创建训练/测试数据集。Keras datasets 为它提供了一个内置函数,该函数返回以下带有序列和标签列表的元组对:
X_train
、X_test
:这些是具有索引列表的序列列表,即分配给每个单词的标准整数。如果在导入数据集时,指定了num_words
参数,则选择的最大可能索引值是num_words-1
,如果指定了maxlen
参数,则它用于选择最大可能序列长度。y_train
、y_test
:这些是整数标签的列表,分别为正面和负面评论指定 1 或 0。
imdb.load_data()
函数采用八个参数来自定义检查数据集选择。以下是对这些论点的详细解释:
path
:如果数据不在本地的 Keras 数据集文件夹中,它将被下载到指定的位置。num_words
:(类型:integer
或None
)选择要考虑用于建模目的的最频繁出现的单词。超出此范围且频率低于此范围的单词将被替换为序列数据中的oov_char
值。skip_top
: (Type:integer
)这将跳过选择中最常用的单词。这种被忽略的字在序列数据中被替换为oov_char
值。maxlen
:(类型:int
)用于指定序列的最大长度。超过指定长度的序列将被截断。seed
:(类型:int
)设置种子以再现数据洗牌start_char
:(类型:int
)该字符标记一个序列的开始。它被设置为 1,因为 0 通常用于填充字符。oov_char
: (Type:int
)由num_words
或skip_top
参数删除的单词将被替换为此字符。index_from
:(类型:int
)索引实际单词等。它是一个单词索引偏移量。
# Creating Train and Test datasets from labeled movie reviews
(X_train, y_train), (X_test, y_test) = imdb.load_data(path="imdb_full.pkl",num_words=None, skip_top=0, maxlen=None, seed=113, tart_char=1, oov_char=2, index_from=3)
> Downloading data from https://s3.amazonaws.com/text-datasets/imdb.npz
评论集中的每个序列的长度为 200,并且已经从训练数据集中创建了进一步的词汇。图 5-6 显示评论中的字数分布。
图 5-6
Distribution of word counts in each of the reviews
X_train[:2]
> array([ list([1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65, 458, 4468, 66, 3941, 4, 173, 36, 256, 5, 25, 100, 43, 838, 112, 50, 670, 22665, ....
t = [item for sublist in X_train for item in sublist]
vocabulary = len(set(t))+1
a = [len(x) for x in X_train]
plt.plot(a)
为从句子中选择的序列指定一个最大长度,如果检查长度小于该长度,则在新创建的序列后添加填充,直到达到最大长度。
max_length = 200 # specifying the max length of the sequence in the sentence
x_filter = []
y_filter = []
# If the selected length is lesser than the specified max_length, 200, then appending padding (0), else only selecting desired length only from sentence
for i in range(len(X_train)):
if len(X_train[i])<max_length:
a = len(X_train[i])
X_train[i] = X_train[i] + [0] * (max_length - a)
x_filter.append(X_train[i])
y_filter.append(y_train[i])
elif len(X_train[i])>max_length:
X_train[i] = X_train[i][0:max_length]
用单词嵌入大小、隐藏单元数量、学习速率、批量大小和训练迭代总数来声明模型超参数。
#declaring the hyper params
embedding_size = 100 # word vector size for initializing the word embeddings
n_hidden = 200
learning_rate = 0.06
training_iters = 100000
batch_size = 32
beta =0.0001
声明与当前模型架构和数据集相关的附加参数,max_length
,要分类的类的数量,自关注 MLP 的隐藏层中的单元数量,以及矩阵嵌入中的行数。
n_steps = max_length # timestepswords
n_classes = 2 # 0/1 : binary classification for negative and positive reviews
da = 350 # hyper-parameter : Self-attention MLP has hidden layer with da units
r = 30 # count of different parts to be extracted from sentence (= number of rows in matrix embedding)
display_step =10
hidden_units = 3000
将训练数据集值和标签分别转换为所需的数组后转换和编码格式。
y_train = np.asarray(pd.get_dummies(y_filter))
X_train = np.asarray([np.asarray(g) for g in x_filter])
创建内部文件夹来记录日志。
logs_path = './recent_logs/'
创建一个DataIterator
类,以产生给定批量的随机数据。
class DataIterator:
""" Collects data and yields bunch of batches of data
Takes data sources and batch_size as arguments """
def __init__(self, data1,data2, batch_size):
self.data1 = data1
self.data2 = data2
self.batch_size = batch_size
self.iter = self.make_random_iter()
def next_batch(self):
try:
idxs = next(self.iter)
except StopIteration:
self.iter = self.make_random_iter()
idxs = next(self.iter)
X =[self.data1[i] for i in idxs]
Y =[self.data2[i] for i in idxs]
X = np.array(X)
Y = np.array(Y)
return X, Y
def make_random_iter(self):
splits = np.arange(self.batch_size, len(self.data1), self.batch_size)
it = np.split(np.random.permutation(range(len(self.data1))), splits)[:-1]
return iter(it)
初始化权重和偏差,并在下一步输入占位符。设置神经网络中权重的一般规则是接近零,但不能太小。一个好的做法是在[y,y]范围内开始您的权重,其中 y = 1/ (n 是给定神经元的输入数量)。
############ Graph Creation ################
# TF Graph Input
with tf.name_scope("weights"):
Win = tf.Variable(tf.random_uniform([n_hidden*r, hidden_units],-1/np.sqrt(n_hidden),1/np.sqrt(n_hidden)), name='W-input')
Wout = tf.Variable(tf.random_uniform([hidden_units, n_classes],-1/np.sqrt(hidden_units),1/np.sqrt(hidden_units)), name='W-out')
Ws1 = tf.Variable(tf.random_uniform([da,n_hidden],-1/np.sqrt(da),1/np.sqrt(da)), name="Ws1")
Ws2 = tf.Variable(tf.random_uniform([r,da],-1/np.sqrt(r),1/np.sqrt(r)), name="Ws2")
with tf.name_scope("biases"):
biasesout = tf.Variable(tf.random_normal([n_classes]), name='biases-out')
biasesin = tf.Variable(tf.random_normal([hidden_units]), name='biases-in')
with tf.name_scope('input'):
x = tf.placeholder("int32", [32,max_length], name='x-input')
y = tf.placeholder("int32", [32, 2], name='y-input')
用嵌入的向量在相同的默认图形上下文中创建张量。这需要嵌入矩阵和输入张量,例如检查向量。
with tf.name_scope('embedding'):
embeddings = tf.Variable(tf.random_uniform([vocabulary, embedding_size],-1, 1), name="embeddings")
embed = tf.nn.embedding_lookup(embeddings,x)
def length(sequence):
# Computing maximum of elements across dimensions of a tensor
used = tf.sign(tf.reduce_max(tf.abs(sequence), reduction_indices=2))
length = tf.reduce_sum(used, reduction_indices=1)
length = tf.cast(length, tf.int32)
return length
使用以下方法重复使用权重和偏差:
with tf.variable_scope('forward',reuse=True):
lstm_fw_cell = rnn_cell.BasicLSTMCell(n_hidden)
with tf.name_scope('model'):
outputs, states = rnn.dynamic_rnn(lstm_fw_cell,embed,sequence_length=length(embed),dtype=tf.float32,time_major=False)
# in the next step we multiply the hidden-vec matrix with the Ws1 by reshaping
h = tf.nn.tanh(tf.transpose(tf.reshape(tf.matmul(Ws1,tf.reshape(outputs,[n_hidden,batch_size*n_steps])), [da,batch_size,n_steps]),[1,0,2]))
# in this step we multiply the generated matrix with Ws2
a = tf.reshape(tf.matmul(Ws2,tf.reshape(h,[da,batch_size*n_steps])),[batch_size,r,n_steps])
def fn3(a,x):
return tf.nn.softmax(x)
h3 = tf.scan(fn3,a)
with tf.name_scope('flattening'):
# here we again multiply(batch) of the generated batch with the same hidden matrix
h4 = tf.matmul(h3,outputs)
# flattening the output embedded matrix
last = tf.reshape(h4,[-1,r*n_hidden])
with tf.name_scope('MLP'):
tf.nn.dropout(last,.5, noise_shape=None, seed=None, name=None)
pred1 = tf.nn.sigmoid(tf.matmul(last,Win)+biasesin)
pred = tf.matmul(pred1, Wout) + biasesout
# Define loss and optimizer
with tf.name_scope('cross'):
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits =pred, labels = y) + beta*tf.nn.l2_loss(Ws2) )
with tf.name_scope('train'):
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
gvs = optimizer.compute_gradients(cost)
capped_gvs = [(tf.clip_by_norm(grad,0.5), var) for grad, var in gvs]
optimizer.apply_gradients(capped_gvs)
optimized = optimizer.minimize(cost)
# Evaluate model
with tf.name_scope('Accuracy'):
correct_pred = tf.equal(tf.argmax(pred,1), tf.argmax(y,1))
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))
tf.summary.scalar("cost", cost)
tf.summary.scalar("accuracy", accuracy)
> <tf.Tensor 'accuracy:0' shape=() dtype=string>
# merge all summaries into a single "summary operation" which we can execute in a session
summary_op =tf.summary.merge_all()
# Initializing the variables
train_iter = DataIterator(X_train,y_train, batch_size)
init = tf.global_variables_initializer()
# This could give warning if in case the required port is being used already
# Running the command again or releasing the port before the subsequent run should solve the purpose
开始训练模型。确保batch_size
足以满足系统要求。
with tf.Session() as sess:
sess.run(init)
# Creating log file writer object
writer = tf.summary.FileWriter(logs_path, graph=tf.get_default_graph())
step = 1
# Keep training until reach max iterations
while step * batch_size < training_iters:
batch_x, batch_y = train_iter.next_batch()
sess.run(optimized, feed_dict={x: batch_x, y: batch_y})
# Executing the summary operation in the session
summary = sess.run(summary_op, feed_dict={x: batch_x, y: batch_y})
# Writing the values in log file using the FileWriter object created above
writer.add_summary(summary, step*batch_size)
if step % display_step == 2:
# Calculate batch accuracy
acc = sess.run(accuracy, feed_dict={x: batch_x, y: batch_y})
# Calculate batch loss
loss = sess.run(cost, feed_dict={x: batch_x, y: batch_y})
print ("Iter " + str(step*batch_size) + ",
Minibatch Loss= " + "{:.6f}".format(loss) + ", Training Accuracy= " + "{:.2f}".format(acc*100) + "%")
step += 1
print ("Optimization Finished!")
> Iter 64, Minibatch Loss= 68.048653, Training Accuracy= 50.00%
> Iter 384, Minibatch Loss= 69.634018, Training Accuracy= 53.12%
> Iter 704, Minibatch Loss= 50.814949, Training Accuracy= 46.88%
> Iter 1024, Minibatch Loss= 39.475891, Training Accuracy= 56.25%
> Iter 1344, Minibatch Loss= 11.115482, Training Accuracy= 40.62%
> Iter 1664, Minibatch Loss= 7.060193, Training Accuracy= 59.38%
> Iter 1984, Minibatch Loss= 2.565218, Training Accuracy= 43.75%
> Iter 2304, Minibatch Loss= 18.036911, Training Accuracy= 46.88%
> Iter 2624, Minibatch Loss= 18.796995, Training Accuracy= 43.75%
> Iter 2944, Minibatch Loss= 56.627518, Training Accuracy= 43.75%
> Iter 3264, Minibatch Loss= 29.162407, Training Accuracy= 43.75%
> Iter 3584, Minibatch Loss= 14.335728, Training Accuracy= 40.62%
> Iter 3904, Minibatch Loss= 1.863467, Training Accuracy= 53.12%
> Iter 4224, Minibatch Loss= 7.892468, Training Accuracy= 50.00%
> Iter 4544, Minibatch Loss= 4.554517, Training Accuracy= 53.12%
> Iter 95744, Minibatch Loss= 28.283163, Training Accuracy= 59.38%
> Iter 96064, Minibatch Loss= 1.305542, Training Accuracy= 50.00%
> Iter 96384, Minibatch Loss= 1.801988, Training Accuracy= 50.00%
> Iter 96704, Minibatch Loss= 1.896597, Training Accuracy= 53.12%
> Iter 97024, Minibatch Loss= 2.941552, Training Accuracy= 46.88%
> Iter 97344, Minibatch Loss= 0.693964, Training Accuracy= 56.25%
> Iter 97664, Minibatch Loss= 8.340314, Training Accuracy= 40.62%
> Iter 97984, Minibatch Loss= 2.635653, Training Accuracy= 56.25%
> Iter 98304, Minibatch Loss= 1.541869, Training Accuracy= 68.75%
> Iter 98624, Minibatch Loss= 1.544908, Training Accuracy= 62.50%
> Iter 98944, Minibatch Loss= 26.138868, Training Accuracy= 56.25%
> Iter 99264, Minibatch Loss= 17.603979, Training Accuracy= 56.25%
> Iter 99584, Minibatch Loss= 21.715031, Training Accuracy= 40.62%
> Iter 99904, Minibatch Loss= 17.485657, Training Accuracy= 53.12%
> Optimization Finished!
模型结果
使用 TensorFlow 摘要或日志记录建模结果,并在运行模型脚本时保存。为了写入日志,使用了日志写入器FileWriter()
,它在内部创建日志文件夹并保存图形结构。TensorBoard 随后将记录的汇总操作用于可视化目的。我们将日志保存在当前工作目录的以下内部文件夹位置:logs_path = './recent_logs/'
。
要启动 TensorBoard,请根据您的选择指定端口:tensorboard --logdir=./ --port=6006.
张量板
为了使 TensorBoard 可视化更具可读性,我们在需要的地方添加了占位符和变量的名称。TensorBoard 有助于代码的调试和优化。
我们已经添加了整个模型的图形和它的一些片段,以帮助将代码与 TensorFlow 图形可视化关联起来。所有的片段都可以与前一子部分中它们相应的代码片段相关联。
图 5-7 显示了情感分类的完整网络架构。该图显示了贯穿代码的变量,这有助于理解模型中的数据流和连接。
图 5-7
TensorFlow graph of the overall model
图 5-8 显示了图中的 MLP 分量,用于在最后一层添加 dropout 的加法,以及 sigmoid 函数来预测最终的情感分类结果。最终预测进一步用于收集模型的准确性和成本。
图 5-8
TensorBoard graph for the MLP segment
图 5-9 显示了网络的嵌入组件。用于初始化embeddings
变量,由[-1,1]范围内均匀分布的随机值组成。embedding_lookup()
技术用于对embeddings
张量执行并行查找,该张量进一步用作 LSTM 层的输入。
图 5-9
TensorBoard graph for the embedding segment
模型精度和成本
以下是在 IMDb 数据集上执行的四次模拟以及具有不同平滑过滤器参数值的两种情况的模型精度和成本图表。
Note
平滑过滤器在 TensorBoard 中用作控制窗口大小的加权参数。权重 1.0 表示使用整个数据集的 50%作为窗口,而权重 0.0 表示使用 0 的窗口(因此,用每个点自身替换每个点)。过滤器作为一个额外的参数来彻底解释图形。
案例 1
对于第一种情况,平滑滤波器的值被设置为0.191
,我们在四个不同的模拟中比较了模型的精度和成本(图 5-10 和 5-11 )。
图 5-11
TensorBoard graph for the cost parameter
图 5-10
TensorBoard graph for the accuracy parameter
案例 2
对于第二种情况,平滑值被设置为0.645
,我们在四个不同的模拟中比较了模型精度和成本(图 5-12 和 5-13 )。
图 5-13
TensorBoard graph for cost parameter
图 5-12
TensorBoard graph for accuracy parameter
改进的余地
从前面的图表中可以推断出,模型的精确度并不高,在某些情况下接近 70%。有几种方法可以进一步改进前面练习中获得的结果,包括改变输入到模型中的训练数据,以及改进模型的超参数。本文中用于情感分析的训练数据集包括 50 万条 Yelp 评论和用于开发和测试目的的 rest。在执行的练习中,我们进行了 25K 次审查。为了进一步提高模型的性能,我们邀请读者对代码进行修改,并比较多次迭代的结果。为改善结果所做的更改应与论文中提到的值一致,从而有助于比较多个数据集的结果。
后续步骤
这本书的最后一章介绍了所选研究论文的情感分析的实现。我们希望所有背景的读者开展这样的活动,并尝试在他们选择的数据集上用他们喜欢的语言复制不同论文和会议上提出的算法和方法。我们相信,这样的练习提高了对研究论文的理解,并拓宽了对不同类型的算法的理解,这些算法可以应用于解决特定问题的相关数据集。
我们希望读者能够享受本书中所有用例的旅程。我们将非常感谢他们的建议,以提高这里提出的代码和理论的质量,我们将确保在我们的代码库中进行任何相关的更改。