Teori dan Implementasi Siamese Network dengan PyTorch untuk One-Shot Learning dan Similarity Detection

Lhuqita Fazry
Deep Learning Siamese Network PyTorch One-Shot Learning Similarity Detection
Teori dan Implementasi Siamese Network dengan PyTorch untuk One-Shot Learning dan Similarity Detection

Konsep Siamese Network dan Arsitektur Dasar

Siamese Network adalah arsitektur deep learning yang dirancang untuk membandingkan dua input dengan cara melewatkannya melalui dua subnetworks identik yang berbagi bobot (weight sharing). Berbeda dengan arsitektur klasifikasi tradisional yang belajar memetakan input ke label kelas tertentu, Siamese Network tidak belajar mengenali kelas secara langsung. Arsitektur ini belajar suatu similarity metric — seberapa mirip dua input satu sama lain.

Komponen utama Siamese Network terdiri dari dua branch CNN identik dengan bobot yang dibagikan, sebuah embedding layer yang menghasilkan representasi vektor fitur, dan distance layer untuk menghitung jarak antar embedding tersebut. Seperti dua timbangan identik yang membandingkan berat dua objek, bukan menghafal nama setiap objek.

Arsitektur Siamese Network

Gambar: Arsitektur Siamese Network — dua branch identik (twin networks) dengan bobot yang dibagikan (shared weights), diakhiri fungsi jarak/similarity pada lapisan atas — Sumber: [Kaya & Bilge, Deep Metric Learning: A Survey (Symmetry, MDPI)](https://www.mdpi.com/2073-8994/11/9/1066) (CC BY 4.0)

Keunggulan utama pendekatan ini sangat relevan untuk use case seperti face verification, signature matching, dan image similarity. Model tidak membutuhkan data besar per kelas dan mampu mengenali kelas baru tanpa proses retraining ulang — inilah esensi dari one-shot learning.

Penting untuk dipahami bahwa embedding yang dihasilkan bukan sekadar representasi perantara. Embedding adalah vektor fitur berdimensi tetap — dalam implementasi kita, setiap gambar dipetakan menjadi vektor 128 dimensi di dalam embedding space. Semua input, terlepas dari resolusi aslinya, direpresentasikan sebagai titik di ruang vektor dengan dimensi yang sama. Kemiripan dua gambar kemudian diukur langsung dari kedekatan dua titik tersebut, sehingga konsep similarity menjadi masalah geometri murni.

Contrastive Loss dan Strategi Pembelajaran Similarity

Agar Siamese Network dapat membedakan pasangan gambar similar dan dissimilar, kita membutuhkan fungsi loss yang tepat — yaitu Contrastive Loss. Fungsi ini mendorong embedding dari gambar yang sejenis untuk saling berdekatan dalam embedding space, sementara embedding dari gambar berbeda dipisahkan sejauh mungkin.

Formula matematis Contrastive Loss adalah:

text
L = (1-Y) * 1/2 * D^2 + Y * 1/2 * {max(0, m-D)}^2

Parameter Y adalah label pasangan: 0 untuk pasangan similar dan 1 untuk dissimilar. D adalah jarak Euclidean antara dua embedding, dan m adalah margin — batas minimum jarak yang harus dipisahkan antar kelas berbeda. Margin ini mencegah model memisahkan semua embedding terlalu jauh tanpa batas.

Contrastive Loss dalam Embedding Space

Gambar: Contrastive Loss — pasangan serupa (similar) ditarik saling berdekatan, sementara pasangan berbeda (dissimilar) didorong terpisah hingga melebihi margin — Sumber: [Kaya & Bilge, Deep Metric Learning: A Survey (Symmetry, MDPI)](https://www.mdpi.com/2073-8994/11/9/1066) (CC BY 4.0)

Berikut implementasi ContrastiveLoss sebagai custom nn.Module di PyTorch:

pythonpython
import torch
import torch.nn as nn

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

    def forward(self, output1, output2, label):
        euclidean_distance = nn.functional.pairwise_distance(
            output1, output2
        )
        loss_similar = (1 - label) * torch.pow(euclidean_distance, 2)
        loss_dissimilar = label * torch.pow(
            torch.clamp(self.margin - euclidean_distance, min=0.0), 2
        )
        return torch.mean(loss_similar + loss_dissimilar) / 2

Cara kerja fungsi ini: Ketika label bernilai 0 (similar), hanya komponen pertama yang aktif — menarik embedding semakin dekat. Ketika label bernilai 1 (dissimilar), komponen kedua aktif — mendorong embedding hingga jaraknya melebihi margin m. Jika jarak sudah melebihi margin, loss-nya nol karena tidak perlu dipisahkan lebih jauh.

Sebagai alternatif, Triplet Loss menggunakan tiga sampel (anchor, positive, negative) dan cocok untuk kasus dengan data lebih banyak. Contrastive Loss lebih sederhana dan efektif untuk skenario one-shot dengan pasangan biner.

Perbedaan mendasar antara keduanya terletak pada peran sampel referensi. Triplet Loss membutuhkan sebuah anchor sebagai titik acuan, lalu membandingkannya dengan satu positive (sampel dari kelas yang sama) dan satu negative (sampel dari kelas berbeda). Model belajar menjaga jarak anchor-positive tetap kecil sekaligus memastikan jarak anchor-negative selalu lebih besar dari jarak anchor-positive ditambah triplet margin. Pada Contrastive Loss, pengukuran dilakukan langsung pada pasangan tanpa sampel ketiga sebagai pembanding, sehingga margin diterapkan pada jarak pasangan itu sendiri.

Kapan kita memilih masing-masing? Triplet Loss menghasilkan embedding yang lebih diskriminatif karena menggunakan informasi relatif tiga sampel, namun membutuhkan data yang cukup banyak agar setiap kelas bisa menyediakan anchor, positive, dan negative yang valid. Contrastive Loss lebih hemat data dan stabil di tahap awal training. Untuk skenario one-shot dengan pasangan biner, Contrastive Loss tetap menjadi pilihan utama.

Deep Learning Bootcamp
Machine Learning • Intermediate

Deep Learning Bootcamp

A beginner-friendly, highly interactive bootcamp designed to take you from found...

Daftar

Implementasi Arsitektur Siamese Network dengan PyTorch

Sebelum memulai, pastikan environment siap dengan library yang diperlukan. Kita akan menggunakan PyTorch sebagai framework utama.

pythonpython
!pip install torch torchvision matplotlib scikit-learn

Backbone CNN yang kita gunakan cukup sederhana: beberapa layer Conv2d dengan BatchNorm dan ReLU, diakhiri dengan AdaptiveAvgPool2d dan sebuah linear layer yang menghasilkan embedding 128 dimensi.

pythonpython
import torch
import torch.nn as nn
import torch.nn.functional as F

class SiameseNetwork(nn.Module):
    def __init__(self):
        super().__init__()
        self.cnn = nn.Sequential(
            nn.Conv2d(1, 64, kernel_size=10),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2),

            nn.Conv2d(64, 128, kernel_size=7),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2),

            nn.Conv2d(128, 256, kernel_size=4),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2),

            nn.AdaptiveAvgPool2d((1, 1)),
            nn.Flatten(),
            nn.Linear(256, 128),
        )

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

Mengapa weight sharing penting? Perhatikan bahwa kita hanya mendefinisikan satu backbone (self.cnn) dan memanggilnya dua kali untuk input yang berbeda. Weight sharing ini memastikan kedua cabang menghasilkan embedding dalam ruang representasi yang identik. Jika kita menggunakan dua network terpisah, representasi yang dihasilkan bisa berada di ruang yang berbeda dan perbandingan jarak menjadi tidak bermakna.

Dataset Preparation dan Training Loop untuk One-Shot Learning

Tahap kritis dalam one-shot learning adalah pembuatan pasangan data (pair generation). Untuk setiap epoch, kita perlu membuat pasangan similar (dua gambar dari kelas yang sama, label 0) dan pasangan dissimilar (gambar dari kelas berbeda, label 1). Kita akan menggunakan dataset MNIST sebagai contoh karena sederhana dan mudah divisualisasikan.

pythonpython
from torch.utils.data import Dataset
import random
import torchvision.transforms as transforms
from torchvision.datasets import MNIST

class SiameseMNISTDataset(Dataset):
    def __init__(self, train=True):
        self.data = MNIST(
            root="./data",
            train=train,
            download=True,
            transform=transforms.Compose([
                transforms.ToTensor(),
                transforms.Normalize((0.1307,), (0.3081,))
            ])
        )
        self.labels = self.data.targets
        self.class_indices = {
            i: torch.where(self.labels == i)[0]
            for i in range(10)
        }

    def __getitem__(self, index):
        img1, label1 = self.data[index]
        # 50% similar, 50% dissimilar
        if random.random() > 0.5:
            # similar pair: same class
            idx2 = random.choice(self.class_indices[label1.item()])
            label = torch.tensor(0, dtype=torch.float32)
        else:
            # dissimilar pair: different class
            neg_label = random.choice([i for i in range(10) if i != label1])
            idx2 = random.choice(self.class_indices[neg_label])
            label = torch.tensor(1, dtype=torch.float32)
        img2, _ = self.data[idx2]
        return img1, img2, label

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

train_dataset = SiameseMNISTDataset(train=True)
train_loader = torch.utils.data.DataLoader(
    train_dataset, batch_size=64, shuffle=True
)

Strategi split untuk evaluasi one-shot: Pastikan kelas di set testing benar-benar tidak muncul di set training. Untuk MNIST, kita bisa menggunakan split standar (60.000 train, 10.000 test) karena kelasnya sudah terpisah.

Mengapa kita menyeimbangkan pasangan similar dan dissimilar dengan rasio 50/50? Jika pasangan dissimilar mendominasi dataset, model bisa belajar solusi trivial: memisahkan semua embedding sejauh mungkin tanpa benar-benar memahami kemiripan. Sebaliknya, jika pasangan similar terlalu dominan, model cenderung menarik semua embedding menjadi satu kelompok. Keseimbangan 50/50 memaksa model mempelajari dua tugas sekaligus — mendekatkan pasangan sejenis dan menjauhkan pasangan berbeda — sehingga tidak ada satu arah optimasi yang diuntungkan.

Pembuatan pasangan acak setiap epoch juga berfungsi sebagai bentuk data augmentation alami. Karena pasangan dibentuk ulang setiap iterasi, model melihat kombinasi gambar yang berbeda di setiap epoch tanpa perlu memperbesar ukuran dataset. Variasi ini membantu model menemukan pola kemiripan yang lebih umum dan mengurangi risiko overfitting terhadap pasangan tertentu yang kebetulan muncul di awal training.

Berikut training loop sederhana untuk melatih Siamese Network:

pythonpython
model = SiameseNetwork()
criterion = ContrastiveLoss(margin=2.0)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

for epoch in range(10):
    total_loss = 0
    for batch_idx, (img1, img2, label) in enumerate(train_loader):
        optimizer.zero_grad()
        output1, output2 = model(img1, img2)
        loss = criterion(output1, output2, label)
        loss.backward()
        optimizer.step()
        total_loss += loss.item()

    avg_loss = total_loss / len(train_loader)
    print(f"Epoch {epoch+1:02d} | Loss: {avg_loss:.4f}")

Output:

text
Epoch 01 | Loss: 0.8281
Epoch 02 | Loss: 0.2498

Yang terjadi selama training: Untuk setiap pasangan gambar, model menghasilkan dua embedding. ContrastiveLoss membandingkan keduanya dan menghitung error berdasarkan label similaritas. Gradient dihitung dan bobot CNN diperbarui melalui backpropagation. Semakin kecil loss, semakin baik model memisahkan embedding kelas berbeda dan mendekatkan embedding kelas yang sama.

Inference, Evaluasi, dan Visualisasi Embedding Space

Setelah model terlatih, kita bisa menggunakannya untuk satu tugas inti: menentukan apakah dua gambar mirip atau tidak. Proses inference cukup sederhana — hitung embedding untuk kedua gambar, lalu hitung Euclidean distance di antara keduanya.

pythonpython
import numpy as np
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt

def compute_similarity(model, img1, img2):
    model.eval()
    with torch.no_grad():
        emb1, emb2 = model(img1.unsqueeze(0), img2.unsqueeze(0))
        distance = nn.functional.pairwise_distance(emb1, emb2).item()
    return distance

# N-way one-shot evaluation
def evaluate_one_shot(model, test_dataset, N=5, n_trials=100):
    correct = 0
    for _ in range(n_trials):
        # Pick random reference class
        ref_class = random.randint(0, 9)
        ref_idx = random.choice(
            test_dataset.class_indices[ref_class].tolist()
        )
        ref_img, _ = test_dataset.data[ref_idx]

        # Create candidates: 1 correct + N-1 distractors
        candidates = [(ref_idx, 0)]  # (idx, label=similar)
        neg_classes = [c for c in range(10) if c != ref_class]
        for _ in range(N - 1):
            neg_c = random.choice(neg_classes)
            neg_idx = random.choice(
                test_dataset.class_indices[neg_c].tolist()
            )
            candidates.append((neg_idx, 1))  # dissimilar

        random.shuffle(candidates)

        # Find closest candidate
        best_dist = float("inf")
        best_idx = None
        for cand_idx, _ in candidates:
            cand_img, _ = test_dataset.data[cand_idx]
            dist = compute_similarity(model, ref_img, cand_img)
            if dist < best_dist:
                best_dist = dist
                best_idx = cand_idx

        if best_idx == ref_idx:
            correct += 1

    return correct / n_trials

# Visualisasi embedding dengan t-SNE
def visualize_embeddings(model, dataset, n_samples=500):
    model.eval()
    embeddings, labels = [], []
    with torch.no_grad():
        for i in range(n_samples):
            img, label = dataset.data[i]
            if isinstance(label, torch.Tensor):
                label = label.item()
            emb = model.cnn(img.unsqueeze(0)).squeeze().numpy()
            embeddings.append(emb)
            labels.append(label)

    tsne = TSNE(n_components=2, random_state=42)
    emb_2d = tsne.fit_transform(np.array(embeddings))

    plt.figure(figsize=(10, 8))
    scatter = plt.scatter(
        emb_2d[:, 0], emb_2d[:, 1],
        c=labels, cmap="tab10", alpha=0.7
    )
    plt.colorbar(scatter)
    plt.title("t-SNE Visualization of Siamese Network Embeddings")
    plt.show()

Output:

text
5-way One-Shot Accuracy: 100.0%
t-SNE Visualization dari Embedding Siamese Network

Cara evaluasi one-shot: Dalam skenario N-way one-shot testing, model diberikan satu gambar referensi dan N pilihan kandidat. Model memilih kandidat yang memiliki embedding paling dekat dengan referensi. Akurasi dihitung dari seberapa sering model memilih kandidat yang benar. Akurasi di atas 90% untuk 5-way classification sudah menunjukkan performa yang baik.

Selain evaluasi berbasis ranking, kita juga sering menerapkan klasifikasi berbasis threshold pada jarak embedding. Prinsipnya sederhana: dua gambar dianggap mirip jika jarak embedding-nya berada di bawah nilai ambang tertentu. Nilai threshold tidak bisa ditebak sembarangan — kita perlu mengukurnya dari data validasi dengan mencoba beberapa nilai kandidat dan mengecek distribusi jarak pasangan similar maupun dissimilar. Titik potong yang optimal biasanya dipilih di area di mana kedua distribusi tumpang tindih seminimal mungkin.

Konsekuensi pemilihan threshold bersifat dua arah. Jika threshold terlalu longgar, banyak pasangan berbeda yang lolos sebagai mirip — muncul false positives yang berbahaya pada sistem face verification karena orang yang berbeda bisa dikenali sebagai orang yang sama. Jika threshold terlalu ketat, pasangan yang sebenarnya mirip justru ditolak — muncul false negatives yang membuat pengguna sah gagal terverifikasi. Menemukan keseimbangan yang tepat adalah bagian penting dari tuning sistem similarity di dunia nyata.

Kapan Siamese Network efektif? Pendekatan ini unggul ketika jumlah data per kelas sangat sedikit (1-5 sampel) dan jumlah kelas bersifat dinamis. Sebaliknya, jika data per kelas melimpah, arsitektur klasifikasi tradisional biasanya lebih efisien secara komputasi.

Langkah Selanjutnya dan Penerapan di Dunia Nyata

Kita telah membangun Siamese Network secara lengkap — dari arsitektur, loss function, training, sampai evaluasi one-shot. Model yang dihasilkan mampu membedakan gambar hanya dengan melihat beberapa contoh per kelas.

Beberapa arah eksplorasi lanjutan yang bisa dicoba: mengganti Contrastive Loss dengan Triplet Loss untuk embedding yang lebih diskriminatif, menggunakan pre-trained backbone seperti ResNet atau EfficientNet untuk dataset yang lebih kompleks, atau menambahkan data augmentation untuk meningkatkan generalisasi.

Penerapan praktis dari Siamese Network sangat luas: sistem face recognition untuk verifikasi identitas, duplicate detection di platform e-commerce, hingga signature verification di industri perbankan. Cobalah bereksperimen dengan dataset sendiri dan modifikasi arsitektur backbone untuk melihat bagaimana performa berubah pada domain yang berbeda.

Ingin mendalami Deep Learning lebih jauh? Program Deep Learning & Computer Vision di Rumah Coding membahas arsitektur neural network, transfer learning, sampai deployment model dengan pendekatan praktis berbasis proyek. Kunjungi [rumahcoding.co.id](https://rumahcoding.co.id) untuk melihat kurikulum lengkap dan jadwal bootcamp terbaru.

Kursus Terkait

GreenGuard: Intelligent Plant Disease Diagnosis Web App
Kursus Premium Machine Learning

Deep Learning Bootcamp

A beginner-friendly, highly interactive bootcamp designed to take you from foundational concepts to deploying real-world Artificial Intelligence applications. Through a completely project-based approach, you will master the core of Deep Learning, Artificial Neural Networks, and Computer Vision using Python and TensorFlow, ultimately building a professional-grade AI web application for your portfolio.

Proyek Akhir

GreenGuard: Intelligent Plant Disease Diagnosis Web App

  • Interactive Image Upload UI: A clean, user-friendly interface built with Streamlit that supports drag-and-drop image uploads directly from a computer or mobile phone.
  • Real-Time AI Inference: Utilizes a lightweight, optimized CNN model (like MobileNetV2) to process the image and return a diagnosis in seconds without heavy server load.
  • Confidence Scoring Dashboard: Visually displays the model's prediction probability (e.g., "95% confident this is Tomato Late Blight") using interactive progress bars or charts.
7 Weeks Intermediate
Lihat Detail Kursus

Artikel Terkait