10  深度学习

深度学习是机器学习中最令人兴奋的领域之一。它已经在金融风控、量化投资、自然语言处理等许多领域取得了突破性进展。在金融领域,深度学习被广泛应用于股票价格预测、信用评分、算法交易等场景。本章介绍深度学习的基础概念,包括神经网络、卷积神经网络、循环神经网络等。

10.1 学习闭环

先修与可观察目标

完成本章后,学生应能达到以下标准:

  1. 对给定的一样本 MLP,写出各张量形状并手算前向值、二元交叉熵、四个梯度与一次更新,数值误差不超过 \(10^{-3}\)
  2. 用“连接范围、参数共享、状态传递”区分 MLP、CNN 与 RNN/LSTM,并能纠正“卷积天然平移不变”的误解。
  3. 在时间有序数据上建立训练—验证—测试三段式比较,所有预处理仅拟合开发期,测试集只评价一次。
  4. 用训练—验证差距、基线比较与学习曲线诊断欠拟合、过拟合和优化失败,不把损失下降等同于样本外有效。
  5. 针对一个陌生任务说明网络、损失、正则化与基线选择,并给出“不应使用深度学习”的证据边界。

入口检查(5 分钟)

不运行代码,回答:若 \(z=wx+b\)\(hat y=\sigma(z)\)\(L=-[y\log\hat y+(1-y)\log(1-\hat y)]\),当 \(x=2,y=1,w=0,b=0\) 时,\(hat y\)\(\partial L/\partial w\) 是多少?

展开入口评分、补救与异形复测

答案为 \(0.5\)\((0.5-1)\times2=-1\)。若不能同时写对,先复习 式 10.4、链式法则和第 3 章逻辑回归,再用 \(x=1,y=0\) 重测;重测答案为 \(0.5\)\(0.5\)

无提示检索与渐隐链

闭书写出“线性变换—激活—损失—反向—更新”五步及每步输入输出。随后完成三轮渐隐:第一轮对照 小节 10.3.3.1 复算全部数值;第二轮只保留初值,自行写梯度;第三轮把标签改为 \(y=0\),预测更新方向并复算。能在 8 分钟内完成第三轮且误差不超过 \(10^{-3}\),才进入 CNN。

陌生迁移

一家苏州制造企业只有 800 条按月设备记录和 20 个表格特征,要求预测下月故障。写一页决策备忘录:比较正则逻辑回归、小型 MLP 与序列网络,明确时间切分、朴素基线、资源预算和放弃深度模型的触发条件。评分证据见练习 8。

目标—评价映射

目标 正文活动 评价证据 达标标准
1 最小 MLP 手算与运行 核心练习 9 的输出梯度桥梁与练习 11 的终端手算(练习 10 为拓展) 练习 9 推导 \(\partial\ell/\partial z=p-y\);练习 11 的前向、BCE、四组梯度与更新误差均不超过 \(10^{-3}\)
2 CNN/RNN 概念段与架构比较 练习 2 能按连接范围、参数共享、状态传递完成四列比较
3 时间验证案例 小节 10.9 的 MP10-1—MP10-3 时间边界、训练期预处理和测试访问三项分别留痕
4 训练诊断 小节 10.9 的 DIAG10-1 同时报基线、训练—验证差距与优化失败判据
5 陌生迁移 练习 8 选择与停用条件均有证据

本章练习均标为 项目:无;深度学习不是贯穿项目的必需里程碑。

10.2 深度学习概述

深度学习是机器学习的一个分支,它使用多层神经网络从数据与任务损失中共同估计中间表示。与完全依赖预先固定特征的流程相比,深度网络可以减少部分手工组合工作;但输入定义、时点对齐、业务约束与稳健特征仍需研究者设计,“表示学习”不等于完全不需要特征工程。

短检索检查:为什么“网络学习中间表示”不能改写成“不需要特征工程”?答题须同时指出:原始输入与信息时点仍由研究者定义,任务损失只会奖励与当前目标相关的表示,样本外稳健性仍需基线和冻结数据验证。

深度学习在金融经济及其他领域取得了巨大成功:

  • 量化投资:资产价格预测、因子挖掘、算法交易策略等
  • 金融风控:信用评分、欺诈检测、反洗钱监控等
  • 自然语言处理:财经新闻情感分析、公告信息提取、智能客服等
  • 时间序列预测:股价走势、波动率预测、宏观经济指标预测等
  • 推荐系统:基金产品推荐、个性化理财等

10.3 单层神经网络

我们从单隐藏层前馈网络开始。严格地说,经典感知机是带阈值判别规则的线性分类器;含可微激活函数与连续输出的网络不应一概称为感知机。令输入 \(x\in\mathbb R^p\),隐藏层有 \(K\) 个单元,输出层有 \(M\) 个单元。第 \(m\) 个输出先形成 logit(或回归线性预测量)

\[ z_m=\beta_{m0}+\sum_{k=1}^{K}\beta_{mk}A_k,\qquad m=1,\ldots,M, \tag{10.1}\]

再由与任务匹配的输出映射 \(q\) 得到

\[ \widehat y=q(z). \tag{10.2}\]

其中

\[ A_k=h_k(x)=g\left(w_{k0}+\sum_{j=1}^{p}w_{kj}x_j\right),\qquad k=1,\ldots,K. \tag{10.3}\]

因此 \(W=(w_{kj})\in\mathbb R^{K\times p}\),输出权重 \(B=(\beta_{mk})\in\mathbb R^{M\times K}\),且 \(A\in\mathbb R^K\)\(z,\widehat y\in\mathbb R^M\)。标量回归取 \(M=1,q(z)=z\);二分类取 \(M=1,q(z)=\sigma(z)\)\(M\) 类互斥分类取 \(q(z)_m=\exp(z_m)/\sum_{r=1}^{M}\exp(z_r)\)。正文统一用 \(y\) 表示观测响应、\(\widehat y\) 表示预测;\(K\) 始终只表示隐藏单元数,\(M\) 始终只表示输出维数或类别数。

这里 \(g(\cdot)\) 是隐藏层激活函数,例如 sigmoid 或 ReLU(修正线性单元);它与任务特定的输出映射 \(q\) 不必相同。

10.3.1 激活函数

激活函数引入非线性使神经网络能够学习复杂的模式。常见的激活函数包括

  1. Sigmoid 函数\[ g(z) = \frac{1}{1 + e^{-z}} \tag{10.4}\]

    值域为开区间 \((0,1)\),常用于二元分类输出层。

  2. tanh 函数\[ g(z) = \tanh(z) = \frac{e^z - e^{-z}}{e^z + e^{-z}} \]

    值域为开区间 \((-1,1)\),并以 0 为中心。

  3. ReLU(修正线性单元)\[ g(z) = \max(0, z) \tag{10.5}\]

    对正输入输出原值,对非正输入输出 0。其正半轴导数为 1,可缓解饱和激活造成的梯度衰减,但不能保证消除梯度问题。

  4. Softmax 函数(用于 \(M\) 类互斥分类)

    \[ \operatorname{softmax}(z)_m=\frac{e^{z_m}}{\sum_{r=1}^{M}e^{z_r}},\qquad m=1,\ldots,M. \tag{10.6}\]

    每个输出位于 \((0,1)\),且 \(\sum_{m=1}^{M}\widehat y_m=1\)。数值实现通常先减去 \(\max_r z_r\),避免指数溢出。

提示:为什么需要非线性激活函数

如果所有隐藏层都使用恒等映射,无论网络有多少层,它都等价于单层仿射模型,因为仿射映射的复合仍是仿射映射。非线性激活扩大了可表示函数族,但“能逼近”必须说明函数类别、定义域、误差度量和网络宽度。

具体来说,若 \(g(a)=a\)(恒等函数),则标量输出的线性预测量满足

\[ z(x)=\beta_0+\sum_{k=1}^{K}\beta_k\left(w_{k0}+\sum_{j=1}^{p}w_{kj}x_j\right)=\widetilde\beta_0+\sum_{j=1}^{p}\widetilde\beta_jx_j. \]

这只是另一个线性回归模型。

补充说明:通用逼近定理的精确边界

令目标函数 \(f\) 是单位立方体 \([0,1]^p\) 上的连续实值函数,隐藏层使用连续 sigmoid 型激活。对任意 \(\varepsilon>0\),存在有限宽度 \(K\) 以及一组参数,使单隐藏层网络 \(F_K\) 满足 \(\sup_{x\in[0,1]^p}|F_K(x)-f(x)|<\varepsilon\) (Cybenko 1989年)。结论可经连续坐标变换推广到紧致长方体,但它不覆盖任意不连续目标,也不说明所需宽度很小、梯度法能找到参数、有限样本泛化良好或深层网络必然优于浅层网络。

短检索检查:闭书回答通用逼近结论中的四个限定——目标函数类别、输入定义域、误差度量、宽度量词。标准答案为“连续函数、紧致域(原定理单位立方体)、一致范数、存在有限但可随 \(\varepsilon\) 增大的宽度”;漏掉任一项,就不能写“网络可逼近任意函数”。

10.3.2 多层神经网络

单层神经网络的表达能力有限。通过堆叠多个隐藏层我们可以构建深度神经网络,能够学习更复杂的函数。

一个具有 \(L\) 个隐藏层、各层宽度分别为 \(K_1,\ldots,K_L\) 的网络可以表示为

\[ z_m=\beta_{m0}+\sum_{k=1}^{K_L}\beta_{mk}A^{(L)}_k,\qquad \widehat y=q(z),\qquad m=1,\ldots,M. \]

其中 \(K_0=p\),并且

\[ A^{(l)}_k=g\left(w^{(l)}_{k0}+\sum_{j=1}^{K_{l-1}}w^{(l)}_{kj}A^{(l-1)}_j\right),\quad k=1,\ldots,K_l,\quad l=1,\ldots,L. \tag{10.7}\]

以及 \(A^{(0)}_j=x_j\)。于是 \(W^{(l)}\in\mathbb R^{K_l\times K_{l-1}}\),输出权重 \(B\in\mathbb R^{M\times K_L}\);层号、单元号与输出号不再共用同一个上界。

每个隐藏层都对前一层表示作参数化变换;在合适数据、损失和优化下,较后层可能形成更贴近任务的组合特征,但“越深必然越抽象”不是定理,应通过探针、消融或样本外表现检验。

10.3.3 最小 MLP:前向、损失、梯度与一次更新

进入 CNN 与 RNN 前,先固定 MLP 的训练闭环:前向传播得到 \(\hat y\),损失函数 \(L(y,\hat y)\) 衡量任务误差,反向传播用链式法则计算梯度,优化器再更新参数。对权重 \(w\) 的一次梯度下降更新为

\[ w^{(t+1)}=w^{(t)}-\eta\frac{\partial L}{\partial w}. \tag{10.8}\]

CNN、RNN 和 LSTM 改变的是连接结构与参数共享方式,不改变“前向—损失—反向—更新”的基本逻辑。下面用一个样本、两个输入、一个 sigmoid 隐单元和一个 sigmoid 输出单元完成所有步骤。

10.3.3.1 手算 worked example

给定 \(x=(1,2)^\top,y=1\),隐层参数 \(w=(0.2,-0.1)^\top,b=0.1\),输出参数 \(v=0.4,c=-0.2\),学习率 \(\eta=0.1\)。隐层预激活记为 \(s\),输出 logit 记为 \(z\);形状依次为 \(x,w\in\mathbb R^2\)\(s,h,z,\widehat y\in\mathbb R\)

第一步:前向传播。

\[ s=w^\top x+b=0.2-0.2+0.1=0.1, \qquad h=\sigma(s)=0.524979. \]

\[ z=vh+c=0.4(0.524979)-0.2=0.009992, \qquad \widehat y=\sigma(z)=0.502498. \]

第二步:损失。\(y=1\),二元交叉熵为

\[ L=-\log(\hat y)=0.688164. \]

第三步:反向传播。 sigmoid 与交叉熵合并后,输出 logit 的误差是 \(\delta_z=\partial L/\partial z=\widehat y-y=-0.497502\)。因此

\[ \frac{\partial L}{\partial v}=\delta_z h=-0.261178, \qquad \frac{\partial L}{\partial c}=\delta_z=-0.497502. \]

隐层误差为

\[ \delta_s=\delta_z v h(1-h)=-0.049626, \]

所以

\[ \nabla_wL=\delta_sx=(-0.049626,-0.099252)^\top, \qquad \frac{\partial L}{\partial b}=\delta_s. \]

第四步:一次更新。 使用 式 10.8 得到

\[ w^+=(0.204963,-0.090075)^\top,\quad b^+=0.104963, \quad v^+=0.426118,\quad c^+=-0.150250. \]

因为标签为 1,本次更新应提高预测概率;把更新后参数重新前向计算,得到 \(\hat y^+\approx0.5191>0.5025\),这是最低限度的方向健全性检查。

列表 10.1: 最小 MLP 的一次前向、反向与更新
import numpy as np  # 使用向量运算逐项复核手算结果
input_features = np.array([1.0, 2.0])  # 固定两个输入以便人工复算
hidden_weights = np.array([0.2, -0.1])  # 固定隐层初始权重
hidden_bias, output_weight, output_bias = 0.1, 0.4, -0.2  # 固定三个标量参数
target_value, learning_rate = 1.0, 0.1  # 固定标签与一步学习率
sigmoid = lambda value: 1.0 / (1.0 + np.exp(-value))  # 定义本例所需激活函数
hidden_value = sigmoid(hidden_weights @ input_features + hidden_bias)  # 完成隐层前向
predicted_probability = sigmoid(output_weight * hidden_value + output_bias)  # 完成输出前向
cross_entropy = -np.log(predicted_probability)  # 计算正类交叉熵
output_delta = predicted_probability - target_value  # 计算输出 logit 梯度
hidden_delta = output_delta * output_weight * hidden_value * (1 - hidden_value)  # 链式回传隐层
hidden_weights -= learning_rate * hidden_delta * input_features  # 更新两个隐层权重
hidden_bias -= learning_rate * hidden_delta  # 更新隐层偏置
output_weight -= learning_rate * output_delta * hidden_value  # 更新输出权重
output_bias -= learning_rate * output_delta  # 更新输出偏置
updated_probability = sigmoid(output_weight * sigmoid(hidden_weights @ input_features + hidden_bias) + output_bias)  # 复核方向
assert updated_probability > predicted_probability  # 正类样本的一步更新必须提高预测概率
gradient_report = {'grad_w': hidden_delta * input_features, 'grad_b': hidden_delta, 'grad_v': output_delta * hidden_value, 'grad_c': output_delta}  # 汇总四组梯度证据
print({'report_label': 'ch10_mlp_one_update', 'loss': cross_entropy, 'gradients': gradient_report, 'updated_probability': updated_probability})  # 输出索引动作所需报告
{'report_label': 'ch10_mlp_one_update', 'loss': 0.6881638222083897, 'gradients': {'grad_w': array([-0.04962604, -0.09925208]), 'grad_b': -0.04962604167707618, 'grad_v': -0.2611782492944771, 'grad_c': -0.49750210203324385}, 'updated_probability': 0.5191443062393047}

列表 10.1 是索引所列核心动作的本地 setup → consumer → report 接口:前六个赋值定义 ch10_mlp_one_update_setup,其后前向、反向与更新构成 ch10_mlp_one_update_consumer,唯一验收标签为输出中的 ch10_mlp_one_update。它只依赖 numpy,可在 fresh kernel 单独运行,不借用正文其他对象。

10.3.3.2 CNN 前必须掌握的基本训练逻辑

  1. 先定义 estimand 与基线:分类至少与训练期多数类或逻辑回归比较;回归至少与训练均值或 last-value 比较。
  2. 再锁定数据边界:训练集拟合权重与标准化器,验证集选结构、学习率和停止轮次,测试集只在全部选择冻结后使用一次。
  3. 用小批量反复更新:一个 epoch 是训练样本的一次遍历;mini-batch 梯度是总体梯度的有噪声近似。
  4. 同时看优化与泛化:训练损失不降提示学习率、缩放或梯度问题;训练降而验证升提示过拟合;两者都高提示欠拟合。
  5. 记录失败模式:随机种子、参数量、各集合 \(n\)、类别比例、停止轮次和基线差异必须随结果交付。

只有能复算 小节 10.3.3.1 并解释上述五点后,CNN 的局部连接、RNN 的状态传递才是“结构扩展”,而非一组孤立 API。

10.4 卷积神经网络(CNN)

卷积神经网络是专门为处理网格结构数据的神经网络。CNN 的核心思想是局部连接权重共享。卷积层对平移是等变的:输入平移会使特征图相应平移;分类层、池化与数据增强可以提高近似平移不变性,但不应把两者混为一谈。

10.4.1 CNN的架构

一个典型的CNN由以下几种层组成:

  1. 卷积层: 使用卷积核滤波器从输入中提取特征
  2. 池化层: 降低特征图的维度,减少计算量并控制过拟合
  3. 全连接层: 在最后进行分类或回归

图 10.1 显示了一个用于图像分类的CNN架构。

当我们从表格那样的一维结构化数据,跨越到如同汪洋大海般包含数百万像素的二维图像数据时,传统的全连接神经网络会瞬间因为参数量爆炸而崩溃。为了让机器“学会看图”,深度学习界祭出了计算机视觉领域最具统治力的架构——卷积神经网络(CNN)。 下面的架构草图用 Python 渲染展示了一个经典的 CNN 骨架。你可以看到,信息在网络中不再是平铺直叙地流动,而是经过了一层层类似“滤镜”的物理挤压。 左侧蓝色的原始图像输入后,首先迎来了红色的卷积层。在这里,无数个小巧的“卷积核”像手电筒的光斑一样在图像上滑动扫描,局部连接与参数共享显著压缩了参数量,并使边缘、纹理等特征对平移保持等变性:输入平移时,特征图也相应平移。近似平移不变性还需要池化、全局聚合或数据增强等环节配合。 紧接着的是绿色的池化层,它通过简单粗暴地保留局部最大值(Max Pooling)来直接砍掉一半的分辨率,这种极其暴力的降维手段不仅大幅减少了计算量,还进一步增强了模型对图像细微形变和扭曲的容忍度。 在经过多次卷积和池化的交替折磨后,原本宽大的二维图像被提取成了极其深邃且抽象的紫色的展平层一维向量,并最终送入橙色的传统全连接层进行逻辑整合,由最右侧的绿松石色Softmax 层输出最终的分类概率。在金融领域,类似的架构可以应用于K线图模式识别、财务报表图表分析等场景。

import numpy as np  # 数值计算库
import matplotlib.pyplot as plt  # 绘图库

# 定义CNN各层的参数:名称、类型、尺寸及其他超参数
layers = [  # CNN层参数列表
    {'name': '输入层', 'type': 'input', 'size': 32, 'channels': 3},  # 输入层:32×32像素RGB图像
    {'name': '卷积层1', 'type': 'conv', 'size': 32, 'filters': 6, 'kernel': 3},  # 第一卷积层:6个3×3滤波器
    {'name': '池化层1', 'type': 'pool', 'size': 16},  # 第一池化层:特征图降至16×16
    {'name': '卷积层2', 'type': 'conv', 'size': 16, 'filters': 12, 'kernel': 3},  # 第二卷积层:12个3×3滤波器
    {'name': '池化层2', 'type': 'pool', 'size': 8},  # 第二池化层:特征图降至8×8
    {'name': '展平层', 'type': 'flatten', 'size': 8},  # 展平层:将2D特征图转为1D向量
    {'name': '全连接层', 'type': 'fc', 'size': 64},  # 全连接层:64个神经元
    {'name': '输出层', 'type': 'output', 'size': 10}  # 输出层:10个类别(0-9)
]  # CNN层超参数定义完成

以下代码定义了一个辅助函数,用于在给定的坐标轴上逐层绘制CNN网络架构示意图。

def draw_input_layer(ax, layer, y):
    rect = plt.Rectangle((8, y - 0.5), 4, 1, facecolor='#3498db', edgecolor='black', linewidth=2)
    ax.add_patch(rect)
    ax.text(10, y, f'输入\n{layer["size"]}×{layer["size"]}×{layer["channels"]}',
            ha='center', va='center', fontsize=10, color='white', weight='bold')
def draw_conv_layer(ax, layer, y):
    for filter_index in range(layer['filters']):
        rect = plt.Rectangle((7 + filter_index * 0.5, y - 0.3), 0.8, 0.6,
                             facecolor='#e74c3c', edgecolor='black', linewidth=1)
        ax.add_patch(rect)
    ax.text(14, y, f'卷积\n{layer["filters"]}个滤波器', ha='left', va='center', fontsize=9)
def draw_pool_layer(ax, layer, y):
    rect = plt.Rectangle((9, y - 0.3), 2, 0.6, facecolor='#2ecc71', edgecolor='black', linewidth=2)
    ax.add_patch(rect)
    ax.text(14, y, f'池化\n{layer["size"]}×{layer["size"]}', ha='left', va='center', fontsize=9)
def draw_flatten_layer(ax, layer, y):
    rect = plt.Rectangle((7, y - 0.15), 6, 0.3, facecolor='#9b59b6', edgecolor='black', linewidth=2)
    ax.add_patch(rect)
    ax.text(14, y, f'展平\n{layer["size"]}×{layer["size"]}×12', ha='left', va='center', fontsize=9)
def draw_dense_layer(ax, layer, y):
    rect = plt.Rectangle((7.5, y - 0.5), 5, 1, facecolor='#f39c12', edgecolor='black', linewidth=2)
    ax.add_patch(rect)
    ax.text(10, y, f'全连接\n{layer["size"]}个单元', ha='center', va='center',
            fontsize=10, color='white', weight='bold')
    for node_index in range(5):
        ax.add_patch(plt.Circle((8 + node_index, y), 0.15, facecolor='white', edgecolor='black'))
def draw_output_layer(ax, layer, y):
    rect = plt.Rectangle((8, y - 0.4), 4, 0.8, facecolor='#1abc9c', edgecolor='black', linewidth=2)
    ax.add_patch(rect)
    ax.text(10, y, f'Softmax\n{layer["size"]}个类别', ha='center', va='center',
            fontsize=10, color='white', weight='bold')
layer_drawers = {
    'input': draw_input_layer, 'conv': draw_conv_layer, 'pool': draw_pool_layer,
    'flatten': draw_flatten_layer, 'fc': draw_dense_layer, 'output': draw_output_layer,
}

def draw_cnn_architecture(ax, layers):
    y_position = len(layers) - 1
    for layer_index, layer in enumerate(layers):
        layer_drawers[layer['type']](ax, layer, y_position - 2 * layer_index)
        if layer_index > 0:
            current_y = y_position - 2 * layer_index
            ax.add_line(plt.Line2D([10, 10], [current_y + 0.5, current_y + 1.5], color='black', linewidth=2, alpha=0.5))
    return y_position

以下代码调用上述函数创建并显示完整的CNN架构可视化图。

plt.rcParams['font.sans-serif'] = ['Source Han Serif SC']  # 使用系统已安装的思源黑体显示中文
plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示问题
fig, ax = plt.subplots(figsize=(14, 10))  # 创建14×10英寸的画布和坐标轴
y_pos = draw_cnn_architecture(ax, layers)  # 调用辅助函数绘制CNN各层架构
ax.set_xlim(0, 20)  # 设置x轴范围为0到20
ax.set_ylim(-1, y_pos + 1)  # 设置y轴范围覆盖所有层的绘制区域
ax.set_aspect('equal')  # 设置坐标轴等比例显示
ax.axis('off')  # 隐藏坐标轴刻度和边框
ax.set_title('卷积神经网络(CNN)架构', fontsize=16, fontname='Source Han Serif SC', pad=20)  # 使用已安装中文字体设置标题
plt.tight_layout()  # 自动调整子图间距
plt.show()  # 显示最终的CNN架构图
从左至右的层级框图依次显示彩色输入、两组卷积与池化、展平、全连接和十类输出,并标注各层尺寸。
图 10.1: 用于图像分类的卷积神经网络架构示例。该网络包含两个卷积层,每个后面跟着一个最大池化层,最后是全连接层和输出层。

10.4.2 卷积操作

卷积层使用一组可学习的滤波器(也称为卷积核)从输入中提取特征。对于一个二维输入I\(和一个H \times W\)的滤波器\(K\),卷积操作定义为

\[ (I * K)_{i,j} = \sum_{m=0}^{H-1} \sum_{n=0}^{W-1} I_{i+m, j+n} K_{m, n} \tag{10.9}\]

深度学习库通常实现互相关:滤波器不翻转;工程文献习惯把该运算称为卷积。“valid”描述是否补零及输出尺寸,不是互相关的同义词。

10.4.3 池化操作

池化层用于降低特征图的维度最常见的池化操作是最大池化:

\[ \text{MaxPool}(I)_{i,j} = \max_{(m,n) \in R_{i,j}} I_{m,n} \tag{10.10}\]

其中 \(R_{i,j}\)\((i,j)\) 位置附近的局部区域,通常是 \(2 \times 2\) 窗口。最大池化降低维度,并可提高对小幅平移的稳定性,但不保证对任意平移严格不变。

10.4.4 案例:手写数字识别(MNIST)

我们使用MNIST手写数字数据集来演示CNN的应用。MNIST 数据集包含 60,000 个训练样本和 10,000 个测试样本,每个样本是 \(28 \times 28\) 的灰度图像。

下面用 TensorFlow/Keras 在 MNIST 上构造一个紧凑 CNN。卷积、池化和全连接层的作用由结构与运行日志解释;ReLU 只能缓解部分优化问题,不能保证消除梯度消失。三个 epoch 是受课堂预算约束的演示设置,不预先保证准确率,也不构成 CNN 普遍优于其他方法的证据。训练完成后应同时读取验证曲线、测试指标和明确的简单基线,再决定本次运行是否提供增量证据。

import os  # 导入 os 用于在导入 TensorFlow 之前设置环境变量
import numpy as np  # 导入numpy用于数值计算
import matplotlib.pyplot as plt  # 导入matplotlib用于数据可视化
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'  # 屏蔽 TensorFlow 的 INFO/WARNING 级别底层日志
os.environ['TF_ENABLE_ONEDNN_OPTS'] = '0'  # 关闭 oneDNN 提示日志,减少非关键告警输出
os.environ['CUDA_VISIBLE_DEVICES'] = '-1'  # 显式禁用 GPU 探测,避免 CUDA 初始化失败日志
stderr_backup_fd = os.dup(2)  # 备份当前标准错误文件描述符,便于导入 TensorFlow 后恢复
with open(os.devnull, 'w') as null_stderr:  # 打开空设备文件,用于临时吞掉 TensorFlow 的底层启动日志
    os.dup2(null_stderr.fileno(), 2)  # 将进程级标准错误重定向到空设备,覆盖 C/C++ 层日志输出
    try:  # 尝试在静默状态下导入 TensorFlow 相关模块
        from tensorflow import keras  # 导入keras深度学习框架
        from tensorflow.keras import layers, models  # 导入神经网络层和模型构建工具
    finally:  # 无论导入是否成功,都恢复原始标准错误输出
        os.dup2(stderr_backup_fd, 2)  # 恢复原始标准错误文件描述符,避免影响后续代码块输出
        os.close(stderr_backup_fd)  # 关闭备份文件描述符,避免文件描述符泄漏
列表 10.2: MNIST 预处理与 CNN 结构
# 加载MNIST手写数字数据集(含60000训练样本和10000测试样本)
mnist = keras.datasets.mnist  # 获取MNIST数据集对象
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()  # 解包训练集和测试集

# 预处理数据:调整形状为(样本数, 28, 28, 1)并归一化像素值到[0,1]
train_images = train_images.reshape((60000, 28, 28, 1)).astype('float32') / 255  # 训练集reshape并归一化
test_images = test_images.reshape((10000, 28, 28, 1)).astype('float32') / 255  # 测试集reshape并归一化

# 构建CNN模型:两组卷积+池化,最后全连接输出
model = models.Sequential([  # 拟合model模型
    layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)),  # 第一卷积层:32个3×3滤波器
    layers.MaxPooling2D((2, 2)),  # 第一池化层:2×2最大池化
    layers.Conv2D(64, (3, 3), activation='relu'),  # 第二卷积层:64个3×3滤波器
    layers.MaxPooling2D((2, 2)),  # 第二池化层:2×2最大池化
    layers.Conv2D(64, (3, 3), activation='relu'),  # 第三卷积层:64个3×3滤波器
    layers.Flatten(),  # 展平层:将多维特征图转为一维向量
    layers.Dense(64, activation='relu'),  # 全连接层:64个神经元,ReLU激活
    layers.Dense(10, activation='softmax')  # 输出层:10个类别的softmax概率
])  # 完成构建

# 编译模型:指定优化器、损失函数和评估指标
model.compile(optimizer='adam',  # Adam自适应学习率优化器
              loss='sparse_categorical_crossentropy',  # 稀疏分类交叉熵损失(标签为整数形式)
              metrics=['accuracy'])  # 监控准确率指标

# 训练模型(为节省计算时间仅训练3个epoch;生产环境建议10-20个epoch)
print('开始训练CNN模型...')  # 输出训练开始提示
history = model.fit(train_images, train_labels, epochs=3,  # 在训练集上训练3轮
                    batch_size=64, validation_split=0.2, verbose=1)  # 批大小64,20%数据用于验证
开始训练CNN模型...
Epoch 1/3
/tmp/tmp.CfULtrnrMQ/peter-validation/lib/python3.10/site-packages/keras/src/layers/convolutional/base_conv.py:113: UserWarning: Do not pass an `input_shape`/`input_dim` argument to a layer. When using Sequential models, prefer using an `Input(shape)` object as the first layer in the model instead.
  super().__init__(activity_regularizer=activity_regularizer, **kwargs)
  1/750 ━━━━━━━━━━━━━━━━━━━━ 9:06 730ms/step - accuracy: 0.0625 - loss: 2.3166

  6/750 ━━━━━━━━━━━━━━━━━━━━ 7s 11ms/step - accuracy: 0.1512 - loss: 2.2879   

 12/750 ━━━━━━━━━━━━━━━━━━━━ 7s 10ms/step - accuracy: 0.2157 - loss: 2.2499

 18/750 ━━━━━━━━━━━━━━━━━━━━ 7s 10ms/step - accuracy: 0.2595 - loss: 2.1946

 24/750 ━━━━━━━━━━━━━━━━━━━━ 6s 10ms/step - accuracy: 0.2964 - loss: 2.1256

 30/750 ━━━━━━━━━━━━━━━━━━━━ 6s 10ms/step - accuracy: 0.3316 - loss: 2.0477

 36/750 ━━━━━━━━━━━━━━━━━━━━ 6s 10ms/step - accuracy: 0.3643 - loss: 1.9665

 42/750 ━━━━━━━━━━━━━━━━━━━ 6s 10ms/step - accuracy: 0.3929 - loss: 1.8897

 48/750 ━━━━━━━━━━━━━━━━━━━ 6s 10ms/step - accuracy: 0.4182 - loss: 1.8192

 54/750 ━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.4410 - loss: 1.7535 

 60/750 ━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.4618 - loss: 1.6927

 66/750 ━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.4806 - loss: 1.6369

 72/750 ━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.4978 - loss: 1.5855

 78/750 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.5134 - loss: 1.5383

 83/750 ━━━━━━━━━━━━━━━━━━━━ 6s 10ms/step - accuracy: 0.5255 - loss: 1.5017

 89/750 ━━━━━━━━━━━━━━━━━━━━ 6s 10ms/step - accuracy: 0.5391 - loss: 1.4607

 95/750 ━━━━━━━━━━━━━━━━━━━━ 6s 10ms/step - accuracy: 0.5516 - loss: 1.4226

101/750 ━━━━━━━━━━━━━━━━━━━━ 6s 10ms/step - accuracy: 0.5633 - loss: 1.3870

107/750 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.5742 - loss: 1.3537 

113/750 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.5844 - loss: 1.3226

119/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.5939 - loss: 1.2933

124/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.6015 - loss: 1.2703

129/750 ━━━━━━━━━━━━━━━━━━━━ 5s 10ms/step - accuracy: 0.6086 - loss: 1.2484

135/750 ━━━━━━━━━━━━━━━━━━━━ 5s 10ms/step - accuracy: 0.6167 - loss: 1.2234

141/750 ━━━━━━━━━━━━━━━━━━━━ 5s 10ms/step - accuracy: 0.6245 - loss: 1.1996

147/750 ━━━━━━━━━━━━━━━━━━━━ 5s 10ms/step - accuracy: 0.6319 - loss: 1.1770

153/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.6388 - loss: 1.1555 

159/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.6455 - loss: 1.1351

165/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.6517 - loss: 1.1157

171/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.6577 - loss: 1.0971

177/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.6634 - loss: 1.0794

183/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.6689 - loss: 1.0624

189/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.6741 - loss: 1.0461

195/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.6791 - loss: 1.0305

201/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.6839 - loss: 1.0155

207/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.6885 - loss: 1.0011

213/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.6929 - loss: 0.9873

219/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.6972 - loss: 0.9738

225/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.7013 - loss: 0.9609

231/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.7053 - loss: 0.9484

237/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.7091 - loss: 0.9363

243/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.7128 - loss: 0.9246

249/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.7164 - loss: 0.9132

255/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.7199 - loss: 0.9023

261/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.7232 - loss: 0.8917

267/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.7265 - loss: 0.8814

273/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.7296 - loss: 0.8715

279/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.7327 - loss: 0.8618

285/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.7357 - loss: 0.8523

291/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.7386 - loss: 0.8432

297/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.7414 - loss: 0.8343

303/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.7441 - loss: 0.8257

309/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.7468 - loss: 0.8173

315/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.7493 - loss: 0.8091

321/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.7518 - loss: 0.8012

326/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.7539 - loss: 0.7947

331/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.7559 - loss: 0.7884

337/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.7582 - loss: 0.7810

343/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.7605 - loss: 0.7737

349/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.7627 - loss: 0.7667

355/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.7649 - loss: 0.7598

361/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.7670 - loss: 0.7531

367/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.7690 - loss: 0.7465

373/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.7710 - loss: 0.7401

379/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.7730 - loss: 0.7338

385/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.7749 - loss: 0.7277

391/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.7768 - loss: 0.7217

397/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.7786 - loss: 0.7158

403/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.7804 - loss: 0.7100

409/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.7822 - loss: 0.7044

415/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.7839 - loss: 0.6989

421/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.7856 - loss: 0.6935

427/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.7873 - loss: 0.6882

433/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.7889 - loss: 0.6830

439/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.7905 - loss: 0.6779

445/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.7920 - loss: 0.6729

451/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.7936 - loss: 0.6680

457/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.7950 - loss: 0.6632

463/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.7965 - loss: 0.6585

469/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.7979 - loss: 0.6539

475/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.7994 - loss: 0.6494

481/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.8008 - loss: 0.6449

486/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.8019 - loss: 0.6413

491/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.8030 - loss: 0.6377

496/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.8041 - loss: 0.6342

502/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.8054 - loss: 0.6300

507/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.8065 - loss: 0.6265

512/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.8075 - loss: 0.6231

517/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.8086 - loss: 0.6198

522/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.8096 - loss: 0.6165

527/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.8106 - loss: 0.6133

532/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.8116 - loss: 0.6101

536/750 ━━━━━━━━━━━━━━━━━━━━ 2s 10ms/step - accuracy: 0.8124 - loss: 0.6075

540/750 ━━━━━━━━━━━━━━━━━━━━ 2s 10ms/step - accuracy: 0.8132 - loss: 0.6050

545/750 ━━━━━━━━━━━━━━━━━━━━ 1s 10ms/step - accuracy: 0.8141 - loss: 0.6019

550/750 ━━━━━━━━━━━━━━━━━━━━ 1s 10ms/step - accuracy: 0.8151 - loss: 0.5988

555/750 ━━━━━━━━━━━━━━━━━━━━ 1s 10ms/step - accuracy: 0.8160 - loss: 0.5958

560/750 ━━━━━━━━━━━━━━━━━━━━ 1s 10ms/step - accuracy: 0.8170 - loss: 0.5928

565/750 ━━━━━━━━━━━━━━━━━━━━ 1s 10ms/step - accuracy: 0.8179 - loss: 0.5899

570/750 ━━━━━━━━━━━━━━━━━━━━ 1s 10ms/step - accuracy: 0.8188 - loss: 0.5870

576/750 ━━━━━━━━━━━━━━━━━━━━ 1s 10ms/step - accuracy: 0.8198 - loss: 0.5836

581/750 ━━━━━━━━━━━━━━━━━━━━ 1s 10ms/step - accuracy: 0.8207 - loss: 0.5807

586/750 ━━━━━━━━━━━━━━━━━━━━ 1s 10ms/step - accuracy: 0.8216 - loss: 0.5780

592/750 ━━━━━━━━━━━━━━━━━━━━ 1s 10ms/step - accuracy: 0.8226 - loss: 0.5747

598/750 ━━━━━━━━━━━━━━━━━━━━ 1s 10ms/step - accuracy: 0.8236 - loss: 0.5714

604/750 ━━━━━━━━━━━━━━━━━━━━ 1s 10ms/step - accuracy: 0.8246 - loss: 0.5682

610/750 ━━━━━━━━━━━━━━━━━━━━ 1s 10ms/step - accuracy: 0.8256 - loss: 0.5651

616/750 ━━━━━━━━━━━━━━━━━━━━ 1s 10ms/step - accuracy: 0.8265 - loss: 0.5620

622/750 ━━━━━━━━━━━━━━━━━━━━ 1s 10ms/step - accuracy: 0.8275 - loss: 0.5590

628/750 ━━━━━━━━━━━━━━━━━━━━ 1s 10ms/step - accuracy: 0.8284 - loss: 0.5560

634/750 ━━━━━━━━━━━━━━━━━━━━ 1s 10ms/step - accuracy: 0.8293 - loss: 0.5530

640/750 ━━━━━━━━━━━━━━━━━━━━ 1s 10ms/step - accuracy: 0.8302 - loss: 0.5501

646/750 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - accuracy: 0.8311 - loss: 0.5472

651/750 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - accuracy: 0.8319 - loss: 0.5448

656/750 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - accuracy: 0.8326 - loss: 0.5425

662/750 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - accuracy: 0.8335 - loss: 0.5397

668/750 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - accuracy: 0.8343 - loss: 0.5370

674/750 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - accuracy: 0.8351 - loss: 0.5342

680/750 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - accuracy: 0.8360 - loss: 0.5316

686/750 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - accuracy: 0.8368 - loss: 0.5289

692/750 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - accuracy: 0.8376 - loss: 0.5263

698/750 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - accuracy: 0.8384 - loss: 0.5238

703/750 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - accuracy: 0.8391 - loss: 0.5217

708/750 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - accuracy: 0.8397 - loss: 0.5196

714/750 ━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - accuracy: 0.8405 - loss: 0.5171

719/750 ━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - accuracy: 0.8411 - loss: 0.5151

725/750 ━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - accuracy: 0.8419 - loss: 0.5126

730/750 ━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - accuracy: 0.8425 - loss: 0.5106

735/750 ━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - accuracy: 0.8431 - loss: 0.5087

740/750 ━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - accuracy: 0.8437 - loss: 0.5067

745/750 ━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - accuracy: 0.8443 - loss: 0.5048

750/750 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - accuracy: 0.8449 - loss: 0.5029

750/750 ━━━━━━━━━━━━━━━━━━━━ 9s 11ms/step - accuracy: 0.9335 - loss: 0.2171 - val_accuracy: 0.9812 - val_loss: 0.0674

Epoch 2/3


  1/750 ━━━━━━━━━━━━━━━━━━━━ 19s 26ms/step - accuracy: 1.0000 - loss: 0.0066

  6/750 ━━━━━━━━━━━━━━━━━━━━ 8s 11ms/step - accuracy: 0.9970 - loss: 0.0371 

 11/750 ━━━━━━━━━━━━━━━━━━━━ 8s 11ms/step - accuracy: 0.9920 - loss: 0.0474

 16/750 ━━━━━━━━━━━━━━━━━━━━ 8s 11ms/step - accuracy: 0.9889 - loss: 0.0523

 21/750 ━━━━━━━━━━━━━━━━━━━━ 8s 11ms/step - accuracy: 0.9875 - loss: 0.0545

 26/750 ━━━━━━━━━━━━━━━━━━━━ 7s 11ms/step - accuracy: 0.9859 - loss: 0.0566

 31/750 ━━━━━━━━━━━━━━━━━━━━ 7s 11ms/step - accuracy: 0.9847 - loss: 0.0585

 37/750 ━━━━━━━━━━━━━━━━━━━━ 7s 11ms/step - accuracy: 0.9834 - loss: 0.0610

 42/750 ━━━━━━━━━━━━━━━━━━━ 7s 11ms/step - accuracy: 0.9826 - loss: 0.0627

 48/750 ━━━━━━━━━━━━━━━━━━━ 7s 11ms/step - accuracy: 0.9817 - loss: 0.0643

 54/750 ━━━━━━━━━━━━━━━━━━━ 7s 11ms/step - accuracy: 0.9810 - loss: 0.0653

 59/750 ━━━━━━━━━━━━━━━━━━━ 7s 11ms/step - accuracy: 0.9806 - loss: 0.0658

 64/750 ━━━━━━━━━━━━━━━━━━━ 7s 11ms/step - accuracy: 0.9803 - loss: 0.0662

 70/750 ━━━━━━━━━━━━━━━━━━━ 7s 11ms/step - accuracy: 0.9800 - loss: 0.0666

 75/750 ━━━━━━━━━━━━━━━━━━━━ 7s 11ms/step - accuracy: 0.9797 - loss: 0.0670

 80/750 ━━━━━━━━━━━━━━━━━━━━ 7s 11ms/step - accuracy: 0.9794 - loss: 0.0673

 86/750 ━━━━━━━━━━━━━━━━━━━━ 6s 11ms/step - accuracy: 0.9792 - loss: 0.0676

 92/750 ━━━━━━━━━━━━━━━━━━━━ 6s 10ms/step - accuracy: 0.9790 - loss: 0.0679

 97/750 ━━━━━━━━━━━━━━━━━━━━ 6s 10ms/step - accuracy: 0.9789 - loss: 0.0680

103/750 ━━━━━━━━━━━━━━━━━━━━ 6s 10ms/step - accuracy: 0.9788 - loss: 0.0680

108/750 ━━━━━━━━━━━━━━━━━━━━ 6s 10ms/step - accuracy: 0.9787 - loss: 0.0680

114/750 ━━━━━━━━━━━━━━━━━━━━ 6s 10ms/step - accuracy: 0.9787 - loss: 0.0680

120/750 ━━━━━━━━━━━━━━━━━━━━ 6s 10ms/step - accuracy: 0.9786 - loss: 0.0679

126/750 ━━━━━━━━━━━━━━━━━━━━ 6s 10ms/step - accuracy: 0.9786 - loss: 0.0679

132/750 ━━━━━━━━━━━━━━━━━━━━ 6s 10ms/step - accuracy: 0.9785 - loss: 0.0679

138/750 ━━━━━━━━━━━━━━━━━━━━ 6s 10ms/step - accuracy: 0.9785 - loss: 0.0679

144/750 ━━━━━━━━━━━━━━━━━━━━ 6s 10ms/step - accuracy: 0.9785 - loss: 0.0680

150/750 ━━━━━━━━━━━━━━━━━━━━ 6s 10ms/step - accuracy: 0.9785 - loss: 0.0680

156/750 ━━━━━━━━━━━━━━━━━━━━ 5s 10ms/step - accuracy: 0.9784 - loss: 0.0681

162/750 ━━━━━━━━━━━━━━━━━━━━ 5s 10ms/step - accuracy: 0.9784 - loss: 0.0681

168/750 ━━━━━━━━━━━━━━━━━━━━ 5s 10ms/step - accuracy: 0.9784 - loss: 0.0681

174/750 ━━━━━━━━━━━━━━━━━━━━ 5s 10ms/step - accuracy: 0.9784 - loss: 0.0681

180/750 ━━━━━━━━━━━━━━━━━━━━ 5s 10ms/step - accuracy: 0.9784 - loss: 0.0681

186/750 ━━━━━━━━━━━━━━━━━━━━ 5s 10ms/step - accuracy: 0.9784 - loss: 0.0681

191/750 ━━━━━━━━━━━━━━━━━━━━ 5s 10ms/step - accuracy: 0.9784 - loss: 0.0681

196/750 ━━━━━━━━━━━━━━━━━━━━ 5s 10ms/step - accuracy: 0.9784 - loss: 0.0681

202/750 ━━━━━━━━━━━━━━━━━━━━ 5s 10ms/step - accuracy: 0.9784 - loss: 0.0681

208/750 ━━━━━━━━━━━━━━━━━━━━ 5s 10ms/step - accuracy: 0.9784 - loss: 0.0681

214/750 ━━━━━━━━━━━━━━━━━━━━ 5s 10ms/step - accuracy: 0.9784 - loss: 0.0681

220/750 ━━━━━━━━━━━━━━━━━━━━ 5s 10ms/step - accuracy: 0.9784 - loss: 0.0681

226/750 ━━━━━━━━━━━━━━━━━━━━ 5s 10ms/step - accuracy: 0.9784 - loss: 0.0681

232/750 ━━━━━━━━━━━━━━━━━━━━ 5s 10ms/step - accuracy: 0.9784 - loss: 0.0681

238/750 ━━━━━━━━━━━━━━━━━━━━ 5s 10ms/step - accuracy: 0.9784 - loss: 0.0680

244/750 ━━━━━━━━━━━━━━━━━━━━ 4s 10ms/step - accuracy: 0.9785 - loss: 0.0679

250/750 ━━━━━━━━━━━━━━━━━━━━ 4s 10ms/step - accuracy: 0.9785 - loss: 0.0679

256/750 ━━━━━━━━━━━━━━━━━━━━ 4s 10ms/step - accuracy: 0.9785 - loss: 0.0678

262/750 ━━━━━━━━━━━━━━━━━━━━ 4s 10ms/step - accuracy: 0.9785 - loss: 0.0677

267/750 ━━━━━━━━━━━━━━━━━━━━ 4s 10ms/step - accuracy: 0.9786 - loss: 0.0677

272/750 ━━━━━━━━━━━━━━━━━━━━ 4s 10ms/step - accuracy: 0.9786 - loss: 0.0676

278/750 ━━━━━━━━━━━━━━━━━━━━ 4s 10ms/step - accuracy: 0.9786 - loss: 0.0675

284/750 ━━━━━━━━━━━━━━━━━━━━ 4s 10ms/step - accuracy: 0.9786 - loss: 0.0675

290/750 ━━━━━━━━━━━━━━━━━━━━ 4s 10ms/step - accuracy: 0.9786 - loss: 0.0674

296/750 ━━━━━━━━━━━━━━━━━━━━ 4s 10ms/step - accuracy: 0.9787 - loss: 0.0673

302/750 ━━━━━━━━━━━━━━━━━━━━ 4s 10ms/step - accuracy: 0.9787 - loss: 0.0673

308/750 ━━━━━━━━━━━━━━━━━━━━ 4s 10ms/step - accuracy: 0.9787 - loss: 0.0672

314/750 ━━━━━━━━━━━━━━━━━━━━ 4s 10ms/step - accuracy: 0.9787 - loss: 0.0672

320/750 ━━━━━━━━━━━━━━━━━━━━ 4s 10ms/step - accuracy: 0.9788 - loss: 0.0671

325/750 ━━━━━━━━━━━━━━━━━━━━ 4s 10ms/step - accuracy: 0.9788 - loss: 0.0671

330/750 ━━━━━━━━━━━━━━━━━━━━ 4s 10ms/step - accuracy: 0.9788 - loss: 0.0670

336/750 ━━━━━━━━━━━━━━━━━━━━ 4s 10ms/step - accuracy: 0.9788 - loss: 0.0670

342/750 ━━━━━━━━━━━━━━━━━━━━ 3s 10ms/step - accuracy: 0.9788 - loss: 0.0669

348/750 ━━━━━━━━━━━━━━━━━━━━ 3s 10ms/step - accuracy: 0.9788 - loss: 0.0669

354/750 ━━━━━━━━━━━━━━━━━━━━ 3s 10ms/step - accuracy: 0.9788 - loss: 0.0668

360/750 ━━━━━━━━━━━━━━━━━━━━ 3s 10ms/step - accuracy: 0.9789 - loss: 0.0668

366/750 ━━━━━━━━━━━━━━━━━━━━ 3s 10ms/step - accuracy: 0.9789 - loss: 0.0667

372/750 ━━━━━━━━━━━━━━━━━━━━ 3s 10ms/step - accuracy: 0.9789 - loss: 0.0666

378/750 ━━━━━━━━━━━━━━━━━━━━ 3s 10ms/step - accuracy: 0.9789 - loss: 0.0666

384/750 ━━━━━━━━━━━━━━━━━━━━ 3s 10ms/step - accuracy: 0.9790 - loss: 0.0665

390/750 ━━━━━━━━━━━━━━━━━━━━ 3s 10ms/step - accuracy: 0.9790 - loss: 0.0664

396/750 ━━━━━━━━━━━━━━━━━━━━ 3s 10ms/step - accuracy: 0.9790 - loss: 0.0663

402/750 ━━━━━━━━━━━━━━━━━━━━ 3s 10ms/step - accuracy: 0.9790 - loss: 0.0662

408/750 ━━━━━━━━━━━━━━━━━━━━ 3s 10ms/step - accuracy: 0.9790 - loss: 0.0662

414/750 ━━━━━━━━━━━━━━━━━━━━ 3s 10ms/step - accuracy: 0.9791 - loss: 0.0661

420/750 ━━━━━━━━━━━━━━━━━━━━ 3s 10ms/step - accuracy: 0.9791 - loss: 0.0660

426/750 ━━━━━━━━━━━━━━━━━━━━ 3s 10ms/step - accuracy: 0.9791 - loss: 0.0659

432/750 ━━━━━━━━━━━━━━━━━━━━ 3s 10ms/step - accuracy: 0.9791 - loss: 0.0659

438/750 ━━━━━━━━━━━━━━━━━━━━ 2s 10ms/step - accuracy: 0.9792 - loss: 0.0658

444/750 ━━━━━━━━━━━━━━━━━━━━ 2s 10ms/step - accuracy: 0.9792 - loss: 0.0657

450/750 ━━━━━━━━━━━━━━━━━━━━ 2s 10ms/step - accuracy: 0.9792 - loss: 0.0656

456/750 ━━━━━━━━━━━━━━━━━━━━ 2s 10ms/step - accuracy: 0.9792 - loss: 0.0656

462/750 ━━━━━━━━━━━━━━━━━━━━ 2s 10ms/step - accuracy: 0.9792 - loss: 0.0655

468/750 ━━━━━━━━━━━━━━━━━━━━ 2s 10ms/step - accuracy: 0.9793 - loss: 0.0654

474/750 ━━━━━━━━━━━━━━━━━━━━ 2s 10ms/step - accuracy: 0.9793 - loss: 0.0653

480/750 ━━━━━━━━━━━━━━━━━━━━ 2s 10ms/step - accuracy: 0.9793 - loss: 0.0653

486/750 ━━━━━━━━━━━━━━━━━━━━ 2s 10ms/step - accuracy: 0.9793 - loss: 0.0652

492/750 ━━━━━━━━━━━━━━━━━━━━ 2s 10ms/step - accuracy: 0.9794 - loss: 0.0651

498/750 ━━━━━━━━━━━━━━━━━━━━ 2s 10ms/step - accuracy: 0.9794 - loss: 0.0651

504/750 ━━━━━━━━━━━━━━━━━━━━ 2s 10ms/step - accuracy: 0.9794 - loss: 0.0650

510/750 ━━━━━━━━━━━━━━━━━━━━ 2s 10ms/step - accuracy: 0.9794 - loss: 0.0649

516/750 ━━━━━━━━━━━━━━━━━━━━ 2s 10ms/step - accuracy: 0.9794 - loss: 0.0648

522/750 ━━━━━━━━━━━━━━━━━━━━ 2s 10ms/step - accuracy: 0.9795 - loss: 0.0648

528/750 ━━━━━━━━━━━━━━━━━━━━ 2s 10ms/step - accuracy: 0.9795 - loss: 0.0647

534/750 ━━━━━━━━━━━━━━━━━━━━ 2s 10ms/step - accuracy: 0.9795 - loss: 0.0646

540/750 ━━━━━━━━━━━━━━━━━━━━ 1s 10ms/step - accuracy: 0.9795 - loss: 0.0646

546/750 ━━━━━━━━━━━━━━━━━━━━ 1s 10ms/step - accuracy: 0.9795 - loss: 0.0645

552/750 ━━━━━━━━━━━━━━━━━━━━ 1s 10ms/step - accuracy: 0.9796 - loss: 0.0645

558/750 ━━━━━━━━━━━━━━━━━━━━ 1s 10ms/step - accuracy: 0.9796 - loss: 0.0644

564/750 ━━━━━━━━━━━━━━━━━━━━ 1s 10ms/step - accuracy: 0.9796 - loss: 0.0643

570/750 ━━━━━━━━━━━━━━━━━━━━ 1s 10ms/step - accuracy: 0.9796 - loss: 0.0643

576/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9796 - loss: 0.0642 

582/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9796 - loss: 0.0641

588/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9797 - loss: 0.0641

594/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9797 - loss: 0.0640

600/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9797 - loss: 0.0640

606/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9797 - loss: 0.0639

612/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9797 - loss: 0.0638

618/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9798 - loss: 0.0638

624/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9798 - loss: 0.0637

630/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9798 - loss: 0.0637

636/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9798 - loss: 0.0636

642/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9798 - loss: 0.0636

648/750 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9798 - loss: 0.0635

654/750 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9799 - loss: 0.0635

660/750 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9799 - loss: 0.0634

666/750 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9799 - loss: 0.0634

672/750 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9799 - loss: 0.0633

678/750 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9799 - loss: 0.0633

684/750 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9799 - loss: 0.0632

690/750 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9799 - loss: 0.0632

696/750 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9799 - loss: 0.0632

702/750 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9800 - loss: 0.0631

708/750 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9800 - loss: 0.0631

714/750 ━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9800 - loss: 0.0630

720/750 ━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9800 - loss: 0.0630

726/750 ━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9800 - loss: 0.0630

732/750 ━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9800 - loss: 0.0629

738/750 ━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9800 - loss: 0.0629

743/750 ━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9800 - loss: 0.0629

748/750 ━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9800 - loss: 0.0628

750/750 ━━━━━━━━━━━━━━━━━━━━ 8s 10ms/step - accuracy: 0.9816 - loss: 0.0580 - val_accuracy: 0.9856 - val_loss: 0.0501

Epoch 3/3


  1/750 ━━━━━━━━━━━━━━━━━━━━ 16s 22ms/step - accuracy: 0.9688 - loss: 0.0567

  7/750 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9809 - loss: 0.0438  

 13/750 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9835 - loss: 0.0413

 19/750 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9845 - loss: 0.0394

 25/750 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9854 - loss: 0.0375

 31/750 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9860 - loss: 0.0366

 37/750 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9862 - loss: 0.0361

 43/750 ━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9864 - loss: 0.0356

 49/750 ━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9865 - loss: 0.0354

 54/750 ━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9865 - loss: 0.0356

 60/750 ━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9865 - loss: 0.0358

 66/750 ━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9866 - loss: 0.0359

 72/750 ━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9866 - loss: 0.0360

 78/750 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9866 - loss: 0.0361

 84/750 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9866 - loss: 0.0362

 90/750 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9867 - loss: 0.0362

 96/750 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9867 - loss: 0.0361

102/750 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9868 - loss: 0.0362

108/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.9869 - loss: 0.0362

114/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.9869 - loss: 0.0362

120/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.9869 - loss: 0.0363

126/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.9870 - loss: 0.0363

132/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.9870 - loss: 0.0364

138/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.9870 - loss: 0.0365

144/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.9870 - loss: 0.0365

150/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.9871 - loss: 0.0366

156/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.9871 - loss: 0.0367

162/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.9871 - loss: 0.0368

168/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.9871 - loss: 0.0369

174/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.9871 - loss: 0.0370

180/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.9871 - loss: 0.0371

186/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.9871 - loss: 0.0372

192/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.9871 - loss: 0.0373

198/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.9871 - loss: 0.0374

204/750 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.9871 - loss: 0.0375

210/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.9871 - loss: 0.0376

216/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.9871 - loss: 0.0377

222/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.9871 - loss: 0.0377

228/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.9871 - loss: 0.0378

234/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.9871 - loss: 0.0379

240/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.9871 - loss: 0.0379

246/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.9871 - loss: 0.0380

252/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.9871 - loss: 0.0380

258/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.9871 - loss: 0.0380

264/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.9871 - loss: 0.0380

270/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.9872 - loss: 0.0381

276/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.9872 - loss: 0.0381

282/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.9872 - loss: 0.0381

288/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.9872 - loss: 0.0381

294/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.9872 - loss: 0.0382

300/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.9872 - loss: 0.0382

306/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.9872 - loss: 0.0382

312/750 ━━━━━━━━━━━━━━━━━━━━ 4s 9ms/step - accuracy: 0.9872 - loss: 0.0382

318/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.9872 - loss: 0.0382

324/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.9872 - loss: 0.0383

330/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.9872 - loss: 0.0383

336/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.9872 - loss: 0.0383

342/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.9872 - loss: 0.0383

348/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.9873 - loss: 0.0383

354/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.9873 - loss: 0.0383

360/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.9873 - loss: 0.0383

366/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.9873 - loss: 0.0383

372/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.9873 - loss: 0.0383

378/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.9873 - loss: 0.0383

384/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.9873 - loss: 0.0383

390/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.9873 - loss: 0.0383

396/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.9873 - loss: 0.0383

402/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.9874 - loss: 0.0383

408/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.9874 - loss: 0.0383

414/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.9874 - loss: 0.0383

420/750 ━━━━━━━━━━━━━━━━━━━━ 3s 9ms/step - accuracy: 0.9874 - loss: 0.0383

426/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.9874 - loss: 0.0384

432/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.9874 - loss: 0.0384

438/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.9874 - loss: 0.0384

444/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.9874 - loss: 0.0384

450/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.9874 - loss: 0.0384

456/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.9874 - loss: 0.0384

462/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.9874 - loss: 0.0385

468/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.9874 - loss: 0.0385

474/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.9874 - loss: 0.0385

480/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.9874 - loss: 0.0385

486/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.9875 - loss: 0.0385

492/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.9875 - loss: 0.0385

498/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.9875 - loss: 0.0385

504/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.9875 - loss: 0.0385

510/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.9875 - loss: 0.0385

516/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.9875 - loss: 0.0385

522/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.9875 - loss: 0.0385

528/750 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.9875 - loss: 0.0386

534/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9875 - loss: 0.0386

540/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9875 - loss: 0.0386

546/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9875 - loss: 0.0386

552/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9875 - loss: 0.0386

558/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9875 - loss: 0.0386

564/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9875 - loss: 0.0387

570/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9875 - loss: 0.0387

576/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9875 - loss: 0.0387

582/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9875 - loss: 0.0387

588/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9875 - loss: 0.0387

594/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9875 - loss: 0.0387

600/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9875 - loss: 0.0388

605/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9875 - loss: 0.0388

611/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9875 - loss: 0.0388

617/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9875 - loss: 0.0388

623/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9875 - loss: 0.0388

629/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9875 - loss: 0.0388

635/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9875 - loss: 0.0388

641/750 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - accuracy: 0.9875 - loss: 0.0389

647/750 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9875 - loss: 0.0389

653/750 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9875 - loss: 0.0389

659/750 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9875 - loss: 0.0389

665/750 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9875 - loss: 0.0389

671/750 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9875 - loss: 0.0389

677/750 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9875 - loss: 0.0389

683/750 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9875 - loss: 0.0389

689/750 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9875 - loss: 0.0389

695/750 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9876 - loss: 0.0390

701/750 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9876 - loss: 0.0390

707/750 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9876 - loss: 0.0390

713/750 ━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9876 - loss: 0.0390

719/750 ━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9876 - loss: 0.0390

725/750 ━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9876 - loss: 0.0390

731/750 ━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9876 - loss: 0.0390

737/750 ━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9876 - loss: 0.0390

743/750 ━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9876 - loss: 0.0390

749/750 ━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9876 - loss: 0.0390

750/750 ━━━━━━━━━━━━━━━━━━━━ 8s 10ms/step - accuracy: 0.9878 - loss: 0.0398 - val_accuracy: 0.9853 - val_loss: 0.0475

训练日志用于比较训练与验证曲线,具体数值以当前执行输出为准。仅凭三个 epoch 的单次曲线不能证明不存在过拟合;若验证损失开始上升而训练损失继续下降,应使用早停、正则化或更多独立验证来检查泛化。

CNN 模型训练完成后,先用 列表 10.3 留存一次测试访问,再由 图 10.2 展示训练期与验证期曲线;二者分别承担数值审计与图形诊断,不能共用表格标签。

列表 10.3: MNIST 锁定测试集的一次评价记录
test_loss, test_acc = model.evaluate(test_images, test_labels, verbose=0)  # 在测试集上评估模型性能
print(f'\n测试集准确率: {test_acc:.4f}')  # 打印测试集准确率
print(f'测试集损失: {test_loss:.4f}')  # 同时记录与训练目标一致的测试损失

测试集准确率: 0.9876
测试集损失: 0.0343
plt.figure(figsize=(12, 4))  # 创建12×4英寸的宽幅画布

# 左图:准确率曲线
plt.subplot(1, 2, 1)  # 1行2列的第1个子图
plt.plot(history.history['accuracy'], label='训练集准确率')  # 绘制训练集准确率曲线
plt.plot(history.history['val_accuracy'], label='验证集准确率')  # 绘制验证集准确率曲线
plt.xlabel('Epoch', fontsize=12)  # x轴标签:训练轮次
plt.ylabel('准确率', fontsize=12)  # y轴标签:准确率
plt.title('模型准确率', fontsize=14, fontname='Source Han Serif SC')  # 使用已安装中文字体设置子图标题
plt.legend(fontsize=10)  # 添加图例
plt.grid(True, alpha=0.3)  # 添加半透明网格线

# 右图:损失曲线
plt.subplot(1, 2, 2)  # 1行2列的第2个子图
plt.plot(history.history['loss'], label='训练集损失')  # 绘制训练集损失曲线
plt.plot(history.history['val_loss'], label='验证集损失')  # 绘制验证集损失曲线
plt.xlabel('Epoch', fontsize=12)  # x轴标签:训练轮次
plt.ylabel('损失', fontsize=12)  # y轴标签:损失值
plt.title('模型损失', fontsize=14, fontname='Source Han Serif SC')  # 使用已安装中文字体设置子图标题
plt.legend(fontsize=10)  # 添加图例
plt.grid(True, alpha=0.3)  # 添加半透明网格线

plt.tight_layout()  # 自动调整子图间距
plt.show()  # 显示图形
左图为训练与验证准确率随轮次变化,右图为训练与验证损失随轮次变化;两图不包含测试集选模轨迹。
图 10.2: MNIST CNN 的训练期与验证期准确率、损失曲线

测试准确率与损失由 列表 10.3 现场报告。阅读 图 10.2 时应同时关注训练—验证差距、随机初始化和数据集难度;本例只说明该架构在 MNIST 上的一个可复现实验流程,不把一次运行的高分外推到更复杂图像任务。

补充说明:在深度学习里,准确率曲线和损失曲线分别告诉你什么

初学者看训练图时,最容易把“准确率高”直接等同于“模型已经完全学明白了”。实际上,准确率曲线和损失曲线各自传递的信息并不一样。

  • 准确率曲线回答的是:在当前阈值和当前任务定义下,模型判对了多少样本;
  • 损失曲线回答的是:模型给正确类别分配的概率到底有多自信、错误类别分配的概率又有多离谱。

举例来说,两个模型都可能达到99%的准确率,但其中一个对正确类别给出0.99概率,另一个只给0.55概率。它们的准确率一样,可后者显然更不稳定、更容易在稍有扰动时翻车,这种差异往往会反映在损失值上。

因此,在深度学习训练中更完整的阅读顺序是:

  1. 先看训练损失是否稳定下降,判断优化是否正常进行;
  2. 再看验证损失是否同步下降,判断是否开始过拟合;
  3. 最后看准确率是否达到业务可接受水平

对 CNN 来说,这样的训练曲线还有一个额外价值:它帮助我们判断模型问题究竟出在“网络学不会”还是“网络学过头”。如果训练准确率和验证准确率都低,往往是模型容量不足或特征提取不够;如果训练准确率很高、验证准确率明显掉队,则更像是过拟合。

注意:运行时间和计算资源

深度学习模型(尤其是CNN)通常需要较长的训练时间和较多的计算资源。在生产环境中你应该

  1. 使用GPU加速训练可以显著加快训练速度)
  2. 增加训练epoch数量以达到更好的性能
  3. 使用更深的网络和更多的滤波器
  4. 使用数据增强技术来防止过拟合

在这个示例中,为了节省时间和计算资源我们只训练了3个epoch。在实际应用中你可能需要训练0-20个epoch或更多。

10.5 循环神经网络(RNN)

循环神经网络(RNN)是专门用于处理序列数据的神经网络,如时间序列、文本、语音等。与CNN处理空间结构不同,RNN处理时间序列结构。

10.5.1 RNN的基本结构

在一个简单的 RNN 中,输入序列记为 \(x_1,\ldots,x_T\),其中 \(x_t\in\mathbb R^p\);隐藏状态统一记为 \(h_t\in\mathbb R^K\)。第 \(k\) 个隐藏分量为

\[ h_{tk}=g\left(b_k+\sum_{j=1}^{p}W_{kj}x_{tj}+\sum_{s=1}^{K}U_{ks}h_{t-1,s}\right),\qquad k=1,\ldots,K. \tag{10.11}\]

若每个时点都需要一个标量预测,则

\[ \widehat y_t=\beta_0+\sum_{k=1}^{K}\beta_kh_{tk}. \tag{10.12}\]

对只使用末时点输出的标量回归,观测响应为 \(y\),平方损失为

\[ \mathcal L(y,\widehat y_T)=(y-\widehat y_T)^2. \tag{10.13}\]

这里 \(W\in\mathbb R^{K\times p}\)\(U\in\mathbb R^{K\times K}\)\(\beta\in\mathbb R^K\)\(T\) 是序列长度,\(K\) 是隐藏宽度,二者不可混用。

图 10.3 显示了RNN的结构。

一旦我们处理的数据从静态的图像变成了随着时间流逝不断演进的序列(比如一句话中的单词、一段语音,或是股票市场每天的收盘价),那种“一次性把所有特征看全”的前馈网络就显得力不从心了。此时,我们需要给网络装上“记忆”——这便是循环神经网络(RNN)的由来。 在下面这幅略显抽象的网络结构示意图中,左侧是一个被极端压缩的 RNN 核心单元(那个带着自循环箭头的红色方块)。它的精妙之处在于:当每一个新的时间步特征 \(x_t\) 输入时,隐藏层不仅会接收这个新鲜的信号,还会同时读取前一个时间步自己刚刚产生的隐藏状态 \(h_{t-1}\)。 这种将过去的记忆与现在的观测混合处理的机制,在右侧那张“按时间轴展开”的完整逻辑图中表现得淋漓尽致。你可以清晰地看到一条红色的记忆横轴(隐藏层状态)贯穿了整个时间序列,信息像接力棒一样从开头一直传递到了结尾。正是这种能够跨越时间的内部状态流转,使得模型具有了理解“上下文”的超能力。

完成输入序列节点的绘制后,在左图上添加 RNN 循环层模块(红色矩形)、输出节点(绿色圆圈)以及它们之间的连接线,展示 RNN 的紧凑表示形式。下面绘制RNN的展开形式,展示各时间步的输入、隐藏层和输出之间的连接关系。

import numpy as np  # 导入numpy用于数值计算
import matplotlib.pyplot as plt  # 导入matplotlib用于数据可视化

# 创建一行两列的画布:左侧紧凑表示,右侧展开形式
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 8))  # 创建16×8英寸的两子图画布

# 左图:紧凑表示 — 将RNN表示为单个循环模块
ax1.set_xlim(0, 10)  # 设置左图x轴范围
ax1.set_ylim(0, 10)  # 设置左图y轴范围
ax1.axis('off')  # 隐藏左图坐标轴

# 绘制输入序列(4个输入节点X1-X4)
for i in range(4):  # 遍历4个输入时间步
    circle = plt.Circle((1, 8 - i * 2), 0.4, facecolor='#3498db',  # 创建蓝色输入节点圆圈
                       edgecolor='black', linewidth=2)  # 定义edgecolor变量
    ax1.add_patch(circle)  # 将输入节点添加到左图
    ax1.text(1, 8 - i * 2, f'$X_{i+1}$', ha='center', va='center',  # 标注输入节点名称
            fontsize=11, fontweight='bold')  # 定义fontsize变量
    if i < 3:  # 在非最后一个节点之间绘制箭头
        ax1.arrow(1.5, 8 - i * 2, 0, -1.2, head_width=0.15,  # 绘制向下的箭头连接
                 head_length=0.2, fc='black', ec='black')  # 定义head_length变量

# 绘制RNN循环层(红色矩形模块)
rect = plt.Rectangle((4, 6), 2, 3, facecolor='#e74c3c',  # 创建红色RNN层矩形
                     edgecolor='black', linewidth=2)  # 定义edgecolor变量
ax1.add_patch(rect)  # 将RNN层矩形添加到左图
ax1.text(5, 7.5, 'RNN', ha='center', va='center', fontsize=12,  # 标注RNN层名称
        color='white', weight='bold')  # 定义color变量

# 绘制输入节点到RNN层的连接线
for i in range(4):  # 每个输入节点都连接到RNN层
    ax1.plot([1.5, 4], [8 - i * 2, 7.5 - i * 0.5], 'k-',  # 绘制连接线
             linewidth=1.5, alpha=0.5)  # 定义linewidth变量

# 绘制输出节点(绿色圆圈)
circle = plt.Circle((9, 5), 0.4, facecolor='#2ecc71',  # 创建绿色输出节点圆圈
                   edgecolor='black', linewidth=2)  # 定义edgecolor变量
ax1.add_patch(circle)  # 将输出节点添加到左图
ax1.text(9, 5, '$\\hat y_T$', ha='center', va='center', fontsize=11, fontweight='bold')  # 标注末时点预测而非观测响应
ax1.plot([6, 8.6], [7.5, 5], 'k-', linewidth=1.5, alpha=0.5)  # RNN层到输出节点的连接线

ax1.set_title('紧凑表示', fontsize=14, fontname='Source Han Serif SC')  # 使用已安装中文字体设置左图标题

# 右图:展开形式 — 展示各时间步的详细连接
ax2.set_xlim(0, 16)  # 设置右图x轴范围
ax2.set_ylim(0, 10)  # 设置右图y轴范围
ax2.axis('off')  # 隐藏右图坐标轴

# 绘制展开的RNN:4个时间步的完整结构
for i in range(4):  # 遍历4个时间步
    # 绘制输入节点(蓝色圆圈)
    circle = plt.Circle((1 + i * 3.5, 8), 0.4, facecolor='#3498db',  # 创建蓝色输入节点
                       edgecolor='black', linewidth=2)  # 定义edgecolor变量
    ax2.add_patch(circle)  # 将输入节点添加到右图
    ax2.text(1 + i * 3.5, 8, f'$X_{i+1}$', ha='center', va='center',  # 标注输入节点名称
            fontsize=11, fontweight='bold')  # 定义fontsize变量

    # 绘制隐藏层(红色矩形)
    rect = plt.Rectangle((3 + i * 3.5, 5), 2, 3, facecolor='#e74c3c',  # 创建红色隐藏层矩形
                         edgecolor='black', linewidth=2)  # 定义edgecolor变量
    ax2.add_patch(rect)  # 将隐藏层矩形添加到右图
    ax2.text(4 + i * 3.5, 6.5, f'$h_{i+1}$', ha='center', va='center',  # 标注统一记号下的隐藏状态
            fontsize=12, color='white', weight='bold')  # 定义fontsize变量

    # 绘制输出节点(绿色小圆圈)
    circle = plt.Circle((7 + i * 3.5, 8), 0.3, facecolor='#2ecc71',  # 创建绿色输出节点
                       edgecolor='black', linewidth=2)  # 定义edgecolor变量
    ax2.add_patch(circle)  # 将输出节点添加到右图
    ax2.text(7 + i * 3.5, 8, f'$O_{i+1}$', ha='center', va='center',  # 标注输出节点名称
            fontsize=10, fontweight='bold')  # 定义fontsize变量

    # 绘制时间步之间的连接线
    if i < 3:  # 非最后一个时间步
        ax2.plot([1.5 + i * 3.5, 3 + i * 3.5], [8, 7], 'k-',  # 输入到隐藏层的连接
                 linewidth=1.5, alpha=0.5)  # 定义linewidth变量
        ax2.plot([5 + i * 3.5, 6.7 + i * 3.5], [6.5, 8], 'k-',  # 隐藏层到输出的连接
                 linewidth=1.5, alpha=0.5)  # 定义linewidth变量
        ax2.plot([5 + i * 3.5, 3 + (i+1) * 3.5], [6, 7], 'r-',  # 隐藏层之间的递归连接(红色)
                 linewidth=2)  # 定义linewidth变量

    # 最后一个时间步的最终输出连接
    if i == 3:  # 仅在最后一个时间步绘制
        ax2.plot([7 + i * 3.5, 13.5], [8, 5], 'k-',  # 连接到最终输出节点
                 linewidth=2)  # 定义linewidth变量
        circle = plt.Circle((14, 5), 0.4, facecolor='#f39c12',  # 创建黄色最终输出节点
                           edgecolor='black', linewidth=2)  # 定义edgecolor变量
        ax2.add_patch(circle)  # 将最终输出节点添加到右图
        ax2.text(14, 5, '$\\hat y_T$', ha='center', va='center',  # 标注最终预测而非观测响应
                fontsize=12, fontweight='bold')  # 定义fontsize变量

ax2.set_title('展开形式', fontsize=14, fontname='Source Han Serif SC')  # 使用已安装中文字体设置右图标题

plt.suptitle('循环神经网络(RNN)结构', fontsize=16, fontname='Source Han Serif SC', y=0.95)  # 使用已安装中文字体设置总标题
plt.tight_layout()  # 自动调整子图间距
plt.show()  # 显示RNN结构图
左图以自循环方框表示 RNN,右图将四个时间步横向展开,箭头连接相邻隐藏状态及各步输入输出。
图 10.3: 循环神经网络(RNN)的结构示意图。左侧是紧凑表示,右侧是展开形式。网络按顺序处理输入序列,每个时间步的隐藏层接收前一步的激活并产生当前激活。

10.5.2 长短期记忆网络(LSTM)

标准 RNN 在长序列上容易发生梯度消失或爆炸,使网络难以学习长期依赖。长短期记忆网络(LSTM)引入门控与加法细胞状态通路来改善误差传递 (Hochreiter 和 Schmidhuber 1997年);这只能缓解困难,并不保证任意长期依赖一定可学、梯度一定稳定或样本外预测一定改善。

令输入为 \(x_t\in\mathbb R^p\),隐藏状态为 \(h_t\in\mathbb R^K\),细胞状态为 \(c_t\in\mathbb R^K\)。LSTM 的三个门、候选状态与更新方程为

\[ f_t=\sigma(W_fx_t+U_fh_{t-1}+b_f),\qquad i_t=\sigma(W_ix_t+U_ih_{t-1}+b_i), \tag{10.14}\]

\[ \widetilde c_t=\tanh(W_cx_t+U_ch_{t-1}+b_c),\qquad c_t=f_t\odot c_{t-1}+i_t\odot\widetilde c_t, \tag{10.15}\]

\[ o_t=\sigma(W_ox_t+U_oh_{t-1}+b_o),\qquad h_t=o_t\odot\tanh(c_t). \tag{10.16}\]

其中 \(W_\bullet\in\mathbb R^{K\times p}\)\(U_\bullet\in\mathbb R^{K\times K}\)\(b_\bullet\in\mathbb R^K\)\(\odot\) 表示逐元素乘法。三个门的作用分别是

  1. 遗忘门: 控制丢弃多少细胞状态
  2. 输入门: 控制多少新信息写入细胞状态
  3. 输出门: 控制输出多少细胞状态

提示:为什么 LSTM 只能说“缓解”长期依赖困难?

式 10.15 可见,细胞状态的局部导数包含 \(\partial c_t/\partial c_{t-1}=f_t\) 这条加法通路。当遗忘门分量接近 1 时,信息和梯度可以跨越更多时间步;但若门饱和、优化不良、序列过长或训练信号不足,梯度仍会衰减或模型仍会失败。门控机制允许网络

  1. 遗忘: 通过遗忘门 \(f_t\) 决定保留多少旧信息
  2. 更新: 通过输入门 \(i_t\) 决定添加多少新信息
  3. 输出: 通过输出门 \(o_t\) 决定输出多少信息

这种设计使 LSTM 能选择性保留或遗忘信息,因此比普通 RNN 更有机会学习较长依赖;它是结构偏好,不是性能保证。文本、语音或金融时间序列中仍须用冻结验证集和同键简单基线检验增量证据。

10.6 深度学习的训练

训练深度神经网络需要解决一些特殊的挑战:

数学推导:反向传播算法的链式法则核心

反向传播(Backpropagation)把多变量链式法则组织为网络中的高效误差回传与参数梯度计算 (Rumelhart 等 1986年);它是训练多层网络的基础算法,但不保证非凸优化得到全局最优解。

沿用 式 10.7 的约定:\(L\) 只表示隐藏层数,\(A^{(0)}=x\);对 \(l=1,\ldots,L\),前向传播计算 \(Z^{(l)}=W^{(l)}A^{(l-1)}+b^{(l)}\)\(A^{(l)}=g(Z^{(l)})\)。输出层另记为 \(z=BA^{(L)}+\beta_0\)\(\widehat y=q(z)\),其中 \(B\in\mathbb R^{M\times K_L}\);不再把第 \(L\) 隐藏层同时称为输出层。

为了使用梯度下降优化 \(B\)\(\beta_0\) 与各 \(W^{(l)}\),先定义输出 logit 误差 \(\delta^{\mathrm{out}}=\nabla_zJ\),再定义隐藏层局部误差 \(\delta^{(l)}=\partial J/\partial Z^{(l)}\)

  1. 一般输出映射:设 \(Dq(z)\)\(q\) 的 Jacobian,则 \[ \delta^{\mathrm{out}}=[Dq(z)]^\top\nabla_{\widehat y}J. \tag{10.17}\] 对 sigmoid 二分类与 BCE,融合后 \(\delta^{\mathrm{out}}=\widehat y-y\);对 softmax 互斥多分类与 one-hot 交叉熵,融合后同样有 \(\delta^{\mathrm{out}}=\widehat y-y\)。这两个简式依赖指定的“输出映射—损失”配对,不是任意 \(q\) 的逐元素求导公式。
  2. 从输出回传到隐藏层:最后一个隐藏层和更早隐藏层依次满足 \[ \delta^{(L)}=(B^\top\delta^{\mathrm{out}})\odot g'(Z^{(L)}), \] \[ \delta^{(l)}=((W^{(l+1)})^\top\delta^{(l+1)})\odot g'(Z^{(l)}),\qquad l=L-1,\ldots,1. \tag{10.18}\]
  3. 计算参数梯度:输出层与隐藏层分别为 \[ \nabla_BJ=\delta^{\mathrm{out}}(A^{(L)})^\top,\quad \nabla_{\beta_0}J=\delta^{\mathrm{out}},\quad \nabla_{W^{(l)}}J=\delta^{(l)}(A^{(l-1)})^\top,\quad \nabla_{b^{(l)}}J=\delta^{(l)}. \tag{10.19}\]

CNN 与RNN 中的反向传播延伸: - CNN(卷积层):由于“权重共享(Weight Sharing)”特性,某个特定卷积核的参数梯度在反向传播时是所有该局部感受野(Receptive Field)应用位置处梯度的全局累加。 - RNN(循环层):由于存在时间步自循环,“时间反向传播(BPTT, Backpropagation Through Time)”展开了递归计算图。在给定的时间步 \(t\) 计算的误的\(\delta_t\) 不仅取决于当前时间步的直接输出前向损失梯度的回传,还强烈依赖的\(t+1\) 步递归回传的时序误差项。在长序列分析中连续反复乘以隐藏状态转移权重矩的\(W_{hh}\),这正是导致梯度消失(Vanishing Gradient)或由于谱半径大于1而导致爆炸的数学根源,也是发明LSTM 中加法更新状态(长效记忆通路保持恒定误差流)的底层理论动机。

10.6.1 梯度消失和爆炸

在深度网络中,梯度在反向传播过程中可能会变得非常小(消失)或非常大(爆炸),这使得网络难以训练。

解决方案:

  • 使用ReLU激活函数缓解梯度消失)
  • 使用批量归一化Batch Normalization)
  • 使用残差连接(ResNet)
  • 使用梯度裁剪(针对梯度爆炸)

10.6.2 正则化技术

深度学习模型通常有大量参数容易过拟合。常见的正则化技术包括

  1. Dropout: 在训练过程中随机丢弃一些神经元的输出
  2. L1/L2正则化: 在损失函数中添加权重惩罚项
  3. 数据增强: 对训练数据进行随机变换如图像旋转、平移等)
  4. 早停: 根据验证集性能提前停止训练

10.6.3 优化算法

深度学习常用的优化算法包括

  1. SGD: 随机梯度下降
  2. Momentum: 动量法加速收敛
  3. Adam: 自适应矩估计结合了动量法和自适应学习率
  4. RMSprop: 均方根传播

10.7 文档分类应用

本节先用随机张量做形状与 API 的受控健全性检查。随机 token 与随机标签不包含语义,绝不能称为 IMDb 数据或用其性能评价情感分类。真实文本案例必须另行提供可追溯数据来源与许可。

10.7.1 词嵌入

在自然语言处理中我们需要将文本转换为数值表示。简单的方法是词袋模型(Bag of Words),但更好的方法是使用词嵌入(Word Embeddings)。

词嵌入将每个词表示为一个低维实数向量使得语义相似的词在嵌入空间中距离更近。常见的预训练词嵌入包括Word2Vec和GloVe。

10.7.2 情感分析示例

下面只检查 Embedding、池化与 Dense 的张量接口是否连通,不作经验结论。

下面的随机输入是非性能演示:词索引与标签彼此独立,模型理论上不应获得稳定的样本外信号。 这里的 Embedding 只把随机整数索引映射为可训练的 32 维向量,随后 FlattenDense 用于检查张量形状、前向传播和反向传播是否连通。由于 token 与标签均随机生成,向量之间没有可解释的词义距离,输出也不代表真实影评情感。只有换成来源、许可、词表和切分均可追溯的文本后,才能讨论语义结构或分类性能。

import numpy as np  # 导入numpy用于数值计算
import matplotlib.pyplot as plt  # 导入matplotlib用于数据可视化
from tensorflow import keras  # 导入keras深度学习框架
from tensorflow.keras import layers, models  # 导入神经网络层和模型构建工具
from tensorflow.keras.preprocessing.text import Tokenizer  # 导入文本分词器(本例未实际使用,保留供真实数据场景参考)
from tensorflow.keras.preprocessing.sequence import pad_sequences  # 导入序列填充工具(本例未实际使用,保留供真实数据场景参考)
keras.utils.set_random_seed(20260812)  # 固定Python、NumPy与TensorFlow随机状态
sentiment_rng = np.random.default_rng(20260812)  # 使用独立生成器冻结教学张量

# 设置模型超参数
vocab_size = 10000  # 词汇表大小:仅保留频率最高的10000个词
max_len = 200       # 序列最大长度:每条评论截取或填充至200个词
embedding_dim = 32   # 嵌入维度:每个词用32维向量表示

# 创建模拟数据(实际应用中应使用真实IMDb数据)
num_samples = 2500  # 模拟样本总数
# 生成模拟的词索引序列(随机整数代表词表中的词)
train_sequences = sentiment_rng.integers(1, vocab_size, size=(int(0.8 * num_samples), max_len))  # 冻结训练词索引
train_labels = sentiment_rng.integers(0, 2, size=int(0.8 * num_samples))  # 冻结独立训练标签
test_sequences = sentiment_rng.integers(1, vocab_size, size=(int(0.2 * num_samples), max_len))  # 冻结测试词索引
test_labels = sentiment_rng.integers(0, 2, size=int(0.2 * num_samples))  # 冻结独立测试标签

# 构建情感分类模型:Embedding + Flatten + Dense
model = models.Sequential([  # 拟合model模型
    layers.Embedding(vocab_size, embedding_dim, input_length=max_len),  # 词嵌入层:将词索引映射为密集向量
    layers.Flatten(),  # 展平层:将嵌入矩阵拉平为一维向量
    layers.Dense(16, activation='relu'),  # 隐藏层:16个神经元,ReLU激活
    layers.Dense(1, activation='sigmoid')  # 输出层:单个神经元,Sigmoid输出概率
])  # 完成构建

model.compile(optimizer='adam',  # Adam优化器
              loss='binary_crossentropy',  # 二元交叉熵损失函数
              metrics=['accuracy'])  # 监控准确率
/tmp/tmp.CfULtrnrMQ/peter-validation/lib/python3.10/site-packages/keras/src/layers/core/embedding.py:97: UserWarning: Argument `input_length` is deprecated. Just remove it.
  warnings.warn(

以上完成了数据准备和模型搭建。列表 10.4 记录训练与一次测试访问;图 10.4 只承担曲线诊断。这里没有真实语料,因此两个对象都不能称为 IMDb 性能证据。

列表 10.4: 随机张量接口的训练与一次测试访问记录(非 IMDb 性能)
print('开始训练情感分析模型...')  # 输出训练开始提示
history = model.fit(train_sequences, train_labels, epochs=5,  # 训练5个epoch
                    batch_size=128, validation_split=0.2, verbose=1)  # 批大小128,20%验证集

# 评估模型在测试集上的表现
test_loss, test_acc = model.evaluate(test_sequences, test_labels, verbose=0)  # 计算测试集损失和准确率
print(f'\n测试集准确率: {test_acc:.4f}')  # 打印测试集准确率
print(f'测试集损失: {test_loss:.4f}')  # 同时输出随机接口的测试损失
开始训练情感分析模型...

Epoch 1/5


 1/13 ━━━━━━━━━━━━━━━━━━━ 7s 583ms/step - accuracy: 0.4922 - loss: 0.6921

10/13 ━━━━━━━━━━━━━━━━━━━━ 0s 6ms/step - accuracy: 0.4841 - loss: 0.6937  

13/13 ━━━━━━━━━━━━━━━━━━━━ 1s 17ms/step - accuracy: 0.4894 - loss: 0.6939 - val_accuracy: 0.5250 - val_loss: 0.6931

Epoch 2/5


 1/13 ━━━━━━━━━━━━━━━━━━━ 0s 18ms/step - accuracy: 0.9922 - loss: 0.6090

10/13 ━━━━━━━━━━━━━━━━━━━━ 0s 6ms/step - accuracy: 0.9916 - loss: 0.6270 

13/13 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.9806 - loss: 0.6350 - val_accuracy: 0.4975 - val_loss: 0.6931

Epoch 3/5


 1/13 ━━━━━━━━━━━━━━━━━━━ 0s 18ms/step - accuracy: 1.0000 - loss: 0.5610

10/13 ━━━━━━━━━━━━━━━━━━━━ 0s 6ms/step - accuracy: 1.0000 - loss: 0.5736 

13/13 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 1.0000 - loss: 0.5696 - val_accuracy: 0.4750 - val_loss: 0.6941

Epoch 4/5


 1/13 ━━━━━━━━━━━━━━━━━━━ 0s 19ms/step - accuracy: 1.0000 - loss: 0.4740

10/13 ━━━━━━━━━━━━━━━━━━━━ 0s 6ms/step - accuracy: 1.0000 - loss: 0.4756 

13/13 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 1.0000 - loss: 0.4504 - val_accuracy: 0.4950 - val_loss: 0.7003

Epoch 5/5


 1/13 ━━━━━━━━━━━━━━━━━━━ 0s 23ms/step - accuracy: 1.0000 - loss: 0.3245

10/13 ━━━━━━━━━━━━━━━━━━━━ 0s 6ms/step - accuracy: 1.0000 - loss: 0.3123 

13/13 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 1.0000 - loss: 0.2741 - val_accuracy: 0.5050 - val_loss: 0.7153



测试集准确率: 0.4860

测试集损失: 0.7196
plt.figure(figsize=(12, 4))  # 创建12×4英寸的宽幅画布

# 左图:情感分析准确率曲线
plt.subplot(1, 2, 1)  # 1行2列的第1个子图
plt.plot(history.history['accuracy'], label='训练集准确率')  # 训练集准确率曲线
plt.plot(history.history['val_accuracy'], label='验证集准确率')  # 验证集准确率曲线
plt.xlabel('Epoch', fontsize=12)  # x轴标签
plt.ylabel('准确率', fontsize=12)  # y轴标签
plt.title('情感分析模型准确率', fontsize=14, fontname='Source Han Serif SC')  # 使用已安装中文字体设置子图标题
plt.legend(fontsize=10)  # 添加图例
plt.grid(True, alpha=0.3)  # 添加半透明网格线

# 右图:情感分析损失曲线
plt.subplot(1, 2, 2)  # 1行2列的第2个子图
plt.plot(history.history['loss'], label='训练集损失')  # 训练集损失曲线
plt.plot(history.history['val_loss'], label='验证集损失')  # 验证集损失曲线
plt.xlabel('Epoch', fontsize=12)  # x轴标签
plt.ylabel('损失', fontsize=12)  # y轴标签
plt.title('情感分析模型损失', fontsize=14, fontname='Source Han Serif SC')  # 使用已安装中文字体设置子图标题
plt.legend(fontsize=10)  # 添加图例
plt.grid(True, alpha=0.3)  # 添加半透明网格线

plt.tight_layout()  # 自动调整子图间距
plt.show()  # 显示图形
左图比较训练与验证准确率,右图比较训练与验证损失;随机词索引和随机标签不承载语义。
图 10.4: 随机词索引接口的训练与验证曲线(非真实情感分析)

图 10.4 的测试准确率应围绕类别基准波动,因为输入与标签由独立随机机制生成;训练集拟合只说明高容量模型能够记忆噪声。该结果不能归因于词嵌入维度、预训练词向量或 IMDb 语义,也不能据此比较 NLP 架构。若要做真实情感分析,必须换用许可与切分均可追溯的文本,并预先规定朴素词频或线性分类基线。

10.8 时间序列预测应用

RNN/LSTM 在时间序列预测中也有广泛应用。下面以 LSTM 为候选模型,使用海康威视的历史收盘价预测下一交易日收盘价;响应始终记为 \(y\),预测记为 \(\widehat y\)

10.8.1 金融时间序列示例

最后,让我们把视角拉回那个最令人血脉偾张、却也最难以捉摸的领域——金融量化前沿。在这个终极挑战中,我们要用带有记忆的深度网络去预测海康威视的未来股价轨迹。 请注意代码第 3 步那极其特殊的“序列数据重构”函了create_sequences。由于我们要利用“过了60 个交易日”的漫长记忆去推断“第 61 天”的走势,原本一维的时间序列被这个函数极其巧妙地切割并卷叠成了一了(样本的 60, 1) 的三维张量。这就好比在一张长长的 K 线图上,开着一个宽度为 60 天的滑动窗口,一点点向右平移并截取视野。 本例把 LSTM 作为一种候选序列模型,而不是预设的“工业标准”。堆叠层允许模型表达较长依赖,Dropout(0.2) 只是训练期正则化机制;两者都不保证样本外改进。最终判断必须来自同一锁定测试期上相对于 last-value 和线性基线的损失。

表 10.1: LSTM 时间序列样本准备审计
import numpy as np  # 导入numpy用于数值计算
import pandas as pd  # 导入pandas用于数据处理
import matplotlib.pyplot as plt  # 导入matplotlib用于数据可视化
from sklearn.preprocessing import MinMaxScaler  # 导入最小-最大归一化工具用于数据标准化
from tensorflow import keras  # 导入keras深度学习框架
from tensorflow.keras import layers, models  # 导入神经网络层和模型构建工具

# 1. 加载海康威视股价数据
import os  # 导入操作系统模块用于跨平台路径处理
from pathlib import Path  # 使用跨平台路径对象解析显式数据根
DATA_DIR = Path(os.environ['BOOK_DATA_DIR']).expanduser().resolve()  # 从必需环境变量取得数据根
if not DATA_DIR.is_dir():  # 在读取前验证数据根
    raise FileNotFoundError(f'BOOK_DATA_DIR 不存在或不是目录: {DATA_DIR}')  # 失败即停止,不回退固定路径
path = DATA_DIR / 'stock' / 'stock_price_post_adjusted.h5'  # 构建后复权股价数据文件路径
stock_price_history = pd.read_hdf(path).reset_index()  # 读取后复权股价数据并重置MultiIndex
haikang_data = stock_price_history[stock_price_history['order_book_id'] == '002415.XSHE'].copy()  # 筛选海康威视股票数据

haikang_data = haikang_data.sort_values('date')  # 按日期排序确保时序正确
closing_prices = haikang_data['close'].values.reshape(-1, 1)  # 提取收盘价并转为二维数组

# 2. 先固定原始时间边界,再只用训练期拟合缩放器
raw_split_index = int(len(closing_prices) * 0.8)  # 最后20%原始观测作为锁定测试期
scaler = MinMaxScaler(feature_range=(0, 1))  # 创建最小-最大归一化器
scaler.fit(closing_prices[:raw_split_index])  # 缩放参数只能看到训练期价格
scaled_closing_prices = scaler.transform(closing_prices)  # 用训练期参数转换全序列

接下来定义序列数据构造函数,将一维时间序列切割为固定窗口长度的训练样本对,并划分训练集与测试集。

# 3. 准备序列数据:用滑动窗口法将时间序列转为监督学习格式
def create_sequences(data, length_of_sequence):  # 定义函数create_sequences
    """将时间序列转换为(特征序列, 目标值)对"""  # 执行数据处理操作
    sequence_features, sequence_targets = [], []  # 初始化特征和目标列表
    for i in range(len(data) - length_of_sequence):  # 滑动窗口遍历
        sequence_features.append(data[i:i + length_of_sequence])  # 截取窗口内的数据作为特征
        sequence_targets.append(data[i + length_of_sequence])  # 窗口后一天的值作为目标
    return np.array(sequence_features), np.array(sequence_targets)  # 转为numpy数组返回

seq_length = 60  # 使用过去60个交易日预测下一天
sequence_features, sequence_targets = create_sequences(scaled_closing_prices, seq_length)  # 构造序列数据

# 4. 分割训练集和测试集(约80%训练, 约20%测试)
# 注意: 时间序列不能随机打乱,必须按时间顺序切分
split_index = raw_split_index - seq_length  # 使训练目标严格早于原始时间边界
train_sequences, test_sequences = sequence_features[:split_index], sequence_features[split_index:]  # 按时间顺序分割特征
train_targets, test_targets = sequence_targets[:split_index], sequence_targets[split_index:]  # 按时间顺序分割目标

下面构建双层LSTM模型并进行训练。LSTM相比普通RNN能更好地捕获长期时间依赖关系。

# 5. 构建LSTM模型(双层LSTM + Dropout正则化)
model = models.Sequential([  # 拟合model模型
    layers.LSTM(50, return_sequences=True, input_shape=(seq_length, 1)),  # 第一层LSTM:50个单元,返回完整序列
    layers.Dropout(0.2),  # Dropout层:随机丢弃20%的神经元防止过拟合
    layers.LSTM(50, return_sequences=False),  # 第二层LSTM:50个单元,只返回最后时间步
    layers.Dropout(0.2),  # Dropout层:再次随机丢弃20%
    layers.Dense(25),  # 全连接层:25个神经元
    layers.Dense(1)  # 输出层:预测1个值(下一天收盘价)
])  # 完成构建

model.compile(optimizer='adam', loss='mean_squared_error')  # 编译模型:Adam优化器 + MSE损失

# 6. 训练模型(为节省时间仅训练20个epoch;生产环境建议50-100个epoch)
print('开始训练LSTM模型...')  # 输出训练开始提示
history = model.fit(train_sequences, train_targets, epochs=20,  # 训练20轮
                    batch_size=64, validation_split=0.1, shuffle=False, verbose=1)  # 验证集取训练期末段且禁止打乱
开始训练LSTM模型...
Epoch 1/20
/tmp/tmp.CfULtrnrMQ/peter-validation/lib/python3.10/site-packages/keras/src/layers/rnn/rnn.py:199: UserWarning: Do not pass an `input_shape`/`input_dim` argument to a layer. When using Sequential models, prefer using an `Input(shape)` object as the first layer in the model instead.
  super().__init__(**kwargs)
 1/42 ━━━━━━━━━━━━━━━━━━━━ 59s 1s/step - loss: 2.1691e-04

 4/42 ━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 1.7970e-04

 7/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 1.6010e-04

10/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 1.6907e-04

12/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 1.7561e-04

14/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 1.9044e-04

16/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 2.2507e-04

19/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 3.1634e-04

22/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 4.4256e-04

24/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 5.0239e-04

27/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 5.8079e-04

29/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 6.3923e-04

32/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 7.4642e-04

35/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 8.6335e-04

38/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 9.9940e-04

41/42 ━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0012    

42/42 ━━━━━━━━━━━━━━━━━━━━ 3s 31ms/step - loss: 0.0037 - val_loss: 0.0349

Epoch 2/20


 1/42 ━━━━━━━━━━━━━━━━━━━━ 1s 39ms/step - loss: 0.0791

 3/42 ━━━━━━━━━━━━━━━━━━━ 1s 26ms/step - loss: 0.0740

 6/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0649

 9/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0566

12/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0497

15/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0445

18/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0405

21/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0375

24/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0351

27/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0331

30/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0315

33/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0302

35/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0294

37/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0287

40/42 ━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0278

42/42 ━━━━━━━━━━━━━━━━━━━━ 1s 26ms/step - loss: 0.0188 - val_loss: 0.0025

Epoch 3/20


 1/42 ━━━━━━━━━━━━━━━━━━━━ 1s 37ms/step - loss: 0.0222

 4/42 ━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0235

 7/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0226

10/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0206

12/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0193

15/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0177

17/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0168

19/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0161

22/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0153

24/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0148

27/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0141

29/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0138

31/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0135

34/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0132

36/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0131

39/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0129

42/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0128

42/42 ━━━━━━━━━━━━━━━━━━━━ 1s 26ms/step - loss: 0.0114 - val_loss: 8.0824e-04

Epoch 4/20


 1/42 ━━━━━━━━━━━━━━━━━━━━ 1s 40ms/step - loss: 0.0022

 4/42 ━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0016

 6/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0013

 8/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0012

11/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 9.8716e-04

13/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 9.0609e-04

16/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 8.1499e-04

19/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 7.5647e-04

22/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 7.2063e-04

24/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 7.0417e-04

27/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 7.4085e-04

29/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 9.0175e-04

31/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0011    

34/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0014

37/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0016

40/42 ━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0017

42/42 ━━━━━━━━━━━━━━━━━━━━ 1s 26ms/step - loss: 0.0041 - val_loss: 0.0078

Epoch 5/20


 1/42 ━━━━━━━━━━━━━━━━━━━━ 1s 37ms/step - loss: 0.0240

 3/42 ━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0237

 6/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0221

 9/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0199

11/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0185

14/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0167

17/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0152

19/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0144

22/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0134

25/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0125

27/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0120

30/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0115

33/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0110

36/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0106

39/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0102

42/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0100

42/42 ━━━━━━━━━━━━━━━━━━━━ 1s 26ms/step - loss: 0.0071 - val_loss: 0.0039

Epoch 6/20


 1/42 ━━━━━━━━━━━━━━━━━━━━ 1s 38ms/step - loss: 4.7381e-04

 4/42 ━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.8796e-04

 7/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.7069e-04

 9/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 3.6891e-04

11/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 3.9203e-04

14/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 4.8630e-04

16/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 5.2989e-04

19/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 6.0407e-04

22/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 7.0498e-04

25/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 7.6730e-04

27/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 8.5061e-04

30/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0011    

33/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0014

36/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0018

39/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0022

42/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0026

42/42 ━━━━━━━━━━━━━━━━━━━━ 1s 26ms/step - loss: 0.0074 - val_loss: 0.0035

Epoch 7/20


 1/42 ━━━━━━━━━━━━━━━━━━━━ 1s 37ms/step - loss: 0.0079

 4/42 ━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0061

 7/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0049

10/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0042

13/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0039

16/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0037

19/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0035

22/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0034

25/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0032

27/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0032

30/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0031

33/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0030

36/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0030

39/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0029

42/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0029

42/42 ━━━━━━━━━━━━━━━━━━━━ 1s 26ms/step - loss: 0.0030 - val_loss: 0.0010

Epoch 8/20


 1/42 ━━━━━━━━━━━━━━━━━━━━ 1s 42ms/step - loss: 0.0103

 3/42 ━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0090

 6/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0075

 9/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0063

11/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0057

14/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0051

17/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0047

20/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0044

22/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0042

25/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0039

27/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0038

30/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0036

33/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0035

36/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0034

39/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0033

42/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0032

42/42 ━━━━━━━━━━━━━━━━━━━━ 1s 26ms/step - loss: 0.0024 - val_loss: 0.0013

Epoch 9/20


 1/42 ━━━━━━━━━━━━━━━━━━━━ 1s 37ms/step - loss: 3.6604e-04

 3/42 ━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 3.7498e-04

 6/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.6254e-04

 9/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.4663e-04

11/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.3568e-04

14/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.2080e-04

17/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.0816e-04

20/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.0127e-04

23/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 2.9569e-04

26/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 2.8960e-04

28/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 2.9431e-04

31/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.1616e-04

34/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.4136e-04

36/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.6041e-04

39/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.9393e-04

42/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 4.5225e-04

42/42 ━━━━━━━━━━━━━━━━━━━━ 1s 26ms/step - loss: 0.0013 - val_loss: 0.0012

Epoch 10/20


 1/42 ━━━━━━━━━━━━━━━━━━━━ 1s 39ms/step - loss: 0.0013

 3/42 ━━━━━━━━━━━━━━━━━━━ 1s 28ms/step - loss: 0.0012

 6/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0011

 9/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 9.7303e-04

11/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 9.1703e-04

14/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 8.6956e-04

17/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 8.3872e-04

20/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 8.2759e-04

23/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 8.1979e-04

25/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 8.1089e-04

28/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 8.1620e-04

30/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 8.4112e-04

33/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 8.7751e-04

36/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 9.2168e-04

38/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 9.5608e-04

41/42 ━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0010    

42/42 ━━━━━━━━━━━━━━━━━━━━ 1s 26ms/step - loss: 0.0020 - val_loss: 0.0016

Epoch 11/20


 1/42 ━━━━━━━━━━━━━━━━━━━━ 1s 37ms/step - loss: 0.0059

 4/42 ━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0050

 7/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0042

10/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0036

13/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0032

16/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0030

19/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0028

21/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0027

24/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0026

27/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0025

30/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0024

33/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0023

35/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0023

37/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0022

40/42 ━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0022

42/42 ━━━━━━━━━━━━━━━━━━━━ 1s 26ms/step - loss: 0.0019 - val_loss: 7.4034e-04

Epoch 12/20


 1/42 ━━━━━━━━━━━━━━━━━━━━ 1s 37ms/step - loss: 0.0035

 4/42 ━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0029

 7/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0024

10/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0021

13/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0019

16/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0017

19/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0016

22/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0015

24/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0014

26/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0014

29/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0013

32/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0013

34/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0013

37/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0012

40/42 ━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0012

42/42 ━━━━━━━━━━━━━━━━━━━━ 1s 26ms/step - loss: 0.0013 - val_loss: 8.5570e-04

Epoch 13/20


 1/42 ━━━━━━━━━━━━━━━━━━━━ 1s 37ms/step - loss: 4.2567e-04

 4/42 ━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 3.8912e-04

 6/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 3.7947e-04

 9/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.6363e-04

12/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.4671e-04

15/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.2957e-04

18/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.1573e-04

20/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.0904e-04

23/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 2.9897e-04

26/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 2.8963e-04

29/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 2.9020e-04

32/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.0067e-04

34/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.0954e-04

36/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.1906e-04

38/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 3.2917e-04

40/42 ━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 3.4570e-04

42/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 3.7395e-04

42/42 ━━━━━━━━━━━━━━━━━━━━ 1s 27ms/step - loss: 9.9287e-04 - val_loss: 0.0013

Epoch 14/20


 1/42 ━━━━━━━━━━━━━━━━━━━━ 1s 46ms/step - loss: 2.8514e-04

 3/42 ━━━━━━━━━━━━━━━━━━━ 1s 27ms/step - loss: 3.0692e-04

 5/42 ━━━━━━━━━━━━━━━━━━━━ 1s 27ms/step - loss: 3.1036e-04

 7/42 ━━━━━━━━━━━━━━━━━━━━ 0s 27ms/step - loss: 3.1049e-04

 9/42 ━━━━━━━━━━━━━━━━━━━━ 0s 27ms/step - loss: 3.0823e-04

11/42 ━━━━━━━━━━━━━━━━━━━━ 0s 27ms/step - loss: 3.0492e-04

13/42 ━━━━━━━━━━━━━━━━━━━━ 0s 27ms/step - loss: 3.0280e-04

15/42 ━━━━━━━━━━━━━━━━━━━━ 0s 27ms/step - loss: 3.0044e-04

17/42 ━━━━━━━━━━━━━━━━━━━━ 0s 27ms/step - loss: 2.9695e-04

20/42 ━━━━━━━━━━━━━━━━━━━━ 0s 26ms/step - loss: 2.9993e-04

23/42 ━━━━━━━━━━━━━━━━━━━━ 0s 26ms/step - loss: 3.0362e-04

26/42 ━━━━━━━━━━━━━━━━━━━━ 0s 26ms/step - loss: 3.0503e-04

29/42 ━━━━━━━━━━━━━━━━━━━━ 0s 26ms/step - loss: 3.3667e-04

31/42 ━━━━━━━━━━━━━━━━━━━━ 0s 26ms/step - loss: 3.7209e-04

34/42 ━━━━━━━━━━━━━━━━━━━━ 0s 26ms/step - loss: 4.2738e-04

36/42 ━━━━━━━━━━━━━━━━━━━━ 0s 26ms/step - loss: 4.7180e-04

39/42 ━━━━━━━━━━━━━━━━━━━━ 0s 26ms/step - loss: 5.4557e-04

42/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 6.3447e-04

42/42 ━━━━━━━━━━━━━━━━━━━━ 1s 27ms/step - loss: 0.0019 - val_loss: 0.0027

Epoch 15/20


 1/42 ━━━━━━━━━━━━━━━━━━━━ 1s 37ms/step - loss: 0.0018

 4/42 ━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0015

 6/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0013

 8/42 ━━━━━━━━━━━━━━━━━━━━ 0s 26ms/step - loss: 0.0012

11/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0011

14/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0011

17/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0010

20/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 9.9939e-04

22/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 9.8043e-04

24/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 9.5956e-04

26/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 9.3940e-04

28/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 9.3651e-04

30/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 9.5079e-04

32/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 9.6571e-04

34/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 9.8430e-04

36/42 ━━━━━━━━━━━━━━━━━━━━ 0s 26ms/step - loss: 0.0010    

38/42 ━━━━━━━━━━━━━━━━━━━━ 0s 26ms/step - loss: 0.0010

40/42 ━━━━━━━━━━━━━━━━━━━ 0s 26ms/step - loss: 0.0011

42/42 ━━━━━━━━━━━━━━━━━━━━ 1s 28ms/step - loss: 0.0020 - val_loss: 0.0015

Epoch 16/20


 1/42 ━━━━━━━━━━━━━━━━━━━━ 1s 41ms/step - loss: 0.0038

 3/42 ━━━━━━━━━━━━━━━━━━━ 1s 26ms/step - loss: 0.0032

 5/42 ━━━━━━━━━━━━━━━━━━━━ 0s 26ms/step - loss: 0.0027

 7/42 ━━━━━━━━━━━━━━━━━━━━ 0s 27ms/step - loss: 0.0024

 9/42 ━━━━━━━━━━━━━━━━━━━━ 0s 27ms/step - loss: 0.0022

11/42 ━━━━━━━━━━━━━━━━━━━━ 0s 27ms/step - loss: 0.0020

13/42 ━━━━━━━━━━━━━━━━━━━━ 0s 27ms/step - loss: 0.0019

15/42 ━━━━━━━━━━━━━━━━━━━━ 0s 27ms/step - loss: 0.0018

17/42 ━━━━━━━━━━━━━━━━━━━━ 0s 27ms/step - loss: 0.0018

19/42 ━━━━━━━━━━━━━━━━━━━━ 0s 27ms/step - loss: 0.0017

21/42 ━━━━━━━━━━━━━━━━━━━━ 0s 27ms/step - loss: 0.0016

23/42 ━━━━━━━━━━━━━━━━━━━━ 0s 27ms/step - loss: 0.0016

25/42 ━━━━━━━━━━━━━━━━━━━━ 0s 27ms/step - loss: 0.0015

27/42 ━━━━━━━━━━━━━━━━━━━━ 0s 27ms/step - loss: 0.0015

29/42 ━━━━━━━━━━━━━━━━━━━━ 0s 27ms/step - loss: 0.0014

31/42 ━━━━━━━━━━━━━━━━━━━━ 0s 27ms/step - loss: 0.0014

34/42 ━━━━━━━━━━━━━━━━━━━━ 0s 26ms/step - loss: 0.0014

37/42 ━━━━━━━━━━━━━━━━━━━━ 0s 26ms/step - loss: 0.0013

40/42 ━━━━━━━━━━━━━━━━━━━ 0s 26ms/step - loss: 0.0013

42/42 ━━━━━━━━━━━━━━━━━━━━ 0s 26ms/step - loss: 0.0013

42/42 ━━━━━━━━━━━━━━━━━━━━ 1s 28ms/step - loss: 0.0013 - val_loss: 0.0011

Epoch 17/20


 1/42 ━━━━━━━━━━━━━━━━━━━━ 1s 39ms/step - loss: 0.0027

 4/42 ━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0022

 7/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0019

10/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0016

13/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0015

16/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0014

18/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0013

20/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0013

23/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0012

26/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0012

29/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0011

32/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0011

35/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0011

38/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0010

41/42 ━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0010

42/42 ━━━━━━━━━━━━━━━━━━━━ 1s 26ms/step - loss: 0.0012 - val_loss: 7.4162e-04

Epoch 18/20


 1/42 ━━━━━━━━━━━━━━━━━━━━ 1s 37ms/step - loss: 0.0020

 4/42 ━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0016

 7/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 0.0013

 9/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0012

12/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0011

15/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 0.0010

18/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 9.5795e-04

20/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 9.2312e-04

22/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 8.9048e-04

25/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 8.4554e-04

28/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 8.1096e-04

31/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 7.9271e-04

34/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 7.7994e-04

36/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 7.7317e-04

39/42 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step - loss: 7.6670e-04

42/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 7.7810e-04

42/42 ━━━━━━━━━━━━━━━━━━━━ 1s 26ms/step - loss: 0.0010 - val_loss: 8.8863e-04

Epoch 19/20


 1/42 ━━━━━━━━━━━━━━━━━━━━ 1s 37ms/step - loss: 6.8699e-04

 4/42 ━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 5.8854e-04

 7/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 5.4203e-04

10/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 5.0871e-04

13/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 4.8332e-04

16/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 4.6230e-04

19/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 4.4431e-04

22/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 4.2817e-04

25/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 4.1205e-04

28/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 4.0048e-04

31/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 4.0007e-04

34/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 4.0345e-04

37/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 4.0883e-04

40/42 ━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 4.2413e-04

42/42 ━━━━━━━━━━━━━━━━━━━━ 1s 26ms/step - loss: 9.2875e-04 - val_loss: 0.0012

Epoch 20/20


 1/42 ━━━━━━━━━━━━━━━━━━━━ 1s 37ms/step - loss: 4.4314e-04

 4/42 ━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 4.3876e-04

 7/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 4.2530e-04

10/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 4.1159e-04

13/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 4.0293e-04

16/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.9450e-04

19/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.8572e-04

22/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.7763e-04

25/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.6763e-04

28/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.6419e-04

30/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.7185e-04

33/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 3.8746e-04

36/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 4.0714e-04

39/42 ━━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 4.3172e-04

41/42 ━━━━━━━━━━━━━━━━━━━ 0s 24ms/step - loss: 4.6024e-04

42/42 ━━━━━━━━━━━━━━━━━━━━ 1s 26ms/step - loss: 0.0012 - val_loss: 0.0026

训练与验证损失应从本次 history 对象读取。两条曲线共同下降只说明优化在当前切分上正常进行;是否过拟合以及是否具有增量预测能力,仍要看锁定测试期相对于 last-value 和线性基线的损失,不能由预写日志或归一化尺度下的 MSE 单独判断。

模型训练完成后,使用测试集进行预测并将归一化的价格还原为原始价格,最后以图表形式展示预测效果。

列表 10.5: LSTM、最近值与线性基线的锁定测试比较准备
predicted_targets = model.predict(test_sequences)
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
naive_targets = test_sequences[:, -1, 0].reshape(-1, 1)
linear_baseline = LinearRegression()
linear_baseline.fit(train_sequences.reshape(len(train_sequences), -1), train_targets)
linear_targets = linear_baseline.predict(test_sequences.reshape(len(test_sequences), -1))
baseline_comparison = pd.DataFrame({'model': ['last_value', 'linear', 'lstm'], 'test_mse_scaled': [mean_squared_error(test_targets, naive_targets), mean_squared_error(test_targets, linear_targets), mean_squared_error(test_targets, predicted_targets)]})
print(baseline_comparison)
rescaled_predicted_targets = scaler.inverse_transform(predicted_targets)
rescaled_test_targets = scaler.inverse_transform(test_targets)
 1/24 ━━━━━━━━━━━━━━━━━━━━ 3s 139ms/step

11/24 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step  

21/24 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step

24/24 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step

24/24 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step

        model  test_mse_scaled

0  last_value         0.000093

1      linear         0.000100

2        lstm         0.001655
# 8. 绘制预测结果对比图
plt.figure(figsize=(14, 6))  # 创建14×6英寸的画布

# 为了图表清晰,只展示最近365天(约一年)的数据
plot_len = 365  # 展示天数
if len(rescaled_test_targets) > plot_len:  # 如果测试集超过365天
    plot_test_targets = rescaled_test_targets[-plot_len:]  # 取最后365天的真实值
    plot_predicted_targets = rescaled_predicted_targets[-plot_len:]  # 取最后365天的预测值
else:  # 测试集不足365天则全部展示
    plot_test_targets = rescaled_test_targets  # 使用全部真实值
    plot_predicted_targets = rescaled_predicted_targets  # 使用全部预测值

plt.plot(plot_test_targets, label='真实股价', linewidth=2)  # 绘制真实股价曲线
plt.plot(plot_predicted_targets, label='预测股价', linewidth=2, linestyle='--')  # 绘制预测股价曲线(虚线)
plt.title('海康威视股价预测 (LSTM)', fontsize=14)  # 设置图标题
plt.xlabel('时间 (天)', fontsize=12)  # x轴标签
plt.ylabel('收盘价', fontsize=12)  # y轴标签
plt.legend()  # 显示图例
plt.grid(True, alpha=0.3)  # 添加半透明网格线
plt.show()  # 显示预测结果图

# 9. 训练过程损失曲线可视化
plt.figure(figsize=(12, 4))  # 创建12×4英寸的画布
plt.plot(history.history['loss'], label='训练集Loss')  # 绘制训练集损失曲线
plt.plot(history.history['val_loss'], label='验证集Loss')  # 绘制验证集损失曲线
plt.title('模型训练损失')  # 设置图标题
plt.legend()  # 显示图例
plt.show()  # 显示损失曲线图
上图叠加锁定测试期真实价格实线与 LSTM 预测虚线,下图显示训练与验证损失随轮次变化。
(a) LSTM模型对海康威视股价预测结果。虚线为模型预测值,实线为真实股价。
上图叠加锁定测试期真实价格实线与 LSTM 预测虚线,下图显示训练与验证损失随轮次变化。
(b)
图 10.5

图 10.5 的曲线视觉贴合不能证明预测能力,因为价格水平高度持久,简单的 last-value 基线也会呈现相似走势。应以同一锁定测试期的 baseline_comparison 为准;若 LSTM 未稳定优于朴素与线性基线,则报告“没有观察到增量预测证据”。训练损失下降只证明优化过程降低了训练目标,不替代样本外比较。

但需要指出的是,该模型存在一些典型的局限性:(1) 预测滞后效应——仔细观察可发现,预测曲线往往”追随”真实价格,存在一定的时间滞后,这是基于历史数据进行时序预测的通病;(2) 对突变的捕捉能力有限——在股价出现剧烈波动时,模型的预测往往反应不够灵敏;(3) 不代表实际投资建议——股价受到宏观经济、政策、市场情绪等众多复杂因素影响,单纯依赖历史价格数据的LSTM模型并不足以作为实际投资决策依据。在金融量化实践中,通常还需要融合基本面数据、技术指标、新闻情绪等多源信息来构建更完善的预测系统。

10.9 时间序列支线审计任务

本任务是 A10:60 分钟、10 分的必做独立评价。标准教学路线与压缩教学路线都必须完成 A10;它不是贯穿项目里程碑,不能由项目成果、正文练习或其他章节任务替代。A10 不消费正文的海康威视对象,也不要求本地金融数据库或大型网络。它使用人工构造的流程审计 fixture,只检验时间边界、预处理、基线和测试访问;任何输出都不得写成真实证券收益或可交易证据。

10.9.1 本地 setup 与 fixture 合同

fixture 合同见 表 10.2。固定内容哈希针对运行器持有的密封完整 fixture,按列顺序、日期格式与六位小数序列化为 UTF-8 CSV 字节;任一代码、行序或序列化规则改变都必须升版本,不能沿用旧哈希。列表 10.14 从不构造测试标签:学生端 ch10_audit_fixture 的测试段 target 直接初始化为缺失;只有原子预留成功后的私有回调才现场物化测试标签。

表 10.2: Chapter 10 核心任务 fixture 的版本、模式与资源合同
合同项 冻结值
contract_id ch10-sequence-audit-v7
性质 人工构造的教学 fixture;不代表任何真实证券
合同冻结日 2026-08-12(Asia/Shanghai)
观测日期 2024-01-032024-04-10,70 行,按营业日顺序
公开 schema fixture_version:str, sample_id:int64, prediction_date:datetime64[ns], label_date:datetime64[ns], lag_1:float64, lag_2:float64, target:Int8 nullable, split:str;测试段 target 全为缺失
切分与 purge 训练 1—40;purge_train_validation 41—42;验证 43—54;purge_validation_test 55—56;测试 57—70
密封完整 fixture SHA-256 df86d5fb7c67fd7d4a7801dae85d90f609b46bfe043d91afb55b1e0ca1b4bd08(v7 完整 fixture 按六位小数序列化;学生 builder 不读取测试标签)
一次评分审计 BOOK_CH10_TEST_AUDIT_DIR 指向仅教师运行器可写的审计根。submission_idattempt_id 均由运行器签发并私有绑定,学生不得提交、选择或轮换任何身份。issued.jsonreserved.jsonread-observations.jsoncompleted.jsonstopped.json 只追加、不覆盖
资源上限 普通 CPU 单核、内存 256 MB、总运行 120 秒、候选模型不超过 2 个;L-BFGS 候选最多 30 次优化迭代(不是 epoch)
列表 10.6: Chapter 10 核心任务的独立 fixture setup 与内容哈希
import hashlib  # 为人工 fixture 建立内容寻址证据
import numpy as np  # 生成无随机性的教学序列
import pandas as pd  # 构造带日期、标签和切分的标准接口
import json  # 用稳定JSON冻结选择、预测与指标证据
import os  # 从运行器环境取得授权审计路径
import uuid  # 为学生尝试生成不复用的审计身份
from datetime import datetime  # 为唯一评分事件记录时间
from pathlib import Path  # 解析不依赖工作目录的审计路径
from zoneinfo import ZoneInfo  # 统一使用Asia/Shanghai时区

def ch10_stable_hash(payload):  # 对选择、预测与结果使用同一序列化
    stable_bytes = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode('utf-8')  # 固定键序与编码
    return hashlib.sha256(stable_bytes).hexdigest()  # 返回可复算SHA-256
列表 10.7: Chapter 10 审计事件的一次性原子发布
def ch10_write_event_once(event_path, event_record):  # 先完整落盘再原子发布不可覆盖事件
    event_bytes = (json.dumps(event_record, ensure_ascii=False, sort_keys=True, separators=(',', ':')) + '\n').encode('utf-8')  # 定型事件字节
    temporary_path = event_path.with_name(f'.{event_path.name}.{uuid.uuid4().hex}.tmp')  # 在同文件系统生成唯一临时文件
    with temporary_path.open('xb') as temporary_file:  # 排他创建临时载体
        temporary_file.write(event_bytes)  # 先写入全部事件字节
        temporary_file.flush()  # 将Python缓冲区推送给操作系统
        os.fsync(temporary_file.fileno())  # 在发布前持久化事件内容
    try:  # 硬链接发布保证同名事件不被覆盖
        os.link(temporary_path, event_path)  # 以单次原子操作公布完整文件
    finally:  # 成功或冲突都清理未公布的临时名
        temporary_path.unlink(missing_ok=True)  # 不删除任何已发布审计事件
    return hashlib.sha256(event_bytes).hexdigest()  # 返回可复核的事件指纹
列表 10.8: Chapter 10 运行器私有的延迟标签读取器
def _ch10_read_mock_test_labels():  # 教学运行器在预留后才调用此私有回调
    private_positions = np.arange(72)  # 在读取时现场恢复密封序列
    private_targets = (private_positions[2:72] % 8 < 4).astype('int8')  # 只在延迟读取器内物化平衡标签
    private_test_labels = private_targets[56:70].copy()  # 只在私有回调内取得密封测试段
    assert hashlib.sha256(private_test_labels.tobytes()).hexdigest() == '48303c3a6b74724e8f025d386e2208c6875015cd2eaba61a3b961da2e6e64ec8'  # 读取后核对v7标签字节
    return private_test_labels  # 只向已预留的评分服务返回密封测试段
列表 10.9: Chapter 10 可信运行器从冻结行级输入重算时点与预处理
def ch10_recompute_development_contract(frozen_inputs):  # 不信任学生摘要而从教师冻结行逐项恢复MP10-1/2
    feature_columns = frozen_inputs['feature_columns']  # 取得教师冻结的共同特征列
    development_rows = pd.DataFrame(frozen_inputs['development_rows'])  # 恢复权威公开fixture逐行快照
    assert development_rows.columns.tolist() == ['sample_id', 'prediction_date', 'label_date', *feature_columns, 'target', 'split'] and development_rows['sample_id'].is_unique  # 缺列、增列、乱序或重复键均停止
    raw_prediction_dates, raw_label_dates = development_rows['prediction_date'].copy(), development_rows['label_date'].copy()  # 保存待规范核验的原字符串
    development_rows['prediction_date'] = pd.to_datetime(raw_prediction_dates, format='%Y-%m-%d', errors='raise')  # 严格解析预测日
    development_rows['label_date'] = pd.to_datetime(raw_label_dates, format='%Y-%m-%d', errors='raise')  # 严格解析标签日
    assert development_rows['prediction_date'].dt.strftime('%Y-%m-%d').tolist() == raw_prediction_dates.tolist() and development_rows['label_date'].dt.strftime('%Y-%m-%d').tolist() == raw_label_dates.tolist()  # 拒绝非规范日期
    assert development_rows['prediction_date'].is_monotonic_increasing and not development_rows['prediction_date'].duplicated().any() and development_rows['prediction_date'].lt(development_rows['label_date']).all()  # 日期须严格递增且标签在后
    split_order = ['train', 'purge_train_validation', 'validation', 'purge_validation_test', 'test']  # 冻结五段顺序
    split_rank = development_rows['split'].map({name: position for position, name in enumerate(split_order)})  # 将分段映射为顺序秩
    assert split_rank.notna().all() and split_rank.is_monotonic_increasing and set(development_rows['split']) == set(split_order)  # 两组purge必须非空且五段连续
    split_rows = {name: development_rows.loc[development_rows['split'].eq(name)].copy() for name in split_order}  # 恢复每个权威分段
    assert split_rows['test']['target'].isna().all() and development_rows.loc[~development_rows['split'].eq('test'), 'target'].isin([0, 1]).all()  # 测试无标签且开发标签二元
    assert split_rows['train']['label_date'].max() < split_rows['validation']['prediction_date'].min() and split_rows['validation']['label_date'].max() < split_rows['test']['prediction_date'].min()  # 两个边界严格无标签重叠
    derived_boundaries = {'train': [value.date().isoformat() for value in [split_rows['train']['prediction_date'].min(), split_rows['train']['prediction_date'].max(), split_rows['train']['label_date'].max()]], 'validation': [value.date().isoformat() for value in [split_rows['validation']['prediction_date'].min(), split_rows['validation']['prediction_date'].max(), split_rows['validation']['label_date'].max()]], 'test': [value.date().isoformat() for value in [split_rows['test']['prediction_date'].min(), split_rows['test']['prediction_date'].max()]], 'purge_keys': [split_rows['purge_train_validation']['sample_id'].tolist(), split_rows['purge_validation_test']['sample_id'].tolist()]}  # 独立推导时点与purge键
    assert frozen_inputs['time_boundaries'] == derived_boundaries  # 学生摘要只能与运行器推导值交叉核验
    train_features = split_rows['train'][feature_columns].astype('float64')  # 只恢复训练段拟合总体
    derived_preprocessing = {'fit_keys': split_rows['train']['sample_id'].tolist(), 'mean': train_features.mean().to_dict(), 'scale': train_features.std(ddof=0).to_dict()}  # 独立重算训练均值与总体尺度
    assert np.isfinite(train_features.to_numpy()).all() and all(np.isfinite(list(derived_preprocessing[field].values())).all() for field in ['mean', 'scale']) and all(value > 0 for value in derived_preprocessing['scale'].values())  # 特征与参数必须有限且尺度严格为正
    declared_preprocessing = frozen_inputs['preprocessing_record']  # 取得只用于对照的学生摘要
    assert declared_preprocessing['fit_keys'] == derived_preprocessing['fit_keys'] and set(declared_preprocessing['mean']) == set(feature_columns) and set(declared_preprocessing['scale']) == set(feature_columns)  # 禁止未来拟合键或参数列漂移
    assert np.allclose([declared_preprocessing['mean'][name] for name in feature_columns], [derived_preprocessing['mean'][name] for name in feature_columns], rtol=1e-12, atol=1e-12) and np.allclose([declared_preprocessing['scale'][name] for name in feature_columns], [derived_preprocessing['scale'][name] for name in feature_columns], rtol=1e-12, atol=1e-12) and all(declared_preprocessing['scale'][name] > 0 for name in feature_columns)  # 逐值交叉核验且不接受负尺度
    assert frozen_inputs['train_targets'] == split_rows['train']['target'].astype(int).tolist() and frozen_inputs['validation_targets'] == split_rows['validation']['target'].astype(int).tolist() and frozen_inputs['test_size'] == len(split_rows['test'])  # 门禁数组与测试规模必须来自同一行级成员
    return derived_boundaries, derived_preprocessing, ch10_stable_hash(frozen_inputs['development_rows'])  # 只返回运行器推导证据及行级哈希
列表 10.10: Chapter 10 一次评分的输入校验与终态构造助手
def ch10_recompute_development_gate(frozen_inputs):  # 可信运行器只从冻结开发输入重建选择与门禁
    derived_boundaries, derived_preprocessing, development_rows_sha256 = ch10_recompute_development_contract(frozen_inputs)  # 先独立核验MP10-1/2
    training_targets = np.asarray(frozen_inputs['train_targets'], dtype='int8')  # 恢复冻结训练标签
    validation_targets = np.asarray(frozen_inputs['validation_targets'], dtype='int8')  # 恢复冻结验证标签
    training_probabilities = frozen_inputs['candidate_training_probabilities']  # 恢复两候选训练预测
    validation_probabilities = frozen_inputs['candidate_validation_probabilities']  # 恢复两候选验证预测
    training_bce = {name: ch10_binary_metrics(training_targets, np.asarray(values))['bce'] for name, values in training_probabilities.items()}  # 重算训练BCE
    validation_bce = {name: ch10_binary_metrics(validation_targets, np.asarray(values))['bce'] for name, values in validation_probabilities.items()}  # 重算验证BCE
    selected_name = sorted(validation_bce, key=lambda name: (validation_bce[name], name != 'lr'))[0]  # 重演冻结平局规则
    majority_probability = float(training_targets.mean())  # 只从冻结训练标签重算基线概率
    majority_bce = ch10_binary_metrics(validation_targets, np.repeat(majority_probability, len(validation_targets)))['bce']  # 重算验证基线
    mlp_hit_budget = frozen_inputs['mlp_iterations'] >= frozen_inputs['iteration_budget']  # 重算优化预算状态
    diagnostic = {'candidate_training_bce': training_bce, 'candidate_validation_bce': validation_bce, 'majority_validation_bce': majority_bce, 'mlp_hit_iteration_budget': mlp_hit_budget, 'selected_train_validation_gap': validation_bce[selected_name] - training_bce[selected_name]}  # 形成权威诊断
    should_proceed = validation_bce[selected_name] < majority_bce and not (selected_name == 'mlp' and mlp_hit_budget)  # 执行权威go/no-go
    decision = {'status': 'proceed_fixture_only' if should_proceed else 'stop', 'reason': 'development_gate_passed' if should_proceed else 'baseline_or_optimization_gate'}  # 形成权威门禁
    selection = {'status': 'frozen', 'candidate': selected_name, 'candidate_parameters': frozen_inputs['candidate_parameters'][selected_name], 'tie_break': 'lower_complexity_lr'}  # 重建无客户端身份的选择
    gate_evidence = {'development_inputs_sha256': ch10_stable_hash(frozen_inputs), 'development_rows_sha256': development_rows_sha256, 'time_boundaries': derived_boundaries, 'time_boundaries_sha256': ch10_stable_hash(derived_boundaries), 'preprocessing_record': derived_preprocessing, 'preprocessing_sha256': ch10_stable_hash(derived_preprocessing), 'diagnostic_metrics': diagnostic, 'decision': decision}  # 绑定行级来源与运行器重算证据
    return selection, gate_evidence  # 返回运行器权威结果
def ch10_validate_score_request(selection_record, predictions, frozen_inputs):  # 标签读取前完成全部客户输入校验
    selection_snapshot = json.loads(json.dumps(selection_record, ensure_ascii=False, sort_keys=True))  # 防止评分后篡改
    assert not {'submission_id', 'attempt_id', 'gate_evidence', 'gate_evidence_sha256'}.intersection(selection_snapshot)  # 学生不得选择身份或提交门禁对象
    authoritative_selection, gate_evidence = ch10_recompute_development_gate(frozen_inputs)  # 从运行器冻结输入独立重算
    assert selection_snapshot == authoritative_selection  # 客户选择声明必须与权威选择完全一致
    probabilities = np.asarray(predictions, dtype='float64')  # 校验不依赖标签的预测
    assert probabilities.ndim == 1 and np.isfinite(probabilities).all()  # 必须为有限一维序列
    assert ((probabilities >= 0) & (probabilities <= 1)).all()  # 必须为二分类概率
    assert probabilities.size == frozen_inputs['test_size']  # 预测成员数必须来自冻结合同
    return authoritative_selection, gate_evidence, probabilities  # 只返回运行器权威对象
def ch10_binary_metrics(labels, probabilities):  # 只计算两个聚合指标
    clipped = np.clip(probabilities, 1e-12, 1 - 1e-12)  # 保持BCE有限
    return {'bce': float(np.mean(-labels * np.log(clipped) - (1 - labels) * np.log(1 - clipped))), 'accuracy': float(np.mean((probabilities >= 0.5) == labels))}
def ch10_terminal_event(submission_sha256, attempt_sha256, status, **fields):  # 统一追加终态的最小公共字段
    return {'submission_sha256': submission_sha256, 'attempt_sha256': attempt_sha256, 'status': status, f'{status}_at': datetime.now(ZoneInfo('Asia/Shanghai')).isoformat(), **fields}
列表 10.11: Chapter 10 持久读探针与教师证据核验方法
def ch10_service_read_probe(self):  # 从持久事件恢复实际读数而非进程变量
    observation_path = self._submission_dir / 'read-observations.json'  # 唯一读证据存在即已读一次
    return {'test_label_read_count': int(observation_path.is_file())}  # 重启后仍返回同一累计值
def ch10_service_verify_access_log(self, access_log):  # 把学生观察与运行器持久事件交叉核验
    reserved_bytes = (self._submission_dir / 'reserved.json').read_bytes()  # 读取权威预留事件字节
    reserved = json.loads(reserved_bytes)  # 恢复持久选择、预测与门禁
    observation_bytes = (self._submission_dir / 'read-observations.json').read_bytes()  # 读取不可改写事件
    persisted = json.loads(observation_bytes)  # 恢复实际读取后的持久记录
    completed = json.loads((self._submission_dir / 'completed.json').read_text(encoding='utf-8'))  # 读取完成事件
    authoritative_selection, authoritative_gate = ch10_recompute_development_gate(self._frozen_development_inputs)  # 核验时再次重算权威门禁
    for evidence_name, evidence_value in {'selection': authoritative_selection, 'prediction': reserved['prediction'], 'gate_evidence': authoritative_gate}.items():  # 逐项核验三类前置证据
        evidence_sha256 = ch10_stable_hash(evidence_value)  # 从值现场重算哈希
        assert reserved[evidence_name] == evidence_value and reserved[f'{evidence_name}_sha256'] == evidence_sha256  # 核验预留值与哈希
        assert completed[evidence_name] == evidence_value and completed[f'{evidence_name}_sha256'] == evidence_sha256  # 核验完成值与哈希
        assert access_log[evidence_name] == evidence_value and access_log[f'{evidence_name}_sha256'] == evidence_sha256  # 核验学生日志值与哈希
    assert completed['metrics_sha256'] == ch10_stable_hash(completed['metrics'])  # 从持久指标值重算哈希
    assert access_log['metrics'] == completed['metrics'] and access_log['metrics_sha256'] == completed['metrics_sha256']  # 交叉核验指标值与哈希
    persisted_sha256 = hashlib.sha256(observation_bytes).hexdigest()  # 按原始事件字节复算指纹
    assert completed['read_observations_sha256'] == persisted_sha256  # 拒绝替换运行器观察事件
    actual_observed = {'before_read': persisted['before_read'], 'after_read': persisted['after_read']}  # 教师只取读数
    assert access_log['read_observations'] == actual_observed == {'before_read': 0, 'after_read': 1}  # 拒绝伪造前后读数
    return {**actual_observed, 'gate_evidence_sha256': ch10_stable_hash(authoritative_gate), 'metrics_sha256': completed['metrics_sha256'], 'persistent_evidence_sha256': persisted_sha256}  # 返回已核验全链证据
def ch10_service_get_access_log(self):  # 从持久事件组装待教师复核的学生交付日志
    reserved = json.loads((self._submission_dir / 'reserved.json').read_text(encoding='utf-8'))  # 恢复预留选择、预测与门禁
    completed = json.loads((self._submission_dir / 'completed.json').read_text(encoding='utf-8'))  # 恢复持久指标
    observed = json.loads((self._submission_dir / 'read-observations.json').read_text(encoding='utf-8'))  # 恢复实际读数
    return {'selection': reserved['selection'], 'selection_sha256': reserved['selection_sha256'], 'prediction': reserved['prediction'], 'prediction_sha256': reserved['prediction_sha256'], 'gate_evidence': reserved['gate_evidence'], 'gate_evidence_sha256': reserved['gate_evidence_sha256'], 'metrics': completed['metrics'], 'metrics_sha256': completed['metrics_sha256'], 'read_observations': {'before_read': observed['before_read'], 'after_read': observed['after_read']}}  # 交付完整值与哈希但不暴露身份
列表 10.12: Chapter 10 submission 级一次评分方法
def ch10_service_score_once(self, selection_record, predictions):  # 只接收冻结选择与同序概率预测
    selection_snapshot, gate_evidence, predicted_probabilities = ch10_validate_score_request(selection_record, predictions, self._frozen_development_inputs)  # 预留前由运行器重算门禁
    self._submission_dir.parent.mkdir(parents=True, exist_ok=True)  # 教学模拟只创建授权根
    reserved_event = {'contract_id': 'ch10-sequence-audit-v7', 'submission_sha256': self._submission_sha256, 'attempt_sha256': self._attempt_sha256, 'selection': selection_snapshot, 'selection_sha256': ch10_stable_hash(selection_snapshot), 'prediction': predicted_probabilities.tolist(), 'prediction_sha256': ch10_stable_hash(predicted_probabilities.tolist()), 'gate_evidence': gate_evidence, 'gate_evidence_sha256': ch10_stable_hash(gate_evidence), 'test_key_sha256': self._test_key_sha256, 'status': 'reserved', 'reserved_at': datetime.now(ZoneInfo('Asia/Shanghai')).isoformat()}  # 同时持久化权威值与哈希
    try:  # 预留后任何普通异常都追加stopped终态
        reserved_sha256 = ch10_write_event_once(self._submission_dir / 'reserved.json', reserved_event)  # 先发布预留
        if gate_evidence['decision']['status'] != 'proceed_fixture_only':  # 门禁失败不得取得测试标签
            stopped_event = ch10_terminal_event(self._submission_sha256, self._attempt_sha256, 'stopped', reason='development_gate_failed', reserved_sha256=reserved_sha256, gate_evidence=gate_evidence, gate_evidence_sha256=ch10_stable_hash(gate_evidence), test_label_read_count=0); ch10_write_event_once(self._submission_dir / 'stopped.json', stopped_event); raise PermissionError('development_gate_failed')  # 零读取停止
        probe_before = self.read_probe()['test_label_read_count']  # 持久探针必须从零开始
        assert probe_before == 0  # 非零说明状态不一致并停止
        private_labels = np.asarray(self._label_reader(), dtype='int8')  # 预留落盘后才实际取得标签
        observations = {'submission_sha256': self._submission_sha256, 'attempt_sha256': self._attempt_sha256, 'before_read': probe_before, 'after_read': 1}  # 保存实际观察
        observations_sha256 = ch10_write_event_once(self._submission_dir / 'read-observations.json', observations)  # 发布读证据
        assert private_labels.shape == predicted_probabilities.shape  # 密封成员数必须与预测一致
        metrics = ch10_binary_metrics(private_labels, predicted_probabilities)  # 只形成聚合指标
        completed_event = ch10_terminal_event(self._submission_sha256, self._attempt_sha256, 'completed', reserved_sha256=reserved_sha256, selection=selection_snapshot, selection_sha256=ch10_stable_hash(selection_snapshot), prediction=predicted_probabilities.tolist(), prediction_sha256=ch10_stable_hash(predicted_probabilities.tolist()), gate_evidence=gate_evidence, gate_evidence_sha256=ch10_stable_hash(gate_evidence), metrics=metrics, metrics_sha256=ch10_stable_hash(metrics), read_observations_sha256=observations_sha256)  # 绑定选择、预测、门禁、指标及读证据的值与哈希
        ch10_write_event_once(self._submission_dir / 'completed.json', completed_event)  # 追加完成事件
        return metrics  # 客户端只取得BCE与准确率
    except BaseException as scoring_error:  # 保留预留后的可解释失败
        stopped_path = self._submission_dir / 'stopped.json'  # 门禁可能已经发布停止事件
        if not stopped_path.exists(): stopped_event = ch10_terminal_event(self._submission_sha256, self._attempt_sha256, 'stopped', error_type=type(scoring_error).__name__, reserved_sha256=reserved_sha256, test_label_read_count=self.read_probe()['test_label_read_count']); ch10_write_event_once(stopped_path, stopped_event)  # 其他异常补终态
        raise  # 保留原异常阻止不完整结果继续流转
列表 10.13: Chapter 10 一次评分服务的最小绑定类
class _Ch10OneUseTestService:  # 小类只绑定已分块审计的方法
    read_probe, verify_access_log, get_access_log, score_once = ch10_service_read_probe, ch10_service_verify_access_log, ch10_service_get_access_log, ch10_service_score_once
    def __init__(self, label_reader, test_key_sha256, submission_id, attempt_id, frozen_development_inputs):  # 注入两类运行器身份与冻结开发输入
        ch10_recompute_development_gate(frozen_development_inputs)  # 在签发目录与标签读取前验证行级时点和预处理
        assert uuid.UUID(submission_id).hex == submission_id and uuid.UUID(attempt_id).hex == attempt_id  # 运行器必须签发两类规范UUID
        self._label_reader, self._test_key_sha256 = label_reader, test_key_sha256  # 不预装标签
        self._submission_sha256 = ch10_stable_hash({'submission_id': submission_id})  # 只保存身份指纹
        self._attempt_sha256 = ch10_stable_hash({'attempt_id': attempt_id})  # 只保存尝试身份指纹
        self._frozen_development_inputs = json.loads(json.dumps(frozen_development_inputs, sort_keys=True))  # 防止评分期间修改权威开发输入
        self._submission_dir = Path(os.environ['BOOK_CH10_TEST_AUDIT_DIR']).expanduser().resolve() / self._submission_sha256  # 持久槽
        self._submission_dir.mkdir(parents=True, exist_ok=False)  # 由运行器排他签发全新submission目录
        issued_event = {'contract_id': 'ch10-sequence-audit-v7', 'submission_sha256': self._submission_sha256, 'attempt_sha256': self._attempt_sha256, 'issued_by': 'instructor_runner'}  # 冻结私有身份指纹
        ch10_write_event_once(self._submission_dir / 'issued.json', issued_event)  # 在接收学生请求前发布签发事件
列表 10.14: Chapter 10 先独立构造公开 fixture,暂不签发评分服务
def ch10_build_audit_fixture():  # 先组装学生开发所需公开fixture而不读取教师身份或开发输入文件
    fixture_dates = pd.date_range('2024-01-02', periods=72, freq='B')  # 冻结教学日期轴
    fixture_positions = np.arange(72)  # 冻结位置索引而不调用随机数
    fixture_levels = 100 + 0.04 * fixture_positions + 0.3 * ((fixture_positions % 8) - 4)  # 构造非经验教学轨迹
    fixture_sample_ids = np.arange(1, 71)  # 为70个一步预测样本建立稳定主键
    public_fixture = pd.DataFrame({'fixture_version': 'ch10-sequence-audit-v7', 'sample_id': fixture_sample_ids, 'prediction_date': fixture_dates[1:71], 'label_date': fixture_dates[2:72], 'lag_1': fixture_levels[1:71], 'lag_2': fixture_levels[0:70], 'target': pd.Series(pd.NA, index=fixture_sample_ids - 1, dtype='Int8')})  # 测试标签从未进入学生builder
    public_fixture['split'] = np.select([public_fixture['sample_id'].le(40), public_fixture['sample_id'].le(42), public_fixture['sample_id'].le(54), public_fixture['sample_id'].le(56)], ['train', 'purge_train_validation', 'validation', 'purge_validation_test'], default='test')  # 在相邻集合之间留两行purge
    public_positions = np.arange(58)  # 只恢复测试前所需的开发期轨迹
    public_fixture.loc[public_fixture['sample_id'].le(56), 'target'] = (public_positions[2:58] % 8 < 4).astype('int8')  # 仅填充训练、purge与验证标签
    test_mask = public_fixture['split'].eq('test')  # 在公开对象中标记无标签测试段
    test_keys = public_fixture.loc[test_mask, ['sample_id', 'prediction_date', 'label_date']]  # 只用公开键绑定测试成员
    test_key_bytes = test_keys.to_csv(index=False, date_format='%Y-%m-%d', lineterminator='\n').encode('utf-8')  # 按稳定规则序列化测试键
    test_key_sha256 = hashlib.sha256(test_key_bytes).hexdigest()  # 保存不含标签的成员哈希
    fixture_sha256 = 'df86d5fb7c67fd7d4a7801dae85d90f609b46bfe043d91afb55b1e0ca1b4bd08'  # 公开外部运行器冻结的v7内容指纹
    return public_fixture, test_key_sha256, fixture_sha256  # 开发期只返回无标签公开对象与成员哈希

ch10_audit_fixture, ch10_test_key_sha256, ch10_fixture_sha256 = ch10_build_audit_fixture()  # 先完成开发输入,稍后才由教师CLI签发服务

10.9.2 consumer 与提交接口

顺序必须是“公开 fixture → 学生开发期计算 → 教师从权威 fixture 独立导出并冻结行级快照 → 运行器交叉核验学生摘要 → 签发服务 → 学生提交无身份证据 → 教师评分核验”。因此 列表 10.14 只建立无测试标签的公开 fixture;学生先完成 列表 10.15列表 10.16列表 10.17。教师不能把学生提交的 time_boundariespreprocessing_record 或其哈希当作权威证据:development_rowsfeature_columns 必须由教师从同版 fixture 重新导出,连同学生候选预测保存为只读 JSON,再运行下文 provision。学生提交包只能含 selectionpredictions,客户端自报的 gate_evidencesubmission_idattempt_id 会在预留前失败。服务在签发和标签读取前从教师行级文件重新计算日期顺序、五段成员、两组 purge、训练拟合键、均值/尺度、候选选择、基线、诊断与 go/no-go;任一摘要不符都不签发目录或 verified 量规,门禁失败则追加 stopped.json 且读数为 0,只有全部通过才读取标签并追加 read-observations.jsoncompleted.json

scripts/ch10_instructor_harness.py 是本节的可执行权威实现;正文分块用于解释字段,不能替代隔离进程与目录权限。该实现把选择、预测、门禁和指标的值与哈希同时写入 reserved.json/completed.json,教师核验时再从冻结输入重算门禁并逐项交叉检查。真实部署还必须使审计根和冻结开发输入对学生只读或不可见。

安全边界声明:这里的延迟回调是可执行的流程教具,不是敌对性安全边界;同一 Python 解释器内的高权限用户仍可以内省或调用私有回调。真实保密必须由外部评分运行器保持标签文件/进程对学生不可读,只向学生暴露“冻结选择 + 预测 → 聚合指标”服务,并使审计根对学生不可删除、覆盖或替换。教学 mock 只能证明时序与 fail-closed 语义,不能声称真实秘密性。

列表 10.15: Chapter 10 核心任务的时间、模式与训练期预处理守卫
required_columns = ['fixture_version', 'sample_id', 'prediction_date', 'label_date', 'lag_1', 'lag_2', 'target', 'split']  # 冻结消费端列名与顺序
assert ch10_audit_fixture.columns.tolist() == required_columns  # 缺列、额外列或乱序均结构化停止
assert ch10_audit_fixture['sample_id'].is_unique  # 主键重复会破坏访问审计
assert ch10_audit_fixture['prediction_date'].lt(ch10_audit_fixture['label_date']).all()  # 标签日期必须晚于预测日期
ch10_train = ch10_audit_fixture.loc[ch10_audit_fixture['split'].eq('train')].copy()  # 提取最早训练段
ch10_validation = ch10_audit_fixture.loc[ch10_audit_fixture['split'].eq('validation')].copy()  # 提取中间验证段
ch10_test_features = ch10_audit_fixture.loc[ch10_audit_fixture['split'].eq('test')].drop(columns='target').copy()  # 公开测试特征不含标签列
ch10_purge_train_validation = ch10_audit_fixture.loc[ch10_audit_fixture['split'].eq('purge_train_validation')].copy()  # 保留第一段purge审计键
ch10_purge_validation_test = ch10_audit_fixture.loc[ch10_audit_fixture['split'].eq('purge_validation_test')].copy()  # 保留第二段purge审计键
assert ch10_audit_fixture.loc[ch10_audit_fixture['split'].eq('test'), 'target'].isna().all()  # 章节全局对象不得暴露任何测试标签
assert ch10_purge_train_validation['sample_id'].tolist() == [41, 42]  # 第一purge键必须与合同一致
assert ch10_purge_validation_test['sample_id'].tolist() == [55, 56]  # 第二purge键必须与合同一致
assert ch10_train['label_date'].max() < ch10_validation['prediction_date'].min()  # 训练标签不得越过验证预测日
assert ch10_validation['label_date'].max() < ch10_test_features['prediction_date'].min()  # 验证标签不得越过测试预测日
ch10_feature_columns = ['lag_1', 'lag_2']  # 冻结两个候选模型的共同输入
ch10_train_mean = ch10_train[ch10_feature_columns].mean()  # 只用训练段拟合中心参数
ch10_train_scale = ch10_train[ch10_feature_columns].std(ddof=0)  # 只用训练段拟合尺度参数
assert ch10_train_scale.gt(0).all()  # 零方差特征必须停止而非静默替换
print({'contract_id': 'ch10-sequence-audit-v7', 'sha256': ch10_fixture_sha256, 'split_sizes': ch10_audit_fixture.groupby('split').size().to_dict(), 'purge_train_validation_keys': ch10_purge_train_validation['sample_id'].tolist(), 'purge_validation_test_keys': ch10_purge_validation_test['sample_id'].tolist()})  # 输出严格边界与purge键证据
{'contract_id': 'ch10-sequence-audit-v7', 'sha256': 'df86d5fb7c67fd7d4a7801dae85d90f609b46bfe043d91afb55b1e0ca1b4bd08', 'split_sizes': {'purge_train_validation': 2, 'purge_validation_test': 2, 'test': 14, 'train': 40, 'validation': 12}, 'purge_train_validation_keys': [41, 42], 'purge_validation_test_keys': [55, 56]}
列表 10.16: Chapter 10 只用开发段比较 LR 与小型 MLP
from sklearn.linear_model import LogisticRegression  # 建立低复杂度概率基线
from sklearn.neural_network import MLPClassifier  # 建立单隐层小型网络候选
from sklearn.metrics import log_loss  # 用同一BCE比较训练与验证
ch10_train_features = (ch10_train[ch10_feature_columns] - ch10_train_mean) / ch10_train_scale  # 只用训练期参数转换训练段
ch10_validation_features = (ch10_validation[ch10_feature_columns] - ch10_train_mean) / ch10_train_scale  # 不重拟合地转换验证段
ch10_scaled_test_features = (ch10_test_features[ch10_feature_columns] - ch10_train_mean) / ch10_train_scale  # 只转换无标签测试特征
ch10_candidate_models = {'lr': LogisticRegression(C=1.0, max_iter=30, random_state=42), 'mlp': MLPClassifier(hidden_layer_sizes=(4,), alpha=0.01, solver='lbfgs', max_iter=30, random_state=42)}  # 冻结两候选与共同资源上限
for candidate_model in ch10_candidate_models.values():  # 两候选都只拟合最早训练段
    candidate_model.fit(ch10_train_features, ch10_train['target'].astype(int))  # 禁止验证或测试标签进入拟合
ch10_validation_probabilities = {name: model.predict_proba(ch10_validation_features)[:, 1] for name, model in ch10_candidate_models.items()}  # 只在验证段产生选择证据
ch10_validation_bce = {name: log_loss(ch10_validation['target'].astype(int), probabilities, labels=[0, 1]) for name, probabilities in ch10_validation_probabilities.items()}  # 按同一标签口径计算BCE
ch10_selected_name = sorted(ch10_validation_bce, key=lambda name: (ch10_validation_bce[name], name != 'lr'))[0]  # BCE并列时预先选低复杂度LR
ch10_selected_model = ch10_candidate_models[ch10_selected_name]  # 冻结现场验证胜者
ch10_selected_test_predictions = ch10_selected_model.predict_proba(ch10_scaled_test_features)[:, 1]  # 冻结同测试键升序的概率预测
列表 10.17: Chapter 10 测试访问前冻结开发期门禁
ch10_preprocessing_record = {'mean': ch10_train_mean.to_dict(), 'scale': ch10_train_scale.to_dict(), 'fit_keys': ch10_train['sample_id'].tolist()}  # 冻结训练期预处理参数与拟合键
ch10_training_bce = {name: log_loss(ch10_train['target'].astype(int), model.predict_proba(ch10_train_features)[:, 1], labels=[0, 1]) for name, model in ch10_candidate_models.items()}  # 只用开发段计算差距
ch10_majority_probability = float(ch10_train['target'].mean())  # 只用训练标签建立多数类概率基线
ch10_majority_validation_bce = log_loss(ch10_validation['target'].astype(int), np.repeat(ch10_majority_probability, len(ch10_validation)), labels=[0, 1])  # 在验证段评价基线
ch10_diagnostic_metrics = {'report_label': 'ch10_diagnostic_metrics', 'majority_validation_bce': ch10_majority_validation_bce, 'candidate_validation_bce': ch10_validation_bce, 'selected_train_validation_gap': ch10_validation_bce[ch10_selected_name] - ch10_training_bce[ch10_selected_name], 'mlp_hit_iteration_budget': ch10_candidate_models['mlp'].n_iter_ >= 30}  # 冻结开发期诊断
ch10_should_proceed = ch10_validation_bce[ch10_selected_name] < ch10_majority_validation_bce and not (ch10_selected_name == 'mlp' and ch10_diagnostic_metrics['mlp_hit_iteration_budget'])  # 测试前执行go/no-go
ch10_decision = {'report_label': 'ch10_decision', 'status': 'proceed_fixture_only' if ch10_should_proceed else 'stop', 'reason': 'development_gate_passed' if ch10_should_proceed else 'baseline_or_optimization_gate'}  # 测试前冻结决策
ch10_development_time_boundaries = {'train': [value.date().isoformat() for value in [ch10_train['prediction_date'].min(), ch10_train['prediction_date'].max(), ch10_train['label_date'].max()]], 'validation': [value.date().isoformat() for value in [ch10_validation['prediction_date'].min(), ch10_validation['prediction_date'].max(), ch10_validation['label_date'].max()]], 'test': [value.date().isoformat() for value in [ch10_test_features['prediction_date'].min(), ch10_test_features['prediction_date'].max()]], 'purge_keys': [[41, 42], [55, 56]]}  # 把MP10-1边界转为规范日历日字符串
ch10_development_rows = [{'sample_id': int(row.sample_id), 'prediction_date': row.prediction_date.date().isoformat(), 'label_date': row.label_date.date().isoformat(), 'lag_1': float(row.lag_1), 'lag_2': float(row.lag_2), 'target': None if pd.isna(row.target) else int(row.target), 'split': row.split} for row in ch10_audit_fixture.itertuples(index=False)]  # 教师从权威公开fixture导出可独立复算的行级快照
ch10_development_inputs = {'candidate_parameters': {name: model.get_params(deep=False) for name, model in ch10_candidate_models.items()}, 'candidate_training_probabilities': {name: model.predict_proba(ch10_train_features)[:, 1].tolist() for name, model in ch10_candidate_models.items()}, 'candidate_validation_probabilities': {name: values.tolist() for name, values in ch10_validation_probabilities.items()}, 'development_rows': ch10_development_rows, 'feature_columns': ch10_feature_columns, 'iteration_budget': 30, 'mlp_iterations': int(ch10_candidate_models['mlp'].n_iter_), 'preprocessing_record': ch10_preprocessing_record, 'test_size': len(ch10_test_features), 'time_boundaries': ch10_development_time_boundaries, 'train_targets': ch10_train['target'].astype(int).tolist(), 'validation_targets': ch10_validation['target'].astype(int).tolist()}  # 行级快照是权威来源,摘要只供运行器交叉核验
ch10_gate_preview = {'preprocessing_sha256': ch10_stable_hash(ch10_preprocessing_record), 'diagnostic_metrics': ch10_diagnostic_metrics, 'decision': ch10_decision}  # 学生本地预览只用于反馈且不是评分依据

教师在隔离进程中从同版 ch10_audit_fixture 独立导出 development_rows/feature_columns,再把学生候选预测合入 ch10_development_inputs,保存到学生不可写的 BOOK_CH10_FROZEN_DEVELOPMENT_INPUTS 后才执行 provision。教师不得复制学生自报的行级快照;学生的 time_boundariespreprocessing_record 只作为待交叉核验摘要。若使用本章教学 mock 演示 API,下面的绑定也必须在该文件存在且行级来源已复核时才执行;真实 A10 以 scripts/ch10_instructor_harness.py 的跨进程命令为准。

列表 10.18: Chapter 10 开发输入冻结后才绑定评分服务
ch10_frozen_development_path = Path(os.environ['BOOK_CH10_FROZEN_DEVELOPMENT_INPUTS']).expanduser().resolve()  # 教师先提供只读冻结文件
ch10_frozen_development_inputs = json.loads(ch10_frozen_development_path.read_text(encoding='utf-8'))  # 从教师文件独立恢复输入
assert ch10_stable_hash(ch10_frozen_development_inputs) == ch10_stable_hash(ch10_development_inputs)  # 拒绝冻结前后漂移
ch10_submission_id = os.environ['BOOK_CH10_SUBMISSION_ID']  # 只从运行器私有环境取得签发身份
ch10_attempt_id = os.environ['BOOK_CH10_ATTEMPT_ID']  # 只从运行器私有环境取得尝试身份
ch10_test_service = _Ch10OneUseTestService(_ch10_read_mock_test_labels, ch10_test_key_sha256, ch10_submission_id, ch10_attempt_id, ch10_frozen_development_inputs)  # 冻结后才建立服务
ch10_score_test_once = ch10_test_service.score_once  # 暴露无身份评分调用
ch10_test_read_probe = ch10_test_service.read_probe  # 暴露持久读探针
ch10_get_test_access_log = ch10_test_service.get_access_log  # 暴露待教师核验日志
列表 10.19: Chapter 10 学生端提交无身份选择与预测并且只评分一次
ch10_selection_record = {'status': 'frozen', 'candidate': ch10_selected_name, 'candidate_parameters': ch10_selected_model.get_params(deep=False), 'tie_break': 'lower_complexity_lr'}  # 学生不得提交submission、attempt或gate字段
ch10_probe_before_score = ch10_test_read_probe()  # 首次评分前实际标签读数必须为零
ch10_test_metrics = ch10_score_test_once(ch10_selection_record, ch10_selected_test_predictions)  # 绑定服务用运行器身份并重算开发门禁
ch10_probe_after_score = ch10_test_read_probe()  # 首次评分后实际标签读数必须为一
assert ch10_probe_before_score['test_label_read_count'] == 0 and ch10_probe_after_score['test_label_read_count'] == 1  # 证明预留后恰好读取一次
assert set(ch10_test_metrics) == {'bce', 'accuracy'}  # 学生返回值不得含标签或审计内部
ch10_test_access_log = ch10_get_test_access_log()  # 从运行器持久事件取得待教师核验的值与哈希
列表 10.20: Chapter 10 参考路径的五份完整量规记录
ch10_time_boundaries = {'report_label': 'ch10_time_boundaries', 'train': [ch10_train['prediction_date'].min(), ch10_train['prediction_date'].max(), ch10_train['label_date'].max()], 'validation': [ch10_validation['prediction_date'].min(), ch10_validation['prediction_date'].max(), ch10_validation['label_date'].max()], 'test': [ch10_test_features['prediction_date'].min(), ch10_test_features['prediction_date'].max()], 'purge_keys': [[41, 42], [55, 56]]}  # 交付时间与purge边界
ch10_preprocessing_fit = {'report_label': 'ch10_preprocessing_fit', 'preprocessing_sha256': ch10_stable_hash(ch10_preprocessing_record), 'train_mean': ch10_train_mean.to_dict(), 'train_scale': ch10_train_scale.to_dict()}  # 交付仅训练期拟合证据
ch10_test_access_log['report_label'] = 'ch10_test_access_log'  # 为运行器日志补充唯一评分标签
for ch10_rubric_record in [ch10_time_boundaries, ch10_preprocessing_fit, ch10_test_access_log, ch10_diagnostic_metrics, ch10_decision]:  # 逐份输出量规所需的唯一报告标签
    print(ch10_rubric_record)  # 保持五份证据可独立评分
列表 10.21: Chapter 10 教师端隔离harness的四类命令
ch10_instructor_commands = ['conda run -n peter python scripts/ch10_instructor_harness.py --self-test', 'conda run -n peter python scripts/ch10_instructor_harness.py provision ...', 'conda run -n peter python scripts/ch10_instructor_harness.py score-verify ...', 'conda run -n peter python scripts/ch10_instructor_harness.py resubmit ...']  # 登记课前、签发、评分与补发入口
print(ch10_instructor_commands)  # 教师课前核对四类入口均存在
['conda run -n peter python scripts/ch10_instructor_harness.py --self-test', 'conda run -n peter python scripts/ch10_instructor_harness.py provision ...', 'conda run -n peter python scripts/ch10_instructor_harness.py score-verify ...', 'conda run -n peter python scripts/ch10_instructor_harness.py resubmit ...']

学生规范路径在 列表 10.17 前不产生测试指标。学生先在验证段比较 LR/MLP 并形成候选摘要;教师从权威 fixture 独立冻结行级成员与特征,再由运行器重算并核验日期严格有序、两组 purge 非空且匹配、训练/验证标签早于下一段预测、拟合键全属训练段、均值有限、尺度有限且严格为正。任何非法组合都在身份、目录、预留与标签读取前失败,不能生成 verified 量规。通过后运行器才签发私有 submission/attempt 身份,并只接受无身份、无门禁对象的冻结选择声明与预测。ch10_score_test_once 继续重算选择、基线、训练—验证差距、优化预算与 go/no-go,只返回 BCE 与准确率。教师用 verify_access_log 交叉核验 reserved.jsoncompleted.json 和学生日志中的选择、预测、门禁、指标的值与哈希,并核对真实事件字节链接与 0 → 1

教师补救与重交政策:旧 submission 只有在已经追加唯一 stopped.json、不存在 completed.json,且持久读探针与停止事件都验证 test_label_read_count == 0 时,教师隔离运行器才可接受一份纠正后的冻结开发输入文件并签发全新的 submission/attempt 身份。新 issued.json 同时保存 corrected_development_inputs_sha256replaces_submission_sha256replaces_audit_sha256,旧目录保持只读且永不重开。旧 submission 若已 completed、已读取一次,或没有可验证的停止终态,一律不得重交;学生不能建议、提交或轮换任何 ID。新 submission 对纠正证据实际产生 0 → 1 后,再次调用必须在标签读取前失败且持久读数仍为 1。

教师从仓库根目录先运行课前自检:

conda run -n peter python scripts/ch10_instructor_harness.py --self-test

唯一通过条件是进程退出码为 0,JSON 同时报告 status: verifiedrow_level_evidence_verified: trueinvalid_development_inputs_rejected 下的 dates/purge/scale/fit_keys 全为 trueverified_probe_sequence: [0, 1, 1]client_gate_rejected: truecompleted_resubmission_blocked: trueread_one_resubmission_blocked: truereplacement_reentry_blocked: truezero_read_resubmission_issued: truereplacement_links_old_audit: true。实际发放时,教师先从权威 fixture 独立导出行级输入并把复核后的完整开发包保存为 /teacher/a10/development.json,再依次执行:

conda run -n peter python scripts/ch10_instructor_harness.py provision --audit-root /teacher/a10/audit --development-inputs /teacher/a10/development.json
conda run -n peter python scripts/ch10_instructor_harness.py score-verify --audit-root /teacher/a10/audit --submission-handle RUNNER_RETURNED_HANDLE --development-inputs /teacher/a10/development.json --student-evidence /teacher/a10/student-evidence.json --test-labels /teacher/private/ch10-labels.json --rubric-output /teacher/a10/rubric.json

development.json 必须含教师导出的 development_rowsfeature_columns;运行器从它们重算 MP10-1/2,再与 time_boundariespreprocessing_record、训练/验证目标和测试规模逐值交叉。student-evidence.json 顶层必须恰好只有 selectionpredictionsrubric.json 输出 MP10-1—MP10-3、DIAG10-1、LIMIT10-1,其中 MP10-1/2 绑定同一 development_rows_sha256 且只使用运行器推导值,不含 submission/attempt 身份或测试标签。若第一次 score-verify 返回 development_gate_failed 且读数 0,教师复核纠正输入后运行:

conda run -n peter python scripts/ch10_instructor_harness.py resubmit --audit-root /teacher/a10/audit --old-submission-handle OLD_HANDLE --old-development-inputs /teacher/a10/development.json --corrected-development-inputs /teacher/a10/corrected-development.json
conda run -n peter python scripts/ch10_instructor_harness.py score-verify --audit-root /teacher/a10/audit --submission-handle NEW_RUNNER_HANDLE --development-inputs /teacher/a10/corrected-development.json --student-evidence /teacher/a10/corrected-evidence.json --test-labels /teacher/private/ch10-labels.json --rubric-output /teacher/a10/corrected-rubric.json

任何命令字段缺失、日期非严格递增、分段乱序、purge 为空或不匹配、标签跨界、拟合键含验证/测试成员、均值/尺度不符、尺度非有限或非正、目标/规模漂移或哈希不符,都在标签读取前停止 A10 流程且不得输出 verified 量规;不得以手工删除旧审计目录补救。

时间安排为:0—15 分钟生成待核验的 ch10_time_boundaries;15—30 分钟生成待核验的 ch10_preprocessing_fit;30—45 分钟比较 LR/MLP 并提交候选摘要;45—50 分钟由教师运行器从行级快照重算 MP10-1/2、ch10_diagnostic_metricsch10_decision,任一核验或门禁失败即以总读数 0 停止;50—60 分钟仅对通过门禁的 submission 评分一次并生成 ch10_test_access_log。教师导出行级快照、全链证据核验与重交状态机在课前/课后隔离 harness 完成,不占学生60分钟。

表 10.3: A10 必做独立评价的报告标签与评分证据
评分键 目标 首次证据与报告标签 分值 结构化失败
MP10-1 3 运行器从教师冻结 development_rows 重算的 ch10_time_boundaries:三段最小/最大预测日、最大标签日、两组非空 purge 键、development_rows_sha256 与边界哈希 2 CH10-MP1-TIME
MP10-2 3 运行器从同一行级快照训练段重算的 ch10_preprocessing_fit:训练均值/正尺度、完整拟合键、development_rows_sha256 与参数哈希 2 CH10-MP2-PREPROCESS
MP10-3 3 学生 ch10_test_access_log:不含客户端 ID,含选择、预测、运行器重算门禁、指标及实际 0 → 1 的值与哈希;教师 harness:交叉核验 reserved/completed 全链并验证完成态不可重开、仅零读取停止态可签发有旧审计链接的新 submission 2 CH10-MP3-TEST-ACCESS
DIAG10-1 4 ch10_diagnostic_metrics:多数类基线、两候选验证 BCE、训练—验证差距与优化失败判据 3 CH10-DIAG-MISSING
LIMIT10-1 4 ch10_decision:若 MLP 未胜基线或资源越界则停止上线,且声明 fixture 非经验数据 1 CH10-LIMIT-OVERCLAIM

MP10-1—MP10-3 任一失败时总分最高 4 分;DIAG10-1 缺基线、差距或优化失败判据中的任一项,该项为 0 分。A10 在两条标准路线中都必做、不可替代,且不计作贯穿项目里程碑。以上证据必须由本 fixture 独立生成,不得复用 图 10.5、正文测试结果或练习 6—7 的扩展输出。

10.10 小结

本章介绍了深度学习的基础概念和应用。深度学习之所以强大是因为

  1. 联合表示学习:可从任务损失中学习部分中间表示,但仍需要输入、时点与业务约束设计
  2. 端到端学习: 从原始数据直接学习到最终输出
  3. 强大的表达能力: 在 小节 10.3.1 的通用逼近条件下,有限宽度单隐层网络对紧致域上的连续函数可以达到任意给定的一致误差;这是存在性结论,不保证优化成功或样本外泛化
  4. 条件性可扩展: 更多数据和计算可以支持更大模型与更充分的搜索,但性能是否提升仍取决于数据质量、分布漂移、结构偏好、优化和冻结评价设计

深度学习的挑战

  1. 数据需求: 通常需要大量标注数据
  2. 计算资源: 训练深度模型需要大量计算资源
  3. 可解释性: 深度学习模型通常是黑盒”
  4. 超参数调优: 有许多超参数需要调整
  5. 过拟合风险: 深度模型容易过拟合训练数据

何时使用深度学习:

  • 有大量数据可用
  • 问题具有复杂的模式
  • 传统方法性能瓶颈
  • 有足够的计算资源

Python深度学习库

  • TensorFlow/Keras: Google开发最流行的深度学习框架
  • PyTorch: Facebook开发研究和生产中的热门选择
  • Scikit-learn: 提供MLPClassifier等简单的神经网络实现

10.11 理论来源与前沿

深度学习的发展既依赖算法与理论突破(反向传播、非凸优化、正则化与初始化),也依赖数据与算力的工程进步(GPU、分布式训练)。从统计学习视角看,深度网络是一个高度灵活的函数族,其泛化性能受到隐式正则化、网络结构偏置与优化路径的共同影响。

近年来的前沿趋势包括:

  1. 大模型与迁移学习:预训练-微调范式降低下游任务的数据需求,在文本、视觉与多模态中尤为突出。
  2. 对齐与安全:关注模型输出可靠性、偏见控制与对抗鲁棒性,适配高风险部署。
  3. 可解释性与监控:在金融与医疗等领域,需要可解释方法与上线后漂移监控体系。

10.12 练习

10.12.1 概念题

  1. [核心|难度:1|时间:8分钟|分值:4|项目:无] 解释深度神经网络中的“表示学习(representation learning)”是什么意思。它与手工特征工程的关系是什么?

  2. [核心|难度:2|时间:10分钟|分值:5|项目:无] 按“连接范围、参数共享、跨时点状态”三列比较 MLP、CNN、RNN/LSTM;再分别指出图像平移分类与月度设备故障序列更匹配哪一种结构偏好,以及这种偏好为什么不保证样本外优胜。

  3. [核心|难度:2|时间:10分钟|分值:5|项目:无] Dropout 与\(\ell_2\) 正则化的作用相同吗?它们分别更擅长缓解什么类型的过拟合?

  4. [拓展|难度:2|时间:10分钟|分值:4|项目:无] Batch Normalization(或 LayerNorm)在训练中通常带来哪些好处?它是否一定提升泛化?

  5. [核心|难度:2|时间:8分钟|分值:4|项目:无] 早停(early stopping)为什么可以被视为一种正则化?

10.12.2 应用题

  1. [拓展|难度:3|时间:45分钟|分值:16|项目:无] 使用你本机A 股数据构造一个分类或回归任务(例如“下月是否跑赢基准未来波动率预测”),比较:

    • 线性模型(逻辑回归/线性回归)
    • 多层感知机(MLP)

要求:统一采用时间切分评估;报告测试集指标与过拟合迹象(训练测试差距),并给出你选择网络宽度、深度与正则化强度的依据。

  1. [拓展|难度:3|时间:35分钟|分值:12|项目:无] 设计一个“特征归一化+ 学习率策略”的消融实验:保持模型结构不变,只改变

    • 是否标准化输入
    • 学习率(常数 vs 余弦退火分段下降)

比较收敛速度与最终测试指标。

  1. [核心|难度:2|时间:15分钟|分值:8|项目:无] 如果你的样本量不大(典型的结构化表格金融数据),说明你会如何优先选择模型与训练策略,使其更稳健(例如更小网络、强正则、交叉验证、集成等)。

10.12.3 理论题

  1. [核心|难度:2|时间:15分钟|分值:8|项目:无] 对二分类的逻辑回归/神经网络输出层,设\(p=\sigma(z)\),交叉熵损失为\(\ell(y,p)=-[y\log p+(1-y)\log(1-p)]\)。推导\(\frac{\partial \ell}{\partial z}\) 的简洁形式,并解释它为何有利于数值稳定训练。本题是目标 1 的必修梯度桥梁;练习 11 仍是完整前向—反向—更新的终端核心证据。

  2. [拓展|难度:3|时间:20分钟|分值:10|项目:无] 以两层网络(输入-隐层-输出)为例,写出反向传播计算梯度的链式法则结构,说明梯度如何从输出层逐层传回。本题推广矩阵形式;核心晋级仍由练习 11 的完整数值证据判定。

  3. [核心|难度:3|时间:20分钟|分值:12|项目:无] 对一个样本的两输入—单隐单元—单输出网络,给定 \(x=(2,-1)^\top,y=0\)\(w=(0.1,0.3)^\top,b=-0.2,v=-0.4,c=0.15\),两层均用 sigmoid,损失为二元交叉熵,学习率 \(\eta=0.05\)。依次计算:(a) 隐层预激活 \(s=w^\top x+b\)\(h=\sigma(s)\)、输出 logit \(z=vh+c\)\(\widehat y=\sigma(z)\) 与 BCE;(b) 四组梯度 \(\nabla_wL,\partial L/\partial b,\partial L/\partial v,\partial L/\partial c\);(c) 一次梯度下降后的 \(w^+,b^+,v^+,c^+\)\(\widehat y^+\)。每个标量及每个向量分量允许 \(10^{-3}\) 绝对误差,并用“\(y=0\) 时更新后概率应下降”做方向检查。

10.13 练习参考解答

展开完整解答、评分点与常见失败模式

评分以题面分值为准;手算允许 \(10^{-3}\) 绝对误差,随机训练结果不设固定数值容差,但必须保持同一切分、种子和预算。应用题若缺基线、训练—测试差距或测试集被用于选模,各扣 25%;把训练损失下降解释为预测有效,结论项不得分。

10.13.1 概念题参考解答

  1. 表示学习:模型自动从原始输入中学习到对任务有用的中间表示(特征),而不是完全依赖手工设计的因子/规则。对结构化金融数据而言,手工特征仍重要,深度模型更多用于自动组合与非线性拟合。

  2. 架构比较:MLP 通常全连接、不同位置不共享参数、没有跨时点状态;CNN 只连接局部感受野并在位置间共享卷积核,本身提供平移等变结构;RNN/LSTM 在时间步间共享转移参数并递推隐藏状态。图像任务可优先检验 CNN,月度序列可优先检验 RNN/LSTM,但最终选择仍取决于冻结验证集与同键简单基线,结构偏好不等于泛化保证。

  3. Dropout vs \(\ell_2\):二者都能抑制过拟合,但机制不同:\(\ell_2\) 直接惩罚权重大小、偏向更平滑的函数;Dropout 训练时随机屏蔽神经元,近似对大量子网络做模型平均,更能缓解共适应(co-adaptation)。

  4. 归一化的好处与限制:常见好处是加速收敛、改善条件数、提高训练稳定性并允许更大学习率;但并不保证一定提升泛化,且对小批量、分布漂移等场景需要谨慎。

  5. 早停视为正则化:在优化过程中,模型从简单到复杂逐步拟合数据;过长训练会把噪声也拟合进去。早停相当于限制了有效复杂度(类似控制参数范数或隐式正则)。

10.13.2 应用题参考解答(模板)

  1. 线性模型vs MLP(结构化数据):建议先用线性模型作为强基线,再用小型MLP(2 个隐层)并加强正则(权重衰减、早停)。

10.13.2.1 第 6—7 题唯一共享 setup

第 6、7 题共同消费下面从数据读取到三段切分的显式 setup;fresh kernel 单独执行第 7 题时,必须先执行本小节标记的加载与 lst-ex6-features-split,不得借用正文或其他答案对象。setup 之外两题的模型、随机种子和输出互不共享。

补充说明:为什么这里比较 MLP 时更适合报告 AUC

当任务是预测“明天涨还是跌”这类二分类问题时,很多神经网络教材会先让学生看准确率;但在结构化金融数据中,AUC 往往更能反映模型是否真的学到了可排序的信号。

原因在于,MLP 和逻辑回归都先输出一个概率分数。若只看准确率,你等于默认把阈值固定在0.5;可这个阈值未必是业务上最优的。AUC 则不依赖某一个固定阈值,而是评价:模型能否把更可能上涨的样本整体排在更前面。

这对于比较线性模型和 MLP 尤其重要:

  • 如果 MLP 的 AUC 明显高于逻辑回归,说明它确实从非线性结构中提取到了额外排序信息;
  • 如果两者 AUC 差不多,甚至 MLP 更差,那就说明更复杂的网络结构并没有带来稳定收益,反而可能只是增加了训练不确定性。

换句话说,AUC 更像是在比较“谁更会排队”,而准确率更像是在比较“在某一条门槛线上谁判得更对”。做模型研究时先看前者,做业务执行时再结合阈值去看后者,通常更稳妥。

import pandas as pd  # 导入pandas用于数据处理
import numpy as np  # 导入numpy用于数值计算
from sklearn.preprocessing import StandardScaler  # 导入标准化工具
from sklearn.pipeline import Pipeline  # 导入机器学习管道工具
from sklearn.linear_model import LogisticRegression  # 导入Logistic回归分类器
from sklearn.neural_network import MLPClassifier  # 导入多层感知器分类器
from sklearn.metrics import roc_auc_score  # 导入AUC评估指标

# 1. 加载数据
import os  # 导入操作系统模块用于跨平台路径处理
from pathlib import Path  # 使用跨平台路径对象解析显式数据根
DATA_DIR = Path(os.environ['BOOK_DATA_DIR']).expanduser().resolve()  # 从必需环境变量取得数据根
if not DATA_DIR.is_dir():  # 在读取前验证数据根
    raise FileNotFoundError(f'BOOK_DATA_DIR 不存在或不是目录: {DATA_DIR}')  # 失败即停止,不回退固定路径
path = DATA_DIR / 'stock' / 'stock_price_post_adjusted.h5'  # 构建后复权股价文件路径
stock_price_history = pd.read_hdf(path).reset_index()  # 读取后复权股价数据并重置MultiIndex
stock_price_history = stock_price_history[stock_price_history['order_book_id'] == '002415.XSHE'].copy()  # 筛选海康威视数据

stock_price_history = stock_price_history.sort_values('date')  # 按日期排序
列表 10.22: 习题6:滞后特征与时间三分
# 2. 构造特征(使用滞后收益率作为预测特征)
stock_price_history['Ret'] = stock_price_history['close'].pct_change()  # 计算日收益率
for lag in range(1, 6):  # 构造1-5阶滞后特征
    stock_price_history[f'Lag_{lag}'] = stock_price_history['Ret'].shift(lag)  # 第lag天前的收益率
    
stock_price_history['y'] = (stock_price_history['Ret'] > 0).astype(int)  # 构造二分类标签(涨=1,跌=0)
stock_price_history = stock_price_history.dropna().iloc[-2000:]  # 删除缺失值并取最近2000条记录

lag_features_matrix = stock_price_history[[c for c in stock_price_history.columns if c.startswith('Lag')]].values  # 提取滞后特征矩阵
next_day_direction = stock_price_history['y'].values  # 提取涨跌方向标签

train_end_index = int(len(stock_price_history) * 0.6)  # 最早60%用于候选模型拟合
validation_end_index = int(len(stock_price_history) * 0.8)  # 中间20%仅用于选择MLP超参数
stock_train_features = lag_features_matrix[:train_end_index]
stock_validation_features = lag_features_matrix[train_end_index:validation_end_index]
stock_test_features = lag_features_matrix[validation_end_index:]
stock_train_labels = next_day_direction[:train_end_index]
stock_validation_labels = next_day_direction[train_end_index:validation_end_index]
stock_test_labels = next_day_direction[validation_end_index:]

for split_name, split_slice in {
    'train': slice(0, train_end_index),
    'validation': slice(train_end_index, validation_end_index),
    'test': slice(validation_end_index, None),
}.items():
    split_dates = stock_price_history['date'].iloc[split_slice]
    print(f'{split_name}: {split_dates.min().date()} -> {split_dates.max().date()}, n={len(split_dates)}')
train: 2017-10-11 -> 2022-09-13, n=1200
validation: 2022-09-14 -> 2024-05-14, n=400
test: 2024-05-15 -> 2025-12-31, n=400
列表 10.23: 习题6:只用训练与验证期选择 MLP 结构
# 3. 构建Logistic回归管道(标准化 + 分类器)
logistic_regression_pipeline = Pipeline([
        ('scaler', StandardScaler()),  # 标准化预处理
        ('clf', LogisticRegression(max_iter=2000))  # Logistic回归分类器
])
列表 10.24: 习题6:验证期 MLP 结构网格
# 4. 仅用按时间排在训练集之后的验证集选择MLP复杂度。
# MLPClassifier的内置early_stopping会随机抽取验证样本,因此在时序任务中显式关闭。
candidate_hidden_layers = [(16,), (32, 16), (64, 32)]
validation_auc_by_architecture = {}
for hidden_layers in candidate_hidden_layers:
    candidate_pipeline = Pipeline([
        ('scaler', StandardScaler()),
        ('clf', MLPClassifier(
            hidden_layer_sizes=hidden_layers,
            alpha=1e-4,
            learning_rate_init=1e-3,
            max_iter=200,
            early_stopping=False,
            random_state=0,
        )),
    ])
    candidate_pipeline.fit(stock_train_features, stock_train_labels)
    validation_probability = candidate_pipeline.predict_proba(stock_validation_features)[:, 1]
    validation_auc_by_architecture[hidden_layers] = roc_auc_score(
        stock_validation_labels, validation_probability
    )

selected_hidden_layers = max(validation_auc_by_architecture, key=validation_auc_by_architecture.get)
print('validation AUC:', validation_auc_by_architecture)
print('selected hidden layers:', selected_hidden_layers)
/tmp/tmp.CfULtrnrMQ/peter-validation/lib/python3.10/site-packages/sklearn/neural_network/_multilayer_perceptron.py:781: ConvergenceWarning: Stochastic Optimizer: Maximum iterations (200) reached and the optimization hasn't converged yet.
  warnings.warn(
validation AUC: {(16,): 0.47945033727024244, (32, 16): 0.5399834499360565, (64, 32): 0.5278467363775421}
selected hidden layers: (32, 16)
/tmp/tmp.CfULtrnrMQ/peter-validation/lib/python3.10/site-packages/sklearn/neural_network/_multilayer_perceptron.py:781: ConvergenceWarning: Stochastic Optimizer: Maximum iterations (200) reached and the optimization hasn't converged yet.
  warnings.warn(
列表 10.25: 习题6:锁定结构后的基线、测试 AUC 与过拟合差距
# 5. 选择完成后,用训练+验证时段重拟合;测试集只在最后评估一次。
development_features = lag_features_matrix[:validation_end_index]
development_labels = next_day_direction[:validation_end_index]
neural_network_pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('clf', MLPClassifier(
        hidden_layer_sizes=selected_hidden_layers,
        alpha=1e-4,
        learning_rate_init=1e-3,
        max_iter=200,
        early_stopping=False,
        random_state=0,
    )),
])

for name, model in [('LR', logistic_regression_pipeline), ('MLP', neural_network_pipeline)]:
    model.fit(development_features, development_labels)
    development_probabilities = model.predict_proba(development_features)[:, 1]
    predicted_probabilities = model.predict_proba(stock_test_features)[:, 1]
    development_auc = roc_auc_score(development_labels, development_probabilities)
    test_auc = roc_auc_score(stock_test_labels, predicted_probabilities)
    print(f'{name} development AUC={development_auc:.4f}, test AUC={test_auc:.4f}, gap={development_auc - test_auc:.4f}')
LR development AUC=0.5250, test AUC=0.5029, gap=0.0221
MLP development AUC=0.7320, test AUC=0.4576, gap=0.2744
/tmp/tmp.CfULtrnrMQ/peter-validation/lib/python3.10/site-packages/sklearn/neural_network/_multilayer_perceptron.py:781: ConvergenceWarning: Stochastic Optimizer: Maximum iterations (200) reached and the optimization hasn't converged yet.
  warnings.warn(

题 6 的可评分证据是三段日期与 \(n\)、验证 AUC 网格、所选宽度/深度、LR 与 MLP 的开发期 AUC、测试 AUC 和差距。若任一集合只有一个类别,AUC 不可定义,应登记失败并改用事前扩大日期窗,不能改看测试集选窗。若 MLP 测试 AUC 未超过 LR,则结论是“没有增量证据”。

  1. 归一化与学习率策略的完整消融

本题的唯一允许依赖是 小节 10.13.2.1 产生的六个训练/验证/测试数组;评分器会在新进程先执行该 setup,再只执行本题代码。

下例保持 Dense(16)-Dense(1)、批量、epoch 和种子不变,形成“原尺度/训练期标准化 × 常数/余弦衰减”四格。选择与收敛比较只看验证期;测试期仍只作最终报告。

列表 10.26: 习题7:固定网络并切换尺度与学习率策略
import tensorflow as tf  # 使用同一深度学习框架完成四格消融
from sklearn.preprocessing import StandardScaler  # 只用训练期拟合标准化器
tf.keras.utils.set_random_seed(20260812)  # 固定初始化与批次随机性
training_scaler = StandardScaler().fit(stock_train_features)  # 严格只拟合最早训练段
scaled_train = training_scaler.transform(stock_train_features)  # 转换训练输入
scaled_validation = training_scaler.transform(stock_validation_features)  # 用冻结尺度转换验证输入
def build_ablation_model(schedule_name, step_count):  # 构造结构完全相同的候选网络
    learning_rate = 1e-3 if schedule_name == 'constant' else tf.keras.optimizers.schedules.CosineDecay(1e-3, step_count, alpha=0.1)  # 只切换学习率路径
    ablation_model = tf.keras.Sequential([tf.keras.layers.Input((stock_train_features.shape[1],)), tf.keras.layers.Dense(16, activation='relu'), tf.keras.layers.Dense(1, activation='sigmoid')])  # 固定宽度深度
    ablation_model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate), loss='binary_crossentropy', metrics=[tf.keras.metrics.AUC(name='auc')])  # 固定优化器与指标
    return ablation_model  # 返回待训练候选模型
列表 10.27: 习题7:四格消融的训练—验证选择证据
ablation_records = []  # 保存四个候选的可比证据
for should_scale in [False, True]:  # 切换是否使用训练期标准化
    candidate_train = scaled_train if should_scale else stock_train_features  # 选择训练输入尺度
    candidate_validation = scaled_validation if should_scale else stock_validation_features  # 保持验证尺度一致
    for schedule_name in ['constant', 'cosine']:  # 切换常数与余弦衰减
        steps = 30 * int(np.ceil(len(candidate_train) / 32))  # 按固定 epoch 计算余弦步数
        candidate_model = build_ablation_model(schedule_name, steps)  # 建立同结构候选网络
        history = candidate_model.fit(candidate_train, stock_train_labels, validation_data=(candidate_validation, stock_validation_labels), epochs=30, batch_size=32, verbose=0)  # 只用训练与验证
        best_validation_loss = min(history.history['val_loss'])  # 衡量最终可达验证损失
        convergence_epoch = next(i + 1 for i, value in enumerate(history.history['val_loss']) if value <= 1.05 * best_validation_loss)  # 定义进入最佳值 5% 的速度
        ablation_records.append({'standardized': should_scale, 'schedule': schedule_name, 'convergence_epoch': convergence_epoch, 'best_validation_loss': best_validation_loss})  # 只留存开发期字段
ablation_validation_table = pd.DataFrame(ablation_records).sort_values(['best_validation_loss', 'convergence_epoch', 'standardized', 'schedule'])  # 应用预注册确定性排序
selected_ablation = ablation_validation_table.iloc[0].to_dict()  # 仅按验证损失与固定平局规则冻结一个配置
print(ablation_validation_table)  # 输出四格验证证据而不接触测试集
   standardized  schedule  convergence_epoch  best_validation_loss
1         False    cosine                  1              0.692664
0         False  constant                  1              0.693122
3          True    cosine                  1              0.696931
2          True  constant                  1              0.701254

选择完成后,把唯一配置、模型结构、训练轮数和平局规则序列化为哈希。本实现选择执行允许的训练+验证重拟合,再在一个守卫代码块中访问测试集恰好一次。

列表 10.28: 习题7:冻结配置、可选开发期重拟合与唯一测试访问
import hashlib  # 为冻结配置建立内容寻址证据
import json  # 使用稳定键序列化配置
selected_ablation['refit_epochs'] = int(selected_ablation['convergence_epoch'])  # 在看测试前冻结重拟合轮数
selected_ablation['architecture'] = 'Dense(16)-Dense(1)'  # 登记保持不变的网络结构
selected_ablation['tie_break'] = 'loss,epoch,standardized,schedule'  # 登记确定性平局规则
selected_bytes = json.dumps(selected_ablation, sort_keys=True, separators=(',', ':')).encode('utf-8')  # 规范序列化唯一配置
selected_ablation_sha256 = hashlib.sha256(selected_bytes).hexdigest()  # 在测试访问前冻结哈希
development_features = np.vstack([stock_train_features, stock_validation_features])  # 可选地合并训练与验证输入重拟合
development_labels = np.concatenate([stock_train_labels, stock_validation_labels])  # 合并对应开发期标签
development_scaler = StandardScaler().fit(development_features) if selected_ablation['standardized'] else None  # 仅在所选配置需要时重拟合尺度
refit_features = development_scaler.transform(development_features) if development_scaler else development_features  # 按冻结配置转换开发期
refit_steps = selected_ablation['refit_epochs'] * int(np.ceil(len(refit_features) / 32))  # 从冻结轮数计算调度步数
refit_model = build_ablation_model(selected_ablation['schedule'], refit_steps)  # 只重建唯一入选模型
refit_model.fit(refit_features, development_labels, epochs=selected_ablation['refit_epochs'], batch_size=32, shuffle=False, verbose=0)  # 不再使用测试信息训练
test_access_count = 0  # 在唯一测试消费者外禁止访问测试标签
test_features_once = development_scaler.transform(stock_test_features) if development_scaler else stock_test_features  # 按冻结预处理转换一次测试输入
test_access_count += 1  # 在评价调用前登记唯一访问
selected_test_metrics = refit_model.evaluate(test_features_once, stock_test_labels, verbose=0, return_dict=True)  # 只评价入选配置一次
assert test_access_count == 1  # 访问计数不为一即结构化失败
print({'selected_config': selected_ablation, 'config_sha256': selected_ablation_sha256, 'test_access_count': test_access_count, 'test_metrics': selected_test_metrics})  # 输出冻结与唯一测试证据
{'selected_config': {'standardized': False, 'schedule': 'cosine', 'convergence_epoch': 1, 'best_validation_loss': 0.6926635503768921, 'refit_epochs': 1, 'architecture': 'Dense(16)-Dense(1)', 'tie_break': 'loss,epoch,standardized,schedule'}, 'config_sha256': '59e4ae8c376f410d7ebbfdd6e754e74570377a778362a85b331e54535d172bbe', 'test_access_count': 1, 'test_metrics': {'auc': 0.4906122386455536, 'loss': 0.6933269500732422}}

容差与失败模式:相同配置重复时指标可有随机波动,不设固定优胜者;必须固定预算并完整报告四格验证表。若损失非有限、任一集合单类、四格预算不同、配置哈希在测试后才生成或访问计数不为 1,该次消融无效。不能预写“标准化/余弦一定更好”,只能根据验证期选择与唯一锁定测试结果作条件式结论。

  1. 小样本表格数据的稳健策略:优先考虑更简单的模型(线性/树模型/MLP),严格时间切分,强正则化与早停,必要时做模型集成,并把重心放在特征质量与数据泄露控制。

10.13.3 理论题参考解答(练习 9 为核心梯度桥梁,练习 10 为拓展)

  1. 交叉熵对 logit 的梯度\(p=\sigma(z)=\frac{1}{1+e^{-z}}\),有

\[ \frac{\partial \ell}{\partial z}=\frac{\partial \ell}{\partial p}\cdot\frac{\partial p}{\partial z}= \Big(-\frac{y}{p}+\frac{1-y}{1-p}\Big)\cdot p(1-p)=p-y. \]

得到简洁形式\(\partial\ell/\partial z = p-y\),数值稳定且便于实现(也是很多框架将 sigmoid 与BCE 合并实现的原因)。

  1. 两层网络的链式结构:设

\[ h=\phi(W_1x+b_1),\quad z=W_2h+b_2,\quad \hat y=\psi(z), \]

损失为\(\ell(y,\hat y)\)。反向传播按链式法则:先算输出层误差项\(\delta_2=\partial\ell/\partial z\),再传回隐层 \(\delta_1=(W_2^\top\delta_2)\odot \phi'(W_1x+b_1)\),从而得到

\[ \nabla_{W_2}\ell=\delta_2 h^\top,\;\nabla_{b_2}\ell=\delta_2,\;\nabla_{W_1}\ell=\delta_1 x^\top,\;\nabla_{b_1}\ell=\delta_1. \]

这说明梯度从输出层逐层乘上权重转置与激活导数向前传播。

  1. 一个新初值下的一步完整更新与评分证据

前向传播先给出

\[ s=0.1(2)+0.3(-1)-0.2=-0.3,\qquad h=\sigma(s)=0.425557, \]

\[ z=-0.4(0.425557)+0.15=-0.020223,\qquad \widehat y=\sigma(z)=0.494944. \]

由于 \(y=0\),BCE 为

\[ L=-\log(1-\widehat y)=0.683087. \]

输出误差是 \(\delta_z=\widehat y-y=0.494944\),故四组梯度为

\[ \frac{\partial L}{\partial v}=\delta_zh=0.210627,\qquad \frac{\partial L}{\partial c}=\delta_z=0.494944, \]

\[ \delta_s=\delta_zvh(1-h)=-0.048397, \]

\[ \nabla_wL=\delta_sx=(-0.096795,0.048397)^\top,\qquad \frac{\partial L}{\partial b}=\delta_s=-0.048397. \]

代入 \(\theta^+=\theta-0.05\nabla_\theta L\)

\[ w^+=(0.104840,0.297580)^\top,\quad b^+=-0.197580, \]

\[ v^+=-0.410531,\quad c^+=0.125253,\quad \widehat y^+=0.487275. \]

完整计分证据为:前向四量 \(s,h,z,\widehat y\) 共 2 分;BCE 1 分;四组梯度各 1 分,共 4 分;四组更新各 1 分,共 4 分;方向检查 1 分。任一向量分量也按 \(10^{-3}\) 容差单独判断;只写公式不代入数值时,对应数值项不得分。这里 \(\widehat y^+<\widehat y\) 与负类标签方向一致。

10.14 章末闭环

逐项目标自检:不看正文解释激活函数与反向传播;为 MLP 写出训练—验证—测试时间线;识别正则化、学习率和早停各自控制的对象;比较线性基线与神经网络的同键样本外损失;说明 CNN 的平移等变性与 RNN/LSTM 的状态递推边界。时间切分、训练期预处理和一次性测试是 must-pass;三项全过且其余两项至少一项有证据,才进入下一章。

三项 must-pass 必须逐项取证,不能用一个“整体流程正确”印象替代。首次证据、失败码、补救、异形复测、评分键与重入点的一一映射如下;复测数据与首次 fixture 在表面情境和数值上不同,答案不得回填为首次证据。

表 10.4: Chapter 10 三项 must-pass 的一一补救与重入合同
must-pass 首次证据 失败码 补救锚点 表面不同的复测 评分键 通过后的重入
MP10-1 时间切分 表 10.3ch10_time_boundaries CH10-MP1-TIME 小节 10.9.2 给出六个月供应链延迟表,逐月另列预测日与次月标签日;写出 3/2/1 月三段并验证相邻段 max(label_date) < min(prediction_date) MP10-1,2 分全得才通过 小节 10.8
MP10-2 训练期预处理 表 10.3ch10_preprocessing_fit CH10-MP2-PREPROCESS 小节 10.3.3.2 给出四批客户流失特征及异常验证极值;只用首两批计算均值/尺度,再手算第三批首行的标准分 MP10-2,2 分全得才通过 小节 10.6
MP10-3 测试只评分一次 表 10.3 的学生 ch10_test_access_log 与教师隔离 harness CH10-MP3-TEST-ACCESS 小节 10.9 运行器私有签发 submission/attempt 并从冻结开发输入重算门禁;学生不得选择 ID 或提交门禁;教师交叉核验选择、预测、门禁、指标的持久值与哈希 MP10-3,通过时真实读数为 0 → 1;完成态和已读一停止态永不重开,仅零读取停止态可由教师签发链接旧审计的新 submission,才得 2 分 小节 10.10

只有 表 10.4 三行都通过,才检查目标 4—5 的其余证据并决定是否进入下一章;一行失败只补该行,不用其他行的高分抵消。

禁用情境:样本很小却没有强正则化或简单基线时,不应把深度网络作为默认方案;验证期参与结构选择后,不得再把同一验证成绩称为最终泛化证据。常见误区是把训练损失下降等同于增量预测能力,或把 CNN/RNN 的结构偏好当成对所有表格/时序任务的保证。

无提示检索:1)二元交叉熵对 logit 的梯度是什么?2)标准化器和早停判据分别只能在哪个集合拟合或监控?3)若 MLP 未胜过同切分逻辑回归,应如何写结论?这三题只检查概念检索,不替代 表 10.4 的三份流程证据。

展开检索反馈与学习决策

1)梯度为 \(\hat p-y\)。2)标准化器只拟合训练集;早停可监控冻结验证集,但不能查看测试集。3)写成“当前数据、特征、预算和切分下没有观察到相对线性基线的增量证据”,不能把失败改写为网络无效。第 1 题错:补修 小节 10.3.3.1,以 \(x=3,y=0\) 重算梯度,误差不超过 \(10^{-3}\);第 2 题错:补修 小节 10.13.2.1,为新的四段月序列标出 scaler/选择/测试边界,三处全对;第 3 题错:补修 小节 10.9,对“模型不胜季节朴素基线”写两句限定结论。三项复测均通过后才从 小节 10.10 重入;答案不得作为首次作答证据。

陌生迁移:为新的供应链延迟或客户流失序列,先冻结预测时点与基线,再决定 MLP、CNN 或 RNN 的输入结构,并写出一个会使复杂模型停止上线的失败条件。下一章把“事件是否发生”扩展为带删失的“何时发生”。

Cybenko, George. 1989年. 《Approximation by Superpositions of a Sigmoidal Function》. Mathematics of Control, Signals, and Systems 2 (4): 303~14. https://doi.org/10.1007/BF02551274.
Hochreiter, Sepp, 和 Jürgen Schmidhuber. 1997年. 《Long Short-Term Memory》. Neural Computation 9 (8): 1735~80. https://doi.org/10.1162/neco.1997.9.8.1735.
Rumelhart, David E., Geoffrey E. Hinton, 和 Ronald J. Williams. 1986年. 《Learning Representations by Back-Propagating Errors》. Nature 323: 533~36. https://doi.org/10.1038/323533a0.