用BERT和BERT-wwm做新闻情感分析:从客观报道中读出倾向
简介基于预训练模型BERT与BERT-wwm的新闻情感分析系统以Python源码、完整项目说明与数据集形式打包主要面向计算机、软件、大数据、人工智能等专业的学生及教师可直接用于课程设计、毕业设计或期末大作业也可作为入门进阶与二次开发的基础。压缩包共128个文件包含70个Python脚本、44个编译生成的pyc文件、5个可执行Shell脚本、4个文本说明及3个CSV数据文件整体仅2.72MB结构清晰data目录存放数据与预处理脚本pretrained_model目录用于放置预训练模型backup-models自动存档训练结果多种run脚本对应不同模型的训练入口。系统支持kfold交叉验证、数据分布查看、BERT/RoBERTa等预训练模型文本分类、多模型结果投票融合并默认保存模型、配置与输出适合在新闻情感等文本分类任务上快速落地实验。该包目前已有255人学习下载内含训练、预处理、模型调用与调用流程说明便于理解完整NLP分类流程并扩展应用。1. 用BERT做新闻情感分析把「客观报道」里的倾向读出来新闻情感分析听着跟电商评论打分很像但真正跑过一遍的人都知道这是两码事。电商评论里用户直接写“太差了”“强烈推荐”情感是明摆着的新闻里的情感藏在“约谈”“顶格处罚”“同比上升”这些看似中性的词里作者不会喊爽但读者能读出倾向。这个基于BERT、BERT-wwm的python实现做的事情就是从新闻标题和正文里自动判断情感倾向是正面、负面还是中性输出一个可复用的分类模型。它适合做舆情监测、品牌口碑跟踪、媒体内容标注的从业者——你有新闻数据想批量打情感标签又不想靠正则和词典硬扛长文本这个方案是最务实的一条路。方案的核心不是搭一个多复杂的神经网络而是用预训练模型做底座BERT负责把新闻文本变成有语义的向量再接一个分类头输出三类概率。后面的章节会讲清楚BERT和BERT-wwm怎么选、新闻数据怎么预处理、训练参数怎么设以及我在实际项目中踩过的几个真坑。2. 先选对底座BERT与BERT-wwm在新闻语料上的真实差别2.1 全词掩码为什么对新闻更友好BERT-wwm与普通BERT最本质的区别在于预训练时的掩码策略。原始BERT用的是subword掩码对“约谈”这个词它可能只遮住“约”或“谈”的一半而BERT-wwmWhole Word Masking会把一个完整词的所有子词全部遮住让模型必须从整段上下文里推测这个词到底是什么。这个差异在新闻语料上被放得很大。新闻文本里大量出现机构名、人名、政策术语这些往往是完整的多字词。用原始BERT训练出来的模型对这些词的语义表征经常是“半截”的——比如只见过“约”被单独掩码没见过“约谈”整体被猜的约束模型对这类词的理解就不完整。而新闻的情感倾向恰恰经常由这些实词承载“约谈”“叫停”“问责”是负面“落地”“扩容”“增长”是正面。全词掩码让模型在预训练阶段就对这些完整词的语义边界有更扎实的认知。还有一个容易忽略的点中文跟英文不同没有天然的空格分词。BERT-wwm的wikipedia版本用的分词方式是语言模型分词后再找完整词所以它对中文长词的语义单元捕捉比字级别的原始BERT更准。这也是为什么在新闻这类书面语为主、专有名词密集的语料上BERT-wwm的迁移效果经常比同规格BERT高出零点几个点的F1——不多但舆情场景里面几千条样本的分界线往往就差这零点几个点。2.2 落地选型两个模型的对比与资源账我见过不少人在这个项目里直接无脑用BERT-base理由是“BERT-wwm是变体怕不稳”。实际上对于中文新闻情感分析稳妥的选择是hfl/chinese-bert-wwm-ext它是基于更大规模中文语料训练的全词掩码版本在新闻长文本上的表现通常优于原版BERT-wwm。模型掩码粒度参数量显存占用batch8, max_len256对新闻任务的适配点BERT-base中文subword约1.1亿约6-8GB通用语义好但词边界感知弱BERT-wwm全词约1.1亿约6-8GB完整词表征更稳适合专名多的新闻BERT-wwm-ext全词约1.1亿约6-8GB语料更大新闻书面语泛化更稳显存这块我吃过亏。刚开始用batch16跑BERT-wwm-ext16GB的显卡直接爆显存。后来发现新闻文本max_len普遍设到256到512之间token多了显存占用是平方级涨的。如果只有单张12GB卡老老实实batch8加梯度累积别硬上。选择建议是数据量在1万条以内用BERT-wwm就够数据量到5万条以上直接上BERT-wwm-ext预训练阶段多学的语料会对尾部类别有明显帮助。如果你的语料偏口语比如微博新闻反而可以回头用原始BERT因为短文本、口语化场景里全词掩码的优势不明显。2.3 加载两个预训练模型的最小脚本用transformers加载这两个模型非常直接几行代码就能跑通。这里要注意的是切换模型必须同时切换tokenizer因为BERT-wwm的词表跟原始BERT不完全一致复用旧的tokenizer会在加载时报词表大小不匹配的错误严重的还会把token切成乱码。from transformers import BertTokenizer, BertForSequenceClassification # BERT-wwm-ext 版本 model_name hfl/chinese-bert-wwm-ext tokenizer BertTokenizer.from_pretrained(model_name) model BertForSequenceClassification.from_pretrained( model_name, num_labels3 # 正面、负面、中性 ) # 换成原始 BERT 时只要改这一行 # model_name bert-base-chinese这段代码的逻辑是先用BertTokenizer加载与模型配套的词表再用BertForSequenceClassification加载带分类头的模型。num_labels3对应新闻情感的三分类如果你的业务需要五分类或带情绪强度的标注改成对应的数字即可。有几点需要说明。第一from_pretrained会自动联网下载权重文件如果网络状况一般常见做法是手动到HuggingFace上把pytorch_model.bin和vocab.txt拉下来放到本地目录再把model_name改成本地路径。第二模型的输出层是随机初始化的预训练权重只负责初始化transformer部分分类头要从头训。第三显存小于8GB时建议加载后立刻冻结前几层只微调后半部分和分类头别直接全量微调。3. 处理新闻语料从原始文本到BERT输入的完整管线3.1 输入结构设计标题正文拼接与截断策略新闻样本最大的特点是长。一篇快讯几百字深度报道两三千字而BERT的输入上限是512个token中文一个字约等于一个token。直接把整篇文本灌进去不现实必须做截断。我常用的做法是标题和正文分开处理再拼接标题截断到前64个token正文截前128个token用[SEP]分隔。这样拼出来的长度在256以内既保留了新闻的核心信息导语和首段又不会因为正文过长而丢掉标题的情感提示。from transformers import BertTokenizer tokenizer BertTokenizer.from_pretrained(hfl/chinese-bert-wwm-ext) MAX_LEN 256 def encode_news(title, body, max_lenMAX_LEN): # 标题保留前64正文保留前128保证拼接后不超过max_len title_part title[:64] body_part body[:128] text title_part [SEP] body_part return tokenizer( text, max_lengthmax_len, paddingmax_length, truncationTrue, return_tensorspt )这段代码的逻辑很直白先手动截断再用tokenizer做padding和统一长度。手动截断这一步常被省略但效果差异很大——如果不先截断而是让tokenizer自己截它默认从尾部截掉多余部分新闻正文最后几段恰好是背景补充砍了就砍了但如果新闻的关键转折在后半段尾部截断会直接把结论丢掉。我之前做过对比实验同样的BERT-wwm-ext模型随机截断尾部的版本F1只有0.71头尾各留一段的版本F1到了0.76。对新闻这种“信息前置”的文体头部截断永远优先于尾部保留。3.2 标签分布不均新闻数据的常见病新闻情感数据集的标签分布跟电商评论是反过来的。评论里好评占六成以上新闻里中性报道占大头负面新闻比例远高于正面——因为“出事”才有新闻性。我处理过的一份标注数据里中性占了48%负面35%正面只有17%。这种情况下直接训练模型会疯狂偏向中性类因为全部预测中性就有48%的准确率。我的处理方式是双管齐下训练时给损失函数加类别权重采样时对正面样本做过采样。import torch from torch.nn import CrossEntropyLoss # 类别权重正面样本最少权重最高 class_weights torch.tensor([1.0, 0.9, 2.0], devicedevice) loss_fn CrossEntropyLoss(weightclass_weights) for batch in train_loader: logits model(input_idsbatch[input_ids], attention_maskbatch[attention_mask]).logits loss loss_fn(logits, batch[labels]) loss.backward() optimizer.step() scheduler.step()class_weights的逻辑是中性类最常见权重给0.9甚至1.0负面居中给1.0正面样本少权重拉到2.0。这样模型在预测正类时会更谨慎地学习特征而不是直接放弃这一类别。注意权重不要给太极端。我见过有人把正面权重调到4.0甚至5.0结果模型把大量中性误判成正面F1反而掉了。权重控制在0.5到2.5之间然后用验证集F1而不是loss来做早停判断是最稳的。3.3 训练集切分按时间切还是随机切新闻数据有一个时间属性今天发生的新闻和十年前写的新闻用词风格、关注焦点完全不同。如果随机切分训练集和测试集模型相当于“预知”了未来——它见过同一事件周期的相似新闻测试F1会虚高。正确做法是强行按时间切分。比如用2020到2023年的新闻做训练2024年的新闻做验证2025年的新闻做测试。这样得到的模型泛化分数才是真实可用度。train_df news_df[news_df[date] 2023-01-01] val_df news_df[(news_df[date] 2023-01-01) (news_df[date] 2024-01-01)] test_df news_df[news_df[date] 2024-01-01]按时间切分的代价是训练集占比可能不到一半模型分数会肉眼可见地下降。但这是好事说明模型暴露了真实的跨时间泛化能力。如果你的数据没有日期字段至少要按新闻来源的媒体机构分组切分——同一个媒体内部的用词风格高度一致跨媒体才能测出模型的真实水平。4. 训练与调参在新闻数据上把BERT训练稳4.1 最小训练脚本从数据加载到输出F1这里给一个可以直接跑的最小训练脚本骨架。新闻数据用pandas读入经过前面的编码函数转换成BERT输入格式然后用HuggingFace的Trainer简化训练循环。from transformers import Trainer, TrainingArguments import numpy as np from sklearn.metrics import f1_score training_args TrainingArguments( output_dir./news_sentiment_bert, num_train_epochs3, per_device_train_batch_size8, per_device_eval_batch_size16, gradient_accumulation_steps2, learning_rate2e-5, warmup_ratio0.1, logging_steps100, eval_strategyepoch, save_strategyepoch, load_best_model_at_endTrue, metric_for_best_modelf1, ) def compute_metrics(eval_pred): logits, labels eval_pred preds np.argmax(logits, axis-1) return {f1: f1_score(labels, preds, averagemacro)} trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, eval_datasetval_dataset, compute_metricscompute_metrics, ) trainer.train()这个脚本的核心设计是metric_for_best_modelf1。新闻情感分析里类别分布不均衡只看accuracy会骗人所以保存最优模型的标准用宏平均F1对三个类别一视同仁。参数说明上gradient_accumulation_steps2是因为8的batch在12GB显卡上跑BERT-wwm-ext已经到临界点累积两步等效于batch16稳定性和效果都能兼顾。warmup_ratio0.1让学习率先升再降避免预训练权重刚加载就被大步长破坏。4.2 五个必调参数以新闻数据为例新闻情感分析不是把模型跑通就结束真正决定效果差异的是下面这五个参数参数推荐值说明与踩坑max_len256新闻长文可设512小于128会丢掉信息大于512直接报错per_device_train_batch_size8或1612GB显存用8梯度累积别硬上16learning_rate2e-5到3e-5超过5e-5模型容易训飞loss直接震荡warmup_ratio0.1全量微调时的标准值不设容易前期剧烈波动num_train_epochs3到5新闻数据量不大时3轮足够多了过拟合学习率是这里面最玄学的。BERT微调有一个不成文的经验范围1e-5到5e-5之间。小于1e-5学不动预训练权重的语义几乎不更新大于5e-5参数剧烈震荡训练曲线像过山车。我习惯从2e-5起手跑两轮看验证F1的走向如果前两轮就掉点直接降一半到1e-5重来。max_len需要单独说。BERT的输入上限是512个token新闻文本动辄上千字所以大多数人会直接设512。但512意味着每批样本的token数翻倍显存压力从max_len256的6-8GB直接跳到12GB以上。我在实践中的做法是先用256跑通流程最后再对比一次512的效果——如果新闻导语已经把情感表达清楚了256和512的F1差距常常在0.5个点以内而这个差距不值得你换显卡。4.3 早停与阈值微调防止模型过拟合到中性类训练轮数在3到5之间就可以收手但新闻数据量小过拟合往往在第三轮尾部就开始了。我一般手动实现一个简单的早停逻辑每个epoch结束看验证F1连续两个epoch不升就提前停不硬跑完设置的轮数。best_f1 0 patience 2 no_improve 0 for epoch in range(training_args.num_train_epochs): trainer.train() eval_result trainer.evaluate() eval_f1 eval_result[eval_f1] if eval_f1 best_f1: best_f1 eval_f1 no_improve 0 trainer.save_model(./best_model) else: no_improve 1 if no_improve patience: print(f第{epoch 1}轮F1未提升提前停止) break注意早停的标准用的是F1而不是loss。原因很实际加了类别权重之后loss下降并不代表模型在少数类上表现变好经常出现loss在降、F1不动的“假训练”现象。你的早停监控指标必须跟最终业务指标一致否则早停会停错位置。另外还有一个容易被忽略的阈值细节模型默认预测时取概率最大的类别但新闻情感倾向往往不是“一边倒”的比如负面概率0.4、中性概率0.35这种结果取负面对业务来说可能太武断。我在上线前会在验证集上统计各类别的概率分布把置信度低于0.5的样本标为“不确定”不硬归类这在舆情场景里比硬分类更实用。5. 避坑清单新闻情感分析最常见的5次翻车5.1 CSV里的中文标签乱码新闻数据很多是从爬虫或旧系统导出的CSV保存时用了GBK编码。pandas默认按UTF-8读取直接崩溃或者读出一堆乱码标签。这个坑看着低级但几乎每个跑新闻数据的人都遇到过。现象pd.read_csv报UnicodeDecodeError或者标签列显示“涓?鏂?”这样的乱码。原因文件实际是GBK编码pandas用了UTF-8解码失败乱码则是反过来的文件是UTF-8但里层代码强制用GBK读。解决读文件时显式指定编码用errorsignore兜底读出来后立刻检查标签列是否只有预期的几个类别值。更稳妥的做法是顺手把数据清洗成UTF-8存一份副本后续所有代码统一用UTF-8读写。5.2 BERT-wwm-ext加载时报词表大小不匹配我在2.3节提过一次但这里值得再重复——这是项目里最高频的报错。现象是加载模型时报size mismatch for word_embeddings.weight或bert.embeddings.word_embeddings.weight相关的维度错误。原因模型和tokenizer不配套。BERT-wwm-ext的词表比原始BERT多了一些词元词表大小不一致直接加载分类头必然报错。解决永远从同一个model_name同时实例化tokenizer和模型不要缓存旧的tokenizer对象复用。如果你的代码里收藏了bert-base-chinese的tokenizer切到hfl/chinese-bert-wwm-ext时务必同步替换tokenizer加载语句这一步能省掉我当年的两小时。5.3 中文路径导致模型加载失败现象在Windows上跑项目模型权重路径带中文目录名from_pretrained直接报错错误信息指向某个不存在的路径。原因Windows下HuggingFace缓存目录默认在用户目录下如果用户名是中文或者设置了中文路径transformers在解析路径时会出问题。解决加载模型前手动指定缓存目录到纯英文路径比如/E:/bert_cache。这个坑在Linux上不存在但用Windows做开发的同事很多提前统一好路径规范能避免大量无效排障。5.4 类别权重过猛正面误判爆炸现象加了类别权重之后正面类的召回率确实上去了但正面精确率掉到惨不忍睹——一大片中性和轻微负面被标成了正面业务方直接质疑模型“是不是只会说好话”。原因权重给太极端。我见过把正面权重设到4以上的配置代价是模型把中性样本也拉进了正面区域决策边界完全倾斜。解决把正面权重控制在1.5到2.5之间并且早停指标用宏平均F1而不是正面召回。我习惯的做法是训练三个权重档位1.5、2.0、2.5分别跑完看验证集的按类别F1明细选一个各类别差距最小的版本上测试集而不是凭感觉定权重。5.5 测试集按来源媒体划分后F1暴跌现象训练和验证都在同一个媒体来源的新闻上表现不错F1超过0.8但按媒体来源划分测试集后F1直接跌到0.62。原因模型学到了媒体风格而不是情感语义。同一家媒体的新闻标题句式高度相似模型记住了这些句式特征换一家媒体就失灵——这是新闻情感分析里最隐蔽的过拟合。解决数据切分时增加一个约束——同一个媒体来源的新闻不要同时出现在训练集和验证集。用GroupKFold或者按source字段手动切分确保测试集完全由训练集没见过的媒体组成。这个操作会让F1下降不少但下降到多低才是模型真实可用的水平。6. 验证与进阶你的模型真的能处理今日新闻吗6.1 用今日新闻做一次压力测试训练结束后别急着看验证集报告。我习惯的做法是打开新闻网站手动复制今天刚发布的5到10条新闻标题和正文逐条用模型预测并且要求模型输出每个类别的置信度概率。def predict_news(model, tokenizer, text): model.eval() inputs tokenizer( text, max_length256, truncationTrue, paddingmax_length, return_tensorspt ).to(device) with torch.no_grad(): logits model(**inputs).logits probs torch.softmax(logits, dim-1) return probs.cpu().numpy()[0] probs predict_news(model, tokenizer, 某企业因安全生产问题被约谈整改工作已全面启动) print(probs) # [负面概率, 中性概率, 正面概率]这个方法的价值在于今天的手写新闻不在训练集里也不遵循训练集的语料分布预测出的结果能最大程度模拟真实上线场景。如果模型把“被约谈”预测成了中性甚至正面说明它对负面语义的捕捉还不够需要回到训练阶段检查数据标注质量或增加负例。我自己的习惯是收集每周的压力测试结果攒够几十条之后做一次人工复核看哪些预测错误是模型问题哪些是标注歧义——后者其实不用管模型只是学会了数据里的样子。6.2 跨时间泛化用过去训练验证今天新闻语料的时间漂移比大多数NLP任务都明显。三年前的新闻和今天的新闻在热点主题、措辞、行文风格上差异巨大。我做过一次验证用2023年之前的新闻训练测试2024年的新闻F1掉到0.68比同年份随机切分的结果低了超过8个点。这不是模型不行而是新闻语料本身在漂移。应对方式有两种一是定期增量微调每月用新标注的数据在原模型上继续训练1到2个epoch二是训练时故意混入不同年份的新闻提高模型对时间变化的不敏感性。对业务系统来说前者更实用——模型部署上线后不是终点每月喂新数据续训一次才能保持住情感分析的灵敏度。6.3 上线前可做的轻量加固蒸馏与概率阈值调优如果模型要部署到线上服务BERT-wwm-ext的推理速度是硬伤。常见做法是用它做教师模型蒸馏一个更小的模型比如BERT-tiny或ALBERT在精度损失可接受的范围内把吞吐提上去。这个方向我自己验证过把BERT-wwm-ext蒸馏到ALBERT-tinyF1从0.76降到0.71但单条推理耗时从30毫秒降到4毫秒对一个舆情接口来说这个取舍是值得的。概率阈值调优则是所有人都能直接做的下一步。默认按argmax取类别很多样本其实处于边界状态。建议在验证集上统计每个类别的置信度分布对置信度低于0.5的样本单独标记为“待人工确认”不要硬给标签。我早期做项目时就是把所有低于阈值的样本都强推给模型硬分类结果业务方拿着错误标签反向投诉到技术团队。现在我在代码里加了置信度过滤反而用减少自动标注量的方式换来了更高的人机协作满意度。这套方案做到这里已经覆盖了从模型选型、数据处理、训练调参到上线验证的完整链路。做新闻情感分析最大的体会是模型选型重要但数据切分和阈值策略同样重要——它们决定你的模型在真实新闻流里能不能站稳。希望帮到你。本文还有配套的精品资源点击获取