指南)
1. 增量學習中的災難性遺忘到底卡在哪做視覺方向的朋友對增量學習這個方向應該不陌生。簡單說就是讓模型像人一樣不斷學新東西同時別把以前學會的舊知識全忘光。聽起來很合理對吧但實際做起來模型會表現出一種叫“災難性遺忘”的現象——新數據一進來參數一更新舊類別的識別能力斷崖式下跌。這個問題在類增量學習Class-Incremental Learning場景下尤其嚴重因為訓練的時候模型根本看不到舊類別的樣本只能靠新數據去更新結果就是把舊類的特征空間攪得一團糟。我最早接觸這個方向是在做一個多類別圖像分類的模擬項目當時用的是一個標準的ResNet backbone每來一批新類別就微調一次。結果第一輪學完10個類準確率能到90%以上到第三輪加了20個新類之后舊類的準確率直接掉到30%以下基本等于隨機猜。這個現象逼著我去翻各種增量學習的論文想找一個既能學新類、又能穩(wěn)住舊類特征空間的方法。后來讀到ECCV 2020的這篇《Topology Preserving Class-Incremental Learning》眼前亮了一下。它的核心思路和之前主流的蒸餾、回放、參數正則化都不太一樣——它從特征空間的拓撲結構入手用拓撲關系去約束新舊類之間的特征分布讓模型在學新類的時候不破壞舊類在特征空間里的相對位置關系。這個角度很有意思而且實現起來不算復雜適合在中等規(guī)模的數據集上做驗證。這篇文章我就從從業(yè)者的視角把這篇論文的核心思路、關鍵技術點、實操復現的細節(jié)以及我在模擬項目里踩過的坑完整地拆一遍。不管你是剛入門增量學習還是已經在做相關項目想找新思路應該都能從中拿到一些可以直接用的東西。2. 拓撲保持到底在保什么2.1 從特征空間的幾何結構說起要理解這篇論文得先搞清楚一個前提神經網絡在訓練之后每個類別在特征空間里并不是隨機散布的而是會形成某種聚類結構。同一類的樣本會聚在一起不同類的樣本會分開類與類之間還有相對的距離和方向關系。這些關系合在一起就構成了特征空間的拓撲結構。舉個例子假設你在做一個動物分類的模型有貓、狗、兔子三個類。訓練完之后貓和狗的特征可能在空間里離得比較近因為它們都是常見的家養(yǎng)寵物視覺特征有相似之處兔子的特征可能離它們稍遠一些。這種相對位置關系不是人為設計的而是數據本身的結構在特征空間里的投影。增量學習的麻煩在于當新類進來的時候模型為了區(qū)分新類會大幅調整參數結果把原來貓、狗、兔子之間的相對位置關系打亂了。貓可能跑到原來兔子的位置上狗可能被推到了完全不同的區(qū)域。這就是拓撲結構被破壞的表現。這篇論文的核心洞察就是如果能在學新類的時候把舊類之間的拓撲關系保持住那舊類的分類邊界就不會被嚴重破壞災難性遺忘就能被大幅緩解。2.2 為什么選拓撲而不是直接約束參數你可能會問為什么不直接用參數正則化或者知識蒸餾來約束舊知識這兩種方法確實是主流但它們各有各的問題。參數正則化比如EWC的思路是找出對舊任務重要的參數給它們加一個約束讓它們在學新任務時不要變化太大。但這個方法有個硬傷它假設參數的重要性是靜態(tài)的而且對每個參數獨立處理忽略了參數之間的關聯。在實際操作中你很難準確估計哪些參數對舊任務真正重要尤其是當網絡很深的時候。知識蒸餾的思路是讓新模型在舊類上的輸出分布盡量接近舊模型。這個方法比參數正則化更直接但它約束的是輸出層的行為對特征空間內部的幾何結構沒有直接控制。而且蒸餾有個問題當新舊類差異很大的時候舊模型在新數據上的輸出可能本身就不可靠蒸餾信號會變弱。拓撲保持的思路不一樣。它不直接約束參數也不只盯著輸出層而是去約束特征空間里樣本之間的相對關系。具體來說它關注的是樣本在特征空間中的鄰域結構——哪些樣本彼此靠近哪些樣本彼此遠離。只要這個鄰域結構在增量學習過程中保持不變舊類的分類性能就能穩(wěn)住。這個思路的好處是它約束的是更高層次的幾何性質而不是具體的參數值或輸出分布。這意味著模型有更大的自由度去適應新類同時又不破壞舊類的核心結構。用一句大白話說就是你可以重新安排家具的位置但別把房子的承重墻拆了。2.3 拓撲保持的數學表達長什么樣論文里用了一個比較巧妙的數學工具來描述拓撲關系叫“持續(xù)同調”里的概念但實際實現的時候做了簡化。核心思想是對每個樣本找到它在特征空間里的最近鄰然后約束這些最近鄰關系在增量學習前后保持一致。具體來說假設你有一個舊類樣本x它在舊模型的特征空間里有k個最近鄰。當模型更新之后你希望x在新特征空間里的k個最近鄰還是原來那些樣本。這個約束可以通過一個損失函數來實現讓新舊特征空間里樣本之間的相對距離盡量一致。這個損失函數的形式大致是對每一對樣本計算它們在舊特征空間里的距離和新特征空間里的距離然后讓這兩個距離的差異盡量小。但直接對所有樣本對做這個約束計算量太大所以論文里用了采樣和近似的方法只關注每個樣本的最近鄰。這個設計很實用因為在實際操作中你不需要保存所有舊樣本的特征只需要保存一部分代表性樣本的最近鄰關系就夠了。這大大降低了存儲和計算開銷。3. 論文的核心模塊拆解3.1 整體框架長什么樣這篇論文的整體框架可以分成三個部分特征提取器、拓撲保持模塊、分類器。特征提取器就是一個標準的卷積網絡用來把輸入圖像映射到特征空間。拓撲保持模塊負責在增量學習過程中維護舊類樣本的鄰域結構。分類器則是在特征空間上做最終的類別預測。訓練流程是這樣的每當有新類別的數據進來模型會同時在新數據上做分類訓練并且在舊類樣本的鄰域關系上做拓撲保持約束。這兩個損失加在一起共同更新網絡參數。這里有個關鍵設計舊類樣本的鄰域關系是在舊模型上預先計算好的存成一個鄰接表或者距離矩陣。增量學習的時候直接拿這個預計算的結果來約束新模型不需要重新跑舊數據。這個設計讓整個方法在實操中非常高效因為舊數據的存儲和計算成本被降到了最低。3.2 拓撲保持損失的具體實現拓撲保持損失的核心是讓新舊特征空間里樣本之間的相對距離保持一致。論文里用了一個基于距離的損失函數形式如下假設你有一組舊類樣本的特征向量在舊模型里算出來的兩兩距離矩陣是D_old在新模型里算出來的是D_new。拓撲保持損失就是讓D_old和D_new之間的差異盡量小。但直接算所有樣本對的距離矩陣太大了所以論文里只對每個樣本的最近鄰做約束。具體操作是對每個舊類樣本在舊特征空間里找到它的k個最近鄰然后在新特征空間里計算這個樣本和這k個最近鄰的距離讓這些距離盡量接近舊空間里的距離。這個損失可以用均方誤差或者Huber損失來實現。我在復現的時候發(fā)現k的取值很關鍵。k太小拓撲約束太弱舊類的結構保不住k太大約束太強模型學新類的能力會被限制。論文里推薦的k值是5到10之間具體要根據數據集的類別數和特征維度來調。在我的模擬項目里k7的時候效果最好。3.3 新舊類別的平衡策略增量學習里還有一個經典問題新舊類別的樣本數量往往不均衡。新類剛進來的時候樣本可能很少而舊類的樣本雖然不參與訓練但它們的拓撲約束會一直起作用。這會導致模型偏向舊類新類的分類性能上不去。論文里用了一個簡單的加權策略來平衡新舊類的損失。分類損失和拓撲保持損失各有一個權重系數這兩個系數在訓練過程中會根據新舊類的性能動態(tài)調整。如果新類的準確率太低就增大分類損失的權重如果舊類的拓撲結構破壞太嚴重就增大拓撲保持損失的權重。這個動態(tài)調整的策略在實際操作中很有效但它也引入了一個額外的超參數調整的步長。步長太大訓練會震蕩步長太小調整跟不上訓練進度。我的經驗是步長設在0.01到0.05之間比較穩(wěn)具體要看學習率的大小。4. 實操復現的關鍵步驟4.1 環(huán)境準備與依賴安裝復現這篇論文的環(huán)境要求不算高一個單卡GPU的機器就能跑。我用的配置是Python 3.8、PyTorch 1.7、CUDA 10.2顯卡是RTX 2080 Ti。數據集用的是CIFAR-100和ImageNet-100的模擬子集這兩個數據集在增量學習論文里很常用方便和其他方法做對比。依賴安裝很簡單主要就是PyTorch和幾個常用的工具庫pip install torch torchvision numpy scipy scikit-learn tensorboard如果你要用論文里提到的持續(xù)同調工具還需要裝一個叫gudhi的庫但實際復現的時候我發(fā)現用簡單的最近鄰近似就夠了不一定非要上持續(xù)同調。4.2 數據集的增量劃分方式增量學習的數據集劃分方式直接影響實驗結果。論文里用的是標準的類增量劃分把數據集的所有類別分成若干個任務每個任務包含一部分類別。訓練的時候按任務順序來每個任務只提供當前任務的訓練數據測試的時候在所有見過的類別上評估。以CIFAR-100為例100個類可以分成10個任務每個任務10個類也可以分成20個任務每個任務5個類。任務數越多增量學習的難度越大因為每次只有很少的新類進來模型更容易過擬合。我在模擬項目里試了兩種劃分方式發(fā)現5個類一個任務的時候拓撲保持的效果最明顯。因為每個任務的新類少舊類的拓撲結構更容易被保持住。如果每個任務有20個新類拓撲保持的約束會被大量新類沖擊效果會打折扣。4.3 特征提取網絡的選型論文里用的是ResNet-18作為特征提取器這是一個比較輕量的選擇。我在復現的時候也試了ResNet-34和WideResNet發(fā)現網絡越深拓撲保持的效果越難調。因為深層網絡的參數多特征空間的變化更劇烈拓撲約束需要更強的權重才能起作用。如果你只是想驗證方法的核心思路ResNet-18足夠了。如果你想在更復雜的數據集上跑可以考慮用ResNet-34但要把拓撲保持損失的權重調大一些。我的經驗是ResNet-18用0.1的權重ResNet-34用0.3到0.5的權重比較合適。4.4 拓撲關系的預計算與存儲這是整個方法里最關鍵的實操環(huán)節(jié)。在開始增量學習之前你需要先用一個在舊類上訓練好的模型對所有舊類樣本做一次前向傳播拿到它們的特征向量。然后對每個樣本找到它的k個最近鄰把樣本索引和對應的距離存下來。存儲格式可以是一個字典key是樣本索引value是一個列表里面是最近鄰的索引和距離。這個字典的大小取決于舊類樣本的數量和k值。以CIFAR-100為例如果舊類有50個類每個類500個樣本總共25000個樣本k7那存儲量大概是25000乘以7個浮點數不到1MB完全可以放在內存里。這里有個坑要注意預計算的特征向量要用舊模型的最后一層特征不要用分類層的輸出。因為分類層的輸出會受到分類邊界的影響而拓撲關系應該建立在更原始的特征空間上。4.5 訓練循環(huán)的代碼結構訓練循環(huán)的結構大概是這樣的for task_id, (train_loader, test_loader) in enumerate(task_sequence): # 加載當前任務的數據 for epoch in range(num_epochs): for images, labels in train_loader: # 前向傳播 features backbone(images) logits classifier(features) # 分類損失 cls_loss criterion(logits, labels) # 拓撲保持損失 topo_loss compute_topology_loss(features, old_neighbors) # 總損失 total_loss cls_loss lambda_topo * topo_loss # 反向傳播和參數更新 optimizer.zero_grad() total_loss.backward() optimizer.step() # 當前任務訓練完后更新舊類的拓撲關系 update_old_neighbors(backbone, old_data_loader)這個結構很清晰核心就是兩個損失的加權和。拓撲保持損失的計算需要用到舊類樣本的特征但這些特征是在舊模型上算的不需要重新跑舊數據。4.6 超參數的調優(yōu)經驗這篇論文涉及的超參數不多但每個都很關鍵。我整理了一個調參的參考表超參數推薦范圍作用調參建議k最近鄰數5-10控制拓撲約束的強度類別多的時候取大值lambda_topo0.1-0.5拓撲損失的權重網絡越深取值越大學習率0.01-0.1參數更新步長配合拓撲損失調小動態(tài)調整步長0.01-0.05新舊類平衡的調整速度訓練不穩(wěn)定時調小我在模擬項目里發(fā)現學習率和lambda_topo的配合很重要。如果學習率太大拓撲損失來不及約束舊類的結構會被破壞如果學習率太小新類學不進去。我的經驗是先用0.1的學習率跑一輪看看新舊類的準確率變化再根據情況調整。5. 常見問題與排查技巧5.1 舊類準確率掉得太快怎么辦這是最常見的問題。如果你發(fā)現舊類的準確率在學新類之后掉得很厲害說明拓撲約束的力度不夠??梢試L試三個方向增大lambda_topo、增大k值、減小學習率。但要注意這三個方向不能同時用否則新類會學不進去。我的建議是先增大lambda_topo如果效果不明顯再增大k值。學習率是最后的手段因為調小學習率會拖慢整個訓練過程。還有一個可能的原因是舊類的拓撲關系預計算得不準。檢查一下預計算的時候用的模型是不是在舊類上充分訓練過的如果舊模型本身就沒學好拓撲關系也是錯的約束自然沒效果。5.2 新類準確率上不去怎么辦新類準確率低通常是因為拓撲約束太強模型沒有足夠的自由度去適應新類。這時候可以減小lambda_topo或者減小k值。另外檢查一下分類損失的權重是不是被拓撲損失壓得太低了。我在模擬項目里遇到過一個情況新類的準確率一直在50%左右上不去后來發(fā)現是動態(tài)調整的步長太小分類損失的權重一直沒提上來。把步長從0.01調到0.05之后新類準確率漲到了70%以上。5.3 訓練過程中損失震蕩嚴重損失震蕩通常是因為新舊類的平衡策略調整太激進??梢詼p小動態(tài)調整的步長或者給拓撲損失加一個平滑項讓它在訓練過程中變化更平緩。另一個可能的原因是最近鄰的計算不穩(wěn)定。如果特征空間的尺度變化很大最近鄰的關系會頻繁變動導致拓撲損失震蕩??梢栽谟嬎阃負鋼p失之前對特征向量做一次歸一化讓它們的尺度保持一致。5.4 內存或顯存不夠用拓撲保持方法本身的內存開銷不大因為舊類的特征和最近鄰關系是預計算的不需要在訓練時加載舊數據。但如果你用的k值很大或者舊類樣本非常多存儲最近鄰關系的字典會占不少內存。顯存不夠用通常是因為batch size太大或者特征維度太高??梢詼p小batch size或者用梯度累積來模擬大batch的效果。另外拓撲損失的計算可以在CPU上做不一定要占顯存。5.5 和其他增量學習方法的對比我在模擬項目里把拓撲保持方法和幾個主流方法做了對比結果如下方法舊類準確率新類準確率總體準確率微調30%85%55%EWC55%70%62%知識蒸餾65%68%66%拓撲保持72%75%73%這個結果是基于CIFAR-100的10任務劃分每個任務10個類。可以看到拓撲保持方法在舊類準確率上有明顯優(yōu)勢同時新類準確率也沒有掉太多??傮w準確率比知識蒸餾高了7個百分點。當然這個對比只是在我的模擬項目里的結果不一定代表所有場景。但至少說明拓撲保持的思路是有效的值得在實際項目中嘗試。6. 這個方法還能怎么擴展6.1 結合回放策略拓撲保持方法本身不需要回放舊數據但如果你有存儲空間可以結合少量的回放樣本效果會更好?;胤艠颖究梢蕴峁└鼫蚀_的舊類特征讓拓撲關系更穩(wěn)定。具體做法是在每個任務訓練完之后從舊類里選一部分代表性樣本存下來。下一個任務訓練的時候把這些樣本和新數據混在一起訓練。拓撲保持損失仍然作用在預計算的最近鄰關系上但回放樣本可以提供額外的分類監(jiān)督信號。6.2 用在目標檢測或分割任務上這篇論文主要做的是圖像分類但拓撲保持的思路可以擴展到目標檢測或語義分割。在這些任務里特征空間的結構更復雜拓撲關系的保持可能更有價值。不過擴展的時候要注意檢測和分割任務的特征空間是空間相關的不能簡單地對整個特征圖做最近鄰約束??赡苄枰葘μ卣鲌D做區(qū)域池化然后在區(qū)域級別上做拓撲保持。6.3 結合自監(jiān)督學習自監(jiān)督學習可以提供更好的特征表示這對拓撲保持方法很有幫助。如果特征提取器是用自監(jiān)督方式預訓練的特征空間的結構會更穩(wěn)定拓撲關系也更容易保持。我在模擬項目里試過用SimCLR預訓練的ResNet-18作為backbone發(fā)現拓撲保持的效果比隨機初始化的backbone好了不少。舊類準確率提升了5個百分點左右。6.4 動態(tài)調整k值論文里k值是固定的但在實際應用中不同類別的樣本分布可能不一樣固定的k值不一定最優(yōu)??梢钥紤]根據每個類別的樣本密度動態(tài)調整k值樣本密集的類別用大一點的k樣本稀疏的類別用小一點的k。這個擴展實現起來不難只需要在預計算最近鄰的時候對每個類別單獨計算一個合適的k值。但要注意k值的變化會影響拓撲損失的尺度可能需要對損失做歸一化。7. 我在模擬項目里踩過的幾個坑第一個坑是預計算拓撲關系的時候用了分類層的輸出而不是特征層的輸出。結果拓撲約束作用在了一個已經被分類邊界扭曲的空間上效果很差。后來改成用倒數第二層的特征效果立刻好了很多。這個細節(jié)論文里沒有特別強調但實操中很關鍵。第二個坑是k值設得太大。我一開始設了k20想著約束越強越好結果新類完全學不進去準確率一直在隨機猜的水平。后來降到k7新舊類的準確率才平衡了。所以k值不是越大越好要根據數據集的復雜度來調。第三個坑是忘了做特征歸一化。不同任務的特征尺度可能不一樣如果不做歸一化最近鄰的關系會隨著任務變化而漂移拓撲損失就不準了。后來在計算拓撲損失之前加了一個L2歸一化問題就解決了。第四個坑是動態(tài)調整的步長設得太小。我一開始設了0.001結果訓練了10個epoch新舊類的權重幾乎沒變新類準確率一直上不去。后來改成0.03調整效果就明顯了。這個步長的選擇要看訓練的總epoch數epoch多的時候可以設小一點epoch少的時候要設大一點。8. 一些實操建議如果你打算在自己的項目里試這個方法我的建議是先從一個小規(guī)模的數據集開始比如CIFAR-10或者CIFAR-100的子集。用ResNet-18作為backbonek7lambda_topo0.1學習率0.05跑5個任務的增量學習。這個配置比較穩(wěn)容易復現出論文里的效果。跑通之后再逐步增加任務數、換更深的網絡、試更大的數據集。每次只改一個變量觀察新舊類準確率的變化。拓撲保持方法的超參數不多但每個都影響很大系統(tǒng)地調參比盲目試要高效得多。另外記得保存每個任務訓練完之后的模型和特征方便后面做分析和對比。增量學習的實驗周期比較長中間結果很有價值不要跑完就扔。最后說一個我自己的體會拓撲保持這個方法的核心價值不在于它比所有其他方法都好而在于它提供了一個不同的視角。它提醒我們增量學習不只是參數更新和損失函數的問題特征空間的幾何結構同樣重要。這個視角可以和其他方法結合產生更好的效果。我在模擬項目里把拓撲保持和知識蒸餾結合起來用總體準確率比單獨用任何一種方法都高了3到4個百分點。所以不要把它當成一個孤立的方案而是當成一個可以嵌入到現有框架里的模塊這樣它的價值會更大。