InternVL系列

Jul 31, 2024
2 views
Large Model

InternVL 1.0

项目地址:

🔖 https://internvl.github.io/blog/2023-12-12-InternVL-1.0/

拼接视觉+语言模型,“对齐”是个万能胶

语言模型和视觉模型各自发展,各有突破,但如何让语言模型会看图,或者让视觉模型会说话?为了将视觉模型与语言模型进行连接,对齐如同“胶水”,将两种模型链接在一起,如使用QFormer或线性投影这样的轻量级“胶水”层,来形成视觉-语言模型,如InstructBLIP和LLaVA,但均存在局限性。

现有对齐策略的局限性

  1. 参数规模的不一致:LLM的参数规模已经达到1000亿,而广泛使用的VLLM的视觉编码器仍在10亿参数左右。这种差距可能导致LLM的能力无法被充分利用。
  2. 特征表示的不一致:在纯视觉数据上训练的视觉模型或与BERT系列对齐的模型往往与LLM存在表示上的不一致。
  3. 连接效率低下:“胶水”层通常是轻量的、随机初始化的,可能无法捕捉到多模态理解和生成所需的丰富的跨模态交互和依赖关系。

InternVL引入全新的对齐策略

渐进式的对齐训练策略,从海量噪声数据上的对比学习开始,逐渐过渡到高质量数据上的生成式学习。通过这种方式,我们充分利用了互联网上各种来源的海量图像-文本对数据,得到了经过良好对齐的视觉编码器和语言中间件。 细节将在下文详解。

image

InternVL方法与模型详解

如图所示,与传统的仅使用视觉骨干网络的方法以及双编码器的模型不同,我们提出的InternVL包含了一个视觉编码器InternViT-6B和一个语言中间件QLLaMA

  • InternViT-6B:60亿参数的ViT模型,通过自定义结构超参数,实现了性能、效率和稳定性之间的良好平衡。
  • QLLaMA: 80亿参数的语言中间件,使用多语言增强的LLaMA-7B进行初始化。它可以为图像-文本对的对比学习提供鲁棒的多语言表示,或者作为连接视觉编码器和现有的LLM解码器的桥梁。
    image

配合全新的渐进式对齐策略,形成了InternVL强大的的视觉-语言多模态能力。

Part1. 模型设计

大规模视觉编码器: InternViT-6B

我们基于原始ViT结构来构建InternVL的视觉编码器。为了与LLM的规模相匹配,我们将视觉编码器扩大到了60亿参数,从而得到了InternViT-6B模型。为了在准确性、速度和训练稳定性之间取得较好的权衡,我们对InternViT-6B进行了超参数搜索,主要包括模型深度(32,48,64,80)、注意力头的维度(64,128),以及MLP的比率(4,8)。我们在LAION-en数据集的一个100M子集上,使用对比学习来对比各种6B模型变体的准确性、速度和稳定性,通过实验最终确定了现在的模型结构。

image

语言中间件: QLLaMA

语言中间件QLLaMA旨在进一步对齐视觉和语言特征。QLLaMA在上一阶段LLaMA权重的基础之上,额外添加了随机初始化的96个可学习Query以及交叉注意力层(共有10亿参数)。通过这种方式,QLLaMA可以将视觉特征平滑地整合到语言模型中,从而进一步增强了视觉特征与语言特征的对齐程度。**这里是BLIP2的做法 **

Part2. 渐进式对齐策略

InternVL的对齐分为三个渐进式阶段,包括视觉-语言对比训练、视觉-语言生成训练和有监督微调(SFT)。这些阶段有效地利用了互联网上不同来源的开源数据。通过这种策略,我们的训练从带有噪声的图像-文本对逐渐过渡到高质量的标题、视觉问答和多模态对话数据集。

第一阶段,我们通过对比学习将InternViT-6B与多语言的LLaMA-7B对齐,使用海量的公开多语言图文对数据集,包括LAION-en、LAION-multi、LAION-COCO、COYO、Wukong等。我们对这些数据集进行轻微的过滤来剔除极端异常数据。原始数据集总共包含60.3亿图文对,经过清理后(包括剔除下载失败的样本)剩下49.8亿图文对。

image

第二阶段,我们通过交叉注意力来连接InternViT-6B和QLLaMA,并采用生成式的训练策略。具体来说,QLLaMA 继承了第一阶段的LLaMA-7B的权重。我们保持 InternViT-6B 和 QLLaMA 的权重不变,只训练添加的可学习Query和交叉注意力层。在这一阶段,我们根据文本的质量进一步过滤了低质量的数据,将其从第一阶段的49.8亿个图文对减少到10.3亿个图文对。 这一阶段的损失函数由图文对比损失(ITC)、图文匹配损失(ITM)和图像引导的文本生成损失(ITG)组成。这使得Query能够提取鲁棒的视觉表示,并与以LLM为初始化的QLLaMA进一步对齐特征空间。

有监督微调(SFT)

为了展示InternVL在创建多模态对话系统方面的有效性,我们将其与现有的LLM解码器(例如,Vicuna或InternLM)通过一个MLP层连接,并进行有监督微调(SFT)。我们在互联网上收集了一系列高质量的指令数据,总共包含约为400万个样本。对于非对话数据集,我们参考LLaVA-1.5的方法进行格式转换。由于QLLaMA和LLM解码器具有相似的特征空间,即使冻结LLM解码器,选择仅训练MLP层,我们仍然可以实现不错的性能。这种方法不仅加快了SFT的速度,还保持了LLM的原始语言能力。

image

模型使用

InternVL的不同使用方式通过灵活组合视觉编码器和语言中间件,InternVL可以支持各种视觉或视觉-语言任务,堪称“瑞士军刀”版基础模型 ,你可以用它:

1.做纯视觉任务的主干网络:InternViT-6B可以替代ViT、ResNet,直接作为骨干网络;

2.替代CLIP:对于对比式任务,我们有两种使用方式,分别是InternVL-C(ontrastive)和InternVL-G(enerative),如图4(a)(b)所示。我们对InternViT-6B的输出特征或者QLLaMA的Query特征做attention pooling得到视觉特征,将QLLaMA的EOS token对应的特征作为文本特征,从而可以支持图文检索等任务;

3.用在LLaVA等视觉对话模型上:对于多模态对话,我们将InternVL作为视觉特征提取器:既可以单独使用InternViT-6B,也可以上图(c)(d)所示,将InternViT-6B与QLLaMA作为整体。

image

实验

InternVL 1.1

🔖 https://internvl.github.io/blog/2024-01-24-InternVL-1.1/

InternVL-Chat-V1-1,其结构类似于 LLaVA,包括 ViT、MLP 投影器和LLM。如图所示,将 InternViT-6B 通过简单的 MLP 投影器连接到 LLaMA2-13B。请注意,这里使用的 LLaMA2-13B 不是原始模型,而是通过逐步预训练和微调 LLaMA2-13B 基础模型以适应中文任务而获得的内部聊天版本。总体而言,模型总共有 19B参数。

image

InternVL 1.5

项目地址:

🔖 https://internvl.github.io/blog/2024-04-30-InternVL-1.5/

模型结构

image

InternVL 1.5整体结构包括3个部分:ViT、MLP Projector和LLM。

  • ViT模型选择的是一个6B的模型InternViT-6B-448px-V1.5 ,作为强视觉特征提取器;
  • MLP Projector则是负责将视觉特征和语言模型的特征空间进行对齐,InternLM2-Chat-20B的embedding维度是6144,MLP Projector会将视觉特征转换到相同的维度;
  • LLM选择的是自家的InternLM2-Chat-20B。
    InternVL 1.5的总体参数量是26B。并且,在整体框架中我们还看到另外两个技术手段,分别是Pixel ShuffleDynamic High Resolution;Pixel Shuffle主要是用来重排ViT提取到的pixel feature,目的是减少visual token的数量并保持特征信息不丢失,而Dynamic High Resolution,即动态高分辨率,则是为了让ViT模型能够尽可能获取到更细节的图像信息,提高视觉特征的表达能力。接下来,本文会对这个两个技术做详细的解析。

Dynamic High-Resolution

image

Dynamic High Resolution具体的做法为:

  • 预设纵横比集合:例如{1:1, 1:2, 1:3, 1:4, 1:5, 1:6, 2:3, 3:2 …,2:6}多种可能的组合(这取决于自定义min和max两个变量,后面代码会说到)
  • 最优匹配:对于每个输入图像,系统会计算其纵横比,并与预定义的集合进行比较,找出差异最小的纵横比。那么如果有多个匹配的纵横比(即并列最小差异)怎么办?比较原始图像面积与特定纵横比下的图像面积来实现的。如果特定纵横比下的图像面积大于原始图像面积的一半,那么这个纵横比会被选为最优纵横比。
  • patch 分割:输入图像被动态分割成448x448的patch ,patch的数量是根据图像匹配的纵横比和分辨率 (在1到12之间变化)。
  • 图像分割与缩略图(Image Division & Thumbnail)
    调整图像分辨率:一旦确定了合适的纵横比,图像将被调整到相应的分辨率。例如,一个800×1300的图像将被调整到896×1344。
    分割图像:调整后的图像被分割成448×448像素的瓦片。在训练阶段,根据图像的纵横比和分辨率,瓦片的数量可以在1到12,推理时候是1到40
    全局上下文缩略图:同时会resize 原始图像到448x448,帮助模型理解整体场景。
    核心代码如下,比较简单不做注释了:
from transformers import AutoTokenizer, AutoModel
import torch
import torchvision.transforms as T
from PIL import Image

from torchvision.transforms.functional import InterpolationMode


IMAGENET_MEAN = (0.485, 0.456, 0.406)
IMAGENET_STD = (0.229, 0.224, 0.225)


def build_transform(input_size):
    MEAN, STD = IMAGENET_MEAN, IMAGENET_STD
    transform = T.Compose([
        T.Lambda(lambda img: img.convert('RGB') if img.mode != 'RGB' else img),
        T.Resize((input_size, input_size), interpolation=InterpolationMode.BICUBIC),
        T.ToTensor(),
        T.Normalize(mean=MEAN, std=STD)
    ])
    return transform


def find_closest_aspect_ratio(aspect_ratio, target_ratios, width, height, image_size):
    best_ratio_diff = float('inf')
    best_ratio = (1, 1)
    area = width * height
    for ratio in target_ratios:
        target_aspect_ratio = ratio[0] / ratio[1]
        ratio_diff = abs(aspect_ratio - target_aspect_ratio)
        if ratio_diff < best_ratio_diff:
            best_ratio_diff = ratio_diff
            best_ratio = ratio
        elif ratio_diff == best_ratio_diff:
            if area > 0.5 * image_size * image_size * ratio[0] * ratio[1]:
                best_ratio = ratio
    return best_ratio

#动态分辨率预处理
def dynamic_preprocess(image, min_num=1, max_num=6, image_size=448, use_thumbnail=False):
    orig_width, orig_height = image.size
    aspect_ratio = orig_width / orig_height

    # calculate the existing image aspect ratio
    target_ratios = set(
        (i, j) for n in range(min_num, max_num + 1) for i in range(1, n + 1) for j in range(1, n + 1) if
        i * j <= max_num and i * j >= min_num)
    target_ratios = sorted(target_ratios, key=lambda x: x[0] * x[1])

    # find the closest aspect ratio to the target
    target_aspect_ratio = find_closest_aspect_ratio(
        aspect_ratio, target_ratios, orig_width, orig_height, image_size)

    # calculate the target width and height
    target_width = image_size * target_aspect_ratio[0]
    target_height = image_size * target_aspect_ratio[1]
    blocks = target_aspect_ratio[0] * target_aspect_ratio[1]

    # resize the image
    resized_img = image.resize((target_width, target_height))
    processed_images = []
    for i in range(blocks):
        box = (
            (i % (target_width // image_size)) * image_size,
            (i // (target_width // image_size)) * image_size,
            ((i % (target_width // image_size)) + 1) * image_size,
            ((i // (target_width // image_size)) + 1) * image_size
        )
        # split the image
        split_img = resized_img.crop(box)
        processed_images.append(split_img)
    assert len(processed_images) == blocks
    if use_thumbnail and len(processed_images) != 1:
        thumbnail_img = image.resize((image_size, image_size))
        processed_images.append(thumbnail_img)
    return processed_images


def load_image(image_file, input_size=448, max_num=6):
    image = Image.open(image_file).convert('RGB')
    transform = build_transform(input_size=input_size)
    images = dynamic_preprocess(image, image_size=input_size, use_thumbnail=True, max_num=max_num)
    pixel_values = [transform(image) for image in images]
    pixel_values = torch.stack(pixel_values)
    return pixel_values

额外的实验结果是,训练在1-12 块Patch的范围内,但是推理时候泛化到了40个,(开始说过VIT模型输出是256个token,所以256x(40+1)=10496),实验证明24块为最优效果。

Pixel Shuffle

为了增强模型对大分辨率图像的支持,作者引入了pixel shuffle的操作降低visual token数量

Pixel Shuffle在超分任务中是一个常见的操作,PyTorch中有官方实现,即nn.PixelShuffle(upscale_factor) 该类的作用就是将一个tensor中的元素值进行重排列,假设tensor维度为[B, C, H, W], PixelShuffle操作不仅可以改变tensor的通道数,也会改变特征图的大小,先看官方文档:

image

InternVL 1.5中是自己写了一个pixel shuffle的操作,他的这个操作刚好相反,是一个下采样操作,实际使用的scale_factor为0.5,其实就相当于把更多的像素保存在channel维度上,所以pixel shuffle后,H, W变小了,channel数变多了。对于scale_factor,比如0.5,pixel shuffle将输入[N, W, H, C]转换成shape为[N, H x scale, W x scale, C//(scale^2)]的Tensor。

image

可见,对于一个448x448的image来说,经过ViT + pixel shuffle后,visual token数由原来的32x32下降到了16x16,token数下降到了原来的1/4,既保留了原始的feature信息,又达到了减少上下文长度的效果。

模型训练

InternVL 1.5训练分为2个阶段。

阶段一(pre-training):对MLP Projector和InternViT-6B模型做预训练,LLM底座权重冻结,这个阶段主要是针对视觉特征提取器进行优化;

阶段二(finetuning):InternViT-6B + MLP Projector + InternLM2-20B总共26B的参数全部参与训练,上下文长度设置为4096,并且采用和LLaVA一样的prompt格式。

值得一提的是在finetuning的数据中, 由于之前的模型对非英语的支持不是非常好, 作者加入了 translation的pipeline ,

image

InternVL2

2024年7月份,InternVL团队发布了InternVL 2.0,效果比InternVL 1.5更好。InternVL 2.0整体的网络结构和InternVL 1.5是一样的,Pixel Shuffle和Dynamic High Resolution的技术从InternVL 1.5继承了下来。但是InternVL 2.0进一步支持了医疗图像和视频作为输入,在功能上是比较大的变化。

image

相对于InternVL 1.5,这次更新的InternVL 2.0主要的创新有:

  • 提出了渐进式对齐的训练策略:实现了与LLM原生对齐的视觉基座模型,渐进式训练策略使得模型从小到大,数据从粗到细训练。InternVL2.0以相对较低的成本完成了大模型的训练。这种方法在有限的资源下表现出了出色的性能。
  • 多模态输入:通过一组统一的参数,InternVL2.0模型支持多种输入模态,包括文本、图像、视频和医疗数据。
  • 多任务输出:基于VisionLLMv2,InternVL2.0模型支持各种输出格式,例如图像、边界框和掩模,具有广泛的多功能性。并且,通过将 MLLM 与多个下游任务解码器连接,InternVL2 可以推广到数百个视觉语言任务,同时实现与专家模型相当的性能。
  • InternVL2 系列包括从 1B 模型,适用于边缘设备,到 108B 模型,后者功能显著更强大。随着更大规模的语言模型,InternVL2-Pro 展示了卓越的多模态理解能力,在各种基准测试中与商业闭源模型的性能相匹配
    image

代码解读

数据处理

这里和mini-gemini的比较相似(大家可能做的可能都比较相似), 只不过这里会分成几个函数分别处理不同形式的数据组合:multi_modal_multi_image_get_itemmulti_modal_get_itemvideo_get_itempure_text_get_item

def __getitem__(self, i) -> Dict[str, torch.Tensor]:
    i = i % len(self.raw_data)
    while True:
            data_item = json.loads(self.raw_data[i])
            if 'image' in data_item and len(data_item['image']) != 0:
                if type(data_item['image']) == list:
                    ret = self.multi_modal_multi_image_get_item(data_item)
                else:
                    ret = self.multi_modal_get_item(data_item)
            elif 'video' in data_item and data_item['video'] is not None and data_item['video'] != '':
                ret = self.video_get_item(data_item)
            else:
                ret = self.pure_text_get_item(data_item)
            break
    return ret

这里以单图的多模态数据处理方式举例说明:

  • load image
  • dynamic_image_size 动态处理图像,变成多个\(448\times448\) 的图像
  • 交给特定的process去处理输入和label
    def multi_modal_get_item(self, data_item):
        # Build transformation function
        transform = self.get_transform()
    
        # Ensure the first conversation contains an image placeholder
        if '<image>' not in data_item['conversations'][0]['value']:
            data_item['conversations'][0]['value'] = '<image>\n' + data_item['conversations'][0]['value']
    
        # Merge the image path
        image_path = self.get_image_path(data_item['image'])
    
        # Load the image using tcs_loader if available, otherwise use PIL
        image = self.load_image(image_path)
    
        if self.dynamic_image_size:  # If dynamic image size is enabled, preprocess the image dynamically
            images = dynamic_preprocess(image, min_num=self.min_dynamic_patch, max_num=self.max_dynamic_patch,
                                        image_size=self.image_size, use_thumbnail=self.use_thumbnail)
        else:  # Otherwise, use the original image as a single patch
            images = [image]
    
        # Apply the transformation to each image and stack the results into a tensor
        pixel_values = [transform(image) for image in images]
        pixel_values = torch.stack(pixel_values)
    
        # Ensure that there is only one patch if dynamic image size is not enabled
        num_patches = pixel_values.size(0)
        if not self.dynamic_image_size:
            assert num_patches == 1, f'The number of patches should be 1, but got {num_patches}.'
    
        # Select the appropriate preprocessing function based on the template name
        preprocess_function = self.get_preprocess_function()
    
        # Preprocess the conversations and generate the return dictionary
        ret = preprocess_function(self.template_name, [deepcopy(data_item['conversations'])],
                                  self.tokenizer, [self.num_image_token * num_patches],
                                  group_by_length=self.group_by_length, ds_name=self.ds_name)
    
        # Create the final return dictionary
        ret = dict(
            input_ids=ret['input_ids'][0],
            labels=ret['labels'][0],
            attention_mask=ret['attention_mask'][0],
            pixel_values=pixel_values,
            image_flags=torch.tensor([1] * num_patches, dtype=torch.long)
        )
        return ret
    

处理token的形式也和mini-gemini的时候差不多

  • 将数据转换为对话模板
  • 对imgae token 部分进行填充IMG_CONTEXT_TOKEN