暇さえあればアルゴリズムいじり

暇があればアルゴリズムいじり

Obsessed with algorithms whenever I have a free moment.

AI & IT Engineer / Father of 3

"Dream shall be realized with dream — Always tinkering with algorithms"

RustのEnumは超強力

Rustの 列挙型(enum は、他の言語のEnumよりも全然多機能で、Rustという言語の「安全性」と「表現力」を支えるRust的に重要な機能です。

単なる「定数のリスト」ではなく、 「データの型を拡張できる仕組み」 として理解すると分かりやすくなるかもしれません。

girl reading a book

概要:列挙型とは?

列挙型(enum)は、 「いくつかの選択肢のうち、いずれか一つ」 の状態を表現するデータ構造です。

例えば、「信号機の色」なら「赤・黄・青」のいずれか一つ、「メッセージの送信状態」なら「成功・失敗・待機」のいずれか一つです。

enum TrafficLight {
    Red,
    Yellow,
    Green,
}

Rustの列挙型ならではの大きな特徴

他の言語のEnumと大きく異なるのはここからです。

① 各バリアントに「データ」を持たせられる

これがRustのenumの最大の強みです。ただのラベルではなく、それぞれの選択肢に異なる型のデータを持たせることができます。

enum Message {
    Quit,                       // データなし
    Move { x: i32, y: i32 },    // 構造体のようなデータ
    Write(String),              // 文字列データ
    ChangeColor(i32, i32, i32), // タプルのようなデータ
}

C言語Javaenumでは、全ての選択肢が同じ形である必要がありますが、Rustでは 「終了」という空の状態と「座標移動」という数値データを持つ状態を共存 させられます。

match式による強力なパターンマッチング

Rustのコンパイラは、enummatch 式で扱う際、 「全ての選択肢(バリアント)が網羅されているか」 を厳格にチェックします。

let msg = Message::Write(String::from("hello"));

match msg {
    Message::Quit => println!("終了"),
    Message::Move { x, y } => println!("座標: {}, {}", x, y),
    Message::Write(text) => println!("メッセージ: {}", text),
    // ここで ChangeColor の処理を書き忘れるとコンパイルエラーになる!
    Message::ChangeColor(r, g, b) => println!("色変更"),
}

これにより、新しい状態を追加したときに、その処理を書き忘れるというバグを未然に防ぐことができます。

③ メソッドを実装できる(impl

構造体(struct)と同様に、列挙型に対しても impl ブロックを使ってメソッドを定義できます。 これにより、データの「状態」とその状態に基づく「振る舞い」をひとまとめに管理できます。

例えば、Webブラウザの操作をシミュレートする WebEvent という列挙型にメソッドを追加してみましょう。

enum WebEvent {
    PageLoad,
    PageUnload,
    KeyPress(char),
    Paste(String),
}

impl WebEvent {
    // 列挙型に定義したメソッド
    fn inspect(&self) {
        match self {
            WebEvent::PageLoad => println!("ページがロードされました"),
            WebEvent::PageUnload => println!("ページが閉じられました"),
            // バリアントの中身(データ)を利用することも可能
            WebEvent::KeyPress(c) => println!("キーが押されました: {}", c),
            WebEvent::Paste(s) => println!("テキストが貼り付けられました: \"{}\"", s),
        }
    }
}

fn main() {
    let pressed = WebEvent::KeyPress('A');
    let pasted  = WebEvent::Paste(String::from("Rustの世界"));

    // メソッドの呼び出し
    pressed.inspect();
    pasted.inspect();
}

列挙型を使うメリット

  • 型安全性: 文字列で "Success""Error" と管理するのと違い、スペルミスによるバグが起きません。
  • 「存在しない状態」を排除: Option<T>(値があるか空か)や Result<T, E>(成功か失敗か)といった、Rustの根幹をなす仕組みもすべてこの列挙型でできています。
  • コードが簡潔になる: 構造体だと「状態フラグ」と「その時に使うデータ」を別々に管理して複雑になりがちですが、enumなら一つにまとめられます。

用途

Rustの列挙型(enum)は、 「状態を厳密に管理したい場面」 で非常に強力です。構造体が「データの塊」なら、列挙型は 「排他的な選択肢(どれか一つだけ)」 を表現するのに適しています。

具体的に有効な3つの場面を紹介します。

1. 複数の異なる「状態」を切り替えるとき

例:通信ステータス、ゲームの進行状況、UIの表示状態

「ロード中」「成功(データあり)」「エラー(メッセージあり)」という、同時に起こり得ない状態を管理する場面です。

  • なぜ有効か?: 構造体でこれをやろうとすると、dataerror_message の両方のフィールドを持ち、どちらかが null かどうかをチェックする複雑なコードになりがちです。 enum なら、**「成功ならデータのみ」「エラーならエラー文のみ」**を型として定義できるため、矛盾した状態(データがあるのにエラーメッセージもある、など)が発生しません。

2. 「種類は違うが、共通の処理」をさせたいとき

例:図形計算(円、四角、三角)、コマンド処理(移動、停止、攻撃)

形は違うけれど、どれも「面積を計算できる」とか「コマンドとして実行できる」という共通点がある場合です。

  • なぜ有効か?: 異なる構造体を一つのリスト(配列)に混ぜることは通常できませんが、enum でまとめれば Vec<Shape> のように一つの配列に放り込めます。そして match を使って、種類に応じた計算式を安全に適用できます。

3. 値の「有無」や「成功・失敗」を扱うとき(Rustの核心)

例:検索結果(見つかった vs なかった)、ファイルの読み込み(成功 vs 失敗)

Rustの標準ライブラリで最も重要な Option<T>Result<T, E>enum です。

  • なぜ有効か?: 他の言語の null のように「値がないかもしれないのに、あると思ってアクセスしてクラッシュする」という事故を防げます。enum を使うことで、 「値がない可能性」をコンパイラが強制的に意識させてくれる ため、堅牢なプログラムになります。

ということでRustのEnumは他の言語とは全然異なる、ということでした。

 

LLM: 特徴量を用いた知識蒸留の実験

知識蒸留における「出力のみ(Response-based)」と「中間層(Feature-based)」の違いを比較する実験として、MNIST(手書き数字)の分類タスクを用いた軽量な実験を行いました。

本試験はGoogle Colabで数分以内に完了することが出来るものです。

トランピング・ピノキオ

実験目的

知識蒸留の方法として出力層の蒸留と中間層・特徴量の蒸留があります。 一般的には中間層・特徴量の蒸留の方がより良いとされています。 実際のところどうなのかということを実験してみようと思います。

実験の概要

実験環境

google colab

実験モデル

先生モデルと生徒モデルを以下のように用意します。

  1. 教師モデル (Teacher): 高性能なCNN(例:3層のConv層 + 隠れ層512次元)。
  2. 生徒モデル (Student): 非常にシンプルなMLP(例:1層の隠れ層128次元)。
  3. 比較パターン:

比較パターンは3種類です。

  • Baseline: 生徒モデルを通常のラベルのみで学習。
  • Pattern A (出力蒸留): 教師のSoft Target(確率分布)のみを模倣。
  • Pattern B (特徴蒸留): 教師の中間層(512次元)を生徒の中間層(128次元)で模倣(Regressorを使用)。

実験に使うデータセット

MNIST(手書き数字)

期待される結果

正解率で比較を行い、おそらくは、以下のような結果になると期待しています。

BaseLine < Pattern A < Pattern B

Google Colab用 実装コード

今回実験にあたり工夫した内容は以下通りです。

  1. 先生・生徒モデルサイズ

小さすぎると、中間層からの特徴量を比較できなくなるため、先生3層に対して生徒2層。 先生側には問題をしっかりと学習してもらうため、epochは8回を設定。 生徒側は先生から直接教えてもらうため、学習のepochは少な目に設定。

  1. 表現整合レイヤ

先生モデルと生徒モデルの中間層のサイズが変わるため、揃える必要があります。 →表現整合レイヤとしてregressorを用います。 生徒側の特徴量を、先生側の特徴量にマッピングします。 このネットワークも学習と同時に学習していきます。

  1. 特徴量の正規化

教師・生徒で 特徴量のスケールが異なると、MSEがスケール差を学習してしまうことになります。 このため、正規化を行い、スケールを揃えます。 これにより学習が安定化されます。

といったことを意識した上で以下のようにコードを実装しました。 実行することで、学習法ごとの精度の違いを検証できます。

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
import numpy as np
import random

# ===============================
# 1. 再現性・環境設定
# ===============================
seed = 0
torch.manual_seed(seed)
np.random.seed(seed)
random.seed(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.1307,), (0.3081,))
])

train_loader = DataLoader(
    datasets.MNIST('./data', train=True, download=True, transform=transform),
    batch_size=64, shuffle=True
)
test_loader = DataLoader(
    datasets.MNIST('./data', train=False, transform=transform),
    batch_size=1000
)

# ===============================
# 2. モデル定義
# ===============================

# ---- Teacher (やや厚めCNN) ----
class TeacherModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(1, 32, 3, padding=1), nn.ReLU(),
            nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(),
            nn.MaxPool2d(2),

            nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(),
            nn.MaxPool2d(2)
        )
        self.fc1 = nn.Linear(128 * 7 * 7, 512)
        self.fc2 = nn.Linear(512, 10)

    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size(0), -1)
        feat = F.relu(self.fc1(x))
        logits = self.fc2(feat)
        return logits, feat


# ---- Student (Convベースで表現を近づける) ----
class StudentModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(1, 16, 3, padding=1), nn.ReLU(),
            nn.MaxPool2d(2),

            nn.Conv2d(16, 32, 3, padding=1), nn.ReLU(),
            nn.MaxPool2d(2)
        )
        self.fc1 = nn.Linear(32 * 7 * 7, 128)
        self.fc2 = nn.Linear(128, 10)

    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size(0), -1)
        feat = F.relu(self.fc1(x))
        logits = self.fc2(feat)
        return logits, feat


# ===============================
# 3. 評価関数
# ===============================
def test(model):
    model.eval()
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            logits, _ = model(data)
            pred = logits.argmax(dim=1)
            correct += pred.eq(target).sum().item()
    return 100. * correct / len(test_loader.dataset)


# ===============================
# 4. 学習ループ(蒸留切替)
# ===============================
def train_process(teacher, student, mode='none', epochs=5, alpha=0.7):
    student.to(device)
    teacher.eval()

    if mode == 'feature':
        regressor = nn.Sequential(
            nn.Linear(128, 512),
            nn.BatchNorm1d(512)
        ).to(device)
        optimizer = optim.Adam(
            list(student.parameters()) + list(regressor.parameters()), lr=1e-3
        )
    else:
        regressor = None
        optimizer = optim.Adam(student.parameters(), lr=1e-3)

    history = []
    print(f"\n=== Training Mode: {mode} ===")

    for epoch in range(1, epochs + 1):
        student.train()
        for data, target in train_loader:
            data, target = data.to(device), target.to(device)
            optimizer.zero_grad()

            s_logits, s_feat = student(data)
            with torch.no_grad():
                t_logits, t_feat = teacher(data)

            loss_cls = F.cross_entropy(s_logits, target)

            if mode == 'output':
                T = 3.0
                loss_distill = F.kl_div(
                    F.log_softmax(s_logits / T, dim=1),
                    F.softmax(t_logits / T, dim=1),
                    reduction='batchmean'
                ) * (T ** 2)
                loss = alpha * loss_cls + (1 - alpha) * loss_distill

            elif mode == 'feature':
                # ---- Feature正規化付き蒸留 ----
                s_proj = F.normalize(regressor(s_feat), dim=1)
                t_norm = F.normalize(t_feat, dim=1)
                loss_distill = F.mse_loss(s_proj, t_norm)
                loss = alpha * loss_cls + (1 - alpha) * loss_distill

            else:
                loss = loss_cls

            loss.backward()
            optimizer.step()

        acc = test(student)
        history.append(acc)
        print(f"Epoch {epoch}: Test Accuracy = {acc:.2f}%")

    return history


# ===============================
# 5. 実行
# ===============================

# ---- 教師モデル学習 ----
print("\n--- Training Teacher Model ---")
teacher = TeacherModel().to(device)
t_optimizer = optim.Adam(teacher.parameters(), lr=1e-3)

for epoch in range(0, 8):
    teacher.train()
    for data, target in train_loader:
        data, target = data.to(device), target.to(device)
        t_optimizer.zero_grad()
        logits, _ = teacher(data)
        loss = F.cross_entropy(logits, target)
        loss.backward()
        t_optimizer.step()

    print(f"Teacher Epoch {epoch}: Accuracy = {test(teacher):.2f}%")

# ---- Student比較 ----
epochs = 3
history_none = train_process(teacher, StudentModel(), mode='none', epochs=epochs)
history_output = train_process(teacher, StudentModel(), mode='output', epochs=epochs)
history_feature = train_process(teacher, StudentModel(), mode='feature', epochs=epochs)


# ===============================
# 6. 可視化
# ===============================
plt.figure(figsize=(10, 5))
plt.plot(history_none, marker='o', label='Label Only')
plt.plot(history_output, marker='s', label='Output Distillation')
plt.plot(history_feature, marker='^', label='Feature Distillation')
plt.xlabel("Epoch")
plt.ylabel("Accuracy (%)")
plt.title("Knowledge Distillation Comparison")
plt.legend()
plt.grid(True)
plt.show()

実験結果

  1. Baseline vs Pattern A: 出力(確率分布)を真似るだけで、生徒モデルは「どの数字がどの数字に似ているか」という教師の判断基準を学び、精度が向上します。
  2. Pattern A vs Pattern B: 特徴ベースの蒸留は、中間層(抽象的な概念)を直接コピーするため、出力のみよりもさらに収束が早く、高い精度(特にデータが少ない場合)に到達しやすくなります。

学習ごとのtestの正解率結果

学習法ごとの学習に使っていないtestデータの正解率の推移を以下に示します。 もう少し差が出てればわかりやすかったですが。。。

中間層・特徴量による知識蒸留が最良となることを期待しましたが、結果は出力を用いた知識蒸留が最良となりました。

loss transition



 

結果の考察です。 結論ですが、 Feature蒸留の効果は「高難度タスク」で顕在化しやすい

過去研究でも:

  • CIFAR-10/100
  • ImageNet
  • Dense Prediction(Segmentation)

では Feature蒸留 ≥ Output蒸留 になりやすいようです。

一方、

  • MNIST
  • Fashion-MNIST
  • 低解像度・単純分類

では 出力のみによる蒸留が勝つ ケースが非常に多いようです。

結果が分かっていまさらですが、MNISTはタスクとしては簡単すぎて、特徴量を使う知識蒸留を使うほどのタスクではなかった、ということかもしれません。

絵ごとの推論結果比較

先生モデルと特徴量の知識蒸留を行った生徒モデルを、同じ数字画像で推論させています。 両方とも正解率が高いので、間違いが出ませんでした。

 

output of s/t

 

 

LLM: マルチモーダルモデルの変換アーキテクチャ"Q-Former"

マルチモーダル学習、特に画像と言語を繋ぐ技術において、Q-Formerは非常に重要な役割を果たすコンポーネントです。

簡単に言うと、Q-Formerは 「膨大な画像情報の中から、言語モデル(LLM)が理解しやすい重要なエッセンスだけを抽出する通訳者」 のような存在です。Salesforceが開発した「BLIP-2」というモデルで初めて導入されました。

ピノキオ

 

Q-Formerの役割と仕組み

画像データは非常に情報量が多いですが、そのすべてがテキストでの理解に必要なわけではありません。Q-Formerは以下のステップで情報を整理します。

  1. 情報の圧縮(ボトルネック: 画像エンコーダ(CLIPなど)が出力する膨大な特徴量を、数個〜数十個の「Learnable Queries(学習可能なクエリ)」に集約します。これにより、LLMに渡すデータ量を劇的に減らしつつ、重要な意味情報を保持します。
  2. 2つのトランスフォーマー構造: Q-Formerの内部は、画像を見るための「Self-Attention」と、テキストとの関連性を探る「Cross-Attention」が組み合わさった構造をしています。
  3. 共通空間へのマッピング: 画像と言語という異なる形式のデータを、同じ意味空間で扱えるように橋渡しをします。

なぜQ-Formerが画期的なのか?

Q-Formerが登場する前は、画像エンコーダとLLMを直接繋ぐ方法が主流でしたが、それにはいくつかの課題がありました。Q-Formerはそれを解決しています。

  • LLMの負担を軽減: LLMに画像をそのまま流し込むのではなく、Q-Formerが「固定長(例えば32トークンなど)」に情報を凝縮するため、計算コストが抑えられます。
  • 既存モデルの活用(Frozen Weights): すでに賢い「画像モデル」と「言語モデル」を重みを固定したまま使い、その間を繋ぐQ-Formerだけを訓練すればよいため、非常に効率的に学習が可能です。
  • 精度の向上: 画像の中の「何が重要か」を言語の文脈に合わせて抽出するため、画像キャプショニングやVQA(画像応答)の精度が飛躍的に向上しました。

代表的な活用例:BLIP-2

Q-Formerの有効性を証明したのがBLIP-2です。このモデルは、Q-Formerを介して画像モデルとLLM(OPTやFlan-T5など)を結合することで、当時、はるかに巨大なパラメータを持つモデルよりも高い性能を発揮しました。

構造

Q-Formerの具体的な実装は、標準的な Transformer(特にBERTベース) をベースにしつつ、画像と言語を融合させるために「クエリ」と「アテンション」の仕組みを独自にカスタマイズしたものになっています。

Q-former



 

ざっと概要を説明すると以下のようになります。

  • 画像エンコーダと相互作用して視覚特徴を抽出する画像変換モジュール
  • テキストエンコーダとテキストデコーダの両方として機能するテキスト変換モジュール
  • Self Attention層はこれらのサブモジュール間で共有
  • 画像変換モジュールのために、学習可能なクエリ埋め込みを作成
  • クエリはSelf Attention層で互いに作用した後、Cross Attention層にて画像エンコーダの特徴と作用

構造による主要な特徴を説明します。

Self Attention層の共有

Q-Formerにおいて、画像処理(クエリ側)とテキスト処理(テキスト入力側)で Self-Attention層の重みを共有(Shared Self-Attention) することには、主に以下の3つの重要な効果があります。 特に一つ目は他のマルチモーダルでも同じく提唱されている考えです。 もしかするとマルチモーダルモデルにおける重要なキーパーツとなっているかもしれません。

1. 「マルチモーダルな共通空間」の強制的な学習

画像由来の情報(クエリ)と、言語由来の情報(テキスト)が同じSelf-Attention層を通ることで、モデルは 画像の特徴と言語の概念を同じ次元の「意味」として扱う ことを余儀なくされます。

  • 効果: 画像の中の「犬」という特徴と、テキストの「犬」という単語が、モデル内部で近い表現(ベクトル)としてマッピングされやすくなります。これにより、画像と言語の橋渡し(アライメント)がよりスムーズかつ強力になります。

2. インタラクション(相互作用)の効率化

重みを共有することで、画像クエリとテキストが互いに情報を補完し合うことができます。

  • 効果: テキスト側から得た文脈(例:「色について説明して」という意図)が、共有されたAttention層を通じて画像クエリ側の処理にも反映されます。その結果、Cross-Attentionで画像から情報を抽出する際に、「何に注目すべきか(この場合は『色』)」をより正確に判断できるようになります。

3. パラメータの節約と過学習の防止

画像用とテキスト用に別々のTransformerを用意する場合、パラメータ数が膨大になり、学習に必要なデータ量や計算リソースも増えてしまいます。

  • 効果:
  • 軽量化: 2つの独立したエンコーダを持つよりもパラメータ数を抑えられます。
  • 汎化性能の向上: 同じ重みで異なるモダリティ(画像とテキスト)を処理することで、特定のデータ形式に依存しすぎない、より抽象的で汎用的な特徴を学習するようになります(正則化のような効果)。

画像エンコーダと相互作用して視覚特徴を抽出する画像変換モジュール

「画像変換モジュール(Q-Formerなど)」は、いわば 「情報の翻訳機 兼 編集者」 です。

具体的には、画像エンコーダ(CLIPのViTなど)が出力する膨大な視覚データを、LLM(言語モデル)が好む「テキストに近い意味論的なデータ」に作り替える役割を担います。

具体的にどのようなものか

技術的には、以下の3つの要素で構成された小型のTransformerデコーダのような構造をしています。

  1. 学習可能なクエリ(Learnable Queries): モデルの初期入力として、32個程度の固定されたベクトル(クエリ)を用意します。これは「画像の中から何を探すべきか」を学習する「記憶の種」のようなものです。
  2. クロス・アテンション(Cross-Attention)層: これが相互作用の核です。先ほどの「クエリ」をQuery(探索側)とし、画像エンコーダから出力された「視覚特徴」をKeyおよびValue(参照側)として計算します。
  3. 情報圧縮のメカニズム: 画像エンコーダは通常、画像から数百〜数千のパッチ(断片)データを出力しますが、モジュールはこれを32個程度のクエリに集約します。

どのような効果があるのか?

単に画像とテキストを繋ぐ以上の、以下の3つの決定的なメリットがあります。

1. 情報の「ノイズ」を取り除き、密度を上げる

画像エンコーダの出力には、背景の細かい模様やLLMが理解に必要としない微細なピクセル情報が含まれています。

  • 効果: 画像変換モジュールが「重要な文脈(何があるか、何をしているか)」だけを抽出して凝縮するため、LLMに渡されるデータが非常に高密度になります。

2. 計算コストの劇的な削減

LLMに1,000個の画像パッチをそのまま入力すると、LLMの文脈ウィンドウ(入力枠)を圧迫し、動作が重くなります。

  • 効果: 画像情報をあらかじめ「固定された数(例:32トークン)」に圧縮するため、LLMの計算負荷を最小限に抑えつつ、長い会話や複雑な指示に対応できるようになります。

3. 「見たいもの」への適応(アライメント)

画像エンコーダは「画像全体」をフラットに見るのが得意ですが、言語モデルは「特定の対象」についての質問に答えたい場合があります。

  • 効果: 変換モジュールを介することで、テキスト側の文脈(質問の内容など)に合わせて、画像エンコーダの出力から 最適な情報を「引き出す」 柔軟性が生まれます。

テキストエンコーダとテキストデコーダの両方として機能するテキスト変換モジュール

Q-Formerにおける「テキスト変換モジュール」としての側面は、まさにQ-Formerが単なる「画像圧縮器」ではなく、 「言語と画像を等価に扱うバイリンガルな変換器」 である理由そのものです。

Q-Formerは、学習フェーズやタスクに応じて、入力されたテキストを「理解(エンコード)」したり、新しいテキストを「生成(デコード)」したりする二面性を持っています。

1. 「テキストエンコーダ」としての機能と効果

テキストを入力として受け取り、その意味をベクトル化する機能です。

  • 具体的な仕組み: 入力されたテキスト(例:「猫が座っている」)に対してSelf-Attentionを適用し、テキストの特徴量を抽出します。
  • 効果(対照学習 / ITC): 画像側から抽出された「クエリ」と、テキスト側から抽出された「特徴量」を比較し、 「この画像とこの文章は一致しているか?」 を判定します。これにより、画像と単語の結びつき(アライメント)が極めて正確になります。

2. 「テキストデコーダ」としての機能と効果

画像情報に基づいて、テキストを1語ずつ生成していく機能です。

  • 具体的な仕組み: 「クエリ」が持つ画像情報を参照(Cross-Attention)しながら、自己回帰的(Autoregressive)に次の単語を予測します。
  • 効果(画像キャプショニング学習 / ITG): 画像の内容を言語化する訓練(Image-grounded Text Generation)を行うことで、Q-Formerは 「どの画像特徴がどの言葉に対応するのか」 をより深く学習します。ただ画像を分類するだけでなく、「説明する能力」を養うための重要なステップです。

3. 「エンコーダ」と「デコーダ」を兼ねるメリット

1つのモジュールで両方をこなす(ユニファイド・アーキテクチャ)ことには、以下の大きな効果があります。

マルチタスク学習による相乗効果

「画像とテキストを比べる(エンコード)」能力と「画像からテキストを作る(デコード)」能力を同時に学習することで、視覚と情報の結びつきがより多角的で強固になります。

② 柔軟なアテンション・マスクの切り替え

Q-Formerの実装では、アテンション・マスクを動的に切り替えることで、同じ重みのまま「エンコーダ」と「デコーダ」を使い分けます。

  • Bi-directional Mask: 全単語が互いに見合える状態(エンコーダ用:全体理解)
  • Causal Mask: 後の単語を見ない状態(デコーダ用:文章生成)
  • Multimodal Mask: クエリがテキストを参照し、テキストもクエリを参照する状態

③ 効率的なLLMへの橋渡し

このモジュールが「テキストの文法」と「視覚の意味」の両方を深く理解しているため、最終的にLLMへ渡される「視覚トークン」が、LLMにとって極めて「読みやすい(言語に近い)」形式になります。

実装

主要な実装のポイントを3つに整理して解説します。

1. 内部構造:2つのサブモジュールの共有

Q-Formerは、内部的に「画像用」と「テキスト用」の2つの役割を1つのモデルで切り替えて動かせる構造をしています。

  • Self-Attention層の共有: 内部のSelf-Attention(自己注意)層は、画像情報の処理とテキスト情報の処理の両方で共通の重みを使います。これにより、画像とテキストを共通の概念空間で捉えられるようになります。

  • Cross-Attention層の挿入: Transformerブロックの途中に「Cross-Attention」が配置されています。ここで、学習可能な Query Embeddings が画像エンコーダから出力された視覚特徴(Visual Features)を参照し、必要な情報を吸い上げます。

2. 学習可能なクエリ (Learnable Query Embeddings)

実装上の最も特徴的な部分です。

  • 固定数の入力: モデルの入力として、あらかじめ決められた数(BLIP-2では通常 32個)の学習可能なベクトル(Query)を用意します。
  • 情報集約のボトルネック: 画像がどんなに高解像度でも、Q-Formerはこの32個のベクトルに情報を凝縮します。これが「ボトルネック」として機能し、LLMにとってノイズの少ない「純粋な意味情報」だけを抽出します。

3. アテンション・マスキングによる多機能化

実装レベルでは、 アテンション・マスク(Attention Mask) を切り替えることで、1つのQ-Formerに3つの異なる学習目的を持たせています。

学習目的 マスクの仕組み 役割
画像とテキストの対照学習 双方向マスク 画像とテキストの「一致度」を測る
画像に基づくテキスト生成 因果的マスク 画像を見て説明文(キャプション)を書く訓練
画像と言語の照合 特殊なマスク 画像と特定の単語が合っているか細かく判定する

実装のスペック(BLIP-2の場合)

Hugging Faceなどのライブラリで公開されている実装値は以下の通りです。

  • ベースモデル: BERT-base(約188Mパラメータ)
  • クエリ数: 32個
  • 隠れ層の次元: 768次元
  • レイヤー数: 12層(Cross-Attentionは通常2層おきに挿入)

動かしてみる

Q-Formerに則って構築されたBLIP-2のQ-Formerを動かし、 32個のクエリが画像のどこに注目しているか(アテンション・マップ) を可視化してみます。

  1. output_attentions=True: これを指定することで、Q-Former内部で行われている計算の「中間結果(どのクエリがどのピクセルに注目したか)」を抽出できます。

  2. クロスアテンションの抽出: Q-Formerの各層には、画像特徴を参照するためのクロスアテンション層があります。ここでは情報の統合が最も進んだ「最終層」のデータを使用しています。

!pip install -q transformers accelerate pillow matplotlib

以下をgoogle colab等に張り付けて実行してください。

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

# 1. デバイスの設定とモデルのロード
device = "cuda" if torch.cuda.is_available() else "cpu"
model_id = "salesforce/blip2-opt-2.7b"

# メモリ節約のため float16 を使用
processor = Blip2Processor.from_pretrained(model_id)
model = Blip2ForConditionalGeneration.from_pretrained(
    model_id, torch_dtype=torch.float16, device_map="auto"
)

# 2. 画像の取得(ここでは公式のサンプル画像を使用)
url = "http://images.cocodataset.org/val2017/000000039769.jpg" # 2匹の猫が寝ている画像
raw_image = Image.open(requests.get(url, stream=True).raw).convert("RGB")

# 3. 前処理
inputs = processor(images=raw_image, return_tensors="pt").to(device, torch.float16)

# 4. Q-Formerの出力を取得 (output_attentions=True がポイント)
with torch.no_grad():
    outputs = model.get_text_features(
        **inputs, 
        return_dict=True, 
        output_attentions=True
    )

# Q-Formerの最終層のクロスアテンションを取得
# 形状: [batch, num_heads, num_queries, num_patches]
# BLIP-2のViTパッチ数は通常 257 (16x16 + CLSトークン)
cross_attentions = outputs.qformer_outputs.cross_attentions[-1]

# 5. 可視化 (最初のいくつかのクエリがどこを見ているか)
num_queries_to_show = 4
fig, axes = plt.subplots(1, num_queries_to_show + 1, figsize=(20, 5))

# 元画像の表示
axes[0].imshow(raw_image)
axes[0].set_title("Original Image")
axes[0].axis("off")

# 各クエリのアテンションマップを表示
# ヘッド平均を取って簡易化
avg_attn = cross_attentions[0].mean(dim=0).cpu().float().numpy() 

for i in range(num_queries_to_show):
    # CLSトークンを除いた 16x16 のパッチにリシェイプ
    # BLIP-2のパッチ配置に合わせてリサイズ
    mask = avg_attn[i, 1:].reshape(16, 16)
    
    # 元画像と同じサイズにリサイズ(補完あり)
    axes[i+1].imshow(raw_image)
    axes[i+1].imshow(mask, cmap='jet', alpha=0.6, extent=(0, raw_image.size[0], raw_image.size[1], 0))
    axes[i+1].set_title(f"Query {i+1} Attention")
    axes[i+1].axis("off")

plt.tight_layout()
plt.show()

出力の結果

以下にコードの出力を掲載します。

アテンションマップの見方:

  • 青色から赤色のグラデーション: 赤い部分ほど、そのクエリが強く注目している場所です。
  • クエリごとの違い: 実行結果を見ると、Query 1 はリモコン、Query 2 はしっぽ、Query 3 は背景の一部など、32個のクエリがそれぞれ異なる特徴を捉えようとしている(分業している)のが視覚的に確認できるはずです。

original image

 

attention map



Rust: 構造体の使い方

Rustの構造体についてまとめます。

Rustにおける構造体は、関連するデータをひとまとめにして扱うためのデータ構造です。 C言語C++の構造体と似ていますが、Rustの構造体はより強力で柔軟な機能を持っています。

 

rusting girl

概要

ざっとリストアップします。

  • データの集約: 複数の異なる型のデータを、意味のあるまとまりとして扱うことができます。
  • メソッドの定義: 構造体に対してメソッドを定義することで、構造体が持つデータに対する操作をカプセル化できます。
  • トレイトの実装: 構造体にトレイトを実装することで、共通のインターフェースを持つ複数の構造体を統一的に扱うことができます。
  • ジェネリクス: ジェネリクスを使用することで、様々な型のデータを扱うことができる汎用的な構造体を定義できます。

メリット

Rustで構造体を使うメリットは以下の通りです。 ほぼ、通常のclassと同じ。

もし、差別化になる要素を知りたいという方はこの下の方で説明させて頂いています。

  • コードの可読性向上: 関連するデータを構造体にまとめることで、コードの意図が明確になり、可読性が向上します。
  • 保守性の向上: データとメソッドをカプセル化することで、データ構造の変更が他に与える影響を最小限に抑え、保守性を向上させることができます。
  • 再利用性の向上: 構造体やトレイトを再利用することで、コードの重複を減らし、開発効率を向上させることができます。
  • 安全性: Rustの所有権システムや借用規則と組み合わせることで、メモリ安全性やデータ競合などの問題を防ぐことができます。

補足

Rustには、構造体以外にもEnumTupleといったデータ構造があります。それぞれのデータ構造には特徴があり、適切なものを選択することで、より効果的にコードを記述することができます。

実装

続いてRustにおける構造体の実装法について説明します。

Rustの構造体:実装の3ステップ

Rustで構造体を使う際は、一般的に 「定義」「インスタンス化」「動作(メソッド)の実装」 という手順を踏みます。

1. 構造体を定義する(設計図を作る)

まず、どのようなデータを持たせるかを決めます。structキーワードを使い、フィールド名とその型を指定します。

// 「本」を表現する構造体の定義
struct Book {
    title: String,
    author: String,
    pages: u32,
    is_available: bool,
}

2. 構造体をインスタンス化する(実体を作る)

定義した設計図をもとに、実際のデータを作成します。

fn main() {
    // 構造体のインスタンスを作成
    let my_book = Book {
        title: String::from("Rust入門"),
        author: String::from("Rustacean"),
        pages: 350,
        is_available: true,
    };

    // ドット演算子 (.) でデータにアクセスできる
    println!("書名: {}", my_book.title);
}

3. メソッドを実装する(動作を与える)

ここがクラス(class)との大きな違いです。構造体の定義の外に impl ブロックを作り、その中で関数(メソッド)を書きます。

  • メソッド: &self を引数に取り、特定のインスタンスに対して実行するもの。
  • 関連関数: self を取らず、Book::new() のように呼び出すもの(他言語のスタティックメソッドに近い)。
impl Book {
    // 関連関数:新しい本を作る「コンストラクタ」のような役割
    fn new(title: &str, author: &str, pages: u32) -> Self {
        Self {
            title: title.to_string(),
            author: author.to_string(),
            pages,
            is_available: true,
        }
    }

    // メソッド:本の概要を表示する動作
    fn print_summary(&self) {
        println!("『{}』({}著、{}ページ)", self.title, self.author, self.pages);
    }
}

// 使い方
fn main() {
    let book2 = Book::new("実践Rust", "エンジニアA", 420);
    book2.print_summary(); // メソッドの呼び出し
}

classとの違い

Rustの構造体 (struct) と、C++JavaPythonなどのオブジェクト指向言語におけるクラス (class) は、どちらもデータとその操作をまとめるための仕組みですが、その設計思想や機能にはいくつかの重要な違いがあります。

類似点:

  • データと関数のカプセル化: どちらも、関連するデータ (フィールド/メンバ変数) とそれらを操作する関数 (メソッド/メンバ関数) をひとまとめにすることができます。
  • インスタンス化: 定義に基づいて、具体的なデータを持つインスタンス (オブジェクト) を生成できます。

相違点:

  1. 継承:
  • クラス: 継承をサポートしており、既存のクラスを基に新しいクラスを作成し、機能やデータを引き継いだり拡張したりできます。
  • 構造体: 継承を直接サポートしていません。代わりに、トレイト (trait) を使って共通の振る舞いを定義し、複数の構造体に実装させることで、継承に近いことを実現します。
  1. メソッド定義:
  • クラス: メソッド定義はクラス定義の中に含まれます。
  • 構造体: メソッド定義は impl ブロックを使って構造体定義の外で行われます。
  1. 所有権と借用:
  • クラス: メモリ管理はガベージコレクションや参照カウントなどで行われることが多く、所有権や借用の概念は意識しにくい場合があります。
  • 構造体: Rustの所有権と借用システムが適用され、メモリ管理が厳密に行われます。これにより、メモリリークやデータ競合などの問題を防ぐことができます。
  1. データレイアウト:
  • クラス: フィールドのメモリ上の配置は言語やコンパイラによって異なる場合があります。
  • 構造体: デフォルトではフィールドは定義順に配置されますが、repr(C) などのアトリビュートを使って明示的に指定することもできます。
  1. 用途:
  • クラス: オブジェクト指向プログラミングにおいて、データと振る舞いをまとめるための基本的な構成要素として使われます。
  • 構造体: データ構造を定義したり、メソッドをカプセル化したりするために使われます。Rustでは、オブジェクト指向的な設計よりも、データ構造と関数を組み合わせた関数型プログラミング的な設計が好まれる傾向があります。

構造体を使うべき場面

Rustの構造体(struct)は、単にデータをまとめるだけでなく、 「データの整合性を保ち、プログラムの複雑さを制御する」 ために非常に効果的です。

具体的にどのような場面で使うのが効果的なのか、3つの主要なシーンとその理由を解説します。

1. 複数の関連する情報を「一つの概念」として扱うとき

例:ユーザー情報、商品の座標、センサーの計測データなど

バラバラの変数(user_name, user_email, user_age)として管理するのではなく、一つの「User構造体」にまとめる場面です。

  • 理由(可読性と保守性): 関数の引数にバラバラと値を渡す必要がなくなり、fn register(user: User) のように書けるため、コードがスッキリします。また、「名前とメールアドレスは必ずセットである」という ドメイン(業務知識) をコード上で表現できます。

2. データの「正しい状態」を強制したいとき

例:バリデーションが必要な値(メールアドレス、パスワード、年齢など)

ただの文字列(String)として扱うのではなく、特定のルールを守った構造体として定義する場面です。

  • 理由(安全性): 構造体の「関連関数(new関数など)」の中でチェックを行い、不正なデータ(例:マイナスの年齢)を弾くように実装すれば、 その構造体のインスタンスが存在している=正しいデータであることが保証される 状態を作れます。これにより、プログラムの他の場所で何度もチェックを行う手間が省けます。

3. データに「特定の振る舞い(メソッド)」を持たせたいとき

例:図形の面積計算、ネットワーク接続の管理、ゲームのキャラクター操作など

データそのものだけでなく、そのデータを使って「何ができるか」をカプセル化したい場面です。

  • 理由(カプセル化: implブロックでメソッドを定義することで、内部のデータ構造(どうやって計算しているか)を隠したまま、利用者には便利な機能だけを提供できます。 例えば、Rectangle構造体に.area()メソッドを持たせれば、利用者は幅や高さの計算式を知らなくても「面積を出す」という目的を達成できます。

 

RUST: ファイル参照法

Rustの勉強していて、練習程度ならば、一つのファイルで文法チェックしてればよかったのですが、少し機能を持たせる成果物を作りたいとなってきて、ファイルを分割したくなってきました。 なので、他ファイルの参照の仕組みをようやく使うときが来ました。

Rustで別のファイルにある関数を読み込むには、 「モジュール(module)」 という仕組みを使います。

他の言語(PythonimportC++#include)と少し感覚が異なり、 「ファイル構造=モジュール構造」 としてRustに教えてあげる必要があります。

 

基本的な手順(Cargoプロジェクトの場合)

例えば、main.rs から utils.rs にある関数を呼び出す手順を説明します。

ファイルはこんな感じで配置するとします。

my_project/
├── Cargo.toml
└── src/
    ├── main.rs   <-- (親) ここで mod utils; を宣言
    └── utils.rs  <-- (子) 実際の処理を書く。関数には pub をつける

1. 呼び出される側のファイル (src/utils.rs)

関数を他のファイルから見えるようにするために、pub キーワードを付けます。

// src/utils.rs

// pub を付けることで外部(他のファイル)からアクセス可能になる
pub fn hello_from_utils() {
    println!("utils.rsの関数が呼ばれました!");
}

呼び出す側のファイル (src/main.rs)

main.rs で「utils.rs というファイルが存在し、それをモジュールとして使う」ことを宣言します。

// src/main.rs

// 1. utils.rs をモジュールとして登録(ファイル名がモジュール名になる)
mod utils;

fn main() {
    // 2. 「モジュール名::関数名」で呼び出す
    utils::hello_from_utils();
}

フォルダ構成のイメージ

Cargoプロジェクトでは、ファイルは必ず src フォルダの中に置く必要があります。

知っておくと便利な応用

use を使って短縮する

毎回 utils:: と書くのが面倒な場合は、use を使うと直接関数名だけで呼べるようになります。

mod utils;
use utils::hello_from_utils; // これを足すと...

fn main() {
    hello_from_utils(); // 直接呼べる!
}

フォルダで管理する場合

ファイルが増えて、src/tools/network.rs のようにフォルダ分けしたい場合は、少し特殊なルール(mod.rs を置くか、フォルダ名と同じ名前の .rs ファイルを作る)が必要になります。

  1. src/tools.rs を作成し、その中で pub mod network; と宣言する。
  2. src/tools/network.rs に関数を書く。
  3. main.rsmod tools; と宣言する。

トラブルシューティングmod を書く場所

よくある間違いは、 utils.rs の中にも mod utils; と書いてしまうこと です。

  • mod 宣言は、そのファイルを「子」として持つ「親」のファイル(今回なら main.rs)に一度だけ書きます。