└── README.md /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 写在前面: 4 | 5 | 前段时间我将使用 `PyTorch` 的过程中总结的一些小trick发到 [知乎]() 上,突然就得到很多人点赞,非常感谢大家的认可。 6 | 7 | 有人评论可以转到github上,这样大家可以一起在上面分享自己的使用心得,我觉得这个主意不错,因为我个人接触面有限,总结不是很全面,如果大家群策群力,肯定能得到更好的结果。 8 | 9 | 啰嗦结束。 10 | 11 | 12 | 13 | 14 | 15 | ## pytorch_tricks 16 | 17 | 18 | 19 | **目录:** 20 | 21 | 1. **指定GPU编号** 22 | 2. **查看模型每层输出详情** 23 | 3. **梯度裁剪** 24 | 4. **扩展单张图片维度** 25 | 5. **独热编码** 26 | 6. **防止验证模型时爆显存** 27 | 7. **学习率衰减** 28 | 8. **冻结某些层的参数** 29 | 9. **对不同层使用不同学习率** 30 | 10. **模型相关操作** 31 | 11. **Pytorch内置one hot函数** 32 | 12. **网络参数初始化** 33 | 34 | 35 | 36 | 37 | ### 1、指定GPU编号 38 | 39 | - 设置当前使用的GPU设备仅为0号设备,设备名称为 `/gpu:0`:`os.environ["CUDA_VISIBLE_DEVICES"] = "0"` 40 | - 设置当前使用的GPU设备为0,1号两个设备,名称依次为 `/gpu:0`、`/gpu:1`: `os.environ["CUDA_VISIBLE_DEVICES"] = "0,1"` ,根据顺序表示优先使用0号设备,然后使用1号设备。 41 | 42 | 指定GPU的命令需要放在和神经网络相关的一系列操作的前面。 43 | 44 | 45 | 46 | 47 | 48 | ### 2、查看模型每层输出详情 49 | 50 | Keras有一个简洁的API来查看模型的每一层输出尺寸,这在调试网络时非常有用。现在在PyTorch中也可以实现这个功能。 51 | 52 | 使用很简单,如下用法: 53 | 54 | ```python3 55 | from torchsummary import summary 56 | summary(your_model, input_size=(channels, H, W)) 57 | ``` 58 | 59 | `input_size` 是根据你自己的网络模型的输入尺寸进行设置。 60 | 61 | [**pytorch-summary**](https://link.zhihu.com/?target=https%3A//github.com/sksq96/pytorch-summary) 62 | 63 | 64 | 65 | 66 | 67 | ### 3、梯度裁剪(Gradient Clipping) 68 | 69 | ```python3 70 | import torch.nn as nn 71 | 72 | outputs = model(data) 73 | loss= loss_fn(outputs, target) 74 | optimizer.zero_grad() 75 | loss.backward() 76 | nn.utils.clip_grad_norm_(model.parameters(), max_norm=20, norm_type=2) 77 | optimizer.step() 78 | ``` 79 | 80 | `nn.utils.clip_grad_norm_` 的参数: 81 | 82 | - **parameters** – 一个基于变量的迭代器,会进行梯度归一化 83 | - **max_norm** – 梯度的最大范数 84 | - **norm_type** – 规定范数的类型,默认为L2 85 | 86 | 知乎用户 `不椭的椭圆` 提出:梯度裁剪在某些任务上会额外消耗大量的计算时间,可移步评论区查看详情。 87 | 88 | 89 | 90 | 91 | 92 | ### 4、扩展单张图片维度 93 | 94 | 因为在训练时的数据维度一般都是 (batch_size, c, h, w),而在测试时只输入一张图片,所以需要扩展维度,扩展维度有多个方法: 95 | 96 | ```python3 97 | import cv2 98 | import torch 99 | 100 | image = cv2.imread(img_path) 101 | image = torch.tensor(image) 102 | print(image.size()) 103 | 104 | img = image.view(1, *image.size()) 105 | print(img.size()) 106 | 107 | # output: 108 | # torch.Size([h, w, c]) 109 | # torch.Size([1, h, w, c]) 110 | ``` 111 | 112 | 或 113 | 114 | ```python3 115 | import cv2 116 | import numpy as np 117 | 118 | image = cv2.imread(img_path) 119 | print(image.shape) 120 | img = image[np.newaxis, :, :, :] 121 | print(img.shape) 122 | 123 | # output: 124 | # (h, w, c) 125 | # (1, h, w, c) 126 | ``` 127 | 128 | 或(感谢知乎用户 `coldleaf` 的补充) 129 | 130 | ```python3 131 | import cv2 132 | import torch 133 | 134 | image = cv2.imread(img_path) 135 | image = torch.tensor(image) 136 | print(image.size()) 137 | 138 | img = image.unsqueeze(dim=0) 139 | print(img.size()) 140 | 141 | img = img.squeeze(dim=0) 142 | print(img.size()) 143 | 144 | # output: 145 | # torch.Size([(h, w, c)]) 146 | # torch.Size([1, h, w, c]) 147 | # torch.Size([h, w, c]) 148 | ``` 149 | 150 | `tensor.unsqueeze(dim)`:扩展维度,dim指定扩展哪个维度。 151 | 152 | `tensor.squeeze(dim)`:去除dim指定的且size为1的维度,维度大于1时,squeeze()不起作用,不指定dim时,去除所有size为1的维度。 153 | 154 | 155 | 156 | 157 | 158 | ### 5、独热编码 159 | 160 | 在PyTorch中使用交叉熵损失函数的时候会自动把label转化成onehot,所以不用手动转化,而使用MSE需要手动转化成onehot编码。 161 | 162 | ```python3 163 | import torch 164 | class_num = 8 165 | batch_size = 4 166 | 167 | def one_hot(label): 168 | """ 169 | 将一维列表转换为独热编码 170 | """ 171 | label = label.resize_(batch_size, 1) 172 | m_zeros = torch.zeros(batch_size, class_num) 173 | # 从 value 中取值,然后根据 dim 和 index 给相应位置赋值 174 | onehot = m_zeros.scatter_(1, label, 1) # (dim,index,value) 175 | 176 | return onehot.numpy() # Tensor -> Numpy 177 | 178 | label = torch.LongTensor(batch_size).random_() % class_num # 对随机数取余 179 | print(one_hot(label)) 180 | 181 | # output: 182 | [[0. 0. 0. 1. 0. 0. 0. 0.] 183 | [0. 0. 0. 0. 1. 0. 0. 0.] 184 | [0. 0. 1. 0. 0. 0. 0. 0.] 185 | [0. 1. 0. 0. 0. 0. 0. 0.]] 186 | ``` 187 | 188 | [Convert int into one-hot format]() 189 | 190 | 注:第11条有更简单的方法。 191 | 192 | 193 | 194 | 195 | ### 6、防止验证模型时爆显存 196 | 197 | 验证模型时不需要求导,即不需要梯度计算,关闭autograd,可以提高速度,节约内存。如果不关闭可能会爆显存。 198 | 199 | ```python3 200 | with torch.no_grad(): 201 | # 使用model进行预测的代码 202 | pass 203 | ``` 204 | 205 | 感谢知乎用户`zhaz` 的提醒,我把 `torch.cuda.empty_cache()` 的使用原因更新一下。 206 | 207 | 这是原回答: 208 | 209 | > Pytorch 训练时无用的临时变量可能会越来越多,导致 `out of memory` ,可以使用下面语句来清理这些不需要的变量。 210 | 211 | 212 | 213 | [官网](https://link.zhihu.com/?target=https%3A//pytorch.org/docs/stable/cuda.html%3Fhighlight%3Dcache%23torch.cuda.empty_cache) 上的解释为: 214 | 215 | > Releases all unoccupied cached memory currently held by the caching allocator so that those can be used in other GPU application and visible innvidia-smi.`torch.cuda.empty_cache()` 216 | 217 | 意思就是PyTorch的缓存分配器会事先分配一些固定的显存,即使实际上tensors并没有使用完这些显存,这些显存也不能被其他应用使用。这个分配过程由第一次CUDA内存访问触发的。 218 | 219 | 220 | 221 | 而 `torch.cuda.empty_cache()` 的作用就是释放缓存分配器当前持有的且未占用的缓存显存,以便这些显存可以被其他GPU应用程序中使用,并且通过 `nvidia-smi`命令可见。注意使用此命令不会释放tensors占用的显存。 222 | 223 | 对于不用的数据变量,Pytorch 可以自动进行回收从而释放相应的显存。 224 | 225 | 更详细的优化可以查看 [优化显存使用](https://link.zhihu.com/?target=https%3A//blog.csdn.net/qq_28660035/article/details/80688427) 和 [显存利用问题](https://link.zhihu.com/?target=https%3A//oldpan.me/archives/pytorch-gpu-memory-usage-track)。 226 | 227 | 228 | 229 | 230 | 231 | ### 7、学习率衰减 232 | 233 | ```python3 234 | import torch.optim as optim 235 | from torch.optim import lr_scheduler 236 | 237 | # 训练前的初始化 238 | optimizer = optim.Adam(net.parameters(), lr=0.001) 239 | scheduler = lr_scheduler.StepLR(optimizer, 10, 0.1) 240 | 241 | # 训练过程中 242 | for n in n_epoch: 243 | scheduler.step() 244 | ... 245 | ``` 246 | 247 | 关键语句为`lr_scheduler.StepLR(optimizer, 10, 0.1)`,表示每过10个epoch,学习率乘以0.1。 248 | 249 | 250 | 251 | 252 | 253 | ### 8、冻结某些层的参数 254 | 255 | 参考:[Pytorch 冻结预训练模型的某一层](https://www.zhihu.com/question/311095447/answer/589307812) 256 | 257 | 在加载预训练模型的时候,我们有时想冻结前面几层,使其参数在训练过程中不发生变化。 258 | 259 | 我们需要先知道每一层的名字,通过如下代码打印: 260 | 261 | ```python3 262 | net = Network() # 获取自定义网络结构 263 | for name, value in net.named_parameters(): 264 | print('name: {0},\t grad: {1}'.format(name, value.requires_grad)) 265 | ``` 266 | 267 | 268 | 269 | 假设前几层信息如下: 270 | 271 | ```python3 272 | name: cnn.VGG_16.convolution1_1.weight, grad: True 273 | name: cnn.VGG_16.convolution1_1.bias, grad: True 274 | name: cnn.VGG_16.convolution1_2.weight, grad: True 275 | name: cnn.VGG_16.convolution1_2.bias, grad: True 276 | name: cnn.VGG_16.convolution2_1.weight, grad: True 277 | name: cnn.VGG_16.convolution2_1.bias, grad: True 278 | name: cnn.VGG_16.convolution2_2.weight, grad: True 279 | name: cnn.VGG_16.convolution2_2.bias, grad: True 280 | ``` 281 | 282 | 283 | 284 | 后面的True表示该层的参数可训练,然后我们定义一个要冻结的层的列表: 285 | 286 | ```python3 287 | no_grad = [ 288 | 'cnn.VGG_16.convolution1_1.weight', 289 | 'cnn.VGG_16.convolution1_1.bias', 290 | 'cnn.VGG_16.convolution1_2.weight', 291 | 'cnn.VGG_16.convolution1_2.bias' 292 | ] 293 | ``` 294 | 295 | 296 | 297 | 冻结方法如下: 298 | 299 | ```python3 300 | net = Net.CTPN() # 获取网络结构 301 | for name, value in net.named_parameters(): 302 | if name in no_grad: 303 | value.requires_grad = False 304 | else: 305 | value.requires_grad = True 306 | ``` 307 | 308 | 309 | 310 | 冻结后我们再打印每层的信息: 311 | 312 | ```python3 313 | name: cnn.VGG_16.convolution1_1.weight, grad: False 314 | name: cnn.VGG_16.convolution1_1.bias, grad: False 315 | name: cnn.VGG_16.convolution1_2.weight, grad: False 316 | name: cnn.VGG_16.convolution1_2.bias, grad: False 317 | name: cnn.VGG_16.convolution2_1.weight, grad: True 318 | name: cnn.VGG_16.convolution2_1.bias, grad: True 319 | name: cnn.VGG_16.convolution2_2.weight, grad: True 320 | name: cnn.VGG_16.convolution2_2.bias, grad: True 321 | ``` 322 | 323 | 324 | 325 | 可以看到前两层的weight和bias的requires_grad都为False,表示它们不可训练。 326 | 327 | 最后在定义优化器时,只对requires_grad为True的层的参数进行更新。 328 | 329 | ```python3 330 | optimizer = optim.Adam(filter(lambda p: p.requires_grad, net.parameters()), lr=0.01) 331 | ``` 332 | 333 | 334 | 335 | 336 | 337 | ### 9、对不同层使用不同学习率 338 | 339 | 我们对模型的不同层使用不同的学习率。 340 | 341 | 还是使用这个模型作为例子: 342 | 343 | ```python3 344 | net = Network() # 获取自定义网络结构 345 | for name, value in net.named_parameters(): 346 | print('name: {}'.format(name)) 347 | 348 | # 输出: 349 | # name: cnn.VGG_16.convolution1_1.weight 350 | # name: cnn.VGG_16.convolution1_1.bias 351 | # name: cnn.VGG_16.convolution1_2.weight 352 | # name: cnn.VGG_16.convolution1_2.bias 353 | # name: cnn.VGG_16.convolution2_1.weight 354 | # name: cnn.VGG_16.convolution2_1.bias 355 | # name: cnn.VGG_16.convolution2_2.weight 356 | # name: cnn.VGG_16.convolution2_2.bias 357 | ``` 358 | 359 | 对 convolution1 和 convolution2 设置不同的学习率,首先将它们分开,即放到不同的列表里: 360 | 361 | ```python3 362 | conv1_params = [] 363 | conv2_params = [] 364 | 365 | for name, parms in net.named_parameters(): 366 | if "convolution1" in name: 367 | conv1_params += [parms] 368 | else: 369 | conv2_params += [parms] 370 | 371 | # 然后在优化器中进行如下操作: 372 | optimizer = optim.Adam( 373 | [ 374 | {"params": conv1_params, 'lr': 0.01}, 375 | {"params": conv2_params, 'lr': 0.001}, 376 | ], 377 | weight_decay=1e-3, 378 | ) 379 | ``` 380 | 381 | 382 | 383 | 我们将模型划分为两部分,存放到一个列表里,每部分就对应上面的一个字典,在字典里设置不同的学习率。当这两部分有相同的其他参数时,就将该参数放到列表外面作为全局参数,如上面的`weight_decay`。 384 | 385 | 也可以在列表外设置一个全局学习率,当各部分字典里设置了局部学习率时,就使用该学习率,否则就使用列表外的全局学习率。 386 | 387 | 388 | 389 | 390 | 391 | ### 10、模型相关操作 392 | 393 | 这个内容比较多,我就写成了一篇文章。 394 | 395 | [PyTorch 中模型的使用](https://zhuanlan.zhihu.com/p/73893187) 396 | 397 | 398 | 399 | 400 | 401 | ### 11、Pytorch内置one_hot函数 402 | 感谢 **yangyangyang** 补充:Pytorch 1.1后,one_hot可以直接用 `torch.nn.functional.one_hot`。 403 | 然后我将Pytorch升级到1.2版本,试用了下 `one_hot` 函数,确实很方便。 404 | 具体用法如下: 405 | ```python3 406 | import torch.nn.functional as F 407 | import torch 408 | 409 | tensor = torch.arange(0, 5) % 3 # tensor([0, 1, 2, 0, 1]) 410 | one_hot = F.one_hot(tensor) 411 | 412 | # 输出: 413 | # tensor([[1, 0, 0], 414 | # [0, 1, 0], 415 | # [0, 0, 1], 416 | # [1, 0, 0], 417 | # [0, 1, 0]]) 418 | ``` 419 | 420 | `F.one_hot` 会自己检测不同类别个数,生成对应独热编码。我们也可以自己指定类别数: 421 | 422 | ```python3 423 | tensor = torch.arange(0, 5) % 3 # tensor([0, 1, 2, 0, 1]) 424 | one_hot = F.one_hot(tensor, num_classes=5) 425 | 426 | # 输出: 427 | # tensor([[1, 0, 0, 0, 0], 428 | # [0, 1, 0, 0, 0], 429 | # [0, 0, 1, 0, 0], 430 | # [1, 0, 0, 0, 0], 431 | # [0, 1, 0, 0, 0]]) 432 | ``` 433 | 434 | 升级 Pytorch (cpu版本)的命令:`conda install pytorch torchvision -c pytorch` 435 | (希望Pytorch升级不会影响项目代码) 436 | 437 | 438 | 439 | ### 12、网络参数初始化 440 | 神经网络的初始化是训练流程的重要基础环节,会对模型的性能、收敛性、收敛速度等产生重要的影响。 441 | 以下介绍两种常用的初始化操作。 442 | 443 | (1) 使用pytorch内置的torch.nn.init方法。 444 | 445 | 常用的初始化操作,例如正态分布、均匀分布、xavier初始化、kaiming初始化等都已经实现,可以直接使用。 446 | 具体详见[PyTorch 中 torch.nn.init 中文文档](https://ptorch.com/docs/1/nn-init)。 447 | 448 | ```python3 449 | init.xavier_uniform(net1[0].weight) 450 | ``` 451 | 452 | (2) 对于一些更加灵活的初始化方法,可以借助numpy。 453 | 454 | 对于自定义的初始化方法,有时tensor的功能不如numpy强大灵活,故可以借助numpy实现初始化方法,再转换 455 | 到tensor上使用。 456 | 457 | ```python3 458 | for layer in net1.modules(): 459 | if isinstance(layer, nn.Linear): # 判断是否是线性层 460 | param_shape = layer.weight.shape 461 | layer.weight.data = torch.from_numpy(np.random.normal(0, 0.5, size=param_shape)) 462 | # 定义为均值为 0,方差为 0.5 的正态分布 463 | ``` 464 | 465 | --------------------------------------------------------------------------------