简介:这份资源面向想用BP神经网络做股票价格预测的初学者与进阶开发者,提供Python与MATLAB双平台的完整实现思路。压缩包共2个文件,包含1个m脚本与1个csv数据文件,整体约23KB,其中m文件用于读取数据、构建并训练BP网络,csv文件则存放开盘价、最高价、最低价、收盘价等历史行情数据,便于直接运行与调试。资源围绕数据归一化、训练集与测试集划分、隐藏层节点调参、反向传播权重更新等关键环节展开,帮助读者理解如何用非线性模型拟合股票市场的复杂波动。目前已有5767人学习下载,适合作为课程设计、毕业项目或自学神经网络的练手案例,也可在此基础上替换数据、调整网络结构,形成自己的预测实验方案。
1. BP神经网络预测股票:从收盘价序列到可复现的预测管线
用 BP 神经网络预测股票价格,是很多人入门时序预测的第一站。它不需要复杂的门控结构,一个三层的反向传播网络就能把「过去 N 天收盘价」映射到「下一天收盘价」。但真正跑起来你会发现,同样的 python 代码,有人 RMSE 0.8,有人 RMSE 8,差别不在网络本身,而在数据怎么切、特征怎么造、归一化怎么做。这篇笔记面向想用 python 或 matlab 把这条链路跑通的从业者:先讲清 BP 为什么能拟合价格序列、它的能力边界在哪,再给出一份能直接抄的 python 代码,最后把归一化泄漏、滞后预测、过拟合这几个高频翻车点拆开讲。读完你应该能自己搭一条从数据到评估的完整管线,并知道哪些参数值得调、哪些纯属玄学。
2. 先想清楚:BP 网络到底在拟合什么
2.1 价格序列被当成一个滑动窗口回归问题
BP 神经网络本身只是一个多层感知机,它不理解时间。要让它在股票价格上工作,必须先把时间序列改造成监督学习样本:用前lookback天的收盘价作为一个输入向量,第lookback+1天的收盘价作为标签。这样一条长度为 T 的序列就能切出 T-lookback 个样本。
这个改造决定了模型的上限。它假设「未来价格只由过去固定窗口内的价格决定」,这在弱有效市场里几乎不成立,所以别指望它预测涨跌方向,它更擅长的是拟合价格的短期惯性。我一般把目标定位成「给出一个平滑的趋势参考」,而不是「预测明天涨还是跌」。
常见做法是只用一个特征(收盘价),但更稳的做法是加入成交量、最高最低价差、简单移动平均等,让输入维度从 1 变成 4~6。维度上去了,样本相对变少,过拟合风险也跟着上去,这是后面要反复权衡的点。
2.2 隐藏层、激活函数与学习率的选型理由
三层结构(输入-隐藏-输出)足够应付价格回归。隐藏层神经元数量没有理论最优解,经验区间是输入维度的 1~3 倍。输入 5 维、窗口 20 天时,隐藏层取 32 或 64 都合理,取 256 基本就是给过拟合开门。
激活函数上,隐藏层用tanh比relu更适合价格数据,因为归一化后的价格落在 [-1,1],tanh的输出区间天然匹配,梯度也不会像relu那样在负半轴直接死掉。输出层必须是线性的,回归任务不能加激活。
学习率是血泪经验最集中的地方。0.01起步,配合 Adam 优化器通常能收敛;用 SGD 的话0.001~0.005更稳。学习率太大,loss 会震荡甚至发散;太小,几百轮都不动。判断方法很直接:看训练 loss 曲线,如果前 50 轮几乎水平,就是太小;如果上下跳,就是太大。
2.3 为什么必须做归一化,以及用哪种
股票价格量纲差异大,一只票 5 块,另一只 500 块,直接喂进网络会让梯度被大数值主导。归一化到 [0,1] 或 [-1,1] 是标配。Min-Max 归一化最常用,公式是(x - min) / (max - min)。
关键坑在于:min和max必须只用训练集算,然后拿同一组参数去变换验证集和测试集。如果全量数据一起算 min/max,测试集的信息就泄漏进了训练,评估结果会虚高,实盘直接打脸。这是新手最容易翻车的地方,没有之一。
3. 用 python 跑通最小可复现管线
3.1 数据准备与滑动窗口切分
下面这段代码用 pandas 读入一份 CSV(列名假设为date, close),完成归一化和窗口切分。数据文件请自行准备,格式对齐即可。
import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler # 读取数据,按日期排序 df = pd.read_csv("stock.csv", parse_dates=["date"]).sort_values("date") close = df["close"].values.reshape(-1, 1) # 按 7:2:1 切分训练/验证/测试,先切再归一化,避免泄漏 n = len(close) train_end = int(n * 0.7) val_end = int(n * 0.9) scaler = MinMaxScaler(feature_range=(0, 1)) # 只用训练集拟合 scaler scaler.fit(close[:train_end]) scaled = scaler.transform(close) # 用同一参数变换全量 def make_windows(data, lookback): X, y = [], [] for i in range(len(data) - lookback): X.append(data[i:i + lookback, 0]) y.append(data[i + lookback, 0]) return np.array(X), np.array(y) lookback = 20 X, y = make_windows(scaled, lookback) # 按切分点对齐窗口样本 train_X, train_y = X[:train_end - lookback], y[:train_end - lookback] val_X, val_y = X[train_end - lookback:val_end - lookback], y[train_end - lookback:val_end - lookback] test_X, test_y = X[val_end - lookback:], y[val_end - lookback:] # 给 BP 网络用,展平成二维 train_X = train_X.reshape(len(train_X), -1) val_X = val_X.reshape(len(val_X), -1) test_X = test_X.reshape(len(test_X), -1)逻辑说明:scaler.fit只吃训练段,这是防泄漏的核心。make_windows把一维序列变成(样本数, lookback)的矩阵,再展平成(样本数, lookback)喂给全连接层。切分点用train_end - lookback对齐,是因为窗口样本比原始点少了lookback个,不对齐会导致训练集和验证集重叠。
参数说明:lookback=20对应约一个月交易日,适合日频;做周频可以调到 8~12。feature_range=(0,1)是默认,若隐藏层用tanh,改成(-1,1)收敛更顺。
3.2 搭建 BP 网络并训练
用 PyTorch 写一个三层网络,结构清晰、便于调参。
import torch import torch.nn as nn class BPNet(nn.Module): def __init__(self, input_dim, hidden=64): super().__init__() self.net = nn.Sequential( nn.Linear(input_dim, hidden), nn.Tanh(), nn.Linear(hidden, hidden // 2), nn.Tanh(), nn.Linear(hidden // 2, 1) # 输出层线性,回归任务 ) def forward(self, x): return self.net(x) device = "cuda" if torch.cuda.is_available() else "cpu" model = BPNet(input_dim=lookback, hidden=64).to(device) criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.01) def to_tensor(a): return torch.tensor(a, dtype=torch.float32).to(device) train_X_t, train_y_t = to_tensor(train_X), to_tensor(train_y).unsqueeze(1) val_X_t, val_y_t = to_tensor(val_X), to_tensor(val_y).unsqueeze(1) best_val = float("inf") patience, wait = 30, 0 for epoch in range(500): model.train() optimizer.zero_grad() loss = criterion(model(train_X_t), train_y_t) loss.backward() optimizer.step() model.eval() with torch.no_grad(): val_loss = criterion(model(val_X_t), val_y_t).item() if val_loss < best_val: best_val = val_loss wait = 0 torch.save(model.state_dict(), "best.pt") else: wait += 1 if wait >= patience: # 早停,防过拟合 print(f"early stop at epoch {epoch}") break逻辑说明:两层隐藏层比单层更能拟合非线性,但层数再多收益递减。MSELoss是回归标配。早停机制监控验证集 loss,连续 30 轮不下降就停,这是对抗过拟合最省事的手段。
参数说明:hidden=64是输入维度 20 的 3 倍左右,落在经验区间。lr=0.01配 Adam 是稳妥起点。patience=30可根据数据量调,数据少就设小一点。
3.3 反归一化与评估指标
预测出来的是 [0,1] 的值,必须反变换回价格量纲才能算真实误差。
model.load_state_dict(torch.load("best.pt")) model.eval() with torch.no_grad(): pred_scaled = model(to_tensor(test_X)).cpu().numpy() # 反归一化:scaler 是按单列拟合的,直接 inverse_transform pred = scaler.inverse_transform(pred_scaled) true = scaler.inverse_transform(test_y.reshape(-1, 1)) rmse = np.sqrt(np.mean((pred - true) ** 2)) mae = np.mean(np.abs(pred - true)) # 方向准确率:只看涨跌符号是否一致 dir_acc = np.mean(np.sign(np.diff(pred.flatten())) == np.sign(np.diff(true.flatten()))) print(f"RMSE={rmse:.3f}, MAE={mae:.3f}, 方向准确率={dir_acc:.3f}")逻辑说明:inverse_transform用的是训练集拟合的 scaler,保证量纲一致。RMSE 和 MAE 衡量数值拟合,方向准确率衡量涨跌判断,后者才是交易里真正关心的。注意np.diff会少一个点,两个序列要同步处理。
参数说明:RMSE 没有绝对好坏,要和「直接用昨天价格预测今天」的朴素基线比。如果模型 RMSE 还不如朴素基线,说明网络没学到东西,回去查归一化和窗口切分。
4. matlab 版本:同一套逻辑的另一种写法
4.1 用 timetables 组织数据与窗口
matlab 里用timetable管理时间序列更自然,配合lagmatrix造滞后特征。
% 假设 T 是含 Date 和 Close 两列的 timetable data = T.Close; n = length(data); trainEnd = floor(n * 0.7); valEnd = floor(n * 0.9); % 归一化,只用训练段拟合 trainMin = min(data(1:trainEnd)); trainMax = max(data(1:trainEnd)); scaled = (data - trainMin) / (trainMax - trainMin); lookback = 20; X = lagmatrix(scaled, 1:lookback); % 每列是一个滞后 X = X(lookback+1:end, :); y = scaled(lookback+1:end); trainX = X(1:trainEnd-lookback, :)'; trainY = y(1:trainEnd-lookback)'; valX = X(trainEnd-lookback+1:valEnd-lookback, :)'; valY = y(trainEnd-lookback+1:valEnd-lookback)'; testX = X(valEnd-lookback+1:end, :)'; testY = y(valEnd-lookback+1:end)';逻辑说明:lagmatrix一次性生成 1 到lookback阶滞后,比手写循环干净。归一化同样只用训练段算 min/max。matlab 的神经网络工具箱要求样本按列排列,所以做了转置。
参数说明:lookback=20与 python 版保持一致,方便对比。lagmatrix会产生前lookback行 NaN,用lookback+1:end切掉。
4.2 用 fitnet 训练并设置早停
net = fitnet([64 32], 'trainlm'); % 两层隐藏层,Levenberg-Marquardt net.trainParam.epochs = 500; net.trainParam.lr = 0.01; net.trainParam.max_fail = 30; % 验证失败 30 次即早停 net.divideFcn = 'dividetrain'; % 我们自己切好了,不让工具箱再分 [net, tr] = train(net, trainX, trainY, valX, valY); predScaled = net(testX); pred = predScaled * (trainMax - trainMin) + trainMin; true = testY * (trainMax - trainMin) + trainMin; rmse = sqrt(mean((pred - true).^2)); fprintf('RMSE=%.3f\n', rmse);逻辑说明:fitnet是 matlab 的前馈网络封装,[64 32]表示两层隐藏层。trainlm在小数据集上收敛快,但内存占用高,数据量大时换trainscg。divideFcn='dividetrain'很关键,否则工具箱会自己再切一份验证集,和我们手动的切分冲突。
参数说明:max_fail=30对应早停耐心值。lr=0.01与 python 对齐。反归一化用训练段的 min/max,不能用全量。
5. 避坑与排查:五个真实翻车点
5.1 归一化泄漏导致评估虚高
现象:测试集 RMSE 低到 0.5,实盘一跑误差翻几倍。原因:用全量数据算了 min/max,测试集的极值信息提前进了训练。解决:scaler.fit只吃训练段,验证和测试一律transform,反归一化也用训练段的参数。这条我踩过不止一次,务必在代码里写死。
5.2 滞后预测:模型只是复制了昨天
现象:预测曲线和真实曲线几乎重合,但整体右移一天。原因:价格自相关极强,网络学会了「输出约等于输入最后一个值」。解决:看方向准确率而不是 RMSE,如果方向准确率接近 0.5,说明模型没学到增量信息。可以改成预测「收益率」而非「价格」,或者加入外部特征打破这种平凡解。
5.3 过拟合:训练 loss 一路降,验证 loss 反弹
现象:训练 200 轮后训练 loss 接近 0,验证 loss 从第 50 轮开始上升。原因:隐藏层太大或训练太久,网络把训练集噪声也记住了。解决:加早停、减小隐藏层、加 dropout(nn.Dropout(0.2))。判断标准是验证 loss 曲线,不是训练 loss。
5.4 数据频率与 lookback 不匹配
现象:日频数据用lookback=5,模型几乎不收敛。原因:窗口太短,输入信息不足以支撑预测。解决:日频一般 10~30,周频 4~12,月频 3~6。经验是窗口覆盖一个完整的短期周期,比如一个月。
5.5 随机种子没固定,结果不可复现
现象:同一份代码跑两次,RMSE 差 0.3。原因:权重初始化、数据打乱都带随机性。解决:python 里设torch.manual_seed(42)和np.random.seed(42),matlab 里设rng(42)。做对比实验前先固定种子,否则调参就是碰运气。
6. 进阶技巧:用滚动预测和基线对比验证真实价值
单次切分的评估说服力有限,因为测试集只有一段。更靠谱的做法是滚动预测(walk-forward):每次用前一段训练,预测下一天,然后窗口前移一天,重复几十次,统计误差分布。这样能看出模型在不同市场阶段的稳定性。
def walk_forward(scaled, lookback, train_size, step=1): errors = [] for start in range(train_size, len(scaled) - lookback, step): train_seg = scaled[start - train_size:start] # 每次重新拟合 scaler,严格只用当前训练段 s = MinMaxScaler().fit(train_seg.reshape(-1, 1)) seg = s.transform(scaled[start - train_size:start + lookback + 1].reshape(-1, 1)) Xw, yw = make_windows(seg, lookback) # 这里复用前面的 BPNet 训练流程,略去重复代码 # pred 反归一化后与真实值比较,记录误差 errors.append(abs(pred_val - true_val)) return np.array(errors)逻辑说明:每次滚动都重新拟合 scaler,杜绝跨窗口泄漏。train_size决定训练窗口长度,一般取 250~500 个交易日。误差序列可以画箱线图,看中位数和尾部风险。
参数说明:step=1是逐日滚动,计算量大但最真实;step=5是每周滚动,速度快,适合快速验证。train_size太小模型不稳,太大则对市场变化反应迟钝。
另一个必做动作是基线对比。最朴素的基线是「用昨天价格预测今天」,如果 BP 网络的 RMSE 打不过它,说明这套管线没有实际价值。我一般会同时跑三条线:朴素基线、单层 BP、双层 BP,用同一份测试集对比。只有稳定超过基线,才值得继续投入调参。
最后说个习惯:每次实验都把 lookback、隐藏层、学习率、种子、RMSE、方向准确率记进一张表,跑够 20 组再下结论。凭单次结果调参,十有八九是在拟合噪声。希望帮到你。
本文还有配套的精品资源,点击获取