短语挖掘在应用层面上与新词发现有重叠部分,关于新词发现的内容可以参考我的这篇博客《新词发现》。如果我们希望能够从一大段文本中挖掘出新的短语,那么短语挖掘的做法与新词发现相差不大,通过凝聚程度、自由程度等指标对文本片段进行划分,找出新的文本片段作为新的短语。
另一个应用是根据已有的短语从文本中找出语义相似的短语,本篇博客主要介绍这一应用的一个简单实践。
实现思路
- 首先,我们可以借助分词工具对文本进行分词;
- 然后,将分词后的词列表映射到词向量空间;
- 接着,把已有的短语视为启动词,依次便利每个启动词,以启动词在词向量空间中的位置和指定长度半径寻找相似的新词语;
- 重复第三个步骤,将找到的新词语作为启动词继续挖掘,但这次减小半径长度;
- 不断重复第 3 和第 4 步骤,直到找不到新的词语。
上述做法与聚类算法中的 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))
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 中获得。
参考