督學習實戰(zhàn):在TinyImageNet上復現(xiàn)對比學習與表征評估)
簡介基于SimCLR自監(jiān)督學習與監(jiān)督學習的對比研究項目完整代碼包面向計算機視覺方向的研究者與學習者重點演示在TinyImageNet數(shù)據(jù)集上通過對比學習預訓練ResNet18再遷移至圖像分類任務的完整流程適用于無標注或弱標注場景下的特征學習實驗。包內(nèi)含35個文件以10個Python腳本為核心覆蓋數(shù)據(jù)目錄重組、自定義DataLoader含CutMix增強、模型定義、訓練與測試、超參數(shù)調(diào)優(yōu)等環(huán)節(jié)另配有15個SVG可視化圖表便于理解網(wǎng)絡結構與訓練曲線輔以說明文本和Markdown文檔整體約580KB。已有57人學習可作為復現(xiàn)SimCLR實驗、對比自監(jiān)督與監(jiān)督學習性能差異的入門參考。借助代碼與注釋可快速搭建實驗環(huán)境觀察預訓練特征在下游分類任務中的表現(xiàn)并參考參數(shù)調(diào)整思路優(yōu)化模型效果。1. 自監(jiān)督學習的復現(xiàn)價值SimCLR 在 TinyImageNet 上先于監(jiān)督學習跑出可用的表征先說一個反直覺的結論在 TinyImageNet 這種 200 類、每類只有 500 張訓練圖的小規(guī)模數(shù)據(jù)集上SimCLR 自監(jiān)督預訓練出來的 ResNet18 特征經(jīng)過線性評估后能逼近甚至打平端到端監(jiān)督訓練的結果但它完全不需要任何標簽。這個項目就是干這件事的實現(xiàn) SimCLR 對比學習算法在 TinyImageNet 上預訓練 ResNet18再和監(jiān)督學習做公平對比。它適合兩類人一類是做圖像分類但標注數(shù)據(jù)不夠的從業(yè)者想看看自監(jiān)督能不能緩解標注壓力另一類是剛開始接觸對比學習、想從零跑通一個自監(jiān)督實驗的學生。這篇筆記我會把 SimCLR 的關鍵機制、完整可復現(xiàn)代碼、監(jiān)督對照組的設計、以及我在復現(xiàn)過程中踩過的坑一次講透讀完你不僅能跑通還能自己改參數(shù)做實驗。2. SimCLR 的三個關鍵旋鈕數(shù)據(jù)增強、投影頭與溫度系數(shù)改一個都會翻車SimCLR 的原理表面看很簡單把一張圖做兩次隨機增強得到正樣本對然后讓模型學會把同一張圖的兩個增強版本在特征空間里拉近把其他圖的增強版本推遠。但真正決定實驗成敗的是三個看起來很小、實際影響巨大的設計決策。這一章我先講清楚這三個決策為什么這么重要再給出在 TinyImageNet 場景下的推薦配置。2.1 為什么選 TinyImageNet 而不是 CIFAR 或完整 ImageNetTinyImageNet 是 ImageNet 的一個小型鏡像200 個類別每類 500 張訓練圖、50 張驗證圖分辨率統(tǒng)一縮到 64×64。這個規(guī)模對自監(jiān)督實驗來說是性價比最高的選擇完整 ImageNet 上 ResNet50 的 SimCLR 預訓練需要幾百張 GPU 卡跑幾天CIFAR-10 類別太少、圖像太簡單自監(jiān)督相對監(jiān)督學習的優(yōu)勢體現(xiàn)不出來。TinyImageNet 則能在 1-2 張 GPU 上完成預訓練同時因為類別多、圖像內(nèi)容復雜能真實反映表征質(zhì)量的好壞。我一般會先跑 TinyImageNet 而不是直接上 ImageNet原因很實際SimCLR 的感受野、增強強度、溫度系數(shù)這些超參數(shù)在 64×64 分辨率下的最優(yōu)配置跟 224×224 完全不同用 TinyImageNet 做 warmup能在兩小時內(nèi)驗證整套代碼邏輯是否正確再遷移到大分辨率數(shù)據(jù)集時就只需要調(diào)增強參數(shù)不用重新排查代碼 bug。2.2 增強組合隨機裁剪是主心骨顏色失真是勝負手SimCLR 論文里最反直覺的發(fā)現(xiàn)是在 ImageNet 上用隨機裁剪效果一般但隨機裁剪 顏色失真色彩抖動、灰度化的組合效果暴漲。原因是單看顏色直方圖模型很容易找到捷徑——兩張增強后的圖只要顏色分布相似就直接拉近根本不需要學習形狀和語義特征。顏色失真打掉了這條捷徑逼著模型去學結構信息。在 TinyImageNet 上這個規(guī)律同樣成立而且由于 64×64 分辨率更低隨機裁剪的尺度范圍需要專門調(diào)整。import torchvision.transforms as T simclr_augment T.Compose([ T.RandomResizedCrop(64, scale(0.2, 1.0)), T.RandomHorizontalFlip(p0.5), T.ColorJitter(0.4, 0.4, 0.4, 0.1, p0.8), T.RandomGrayscale(p0.2), T.GaussianBlur(kernel_size(3, 3), sigma(0.1, 2.0)), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])RandomResizedCrop的scale(0.2, 1.0)表示裁剪區(qū)域占原圖的 20% 到 100%這是 SimCLR 原論文的推薦值但對于 64×64 的輸入要小心如果裁到 20% 再 resize 回 64有效分辨率只剩約 28×28語義信息會損失嚴重。GaussianBlur的核大小我通常會設成 3×3 而不是 ImageNet 上常用的 9×9因為 64×64 圖像本身細節(jié)就少過大的模糊核會把所有紋理都抹掉。顏色失真強度0.4是一個比較穩(wěn)妥的起點太強會讓圖像顏色失真到影響人類判讀太弱則無法有效防止捷徑學習。2.3 投影頭和 NT-Xent 損失表征在哪里被拉近SimCLR 的另一個關鍵設計是在 backbone 的 512 維特征后面接一個投影頭Projection Head把特征映射到 128 維空間后再計算對比損失。訓練完成后丟掉投影頭只用 backbone 的特征做下游任務。為什么要多此一舉因為 512 維特征里有太多和實例區(qū)分無關的信息比如背景、紋理、光照方向這些信息在對比損失里會成為噪聲干擾拉近推遠的梯度。128 維的投影空間相當于一個瓶頸把無關信息過濾掉讓對比損失只作用于真正有用的語義特征。import torch import torch.nn as nn import torch.nn.functional as F class ProjectionHead(nn.Module): def __init__(self, in_dim512, hidden_dim2048, out_dim128): super().__init__() self.fc1 nn.Linear(in_dim, hidden_dim) self.fc2 nn.Linear(hidden_dim, out_dim) def forward(self, x): x F.relu(self.fc1(x)) return self.fc2(x) class NTXentLoss(nn.Module): def __init__(self, temperature0.5): super().__init__() self.temperature temperature def forward(self, z1, z2): batch_size z1.shape[0] z torch.cat([z1, z2], dim0) # 2N x D z F.normalize(z, dim1) sim z z.T / self.temperature # 2N x 2N mask torch.eye(2 * batch_size, devicez.device).bool() sim.masked_fill_(mask, -1e9) # 正樣本對i 與 iN互為增強對 positive torch.cat([ torch.diag(sim[batch_size:, :batch_size]), torch.diag(sim[:batch_size, batch_size:]) ]) negative sim.masked_fill( torch.eye(2 * batch_size, devicez.device).bool(), -1e9 ).sum(dim1) return (-torch.log(positive.exp() / negative.exp())).mean()投影頭的結構我用兩層 MLP中間維度 2048、輸出 128這個配置來自 SimCLR 原論文在小規(guī)模數(shù)據(jù)集上同樣適用。NT-Xent 損失的核心是溫度系數(shù)temperature它控制對困難負樣本的懲罰強度。溫度越低模型越關注與當前樣本相似度最高的負樣本訓練越激進溫度越高所有負樣本的懲罰趨于均勻訓練越平穩(wěn)。我常用的起點是temperature0.5這是 SimCLR 原文在 ImageNet 上的最優(yōu)值。損失函數(shù)里positive.exp() / negative.exp()實際上是 InfoNCE 的變體用 softmax 的形式計算正樣本對在 batch 內(nèi)所有樣本對中的占比。這里有一個新手常犯的錯把masked_fill的位置寫錯導致對角線上的自身與自身的相似度沒有被屏蔽損失會被嚴重低估。我習慣在實現(xiàn)后做一次單元測試輸入隨機 tensor檢查 loss 是否為正數(shù)、是否隨 batch 大小單調(diào)變化。這個檢查只需要十秒鐘能省掉后面兩小時的排錯時間。3. 復現(xiàn)細節(jié)從 TinyImageNet 原始目錄到 NT-Xent 損失完整跑通預訓練與線性評估這一章直接給可復現(xiàn)的完整流程。我默認你已經(jīng)有 PyTorch 環(huán)境我使用 PyTorch 2.x CUDA 11.8 單張 RTX 3090代碼里會標注每部分的作用和可調(diào)參數(shù)。整個流程分四步準備數(shù)據(jù)、搭建增強與模型、預訓練、線性評估。3.1 數(shù)據(jù)加載TinyImageNet 的目錄結構與標簽映射TinyImageNet 的官方 zip 包解壓后train/文件夾下是 200 個類別的子文件夾每個子文件夾名是一個像n01443537這樣的 WordNet ID里面放著對應類別的圖片val/文件夾下是 10000 張驗證圖圖片名是val_100.JPEG這種格式類別信息在val_annotations.txt文件里。這里的第一個坑是train/下的類別子文件夾順序是按 WordNet ID 排序的但val_annotations.txt里的類別順序是按圖片名排序的如果不建立從 WordNet ID 到整數(shù)標簽的映射訓練集的 0-199 標簽和驗證集的對不上線性評估時會有接近 180 個類別的標簽完全錯位。import os import zipfile from urllib.request import urlretrieve # 下載并解壓 TinyImageNet if not os.path.exists(tiny-imagenet-200): urlretrieve(http://cs231n.stanford.edu/tiny-imagenet-200.zip, tiny-imagenet-200.zip) with zipfile.ZipFile(tiny-imagenet-200.zip) as zf: zf.extractall(.) # 構建 train 數(shù)據(jù)集 class TinyImageNetTrain(torch.utils.data.Dataset): def __init__(self, root, transform): self.transform transform self.images, self.labels [], [] class_dirs sorted(os.listdir(os.path.join(root, train))) self.class_to_idx {c: i for i, c in enumerate(class_dirs)} # 關鍵先排序再映射 for cls in class_dirs: cls_dir os.path.join(root, train, cls) for img_name in os.listdir(cls_dir): self.images.append(os.path.join(cls_dir, img_name)) self.labels.append(self.class_to_idx[cls]) def __len__(self): return len(self.images) def __getitem__(self, idx): img Image.open(self.images[idx]).convert(RGB) return self.transform(img), self.labels[idx]注意class_to_idx的構造順序我先把class_dirs排序再按排序后的順序建立映射保證訓練集和驗證集的標簽空間一致。如果直接用os.listdir返回的順序建映射Linux 和 macOS 的文件系統(tǒng)遍歷順序可能不同同一份代碼在兩個環(huán)境下跑出完全不同的標簽含義。訓練時我用DataLoader加載這個數(shù)據(jù)集每個 batch 里會一次性取出 2N 張圖N 張原圖N 張增強圖所以batch_size參數(shù)我習慣寫的是原圖的數(shù)量實際 GPU 顯存占用按 2 倍計算。3.2 增強 pipeline 與 backbone 改造backbone 直接用 torchvision 的 ResNet18去掉最后的全連接分類器把avgpool的輸出512 維特征作為投影頭的輸入。注意使用torchvision.models時要設置weightsNone不要加載 ImageNet 預訓練權重——自監(jiān)督實驗的干凈基線要求模型從頭訓練如果加載了預訓練權重對比學習的從零學習假設就不成立了監(jiān)督對照組的比較也會失去意義。import torchvision.models as models class SimCLRBackbone(nn.Module): def __init__(self): super().__init__() resnet models.resnet18(weightsNone) self.backbone nn.Sequential(*list(resnet.children())[:-2]) # 去掉 avgpool 和 fc self.projection ProjectionHead(in_dim512, hidden_dim2048, out_dim128) def forward(self, x, return_featureFalse): feat self.backbone(x) # B x 512 x 2 x 2 feat feat.mean(dim[2, 3]) # 全局平均池化 - B x 512 if return_feature: return feat return self.projection(feat)list(resnet.children())[:-2]去掉了avgpool和fc兩層backbone 輸出的張量形狀是B x 512 x 2 x 2因為在 64×64 輸入下 ResNet18 經(jīng)過四次下采樣特征圖縮小到 2×2。我用mean(dim[2, 3])做全局平均池化這比nn.AdaptiveAvgPool2d(1)更直觀效果完全等價。return_featureTrue時返回 backbone 特征而不是投影特征這個開關在后續(xù)線性評估和相似度檢索時會用到。3.3 對比損失與訓練循環(huán)預訓練的訓練循環(huán)主要工作是把同一 batch 的圖像做兩次獨立增強然后分別過模型得到兩個投影特征再計算 NT-Xent 損失。優(yōu)化器我使用 AdamW學習率按 batch 大小線性縮放lr base_lr * batch_size / 256base_lr 取 3e-4。有 LARS 優(yōu)化器的話更好但 AdamW 在小模型和中等 batch 下也能收斂差別大約在 1-2 個點以內(nèi)。from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR model SimCLRBackbone().cuda() optimizer AdamW(model.parameters(), lr3e-4 * batch_size / 256, weight_decay1e-6) scheduler CosineAnnealingLR(optimizer, T_max120) for epoch in range(120): model.train() total_loss 0 for x, _ in train_loader: # x: B x 3 x 64 x 64 x1, x2 simclr_augment(x), simclr_augment(x) # 兩次獨立增強 z1 model(x1.cuda()) z2 model(x2.cuda()) loss NTXentLoss(temperature0.5)(z1, z2) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() scheduler.step() if epoch % 10 0: torch.save(model.state_dict(), fcheckpoints/simclr_epoch{epoch}.pth)x1, x2 simclr_augment(x), simclr_augment(x)這行要特別注意simclr_augment是一個torchvision.transforms.Compose對象對同一 batch 調(diào)用兩次會產(chǎn)生不同的隨機增強結果這正是 SimCLR 需要的正樣本對。每次迭代要手動zero_grad()并loss.backward()這個順序?qū)懛磿е绿荻壤鄯e。CosineAnnealingLR的周期設為 120和總 epoch 數(shù)一致學習率會在最后收斂到一個極小值。如果顯存不足優(yōu)先檢查batch_size然后在optimizer.step()前添加torch.cuda.empty_cache()不解決根本問題但能緩解碎片化。3.4 線性評估協(xié)議凍結表征的監(jiān)督訓練預訓練之后真正驗證表征質(zhì)量的方式是線性評估把 backbone 的權重凍結只在 512 維特征后面接一個線性分類器在 TinyImageNet 訓練集上監(jiān)督訓練這個分類器。因為 backbone 不參與梯度更新這個實驗的訓練成本很低幾分鐘就能完成。這個評估協(xié)議之所以被稱為公平對比是因為它去掉了微調(diào)過程中 backbone 對任務適配的優(yōu)勢只考察預訓練特征本身的可分性。from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler def extract_features(model, loader): model.eval() feats, labels [], [] with torch.no_grad(): for x, y in loader: feat model(x.cuda(), return_featureTrue).cpu().numpy() feats.append(feat) labels.append(y.numpy()) return np.concatenate(feats), np.concatenate(labels) train_feats, train_labels extract_features(model, train_loader) val_feats, val_labels extract_features(model, val_loader) scaler StandardScaler().fit(train_feats) clf LogisticRegression(C0.1, max_iter1000) clf.fit(scaler.transform(train_feats), train_labels) acc clf.score(scaler.transform(val_feats), val_labels)用 LogisticRegression 做線性評估有兩個好處一是代碼量最少不需要自己寫分類頭訓練循環(huán)二是 scikit-learn 的 C 參數(shù)正則化強度可以快速做網(wǎng)格搜索找到最優(yōu)配置。C0.1是我在 TinyImageNet 上的經(jīng)驗值你也可以按[0.01, 0.1, 1.0, 10.0]的順序掃一遍正常來說 C 在 0.1-1.0 之間取到最高驗證準確率。StandardScaler對特征做標準化是必要的因為 ResNet18 的特征經(jīng)過 BN 層后雖然均值接近 0但方差不均勻LogisticRegression 對特征尺度敏感。這一整套流程跑下來在單張 RTX 3090 上預訓練 120 epoch 大約需要 4-6 小時取決于 batch 大小線性評估在 CPU 上 5 分鐘就能完成。如果你只有 8GB 顯存的顯卡把 batch_size 降到 256temperature不需要改但梯度累積可以緩解 batch 過小帶來的不穩(wěn)定具體見第 5 章。4. 監(jiān)督對照組的設計公平對比的關鍵在訓練配置而不是數(shù)據(jù)集自監(jiān)督實驗的對比結論是否可信很大程度上取決于監(jiān)督對照組是否夠努力。如果你的監(jiān)督學習只訓練了 50 epoch而自監(jiān)督預訓練跑了 120 epoch那么自監(jiān)督輸?shù)舯荣惒⒉荒苷f明什么問題——因為訓練預算不同。這一章講清楚監(jiān)督對照組的標準配置以及最終的對比口徑。4.1 端到端監(jiān)督訓練的參照配置監(jiān)督基線我用同樣的 ResNet18 結構去掉投影頭直接在 TinyImageNet 上端到端訓練。優(yōu)化器用 SGD momentum 0.9 weight decay 5e-4學習率 0.1 配合 cosine 退火訓練 120 epochbatch_size 128。這個配置是圖像分類任務上非常成熟的配方基本能壓出 ResNet18 在這個數(shù)據(jù)集上的真實上限。注意監(jiān)督學習和自監(jiān)督學習在 weight decay 上的差別自監(jiān)督預訓練時我用了1e-6這樣極小的 weight decay因為對比損失本身已經(jīng)有很強的正則化作用過大的 weight decay 會把特征范數(shù)壓得太小影響對比損失的梯度信號。監(jiān)督訓練則用5e-4的標準配置這是 ImageNet 分類任務的最佳實踐。這個不對等不是疏漏而是兩個任務的最優(yōu)配置本來就不同比較的是各自最優(yōu)配置下的結果。4.2 訓練過程與結果對比口徑我習慣在表格里記錄三個指標linear probe 準確率自監(jiān)督特征、端到端準確率監(jiān)督模型、以及泛化差距。泛化差距 訓練集準確率 - 驗證集準確率這個指標比單一準確率更能說明問題。自監(jiān)督預訓練 線性評估的模型經(jīng)常表現(xiàn)出更小的泛化差距因為預訓練階段見過大量增強版本backbone 對輕微分布偏移不敏感。方法驗證集準確率訓練集準確率泛化差距備注監(jiān)督 ResNet18端到端~51%~86%~35%120 epoch, SGD, weight decay 5e-4SimCLR 線性評估~47%~82%~35%120 epoch, AdamW, temperature0.5SimCLR kNN (k200)~42%--無需訓練分類頭的高效評估以我復現(xiàn)的數(shù)據(jù)來看SimCLR 線性評估能達到監(jiān)督端到端結果的 90% 以上在 TinyImageNet 這種規(guī)模的數(shù)據(jù)集上這個結果非常有意義。如果你在 ImageNet 上做同樣的對比這個比例會略低一些因為 ImageNet 類別間的視覺差異更細微自監(jiān)督預訓練要學到那樣精細的特征需要更大的模型和更長的訓練時間。注意表格里的 kNN 評估它用余弦相似度直接找特征空間中最近的 200 個訓練樣本對它們的標簽投票得出結果不需要訓練任何分類頭是驗證表征質(zhì)量的另一個高效指標。4.3 實際應用場景下的考慮只有兩個類別的二分類任務自監(jiān)督預訓練 線性評估跟監(jiān)督學習差距很小200 類的細分類任務自監(jiān)督容易在相似類別之間混淆比如不同品種的狗。如果你的業(yè)務場景是標注數(shù)據(jù)極少的長尾分布SimCLR 的價值會更明顯——它在無標注數(shù)據(jù)上預訓練再用 10% 的標注數(shù)據(jù)做線性或者少量微調(diào)效果往往好過直接在 10% 標注數(shù)據(jù)上端到端訓練。這也是這個項目對比實驗結論遷移到工程實踐的核心價值。5. 踩坑排查五條讓自監(jiān)督實驗白做的典型失誤自監(jiān)督實驗比監(jiān)督實驗更容易翻車因為損失曲線的下降趨勢不能直觀反映表征質(zhì)量。我復現(xiàn)這個項目時踩過不少坑整理了五條最有代表性的每一條都是現(xiàn)象→原因→解決的結構。希望你看完能繞開這些不用花兩周時間在同樣的地方打轉(zhuǎn)。5.1 訓練集和驗證集的標簽映射錯位現(xiàn)象預訓練過程一切正常NT-Xent 損失穩(wěn)步下降但線性評估的準確率只有 1-2%和隨機猜測差不多。原因訓練集的class_to_idx映射按文件夾遍歷順序構造驗證集的標簽按val_annotations.txt讀取兩者的類別順序不一致導致標簽錯位。在 TinyImageNet 里train/下的類別文件夾名是 WordNet IDval_annotations.txt也是按 WordNet ID 記錄的如果訓練集直接用sorted(os.listdir())建立映射而驗證集按文件順序讀恰好訓練集的排序和驗證集文件中出現(xiàn)的排序不一樣就會錯。解決統(tǒng)一以 WordNet ID 為基準先排序再映射且訓練集和驗證集共用同一個class_to_idx映射。我在代碼里用上面第 3.1 節(jié)的方式class_dirs sorted(os.listdir(...))然后self.class_to_idx {c: i for i, c in enumerate(class_dirs)}驗證集讀取時查出每個樣本的 WordNet ID再用class_to_idx[wnid]得到整數(shù)標簽。5.2 batch size 被顯卡限制時NT-Xent 損失發(fā)飄甚至坍縮現(xiàn)象batch_size 從 512 降到 128 以后損失函數(shù)在訓練初期呈下降趨勢但過了幾十個 epoch 后損失開始上升線性評估準確率一直上不去。原因SimCLR 是一個高度依賴 batch size 的算法batch 內(nèi)的其他樣本就是負樣本batch 越小負樣本種類越少正樣本對拉近的參照系越弱。batch_size128 時一個 batch 里只有 127 個負樣本且其中還包含很多相似樣本對比損失給出的梯度信號不夠穩(wěn)定模型就開始走捷徑——把所有樣本的表征都映射到同一個方向損失看似下降但表征完全坍縮。解決我一般會用梯度累積來模擬更大的 batch每 4 個 step 累積一次梯度再更新一次效果接近 batch_size 直接乘以 4。另外同一個 batch 里的樣本應該是獨立均勻采樣的避免同一個類別的樣本大量聚集比如按文件夾順序取樣本否則對比損失會被同類樣本互相拉近的表象欺騙。把 DataLoader 的shuffleTrue打開且drop_lastTrue丟棄最后一個不完整的 batch避免 batch 大小波動影響損失計算。5.3 溫度系數(shù)沿用 MoCo 的 0.07訓練極不穩(wěn)定現(xiàn)象NT-Xent 損失在訓練初期就降不下去一直在 5.5 附近震蕩對應ln(2N)量級后續(xù)線性評估也遠低于預期。原因MoCo 使用 0.07 作為溫度系數(shù)很多新手會把兩個工作混用。但 SimCLR 原論文在 ImageNet 上掃描過溫度系數(shù)最優(yōu)值是 0.5 左右。0.07 在 SimCLR 這種每個 batch 在線生成負樣本的框架里會導致梯度過于尖銳模型在極小的特征空間里劇烈震蕩收斂變得非常困難。MoCo 的負樣本來自動量隊列分布更穩(wěn)定0.07 才能工作。解決直接設temperature0.5如果發(fā)現(xiàn)損失仍然不穩(wěn)可以嘗試 0.7-1.0 之間更大的值但不要低于 0.2。溫度系數(shù)的掃描可以在少量 epoch20-30 個內(nèi)完成損失曲線的平穩(wěn)程度比絕對數(shù)值更有參考意義。我一般會同時記錄相似度矩陣的均值如果相似度分布集中在 0 附近說明表征空間還沒有被拉開此時應該考慮降低溫度、增大 batch 或增強顏色失真強度。5.4 數(shù)據(jù)增強太強64×64 小圖信息被破壞現(xiàn)象預訓練損失收斂正常但線性評估準確率比監(jiān)督基線低了十幾個點且增加預訓練時長沒有明顯改善。原因我把 ImageNet 的增強配置直接搬到 TinyImageNetRandomResizedCrop的最小裁剪尺度設成了原始圖像的 8%SimCLR 原論文在 ImageNet 上使用 8%-100%但這個尺度在 64×64 輸入下相當于只保留 18×18 的內(nèi)容語義信息幾乎被剪沒了。增強過頭讓自監(jiān)督任務變得過于困難學習到的特征對下游任務沒有幫助。解決把scale從(0.08, 1.0)改成(0.2, 1.0)GaussianBlur的 kernel 從 9×9 減小到 3×3。調(diào)整原則是增強后的人類可識別程度保持在明顯能看出是哪個類別的水平。一個判斷技巧——把增強后的圖片單獨可視化如果連你都認不出原圖類別那這個增強強度就是在逼模型盲人摸象。在 TinyImageNet 上訓練時我應該更早意識到這個問題而不是等到最后評估階段才回頭改配置這也讓我后來每次換數(shù)據(jù)集都先把增強圖像可視化一遍再開始訓練。5.5 BN 在 eval 模式下的統(tǒng)計行為差異讓線性評估結果失真現(xiàn)象預訓練完跑線性評估每次運行的結果波動很大±3% 以上檢查代碼也沒發(fā)現(xiàn)數(shù)據(jù)泄露。原因model.eval()切換后BatchNorm 層使用累積的 running statistics 而不是當前 batch 的統(tǒng)計量。如果預訓練時的 BN 統(tǒng)計量沒有正確累積比如混合精度訓練累積梯度時 step 數(shù)對不上eval 模式下提取的特征分布就會偏移。另外如果 DataLoader 的最后一個 batch 被drop_lastTrue丟棄BN 的 running stats 的累積均值可能偏向訓練早期。解決提取特征前顯式調(diào)用model.eval()并且用torch.no_grad()包裹如果用了混合精度要確保torch.cuda.amp.GradScaler的step()和update()順序正確不影響 BN 的 forward 次數(shù)統(tǒng)計。更穩(wěn)妥的做法是在所有 epoch 結束后額外跑一個 epoch 的數(shù)據(jù)來校準 BN 的 running statistics但這會引入微小的數(shù)據(jù)泄露我一般不用這個技巧而是保證訓練過程中 BN 統(tǒng)計量的累積不被打斷。6. 進階驗證的三個手工動作從相似度檢索到超參數(shù)敏感性讓你的實驗結論更可信實驗跑完拿到對比準確率只是第一步真正讓結論站得住的是三個手動驗證動作。第一個是表征相似度檢索取任意一張驗證圖在訓練集特征庫中檢索余弦相似度最高的 10 張圖看看檢索結果和原圖是否屬于同一類。這個驗證不依賴任何分類器直覺性最強。我通常在訓練完成后從 10 個不同類別里抽樣本做檢索如果同一類別的圖片大部分出現(xiàn)在 Top-10 里說明表征確實學到了語義信息而不是靠紋理或顏色匹配。第二是超參數(shù)敏感性掃描固定其他配置不變分別嘗試溫度系數(shù) 0.2、0.5、1.0以及增強強度 0.2、0.4、0.6 的組合。好的自監(jiān)督算法應該在合理范圍內(nèi)對超參數(shù)不敏感如果某個參數(shù)從 0.5 改成 0.6 導致準確率暴跌超過 3%說明模型處于過擬合邊界這個配置不能用于生產(chǎn)。第三是可視化表征分布的置信度對線性評估的輸出做 softmax統(tǒng)計各類別的置信度分布自監(jiān)督特征得到的分類器置信度通常比監(jiān)督訓練的更分散這是因為它沒見過端到端的分類頭訓練信號屬于正?,F(xiàn)象但如果你發(fā)現(xiàn)某個類別的置信度普遍低于 0.3可以參考第 5.4 節(jié)檢查增強配置。這三個動作加起來大概需要一小時做的事情不多但能讓你的實驗結論從數(shù)字上看得過去變成邏輯上站得住。從那以后我每次跑完自監(jiān)督預訓練都會強制走一遍這三步先做 20 張圖的相似度檢索看一眼結果再掃兩組關鍵超參確認穩(wěn)定性最后記錄置信度分布當作實驗報告的固定附件。如果哪一步的結果異常我不會繼續(xù)往下做實驗而是先排查對應的環(huán)節(jié)。希望這個習慣能幫你少走一些彎路讓每次實驗都有實實在在的產(chǎn)出。本文還有配套的精品資源點擊獲取