本篇博客主要是记录在参加2023年第二届全国大学生数据分析大赛文本分析方向的历程。
比赛给出了关于网络平台的违规信息与非违规信息,要求参赛者针对这些违规与非违规信息进行数据分析与数据挖掘,然后利用给定的数据建立模型进行违规信息识别。
我首先按照题目的相关要求对给定数据进行了数据分析,然后基于LDA主题模型分析了主题,最后使用RBT3文本分类模型进行了训练与测试,并按赛题要求提交了test文件。
以下是相关代码与文字描述
完整的代码文件与相关模型可以查看我的代码仓库
Gitee(不包含RBT3权重):https://gitee.com/ming-ming-0201/text-analysis.git
百度网盘(包含权重):https://pan.baidu.com/s/1cX_SQgVXzj4RXU2z_7VoJw?pwd=x3m3
2023 年全国大学生数据分析大赛
B题 基于文本内容的违规信息识别
0 背景与挖掘目标
近年来,短视频平台越来越受到广大民众的喜爱。短视频的快节奏、视觉冲击力以及互动性,吸引了亿万人的眼球。但是随之而来的问题就是平台上涌现出了大量的低俗、暴力等违法违规内容。这些内容严重影响了社会公共道德和互联网生态,也让短视频平台面临着巨大的挑战。本案例通过对某短视频平台的大量文本数据进行采集、清洗、分析与挖掘,并通过基于深度学习的文本分类模型对文本进行分类训练,得到的模型能够检测色情、暴力、恐怖、政治敏感等内容,并能在短时间内处理大量的违规内容,从而维护社会公共道德和互联网生态。
1 整合数据
1.1 获取数据与数据合并
加载给定的违规数据与非违规数据,并将违规数据与非违规数据进行拼接整合为模型训练的数据集。实现代码见代码1。
import pandas as pd
import os
import matplotlib.pyplot as plt
from itertools import accumulate
#数据导入,设置第一行不为头标签并设置第一列数据为索引
# 获取违规数据
f_s = open('./train_sensitiveness.csv',encoding='gb18030')
# 获取非违规数据
f_i = open('./train_insensitiveness.csv',encoding='gb18030')
data_s = pd.read_csv(f_s)
data_i = pd.read_csv(f_i)
data_s.columns =["message","label"]
data_i.columns =["message","label"]
print(f'非违规数据一共有{len(data_i)}条','标签label为:1')
print(f'违规数据一共有{len(data_s)}条','标签label为:0')
# 数据合并
df = pd.concat((data_s, data_i), axis=0, join='inner')
print(f'数据合并后一共有{len(df)}条')
非违规数据一共有4503条 标签label为:1
违规数据一共有12223条 标签label为:0
数据合并后一共有16726条
2 文本数量分析
2.1 文本词长分析
统计拼接后的数据,将句子长度以及出现频次进行可视化。同时绘制句子长度累积分布图像。句子长度及出现频数统计图代码见代码2;句子长度累积分布函数图像代码见代码3。
# 统计样本长度
df['length'] = df['message'].apply(lambda x: len(x))
len_df = df.groupby('length').count()
sent_length = len_df.index.tolist()
sent_freq = len_df['message'].tolist()
plt.rcParams['font.sans-serif']=['SimHei'] #图中文字体设置为黑体
# 绘制句子长度及出现频数统计图
plt.figure(figsize=(5,5))
plt.bar(sent_length, sent_freq,2)
plt.title("样本长度及出现频数统计图", )
plt.xlabel("样本长度")
plt.ylabel("样本长度出现的频数")
plt.savefig("./样本长度及出现频数统计图.png")
plt.show
从上图可以看出,句子长度最多的区间大约为10到45,同时出现频次最多的句子超过250。
#绘制句子长度累积分布函数(CDF)
sent_pentage_list = [(count/sum(sent_freq)) for count in accumulate(sent_freq)]
plt.figure(figsize=(5,5))
# 绘制CDF
plt.plot(sent_length, sent_pentage_list)
# 寻找分位点为quantile的句子长度
quantile = 0.90
for length, per in zip(sent_length, sent_pentage_list):
if round(per, 2) == quantile:
index = length
break
print("\n分位点为%s的句子长度:%d." % (quantile, index))
# 绘制句子长度累积分布函数图
plt.plot(sent_length, sent_pentage_list,linewidth=4)
plt.hlines(quantile, 0, index, colors="c", linestyles="dashed",linewidth=4)
plt.vlines(index, 0, quantile, colors="c", linestyles="dashed",linewidth=4)
plt.text(0, quantile, str(quantile),fontsize=10)
plt.text(index, 0, str(index))
plt.title("样本长度累积分布函数图",fontsize=20)
plt.xlabel("样本长度",fontsize=15)
plt.ylabel("样本长度累积频率",fontsize=15)
plt.savefig("./样本长度累积分布函数图.png")
plt.show
分位点为0.9的句子长度:100.
从上图可以看出,大多数样本的句子长度集中在1-100之间,句子长度累计频率取0.9。
2.2 违规信息和非违规信息占比分析
计算违规数据与非违规数据分别占整体数据的比例并进行可视化。实现代码见代码4。
num = df['label'].value_counts() # 统计词频
str1 = ['违规数据','非违规数据']
plt.figure(figsize=(5, 5))
#plt.pie(num[:], autopct="%.2f %%", labels=num.index[:20])
plt.pie(num[:], autopct="%.2f %%", labels=str1)
plt.title('违规数据与非违规数据占比分析')
plt.savefig("./违规数据与非违规数据占比分析.png")
plt.show()
从上图可以看出,违规数据占整体数据73.08%,非违规数据占整体数据26.92%
3 文本内容分析
3.1 词云图分析
通过进行词云图可视化分析,分别分析违规数据、非违规数据、整体数据中出现频次最高的词语,实现代码见代码5。
# 分词并去除停用词
import jieba
import itertools
from wordcloud import WordCloud
import matplotlib.pyplot as plt
with open('./停用词库.txt', 'r', encoding='utf-8') as f:
stop = f.read()
stop = stop.split()
stop = [' ', '\n','没','好'] + stop
# 合并数据分词处理
data_cut = df['message'].apply(jieba.lcut) # 分词
# 去除停用词
data_after = data_cut.apply(
lambda x: [i for i in x if i not in stop]
)
# 违规数据分词处理
data_cut1 = data_s['message'].apply(jieba.lcut) # 分词
# 去除停用词
data_after1 = data_cut1.apply(
lambda x: [i for i in x if i not in stop]
)
# 非违规数据分词处理
data_cut2 = data_i['message'].apply(jieba.lcut) # 分词
# 去除停用词
data_after2 = data_cut2.apply(
lambda x: [i for i in x if i not in stop]
)
#print(data_after.head())
num = pd.Series(list(itertools.chain(*list(data_after)))).value_counts() # 统计词频
num1 = pd.Series(list(itertools.chain(*list(data_after1)))).value_counts()
num2 = pd.Series(list(itertools.chain(*list(data_after2)))).value_counts()
pic = plt.imread('./ikun.jpg')
wc = WordCloud(font_path='./simhei.ttf', background_color='White', mask=pic)
wc1 = WordCloud(font_path='./simhei.ttf', background_color='White', mask=pic)
wc2 = WordCloud(font_path='./simhei.ttf', background_color='White', mask=pic)
wc3 = wc.fit_words(num)
wc4 =wc1.fit_words(num1)
wc5 = wc2.fit_words(num2)
fig = plt.figure(figsize=(15,15))
sub1 = fig.add_subplot(131)
plt.imshow(wc3)
plt.title('整体数据词云图')
plt.axis('off')
sub1 = fig.add_subplot(132)
plt.imshow(wc4)
plt.title('违规数据词云图')
plt.axis('off')
sub1 = fig.add_subplot(133)
plt.imshow(wc5)
plt.title('非违规数据词云图')
plt.axis('off')
plt.savefig("./违规数据词云图.png")
plt.show()
通过以上词云图可视化,可以看出违规数据、非违规数据、整体数据中占比最大的词语。
3.2 违规信息次数前10
对违规数据中出现次数前10的进行详细输出与可视化,实现代码见代码6。
plt.bar(range(len(num[:10])), num[:10])
plt.xticks(range(len(num[:10])), num[:10].index, rotation=45)
plt.title('违规数据中词语出现次数前10')
plt.grid()
plt.savefig("./违规数据前10.png")
plt.show()
通过上图,可以看出违规数据中出现前10的词语有:黑人、歧视、中国、一个、恶心、美国、女性、真的、白人、黑
4. 主题模型分析
通过建立LDA(潜在狄利克雷分配)主题模型,对违规数据进行主题分析。
4.1 违规信息困惑度分析
主题困惑度分析并确定主题数量,首先是困惑度分析,分析困惑度并确定主题数量,困惑度分析实现代码见代码7。
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation
# 格式转换
s = data_after1.astype('string')
n_features = 300 #提取300个特征词语
tf_vectorizer = CountVectorizer(strip_accents = 'unicode',
max_features=n_features,
stop_words='english',
max_df = 0.5,
min_df = 10)
tf = tf_vectorizer.fit_transform(s)
plexs = []
scores = []
n_max_topics = 16
for i in range(1,n_max_topics):
print(i)
lda = LatentDirichletAllocation(n_components=i, max_iter=50,
learning_method='batch',
learning_offset=50,random_state=0)
lda.fit(tf)
plexs.append(lda.perplexity(tf))
scores.append(lda.score(tf))
n_t=15#区间最右侧的值。注意:不能大于n_max_topics
x=list(range(1,n_t+1))
plt.plot(x,plexs[0:n_t])
plt.xlabel("主题数")
plt.ylabel("困惑度")
plt.savefig("./困惑度曲线.png")
plt.show()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
根据上图可以看出,当主题数为2与8时有明显拐点,而主题为2时,整体主题数偏少,因此确定主题为8再进行后续分析。
4.2 构建模型分析
根据困惑度分析得到的主题数为8,构建LDA主题模型对违规数据进行分析,实现代码见代码8。
n_topics = 8
lda = LatentDirichletAllocation(n_components=n_topics, max_iter=50,
learning_method='batch',
learning_offset=50,
# doc_topic_prior=0.1,
# topic_word_prior=0.01,
random_state=0)
lda.fit(tf)
4.2.1打印各主题内容信息
打印出各主题信息中出现频次最高的25个词语并分析归纳出其主题,实现代码见代码9。
#from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
def print_top_words(model, feature_names, n_top_words):
tword = []
for topic_idx, topic in enumerate(model.components_):
print("Topic #%d:" % topic_idx)
topic_w = " ".join([feature_names[i] for i in topic.argsort()[:-n_top_words - 1:-1]])
tword.append(topic_w)
print(topic_w)
return tword
n_top_words = 25
#tf_feature_names = tf_vectorizer.get_feature_names()
tf_feature_names = tf_vectorizer.get_feature_names_out()
topic_word = print_top_words(lda, tf_feature_names, n_top_words)
Topic #0:
恶心 真的 一个 女生 感觉 台湾 骂人 男生 台湾人 四川人 特别 正确 口音 杀人 两个 东西 大陆 香港 喜欢 吵架 那种 广东 很多 四川 重男轻女
Topic #1:
女性 男性 女权 社会 性别歧视 侮辱 打架 男女 工作 回答 知乎 家暴 压迫 地位 性别 权利 女拳 一个 这是 男权 很多 尊重 平等 评论 现象
Topic #2:
上海 北京 外地人 垃圾 落后 看不起 地方 肯定 排外 说话 法律 女孩 威胁 城市 本地人 瞧不起 地区 讨论 暴力 打死 电影 浙江人 数据 可怕 事实
Topic #3:
黑人 美国 白人 警察 一个 亚裔 种族歧视 种族 犯罪率 华人 奴隶 非洲 犯罪 国家 抢劫 暴力 智商 反抗 美国黑人 华裔 地位 政治 黑鬼 社会 支持
Topic #4:
中国 日本 讨厌 国家 印度人 欺负 韩国 坏人 外国人 印度 民族 国外 一个 云南 厌恶 好人 很多 西方 人民 历史 种族歧视 国人 文化 广州 喜欢
Topic #5:
歧视 黑人 亚裔 种族歧视 白人 黄种人 反对 喜欢 别人 捂脸 亚洲 同性恋 华人 很多 飙泪 国内 逻辑 为啥 代表 欧美 原因 犹太人 不算 一种 难道
Topic #6:
男人 女人 强奸 教育 一个 孩子 恶心 好看 出轨 不行 文化 学校 接受 嫁给 居然 老婆 水平 环境 别人 导致 结婚 活该 问号 自私 老公
Topic #7:
河南人 东北 地域 河南 素质 井盖 一个 骗子 山东 新疆 山东人 不好 小偷 很多 真的 地方 别人 答主 印象 确实 女方 网上 呵呵 男方 朋友
根据各主题内容中词语出现的频次与具体内容,可分析归纳出其主题:
主题1:性别歧视
主题2:男权与女权
主题3:大城市排外现象
主题4:美国种族歧视
主题5:亚洲多国关系
主题6:性别歧视
主题7:性别与婚姻
主题8:地域歧视
4.2.2可视化主题分析
将8个主题进行可视化分析其相关性与内容。实现代码见代码10。
import pyLDAvis
import pyLDAvis.sklearn
from sklearn.feature_extraction import DictVectorizer
pyLDAvis.enable_notebook()
pic = pyLDAvis.sklearn.prepare(lda, tf, tf_vectorizer)
pyLDAvis.display(pic)
#pyLDAvis.save_html(pic, 'lda_pass'+str(n_topics)+'.html')
#pyLDAvis.display(pic)
pic = plt.imread('./img1.jpg')
fig = plt.figure(figsize=(15,15))
plt.imshow(pic)
plt.show()
通过上图分析可知,得到的8个模型相交偏少、说明8个主题模型的内容独立性较强。
5.建立违规信息的识别模型
通过给定的违规数据与非违规数据合并后得到的整体数据作为数据集,训练了一种基于深度学习的文本违规识别模型,该模型通过深度学习的方法进行训练,能够最大效率对给定数据进行快速违规识别。本识别模型为基于Transformer架构的rbt3文本分类模型,这是一种基于自注意力机制的深度学习模型,适用于各种自然语言处理任务。该模型具有上下文信息聚合能力强、多语言支持、识别精度高等优势。
5.1 数据集处理
由于整合后的整体数据在上述分析中增添了其他列,此处进行数据与处理,仅保留数据中的文本与类别标签。实现代码见代码11。
del df['length'] # 删除无关列
df = df.dropna()
5.2 模型设计
5.2.1导包
使用的模型基于transformers开发,因此需要导入相关组件。
from transformers import AutoTokenizer,AutoModelForSequenceClassification
import math
5.2.2 创建Dataset
from torch.utils.data import Dataset
class MyDataset(Dataset):
def __init__(self,data) -> None:
super().__init__()
self.data = data
self.data = self.data.dropna()
def __getitem__(self, index):
return self.data.iloc[index]["message"], self.data.iloc[index]["label"]
def __len__(self):
return len(self.data)
dataset = MyDataset(df)
5.2.3 划分数据集
将整体数据集按8:2的比例划分为训练集与验证集,其中训练集用于模型训练,验证集用于验证模型识别能力。实现代码见代码14。
from torch.utils.data import random_split
train_ratio, valid_ratio = 0.8, 0.2
len_dataset = dataset.__len__()
trainset, validset = random_split(dataset, lengths=[int(train_ratio * len_dataset), math.ceil(valid_ratio * len_dataset)])
len(trainset), len(validset)
(13380, 3346)
5.2.4 创建Dataloader
创建Dataloader,将数据集按批量导入模型,实现代码见代码15。
import torch
tokenizer = AutoTokenizer.from_pretrained("rbt3")
def collate_func(batch):
texts, labels = [], []
for item in batch:
texts.append(item[0])
labels.append(item[1])
inputs = tokenizer(texts, max_length=128, padding="max_length", truncation=True, return_tensors="pt")
inputs["labels"] = torch.tensor(labels)
return inputs
from torch.utils.data import DataLoader
trainloader = DataLoader(trainset, batch_size=32, shuffle=True, collate_fn=collate_func)
validloader = DataLoader(validset, batch_size=64, shuffle=False, collate_fn=collate_func)
5.2.5 创建模型与优化器
创建基于rbt3的文本分类模型,使用优化器为Adam。实现代码见代码16。
from torch.optim import Adam
model = AutoModelForSequenceClassification.from_pretrained("./rbt3/")
if torch.cuda.is_available():
model = model.cuda()
optimizer = Adam(model.parameters(), lr=2e-5)
Some weights of BertForSequenceClassification were not initialized from the model checkpoint at ./rbt3/ and are newly initialized: ['classifier.weight', 'classifier.bias']
You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.
5.2.6 训练与验证函数构建
构建模型训练与验证函数,其中设置训练迭代次数为15代。实现代码见代码17。
def evaluate():
model.eval()
acc_num = 0
with torch.inference_mode():
for batch in validloader:
if torch.cuda.is_available():
batch = {k: v.cuda() for k, v in batch.items()}
output = model(**batch)
pred = torch.argmax(output.logits, dim=-1)
acc_num += (pred.long() == batch["labels"].long()).float().sum()
return acc_num / len(validset)
def train(epoch=15, log_step=100):
acc_list = []
global_step = 0
for ep in range(epoch):
model.train()
for batch in trainloader:
if torch.cuda.is_available():
batch = {k: v.cuda() for k, v in batch.items()}
optimizer.zero_grad()
output = model(**batch)
output.loss.backward()
optimizer.step()
if global_step % log_step == 0:
print(f"ep: {ep}, global_step: {global_step}, loss: {output.loss.item()}")
global_step += 1
acc = evaluate()
acc_list.append(acc)
print(f"ep: {ep}, acc: {acc}")
return acc_list
5.3 模型训练
模型开始训练并打印出每一代相应验证输出精度。实现代码见代码18。
acc_y = train()
ep: 0, global_step: 0, loss: 0.6902273893356323
ep: 0, global_step: 100, loss: 0.21782542765140533
ep: 0, global_step: 200, loss: 0.36842605471611023
ep: 0, global_step: 300, loss: 0.0858207568526268
ep: 0, global_step: 400, loss: 0.43856436014175415
ep: 0, acc: 0.910041868686676
ep: 1, global_step: 500, loss: 0.4276908040046692
ep: 1, global_step: 600, loss: 0.15346968173980713
ep: 1, global_step: 700, loss: 0.22964982688426971
ep: 1, global_step: 800, loss: 0.3592093288898468
ep: 1, acc: 0.9178123474121094
ep: 2, global_step: 900, loss: 0.19287952780723572
ep: 2, global_step: 1000, loss: 0.1276102066040039
ep: 2, global_step: 1100, loss: 0.15349173545837402
ep: 2, global_step: 1200, loss: 0.133888378739357
ep: 2, acc: 0.919605553150177
ep: 3, global_step: 1300, loss: 0.05678051710128784
ep: 3, global_step: 1400, loss: 0.1400693655014038
ep: 3, global_step: 1500, loss: 0.05892172083258629
ep: 3, global_step: 1600, loss: 0.053844839334487915
ep: 3, acc: 0.9187089204788208
ep: 4, global_step: 1700, loss: 0.05965070053935051
ep: 4, global_step: 1800, loss: 0.08092837780714035
ep: 4, global_step: 1900, loss: 0.15395665168762207
ep: 4, global_step: 2000, loss: 0.0034637406934052706
ep: 4, acc: 0.9154214262962341
ep: 5, global_step: 2100, loss: 0.023091472685337067
ep: 5, global_step: 2200, loss: 0.03659356012940407
ep: 5, global_step: 2300, loss: 0.00926100742071867
ep: 5, global_step: 2400, loss: 0.13580285012722015
ep: 5, global_step: 2500, loss: 0.0952908992767334
ep: 5, acc: 0.9187089204788208
ep: 6, global_step: 2600, loss: 0.004672945477068424
ep: 6, global_step: 2700, loss: 0.047958239912986755
ep: 6, global_step: 2800, loss: 0.01123974658548832
ep: 6, global_step: 2900, loss: 0.010060840286314487
ep: 6, acc: 0.9213986992835999
ep: 7, global_step: 3000, loss: 0.08767503499984741
ep: 7, global_step: 3100, loss: 0.006569660734385252
ep: 7, global_step: 3200, loss: 0.013985740952193737
ep: 7, global_step: 3300, loss: 0.004442989826202393
ep: 7, acc: 0.9151225686073303
ep: 8, global_step: 3400, loss: 0.001173974247649312
ep: 8, global_step: 3500, loss: 0.0009066518978215754
ep: 8, global_step: 3600, loss: 0.007913547568023205
ep: 8, global_step: 3700, loss: 0.0015792080666869879
ep: 8, acc: 0.9124327898025513
ep: 9, global_step: 3800, loss: 0.012986725196242332
ep: 9, global_step: 3900, loss: 0.001142944791354239
ep: 9, global_step: 4000, loss: 0.024037053808569908
ep: 9, global_step: 4100, loss: 0.01758541353046894
ep: 9, acc: 0.9115362167358398
ep: 10, global_step: 4200, loss: 0.0029433395247906446
ep: 10, global_step: 4300, loss: 0.0012714763870462775
ep: 10, global_step: 4400, loss: 0.00482137780636549
ep: 10, global_step: 4500, loss: 0.0006058422732166946
ep: 10, global_step: 4600, loss: 0.13005594909191132
ep: 10, acc: 0.9127316474914551
ep: 11, global_step: 4700, loss: 0.23396670818328857
ep: 11, global_step: 4800, loss: 0.0035065975971519947
ep: 11, global_step: 4900, loss: 0.0020987631287425756
ep: 11, global_step: 5000, loss: 0.004210058134049177
ep: 11, acc: 0.9163179993629456
ep: 12, global_step: 5100, loss: 0.000648736022412777
ep: 12, global_step: 5200, loss: 0.00013069214764982462
ep: 12, global_step: 5300, loss: 0.0028381843585520983
ep: 12, global_step: 5400, loss: 0.0007729174685664475
ep: 12, acc: 0.9190077781677246
ep: 13, global_step: 5500, loss: 0.12422686070203781
ep: 13, global_step: 5600, loss: 0.0016292538493871689
ep: 13, global_step: 5700, loss: 0.004240889102220535
ep: 13, global_step: 5800, loss: 0.00028072798158973455
ep: 13, acc: 0.9202032685279846
ep: 14, global_step: 5900, loss: 0.03281307965517044
ep: 14, global_step: 6000, loss: 0.0006321219843812287
ep: 14, global_step: 6100, loss: 0.04177972674369812
ep: 14, global_step: 6200, loss: 0.02917810156941414
ep: 14, acc: 0.9130305051803589
可视化迭代验证精度,精度图实现代码见代码19。
# 验证精度图绘制
import numpy
j = []
x_list = [i for i in range(1, 16)]
for i in acc_y:
j.append(i.cpu())
plt.plot(x_list,j)
plt.ylabel('acc')
plt.xlabel('训练代数')
plt.savefig("./验证精度曲线.png")
plt.show()
通过上图分析,在7代效果最优,精度为0.921
5.4 模型测试
获取给定的测试数据,将测试文本输入到模型中,得到分类标签,并将分类标签写入给定数据的csv第一列中保存,实现代码见代码20。
from transformers import pipeline
import numpy as np
model.config.id2label = {0: "0", 1: "1"}
pipe = pipeline("text-classification", model=model, tokenizer=tokenizer, device=0)
data_test = open('./test.csv',encoding='gb18030')
test_txt = pd.read_csv(data_test)
csvDataList = np.array(test_txt)
# 得到所有预测标签
test_label = []
for i in csvDataList:
test_label.append(pipe(str(i))[0]['label'])
test_txt.insert(loc=0, column='', value=test_label)
test_txt.to_csv('test.csv',index=False)
test_txt.to_excel('test.xlsx', index=False)