聚类算法(四)—— 基于词语相似度的聚类算法(含代码)

tech2022-10-05  100

转载请注明出处

简单了解了下目前的一些聚类算法, 

 

聚类算法(一)——DBSCAN

聚类算法(二)—— 优缺点对比

聚类算法(三)—— 评测方法1

聚类算法(三)—— 评测方法2

聚类算法(三)—— 评测方法3(代码)

聚类算法(四)—— 基于词语相似度的聚类算法(含代码)

聚类算法(五)——层次聚类 linkage (含代码)

聚类算法(六)——谱聚类 (含代码)

 

 

目前了解的太少了,后面再进一步调研吧 /(ㄒoㄒ)/~~

然后当时就自己拍脑袋写了个基于词语相似度的聚类算法,主要一开始的时候,需求跟聚类还不太一样,类似于词语相似度,扩充词典那样,后来需求变啊变,就变成聚类了,索性就改吧改吧代码,拍出来一个算法

 

原理

1. 词与词之间相似度计算,可以用word2vec、fasttext词向量、词林相似度等等等等

2. 初始化,可以有少量已知的类别词语,也可以冷启动

3. 聚类方法,采用计算词语和类中词语平均相似度,根据阈值进行判断是否加入到当前类中,否则添加新的类别

步骤:

获取需要聚类的文本/词语对应的向量,如果采用word2vec,对于短语或句子,则取切词后词语的向量均值作为其向量,采用fasttext,则直接用句子向量初始化类别为空,第一个文本放到第一个类别对剩余文本进行聚类,判断与其对应的向量余弦相似度最近的类别(类别向量取类别中句子向量的平均值表示),如果相似度大于阈值,则加入此类别,否则当前文本作为新的类别。所有单一文本为一个类别的数据为此次聚类的未成功聚类文本

这种自举的算法虽然不能对所有文本进行聚合,但是能保证成功聚类的点的准确率,简单说,使得聚类的结果中类别内数据更干净。

另外发现,对于一些词语或短句效果有时候能达到意想不到的效果。使用的过程中,可以根据结果,进行相应的数据清洗,例如将一些无关的前后缀或短句中的聚类无关词语去除掉。 举个栗子,比如我聚类得到的一个类别,很多都是 “XX报道”,但是我其实更想将具体的相同的报道内容聚类到一起,那就直接把“报道”去掉就好了。那如果只是想单纯把“报道”相关和“刊物”相关区分,那就不需要去掉了。    

 

优缺点

优点:①比较灵活,不用设置类别数;②可以根据项目具体需求随意调节阈值,得到不同大小/紧密程度的类

缺点:①会有一些词语聚合不到相应的类别;②效果好坏对相似度算法依赖较大 ③ 需要人工对数据分析设置阈值

和KMeans相比, KMeans需要设置类别,如果想聚成很多大的类别,或者这个类别数目是好确定的而且相对来说每个类的数据量比较大,那选用KMeans比较好; 如果类中的数据量比较少,且类别数相对不好确定,而且数据中有很多异常点(不好归类的点), 或者是有很多可以分到多个类别的点,那采用这个方法会有一个比较好的效果。    

另外,如果不知道KMeans类别数怎么设置,你时间也ok的话,那不妨简单设置几个0-1之间的阈值试下这里的方法,看看哪个效果好,采用对应的大概类别数来跑个KMeans吧!

 

代码

相似度部分,这块目前是实现了fasttext 和word2vec词向量加载( 词向量请自行训练 训练代码可以参考这里  文本表示(二)—— word2vec 词向量训练代码  分类算法(二)—— FastText  文本表示(三)—— fasttext 词向量调用代码)

这里也可以改成其它相似度计算代码。 之前试过词林算法,相对来说比较慢,效果跟word2vec比差一点。

词林相似度 代码 : https://github.com/ashengtx/CilinSimilarity     https://github.com/240400968/hownet-similarity 两个版本,都可跑通。

# -*- encoding=utf-8 -*- ''' cluster words ''' import gensim import os import jieba import time import json import pandas as pd import numpy as np from tqdm import tqdm import copy import fasttext class Embedding(object): def __init__(self, model_file, embedding_size, type='w2v'): self.type = type self.embedding_size = embedding_size self.unk_w2v = np.random.randn(embedding_size) self.unexist = set() if type=='w2v': # 加载词向量模型 self.model = gensim.models.Word2Vec.load(model_file) self.vocab = self.model.wv.vocab.keys() print('vocab length: {}'.format(len(self.vocab))) elif type=='fasttext': self.model = fasttext.load_model(model_file) else: raise Exception('Embedding model type error!') def get_word_embedding(self, word): ''' 获取word的词向量 :param word: :return: ''' if self.type=='w2v': return self.get_word_w2v(word) elif self.type=='fasttext': return self.model.get_sentence_vector(word) return np.random.randn(self.embedding_size) def get_word_w2v(self, word): def norm(w2v): return w2v/np.linalg.norm(w2v) #返回单位向量 # 词语在vocab中,返回model[x] if word in self.vocab: return norm(self.model[word]) # 词语不在vocab中,返回分词后的模平均向量 word_cut = jieba.cut(word) word_cut = [x for x in word_cut] word_cut_w2v = [norm(self.model[x]) for x in word_cut if x in self.vocab] if len(word_cut_w2v) == 0: self.unexist.add(word) # 若分词后词语未在vocab中,添加到未知词中 return norm(self.unk_w2v) w2v = np.mean(word_cut_w2v, axis=0) return w2v

聚类算法代码

class WordsCluster(object): def __init__(self, embedding_file, embedding_size, embedding_type, sim_threshold=0.55, del_sim_threshold=0.2, ignore_keywords=[]): self.sim_threshold = sim_threshold self.del_sim_threshold = del_sim_threshold self.embedding_model = Embedding(embedding_file, embedding_size, embedding_type) self.ignore_keywords = ignore_keywords self.unexist = set() # 初始化embedding信息 def initial_embedding_info(self, keywords, collection_words_list): # 构建keywords的embedding词典 all_keywords = set(keywords) for class_words in collection_words_list: all_keywords.update(class_words) all_keywords_w2v_list = [(x, self.embedding_model.get_word_embedding(x)) for x in all_keywords] self.words_w2v_dic = dict(all_keywords_w2v_list) # 补充已知类词语的词向量数组 if len(collection_words_list) == 0: return [] collections = [] for collection_words in collection_words_list: collections.append({"words":collection_words, "words_w2v": [self.words_w2v_dic[x] for x in collection_words] }) return collections # 获取类标 def get_class_represent_word(self, collection): if len(collection) == 0: return None if len(collection) == 1: return collection[0] # 计算模平均,求模平均w2v相似度最高词语 sim_list = np.dot(collection['words_w2v'], np.mean(collection['words_w2v'], axis=0)) max_sim = np.max(sim_list) max_sim_index = np.where(sim_list == max_sim)[0][0] represent_word = collection["words"][max_sim_index] represent_word_sim = max_sim # 替换为词频较高分词结果 max_count_word = '' max_count = 0 seg_word_count_dic = {} for i, word in enumerate(collection["words"]): seg_words = jieba.lcut(word) for seg_word in seg_words: seg_word_count_dic.setdefault(seg_word, 0) seg_word_count_dic[seg_word] += 1 if seg_word_count_dic[seg_word] > max_count: max_count_word, max_count = seg_word, seg_word_count_dic[seg_word] if max_count > 1 and float(max_count) / len(collection["words"]) > 0.3 and \ seg_word_count_dic.get(represent_word, 0) != max_count and len(max_count_word) > 1: represent_word, represent_word_sim = max_count_word, float(max_count) / len(collection["words"]) return represent_word, represent_word_sim # 过滤 def filt_noise_words(self, collection): delete_noise_opinion_words_indexes = [] scores = [np.mean(np.dot(collection['words_w2v'], x)) for x in collection['words_w2v']] # 根据阈值过滤类中相似度较小词 for i in range(len(scores)): if scores[i] <= self.del_sim_threshold: delete_noise_opinion_words_indexes.append(i) collection['words_w2v'] = [x for (i, x) in enumerate(collection['words_w2v']) if i not in delete_noise_opinion_words_indexes] collection['words'] = [x for (i, x) in enumerate(collection['words']) if i not in delete_noise_opinion_words_indexes] def cluster(self, keywords, collection_words_list=[], sim_threshold=None): ''' keywords词语聚类 :param keywords_dic: :return: ''' result = [] if not sim_threshold: sim_threshold = self.sim_threshold collections = self.initial_embedding_info(keywords, collection_words_list) # 聚类,根据词语同类中所有词语相似度均值将keyword分类到collections类别中 un_seg_words = [] for keyword in tqdm(keywords): if keyword in self.embedding_model.unexist: un_seg_words.append(keyword) elif keyword in self.ignore_keywords: continue else: # 将keyword聚类到当前collection keyword_w2v = self.words_w2v_dic[keyword] if len(collections) <= 0: collections.append({"words": [keyword], "words_w2v": [keyword_w2v]}) continue similarity = [np.mean(np.dot(collection['words_w2v'], keyword_w2v), axis=0) for collection in collections] max_score = max(similarity) max_score_index = similarity.index(max_score) if max_score >= sim_threshold: collections[max_score_index]["words"].append(keyword) collections[max_score_index]["words_w2v"].append(keyword_w2v) else: collections.append({"words": [keyword], "words_w2v": [keyword_w2v]}) for ci, collection in enumerate(collections): represent_word, represent_word_sim = self.get_class_represent_word(collection) collection['represent_word'] = represent_word collection["represent_word_sim"] = represent_word_sim result.append({"words": collection['words'], "represent_word": represent_word, "represent_word_sim": represent_word_sim }) return result, un_seg_words def upgrade_cluster(self, keywords, collection_words_list=[], sim_threshold=None): if not sim_threshold: sim_threshold = self.sim_threshold for i in range(7, min(1, int(sim_threshold * 10) - 3), -1): collections, un_seg_words = self.cluster(keywords, collection_words_list, i * 0.1) keywords = un_seg_words collection_words_list = [x['words'] for x in collections] return collections, un_seg_words def sim(self, w1, w2): if w1 in self.embedding_model.unexist or w2 in self.unexist: return -1 w1_w2v = self.embedding_model.get_word_embedding(w1) w2_w2v = self.embedding_model.get_word_embedding(w2) return np.dot(w1_w2v, w2_w2v)

评测

方法相关评测参见:

聚类算法(三)—— 评测方法1

聚类算法(三)—— 评测方法2

聚类算法(三)—— 评测方法3(代码)

 

转载请注明出处

觉得有用,麻烦点个赞,喜欢我请关注!!!

最新回复(0)