短语挖掘在应用层面上与新词发现有重叠部分,关于新词发现的内容可以参考我的这篇博客《新词发现》。如果我们希望能够从一大段文本中挖掘出新的短语,那么短语挖掘的做法与新词发现相差不大,通过凝聚程度、自由程度等指标对文本片段进行划分,找出新的文本片段作为新的短语。

另一个应用是根据已有的短语从文本中找出语义相似的短语,本篇博客主要介绍这一应用的一个简单实践。

实现思路

  1. 首先,我们可以借助分词工具对文本进行分词;
  2. 然后,将分词后的词列表映射到词向量空间;
  3. 接着,把已有的短语视为启动词,依次便利每个启动词,以启动词在词向量空间中的位置和指定长度半径寻找相似的新词语;
  4. 重复第三个步骤,将找到的新词语作为启动词继续挖掘,但这次减小半径长度;
  5. 不断重复第 3 和第 4 步骤,直到找不到新的词语。

上述做法与聚类算法中的 DBSCAN(可以参考这篇博客《聚类算法之DBSCAN算法之一:经典DBSCAN》)类似,区别在于 DBSCAN 的半径保持不变,而上述做法中的半径会随着迭代逐渐减小。

【问】:为什么要在迭代的过程中减小半径呢?

【答】:如果按照固定的半径不断寻找新的词语,那么就会发生下图所示的问题,最左边的红色点和最右边的红色点肯定不相似呀,所以需要不断对半径进行衰减来避免这种现象的发生。

DBSCAN

上述方案实际上是苏神苏剑林大佬提出的方法,具体内容可参考苏神的博客《分享一次专业领域词汇的无监督挖掘》

具体实现

首先,导入所需的包:

1
2
3
4
5
import json
import jieba
import numpy as np
from queue import Queue
from gensim.models import Word2Vec

在这使用 jieba 作为分词器,当然读者们也可以使用其他分词器,分词的好坏决定了后续短语挖掘的效果。

接着,读取数据,进行分词训练 word2vec 模型。

1
2
3
4
5
6
7
8
9
data_word_list = []

with open("data/content.json", "r", encoding="utf-8") as file:
dataset = json.load(file)

for data in dataset:
data_word_list.append(jieba.lcut(data))

model_wv = Word2Vec(data_word_list, window=5, size=100, min_count=10, sg=0, negative=5, workers=5)

然后编写短语挖掘的主体函数,在苏神的代码上略作了修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class PhraseMining:

def __init__(self, model_wv=None):
self.model_wv = model_wv

def find(self, start_words: list, center_words: "ndarray" = None, neg_words: "ndarray" = None, min_sim: float = 0.6, max_sim: float = 1.0, alpha: float = 0.25) -> list:
"""
根据启动的种子词去挖掘新词
"""
if self.model_wv is None:
print("The word2vec model is None!")
return []

# 获取词向量大小
word_size = self.model_wv.vector_size

if center_words is None and neg_words is None:
min_sim = max(min_sim, 0.6)

center_vec, neg_vec = np.zeros([word_size]), np.zeros([word_size])

if center_words:
_ = 0
for w in center_words:
if w in self.model_wv.wv.vocab:
center_vec += self.model_wv[w]
_ += 1
if _ > 0:
center_vec /= _

if neg_words:
_ = 0
for w in neg_words:
if w in self.model_wv.wv.vocab:
neg_vec += self.model_wv[w]
_ += 1
if _ > 0:
neg_vec /= _

queue_count = 1
task_count = 0
cluster = []
queue = Queue()

for w in start_words:
queue.put((0, w))
if w not in cluster:
cluster.append(w)

while not queue.empty():
idx, word = queue.get()
queue_count -= 1
task_count += 1
if word not in self.model_wv.wv:
continue
sims = self._most_similar(self.model_wv, word, center_vec, neg_vec)
min_sim_ = min_sim + (max_sim - min_sim) * (1 - np.exp(-alpha * idx))
if task_count % 10 == 0:
log = '%s in cluster, %s in queue, %s tasks done, %s min_sim' % (len(cluster), queue_count, task_count, min_sim_)
print(log)
for i, j in sims:
if j >= min_sim_:
if i not in cluster:
queue.put((idx + 1, i))
if i not in cluster:
cluster.append(i)
queue_count += 1
return cluster

@staticmethod
def _most_similar(model_wv, word, center_vec=None, neg_vec=None):
vec = model_wv[word] + center_vec - neg_vec
return model_wv.similar_by_vector(vec, topn=200)

最后,开始一次短语挖掘!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
phrase_model = PhraseMining(model_wv)
print(phrase_model.find(["尾灯"], min_sim=0.7, alpha=0.5))
"""
21 in cluster, 11 in queue, 10 tasks done, 0.8896361676485673 min_sim
21 in cluster, 1 in queue, 20 tasks done, 0.8896361676485673 min_sim
['尾灯',
'前大灯',
'贯穿',
'灯组',
'前灯',
'大灯',
'内部结构',
'车尾',
'头灯',
'扁平',
'两侧',
'细长',
'羽式',
'箭',
'灯带',
'狭长的',
'进气口',
'日行',
'下部',
'尾部',
'光源']
"""

从上述结果中可以看到仍然存在不少“噪声”,例如“内部结构”、“偏平”等词语,这些可以通过后处理的方式进行过滤。整体来说,效果还算可以,能够抽取“前大灯、前灯、头灯”等相似词语。读者们可以尝试调整 min_sim 最小相似度阈值 和 alpha 相似度阈值增加系数来获得不同的挖掘结果。

除此之外,通过添加中心词和否定词也能限制在词向量空间的搜索范围。

1
2
3
4
5
print(phrase_model.find(["尾灯"], ["大灯"], min_sim=0.7, alpha=0.5))
# ['尾灯', '大灯', '前大灯', '灯组', '头灯', '前灯', '扁平', '光源', '组', '内部结构', '灯带', '细长', '狭长', 'LED']

print(phrase_model.find(["尾灯"], ["大灯"], ["细长"], min_sim=0.7, alpha=0.5))
# ['尾灯', '大灯', '前大灯', '头灯', '灯组', '光源', '前灯']

为了方便后续使用,我们可以将训练好的 word2vec 模型保存到本地。

1
phrase_model.model_wv.save("/path/word2vec.model")

下次使用时,读取 word2vec 模型并作为参数出给 PhraseMining 构造函数,此时就不需要再次训练 Word2Vec 模型了,直接使用 find() 函数即可进行短语挖掘。

1
2
3
model_wv = Word2Vec.load("/path/word2vec.model")
phrase_model = PhraseMining(model_wv)
phrase_model.find(["尾灯"], min_sim=0.7, alpha=0.5)

关于 gensim 包中 Word2Vec 模型的更多用法请参考:https://radimrehurek.com/gensim/models/word2vec.html

后话

在这篇博客中使用了 Word2Vec 将词语映射到词向量空间,此时得到的是静态词向量,我们也可以使用 ELMo 或者 Bert 来映射得到动态词向量。相比静态词向量,动态词向量能够解决一词多义问题,具体内容可以参考 《腾讯抗黑灰产——自监督发现行话黑词识别一词多义》

相关代码可从 Repo https://github.com/clvsit/nlp_simple_task_impl 中获得。

参考