简介一份关于卷积神经网络与Transformer结合应用于金融时序分析的深度学习研究报告面向金融量化研究员、AI算法工程师及深度学习爱好者系统解决了高维非线性金融数据难以建模的问题。内容梳理卷积神经网络基本架构与典型网络、Transformer自注意力机制与编码器-解码器结构并重点解析CTTS模型先用一维卷积提取日内股价局部特征再通过多层Transformer捕捉长距离依赖最终由MLP输出涨跌平概率同时给出高精度实验结果及三个量化研究方案。资源为1个PDF文档包体仅1.37MB内含完整章节、图表与参考文献方便快速定位模型原理与实验配置。已吸引956人学习对准备开展高频选股因子挖掘、股票择时策略验证或不同频率数据合成的读者具有直接参考价值。1. 基于 CNN-Transformer 的深度学习模型探究.pdf这份文档在解决什么问题拿到《基于 CNN-Transformer 的深度学习模型探究.pdf》这个标题先别急着当成一篇综述略读。它背后指向的是一个很具体的工程问题纯 CNN 在局部特征提取上很能打但感受野有限长程依赖建模要靠堆深度纯 Transformer 擅长全局建模但缺了 CNN 的归纳偏置小数据集上很容易欠拟合。把两者拼在一个结构里是近两年视觉、时序、遥感、交通流预测等任务里最常见的提精度手段。这篇 PDF 大概率就是在讲这种混合结构从理论到实现的全过程。适合谁读正在做图像分类、目标检测、时序预测但发现单一模型到了瓶颈、想换结构又怕踩坑的从业者。接下来的内容我会按这个方向把混合模型的原理、复现步骤和调参经验拆开讲你照着做能省下至少两周的试错时间。2. CNN 与 Transformer 的分工逻辑为什么拼在一起而不是二选一2.1 混合结构的核心动机局部归纳偏置加全局上下文CNN 的卷积核天生带着两个硬约束局部性和平移等变性。一个 3×3 的卷积核只看周围 8 个像素这让它在边缘、纹理这类局部模式上非常高效而且参数量小、收敛快。代价是感受野的增长是线性的想看到图像里距离很远的两个区域之间的关系得堆很多层。Transformer 的自注意力机制恰恰相反。它在计算每个位置的时候理论上可以 attention 到序列里所有其他位置一步到位建全局依赖。但这也是它的短板它没有 CNN 那种“相邻像素应该相关”的先验所有关系都要从数据里学。ViT 在 ImageNet 上能刷到很高的分数前提是几亿张图片的预训练你拿 ViT 在几千张的小数据集上从头训效果往往不如一个 ResNet-18。混合模型的设计动机就是把两者的优势叠起来。常见做法分三类串行结构先用 CNN 提特征再把特征图送入 Transformer、并行结构CNN 和 Transformer 各走一支最后融合、嵌套结构Transformer 的 token 本身就用卷积来生成。Swin Transformer 其实就是串行结构的一个变体——它用 patch 划分加卷积 embedding 来生成 token再用窗口注意力做全局建模这也是 Transformer 在视觉任务上真正站稳脚跟的关键设计。2.2 token 化与位置编码最影响模型上限的两个细节很多第一次写 CNN-Transformer 混合模型的人会把精力全放在堆模块上但真正决定模型表现的是两个不起眼的部件token 怎么来位置信息怎么加。token 化的方式直接决定了 Transformer 部分看到的序列长度。拿一张 224×224 的输入图来说如果按 16×16 的 patch 切得到 14×14196 个 token序列长度就是 196。如果用卷积 stride4 做下采样得到 56×563136 个 token计算量直接翻十几倍。我一般会先用卷积做两次 stride2 的下采样把特征图缩到 56×56 或 28×28再拉平送入 Transformer。这样既保留了 CNN 提取局部特征的能力又不会让序列长到把显存吃爆。位置编码的选择则和输入尺寸的灵活性直接相关。可学习位置编码简单粗暴但训练时用 224×224部署时换到 384×384位置编码就得做插值效果会掉。Swin 的相对位置偏置就没有这个问题这也是它比 ViT 更容易在多种分辨率下迁移的原因之一。import torch import torch.nn as nn class ConvTokenizer(nn.Module): 用卷积做 token 化先下采样提特征再展平为序列 def __init__(self, in_channels3, embed_dim192): super().__init__() self.conv1 nn.Conv2d(in_channels, 64, kernel_size3, stride2, padding1) self.bn1 nn.BatchNorm2d(64) self.conv2 nn.Conv2d(64, embed_dim, kernel_size3, stride2, padding1) self.bn2 nn.BatchNorm2d(embed_dim) self.relu nn.ReLU(inplaceTrue) self.pool nn.AdaptiveAvgPool2d((7, 7)) # 强制缩到固定尺寸 def forward(self, x): x self.relu(self.bn1(self.conv1(x))) x self.relu(self.bn2(self.conv2(x))) x self.pool(x) x x.flatten(2).transpose(1, 2) # (B, C, H, W) - (B, 49, embed_dim) return x这里用两层 stride2 的卷积加上 AdaptiveAvgPool把任意尺寸的输入统一映射到 49 个 token每个 token 的维度是 embed_dim。AdaptiveAvgPool 是这里的关键它让模型对输入尺寸不敏感省去了位置编码插值的麻烦。如果不用 pool就得在 forward 里动态计算位置编码或者干脆用相对位置编码。token 数量 49 是个比较保守的取值计算量小适合验证结构能不能跑通真做实验时我会把这个 pool 去掉改用固定 stride 的卷积链让 token 数量随输入分辨率变化再用 Swin 风格的位置编码兜底。2.3 主干怎么选ResNet 还是 Stem 还是纯卷积堆确定混合结构之后下一个问题是 CNN 部分用现成主干还是自己搭。工程上我建议先别急着上 ResNet-50 这种大主干原因有两个一是大主干的特征图通道数高2048接入 Transformer 之前要先做降维多一层投影就多一层调参空间二是大主干本身的效果已经足够好Transformer 加成不明显反而让人搞不清精度提升到底来自哪部分。更可控的做法是自建一个轻量 Stem三层卷积、每层后面跟 BN 和 ReLU通道数从 64 逐步加到 192 或 256。这个 Stem 的输出直接作为 Transformer 的输入序列。这样整个模型的参数量大概在 10M30M 之间既能在单卡上快速迭代又能清楚看到每一部分的贡献。class SimpleStem(nn.Module): 轻量卷积主干输出可直接送进 Transformer Encoder def __init__(self, in_channels3, out_channels192): super().__init__() self.layers nn.Sequential( nn.Conv2d(in_channels, 64, kernel_size3, stride2, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.Conv2d(64, 128, kernel_size3, stride2, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.Conv2d(128, out_channels, kernel_size3, stride1, padding1), nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue), ) def forward(self, x): x self.layers(x) # (B, C, H/4, W/4) B, C, H, W x.shape x x.flatten(2).transpose(1, 2) # (B, H*W, C) return x这组卷积把输入从 224×224 下采样到 56×56展平后得到 3136 个 token。这个序列长度对标准 Transformer 来说已经偏长了所以后面接的 Encoder 层数不宜太深6 层足够每层的 head 数设 8embed_dim 保持 192。这样算下来的 attention 矩阵是 3136×3136单卡能扛住但如果你想跑更深的 Encoder就得把 stride 调成 4 或者引入窗口注意力否则显存会很紧张。3. 复现模型的最小配置从环境到第一次训练跑通3.1 环境与依赖PyTorch 版本和 CUDA 的搭配经验复现这类模型环境问题往往是第一道坎。我的建议是直接用 PyTorch 2.x 的稳定版搭配配套的 CUDA 版本不要贪新。PyTorch 2.x 自带的 torch.compile 对 Transformer 的加速效果很明显编译后训练速度能提升 20%40%这在你反复调参的时候能省下大量时间。配套的 torchvision 用来加载预训练权重和标准数据集但混合模型的 CNN 部分我通常不用 torchvision 里的现成主干原因在前面说过方便控制通道数。CUDA 版本方面11.8 或 12.1 都可以关键看你的显卡驱动支持哪个。一个容易翻车的点装了新 CUDA 后忘记检查nvcc -V和torch.cuda.is_available()是否对应结果 PyTorch 静默用 CPU 跑训练慢得离谱还不报错。组件版本选择作用备注PyTorch2.x 稳定版模型搭建与训练配 torch.compile 提速CUDA11.8 或 12.1GPU 加速以驱动版本为准torchvision与 PyTorch 匹配数据集加载与增广不要混版本显存≥ 8GB训练混合模型序列长度大时 8GB 是底线3.2 跑通第一个训练循环以 CIFAR-10 为例的最小实现我习惯先用 CIFAR-10 这类小数据集验证结构能不能收敛再迁移到自己的业务数据上。CIFAR-10 分辨率只有 32×32不需要大 stride 的 Stem而是要把 patch 切得更细否则 token 太少Transformer 学不到东西。import torch import torch.nn as nn import torch.optim as optim import torchvision import torchvision.transforms as transforms # 数据加载与增广 transform_train transforms.Compose([ transforms.RandomCrop(32, padding4), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)), ]) trainset torchvision.datasets.CIFAR10(root./data, trainTrue, downloadTrue, transformtransform_train) trainloader torch.utils.data.DataLoader(trainset, batch_size64, shuffleTrue, num_workers4) # 模型轻量 CNN Stem Transformer Encoder 分类头 class CNNTransformer(nn.Module): def __init__(self, num_classes10): super().__init__() self.stem nn.Sequential( nn.Conv2d(3, 64, kernel_size3, stride1, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.Conv2d(64, 128, kernel_size3, stride2, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), ) # 16x16 特征图展平后 256 个 token self.embed nn.Linear(128, 192) encoder_layer nn.TransformerEncoderLayer(d_model192, nhead8, dim_feedforward768, batch_firstTrue) self.encoder nn.TransformerEncoder(encoder_layer, num_layers4) self.cls_token nn.Parameter(torch.randn(1, 1, 192)) self.head nn.Linear(192, num_classes) def forward(self, x): x self.stem(x) # (B, 128, 16, 16) B x.shape[0] x x.flatten(2).transpose(1, 2) # (B, 256, 128) x self.embed(x) # (B, 256, 192) cls self.cls_token.expand(B, -1, -1) # 在序列开头拼一个分类 token x torch.cat([cls, x], dim1) x self.encoder(x) # (B, 257, 192) out self.head(x[:, 0]) # 取分类 token 的输出 return out model CNNTransformer().cuda() criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.parameters(), lr1e-3, weight_decay5e-4) for epoch in range(30): model.train() running_loss 0.0 for images, labels in trainloader: images, labels images.cuda(), labels.cuda() optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) # 梯度裁剪防震荡 optimizer.step() running_loss loss.item() print(fEpoch {epoch1}/30, Loss: {running_loss/len(trainloader):.4f})这里的代码有三个地方值得展开说。第一个是 cls_token 的用法这是 BERT 和 ViT 传下来的惯例——在序列开头额外拼一个可学习的 token最后用它的输出来做分类相当于让模型自己决定从全局信息里抽取哪些特征来分类。第二个是batch_firstTruePyTorch 的 Transformer Encoder 层默认序列维度在第一维不设这个参数的话输入形状得是 (seq_len, batch, dim)很容易踩形状不对的坑。第三个是梯度裁剪clip_grad_norm_值为 1.0Transformer 的训练比 CNN 脆梯度范数容易暴涨裁一刀能避免大部分发散问题。用这套配置在 CIFAR-10 上训练 30 轮准确率大概能到 80%85% 之间。作为对比同样规模的 ResNet-18 大概能到 92% 左右。别急着下结论说混合模型不行——CIFAR-10 是 32×32 的小图CNN 的归纳偏置在这种低分辨率场景下优势太强Transformer 的全局建模能力发挥不出来。混合模型真正的主场是中高分辨率图像和时序数据这点放到第 4 章的参数调整里细说。3.3 验证结构是否收敛用 loss 曲线判断而不是等训练完第一次跑通之后不要急着调参先看 loss 曲线的形状。正常的收敛曲线是前 5 轮 loss 快速下降之后逐步变缓。如果 loss 在前几轮不降反升先检查学习率和数据预处理别怀疑模型结构——结构问题一般表现为 loss 卡在某个平台不动而不是发散。我习惯在训练脚本里同时记录 train_loss 和 val_acc每 5 轮打印一次。如果 train_loss 在降但 val_acc 不动说明过拟合开始了这时候需要加正则化或者增强如果两者都在 20 轮后还有明显波动先怀疑学习率。这套判断逻辑适用于任何 CNN-Transformer 混合结构比看最终准确率更能定位问题出在哪个环节。4. 三个必调参数与数据预处理细节从“能跑”到“能打”4.1 patch 大小与序列长度先定 token 数量再定模型容量混合模型里Transformer 部分的计算量和 token 数量是平方关系。token 数翻一倍attention 矩阵的计算量翻四倍。所以 patch 大小不是随心设的它直接决定了你能用多深的 Encoder。以 224×224 输入为例patch16 时序列长度是 196patch8 时是 784patch4 时是 3136。做分类任务时我一般让序列长度落在 1961024 之间。序列太短比如小于 49Transformer 学不到有意义的全局关系不如去掉直接上纯 CNN序列太长显存和训练时间压力大而且需要更多数据才能收敛。时序数据同理——如果你做的是 Transformer 预测正弦数据这类任务输入序列长度通常就是 sliding window 的大小比如 128 个时间步这个长度对 Transformer 来说是舒适的区间。# patch size 设置示例动态计算序列长度方便切换 patch_size 16 img_size 224 num_tokens (img_size // patch_size) ** 2 print(fpatch{patch_size}, 序列长度{num_tokens}) # 输出 196一个小技巧把 patch size 和 embedding 维度联起来调。patch 从 16 减到 8token 数从 196 涨到 784这时可以把 embed_dim 从 192 降到 128总计算量基本不变但模型对局部细节的感知能力会提升。反过来如果你的任务偏全局语义比如场景分类、遥感影像分割调大 patch 到 32同时把 embed_dim 提到 384效果往往更好。4.2 学习率与 warmup 策略混合模型的命门Transformer 对学习率极其敏感这是和纯 CNN 最大的区别。CNN 用 0.1 的 SGD 也能训Transformer 用 1e-3 的 AdamW 都可能直接发散。具体原因跟 LayerNorm 和 Attention 的梯度分布有关这里不展开理论直接给结论AdamW 学习率 1e-43e-4 linear warmup 是稳定的起点。warmup 的作用是让模型在前几个 step 用很小的学习率“热身”。Transformer 刚初始化时各层的梯度尺度差异很大一上来就用大学习率容易把参数推到一个坏区域。我一般设 warmup 步数为总步数的 5%10%之后接 cosine 衰减把学习率平滑降到接近 0。# 学习率调度warmup cosineTransformer 标准配置 from torch.optim.lr_scheduler import LambdaLR import math total_steps 5000 warmup_steps int(total_steps * 0.05) # 250 步 def lr_lambda(step): if step warmup_steps: return step / max(1.0, warmup_steps) progress (step - warmup_steps) / max(1, total_steps - warmup_steps) return 0.5 * (1.0 math.cos(math.pi * progress)) scheduler LambdaLR(optimizer, lr_lambdalr_lambda)这里有个容易忽略的细节混合模型的 CNN 部分和 Transformer 部分可以设置不同的学习率。CNN 的归纳偏置让它收敛得更快用大一点的学习率没问题Transformer 部分用稍小的学习率更稳。实操上我会给 Transformer 的层设lr3e-4CNN 的层设lr1e-3在 AdamW 的param_groups里分别指定。这么做的好处是收敛速度快很多而且不容易出现 CNN 已经过拟合、Transformer 还没学到位的情况。4.3 数据预处理与增强小数据集上的保命手段CNN-Transformer 混合模型对数据量的需求介于纯 CNN 和纯 Transformer 之间。它比纯 CNN 需要更多数据来学习全局关系但又没有纯 Transformer 那么依赖预训练。在几千张图的小数据集上数据增强不是可选项是保命项。增强策略上除了常规的随机裁剪、翻转和颜色抖动MixUp 和 CutMix 对混合模型的收益比纯 CNN 更明显。原因在于它们强迫模型关注全局上下文——MixUp 把两张图的像素按比例混合模型必须从整体结构里提取有效信息正好弥补 Transformer 在小数据上容易 “走捷径” 的问题。# CutMix 简单实现只做混合不做标签平滑适合小数据集 def cutmix_data(x, y, alpha1.0): batch_size x.size(0) lam torch.distributions.Beta(alpha, alpha).sample().item() index torch.randperm(batch_size).cuda() y_b y[index] bx1, by1 torch.randint(0, x.size(2), (2,)).tolist() bx2, by2 torch.randint(0, x.size(3), (2,)).tolist() x[:, :, bx1:bx2, by1:by2] x[index][:, :, bx1:bx2, by1:by2] return x, y, y_b, lam时序数据的预处理思路不同但同样关键。做 Transformer 预测正弦数据这类任务时数据归一化方式直接决定模型能不能收敛。正弦数据本身有界归一化到 [-1, 1] 或 [0, 1] 都行但如果是股票、流量这类无界数据必须做差分或者 StandardScaler并且要小心未来信息的泄漏——归一化的参数只能用训练集的统计量不能混入验证集和测试集。5. 避坑笔记CNN-Transformer 训练中的五个翻车现场5.1 验证集准确率震荡不收敛loss 忽高忽低现象训练前 10 轮 loss 一直在 1.5 到 2.5 之间来回跳动验证集准确率也跟过山车一样。原因学习率偏大加上 warmup 步数不足。Transformer 的自注意力层对参数扰动非常敏感lr 稍微大一点梯度就会互相打架出现 loss 震荡。解决先把学习率降到 1e-4warmup 步数加到总步数的 10%。如果还在震荡检查是否用对了 AdamW 而不是 Adam——AdamW 的权重衰减方式和 Adam 不一样混合模型里 weight decay 设 5e-4 是安全值太大反而会让模型欠拟合。这个坑我至少踩过三次每次都是先怀疑模型结构最后发现是 lr 的问题。5.2 显存 OOM小 batch 都跑不动现象batch size 设 32输入 224×224 的图forward 到 Transformer Encoder 时报 CUDA out of memory。原因token 数太多。224×224 的图如果用 stride2 的下采样特征图是 112×112展平后有 12544 个 tokenattention 矩阵是 12544×12544单张 24GB 显存都难扛。解决调整下采样倍率或引入窗口注意力。我通常先把下采样改到 stride4 或 stride8让 token 数降到 784 左右如果这样还 OOM就把 Encoder 层数从 6 减到 4或者把 nhead 从 8 减到 4。千万别为了省显存把 batch size 降到 2BatchNorm 在小 batch 下统计量不稳定整个损失曲线都会变得很奇怪。5.3 CNN 部分已经过拟合Transformer 部分还在欠拟合现象训练集准确率到了 95%验证集只有 70%差距还在持续拉大。原因混合模型两部分的收敛速度不一致。CNN 参数少、归纳偏置强学得快Transformer 参数多、需要更多数据来拟合全局关系学得慢。用同一个学习率训练CNN 学过头了Transformer 还没跟上。解决分开设置学习率。给 CNN 主干设置lr1e-3Transformer Encoder 设置lr3e-4每类参数单独一个 param group。另外可以对 Transformer 部分加一点 Dropout0.10.2对 CNN 部分保持默认——它没那么容易过拟合。这么调之后验证集准确率通常能涨 35 个百分点。5.4 换分辨率后模型效果暴跌现象训练时用 224×224推理时换到 384×384 输入准确率掉了一大截。原因可学习位置编码是按 224×224 训练的换分辨率后位置编码需要插值到新的长度插值带来的信息失真直接污染了 attention 计算。这是 ViT 系列模型的通病。解决方案有两个。一是在训练时就做多尺度训练random resize让模型适应不同分辨率二是改用相对位置编码Swin Transformer 的 bias 方式对这种场景免疫。如果你用的是自己的混合模型优先做多尺度训练改动最小效果立竿见影。5.5 训练 loss 正常下降但推理时 GPU 显存比训练还高现象训练阶段显存占用正常一跑推理就 OOM逻辑上说不通。原因推理时没有关梯度计算模型仍然保留了中间激活值。新手特别容易踩这个坑——写推理脚本时直接model(input)忘了包在torch.no_grad()里。解决推理代码统一写with torch.no_grad(): outputs model(input)并且把 model 切到 eval 模式。eval 模式会关闭 Dropout 和 BatchNorm 的 batch 统计量这两者在推理时都会额外占用内存或引入不一致。这个不是混合模型特有的踩坑但在混合模型上更容易暴露——Transformer Encoder 的中间激活比 CNN 占内存多得多出错时后果更严重。6. 把模型从“能跑”调到“好用”验证方法与两个实用技巧先说一个我自己的教训复现混合模型最忌讳改完结构就跑跑完看准确率准确率不行就再改结构。这种做法会让你陷入玄学调参的黑匣子永远不知道瓶颈在哪里。我现在固定用两板斧来验证模型有没有真正学到该学的东西。第一板斧是看 attention map。取验证集里的一两张图把 Transformer Encoder 最后一层的 attention 权重可视化出来。如果模型学得好你会看到分类 token 对图像中语义相关的区域分配了高权重如果 attention 是均匀分布或者完全随机说明模型根本没建立起全局依赖问题多半出在 token 化或位置编码上。可视化代码很少十几行就能写出来但它的诊断价值远超一切 loss 曲线。第二板斧是梯度范数监控。每个 step 计算一次整体梯度的 L2 范数打印出来。梯度范数长期保持在固定区间说明训练稳定如果某个 epoch 突然暴涨往往是学习率调度的 bug 或者数据里有异常的样本。进阶技巧方面我用的最多的是知识蒸馏。当你发现训练好的混合模型太大、部署太慢别急着换小模型。把大模型当 teacher用它的输出 soft label 去训练一个小模型通常可以让小模型达到大模型 95% 以上的效果。类别数越多的任务收益越大因为 soft label 里包含了类别之间的相似度信息这是 hard label 给不了的。这会带来第二个技巧小数据集场景下干脆不用大预训练模型先用混合模型在自己的小数据上练一个 teacher这个 teacher 再蒸馏出一个更小的 student 用于线上部署。比直接拿 ImageNet 预训练权重蒸馏更贴合你的数据分布。这个方向值不值得投入我的判断是值得。CNN-Transformer 混合结构的适用范围很广从图像分类、目标检测到时序预测、语音识别都能用上算力需求又没有纯大模型那么高一张 8GB 显存的卡能跑通整个实验闭环。关键是按对顺序来先在小数据集上验证收敛再逐步放大数据和模型最后用可视化和梯度诊断确认每一部分都在干活。我每次做新任务时都提醒自己先怀疑参数再怀疑数据最后才怀疑结构。希望帮到你。本文还有配套的精品资源点击获取