ResNet ネットワーク分析と実際のケース

ネットワークが深いほど、より多くの情報が得られ、機能が豊富になります。しかし実際には、ネットワークが深くなるにつれて最適化効果が悪化し、テスト データとトレーニング データの精度が低下します。下の図に示すように、56 層と 20 層のニューラル ネットワークは、トレーニング エラーとテスト エラーを比較します.
ここに画像の説明を挿入
深いネットワーク劣化の問題を考慮して、He Yuming らは、ImageNet 画像の残差ネットワーク (ResNet) を提案しました。 2015 年の認識チャレンジで優勝し、その後のディープ ニューラル ネットワークの設計に大きな影響を与えました。

1 残りのブロック

F(x) が 2 つのレイヤーのみを持つマッピング関数を表し、x が入力で、F(x) が出力であるとします。それらが同じ寸法であると仮定します。トレーニング プロセス中に、ネットワーク内の w と b を変更することにより、理想的な H(x) (入力から出力への理想的なマッピング関数) を適合させたいと考えています。つまり、目標は、予測値 F(x) の w と b を変更して、実際の値 H(x) に近づけることです。考え方を変えて、F(x) を使用して H(x)-x を近似すると、最終的な出力は F(x)+x になります (ここでの加算とは、対応する位置での要素の加算を指し、また、要素ごとの加算であり、入力から出力に直接接続された構造はショートカットとも呼ばれ、構造全体が ResNet の基本モジュールである剰余ブロックです。

ここに画像の説明を挿入

ネットワーク構造が下の図のようであると仮定すると、19 層に短い接続を追加し、20 層と 21 層をバイパスします. ネットワークのトレーニング プロセス中に、20 層と 21 層の効果が良くない場合は、対応する重みパラメータは、反復トレーニング中に小さくなり続けるか、直接ゼロに設定されることさえあります。つまり、ネットワークはこの時点で 20 番目と 21 番目の層を放棄し、短い接続を介してそれらを直接バイパスします。 20 番目と 21 番目のレイヤーは非常に優れているため、対応する重みパラメーターは反復トレーニング中に増加し続けます。つまり、20 番目と 21 番目のレイヤーに注意を払うか、これらの 2 つのレイヤーを破棄するかをネットワークに選択させます。そのため、He Kaiming はかつて次のように述べています。実際のシナリオでは、通常、このような残余ブロックが多数作成されます. 一部のレイヤーのパフォーマンスが低下しても問題ありません. レイヤーの1つがうまく機能する限り、元のパフォーマンスを向上させることができます.
ここに画像の説明を挿入
残余構造は主枝とショートカット枝(ショートコネクションとも呼ばれる)からなり、左半分が基本モジュール(BasicBlock)、右半分がボトルネックモジュール(Bottleneck)の2つの形態を持つ下図の部分。

ここに画像の説明を挿入
ResNet18/34 (左図) と ResNet50/101/152 (右図) にそれぞれこの 2 つの構造を使用しています. 右図のボトルネック モジュールはパラメータの量を減らすことです.

ボトルネック モジュールでは、最初の 1x1 畳み込みでチャネル数が 1/4 に減少し、2 番目の 1x1 畳み込みでチャネル数が 4 倍に増加します. 公式の pytorch コード実装では、この関数を実現するためにハイパーパラメータの展開 = 4 を設定します。
ここに画像の説明を挿入

pytorch 公式 Web サイトでの resnet の実装場所は、anaconda\envs\Lib\site-packages\torchvision\models\resnet.py です。

ボトルネック モジュールは、2 つの 3x3 畳み込み層を 1x1 + 3x3 + 1x1 に置き換えます. 新しい構造の中間の 3x3 畳み込み層は、最初に次元削減 1x1 畳み込み層で計算を削減し、次に別の 1x1 畳み込み層で計算を削減します. 下層は復元されました. 最初の 1x1 畳み込みで 256 次元のチャネルを 64 次元に縮小し、最後に 1x1 畳み込みを介して元に戻します. 全体として使用されるパラメータの数: 1x1x256x64 + 3x3x64x64 + 1x1x64x256 = 69632, ボトルネックを使用しない場合、2 3x3x256 Convolution は、下図のように、この時のパラメータ数は 3x3x256x256x2 = 1179648 となり、前者の 16.94 倍になります。
ここに画像の説明を挿入

ネットワークの 2 つの異なるレイヤー

一般的な resnet 構造には、主に resnet18、34、50、101、および 152 が含まれます。次の表は、それらの特定の構造を示しています。最初に表
ここに画像の説明を挿入
の左端を見てください。すべてのネットワークは 5 つの部分、つまり、conv1、conv2_x、conv3_x、conv4_x に分割されています。 、conv5_x、たとえば: 101-layer は 101-layer ネットワークを指し、最初に入力 7x7x64 との畳み込みがあり、次に 3 + 4 + 23 + 3 = 33 残余ブロックの後、各残余ブロックは 3 レイヤーなので、は 33 x 3 = 99 層であり、最後に全結合層 (分類用) があるため、1 + 99 + 1 = 101 層、合計 101 層のネットワーク; 2 つの 50 層と101 層の列、両者の違いは conv4_x だけで、ResNet50 は 6 ブロック、ResNet101 は 23 ブロックであることがわかります。この 2 つの間には 17 ブロックの違い、つまり 17 x 3 = 51 層があります。

注: ネットワーク層は畳み込み層または全結合層のみを指し、活性化層またはプーリング層はカウントされません。

以下の図 1 は図中のシンボルの説明であり、図 2 は resnet18 と resnet34 の構造図です。
ここに画像の説明を挿入
ここに画像の説明を挿入

表では、すべての残差構造が 4 つのモジュール、つまり conv2_x、conv3_x、conv4_x、conv5_x に分割されています (上の図では、異なるモジュールを示すために異なる色が使用されています)。このうち、conv3_x、conv4_x、conv5_x の 3 つのモジュールの残差構造の最初の層は、入力された特徴マップの 2 倍のダウンサンプリングを実行し、チャネルを次の層の残差構造が必要とするチャネルに調整します。ショートカット ブランチ (図の点線) では、ショートカット ブランチの出力とメイン ブランチの出力が同じサイズになるように、ダウンサンプリングとチャネル調整に 1x1 畳み込みが使用されます。直接追加できます。conv2_x モジュールの場合、ダウンサンプリングは実行されません (ストライド 2 の最大プーリング層が conv2_x モジュールの前に使用されるため、モジュールは高さと幅を縮小する必要がないため)。 101/
152 たとえば、conv2_x モジュールの第 1 層の残差ブロックのショートカット ブランチも 1x1 畳み込みを使用します. その機能は、チャネル数を (ダウンサンプリングなしで) 調整することです。ショートカットブランチは直接関連しています。残差構造のショートカット分岐の入力サイズは [56, 56, 64]、出力サイズは [56, 56, 256]、主分岐の出力サイズも [56, 56, 256] です。直接追加できます。

上記のブランチによると、ショートカット ブランチには 2 つのタイプがあります. 1 つ目は入力と出力が等しい (同一性マッピング) ことであり、これは実線で表され、2 つ目は入力が 1x1 の畳み込み層を通過することです.出力は点線で表されます。

3 ネットワーク構築

pytorch 公式 Web サイトでの resnet の実装場所は、anaconda\envs\Lib\site-packages\torchvision\models\resnet.py です。

3.1 基本的な残差ブロックの構築

ResNet18/34 ネットワークの構築に必要な残余ブロック: 最初のモジュール (つまり conv2_x) の最初の残余ブロック (左半分) と 2 番目のモジュール (つまり conv3_x) の最初の残余ブロックを下の図に示します。残りのブロック (右半分) の。左半分の短い接続は実線で示され、この残差ブロックの入力チャネルは出力チャネルと同じですが、右半分の短い接続は実線で示され、この残差の出力チャネルはブロックは入力チャネルの 2 倍です。
ここに画像の説明を挿入

主に BasicBlock クラスを介して実装されます。コードは次のとおりです。

class BasicBlock(nn.Module):
    expansion = 1

    def __init__(self, in_channel, out_channel, stride=1, downsample=None, **kwargs):
        super(BasicBlock, self).__init__()
        self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,
                               kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channel)
        self.relu = nn.ReLU()
        self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel,
                               kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channel)
        self.downsample = downsample

    def forward(self, x):
        identity = x
        if self.downsample is not None:
            identity = self.downsample(x)

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)

        out += identity
        out = self.relu(out)

        return out

3.2 ボトルネックモジュールの構築

ボトルネック モジュールは、多くの層を持つ resnet50/101/152 ネットワークを構築するために使用されます。最初のモジュール (つまり、conv2_x) 内の最初の残りのブロック (つまり、conv2_x) と 2 番目のモジュール (つまり、conv2_x) を下の図に示します。 conv3_x の最初の残差ブロック (右半分) の構造)。左半分の短い接続は実線で示され、この残差ブロックの入力チャネルは出力チャネルと同じですが、右半分の短い接続は実線で示され、この残差の出力チャネルはブロックは入力チャネルの 2 倍です。

ここに画像の説明を挿入
ここに画像の説明を挿入

これはクラス Bottleneck によって実現され、コードは次のようになります。

class Bottleneck(nn.Module):
    """
    注意:原论文中,在虚线残差结构的主分支上,第一个1x1卷积层的步距是2,第二个3x3卷积层步距是1。
    但在pytorch官方实现过程中是第一个1x1卷积层的步距是1,第二个3x3卷积层步距是2,
    这么做的好处是能够在top1上提升大概0.5%的准确率。
    可参考Resnet v1.5 https://ngc.nvidia.com/catalog/model-scripts/nvidia:resnet_50_v1_5_for_pytorch
    """
    expansion = 4

    def __init__(self, in_channel, out_channel, stride=1, downsample=None):
        super(Bottleneck, self).__init__()

        self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,
                               kernel_size=1, stride=1, bias=False)  # squeeze channels
        self.bn1 = nn.BatchNorm2d(out_channel)
        # -----------------------------------------
        self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel,
                               kernel_size=3, stride=stride, bias=False, padding=1)
        self.bn2 = nn.BatchNorm2d(out_channel)
        # -----------------------------------------
        self.conv3 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel * self.expansion,
                               kernel_size=1, stride=1, bias=False)  # unsqueeze channels
        self.bn3 = nn.BatchNorm2d(out_channel * self.expansion)
        self.relu = nn.ReLU(inplace=True)
        self.downsample = downsample

    def forward(self, x):
        identity = x
        if self.downsample is not None:
            identity = self.downsample(x)

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu(out)

        out = self.conv3(out)
        out = self.bn3(out)

        out += identity
        out = self.relu(out)

        return out

3.3 Resnet ネットワークの構築

上記で作成したresidualモジュールを介してresnetネットワークを構築し、

ここに画像の説明を挿入

コードは次のようになります。

class ResNet(nn.Module):

    def __init__(self,
                 block,  # 残差块的选择:如果定义ResNet18/34时,就选择基础模块(BasicBlock),如果定义ResNet50/101/152,就使用瓶颈模块(Bottleneck)
                 blocks_num,  # 定义所使用的残差块的数量,它是一个列表参数
                 num_classes=1000,  # 网络的分类个数
                 ):
        super(ResNet, self).__init__()
        self.in_channel = 64
        self.conv1 = nn.Conv2d(3, self.in_channel, kernel_size=7, stride=2,
                               padding=3, bias=False)
        self.bn1 = nn.BatchNorm2d(self.in_channel)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        # 下面的 self.layer1,self.layer2,self.layer3,self.layer4分别是不同的模块,即对应着上面表格中的conv1,conv2_x,conv3_x,conv4_x,conv5_x中的残差结构
        self.layer1 = self._make_layer(block, 64, blocks_num[0])
        self.layer2 = self._make_layer(block, 128, blocks_num[1], stride=2)
        self.layer3 = self._make_layer(block, 256, blocks_num[2], stride=2)
        self.layer4 = self._make_layer(block, 512, blocks_num[3], stride=2)

        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))  # 自适应平均池化(即全局平均池化),它会将输入特征图池化成1x1大小
        # 因为前边经过自适应平均池化后特征图大小变为1x1,并且有512 * block.expansion个通道,所以展平后的维度是512 * block.expansion,所以下面的全连接层的输入维度是512 * block.expansion
        self.fc = nn.Linear(512 * block.expansion, num_classes)

        #对卷积层的参数进行初始化
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')

    def _make_layer(self, block, channel, block_num, stride=1):
        downsample = None
        if stride != 1 or self.in_channel != channel * block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.in_channel, channel * block.expansion, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(channel * block.expansion))

        layers = []
        layers.append(block(self.in_channel,
                            channel,
                            downsample=downsample,
                            stride=stride))
        self.in_channel = channel * block.expansion

        for _ in range(1, block_num):
            layers.append(block(self.in_channel,
                                channel))

        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)


        x = self.avgpool(x) # 自适应平均池化(即全局平均池化),它会将输入特征图池化成1x1大小
        x = torch.flatten(x, 1)
        x = self.fc(x)

        return x

3.4 ネットワークのさまざまなレイヤーの構築

異なる層のネットワークを構築する場合、conv2_x、conv3_x、conv4_x、conv5_x の残差ブロック数を次の表に従って設定し、ボトルネック モジュールを使用するか、基本残差モジュールを使用するかを指定します
ここに画像の説明を挿入
。 101/152 などのネットワーク層

def resnet18(num_classes=1000):
    #预训练权重下载链接:  https://download.pytorch.org/models/resnet18-5c106cde.pth
    return ResNet(BasicBlock, [2, 2, 2, 2], num_classes=num_classes)


def resnet34(num_classes=1000):
    #预训练权重下载链接: https://download.pytorch.org/models/resnet34-333f7ec4.pth
    return ResNet(BasicBlock, [3, 4, 6, 3], num_classes=num_classes)


def resnet50(num_classes=1000):
    #预训练权重下载链接: https://download.pytorch.org/models/resnet50-19c8e357.pth
    return ResNet(Bottleneck, [3, 4, 6, 3], num_classes=num_classes)


def resnet101(num_classes=1000, include_top=True):
    #预训练权重下载链接: https://download.pytorch.org/models/resnet101-5d3b4d8f.pth
    return ResNet(Bottleneck, [3, 4, 23, 3], num_classes=num_classes)


def resnet152(num_classes=1000):
    #预训练权重下载链接: https://download.pytorch.org/models/resnet152-b121ed2d.pth
    return ResNet(Bottleneck, [3, 8, 36, 3], num_classes=num_classes)

3.5 コード全体

import torch.nn as nn
import torch


class BasicBlock(nn.Module):
    expansion = 1

    def __init__(self, in_channel, out_channel, stride=1, downsample=None, **kwargs):
        super(BasicBlock, self).__init__()
        self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,
                               kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channel)
        self.relu = nn.ReLU()
        self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel,
                               kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channel)
        self.downsample = downsample

    def forward(self, x):
        identity = x
        if self.downsample is not None:
            identity = self.downsample(x)

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)

        out += identity
        out = self.relu(out)

        return out


class Bottleneck(nn.Module):
    """
    注意:原论文中,在虚线残差结构的主分支上,第一个1x1卷积层的步距是2,第二个3x3卷积层步距是1。
    但在pytorch官方实现过程中是第一个1x1卷积层的步距是1,第二个3x3卷积层步距是2,
    这么做的好处是能够在top1上提升大概0.5%的准确率。
    可参考Resnet v1.5 https://ngc.nvidia.com/catalog/model-scripts/nvidia:resnet_50_v1_5_for_pytorch
    """
    expansion = 4

    def __init__(self, in_channel, out_channel, stride=1, downsample=None):
        super(Bottleneck, self).__init__()

        self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,
                               kernel_size=1, stride=1, bias=False)  # squeeze channels
        self.bn1 = nn.BatchNorm2d(out_channel)
        # -----------------------------------------
        self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel,
                               kernel_size=3, stride=stride, bias=False, padding=1)
        self.bn2 = nn.BatchNorm2d(out_channel)
        # -----------------------------------------
        self.conv3 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel * self.expansion,
                               kernel_size=1, stride=1, bias=False)  # unsqueeze channels
        self.bn3 = nn.BatchNorm2d(out_channel * self.expansion)
        self.relu = nn.ReLU(inplace=True)
        self.downsample = downsample

    def forward(self, x):
        identity = x
        if self.downsample is not None:
            identity = self.downsample(x)

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu(out)

        out = self.conv3(out)
        out = self.bn3(out)

        out += identity
        out = self.relu(out)

        return out


class ResNet(nn.Module):

    def __init__(self,
                 block,  # 残差块的选择:如果定义ResNet18/34时,就选择基础模块(BasicBlock),如果定义ResNet50/101/152,就使用瓶颈模块(Bottleneck)
                 blocks_num,  # 定义所使用的残差块的数量,它是一个列表参数
                 num_classes=1000,  # 网络的分类个数
                 ):
        super(ResNet, self).__init__()
        self.in_channel = 64
        self.conv1 = nn.Conv2d(3, self.in_channel, kernel_size=7, stride=2,
                               padding=3, bias=False)
        self.bn1 = nn.BatchNorm2d(self.in_channel)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        # 下面的 self.layer1,self.layer2,self.layer3,self.layer4分别是不同的模块,即对应着上面表格中的conv1,conv2_x,conv3_x,conv4_x,conv5_x中的残差结构
        self.layer1 = self._make_layer(block, 64, blocks_num[0])
        self.layer2 = self._make_layer(block, 128, blocks_num[1], stride=2)
        self.layer3 = self._make_layer(block, 256, blocks_num[2], stride=2)
        self.layer4 = self._make_layer(block, 512, blocks_num[3], stride=2)

        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))  # 自适应平均池化(即全局平均池化),它会将输入特征图池化成1x1大小
        # 因为前边经过自适应平均池化后特征图大小变为1x1,并且有512 * block.expansion个通道,所以展平后的维度是512 * block.expansion,所以下面的全连接层的输入维度是512 * block.expansion
        self.fc = nn.Linear(512 * block.expansion, num_classes)

        #对卷积层的参数进行初始化
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')

    def _make_layer(self, block, channel, block_num, stride=1):
        downsample = None
        if stride != 1 or self.in_channel != channel * block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.in_channel, channel * block.expansion, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(channel * block.expansion))

        layers = []
        layers.append(block(self.in_channel,
                            channel,
                            downsample=downsample,
                            stride=stride))
        self.in_channel = channel * block.expansion

        for _ in range(1, block_num):
            layers.append(block(self.in_channel,
                                channel))

        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)


        x = self.avgpool(x) # 自适应平均池化(即全局平均池化),它会将输入特征图池化成1x1大小
        x = torch.flatten(x, 1)
        x = self.fc(x)

        return x


def resnet18(num_classes=1000):
    #预训练权重下载链接:  https://download.pytorch.org/models/resnet18-5c106cde.pth
    return ResNet(BasicBlock, [2, 2, 2, 2], num_classes=num_classes)


def resnet34(num_classes=1000):
    #预训练权重下载链接: https://download.pytorch.org/models/resnet34-333f7ec4.pth
    return ResNet(BasicBlock, [3, 4, 6, 3], num_classes=num_classes)


def resnet50(num_classes=1000):
    #预训练权重下载链接: https://download.pytorch.org/models/resnet50-19c8e357.pth
    return ResNet(Bottleneck, [3, 4, 6, 3], num_classes=num_classes)


def resnet101(num_classes=1000, include_top=True):
    #预训练权重下载链接: https://download.pytorch.org/models/resnet101-5d3b4d8f.pth
    return ResNet(Bottleneck, [3, 4, 23, 3], num_classes=num_classes)


def resnet152(num_classes=1000):
    #预训练权重下载链接: https://download.pytorch.org/models/resnet152-b121ed2d.pth
    return ResNet(Bottleneck, [3, 8, 36, 3], num_classes=num_classes)




3.6 pytorch によって公式にパッケージ化された Resnet コード

実際、resnet コードは公式に pytorch にパッケージ化されており、呼び出すのに必要なコードは 1 行だけで、自分でモデルを構築する必要はありません。

resnet18

  • resnet18 の事前トレーニング済みモデルをインポートする
    import torchvision
    model = torchvision.models.resnet18(pretrained=True)
  • resnet18 ネットワーク構造のみが必要で、事前トレーニング済みモデルのパラメーターで初期化する必要がない場合は、
    model = torchvision.models.resnet50(pretrained=False)

深刻な50

  • resnet50 の事前トレーニング済みモデルをインポートする
    import torchvision
    model = torchvision.models.resnet50(pretrained=True)
  • resnet50 ネットワーク構造のみが必要で、事前トレーニング済みモデルのパラメーターで初期化する必要がない場合は、
    model = torchvision.models.resnet50(pretrained=False)

resnet101

  • resnet101 の事前トレーニング済みモデルをインポートする
    import torchvision
    model = torchvision.models.resnet101(pretrained=True)
  • resnet50 ネットワーク構造のみが必要で、事前トレーニング済みモデルのパラメーターで初期化する必要がない場合は、
    model = torchvision.models.resnet101(pretrained=False)

resnet152

  • resnet152 の事前トレーニング済みモデルをインポートする
    import torchvision
    model = torchvision.models.resnet152(pretrained=True)
  • resnet50 ネットワーク構造のみが必要で、事前トレーニング済みモデルのパラメーターで初期化する必要がない場合は、
    model = torchvision.models.resnet152(pretrained=False)

4 2 つの分類のために Resnet-18 を微調整する

次に、Imagenet で事前トレーニング済みの Resnet-18 を使用して、バイナリ分類モデルの事前
トレーニング重みダウンロード アドレスの微調整 (微調整) を実行します: https://download.pytorch.org/models/resnet18-5c106cde.pth

アリとミツバチの二値分類データセットには以下が含まれます:
トレーニング セット: 各 120 ~ 個 検証セット: 各 70 ~ 個
各カテゴリの写真は別のフォルダーに格納され、フォルダー名はラベル名です。ここにあるデータの量は非常に少ないため、モデル
データセットのダウンロード アドレスを微調整することしかできません: https://download.pytorch.org/tutorial/hymenoptera_data.zip

ここでの微調整の部分は、最後の完全に接続されたレイヤーで、元の 1000 個のニューロンを 2 個のニューロンに変更し、
ここに画像の説明を挿入
バイナリ分類プロジェクトのすべてのコード、データセット、およびモデルの重みを私の github ウェアハウスに配置しました。https://github.com/mojieok/classification

  • 全体の配置
    ここに画像の説明を挿入
  • torch.utils.data.Dataset クラスを継承する AntsDataset クラスをカスタマイズします。その場所は tools/my_dataset ファイルにあります。
import numpy as np
import torch
import os
import random
from PIL import Image
from torch.utils.data import Dataset

class AntsDataset(Dataset):
    def __init__(self, data_dir, transform=None):
        #每个类别的图片分别存放在不同的文件夹中,并且该文件夹名就是标签名
        self.label_name = {"ants": 0, "bees": 1}#获取标签名称
        self.data_info = self.get_img_info(data_dir)#data_info是一个List,里边存放图片的位置以及标签
        self.transform = transform

    def __getitem__(self, index):
        path_img, label = self.data_info[index]
        img = Image.open(path_img).convert('RGB')

        if self.transform is not None:
            img = self.transform(img)

        return img, label

    def __len__(self):
        return len(self.data_info)#返回数据集的样本总数

    def get_img_info(self, data_dir):#data_dir是数据所在文件夹
        data_info = list()
        for root, dirs, _ in os.walk(data_dir):#遍历数据所在文件夹
            # 遍历类别
            for sub_dir in dirs:
                img_names = os.listdir(os.path.join(root, sub_dir))
                img_names = list(filter(lambda x: x.endswith('.jpg'), img_names))

                # 遍历图片
                for i in range(len(img_names)):
                    img_name = img_names[i]
                    path_img = os.path.join(root, sub_dir, img_name)
                    label = self.label_name[sub_dir]
                    data_info.append((path_img, int(label)))

        if len(data_info) == 0:
            #判断data_dir文件夹中是否有图片,如果没有就抛出异常
            raise Exception("\ndata_dir:{} is a empty dir! Please checkout your path to images!".format(data_dir))
        return data_info


  • 乱数シード関数の実装、その場所は tools/common_tools ファイルにあります
import torch
import random
import numpy as np
def set_seed(seed=1):
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)
  • モデルトレーニング、その場所は ./finetune_resnet18
import os
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
import torchvision.transforms as transforms
import torch.optim as optim
from matplotlib import pyplot as plt
from tools.my_dataset import AntsDataset
from tools.common_tools import set_seed
import torchvision.models as models
import torchvision
BASEDIR = os.path.dirname(os.path.abspath(__file__))
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("use device :{}".format(device))

set_seed(1)  # 设置随机种子
label_name = {"ants": 0, "bees": 1}

# 参数设置
MAX_EPOCH = 25
BATCH_SIZE = 16
LR = 0.001
log_interval = 10
val_interval = 1
classes = 2
start_epoch = -1
lr_decay_step = 7


# ============================ step 1/5 数据 ============================
data_dir = os.path.join(BASEDIR, "data")
train_dir = os.path.join(data_dir, "train")
valid_dir = os.path.join(data_dir, "val")

norm_mean = [0.485, 0.456, 0.406]
norm_std = [0.229, 0.224, 0.225]

train_transform = transforms.Compose([
    transforms.RandomResizedCrop(224),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize(norm_mean, norm_std),
])

valid_transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(norm_mean, norm_std),
])

# 构建MyDataset实例
train_data = AntsDataset(data_dir=train_dir, transform=train_transform)
valid_data = AntsDataset(data_dir=valid_dir, transform=valid_transform)

# 构建DataLoder
train_loader = DataLoader(dataset=train_data, batch_size=BATCH_SIZE, shuffle=True)
valid_loader = DataLoader(dataset=valid_data, batch_size=BATCH_SIZE)

# ============================ step 2/5 模型 ============================

# 1/3 构建模型
resnet18_ft = models.resnet18()#通过torchvision.models构建预训练模型resnet18

# 2/3 加载参数
# flag = 0
flag = 1
if flag:
    path_pretrained_model = os.path.join(BASEDIR,  "data/resnet18-5c106cde.pth")
    state_dict_load = torch.load(path_pretrained_model)
    resnet18_ft.load_state_dict(state_dict_load)#将参数加载到模型中

# 法1 : 冻结卷积层(它适用于当前任务数据量比较小,不足以训练卷积层,而只对最后的全连接层进行训练)
flag_m1 = 1
# flag_m1 = 1
if flag_m1:
    for param in resnet18_ft.parameters():
        param.requires_grad = False
    # 打印第一个卷积层的卷积核参数,由输出结果可知,因为冻结了卷积层的参数,所以每次迭代时打印的卷积层的参数都不发生变化
   # print("conv1.weights[0, 0, ...]:\n {}".format(resnet18_ft.conv1.weight[0, 0, ...]))


# 3/3 替换fc层
num_ftrs = resnet18_ft.fc.in_features#首先需要获取原模型的最后的全连接层的输入大小
resnet18_ft.fc = nn.Linear(num_ftrs, classes)#然后使用自己定义好的全连接层替换原来的输出层(即最后的全连接层),因为当前任务是2分类,所以classes=2


resnet18_ft.to(device)
# ============================ step 3/5 损失函数 ============================
criterion = nn.CrossEntropyLoss()                                                   # 选择损失函数

# ============================ step 4/5 优化器 ============================
# 法2 : 给卷积层设置较小的学习率
# flag = 1
flag = 0
if flag:
    #获取最后的全连接层的参数地址,将它们存储成列表的形式,列表中的每个元素对应着每个参数的地址
    fc_params_id = list(map(id, resnet18_ft.fc.parameters()))     # 返回的是parameters的 内存地址
    #过滤掉resnet18中最后的全连接层的参数
    base_params = filter(lambda p: id(p) not in fc_params_id, resnet18_ft.parameters())
    #通过上面两行代码,我们就可以分别获取resnet18的卷积层和全连接层,然后对这两部分分别设置不同的学习率
    optimizer = optim.SGD([
        {'params': base_params, 'lr': LR*0.1},   #卷积层设置的学习率是原始学习率的0.1倍
        #{'params': base_params, 'lr': LR * 0},  #也可以将卷积层的学习率设置为0,这样就相当于固定卷积层不训练
        {'params': resnet18_ft.fc.parameters(), 'lr': LR}], momentum=0.9)

else:
    optimizer = optim.SGD(resnet18_ft.parameters(), lr=LR, momentum=0.9)               # 选择优化器

scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=lr_decay_step, gamma=0.1)     # 设置学习率下降策略


# ============================ step 5/5 训练 ============================
train_curve = list()
valid_curve = list()

for epoch in range(start_epoch + 1, MAX_EPOCH):

    loss_mean = 0.
    correct = 0.
    total = 0.

    resnet18_ft.train()
    for i, data in enumerate(train_loader):

        # forward
        inputs, labels = data
        inputs, labels = inputs.to(device), labels.to(device)
        outputs = resnet18_ft(inputs)

        # backward
        optimizer.zero_grad()
        loss = criterion(outputs, labels)
        loss.backward()

        # update weights
        optimizer.step()

        # 统计分类情况
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).squeeze().cpu().sum().numpy()

        # 打印训练信息
        loss_mean += loss.item()
        train_curve.append(loss.item())
        if (i+1) % log_interval == 0:
            loss_mean = loss_mean / log_interval
            print("Training:Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format(
                epoch, MAX_EPOCH, i+1, len(train_loader), loss_mean, correct / total))
            loss_mean = 0.

            # if flag_m1:
            #print("epoch:{} conv1.weights[0, 0, ...] :\n {}".format(epoch, resnet18_ft.conv1.weight[0, 0, ...]))

    scheduler.step()  # 更新学习率
    
    #保存模型权重
    checkpoint = {"model_state_dict": resnet18_ft.state_dict(),
                  "optimizer_state_dict": optimizer.state_dict(),
                  "epoch": epoch}
    PATH = f'./checkpoint_{epoch}_epoch.pkl'
    torch.save(checkpoint,PATH)


    # validate the model
    if (epoch+1) % val_interval == 0:

        correct_val = 0.
        total_val = 0.
        loss_val = 0.
        resnet18_ft.eval()
        with torch.no_grad():
            for j, data in enumerate(valid_loader):
                inputs, labels = data
                inputs, labels = inputs.to(device), labels.to(device)

                outputs = resnet18_ft(inputs)
                loss = criterion(outputs, labels)

                _, predicted = torch.max(outputs.data, 1)
                total_val += labels.size(0)
                correct_val += (predicted == labels).squeeze().cpu().sum().numpy()

                loss_val += loss.item()

            loss_val_mean = loss_val/len(valid_loader)
            valid_curve.append(loss_val_mean)
            print("Valid:\t Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format(
                epoch, MAX_EPOCH, j+1, len(valid_loader), loss_val_mean, correct_val / total_val))
        resnet18_ft.train()

train_x = range(len(train_curve))
train_y = train_curve

train_iters = len(train_loader)
valid_x = np.arange(1, len(valid_curve)+1) * train_iters*val_interval # 由于valid中记录的是epochloss,需要对记录点进行转换到iterations
valid_y = valid_curve

plt.plot(train_x, train_y, label='Train')
plt.plot(valid_x, valid_y, label='Valid')

plt.legend(loc='upper right')
plt.ylabel('loss value')
plt.xlabel('Iteration')
plt.show()


  • モデル予測、その場所は ./resnet_inference にあります
import os
import time
import torch.nn as nn
import torch
import torchvision.transforms as transforms
from PIL import Image
from matplotlib import pyplot as plt
import torchvision.models as models
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
device = torch.device("cpu")

# config
vis = True
# vis = False
vis_row = 4

norm_mean = [0.485, 0.456, 0.406]
norm_std = [0.229, 0.224, 0.225]

inference_transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(norm_mean, norm_std),
])

classes = ["ants", "bees"]


def img_transform(img_rgb, transform=None):
    """
    将数据转换为模型读取的形式
    :param img_rgb: PIL Image
    :param transform: torchvision.transform
    :return: tensor
    """

    if transform is None:
        raise ValueError("找不到transform!必须有transform对img进行处理")

    img_t = transform(img_rgb)
    return img_t


def get_img_name(img_dir, format="jpg"):
    """
    获取文件夹下format格式的文件名
    :param img_dir: str
    :param format: str
    :return: list
    """
    file_names = os.listdir(img_dir)
    img_names = list(filter(lambda x: x.endswith(format), file_names))

    if len(img_names) < 1:
        raise ValueError("{}下找不到{}格式数据".format(img_dir, format))
    return img_names


def get_model(m_path, vis_model=False):

    resnet18 = models.resnet18()
    num_ftrs = resnet18.fc.in_features
    resnet18.fc = nn.Linear(num_ftrs, 2)

    checkpoint = torch.load(m_path)
    resnet18.load_state_dict(checkpoint['model_state_dict'])

    if vis_model:
        from torchsummary import summary
        summary(resnet18, input_size=(3, 224, 224), device="cpu")

    return resnet18


if __name__ == "__main__":

    img_dir = os.path.join( "data/val/bees")
    model_path = "./checkpoint_14_epoch.pkl"
    time_total = 0
    img_list, img_pred = list(), list()

    # 1. data
    img_names = get_img_name(img_dir)
    num_img = len(img_names)

    # 2. model
    resnet18 = get_model(model_path, True)
    resnet18.to(device)
    resnet18.eval()#在模型预测阶段,一定要使用函数eval()将模型的状态设置为预测状态,而不是训练状态

    with torch.no_grad(): #在模型预测阶段,一定要使用 with torch.no_grad()设置模型不去计算梯度,所以就不用保存这些梯度,这样可以既提高运算速度,又节省了显存
        for idx, img_name in enumerate(img_names):

            path_img = os.path.join(img_dir, img_name)

            # step 1/4 :将图像转化为RGB格式
            img_rgb = Image.open(path_img).convert('RGB')

            # step 2/4 : 将RGB图像转化为张量的形式
            img_tensor = img_transform(img_rgb, inference_transform)
            #增加一个batch维度,将3维张量转化为4维张量
            img_tensor.unsqueeze_(0)
            img_tensor = img_tensor.to(device)

            # step 3/4 : 将张量送入模型进行运算
            time_tic = time.time()
            outputs = resnet18(img_tensor)
            time_toc = time.time()

            # step 4/4 : visualization
            _, pred_int = torch.max(outputs.data, 1)
            pred_str = classes[int(pred_int)]

            if vis:
                img_list.append(img_rgb)
                img_pred.append(pred_str)

                if (idx+1) % (vis_row*vis_row) == 0 or num_img == idx+1:
                    for i in range(len(img_list)):
                        plt.subplot(vis_row, vis_row, i+1).imshow(img_list[i])
                        plt.title("predict:{}".format(img_pred[i]))
                    plt.show()
                    plt.close()
                    img_list, img_pred = list(), list()

            time_s = time_toc-time_tic
            time_total += time_s

            print('{:d}/{:d}: {} {:.3f}s '.format(idx + 1, num_img, img_name, time_s))

    print("\ndevice:{} total time:{:.1f}s mean:{:.3f}s".
          format(device, time_total, time_total/num_img))
    if torch.cuda.is_available():
        print("GPU name:{}".format(torch.cuda.get_device_name()))

おすすめ

転載: blog.csdn.net/m0_56192771/article/details/124229267