PyTorch
PyTorch最好的资料是官方文档。

1、基本配置

导入包和版本查询

  1. import torch
  2. import torch.nn as nn
  3. import torchvision
  4. print(torch.__version__)
  5. print(torch.version.cuda)
  6. print(torch.backends.cudnn.version())
  7. print(torch.cuda.get_device_name(0))

可复现性

在硬件设备(CPU、GPU)不同时,完全的可复现性无法保证,即使随机种子相同。但是,在同一个设备上,应该保证可复现性。具体做法是,在程序开始的时候固定torch的随机种子,同时也把numpy的随机种子固定

  1. np.random.seed(0)
  2. torch.manual_seed(0)
  3. torch.cuda.manual_seed_all(0)
  4. torch.backends.cudnn.deterministic = True
  5. torch.backends.cudnn.benchmark = False

显卡设置

如果只需要一张显卡

  1. # Device configuration
  2. device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

如果需要指定多张显卡,比如0,1号显卡。

  1. import os
  2. os.environ['CUDA_VISIBLE_DEVICES'] = '0,1'

也可以在命令行运行代码时设置显卡:

  1. CUDA_VISIBLE_DEVICES=0,1 python train.py

清除显存

  1. torch.cuda.empty_cache()

也可以使用在命令行重置GPU的指令

  1. nvidia-smi --gpu-reset -i [gpu_id]

2、张量(Tensor)处理

张量的数据类型

PyTorch有9种CPU张量类型和9种GPU张量类型。
PyTorch常用代码段合集 - 图1

张量基本信息

  1. tensor = torch.randn(3,4,5)
  2. print(tensor.type()) # 数据类型
  3. print(tensor.size()) # 张量的shape,是个元组
  4. print(tensor.dim()) # 维度的数量

命名张量

张量命名是一个非常有用的方法,这样可以方便地使用维度的名字来做索引或其他操作,大大提高了可读性、易用性,防止出错。

  1. # 在PyTorch 1.3之前,需要使用注释
  2. # Tensor[N, C, H, W]
  3. images = torch.randn(32, 3, 56, 56)
  4. images.sum(dim=1)
  5. images.select(dim=1, index=0)
  6. # PyTorch 1.3之后
  7. NCHW = [‘N’, C’, H’, W’]
  8. images = torch.randn(32, 3, 56, 56, names=NCHW)
  9. images.sum('C')
  10. images.select('C', index=0)
  11. # 也可以这么设置
  12. tensor = torch.rand(3,4,1,2,names=('C', 'N', 'H', 'W'))
  13. # 使用align_to可以对维度方便地排序
  14. tensor = tensor.align_to('N', 'C', 'H', 'W')

数据类型转换

  1. # 设置默认类型,pytorch中的FloatTensor远远快于DoubleTensor
  2. torch.set_default_tensor_type(torch.FloatTensor)
  3. # 类型转换
  4. tensor = tensor.cuda()
  5. tensor = tensor.cpu()
  6. tensor = tensor.float()
  7. tensor = tensor.long()

**torch.Tensor****np.ndarray**转换

除了CharTensor,其他所有CPU上的张量都支持转换为numpy格式然后再转换回来。

  1. ndarray = tensor.cpu().numpy()
  2. tensor = torch.from_numpy(ndarray).float()
  3. tensor = torch.from_numpy(ndarray.copy()).float() # If ndarray has negative stride.

**Torch.tensor****PIL.Image**转换

  1. # pytorch中的张量默认采用[N, C, H, W]的顺序,并且数据范围在[0,1],需要进行转置和规范化
  2. # torch.Tensor -> PIL.Image
  3. image = PIL.Image.fromarray(torch.clamp(tensor*255, min=0, max=255).byte().permute(1,2,0).cpu().numpy())
  4. image = torchvision.transforms.functional.to_pil_image(tensor) # Equivalently way
  5. # PIL.Image -> torch.Tensor
  6. path = r'./figure.jpg'
  7. tensor = torch.from_numpy(np.asarray(PIL.Image.open(path))).permute(2,0,1).float() / 255
  8. tensor = torchvision.transforms.functional.to_tensor(PIL.Image.open(path)) # Equivalently way

**np.ndarray****PIL.Image**的转换

  1. image = PIL.Image.fromarray(ndarray.astype(np.uint8))
  2. ndarray = np.asarray(PIL.Image.open(path))

从只包含一个元素的张量中提取值

  1. value = torch.rand(1).item()

张量形变

  1. # 在将卷积层输入全连接层的情况下通常需要对张量做形变处理,
  2. # 相比torch.view,torch.reshape可以自动处理输入张量不连续的情况。
  3. tensor = torch.rand(2,3,4)
  4. shape = (6, 4)
  5. tensor = torch.reshape(tensor, shape)

打乱顺序

  1. tensor = tensor[torch.randperm(tensor.size(0))] # 打乱第一个维度

水平翻转

  1. # pytorch不支持tensor[::-1]这样的负步长操作,水平翻转可以通过张量索引实现
  2. # 假设张量的维度为[N, D, H, W].
  3. tensor = tensor[:,:,:,torch.arange(tensor.size(3) - 1, -1, -1).long()]

复制张量

  1. # Operation | New/Shared memory | Still in computation graph |
  2. tensor.clone() # | New | Yes |
  3. tensor.detach() # | Shared | No |
  4. tensor.detach.clone() # | New | No |

张量拼接

  1. '''
  2. 注意torch.cat和torch.stack的区别在于torch.cat沿着给定的维度拼接,
  3. 而torch.stack会新增一维。例如当参数是3个10x5的张量,torch.cat的结果是30x5的张量,
  4. 而torch.stack的结果是3x10x5的张量。
  5. '''
  6. tensor = torch.cat(list_of_tensors, dim=0)
  7. tensor = torch.stack(list_of_tensors, dim=0)

将整数标签转为one-hot编码

  1. # pytorch的标记默认从0开始
  2. tensor = torch.tensor([0, 2, 1, 3])
  3. N = tensor.size(0)
  4. num_classes = 4
  5. one_hot = torch.zeros(N, num_classes).long()
  6. one_hot.scatter_(dim=1, index=torch.unsqueeze(tensor, dim=1), src=torch.ones(N, num_classes).long())

得到非零元素

  1. torch.nonzero(tensor) # index of non-zero elements
  2. torch.nonzero(tensor==0) # index of zero elements
  3. torch.nonzero(tensor).size(0) # number of non-zero elements
  4. torch.nonzero(tensor == 0).size(0) # number of zero elements

判断两个张量相等

  1. torch.allclose(tensor1, tensor2) # float tensor
  2. torch.equal(tensor1, tensor2) # int tensor

张量扩展

  1. # Expand tensor of shape 64*512 to shape 64*512*7*7.
  2. tensor = torch.rand(64,512)
  3. torch.reshape(tensor, (64, 512, 1, 1)).expand(64, 512, 7, 7)

矩阵乘法

  1. # Matrix multiplcation: (m*n) * (n*p) * -> (m*p).
  2. result = torch.mm(tensor1, tensor2)
  3. # Batch matrix multiplication: (b*m*n) * (b*n*p) -> (b*m*p)
  4. result = torch.bmm(tensor1, tensor2)
  5. # Element-wise multiplication.
  6. result = tensor1 * tensor2

计算两组数据之间的两两欧式距离

利用broadcast机制

  1. dist = torch.sqrt(torch.sum((X1[:,None,:] - X2) ** 2, dim=2))

3、模型定义和操作

一个简单两层卷积网络的示例

  1. # convolutional neural network (2 convolutional layers)
  2. class ConvNet(nn.Module):
  3. def __init__(self, num_classes=10):
  4. super(ConvNet, self).__init__()
  5. self.layer1 = nn.Sequential(
  6. nn.Conv2d(1, 16, kernel_size=5, stride=1, padding=2),
  7. nn.BatchNorm2d(16),
  8. nn.ReLU(),
  9. nn.MaxPool2d(kernel_size=2, stride=2))
  10. self.layer2 = nn.Sequential(
  11. nn.Conv2d(16, 32, kernel_size=5, stride=1, padding=2),
  12. nn.BatchNorm2d(32),
  13. nn.ReLU(),
  14. nn.MaxPool2d(kernel_size=2, stride=2))
  15. self.fc = nn.Linear(7*7*32, num_classes)
  16. def forward(self, x):
  17. out = self.layer1(x)
  18. out = self.layer2(out)
  19. out = out.reshape(out.size(0), -1)
  20. out = self.fc(out)
  21. return out
  22. model = ConvNet(num_classes).to(device)

卷积层的计算和展示可以用这个网站辅助。
https://ezyang.github.io/convolution-visualizer/index.html

双线性汇合(bilinear pooling)

  1. X = torch.reshape(N, D, H * W) # Assume X has shape N*D*H*W
  2. X = torch.bmm(X, torch.transpose(X, 1, 2)) / (H * W) # Bilinear pooling
  3. assert X.size() == (N, D, D)
  4. X = torch.reshape(X, (N, D * D))
  5. X = torch.sign(X) * torch.sqrt(torch.abs(X) + 1e-5) # Signed-sqrt normalization
  6. X = torch.nn.functional.normalize(X) # L2 normalization

多卡同步 BN(Batch normalization)

当使用 torch.nn.DataParallel 将代码运行在多张 GPU 卡上时,PyTorch 的 BN 层默认操作是各卡上数据独立地计算均值和标准差,同步 BN 使用所有卡上的数据一起计算 BN 层的均值和标准差,缓解了当批量大小(batch size)比较小时对均值和标准差估计不准的情况,是在目标检测等任务中一个有效的提升性能的技巧。

  1. sync_bn = torch.nn.SyncBatchNorm(num_features, eps=1e-05, momentum=0.1, affine=True,
  2. track_running_stats=True)

将已有网络的所有BN层改为同步BN层

  1. def convertBNtoSyncBN(module, process_group=None):
  2. '''Recursively replace all BN layers to SyncBN layer.
  3. Args:
  4. module[torch.nn.Module]. Network
  5. '''
  6. if isinstance(module, torch.nn.modules.batchnorm._BatchNorm):
  7. sync_bn = torch.nn.SyncBatchNorm(module.num_features, module.eps, module.momentum,
  8. module.affine, module.track_running_stats, process_group)
  9. sync_bn.running_mean = module.running_mean
  10. sync_bn.running_var = module.running_var
  11. if module.affine:
  12. sync_bn.weight = module.weight.clone().detach()
  13. sync_bn.bias = module.bias.clone().detach()
  14. return sync_bn
  15. else:
  16. for name, child_module in module.named_children():
  17. setattr(module, name) = convert_syncbn_model(child_module, process_group=process_group))
  18. return module

类似 BN 滑动平均

如果要实现类似 BN 滑动平均的操作,在 forward 函数中要使用原地(inplace)操作给滑动平均赋值。

  1. class BN(torch.nn.Module)
  2. def __init__(self):
  3. ...
  4. self.register_buffer('running_mean', torch.zeros(num_features))
  5. def forward(self, X):
  6. ...
  7. self.running_mean += momentum * (current - self.running_mean)

计算模型整体参数量

  1. num_parameters = sum(torch.numel(parameter) for parameter in model.parameters())

查看网络中的参数

可以通过model.state_dict()或者model.named_parameters()函数查看现在的全部可训练参数(包括通过继承得到的父类中的参数)

  1. params = list(model.named_parameters())
  2. (name, param) = params[28]
  3. print(name)
  4. print(param.grad)
  5. print('-------------------------------------------------')
  6. (name2, param2) = params[29]
  7. print(name2)
  8. print(param2.grad)
  9. print('----------------------------------------------------')
  10. (name1, param1) = params[30]
  11. print(name1)
  12. print(param1.grad)

模型可视化(使用pytorchviz)

https://github.com/szagoruyko/pytorchviz

类似 Keras 的 **model.summary()** 输出模型信息(使用pytorch-summary )

https://github.com/sksq96/pytorch-summary
模型权重初始化
注意 model.modules()model.children() 的区别:model.modules() 会迭代地遍历模型的所有子层,而 model.children() 只会遍历模型下的一层。

  1. # Common practise for initialization.
  2. for layer in model.modules():
  3. if isinstance(layer, torch.nn.Conv2d):
  4. torch.nn.init.kaiming_normal_(layer.weight, mode='fan_out',
  5. nonlinearity='relu')
  6. if layer.bias is not None:
  7. torch.nn.init.constant_(layer.bias, val=0.0)
  8. elif isinstance(layer, torch.nn.BatchNorm2d):
  9. torch.nn.init.constant_(layer.weight, val=1.0)
  10. torch.nn.init.constant_(layer.bias, val=0.0)
  11. elif isinstance(layer, torch.nn.Linear):
  12. torch.nn.init.xavier_normal_(layer.weight)
  13. if layer.bias is not None:
  14. torch.nn.init.constant_(layer.bias, val=0.0)
  15. # Initialization with given tensor.
  16. layer.weight = torch.nn.Parameter(tensor)

提取模型中的某一层

modules()会返回模型中所有模块的迭代器,它能够访问到最内层,比如self.layer1.conv1这个模块,还有一个与它们相对应的是name_children()属性以及named_modules(),这两个不仅会返回模块的迭代器,还会返回网络层的名字。

  1. # 取模型中的前两层
  2. new_model = nn.Sequential(*list(model.children())[:2]
  3. # 如果希望提取出模型中的所有卷积层,可以像下面这样操作:
  4. for layer in model.named_modules():
  5. if isinstance(layer[1],nn.Conv2d):
  6. conv_model.add_module(layer[0],layer[1])

部分层使用预训练模型

注意如果保存的模型是 torch.nn.DataParallel,则当前的模型也需要是

  1. model.load_state_dict(torch.load('model.pth'), strict=False)

将在 GPU 保存的模型加载到 CPU

  1. model.load_state_dict(torch.load('model.pth', map_location='cpu'))

导入另一个模型的相同部分到新的模型

模型导入参数时,如果两个模型结构不一致,则直接导入参数会报错。用下面方法可以把另一个模型的相同的部分导入到新的模型中。

  1. # model_new代表新的模型
  2. # model_saved代表其他模型,比如用torch.load导入的已保存的模型
  3. model_new_dict = model_new.state_dict()
  4. model_common_dict = {k:v for k, v in model_saved.items() if k in model_new_dict.keys()}
  5. model_new_dict.update(model_common_dict)
  6. model_new.load_state_dict(model_new_dict)

4、数据处理

计算数据集的均值和标准差

  1. import os
  2. import cv2
  3. import numpy as np
  4. from torch.utils.data import Dataset
  5. from PIL import Image
  6. def compute_mean_and_std(dataset):
  7. # 输入PyTorch的dataset,输出均值和标准差
  8. mean_r = 0
  9. mean_g = 0
  10. mean_b = 0
  11. for img, _ in dataset:
  12. img = np.asarray(img) # change PIL Image to numpy array
  13. mean_r += np.mean(img[:, :, 0])
  14. mean_g += np.mean(img[:, :, 1])
  15. mean_b += np.mean(img[:, :, 2])
  16. mean_r /= len(dataset)
  17. mean_g /= len(dataset)
  18. mean_b /= len(dataset)
  19. diff_r = 0
  20. diff_g = 0
  21. diff_b = 0
  22. N = 0
  23. for img, _ in dataset:
  24. img = np.asarray(img)
  25. diff_r += np.sum(np.power(img[:, :, 0] - mean_r, 2))
  26. diff_g += np.sum(np.power(img[:, :, 1] - mean_g, 2))
  27. diff_b += np.sum(np.power(img[:, :, 2] - mean_b, 2))
  28. N += np.prod(img[:, :, 0].shape)
  29. std_r = np.sqrt(diff_r / N)
  30. std_g = np.sqrt(diff_g / N)
  31. std_b = np.sqrt(diff_b / N)
  32. mean = (mean_r.item() / 255.0, mean_g.item() / 255.0, mean_b.item() / 255.0)
  33. std = (std_r.item() / 255.0, std_g.item() / 255.0, std_b.item() / 255.0)
  34. return mean, std

得到视频数据基本信息

  1. import cv2
  2. video = cv2.VideoCapture(mp4_path)
  3. height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
  4. width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
  5. num_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
  6. fps = int(video.get(cv2.CAP_PROP_FPS))
  7. video.release()

TSN 每段(segment)采样一帧视频

  1. K = self._num_segments
  2. if is_train:
  3. if num_frames > K:
  4. # Random index for each segment.
  5. frame_indices = torch.randint(
  6. high=num_frames // K, size=(K,), dtype=torch.long)
  7. frame_indices += num_frames // K * torch.arange(K)
  8. else:
  9. frame_indices = torch.randint(
  10. high=num_frames, size=(K - num_frames,), dtype=torch.long)
  11. frame_indices = torch.sort(torch.cat((
  12. torch.arange(num_frames), frame_indices)))[0]
  13. else:
  14. if num_frames > K:
  15. # Middle index for each segment.
  16. frame_indices = num_frames / K // 2
  17. frame_indices += num_frames // K * torch.arange(K)
  18. else:
  19. frame_indices = torch.sort(torch.cat((
  20. torch.arange(num_frames), torch.arange(K - num_frames))))[0]
  21. assert frame_indices.size() == (K,)
  22. return [frame_indices[i] for i in range(K)]

常用训练和验证数据预处理

其中 ToTensor 操作会将 PIL.Image 或形状为 H×W×D,数值范围为 [0, 255] 的 np.ndarray 转换为形状为 D×H×W,数值范围为 [0.0, 1.0] 的 torch.Tensor

  1. train_transform = torchvision.transforms.Compose([
  2. torchvision.transforms.RandomResizedCrop(size=224,
  3. scale=(0.08, 1.0)),
  4. torchvision.transforms.RandomHorizontalFlip(),
  5. torchvision.transforms.ToTensor(),
  6. torchvision.transforms.Normalize(mean=(0.485, 0.456, 0.406),
  7. std=(0.229, 0.224, 0.225)),
  8. ])
  9. val_transform = torchvision.transforms.Compose([
  10. torchvision.transforms.Resize(256),
  11. torchvision.transforms.CenterCrop(224),
  12. torchvision.transforms.ToTensor(),
  13. torchvision.transforms.Normalize(mean=(0.485, 0.456, 0.406),
  14. std=(0.229, 0.224, 0.225)),
  15. ])

5、模型训练和测试

分类模型训练代码

  1. # Loss and optimizer
  2. criterion = nn.CrossEntropyLoss()
  3. optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
  4. # Train the model
  5. total_step = len(train_loader)
  6. for epoch in range(num_epochs):
  7. for i ,(images, labels) in enumerate(train_loader):
  8. images = images.to(device)
  9. labels = labels.to(device)
  10. # Forward pass
  11. outputs = model(images)
  12. loss = criterion(outputs, labels)
  13. # Backward and optimizer
  14. optimizer.zero_grad()
  15. loss.backward()
  16. optimizer.step()
  17. if (i+1) % 100 == 0:
  18. print('Epoch: [{}/{}], Step: [{}/{}], Loss: {}'
  19. .format(epoch+1, num_epochs, i+1, total_step, loss.item()))

分类模型测试代码

  1. # Test the model
  2. model.eval() # eval mode(batch norm uses moving mean/variance
  3. #instead of mini-batch mean/variance)
  4. with torch.no_grad():
  5. correct = 0
  6. total = 0
  7. for images, labels in test_loader:
  8. images = images.to(device)
  9. labels = labels.to(device)
  10. outputs = model(images)
  11. _, predicted = torch.max(outputs.data, 1)
  12. total += labels.size(0)
  13. correct += (predicted == labels).sum().item()
  14. print('Test accuracy of the model on the 10000 test images: {} %'
  15. .format(100 * correct / total))

自定义loss

继承torch.nn.Module类写自己的loss。

  1. class MyLoss(torch.nn.Moudle):
  2. def __init__(self):
  3. super(MyLoss, self).__init__()
  4. def forward(self, x, y):
  5. loss = torch.mean((x - y) ** 2)
  6. return loss

标签平滑(label smoothing)

写一个label_smoothing.py的文件,然后在训练代码里引用,用LSR代替交叉熵损失即可。label_smoothing.py内容如下:

  1. import torch
  2. import torch.nn as nn
  3. class LSR(nn.Module):
  4. def __init__(self, e=0.1, reduction='mean'):
  5. super().__init__()
  6. self.log_softmax = nn.LogSoftmax(dim=1)
  7. self.e = e
  8. self.reduction = reduction
  9. def _one_hot(self, labels, classes, value=1):
  10. """
  11. Convert labels to one hot vectors
  12. Args:
  13. labels: torch tensor in format [label1, label2, label3, ...]
  14. classes: int, number of classes
  15. value: label value in one hot vector, default to 1
  16. Returns:
  17. return one hot format labels in shape [batchsize, classes]
  18. """
  19. one_hot = torch.zeros(labels.size(0), classes)
  20. #labels and value_added size must match
  21. labels = labels.view(labels.size(0), -1)
  22. value_added = torch.Tensor(labels.size(0), 1).fill_(value)
  23. value_added = value_added.to(labels.device)
  24. one_hot = one_hot.to(labels.device)
  25. one_hot.scatter_add_(1, labels, value_added)
  26. return one_hot
  27. def _smooth_label(self, target, length, smooth_factor):
  28. """convert targets to one-hot format, and smooth
  29. them.
  30. Args:
  31. target: target in form with [label1, label2, label_batchsize]
  32. length: length of one-hot format(number of classes)
  33. smooth_factor: smooth factor for label smooth
  34. Returns:
  35. smoothed labels in one hot format
  36. """
  37. one_hot = self._one_hot(target, length, value=1 - smooth_factor)
  38. one_hot += smooth_factor / (length - 1)
  39. return one_hot.to(target.device)
  40. def forward(self, x, target):
  41. if x.size(0) != target.size(0):
  42. raise ValueError('Expected input batchsize ({}) to match target batch_size({})'
  43. .format(x.size(0), target.size(0)))
  44. if x.dim() < 2:
  45. raise ValueError('Expected input tensor to have least 2 dimensions(got {})'
  46. .format(x.size(0)))
  47. if x.dim() != 2:
  48. raise ValueError('Only 2 dimension tensor are implemented, (got {})'
  49. .format(x.size()))
  50. smoothed_target = self._smooth_label(target, x.size(1), self.e)
  51. x = self.log_softmax(x)
  52. loss = torch.sum(- x * smoothed_target, dim=1)
  53. if self.reduction == 'none':
  54. return loss
  55. elif self.reduction == 'sum':
  56. return torch.sum(loss)
  57. elif self.reduction == 'mean':
  58. return torch.mean(loss)
  59. else:
  60. raise ValueError('unrecognized option, expect reduction to be one of none, mean, sum')

或者直接在训练文件里做label smoothing

  1. for images, labels in train_loader:
  2. images, labels = images.cuda(), labels.cuda()
  3. N = labels.size(0)
  4. # C is the number of classes.
  5. smoothed_labels = torch.full(size=(N, C), fill_value=0.1 / (C - 1)).cuda()
  6. smoothed_labels.scatter_(dim=1, index=torch.unsqueeze(labels, dim=1), value=0.9)
  7. score = model(images)
  8. log_prob = torch.nn.functional.log_softmax(score, dim=1)
  9. loss = -torch.sum(log_prob * smoothed_labels) / N
  10. optimizer.zero_grad()
  11. loss.backward()
  12. optimizer.step()

Mixup训练

  1. beta_distribution = torch.distributions.beta.Beta(alpha, alpha)
  2. for images, labels in train_loader:
  3. images, labels = images.cuda(), labels.cuda()
  4. # Mixup images and labels.
  5. lambda_ = beta_distribution.sample([]).item()
  6. index = torch.randperm(images.size(0)).cuda()
  7. mixed_images = lambda_ * images + (1 - lambda_) * images[index, :]
  8. label_a, label_b = labels, labels[index]
  9. # Mixup loss.
  10. scores = model(mixed_images)
  11. loss = (lambda_ * loss_function(scores, label_a)
  12. + (1 - lambda_) * loss_function(scores, label_b))
  13. optimizer.zero_grad()
  14. loss.backward()
  15. optimizer.step()

L1 正则化

  1. l1_regularization = torch.nn.L1Loss(reduction='sum')
  2. loss = ... # Standard cross-entropy loss
  3. for param in model.parameters():
  4. loss += torch.sum(torch.abs(param))
  5. loss.backward()

不对偏置项进行权重衰减(weight decay)

pytorch里的weight decay相当于l2正则

  1. bias_list = (param for name, param in model.named_parameters() if name[-4:] == 'bias')
  2. others_list = (param for name, param in model.named_parameters() if name[-4:] != 'bias')
  3. parameters = [{'parameters': bias_list, 'weight_decay': 0},
  4. {'parameters': others_list}]
  5. optimizer = torch.optim.SGD(parameters, lr=1e-2, momentum=0.9, weight_decay=1e-4)

梯度裁剪(gradient clipping)

  1. torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=20)

得到当前学习率

  1. # If there is one global learning rate (which is the common case).
  2. lr = next(iter(optimizer.param_groups))['lr']
  3. # If there are multiple learning rates for different layers.
  4. all_lr = []
  5. for param_group in optimizer.param_groups:
  6. all_lr.append(param_group['lr'])

另一种方法,在一个batch训练代码里,当前的lr是optimizer.param_groups[0][‘lr’]

学习率衰减

  1. # Reduce learning rate when validation accuarcy plateau.
  2. scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='max', patience=5, verbose=True)
  3. for t in range(0, 80):
  4. train(...)
  5. val(...)
  6. scheduler.step(val_acc)
  7. # Cosine annealing learning rate.
  8. scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=80)
  9. # Reduce learning rate by 10 at given epochs.
  10. scheduler = torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones=[50, 70], gamma=0.1)
  11. for t in range(0, 80):
  12. scheduler.step()
  13. train(...)
  14. val(...)
  15. # Learning rate warmup by 10 epochs.
  16. scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=lambda t: t / 10)
  17. for t in range(0, 10):
  18. scheduler.step()
  19. train(...)
  20. val(...)

优化器链式更新

从1.4版本开始,torch.optim.lr_scheduler 支持链式更新(chaining),即用户可以定义两个 schedulers,并交替在训练中使用。

  1. import torch
  2. from torch.optim import SGD
  3. from torch.optim.lr_scheduler import ExponentialLR, StepLR
  4. model = [torch.nn.Parameter(torch.randn(2, 2, requires_grad=True))]
  5. optimizer = SGD(model, 0.1)
  6. scheduler1 = ExponentialLR(optimizer, gamma=0.9)
  7. scheduler2 = StepLR(optimizer, step_size=3, gamma=0.1)
  8. for epoch in range(4):
  9. print(epoch, scheduler2.get_last_lr()[0])
  10. optimizer.step()
  11. scheduler1.step()
  12. scheduler2.step()

模型训练可视化

PyTorch可以使用tensorboard来可视化训练过程。
安装和运行TensorBoard。

  1. pip install tensorboard
  2. tensorboard --logdir=runs

使用SummaryWriter类来收集和可视化相应的数据,放了方便查看,可以使用不同的文件夹,比如’Loss/train’和’Loss/test’。

  1. from torch.utils.tensorboard import SummaryWriter
  2. import numpy as np
  3. writer = SummaryWriter()
  4. for n_iter in range(100):
  5. writer.add_scalar('Loss/train', np.random.random(), n_iter)
  6. writer.add_scalar('Loss/test', np.random.random(), n_iter)
  7. writer.add_scalar('Accuracy/train', np.random.random(), n_iter)
  8. writer.add_scalar('Accuracy/test', np.random.random(), n_iter)

保存与加载断点

注意为了能够恢复训练,需要同时保存模型和优化器的状态,以及当前的训练轮数。

  1. start_epoch = 0
  2. # Load checkpoint.
  3. if resume: # resume为参数,第一次训练时设为0,中断再训练时设为1
  4. model_path = os.path.join('model', 'best_checkpoint.pth.tar')
  5. assert os.path.isfile(model_path)
  6. checkpoint = torch.load(model_path)
  7. best_acc = checkpoint['best_acc']
  8. start_epoch = checkpoint['epoch']
  9. model.load_state_dict(checkpoint['model'])
  10. optimizer.load_state_dict(checkpoint['optimizer'])
  11. print('Load checkpoint at epoch {}.'.format(start_epoch))
  12. print('Best accuracy so far {}.'.format(best_acc))
  13. # Train the model
  14. for epoch in range(start_epoch, num_epochs):
  15. ...
  16. # Test the model
  17. ...
  18. # save checkpoint
  19. is_best = current_acc > best_acc
  20. best_acc = max(current_acc, best_acc)
  21. checkpoint = {
  22. 'best_acc': best_acc,
  23. 'epoch': epoch + 1,
  24. 'model': model.state_dict(),
  25. 'optimizer': optimizer.state_dict(),
  26. }
  27. model_path = os.path.join('model', 'checkpoint.pth.tar')
  28. best_model_path = os.path.join('model', 'best_checkpoint.pth.tar')
  29. torch.save(checkpoint, model_path)
  30. if is_best:
  31. shutil.copy(model_path, best_model_path)

提取 ImageNet 预训练模型某层的卷积特征

  1. # VGG-16 relu5-3 feature.
  2. model = torchvision.models.vgg16(pretrained=True).features[:-1]
  3. # VGG-16 pool5 feature.
  4. model = torchvision.models.vgg16(pretrained=True).features
  5. # VGG-16 fc7 feature.
  6. model = torchvision.models.vgg16(pretrained=True)
  7. model.classifier = torch.nn.Sequential(*list(model.classifier.children())[:-3])
  8. # ResNet GAP feature.
  9. model = torchvision.models.resnet18(pretrained=True)
  10. model = torch.nn.Sequential(collections.OrderedDict(
  11. list(model.named_children())[:-1]))
  12. with torch.no_grad():
  13. model.eval()
  14. conv_representation = model(image)

提取 ImageNet 预训练模型多层的卷积特征

  1. class FeatureExtractor(torch.nn.Module):
  2. """Helper class to extract several convolution features from the given
  3. pre-trained model.
  4. Attributes:
  5. _model, torch.nn.Module.
  6. _layers_to_extract, list<str> or set<str>
  7. Example:
  8. >>> model = torchvision.models.resnet152(pretrained=True)
  9. >>> model = torch.nn.Sequential(collections.OrderedDict(
  10. list(model.named_children())[:-1]))
  11. >>> conv_representation = FeatureExtractor(
  12. pretrained_model=model,
  13. layers_to_extract={'layer1', 'layer2', 'layer3', 'layer4'})(image)
  14. """
  15. def __init__(self, pretrained_model, layers_to_extract):
  16. torch.nn.Module.__init__(self)
  17. self._model = pretrained_model
  18. self._model.eval()
  19. self._layers_to_extract = set(layers_to_extract)
  20. def forward(self, x):
  21. with torch.no_grad():
  22. conv_representation = []
  23. for name, layer in self._model.named_children():
  24. x = layer(x)
  25. if name in self._layers_to_extract:
  26. conv_representation.append(x)
  27. return conv_representation

微调全连接层

  1. model = torchvision.models.resnet18(pretrained=True)
  2. for param in model.parameters():
  3. param.requires_grad = False
  4. model.fc = nn.Linear(512, 100) # Replace the last fc layer
  5. optimizer = torch.optim.SGD(model.fc.parameters(), lr=1e-2, momentum=0.9, weight_decay=1e-4)

以较大学习率微调全连接层,较小学习率微调卷积层

  1. model = torchvision.models.resnet18(pretrained=True)
  2. finetuned_parameters = list(map(id, model.fc.parameters()))
  3. conv_parameters = (p for p in model.parameters() if id(p) not in finetuned_parameters)
  4. parameters = [{'params': conv_parameters, 'lr': 1e-3},
  5. {'params': model.fc.parameters()}]
  6. optimizer = torch.optim.SGD(parameters, lr=1e-2, momentum=0.9, weight_decay=1e-4)

6、其他注意事项

  • 不要使用太大的线性层。因为nn.Linear(m,n)使用的是 的内存,线性层太大很容易超出现有显存。
  • 不要在太长的序列上使用RNN。因为RNN反向传播使用的是BPTT算法,其需要的内存和输入序列的长度呈线性关系。
  • model(x) 前用 **model.train()****model.eval()** 切换网络状态。
  • 不需要计算梯度的代码块用 **with torch.no_grad()** 包含起来。
  • model.eval()torch.no_grad() 的区别在于,model.eval() 是将网络切换为测试状态,例如 BN 和dropout在训练和测试阶段使用不同的计算方法。torch.no_grad() 是关闭 PyTorch 张量的自动求导机制,以减少存储使用和加速计算,得到的结果无法进行 loss.backward()
  • **model.zero_grad()**会把整个模型的参数的梯度都归零,而optimizer.zero_grad()只会把传入其中的参数的梯度归零.
  • torch.nn.CrossEntropyLoss 的输入不需要经过 Softmax。torch.nn.CrossEntropyLoss 等价于 torch.nn.functional.log_softmax + torch.nn.NLLLoss
  • loss.backward() 前用 optimizer.zero_grad() 清除累积梯度。
  • torch.utils.data.DataLoader尽量设置 **pin_memory=True**,对特别小的数据集如 MNIST 设置 pin_memory=False 反而更快一些。num_workers 的设置需要在实验中找到最快的取值。
  • 用 del 及时删除不用的中间变量,节约 GPU 存储。
  • 使用 inplace 操作可节约 GPU 存储,如

    1. x = torch.nn.functional.relu(x, inplace=True)
  • 减少 CPU 和 GPU 之间的数据传输。例如如果想知道一个 epoch 中每个 mini-batch 的 loss 和准确率,先将它们累积在 GPU 中等一个 epoch 结束之后一起传输回 CPU 会比每个 mini-batch 都进行一次 GPU 到 CPU 的传输更快。

  • 使用半精度浮点数 half() 会有一定的速度提升,具体效率依赖于 GPU 型号。需要小心数值精度过低带来的稳定性问题。
  • 时常使用 assert tensor.size() == (N, D, H, W) 作为调试手段,确保张量维度和设想中一致。
  • 除了标记 y 外,尽量少使用一维张量,使用 n*1 的二维张量代替,可以避免一些意想不到的一维张量计算结果。
  • 统计代码各部分耗时 ```python with torch.autograd.profiler.profile(enabled=True, use_cuda=False) as profile: … print(profile)

或者在命令行运行

python -m torch.utils.bottleneck main.py

  1. - 使用TorchSnooper来调试PyTorch代码,程序在执行的时候,就会自动 print 出来每一行的执行结果的 tensor 的形状、数据类型、设备、是否需要梯度的信息。
  2. ```python
  3. # pip install torchsnooper
  4. import torchsnooper
  5. # 对于函数,使用修饰器
  6. @torchsnooper.snoop()
  7. # 如果不是函数,使用 with 语句来激活 TorchSnooper,把训练的那个循环装进 with 语句中去。
  8. with torchsnooper.snoop():
  • 原本的代码

https://github.com/zasdfgbnm/TorchSnooper

  • 模型可解释性,使用captum库

https://captum.ai/