多模态题目理解的架构设计:让 AI 同时“看“文字和“读“图表

📅 2026/7/16 1:11:01
多模态题目理解的架构设计:让 AI 同时“看“文字和“读“图表
多模态题目理解的架构设计让 AI 同时看文字和读图表一、一道带图的算法题难倒了纯文本模型有个 LeetCode 题要求实现一个迷宫寻路算法题干包含了 9x9 的 ASCII 网格和一张示意图。当我把题目原文丢给 CodeLlama 时它生成的题解只处理了文字描述完全忽略了图中的关键信息——起点位置和边界限制。这是因为纯文本模型天生缺少对视觉信息的感知能力。而在真实的刷题场景中约 20% 的题目包含图表、公式渲染、数据结构示意图等非文本信息。如果题解生成系统只能理解纯文本这 20% 的题目就会被降级处理。多模态题目理解要解决的核心问题如何将文本描述 题目截图 公式图片融合到统一的语义表示中让模型既读得懂文字又看得懂图。flowchart TB A[用户提交题目] -- B{输入类型判断} B --|纯文本描述| C[Text Encoder] B --|含图片/公式| D[Vision Encoder] B --|含代码片段| E[Code Encoder] C -- F[多模态融合层] D -- F E -- F F -- G[跨模态 Attention] G -- H[统一语义表示] H -- I[Decoder题解生成] I -- J[输出思路分析 代码 复杂度] subgraph 对齐机制 K[图文对齐损失] -- F L[对比学习] -- F end style F fill:#ccf style G fill:#ccf二、多模态融合的三种架构范式当前主流的多模态大模型架构可以分为三类各有优劣2.1 串行注入Q-Former 桥接用一个轻量级的 Q-FormerQuery Transformer将视觉编码器的输出翻译成文本编码器能理解的 token 序列。代表BLIP-2、InstructBLIP。优点可以冻结文本 LLM训练成本低视觉 token 数可控64 个 query token 即可缺点Q-Former 是信息瓶颈可能丢失细节如迷宫网格的精确坐标2.2 早期融合全量 Attention将图文 token 拼接后一起送进 Transformer。代表Flamingo、LLaVA。优点图文交互最充分信息无损缺点视觉 token 数不可控224×224 图像可能产生 256 个 token显存消耗大2.3 解耦编码 跨模态对齐视觉编码器和文本编码器独立通过对比学习CLIP 风格将图文映射到同一语义空间。代表CLIP、SigLIP。优点训练和推理解耦灵活性强缺点对齐精度取决于训练数据质量对于题目理解场景早期融合最为合适——题目中的图表与文字紧密相关图中标注的变量名需要和文字中的变量对应需要最充分的跨模态交互。三、多模态题目理解系统的工程实现以下代码展示了一个简化的多模态题目理解系统包含Vision EncoderViT、Text Encoder、跨模态融合层和题目分析器。 多模态题目理解系统 输入题目截图 文字描述 代码片段 输出统一的题目语义表示 → 后续用于题解生成 import torch import torch.nn as nn import torch.nn.functional as F from typing import List, Dict, Optional, Tuple from dataclasses import dataclass dataclass class ProblemInput: 一道题目的多模态输入 text_description: str # 文字描述 screenshot_features: Optional[torch.Tensor] None # 截图特征 [1, N, D] code_template: Optional[str] None # 代码模板含函数签名 has_image: bool False # 是否包含图片 class ViTEncoder(nn.Module): Vision Transformer 编码器简化版 def __init__(self, img_size: int 224, patch_size: int 16, hidden_dim: int 768, num_layers: int 4): super().__init__() self.patch_size patch_size self.num_patches (img_size // patch_size) ** 2 # 196 patches # Patch Embedding将 16×16 的小块映射为向量 self.patch_embed nn.Linear( patch_size * patch_size * 3, hidden_dim ) # 位置编码可学习 self.pos_embed nn.Parameter( torch.randn(1, self.num_patches 1, hidden_dim) ) # CLS token self.cls_token nn.Parameter(torch.randn(1, 1, hidden_dim)) # Transformer 层 self.transformer nn.TransformerEncoder( nn.TransformerEncoderLayer( d_modelhidden_dim, nhead12, batch_firstTrue ), num_layersnum_layers, ) def forward(self, image: torch.Tensor) - torch.Tensor: Args: image: [batch, 3, 224, 224] RGB 图像 Returns: visual_features: [batch, num_patches1, hidden_dim] batch, c, h, w image.shape # 分块将 224×224 拆成 14×14196 个 16×16 的 patch patches image.unfold(2, self.patch_size, self.patch_size) \ .unfold(3, self.patch_size, self.patch_size) patches patches.permute(0, 2, 3, 1, 4, 5).contiguous() patches patches.view(batch, self.num_patches, -1) # [batch, 196, 768] patch_features self.patch_embed(patches) # 添加 CLS token cls_tokens self.cls_token.expand(batch, -1, -1) tokens torch.cat([cls_tokens, patch_features], dim1) # 添加位置编码 tokens tokens self.pos_embed # Transformer 编码 visual_features self.transformer(tokens) return visual_features # [batch, 197, 768] class TextEncoder(nn.Module): 文本编码器简化版 Transformer def __init__(self, vocab_size: int 50000, hidden_dim: int 768, max_len: int 512, num_layers: int 4): super().__init__() self.token_embed nn.Embedding(vocab_size, hidden_dim) self.pos_embed nn.Parameter( torch.randn(1, max_len, hidden_dim) ) self.transformer nn.TransformerEncoder( nn.TransformerEncoderLayer( d_modelhidden_dim, nhead12, batch_firstTrue ), num_layersnum_layers, ) self.max_len max_len def forward(self, input_ids: torch.Tensor) - torch.Tensor: Args: input_ids: [batch, seq_len] token 序列 Returns: text_features: [batch, seq_len, hidden_dim] batch, seq_len input_ids.shape seq_len min(seq_len, self.max_len) # token embedding tokens self.token_embed(input_ids[:, :seq_len]) # 位置编码 tokens tokens self.pos_embed[:, :seq_len, :] # Transformer 编码 return self.transformer(tokens) class CrossModalFusion(nn.Module): 跨模态融合层 核心让文本 token 和视觉 token 做交叉 Attention def __init__(self, hidden_dim: int 768): super().__init__() # 文本 → 视觉 的交叉 Attention self.text_to_visual nn.MultiheadAttention( embed_dimhidden_dim, num_heads12, batch_firstTrue, ) # 视觉 → 文本 的交叉 Attention self.visual_to_text nn.MultiheadAttention( embed_dimhidden_dim, num_heads12, batch_firstTrue, ) # 融合后的投影层 self.fusion_proj nn.Linear(hidden_dim * 2, hidden_dim) self.layer_norm nn.LayerNorm(hidden_dim) def forward(self, text_features: torch.Tensor, visual_features: torch.Tensor) - torch.Tensor: Args: text_features: [batch, T, D] 文本特征 visual_features: [batch, V, D] 视觉特征 Returns: fused_features: [batch, T, D] 融合后的特征 # 文本 query 去 attend 视觉 key/value文本看懂图片 text_attended, _ self.text_to_visual( querytext_features, keyvisual_features, valuevisual_features, ) # 视觉 query 去 attend 文本 key/value图片理解上下文 visual_attended, _ self.visual_to_text( queryvisual_features, keytext_features, valuetext_features, ) # 融合以文本为主融入视觉信息 fused torch.cat([text_features, text_attended], dim-1) fused self.fusion_proj(fused) fused self.layer_norm(fused text_features) # 残差连接 return fused class MultimodalProblemAnalyzer(nn.Module): 多模态题目分析器完整流水线 输入题目截图 文字描述 输出统一的题目语义表示 def __init__(self, hidden_dim: int 768): super().__init__() self.vision_encoder ViTEncoder(hidden_dimhidden_dim) self.text_encoder TextEncoder(hidden_dimhidden_dim) self.fusion CrossModalFusion(hidden_dimhidden_dim) # 题目分析头 self.analyzer_head nn.Sequential( nn.Linear(hidden_dim, hidden_dim), nn.GELU(), nn.Linear(hidden_dim, 3), # 输出难度、类型、标签概率 ) def forward(self, input_ids: torch.Tensor, image: Optional[torch.Tensor] None ) - Tuple[torch.Tensor, torch.Tensor]: Args: input_ids: [batch, seq_len] 文字描述 token 序列 image: [batch, 3, 224, 224] 可选截图 Returns: fused_features: 融合后的特征 analysis: 题目分析结果难度/类型/标签 # 文本编码 text_features self.text_encoder(input_ids) # [B, T, D] if image is not None: # 视觉编码 visual_features self.vision_encoder(image) # [B, V, D] # 跨模态融合 fused_features self.fusion(text_features, visual_features) else: # 无图片时直接使用文本特征 fused_features text_features # 取 CLS token或平均池化作为全局表示 global_feature fused_features.mean(dim1) # [B, D] analysis self.analyzer_head(global_feature) return fused_features, analysis # 数据准备与训练辅助 class ProblemPairingDataset: 图文配对数据集 用于训练跨模态对齐 def __init__(self, pairs: List[Tuple[str, str, Optional[int]]]): Args: pairs: [(text_description, image_path, label), ...] self.pairs pairs def get_positive_pairs(self) - List[Tuple[str, str]]: 获取正样本文字匹配图片 return [(text, img) for text, img, _ in self.pairs] def get_negative_pairs(self) - List[Tuple[str, str]]: 获取负样本文字不匹配图片用于对比学习 neg_pairs [] n len(self.pairs) # 简单策略每段文字随机配对一张不匹配的图 for i in range(n): j (i 1) % n # 用下一个样本的图片作为负样本 neg_pairs.append((self.pairs[i][0], self.pairs[j][1])) return neg_pairs def compute_contrastive_loss(self, text_encoder, vision_encoder, batch_text, batch_image) - torch.Tensor: 计算对比学习损失InfoNCE Loss 让匹配的图文对在语义空间中更接近 # 编码文本和图片 text_feat text_encoder(batch_text) # [B, D] img_feat vision_encoder(batch_image) # [B, D] # 计算相似度矩阵 similarity torch.matmul( F.normalize(text_feat, dim-1), F.normalize(img_feat, dim-1).transpose(0, 1) ) # [B, B] # 对角线是正样本对其余是负样本 labels torch.arange(similarity.size(0)).to(similarity.device) # 使用交叉熵损失softmax NLL loss_t2i F.cross_entropy(similarity, labels) # 文本→图片 loss_i2t F.cross_entropy(similarity.t(), labels) # 图片→文本 return (loss_t2i loss_i2t) / 2 # 使用示例 def demo_multimodal_analysis(): 演示多模态题目分析 model MultimodalProblemAnalyzer(hidden_dim768) # 模拟输入 batch 2 # 模拟 token 序列实际使用 tokenizer 编码的 ID input_ids torch.randint(0, 50000, (batch, 128)) # 模拟题目截图随机生成实际是用户上传的截图 image torch.randn(batch, 3, 224, 224) # 多模态推理 fused_features, analysis model(input_ids, image) print(f融合特征形状: {fused_features.shape}) print(f分析输出: {analysis.shape}) # 纯文本推理对比 fused_text_only, _ model(input_ids, imageNone) # 计算文本和多模态特征的余弦相似度 sim F.cosine_similarity( fused_features.mean(dim[1, 2]), fused_text_only.mean(dim[1, 2]), dim0 ) print(f多模态 vs 纯文本 特征差异度: {1 - sim.item():.4f}) if __name__ __main__: demo_multimodal_analysis()四、多模态题目理解的挑战与边界图像质量和OCR噪声用户上传的截图可能是手机拍照倾斜、光照不均OCR 识别错误会导致文本特征和视觉特征的不一致。解决方案先用图像增强去倾斜、二值化预处理再送入视觉编码器。训练数据的匮乏带图文对齐标注的算法题目数据集几乎不存在。工程上只能用题目文字 从题库截图的图表做弱监督训练效果有限。推理延迟的代价ViT 编码一张 224×224 图片约需 10msA100加上跨模态融合后总开销约 15ms。对于需要 2 秒内返回的题解生成来说可接受但如果批量处理 100 张图需要做推理批处理优化。长文本 图片的上下文限制题目描述可能超过 2000 字加上图片的 196 个视觉 token总 token 数接近 Transformer 的上下文窗口上限通常 2048-4096。需要做文本截断或压缩。五、总结多模态题目理解是刷题系统从好用到智能的关键跃迁20% 的题目包含图表纯文本模型天然无法处理——这是必须支持多模态的业务驱动力。跨模态融合的三种架构各有场景题目理解适合早期融合图文紧密相关而知识检索适合解耦编码图文可独立查询。对比学习是图文对齐的基础InfoNCE Loss 让匹配的图文在高维空间中靠近不匹配的远离。工程上要权衡精度和延迟Vision Encoder 的推理开销可以接受但批量处理时需优化。作为实习生这个项目的最大收获是光读论文不够真正把 ViT Text Encoder 对接起来处理一个真实用户上传的模糊截图才发现理论和工程的差距有多大。本文从多模态模型的三类架构出发实现了一个包含 ViT 编码器、文本编码器、跨模态融合层的题目分析系统。代码中的对比学习损失函数可直接用于图文对齐训练跨模态 Attention 机制是理解图文怎么融合的核心。