论文地址:Discriminative Deep Hashing for Scalable Face Image Retrieval(DDH), IJCAI 2017
Github地址:GitHub - 1113609151/pytorch_DDH
1. torch.nn.CrossEntropyLoss
的使用
torch.nn.CrossEntropyLoss(input, target)
中的标签target
既可以使用one-hot
形式,也可以是类别的序号。形如target = [1, 3, 2]
或者target = [[1, 0, 0], [0, 0, 1], [0, 1, 0]]
。表示3个样本分别属于第1类、第3类、第2类。不过官方建议以类别作为输入,性能会更高。官方示例代码如下。
>>> # Example of target with class indices
>>> loss = nn.CrossEntropyLoss()
>>> input = torch.randn(3, 5, requires_grad=True)
>>> target = torch.empty(3, dtype=torch.long).random_(5)
>>> output = loss(input, target)
>>> output.backward()
>>>
>>> # Example of target with class probabilities
>>> input = torch.randn(3, 5, requires_grad=True)
>>> target = torch.randn(3, 5).softmax(dim=1)
>>> output = loss(input, target)
>>> output.backward()
torch.nn.CrossEntropyLoss(input, target)
的input
是没有归一化的每个类的得分,而不是softmax
之后的分布。softmax
过程在模块内计算,无需在外部计算。
2. Pytorch版的Locally connected layers
的实现
- Pytorch官方社区讨论
- GitHub PR版
- 虽然PR版最终没有通过,但复现代码时仍旧使用了它。
3. 记录numpy相关函数的使用
np.vstack([List])
,np.hstack([List])
,np.concatenate([List], axis=)
np.vstack()
:垂直堆叠(vertical stack
)函数。它将多个数组按垂直方向堆叠在一起,即沿着行的方向进行堆叠。这意味着它将数组按行连接起来,生成一个新的数组。例如,假设有两个数组array1
和array2
,它们的形状都是(m,n),其中m表示行数,n表示列数。使用np.vstack()
函数将它们垂直堆叠在一起,将生成一个新的数组,形状为(2m,n),即行数变为原来的两倍。等价于np.concatenate((array1,array2), axis=0)
np.hstack()
:水平堆叠(horizontal stack
)函数。它将多个数组按水平方向堆叠在一起,即沿着列的方向进行堆叠。这意味着它将数组按列连接起来,生成一个新的数组。例如,假设有两个数组array1
和array2
,它们的形状都是(m, n),其中m表示行数,n表示列数。使用np.hstack()
函数将它们水平堆叠在一起,将生成一个新的数组,形状为(m, 2n),即列数变为原来的两倍。等价于np.concatenate((array1,array2), axis=1)
#在项目中的运用
datas = []
labels = []
for file_name in file_names:
with open(file_name, 'rb') as f:
x, y = pickle.load(f, encoding='latin1')
datas.append(x)
labels.append(y)
data_array = np.vstack(datas)
label_array = np.hstack(labels)
4. 关于torchvision.transforms
- 在使用
torchvision.transforms
对图像进行transform时,要将图片的维度调整为[Height, Width, Channels]
。若使用PIL读取图片,需要将图片convert to RGB。ToTensor()
将shape
为(H, W, C)
的nump.ndarray
或img
转为shape
为(C, H, W)
的tensor
,其将每一个数值归一化到[0,1]
,其归一化方法比较简单,直接除以255即可。
5. 创建和读取checkpoints
torch.save(model.state_dict(), WEIGHTS_SAVE_PATH + WEIGHTS_FILE_NAME)
model.load_state_dict(torch.load(WEIGHTS_SAVE_PATH + WEIGHTS_FILE_NAME))
6. CrossEntropyLoss
基本不下降
经过200个epcho的训练,CrossEntropyLoss从7.43下降为7.37,基本没有变化。损失函数的公式如下所示。代码中分别取为0.0001,为0.01。其中经过训练,模型参数损失由于量级较小,变化不大;01损失由53.75下降为1.6105,变化明显。
min
∑
i
=
1
N
∑
j
=
1
M
−
Y
i
j
log
e
w
j
T
b
i
∑
k
=
1
M
e
w
k
T
b
i
+
α
2
(
∥
W
∥
F
2
+
Ω
)
+
β
∑
i
=
1
N
∥
∣
b
i
∣
−
1
∥
1
\begin{aligned}\min&\sum_{i=1}^N\sum_{j=1}^M-Y_{ij}\log\frac{e^{\mathbf{w}_j^T\mathbf{b}_i}}{\sum_{k=1}^Me^{\mathbf{w}_k^T\mathbf{b}_i}}+\frac{\alpha}{2}(\|\mathbf{W}\|_F^2+\Omega)+\beta\sum_{i=1}^N\||\mathbf{b}_i|-\mathbf{1}\|_1\end{aligned}
mini=1∑Nj=1∑M−Yijlog∑k=1MewkTbiewjTbi+2α(∥W∥F2+Ω)+βi=1∑N∥∣bi∣−1∥1
经过训练,模型的输出哈希码大多分布在-1与1之中,效果良好;但CrossEntropyLoss变化不大,导致模型无法分辨人像所属类别。因此暂且仅保留CrossEntropyLoss进行训练,找出问题所在。在仅保留CrossEntropyLoss的情况下,经过训练200个epcho,CrossEntropyLoss依旧由7.43下降为7.36,下降幅度依旧不大,证明该问题与另外两个Loss之间的关系不大。
- 由于
torch.nn.CrossEntropyLoss(input,target)
自带softmax
操作,如果model
内有softmax
层则会导致进行了双层softmax
,使梯度无法传达。经检查不存在此问题。 - 检查
torch.nn.CrossEntropyLoss(input,target)
前的语句。
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output, prob = model(data)
label = torch.argmax(target, dim=1)
loss_cel = nn.CrossEntropyLoss()(prob, label)
其中,output(size=[batch, hash_num])
为输出的长度为hash_num
的哈希码,prob(size=[batch, class_num])
为长度为class_num
的向量,对应每个种类的概率。由于target(size=[batch class_num])
为one-hot
类型,在计算前需要转化为label(size = [batch,])
形式。经检验,转换后的label与原始label值相同,因此不存在问题。
data_array, label_array = load_data_xy(get_files(TRAIN_SET_PATH))
dataset = DDH_train_dataset(TRAIN_SET_PATH)
dataloader = DataLoader(dataset, batch_size=32, shuffle=False, num_workers=0)
data, target = next(iter(dataloader))
labels = torch.argmax(target, dim=1)
true_label = label_array[:32]
print(torch.from_numpy(true_label) == labels)
#tensor([True, True, True, True, True, True, True, True, True, True, True, True,
#True, True, True, True, True, True, True, True, True, True, True, True,
#True, True, True, True, True, True, True, True])
- 检查模型架构,发现问题!!!
在模型代码中,有些网络并没有在__init__
层被定义,而是直接在forward
函数中创建并进行使用,如下所示。代码块中,nn.Linear(x.shape[1], self.hash_num * self.split_num)
和nn.Linear(slice_array.shape[1], 1)
均为原地创建并使用,此处出现了问题。
def forward(self, x):
...
x = nn.Linear(x.shape[1], self.hash_num * self.split_num).cuda()(x)
x = self.C5_block(x)
outs = []
for i in range(self.hash_num):
slice_array = x[:, i * self.split_num : (i + 1) * self.split_num]
fuse_layer = nn.Linear(slice_array.shape[1], 1).cuda()
outs.append(fuse_layer(slice_array))
...
return x, prob
测试代码如下。输出为空,表示model
没有可优化的参数。说明在forward
层中创建的layer
不会参与优化过程!!
class MyModel(nn.Module):
def __init__(self):
super(MyModel, self).__init__()
def forward(self, x):
x = nn.Linear(10, 5)(x) # 在forward方法中创建的网络层
return x
model = MyModel()
for name, param in model.named_parameters():
if param.requires_grad:
print(name, param.data)
#输出为空
修改后,代码如下,问题解决!
self.module_list = nn.ModuleList([])
for _ in range(self.hash_num):
self.module_list.append(nn.Linear(self.split_num, 1))
7.关于model.eval()
model.eval()
主要是针对model 在训练时和评价时不同的 Batch Normalization
和 Dropout
方法模式。
如果模型中有 Batch Normalization
层和 Dropout
层,需要在训练时添加 model.train()
,在测试时添加 model.eval()
。
model.eval()
时,pytorch 会自动把 BN 和 DropOut 固定住,不会取平均,而是用训练好的值。不然的话,一旦 test 的 batch_size
过小,很容易就会被 BN 层导致生成图片颜色失真极大。model.eval()
在非训练的时候是需要加的,没有这句代码,一些网络层的值会发生变动,不会固定,你神经网络每一次生成的结果也是不固定的,生成质量可能好也可能不好。
8.关于logging
import logging
import os
import time
import utils.config as cf
def getLogger():
logger = logging.getLogger()
logger.setLevel(logging.INFO) # Log等级总开关
formatter = logging.Formatter(fmt="[%(asctime)s|%(filename)s|%(levelname)s] %(message)s",
datefmt="%a %b %d %H:%M:%S %Y")
# StreamHandler
sHandler = logging.StreamHandler()
sHandler.setFormatter(formatter)
logger.addHandler(sHandler)
# FileHandler
work_dir = os.path.join(cf.train_log,
time.strftime("%Y-%m-%d-%H.%M", time.localtime())) # 日志文件写入目录
if not os.path.exists(work_dir):
os.makedirs(work_dir)
fHandler = logging.FileHandler(work_dir + '/log.txt', mode='w')
fHandler.setLevel(logging.DEBUG) # 输出到file的log等级的开关
fHandler.setFormatter(formatter) # 定义handler的输出格式
logger.addHandler(fHandler) # 将logger添加到handler里面
return logger
logger = get_logger()
logger.info(f'los = {}')