Notebook:
Review_Text_Word_Similarity_Analysis
用户在查找商店的时候一般会输入关键词进行搜索,从而找到跟该关键词相似度较高的商店。具体地,基于评论文本,我们可以建立一个词汇之间的相似度模型,让用户输入关键词即可找到相似的词汇,并因此找到相对应的商店。
在实现上,本文使用了 Word2Vec 工具来构建评论文本词汇之间的相似度模型,并利用 Elasticsearch 搜索引擎来查找与关键词相似的词汇。
词相似度模型
词向量
在自然语言处理领域,一般需要把词转换为计算机可以理解的词向量形式。
一种比较简单的构造词向量的方法是 One-hot 表示法。但这种方法构造出来的词向量长度随着词汇表的增大而增大,存在维度过大的隐患;数据很稀疏(词向量只有一个位置为 1,其余都为 0);而且每个词向量都是独立的,无法体现词与词之间的关系。
另外一种构造词向量的方法是分布式表示法(Dristributed representation)。它通过某种训练将词用一个简短的词向量来表示,且词向量的长度由我们指定,不存在维度过大问题;训练出来的词向量是稠密的,不存在数据稀疏问题;另外重要的是每个词向量包含语义信息,可以通过计算词向量之间的余弦来表示词之间的相似度。谷歌推出的 Word2Vec 工具就是训练此种词向量的一种方法。
词相似度
当我们得到词向量(记得有方向)之后,我们就可以计算两个词之间的余弦距离,从而得到它们的相似程度。如对词 A、B,它们之间的相似度公式如下:
$$
similarity(A,B)=\frac{A\cdot B}{\left || A \right ||\left || B \right ||} =\frac{\sum_{n}^{i}A_{i}\times B_{i}}{\sqrt{\sum_{n}^{i}A_{i}^{2}}\times \sqrt{\sum_{n}^{i}B_{i}^{2}}}
$$
它的意义在于,如果两个词很相似,那它们词向量之间的夹角应该很小(甚至为 0),方向很一致。
我们可以来看一个很有名的例子 King-Man+Woman=Queen:
初次看到可能会觉得很奇怪 King、Man、Woman、Queen 这些怎么都可以相加减的?因为上图中的词表示的是词向量,而且既然是向量,那就有大小和方向,自然可以相加减。
那我们再看看是怎么得到 King-Man+Woman=Queen 这个结论的:
从上图可以看出,King-Man+Woman 的结果向量跟 Queen(几乎)是重合的,也即是说 King-Man+Woman 跟 Queen 相似度很高,所以才有例子中的等式。
词向量训练
这里我们使用 Word2Vec 来训练词向量。
Word2Vec 提供了 CBOW (Continuous Bag-of-words) 和 Skip-Gram 两种方式来训练词向量。下边我们简要介绍一下这两者。
CBOW
CBOW 的训练目标是当给定上下文词汇时,最大化某个词发生的条件概率
,如下边例子,当输入上下文为 an, efficient, method, for, high, quality, distributed, vector 时,我们希望能够最大化 learning 词发生的条件概率:
具体的训练架构如下图所示:
Skip-Gram
Skip-Gram 跟 CBOW 的过程相反,即输入一个词,预测该词的上下文。
如输入 learning,我们希望输出是 an, efficient, method, for, high, quality, distributed, vector。
它的目标函数是最小化预测到的上下文的误差的总和。
其训练架构如下图所示:
具体训练中,我们采用了 Gensim 的 Word2Vec 库,并选择了其默认的 CBOW 训练方式。详细的训练过程可参考 Notebook Review_Text_Word_Similarity_Analysis。
关键词搜索
当我们完成了训练之后,我们就可以直接调用训练好的模型获取跟某个用户输入的关键词词最相似的 TopN 个词,如:
接下来我们就要根据关键词+相似词
(这里包括关键词是因为它本身也是用户关心的)对评论文本进行搜索,以找出相关的商店。
直接内存搜索存在的问题
待搜索评论数量达到 1,604,044 条,而且每条评论的文本平均单词数也达到 57.96:
数据量虽说不是很大,但直接在内存搜索速度很慢
,可看下搜索结果:
而且,还有问题是,返回结果太多(这里都返回了全部),无法做排序
;而且我们无法在查找过程中做一些聚合
。这样我们就需要后期做很多的排序、聚合等工作。
基于 Elasticsearch 搜索
Elasticsearch 是一个分布式的 RESTful 风格的搜索和数据分析引擎,承载数据量可达 PB 级,而且速度很快。
虽然使用 Elasticsearch 来搜索我们的评论文本有点杀鸡用牛刀的意味,但为了更快更方便对数据进行分析,这样选择也是值得的。
关于它的安装及更多介绍这里就不多说了,网络有很多资料可参考。
数据导入
将 CSV 文件导入 Elasticsearch 数据库,这里使用了一个开源的脚本 CSV-to-ElasticSearch。
具体的导入命令如下:
1 | python csv_to_elastic.py \ |
分片存在的问题
采用以上方法直接导入的数据默认会存在在 5 个分片(Shard)中,我们可以用 Http Get 看一下结果:
1 | GET review |
这样一来,如果我们对查询到的数据进行聚合(Aggregation)的话,就可能会得到不正确的结果。因为 Elasticsearch 是对各分片返回的数据做聚合,这样会导致部分数据统计不准确。
我们由一个官方的例子来说明一下这个问题。
原始数据存放在 3 个分片中
查找总数最多的前 5 个产品
1
2
3
4
5
6
7
8
9
10
11GET /_search
{
"aggs" : {
"products" : {
"terms" : {
"field" : "product",
"size" : 5
}
}
}
}
结果如下:
- 结果分析
对比一下原始数据,我们会发现聚合统计的部分结果有误,例如产品 C,它在原始数据集中的总量是 6+4+44=54,但返回结果只有 50。这是因为产品的数据分布放在 3 个分片中,而聚合获取数据时是按各个分片取 5 个数量最多的产品,但在分片 2 中,产品 C 数量垫底,没被返回,从而导致最终数据统计错误。下图是 3 个分片各自返回的数据:
所以,为了规避这个问题,我们这里就将评论文本数据都放在一个分片中,这样统计出来的数据就是准确的了。
重新导入数据
首先,创建一个 Elasticsearch 索引(Index),指明分片数量:
1 | PUT yelp_review |
导入数据:
1 | python csv_to_elastic.py \ |
聚合分析
接下来我们用关键词+相似词
搜索 Elasticsearch 数据库,并按商店分组,最后按搜索命中的评论数量对商店进行从高到低的排序。例如,搜索结果有 50 条评论文本,其中 20 条属于对商店 A 的评论,30 属于对商店 B 的评论,那在返回结果的时候,商店 B 就会排在 A 的前头。
这里之所以选择按命中的评论数量对商店进行排序,是因为一个商店名下被返回(命中)的评论越多,可以认为它就越符合要求
。
一个搜索并聚合的例子如下:
1 | GET /yelp_review/text/_search |
下边是返回结果:
1 | { |
从以上结果可以看出,基于 Elasticsearch 的搜索有如下特点:
- 速度快,比基于内存字符串匹配的要小两个数量级
- 可以对搜索结果进行聚合、排序,这对我们找 TopN 商店非常方便
除了以上提到的优点,我们还可以对搜索结果做进一步的聚合,如聚合每个商店被命中的评论文本的 score 平均值(Elasticsearch 搜索到的结果都会有一个匹配度 score 值),然后按新的聚合值排序。