十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

BERT-wwm新闻情感分析系统实战:从原理到源码解析

BERT-wwm新闻情感分析系统实战:从原理到源码解析 简介基于预训练模型BERT与BERT-wwm的新闻情感分析系统Python源码包面向计算机、人工智能、数据科学等专业在校生及教师可直接用于课程设计、期末大作业或毕业设计也可作为NLP文本分类入门的进阶项目。压缩包共128个文件主要包含70个Python源码文件覆盖数据预处理、模型训练、预测与ensemble投票融合、44个编译缓存文件、5个shell训练脚本、4个说明文档及3个CSV数据文件整体大小仅2.72MB轻量易用。项目支持训练数据kfold划分与信息查看可调用roberta_wwm_ext_large、roberta_large等预训练模型并预留xlnet_large接口训练时将文本截成k段分别输入语言模型顶层用GRU拼接可通过调节max_length与split_num平衡显存占用与模型效果代码结构清晰。训练完成自动保存模型、配置与结果附带完整使用流程说明便于快速复现和二次开发。已有255人学习下载适合希望系统掌握预训练模型在情感分析任务中应用细节的开发者。1. 从预训练模型到可交付的新闻情感分析系统这条路不是写个分类器那么简单拿到一份“基于预训练模型BERT、BERT-wwm的新闻情感分析系统python源码项目说明数据集”的资源很多人最先想到的是“跑通它然后换自己的数据”。但真正在工程里做过情感分析的人都知道BERT 只是这条链路里最不操心的一环——真正决定系统能不能用的是中文预训练模型的选型、长文本新闻的处理策略、以及最后那个分类模型如何被包装成别人能看懂、能复现、能改参数的 Python 项目。用 BERT-wwm 而不是原版 BERT在中文新闻场景下不是一个“追求 SOTA ”的噱头而是整词掩码对中文短语语义的建模方式更贴合新闻文本的书面语特征。这篇文章会把理论、代码、参数和坑位一次讲透让新手能按步骤跑通让有经验的人能在选型和调参上少走弯路。2. 先把 BERT 与 BERT-wwm 的情感分类原理讲清再拆解源码包的典型结构2.1 BERT 怎么完成情感分类从 token 嵌入到分类头的完整路径BERT 本质是一个双向 Transformer 编码器它本身并没有“情感”这个概念。所谓情感分析只是在预训练模型之上拼接了一个分类头把最后一层[CLS]位置的输出向量映射到类别空间。在 Hugging Face 的transformers体系里BertForSequenceClassification已经把这个过程封装好了。from transformers import BertTokenizer, BertForSequenceClassification tokenizer BertTokenizer.from_pretrained(bert-base-chinese) model BertForSequenceClassification.from_pretrained( bert-base-chinese, num_labels2 ) text 央行宣布降准市场情绪明显回暖。 inputs tokenizer(text, return_tensorspt, truncationTrue, max_length512) outputs model(**inputs) logits outputs.logits proba torch.softmax(logits, dim-1)逻辑说明tokenizer先把中文句子转成input_ids、attention_mask和token_type_ids模型内部经过 12 层 Transformer 编码后取出[CLS]向量输入分类层。num_labels2定义输出维度二分类情感场景下就是正负两个类别。参数说明max_length512是 BERT 的硬上限超过部分会被截断。return_tensorspt指定返回 PyTorch 张量而非 Python 列表这是源码包中最常见的用法也是新手最容易因为格式不匹配而报错的点。2.2 BERT-wwm 与 BERT 的差异整词掩码到底是什么BERT-wwmWhole Word Masking与 BERT 的唯一区别在预训练阶段。原版 BERT 对 token 进行掩码而中文原版 BERT 是在字级别掩码——随机遮住某个字。BERT-wwm 将掩码单位提升到词级别即如果一个词被选中则该词包含的所有字都被掩码。注意中文的“词”不是靠分词器先验切分而是通过分词工具在预训练前对语料进行词边界标注。举例来说原版 BERT 在掩码“不看好”时可能只遮住“看”让模型从上下文猜到“看”而 BERT-wwm 会遮住整词“看好”。后者的难点明显更高模型被迫理解整个短语的语义而不是局部补字。在新闻情感分析中“不看好”、“急转直下”这类短语的情感极性由整体决定整词掩码带来的语义建模能力恰好匹配这个需求。hfl/chinese-bert-wwm-ext是常用的扩展版中文预训练模型比原始 wwm 版本多训练了更多数据在大多数中文下游任务上表现略好。它仍然是 BERT 结构可以无缝替换任何from_pretrained(bert-base-chinese)的位置。2.3 新闻文本与通用评论文本在情感分类上的差异新闻情感分析和商品评论情感分析有本质区别。评论的文本短、口语化、情感词密集新闻的标题与正文文体差异大更多是客观陈述加叙述者态度渗透。新闻中的情感往往隐藏在动词短语和程度副词里而不是直接出现“好”、“差”这类显性情感词。因此直接把针对微博或电商评论训练的情感分类器套到新闻上效果会大幅下降。这也是这个标题的项目必须结合“新闻”场景单独设计的原因。在后续的清洗和训练环节需要针对新闻文本的导语结构、[SEP]分隔的标题正文关系做专门处理。2.4 源码包的标准目录结构一个可复现的训练项目该有哪些文件拿到一份源码包第一步不是跑而是先看清楚目录结构。一个合格的 BERT 情感分析项目应该包含以下模块project/ ├── data/ │ ├── train.csv │ ├── val.csv │ └── test.csv ├── src/ │ ├── model.py │ ├── dataset.py │ ├── train.py │ ├── predict.py │ └── utils.py ├── output/ │ ├── checkpoint-best-f1.pt │ └── eval_results.txt ├── requirements.txt └── README.md逻辑说明data目录存放划分好的数据集src目录放训练和推理代码output目录保存中间结果和最优模型。这套结构不太会因为模型结构变化而需要大规模重构是当前开源项目的主流组织方式。参数说明eval_results.txt应该有每轮验证的 loss、准确率、F1 和保存时间。检查这个文件能最快判断一份源码是否真正运行过——如果只有代码没有输出文件大概率作者只提交了代码而没跑出最终结果。3. 中文预训练选型为什么新闻情感分析更适合 BERT-wwm以及和 RoBERTa 的边界3.1 新闻情感标注与 wwm 预训练目标的契合点新闻文本对否定语义极其敏感。“专家表示经济并未衰退”、“公司否认存在违规行为”这类句子里的情感判断取决于“并未”、“否认”这些否定词覆盖的范围。原版 BERT 的字级掩码在预训练时可能只掩掉否定词的一个字比如掩掉“否”而保留“认”模型还是能从“认”推断出“否”字的概率。wwm 把“否认”作为整词掩码模型无法只依赖词的某个字必须从上下文中重构整个否定词。这种训练强度上的差异在新闻语料这种否定结构密集的场景下会被放大。实际使用中我一般优先试hfl/chinese-bert-wwm-ext它比原版bert-base-chinese少踩很多坑。“hfl”是 Hugging Face 上的一个组织名称chinese-bert-wwm-ext是它发布的模型权重名称不是某个不可信的第三方包。在源码里写from_pretrained(hfl/chinese-bert-wwm-ext)的时候系统会自动下载对应权重不需要本地准备文件。3.2 RoBERTa 中文版和 BERT-wwm 在新闻场景的实测边界热词里提到的roberta中文预训练模型也常被拿来对比。中文 RoBERTa 在 BERT-wwm 之上做了两点改动更多训练步数和动态掩码策略。动态掩码意味着每个 epoch 掩码位置不同模型每次看到一个句子的掩码状态都不一样理论上泛化性更好。但在新闻情感分析这个任务上RoBERTa 的提升并不总是显著的。如果数据集中句子较短、情感词明显BERT-wwm-ext 和中文 RoBERTa 的差异往往在 0.5 个点的 F1 以内。而中文 RoBERTa 的权重通常更大、推理更慢部署成本更高。我的经验是先用 wwm-ext 作为基线模型完成整个新闻情感分析系统的开发和验证如果后续发现系统在长尾表达上持续有误报再把分类器替换为hfl/chinese-roberta-wwm-ext做对比实验。不要从一开始就在多个预训练模型之间摇摆。3.3 加载 BERT-wwm 作为情感分类器的 3 个常用参数from transformers import BertConfig, BertForSequenceClassification config BertConfig.from_pretrained(hfl/chinese-bert-wwm-ext) config.num_labels 2 config.id2label {0: negative, 1: positive} config.label2id {negative: 0, positive: 1} model BertForSequenceClassification.from_pretrained( hfl/chinese-bert-wwm-ext, configconfig, ignore_mismatched_sizesTrue )逻辑说明config控制模型结构和分类头参数。id2label和label2id定义了情感标签的映射关系如果漏掉这一步模型预测输出的是LABEL_0而不是negative下游表格展示就不直观。参数说明ignore_mismatched_sizesTrue非常关键。若从原版 BERT 加载权重到 BERT-wwm 结构或者从三分类模型切换为二分类模型最后一层权重维度不匹配会让from_pretrained直接报错。这个参数允许跳过分类头权重只加载 encoder 部分的预训练参数。3.4 一个反直觉结论冻结部分预训练层可能让新闻情感分析效果更好新闻语料与预训练语料差异较大尤其是财经新闻和政策新闻里的专有名词比例高。常规做法是微调所有层但实际操作中冻结 embedding 层和低层 Transformer 层只微调后 4 层与分类头往往能防止模型在少量标注数据上过拟合到新闻中的专有名词。model BertForSequenceClassification.from_pretrained(hfl/chinese-bert-wwm-ext) for name, param in model.named_parameters(): if encoder.layer in name: layer_num int(name.split(.)[2]) if layer_num 8: param.requires_grad False逻辑说明named_parameters返回参数名和参数张量encoder.layer后面跟的数字就是 Transformer 层的索引。前 8 层的参数被冻结后反向传播不会更新它们只有后 4 层和分类头参与学习。参数说明这个技巧不适用于极端小数据若训练集少于 1000 条冻结前 10 层会让可学习参数太少模型欠拟合。常见做法是从冻结前 6 层开始试根据验证集 F1 决定下探层数。4. 数据集处理与源码包的 Python 实现细节长文本、标签不均衡和可复现训练4.1 新闻文本的截断策略不是简单地截前 512 个字新闻文本普遍超过 512 个 token而 BERT 的max_position_embeddings通常为 512。直接把正文前 512 字作为输入会丢掉新闻的核心信息——新闻的情感倾向往往在结尾部分才给出判断。B站用户评论可以做到短文本全量输入但新闻系统必须做“策略性截断”。常见做法是标题全文保留正文部分截取前 300 个字加后 200 个字。import pandas as pd def news_truncate(title, body, max_len450): body_prefix body[:300] body_suffix body[-200:] combined f{title}。{body_prefix}……{body_suffix} return combined[:max_len]逻辑说明标题和正文的拼接顺序会影响情感分析结果。新闻标题往往凝练编辑倾向正文后段则是事件结果。以标题 导语 正文尾部的方式拼接可以让 BERT 同时看到开头和结尾的信息比单纯截前 512 字合理得多。参数说明max_len450留下来约 60 个 token 的余量给[SEP]、[CLS]等特殊字符避免截断后拼接异常。如果数据集中有些正文长度只有 50 字body[:300]不会报错只会返回全部内容。4.2 构建 Dataset 与 DataLoader 的源码级结构Hugging Facetransformers自带了Trainer但很多源码包会自己实现Dataset和DataLoader以便更细粒度地控制采样策略。以下是新闻情感分类场景中一份标准的dataset.pyfrom torch.utils.data import Dataset from transformers import BertTokenizer class NewsSentimentDataset(Dataset): def __init__(self, df, tokenizer_name, max_len512): self.df df.reset_index(dropTrue) self.tokenizer BertTokenizer.from_pretrained(tokenizer_name) self.max_len max_len def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.loc[idx] title str(row[title]) body str(row[content]) text f{title}。{body[:450]} if body else title label int(row[label]) inputs self.tokenizer.encode_plus( text, truncationTrue, max_lengthself.max_len, paddingmax_length, return_tensorspt, ) return { input_ids: inputs[input_ids].squeeze(0), attention_mask: inputs[attention_mask].squeeze(0), labels: torch.tensor(label, dtypetorch.long), }逻辑说明encode_plus在源码包里比tokenizer(text, ...)更常见它显式返回所有编码结果。squeeze(0)去掉 batch 维让每条样本以[seq_len]形状返回Dataloader 会自动 stack 成[batch_size, seq_len]。参数说明paddingmax_length会让所有样本补齐到 512虽然浪费显存但可以避免 Dataloader 动态 concat 时报错。如果你的显存吃紧可以改成paddingTrue配合collate_fn动态填充但要注意attention_mask必须跟着动态填充变化。4.3 训练脚本中的 4 个核心参数batch size、学习率、warmup、梯度累积from transformers import get_linear_schedule_with_warmup training_args { epochs: 3, batch_size: 16, learning_rate: 2e-5, warmup_ratio: 0.1, weight_decay: 0.01, } optimizer torch.optim.AdamW(model.parameters(), lrtraining_args[learning_rate]) total_steps len(train_loader) * training_args[epochs] scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_stepsint(total_steps * training_args[warmup_ratio]), num_training_stepstotal_steps, )逻辑说明warmup让学习率在前 10% 的 step 里从 0 线性升到预设值防止冻结层以外的分类头在初始梯度方向不稳定。新闻数据集通常标签噪声大warmup 可以提高收敛稳定性。参数说明batch_size16在 12G 显存下是安全的如果文本长度都接近 512需要降低到 8 并使用梯度累积模拟大 batch。weight_decay0.01只在 LayerNorm 和 bias 之外生效这是 BERT 微调的标准配置。梯度累积的代码段如下accumulation_steps 4 for step, batch in enumerate(train_loader): outputs model(**batch) loss outputs.loss / accumulation_steps loss.backward() if (step 1) % accumulation_steps 0: optimizer.step() scheduler.step() optimizer.zero_grad()逻辑说明loss outputs.loss / accumulation_steps将单步 loss 除以累积步数让 4 次小 batch 的梯度累积效果等同于 1 次大 batch。这在处理超长新闻文本时是必要手段因为单 batch 大小无法容纳 512 token 的输入。4.4 类别不均衡时F1 比准确率更值得追踪新闻数据集中正负样本比例很少是 5:5尤其是在筛选“负面新闻”的场景里负面样本可能只占 15%。直接优化准确率会导致模型把大多数样本预测为多数类。此时需要在compute_metrics里返回 F1用 F1 作为模型保存的判定指标。from sklearn.metrics import f1_score, accuracy_score def compute_metrics(eval_pred): logits, labels eval_pred preds logits.argmax(-1) f1 f1_score(labels, preds, averagebinary) acc accuracy_score(labels, preds) return {f1: f1, acc: acc}逻辑说明eval_pred是Trainer在评估阶段传入的元组包含 logits 和 labels。argmax(-1)在最后一维取最大概率对应的索引。这个函数返回字典会被Trainer内部用于日志记录和 checkpoint 打分。参数说明averagebinary要求标签恰好是两个类别如果数据里出现-1表示中性但代码没过滤f1_score会报错。常见做法是在数据预处理阶段去掉中性样本或单独映射为 0/1。5. 从源码包到可运行系统的三个步骤环境搭建、运行训练、口径对齐5.1 CPU 与 GPU 环境下的运行差异一份源码包是否有用拿到手先看它默认的设备配置。model.to(cuda)在无 GPU 的机器上直接报错而model.to(cpu)在 GPU 机器上只是慢不会报错。device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device)逻辑说明这是判断运行环境的规范写法。调用torch.cuda.is_available()动态选择设备避免硬编码。如果你使用的是 Mac M 系列芯片mps也是可选设备但更常见的做法是直接用 CPU 跑一个小 epoch 验证源码是否正确。参数说明CPU 上跑 BERT 训练非常慢一个 epoch 可能需要数小时。源码包里如果自带sample_data.csv这类文件先在 100 条样本上跑通流程再切换到完整数据集。5.2 训练一个最小可复现的新闻情感分类器完整代码串联以下代码把前四节的要点串起来形成一个可以直接运行的源码文件适用于任意包含title、content、label三列的 CSV 数据import torch import pandas as pd from torch.utils.data import DataLoader from transformers import BertTokenizer, BertForSequenceClassification, AdamW df pd.read_csv(data/news_sentiment.csv) texts [f{t}。{c[:450]} for t, c in zip(df[title], df[content])] labels df[label].astype(int).tolist() tokenizer BertTokenizer.from_pretrained(hfl/chinese-bert-wwm-ext) model BertForSequenceClassification.from_pretrained( hfl/chinese-bert-wwm-ext, num_labels2 ) encodings tokenizer( texts, truncationTrue, paddingTrue, max_length512, return_tensorspt ) class NewsDataset(torch.utils.data.Dataset): def __getitem__(self, idx): return { input_ids: encodings[input_ids][idx], attention_mask: encodings[attention_mask][idx], labels: torch.tensor(labels[idx], dtypetorch.long), } def __len__(self): return len(labels) loader DataLoader(NewsDataset(), batch_size8, shuffleTrue) optimizer AdamW(model.parameters(), lr2e-5) for epoch in range(3): for step, batch in enumerate(loader): outputs model(**batch) loss outputs.loss loss.backward() optimizer.step() optimizer.zero_grad() torch.save(model.state_dict(), fcheckpoint-epoch-{epoch}.pt)逻辑说明这份代码省略了 warmup 和梯度累积只保留训练主链路。batch8是为 CPU 或低显存环境预留若你的显卡显存大于 16G可以调整到 16 或 32 并移除paddingTrue改回paddingmax_length避免动态 shape 影响性能。参数说明truncationTrue配合max_length512会让 tokenizer 自动截断超长文本不必在数据预处理里手动截断。这比前文news_truncate更保险但会丢掉正文尾部信息所以数据预处理阶段仍然建议先做好标题与正文比例设计。5.3 源码包常见报错tokenizer 与模型不匹配的排查逻辑最常见的报错之一是Some weights of BertForSequenceClassification were not initialized from the model checkpoint at hfl/chinese-bert-wwm-ext。这句话说明你加载的模型权重是 12 层 encoder但分类头是随机初始化的——这是正常现象不是错误但会挡掉很多人。如果日志里出现The model weights are not tied则说明你换用了不同num_labels的 checkpointignore_mismatched_sizes没打开。如果出现Tried to instantiate class BertForSequenceClassification but no such class则是 transformers 安装版本太旧升级包版本即可。pip install transformers4.20.0逻辑说明transformers4.20 之后才统一了AutoModelForSequenceClassification和BertForSequenceClassification的加载入口。源码包里写model BertForSequenceClassification.from_pretrained(hfl/xxx)在旧版本下同样可用但生态兼容性差。5.4 保存与加载 checkpoint 的口径对齐训练完的state_dict是纯参数文件不含 tokenizer 和 config。正确的保存方式是保存整个模型目录model.save_pretrained(output/news-sentiment-wwm) tokenizer.save_pretrained(output/news-sentiment-wwm) loaded_model BertForSequenceClassification.from_pretrained( output/news-sentiment-wwm )逻辑说明save_pretrained会同时保存config.json与pytorch_model.bin下次加载时不需要手动指定num_labels。对比torch.save(model.state_dict())的方式目录保存更利于工程交付和版本回滚。参数说明如果你从源码包拿到的只有.bin权重文件而没有config.json加载时必须在from_pretrained里显式传config或num_labels。这类残缺包在 GitHub 等平台非常常见需要额外留意。6. 系统验证心得用置信度阈值和典型样本来检查模型在新闻数据上的真实表现6.1 用验证集预测分布动态调阈值BERT 分类器默认取概率最大的类别但新闻情感分析的目标往往不是同等关注正类和负类。在财经风控场景中系统需要尽可能多地召回负面新闻这时可以把阈值从默认 0.5 调低到 0.3。with torch.no_grad(): logits loaded_model(**inputs).logits proba torch.softmax(logits, dim-1) negative_prob proba[0][0].item() predicted 1 if negative_prob 0.3 else 0逻辑说明negative_prob proba[0][0]取出第一个类别的概率假设0是负面。predicted (negative_prob 0.3)表示只要负面概率超过 0.3 就算负面样本这样会让模型更敏感但也更容易产生假正例。参数说明阈值不是模型参数不会随 checkpoint 保存。每个业务场景需要的阈值都不同建议在下游脚本里把这个值配置化而不是硬编码在预测函数中。6.2 最容易暴露 BERT 新闻模型毛病的样本类型信息缺失与价格主体超过一半的分析偏差来自“需要事件背景才能判断情感”的新闻条目。例如标题为“XX 公司股价剧烈波动”正文通篇讲宏观政策没有直接点明利好还是利空。这类文本模型强答出的标签置信度普遍在 0.5 到 0.7 之间。在验证模型时我建议挑出置信度在 0.45 到 0.65 之间的样本逐条看原文。suspicious [] for i in range(len(val_df)): conf val_pred_proba[i] if 0.45 conf 0.65: suspicious.append((val_df[title][i], val_df[label][i], conf))逻辑说明置信度区间内的样本大多表现为情感表达含糊或标签噪声较大。这些比 high-confidence error 更值得排查因为前者说明数据标注问题而后者可能只是模型偶然判断错误。6.3 误报消解的常用手段把预测结果与规则策略拼接如果模型在“政策中性新闻”上持续输出负面一个有效手段是在源码分类器外层套一个环境词典。预置与内部正负面无关的词汇结构让模型的输出在这个词表范围内被覆盖或降权。比如“召开会议、正式发布”这类新闻强制归入中性类别。def rule_based_override(text, proba): neutral_keywords [记者, 通报, 例行, 纪要] for kw in neutral_keywords: if kw in text: return 1 # 归入非负面 return None逻辑说明rule_based_override在模型置信度低于 0.5 时优先返回规则判断如果规则命中直接覆盖模型输出。它不是为了替代语义模型而是拦截掉明显中性的新闻误报。注意不能对置信度高的样本也执行覆盖否则会破坏已经学好的情感边界。6.4 一份可带走的 shell 验证命令python predict.py \ --model_dir output/news-sentiment-wwm \ --test_file data/test.csv \ --output_file output/predictions.csv \ --threshold 0.5逻辑说明predict.py读入测试集按行推理输出标签和置信度。命令里的--threshold会传到预测函数中与第六节讨论阈值的作用一致。output/predictions.csv应该同时包含原始文本、预测类别和置信度供后续人工抽检。参数说明如果源码包里没有提供predict.py可以直接在 Python 交互式环境中调用上一节的NewsSentiment类完成相同功能。验证的最短路径是确认测试集上负类的召回率比基线模型不低于两个百分点才算真正把 wwm 的优势带进了新闻场景。本文还有配套的精品资源点击获取
返回列表