
简介面向毕业设计、课程设计的中文情感分析需求基于BERT实现的数据分类Python源码提供了从数据处理、模型训练到预测部署的完整方案。代码配有详细注释适合具备Python基础、希望快速上手NLP情感分析任务并用于项目展示的学习者。压缩包共74个文件包含30个Python脚本、25个CSV数据文件、12个TXT文本及2个Shell脚本整体约53MB。Python脚本覆盖了分词、建模、训练、预测、模型导出等关键环节CSV与TXT文件提供了训练语料与示例文本可直接运行体验。项目文件结构按BERT分类流程组织便于按模块阅读和二次开发。目前已有254人学习参考该项目。下载后即可快速部署运行既能作为毕业设计的高分参考也可作为期末大作业或课程设计的实用代码库对于理解BERT微调、情感倾向分类等核心概念也很有帮助。1. 为什么毕业设计选 BERT 做中文情感分析从“词频统计”到“语义理解”的差距“基于BERT实现的中文情感分析数据分类python源码”这个题目听起来像某个现成项目但拆开看就是一条完整技术链路用预训练模型做中文文本分类并且要拿出能跑通、能答辩、能写进论文的 Python 代码。很多同学第一版用词频统计加朴素贝叶斯准确率卡在七十几优化一周也上不去换成 BERT 微调之后同一份数据往往能拉到九十以上这是语义理解带来的实打实差距。这篇文章我把自己跑通这条链路的过程写下来覆盖环境搭建、数据预处理、模型微调、参数设置和常见坑点新手能照着复现熟手可以直接借鉴超参数和排查顺序。2. 跑通 BERT 中文情感分析的最小环境Python、torch、transformers 版本怎么配2.1 先装好 Python 虚拟环境别直接往系统环境里塞包很多同学一上来就在系统 Python 里pip install装到一半发现某个包和另一个包冲突最后只能重装解释器白白浪费一下午。BERT 相关包对版本敏感尤其是 transformers、numpy、scikit-learn 之间经常有依赖约束我一般建议用 Miniconda 创建独立虚拟环境。虚拟环境里翻车直接删掉重建不碰系统环境这是最稳妥的收场方式。conda create -n bert-sentiment python3.8 conda activate bert-sentiment pip install torch1.13.1 --index-url https://download.pytorch.org/whl/cu117 pip install transformers4.30.2 pandas scikit-learn matplotlib这里有几个关键点。Python 版本选 3.8 而不是最新的 3.12是因为很多深度学习包的预编译 wheel 在新版本上不一定齐3.8 到 3.10 是当前兼容性最稳的区间。torch 的安装命令里写了--index-url它指向 CUDA 11.7 的预编译包如果你的机器没有 NVIDIA 显卡把这一整段参数去掉安装的是 CPU 版也能跑只是训练慢很多。可以用nvidia-smi看自己的 CUDA 版本版本不匹配时程序会报找不到 CUDA driver 一类的错需要回头换对应的 cu 版本号。transformers 我锁在 4.30.2这个版本的 API 比较稳定太新的版本会把不少参数改名让按老教程写的代码直接报错。装完之后建议顺手验证一下环境在终端执行下面这条命令python -c import torch; print(torch.__version__, torch.cuda.is_available())能正常输出版本号就说明 PyTorch 装好了torch.cuda.is_available()输出True表示 GPU 可用False表示现在走 CPU。这一步成本极低但能挡住后面大量“跑了一半才发现设备不对”的尴尬。如果后续要在 VSCode 里写代码记得在命令面板里选 python 解释器把路径指到刚才创建的 conda 环境否则 VSCode 底层还是系统 Pythonimport 包会到处报错。2.2 下载 BERT 中文预训练模型直接用 bert-base-chineseBERT 的中文预训练模型里bert-base-chinese是毕业设计最省事的选择。它是一个 12 层、768 维的 Transformer 编码器预训练语料覆盖新闻、百科、网页等对电商评论、酒店评论、影评这类日常文本已经有不错的理解能力。用 transformers 的from_pretrained方法会自动下载权重但模型文件几百兆网络不够稳定时需要花不少时间。from transformers import BertTokenizer, BertModel tokenizer BertTokenizer.from_pretrained(bert-base-chinese) model BertModel.from_pretrained(bert-base-chinese) print(模型加载完成)参数说明from_pretrained的第一个参数既可以是官方模型名也可以是一个本地目录。如果你的实验机器不方便联网可以在一台能下载的机器上先跑一次加载然后把缓存目录整个复制过去再把代码里的参数改成本地路径例如from_pretrained(/data/models/bert-base-chinese)。模型目录里需要包含词表文件vocab.txt和权重文件pytorch_model.bin缺少任何一个都会在加载时报错。这里还要理解一个特性BERT 的中文词表是字级别的它把句子拆成单字而不是词。你输入“太棒了”它转成“太 棒 了”这样的 token 序列遇到生僻字也能通过字组合得到表示。这正是 BERT 在中文情感分类上比传统 word2vec 更稳的原因之一模型不再依赖外部分词器的质量。分词器出了错语义就会被带偏而 BERT 直接绕开了这一步。2.3 用现成 Pipeline 做一次推理验证环境全部打通环境装完、模型加载完先不要急着写训练脚本。我习惯先用 transformers 自带的 pipeline 跑一次真实推理确认 GPU、框架、模型链路是通的避免写完整套训练代码后才发现最底层调用有问题。from transformers import pipeline classifier pipeline( sentiment-analysis, model/data/models/bert-base-chinese, ) print(classifier(这家酒店的床特别舒服早餐也很好))这段代码会返回一个包含标签和置信度的列表类似[{label: POSITIVE, score: 0.99}]具体标签形式取决于模型。如果你的机器是 CPU第一次加载会慢一些但单条预测的时间可以接受如果直接报错说没有可用设备回头查第 2.1 节的 torch 安装命令。pipeline 的好处是内部自动完成 tokenizer 转换和模型推理后面做演示界面时可以直接把微调后的模型目录替换到这个参数上几行代码就能封装成可交互的分类器。3. 数据预处理中文情感数据的清洗、Tokenization 与数据集划分3.1 中文文本清洗去掉链接和表情但不要去掉停用词传统机器学习做中文情感分析需要分词、去停用词再构造词频特征。用 BERT 之后清洗逻辑可以简洁很多因为模型本身理解上下文不需要去掉“的、了、是”这类词反而这些词保留下来能帮助判断语气。清洗的目标变成三件事去掉 URL、去掉 HTML 残留、压缩重复标点目的是避免给模型制造无意义的噪声。import re def clean_text(text: str) - str: # 去掉 url text re.sub(rhttps?://\S|www\.\S, , text) # 去掉 html 标签 text re.sub(r.*?, , text) # 保留中文、英文、数字和常用标点表情符号和特殊字符直接丢弃 text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9。、\\\《》【】\s], , text) # 把连续重复标点压缩成一个 text re.sub(r([。])\1, r\1, text) return text.strip() sample 这家酒店太棒了 服务超级好 https://example.com/review print(clean_text(sample))代码逻辑是四步正则逐步过滤。第一步去超链接防止 URL 字符干扰 tokenizer第二步去 HTML 标签处理从网页抓取时残留的标签第三步是核心它只允许中文字、英文字母、数字和几种常见标点通过emoji 和特殊符号在这里被丢弃第四步把“”压缩成“”避免模型把多个感叹号当成一种固定情绪特征。输出结果是“这家酒店太棒了 服务超级好”原意没有受损。这里有个常见的反向操作把评论里的表情符号当作情感特征去保留。我理解这个想法的来源传统特征工程里 emoji 确实有用但 BERT 的字表里没有这些符号强行保留只会变成[UNK]反而损失信息。如果你手里的数据来自爬虫清洗前还要再做一次完全相同文本的去重同一句话反复出现会让验证集准确率虚高答辩时候被问起来很尴尬。3.2 用 tokenizer 把文本转成张量padding 和 truncation 必须显式设置BERT 不能直接处理字符串要先把句子转成input_ids和attention_mask。这是全流程里最容易写错的环节也是最容易被忽略的细节。许多报错都是因为这里操作不规范导致后面训练时 tensor shape 对不上。from transformers import BertTokenizer import torch tokenizer BertTokenizer.from_pretrained(/data/models/bert-base-chinese) def encode_text(text: str, max_len: int 128): encoding tokenizer( text, truncationTrue, paddingmax_length, max_lengthmax_len, return_tensorspt, ) return encoding enc encode_text(这家酒店的早餐很丰富) print(tokenizer.convert_ids_to_tokens(enc[input_ids][0])) print(enc[attention_mask][0])输出里能看到句子被拼上了[CLS]和[SEP]其余位置补满了[PAD]。attention_mask在真实 token 位置是 1在 padding 位置是 0模型计算注意力时会忽略这些补位符。参数说明truncationTrue表示超过max_len的部分直接截断paddingmax_length表示统一补到max_len让同一个 batch 里所有样本长度相等。return_tensorspt返回 PyTorch 张量省去手动转换。max_len我一般设 128。大多数中文电商评论的有效信息不超过 64 个字截断影响很小但如果是电影长评就需要提到 256否则后半段的转折信息会丢失模型只能看到前半段的情绪。另一个容易踩的坑是把 padding 写成paddingTrue它只会按当前 batch 里最长的那条来补速度和显存稍好但训练和推理时 batch 内长度不一致可能触发 shape mismatch 报错。为了省心统一用max_length稍微多占一点显存但不会出问题。3.3 划分数据集并构造 DataLoader分层抽样是必须的数据准备好后先切分训练集、验证集、测试集再交给 DataLoader。切分并不是随便split一下就行情感分类数据集中正负样本量经常不均衡必须用分层抽样保证每个集合里的类别比例和原始数据一致。from sklearn.model_selection import train_test_split # texts: list[str], labels: list[int] train_texts, temp_texts, train_labels, temp_labels train_test_split( texts, labels, test_size0.2, random_state42, stratifylabels ) val_texts, test_texts, val_labels, test_labels train_test_split( temp_texts, temp_labels, test_size0.5, random_state42, stratifytemp_labels )这个切分逻辑是第一次分出 20% 作为临时集第二次把临时集对半分成验证集和测试集最终比例约 8:1:1。stratifylabels是核心参数它确保正样本和负样本在每个集合里的比例一致防止某个类被全部切到测试集里。random_state42固定随机种子让实验结果可以复现写论文时这个方法部分可以直接引用。然后构造 Dataset 类import torch from torch.utils.data import Dataset, DataLoader class SentimentDataset(Dataset): def __init__(self, texts, labels, tokenizer, max_len128): self.texts texts self.labels labels self.tokenizer tokenizer self.max_len max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): text str(self.texts[idx]) label int(self.labels[idx]) encoding self.tokenizer( text, truncationTrue, paddingmax_length, max_lengthself.max_len, return_tensorspt, ) return { input_ids: encoding[input_ids].squeeze(0), attention_mask: encoding[attention_mask].squeeze(0), labels: torch.tensor(label, dtypetorch.long), }这里在__getitem__里实时做 tokenization而不是预处理时一次性转好好处是可以灵活调整max_len也便于对样本做动态增强。返回的三项分别是input_ids、attention_mask和labels分别对应模型输入的 token 编号、有效位置掩码和分类标签。squeeze(0)是去掉单条样本的 batch 维度因为传入的是单条文本tokenizer 默认会加一个维度。train_loader DataLoader( SentimentDataset(train_texts, train_labels, tokenizer), batch_size16, shuffleTrue, )shuffleTrue让每个 epoch 的遍历顺序不同避免模型记住样本顺序。验证集和测试集的 DataLoader 不需要 shuffle。到这里数据到模型的接口已经完全打通可以进入训练阶段。4. 训练 BERT 分类模型微调代码、优化器与参数调整4.1 加载分类模型BertForSequenceClassification 已经帮你接好了分类头BERT 本身输出的是每个 token 的向量序列做情感分类还需要一个分类头。直接用BertForSequenceClassification最省事它会在[CLS]这个位置的输出向量上接一个线性层把 768 维映射到类别数量。微调时如果数据集很小可以冻结底层只训练顶层防止过拟合也省显存如果数据量在几千条以上我倾向全量微调效果更稳。from transformers import BertForSequenceClassification import torch device torch.device(cuda if torch.cuda.is_available() else cpu) model BertForSequenceClassification.from_pretrained( /data/models/bert-base-chinese, num_labels2, ) model.to(device)参数说明num_labels对应类别数二分类就是 2。from_pretrained会把原来的预训练输出头替换成新的分类层不需要手动修改模型结构。model.to(device)把模型放到 GPU 或 CPU 上后续所有输入张量也需要同步到同一设备否则会报 device mismatch。4.2 训练循环优化器、学习率调度器与标准三步BERT 微调的主流优化器是 AdamW注意它是带权重衰减的变体不是普通 Adam。学习率一般取 2e-5 到 5e-5比从头训练小一个数量级因为预训练权重已经处于一个较优位置学习率太大容易破坏已经学到的语言表示。from transformers import AdamW, get_linear_schedule_with_warmup epochs 3 total_steps len(train_loader) * epochs optimizer AdamW(model.parameters(), lr2e-5) scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_stepsint(total_steps * 0.1), num_training_stepstotal_steps, ) for epoch in range(epochs): model.train() total_loss 0 for step, batch in enumerate(train_loader): input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) labels batch[labels].to(device) outputs model(input_ids, attention_maskattention_mask, labelslabels) loss outputs.loss loss.backward() optimizer.step() scheduler.step() optimizer.zero_grad() total_loss loss.item() if (step 1) % 50 0: print(fepoch {epoch 1}, step {step 1}, loss {loss.item():.4f})代码逻辑说明outputs.loss是模型内部算好的交叉熵损失不需要自己手动算。scheduler是线性学习率调度器前 10% 的步数从一个小值升到设定学习率后面再逐步降为 0这种预热策略能让训练早期更稳定。optimizer.zero_grad()放在optimizer.step()之后这是 PyTorch 的标准写法如果放在loss.backward()之前会清空刚算出来的梯度导致 loss 不下降。每 50 步打印一次 loss是为了及时观察训练状态但如果 batch 总数很少可以改成每个 epoch 打印一次。关于训练时长多说一句。只有 CPU 时跑三到五个 epoch 可能需要数小时我一般先把max_len降到 64再取 2000 条数据试跑看完整流程是否正常然后才决定要不要换 GPU 环境或上云。不要一上来就全量训练这是很多人第一天就放弃训练的主要原因。4.3 关键超参数怎么调先固定一组再逐个动参数常用范围我的建议batch_size8~32显存不够先降到 8learning_rate2e-5~5e-5二分类小数据集从 2e-5 起步epochs2~5先跑 3再观察验证集曲线max_len64~256短评论 128 够用这组参数是 BERT 微调里最敏感的部分。batch_size主要影响显存和训练速度对最终效果影响相对小learning_rate影响收敛是否平稳过大会让 loss 震荡过小则收敛太慢epochs多了容易过拟合少了又学不够max_len决定模型能看到多长的上下文。我调参的顺序是先固定max_len128、batch_size16跑 3 个 epoch 看训练 loss 和验证集准确率再根据结果调整学习率。训练结束后保存模型model.save_pretrained(./output/bert-sentiment) tokenizer.save_pretrained(./output/bert-sentiment)保存目录里会有模型权重和词表文件之后评估、写论文、做答辩演示都可以直接加载不用重新训练。5. BERT 情感分析避坑指南显存不足、过拟合和标签不均衡先排查哪个5.1 现象CUDA out of memory程序跑不了几个 batch原因BERT 的自注意力计算复杂度随序列长度二次增长max_len128、batch_size16在 8G 显存上已经很接近临界点。多数情况不是代码写错而是资源不够。解决优先把batch_size降到 8max_len降到 64还不够就先用一个更小的中文预训练模型跑通流程再切回完整模型。不要一边训练一边开着十几个浏览器页面GPU 显存被图形界面占用的情况很常见。我自己的经验是先关掉 VSCode 里的大文件缓存和多余插件往往能多挤出 1G 显存。5.2 现象训练 loss 持续下降验证集准确率却不涨甚至下降原因典型过拟合。BERT 参数量过亿几千条数据训练几十个 epoch模型完全可以把训练样本背下来泛化能力反而变差。解决先回看每个 epoch 的验证集表现找到峰值在哪个 epoch然后减少训练轮数。也可以用早停法当验证集指标连续两轮不提升就截断。BERT 本身的 dropout 机制通常够用不需要额外加正则化。如果必须全量微调把学习率降到 2e-5 以下也能缓解一点。5.3 现象准确率很高但预测结果几乎全是“负面”或“正面”原因标签不均衡。比如数据里负面占 90%、正面占 10%模型直接全预测为负面就能拿到 90% 的准确率。这个分数看起来很高但实际没有区分能力。解决训练前统计类别分布用compute_class_weight算出权重把这个权重用于损失函数让模型对少数类的错误更敏感。from sklearn.utils.class_weight import compute_class_weight import numpy as np import torch class_weights compute_class_weight( balanced, classesnp.unique(labels), ylabels ) class_weights torch.tensor(class_weights, dtypetorch.float).to(device)这段代码会按类别频率反比计算权重少数类得到更大的权重。具体使用时把class_weights作为额外参数传给模型的loss_fct。同时评估时不能只盯准确率要看每个类别的精确率和召回率通常用宏平均 F1 作为最终指标。5.4 现象tokenizer 或者 DataLoader 报 shape 不一致原因最常见的是 padding 参数写成了paddingTrue或者只做了truncationTrue而没有统一 padding。另一个原因是数据集里存在空字符串或非字符串值比如 NaN编码后变成了空张量。解决统一写成paddingmax_length并显式设置max_length。清洗阶段提前过滤掉空值和空白文本打印一两条 tokenizer 的输出对比能快速定位问题来源。提示当你同时遇到显存不足和过拟合时优先解决显存不足。先把模型跑起来再用验证集曲线判断过拟合不要一上来就同时调五六个参数。6. 从“跑通”到“高分”评估指标、消融实验与演示界面6.1 评估指标看全别只报一个准确率模型训练完用classification_report输出每个类别的精确率、召回率和 F1这是答辩时最规范的展示方式。from sklearn.metrics import classification_report y_pred [] with torch.no_grad(): model.eval() for batch in test_loader: input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) outputs model(input_ids, attention_maskattention_mask) preds torch.argmax(outputs.logits, dim1) y_pred.extend(preds.cpu().tolist()) print(classification_report(test_labels, y_pred, target_names[negative, positive]))只看准确率会掩盖标签不均衡问题有了精确率和召回率才能判断模型是真正学到了语义还是只会输出多数类。6.2 消融实验用 TF-IDF 加 SVM 做对比证明 BERT 的价值毕业设计拿高分的常见做法是加一个对比实验。用同样的数据跑一个传统机器学习模型证明 BERT 的提升来自语义理解。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.svm import SVC from sklearn.pipeline import make_pipeline pipeline make_pipeline(TfidfVectorizer(), SVC(kernellinear)) pipeline.fit(train_texts, train_labels) svm_acc pipeline.score(test_texts, test_labels)这段代码几行就能跑完。把它和 BERT 的结果放在一张表里标注训练时间、准确率、F1 三个字段论文的“实验结果”部分就非常扎实。6.3 用 Gradio 封装一个演示界面答辩现场加分微调好的模型可以直接用 pipeline 加载再套一个 Gradio 的网页界面输入一句话立刻出情感结果和置信度。import gradio as gr from transformers import pipeline classifier pipeline(sentiment-analysis, model./output/bert-sentiment) def predict(text): result classifier(text)[0] return f情感倾向{result[label]}置信度{result[score]:.2%} gr.Interface(fnpredict, inputstext, outputstext).launch()这个界面是本地网页不用再做前后端几行代码就能在答辩演示时直接给老师看效果。我每次跑新模型固定动作是先跑通最小推理再做数据清洗和训练这个习惯让我省掉了至少一半的玄学报错。希望帮到你。本文还有配套的精品资源点击获取