全文共8395字,预计学习时长17分钟
本文将介绍怎样用Word2Vec创建反向词典。除了Word2Vec之外,使用其他文字嵌入模型也会得到同样的结果。如果你不知道这意味着什么也不要紧,我们会解释清楚的。反向词典就是一种词典,需要先输入定义,然后找出与该定义相匹配的单词。
所需代码可以在companion repository中找到:https://github.com/unosviluppatore/reverse-dictionary
自然语言处理的应用
自然语言处理是一个很棒的领域。这个领域很有趣,客户也经常在应用程序中使用。因为自然语言处理是个很难进入的领域。你永远不知道,一个问题是能够在一天之内通过一个现成的库得以解决?还是需要一个研发团队研究两年才能获得不错的结果。简单点说:自然语言处理困难的地方不在于技术,而在于理解怎样成功运用。
基于机器学习解决上述问题时就更突出了,你需要了解一点机器学习的背景。有时即使解决方案行得通,仍需几个星期来调整参数和权重。
所以,本文中只列举一种技术,也就是一个简单的程序:反向词典。也就是说,怎么通过定义找到单词。这是一款简洁的程序,是买不到的,也无法用确定性算法来创建。
机器学习中的单词表征
机器学习的第一步是理解如何表达所处理的数据。最基本的方式就是使用one-hot编码,有以下几种方式:
· 收集所有的可能值(例如,10000个可能值)
· 每个可能值都用一个向量表示,这个向量包含尽可能多的分量(每个值由一个包含10000个组件的向量表示)
· 为了表达每个值,除了给一个组件赋值为1外,其他组件都赋值为0(即,每个组件都是0,除了一个组件是1)
将其应用于单词,就意味着:
· 需要10000个单词量
· 每个单词都用一个向量表示,这个向量包含10000个组件
· 单词 dog 表达为[1, 0, 0 …],单词 cat 表达为 [0, 1, 0 …],等等
这种方式可以表达所有单词,但还存在两大缺点:
· 每个向量都非常稀疏;大多数向量都是0
· 表达不具备任何语义;“father”和“mother”意思非常接近,但你永远不会看到这两个单词用one-hot编码
解决之道:单词嵌入
为了克服上面的两大缺点,单词嵌入应运而生。这种类型的单词表征的关键在于,具有相似含义的单词使用相似的表达。为了捕捉单词意思,或者与其他单词相关联,该表达允许有密集的向量。意思很简单,嵌入的单词并没有真正捕捉到father的意思,但是它的表达将与mother的表达类似。
这是一个强大的特点,可以应用于各种情况。例如,可以解决下面这个问题:
什么对于父亲相当于母亲对于女儿?
第一个单词嵌入模型是 Word2Vec ,我们将用它来创建反向词典。这一模型对该领域进行了革新,并催生了许多其他模型,例如FastText、GloVe这些模型之间有着细微差别——例如GloVe和Word2Vec是在单词层面上训练,而FastText则是在character n-grams上训练的。但是它们的原理、应用以及结果都非常相似。
Word2Vec的工作原理
Word2Vec的有效性得益于下面这个技巧:针对特定任务训练神经网络,然后将这个神经网络用于其他用途。这个技巧并不是Word2Vec所独有的,这是机器学习中最常用的一个技巧。基本上只需训练神经网络来得到一个特定的输出,然后去掉输出层,只保留神经网络隐藏层的权重。
训练过程和往常一样:给神经网络一个输入值以及一个与输入值相对应的输出。这样,神经网络可以慢慢学习如何生成正确的输出。
这个训练任务是为了计算在给定输入单词的情况下,在上下文中出现某个单词的概率。例如,如果有programmer这个单词,那么在上下文的短语中看到computer这个单词的概率是多少?
Word2Vec训练策略
Word2Vec有两种典型的策略:CBOW和skip-gram——这两个策略是可逆的。用CBOW输入单词的上下文,在输出中得到想要的单词。用skip-gram则相反:先输入单词,然后预测它所适用的上下文。
在最基本的层面上,“context (上下文)”这个术语仅仅是指目标单词周围的词,比如目标单词前后的词。但是,“上下文”也可用于表达句法意义(例如,指主语,如果目标词是动词)。这里只采用“上下文”最简单的含义。
例如这句话:
the gentle giant graciously spoke
在CBOW中, 需要输入[gentle, graciously]来得到输出giant。在skip-gram中,我们将输入“输入giant”,同时输入“输出[gentle, graciously]”。训练是用单个单词完成的,所以,在实践中,对CBOW来说:
· 第一次,输入gentle,期待结果输出giant。
· 第二次,输入 graciously ,期待结果输出 giant。
如果用skip-gram,就要把输入和输出颠倒过来。
神经网络会带来什么
就像前面所说,训练的最后会删除输出层,因为我们并不真正关心一个单词出现在目标单词附近的概率。例如,当输入单词flag时,我们并不真正关心 USA会有多大的概率出现在上下文中。然而,我们将保留神经网络隐藏层的权重,并用它来表达单词。这有什么用呢?
这是网络结构的特点所致。输入是单词的one-hot编码。例如,dog使用[1,0,0 …]来表达。在训练期间,输出也是单词的one-hot编码(例如,对于the dog barks,使用CBOW应输入dog,输出barks)。最终,输出层将会有一系列可能性。例如,给定输入单词cat,输出层将有可能在cat的上下文中出现单词dog。pet这个单词也会有出现的可能性,依此类推。
Word2Vec的神经网络
每个神经元对每个单词都有权重,所以在训练结束时,将有无数个神经元,每个神经元对每个单词都有权重。此外,要记住,表达单词的输入向量除了一个分量是1外,其他都是零。
因此,在矩阵乘法的数学规则中,如果用神经元矩阵乘以一个输入向量,输入向量为0,则神经元中的大多数权重无效,每个神经元中,将剩下一个与输入单词相关联的权重。每个神经元中的一系列非零权重将是Word2Vec中表达单词的权重。
上下文相似的单词会得到相似的输出,所以在特定单词的上下文中出现的概率也相似。换句话说,dog和cat会得到相似的输出。因此,它们也将具有相似的权重。这就是为什么意义相近的单词在Word2Vec中用相近的向量来表示。
Word2Vec的判断相当简单:相似的单词会出现在相似的短语中。然而,这一点也相当有效。当然,前提是有足够大的数据集用于训练。稍后会处理这个问题。
Word2Vec的意义
现在知道了Word2Vec的工作原理,下面来看看怎么用它创建反向词典。反向词典是通过输入定义来查找单词的词典。所以,理想情况下,输入group of relative(一群亲戚),程序会给出family(家族)。
从显而易见的开始:同义词词典使用的是单词嵌入法。如前所述,在这个系统中,相似的词有相似的表达。因此,如果想要系统找到与输入单词相近的单词,它会找到一个意思相似的单词。例如,如果输入happiness,可能会输出joy。
由此,你也许认为,做与上面相反的事情也行得通,比如找到输入单词的反义词。很不幸,这是不可能直接实现的,因为表达单词的向量不能理解单词之间那么精确的关系。基本上,sad这个单词的向量与 happy这个词的向量并不是镜像相反。
为什么可以用Word2Vec
请看下面单词向量的简化表示。
单词关系的图形表达
系统可以找到由?表达的单词,仅仅因为它可以从father的向量中添加两个给定单词向量(mother 和 daughter)之间的差异。该系统确实能够捕捉到单词之间的关系,但是这种捕捉方式并不容易理解。换句话说:向量的位置是有意义的,但这也意味着这些位置的定义不是绝对的(例如,相反的),而是相对的(例如,一个看起来像是A-B的单词)。
这也解释了为什么无法直接找到反义词:在Word2Vec中没有可以用来描述这种关系的数学运算。
反向词典的工作原理
现在既然完全理解了单词向量的作用,那就可以来理解怎么用它们来创建一个反向词典了。反向词典基本可以用来查找到一个与输入内容(也就是定义)相似的单词。这是可以实现的,因为系统使用的是向量数学。
例如,如果输入 group of relatives,它就会找到 family。也可以在定义中使用否定的词来帮助识别一个词。例如,亲属团体决心组织起来。稍后将看到否定一个词的确切含义。也可以在定义中使用否定词来帮助识别单词。例如,将group of -relatives解析为organization。稍后将会解释否定一个单词有什么意义。
Word2Vec模型的数据
既然理论都清楚了,下面可以看看代码,然后创建反向词典。
第一步是创建字典。创建字典本身并不难,但是要花费很长时间。更重要的是,内容越多越好。对于普通用户来说,下载和存储大量数据并不轻松。仅英文维基百科的转储,提取时就可能需要超过50 GB(只包含文本)。通用爬网(可自由访问的已爬网页面)的数据可能会占用数千兆字节的存储空间。
出于实用考虑,最好使用谷歌共享的资料,例如谷歌新闻数据预先训练好的模型:GoogleNews-vectors-negative300.bin.gz。如果对这个文件进行搜索,很多地方都找得到。以前,谷歌代码项目是该文件的官方来源,但现在最好的来源似乎是谷歌硬盘。该文件是1.6GB的压缩版,不需要解压。
数据下载完成后,可以放在项目下的 models目录中。有许多针对Word2Vec的库,所以可以使用多种语言,但鉴于Python在机器学习中的流行度,本文会选择Python作为示例。本文将使用gensim库的Word2Vec,因为它是最优的。至于网络界面,则会使用Flask。
加载数据
首先,需要用3行简单的代码将Word2Vec加载至内存。
model = KeyedVectors.load_word2vec_format("./models/GoogleNews-vectors-negative300.bin.gz", binary=True)
model.init_sims(replace=True) #
model.syn0norm = model.syn0 # prevent recalc of normed vectors
第一行是唯一真正需要加载数据的行。另外两行只是在开始时做一些初步的计算,所以后面就不需要为每个请求做这些计算了。
但是,这3行代码在普通计算机上执行可能需要2-3分钟(用SSD)。一开始只需一些等待,所以这也不算太糟糕,但也不是很理想。另还有一种方法,就是可以一劳永逸地进行一些计算和优化,之后把它们保存起来,以后每次开始时,先从磁盘中加载。
添加这个函数来创建优化版的数据。
def generate_optimized_version():
model = KeyedVectors.load_word2vec_format("./models/GoogleNews-vectors-negative300.bin.gz", binary=True)
model.init_sims(replace=True)
model.save('./models/GoogleNews-vectors-gensim-normed.bin')
主函数每次都使用这段代码来加载Word2Vec的数据。
optimized_file = Path('./models/GoogleNews-vectors-gensim-normed.bin')
if optimized_file.is_file():
model = KeyedVectors.load("./models/GoogleNews-vectors-gensim-normed.bin",mmap='r')
else:
generate_optimized_version()
# keep everything ready
model.syn0norm = model.syn0 # prevent recalc of normed vectors
这将加载时间从几分钟缩短到几秒钟。
清理词典
还有一件事要做:清理数据。一方面,谷歌新闻模型很棒,它是由一个非常大的数据集生成的,所以非常准确。这要比自己创建模型好的多。然而,由于它是基于新闻来创建的,所以包含许多拼写错误,更重要的是,我们并不需要这个实体模型。
换句话说,它不仅仅包含单词,还包含新闻中提到的建筑物和机构名称等内容。这可能会阻碍反向词典的建立。例如,如果输入一个类似a tragic event的定义,系统可能会发现,与这组输入单词最相似的是发生过悲剧事件的地方。然而,这并不是我们想要的。
因此,必须对模型输出的所有项进行过滤,以确保输出的是字典中可以找到的常用单词。这类单词列表来自SCOWL(Spell Checker Oriented Word Lists:http://wordlist.aspell.net/)。使用这个链接中的工具创建了一个自定义词典,并将它放在了 models 文件夹中。
# read dictionary words
dict_words = []
f = open("./models/words.txt", "r")
for line in f:
dict_words.append(line.strip())
f.close()
# remove copyright notice
dict_words = dict_words[44:]
现在可以很容易地加载单词列表,来比较Word2Vec系统过滤掉的条目。
反向词典
反向词典的功能代码非常简单。
def find_words(definition, negative_definition):
positive_words = determine_words(definition)
negative_words = determine_words(negative_definition)
similar_words = [i[0] for i in model.most_similar(positive=positive_words, negative=negative_words, topn=30)]
words = []
for word in similar_words:
if (word in dict_words):
words.append(word)
if (len(words) > 20):
words = words[0:20]
return words
从用户的输入中可以收到对所查找单词的肯定性描述,也可以接收到否定词,必须从其他词中减去这些词的向量。
这一操作的用意更难理解:似乎并没有将这个词的反义词包含进来。相反,还想去掉否定词。例如,假设定义是group of -relatives,relatives是否定词, group 和of是肯定词。想要找到一个与输入内容中所标识的单词意思最接近的词,但是去除了所有 relatives特别添加的意义。
第5行中有与肯定的定义意思最相似的前30个单词。该函数会返回单词和相关分数,我们只对单词本身感兴趣,所以可以忽略分数。
其余的代码很容易理解。通过比较之前加载的词典单词数据库中的单词,从之前返回的单词中,选择出那些真正的单词,而不是地点或事件。将单词列表减少到最多20个单词。
清除输入的单词
这一步通过find_words实现。第2行和第3行中有一个determine words函数。这个函数从输入的字符串中生成一个单词列表。如果一个单词前面有负号,则是一个否定单词。
def determine_words(definition):
possible_words = definition.split()
for i in range(len(possible_words) - 1, -1, -1):
if possible_words[i] not in model.vocab:
del possible_words[i]
possible_expressions = []
for w in [possible_words[i:i+3] for i in range(len(possible_words)-3+1)]:
possible_expressions.append('_'.join(w))
ex_to_remove = []
for i in range(len(possible_expressions)):
if possible_expressions[i] in model.vocab:
ex_to_remove.append(i)
words_to_remove = []
for i in ex_to_remove:
words_to_remove += [i, i+1, i+2]
words_to_remove = sorted(set(words_to_remove))
words = [possible_expressions[i] for i in ex_to_remove]
for i in range(len(possible_words)):
if i not in words_to_remove:
words.append(possible_words[i])
return words
首先,这个函数会生成一个单词列表,还会添加表达式。这是因为谷歌新闻模型除了简单的单词外,还有短语的向量表达。我们试图通过简单地组合3-gram单词(即3个单词的滑动集合)来找到表达式。如果在谷歌新闻模型(第14行)中发现了表达式,则必须将该表达式作为一个整体添加,并删除单个单词。
网络App
为了便于使用,创建了一个简单的Flask应用程序:这是一个基本的网络界面,可以让用户输入定义并阅读单词列表。
def create_app():
app = Flask(__name__)
return app
app = create_app()
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
words = request.form['definition'].split()
negative_words = ''
positive_words = ''
for i in range(len(words)):
if(words[i][0] == '-'):
negative_words += words[i][1:] + ' '
else:
positive_words += words[i] + ' '
return jsonify(find_words(positive_words, negative_words))
else:
return render_template('index.html')
该应用程序支持根目录路径。它会显示带有定义的页面,并在收到定义时,输出相应的单词列表。多亏了Flask,用几行代码就能搞定这些。
正在运行的反向词典
反向词典运行得怎么样呢?它对于描述性定义非常有效,也就是说,如果使用的是对目标词进行描述的定义,就很可能会在应用程序返回的单词列表中找到正确的单词。它可能不是列表中的第一个单词,但肯定能在列表中找得到。但是对于逻辑性定义来说,它并不那么有效,例如female spouse。因此,这个词典当然不够完美,但对于简单的查找来说,它还是非常有效的。
总结
这篇文章用几行代码创建了一个有效的反向词典,这要归功于Word2Vec和现成的数据库。这对于一个小教程来说,是一个很好的结果。但是在机器学习方面,应该牢记:应用程序的成功不仅仅取决于代码。对于许多机器学习的应用程序来说,成功在很大程度上取决于拥有的数据,无论是使用的训练数据,还是向程序提供数据的方法,都很重要。
例如,对于数据集的选择,忽略了一些试验和错误。因为我们知道谷歌新闻数据集包含新闻事件,所以已经预料到了会存在一些问题。所以尝试基于英文维基百科建立自己的数据集。然而非常悲惨地失败了,得到的结果比使用谷歌新闻数据集得到的更糟糕。一部分原因在于数据集较小,但真正的原因在于我们的能力不如谷歌的工程师。选择合适的训练参数需要一种“黑魔法”,只有积累大量的经验才能掌握。最后,对我们来说,最好的解决方案就是使用谷歌新闻数据集,并过滤输出,以消除应用程序不需要的结果。虽然这一点让人感到羞愧,但还是值得牢记于心的。
留言 点赞 关注
我们一起分享AI学习与发展的干货
欢迎关注全平台AI垂类自媒体 “读芯术”
(添加小编微信:dxsxbb,加入读者圈,一起讨论最新鲜的人工智能科技哦~)
转载:https://blog.csdn.net/duxinshuxiaobian/article/details/101976059