深度学习同时难在表示学习训练稳定与应用判断
理解深度学习,至少要同时面对三层问题:
- 表示学习问题:模型怎样从原始输入中逐层构造有效特征。
- 训练稳定问题:高参数网络为什么常常会出现不收敛、过拟合或数值不稳定。
- 应用判断问题:什么数据结构真正适合深度学习,什么场景并不值得盲目上深网。
对金融学生而言,本章最重要的不是多记几个模型名称,而是建立四个判断:
- 看懂神经网络从单层到深层的表达力演化。
- 分清 CNN、RNN、LSTM 与注意力机制各自利用了什么结构先验。
- 读懂一个基于股票时间序列的 LSTM 案例到底在做什么。
- 判断深度学习在金融中何时值得用、何时不值得用。
理解神经网络要先抓住神经元激活与训练链条
进入具体模型之前,先把最底层的共同语法抓牢:
- 神经元本质上只是加权求和再接非线性变换。
- 单层模型为什么只能画出线性边界。
- 激活函数为什么决定了深层网络是否真的具备非线性表达力。
- 前向传播、损失函数、反向传播怎样把“表示”变成“可训练的参数系统”。
- CNN 为什么能把图像中的局部结构与位置复用直接写进模型。
把这些问题理解透之后,后面的不同网络就不会像彼此孤立的名词表,而会呈现出共同底层:
- 深度学习仍然是函数逼近与优化的结合。
- 深度的价值主要来自分层表示,而不只是层数增加。
- 结构设计的关键在于把数据自身的组织方式写进网络。
深度学习把“手工特征工程”部分内生化了
传统机器学习通常要求研究者先把原始数据整理成一组人为设计的特征,再交给模型学习。
深度学习的变化在于:
- 它允许模型直接从原始输入中逐层提取表示。
- 低层表示往往对应局部模式,高层表示更接近任务语义。
- 研究者从“设计全部特征”转向“设计网络结构和训练流程”。
- 这并不意味着业务知识不重要,而是业务知识被转移到了数据组织和网络设计上。
对金融学生最重要的提醒
- 如果你的输入只是几十个财务比率,深度学习未必比树模型更占优。
- 如果你的输入是长文本、图像、语音或超长时序,深度学习的优势才更明显。
深度学习崛起来自数据算力算法三股力量同向推进
深度学习不是突然“被发明出来”,而是三种条件同时成熟后的结果:
- 数据规模变大,模型终于有足够样本去学习高维表示。
- GPU 与并行计算降低了训练超大网络的时间成本。
- 反向传播、初始化、正则化、优化器等技术不断改进,训练变得可操作。
| 数据 |
小样本下高参数模型难以估计 |
文本、公告、订单流、分钟级数据更适合 |
| 算力 |
训练太慢无法迭代 |
团队能更快试验不同结构 |
| 算法 |
深层网络容易不收敛 |
网络从概念模型变成可部署工具 |
有大量非结构化数据时深度学习更具优势
深度学习最擅长的不是所有问题,而是这几类问题:
- 输入维度极高,人工特征很难穷尽。
- 局部模式或顺序结构非常重要。
- 原始数据与预测目标之间的映射高度非线性。
- 训练样本足够多,可以支持大规模参数学习。
在金融中较典型的合适场景
- 财经公告、研报、新闻等文本信息抽取。
- K 线图、票据图像、文档版面识别。
- 高频交易与长序列订单流建模。
- 多源异构数据的联合表示学习。
在小样本结构化表格数据上不要盲目上深度学习
如果任务满足下面特征,深度学习往往不是起手式:
- 样本量只有几千到几万,而特征维度有限。
- 特征已经是人工整理好的财务指标、估值指标或宏观变量。
- 业务要求强解释性,且上线决策链条严格受监管。
- 训练资源有限,模型更新频率又不高。
此时更应先比较的基线
- 线性模型与惩罚回归。
- 树模型与梯度提升树。
- 支持向量机、随机森林等中等复杂度方法。
本科阶段要形成一个习惯:
先问数据结构,再问模型结构。
神经元本质上只是加权求和再经过非线性变换
一个最基本的神经元并不神秘,它做的事情可以写成:
\[ \large{z = w_0 + w_1 x_1 + \cdots + w_p x_p, \quad a = g(z)} \]
其中:
- \(x_1, \ldots, x_p\) 是输入特征。
- \(w_0\) 是偏置项。
- \(w_1, \ldots, w_p\) 衡量各输入的重要性。
- \(g(\cdot)\) 是激活函数,用来引入非线性。
感知机可以画出一条线性分类边界
如果把激活函数理解为“是否超过阈值”,那么神经元就变成了最早期的感知机分类器。
它能完成的事情是:
- 先给每个特征一个权重。
- 计算线性得分 \(z = w'x + b\)。
- 根据 \(z\) 是否大于阈值来决定分类结果。
在线性可分数据中,这个模型对应的是一条直线或一个超平面。
因此感知机能做的,本质上是线性划分。
异或问题说明单层模型有明确天花板
单层感知机最经典的局限就是异或问题:
- 两个输入相同,输出为 0。
- 两个输入不同,输出为 1。
- 这四个点无法用一条直线正确分开。
这张图的教学意义
- 感知机不是“太弱”,而是它的几何能力就是线性的。
- 想处理更复杂边界,就必须引入隐藏层和非线性激活。
多层网络通过隐藏层重组特征空间
多层感知机的关键不是多做了几次加权求和,而是:
- 第一层先把原始特征组合成中间表示。
- 第二层再对这些中间表示继续组合。
- 网络深度增加后,模型获得了逐层重写特征空间的能力。
\[ \large{a^{(l)} = g\left(W^{(l)} a^{(l-1)} + b^{(l)}\right)} \]
其中:
- \(a^{(0)} = x\) 是原始输入。
- \(a^{(1)}\)、\(a^{(2)}\) 等是逐层生成的隐藏表示。
- 最终输出层把高层表示映射到预测结果。
激活函数让“堆层”真正有意义
如果网络每一层都只做线性变换,那么无论堆多少层,最后仍然只是一个线性模型。
这是因为:
- 线性变换的复合仍然是线性变换。
- 所以层数本身不会自动创造非线性能力。
- 真正让深层网络有表达力的,是每一层之间插入的激活函数。
因此一定要记住一句话
没有激活函数,就没有真正意义上的深度学习。
Sigmoid 适合概率输出但不适合深层隐藏层
Sigmoid 函数写成:
\[ \large{g(z) = \frac{1}{1 + e^{-z}}} \]
它的特点是:
- 输出在 0 到 1 之间,很适合表示概率。
- 在二分类输出层中经常使用。
- 当 \(z\) 很大或很小时,函数会接近平坦,梯度变小。
对隐藏层的短板
- 容易造成梯度消失。
- 输出总是正的,不利于优化过程中的居中调整。
- 深层网络中一般不再把它作为隐藏层默认选择。
Tanh 让隐藏层输出更接近居中
Tanh 函数写成:
\[ \large{g(z) = \tanh(z)} \]
与 Sigmoid 相比,它的优势是:
- 输出范围在 -1 到 1 之间。
- 均值更接近 0,有助于优化稳定。
- 在一些较浅网络中比 Sigmoid 更容易训练。
但它仍然保留了饱和区间,因此:
- 在很深的网络里仍然会面临梯度衰减问题。
- 现在更多被 ReLU 系列函数替代。
ReLU 把深层网络训练带入实用阶段
ReLU 函数非常简单:
\[ \large{g(z) = \max(0, z)} \]
它之所以重要,不是因为形式优美,而是因为训练时更稳:
- 正区间梯度恒为 1,不容易饱和。
- 计算代价低,实现简单。
- 稀疏激活有时还能带来一定正则化效果。
但它也不是完美的
- 如果某个神经元长期落在负区间,可能出现“死亡 ReLU”。
- 这也是 Leaky ReLU、ELU 等变体出现的原因。
Softmax 把多个得分统一变成可比较的概率
在多分类问题中,输出层常使用 Softmax:
\[ \large{\text{softmax}(z_k) = \frac{e^{z_k}}{\sum_{j=1}^{K} e^{z_j}}} \]
它的作用不是“再做一层复杂变换”,而是:
- 把多个类别得分压缩成一组非负数。
- 所有概率之和等于 1。
- 便于和交叉熵损失一起使用。
Softmax 适用的问题场景很明确:
深度网络是在逐层重写数据表示
理解深度学习,一个非常重要的词叫表示学习。
表示学习意味着:
- 低层先识别简单模式。
- 中层把简单模式组合成更复杂结构。
- 高层再把复杂结构映射成任务相关语义。
比如图像任务中常见的层级是:
- 边缘与纹理。
- 局部形状与部件。
- 整体对象与场景语义。
这也是为什么“深”往往比“单纯更宽”更有意义。
宽度决定同层表达而深度决定组合层级
宽度和深度都能提高模型能力,但提高方式不同:
- 宽度增加,意味着同一层可同时容纳更多模式。
- 深度增加,意味着模型可以进行更多层级组合。
- 很多复杂结构更适合用分层组合表达,而不是在一层里一次性写完。
教学上可以这样理解
- 增宽更像“同一层同时招更多工人”。
- 加深更像“把生产流程分成更多道工序”。
对复杂模式识别来说,后者往往更有效。
参数越多表达越强但估计越难
深度学习模型常常拥有大量参数,这带来两面性:
- 好处是表达能力更强。
- 代价是需要更多样本与更稳定的训练机制。
- 参数越多,越容易记住噪声而不是结构。
- 模型调参空间也会迅速膨胀。
这里要建立的不是“参数越多越先进”的直觉,而是:
参数规模必须与数据规模、任务复杂度、业务容错水平一起考虑。
前向传播就是一条从输入到输出的编码流水线
前向传播可以看成神经网络的“生产环节”:
- 输入进入第一层。
- 每层先做线性变换,再过激活函数。
- 中间表示一层一层被更新。
- 最后一层输出预测值或概率。
\[ \large{a^{(l)} = g\left(W^{(l)} a^{(l-1)} + b^{(l)}\right)} \]
这里最容易被忽视的一点是:
- 前向传播不仅是在“算答案”。
- 它还在保存后续反向传播所需的中间结果。
损失函数定义了模型真正关心的目标
网络训练不是为了把输出“看起来合理”,而是为了最小化损失函数。
不同任务下常见的损失函数不同:
- 回归问题常用均方误差。
- 二分类常用二元交叉熵。
- 多分类常用多类交叉熵。
要点不是记名字,而是理解它们在惩罚什么
- 均方误差惩罚数值偏差。
- 交叉熵惩罚概率判断的偏离。
- 损失函数决定了模型把哪种错误看得更严重。
反向传播是在逐层分配误差责任
如果前向传播是“生产答案”,反向传播就是“追责过程”。
它回答的问题是:
- 最终预测错了,究竟是哪一层参数导致的。
- 每个参数应该往哪个方向调整。
- 每个参数应该调整多大幅度。
这件事看起来复杂,但逻辑很朴素:
- 先看输出错了多少。
- 再把这份误差信息逐层向前传递。
- 每一层只需要知道自己对下一层造成了多大影响。
深层网络不稳往往不是不会算而是梯度传不回去
很多初学者以为深度学习难,是因为公式复杂。真正更常见的问题是:
- 梯度太小,更新几乎不动。
- 梯度太大,训练直接发散。
- 各层分布不断变化,优化过程不稳定。
- 参数太多,容易在训练集上记忆噪声。
所以“模型会不会收敛”与“模型能不能表达”是两回事。
梯度消失和爆炸都来自链式连乘效应
当网络很深时,梯度是许多导数相乘的结果:
- 若多数导数绝对值小于 1,连乘后会越来越小。
- 若多数导数绝对值大于 1,连乘后会越来越大。
这就是为什么:
- 早期深层网络常常训练不起来。
- RNN 在长序列中尤其容易出现这个问题。
- 激活函数、初始化、归一化、残差结构都在试图缓解它。
初始化决定训练从哪里起跑
随机初始化不是随便填几个数,而是在决定优化起点。
初始化如果太大:
初始化如果太小:
因此才会有 Xavier、He 等初始化方案,它们试图让:
- 前向传播的方差不要越来越离谱。
- 反向传播的梯度也尽量保持稳定。
学习率是优化速度与稳定性的共同阀门
学习率决定每一步参数更新幅度。
- 太大时,损失函数可能来回震荡甚至发散。
- 太小时,训练会极慢,甚至停在糟糕区域附近。
- 在深度学习中,学习率通常比“模型多一层还是少一层”更先决定成败。
这里必须建立一个判断:
训练不理想时,先不要急着换模型,先看学习率和数据尺度。
BatchNorm 稳定的是每层接收到的数值分布
Batch Normalization 的直觉是:
- 每一层接收到的输入分布如果一直变化,训练会很难。
- 归一化可以让不同批次、不同层的数值尺度更稳定。
- 这样网络允许使用更大学习率,也更容易收敛。
它常被理解为“内部协变量偏移”的缓解手段,但本科阶段更重要的理解是:
- 它在帮优化器面对更平稳的数值环境。
- 它既有优化收益,也常带来一定正则化效果。
Dropout 通过随机失活打散神经元共适应
Dropout 的操作非常直接:
- 训练时随机把一部分神经元输出置零。
- 强迫网络不要过度依赖某几个固定通道。
- 促使模型学习更稳健、更分散的内部表示。
它缓解的是一种典型问题:
- 神经元之间形成过强共适应。
- 训练集表现很好,但样本外泛化很差。
所以 Dropout 的本质不是“制造噪声好玩”,而是在防止网络记忆化。
验证集与早停是在防止模型越学越坏
深度学习里一个常见现象是:
这意味着:
- 模型继续学习时,可能正在吸收噪声。
- 继续训练并不等于更好泛化。
早停的意义就是:
- 不是让模型把训练集学到极致。
- 而是在验证集表现最优处停止训练。
深度学习基础要抓住表达训练稳定性三条链条
到这里,深度学习基础可以压缩成三条最关键的链条:
- 神经元 → 激活函数 → 多层网络,这是表达力链条。
- 前向传播 → 损失函数 → 反向传播,这是训练链条。
- 初始化 → 学习率 → 归一化与正则化,这是稳定性链条。
后面的 CNN、RNN、LSTM 与注意力机制,并不是跳出这三条链条之外的新体系,而是在不同数据结构下对这三条链条的具体展开。
CNN 之所以必要是因为图像参数太多
如果把一张图像直接展平成长向量,再喂给全连接网络,会出现两个问题:
- 参数数量非常庞大。
- 空间邻近关系被展平后难以保留。
CNN 的出发点就是:
- 图像中相邻像素往往共同构成局部模式。
- 相同模式可能出现在图像不同位置。
- 所以模型应该利用“局部性”和“位置复用”这两个先验。
局部感受野让模型先学局部纹理而不是整张图一次看完
局部感受野意味着:
- 一个神经元不必连接整张图。
- 它只关注一个小窗口,比如 \(3 \times 3\) 或 \(5 \times 5\)。
- 这样更符合图像中边缘、角点、纹理等局部模式的形成方式。
这相当于告诉网络:
- 不要一上来就试图理解整张图。
- 先把局部基础构件学会,再逐层组合。
参数共享让同一个模式可以在不同位置重复被识别
CNN 中同一个卷积核会在整张图像上滑动。
这意味着:
- 用同一组参数反复扫描不同位置。
- 如果某种纹理在左上角和右下角都出现,卷积核都能识别。
- 参数数量因此大幅下降。
这就是 CNN 比全连接更高效的根本原因之一。
特征图记录的是某种模式在不同位置的响应强弱
卷积操作之后得到的特征图,不是“新的图片”而是“模式响应图”。
可以这样理解:
- 如果卷积核像一个边缘探测器,那么特征图上亮的地方说明边缘响应强。
- 如果卷积核像一个纹理探测器,那么特征图记录的是该纹理在哪些位置更明显。
- 不同卷积核对应不同模式探测器。
所以一层卷积网络其实是在回答:
图像的不同位置上,哪些局部模式出现了。
stride 和 padding 改变的是分辨率与边界信息
卷积层除了卷积核大小,还常见两个超参数:
- stride 决定卷积核每次滑动的步长。
- padding 决定是否在边界补零。
它们影响的不是抽象意义上的“效果”,而是很具体的两件事:
经验判断
- stride 大,输出会更快缩小。
- 不做 padding,边界信息更容易被裁掉。
池化层保留显著响应并主动丢弃细节
池化层最常见的是最大池化,它做的事很简单:
- 在一个小窗口里只保留最大值。
- 降低特征图空间尺寸。
- 让模型更关注“有没有出现某模式”,而不是“精确出现在像素几”。
这带来两个后果:
但代价也很明确:
典型 CNN 是卷积池化展平全连接的流水线
一个入门级 CNN 通常包含如下阶段:
- 若干卷积层提取局部特征。
- 若干池化层压缩空间尺寸。
- 展平层把二维特征图转换成一维向量。
- 全连接层负责完成最终分类或回归。
这种结构告诉我们:
- 前半段负责“看见什么”。
- 后半段负责“用这些信息做什么判断”。
CNN 相比全连接网络更像一台局部扫描器
把 CNN 和全连接网络对比时,最该抓的不是公式,而是工作方式:
- 全连接网络像把整张图一次性摊平后整体打分。
- CNN 像拿着一个小型扫描器在图上移动,寻找局部模式。
- CNN 先把“局部”学好,再慢慢组合成“整体”。
这就是为什么:
- 在图像问题上,CNN 往往比简单 MLP 更合适。
- 在无明显局部结构的表格问题上,CNN 的优势就不明显。
在金融图像任务里 CNN 关注的是局部形态与版面结构
把 CNN 放到金融问题中,最常见的不是“直接看股价表格”,而是下面几类任务:
- K 线图与技术形态识别。
- 发票、票据、财报扫描件的版面识别。
- 风险控制中的证件图像核验。
- 卫星图、门店图像与另类数据分析。
这类任务的共同特点是:
- 数据有空间结构。
- 局部局势比单个像素值更重要。
- 同一模式可能在不同区域重复出现。
CNN 适合二维局部模式却不擅长长期时序依赖
CNN 再强,也不是所有结构问题的通解。
它不太适合直接处理的问题包括:
- 特别长的时间依赖。
- 语义顺序极其关键的文本推理。
- 需要显式记忆过去状态的序列任务。
所以当任务核心从“局部空间模式”转向“长期顺序关系”时,我们通常要从 CNN 走向 RNN、LSTM 或注意力机制。
CNN 把图像中的空间结构先验直接写进网络
从 CNN 可以看到,深度学习真正强的地方不只是参数更多,而是能把数据结构中的先验直接写进模型:
- 图像中的局部邻近关系被写成局部感受野。
- 相同模式在不同位置重复出现的事实被写成参数共享。
- 从局部纹理到整体对象的层级组合被写成多层卷积与池化。
这说明模型设计的关键问题从来不是“复杂不复杂”,而是“结构是否贴合数据本身”。
顺序数据建模要同时处理上下文记忆与长期依赖
当输入从图像换成文本或时间序列,核心困难也随之变化:
- 当前判断必须依赖过去上下文,而不是只看当前值。
- 重要信息可能来自很久之前,模型必须处理长期依赖。
- 历史信息不能只被机械累积,还要被选择性保留、遗忘与调用。
- 计算成本会随着序列长度上升,因此结构设计必须兼顾表达力与效率。
RNN、LSTM、GRU 与注意力机制,正是在回答这些顺序数据问题时形成的不同方案。
序列任务的关键不是值本身而是出现顺序
在序列数据中,顺序往往和数值一样重要。
例如:
- 文本里“不是很好”和“很好不是”不能等价处理。
- 金融市场中利好消息出现在暴跌前和暴跌后,意义不同。
- 连续几天的收益路径常常比某一天的孤立收益更重要。
因此序列模型必须处理的问题不是简单聚合,而是:
如何让当前判断依赖过去上下文。
RNN 的创新是把过去状态带到现在
RNN 的核心想法非常朴素:
- 当前时点不只看当前输入 \(x_t\)。
- 还要把上一个时点留下的隐藏状态 \(h_{t-1}\) 一起带入。
于是它的基本形式可以写成:
\[ \large{h_t = g(W_x x_t + W_h h_{t-1} + b)} \]
这里的隐藏状态 \(h_t\) 就像一个滚动更新的摘要。
展开图说明 RNN 在时间轴上共享同一套参数
RNN 的结构图常有两种画法:
- 紧凑图,看起来像一个带回路的单元。
- 展开图,把不同时间步一格一格展开。
展开图特别重要,因为它揭示了:
- 每个时间步其实都在重复使用同一套参数。
- 序列长度变长,不等于参数数量同步变长。
- 模型真正增长的是计算链条长度,而不是参数表规模。
隐状态可以理解为到当前时点为止的信息摘要
隐藏状态 \(h_t\) 不等于“记住全部过去”,它更像:
- 一个压缩后的上下文摘要。
- 一个服务于当前任务的内部表示。
- 一个会随着新输入不断更新的动态记忆。
所以在教学上,我们不必把它神秘化。更好的理解是:
- 模型每读入一个新时点,就把过去摘要和当前观测重新融合一次。
- 新生成的摘要再传给下一时点。
时间反向传播让误差信息沿时间轴回流
训练 RNN 时,反向传播不只沿层传播,还要沿时间传播。
这被称为 BPTT,也就是 Through Time:
- 先把序列展开。
- 再像普通深层网络一样逐步反传误差。
- 于是时间越长,链式法则连乘越长。
这也直接导致了后面的核心困难:
普通 RNN 抓不住长期依赖并不意外
为什么普通 RNN 难以记住很久以前的信息?
- 因为旧信息必须经过很多时间步不断传递。
- 每一步都会经历线性变换与激活函数。
- 误差信号在长期回传时容易越来越弱。
结果就是:
- 近处信息容易学到。
- 远处信息经常在训练过程中被冲淡。
这不是实现细节,而是普通 RNN 的结构性限制。
LSTM 通过门控和细胞状态延长了可用记忆
LSTM 的出现,就是为了缓解长期依赖问题。
它的关键变化有两点:
- 引入细胞状态 \(c_t\),让信息有一条相对稳定的主通道。
- 引入门控机制,决定什么该忘、什么该写入、什么该输出。
这使得模型不再被迫把所有历史信息都压缩在同一套短期隐藏状态里。
GRU 是更轻量的门控记忆结构
GRU 可以理解为对 LSTM 的简化:
它与 LSTM 的关系可以这样把握:
- 如果任务复杂、长期依赖明显,LSTM 往往是更稳妥起点。
- 如果数据规模有限、训练成本敏感,GRU 往往值得尝试。
注意力机制把压缩全部历史改成按需查阅历史
RNN 的一个天然困难是:
注意力机制的改变在于:
- 当前时点不一定只依赖一个压缩摘要。
- 它可以回头看历史各位置,并给不同位置不同权重。
所以注意力的核心思想是:
不是把历史全部塞进一个小盒子,而是在需要时重新翻阅历史。
词嵌入把离散词汇变成连续语义坐标
文本不能直接喂给神经网络,所以要先把词转换成数值表示。
词嵌入比独热编码更有价值,因为:
- 它用低维连续向量表示词语。
- 语义相近的词会在向量空间中更接近。
- 网络能在训练中逐渐学习这些语义关系。
对金融文本来说,这尤其重要,因为:
- ‘增长放缓’和‘盈利承压’虽然字面不同,但语义相关。
- 好的嵌入层能帮助模型捕捉这种隐含关系。
金融文本任务的流程是清洗分词嵌入再建模
一个标准的金融文本深度学习流程通常包含:
- 文本清洗与去噪。
- 分词或子词切分。
- 嵌入表示。
- 序列模型或注意力模型。
- 分类、抽取或打分输出。
这提醒我们:
- 深度学习不等于只搭网络。
- 文本预处理质量会直接决定上限。
情感分类最重要的是区分词频信息与上下文信息
同样是情感分析,不同模型抓住的信息并不一样:
- 词袋模型更依赖词频。
- RNN、LSTM、注意力模型更重视上下文顺序。
- 否定词、转折词、程度副词常常决定最终语义方向。
在财经文本里这点更关键:
- ‘利润增长但现金流承压’这种句子,不能只看正面词频。
- 模型必须处理转折和组合语义。
财经公告分析比影评分类更难是因为语言更克制
教材里常用影评分类做例子,是因为标签清晰、语气明显。
但真正的财经文本更难,原因包括:
- 表述更正式、更克制。
- 同一词语在不同上下文下含义可能变化。
- 关键信息可能藏在长段落、脚注、表格说明中。
- 标签本身也未必天然清晰,比如“利好”经常要结合市场预期判断。
因此金融文本深度学习一定要配合领域知识。
时间序列案例先把预测对象定义清楚
下面我们用海康威视的后复权收盘价构造一个入门级 LSTM 预测案例。
在任何时序建模之前,先要明确三件事:
- 预测对象是什么,是价格、收益率还是波动率。
- 预测步长是什么,是下一天还是下一周。
- 评价标准是什么,是误差、方向准确率还是可交易收益。
这次案例选择的是:
预处理往往比网络结构更能决定结果
时间序列模型里,很多失败并不是网络不够深,而是预处理不规范:
- 日期顺序错乱。
- 缺失值处理随意。
- 训练测试切分出现未来信息泄漏。
- 价格尺度过大导致优化不稳定。
所以案例开始之前,先做数据清洗和尺度整理。
import os # 导入操作系统模块以便区分不同平台的数据路径
import numpy as np # 导入 numpy 用于数值运算和数组重塑
import pandas as pd # 导入 pandas 用于读取和整理股票时间序列数据
from sklearn.preprocessing import MinMaxScaler # 导入最小最大缩放器用于归一化价格序列
linux_base_dir = '/home/ubuntu/r2_data_mount/data' if os.path.exists('/home/ubuntu/r2_data_mount/data') else '/home/ubuntu/r2_data_mount/qiufei/data' # 优先选择当前 Linux 机器真实存在的数据挂载目录并兼容旧目录结构
base_dir = 'C:/qiufei/data' if os.name == 'nt' else linux_base_dir # 根据操作系统选择本地数据根目录
stock_path = os.path.join(base_dir, 'stock/stock_price_post_adjusted.h5') # 组合得到后复权股价数据文件路径
haikvision_frame = pd.read_hdf(stock_path, key='data', where="order_book_id='002415.XSHE'", columns=['date', 'close']).reset_index() # 按股票代码选择性读取海康威视后复权行情并将索引还原为普通列
haikvision_frame = haikvision_frame.loc[:, ['date', 'close']].sort_values('date').reset_index(drop=True) # 仅保留日期与收盘价两列并按日期升序排列以保证窗口切分顺序正确
haikvision_frame['date'] = pd.to_datetime(haikvision_frame['date']) # 将日期列明确转换为时间戳格式以保证后续切分正确
haikvision_frame['log_return'] = np.log(haikvision_frame['close']).diff() # 计算对数收益率用于观察价格序列变化节奏
haikvision_frame = haikvision_frame.dropna().reset_index(drop=True) # 删除首行收益率缺失值并重建顺序索引
scaler = MinMaxScaler() # 创建最小最大归一化器为后续 LSTM 输入统一数值尺度
haikvision_frame['scaled_close'] = scaler.fit_transform(haikvision_frame[['close']]) # 将收盘价缩放到 0 到 1 区间
haikvision_frame.tail(5) # 展示预处理后的最近五条样本帮助学生确认数据结构
归一化本质上是在统一数值尺度而不是制造信息
为什么价格序列常常要先归一化?
- 因为神经网络训练依赖梯度下降。
- 如果不同输入量级差异过大,更新过程会不稳定。
- 归一化不会凭空创造预测能力,但能改善训练数值条件。
这里要特别避免一个误解:
- 标准化、归一化不是“高级特征工程”。
- 它首先是数值计算层面的稳定化操作。
滑动窗口把时间序列改写成监督学习样本
LSTM 并不是直接对“一长串历史”整体打分,而是先把序列切成很多个窗口样本。
例如:
- 用前 30 天作为输入。
- 第 31 天作为目标值。
- 窗口不断向前滑动,形成许多监督学习样本对。
window_length = 30 # 设定每个样本使用过去 30 个交易日的收盘价作为输入窗口
scaled_prices = haikvision_frame['scaled_close'].to_numpy() # 提取归一化后的收盘价序列供窗口函数使用
def create_windows(series, window_length): # 定义滑动窗口函数将时间序列重构为监督学习数据
sequence_features = [] # 初始化特征窗口列表以存储每个时间段的输入序列
sequence_targets = [] # 初始化目标列表以存储每个窗口后一天的预测目标
for start_index in range(len(series) - window_length): # 从序列起点开始逐步滑动窗口直到最后一个完整窗口
sequence_features.append(series[start_index:start_index + window_length]) # 将当前窗口内的价格切片追加到特征列表
sequence_targets.append(series[start_index + window_length]) # 将窗口结束后紧邻的一天价格作为监督学习目标
return np.array(sequence_features), np.array(sequence_targets) # 将列表转换为数组后返回给后续模型训练阶段
sequence_features, sequence_targets = create_windows(scaled_prices, window_length) # 执行窗口重构得到样本特征与目标序列
window_example = pd.DataFrame({'第1个窗口前3个值': sequence_features[0][:3], '第1个窗口后3个值': sequence_features[0][-3:], '对应目标值': [sequence_targets[0], np.nan, np.nan]}) # 构造一个紧凑表格帮助学生直观看到窗口与目标的对应关系
window_example # 输出首个窗口示意表格用于课堂讲解
时间序列切分必须严格遵守时间方向
和普通 IID 数据不同,时间序列不能随意随机打乱切分。
正确做法是:
因为如果把未来样本混进训练集,就会出现:
- 模型在训练时偷看未来。
- 评估结果虚高。
- 真正上线后性能大幅下滑。
双层 LSTM 结构是在用两层记忆做层级抽象
本案例使用一个非常入门的双层 LSTM:
- 第一层负责从窗口序列中抽取较基础的时间模式。
- 第二层在第一层输出的序列之上继续做抽象。
- 最后接全连接层输出下一日预测值。
优化器正则化和 epoch 要放在一起理解
同学们经常会把优化器、Dropout、epoch 数量分开记忆,但在实践里它们是联动的:
- 学习率和优化器决定每一步怎么走。
- Dropout 决定模型是否会过度记忆局部样本。
- epoch 决定模型看训练数据多少遍。
课堂案例使用较短训练轮数的原因
- 这是教学演示,不追求极限精度。
- 真正研究中应进一步调参,并通过滚动样本外检验确认稳健性。
预测图要先看方向再看幅度再看拐点
在时序预测图中,最常见的误区是直接看“像不像”。
更合理的阅读顺序是:
- 先看大方向是否一致。
- 再看波动幅度有没有系统性低估或高估。
- 最后看拐点附近是否明显滞后。
Code
predicted_scaled = lstm_model.predict(test_features, verbose=0) # 使用训练完成的 LSTM 模型对测试区间样本逐个生成归一化预测值
predicted_close = scaler.inverse_transform(predicted_scaled) # 将模型预测的归一化收盘价反变换回原始价格尺度便于经济解释
actual_close = scaler.inverse_transform(test_targets.reshape(-1, 1)) # 将测试目标值同步反变换为原始价格尺度作为真实基准路径
plot_length = 180 # 仅截取最近一百八十个交易日绘图以保证课堂图形清晰可读
plot_actual = actual_close[-plot_length:] # 取测试区间末端真实收盘价序列用于展示最新阶段表现
plot_predicted = predicted_close[-plot_length:] # 取测试区间末端预测收盘价序列与真实值对比
loss_frame = pd.DataFrame({'训练损失': training_history.history['loss'], '验证损失': training_history.history['val_loss']}) # 将训练损失历史整理成数据框以便统一绘图展示
import matplotlib.pyplot as plt # 导入 Matplotlib 用于将预测结果与损失历史绘制成课堂展示图
plt.rcParams['font.family'] = ['Source Han Serif SC', 'Noto Serif CJK SC', 'SimSun'] # 指定中文图形字体优先级以满足项目统一字体要求
plt.rcParams['axes.unicode_minus'] = False # 关闭负号乱码问题以保证中文图形正常显示
fig, axes = plt.subplots(1, 2, figsize=(13, 4.8)) # 创建一行两列画布同时展示价格路径和训练损失
axes[0].plot(plot_actual, label='真实收盘价', linewidth=2) # 绘制真实收盘价路径帮助学生观察样本外实际走势
axes[0].plot(plot_predicted, label='LSTM 预测值', linewidth=2, linestyle='--') # 绘制模型预测路径比较趋势追踪与幅度偏差
axes[0].set_title('最近 180 个交易日的样本外预测') # 给左侧子图添加标题突出样本外预测场景
axes[0].set_xlabel('交易日序号') # 为左侧子图设置横轴标签说明时间顺序
axes[0].set_ylabel('收盘价') # 为左侧子图设置纵轴标签说明度量指标
axes[0].legend() # 显示左侧图例帮助学生识别真实值与预测值两条路径
axes[0].grid(True, alpha=0.25) # 为左侧子图加入浅色网格方便比较路径偏离程度
axes[1].plot(loss_frame['训练损失'], label='训练损失', linewidth=2) # 绘制训练集损失轨迹帮助理解模型收敛过程
axes[1].plot(loss_frame['验证损失'], label='验证损失', linewidth=2) # 绘制验证集损失轨迹帮助判断是否过拟合
axes[1].set_title('课堂演示版训练损失轨迹') # 给右侧子图添加标题强调这是教学版训练结果
axes[1].set_xlabel('Epoch') # 为右侧子图设置横轴标签说明训练轮次
axes[1].set_ylabel('均方误差') # 为右侧子图设置纵轴标签说明损失度量
axes[1].legend() # 显示右侧图例区分训练损失与验证损失两条曲线
axes[1].grid(True, alpha=0.25) # 为右侧子图添加浅色网格方便观察收敛速度变化
plt.tight_layout() # 自动调整双子图间距避免文字与图例重叠
plt.show() # 输出最终图形到当前幻灯片页面供课堂讲解使用
时序预测中的滞后是最常见也最容易被误判的问题
如果你在预测图里发现:
- 走势大体跟对了。
- 但 turning point 总是慢半拍。
这通常不是偶然,而是时序深度模型的常见现象。
原因包括:
- 模型严重依赖过去窗口信息,本质上在做平滑外推。
- 极端波动往往由新信息触发,而这些信息不在纯价格序列里。
- 用单一价格历史预测未来,本来就信息不足。
低误差不等于可以直接赚钱
金融应用里一个尤其重要的判断是:
原因包括:
- 预测方向可能对,但幅度不足以覆盖交易成本。
- 回撤控制、仓位约束、滑点冲击都会改变最终收益。
- 某些模型只是跟随趋势,却不能在关键拐点提供超额信息。
所以从研究走向投资决策时,必须多走一步:
把预测能力转化成策略评估。
深度学习在金融里最常见的失败是样本外失效
在金融市场中,深度学习最常见的失败模式包括:
- 训练集表现极好,样本外大幅退化。
- 市场结构变了,历史模式失效。
- 标签定义不稳定,导致模型学到的是噪声代理。
- 特征与目标之间存在看似显著但不可复制的偶然关系。
这也是为什么金融中的深度学习必须格外重视:
上线前必须检查漂移成本风险与解释性
一个深度学习模型在课堂上跑通,不代表它可以直接上线。
上线前至少要检查:
- 数据分布是否漂移。
- 推理速度是否满足业务时效。
- 错误代价是否可接受。
- 是否有风控兜底与人工复核机制。
- 是否具备最基本的可解释与监控方案。
对于金融业务来说,这些问题往往比模型再多 1 个点精度更重要。
深度学习在金融中的边界比前景更值得先想清楚
深度学习当然有前景,但对于金融研究者,先想清楚边界更重要:
- 市场是会适应模型的,不像静态图像数据那样稳定。
- 监管、可解释性、审计要求会约束黑箱模型使用范围。
- 很多金融数据的有效样本量并没有想象中那么大。
- 高维复杂模型更容易把偶然关系误当成结构。
所以一个成熟的判断应该是:
- 深度学习是工具箱中的高能力工具。
- 不是默认起手式,更不是无条件替代所有传统方法。