pytorchの画像分類モデルの使い方（と速度比較）

# 知ってるようで知らない画像分類の使い方
画像分類は基本タスクな感じがするけど、じゃあどうすればいいのかとなるとどうするんだっけという感じもする。
state of artの先端アーキテクチャも色々あるんだろうけど、torchvisionのモデルでけっこう精度でるし、自分はけっこう十分です。

# 推論の仕方 

利用可能なモデルとweightsは以下を参照。

https://pytorch.org/vision/stable/models.html

### モデルの初期化

```python
import torchvision.models as models

weights = models.ViT_L_16_Weights.IMAGENET1K_SWAG_E2E_V1
model = models.vit_l_16(weights=weights)
```

### 画像の前処理

```python
from PIL import Image

transforms = weights.transforms()

im = Image.open("cat.jpg")
input = transforms(im)
```

### クラスの読み込み

ImageNet1kのクラス名です。

```python
classes = weights.meta["categories"]
```

### 推論

```python
import torch

with torch.no_grad():
  outputs = model(im.unsqueeze(dim=0).to("cpu"))
  outputs = torch.nn.Softmax(dim=1)(outputs)
  conf, pred = torch.max(outputs, 1)
  print(classes[int(pred)])
  print(float(conf[0]))
```

この画像で推論します。
![cat.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/95e492ef-c11c-31e7-0304-3f1114e1ba58.jpeg)

>tabby, tabby cat
0.5702

# 速度比較

Colabの一番安いT4GPUでテスト。

|model|ImageNetTop1 Acc|sec|
|----|----|----|
|ViT_L_16_Weights.IMAGENET1K_SWAG_E2E_V1|88.1|13.3|
|ViT_B_16_Weights.IMAGENET1K_SWAG_E2E_V1|85.3|2.2|
|Swin_S_Weights.IMAGENET1K_V1|83.2|0.4|
|ConvNeXt_Small_Weights.IMAGENET1K_V1|83.6|0.3|

# 学習

### 必要なライブラリのインポート

```python
from __future__ import print_function, division

import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim import lr_scheduler
import torch.backends.cudnn as cudnn
import numpy as np
import torchvision
from torchvision import datasets, models, transforms
import matplotlib.pyplot as plt
import time
import os
import copy

cudnn.benchmark = True
plt.ion()   # interactive mode
```

### データの準備

以下のディレクトリ構造でクラスごとにフォルダ分けした画像をtrain、valそれぞれ用意

```
my_dataset
  |
  |__train
  |     |__class0
  |     |     |__****.jpg
  |     |     |__****.jpg
  |     |     |
  |     |     
  |     |__class1
  |     |
  |     
  |__val
        |__class0
        |     |__****.jpg
        |     |__****.jpg
        |     |
        |     
        |__class1
        |
```

データをtorchvisionのdataset形式にする。
リサイズのサイズは、torchvisionの各モデルのページを参照。

https://pytorch.org/vision/stable/models.html


```python
# Data augmentation and normalization for training
# Just normalization for validation
data_transforms = {
    'train': transforms.Compose([
        transforms.Resize([256,256]),
        # transforms.RandomResizedCrop(224),
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
    'val': transforms.Compose([
        transforms.Resize([256,256]),
        # transforms.CenterCrop(224),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
}

data_dir = 'my_dataset'
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),
                                          data_transforms[x])
                  for x in ['train', 'val']}
dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=4,
                                             shuffle=True, num_workers=4)
              for x in ['train', 'val']}
dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'val']}
class_names = image_datasets['train'].classes

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
```

表示してデータを確認。クラス名も取得。

```python
def imshow(inp, title=None):
    """Imshow for Tensor."""
    inp = inp.numpy().transpose((1, 2, 0))
    mean = np.array([0.485, 0.456, 0.406])
    std = np.array([0.229, 0.224, 0.225])
    inp = std * inp + mean
    inp = np.clip(inp, 0, 1)
    plt.imshow(inp)
    if title is not None:
        plt.title(title)
    plt.pause(0.001)  # pause a bit so that plots are updated


# Get a batch of training data
inputs, classes = next(iter(dataloaders['train']))

# Make a grid from batch
out = torchvision.utils.make_grid(inputs)

imshow(out, title=[class_names[x] for x in classes])
```

### train関数
関数内でweightsチェックポイントの保存先を指定。

```python
def train_model(model, criterion, optimizer, scheduler, num_epochs=25):
    since = time.time()

    best_model_wts = copy.deepcopy(model.state_dict())
    best_acc = 0.0

    for epoch in range(num_epochs):
        print(f'Epoch {epoch}/{num_epochs - 1}')
        print('-' * 10)

        # Each epoch has a training and validation phase
        for phase in ['train', 'val']:
            if phase == 'train':
                model.train()  # Set model to training mode
            else:
                model.eval()   # Set model to evaluate mode

            running_loss = 0.0
            running_corrects = 0

            # Iterate over data.
            for inputs, labels in dataloaders[phase]:
                inputs = inputs.to(device)
                labels = labels.to(device)

                # zero the parameter gradients
                optimizer.zero_grad()

                # forward
                # track history if only in train
                with torch.set_grad_enabled(phase == 'train'):
                    outputs = model(inputs)
                    _, preds = torch.max(outputs, 1)
                    loss = criterion(outputs, labels)

                    # backward + optimize only if in training phase
                    if phase == 'train':
                        loss.backward()
                        optimizer.step()

                # statistics
                running_loss += loss.item() * inputs.size(0)
                running_corrects += torch.sum(preds == labels.data)
            if phase == 'train':
                scheduler.step()

            epoch_loss = running_loss / dataset_sizes[phase]
            epoch_acc = running_corrects.double() / dataset_sizes[phase]
            save_path = "save_dir/my_model"+str(epoch)+".pt"
            torch.save({'epoch': epoch,'model_state_dict': model.state_dict(),'optimizer_state_dict': optimizer.state_dict(),'loss': loss,}, save_path)

            print(f'{phase} Loss: {epoch_loss:.4f} Acc: {epoch_acc:.4f}')

            # deep copy the model
            if phase == 'val' and epoch_acc > best_acc:
                best_acc = epoch_acc
                best_model_wts = copy.deepcopy(model.state_dict())

        print()

    time_elapsed = time.time() - since
    print(f'Training complete in {time_elapsed // 60:.0f}m {time_elapsed % 60:.0f}s')
    print(f'Best val Acc: {best_acc:4f}')

    # load best model weights
    model.load_state_dict(best_model_wts)
    return model
```

### 事前トレーニング済みモデルとトレーニングシステムの初期化

```python
num_ftrs = model.head.in_features
model.head = nn.Linear(num_ftrs, len(class_names))

model = model.to(device)

criterion = nn.CrossEntropyLoss()

# Observe that all parameters are being optimized
optimizer_ft = optim.SGD(model.parameters(), lr=0.001, momentum=0.9,weight_decay=0.02)

# Decay LR by a factor of 0.1 every 7 epochs
exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)
```

### トレーニング開始

```python
model_ft = train_model(model, criterion, optimizer_ft, exp_lr_scheduler,
                       num_epochs=1000)
```

### トレーニングしたweightsで推論

```python
import torch
from torchvision import models, transforms
from PIL import Image

model = models.swin_v2_s(weights=None)
num_ftrs = model.head.in_features
model.head = torch.nn.Linear(num_ftrs, len(class_names))
if torch.cuda.is_available():
  checkpoint = torch.load(weights_path)
  device = 'cuda'
else:
  checkpoint = torch.load(weights_path,map_location=torch.device('cpu'))
  device = 'cpu'

model.load_state_dict(checkpoint['model_state_dict'])
model.eval()
model = model.to(device)
```