简介这是一份基于粒子群优化算法与神经网络结合的股票价格预测项目资源旨在解决股票预测中神经网络参数难以人工确定的问题面向量化研究人员、金融人工智能学习者及需要提升预测精度的投资者。资源共包含十二个文件压缩包仅1.9MB内含Python脚本、三个CSV股票历史数据、四份Word文档、两篇相关PDF论文及一份项目说明Word文档覆盖训练过程记录与数据问题分析PDF为岩爆预测、证券投资组合等扩展文献脚本与数据可直接配合运行。目前已有130人学习下载通过项目中的Python源码、CSV历史数据、训练过程Word文档以及相关PDF论文读者可完整复现PSO优化神经网络的实验流程掌握粒子群算法自动调整网络权重、结构、学习率等关键参数的方法理解均方误差、均方根误差等评价指标在股票预测模型优化中的实际作用并在此基础上尝试改进模型提升预测准确性。1. 粒子群优化算法加神经网络做股价预测这份资源包的实战底子股票预测这个方向一提到神经网络大多数人的第一反应是拿着历史K线跑一个BP网络但跑出来的结果每次都不一样——同一个数据集、同一个网络结构换个随机种子预测曲线就变得千奇百怪。这份资源包的核心理念不复杂与其靠随机初始化碰运气不如让粒子群优化算法PSO去搜一组更靠谱的神经网络初始权重和阈值把“靠天吃饭”的随机初始化变成有方向性的搜索。包里既有股票数据、Python实现脚本也有训练过程的记录文档和两篇论文参考适合想搞清楚“PSO到底怎么嵌进神经网络训练流程”的从业者和研究生。接下来我会把数据处理、PSO实现细节和几个高频坑逐一拆开。2. 把K线数据变成网络能吃的样本特征构造、归一化与滑动窗口2.1 先定输入输出预测的对象和特征怎么选包里带的CSV文件是601601、600019、600100这类个股的历史行情字段不外乎日期、开盘价、最高价、最低价、收盘价、成交量、成交额。做股价预测最常见的做法是把前N天的行情数据作为输入预测未来第M天的收盘价或者直接预测下一天的收盘价。这里的N和M没有标准答案但要注意隐藏层神经元数量通常和输入维度有关输入特征越多网络参数就越多PSO搜索空间就越大收敛难度也会上去。我一般会先固定一个简单的结构输入取前5个交易日的开盘价、最高价、最低价、收盘价、成交量也就是25个特征输出是下一交易日收盘价。这个设计的好处是特征维度不高粒子维度可控跑起来快后续要改也方便。如果你手里的代码是stock_predict(1600)这个目录它大概率是在这个思路上做了不同窗口长度的对比实验。2.2 data.py脚本里实际要做的三件事从股价序列到监督学习样本中间隔着一个滑动窗口。包里data.py干的事本质上就是以下三步读CSV、补缺失值、按窗口切样本。这里有一个非常关键的细节股价数据是按时间排序的所以窗口切片时不能打乱顺序否则模型会看到“未来数据”预测就失去了意义。import pandas as pd import numpy as np def load_stock_data(csv_path): df pd.read_csv(csv_path) # 统一列名有的CSV列名是中文有的可能是英文 df.columns [date, open, high, low, close, volume] # 按日期升序排列确保时间顺序不会被破坏 df df.sort_values(date).reset_index(dropTrue) return df def create_sample(df, lookback5, pred_step1): lookback: 用过去多少个交易日作为输入窗口 pred_step: 预测未来第几个交易日 features, labels [], [] data df[[open, high, low, close, volume]].values for i in range(len(data) - lookback - pred_step): # 输入窗口从 i 到 ilookback-1 x data[i:ilookback].flatten() # 标签未来第 pred_step 天的收盘价 y data[ilookbackpred_step-1, 3] features.append(x) labels.append(y) return np.array(features), np.array(labels)这段代码的核心在create_sample函数里range(len(data) - lookback - pred_step)控制了样本总数保证每个输入窗口和它对应的标签都在序列范围内。flatten()把二维窗口数据展开成一维特征向量这一步是为了和神经网络的输入层对齐。如果后面PSO粒子维度算出来不对先回去检查这里lookback * 5是否等于你设定的输入层节点数。2.3 归一化训练集和测试集必须分开做神经网络对输入数据的量纲极其敏感。股价从几块钱到几百块钱都有成交量更是动不动就上千万不归一化的话距离计算会被大数值特征主导网络训练会非常不稳定。包里数据问题.docx记录过一版对比没归一化时训练误差振荡剧烈归一化之后曲线明显平滑。归一化这里有一个隐藏很深的坑整个数据集先算均值方差再切分和先切分再各自归一化结果完全不同。前者属于数据泄露的一种测试集的信息被提前引入了训练过程模型评估会偏乐观。正确的做法是只用训练集的统计量去归一化测试集。from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(0, 1)) # 训练集和测试集按时间顺序切分前80%训练后20%测试 split_idx int(len(features) * 0.8) train_x, test_x features[:split_idx], features[split_idx:] train_y, test_y labels[:split_idx], labels[split_idx:] # 关键只在训练集上fit测试集直接用同一个scaler train_x_scaled scaler.fit_transform(train_x) test_x_scaled scaler.transform(test_x) # 标签也建议做归一化后面反归一化回来算误差 scaler_y MinMaxScaler(feature_range(0, 1)) train_y_scaled scaler_y.fit_transform(train_y.reshape(-1, 1)).flatten() test_y_scaled scaler_y.transform(test_y.reshape(-1, 1)).flatten()为什么测试集不能用fit_transform因为fit会重新计算测试集的均值和范围相当于隐式地把测试集分布的信息泄漏给了模型。实际跑预测时你用到的永远是训练集上估计的scaler预测出的结果是归一化后的数值还要通过scaler_y.inverse_transform换回真实股价。2.4 评价指标的选型MSE、RMSE和MAE各看什么摘要里反复提到MSE、RMSE和MAE这三者的区别决定了你判断PSO有没有优化成功的方式。MSE对误差平方放大会让模型更在意大偏差样本RMSE相当于MSE开根号量纲和股价一致好解释MAE则完全线性对异常值不那么敏感。股票预测场景里我一般三个都打印出来但最终决策看RMSE因为它在惩罚大误差的同时单位还是“元”直接对得上股价。指标公式特点适用判断MSEmean(y - y_pred)^2大误差权重高训练收敛判断RMSEsqrt(MSE)量纲对齐股价模型对比主指标MAEmean(abs(y - y_pred))抗异常值稳健性参考3. 用PSO替代随机初始化的核心实现粒子编码、适应度与收敛细节3.1 PSO为什么适合干这件事一个全局搜索的视角BP网络训练本质上是梯度下降它对初始位置非常敏感。初始化落在陡峭的局部极小附近训练很容易被吸住导致预测结果方差很大。PSO的思路不同它不依赖梯度而是维护一群粒子在参数空间里飞行每个粒子代表一组完整的网络权重和阈值通过个体最优pbest和全局最优gbest的牵引不断更新位置。这等于在训练前先做一轮全局搜索找到一处比较有前景的参数区域再交给BP反向传播做局部细化。这份资源里stock.py的核心逻辑就是把PSO和神经网络串成这样的两阶段流程先用PSO搜权重把搜到的最优位置作为BP网络的初始权重再继续训练。这样做的好处是即便后面BP仍然会落入局部极小起始点已经比随机初始化好得多。同时需要注意PSO阶段的目标函数并非神经网络训练损失而是适应度函数通常直接用验证集或训练集的MSE。3.2 粒子编码权值和阈值全部拼成一维向量粒子编码是整个实现里最容易出错的部分。一个简单的前馈网络输入层25个节点、隐藏层8个节点、输出层1个节点参数总数为(258 8) (81 1)等于217个。每个粒子就是一维向量长度等于217。编码顺序要固定比如先是输入层到隐藏层的权重再是隐藏层阈值接着是隐藏层到输出层的权重最后是输出层阈值。import numpy as np def init_particle(dim, lb-1.0, ub1.0): 初始化单个粒子参数范围[-1, 1] return np.random.uniform(lb, ub, dim) # 网络结构参数 input_dim 25 hidden_dim 8 output_dim 1 # 粒子向量总维度 dim (input_dim * hidden_dim hidden_dim) (hidden_dim * output_dim output_dim) print(粒子维度:, dim) # 生成一个粒子 particle init_particle(dim) def decode_weights(particle, input_dim, hidden_dim, output_dim): 把一个粒子拆解成网络的权重矩阵和阈值向量 idx 0 # 第一组输入层到隐藏层权重 w1 particle[idx:idxinput_dim*hidden_dim].reshape(input_dim, hidden_dim) idx input_dim * hidden_dim # 第一组阈值隐藏层偏置 b1 particle[idx:idxhidden_dim] idx hidden_dim # 第二组隐藏层到输出层权重 w2 particle[idx:idxhidden_dim*output_dim].reshape(hidden_dim, output_dim) idx hidden_dim * output_dim # 第二组阈值输出层偏置 b2 particle[idx:idxoutput_dim] return w1, b1, w2, b2这里最隐蔽的问题是如果你把粒子的概率分布范围设成[0, 1]而不是[-1, 1]网络权重的符号自由度会被锁死很多特征组合根本表达不出来。错误地设置边界会压缩搜索空间导致预测效果显著变差。通常建议权重粒子范围设为[-1, 1]或至少[-2, 2]偏置范围可以更宽一些。3.3 适应度函数设计用哪些样本算误差PSO阶段需要评估每个粒子对应的网络性能。如果拿全部训练样本去算MSE适应度评估会很慢。实际做法是在训练集里随机抽一批样本比如100条到200条用这批样本的MSE作为适应度值。这样做虽然不是全量评估但足以在迭代早期区分粒子好坏。def fitness(particle, train_x, train_y): 粒子适应度前向传播后计算MSE越小越好 w1, b1, w2, b2 decode_weights(particle, input_dim, hidden_dim, output_dim) # 隐藏层计算 hidden np.tanh(np.dot(train_x, w1) b1) # 输出层计算线性激活 output np.dot(hidden, w2) b2 mse np.mean((output.flatten() - train_y) ** 2) return mse这一段有三个细节值得留神。第一隐藏层用tanh激活输出层不加激活函数因为股价预测是回归问题输出层加sigmoid或tanh会把输出限制在一个小区间模型很难预测出大幅涨跌。第二train_x传入的是已经归一化后的子集不要传原始股价数据否则大数值特征会把隐藏层输入推向饱和区。第三, b2虽然是(1,)维度的数组但广播机制会正确加到输出上如果你这里写错成b2[0]也问题不大但bshape错了就会报维度不匹配。3.4 PSO主循环速度更新、惯性权重与收敛判断粒子群优化的更新公式是这份资源的核心。位置更新分两个公式速度更新由惯性权重、个体认知项和社会认知项组成位置更简单就是旧位置加新速度。惯量权重w通常从0.9线性递减到0.4前期保持全局探索后期转向局部细化。def pso_optimize(objective_func, dim, pop_size30, max_iter100, w_max0.9, w_min0.4, c12.0, c22.0): # 初始化种群 positions np.random.uniform(-1, 1, (pop_size, dim)) velocities np.random.uniform(-0.1, 0.1, (pop_size, dim)) # 个体最优 pbest positions.copy() pbest_scores np.array([objective_func(p) for p in positions]) # 全局最优 gbest_idx np.argmin(pbest_scores) gbest positions[gbest_idx].copy() gbest_score pbest_scores[gbest_idx] scores_history [] for t in range(max_iter): # 线性递减惯量权重 w w_max - (w_max - w_min) * t / max_iter for i in range(pop_size): # 速度更新三部分叠加 velocities[i] (w * velocities[i] c1 * np.random.random(dim) * (pbest[i] - positions[i]) c2 * np.random.random(dim) * (gbest - positions[i])) positions[i] velocities[i] # 边界处理超出范围的粒子拉回来 positions[i] np.clip(positions[i], -1.0, 1.0) # 重新评估适应度 score objective_func(positions[i]) if score pbest_scores[i]: pbest_scores[i] score pbest[i] positions[i].copy() if score gbest_score: gbest_score score gbest positions[i].copy() scores_history.append(gbest_score) return gbest, gbest_score, scores_history惯性权重公式这里有个经验值可以用w w_max - (w_max - w_min) * t / max_iter前期不好好探索全局后期又无法细致收敛。学习因子c1和c2都设为2.0是很多入门代码里的写法但如果你发现粒子在迭代后期不断震荡、最优值降不下去可以把c1和c2都降到1.5或1.2。种群规模和迭代次数也要注意这份资源里面的训练过程文档记录过一种现象迭代次数不是越大越好超过一定阈值后最优值不再下降反而因为粒子收敛到同一区域而失去多样性泛化能力变差。3.5 PSO和BP的衔接搜到的权重怎么交棒拿到PSO返回的gbest向量后需要把它decode成权重矩阵赋值给神经网络再继续用BP算法训练。这一步要注意如果你用框架搭建网络不能直接对model.layers[i].kernel赋值就完事因为框架内部权重初始化和优化器状态是绑定在一起的我一般直接手写一个前向传播函数把PSO解码出的权重作为初始参数再用最朴素的梯度下降或L-BFGS做后续训练。def train_with_pso(particle, train_x, train_y, epochs500, lr0.01): PSO初始化后继续用梯度下降训练 w1, b1, w2, b2 decode_weights(particle, input_dim, hidden_dim, output_dim) for epoch in range(epochs): # 正向传播 hidden np.tanh(np.dot(train_x, w1) b1) output np.dot(hidden, w2) b2 # 反向传播计算梯度简化解法 output_error output.flatten() - train_y w2_grad np.dot(hidden.T, output_error.reshape(-1, 1)) b2_grad np.sum(output_error) hidden_error np.dot(output_error.reshape(1, -1), w2.T) * (1 - hidden ** 2) w1_grad np.dot(train_x.T, hidden_error) b1_grad np.sum(hidden_error, axis0) # 更新权重 w1 - lr * w1_grad b1 - lr * b1_grad w2 - lr * w2_grad b2 - lr * b2_grad return w1, b1, w2, b2这段代码里的梯度是手动推导的有几个值得注意的点。hidden_error的计算里(1 - hidden ** 2)是tanh函数的导数如果隐藏层用了sigmoid激活这里要换成hidden * (1 - hidden)。学习率lr不能给太大0.01或0.005是常见起步值因为此时权重已经处于PSO搜索出的较优区域大步长更新反而会破坏它。这里没有加入动量和学习率衰减如果你发现损失曲线后期抖动可以加一个简单的动量项。训练完成后评估阶段需要切到独立的测试集。这时候把测试集数据过一遍完整前向传播得到预测值再通过scaler_y.inverse_transform还原成股价然后计算RMSE和MAE。4. 避坑数据泄露、归一化错位与维度不匹配的排查记录4.1 归一化顺序错位导致预测曲线异常平滑现象训练完成后预测曲线看起来和真实曲线高度重合RMSE数值小到不真实但把预测值往前平移了几个交易日后才发现它几乎是真实行情前一天的延迟拷贝。原因这份资源的数据问题.docx里记录的第一个坑就是这种。原因是代码在切分训练集测试集之前对整个数据集做了归一化测试集的未来信息进入了训练集输入的分布范围。模型学到的是用当天的特征去“复盘”当天的收盘价而复盘用的数据本身就来自未来窗口预测自然看起来完美。解决严格按2.3里的示范先切分再归一化scaler只fit训练集。验证方法是打印训练集和测试集的均值和标准差如果两者几乎一致就要怀疑是不是归一化阶段的顺序错了。另一个信号是测试集MAE远大于训练集且差值异常大这时要回头检查有没有数据泄漏。4.2 粒子维度算错导致decode时报形状错误现象运行PSO时一切正常但进入适应度评估的decode阶段np.dot报shape mismatch输入是(25, 8)、权值却给出了一维数组的长度和预期不一致。原因维度计算时没有换算隐藏层偏置数量。常见错误是只算了权重数量、把阈值漏了或者把输出层的偏置数量写成了等于隐藏层节点数。粒子维度和decode时的index游标各自独立维护一旦一个粒子在某次迭代里被随机初始化为异常分布代码不会报错但decode出来的矩阵会错位。解决在init_particle之后立刻做一次维度自检把粒子拆开再拼回去确认能够还原到原始长度。另外固定设置一个全局常量DIM所有位置引用这个常量不要散落在多个函数里手填数字。4.3 用全量训练集做适应度评估导致迭代极慢现象PSO每次迭代评估30个粒子每个粒子都在上万条样本上前向传播单轮迭代要跑几十秒迭代几十次之后几乎走不动。原因适应度函数没有抽样每次都用全部训练数据。PSO的评估次数远高于BP训练全量评估的时间代价在算法里被放大。解决每次适应度评估前从训练集中随机抽128条或256条样本固定一个随机种子。这样既保证评估稳定性速度也能提升一个数量级。抽样带来的误差在迭代后期会自然被gbest的多次评估修正掉实测结果几乎不影响最终权重质量。4.4 隐藏层节点数设置过大导致过拟合与搜索空间爆炸现象把隐藏层节点从8改成32后训练集MSE非常好测试集RMSE不但没降反而升高而且PSO收敛速度明显变慢。原因隐藏层节点增加粒子维度从二百多涨到一千多PSO在高维空间搜索难度指数级上升。过拟合在这个场景里也不可忽视复杂的网络结构在股票这类高噪声数据上特别容易记住历史细节而不是泛化规律。解决隐藏层节点控制在输入维度的1/3到1倍之间比如输入25个特征时816个隐藏节点是比较稳妥的范围。另外粒子数量的设置也值得注意维度越高种群规模也要相应增大但不要盲目从30调到100可以先按维度开根号再乘2来粗估。4.5 PSO阶段和BP阶段用了不同的训练样本子集现象PSO搜出来的权重代入网络继续训练后损失不降反升甚至直接发散。原因PSO阶段适应度评估抽的样本和后续BP阶段用的样本分布不一致。如果PSO阶段固定用了某128条样本而BP阶段用的是全部训练数据模型会被“拉”到另一个方向先前的搜索成果就被浪费了。解决把抽样样本固定为一次整份资源的实验都在这同一份样本上进行最后训练阶段再放开到全量数据。这其实相当于先在子集上粗调、再在全量上细调两阶段目标一致收敛行为才会稳定。5. 从股价到岩爆和投资组合这份资源的迁移价值5.1 岩爆预测PDF里藏着另一套PSO调参思路包里那个《基于粒子群算法和广义回归神经网络的岩爆预测》PDF名字看起来和股票没什么关系但它恰好展示了一个重要事实PSO配神经网络不是只能做时间序列回归换到分类问题也是一样的套路。岩爆预测的输入是地质应力、岩石强度等指标输出是岩爆等级网络从单个输出节点变成多个输出节点的分类头PSO阶段的目标函数从MSE换成交叉熵或分类错误率。这意味着你拿到手的stock.py只要改三处就能迁移到岩爆这类问题上一是输入特征矩阵换掉二是输出层的激活和损失函数换成分类版本三是适应度函数里的误差计算改成分类准确度。代码骨架完全不用动。这也是我建议拿到资源后先读PDF而不是只跑代码的原因论文里往往记录了PSO迭代曲线和不同参数组合的对比这些信息在代码里是看不到的。5.2 投资组合优化文档补充了改进PSO的细节《基于改进粒子群算法的神经网络优化证券投资组合方法》这篇论文的价值在于“改进”两个字。它提到的改进点主要集中在惯性权重的调整策略上不是简单的线性递减而是根据粒子聚集程度动态调整当粒子分布过于集中时增大惯性权重让种群重新分散探索。这个思路同样适用于股票预测场景里的PSOBP——你的gbest连续多轮没有变化时说明粒子可能早熟收敛此时人为增大w或者随机重置部分粒子位置可以保住探索能力。5.3 参数迁移的边界哪些能沿用哪些必须重置从股价场景迁到岩爆或投资组合场景有几个参数不能顺手沿用。粒子边界范围要重新审视股价数据归一化后权重范围[-1, 1]合适但投资组合场景如果输出是仓位权重总和需要等于1这时必须在decode之后加一次softmax或归一化不能直接丢给网络。学习率和迭代次数也要重新调整分类问题通常需要更多迭代才能稳定但每次适应度评估更便宜。5.4 文件包里的训练过程文档怎么用来做参考训练过程文档里记录了训练集和测试集误差的变化曲线这可能比代码本身更有参考价值。对照这些文档看自己的运行结果如果趋势不一致优先怀疑数据预处理而不是算法实现。这类文档存在多个docx格式的原因通常是作者实验过程中保存的不同阶段版本翻一版最完整的对照即可不用每份逐字读。6. 判断PSO有没有真的优化成功三个可落地的验证技巧判断优化效果时首要原则是必须设置对照组同一网络结构、同一数据分割、同一初始化范围下跑三次随机BP再跑一次PSOBP对比测试集RMSE。PSO的优势应当体现在结果波动更小、预测误差中位数更低而不是单次预测RMSE小到飞起因为单次结果可能只是随机种子运气好。放两组对照后再看数据往往会发现PSO带来的提升未必是RMSE降幅巨大而是三次结果的标准差从十几缩到了个位数这才是它的实战价值。第二个技巧是滚动回测。固定一段历史数据比如用2019到2021年做训练然后把2022年按季度切片每个季度末重新用已有数据训练一次再预测下一个月的走势。这种回测能暴露出模型在不同行情段的表现比如震荡市和单边市的预测误差差异很大只用一个测试集打分很容易掩盖这一点。第三个技巧是观察方向命中率。RMSE体现的是数值精确度但股票交易关心的是方向和时机。代码里增加一行判断如果预测收盘价比当前收盘价高就记为上涨否则记为下跌然后统计预测方向和真实方向的匹配率。实测中方向命中率超过55%已经比随机好不少。如果你发现RMSE不错但方向命中率只有45%左右这说明模型在“平均意义”上拟合了股价但转折点基本没抓住需要回头调整输入特征或窗口长度。最后从我自己的习惯说起自从跑过一份忘记归一化就训练的代码、看着损失函数曲线一路抽风之后我每次跑这类预测实验都会强制自己走一遍固定检查流程——第一检查训练测试集是否在归一化前就切分第二打印粒子维度确认和网络参数总量一致第三跑三组随机种子做方差对比。股票数据本身噪声很大把实验流程固化成模板才能保证你优化出来的差异真的来自PSO而不是偶然的随机种子。希望这些排错思路能帮你在自己的实验里少走一段弯路。本文还有配套的精品资源点击获取