[pytorch][stepbystep]在pytorch上實現卷積神經網路(CNN)的裁剪(purning)


利用VGG-16對Dogs-vs-Cats數據集進行訓練,裁剪VGG-16可以獲得3x的運算加速和4x的模型減小

簡介

puring神經網絡是一個古老的idea,具體可以追溯到1990年(與Yann LeCun的最佳腦損傷[1]工作)。這個想法是,在網絡中的許多參數中,有些是冗余的,對輸出沒有太大貢獻。

如果您可以根據它們貢獻的數量對網絡中的神經元進行排名,那么您可以從網絡中刪除低級神經元,從而產生更小更快的網絡。

獲得更快/更小的網絡對於在移動設備上運行這些深度學習網絡非常重要。

可以根據神經元權重的L1/L2均值、平均激活值、在某些驗證集上神經元不為零的次數以及其他創造性方法來進行排名。 在修剪之后,准確度將可能會下降(如果排名足夠好就不會下降太多),並且網絡通常重訓練以恢復。

如果一次性裁剪過多,那么神經網絡將被破壞而無法恢復。因此,在實踐中,這是一個迭代的過程——通常稱為“迭代剪枝”:修剪/訓練/重復。

purning沒有流行的原因

有很多關於修剪的論文,但我從未在現實生活深度學習項目中遇到過修剪。

考慮到在移動設備上運行深度學習的所有努力,這是令人驚訝的。 我想原因是:

  • 直到現在,排名方法還不夠好,導致精度下降太大。
  • 實現起來很復雜。
  • 那些使用修剪的人,保留它作為一個秘密或者優勢。

所以,我決定自己實現purning,看看能不能用它取得好成績。

在這篇文章中,我們將介紹一些修剪方法,然后深入研究最近一種方法的實現細節。

我們將微調一個VGG網絡,對Kaggle的Dogs vs Cats數據集中的貓/狗進行分類,這代表了一種在實踐中很常見的遷移學習。

然后我們將修剪網絡並將得到了近3x的運算加速和4x的模型減小。

purning網絡得到更快更小的模型

在VGG16中,90%的權重位於完全連接的層中,但這些權重僅占總浮點運算的1%。

直到最近,大部分工作都集中在修剪完全連接的層上。 通過修剪它們,可以顯着減小模型尺寸。

我們將重點關注在卷積層中修剪整個濾波器(譯者注:也即是一個kernel)。

但這也有減少記憶的冷靜副作用。 正如在[2]論文中所觀察到的那樣,越深的網絡就越容易被修剪。

這意味着最后的卷積層將被修剪很多,並且跟隨它的完全連接層中的許多神經元也將被丟棄!

修剪卷積濾波器時,另一種選擇是減少每個濾波器的權重,或者刪除單個內核的特定維度。你可以得到稀疏的過濾器,但是並不容易使計算速度提升。 最近的paper提倡“結構稀疏性”,其中整個過濾器被修剪。

這些論文中的一些重要的一點是,通過訓練然后修剪一個更大的網絡,特別是在遷移學習的情況下,他們得到的結果比從頭開始訓練一個較小的網絡要好得多。

現在讓我們簡要回顧幾種裁剪方法。

Pruning Filters for Efficient ConvNets[3]

在這項工作中,他們主張修剪整個卷積濾波器。 修剪索引為k的過濾器會影響它所在的​​層以及下一層。 必須刪除下一層中索引k處的所有輸入通道,因為在修剪之后它們將不再存在。

如果下一層是完全連接的層,並且該通道的特征圖的大小將是MxN,則從完全連接的層移除MxN神經元。

這項工作中的神經元排名非常簡單。 它是每個濾波器權重的L1范數。

在每次修剪迭代時,他們對所有過濾器進行排名,在所有層中全局修剪m個最低排名過濾器,重新訓練和重復。

Structured Pruning of Deep Convolutional Neural Networks[4]

這項工作看似相似,但排名要復雜得多。 他們保留了一組N個粒子濾波器,它們代表N個卷積濾波器被修剪。

當粒子表示的過濾器未被遮蓋時,基於驗證集上的網絡准確度為每個粒子分配分數。 然后根據新分數,對新的修剪掩模進行采樣。

由於運行此過程很繁重,他們使用一個小的驗證集來測量粒子分數。

Pruning Convolutional Neural Networks for Resource Efficient Inference[2]

這是來自Nvidia的非常酷的作品。

首先,他們將修剪問題稱為組合優化問題:選擇權重B的子集,以便在修剪它們時,網絡成本變化將是最小的。

注意他們如何使用絕對差異而不僅僅是差異。 使用絕對差異強制修剪的網絡不會過多地降低網絡性能,但也不應該增加它。 在論文中,他們表明這會產生更好的結果,大概是因為它更穩定。

現在所有排名方法都可以通過此成本函數來判斷。

Oracle Purning

VGG16有4224個卷積濾波器。 “理想”排名方法將是強力 - 修剪每個過濾器,然后觀察在訓練集上運行時成本函數如何變化。 由於他們來自Nvidia並且他們可以訪問數以千計的GPU,他們就是這樣做的。 這被稱為oracle排名 - 最小化網絡成本變化的最佳排名。 現在,為了衡量其他排名方法的有效性,他們計算了與oracle的spearman相關性。 令人驚訝的是,他們提出的排名方法(下面描述)與oracle最相關。

他們提出了一種新的神經元排序方法,該方法基於網絡成本函數的第一階(意味着快速計算)泰勒展開。

修剪濾波器h與將其歸零相同。

當網絡權重設置為W時,C(W,D)是數據集D上的平均網絡成本函數。現在我們可以將C(W,D)評估為C(W,D,h = 0)附近的擴展。 它們應該非常接近,因為移除單個過濾器不應該太大地影響成本。

h的等級則是abs(C(W,D,h = 0)-C(W,D))。


然后,通過該層中的等級的L2范數對每個層的排名進行歸一化。 我想這種經驗,我不知道為什么需要它,但它會極大地影響修剪的質量。

這個等級非常直觀。 我們可以使用激活和漸變作為排序方法。 如果它們中的任何一個很高,那意味着它們對輸出很重要。 如果漸變或激活非常低或高,則將它們相乘給我們提供拋出/保持濾波器的方法。

這讓我感到奇怪 - 他們是否將修剪問題歸結為最小化網絡成本的差異, 然后提出泰勒擴展方法,或者是其他方式 ,並且網絡成本的差異oracle是一種備份方式他們的新方法? 😃

在論文中,他們的方法在准確性方面也優於其他方法,因此看起來oracle是一個很好的指標。

無論如何,我認為這是一個比編碼和測試更友好的方法,比如粒子濾波器更友好,所以我們將進一步探索這個!

使用泰勒級數排名重要度然后purning

因此,假設我們有一個轉移學習任務,我們需要從相對較小的數據集創建分類器。 就像在這篇Keras博客文章[5]中一樣。

我們可以使用像VGG這樣強大的預訓練網絡進行傳輸學習,然后修剪網絡嗎?

如果在VGG16中學到的許多功能都是關於汽車,人和房屋 - 它們對簡單的狗/貓分類器有多大貢獻?

這是一個我認為很常見的問題。

作為訓練集,我們將使用Kaggle Dogs vs Cats數據集[6]中的1000張貓圖像和1000張狗圖像。作為測試集,我們將使用400張貓的圖像和400張狗的圖像。

最終結果:

准確率從98.7%下降到97.5%。

網絡大小從538 MB減少到150 MB。

在i7 CPU上,單個圖像的推理時間從0.78減少到0.277秒, 幾乎減少了x3倍!

step1. 訓練一個大的網絡

我們將采用VGG16,丟棄完全連接的層,並添加三個新的完全連接的層。 我們將固定卷積層,並僅重新訓練新的完全連接的層。 在PyTorch中,新的網絡層看起來像這樣:

self.classifier = nn.Sequential(
	    nn.Dropout(),
	    nn.Linear(25088, 4096),
	    nn.ReLU(inplace=True),
	    nn.Dropout(),
	    nn.Linear(4096, 4096),
	    nn.ReLU(inplace=True),
	    nn.Linear(4096, 2))

在使用數據增強訓練20個時刻之后,我們在測試集上獲得了98.7%(實際上 譯者獲得了98.8%)的准確度。

step2. 對網絡中的參數進行排名

為了計算泰勒級數,我們需要在我們的數據集上執行前向+后向傳遞(如果它太大,則需要在它的較小部分上執行,但由於我們只有2000個圖像可以直接使用它)。

現在我們需要以某種方式獲得漸變和卷積層的激活。 在PyTorch中,我們可以在梯度計算上注冊一個鈎子,因此在它們准備就緒時會調用一個回調:

for layer, (name, module) in enumerate(self.model.features._modules.items()):
	x = module(x)
	if isinstance(module, torch.nn.modules.conv.Conv2d):
		x.register_hook(self.compute_rank)
		self.activations.append(x)
		self.activation_to_layer[activation_index] = layer
		activation_index += 1

現在我們在self.activations中進行了激活,當漸變准備就緒時,將調用compute_rank:

def compute_rank(self, grad):
	activation_index = len(self.activations) - self.grad_index - 1
	activation = self.activations[activation_index]
	values = \
		torch.sum((activation * grad), dim = 0).\
			sum(dim=2).sum(dim=3)[0, :, 0, 0].data

	# Normalize the rank by the filter dimensions
	values = \
		values / (activation.size(0) * activation.size(2) * activation.size(3))

	if activation_index not in self.filter_ranks:
		self.filter_ranks[activation_index] = \
			torch.FloatTensor(activation.size(1)).zero_().cuda()

	self.filter_ranks[activation_index] += values
	self.grad_index += 1

這對批處理中的每個激活和它的梯度進行了逐點乘法,然后對於每次激活(即卷積的輸出),除了輸出的維度之外,我們在所有維度上求和。

例如,如果批量大小為32,則特定激活的輸出數為256,激活的空間大小為112x112,激活/梯度形狀為32x256x112x112,則輸出將為256大小的向量,表示排名該層中的256個過濾器。

現在我們有了排名,我們可以使用最小堆來獲得N個最低排名的過濾器。 與Nvidia論文不同,他們在每次迭代時使用N = 1,為了更快地獲得結果,我們將使用N = 512! 這意味着每次修剪迭代時,我們將從4224個卷積濾波器的原始數量中刪除12%。

低排名過濾器的分布很有意思。 被修剪的大多數過濾器來自更深層。 以下是第一次迭代后修剪過濾器的方法:

層數 修剪過的修剪過濾器數量
第0層 6
第2層 1
第5層 4
第7層 3
第10層 23
第12層 13
第14層 9
第17層 51
第19層 35
第21層 52
第24層 68
第26層 74
第28層 73

step3. 微調網絡並重復裁剪

在這個階段,我們解凍所有層並重新訓練網絡10個epoches,這足以在此數據集上獲得良好的結果。 然后我們使用修改后的網絡返回步驟1,並重復。

這是我們支付的實際價格 - 這是用於訓練網絡的epoc數量的50%,在一次迭代中。 在這個玩具數據集中,我們可以使用它,因為數據集很小。 如果您正在為大型數據集執行此操作,則最好使用大量GPU。

引用

[1]. http://yann.lecun.com/exdb/publis/pdf/lecun-90b.pdf

[2]. https://arxiv.org/abs/1611.06440

[3]. https://arxiv.org/abs/1608.08710

[4]. https://arxiv.org/abs/1512.08571

[5]. https://blog.keras.io/building-powerful-image-classification-models-using-very-little-data.html

[6]. https://www.kaggle.com/c/dogs-vs-cats


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM