基于词向量的相似度短语挖掘

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

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

实现思路

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

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

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

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

DBSCAN

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

具体实现

首先,导入所需的包:

import json
import jieba
import numpy as np
from queue import Queue
from gensim.models import Word2Vec

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

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

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)

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

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)

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

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 相似度阈值增加系数来获得不同的挖掘结果。

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

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

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

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

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

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

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 中获得。

参考

  • 新词发现:https://blog.csdn.net/weixin_43378396/article/details/103848628
  • 聚类算法之DBSCAN算法之一:经典DBSCAN:https://blog.csdn.net/vainfanfan/article/details/83116589
  • 分享一次专业领域词汇的无监督挖掘:https://spaces.ac.cn/archives/6540
  • 腾讯抗黑灰产——自监督发现行话黑词识别一词多义:https://www.jiqizhixin.com/articles/2019-11-27
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 成长之路 设计师:Amelia_0503 返回首页