在 ICML 2015的论文 Unsupervised Domain Adaptation by Backpropagation (DANN)网络中提出了一个梯度翻转层(Gradient Reverse Layer,GRL),这个层是用来干什么的?

§ 梯度翻转层 - 图1

在训练阶段我们要做的是如下两个任务,第一个则是实现源域数据集准确分类,实现现图像分类误差的最小化;第二个任务则是要混淆源域数据集和目标域数据集,实现域分类误差的最大化,混淆目标域数据集与源域数据集。现在来看一下是怎么实现这两个任务的

上述模型中主要有3个部分:

  • 绿色部分:是用来特征提取的
  • 蓝色部分:对源域进行训练,这部分的标签是输入数据的真实标签,在测试的时候就是测试集的标签,得到的就是分类误差
  • 红色部分:一个二分类器,这部分的标签是源域标签/目标域标签。如果输入为训练集(源域),那么这里就是测试集(目标域)的标签;如果输入为测试集(目标域),那么这里就是训练集(源域)的标签。这部分目的是使源域和目标域之间的分类损失最小

一、为什么要GRL

上述的蓝色部分对源域(训练集)数据进行训练,随着轮回次数(epoch)的增多,该部分会发生过拟合现象;而红色部分的目标是最大化二分类误差,从而将源域数据向目标域方向移动,目标域数据向源域方向移动,最完美的理想情况是让源域和目标域的数据能够落于同一个分布上。

§ 梯度翻转层 - 图2

为什么要移动呢?因为我们对训练集(源域)经过不断优化找到的分割线在目标域中表现很差,所以把这两个数据集往一块靠拢,等到两部分数据集并拢到一块的时候,在源域(训练集)上能够达到优秀分类的模型自然在测试集上也能达到优秀的效果。

§ 梯度翻转层 - 图3

原文链接:https://blog.csdn.net/qq_30091945/article/details/104478550

二、GRL的一些要点(调试必看)

这部分主要是记录别人在使用GRL的过程中总结出的一些要点,通过重点关注这些要点从而实现有的放矢。

  1. 验证分类器有效:在调试GRL前,先单独对分类器进行训练,确保网络能够进行分类;
  2. 验证GRL是否工作:在输出结果层(最后一层)后面加上GRL,令整个梯度都是反向的,并且将GRL参数设置为常数1,观察训练loss是否越来越大;
  3. 以上功能确保正常后,即可将GRL加到网络进行训练。理想的网络分类器损失先下降后增加的趋势,最后网络无法对其分类
    然而,实验中出现了分类器损失一直保持不变,既开始分类器一直学不到东西,通过修改学习率、优化器等皆无果,后面想起GRL还有一个参数,对其进行调整,网络终于开始能学着分类。所以很重要的是GRL的参数α(既反向梯度所乘系数),我采用的是原论文的系数设置,随着迭代次数的增加,α由0增加到1。但是在调试时,发现α设置为0.001是能满足前面第3点提到的趋势,大一点则损失不降,因此最后设置是α由0增加到0.002。所以不止分类器和GRL是否有效工作很重要,GRL的参数α也很重要,但发现分类器损失一动不动时,试着把α减小一个数量级在跑跑看

题外话:在调试时,可以选择val set或者少量数据进行训练,这样更快能看出方法是否有效,而不是一整个数据集丢进去跑,太浪费时间了!毕竟迭代的次数多了,才能看得出网络损失的趋势。同时,在进行实验时,一定要对数据集和标签进行可视化(我做的是图像的目标检测),粗略的过一遍,确保数据集的处理没有问题。 做了好几个项目,还总是没有吸取教训,希望说的点能帮到大家。

原文链接:https://blog.csdn.net/qq_36552489/article/details/105234425

三、GRL的实现

这部分主要是使用PyTorch来实现的,定义了一个梯度翻转层,其中的backward表示的就是对梯度进行翻转。

§ 梯度翻转层 - 图4 定义梯度翻转层GRL

  1. from torch.autograd import Function
  2. class ReverseLayerF(Function):
  3. @staticmethod
  4. def forward(ctx, x, alpha):
  5. ctx.alpha = alpha
  6. return x.view_as(x)
  7. @staticmethod
  8. def backward(ctx, grad_output):
  9. output = grad_output.neg() * ctx.alpha
  10. return output, None

§ 梯度翻转层 - 图5 使用定义好的梯度翻转层GRL

在上面定义好了以后,就要使用该层,在哪里使用,又怎么使用呢?

现在,我们回想整个过程,我们使用的GRL层在红色区域的前面,所以同样在代码中就在这个地方使用即可。

§ 梯度翻转层 - 图6

假设以下代码中的x表示绿色部分提取出的最后特征,则使用步骤如下所示

  1. # 1.先将绿色部分最后的特征投入GRL
  2. reverse_x = ReverseLayerF.apply(x, alpha)
  3. # 2.之后的网络结构就是上述的红色部分的结构,这个自定义
  4. self.domain_classifier = nn.Linear(reverse_x.shape[1], 2)
  5. domain_output = self.domain_classifier(reverse_x)

四、参考PyTorch

上述只是给出了GRL实现的代码,并没有详细的解释为什么这么做,这个部分可以参考PyTorch的官网。

在PyTorch框架下,我们在进行反馈的时候都会简单地加上一句话model.backward(),这句代码中的backward()函数已经帮助我们计算了反馈所需要的梯度。而这么个简单的backward运算符实际上是两个在Tensors上操作的函数,就是我们上面定义的forward()backward(),这两个函数位于torch.autograd.Function类中。

其中,forward()函数计算输入张量(tensor)的输出张量;backward()函数返回输出张量的梯度,这个梯度的大小和一个常数有关,这个常数就是forward()函数里的§ 梯度翻转层 - 图7变量。

在PyTorch里,我们可以自定义forward()backward()这两个函数,这样在使用model.backward()的时候调用的就是自己定义的forwardbackward函数了。

在下面这个例子中,我们的模型为一个线性函数§ 梯度翻转层 - 图8#card=math&code=y%20%3D%20a%2BbP_3%28c%2Bdx%29&id=tFho3),在给定的§ 梯度翻转层 - 图9区间范围内,对某个§ 梯度翻转层 - 图10值进行拟合,找到拟合后的§ 梯度翻转层 - 图11, § 梯度翻转层 - 图12, § 梯度翻转层 - 图13, § 梯度翻转层 - 图14值。其中的§ 梯度翻转层 - 图15函数为§ 梯度翻转层 - 图16%3D%5Cfrac%7B1%7D%7B2%7D(5x%5E3-3x)#card=math&code=P_3%28x%29%3D%5Cfrac%7B1%7D%7B2%7D%285x%5E3-3x%29&id=QCJN7),看一下这个例子中如何对§ 梯度翻转层 - 图17函数重写forwardbackward函数。

  • 前向过程:§ 梯度翻转层 - 图18%3D%5Cfrac%7B1%7D%7B2%7D(5x%5E3-3x)#card=math&code=P_3%28x%29%3D%5Cfrac%7B1%7D%7B2%7D%285x%5E3-3x%29&id=ZIxcF)
  • 回退过程:§ 梯度翻转层 - 图19%3D%5Cfrac%7B1%7D%7B2%7D3(5x%5E2-1)%20%3D%201.5(5x%5E2-1)#card=math&code=P_3%27%28x%29%3D%5Cfrac%7B1%7D%7B2%7D%2A3%285x%5E2-1%29%20%3D%201.5%2A%285x%5E2-1%29&id=ugSPL)
  1. # -*- coding: utf-8 -*-
  2. import torch
  3. import math
  4. class CustomBackward(torch.autograd.Funciton):
  5. '''
  6. 通过引入torch.autograd.Function帮助实现自顶forward和backward函数
  7. '''
  8. @staticmethod
  9. def forward(ctx, input):
  10. '''
  11. 在forward阶段,因为y=1/2(5x^3 - 3x),所以直接返回该值即可.
  12. 为了方便在backward阶段使用变量,可以使用ctx缓存任意对象。
  13. '''
  14. ctx.save_for_backward(input)
  15. return 0.5*(5*input**3 - 3*input)
  16. @staticmethod
  17. def backward(ctx, grad_output):
  18. '''
  19. 在backward阶段,因为损失函数为链式推导的原因,我们得到了一个与输
  20. 出的损失函数有关的梯度张量grad_output。
  21. 现在,我们要根据链式求导法则,把这个梯度返还到forward阶段,使用的是P3'(x)
  22. '''
  23. input, = ctx.saved_tensors
  24. return grad_output * 1.5 * (5*input**2 - 1)
  25. dtype = torch.float
  26. device = torch.device("cpu")
  27. # 通过设定 requires_grad=False表示我们对某个变量自动计算梯度而不用手动计算
  28. x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype)
  29. y = torch.sin(x)
  30. # 在上面的P3(x)中有4个需要拟合的变量即a, b, c和d
  31. a = torch.full((), 0.0, device=device, dtype=dtype, requires_grad=True)
  32. b = torch.full((), -1.0, device=device, dtype=dtype, requires_grad=True)
  33. c = torch.full((), 0.0, device=device, dtype=dtype, requires_grad=True)
  34. d = torch.full((), 0.3, device=device, dtype=dtype, requires_grad=True)
  35. learning_rate = 5e-6
  36. for t in range(2000):
  37. # 在前向阶段就需要使用自定义的forward函数,通过apply函数调用
  38. P3 = LegendrePolynomial3.apply
  39. y_pred = a + b * P3(c + d * x)
  40. # 计算损失函数
  41. loss = (y_pred - y).pow(2).sum()
  42. if t % 100 == 99:
  43. print(t, loss.item())
  44. # 自动计算反馈回去的梯度
  45. loss.backward()
  46. # 更新参数a, b, c, d
  47. with torch.no_grad():
  48. a -= learning_rate * a.grad
  49. b -= learning_rate * b.grad
  50. c -= learning_rate * c.grad
  51. d -= learning_rate * d.grad
  52. # 手动设置a, b, c, d的梯度为0
  53. a.grad = None
  54. b.grad = None
  55. c.grad = None
  56. d.grad = None
  57. print(f'Result: y = {a.item()} + {b.item()} * P3({c.item()} + {d.item()} x)')

参考链接:https://pytorch.org/tutorials/beginner/pytorch_with_examples.html#pytorch-defining-new-autograd-functions

五、GRL的使用

上面定义了一个GRL翻转层,这个层怎么使用呢?知乎上这位博主给了一个”hello, world”,内容如下

5.1定义GRL

  1. from torch.autograd import Function
  2. class GRL(Function):
  3. @staticmethod
  4. def forward(ctx, x, alpha):
  5. ctx.alpha = alpha
  6. return x.view_as(x)
  7. @staticmethod
  8. def backward(ctx, grad_output):
  9. output = grad_output.neg() * ctx.alpha
  10. return output, None

5.2 使用该层

通过上面的论文我们知道,GRL是在特征提取层以后的,后接域分类器,所以整个网络的框架如下

  1. class DANN(nn.Module):
  2. def __init__(self,num_classes=10):
  3. super(DANN,self).__init__()
  4. # 1.define the feature extraction layer
  5. self.features=nn.Sequential(
  6. nn.Conv2d(3,32,5),
  7. nn.ReLU(inplace=True),
  8. nn.MaxPool2d(2),
  9. nn.Conv2d(32,48,5),
  10. nn.ReLU(inplace=True),
  11. nn.MaxPool2d(2),
  12. )
  13. self.avgpool=nn.AdaptiveAvgPool2d((5,5))
  14. # 2.define the classification layer
  15. self.task_classifier=nn.Sequential(
  16. nn.Linear(48*5*5,100),
  17. nn.ReLU(inplace=True),
  18. nn.Linear(100,100),
  19. nn.ReLU(inplace=True),
  20. nn.Linear(100,num_classes)
  21. )
  22. # 3.define the domain classification layer
  23. self.domain_classifier=nn.Sequential(
  24. nn.Linear(48*5*5,100),
  25. nn.ReLU(inplace=True),
  26. nn.Linear(100,2)
  27. )
  28. self.GRL=GRL()
  29. def forward(self,x,alpha):
  30. x = x.expand(x.data.shape[0], 3, image_size,image_size)
  31. x=self.features(x)
  32. x=self.avgpool(x)
  33. x=torch.flatten(x,1)
  34. task_predict=self.task_classifier(x)
  35. x=GRL.apply(x,alpha)
  36. domain_predict=self.domain_classifier(x)
  37. return task_predict,domain_predict