timm の学習済みモデルで画像分類を転移学習する:データセットの作り方と学習パラメーター
手元の画像を分類するモデルを作るとき、ニューラルネットワークをゼロから学習させる必要はほとんどありません。ImageNet などの大きなデータで 学習済みのモデル を持ってきて、最後の層を自分のクラス数に付け替えて学習し直す 転移学習 を使えば、数千枚の画像でも高い正解率が出ます。
この記事では、PyTorch の画像モデルのライブラリ timm から学習済みのモデルを持ってきて、花の写真を 5 種類(デイジー・タンポポ・バラ・ヒマワリ・チューリップ)に分類するモデルを作ります。「どんなフォルダー構成でデータセットを作り、どんなパラメーターで学習させるか」を、そのまま使い回せるスクリプトの形でまとめます。転移学習の考え方そのものは 転移学習とファインチューニングのしくみ で説明しています。
この記事では、モデルのうち画像から特徴を作る部分を 体(バックボーン)、最後の特徴からクラスごとの点数を出す層を 頭(ヘッド)と呼びます。
結論を先に書くと、次のようにします。
| 項目 | やり方 |
|---|---|
| モデル | timm.create_model("<モデル名>", pretrained=True, num_classes=<クラス数>) で、学習済みの体に新しい頭を付ける |
| データセット | train/<クラス名>/ と val/<クラス名>/ に画像を分けて置き、ImageFolder で読む |
| 前処理 | timm.data.resolve_data_config でモデルの事前学習の設定を取り出し、create_transform で作る |
| 学習 | 頭だけを数エポック学習してから、全体を微調整する 2 段階 |
| 学習率 | AdamW で頭 1e-3、体はその 1/10 の 1e-4。ウォームアップ + コサインで下げる |
この設定で学習すると、約 2900 枚の学習用の画像から、検証用の 731 枚で 正解率 0.906 のモデルができました。CPU(4 コア)だけで、学習時間は合計 26 分ほどでした。
環境
スクリプトを動かした環境です。GPU は使わず、CPU(4 コア)だけで学習しています。GPU があれば、スクリプトは自動で GPU を使います。
| 項目 | バージョン |
|---|---|
| Python | 3.13 |
| PyTorch | 2.15 |
| torchvision | 0.29 |
| timm | 1.0 |
パッケージは pip で入れます。
pip install torch torchvision timm
最終的なファイルの構成は次のとおりです。
.
├── split_dataset.py # 画像を train / val に分ける
├── train.py # 学習する
├── predict.py # 学習したモデルで推論する
├── flower_photos/ # 元の画像(クラスごとのフォルダー)
├── data/flowers/ # split_dataset.py が作るデータセット
│ ├── train/<クラス名>/*.jpg
│ └── val/<クラス名>/*.jpg
└── outputs/best.pt # 検証用データでいちばん良かったモデル
学習済みモデルを選ぶ
timm のモデル名の読み方
timm のモデル名は アーキテクチャ.事前学習のタグ の形になっています。同じ ResNet-18 でも、学習のしかたやデータが違う重みが複数あります。
import timm
print(timm.list_models("resnet18*", pretrained=True))
実行結果:
['resnet18.a1_in1k', 'resnet18.a2_in1k', 'resnet18.a3_in1k', 'resnet18.fb_ssl_yfcc100m_ft_in1k', 'resnet18.fb_swsl_ig1b_ft_in1k', 'resnet18.gluon_in1k', 'resnet18.tv_in1k', 'resnet18d.ra2_in1k', 'resnet18d.ra4_e3600_r224_in1k']
タグの in1k は ImageNet-1k(1000 クラス・約 128 万枚)、in22k や in21k はより大きな ImageNet-21k で学習したことを表します。a1 は timm の改良した学習方法(ResNet strikes back)の設定の名前、tv は torchvision の重みです。各モデルの詳しい説明は、Hugging Face Hub の timm/<モデル名> のページにあります。
最初に試すモデル
最初は小さいモデルで一通り動かし、精度が足りなければ大きいモデルに替えるのが効率的です。モデル名を変えるだけで、ほかのコードはそのまま使えます。代表的なモデルを並べます(パラメーター数は timm で数えた値)。
| モデル名 | パラメーター数 | 特徴 |
|---|---|---|
resnet18.a1_in1k | 1170 万 | 軽く、CPU でも学習できる。この記事で使う |
efficientnet_b0.ra_in1k | 530 万 | 小さいわりに精度が高い。スマートフォンなどで動かすときの候補 |
resnet50.a1_in1k | 2560 万 | 定番の中くらいのモデル |
convnext_tiny.fb_in22k_ft_in1k | 2860 万 | ImageNet-21k で学習してから 1k で微調整した重み。転移しやすい |
vit_small_patch16_224.augreg_in21k_ft_in1k | 2210 万 | Vision Transformer。データ拡張を強めて学習した重み |
事前学習の設定を確かめる
モデルごとに、事前学習のときの入力の大きさや色の正規化の値が決まっていて、pretrained_cfg に入っています。resolve_data_config を使うと、前処理に必要な値だけを取り出せます。
for name in ["resnet18.a1_in1k", "vit_small_patch16_224.augreg_in21k_ft_in1k"]:
model = timm.create_model(name, pretrained=False)
print(name, timm.data.resolve_data_config({}, model=model))
実行結果:
resnet18.a1_in1k {'input_size': (3, 224, 224), 'interpolation': 'bicubic', 'mean': (0.485, 0.456, 0.406), 'std': (0.229, 0.224, 0.225), 'crop_pct': 0.95, 'crop_mode': 'center'}
vit_small_patch16_224.augreg_in21k_ft_in1k {'input_size': (3, 224, 224), 'interpolation': 'bicubic', 'mean': (0.5, 0.5, 0.5), 'std': (0.5, 0.5, 0.5), 'crop_pct': 0.9, 'crop_mode': 'center'}
ResNet-18 の正規化の値は ImageNet でよく使われる (0.485, 0.456, 0.406) ですが、この ViT は (0.5, 0.5, 0.5) です。前処理を手で書いて値を決め打ちにすると、モデルを替えたときにずれてしまうので、前処理はモデルの設定から作る ようにします。
データセットを作る
フォルダー構成
画像は、クラス名のフォルダーに分けて置きます。学習用(train)と検証用(val)は、最初にフォルダーごと分けておきます。
data/flowers/
├── train/
│ ├── daisy/ *.jpg
│ ├── dandelion/
│ ├── roses/
│ ├── sunflowers/
│ └── tulips/
└── val/
├── daisy/
└── ...(train と同じクラス名)
この形にしておくと、torchvision の ImageFolder でそのまま読めます。ImageFolder は、フォルダー名をクラス名にし、フォルダー名の辞書順 に 0, 1, 2, … と番号を付けます。train と val で同じクラス名のフォルダーをそろえておけば、番号も一致します。
分け方をコードの中の乱数ではなくフォルダーで固定しておくと、次の利点があります。
- モデルや設定を変えても、いつも同じ画像で検証できるので、結果を比べられる
- 検証用の画像を目で見て確かめられる(間違えた画像を開く、ラベルの間違いを直す)
- 同じ物を写した似た画像(連写・同じ製品)があるときは、それらがまとめて同じ側に入るように手で分けられる。train と val に分かれて入ると、検証の正解率が実際より高く出てしまう
元の画像を用意する
例として、TensorFlow のチュートリアルで配布されている花の写真のデータセット(約 3700 枚、CC BY 2.0)を使います。
curl -LO https://storage.googleapis.com/download.tensorflow.org/example_images/flower_photos.tgz
tar xzf flower_photos.tgz
展開すると、flower_photos/<クラス名>/*.jpg の形で画像が入っています。自分の画像で試すときも、この形に並べれば同じスクリプトが使えます。
train と val に分ける
クラスごとに 2 割を検証用にして、data/flowers/ にコピーします。クラスごとに分けるのは、クラスの割合が train と val でそろうようにするためです。
split_dataset.py:
"""クラスごとのフォルダーに入った画像を、train / val に 8:2 で分けてコピーする"""
import random
import shutil
from pathlib import Path
SRC = Path("flower_photos") # 元の画像: flower_photos/<クラス名>/*.jpg
DST = Path("data/flowers") # 出力先: data/flowers/{train,val}/<クラス名>/*.jpg
VAL_RATIO = 0.2
EXTS = {".jpg", ".jpeg", ".png", ".webp"}
rng = random.Random(42) # 何度実行しても同じ分け方になるように固定する
for class_dir in sorted(p for p in SRC.iterdir() if p.is_dir()):
files = sorted(p for p in class_dir.iterdir() if p.suffix.lower() in EXTS)
rng.shuffle(files)
n_val = int(len(files) * VAL_RATIO)
for split, items in (("val", files[:n_val]), ("train", files[n_val:])):
out = DST / split / class_dir.name
out.mkdir(parents=True, exist_ok=True)
for f in items:
shutil.copy2(f, out / f.name)
print(f"{class_dir.name}: train {len(files) - n_val}, val {n_val}")
python split_dataset.py
実行結果:
daisy: train 507, val 126
dandelion: train 719, val 179
roses: train 513, val 128
sunflowers: train 560, val 139
tulips: train 640, val 159
枚数の目安
クラスごとの枚数が少ないほど、転移学習の効き目は大きくなります。目安としては次のように考えます。
| 1 クラスあたりの枚数 | 進め方 |
|---|---|
| 数十枚 | 頭だけを学習する(段階 1 だけ)。データ拡張を必ず入れる |
| 数百〜数千枚 | 頭を学習してから全体を微調整する(この記事の方法) |
| 数万枚以上 | 全体の微調整が中心。学習率を少し上げてもよい |
検証用は、1 クラスあたり少なくとも数十枚は残します。少なすぎると、1 枚の当たり外れで正解率が大きく動き、設定の良し悪しを判断できません。
学習のパラメーター
使った値と理由
train.py の先頭にまとめた設定です。
| パラメーター | 値 | 理由 |
|---|---|---|
| 入力の大きさ | 224 × 224 | モデルの事前学習の設定(input_size)に合わせる |
| バッチサイズ | 32 | メモリーに収まる範囲で。小さいモデルなら 32〜128 が目安 |
| オプティマイザー | AdamW | 学習率の調整が楽で、微調整でよく使われる |
| 頭の学習率 | 1e-3 | 頭は乱数から学習するので大きめにする |
| 体の学習率 | 1e-4 | 学習済みの特徴を壊さないよう、頭の 1/10 にする |
| 重み減衰(weight decay) | 0.05 | 過学習を抑える。AdamW では 0.01〜0.05 がよく使われる |
| 学習率のスケジュール | 1 エポックのウォームアップ → コサインで下げる | 微調整の始めに体が大きく動くのを防ぎ、最後は小さく動かして落ち着かせる |
| エポック数 | 頭だけ 2 + 全体 6 | 転移学習は収束が速い。検証の正解率が頭打ちになるところを見て決める |
| ラベルスムージング | 0.1 | 正解のクラスに確率 1 を出させず、自信過剰を防ぐ |
| データ拡張 | ランダムな切り抜き・左右反転・色の揺らぎ | timm の create_transform(is_training=True) の初期設定 |
いちばん効くのは 体の学習率 です。大きくしすぎると、学習済みの特徴が少ないデータに合わせて書き換わり、かえって精度が下がります(転移学習とファインチューニングのしくみ の「微調整の学習率」の実験)。迷ったら、1e-4 から始めて 3e-5・3e-4 と前後を試します。
なぜ 2 段階で学習するのか
モデルを作った直後の頭は乱数なので、いきなり全体を学習すると、大きく間違った勾配が体にまで流れ込み、学習済みの特徴を崩してしまいます。そこで、
- 段階 1: 体を固定して、頭だけを学習する
- 段階 2: 頭がある程度できてから、全体を小さな学習率で微調整する
の順にします。この順で学習すると特徴が崩れにくく、精度も上がりやすいことが知られています(LP-FT、Kumar ら, 2022)。
学習スクリプト
モデルとデータを用意する
num_classes を指定すると、timm は学習済みの重みを読み込んだうえで、最後の全結合層(頭)だけを指定したクラス数の新しい層に取り替えます。新しい頭の重みは乱数で初期化されます。
前処理は resolve_data_config で取り出した設定から、学習用(データ拡張あり)と検証用(縮小と中央の切り抜きだけ)を作ります。
train.py(前半):
import time
from pathlib import Path
import timm
import torch
from timm.scheduler import CosineLRScheduler
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
CFG = {
"model": "resnet18.a1_in1k", # timm のモデル名(アーキテクチャ.事前学習のタグ)
"data_dir": "data/flowers",
"out": "outputs/best.pt",
"batch_size": 32,
"num_workers": 4,
"head_epochs": 2, # 段階 1: 頭だけを学習するエポック数
"full_epochs": 6, # 段階 2: 全体を微調整するエポック数
"head_lr": 1e-3, # 頭の学習率
"body_lr": 1e-4, # 体の学習率(頭の 1/10)
"weight_decay": 0.05,
"warmup_epochs": 1,
"label_smoothing": 0.1,
"seed": 0,
}
torch.manual_seed(CFG["seed"])
device = "cuda" if torch.cuda.is_available() else "cpu"
# --- モデル: 学習済みの体 + クラス数に合わせた新しい頭 ---
data_dir = Path(CFG["data_dir"])
classes = sorted(p.name for p in (data_dir / "train").iterdir() if p.is_dir())
model = timm.create_model(CFG["model"], pretrained=True, num_classes=len(classes))
model.to(device)
# --- データ: 前処理はモデルの事前学習の設定から作る ---
data_cfg = timm.data.resolve_data_config({}, model=model)
train_tf = timm.data.create_transform(**data_cfg, is_training=True)
val_tf = timm.data.create_transform(**data_cfg)
train_ds = datasets.ImageFolder(data_dir / "train", transform=train_tf)
val_ds = datasets.ImageFolder(data_dir / "val", transform=val_tf)
assert train_ds.classes == classes == val_ds.classes
train_dl = DataLoader(train_ds, batch_size=CFG["batch_size"], shuffle=True,
num_workers=CFG["num_workers"], drop_last=True)
val_dl = DataLoader(val_ds, batch_size=CFG["batch_size"] * 2, num_workers=CFG["num_workers"])
print(f"classes={classes} train={len(train_ds)} val={len(val_ds)} device={device}")
作られた前処理は、print(train_tf) で確かめられます。
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]))
)
学習用は、画像の面積の 8〜100% にあたる範囲をランダムな縦横比で切り抜いて 224 × 224 にし、左右反転と明るさ・コントラスト・彩度の揺らぎを加えます。検証用(print(val_tf))は、短い辺を 235 に縮小して中央の 224 × 224 を切り抜くだけです(235 = 224 ÷ crop_pct の 0.95)。
2 段階で学習する
頭と体のパラメーターを分けておき、段階ごとに学習するものを切り替えます。オプティマイザーに パラメーターのグループ を渡すと、グループごとに学習率を変えられます。timm の CosineLRScheduler は、グループごとの学習率を同じ割合で上げ下げします。
学習率は、エポックごとではなく バッチごと に更新します(t_in_epochs=False と step_update)。エポックごとに更新すると、1 エポックのウォームアップのあいだずっと学習率が初期値の 1e-6 のままになり、そのエポックがほとんど無駄になるためです。
検証用のデータで正解率がいちばん良かったときのモデルを、クラス名・モデル名といっしょに保存します。
train.py(後半):
loss_fn = nn.CrossEntropyLoss(label_smoothing=CFG["label_smoothing"])
head_params = list(model.get_classifier().parameters())
head_ids = {id(p) for p in head_params}
body_params = [p for p in model.parameters() if id(p) not in head_ids]
def evaluate():
model.eval()
loss_sum = correct = 0
with torch.no_grad():
for x, y in val_dl:
x, y = x.to(device), y.to(device)
logits = model(x)
loss_sum += loss_fn(logits, y).item() * len(y)
correct += (logits.argmax(1) == y).sum().item()
return loss_sum / len(val_ds), correct / len(val_ds)
best_acc = 0.0
def run_stage(name, optimizer, epochs, freeze_body):
global best_acc
for p in body_params:
p.requires_grad = not freeze_body
# 学習率はバッチごとに更新する(t_in_epochs=False)
steps = len(train_dl)
sched = CosineLRScheduler(optimizer, t_initial=epochs * steps,
warmup_t=0 if freeze_body else CFG["warmup_epochs"] * steps,
warmup_lr_init=1e-6, t_in_epochs=False)
for epoch in range(epochs):
t0 = time.time()
# 体を固定するときは eval モードにして、BatchNorm の統計を更新しない
model.train(not freeze_body)
loss_sum = n = 0
for i, (x, y) in enumerate(train_dl):
x, y = x.to(device), y.to(device)
loss = loss_fn(model(x), y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
sched.step_update(epoch * steps + i + 1)
loss_sum += loss.item() * len(y)
n += len(y)
val_loss, val_acc = evaluate()
print(f"[{name}] epoch {epoch + 1}/{epochs} train_loss {loss_sum / n:.3f} "
f"val_loss {val_loss:.3f} val_acc {val_acc:.3f} ({time.time() - t0:.0f}s)", flush=True)
if val_acc > best_acc:
best_acc = val_acc
Path(CFG["out"]).parent.mkdir(parents=True, exist_ok=True)
torch.save({"model": CFG["model"], "classes": classes,
"state_dict": model.state_dict()}, CFG["out"])
# 段階 1: 体を固定して、新しい頭だけを学習する
opt = torch.optim.AdamW(head_params, lr=CFG["head_lr"], weight_decay=CFG["weight_decay"])
run_stage("head", opt, CFG["head_epochs"], freeze_body=True)
# 段階 2: 全体を微調整する。体の学習率は頭より小さくする
opt = torch.optim.AdamW([
{"params": body_params, "lr": CFG["body_lr"]},
{"params": head_params, "lr": CFG["head_lr"]},
], weight_decay=CFG["weight_decay"])
run_stage("full", opt, CFG["full_epochs"], freeze_body=False)
print(f"best val_acc {best_acc:.3f}")
学習の結果
python train.py
実行結果:
classes=['daisy', 'dandelion', 'roses', 'sunflowers', 'tulips'] train=2939 val=731 device=cpu
[head] epoch 1/2 train_loss 1.251 val_loss 1.008 val_acc 0.793 (143s)
[head] epoch 2/2 train_loss 1.010 val_loss 0.955 val_acc 0.815 (111s)
[full] epoch 1/6 train_loss 1.048 val_loss 0.783 val_acc 0.844 (219s)
[full] epoch 2/6 train_loss 0.803 val_loss 0.659 val_acc 0.884 (230s)
[full] epoch 3/6 train_loss 0.726 val_loss 0.637 val_acc 0.891 (221s)
[full] epoch 4/6 train_loss 0.688 val_loss 0.622 val_acc 0.903 (224s)
[full] epoch 5/6 train_loss 0.669 val_loss 0.615 val_acc 0.902 (216s)
[full] epoch 6/6 train_loss 0.667 val_loss 0.618 val_acc 0.906 (215s)
best val_acc 0.906
ログの読み方です。
- 頭だけの学習で、すでに 0.8 を超える。新しい頭の 2565 個(512 × 5 + 5)の重みを 2 エポック学習しただけで 0.815 です。ImageNet で学習した特徴が、花の見分けにもそのまま役立っていることが分かります
- 全体の微調整で、さらに 0.9 まで上がる。体の特徴が花の写真に合わせて少しずつ調整されるためです。4 エポック目からはほぼ横ばいなので、この設定ではこれ以上エポックを増やしても大きな改善は見込めません
- train_loss は 0 まで下がらない。ラベルスムージングで正解のクラスの目標を 1 ではなく 0.92 にしているためで、5 クラス・0.1 のときの損失の下限は約 0.39 です。train_loss と val_loss の差が小さいので、過学習もしていません
- 1 エポックの時間は、全体の微調整が頭だけの約 2 倍。頭だけのときは体の勾配を計算しないためです
正解率が足りないときは、上から順に試します。
- エポック数を増やす(val_acc がまだ上がっているとき)
- 体の学習率を 3e-5・3e-4 と前後に振る
- 大きいモデル(
convnext_tiny.fb_in22k_ft_in1kなど)に替える - 間違えた検証用の画像を見て、ラベルの間違い・紛らわしい画像がないか確かめる
保存したモデルで推論する
保存したファイルには、モデル名とクラス名も入れてあるので、推論のスクリプトはそれだけで同じ形のモデルを作り直せます。重みは保存したものを読むので pretrained=False にします。推論の前に model.eval() にし、検証用と同じ前処理 をかけます。
predict.py:
import sys
import timm
import torch
from PIL import Image
ckpt = torch.load("outputs/best.pt", map_location="cpu")
model = timm.create_model(ckpt["model"], pretrained=False, num_classes=len(ckpt["classes"]))
model.load_state_dict(ckpt["state_dict"])
model.eval()
data_cfg = timm.data.resolve_data_config({}, model=model)
tf = timm.data.create_transform(**data_cfg) # 検証用と同じ前処理
for path in sys.argv[1:]:
img = Image.open(path).convert("RGB")
with torch.no_grad():
prob = model(tf(img).unsqueeze(0)).softmax(1)[0]
top = prob.topk(3)
print(path, ", ".join(f"{ckpt['classes'][i]} {p:.3f}" for p, i in zip(top.values, top.indices)))
検証用の画像を 3 枚渡して試します。
python predict.py data/flowers/val/daisy/102841525_bd6628ae3c.jpg data/flowers/val/roses/102501987_3cdb8e5394_n.jpg data/flowers/val/tulips/10128546863_8de70c610d.jpg
実行結果:
data/flowers/val/daisy/102841525_bd6628ae3c.jpg daisy 0.992, tulips 0.003, sunflowers 0.002
data/flowers/val/roses/102501987_3cdb8e5394_n.jpg roses 0.987, tulips 0.009, sunflowers 0.002
data/flowers/val/tulips/10128546863_8de70c610d.jpg tulips 0.986, roses 0.008, daisy 0.003
保存したファイル(outputs/best.pt)は約 45 MB でした。
ハマりどころ
前処理が事前学習のときと違う
正規化の値や入力の大きさが事前学習のときと違うと、学習済みの特徴がうまく働かず、精度が大きく下がります。エラーは出ないので気づきにくい間違いです。前処理を手で書くときも、値は resolve_data_config の結果から取ります。
体を固定したのに BatchNorm の統計が変わる
ResNet などの BatchNorm の層は、model.train() のあいだ、入力の平均と分散の統計(running mean / var)を更新し続けます。これは requires_grad = False にしても止まりません。体を固定して頭だけを学習するときは、model.eval() のまま学習します。model.eval() でも勾配の計算と重みの更新はふつうにできます。
推論のときに model.eval() を忘れる
model.train() のままだと、BatchNorm がバッチの統計を使うため、1 枚ずつ推論すると結果がおかしくなります。Dropout があるモデルでは、毎回結果も変わります。
クラスの番号がずれる
ImageFolder の番号はフォルダー名の辞書順です。train と val でフォルダー名が違ったり、片方にしかないクラスがあったりすると、番号がずれます。スクリプトでは assert で両方のクラス名が一致することを確かめ、推論のためにクラス名をモデルといっしょに保存しています。
まとめ
- timm の
create_model(..., pretrained=True, num_classes=N)で、学習済みの体に新しい頭を付けたモデルが作れる。モデル名を替えるだけで、別のアーキテクチャも試せる - データセットは
train/<クラス名>/・val/<クラス名>/のフォルダーで分けておくと、ImageFolderでそのまま読め、検証用の画像も固定できる - 前処理は
resolve_data_configとcreate_transformで、モデルの事前学習の設定から作る - 学習は「頭だけ → 全体」の 2 段階。AdamW で頭 1e-3・体 1e-4、ウォームアップとコサインのスケジュールが出発点になる
- 推論では
model.eval()にして、検証用と同じ前処理をかける
参考
- timm のドキュメント(huggingface.co/docs/timm)
- PyTorch Tutorials, "Transfer Learning for Computer Vision Tutorial"(pytorch.org)
- torchvision, "ImageFolder"(docs.pytorch.org)
- TensorFlow Tutorials, "Load and preprocess images"(花の写真のデータセット、tensorflow.org)
- K. He et al., "Deep Residual Learning for Image Recognition", CVPR 2016(arXiv:1512.03385)
- R. Wightman et al., "ResNet strikes back: An improved training procedure in timm", 2021(arXiv:2110.00476)
- A. Kumar et al., "Fine-Tuning can Distort Pretrained Features and Underperform Out-of-Distribution", ICLR 2022(arXiv:2202.10054)
- I. Loshchilov and F. Hutter, "Decoupled Weight Decay Regularization", ICLR 2019(AdamW、arXiv:1711.05101)