一、引言
在当今数字社交时代,微博已经成为人们分享生活、观点和信息的主要平台之一。然而,伴随着微博的广泛使用,垃圾信息、广告以及无关紧要的内容也开始泛滥。当我们想要在庞大而多样的微博签到文本中寻找有用信息时,面临的一个挑战是识别和过滤掉其中的垃圾文本,例如广告、噪音和无关紧要的内容。这个任务是如何处理和过滤这些文本,以提供更有价值的信息。
微博分类任务是一个有趣且具有挑战性的问题,它要求我们应用最新的自然语言处理技术,如BERT,来自动识别和分类微博签到文本。在这个任务中,我们的目标是建立微博文本分类模型,能够自动将微博文本分为两个主要类别:垃圾文本和非垃圾文本。通过实现这一目标,我们可以提供更清洁和有用的微博内容。
本任务我们将深入探讨微博分类任务的各个方面,包括数据预处理、模型训练和性能评估等关键步骤。我们将展示如何利用BERT这一先进的自然语言处理模型,结合监督学习技术,来解决这一实际问题。
BERT(Pre-training of Deep Bidirectional Transformers for
Language Understanding)是一种预训练的深度学习模型,能够理解自然语言的上下文,因此在文本分类、问答、命名实体识别等各种NLP任务上表现出色。建议大家阅读原文了解更多细节。
标注数据、预测数据和bert-base-chinese模型
可以在下方链接获取:
链接:https://pan.baidu.com/s/1c9o0AUJBrQaiM0MlOPOFOg?pwd=1111
二、模型训练
2.1 导入包
import os
os.environ['TRANSFORMERS_CACHE'] = 'My_Model'
os.environ['CUDA_LAUNCH_BLOCKING'] = "1"
import pandas as pd
import numpy as np
import torch
from torch.utils.data import TensorDataset, DataLoader, RandomSampler, SequentialSampler
import transformers
from transformers import BertConfig, BertTokenizer, BertForSequenceClassification, AdamW, get_linear_schedule_with_warmup
from tqdm import trange, notebook
from tqdm import *
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score,f1_score, roc_auc_score
import warnings
warnings.filterwarnings("ignore")
print(f'torch version: {torch.__version__}\ntransformers version: {transformers.__version__}')
实验环境的 torch 和 transformer 版本
torch version: 2.0.1+cu117
transformers version: 4.27.1
2.2 导入标注数据
# 目标:保留话题的文字、移除poi超链接、视频超链接、保留表情图片中的标题文字、移除其他html标签
def prepare(text):
import re
# 删除包含 wbicon 的 <i> 标签以及它们之间的内容
tokens = re.sub(re.compile(r'(<i\s+.*?wbicon.*?>.*?</i>)', re.S), '', text)
# 删除与 HTML 匹配的标签,包括尖括号 < 和 > 之间的内容
tokens = re.sub(re.compile(r'<(.*?)>', re.S), '', text)
# 保留 <img title="xxx"> 中的 title 信息
tokens = re.sub(re.compile(r'<img.*?(alt=["|\']{0,1}(.*?)["|\']{0,1}|title=["|\']{0,1}(.*?)["|\']{0,1})\s+.*?>', re.S|re.M), '\g<2>', text).strip()
# 将匹配到的 <br/> 替换为 \n
tokens = re.sub(re.compile(r'<br/>',re.S),'\n',text)
# 移除文本内的连续重复内容
# 移除连续发生3次及以上次数的重复性内容
# 重复内容的字符串长度>=3
tokens = re.sub(re.compile(r'([\s|\S]{2,}?)\1{2,}',re.S|re.M),'\g<1>',text)
# 移除 poi链接、视频链接、直播链接
urls=re.findall(r"<a.*?href=.*?<\/a>", text, re.I|re.S|re.M)
url=[u for u in urls if '>2<' in u or 'location_default.png' in u or '视频</a>' in u or '视频</span></a>' in u or '直播</a>' in u]
if len(url)>0:
for u in url:
tokens=text.replace(u,'')
return tokens
df_label = pd.read_csv('data/weibo_label.csv')
# pandarallel 可以加速 pandas 运算速度
import psutil
from pandarallel import pandarallel
pandarallel.initialize(nb_workers=psutil.cpu_count(logical=False))
# 数据处理,调用了上面定义的 prepare 函数
df_label['message'] = df_label['message'].parallel_apply(prepare)
df_label.sentiment.value_counts() # 查看各标签文本数量
sentiment
0 2771
1 2102
-1 683
6 638
Name: count, dtype: int64
- 0表示中性文本
- 1表示正面情感文本
- -1表示负面情感文本
- 6表示垃圾文本
再进行处理,重新定义标签,我们将6赋值为0表示垃圾文本;不为6的标签赋值为1表示非垃圾文本
df = df_label.copy()
df.loc[df[df.sentiment!=6].index,'sentiment'] = 1
df = df[(df['sentiment']==1)|(df['sentiment']==6)]
# 随机丢弃标注列表中量较多的数据,以保持二者的标注量基本相同,提高后期模型预测的准确率
drop_size = len(df[df['sentiment']==1].sentiment)-len(df[df['sentiment']==6].sentiment)
df.drop(df[df['sentiment']==1].sample(drop_size).index, inplace=True)
df.loc[df[df.sentiment==6].index,'sentiment'] = 0
df.sample(10)
2.3 模型初始化
model_name = 'bert-base-chinese'
# 下面三个文件的路径为 bert-base-chinese 文件夹,根据自己的存储路径更换
config = BertConfig.from_pretrained('../model/'+model_name, finetuning_task='binary') # BERT 模型配置
tokenizer = BertTokenizer.from_pretrained('../model/'+model_name) # BERT 的分词器
model = BertForSequenceClassification.from_pretrained('../model/'+model_name, num_labels=2) # BERT 的文本分类模型
# 用于将文本转换为BERT模型的输入标记
def get_tokens(text, tokenizer, max_seq_length, add_special_tokens=True):
# 使用分词器将文本转换为模型可以接受的输入格式
input_ids = tokenizer.encode(text,
add_special_tokens=add_special_tokens,
truncation=True,
max_length=max_seq_length,
pad_to_max_length=True)
# 创建一个关注掩码,标记哪些标记是真实文本标记
attention_mask = [int(id > 0) for id in input_ids]
# 确保输入标记和关注掩码的长度等于最大序列长度
assert len(input_ids) == max_seq_length
assert len(attention_mask) == max_seq_length
return (input_ids, attention_mask)
2.4 数据集划分
X_train, X_test, Y_train, Y_test = train_test_split(df['message'], # 文本消息数据
df['sentiment'], # 文本情感标签
test_size=0.2, # 测试集占总数据的比例
random_state=42, # 随机种子,以确保可重复性
stratify=df['sentiment']) # 根据情感标签进行分层抽样
# 使用自定义函数 get_tokens 对训练集和测试集的文本进行分词,每个文本最多包含150个标记
X_train_tokens = X_train.apply(get_tokens, args=(tokenizer, 150))
X_test_tokens = X_test.apply(get_tokens, args=(tokenizer, 150))
2.5 训练准备
# 将训练集的文本特征转换为PyTorch张量
input_ids_train = torch.tensor(
[features[0] for features in X_train_tokens.values], dtype=torch.long) # 输入特征 ID
input_mask_train = torch.tensor(
[features[1] for features in X_train_tokens.values], dtype=torch.long) # 输入掩码
label_ids_train = torch.tensor(Y_train.values, dtype=torch.long) # 标签 ID
# # 输出训练集张量的形状
# print(input_ids_train.shape) # 输出训练集输入特征的形状
# print(input_mask_train.shape) # 输出训练集输入掩码的形状
# print(label_ids_train.shape) # 输出训练集标签的形状
# 创建训练数据集
train_dataset = TensorDataset(input_ids_train, input_mask_train, label_ids_train)
# 将测试集的文本特征转换为PyTorch张量
input_ids_test = torch.tensor([features[0] for features in X_test_tokens.values], dtype=torch.long)
input_mask_test = torch.tensor([features[1] for features in X_test_tokens.values], dtype=torch.long)
label_ids_test = torch.tensor(Y_test.values, dtype=torch.long)
# 创建测试数据集
test_dataset = TensorDataset(input_ids_test, input_mask_test, label_ids_test)
# 训练批次大小和训练周期数
train_batch_size = 64
num_train_epochs = 3
# 创建训练数据采样器和数据加载器
train_sampler = RandomSampler(train_dataset) # 随机采样器,用于随机选择训练样本
train_dataloader = DataLoader(train_dataset,
sampler=train_sampler,
batch_size=train_batch_size) # 创建训练数据加载器
t_total = len(train_dataloader) // num_train_epochs # 计算总的训练步数
# 输出一些训练相关的信息
print("样本数量 =", len(train_dataset)) # 输出训练集样本数量
print("训练周期数 =", num_train_epochs) # 输出训练周期数
print("总的训练批次大小 =", train_batch_size) # 输出总的训练批次大小
print("总的优化步数 =", t_total) # 输出总的优化步数
# 优化器和学习率调度器的设置
learning_rate = 5e-5 # 学习率
adam_epsilon = 1e-8 # Adam优化器的epsilon值
warmup_steps = 0 # 学习率预热步数
# 创建AdamW优化器和学习率调度器
optimizer = AdamW(model.parameters(), lr=learning_rate, eps=adam_epsilon)
scheduler = get_linear_schedule_with_warmup(optimizer,
num_warmup_steps=warmup_steps,
num_training_steps=t_total) # 创建学习率调度器
2.6 训练
# 检测是否有GPU可用,如果有则使用GPU,否则使用CPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 创建一个描述训练周期的迭代器
train_iterator = trange(num_train_epochs, desc="Epoch")
# 将模型置于 train 模式
model.train()
for epoch in train_iterator:
# 创建一个描述迭代的迭代器
epoch_iterator = tqdm(train_dataloader, desc="Iteration")
for step, batch in enumerate(epoch_iterator):
# 重置每个迭代开始时的所有梯度
model.zero_grad()
# 将模型和输入数据移到GPU(如果可用)
# torch.cuda.empty_cache() # 清理GPU缓存
model.to(device) # 将模型移到GPU或CPU
cuda = next(model.parameters()).device
batch = tuple(t.to(cuda) for t in batch) # 将批次数据移到GPU或CPU
# 确定传递给模型的输入
inputs = {
'input_ids': batch[0], # 输入特征ID
'attention_mask': batch[1], # 输入掩码
'labels': batch[2] # 标签
}
# 通过模型进行前向传播:输入 -> 模型 -> 输出
outputs = model(**inputs)
# 计算损失
loss = outputs[0]
# 打印当前损失值
print("\r%f" % loss, end='')
# 反向传播损失,自动计算梯度
loss.backward()
# 通过将梯度限制在一定范围内来防止梯度爆炸
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
# 更新模型参数和学习率
optimizer.step()
scheduler.step()
保存模型
model.save_pretrained('My_Model/weibo-bert-rubbish-model')
2.7 验证
# 测试批次大小
test_batch_size = 64
# 创建测试数据采样器和数据加载器
test_sampler = SequentialSampler(test_dataset) # 顺序采样器,用于顺序选择测试样本
test_dataloader = DataLoader(test_dataset,
sampler=test_sampler,
batch_size=test_batch_size) # 创建测试数据加载器
# 加载之前保存的预训练模型
# model = model.from_pretrained('/outputs')
# 初始化预测和实际标签
preds = None
out_label_ids = None
# 将模型置于 eval 模式
model.eval()
for batch in tqdm(test_dataloader, desc="评估中"):
# 将模型和输入数据移到GPU(如果可用)
model.to(device)
batch = tuple(t.to(device) for t in batch)
# 在 eval 模式下不跟踪任何梯度
with torch.no_grad():
inputs = {
'input_ids': batch[0], # 输入特征ID
'attention_mask': batch[1], # 输入掩码
'labels': batch[2] # 标签
}
# 通过模型进行前向传播
outputs = model(**inputs)
# 我们得到损失,因为我们提供了标签
tmp_eval_loss, logits = outputs[:2]
# 测试数据集可能包含多个批次的项目
if preds is None:
preds = logits.detach().cpu().numpy()
out_label_ids = inputs['labels'].detach().cpu().numpy()
else:
preds = np.append(preds, logits.detach().cpu().numpy(), axis=0)
out_label_ids = np.append(out_label_ids,
inputs['labels'].detach().cpu().numpy(),
axis=0)
# 计算最终损失、预测和准确度
preds = np.argmax(preds, axis=1) # 获取预测类别
acc_score = accuracy_score(preds, out_label_ids) # 计算准确度
f1_score = f1_score(preds, out_label_ids) # 计算F1分数
print ('测试集中的Accuracy分数: ', acc_score)
print ('测试集中的F1分数: ', f1_score)
2.8 预测
读取待分类微博数据,使用微调模型进行类别预测
df_origin = pd.read_csv('data/weibo_origin.csv')
df_origin['label'] = 0 #统一初始化为0
df_origin['text'] = df_origin.message.str.replace('\n',' ')
df_origin
# 这里更上面创建训练和验证集一样的道理
X_pred=df_origin['text']
Y_pred=df_origin['label']
X_pred_tokens = X_pred.parallel_apply(get_tokens, args=(tokenizer, 150))
input_ids_pred = torch.tensor(
[features[0] for features in X_pred_tokens.values], dtype=torch.long)
input_mask_pred = torch.tensor(
[features[1] for features in X_pred_tokens.values], dtype=torch.long)
label_pred=torch.tensor(Y_pred.values,dtype=torch.long)
pred_dataset = TensorDataset(input_ids_pred,input_mask_pred,label_pred)
pred_batch_size = 256
pred_sampler = SequentialSampler(pred_dataset)
pred_dataloader = DataLoader(pred_dataset,
sampler=pred_sampler,
batch_size=pred_batch_size)
调用微调模型,也就是刚才训练好的模型
model = model.from_pretrained('My_Model/weibo-bert-rubbish-model')
preds = None
model.eval()
# 预测
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
for batch in tqdm(pred_dataloader, desc="Predict"):
batch = tuple(t.to(device) for t in batch)
with torch.no_grad():
inputs = {
'input_ids': batch[0],
'attention_mask': batch[1],
'labels': batch[2]
}
outputs = model(**inputs)
_, logits = outputs[:2]
if preds is None:
preds = logits.detach().cpu().numpy()
else:
preds = np.append(preds, logits.detach().cpu().numpy(), axis=0)
得到预测结果
prob = torch.nn.functional.softmax(torch.tensor(preds), dim=1) # 使用softmax函数计算预测的概率分布
preds = np.argmax(preds, axis=1) # 计算每个样本的最终预测类别
df_origin['ad_prob'] = [p[1].item() for p in prob] # 将概率分布的第二列(表示"1"类别的概率)添加到DataFrame中
df_origin['pred'] = preds # 将最终的预测类别添加到DataFrame中
df_origin.to_csv('data/weibo_pre.csv', index=False)
df_origin.sample(10)
三、总结
-
F1分数和精确度:模型在评估阶段获得了非常良好的性能,F1分数和精确度都超过了80%。这表明模型在测试数据上具有很高的分类性能,可以有效地识别文本数据中的目标情感或类别。
-
训练过程:在训练期间,损失(loss)在下降。这是一个积极的迹象,表明模型正在逐渐学习并适应训练数据。随着训练的进行,模型逐渐提高其性能,以使损失最小化。
-
训练设备:我自己的电脑显卡为1660S,6G的显存,训练的时候报错,建议大家使用本文章代码时使用显存大一点的电脑。