Skip to content
Published on

メタ学習とFew-Shot学習完全ガイド:MAML、Prototypical Networks、In-Context Learning

Authors

メタ学習とFew-Shot学習完全ガイド:MAML、Prototypical Networks、In-Context Learning

人間はわずかな例からすぐに新しい概念を学習できます。子供に「これはシマウマだ」と一度見せれば、翌日には全く別の写真でもシマウマを認識できます。しかし、従来のディープラーニングモデルはシマウマの分類器を構築するだけでも何千もの画像が必要です。

メタ学習Few-Shot学習はこのギャップを埋めます。メタ学習のコアアイデアは「学習を学ぶ」ことです。モデルはトレーニング中にさまざまなタスクを経験することで、新しいタスクに素早く適応する能力を身につけます。

このガイドでは、メタ学習の理論的基礎からIn-Context Learningの最新の発展まですべてを網羅しています。


1. メタ学習の基礎

1.1 学習を学ぶ

従来の機械学習では、モデルは単一のタスクのためにトレーニングされます。猫の分類器は猫のデータだけでトレーニングされ、新しい動物(例えばオセロット)を分類する必要が生じたとき、最初からトレーニングをやり直す必要があります。

メタ学習は視点を変えます。モデルが学ぶべきことは「猫の分類方法」ではなく「新しい動物を素早く分類する方法を学ぶ方法」です。

したがって、メタ学習には2つのレベルの学習があります。

  • メタ学習(外側のループ):多くのタスクを経験することで、良い初期化や学習アルゴリズムを学ぶ
  • タスク学習(内側のループ):少数の例から各特定タスクに素早く適応する

1.2 従来の学習の限界

従来の学習の限界:

データ非効率性:ImageNetでトレーニングされたモデルは何百万もの画像を必要とします。新しいクラスを追加するには何千ものサンプルが必要です。

汎化の欠如:トレーニング分布と大きく異なる新しいタスクでは性能が急激に低下します。

壊滅的忘却:新しいタスクを学習すると、モデルは以前に学習したタスクを忘れてしまいます。

1.3 タスク分布

メタ学習の中心的な概念は**タスク分布p(T)**です。メタ学習は単純にデータから学習するのではなく、タスクの分布から学習します。

各タスクTには:

  • 入出力ペアの分布p(x, y)
  • タスク損失関数L

が含まれます。

メタ学習の目的は:

thetaを最小化: T ~ p(T)Eに対して [L_T(f_theta)]

1.4 サポートセット vs クエリセット

Few-Shot学習では、データは2つの役割に分けられます。

サポートセット:モデルが新しいタスクを学習する際に参照として使用する少数の例。従来の学習のトレーニングデータに対応しますが、非常に少数です(例:クラスごとに1〜5例)。

クエリセット:モデルの性能を評価するために使用されるデータ。従来の学習のテストデータに対応します。

1.5 N-way K-shotセットアップ

Few-Shot学習で最も重要な設定はN-way K-shotです。

  • N-way:分類するクラス数
  • K-shot:クラスごとのサポート例数

例えば、5-way 1-shotはクラスごとに1つの例だけで5クラスを分類するタスクです。5-way 5-shotはクラスごとに5つの例を使用します。

import torch
import torch.nn as nn
import numpy as np
from typing import List, Tuple, Dict


def create_episode(
    dataset,
    n_way: int,
    k_shot: int,
    n_query: int,
    classes: List[int] = None
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
    """
    N-way K-shotエピソードを作成
    返り値: (support_x, support_y, query_x, query_y)
    """
    all_classes = list(set(dataset.targets.tolist()))
    if classes is None:
        selected_classes = np.random.choice(
            all_classes, n_way, replace=False
        )
    else:
        selected_classes = classes

    support_x, support_y = [], []
    query_x, query_y = [], []

    for new_label, cls in enumerate(selected_classes):
        cls_indices = (dataset.targets == cls).nonzero(as_tuple=True)[0]
        chosen = np.random.choice(
            len(cls_indices), k_shot + n_query, replace=False
        )

        for i, idx in enumerate(cls_indices[chosen]):
            x, _ = dataset[idx.item()]
            if i < k_shot:
                support_x.append(x)
                support_y.append(new_label)
            else:
                query_x.append(x)
                query_y.append(new_label)

    support_x = torch.stack(support_x)
    support_y = torch.tensor(support_y)
    query_x = torch.stack(query_x)
    query_y = torch.tensor(query_y)

    return support_x, support_y, query_x, query_y

2. 距離ベースのメタ学習

2.1 Matching Networks

2016年にVinyalsらが提案したMatching Networksは、アテンションメカニズムとkNNのアイデアを組み合わせています。コアアイデアはクエリサンプルをサポートセットラベルのアテンション重み付き和として予測することです。

予測式

y_hat = i全体の和: a(x_hat, x_i) * y_i

ここでa(x_hat, x_i)はクエリx_hatとサポートサンプルx_iの間のアテンション重みで、コサイン類似度のsoftmaxとして計算されます。

class MatchingNetworks(nn.Module):
    """Matching Networksの実装"""

    def __init__(self, encoder: nn.Module, use_fce: bool = False):
        """
        encoder: 特徴量抽出器
        use_fce: Full Context Embeddingを使用するかどうか
        """
        super().__init__()
        self.encoder = encoder
        self.use_fce = use_fce

    def cosine_similarity(
        self,
        query: torch.Tensor,
        support: torch.Tensor
    ) -> torch.Tensor:
        """
        コサイン類似度を計算
        query: (n_query, embed_dim)
        support: (n_support, embed_dim)
        返り値: (n_query, n_support)
        """
        query_norm = nn.functional.normalize(query, dim=-1)
        support_norm = nn.functional.normalize(support, dim=-1)
        return torch.mm(query_norm, support_norm.t())

    def forward(
        self,
        support_x: torch.Tensor,
        support_y: torch.Tensor,
        query_x: torch.Tensor,
        n_way: int
    ) -> torch.Tensor:
        """
        support_x: (n_way * k_shot, C, H, W)
        support_y: (n_way * k_shot,)
        query_x: (n_query, C, H, W)
        """
        support_emb = self.encoder(support_x)   # (n_support, D)
        query_emb = self.encoder(query_x)        # (n_query, D)

        similarities = self.cosine_similarity(
            query_emb, support_emb
        )  # (n_query, n_support)

        # Softmaxアテンション
        attention = nn.functional.softmax(similarities, dim=-1)

        # ワンホットラベル
        support_labels_one_hot = nn.functional.one_hot(
            support_y, n_way
        ).float()  # (n_support, n_way)

        # アテンション重み付き予測
        logits = torch.mm(attention, support_labels_one_hot)
        return logits

2.2 Prototypical Networks

2017年にSnellらが発表したPrototypical Networksは、最もエレガントで直感的なメタ学習アルゴリズムの一つです。コアアイデア:埋め込み空間で各クラスを単一のプロトタイプ(重心)として表現する。

プロトタイプの計算

クラスcのプロトタイプは、そのクラスのサポートサンプルの埋め込みの平均です。

p_c = (1/|S_c|) S_cの(x_i, y_i)全体の和: f_phi(x_i)

分類

クエリサンプルxを最近傍プロトタイプに分類します。

p(y=c | x) = softmax(-d(f_phi(x), p_c))

ここでdはユークリッド距離です。

class ConvEncoder(nn.Module):
    """Few-Shot学習のための4層CNN エンコーダ"""

    def __init__(
        self,
        in_channels: int = 1,
        hidden_dim: int = 64,
        out_dim: int = 64
    ):
        super().__init__()

        def conv_block(in_ch, out_ch):
            return nn.Sequential(
                nn.Conv2d(in_ch, out_ch, 3, padding=1),
                nn.BatchNorm2d(out_ch),
                nn.ReLU(),
                nn.MaxPool2d(2)
            )

        self.net = nn.Sequential(
            conv_block(in_channels, hidden_dim),
            conv_block(hidden_dim, hidden_dim),
            conv_block(hidden_dim, hidden_dim),
            conv_block(hidden_dim, out_dim),
            nn.Flatten()
        )

    def forward(self, x):
        return self.net(x)


class PrototypicalNetworks(nn.Module):
    """Prototypical Networksの完全実装"""

    def __init__(self, encoder: nn.Module):
        super().__init__()
        self.encoder = encoder

    def compute_prototypes(
        self,
        support_x: torch.Tensor,
        support_y: torch.Tensor,
        n_way: int
    ) -> torch.Tensor:
        """
        クラスプロトタイプを計算(埋め込みの平均)
        support_x: (n_way * k_shot, C, H, W)
        support_y: (n_way * k_shot,)
        返り値: (n_way, embed_dim)
        """
        support_emb = self.encoder(support_x)  # (n_support, D)
        prototypes = []

        for cls in range(n_way):
            mask = (support_y == cls)
            cls_embeddings = support_emb[mask]
            prototype = cls_embeddings.mean(dim=0)
            prototypes.append(prototype)

        return torch.stack(prototypes)  # (n_way, D)

    def euclidean_dist(
        self,
        x: torch.Tensor,
        y: torch.Tensor
    ) -> torch.Tensor:
        """
        ユークリッド距離
        x: (n, D)
        y: (m, D)
        返り値: (n, m)
        """
        # ||x - y||^2 = ||x||^2 + ||y||^2 - 2*x.y
        n = x.size(0)
        m = y.size(0)
        x_sq = (x ** 2).sum(dim=1, keepdim=True).expand(n, m)
        y_sq = (y ** 2).sum(dim=1, keepdim=True).expand(m, n).t()
        xy = torch.mm(x, y.t())
        dist = x_sq + y_sq - 2 * xy
        return dist.clamp(min=0).sqrt()

    def forward(
        self,
        support_x: torch.Tensor,
        support_y: torch.Tensor,
        query_x: torch.Tensor,
        n_way: int
    ) -> torch.Tensor:
        """
        Prototypical Networksのフォワードパス
        返り値:クエリサンプルの対数確率 (n_query, n_way)
        """
        prototypes = self.compute_prototypes(
            support_x, support_y, n_way
        )  # (n_way, D)

        query_emb = self.encoder(query_x)  # (n_query, D)

        dists = self.euclidean_dist(
            query_emb, prototypes
        )  # (n_query, n_way)

        log_probs = nn.functional.log_softmax(-dists, dim=-1)
        return log_probs


# ========== トレーニングループ ==========

def train_prototypical(
    model: PrototypicalNetworks,
    train_dataset,
    n_way: int = 5,
    k_shot: int = 5,
    n_query: int = 15,
    n_episodes: int = 100,
    lr: float = 1e-3,
    device: str = 'cpu'
):
    """Prototypical Networksをトレーニング"""
    model = model.to(device)
    optimizer = torch.optim.Adam(model.parameters(), lr=lr)
    criterion = nn.NLLLoss()

    model.train()
    episode_losses = []
    episode_accs = []

    for episode in range(n_episodes):
        support_x, support_y, query_x, query_y = create_episode(
            train_dataset, n_way, k_shot, n_query
        )
        support_x = support_x.to(device)
        support_y = support_y.to(device)
        query_x = query_x.to(device)
        query_y = query_y.to(device)

        optimizer.zero_grad()

        log_probs = model(support_x, support_y, query_x, n_way)
        loss = criterion(log_probs, query_y)
        loss.backward()
        optimizer.step()

        preds = log_probs.argmax(dim=-1)
        acc = (preds == query_y).float().mean().item()

        episode_losses.append(loss.item())
        episode_accs.append(acc)

        if (episode + 1) % 100 == 0:
            print(
                f"Episode {episode+1}/{n_episodes} | "
                f"Loss: {np.mean(episode_losses[-100:]):.4f} | "
                f"Acc: {np.mean(episode_accs[-100:]):.4f}"
            )

    return episode_losses, episode_accs

2.3 Relation Networks

SungらのRelation NetworksはPrototypical Networksに似ていますが、距離関数が学習可能なニューラルネットワークに置き換えられています。ネットワークはクエリ埋め込みとクラスプロトタイプを連結してリレーションスコアを計算する方法を学習します。

class RelationNetwork(nn.Module):
    """Relation Networks:学習可能な距離関数"""

    def __init__(self, encoder: nn.Module, embed_dim: int = 64):
        super().__init__()
        self.encoder = encoder

        # リレーションモジュール:連結された埋め込みを入力し、リレーションスコアを出力
        self.relation_module = nn.Sequential(
            nn.Linear(embed_dim * 2, 256),
            nn.ReLU(),
            nn.Linear(256, 64),
            nn.ReLU(),
            nn.Linear(64, 1),
            nn.Sigmoid()
        )

    def forward(
        self,
        support_x: torch.Tensor,
        support_y: torch.Tensor,
        query_x: torch.Tensor,
        n_way: int
    ) -> torch.Tensor:
        """
        返り値: リレーションスコア (n_query, n_way)
        """
        support_emb = self.encoder(support_x)
        query_emb = self.encoder(query_x)

        prototypes = []
        for cls in range(n_way):
            mask = (support_y == cls)
            proto = support_emb[mask].mean(dim=0)
            prototypes.append(proto)
        prototypes = torch.stack(prototypes)  # (n_way, D)

        n_query = query_emb.size(0)

        query_expanded = query_emb.unsqueeze(1).expand(
            n_query, n_way, -1
        )  # (n_query, n_way, D)
        proto_expanded = prototypes.unsqueeze(0).expand(
            n_query, n_way, -1
        )  # (n_query, n_way, D)

        pairs = torch.cat(
            [query_expanded, proto_expanded], dim=-1
        )  # (n_query, n_way, 2D)

        scores = self.relation_module(
            pairs.view(-1, pairs.size(-1))
        ).view(n_query, n_way)

        return scores

3. 最適化ベースのメタ学習:MAML

3.1 MAMLのコアアイデア

MAML(Model-Agnostic Meta-Learning)は2017年にFinnらが提案したメタ学習の最も影響力のある研究の一つです。

MAMLの目的は**「素早い適応が可能な初期パラメータのセットthetaを見つけること」**です。

具体的には、わずかな勾配更新ステップで新しいタスクでの良い性能が達成できる初期化点を見つけます。

3.2 内側のループ vs 外側のループ

MAMLは2つのループで構成されています。

内側のループ(タスク固有の適応)

各タスクT_iに対して:

theta_i' = theta - alpha * grad_theta L_{T_i}(f_theta)

サポートセットで1〜5回の勾配更新を実行して、タスク固有のパラメータtheta_i'を得ます。

外側のループ(メタ更新)

theta = theta - beta * grad_theta sum_i L_{T_i}(f_{theta_i'})

タスク適応パラメータtheta_i'を使ってクエリセットの損失を計算し、その損失に基づいてメタパラメータthetaを更新します。

3.3 二階微分

MAMLの主要な技術的課題は、外側のループの勾配に**二階微分(内側のループを通じた逆伝播)**が必要なことです。

grad_theta L(f_{theta_i'}) = grad_theta L(f_{theta - alpha * grad L(theta)})

thetaに関する勾配を計算するには内側のループの更新(ヘッセ行列が含まれる)を通じて微分する必要があります。これは計算コストが高いです。

実践では、二階微分項を無視して近似勾配を使用する**FOMAML(First-Order MAML)**がよく使われます。

3.4 MAMLの完全実装

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
import numpy as np
from copy import deepcopy
from typing import List, Tuple


class MAML:
    """
    MAML: Model-Agnostic Meta-Learning
    Finn et al., 2017 (arXiv:1703.03400)
    """

    def __init__(
        self,
        model: nn.Module,
        inner_lr: float = 0.01,       # alpha:内側のループ学習率
        outer_lr: float = 0.001,      # beta:外側のループ学習率
        n_inner_steps: int = 5,       # 内側のループ更新回数
        first_order: bool = False,    # FOMAMLを使用するかどうか
        device: str = 'cpu'
    ):
        self.model = model.to(device)
        self.inner_lr = inner_lr
        self.outer_lr = outer_lr
        self.n_inner_steps = n_inner_steps
        self.first_order = first_order
        self.device = device

        self.meta_optimizer = torch.optim.Adam(
            self.model.parameters(), lr=outer_lr
        )

    def inner_loop(
        self,
        support_x: torch.Tensor,
        support_y: torch.Tensor,
        model_params=None
    ) -> dict:
        """
        内側のループ:サポートセットでのタスク固有の適応
        返り値:適応されたパラメータ辞書
        """
        if model_params is None:
            params = {
                name: param.clone()
                for name, param in self.model.named_parameters()
            }
        else:
            params = {k: v.clone() for k, v in model_params.items()}

        for step in range(self.n_inner_steps):
            logits = self._forward_with_params(support_x, params)
            loss = F.cross_entropy(logits, support_y)

            grads = torch.autograd.grad(
                loss,
                params.values(),
                create_graph=not self.first_order
            )

            params = {
                name: param - self.inner_lr * grad
                for (name, param), grad in zip(params.items(), grads)
            }

        return params

    def _forward_with_params(
        self,
        x: torch.Tensor,
        params: dict
    ) -> torch.Tensor:
        """特定のパラメータでのフォワードパス"""
        original_params = {}
        for name, param in self.model.named_parameters():
            original_params[name] = param.data
            param.data = params[name].data if name in params else param.data

        output = self.model(x)

        for name, param in self.model.named_parameters():
            if name in original_params:
                param.data = original_params[name]

        return output

    def meta_train_step(
        self,
        tasks: List[Tuple]
    ) -> float:
        """
        1回のメタトレーニングステップ(複数タスク)
        tasks: [(support_x, support_y, query_x, query_y), ...]
        """
        self.meta_optimizer.zero_grad()

        meta_loss = 0.0

        for support_x, support_y, query_x, query_y in tasks:
            support_x = support_x.to(self.device)
            support_y = support_y.to(self.device)
            query_x = query_x.to(self.device)
            query_y = query_y.to(self.device)

            adapted_params = self.inner_loop(support_x, support_y)

            query_logits = self._forward_with_params(
                query_x, adapted_params
            )
            query_loss = F.cross_entropy(query_logits, query_y)
            meta_loss += query_loss

        meta_loss /= len(tasks)
        meta_loss.backward()
        self.meta_optimizer.step()

        return meta_loss.item()

    def fine_tune(
        self,
        support_x: torch.Tensor,
        support_y: torch.Tensor,
        n_steps: int = None
    ) -> nn.Module:
        """
        新しいタスクへのファインチューニング(推論時に使用)
        """
        n_steps = n_steps or self.n_inner_steps
        model_copy = deepcopy(self.model)
        optimizer = torch.optim.SGD(
            model_copy.parameters(), lr=self.inner_lr
        )

        model_copy.train()
        for step in range(n_steps):
            optimizer.zero_grad()
            logits = model_copy(support_x.to(self.device))
            loss = F.cross_entropy(logits, support_y.to(self.device))
            loss.backward()
            optimizer.step()

        return model_copy

    def evaluate(
        self,
        tasks: List[Tuple],
        n_fine_tune_steps: int = 5
    ) -> Tuple[float, float]:
        """
        メタテスト:新しいタスクに適応して評価
        """
        total_loss = 0.0
        total_acc = 0.0

        for support_x, support_y, query_x, query_y in tasks:
            adapted_model = self.fine_tune(
                support_x, support_y, n_fine_tune_steps
            )
            adapted_model.eval()

            with torch.no_grad():
                query_logits = adapted_model(query_x.to(self.device))
                loss = F.cross_entropy(
                    query_logits, query_y.to(self.device)
                )
                preds = query_logits.argmax(dim=-1)
                acc = (preds == query_y.to(self.device)).float().mean()

            total_loss += loss.item()
            total_acc += acc.item()

        return total_loss / len(tasks), total_acc / len(tasks)


def train_maml(
    maml: MAML,
    dataset,
    n_way: int = 5,
    k_shot: int = 1,
    n_query: int = 15,
    meta_batch_size: int = 32,
    n_iterations: int = 60000,
    device: str = 'cpu'
):
    """MAMLトレーニングループ"""
    print(f"MAMLトレーニング: {n_way}-way {k_shot}-shot")
    print(f"メタバッチサイズ: {meta_batch_size}")
    print(f"総イテレーション数: {n_iterations}")

    losses = []

    for iteration in range(n_iterations):
        tasks = []
        for _ in range(meta_batch_size):
            task = create_episode(dataset, n_way, k_shot, n_query)
            tasks.append(task)

        meta_loss = maml.meta_train_step(tasks)
        losses.append(meta_loss)

        if (iteration + 1) % 1000 == 0:
            avg_loss = np.mean(losses[-1000:])
            print(
                f"Iteration {iteration+1}/{n_iterations} | "
                f"Meta Loss: {avg_loss:.4f}"
            )

    return losses

3.5 Reptile:MAMLの簡略化

Reptile(Nichol et al., 2018)はMAMLを大幅に簡略化したバージョンです。二階微分が不要で、実装がはるかに簡単です。

コアアイデア:タスクでSGDを複数回実行した後、結果のパラメータに向けてメタパラメータを移動させます。

theta = theta + epsilon * (W_k - theta)

ここでW_kはタスクTでk回SGD更新した後のパラメータです。

class Reptile:
    """
    Reptile: A Scalable Meta-learning Algorithm
    Nichol et al., 2018 (arXiv:1803.02999)
    """

    def __init__(
        self,
        model: nn.Module,
        inner_lr: float = 0.02,
        outer_lr: float = 0.001,  # epsilon
        n_inner_steps: int = 5,
        device: str = 'cpu'
    ):
        self.model = model.to(device)
        self.inner_lr = inner_lr
        self.outer_lr = outer_lr
        self.n_inner_steps = n_inner_steps
        self.device = device

    def inner_train(
        self,
        support_x: torch.Tensor,
        support_y: torch.Tensor
    ) -> dict:
        """タスク固有の内部トレーニング(k回のSGDステップ)"""
        model_copy = deepcopy(self.model)
        optimizer = torch.optim.SGD(
            model_copy.parameters(), lr=self.inner_lr
        )

        model_copy.train()
        for step in range(self.n_inner_steps):
            optimizer.zero_grad()
            logits = model_copy(support_x)
            loss = F.cross_entropy(logits, support_y)
            loss.backward()
            optimizer.step()

        return dict(model_copy.named_parameters())

    def meta_update(self, task_params_list: List[dict]):
        """
        Reptileメタ更新:
        theta += epsilon * (mean(W_k) - theta)
        """
        with torch.no_grad():
            for name, param in self.model.named_parameters():
                task_mean = torch.stack([
                    task_params[name].data
                    for task_params in task_params_list
                ]).mean(dim=0)

                param.data += self.outer_lr * (task_mean - param.data)

    def train(
        self,
        dataset,
        n_way: int = 5,
        k_shot: int = 5,
        meta_batch_size: int = 5,
        n_iterations: int = 100000
    ):
        """Reptileのフルトレーニングループ"""
        print(f"Reptileトレーニング: {n_way}-way {k_shot}-shot")

        for iteration in range(n_iterations):
            task_params_list = []

            for _ in range(meta_batch_size):
                support_x, support_y, _, _ = create_episode(
                    dataset, n_way, k_shot, n_query=0
                )
                support_x = support_x.to(self.device)
                support_y = support_y.to(self.device)

                task_params = self.inner_train(support_x, support_y)
                task_params_list.append(task_params)

            self.meta_update(task_params_list)

            if (iteration + 1) % 10000 == 0:
                print(f"Iteration {iteration+1}/{n_iterations} 完了")

4. LLMにおけるIn-Context Learning

4.1 In-Context Learningとは?

In-Context Learning(ICL)は、大規模言語モデル(LLM)がプロンプト内の例から新しいタスクを実行できる能力です。モデルはパラメータを更新しません。入力コンテキスト(プロンプト)だけから「学習」します。

この能力はGPT-3の登場で大きな注目を集めました。例えば:

英語からフランス語への翻訳:
sea otter => loutre de mer
peppermint => menthe poivree
plush giraffe => girafe en peluche
cheese => ?

このプロンプト形式が与えられると、GPT-3は「fromage」と答えます。フランス語翻訳のためにトレーニングされていないように見えますが、事前トレーニング中にすでにそのパターンを学習しています。

4.2 なぜ効果的なのか?

ICLが効果的な理由はまだ活発に研究されています。主要な仮説には:

パターン補完の観点:LLMは次のトークンを予測することでトレーニングされています。プロンプトのパターン(入力から出力へのペア)を見ることで、そのパターンを継続するように効果的にトレーニングされています。

潜在概念推論の観点:Brownらの研究によれば、ICLはモデルがプロンプトから潜在概念を推論するベイズ推論に似ています。

勾配降下メタファー:Akyürekらは、Transformerのアテンションメカニズムが勾配降下に類似した操作を暗黙的に実行することを示しました。

4.3 効果的なFew-Shotプロンプティング戦略

from typing import List, Dict, Any
import numpy as np


class FewShotPromptBuilder:
    """Few-Shotプロンプトビルダー"""

    def __init__(self):
        self.examples = []
        self.instruction = ""
        self.template = "{input} => {output}"

    def set_instruction(self, instruction: str):
        """タスク指示を設定"""
        self.instruction = instruction
        return self

    def add_example(self, input_text: str, output_text: str):
        """例を追加"""
        self.examples.append({
            'input': input_text,
            'output': output_text
        })
        return self

    def build_prompt(self, query: str) -> str:
        """完全なFew-Shotプロンプトを構築"""
        parts = []

        if self.instruction:
            parts.append(self.instruction)
            parts.append("")

        for ex in self.examples:
            parts.append(self.template.format(
                input=ex['input'],
                output=ex['output']
            ))

        # 出力が空白のクエリ
        parts.append(f"{query} =>")

        return "\n".join(parts)

    def build_chat_messages(
        self,
        query: str,
        system_prompt: str = None
    ) -> List[Dict]:
        """チャット形式のメッセージを構築(GPT-4、Claudeなど向け)"""
        messages = []

        if system_prompt:
            messages.append({
                'role': 'system',
                'content': system_prompt
            })

        for ex in self.examples:
            messages.append({
                'role': 'user',
                'content': ex['input']
            })
            messages.append({
                'role': 'assistant',
                'content': ex['output']
            })

        messages.append({
            'role': 'user',
            'content': query
        })

        return messages


class DynamicExampleSelector:
    """
    動的な例選択器
    より効果的なFew-Shot学習のためにクエリに最も似た例を選択
    """

    def __init__(self, examples: List[Dict], encoder=None):
        self.examples = examples
        self.encoder = encoder  # sentence-transformersなど

    def select_similar(
        self,
        query: str,
        n_examples: int = 3
    ) -> List[Dict]:
        """
        クエリに最も似たn個の例を選択
        """
        if self.encoder is None:
            return np.random.choice(
                self.examples, n_examples, replace=False
            ).tolist()

        query_emb = self.encoder.encode(query)
        example_embs = self.encoder.encode(
            [ex['input'] for ex in self.examples]
        )

        similarities = np.dot(example_embs, query_emb) / (
            np.linalg.norm(example_embs, axis=1)
            * np.linalg.norm(query_emb)
        )

        top_indices = np.argsort(similarities)[-n_examples:][::-1]
        return [self.examples[i] for i in top_indices]

    def select_diverse(
        self,
        n_examples: int = 3
    ) -> List[Dict]:
        """
        多様性を最大化する例を選択(MMRアルゴリズム)
        """
        if len(self.examples) <= n_examples:
            return self.examples

        if self.encoder is None:
            return np.random.choice(
                self.examples, n_examples, replace=False
            ).tolist()

        embeddings = self.encoder.encode(
            [ex['input'] for ex in self.examples]
        )

        selected = [0]
        remaining = list(range(1, len(self.examples)))

        while len(selected) < n_examples:
            selected_embs = embeddings[selected]
            best_idx = None
            best_score = float('-inf')

            for idx in remaining:
                sim_to_selected = np.max(
                    np.dot(selected_embs, embeddings[idx]) / (
                        np.linalg.norm(selected_embs, axis=1)
                        * np.linalg.norm(embeddings[idx])
                    )
                )
                score = -sim_to_selected  # 多様性を最大化

                if score > best_score:
                    best_score = score
                    best_idx = idx

            selected.append(best_idx)
            remaining.remove(best_idx)

        return [self.examples[i] for i in selected]


# ========== 実践的な例 ==========

def sentiment_analysis_few_shot():
    """感情分析のFew-Shot例"""
    builder = FewShotPromptBuilder()

    builder.set_instruction(
        "以下の映画レビューの感情を分析してください。"
        "PositiveまたはNegativeと答えてください。"
    )

    builder.add_example(
        "この映画は本当に感動的で、演技が素晴らしかった。",
        "Positive"
    )
    builder.add_example(
        "ストーリーが退屈すぎて、エンディングが失望的だった。",
        "Negative"
    )
    builder.add_example(
        "特殊効果は印象的だったが、プロットが弱すぎた。",
        "Negative"
    )
    builder.add_example(
        "家族みんなで楽しめる温かい映画。",
        "Positive"
    )

    query = "演出が独特で、音楽が映像と完璧に合っていた。"
    prompt = builder.build_prompt(query)

    print("=== Few-Shotプロンプト ===")
    print(prompt)
    return prompt


def code_generation_few_shot():
    """コード生成のFew-Shot例"""
    builder = FewShotPromptBuilder()

    builder.set_instruction(
        "自然言語の説明をPythonコードに変換してください。"
    )

    builder.add_example(
        "リストの最大要素を見つける",
        "def find_max(lst):\n    return max(lst)"
    )
    builder.add_example(
        "文字列が回文かどうかを確認する",
        "def is_palindrome(s):\n    return s == s[::-1]"
    )
    builder.add_example(
        "ネストされたリストをフラットにする",
        "def flatten(lst):\n    return [x for sublist in lst for x in sublist]"
    )

    query = "リスト内の各要素の出現頻度を数える"
    prompt = builder.build_prompt(query)

    print("=== コード生成Few-Shotプロンプト ===")
    print(prompt)
    return prompt

4.4 クロスリンガルFew-Shot

多言語モデルはゼロショットのクロスリンガル転移能力を示します。英語のみでトレーニングされたタスクを日本語やその他の言語に適用できます。

class CrossLingualFewShot:
    """
    クロスリンガルFew-Shot学習
    英語の例 + ターゲット言語クエリ
    """

    def __init__(self, model_name: str = "xlm-roberta-large"):
        from transformers import AutoTokenizer, AutoModelForSequenceClassification
        import torch

        self.tokenizer = AutoTokenizer.from_pretrained(model_name)
        self.model = AutoModelForSequenceClassification.from_pretrained(
            model_name, num_labels=2
        )

    def encode_text(self, text: str) -> torch.Tensor:
        """テキストを埋め込みにエンコード"""
        inputs = self.tokenizer(
            text,
            return_tensors='pt',
            padding=True,
            truncation=True,
            max_length=128
        )
        with torch.no_grad():
            outputs = self.model(**inputs, output_hidden_states=True)
            embedding = outputs.hidden_states[-1][:, 0, :]
        return embedding

    def classify_zero_shot(
        self,
        query: str,
        class_descriptions_en: List[str]
    ) -> int:
        """
        英語のクラス説明を使ってクエリを分類
        (クエリはどの言語でもOK)
        """
        import torch.nn.functional as F

        query_emb = self.encode_text(query)
        class_embs = torch.cat([
            self.encode_text(desc) for desc in class_descriptions_en
        ])

        similarities = F.cosine_similarity(
            query_emb.expand(len(class_descriptions_en), -1),
            class_embs
        )

        return similarities.argmax().item()

    def few_shot_classify(
        self,
        support_texts: List[str],
        support_labels: List[int],
        query_text: str,
        n_classes: int
    ) -> int:
        """
        Few-Shot分類(プロトタイプアプローチ)
        サポートとクエリは異なる言語でもOK
        """
        import torch
        import torch.nn.functional as F

        support_embs = torch.cat([
            self.encode_text(t) for t in support_texts
        ])
        query_emb = self.encode_text(query_text)

        prototypes = []
        for cls in range(n_classes):
            mask = torch.tensor([l == cls for l in support_labels])
            proto = support_embs[mask].mean(dim=0, keepdim=True)
            prototypes.append(proto)
        prototypes = torch.cat(prototypes)

        dists = torch.cdist(query_emb, prototypes)
        return dists.argmin().item()

5. ハンズオン:医療画像のFew-Shot分類

5.1 希少疾患診断システム

臨床環境では、希少疾患のトレーニングデータは非常に限られています。Few-Shot学習により、わずかな確認症例から新しい疾患パターンを認識できます。

import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision.models as models
import torchvision.transforms as transforms
from torch.utils.data import Dataset, DataLoader
from PIL import Image
import numpy as np


class MedicalImageEncoder(nn.Module):
    """
    医療画像のエンコーダ
    ResNet-18ベース、医療画像の特性に適応
    """

    def __init__(self, embed_dim: int = 512, pretrained: bool = True):
        super().__init__()

        backbone = models.resnet18(pretrained=pretrained)
        self.backbone = nn.Sequential(*list(backbone.children())[:-1])

        self.embed_head = nn.Sequential(
            nn.Flatten(),
            nn.Linear(512, embed_dim),
            nn.LayerNorm(embed_dim),
            nn.ReLU(),
            nn.Linear(embed_dim, embed_dim)
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        features = self.backbone(x)
        return self.embed_head(features)


class MedicalFewShotClassifier:
    """
    医療画像のFew-Shot分類器
    Prototypical Networksベース
    """

    def __init__(
        self,
        encoder: MedicalImageEncoder,
        device: str = 'cpu'
    ):
        self.encoder = encoder.to(device)
        self.device = device
        self.prototypes = {}
        self.disease_names = {}

    def register_disease(
        self,
        disease_id: int,
        disease_name: str,
        support_images: List,
        transform=None
    ):
        """
        新しい疾患を登録(少数の例から)
        """
        self.disease_names[disease_id] = disease_name
        self.encoder.eval()

        embeddings = []
        with torch.no_grad():
            for img in support_images:
                if transform:
                    img_tensor = transform(img).unsqueeze(0).to(self.device)
                else:
                    img_tensor = img.unsqueeze(0).to(self.device)
                emb = self.encoder(img_tensor)
                embeddings.append(emb)

        prototype = torch.cat(embeddings).mean(dim=0)
        self.prototypes[disease_id] = prototype

        print(
            f"疾患を登録: {disease_name} "
            f"({len(support_images)}例)"
        )

    def diagnose(
        self,
        query_image: torch.Tensor,
        top_k: int = 3
    ) -> List[Dict]:
        """
        クエリ画像を診断
        返り値:類似度スコア付きのトップk疾患
        """
        self.encoder.eval()

        with torch.no_grad():
            query_emb = self.encoder(
                query_image.unsqueeze(0).to(self.device)
            )

        results = []
        for disease_id, prototype in self.prototypes.items():
            similarity = F.cosine_similarity(
                query_emb, prototype.unsqueeze(0)
            ).item()
            results.append({
                'disease_id': disease_id,
                'disease_name': self.disease_names[disease_id],
                'similarity': similarity
            })

        results.sort(key=lambda x: x['similarity'], reverse=True)
        return results[:top_k]

    def update_prototype(
        self,
        disease_id: int,
        new_image: torch.Tensor,
        momentum: float = 0.9
    ):
        """
        新しい確認症例でプロトタイプを更新(オンライン学習)
        """
        self.encoder.eval()

        with torch.no_grad():
            new_emb = self.encoder(
                new_image.unsqueeze(0).to(self.device)
            ).squeeze(0)

        if disease_id in self.prototypes:
            # 指数移動平均更新
            self.prototypes[disease_id] = (
                momentum * self.prototypes[disease_id]
                + (1 - momentum) * new_emb
            )
            print(
                f"プロトタイプを更新: {self.disease_names[disease_id]}"
            )
        else:
            print(f"警告: 疾患 {disease_id} は登録されていません。")


def demo_medical_few_shot():
    """医療Few-Shot分類のデモ"""
    transform = transforms.Compose([
        transforms.Resize((224, 224)),
        transforms.ToTensor(),
        transforms.Normalize(
            mean=[0.485, 0.456, 0.406],
            std=[0.229, 0.224, 0.225]
        )
    ])

    encoder = MedicalImageEncoder(embed_dim=512, pretrained=True)
    classifier = MedicalFewShotClassifier(encoder)

    print("=== 医療画像Few-Shot分類システム ===")
    print("このシステムはわずかな確認症例から新しい疾患を認識します。")
    print()
    print("使用法:")
    print("1. classifier.register_disease(id, name, support_images)")
    print("2. results = classifier.diagnose(patient_image)")
    print("3. classifier.update_prototype(disease_id, new_confirmed_image)")

6. learn2learnライブラリの使用

6.1 learn2learnの紹介

learn2learnはMAMLやProtoNetなどのメタ学習アルゴリズムを簡単に実装できるライブラリです。

pip install learn2learn

6.2 learn2learnでMAML

import learn2learn as l2l
import torch
import torch.nn as nn
from typing import Tuple


def build_l2l_maml(
    model: nn.Module,
    lr: float = 0.01,
    first_order: bool = False
) -> l2l.algorithms.MAML:
    """learn2learn MAMLラッパーを作成"""
    return l2l.algorithms.MAML(
        model,
        lr=lr,
        first_order=first_order,
        allow_unused=True
    )


def train_with_l2l(
    maml_model: l2l.algorithms.MAML,
    tasksets,
    n_way: int = 5,
    k_shot: int = 1,
    n_query: int = 15,
    meta_lr: float = 0.003,
    n_iterations: int = 1000,
    adaptation_steps: int = 1,
    device: str = 'cpu'
):
    """learn2learnでMAMLトレーニング"""
    maml_model = maml_model.to(device)
    meta_optimizer = torch.optim.Adam(
        maml_model.parameters(), lr=meta_lr
    )
    criterion = nn.CrossEntropyLoss(reduction='mean')

    for iteration in range(n_iterations):
        meta_optimizer.zero_grad()
        meta_train_loss = 0.0
        meta_train_acc = 0.0

        for task in range(4):
            X, y = tasksets.train.sample()
            X, y = X.to(device), y.to(device)

            learner = maml_model.clone()

            support_indices = torch.zeros(X.size(0), dtype=torch.bool)
            for cls in range(n_way):
                cls_idx = (y == cls).nonzero(as_tuple=True)[0]
                support_idx = cls_idx[:k_shot]
                support_indices[support_idx] = True

            query_indices = ~support_indices
            support_x, support_y = X[support_indices], y[support_indices]
            query_x, query_y = X[query_indices], y[query_indices]

            # 内側のループ:適応
            for step in range(adaptation_steps):
                support_logits = learner(support_x)
                support_loss = criterion(support_logits, support_y)
                learner.adapt(support_loss)

            # 外側のループ:メタ勾配
            query_logits = learner(query_x)
            query_loss = criterion(query_logits, query_y)
            meta_train_loss += query_loss

            preds = query_logits.argmax(dim=-1)
            acc = (preds == query_y).float().mean()
            meta_train_acc += acc

        meta_train_loss /= 4
        meta_train_acc /= 4

        meta_train_loss.backward()
        meta_optimizer.step()

        if (iteration + 1) % 100 == 0:
            print(
                f"Iteration {iteration+1}/{n_iterations} | "
                f"Meta Loss: {meta_train_loss.item():.4f} | "
                f"Meta Acc: {meta_train_acc.item():.4f}"
            )


def setup_omniglot_maml():
    """
    OmniglotデータセットでMAMLをセットアップ
    Omniglot:50のアルファベットから1623のキャラクタークラス(各20サンプル)
    """
    tasksets = l2l.vision.benchmarks.get_tasksets(
        'omniglot',
        train_ways=5,
        train_samples=2 * 1 + 2 * 15,
        test_ways=5,
        test_samples=2 * 1 + 2 * 15,
        root='./data',
        device='cpu'
    )

    model = l2l.vision.models.OmniglotCNN(
        output_size=5,
        hidden_size=64,
        layers=4
    )

    maml = build_l2l_maml(model, lr=0.4, first_order=False)

    return maml, tasksets


def evaluate_l2l(
    maml_model: l2l.algorithms.MAML,
    tasksets,
    n_way: int = 5,
    k_shot: int = 1,
    n_query: int = 15,
    n_test_tasks: int = 600,
    adaptation_steps: int = 3,
    device: str = 'cpu'
) -> Tuple[float, float]:
    """learn2learnでメタ評価"""
    criterion = nn.CrossEntropyLoss()
    total_loss = 0.0
    total_acc = 0.0

    maml_model.eval()

    for _ in range(n_test_tasks):
        X, y = tasksets.test.sample()
        X, y = X.to(device), y.to(device)

        learner = maml_model.clone()

        support_indices = torch.zeros(X.size(0), dtype=torch.bool)
        for cls in range(n_way):
            cls_idx = (y == cls).nonzero(as_tuple=True)[0]
            support_idx = cls_idx[:k_shot]
            support_indices[support_idx] = True

        query_indices = ~support_indices
        support_x, support_y = X[support_indices], y[support_indices]
        query_x, query_y = X[query_indices], y[query_indices]

        for step in range(adaptation_steps):
            support_loss = criterion(learner(support_x), support_y)
            learner.adapt(support_loss)

        with torch.no_grad():
            query_logits = learner(query_x)
            loss = criterion(query_logits, query_y)
            acc = (query_logits.argmax(dim=-1) == query_y).float().mean()

        total_loss += loss.item()
        total_acc += acc.item()

    return total_loss / n_test_tasks, total_acc / n_test_tasks

7. ベンチマークと評価

7.1 主要ベンチマークデータセット

Omniglot

50のアルファベット系統から1623のキャラクタークラス、クラスごとに20サンプル。主に20-way 1-shotの設定で評価されます。

Mini-ImageNet

ImageNetの100クラスのサブセット。クラスごとに600画像(84x84)。5-way 1/5-shotの設定が標準です。

tieredImageNet

Mini-ImageNetのより難しいバージョン。クラスは上位クラスの概念でグループ化されており、メタトレーニングとメタテストクラス間の意味的ギャップが大きくなっています。

CIFAR-FS

CIFAR-100から派生したFew-Shotベンチマーク。Mini-ImageNetよりも実験が速いです。

7.2 標準的な評価プロトコル

def standard_few_shot_evaluation(
    model,
    test_dataset,
    n_way: int = 5,
    k_shot: int = 1,
    n_query: int = 15,
    n_episodes: int = 600,
    confidence_interval: bool = True
) -> Dict:
    """
    標準的なFew-Shot評価プロトコル
    600エピソードの平均と95%信頼区間
    """
    accs = []
    model.eval()

    for episode in range(n_episodes):
        support_x, support_y, query_x, query_y = create_episode(
            test_dataset, n_way, k_shot, n_query
        )

        with torch.no_grad():
            log_probs = model(support_x, support_y, query_x, n_way)
            preds = log_probs.argmax(dim=-1)
            acc = (preds == query_y).float().mean().item()

        accs.append(acc)

    mean_acc = np.mean(accs)
    std_acc = np.std(accs)

    if confidence_interval:
        ci = 1.96 * std_acc / np.sqrt(n_episodes)
        return {
            'mean_accuracy': mean_acc,
            'std': std_acc,
            'confidence_interval_95': ci,
            'result_string': f"{mean_acc*100:.2f} +/- {ci*100:.2f}%"
        }

    return {'mean_accuracy': mean_acc, 'std': std_acc}

8. メタ学習の未来

メタ学習はAI研究のコアパラダイムとして確立されています。注目すべき主要なトレンド:

LLMとの融合

GPT-4やClaudeなどの大規模言語モデルは強力なICL能力を示しています。これらのモデルを任意のドメインでFew-Shot学習を実行するメタ学習器として見る活発な研究が行われています。

マルチモーダルFew-Shot学習

テキスト、画像、音声を統合したFew-Shot学習。GPT-4VやGemini Ultraなどのモデルは視覚的なFew-Shotタスクで印象的な性能を示しています。

継続的学習との組み合わせ

メタ学習で初期化されたモデルは、新しいタスクを学習する際に以前の知識をあまり忘れないことが研究で示されています。継続的学習とメタ学習の組み合わせは活発な研究分野です。

ドメイン適応への応用

データが乏しい産業応用(希少疾患診断、衛星画像分析、専門コード生成)がFew-Shot学習の最も実用的なユースケースとして浮上しています。


参考文献

  • Finn, C., et al. (2017). Model-Agnostic Meta-Learning for Fast Adaptation. ICML 2017. arXiv:1703.03400
  • Snell, J., et al. (2017). Prototypical Networks for Few-shot Learning. NeurIPS 2017. arXiv:1703.05175
  • Vinyals, O., et al. (2016). Matching Networks for One Shot Learning. NeurIPS 2016. arXiv:1602.01783
  • Nichol, A., et al. (2018). On First-Order Meta-Learning Algorithms. arXiv:1803.02999
  • Brown, T., et al. (2020). Language Models are Few-Shot Learners (GPT-3). NeurIPS 2020.
  • learn2learnライブラリ: https://github.com/learnables/learn2learn