ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

多源Transformer在小微企业信贷评分中的落地实践与避坑指南

多源Transformer在小微企业信贷评分中的落地实践与避坑指南 简介一份面向金融风控从业者、机器学习和深度学习研究者的PDF资料聚焦小微企业信贷风险评估提出基于多源Transformer整合非结构化数据的评分模型。文档共42页单个PDF文件约2.14MB支持目录章节跳转与阅读器大纲快速定位页面内容清晰完整排版正常。目前已有72人学习下载。内容全面覆盖小微企业信贷风险来源与传统评估方法、非结构化数据的类型与价值、Transformer模型结构与注意力机制原理并重点阐述多源数据编码器、基于注意力机制的特征融合、风险评分模块设计、模型可解释性实现等核心环节。同时涵盖数据预处理、模型开发环境搭建、训练优化、评估指标与验证方法适合需要系统学习深度学习在信贷风控领域落地应用的读者参考。1. 多源Transformer凭什么进小微企业信贷评分小微贷款审批现场客户经理通常先翻核心流水、再看购销合同、再翻聊天记录和组织架构最后手写一段几百字的尽调意见。这一堆信息最终落到审批系统里往往只剩一张财务报表评分卡。系统只能看到结构化字段文本描述、流水节奏、舆情波动全被丢掉。这个标题把多源Transformer嵌入小微企业评分模型本质就是让模型替客户经理把没进评分卡的信息读回来——流水时序、尽调文本、外部数据各自走编码器再用注意力机制融合输出一个可解释的违约分。这篇笔记讲清楚数据怎么对齐、模型怎么搭、参数怎么调以及信贷场景最容易在哪几个地方翻车。适合正在做评分卡升级或从零搭小微风控模型的团队。2. 多源数据对齐把财报、流水、文本和舆情整理成一套张量先说清什么是“多源”。在小微评分里常规说法是四个来源结构化数值注册资本、从业年限、财务报表关键科目、征信摘要。时序数据核心结算账户的银行流水、纳税频率、开票金额变化。文本数据客户经理尽调意见、合同备注、行业研报摘要。外部标签数据工商变更、被执行人记录、舆情事件。这四个来源的挑战与其说是模型难不如说是“对齐难题”。流水是按天一条条的文本是一段段的财务字段是一行行的。要让Transformer把多源非结构化数据放进同一个样本第一步是先把它们转成统一格式。我一般用“窗口切分 token映射”来做。文本按分词结果截断到N个token流水把近12个月按月聚合出12 x D的张量再按周粒度补一条得到时序矩阵数值字段先做分位数归一化再映射成固定长度向量。这里有个常见误解Transformer要求输入是一个序列很多团队就把所有特征拼成一个长向量再硬塞进编码器。这确实能跑通但融合效果会差很多因为文本和数值的语义密度完全不同强行拼接会让注意力机制学到的是“位置偏移”而不是“语义关联”。更稳的做法是每个源先自己编码到融合层再交互。下面这份预处理代码把三类源数据压成一条样本import torch import numpy as np import pandas as pd TEXT_MAX_LEN 120 # 文本token上限 FLOW_DAYS 360 # 流水窗口长度近一年 FEAT_DIM 18 # 结构化特征个数 FIN_FLOW_DIM 8 # 月流量统计维度 def make_sample(row, texts, flows): # texts: dict段落到id序列已提前分词 # flows: 该样本的流水DataFrame [date, amount, balance] # 1) 文本源截断/补齐到定长 tokens texts[row[sample_id]][:TEXT_MAX_LEN] if len(tokens) TEXT_MAX_LEN: tokens tokens [0] * (TEXT_MAX_LEN - len(tokens)) # 0为pad id text_tensor torch.tensor(tokens, dtypetorch.long) # 2) 时序源按月重采样生成 12 x 8 flow flows[flows[sample_id] row[sample_id]].copy() flow[month] pd.to_datetime(flow[date]).dt.to_period(M) month_stat flow.groupby(month).agg( sum_amt(amount, sum), cnt(amount, count), avg_bal(balance, mean) ) seq np.zeros((12, FIN_FLOW_DIM), dtypenp.float32) for i in range(len(month_stat)): if i 12: break seq[i, 0] np.log1p(month_stat[sum_amt].iloc[i]) seq[i, 1] np.log1p(month_stat[cnt].iloc[i]) seq[i, 2] np.log1p(month_stat[avg_bal].iloc[i]) # 维度不足8的部分用其他特征补大额支出次数、透支天数等 flow_tensor torch.tensor(seq, dtypetorch.float32) # 3) 结构化特征分位数归一化 num_cols [age, reg_capital, credit_score, tax_rate] x_num row[num_cols].astype(np.float32).values / 100.0 num_tensor torch.tensor(x_num, dtypetorch.float32) return text_tensor, flow_tensor, num_tensor这段代码的核心是“三个张量、三种语义”文本是离散token时序是连续矩阵数值是低维稠密向量。注意月份序列顺序不能乱这里的index 0是最近一个月还是最早一个月必须在数据管线里统一后面训练时很容易在这里出错。另一个容易错的地方是归一化。结构化特征用 /100 是偷懒写法正式场景应该按全样本分位数做单调变换保留原始排序关系。文本截断的120是经验值太长会拖慢编码器太短会丢尽调意见里的风险信号我一般用尽调意见的平均字数乘以1.2作为截断上限。说一下嵌入维度设计。文本分支嵌入维度取128时序分支用两层Conv1D先压到64维再进Transformer数值分支只做线性映射到64维。后面的融合层统一对齐到64维这样多头注意力才有共同的可比空间。别把文本嵌入维度设成768再把时序压到16后面融合时维度差太大注意力矩阵会很稀疏。预处理完成后通常还要做一次“负样本审计”。在原始样本上看一看每个坏客户的尽调文本里是不是真的存在风险描述词流水里是不是真的有断流、集中转出这类模式。这一步帮我在后期解释模型时省了很多力。3. 模型结构编三个编码器分支再用门控融合成违约分把“多源”看清楚之后模型结构基本上是照着标题定死的每个源一个编码器再融合。常见做法不是“大而全的单个Transformer”而是“小分支 融合注意力”。原因很直接——小微评分的样本量通常在几万级单个大模型的参数规模撑不住。这里的结构我按三个分支来讲。文本分支用一个小型的Transformer Encoder4层隐层1284个注意力头。分词可以用已有的金融领域词表也可以直接上通用中文预训练模型的embedding层后面不跟着大模型跑只把它当成静态词向量。训练时冻结embedding能极大减少参数量。时序分支流水数据不是天然的语言序列我把按月的12 x 8矩阵摊平后再加一层位置嵌入编码器2层即可。数值分支只有18个特征直接用MLP映射到64维然后参与融合。常见的错误是让数值特征也过编码器这理论上可行但18个特征做自注意力信息量不足训练时很容易只学到冗余的重复向量反而干扰。融合层我用“门控融合 拼接”。具体是三个分支的输出向量分别叫 h_text、h_flow、h_num先都映射到64维然后计算一个门控向量 g sigmoid(W·[h_text; h_flow; h_num])最后融合向量 h_fused g_text * h_text g_flow * h_flow g_num * h_num。门控的好处是给不同源一个可学习的权重而不是固定拼接。真正上线后你会发现有的企业文本信息很弱、流水信息很强门控能自动压制弱源。下面是一个多源Transformer评分模型的核心定义用PyTorch写的import torch import torch.nn as nn class PosEmbedding(nn.Module): def __init__(self, d_model, max_len512): super().__init__() self.pe nn.Embedding(max_len, d_model) def forward(self, x): pos torch.arange(x.size(1), devicex.device) return x self.pe(pos) class BranchEncoder(nn.Module): def __init__(self, d_model, nhead, num_layers): super().__init__() encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, batch_firstTrue ) self.encoder nn.TransformerEncoder(encoder_layer, num_layersnum_layers) def forward(self, x): return self.encoder(x) class MultiSourceScorer(nn.Module): def __init__(self, vocab_size, d_model64, nhead4, num_text_layers4, num_flow_layers2, feat_dim18): super().__init__() self.d_model d_model # 文本分支词向量 位置编码 Encoder self.text_emb nn.Embedding(vocab_size, d_model, padding_idx0) self.text_pos PosEmbedding(d_model) self.text_enc BranchEncoder(d_model, nhead, num_text_layers) # 时序分支先过两层1D卷积抽局部特征再过Encoder self.flow_conv nn.Sequential( nn.Conv1d(8, 32, kernel_size3, padding1), nn.GELU(), nn.Conv1d(32, d_model, kernel_size3, padding1), nn.GELU(), ) self.flow_pos PosEmbedding(d_model, max_len12) self.flow_enc BranchEncoder(d_model, nhead, num_flow_layers) # 数值分支直接MLP到d_model self.num_proj nn.Sequential( nn.Linear(feat_dim, 64), nn.GELU(), nn.Linear(64, d_model), ) # 门控融合 self.gate nn.Linear(d_model * 3, 3) self.head nn.Sequential( nn.Linear(d_model, 32), nn.GELU(), nn.Linear(32, 1), ) def forward(self, text_ids, flow_seq, num_feat): # 文本分支输出取全局最大池化避免依赖单token t self.text_emb(text_ids) t self.text_pos(t) t self.text_enc(t) t torch.max_pool1d(t.transpose(1, 2), kernel_sizet.size(1)).squeeze(-1) # 时序分支 f flow_seq.transpose(1, 2) # (B, 8, 12) f self.flow_conv(f) # (B, d_model, 12) f f.transpose(1, 2) # (B, 12, d_model) f self.flow_pos(f) f self.flow_enc(f) f f.mean(dim1) # 平均池化 # 数值分支 n self.num_proj(num_feat) # (B, d_model) # 门控融合 fused_all torch.cat([t, f, n], dim-1) g torch.softmax(self.gate(fused_all), dim-1) h_fused g[:, 0:1] * t g[:, 1:2] * f g[:, 2:3] * n # 输出违约概率 logit self.head(h_fused) return torch.sigmoid(logit).squeeze(-1)这段代码的几处设置说明一下。位置编码用的nn.Embedding而不是原始的sin/cos公式是因为流水和文本的“位置”语义不同——文本里位置偏句法流水里位置代表月份先后两者用同一套三角公式意义不大直接让模型学更省事。这算是“Transformer的位置信息怎么计算”这个老问题在小微场景里的一个变体答案。文本分支的池化用最大池化而不是CLS token是因为小微尽调文本很短CLS在短文本上不稳定。时序分支用均值池化因为流水信号是累积性的。门控融合把三个分支的置信度做成可学习的权重比直接concat多一个好处训练后期收敛更稳不会因为某一个分支的梯度主导整个网络。如果你是在TensorFlow环境里做等价实现就是Keras Functional API拼三路输入逻辑一样。不建议用那种把文本、流水、数值拼在一个二维输入里的实现那是把多源做成了伪单源。4. 训练与调参不平衡样本下的收敛策略模型结构定了之后真正决定好坏的是训练流程。信贷评分和别的分类任务有两个显著差异正负样本严重不平衡以及错误代价不对称——把坏客户放进来比把好客户拒掉贵得多。以我见过的典型小微数据为例坏样本占比通常在3%到8%之间。直接用BCE Loss模型会很快学到“全预测为好”这个局部最优。解决方案按数据量分样本只有几千条时用负采样把好客户压到坏客户的两三倍样本足够多时用Focal Loss让模型把注意力放在那些“难分”的坏客户上。Focal Loss的实现直接给class FocalLoss(nn.Module): def __init__(self, alpha0.75, gamma2.0): super().__init__() self.alpha alpha # 正样本权重 self.gamma gamma # 困难样本聚焦系数 def forward(self, prob, target): eps 1e-8 prob torch.clamp(prob, eps, 1 - eps) pt target * prob (1 - target) * (1 - prob) alpha_t self.alpha * target (1 - self.alpha) * (1 - target) loss -alpha_t * (1 - pt) ** self.gamma * torch.log(pt) return loss.mean()alpha在这里表示“正样本的权重”0.75意味着一个坏客户在损失里的权重是一个好客户的3倍。gamma控制对易分样本的降权程度2.0是常用起点gamma太小易分样本仍然主导太大则训练初期震荡。跑第一轮验证时我会盯两个指标坏样本的召回率以及损失曲线有没有在第二三个epoch之后稳定下降。如果召回率一直在0.1以下先调alpha不要急着动模型结构。训练循环里还有三个我认为比学习率更重要的点。第一是梯度裁剪。多源Transformer融合层很容易梯度爆炸尤其是文本分支词向量解冻之后。我一般把max_grad_norm设为1.0既能压住爆炸又不影响收敛速度。第二是早停的指标。很多团队用验证集AUC早停但信贷场景AUC对坏样本占比并不敏感一个阈值预测也可能有虚高AUC。我会在验证集上同时计算AUC和坏样本召回率5%即分数最高的5%客户里覆盖了多少真实坏客户以这个复合指标作为早停依据。第三是学习率调度。先warmup 500步然后余弦衰减。多源模型的前几个epoch里不同分支的学习速度不一致直接上一层大的全局学习率经常出现文本分支已经过拟合、时序分支还没动静的情况。下面是一个典型训练循环的骨架optimizer torch.optim.AdamW(model.parameters(), lr3e-4, weight_decay1e-5) scheduler torch.optim.lr_scheduler.OneCycleLR( optimizer, max_lr3e-4, total_stepstotal_steps, pct_start0.15, ) scaler torch.cuda.amp.GradScaler() for epoch in range(max_epochs): for batch in train_loader: text_ids, flow_seq, num_feat, label batch optimizer.zero_grad() with torch.cuda.amp.autocast(): prob model(text_ids, flow_seq, num_feat) loss focal_loss(prob, label) scaler.scale(loss).backward() scaler.unscale_(optimizer) torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) scaler.step(optimizer) scaler.update() scheduler.step()强调一下max_lr和pct_start的关系。warmup占比15%是给文本分支的词向量“热启动”用的这一阶段学习率从0升到3e-4等文本分支学到基本词义后再统一下降。如果发现训练早期损失震荡把pct_start提调到0.3比手动降学习率更省事。还有一个容易被忽略的点Batch Size。多源Transformer每个样本至少三条输入流显存消耗大Batch Size设到128以上才比较稳。显存不够时不要粗暴减小Batch Size应该先减小文本截断长度比如从120降到80把显存给Batch Size留足。混合精度在这个场景收益明显能提升30%以上的吞吐且评分模型是数值敏感任务FP16对最终AUC几乎无影响。用这套流程训练完模型在开发集上的KS通常能做到0.35上下比纯结构化模型高0.08到0.12。但先别高兴下一章讲的几个坑几乎每个模型都会踩至少两三个。5. 避坑清单多源Transformer评分模型最容易翻车的五个位置这个标题看着新颖落地时翻车点却相当集中。下面按“现象→原因→解决”写最常见的五个都是我自己的血泪经验。坑一验证集里混进了“未来信息”离线指标虚高现象是验证集AUC 0.72上线后只有0.58回测怎么都对不上。原因是预处理里有一步“最近一个月流水统计”和标签生成用的观测期重叠了。比如用第12个月的数据预测第13个月的违约那么流水窗口末端和未来违约区间天然有信息泄漏。解决的规则只有一条按时间切分把整个过程串起来——[1, 10]月训练、11月做验证、12月做测试任何落在验证/测试区间内的特征一律归零或用历史均值填充。这条坑是新手最容易踩的但老手也常在文本字段上翻车尽调意见里偶尔会手写“客户已于X月逾期”这份意见又在验证集里被当成输入。所以在清洗阶段就要过滤掉所有包含“逾期”“违约”“不良”字样的尽调文本。坑二多源特征直接concat训练初期损失不降现象是loss从一开始就卡在0.69附近全预测为好客户的CE值怎么调都不动。原因通常是三个分支的数值尺度差异太大——文本输出是0到1的sigmoid值时序是归一化后的对数金额数值分支是原始字段三者拼起来后梯度被某个大尺度分支主导其他分支的权重几乎不更新。解决分两步一是每个分支输出过LayerNorm再进融合层二是检查各分支梯度的范数如果差异超过一个数量级就给大梯度分支的输入乘0.1的缩放系数让各分支的学习节奏接近。坑三固定窗口截断文本把尽调意见后半段的信号全丢了现象是线上精确率还行召回率始终提不上去调了很多参数都没用。原因在于很多尽调意见的格式是“前半段企业概况后半段风险提示”截断到120 token时风险提示正好被切掉。解决方法是分两段截断先截head 80 token tail 40 token中间用省略符接上Transformer的注意力能抓住两端信息或者直接按段落切分每段保留首尾句。这个方案对短文本尤其有效我在小微场景用后坏客户召回率涨了6个点。坑四Transformer在几千条样本上严重过拟合现象很典型训练AUC 0.95验证0.68而且dropout从0.1加到0.5都没用。原因是样本量撑不起4层编码器的参数量。解决策略把文本分支的层数从4降到2词向量冻结时序分支不做1D卷积直接线性映射dropout仍然设在0.2。如果还是过拟合就用预训练编码器加只训练最后两层把整个模型的前半段当特征抽取器用。小微数据通常不满一万条这种情况下与其改结构不如做特征工程把文本转换成规则特征是否含负面词、风险描述数量、长度并到融合层里。理论上是退回了特征工程但它能让模型更稳。坑五门控融合在推理阶段出现“源遮蔽”现象是融合权重g_text总是0.9其他两个源无论怎么调都占不到权重。原因是Softmax让门控向量的早期梯度方向过于一致训练后期固定成了局部最优。解决方式是在门控层的输出上加温度系数g softmax(gate / 0.5)让权重在0.1到0.9之间波动更缓和。更硬核的做法是直接去掉门控改用三个分支的加权均值固定权重文本0.5、时序0.3、数值0.2然后用融合层接一个1x1卷积去学习残差。后者在噪声数据上更稳定缺点是解释性差一些。五个坑说完你会发现它们大都发生在数据和训练策略层真正模型结构本身反而不会太作妖。这也符合这类方案的规律——瓶颈不在Transformer而在数据管线。6. 可解释性与上线验证让多方愿意接受这个黑匣子评级小微评分模型有一个特有约束客户经理、风控主管、贷审会都要能看懂“为什么给这个分数”。按上面的结构直接上线大概率会被挑战。所以我在训练完后会做两件事注意力归因和分数校准。先说注意力归因。Transformer本身有注意力矩阵把文本分支最后一层的注意力权重按token汇总可以看到哪些词在影响违约概率。方法类似把attention_map按平均头展开再做一个逐token的相对权重正权重词表里能直接看到“赌博”“民间借贷”“隐瞒”这时候模型的可信度会提高不少。不过注意力权重不等于因果贡献别拿去跟业务解释“因为这个词所以降分”更适合当特征洞察工具用。更严谨的解释用SHAP做。对数值分支和门控融合层的输出做SHAP归因能算出每个字段对最终分数的边际贡献。常见做法是把样本分箱对每箱跑200次蒙特卡洛采样得到每个特征的期望贡献值再画一个贡献排名表。这里有个坑SHAP在多源模型上计算量大建议先对融合层输出做归因不直接归因到词级别业务上足够用。然后说分数校准。模型输出是0到1的概率风控体系需要0到100的评分。我用等频分箱把开发集概率映射到1到100分分箱边界存成JSON上线时用二分查找落箱。这里要注意概率和分数的方向。业务上通常是“分数越高、风险越高”还是“分数越低、风险越高”必须在映射时定死否则后面总会有人拿错方向的数据去做权限。最后做一套“上线前体检”我自己固定跑这三个检查检查项通过标准失败处理训练集/验证集/测试集分布PSI 0.1重新切分或采集样本坏样本覆盖率5% 60%调采样或加特征关键因子单调性无明显倒挂检查数据质量这三项都过了模型才敢交到审批环节试运行。试运行头一个月每周看一次监控报表重点观察分数分布有没有漂移、新发放贷款里80分以上客户占比有没有异常提升。我现在做新模型有个习惯拿到任何评分模型的第一个动作是先画分数分布对比图看新旧模型对同一批客户的排序差异而不是盯着AUC不放。凡是对排序改变过大的模型先想办法降低激进程度再聊准确率。这个习惯帮我挡掉了至少两个上线后要返工的方案。这套多源Transformer的路子按上面几章走一万条样本以内也能跑出可用的模型关键是把对齐、训练、验证三条链路都控住。希望帮到你。本文还有配套的精品资源点击获取
返回列表