ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

微博情感分析实战:BERT微调与WeiboSenti100k源码解析

微博情感分析实战:BERT微调与WeiboSenti100k源码解析 简介这份资源是面向高校学生与NLP入门者的中文情感分析实战项目围绕WeiboSenti100k数据集与bert-base-chinese预训练模型展开可用于毕业设计、课程设计或软件工程实践帮助读者掌握从数据预处理到模型微调的完整流程。压缩包共5个文件包含2个Python脚本、1个CSV数据集、1个TXT依赖清单和1个Markdown项目说明整体约9.73MB其中脚本承担训练与推理任务CSV提供微博情感语料说明文档梳理项目结构与运行方式。目前已有196人学习下载。项目覆盖数据清洗、分词与特殊标记处理、分类层构建、损失函数与优化器设置、准确率与F1等指标评估以及新文本情感极性预测读者可据此理解预训练模型微调思路积累深度学习框架与transformers库的实操经验为后续NLP研究或开发打下基础。1. 微博情感分析项目从数据到可运行源码的完整落地微博评论区的情绪判断很多人第一反应是调个大模型 API 就完事但真到毕业设计或课程设计答辩现场评委问的是「你的模型怎么微调的、数据怎么处理的、F1 多少」这时候手里没一套能跑通的源码就很被动。这个资源包解决的正是这个问题它把 WeiboSenti100k 数据集、bert-base-chinese 预训练权重加载逻辑、训练脚本、推理脚本和项目说明打包在一起拿到手就能在自己的机器上跑一遍完整的中文情感二分类流程。适合正在做 NLP 方向毕业设计、课程设计的学生也适合想亲手跑一次大模型微调实战但不想从零搭脚手架的工程师。整个包的结构很直白——code目录放脚本dataset放weibo_senti_100k.csv根目录下train.py、inference.py、requirements.txt、README.md各司其职没有多余的抽象层读起来不费劲。2. 数据管道与 bert-base-chinese 加载训练前的两件硬活2.1 WeiboSenti100k 的字段结构与标签分布拿到weibo_senti_100k.csv之后第一件事不是急着喂给模型而是先搞清楚它长什么样。这个数据集常见做法是两列一列label一列review有的版本列名是text或content以实际文件为准。label是 0/1 二值0 代表负面、1 代表正面没有中性类——这一点和摘要里提到的「正面、负面和中性」有出入实际二分类任务只处理正负两类别被误导。加载时用 pandas 读进来先看几行import pandas as pd # 读取数据集注意编码微博文本常见 utf-8 df pd.read_csv(dataset/weibo_senti_100k.csv, encodingutf-8) # 查看列名和前 5 行确认字段结构 print(df.columns.tolist()) print(df.head()) # 标签分布检查是否严重不平衡 print(df[label].value_counts())逻辑说明encodingutf-8是必须显式指定的Windows 上默认 gbk 会直接报UnicodeDecodeError。value_counts()用来确认正负样本比例WeiboSenti100k 大致均衡如果发现某一类占比超过 70%后面训练时就得考虑加class_weight或者过采样。参数上pd.read_csv的sep默认逗号如果文件是 tab 分隔要改成sep\t。2.2 用 BertTokenizer 做分词与特殊标记对齐bert-base-chinese 的分词器和英文 BERT 不一样它按字切分中文一个字一个 token标点和英文单词另有处理。这一步的核心是把原始文本转成input_ids、attention_mask、token_type_ids三件套并且统一长度。常见做法是封装一个 Dataset 类from torch.utils.data import Dataset from transformers import BertTokenizer tokenizer BertTokenizer.from_pretrained(bert-base-chinese) class WeiboDataset(Dataset): def __init__(self, texts, labels, max_len128): self.texts texts self.labels labels self.max_len max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): enc tokenizer( str(self.texts[idx]), max_lengthself.max_len, paddingmax_length, truncationTrue, return_tensorspt ) return { input_ids: enc[input_ids].squeeze(0), attention_mask: enc[attention_mask].squeeze(0), token_type_ids: enc[token_type_ids].squeeze(0), label: torch.tensor(self.labels[idx], dtypetorch.long) }逻辑说明max_length128是微博场景的常用值微博单条文本短128 个 token 足够覆盖绝大多数样本设太大只会浪费显存。paddingmax_length保证一个 batch 内长度一致truncationTrue处理超长文本。squeeze(0)是因为return_tensorspt会多出一个 batch 维度在__getitem__里要压掉。注意token_type_ids在单句分类任务里其实全 0但 BERT 的 forward 接受它保留着不碍事。2.3 数据集划分与 DataLoader 的 batch 组织训练前要按比例切分训练集和验证集常见 8:2 或 9:1。用train_test_split时记得固定random_state否则每次跑结果都不一样调参时根本分不清是改动生效还是随机波动。from sklearn.model_selection import train_test_split from torch.utils.data import DataLoader train_texts, val_texts, train_labels, val_labels train_test_split( df[review].tolist(), df[label].tolist(), test_size0.1, random_state42, stratifydf[label] ) train_dataset WeiboDataset(train_texts, train_labels) val_dataset WeiboDataset(val_texts, val_labels) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers0) val_loader DataLoader(val_dataset, batch_size64, shuffleFalse, num_workers0)逻辑说明stratifydf[label]保证切分后正负比例和原始一致避免验证集里某一类样本过少导致指标失真。batch_size32是 8G 显存下的稳妥选择显存够可以往上加。num_workers0在 Windows 上最省心设大了容易遇到多进程报错Linux 下可以设 2 或 4 加速数据加载。3. 微调训练脚本拆解train.py 里的关键参数与训练循环3.1 模型加载与分类头替换train.py的核心逻辑是加载bert-base-chinese把预训练模型的 pooler 输出接到一个二分类全连接层上。transformers 库提供了BertForSequenceClassification直接指定num_labels2就行不用自己手写分类头。import torch from transformers import BertForSequenceClassification, AdamW, get_linear_schedule_with_warmup device torch.device(cuda if torch.cuda.is_available() else cpu) model BertForSequenceClassification.from_pretrained( bert-base-chinese, num_labels2 ) model.to(device)逻辑说明num_labels2决定了分类头输出维度二分类就是 2。from_pretrained会自动下载权重到~/.cache/huggingface/下第一次跑需要联网之后离线也能加载。如果显存紧张可以在加载时加torch_dtypetorch.float16做半精度但要注意某些操作在 fp16 下会溢出稳妥起见先跑 fp32。3.2 优化器、学习率与 warmup 策略BERT 微调的学习率不能设大常见范围是 2e-5 到 5e-5比从头训练小一两个数量级。优化器用 AdamW权重衰减设 0.01。warmup 的作用是前几百步慢慢把学习率升上去避免一开始就把预训练权重冲乱。EPOCHS 3 LR 2e-5 WARMUP_RATIO 0.1 optimizer AdamW(model.parameters(), lrLR, weight_decay0.01) total_steps len(train_loader) * EPOCHS warmup_steps int(total_steps * WARMUP_RATIO) scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_stepswarmup_steps, num_training_stepstotal_steps )逻辑说明EPOCHS3是微调的常见轮数BERT 在小数据集上训太多轮会过拟合验证集 loss 会先降后升。LR2e-5是经验值如果 loss 一直不降可以试 3e-5如果震荡厉害就降到 1e-5。WARMUP_RATIO0.1表示前 10% 的步数用于 warmup这个比例在几千到几万条数据规模下比较稳。3.3 训练循环与验证指标计算训练循环本身不复杂关键是每个 epoch 结束后在验证集上算准确率和 F1别只看 loss。二分类任务里如果正负样本均衡准确率够用如果不均衡F1 更有参考价值。from sklearn.metrics import accuracy_score, f1_score for epoch in range(EPOCHS): model.train() total_loss 0 for batch in train_loader: optimizer.zero_grad() input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) token_type_ids batch[token_type_ids].to(device) labels batch[label].to(device) outputs model( input_idsinput_ids, attention_maskattention_mask, token_type_idstoken_type_ids, labelslabels ) loss outputs.loss loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() scheduler.step() total_loss loss.item() # 验证阶段 model.eval() preds, trues [], [] with torch.no_grad(): for batch in val_loader: input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) token_type_ids batch[token_type_ids].to(device) labels batch[label].to(device) outputs model( input_idsinput_ids, attention_maskattention_mask, token_type_idstoken_type_ids ) pred torch.argmax(outputs.logits, dim1) preds.extend(pred.cpu().numpy()) trues.extend(labels.cpu().numpy()) acc accuracy_score(trues, preds) f1 f1_score(trues, preds, averagebinary) print(fEpoch {epoch1} | Loss {total_loss/len(train_loader):.4f} | Acc {acc:.4f} | F1 {f1:.4f})逻辑说明clip_grad_norm_(max_norm1.0)是防止梯度爆炸的常规操作BERT 微调时加上更稳。验证阶段用torch.no_grad()关掉梯度计算省显存也提速。averagebinary在二分类下算的是正类的 F1如果想看宏平均改成macro。训练完记得torch.save(model.state_dict(), model.pt)保存权重不然白跑。4. 推理与部署inference.py 怎么用、输出怎么读4.1 单条文本预测的完整调用链inference.py的职责是加载训练好的权重对新的微博文本做情感判断。它需要重复 tokenizer 的加载和模型初始化然后读入权重。import torch from transformers import BertTokenizer, BertForSequenceClassification device torch.device(cuda if torch.cuda.is_available() else cpu) tokenizer BertTokenizer.from_pretrained(bert-base-chinese) model BertForSequenceClassification.from_pretrained(bert-base-chinese, num_labels2) model.load_state_dict(torch.load(model.pt, map_locationdevice)) model.to(device) model.eval() def predict(text): enc tokenizer(text, max_length128, paddingmax_length, truncationTrue, return_tensorspt) with torch.no_grad(): outputs model( input_idsenc[input_ids].to(device), attention_maskenc[attention_mask].to(device), token_type_idsenc[token_type_ids].to(device) ) prob torch.softmax(outputs.logits, dim1) pred torch.argmax(prob, dim1).item() return {label: pred, confidence: prob[0][pred].item()} print(predict(今天心情特别好阳光明媚))逻辑说明load_state_dict加载的是训练时保存的权重map_locationdevice保证在 CPU 上也能加载 GPU 训出来的权重。softmax把 logits 转成概率confidence是模型对当前预测的置信度低于 0.6 的结果建议人工复核。注意推理时model.eval()必须调用否则 dropout 会随机丢弃神经元同一句话两次预测结果可能不一样。4.2 批量推理与结果落盘实际用的时候往往是一批文本要跑逐条循环太慢可以组 batch。把文本列表按 batch_size 切块每块做一次 forward。def batch_predict(texts, batch_size64): results [] for i in range(0, len(texts), batch_size): chunk texts[i:ibatch_size] enc tokenizer(chunk, max_length128, paddingTrue, truncationTrue, return_tensorspt) with torch.no_grad(): outputs model( input_idsenc[input_ids].to(device), attention_maskenc[attention_mask].to(device), token_type_idsenc[token_type_ids].to(device) ) probs torch.softmax(outputs.logits, dim1) preds torch.argmax(probs, dim1).cpu().numpy() for j, p in enumerate(preds): results.append({text: chunk[j], label: int(p), confidence: float(probs[j][p])}) return results逻辑说明paddingTrue表示按 batch 内最长序列动态补齐比固定max_length省计算。结果里同时保留文本、标签和置信度方便后续筛选低置信样本做人工标注。落盘用pd.DataFrame(results).to_csv(predictions.csv, indexFalse)即可。5. 避坑与排查跑这个项目最容易翻车的五个地方5.1 现象加载数据集报 UnicodeDecodeError原因Windows 默认编码是 gbk而 csv 文件是 utf-8 编码pandas 读取时按系统默认编码解析就炸了。解决pd.read_csv显式加encodingutf-8如果还报错就试encodingutf-8-sig后者能处理带 BOM 头的文件。5.2 现象CUDA out of memory原因batch_size设太大或者max_length设了 256 以上显存扛不住。解决先把batch_size降到 16 甚至 8max_length保持 128。如果还不行在模型加载时加torch_dtypetorch.float16做半精度推理显存占用能降接近一半。另外检查是不是忘了在验证阶段加torch.no_grad()那会导致显存持续累积。5.3 现象训练 loss 一直不降准确率卡在 50% 左右原因学习率设太大导致权重被冲乱或者标签列读进来是字符串0/1而不是整数模型学不到东西。解决先print(df[label].dtype)确认类型如果是 object 就df[label] df[label].astype(int)。学习率从 2e-5 往下调到 1e-5 试试同时确认optimizer.zero_grad()在每次 backward 前调用了。5.4 现象验证集 F1 比准确率低很多原因正负样本不均衡模型倾向于预测多数类准确率看着还行但少数类全错。解决先看value_counts()确认比例如果偏差大在 loss 里加class_weight或者用WeightedRandomSampler做过采样。也可以把评估指标从准确率换成 F1 作为模型选择依据。5.5 现象推理时同一句话两次结果不一样原因忘了调model.eval()dropout 层还在随机丢弃神经元。解决推理前必须model.eval()训练前必须model.train()这两个状态切换是 PyTorch 的基本纪律漏了就会出现这种「玄学」波动。6. 进阶技巧用学习率预热和分层采样把 F1 再往上推一档跑通基础流程之后如果想把验证集 F1 从 0.90 推到 0.93 以上有两个方向值得试。第一个是学习率预热比例的调整。默认WARMUP_RATIO0.1在数据量一万条以下时偏大可以降到 0.05 甚至 0.02让模型更快进入有效学习阶段。我一般会跑三组对比0.02、0.05、0.1看验证集 F1 曲线哪个先到峰值且不震荡。第二个是分层采样。虽然 WeiboSenti100k 整体均衡但切分后验证集里如果某类样本太少F1 波动会很大。用sklearn的StratifiedKFold做 5 折交叉验证每折都保证正负比例一致最后取 5 折 F1 的均值作为最终指标比单次切分靠谱得多。from sklearn.model_selection import StratifiedKFold import numpy as np skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) f1_scores [] for fold, (train_idx, val_idx) in enumerate(skf.split(df[review], df[label])): train_texts df[review].iloc[train_idx].tolist() val_texts df[review].iloc[val_idx].tolist() train_labels df[label].iloc[train_idx].tolist() val_labels df[label].iloc[val_idx].tolist() # 重新初始化模型避免折间污染 model BertForSequenceClassification.from_pretrained(bert-base-chinese, num_labels2) model.to(device) # ... 训练与验证流程同上记录该折 F1 # f1_scores.append(fold_f1) print(f5-fold F1 mean: {np.mean(f1_scores):.4f} ± {np.std(f1_scores):.4f})逻辑说明每折必须重新from_pretrained初始化模型不能复用上一折的权重否则折间信息泄漏F1 会虚高。np.std反映模型在不同数据切分下的稳定性标准差超过 0.02 说明模型对数据划分敏感需要检查数据量是否够或正则化是否到位。还有一个容易忽略的点是最大序列长度的动态调整。微博文本普遍短统计一下 token 长度分布如果 95% 的样本都在 64 以内把max_length从 128 降到 64训练速度能快接近一倍显存占用也降F1 基本不掉。这个习惯是我踩过几次「显存不够只能降 batch_size 导致训练变慢」的坑之后养成的——每次拿到新数据集先跑一遍长度分布统计再决定max_length而不是无脑设 128 或 256。希望帮到你。本文还有配套的精品资源点击获取
返回列表