简介:面向Python与TensorFlow股票预测学习者,这份压缩包围绕CNN与DQN深度学习模型预测股票走势展开,提供完整可运行的代码工程与教学配套,适合具备Python基础并希望深入实践深度学习的开发者。包内共34个文件,总大小5.16MB,以6个py脚本、5个ipynb笔记本和8张PNG结果图为主体,配套2份PDF说明文档,并附带数据文件、模型checkpoint与pyc缓存,方便直接复现数据预处理、模型训练与预测过程。已有1440人浏览学习,适合对时序预测、强化学习及指标可视化感兴趣的Python开发者。资源按CNN股票分类、DQN图像预测、KD价值分析等模块组织,覆盖数据预处理、模型搭建、训练评估与绘图展示全流程,notebook与脚本相互对照,可快速上手修改参数并重新训练,结合PDF大纲还能进一步掌握分布式TensorFlow与股票预测的整体思路。
1. TensorFlow CNN预测股票走势:从卷积核到K线的落地拆解
把卷积神经网络用在股票预测上,第一眼看上去像玄学——CNN不是用来识别猫狗图片的吗?但看完这套代码包你会发现,卷积的局部感受野恰好能捕捉连续几天K线组合形成的小形态,比如三连阳、长上影、量价背离。包里的内容分两条线:一条用CNN直接回归收盘价和KD值,一条把DQN和CNN组合到年线判断上。适合已经会调pandas、想往深度学习+量化方向踩第一脚的人。这篇文章会把数据预处理、模型参数、三个预测场景的代码逻辑和踩坑点拆开来讲。
2. 数据预处理与滑动窗口:把时间序列变成CNN能识别的二维输入
2.1 为什么CNN能用在股票时间序列上
很多人一听到"CNN预测股票"就觉得是噱头,其实CNN的本质是局部特征提取器。一张图片里,边缘、纹理这些局部模式组合成语义;一段K线里,连续几天的开盘、收盘、高低价组合,同样构成了有意义的局部模式。比如"连续三天收盘价逐步抬高且量能放大"这种形态,跟图像里的边缘特征在数学表达上是一致的——都是局部窗口内的数值关系。
这个项目里同时走了Conv1D和Conv2D两条路。Conv1D直接作用在价格序列上,每个卷积核扫过一个长度为window_size的窗口,提取出这个窗口内的价格形态;Conv2D则先把行情数据渲染成图片,再用图像卷积的思路去识别K线图上的视觉模式。后者在DQN_img_closePrice.py和DQN_kd_pic.py里体现得很明显,文件名里的"img"和"pic"就是证据。这种"把行情画成图再喂给卷积网络"的做法,在量化圈里并不少见,核心动机是让CNN在它最擅长的图像领域发挥优势。
但不管走哪条路,前提都是数据要整理成CNN能吃的样子。CNN不关心你输入的是不是价格,它只关心张量形状。所以第一步永远是数据处理。
2.2 滑动窗口构造样本:窗口长度、步长与标签对齐
时间序列预测和普通机器学习任务最大的区别在于样本构造方式。你不能像分类猫狗那样把每张图独立打乱,股票数据的每个样本天然带有时间顺序。常见做法是滑动窗口——用过去N天的数据预测下一天,窗口不断往后移,生成成百上千个样本。这个项目的数据预处理走的也是这个路子。
import numpy as np import pandas as pd def build_window_dataset(df, feature_cols, target_col='close', window_size=10, step=1): X, y = [], [] df = df.reset_index(drop=True) for i in range(window_size, len(df), step): X.append(df[feature_cols].iloc[i-window_size:i].values) y.append(df[target_col].iloc[i]) return np.array(X), np.array(y) # 假设df已经按日期排好序,包含open/high/low/close/volume列 feature_cols = ['open', 'high', 'low', 'close', 'volume'] X, y = build_window_dataset(df, feature_cols, target_col='close', window_size=10, step=1) print(X.shape) # (样本数, 10, 5) print(y.shape) # (样本数,)这段代码的核心逻辑是:从第window_size行开始往后遍历,每次取前10行数据作为特征,当前行收盘价作为标签。step控制滑动步长,设为1表示每天都生成一个样本;如果想让训练样本更稀疏、降低相关性,可以调成2或3。X的形状是(样本数, window_size, 特征数),这个三维张量正好是Conv1D的输入格式。
有几个细节要留意。第一,df必须按时间升序排好,否则窗口切出来的数据是乱的;第二,target_col既可以是收盘价本身,也可以是未来第N天的价格,取决于你想预测"明天"还是"未来一周";第三,如果你想预测涨跌方向而不是具体价格,y应该改成np.where(df['close'].shift(-1) > df['close'], 1, 0)这样的二值标签。
2.3 数据标准化与训练集/验证集划分:防止未来数据泄漏
标准化在股票预测里是个容易翻车的环节,尤其是用StandardScaler或MinMaxScaler时,稍不注意就会把验证集信息提前泄漏到训练集里。正确的做法是:先切分数据,再在训练集上fit标准化器,然后用同一个标准化器去transform验证集和测试集。
from sklearn.preprocessing import StandardScaler train_size = int(len(X) * 0.7) val_size = int(len(X) * 0.15) X_train, y_train = X[:train_size], y[:train_size] X_val, y_val = X[train_size:train_size+val_size], y[train_size:train_size+val_size] X_test, y_test = X[train_size+val_size:], y[train_size+val_size:] # 注意:scaler按特征维度fit,需要先reshape成2D scaler = StandardScaler() X_train_2d = X_train.reshape(-1, X_train.shape[-1]) scaler.fit(X_train_2d) def transform_X(arr): shape = arr.shape return scaler.transform(arr.reshape(-1, shape[-1])).reshape(shape) X_train = transform_X(X_train) X_val = transform_X(X_val) X_test = transform_X(X_test)这里容易犯两个错。第一个是把所有数据混在一起fit_scaler,导致验证集的最大值、均值参与了一训练集的归一化计算,模型在评估时看到的分布是"被剧透过"的,验证集指标虚高。第二个是随机打乱样本——train_test_split默认会洗牌,这在时间序列场景下会破坏时间连续性,模型等于"偷看"了未来数据。正确做法是直接按时间顺序切片,像我上面这样。
标准化之后,还需要把标签也保存好原始尺度。因为模型预测出来的是标准化后的价格,要还原成真实价格才能画图、算收益。这个"反标准化"的步骤需要保留 scaler 的mean_和scale_属性,或者干脆用inverse_transform。项目里closePrice_rst.PNG那些结果图,就是预测完反标准化之后画出来的。
3. 搭建CNN回归模型:从Sequential到卷积核参数的完整配置
3.1 Conv1D和Conv2D怎么选:先看输入数据的形状
项目代码包里CNN_Classifier.ipynb用的是分类思路,而DQN_img_closePrice.py则把数据渲染成了图片。这意味着项目本身对两种输入格式都有尝试。选型逻辑其实很直接:如果你的X是(样本数, 窗口长度, 特征数),用Conv1D;如果你把每天的行情画成了(高度, 宽度, 通道数)的图片,用Conv2D。
Conv1D的优点是结构简单、训练快,对纯时间序列数据来说是更自然的选择。Conv2D的优势在于能捕捉K线图上的空间形态——比如影线长度、阳线阴线的视觉比例,这些在像素层面有体现。但代价是数据渲染过程引入了额外的主观性:图片怎么画、像素多大、颜色通道怎么编码,都会影响模型效果。我一般建议先从Conv1D起步,把预测流程打通之后,再考虑Conv2D做对比实验。
3.2 模型骨架:卷积核数量、池化窗口、dropout怎么配
这个场景下模型深度不用太夸张,因为输入本身只有10到20个时间步,总共就几十个特征维度。堆太深反而容易过拟合。下面这个结构是我按这套代码的常见风格搭的一个基线模型,适用于收盘价回归任务。
import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv1D, MaxPooling1D, Flatten, Dense, Dropout, BatchNormalization model = Sequential([ Conv1D(filters=64, kernel_size=3, activation='relu', padding='same', input_shape=(10, 5)), BatchNormalization(), MaxPooling1D(pool_size=2), Conv1D(filters=32, kernel_size=3, activation='relu', padding='same'), BatchNormalization(), MaxPooling1D(pool_size=2), Flatten(), Dense(64, activation='relu'), Dropout(0.3), Dense(1) # 回归任务,输出层不加激活函数 ]) model.compile(optimizer='adam', loss='mse', metrics=['mae']) model.summary()Conv1D的第一个参数filters=64是卷积核数量,可以理解成模型想从窗口里提取64种不同的局部模式。kernel_size=3表示每个卷积核看3个连续时间步的组合,这是时间序列里常用的经验值,相当于只观察"三天内的价格关系"。padding='same'保持序列长度不变,避免卷积之后窗口尺寸缩得太快。
BatchNormalization加在卷积层和激活函数之后,作用是稳定训练、加速收敛,尤其当你的数据标准化不够完美时,它能兜底。MaxPooling1D(pool_size=2)把序列长度减半,降低计算量的同时强制模型关注更粗粒度的特征。两个卷积层之后接Flatten把二维特征图拉平,再接两个全连接层。Dropout(0.3)在全连接层之前随机丢弃30%的神经元连接,这是对付过拟合最常见的手段。
最后一层Dense(1)没有加激活函数,因为回归任务希望输出任意实数域的价格值,而不是被sigmoid或者tanh限制在某个范围内。这一点和分类任务不一样——分类的最后一层通常是softmax,而回归就是裸的线性输出。
3.3 编译与训练:损失函数、优化器和训练参数怎么定
回归任务默认用mse(均方误差)作为损失函数,这没问题,但有个隐患:如果价格序列里有极端值(比如某天暴涨暴跌),MSE会给这些样本非常大的梯度权重,导致模型被几个极端样本带偏。稳健一点的替代方案是mae(平均绝对误差),它对离群点没那么敏感。我自己的习惯是loss='mae'、metrics=['mae'],因为股票数据的尾部波动本来就大,没必要让模型拼命拟合那几次极端行情。
优化器直接选adam,初始学习率默认0.001就行。如果你的训练loss波动很大,可以把学习率降到0.0005或者配合ReduceLROnPlateau回调动态调整。训练轮数上,epochs=50配batch_size=64是一个比较稳妥的起点,EarlyStopping 放到监控验证集loss上。
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau callbacks = [ EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True), ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=5, min_lr=1e-6) ] history = model.fit( X_train, y_train, validation_data=(X_val, y_val), epochs=50, batch_size=64, callbacks=callbacks, verbose=1 )EarlyStopping的patience=10表示如果验证集loss连续10个epoch没有改善,就提前终止训练,并自动恢复到验证集最优时的权重。ReduceLROnPlateau在loss停滞时把学习率减半,帮助模型跨过局部极小点。这两个回调组合起来,基本能避免"训练集越训越好、验证集越训越差"的困境。
训练完之后,评估要用测试集做一次彻底的体检:
test_loss, test_mae = model.evaluate(X_test, y_test, verbose=0) print(f"Test MAE: {test_mae:.4f}") pred = model.predict(X_test).flatten()model.predict返回的是标准化尺度下的预测值,别忘了反标准化。这一步很多人会漏,结果画出来的预测曲线和真实价格差了十万八千里。
4. 项目实战走读:收盘价、KD值、年线三条预测线的代码逻辑
4.1 closePrice预测:CNN直接回归收盘价
项目里的DQN_img_closePrice.py和DQN_CNN_image目录盯的都是收盘价。收盘价是股票市场最基础的标签,模型的目标是给出"明天收在多少"或"未来N天收在多少"的预测值。如果你打开DQN_img_closePrice.py,会发现它走的路径是:先取历史行情,把收盘价序列和开高低收四条线画到一张图上,然后CNN对这个图像做特征提取,最后输出一个价格数值。
这个思路比直接用Conv1D更重,但有个隐性优势——图像渲染天然包含了K线的视觉结构,比如阳线实体的长度、上下影线的比例,这些几何信息在纯数值序列里需要通过多列特征组合才能表达。如果你是刚入门,建议先用前面第3章的Conv1D模型跑通收盘价预测,再去看这个文件里图像是怎么渲染的。图像渲染代码的核心就两步:拿到matplotlib的Figure对象,然后canvas.draw()转成numpy数组,作为CNN输入。
用收盘价做回归预测,最直接的评估指标不是MSE,而是方向准确率——预测值和真实值相比,涨跌方向对不对。这个指标在交易里比数值误差更重要:你预测明天收盘价是10.5元,实际是10.4元,数值只差了0.1元,但如果方向判断反了,做多就亏钱。所以后半段要单独设一个阈值来判断方向。
4.2 KD值预测:输出从"价格"换成"随机指标"
DQN_KD_value目录下有一组和KD指标强相关的文件:KD_draw.py、DQN_kd_pic.py。KD指标是随机指标(Stochastic Oscillator),由K线和D线组成,取值在0到100之间,用来衡量当前收盘价在最近一段涨跌区间中的相对位置。KD指标本身不是价格,它是一个技术分析信号,但很多量化策略把它当因子用,所以直接建模预测KD值的人也不少。
用CNN预测KD值,思路和预测收盘价完全一致,只是标签从原始价格换成了KD指标值。需要注意的是KD值本身做过平滑处理,有粘性——今天的K值很大程度上取决于昨天的K值,所以CNN很容易"偷懒",直接记住前一天的值就能把loss压得很低,但这不代表模型真的学到了新的市场模式。做一个baseline对比就明白了:用y_pred = y_shifted_by_one(也就是直接预测"等于前一天值"),如果这个简单策略的误差和CNN差不多,说明模型没有学到额外信息。
预测KD值有一个实际用途:当K值从低位上穿D值时,常被视为买入信号。如果你能提前一到两天预测出这个上穿动作,就有了先手优势。但要把"预测KD数值"转化成"预测上穿事件",需要另外加一层规则判断,而不是直接看数值误差。
4.3 yearline年线判断:引入DQN强化学习思路
项目里最有意思的应该是DQN_yearline_reward.py和DQN_draw_yearline.py。年线通常指250日均线,是判断长期趋势的重要参考线。这个文件没有走标准的监督学习路径,而是引入了DQN(Deep Q-Network)——一种强化学习算法。它不是直接预测"年线上方还是下方"作为标签,而是让模型在模拟环境里做动作(比如买入、持有、卖出),reward是动作带来的收益变化,DQN通过最大化累计收益来学会判断年线趋势。
DQN在这套代码里的角色,可以理解成一个把行情图像当作状态的决策器:状态是当前和历史K线渲染出来的图像,动作是交易决策,奖励是账户收益的变化。saved_year_r目录里的文件应该是训练好的模型权重或者reward曲线数据。这种做法的优点是模型目标更贴近真实交易——它学的不是"预测对错",而是"赚不赚钱";缺点是强化学习训练不稳定,reward尺度、折扣因子gamma、经验回放池大小任何一处设置不当,都会导致训练发散。
我用一个表格来梳理三条预测线的差异:
| 预测目标 | 模型类型 | 标签形式 | 主要文件 | 适用场景 |
|---|---|---|---|---|
| 收盘价 | CNN回归 | 标准化后的价格数值 | DQN_img_closePrice.py | 判断未来价格中枢 |
| KD值 | CNN回归 | 0-100的KD指标 | DQN_kd_pic.py、KD_draw.py | 识别超买超卖拐点 |
| 年线趋势 | DQN强化学习 | 交易动作+累计收益 | DQN_yearline_reward.py | 长期趋势策略 |
三条线的数据预处理有一个共同点:都是先把行情转成图像再进网络。这也是这套代码包和普通CNN股票预测教程最大的区别——别人用的是Conv1D直接吃数值,这套代码把K线渲染成了图片,跑Conv2D。两种方式各有利弊,前者简单直接,后者能利用CNN在图像识别上积累的成熟经验,但渲染环节多出来的自由度(图片尺寸、线条颜色、是否画均线)本身就是一组需要调的超参数。
5. 股票预测避坑指南:数据泄漏、滞后预测与模型失效的排查
5.1 训练集loss很低,验证集却一路走高
现象:model.fit打印出的loss从0.1降到0.01,但val_loss不仅不降,还在往上跳。
原因:这个症状八成是数据泄漏。最常见的手法是先对全量数据做标准化再切分,或者用train_test_split默认的随机切分,导致验证集的信息提前进入了训练过程。另外一个隐蔽问题是标签错位——在构造y时用了np.roll或shift(-1)没处理边界值,等于把当天数据同时放进了X和y里,模型直接"抄袭"了答案。
解决:标准化只fit训练集,验证集和测试集用同一个scalertransform;切分必须按时间顺序走,不shuffle;检查标签构造逻辑,确认X窗口的最后一天是y的前一天,两者没有重叠列。如果怀疑是标签错位,打印一条X[-1]和y[-1]的日期对比一下就知道。
5.2 预测曲线总比真实价格慢半拍
现象:把预测值和真实价格画在一张图上,预测曲线形状和真实曲线几乎一样,但整体向右平移了一到两根K线,像影子一样滞后。
原因:这是时间序列回归的经典陷阱。因为窗口里的最后一天和预测目标高度相关,模型学到的不是"预判未来",而是"复制昨天的收盘价"。如果昨天涨了,模型就预测今天涨;今天继续涨,模型就预测明天涨——在单边趋势里看起来命中率很高,一进入震荡行情就原形毕露。
解决:把标签从明天收盘价改成未来N日平均价格或者未来N日的收益率方向,迫使模型必须预测更远的未来而不是复读当天值。另一个做法是在评估时单独统计方向准确率,别只看MSE或MAE。方向准确率低于52%的模型,基本可以判定存在滞后效应,需要重新设计标签和窗口结构。
5.3 换一只股票模型就崩了
现象:模型在A股票上训练,MAE很小、趋势拟合很好;换成B股票测试,预测结果完全不着调,甚至方向准确率只剩40%。
原因:模型过拟合到了A股票的波动特性。每只股票的波动率、涨跌节奏、跳空习惯都不一样,如果训练数据只有一只股票,CNN提取的特征大概率是这只股票的"专属规律",而不是通用的市场规律。另外一个原因是训练集覆盖的市场环境太单一——比如只训练了上涨行情,模型没见过下跌和横盘。
解决:训练集至少要包含3到5只不同行业、不同波动特征的股票,并且覆盖至少一个完整的牛熊周期。更稳妥的做法是把多只股票的数据混合在一起训练,每只股票按时间顺序切分,验证集从没参与训练的股票里取。代码包里如果能同时跑通两只不同的股票,模型才算有一点泛化能力。
5.4 DQN训练不收敛或reward震荡剧烈
现象:DQN_yearline_reward.py训练时累计收益曲线上下乱跳,几万个step之后也没有稳定上升的趋势,有时候奖励值突然从正变成负。
原因:DQN对超参数比CNN敏感得多。reward尺度不统一是最常见的问题——如果一笔交易的盈利是100,另一笔是0.1,模型会把注意力全放在大额盈利上,小波动直接被忽略。折扣因子gamma设得太大(接近1)会让模型过度关注远期收益,在长周期策略里反而学不到东西;设太小则只顾眼前利益。
解决:控制reward尺度,把每次交易的收益除以一个基准数值,或者用np.tanh把收益压到-1到1之间。gamma建议从0.9起步,观察如果训练发散就调到0.8以下。经验回放池buffer_size通常不能小于一个epoch的样本总量,采样时用优先经验回放(之前TD误差大的样本更常被抽到)效果更好。如果这些都不见效,干脆先跑通CNN基础上加一层简单的决策规则,不要一上来就碰强化学习。
6. 让预测结果可验证:一个简单的方向一致率评估方法
训练完模型,别急着下结论说"能用来交易"。我习惯先做一个方向一致率检查,这是验证预测模型有没有价值的最快方式。原理很简单:不看模型预测的价格准不准,只看它预测的涨跌方向和真实方向是否一致。
def direction_accuracy(y_true, y_pred, threshold=0.0): y_true = np.asarray(y_true).flatten() y_pred = np.asarray(y_pred).flatten() if len(y_true) != len(y_pred): raise ValueError("长度不一致") true_dir = np.diff(np.concatenate(([y_true[0]], y_true))) pred_dir = np.diff(np.concatenate(([y_pred[0]], y_pred))) correct = ((true_dir > threshold) & (pred_dir > threshold)) | \ ((true_dir < -threshold) & (pred_dir < -threshold)) return np.mean(correct) acc = direction_accuracy(y_test_original, y_pred_original) print(f"Direction Accuracy: {acc:.2%}")这个函数有两个隐藏细节。第一,因为diff会丢一个值,我用concatenate把预测首日作为基准,保持序列对齐。第二,threshold用来忽略微小波动——如果预测涨0.01元、实际跌0.005元,这种"方向错误"本来就没什么参考意义,把threshold设成当日振幅的0.1或0.2,过滤掉噪声。我一般会把threshold调成0.5 * 当日平均波动幅度,这样方向准确率不会因为几个擦边样本而失真。
把方向准确率算出来之后,还要结合滞后性检查一起看。我会把预测值和真实值画在同一张图上,缩放之后直接叠在一起,放大看波峰波谷处预测是不是普遍提前或滞后。如果预测曲线的波峰总是比真实波峰晚出现一两天,说明模型大概率在复读行情,方向准确率再高也不可信。
从那以后我每次训练完模型,都强制走一遍"方向准确率 + 滞后性可视化"这个流程,不给模型自吹自擂的机会。这套代码包的价值不在于帮你直接赚钱,而在于把CNN到DQN的完整链路走通,让你在真实数据上对比不同预测目标的区别。希望这份拆解能帮你避开我踩过的那些坑,下载后少走弯路。
本文还有配套的精品资源,点击获取