☰
基于文本分析的股票预测系统:numpy手写RNN与情绪特征实战
2026/10/22 19:41:50 网站建设 项目流程

简介:这份毕业设计资源围绕文本分析与股票预测的交叉场景展开,面向计算机、人工智能、通信工程、自动化等专业的学生与科研人员,可用于毕业设计、课程设计或项目初期演示。项目将爬虫采集、文本情感分析与RNN时间序列预测相结合,帮助读者理解从非结构化文本到股价走势建模的完整链路。压缩包共18个文件,约288KB,包含5个Python源码文件、6个XML配置、2个CSV数据集、2份Markdown说明文档,以及设计报告docx和缓存文件,覆盖数据采集、模型训练与结果输出等环节。已有43人学习关注。源码经过测试,功能完备且运行稳定,具备一定基础的用户可在其基础上修改扩展,实现更多预测功能;初学者也能借助配套报告与说明文档梳理项目结构、复现实验流程,并参考排错思路完成环境配置与调试。

1. 文本情绪真能预测股价吗:一份毕设级股票预测系统的拆包记录

先说结论:文本情绪和股价之间确实存在统计上的相关性,但把它做成一个能跑通、能写进毕业设计报告的系统,和把它做成一个能赚钱的策略,是两件完全不同的事。这份「基于文本分析的股票预测系统」资源包,核心价值在于前者——它提供了一个从新闻/公告文本抓取、情绪打分,到用 numpy 手写 RNN 做时间序列预测的完整闭环,附带一份设计报告文档。适合正在做毕业设计、课程设计,或者想找一个「文本特征 + 时序模型」组合练手项目的同学。它不依赖 PyTorch、TensorFlow 这类重型框架,RNN 部分是用 numpy 从零实现的,这对理解反向传播和梯度消失问题反而更直观。下面我按拆包顺序,把目录结构、数据流、模型实现和几个容易翻车的地方逐一讲清楚。

2. 拆开压缩包先看什么:目录结构与数据流梳理

拿到一个毕设资源包,我习惯先不跑代码,而是把目录树和文件职责理一遍。这个包的结构大致是:根目录下有CompanyScrapy(爬虫模块)、data(数据存放)、utils.py(工具函数)、main.py(主入口)、main_nobatch.py(无批次版本)、main_noactivation.py(无激活函数版本),以及一份设计报告-仅供参考学习.docx。三个 main 文件的存在说明作者在实验不同配置下的训练效果,这本身就是很好的对比学习素材。

2.1 各模块职责与依赖关系

CompanyScrapy负责从公开财经页面抓取公司相关的新闻或公告文本,输出到data目录。utils.py里通常放的是文本清洗、情绪词典加载、数据归一化、滑动窗口切分这些函数。main.py是标准训练流程:读数据、构造特征、搭 RNN、训练、预测、评估。main_nobatch.py把批次训练改成全量训练,适合小数据集调试;main_noactivation.py去掉了 RNN 单元里的激活函数,用来观察非线性缺失后模型退化成什么样。

依赖方面,从文件名和常见做法推断,核心依赖是numpy,爬虫部分可能用到requests和BeautifulSoup或scrapy。没有看到requirements.txt,所以需要自己确认环境。我一般会先建一个干净的虚拟环境,然后手动装这几个包,避免和系统里已有的版本打架。

python -m venv venv_stock source venv_stock/bin/activate # Windows 用 venv_stock\Scripts\activate pip install numpy requests beautifulsoup4 pandas

这里pandas不是必须的,但读 CSV 和做时间对齐会方便很多。如果你打算改爬虫,scrapy也可以装上,但注意CompanyScrapy这个目录名暗示它可能是一个 scrapy 项目,里面会有settings.py、items.py、pipelines.py这些标准文件。先确认它到底是独立脚本还是完整 scrapy 工程,再决定装不装 scrapy。

2.2 数据文件的格式与字段含义

data目录是整条流水线的起点。常见做法是放两个文件:一个是文本情绪得分的时间序列,一个是股票价格的时间序列。文本侧字段一般包括日期、公司标识、情绪分值(正向减负向)、新闻条数;价格侧字段包括日期、开盘、收盘、最高、最低、成交量。两个序列按日期对齐后,才能构造「用前 N 天情绪和价格预测后一天收盘价」这样的监督学习样本。

提示:如果data目录里只有原始文本没有打分结果,那说明情绪计算是在utils.py或main.py里实时做的,需要先确认情绪词典文件在不在。词典缺失是这类项目最常见的「跑不起来」原因。

我建议先写一个极短的检查脚本,把数据读进来打印形状和前后几行,确认日期范围、缺失值比例和字段类型。这一步花五分钟,能省掉后面半小时的报错排查。

import pandas as pd price = pd.read_csv('data/price.csv', parse_dates=['date']) sentiment = pd.read_csv('data/sentiment.csv', parse_dates=['date']) print(price.shape, price['date'].min(), price['date'].max()) print(sentiment.shape, sentiment['date'].min(), sentiment['date'].max()) print(price.isna().sum()) print(sentiment.isna().sum())

parse_dates把日期列转成 datetime 类型,后续合并和排序才不会出错。isna().sum()看每列缺失数量,如果情绪列缺失超过三成,那模型学到的多半是噪声,得先决定是插值还是丢弃。

3. 用 numpy 手写 RNN:前向传播、反向传播与三个 main 文件的差异

这个资源包最有学习价值的部分,就是它没有调库,而是用 numpy 把 RNN 的每个矩阵乘法都写出来了。很多人做毕设直接from tensorflow.keras.layers import SimpleRNN,跑通了但说不清梯度怎么传。这份代码逼着你面对Wx、Wh、Wy三个权重矩阵和它们对应的梯度。

3.1 RNN 单元的前向计算与参数初始化

一个最简 RNN 单元在每个时间步做两件事:把当前输入和上一时刻隐状态拼接后线性变换,再过激活函数得到新隐状态;然后用新隐状态线性变换得到输出。用 numpy 写出来大概是这样:

import numpy as np def rnn_forward(x, h_prev, Wx, Wh, Wy, bh, by): # x: (input_dim,), h_prev: (hidden_dim,) h = np.tanh(np.dot(Wx, x) + np.dot(Wh, h_prev) + bh) y = np.dot(Wy, h) + by return h, y, (x, h_prev, h)

Wx形状是(hidden_dim, input_dim),Wh是(hidden_dim, hidden_dim),Wy是(output_dim, hidden_dim)。np.tanh是默认激活,换成sigmoid或relu就是main_noactivation.py想对比的变量——它把激活去掉,等价于线性 RNN,理论上无法拟合非线性关系,股价预测效果会明显变差,这个对比实验放在报告里很有说服力。

初始化不能全零,否则所有隐单元对称,梯度更新后仍然对称,等于白学。常见做法是用np.random.randn乘一个小的缩放因子,比如0.01。

hidden_dim = 32 input_dim = 5 # 例如:情绪分、开盘、收盘、最高、最低 output_dim = 1 # 预测次日收盘 Wx = np.random.randn(hidden_dim, input_dim) * 0.01 Wh = np.random.randn(hidden_dim, hidden_dim) * 0.01 Wy = np.random.randn(output_dim, hidden_dim) * 0.01 bh = np.zeros((hidden_dim, 1)) by = np.zeros((output_dim, 1))

缩放因子取0.01是经验值,太大容易梯度爆炸,太小则学不动。如果你发现 loss 从头到尾几乎不变,先检查这个值。

3.2 时间步展开与损失计算

把单步计算沿时间轴展开,就得到完整的前向传播。假设一个样本是T个时间步,每个时间步输入x_t,隐状态h_t传递下去,最后拿最后一个时间步的输出y_T和真实值算损失。回归任务常用均方误差:

def forward_sequence(inputs, h0, Wx, Wh, Wy, bh, by): h = h0 cache = [] for x_t in inputs: h, y, c = rnn_forward(x_t, h, Wx, Wh, Wy, bh, by) cache.append(c) loss = 0.5 * (y - target) ** 2 return y, loss, cache

inputs是一个长度为T的列表,每个元素是(input_dim, 1)的列向量。cache存下每个时间步的输入、上一隐状态和当前隐状态,反向传播时要用。这里0.5是为了求导后系数为 1,纯粹是数学上的方便。

反向传播沿时间反向逐层求梯度,核心是链式法则。main_nobatch.py和main.py的差别就在这一步之后:前者用全部样本算一次梯度再更新,后者按小批次更新。全量训练在小数据上更稳,但慢;批次训练快,但批次大小选不好会震荡。

3.3 三个 main 文件该怎么对比着跑

我建议按这个顺序跑:先main_nobatch.py,因为全量梯度下降在几百条样本上通常几十轮就收敛,方便确认代码本身没 bug;再跑main.py,观察批次大小对收敛曲线的影响;最后跑main_noactivation.py,把它的 loss 曲线和前两个叠在一起画图,作为报告里「激活函数必要性」的论据。

python main_nobatch.py python main.py python main_noactivation.py

跑之前把每个文件里的epochs、learning_rate、hidden_dim打印出来,或者统一写到一个配置字典里。我见过太多毕设代码把超参数散落在各处,改一个忘一个,最后报告里的数字和代码对不上。统一配置是基本习惯。

注意:如果main_noactivation.py的 loss 反而更低,先别高兴,检查是不是数据泄漏——比如归一化时用了全量数据的均值和方差,或者预测目标里混入了未来信息。线性模型在噪声大的股价数据上偶尔「表现好」,多半是这类问题。

4. 文本情绪怎么变成特征:分词、词典打分与时间对齐

文本分析这部分,资源包里没有用 BERT 这类预训练模型,而是走词典打分路线。这对毕设来说其实是明智的:可解释、依赖少、跑得快,报告里也好写公式。核心流程是:抓取文本 → 分词 → 查情绪词典 → 累加得分 → 按日期聚合成日度情绪指标。

4.1 情绪词典打分的实现与局限

最简单的做法是准备两个词表,正向词和负向词,每个词给一个权重,文本得分等于正向词权重和减去负向词权重和,再除以文本长度做归一化。

pos_words = {'增长': 1.0, '盈利': 1.0, '突破': 0.8, '合作': 0.6} neg_words = {'亏损': 1.0, '下滑': 0.8, '违规': 1.0, '风险': 0.5} def score_text(text, pos, neg): score = 0.0 for w, v in pos.items(): score += text.count(w) * v for w, v in neg.items(): score -= text.count(w) * v return score / max(len(text), 1)

text.count(w)是朴素匹配,不做分词也能跑,但会漏掉「不增长」这种否定结构。改进方向是引入否定词窗口:如果「不」出现在正向词前三个字符内,就把该词权重取反。这个改动不大,但报告里可以单独写一小节,显得你对文本情感分析有思考。

词典的局限在于领域适配。通用情绪词典在财经文本上经常失灵,比如「空头」在别处是负面,在财经语境里是中性术语。所以如果时间允许,从data里的文本中统计高频词,人工标注几十个补充进词典,效果提升比换模型明显。

4.2 把日度情绪对齐到交易日

新闻不一定每个交易日都有,周末和节假日也会有公告。对齐策略常见两种:一是只保留有新闻的交易日,样本变少但干净;二是用前向填充,把最近一次情绪得分延续到下一个新闻日。我一般选后者,因为股价预测需要连续的时间步,断档会让 RNN 的隐状态传递失去意义。

sentiment = sentiment.set_index('date').sort_index() price = price.set_index('date').sort_index() merged = price.join(sentiment, how='left') merged['sentiment'] = merged['sentiment'].ffill().fillna(0) merged = merged.dropna()

ffill()是前向填充,fillna(0)处理最开始没有新闻的那几天。dropna()去掉价格本身缺失的行。合并后检查一下样本量,如果从几千条掉到几十条,说明日期格式没对齐,多半是时区或字符串格式问题。

4.3 滑动窗口构造监督样本

RNN 需要序列输入,所以要把合并后的数据切成一个个窗口。假设用前 10 天的情绪和价格特征预测第 11 天收盘价:

def make_windows(features, target, window=10): X, y = [], [] for i in range(len(features) - window): X.append(features[i:i+window]) y.append(target[i+window]) return np.array(X), np.array(y)

features是归一化后的二维数组,target是收盘价一维数组。window取 10 是常见起点,可以试 5、20 做对比。注意归一化要在切窗口之前做,且只能用训练集的均值和方差去变换测试集,否则就是数据泄漏。这个坑我在第 5 章会展开。

5. 避坑与排查:从环境到数据泄漏的五条血泪经验

这一章是我拆这类毕设包时踩过的坑里挑出来的,每条都按「现象 → 原因 → 解决」写,你对照着排查能省不少时间。

5.1 现象:ModuleNotFoundError 或 numpy 版本报错

原因通常是环境里装了多个 Python,或者 numpy 版本太新导致旧写法失效。比如np.float在 numpy 1.24 之后被移除,老代码里如果用了会直接报错。

解决:先python -c "import numpy; print(numpy.__version__)"确认版本,然后pip install "numpy<1.24"降级,或者把代码里的np.float改成float、np.int改成int。我一般倾向改代码而不是降级,因为降级可能影响其他包。

5.2 现象:爬虫跑完 data 目录是空的

原因可能是目标页面结构变了,或者请求被拒绝,或者CompanyScrapy的start_urls还是示例地址。

解决:先手动用requests.get访问一个 URL,打印状态码和前 500 字符,确认能拿到内容。如果返回 403,加User-Agent头;如果返回的 HTML 里找不到预期标签,说明选择器要更新。爬虫部分不要指望一次跑通,留出调试时间。

5.3 现象:loss 变成 nan 或无限大

原因多半是学习率太大,或者输入没有归一化,导致梯度爆炸。

解决:先把学习率降到1e-4甚至1e-5试一轮。然后检查输入特征的范围,如果收盘价是几十上百而情绪分是零点几,量纲差异会让梯度极不稳定。对每个特征做标准化:

mean = features_train.mean(axis=0) std = features_train.std(axis=0) + 1e-8 features_train = (features_train - mean) / std features_test = (features_test - mean) / std

1e-8防止除零。注意mean和std只能从训练集算,这是铁律。

5.4 现象:测试集准确率异常高,但实盘逻辑说不通

原因几乎可以肯定是数据泄漏。常见形式有三种:归一化用了全量数据、滑动窗口跨越了训练测试边界、情绪得分里混入了未来新闻。

解决:按时间顺序切分,前 80% 训练,后 20% 测试,不要随机打乱。归一化统计量只从训练段计算。切窗口时确保测试段的第一个窗口只包含测试段之前的数据。检查一遍main.py里train_test_split有没有设shuffle=False。

5.5 现象:设计报告里的图表和代码跑出来的对不上

原因通常是报告是早期版本,代码后来改过但报告没同步,或者报告里的数字是手动编的。

解决:以代码为准,重新跑一遍生成图表,替换报告里的旧图。如果报告里的结论和你的实验结果矛盾,在报告里如实写「本次复现结果与原始描述存在差异」,这比硬凑数字诚实,答辩时也经得起追问。

6. 进阶玩法:把情绪特征做消融实验,用对比表格说话

跑通基础版本之后,真正能让这份毕设出彩的是消融实验。所谓消融,就是逐个去掉或替换模型组件,看性能变化,从而证明每个部分都有用。对这个项目,我建议做三组对比:只用价格特征、只用情绪特征、价格加情绪;以及 RNN 用 tanh、用 relu、不用激活。把结果整理成表格,比任何文字描述都有力。

实验配置输入特征激活函数测试集 RMSE测试集 MAE
基线价格tanh待填待填
加情绪价格+情绪tanh待填待填
无激活价格+情绪无待填待填
换激活价格+情绪relu待填待填

填这张表时,每个配置至少跑三次取平均,因为 numpy 的随机初始化会让结果有波动。如果三次方差很大,说明模型不稳定,得先固定随机种子np.random.seed(42)再比。

def evaluate(y_true, y_pred): rmse = np.sqrt(np.mean((y_true - y_pred) ** 2)) mae = np.mean(np.abs(y_true - y_pred)) return rmse, mae

rmse对大误差更敏感,mae更稳健,两个都报。如果加情绪后 RMSE 反而上升,别急着删掉情绪特征,先检查情绪对齐是不是有问题——很多时候不是特征没用,是特征没对齐。

还有一个技巧:把预测值和真实值画在同一张时间轴上,观察模型是在哪些时间段误差大。如果误差集中在某几个剧烈波动的日子,说明模型对突发事件无能力,这是数据本身的限制,不是代码 bug。把这个观察写进报告,比假装模型完美要专业得多。

从那以后我每次拿到这类毕设资源包,都强制自己先跑通最小闭环,再做一组消融,最后才动报告。顺序反了,返工是必然的。希望帮到你。

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

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

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

立即咨询