李宏毅《机器学习》总结 - 2022 HW8(Anomaly Detection、ResNet) Strong Baseline

重新学习了一下 ResNet。。这作业平均一跑就是3、4个小时
题目大意是让你做异常检测(anomaly detection),即给你一些正常的图片,再让你测试图片是正常的还是异常的(可以理解为 2 分类问题,只不过其中一个类别是无限大的)
image

代码:https://www.kaggle.com/code/skyrainwind/hw8-anomaly-detection

题目分析

调一下 fcn 的结构(比如加一层),调一下训练次数就能过 medium
再把 fcn 换成 cnn,发现 performance 并没有明显提高,考虑再换成 resnet,就达到 strong baseline 了,其中因为 decoder 没调好还多 train 了一次

代码分析

anomaly detection 的基本思想就是训练一个 encoder,一个 decoder,使得图片等输入到 encoder(encoder 的输出一般是一个比较小的向量,直观来说就是降维、选出了关键影响因素) 和 decoder 之后,与原输入的差距越小越好。
在过 medium 的 fcn 中,我加了一层 fcn,并调整 encoder 的输出为一个 10 维向量(而非 3 维):

class fcn_autoencoder(nn.Module):
    def __init__(self):
        super(fcn_autoencoder, self).__init__()
        self.encoder = nn.Sequential(
            nn.Linear(64 * 64 * 3, 1024),
            nn.ReLU(),
            nn.Linear(1024, 512),
            nn.ReLU(),
            nn.Linear(512, 256),
            nn.ReLU(), 
            nn.Linear(256, 64), 
            nn.ReLU(), 
            nn.Linear(64, 10)
        )
        self.decoder = nn.Sequential(
            nn.Linear(10, 64),
            nn.ReLU(), 
            nn.Linear(64, 256),
            nn.ReLU(),
            nn.Linear(256, 512),
            nn.ReLU(),
            nn.Linear(512, 1024),
            nn.ReLU(), 
            nn.Linear(1024, 64 * 64 * 3), 
            nn.Tanh()
        )

resnet(深度残差网络)在图像处理和 transformer 等领域都有着广泛应用,其核心是每一个 Residual_block 中的最后输出都加上输入,这个过程也叫 shortcut。而每个 Residual_block 都由好几个卷积层构成,这样可以有效避免产生网络退化的问题。在 transformer 中,encoder 和 decoder 都有在 attention 层之后的 add&norm 过程,这个 add 就是 shortcut 的过程。
在代码上的实现,就可以先实现一个 residual_block 包含几个 cnn 层,并加上 shortcut(值得注意的是,输出和对应的输入可能大小不同,需要额外 downsample 一下),在大的 resnet 中包含若干个小的 residual_block 作为 encoder 层,类似于一个“压缩”的过程,decoder 就是直接卷积层实现,注意一下最后直接连激活函数 tanh 即可,不用再 batch normalization 了!
卷积层的参数也要调一调,调的结果就是卷积层让图片长宽缩小一半,反卷积层扩大一倍。

class Residual_block(nn.Module):
    def __init__(self, ic, oc, stride=1): # 当传入 stride = 2 时,会把图片长宽缩小一倍
        super().__init__()
        self.conv1 = nn.Sequential(
            nn.Conv2d(ic, oc, stride=stride, padding=1, kernel_size=3), # stride=2: (H,W) -> (H/2, W/2)
            nn.BatchNorm2d(oc),
            nn.ReLU(inplace=True)
        )
        self.conv2 = nn.Sequential(
            nn.Conv2d(oc, oc, stride=1, padding=1, kernel_size=3), # (H,W) -> (H,W)
            nn.BatchNorm2d(oc)
        )
        
        self.downsample = None # 让原来的 x 变成能和 forward(x) 相加的形状,包括 channel 和 (H,W) 都应相同
        if((stride != 1)  or (ic != oc)): # stride != 1 -> (H,W) 变小 ic != oc -> channel 不同
            self.downsample = nn.Sequential(
                nn.Conv2d(ic, oc, stride=stride, kernel_size=1),
                nn.BatchNorm2d(oc)
            )
            
    def forward(self, x):
        residual = x
        x = self.conv1(x)
        x = self.conv2(x)
        
        if(self.downsample != None):
            residual = self.downsample(residual)
        
        x = x + residual
        x = nn.ReLU(inplace = True)(x)
        return x

class ResNet(nn.Module):
    def __init__(self, block=Residual_block, num_layers = [2, 1, 1, 1]):
        super().__init__()
        self.preconv = nn.Sequential( # 3*64*64 --> 32*64*64
            nn.Conv2d(3, 32, kernel_size=3, padding=1, stride=1, bias=False),
            nn.BatchNorm2d(32),
            nn.ReLU(inplace=True)
        )
        
        def make_residual(block, ic, oc, num_layer, stride=1):
            layers = []
            layers.append(block(ic, oc, stride))
            for i in range(num_layer-1):
                layers.append(block(oc, oc))
            return nn.Sequential(*layers)

        self.layer0 = make_residual(block, ic=32, oc=64, num_layer=num_layers[0], stride=2)
        self.layer1 = make_residual(block, ic=64, oc=128, num_layer=num_layers[1], stride=2)
        self.layer2 = make_residual(block, ic=128, oc=128, num_layer=num_layers[2], stride=2)
        self.layer3 = make_residual(block, ic=128, oc=64, num_layer=num_layers[3], stride=2) 

        self.fc = nn.Sequential(
            nn.Flatten(), # 也可以用 .view(shape[0], -1)
            nn.Dropout(0.2),
            nn.Linear(64*4*4, 64),
            nn.BatchNorm1d(64),
            nn.ReLU(inplace = True)
        )
        
        # 关于 ConvTranspose2d 的介绍:https://blog.csdn.net/qq_36201400/article/details/112604740
        # 大概就是在原图相邻的两个格子之间插 stride-1 个 0(这样原图就会变大了),padding <- kernel-padding-1
        self.decoder = nn.Sequential(
            nn.Linear(64, 64*4*4), # (64) -> (64*4*4)
            nn.BatchNorm1d(64*4*4),
            nn.ReLU(),
            nn.Unflatten(1, (64, 4, 4)), # (64*4*4) -> (64, 4, 4)
            nn.ConvTranspose2d(64,128,kernel_size=4,stride=2,padding=1), # (64,4,4) -> (128,8,8)
            nn.BatchNorm2d(128),
            nn.ReLU(),
            nn.ConvTranspose2d(128,128,kernel_size=4,stride=2,padding=1), # (128,8,8) -> (128,16,16)
            nn.BatchNorm2d(128),
            nn.ReLU(),
            nn.ConvTranspose2d(128,128,kernel_size=4,stride=2,padding=1), # (128,16,16) -> (128,32,32)
            nn.BatchNorm2d(128),
            nn.ReLU(),
            nn.ConvTranspose2d(128,3,kernel_size=4,stride=2,padding=1), # (128,32,32) -> (3,64,64)
            nn.Tanh()
        )
    
    def encoder(self, x):
        x = self.preconv(x) # (3,64,64) -> (32,64,64)
        x = self.layer0(x) # (32,64,64) -> (64,32,32) 且通过 resnet(shortcut) 实现,下同
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x) # (128,8,8) -> (64,4,4)
        x = self.fc(x) # (64,4,4) -> (64*4*4) -> (64)
        return x
        
    def forward(self, x): # x : (3, 64, 64)
        x = self.encoder(x)
        x = self.decoder(x)
        return x
posted @ 2024-02-20 15:57  SkyRainWind  阅读(124)  评论(0编辑  收藏  举报