メインコンテンツまでスキップ

BLIP-2とは?

概要
(画像は、Google AI Studioの「Nano Banana 2」モデルを用いて作成されたものです)

BLIP-2の概要

BLIP-2は、Salesforce Researchが2023年に開発・発表した、非常に効率的かつ高性能なマルチモーダル(視覚言語)モデルです(論文はBLIP-2: Bootstrapping Language-Image Pre-training with Frozen Image Encoders and Large Language Modelsです)。

最大の特徴は、既存の強力な学習済みモデルである「画像エンコーダー」と「大規模言語モデル(LLM)」の双方を凍結(Frozen)した状態で活用し、少ない学習コストで高い視覚言語理解能力を実現している点にあります。

BLIP-2では、凍結されたこれら2つのモデルの間に、Querying Transformer(Q-Former)と呼ばれる軽量なネットワークを配置します。このQ-Formerのみを事前学習させることにより、画像エンコーダーから得られる視覚情報を、凍結されたLLMが最も理解しやすい形式(Visual Prompt)へと効率的にアラインメント(整列)して伝達することを可能にしています。

BLIP-2の処理概要

処理概要
(画像は、Google AI Studioの「Nano Banana 2」モデルを用いて作成されたものです)
BLIP-2は上図のように、すでに高度に学習された強力な既存モデル(画像エンコーダと大規模言語モデル)を凍結(Frozen)した状態で活用し、その間を軽量なトランスフォーマーネットワークであるQ-Formerで接続することで、効率的にマルチモーダルな能力を実現するモデルです。

図の左側に示すQ-Formerと第1段階事前学習では、凍結された画像エンコーダ(例:ViT-L)から視覚的特徴を抽出し、それをテキスト特徴と整列(アライメント)させることを目的とします。Q-Formerは、学習済みのBERTをベースにクロスアテンション層を導入した構成で、入力として受け取る「学習可能なトークン群(Query Tokens)」を介して、凍結画像エンコーダの出力から関連する視覚特徴をクエリ(問い合わせ)して抽出します。この第1段階では、抽出した視覚特徴が入力テキストと整合するように、画像-テキストコントラスティブ学習(ITC)画像-テキストマッチング(ITM)画像条件付きテキスト生成(ITG)という3つの目的関数を最小化するよう、Q-Formerのみを学習させます。これにより、Q-FormerはLLMが理解できる形式で、かつテキストと整列した視覚トークンを生成可能になります。

続く図の右側の大規模言語モデル(LLM)と第2段階事前学習では、学習済みのQ-Formerを活用し、凍結された大規模言語モデル(LLM)(例:Flan-T5, OPT)に対して、画像情報に基づいたテキスト生成能力を学習させます。Q-Formerは、入力画像から抽出した視覚特徴を、LLMに対するSoft Visual Promptという形式の視覚プロンプトに変換します。このSoft Visual Promptと、ユーザーからの「テキストプロンプト(例:質問)」を結合して凍結LLMに入力し、画像条件付き言語モデリング(LM)タスク(凍結LLMに対するQ-Formerの適応学習)を行います。これにより、LLMは視覚情報とテキスト情報を統合し、画像の内容を正確に反映した自然言語による最終的なテキスト出力や回答を生成できるようになります。BLIP-2の実装では、これらのプロセスを通じて、全てのモデルをゼロから学習させるよりも遥かに少ない計算コストで、高性能なマルチモーダルAIを構築しています。

BLIP-2の実装(概念的なシンプルな実装)

BLIP-2の全体像

以下のコードでは、凍結された画像エンコーダと大規模言語モデル(LLM)の間に、クエリトークンを用いて特徴をアラインメントする簡易的なQ-Formerを挟み込み、BLIP-2の基本的な情報伝達プロセスを再現したモデルMiniBLIP2を定義します。

import torch
import torch.nn as nn
import torch.nn.functional as F
OPTForCausalLM,
CLIPImageProcessor,
AutoTokenizer
)
from PIL import Image
import requests
import matplotlib.pyplot as plt


class SimpleQFormer(nn.Module):
"""画像特徴量から必要な情報を抽出する簡易Q-Former"""
def __init__(self, vision_hidden_size, qformer_hidden_size, llm_hidden_size, num_queries=32):
super().__init__()
# 32個の学習可能なクエリトークン
self.query_tokens = nn.Parameter(torch.randn(1, num_queries, qformer_hidden_size))

# Cross-Attentionを模倣するTransformerDecoder
decoder_layer = nn.TransformerDecoderLayer(
d_model=qformer_hidden_size, nhead=8, batch_first=True
)
self.transformer = nn.TransformerDecoder(decoder_layer, num_layers=4)

# LLMの入力次元に合わせるプロジェクション層
self.llm_proj = nn.Linear(qformer_hidden_size, llm_hidden_size)

def forward(self, vision_features):
batch_size = vision_features.shape[0]
queries = self.query_tokens.expand(batch_size, -1, -1)
qformer_output = self.transformer(tgt=queries, memory=vision_features)
visual_prompts = self.llm_proj(qformer_output)
return visual_prompts

このコードは、画像エンコーダから出力される視覚特徴量から、言語モデル(LLM)への入力として必要な情報のみを抽出・集約する役割を持つSimpleQFormerクラスの実装です。

初期化メソッドであるinitでは、まず32個の学習可能なパラメータベクトルであるクエリトークン群(self.query_tokens)を定義します。このトークン群は、膨大な視覚特徴の中から「言語モデル(LLM)がテキストを生成するために必要な情報」だけを抽出・圧縮し、視覚と自然言語の異なる情報空間を結びつける(アラインメントする)ための橋渡し役となります。このクエリトークンを起点としてアテンション計算を行い、画像の特徴を動的に集約するため、PyTorchのnn.TransformerDecoderを使用しています。なお、クエリトークンの数である「32」は、BLIP-2の元の論文(BLIP-2: Bootstrapping Language-Image Pre-training with Frozen Image Encoders and Large Language Models)で設定されている標準的なトークン数です。このトークン数は、画像解像度や入力サイズに関わらず視覚情報を固定長の特徴量に圧縮するボトルネックとして機能し、言語モデル(LLM)への入力トークン数を一定に保つことで、計算効率を維持する役割を果たしています。さらに、抽出された特徴量をLLMの埋め込み表現の次元数(llm_hidden_size)に変換するための線形射影層であるself.llm_projを定義します。

フォワードパス(forward)の処理手順は以下の通りです。

  1. 入力バッチサイズに応じて、クエリトークン群(self.query_tokens)をバッチ次元方向に複製(expand)します。
  2. self.transformernn.TransformerDecoder)に対し、クエリ(tgt=queries)と画像エンコーダからの出力特徴量(memory=vision_features)を入力します。これにより、クエリトークンが画像特徴から関連性の高い情報をアテンションを介して吸い出します。
  3. デコーダから出力された特徴量をself.llm_projに通すことで、LLMに入力可能な次元の視覚プロンプト(visual_prompts)へと投影して返します。

凍結モデルを接続する簡易VLMの実装

以下のコードでは、事前学習済みのCLIP画像エンコーダとOPT言語モデルをロードしてパラメータを凍結し、定義した簡易Q-Formerで双方を繋ぎ合わせた簡易VLMクラスであるTinyBLIP2を定義します。

class TinyBLIP2(nn.Module):
"""CLIPとOPTをQ-Formerで繋ぐ簡易VLMアーキテクチャ"""
def __init__(self):
super().__init__()
# 凍結する画像エンコーダ
self.vision_model = CLIPVisionModel.from_pretrained("openai/clip-vit-base-patch32")
for param in self.vision_model.parameters():
param.requires_grad = False

# 凍結する言語モデル
self.llm_model = OPTForCausalLM.from_pretrained("facebook/opt-125m")
for param in self.llm_model.parameters():
param.requires_grad = False

# 学習対象のQ-Former
vision_dim = self.vision_model.config.hidden_size
llm_dim = self.llm_model.config.hidden_size
self.qformer = SimpleQFormer(
vision_hidden_size=vision_dim,
qformer_hidden_size=768,
llm_hidden_size=llm_dim,
num_queries=32
)

def forward(self, pixel_values, input_ids, attention_mask=None, labels=None):
with torch.no_grad():
vision_features = self.vision_model(pixel_values=pixel_values).last_hidden_state

visual_prompts = self.qformer(vision_features) # [Batch, 32, LLM_Dim]

with torch.no_grad():
text_embeddings = self.llm_model.get_input_embeddings()(input_ids)

# 視覚プロンプトとテキストを結合
inputs_embeds = torch.cat([visual_prompts, text_embeddings], dim=1)

if attention_mask is not None:
visual_mask = torch.ones(
attention_mask.shape[0], visual_prompts.shape[1],
dtype=attention_mask.dtype,
device=attention_mask.device
)
attention_mask = torch.cat([visual_mask, attention_mask], dim=1)

# Loss計算用のラベルマスク処理
if labels is not None:
ignore_index = -100
visual_labels = torch.full(
(labels.shape[0], visual_prompts.shape[1]),
ignore_index,
dtype=labels.dtype,
device=labels.device
)
labels = torch.cat([visual_labels, labels], dim=1)

outputs = self.llm_model(
inputs_embeds=inputs_embeds,
attention_mask=attention_mask,
labels=labels
)
return outputs

このコードは、凍結されたCLIP画像エンコーダと言語モデル(OPT-125m)を、学習対象であるQ-Formerを介して接続し、画像に基づいたテキスト生成を行う簡易的なVLM(Vision-Language Model)であるTinyBLIP2クラスの実装です。

初期化メソッドであるinitでは、事前学習済みの画像エンコーダ(CLIPVisionModel)と因果的言語モデル(OPTForCausalLM)をロードします。これらの既存モデルは、アラインメント能力のみを効率良く学習させるため、パラメータをすべて凍結(requires_grad = False)します。そして、双方の次元数をアラインメントするためのSimpleQFormerを定義します。

フォワードパス(forward)の処理手順は以下の通りです。

  1. torch.no_grad()のもとで画像入力(pixel_values)からパッチ特徴量(vision_features)を抽出します。
  2. 抽出した特徴量をself.qformerに入力し、32個の視覚特徴プロンプト(visual_prompts)へとアラインメントします。
  3. 同様に凍結LLMの埋め込み層を通じてテキスト入力(input_ids)をベクトル化し、torch.catを用いてシーケンス方向に視覚プロンプトとテキストを結合します。
  4. アテンションマスクや損失(Loss)計算用のラベルも、先頭に追加した32個の視覚プロンプトのサイズ分だけ拡張(パディング・無視設定用の値 -100 を付与)して整合性を合わせます。
  5. 最後に結合した埋め込み表現などを凍結LLMに入力し、出力を返します。

データセットローダの実装

以下のコードでは、データセットの各アイテムから画像URLを取得して動的にダウンロードし、前処理やトークン化、損失計算用のラベル設定を適用してモデル入力用のバッチデータを構築するクラスVLMDatasetを定義します。

class VLMDataset(Dataset):
"""URLから画像を取得し、テキストと共にテンソルに変換するデータセット"""
def __init__(self, hf_dataset, image_processor, tokenizer, max_length=32):
self.dataset = hf_dataset
self.image_processor = image_processor
self.tokenizer = tokenizer
self.max_length = max_length

def __len__(self):
return len(self.dataset)

def __getitem__(self, idx):
# 最大試行回数を設定して無限ループを防ぐ
for i in range(10): # 最大10回まで試す
try:
item = self.dataset[(idx + i) % len(self.dataset)]
url = item["url"]


headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
}
response = requests.get(url, headers=headers, stream=True, timeout=10)
if response.status_code != 200:
continue # 次へ

image = Image.open(response.raw).convert("RGB")

# 正常に読み込めたら処理を続行
pixel_values = self.image_processor(images=image, return_tensors="pt").pixel_values.squeeze(0)

caption = item["caption"] + self.tokenizer.eos_token

encoded_text = self.tokenizer(
caption, padding="max_length", truncation=True,
max_length=self.max_length, return_tensors="pt",
add_special_tokens=False
)

input_ids = encoded_text.input_ids.squeeze(0)
attention_mask = encoded_text.attention_mask.squeeze(0)

labels = input_ids.clone()
labels[labels == self.tokenizer.pad_token_id] = -100

return {
"pixel_values": pixel_values,
"input_ids": input_ids,
"attention_mask": attention_mask,
"labels": labels
}
except Exception as e:
# 失敗した場合は次のデータへ
continue

# 10回試してもダメなら、仕方なく最後のエラーを発生させるか、ダミーを返す
raise RuntimeError("有効な画像を読み込めませんでした")

このコードは、データセットからアイテムをロードする際、画像URLから動的に画像をダウンロードして前処理を施し、同時に正解テキストをトークナイズしてVLMに入力可能な状態にするVLMDatasetクラスの実装です。

初期化メソッドであるinitでは、Hugging Faceデータセット、画像プロセッサ、トークナイザを格納します。

各インデックスデータの取得メソッドであるgetitemの処理は以下の通りです。

  1. ネットワークエラーやリンク切れによる学習停止を防ぐため、最大10回まで試行するリトライループ(for i in range(10))を構築しています。
  2. 画像URLからrequests.getを実行して画像データをストリーミング取得し、PILでRGB形式の画像オブジェクトに変換します。応答タイムアウトは10秒に設定されています。
  3. 取得した画像を画像プロセッサ(self.image_processor)で正規化およびテンソル化します。
  4. 正解キャプションの末尾にLLMの終了トークン(self.tokenizer.eos_token)を明示的に付与した上で、トークナイザを用いてパディングおよび切り捨て(最大32トークン)を行います。
  5. パディングトークンに対応するラベルの位置には、クロスエントロピー損失の計算時に無視されるインデックス値(-100)を設定したターゲットラベルを生成し、バッチ用辞書として返します。

モデルの初期化

以下のコードでは、構築したモデルのインスタンス生成、トークナイザやデータローダのロード、最適化対象となるパラメータ(Q-Former)の指定とオプティマイザ(AdamW)の初期設定を実行します。

print("モデルとプロセッサを初期化中...")
model = TinyBLIP2()
image_processor = CLIPImageProcessor.from_pretrained("openai/clip-vit-base-patch32")
tokenizer = AutoTokenizer.from_pretrained("facebook/opt-125m")
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token

hf_dataset = load_dataset("Spawning/megalith-cc0", split="train[:1]")
# データセットの構造を確認
train_dataset = VLMDataset(hf_dataset, image_processor, tokenizer)
train_dataloader = DataLoader(train_dataset, batch_size=1, shuffle=True)

device = "cuda" if torch.cuda.is_available() else "cpu"

# モデル全体(CLIP, OPT, Q-Former全て)を確実に単精度(float32)に統一する
model.to(device, dtype=torch.float32)
model.train()

# 凍結されていないパラメータ(Q-Former関連)のみをオプティマイザに渡す
trainable_params = [p for p in model.parameters() if p.requires_grad]
optimizer = AdamW(trainable_params, lr=1e-4)

このコードは、定義した簡易VLMモデル、事前学習済みモデルに対応するプロセッサ・トークナイザ、学習に使用するデータセットをロードし、最適化に必要な初期設定を行う処理です。

  1. TinyBLIP2モデルのインスタンスを生成し、CLIP用の画像プロセッサおよびOPT用のトークナイザをロードします。トークナイザのパディングIDが未定義の場合はEOS IDを代入して代用します。
  2. Spawning提供の「megalith-cc0」データセットから先頭の1件をロードし、作成したデータセットクラスおよびデータローダに格納します。なお、本家のBLIP-2では、16台のA100 GPUを用いて第1段階(Stage 1)に約6日間、第2段階(Stage 2)に約3日間をかけた大規模な事前学習が行われています。しかし本記事では、読者自身の環境で簡易的に動作確認やデータフローの理解を行えるよう、1サンプルの画像とテキストのみを用いて意図的に過学習(オーバーフィット)させる構成にしています。
  3. モデル全体をGPUまたはCPUデバイスへ転送し、数値精度の整合性を取るため単精度(torch.float32)に統一した上で訓練モードに切り替えます。
  4. 最適化対象のパラメータ(パラメータが更新される部分)として、凍結されていない(requires_grad = True)パラメータであるQ-Formerのみをオプティマイザ(AdamW)に登録します。

第1段階事前学習の実行

以下のコードでは、コサイン類似度損失を用いて、Q-Formerが抽出した視覚プロンプトをテキストの埋め込み表現へと近づける「第1段階事前学習(表現アラインメント学習)」のループを実行します。

本家BLIP-2の論文におけるStage 1では、「ITC(画像-テキストコントラスティブ学習)」「ITM(画像-テキストマッチング)」「ITG(画像条件付きテキスト生成)」という3つの複雑な損失(Loss)を計算してQ-Formerを学習させています。これらはQ-Former自体に高度な表現力や生成能力を持たせるための工夫ですが、今回の簡易実装(MiniBLIP2)のStage 1では、アラインメントの本質である「ベクトルを近づける」ことに焦点を絞り、コサイン類似度損失(簡易版ITC)のみを使用しています。

VLMの根本的な原理を学ぶにあたっては、Q-Formerが抽出した視覚プロンプトが大まかにテキストベクトルと同じ方向を向くことさえ達成できれば、後続のStage 2において強力な凍結LLMが文脈を補ってくれます。したがって、今回の構成は機能が不足しているのではなく、教育的意図を持って本質だけを残し、シンプルに削ぎ落とした設計となっています。

print("\n--- 【Stage 1】 簡易的な表現学習(アラインメント)を開始 ---")
# 1件のデータを過学習させる設定
epochs_stage1 = 20

for epoch in range(epochs_stage1):
total_loss = 0
for batch in train_dataloader: # 1件しか入っていないDataLoader
pixel_values = batch["pixel_values"].to(device, dtype=torch.float32)
input_ids = batch["input_ids"].to(device)

optimizer.zero_grad()

# 1. Q-Formerで画像から「視覚プロンプト(32個)」を抽出
with torch.no_grad():
vision_features = model.vision_model(pixel_values=pixel_values).last_hidden_state
visual_prompts = model.qformer(vision_features) # [Batch, 32, 768]

# 2. 正解テキストから「テキスト埋め込み」を抽出
with torch.no_grad():
text_embeds = model.llm_model.get_input_embeddings()(input_ids) # [Batch, SeqLen, 768]

# 3. 両者の特徴量を要約(平均プーリング)して1つのベクトルにする
visual_repr = visual_prompts.mean(dim=1)
text_repr = text_embeds.mean(dim=1)

# 4. コサイン類似度ベースのLossを計算(1に近いほど正解とする)
target = torch.ones(visual_repr.size(0)).to(device)
loss = F.cosine_embedding_loss(visual_repr, text_repr, target)

loss.backward()
optimizer.step()
total_loss += loss.item()

if (epoch + 1) % 5 == 0:
print(f"Stage 1 - Epoch {epoch+1} | Cosine Loss: {total_loss:.4f}")

print("--- Stage 1 完了 ---\n")

このコードは、BLIP-2の「第1段階事前学習(テキスト表現とのアライメント)」を模擬した訓練ループです。

  1. Q-Formerが画像特徴量から抽出した視覚プロンプト表現と、正解テキストをトークナイズした埋め込み表現の関連性を近づけるため、コサイン類似度損失(F.cosine_embedding_loss)を用いて20エポック最適化します。ここで「損失(Loss)が0になるまで完全に一致させれば、より完璧なモデルになるのでは?」と疑問に思うかもしれません。しかし、実は視覚ベクトルとテキストベクトルを近づけすぎるのはNGです。画像は「夕焼け、草むら、質感」など膨大な情報を持っていますが、テキストはそれを「可愛いキャラクター」などと要約した短い情報に過ぎません。両者を完全に一致させようとすると、Q-Formerはテキストに書かれていない画像の豊かなディテールをすべて切り捨ててしまいます。32個のクエリトークンには、テキストには表れきれない画像の「余白の情報」を多様なベクトルのままLLMへ届けるという重要な使命があります。だからこそ、Stage 1の学習は「大まかな方向性を合わせる程度」に留め、近づけすぎない(過学習させない)寸止めが重要になります。そのため、エポック数をあえて20という収束しきらない中途半端な値に設定しています。
  2. 凍結画像エンコーダおよび凍結言語モデルからの特徴量抽出処理は勾配計算から除外し、Q-Formerのみを更新します。
  3. 各エポック終了後にコサイン損失を表示し、損失値が順調に低下(0に近い値)して表現力のアラインメントが学習できているかを確認します。

第2段階事前学習の実行

以下のコードでは、第1段階で整列させたQ-Formerの視覚表現と言語モデルのテキスト埋め込みを入力し、画像に基づくテキスト生成能力(因果言語モデリング)を最適化する「第2段階事前学習(LLM適応学習)」のループを実行します。

print(f"--- 【Stage 2】 生成学習(キャプション生成)を開始 (Device: {device}) ---")
epochs = 200
# (バッチサイズは1件しかないので自動的に1になります)
for epoch in range(epochs):
total_loss = 0
for batch in train_dataloader:
# 画像テンソルも明示的に float32 に変換してモデルに渡す
pixel_values = batch["pixel_values"].to(device, dtype=torch.float32)

input_ids = batch["input_ids"].to(device)
attention_mask = batch["attention_mask"].to(device)
labels = batch["labels"].to(device)

optimizer.zero_grad()
outputs = model(
pixel_values=pixel_values,
input_ids=input_ids,
attention_mask=attention_mask,
labels=labels
)
loss = outputs.loss
loss.backward()
optimizer.step()
total_loss += loss.item()
if (epoch+1) % 50 == 0:
print(f"Epoch {epoch+1} 完了 | 平均Loss: {total_loss / len(train_dataloader):.4f}")
print("--- Stage 2 完了 ---")

このコードは、BLIP-2の「第2段階事前学習(LLMへのアライメント)」を模擬した訓練ループです。

  1. 1件のサンプル画像とテキストのペアを用いて、過学習を意図した200エポックの生成学習(画像条件付き言語モデリング)を実行します。
  2. 画像とアテンションマスク、正解ラベル(パディング箇所は -100)をモデルのフォワードパスに入力し、凍結LLMから出力される因果的クロスエントロピー損失を最小化するようにQ-Formerのパラメータを更新します。
  3. 50エポックごとに平均損失を出力し、言語生成に必要な重みの最適化プロセスをモニターします。

推論の実行

以下のコードでは、学習を完了したモデルに対して未知のテスト画像を入力し、画像特徴からアラインメントされたプロンプトを経由してキャプションを自己回帰的に生成・可視化する推論デモンストレーションを実行します。

def generate_caption(model, image_processor, tokenizer, image, device="cpu"):
"""学習済みモデルに未知の画像を渡し、キャプションを生成する"""
model.eval()
with torch.no_grad():
pixel_values = image_processor(images=image, return_tensors="pt").pixel_values.to(device, dtype=torch.float32)
vision_features = model.vision_model(pixel_values=pixel_values).last_hidden_state
visual_prompts = model.qformer(vision_features)

attention_mask = torch.ones(
visual_prompts.shape[0], visual_prompts.shape[1], device=device
)

generated_ids = model.llm_model.generate(
inputs_embeds=visual_prompts,
attention_mask=attention_mask,
max_new_tokens=150,
pad_token_id=tokenizer.pad_token_id,
eos_token_id=tokenizer.eos_token_id,
do_sample=False
)
return tokenizer.decode(generated_ids[0], skip_special_tokens=True).strip()

sample = hf_dataset[0]
url = sample["url"]
overfit_image = Image.open(requests.get(url, stream=True).raw).convert("RGB")
target_text = sample["caption"]

print(f"\n[正解のテキスト]: {target_text}")
# 推論実行
generated_text = generate_caption(model, image_processor, tokenizer, overfit_image, device)

print(f"[モデルの出力]: {generated_text}")

# --- 画像と生成テキストの可視化 ---
plt.figure(figsize=(6, 6))
plt.imshow(overfit_image)
plt.axis("off") # 軸のメモリを非表示にして綺麗に見せる

# 画像のタイトルとして生成されたキャプションを表示(長ければ自動で折り返し)
plt.title(f"Target: {target_text}\nGenerated: {generated_text}",
fontsize=12, wrap=True, pad=20, backgroundcolor="white")

plt.show()

このコードは、学習を終えたモデルの挙動を確認するために、同じ画像ペアを入力としてキャプション生成を実行するデモンストレーション処理です。

  1. 推論用関数であるgenerate_captionでは、モデルを評価モード(model.eval())にし、勾配計算を無効化した上で、画像の視覚プロンプトを抽出して凍結言語モデルのテキスト生成メソッドであるmodel.llm_model.generateへ直接入力(inputs_embeds=visual_prompts)します。
  2. tokenizer.decodeにより出力されたトークンIDを通常のテキストにデコードし、コンソールに正解のテキストとモデルの出力を比較表示します。
  3. 最後に、可視化のためにmatplotlibを使用して入力画像を表示し、その上に生成されたテキストと元の正解テキストを重ねて表示します。

実行結果

上記のコードを実行すると以下の結果が得られます。

モデルとプロセッサを初期化中...
--- 【Stage 1】 簡易的な表現学習(アラインメント)を開始 ---
Stage 1 - Epoch 5 | Cosine Loss: 0.2278
Stage 1 - Epoch 10 | Cosine Loss: 0.0557
Stage 1 - Epoch 15 | Cosine Loss: 0.0154
Stage 1 - Epoch 20 | Cosine Loss: 0.0120
--- Stage 1 完了 ---

--- 【Stage 2】 生成学習(キャプション生成)を開始 (Device: cuda) ---
Epoch 50 完了 | 平均Loss: 1.4167
Epoch 100 完了 | 平均Loss: 1.0563
Epoch 150 完了 | 平均Loss: 0.8518
Epoch 200 完了 | 平均Loss: 0.8577
--- Stage 2 完了 ---

[正解のテキスト]: The image shows two ancient artifacts, possibly from the Roman Empire, displayed on a beige background. The artifacts appear to be made of metal and have a greenish-brown color. They have a pointed tip with a small handle on one end and a smaller handle on the other end. The handle is decorated with a floral design and has a small loop at the top for attaching to a belt or bag. The blade of the artifact is slightly curved and appears to be slightly pointed. The overall appearance of the artifacts is weathered and aged.
[モデルの出力]: The image shows two ancient artifacts, possibly from the Roman Empire, displayed on a beige background. The artifacts appear to be made of metal and have a green beige color, and are displayed on a beige background. The artifacts appear to be made of metal and have a green beige background. The artifacts appear to be made of metal and have a green beige color, and the artifacts appear to be made of metal. The artifacts appear to be made of metal and have a green beige background. The artifacts appear to be made of metal and have a green beige background. The artifacts appear to be made of metal and have a green beige background. The artifacts appear to be made of metal and have a green beige

出力結果を確認すると、第1段階の表現アラインメント学習(コサイン損失)は0.2278から0.0120へと大幅に減少し、第2段階の言語モデル適合タスク(生成損失)も1.4167から0.8518へと順調に低下していることが確認できます。

モデルが出力したテキストの日本語訳と、正解のテキストとの比較は以下の通りです。

  • 正解のテキスト(日本語訳): 「この画像は、おそらくローマ帝国のものである2つの古代のアーティファクトをベージュの背景に展示したものである。アーティファクトは金属製で、緑がかった褐色の色合いをしている。一端に小さなハンドルの付いた尖った先端と、もう一端に小さめのハンドルを備えている。ハンドルは植物のデザインで装飾され、ベルトやバッグに取り付けるための小さなループが上部にある。アーティファクトの刃はわずかに湾曲し、少し尖って見える。アーティファクトの全体的な外見は、風化し古びている。」
  • モデルの出力(日本語訳): 「この画像は、おそらくローマ帝国のものである2つの古代のアーティファクトをベージュの背景に展示したものである。アーティファクトは金属製で、緑がかったベージュの色合いをしており、ベージュの背景に展示されている。アーティファクトは金属製で、緑がかったベージュの背景をしている。アーティファクトは金属製で、緑がかったベージュの色合いをしており、アーティファクトは金属製に見える。アーティファクトは金属製で、緑がかったベージュの背景をしている。(以下、同様の文の繰り返し)」

モデルは画像の内容や色調などの大枠を正しく捉えられていますが、文末において「緑がかったベージュの背景をしている」や「金属製である」といった同じ語句・文を何度も繰り返すループ現象が発生しています。これは、今回のモデルの事前学習が1件のデータに対する過学習であることや、使用している言語モデル(OPT-125m)の表現力、およびビームサーチ等の生成デコードパラメータの制限に起因しています。 推論結果

実際のHugging Faceモデルを用いた推論テスト

ここまでは簡易モデルを自作して学習の基礎を理解してきましたが、ここからは実際の開発に役立つよう、Hugging Faceの事前学習済みBLIP-2モデル(OPT 2.7B)を使用したZero-Shotキャプション生成を実演します。

このコードでは、画像とテキストを統合前処理するBlip2Processor、および大規模言語モデルを内包したBlip2ForConditionalGenerationモジュールをロードします。また、画像データにはSpawningがパブリックドメイン(CC0)ライセンスの画像を集約して構築した大規模データセットである「Spawning/megalith-cc0」に含まれる特定の画像URLを使用し、ダウンロードして推論処理を行います。

import torch
from PIL import Image
import requests
from transformers import Blip2Processor, Blip2ForConditionalGeneration
import matplotlib.pyplot as plt

# デバイスの設定
device = "cuda" if torch.cuda.is_available() else "cpu"

# プロセッサとモデルの読み込み
model_id = "Salesforce/blip2-opt-2.7b"
processor = Blip2Processor.from_pretrained(model_id)
model = Blip2ForConditionalGeneration.from_pretrained(
model_id, torch_dtype=torch.float16 if device == "cuda" else torch.float32
).to(device)

# 画像の準備(Huggingface Dataset Spawning/megalith-cc0 ID:f81bdee3f9e7da521751c86a35f1baf0)
img_url = "https://megalith-8m.s3.us-west-2.amazonaws.com/images/f10e65af-3cd6-5100-a0e7-50c2e43f4974.jpeg"
raw_image = Image.open(requests.get(img_url, stream=True).raw).convert('RGB')

# 入力データの準備
inputs = processor(raw_image, return_tensors="pt").to(device, torch.float16 if device == "cuda" else torch.float32)

# キャプション生成
generated_ids = model.generate(**inputs, max_new_tokens=20)
generated_text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0].strip()

plt.figure(figsize=(8, 6))
plt.imshow(raw_image)
plt.axis('off') # 軸を非表示にする
plt.title(f"Caption: {generated_text}", fontsize=12)
plt.show()

print(f"Generated caption: {generated_text}")

このコードは、事前学習済みの本格的なBLIP-2モデル(Salesforce/blip2-opt-2.7b)をロードし、「Spawning/megalith-cc0」データセットからダウンロードした画像に対して自動キャプション生成を行う実装です。

処理のフローは以下の通りです。

  1. Blip2Processor.from_pretrainedを使用して画像およびテキストのマルチモーダルな前処理エンジンをロードし、Blip2ForConditionalGeneration.from_pretrainedでパラメータが最適化されたBLIP-2の本体モデルをロードします。GPUが利用可能な環境(cuda)では、省メモリと高速化のためにFP16(torch.float16)に精度をキャストしてロードします。
  2. 「Spawning/megalith-cc0」の指定URLからテスト画像をストリーミングダウンロードしてPIL画像に変換します。
  3. processorに画像を渡し、モデルが必要とする形状の入力テンソル(inputs)に正規化変換します。
  4. model.generateを呼び出し、モデル自身に「画像内に何が映っているか」を言語生成で説明させ、生成されたトークンIDをデコードしてコンソールおよびmatplotlibのタイトルとして結果を表示します。

これにより、高精度な事前学習済みアライメントモデル(Q-Former)が、巨大なLLMの能力を一切損なうことなく視覚情報と接続し、高いZero-Shot記述能力を発揮している結果が得られます。 上記のコードを実行すると以下の結果が得られます。

実行結果

まとめ

本記事では、既存の強力なモデルを凍結した状態で接続する効率的なマルチモーダルモデルであるBLIP-2について、そのアーキテクチャからPyTorchによる簡易再現、そして公式モデルを用いた実践的な推論までを解説しました。

記事を通じて学習した主なポイントは以下の通りです。

  • Q-Formerの役割: 凍結された画像エンコーダとLLMの間の橋渡しとして、学習可能なクエリトークンを用いたクロスアテンションにより、画像特徴量をLLMが解釈可能な「Soft Visual Prompt」へ変換する設計思想を理解しました。
  • 2段階の事前学習: 第1段階(テキスト表現との整列: ITC/ITM/ITG損失)と、第2段階(LLMとの適応学習: LM損失)による効率的なアラインメント学習アプローチを学びました。
  • 概念的な実装と公式モデルの活用: クエリトークンを用いたCross-AttentionのPyTorchでの最小限の再現コード、およびHugging Faceのtransformersを用いた事前学習済みBLIP-2の実行方法を習得しました。

BLIP-2は、全てのモデルパラメータをゼロから再学習させることなく、接続部分のみを最適化することで超巨大LLMの言語生成能力をマルチモーダルタスクに転用できることを示した画期的な手法です。本記事で解説したアプローチを足がかりに、最先端のVLMやマルチモーダルエージェントの理解をさらに深めてみてください。

本記事の文章・構成の一部に生成AIを使用しています。