资讯中心

金融文本情感分析 Python 实战:从词典到 FinBERT 微调

📅 2026/9/26 1:50:40
金融文本情感分析 Python 实战:从词典到 FinBERT 微调
简介面向金融文本情感分析任务的代码实现适合金融数据分析、自然语言处理与机器学习领域的初学者和相关从业者。资源围绕完整的项目流程展开依次覆盖文本清洗、分词、去停用词等预处理操作情感词典与特征构建以及朴素贝叶斯、逻辑回归等多种模型的训练与效果评估并附带调参思路与可视化脚本可帮助读者从零搭建一套可运行的情感分析模型。压缩包内共包含七十八个文件以脚本、文本、模型、配置和少量结果文件为主其中脚本文件负责数据处理、模型训练与结果展示文本文件提供原始语料与词典模型文件保存已训练参数压缩包总大小约一点一五兆字节结构清晰便于学习。目前已有三千三百五十人学习下载。通过该资料读者能获得完整的代码框架与实验数据既能按步骤复现项目也能在此基础上调整模型与特征应用到金融新闻、公告或社交评论等真实场景中提升对情感分析技术链路与工程实现的理解。1. 金融文本情感分析一份能直接跑的 Python 代码包做量化或者盯基本面的朋友应该都有过这种体验公告出了、新闻刷屏了、股吧里吵翻天了可你手里的行情软件只给你一根K线情绪这东西得自己从文字里扒。这份基于 Python 的金融文本情感分析模型代码实现就是把「消息面」翻译成「情绪分」的现成工具箱。它解决的不是「读不读得懂」的问题而是「怎么把非结构化的文本变成可回测、可入模的数字特征」覆盖从文本清洗、金融领域分词、情感词典打分、到 FinBERT 微调的全流程。适合两类人一类是刚入门的量化分析者想跑通一个完整的情感分析链路另一类是已经跑过通用情感分析但发现金融文本效果不佳的开发者可以直接拿代码改成自己的数据集。2. 金融文本情感分析与通用情感分析三个关键差异与选型思路2.1 情感词的强度阈值在金融场景里完全不同通用情感分析里「好」就是正向「坏」就是负向分词之后做词典匹配基本够用。但金融文本里最典型的反直觉场景是「增长」这个词出现在财报里它可能是利好也可能因为「低于预期」而变成利空。更麻烦的是「风险」这个词在风控公告里它是消极的但在期权定价模型里它是中性的专业术语。这份代码包第一个值得复现的点就是它对情感词典做了一层金融领域修正。我拆代码时看到它的核心做法是引入一个金融强度阈值表给每个情感词额外标注一个「领域强度系数」。比如「飙升」在通用词典里是 1.8 的正向分但在金融语境下只有当它出现在成交量或者价格变动的语境里才维持这个强度如果出现在基本面陈述里强度会被下调到 1.2。def adjust_finance_intensity(word, context): intensity general_lexicon.get(word, 0) if word in finance_stopwords: return 0 if context price_volume and word in strong_price_words: return min(intensity * 1.5, 3.0) if context fundamental and word in strong_fundamental_words: return intensity * 0.8 return intensity这段代码的逻辑关键是它对同一个词做了「上下文语境」的区分而不是一个词一个固定分数。参数上price_volume和fundamental这两个上下文类型由前一步的句子分类结果决定如果你改了文本来源这个分类规则要跟着改——这属于后面会专门讲的坑。2.2 否定词与转折词的句法作用被单独建模金融文本里最害人的句式是「业绩没有下滑」和「业绩下滑被控制住」前者是双重否定表肯定后者是转折后带出残余风险。通用情感分析遇到这种句子往往直接翻车因为词典匹配是词粒度的管不了句法关系。这份代码里我注意到它专门做了一个 negation_scope 模块不是简单地把句子里的否定词计数取反而是先识别否定词的作用范围。作用范围由标点符号和转折词限定比如「但是」「然而」「不过」后面的内容否定词只能影响它前面的那个分句。这是代码包里很实用的一个设计尤其当你拿到的新闻正文是长段落而不是一句话时直接全句取反必然出错。def split_clause_by_conjunction(sentence): clauses re.split(r(?[。])|(但是|然而|不过|虽然), sentence) return [c for c in clauses if c.strip()]这个函数看着简单但它决定了否定修饰的生效边界。参数上如果你处理的是研报段落建议把「然而」「尽管」也加进分隔符列表否则转折后的内容会被错误纳入否定范围。2.3 选词典法还是选微调模型先看你的数据规模代码包里同时包含了两条技术路线。词典法是 Loughran-McDonald 金融情感词典的中文适配版配合 jieba 分词做基础打底深度路线是 FinBERT 预训练模型在自有数据上的微调。两条路线共存并不是冗余——词典法在样本量少于 300 条时更稳定而微调模型在你有 3000 条以上标注数据时才有意义。选型判断我一般看三点数据量、标签质量、部署环境。数据量不足 300 条时微调 BERT 大概率过拟合你拿到手的验证集精度可能很高但换一批时间段的文本立刻拉胯。而数据量足够时词典法的上限很低它理解不了「暴雷」「关灯吃面」这类网络化金融黑话。这份代码包的好处是两条路都有完整实现你可以先用词典法跑通管线再逐步加入微调部分对比收益。3. 把情感打分跑通从清理文本到输出结构化分数3.1 代码包结构拆解先分清四个模块的职责拿到代码包第一件事别急着运行先看清目录。我拆的时候发现它的结构设计比较清楚data 目录放原始文本和标注样本lexicon 目录放金融情感词典词表service 目录是清洗和特征提取核心代码model 目录是训练好的 FinBERT 权重和推理脚本。你后面所有踩坑基本都集中在 service 和 lexicon 里model 部分直接用就行。这种分层的意义在于情感分析项目 80% 的时间花在数据清洗和特征工程上模型反而像「最后一道工序」。如果你的目标是复现建议先看 service 下的文本清洗脚本因为金融文本的噪声模式和通用文本有明显差异——比如股票代码、财报数字、百分比符号这些如果不清洗干净会直接干扰分词的输出。3.2 文本清洗与分词金融实体保护是第一优先级金融文本清洗有一条常见做法处理得不好就全盘皆输如果你直接把「贵州茅台600519今日涨5.2%」丢给 jieba 分词大概率切出「贵州」「茅台」「600519」「今日」「涨」「5.2%」其中「600519」会被当作普通数字词情感识别对它不会有任何区分。这个代码包里用了一个预设实体表加正则结合的方式先把股票代码、公司简称、百分比数字提取出来替换成占位符再进分词管线。import jieba import re def protect_finance_entities(text): code_pattern r[036]\d{5} pct_pattern r\d(?:\.\d)?% t re.sub(code_pattern, STOCKCODE , text) t re.sub(pct_pattern, PCTNUM , t) return t def segment_finance_text(text): text protect_finance_entities(text) jieba.load_userdict(lexicon/finance_words.txt) tokens jieba.lcut(text) return tokens注意jieba.load_userdict这一步很关键金融词典里需要加入「估值」「市盈率」「量化宽松」这类领域词否则 jieba 会把「市盈率」切成「市」「盈率」后面情感词典匹配时直接失配。如果你的语料是港股或美股公告建议把代码格式的正则也改成对应的五位或四位代码格式不然实体保护就失效了。3.3 情感打分词典匹配加否定翻转让分数更可信做完分词核心的打分逻辑大致是这样一个流程遍历每个 token先查金融情感词典得到基础分再判断该 token 是否落在否定作用范围内如果落在则对基础分取反。这个流程里最值得调参的是词典的分数归一化方式——代码包里默认用的是最大值归一化把所有情感分映射到 -1 到 1 之间避免长文本因为词多而天然得分高。def score_sentence(tokens, neg_scope): total 0 for i, tok in enumerate(tokens): base finance_lexicon.get(tok, 0) if base ! 0: polarity 1 if base 0 else -1 magnitude abs(base) if i in neg_scope: polarity * -1 total polarity * magnitude score max(min(total / max(len(tokens), 1), 1.0), -1.0) return score这段代码的参数含义要说明清楚neg_scope是一个位置索引集合表示句子中哪些词位落在否定词影响范围内finance_lexicon是词典 dictkey 是词value 是带符号的权重。如果你发现某个句子的得分总是异常极端优先检查finance_lexicon里是不是混入了通用词典的高权重词比如「牛」在通用词典里是中性偏正向但在金融语境里如果是「牛市」「牛回」才应该正向「牛股」则可能是黑话。3.4 参数怎么调自定义金融词典的扩展方式代码包提供的词典不一定覆盖你手里的特定语料比如你是做债券市场分析的那「违约」「展期」「评级下调」这些词的权重可能需要单独调整。代码里留了一个外部词表接口你不需要改主逻辑只要在 lexicon 目录下新增一个custom_lexicon.txt格式是「词 TAB 情感分数」加载时会自动覆盖默认词典的权重。暴雷 -2.5 关灯吃面 -2.0 减持 -1.5 增持 1.8这个机制很有用但有一个隐性要求词表里同一个词只允许出现一次否则后面的值会覆盖前面的。我第一次跑的时候没注意在自定义词表里同时放了「减持」和「大股东减持」结果系统自动匹配更长的词短期没问题但换一批文本后「减持」这个词的上下文变了分数就飘了。如果你也遇到类似情况记住一个原则自定义词表的粒度只到词不要做短语级匹配短语留给否定处理和转折处理去解决。4. 接上 FinBERT 微调从通用语义到金融语境的迁移4.1 为什么词典之后还要上预训练模型词典法能做到 70% 左右的准确率就是极限了因为金融文本里大量依赖语境才能判断情感比如「低于预期」这个词组「低」是负向「预期」是中性组合起来整体是负向但词典法会算成「低」的负向分「预期」不加分这种句子的情感强度明显被低估。FinBERT 这类预训练模型在理解句法关系和上下文语义上远比词典法强前提是你有足够的标注数据。4.2 用 Transformers 加载中文金融预训练模型做迁移学习代码包里用的是transformers库加载中文金融语料预训练的 BERT 权重。核心思路是不从头训练而是在预训练权重的基础上用你自己的标注文本做最后一层的 Fine-tune让模型适应你的具体任务。下面是拆出的一段脚本逻辑我在自己的数据上跑过结构可以直接复用。from transformers import BertTokenizer, BertForSequenceClassification from transformers import Trainer, TrainingArguments tokenizer BertTokenizer.from_pretrained(finbert_model/) model BertForSequenceClassification.from_pretrained( finbert_model/, num_labels3 ) def encode_batch(texts, labels): enc tokenizer( texts, truncationTrue, paddingTrue, max_length128, return_tensorspt ) return enc, labels training_args TrainingArguments( output_dir./results, num_train_epochs4, per_device_train_batch_size16, learning_rate2e-5, warmup_steps200, logging_dir./logs, )这里三个参数是复现效果的关键。max_length128是文本截断长度金融公告一篇动辄几百上千字直接截到 128 会把关键信息切在窗外。我一般对公告类文本开 256对股吧短评用 128 就够。batch_size16根据显存调整如果你用的是 6G 显存的卡建议降到 8否则容易 OOM。learning_rate2e-5是 BERT 微调的标准起点调大容易让预训练权重被覆盖调小则收敛太慢。4.3 你自己的数据集格式要求微调环节最容易出差错的是数据格式。代码包里的标注样本格式是 JSON 数组每一项包含text和label两个字段label 用 0/1/2 对应负向/中性/正向。如果你手里的数据是 CSV 或者 Excel需要先做一个格式转换这里没有捷径。import json import pandas as pd df pd.read_csv(my_annotated_data.csv) samples [ {text: row[content], label: int(row[sentiment])} for _, row in df.iterrows() ] with open(finetune_data.json, w, encodingutf-8) as f: json.dump(samples, f, ensure_asciiFalse, indent2)有一个容易被忽略的地方label 的映射关系必须从 0 开始且连续你不能用「-1、0、1」这种常见标注习惯否则模型输出的分类头和你的标签对不上。代码包里自带数据格式转换脚本但它是针对 CSV 两列的如果你的列名不是content和sentiment记得先改列名再跑。5. 金融文本情感分析的避坑清单四类高频翻车现场5.1 时间序列数据泄漏回测精度虚高的元凶现象微调模型做时间切分验证时验证集准确率 92%但放到真实交易场景里完全不灵。原因标注数据集来自同一时间段训练集和验证集里的文本在事件上是关联的——比如你按随机 8:2 切分同一家公司 3 月份的两条新闻一条在训练集一条在验证集模型等于见过答案。解决改成按时间先后切分训练集用前 80% 时间段的文本验证集用后 20%代码里加一行排序就好。df df.sort_values(publish_time) split_idx int(len(df) * 0.8) train_data df.iloc[:split_idx] valid_data df.iloc[split_idx:]这是金融文本情感分析翻车频率最高的坑而且它不会在验证集上暴露问题。你去看回测曲线都是好的一上线就亏钱大概率是数据泄漏。5.2 类别失衡让模型学会「躺平」现象训练完成后模型几乎把所有文本都判成中性。原因标注数据里中性样本占到 70% 以上损失函数被大头类别绑架了。解决要么对训练样本做欠采样要么在损失函数里给少数类加权。代码包里用的是后一种方式在训练集构造时给每个样本按类别乘一个权重系数让负向和正向样本的梯度贡献放大。class_weight { 0: 1.0, # 中性样本最多权重保持低 1: 2.5, # 负向样本较少提高权重 2: 2.5 # 正向样本较少提高权重 } sample_weights [class_weight[label] for label in train_labels]注意这个权重不是越高越好我试过把少数类权重拉到 5.0训练时损失振荡加剧最终精度反而下降。2.5 到 3.0 之间是比较平衡的区间。5.3 文本截断把关键信息切在窗外现象BERT 微调后长公告的情感得分明显偏离人工判断。原因前面提到的max_length128截断策略把公告末尾的「风险提示」截掉了。解决对长文本做分段推理再融合每 200 字切成一个 chunk分别打分最后按段落位置加权正文中段的权重比开头和结尾高。这比简单调大max_length更省显存也保留了更多语义信息。def score_long_text(text, model, tokenizer): chunks [text[i:i200] for i in range(0, len(text), 200)] scores [] for c in chunks: enc tokenizer(c, truncationTrue, paddingTrue, max_length128, return_tensorspt) logits model(**enc).logits probs logits.softmax(dim1) scores.append(probs.argmax().item()) weight [0.2, 0.6, 0.2][:len(scores)] return int(sum(p * w for p, w in zip(scores, weight)))这里的三段权重是经验值如果你处理的文本结构是「事件在前、影响在后」那你应该反过来加重结尾的权重。需要强调的是这个分段推理方案只适合推理阶段不适合训练阶段训练时的样本还是要统一截断长度。5.4 通用情感模型迁移到金融场景的标签错位现象你拿现成的通用情感分析库跑金融文本发现「降息」「宽松」这类词被判成负面。原因通用模型的情感基准是日常语境在普通语境里「降」通常不好但金融语境中「降息」是宽松政策信号本质是利好的。解决不要直接用通用模型的输出作为特征入模至少要在金融标注样本上做一层校准。代码包里提供了一个校准函数本质是学习一个从通用模型概率分布到金融标签的映射。如果你的数据量少于 500 条建议用简单线性映射而不是再训一个小模型否则过拟合风险大于收益。6. 把情感分数接到策略上多空情绪浓度的验证方法6.1 事件研究法验证情感分数的有效性模型输出的情感分不是最终目的你要验证它是否真的和市场表现相关。我习惯用事件研究法取情感分绝对值最高的前 10% 文本对应的事件日回看事件日后 1、3、5 日的收益差。这个逻辑很简单——如果你模型说「强烈正面」但事件日后三天平均收益是负的那你的情感分大概率是噪声不能直接作为策略信号。import statsmodels.api as sm returns df[future_3d_return] sentiment df[sentiment_score] X sm.add_constant(sentiment) model sm.OLS(returns, X).fit() print(model.summary())关注回归系数的 P 值大于 0.05 说明情感分对收益的解释力不显著。一个我反复踩过的教训是很多人直接全样本回归得到系数显著就兴冲冲上策略结果在牛市里所有文本都偏正情感分和上涨的相关性其实是市场整体的 beta不是文本的 alpha。做法上先把市场大涨大跌的日子过滤掉再跑回归还能显著才是真本事。6.2 情绪持续性用滑动窗口分数而不是单条消息单条文本的情感分会被偶发的极端消息带偏——一篇极具煽动性的帖子情感上是极强的负面但市场可能完全不理会。我一般会把情感分做三日均值平滑形成一个连续的情绪指标再做阈值突破判断。这样处理之后信号的稳定性明显提升单文本的噪声被摊薄。从那以后我每次拿到新的文本数据源都会强制走一遍「标注检查 → 时间切分 → 事件窗口回归」三道验证流程前两道能挡住数据泄漏和标签错位最后一道能告诉你这份情感分数到底值不值得进入你的策略。这套习惯救过我很多次也希望能帮到你。本文还有配套的精品资源点击获取

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案