掘金 人工智能 07月04日 09:21
代码层面上解读Florence2模型,专用于视觉任务的小体积语言模型
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入探讨了微软发布的 Florence2 模型,一个体积小巧但功能强大的视觉语言模型。文章详细介绍了模型的加载方法、适用任务,包括图像描述、目标检测、语义分割和OCR等,并分析了其推理过程,包括输入预处理、视觉编码DaViT结构以及语言模型生成文本的细节。此外,文章还提到了在模型使用过程中遇到的问题和技术细节,为研究者和开发者提供了宝贵的参考。

💡Florence2 是微软推出的一个体积较小的视觉语言模型,适用于多种视觉任务,如图像描述、目标检测和OCR等。

⚙️加载Florence2模型时,官方提供的加载方式可能较慢,但可以通过手动下载模型仓库文件并创建相应文件夹来加速加载。

🖼️Florence2通过使用不同的任务标识,如 <CAPTION>、<OD>等,结合预先转换的任务描述,来实现不同的视觉任务。

🔍模型目标检测任务中,输出的坐标基于归一化到[0, 999]的网格,且label只能是英文,语义分割任务使用与目标检测相同的特殊token。

📝Florence2的推理过程包括输入预处理,使用DaViT进行视觉编码,以及语言模型生成文本,其中DaViT包含空间和通道双重注意力机制。

1. 总览

Florence2 是微软于 2024 年 6 月推出的专用于视觉任务的小体积语言模型,large 版 0.77B 大小,适用于目标检测、对象分割、提取文字等图片模态任务。只支持英文,对预训练范围外的任务效果差。

本文记录了调试 microsoft/Florence-2-large-ft 模型时的一些发现,包括如何使用模型、视觉编码 DaViT、模型结构等内容。

总之 Florence2 是个很有价值的研究,基于 Encoder-Decoder 的 transformer 模型,开放权重、示例完备,直接用和微调都挺好的。代码写得稍微不太考究(搞错了 _decoder_start_token_tensor 实属不应该,好在不大影响推理效果),好在够用,能出效果。

2. 改善极其缓慢的加载

由于未知原因,使用官方 HuggingFace 页面上提供的模型加载代码运行极其缓慢。

已经有人发出 issue,直到现在(20250702)仍未得到解决。好在 issue 里给出了另一种加载方式,速度快得多。需要提前下载模型仓库,并且由于 transformers 库没有内置 Florence_2,需要手动下载仓库中的 py 文件创建一个文件夹。

from pathlib import Pathfrom Florence_2.configuration_florence2 import Florence2Configfrom Florence_2.modeling_florence2 import Florence2ForConditionalGenerationimport torchfrom transformers import AutoProcessorfrom PIL import Imagecheckpoint = Path("/media/dolen/red_3t/models/Florence-2-large")config_path = checkpoint / "config.json"pretrained_model_path = checkpoint / "pytorch_model.bin"florence2_config = Florence2Config.from_pretrained(config_path)model = Florence2ForConditionalGeneration(florence2_config)model.load_state_dict(torch.load(pretrained_model_path, map_location="cpu"))model = model.to("cuda")model = model.to(torch.float16)processor = AutoProcessor.from_pretrained(checkpoint, trust_remote_code=True)

3. Florence2 适用的任务

大模型的一大好处是,只需要修改 prompt 就可以让模型适应不同的任务目标。Florence2 预训练了图像描述、OCR、目标检测等任务,只需要将任务标识输入到 processor 的 text 中去就可以指定对应任务。

任务标识会像是 <CAPTION> <OD>实际输入到模型的文字不是标识本身,而是会在 processor 里预先转换为任务描述。例如 <CAPTION> 对应 “What does the image describe?” 这句话。稍微有点脱裤子放屁,但毕竟是小模型没有那样强大的泛化能力,限定提示词保证性能倒也说得过去。标识与任务描述的对应字典存储在 processor 的 self.task_prompts_without_inputsself.task_prompts_with_input 中。

图片描述:

目标检测(绘制方框):

语义分割:

指定区域描述:

OCR 文字识别:

3.1. 任务:目标检测

使用 <OD> 标识,可以让模型进行目标检测。

Florence2 的 toekenizer 在 BartTokenizer 的基础上添加了一系列指示图像位置的特殊 token,像是 <loc_162> <loc_11> 这种形式,从 0 到 999 总共一千个。推理出来的文本会是这个形式:

</s><s>car<loc_52><loc_334><loc_932><loc_774></s>

四个 <loc_xxx> 分别代表 x1 y1 x2 y2,即非常经典的标注格式:方框的左上角坐标、方框的右下角坐标。

模型输出的坐标无视图像原分辨率,长宽都统一归一化到 [0, 999],视原图为 1000×1000 的网格。具体来说,<loc_0><loc_0> 含义是第一格(最左上角那个网格)的中点,<loc_999><loc_999> 是最后一格(最右下角那个网格)的中点。

注意,processor.post_process_generation() 不仅能将模型输出处理为真实的标注框坐标,还会强制 label 为 ascii,即检测 bbox 模式下只能是英文 label,label 不能有中文。

phrase = phrase.encode('ascii',errors='ignore').decode('ascii')

只有这里会限死英文。其他任务,例如 OCR 就没这样的限制。挺奇怪的。

3.2. 任务:语义分割

使用 <REFERRING_EXPRESSION_SEGMENTATION> 标识,可以让模型进行语义分割。

语义分割用到的特殊 token 与目标检测用到的一样,都是 <loc_162> <loc_11> 这种形式。推理出来的文本会是这个形式(太长了中间省略一部分):

'</s><s><loc_268><loc_383><loc_282><loc_375><loc_290><loc_370>···</s>'

每两个位置 token 代表一个点坐标,围出一个多边形区域。转换为数字并缩放坐标后是可以用 PIL.Image.polygon() 绘制出来的。

4. 推理过程

4.1. 输入预处理

模型的 processor 包含 image_processor(CLIPImageProcessor)和 tokenizer(BartTokenizerFast),复用的 transformers 库里的类。分别处理文本和图片

输入的像是 <CAPTION> 这种任务标识会先转换为任务文字描述再传给 tokenizer。注意 processor 要求输入的 text 必须是预制的任务标识之一,否则会报错。

图片不论原长宽比如何都会会强制 resize 到 768×768(虽说图像编码器是支持任意分辨率的)。图片数值从 [0, 255] 缩放到 [0, 1],之后使用 mean=[0.485, 0.456, 0.406] 和 std=[0.229, 0.224, 0.225] 进行归一化,

如此经过 tokenizer 和 image_processor 处理后,得到 input_ids 和 pixel_values。接下来就把两者输入到模型中去。

4.2. 模型推理:视觉模型

为了方便表达,本节结合伪代码描述模型。其中涉及的具体数值只对第一个层有效,请注意。

模型对象 Florence2ForConditionalGeneration 包含图像编码器 self.vision_tower,是 DaViT 实例,编写在自己的脚本中而没集成在 timm 或者 transformers 库中。经过 self.vision_tower 处理后获得维度为 [batch, 577, 2048] 的 tensor。

4.2.1. DaViT

DaViT 提出于论文 DaViT: Dual Attention Vision Transformers (2022),微软家的。大致思路是在空间和通道分别施加注意力机制,形成双注意力。

DaViT 由一系列 ConvEmbed 实例 self.convs 和 attention 层 self.blocks 交错构成。传入 [b c h w] 维度的图像 x,会经过如此流程:

for conv, block in zip(self.convs, self.blocks):    x = conv(x)    x = block(x)

每一次经过 conv 都会降低分辨率和增加维度。

conv() 流程伪代码:

conv = [    Conv2d(3, 256, kernel_size=7, stride=4, padding=3),    rearrange('b c h w -> b (h w) c'),    LayerNorm(256),]

注意到会有 rearrange 压平 hw 维度的步骤。为了保留长宽信息 conv() 会额外返回 input_size 指示 Conv2d 后、压平前的长宽,输入到 block() 中。

blocks 层先后包括 SpatialBlock 实例和 ChannelBlock 实例。

block = [    SpatialBlock()    ChannelBlock()]

两种 block 的大致结构如下,用 Residual() 表达残差。

# 整体 Block 直接写成 Residual 的序列SpatialBlock = [    Residual(DepthWiseConv2d),  # 相当于给每个通道一个 scale    Residual(        LayerNorm,        WindowAttention,  # 空间 attention,在 patch 内部进行注意力    ),    Residual(DepthWiseConv2d),    Residual(        LayerNorm,        Mlp,    ),]ChannelBlock = [    Residual(DepthWiseConv2d),    Residual(        LayerNorm,        WindowAttention,  # 通道 attention,qkv 正常获得但在计算 score 时获得的是 channel 的 score        DropPath(0.004),  # 类似于 Dropout 的正则化方法,但会让部分像素的所有通道全为 0,而不是随机置 0。定义于 timm 库    ),    Residual(DepthWiseConv2d),    Residual(        LayerNorm,        Mlp,  # 很标准的 mlp 结构。没有使用 GLU(Gated Linear Unit)那一套        DropPath(0.004),    ),]

可见,两者基本流程都是 “通道 scale” -> attention -> “通道 scale” -> MLP。

结构细节:

DepthWiseConv2d = [Conv2d(256, 256, kernel_size=3, stride=1, padding=1, groups=256)]WindowAttention = [  # window_size=12    F.pad,  # 填充到 window_size 的整倍数    rearrange('b (h s1) (w s2) c -> (b h w) (s1 s2) c' s1=window_size, s2=window_size),    MultiHeadAttention(heads=8, bias=True),    x = x[:, :H, :W, :],  # 若进行了 F.pad,此步骤用于还原 x]ChannelAttention = [    # 与标准 attention 的唯一区别是,计算 score 时不是 q @ k.transpose(-2, -1) 而是 q.transpose(-1, -2) @ k    MultiHeadAttention(heads=8, bias=True)  ]Mlp = [    Linear(256, 1024)    GELU(approximate='none')    Linear(1024, 256)]

4.2.2. 图像编码后的处理

首先是位置编码。self.image_pos_embed,两个可学习位置编码各自占据一半 channel,提供长和宽的位置信息。与 DaViT 的处理结果直接相加。

额外有 self.visual_temporal_embed,一维正余弦编码,似乎是用来让模型能够分辨多张图的先后顺序。与上一步的处理结果直接相加。但代码硬编码写死了 T=1,并没有输入多张图的途径。保留这个层也许只是为了维持兼容性。

然后,获得池化结果。spatial_avg_pool_x 是对像素维度取平均,temporal_avg_pool_x 是对时间维度取平均(如刚刚所说,由于只能有一张图,这就相当于复制了一份 x)。然后令 x = torch.cat([spatial_avg_pool_x, temporal_avg_pool_x], dim=1),在像素维度拼接出一个新 x

随后经过一个没有 bias 的线性层将 x 维度从 2048 映射到 1024,作为最终编码结果输出出去。

4.3. 模型推理:语言模型

现有图像特征 tensor image_features 和文本提示词特征 tensor inputs_embeds。两者按先图后文的顺序拼接后,传给语言模型作为编码器的输入,开始进行文本生成。

语言模型是 Florence2LanguageForConditionalGeneration 实例,调用其 .generate() 方法进行文本生成。

此时 transformers 库会给出警告,Florence2LanguageForConditionalGeneration 没有继承于 transformers 库的 GenerationMixin,transformers 从 v4.50 开始若不继承 GenerationMixin 则会失去 .generate() 的能力。

实际上这是因为 Florence2 选择继承自己脚本的 GenerationMixin。理由可能是为了避免 transformers 库版本影响推理?

Florence2 选择使用 Encoder-Decoder 的 transformer 结构,hidden_size 为 1024,编解码器各自 12 层。

MLP 部分没有使用 GLU,而是很朴素的 Linear(1024, 4096) -> GELU -> Dropout -> Linear(4096, 1024);没有使用 Pre-Norm 方案,归一化层放在了注意力和前馈层的后面;使用可学习位置编码。

似乎是为了避免 float16 精度训练时 Inf 和 NaN 值影响训练稳定性,Encoder 的 forward 代码里额外有把这些无效值替换为 float16 表达上限减去 1000 的值的操作。记录一下万一能用上:

if hidden_states.dtype == torch.float16 and (    torch.isinf(hidden_states).any() or torch.isnan(hidden_states).any()):    clamp_value = torch.finfo(hidden_states.dtype).max - 1000    hidden_states = torch.clamp(hidden_states, min=-clamp_value, max=clamp_value)

默认的 generation_config 似乎有问题,其中的 _decoder_start_token_tensor 是代表 </s>2 而不是代表 <s>0,导致 decoder 会先生成一个 <s> 再正式输出回答。最终就会形成 </s><s>balabala</s> 这样奇怪的输出,虽说不影响结果吧但有点膈应。

但不能随便修正,processor.post_process_generation() 里似乎也有个 bug,而以 </s> 开头刚好能跳过这个 bug。唔。

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

Florence2 视觉语言模型 DaViT 目标检测 语义分割
相关文章