孪生网络的介绍和实现

Introduction and Implementation of Siamese Networks

介绍

孪生网络提供了一种有趣的分类方法,只需一个示例即可实现准确的图像分类。这些网络采用了一种称为对比损失的概念来衡量数据集中图像对的相似性。与传统方法关注解析图像内容不同,孪生网络集中于理解图像之间的变化和相似之处。这种独特的学习方法有助于提高其在有限数据情况下的鲁棒性,即使没有领域特定的知识也能提高性能。

本文通过孪生网络的视角深入探讨了签名验证的迷人领域。我们将指导您使用PyTorch创建一个功能模型,并提供见解和实际实施步骤。

学习目标

  • 了解孪生网络的概念及其涉及的双子子网络的独特架构。
  • 区分在孪生网络中使用的损失函数,包括二元交叉熵损失、对比损失和三元损失。
  • 识别和描述孪生网络可有效应用于的现实世界应用,如人脸识别、指纹识别和文本相似性评估。
  • 总结孪生网络在一次性学习、通用性和领域无关性能方面的优缺点。

本文是Data Science Blogathon的一部分。

什么是孪生网络?

孪生网络属于一类使用两个相同的子网络进行一次性分类的网络。这些子网络在不同的输入下共享相同的设置、参数和权重。孪生网络学习一种相似性函数,与传统的卷积神经网络不同,后者需要使用大量数据进行训练以预测多个类别。这个函数使我们能够使用最少的数据区分类别,使它们在一次性分类中特别有效。这种独特的能力意味着,在许多情况下,这些网络只需要一个示例就可以准确分类图像。

孪生网络在人脸识别和签名验证任务中具有实际应用。想象一家公司正在实施一个基于人脸的自动考勤系统。只有每个员工的一张照片可用时,传统的卷积神经网络将难以准确分类成千上万名员工。而孪生网络则在这种情况下表现出色。

探索少样本学习

在少样本学习中,模型通过有限数量的示例进行训练来进行预测。这与传统方法相反,传统方法需要大量标记数据进行训练。少样本学习的重要性在于当获取足够的标记数据变得困难或昂贵时。

少样本模型的架构利用少量样本之间的细微差异,仅依靠少量甚至单个示例进行预测。像孪生网络、元学习和类似的方法等各种设计框架使这种能力成为可能。这些框架使模型能够提取有意义的数据表示,并将其用于新的、未见过的样本。

少样本学习发挥作用的几个实际例子包括:

  1. 监控中的目标检测:少样本学习可以在只有少量目标示例的情况下有效识别监控录像中的对象。在对一组有限的标记示例进行训练后,模型可以在新的录像中检测到这些对象,即使它以前从未遇到过这些对象。

2. 个性化医疗:在个性化医疗中,医疗专业人员可能只拥有患者的少量医疗记录,包括少量CT扫描或血液检测。使用少样本学习模型,我们可以利用这些少量示例来预测患者的预期健康状况。这可能包括对某种特定疾病的潜在发作或对特定治疗方法的可能反应的预测。

孪生网络的架构

Siamese网络设计包括两个相同的子网络,每个子网络处理一个输入。最初,输入通过卷积神经网络(CNN)进行处理,从提供的图像中提取重要特征。然后,这些子网络通过完全连接层生成编码输出,从而得到输入数据的压缩表示。

CNN由两个分支和一个共享的特征提取组件组成,包括卷积、批归一化和ReLU激活层,然后是最大池化和dropout层。最后的部分涉及FC层,将提取的特征映射到最终的分类结果。一个函数定义了一个线性层,后面是一系列的ReLU激活和一系列连续的操作(卷积、批归一化、ReLU激活、最大池化和dropout)。前向函数将输入引导通过网络的两个分支。

差异层用于识别输入之间的相似性并放大不同对之间的差异,通过欧氏距离函数实现:

距离(x₁, x₂) = ∥f(x₁) – f(x₂)∥₂

在此情况下,

  • x₁、x₂是两个输入。
  • f(x)表示编码的输出。
  • Distance表示距离函数。

这个属性使得网络能够获得有效的数据表示并将其应用到新的、未见过的样本中。因此,网络生成一个编码,通常表示为相似度得分,有助于类别的区分。

在附图中描述网络的架构。值得注意的是,这个网络作为一个一次性分类器操作,不需要每个类别的许多示例。

Siamese网络中使用的损失函数

损失函数是一个数学工具,用于衡量机器学习模型中预期输出与实际输出之间的差异,给定特定的输入。在训练模型时,目标是通过调整模型的参数来最小化这个损失函数。

许多损失函数适用于不同类型的问题。例如,均方差适用于回归问题,而交叉熵损失适用于分类任务。

与其他几种网络类型不同,Siamese网络采用多个损失函数,下面进行详细说明。

二元交叉熵损失

二元交叉熵损失对于二分类任务非常有价值,其目标是预测两个可能结果之间的区别。在Siamese网络的上下文中,目标是将图像分类为“相似”或“不相似”。

这个函数量化了正类别的预测概率与实际结果之间的差距。在Siamese网络中,预测的概率涉及图像相似性的可能性,而实际结果以二进制形式表示:1表示图像相似性,0表示不相似性。

该函数的公式涉及到真实类别概率的负对数,计算公式为:−(ylog(p)+(1−y)log(1−p))

在这里,

  • y表示真实标签。
  • p表示预测的概率。

使用二元交叉熵损失训练模型的目标是通过参数调整来最小化这个函数。通过这种最小化,模型在准确的类别预测方面获得了能力。

对比损失

对比损失通过使用距离作为相似性度量来区分图像对。当每个类别的训练实例数量有限时,这个函数非常有优势。需要注意的是,对比损失需要负样本和正样本的成对训练样本。附图提供了这个损失的可视化。

对比损失的方程可以是:

(1 – Y) * 0.5 * D^2 + Y * 0.5 * max(0, m – D^2)

这里是分解:

  • Y表示输入参数。
  • D表示欧氏距离。
  • 当Y等于0时,输入属于同一类别。另一方面,Y值为1表示它们来自不同的类别。
  • 参数’m’定义了距离函数的边界,帮助识别导致损失的成对样本。值得注意的是,’m’的值始终大于0。

三元损失

三元损失使用数据的三元组。下图说明了这些三元组。

三元损失函数的目标是增强锚点和负样本之间的分离,同时减小锚点和正样本之间的间隔。

数学上,三元损失函数定义为锚点到正样本距离(d(a,p))和锚点到负样本距离(d(a,n))之间的最大差值减去一个边界值。当这个差值为正数时,计算得到的值就是损失;否则,将其设置为零。

以下是各个组成部分的解释:

  • d表示欧几里得距离。
  • a表示锚点输入。
  • p表示正样本输入。
  • n表示负样本输入。

主要目标是确保正样本比负样本更接近锚点输入,保持一定的分离间隔。

构建基于孪生网络的签名验证模型

签名验证涉及将众多真实签名与伪造签名区分开来。在这种情况下,模型必须理解多个签名之间的细微差别。当面对真实或伪造签名时,它必须能够区分它们的真实性。对于传统的卷积神经网络来说,实现这个验证目标是一个相当大的挑战,因为存在着复杂的变化和有限的训练样本。而且,通常每个人只有一张签名,要求模型能够验证数千个人的签名。下面的部分将深入探讨如何创建一个基于PyTorch的模型来解决这个复杂的任务。

数据集

我们将使用的数据集是关于签名验证的ICDAR 2011数据集。该数据集包含了荷兰签名,包括真实签名和伪造签名。下面是数据的一个样本,用于参考。数据集链接。

问题描述

本文深入研究了在签名验证环境中检测伪造签名的任务。我们的目标是利用一组签名数据,并使用孪生网络来预测测试签名的真实性,区分真实签名和伪造签名。为了实现这个目标,我们必须建立一个逐步的过程。这包括从数据集中获取数据,创建图像对,以及将它们通过孪生网络进行处理。在使用提供的数据集训练网络之后,我们将开发预测函数。

导入必要的库

构建孪生网络需要包含多个关键库。我们引入Pillow库(PIL)用于图像处理,matplotlib用于可视化,numpy用于数值操作,tqdm用于进度条显示。此外,我们还利用PyTorch和torchvision来方便地进行网络训练和构建。

import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision.transforms as transforms
import torchvision.utils as tv_utils
from torch.autograd import Variable
from torch.utils.data import DataLoader, Dataset
import PIL.Image as Image
import matplotlib.pyplot as plt
import numpy as np
import os
import pandas as pd
import torch.utils.data as custom_data
from tqdm import tqdm

实用函数

为了可视化网络的输出结果,我们创建一个实用函数。这个函数接受图像及其对应的标签作为输入,并将它们排列成网格以便于可视化。

import numpy as np
import matplotlib.pyplot as plt

def display_image(img, caption=None, save=False):
    image_array = img.numpy()
    plt.axis("off")
    
    if caption:
        plt.text(
            75,
            8,
            caption,
            style="italic",
            fontweight="bold",
            bbox={"facecolor": "white", "alpha": 0.8, "pad": 10},
        )
    
    plt.imshow(np.transpose(image_array, (1, 2, 0)))
    plt.show()

数据预处理

孪生网络使用的数据结构与传统的图像分类网络有很大的不同。与提供单个图像-标签对不同,孪生网络的数据集生成器需要提供图像对。这些图像对经过转换处理,包括转换为黑白图像,调整大小,最终转换为张量。图像对分为两个不同的类别:正样本对,其特点是输入图像相同;负样本对,其特点是输入图像不同。此外,还提供了一个函数,用于在调用时获取数据集的大小。

import os
import pandas as pd
import torch
import torch.utils.data as data
from PIL import Image
import numpy as np

class PairedDataset(data.Dataset):
    def __init__(self, df_path=None, data_dir=None, transform=None, subset=None):
        self.df = pd.read_csv(df_path)
        if subset is not None:
            self.df = self.df[:subset]
        self.df.columns = ["image1", "image2", "label"]
        self.data_dir = data_dir
        self.transform = transform

    def __getitem__(self, index):
        pair1_path = os.path.join(self.data_dir, self.df.iat[index, 0])
        pair2_path = os.path.join(self.data_dir, self.df.iat[index, 1])

        pair1 = Image.open(pair1_path).convert("L")
        pair2 = Image.open(pair2_path).convert("L")

        if self.transform:
            pair1 = self.transform(pair1)
            pair2 = self.transform(pair2)

        label = torch.tensor([int(self.df.iat[index, 2])], dtype=torch.float32)

        return pair1, pair2, label

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

功能简要介绍

网络的输入由正负数据对组成的图像组成。我们将这些对表示为图像数据,并将它们转换为张量格式,有效地封装了底层图像信息。与Siamese网络相关联的标签是分类的。

特征标准化过程

一个关键步骤涉及特征标准化和将图像转换为黑白。此外,我们将所有图像统一调整大小为(105×105)的正方形格式,因为Siamese网络需要这个尺寸。之后,我们将所有图像转换为张量,这提高了计算效率并实现了GPU的利用。

data_transform = transforms.Compose([
    transforms.Resize((105, 105)),
    transforms.ToTensor()
])

划分数据集

我们将数据集分为不同的训练和测试部分,以便进行模型的训练和测试。为了方便说明,我们只关注前1000个数据点。选择’load_subset’函数值为None将使用完整的数据集,但会导致处理时间延长。考虑数据增强作为提高网络长期性能的一种方法。

train_dataset = PairedDataset(
    df_train,
    dir_train,
    transform=transforms.Compose([
        transforms.Resize((105, 105)),
        transforms.ToTensor()
    ]),
    subset=1000
)

evaluation_dataset = PairedDataset(
    df_val,
    dir_val,
    transform=transforms.Compose([
        transforms.Resize((105, 105)),
        transforms.ToTensor()
    ]),
    subset=1000
)

神经网络架构

构建上述架构涉及一系列步骤。首先,我们建立一个函数,用于构建卷积、批量归一化和ReLU层的集合,灵活地包含或排除最后的Dropout层。另一个函数被设计用于生成由全连接(FC)层组成的序列,后面跟随ReLU层。一旦通过上述函数构建了CNN组件,就将注意力转移到网络的FC部分。值得注意的是,整个网络中实现了不同的填充和卷积核大小。

FC部分由线性层组成的块,后面跟随ReLU激活。定义了架构之后,我们执行前向传播,通过网络处理提供的数据。一个值得强调的重要方面是“view”函数,它通过扁平化的维度对前面块的输出进行了重塑。在建立了这个机制之后,就为使用提供的数据训练Siamese网络做好了准备。

class SiameseNetwork(nn.Module):
    def __init__(self):
        super(SiameseNetwork, self).__init__()

        self.cnn1 = nn.Sequential(
            self.create_conv_block(1, 96, 11, 1, False),
            self.create_conv_block(96, 256, 5, 2, True),
            nn.Conv2d(256, 384, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(384),
            nn.ReLU(inplace=True),
            self.create_conv_block(384, 256, 3, 1, True),
        )

        self.fc1 = nn.Sequential(
            self.create_linear_relu(30976, 1024),
            nn.Dropout2d(p=0.5),
            self.create_linear_relu(1024, 128),
            nn.Linear(128, 2)
        )

    def create_linear_relu(self, input_channels, output_channels):
        return nn.Sequential(nn.Linear(input_channels, output_channels),
        nn.ReLU(inplace=True))

    def create_conv_block(self, input_channels, output_channels, kernel_size,
    padding, dropout=True):
        if dropout:
            return nn.Sequential(
                nn.Conv2d(input_channels, output_channels, kernel_size=kernel_size,
                stride=1, padding=padding),
                nn.BatchNorm2d(output_channels),
                nn.ReLU(inplace=True),
                nn.MaxPool2d(3, stride=2),
                nn.Dropout2d(p=0.3)
            )
        else:
            return nn.Sequential(
                nn.Conv2d(input_channels, output_channels, kernel_size=kernel_size,
                stride=1),
                nn.BatchNorm2d(output_channels),
                nn.ReLU(inplace=True),
                nn.MaxPool2d(3, stride=2)
            )

    def forward_once(self, x):
        output = self.cnn1(x)
        output = output.view(output.size()[0], -1)
        output = self.fc1(output)
        return output

    def forward(self, input1, input2):
        out1 = self.forward_once(input1)
        out2 = self.forward_once(input2)
        return out1, out2

损失函数

对于孪生网络来说,对比损失函数是最关键的损失函数。定义这个损失函数涉及到之前在文章中阐述的方程。为了提高代码的效率,与其将损失定义为一个简单的函数,一种替代方法是从nn.Module类中继承。这允许创建一个定制的类来提供函数的输出。这样的包装器使得PyTorch能够优化代码执行,从而提高整体运行时性能。

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

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

训练孪生网络

在加载和预处理数据之后,开始训练孪生网络的阶段。为了启动这个过程,首先建立训练和测试的数据加载器。值得注意的是,评估数据加载器的批量大小设置为1,以便进行个别评估。随后,将模型部署到GPU上,并定义了关键组件,如对比损失函数和Adam优化器。

train_loader = DataLoader(train_dataset,
                        shuffle=True,
                        num_workers=8,
                        batch_size=bs) 
eval_loader = DataLoader(evaluation_dataset,
                        shuffle=True,
                        num_workers=8,
                        batch_size=1) 

siamese_net = SiameseNetwork().cuda()
loss_function = ContrastiveLoss()
optimizer = torch.optim.Adam(siamese_net.parameters(), lr=1e-3, weight_decay=0.0005)

随后,创建一个函数,接受训练数据加载器作为输入。在这个函数内部,维护一个持续的数组来跟踪损失,以及一个计数器来方便未来的绘图工作。接下来的迭代过程遍历数据加载器中的数据点。对于每个数据点,将图像对传输到GPU上,经过网络处理,并计算对比损失。随后的步骤包括执行反向传播,并提供一批数据的净损失。

def train(train_loader, model, optimizer, loss_function):
    total_loss = 0.0
    num_batches = len(train_loader)

    model.train()

    for batch_idx, (pair_left, pair_right, label) in
    enumerate(tqdm(train_loader, total=num_batches)):
        pair_left, pair_right, label = pair_left.cuda(),
        pair_right.cuda(), label.cuda()

        optimizer.zero_grad()
        output1, output2 = model(pair_left, pair_right)

        contrastive_loss = loss_function(output1, output2, label)
        contrastive_loss.backward()
        optimizer.step()

        total_loss += contrastive_loss.item()

    mean_loss = total_loss / num_batches

    return mean_loss

使用我们设计的函数可以在多个epoch上训练模型。在这个演示中,文章仅覆盖了有限数量的epoch。如果在训练过程中达到的评估损失代表了整个训练过程中观察到的最佳性能,那么模型将被保存以便在特定的epoch进行后续推理。

best_eval_loss = float('inf')

for epoch in tqdm(range(1, num_epoch)):
    train_loss = train(train_loader)
    eval_loss = evaluate(eval_loader)

    print(f"Epoch: {epoch}")
    print(f"Training loss: {train_loss}")
    print(f"Evaluation loss: {eval_loss}")

    if eval_loss < best_eval_loss:
        best_eval_loss = eval_loss
        print(f"Best Evaluation loss: {best_eval_loss}")
        torch.save(siamese_net.state_dict(), "model.pth")
        print("Model Saved Successfully")

测试模型

在模型训练之后进行评估阶段,以评估其性能并对个别数据点进行推理。类似于训练函数,构建了一个评估函数,以测试数据加载器作为输入。通过迭代数据加载器,逐个处理实例。随后,提取用于测试的图像对。然后将这些对传输到GPU上,使模型执行。模型的输出结果用于计算对比损失,并将其存储在指定的列表中。

def evaluate(eval_loader):
    loss_list = []
    counter_list = []
    iteration_number = 0

    for i, data in tqdm(enumerate(eval_loader, 0), total=len(eval_loader)):
        pair_left, pair_right, label = data
        pair_left, pair_right, label = pair_left.cuda(), pair_right.cuda(), label.cuda()
        
        output1, output2 = siamese_net(pair_left, pair_right)
        contrastive_loss = loss_function(output1, output2, label)
        loss_list.append(contrastive_loss.item())
    
    loss_array = np.array(loss_list)
    mean_loss = loss_array.mean() / len(eval_loader)
    
    return mean_loss

我们可以执行代码,对所有测试数据点进行单次评估。为了以可视方式评估性能,我们将生成描绘图像并显示模型在数据点之间识别的成对距离的图表。以网格的形式呈现这些结果。

for i, data in enumerate(dl_eval, 0):
    x0, x1, label = data
    concat_images = torch.cat((x0, x1), 0)
    out1, out2 = siamese_net(x0.to('cuda'), x1.to('cuda'))

    euclidean_distance = F.pairwise_distance(out1, out2)
    print(label)
    if label == torch.FloatTensor([[0]]):
        label_text = "原始签名对"
    else:
        label_text = "伪造签名对"

    display_images(torchvision.utils.make_grid(concat_images))
    print("预测的欧氏距离:", euclidean_distance.item())
    print("实际标签:", label_text)
    if i == 4:
        break

输出

Siamese网络的优缺点

缺点

  • Siamese网络的一个明显缺点是其输出提供的是相似度分数,而不是总和为1的概率分布。这种特性在某些应用中可能会带来挑战,因为概率-based的输出更可取。

优点

  • Siamese网络在处理不同类别中不同数量的示例时表现出韧性。这种适应能力源于网络在有限的类别信息下能够有效运行。
  • 网络的分类性能不依赖于提供特定于领域的信息,从而增加了其灵活性。
  • Siamese网络甚至可以仅使用每个类别的单个图像进行预测。

Siamese网络的应用

Siamese网络在各种应用中发挥作用,下面是一些例子。

人脸识别:Siamese网络在一次性人脸识别任务中表现出优势。通过利用对比损失,这些网络区分不同的面孔和相似的面孔,能够通过最少的数据样本实现有效的人脸识别。

指纹识别:利用Siamese网络进行指纹识别。通过将经过预处理的指纹对提供给网络,它学会区分有效和无效的指纹,提高了基于指纹的身份验证的准确性。

签名验证:本文主要介绍了通过Siamese网络进行签名验证的实现。如演示的那样,该网络处理签名对以确定签名的真实性,区分真实和伪造的签名。

文本相似度:Siamese网络在评估文本相似性方面也具有相关性。通过配对输入,网络可以识别不同文本片段之间的相似性。实际应用包括在问题库中识别类似的问题或从文本存储库中检索相似的文档。

结论

孪生神经网络,通常缩写为SNN,属于包含两个或多个共享相同结构的子网络的神经网络设计范畴。在这个背景下,“相同”意味着具有相同的配置、参数和权重。这些子网络之间参数更新的同步决定了通过特征向量比较来判断输入之间的相似性。

主要要点

  • 孪生网络在对每个类别样本有限的数据集进行分类方面表现出色,使其在训练数据稀缺的场景中具有价值。
  • 通过这个探索,我们深入了解了孪生网络的基本原理,包括其架构、使用的损失函数以及训练此类网络的过程。
  • 我们的探索涵盖了在Signature验证的背景下孪生网络在ICDAR 2011数据集上的实际应用。这涉及到创建一个能够检测伪造签名的模型。
  • 孪生网络的训练和测试流程变得清晰,全面了解了这些网络的运行方式。我们深入研究了成对数据的表示,这是它们有效性的关键方面。

常见问题

本文中显示的媒体不归Analytics Vidhya所有,仅根据作者的决定使用。