我的视觉模型之旅

社区文章 发布于 2025 年 4 月 12 日

cover

计算机视觉对我来说一直是一个引人入胜的领域。我记得第一次尝试图像识别是在高中时期——只是暑假里一个有趣的小项目😂。美好的时光!

随后迎来了大型语言模型(LLM)的黄金时代,和许多人一样,我为它们的快速进展感到非常兴奋。我开始学习 Transformer 架构,以及它如何不仅彻底改变了 NLP 领域,也开始在计算机视觉领域掀起波澜。

幸运的是,我于 2024 年 8 月加入了 Hugging Face。这让我终于可以投入更多时间到像 llama.cpp 这样的项目中。那时,视觉支持仍处于早期阶段,但我渴望贡献。多亏了 Hugging Face 的杰出团队,我学到了大量关于视觉模型最新进展的知识。

在这篇文章中,我想分享我探索视觉模型世界以及将其集成到 llama.cpp 的旅程。我希望这能激励其他人探索计算机视觉的激动人心的领域,甚至为开源社区做出贡献!

是的,封面图片上就是我,带着我信赖的富士 X-E1 📷
在这里查看我关于摄影的帖子 这里

视觉模型概述

目前大多数视觉模型都有两个主要部分:视觉编码器和语言解码器。视觉编码器通常基于 Transformer 架构,因此得名“视觉 Transformer”(ViT)。

为了更好地理解这一点,想象两个人一起工作

  • 一个人可以一张图片。
  • 另一个人只能阅读描述。

第一个人看着图片并向第二个人描述他们所看到的内容。然后,第二个人使用该描述来回答问题或生成文本。

meme illustration

这大致就是视觉模型的工作原理!**视觉塔**(或视觉编码器)就像第一个人一样,“看”图像并将其压缩成一组中间表示。然后,**语言模型**(或语言解码器)就像第二个人一样,根据这些表示生成答案。

这些**中间表示**可以有各种形式(如用于交叉注意力的 KV 向量),但最常见的形式是一组**嵌入向量**。在接下来的章节中,我们将主要关注这些。

构成部分

这是一个整个流程的简化图

image/png

预处理

预处理是魔法开始的地方。它通常包括:

  1. 将图像从其舒适的文件格式(例如 JPEG、PNG)转换为原始位图。
  2. 将图像调整为固定大小(通常是缩小)。如果宽高比与目标不匹配,这可能需要一些填充或裁剪。
  3. 对于可以处理不同图像大小的模型,我们可能需要将图像切片(更多内容见下文)。
  4. 将图像转换为张量并标准化其像素值。

视觉编码器通常需要固定且通常相当小的输入图像尺寸。这意味着如果原始图像太大,宝贵的细节可能会丢失。为了解决这个问题:

  • 一些模型(如 LLaVA-UHD、MiniCPM-V)巧妙地将大图像分成更小的切片,同时处理这些切片和缩小后的原始图像。
  • 其他模型则直接接受更大的图像输入(例如 Gemma 3)。
  • 最值得注意的是,Qwen2-VL 使用一种特殊的定位嵌入技术,称为 M-RoPE,即使在不同大小的图像中,也能跟踪补丁的来源,而不会丢失空间上下文。非常酷!

有一点让我挠头的是,虽然切片看起来是**纯粹的算法**(这里不需要花哨的机器学习),但具体的切片策略却可能出奇地复杂,并且在不同模型之间差异很大。

LLaVA-UHD 自适应切片算法示例 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 映射到其对应的嵌入向量。

所以,实际发生的事情看起来更像是这样

image/png

看到了吗?从语言模型的角度来看,图像嵌入只是另一组输入向量,与文本嵌入无缝连接。关键的区别在于,图像嵌入是**动态的**(它们根据输入图像而变化),而文本 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 示意图 M-RoPE 示意图。来源:Qwen2-VL 技术报告

这种多样性意味着在像 llama.cpp 这样的下游项目中找出处理多切片嵌入的正确方法,感觉就像没有说明书组装宜家家具一样——绝对具有挑战性!

未来展望

我们已经探讨了视觉模型的概念和内部工作原理。

如你所见,视觉编码器本质上是一种将图像翻译成 LLM 可以理解的语言(嵌入)的方法。酷炫之处在于,这种“编码器-解码器”概念不仅仅适用于图像!我们可以将相同的基本思想应用于其他模态,如音频或视频。主要区别在于将视觉编码器替换为适合该特定模态的编码器。

例如,为了处理视频输入,像 Qwen2.5-Omni 这样的模型会逐帧使用其视觉编码器来处理视觉流,并使用另一个 Transformer 来编码音频流。然后,这两个编码器的输出都会被馈送给语言模型,以理解组合的视听输入。

Qwen2.5-Omni 视频处理示意图 Qwen2.5-Omni 视频处理示意图。来源:Qwen2.5-Omni-7B 模型卡片

多模态人工智能的可能性广阔且令人兴奋!

结论

在这篇文章中,我分享了我在视觉模型领域摸爬滚打的一些经历,希望能为大家揭示这些迷人“野兽”的内部运作原理。从高层概念到细节的实现(以及偶尔的头痛!),这真是一段奇妙的旅程。

我希望这篇对计算机视觉世界的浅尝辄止能激励你进一步探索,甚至投入到充满活力的开源人工智能社区中去。总有更多的东西需要学习和创造!

社区

写得真棒 ❤️

注册登录 以发表评论