简介一份聚焦神经网络在金融时序分析中应用的科研论文PDF面向量化研究、金融建模及机器学习实践者。资料系统梳理了数据预处理环节中的异常值检测与缺失值填补流程重点讲解了RBP神经网络补全缺损数据、Markowitz均值-方差组合模型优化收益风险以及NARX非线性多步预测网络构建股指预测模型的方法同时结合Python绘制时间序列调和曲线直观展示数据缺失情况。NARX模型采用三层结构隐含层使用高斯激活函数依据链式偏微分法则调整网络参数最终给出平均误差0.092以内的实证结果。内容还覆盖了投资组合相关矩阵与协方差矩阵的评价维度以及过拟合与交叉验证等实操问题从数据清洗、组合评估到模型优化形成完整链路。资源为单份PDF文件共1个文件压缩包大小1.83MB已有119人学习浏览。对于希望快速理解神经网络选股思路、NARX动态建模技巧以及风险收益平衡机制的读者这份材料能提供较清晰的算法推导与实验参考价值适合作为金融科技、数据建模方向的入门研读或项目参考资料。1. 股指预测不是玄学但网络选错、数据切错都会翻车量化圈每隔一段时间就有人晒出“基于神经网络的股指预测与选股”的优美曲线点开一看多半是验证集在涨。做过的人心里都有数这个方向真正的难点从来不是把模型跑起来而是让预测严格落在未来信息之外让选股信号在回测里真实可复现。它要解决两件事——对指数未来涨跌给出概率判断再把同一套方法平移到全市场个股上排序选出值得持仓的组合。适合已有Python和基础机器学习底子、想用深度神经网络替代经验规则的从业者。看懂本文你拿到的是一整套从数据清洗、特征构造、LSTM训练到选股验证的落地路径而不是一张画饼的收益曲线。2. 把涨跌装进监督学习问题建模与网络选型2.1 先定义问题为什么预测方向比预测价格靠谱我最初的做法很幼稚把每天收盘价丢给LSTM让它输出明天收盘价。训练损失降到接近0验证集拟合得像印上去一样可实盘一跑就钝。后来才想明白价格序列是非平稳的十年前3000点和现在3000点的含义完全不同。让网络去拟合绝对价格等于逼它记下一条长期漂移曲线而对真正要抓的短期相对变化几乎没有表达力。常见做法是把它重构成分类问题给定过去N个交易日的特征序列预测未来H个交易日收益率是否大于某个阈值。数学化一点x_t 是过去N天的特征向量序列y 是未来H日收益率的正负号。为什么是分类而不是回归因为分类对噪声更鲁棒涨跌方向的信噪比远高于具体收益数值同时分类输出天然能转成概率为后面选股排序提供同一把尺子。阈值通常设在0.5%到1%之间太大会让正样本稀少太小则几乎全是噪声标签。另一个必须想清楚的点是滑窗长度N。对日线数据我一般取20个交易日约一个月作为基础窗口。20这个数字不是玄学它对应A股月级别趋势的记忆长度太长会把远古行情塞进来太短则抓不住均线支撑。窗口定了之后整个数据集的样本量由总交易日减去窗口和预测跨度决定一般能拿到两千到三千条样本够一个中小规模网络吃饱。2.2 网络选型前馈神经网络、LSTM、一维卷积与GNN怎么挑模型选型是新手最容易翻车的地方。一上来就上几十层Transformer结果验证集还不如“昨天涨今天就买”的朴素基线。先看前馈神经网络也就是常说的BP神经网络它结构最简单输入是一个打平的特征向量经过若干隐藏层后接输出层靠反向传播更新权重。它的问题在于完全没有时间顺序概念——把第1天的数据和第19天的数据交换位置模型输出不变这对行情序列是致命的。用它做序列预测调参再狠准确率也基本在52%以下定位只能是基线对照。循环神经网络RNN在设计上天生为序列建模但经典RNN存在梯度消失长序列记忆很差。LSTM神经网络用输入门、遗忘门和输出门把长期信息保存在单元状态里是目前RNN最常用的替代。对股指日线这种几百到几千个样本的序列两层LSTM把小周期波动和大周期趋势分开建模是最稳的起点方案。卷积神经网络在序列预测里的用法比较特别一维卷积神经网络用一维卷积核扫描时间轴提取K线形态的局部模式比如“连续三根阳线”“长下影线放量”。它训练快、不挑数据规模但单用对全局趋势捕捉弱常见做法是把1D CNN输出再接LSTM让CNN提特征、LSTM记时序。图神经网络GNN是这几年的热点把个股之间的关联、行业联动建模成图结构理论上对选股很有吸引力。但图的构造方式、邻接矩阵的时效性都很难调数据量也不够支撑入门阶段不建议动它等LSTM流水线跑通再研究不迟。我给的选型结论是三步走BP做基线LSTM作为主力模型先把预测做稳之后用1D CNNLSTM的复合结构去提特征换取边际收益。架构对比见表。模型时间顺序训练成本过拟合风险在股指预测中的角色前馈/BP神经网络不支持低中基线对照RNN/LSTM支持中高主力方案一维卷积神经网络局部支持低中特征提取器CNNLSTM支持中高高进阶方案图神经网络GNN支持高高选股扩展2.3 输入设计只喂收盘价等于让模型闭眼猜我见过很多人复现代码训练数据就是一个收盘价序列网络怎么调都上不去。原因很简单单序列信息量太低成交量、换手率、振幅、量比、均线偏离这些特征直接关系到参与者情绪和资金行为不加等于少了一整维信息。我常用的特征集按四维组织。第一维度是价格开高低收以及它们衍生出的涨跌幅和振幅。第二维度是量能成交量、成交额、换手率、量比当日成交量与过去5日均量的比值。第三维度是资金指数层面可以看北向资金净流入个股层面如果拿不到就用成交额替代。第四维度是技术形态5日和20日均线偏离度、RSI、布林带位置。特征不是越多越好这是血泪教训。原始序列堆到三四十维网络很快开始背特征验证集幻觉式上涨。经验上限是20维左右超过的部分该筛就筛。另外所有特征在进网络前要做标准化量纲不统一会让LSTM的门控机制失效——比如成交额以亿为单位、RSI在0到100之间直接把原始值拼进特征向量梯度会被大数值特征带走小数值特征等于白放。3. 从行情数据到训练样本特征构造与数据集切分3.1 数据获取与清洗停牌、复权和跳空三件事以沪深300指数日线为例常见做法是用akshare这类免费数据接口直接拿数据代码很短import akshare as ak import pandas as pd # 拉取沪深300指数日线接口返回的列名在不同版本略有差异 raw ak.index_zh_a_hist(symbol000300, perioddaily, start_date20100101, end_date20241231) raw.columns [date, code, open, close, high, low, volume, amount, amplitude, pct_change, change, turnover] # 只保留建模要用的列并按时间升序排列 df raw[[date, open, close, high, low, volume, amount, turnover]].copy() df[date] pd.to_datetime(df[date]) df df.sort_values(date).reset_index(dropTrue) # 有缺失的交易日直接剔除不做填充 df df.dropna().reset_index(dropTrue) print(df.tail())这段代码的逻辑是先拿全历史日线再统一列名然后按时间排序最后剔除缺失值。需要说明的是指数日线本身不存在复权问题但如果把模型平移到个股上一定要用前复权价格否则除权日会出现一个与真实交易无关的假跳空模型会当成重要信号学进去。停牌日同样要处理常见做法是删除该日记录而不是用前后填充值硬补因为补出来的价格不真实。参数上start_date选2010年而不是更早一是A股早期流动性结构和现在差异大二是数据区间越长行情风格漂移越明显。对日线级预测10到15年数据已经能覆盖两三轮牛熊再多反而让模型去拟合早已失效的旧规则。3.2 特征构造与标签生成未来5日方向怎么写才不泄漏特征构造我习惯用向量化操作一次性算完写在一个函数里清洗和调试都方便def build_features(df): # 基础涨跌幅和振幅 df[ret_1d] df[close].pct_change() df[amp] (df[high] - df[low]) / df[close] # 均线偏离度收盘价相对均线的位置 for win in [5, 10, 20]: ma df[close].rolling(win).mean() df[fma_dev_{win}] df[close] / ma - 1 # 量比当日成交量除以5日均量反映放量缩量 df[vol_ratio] df[volume] / df[volume].rolling(5).mean() # RSI(14)常用动量指标 diff df[close].diff() gain diff.clip(lower0).rolling(14).mean() loss (-diff.clip(upper0)).rolling(14).mean() rs gain / (loss 1e-9) df[rsi_14] 100 - 100 / (1 rs) # 标签未来5日收益率是否超过0.5%作为上涨正样本 df[future_ret] df[close].shift(-5) / df[close] - 1 df[label] (df[future_ret] 0.005).astype(int) # 删除因滚动窗口和shift产生的前后缺失行 df df.dropna().reset_index(dropTrue) return df这里有一个关键细节标签的future_ret用的是close.shift(-5)意味着第t行样本的标签只有到t5日收盘后才能确定。真实部署时要格外注意当天收盘后只能做出当天的特征和预测不能用未来数据验证这是很多回测失效的根源。量比、均线偏离、RSI都是无量纲或有界量进入LSTM前不需要再做标准化成交量、成交额这类绝对量则要取对数或交给StandardScaler。不要把原始成交量直接喂进去几百亿和几百万的数值差距会把梯度推向一个方向小特征直接失效。3.3 按时间切分与标准化训练集、验证集、测试集顺序不能乱切分股票数据不能像图像分类那样随机打乱必须按时间顺序硬切import numpy as np def split_sequences(df, feature_cols, seq_len20): data df[feature_cols].values labels df[label].values X, y [], [] for i in range(len(data) - seq_len): win_x data[i:i seq_len] win_y labels[i seq_len - 1] # 窗口内最后一天的标签 X.append(win_x) y.append(win_y) X np.array(X, dtypenp.float32) y np.array(y, dtypenp.int64) # 按时间顺序前70%训练中间15%验证最后15%测试 n len(X) idx_train int(n * 0.70) idx_val int(n * 0.85) X_train, y_train X[:idx_train], y[:idx_train] X_val, y_val X[idx_train:idx_val], y[idx_train:idx_val] X_test, y_test X[idx_val:], y[idx_val:] return (X_train, y_train), (X_val, y_val), (X_test, y_test)这里seq_len20代表用最近20个交易日的特征序列预测未来5日方向。特征窗口和标签窗口前后相接不存在重叠所以每个滑窗样本的信息只来自窗口内的过去20天。标准化必须在切分之后进行且只能fit训练集再拿同一组参数transform验证集和测试集from sklearn.preprocessing import StandardScaler scaler StandardScaler() n_features X_train.shape[2] X_train_flat X_train.reshape(-1, n_features) scaler.fit(X_train_flat) # 只学训练集的均值和方差 X_train scaler.transform(X_train_flat).reshape(X_train.shape) X_val scaler.transform(X_val.reshape(-1, n_features)).reshape(X_val.shape) X_test scaler.transform(X_test.reshape(-1, n_features)).reshape(X_test.shape)如果把scaler.fit放在切分前或对整个数据集做验证集和测试集的信息就会泄漏进训练过程这是时序预测里最隐蔽的坑也是很多模型实验集指标高得离谱、实盘一塌糊涂的原因。注意标准化一定要在切分之后做。先切分、再fit、再transform这个顺序错了后面的所有评估都失去意义。4. LSTM模型训练与调参先让预测稳定再谈收益4.1 搭建一个可复现的两层LSTM预测器模型结构我固定成“LSTM层全连接层”代码短、调试容易也方便后续把数据规模扩大后换更复杂的结构import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size, hidden_size, num_layers, dropout0.3): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0.0, ) # 二分类输出1个logit配合BCEWithLogitsLoss使用 self.fc nn.Linear(hidden_size, 1) def forward(self, x): # x shape: (batch, seq_len, input_size) out, (h_n, c_n) self.lstm(x) # 取最后一个时间步的隐状态作为序列表示 last out[:, -1, :] logits self.fc(last).squeeze(-1) return logits参数含义逐一说清楚input_size是特征维度也就是第3章feature_cols的长度hidden_size是LSTM隐状态维度64是起步值再小表达能力不足再大容易过拟合num_layers2分成两个堆叠层让网络先学短周期再学长周期batch_firstTrue表示输入张量维度顺序是(批次, 序列长度, 特征数)PyTorch默认是(序列长度, 批次, 特征数)写错就会出现维度报错。forward里out[:, -1, :]取的是序列最后一个时间步的输出。有人会把所有时间步的输出做mean pooling也可以但最后一个时间步的信息最接近预测点实操里效果通常更好。全连接层把隐状态压到1个logit对应二分类如果要换成三分类涨/平/跌把fc输出维度改成3并把损失函数换成CrossEntropyLoss即可。4.2 训练配置学习率、早停与学习率衰减训练循环用PyTorch标配写法加上梯度裁剪、早停和学习率衰减三个机制def train_model(model, train_loader, val_loader, epochs60, lr1e-3, patience10): optimizer torch.optim.Adam(model.parameters(), lrlr, weight_decay1e-5) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience5) criterion nn.BCEWithLogitsLoss() best_val_loss float(inf) best_state None wait 0 for epoch in range(epochs): # 训练阶段 model.train() total_loss 0.0 for xb, yb in train_loader: optimizer.zero_grad() logits model(xb) loss criterion(logits, yb.float()) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() * xb.size(0) # 验证阶段 model.eval() val_loss 0.0 with torch.no_grad(): for xb, yb in val_loader: logits model(xb) loss criterion(logits, yb.float()) val_loss loss.item() * xb.size(0) scheduler.step(val_loss) # 早停验证损失连续patience轮不下降就恢复最佳权重 if val_loss best_val_loss: best_val_loss val_loss best_state {k: v.clone() for k, v in model.state_dict().items()} wait 0 else: wait 1 if wait patience: model.load_state_dict(best_state) print(f早停于 epoch {epoch}) break else: model.load_state_dict(best_state) return model三个关键点。第一梯度裁剪clip_grad_norm_是LSTM训练必须的序列建模的梯度范数容易爆炸不裁剪loss会突然变成nanmax_norm1.0是保守值想放开可以试2.0。第二weight_decay1e-5做小的L2正则对抗过拟合。第三ReduceLROnPlateau在验证损失平稳时把学习率减半比固定学习率更容易收敛到好的局部极小值。EarlyStopping的patience10轮和scheduler的patience5轮要区分开scheduler先一步开始降学习率给模型机会跳出平台期如果降了学习率还改善不了再触发早停这比直接早停更稳。epochs60只是上限实战里数据量小时往往20到40轮就早停了。4.3 评估预测方向准确率、F1和朴素基线对比训练完不要只看loss要看预测方向的命中情况。我固定用下面这组指标卡模型from sklearn.metrics import accuracy_score, f1_score, confusion_matrix def evaluate(model, X, y): model.eval() with torch.no_grad(): logits model(torch.tensor(X)) prob torch.sigmoid(logits) pred (prob 0.5).int().numpy() acc accuracy_score(y, pred) f1 f1_score(y, pred) cm confusion_matrix(y, pred) return {accuracy: acc, f1: f1, confusion: cm}除了accuracy我还额外看两个东西。一是F1分数因为涨跌样本经常不平衡单边行情里会严重倾斜准确率会被多数类放大。二是看涨类别的召回率——选股任务里更关心“该涨的股票有没有被选出来”召回率低说明模型把大量真上涨漏掉了排序选股的意义就大打折扣。最重要的对照实验是朴素基线直接用“今日收盘涨则预测明天也涨”做规则准确率大约51%到53%。神经网络如果连这个都超不过别谈后面的选股先去查特征和泄漏。当模型在测试集上准确率稳定大于55%时方向统计意义上可用想冲到60%以上日线级别的指数预测很难复现不如转向选股场景——样本量从1个指数变成几千只股票模型能学到的东西才够多。5. 时序预测的五个经典坑数据泄漏、过拟合与失效特征5.1 标准化和标签里的未来函数两个最隐蔽的数据泄漏现象训练完的模型在验证集上准确率高达80%曲线完美得像资产翻倍可实盘交易几天就连续亏损。这是我在这个方向踩过的最大一个坑排查到最后发现泄漏根本不在模型里而在数据处理。原因一有的人在切分数据前对整份DataFrame做StandardScaler把验证集、测试集的均值和方差也学进了转换参数。模型等于开卷考试测试数据的信息已经被编码进特征分布里验证集当然好看。原因二标签构造时误用未来信息比如用当天的最高最低价来决定当天是否持仓或者用未来复权数据反推现在的价格都属于未来函数。解决严格遵循“先按时间切分再只fit训练集”两步法标签必须由t1日及之后的纯未来数据计算且特征窗口和标签窗口之间不能有重叠。我现在的代码里专门写了一条断言测试集样本的时间戳必须全部晚于训练集跑数前先校验宁可报错也不要带病训练。5.2 过拟合与随机种子为什么两次训练结果差很多现象训练集loss一路降到0.1以下验证集loss在某个epoch后突然反弹或者模型输出基本只有“涨”一类。另一个现象是同一份数据、同一个脚本两次训练的准确率能差3到5个百分点这让回测和调参都不可信神经网络在这里表现得像个黑匣子。原因hidden_size设到256甚至512LSTM层数加到3层以上再加上全连接层后参数量轻松过千万而日线样本量只有两三千条模型直接背题。随机种子不固定是另一个隐形原因PyTorch的DataLoader默认shuffleTrue加上没有固定torch.manual_seed每个epoch的样本顺序都在变训练结果自然漂移。解决从LSTM两层、hidden_size64起步dropout0.3加早停和梯度裁剪在脚本开头固定torch.manual_seed(42)、np.random.seed(42)DataLoader里设置generator。如果调大参数量要同步增加正则强度并把训练集样本扩到1万条以上否则就是拿参数量换幻觉。5.3 类别不平衡与回测滑点选股信号失效的两个放大器现象把模型从指数平移到全市场个股后Top20组合的回测年化收益很高但实盘跟单跑不赢有的月份还跑输指数。原因一全市场个股的涨跌分布并非均衡。指数成分股在震荡市大约五成交易日上涨但个股端牛市里上涨占比可到60%以上。模型用默认阈值0.5做分类时会把大量边缘上涨判成下跌选股排名的区分度被压缩。原因二回测只按收盘价成交没扣手续费和滑点。A股双边费率加滑点一般在万三到万五5日换仓一次一年约50次换手成本累计吃掉3到5个点预测本身只有55%左右的准确率这点优势很容易被交易成本抹平。解决对类别做加权或降低判断阈值回测里每笔成交强制扣除双边费用至少扣万五再算净收益选股周期不要短于5个交易日换手越频繁成本越敏感。我现在的做法是预测概率只用于排序排名稳定后再结合基本面做人工过滤不盲目相信分数本身。5.4 特征失效与行情漂移模型不是训一次用一年现象模型在测试集上准确率56%照此跑了一个月第二个月开始连续低于50%Top20组合跑不赢指数。很多人第一反应是“模型坏了”其实只是市场风格变了。原因日线特征均线、RSI、量比在趋势市有效进入震荡市后特征分布整体偏移模型学到的是旧风格下的规律。行情风格漂移不是异常而是常态用固定模型去对抗漂移等于拿去年的地图找今年的路。解决滚动重训每月底用最近12个月数据重新训练一次同时对特征分布做监控比如计算特征均值、方差的变化幅度一旦超过阈值就触发重训。这是我做这个方向最持续的维护成本也是想长期投入必须接受的现实模型是一个需要定期保养的工具不是一劳永逸的印钞机。6. 从预测分数到选股组合用RankIC评估选股能力6.1 用预测概率排序构建等权组合训练好模型后等收盘拿到当日全市场股票特征逐只推理出上涨概率按概率从高到低排序取前20只等权持有5个交易日scores [] for stock_df in all_stocks: # 每只股票的特征序列 prob predict(model, stock_df) # 取sigmoid后的概率 scores.append(prob) # 按概率降序取前20只等权分配资金 picks sorted(range(len(scores)), keylambda i: -scores[i])[:20] weight 1 / 20等权是为了不把赌注押在单只股票上。排序而不是设阈值选股是因为模型校准不一定完美——概率绝对值可能有偏但相对排名在同类股票之间更稳定。6.2 RankIC与分组回测看排序能力不看准确率选股模型的评价指标和预测模型不一样准确率只说明“涨跌命中”选股还看重“概率高的是不是真的涨更多”。我固定用下一期收益和预测概率的Spearman相关系数也就是RankIC来判断RankIC区间判断大于0.05较好的选股能力值得继续投入0.03到0.05有一定区分度可小仓位验证0到0.03统计不显著先回去查特征泄漏小于0模型在反向选择立即停用我自己的习惯是每天盘中跑完选股后把Top20组合收益和全市场平均收益对比连续跟踪4到8周看超额收益是否稳定为正。这个频率能过滤掉单个月的行情运气也比训练一次看一个测试集更接近实盘真相。这个方向值得做但它不是一夜暴富的捷径靠的是把数据、特征、回测纪律一件件磨到位。我自己也栽过“验证集75%、实盘亏损”的跟头后来才明白时序预测里最大的敌人不是模型容量而是无处不在的未来信息。希望你在这个方向上少走几步弯路。本文还有配套的精品资源点击获取