PyTorch(二)——搭建和自定义网络


PyTorch(二)——搭建和自定义网络

 

目录连接 
(1) 数据处理 
(2) 搭建和自定义网络 
(3) 使用训练好的模型测试自己图片 
(4) 视频数据的处理 
(5) PyTorch源码修改之增加ConvLSTM层 
(6) 梯度反向传递(BackPropogate)的理解 
(总) PyTorch遇到令人迷人的BUG

PyTorch的学习和使用(二)

最近刚好在看一篇与Siamese network有关的论文,在PyTorch中没有example,caffe中有,刚好使用PyTorch实现。(PS:图片单独打开更清晰) 
主要步骤为:

  • 数据预处理
  • 模型搭建
  • 模型训练

数据预处理

Siamese的网络结构如下:

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()实现如下:

Siamese

上述代码中,bottom[0],bottom[1]存的2张图片的特征,bottom[2],bottom[3]存的2张图片的标签,然后根据其是否相同进行如下计算: 

loss=⎧⎩⎨||aibi||22,max{0,m||aibi||22}2,if a == bif a != b

 

 

Loss=12Nloss


在论文Learning a Similarity Metric Discriminatively, with Application to Face Verification中,其定义的损失函数为:
Siamese1 
要求满足: 
Siamese2

 

则当EW(X1,X2)>0时, EW(X1,X2)>m

backward()如下: 
Siamese3

12loss的导数为 

lossa=⎧⎩⎨(ab),(m||aibi||22)(ab),if a == bif a != b


根据导数计算,实现bankward()。当前导数乘后一层梯度为当前梯度。

 

CosineEmbeddingLoss实现

然后,在PyTorch中实现。在PyTorch中有实现CosineEmbeddingLoss损失函数,其定义为: 

loss(x,y)=⎧⎩⎨1cos(x1,x2),max{0,cos(x1,x2)},if y == 1if y == -1


该函数和我们需要实现的Contrastive_loss损失函数类似,我们先分析CosineEmbeddingLoss函数的实现,任何构造自己的Contrastive_loss损失函数。

 

就如同PyTorch文档中所讲的,如果实现扩展 torch.autograd,需要实现3个方法: 
init (optional), 用于传递一些参数,比如margin, 和size_average.。 
- forward(), 前向传播,就是进行计算。 
- backward(), 反向传播,就是求导计算梯度。

forward()实现如下:

a'

实现CosineEmbeddingLoss函数主要就是完成cos(a, b)的计算。cos(a,b)=ab|a||b|
代码主要可以分为4部分,如下图所示: 
- 第一部分计算a向量和b向量的乘积ab 
- 第二部分计算a向量和b向量模平方分之1,1|a|21|b|2 
- 第三部分计算a向量乘b向量模分之1,1|a||b| 
- 第四部分计算cos(a,b)=ab|a||b|

backward()如下: 
b'

backward()就是实现CosineEmbeddingLoss的导数,主要计算cos(a,b)=ab|a||b|的导数。根据(uv)=uvuvv2得: 

cos(a,b)=(ab|a||b|)=1|b|(ab)|a|(ab)|a||a|2


由于: 

(ab)=[(a1,a2,...,an)b]=b


|a|=[(a21,a22,...,a2n)1/2]=122(a1,a2,...,an)(a21,a22,...,a2n)1/2=a|a|

 

因此,可得: 

1|b|(ab)|a|(ab)|a||a|2=1|b|b|a|(ab)a|a||a|2=ab|a|2ab|a||b|

 

在上图代码的说明中: 
- 1表示:ab|a|2 
- 2表示:ab|a|2ab 
- 3表示:ab|a|2ab|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++实现要方便的多(至少不用指针指来指去),值得注意的有以下几点:

  1. 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

  2. 使用索引时不能进行in-place。 比如有个索引idx,Tensor变量a,当使用a[idx].add_()时不会改变a的值。 
    idx

  3. 少使用=进行赋值,多使用in-place和out=。 因为=进行赋值为浅拷贝,赋值的是地址指针,当其中一个改变时,会影响另一个值。如下所示: 
    out

Contrastive_loss损失层的增加

Contrastive_loss的算法已经实现,需要增加到PyTorch中。PyTorch为动态的实现,因此在改变源码后不需要重新编译。

增加一个自定义的层需要完成以下几步: 
1. 找到PyTorch的包路径。一般在自己python环境路径下的torch下。 
2. 在nn._functions.loss.py中增加上面的Contrastive_loss实现代码。 
1 
3. 在nn.functional.py 中增加Contrastive_loss的包装。(可选) 
2 
4. 在nn.modules.loss.py中增加Contrastive_loss扩展。 
3

然后在nn.modules.init.py中进行定义,以便可以进行调用。

4

最后在nn.backends.thnn.py中进行backend访问定义。

5

Siamese网络训练结果

至此,数据的处理和网络的搭建都已近完成了。其训练结果如下:

train


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM