# Log12 予備実験:PatchCore(WideResNet50)で配膳ロボットの長い止まりを見つけられるか # 覚えさせるのは「普段」(往復の途中で走っている所)の切り出しだけ。 # 確かめは、別の回の 普段・短い止まり(3〜30秒)・長い止まり(30秒以上、始まりから15秒後)。 # 使い方: python3 patchcore.py <結果のフォルダ> import sys, os, glob, json, csv, random import numpy as np, torch, torch.nn.functional as F from torchvision.models import wide_resnet50_2, Wide_ResNet50_2_Weights from PIL import Image from sklearn.metrics import roc_auc_score root, kind, out = sys.argv[1], sys.argv[2], sys.argv[3] os.makedirs(out, exist_ok=True) torch.manual_seed(0); random.seed(0); np.random.seed(0) torch.set_num_threads(os.cpu_count()) model = wide_resnet50_2(weights=Wide_ResNet50_2_Weights.IMAGENET1K_V1).eval() feats = {} model.layer2.register_forward_hook(lambda m, i, o: feats.__setitem__('l2', o)) model.layer3.register_forward_hook(lambda m, i, o: feats.__setitem__('l3', o)) MEAN = torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1) STD = torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1) def load(paths): x = np.stack([np.asarray(Image.open(p).convert('RGB'), dtype=np.float32) / 255 for p in paths]) return (torch.from_numpy(x).permute(0, 3, 1, 2) - MEAN) / STD @torch.no_grad() def embed(paths, bs=16): """1枚を 28x28 の区画に分け、区画ごとの特徴(layer2+layer3、3x3でならす)を返す""" outs = [] for i in range(0, len(paths), bs): model(load(paths[i:i + bs])) l2 = F.avg_pool2d(feats['l2'], 3, 1, 1) l3 = F.interpolate(F.avg_pool2d(feats['l3'], 3, 1, 1), size=l2.shape[-2:], mode='bilinear', align_corners=False) outs.append(torch.cat([l2, l3], 1)) # (b, 1536, 28, 28) return torch.cat(outs) def coreset(X, n, proj_dim=128): """貪欲法で代表の区画だけ残す(PatchCore の記憶を小さくする)""" P = X @ torch.randn(X.shape[1], proj_dim) / proj_dim ** 0.5 idx = [random.randrange(len(X))] d = torch.cdist(P, P[idx]).squeeze(1) for _ in range(n - 1): j = int(torch.argmax(d)); idx.append(j) d = torch.minimum(d, torch.cdist(P, P[j:j + 1]).squeeze(1)) return X[idx] def score(paths, bank, bs=8): """区画ごとに、記憶の中で一番近い区画までの距離。1枚の点数は最大値。地図も返す""" sc, maps = [], [] for i in range(0, len(paths), bs): E = embed(paths[i:i + bs]); b, c, h, w = E.shape Q = E.permute(0, 2, 3, 1).reshape(-1, c) dmin = torch.cat([torch.cdist(Q[k:k + 4096], bank).min(1).values for k in range(0, len(Q), 4096)]) m = dmin.view(b, h, w) sc += m.flatten(1).max(1).values.tolist(); maps.append(m) return np.array(sc), torch.cat(maps).numpy() d = f'{root}/{kind}' train = sorted(glob.glob(f'{d}/train/normal/*.png')) test = {lab: sorted(glob.glob(f'{d}/test/{lab}/*.png')) for lab in ('normal', 'short', 'long')} print(kind, 'train', len(train), {k: len(v) for k, v in test.items()}, flush=True) # 区画を全部持つとメモリが足りないので、1枚あたり200区画を無作為に残してから代表を選ぶ Xs = [] for i in range(0, len(train), 16): E = embed(train[i:i + 16]); c = E.shape[1] Q = E.permute(0, 2, 3, 1).reshape(E.shape[0], -1, c) for q in Q: Xs.append(q[torch.randperm(q.shape[0])[:200]]) X = torch.cat(Xs); n_all = len(train) * 28 * 28 bank = coreset(X, int(n_all * 0.01)) print('bank', tuple(bank.shape), flush=True) res, maps = {}, {} for lab, ps in test.items(): res[lab], maps[lab] = score(ps, bank) def auc(a, b): return float(roc_auc_score([0] * len(a) + [1] * len(b), np.r_[a, b])) # しきい値は 普段 の確かめ画像の 95% 点(普段のうち5%を誤報にする置き方) thr = float(np.quantile(res['normal'], 0.95)) summary = { 'kind': kind, 'n_train': len(train), 'n_test': {k: len(v) for k, v in test.items()}, 'bank': list(bank.shape), 'auroc_long_vs_normal': auc(res['normal'], res['long']), 'auroc_short_vs_normal': auc(res['normal'], res['short']), 'auroc_long_vs_short': auc(res['short'], res['long']), 'threshold_p95_normal': thr, 'hit_rate_long_at_thr': float((res['long'] > thr).mean()), 'hit_rate_short_at_thr': float((res['short'] > thr).mean()), 'false_alarm_normal_at_thr': float((res['normal'] > thr).mean()), 'score_median': {k: float(np.median(v)) for k, v in res.items()}, } print(json.dumps(summary, ensure_ascii=False, indent=1), flush=True) json.dump(summary, open(f'{out}/summary_{kind}.json', 'w'), ensure_ascii=False, indent=1) with open(f'{out}/scores_{kind}.csv', 'w', newline='') as fo: w = csv.writer(fo); w.writerow(['label', 'file', 'score']) for lab, ps in test.items(): for p, s in zip(ps, res[lab]): w.writerow([lab, os.path.basename(p), round(float(s), 4)]) np.savez_compressed(f'{out}/maps_{kind}.npz', **{lab: maps[lab] for lab in maps}, **{f'{lab}_files': np.array([os.path.basename(p) for p in ps]) for lab, ps in test.items()})