一、概述
1、是什么
是一款面向终端设备的多模态大型语言模型(MLLM),论文暂未发布
,它专注于实现在手机等资源受限设备上的高级AI功能,参数8B(qwen2 7B +
SigLIP ViT-400m/14 + 视觉标记压缩层
)。该模型能够处理包括文本、图像在内的多种数据类型,具备图片描述、单图文问答、代码编写和debug、
多图问对话、视频理解对话
、json格式、高清OCR解析(函数调用论文暂时未提)。
2、亮点
🔥 领先的性能。 在最新版本 OpenCompass 榜单上(综合 8 个主流多模态评测基准)平均得分 65.2,以8B量级的大小在单图理解方面超越了 GPT-4o mini、GPT-4V、Gemini 1.5 Pro 和 Claude 3.5 Sonnet 等主流商用闭源多模态大模型。
🖼️ 多图理解和上下文学习。支持多图对话和推理。它在 Mantis-Eval、BLINK、Mathverse mv 和 Sciverse mv 等主流多图评测基准中取得了最佳水平,并展现出了优秀的上下文学习能力。
🎬 视频理解。接受视频输入,进行对话和提供涵盖时序和空间信息的详细视频描述。模型在 有/无字幕 评测场景下的 Video-MME 表现均超过了 GPT-4V、Claude 3.5 Sonnet 和 LLaVA-NeXT-Video-34B等商用闭源模型。
💪 强大的 OCR 能力及其他功能。 可以处理任意长宽比的图像,像素数可达 180 万(如 1344x1344)。在 OCRBench 上取得最佳水平,超过 GPT-4o、GPT-4V 和 Gemini 1.5 Pro 等商用闭源模型。基于最新的 RLAIF-V 和 VisCPM 技术,其具备了可信的多模态行为,在 Object HalBench 上的幻觉率显著低于 GPT-4o 和 GPT-4V,并支持英语、中文、德语、法语、意大利语、韩语等多种语言。
🚀 卓越的效率。 除了对个人用户友好的模型大小,MiniCPM-V 2.6 还表现出最先进的视觉 token 密度(即每个视觉 token 编码的像素数量)。它仅需 640 个 token 即可处理 180 万像素图像,比大多数模型少 75%。这一特性优化了模型的推理速度、首 token 延迟、内存占用和功耗。因此,MiniCPM-V 2.6 可以支持 iPad 等终端设备上的高效实时视频理解。
💫 易于使用。 MiniCPM-V 2.6 可以通过多种方式轻松使用:(1) llama.cpp 和 ollama 支持在本地设备上进行高效的 CPU 推理,(2) int4 和 GGUF 格式的量化模型,有 16 种尺寸,(3) vLLM 支持高吞吐量和内存高效的推理,(4) 针对新领域和任务进行微调,(5) 使用 Gradio 快速设置本地 WebUI 演示,(6) 在线demo即可体验。
PS
目前20240815官方没有更新论文,按照2.5的时间估计论文得年底了,主要是一些宣传PR,只能通过源码来窥探模型结构,至于训练数据还要等官方论文。
目前从源码看除了LLM换成了qwen2 7B, 其他模型结构和图片的处理等没有改变,主要修改了文本数据的输入格式,以支持多图和视频对话理解。
建议配合上一篇解读来看,互补,上一篇讲了模型数据,这一片重点讲
图片预处理、文本token格式、训练推理加速中padding的处理方法。
二、模型
1、模型结构
由三部分组成:视觉编码器、压缩层、大语言模型,一共8B参数。具体如下:
视觉编码器
采用SigLIP SoViT-400m/14,将输入图像转换为视觉标记。但是对SigLIP源码进行了修改,主要是支持后面数据标签部分讲的slice和改进版的padding。
这里需要注意,图片的分割方式(和上一篇2.5是一样的,那篇没看源码,所以详细步骤以这个为准),如上面figure3 a 首先有一张完整的原图直接进行缩放,然后根据如下公式来匹配最终要切成几个子图(
为了处理具有不同纵横比的原始高分辨率图像,将
图像分割成切片,每个切片在分辨率和纵横比方面更好地匹配ViT的预训练设置,并且有利于处理密集的文本任务)。
给定一个分辨率为(WI, HI)的图像,和一个在分辨率为(Wv, Hv)的图像上预训练的ViT:
1)计算切片大致数量:
N = min (⌈WI×HI / (Wv×Hv)⌉,
max_slice_nums
),这里
⌈
⌉
是向上取整的意思,
max_slice_nums 这里设置的是9
。
2)将N进行扩充,如果N <= 1 或者超参数
nerver_split = True,则不进行后面的切片。
否则将 N 扩充为N-1、N、N+1,但那是依旧需要 1<= N’ <= max_slice_nums,其中N’ 是扩充后的N的意思,也就是
N-1、N、N+1 都需要满足这个条件。
3)对扩从后的每个N’从集合CN = {(m, n)|m × n = N, m ∈ N, n ∈ N}中选择行数n和列数m的组合,选择策略:
注意这里:
*最终输入vit 的不是标准的448*448,是根据长宽比缩放的分辨率,对于一张图来说,假设slice图有9个,那么最终加上原图的整图有十个输入,其中原图整图一个分辨率,其余9个slice一个分辨率,就会产生2中分辨率。
主要涉及源码如下,省略
self.find_best_resize、
self.get_refine_siz、
self.split_to_patches函数。
def get_sliced_grid(self, image_size, max_slice_nums, nerver_split=False):
original_width, original_height = image_size
log_ratio = math.log(original_width / original_height)
ratio = original_width * original_height / (self.scale_resolution * self.scale_resolution)
multiple = min(math.ceil(ratio), max_slice_nums)
if multiple <= 1 or nerver_split:
return None
candidate_split_grids_nums = []
for i in [multiple - 1, multiple, multiple + 1]:
if i == 1 or i > max_slice_nums:
continue
candidate_split_grids_nums.append(i)
candidate_grids = []
for split_grids_nums in candidate_split_grids_nums:
m = 1
while m <= split_grids_nums:
if split_grids_nums % m == 0:
candidate_grids.append([m, split_grids_nums // m])
m += 1
best_grid = [1, 1]
min_error = float("inf")
for grid in candidate_grids:
error = abs(log_ratio - math.log(grid[0] / grid[1]))
if error < min_error:
best_grid = grid
min_error = error
return best_grid
def slice_image(
self, image, max_slice_nums=9, scale_resolution=448, patch_size=14, never_split=False
):
original_size = image.size
source_image = None
best_grid = self.get_sliced_grid(original_size, max_slice_nums, never_split)
patches = []
if best_grid is None:
# dont need to slice, upsample
best_size = self.find_best_resize(
original_size, scale_resolution, patch_size, allow_upscale=True
)
source_image = image.resize(best_size, resample=Image.Resampling.BICUBIC)
else:
# source image, down-sampling and ensure divided by patch_size
best_resize = self.find_best_resize(original_size, scale_resolution, patch_size)
source_image = image.copy().resize(best_resize, resample=Image.Resampling.BICUBIC)
refine_size = self.get_refine_size(
original_size, best_grid, scale_resolution, patch_size, allow_upscale=True
)
refine_image = image.resize(refine_size, resample=Image.Resampling.BICUBIC)
patches = self.split_to_patches(refine_image, best_grid)
return source_image, patches, best_grid
压缩层
具有一层交叉注意的感知器重采样结构,其中包含64(2.5版本是96个)个可学习的query,将每个图像切片
压缩成64个,大大降低token数。(注意如果图像产生了1个总图+9个分图共10个切片,那么将需要64*10=640 个token输入LLM)
大语言模型
采用Qwen2 7B,接收压缩后的视觉和文本标记,生成输出。这里注意后面数据标签部分提到的对图片token的特殊处理方式。
2、模型亮点
自适应视觉编码:能够处理高达1.8M像素的高分辨率图像,适应任意纵横比。
多图理解和上下文学习。支持多图对话和推理。
视频理解。接受视频输入,进行对话和提供涵盖时序和空间信息的详细视频描述。
PS
没有提具体LLM版本,应该是基础版Qwen2 7B,源码是:self.llm = Qwen2ForCausalLM(config)
三、数据
这部分因为还没有论文发布,目前的官方信息都还没有这部分内容。
1、数据标签
因为支持多图和视频外加一张图本身有多个切片,所以最终的数据反应到token 维度是:
<|im_start|>system
You are a helpful assistant.<|im_end|>
<|im_start|>user
<image_id>0</image_id><image><unk>...<unk></image><slice><unk>...<unk></slice><slice><unk>...<unk></slice><slice><unk>...<unk></slice>
<slice><unk>...<unk></slice><slice><unk>...<unk></slice><slice><unk>...<unk></slice>
<slice><unk>...<unk></slice><slice><unk>...<unk></slice><slice><unk>...<unk></slice>
Describe this image<|im_end|>
<|im_start|>assistant
注意
1、开始标志变为<|im_start|>,对应的结束标志变为<|im_end|>。
2、因为有多图和视频所以有标志图片顺序的: <image_id>0</image_id>。
3、因为有切片图和整图,所以要区分:
整图的标识 <image></image> 和 slice 的标识 <slice></slice>。其中对于切片图每行后面有一个额外的标志“换行符”。
4、其中的<unk>是占位符,因为2.6版本每个图片原图或切片被重新采样编码为64个token,所以其实有64个占位符。最终LLM推理的时候这个占位符是要替换为真是的图像token的。
PS
关于为什么有这个 <unk>占位符:
1、实现方式:源码实现的时候,是先通过计算图片的切片等数,使用占位符填充LLM的输入,然后进行文本的token 化,然后再将图片batch 化进入视觉模型进行请求,然后再将真实的视觉重采样后的token 输入LLM。特别注意:前面说的batch化不是prompt 自己的batch请求,包含:prompt本身batch、本身一条prompt里面有多个图、一个图又有本身+切片导致其实有三个维度会造成很多图,统一成一个超大batch。
2、猜测原因:1)如上面说,为了batch化请求视觉模型,提升吞吐。2)作者再2.5版本的论文中也提到,这样可以解耦视觉和LLM,必要时可以部署到不同的硬件(CPU、NPU)或者说不同时加载来降低内存的需要。
3、继续进阶一下,这么大的batch,因为上面看到每一个最终输入视觉模型的图片的输入分辨率并不同(一张图的原图缩放和切片都不同,但是一张图的所有切片图是相同的,有点绕),但是batch推理的时候需要分辨率相同,那就需要padding,如何高效的padding?作者的做法就是把图片变换为h_des=patch_size,w_des = h_src * w_src / patch_size 的尺寸,也就是变成了一个长条,然后当成语言模型一样,只在一维填充,这样就降低了填充量,提高填充效率。如下图:
相关源码
占位符涉及到的源码主要在
MiniCPM-V-2_6/processing_minicpmv.py 和 MiniCPM-V-2_6/image_processing_minicpmv.py两个文件,因为涉及函数确实比较多,建议可以看两个源码,比较容易看懂。
padding涉及到的源码片段如下
""" MiniCPM-V-2_6/image_processing_minicpmv.py """
def reshape_by_patch(self, image):
"""
:param image: shape [3, H, W]
:param patch_size:
:return: [3, patch_size, HW/patch_size]
"""
image = torch.from_numpy(image)
patch_size = self.patch_size
patches = torch.nn.functional.unfold(
image,
(patch_size, patch_size),
stride=(patch_size, patch_size)
)
patches = patches.reshape(image.size(0), patch_size, patch_size, -1)
patches = patches.permute(0, 1, 3, 2).reshape(image.size(0), patch_size, -1)
return patches.numpy()
for slice_image in image_patches:
new_images.append(self.reshape_by_patch(slice_image))
tgt_sizes.append(np.array((slice_image.shape[1] // self.patch_size, slice_image.shape[2] // self.patch_size)))
new_images_list.append(new_images)
tgt_sizes_list.append(tgt_sizes)
"""MiniCPM-V-2_6/modeling_minicpmv.py"""
all_pixel_values = []
for pixel_values in pixel_values_list:
img_cnt.append(len(pixel_values))
all_pixel_values.extend([i.flatten(end_dim=1).permute(1, 0) for i in pixel_values])
# exist image
if all_pixel_values:
tgt_sizes = [tgt_size for tgt_size in tgt_sizes if isinstance(tgt_size, torch.Tensor)]
tgt_sizes = torch.vstack(tgt_sizes).type(torch.int32)
max_patches = torch.max(tgt_sizes[:, 0] * tgt_sizes[:, 1])
all_pixel_values = torch.nn.utils.rnn.pad_sequence(all_pixel_values, batch_first=True,
padding_value=0.0)
B, L, _ = all_pixel_values.shape
all_pixel_values = all_pixel_values.permute(0, 2, 1).reshape(B, 3, -1, L)
patch_attn_mask = torch.zeros((B, 1, max_patches), dtype=torch.bool, device=device)
for i in range(B):
patch_attn_mask[i, 0, :tgt_sizes[i][0] * tgt_sizes[i][1]] = True
vision_batch_size = self.config.vision_batch_size
all_pixel_values = all_pixel_values.type(dtype)
if B > vision_batch_size:
hs = []
for i in range(0, B, vision_batch_size):
start_idx = i
end_idx = i + vision_batch_size
tmp_hs = self.vpm(all_pixel_values[start_idx:end_idx], patch_attention_mask=patch_attn_mask[start_idx:end_idx], tgt_sizes=tgt_sizes[start_idx:end_idx]).last_hidden_state
hs.append(tmp_hs)
vision_embedding = torch.cat(hs, dim=0)
else:
vision_embedding = self.vpm(all_pixel_values, patch_attention_mask=patch_attn_mask, tgt_sizes=tgt_sizes).last_hidden_state
""" MiniCPM-V-2_6/modeling_navit_siglip.py """
self.patch_embedding = nn.Conv2d(
in_channels=config.num_channels,
out_channels=self.embed_dim,
kernel_size=self.patch_size,
stride=self.patch_size,
padding="valid",
)
batch_size = pixel_values.size(0)
patch_embeds = self.patch_embedding(pixel_values)
embeddings = patch_embeds.flatten(2).transpose(1, 2)
max_im_h, max_im_w = pixel_values.size(2), pixel_values.size(3)
max_nb_patches_h, max_nb_patches_w = max_im_h // self.patch_size, max_im_w // self.patch_size
2、数据构成
暂无。
3、数据清洗
暂无。
四、策略
这部分因为还没有论文发布,目前的官方信息都还没有这部分内容。
1、训练过程
几个阶段训练、冻结哪个网络模块、训练超参。
2、推理过程
可以参见官方的推理教程,包含单图、多图、视频,这里需要注意视频的处理逻辑是每秒一帧,最高支持64帧,也就是差不多一分钟的视频理解:
魔搭社区
五、结果
1、多维度对比
N边型战士
单图评测结果
多图评测结果
视频评测结果
少样本评测结果
典型case
主要是OCR识别(格式化输出和计算里面的数字)、多图对话、代码debug、视频理解。参见官方README。
2、消融实验
暂无
六、使用方法
官方给出了单图、多图、视频的实例code,可以参考:
魔搭社区
七、待解决
训练数据源,训练策略。
八、参考链接
官方魔塔社区README:
魔搭社区
部署指南:
Docs
训练指南:
Docs
图片处理方式:
Docs