非常详细的Kaggle实战(三)孪生网络的搭建

孪生网络

孪生网络会比较两幅图像,并判断两幅图像是来自同一类鲸鱼还是不同的鲸鱼。 通过将测试集中的每幅图像与训练集中的每幅图像进行比较,从而对图片的相似度进行匹配排序来识别最有可能的鲸鱼类别。
孪生网络由两部分组成。 卷积神经网络(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 3Block 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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容