news 2026/10/27 17:25:27

TensorFlow实战:用全连接神经网络拟合股票收盘均价

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TensorFlow实战:用全连接神经网络拟合股票收盘均价

1. 从一条均线说起:这个项目到底在做什么

股票收盘均价这条线,看起来平平无奇,但真拿它当神经网络的拟合目标,你会发现里面藏着不少门道。这个项目的核心思路很直接:用 TensorFlow 搭建一个最基础的前馈神经网络,输入是历史价格序列(或者时间窗口内的价格特征),输出是对收盘均价的逼近值。说白了,就是让网络学会“画”出一条跟真实均价走势高度重合的曲线。

它解决的问题不是预测明天涨跌——那是另一回事——而是验证一个更底层的能力:神经网络能不能从带噪声的、非平稳的价格序列里,提取出平滑的趋势信号。这件事在量化研究的早期阶段特别有价值,因为如果你连均线都拟合不好,后面做多因子、做时序建模基本是空中楼阁。

适合谁来参考?三类人:第一类是在学 TensorFlow 基础操作、想找一个比 MNIST 更有“业务感”的练手项目的人;第二类是做量化策略原型、需要快速验证某个网络结构对价格序列拟合能力的人;第三类是对“神经网络到底在拟合什么”这件事有困惑、想通过可视化手段看清楚的开发者。这篇文章会把整个实现过程拆开,从数据构造、网络设计、训练细节到结果诊断,一步步讲清楚,并且补充很多原始教程里不会提的坑。

2. 整体设计思路与方案选型

2.1 为什么用回归而不是分类

很多人做股票相关的神经网络项目,第一反应是做成涨跌分类。但这个项目的目标是“逼近收盘均价”,注意“逼近”两个字,它天然是一个回归问题。均线本身是连续值,你用分类去做,要么把连续值离散化成区间导致精度丢失,要么用 softmax 输出概率再映射回数值,绕了一大圈还不讨好。

回归的损失函数直接用均方误差(MSE),梯度信号干净,收敛过程也容易观察。我在实际跑的时候发现,MSE 的下降曲线跟拟合曲线的贴合程度有很强的对应关系——MSE 降到某个量级之后,曲线基本就贴上了。这种直观的反馈对初学者特别友好。

2.2 网络结构为什么选全连接而不是 LSTM

标题里说的是“基本操作”,所以这里刻意选了最朴素的全连接网络(Dense Layer)。LSTM 当然更适合时序数据,但它引入的门控机制会让初学者搞不清楚到底是网络在拟合,还是记忆单元在起作用。用全连接网络,输入窗口固定,比如用前 N 天的价格特征预测当前均价,结构简单,每一层的权重都能拿出来分析。

具体结构我建议是这样的:输入层节点数等于窗口大小乘以特征数,中间一到两层隐藏层,每层用 ReLU 激活,输出层一个节点、不加激活函数(回归任务的标准做法)。隐藏层节点数不用太多,64 到 128 之间就够了,再多容易过拟合,再少拟合能力不足。

2.3 数据预处理的取舍

价格序列的预处理有几个关键决策。第一,要不要做归一化?必须做。原始价格可能在几十到几百之间波动,直接喂给网络会导致梯度爆炸或者收敛极慢。我通常用 Min-Max 归一化把价格缩放到 [0,1] 区间,或者用 Z-Score 标准化。Min-Max 的好处是保留了原始分布的相对关系,坏处是对异常值敏感。股票数据里偶尔出现极端价格,这时候 Z-Score 更稳。

第二,窗口怎么选?窗口太小,网络看不到趋势;窗口太大,参数量暴涨且容易过拟合。根据我的经验,对于日线级别的收盘均价拟合,窗口取 10 到 20 个交易日比较合适。这个范围既能捕捉短期趋势,又不会让输入维度失控。

第三,训练集和测试集的划分不能用随机打乱。时序数据必须按时间顺序切分,否则会出现“用未来数据预测过去”的泄漏问题。我一般按 8:2 或者 7:3 切,前面做训练,后面做验证。

3. 核心细节解析与实操要点

3.1 数据构造:从原始价格到训练样本

假设你手里有一列收盘价,长度是 T。构造样本的逻辑是滑动窗口:对于每一个时间点 t(从窗口大小开始),取 [t-w, t-1] 这段价格作为输入特征,取 t 时刻的收盘均价作为标签。收盘均价可以定义为过去 m 天收盘价的简单移动平均,m 可以跟窗口大小一致,也可以单独设定。

这里有个细节很多人会忽略:均线的计算本身就用到了历史价格,而你的输入窗口也是历史价格,两者存在信息重叠。这会导致网络学到一个“捷径”——它可能直接对输入做加权平均就输出了,根本没有学到什么非线性映射。如果你发现训练损失下降得特别快、权重看起来像均匀分布,大概率就是这个问题。

解决办法有两个:一是让均线的周期大于输入窗口,比如窗口取 10 天,均线取 20 天,这样网络必须学会从局部信息推断更长期的趋势;二是在输入里加入一些非线性特征,比如收益率的绝对值、成交量变化等,迫使网络做更复杂的变换。

3.2 网络搭建中的参数选择

用 TensorFlow 的 Keras API 搭建这个网络,代码大概长这样:

import tensorflow as tf model = tf.keras.Sequential([ tf.keras.layers.Dense(64, activation='relu', input_shape=(window_size,)), tf.keras.layers.Dense(32, activation='relu'), tf.keras.layers.Dense(1) ])

隐藏层用 ReLU 是常规操作,但如果你发现训练过程中出现大量神经元“死亡”(输出恒为 0),可以换成 LeakyReLU。输出层不加激活函数,因为回归任务的输出范围没有限制。

优化器选 Adam,学习率从 0.001 开始试。Adam 的自适应学习率在回归任务上表现通常比 SGD 稳定,尤其是数据量不大的时候。损失函数用 MSE,评估指标可以加上 MAE(平均绝对误差),因为 MAE 的数值更直观,跟价格在同一量纲上。

3.3 训练过程中的监控要点

训练的时候不要只盯着 loss 看,要同时观察训练集和验证集的 loss 曲线。如果训练 loss 持续下降但验证 loss 开始上升,说明过拟合了,这时候要么加 Dropout,要么减少网络容量,要么增加数据量。

另一个要监控的是预测值的分布。我习惯在每个 epoch 结束后,抽一批验证数据看看预测值和真实值的散点图。如果散点集中在 45 度线附近,说明拟合得不错;如果散点分散或者有系统性偏移,说明网络还有改进空间。

注意:股票数据是非平稳的,训练集上的统计特性在测试集上可能发生变化。所以验证 loss 比训练 loss 高是正常的,但如果高出一个数量级,就要检查是不是数据分布漂移太严重。

4. 完整实操流程与关键环节实现

4.1 环境准备与数据加载

先确保 TensorFlow 版本在 2.x 以上,因为 Keras 已经集成在主包里了。数据加载部分,假设你有一个 CSV 文件,包含日期和收盘价两列。用 pandas 读进来之后,先检查有没有缺失值,有的话用前向填充处理。

import pandas as pd import numpy as np df = pd.read_csv('price_data.csv') df['close'] = df['close'].ffill() prices = df['close'].values

然后计算收盘均价。这里我用 20 日简单移动平均作为目标:

ma_period = 20 ma = pd.Series(prices).rolling(window=ma_period).mean().values

注意前 19 个值是 NaN,后面构造样本的时候要跳过。

4.2 归一化与样本构造

归一化用 Min-Max,但要注意:归一化的参数(最小值和最大值)只能从训练集计算,然后应用到验证集。如果从全量数据计算,就引入了未来信息。

train_size = int(len(prices) * 0.8) train_prices = prices[:train_size] min_val = train_prices.min() max_val = train_prices.max() def normalize(x): return (x - min_val) / (max_val - min_val)

构造样本的时候,窗口大小取 10,均线周期取 20。因为均线前 19 个是 NaN,所以有效样本从第 20 个开始。

window_size = 10 X, y = [], [] for i in range(ma_period, len(prices)): if np.isnan(ma[i]): continue x_window = prices[i-window_size:i] X.append(normalize(x_window)) y.append(normalize(ma[i])) X = np.array(X) y = np.array(y)

4.3 模型训练与回调设置

训练的时候加上 EarlyStopping 回调,防止过拟合。patience 设 10 到 20 个 epoch,意思是验证 loss 连续这么多轮不下降就停止训练。

callback = tf.keras.callbacks.EarlyStopping( monitor='val_loss', patience=15, restore_best_weights=True ) history = model.fit( X_train, y_train, validation_data=(X_val, y_val), epochs=200, batch_size=32, callbacks=[callback], verbose=1 )

batch_size 取 32 是个比较稳妥的默认值。数据量小的时候可以降到 16,数据量大可以升到 64 或 128。学习率如果发现 loss 震荡厉害,可以降到 0.0005。

4.4 结果可视化与诊断

训练完之后,把预测值反归一化,跟真实均线画在一起对比。我一般会画三张图:第一张是整体走势对比,第二张是局部放大(比如最近 100 个点),第三张是残差分布直方图。

残差如果近似正态分布且均值接近 0,说明网络没有系统性偏差。如果残差有明显的自相关(比如连续多天同号),说明网络没有捕捉到某些时序模式,这时候可以考虑加入更多特征或者换用时序模型。

5. 常见问题与排查技巧实录

5.1 损失不下降或者下降极慢

这是最常见的问题。排查顺序是这样的:先检查数据归一化有没有做对,如果输入值范围在几百,网络根本学不动;再检查学习率是不是太小,可以试着放大 10 倍看看;然后检查标签有没有构造错,比如窗口和标签错位了。

还有一个隐蔽的原因:如果均线周期和窗口大小完全一样,网络可能陷入一个“平凡解”——直接输出输入的均值。这时候损失会下降到一个平台期就卡住。解决办法是让均线周期大于窗口,或者加入额外特征。

5.2 过拟合:训练集拟合很好但验证集很差

过拟合在股票数据上特别容易出现,因为噪声大、有效信号少。应对手段按优先级排列:第一,增加 Dropout 层,rate 取 0.2 到 0.5;第二,减小网络容量,比如隐藏层节点从 128 降到 32;第三,增加训练数据量(如果可能的话);第四,加 L2 正则化。

我个人的经验是,Dropout 加 EarlyStopping 的组合能解决大部分过拟合问题。如果还不行,说明这个网络结构对这个数据集就是不适合,别硬调。

5.3 预测曲线滞后于真实曲线

如果你发现预测的均线总是比真实均线“慢半拍”,这是因为网络学到的是一个平滑映射,而均线本身也有滞后性,两者叠加导致滞后更明显。这个问题没有完美的解法,但可以通过以下方式缓解:在输入里加入动量特征(比如价格的变化率),或者把预测目标改成均线的差分而不是均线本身。

5.4 常见问题速查表

问题现象可能原因排查方法解决手段
损失不下降未归一化 / 学习率过小检查输入范围 / 试大学习率归一化 / 调学习率
验证损失上升过拟合对比训练验证曲线Dropout / 减容量
预测滞后目标本身滞后观察残差自相关加动量特征 / 改差分目标
输出恒定值神经元死亡检查激活值分布换 LeakyReLU
训练极慢batch_size 太小看每 epoch 耗时增大 batch_size

实操心得:每次调整只改一个参数,改完记录结果。我见过太多人一次改三四个地方,最后不知道是哪个起了作用。建立一个实验记录表,把学习率、网络结构、窗口大小、均线周期都记下来,跑上十几组之后你就能摸到这个数据集的最优区间。

6. 进阶扩展与个人经验补充

6.1 从单特征到多特征

只用收盘价做输入,信息量其实很有限。你可以加入开盘价、最高价、最低价、成交量等特征,输入维度变成 window_size 乘以特征数。但要注意,特征多了之后归一化要分列做,不能整体归一化,否则量纲差异会掩盖小量纲特征的作用。

加入多特征之后,网络参数量会增加,过拟合风险也上升。这时候 Dropout 和正则化就更必要了。我试过加入成交量之后,验证 loss 大概能降 10% 到 15%,但训练时间翻倍,需要权衡。

6.2 用不同均线周期做多任务学习

与其只拟合一条均线,不如让网络同时输出 5 日、10 日、20 日三条均线的逼近值。这样网络必须学到更丰富的趋势表示,而不是只盯着一个平滑目标。多任务学习的实现很简单,输出层改成三个节点,损失函数用三个 MSE 的加权和。

这种做法的好处是,网络学到的特征表示更通用,后续如果要做其他任务(比如波动率估计),可以直接复用前面的层。坏处是训练难度增加,需要调权重。

6.3 关于评估指标的补充

MSE 和 MAE 是最基础的指标,但它们对极端值敏感。股票数据里偶尔出现的大幅波动会主导损失值,导致网络忽略常态区间。这时候可以用 Huber Loss,它在误差小的时候表现为 MSE,误差大的时候表现为 MAE,兼顾了稳定性和鲁棒性。

另外,我建议在评估的时候加上 R 平方(决定系数),它衡量的是模型解释了目标变量多少比例的方差。R 平方在 0.8 以上说明拟合得不错,低于 0.5 就要反思网络结构或者特征选择了。

6.4 一个容易被忽略的细节:随机种子

TensorFlow 的权重初始化、Dropout、数据打乱都涉及随机性。如果你不做任何设置,每次跑出来的结果都不一样,根本没法对比实验。在代码开头加上这几行:

import tensorflow as tf import numpy as np import random seed = 42 tf.random.set_seed(seed) np.random.seed(seed) random.seed(seed)

这样至少能保证同一台机器上结果可复现。但要注意,不同 TensorFlow 版本之间即使设了种子,结果也可能有差异,所以实验记录里要写清楚版本号。

6.5 关于这个项目的后续方向

如果你已经跑通了基础版本,下一步可以尝试几个方向。一是把全连接换成一维卷积(Conv1D),卷积核在时间轴上滑动,能更好地捕捉局部模式;二是加入注意力机制,让网络自己决定哪些时间点更重要;三是把单步预测改成多步预测,一次性输出未来几天的均线值。

但不管怎么扩展,基础版本的价值在于它建立了一个可对比的基线。你后面做的任何改进,都要跟这个基线比,看验证 loss 有没有实质下降。没有基线,改进就是盲目的。

我个人在这个项目上踩过最大的坑,是一开始没有做归一化,网络跑了 500 个 epoch 损失还在 0.3 左右晃。后来加上 Min-Max 归一化,20 个 epoch 就降到 0.01 以下。所以如果你现在正卡在损失不下降的阶段,先去检查数据预处理,十有八九问题出在那里。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 14:36:35

离线知识问答系统实战:企业文档解析与RAG检索生成

你的公司里是不是也有这么一批文件——扫描存档的合同、供应商发来的PDF手册、几个部门攒了五年的制度文档,占着共享文件夹几个G的空间,真到用的时候谁也找不到里面写了什么。我做过一个项目,把这一堆“沉睡资产”捞了出来,接上本…

作者头像 李华
网站建设 2026/10/21 10:45:20

从零搭建轻量级地图可视化组件lingbot-map:设计、实现与性能优化

1. 从零搭建一个轻量级地图可视化组件:lingbot-map 项目全记录 前阵子在做数据看板的时候,产品那边提了个需求:要在一个页面里同时展示设备分布、区域热度和轨迹回放,而且要求加载快、能离线跑、还得方便嵌入到已有的前端工程里。…

作者头像 李华
网站建设 2026/10/23 5:06:44

航天器动力学建模与制导算法实践

1. 神话与科技的千年对话 人类仰望星空的历史,就是一部不断打破认知边界的历史。从嫦娥奔月的传说到万户飞天的尝试,再到现代航天器的精准入轨,我们正在用代码重写那些曾经被视为神迹的幻想。在航天控制中心,我常看到老一辈工程师…

作者头像 李华
网站建设 2026/10/17 12:29:04

lingbot-map 地图可视化项目实战:坐标系、图层管理与性能优化

1. 从零拆解 lingbot-map:一个地图可视化项目到底在解决什么问题第一次看到lingbot-map这个项目名,我的直觉是:这大概率是一个围绕“地图渲染 机器人/智能体交互”方向做的小型可视化工具。名字里带map,说明核心对象是地图&#…

作者头像 李华
网站建设 2026/10/22 14:19:02

AnyPS5跨平台抽象层设计:统一文件、线程与网络接口的工程实践

1. 项目缘起与核心定位 AnyPS5 这个名字第一次出现在我视野里的时候,我正被一堆跨平台构建脚本折腾得焦头烂额。当时团队里有个需求,要把一套图像处理管线同时跑在桌面端、移动端和浏览器里,每换一个目标平台就得重写一遍胶水代码&#xff0c…

作者头像 李华