金融文本情感分析模型:Python工程实战与避坑指南
2026/9/24 18:18:51 网站建设 项目流程

简介:这份资源是面向计算机、人工智能、自动化等专业学生与从业者的金融文本情感分析项目源码包,源自个人毕业设计,答辩评审分达98分,代码经调试测试可稳定运行。项目以Python实现金融文本情感分析模型,涵盖数据清洗、分词处理、特征提取、模型训练与结果可视化等完整流程,适合作为课程设计、大作业或毕业设计的参考模板,也便于基础较好的学习者在此基础上修改扩展。压缩包共79个文件,约3.59MB,包含17个py源码文件、36个txt数据与说明文本、6个plk模型文件,以及xml配置、json、npz、csv、xlsx等数据与配置资源,另附md文档、pdf说明和png结果图,目录结构清晰,便于按模块查阅。目前已有203人学习下载,可帮助读者快速理解金融情感分析项目的整体架构、模型实现思路与数据处理细节,节省从零搭建的时间成本。

1. 金融文本情感分析模型:从一条股评到可复现的 Python 工程

一条股吧评论写着「业绩超预期但股价已经透支」,传统词典法会因为它同时含「超预期」和「透支」判成中性,而金融场景里这句话的真实倾向是偏空。金融文本情感分析要解决的就是这类问题:把研报摘要、财报问答、股评、公告标题里的主观倾向,映射成可量化、可回测的信号。它和通用情感分析最大的差别在于领域词典、否定与转折处理、以及数值上下文的权重。这套基于 Python 的金融文本情感分析模型,适合做量化因子、舆情监控、课程设计或人工智能大作业的从业者和学生。下面按「数据怎么来、模型怎么搭、参数怎么调、坑在哪」的顺序,把一套能跑通的方案讲清楚,源码结构也会在中间章节落到具体文件。

2. 数据与标签:金融语料从哪来、怎么标才不翻车

2.1 三类可用语料与获取边界

金融情感分析的语料来源大致分三类,选哪类决定了后面模型的上限。

第一类是公开标注数据集,比如中文金融情感语料里常见的股评、研报句子级标注,通常带 0/1/2 三分类标签。这类数据拿来就能训练,缺点是领域窄、句子短、口语化程度低,直接上生产会明显掉点。

第二类是自采语料,用 Python 爬虫抓股吧、财经资讯标题、互动易问答。这里要注意合规边界:只抓公开页面、控制频率、遵守 robots,不要碰需要登录或明确禁止抓取的内容。抓下来的是原始文本,标签得自己造。

第三类是弱标注,用涨跌方向、研报评级变动、公告类型反推情感标签。比如公告标题含「预增」「中标」标正,含「减持」「问询」标负。弱标注噪声大,但量大,适合做预训练或蒸馏的教师信号。

我一般会先用公开数据集把 pipeline 跑通,再混入自采数据做领域适配。纯自采从零标几千条,成本高且一致性差,新手容易在这一步就放弃。

2.2 标签体系:三分类还是回归

金融情感到底分几类,是个必须先定死的问题。常见两种:

方案标签适用场景缺点
三分类正/中/负舆情监控、信号计数中性样本难界定
五分类强正/弱正/中/弱负/强负研报评级、精细因子标注一致性差
回归情感得分 [-1,1]因子加权、组合标注成本最高

新手建议先做三分类,把「中性」定义为不含明显倾向或正负抵消的句子。等 pipeline 稳了再考虑回归。标签体系一旦定了,后面所有脚本、评估指标、可视化都围绕它走,中途改标签等于重做。

2.3 清洗与分词:金融文本的三个特殊处理

通用中文清洗流程是去 HTML、去表情、统一全半角、去停用词。金融文本还要额外做三件事:

一是保留数值和单位。「增长 30%」和「增长 3%」情感强度完全不同,清洗时不能把数字当噪声删掉,反而要把「30%」归一化成 token。

二是处理股票代码和简称。「600519」「茅台」「贵州茅台」指向同一实体,做实体归一能减少稀疏。

三是分词词典要加领域词。用 jieba 默认词典,「涨停」「破净」「商誉减值」可能被切碎。加载自定义词典是必做步骤。

import jieba import re # 加载金融领域自定义词典,每行格式:词语 词频 词性 jieba.load_userdict("finance_dict.txt") def clean_finance_text(text): # 去 HTML 标签 text = re.sub(r"<[^>]+>", "", text) # 全角转半角 text = text.replace("%", "%").replace(".", ".") # 数值+单位归一化,保留百分比语义 text = re.sub(r"(\d+(?:\.\d+)?)\s*%", r" NUM_PERCENT ", text) # 去多余空白 text = re.sub(r"\s+", " ", text).strip() return text def tokenize(text): text = clean_finance_text(text) return [w for w in jieba.lcut(text) if w.strip()]

这段代码的逻辑是:先做通用清洗,再把百分比替换成占位符NUM_PERCENT,让模型学到「有数值修饰」这个特征,而不是记住具体数字。finance_dict.txt需要自己维护,格式是「词语 词频 词性」,词频给个中等值如 100 即可,词性可省略。分词后过滤空白,停用词可以在后续 TF-IDF 阶段再处理,不必在分词时删,方便调试。

提示:自定义词典不要一次性塞几千个词,先加 50 到 100 个高频领域词,观察分词结果再迭代,否则容易引入错误切分。

3. 模型选型:从词典法到 BERT 的四级台阶

3.1 词典法:最快基线,也是最好的对照组

词典法的逻辑是:给每个情感词一个极性分,句子得分等于命中词得分之和,再处理否定和程度副词。金融领域有现成的情感词典可以借鉴,但通用词典对「回调」「盘整」这类词判不准,需要自己扩。

# 极简金融情感词典法 POS_WORDS = {"增长": 1.0, "超预期": 1.5, "中标": 1.2, "回购": 1.0} NEG_WORDS = {"下滑": -1.0, "亏损": -1.5, "减持": -1.2, "问询": -0.8} NEGATORS = {"不", "未", "没有", "难以"} DEGREE = {"大幅": 1.5, "略微": 0.6, "显著": 1.4} def dict_score(tokens): score, i = 0.0, 0 while i < len(tokens): w = tokens[i] val = POS_WORDS.get(w, NEG_WORDS.get(w, 0.0)) if val != 0.0: # 向前看两个词,处理否定和程度 window = tokens[max(0, i-2):i] for ctx in window: if ctx in NEGATORS: val *= -1 if ctx in DEGREE: val *= DEGREE[ctx] score += val i += 1 return score

逻辑说明:遍历 token,命中情感词后回看前两个词,遇到否定词翻转极性,遇到程度副词放大。参数上,POS_WORDSNEG_WORDS的权重需要根据语料统计调整,DEGREE的倍数建议控制在 0.5 到 1.5 之间,过大容易过拟合个别样本。词典法的价值不在精度,而在于它是所有复杂模型的对照组——如果 BERT 打不过词典法,说明数据或标签有问题。

3.2 机器学习:TF-IDF 加线性模型的性价比

当标注数据有几千条时,TF-IDF 加 Logistic Regression 或 SVM 是性价比最高的选择。它训练快、可解释、部署轻,很多生产环境的第一版就是它。

from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report texts = ["业绩超预期增长", "商誉减值风险暴露", "股价回调后企稳"] labels = [2, 0, 1] # 2正 1中 0负 X_train, X_test, y_train, y_test = train_test_split( texts, labels, test_size=0.2, random_state=42, stratify=labels ) pipe = Pipeline([ ("tfidf", TfidfVectorizer( tokenizer=lambda x: x.split(), # 假设已分词,用空格连接 ngram_range=(1, 2), # 一元+二元,捕捉"不+增长" max_features=20000, min_df=2, sublinear_tf=True # 抑制高频词权重 )), ("clf", LogisticRegression( C=1.0, # 正则强度,越小越强 class_weight="balanced", # 类别不平衡时必开 max_iter=1000 )) ]) pipe.fit(X_train, y_train) print(classification_report(y_test, pipe.predict(X_test)))

参数说明:ngram_range=(1,2)是关键,金融文本里「不增长」「未亏损」这种否定搭配必须靠二元组捕捉,只用一元会丢信息。min_df=2过滤只出现一次的词,降低噪声。sublinear_tf=True用对数缩放词频,避免「涨停」出现十次就主导权重。class_weight="balanced"在正负样本不均衡时必开,否则模型会偏向多数类。C是正则强度的倒数,数据少时调小到 0.5,数据多时可以到 2 到 5。

3.3 深度学习:BERT 微调的收益与代价

当数据上万条、且对精度要求高时,中文预训练模型微调是主流。金融领域有专门的预训练模型,通用模型也能用,但需要更多领域数据微调。

from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer model_name = "bert-base-chinese" # 换成金融领域预训练模型效果更好 tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=3) def encode(batch): return tokenizer( batch["text"], truncation=True, max_length=128, # 金融句子短,128足够,长文本再调大 padding="max_length" ) # Trainer 参数重点: # learning_rate=2e-5,太大灾难性遗忘,太小收敛慢 # num_train_epochs=3~5,金融数据少,多了过拟合 # warmup_ratio=0.1,前10%步数预热,稳定训练

逻辑说明:max_length=128对句子级金融文本够用,研报段落级要调到 256 或 512,但显存和速度会明显上升。学习率 2e-5 是微调的经验值,金融数据量小的时候可以降到 1e-5。epoch 数控制在 3 到 5,配合早停。BERT 的代价是推理慢、显存高,如果只是做舆情计数,TF-IDF 方案可能更划算。

3.4 选型决策表

数据量推荐方案预期 F1推理成本
无标注词典法0.55-0.65极低
1k-5kTF-IDF+LR0.75-0.85
5k-20kBERT 微调0.85-0.92中高
20k+领域预训练+微调0.90+

这张表是经验区间,具体数值取决于标签质量和领域匹配度。新手不要一上来就上 BERT,先用词典法和 TF-IDF 把数据问题暴露出来,再决定要不要上深度模型。

4. 工程落地:源码结构与训练脚本怎么组织

4.1 目录结构:让源码可读可复现

一套能交付的金融情感分析源码,目录应该清晰到别人 clone 下来就知道从哪跑。

finance_sentiment/ ├── data/ │ ├── raw/ # 原始语料 │ ├── processed/ # 清洗分词后 │ └── finance_dict.txt # 领域词典 ├── src/ │ ├── preprocess.py # 清洗、分词、标签处理 │ ├── dataset.py # Dataset 封装 │ ├── model_dict.py # 词典法基线 │ ├── model_ml.py # TF-IDF + 线性模型 │ ├── model_bert.py # BERT 微调 │ └── evaluate.py # 评估与混淆矩阵 ├── configs/ │ └── default.yaml # 超参数集中管理 ├── outputs/ │ ├── models/ │ └── logs/ ├── requirements.txt └── README.md

关键点是configs/default.yaml把超参数抽出来,训练脚本不写死参数。这样换数据集、调参不用改代码。outputs分 models 和 logs,方便回溯哪次实验对应哪个模型。

4.2 配置文件与训练入口

# configs/default.yaml data: train_path: data/processed/train.csv val_path: data/processed/val.csv max_length: 128 model: name: tfidf_lr # 可选 dict / tfidf_lr / bert tfidf: ngram_range: [1, 2] max_features: 20000 min_df: 2 lr: C: 1.0 class_weight: balanced train: seed: 42 batch_size: 32 epochs: 5 lr: 2.0e-5
import yaml from src import preprocess, model_ml, model_bert, evaluate def main(config_path="configs/default.yaml"): with open(config_path, encoding="utf-8") as f: cfg = yaml.safe_load(f) train_df = preprocess.load(cfg["data"]["train_path"]) val_df = preprocess.load(cfg["data"]["val_path"]) if cfg["model"]["name"] == "tfidf_lr": model = model_ml.build(cfg["model"]) model.fit(train_df["tokens"], train_df["label"]) preds = model.predict(val_df["tokens"]) elif cfg["model"]["name"] == "bert": model = model_bert.build(cfg) preds = model.train_and_predict(train_df, val_df) else: raise ValueError("未知模型类型") evaluate.report(val_df["label"], preds, cfg["outputs"]["log_dir"]) if __name__ == "__main__": main()

逻辑说明:入口只做三件事——读配置、按配置选模型、评估。preprocess.load负责读 CSV 并保证有tokenslabel两列。evaluate.report输出分类报告和混淆矩阵到日志目录。参数上,seed固定随机种子保证可复现,batch_size在 BERT 场景下受显存限制,32 是 8G 显存的保守值。

4.3 评估:别只看准确率

金融情感分析里类别不平衡很常见,中性样本往往最多。只看 accuracy 会被多数类骗。必须看每个类别的 precision、recall、F1,以及混淆矩阵。

from sklearn.metrics import confusion_matrix, classification_report import matplotlib.pyplot as plt import seaborn as sns def report(y_true, y_pred, log_dir): print(classification_report(y_true, y_pred, digits=4)) cm = confusion_matrix(y_true, y_pred) sns.heatmap(cm, annot=True, fmt="d", cmap="Blues") plt.xlabel("预测") plt.ylabel("真实") plt.savefig(f"{log_dir}/confusion_matrix.png", dpi=150) plt.close()

重点看负类召回。金融风控场景里,漏掉一条负面舆情比误报一条正面代价大得多。如果负类 recall 低于 0.7,要么补负样本,要么调class_weight,要么在阈值上做文章。

注意:混淆矩阵要保存下来,每次调参后对比,否则改了什么导致指标变化都说不清,这是血泪经验。

5. 避坑与排查:金融情感分析最常见的五个翻车点

5.1 现象:验证集 F1 很高,上线就崩

原因:训练集和验证集同分布,但线上文本来自不同来源,用词、长度、句式都变了。金融领域尤其明显,研报和股吧是两种语言。

解决:按来源划分验证集,而不是随机划分。留出一个完全没见过的来源做测试,比如训练用研报、测试用股吧。如果跨来源掉点超过 15 个点,说明模型没学到通用特征,需要做领域对抗或增加来源多样性。

5.2 现象:否定句全部判反

原因:TF-IDF 一元特征或 BERT 截断导致「不」和情感词被分开,或者分词把「不涨」切成「不」「涨」后,「不」被当停用词删了。

解决:停用词表里绝对不能删否定词。TF-IDF 必须开ngram_range=(1,2)。BERT 场景检查max_length是否截断了关键否定。另外,中文里「不」和「没」的否定范围不同,可以在预处理阶段把「不+动词」合并成一个 token。

5.3 现象:数值相关句子情感判错

原因:清洗时把数字删了,或者模型没见过「增长 300%」和「增长 3%」的强度差异。

解决:保留数值占位符,并在特征里加入数值分桶特征。比如把百分比分成「<0」「0-10%」「10-50%」「>50%」四档,作为额外特征拼进去。BERT 场景可以在输入里保留数字原文,让模型自己学。

5.4 现象:中性样本被大量误判为正或负

原因:中性定义模糊,标注时把「股价持平」标成中性,但模型从「持平」里学不到稳定特征。或者中性样本太少,被多数类淹没。

解决:重新审视中性定义,把「无明显倾向」和「正负抵消」分开。正负抵消的句子可以单独做规则处理,不交给模型。中性样本不足时,用数据增强或对中性类过采样。

5.5 现象:训练 loss 不降或震荡

原因:学习率太大、batch 太小、标签有错、或者数据没 shuffle。

解决:先降学习率到 1e-5 试。检查标签分布,随机抽 50 条人工核对,金融语料标注错误率经常比想象高。确认 DataLoader 开了 shuffle。如果用的是 BERT,检查 tokenizer 和模型是否匹配,中文模型用英文 tokenizer 会直接崩。

6. 进阶技巧:用置信度过滤和阈值调优把负类召回拉上来

模型输出概率后,直接取 argmax 是最粗暴的做法。金融场景里,负类漏报代价高,可以通过调整决策阈值来换召回。具体做法是:对负类概率设一个低于 0.5 的阈值,比如 0.35,只要负类概率超过 0.35 就判负。这会牺牲一些精确率,但能把召回拉上来。

import numpy as np def predict_with_threshold(probs, neg_threshold=0.35): # probs: shape [N, 3],列顺序 [负, 中, 正] preds = np.argmax(probs, axis=1) # 负类概率超过阈值且不是 argmax 的,强制判负 neg_override = (probs[:, 0] >= neg_threshold) & (preds != 0) preds[neg_override] = 0 return preds

逻辑说明:先取 argmax 作为基础预测,再把负类概率超过阈值的样本强制改判为负。neg_threshold是核心参数,从 0.5 往下调,每调 0.05 看一次验证集上负类 recall 和整体 F1 的变化,找到拐点。一般 0.3 到 0.4 之间能取得较好的平衡。这个技巧对词典法和 BERT 都适用,本质是用先验代价调整决策边界。

另一个进阶方向是置信度过滤:对最高概率低于某个值(比如 0.6)的样本,不输出硬标签,而是标记为「待人工复核」。在舆情监控系统里,这能显著降低误报带来的无效告警。实现上就是在预测后加一层判断,把低置信样本单独落库。

阈值策略负类召回整体 F1适用场景
argmax0.720.86均衡场景
neg=0.40.810.84风控偏召回
neg=0.30.880.79强风控
置信度过滤0.750.87人工复核

这张表是我在几个金融语料上跑出来的经验区间,具体数值会随数据变,但趋势稳定:阈值越低,负类召回越高,整体 F1 先稳后降。调参时建议画一条 recall-F1 曲线,选拐点而不是极值点。

最后说个习惯:每次改完预处理或阈值,我都会把当次的混淆矩阵和阈值曲线存进outputs/logs,文件名带上日期和参数。金融情感分析的坑大多不在模型本身,而在数据分布和标签一致性上,没有这些记录,两周后根本想不起来当时为什么这么调。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询