Janus多模态模型的设计核心在于视觉编码的解耦。传统多模态模型通常使用单一的视觉编码器来处理多模态理解和视觉生成任务,但由于这两种任务对视觉特征的需求存在显著差异,单一编码器往往难以同时满足两种任务的需求,从而导致性能瓶颈。为了解决这一问题,Janus模型提出了双路径视觉编码架构,将多模态理解和视觉生成任务的视觉编码过程完全分离,从而避免了任务间的冲突,并显著提升了模型在多模态任务中的表现。
Janus模型的整体架构基于自回归Transformer,这是一种强大的序列生成框架,广泛应用于自然语言处理和多模态任务中。自回归Transformer通过逐个生成序列中的元素(如文本中的单词或图像中的像素),能够有效地捕捉序列中的依赖关系。在Janus模型中,自回归Transformer不仅处理文本输入,还整合了来自视觉模态的特征,从而实现多模态数据的统一处理。
1. 视觉编码路径
Janus模型的核心设计是将视觉编码分为如下两个独立的路径:
(1)多模态理解:专门用于处理需要理解图像语义的任务,如视觉问答(VQA)、图像描述生成、图文匹配等。这一路径的目标是从图像中提取高维语义特征,并将其映射到与语言模型兼容的输入空间。
(2)视觉生成:专门用于处理需要生成图像的任务,如文本到图像的生成。这一路径的目标是将图像转换为离散的视觉token,并通过生成适配器将其嵌入到语言模型的输入空间中。
这两种路径分别处理不同任务的输入数据,但最终会将生成的特征序列拼接在一起,形成一个统一的多模态特征序列,这个序列随后被输入到自回归Transformer中进行进一步处理。通过这种设计,Janus模型能够在同一个框架下高效地处理多模态理解和视觉生成任务,同时避免了传统模型中视觉编码器在两种任务间的功能冲突。
2. Janus架构设计的优势
Janus多模态模型通过解耦视觉编码路径,实现了多模态理解和视觉生成任务的高效统一。这种架构设计不仅解决了传统模型中视觉编码器的功能冲突问题,还提升了模型的性能和扩展性。具体来说,Janus模型的架构设计带来了以下显著优势。
多模态理解的目标是从图像中提取丰富的语义信息,以支持视觉问答、图像描述等任务。为了实现这一目标,Janus 模型在这一部分采用了专门设计的模块和操作流程,具体说明如下所示。
1. 视觉编码器
Janus 模型选用了 SigLIP 编码器作为多模态理解路径的核心。SigLIP 是一种基于 Transformer 的视觉编码器,设计初衷是捕捉图像中的高层语义信息,SigLIP的工作如下:
2. 特征处理
SigLIP 编码器输出的是一个二维的特征图,这个特征图类似于一个由多个特征向量构成的网格,每个向量对应图像中某一局部区域的语义描述。为了更好地利用这些视觉特征,Janus 模型在后续处理中引入了两项关键策略,这两项策略相辅相成,共同确保了视觉信息能够在多模态建模中发挥最大效用。特征处理的工作如下:
3. 理解适配器
为了将展平后的高维视觉特征进一步映射到与语言模型相同的嵌入空间,Janus 模型引入了一个两层多层感知机(MLP)作为理解适配器。
在DeepSeek的开源代码中,文件projector.py定义了一个名为 MlpProjector 的类,作为一个多层感知机(MLP)投影器,用于将输入数据映射到指定的嵌入空间。在多模态模型中,投影器是连接不同模态特征的关键组件。MlpProjector投影器根据配置(cfg)的不同,支持多种类型的投影方式:identity、linear、mlp_gelu、和 low_high_hybrid_split_mlp_gelu。如果配置为 low_high_hybrid_split_mlp_gelu,会将输入分为两部分(高分辨率和低分辨率),然后分别进行投影并合并。前向传播方法接受一个输入(可以是元组,也可以是单一张量),并通过配置的投影器进行转换,返回投影后的结果。
class MlpProjector(nn.Module):
def __init__(self, cfg):
super().__init__()
self.cfg = cfg
if cfg.projector_type == "identity":
modules = nn.Identity()
elif cfg.projector_type == "linear":
modules = nn.Linear(cfg.input_dim, cfg.n_embed)
elif cfg.projector_type == "mlp_gelu":
mlp_depth = cfg.get("depth", 1)
modules = [nn.Linear(cfg.input_dim, cfg.n_embed)]
for _ in range(1, mlp_depth):
modules.append(nn.GELU())
modules.append(nn.Linear(cfg.n_embed, cfg.n_embed))
modules = nn.Sequential(*modules)
elif cfg.projector_type == "low_high_hybrid_split_mlp_gelu":
mlp_depth = cfg.get("depth", 1)
self.high_up_proj = nn.Linear(cfg.input_dim, cfg.n_embed // 2)
self.low_up_proj = nn.Linear(cfg.input_dim, cfg.n_embed // 2)
modules = []
for _ in range(1, mlp_depth):
modules.append(nn.GELU())
modules.append(nn.Linear(cfg.n_embed, cfg.n_embed))
modules = nn.Sequential(*modules)
else:
raise ValueError(f"Unknown projector type: {cfg.projector_type}")
self.layers = modules
def forward(
self, x_or_tuple: Union[Tuple[torch.Tensor, torch.Tensor], torch.Tensor]
):
"""
参数:
x_or_tuple (Union[Tuple[torch.Tensor, torch.Tensor], torch.Tensor]):如果是一个元组,则来自混合视觉编码器,其中x = high_res_x,low_res_x;
否则,它是来自单一视觉编码器的特征。
返回:
x (torch.Tensor):[b, s, c]
"""
if isinstance(x_or_tuple, tuple):
# self.cfg.projector_type == "low_high_hybrid_split_mlp_gelu":
high_x, low_x = x_or_tuple
high_x = self.high_up_proj(high_x)
low_x = self.low_up_proj(low_x)
x = torch.concat([high_x, low_x], dim=-1)
else:
x = x_or_tuple
return self.layers(x)
if __name__ == "__main__":
cfg = AttrDict(
input_dim=1024,
n_embed=2048,
depth=2,
projector_type="low_high_hybrid_split_mlp_gelu",
)
inputs = (torch.rand(4, 576, 1024), torch.rand(4, 576, 1024))
m = MlpProjector(cfg)
out = m(inputs)
print(out.shape)
通过上述流程,Janus 的多模态模型有效地将图像中的视觉信息转换为与文本特征相兼容的表示。该过程不仅保留了图像的全局和局部语义,同时也为自回归 Transformer 模型提供了一个统一的输入,从而在后续任务中实现高效的多模态融合与信息建模。
视觉生成的核心目标是将图像转换为离散的ID序列,并根据文本描述生成对应的图像。设计旨在解决传统多模态模型中视觉生成任务的挑战,例如如何高效地将图像内容与文本描述对齐,以及如何生成高质量且语义一致的图像。通过将图像离散化为视觉token,并将其嵌入到语言模型的输入空间中,Janus模型能够以一种类似于处理文本的方式处理图像,从而实现高效的视觉生成。
1. 视觉编码器:VQ Tokenizer
在视觉生成中,Janus模型使用了VQ Tokenizer作为核心组件。VQ Tokenizer基于矢量量化(Vector Quantization)技术,能够将图像分割为离散的视觉token。具体实现过程如下:
(1)图像分割:VQ Tokenizer首先将输入图像划分为多个小块(patches)。这些小块通常是固定大小的正方形区域,例如16×16像素。通过这种方式,图像被分解为多个局部区域,每个区域代表图像的一个局部特征。
(2)矢量量化:每个小块被提取为一个特征向量,并通过矢量量化技术映射到一个离散的编码空间中。矢量量化是一种将连续的特征向量映射到离散符号的技术,类似于将图像中的每个小块“编码”为一个特定的符号或token。这些离散的token能够有效地表示图像的局部特征,同时减少了计算复杂度。
(3)离散化处理:通过矢量量化,图像被转换为一系列离散的ID序列。这种离散化处理使得图像能够以一种类似于文本的方式被处理,每个视觉token类似于文本中的单词或字符。这种设计不仅便于与语言模型的输入格式对齐,还使得图像生成过程能够利用语言模型的强大生成能力。
在介绍了Janus模型中视觉生成路径的核心组件——VQ Tokenizer之后,我们转向模型的另一个重要组成部分:视觉编码器配置。在DeepSeek Janus模型的开源代码中,文件clip_encoder.py定义了一个名为 CLIPVisionTower 的类,用于构建并使用不同类型的视觉模型(如 siglip、sam 或 HuggingFace 的 CLIP)。这个类包括图像预处理(如像素均值和标准差归一化)、选择不同层输出的功能,以及根据不同模型配置生成相应的视觉模型。具体来说,CLIPVisionTower 将图像通过视觉塔(vision tower)进行特征提取,提取指定层的特征,并根据 select_feature 参数选择合适的特征(如“patch”或“cls_patch”)。它还支持自定义图像归一化。
class CLIPVisionTower(nn.Module):
def __init__(
self,
model_name: str = "siglip_large_patch16_384",
image_size: Union[Tuple[int, int], int] = 336,
select_feature: str = "patch",
select_layer: int = -2,
select_layers: list = None,
ckpt_path: str = "",
pixel_mean: Optional[List[float]] = None,
pixel_std: Optional[List[float]] = None,
**kwargs,
):
super().__init__()
self.model_name = model_name
self.select_feature = select_feature
self.select_layer = select_layer
self.select_layers = select_layers
vision_tower_params = {
"model_name": model_name,
"image_size": image_size,
"ckpt_path": ckpt_path,
"select_layer": select_layer,
}
vision_tower_params.update(kwargs)
self.vision_tower, self.forward_kwargs = self.build_vision_tower(
vision_tower_params
)
if pixel_mean is not None and pixel_std is not None:
image_norm = torchvision.transforms.Normalize(
mean=pixel_mean, std=pixel_std
)
else:
image_norm = None
self.image_norm = image_norm
def build_vision_tower(self, vision_tower_params):
if self.model_name.startswith("siglip"):
self.select_feature = "same"
vision_tower = create_siglip_vit(**vision_tower_params)
forward_kwargs = dict()
elif self.model_name.startswith("sam"):
vision_tower = create_sam_vit(**vision_tower_params)
forward_kwargs = dict()
else: # huggingface
from transformers import CLIPVisionModel
vision_tower = CLIPVisionModel.from_pretrained(**vision_tower_params)
forward_kwargs = dict(output_hidden_states=True)
return vision_tower, forward_kwargs
def feature_select(self, image_forward_outs):
if isinstance(image_forward_outs, torch.Tensor):
# 输出已经是 self.select_layer 对应的特征
image_features = image_forward_outs
else:
image_features = image_forward_outs.hidden_states[self.select_layer]
if self.select_feature == "patch":
# 如果输出中包含 cls_token
image_features = image_features[:, 1:]
elif self.select_feature == "cls_patch":
image_features = image_features
elif self.select_feature == "same":
image_features = image_features
else:
raise ValueError(f"Unexpected select feature: {self.select_feature}")
return image_features
def forward(self, images):
"""
参数:
images (torch.Tensor): [b, 3, H, W]
返回:
image_features (torch.Tensor): [b, n_patch, d]
"""
if self.image_norm is not None:
images = self.image_norm(images)
image_forward_outs = self.vision_tower(images, **self.forward_kwargs)
image_features = self.feature_select(image_forward_outs)
return image_features
总之,类CLIPVisionTower为多模态模型提供了图像特征提取功能,这些特征可以用于与文本特征的对齐和融合。
2. 特征处理:生成适配器
VQ Tokenizer输出的是一系列离散的ID序列,这些ID序列需要进一步处理以适应语言模型的输入格式。具体步骤如下:
(1)序列展平:VQ Tokenizer输出的ID序列通常是二维的(对应于图像的行和列),为了与语言模型的输入格式一致,Janus模型将这些二维ID序列展平为一维序列。这种展平操作保留了图像的空间顺序信息,使得语言模型能够更好地理解图像的局部和全局结构。
(2)码本嵌入映射:每个离散ID对应于一个码本嵌入(codebook embeddings),这些嵌入是VQ Tokenizer在训练过程中学习到的特征表示。为了将视觉token嵌入到语言模型的输入空间中,Janus模型使用了一个生成适配器。生成适配器由两层多层感知机(MLP)组成,其作用是将码本嵌入映射到语言模型的输入空间中。
(3)交互与融合:通过生成适配器的映射操作,视觉token能够与文本token在同一个空间中进行交互。这种交互使得语言模型能够同时处理文本和视觉特征,从而实现高效的视觉生成任务。例如,在文本到图像的生成任务中,模型可以根据文本描述中的语义信息,选择合适的视觉token来生成对应的图像内容。
3. 向量量化模型
视觉生成路径的目标是将图像转换为离散的 ID 序列,并根据文本描述生成对应的图像。这一路径的核心组件是基于向量量化(Vector Quantization)的模型,具体实现为 VQ-VAE(Vector Quantized Variational Autoencoder)。在DeepSeek Janus模型的开源代码中,文件 vq_model.py 实现了这一模型,包括编码器、解码器和向量量化器(Vector Quantizer),支持图像的压缩和重建。这是多模态模型中图像处理的核心组件,为图像的嵌入和生成提供了基础。文件的核心思想是借助 VQ-VAE 对输入数据进行编码、离散化,并使用向量量化方法来学习更好的表示。文件 vq_model.py的具体实现流程如下所示。
(1)类Encoder 是 VQ-VAE(或者类似自编码器)中的编码器,负责将输入图像转换为紧凑的特征表示(即 潜变量)。整个过程包括特征提取、分辨率逐步降低(下采样)、残差学习、注意力机制 等,以提取关键信息并减少冗余,为后续的离散化和解码提供高质量的潜在特征。
Encoder的结构如下:
(2)类Decoder 是一种神经网络解码器结构,主要用于将编码器产生的潜在空间特征图(z)转化为最终的输出图像。它通过若干残差块和注意力机制模块逐层恢复图像的分辨率,同时保持输入特征图的语义信息。解码器的核心目标是利用输入的潜在特征图 z 生成和输入图像维度相同的输出,通常用于图像生成任务。类Decoder中的成员如下:
(3)VectorQuantizer 是一种用于量化的神经网络模块,主要用于将连续的向量表示映射到一个离散的码本空间。这种技术通常用于变分自编码器(VAE)和生成模型中,通过向量量化来增强模型的表达能力。该模块使用嵌入式码本来实现向量量化,同时支持可调的损失函数和不同的正则化策略,帮助模型在训练过程中有效地逼近目标分布。
VectorQuantizer中的成员如下所示:
(4)AttnBlock 是一个实现自注意力机制的神经网络模块,该模块通过使用三个卷积层(分别用于查询、键、值)来计算输入特征图的注意力权重。首先,输入特征图经过归一化处理后,生成查询(Q)、键(K)和值(V)。然后,计算查询和键之间的相似度,生成注意力权重,并使用 softmax 函数进行归一化。接着,这些权重与值进行加权平均,得到加权后的特征图,并通过一个卷积层进行最终的输出投影。最后,输出结果与原始输入特征图相加,形成残差连接。该模块的主要目的是使网络能够捕捉不同空间位置之间的依赖关系,从而增强特征的表达能力。
(5)类VQModel 是一个向量量化模型,主要用于图像或其他数据的编码和解码过程。该模型包括编码器、解码器以及向量量化模块,具体说明如下:
VQModel模型通过向量量化将连续的输入映射到离散的编码空间,在图像生成和重建任务中起到重要作用。