我的视觉模型之旅

计算机视觉对我来说一直是一个引人入胜的领域。我记得第一次尝试图像识别是在高中时期——只是暑假里一个有趣的小项目😂。美好的时光!
随后迎来了大型语言模型(LLM)的黄金时代,和许多人一样,我为它们的快速进展感到非常兴奋。我开始学习 Transformer 架构,以及它如何不仅彻底改变了 NLP 领域,也开始在计算机视觉领域掀起波澜。
幸运的是,我于 2024 年 8 月加入了 Hugging Face。这让我终于可以投入更多时间到像 llama.cpp 这样的项目中。那时,视觉支持仍处于早期阶段,但我渴望贡献。多亏了 Hugging Face 的杰出团队,我学到了大量关于视觉模型最新进展的知识。
在这篇文章中,我想分享我探索视觉模型世界以及将其集成到 llama.cpp 的旅程。我希望这能激励其他人探索计算机视觉的激动人心的领域,甚至为开源社区做出贡献!
是的,封面图片上就是我,带着我信赖的富士 X-E1 📷
在这里查看我关于摄影的帖子 这里
视觉模型概述
目前大多数视觉模型都有两个主要部分:视觉编码器和语言解码器。视觉编码器通常基于 Transformer 架构,因此得名“视觉 Transformer”(ViT)。
为了更好地理解这一点,想象两个人一起工作
- 一个人可以看一张图片。
- 另一个人只能阅读描述。
第一个人看着图片并向第二个人描述他们所看到的内容。然后,第二个人使用该描述来回答问题或生成文本。
这大致就是视觉模型的工作原理!**视觉塔**(或视觉编码器)就像第一个人一样,“看”图像并将其压缩成一组中间表示。然后,**语言模型**(或语言解码器)就像第二个人一样,根据这些表示生成答案。
这些**中间表示**可以有各种形式(如用于交叉注意力的 KV 向量),但最常见的形式是一组**嵌入向量**。在接下来的章节中,我们将主要关注这些。
构成部分
这是一个整个流程的简化图
预处理
预处理是魔法开始的地方。它通常包括:
- 将图像从其舒适的文件格式(例如 JPEG、PNG)转换为原始位图。
- 将图像调整为固定大小(通常是缩小)。如果宽高比与目标不匹配,这可能需要一些填充或裁剪。
- 对于可以处理不同图像大小的模型,我们可能需要将图像切片(更多内容见下文)。
- 将图像转换为张量并标准化其像素值。
视觉编码器通常需要固定且通常相当小的输入图像尺寸。这意味着如果原始图像太大,宝贵的细节可能会丢失。为了解决这个问题:
- 一些模型(如 LLaVA-UHD、MiniCPM-V)巧妙地将大图像分成更小的切片,同时处理这些切片和缩小后的原始图像。
- 其他模型则直接接受更大的图像输入(例如 Gemma 3)。
- 最值得注意的是,Qwen2-VL 使用一种特殊的定位嵌入技术,称为 M-RoPE,即使在不同大小的图像中,也能跟踪补丁的来源,而不会丢失空间上下文。非常酷!
有一点让我挠头的是,虽然切片看起来是**纯粹的算法**(这里不需要花哨的机器学习),但具体的切片策略却可能出奇地复杂,并且在不同模型之间差异很大。
LLaVA-UHD 自适应切片算法示例。来源:LLaVA-UHD Github 仓库
**重要提示**:每个切片在进入编码器时都被视为一个单独的图像。因此,当我谈论编码器时,我可能会互换使用“切片”和“图像”。
分割成补丁
接下来,我们将图像(或切片)切割成更小的、大小相等的补丁。可以把它想象成把一张照片切成一个由小方块组成的网格。每个补丁然后被展平为一个单独的向量。
在许多实现中,这种切割不是用剪刀完成的,而是通过一种称为**2D 卷积**的数学操作,使用与所需补丁大小匹配的核大小。这一步还巧妙地将一些额外的信息嵌入到补丁中,这要归功于卷积的核和偏置。
位置嵌入也被添加到这些补丁向量中。这至关重要,因为 Transformer 本身没有固有的空间感——这些嵌入告诉模型每个补丁在原始图片中的来源。
将图像或切片分割成补丁的示意图。来源:ResearchGate
您可能会问:“为什么不直接将整个图像输入呢?” 好问题!但那样做会使输入向量变得巨大,导致模型大小呈指数级膨胀。分块处理可以保持可管理性。
视觉编码器
视觉编码器通常是基于 Transformer 的模型。它将已注入位置信息的补丁作为输入,并输出一组嵌入向量。
这部分是视觉处理的核心。它的实现通常相对简单(呼!),部分原因是我们不需要担心生成式语言模型中 KV 缓存的复杂性。Transformer 以**非因果**方式处理所有补丁。这意味着它可以同时查看所有补丁,从而找出它们之间的关系。
因果与非因果注意力示意图。来源:ResearchGate
底层的 Transformer 从补丁中提取特征。如果图像中有一只猫和一只狗,Transformer 会生成嵌入向量,以某种方式表示从这些补丁中提取的“猫性”和“狗性”。
使用非因果注意力很重要,因为一个物体可能横跨多个补丁。例如,一个补丁可能只包含猫雄伟的鼻子,而另一个补丁则包含一只耳朵。非因果注意力有助于模型将这些不同部分拼接成完整的猫。
当然,试图直接解释这些嵌入向量中有什么,就像试图理解抽象艺术一样——有意义,但很棘手!下面是一个高度简化的想法
嵌入向量可能代表什么的简化示例
投影仪
好的,我们有一组很好的代表图像的嵌入向量。但是等等!在大多数情况下,语言模型期望的输入向量大小(维度)与视觉编码器输出的不同。啊哦。
为了弥补这个差距,我们需要一个**投影仪**。最简单的方法通常是多层感知器(MLP),它涉及几次矩阵乘法,中间夹杂一个激活函数(如 GELU)。这是一个基本的代码思路
import torch
import torch.nn as nn
class Projector(nn.Module):
# input_dim: dimension of the vision encoder's output vectors
# hidden_size: dimension of the MLP's hidden layer
# output_dim: dimension the language model expects
def __init__(self, input_dim, hidden_size, output_dim):
super(Projector, self).__init__()
self.fc1 = nn.Linear(input_dim, hidden_size)
self.activation = nn.GELU()
self.fc2 = nn.Linear(hidden_size, output_dim)
def forward(self, x):
# input: n vectors of size input_dim
x = self.fc1(x) # Project from input_dim to hidden_size
x = self.activation(x)
x = self.fc2(x) # Project from hidden_size to output_dim
return x # output: n vectors of size output_dim
但生活并非总是一帆风顺!有些模型变得**更花哨**
- MiniCPM-V 为了投影又加入了另一个 Transformer 😂!
- Gemma 3、Phi-4-multimodal 和 MobileVLM 等模型使用 `Pool2D` 层来减少输出向量的数量,有点像“总结”图像嵌入,从而减少输入到语言模型的 token 数量。
是的,由于这些不同的复杂性,投影仪通常是重新实现时最棘手的部分之一。
语言解码器
“等等,”你可能会想,“语言模型是处理token(基本上就是数字)的,对吧?我们怎么能把这些连续的嵌入向量输入给它们呢?”
问得好!事实证明,文本 token 也会在语言模型内部被转换成嵌入向量。大多数(如果不是全部)LLM 都有一个内置的查找表(通常是一个名为 `embed_tokens.weight` 的张量),它将每个 token ID 映射到其对应的嵌入向量。
所以,实际发生的事情看起来更像是这样
看到了吗?从语言模型的角度来看,图像嵌入只是另一组输入向量,与文本嵌入无缝连接。关键的区别在于,图像嵌入是**动态的**(它们根据输入图像而变化),而文本 token 嵌入通常是**学习到的且固定的**。
在训练过程中,模型学会将这些图像嵌入与周围的文本上下文关联起来。如果它看到与猫对应的图像嵌入,它就会知道在这种上下文中生成“猫”(或相关概念)这个词是合适的。
多切片图像解码
到目前为止,我们主要讨论的是单张图像或切片。但当模型处理多个切片时(例如 LLaVA-UHD、MiniCPM-V、Idefics),事情就变得更加有趣了。语言模型如何知道哪些嵌入属于哪个切片,或者它们在空间上如何排列?
一种常见技术是在输入序列中使用特殊“标记”token 来划分来自不同切片或切片行的嵌入。例如,对于一个被分成 4 个切片加上缩小后的原始图像
[Downscaled Image] --> [Slice 1] --> [Slice 2]
| |
+--> [Slice 3] --> [Slice 4]
输入到 LLM 的嵌入结构可能如下(使用假设的特殊 token)
<image>[Downscaled Image]</image>\n
<slice>[Slice 1]</slice><slice>[Slice 2]</slice>\n
<slice>[Slice 3]</slice><slice>[Slice 4]</slice>\n
当然,具体的特殊 token(`<image>`、`<slice>`、`<row>`等)和结构在不同模型之间差异很大。
一些模型,如 Qwen2-VL,则采用不同的路径,使用前面提到的 M-RoPE 技术来隐式编码每个补丁嵌入的二维位置,从而避免了对显式切片令牌的需求。
M-RoPE 示意图。来源:Qwen2-VL 技术报告
这种多样性意味着在像 llama.cpp 这样的下游项目中找出处理多切片嵌入的正确方法,感觉就像没有说明书组装宜家家具一样——绝对具有挑战性!
未来展望
我们已经探讨了视觉模型的概念和内部工作原理。
如你所见,视觉编码器本质上是一种将图像翻译成 LLM 可以理解的语言(嵌入)的方法。酷炫之处在于,这种“编码器-解码器”概念不仅仅适用于图像!我们可以将相同的基本思想应用于其他模态,如音频或视频。主要区别在于将视觉编码器替换为适合该特定模态的编码器。
例如,为了处理视频输入,像 Qwen2.5-Omni 这样的模型会逐帧使用其视觉编码器来处理视觉流,并使用另一个 Transformer 来编码音频流。然后,这两个编码器的输出都会被馈送给语言模型,以理解组合的视听输入。
Qwen2.5-Omni 视频处理示意图。来源:Qwen2.5-Omni-7B 模型卡片
多模态人工智能的可能性广阔且令人兴奋!
结论
在这篇文章中,我分享了我在视觉模型领域摸爬滚打的一些经历,希望能为大家揭示这些迷人“野兽”的内部运作原理。从高层概念到细节的实现(以及偶尔的头痛!),这真是一段奇妙的旅程。
我希望这篇对计算机视觉世界的浅尝辄止能激励你进一步探索,甚至投入到充满活力的开源人工智能社区中去。总有更多的东西需要学习和创造!