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

Chameleonとは?

概要

Chameleonの概要

Metaが2024年に発表したChameleon(カメレオン) は、マルチモーダルAIのパラダイムシフトを予感させるエキサイティングな技術です(原著論文: Chameleon: Mixed-Modal Early-Fusion Foundation Models (arXiv:2405.09818))。

Chameleonを一言で表現すると、「最初から最後まで、画像とテキストを完全に『同じ扱い』で処理する、真の初期融合型(Early-fusion)マルチモーダル基礎モデル」 です。

従来のGPT-4VやGeminiなどの多くは、画像用とテキスト用に別々のエンコーダー(CLIPなど)を後から「継ぎ接ぎ(Late-fusion)」する構造が主流でした。一方、Chameleonは画像もテキストも最初からまったく同じ「トークン」として扱い、単一の巨大なTransformerアーキテクチャでエンドツーエンドで学習します。

3つのコア・テクノロジー

  1. 「完全な初期融合(Early-fusion)」とインターリーブ処理
    Chameleonは、テキストの単語だけでなく、画像もパッチに分割して「画像トークン」へと変換します。これにより、「テキスト、画像、テキスト、画像…」と交互に並んだシーケンス(インターリーブデータ)を、完全に地続きのものとして自然に処理・生成(任意順序での入力・出力)することが可能になりました。

  2. トークン化の魔法(CM3leon VQ-GANの進化)
    画像を独自のVQ-GANベースの画像トークナイザーでデジタルな符号(離散トークン)に変換します。例えば、512×512512 \times 512ピクセルの画像を1024個の離散トークン(コードブックサイズ8192)に落とし込むことで、テキストのトークンと完全に等価な表現(共通の語彙セット)として扱えるようにしています。

  3. 安定した学習を支えるアーキテクチャ改良
    画像とテキストを1つのモデルで同時に学習させようとすると、異なるモダリティ間の勾配バランスや干渉により、学習が極めて不安定(勾配爆発など)になります。Metaの研究チームはこれを解決するために、QK-LayerNorm (Query-Key Layer Normalization) の導入や、Dropoutの配置最適化など、アーキテクチャの根幹に執念に似た高度な最適化を施しています。

従来のモデルと何が違うのか?(比較表)

特徴従来のマルチモーダル(Late-fusion)Chameleon(Early-fusion)
構造テキストモデル + 画像エンコーダーの結合1つの共通Transformerアーキテクチャ
画像の扱い特徴量ベクトルとしてテキストに「注入」テキストと等価な「トークン」として処理
出力能力主にテキスト出力(画像は別のDiffusion等に丸投げ)テキストと画像を同時に、地続きで生成可能
学習の難易度比較的安定している非常に不安定(高度な最適化が必要)

Chameleonの処理概要

処理概要 Chameleonの最大の特徴は、図中央の「早期融合(Early Fusion)」にあります。これは、テキストと画像を別々のエンコーダーで処理してから後で結合する(Late Fusion)のではなく、最初から両者を区別なく「トークン」の列として扱い、単一のTransformerで学習させるというアプローチです。 このフローがどのように実現されているのか、図の左から順に見ていきましょう。
1. 前処理とトークン化:すべてを「トークン」へ
モデルがデータを理解するための最初のステップは、異なるモダリティ(テキストと画像)を、モデルが扱える共通の形式である「離散トークン」に変換することです。

  • テキストBPE(上段緑枠):
    テキスト入力は、一般的なLLMと同様にByte-Pair Encoding (BPE) を用いてサブワード単位のトークン列に分割されます。例えば「猫がソファで寝ている。」は、図のように効率的な小さな単位に分割されます。
  • 画像VQ-VAE(下段緑枠):
    ここが重要なポイントです。連続値である画像データも、VQ-VAE(Vector Quantized Variational AutoEncoder)という技術を用いて「離散化」されます。画像は小さなパッチに分割され、それぞれのパッチが事前に学習されたコードブック(辞書)のどのIDに最も近いかによって、テキストと同様の「整数のトークン列」に変換されます。 この段階で、テキストも画像も、モデルにとっては単なる「整数の羅列」として統一されます。

2. 早期融合(Early Fusion):埋め込み空間での統合
図中央の「早期融合」セクションが、Chameleonの核心です。

  • Unified Embedding Space(統一埋め込み空間): テキストトークンと画像トークンは、ここで単一のシーケンスとして結合(インターリーブ)されます。図のように「画像のトークン」と「テキストのトークン」が混在した長い一列のデータとなります。これらは共通の埋め込み層を通じて、高次元のベクトル空間(Unified Embedding Space)へマッピングされます。 このアプローチにより、モデルは「テキストの後に画像が来る」「画像の説明がテキストで続く」といった、モダリティ間の複雑な依存関係を学習の最初期段階から捉えることが可能になります。

3. Chameleon Transformer:最新技術の結集
統合されたトークン列は、右側の巨大なTransformerブロック(N個のレイヤー)に入力されます。ChameleonはGPT系列と同様の「デコーダーのみ」のアーキテクチャを採用しており、自己回帰的(次のトークンを予測する方式)に学習します。 図に示されているように、ChameleonのTransformerブロックには、学習の安定性と性能を向上させるための最新技術が多数採用されています。

  • RMSNorm & QK-Norm: 従来のLayerNormの代わりに、計算効率が良く安定性が高いRMSNorm(二乗平均平方根正規化)を採用。さらに、Attention層のクエリ(Q)とキー(K)に対しても正規化(QK-Norm)を適用することで、学習の不安定さを抑えています。
  • SwiGLU: 前向きネットワーク(Feed-Forward Network)の活性化関数には、LLaMAなどでも採用され、高い性能が実証されているSwiGLU(ゲート付き線形ユニット)が用いられています。
  • RoPE (回転位置埋め込み):トークンの位置情報を埋め込む手法として、相対的な位置関係を捉えるのに優れたRoPEが適用されています。
  • トークンマスキング(因果関係マスク):自己回帰生成を行うため、未来のトークン情報を参照できないようにする因果関係マスクが適用されます。これはテキスト生成だけでなく、画像の生成においても同様に適用されます。

4. 出力とデコード:トークンからモダリティへ
Transformerによって次々と生成されたトークンは、最後にそれぞれのモダリティに応じたデコーダーによって、人間が理解できる形式に戻されます。 テキスト生成: テキスト用のデコーダー(トークンIDを単語に戻す)を経て、最終的な文章が出力されます。 画像生成(VQ-VAEデコーダー): モデルが生成した画像の離散トークン列は、VQ-VAEのデコーダーに入力され、元の画素データへと再構成されます。

Chameleonの実装(概念的なシンプルな実装)

ChameleonModelの実装

概要

以下のコードでは、Chameleonモデルの全体像を定義する ChameleonModel クラスをPyTorchを用いて実装します。このクラスは、複数の ChameleonLayer を重ねたTransformerアーキテクチャを構築し、入力されたトークンシーケンスに対して順伝播処理を行い、最終的な予測結果を出力する役割を担います。

import torch
import torch.nn as nn
import torch.nn.functional as F
import math
import requests
from PIL import Image
from io import BytesIO
from transformers import AutoTokenizer
import matplotlib.pyplot as plt
import japanize_matplotlib


def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0):
freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim))
t = torch.arange(end, device=freqs.device, dtype=torch.float32)
freqs = torch.outer(t, freqs)
freqs_cis = torch.polar(torch.ones_like(freqs), freqs)
return freqs_cis


class ChameleonModel(nn.Module):
def __init__(self, total_vocab, dim=128, depth=4, heads=4):
super().__init__()
self.total_vocab = total_vocab
self.tok_embeddings = nn.Embedding(self.total_vocab, dim)

self.layers = nn.ModuleList([
nn.ModuleDict({
'norm1': RMSNorm(dim),
'attention': ChameleonAttention(dim, heads),
'norm2': RMSNorm(dim),
'ffn': SwiGLU(dim, int(dim * 4 * 2 / 3))
}) for _ in range(depth)
])
self.norm = RMSNorm(dim)
self.output_layer = nn.Linear(dim, self.total_vocab, bias=False)

def forward(self, input_ids):
B, L = input_ids.shape
x = self.tok_embeddings(input_ids)

head_dim = self.layers[0]['attention'].head_dim
freqs_cis = precompute_freqs_cis(head_dim, L).to(x.device)

for layer in self.layers:
attn_out = layer['attention'](layer['norm1'](x), freqs_cis)
x = x + attn_out
x = x + layer['ffn'](layer['norm2'](x))

x = self.norm(x)
return self.output_layer(x)

ChameleonModel__init__ メソッドでは、モデルの構成要素を定義します。

  • tok_embeddings: 入力トークンIDを分散表現ベクトルに変換する埋め込み層です。テキストと画像トークンを合わせた総語彙数 total_vocab を受け取ります。
  • layers: Transformerの基本ブロックである ChameleonLayerdepth 層重ねています。各レイヤーは RMSNormChameleonAttention、そして SwiGLU を含む nn.ModuleDict として定義されています。
  • norm: 最終層の出力を正規化する RMSNorm です。
  • output_layer: 最終的な出力を語彙数分の次元に変換し、次のトークンの確率分布を計算するための線形層です。

forward メソッドでは、実際の順伝播処理を定義しています。

  1. 入力 input_ids (形状: [バッチサイズ, シーケンス長]) を tok_embeddings でベクトルに変換します。
  2. precompute_freqs_cis を使って、RoPE(回転位置埋め込み)に必要な複素数をあらかじめ計算しておきます。
  3. forループ内で、各 ChameleonLayer を順番に適用します。アテンション層の出力とFFN層の出力は、それぞれ残差接続(x = x + ...)によって入力に足し合わされます。これは深層学習で勾配消失を防ぐための重要なテクニックです。
  4. 最後に、全体の出力を norm で正規化し、output_layer を通して次のトークン予測のロジットを返します。

RMSNormの実装

概要

ここでは、Transformerの学習を安定させるために用いられる正規化手法の一つである RMSNorm を実装します。従来の LayerNorm よりも計算効率が高いとされており、PyTorchの nn.Module を継承してカスタムレイヤーとして定義します。

class RMSNorm(nn.Module):
def __init__(self, dim, eps=1e-6):
super().__init__()
self.eps = eps
self.weight = nn.Parameter(torch.ones(dim))
def forward(self, x):
norm_x = torch.mean(x ** 2, dim=-1, keepdim=True)
return x * torch.rsqrt(norm_x + self.eps) * self.weight

RMSNormforward メソッドでは、入力テンソル x に対して正規化処理を行います。まず、torch.mean(x ** 2, dim=-1, keepdim=True) で、テンソルの最後の次元(特徴量次元)に沿って要素の二乗平均を計算します。次に、torch.rsqrt でその値の逆数の平方根を計算し、元の入力 x に乗算します。これにより、ベクトルの大きさが正規化されます。最後に、学習可能なパラメータである self.weight を乗算することで、モデルが正規化の度合いを適応的に調整できるようにしています。self.eps は、ゼロ除算を防ぐための小さな値です。

ChameleonAttentionの実装

概要

次に、Chameleonモデルの心臓部であるアテンション機構 ChameleonAttention を実装します。このコードには、Query(Q)とKey(K)に回転位置埋め込み(RoPE)を適用する apply_rotary_emb 関数と、QK正規化やマルチヘッドアテンションの計算ロジックを含む ChameleonAttention モジュール本体が含まれています。

def apply_rotary_emb(xq: torch.Tensor, xk: torch.Tensor, freqs_cis: torch.Tensor):
xq_ = torch.view_as_complex(xq.float().reshape(*xq.shape[:-1], -1, 2))
xk_ = torch.view_as_complex(xk.float().reshape(*xk.shape[:-1], -1, 2))
ndim = xq_.ndim
shape = [d if i == 1 or i == ndim - 1 else 1 for i, d in enumerate(xq_.shape)]
freqs_cis = freqs_cis.view(*shape)
xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(3)
xk_out = torch.view_as_real(xk_ * freqs_cis).flatten(3)
return xq_out.type_as(xq), xk_out.type_as(xk)


class ChameleonAttention(nn.Module):
def __init__(self, dim, num_heads):
super().__init__()
self.num_heads = num_heads
self.head_dim = dim // num_heads
self.wq = nn.Linear(dim, dim, bias=False)
self.wk = nn.Linear(dim, dim, bias=False)
self.wv = nn.Linear(dim, dim, bias=False)
self.wo = nn.Linear(dim, dim, bias=False)
self.q_norm = RMSNorm(self.head_dim)
self.k_norm = RMSNorm(self.head_dim)

def forward(self, x, freqs_cis):
B, L, D = x.shape
q = self.wq(x).view(B, L, self.num_heads, self.head_dim)
k = self.wk(x).view(B, L, self.num_heads, self.head_dim)
v = self.wv(x).view(B, L, self.num_heads, self.head_dim)
q = self.q_norm(q)
k = self.k_norm(k)
q, k = apply_rotary_emb(q, k, freqs_cis)
scores = torch.einsum('blhd,bshd->bhls', q, k) / math.sqrt(self.head_dim)
attn = F.softmax(scores, dim=-1)
out = torch.einsum('bhls,bshd->blhd', attn, v).reshape(B, L, D)
return self.wo(out)

ChameleonAttention は、2つの主要部分から構成されます。

  1. apply_rotary_emb 関数: この関数は、Queryベクトル xq とKeyベクトル xk に回転位置埋め込み(RoPE)を適用します。入力テンソルを複素数として扱い、事前に計算された回転行列 freqs_cis を乗算することで、トークンの絶対位置を相対的な回転として埋め込みます。これにより、モデルはトークン間の相対的な位置関係をより効果的に捉えることができます。

  2. ChameleonAttention クラス:

    • init: Query, Key, Value, および出力のための線形層(wq, wk, wv, wo)を初期化します。Chameleonの論文で提案されているように、QueryとKeyの正規化を行うための q_normk_norm もここで定義します。
    • forward:
      • 入力 x を線形層に通して、Query, Key, Value(Q, K, V)を生成します。
      • q_normk_norm を使って、QとKをそれぞれ正規化します。これが論文で強調されている QK-LayerNorm です。
      • apply_rotary_emb を呼び出し、正規化されたQとKに位置情報を付与します。
      • torch.einsum を使ってQとKの内積を計算し、アテンションスコアを求めます。計算結果はヘッドの次元数 head_dim の平方根でスケーリングされ、学習の安定化が図られます。
      • スコアを F.softmax で正規化し、アテンションウェイトに変換します。
      • 最後に、アテンションウェイトとValue(V)の加重和を torch.einsum で計算し、出力層 wo を通して最終的なアテンション出力を得ます。

SwiGLUの実装

概要

ここでは、TransformerのFeed-Forward層で利用される活性化関数 SwiGLU (Gated Linear Unit with SiLU) を実装します。SwiGLU は、LLaMAなどの高性能なモデルで採用されており、表現力を向上させる効果があります。この実装では、3つの線形層を組み合わせてゲート機構を実現しています。

class SwiGLU(nn.Module):
def __init__(self, in_features, hidden_features):
super().__init__()
self.w1 = nn.Linear(in_features, hidden_features, bias=False)
self.w2 = nn.Linear(in_features, hidden_features, bias=False)
self.w3 = nn.Linear(hidden_features, in_features, bias=False)
def forward(self, x):
return self.w3(F.silu(self.w1(x)) * self.w2(x))

SwiGLUforward メソッドは、入力 x を2つの異なる線形層 self.w1self.w2 に通します。w1 の出力には SiLU (Sigmoid-weighted Linear Unit) 活性化関数 F.silu が適用され、その結果が w2 の出力と要素ごとに乗算されます。このゲート(Gate)のような機構が、情報の流れを動的に制御し、モデルの表現力を高めます。最後に、乗算された結果が3つ目の線形層 self.w3 を通って、元の次元に戻されます。

ChameleonProcessorの実装

概要 このセクションでは、テキストと画像という異なるモダリティのデータを一元的に扱うための ChameleonProcessor クラスを実装します。このクラスは、テキスト用のBPEトークナイザーと画像用のVQ-GANエンコーダーを内部に持ち、テキストと画像をそれぞれトークン化した後、それらを結合してモデルへの入力シーケンスを作成する役割を担います。

class ChameleonProcessor:
"""テキスト(BPE)と画像(VQ-VAE)のトークン化・結合を管理する統合クラス"""
def __init__(self, text_vocab_size=50257, image_vocab_size=8192, embed_dim=256):
self.text_vocab_size = text_vocab_size
self.image_vocab_size = image_vocab_size

# トークンIDのオフセットと特殊トークンの定義
self.image_offset = text_vocab_size
self.boi_token_id = text_vocab_size + image_vocab_size
self.eoi_token_id = self.boi_token_id + 1
self.total_vocab = self.eoi_token_id + 1

# 1. BPEトークナイザーの初期化
self.tokenizer = AutoTokenizer.from_pretrained("gpt2")

# 2. VQ-VAEエンコーダーの初期化
self.vqgan = VQGANEncoder(image_vocab_size=image_vocab_size, embed_dim=embed_dim)
self.vqgan.eval() # エンコード専用のためevalモード

def encode_text(self, text: str) -> list[int]:
"""テキストをBPEトークンに変換"""
return self.tokenizer.encode(text)

def decode_text(self, tokens: list[int]) -> str:
"""トークンIDのリストからテキストを復元(画像や特殊トークンは除外)"""
valid_tokens = [t for t in tokens if t < self.text_vocab_size]
return self.tokenizer.decode(valid_tokens)

def encode_image(self, img_tensor: torch.Tensor, device="cpu") -> list[int]:
"""画像テンソルをVQ-VAEでエンコードし、オフセットを加算"""
self.vqgan.to(device)
img_tensor = img_tensor.to(device)
with torch.no_grad():
raw_tokens = self.vqgan.encode(img_tensor)[0].tolist()
return [t + self.image_offset for t in raw_tokens]

def prepare_interleaved_input(self, text: str, img_tensor: torch.Tensor, device="cpu") -> torch.Tensor:
"""テキストと画像を処理し、モデルに入力可能な1次元シーケンスを生成"""
text_tokens = self.encode_text(text)
image_tokens = self.encode_image(img_tensor, device)

# [<boi>] + [画像トークン] + [<eoi>] + [テキストトークン]
interleaved = [self.boi_token_id] + image_tokens + [self.eoi_token_id] + text_tokens
return torch.tensor(interleaved, dtype=torch.long).unsqueeze(0)

ChameleonProcessor は、Chameleonモデルのデータ前処理をカプセル化する重要なクラスです。

  • init:
    • テキストと画像の語彙サイズを受け取り、モデルで扱うトークンIDの範囲を定義します。具体的には、画像トークンIDに image_offset を加算し、さらに画像シーケンスの開始と終了を示す特殊トークン boi_token_ideoi_token_id を定義します。
    • テキストをBPEトークンに変換するための AutoTokenizergpt2 から読み込みます。
    • 画像を離散トークンに変換するための VQGANEncoder を初期化します。
  • encode_text: 入力された文字列をBPEトークナイザーでトークンIDのリストに変換します。
  • decode_text: モデルが生成したトークンIDのリストから、テキスト部分のみを抽出して文字列に復元します。
  • encode_image: 入力された画像テンソルを vqgan でエンコードし、得られたトークンIDにオフセット self.image_offset を加算します。これにより、テキストの語彙と衝突しない一意のIDが割り当てられます。
  • prepare_interleaved_input: このメソッドがChameleonの「早期融合」を実現する核となる部分です。テキストと画像をそれぞれエンコードし、[<boi>] + [画像トークン] + [<eoi>] + [テキストトークン] という順序で1つのシーケンスに結合します。この統一されたシーケンスが、Transformerモデルへの最終的な入力となります。

VQGANEncoderの実装

概要 以下のコードでは、画像を離散的なトークン列に変換するための VQGANEncoder を実装します。このエンコーダーは、複数の畳み込み層(CNN)で画像の特徴量を抽出し、その特徴量を後述の VectorQuantizer を通じて最も近いコードブック内のベクトルインデックス(=画像トークン)に変換する役割を持ちます。

class VQGANEncoder(nn.Module):
def __init__(self, image_vocab_size=8192, embed_dim=256):
super().__init__()
self.encoder = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=4, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(64, 128, kernel_size=4, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(128, 256, kernel_size=4, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(256, embed_dim, kernel_size=4, stride=2, padding=1),
)
self.quantizer = VectorQuantizer(image_vocab_size, embed_dim)

def encode(self, x):
z = self.encoder(x)
indices = self.quantizer(z)
return indices.view(x.shape[0], -1)

VQGANEncoder__init__ メソッドでは、2つの主要なコンポーネントを定義します。

  • self.encoder: 複数の nn.Conv2dnn.ReLU を重ねたシンプルな畳み込みニューラルネットワークです。入力画像 x (形状: [B, 3, H, W]) を受け取り、ストライド付きの畳み込みを繰り返すことで、空間的な次元を削減しながら特徴量を抽出します。最終的に、特徴マップ z (形状: [B, embed_dim, H', W']) を出力します。
  • self.quantizer: 抽出された特徴マップ z を離散的なトークンIDに変換する VectorQuantizer のインスタンスです。

encode メソッドでは、まず入力画像 xself.encoder に通して特徴マップ z を得ます。次に、この zself.quantizer に渡して、各パッチに対応するトークンID(インデックス)を取得します。最後に、indices.view(x.shape[0], -1) を使って、トークンIDのグリッドを1次元のシーケンス(形状: [B, H' * W'])に平坦化して返します。

VectorQuantizerの実装

概要 ここでは、画像エンコーダから出力された連続的な特徴量ベクトルを、離散的なトークンIDに変換する VectorQuantizer を実装します。コードブック(埋め込みベクトル群)を保持し、入力された各ベクトルと最も距離が近いコードブックのインデックスを計算することで、「ベクトル量子化」と呼ばれるプロセスを実現します。

class VectorQuantizer(nn.Module):
def __init__(self, num_embeddings, embedding_dim):
super().__init__()
self.num_embeddings = num_embeddings
self.embedding_dim = embedding_dim
self.embedding = nn.Embedding(self.num_embeddings, self.embedding_dim)
self.embedding.weight.data.uniform_(-1.0 / self.num_embeddings, 1.0 / self.num_embeddings)

def forward(self, z):
z_flattened = z.permute(0, 2, 3, 1).reshape(-1, self.embedding_dim)
d = (
torch.sum(z_flattened ** 2, dim=1, keepdim=True)
+ torch.sum(self.embedding.weight ** 2, dim=1)
- 2 * torch.matmul(z_flattened, self.embedding.weight.t())
)
min_encoding_indices = torch.argmin(d, dim=1)
z_q_idx = min_encoding_indices.view(z.shape[0], z.shape[2], z.shape[3])
return z_q_idx

VectorQuantizer は、連続的な特徴ベクトルを離散的なインデックスに変換する役割を担います。

  • init:
    • self.embedding: num_embeddings 個のベクトル(コードブック)を保持する埋め込み層を定義します。各ベクトルは embedding_dim 次元の大きさです。
  • forward:
    • 入力された特徴マップ z (形状: [B, C, H, W]) を、permutereshape を使って、ベクトルのリスト(形状: [BHW, C])に変換します。
    • 変数 d の計算が量子化の核となる部分です。これは、入力された各ベクトル z_flattened と、コードブック内のすべてのベクトル self.embedding.weight との間のユークリッド距離の二乗を効率的に計算しています。展開すると (a-b)^2 = a^2 + b^2 - 2ab となることを利用した賢い実装です。
    • torch.argmin(d, dim=1) を使って、各入力ベクトルに対して最も距離が近いコードブックベクトルのインデックス(ID)を求めます。
    • 最後に min_encoding_indices.view(...) で、1次元のインデックスリストを元の特徴マップと同じ空間的な形状([B, H, W])に戻して返します。

モデルの初期化とデータの読み込み

これまでに定義した各モジュールを統合し、実際にモデルを動かすための準備を行います。具体的には、テキストと画像(Wikimedia Commonsから取得した猫のサンプル画像)を読み込み、ChameleonProcessor を使ってモデルへの入力となるトークンシーケンスを作成します。

サンプル画像のクレジット表記について

テストに使用している猫の画像はWikimedia Commonsのコンテンツです。クリエイティブ・コモンズ(CC)ライセンスに基づき、記事の末尾などでクレジット表記(作者名、ライセンス種別)を行う必要があります。

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("テスト用データを読み込み、トークン化中...")

# プロセッサの初期化
processor = ChameleonProcessor()

# --- A. テキストデータの準備 ---
text = "この画像には、猫が描かれています。猫は、白い壁に乗っているように、壁の上に寝ています。"

# --- B. 画像データの準備 ---
wiki_image_url = "https://upload.wikimedia.org/wikipedia/commons/thumb/1/15/Cat_August_2010-4.jpg/1920px-Cat_August_2010-4.jpg"
headers = {'User-Agent': 'Mozilla/5.0'}
response = requests.get(wiki_image_url, headers=headers)
img = Image.open(BytesIO(response.content)).convert("RGB").resize((64, 64))

# 画像をmatplotlibで表示
plt.figure(figsize=(4, 4))
plt.imshow(img)
plt.title(f"Input Image (64x64):{text}")
plt.axis("off")
plt.show()

# PIL画像をテンソルに変換
img_data = list(img.getdata())
img_tensor = torch.tensor(img_data, dtype=torch.float32).view(64, 64, 3)
img_tensor = (img_tensor.permute(2, 0, 1).unsqueeze(0) / 255.0) * 2.0 - 1.0

# --- C. データの結合 (プロセッサを経由) ---
input_sequence = processor.prepare_interleaved_input(text, img_tensor, device=device)
input_sequence = input_sequence.to(device)

print(f"データの準備完了!総トークン数: {input_sequence.size(1)}")

このコードブロックでは、学習と推論に使用するデバイス(GPUまたはCPU)を決定し、ChameleonProcessor を初期化します。

  • A. テキストデータの準備: 学習データとなる日本語のテキストを文字列として定義します。
  • B. 画像データの準備:
    • requests ライブラリを使って、Wikimedia Commons上にある猫の画像のURLからデータを取得します。
    • PILBytesIO を使って、取得したバイナリデータを画像として開き、RGB形式に変換後、実験のために小さな 64x64 サイズにリサイズします。
    • matplotlib を使って、入力画像と対応するテキストを可視化しています。
    • 最終的に、PIL画像をPyTorchテンソルに変換します。permute(2, 0, 1) で次元の順番を [高さ, 幅, チャンネル] から [チャンネル, 高さ, 幅] というPyTorch標準の形式に並べ替え、値を [-1, 1] の範囲に正規化します。
  • C. データの結合: processor.prepare_interleaved_input を呼び出し、準備したテキストと画像テンソルを、モデルに入力できる単一のトークンシーケンス input_sequence に変換します。

モデルの学習

ここでは、準備した単一のデータ(猫の画像と説明文)を使って、ChameleonModel の学習(過学習)を行います。モデルをインスタンス化し、AdamWオプティマイザを設定した後、クロスエントロピー損失を計算してモデルのパラメータを更新する、という基本的な学習ループを実装します。

model = ChameleonModel(total_vocab=processor.total_vocab, dim=1024, depth=4, heads=8).to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3)
epochs = 200

print(f"\n学習デバイス: {device}")
print("学習を開始します (過学習)...")

model.train()
for epoch in range(epochs):
optimizer.zero_grad()

x = input_sequence[:, :-1]
y = input_sequence[:, 1:]

logits = model(x)
loss = F.cross_entropy(logits.view(-1, model.total_vocab), y.reshape(-1))

loss.backward()
optimizer.step()

if (epoch + 1) % 20 == 0:
print(f"Epoch {epoch+1:3d}/{epochs} | Loss: {loss.item():.4f}")

このブロックでは、モデルの学習処理を実装しています。これは概念実証のため、単一のデータセットに対して意図的に過学習(overfitting)させています。

  • model.train() でモデルを学習モードに設定します。
  • forループ内で、エポックごとに以下の処理を繰り返します。
    • optimizer.zero_grad() で、前回の勾配計算結果をリセットします。
    • x = input_sequence[:, :-1] で、入力シーケンスの最後のトークンを除いたものをモデルへの入力 x とします。
    • y = input_sequence[:, 1:] で、入力シーケンスの最初のトークンを除いたものを、モデルが予測すべき正解ラベル y とします。これは、次のトークンを予測する「Next Token Prediction」タスクの典型的な実装です(Teacher Forcing)。
    • logits = model(x) で、モデルの順伝播計算を行い、次のトークンの予測結果(ロジット)を得ます。
    • F.cross_entropy を使って、モデルの予測 logits と正解ラベル y との間のクロスエントロピー損失を計算します。
    • loss.backward() で、損失に基づいて勾配を計算します(逆伝播)。
    • optimizer.step() で、計算された勾配を使ってモデルの全パラメータを更新します。

予測のテスト

学習が完了したモデルの性能を評価するため、テキスト生成テストを行います。ここでは、入力として与えた画像のトークン列をプロンプトとし、それに続くテキストをモデルに自己回帰的に生成させます。生成されたトークンをデコードし、元のテキストと一致するかを確認することで、モデルがデータを正しく学習できたかを検証します。

print("\n学習完了!生成テストを行います...")
model.eval()

# [<boi> + 画像トークン + <eoi>] までをプロンプトとして切り出す
# ※画像は64x64が16x16に圧縮されるため、16トークン生成されます。
num_image_tokens = 16
prompt_length = 1 + num_image_tokens + 1
generated_tokens = input_sequence[:, :prompt_length].clone()

# 生成するテキストトークンの数(正解テキストと同じ長さを生成してみる)
num_generate = input_sequence.size(1) - prompt_length

with torch.no_grad():
for _ in range(num_generate):
logits = model(generated_tokens)
next_token_logits = logits[:, -1, :]

# モーダル制約: テキストボキャブラリ以外をマスキング
next_token_logits[:, processor.text_vocab_size:] = float('-inf')

next_token = torch.argmax(next_token_logits, dim=-1, keepdim=True)
generated_tokens = torch.cat([generated_tokens, next_token], dim=1)

# 生成されたテキストトークンIDの抽出とデコード
output_text_tokens = generated_tokens[0, prompt_length:].cpu().tolist()

try:
output_text = processor.decode_text(output_text_tokens)
print("\n【生成されたテキスト】")
print(output_text)
except Exception as e:
print(f"\n⚠️ デコード中にエラーが発生しました: {e}")

過学習させたモデルを使って、テキスト生成を行います。

  • model.eval() でモデルを推論モードに切り替えます。
  • プロンプトとして、入力シーケンスの画像部分(<boi> + 画像トークン + <eoi>)を generated_tokens にセットします。
  • with torch.no_grad() ブロック内で、勾配計算を無効にして推論の効率を上げます。
  • forループを回して、1トークンずつ自己回帰的にテキストを生成します。
    • 現在の generated_tokens をモデルに入力し、次のトークンのロジット logits を得ます。
    • logits[:, -1, :] で、シーケンスの最後のトークン(=次に生成されるトークン)に関するロジットのみを抽出します。
    • next_token_logits[:, processor.text_vocab_size:] = float('-inf') の部分が重要です。ここでは モーダル制約 をかけており、テキストを生成する段階では画像トークンや特殊トークンが生成されないように、それらのロジットをマイナス無限大に設定しています。
    • torch.argmax を使って、最も確率の高いトークンを次のトークン next_token として選び出します(Greedy Search)。
    • torch.cat で、生成されたトークンを generated_tokens に追加し、次のループの入力とします。
  • 最後に、生成されたトークン列からプロンプト部分を除き、processor.decode_text を使って人間が読めるテキストに変換します。

実行結果

テスト用データを読み込み、トークン化中...

概要

データの準備完了!総トークン数: 76

学習デバイス: cuda
学習を開始します (過学習)...
Epoch 20/200 | Loss: 0.0043
Epoch 40/200 | Loss: 0.0003
Epoch 60/200 | Loss: 0.0002
Epoch 80/200 | Loss: 0.0001
Epoch 100/200 | Loss: 0.0001
Epoch 120/200 | Loss: 0.0001
Epoch 140/200 | Loss: 0.0001
Epoch 160/200 | Loss: 0.0001
Epoch 180/200 | Loss: 0.0001
Epoch 200/200 | Loss: 0.0001

学習完了!生成テストを行います...

【生成されたテキスト】
この画像には、猫が描かれています。猫は、白い壁に乗っているように、壁の上に寝ています。

実行結果を見ると、まず 学習を開始します と表示され、20エポックごとに損失(Loss)が出力されています。損失が 0.0001 のように非常に小さい値に収束しており、モデルが与えられた単一のデータを完全に記憶した(過学習した)ことがわかります。 その後、生成テストを行います... が表示され、最終的に 【生成されたテキスト】 として、元の入力と全く同じ文章が生成されています。これは、モデルが画像の内容を理解し、それに対応するテキストを正しく生成できるようになったことを示しています。意図通りに過学習が成功した結果と言えます。

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

これまでは概念実証のためのシンプルな実装を見てきましたが、ここではHugging Faceで公開されているMetaの公式Chameleon-7Bモデルを使って、実際の推論を試します。transformers ライブラリを利用し、モデルとプロセッサを読み込んで、画像とプロンプトからテキストを生成するまでの一連の流れを解説します。

モデルの利用に関する重要事項
  • Gated Model: Chameleonはライセンス同意が必要な「Gated Model」です。コードを実行するには、事前にHugging Faceのモデルページで利用規約に同意し、アクセストークンを取得する必要があります。
  • 利用規約の遵守: Metaの利用規約(Acceptable Use Policy)に従う必要があります。不適切なコンテンツの生成は禁じられています。
  • 引用: このモデルを利用・参照する際は、以下の論文を引用することが推奨されます。
    @article{Chameleon_Team_Chameleon_Mixed-Modal_Early-Fusion_2024,
    author = {Chameleon Team},
    doi = {10.48550/arXiv.2405.09818},
    journal = {arXiv preprint arXiv:2405.09818},
    title = {Chameleon: Mixed-Modal Early-Fusion Foundation Models},
    url = {https://github.com/facebookresearch/chameleon},
    year = {2024}
    }
import requests
from io import BytesIO
import torch
import requests
from PIL import Image
from transformers import ChameleonProcessor, ChameleonForConditionalGeneration

# 1. デバイスとデータ型の設定
# 7Bモデルはサイズが大きいため、bfloat16(またはfloat16)で読み込むことを推奨します
device = "cuda" if torch.cuda.is_available() else "cpu"
torch_dtype = torch.bfloat16 if torch.cuda.is_available() else torch.float32

print("モデルとプロセッサを読み込み中...")
model_id = "facebook/chameleon-7b"

# 2. プロセッサとモデルの初期化
# ※事前に huggingface-cli login を済ませておくか、token="YOUR_TOKEN" を引数に渡してください
processor = ChameleonProcessor.from_pretrained(model_id)
model = ChameleonForConditionalGeneration.from_pretrained(
model_id,
torch_dtype=torch_dtype,
device_map="auto" # GPUメモリに自動配置
)

# 3. テスト用画像データの準備 (Wikipediaの猫画像)
print("画像をダウンロード中...")
wiki_image_url = "https://upload.wikimedia.org/wikipedia/commons/thumb/1/15/Cat_August_2010-4.jpg/1920px-Cat_August_2010-4.jpg"

# Wikipedia/WikimediaはUser-Agentが弾かれることがあるため、少し具体的にします
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(wiki_image_url, headers=headers)

# URLエラーや403アクセス拒否がないかチェック
response.raise_for_status()

# メモリ上にバイナリデータを展開してからPILで開く
image = Image.open(BytesIO(response.content)).convert("RGB")
print("画像の読み込みに成功しました。")

# 4. プロンプトの作成
# Chameleonモデルでは、画像を埋め込む位置に特殊トークン `<image>` を配置します
prompt = "画像に何が写っているか、詳細に説明してください。\n<image>"

# 5. 入力データの処理
# processorが自動的に画像をVQ-VAEトークン(またはテンソル)に変換し、テキストと統合します
inputs = processor(
images=image,
text=prompt,
return_tensors="pt"
).to(model.device, dtype=torch_dtype)

# 6. 生成プロセスの実行
print("推論を実行中...")
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=100, # 生成する最大トークン数
do_sample=True, # サンプリングを有効化
temperature=0.7, # 生成の多様性
top_p=0.9
)

# 7. 結果のデコードと表示
# 入力プロンプト部分をスキップして、生成されたテキストのみを取得したい場合の設定
generated_text = processor.decode(outputs[0], skip_special_tokens=True)

print("\n【生成されたテキスト】")
print(generated_text)

Hugging Faceの transformers ライブラリを使うことで、非常に簡潔なコードで推論を実行できます。

  1. モデルとプロセッサの読み込み:
    • model_id で使用するモデル(facebook/chameleon-7b)を指定します。
    • ChameleonProcessor.from_pretrained で、対応するプロセッサを読み込みます。これには画像の前処理やテキストのトークン化の機能が含まれています。
    • ChameleonForConditionalGeneration.from_pretrained で、学習済みのモデル本体を読み込みます。7Bのような巨大なモデルでは、torch_dtype=torch.bfloat16 のように半精度で読み込むことで、メモリ使用量を削減できます。device_map="auto" は、利用可能なGPUにモデルを自動的に配置する便利なオプションです。
  2. 画像の取得と安全な読み込み:
    • ネットワークを介して画像をダウンロードするため、requests を使用しています。Wikipediaのサーバーによるアクセス制限を回避するため、具体的なユーザーエージェント(headers)を定義してリクエストを送信します。
    • response.raise_for_status() により、HTTPステータスコードがエラー(403や404など)を示した場合に例外を発生させ、壊れたデータでの処理を防ぐ堅牢な実装にしています。
    • 取得したバイナリデータを BytesIO(response.content) でメモリ上に展開し、Pillowの Image.open を用いて安全にRGB画像としてロードしています。
  3. 入力データの処理:
    • プロンプトとして、画像とテキストを組み合わせたものを定義します。Chameleonでは、画像の挿入箇所を特殊トークン <image> で指定します。
    • processor() に画像とテキストを渡すだけで、ライブラリが内部でトークン化と埋め込みを行い、モデルへの入力 inputs を自動で作成してくれます。
  4. 生成プロセスの実行:
    • model.generate() を呼び出して、テキスト生成を開始します。
    • max_new_tokens で生成する最大のトークン数を指定します。
    • do_sample=True, temperature=0.7, top_p=0.9 などのパラメータは、生成されるテキストの多様性と品質を調整するためのものです。これにより、毎回少しずつ異なる、より自然な文章が生成されやすくなります。
  5. 結果のデコード:
    • 最後に、processor.decode() を使って、生成されたトークンID outputs[0] を人間が読める文字列に変換します。skip_special_tokens=True を指定することで、<image> のような特殊トークンを除いた綺麗なテキストが得られます。

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

モデルとプロセッサを読み込み中...
画像をダウンロード中...
画像の読み込みに成功しました。
[transformers] Setting `pad_token_id` to `eos_token_id`:2 for open-end generation.
推論を実行中...

【生成されたテキスト】
画像に何が写っているか、詳細に説明してください。
The image depicts a tiger cat lying on a white wall. The cat has a black and orange striped coat with dark spots. It is resting with its head raised and its eyes closed.
()その画像には、白い壁の上に横たわるトラ猫が写っています。この猫は、黒とオレンジの縞模様に暗い斑点のある毛並みをしています。頭を持ち上げ、目を閉じた状態でくつろいでいます。

実行結果から得られたテキストと、その描写精度について考察します。

  • 極めて正確な画像理解: 入力されたWikipediaの猫画像に対し、モデルは The image depicts a tiger cat lying on a white wall...(白い壁の上に横たわるトラ猫が写っています...)と出力しました。 実際の画像には、白いコンクリート壁の上で頭を上げ、目を閉じて丸くなっている茶トラの猫が写っており、lying on a white wall(白い壁の上に横たわる)、striped coat(縞模様の毛並み)、head raised and its eyes closed(頭を持ち上げ目を閉じている)といった特徴が完璧に描写されていることが分かります。
  • 初期融合(Early-fusion)の真価: この極めて高い描写能力は、テキストと画像を最初から等価なトークンとして単一のTransformerで地続きに処理する、Chameleonの「初期融合アーキテクチャ」の強力さを裏付けています。画像データがプロセッサ経由で離散的な画像トークンに圧縮され、プロンプトの <image> 特殊トークンの位置にシームレスに挿入されたことで、モデルは画像とそれに続く「詳細に説明してください」というテキスト指示(コンテキスト)を完全に一体化して解釈し、自己回帰的な生成を行うことができました。
  • 高度なゼロショット記述能力: この検証コードは追加の微調整(ファインチューニング)を一切行わず、Metaの提供する公式の学習済み7Bモデルをそのまま呼び出したものです。それにもかかわらず、文脈のねじれや不自然な単語の出力(ハルシネーション)を起こすことなく、非常に滑らかで詳細な説明文を英語で生成できており、Chameleon基礎モデル自体の極めて高いゼロショット記述能力と堅牢性が実証されています。

まとめ

本記事では、画像とテキストを最初から等価なトークンとして扱う、真の「初期融合型」マルチモーダルモデルである Chameleon について、その革新的なアーキテクチャと実装方法を解説しました。

記事を通じて、以下の内容を実践しました。

  • Chameleonのコア技術の理解: テキストと画像を単一のTransformerで処理する「早期融合」の概念や、それを安定させるための RMSNorm, QK-LayerNorm, SwiGLU などの主要技術を学びました。
  • 概念モデルのスクラッチ実装: PyTorchを用いて、ChameleonModel や画像トークン化のための VQGANEncoder などをゼロから構築し、モデルの内部構造をコードレベルで確認しました。
  • モデルの学習と推論の検証: 実装したモデルを単一データで過学習させ、画像を入力プロンプトとして正しくテキストを生成できることを検証しました。
  • Hugging Faceモデルによる実践的な推論: transformers ライブラリを利用し、Metaが公開している7Bサイズの学習済みモデルを使って、より実践的な推論を手軽に実行する方法を確認しました。

Chameleonのように、異なるモダリティをシームレスに扱うアプローチは、今後のマルチモーダルAIの発展における重要なマイルストーンです。ぜひ本記事のコードを参考に、このエキサイティングな技術をご自身のプロジェクトでも試してみてください。


画像出典: Wikimedia Commons, by Thegreenj, CC BY-SA 3.0

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