代码层面上解读Florence2模型,专用于视觉任务的小体积语言模型

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 文件创建一个文件夹。

python 复制代码
from pathlib import Path

from Florence_2.configuration_florence2 import Florence2Config
from Florence_2.modeling_florence2 import Florence2ForConditionalGeneration
import torch
from transformers import AutoProcessor
from PIL import Image


checkpoint = 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 中。

图片描述:

  • <CAPTION>,描述图片,会输出一段纯文字的描述
    • 'What does the image describe?'
  • <DETAILED_CAPTION>,同 <CAPTION>,但描述更细致
    • 'Describe in detail what is shown in the image.'
  • <MORE_DETAILED_CAPTION>,同 <CAPTION>,但会用一大段文本去描述图片
    • 'Describe with a paragraph what is shown in the image.'

目标检测(绘制方框):

  • <OD>,目标检测,会输出所有能识别的物体,不能指定要检测目标
    • 'Locate the objects with category name in the image.'
  • <DENSE_REGION_CAPTION>,类似 <OD>,但给出的物体标签更具体
    • 'Locate the objects in the image, with their descriptions.'
  • <REGION_PROPOSAL>,定位物体,但不给出 label(label 为空)
    • 'Locate the region proposals in the image.'
  • <CAPTION_TO_PHRASE_GROUNDING>,模型根据一句话中所描述的物体(可以不止一个),找出图中目标
    • "Locate the phrases in the caption: {input}"

语义分割:

  • <REFERRING_EXPRESSION_SEGMENTATION>,输出给定描述的物体的多边形遮罩(polygon mask)
    • 'Locate {input} in the image with mask'
  • <REGION_TO_SEGMENTATION>,输出给定区域的多边形遮罩。区域需要使用 4 个 <loc123> 特殊 token 指示 x1 y1 x2 y2
    • 'What is the polygon mask of region {input}'

指定区域描述:

  • <REGION_TO_CATEGORY>,输出给定区域的物体类别。区域需要使用 4 个 <loc123> 特殊 token 指示 x1 y1 x2 y2
    • 'What is the region {input}?'
  • <REGION_TO_DESCRIPTION>,输出给定区域的文字描述。区域需要使用 4 个 <loc123> 特殊 token 指示 x1 y1 x2 y2
    • 'What does the region {input} describe?'

OCR 文字识别:

  • <OCR>,输出能识别的所有文字
    • 'What is the text in the image?'
  • <OCR_WITH_REGION>,类似 <OD> 会输出框和 label,而 label 内容会换为对应的文本内容
    • 'What is the text in the image, with regions?'

3.1. 任务:目标检测

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

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

html 复制代码
</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 不能有中文。

python 复制代码
phrase = phrase.encode('ascii',errors='ignore').decode('ascii')

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

3.2. 任务:语义分割

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

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

html 复制代码
'</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,会经过如此流程:

python 复制代码
for conv, block in zip(self.convs, self.blocks):
    x = conv(x)
    x = block(x)

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

  • 分辨率:768 -> 192 -> 96 -> 48 -> 24
  • 维度:3 -> 256 -> 512 -> 1024 -> 2048

conv() 流程伪代码:

python 复制代码
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 实例。

python 复制代码
block = [
    SpatialBlock()
    ChannelBlock()
]

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

python 复制代码
# 整体 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。

结构细节:

python 复制代码
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 的值的操作。记录一下万一能用上:

python 复制代码
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。唔。

相关推荐
夫子3961 小时前
【深度干货】Transformer推理优化完全指南:模型压缩、推理加速与硬件调优
人工智能·llm
智泊AI3 小时前
终于有人把AI大模型训练过程讲明白了!!!
llm
数据智能老司机6 小时前
建构 AI Agent 应用——Agentic 系统的学习机制
架构·llm·agent
数据智能老司机8 小时前
建构 AI Agent 应用——编排
架构·llm·agent
镰刀韭菜1 天前
【AI4S】大语言模型与化学的未来,以及整合外部工具和聊天机器人的潜力
llm·transformer·大语言模型·药物设计·分子发现·chemchat·smiles
数据智能老司机1 天前
建构 AI Agent 应用——工具调用
架构·llm·agent
aopstudio1 天前
llms.txt:为大模型打造的“网站说明书”
人工智能·python·llm·开发者工具
AI大模型API向量引擎3 天前
开发者必看:Luma Video API 对接教程 + 电影级视频生成技巧,NeRF 技术落地实践
llm
AndrewHZ3 天前
【3D图像技术讨论】3A游戏场景重建实战指南:从数据采集到实时渲染的开源方案
人工智能·算法·游戏·3d·开源·llm·colmap
马诗剑3 天前
Qwen3-8B 在华为昇腾平台上的保姆级微调与推理教程 (基于AutoDL)
llm