マルチモーダル言語モデル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()
生成の様子
自動実験の様子
Phi3.5-vision-instructによる動画のアノテーションのテスト pic.twitter.com/yUawSviNPy
— 畠山 歓 Kan Hatakeyama (@kanhatakeyama) October 1, 2024
本日誕生した東京科学大学の様子
アノテーションテスト その2 https://t.co/KbPNWAZQD2 pic.twitter.com/xBp8eAFlOW
— 畠山 歓 Kan Hatakeyama (@kanhatakeyama) October 1, 2024
まとめ
小型のマルチモーダルモデルは、リアルタイムに近い速度で画像に対するアノテーションが可能なことがわかりました。
今回は4 B程度のモデルで1 fps程度でしたが、2BのQwen2などを使えば、更に高速化できそうです。
定点カメラでの異常検知や見守り、何らかの作業の観察・記録、といった用途に使えるような気がしました。