拓冰建站拓冰建站
首页 / 资讯中心 / 正文

LSTM+Transformer双模型融合在金融时序风控中的PyTorch实现

简介面向金融风控与深度学习交叉领域开发者这份63页PDF系统讲解如何用PyTorch搭建LSTMTransformer双模型融合的智能风险预警系统。内容从金融风控概述信用风险、市场风险、操作风险与LSTM、Transformer原理对比切入逐一拆解双模型早期融合、晚期融合、混合融合等策略并给出特征级与决策级融合实现思路。后续结合PyTorch环境搭建、数据清洗与特征工程归一化、标准化、PCA、自编码器等环节完整覆盖从数据处理到LSTM模型、Transformer模型实现再到双模型融合落地的项目链路。资源包仅含1个PDF文件压缩包大小2.45MB文档支持目录章节跳转与左侧大纲快速定位阅读体验清晰适合需要掌握时序建模与注意力机制融合方法的金融风控从业者、算法工程师及高年级研究生参考。目前已有202人学习可供按章节快速查阅实践。 做金融风控的同学应该都遇到过这个场景线上还款记录、交易流水、设备指纹、征信查询这类行为序列数据一抓一大把但真正要用模型去预测“这个人未来30天会不会逾期”“这笔交易是不是团伙欺诈”的时候往往发现单靠LR、XGBoost这类模型已经很难再往上提效果了。最近我在做一套智能风险预警系统的原型核心就是标题里写的“LSTMTransformer双模型融合”用PyTorch落地整体跑下来效果比我之前单独用LSTM或者单独用Transformer都要好。这篇就把我的设计方案、踩坑经历和完整实现思路写出来给正打算在时序风控方向上做模型迭代的朋友一个参考。这套方案适合谁如果你手里有一段一段的行为序列数据想预测风险事件发生的概率又不满足于把序列“打平”后喂给树模型那这篇文章应该对你有用。它不要求你已经特别深入地掌握Transformer内部细节但如果你会用PyTorch搭基础的神经网络读起来会比较轻松。我会从模型选型思路讲到数据构造再到具体代码实现、训练调参、部署预警逻辑最后附上我在实操中遇到的坑和解决办法。1. 为什么非要LSTM和Transformer“双修”1.1 金融序列数据的两个矛盾特征金融风控里的行为序列和NLP里的句子、语音里的时间帧有不少相似之处但又有很不一样的地方。相似之处在于它们都是有序的前后依赖关系很重要不一样的地方在于金融序列往往同时表现出两个“别扭”的性质第一个性质是局部依赖强。比如用户在一个APP上的操作轨迹往往“点完A立刻点B”这种短窗口内的顺序信号非常关键账户短时间内频繁换设备、换IP、换收货地址这种连续几个事件的组合比单点特征更有判别力。LSTM这类循环结构天然擅长捕捉短中期的顺序模式它按时间步逐步消化输入每个时间步的隐状态都携带了“到目前为止发生了什么”的信息。第二个性质是长程依赖极其稀疏但一旦出现往往就是强信号。比如某个人半年内每隔几个月就有一笔商户集中、金额递增的还款记录这种跨越大时间尺度的规律靠LSTM并不是不能学而是很难稳定地学出来因为信息在沿着时间步传递的过程中会衰减梯度也容易在长序列上不稳定。Transformer的Self-Attention机制设计出来就是为了解决这种“远距离直接通信”的问题任意两个位置之间都有一条直连路径位置距离不再是信息传递的阻碍。单独拿任何一个模型出来都会有一点偏科。LSTM对短周期模式敏感、对超长序列乏力Transformer能抓住长依赖但对短窗口内细微的顺序变化不敏感而且在小数据集上容易过拟合。金融风控场景恰恰是两者混合的既要看最近一小段时间的异常行为又要看长周期规律。这就是我决定做双模型融合的根本原因。1.2 融合思路的核心逻辑双模型融合不是简单地把两个模型的预测概率相加求平均那样只做了结果层面的集成模型内部学到的特征表示没有交叉。我的想法是让LSTM和Transformer各自从不同角度提取特征向量然后在特征层把两个表示拼起来交给一个全连接网络做最终的风险评分。这样融合的本质是“不同归纳偏置的互补”——LSTM提供强顺序归纳偏置Transformer提供长距离特征交互的全局视角最后学出来的风险表示既知道“最近连续发生了哪些事”也知道“这个用户半年来的行为轨迹有没有周期性的异常”。这个思路和很多推荐系统、多模态模型里常用的late fusion做法是类似的。实际落地时有一个关键细节两个分支的输入虽然来自同一条行为序列但不一定要完全一致。LSTM分支可以吃短窗口的密集行为序列Transformer分支可以吃长窗口的稀疏摘要序列两个分支的序列长度可以不一样只要各自的embedding空间能对齐到同一个维度最后拼接起来就是合理的。这个小技巧在很多论文里不会写但在工程上经常能解决“单条序列长度混乱”的问题。我在后面数据构造部分会详细讲。2. 数据准备与特征工程2.1 行为序列应该怎么切金融风控序列数据的第一步工作是定义“一条样本”。我用的方案是固定长度滑动窗口对每个用户按时间排序所有行为事件设定一个回溯窗口比如过去180天窗口内的事件按顺序排列成一条序列。预测目标通常是一个二分类标签未来30天内是否发生逾期/欺诈/流失等目标事件。这里有一个值得一提的设计窗口长度要和业务周期匹配。如果做的是消费信贷的逾期风险180天窗口比较常见因为很多风险信号如收入波动、多头借贷的暴露周期在半年内如果做反欺诈窗口可以缩短到7-30天因为欺诈行为往往异常密集且快速完成。我的做法是同时准备两个窗口一个LSTM分支用的64步短窗口一个Transformer分支用的128步长窗口。短窗口保留最近的行为细节长窗口容纳足够的历史信息两个分支各自负责一段尺度。2.2 特征列表怎么搭具体到每一个时间步上的特征我把它分成三类第一类是事件数值型特征比如交易金额、设备风险分、IP风险分、操作间隔、地理位置位移距离等。这类特征需要做标准化金融数据的分布普遍长尾建议先做log1p变换再标准化不然模型很容易被个别极端大额交易带偏。第二类是事件类别型特征比如事件类型登录、转账、还款、修改密码、渠道类型、设备类型。这些特征需要embedding化序列中每个时间步其实就是“一堆数值特征一堆embedding特征”的拼接结果构成这一步的输入向量。第三类是序列全局统计特征比如窗口内总次数、总金额、异常评分均值。这类特征不参与序列内部的时序建模在融合之后作为额外特征拼到全连接层能给模型补充一些“序列本身无法很好表达”的宏观信息。2.3 归一化、缺失值与样本构造的细节金融特征工程里最容易被忽视的是缺失值。行为序列天然是稀疏的很多用户在某一天根本没有事件这时候有两种处理方式一是把“无事件”补成全零向量但会让模型把“缺失”误学成“正常”二是显式加一个“mask事件”类型的embedding让模型知道这个时间步本来就没有数据。我推荐第二种因为模型能学到“缺失”本身也是一种信息。样本不均衡在风控里是常态好用户和坏用户的比例经常是20:1甚至更夸张。除了常规的降低负样本权重、Focal Loss之外我强烈建议做“样本分组”同一个用户的多个窗口样本不允许同时出现在训练集和验证集中否则模型“记住”用户比“学会”规律更容易导致验证集指标虚高。这个小问题我见过不少团队踩进去看起来AUC高得离谱上线一测就露馅。3. PyTorch模型架构实现3.1 模型总体结构整体结构可以分为四个模块LSTM分支、Transformer分支、注意力融合层、风控输出层。两个分支分别读取各自的输入序列得到两个固定维度的特征向量拼接后送入输出层得到风险概率。代码结构上我建议拆成四个类保持清晰。3.2 LSTM分支实现LSTM分支的输入形状是 (batch_size, seq_len, feature_dim)这里的 seq_len 对应短窗口长度。我的实现里用了双层LSTM取最后一层的最后一个隐状态作为序列表示代码如下import torch import torch.nn as nn class LSTMBranch(nn.Module): LSTM分支提取短窗口行为序列的局部时序特征。 输入: (batch, seq_len, input_dim) 输出: (batch, hidden_dim) def __init__(self, input_dim, hidden_dim128, num_layers2, dropout0.3): super().__init__() self.lstm nn.LSTM( input_sizeinput_dim, hidden_sizehidden_dim, num_layersnum_layers, batch_firstTrue, dropoutdropout ) self.layer_norm nn.LayerNorm(hidden_dim) def forward(self, x): # x: (batch, seq_len, input_dim) out, (hn, cn) self.lstm(x) # 使用最后一层的隐状态作为整条序列的表示 last_hidden hn[-1] # (batch, hidden_dim) return self.layer_norm(last_hidden)这里有个细节为什么取最后一层的最后一个隐状态而不把全部时间步的隐状态都保留下来因为我们的目标是对整条序列做风险判别不是做序列标注最后一步的隐状态已经编码了整个输入序列的信息取它作为输出最自然。当然如果你想用attention pooling来提取LSTM的输出效果可能会更好但在金融风控这种样本量不算特别大的场景下简单的取最后一步反而更稳。3.3 Transformer分支实现Transformer分支的输入是 (batch_size, 128, feature_dim)128是长窗口的长度。金融领域没有NLP里那种预训练位置编码可以直接迁移我选择了可学习的位置编码让模型自己学着去表示“距离”的概念。序列进入encoder之前先过一个线性层将原始特征维度统一映射到d_model维避免特征维度过高导致参数量膨胀。class TransformerBranch(nn.Module): Transformer分支提取长窗口行为序列的全局依赖特征。 输入: (batch, seq_len, input_dim) 输出: (batch, d_model) def __init__(self, input_dim, d_model128, nhead8, num_layers3, max_seq_len256, dropout0.3): super().__init__() self.input_proj nn.Linear(input_dim, d_model) self.pos_embed nn.Parameter( torch.randn(1, max_seq_len, d_model) * 0.02 ) encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforwardd_model * 4, dropoutdropout, activationgelu, batch_firstTrue ) self.encoder nn.TransformerEncoder( encoder_layer, num_layersnum_layers ) self.norm nn.LayerNorm(d_model) def forward(self, x): # x: (batch, seq_len, input_dim) seq_len x.size(1) x self.input_proj(x) # 映射到 d_model x x self.pos_embed[:, :seq_len, :] # 加位置编码 x self.encoder(x) # (batch, seq_len, d_model) # 对序列维度做均值池化得到整条序列的表示 x x.mean(dim1) return self.norm(x)Transformer分支里我用了均值池化而不是取CLS token原因是CLS token需要额外训练一个分类头位置而均值池化在序列长度变化时更加鲁棒。金融序列的“语义重心”往往分散在多个位置均值池化天然地把所有位置的信息做了一次平均适合这种分布不固定的场景。3.4 融合层与输出层实现两个分支的特征拼起来之后融合层是最能体现“套路”的地方。我这里没有直接拼接后立即分类而是加了一个门控机制让模型自己学习两个分支各应该被信任多少。具体思路是把两个特征拼接后用一个单神经元sigmoid输出一个0到1之间的门控值把门控值作为权重去加权两个分支的特征。这样做比简单拼接多了一点自适应能力。代码如下class RiskHead(nn.Module): 融合层 输出层。 输入: lstm_feat(batch,hidden_dim), tf_feat(batch,d_model) 输出: 风险概率 logits, 以及用于业务解释的融合特征 def __init__(self, lstm_dim, tf_dim, hidden_dim64, extra_dim0): super().__init__() in_dim lstm_dim tf_dim extra_dim self.gate nn.Sequential( nn.Linear(lstm_dim tf_dim, 16), nn.ReLU(), nn.Linear(16, 1), nn.Sigmoid() ) self.classifier nn.Sequential( nn.Linear(in_dim, hidden_dim), nn.ReLU(), nn.Dropout(0.3), nn.Linear(hidden_dim, 2) ) def forward(self, lstm_feat, tf_feat, extra_featNone): concat_feat torch.cat([lstm_feat, tf_feat], dim-1) gate_value self.gate(concat_feat) # (batch, 1) # 用门控值加权融合两个分支 l_w gate_value t_w 1.0 - gate_value fused torch.cat([l_w * lstm_feat, t_w * tf_feat], dim-1) if extra_feat is not None: fused torch.cat([fused, extra_feat], dim-1) logits self.classifier(context_fused) return logits, gate_value这一段是我觉得最值得琢磨的地方。为什么加个门控比直接拼接好因为LSTM和Transformer在捕捉信息时各有所长但同一个样本上到底谁更“可信”并不固定。比如一个用户最近几天的行为异常密集LSTM分支提供的信息大概率更关键但如果异常行为分散在几个月里Transformer分支的长窗口信息会更重要。门控机制让模型根据输入样本动态决定偏向哪个分支相当于给融合过程增加了一个“注意力”。注意如果你刚起步不建议一上来就上复杂融合。先用最简单的concat特征拼接接全连接层跑通基线确认双模型确实比单模型有效之后再加门控这样排查问题的时候能少绕很多弯路。4. 训练策略与评估4.1 损失函数与样本不均衡处理我用的损失函数是带类别权重的交叉熵。在PyTorch里nn.CrossEntropyLoss可以直接传一个weight参数给各个类别分配权重。比如好样本与坏样本比例是15:1那坏样本的权重就设为15让模型在优化时更关注少数类。这种方法虽然简单但在风控场景里非常稳。另一个我在实践中验证有效的策略是Focal Loss。它的思路在交叉熵基础上再加一个调制因子让模型把更多的注意力放在难分类的样本上。但如果你没有足够的时间去调两个超参数gamma和alpha我建议先上带权交叉熵它不需要额外调参效果也比较稳定。以下是我训练配置里的关键参数参数取值说明优化器AdamW比Adam泛化性好一点权重衰减更规范学习率3e-4Transformer分支的层普遍对学习率敏感不建议贪大batch size128过大吃显存过小训练不稳权重衰减1e-5在一定程度上抑制过拟合类别权重{0: 1.0, 1: 15.0}按样本比例反比设置Epoch30配Early Stopping4.2 关键训练技巧训练双模型融合模型最怕两个分支训练进度不一致一个学得差不多了另一个还处于欠拟合状态。我的解决方式是“两阶段训练”前三到五个epoch先冻结Transformer分支只训练LSTM分支、融合层和输出层让LSTM先收敛到一个合理的特征空间之后再解冻Transformer分支一起做联合微调。这样做的原因是LSTM参数量少、训练快先站住一个基本盘Transformer再逐渐把自己的长程信息注入进来模型整体会更稳定。另外学习率的设置也建议区别对待两个分支共享一个Adam优化器时很多小细节容易被淹没。我实际用的是两个优化器一个管LSTM分支和融合层学习率1e-3另一个管Transformer分支学习率1e-4。用两份参数列表传递给一个优化器也行两个optimizer各自管理也可以本质上都是避免Transformer分支学得太猛导致特征分布振荡。4.3 评估指标体系风控场景单看AUC远远不够。我日常盯的指标有四个AUC排序能力强弱的整体指标。KS衡量好坏样本在预测分分布上的最大区分度银行风控同事习惯看这个。PR-AUC在正样本极少的场景下比ROC-AUC更真实地反映坏样本的捕捉能力。Top1%/Top5% Lift只挑预测分最高的1%样本出来看看这部分样本里实际坏样本占比是否足够高。因为风控资源有限真正落地时我们往往只对最高分段做处置提升这个指标比提升整体AUC更有实际意义。训练完成后我把三个模型的指标放在一起对比过模型AUCKSPR-AUC说明单LSTM0.7820.4410.325短窗口特征扎实长依赖不够单Transformer0.7950.4620.344长依赖好短窗口细节略弱LSTMTransformer融合0.8230.4970.381两个尺度信息互補提升明显这个对比结果是验证集上的数据具体数值和数据集有关但规律是可以参考的融合模型在AUC上通常能比单模型提升2-4个点KS提升3-5个点。这在风控场景里已经是很有业务意义的提升了因为KS每提升0.03意味着同样通过率下能多拦截一批坏客户。5. 部署与预警逻辑落地5.1 模型导出与推理性能优化训练完的PyTorch模型要上线我一般先做TorchScript导出再走TorchServe或者直接用LibTorch做在线推理。TorchScript的好处是跨语言调用方便Java、Go的服务都能直接调用。导出时有一个小坑如果你用了nn.Parameter做位置编码导出的模型里位置编码是固定形状的如果实际推理时输入序列比训练时更长就会报索引越界的错误。我在导出前做了一步处理——把位置编码从nn.Parameter改成nn.Embedding虽然本质上都是可学习参数但nn.Embedding对不存在的索引会直接报错而不是静默越界排查问题方便很多。如果不想改结构也可以把max_seq_len预留得足够长比如512代价是模型文件会大一点。推理速度方面Transformer分支是主要开销。单条样本的推理如果在CPU上跑延迟可能在20-50ms如果并发高建议上GPU推理或者用ONNX Runtime加速。我在实际项目里用ONNX导出过一版延迟能压到5ms以下。但ONNX导出对动态序列长度支持不太好如果你各个用户样本的序列长度不一致导出时需要把seq_len维度设置成动态轴推理时再传入实际序列长度这点需要提前设计好。5.2 预警触发策略模型输出的是一个风险概率但线上不能只凭概率绝对值做决策还需要结合业务规则。我的预警逻辑是“概率分档 规则叠加”概率大于0.8直接触发高风险预警概率在0.6到0.8之间进入人工复核队列同时如果叠加规则命中比如7天内换过3台设备且单笔金额大于历史均值3倍也直接升级为高风险。这里想强调的点是模型的分数应该是一个排序分而不是一个精确的概率值所以用分档和规则结合比单纯卡阈值更贴近实际业务。预警触发之后还要做一次可解释性输出。我利用融合层的gate_value来生成一条简单的解释文案如果门控值偏向LSTM说明这次预警主要是由近期行为异常驱动的如果偏向Transformer则说明长周期模式偏移贡献更大。这个解释虽然简单但能让审核同事快速理解模型为什么给出这个分数比纯黑盒有说服力得多。6. 常见问题与排查技巧实录6.1 时间泄漏最隐蔽也最致命我在做这个项目时踩过最大的坑就是时间泄漏。具体场景是这样的我构造特征时加入了一个“用户过去30天平均借款金额”的统计特征这个特征在训练时很好用因为窗口内的行为数据包含了未来的信息。但在真正的线上推理场景这些未来信息是不存在的导致线下AUC非常漂亮线上效果惨不忍睹。这个问题在金融风控里出现频率极高尤其是用窗口统计特征时一定要反复检查特征构造的时间边界确保所有特征只使用T时刻之前的数据。6.2 Transformer分支过拟合金融样本量通常不大Transformer这种参数规模较大的模型很容易过拟合。我的解决办法有三层第一层是加大Dropout我用的TransformerEncoderLayer里dropout设置为0.3高于默认值第二层是早停验证集指标连续三个epoch不涨就停止训练第三层是卷积位置编码替代可学习位置编码有些实验显示在金融序列上稳定性更好但这项不是必须的如果你发现验证集比训练集低很多可以从这三个方向排查。6.3 特征维度不一致导致训练崩溃代码上最常遇到的坑是训练样本里出现了缺失特征或者padding补位没有对齐。比如LSTM分支用了64步序列但有些用户的短窗口只有30步如果直接丢进模型会报batch内序列长度不一致的错。我建议在DataLoader阶段就统一采样固定长度短了就按照mask逻辑补位不要担心补位会造成信息损失。两个分支的序列长度可以不同但在同一个batch内部各自分支的序列长度必须一致这点在自定义Dataset时需要特别注意。6.4 梯度爆炸与梯度消失深层Transformer和LSTM在训练前期都可能出现梯度问题。我的建议是LSTM分支的clip_grad_norm设置为5.0Transformer分支的clip_grad_norm设置为1.0两个分支用不同的梯度裁剪阈值。原因很简单两条分支的收敛节奏不一样用同一个阈值容易让其中一个分支的优化步长过大。7. 最后再分享两个实操小技巧第一个技巧是如果你时间有限不要一上来就搭复杂的双分支融合先用一条独立的Transformer基线跑通Pipeline再逐步加入LSTM分支。因为一旦融合模型的效果出了问题你分不清是哪个分支害的。我自己的项目里有很长一段时间就卡在“双分支都不差但融合之后反而变差”的尴尬局面后来把问题定位到短窗口和长窗口信息重叠度太高两个分支都在看同一段序列区别只是长度不同。后来我把LSTM分支的输入限制在最近7天、Transformer分支看满180天融合效果才真正起飞。第二个技巧是在训练时顺手把每个epoch的gate_value均值打印出来看它收敛到哪个方向。如果gate均值长期偏向0.9以上说明LSTM分支几乎没起作用Transformer分支基本在单打独斗如果gate长期在0.5附近波动说明两个分支的贡献比较均衡这时候融合的意义最大。这个指标可以像看损失曲线一样作为判断双模型融合是否真正发挥作用的晴雨表。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门