
简介本资源为Python实现基于文本与多模态数据的风险识别项目源码源自字节跳动安全AI挑战赛色情导流用户识别赛题面向计算机、人工智能相关专业学生及需要课程设计、期末大作业或竞赛练手的开发者帮助理解多模态特征融合与风险用户识别的完整建模流程。压缩包共11个文件约88KB以7个Python脚本为核心覆盖词向量训练、数据合并、K折训练、伪标签等环节另含txt依赖清单、docx说明手册、md文档与sh运行脚本便于快速复现与二次开发。目前已有360人学习。读者可获取从数据预处理、特征提取到模型训练评估的完整赛题方案参考配置与运行脚本理清工程结构并借助伪标签与K折思路提升识别效果适合作为多模态风险识别入门与进阶的实践素材。1. 文本加多模态做风险识别为什么单看文字已经不够用了电商评论里写「质量很好」配图却是一张开裂的实物照信贷申请材料文字工整上传的营业执照却是 PS 拼接的。这类场景里只跑一个文本分类模型准确率会被图片里的信息直接拖垮。python实现基于文本和多模态数据的风险识别要解决的就是这件事把文本、图像甚至结构化字段放进同一个判断链路输出一个可解释的风险分。它适合两类人——手上已有文本风控模型、想加一路图像信号的算法工程师以及要快速搭出可演示原型的在校开发者。热搜里 python、多模态数据、风险识别、源码这几个词高频出现说明大家真正卡住的不是概念而是「文本和图像怎么对齐、特征怎么拼、源码怎么跑起来」。这篇就按我实际搭过的顺序从数据组织讲到融合层再讲部署时那些让人翻车的细节。2. 多模态风险识别的数据组织与标签设计2.1 文本、图像、结构化字段怎么对齐成一条样本多模态项目第一个坑不在模型在数据。文本和图像天然是两种存储评论 ID、订单号、用户 ID 是它们之间唯一的桥。我一般会先定一张宽表主键用业务唯一 ID文本字段存原始字符串图像字段存相对路径而不是二进制结构化字段金额、频次、设备数单独列。这样做的原因是训练时要能按 ID 快速取图推理时要能只传文本降级运行。import pandas as pd # 宽表结构一行 一个风险事件样本 df pd.DataFrame({ sample_id: [s001, s002], text: [质量很好下次还来, 货不对板要求退款], image_path: [img/s001.jpg, img/s002.jpg], # 存相对路径不存二进制 amount: [199.0, 39.9], # 结构化字段 device_cnt: [1, 5], label: [0, 1] # 0 正常 1 风险 }) # 校验图文是否一一对应缺失的直接剔除别让模型学空图 df df[df[image_path].apply(lambda p: os.path.exists(p))]逻辑说明宽表把三种模态绑在同一主键下image_path存路径是为了让数据集类按需加载避免一次性把几万张图读进内存。参数上label用 0/1 二分类起步多级风险低/中/高建议先做二分类再拆阈值否则小样本类别会直接训崩。缺失图像样本要么剔除要么走纯文本分支不要用全黑图填充那会让模型学到「黑图正常」的伪相关。2.2 标签从哪来弱监督与人工复核的配比风险识别最贵的是标签。真实业务里正样本往往不到 5%全靠人工标不现实。常见做法是先用规则打一批弱标签命中敏感词、图像重复上传、金额异常再抽 10% 到 20% 做人工复核用复核结果去校准规则。我一般会把弱标签和人工标签分两列存训练时用人工标签弱标签只做预训练或样本筛选。# 弱标签规则示例命中任一条件先标为疑似风险 df[weak_label] ( (df[text].str.contains(退款|投诉|假)) | (df[device_cnt] 3) | (df[amount] 500) ).astype(int) # 人工复核只覆盖弱标签为正的样本控制成本 review_pool df[df[weak_label] 1].sample(frac0.2, random_state42)逻辑说明weak_label是启发式规则产物只用来缩小人工复核范围不能直接当训练标签否则模型只是在复现你的规则。frac0.2是复核比例样本量小时可以提到 0.3但别超过 0.5否则人工成本失控。这一步的产出是一份「干净标签子集」后面所有指标都以它为准。2.3 划分数据集时最容易忽略的泄漏问题文本和图像如果来自同一用户或同一批次随机划分会让训练集和验证集共享同一来源指标虚高。血泪经验是按用户 ID 或时间做分组划分而不是按行随机。比如用GroupShuffleSplit按user_id分组保证同一用户不出现在两边。from sklearn.model_selection import GroupShuffleSplit gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(gss.split(df, groupsdf[user_id])) train_df, val_df df.iloc[train_idx], df.iloc[val_idx]逻辑说明groups传用户维度test_size0.2是验证集比例。如果业务是时序场景直接按时间切用前 80% 训练、后 20% 验证别用随机。这一步做错后面融合层调得再漂亮上线也会打回原形。3. 文本分支从分词到风险语义向量3.1 中文风险文本的预处理与截断策略风险文本往往短、口语化、带错别字直接上大模型分词器未必最优。我一般先做一轮清洗去 URL、去连续重复字符、统一全半角再用预训练分词器编码。截断长度设 128 还是 256取决于你的文本分布——评论类 128 够用工单类建议 256。超过长度的部分直接截尾不要截头因为风险信号常在句尾「要求退款」「已投诉」。import re from transformers import AutoTokenizer def clean_text(t): t re.sub(rhttp\S, , t) # 去链接 t re.sub(r(.)\1{3,}, r\1\1, t) # 连续重复字压缩 return t.strip() tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) enc tokenizer( [clean_text(x) for x in train_df[text]], max_length128, truncationTrue, paddingmax_length, return_tensorspt )逻辑说明max_length128是文本分支的硬参数改大显存翻倍改小可能丢信号建议先用验证集跑 128/256 两档对比。paddingmax_length保证 batch 内等长方便后续和图像特征拼接。清洗函数里的重复字压缩能减少噪声但别过度清洗把「好好好」压成「好好」可能改变语义强度。3.2 用预训练模型抽风险语义向量文本分支的产出不是分类结果而是一个定长向量供后面融合。常见做法是取[CLS]位置的输出或者对最后一层做平均池化。我一般用[CLS]因为它在预训练阶段就被训练成句子级表示。import torch from transformers import AutoModel text_encoder AutoModel.from_pretrained(bert-base-chinese) with torch.no_grad(): out text_encoder(**enc) text_vec out.last_hidden_state[:, 0, :] # 取 [CLS]形状 [B, 768]逻辑说明last_hidden_state[:, 0, :]取的是每个样本的[CLS]向量维度 768。如果显存紧张可以冻结前 8 层只微调后 4 层或者直接用句向量模型。注意torch.no_grad()只在纯推理时用训练时要放开梯度。这一步的输出text_vec就是文本模态的「身份证」。3.3 文本分支单独评估先跑通再融合很多人一上来就搭融合网络结果文本分支本身就没调好融合后根本不知道是谁的锅。我的习惯是先把文本分支单独训一个分类头看它在验证集上的 AUC 和召回达标了再进融合。from torch import nn class TextOnly(nn.Module): def __init__(self, encoder, hidden768): super().__init__() self.encoder encoder self.head nn.Linear(hidden, 2) # 二分类 def forward(self, input_ids, attention_mask): out self.encoder(input_idsinput_ids, attention_maskattention_mask) cls out.last_hidden_state[:, 0, :] return self.head(cls)逻辑说明nn.Linear(768, 2)是分类头输出两类 logits。训练时用交叉熵正样本少就加pos_weight。这一步的指标是基线融合后如果没超过它说明图像分支在帮倒忙要回去查图像质量或融合方式。4. 图像分支与多模态融合层的实现4.1 图像预处理与轻量骨干选型风险场景的图像多是截图、证件照、商品图分辨率参差。预处理统一到 224×224归一化用 ImageNet 均值方差即可。骨干网络我一般选 ResNet18 或 EfficientNet-B0理由是推理快、显存友好风险识别不需要 ImageNet 冠军级别的容量。from torchvision import transforms from torchvision.models import resnet18 img_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) img_encoder resnet18(weightsIMAGENET1K_V1) img_encoder.fc nn.Identity() # 去掉原分类头输出 512 维特征逻辑说明Resize((224,224))是硬参数和骨干输入绑定。fc nn.Identity()把 ResNet 最后的全连接换成恒等映射输出 512 维图像向量。weightsIMAGENET1K_V1用预训练权重小样本场景下比从头训稳得多。如果图像里有大量文字截图类可以额外接一个 OCR 分支但那是另一个工程量级先别贪。4.2 早期融合、晚期融合怎么选融合方式决定模型上限。早期融合是文本向量和图像向量直接拼接后过分类头晚期融合是两路各自出分再加权。风险识别里我倾向早期融合因为文本和图像的交互信号图文矛盾只有在特征层才能被学到。class FusionModel(nn.Module): def __init__(self, text_encoder, img_encoder, text_dim768, img_dim512): super().__init__() self.text_encoder text_encoder self.img_encoder img_encoder self.fc nn.Sequential( nn.Linear(text_dim img_dim, 256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, 2) ) def forward(self, input_ids, attention_mask, images): t self.text_encoder(input_idsinput_ids, attention_maskattention_mask).last_hidden_state[:, 0, :] v self.img_encoder(images) x torch.cat([t, v], dim1) # 拼接768 512 1280 return self.fc(x)逻辑说明torch.cat([t, v], dim1)是早期融合的核心拼接后维度 1280。Dropout(0.3)防过拟合样本少时可提到 0.5。如果两路量纲差异大拼接前各加一层LayerNorm会更稳。晚期融合实现简单但学不到交互图文矛盾类风险会漏。4.3 训练循环与类别不平衡处理风险样本少损失函数要动。常见做法是交叉熵加pos_weight或者换 Focal Loss。我一般先用带权交叉熵简单可控。from torch.optim import AdamW pos_weight torch.tensor([neg_count / pos_count]) # 负正比 criterion nn.CrossEntropyLoss(weighttorch.tensor([1.0, pos_weight.item()])) optimizer AdamW(model.parameters(), lr2e-5, weight_decay0.01) for epoch in range(5): model.train() for batch in train_loader: optimizer.zero_grad() logits model(batch[input_ids], batch[attention_mask], batch[image]) loss criterion(logits, batch[label]) loss.backward() optimizer.step()逻辑说明pos_weight用负正样本比正样本越少权重越大但别超过 20否则模型全预测为正。lr2e-5是预训练模型微调的常用学习率融合层可以单独设大一点如 1e-3。训练轮数 5 起步看验证集 AUC 早停。5. 避坑与排查多模态风险识别最常见的 5 个翻车点5.1 指标虚高验证集和训练集共享了同一来源现象验证集 AUC 0.98上线后掉到 0.6。原因按行随机划分同一用户的文本和图像同时进了训练和验证。解决按用户 ID 或时间做分组划分重跑一遍指标通常 AUC 会回落到真实水平。5.2 图像分支拖后腿模型学会了「有图正常」现象融合后召回反而低于纯文本基线。原因正常样本几乎都有图风险样本图像缺失多模型把「有无图」当成了强特征。解决对缺失图像样本做掩码或在训练时随机丢弃部分图像输入逼模型不依赖单一模态。5.3 显存爆炸batch 里文本和图像同时加载现象batch_size 设 32 直接 OOM。原因文本编码器和图像编码器同时前向激活值叠加。解决文本和图像分开编码再拼接或者用梯度累积把等效 batch 做大实际 batch 设 8 到 16。5.4 分词截断把风险信号截没了现象长工单文本的风险召回低。原因max_length128截尾而风险描述常在末尾。解决改截头保留尾部或把长度提到 256用验证集确认召回变化。5.5 融合层过拟合训练集 loss 降验证集不降现象训练 3 轮后验证 loss 反弹。原因融合层参数多、样本少。解决加 Dropout、加 weight_decay、冻结文本编码器前若干层或者先训文本分支再解冻融合。6. 把风险分做成可解释输出与上线前的验证习惯模型出分只是第一步业务方要的是「为什么判风险」。我一般会在融合层后加一个简单归因文本侧看注意力权重最高的几个词图像侧看 Grad-CAM 高亮区域两者拼成一句人话解释。这不是为了炫技是为了让运营能复核、能申诉。# 文本侧取注意力权重最高的 token 作为关键词 attn out.attentions[-1].mean(dim1) # 平均多头 weights attn[0, 0, :] # [CLS] 对各 token 的关注 topk torch.topk(weights, k5).indices keywords tokenizer.convert_ids_to_tokens(enc[input_ids][0][topk])逻辑说明attentions[-1]取最后一层注意力mean(dim1)对多头平均[0, 0, :]是[CLS]对所有位置的权重。k5取前五关键词够业务看即可。图像侧用pytorch-grad-cam生成热力图叠加原图输出。注意注意力权重不等于因果只作参考。上线前我固定做三件事一是用分组划分的验证集跑一遍确认 AUC 和召回二是构造一批图文矛盾的对抗样本文字正面、图像负面看模型是否真学到了跨模态信号三是把纯文本、纯图像、融合三组指标并排打表确认融合确实有增益。验证项纯文本纯图像融合判断标准验证集 AUC0.820.710.89融合需高于两者风险召回0.650.520.78召回优先于精确对抗样本准确率0.550.600.80融合需明显领先这张表是我每次迭代必看的任何一列融合没赢就先别急着上线。最后说个习惯多模态项目的后悔药是数据版本管理文本、图像、标签三者任一变动都要打版本号否则两周后你根本复现不出当时那个 0.89。我吃过这个亏现在每个实验目录里必存一份数据快照的哈希。希望帮到你。本文还有配套的精品资源点击获取