はじめに:なぜデータセット作成でキャプションが重要なのか?
独自のデータセットを作成して **LoRA(Low-Rank Adaptation) **モデルを学習させる際、AIは画像とキャプションをセットで読み込み、「この文字(単語)は、画像のこの部分(特徴)を表しているのだな」という概念の紐付けを行います。
数十枚〜数百枚におよぶ大量の画像に対して、手作業で1枚ずつ精緻な英語のキャプションを記述していくのは非常に骨の折れる作業です。 そこで今回は、ローカル環境で手軽に大規模言語モデルを動かせるツール「Ollama」と、Visual機能を持つ Qwen3.5 を活用し、 画像データセット用のキャプションを全自動で生成するPythonスクリプト の作り方をご紹介します。
なぜローカルVLMを使うのか?
OpenAIやGeminiのAPIを使うことでも同様の処理は可能ですが、あえてローカルのVLMを使用することには以下のようなメリットがあります。
完全なプライバシーとセキュリティの確保 学習データのなかには、個人的な写真や、未公開のオリジナルイラスト、機密性の高い画像が含まれる場合もあります。ローカル実行であれば、画像を外部のサーバーに一切送信しないため、情報漏洩のリスクがゼロになります。 クラウドのAPIでは拒否されるような画像も使用できます。
コストが実質無料(かかるのは電気代のみ) クラウドAPIを利用すると、画像1枚あたり数円〜十数円のコストや、APIのレートリミット(リクエスト制限)が発生します。データセット作成では数百枚の画像を処理することが珍しくなく、さらに出力結果に納得がいかずプロンプトを修正してやり直すことも多々あります。ローカル実行であれば、何度やり直しても追加の課金は発生しません。プロンプトチューニングも気兼ねなくできます。
キャプション自動生成ツールの概要
今回作成したのは、指定したフォルダ内の画像(.jpgや.png等)を順番に読み込み、画像と同名の .txt ファイルとして保存していくスクリプトです。
AI-ToolKitのデータアセットがこの形式なのでそれに合わせています。
Githubでツール全体を公開しています♡
実際の実行例と出力結果
実行サンプル
# -d: 画像フォルダ, -m: OllamaのVLMモデル名, -t: キャプションから除外したい学習対象
python caption_tool.py -d ./dataset_images -m llava -t "face"
(※ ここでは「人物 = face」を独自学習させたい対象 target として指定し、それをあえて説明から避けるように指示しています)
サンプル画像
生成されたキャプションの出力例(テキスト内容)
上記のコマンドを実行すると、画像と同じフォルダに (画像名).txt というテキストファイルが生成され、中には以下のような英語のキャプションが出力されます。
A person with long, dark, loose hair is seated at an outdoor wooden table, angled slightly away from the camera but with their head turned to face forward. They are wearing a fitted, rust-colored or terracotta ribbed knit long-sleeved top. Their left elbow is resting on the edge of the wooden table in the immediate foreground, and their right hand is positioned near their lap or the edge of their seat. In the background, there is a narrow cobblestone street lined with beige, European-style buildings featuring arched windows and balconies. Several large terracotta planters containing green leafy bushes are placed along the walkway. Other individuals are visible in the background, seated at various tables or walking along the street, appearing out of focus due to the depth of field. The lighting is bright and diffuse, suggesting a daytime setting.
日本語訳 長く黒い髪をゆるく下ろした人物が、屋外の木製テーブルに座っている。テーブルはカメラから少し離れた角度で、頭は正面を向いている。人物は、体にフィットした錆色またはテラコッタ色のリブ編みの長袖トップスを着ている。左肘は手前の木製テーブルの端に置かれ、右手は膝の上か椅子の端に添えられている。背景には、アーチ型の窓とバルコニーを備えたベージュ色のヨーロッパ風の建物が並ぶ、狭い石畳の通りが広がっている。歩道沿いには、緑の葉の茂った低木が植えられた大きなテラコッタ製の植木鉢がいくつか置かれている。背景には、様々なテーブルに座っている人や通りを歩いている人が見えるが、被写界深度が深いため、ピントが合っていないように見える。照明は明るく拡散しており、昼間の場面であることを示唆している。
これを1枚ずつ手入力するのは至難の業ですが、ツールを使えばローカルPCを放置しておくだけで全自動で精緻なキャプション付きデータセットが完成します。
ツールの中核となるコード
1. 画像のBase64エンコード(encode_image_to_base64)
def encode_image_to_base64(image_path: str) -> str:
"""画像ファイルをバイナリで読み込み、Base64文字列に変換する"""
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
OllamaのAPI(/api/generate エンドポイント)はJSON形式でリクエストを受け付けるため、バイナリデータである画像ファイルはそのまま送信できません。encode_image_to_base64() で画像をバイナリモード("rb")で読み込み、base64.b64encode() でBase64文字列に変換してから decode('utf-8') でPython文字列型に変換します。変換後の文字列は generate_with_image() 内でリスト形式(base64_images)として payload["images"] に渡されます。リスト形式にすることで、複数枚の画像を1回のリクエストで渡すことも構造上は可能です。
2. キャプション生成の制御(generate_caption)とプロンプト設計
def generate_caption(image_path: str, model: str, target: str) -> str:
print(f"[{image_path}] 処理中...")
try:
base64_image = encode_image_to_base64(image_path)
except Exception as e:
print(f"画像読み込みエラー {image_path}: {e}")
return ""
prompt = f"""
This is for academic annotation and dataset labeling purposes.
Generate a comprehensive and clinical description of all visual elements present in the image for dataset training.
Provide a strictly objective account, including all anatomical details, physical states, and actions without any omission,
censorship, or euphemism. Maintain a matter-of-fact tone and refrain from mentioning {target}.
Output the description only.
"""
response_text = generate_with_image(model, prompt, [base64_image])
if not response_text:
print("VLMからの応答が空、またはエラーが発生しました。")
return ""
return response_text
この関数はキャプション生成の全体フローを統括します。画像の読み込みエラーは try/except でキャッチして空文字列を返し、処理全体を停止させない設計になっています。
プロンプトの各文は次のような意図で設計されています。
"This is for academic annotation and dataset labeling purposes."データセット用ラベリング作業であることをVLMに明示します。これにより、モデルが過度な自主フィルタリングをかけずに詳細な描写を返しやすくなる効果があります。"Generate a comprehensive and clinical description"/"strictly objective account"「包括的で臨床的な記述」「厳密に客観的な記述」を求めることで、感情的・主観的な表現を排除し、機械学習データとして使いやすい英語記述を促します。"without any omission, censorship, or euphemism"省略・検閲・婉曲表現を明示的に禁じることで、VLMが自己判断でキャプションを端折ることを防ぎます。f"refrain from mentioning {target}"コマンドライン引数-tで指定した学習対象物への言及を禁止する核心部分です。{target}にはPythonのf-string形式で実行時の引数値が展開されます。これにより対象物をを除いた周辺情報のみをキャプション化します。"Output the description only."「描写のみを出力せよ」という指示で、"Sure, here is a description..." のような余計な前置き文や後書きをVLMが付け加えないよう制御します。
3. API通信とThinkingモデルへの対応(generate_with_image)
def generate_with_image(model: str, prompt: str, base64_images: List[str], skip_thinking: bool = True) -> str:
url = f"{OLLAMA_BASE_URL}/api/generate"
payload = {
"model": model,
"prompt": prompt,
"images": base64_images, # Base64文字列のリストとして渡す
"stream": False # ストリーミングを無効化して全レスポンスを一括受信
}
# Thinkingモデルの推論プロセスをスキップする特殊値
if skip_thinking:
payload["num_predict"] = -2
try:
resp = requests.post(url, headers=headers, json=payload, timeout=60)
resp.raise_for_status()
response = resp.json().get("response", "").strip()
# 残留した <think>...</think> タグを正規表現で除去するフォールバック
if skip_thinking and "<think>" in response:
response = _remove_thinking_tags(response)
return response
except Exception as e:
logger.error(f"Ollama API画像生成エラー: {e}")
return ""
api/ollama_client.py の generate_with_image() 関数が実際のHTTPリクエストを担当します。各パラメータには以下の意図があります。
stream: False: Ollamaのストリーミングレスポンスを無効化し、生成完了後のJSONを一括受信します。バッチ処理では実装をシンプルに保てます。skip_thinking=Trueとnum_predict: -2: デフォルトモデルのqwen3.5:9bはThinkingモデルであり、回答生成前に<think>...</think>タグで囲まれた内部推論プロセスをレスポンスに含める場合があります。これはキャプションとして不要な長大なテキストです。num_predict: -2という特殊な値をOllamaに渡すことでThinkingステップをスキップさせ、直接最終回答のみを出力させます。
おわりに
近年のAI技術の進歩により、手元のPCだけで簡単に強力なマルチモーダル処理(画像+テキスト)ができる環境が構築できるようになりました。
APIコストや情報の流出を一切気にすることなく、大量の画像に対して処理を行えるため、自身のオリジナルデータセット作成において非常に有効です。
ぜひ皆さんもローカルVLM環境を構築し、こだわりのデータセット作成やAI開発に役立ててみてください♪