白话词嵌入:从计数向量到Word2Vec

2019-09-18 16:32:48 浏览数 (1)

NSS的这篇文章实在是写得很经典,简要翻译学习一下。 原文链接:https://www.analyticsvidhya.com/blog/2017/06/word-embeddings-count-word2veec/

目录 0 介绍 1 什么是词嵌入? 2 不同种类的词嵌入 2.1 词频嵌入 2.1.1 计数向量 2.1.2 TF-IDF 2.1.3 共现矩阵 2.2 预测嵌入 2.2.1 CBOW 2.2.2 Skip-gram 3 词嵌入的应用案例 4 使用预训练的词向量 5 训练属于自己的词向量 6 结语

我喜欢唱、跳、打怪、篮。。。(原谅我,让我皮一下 ^_^)

0 介绍

开始之前先看几个例子:

  1. 在谷歌搜索框中输入一个新闻标题,能返回几百篇相关的结果;
  2. Nate Silver通过分析几百万推文预测2008年美国大选,正确率达到了49/50;
  3. 在谷歌翻译中输入一句英语,得到它的中文翻译。

这些例子有什么相同点?

答案是——“文本处理”。上面三个场景通过处理海量文本,完成了三个不同的任务:聚类、分类和机器翻译。

人类处理文本任务既不可扩展,也十分低效。

让机器代替人力,关键是创建词的表征,该表征可以获取词义、语义关系和不同的上下文种类。

表征可以通过词嵌入数值表征来完成。

下面就来看看什么是词嵌入,和词嵌入的不同类型,以及如何使用词嵌入完成返回搜索结果的任务。

1 什么是词嵌入?

简单来说,词嵌入就是将文本转换成数字,方法不同,数值表征也不同。在深入之前,先来讨论下为什么需要词嵌入?

人们经过实践得出结论,多数机器学习算法和几乎所有的深度学习框架都不能处理原始个格式的字符串和文本。机器需要数字作为输入,才能执行分类回归这样的任务。文本中蕴含着海量的数据,有必要从中提取出有用的东西,并创建应用,比如亚马逊商品评论、文档或新闻的情感分析、谷歌搜索的分类和聚类。

正式给词嵌入下个定义:词嵌入是使用词典,将单词映射到矢量上。把这句话分解,逐一分析。

看这个例子:

代码语言:javascript复制
# 将sentence作为一个变量
sentence = "Word Embeddings are Word converted into numbers"

所谓的字典,是sentence中不同单词组成的列表,也就是:

代码语言:javascript复制
[‘Word’,’Embeddings’,’are’,’Converted’,’into’,’numbers’]

可以用独热编码来生成矢量,在独热编码中,1表示单词在该位置存在,0表示不存在。根据上面的字典,单词numbers的独热编码是[0,0,0,0,0,1]converted的编码是[0,0,0,1,0,0]

这只是用矢量表征单词的一个非常简单的方法。接下来看看不同的词嵌入或词向量的方法,以及各自的优缺点。

2 不同类型的词嵌入

可以将词嵌入大致分成两类:

  1. 基于频率嵌入
  2. 基于预测嵌入

2.1 基于频率嵌入

基于频率,有三种向量表示法:

  1. 计数向量
  2. TF-IDF向量
  3. 共现向量

2.1.1 计数向量

一个包含D篇文档{D1,D2…..DD}的语料库C,包含有N个不同的单词。这N个单词就组成了词典。计数向量矩阵M的形状是D x N。矩阵M的每一行,是单词出现在D(i)中的频率。

这么说很难懂,举个栗子?:

代码语言:javascript复制
D1: He is a lazy boy. She is also lazy.
D2: Neeraj is a lazy person.

D1D2两个文档的词典是不同单词组成的列表,也就是

代码语言:javascript复制
corpus =[‘He’,’She’,’lazy’,’boy’,’Neeraj’,’person’]

有文档共有两篇、词典中有六个单词,所以D=2, N=6

根据计数矩阵的定义,就该表示成一个2 x 6的矩阵:

其中,每一列就是单词的词向量,例如,lazy的词向量就是[2,1]

计数向量矩阵有几种变体,区别在于:

  1. 构成词典的方式不同 —— 因为在真实世界的案例中,语料库可能会包含数百万篇文档。从如此多的文档中,可以提取出数百万不同的单词。所以用上面方法来生成矩阵,矩阵会特别稀疏(矩阵中的0特别多),会导致计算效率低下。所以只采用总词典中,频率最高的10000个词,作为真正使用的词典。
  2. 每个单词的计数方法不同 —— 我们可以使用频率(某个单词在文档中出现的次数)或是否出现(出现就是1,否则是0)作为矩阵中的值。一般来说,词频方法用的更多。

下面是一个矩阵的表征图(注:和刚才的例子相比,文档和词的位置发生了转置):

2.1.2 TF-IDF矢量化

TF-IDF也是一种基于词频的方法,跟计数向量不同的地方是,他不仅考虑了某个词在一篇文档中的出现次数,也考虑了单词在整个预料库中的出现情况。

像is、the、a这样的常见词,总是在文章有更多的出现机会。我们要做的就是降低这些常见词的权重。

TF-IDF是这么做的,考虑下面的两个文档:

先来解释下TF和IDF分别是什么?

TF是词频两个单词term frequency的缩写:

代码语言:javascript复制
TF = (某个词在文档中出现的次数) / (文档中所有词的总频次数)

所以,This的TF值应该如下:

代码语言:javascript复制
TF(This, Document1) = 1/8
TF(This, Document2) = 1/5

TF可以表示某个单词对文档的贡献,也就是说,和文档相关性强的词应该出现的多,例如,一篇关于梅西的文章,应该会多次出现梅西的名字“Messi”。

IDF是逆文档频率inverse document frequency的缩写:

代码语言:javascript复制
IDF = log(N/n)
# N是总文档数,n是文档出现过某个单词的文档数

因此,This和Messi的IDF值是:

代码语言:javascript复制
IDF(This) = log(2/2) = 0
IDF(Messi) = log(2/1) = 0.301

IDF的意义是,如果一个单词在所有文档中都出现过,那么这个单词相对于任何一篇文档都不重要。如果一个单词只在某些文档中出现过,说明该单词和这些文档有相关性。

将TF和IDF结合起来,再比较This和Messi两个词的值:

代码语言:javascript复制
TF-IDF(This,Document1) = (1/8) * (0) = 0
TF-IDF(This, Document2) = (1/5) * (0) = 0
TF-IDF(Messi, Document1) = (4/8)*0.301 = 0.15

对于Document1来说,This的分值低于Messi的分值,说明Messi对于Document1更重要。

2.1.3 共现矩阵(固定内容窗口)

根据常识,相似的词通常会出现在相似的上下文,比如这两句话:

代码语言:javascript复制
Apple is a fruit. 
Mango is a fruit.

Apple和Mango有相似的上下文,也就是fruit。

先解释下什么是共现矩阵和内容窗口:

  • 共现矩阵:对于给定的预料,两个词w1和w2的共现次数是它们出现在内容窗口中的次数;
  • 内容窗口:某个单词的一定的前后范围称为内容窗口。

绿色部分就是单词Fox的大小为2的内容窗口,在计算共现时,只有内容窗口之内的词才会被计算

看一个具体的例子,语料如下:

代码语言:javascript复制
Corpus = He is not lazy. He is intelligent. He is smart.

内容窗口大小为2的共现矩阵

红格子 —— 窗口大小为2时,He和is共现了4次; 蓝格子 —— lazy从来没有和intelligent出现在窗口中;

示意图:He和is的4次共现

共现矩阵的变化

假设语料中有V个不同的词。共现矩阵可以有两种变体:

  1. 共现矩阵的大小是V x V。这样的矩阵,对于任何语料V,都会特别大且难于处理,所以很少用;
  2. 共现矩阵的大小是V x N,N是V(例如去除了停用词the、a这样的词)的子集,矩阵仍然很大,计算还是困难。

其实,共现矩阵并不是通常使用的词向量,而是经过PCA(主成分分析)、SVD(奇异值分解)之后,才构成词向量。

假如对上面大小是V x V的矩阵做了主成分分析,可以获得V个主成分,从其中挑出k个,就可以构成一个大小是V x k的矩阵。

对于某一个单词,就算经过了降维,语义也不会下降很多。k的大小通常是数百。

其实PCA的作用,是将贡献矩阵分解成三个三个矩阵U、S和V,U和V都是正交矩阵。重点在于,U和S的点积是词向量表征,V是词的上下文表征。

共现矩阵的优点:

  1. 保留了词之间的语义关系,比如:“男人”和“女人”通常比“男人”和“苹果”离得更近;
  2. 使用主成分分析或奇异值分解,可以获得更准确的词向量;
  3. 一经算好一个共现矩阵,可以多次使用。

共现矩阵的缺点

  1. 存储矩阵要耗费大量内存(但是可以通过分解,将矩阵缩小,将缩小后的矩阵存储在集群中)

2.2 基于预测的矢量

Mitolov推出的word2vec是一种基于预测的方法,性能比前面的方法好的多。

word2vec是两种技术的集合 —— CBOW(连续词袋)和Skip-gram模型。这两种方法都是浅层神经网络。

2.2.1 CBOW

CBOW的原理是通过给定的上下文,预测词的概率。上下文可以是一个词,也可以是一组词。简单起见,我举的例子是用一个词来预测另一个词。

假设语料如下:

代码语言:javascript复制
C = “Hey, this is sample corpus using only one context word.”

内容窗口的大小是1。这个语料可以转化为如下的CBOW模型的训练集。下图的左边是输入和输出,右边是独热编码矩阵,一共包含17个数据点。

将这个矩阵输入给一个只有3层的神经网络:一个输入层、一个隐藏层、一个输出层。输出层是softmax层,确保输出层的概率之和是1。下面就来看看前向传播是如何计算隐藏层的。

先来看一个图像化的CBOW:

一个数据点的向量表征如下所示:

过程如下:

  1. 输入层和目标值,都是大小为1 x V的独热编码,在这个例子中V=10;
  2. 有两组权重值,一组在输入层和隐藏层之间,另一组在隐藏层和输出层之间;
  3. 层和层之间没有激活函数;
  4. 输入值先乘以输入-隐藏权重矩阵,得到隐藏激活矢量;
  5. 隐藏输入层乘以隐藏-输出矩阵,得到输出值;
  6. 计算输出和目标值之间的误差,使用误差调整权重;
  7. 将隐藏层和输出层之间的矩阵,作为词向量。

这是只有一个上下文词的情况。假如有多个上下文词,就要像下图来进行计算:

下图是矩阵表征:

在这张图中,使用3个上下文词来预测目标值。输入层有3个1 x V的矢量,输出层是1个1 x V矢量。不同的地方是隐藏激活矢量需要做一次取平均值。

在上下文词是1和3的两种情况下,画的图都是只到隐藏激活矢量而已,因为这部分是CBOW区别于多层感知机网络MLP的地方。

MLP和CBOW的区别在于:

  1. MLP的目标函数是平均方根MSE,CBOW的目标函数是给定上下文时,求某个词的负对数概率,即-log(p(wo/wi))p(wo/wi)如下:

wo : 输出词, wi : 上下文词

  1. 对于隐藏-输出权重矩阵和输入-隐藏权重矩阵的误差梯度不同,这是因为MLP使用的是sigmoid激活函数,CBOW是线性激活函数。但是,计算梯度的方法是一样的。

CBOW的优势:

  1. 基于概率的方法,拥有更好的性能;
  2. 因为不用存储共现矩阵,CBOW消耗内存低。

CBOW的劣势:

  1. CBOW计算得到的是某个词的上下文平均值。Apple可以表示水果也可以表示公司,但CBOW得到的是二者的平均值;
  2. 如果没有优化,训练CBOW会耗时很久。

2.2.2 Skip-gram模型

Skip-gram的底层原理和CBOW差不多,就是把CBOW正好给反了过来。Skip-gram的目标是根据单词预测上下文。还是使用前面的语料,构建训练数据:

代码语言:javascript复制
C=”Hey, this is sample corpus using only one context word.”

skip-gram的输入矢量跟上下文是1时的CBOW模型很像。另外,输入层到隐藏层的计算也一模一样。不同的地方在于目标值。因为定义的内容窗口大小是1,所以有两个独热编码目标值,两个对应的输出(下图中的蓝色部分)。

分别根据两个目标值得到两个独立的误差矢量,两个误差矢量合并成一个误差矢量,再反向传播更新权重。

训练完成后,输入层和隐藏层之间的权重矩阵作为词向量。

skip-gram的架构如下所示:

矩阵计算示意图如下:

在这张图中,输入层的大小是1 x V,输入-隐藏层权重矩阵的大小是V x N,隐藏层的神经元数量是N,隐藏-输出权重矩阵大小是N x V,输出层的大小是C个1 x V

在这个例子中,窗口大小C=2, V=10,N=4:

  1. 红色部分是隐层激活层,大小和输入-隐藏矩阵一样;
  2. 黄色部分是隐藏-输出权重矩阵;
  3. 蓝色部分是隐藏激活层乘以隐藏输出矩阵的结果。对于两个上下文目标词,得到的结果会有两行;
  4. 蓝色部分的每一行都分别经过softmax,得到绿色部分;
  5. 灰色部分是目标值的独热编码矢量;
  6. 浅绿色部分是误差,是绿色减灰色得到的;
  7. 误差相加之后,再反向传播进行计算权重。

skip-gram模型的优势:

  1. skip-gram模型可以对一个词获取两种语义,即它可以有Apple一词的两种表征;
  2. skip-gram经过降采样之后,比其它方法表现都好。

3 词嵌入的应用案例

因为词嵌入是词的上下文相似性的表示,可以用来做以下任务:

  1. 计算词的相似度
代码语言:javascript复制
model.similarity('woman','man')
0.73723527
  1. 过滤异常值输出
代码语言:javascript复制
model.doesnt_match('breakfast cereal dinner lunch';.split())
'cereal'
  1. 进行这样的计算woman king - man = queen
代码语言:javascript复制
model.most_similar(positive=['woman','king'],negative=['man'],topn=1)
queen: 0.508
  1. 文本在模型中的概率
代码语言:javascript复制
model.score(['The fox jumped over the lazy dog'.split()])
0.21

一张word2vec的可视化图:

词向量在二维的t-SNE表示,可以看到Apple的两种上下文都获取到了。

  1. 做机器翻译

这张图表示了中文和英文的双语词嵌入,可以看到语义相似的词,位置也靠近,因此可以用来做翻译。

4 使用预训练的词向量

使用谷歌的预训练模型。词典大小是300万,用大小是1000亿词的谷歌新闻数据集训练而成,大小是1.5GB,下载地址。

代码语言:javascript复制
from gensim.models import Word2Vec

# 加载模型
model = Word2Vec.load_word2vec_format('GoogleNews-vectors-negative300.bin', binary=True, norm_only=True)

# 加载模型之后,就可以完成上面的任务了。

# 获得某个词的词向量
dog = model['dog']

# 做king queen的词语字符串计算
print(model.most_similar(positive=['woman', 'king'], negative=['man']))

# 找出奇异值
print(model.doesnt_match("breakfast cereal dinner lunch".split()))

# 打印相似指数
print(model.similarity('woman', 'man'))

5 训练属于自己的词向量

使用gensim和自己的语料来训练word2vec。

训练数据的格式如下:

代码语言:javascript复制
sentence=[[‘Neeraj’,’Boy’],[‘Sarwan’,’is’],[‘good’,’boy’]]

用这3句话来训练

代码语言:javascript复制
model = gensim.models.Word2Vec(sentence, min_count=1,size=300,workers=4)

参数的含义如下:

sentence —— 包含列表的列表语料; min_count=1 —— 词的阈值,只有高于阈值才会计算; size=300 —— 词的表征维度,即词向量的大小 workers=4 —— 并行的worker数量

6 结语

词嵌入一直就是一个活跃的研究领域,但是近年的研究不仅越来越臃肿,还越来越复杂。这篇文章的目的,是用浅显易懂的语言加少量的数学,回顾词嵌入的发展。

0 人点赞