はじめに
新しくChatGPT-5 が出てきたので、画像認識させるコードを吐き出させてみました。
パラメータをゼロから学習させるのは画像が絶対に足りないので、転移学習を前提に試します。
プロンプト
最初に与えたプロンプトはこちら。
python で転移学習を行うプログラムを作成してください。
- ベースとなるパラメータはImageNetを使用
- モデルはResNet50 を使用
- epoch 毎に、考査検定による学習結果の妥当性を画面に表示すること
- 学習データは学習データディレクトリにまとめられている
- 各クラスタには名前が付けられており、学習データディレクトリの配下にあるディレクトリ(名前はクラスタ名)の中に学習データとなる画像ファイルが格納されている
ちなみに「考査検定」ではなく「交差検定」が正しいのですが、誤字のままプロンプトを放り込んでしまいました…。ちゃんと空気を読んでくれるのかな…?
生成結果
了解です。以下は、PyTorch を使って ResNet50(ImageNet 事前学習済み)で転移学習を行うシンプルな学習スクリプトです。
ポイント
- データは data_dir/クラス名/*.jpg の ImageFolder 形式 を想定
- 検証用データは、--val_split で学習データから分割。(data_dir/val が存在すればそれを優先)。
- 各 epoch 毎に妥当性(検証損失・精度と任意で classification_report)を表示。
- 最良検証精度を更新したモデルを best_model.pth に保存。
- まず全層学習(fine-tuning)ですが、--freeze_backbone を付けるとバックボーンを凍結して全結合だけ学習に切り替え可。
生成コード
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
import os
import time
import copy
from pathlib import Path
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import random_split, DataLoader, Subset
from torchvision import datasets, transforms, models
try:
from sklearn.metrics import classification_report, confusion_matrix
HAS_SKLEARN = True
except Exception:
HAS_SKLEARN = False
IMAGENET_MEAN = [0.485, 0.456, 0.406]
IMAGENET_STD = [0.229, 0.224, 0.225]
def build_dataloaders(data_dir: str, batch_size: int, val_split: float, num_workers: int):
"""
data_dir は ImageFolder 形式を想定:
data_dir/
├─ classA/ img1.jpg ...
├─ classB/ ...
もし data_dir/val が存在する場合は train/val を分離しているものとして利用。
"""
data_dir = Path(data_dir)
# 変換
train_tfms = transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD),
])
eval_tfms = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD),
])
train_dir = data_dir / "train"
val_dir = data_dir / "val"
if train_dir.exists() and val_dir.exists():
train_ds = datasets.ImageFolder(str(train_dir), transform=train_tfms)
val_ds = datasets.ImageFolder(str(val_dir), transform=eval_tfms)
class_names = train_ds.classes
else:
# 単一ディレクトリを train/val に分割
full_ds = datasets.ImageFolder(str(data_dir), transform=None) # transform は後で付け替え
class_names = full_ds.classes
n_total = len(full_ds)
n_val = int(n_total * val_split)
n_train = n_total - n_val
# 再現性のため固定シード
generator = torch.Generator().manual_seed(42)
train_idx, val_idx = random_split(range(n_total), [n_train, n_val], generator=generator)
train_ds = copy.deepcopy(full_ds)
val_ds = copy.deepcopy(full_ds)
# transform を個別に設定
train_ds.transform = train_tfms
val_ds.transform = eval_tfms
# Subset に差し替え
train_ds = Subset(train_ds, train_idx.indices if hasattr(train_idx, 'indices') else train_idx)
val_ds = Subset(val_ds, val_idx.indices if hasattr(val_idx, 'indices') else val_idx)
# Subset では classes 属性がないため、あとで class_names を渡す
train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True,
num_workers=num_workers, pin_memory=True)
val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False,
num_workers=num_workers, pin_memory=True)
return train_loader, val_loader, class_names
def build_model(num_classes: int, freeze_backbone: bool = False):
# PyTorch 2.0+ 推奨 API。古い環境では weights='IMAGENET1K_V2' 等に読み替え
weights = models.ResNet50_Weights.IMAGENET1K_V2
model = models.resnet50(weights=weights)
if freeze_backbone:
for p in model.parameters():
p.requires_grad = False
# 全結合層をクラス数に合わせる
in_features = model.fc.in_features
model.fc = nn.Linear(in_features, num_classes)
return model
def train_one_epoch(model, loader, device, criterion, optimizer):
model.train()
running_loss, running_corrects, n = 0.0, 0, 0
for inputs, labels in loader:
inputs = inputs.to(device, non_blocking=True)
labels = labels.to(device, non_blocking=True)
optimizer.zero_grad(set_to_none=True)
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
_, preds = torch.max(outputs, 1)
running_loss += loss.item() * inputs.size(0)
running_corrects += torch.sum(preds == labels).item()
n += inputs.size(0)
epoch_loss = running_loss / n
epoch_acc = running_corrects / n
return epoch_loss, epoch_acc
@torch.no_grad()
def evaluate(model, loader, device, criterion, class_names):
model.eval()
running_loss, running_corrects, n = 0.0, 0, 0
all_preds = []
all_labels = []
for inputs, labels in loader:
inputs = inputs.to(device, non_blocking=True)
labels = labels.to(device, non_blocking=True)
outputs = model(inputs)
loss = criterion(outputs, labels)
_, preds = torch.max(outputs, 1)
running_loss += loss.item() * inputs.size(0)
running_corrects += torch.sum(preds == labels).item()
n += inputs.size(0)
all_preds.append(preds.cpu())
all_labels.append(labels.cpu())
epoch_loss = running_loss / n
epoch_acc = running_corrects / n
all_preds = torch.cat(all_preds).numpy()
all_labels = torch.cat(all_labels).numpy()
report = None
cm = None
if HAS_SKLEARN:
report = classification_report(all_labels, all_preds, target_names=class_names, digits=4)
cm = confusion_matrix(all_labels, all_preds)
return epoch_loss, epoch_acc, report, cm
def main():
parser = argparse.ArgumentParser(description="Transfer Learning with ResNet50 (ImageNet pretrained)")
parser.add_argument("--data_dir", type=str, required=True,
help="学習データディレクトリ (ImageFolder 形式)")
parser.add_argument("--epochs", type=int, default=20)
parser.add_argument("--batch_size", type=int, default=32)
parser.add_argument("--lr", type=float, default=1e-3)
parser.add_argument("--weight_decay", type=float, default=1e-4)
parser.add_argument("--val_split", type=float, default=0.2,
help="単一ディレクトリからの検証分割比率 (train/val が分かれている場合は無視)")
parser.add_argument("--freeze_backbone", action="store_true",
help="ResNet のバックボーンを凍結して線形層のみ学習")
parser.add_argument("--num_workers", type=int, default=max(2, (os.cpu_count() or 4) // 2))
parser.add_argument("--save_path", type=str, default="best_model.pth")
args = parser.parse_args()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"[INFO] device = {device}")
train_loader, val_loader, class_names = build_dataloaders(
args.data_dir, args.batch_size, args.val_split, args.num_workers
)
if len(class_names) == 0:
# class_names がうまく取れない場合 (Subset 経由時の保険)
if hasattr(train_loader.dataset, 'dataset') and hasattr(train_loader.dataset.dataset, 'classes'):
class_names = train_loader.dataset.dataset.classes
else:
# 最終手段
class_names = sorted([d.name for d in Path(args.data_dir).glob("*") if d.is_dir()])
num_classes = len(class_names)
print(f"[INFO] classes ({num_classes}): {class_names}")
model = build_model(num_classes=num_classes, freeze_backbone=args.freeze_backbone).to(device)
# 凍結時は fc のみ最適化、そうでなければ全層
if args.freeze_backbone:
params_to_update = [p for p in model.fc.parameters() if p.requires_grad]
else:
params_to_update = [p for p in model.parameters() if p.requires_grad]
criterion = nn.CrossEntropyLoss()
optimizer = optim.AdamW(params_to_update, lr=args.lr, weight_decay=args.weight_decay)
lr_scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=args.epochs)
best_acc = 0.0
best_wts = copy.deepcopy(model.state_dict())
for epoch in range(1, args.epochs + 1):
t0 = time.time()
print(f"\n===== Epoch {epoch}/{args.epochs} =====")
train_loss, train_acc = train_one_epoch(model, train_loader, device, criterion, optimizer)
val_loss, val_acc, report, cm = evaluate(model, val_loader, device, criterion, class_names)
lr_scheduler.step()
dt = time.time() - t0
# 画面に妥当性(検証指標)を表示
print(f"[Train] loss: {train_loss:.4f} acc: {train_acc:.4f}")
print(f"[Valid] loss: {val_loss:.4f} acc: {val_acc:.4f} (elapsed {dt:.1f}s)")
if HAS_SKLEARN and report is not None:
print("\n[Valid] Classification Report:")
print(report)
print("[Valid] Confusion Matrix:")
print(cm)
# ベスト更新で保存
if val_acc > best_acc:
best_acc = val_acc
best_wts = copy.deepcopy(model.state_dict())
torch.save({
"model_state": best_wts,
"classes": class_names,
"val_acc": best_acc,
"epoch": epoch,
"arch": "resnet50"
}, args.save_path)
print(f"[INFO] Best model updated and saved to: {args.save_path} (acc={best_acc:.4f})")
# 最良重みで終了
model.load_state_dict(best_wts)
print(f"\n[RESULT] Best validation accuracy: {best_acc:.4f}")
print("[DONE]")
if __name__ == "__main__":
main()
備考
- 「epoch 毎の考査検定による妥当性表示」は、検証セットでの損失・精度に加え、scikit-learn が入っていれば classification_report / 混同行列も表示します(入っていなければ損失・精度のみ表示)。
- 画像サイズは ResNet50 標準の 224×224 に合わせて前処理しています(ImageNet の平均・分散で正規化)。
- 最良モデルは best_model.pth に保存され、classes も一緒に保存します。推論時は保存された classes を使ってラベル名を復元できます。
- 必要に応じて、早期終了(EarlyStopping)や学習率の微調整、より強いデータ拡張の追加なども組み込めます。
とりあえず学習させてみる
生成されたコードをそのまま動かしてみました。
動かしたときの環境は下記のとおり。
- Python 3.13.7
- AMD Ryzen 9 3900X 12-Core
- Mem: 64GB
- グラボ:RTX5060Ti(16GB)
念のため、python のモジュールはこちら(関係のないものが混じってるかも)。
certifi==2025.8.3
charset-normalizer==3.4.2
coloredlogs==15.0.1
filelock==3.18.0
flatbuffers==25.2.10
fsspec==2025.7.0
humanfriendly==10.0
idna==3.10
Jinja2==3.1.6
MarkupSafe==3.0.2
mpmath==1.3.0
networkx==3.5
numpy==2.2.6
onnx==1.18.0
onnxruntime==1.22.1
opencv-python==4.12.0.88
packaging==25.0
pandas==2.3.1
pillow==11.3.0
protobuf==6.31.1
pyreadline3==3.5.4
python-dateutil==2.9.0.post0
pytz==2025.2
requests==2.32.4
setuptools==80.9.0
six==1.17.0
sympy==1.14.0
torch==2.8.0+cu129
torchaudio==2.8.0+cu129
torchvision==0.23.0+cu129
typing_extensions==4.14.1
tzdata==2025.2
urllib3==2.5.0
何はともあれぽちっとな。
$ python ./trainer.py --data_dir ./training_images
学習でEpoch 回しながらパラメータの評価結果も出力せよと書いてあったのも無事反映されています。
クラスタの数はとりあえず2つだけです。
Downloading: "https://download.pytorch.org/models/resnet50-11ad3fa6.pth" to C:\Users\azara/.cache\torch\hub\checkpoints\resnet50-11ad3fa6.pth
100.0%
===== Epoch 1/20 =====
[Train] loss: 0.5953 acc: 0.7750
[Valid] loss: 0.1744 acc: 1.0000 (elapsed 11.9s)
[INFO] Best model updated and saved to: best_model.pth (acc=1.0000)
===== Epoch 2/20 =====
[Train] loss: 0.1521 acc: 1.0000
[Valid] loss: 0.0149 acc: 1.0000 (elapsed 11.4s)
===== Epoch 3/20 =====
[Train] loss: 0.0557 acc: 1.0000
[Valid] loss: 0.0012 acc: 1.0000 (elapsed 11.4s)
...
学習はできているようで、損失(loss)・精度(acc)いずれも学習が進むたびに改善していっていることが分かります。
おわりに
実はこの後さらにONNX 出力やら「バックボーン凍結で学習 → 一度精度を出してから徐々にバックボーンを解凍して fine-tuning」といったパラメータの最適化などもやっていきましたが、全部書くとものすごく長くなりそうだったので、記事の方はいったんここで止めておきます。
初学者が学習の道具として使ったり、たまに触るくらいの人が知識を深堀したりと、勉強する環境として本当に便利になりました。
とはいえ、ハルシネーションには注意が必要なので、要所要所での検証作業は忘れずに。