ARTICLE DETAIL

资讯详情

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

Python机器学习股市情感分析全流程:从词向量到量化因子

Python机器学习股市情感分析全流程:从词向量到量化因子 简介面向金融数据分析与机器学习学习者该资源提供了一套完整的股市情感分析实践方案。项目以股评文本和上证指数为数据源通过Python完成情感分析、情绪指标构建并借助机器学习模型揭示看涨情绪与股市走势之间的关系演示从互联网提取投资者情绪的完整流程。资源包共16个文件压缩后约654.97MB包含6个CSV数据文件原始/分词股评、指数行情、情绪指标及分析结果、4个Python脚本模型训练、情感指数计算与可视化、正负面词表文本、说明文档及预训练词向量数据下载即可运行。目前已有800余人学习适合希望将自然语言处理应用于金融场景的中高级Python学习者参考。通过该项目读者可获得全套可直接运行的代码与数据以及词向量资源便于快速复现从文本清洗、情感判断到指标构建、模型对比的完整链路理解量化投资中的情绪因子构建方法。1. 股市情感分析不是算个正负分就完事把“Python 机器学习 股市情感分析”当成一条 pipeline 来理解才算真正入了门先拿股吧评论、财经新闻、雪球帖子这类文本通过词向量把句子转成模型能吃的样子再用机器学习或深度学习模型把情感极性分出来最后把逐条情感得分聚合到“天”这个粒度上构造成可供量化策略使用的因子。这里最容易踩的坑是单纯算一条文本是正面还是负面对交易毫无意义——市场要的是可回溯、可验证的指标序列情感数据只有被构造成收益率预测里的一个特征才能叫“指标构建”。这篇就按“语料获取 → 词向量训练 → 分类模型 → 指标构造 → 回测验证”这条路用可复现的代码把每个环节讲透。2. 语料获取与预处理情感分析模型的原料质量决定一切2.1 数据源选型哪类文本更适合做股市情感分析做股市情感分析常见的文本源有股吧评论、雪球帖子、财经新闻标题与正文、上市公司公告。不同源的噪声水平和信息密度差异很大。我的经验是短线交易情绪看股吧和雪球评论因为散户主导的文本更能反映恐慌与贪婪而财经新闻正文适合捕捉基本面预期变化但新闻语言相对中性情感极性不如评论鲜明。数据获取的常见做法是用爬虫抓东方财富股吧、新浪财经的公开评论接口抓取字段至少包含评论内容、发布时间、标的代码。这里有一个关键点发布时间的粒度必须精确到分钟不然做日内情感指标聚合时会丢失高频信息。import requests import pandas as pd def fetch_gubar_comments(stock_code, page_start, page_end): rows [] for page in range(page_start, page_end 1): url ( https://gbapi.eastmoney.com/guba/list?code stock_code page str(page) ) resp requests.get(url, headers{User-Agent: Mozilla/5.0}) # 实际返回字段有 post_id, post_title, post_content, post_publish_time for item in resp.json().get(result, []): rows.append({ content: item.get(post_title, ) item.get(post_content, ), time: pd.to_datetime(item.get(post_publish_time), units), code: stock_code }) time.sleep(0.5) # 控制请求频率 return pd.DataFrame(rows) df fetch_gubar_comments(600519, 1, 5) print(df.shape)注意pandas 的 to_datetime 处理 Unix 时间戳时需要units参数不写默认是纳秒解析结果会直接漂到 1970 年。2.2 清洗规则财报数字、停牌通知这类噪声的处理拿到原始文本后第一步清洗至关重要。金融文本里有几类明显的干扰包含涨停、跌停、配合、主力等词的口水帖从别处复制的新闻通稿纯数字组成的无意义串以及带有大量 提及和表情符号的短评论。处理策略是过滤长度小于 8 个字的评论去掉 HTML 标签把全角字符转半角最后按标的代码分组。import re def clean_text(series): series series.str.replace(r[^], , regexTrue) series series.str.replace(rnbsp;, , regexTrue) # 全角转半角 series series.map(lambda x: x.translate(str.maketrans(。, ,.!?()))) # 去除非中英文和数字字符 series series.str.replace(r[^\u4e00-\u9fa5a-zA-Z0-9,.!?() ], , regexTrue) return series df[clean] clean_text(df[content]) df df[df[clean].str.len() 8]清洗逻辑里要注意一个坑正则[^\u4e00-\u9fa5a-zA-Z0-9]会把标点全删掉但句号、问号对后续句子切分有用所以要在字符集里显式保留标点。如果一开始就全删后面 Tokenizer 切分句子时会丢失边界信息。2.3 分词与停用词金融词表为什么不能直接用通用停用词表分词是词向量训练的前置步骤。中文分词我常用 jieba但金融领域需要加载自定义词表否则“贵州茅台”“新能源汽车”会被切成碎片导致后面词向量学不到完整语义。import jieba # 加载金融词典 jieba.load_userdict(finance_dict.txt) # 每行一个词如宁德时代 10 nr fin_stopwords set(line.strip() for line in open(fin_stopwords.txt, encodingutf-8)) def tokenize(text): words jieba.cut(text, cut_allFalse) return [w for w in words if w not in fin_stopwords and len(w.strip()) 1] df[tokens] df[clean].map(tokenize)这里的关键参数是cut_allFalse用精确模式。通用停用词表里有“我们”“你们”“这个”等词但在金融语境下“预期”“下调”“超预期”这些词恰恰是情感判别核心绝不能进停用词表。建议自建停用词表只删语气词和无实义动词。3. 词向量训练与选择从 Word2Vec 到预训练模型3.1 自训 Word2Vec vs 使用预训练词向量怎么选词向量的选择直接决定机器学习模型的天花板。常见的三条路用 gensim 在自己语料上训练 Word2Vec下载中文预训练词向量比如这里推荐一个非官方渠道搜狗新闻词向量网上有公开版本用 BERT 这类动态词向量。我的建议如果语料规模在 50 万条评论以上优先自训 Word2Vec金融文本的专有名词和表达习惯能够被充分捕捉语料少就用预训练词向量但要注意领域适配问题。自训 Word2Vec 用 gensim 的 Word2Vec核心参数就三个vector_size决定向量维度window决定上下文窗口min_count决定低频词的过滤力度。金融语料里很多专业术语出现频率低min_count 设置太高会把它们过滤掉一般设为 3 以下。from gensim.models import Word2Vec sentences df[tokens].tolist() model Word2Vec( sentences, vector_size128, window5, min_count3, sg1, epochs10, workers8 ) model.save(stock_w2v.model)sg1 是 Skip-gram 模型在中小规模语料上比 CBOW 更稳定因为它对低频词更友好。训练完成后需要检查词的相似度是否符合预期“利好”和“上涨”应该距离较近。3.2 词向量可视化验证训练质量要用降维训练完词向量后第一件事不是直接进模型而是拿一批熟悉的词看它的最近邻。这能快速发现训练是否失败如果“利好”的最近邻是一堆数字说明语料里有大量噪声词没过滤干净。from sklearn.decomposition import PCA import matplotlib.pyplot as plt words [利好, 利空, 上涨, 下跌, 买入, 卖出, 超预期, 暴雷] vecs [model.wv[w] for w in words] pca PCA(n_components2) result pca.fit_transform(vecs) plt.scatter(result[:, 0], result[:, 1]) for i, w in enumerate(words): plt.annotate(w, xy(result[i, 0], result[i, 1]), fontsize12) plt.title(Word2Vec PCA Visualization) plt.savefig(w2v_pca.png, dpi150)PCA 降到二维必然有信息损失但只要相近词仍聚在一起说明向量空间结构基本合理。如果发现“利好”和“利空”几乎重叠排查方向有两个分词是否把“利好”和“利空”切碎了或者语料里两个词出现语境高度雷同比如都在“该消息是利好还是利空”这种句子里这种情况需要加大 window 或者改用注意力机制。3.3 全数据验证TensorFlow Embedding 层的写法自训 Word2Vec 只是第一步后续要把它接入机器学习模型。TensorFlow 的 Embedding 层有两种常见写法直接用tf.keras.layers.Embedding从头训练或加载预训练权重再trainableFalse冻结。我的经验是在数据量不大时冻结预训练向量用trainableFalse防止微调破坏原有语义数据量充分时全量微调效果更好。import tensorflow as tf # 构建词索引 word_index {w: i 2 for i, w in enumerate(model.wv.index_to_key)} word_index[PAD] 0 word_index[UNK] 1 embedding_matrix np.zeros((len(word_index), 128)) for w, i in word_index.items(): if w in model.wv: embedding_matrix[i] model.wv[w] embedding_layer tf.keras.layers.Embedding( input_dimlen(word_index), output_dim128, embeddings_initializertf.keras.initializers.Constant(embedding_matrix), trainableFalse, mask_zeroTrue )注意mask_zeroTrue只有在trainableFalse时才有意义冻结后 PAD 位不会参与梯度更新。4. 机器学习情感分类模型从基线到深度学习4.1 多模型基线对比为什么先跑传统机器学习情感分析不是一上来就堆深度网络。先跑几个强基线模型能快速判断数据规模和质量的边界。用 TF-IDF 特征 逻辑回归作为基线在金融文本上往往能得到 80% 以上的准确率足以超过很多人的直觉预期。这一步的意义在于给后续深度模型设一个“及格线”如果 TextCNN 连逻辑回归都打不过问题一定在词向量或数据处理上。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 # X 是清洗后的文本y 是标签0负面1中性2正面 X_train, X_test, y_train, y_test train_test_split( df[clean], df[label], test_size0.2, random_state42, stratifydf[label] ) tfidf TfidfVectorizer(max_features5000, ngram_range(1, 2)) X_train_tf tfidf.fit_transform(X_train) X_test_tf tfidf.transform(X_test) lr LogisticRegression(max_iter1000, C1.0) lr.fit(X_train_tf, y_train) pred lr.predict(X_test_tf) print(classification_report(y_test, pred))ngram_range(1, 2)这个参数很关键“超预期”是 3 个字词级别的 bigram 会把“超预”和“预期”拼一起无法表达完整语义所以金融文本中建议把 ngram_range 调到 (1, 3)。标签体系建议用 3 分类而不是 2 分类中性评论文本占相当比例强行二分会导致模型在中性文本上输出随机方向。4.2 TextCNN 与 BiLSTM 的实现与调参要点TextCNN 是文本分类的经典架构它对关键词的位置不敏感但对 n-gram 特征很有效适合短文本评论BiLSTM 能捕捉长距离依赖但对短文本的优势不明显且训练更慢。我的实践结论5000 字以内的短评TextCNN 更快更稳长文本新闻正文BiLSTM 准确率会有明显提升。TextCNN 的 PyTorch 实现如下import torch import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim, num_filters128, filter_sizes[3, 4, 5]): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.convs nn.ModuleList([ nn.Conv1d(embed_dim, num_filters, kernel_sizefs) for fs in filter_sizes ]) self.fc nn.Linear(num_filters * len(filter_sizes), 3) self.dropout nn.Dropout(0.5) def forward(self, x): x self.embedding(x) # (batch, seq_len, embed_dim) x x.transpose(1, 2) # (batch, embed_dim, seq_len) conv_outs [] for conv in self.convs: c torch.relu(conv(x)) # (batch, num_filters, seq_len - fs 1) p torch.max_pool1d(c, c.size(2)) # 全局最大池化 conv_outs.append(p.squeeze(2)) x torch.cat(conv_outs, dim1) return self.fc(self.dropout(x))卷积核大小选 3、4、5覆盖的 n-gram 范围从 3 元到 5 元适合中文词粒度。训练时学习率设置 1e-3batch size 32epoch 10 就够TextCNN 收敛极快超过 15 个 epoch 就开始过拟合验证集 loss 会掉头向上。4.3 训练细节早停、类别不平衡与冻结词向量类别不平衡在股市情感数据里极其常见上涨日评论大量偏正面下跌日评论偏负面中性文本占比反而不高。处理方式用torch.nn.CrossEntropyLoss(weightclass_weight)权重按类别的逆频率计算。另外早停机制必须做监控验证集 F1 而不是 loss。class_weights torch.tensor([1.2, 0.8, 1.4]) # 负面、中性、正面权重 loss_fn nn.CrossEntropyLoss(weightclass_weights) best_f1 0 patience 3 wait 0 for epoch in range(20): train_one_epoch() val_f1 evaluate() if val_f1 best_f1: best_f1 val_f1 torch.save(model.state_dict(), best_model.pt) wait 0 else: wait 1 if wait patience: print(fEarly stop at epoch {epoch}) break类别权重的设置要注意中性文本权重不能过高否则模型会把模糊评论全判成中性导致最终情感指标整体向零收敛指标区分度就没了。5. 情感指标构建从逐条预测到可入模的日度因子5.1 聚合公式情感分数、情感强度与分歧度逐条评论的情感预测结果出来后需要聚合到日度频率才能与股价数据对齐。聚合维度常用三个指标。指标名称计算公式含义情感分数(N_pos - N_neg) / (N_pos N_neg N_neu)标准化到 [-1, 1]衡量市场总体情绪情感强度abs(情感分数)大于 0.6 时说明市场对未来方向存在一致性预期情感分歧度std(单条评论原始得分)数值越大说明市场对未来走势越纠结情感分数的分母一定别忘了中性评论数量这个细节决定指标能否准确反映市场整体情绪如果分母只算正负评那么无论中性评论有多少情感分数的绝对值都会被夸大——实际市场根本没那么极端。先看代码怎么构造。daily df.groupby([code, date]).apply( lambda x: pd.Series({ sentiment_score: (x[pred].value_counts().get(2, 0) - x[pred].value_counts().get(0, 0)) / len(x), sentiment_intensity: abs((x[pred].value_counts().get(2, 0) - x[pred].value_counts().get(0, 0)) / len(x)), sentiment_disagreement: x[pred_prob].std() }) ).reset_index()注意pred_prob是模型输出的概率列不是 0/1 标签。用概率计算分歧度比用标签更平滑因为softmax输出本身携带了模型的置信度信息。5.2 与行情数据对齐t 日与 t1 日的错位问题情感指标与行情数据对齐时最大的坑是时间错位t 日晚间发布的评论其情感影响的是 t1 日的开盘情绪而非 t 日。如果直接用 t 日情感指标预测 t 日收益率等于在盘中就使用了收盘后才产生的数据这是典型的前视偏差。price pd.read_csv(price_daily.csv, parse_dates[date]) daily daily.sort_values(date) daily[next_ret] price[close].pct_change().shift(-1) aligned pd.merge(daily, price, on[code, date], howinner).dropna()对齐之后再按情感指标分组回测每日按情感分数从高到低分成 5 组计算每组第二天的平均收益率检验分组收益是否呈现单调性。如果第 5 组情感最高组的次日平均收益显著高于第 1 组说明情感指标具有预测能力如果分组收益完全无序说明情感因子无效问题可能出在模型预测、文本质量、或者本身市场对公开情绪不敏感。5.3 行业轮动中的指标再加工差分与标准化日频情感指标带有很强的自相关性直接用原始值做线性回归多重共线性问题不严重但会造成因子拥挤。常见的处理方式取 5 日均线daily[sentiment_score].rolling(5).mean()平滑噪声再用 z-score 标准化(x - x.mean()) / x.std()消除不同股票之间评论数量的量纲差异。个股间的评论数量差异太大——茅台一天的评论数可能是小市值股的几十倍直接比对情感分数没有意义。按股票分组做 z-score 可以消除这个偏差。6. 用新数据做情感验证与多模态扩展作为模型上线前的最后一步情感指标构建完成后需要做样本外验证而不是只用回测结果说服自己。具体做法是用模型对最近 30 天的新评论做预测并构建指标与真实行情做滚动相关性检验。import numpy as np from scipy.stats import spearmanr # daily_aligned 中已有 sentiment_score 和 next_ret rolling_corr daily_aligned[sentiment_score].rolling(20).corr(daily_aligned[next_ret]) print(f平均滚动IC: {rolling_corr.mean():.4f}, IC标准差: {rolling_corr.std():.4f}, IR: {rolling_corr.mean() / rolling_corr.std():.4f})滚动 IC 均值大于 0.02 且 IR 大于 0.3因子在统计上具备使用价值IR 为负说明模型预测方向反了这时不用急着改模型架构先检查标签方向是否定义反了。一个值得尝试的进阶方向把评论长度、发布时段、用户历史发帖频率作为元特征拼到情感分数里构造一个多模态情感因子。例如将日内评论数量的变化率与情感分数相乘得到“舆论关注度加权情感指标”能有效滤掉无人问津的冷门股票在低评论量下产生的高情感假信号。最后的收尾技巧针对你的股票池用最近 250 个交易日的数据重训词向量和分类模型保证模型始终在当前市场语境下工作。词向量中的“利好”在牛熊市里语义指向差异巨大老模型跑新行情预测的置信度会逐渐失真。本文还有配套的精品资源点击获取
返回列表