0%

ECN

0. 前言

钟准团队的CCPR2019文章,牛逼。思想有一丢丢类似聚类,但是比聚类强的是,赋予了id。与HHL文章有很大的不同。什也不说了,虚心学习。

  • [x] 2019-06-03: 这篇文章和 One Example reID 有些相似啊。感觉大家的论文之间的联系错综复杂。

1. Introduction

作者的注意力还是集中在如何区分 target domain 中的图片。考虑了三个因素,exemplar-invariance, camera-invariance and neighborhood-invariance. 可以简单地理解成每个人是一类,StarGAN生成的图片是一类,特征相似的是一类,并且为了实现上述目标,需要存储 target domain 中 train 数据集的所有特征,这一点有点疑问,需要的存储空间和计算时间消耗大吗?作者在第一节的最后有解答,消耗的空间和时间都很小。等自己实验的时候就知道了。它是用了两个分类器,一个用于训练 source 数据集, label 是 source 的label,一个用于训练 target 数据集, label 是 target 的label,两个label不一样。网络模型与HHL略有不同。

第一,通过分类损失使 target domain 的图片全部可分。分类模型学到的更可能是图片视觉上的相似性而不是语义相似性,并且数据集中同一id的图片在视觉上变化很大,所以,索性将target数据集上的每张图片作为一类,迫使不同id的相似图片变得不相似。行吧,这个理由,(⊙o⊙)…,怎么有点牵强吧,因为和后面的特征相似的作为一类有一丢丢矛盾。不对,有点类似既然不知道你们的id,那就把让你们之间先全部可分,这个思路在哪篇论文见过,想不起来了。

第二,同一 content 不同 camera style 的图片视为一类。这个和HHL的数据集处理方法一样,用 StarGAN 生成同一图片在不同 camera 下的图片,并视为一类,因为刚刚对图片使用了分类损失,所以这时候可以直接用到分类损失上,而不用再考虑三元组损失,(我觉得可能是基于分类损失的作用一直大于二元组损失,三元组损失),这时可以保证同一 content 不同 camera style 的图片视为一类。这时 StarGAN 生成图片中的人的形态等基本保留,感觉更多是亮度上的变化(肉眼可见),偶尔会有衣服颜色的变换,但是不能变换这个人的姿势,比如正面走变成侧面走这种,HHL 已经证明了 camera style 的影响,但是我觉得姿势的变换也是一个重要的因素才对,因为分类模型的起家就是识别同一类物体不同形态的图片。好像 CVPR 2019 这个团队有做这方面的内容,抽空膜拜一波。

第三,k近邻的图片视为一类。这应该像是之间看到的一些关于聚类的或者k近邻的论文,总体思想是因为不知道图片的真id,那就可能赋予一个比较接近真实的id,又因为同一id的图片的特征应该是临近的,所以把临近的图片作为同一id也不过分,其实有的论文用的是聚类,但本质是一样的。

我觉得钟准厉害的地方不仅仅是能想到这三个创新点,更重要的是能把这三个创新点融合到一起,因为这三个创新点在别的论文里或多或少都可以看到影子,但是因为实现的的方法、思路、具体过程都不一样,很难看见一个创新点就把融到一起,看到一个想法就放到自己的模型里,或者看到一个idea就觉得有用并且一试还真有用,并能给出属于自己的解释,而不是明显的生搬硬造,比如这里的 memory module 很有想法。

Unsupervised domain adaptation. 一种方法是对齐两个域的特征,其基本假设是源域和目标域的类别一样。另一种方法是丢弃目标域的未知类别样本,学习源域到目标域的映射。

Unsupervised person re-identification. ECCV2018_HHL 可能忽略了 target domain 中其他因素对 id 的影响,比如姿势等。与 BMVC2018_DAL 不同的是,作者设计了软分类损失。

3. The Proposed Method

简单定义下数学符号,直接用英文吧,能知道就行。

数学符号 含义
$\lbrace X_s, Y_s \rbrace, N_s$ source domain, $Ns$ person images, $(x{si}, y_{s,i})$, $M$ person identities
$X_t, N_t$ target domain, $N_t$ person images

3.1 Overview of Framework

其实模型没有画的这么复杂,基本还是保留ResNet-50的基本网络,后面加个FC-4049,FC-#id/ exemplar memory,其中FC-#id用于 source domain 的数据, exemplar memory 用于计算 target domain 的数据,等下直接看代码一目了然。

3.2 Supervised Learning for Source Domain

这个容易理解, source domain 直接用个分类损失。

3.3 Exemplar Memory

Exemplar Memory 借鉴的是 Joint Detection and Identification Feature Learning for Person Search,(已经有两篇论文没有看过了,自己菜不是没有原因的),采用的是 key-value 架构 $K,V$,其中在 key-memory 中存储每张图片的 FC-4096 特征, value-memory 中存储图片的lable,其中视每张图片为一类,index 即为 label。key-memory 中的特征初始化成0,value-memory 初始化成 $V[i]=i$,并且在整个训练过程中保持不变。key-memory 的更新策略是:

其中$\alpha\in [0,1]$,$K[i]\gets \parallel K[i] \parallel_2$,也就是每次都会进行一次归一化。这里的 $\alpha$ 是线性变化的,初始化为 0.01, 后续变化为 $\alpha = 0.01*epoch $。其中每次更新都是发生在反向传播时,顺便进行更新的。写的很巧妙。

3.4 Invariance Learning for Target Domain

开始祭出三板斧了。

Exemplar-invariance: 因为同一id的图片在外观上也会有很大的变化,即每一张图片都应该只与自己相似,与其他图片差别很大,因此,可以视每一张图片为一类。具体过程是,对于给定图片$x{t,i}$,先计算 $x{t,i}$ 的特征与其他图片在 key-memory 存储的特征的 cosine 距离,距离越大越相似。然后用这个距离去预测 $x_{t,i}$ 属于第 i 类的概率。这种预测与我见过的分类不太一样,因为分类模型一般都是直接用特征去预测类别,但这里是用距离去预测类别,或者说用距离当预测概率,还是第一次见。

其中 $\beta\in (0,1]$ ,用于平衡分布的趋势,记为 temperature,$\beta=0.05$,根据代码来看,$K[i]$ 表示当前 model 运行之前存储的, $f(x_{t,i})$表示当前 model 的结果,也就是说两者不一样。下面求概率的过程同理。

这个公式和 distillation 很像。

其中,$z_i$是类别预测概率。

所以分类损失可以写成:

Camera-invariance:视经过 StarGAN 转换的图片$\hat{x}_{t,i}$是同一类,与HHL一样,但是与HHL不同的是,HHL用于三元组损失,ECN用于分类损失,类似 Exemplar-invariance 的概率求解和loss损失,得:

其中,$\hat{x} {t,i}$是在生成的C张图片中$\lbrace \hat{x} {t,i,1},…, \hat{x}_{t,i,C} \rbrace$随机选了一张。

这里有个问题,Exemplar Memory 中是否包含 CamStyle 生成的图片。等看代码就知道了。

Neighborhood-invariance: 对每一张图片,在数据集中一定至少存在一张与之相同id的图片,如果能得到这些图片,那就更好了,这样就可以获得同一id不同姿态的图片。具体方法是在 exemplar memory 中计算 cos 距离,用k近邻方法得到最近的k张图片的index,记为 $M(x_{t,i},k)$,显然最近的是i. $k=6$

基于假设k近邻得到的图片$M(x{t,i},k)$属于同一类,因此可以得到 $x{t,i}$ 属于第 j 类的概率的权重是:

这种赋予权重的方法是把 $x{t,i}$ 属于另一张图片类别的概率,而不是我之前以为的 $M(x{t,i},k)$ 中的图片属于 $x_{t,i}$ 类别的概率。

因此损失可以写成:

其中,为了区分 Exemplar-invariance 和 Neighborhood-invariance ,这里没有再次计算 $x_{t,i}$ 属于第 i 类的损失。

Overall loss of invariance learning: invariance learning 的总损失可以表示成:

其中,$x^* {t,i}$ 表示随机从$\lbrace x{t,i}, \hat{x} {t,i,1},…, \hat{x}{t,i,C}, \rbrace$ 选一张。

看看代码再说这个损失的具体实现吧。

3.5 Final Loss for Network

其中,$\lambda \in (0, 1]$, $\lambda=0.3$

这里的损失没有用常见的加法,而是一个线性组合?

4. Experiment

4.1 Experiment Setting

前5个 epoch 只训练 exemplar-invariance and camera-invariance,也就是通用的交叉熵损失,5个epoch之后再加入 neighborhood-invariance。

4.2 Parameter Analysis

temperature fact $\beta$, weight of loss $\lambda$, number of candidate positive samples $k$.

Temperature fact $\beta$

当 $\beta$ 比较小的时候,分布越陡,值越大,损失越小,结果也越好。最好的结果是 $\beta$ 在0.05 左右。同时通过表格可以看出,当 $\beta$ 变化从0.05到0.5时,rank-1下降了17。但同时,在最优解的附近也接近最优解。

1
2
3
4
5
6
7
8
9
10
# 在蒸馏网络中,T越大,分布越平缓
from torch.nn import functional as F
import torch
fun1 = F.softmax
aa = torch.tensor([0.2, 0.8, 0.2])
fun1(aa, dim=0)
tensor([0.2616, 0.4767, 0.2616])
bb = torch.tensor([0.2, 0.8, 0.2])/0.1
fun1(bb, dim=0)
tensor([0.0025, 0.9951, 0.0025])

The weight of source and target losses $\lambda$:

可以看出即便只有 target loss, 性能也超过 baseline, 当取其他值的时候,性能几乎不变。

Number of positive samples $k$:

可以看出,性能对k挺敏感的。

4.4 Evaluation

通过表格中的数据集可以计算出,C+N>C,N,C和N共同使用互相还有促进作用。

作者也在 MSMT 数据集上做了训练测试。

感觉大家都开始在 MSMT17 上做实验了,不怎么管 CUHK 了。

5. code

代码和HHL的差不多

5.1 model

5.1.1 baseline

有以下几个改变:

  • conv1 ~ layer2 的参数不再更新
  • source: layer4-avgpool-Linear(4096)-bn-relu-drop(0.5)-Linear(num_class)
  • target: layer4-avgpool-Linear(4096)-bn-F.normalize-drop(0.5)

Question:先normalize后drop的话就不满足归一化的定义了

Question: conv1~layer2 固定参数是有什么含义吗?

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
class ResNet(nn.Module):
__factory = {
18: torchvision.models.resnet18,
34: torchvision.models.resnet34,
50: torchvision.models.resnet50,
101: torchvision.models.resnet101,
152: torchvision.models.resnet152,
}

def __init__(self, depth, pretrained=True, cut_at_pooling=False,
num_features=0, norm=False, dropout=0, num_classes=0, num_triplet_features=0):
super(ResNet, self).__init__()

self.depth = depth
self.pretrained = pretrained
self.cut_at_pooling = cut_at_pooling

# Construct base (pretrained) resnet
if depth not in ResNet.__factory:
raise KeyError("Unsupported depth:", depth)
self.base = ResNet.__factory[depth](pretrained=pretrained)

# Fix layers [conv1 ~ layer2]
fixed_names = []
for name, module in self.base._modules.items():
if name == "layer3":
# assert fixed_names == ["conv1", "bn1", "relu", "maxpool", "layer1", "layer2"]
break
fixed_names.append(name)
for param in module.parameters():
param.requires_grad = False

if not self.cut_at_pooling:
self.num_features = num_features
self.norm = norm
self.dropout = dropout
self.has_embedding = num_features > 0
self.num_classes = num_classes
self.num_triplet_features = num_triplet_features

self.l2norm = Normalize(2)

out_planes = self.base.fc.in_features

# Append new layers
if self.has_embedding:
self.feat = nn.Linear(out_planes, self.num_features)
self.feat_bn = nn.BatchNorm1d(self.num_features)
init.kaiming_normal_(self.feat.weight, mode='fan_out')
init.constant_(self.feat.bias, 0)
init.constant_(self.feat_bn.weight, 1)
init.constant_(self.feat_bn.bias, 0)
else:
# Change the num_features to CNN output channels
self.num_features = out_planes
if self.dropout >= 0:
self.drop = nn.Dropout(self.dropout)
if self.num_classes > 0:
self.classifier = nn.Linear(self.num_features, self.num_classes)
init.normal_(self.classifier.weight, std=0.001)
init.constant_(self.classifier.bias, 0)

if not self.pretrained:
self.reset_params()

def forward(self, x, output_feature=None):
for name, module in self.base._modules.items():
if name == 'avgpool':
break
else:
x = module(x)

if self.cut_at_pooling:
return x

x = F.avg_pool2d(x, x.size()[2:])
x = x.view(x.size(0), -1)

if output_feature == 'pool5':
x = F.normalize(x)
return x

if self.has_embedding:
x = self.feat(x)
x = self.feat_bn(x)
tgt_feat = F.normalize(x)
tgt_feat = self.drop(tgt_feat)
if output_feature == 'tgt_feat':
return tgt_feat
if self.norm:
x = F.normalize(x)
elif self.has_embedding:
x = F.relu(x)
if self.dropout > 0:
x = self.drop(x)
if self.num_classes > 0:
x = self.classifier(x)
return x

def reset_params(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
init.kaiming_normal(m.weight, mode='fan_out')
if m.bias is not None:
init.constant(m.bias, 0)
elif isinstance(m, nn.BatchNorm2d):
init.constant(m.weight, 1)
init.constant(m.bias, 0)
elif isinstance(m, nn.Linear):
init.normal(m.weight, std=0.001)
if m.bias is not None:
init.constant(m.bias, 0)

L2 正则化定义了专门的层,但没有使用,可能是嫌慢吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import torch
from torch.autograd import Variable
from torch import nn

class Normalize(nn.Module):

def __init__(self, power=2):
super(Normalize, self).__init__()
self.power = power

def forward(self, x):
norm = x.pow(self.power).sum(1, keepdim=True).pow(1./self.power)
out = x.div(norm)
return out

5.1.2 InvNet

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# Pytorch>=1.0.0
import torch
import torch.nn.functional as F
from torch import nn, autograd
from torch.autograd import Variable, Function
import numpy as np
import math
# 这应该是最新版的自定义 Function 的实现
class ExemplarMemory(Function):
def __init__(self, em, alpha=0.01):
super(ExemplarMemory, self).__init__()
self.em = em
self.alpha = alpha

def forward(self, inputs, targets):
# inputs: b*2048, targets: b*1 (index)
self.save_for_backward(inputs, targets)
# outputs: b*12936
outputs = inputs.mm(self.em.t())
return outputs

def backward(self, grad_outputs):
# 这个backward 主要用于更新 em
inputs, targets = self.saved_tensors
grad_inputs = None
if self.needs_input_grad[0]:
grad_inputs = grad_outputs.mm(self.em)
for x, y in zip(inputs, targets):
self.em[y] = self.alpha * self.em[y] + (1. - self.alpha) * x
self.em[y] /= self.em[y].norm()
return grad_inputs, None


# Invariance learning loss
class InvNet(nn.Module):
def __init__(self, num_features, num_classes, beta=0.05, knn=6, alpha=0.01):
super(InvNet, self).__init__()
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
self.num_features = num_features
self.num_classes = num_classes
self.alpha = alpha # Memory update rate
self.beta = beta # Temperature fact
self.knn = knn # Knn for neighborhood invariance

# Exemplar memory, 12936x2048
self.em = nn.Parameter(torch.zeros(num_classes, num_features))

def forward(self, inputs, targets, epoch=None):
# inputs: b*2048, targets: b*1 (index)
alpha = self.alpha * epoch
# 每次都是重建一个 ExemplarMemory,我觉得可能是因为alpha每次要改变。
inputs = ExemplarMemory(self.em, alpha=alpha)(inputs, targets)
# inputs: b*12936

inputs /= self.beta
if self.knn > 0 and epoch > 4:
# With neighborhood invariance
loss = self.smooth_loss(inputs, targets)
else:
# Without neighborhood invariance
loss = F.cross_entropy(inputs, targets)
return loss

def smooth_loss(self, inputs, targets):
# overall loss of invariance loss
# inputs: b*12936, targets: b*1 (index)
targets = self.smooth_hot(inputs.detach().clone(), targets.detach().clone(), self.knn)
# targets: b*12936, weights
outputs = F.log_softmax(inputs, dim=1)
loss = - (targets * outputs)
loss = loss.sum(dim=1)
loss = loss.mean(dim=0)
return loss

def smooth_hot(self, inputs, targets, k=6):
# Sort
# inputs: b*12936, targets: b*1 (index)
# targets_onehot: b*12936
# targets_onehot: 记录的是当前样本属于其他类的概率的权重 1/k or 1
_, index_sorted = torch.sort(inputs, dim=1, descending=True)

ones_mat = torch.ones(targets.size(0), k).to(self.device)
targets = torch.unsqueeze(targets, 1)
targets_onehot = torch.zeros(inputs.size()).to(self.device)
# 这里的 weights 应该是 1/k, 因为每个值都一样,所以 softmax 之后就是 1/k
# 猜测作者这里刚开始不是直接想要 1/k 的权重,而是根据距离远近赋予权重,比如选
# 择6个最近的,然后根据相似性赋予权重(等同于概率的求解)
# Question:weights = F.softmax(ones_mat, dim=1)
# targets_onehot.scatter_(1, index_sorted[:, 0:k], ones_mat * weights)
weights = F.softmax(ones_mat, dim=1)
# 根据位置填充权重
targets_onehot.scatter_(1, index_sorted[:, 0:k], ones_mat * weights)
# Question: 怎么保证前k个就一定没有index?
targets_onehot.scatter_(1, targets, float(1))

return targets_onehot

补充 gatherscatter 的用法

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
# gather
# torch.gather(input, dim, index, out=None)
# output[i][j][k]=input[index[[i][j][k]]][j][k] # if dim=0
# output[i][j][k]=input[i][index[[i][j][k]]][k] # if dim=1
# output[i][j][k]=input[i][j][index[[i][j][k]]] # if dim=2
# 光看公式很绕,
# 其中 out 的size和 index 的size相同
# input: a0*a1*a2*...ai-1*ai*ai+1,...,an-1
# index: a0*a1*a2*...ai-1*y*ai+1,...,an-1
# output:a0*a1*a2*...ai-1*y*ai+1,...,an-1
# 除了第dim维,其他维度上 index 的size和input的size相同

# 或者我们换个角度,不要管公式,把index分成y份,index_0,index_1, ..., index_y-1
# index_0: a0*a1*a2*...ai-1*1*ai+1,...,an-1
# index_0 就是在第i个维度上选取对应位置第y[0]个通道的数字,
# 以此类推,下面用二维矩阵做个示范
# 可以理解成降维
import torch
input = torch.Tensor([[1,2],[3,4]])
tensor([[1., 2.],
[3., 4.]])
# [0,0] 表示选取第0维第0个通道的数字,第0维第0个通道的数字
torch.gather(input, 0, torch.LongTensor([[0,0]]) )
tensor([[1., 2.]])
# [0,0] 表示选取第0维第0个通道的数字,第0维第1个通道的数字
torch.gather(input, 0, torch.LongTensor([[0,1]]) )
tensor([[1., 4.]])
torch.gather(input, 0, torch.LongTensor([[0,1], [0,1], [0,1]]) )
tensor([[1., 4.],
[1., 4.],
[1., 4.]])
# 三维矩阵
# 三维矩阵更好理解,可以通过投射的方式理解
a = torch.arange(0,27).view(3,3,3)
# 第0维第0个通道
tensor([[[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8]],
# 第0维第1个通道
[[ 9, 10, 11],
[12, 13, 14],
[15, 16, 17]],
# 第0维第2个通道
[[18, 19, 20],
[21, 22, 23],
[24, 25, 26]]])
# 在对应位置,分别选取第0维的第0个,第1个,第2个通道的数字
c = [[[0,1,2],[0,1,2],[0,1,2]]]
index = torch.LongTensor(c)
a.gather(0, index)
tensor([[[ 0, 10, 20],
[ 3, 13, 23],
[ 6, 16, 26]]])
# output的第0维第0个通道:分别选取0,1,2
# output的第0维第1个通道:分别选取0,1,0
c = [[[0,1,2],[0,1,2],[0,1,2]], [[0,1,0],[0,1,0],[0,1,0]]]
index = torch.LongTensor(c)
a.gather(0, index)
tensor([[[ 0, 10, 20],
[ 3, 13, 23],
[ 6, 16, 26]],

[[ 0, 10, 2],
[ 3, 13, 5],
[ 6, 16, 8]]])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# scatter
# torch.scatter_(dim, index, src)
# 可以理解成gather的相反操作
# src 和 index 的size相同
# out[index[i][j][k]][j][k]=src[i][j][k] if dim=0
# out[i][index[i][j][k]][k]=src[i][j][k] if dim=1
# out[i][j][index[i][j][k]]=src[i][j][k] if dim=2
x = torch.rand(2, 5)
0.4319 0.6500 0.4080 0.8760 0.2355
0.2609 0.4711 0.8486 0.8573 0.1029
[torch.FloatTensor of size 2x5]
torch.zeros(3, 5).scatter_(0, torch.LongTensor([[0, 1, 2, 0, 0], [2, 0, 0, 1, 2]]), x)
0.4319 0.4711 0.8486 0.8760 0.2355
0.0000 0.6500 0.0000 0.8573 0.0000
0.2609 0.0000 0.4080 0.0000 0.1029
[torch.FloatTensor of size 3x5]

5.2 data

对于 target domain 中的图片,每次都是从原始图片和生成图片组成的集合中随机取一张图片,当选取的 camid 是原始图片的 camid 时,取原始图片,当取到的 camid 不是原始图片的 camid 时,取生成图片,实际只使用了 C-1 张生成图片和 1 张原始图片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# reid/utils/data/preprocessor.py
def _get_single_item(self, index):
fname, pid, camid = self.dataset[index]
sel_cam = torch.randperm(self.num_cam)[0]
if sel_cam == camid:
fpath = osp.join(self.root, fname)
img = Image.open(fpath).convert('RGB')
else:
if 'msmt' in self.root:
fname = fname[:-4] + '_fake_' + str(sel_cam.numpy() + 1) + '.jpg'
else:
fname = fname[:-4] + '_fake_' + str(camid + 1) + 'to' + str(sel_cam.numpy() + 1) + '.jpg'
fpath = osp.join(self.camstyle_root, fname)
img = Image.open(fpath).convert('RGB')
if self.transform is not None:
img = self.transform(img)
return img, fname, pid, index

5.3 optimizer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 这个写的可以,清晰明了,只是在HHL中,只有classifier层设置为1.0,其他新层则是0.0,这里则把新层全部设置成了1.0,应该也是实验所得吧。
base_param_ids = set(map(id, model.module.base.parameters()))

base_params_need_for_grad = filter(lambda p: p.requires_grad, model.module.base.parameters())

new_params = [p for p in model.parameters() if
id(p) not in base_param_ids]
param_groups = [
{'params': base_params_need_for_grad, 'lr_mult': 0.1},
{'params': new_params, 'lr_mult': 1.0}]

optimizer = torch.optim.SGD(param_groups, lr=args.lr,
momentum=args.momentum,
weight_decay=args.weight_decay,
nesterov=True)

5.4 train

1
2
3
4
5
6
7
8
9
10
11
12
13
# main.py and reid/trainers.py
# self.model: ResNet-50
# self.model_inv : InvNet

# source domain 的交叉熵损失
inputs, pids = self._parse_data(inputs)
outputs = self.model(inputs)
source_pid_loss = self.pid_criterion(outputs, pids)
# target domain 的损失
outputs = self.model(inputs_target, 'tgt_feat')
loss_un = self.model_inv(outputs, index_target, epoch=epoch)
# overall loss
loss = (1 - self.lmd) * source_pid_loss + self.lmd * loss_un

知识点补充:因为 ResNet-50 的 conv1~layer2 的参数已经设置不参与更新,自然这些层的 bn 层也应该是 eval 状态。

Question: 上面的知识点补充是否正确?

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
def set_model_train(self):
self.model.train()

# Fix first BN
fixed_bns = []
for idx, (name, module) in enumerate(self.model.module.named_modules()):
if name.find("layer3") != -1:
# assert len(fixed_bns) == 22
break
if name.find("bn") != -1:
fixed_bns.append(name)
module.eval()
len(fixed_bns)
22
['bn1',
'layer1.0.bn1',
'layer1.0.bn2',
'layer1.0.bn3',
'layer1.1.bn1',
'layer1.1.bn2',
'layer1.1.bn3',
'layer1.2.bn1',
'layer1.2.bn2',
'layer1.2.bn3',
'layer2.0.bn1',
'layer2.0.bn2',
'layer2.0.bn3',
'layer2.1.bn1',
'layer2.1.bn2',
'layer2.1.bn3',
'layer2.2.bn1',
'layer2.2.bn2',
'layer2.2.bn3',
'layer2.3.bn1',
'layer2.3.bn2',
'layer2.3.bn3']

到此应该代码全部看完了。