Siamese Networksの概要とアルゴリズム及び実装例

機械学習技術 自然言語技術 人工知能技術 デジタルトランスフォーメーション技術 画像処理技術 強化学習技術 確率的生成モデル 深層学習技術 Python 本ブログのナビ
Siamese Networksの概要

Siamese Network は、2つ(または複数)の同一構造のニューラルネットワークを共有重みで並列に配置し、入力間の類似度を学習・評価するモデルアーキテクチャであり、元々は 署名認証 や 顔認識 など、類似性判定タスクのために開発されましたものとなる(Bromley et al., 1993)。

Siamese Networkは、でのそれぞれのネットワークは、例えば画像Aと画像Bといった2つの入力(ペア)を個別に受け取り、それぞれから特徴ベクトル(エンベディング)を抽出する。重みが共有されているため、両方のネットワークは入力に対して同じ変換を行い、比較の公平性が保たれる。

出力された2つの特徴ベクトルに対しては、L1距離(マンハッタン距離)やL2距離(ユークリッド距離)、あるいはコサイン類似度などを用いて、2つの入力の類似度が計算される。

この類似度に基づき、ネットワークの学習には、例えばContrastive Loss(対照損失)やTriplet Loss(3項損失)といった損失関数が用いられる。Contrastive Lossは、類似ペアは近く、非類似ペアは一定以上離れるように学習させる損失関数であり、Triplet Lossは「Anchor」「Positive」「Negative」の3つのサンプルを使って、Anchorに近いPositiveと遠いNegativeの距離差を最大化するように学習するものとなる。

このように、Siamese Networkは「比較可能な特徴空間の学習」に特化しており、顔認証や署名認証、類似画像検索などのタスクで広く活用されている技術となっている。

関連するアルゴリズム

Siamese Networks(シアミーズネットワーク)に関連するアルゴリズムは、主に「類似性学習(Similarity Learning)」や「メトリック学習(Metric Learning)」に関係するものがある。

「入力間の類似度を埋め込み空間で測定する」という枠組みに基づき、さまざまな派生アルゴリズムや応用が提案されている。

損失関数による分類

類似度学習では、データ間の距離(または類似度)を基準にして、ネットワークが「何が似ていて、何が異なるのか」を学習できるようにするために、さまざまな損失関数が提案されている。

  • Contrastive Loss: 2つの入力が類似する場合は埋め込みベクトル間の距離を小さく、非類似であれば距離を大きくなるようにネットワークを訓練するもの。これにより、同じクラスのデータは近接し、異なるクラスのデータは分離された特徴空間が形成される。

数式:
    L = (1 – y) * d² + y * max(0, margin – d)²
ここで、dは2つのサンプル間の距離、yはラベル(y = 0なら同類、y = 1なら異類)。

  • Triplet Loss: Anchor(基準)、Positive(同類)、Negative(異類)という3つの入力を使用し、「AnchorとPositiveの距離 < AnchorとNegativeの距離」となるように学習を行うもの。この関係を学習することで、より明確な境界を持つ識別的な空間が得られる。

数式:
    L = max(d(a, p) – d(a, n) + margin, 0)
ここで、aはAnchor、pはPositive(同じクラス)、nはNegative(異なるクラス)。

  • Quadruplet Loss: Tripletにさらに1つのNegativeサンプルを加えることで、「類似サンプル間の距離」だけでなく「異なるNegative同士の距離関係」にも制約を課すもの。これにより、類似度の境界がより厳密に学習され、識別性能が向上する。

数式:
    L = Triplet_Loss + α * (d(n1, n2) – d(p1, p2))                 ここで、a制約の強さを調整するハイパーパラメータ、(n1, n2)はNegative同士の距離、(p1, p2)はPositive同士の距離

  • N-pair Loss: 1つのAnchorと複数のNegativeを同時に扱い、ミニバッチ全体での識別関係を効率よく学習する手法。これにより、ペア単位やTriplet単位での比較よりも効率的に特徴空間を構築できる。

数式:
    L = log(1 + ∑exp(f⁺ᵀf⁻ – f⁺ᵀf⁺))
ここで、f⁺はPositiveサンプルの特徴ベクトル、f⁻はNegativeサンプル。

Few-shot学習での応用

  • Matching Networks: LSTMとAttentionメカニズムを活用し、「クエリ(分類対象)」と「サポートセット(少数のラベル付きデータ)」との類似度に基づいて分類を行うもの。各クエリサンプルは、Support Set内のサンプルとのコサイン類似度を計算し、類似度の重みによってクラスを予測し、Siameseと同様、比較に基づいた分類を行う点が共通している。
  • Prototypical Networks: 各クラスのサポートセットに属する埋め込みベクトルの平均値(プロトタイプ)を求め、クエリサンプルとの距離(通常はユークリッド距離)を計算して分類するもの。Siamese Networkが「ペアの距離」を用いるのに対し、Prototypical Networksは「クラスの代表ベクトルとクエリの距離」を使う点で拡張的といえ、非常にシンプルかつ強力なfew-shot分類の代表例となる。
  • Relation Networks: クエリとサポートの間の単純な距離関数ではなく、その類似度を出力するネットワーク(Relation Module)自体を学習するもの。これは「類似度とは何か」をネットワークに任せるという点で、Siameseより柔軟性がある。画像の類似性や空間関係といった複雑な情報を考慮する場合に有効である。
  • Meta-Learning系手法との関係(例:MAML): Siamese系のアプローチは、Model-Agnostic Meta-Learning(MAML)などのメタラーニング手法とも併用可能。MAMLは「モデルが少数のステップで新しいタスクに適応すること」を目的としているが、その基礎となる表現学習において、Siamese構造を導入することで、より高速かつ汎化性の高い適応能力を実現することができる。

さらに、自然言語処理の文類似性タスクでは、Siamese BERT(SBERT)という拡張が登場している。これは、BERTなどの文埋め込みモデルをSiamese構造で並列に使用し、2つの文を独立にエンコードした後、それらのベクトル間の距離(通常はコサイン類似度)で意味的な近さを評価する手法で、SBERTは、意味的テキスト類似度(STS)や検索・ランキングタスクで高い性能を示している。

応用実装例

Siamese Networks は、視覚・自然言語処理・バイオメトリクス・医療・検索など、さまざまな分野で活用されている。以下に実装例について述べる。

画像認識応用例: Siamese Networkによる手書き数字の比較(MNIST)

使用技術:

  • TensorFlow/Keras or PyTorch

  • MNISTデータセット

  • Contrastive Loss or Triplet Loss

実装概要:

  1. MNIST画像ペアを作成(正例: 同じ数字、負例: 異なる数字)

  2. CNNベースのSiamese Networkを構築

  3. 最後に2つの特徴ベクトル間の距離をL2で測る

  4. 損失関数に Contrastive Loss を適用

  5. 学習後、新しい画像同士の類似度を測定

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

# 1. データ前処理とペア生成
class SiameseMNIST(Dataset):
    def __init__(self, mnist_dataset):
        self.mnist_dataset = mnist_dataset
        self.targets = mnist_dataset.targets
        self.data = mnist_dataset.data

    def __getitem__(self, index):
        img1, label1 = self.data[index], int(self.targets[index])
        # 同じラベルのペア or 異なるラベルのペアをランダムに選択
        should_get_same_class = random.randint(0, 1)
        while True:
            index2 = random.randint(0, len(self.data) - 1)
            label2 = int(self.targets[index2])
            if should_get_same_class == (label1 == label2):
                break
        img2 = self.data[index2]

        transform = transforms.Compose([transforms.ToPILImage(), transforms.ToTensor()])
        return (transform(img1), transform(img2), torch.tensor([int(label1 == label2)], dtype=torch.float32))

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

# 2. Siamese ネットワーク定義
class SiameseNetwork(nn.Module):
    def __init__(self):
        super(SiameseNetwork, self).__init__()
        self.cnn = nn.Sequential(
            nn.Conv2d(1, 16, 3), nn.ReLU(), nn.MaxPool2d(2),
            nn.Conv2d(16, 32, 3), nn.ReLU(), nn.MaxPool2d(2)
        )
        self.fc = nn.Sequential(
            nn.Linear(32 * 5 * 5, 128),
            nn.ReLU(),
            nn.Linear(128, 64)
        )

    def forward_once(self, x):
        x = self.cnn(x)
        x = x.view(x.size()[0], -1)
        return self.fc(x)

    def forward(self, input1, input2):
        output1 = self.forward_once(input1)
        output2 = self.forward_once(input2)
        return output1, output2

# 3. Contrastive Loss 定義
class ContrastiveLoss(nn.Module):
    def __init__(self, margin=1.0):
        super(ContrastiveLoss, self).__init__()
        self.margin = margin

    def forward(self, output1, output2, label):
        euclidean_distance = F.pairwise_distance(output1, output2)
        loss = torch.mean((1 - label) * torch.pow(euclidean_distance, 2) +
                          (label) * torch.pow(torch.clamp(self.margin - euclidean_distance, min=0.0), 2))
        return loss

# 4. 学習
def train(model, dataloader, loss_fn, optimizer, epochs=5):
    for epoch in range(epochs):
        total_loss = 0
        for img1, img2, label in dataloader:
            img1, img2, label = img1.cuda(), img2.cuda(), label.cuda()
            output1, output2 = model(img1, img2)
            loss = loss_fn(output1, output2, label)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            total_loss += loss.item()
        print(f"Epoch {epoch+1}, Loss: {total_loss:.4f}")

# 5. 実行
if __name__ == "__main__":
    transform = transforms.ToTensor()
    mnist_train = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
    siamese_dataset = SiameseMNIST(mnist_train)
    dataloader = DataLoader(siamese_dataset, shuffle=True, batch_size=64)

    model = SiameseNetwork().cuda()
    loss_fn = ContrastiveLoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

    train(model, dataloader, loss_fn, optimizer, epochs=5)

自然言語処理の実装例: Sentence-BERT(SBERT)による文類似度判定

使用技術:

  • Hugging Face Transformers

  • Pre-trained sentence-transformers/bert-base-nli-mean-tokens

  • コサイン類似度評価

実装概要:

  1. 任意の2文をBERTに入力し、それぞれ文埋め込みベクトルを取得

  2. コサイン類似度で意味の近さを数値で評価(0〜1)

  3. 高スコアなら類似、低スコアなら非類似

from sentence_transformers import SentenceTransformer, util

# 1. モデルの読み込み(日本語や多言語にも対応する場合は別モデル可)
model = SentenceTransformer('all-MiniLM-L6-v2')  # 軽量で高速

# 2. 比較対象の文を用意
sentences = [
    "This is a book about deep learning.",
    "This book explains neural networks in detail.",
    "I like to play soccer on weekends.",
]

# 3. 文章をベクトルに変換
embeddings = model.encode(sentences, convert_to_tensor=True)

# 4. 類似度を計算(コサイン類似度)
cosine_scores = util.pytorch_cos_sim(embeddings, embeddings)

# 5. 出力
print("文間の類似度行列:")
for i in range(len(sentences)):
    for j in range(len(sentences)):
        print(f"({i}, {j}): {cosine_scores[i][j]:.4f}")

医療応用例: 皮膚病変の類似画像検索(ISIC Dataset)

  • 患者画像と既知の皮膚病画像をSiameseで比較し、既存のケースと類似性を提示。

  • 医師の意思決定支援に貢献。

  • 医療画像の特徴抽出にはResNetやEfficientNetベースのバックボーンを使用。

# ライブラリ
pip install torch torchvision scikit-learn matplotlib

# データ定義
from torch.utils.data import Dataset
import os
from PIL import Image
import random
import torch
from torchvision import transforms

class ISICSiameseDataset(Dataset):
    def __init__(self, image_dir, labels_dict, transform=None):
        self.image_dir = image_dir
        self.labels_dict = labels_dict  # filename -> label
        self.transform = transform or transforms.ToTensor()
        self.image_filenames = list(labels_dict.keys())

    def __getitem__(self, idx):
        img1_name = self.image_filenames[idx]
        label1 = self.labels_dict[img1_name]

        # 同じ/異なるラベル画像をランダムに選択
        should_match = random.randint(0, 1)
        while True:
            img2_name = random.choice(self.image_filenames)
            label2 = self.labels_dict[img2_name]
            if (label1 == label2) == should_match:
                break

        img1 = Image.open(os.path.join(self.image_dir, img1_name)).convert('RGB')
        img2 = Image.open(os.path.join(self.image_dir, img2_name)).convert('RGB')

        return self.transform(img1), self.transform(img2), torch.tensor([int(label1 == label2)], dtype=torch.float32)

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

# モデル定義
import torch.nn as nn
import torchvision.models as models

class SiameseResNet(nn.Module):
    def __init__(self):
        super().__init__()
        base_model = models.resnet18(pretrained=True)
        base_model.fc = nn.Identity()  # Remove classification layer
        self.encoder = base_model
        self.fc = nn.Sequential(
            nn.Linear(512, 256),
            nn.ReLU(),
            nn.Linear(256, 128)
        )

    def forward_once(self, x):
        x = self.encoder(x)
        x = self.fc(x)
        return x

    def forward(self, x1, x2):
        out1 = self.forward_once(x1)
        out2 = self.forward_once(x2)
        return out1, out2

# Contrastive Loss
import torch.nn.functional as F

class ContrastiveLoss(nn.Module):
    def __init__(self, margin=2.0):
        super().__init__()
        self.margin = margin

    def forward(self, out1, out2, label):
        distance = F.pairwise_distance(out1, out2)
        loss = torch.mean((1 - label) * distance.pow(2) +
                          (label) * F.relu(self.margin - distance).pow(2))
        return loss

# 学習ループ
def train(model, dataloader, optimizer, criterion, epochs=5):
    model.train()
    for epoch in range(epochs):
        total_loss = 0
        for x1, x2, label in dataloader:
            x1, x2, label = x1.cuda(), x2.cuda(), label.cuda()
            out1, out2 = model(x1, x2)
            loss = criterion(out1, out2, label)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            total_loss += loss.item()
        print(f"Epoch {epoch+1}: Loss {total_loss:.4f}")

# 類似画像検索の推論
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np

def find_similar(query_img, dataset, model, top_k=5):
    model.eval()
    with torch.no_grad():
        q_emb = model.forward_once(query_img.unsqueeze(0).cuda()).cpu().numpy()
        embeddings = []
        paths = []

        for img_path in dataset.image_filenames:
            img = Image.open(os.path.join(dataset.image_dir, img_path)).convert('RGB')
            img_tensor = dataset.transform(img).unsqueeze(0).cuda()
            emb = model.forward_once(img_tensor).cpu().numpy()
            embeddings.append(emb)
            paths.append(img_path)

        similarities = cosine_similarity(q_emb, np.vstack(embeddings))[0]
        top_indices = similarities.argsort()[::-1][:top_k]
        return [paths[i] for i in top_indices], [similarities[i] for i in top_indices]

# 結果の可視化(matplotlib)
import matplotlib.pyplot as plt

def show_similar(query_img, similar_imgs, sim_scores, dataset):
    plt.figure(figsize=(15, 3))
    plt.subplot(1, len(similar_imgs)+1, 1)
    plt.imshow(query_img.permute(1, 2, 0))
    plt.title("Query")
    plt.axis('off')
    for i, (img_path, score) in enumerate(zip(similar_imgs, sim_scores)):
        img = Image.open(os.path.join(dataset.image_dir, img_path)).convert('RGB')
        plt.subplot(1, len(similar_imgs)+1, i+2)
        plt.imshow(img)
        plt.title(f"Score: {score:.2f}")
        plt.axis('off')
    plt.show()

Web検索・レコメンド応用: 類似商品推薦

  • ユーザの閲覧履歴商品画像と、全商品の画像特徴を比較。

  • 類似ベクトル上位のアイテムを推薦する仕組み。

1-shot分類(Omniglot)

  • 未知のクラスに対して、1枚のサンプルだけから分類可能。

  • Omniglot(50言語以上の文字データ)で検証されることが多い。

  • Siamese構造による few-shot learning の代表事例。

# ライブラリインストール
pip install torch transformers scikit-learn

# data
product_data = [
{"title": "Wireless Bluetooth Earphones with Noise Cancellation", "label": "audio"},
{"title": "Over-Ear Noise Cancelling Headphones", "label": "audio"},
{"title": "Smart Fitness Watch with Heart Rate Monitor", "label": "wearable"},
{"title": "Leather Smart Watch Band for Apple Watch", "label": "wearable"},
{"title": "Portable Bluetooth Speaker for Outdoors", "label": "audio"},
{"title": "Cotton Yoga Pants for Women", "label": "clothing"},
]

# siameseコード
import random

def make_pairs(data):
    pairs = []
    for i in range(len(data)):
        for j in range(i + 1, len(data)):
            label = 1 if data[i]["label"] == data[j]["label"] else 0
            pairs.append((data[i]["title"], data[j]["title"], label))
    return pairs

from transformers import AutoTokenizer, AutoModel
import torch.nn as nn
import torch

class SiameseBERT(nn.Module):
    def __init__(self, model_name="sentence-transformers/all-MiniLM-L6-v2"):
        super().__init__()
        self.bert = AutoModel.from_pretrained(model_name)
        self.tokenizer = AutoTokenizer.from_pretrained(model_name)

    def encode(self, sentences):
        encoded = self.tokenizer(sentences, padding=True, truncation=True, return_tensors="pt")
        with torch.no_grad():
            model_output = self.bert(**encoded)
        embeddings = model_output.last_hidden_state[:, 0, :]  # CLS token
        return embeddings

from sklearn.metrics.pairwise import cosine_similarity
import numpy as np

def recommend(query_text, product_data, model, top_k=3):
    all_titles = [item["title"] for item in product_data]
    embeddings = model.encode(all_titles)
    query_emb = model.encode([query_text])
    sims = cosine_similarity(query_emb, embeddings)[0]
    top_idx = sims.argsort()[::-1][1:top_k+1]
    return [(all_titles[i], sims[i]) for i in top_idx]

model = SiameseBERT()

query = "Compact Bluetooth Noise Cancelling Earbuds"
recommendations = recommend(query, product_data, model)

for title, score in recommendations:
    print(f"{title} (Score: {score:.4f})")
具体的な適用事例

Siamese Networks の具体的な適用事例について述べる。

1. 顔認識・認証における代表的な技術事例

FaceNet(Google) は、Triplet Loss を活用することで、1枚の顔画像からその人物の特徴を高次元ベクトル空間上に投影し、個人を識別・クラスタリングできる技術である。この手法は、本人認証や顔検索において高精度を実現し、スマートフォンの顔認証機能や Google フォトにおける人物ごとの顔分類などに応用されている。

一方、DeepFace(Facebook) は Siamese Network に近い構造を持ち、2枚の顔画像が同一人物かどうかを判定するタスクに特化している。この技術は Facebook 上の写真における顔の自動タグ付けや、人物の識別・推薦機能として広く活用されている。

Siamese Networkは、「未知の入力を既知のものと比較する」設計思想に優れており、「分類不可能な少数クラス」や「リアルタイム識別」が求められる分野に特に向いている。

参考文献

Siamese Networks(シアミーズネットワーク)に関する参考文献について述べる。

1. 原典・理論的基礎

2. 応用・発展型アーキテクチャ

  • Schroff et al. (2015, Google)

    • タイトル:FaceNet

    • 内容:Triplet Loss を活用した顔認識とクラスタリング。Siamese発展系。

  • Koch et al. (2015)

  • Vinyals et al. (2016, NIPS)

  • Snell et al. (2017, NeurIPS)

    • タイトル:Prototypical Networks

    • 内容:クラスの中心(プロトタイプ)との距離に基づくFew-shot分類。Siameseの簡易化。

  • Han Hu et.al

3. 実装・実践書籍・リソース

4. 実験・ベンチマークデータセット(参考)

  • Omniglot

    • 用途:Few-shot分類

    • 概要:1-shot学習で用いられる1623クラスの手書き文字データ。

  • LFW(Labeled Faces in the Wild)

    • 用途:顔認識

    • 概要:顔画像のペア分類。FaceNet/Siameseの代表的ベンチマーク。

  • Quora Question Pairs (QQP)

    • 用途:意味類似判定(NLP)

    • 概要:2つの質問が同一意味かを判定する二値分類。SBERT等で評価。

  • ISIC(Skin Lesion Dataset)

    • 用途:医療画像類似度学習

    • 概要:皮膚病変の診断補助。画像間の視覚的類似性学習に使用。

コメント

タイトルとURLをコピーしました