技術共有

VGG16 は画像分類の pytorch 実装を実装し、その手順を詳細に説明します

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

VGG16 は画像分類を実装します

ここでは、CIFAR データセットを分類するために VGG-16 ネットワークを実装します。

VGG16ネットワークの紹介

序文

《大規模画像認識のための超深層畳み込みネットワーク》

ICLR2015 国際会議

VGGGオックスフォード出身です通常の幾何学的グループのグループによって提案されました(VGG の名前の由来がわかるはずです)。このネットワークは、ILSVRC 2014 での関連作業です。主な作業は、ネットワークの深さを増やすと、ネットワークの最終パフォーマンスにある程度の影響を与える可能性があることを証明することです。 VGG には VGG16 と VGG19 の 2 つの構造があります。両者に本質的な違いはありませんが、ネットワークの深さが異なります。

VGG原理

AlexNet と比較した VGG16 の改善点は次のとおりです。複数の連続した 3x3 コンボリューション カーネルを使用して、AlexNet (11x11、7x7、5x5) のより大きなコンボリューション カーネルを置き換えます。 。特定の受容野 (出力に対する入力画像の局所的なサイズ) については、複数の非線形層によりネットワークの深さが増し、より複雑な学習モードが保証されるため、スタックされた小さな畳み込みカーネルを使用する方が、大きな畳み込みカーネルを使用するよりも優れています。コストは比較的小さい (パラメータが少ない)。

簡単に言うと、VGG では、7x7 コンボリューション カーネルを置き換えるために 3 つの 3x3 コンボリューション カーネルが使用され、5*5 コンボリューション カーネルを置き換えるために 2 つの 3x3 コンボリューション カーネルが使用されます。これの主な目的は、同じ条件下で同じことを保証することです。受容野の深さが増し、ネットワークの深さが増し、ニューラルネットワークの効果がある程度向上します。

たとえば、ストライド 1 の 3 つの 3x3 コンボリューション カーネルのレイヤーごとの重ね合わせは、サイズ 7 の受容野とみなすことができます (実際には、3 つの 3x3 の連続コンボリューションが 7x7 コンボリューションと同等であることを意味します)。合計パラメータは 3x(9xC^2) です。7x7 畳み込みカーネルが直接使用される場合、パラメータの合計数は 49xC^2 です。ここで、C は入力チャネルと出力チャネルの数を指します。明らかに、27xC2 49xC未満2、つまりパラメータが削減され、3x3 コンボリューション カーネルは画像のプロパティをより適切に維持するのに役立ちます。

5*5 コンボリューション カーネルの代わりに 2 つの 3x3 コンボリューション カーネルを使用できる理由を以下に説明します。

5x5 畳み込みは、5x5 領域内でスライドする小さな完全接続ネットワークと見なされます。最初に 3x3 畳み込みフィルターを使用して畳み込み、次に、この完全接続層を使用して 3x3 畳み込み出力を接続することもできます。 3x3 畳み込み層として見られます。このようにして、1 つの 5x5 コンボリューションの代わりに 2 つの 3x3 コンボリューションをカスケード (重ね合わせる) ことができます。

詳細を次の図に示します。

ここに画像の説明を挿入します

VGG ネットワーク構造

ここに画像の説明を挿入します

VGG ネットワークの構造は次のとおりです (VGG16 と VGG19 の両方が存在します)。

ここに画像の説明を挿入します

GGネットワ​​ーク構造

上の図の列 D に示すように、VGG16 には 16 の隠れ層 (13 の畳み込み層と 3 つの全結合層) が含まれています。

上図の列 E に示すように、VGG19 には 19 の隠れ層 (16 の畳み込み層と 3 つの全結合層) が含まれています。

VGG ネットワークの構造は非常に一貫しており、最初から最後まで 3x3 コンボリューションと 2x2 最大プーリングを使用します。

VGGの利点

VGGNet の構造は非常に単純で、ネットワーク全体で同じコンボリューション カーネル サイズ (3x3) と最大プーリング サイズ (2x2) が使用されます。

いくつかの小さなフィルター (3x3) 畳み込み層の組み合わせは、1 つの大きなフィルター (5x5 または 7x7) 畳み込み層よりも優れています。

ネットワーク構造を継続的に深くすることでパフォーマンスが向上することが確認されました。

VGGのデメリット

VGG はより多くのコンピューティング リソースを消費し、より多くのパラメーター (これは 3x3 畳み込みのポットではありません) を使用するため、メモリ使用量も増加します (140M)。

データセットの処理

データセットの紹介

CIFAR (Canadian Institute For Advanced Research) データセットは、コンピューター ビジョンの分野で広く使用されている小さな画像データセットで、主に機械学習とコンピューター ビジョン アルゴリズムのトレーニング、特に画像認識や分類などのタスクに使用されます。 CIFAR データセットは、CIFAR-10 と CIFAR-100 の 2 つの主要な部分で構成されます。

CIFAR-10 は、60,000 枚の 32x32 カラー画像を含むデータセットであり、10 のカテゴリに分割されており、各カテゴリには 6,000 枚の画像が含まれています。 10 カテゴリは、飛行機、車、鳥、猫、鹿、犬、カエル、馬、ボート、トラックです。データセットでは、トレーニングに 50,000 枚の画像が使用され、テストに 10,000 枚の画像が使用されます。 CIFAR-10 データセットは、適度なサイズと豊富なクラス情報により、コンピューター ビジョン分野の研究および教育において非常に人気のあるデータセットの 1 つとなっています。

データセットの特性
  • ミディアムサイズ: CIFAR データセット (32x32) の画像サイズは小さいため、新しいコンピューター ビジョン アルゴリズムを迅速にトレーニングおよびテストするのに最適です。
  • さまざまなカテゴリー: CIFAR-10 は基本的な画像分類タスクを提供しますが、CIFAR-100 はアルゴリズムのきめ細かい分類機能にさらに挑戦します。
  • 広く使われています: これらの特性により、CIFAR データセットは、コンピューター ビジョン、機械学習、ディープ ラーニング、その他の分野の研究と教育に広く使用されています。
使用するシーン

CIFAR データ セットは、画像分類、オブジェクト認識、畳み込みニューラル ネットワーク (CNN) のトレーニングとテストなどのタスクに一般的に使用されます。適度なサイズと豊富なカテゴリ情報により、画像認識アルゴリズムを検討する初心者や研究者にとって理想的です。さらに、コンピューター ビジョンや機械学習の多くのコンテストでも、出場者のアルゴリズムのパフォーマンスを評価するベンチマークとして CIFAR データセットが使用されています。

データセットを準備するには、すでにダウンロードしていますが、うまくいかない場合は、公式Webサイトからダウンロードするか、直接お渡しします。

データセットが必要な場合は、電子メールでお問い合わせください: [email protected]

私のデータセットはもともと torchvision でダウンロードされたデータを通じて生成されましたが、今はそれをやりたくありません。データセットの定義と DataLoader の読み込みを段階的に実装し、このプロセスを理解したいと考えています。データセットの処理プロセスを学ぶことで、ディープラーニングについてさらに深く学ぶことができます。

データセットのスタイルは次のとおりです。

ここに画像の説明を挿入します

データセットのすべてのラベルを解析します

データセットのラベル カテゴリは、.metaファイルは保存されているので、解析する必要があります .meta ファイルを使用してすべてのタグデータを読み取ります。解析コードは次のとおりです。

# 首先了解所有的标签,TODO 可以详细了解一下这个解包的过程
import pickle


def unpickle(file):
    with open(file, 'rb') as fo:
        dict = pickle.load(fo, encoding='bytes')
    return dict


meta_data = unpickle('./dataset_method_1/cifar-10-batches-py/batches.meta')
label_names = meta_data[b'label_names']
# 将字节标签转换为字符串
label_names = [label.decode('utf-8') for label in label_names]
print(label_names)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

分析の結果は次のとおりです。

['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']
  • 1

データの簡単なテストのためにデータの単一バッチをロードします。

データセットはダウンロードされているので、ファイルの内容を読み取る必要があります。ファイルはバイナリファイルであるため、読み取るにはバイナリ読み取りモードを使用する必要があります。

読み取りコードは次のとおりです。

# 载入单个批次的数据
import numpy as np


def load_data_batch(file):
    with open(file, 'rb') as fo:
        dict = pickle.load(fo, encoding='bytes')
        X = dict[b'data']
        Y = dict[b'labels']
        X = X.reshape(10000, 3, 32, 32).transpose(0, 2, 3, 1)  # reshape and transpose to (10000, 32, 32, 3)
        Y = np.array(Y)
    return X, Y


# 加载第一个数据批次
data_batch_1 = './dataset_method_1/cifar-10-batches-py/data_batch_1'
X1, Y1 = load_data_batch(data_batch_1)

print(f'数据形状: {X1.shape}, 标签形状: {Y1.shape}')

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

結果:

数据形状: (10000, 32, 32, 3), 标签形状: (10000,)
  • 1

すべてのデータをロードする

上記のテストの後、データをロードする方法がわかりました。次に、すべてのデータをロードしましょう。

トレーニング セットをロードします。

# 整合所有批次的数据
def load_all_data_batches(batch_files):
    X_list, Y_list = [], []
    for file in batch_files:
        X, Y = load_data_batch(file)
        X_list.append(X)
        Y_list.append(Y)
    X_all = np.concatenate(X_list)
    Y_all = np.concatenate(Y_list)
    return X_all, Y_all


batch_files = [
    './dataset_method_1/cifar-10-batches-py/data_batch_1',
    './dataset_method_1/cifar-10-batches-py/data_batch_2',
    './dataset_method_1/cifar-10-batches-py/data_batch_3',
    './dataset_method_1/cifar-10-batches-py/data_batch_4',
    './dataset_method_1/cifar-10-batches-py/data_batch_5'
]

X_train, Y_train = load_all_data_batches(batch_files)
print(f'训练数据形状: {X_train.shape}, 训练标签形状: {Y_train.shape}')
Y_train = Y_train.astype(np.int64)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

出力:

训练数据形状: (50000, 32, 32, 3), 训练标签形状: (50000,)
  • 1

テストセットをロードします:

test_batch = './dataset_method_1/cifar-10-batches-py/test_batch'
X_test, Y_test = load_data_batch(test_batch)
Y_test = Y_test.astype(np.int64)
print(f'测试数据形状: {X_test.shape}, 测试标签形状: {Y_test.shape}')

  • 1
  • 2
  • 3
  • 4
  • 5

出力:

测试数据形状: (10000, 32, 32, 3), 测试标签形状: (10000,)
  • 1

Dataset のサブクラスを定義する

Dataset クラスのサブクラスを定義すると、その後のバッチ トレーニング用のデータローダーのロードが容易になります。

Dataset のサブクラスが実装する必要があるメソッドは 3 つあります。

  • __init__()クラスコンストラクター
  • __len__()データセットの長さを返します
  • __getitem__()データセットからデータの一部を取得する

ここでの私の実装は次のとおりです。

from torch.utils.data import DataLoader, Dataset


# 定义 Pytorch 的数据集 
class CIFARDataset(Dataset):
    def __init__(self, images, labels, transform=None):
        self.images = images
        self.labels = labels
        self.transform = transform

    def __len__(self):
        return len(self.images)

    def __getitem__(self, idx):
        image = self.images[idx]
        label = self.labels[idx]

        if self.transform:
            image = self.transform(image)

        return image, label
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

データセットをデータローダーとしてロードします

  1. データを強化するための変換を定義します。最初にトレーニング セットを 4 ピクセル拡大し、正規化して、水平方向に反転し、グレースケール処理して、最後に元の 32 * 32 ピクセルに戻す必要があります。
transform_train = transforms.Compose(
    [transforms.Pad(4),
     transforms.ToTensor(),
     transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)),
     transforms.RandomHorizontalFlip(),
     transforms.RandomGrayscale(),
     transforms.RandomCrop(32, padding=4),
     ])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  1. これには画像処理が含まれており、バイナリ ファイルから読み取ったデータは numpy データであるため、画像処理を容易にするために numpy 配列を画像データに変換する必要があります。次のように処理します。
# 把数据集变成 Image 的数组,不然好像不能进行数据的增强
# 改变训练数据
from PIL import Image
def get_PIL_Images(origin_data):
    datas = []
    for i in range(len(origin_data)):
        data = Image.fromarray(origin_data[i])
        datas.append(data)
    return datas
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  1. トレーニングされたデータローダーを取得する
train_data = get_PIL_Images(X_train)
train_loader = DataLoader(CIFARDataset(train_data, Y_train, transform_train), batch_size=24, shuffle=True)
  • 1
  • 2
  1. テスト データローダー テスト セットの取得には、それほど多くの処理は必要ありません。コードはここに直接示されています。
# 测试集的预处理
transform_test = transforms.Compose(
    [
        transforms.ToTensor(),
        transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))]
)
test_loader = DataLoader(CIFARDataset(X_test, Y_test, transform_test), batch_size=24, shuffle=False)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

ネットワークを定義する

上記の VGG16 ネットワークに基づいて Pytorch フレームワークを実装します。

主に分けられるのは:

  • 畳み込み層
  • 全結合層
  • 分類レイヤー

実装は次のとおりです。

class VGG16(nn.Module):
    def __init__(self):
        super(VGG16, self).__init__()
        # 卷积层,这里进行卷积
        self.convolusion = nn.Sequential(
            nn.Conv2d(3, 96, kernel_size=3, padding=1), # 设置为padding=1 卷积完后,数据大小不会变
            nn.BatchNorm2d(96),
            nn.ReLU(inplace=True),
            nn.Conv2d(96, 96, kernel_size=3, padding=1),
            nn.BatchNorm2d(96),
            nn.ReLU(inplace=True),
            nn.Conv2d(96, 96, kernel_size=3, padding=1),
            nn.BatchNorm2d(96),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.Conv2d(96, 128, kernel_size=3, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.Conv2d(128, 128, kernel_size=3, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.Conv2d(128, 128, kernel_size=3, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.Conv2d(128, 256, kernel_size=3, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.Conv2d(256, 512, kernel_size=3, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.AvgPool2d(kernel_size=1, stride=1)
        )
        # 全连接层
        self.dense = nn.Sequential(
            nn.Linear(512, 4096), # 32*32 的图像大小经过 5 次最大化池化后就只有 1*1 了,所以就是 512 个通道的数据输入全连接层
            nn.ReLU(inplace=True),
            nn.Dropout(0.4),
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(0.4),
        )
        # 输出层
        self.classifier = nn.Linear(4096, 10)

    def forward(self, x):
        out = self.convolusion(x)
        out = out.view(out.size(0), -1)
        out = self.dense(out)
        out = self.classifier(out)
        return out
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75

トレーニングとテスト

トレーニングとテストの場合は、モデルをインスタンス化し、最適化関数、損失関数、損失率を定義して、トレーニングとテストを実行するだけです。

コードは以下のように表示されます。

ハイパーパラメータの定義:

# 定义模型进行训练
model = VGG16()
# model.load_state_dict(torch.load('./my-VGG16.pth'))
optimizer = optim.SGD(model.parameters(), lr=0.01, weight_decay=5e-3)
loss_func = nn.CrossEntropyLoss()
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.4, last_epoch=-1)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

テスト機能:

def test():
    model.eval()
    correct = 0  # 预测正确的图片数
    total = 0  # 总共的图片数
    with torch.no_grad():
        for data in test_loader:
            images, labels = data
            images = images.to(device)
            outputs = model(images).to(device)
            outputs = outputs.cpu()
            outputarr = outputs.numpy()
            _, predicted = torch.max(outputs, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum()
    accuracy = 100 * correct / total
    accuracy_rate.append(accuracy)
    print(f'准确率为:{accuracy}%'.format(accuracy))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

トレーニング エポック:

# 定义训练步骤
total_times = 40
total = 0
accuracy_rate = []
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

for epoch in range(total_times):
    model.train()
    model.to(device)
    running_loss = 0.0
    total_correct = 0
    total_trainset = 0
    print("epoch: ",epoch)
    for i, (data,labels) in enumerate(train_loader):
        data = data.to(device)
        outputs = model(data).to(device)
        labels = labels.to(device)
        loss = loss_func(outputs,labels).to(device)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        running_loss += loss.item()
        _,pred = outputs.max(1)
        correct = (pred == labels).sum().item()
        total_correct += correct
        total_trainset += data.shape[0]
        if i % 100 == 0 and i > 0:
            print(f"正在进行第{i}次训练, running_loss={running_loss}".format(i, running_loss))
            running_loss = 0.0
    test()
    scheduler.step()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

トレーニングされたモデルを保存します。

torch.save(model.state_dict(), './my-VGG16.pth')
accuracy_rate = np.array(accuracy_rate)
times = np.linspace(1, total_times, total_times)
plt.xlabel('times')
plt.ylabel('accuracy rate')
plt.plot(times, accuracy_rate)
plt.show()
print(accuracy_rate)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

テスト

  1. モデルの定義
model_my_vgg = VGG16()
  • 1
  1. トレーニング済みモデル データを追加する
model_my_vgg.load_state_dict(torch.load('./my-VGG16-best.pth',map_location='cpu'))
  • 1
  1. 自分で見つけた検証画像を加工してみる
from torchvision import transforms
from PIL import Image

# 定义图像预处理步骤
preprocess = transforms.Compose([
    transforms.Resize((32, 32)),
    transforms.ToTensor(),
    # transforms.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]),
])

def load_image(image_path):
    image = Image.open(image_path)
    image = preprocess(image)
    image = image.unsqueeze(0)  # 添加批次维度
    return image

image_data = load_image('./plane2.jpg')
print(image_data.shape)
output = model_my_vgg(image_data)
verify_data = X1[9]
verify_label = Y1[9]
output_verify = model_my_vgg(transform_test(verify_data).unsqueeze(0))
print(output)
print(output_verify)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

出力:

torch.Size([1, 3, 32, 32])
tensor([[ 1.5990, -0.5269,  0.7254,  0.3432, -0.5036, -0.3267, -0.5302, -0.9417,
          0.4186, -0.1213]], grad_fn=<AddmmBackward0>)
tensor([[-0.6541, -2.0759,  0.6308,  1.9791,  0.8525,  1.2313,  0.1856,  0.3243,
         -1.3374, -1.0211]], grad_fn=<AddmmBackward0>)
  • 1
  • 2
  • 3
  • 4
  • 5
  1. 印刷結果
print(label_names[torch.argmax(output,dim=1,keepdim=False)])
print(label_names[verify_label])
print("pred:",label_names[torch.argmax(output_verify,dim=1,keepdim=False)])
  • 1
  • 2
  • 3
airplane
cat
pred: cat
  • 1
  • 2
  • 3

ここに画像の説明を挿入します

馬を確認する

ここに画像の説明を挿入します

検証犬

ここに画像の説明を挿入します