ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python NLP实战:新闻标题情感分析从TF-IDF到BERT

Python NLP实战:新闻标题情感分析从TF-IDF到BERT 简介这份资源是面向高校学生与NLP入门者的新闻标题情感分析项目源码适合用作课程大作业、毕业设计参考或自然语言处理练手案例帮助解决从文本预处理到情感判别的完整实现问题。压缩包共6个文件约420KB包含Python主程序脚本、新闻标题数据集CSV、说明文档Markdown、开源协议及两张运行效果截图覆盖数据读取、分词处理、情感分类与结果展示等环节结构精简便于快速理解项目脉络。目前已有276人学习下载说明该案例在同类作业中具有一定参考价值。读者可据此掌握新闻标题情感倾向的判定流程了解NLP算法在短文本场景下的落地方式并借助现成数据集与脚本复现实验、调整模型参数或替换语料为后续扩展至评论、弹幕等文本分析任务提供可复用的代码框架与排错思路。1. 新闻标题情感分析从一条“标题党”说起刷到一条新闻推送标题写着“某地楼市一夜回暖购房者排队抢房”点进去发现正文讲的是某个楼盘搞促销送车位。标题的情绪和正文完全两回事——这种落差正是新闻标题情感分析要处理的问题。基于 Python 的 NLP 算法做新闻标题情感分析核心目标就一句话给一条短文本标题判断它偏正面、负面还是中性并给出可解释的置信度。它适合三类人想入门 NLP 的 Python 学习者、需要做舆情监控或内容分发的后端工程师、以及拿它当课程设计或毕业设计的同学。标题短、口语化、反讽多比长文本情感分析更考验特征工程和模型选型这也是它值得单独做一个项目的原因。2. 新闻标题情感分析的技术选型为什么不能直接套用长文本方案2.1 标题文本的三个特殊性决定了预处理策略新闻标题和微博、评论、影评都不一样。第一长度极短通常 10 到 30 个字去掉停用词后有效特征可能只剩五六个词TF-IDF 向量会非常稀疏。第二标题里高频出现“暴涨”“暴跌”“紧急”“重磅”这类情绪强度词但它们的方向性依赖上下文比如“重磅利好”和“重磅利空”只差一个字。第三标题常省略主语和连接词句法结构不完整依存句法分析基本用不上。所以预处理不能照搬长文本那套“分词→去停用词→词干化”就完事。我一般会做三件事保留程度副词“大幅”“略微”因为它们直接调制情感强度对否定词做特殊标记把“不看好”合并成一个 token 而不是拆成“不”和“看好”对数字和百分比做归一化比如“涨了 3%”和“涨了 5%”在情感极性上应该一致但强度不同。import jieba import re # 自定义词典把情感短语作为整体保留 sentiment_phrases [不看好, 超预期, 不及预期, 大幅上涨, 小幅回落, 创历史新高] for phrase in sentiment_phrases: jieba.add_word(phrase) def preprocess_title(title): # 去除特殊符号保留中文、数字和百分号 title re.sub(r[^\u4e00-\u9fa50-9%], , title) # 全角转半角 title title.replace(, %) # 分词 words jieba.lcut(title) # 过滤单字停用词但保留否定词和程度副词 stopwords {的, 了, 在, 是, 和, 与, 及} keep_words {不, 没, 未, 很, 太, 超, 略} words [w for w in words if w not in stopwords or w in keep_words] return words # 测试 print(preprocess_title(楼市一夜回暖购房者排队抢房涨幅超预期)) # 输出: [楼市, 一夜, 回暖, 购房者, 排队, 抢房, 涨幅, 超预期]这段代码的关键在jieba.add_word那几行。如果不把“超预期”加进词典jieba 会切成“超”和“预期”前者是程度词后者是中性词模型学到的权重就散了。参数上停用词表不要用网上下载的通用大表那个表会把“不”“没”也删掉导致否定信息丢失。我一般只保留 20 到 30 个高频虚词作为停用词其余全部保留。2.2 模型选型从 TF-IDF 逻辑回归到 BERT 微调的取舍选型要看你的数据量和部署环境。如果只有几千条标注数据别上 BERT会过拟合到怀疑人生。常见做法是 TF-IDF 加逻辑回归或 SVM训练快、可解释、部署简单。如果有一万条以上标注数据且允许 GPU 推理用 BERT 类预训练模型微调准确率通常能提升 8 到 15 个百分点。方案数据量要求训练时间推理速度可解释性适用场景TF-IDF 逻辑回归1000秒级极快强课程设计、快速原型Word2Vec LSTM5000分钟级快中中等规模舆情系统BERT 微调10000小时级慢弱高精度生产环境我一般会先用 TF-IDF 方案跑一个基线看混淆矩阵里哪类错得多。如果负面和中性混淆严重说明特征区分度不够再考虑上 BERT。不要一上来就 BERT调参成本高而且标题短文本在 BERT 上容易受 [CLS] token 表征不充分的影响需要额外做池化策略调整。2.3 用 TF-IDF 和逻辑回归跑通第一个基线模型下面是一个完整可复现的基线流程。假设你已经有一份 CSV 文件两列title和labellabel 取值为 0负面、1中性、2正面。import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report # 读取数据 df pd.read_csv(news_titles.csv) # 用空格连接分词结果作为 TF-IDF 输入 df[seg] df[title].apply(lambda x: .join(preprocess_title(x))) # 划分训练集和测试集stratify 保证类别比例一致 X_train, X_test, y_train, y_test train_test_split( df[seg], df[label], test_size0.2, random_state42, stratifydf[label] ) # TF-IDF 向量化限制最大特征数过滤低频词 vectorizer TfidfVectorizer( max_features5000, # 标题短5000 足够覆盖 ngram_range(1, 2), # 加入二元词组捕捉“大幅上涨”这类组合 min_df2, # 至少出现 2 次才保留降噪 max_df0.95 # 出现在 95% 以上文档的词丢弃 ) X_train_vec vectorizer.fit_transform(X_train) X_test_vec vectorizer.transform(X_test) # 逻辑回归多分类用 multinomialsolver 选 lbfgs clf LogisticRegression( C1.0, # 正则强度默认 1.0过拟合就调小 max_iter1000, multi_classmultinomial, solverlbfgs ) clf.fit(X_train_vec, y_train) # 评估 y_pred clf.predict(X_test_vec) print(classification_report(y_test, y_pred, target_names[负面, 中性, 正面]))参数说明ngram_range(1,2)对标题特别重要因为“不涨”和“涨”是两个完全不同的信号一元词会丢掉这个区别。min_df2是防止只出现一次的生僻词进入特征空间标题数据里这种词往往是噪声。C1.0是正则化强度的倒数值越小正则越强如果发现训练集准确率 98% 但测试集只有 70%先把 C 调到 0.1 试试。跑完这个基线你大概能得到 75% 到 85% 的准确率取决于数据质量和标注一致性。如果低于 70%先检查标注有没有问题而不是急着换模型。3. 从词向量到 BERT把准确率再往上推一截3.1 用 Word2Vec 做特征增强的实操步骤TF-IDF 的弱点是它把每个词当成独立维度学不到“上涨”和“攀升”的语义相似性。Word2Vec 可以把词映射到稠密向量语义相近的词在向量空间里距离近。做法是先在大量无标注新闻标题上训练 Word2Vec得到词向量表然后把每条标题的词向量取平均或加权平均作为分类器的输入特征。from gensim.models import Word2Vec import numpy as np # 用全部标题含无标注训练词向量 all_sentences df[title].apply(preprocess_title).tolist() w2v_model Word2Vec( sentencesall_sentences, vector_size100, # 标题短100 维足够 window3, # 窗口小因为标题词序紧凑 min_count2, # 低频词不训练 workers4, epochs20 ) def title_to_vec(words): # 只保留在词向量表中的词 vecs [w2v_model.wv[w] for w in words if w in w2v_model.wv] if len(vecs) 0: return np.zeros(100) # 加权平均IDF 越高的词权重越大 return np.mean(vecs, axis0) X_w2v np.array([title_to_vec(words) for words in all_sentences])这里window3是刻意调小的。新闻标题通常只有十几二十个字窗口设 5 或 10 会把不相关的词也纳入上下文反而降低词向量质量。vector_size100也是同样道理标题语义空间没那么复杂维度太高会稀疏。拿到X_w2v后可以接一个 SVM 或浅层神经网络。我一般用 SVM因为小样本下它比神经网络稳定。from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_w2v_scaled scaler.fit_transform(X_w2v) X_train_w, X_test_w, y_train_w, y_test_w train_test_split( X_w2v_scaled, df[label], test_size0.2, random_state42, stratifydf[label] ) svm_clf SVC(kernelrbf, C1.0, gammascale) svm_clf.fit(X_train_w, y_train_w) print(svm_clf.score(X_test_w, y_test_w))Word2Vec SVM 通常比 TF-IDF 逻辑回归高 3 到 5 个百分点但前提是你的无标注数据至少有几千条。如果只有几百条词向量训练不充分效果可能反而不如 TF-IDF。3.2 BERT 微调时标题短文本的三个调参要点BERT 做标题情感分析最大的坑是max_length设太大。标题平均 20 个字tokenize 后大概 25 到 30 个 token你设max_length128会填充大量[PAD]不仅浪费算力还会让注意力机制分散。我一般设max_length48覆盖 99% 的标题。第二个坑是学习率。BERT 微调的学习率要比预训练小一到两个数量级常用 2e-5 到 5e-5。设 1e-3 会直接把预训练权重冲垮loss 震荡不下降。第三个坑是池化策略。标题短文本用[CLS]token 的表征做分类效果不如对所有 token 输出做平均池化。因为[CLS]是在句首对标题后半段的情感词关注不够。from transformers import BertTokenizer, BertForSequenceClassification import torch tokenizer BertTokenizer.from_pretrained(bert-base-chinese) model BertForSequenceClassification.from_pretrained(bert-base-chinese, num_labels3) def encode_title(title): return tokenizer( title, max_length48, paddingmax_length, truncationTrue, return_tensorspt ) # 训练时用 AdamW学习率 2e-5 optimizer torch.optim.AdamW(model.parameters(), lr2e-5, weight_decay0.01)如果显存不够把batch_size降到 16 或 8同时把梯度累积步数设成 2 或 4等效于更大的 batch。不要用max_length512去跑标题那是给长文档用的。3.3 类别不平衡时用 focal loss 替代交叉熵新闻标题情感分布通常不均匀中性标题占大多数正面和负面偏少。直接用交叉熵会让模型倾向于预测中性负面和正面的召回率很低。Focal loss 通过降低易分类样本的权重让模型聚焦难样本。import torch.nn.functional as F class FocalLoss(torch.nn.Module): def __init__(self, alpha1, gamma2): super().__init__() self.alpha alpha self.gamma gamma def forward(self, inputs, targets): ce_loss F.cross_entropy(inputs, targets, reductionnone) pt torch.exp(-ce_loss) focal_loss self.alpha * (1 - pt) ** self.gamma * ce_loss return focal_loss.mean() # 使用 criterion FocalLoss(alpha1, gamma2)gamma2是原论文推荐值表示对难样本的关注程度。alpha用来平衡类别如果负面样本特别少可以把 alpha 设成 2 或 3。注意 focal loss 对学习率敏感用的时候把学习率再降一半。4. 避坑与排查新闻标题情感分析里最容易翻车的五个地方4.1 标注一致性差导致模型学不到边界现象训练集准确率 95%测试集只有 65%混淆矩阵里正面和中性大量互错。原因标注时没有明确标准。“某公司发布新品”算中性还是正面不同标注员判断不一致模型学到的是噪声。解决先写一份标注规范明确“中性”的定义——不含明显褒贬词、不表达情绪倾向的陈述句。然后抽 200 条做双人标注计算 Kappa 系数低于 0.7 就重新对齐标准。我一般会要求标注员对每条标题写一句判断理由方便回溯。4.2 否定词被分词器拆散导致极性反转现象模型把“不看好后市”预测为正面因为“看好”是正面词。原因jieba 默认把“不”和“看好”切成两个 tokenTF-IDF 里“看好”权重高“不”权重低模型忽略了否定。解决在分词前用自定义词典把“不看好”“不及预期”“未达标”等否定短语加进去让它们作为整体 token。或者在预处理阶段用规则把“不正面词”替换成一个特殊标记比如NEG_看好。4.3 标题里的数字和百分比被当成情感信号现象模型把“涨幅 3%”和“涨幅 300%”预测成不同极性但实际都是正面。原因TF-IDF 把“3%”和“300%”当成不同特征模型学到了数字本身和标签的虚假相关。解决在预处理阶段把百分比和数字归一化比如统一替换成NUM%或NUM。如果强度信息重要单独加一列“强度特征”不要混在文本特征里。4.4 用 accuracy 评估导致负面类完全被忽略现象模型 accuracy 有 80%但负面类召回率只有 20%。原因中性样本占 70%模型全预测中性就能拿 70% 准确率但业务上负面漏报代价很大。解决评估时看 macro F1 和各类召回率不要只看 accuracy。训练时用 class_weightbalanced 或 focal loss。如果负面样本极少先做过采样或 SMOTE但注意 SMOTE 对文本高维稀疏特征效果一般优先考虑 focal loss。4.5 部署时 tokenizer 和训练时不一致现象本地测试准确率 85%上线后掉到 60%。原因训练时用 jieba 分词后接 TF-IDF部署时直接传原始标题给模型没有走同样的预处理管道。解决把预处理逻辑封装成一个类或函数训练和推理共用同一份代码。用 sklearn 的 Pipeline 把 vectorizer 和 classifier 串起来保存整个 pipeline 而不是单独保存模型。from sklearn.pipeline import Pipeline pipeline Pipeline([ (tfidf, TfidfVectorizer(max_features5000, ngram_range(1,2))), (clf, LogisticRegression(C1.0, max_iter1000)) ]) pipeline.fit(X_train, y_train) # 保存 pipeline推理时直接 predict import joblib joblib.dump(pipeline, sentiment_pipeline.pkl)5. 用置信度阈值和错误分析把模型调到能用模型跑通不难难的是让它达到“敢用”的程度。我的习惯是先看 macro F1如果低于 0.75回去查标注和特征如果在 0.75 到 0.85 之间做错误分析找系统性偏差高于 0.85 再考虑上线。错误分析具体怎么做把测试集里预测错的样本按置信度从高到低排序先看那些“模型很自信但错了”的样本。这类样本往往暴露了标注错误或特征盲区。比如模型把“股价小幅回调”自信地预测为正面原因是“回调”在训练集里多数出现在正面语境但这条标题的“小幅”暗示了负面倾向。这时候要么补充这类样本要么在特征里加入程度副词和情感词的交互项。另一个实用技巧是设置置信度阈值。对于置信度低于 0.6 的样本不直接输出极性而是标记为“待人工复核”。这在舆情监控场景里很关键宁可漏报也不要误报。# 获取预测概率 proba pipeline.predict_proba(X_test) max_proba proba.max(axis1) pred pipeline.predict(X_test) # 低于阈值的不输出 threshold 0.6 final_pred [p if m threshold else -1 for p, m in zip(pred, max_proba)]最后说一个我踩过的坑不要用测试集调参。我见过有人反复在测试集上试不同 C 值最后测试集准确率 90%换一批新数据直接掉到 60%。正确做法是划出验证集调参测试集只在最后评估一次。如果数据量实在少用交叉验证但交叉验证的分数通常比真实泛化高 3 到 5 个百分点心里要有数。这个方向值不值得做如果你只是想跑通一个 NLP 项目练手TF-IDF 加逻辑回归两天就能出结果。如果想做成能上线的舆情工具重点不在模型多深而在标注质量和预处理管道的一致性。我自己的习惯是先把基线跑出来再根据错误分析决定要不要上 BERT而不是反过来。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进