使用Gensim来实现Word2Vec和FastText
作者:Steeve Huang
编译:ronghuaiyang
导读
嵌入是NLP的基础,这篇文章教你使用Gensim来实现Word2Vec和FastText,并通俗易懂的描述了Word2Vec和FastText的基本原理。
在自然语言处理(NLP)中,我们经常将单词映射成向量,以便机器能够理解它。单词嵌入是一种映射类型,它允许具有相似含义的单词具有相似的表示。本文将介绍两种最先进的word嵌入方法,Word2Vec和FastText及其在Gensim中的实现。
传统方法
传统的表示单词的方法是独热向量,它本质上是一个只有一个目标元素为1,其他元素为0的向量。向量的长度等于语料库中所有唯一词汇量的大小。按照惯例,这些独特的单词是按字母顺序编码的。也就是说,以“a”开头的单词的独热向量中1的位置具有较小的索引,而以“z”开头的单词有较高的索引。
虽然单词的这种表示方法简单且易于实现,但是存在几个问题。首先,给定两个单词独热表示形式,你无法推断它们之间的任何关系。例如,单词“endure”和“tolerate”虽然意思相似,但它们向量表示中的“1”相距甚远。此外,稀疏性是另一个问题,因为向量中有许多冗余的“0”。这意味着我们正在浪费大量的空间。我们需要更好的表示来解决这些问题。
Word2Vec
Word2Vec是这些问题的有效解决方案,它利用了目标单词的上下文。从本质上讲,我们想用一个神经网络的隐含层来表示单词。
有两种类型的Word2Vec,Skip-gram和Continuous Bag of Words(CBOW)。在下面的段落中,我将简要描述这两种方法的工作原理。
Skip-gram
对于Skip-gram,输入是目标单词,而输出是目标单词的上下文单词。例如,假设窗口大小为5,在句子“I have a cute dog”中,输入为“a”,而输出为“I”、“have”、“cute”和“dog”。所有的输入和输出都是相同尺寸的独热编码。网络包含1个隐含层,其维数等于嵌入大小,小于输入/输出向量大小。在输出层的末尾,使用softmax函数,用来输出向量的每个元素描述特定单词在上下文中出现的可能性。下图显示了网络结构。
将目标词的独热表示输入网络后,提取隐藏层,得到目标词的嵌入。
使用skip-gram,表示维度从词汇量(V)减小到隐含层的长度(N),并且向量在描述单词之间的关系时更加“有意义”。通过减去两个相关单词得到的向量有时表示一个有意义的概念,如性别或动词时态,如下图所示(降维)。
CBOW
Continuous Bag of Words(CBOW)与skip-gram非常相似,只是它交换了输入和输出。这个想法是,给定一个上下文,我们想知道哪个单词最有可能出现在其中。
skip-gram和CBOW最大的区别在于单词vector的生成方式。对于CBOW,所有以目标词为目标的例子都被输入到网络中,并取提取的隐含层的平均值。例如,假设我们只有两句话,“He is a nice guy”和“She is a wise queen”。为了计算单词“a”的单词表示,我们需要输入这两个例子,“He is nice guy”和“She is wise queen”到神经网络中,并取隐藏层中值的平均值。skip-gram只输入一个目标词的独热向量作为输入。
据说,skip-gram在罕见的词汇中表现得更好。不过,整体来看,skip-gram和CBOW的性能基本相同。
实现
我将向你展示如何使用Gensim、一个功能强大的NLP工具包和一个TED Talk数据集来执行word嵌入。
首先,我们使用urllib下载数据集,从文件中提取副标题。
让我们看看input_text变量存储了什么,如下图所示。
input_text
很明显,有一些多余的信息对我们理解对话没有帮助,比如括号里描述声音的单词和说话人的名字。我们用正则表达式把这些词去掉。
# remove parenthesis input_text_noparens = re.sub(r'\([^)]*\)', '', input_text) # store as list of sentences sentences_strings_ted = [] for line in input_text_noparens.split('\n'): m = re.match(r'^(?:(?P<precolon>[^:]{,20}):)?(?P<postcolon>.*)$', line) sentences_strings_ted.extend(sent for sent in m.groupdict()['postcolon'].split('.') if sent) # store as list of lists of words sentences_ted = [] for sent_str in sentences_strings_ted: tokens = re.sub(r'[^a-z0-9]+', ' ', sent_str.lower()).split() sentences_ted.append(tokens)
现在,已经将sentences_ted转换为一个二维数组,每个元素都是一个单词。让我们打印出第一个和第二个元素。
sentences_ted
这是准备输入Gensim中定义的Word2Vec模型的格式。Word2Vec模型可以很容易地用一行代码进行训练,如下面的代码所示。
- sentences:拆分句子后的列表。
- size:嵌入向量的维数
- window:查看的上下文单词的数量
- min_count:告诉模型忽略总计数小于这个数字的单词。
- workers:使用的线程数
- sg:使用skip-gram还是CBOW
现在,让我们来看看哪些单词与“man”最相似。
model_ted.wv.most_similar(“man”)
似乎与men/women/kid相关的词语与“man”最为相似。
虽然Word2Vec成功地处理了由独热向量引起的问题,但是它有几个限制。最大的挑战是它不能表示训练数据集中没有出现的单词。即使使用包含更多词汇的更大的训练集,一些很少使用的罕见词汇也永远无法映射到向量。
FastText
FastText是Facebook在2016年提出的Word2Vec的扩展。FastText不是将单个单词输入神经网络,而是将单词分解成几个n-grams(子单词)。例如,单词apple的三元组是app、ppl和ple(忽略单词边界的开始和结束)。“apple”的嵌入向量是所有这些n-grams的和。在对神经网络进行训练后,我们将对给定训练数据集的所有n-grams进行单词嵌入。稀有单词现在可以正确地表示了,因为它们的一些n-grams很可能也出现在其他单词中。在下一节中,我将向你展示如何在Gensim中使用FastText。
实现
与Word2Vec类似,我们只需要一行代码就可以指定训练单词嵌入的模型。
让我们用单词Gastroenteritis来尝试一下,这个单词很少使用,也没有出现在训练数据集中。
model_ted.wv.most_similar('Gastroenteritis')
虽然在训练数据集中没有“Gastroenteritis”这个词,但它仍然能够找出这个词与一些医学术语密切相关。如果我们在前面定义的Word2Vec中尝试这样做,它将弹出错误,因为在训练数据集中不存在这样的单词。尽管训练FastText模型(n-grams的数量>单词数)需要更长的时间,但是它的性能比Word2Vec更好,并且允许适当地表示罕见的单词。
英文原文:https://towardsdatascience.com/word-embedding-with-word2vec-and-fasttext-a209c1d3e12c