PyTorch(二)——搭建和自定義網絡
目錄連接
(1) 數據處理
(2) 搭建和自定義網絡
(3) 使用訓練好的模型測試自己圖片
(4) 視頻數據的處理
(5) PyTorch源碼修改之增加ConvLSTM層
(6) 梯度反向傳遞(BackPropogate)的理解
(總) PyTorch遇到令人迷人的BUG
PyTorch的學習和使用(二)
最近剛好在看一篇與Siamese network有關的論文,在PyTorch中沒有example,caffe中有,剛好使用PyTorch實現。(PS:圖片單獨打開更清晰)
主要步驟為:
- 數據預處理
- 模型搭建
- 模型訓練
數據預處理
Siamese的網絡結構如下:
通過輸入兩張圖片X1和X2,經過權重共享的CNN,各自得到一個輸出特征向量Gw(X1)和Gw(X2),然后使用特征向量的距離度量兩張圖片的相似度。
我們還是使用手寫數據集MINIST作為測試(主要有mnist的列子可以參考),網絡輸入的圖片為成對的,因此在構造訓練的batch時需要定義2張圖片,其標簽為兩張圖片是否為同一個數字。根據上篇文章講的,重新定義transforms.Compose()和torchvision.datasets.SIAMESE()。
由於對數據的讀取和處理都沒有改變,因此不用修改Compose()。torchvision.datasets.SIAMESE()則把相鄰的圖片捆綁在一起,作為一對輸入圖片,如果相同的數字,則標簽為1,不同則為0。
如下所示:
如圖所示每個batch中有2個圖片,但是在訓練中我們希望每個相鄰的batch為一對圖片,因此做如下處理:images.view(batch*2, 1, 28, 28)
得到如下結果:
網絡的搭建
Siamese的網絡定義與LeNet模型基本一致,唯一不同的是把生成10個數字類別概率的頂層替換為生成二維向量的特征層,最后損失函數換為對比損失函數(Contrastive_loss)(很憂傷,該損失函數在PyTorch中暫時沒有實現)。因此需要自己實現Contrastive_loss層。
Contrastive_loss函數定義
首先,理解caffe中Siamese網絡如和實現的。contrastive_loss_layer定義如下:
其forward()實現如下:
上述代碼中,bottom[0],bottom[1]存的2張圖片的特征,bottom[2],bottom[3]存的2張圖片的標簽,然后根據其是否相同進行如下計算:
在論文Learning a Similarity Metric Discriminatively, with Application to Face Verification中,其定義的損失函數為:
要求滿足:
則當EW(X1,X′2)−>0時, EW(X1,X2)−>m
其backward()如下:
12loss的導數為
根據導數計算,實現bankward()。當前導數乘后一層梯度為當前梯度。
CosineEmbeddingLoss實現
然后,在PyTorch中實現。在PyTorch中有實現CosineEmbeddingLoss損失函數,其定義為:
該函數和我們需要實現的Contrastive_loss損失函數類似,我們先分析CosineEmbeddingLoss函數的實現,任何構造自己的Contrastive_loss損失函數。
就如同PyTorch文檔中所講的,如果實現擴展 torch.autograd,需要實現3個方法:
- init (optional), 用於傳遞一些參數,比如margin, 和size_average.。
- forward(), 前向傳播,就是進行計算。
- backward(), 反向傳播,就是求導計算梯度。
其forward()實現如下:
實現CosineEmbeddingLoss函數主要就是完成cos(a, b)的計算。cos(a,b)=a∗b|a||b|.
代碼主要可以分為4部分,如下圖所示:
- 第一部分計算a向量和b向量的乘積a∗b
- 第二部分計算a向量和b向量模平方分之1,1|a|2和1|b|2
- 第三部分計算a向量乘b向量模分之1,1|a||b|
- 第四部分計算cos(a,b)=a∗b|a||b|
其backward()如下:
backward()就是實現CosineEmbeddingLoss的導數,主要計算cos(a,b)=a∗b|a||b|的導數。根據(uv)′=u′v−uv′v2得:
由於:
因此,可得:
在上圖代碼的說明中:
- 1表示:a∗b|a|2
- 2表示:a∗b|a|2⋅a−b
- 3表示:a∗b|a|2⋅a−b|a||b|
最后,當前導數乘后一層梯度為當前梯度。
Contrastive_loss損失函數實現
ps:雖然看了CosineEmbeddingLoss的實現,但是對PyTorch的矩陣計算函數還是不太熟悉,前前后后花了不少時間。
根據上面的公式,Contrastive_loss的代碼實現如下:(輸入為一對圖片input1, input2和標簽y,y==1表示同一物體,y==0表示不同物體)
class ContrastiveLoss(Function):
def __init__(self, margin=1, size_average=True): super(ContrastiveLoss, self).__init__() self.margin = margin self.size_average = size_average def forward(self, input1, input2, y): assert input1.size() == input2.size(), "Input sizes must be equal." self.l22 = input1.new() #l22 = ||a - b||^2 self._outputs = input1.new() _idx = input1.new().byte() epsilon = 1e-12 #avoid div 0 #l22 = ||a - b||^2 _diff = torch.abs(input1 - input2) self.l22 = torch.pow(_diff + epsilon, 2).sum(dim=1) #_output = l22 self._outputs.resize_as_(self.l22).copy_(self.l22) self._outputs = self._outputs.select(1, 0) #_output = first column and it one column vector torch.eq(y, 0, out=_idx) #y==0 self._outputs[_idx] = self._outputs[_idx].mul_(-1).add_(self.margin).clamp_(min=0) #max{0, m-||a-b||}^2 self._outputs[_idx] = self._outputs[_idx].pow_(2) #_output = 1/2 * _output self._outputs.mul_(1.0 / 2.0) output = self._outputs.sum() #sum if self.size_average: output = output / y.size(0) #mean self.save_for_backward(input1, input2, y) return input1.new((output,)) def backward(self, grad_output): v1, v2, y = self.saved_tensors buffer = v1.new() _idx = v1.new().byte() gw1 = grad_output.new() gw2 = grad_output.new() gw1.resize_as_(v1).copy_(v2) #gw1 = b gw2.resize_as_(v2).copy_(v1) #gw2 = a gw1.mul_(-1).add_(v1) #a' = sum(a - b) gw2.mul_(-1).add_(v2) #b' = sum(b- a) torch.le(self._outputs, 0, out=_idx) #find _output < 0 because loss>0 _idx = _idx.view(-1, 1).expand(gw1.size()) gw1[_idx] = 0 gw2[_idx] = 0 #y==0 torch.eq(y, 0, out=_idx) _idx = _idx.view(-1, 1).expand(gw2.size()) torch.add(self.l22, -self.margin, out=buffer) buffer = buffer.expand(gw1.size()) gw1[_idx] = gw1[_idx].mul(buffer[_idx]) gw2[_idx] = gw2[_idx].mul(buffer[_idx]).mul_(-1) if self.size_average: gw1.div_(y.size(0)) gw2.div_(y.size(0)) grad_output_val = grad_output[0] #current = lastgrad*dervative if grad_output_val != 1: gw1.mul_(grad_output_val) gw2.mul_(grad_output_val) return gw1, gw2, None
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
使用梯度檢驗:
input = (Variable(torch.randn(20, 2).double(), requires_grad = True), Variable(torch.randn(20, 2).double(), requires_grad = True),) test = gradcheck(ContrastiveLoss(), input, eps=1e-6, atol=1e-4) print test
- 1
- 2
- 3
- 4
返回的值為True,說明求導的backward沒有問題。(需要注意的是:我們的loss函數需要3個輸入,input1, input2, target. 但是在增加target時會報Kernel died, restarting錯誤,以為是loss代碼寫錯了,使用自帶的CosineEmbeddingLoss進行測試也是同樣的結果,最后去除target得以解決。)(PS:2017.2.15, 官方以修復此bug)
通過以上代碼,可以發現PyTorch的實現要比caffe使用C++實現要方便的多(至少不用指針指來指去),值得注意的有以下幾點:
-
in-palce 和out-place. 比如有Tensor變量a和b,torch.add() 和torch.add_(), 分別為in-palce 和out-place版本。a.add(b) 輸出 a+b,a和b的值不改變,a.add_(b) 輸出a+b 並且把結果給b。這樣做在進行連續計算時有好處,比如計算a*b+c, 只需要a.mul_(b).add_(c)即可。
-
使用索引時不能進行in-place。 比如有個索引idx,Tensor變量a,當使用a[idx].add_()時不會改變a的值。
-
少使用=進行賦值,多使用in-place和out=。 因為=進行賦值為淺拷貝,賦值的是地址指針,當其中一個改變時,會影響另一個值。如下所示:
Contrastive_loss損失層的增加
Contrastive_loss的算法已經實現,需要增加到PyTorch中。PyTorch為動態的實現,因此在改變源碼后不需要重新編譯。
增加一個自定義的層需要完成以下幾步:
1. 找到PyTorch的包路徑。一般在自己python環境路徑下的torch下。
2. 在nn._functions.loss.py中增加上面的Contrastive_loss實現代碼。
3. 在nn.functional.py 中增加Contrastive_loss的包裝。(可選)
4. 在nn.modules.loss.py中增加Contrastive_loss擴展。
然后在nn.modules.init.py中進行定義,以便可以進行調用。
最后在nn.backends.thnn.py中進行backend訪問定義。
Siamese網絡訓練結果
至此,數據的處理和網絡的搭建都已近完成了。其訓練結果如下: