学習しないで物体検出するLLM [Florence 2]

はじめに

hugging faceのトレンドを見てたらFlorence-2-largeというモデルが上位に上がっていました。

microsoft/Florence-2-large · Hugging Face
We’re on a journey to advance and democratize artificial intelligence through open source and open science.

ReadMeを読んでみると、ImagetoTextのLLMのようですが、どうやら簡単なプロンプトから物体検出タスクを行うモデルのようです。

つまり、Yoloなどに変わる物体検出モデルとして使えるかもしれません。しかもYoloと違ってタスク事に学習しないでも、指示できるのです。

今回の記事ではこのFlorence 2を動かしてみて、どの程度のものが確かめてみます。

環境構築

dev-containerで試しました。以下、Dockerfileとdevcontainer.json、docker-compose.ymlです。

Dockerfile

前に作ったhugging faceの実行環境をベースに構築しました。

FROM nvcr.io/nvidia/pytorch:22.04-py3

ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID

ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8
ENV TZ JST-9
ENV TERM xterm

RUN apt-get update \
    && groupadd --gid $USER_GID $USERNAME \
    && useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME \
    && apt-get install -y sudo \
    && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
    && chmod 0440 /etc/sudoers.d/$USERNAME \
    && apt-get -y install locales \
    && localedef -f UTF-8 -i ja_JP ja_JP.UTF-8

RUN apt-get -y install git

RUN pip install --upgrade pip
RUN pip install --upgrade setuptools

RUN python -m pip install \
    torchvision \
    torchaudio \
    invisible_watermark
RUN python -m pip install \
    accelerate \
    datasets \
    hf-doc-builder \
    huggingface-hub \
    Jinja2 \
    librosa \
    numpy \
    scipy \
    tensorboard \
    transformers \
    pytorch-lightning

RUN python -m pip install \
    omegaconf~=2.3.0 \
    tqdm \
    einops \
    vector_quantize_pytorch \
    vocos \
    IPython \
    nemo_text_processing \
    gradio

RUN python -m pip install \
    timm flash_attn

ENV HF_HOME /work/.cache/huggingface
ENV TORCH_HOME /work/.cache/torchvision

devcontainer.json

名前とサービス名を決定してください。また、必要な拡張機能を入れてください。

{
    "name": "transformers-test",
    "service": "transformers-test",
    "dockerComposeFile": "docker-compose.yml",
    "remoteUser": "vscode",
    "workspaceFolder": "/work",
    "customizations": {
      "vscode": {
        "extensions": [
          "ms-python.python",
          "ms-toolsai.jupyter"
        ]
      }
    }
}

docker-compose.yml

設定したサービス名を間違えないでください。

また、GPUを使うので、GPUが使えるようにdeployを書き足します。

version: '3'
services:
  transformers-test:
    container_name: 'transformers-test-container'
    hostname: 'transformers-test-container'
    build: .
    restart: always
    working_dir: '/work' 
    tty: true
    volumes:
      - type: bind
        source: ..
        target: /work
    ulimits:
      memlock: -1
      stack: -1
    shm_size: '10gb'
    deploy:
      resources:
          reservations:
              devices:
                - capabilities: [gpu]

実行して処理を見る

まずは、ReadMeに書いてあったサンプルコードを動かしてみます。

import requests

from PIL import Image
from transformers import AutoProcessor, AutoModelForCausalLM 


model = AutoModelForCausalLM.from_pretrained("microsoft/Florence-2-large", trust_remote_code=True)
processor = AutoProcessor.from_pretrained("microsoft/Florence-2-large", trust_remote_code=True)

prompt = "<OD>"

url = "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/transformers/tasks/car.jpg?download=true"
image = Image.open(requests.get(url, stream=True).raw)

inputs = processor(text=prompt, images=image, return_tensors="pt")

generated_ids = model.generate(
    input_ids=inputs["input_ids"],
    pixel_values=inputs["pixel_values"],
    max_new_tokens=1024,
    num_beams=3,
    do_sample=False
)
generated_text = processor.batch_decode(generated_ids, skip_special_tokens=False)[0]

parsed_answer = processor.post_process_generation(generated_text, task="<OD>", image_size=(image.width, image.height))

print(parsed_answer)

初回実行時はモデルのダウンロードがあるので、少し待ちます。、モデルサイズは1.5GB程度でした。

実行すると次の出力がでてきました。

{'<OD>': {'bboxes': [[34.23999786376953, 160.0800018310547, 597.4400024414062, 371.7599792480469], [456.0, 97.68000030517578, 580.1599731445312, 261.8399963378906], [450.8800048828125, 276.7200012207031, 554.5599975585938, 370.79998779296875], [95.68000030517578, 280.55999755859375, 198.72000122070312, 371.2799987792969]], 'labels': ['car', 'door', 'wheel', 'wheel']}}

Boxesの位置とクラスが出力されたので、どうやら'<OD>’はObject Detection(物体検出)のタスクを行うプロンプトのようです。

各関数の役割と、その実行時間を軽く見てみます。

import requests

from PIL import Image
from transformers import AutoProcessor, AutoModelForCausalLM 
import time

model = AutoModelForCausalLM.from_pretrained("microsoft/Florence-2-large", trust_remote_code=True)
processor = AutoProcessor.from_pretrained("microsoft/Florence-2-large", trust_remote_code=True)

prompt = "<OD>"

url = "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/transformers/tasks/car.jpg?download=true"
image = Image.open(requests.get(url, stream=True).raw)

s = time.time()
inputs = processor(text=prompt, images=image, return_tensors="pt")
print(f"inputs : {inputs}")
print(f"processor time : {time.time()- s}")

s = time.time()
generated_ids = model.generate(
    input_ids=inputs["input_ids"],
    pixel_values=inputs["pixel_values"],
    max_new_tokens=1024,
    num_beams=3,
    do_sample=False
)
print(f"generated_ids : {generated_ids}")
print(f"model.generate time : {time.time()- s}")

s = time.time()
generated_text = processor.batch_decode(generated_ids, skip_special_tokens=False)[0]
print(f"generated_text : {generated_text}")
print(f"processor.batch_decode time : {time.time()- s}")

s = time.time()
parsed_answer = processor.post_process_generation(generated_text, task="<OD>", image_size=(image.width, image.height))
print(f"parsed_answer : {parsed_answer}")
print(f"processor.post_process_generation time : {time.time()- s}")

実行すると次の出力が得られました。

inputs : {'input_ids': tensor([[    0,   574, 22486,     5,  8720,    19,  4120,   766,    11,     5,
          2274,     4,     2]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]), 'pixel_values': tensor([[[[-1.1418, -0.7479, -0.9192,  ..., -1.9295, -1.9467, -1.9638],
          [-0.7650, -0.7137, -0.9192,  ..., -1.9295, -1.9467, -1.9638],
          [-0.2684, -0.7308, -0.9705,  ..., -1.9467, -1.9638, -1.9809],
          ...,
          [ 0.5707,  0.5707,  0.5707,  ...,  0.3481,  0.3481,  0.3652],
          [ 0.5707,  0.5707,  0.5707,  ...,  0.3823,  0.3823,  0.3652],
          [ 0.5707,  0.5707,  0.5707,  ...,  0.4166,  0.3994,  0.3823]],

         [[-1.3880, -0.9853, -1.1604,  ..., -1.6506, -1.6681, -1.6856],
          [-1.0028, -0.9503, -1.1604,  ..., -1.6506, -1.6681, -1.6856],
          [-0.4951, -0.9678, -1.2129,  ..., -1.6681, -1.6856, -1.7031],
          ...,
          [ 0.6078,  0.6078,  0.6078,  ...,  0.3978,  0.3978,  0.4153],
          [ 0.6078,  0.6078,  0.6078,  ...,  0.4328,  0.4328,  0.4153],
          [ 0.6078,  0.6078,  0.6078,  ...,  0.4678,  0.4503,  0.4328]],

         [[-1.1770, -0.7761, -0.9504,  ..., -1.4907, -1.5081, -1.5256],
          [-0.7936, -0.7413, -0.9504,  ..., -1.4907, -1.5081, -1.5256],
          [-0.2881, -0.7587, -1.0027,  ..., -1.5081, -1.5256, -1.5430],
          ...,
          [ 0.8274,  0.8274,  0.8274,  ...,  0.5659,  0.5659,  0.5834],
          [ 0.8274,  0.8274,  0.8274,  ...,  0.6008,  0.6008,  0.5834],
          [ 0.8274,  0.8274,  0.8274,  ...,  0.6356,  0.6182,  0.6008]]]])}
processor time : 0.07494592666625977
generated_ids : tensor([[    2,     0,     0,     0,  5901, 50322, 50602, 51202, 51043, 11219,
         50981, 50472, 51175, 50814, 13630, 50973, 50845, 51135, 51041, 50418,
         50853, 50579, 51042,     2]])
model.generate time : 5.702800273895264
generated_text : </s><s><s><s>car<loc_53><loc_333><loc_933><loc_774>door<loc_712><loc_203><loc_906><loc_545>wheel<loc_704><loc_576><loc_866><loc_772><loc_149><loc_584><loc_310><loc_773></s>
processor.batch_decode time : 0.00020122528076171875
parsed_answer : {'<OD>': {'bboxes': [[34.23999786376953, 160.0800018310547, 597.4400024414062, 371.7599792480469], [456.0, 97.68000030517578, 580.1599731445312, 261.8399963378906], [450.8800048828125, 276.7200012207031, 554.5599975585938, 370.79998779296875], [95.68000030517578, 280.55999755859375, 198.72000122070312, 371.2799987792969]], 'labels': ['car', 'door', 'wheel', 'wheel']}}
processor.post_process_generation time : 0.0013086795806884766

それぞれの出力された変数と関数の役割についてまとめます。

関数名出力変数 役割実行時間(sec)
processorinputs画像をタスク(プロンプト)によって情報が埋め込まれたベクトルへ変換0.0749
model.generategenerated_idsおそらくトークンidのリストを生成5.7028
processor.batch_decodegenerated_textトークンidからトークンにデコード0.0002
processor.post_process_generationparsed_answerトークンリストを整形して、辞書にする0.0013


なるほど、文字(トークン)のリストとして物体検出の情報を吐き出しているみたいですね。

つまり、未知(トークンとして学習していない)の物体は検出できないようです。

他のタスクへの適用

ipynb形式のサンプルコードも置いてありました。複数のタクスで推論してみた例を載せています。

WordPress › エラー

検証しているタスクを取り上げると、

  • テキスト情報無しで推論
    • キャプション (‘<CAPTION>’) : 画像の説明を文章で出力
    • 物体検出 (‘<OD>’) : 物体検出。ラベルと物体のボックスデータを吐き出す
    • 物体検知とキャプション (‘<DENSE_REGION_CAPTION>’) : 物体を検出してそれぞれの説明をする
    • 領域検出 (‘<REGION_PROPOSAL>’) : 分けられそうなボックス領域を提案してくれる
  • テキスト情報ありで推論
    • キャプションから物体検出 (‘<CAPTION_TO_PHRASE_GROUNDING>’) : キャプションを与えると、画像からそのボックス領域を検出
    • 対象の領域を塗る (‘<
    • REFERRING_EXPRESSION_SEGMENTATION>’) : 対象をテキストで与えると、その領域を塗れる
    • ボックスデータからの領域検出 (‘<REGION_TO_SEGMENTATION>’) : ボックスデータを与えると、写っている領域を塗ってくれる
    • 物体検出とOCR (‘<OPEN_VOCABULARY_DETECTION>’) : 物体と文字を検出する
  • OCR
    • 単純なOCR (‘<OCR>’) : 画像中の文字を検出
    • 領域とOCR (‘<OCR_WITH_REGION>’) : 文字とその領域を検出

などがありました。かなり多くのタスクができるようですね。

すこし手元の画像でも試してみましょう。画像はpexelsからもらいました。

Just a moment...

物体検出タスク

タスクODを試してみます。

import requests
from PIL import Image
from transformers import AutoProcessor, AutoModelForCausalLM 
import matplotlib.pyplot as plt  
import matplotlib.patches as patches

model = AutoModelForCausalLM.from_pretrained("microsoft/Florence-2-large", trust_remote_code=True)
processor = AutoProcessor.from_pretrained("microsoft/Florence-2-large", trust_remote_code=True)

prompt = "<OD>"
task = prompt

url = "https://images.pexels.com/photos/26202784/pexels-photo-26202784.jpeg?cs=srgb&dl=pexels-diogo-miranda-2044514-26202784.jpg&fm=jpg"
image = Image.open(requests.get(url, stream=True).raw)

inputs = processor(text=prompt, images=image, return_tensors="pt")
generated_ids = model.generate(
    input_ids=inputs["input_ids"],
    pixel_values=inputs["pixel_values"],
    max_new_tokens=1024,
    num_beams=3,
    do_sample=False
)
generated_text = processor.batch_decode(generated_ids, skip_special_tokens=False)[0]
parsed_answer = processor.post_process_generation(generated_text, task=task, image_size=(image.width, image.height))

fig, ax = plt.subplots() 
ax.imshow(image)
for bbox, label in zip(parsed_answer[task]['bboxes'], parsed_answer[task]['labels']):  
    x1, y1, x2, y2 = bbox  
    rect = patches.Rectangle((x1, y1), x2-x1, y2-y1, linewidth=1, edgecolor='r', facecolor='none')  
    ax.add_patch(rect)  
    plt.text(x1, y1, label, color='white', fontsize=8, bbox=dict(facecolor='red', alpha=0.5))  
ax.axis('off') 
plt.savefig('test.jpg')

ちゃんと犬が検出できました。

精度も悪く無いと思います。さすがにfine-turningしたYoloのほうが精度は良さそうですが。

色塗り

物体の領域に色を塗ってみたいと思います。

import requests
from PIL import Image
from transformers import AutoProcessor, AutoModelForCausalLM 
from PIL import Image, ImageDraw
import numpy as np

model = AutoModelForCausalLM.from_pretrained("microsoft/Florence-2-large", trust_remote_code=True)
processor = AutoProcessor.from_pretrained("microsoft/Florence-2-large", trust_remote_code=True)

prompt = "A green river"

task = "<REFERRING_EXPRESSION_SEGMENTATION>"

url = "https://images.pexels.com/photos/25649817/pexels-photo-25649817.jpeg?cs=srgb&dl=pexels-daniel-guti-181008543-25649817.jpg&fm=jpg"
image = Image.open(requests.get(url, stream=True).raw)

inputs = processor(text=task+prompt, images=image, return_tensors="pt")
generated_ids = model.generate(
    input_ids=inputs["input_ids"],
    pixel_values=inputs["pixel_values"],
    max_new_tokens=1024,
    num_beams=3,
    do_sample=False
)
generated_text = processor.batch_decode(generated_ids, skip_special_tokens=False)[0]
parsed_answer = processor.post_process_generation(generated_text, task=task, image_size=(image.width, image.height))
print(parsed_answer)
draw = ImageDraw.Draw(image)  
scale = 1  
for polygons, label in zip(parsed_answer[task]['polygons'], parsed_answer[task]['labels']):  
    color = 'blue'
    for _polygon in polygons:  
        _polygon = np.array(_polygon).reshape(-1, 2)  
        if len(_polygon) < 3:  
            print('Invalid polygon:', _polygon)  
            continue  
        _polygon = (_polygon * scale).reshape(-1).tolist()  
        draw.polygon(_polygon, outline=color, fill=color)  
image.save('test2.jpg', 'JPEG')

塗れなかった箇所がありますが、人を避けて川を塗ってくれました。

垂直に塗れない箇所が来ているので、ボックス領域として検出する他のタスクの影響を受けているのかもしれません。もしくは、画像をスプリットしてLLMに突っ込む仕様なのかもしないですね。

OCR

最後にOCRを試してみます。

import requests
from PIL import Image
from transformers import AutoProcessor, AutoModelForCausalLM 
from PIL import Image, ImageDraw
import numpy as np
import random
colormap = ['blue','orange','green','purple','brown','pink','gray','olive','cyan','red',
            'lime','indigo','violet','aqua','magenta','coral','gold','tan','skyblue']

model = AutoModelForCausalLM.from_pretrained("microsoft/Florence-2-large", trust_remote_code=True)
processor = AutoProcessor.from_pretrained("microsoft/Florence-2-large", trust_remote_code=True)

prompt = "<OCR_WITH_REGION>"

task = prompt

url = "https://images.pexels.com/photos/241832/pexels-photo-241832.jpeg?cs=srgb&dl=pexels-hermaion-241832.jpg&fm=jpg"
image = Image.open(requests.get(url, stream=True).raw)

inputs = processor(text=prompt, images=image, return_tensors="pt")
generated_ids = model.generate(
    input_ids=inputs["input_ids"],
    pixel_values=inputs["pixel_values"],
    max_new_tokens=1024,
    num_beams=3,
    do_sample=False
)
generated_text = processor.batch_decode(generated_ids, skip_special_tokens=False)[0]
parsed_answer = processor.post_process_generation(generated_text, task=task, image_size=(image.width, image.height))
print(parsed_answer)

scale = 1
draw = ImageDraw.Draw(image)
bboxes, labels = parsed_answer[task]['quad_boxes'], parsed_answer[task]['labels']
for box, label in zip(bboxes, labels):
    color = random.choice(colormap)
    new_box = (np.array(box) * scale).tolist()
    draw.polygon(new_box, width=3, outline=color)
    draw.text((new_box[0]+8, new_box[1]+2),
                "{}".format(label),
                align="right",
    
                fill=color)
    print(box, label)
image.save('test3.jpg', 'JPEG')

かなり煩雑に並んだ文字ですが、全ては検出できなかったものの、半分くらいは検出できました。

パラメーターを調整すれば、もっと行けそうな気もします。

まとめ

今回は、マルチモーダルLLM(?)のFlorence 2を使って学習しないで物体検出してみました。

かなりモデルサイズが小さいので、スマホに搭載することも十分できると思われます。今後のスマホアプリは、こういう物体検出アプリがでてきそうですね。

また、小さいモデルながら、様々なタスクをできる手軽さも、Yoloと比べたときの利点だと思われます。タスクごとに追加学習のデータを用意しなくても良いのはいいですね。

しかし、精度はモデル相応だったとも感じました。精度は今後に期待します。

タイトルとURLをコピーしました