尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

多模态大语言模型实战:从架构演进到部署优化

多模态大语言模型实战:从架构演进到部署优化 简介多模态大语言模型MLLM近两年迅速成为人工智能领域的热点方向。这份PPT重点回应大语言模型无法处理视觉等多模态输入的局限系统梳理了MLLM的演进脉络适合AI研究者、算法工程师以及对多模态技术感兴趣的进阶学习者。资源为单个PPTX文件大小仅7.13MB内容涵盖背景介绍、经典架构拆解视觉编码器、连接器与大语言模型、训练方法模态对齐与指令微调、评测标准常规任务Benchmark与专门Benchmark及未来展望。其中特别介绍了基于CLIP预训练ViT的视觉编码器、连接器使用MLP或Q-Former的投影方式以及通过提高输入分辨率增强模型能力的两类思路。通过学习可快速建立MLLM核心知识框架掌握GPT-4V、Gemini-Pro等代表性模型的实现思路与开源探索。目前已有502人学习是一份兼顾广度与深度的多模态大语言模型入门与进阶资料。1. 多模态大语言模型领域进展分享从“能看会听”到“能操作”当你拿到“多模态大语言模型领域进展分享.pptx”这个标题真正要做的不是过一遍模型列表而是把过去一年里这个领域最重要的变化讲清楚。多模态大语言模型MLLM已经不只是“看图写话”而是把图像、视频、音频、界面截图统一压缩成token序列再交给自回归语言模型做推理和决策。对一线工程师来说感知层怎么接、微调时动哪一层、评估时如何辨别“看见”和“猜到”、部署时怎么压token成本这些问题比记住几个SOTA名字更重要。这篇分享会按照架构、微调、评估、部署四条线往下走帮助准备做技术选型或复现实验的团队少走弯路。2. 多模态大语言模型架构演进的三个关键点连接器、视觉编码器与统一分词2.1 连接器从线性投影、Q-Former到动态分辨率多模态大语言模型的骨架通常由一个视觉编码器、一个连接器和一个基座语言模型组成。连接器是视觉信号进入语言模型的咽喉决定了视觉信息以什么样的密度和格式被读入。最早一批工作LLaVA-1.0采用线性层把视觉特征映射到词向量维度训练简单但很快发现线性映射对空间位置和细节的保留不足LLaVA-1.5改用两层MLP后同等数据下的指标上涨接近3个点这之后MLP连接器成为开源项目默认配置。Q-Former路线走的是“压缩读出”用一组固定数量的query通过交叉注意力从视觉特征中抽取信息再把query输出映射到语言模型。它的优点是视觉token数量固定且少对推理显存友好缺点是训练难度更高query不知道该“看”哪里的问题经常出现。到2024年动态分辨率成为新的主线LLaVA-NeXT和Qwen2-VL都把输入图像分成若干高分辨率块每块独立编码后再与全局图一起进入连接器。这个改动对文档、表格、小目标检测是质变视觉token数量也从固定576变成几百到上千模型需要重新学习“局部块之间的位置关系”。选型的时候我一般会先画一个表格把不同连接器的代价摆出来。下面的对比可以作为内部技术评审的底稿连接器方案代表模型视觉token数量适合场景线性投影LLaVA-1.0固定256简单图像分类、caption两层MLPLLaVA-1.5固定576通用图文问答Q-Former / PerceiverBLIP-2、Flamingo固定32/64低显存场景跨模态检索像素分块动态分辨率LLaVA-NeXT、Qwen2-VL随分辨率增加文档解析、OCR、遥感图像连接器越复杂往往对训练数据的规模和分布越敏感。固定token数量的方案虽然损失细节但在数据量较小的业务里更容易训稳定动态分辨率方案则适合你手里已经积攒了高分辨率业务图片并且能接受推理耗时上升。2.2 视觉编码器要不要解冻CLIP、SigLIP与高分辨率适配视觉编码器决定模型“能看到什么”。CLIP类模型用图文对比学习对齐文本和视觉但224×224的输入分辨率让它在OCR和位置关系上偏弱。SigLIP使用sigmoid对比损失不再受batch内负样本数量限制在同样的CLIP主干下能拿到更好的细粒度特征。如果你是在复现多模态融合论文我一般建议先跑一遍CLIP-L/14作为基线因为它和所有常见连接器、微调框架都兼容排查问题的资料最多。高分辨率是视觉编码器要迈的一道坎。常用的做法有三种对原图做多尺度切块后分别过编码器在CLIP主干后插入小型高分辨率Adapter或者直接把编码器输入分辨率提升并配合位置编码插值。三者都会增加计算量但收益并不线性。实际调参时优先把分辨率的提升放在模型的中间层而不是第一层因为视觉主干的前几层负责边缘纹理对分辨率不敏感最后一两层的语义特征才需要看到更多像素。视觉编码器能不能解冻是微调中最容易被问错的问题。我观察到大量失败案例是因为同时解冻了视觉塔和连接器导致模型的语言能力被视觉梯度和文本梯度交替拉扯。底线做法是第一冻结视觉塔只训练连接器和LLM的LoRA第二如果OCR能力还是不够再解冻视觉塔最后2层或高分辨率Adapter学习率设为LLM的十分之一。这个规则在LLaVA、Qwen2-VL和InternVL上都适用也便于在实验记录里横向对照。2.3 统一分词图像、视频、音频都变成token序列统一分词是“多模态大语言模型”和早期“视觉问答模型”的本质区别。图像经过视觉编码器和连接器变成视觉token文本经过分词器变成文本token它们被拼进同一个序列共享同一套因果注意力和输出层。这样模型在生成下一个token时能同时看到前文文字和图片局部信息跨模态推理变成了一种“序列预测”。视频和音频入场后token序列变得更长。视频按时间轴抽帧每帧产生一组视觉token再叠加帧索引的时间位置编码音频则先做log-mel谱图或预训练编码器输出再把特征压成token。目前工程上更稳的视频方案仍然是“抽帧图像塔”音频能做到“听声识别环境、理解语气”的模型也越来越多但很多还是套用图像塔。多模态时序数据融合方法里真正被验证有效的不是把每一帧全部塞进去而是先由外部模块检测关键帧再让大模型处理压缩后的关键帧序列。统一分词给位置编码扩展提出了新要求。原生长度为4K的LLM突然输入2000个视觉token和一段长文本位置编码必须往外推。常见做法有线性插值、NTK-Aware缩放、YaRN。我简单对比过NTK-Aware在短序列上损失最小但是对超长文本的扩展不够YaRN能扩展到更长场景但需要微调适应。多模态场景推荐先做NTK-Aware然后单独在长视觉序列数据上做少量LoRA纠正不要让统一分词直接暴露原始RoPE的位置盲区。2.4 用Transformers在本地跑通最小推理代码下面的代码用Qwen2-VL系列模型做一次本地推理验证视觉编码器、连接器和语言模型整体链路是否正常。这也是做模型选型的第一步。import torch from PIL import Image from transformers import Qwen2VLForConditionalGeneration, Qwen2VLProcessor model_id Qwen/Qwen2-VL-7B-Instruct processor Qwen2VLProcessor.from_pretrained(model_id) model Qwen2VLForConditionalGeneration.from_pretrained( model_id, torch_dtypetorch.bfloat16, device_mapauto ) image Image.open(./table.png).convert(RGB) messages [ {role: user, content: [ {type: image}, {type: text, text: 请说出这张表格第二行第三列的数字}, ]} ] prompt processor.apply_chat_template( messages, tokenizeFalse, add_generation_promptTrue ) inputs processor( textprompt, images[image], return_tensorspt ).to(model.device) with torch.no_grad(): output model.generate( **inputs, max_new_tokens512, do_sampleFalse, temperature0.0, top_pNone, ) answer processor.decode( output[0][inputs.input_ids.shape[1]:], skip_special_tokensTrue ) print(answer)代码说明processor.apply_chat_template会把多模态消息转成包含特殊token的文本image占位符随后被processor替换成图像特征对应的输入。max_new_tokens512给长表格答案留出余地但如果问题简单可以降到128降低生成时间。do_sampleFalse和temperature0.0是文档抽取类任务的推荐配置因为采样会给OCR结果带来随机性。注意这里的输入图片不要包含EXIF旋转信息很多图像库会忽略旋转参数导致图像内容实际是倒着的。如果显存不够把torch_dtypetorch.bfloat16换成4bit加载需要额外传入load_in_4bitTrue并安装bitandbytes。同时要留意transformers版本Qwen2-VL这类新模型要求较新的依赖如果遇到Positional Embedding相关报错先升级transformers再排查不要直接去改模型代码。3. 多模态大语言模型的微调与对齐数据、LoRA与偏好优化3.1 微调数据怎么攒多模态指令数据、数据集下载与负样本微调数据是决定模型“听不听话”的第一因素。目前能直接下载的开源多模态指令数据主要来自HuggingFace和ModelScope常见的有LLaVA生成的LLaVA-Instruct-150K/665K以及面向图表推理的WikiTableQuestions转换集。这些数据规模大约从几万到几十万条条目格式通常是“图片路径多轮对话”。如果是做中文业务只靠英文数据不够通常还要加入表格OCR、票据、网页截图等合成数据一起混合。多模态数据集下载回来不能直接进训练。第一步做图片去重很多在线数据集里同一张图片会出现几十次重复的视觉样本会让LoRA过拟合到特定画面。第二步统一分辨率连接器通常期望图片长边是某个像素级倍数太低或太高的图片都会触发不同分支的预处理造成视觉token分布不一致。第三步检查文本质量看图回答里大量“无法回答”“图片模糊”这类答案会让模型学会偷懒拒答。负样本经常被忽略。要让模型知道“图片里没有的东西不要说有”数据集里必须出现这样的样本图片中没有cat但问题问“图片里是否有cat”答案是no。你可以从正样本中随机换图构造但要注意新的图片里不能刚好包含同类别物体。负样本比例建议控制在总样本的10%到20%太高会让模型过度保守。数据集类型常用来源建议用途注意事项通用指令数据LLaVA-Instruct通用图文问答以英文为主需补充中文文档OCR数据开源合成数据集表格、票据、截图分辨率要贴近业务偏好数据对自构建DPO训练需人工或强模型标注负样本数据自构建幻觉治理控制比例在10%-20%3.2 多模态微调的最小微调单位连接器、LoRA与视觉塔的取舍“多模态微调最小微调单位”是最近经常出现在论文和复盘里的提法。它不是指一个rank而是指一组必须同步更新的模块。以LLaVA类模型为例最少需要更新的组合是“连接器语言模型的LoRA”。如果只更新连接器模型能感知图像但语义表达能力仍停留在基座模型水平复杂指令容易答不到点上如果只更新LoRA连接器产出的特征和词向量空间不匹配loss下降快但验证指标一直上不去。LoRA rank的选择要看视觉token数量和任务复杂度。图像token少、分类任务rank在16到32之间就够文档解析、小目标检测这类需要高频视觉细节的任务rank可以到64lora_alpha设为rank的2倍左右。学习率方面全参微调常用2e-5LoRA微调可以从1e-4起步但要注意和连接器的学习率解耦。我一般用AdamW和余弦学习率连接器的学习率即使和LoRA相同也会给它更早的warm-up让视觉特征先稳定下来再更新LoRA。Q-LoRA适合显存受限的本地开发机。它把基座模型量化到4bit在量化后的残差上插LoRA7B模型微调的峰值显存大约是12GB到16GB。要注意的是量化后的模型在反向传播中会引入额外噪声因此learning_rate要比正常LoRA降低20%到30%否则loss容易震荡。另一种常见误用是同时加载FlashAttention和4bit量化部分依赖组合会导致注意力掩码错位表现为图片没输入但模型能答出问题。3.3 多模态偏好优化从RLHF到DPO微调阶段只能让模型学会形式不能从两个答案里选更优的那个。传统RLHF需要多卡同时跑策略模型、参考模型和奖励模型普通团队很难稳定。DPO则通过构建偏好对直接计算策略模型在chosen和rejected上的log概率差作为损失省掉了奖励模型和强化学习环境这让多模态偏好优化变成一个普通二分类训练。构造多模态偏好对时输入包含图片、问题和两个回答。两个回答必须是同一目标分布下的候选最常见的是先让当前模型用不同temperature采样多次再用强模型打分排序。得分时要注意不能只看文本流畅度还要看图是否被真正引用。更好的做法是把强模型的打分拆成“图文一致性”和“指令遵循”两个维度分别加权。DPO的β参数默认0.1但多模态任务建议从0.05开始试。β越小模型对偏好对越不敏感适合数据量少、噪声多的场景β越大模型被偏好对锁定得越紧容易产生“套话式回答”。训练中除了DPO loss通常还会保留10%到20%的纯SFT数据防止偏好数据把模型带偏。3.4 用PEFT LoRA微调一个多模态模型的最小脚本下面以LLaVA-HF模型为例给出一个可放到单卡上跑的LoRA微调骨架。代码里保留数据预处理、LoRA配置和Trainer三部分labels的mask处理按常见做法实现。import torch from PIL import Image from datasets import load_dataset from transformers import ( LlavaForConditionalGeneration, LlavaProcessor, TrainingArguments, Trainer ) from peft import LoraConfig, get_peft_model model_id llava-hf/llava-1.5-7b-hf processor LlavaProcessor.from_pretrained(model_id) model LlavaForConditionalGeneration.from_pretrained( model_id, torch_dtypetorch.bfloat16, device_mapauto, ) lora_config LoraConfig( r32, lora_alpha64, target_modules[q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj], lora_dropout0.05, biasnone, task_typeCAUSAL_LM, ) model get_peft_model(model, lora_config) def preprocess(sample): image Image.open(sample[image]).convert(RGB) user_text fUSER: image\n{sample[question]}\nASSISTANT: answer sample[answer] processor.tokenizer.eos_token user_enc processor(textuser_text, imagesimage, return_tensorspt) answer_ids processor.tokenizer( answer, add_special_tokensFalse, return_tensorspt )[input_ids] input_ids torch.cat([user_enc[input_ids][0], answer_ids[0]]) labels torch.cat([ torch.full_like(user_enc[input_ids][0], -100), answer_ids[0], ]) return { input_ids: input_ids, labels: labels, attention_mask: torch.ones_like(input_ids), pixel_values: user_enc[pixel_values][0], } def collate_fn(batch): input_ids torch.nn.utils.rnn.pad_sequence( [x[input_ids] for x in batch], batch_firstTrue, padding_valueprocessor.tokenizer.pad_token_id, ) labels torch.nn.utils.rnn.pad_sequence( [x[labels] for x in batch], batch_firstTrue, padding_value-100, ) pixel_values torch.stack([x[pixel_values] for x in batch]) attention_mask (input_ids ! processor.tokenizer.pad_token_id).long() return { input_ids: input_ids, labels: labels, attention_mask: attention_mask, pixel_values: pixel_values, } dataset load_dataset(json, data_filestrain.jsonl)[train] dataset dataset.map(preprocess, remove_columnsdataset.column_names) args TrainingArguments( output_dir./mm-lora-run, per_device_train_batch_size1, gradient_accumulation_steps8, learning_rate1e-4, lr_scheduler_typecosine, warmup_ratio0.03, num_train_epochs1, bf16True, logging_steps20, save_strategysteps, save_steps200, ) trainer Trainer( modelmodel, argsargs, train_datasetdataset, data_collatorcollate_fn, ) trainer.train()代码逻辑先用processor把“用户问题图片占位符”编码为输入序列再把答案编码后拼在序列后面。labels把用户问题对应的token全部置为-100只让模型学习预测assistant部分pixel_values只与图片占位符对应顺序不能错乱。参数说明target_modules中包含了LLM的所有attention和MLP投影适合第一版实验r32对7B模型是居中设置后续可以做rank 16和64的对比。per_device_train_batch_size1是因为单张图片的视觉token很多梯度累积8步等效batch size为8显存压力不大。learning_rate1e-4是LoRA常见的起点如果loss震荡就降到5e-5同时把warmup_ratio提高到0.1。训练数据文件train.jsonl每个样例需要包含image、question、answer三个字段图片路径建议写绝对路径。4. 多模态大语言模型的评估与“幻觉”治理从POPE到视觉锚定4.1 评估基准选型MMMU、MathVista、GQA与POPE多模态大语言模型的评估容易被人忽略。MMMU看起来全面但很多题目通过文本背景知识就能蒙对模型并没有真正“看见”图片。MathVista能用手写数字、几何图形这些细节暴露视觉编码器的短板但它计入分数的题往往依赖OCR解析。GQA侧重场景图中的属性与关系适合评估空间位置。POPE只问“图片里是否存在某物体”是评估物体幻觉的轻量探针。基准测评内容常见误用MMMU多学科大学知识视觉理解将平均分当作视觉能力MathVista数学图表、几何、计数忽略OCR下界的干扰GQA场景图属性和关系类别不均衡导致虚高POPE物体存在性幻觉负样本与正样本分布不均实际业务评估我会把四个基准的结果和内部业务测试集的结果放在一起对比。如果模型在MMMU上分数高但在内部截图问答上很差多半是连接器分辨率不够而不是语言模型能力问题。如果POPE负样本的yes率超过15%幻觉治理就要提上日程。评估跑完之后还应该输出一个“按问题类型拆分”的得分而不是只汇报总分。4.2 幻觉的来源文本先验、视觉压缩与解码策略多模态幻觉本质上是语言模型在做“没有视觉证据的合理猜测”。模型在训练语料里见过大量“键盘附近有鼠标”的搭配因此当图片里只有键盘时仍可能在回答中提到鼠标。这种语言先验在复杂视觉场景里会被放大因为模型发现文本路径更容易预测。降低温度或关闭采样能减少随机性但并不能降低先验猜测只是让模型更“自信”地重复高频共现。视觉压缩是另一个根源。动态分辨率目前已能保留局部细节但连接器在压缩过程中仍会丢掉颜色、空间比例等低层信息。视频模型中的关键帧抽取机制不适用静态图片所以一张密集文档里的表格边框、空单元格经常被当成噪声压掉。POPE检测不到这种细粒度错误因为它的负样本是“物体类别”不是“表格结构”。解码策略也在其中起作用。当生成token的概率分布很平坦时模型会随temperature升高而随机挑选词汇容易产生幻觉分布很尖锐时又容易复制训练样本中的常见表述。理想做法是温度随视觉注意力变化当视觉注意力集中时温度略高视觉注意力涣散时温度降到接近0。不过目前这块更多停留在论文里工程上很少做动态温度除非你已经在边缘设备上有固定负载。4.3 用POPE检测幻觉一个可直接跑的脚本POPE的核心是构建一组“存在性提问”统计模型在正负样本上的回答分布。下面的脚本把真实物体和反事实物体分别测试输出准确率和幻觉率from PIL import Image from transformers import pipeline pipe pipeline( image-to-text, modelllava-hf/llava-1.5-7b-hf, device0 ) targets [cat, remote control, sofa] negatives [elephant, toothbrush, oven] def ask(obj): prompt fUSER: image\nIs there a {obj} in the image? Answer yes or no.\nASSISTANT: out pipe(Image.open(test.png), promptprompt, max_new_tokens8) answer out[0][generated_text][len(prompt):].strip().lower() return answer.startswith(yes) yes_real sum(ask(t) for t in targets) / len(targets) yes_fake sum(ask(n) for n in negatives) / len(negatives) print(freal recall: {yes_real:.2f}, hallucination rate: {yes_fake:.2f})代码先用pipeline加载LLaVA模型然后对图片上的三类物体和目标外物体分别提问。max_new_tokens8只留出“yes”或“no”的空间过长输出会影响统计。切片[len(prompt):]只取新生成的文本避免问题文本里的“yes”干扰判断。负样本类别不要与真实物体语义太近否则模型很可能因为它们常和关键词共现而误答。参数调整上真实物体列表要来自图片标注如果没有标注可以先让模型描述图片再用描述中的名词去问。幻觉率超过15%说明模型对“看不见”的物体缺乏拒答能力。该脚本也可以在多张图片上跑后汇总但每张图片的负样本集合要变化不要用同一组负样本否则模型可能记住这组词。4.4 视觉锚定与解码校正识别“空想”并抑制视觉锚定的思想是“生成必须绑定视觉证据”。实现上有一个低成本的近似在模型生成时拿到各层对视觉token的注意力权重如果当前生成token对视觉token的平均注意力非常低就说明模型在依赖语言先验。工程上可以在generate中设置output_attentionsTrue但对长序列会消耗额外显存。更实用的做法是在服务端做一个二次校验把模型输出的实体列表与图片中的物体标签召回结果做交集交集之外的实体标记为低置信度。解码校正如果要在生成过程中做可以用视觉对比解码同时让模型看原图和模糊图将模糊图的logit从原图的logit中减去保留那些确实由原图带来的视觉信号。对OCR和颜色类问题有效对纯文本常识问题会过修正。所以需要先判断问题类型只在涉及视觉细节的请求中开启。最后是数据策略在微调数据里加入“图片信息不足”的样本让模型学会回答“图片中没有足够证据”。这类拒答训练数据不需要很多1000到2000条就能形成稳定的行为模式但需要保证问题问的对象在常见类别里避免模型对所有不确定问题都拒答。5. 多模态大语言模型的部署与Agent化成本控制关键在token5.1 用vLLM启动多模态服务三个必调参数本地部署大语言模型跑多模态任务常见选择是vLLM。它把视觉编码器和语言模型统一调度支持连续批处理吞吐比transformers原生推理高一截。一个可用的服务启动命令如下python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2-VL-7B-Instruct \ --dtype bfloat16 \ --max-model-len 8192 \ --limit-mm-per-prompt image3 \ --enforce-eager--max-model-len 8192要覆盖图片视觉token和回答token的总长设置过小会导致“图片超出上下文长度”报错--limit-mm-per-prompt image3限制单次请求最多带3张图避免一张请求把整个batch卡死--enforce-eager关闭CUDA Graph多模态动态shape下更稳定。服务起来后用OpenAI客户端传image_url即可。5.2 多模态Agent的观察、规划与工具调用多模态Agent把视觉理解变成了“观察-规划-行动”闭环。常用做法是让模型每步输出一个结构化JSON例如点击坐标、OCR区域或检索query。系统提示里只给动作schema不给示例代码示例代码会导致模型输出多余解释。生成时关闭采样并设置max_tokens256因为Agent的一步动作往往很短长输出反而会延迟环境反馈。在Agent里图片token会被反复输入多次多轮操作会产生重复计算。可以将同一张截图的pixel_values缓存起来不同轮次只更新文本部分减少视觉塔重复前向。这个缓存在多模态Agent的迭代中能省下大约30%到40%的prefill时间。5.3 一个具体技巧用注意力分数裁掉低价值视觉token视觉token是成本和显存的主要开销。高分辨率图片进来哪怕不生成内容prefill阶段也要处理上千个token。我常用的一招是裁剪低注意力视觉token。思路是先让模型完整过一遍图片只统计各层平均注意力在视觉token上的分布保留注意力得分最高的前70%位置把其他visual token从输入序列中剪掉再做第二轮生成。这个技巧适合网页截图、扫描件这类信息冗余高的场景。import torch def prune_by_attention(attn_scores, visual_positions, keep_ratio0.7): # attn_scores: [layers, heads, query_len, key_len] avg attn_scores.mean(dim(0, 1)) # 对层和头取平均 visual_attn avg[:, visual_positions].mean(dim0) # 视觉token的最终注意力 keep_count max(1, int(len(visual_positions) * keep_ratio)) keep_idx visual_attn.topk(keep_count).indices keep_pos sorted([visual_positions[i] for i in keep_idx]) return keep_pos这个函数返回保留下来的视觉token位置之后按位置重新构造input_ids和pixel_values并替换image占位符的编码。裁剪比例从30%起调不要一上来就砍掉一半否则文字密集区域会先受害。文本部分永远不裁剪只裁视觉token避免破坏Agent上下文的连续性。本文还有配套的精品资源点击获取
返回列表