说明

使用Siamese网络进行目标的相似度比较,其好处在于避免了许多复杂的数学处理(仿射变换)。本文参考了PyTorch练手项目四:孪生网络(Siamese Network),并结合github上的源码进行了实现,和修改(源码只能使用CUDA)。
涉及到的内容有:

  • 1 模型流程及其对应部分的内容准备
  • 2 主要代码的拆解分析
  • 3 新数据集的实验
  • 4 原理分析(因为神经网络以应用为主,理论反而在后)

本文的实验环境是Mac(CPU模式) + Pytorch。

1 模型流程及其对应部分的内容准备

从模型的本质来看,神经网络和经典机器学习是一样的。都有:

  • 1 数据集准备(Train, Test)
  • 2 模型构建(Model Build)
  • 3 模型拟合(Model Fit)
  • 4 模型评估(Model Evaluation)
  • 5 模型应用(Model Predict)
  • 6 模型持久化/服务化(Model Implementation)

Siamese从某种程度上和余弦相似度之类的方法作用一样,即比较两个图片(向量)之间的相似性。不一样的地方:

  • 1 数据集是图片。一个主体(subject)一个文件夹。
  • 2 模型的主体结构是网络结构。
  • 3 模型的拟合是通过损失函数作为度量,使用神经网络的前馈和反馈计算来调整参数。
  • 4 模型读取和输出比传统机器学习复杂一些。
  • 5 耗时更多。(最好还是上CUDA,过几天我就打算买一块)

先以github拉取的项目为例,使用命令tree -a(或者使用tree -L n控制展开的层级)展示其结构(样本图片在train和test各保存一个):

├── LICENSE
├── README.md
├── Siamese-networks-medium.ipynb  函数都在这里
├── conda-env.yml conda的配置文件,不用conda应该不用理会
├── data  数据
│   └── faces 人脸数据集
│       ├── testing 测试数据集
│       │   ├── s5 某个样本(s5)的10张图片
│       │   │   ├── 1.pgm
│       │   │   ├── 10.pgm
│       │   │   ├── 2.pgm
│       │   │   ├── 3.pgm
│       │   │   ├── 4.pgm
│       │   │   ├── 5.pgm
│       │   │   ├── 6.pgm
│       │   │   ├── 7.pgm
│       │   │   ├── 8.pgm
│       │   │   └── 9.pgm
│       └── training 训练数据集
│           ├── README
│           ├── s1 某个样本(s1)的10张图片
│           │   ├── 1.pgm
│           │   ├── 10.pgm
│           │   ├── 2.pgm
│           │   ├── 3.pgm
│           │   ├── 4.pgm
│           │   ├── 5.pgm
│           │   ├── 6.pgm
│           │   ├── 7.pgm
│           │   ├── 8.pgm
│           │   └── 9.pgm
├── my_model_all.pth  - 原项目没有,这个是我保存的
└── my_model_param.pth - 原项目没有,这个是我保存的

ORL人脸数据集共包含40个不同人的400张图像,是在1992年4月至1994年4月期间由英国剑桥的Olivetti研究实验室创建。此数据集下包含40个目录,每个目录下有10张图像,每个目录表示一个不同的人。所有的图像是以PGM格式存储,灰度图,图像大小宽度为92,高度为112。对每一个目录下的图像,这些图像是在不同的时间、不同的光照、不同的面部表情(睁眼/闭眼,微笑/不微笑)和面部细节(戴眼镜/不戴眼镜)环境下采集的。所有的图像是在较暗的均匀背景下拍摄的,拍摄的是正脸(有些带有略微的侧偏)。
下载的数据集中training文件夹包含37个人物的图像,其余3个人的图像放在testing文件夹中,留给后续测试时候使用。

对于Siamese网络来说,比较的是人脸是否一致(相似度),因此训练时只要读取的是同一个文件夹下的图片,那么就是一致的,否则就是不一致。

  • 关于pgm图片格式,可以参考PGM格式图像详解

PGM 是便携式灰度图像格式(portable graymap file format),在黑白超声图像系统中经常使用PGM格式的图像。文件的后缀名为”.pgm”,PGM格式图像格式分为两类:P2和P5类型。不管是P2还是P5类型的PGM文件,都由两部分组成,文件头部分和数据部分。
文件头部分
文件头包括的信息依次是:
1.PGM文件的格式类型(是P2还是P5);
2.图像的宽度;
3.图像的高度;
4.图像灰度值可能的最大值;
文件头的这四部分信息都是以ASCII码形式存储的,所以可以直接在将P2或P5格式的PGM文件在记事本中打开看到文件头的信息。

  • 关于pth文件格式,可以参考pytorch中保存的模型文件.pth深入解析

在pytorch进行模型保存的时候,一般有两种保存方式,一种是保存整个模型,另一种是只保存模型的参数。

torch.save(model.state_dict(), "my_model.pth")  # 只保存模型的参数
torch.save(model, "my_model.pth")  # 保存整个模型

总体上,按照格式准备数据(自己准备样本的时候稍微麻烦点) -> 按结构放好各部分的数据/代码 -> 执行训练步骤 -> 获得结果。

2 主要代码的拆解分析

导入的包:

import torchvision
import torchvision.datasets as dset
import torchvision.transforms as transforms
# 导入数据
from torch.utils.data import DataLoader,Dataset
import matplotlib.pyplot as plt
import torchvision.utils
import numpy as np
import random
from PIL import Image
import torch
from torch.autograd import Variable
import PIL.ImageOps
import torch.nn as nn
from torch import optim
import torch.nn.functional as F

torch.utils.data.DataLoader用于载入数据, 详细介绍

Pytorch的数据格式:pytorch中TensorDataset函数

# torch.utils.data.TensorDataset(data_tensor, target_tensor)
# data_tensor (Tensor) - 包含样本数据
# target_tensor (Tensor) - 包含样本目标(标签)
# [b, c, h, w] b:几张照片, c:通道, w:宽 ,h:高度

图片显示函数

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

使用类的属性来保存

Epoch: 使用训练集的全部数据对模型进行一次完整训练,叫”一代训练“
Batch: 使用训练集的一小部分数据对模型进行一次反向传播的更新,叫"一批数据"
Iteration: 使用一个Batch进行训练,叫”一次迭代“

class Config():training_dir = "./data/faces/training/"testing_dir = "./data/faces/testing/"train_batch_size = 64# 这里把epochs改的小一点,节约点时间train_number_epochs = 10

自定义Dataset类,参考

torch.utils.data.Dataset是代表自定义数据集方法的抽象类,你可以自己定义你的数据类继承这个抽象类,非常简单,只需要定义__len__和__getitem__这两个方法就可以。
通过继承torch.utils.data.Dataset的这个抽象类,我们可以定义好我们需要的数据类。当我们通过迭代的方式来取得每一个数据,但是这样很难实现取batch,shuffle或者多线程读取数据,所以pytorch还提供了一个简单的方法来做这件事情,通过torch.utils.data.DataLoader类来定义一个新的迭代器,用来将自定义的数据读取接口的输出或者PyTorch已有的数据读取接口的输入按照batch size封装成Tensor,后续只需要再包装成Variable即可作为模型的输入。
总之,通过torch.utils.data.Dataset和torch.utils.data.DataLoader这两个类,使数据的读取变得非常简单,快捷。

在这之前还要了解torchvision.datasets这个组件,参考。

torchvision主要由3个子包组成,分别是:torchvision.datasets、torchvision.models、torchvision.transforms。
torchvision.datasets这个包中包含MNIST、FakeData、COCO、LSUN、ImageFolder、DatasetFolder、ImageNet、CIFAR等一些常用的数据集,并且提供了数据集设置的一些重要参数设置,可以通过简单数据集设置来进行数据集的调用。

需要着重介绍一下torchvision.datasets.ImageFolder 这个子包,参考,要求数据集的组织方式类似

    root/dog/xxx.pngroot/dog/xxy.pngroot/dog/xxz.pngroot/cat/123.pngroot/cat/nsdf3.pngroot/cat/asd932_.png

该类初始化后,会有以下四个参数和三个属性:

1.root:图片存储的根目录,即各类别文件夹所在目录的上一级目录,在下面的例子中是’./data/train/’。
2.transform:对图片进行预处理的操作(函数),原始图片作为输入,返回一个转换后的图片。
3.target_transform:对图片类别进行预处理的操作,输入为 target,输出对其的转换。如果不传该参数,即对 target 不做任何转换,返回的顺序索引 0,1, 2…
4.loader:表示数据集加载方式,通常默认加载方式即可。
另外,该 API 有以下成员变量:1.self.classes:用一个 list 保存类别名称
2.self.class_to_idx:类别对应的索引,与不做任何转换返回的 target 对应
3.self.imgs:保存(img-path, class) tuple的 list,与我们自定义 Dataset类的 def __getitem__(self, index): 返回值类似。
# 将Config类下的文件目录进行映射(改成你使用中实际的位置)
folder_dataset = dset.ImageFolder(root=Config.training_dir)
# 所有的类别
In [9]: folder_dataset.classes
Out[9]:
['s1','s10','s11','s12','s13','s14','s15',
...# 图片的最后一位就是类别的代号(按照类别的字母顺序排的)
In [10]: folder_dataset.imgs
Out[10]:
[('yourpath/data/faces/training/s1/1.pgm',0),
...
]In [11]: folder_dataset.class_to_idx
Out[11]:
{'s1': 0,'s10': 1,'s11': 2,'s12': 3,'s13': 4,'s14': 5,
...

自定义本次孪生网络的数据集,继承了torch.utils.data.Dataset对象,我在原文的代码上加了一些注释

class SiameseNetworkDataset(Dataset):def __init__(self,imageFolderDataset,transform=None,should_invert=True):self.imageFolderDataset = imageFolderDataset    self.transform = transformself.should_invert = should_invert# 当实例对象做p[key] 运算时,会调用类中的方法__getitem__def __getitem__(self,index):# 先选取一张图片img0_tuple = random.choice(self.imageFolderDataset.imgs)#we need to make sure approx 50% of images are in the same class 希望大约一半的图片是来自一个类别的# 随机选取0和1should_get_same_class = random.randint(0,1) # 随机1, 选取同类别的两张图片 if should_get_same_class:while True:#keep looping till the same class image is found# 选取到同类别的就停下img1_tuple = random.choice(self.imageFolderDataset.imgs) if img0_tuple[1]==img1_tuple[1]:break# 随机0,选取不同类别的两张图片else:while True:#keep looping till a different class image is found# 选取到不同类别的就停下img1_tuple = random.choice(self.imageFolderDataset.imgs) if img0_tuple[1] !=img1_tuple[1]:break# 打开图片(可见tuple保存的只是文件的地址)img0 = Image.open(img0_tuple[0])img1 = Image.open(img1_tuple[0])# 转化为灰度图片# PIL的九种格式1,L,P,RGB,RGBA,CMYK,YCbCr,I,Fimg0 = img0.convert("L")img1 = img1.convert("L")# 利用PIL.ImageOps.invert实现二值图像黑白反转if self.should_invert:img0 = PIL.ImageOps.invert(img0)img1 = PIL.ImageOps.invert(img1)if self.transform is not None:img0 = self.transform(img0)img1 = self.transform(img1)# 返回img0, img1以及img0是否和img1是同一类(1,0)return img0, img1 , torch.from_numpy(np.array([int(img1_tuple[1]!=img0_tuple[1])],dtype=np.float32))def __len__(self):return len(self.imageFolderDataset.imgs)

PIL关于图片的操作和九种图片格式可以参考这篇文章

模式 描述
1 1位像素,表示黑和白,但是存储的时候每个像素存储为8bit。
L 8位像素,表示黑和白。
P 8位像素,使用调色板映射到其他模式。
RGB 3x8位像素,为真彩色。
RGBA 4x8位像素,有透明通道的真彩色。
CMYK 4x8位像素,颜色分离。
YCbCr 3x8位像素,彩色视频格式。
I 32位整型像素。
F 32位浮点型像素。

加载一些图片进行可视化

# 取一部分进行可视化, 每个批次取8个结果
vis_dataloader = DataLoader(siamese_dataset,shuffle=True,num_workers=8,batch_size=8)
# 把loader变成迭代对象
dataiter = iter(vis_dataloader)# examplet_batch 是下一个实例
example_batch = next(dataiter)# 目前每个批次返回的形状是 img0, img1 , 0/1
# 将img0 list(8张)和img1 list(8张)按行堆叠,然后把标签打在下面
concatenated = torch.cat((example_batch[0], example_batch[1]), 0)
imshow(torchvision.utils.make_grid(concatenated))
print(example_batch[2].numpy())# --- print的结果
[[1.][0.][1.][1.][1.][0.][0.][1.]]


定义网络,这里暂时没啥要说的(得从原理开始说起)

# 使用CNN
class SiameseNetwork(nn.Module):def __init__(self):super(SiameseNetwork, self).__init__()self.cnn1 = nn.Sequential(nn.ReflectionPad2d(1),nn.Conv2d(1, 4, kernel_size=3),nn.ReLU(inplace=True),nn.BatchNorm2d(4),nn.ReflectionPad2d(1),nn.Conv2d(4, 8, kernel_size=3),nn.ReLU(inplace=True),nn.BatchNorm2d(8),nn.ReflectionPad2d(1),nn.Conv2d(8, 8, kernel_size=3),nn.ReLU(inplace=True),nn.BatchNorm2d(8),)self.fc1 = nn.Sequential(nn.Linear(8*100*100, 500),nn.ReLU(inplace=True),nn.Linear(500, 500),nn.ReLU(inplace=True),nn.Linear(500, 5))def forward_once(self, x):output = self.cnn1(x)output = output.view(output.size()[0], -1)output = self.fc1(output)return outputdef forward(self, input1, input2):output1 = self.forward_once(input1)output2 = self.forward_once(input2)return output1, output2

损失函数定义

class ContrastiveLoss(torch.nn.Module):"""Contrastive loss function.Based on: http://yann.lecun.com/exdb/publis/pdf/hadsell-chopra-lecun-06.pdf"""def __init__(self, margin=2.0):super(ContrastiveLoss, self).__init__()self.margin = margindef forward(self, output1, output2, label):euclidean_distance = F.pairwise_distance(output1, output2, keepdim = True)loss_contrastive = torch.mean((1-label) * torch.pow(euclidean_distance, 2) +(label) * torch.pow(torch.clamp(self.margin - euclidean_distance, min=0.0), 2))return loss_contrastive

训练过程

# 7 训练
# 7.1 训练的数据加载器
train_dataloader = DataLoader(siamese_dataset,shuffle=True,num_workers=8,batch_size=Config.train_batch_size)# 检查是否有gpu
is_gpu = torch.cuda.is_available()
gpu_nums = torch.cuda.device_count()# 如果有gpu就用cuda初始化
if torch.cuda.is_available():net = SiameseNetwork().cuda()
else:net = SiameseNetwork()
# 损失函数实例化
criterion = ContrastiveLoss()
# 优化器定义
optimizer = optim.Adam(net.parameters(), lr=0.0005)
# 定义训练初始参数
counter = []
loss_history = []
iteration_number = 0# 7.2按配置进行训练
for epoch in range(0, Config.train_number_epochs):for i, data in enumerate(train_dataloader, 0):img0, img1, label = data# 如果有gpu使用cudaif is_gpu:img0, img1 , label = img0.cuda(), img1.cuda() , label.cuda()optimizer.zero_grad()output1, output2 = net(img0, img1)loss_contrastive = criterion(output1, output2, label)loss_contrastive.backward()optimizer.step()if i % 10 == 0:print("Epoch number {}\n Current loss {}\n".format(epoch, loss_contrastive.item()))iteration_number += 10counter.append(iteration_number)loss_history.append(loss_contrastive.item())
show_plot(counter, loss_history)
# 7.3 训练结果的保存
torch.save(net.state_dict(), "my_model_param.pth")  # 只保存模型的参数
torch.save(net, "my_model_all.pth")  # 保存整个模型# --- 运行状态
Epoch number 0Current loss 1.3732595443725586Epoch number 1Current loss 4.70654821395874Epoch number 2Current loss 1.6751190423965454Epoch number 3Current loss 0.74247145652771Epoch number 4Current loss 1.3971765041351318Epoch number 5Current loss 1.1309750080108643

测试集观察

# ---- 载入模型并执行测试
# 载入模型
net = torch.load("my_model_all.pth")is_gpu = torch.cuda.is_available()
# 设置测试数据集
folder_dataset_test = dset.ImageFolder(root=Config.testing_dir)
siamese_dataset = SiameseNetworkDataset(imageFolderDataset=folder_dataset_test,transform=transforms.Compose([transforms.Resize((100, 100)),transforms.ToTensor()]), should_invert=False)
# 测试数据集的loader
test_dataloader = DataLoader(siamese_dataset, num_workers=6, batch_size=1, shuffle=True)
dataiter = iter(test_dataloader)
# 先从测试集中获取一张图片
x0, _, _ = next(dataiter)# 随机取出10张别的图片比较,画图并计算欧几里得距离(越大表示差异性越大)
for i in range(10):_,x1,label2 = next(dataiter)concatenated = torch.cat((x0,x1),0)if not is_gpu:output1,output2 = net(Variable(x0),Variable(x1))else:output1,output2 = net(Variable(x0).cuda(),Variable(x1).cuda())euclidean_distance = F.pairwise_distance(output1, output2)imshow(torchvision.utils.make_grid(concatenated),'Dissimilarity: {:.2f}'.format(euclidean_distance.item()))


到此,介绍了数据集的准备,以及训练和测试的代码,训练得到了模型并进行了测试(效果还凑合,cpu着实有点慢)。

— 未完待续

3 新数据集的实验

数据的整理部分内容略多,可以参考我的另一篇文章

展示的部分图片

[[0.][0.][1.][0.][0.][0.][1.][0.]]

执行训练(前几轮有点飘,最后还是收敛了)
Epoch number 0Current loss 1.5784754753112793Epoch number 1Current loss 30.29207420349121Epoch number 2Current loss 18.589834213256836Epoch number 3Current loss 5.201380252838135Epoch number 4Current loss 9.606918334960938Epoch number 5Current loss 9.415582656860352...
Epoch number 93Current loss 0.08404700458049774Epoch number 94Current loss 0.09089607000350952Epoch number 95Current loss 0.06858702003955841Epoch number 96Current loss 0.07817203551530838Epoch number 97Current loss 0.09011589735746384用cpu大致跑了867.70 secs

结果:
1/10

2/10

3/10

4/10

5/10

6/10

7/10

8/10


9/10

10/10

效果差强人意吧,不过这个只是10张图片,随便跑的

补充

关于pth和pt文件,可以参考:

Pytorch的模型文件一般会保存为.pth文件,C++接口一般读取的是.pt文件,因此,C++在调用Pytorch训练好的模型文件的时候就需要进行一个转换,转换为.pt文件,才能够读取。

另外,代码需要保持一个良好的兼容性(cpu和gpu)。有的代码不考虑cpu版本的,这样就需要花时间修改。(例如云服务器一般都不带gpu的,很贵)

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

4 原理分析


Python - 深度学习系列2-人脸比对 Siamese相关推荐

  1. Python - 深度学习系列1-目标识别 yolo

    1 目的 实现基于yolo网络的目标识别. 使用github上开源的代码.那么需要做的事只有几样: 1 原理.多少还是知道一下yolo的原理以及应用特点. 2 环境.对应的安装包,特别是cpu.gpu ...

  2. Python - 深度学习系列13- 显卡与CPU计算对比

    说明 因为装3060Ti的时候踩了坑(雷?),所以不太清楚这张卡是不是如同之前想象的一样,所以这篇文章会进行一系列的实验和对比.可能对其他希望用显卡进行计算的人有所帮助. 1 本篇的代码可以在装好显卡 ...

  3. python系列文章(基础,应用,后端,运维,自动化测试,爬虫,数据分析,可视化,机器学习,深度学习系列内容)

    python基础教程 python基础系列教程--Python的安装与测试:python解释器.PyDev编辑器.pycharm编译器 python基础系列教程--Python库的安装与卸载 pyth ...

  4. 深度学习之视频人脸识别系列一:介绍

    作者 | 东田应子 [导读]本文是深度学习之视频人脸识别系列的第一篇文章,介绍了人脸识别领域的一些基本概念,分析了深度学习在人脸识别的基本流程,并总结了近年来科研领域的研究进展,最后分析了静态数据与视 ...

  5. 深度学习之视频人脸识别系列(一):简介

    阅读时间约4分钟 [介绍]本文是深度学习之视频人脸识别系列的第一篇文章,介绍了人脸识别领域的一些基本概念,分析了深度学习在人脸识别的基本流程,并总结了近年来科研领域的研究进展,最后分析了静态数据与视频 ...

  6. 深度学习之视频人脸识别系列二:人脸检测与对齐

    作者 | 东田应子 [磐创AI导读]本文是深度学习之视频人脸识别系列的第二篇文章,介绍人脸检测与对齐的相关算法.欢迎大家关注我们的公众号:磐创AI. 一.人脸检测与关键点检测 问题描述: 人脸检测解决 ...

  7. 腾讯深度学习系列——深度学习及并行化实现概述

    深度学习及并行化实现概述 摘要: 深度学习可以完成需要高度抽象特征的人工智能任务,如语音识别.图像识别和检索.自然语言理解等.深层模型是包含多个隐藏层的人工神经网络,多层非线性结构使其具备强大的特征表 ...

  8. 「每周CV论文推荐」 初学深度学习单图三维人脸重建需要读的文章

    基于图像的人脸三维重建在人脸分析与娱乐领域里有巨大的应用场景,本文来介绍初学深度学习单张图像人脸三维重建必须要读的文章. 作者&编辑 | 言有三 1 3DMM与数据集 虽然这里推荐的是深度学习 ...

  9. 【深度学习系列】卷积神经网络CNN原理详解(一)——基本原理(1)

    上篇文章我们给出了用paddlepaddle来做手写数字识别的示例,并对网络结构进行到了调整,提高了识别的精度.有的同学表示不是很理解原理,为什么传统的机器学习算法,简单的神经网络(如多层感知机)都可 ...

最新文章

  1. windows7怎么安装python库_如何在Windows 7安装Python2.7
  2. Hadoop集群安装-CDH5(5台服务器集群)
  3. The Relation Between Gradient Descent and Cost Funtion(To be continued)
  4. Spring Boot-Spring Tool Suit + Gradle 构建第一个Spring Boot 项目02
  5. oracle expdp自动导出数据,Oracle expdp数据泵远程导出
  6. php 打印对象到文件,php实现将数组或对象写入到文件的方法小结【三种方法】...
  7. C语言求一个数的倒数的平方根近似值
  8. iPhone程序中如何生成随机数
  9. mysql教程泰牛程序员_mysql高级教程笔记.docx
  10. U分布、T分布、z分位数
  11. 美团取消支付宝支付是“合情合理”?
  12. 数据导入与预处理-第8章-实战演练-数据分析师岗位分析
  13. 吉首大学校赛 A SARS病毒 (欧拉降幂)
  14. 基于ESB权限初始化流程开发总结
  15. 计算机二级相关快捷键,计算机二级考试Word+Excel必备快捷键!
  16. AutoCAD 描图方法小结
  17. linux安装驱动报错权限没有,linux系统下安装显卡驱动程序.doc
  18. 华为新员工入职180天详细培训计划
  19. 芯片盛世70年!张忠谋细数晶体管发展历程
  20. TCH话务量商务智能分析

热门文章

  1. CCP2.1协议基础知识
  2. php基本变量,PHP-语法及变量基本操作
  3. 用本地播放器看直播,录制高清视频streamlink的使用
  4. 宝宝去了幼儿园不爱说话怎么办?
  5. python flask 直接调用摄像头直播
  6. 2019年安徽大学ACM/ICPC实验室新生赛(公开赛)
  7. 数据库原理第一章测验(标黑的为答案)
  8. Linux信号处理简析
  9. 用HTML编写携程旅行,StaticHtmlPage(仿照携程写的静态网页)
  10. 【Java工程中出现问题】XXX has been compiled by a more recent version of the Java Runtime