【精度比較】ノーコードAI開発ツール　VS　PytorchのAIモデル(Pretrained + Fine-tuning)【Few-shot】

[前回の記事](https://qiita.com/umapyoi/items/a5c9e4a628e49046b64a)で、ノーコードAI開発ツール「[ADFI](https://adfi.jp/ja/ai/)」を使って、**Few-shot(少量の学習データ)で100種類（クラス）以上を識別する画像認識モデルを作成したところ、なんと精度100％でした！**
ADFIの画像認識モデルはものすごく高性能なAIアルゴリズムのようです！

そこで、今回、Pytorchで有名な画像認識モデル（Vision Transformerなど）を同じ条件で作成して、ADFIと精度比較してみました。

**結論から言うと、ADFIの方が、圧倒的に精度が高く、なおかつ、学習にかかる時間も短くてすみました！**

もはや、自分でPytorchでAIモデルを開発するのは、時代遅れということでしょうか？

![overview.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2535236/afe3d8c7-775c-ae89-6264-4fbe00de8ecc.png)


# 実験設定
普段、自分が業務でAIモデルを開発する時は、Pytorchの事前学習済みモデル（Pretrained model）をファインチューニング（Fine-tuning）させる際に、事前学習済みモデルの重みを固定して、最終層（全結合層）だけ学習させることが多いです。（学習データが少なくても、比較的高精度なAIモデルが作成できるため）

上記のやり方を踏襲して、今回の実験は以下の条件で行いました。
## 学習方法・測定方法
 - Pytorchの事前学習済みモデルの重みを固定し、最終層だけファインチューニングする。
 - 学習データは少量（Few-shot）とする。（画像3枚／クラスのパターン、5枚／クラスのパターンを測定）
 - 精度比較は、検証データ（Validation data）に対する正解率（accuracy）を測定し、比較する。
 - 学習回数は30回（30エポック）とし、各エポックの正解率の中で最も高い値を採用する。（ADFIは、作成したAIモデルの正解率をそのまま採用。）
 - GPU無しの普通のPCでAIモデルを作成する（CPUで学習する）。
 - 学習開始から終了までの処理時間を測定し、比較する。

## 学習データ・検証データ
 - ネットで公開されている食べ物の画像データセット「[iFood 2019 Challenge](https://github.com/karansikka1/iFood_2019)」で検証を行う。
 - 今回Few-shotで検証するので、学習データ（Training data）は、3枚／クラス、5枚／クラスに絞る。
 - Few-shotの場合、クラス数が多すぎると全てのAIモデルの正解率が低くなりすぎて比較困難になる可能性があるため、クラス数は10クラスに絞る。なお、10クラスは下記（0~9）とする。
 - 検証データ（Validation data）は、10クラスの全ての画像（519枚）を使用する。
 
| クラス番号 | クラス名 | 学習データ数 | 検証データ数 |
|:---------:|:-----:|:-----:|:-----:|
| 0 | macaron | 3 または 5 | 54 |
| 1 | beignet | 3 または 5 | 61 |
| 2 | cruller | 3 または 5 | 53 |
| 3 | cockle_food | 3 または 5 | 51 |
| 4 | samosa | 3 または 5 | 41 |
| 5 | tiramisu | 3 または 5 | 52 |
| 6 | tostada | 3 または 5 | 57 |
| 7 | moussaka | 3 または 5 | 54 |
| 8 | dumpling | 3 または 5 | 49 |
| 9 | sashimi | 3 または 5 | 46 |

![adfi_screen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2535236/01769a87-93ed-5519-5352-433059c8fb24.png)
ADFIでは、画面上で各クラスの画像をアップロードして10クラスを設定。

## AIモデル
画像認識に利用できる[Pytorchの分類モデル（Classification model）](https://pytorch.org/vision/stable/models.html)の中で、有名な下記３種類のAIモデルと、ADFIで作成したAIモデルを比較する。
 - [ResNet50](https://pytorch.org/vision/stable/models/resnet.html)
 - [MobileNet V3](https://pytorch.org/vision/stable/models/mobilenetv3.html)
 - [VisionTransformer](https://pytorch.org/vision/stable/models/vision_transformer.html)

 # 実験結果
 ## PytorchのAIモデルの精度測定結果
 Pytorchで作成した3つのAIモデルの実験結果のグラフです。
 各エポック（Epoch）における、学習データに対する正解率（Train Accuracy）と検証データ（画像519枚）に対する正解率（Val Accuracy）です。
 ### 学習データ数：３／クラス（3-Shot）
![result_3shot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2535236/2d6c72f0-f546-58c8-b67c-287685415cc8.png)

 ### 学習データ数：５／クラス（5-Shot）
 ![result_5shot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2535236/0fbace21-5cf0-65a6-df49-5b6272bad9d1.png)

## ADFIとPytorchのAIモデルの精度比較結果
下記表の通り、「３/クラス」と「５／クラス」の両方のパターンで、ADFIの正解率が90％を超え、全てのAIモデルの中で飛び抜けて高い結果になりました！
２番目に精度が高かったのは、Vision Transformerでした。

|  | ADFI | ResNet50 | MobileNet V3 | Vision Transformer |
|:---------:|:-----:|:-----:|:-----:|:-----:|
| ３／クラス　正解率 | **0.930** | 0.581 | 0.529 | 0.649 |
| ５／クラス　正解率 | **0.940** | 0.705 | 0.657 | 0.801 |

## 学習にかかる時間の比較結果
AIモデルの学習にかかった時間を測定して比較しました。
結果は下記表の通り、MobileNetが最も短く、ADFIが僅差の２番目でした！
Vision Transformerは、モデルのデータサイズが大きいため、だいぶ時間がかかってしまいました。

|  | ADFI | ResNet50 | MobileNet V3 | Vision Transformer |
|:---------:|:-----:|:-----:|:-----:|:-----:|
| ３／クラス　学習時間(秒) | 184 | 713 | **171** | 1392 |
| ５／クラス　学習時間(秒) | 297 | 728 | **207** | 1475 |

# まとめ
iFoodの画像データセットを使って、ノーコードAI開発ツール「[ADFI](https://adfi.jp/ja/ai/)」で作成したAIモデルと、Pytorchで作成したAIモデルの精度と学習時間を測定し、比較しました。
ADFIは、圧倒的に精度が高く、学習時間についてもMobileNetに匹敵するくらいの短時間で学習完了することが判明しました！
**ノーコードAI開発ツールを活用することで、AI開発の効率化と品質向上を同時に実現できそうです！**

これからは、生成AIだけではなく、画像AIもノーコードの時代ですね！

# おまけ
検証時に使ったソースコードを掲載しておきます。（８割くらいChatGPTで作成したので、コメントが若干不自然ですが...）
### ADFIの精度測定プログラム
```python:adfi_val.py
"""
pip install cryptography
pip install pillow
pip install torch
pip install torchvision
pip install ftfy
pip install regex
pip install tqdm
pip install requests
"""
import csv
import os
import time

from adfi_vit_local import adfi_vit

if __name__ == "__main__":
    # 下記項目を設定してください。
    model_path = "runtime_xxxxx.vit_model"  # ADFIで作成したAIモデルをダウンロードして、ファイルパスを設定
    val_csv = "val_info.csv"  # 検証データのアノテーションファイル
    num_classes = 10  # 0～9までの10クラスに対して検証
    dir_path = "val_set"  # 検証データのディレクトリ

    if not os.path.exists(model_path):
        print("File does not exist: ", model_path)

    model = adfi_vit.AI_Model()
    model.load(model_path)

    print(model.get_info())

    samples = []

    # CSV
    with open(val_csv, "r", newline="", encoding="utf-8") as f:
        reader = csv.reader(f)
        for row in reader:
            # CSVの各行: [画像ファイルパス, クラス番号]
            file_name, label_str = row
            try:
                label = int(label_str)
            except ValueError:
                # 整数変換できない場合はスキップ
                continue

            # クラス番号が範囲内かどうかを確認
            if 0 <= label < num_classes:
                samples.append((file_name, label))

    print("len(samples): ", len(samples))
    start_time = time.time()
    correct = 0
    total = 0

    for sample in samples:
        image_path = os.path.join(dir_path, sample[0])
        result_dict = model.predict(image_path)
        total += 1
        if int(result_dict["class"]) == int(sample[1]):
            correct += 1

    val_acc = correct / total
    end_time = time.time()
    execution_time = end_time - start_time

    print("total: ", total)
    print("correct: ", correct)
    print("val_acc: ", val_acc)
    print("execution_time: ", execution_time)
```

### PytorchのVision Transformerの精度測定プログラム
```python:vision_transformer_val.py
import csv
import os
import time

import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from PIL import Image


class CustomImageDataset(torch.utils.data.Dataset):
    def __init__(self, csv_file, class_names, transform=None, dir_path=""):
        """
        csv_file: 画像のパスとクラスラベルが書かれたcsvファイル (ヘッダーなし)
                  1列目: 画像ファイルパス
                  2列目: クラス番号(0, 1, 2, ...)
        class_names: class_list.txt などから読み込んだクラス名のリスト
        transform: 画像変換処理 (torchvision.transforms など)
        """
        self.samples = []
        self.transform = transform
        self.num_classes = len(class_names)
        self.dir_path = dir_path

        with open(csv_file, "r", newline="", encoding="utf-8") as f:
            reader = csv.reader(f)
            for row in reader:
                # CSVの各行: [画像ファイルパス, クラス番号]
                file_name, label_str = row
                try:
                    label = int(label_str)
                except ValueError:
                    # 整数変換できない場合はスキップ
                    continue

                # クラス番号が範囲内かどうかを確認
                if 0 <= label < self.num_classes:
                    self.samples.append((file_name, label))

    def __getitem__(self, index):
        file_name, label = self.samples[index]
        image = Image.open(os.path.join(self.dir_path, file_name)).convert("RGB")
        if self.transform:
            image = self.transform(image)
        return image, label

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


def main():
    # ----------------------------
    # 0. クラス名リストの読み込み
    # ----------------------------
    class_list_file = "class_list.txt"
    with open(class_list_file, "r", encoding="utf-8") as f:
        lines = f.readlines()
    class_names = []
    for line in lines:
        splitted = line.strip().split(maxsplit=1)
        if len(splitted) == 2:
            class_number, class_name = splitted
            if int(class_number) < 10:
                class_names.append(class_name)
    num_classes = len(class_names)

    print("Number of classes:", num_classes)
    print("Classes:", class_names)

    # ----------------------------
    # 1. データセットの準備
    # ----------------------------
    train_csv = "train_info.csv"  # 学習データのアノテーションファイル。各クラス3行、もしくは、5行だけ残して、他の行は全て削除しておく。
    val_csv = "val_info.csv"  # 検証データのアノテーションファイル
    train_img_dir = "train_set"  # 学習データのディレクトリ
    val_img_dir = "val_set"  # 検証データのディレクトリ

    # --- Vision Transformer (ViT-B/16) 用の推奨 Transform を使用 ---
    from torchvision.models import ViT_B_16_Weights

    # ImageNet 学習済み重みを選択
    weights = ViT_B_16_Weights.IMAGENET1K_V1

    # weights.transforms() で推奨の前処理を取得
    transform = weights.transforms()

    train_dataset = CustomImageDataset(
        csv_file=train_csv,
        class_names=class_names,
        transform=transform,
        dir_path=train_img_dir,
    )
    val_dataset = CustomImageDataset(
        csv_file=val_csv,
        class_names=class_names,
        transform=transform,
        dir_path=val_img_dir,
    )

    print("len(train_dataset):", len(train_dataset))
    print("len(val_dataset):", len(val_dataset))

    batch_size = 32
    train_loader = torch.utils.data.DataLoader(
        train_dataset, batch_size=batch_size, shuffle=True, num_workers=4
    )
    val_loader = torch.utils.data.DataLoader(
        val_dataset, batch_size=batch_size, shuffle=False, num_workers=4
    )

    # ----------------------------
    # 2. モデルの用意 (Vision Transformer, ViT-B/16)
    # ----------------------------
    model = torchvision.models.vit_b_16(weights=weights)

    # 全パラメータを固定（fine-tuning したい場合はここを適宜変更）
    for param in model.parameters():
        param.requires_grad = False

    # Vision Transformer の最終層は model.heads.head
    # デフォルトでは out_features=1000 (ImageNet 1000クラス)
    # ここを置き換えてクラス数を num_classes にする
    in_features = model.heads.head.in_features
    model.heads.head = nn.Linear(in_features, num_classes)

    # GPU対応
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model.to(device)
    print("device: ", device)

    # ----------------------------
    # 3. 損失関数とオプティマイザ
    # ----------------------------
    criterion = nn.CrossEntropyLoss()
    # 最終層(model.heads.head)のみ学習させる
    optimizer = optim.Adam(model.heads.head.parameters(), lr=1e-3)

    # ----------------------------
    # 4. 学習・評価ループ
    # ----------------------------
    num_epochs = 30
    train_acc_list = []
    val_acc_list = []

    def evaluate(model, loader):
        model.eval()
        correct = 0
        total = 0
        with torch.no_grad():
            for images, labels in loader:
                images, labels = images.to(device), labels.to(device)
                outputs = model(images)
                _, predicted = torch.max(outputs, 1)
                total += labels.size(0)
                correct += (predicted == labels).sum().item()
        return correct / total

    start_time = time.time()

    for epoch in range(num_epochs):
        model.train()
        train_loss = 0.0
        for images, labels in train_loader:
            images, labels = images.to(device), labels.to(device)

            optimizer.zero_grad()
            outputs = model(images)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()

            train_loss += loss.item()

        train_loss /= len(train_loader)

        train_acc = evaluate(model, train_loader)
        val_acc = evaluate(model, val_loader)
        train_acc_list.append(train_acc)
        val_acc_list.append(val_acc)

        print(
            f"Epoch [{epoch+1}/{num_epochs}] "
            f"Loss: {train_loss:.4f}, "
            f"Train Accuracy: {train_acc:.4f}, "
            f"Val Accuracy: {val_acc:.4f}"
        )

    end_time = time.time()
    elapsed_time = end_time - start_time
    print(f"Training time: {elapsed_time:.2f} [sec]")

    # ----------------------------
    # 5. 学習過程の可視化
    # ----------------------------
    os.makedirs("results", exist_ok=True)

    plt.figure(figsize=(8, 6))
    plt.plot(range(1, num_epochs + 1), train_acc_list, label="Train Accuracy")
    plt.plot(range(1, num_epochs + 1), val_acc_list, label="Val Accuracy")
    plt.xlabel("Epoch")
    plt.ylabel("Accuracy")
    plt.title("Training & Validation Accuracy")
    plt.legend()
    plt.grid(True)

    save_path = os.path.join("results", "vit_accuracy_curve.png")
    plt.savefig(save_path)
    print(f"Accuracy curve saved to: {save_path}")

    plt.show()


if __name__ == "__main__":
    main()
```