マルチモーダル言語モデルPhi-3.5-vision-instructをvllmで高速推論するテスト

はじめに

ローカルマシンで動くマルチモーダル言語モデルの性能が上がっています。

本記事では、軽量・ローカルの強みを生かして、動画に対して高速でテキストをアノテーションしてみます。

セットアップ

以下の記事を参考に、vision系のライブラリなどを入れます。

高速推論のためのvllmを入れます。グラフ表示系のライブラリも入れます。

pip install vllm=0.6.2
pip install seaborn
pip install japanize-matplotlib #日本語フォント用


推論

サーバー立ち上げ

vllmのapiサーバーを立ち上げます。
執筆時点では、日本語性能の高そうなqwen2-vlは対応してなさそうだったので、モデルにはphi-3.5を使います。

 vllm serve microsoft/Phi-3.5-vision-instruct --max-model-len 4096  \
--trust-remote-code --limit-mm-per-prompt image=1 --port 8001

マシンはRTX-6000Adaを使用しました。VRAMは46GBほど消費していました。

クライアント

今回は、予め撮影した動画に対して、テキストを生成していきます。
原理的には、USBカメラの画像などに対してリアルタイムにアノテーションしていくことも可能です。

openaiのクライアントモジュールを使います。

関数定義

import cv2
import base64
from openai import OpenAI
import japanize_matplotlib
import matplotlib.pyplot as plt
from IPython.display import clear_output
import time
from matplotlib.gridspec import GridSpec
import os
import numpy as np

# 画像をBase64エンコードする関数
def encode_image_to_base64(image):
    _, buffer = cv2.imencode('.jpg', image)
    encoded_string = base64.b64encode(buffer).decode('utf-8')
    return f"data:image/jpeg;base64,{encoded_string}"


# 長いキャプションを適当な長さで改行
def wrap_text(text, width=40):
    words = text.split(" ")  # スペースで単語に分割
    lines = []  # 改行された行を格納するリスト
    current_line = ""  # 現在の行を一時的に保持

    for word in words:
        # 現在の行に単語を追加しても幅を超えない場合は追加
        if len(current_line) + len(word) + 1 <= width:
            if current_line:  # 既に何か入っている場合、スペースを追加してから単語を追加
                current_line += " " + word
            else:
                current_line = word
        else:
            # 幅を超える場合は現在の行を確定させ、次の行に移る
            lines.append(current_line)
            current_line = word  # 新しい行に最初の単語を設定
    
    # 最後の行をリストに追加
    if current_line:
        lines.append(current_line)
    
    # 改行で結合して返す
    return "\n".join(lines)

# フレームを指定数スキップ
def skip_frames(cap, num_frames):
    for _ in range(num_frames):
        ret, _ = cap.read()  # フレームを読み捨て
        if not ret:
            break

クライアント関連の定義

# OpenAIクライアントの設定
openai_api_key = "EMPTY"
openai_api_base = "http://localhost:8002/v1"
client = OpenAI(api_key=openai_api_key, base_url=openai_api_base)



# 動画ファイルのパス (適当に設定)
video_path = 'mov/sci1.mp4'

動画処理
リアルタイム感を出すため、処理に要した時間分のフレームはスキップしました。


# OpenCVで動画を読み込む
cap = cv2.VideoCapture(video_path)

fps = cap.get(cv2.CAP_PROP_FPS)  # フレームレートを取得
if fps == 0:  # 万が一fpsが取得できない場合の対処
    fps = 30

# 出力ファイルの設定
fourcc = cv2.VideoWriter_fourcc(*'mp4v')  # 出力フォーマット(MP4)
output_path = video_path.replace(".mp4","_cap.mp4")  # 保存する動画ファイルのパス
frame_size = (640, 480)  # 画像サイズ(動画に合わせる)
out = cv2.VideoWriter(output_path, fourcc, fps, frame_size)

# Matplotlibインタラクティブモードを無効にする
plt.ioff()

# フレームごとにキャプションを生成
while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        break

    # フレームをBase64にエンコード(仮の関数)
    encoded_image = encode_image_to_base64(frame)

    # プロンプト設定
    prompt = "Describe what is in the image."

    # キャプション生成にかかる時間の計測開始
    start_time = time.time()

    # APIにリクエスト(仮想的なクライアント)
    response = client.chat.completions.create(
        model="microsoft/Phi-3.5-vision-instruct",
        messages=[
            {
                "role": "user",
                "content": [
                    {"type": "text", "text": prompt},
                    {"type": "image_url", "image_url": {"url": encoded_image}}
                ]
            }
        ],
        temperature=0,
        max_tokens=100,
        n=1
    )

    # キャプションの取得と改行処理
    caption = response.choices[0].message.content
    wrapped_caption = wrap_text(caption, width=30)  # 30文字ごとに改行

    # 固定サイズの図を作成
    fig = plt.figure(figsize=(10, 6))  # 全体のサイズを設定
    gs = GridSpec(1, 2, width_ratios=[3, 1])  # 画像:キャプションの幅の比率を3:1に設定

    # 画像部分
    ax_image = fig.add_subplot(gs[0])  # GridSpecの左側に画像
    ax_image.imshow(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
    ax_image.axis('off')  # 軸を非表示に

    # キャプション部分
    ax_caption = fig.add_subplot(gs[1])  # GridSpecの右側にキャプション
    ax_caption.text(0.5, 0.5, wrapped_caption, ha='center', va='center', wrap=True, fontsize=12)
    ax_caption.axis('off')  # キャプションエリアの軸も非表示

    # 図を描画し、画像として保存
    fig.canvas.draw()
    image_from_plot = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8)
    image_from_plot = image_from_plot.reshape(fig.canvas.get_width_height()[::-1] + (3,))

    # Matplotlibの画像をOpenCVフォーマットに変換
    image_from_plot_bgr = cv2.cvtColor(image_from_plot, cv2.COLOR_RGB2BGR)
    resized_image = cv2.resize(image_from_plot_bgr, frame_size)  # 動画サイズに合わせてリサイズ

    # キャプション生成にかかった時間を計測
    elapsed_time = time.time() - start_time

    # フレームスキップ数を計算
    num_frames_to_skip = int(elapsed_time * fps)

    # スキップしたフレーム分だけ同じフレームを繰り返し書き込む
    for _ in range(num_frames_to_skip + 1):  # 元のフレームも含めて繰り返し
        out.write(resized_image)
    
    # 表示をクリア
    plt.clf()

# 終了処理
cap.release()
out.release()
plt.close()
cv2.destroyAllWindows()

生成の様子

自動実験の様子

本日誕生した東京科学大学の様子


まとめ

小型のマルチモーダルモデルは、リアルタイムに近い速度で画像に対するアノテーションが可能なことがわかりました。
今回は4 B程度のモデルで1 fps程度でしたが、2BのQwen2などを使えば、更に高速化できそうです。
定点カメラでの異常検知や見守り、何らかの作業の観察・記録、といった用途に使えるような気がしました。

いいなと思ったら応援しよう!