Python实现文本与多模态融合的风险识别源码方案
2026/9/23 8:31:47 网站建设 项目流程

简介:这份源码面向Python开发者、安全方向学生及参加数据挖掘竞赛的选手,提供一套基于文本与多模态数据的风险识别完整实现,核心场景为字节跳动安全AI挑战赛中的色情导流用户识别任务,适合作为课程设计、期末大作业或赛题复现的参考方案。压缩包共11个文件,约88KB,以7个Python脚本为主体,覆盖词向量训练、数据合并、K折训练、伪标签等流程,另含requirements依赖清单、readme说明、docx手册与run.sh运行脚本,便于快速理解项目结构与执行顺序。目前已有360人学习下载,具备一定参考热度。读者可从中获取赛题级别的特征工程思路、多模态融合建模方法、K折交叉验证与伪标签半监督策略,以及完整的目录组织与配置管理方式,对风险识别类项目的落地与竞赛复盘均有实际帮助。

1. 文本加多模态做风险识别:为什么单看文本已经不够用了

金融风控、内容审核、电商反欺诈这些场景里,纯文本模型早就不是新鲜事。但真正在一线跑过项目的人会发现一个尴尬现实:用户提交的申请材料里,文本只占信息量的一小部分,剩下的全在图片、表格截图、语音转写、时序行为日志里。一个典型的信贷反欺诈场景,申请人填写的职业和收入是文本,上传的工资流水是图片,设备传感器和点击流是时序数据,这三者单独看都正常,合在一起才暴露出矛盾。这就是多模态风险识别要解决的问题:把不同模态的信息在特征层或决策层做融合,让模型看到单模态看不到的交叉信号。

这篇文章面向的是想用 python 实现基于文本和多模态数据的风险识别源码的从业者。我会把整个方案的选型理由、数据管线、融合策略、训练脚本和踩坑记录讲清楚,让你能照着搭出一套可复现的基线系统。适合有 python 基础、做过单模态分类任务、想往多模态方向推进的工程师。不适合完全没写过深度学习训练循环的新手,但我会把关键参数和调试方法写细,跟着走也能跑通。

2. 多模态风险识别的数据管线与融合策略选型

2.1 三种模态的预处理路径怎么定

文本模态的处理相对成熟,常见做法是用预训练语言模型做编码,取 CLS 向量或最后一层隐状态做池化。但风险识别场景的文本往往很短,可能只有几十个字,这时候直接用大模型编码反而容易过拟合。我一般会先用轻量级编码器(比如 6 层 Transformer)做基线,确认文本信号本身是否足够强,再决定要不要上更大的模型。

图像模态在风险识别里通常是证件、票据、截图这类文档图像。不要直接套用 ImageNet 预训练的 ResNet,因为文档图像的纹理和自然图像差异很大。更稳的做法是用 OCR 先把文字抽出来,把图像问题转成文本问题,同时保留图像的布局特征(比如文字块的位置、大小、对齐方式)作为辅助特征。如果一定要端到端做,用 Document Image Transformer 这类在文档数据集上预训练过的骨干网络。

时序模态在风控里对应的是用户行为序列,比如点击流、交易流水、设备传感器读数。这类数据的核心是采样频率不统一、长度差异大。常见做法是先用固定窗口做聚合统计(均值、方差、变化率),再把统计特征和原始序列一起送入时序编码器。不要一上来就上 Transformer,先用 1D-CNN 或 GRU 做基线,确认时序信号有增量价值再换复杂模型。

import numpy as np import pandas as pd from sklearn.preprocessing import StandardScaler def preprocess_text(df, text_col='text'): """文本模态:截断+填充,返回 token id 序列""" max_len = 128 # 实际项目中这里接 tokenizer,此处用长度示意 lengths = df[text_col].str.len().clip(upper=max_len) return lengths.values.reshape(-1, 1) def preprocess_image(df, img_path_col='img_path'): """图像模态:提取布局特征(宽高比、文字块数量、平均字号)""" feats = [] for p in df[img_path_col]: # 实际项目中用 cv2 或 PIL 读取并做 OCR # 这里用占位特征说明结构 feats.append([1.0, 0.5, 0.3]) return np.array(feats) def preprocess_sequence(df, seq_col='behavior_seq'): """时序模态:窗口聚合统计""" window = 10 stats = [] for seq in df[seq_col]: arr = np.array(seq) if len(arr) < window: arr = np.pad(arr, (0, window - len(arr))) # 滑动窗口取均值和方差 rolled = np.lib.stride_tricks.sliding_window_view(arr, window) stats.append([rolled.mean(), rolled.std()]) return np.array(stats) # 三路特征拼接 text_feat = preprocess_text(df) img_feat = preprocess_image(df) seq_feat = preprocess_sequence(df) combined = np.concatenate([text_feat, img_feat, seq_feat], axis=1) scaler = StandardScaler() combined = scaler.fit_transform(combined)

这段代码展示了三路模态各自最简的预处理路径。文本用长度做占位,实际要接 tokenizer;图像提取布局特征,实际要接 OCR 和版面分析;时序做滑动窗口聚合。最后用 StandardScaler 做标准化,这一步很关键,因为不同模态的特征量纲差异大,不标准化会让融合层偏向数值大的模态。参数方面,max_len 根据文本实际长度分布定,一般取 95 分位数;window 根据行为序列的采样频率定,高频序列取小窗口,低频取大窗口。

2.2 融合策略:早期融合、晚期融合还是交叉注意力

融合策略的选择直接决定模型上限。早期融合是把三路特征在输入层就拼在一起,送进一个统一的编码器。优点是实现简单,缺点是不同模态的语义空间差异大,强行拼接会让模型难以学到有效的跨模态交互。晚期融合是每路模态单独训练一个分类器,最后对预测概率做加权平均或投票。优点是每路模态可以独立调优,缺点是丢失了模态间的细粒度关联。

交叉注意力融合是目前效果最好的方案,也是我推荐的首选。核心思路是让文本特征作为 query,图像和时序特征作为 key 和 value,做注意力计算,这样文本可以主动从其他模态中提取相关信息。反过来也可以让图像做 query,取决于哪个模态是主模态。在风险识别场景里,文本通常是主模态,因为风险标签的定义往往基于文本描述,所以用文本做 query 更合理。

import torch import torch.nn as nn class CrossModalFusion(nn.Module): def __init__(self, text_dim=256, img_dim=128, seq_dim=64, hidden_dim=128): super().__init__() # 各模态投影到统一维度 self.text_proj = nn.Linear(text_dim, hidden_dim) self.img_proj = nn.Linear(img_dim, hidden_dim) self.seq_proj = nn.Linear(seq_dim, hidden_dim) # 交叉注意力:文本做 query self.cross_attn = nn.MultiheadAttention( embed_dim=hidden_dim, num_heads=4, batch_first=True ) self.norm = nn.LayerNorm(hidden_dim) self.classifier = nn.Sequential( nn.Linear(hidden_dim, 64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64, 2) ) def forward(self, text_feat, img_feat, seq_feat): # 投影到统一维度 t = self.text_proj(text_feat).unsqueeze(1) # (B, 1, H) i = self.img_proj(img_feat).unsqueeze(1) # (B, 1, H) s = self.seq_proj(seq_feat).unsqueeze(1) # (B, 1, H) # 图像和时序拼接作为 key/value kv = torch.cat([i, s], dim=1) # (B, 2, H) # 交叉注意力 attn_out, _ = self.cross_attn(t, kv, kv) # (B, 1, H) out = self.norm(t + attn_out).squeeze(1) # 残差连接 return self.classifier(out)

这段代码实现了一个最简的交叉注意力融合模块。text_proj、img_proj、seq_proj 把三路特征投影到同一维度 hidden_dim,这是融合的前提。cross_attn 用文本做 query,图像和时序拼接后做 key 和 value,注意力机制会自动学习文本应该从哪些模态、哪些位置提取信息。残差连接和 LayerNorm 是稳定训练的关键,不加的话注意力层容易梯度爆炸。参数方面,hidden_dim 一般取 128 或 256,num_heads 取 4 或 8,dropout 在 0.2 到 0.5 之间调,风险识别场景标签噪声大,dropout 可以适当调高。

2.3 损失函数与类别不平衡的处理

风险识别场景的标签分布极度不平衡,正常样本可能占 99% 以上。直接用交叉熵损失会让模型倾向于全部预测为正常。常见做法是给少数类加权,权重取正负样本比例的反比。更稳的做法是用 Focal Loss,它通过调节因子让模型聚焦于难分类样本。我一般会先用加权交叉熵做基线,如果召回率上不去再换 Focal Loss。

class FocalLoss(nn.Module): def __init__(self, alpha=0.25, gamma=2.0): super().__init__() self.alpha = alpha self.gamma = gamma def forward(self, logits, targets): ce_loss = nn.functional.cross_entropy(logits, targets, reduction='none') pt = torch.exp(-ce_loss) focal_loss = self.alpha * (1 - pt) ** self.gamma * ce_loss return focal_loss.mean()

alpha 控制正负样本的权重,一般取 0.25 到 0.75 之间,正样本少就取大一点。gamma 控制难易样本的聚焦程度,取 2.0 是常见默认值。如果训练时发现 loss 下降太慢,可以把 gamma 调到 1.0;如果发现模型对少数类仍然不敏感,把 alpha 调到 0.75 以上。

3. 用 python 搭一套可复现的多模态风险识别训练管线

3.1 数据集构建与模态对齐

多模态项目最容易翻车的地方不是模型,是数据对齐。文本、图像、时序三路数据必须能通过同一个 ID 关联起来,而且时间戳要对齐。我见过太多项目因为图像和文本的时间戳差了几天,导致模型学到的全是噪声。构建数据集时,第一步是确认三路数据的 ID 体系是否一致,不一致的要先做映射。第二步是检查时间窗口,图像和文本的时间差超过阈值(比如 1 小时)的样本要丢弃或标记。

import pandas as pd def build_multimodal_dataset(text_df, img_df, seq_df, time_threshold=3600): """三路数据按 ID 和时间戳对齐""" # 假设三路数据都有 id 和 timestamp 列 merged = text_df.merge(img_df, on='id', suffixes=('_text', '_img')) merged = merged.merge(seq_df, on='id') # 计算时间差 merged['time_diff'] = (merged['timestamp_text'] - merged['timestamp_img']).abs() # 过滤时间差过大的样本 valid = merged[merged['time_diff'] <= time_threshold].copy() # 标签对齐:以文本标签为准,检查其他模态标签是否一致 valid['label'] = valid['label_text'] return valid # 划分训练集和验证集,按时间切分而不是随机切分 valid = valid.sort_values('timestamp_text') split_idx = int(len(valid) * 0.8) train_df = valid.iloc[:split_idx] val_df = valid.iloc[split_idx:]

这段代码的关键点是按时间切分而不是随机切分。风险识别场景的数据有时间泄漏问题,随机切分会让未来信息泄漏到训练集,导致验证指标虚高。按时间切分虽然会让指标看起来低一些,但更接近线上真实表现。time_threshold 根据业务场景定,信贷申请一般取 1 小时,内容审核可以放宽到 24 小时。

3.2 训练循环与早停策略

训练循环本身不复杂,但风险识别场景有几个特殊点。第一是验证指标不能只看准确率,要看 AUC 和召回率。第二是早停策略要基于验证集的 AUC 而不是 loss,因为 loss 和业务指标经常不一致。第三是学习率要用 warmup,前几个 epoch 线性升温,避免初期梯度太大破坏预训练权重。

from sklearn.metrics import roc_auc_score import torch.optim as optim def train_epoch(model, dataloader, optimizer, criterion, device): model.train() total_loss = 0 for batch in dataloader: text_feat = batch['text_feat'].to(device) img_feat = batch['img_feat'].to(device) seq_feat = batch['seq_feat'].to(device) labels = batch['label'].to(device) optimizer.zero_grad() logits = model(text_feat, img_feat, seq_feat) loss = criterion(logits, labels) loss.backward() # 梯度裁剪,防止梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() total_loss += loss.item() return total_loss / len(dataloader) def evaluate(model, dataloader, device): model.eval() all_probs, all_labels = [], [] with torch.no_grad(): for batch in dataloader: text_feat = batch['text_feat'].to(device) img_feat = batch['img_feat'].to(device) seq_feat = batch['seq_feat'].to(device) logits = model(text_feat, img_feat, seq_feat) probs = torch.softmax(logits, dim=1)[:, 1] all_probs.extend(probs.cpu().numpy()) all_labels.extend(batch['label'].numpy()) return roc_auc_score(all_labels, all_probs) # 训练主循环 best_auc = 0 patience = 5 wait = 0 for epoch in range(50): train_loss = train_epoch(model, train_loader, optimizer, criterion, device) val_auc = evaluate(model, val_loader, device) print(f"Epoch {epoch}: loss={train_loss:.4f}, auc={val_auc:.4f}") if val_auc > best_auc: best_auc = val_auc torch.save(model.state_dict(), 'best_model.pt') wait = 0 else: wait += 1 if wait >= patience: print(f"Early stop at epoch {epoch}") break

梯度裁剪的 max_norm 取 1.0 是保守值,如果训练稳定可以放宽到 5.0。patience 取 5 意味着验证 AUC 连续 5 个 epoch 不提升就停止,这个值根据数据集大小调,小数据集可以取 3,大数据集取 10。保存模型时只保存 state_dict,不要保存整个模型对象,否则加载时会依赖原始代码结构。

3.3 模型评估与可解释性分析

风险识别模型上线前必须做可解释性分析,因为业务方需要知道模型为什么判定某笔申请有风险。最常用的方法是 SHAP 值,它可以量化每个特征对预测结果的贡献。对于多模态模型,可以分别计算文本、图像、时序三路特征的 SHAP 值,看哪一路贡献最大。

import shap import numpy as np # 用验证集的一批样本做解释 explainer = shap.DeepExplainer(model, background_tensor) shap_values = explainer.shap_values(test_tensor) # 按模态分组统计贡献 text_contrib = np.abs(shap_values[0][:, :text_dim]).mean() img_contrib = np.abs(shap_values[0][:, text_dim:text_dim+img_dim]).mean() seq_contrib = np.abs(shap_values[0][:, text_dim+img_dim:]).mean() print(f"Text: {text_contrib:.4f}, Image: {img_contrib:.4f}, Seq: {seq_contrib:.4f}")

如果发现某一路模态的贡献接近零,说明该模态没有提供增量信息,可以考虑去掉以简化模型。如果三路贡献都显著,说明融合策略有效。SHAP 计算量大,不要在全量数据上跑,取几百个样本就够了。

4. 多模态风险识别落地时的避坑与排查清单

4.1 模态缺失导致的推理崩溃

现象:训练时三路模态齐全,上线后部分请求只有文本没有图像,模型直接报错或输出异常。

原因:训练管线假设三路模态都存在,没有处理缺失情况。实际业务中图像上传失败、时序数据延迟到达都是常态。

解决:在模型 forward 里加模态掩码,缺失的模态用零向量填充,同时传入一个 mask 向量告诉注意力层哪些位置是有效的。训练时随机丢弃部分模态做数据增强,让模型学会在模态缺失时也能工作。

4.2 时间泄漏让验证指标虚高

现象:验证集 AUC 达到 0.95,上线后掉到 0.7。

原因:数据划分时用了随机切分,未来样本泄漏到训练集。或者特征工程里用了全局统计量(比如全量数据的均值),这些统计量包含了未来信息。

解决:严格按时间切分,特征工程里的统计量只能从训练集计算,然后应用到验证集和测试集。检查所有特征的计算逻辑,确认没有用到当前样本之后的信息。

4.3 模态间特征尺度差异导致融合失效

现象:融合模型的指标和单文本模型差不多,多模态没有带来增量。

原因:不同模态的特征量纲差异大,比如文本 embedding 的数值范围是 [-1, 1],时序统计量的范围是 [0, 10000],融合层会被数值大的模态主导。

解决:每个模态单独做标准化,用各自的均值和方差。如果用了 BatchNorm,注意训练和推理时的行为差异。可以在融合前加一层 LayerNorm,让各模态特征在同一尺度上。

4.4 标签噪声让模型学到错误关联

现象:模型在训练集上表现很好,但人工检查预测结果时发现很多明显错误的判定。

原因:风险识别场景的标签往往来自人工标注或规则引擎,噪声很大。模型会学到标注者的偏见或规则的漏洞。

解决:先用规则引擎做一轮预筛选,把高置信度的正负样本挑出来做训练集,低置信度的样本做验证集。训练时用 label smoothing,把硬标签软化,减少噪声影响。定期人工抽检模型预测结果,发现系统性错误及时修正。

4.5 推理延迟超出业务要求

现象:模型离线指标很好,但上线后单次推理耗时超过 500ms,业务方不接受。

原因:多模态模型参数量大,而且三路模态的预处理耗时叠加。图像 OCR 和时序特征计算都是耗时操作。

解决:把预处理和模型推理分离,预处理结果缓存起来。模型侧做量化或蒸馏,把大模型压缩到小模型。如果业务允许,把三路模态做成异步推理,先返回文本模型的快速结果,其他模态结果出来后再更新。

5. 从基线到进阶:多模态风险识别的调优技巧与验证方法

5.1 用消融实验确认每路模态的增量价值

搭好多模态模型后,第一件事不是调参,是做消融实验。分别训练只有文本、文本+图像、文本+时序、三路全量的模型,对比验证集 AUC。如果文本+图像的指标和纯文本差不多,说明图像模态没有提供增量信息,要么是图像特征提取有问题,要么是图像本身和风险标签无关。消融实验的结果直接决定后续优化方向,不要跳过这一步。

模态组合验证 AUC相对纯文本提升
纯文本0.782基线
文本+图像0.791+0.009
文本+时序0.826+0.044
三路全量0.831+0.049

从这张示意表可以看出,时序模态的增量最大,图像模态增量很小。如果实际项目中也是这个 pattern,可以考虑把图像模态的权重降低,或者把图像特征从端到端改为 OCR 文本后融合。

5.2 跨模态注意力权重的可视化验证

交叉注意力融合的一个好处是注意力权重可以可视化,让你看到文本到底从图像和时序的哪些位置提取了信息。如果发现注意力权重均匀分布,说明模型没有学到有效的跨模态关联,可能是融合层初始化有问题,或者学习率太大导致注意力层没有充分训练。

# 提取注意力权重 def get_attention_weights(model, text_feat, img_feat, seq_feat): t = model.text_proj(text_feat).unsqueeze(1) i = model.img_proj(img_feat).unsqueeze(1) s = model.seq_proj(seq_feat).unsqueeze(1) kv = torch.cat([i, s], dim=1) _, attn_weights = model.cross_attn(t, kv, kv) return attn_weights # (B, num_heads, 1, 2) # 可视化:看文本对图像和时序的注意力分布 weights = get_attention_weights(model, text_feat, img_feat, seq_feat) avg_weights = weights.mean(dim=1).squeeze() # (B, 2) print(f"Image attention: {avg_weights[:, 0].mean():.4f}") print(f"Seq attention: {avg_weights[:, 1].mean():.4f}")

如果图像注意力权重接近 0.5,说明模型对图像和时序的依赖差不多。如果某一路接近 0,说明该模态被模型忽略了。这个信息可以用来指导模态剪枝或特征工程优化。

5.3 线上 A/B 测试的设计要点

离线指标好不代表线上效果好。上线前一定要做 A/B 测试,把多模态模型和现有单模态模型做对比。A/B 测试的设计有几个关键点:第一是分流要随机,不能按用户 ID 的奇偶分,因为用户 ID 可能和风险等级相关。第二是观察指标要选业务指标,比如欺诈拦截率、误杀率、人工审核工作量,不要只看 AUC。第三是测试周期要足够长,至少覆盖一个完整的业务周期,比如一周。

我自己的习惯是,离线 AUC 提升低于 0.02 就不做 A/B 测试了,因为线上噪声很容易淹没这个量级的提升。如果离线提升超过 0.05,才值得投入工程资源做线上实验。这个阈值不是绝对的,根据业务对风险的敏感度调。

多模态风险识别这个方向,技术上限比单模态高很多,但工程复杂度也高一个量级。我踩过最大的坑是过早引入复杂融合策略,结果数据对齐没做好,模型学到的全是噪声。后来学乖了,先用最简的早期融合跑通全流程,确认三路模态都有增量价值,再逐步换成交叉注意力。这个顺序不能反。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询