多源Transformer实战:将非结构化数据引入小微贷款评分卡
发布时间:2026/9/17 4:49:52 作者:尧图编辑部 阅读量:1,286

简介面向金融风控从业者、算法研究员及小微金融领域的建模人员文档系统讲解如何基于多源Transformer架构将文本、图像等非结构化数据与传统结构化数据相融合用于构建小微企业信贷风险评分模型弥补传统评估方法依赖财务数据、信息维度单一的短板。文档共42页为PDF格式压缩包大小2.14MB篇幅适中却覆盖完整建模链路。内容从研究背景与小微企业信贷风险来源入手依次阐述非结构化数据的类型及在声誉评估、市场趋势分析中的应用价值详解Transformer结构、多头注意力机制并重点展开多源数据编码器设计、基于注意力机制的特征融合、风险评分模块构建、模型可解释性设计以及训练优化与评估指标准确率、精确率、召回率、PSI、基尼系数等。读者可按目录章节快速跳转参考其中的数据预处理、模型实现与验证思路。目前已有72人学习。1. 中小企业信贷里的非结构化数据为什么一定要用 Transformer小微企业评分卡普遍只吃税务、发票、征信这一类的结构化字段但真正让审批人员犹豫的往往是一份购销合同照片、一段经营异常说明、几页裁判文书摘要。这些非结构化材料进不了传统模型就意味着同样有逾期风险的两家客户在评分卡上只会呈现一模一样的“低风险”。多源 Transformer 想解决的问题就是把这些来源差异巨大的文本、版式文档和图片统一压缩成一条序列让注意力机制自己决定“股权变更说明”和“开票金额下滑”哪个才是当前违约信号。适用对象是正在搭建企业信贷风控特征的算法团队以及准备从手工特征迁移到深度模型的建模工程师。这篇文章按我从数据梳理到上线校准的顺序讲清每一层该怎么做。2. 多源 Transformer 的输入结构先把非结构化数据治理成“来源序列”直接把所有文本拼起来喂给 BERT在风控场景里效果并不好。原因是“发票明细”和“经营情况说明”虽然都是 token但它们在语义空间里的聚类方向完全不同如果模型不知道这个 token 来自哪一类材料注意力权重就会在不同来源之间来回牵扯。因此第一步不是选模型而是定义清楚有哪些来源再把每个来源编码成可训练的序列信号。2.1 先定义“源”而不是先做特征工程我落地时通常先做一次非结构化数据治理盘点把所有能拿到的材料和业务含义列清楚再决定要不要进入模型。中小微企业的常见来源可以按下面这张表归类源类型原始样本业务含义进入模型的形式文本裁判文书、经营异常说明、企业简介信用记录、履约意愿token ids source_type1版式文档增值税发票 PDF、银行流水经营稳定性、现金流OCR/解析成字段后再 tokenizesource_type2图像门头照、租赁合同、仓库照片欺诈识别、经营真实性离线视觉编码器产出向量source_type3时序动作申报频率、开票间隔、社保缴纳日经营连续性归一化数值拼到序列尾部source_type4这里有一个容易踩的坑source_type 一开始不要分太细。小额企业的样本量通常只有几十万来源类型超过 8 类后每个来源在注意力矩阵里能分到的有效监督信号会被稀释。我一般控制在 4 到 6 类个别来源如果内部语义差异很大可以在同一个 source_type 里加 segment_id 做二次区分。2.2 序列化一个企业实例只保留一条定长序列每个企业会对应多份文件但进入 Transformer 的只能有一个序列。常见做法是给每个来源内的文本做截断再按业务优先级拼接。拼接时需要在 token 级别记录两个信息来源 ID 和段 ID。来源 ID 用来告诉模型这段文本属于“税务发票”还是“裁判文书”段 ID 用来区分同一来源下不同文档的边界。下面是我常用的序列构造代码块输入是一份text_dict键是来源名值是已经清洗过的文本def build_source_sequence(text_dict, tokenizer, max_len512): tokens [] source_ids [] segment_ids [] seg 0 # 固定遍历顺序保证重复训练时不会因为 dict 顺序抖动 for source in sorted(text_dict.keys()): sub tokenizer.tokenize(text_dict[source]) # 每条来源最多占 max_len 长度给最终的 [CLS] 留一个位置 sub sub[: min(len(sub), max_len - len(tokens) - 2)] tokens sub source_ids [source_2_id[source]] * len(sub) segment_ids [seg % 2] * len(sub) seg 1 if len(tokens) max_len - 2: break tokens [tokenizer.cls_token] tokens source_ids [-1] source_ids segment_ids [0] segment_ids return { input_ids: tokenizer.convert_tokens_to_ids(tokens), source_ids: source_ids, segment_ids: segment_ids, attention_mask: [1] * len(tokens), }需要注意两个细节。第一source_2_id要提前固定下来不能在训练集和验证集里发生变动否则模型学到的来源嵌入会失效。第二segment_ids使用seg % 2是因为 BERT 的 token_type_embedding 只有两个位置如果文档超过 2 个就循环使用这个信号只作边界提示真正的文档边界还需要靠 attention mask 或位置编码去弥补。2.3 数值字段和缺失来源怎么放进序列纯文本之外评分模型一定还有年度营收、注册资本、近三个月开票金额这类数值字段。把它们转写成“注册资本500万”这种伪文本 token 不是不行但数值刻度差异会拖慢收敛。更稳定的做法是单独加一个数值嵌入分支将数值字段做 log 变换和分位数裁剪后过一个两层 MLP输出一个d_model维向量作为序列中的“虚拟 token”插在末尾。如果某个用户在税务数据上没有出现不能塞全 0 或补一个“无”那样模型会误认为缺失本身是规律。我的处理方式是训练时随机 mask 部分来源让模型学会在来源缺失的情况下给出分数而不是对某个值产生过度依赖。来源 mask 的逻辑我放在第 4 章讲训练策略时再展开。3. PyTorch 里搭一个能收敛的多源 Transformer 评分模块输入序列有了下一层就是把“来源 ID”变成模型认识的东西。多源 Transformer 在架构上有很多变体但在中小企业评分场景下我最推荐的是单流融合所有来源的 token 进入同一个 Transformer encoder只通过来源嵌入和 attention mask 做区分。双塔结构或交叉注意力更适合底层的语义对齐对只有几十万样本的信贷数据来说参数过多第一是训练不稳第二是线上特征解释困难。3.1 最小可用模型来源嵌入叠加 BERT encoder下面的代码是完整的评分模型主体。input_ids是上一节 build 出来的 token 序列source_ids是每个 token 的来源编号segment_ids用来区分文档段attention_mask标记有效 token 位置。import torch import torch.nn as nn from transformers import BertConfig, BertModel class MultiSourceTransformer(nn.Module): def __init__(self, d_model256, nhead8, num_layers6, num_sources6, num_classes1, dropout0.1): super().__init__() # 使用 BERT 配置但只保留 encoder 部分 config BertConfig( hidden_sized_model, num_attention_headsnhead, num_hidden_layersnum_layers, max_position_embeddings512, type_vocab_size2, hidden_dropout_probdropout, attention_probs_dropout_probdropout, ) # self.bert 提供完整的 embedding 和 encoder 层 self.bert BertModel(config, add_pooling_layerFalse) # 来源嵌入维度必须和 d_model 一致才能叠加 self.source_embed nn.Embedding(num_sources, d_model) self.classifier nn.Sequential( nn.Linear(d_model, 128), nn.GELU(), nn.Dropout(dropout), nn.Linear(128, num_classes), ) def forward(self, input_ids, source_ids, segment_ids, attention_mask): # 这一步拿到 BERT 的 token 位置 段嵌入 token_emb ( self.bert.embeddings(input_idsinput_ids, token_type_idssegment_ids) ) # 来源嵌入直接加在 token 表示上 token_emb token_emb self.source_embed(source_ids) # 使用 BERT encoder 计算语义表示attention_mask 保持原样 hidden self.bert.encoder( token_emb, attention_maskattention_mask.unsqueeze(1).unsqueeze(2), ).last_hidden_state # 取 [CLS] 向量做风险概率输出 logits self.classifier(hidden[:, 0]) return logits, hidden[:, 0]这个代码里最关键的叠加操作是token_emb self.source_embed(source_ids)。source_ids和input_ids长度一致每个 token 都对应一个来源向量。模型在计算 self-attention 时query 和 key 的相似度会天然带上一层“是否同源”的偏置。同源 token 的新闻源嵌入方向更接近attention score 天然偏高跨源信息则必须通过 [CLS] 或语义相关性才能交流。从零训练时建议先做一次领域语料上的 MLM 预训练再拿上百万个企业样本做小额贷款违约分类。如果数据量不到 5 万条直接加载中文预训练 BERT 做 base然后把self.bert替换成预训练模型只初始化source_embed。来源嵌入用正态分布初始化标准差设置为 0.02避免前几个 batch 的 loss 因为嵌入方差过大而发散。3.2 三个必须调对的结构参数参数表如下这是我跑小微企业数据时使用的基础配置适用于样本量在 10 万左右的小微贷款场景参数推荐值说明d_model256小样本别用 768容易过拟合训练速度也明显更快nhead8必须能被 d_model 整除否则 shape 对齐报错num_layers64 层欠拟合8 层以上在非大型数据集上收益很小max_position_embeddings512长文档不要硬塞按来源抽取关键片段后再拼接num_sources4 - 6来源 ID 粒度必须提前固化dropout0.1来源之间随机失活可以提升跨来源泛化能力还有一个容易忽略的点BertModel默认的type_vocab_size是 2对应segment_ids。如果直接用BertConfig的type_vocab_size2超过 2 的 segment 会越界报错业务上有多个文档时按seg % 2处理即可也可以把该参数改成实际最大段数。3.3 长文本来源的分块处理凡是银行流水的 PDF单个来源 token 常常会超过 512 的上限。我的处理方式是先把每个来源拆成长度接近的 chunk每个 chunk 过一遍自注意编码得到 chunk 向量后再拼接成“主题型 token”。这个做法比直接截断前 512 个 token 要稳定得多能够保留来源文档中后部的异常信息比如水电费欠缴记录经常出现在流水末尾。要注意这类 chunk 级聚合的输出不能和普通 token 一样送进深层交互。更合理的结构是让 chunk 经过一层浅层 transformer encoder 后再用一个平均池化生成来源表示最后把所有来源表示拼成一个新的短序列。这个短序列长度等于来源数再进入深层融合层。它能保证 GPU 显存不够时也不会丢失长文档的关键内容。4. 训练多源 Transformer样本切片、参数表和源缺失校验模型结构只是起点。真实信贷场景里训练集一旦按时间随机切分未来信息泄漏的风险会直接毁掉模型。多源 Transformer 对文本和数值特别敏感判决文书里的一句“2023 年列入被执行人”被当成历史特征会严重高估模型能力。因此训练流程必须围绕时间切片来组织。4.1 训练集/验证集/测试集按观察窗口划分推荐用三段时间切片而不是随机train_test_split。下表是我在实践中的常用方案数据段样本起始时间观察点表现期用途train2016-01 至 2020-062020-06-3018 个月训练主模型valid2018-01 至 2021-122021-12-3112 个月调整超参数test2019-01 至 2022-062022-06-3018 个月最终样本外评估观察点之前的文本和数值才能作为模型输入表现期内的逾期标签用来构造责任。举例来说“2022 年 5 月申请贷款2023 年 8 月违约”观察点是申请日表现期要覆盖到 2024 年 1 月左右确保至少能看到 18 个月的违约行为。如果表现期太短模型会把“短期逾期”和“最终违约”混淆分数排序能力大打折扣。这个切片之外还要做拒绝推断。因为模型只能看到已经放出去的客户被拒绝的客户没有表现直接训练会产生选择偏差。常见做法是先用放款样本训练一个初始模型再给拒绝样本打上带权重的“可能违约”伪标签重新训练。这一步不需要做得太复杂但完全不做最终模型在客群扩张时往往会高估好客户比例。4.2 优化器和损失函数配置多源 Transformer 是标准的监督分类任务我使用 BCEWithLogitsLoss 并设置正样本权重。正负样本比在小微贷款里往往高达 1:20 甚至 1:40如果不调pos_weight模型会永远输出低风险。from torch.nn import BCEWithLogitsLoss pos_weight torch.tensor([neg_count / pos_count]) criterion BCEWithLogitsLoss(pos_weightpos_weight) # logits shape: [batch_size, 1] loss criterion(logits.squeeze(-1), labels.float())pos_weight的作用是放大正样本的梯度而不是简单复制样本。比直接过采样更稳定不会让模型因为同一批违约样本反复出现而过拟合。优化器用 AdamW初始学习率 2e-4warmup 比例 0.1weight decay 0.01梯度裁剪max_grad_norm1.0。如果使用预训练 BERT 做初始化学习率要降到 1e-5 或 2e-5否则预训练权重在第一个 epoch 就被扰动。每 epoch 结束计算验证集 AUC 和 KS不只看 loss。loss 下降不代表排序能力好尤其当正样本太稀疏时loss 主要由负样本主导。以 KS 作为 early stopping 指标保留验证集 KS 最高的 checkpoint而不是最后一轮。4.3 来源缺失压力测试别让模型依赖单一来源真实授信过程中非结构化数据治理不可能在每次申请时都拿到全量来源。因此训练时要随机对 source 做 mask模拟缺失环境。实现方式是在每个 batch 内以 10% 到 15% 的概率随机丢弃某一点的数据把对应 token 的 source embedding 置零并且 attention mask 也置为 0。上线前还需要做一次表格化的来源压力测试缺失来源预期分数偏移业务处理策略裁判文书分数略降可继续走自动审批税务发票分数明显下移转人工审批或提高担保要求图像资料无显著变化作为补充材料不参与打分配置如果去掉税务发票后模型分数偏移超过 30 分说明模型把高频开票信息当作强依赖特征。这类样本在实际运行时必须走补充尽调模型分数不能直接对外开放。5. 从模型概率到信贷分分数校准和上线后的持续验证训练完的多源 Transformer 输出的是 logit属于未校准的违约概率不能直接和评分卡阈值对接。信贷业务里更适合用标准分常见的做法是将 logit 映射到 300 到 850 分的范围保持分数越高风险越低。import numpy as np def logit_to_score(logit, base_odds30, pdo40, base_score600): # base_odds: 基准赔率即违约概率/(1-违约概率) # pdo: odds 翻倍时增加的分数典型值为 40 odds np.exp(logit) factor pdo / np.log(2) return base_score factor * np.log(odds / base_odds)这个公式保证了 iflogit增加一个单位分数按固定速率下降。上线前要对校准后的分数做十分位单调性验证将测试集按分数从高到低分成十份每一份的坏账率应当严格递增。如果排名靠后两档坏账率反而更低说明模型对尾部客户学习不稳定需要回头检查训练集中是否缺少这一类样本。模型上线后还要做两类监控。第一类是分数分布漂移监控跟踪每周申请客群的分数均值与标准差如果某个时间点分布整体偏移要结合宏观政策和准入规则进行判断。第二类是来源参与度监控统计每个来源的注意力权重占比当某一来源的平均注意力权重持续上升或下降都需要触发告警。这些现象往往代表数据源质量发生变化或者模型逐渐依赖一个不该成为核心的弱特征。最后补充一个实用技巧把每个企业样本的非结构化数据源记录保留下来做成“评分证据包”。当业务人员审阅到模型拒绝的客户时可以直接查看是裁判文书里的哪句话、还是发票解析结果中的哪一项拉低了分数。多源 Transformer 的收益不仅来自准确率本身也来自它在非结构化数据治理上的完整留存。本文还有配套的精品资源点击获取