网易云音乐《成都》评论的文本聚类与分类
❗❗❗本文最后更新于 147 天前,其中的信息可能已经过时;如有错误请在文章下方评论✅,欢迎纠错🥰!
本文使用的编辑器为Jupyter Notebook

数据来源

该首歌曲的链接为https://music.163.com/#/song?id=436514312,此链接页面底部即为评论区,评论区首页为35条(15条精彩评论+20条按时间排序评论),其他页为20条按时间排序评论。采集过程初期,拟使用requests和BeautifulSoup模块并调用网易云音乐的API接口进行爬取,但由于API接口的调用方式为POST,并且使用AES加密,采集难度偏大,且本项目的重点内容不在于采集数据部分。因此最后调整思路,使用selenium和edge浏览器进行采集。

DataCollection.py如下:

from selenium import webdriver  # 拟人爬取数据模块
from bs4 import BeautifulSoup  # 基于selenium模块获取到的数据解析工具
from tqdm import tqdm  # 进度条模块
from selenium.webdriver.common.by import By  # 控制浏览器模拟鼠标动作模块
from selenium.webdriver.edge.service import Service  # selenium模块启动edge浏览器服务
import time  # 时间模块
import re  # 正则表达式模块
from selenium.webdriver.common.action_chains import ActionChains  # 控制浏览器模拟鼠标动作模块
import csv  # csv模块


def getCommentsAndWrite(href):
    edgedriver = Service('msedgedriver.exe')  # 调用edge浏览器驱动程序
    edgedriver.start()  # 打开浏览器
    browers = webdriver.Remote(edgedriver.service_url)
    file = open('网易云音乐评论1.csv', mode='w', encoding='utf-8')

    browers.get(href)
    time.sleep(2)
    # 数据解析
    page_source = browers.page_source
    soup = BeautifulSoup(page_source, 'html.parser')
    # 查找iframe元素
    iframe = browers.find_element(By.NAME, 'contentFrame')
    # 切换到iframe中
    browers.switch_to.frame(iframe)
    givenButton = browers.find_element(By.XPATH, '/html/body/div[3]/div[1]/div/div/div[2]/div/div[2]/div[3]/div/a[9]')
    for i in tqdm(range(0, 3392), desc="点击进度", colour='green'):
        givenButton.click()
        time.sleep(1)
    time.sleep(10)
    # 获取完当页评论后,点击下一页,使用while循环,直到没有下一页
    while True:
        time.sleep(3)
        # 数据选择丨数据预处理
        # 整个评论区域
        # 用selector定位到评论区域
        div0 = browers.find_element(By.XPATH, '/html/body/div[3]/div[1]/div/div/div[2]/div/div[2]/div[2]')
        div0BS = BeautifulSoup(div0.get_attribute('innerHTML'), 'html.parser')
        div1 = div0BS.find_all('div', attrs={'class': 'itm'})
        # 当页评论
        for item in div1:
            div2 = item.find('div', attrs={'class': 'cnt f-brk'}).get_text()
            # 将div2的内容从第一个冒号开始截取
            div2 = div2.split(':')[1]
            # 将div2中的 替换为空格
            div2 = div2.replace(' ', ' ')
            # 将div2的内容写入csv文件
            file.write(div2 + '\n')
        # 使用By模块定位链接文字“下一页”
        nextPage = browers.find_element(By.LINK_TEXT, '下一页')
        ActionChains(browers).move_to_element(nextPage).click().perform()
        # 判断是否有下一页,如果没有下一页,跳出循环
        next_button_class = browers.find_element(By.LINK_TEXT, '下一页').get_attribute('class')
        if re.match(r'zbtn\s+znxt.+.+js-disabled', next_button_class):
            break
        else:
            continue
    file.close()


def dataProcessing():
    # 读取评论
    commentsFile = open("O:\\北京石油化工学院\\2023春\\文本分析与大数据可视化\\data\\ChengDu.csv", mode="r",
                        encoding="UTF8")
    commentsList = commentsFile.readlines()
    commentsFile.close()

    resultFile = open("O:\\北京石油化工学院\\2023春\\文本分析与大数据可视化\\data\\ChengDu(Pro).csv", mode="w",
                      encoding="UTF8")

    for comments in tqdm(commentsList, desc="数据清洗进度", colour="green"):
        # 空行处理
        if comments == "\n":
            pass
        # 正常评论处理
        else:
            # 过滤 方括号表情
            brackets_pattern = re.compile(r'\[.*?]')  # 匹配方括号及其中的内容
            comments = brackets_pattern.sub('', comments)
            # 匹配中文字符,取出中文(过滤emoji表情)
            pattern = re.compile(r'[^\u4e00-\u9fa5]')  # 匹配所有非中文字符
            comments = pattern.sub('', comments)
            if comments == "":
                pass
            else:
                resultFile.write(comments + "\n")


def concatCSV():
    # 打开第一个CSV文件并读取其中的所有数据
    with open('data/ChengDu1.csv', 'r', encoding="UTF8") as file1:
        csv_reader = csv.reader(file1)
        data1 = list(csv_reader)

    # 打开第二个CSV文件并读取其中的所有数据,并进行倒序操作
    with open('data/ChengDu2.csv', 'r', encoding="UTF8") as file2:
        csv_reader = csv.reader(file2)
        data2 = list(csv_reader)[::-1]

    # 将第二个CSV文件的数据倒序添加到第一个CSV文件的数据之后
    data = data1 + data2

    # 将新的数据写入一个新的CSV文件,或将其覆盖现有的第一个CSV文件
    with open('data/ChengDu.csv', 'w', newline='', encoding="UTF8") as file:
        csv_writer = csv.writer(file)
        csv_writer.writerows(data)


def main():
    startTime = time.time()  # 记录数据采集开始时间
    print("数据采集开始——————————")
    getCommentsAndWrite('https://music.163.com/#/song?id=436514312')
    endTime = time.time()  # 记录数据采集结束时间
    print("数据采集结束——————————")
    print("数据采集总耗时:{:.2f}秒".format(endTime - startTime))
    startTime = time.time()  # 记录数据清洗开始时间
    print("数据清洗开始——————————")
    dataProcessing()
    endTime = time.time()  # 记录数据清洗结束时间
    print("数据清洗结束——————————")
    print("数据清洗总耗时:{:.2f}秒".format(endTime - startTime))
    startTime = time.time()  # 记录数据合并开始时间
    print("数据合并开始——————————")
    concatCSV()
    endTime = time.time()  # 记录数据合并结束时间
    print("数据合并结束——————————")
    print("数据合并总耗时:{:.2f}秒".format(endTime - startTime))


if __name__ == '__main__':
    main()

文本聚类

语料加载

引入所需要的Python 依赖库

import jieba
import pandas as pd
from sklearn.feature_extraction.text import TfidfTransformer 
from sklearn.feature_extraction.text import TfidfVectorizer 
import matplotlib.pyplot as plt 
from sklearn.decomposition import PCA 
from sklearn.cluster import KMeans
from sklearn import metrics
import warnings
warnings.filterwarnings("ignore")

加载停用词字典

加载stopwords.txt文件。

#加载停用词
stopwords=pd.read_csv('data\\stopwords.txt',index_col=False,quoting=3,sep="\t",names=['stopword'], encoding='utf-8')
stopwords=stopwords['stopword'].values

加载语料

加载csv文件,删除nan行,并提取要分词的content列转换为list列表。

#加载语料
music_df = pd.read_csv('data\\ChengDu(Ultra).csv',encoding='utf-8', header=None, names=['content'])
#删除语料的nan行
music_df.dropna(inplace=True)
#转换
music = music_df['content'].tolist()
music_df.head()

分词、去停用词、特征向量提取

定义分词、去停用词的函数

content_lines为语料列表;sentences 为预先定义的 list,用来存储分词后的结果。

#定义分词和打标签函数preprocess_text
    #参数content_lines即为上面转换的list
    #参数sentences是定义的空list,用来储存打标签之后的数据
def preprocess_text(content_lines, sentences):
    for line in content_lines:
        segs = jieba.lcut(line)
        segs = [v for v in segs if not str(v).isdigit()]  #去数字
        segs = list(filter(lambda x: x.strip(), segs))  #去左右空格
        segs = list(filter(lambda x: len(x) > 1, segs))  #长度为1的字符
        segs = list(filter(lambda x: x not in stopwords, segs))  #去掉停用词
        sentences.append((" ".join(segs)))  # 打标签

调用函数、生成训练数据

#调用函数,生成训练集
sentences = []
preprocess_text(music,sentences)

抽取数据查看结果

生成训练集同时观察一下前十条数据。

for sentence in sentences[:10]:
    print(sentence)

抽取特征

将文本中的词语转换为词频矩阵,统计每个词语的tf-idf权值,获得词在对应文本中的tf-idf权重。

#将文本中的词语转换为词频矩阵 矩阵元素a[i][j] 表示j词在i类文本下的词频
vectorizer = TfidfVectorizer(sublinear_tf=True, max_features=23)
# 统计每个词语的tf-idf权值
transformer = TfidfTransformer()
# 计算tf-idf
tfidf = vectorizer.fit_transform(sentences)
# 获取词袋模型中的所有词语
word = vectorizer.get_feature_names()
print(word)
# 将tf-idf矩阵抽取出来,元素w[i][j]表示j词在i类文本中的tf-idf权重
weight = tfidf.toarray()
#查看特征大小
print ('Features length: ' + str(len(word)))

选择算法、聚类、评测

TF-IDF的中文文本 K-means 聚类

numClass = 11  # 聚类分几簇
clf = KMeans(n_clusters=numClass,max_iter=10000,init="k-means++",tol=1e-6)
pca = PCA(n_components=2)  # 输出两维
TnewData = pca.fit_transform(weight)
s = clf.fit(TnewData)
print("s:",s)

定义聚类结果

利用可视化函数plot_cluster,该函数包含3个参数,其中result表示聚类拟合的结果集;newData表示权重weight降维的结果,这里需要降维到2维,即平面可视化;numClass表示聚类分为几簇,绘制代码第一部分绘制结果newData,第二部分绘制聚类的中心点。

# 绘制聚类中心
def plot_cluster(result, newData, numClass):
    plt.figure(figsize=(6, 4))
    Lab = [[] for i in range(numClass)]
    index = 0
    for labi in result:
        Lab[labi].append(index)
        index += 1
    color = ['oy', 'ob', 'og', 'cs', 'ms', 'bs', 'ks', 'ys', 'yv', 'mv', 'bv', 'kv', 'gv', 'y', 'm', 'b', 'k', 'g'] * 3
    for i in range(numClass):
        x1 = []
        y1 = []
        for ind1 in newData[Lab[i]]:
            try:
                y1.append(ind1[1])
                x1.append(ind1[0])
            except:
                pass
        plt.plot(x1, y1, color[i])
    #绘制初始中心点
    x1 = []
    y1 = []
    for ind1 in clf.cluster_centers_:
        try:
            y1.append(ind1[1])
            x1.append(ind1[0])
        except:
            pass
    plt.plot(x1, y1, "rv")  # 绘制中心
    plt.show()

可视化

对数据降维到2维,最后绘制聚类结果。

pca = PCA(n_components=2)#输出二维
newData = pca.fit_transform(weight)
plot_cluster(result,newData,numClass)

聚类结果中能看到是分了11个中心点和11个簇,右边的几簇聚类效果较差。

其中前4类极其接近,整体聚类效果在可接受范围内。

算法评测

查看聚类结果、贴标签

result = list(clf.predict(TnewData))#查看聚类结果
print(result)

music_df['labels'] = result  # 贴标签
music_df.head()

通过显示更多结果,我们可以分析到,聚类结果比较符合我们的认知。

存储到本地

#存储
music_df.to_csv('data\\labels.csv')

KMeans算法效果评价

def km_sse_cs():
    """
    KMeans算法效果评价
    1、簇内误方差(SSE, sum of the squared errors),手肘法,肘部法,其大小表明函数拟合的好坏。
    使用图形工具肘部法,根据簇的数量来可视化簇内误方差。下降率突然变缓时即认为是最佳的k值(拐点)。
    当KMeans算法训练完成后,可以通过使用内置inertia属性来获取簇内的误方差。
    2、轮廓系数法(Silhouette Coefficient)结合了聚类的凝聚度(Cohesion)和分离度(Separation)
    平均轮廓系数的取值范围为[-1,1],系数越大,聚类效果越好。当值为负时,暗含该点可能被误分了。
    :return:
    """
    # 存放设置不同簇数时的SSE值
    sse_list = []
    # 轮廓系数
    silhouettes = []
    
    data=TnewData
    # 循环设置不同的聚类簇数
    for i in range(2, 20):
        model = KMeans(n_clusters=i)
        model.fit(data)
        # kmeans算法inertia属性获取簇内的SSE
        sse_list.append(model.inertia_)
        # 轮廓系数
        silhouette = metrics.silhouette_score(data, model.labels_, metric='euclidean')
        silhouettes.append(silhouette)
    # 绘制簇内误方差曲线
    plt.subplot(211)
    plt.title('KMeans 簇内误方差')
    plt.plot(range(2, 20), sse_list, marker='*')
    plt.xlabel('簇数量')
    plt.ylabel('簇内误方差(SSE)')
    # 绘制轮廓系数曲线
    plt.subplot(212)
    plt.title('KMeans 轮廓系数')
    plt.plot(range(2, 20), silhouettes, marker='o')
    plt.xlabel('簇数量')
    plt.ylabel('轮廓系数')

    plt.tight_layout()
    plt.show()

if __name__ == '__main__':
    import matplotlib as mpl
    # 汉字字体,优先使用楷体,如果找不到楷体,则使用黑体
    mpl.rcParams['font.sans-serif'] = ['KaiTi', 'SimHei', 'FangSong']
    # KMeans算法
    km_sse_cs()

通过簇内误方差我们得出最佳K值为2左右,但由于聚类数据量达20000.通过轮廓系数我们可以知道在簇数量为11左右的时候聚类效果最好。但簇不利于我们进行尝试,所以我们选择了次选项,把后续的内容定为分成11簇。

文本分类

语料加载

引入库

import jieba  # 分词包
import matplotlib.pyplot as plt  # 数据可视化
import pandas as pd  # 数据分析
import seaborn as sns  # 数据可视化
from sklearn.feature_extraction.text import CountVectorizer  # 特征提取
from sklearn.model_selection import train_test_split  # 数据集划分
from sklearn.metrics import confusion_matrix  # 混淆矩阵
# 定义朴素贝叶斯模型,然后对训练集进行模型训练,直接使用 sklearn 中的 MultinomialNB
from sklearn.naive_bayes import MultinomialNB
import warnings  # 忽略警告
import random  # 随机数
plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False  # 解决保存图像是负号'-'显示为方块的问题
warnings.filterwarnings("ignore")  # 忽略警告

加载停用词字典

# 加载停用词
stopwords = pd.read_csv('data\\stopwords.txt', index_col=False, quoting=3, sep="\t", names=['stopword'], encoding='utf-8')
stopwords = stopwords['stopword'].values

停用词字典集我们也利用了去除特殊词汇的字典,因为收集到的评论有小部分是中性无关词语。

加载语料

# 加载语料
music_df = pd.read_csv('data\\labels.csv', encoding='utf-8')
music_df.head()
# 删除语料的nan行
music_df.dropna(inplace=True)
content = music_df['content'].tolist()
labels = music_df['labels'].tolist()

分词、去停用词、特征向量提取

定义分词、去停用词和批量打标签的函数

函数包含3个参数:content_lines参数为语料列表;sentences参数为预先定义的list,用来存储分词并打标签后的结果。

#定义分词和打标签函数preprocess_text
#参数content_lines即为上面转换的list
#参数sentences是定义的空list,用来储存打标签之后的数据
def preprocess_text(content_lines, sentences):
    for line in content_lines:
        segs = jieba.lcut(line)
        segs = [v for v in segs if not str(v).isdigit()]  #去数字
        segs = list(filter(lambda x: x.strip(), segs))  #去左右空格
        segs = list(filter(lambda x: len(x) > 1, segs))  #长度为1的字符
        segs = list(filter(lambda x: x not in stopwords, segs))  #去掉停用词
        sentences.append((" ".join(segs)))  # 打标签

调用函数、生成训练数据

# 转换
music = music_df['content'].tolist()
#调用函数,生成训练集
sentences = []
preprocess_text(music, sentences)

抽取数据查看结果

将得到的数据集打散,生成更可靠的训练集同时观察一下前20条数据。

random.shuffle(sentences)
for sentence in sentences[:20]:
    print(sentence[0], sentence[1])  #下标0是词列表,1是标签

抽取词向量特征

抽取词袋模型特征

vec = CountVectorizer(
    analyzer='word',  # tokenise by character ngrams
    max_features=23,  # keep the most common 1000 ngrams
)

sk-learn 对数据切分,分成训练集和测试集

x, y = zip(*sentences)
x_train, x_test, y_train, y_test = train_test_split(x, y, random_state=42)
len(x_train)

把训练数据转换为词袋模型

#把训练数据转换为词袋模型:
vec.fit(x_train)

选择算法、训练

定义朴素贝叶斯模型,然后对训练集进行模型训练,直接使用 sklearn 中的 MultinomialNB。

classifier = MultinomialNB()
classifier.fit(vec.transform(x_train), y_train)

评测、可视化

评估、计算 AUC 值

#评估、计算 AUC 值
print(classifier.score(vec.transform(x_test), y_test))

得到的评分结果为:0.9434113177364527

调整训练集测试集比例后评分在0.93到0.98之间,没有太大差别,虽然能正常得出结果但也可以看出我们的语料库存在问题。不能很好的进行分析。后续的预测错误率应该会比较大。综合来看主要错误率会体现在后四类中。

进行测试集的预测

# 进行测试集的预测:
pre = classifier.predict(vec.transform(x_test))
pre

可视化

混淆矩阵

# 混淆矩阵
# 真实标签
y_true = y_test
# 预测标签
y_pred = classifier.predict(vec.transform(x_test)).tolist()
c = confusion_matrix(y_true, y_pred)
c

热力图

# 绘制热力图
# fmt参数用于指定数值的格式
# cmap参数用于指定颜色映射
# annot参数用于指定是否在热力图中显示数值
# 绘制热力图
plt.figure(figsize=(8, 6))
sns.heatmap(c, annot=True, cmap='summer', fmt='d', vmax=300)
plt.xlabel('预测标签')
plt.ylabel('真实标签')
plt.show()

混淆矩阵热力图主要是看对角线上的数值,在8:2比例的测试集训练集设置下,正确预测1、2、4、7、8类的准确率很高,当然我们也可以预见其他类被错误预测的结果,导致9、10类的预测结果非常差。调整比例后也是如此,正确预测的数量主要还是第一类。抛开算法的因素,虽然结果不太准确,但总体的过程和结果比较符合我们的预期。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇