【NLP】画像分類にPyTorchを使用した移行学習の例


特徴抽出では、事前トレーニングされたネットワーク構造の後に単純な分類器を変更または追加し、最後に追加された分類器に対してのみ、ソース タスクの事前トレーニングされたネットワークを別のターゲット タスクの特徴抽出器として使用できます。パラメータは再学習されます。ただし、事前トレーニングされたネットワーク パラメーターは変更または凍結されません。

新しいタスクの特徴抽出が完了すると、すべてのパラメータを再学習するのではなく、ソース タスクで学習したパラメータが使用されます。次の例では、例を使用して、特徴抽出の方法を通じて画像を分類する方法を具体的に説明します。

1.インポートモジュール

from datetime import datetime

import matplotlib.pyplot as plt
import numpy as np
import torch
import torchvision
import torchvision.transforms as transforms
from torch import nn
from torchvision import models

2. データのロード

ここでは、CIFAR10 データを事前にローカルにダウンロードする必要があります。時間がかかるため、download=False とします。さらに、データの標準化や画像のトリミングなど、いくつかの前処理機能が追加されました。

def load_data(data, batch_size=64, num_workers=2, mean=None, std=None):
    if std is None:
        std = [0.229, 0.224, 0.225]
    if mean is None:
        mean = [0.485, 0.456, 0.406]
    trans_train = transforms.Compose([transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(),
                                      transforms.ToTensor(), transforms.Normalize(mean=mean, std=std)])
    trans_valid = transforms.Compose([transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(),
                                      transforms.Normalize(mean=mean, std=std)])

    train_set = torchvision.datasets.CIFAR10(root=data, train=True, download=True, transform=trans_train)
    trainloader = torch.utils.data.DataLoader(train_set, batch_size=batch_size, shuffle=True, num_workers=num_workers)

    test_set = torchvision.datasets.CIFAR10(root=data, train=False, download=True, transform=trans_valid)
    testloader = torch.utils.data.DataLoader(test_set, batch_size=batch_size, shuffle=False, num_workers=num_workers)
    return trainloader, testloader

3. モデル処理

このセクションには 3 つの操作が含まれています。

  • 事前トレーニング モデルをダウンロードします。使用される事前トレーニング モデルは resnet18 で、ImageNet の大規模データ セットでトレーニングされています。
  • モデルパラメータの凍結: バックプロパゲーション中に更新されないようにします。
  • 最後のレイヤーの出力カテゴリの数を変更します。このデータセットには 1000 個のカテゴリがあります。つまり、元の出力は 512×1000 ですが、ここで使用される新しいデータセットには 10 カテゴリがあるため、これは 512×10 に変更されます。
def freeze_net(num_class=10):
    # 下载预训练模型
    net = models.resnet18(pretrained=True)
    # 冻结模型参数
    for params in net.parameters():
        params.requires_grad = False
    # 修改最后一层的输出类别数
    net.fc = nn.Linear(512, num_class)
    # 查看冻结前后的参数情况
    total_params = sum(p.numel() for p in net.parameters())
    print(f'原总参数个数:{
      
      total_params}')
    total_trainable_params = sum(p.numel() for p in net.parameters() if p.requires_grad)
    print(f'需训练参数个数:{
      
      total_trainable_params}')
    return net

元のパラメータの総数: 11181642
学習されるパラメータの数: 5130

出力からわかるように、フリーズされていない場合は更新する必要があるパラメータが多すぎるため、フリーズ後は全結合層のパラメータのみを更新する必要があります。

4. モデルのトレーニングと検証

ここでは、クロスエントロピーが損失関数として選択され、SGD がオプティマイザとして使用され、学習率は 1e-3、重み減衰は 1e-3 に設定されています。コードは次のとおりです。

# 训练及验证模型
def train(net, train_data, valid_data, num_epochs, optimizer, criterion):
    prev_time = datetime.now()
    for epoch in range(num_epochs):
        train_loss = 0
        train_acc = 0
        net = net.train()
        for im, label in train_data:
            im = im.to(device)  # (bs, 3, h, w)
            label = label.to(device)  # (bs, h, w)
            # forward
            output = net(im)
            loss = criterion(output, label)
            # backward
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            train_loss += loss.item()
            train_acc += get_acc(output, label)

        cur_time = datetime.now()
        h, remainder = divmod((cur_time - prev_time).seconds, 3600)
        m, s = divmod(remainder, 60)
        time_str = "Time %02d:%02d:%02d" % (h, m, s)
        if valid_data is not None:
            valid_loss = 0
            valid_acc = 0
            net = net.eval()
            for im, label in valid_data:
                im = im.to(device)  # (bs, 3, h, w)
                label = label.to(device)  # (bs, h, w)
                output = net(im)
                loss = criterion(output, label)
                valid_loss += loss.item()
                valid_acc += get_acc(output, label)
            epoch_str = (
                    "Epoch %d. Train Loss: %f, Train Acc: %f, Valid Loss: %f, Valid Acc: %f, "
                    % (epoch, train_loss / len(train_data),
                       train_acc / len(train_data), valid_loss / len(valid_data),
                       valid_acc / len(valid_data)))
        else:
            epoch_str = ("Epoch %d. Train Loss: %f, Train Acc: %f, " %
                         (epoch, train_loss / len(train_data),
                          train_acc / len(train_data)))
        prev_time = cur_time
        print(epoch_str + time_str)

运行結果:
エポック 0. 列車損失: 1.474121、列車応答: 0.498322、有効損失: 0.901339、有効応答: 0.713177、時刻 00:03:26 エポック 1. 列車損失: 1.222752、列車応答: 0.576946、有効損失: 0.818926
、有効なACC:0.730494、時間00:04:35
エポック2.列車損失:1.172832、列車ACC:0.592651、有効な損失:0.777265、有効なACC:0.737759、時間00:04:23エポック3.列車損失:1.158157、
列車ACC ACC : 0.596228、有効な損失: 0.761969、有効な応答: 0.746517、時刻 00:04:28
エポック 4。列車の損失: 1.143113、列車の応答: 0.600643、有効な損失: 0.757134、有効な応答: 0.742138、時刻 00:0 4:24
エポック 5列車の損失: 1.128991、列車のアクセス数: 0.607797、有効な損失: 0.745840、有効なアクセス数: 0.747014、時間 00:04:24
エポック 6. 列車の損失: 1.131602、列車のアクセス数: 0.603561、有効な損失: 0.740176、有効なアクセス数: 0.748109、時刻 00:04:21 エポック 7. 列車の損失: 1.127840、列車のアクセス数: 0.608336、有効な損失:
0.738235 、有効なアカウント: 0.751990、時刻 00:04:19
エポック 8. 列車損失: 1.122831、列車応答: 0.609275、有効損失: 0.730571、有効応答: 0.751692、時刻 00:04:18 エポック 9. 列車損失: 1.118955、列車応答: 0.6
09715、有効な損失: 0.731084、有効な Acc: 0.751692、時刻 00:04:13
エポック 10。列車の損失: 1.111291、列車の応答: 0.612052、有効な損失: 0.728281、有効な Acc: 0.749602、時刻 00:04:09 エポック
11。列車損失: 1.108454、列車応答: 0.612712、有効損失: 0.719465、有効応答: 0.752787、時刻 00:04:15
エポック 12。列車損失: 1.111189、列車応答: 0.612012、有効損失: 0.726525、有効応答: 0.751294、時間00: 04:09
エポック 13。列車損失: 1.114475、列車応答: 0.610594、有効損失: 0.717852、有効な応答: 0.754080、時刻 00:04:06 エポック 14。列車損失: 1.112658、列車応答: 0.608596、有効損失: 0.7233 36
、有効なアカウント: 0.751393、時刻 00:04:14
エポック 15。列車損失: 1.109367、列車応答: 0.614950、有効損失: 0.721230、有効応答: 0.752588、時刻 00:04:06 エポック 16。列車損失: 1.107644、列車応答: 0 .614230
、有効な損失: 0.711586、有効な Acc: 0.755275、時刻 00:04:08
エポック 17。列車の損失: 1.100239、列車の応答: 0.613411、有効な損失: 0.722191、有効な Acc: 0.749303、時刻 00:04:11 エポック
18。列車損失: 1.108576、列車アクセス: 0.611013、有効損失: 0.721263、有効アクセス: 0.753483、時間 00:04:08
エポック 19。列車の損失: 1.098069、列車のアクセス数: 0.618027、有効な損失: 0.705413、有効なアクセス数: 0.757962、時間 00:04:06

結果から、検証セットの正解率は約 75% でした。次に、微調整 + データ強化の手法を使用して精度を向上させ続けます。

5.微調整

微調整により、事前トレーニングされたネットワーク パラメーターを変更してターゲット タスクを学習できるため、トレーニング時間は特徴抽出方法よりも長くなりますが、精度は高くなります。微調整の一般的なプロセスは、事前トレーニングされたネットワークに新しいランダム初期化層を追加することです。さらに、事前トレーニングされたネットワーク パラメーターも更新されますが、学習率が大きく変化するのを防ぐために、より小さい学習率が使用されます。事前トレーニングされたパラメータ

一般的な方法は、最下層のパラメータを固定し、一部の最上層または特定の層のパラメータを調整することですこれにより、トレーニング パラメーターの数が減り、過剰適合を回避できます。特に対象タスクのデータ量が少ない場合には、この方法は非常に有効です。

実際、微調整は、転送された事前トレーニング済みネットワークのパラメーターを最適化して、新しいタスクにより適したものにすることができるため、特徴抽出よりも優れています。
(1) データの前処理
画像のトリミング、回転、色の変更などのいくつかのデータ拡張方法がトレーニング データに追加されます。テストデータは特徴抽出方法と同じです。

    if fine_tuning is False:
        trans_train = transforms.Compose([transforms.RandomResizedCrop(224),
                                          transforms.RandomHorizontalFlip(),
                                          transforms.ToTensor(),
                                          transforms.Normalize(mean=mean, std=std)])
    else:
        trans_train = transforms.Compose([transforms.RandomResizedCrop(size=256, scale=(0.8, 1.0)),
                                          transforms.RandomRotation(degrees=15),
                                          transforms.ColorJitter(),
                                          transforms.RandomResizedCrop(224),
                                          transforms.RandomHorizontalFlip(),
                                          transforms.ToTensor(),
                                          transforms.Normalize(mean=mean, std=std)])

(2) モデルの分類層を変更する
最後の全結合層を変更し、カテゴリ数を 1000 から 10 に変更します。

def freeze_net(num_class=10, fine_tuning=False):
    # 下载预训练模型
    net = models.resnet18(pretrained=True)
    print(net)
    if fine_tuning is False:
        # 冻结模型参数
        for params in net.parameters():
            params.requires_grad = False
    # 修改最后一层的输出类别数
    net.fc = nn.Linear(512, num_class)
    # 查看冻结前后的参数情况
    total_params = sum(p.numel() for p in net.parameters())
    print(f'原总参数个数:{
      
      total_params}')
    total_trainable_params = sum(p.numel() for p in net.parameters() if p.requires_grad)
    print(f'需训练参数个数:{
      
      total_trainable_params}')
    # 打印出第一层的权重
    print(f'第一层的权重:{
      
      net.conv1.weight.type()}')
    return net

训练結果:
エポック 0. 列車の損失: 1.455535、列車のアクセス: 0.488460、有効な損失: 0.832547、有効なアクセス: 0.721400、時刻 00:14:48 エポック 1. 列車の損失: 1.342625、列車のアクセス: 0.530280、有効損失: 0.815430
、有効なACC:0.723500、時間10:31:48
エポック2.列車損失:1.319122、列車ACC:0.535680、有効な損失:0.866512、有効なACC:0.699000、時間00:12:02エポック3.列車損失:1.310949、
列車ACC : 0.541700、有効な損失: 0.789511、有効な応答: 0.728000、時刻 00:12:03
エポック 4。列車の損失: 1.313486、列車の応答: 0.538500、有効な損失: 0.762553、有効な応答: 0.741300、時刻 00:1 2:19
エポック 5列車の損失: 1.309776、列車のアクセス数: 0.540680、有効な損失: 0.777906、有効なアクセス数: 0.736100、時間 00:11:43
エポック 6. 列車損失: 1.302117、列車応答: 0.541780、有効損失: 0.779318、有効な応答: 0.737200、時刻 00:12:00 エポック 7. 列車損失: 1.304539、列車応答: 0.544320、有効損失: 0.795917 、
有効なアカウント: 0.726500、時刻 00:13:16
エポック 8. 列車損失: 1.311748、列車応答: 0.542400、有効損失: 0.785983、有効応答: 0.728000、時刻 00:14:48 エポック 9. 列車損失: 1.302069、列車応答: 0.5 44820
、有効な損失: 0.781665、有効な Acc: 0.734700、時刻 00:14:15
エポック 10。列車の損失: 1.298019、列車の応答: 0.547040、有効な損失: 0.771555、有効な Acc: 0.742200、時刻 00:16:11
エポック 11。列車損失: 1.310127、列車応答: 0.538700、有効損失: 0.764313、有効応答: 0.739300、時刻 00:17:33
エポック 12。列車損失: 1.300172、列車応答: 0.544720、有効損失: 0.765881、有効応答: 0.734200、時間00: 12:04
エポック 13。列車損失: 1.289607、列車応答: 0.546980、有効損失: 0.753371、有効な応答: 0.742500、時刻 00:11:49 エポック 14。列車損失: 1.295938、列車応答: 0.546280、有効損失: 0.8210 99
、有効なアカウント: 0.721900、時間 00:11:43

微調整トレーニング手法を使用すると、特徴抽出手法を使用する場合に比べて時間が大幅に長くなりますが、GPU メモリの制限により、batch_size が 16 に設定されているため、検証セットの精度が向上しません。

6. その他のコード

if __name__ == '__main__':
    data_path = './data'
    classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'forg', 'horse', 'ship', 'truck')
    if torch.cuda.is_available():
        device = torch.device('cuda:0')
        torch.cuda.empty_cache()
    else:
        device = torch.device('cpu')
    # 加载数据
    train_loader, test_loader = load_data(data=data_path, fine_tuning=True)
    # 随机获取部分训练数据
    data_iter = iter(train_loader)
    images, labels = data_iter.next()
    # 显示图像
    imshow(torchvision.utils.make_grid(images[:4]))
    # 打印标签
    print(' '.join('%5s' % classes[labels[j]] for j in range(4)))
    # 加载模型
    net = freeze_net(num_class=len(classes), fine_tuning=True)
    net = net.to(device)

    # 定义损失函数及优化器
    criterion = nn.CrossEntropyLoss()
    # 只需要优化最后一层参数
    optimizer = torch.optim.SGD(net.fc.parameters(), lr=1e-3, weight_decay=1e-3, momentum=0.9)
    # 训练及验证模型
    train(net, train_loader, test_loader, 20, optimizer, criterion)

おすすめ

転載: blog.csdn.net/ARPOSPF/article/details/131842625