简介:这份资源是面向计算机、人工智能、自动化等专业学生与从业者的金融文本情感分析项目源码包,源自个人毕业设计,答辩评审分达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_WORDS和NEG_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-5k | TF-IDF+LR | 0.75-0.85 | 低 |
| 5k-20k | BERT 微调 | 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-5import 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 并保证有tokens和label两列。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 | 适用场景 |
|---|---|---|---|
| argmax | 0.72 | 0.86 | 均衡场景 |
| neg=0.4 | 0.81 | 0.84 | 风控偏召回 |
| neg=0.3 | 0.88 | 0.79 | 强风控 |
| 置信度过滤 | 0.75 | 0.87 | 人工复核 |
这张表是我在几个金融语料上跑出来的经验区间,具体数值会随数据变,但趋势稳定:阈值越低,负类召回越高,整体 F1 先稳后降。调参时建议画一条 recall-F1 曲线,选拐点而不是极值点。
最后说个习惯:每次改完预处理或阈值,我都会把当次的混淆矩阵和阈值曲线存进outputs/logs,文件名带上日期和参数。金融情感分析的坑大多不在模型本身,而在数据分布和标签一致性上,没有这些记录,两周后根本想不起来当时为什么这么调。希望帮到你。
本文还有配套的精品资源,点击获取