孪生网络
孪生网络会比较两幅图像,并判断两幅图像是来自同一类鲸鱼还是不同的鲸鱼。 通过将测试集中的每幅图像与训练集中的每幅图像进行比较,从而对图片的相似度进行匹配排序来识别最有可能的鲸鱼类别。
孪生网络由两部分组成。 卷积神经网络(CNN)将输入图像转换为描述鲸鱼的特征向量。 具有相同权重的CNN模型计算两幅图像。将CNN称为branch model。 使用的自定义模型主要来自于ResNet。
而head model用于比较CNN的特征向量,并确定两条鲸鱼是否匹配。
branch model
branch model是常规的CNN模型。 以下是其设计的关键要素:
一、因为训练数据集很小,所以试图保持模型参数量相对较小,但同时模型还能保持足够的表现力。 例如,类似ResNet的体系结构比类似VGG的网络更有效。
二、事实证明,模型大小是受内存限制的,大多数内存用于存储前向传播的激活,以及用于计算反向传播期间的梯度。使用Windows 10和GTX 1080卡时,大约有6.8GB VRAM可用,并且上述条件限制了模型结构的选择。
分支模型由6个块组成,每个块处理图的分辨率越来越小,并带有中间池层。
Block 1 - 384x384
Block 2 - 96x96
Block 3 - 48x48
Block 4 - 24x24
Block 5 - 12x12
Block 6 - 6x6
Block 1具有一个步幅为2的单个卷积层,然后是2x2的最大池化层。由于分辨率高,它占用大量内存,因此这里要做的工作最少,可以为后续Block节省内存。
Block 2具有两个类似于VGG的3x3卷积。与随后的ResNet块相比,这些卷积占用的内存更少,并且用于节省内存。请注意,此后,张量的尺寸为96x96x64,与初始384x384x1图像的体积相同,因此我们可以假设没有丢失任何重要信息。
Block 3到Block 6执行类似卷积的ResNet。其想法是形成一个子块,该子块具有减少特征数量的1x1卷积,3x3卷积和另一个1x1卷积,以将特征数量恢复为原始特征。然后将这些卷积的输出添加到原始张量(旁路连接)。逐个使用4个这样的子块,再加上一个1x1卷积来增加每个池化层之后的特征数量。
分支模型的最后一步是全局最大池化,这使模型健壮性好,不会总是居中定位。
故一张384x384大小的黑白图片经过Branch Model提取特征后,其维度变化为[1,1,384,384]---->[1,512]。
# ResNet残差网络
class Bottleneck(nn.Module):
def __init__(self, in_channel, out_channel, stride):
super(Bottleneck, self).__init__()
expansion = 4
mid_channel = in_channel // expansion
self.block = nn.Sequential(
nn.Conv2d(in_channel, mid_channel, kernel_size=1, stride=1, bias=False),
nn.BatchNorm2d(mid_channel),
nn.ReLU(),
nn.Conv2d(mid_channel, mid_channel, kernel_size=3, stride=stride, padding=1, bias=False),
nn.BatchNorm2d(mid_channel),
nn.ReLU(),
nn.Conv2d(mid_channel, out_channel, kernel_size=1, stride=1, bias=False),
nn.BatchNorm2d(out_channel)
)
if stride != 1 or in_channel != out_channel:
self.downsample = nn.Sequential(
nn.Conv2d(in_channel, out_channel, kernel_size=3, stride=stride, padding=1, bias=False),
nn.BatchNorm2d(out_channel)
)
else:
self.downsample = None
self.relu = nn.ReLU(inplace=True)
def forward(self, input):
output = self.block(input)
if self.downsample == None:
residual = input
else:
residual = self.downsample(input)
output = self.relu(output + residual)
return output
##############
# BRANCH MODEL
##############
class Branch_Model(nn.Module):
def __init__(self):
super(Branch_Model, self).__init__()
self.module_list: torch.nn.ModuleList = nn.ModuleList()
self.module_list.add_module('block1', nn.Sequential( # [1,1,384,384]--->[1,64,96,96]
nn.Conv2d(1, 64, kernel_size=9, stride=2, padding=4, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.BatchNorm2d(64),
))
self.module_list.add_module('block2', nn.Sequential( # [1,64,96,96]--->[1,64,48,48]
nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.BatchNorm2d(64)
))
self.inplanes = 64
self.module_list.add_module('block3', nn.Sequential( # [1,64,48,48]--->[1,128,24,24]
self._make_layer(Bottleneck, 128, 4),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.BatchNorm2d(self.inplanes)
))
self.module_list.add_module('block4', nn.Sequential( # [1,128,24,24]--->[1,256,12,12]
self._make_layer(Bottleneck, 256, 4),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.BatchNorm2d(self.inplanes)
))
self.module_list.add_module('block5', nn.Sequential( # [1,256,12,12]--->[1,384,6,6]
self._make_layer(Bottleneck, 384, 4),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.BatchNorm2d(self.inplanes)
))
self.module_list.add_module('block6', nn.Sequential( # [1,384,6,6]--->[1,512,6,6]
self._make_layer(Bottleneck, 512, 4),
))
self.module_list.add_module('global_pool', nn.Sequential( # [1,512,6,6]--->[1,512,1,1]
nn.AdaptiveAvgPool2d(output_size=1)
))
def forward(self, input):
out = input
for index, module in enumerate(self.module_list):
out = module(out)
return out.view(input.size(0), -1)
def _make_layer(self, block, planes, blocks, stride=1):
layers = [block(self.inplanes, planes, stride)]
self.inplanes = planes
for i in range(1, blocks):
layers.append(block(self.inplanes, planes, stride=1))
return nn.Sequential(*layers)
Head model
head model用于比较branch model提取出的特征向量,以确定两张图片是相同种类还是不同种类的鲸鱼。典型的方法是使用距离度量(例如L1距离),但是有一些理由尝试不同的方法:
一、距离度量将距离为零的两个特征视为完美匹配,而数值较大但略有不同的两个特征将被视为好,但由于它们不完全相等故不太理想。不过,我仍然认为特征中的正信号要比负信号要多,尤其是ReLU激活时,距离测量失去了这一概念。
ps:作者对上述的解释:举个例子。 假设我们正在搭建一个用于人脸识别的模型,并且该模型的功能之一是检测目标对象的鼻尖是否有疣。 拍摄两张照片且两张照片上的鼻尖都有疣,那么两张图片来自于同一个人有多大可能性? 现在,拍摄两张照片且两张照片上的鼻尖都没有疣,在这种情况下成为同一个人的可能性有多大?个人理解是正信号的特征能够大大增加概率,而负信号的特征对概率影响较小。
二、距离度量不提供特征负相关。考虑一种情况,如果两幅图像都具有特征X,则它们必须是同一类鲸鱼。除非它们都具有特征Y,在这种情况下特征X也不那么清晰。
三、同时,存在一个隐含的假设,即交换两个图像必须产生相同的结果:如果A与B是同一类鲸鱼,则B必须与A在同一类鲸鱼。
为了解决上述问题,按如下进行:
1、对于每个特征,计算总和,乘积,绝对值和差平方x + y、xy、| x-y |、(x-y)^2。
2、这四个值通过一个小型神经网络传递,该网络可以学习如何在匹配的零和接近的非零值之间权衡。具有相同权重的神经网络用于每个特征。
3、输出是经过转换的要素的加权总和,且使用sigmoid激活函数。权重的值是多余的,因为权重只是要素的缩放比例,可以通过另一层来学习。然而允许负权重存在,但使用ReLU激活时无法产生负权重。
############
# HEAD MODEL
############
class Head_Model(nn.Module):
def __init__(self):
super(Head_Model, self).__init__()
self.mid = 32
self.layer1 = nn.Sequential(
nn.Conv2d(1, self.mid, kernel_size=(4, 1)), # [m,1,4,512]--->[m,32,1,512]
nn.ReLU(inplace=True),
)
self.layer2 = nn.Sequential(
nn.Conv2d(1, 1, kernel_size=(1, self.mid)), # [m,1,512,32]--->[m,1,512,1]
)
self.fc = nn.Sequential(
nn.Linear(in_features=512, out_features=1),
nn.Sigmoid()
)
def forward(self, xa_feature: torch.Tensor, xb_feature: torch.Tensor):
sample_num = xa_feature.size(0)
x1 = xa_feature * xb_feature
x2 = xa_feature + xb_feature
x3 = torch.abs(xa_feature - xb_feature)
x4 = torch.pow(xa_feature - xb_feature, 2)
feature = torch.cat([x1, x2, x3, x4], dim=1) # [m,2048]
feature = feature.view(sample_num, 1, 4, xa_feature.size(1)) # [m,1,4,512]
out = self.layer1(feature) # [m,32,1,512]
out = out.view(xa_feature.size(0), 1, -1, self.mid) # [m,1,512,32]
out = self.layer2(out) # [m,1,512,1]
out = out.view(xa_feature.size(0), -1)
out = self.fc(out)
return out
SNN
最终的模型由branch model和head model一同组成,计算输入两幅图像的相似度。
# Siamese Neural Network
# 孪生网络,将两张输入图像feed到两个网络,通过loss评价两个输入的相似程度
class SNN(nn.Module):
'''
通过将测试集的每张图片与训练集的每张图片做对比,判断是否来自于同一条鲸
通过对图片进行排序找到最可能的鲸鱼
'''
def __init__(self):
super(SNN, self).__init__()
self.branch_model = Branch_Model() # 提取输入图片的特征,时间复杂度为 O(n)
self.head_model = Head_Model() # 比较提取出的特征,判断两张图片是否匹配,计算复杂度为O(n*(n-1)/2)
def forward(self, img_a, img_b):
'''
:param img_a: [N,1,384,384]
:param img_b: [N,1,384,384]
:return:
'''
##############
# BRANCH MODEL 提取图片特征
##############
feature_a = self.branch_model(img_a)
feature_b = self.branch_model(img_b)
##############
# HEAD MODEL 根据特征比较相似性
##############
out = self.head_model(feature_a, feature_b)
return out