1. 从一条均线说起:这个项目到底在做什么
股票收盘均价这条线,看起来平平无奇,但真拿它当神经网络的拟合目标,你会发现里面藏着不少门道。这个项目的核心思路很直接:用TensorFlow搭一个最简的前馈网络,输入是历史价格序列,输出是未来某个时间点的收盘均价预测值。说白了,就是让网络自己去找价格波动背后的那条“隐形的线”。
我第一次接触这个思路的时候,心里是犯嘀咕的——均线本身就是一个滑动窗口算出来的东西,用神经网络去拟合它,是不是有点杀鸡用牛刀?后来实际跑了几轮才发现,事情没那么简单。均线是确定性的计算结果,但神经网络拟合的是“从原始价格到均线值”的映射关系,这个映射里包含了噪声过滤、趋势提取、滞后补偿等隐含逻辑。换句话说,网络学到的不是均线公式本身,而是价格序列中哪些模式对均线贡献最大。
这个项目适合谁呢?如果你已经跑过TensorFlow的“Hello World”级别demo,知道什么是placeholder、什么是session,但还没真正把网络用到时间序列上,那这个项目就是为你准备的。它不需要GPU,不需要大数据集,一台普通笔记本就能跑完。但它的价值在于:让你第一次感受到“网络输出和真实值之间的差距”是怎么被一步步缩小的,以及在这个过程中,哪些参数在悄悄影响最终结果。
注意:这个项目用的是历史价格数据做拟合,不是做交易信号。拟合均线和预测未来是两码事,前者是回归问题,后者涉及时间序列外推,难度完全不在一个量级。别把拟合结果直接当成买卖依据。
2. 数据准备:价格序列怎么变成网络能吃的格式
2.1 原始数据的获取与清洗
价格数据的来源有很多种,但不管从哪里拿,第一步永远是清洗。我见过太多人直接把原始数据丢进网络,结果loss曲线像心电图一样上下乱跳,排查半天才发现是数据里有缺失值或者异常跳变。
假设你拿到的是一个CSV文件,里面有两列:日期和收盘价。第一件事是检查缺失值。如果某天数据缺失,最简单的处理方式是前向填充,也就是用前一天的价格补上。但这里有个细节:如果连续缺失超过三天,前向填充就不太靠谱了,因为价格可能已经发生了趋势性变化。我的做法是,连续缺失超过三天的区段直接标记出来,要么整段剔除,要么用线性插值补上。
异常值的处理更微妙。价格数据里偶尔会出现“胖手指”导致的极端值,比如某天价格突然是前一天的十倍。这种值如果不处理,网络会花大量精力去拟合这个异常点,导致整体拟合效果变差。我的经验是,用滚动窗口的均值和标准差做判断,超出三倍标准差的点先标记,然后人工确认是否剔除。
2.2 滑动窗口构造输入输出对
神经网络吃的是固定长度的向量,而价格序列是变长的。所以需要把序列切成一个个固定长度的窗口。假设窗口长度是N,那么输入就是连续N天的收盘价,输出就是第N+1天的收盘均价。
这里有个关键选择:N取多少?N太小,网络看不到足够的上下文,学到的模式很局部;N太大,参数量增加,训练变慢,而且容易过拟合。我试过N=5、N=10、N=20、N=30这几个值,实测下来N=10到N=20之间比较平衡。对于日线数据,N=10大概对应两周的交易信息,N=20对应一个月,这两个窗口长度在捕捉短期趋势和避免噪声之间取得了不错的折中。
构造窗口的时候还有一个容易忽略的点:窗口之间的重叠。如果步长等于窗口长度,窗口之间不重叠,样本量会少很多;如果步长是1,窗口高度重叠,样本量大了但样本之间的相关性很强。我的做法是步长取窗口长度的一半,这样既增加了样本量,又不会让相邻样本过于相似。
2.3 归一化:为什么必须做,怎么做
价格数据的绝对值范围可能很大,比如从几块到几百块。如果不做归一化,网络的权重更新会非常不稳定。归一化的方法有几种:Min-Max归一化、Z-Score标准化、还有用对数收益率代替价格本身。
Min-Max归一化把数据缩放到[0,1]区间,公式是(x - min) / (max - min)。这个方法简单直观,但有个问题:如果未来出现比历史最大值还大的价格,归一化后的值会超出[0,1],网络可能处理不好。Z-Score标准化把数据变成均值为0、标准差为1的分布,公式是(x - mean) / std。这个方法对异常值更鲁棒,但需要保证训练集和测试集用同一套均值和标准差。
我个人的偏好是用Z-Score,因为价格数据的分布通常不是均匀的,用Min-Max容易受极端值影响。但不管用哪种方法,有一条铁律:归一化的参数必须从训练集计算,然后应用到验证集和测试集。如果对整个数据集计算归一化参数,就相当于把测试集的信息泄露到了训练过程中,评估结果会虚高。
提示:归一化之后,网络的输出也是归一化后的值。计算loss之前,要么把真实值也归一化,要么把网络输出反归一化。两种方式等价,但要注意保持一致。
3. 网络结构设计:为什么选全连接而不是LSTM
3.1 全连接网络的适用场景
这个项目用的是最基础的全连接网络,也就是Dense层堆叠。有人可能会问:时间序列不是应该用LSTM或者GRU吗?这个问题问得好,但答案取决于你的目标。
LSTM擅长捕捉长距离依赖,适合序列到序列的预测任务。但这个项目的目标是拟合均线,而均线本身是一个滑动窗口的线性组合。对于这种任务,全连接网络已经足够,因为输入窗口内的价格顺序信息可以通过窗口的排列方式隐式表达。换句话说,你把最近10天的价格按时间顺序排成一个向量,全连接网络虽然不显式建模时间依赖,但通过权重学习,它能够区分“第一天价格高”和“最后一天价格高”这两种模式。
更重要的是,全连接网络训练快、调试简单、参数量少。对于教学和入门项目,这些优势比模型容量更重要。我试过用LSTM跑同样的数据,训练时间多了三倍,最终loss只比全连接低了不到5%。对于这个项目来说,性价比不高。
3.2 层数、神经元数与激活函数的选择
网络结构的设计没有银弹,但有一些经验规则可以遵循。对于这个项目,我推荐的结构是:输入层(N个神经元)→ 隐藏层1(32个神经元,ReLU激活)→ 隐藏层2(16个神经元,ReLU激活)→ 输出层(1个神经元,线性激活)。
为什么是两层隐藏层?一层隐藏层理论上可以逼近任何连续函数,但需要非常多的神经元。两层隐藏层在参数量和表达能力之间取得了更好的平衡。32和16这两个数字不是拍脑袋来的,它们大致遵循“逐层减半”的原则,让网络在浅层提取更丰富的特征,在深层做更抽象的压缩。
激活函数选ReLU而不是Sigmoid或Tanh,原因很实际:ReLU的梯度在正区间恒为1,不会出现梯度消失。Sigmoid在输入较大或较小时梯度接近0,深层网络很难训练。Tanh比Sigmoid好一些,但仍然有饱和问题。ReLU的计算也简单,没有指数运算,训练速度更快。
输出层用线性激活,因为这是一个回归问题,输出值可以是任意实数。如果用Sigmoid,输出被限制在[0,1],反归一化后范围受限,拟合效果会打折扣。
3.3 损失函数与优化器的搭配
损失函数用均方误差(MSE),这是回归问题的标准选择。MSE对大误差的惩罚更重,适合这个项目,因为均线的极端偏差比小偏差更值得关注。
优化器选Adam,学习率设0.001。Adam结合了动量和自适应学习率的优点,对初始学习率不敏感,适合大多数场景。我试过SGD和RMSprop,SGD收敛太慢,RMSprop在后期loss震荡比Adam大。Adam在1000个epoch内基本能收敛到比较稳定的状态。
注意:学习率不是越小越好。0.0001的学习率虽然稳定,但收敛太慢,可能需要上万次迭代。0.01的学习率可能震荡。0.001是一个比较稳妥的起点,如果loss下降太慢,可以尝试0.005;如果震荡严重,降到0.0005。
4. 训练过程:从loss曲线看网络到底学到了什么
4.1 训练集与验证集的划分
时间序列数据的划分不能随机打乱,因为打乱会破坏时间顺序,导致“未来信息泄露”。正确的做法是按时间顺序切分:前70%做训练,中间15%做验证,最后15%做测试。
验证集的作用是监控过拟合。如果训练loss持续下降但验证loss开始上升,说明网络开始记住训练集的噪声,而不是学习通用模式。这时候应该早停,或者增加正则化。
我见过有人把验证集和测试集混用,用测试集调参,最后报告的结果虚高。这是评估的大忌。测试集只能在最后用一次,用来报告最终性能。
4.2 批次大小与训练轮数的权衡
批次大小(batch size)影响梯度估计的噪声和训练速度。批次太小,梯度噪声大,loss震荡;批次太大,梯度估计准确但更新次数少,收敛慢。对于这个项目,数据量不大,批次大小设32或64比较合适。
训练轮数(epoch)不是越多越好。我通常设一个较大的上限(比如2000),然后配合早停策略:如果验证loss在连续50个epoch内没有改善,就停止训练。这样既不会欠拟合,也不会浪费时间。
实测下来,这个网络在500到800个epoch之间基本收敛。loss曲线会呈现先快速下降、然后缓慢下降、最后趋于平稳的形态。如果loss曲线一直不下降,检查学习率是否太小、归一化是否正确、数据是否有问题。
4.3 过拟合的识别与应对
过拟合的典型表现是:训练loss远低于验证loss,且验证loss开始上升。应对方法有几种:增加Dropout层、加L2正则化、减少网络参数量、增加训练数据。
Dropout层在全连接网络中很有效,通常加在隐藏层之后,丢弃率设0.2到0.5。我一般从0.3开始试。L2正则化通过惩罚大权重来限制模型复杂度,系数设0.001到0.01。如果数据量足够,增加数据永远是最好的正则化手段。
但要注意,这个项目的目标是拟合均线,而均线本身是平滑的。如果网络过度拟合了价格的短期波动,反而会偏离均线。所以适度的正则化不仅防止过拟合,还能让输出更平滑,更接近均线的形态。
5. 结果分析与调参经验
5.1 拟合效果的可视化评估
训练完成后,把网络输出和真实均线画在同一张图上,是最直观的评估方式。如果两条线基本重合,说明拟合效果好;如果网络输出滞后于真实均线,说明窗口长度可能太大;如果网络输出震荡剧烈,说明正则化不够或者学习率太大。
我通常会画三张图:训练集拟合图、验证集拟合图、测试集拟合图。训练集拟合好是预期内的,验证集和测试集的拟合效果才是真正衡量泛化能力的指标。如果验证集拟合明显差于训练集,说明过拟合了。
除了可视化,还可以计算一些量化指标:MSE、MAE、R²。R²越接近1越好,但要注意,均线的R²天然会比较高,因为均线本身平滑,预测难度低。所以R²高不代表模型厉害,只是说明任务本身不难。
5.2 窗口长度对拟合效果的影响
窗口长度N是影响拟合效果最直接的超参数。N太小,网络看不到足够的趋势信息,输出会紧跟价格波动,偏离均线;N太大,网络输入维度高,训练慢,而且可能学到太多噪声。
我做过一组对比实验:N=5时,网络输出和均线的相关性只有0.85左右;N=10时,相关性提升到0.92;N=20时,相关性0.94;N=30时,相关性反而降到0.91。这说明N=20左右是一个拐点,再增大窗口,收益递减甚至为负。
这个拐点和均线的计算窗口有关。如果均线是10日均线,那么输入窗口N=20大致对应两个均线周期,网络有足够的信息去推断均线值。如果N远大于均线周期,网络需要处理的信息太多,反而容易分心。
5.3 学习率与批次大小的组合调参
学习率和批次大小是联合起作用的。同样的学习率,批次大小不同,实际更新步长也不同。一般来说,批次大小翻倍,学习率也可以适当增大,但不要线性放大。
我的调参策略是:先固定批次大小为32,用学习率0.001跑一轮,看loss曲线。如果下降太慢,尝试0.005;如果震荡,尝试0.0005。确定学习率后,再调整批次大小。批次大小从32增加到64或128,观察训练速度和最终loss的变化。
实测下来,这个项目对学习率和批次大小不算特别敏感。只要在合理范围内(学习率0.0001到0.01,批次大小16到128),最终loss的差异不会超过10%。所以不用在这上面花太多时间,把精力放在数据质量和窗口长度上,收益更大。
6. 常见问题与排查技巧实录
6.1 loss不下降或下降极慢
这是最常见的问题。排查顺序如下:先检查数据归一化是否正确,如果输入数据范围很大,网络很难收敛;再检查学习率是否太小,尝试增大10倍;然后检查网络结构是否太简单,增加一层或增加神经元数量;最后检查激活函数是否合适,ReLU通常比Sigmoid好。
还有一个容易被忽略的原因:输出层的激活函数。如果输出层用了Sigmoid,输出被限制在[0,1],而真实值归一化后可能不在这个范围,loss自然降不下去。回归问题的输出层一定要用线性激活。
6.2 loss震荡严重
loss震荡通常和学习率太大、批次太小有关。先尝试降低学习率,比如从0.01降到0.001。如果还震荡,增大批次大小。另外,检查数据中是否有异常值,异常值会导致梯度突变,引起loss震荡。
如果震荡发生在训练后期,可能是网络已经接近最优,但学习率相对当前loss曲面来说仍然太大。这时候可以用学习率衰减策略,比如每100个epoch把学习率乘以0.9。
6.3 验证loss先降后升
这是典型的过拟合信号。应对方法按优先级排序:增加Dropout层、增加L2正则化、减少网络参数量、增加训练数据、早停。
但要注意,如果验证loss上升的幅度很小,而且训练loss还在下降,可能只是正常波动。我通常会在验证loss连续上升超过20个epoch后才采取行动。
6.4 网络输出滞后于真实均线
这个问题在时间序列拟合中很常见。原因是网络倾向于用当前输入窗口的“平均”来预测输出,而均线本身也有滞后性,两者叠加导致输出滞后更明显。
解决方法有几种:在输入中加入差分特征,比如价格的一阶差分,让网络看到变化趋势;增大窗口长度,让网络看到更长的历史;在损失函数中加入对滞后的惩罚,比如用Huber损失代替MSE。
我试过在输入中加入一阶差分,滞后问题有明显改善,但网络训练难度也增加了。如果只是做教学演示,不加差分也能接受,毕竟均线本身就有滞后,网络输出滞后一点不算大问题。
6.5 常见问题速查表
| 问题现象 | 可能原因 | 排查顺序 | 解决方法 |
|---|---|---|---|
| loss不下降 | 归一化错误、学习率太小、网络太简单 | 数据→学习率→结构 | 重新归一化、增大学习率、增加层数 |
| loss震荡 | 学习率太大、批次太小、异常值 | 学习率→批次→数据 | 降低学习率、增大批次、剔除异常值 |
| 验证loss上升 | 过拟合 | 正则化→数据量 | 加Dropout、加L2、早停 |
| 输出滞后 | 窗口太小、缺少差分特征 | 窗口→特征 | 增大窗口、加入一阶差分 |
| 输出震荡 | 正则化不够、学习率太大 | 正则化→学习率 | 加Dropout、降低学习率 |
提示:排查问题时,每次只改一个参数,观察loss曲线的变化。同时改多个参数,你无法判断是哪个参数起了作用。
7. 几个容易被忽略的实操细节
7.1 随机种子的设置
TensorFlow的随机种子如果不设置,每次运行的结果都不一样。对于调试和对比实验,这是灾难性的。设置随机种子的方法很简单,在导入TensorFlow之后、定义网络之前,调用tf.random.set_seed(42)。注意,这个种子只影响TensorFlow的随机操作,Python的random和numpy的random需要单独设置。
设置种子后,每次运行的结果可复现,调参时才能准确判断某个改动是否真的有效。我见过有人不设种子,调了半天参数,最后发现效果变化只是随机波动。
7.2 模型保存与加载
训练好的模型应该保存下来,方便后续使用。TensorFlow提供了model.save()和model.load_model(),可以保存整个模型结构、权重和优化器状态。保存格式推荐用SavedModel,这是TensorFlow的标准格式,跨平台兼容性好。
保存模型时要注意,归一化的参数(均值和标准差)也要一起保存。如果只保存模型权重,加载后不知道用什么参数归一化新数据,模型就没法用了。我的做法是把归一化参数存成一个JSON文件,和模型放在同一个目录下。
7.3 训练日志的记录
训练过程中的loss、验证loss、学习率等指标应该记录下来,方便后续分析。最简单的方式是用CSV文件,每个epoch写一行。TensorFlow也提供了TensorBoard,可以可视化loss曲线、网络结构、权重分布等。
我个人的习惯是两者都用:CSV文件用于快速查看和对比,TensorBoard用于深入分析。TensorBoard的启动命令是tensorboard --logdir=./logs,然后在浏览器中打开对应地址即可。
7.4 代码结构的组织
这个项目虽然不大,但代码结构清晰与否直接影响调试效率。我推荐的结构是:数据加载与预处理一个模块,网络定义一个模块,训练循环一个模块,评估与可视化一个模块。每个模块用函数封装,主程序只负责调用。
这样组织的好处是,调参时只需要改配置参数,不用动核心逻辑。比如窗口长度、学习率、批次大小这些参数,统一放在一个配置字典里,改起来方便,也方便做对比实验。
8. 从拟合均线延伸到更实用的场景
8.1 多均线同时拟合
单条均线的拟合跑通之后,可以尝试同时拟合多条均线,比如5日、10日、20日均线。网络结构需要调整:输出层从1个神经元变成3个神经元,损失函数变成三个MSE的加权和。
这个扩展的价值在于,网络需要学习不同时间尺度上的价格模式。5日均线对短期波动敏感,20日均线对长期趋势敏感。同时拟合多条均线,相当于让网络同时关注多个时间尺度,这对理解价格序列的多尺度特性很有帮助。
8.2 加入技术指标作为输入
除了原始价格,还可以加入技术指标作为输入特征,比如RSI、MACD、布林带宽度等。这些指标已经从价格序列中提取了特定信息,网络不需要从零开始学习,训练会更快,效果也可能更好。
但要注意,技术指标的计算本身有参数(比如RSI的周期),这些参数的选择会影响指标的值。如果指标参数和均线周期不匹配,反而可能引入噪声。我的建议是,先用原始价格跑通基线,再逐步加入指标,观察效果变化。
8.3 从拟合到预测的跨越
拟合均线和预测未来价格是两回事。拟合是“给定历史价格,输出当前均线值”,预测是“给定历史价格,输出未来价格”。后者难度大得多,因为未来价格包含大量不可预测的随机成分。
如果要从拟合过渡到预测,第一步是把输出从“当前均线值”改成“未来某天的均线值”。这个改动看似简单,但意味着网络需要外推,而不是插值。外推的误差通常比插值大一个量级。第二步是加入更多特征,比如成交量、市场情绪指标等,帮助网络捕捉更多信息。第三步是重新评估模型,预测任务的评估指标和拟合任务不同,更关注方向准确率而不是MSE。
我个人的经验是,拟合任务适合入门和教学,预测任务适合有一定经验后挑战。不要指望一个简单的全连接网络能在预测任务上表现很好,那需要更复杂的模型和更多的数据。
8.4 用这个项目练手的正确姿势
这个项目最大的价值不是拟合出一条均线,而是让你完整走一遍“数据准备→网络设计→训练调参→结果分析”的流程。这个流程在几乎所有机器学习项目中都是通用的。
我的建议是,不要只跑一遍就结束。跑通之后,尝试改一个参数,观察结果变化。比如把窗口长度从10改成20,看看loss曲线有什么不同;把隐藏层神经元从32改成64,看看训练时间增加多少;把优化器从Adam改成SGD,看看收敛速度差多少。这种对比实验做多了,你对超参数的感觉就出来了,这比看任何教程都管用。
最后再分享一个小技巧:把每次实验的配置和结果记在一个表格里,包括窗口长度、学习率、批次大小、最终loss、训练时间。积累几十次实验后,你就能看出哪些参数组合效果好,哪些组合效果差。这个表格是你自己的经验库,比任何现成的调参指南都靠谱。