📝

画像コンペことはじめ

に公開

はじめに

データアナリティクスラボ株式会社 データソリューション事業部 2025年新卒入社の藤田です。

今回はkaggleの画像コンペにまだ参加したことがなく、これから参加しようとする方に向けた記事を執筆いたしました。その中でも本記事は画像分類タスクに特化した内容になっております。

本記事はDAL Tech Blog Advent Calendar 2025として投稿しました。全ての記事は以下からご確認いただけます。

https://adventar.org/calendars/12288

使用するデータセット

記事の中で画像データを実際に扱っていきながら一連の流れを説明していきます。今回はその見本としてDanishFungi2020Miniというキノコのデータセットを用います(179種類、約3万枚)。

ダウンロードのコードは以下の通りです。

import pathlib, ssl, urllib.request
# 画像データのダウンロード
root = pathlib.Path("data/DF20M"); root.mkdir(parents=True, exist_ok=True)
url = "https://ptak.felk.cvut.cz/plants/DanishFungiDataset/DF20M-images.tar.gz"
dst = root/"DF20M-images.tar.gz"

ctx = ssl._create_unverified_context()
with urllib.request.urlopen(url, context=ctx) as r, open(dst, "wb") as f:
    f.write(r.read())

# メタデータのダウンロード
url = "https://ptak.felk.cvut.cz/plants/DanishFungiDataset/DF20M-metadata.zip"
dst = root/"DF20M-metadata.zip"

ctx = ssl._create_unverified_context()
with urllib.request.urlopen(url, context=ctx) as r, open(dst, "wb") as f:
    f.write(r.read())

DanishFungiのデータセットは画像データとメタデータに分かれているため、ダウンロード後に解凍してメタデータを元に画像データをフォルダ分けをしていきます。

DF20M/
├── train/
│   ├── Amanita_muscaria/
│   │   ├── img_001.jpg
│   │   ├── img_002.jpg
│   │   └── ...
│   ├── Boletus_edulis/
│   │   ├── img_101.jpg
│   │   └── ...
│   └── ...
└── val/
    ├── Amanita_muscaria/
    │   ├── img_501.jpg
    │   └── ...
    ├── Boletus_edulis/
    │   ├── img_701.jpg
    │   └── ...
    └── ...

画像データが用意できたら最初に行うこと

画像データが用意できたらいきなりモデルに入れて学習を行うのではなく、探索的データ分析(EDA)を行っていきます。EDAを行うことでコンペの全体像を掴めるだけでなく、適切な前処理を検討することができ、結果として効率的に学習を進めることができます。最低限確認するべき点は、

  1. 全画像のクラス分布
  2. サンプルをランダムで可視化
  3. 全画像の解像度の分布

の3点です。それぞれコードとともに注目する点を抑えていきます。

1.クラスの分布確認

データセットが不均衡である場合はモデルの精度に影響を与える可能性があるため、どのクラスの画像がどれだけデータセットに含まれているか分布の確認をします。

import os
from pathlib import Path
import pandas as pd
import matplotlib.pyplot as plt

# 入力
root = r"data/DF20M/imagefolder_tv"
split = "train"
split_dir = Path(root) / split

# 画像拡張子の候補
exts = {".jpg", ".jpeg", ".png", ".bmp", ".webp", ".tif", ".tiff"}

# クラスごとの枚数を集計
rows = []
for d in sorted(p for p in split_dir.iterdir() if p.is_dir()):
    n = sum(1 for f in d.iterdir() if f.is_file() and f.suffix.lower() in exts)
    rows.append((d.name, n))

df = pd.DataFrame(rows, columns=["class", "count"]).sort_values("count", ascending=False).reset_index(drop=True)

# 1. 横向きの箱ひげ図 
plt.figure(figsize=(8, 1.8))
plt.boxplot(df["count"].values, vert=False, showfliers=True)
plt.title(f"Distribution of class counts ({split})")
plt.tight_layout()
plt.show()

# 上位,下位10クラスを表示 
top10 = df.head(10)
bottom10 = df.tail(10).sort_values("count", ascending=True)  # 下位を小さい順に

print("Top 10 classes by count")
display(top10)

print("Bottom 10 classes by count")
display(bottom10)


少数クラスと多数クラスの差が極端な場合は、オーバーサンプリングやアンダーサンプリングを行うか、損失関数を設計する際にクラスの重みづけを適用するなどの対処が必要になります。
クラス間の差が極端でない場合はベースモデル学習後の予測精度から上記の対処を検討するとよいでしょう。また、コンペの評価指標によってはそこまで精度に影響を与えない場合もありますので、こちらも確認しておくと効率的に学習を進められます。

2.サンプルの可視化

次に実際にどんな画像データが用意されているのか描画して確認していきます。学習用データセットは膨大なデータ量であるため、ランダムで何枚か可視化してラベルが間違っていないか、画像に対して分類する対象物がどれぐらいの大きさで写っているかなどを確認していきます。この作業を行うことで、どのような前処理が必要になるかという方向性が定めやすくなります。例えば、

  • 対象物が大きく、高解像度
    そのままモデルに入れて学習を回すと処理に時間がかかり非効率になるため、実験初期では解像度を下げることで時間を短縮することができます。
  • 対象物が小さい
    この場合は、むやみに解像度を下げてしまうとモデルの精度に影響を与えてしまうため注意が必要です。
  • データセット内にラベルミスがある
    該当する画像は学習データセットに使用しないようにします。

サンプルの可視化をするコードは以下の通りです。

root = Path("data/DF20M/imagefolder_tv")
split = "train"                 # "train" or "val"
max_classes = 24                # 表示する最大クラス数(例: 12, 24など)
cols = 6                        # グリッドの列数              
exts = {".jpg", ".jpeg", ".png", ".bmp", ".webp", ".tif", ".tiff"}

classes = []
class_to_imgs = {}
for d in sorted(p for p in split_dir.iterdir() if p.is_dir()):
    imgs = [p for p in d.iterdir() if p.suffix.lower() in exts and p.is_file()]
    if imgs:
        classes.append(d.name)
        class_to_imgs[d.name] = imgs

# ランダムにクラスを抽出
if len(classes) > max_classes:
    classes = random.sample(classes, max_classes)

# 各クラスから1枚サンプル
samples = []
for c in classes:
    imgs = class_to_imgs[c]
    samples.append((c, random.choice(imgs)))

# グリッド行数を計算
rows = math.ceil(len(samples) / cols)

# 描画
fig, axes = plt.subplots(rows, cols, figsize=(cols * 2.0, rows * 2.0))
if rows == 1 and cols == 1:
    axes = [[axes]]
elif rows == 1:
    axes = [axes]
elif cols == 1:
    axes = [[ax] for ax in axes]

# 画像配置
i = 0
for r in range(rows):
    for cidx in range(cols):
        ax = axes[r][cidx]
        if i < len(samples):
            cls, img_path = samples[i]
            with Image.open(img_path) as im:
                ax.imshow(im)
            title = cls
            ax.set_title(title, fontsize=9)
            ax.axis("off")
        else:
            ax.axis("off")
        i += 1

plt.tight_layout()
plt.show()


今回の例ではキノコ博士でない限りラベルミスがあるかは判断できませんが、どんな画像を分類するのかイメージがつきやすくなります。(背景は様々で、キノコが上下が反転している画像も含まれているなど)

3.解像度の分布確認

解像度の確認も行いましょう。モデルの入力サイズを決定する際にこの情報が役に立ちます。
一般的に解像度が大きい画像がデータセットに多い場合には、入力サイズも大きめに設定してあげることで、モデルの精度が上がる可能性があります。
画像の高さと幅を散布図でプロットするサンプルコードを下に書きます。

root = Path("data/DF20M/imagefolder_tv/train")  # trainまたはval
exts = {".jpg", ".jpeg", ".png", ".bmp", ".webp", ".tif", ".tiff"}
files = [p for p in root.rglob("*") if p.suffix.lower() in exts]

# 幅・高さの取得
sizes = []
for p in files:
    try:
        with Image.open(p) as im:
            sizes.append(im.size)  # (width, height)
    except Exception:
        pass

sizes = np.array(sizes)
widths, heights = sizes[:, 0], sizes[:, 1]
plt.scatter(widths, heights, alpha=0.4)
plt.xlabel("widths")
plt.ylabel("heights")
plt.tight_layout()
plt.show()


この散布図から今回のデータセットは画像の大きさが1辺数百ピクセル~数千ピクセルまであり、画像の大きさがばらばらであることが分かりました。

この他にもRGB (Red, Green, Blue)統計量やHSV(Hue, Saturation, Value)統計量の確認など、タスクやデータによっては確認しておいたほうがよいことはありますが、まずは最低限として上3つの項目は確認しておくとよいでしょう。

モデルの選定方法

画像データのEDAが一通り終わったらモデルの選定を行っていきます。モデルの選定は精度に関わる大事な作業ですが、手当たり次第色々なモデルを試していると膨大な時間かかってしまうため、以下の情報を元にベースモデルを選定していくと良いと思います。

  • 類似のコンペのSolutionで出てくるモデルを使用する
  • ImageNetでの学習結果を参考にする
  • Backboneを比較した論文
  • 分類の対象となる物体の大きさをもとにモデルを絞る
    • 対象物が小さい場合(=高解像度での学習が必要な場合)
      →解像度スケーリングが得意なEfficientNetやConvNeXtなどのCNN系統
    • 対象物が大きい場合
      →画像全体を理解する場合はVisionTransformerやSwin TransformerなどのViT系統

timmを使ったモデルの作成方法

モデルの選定が終わったら、ベースモデルを作成していきます。
今回は様々な事前学習済みのモデルが手軽に扱えるライブラリであるtimm(PyTorch Images Models)を使用します。
timmで扱えるモデルの一覧は以下のコードで確認できます。

import timm
from pprint import pprint

# 学習済みモデルの一覧
pretrained_models = timm.list_models(pretrained=True)
print(len(pretrained_models), "pretrained models")
pprint(pretrained_models[:50])

# ResNet系統のモデルのみ出力
resnet = timm.list_models("*resnet*", pretrained=True)
print("resnet models:",len(resnet))
pprint(resnet)

モデルの作成はたった一行で書くことができます。

# 事前学習済みのresnet50を使う場合
model = timm.create_model("resnet50", pretrained = True)
# 予測するクラス数を10に変更する場合(クラス数は分類したい数に応じて変更してください)
model = timm.create_model("resnet50", pretrained = True, num_classes = 10)

また以下のコードを実行することでモデルの詳細を確認することができます。

print(model.num_features) # 特徴抽出器の最後のチャネルの数
pprint(model.feature_info) # 中間層の情報
pprint(model.default_cfg) # モデルの概要

>>2048
>>[{'module': 'act1', 'num_chs': 64, 'reduction': 2},
>> {'module': 'layer1', 'num_chs': 256, 'reduction': 4},
>> {'module': 'layer2', 'num_chs': 512, 'reduction': 8},
>> {'module': 'layer3', 'num_chs': 1024, 'reduction': 16},
>> {'module': 'layer4', 'num_chs': 2048, 'reduction': 32}]
>> {'architecture': 'resnet50',
>> 'classifier': 'fc',
>> 'crop_mode': 'center',
>> 'crop_pct': 0.95,
>> 'custom_load': False,
>> 'first_conv': 'conv1',
>> 'fixed_input_size': False,
>> 'hf_hub_id': 'timm/resnet50.a1_in1k',
>> 'input_size': (3, 224, 224),
>> 'interpolation': 'bicubic',
>> 'license': 'apache-2.0',
>> 'mean': (0.485, 0.456, 0.406),
>> 'num_classes': 1000,
>> 'origin_url': 'https://github.com/huggingface/pytorch-image-models',
>> 'paper_ids': 'arXiv:2110.00476',
>> 'pool_size': (7, 7),
>> 'std': (0.229, 0.224, 0.225),
>> 'tag': 'a1_in1k',
>> 'test_crop_pct': 1.0,
>> 'test_input_size': (3, 288, 288),
>> 'url': 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-rsb-weights/resnet50_a1_0-14fe96d1.pth'}

アーキテクチャの情報だけでなくモデルのライセンスやgithubのURLも入っています。

学習

モデルが作成出来たら、次はデータセットをモデルに入れるための前処理を行っていきます。
まずは訓練用と評価用で分割したデータセットのフォルダをtimmで読み込ませます。

# データセットの読み込み
import timm, torch
from timm.data import create_dataset, create_loader, resolve_data_config, create_transform

root = "data/DF20M/imagefolder_tv"  # 直下に train/ と val/ がある

# モデル(前処理パラメータの取得用)
model = timm.create_model("resnet50", pretrained=True, num_classes=179)
config = resolve_data_config({}, model=model) # 前処理に必要なパラメータ

train_ds = create_dataset(name="", root=f"{root}/train", split="train", is_training=True)
val_ds   = create_dataset(name="", root=f"{root}/val",   split="val",   is_training=False)

ここで前処理に必要なパラメータをconfigで呼び出しています。画像のリサイズや正規化の情報が入っていて一括で必要な前処理を行ってくれます。configの中身は以下のコードで確認できます。

# configの中身確認
for k, v in config.items():
    print(k, ":", v)

>> input_size : (3, 224, 224)
>> interpolation : bicubic   #画像をリサイズするときの補間方法
>> mean : (0.485, 0.456, 0.406) #各チャネルの平均値(imagenet)
>> std : (0.229, 0.224, 0.225)  #各チャネルの標準偏差(imagenet)
>> crop_pct : 0.95 
>> crop_mode : center

訓練用と評価用のデータセットにそれぞれ必要な前処理を行います。

# 前処理
train_tfms = create_transform(**config, is_training=True)
val_tfms   = create_transform(**config, is_training=False)
train_ds.transform = train_tfms
val_ds.transform   = val_tfms

訓練用と評価用では必要な前処理が異なります。
それぞれどんな前処理がされているかも確認することができます。

print(train_tfms)

>>Compose(
>>    RandomResizedCropAndInterpolation(size=(224, 224), scale=(0.08, 1.0), ratio=(0.75, 1.3333), interpolation=bicubic)
>>    RandomHorizontalFlip(p=0.5)
>>    ColorJitter(brightness=(0.6, 1.4), contrast=(0.6, 1.4), saturation=(0.6, 1.4), hue=None)
>>    MaybeToTensor()
>>    Normalize(mean=tensor([0.4850, 0.4560, 0.4060]), std=tensor([0.2290, 0.2240, 0.2250]))
)
print(val_tfms)

>>Compose(
>>    Resize(size=235, interpolation=bicubic, max_size=None, antialias=True)
>>    CenterCrop(size=(224, 224))
>>    MaybeToTensor()
>>    Normalize(mean=tensor([0.4850, 0.4560, 0.4060]), std=tensor([0.2290, 0.2240, 0.2250]))
)

create_transformの引数のis_trainingをTrueにすると、ランダムクロップや水平方向の反転、色調変換などの基礎的なデータ拡張を自動で行ってくれます。一方評価用のデータセットは、リサイズと正規化のみでその他の変換は行っていません。

次にデータローダーの設定を行います。
GPUで効率良く学習を行うためにバッチ単位でモデルに入力します。

# DataLoader
train_loader = create_loader(
    train_ds, input_size=config["input_size"],
    batch_size=64, is_training=True, num_workers=4, pin_memory=True
)
val_loader = create_loader(
    val_ds, input_size=config["input_size"],
    batch_size=64, is_training=False, num_workers=4, pin_memory=True
)

ここまででデータの準備が完了したので、モデルに入れて学習を行っていきます。
コードは以下の通りです。

from tqdm import tqdm
import torch, torch.nn.functional as F
# パラメータの設定
num_epochs = 30
device = "cuda"
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)
loss_fn = torch.nn.CrossEntropyLoss()

model.to(device)
best_acc = 0.0

for epoch in range(num_epochs):
    print("EPOCH", epoch)
    # ===== Train =====
    model.train()
    s, n = 0.0, 0
    for inputs, targets in tqdm(train_loader):
        inputs, targets = inputs.to(device), targets.to(device)
        outputs = model(inputs)
        loss = loss_fn(outputs, targets)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        s += loss.item() * inputs.size(0); n += inputs.size(0)
    tr = s / max(1, n)

    # ===== Valid =====
    model.eval()
    s, correct, n = 0.0, 0, 0
    with torch.no_grad():
        for inputs, targets in val_loader:
            inputs, targets = inputs.to(device), targets.to(device)
            outputs = model(inputs)
            s += loss_fn(outputs, targets).item() * inputs.size(0)
            pred = outputs.argmax(dim=1)
            correct += (pred == targets).sum().item()
            n += targets.size(0)
    va = s / max(1, n)
    acc = correct / max(1, n)

    print(f"epoch {epoch}: train_loss={tr:.4f}  val_loss={va:.4f}  val_acc={acc:.4f}")
    
    if acc > best_acc:
        best_acc = acc
        torch.save(model.state_dict(), "model_best.pth") 
        print(f"  -> Best model saved! (Acc: {best_acc:.4f})")

torch.save(model.state_dict(), "model_final.pth")

optimizerはAdam、損失関数はcrossentropylossを採用しています。
ベースモデルの学習結果は以下の通りです。

エポック数が進むごとにtrain_lossとval_lossの差が開いていることから、過学習が起こっていることが読み取れます。

精度向上のための施策

ここからはベースモデルの精度を向上させるためのいくつかの手法をご紹介します。全て試せば必ず精度が上がるというわけではなく、データセットによって効果は様々ですので、色々試していきましょう。

ベースモデルの改良

EDAで分かったデータセットの特徴とベースモデルの学習結果から3つの改良を行います。
まずベースモデルの結果から学習後半のエポックで精度が大きく揺れていることが分かりました。これは学習率が一定であることが原因と考えられるため、改良案として①学習率スケジューラを導入します。
さらに、EDAからクラスが不均衡であることと画像サイズが大きいものが含まれていることを考慮し、②損失関数の重みづけと③入力画像を大きくします。

#1.学習率スケジューラの導入
#学習のコードに追加してください
from torch.optim.lr_scheduler import CosineAnnealingLR
scheduler = CosineAnnealingLR(optimizer, T_max=num_epochs, eta_min=1e-6)
#2.損失関数の重みづけ
import pandas as pd
device = "cuda"
# メタデータから出現数をカウント→逆数を重みとする
train_df = pd.read_csv("data/DF20M/metadata/DF20M-train_metadata_PROD.csv")
class_counts = train_df['species'].value_counts().sort_index().values

weights = 1.0 / (class_counts ** 0.5) 
weights = weights / weights.sum() * len(class_counts)
class_weights = torch.tensor(weights, dtype=torch.float).to(device)
#学習のコードの損失関数を変更してください
loss_fn = torch.nn.CrossEntropyLoss(weight=class_weights)
#3.入力画像のサイズを上げる
# 前処理の際にconfigの値を変更してください
config['input_size'] = (3, 384, 384)
train_tfms = create_transform(**config, is_training=True)
val_tfms   = create_transform(**config, is_training=False)
train_ds.transform = train_tfms
val_ds.transform   = val_tfms

それぞれ順番に追加して学習を行った結果は以下の通りです。学習率スケジューラを導入したことで後半のエポック数で揺れが小さくなり安定した学習が行えるようになっています。

また、それぞれのモデルのMacroF1を比較すると、すべての改良を加えたモデル4でAccとMacroF1が高い値を出していることが分かりました。

Model Val Acc Macro F1
2 0.7051 0.6165
3 0.6999 0.6076
4 0.7204 0.6344

データ拡張

次にデータ拡張について一工夫を加えることで精度を上げることができるか試します。
その前に前章ではデータ拡張についてあまり触れられていなかったので補足します。

データ拡張とは、学習用の画像データに対して何らかの変換を施してデータのバリエーションを増やす手法であり、画像の変換を行うことでデータ数を増加させながら過学習の対策も行えます。
本記事では今回のタスクに有効と思われるデータ拡張と画像分類タスク全般に有効であるデータ拡張の2種類を紹介します。

最初に今回のタスクに有効と思われるデータ拡張についてです。
前章のコードで基礎的なデータ拡張はtimmのライブラリで行えていますが、より高度で今回のタスクのドメインに特化したデータ拡張を行うことで精度が向上する場合があります。

今回はデータ拡張によく使われるライブラリであるAlbumentationsを用います。
最初にEDAでサンプルの可視化を行ったときに、以下の要素があったことを思い出します。

  • キノコの上下が逆さまになっている画像もある
  • キノコの一部が隠れている or 一部しか映っていない画像もある
  • 野外で撮影されているため焦点が合っていない画像もある

これらの画像に対して汎化性を持たせたモデルにするために、学習データセットに以下の変換を加えます。

import albumentations as A
from albumentations.pytorch import ToTensorV2
import numpy as np
from PIL import Image

# === 1. PIL画像をAlbumentationsで処理するためのクラス ===
class AlbumentationsTransform:
    def __init__(self, transform):
        self.transform = transform

    def __call__(self, img):
        # PIL画像をNumPy配列に変換
        img_np = np.array(img)
        # Albumentationsを適用
        augmented = self.transform(image=img_np)
        return augmented['image']

# === 2. データ拡張の定義 ===
input_size = config['input_size'][1] # 224
mean = config['mean']
std = config['std']

# Albumentationsでパイプラインを構築
train_albu = A.Compose([
    # サイズ調整 (timmのRandomResizedCropと同様の挙動を再現)
    A.RandomResizedCrop(size=(input_size, input_size), scale=(0.08, 1.0)),
    
    # 水平反転
    A.HorizontalFlip(p=0.5),

    # 垂直反転
    A.VerticalFlip(p=0.5),
    
    # ぼかし
    A.MotionBlur(blur_limit=(3, 7), p=0.5),

    # 穴あけ (CoarseDropout)
    A.CoarseDropout(num_holes_range=(1, 8),hole_height_range=(1, 32),hole_width_range=(1, 32), fill_value=0,p=0.3),
    
    # 正規化とTensor変換
    A.Normalize(mean=mean, std=std),
    ToTensorV2(),
])

# === 3. データセットに適用 ===
# 自作したクラスでラップして渡すのがポイント
train_ds.transform = AlbumentationsTransform(train_albu)

# Val用は変更なし(timmのcreate_transformでOK)
val_ds.transform = create_transform(**config, is_training=False)

次に画像分類全般に有効なデータ拡張についてです。
良く使われる手法にMixupとCutMixがあります。これらは複数の画像をブレンドすることで新しい学習データを生成する手法です。
Mixupは2枚のサンプルを線形に組み合わせ、CutMixは画像の一部を切り抜いて他の画像を挿入することでデータ拡張を行います。正解ラベルはどのようになるかというと、Mixupではブレンド比率によって再計算され、CutMixでは面積比率によって同様に再計算されます。(キノコA:0.7, キノコB:0.3のように、正解ラベルは0,1ではなく少数の値を取ります。)

MixupとCutMixはtimmのライブラリの機能として用意されています。インポートして先ほどのベースモデルの訓練部分を丸々入れ替えることで、簡単にMixupとCutMixを使うことができます。

#timmのMixupとLossをインポート
from timm.data import Mixup
from timm.loss import SoftTargetCrossEntropy

num_epochs = 30
device = "cuda"

# Mixup/CutMix の設定
mixup_fn = Mixup(
    mixup_alpha=0.8,    # Mixupの混ざり具合
    cutmix_alpha=1.0,   # CutMixの混ざり具合
    prob=0.7,           # 70%の確率でMixup/CutMixを適用する
    switch_prob=0.5,    # MixupとCutMixを半々の確率で切り替える
    mode='batch',
    label_smoothing=0.1,
    num_classes=model.num_classes # モデルのクラス数を渡す
)

optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)

# Loss関数を Train用と Val用で分ける
train_loss_fn = SoftTargetCrossEntropy() # Train用: 混ざったラベルに対応したLoss
val_loss_fn = torch.nn.CrossEntropyLoss() # Val用: 通常のLoss (評価は混ぜないため)

model.to(device)

best_acc = 0.0

for epoch in range(num_epochs):
    print("EPOCH", epoch)
    
    # ===== Train (Mixupあり) =====
    model.train()
    s, n = 0.0, 0
    for inputs, targets in tqdm(train_loader):
        inputs, targets = inputs.to(device), targets.to(device)
        
        # inputs: 混ぜられた画像
        # targets: 混ぜられたラベル (例: [0, 0, 1] -> [0, 0.5, 0.5])
        inputs, targets = mixup_fn(inputs, targets)
        
        outputs = model(inputs)
        
        #SoftTargetCrossEntropy で計算
        loss = train_loss_fn(outputs, targets)
        
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        s += loss.item() * inputs.size(0); n += inputs.size(0)
    
    tr = s / max(1, n)

    # ===== Valid (Mixupなし・通常通り) =====

それぞれのデータ拡張を加えた学習の様子は以下のようになりました。

Model Val Acc Macro F1
2 0.7051 0.6165
3 0.6999 0.6076
4 0.7204 0.6344
5 0.7210 0.6352
6 0.6975 0.5742

今回の条件では、画像コンペに特化したデータ拡張を入れたモデル5が最も精度が高く出ましたが、CutMixやMixupを加える場合は問題が難しくなっているので、一般的にはエポック数も上げる必要があります。そのためエポック数をさらに増やした場合は結果が変わるかもしれません。

アンサンブル手法

最後にアンサンブル手法についてご紹介します。
アンサンブル手法といえば、複数の学習モデルを組み合わせてモデルを作成、または推論を行うModel soupsという手法があります。画像分類のタスクにおいてもこの手法は精度向上のために有効ですが、今回はアンサンブル手法の一つであるTTA(Test Time Augmentation)を紹介します。

TTAでは推論に用いる画像に対してデータ拡張を行い、得られた推論結果を平均、最大、多数決などの集計から最終的な予測結果を導き出します。
そのため、モデルの精度上げが一通り終わった後に最後の一押しという段階で導入します。

Metric Normal With TTA Gain
Accuracy (Top-1) 0.7210 0.7227 +0.0017
Accuracy (Top-5) 0.9184 0.9180 -0.0005
Macro F1-Score 0.6352 0.6368 +0.0016

先ほどまでの実験で最も精度が良かったモデル5についてTTAを行いました。
ほんの少しですがMacroF1が上昇していることが分かりました。こちらも必ず精度が上がるというわけではありませんが、提出前に試すとよいかもしれません。

まとめ

今回は画像コンペに初めて参加する人に向けて、どのような流れで進めていくのが良いかの簡単な道しるべや精度上げのためのいくつかの手法にまとめました。キャッチアップしながらの執筆でしたが、様々な記事を参考にして進め方について大変勉強になりました。
今回は画像分類に限定したお話しでしたが、ゆくゆくは物体検出やセグメンテーション、スライス画像のモデリングなどについても記事を出せればと思っています!

参考文献

Kaggleの「画像コンペ」とは
Kaggle画像コンペで成功するための取り組み
backboneとしてのtimm入門
PyTorch TIMMモデル生成・一覧取得の方法
データ拡張徹底入門!

DAL Tech Blog

Discussion