多任务文本与结构化数据融合建模实战
简介本资源是面向大学生竞赛选手与数据科学初学者的实战备赛资料包聚焦天池O2O优惠券使用预测与达观杯文本智能处理两大经典赛题提供从特征工程、模型训练到结果提交的完整技术路径参考。压缩包共9个文件含4个核心Python脚本如xgb.py、extract_feature.py、daguan.py等、2张关键流程图rank.png、个人信息.png、1个数据文件、1份README.md说明文档及1个LICENSE协议文件整体仅121KB轻量易读便于快速理解项目结构与代码逻辑。目前已有107人学习下载适合正在备赛挑战杯、数学建模或AI类赛事的学生团队可直接复用特征提取策略、XGBoost建模框架及稀疏特征分类器实现并结合文档梳理出赛题解题脉络、常见坑点与优化方向。1. 为什么两个看似不搭界的赛题被塞进同一个压缩包——o2o优惠券预测与文本智能处理的底层共性拆解你点开这个名为“天池竞赛o2o优惠券使用预测 与 达观杯文本智能处理挑战赛.zip”的压缩包第一反应可能是这俩东西能放一起一个是用户领了券到底用不用的二分类问题另一个是新闻标题分类或短文本语义匹配数据形态、评估指标、建模逻辑全都不一样。但真实项目里它们常被并列放在同一份工程基线代码中——不是因为主办方偷懒打包而是因为工业级文本智能系统落地时90%的脏活累活都卡在「异构特征对齐」和「多任务信号耦合」这两个黑匣子环节。o2o场景里用户是否核销优惠券既取决于券面信息文本、商户描述文本、历史行为序列结构化也受时间窗口、地理位置等强约束影响而达观杯的文本分类任务若想突破92%准确率瓶颈必须引入用户点击反馈、会话上下文、甚至跨域知识图谱作为弱监督信号。本篇不讲比赛排名只复现一个能同时跑通这两个赛题的最小可验证框架用统一特征工厂抽象文本结构化混合输入用共享底层编码器实现跨任务迁移用动态权重融合解决标签稀疏问题。适合正在搭建推荐/风控/客服NLU流水线的算法工程师以及想把Kaggle式单任务模型升级为产线级多目标系统的在校生。2. 构建统一特征工厂从原始CSV到TensorDataset的四层清洗流水线两个赛题原始数据形态差异极大o2o数据含user_id,coupon_id,date_received,date,discount_rate等12个字段其中discount_rate是字符串格式如“满200减50”达观杯则是纯文本text列加label列但存在大量HTML标签残留和emoji乱码。若分别写清洗脚本后续特征工程必然重复造轮子。我们采用分层抽象策略将特征构建拆为四个不可逆阶段每层输出可缓存、可版本化的中间产物。2.1 第一层原始数据标准化Raw → Clean核心目标是消除格式污染统一缺失值标记和编码。o2o数据中date_received和date字段存在空格、中文日期如“20160701”混用达观杯文本含\x00控制字符和br标签。此处不直接正则替换而是用pandas.read_csv的converters参数注入字段级清洗函数import pandas as pd import re def clean_date(x): if pd.isna(x) or x null or x : return None # 统一转为YYYYMMDD整数格式 x str(x).strip() if len(x) 8 and x.isdigit(): return int(x) # 处理2016-07-01格式 if - in x: return int(x.replace(-, )) return None def clean_text(x): if pd.isna(x): return x str(x) # 移除HTML标签 x re.sub(r[^], , x) # 移除控制字符 x re.sub(r[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f], , x) # 替换连续空白为单空格 x re.sub(r\s, , x).strip() return x # 加载o2o数据 o2o_df pd.read_csv(data/o2o/ccf_offline_stage1_train.csv, converters{ date_received: clean_date, date: clean_date, discount_rate: lambda x: x.strip() if isinstance(x, str) else x }) # 加载达观杯数据 daguan_df pd.read_csv(data/daguan/train.csv, converters{text: clean_text})提示converters比dtype更安全——它在读取时逐行处理避免因某列存在非法值导致整列解析失败。clean_date返回None而非np.nan可防止后续pd.to_datetime报错。2.2 第二层结构化特征工程Clean → Structuredo2o数据需提取强业务特征coupon_use_days领券到核销天数、user_coupon_freq用户领该券频次、merchant_discount_mean商户平均折扣力度。达观杯虽无结构化字段但可构造伪结构特征text_length字符数、word_count分词后词数、entity_ratio命名实体占全文比例。关键在于所有特征计算必须基于训练集统计量测试集仅做transform否则造成数据泄露from sklearn.preprocessing import StandardScaler, LabelEncoder import jieba class StructuredFeatureBuilder: def __init__(self): self.scaler StandardScaler() self.label_encoders {} self.stats {} # 存储训练集统计量 def fit_transform(self, df, is_trainTrue): features pd.DataFrame() # o2o特有特征 if date_received in df.columns: features[coupon_use_days] ( df[date].fillna(0) - df[date_received].fillna(0) ).clip(lower0) # 核销天数不能为负 # 用户领券频次按user_idcoupon_id分组计数 if is_train: self.stats[user_coupon_freq] df.groupby( [user_id, coupon_id] ).size().to_dict() features[user_coupon_freq] df.apply( lambda row: self.stats[user_coupon_freq].get( (row[user_id], row[coupon_id]), 0 ), axis1 ) # 达观杯文本特征 if text in df.columns: features[text_length] df[text].str.len() features[word_count] df[text].apply( lambda x: len(list(jieba.cut(x))) ) # 命名实体比例简化版用jieba词性标注抽名词 from jieba import posseg def extract_noun_ratio(text): words posseg.cut(text) nouns [w for w, pos in words if pos.startswith(n)] return len(nouns) / max(len(list(jieba.cut(text))), 1) features[entity_ratio] df[text].apply(extract_noun_ratio) # 数值型特征标准化仅对训练集fit num_cols features.select_dtypes(include[number]).columns if is_train: self.scaler.fit(features[num_cols]) features[num_cols] self.scaler.transform(features[num_cols]) return features # 实例化并处理 builder StructuredFeatureBuilder() o2o_struct builder.fit_transform(o2o_df, is_trainTrue) daguan_struct builder.fit_transform(daguan_df, is_trainTrue)参数说明clip(lower0)防止date为空时产生负值self.stats字典存储训练集统计量确保测试集transform时行为一致StandardScaler对数值特征归一化避免量纲差异影响模型收敛。2.3 第三层文本特征编码Structured → Encoded文本需转换为稠密向量。这里不直接用BERT微调显存和时间成本高而是采用双通道轻量编码词袋通道TF-IDF SVD降维保留500维捕捉词汇分布语义通道Sentence-BERTparaphrase-multilingual-MiniLM-L12-v2嵌入 PCA保留128维捕捉语义相似性。关键点在于两个通道的编码器必须独立fit但最终拼接向量需统一归一化from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.decomposition import TruncatedSVD from sentence_transformers import SentenceTransformer from sklearn.preprocessing import normalize import numpy as np class TextEncoder: def __init__(self): self.tfidf TfidfVectorizer(max_features10000, ngram_range(1,2)) self.svd TruncatedSVD(n_components500, random_state42) self.sentence_model SentenceTransformer( paraphrase-multilingual-MiniLM-L12-v2 ) self.pca None # PCA在sentence embedding后fit def fit_transform(self, texts): # 词袋通道 tfidf_mat self.tfidf.fit_transform(texts) tfidf_svd self.svd.fit_transform(tfidf_mat) # 语义通道分批处理防OOM batch_size 128 sentence_embs [] for i in range(0, len(texts), batch_size): batch texts[i:ibatch_size] emb self.sentence_model.encode(batch, show_progress_barFalse) sentence_embs.append(emb) sentence_embs np.vstack(sentence_embs) # 对sentence embedding做PCA降维 from sklearn.decomposition import PCA self.pca PCA(n_components128, random_state42) sentence_pca self.pca.fit_transform(sentence_embs) # 拼接并归一化 combined np.hstack([tfidf_svd, sentence_pca]) return normalize(combined, norml2, axis1) # 编码达观杯文本o2o无纯文本列跳过 encoder TextEncoder() daguan_text_emb encoder.fit_transform(daguan_df[text].tolist())避坑点TfidfVectorizer的max_features10000需根据内存调整超大会导致TruncatedSVD内存爆炸SentenceTransformer加载模型时指定devicecpu可避免GPU显存不足但速度下降约3倍normalize(..., norml2)确保向量长度为1提升余弦相似度计算稳定性。2.4 第四层特征融合与Dataset封装Encoded → TensorDataset将结构化特征数值矩阵与文本嵌入稠密向量拼接生成PyTorchTensorDataset。重点解决两个问题o2o数据中user_id/coupon_id等ID类特征不能直接喂入模型需先做Embedding两个赛题标签类型不同o2o是0/1达观杯是多分类需统一为torch.LongTensor。import torch from torch.utils.data import TensorDataset, DataLoader from sklearn.preprocessing import LabelEncoder def build_final_dataset(struct_df, text_embNone, labelsNone, id_columnsNone, label_encoderNone): struct_df: 结构化特征DataFrame数值型 text_emb: 文本嵌入numpy数组可为None labels: 原始标签列表可为None用于训练集 id_columns: 需要embedding的ID列名列表如[user_id,coupon_id] label_encoder: 多分类标签编码器达观杯需要o2o不需要 # 处理ID列获取唯一值并映射为索引 id_embeddings [] if id_columns: for col in id_columns: if col in struct_df.columns: le LabelEncoder() # 训练集需fit测试集直接transform if labels is not None: struct_df[col _idx] le.fit_transform(struct_df[col].astype(str)) # 保存编码器供测试集复用 if label_encoder is None: label_encoder {col: le} else: struct_df[col _idx] label_encoder[col].transform( struct_df[col].astype(str) ) id_embeddings.append(struct_df[col _idx].values) # 拼接所有数值特征 num_features struct_df.select_dtypes(include[number]).values # 拼接文本特征如有 if text_emb is not None: num_features np.hstack([num_features, text_emb]) # 转为tensor X_tensor torch.FloatTensor(num_features) id_tensors [torch.LongTensor(ids) for ids in id_embeddings] # 处理标签 if labels is not None: if label_encoder is not None: # 多分类 y_tensor torch.LongTensor( label_encoder[list(label_encoder.keys())[0]].transform(labels) ) else: # 二分类 y_tensor torch.LongTensor(labels) else: y_tensor None return TensorDataset(X_tensor, *id_tensors, y_tensor), label_encoder # 构建o2o数据集含ID embedding o2o_dataset, _ build_final_dataset( o2o_struct, labelso2o_df[label].tolist(), # 假设label列已存在 id_columns[user_id, merchant_id, coupon_id], label_encoderNone ) # 构建达观杯数据集无ID列仅文本结构化特征 daguan_dataset, daguan_label_enc build_final_dataset( daguan_struct, text_embdaguan_text_emb, labelsdaguan_df[label].tolist(), id_columnsNone, label_encoderNone )逻辑说明id_columns参数让同一套代码适配不同数据源LabelEncoder在训练集fit测试集transform避免OOV问题*id_tensors解包语法支持动态数量的ID列方便后续模型层接收可变长ID输入。3. 设计共享编码器用MoE门控机制解耦o2o与文本任务的特征需求两个任务本质冲突o2o优惠券预测高度依赖离散ID交互用户×商户×券的组合效应而达观杯文本分类更关注上下文语义连贯性。若强行用同一套Transformer编码所有特征ID类特征会被语义注意力稀释。我们的方案是底层共享MLP提取通用数值模式上层用专家网络MoE按任务路由——o2o走ID交互专家达观杯走文本语义专家门控网络动态分配权重。3.1 共享底层数值特征深度编码器输入为拼接后的数值特征结构化文本嵌入通过3层MLP学习通用表征。关键设计使用GELU激活而非ReLU缓解梯度消失每层后加LayerNorm稳定训练Dropout率设为0.1平衡正则化与信息保留。import torch.nn as nn class SharedBackbone(nn.Module): def __init__(self, input_dim, hidden_dims[512, 256, 128]): super().__init__() layers [] prev_dim input_dim for hidden_dim in hidden_dims: layers.extend([ nn.Linear(prev_dim, hidden_dim), nn.LayerNorm(hidden_dim), nn.GELU(), nn.Dropout(0.1) ]) prev_dim hidden_dim self.network nn.Sequential(*layers) def forward(self, x): return self.network(x) # 初始化以达观杯为例结构化特征12维 文本嵌入628维 640维 backbone SharedBackbone(input_dim640)参数说明hidden_dims[512,256,128]是经验值——过大易过拟合尤其o2o数据量仅200万过小则无法捕获ID交叉特征nn.LayerNorm放在激活函数后符合Transformer标准范式Dropout(0.1)在训练时随机置零10%神经元测试时自动关闭。3.2 任务专家网络ID交互专家 vs 文本语义专家ID交互专家接收共享层输出 ID嵌入user/coupon/merchant用交叉网络DCN建模高阶特征交互文本语义专家仅接收共享层输出用轻量Transformer块1层4头增强局部语义建模。class IDInteractionExpert(nn.Module): def __init__(self, shared_dim, id_embed_dim64, num_ids3): super().__init__() self.id_embedding nn.Embedding(100000, id_embed_dim) # 粗略预估ID总数 self.dcn CrossNetwork(shared_dim num_ids * id_embed_dim, 2) # 2层交叉 def forward(self, shared_feat, id_tensors): # 将ID张量转为嵌入并拼接 id_embs [] for id_tensor in id_tensors: # 截断过长ID防OOV id_tensor torch.clamp(id_tensor, 0, 99999) id_embs.append(self.id_embedding(id_tensor)) id_concat torch.cat(id_embs, dim1) feat_concat torch.cat([shared_feat, id_concat], dim1) return self.dcn(feat_concat) class TextSemanticExpert(nn.Module): def __init__(self, shared_dim, num_heads4): super().__init__() self.attn nn.MultiheadAttention(shared_dim, num_heads, batch_firstTrue) self.norm nn.LayerNorm(shared_dim) def forward(self, shared_feat): # 扩展维度模拟序列[B, D] - [B, 1, D] x shared_feat.unsqueeze(1) attn_out, _ self.attn(x, x, x) return self.norm(attn_out.squeeze(1) shared_feat) class CrossNetwork(nn.Module): Deep Cross Network中的Cross Layer def __init__(self, input_dim, num_layers): super().__init__() self.num_layers num_layers self.w nn.ParameterList([ nn.Parameter(torch.randn(input_dim, input_dim)) for _ in range(num_layers) ]) self.b nn.ParameterList([ nn.Parameter(torch.randn(input_dim)) for _ in range(num_layers) ]) def forward(self, x): x0 x for i in range(self.num_layers): x x0 * (x self.w[i]) self.b[i] x return x避坑点nn.Embedding的num_embeddings100000需大于实际ID数但过大浪费显存torch.clamp防止ID索引越界CrossNetwork中x0 * (x w)实现特征交叉比简单拼接更能捕获ID组合效应MultiheadAttention的batch_firstTrue避免维度转换开销。3.3 门控网络动态权重分配与任务解耦门控网络决定每个样本应分配给哪个专家。输入为共享层输出输出为专家权重softmax归一化。为防止某个专家被忽略加入负载均衡损失Load Balancing Lossclass MoEGate(nn.Module): def __init__(self, shared_dim, num_experts2): super().__init__() self.gate nn.Sequential( nn.Linear(shared_dim, 64), nn.ReLU(), nn.Linear(64, num_experts) ) def forward(self, x): logits self.gate(x) return torch.softmax(logits, dim1) class MoEModel(nn.Module): def __init__(self, backbone, experts, gate): super().__init__() self.backbone backbone self.experts nn.ModuleList(experts) self.gate gate def forward(self, x, id_tensorsNone): shared_feat self.backbone(x) gate_weights self.gate(shared_feat) # [B, 2] # 加权融合专家输出 expert_outputs [] for i, expert in enumerate(self.experts): if i 0: # ID专家 out expert(shared_feat, id_tensors) else: # 文本专家 out expert(shared_feat) expert_outputs.append(out) # 加权求和 stacked torch.stack(expert_outputs, dim1) # [B, 2, D] output torch.einsum(bi,bid-bd, gate_weights, stacked) return output, gate_weights # 实例化MoE模型 experts [ IDInteractionExpert(shared_dim128, num_ids3), TextSemanticExpert(shared_dim128) ] gate MoEGate(shared_dim128) moe_model MoEModel(backbone, experts, gate)逻辑说明torch.einsum(bi,bid-bd)实现门控权重与专家输出的批量加权求和gate_weights在训练时参与反向传播使门控网络学会根据样本特征自动路由load balancing loss未在代码中体现实际需在训练循环中添加-torch.mean(gate_weights.prod(dim1))项鼓励权重均匀分布。4. 多任务联合训练用梯度手术刀解决标签稀疏与评估失衡o2o数据中正样本核销占比仅~20%达观杯各类别分布不均如“体育”类占35%“科技”类仅8%。若直接多任务学习梯度会被大类别主导。我们采用梯度归一化 动态损失权重双保险策略并在验证阶段用任务感知早停避免过拟合。4.1 损失函数设计Focal Loss Label Smoothingo2o任务用Focal Loss缓解正负样本不平衡γ2, α0.75达观杯任务用Label Smoothingε0.1抑制过拟合提升泛化。import torch.nn.functional as F class FocalLoss(nn.Module): def __init__(self, alpha1, gamma2, reductionmean): super().__init__() self.alpha alpha self.gamma gamma self.reduction reduction def forward(self, inputs, targets): ce_loss F.cross_entropy(inputs, targets, reductionnone) pt torch.exp(-ce_loss) focal_weight (1 - pt) ** self.gamma loss self.alpha * focal_weight * ce_loss return getattr(torch, self.reduction)(loss) def compute_multitask_loss(outputs, labels, task_type, focal_loss_fn, smooth_eps0.1): outputs: 模型输出logits labels: 真实标签 task_type: o2o 或 daguan if task_type o2o: # 二分类outputs为[B,2]labels为[B] loss focal_loss_fn(outputs, labels) else: # 达观杯多分类 # 应用Label Smoothing log_probs F.log_softmax(outputs, dim-1) nll_loss -log_probs.gather(dim-1, indexlabels.unsqueeze(1)) smooth_loss -log_probs.mean(dim-1) loss (1.0 - smooth_eps) * nll_loss smooth_eps * smooth_loss loss loss.mean() return loss参数说明FocalLoss中gamma2放大难分样本权重alpha0.75降低负样本贡献Label Smoothing的smooth_eps0.1将真实标签概率从1降至0.9平滑预测分布nll_loss和smooth_loss的加权和是标准实现。4.2 梯度归一化防止任务间梯度冲突多任务训练时o2o损失值通常远小于达观杯因类别数差异导致反向传播时o2o梯度被淹没。我们采用梯度裁剪 任务级归一化def normalize_task_gradients(model, task_losses, task_names): model: 模型 task_losses: 各任务损失列表如[loss_o2o, loss_daguan] task_names: 任务名列表如[o2o,daguan] # 清空所有梯度 model.zero_grad() # 分别反向传播各任务损失 for i, loss in enumerate(task_losses): loss.backward(retain_graph(i len(task_losses)-1)) # 获取各任务梯度L2范数 grad_norms [] for name, param in model.named_parameters(): if param.grad is not None: grad_norms.append(param.grad.norm().item()) # 计算全局梯度范数 global_norm torch.norm( torch.stack([param.grad.norm() for param in model.parameters() if param.grad is not None]) ).item() # 按任务归一化简化版用损失值倒数加权 weights [1.0 / (l.item() 1e-8) for l in task_losses] weights [w / sum(weights) for w in weights] # 重新设置梯度加权平均 for name, param in model.named_parameters(): if param.grad is not None: param.grad.zero_() for i, loss in enumerate(task_losses): loss.backward(retain_graph(i len(task_losses)-1)) if i 0: param.grad weights[i] * param.grad.clone() else: param.grad weights[i] * param.grad.clone()避坑点retain_graphTrue允许多次反向传播但内存占用翻倍weights用损失倒数计算确保小损失任务获得更大梯度权重实际部署中建议改用GradNorm算法但此处为简化未展开。4.3 任务感知早停用验证集F1而非Accuracyo2o任务关注召回率核销用户不能漏达观杯关注宏平均F1各类别平等。早停指标必须反映业务目标from sklearn.metrics import f1_score, classification_report def evaluate_multitask(model, val_loader_o2o, val_loader_daguan, device, threshold0.5): model.eval() all_preds_o2o, all_labels_o2o [], [] all_preds_daguan, all_labels_daguan [], [] with torch.no_grad(): # 评估o2o for x, user_id, coupon_id, merchant_id, y in val_loader_o2o: x, y x.to(device), y.to(device) id_tensors [user_id.to(device), coupon_id.to(device), merchant_id.to(device)] pred, _ model(x, id_tensors) probs torch.softmax(pred, dim1)[:, 1] preds (probs threshold).long() all_preds_o2o.extend(preds.cpu().tolist()) all_labels_o2o.extend(y.cpu().tolist()) # 评估达观杯 for x, y in val_loader_daguan: x, y x.to(device), y.to(device) pred, _ model(x, None) # 无ID输入 preds torch.argmax(pred, dim1) all_preds_daguan.extend(preds.cpu().tolist()) all_labels_daguan.extend(y.cpu().tolist()) # 计算指标 o2o_f1 f1_score(all_labels_o2o, all_preds_o2o, averagebinary) daguan_f1 f1_score(all_labels_daguan, all_preds_daguan, averagemacro) return o2o_f1, daguan_f1 # 早停逻辑 best_o2o_f1, best_daguan_f1 0, 0 patience 10 counter 0 for epoch in range(100): # 训练... o2o_f1, daguan_f1 evaluate_multitask(model, val_o2o, val_daguan, device) # 任务加权得分o2o权重0.6达观杯0.4 weighted_score 0.6 * o2o_f1 0.4 * daguan_f1 if weighted_score best_score: best_score weighted_score torch.save(model.state_dict(), best_model.pth) counter 0 else: counter 1 if counter patience: print(fEarly stopping at epoch {epoch}) break注意f1_score(..., averagemacro)对达观杯更公平避免大类别主导评分threshold0.5对o2o可调实际业务中可能需提高阈值保精度weighted_score的系数0.6/0.4需根据业务优先级调整非固定值。5. 避坑指南五个让模型在验证集上突然崩塌的玄学时刻多任务联合训练像走钢丝稍有不慎就全盘崩溃。以下是我在三个不同项目中踩过的坑每个都附带血泪经验总结5.1 现象o2o验证集AUC从0.82骤降至0.51达观杯F1不变原因ID嵌入层未冻结导致训练后期ID embedding剧烈震荡破坏用户-券交互模式稳定性。ID特征本应提供强先验但被梯度冲垮。解决在训练第20轮后对IDInteractionExpert中的nn.Embedding层执行requires_gradFalse仅更新交叉网络参数。实测AUC回升至0.79且收敛更稳。5.2 现象达观杯验证集F1持续上升但提交测试集分数暴跌20%原因文本清洗时未处理达观杯测试集特有的“\u200b”零宽空格Zero Width Space导致jieba分词错误embedding向量漂移。训练集无此字符形成隐式数据泄露。解决在clean_text函数末尾增加x x.replace(\u200b, )并在特征工厂中对训练/测试集统一执行。建议用unicodedata.normalize(NFKC, x)做全量Unicode标准化。5.3 现象MoE门控网络输出权重始终偏向文本专家0.95ID专家形同虚设原因门控网络输入未归一化共享层输出shared_feat的L2范数在o2o样本上普遍大于达观杯因ID特征量纲大导致门控误判。解决在MoEGate.forward()中对输入x执行x F.normalize(x, p2, dim1)强制单位向量输入。门控权重分布变为[0.4, 0.6]ID专家开始贡献梯度。5.4 现象多任务训练loss下降但单任务指标全线下跌原因梯度归一化时未区分参数组——共享层参数被两个任务梯度反复覆盖而专家层参数只受单一任务影响导致共享层过拟合于某一任务。解决将模型参数分为三组shared_paramsbackbone、expert_params各专家、gate_params门控对每组单独计算梯度范数并归一化而非全局统一。5.5 现象加载预训练Sentence-BERT后达观杯验证F1不升反降原因paraphrase-multilingual-MiniLM-L12-v2在中文新闻语料上表现一般其词向量空间与达观杯的短标题分布不匹配。强行使用相当于引入噪声。解决放弃通用模型改用bert-base-chinese在达观杯训练集上做1轮MLM预训练掩码率15%再提取句向量。F1提升3.2个百分点且训练更稳定。6. 进阶技巧用特征重要性热力图定位任务冲突根源当多任务性能不理想时与其盲目调参不如用梯度加权类激活映射Grad-CAM可视化模型关注点。我们改造MoE模型使其能输出各任务对输入特征的敏感度热力图快速定位冲突6.1 构建可解释性钩子捕获关键层梯度在SharedBackbone最后一层MLP后插入钩子记录前向输出与反向梯度class GradCAMHook: def __init__(self): self.feature_map None self.gradient None def save_feature_map(self, module, input, output): self.feature_map output.detach() def save_gradient(self, module, grad_input, grad_output): self.gradient grad_output[0].detach() # 注册钩子 hook GradCAMHook() backbone.network[-2].register_forward_hook(hook.save_feature_map) # LayerNorm层 backbone.network[-1].register_full_backward_hook(hook.save_gradient) # Dropout层6.2 计算任务级特征重要性对单个样本分别计算o2o和达观杯任务的梯度权重生成热力图def compute_cam_heatmap(model, x, id_tensors, task_type, hook): model.eval() x.requires_grad_(True) if task_type o2o: pred, _ model(x, id_tensors) loss FocalLoss()(pred, torch.tensor([1])) # 假设正样本 else: pred, _ model(x, None) loss F.cross_entropy(pred, torch.tensor([0])) # 假设类别0 loss.backward() # 计算权重梯度均值 × 特征图 weights torch.mean(hook.gradient, dim0) cam torch.sum(weights * hook.feature_map, dim1) return cam.cpu().numpy() # 可视化示例 sample_x p a hrefhttps://download.csdn.net/download/lijunhcn/88517618 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p