舆情/热点聚类算法研究(二):基于word2vec、TF-IDF、Single-Pass实现舆情聚类

前言

随着互联网的快速发展,网络已经成为普通人日常生活的一部分,也成为企业和政府相关工作决策者辅助决策的重要信息来源。网络上海量的文本数据隐藏着人们所需要的大量信息,如何及时准确地从海量数据中发现所需要的信息是研究者致力于解决的问题。话题识别与聚类可以将多变且杂乱的文本数据(例如热点与舆情)汇聚到一起,然后通过数据聚类算法形成事件簇,进而完成话题的聚类。

数据准备:舆情/热点聚类算法研究(一):通过python爬虫实现舆情/热点数据准备


目录

前言

一、话题聚类的基本步骤

二、word2vec

2.1 word2vec介绍

2.2 word2vec模型语料库准备

2.3 word2vec模型训练

三、句子向量化处理

3.1 句子向量化

3.2 代码演示

四、Single-Pass聚类

4.1 算法原理

4.2 代码演示

五、完整代码

六、总结——改进与不足 

6.1 不足

6.2 向量化改进点

6.3 其他相关聚类算法


一、话题聚类的基本步骤

第一步:对原始数据进行预处理

第二步:句子向量化表示(Sent2vec

第三步:聚类

二、word2vec

2.1 word2vec介绍

Word2Vec是一种用来产生词向量的相关模型,属于深度学习领域。Word2Vec的模型有CBOW和Skip-Gram两种,前者通过背景词来预测中心词,后者通过中心词来预测背景词。通过训练,可以把对文本内容的处理简化为K维向量空间中的向量运算,而向量空间上的相似度可以用来表示文本语义上的相似度。因此,Word2Vec输出的词向量可以被用来做很多NLP相关的工作,比如聚类、找同义词、词性分析等等。 

但是要注意的是,word2vec对与句子向量化里句中单词的顺序和语义信息并不敏感,在句子向量化中会存在一定的局限性。

有关word2vec详细的讲解可以查看该篇文章:如何通俗理解Word2Vec (23年修订版)

2.2 word2vec模型语料库准备

对于舆情热点聚类的任务,首先要实现舆情热点的向量化,那么我们可以准备一个专项的语料库来进行特定领域的训练,通过上一节的内容对大量的数据进行爬取,获得语料库后进行数据的预处理,处理成一个词组列表用于训练自己的word2vec模型,这里我们主要通过jieba对文本进行分词处理。

python代码如下:

import os
import jieba

def preprocess_stopwords():  
    # 打开停用词文件  
    stopwords = set()  
    with open('停用词汇总.txt', 'r', encoding='utf-8') as f:  
        # 逐行读取文件  
        for line in f:  
            # 移除行尾的换行符并将单词添加到集合中  
            stopwords.add(line.strip())
        return stopwords

def preprocess_text(text):   
    words = jieba.lcut(text)  # 分词  
    words = [word for word in words if word not in preprocess_stopwords()]  # 去除停用词  
    return ' '.join(words)  # 以空格连接词语,方便后续处理 

# 预处理整个语料库
with open('textData','w',encoding = 'utf-8') as testData:
    with open('articaleData','r',encoding = 'utf-8') as Data:
        for doc in Data:
            line_seg = preprocess_text(doc)
            testData.write(line_seg)

2.3 word2vec模型训练

随后我们将处理好的数据作为训练集对模型进行训练:

from gensim import corpora, models, similarities, matutils
from gensim.models import Word2Vec 

# 读取语料库文件  
sentences = []  
with open('textData', 'r', encoding='utf-8') as file:  
    for line in file:  
        # 假设每行已经分词并用空格隔开  
        sentence = line.strip().split()  
        sentences.append(sentence)  

# 训练Word2Vec模型 
# 向量大小为150、窗口大小为5、 最小收录词频为2
model = Word2Vec(sentences, vector_size=150, window=5, min_count=2, workers=4)  
  
# 保存模型  
model.save("word2vec.model")  
  
# 加载模型  
model = Word2Vec.load("word2vec.model")  

词向量结果演示: 

word2vec_model.wv['女孩']

# [-1.5104203  -1.6679913   0.52640986 -1.4308913  -3.2634459   ...]

word2vec_model.wv.similarity('女孩', '女生') 

# 0.8700093

训练出来的模型性能和语料库高度相关,因此一般需要一个较大的语料库,当然越大的语料库训练成本就更高。在条件有限的情况下,我们也可以通过通过他人分享的预训练模型,再进行微调,可以节约大量成本。

此处分享一个预训练模型:268G+训练好的word2vec模型(中文词向量)

三、句子向量化处理

3.1 句子向量化

本文的句子向量化方法是通过TF-IDF对句子进行特征词提取,然后通过返回的系数与Word2vec加权来将句子进行向量化的处理,本质上是特征词向量的组合,此方法是句子向量化的一个简易的方法。

TF-IDF:词在句子中的权重 = tf(词频) * idf(逆文档频率)

tf=\tfrac{n}{N}   其中n为词在该文档中出现的频次,N为词在全部文档中出现的频次

idf=log(\tfrac{D}{d})  其中D为总文档数,d为词所在文档数

通过TF-IDF可以得到更能代表句子的词语(TF-IDF分数更高)。关于TF-IDF更详细的内容可以阅读:TF-IDF算法介绍及实现

3.2 代码演示

import gensim  
import numpy as np  
import jieba.analyse

model_path = 'word2vec.model'  # 替换为你的Word2Vec模型路径  
word2vec_model = gensim.models.Word2Vec.load(model_path)  

# 句子列表  
sentences = [  
    "1月29日一早,上海中环外圈上中路隧道不到300米,单车撞护栏后与另外两车相撞,事故占据4号车道,3辆事故车都需要牵引,后方通行缓慢。",  
    "在1月29日清晨,上海中环外圈上中路隧道附近发生了一起交通事故。一辆单车在撞上护栏后,又与另外两辆车发生了碰撞。这起事故占据了4号车道,导致三辆事故车辆都需要牵引清除。受此影响,后方的交通通行速度变得缓慢。",  
    "太气了,在乎的不是这点钱】1月28日,四川德阳,一女子爆料视频:老公在农贸市场买红萝卜8.9斤,被收了15元。女子在家复称只有4.4斤,少了一半。随后女子和老公找到商贩,商贩夫妻称看错了退了7元。",  
    "在1月29日清晨,上海中环外圈上中路隧道附近发生了一起交通事故" 
]  

# 计算句子向量(TF-IDF+Word2vec加权)
def cal_sentence2vec(sentence):
    response = jieba.analyse.extract_tags(sentence, topK=12, withWeight=True, allowPOS=())
    vector = np.zeros(150)
    for word in response:
        if word[0] in word2vec_model.wv.key_to_index:
            vector += word2vec_model.wv[word[0]]*word[1]
    return vector
  
# 计算句子之间的相似度(通过余弦相似度来表示,值越接近1表示越相似)
def cosine_similarity(vec1, vec2):  
    return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))  

for sentence in sentences:
    response = jieba.analyse.extract_tags(sentence, topK=12, allowPOS=())
    print(sentence)
    print(response)
    for i in range(0,len(sentences)):
        print(cosine_similarity(cal_sentence2vec(sentence), cal_sentence2vec(sentences[i])))

'''
1月29日一早,上海中环外圈上中路隧道不到300米,单车撞护栏后与另外两车相撞,事故占据4号车道,3辆事故车都需要牵引,后方通行缓慢。
['事故', '29', '300', '外圈', '中环', '单车', '两车', '相撞', '护栏', '车道', '中路', '牵引']
1.0
0.8035275005157771
0.10794818559785112
0.6998954673831901

在1月29日清晨,上海中环外圈上中路隧道附近发生了一起交通事故。一辆单车在撞上护栏后,又与另外两辆车发生了碰撞。这起事故占据了4号车道,导致三辆事故车辆都需要牵引清除。受此影响,后方的交通通行速度变得缓慢。
['事故', '29', '这起', '外圈', '中环', '单车', '两辆车', '三辆', '护栏', '发生', '车道', '交通事故']
0.8035275005157771
1.0000000000000002
0.1016901656565377
0.6179516660106243

太气了,在乎的不是这点钱】1月28日,四川德阳,一女子爆料视频:老公在农贸市场买红萝卜8.9斤,被收了15元。女子在家复称只有4.4斤,少了一半。随后女子和老公找到商贩,商贩夫妻称看错了退了7元。
['女子', '商贩', '老公', '太气', '28', '8.9', '15', '复称', '4.4', '红萝卜', '德阳', '爆料']
0.10794818559785112
0.1016901656565377
0.9999999999999998
0.09395750886237693

在1月29日清晨,上海中环外圈上中路隧道附近发生了一起交通事故
['29', '外圈', '中环', '交通事故', '中路', '清晨', '隧道', '附近', '一起', '上海', '发生']
0.6998954673831901
0.6179516660106243
0.09395750886237693
1.0
'''

四、Single-Pass聚类

4.1 算法原理

增量式Single-Pass算法是一种基于数据流的聚类算法,也被称为单遍聚类或单通道法。该算法只需扫描一次数据流,即可对数据进行聚类。

算法过程:

①读取一条数据,判断是否存在簇,若不存在则创建一个新簇,已该数据为簇中心;

②若已存在簇,则判断句子与簇中心的相似度是否大于设定的阈值;

③若大于设定的阈值,则归为同一簇,并更新簇中心;

④若小于设定的阈值,则创建一个新簇,并已该数据为簇中心;

⑤逐条遍历一次数据,聚类完成;

⑥观察聚类结果,调整阈值重复1-5步骤,并得到最佳阈值

对于大规模数据集,增量式Single-Pass算法的复杂度相对较高。但是,对于流式数据等连续、快速到达的数据流,增量式Single-Pass算法仍是一种高效且简洁的聚类方法。 此外,由于增量式Single-Pass算法具有输入次序依赖特性,其聚类结果会受到数据输入顺序的影响,不过,通过边缘文本的重聚类可以消除这个影响。所以在当前的背景下,Single-Pass能对目标任务有一个较好的完成效率。

4.2 代码演示

# 4. 实现增量式SinglePass聚类算法  
class SinglePassCluster:  
    def __init__(self, threshold=0.7):  
        self.threshold = threshold  
        self.centroids = []    # 记录簇中心
        self.count = []    # 记录簇中元素个数
  
    def assign_cluster(self, vector):  
        if not self.centroids:  
            self.centroids.append(vector)  
            self.count.append(1)  
            return 0  
  
        max_sim = -1  
        cluster_idx = -1  
        for idx, centroid in enumerate(self.centroids):  
            sim = cosine_similarity(vector, centroid)  
            if sim > max_sim:  
                max_sim = sim  
                cluster_idx = idx  
  
        if max_sim < self.threshold:  
            cluster_idx = len(self.centroids)  
            self.centroids.append(vector)  
            self.count.append(1) 
        else : # 重新计算中心,采用指数加权平均动态更新簇中心
            self.centroids[cluster_idx] = 0.1*vector + 0.9*self.centroids[cluster_idx]
            self.count[cluster_idx] += 1
        
        return cluster_idx
  
    def fit(self, doc_vectors):  
        clusters = [] 
        count = []
        for vector in doc_vectors:  
            cluster_id = self.assign_cluster(vector)  
            clusters.append(cluster_id)  
        return clusters,count  

doc_vectors = np.vstack([cal_sentence2vec(doc) for doc in sentences_data])  
 
# 5. 聚类并输出结果  
sp_cluster = SinglePassCluster(threshold=0.8)  
clusters,count = sp_cluster.fit(doc_vectors)  

五、完整代码

import os
import re
import json
import math
import numpy as np
from gensim import corpora, models, similarities, matutils
import pandas as pd
import jieba
import jieba.analyse
import csv
from gensim.models import Word2Vec 

######################################## 数据准备

sentences_data = []

# 测试数据集
sentences = [  
    "1月29日一早,上海中环外圈上中路隧道不到300米,单车撞护栏后与另外两车相撞,事故占据4号车道,3辆事故车都需要牵引,后方通行缓慢。",  
    "在1月29日清晨,上海中环外圈上中路隧道附近发生了一起交通事故。一辆单车在撞上护栏后,又与另外两辆车发生了碰撞。这起事故占据了4号车道,导致三辆事故车辆都需要牵引清除。受此影响,后方的交通通行速度变得缓慢。",  
    "太气了,在乎的不是这点钱】1月28日,四川德阳,一女子爆料视频:老公在农贸市场买红萝卜8.9斤,被收了15元。女子在家复称只有4.4斤,少了一半。随后女子和老公找到商贩,商贩夫妻称看错了退了7元。",  
    "在1月29日清晨,上海中环外圈上中路隧道附近发生了一起交通事故" ,
    "1月31日,徐闻县公安局发布通报,依法对插队砸车的奔驰车主王某作出行政拘留10日并罚款500元的处理",
    "网传插队砸车的奔驰车主系河北高校教师 校方回应",
    "经查,1月29日15时许,一辆白色小轿车与一辆黑色商务车在徐闻港排队购票上船期间,因通行顺序问题引发纠纷。黑色商务车乘客王某(男,40岁,河北省人) 下车站到白色小轿车前,拦车辱骂并用拳头打砸白色小轿车引擎盖,导致引擎盖凹陷。随后,双方各自驾车前行离开。",
    "#男子当小三破坏军婚获刑10个月#"
]  

# 待聚类数据集
with open('./testdata.txt','r',encoding='utf-8') as file: 
    # next(file)
    for sen in file:
        # print(sen)
        if sen not in ('','\n'):
            if len(sen)>5:
                sentences_data.append(sen)

# 计算句子向量(TF-IDF+Word2vec加权)
def cal_sentence2vec(sentence):
    response = jieba.analyse.extract_tags(sentence, topK=12, withWeight=True, allowPOS=())
    vector = np.zeros(128)
    for word in response:
        if word[0] in word2vec_model.wv.key_to_index:
            vector += word2vec_model.wv[word[0]]*word[1]
    return vector
  
# 计算句子之间的相似度  
def cosine_similarity(vec1, vec2):  
    return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))  

# 4. 实现增量式SinglePass聚类算法  
class SinglePassCluster:  
    def __init__(self, threshold=0.7):  
        self.threshold = threshold  
        self.centroids = []  
        self.count = []
  
    def assign_cluster(self, vector):  
        if not self.centroids:  
            self.centroids.append(vector)  
            self.count.append(1)  
            return 0  
  
        max_sim = -1  
        cluster_idx = -1  
        for idx, centroid in enumerate(self.centroids):  
            sim = cosine_similarity(vector, centroid)  
            if sim > max_sim:  
                max_sim = sim  
                cluster_idx = idx  
  
        if max_sim < self.threshold:  
            cluster_idx = len(self.centroids)  
            self.centroids.append(vector)  
            self.count.append(1) 
        else : # 重新计算中心
            self.centroids[cluster_idx] = 0.1*vector + 0.9*self.centroids[cluster_idx]
            self.count[cluster_idx] += 1
        
        return cluster_idx
  
    def fit(self, doc_vectors):  
        clusters = [] 
        count = []
        for vector in doc_vectors:  
            cluster_id = self.assign_cluster(vector)  
            clusters.append(cluster_id)  
        return clusters,count  

doc_vectors = np.vstack([cal_sentence2vec(doc) for doc in sentences_data])  
 
# 5. 聚类并输出结果  
sp_cluster = SinglePassCluster(threshold=0.8)  
clusters,count = sp_cluster.fit(doc_vectors)  

with open('聚类结果.txt','w',encoding = 'utf-8') as file:
    # 输出每个文档的聚类结果  
    for i in range(0,max(clusters)+1):
        print(f"-----------话题:{i}-------------")
        file.write(f"-----------话题:{i}-------------\n")
        j = 0
        for doc, cluster_id in zip(sentences_data, clusters): 
            if cluster_id==i:
                print(f"[{j}]--> {doc}")  
                file.write(f"[{j}]--> {doc}")
                j+=1
                # print(f"Document: {doc[:30]}... Cluster ID: {cluster_id}")  
        print("\n")
        file.write("\n")

六、总结——改进与不足 

6.1 不足

由于没有一个较大的标记测试集,对聚类结果无法量化,但是根据小规模结果观察,该方法在舆情聚类上还是有较好的性能,并且实现起来也比较简单。

6.2 向量化改进点

基于word2vec+TF-IDF加权来进行向量化的方法,可以快速的完成句子的向量化,并且实现简单,但是由于这两个对于词语的顺序和语义信息并不敏感,在句子向量化中会存在一定的局限性,当word2vec模型比较良好且根据目标任务进行特征词提取处理得当,才能得到一个较为优秀的结果。

在句子向量化这个模块,大家使用的更多的算法是BRET算法。BERT算法通过Transformer结构的自注意力机制来捕捉句子中的上下文信息。在训练过程中,BERT会通过双向训练来考虑整个句子的上下文信息,从而得到更加丰富和准确的句子向量表示。与传统的词向量表示相比,BERT得到的句子向量能够更好地捕捉句子的整体语义信息,而不是仅仅依赖于单个词的含义。因此在句子向量化中往往能得到一个较好的结果,对其下游任务也能更加准确。

当然,现在还有很多优秀的开源的预训练模型,对句子向量化有一个更精确的结果。可以在ModelScope社区中找到很多优秀的预训练模型,使用起来也非常便利。

6.3 其他相关聚类算法

当前主流的聚类算法有:K-means聚类、DBSCAN算法、层次聚类算法等。

K-means聚类通过设置K值,也为簇数,根据距离向量不断动态更新簇中心,可以在很少的迭代次数收敛,但是由于K值的选择较为复杂,且在当前背景下,对于动态更新的数据,K值的选择就更为困难,所以暂不使用K-means算法。

DB-SCAN算法是基于密度的聚类算法,通过递归地填充密度相连的区域来发现簇。该算法能够发现任意形状的簇,并且对噪声点具有较强的鲁棒性。

层次聚类算法是通过不断地将相近的数据点合并成新的簇,直到满足一定的终止条件。该算法能够发现不同大小和形状的簇,但时间复杂度较高。

这些算法在聚类任务上各有优势,对于话题的聚类也有应用场景,有待进一步挖掘。

  • 40
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值