pytorch - 生态和部署

1 生态

PyTorch生态在图像、视频、文本等领域中的发展。

1.1 torchvision

torchvision包含了在计算机视觉中常常用到的数据集,模型和图像处理的方式。

  1. torchvision.datasets *
    torchvision.datasets主要包含了一些我们在计算机视觉中常见的数据集
  2. torchvision.models *
    各种预训练好的模型
  3. torchvision.tramsforms *
    图像处理的方法
  4. torchvision.io
    在torchvision.io提供了视频、图片和文件的 IO 操作的功能,它们包括读取、写入、编解码处理操作。随着torchvision的发展,io也增加了更多底层的高效率的API。
  5. torchvision.ops
    torchvision.ops 为我们提供了许多计算机视觉的特定操作,包括但不仅限于NMS,RoIAlign(MASK R-CNN中应用的一种方法),RoIPool(Fast R-CNN中用到的一种方法)。在合适的时间使用可以大大降低我们的工作量,避免重复的造轮子。
  6. torchvision.utils
    torchvision.utils 为我们提供了一些可视化的方法,可以帮助我们将若干张图片拼接在一起、可视化检测和分割的效果。

1.2 PyTorchVideo

PyTorchVideo 是一个专注于视频理解工作的深度学习库。

1.3 torchtext

用于自然语言处理(NLP)的工具包torchtext。方便的对文本进行预处理,例如截断补长、构建词表等。

  1. 构建数据集
    • Field及其使用
      Field是torchtext中定义数据类型以及转换为张量的指令。torchtext 认为一个样本是由多个字段(文本字段,标签字段)组成,不同的字段可能会有不同的处理方式,所以才会有 Field 抽象。定义Field对象是为了明确如何处理不同类型的数据,但具体的处理则是在Dataset中完成的。
      1
      2
      3
      tokenize = lambda x: x.split()
      TEXT = data.Field(sequential=True, tokenize=tokenize, lower=True, fix_length=200)
      LABEL = data.Field(sequential=False, use_vocab=False)
      sequential设置数据是否是顺序表示的;
      ​tokenize用于设置将字符串标记为顺序实例的函数;
      ​lower设置是否将字符串全部转为小写;
      ​fix_length设置此字段所有实例都将填充到一个固定的长度,方便后续处理;
      ​use_vocab设置是否引入Vocab object,如果为False,则需要保证之后输入field中的data都是numerical的;
      构建Field完成后就可以进一步构建dataset了
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      from torchtext import data
      def get_dataset(csv_data, text_field, label_field, test=False):
      fields = [("id", None), # we won't be needing the id, so we pass in None as the field
      ("comment_text", text_field), ("toxic", label_field)]
      examples = []

      if test:
      # 如果为测试集,则不加载label
      for text in tqdm(csv_data['comment_text']):
      examples.append(data.Example.fromlist([None, text, None], fields))
      else:
      for text, label in tqdm(zip(csv_data['comment_text'], csv_data['toxic'])):
      examples.append(data.Example.fromlist([None, text, label], fields))
      return examples, fields
      这里使用数据csv_data中有”comment_text”和”toxic”两列,分别对应text和label
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      train_data = pd.read_csv('train_toxic_comments.csv')
      valid_data = pd.read_csv('valid_toxic_comments.csv')
      test_data = pd.read_csv("test_toxic_comments.csv")
      TEXT = data.Field(sequential=True, tokenize=tokenize, lower=True)
      LABEL = data.Field(sequential=False, use_vocab=False)

      # 得到构建Dataset所需的examples和fields
      train_examples, train_fields = get_dataset(train_data, TEXT, LABEL)
      valid_examples, valid_fields = get_dataset(valid_data, TEXT, LABEL)
      test_examples, test_fields = get_dataset(test_data, TEXT, None, test=True)
      # 构建Dataset数据集
      train = data.Dataset(train_examples, train_fields)
      valid = data.Dataset(valid_examples, valid_fields)
      test = data.Dataset(test_examples, test_fields)
      可以看到,定义Field对象完成后,通过get_dataset函数可以读入数据的文本和标签,将二者(examples)连同field一起送到torchtext.data.Dataset类中,即可完成数据集的构建。使用以下命令可以看下读入的数据情况
      1
      2
      3
      4
      5
      # 检查keys是否正确
      print(train[0].__dict__.keys())
      print(test[0].__dict__.keys())
      # 抽查内容是否正确
      print(train[0].comment_text)
    • 词汇表(vocab)
      Word Embedding 的基本思想是收集一个比较大的语料库(尽量与所做的任务相关),在语料库中使用word2vec之类的方法构建词语到向量(或数字)的映射关系,之后将这一映射关系应用于当前的任务,将句子中的词语转为向量表示。在torchtext中可以使用Field自带的build_vocab函数完成词汇表构建。
      1
      TEXT.build_vocab(train)
    • 数据迭代器
      相当于dataloader
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      from torchtext.data import Iterator, BucketIterator
      # 若只针对训练集构造迭代器
      # train_iter = data.BucketIterator(dataset=train, batch_size=8, shuffle=True, sort_within_batch=False, repeat=False)

      # 同时对训练集和验证集进行迭代器的构建
      train_iter, val_iter = BucketIterator.splits(
      (train, valid), # 构建数据集所需的数据集
      batch_sizes=(8, 8),
      device=-1, # 如果使用gpu,此处将-1更换为GPU的编号
      sort_key=lambda x: len(x.comment_text), # the BucketIterator needs to be told what function it should use to group the data.
      sort_within_batch=False
      )

      test_iter = Iterator(test, batch_size=8, device=-1, sort=False, sort_within_batch=False)
    • 使用自带数据集
      若干常用的数据集
  2. 评测指标(metric)
    NLP中部分任务的评测不是通过准确率等指标完成的,比如机器翻译任务常用BLEU (bilingual evaluation understudy) score来评价预测文本和标签文本之间的相似程度。torchtext中可以直接调用torchtext.data.metrics.bleu_score来快速实现BLEU
    1
    2
    3
    4
    from torchtext.data.metrics import bleu_score
    candidate_corpus = [['My', 'full', 'pytorch', 'test'], ['Another', 'Sentence']]
    references_corpus = [[['My', 'full', 'pytorch', 'test'], ['Completely', 'Different']], [['No', 'Match']]]
    bleu_score(candidate_corpus, references_corpus)
  3. 其他
    由于NLP常用的网络结构比较固定,torchtext并不像torchvision那样提供一系列常用的网络结构。模型主要通过torch.nn中的模块来实现,比如torch.nn.LSTM、torch.nn.RNN等。

2 模型部署

将PyTorch训练好的模型转换为ONNX 格式,然后使用ONNX Runtime运行它进行推理。将得到的权重进行变换才能使我们的模型可以成功部署在上述设备上。

2.1 使用ONNX进行部署并推理

ONNX Runtime 是由微软维护的一个跨平台机器学习推理加速器,它直接对接ONNX,可以直接读取.onnx文件并实现推理,不需要再把 .onnx 格式的文件转换成其他格式的文件。PyTorch借助ONNX Runtime也完成了部署的最后一公里,构建了 PyTorch –> ONNX –> ONNX Runtime 部署流水线,我们只需要将模型转换为 .onnx 文件,并在 ONNX Runtime 上运行模型即可。

2.2 ONNX和ONNX Runtime简介

1
2
3
4
5
6
7
# 激活虚拟环境
conda activate env_name # env_name换成环境名称
# 安装onnx
pip install onnx
# 安装onnx runtime
pip install onnxruntime # 使用CPU进行推理
# pip install onnxruntime-gpu # 使用GPU进行推理

2.3 模型导出为ONNX

  1. 模型转换为ONNX格式
    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
    import torch.onnx 
    # 转换的onnx格式的名称,文件后缀需为.onnx
    onnx_file_name = "xxxxxx.onnx"
    # 我们需要转换的模型,将torch_model设置为自己的模型
    model = torch_model
    # 加载权重,将model.pth转换为自己的模型权重
    # 如果模型的权重是使用多卡训练出来,我们需要去除权重中多的module. 具体操作可以见5.4节
    model = model.load_state_dict(torch.load("model.pth"))
    # 导出模型前,必须调用model.eval()或者model.train(False)
    model.eval()
    # dummy_input就是一个输入的实例,仅提供输入shape、type等信息
    batch_size = 1 # 随机的取值,当设置dynamic_axes后影响不大
    dummy_input = torch.randn(batch_size, 1, 224, 224, requires_grad=True)
    # 这组输入对应的模型输出
    output = model(dummy_input)
    # 导出模型
    torch.onnx.export(model, # 模型的名称
    dummy_input, # 一组实例化输入
    onnx_file_name, # 文件保存路径/名称
    export_params=True, # 如果指定为True或默认, 参数也会被导出. 如果你要导出一个没训练过的就设为 False.
    opset_version=10, # ONNX 算子集的版本,当前已更新到15
    do_constant_folding=True, # 是否执行常量折叠优化
    input_names = ['input'], # 输入模型的张量的名称
    output_names = ['output'], # 输出模型的张量的名称
    # dynamic_axes将batch_size的维度指定为动态,
    # 后续进行推理的数据可以与导出的dummy_input的batch_size不同
    dynamic_axes={'input' : {0 : 'batch_size'},
    'output' : {0 : 'batch_size'}})
  2. ONNX模型的检验
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import onnx
    # 我们可以使用异常处理的方法进行检验
    try:
    # 当我们的模型不可用时,将会报出异常
    onnx.checker.check_model(self.onnx_model)
    except onnx.checker.ValidationError as e:
    print("The model is invalid: %s"%e)
    else:
    # 模型可用时,将不会报出异常,并会输出“The model is valid!”
    print("The model is valid!")
  3. ONNX可视化
    Netron

2.4 使用ONNX Runtime进行推理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 导入onnxruntime
import onnxruntime
# 需要进行推理的onnx模型文件名称
onnx_file_name = "xxxxxx.onnx"

# onnxruntime.InferenceSession用于获取一个 ONNX Runtime 推理器
ort_session = onnxruntime.InferenceSession(onnx_file_name)

# 构建字典的输入数据,字典的key需要与我们构建onnx模型时的input_names相同
# 输入的input_img 也需要改变为ndarray格式
ort_inputs = {'input': input_img}
# 我们更建议使用下面这种方法,因为避免了手动输入key
# ort_inputs = {ort_session.get_inputs()[0].name:input_img}

# run是进行模型的推理,第一个参数为输出张量名的列表,一般情况可以设置为None
# 第二个参数为构建的输入值的字典
# 由于返回的结果被列表嵌套,因此我们需要进行[0]的索引
ort_output = ort_session.run(None,ort_inputs)[0]
# output = {ort_session.get_outputs()[0].name}
# ort_output = ort_session.run([output], ort_inputs)[0]
  • PyTorch模型的输入为tensor,而ONNX的输入为array,因此我们需要对张量进行变换或者直接将数据读取为array格式,我们可以实现下面的方式进行张量到array的转化。
    1
    2
    def to_numpy(tensor):
    return tensor.detach().cpu().numpy() if tensor.requires_grad else tensor.cpu().numpy()
  • 输入的array的shape应该和我们导出模型的dummy_input的shape相同,如果图片大小不一样,我们应该先进行resize操作。
  • run的结果是一个列表,我们需要进行索引操作才能获得array格式的结果。
  • 在构建输入的字典时,我们需要注意字典的key应与导出ONNX格式设置的input_name相同,因此我们更建议使用上述的第二种方法构建输入的字典。

2.5 代码实战

pytorch- EXPORTING A MODEL FROM PYTORCH TO ONNX AND RUNNING IT USING ONNX RUNTIME

参考资料

深入浅出PyTorch

pytorch - 可视化

可视化网络结构

  1. 使用print函数打印模型基础信息
    只能得出基础构件的信息,既不能显示出每一层的shape,也不能显示对应参数量的大小
    1
    2
    import torchvision.models as models
    model = models.resnet18()
  2. 使用torchinfo可视化网络结构
    输出结构化的更详细的信息,包括模块信息(每一层的类型、输出shape和参数量)、模型整体的参数量、模型大小、一次前向或者反向传播需要的内存大小等
    1
    2
    3
    4
    import torchvision.models as models
    from torchinfo import summary
    resnet18 = models.resnet18() # 实例化模型
    summary(resnet18, (1, 3, 224, 224)) # 1:batch_size 3:图片的通道数 224: 图片的高宽

CNN可视化

  1. CNN卷积核可视化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import torch
    from torchvision.models import vgg11

    model = vgg11(pretrained=True)
    print(dict(model.features.named_children()))
    conv1 = dict(model.features.named_children())['3']
    kernel_set = conv1.weight.detach()
    num = len(conv1.weight.detach())
    print(kernel_set.shape)
    for i in range(0,num):
    i_kernel = kernel_set[i]
    plt.figure(figsize=(20, 17))
    if (len(i_kernel)) > 1:
    for idx, filer in enumerate(i_kernel):
    plt.subplot(9, 9, idx+1)
    plt.axis('off')
    plt.imshow(filer[ :, :].detach(),cmap='bwr')
  2. CNN特征图可视化方法
    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
    class Hook(object):
    def __init__(self):
    self.module_name = []
    self.features_in_hook = []
    self.features_out_hook = []

    def __call__(self,module, fea_in, fea_out):
    print("hooker working", self)
    self.module_name.append(module.__class__)
    self.features_in_hook.append(fea_in)
    self.features_out_hook.append(fea_out)
    return None


    def plot_feature(model, idx, inputs):
    hh = Hook()
    model.features[idx].register_forward_hook(hh)

    # forward_model(model,False)
    model.eval()
    _ = model(inputs)
    print(hh.module_name)
    print((hh.features_in_hook[0][0].shape))
    print((hh.features_out_hook[0].shape))

    out1 = hh.features_out_hook[0]

    total_ft = out1.shape[1]
    first_item = out1[0].cpu().clone()

    plt.figure(figsize=(20, 17))


    for ftidx in range(total_ft):
    if ftidx > 99:
    break
    ft = first_item[ftidx]
    plt.subplot(10, 10, ftidx+1)

    plt.axis('off')
    #plt.imshow(ft[ :, :].detach(),cmap='gray')
    plt.imshow(ft[ :, :].detach())
  3. CNN class activation map可视化方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import torch
    from torchvision.models import vgg11,resnet18,resnet101,resnext101_32x8d
    import matplotlib.pyplot as plt
    from PIL import Image
    import numpy as np

    model = vgg11(pretrained=True)
    img_path = './dog.png'
    # resize操作是为了和传入神经网络训练图片大小一致
    img = Image.open(img_path).resize((224,224))
    # 需要将原始图片转为np.float32格式并且在0-1之间
    rgb_img = np.float32(img)/255
    plt.imshow(img)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    from pytorch_grad_cam import GradCAM,ScoreCAM,GradCAMPlusPlus,AblationCAM,XGradCAM,EigenCAM,FullGrad
    from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget
    from pytorch_grad_cam.utils.image import show_cam_on_image

    target_layers = [model.features[-1]]
    # 选取合适的类激活图,但是ScoreCAM和AblationCAM需要batch_size
    cam = GradCAM(model=model,target_layers=target_layers)
    targets = [ClassifierOutputTarget(preds)]
    # 上方preds需要设定,比如ImageNet有1000类,这里可以设为200
    grayscale_cam = cam(input_tensor=img_tensor, targets=targets)
    grayscale_cam = grayscale_cam[0, :]
    cam_img = show_cam_on_image(rgb_img, grayscale_cam, use_rgb=True)
    print(type(cam_img))
    Image.fromarray(cam_img)
  4. 使用FlashTorch快速实现CNN可视化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # Download example images
    # !mkdir -p images
    # !wget -nv \
    # https://github.com/MisaOgura/flashtorch/raw/master/examples/images/great_grey_owl.jpg \
    # https://github.com/MisaOgura/flashtorch/raw/master/examples/images/peacock.jpg \
    # https://github.com/MisaOgura/flashtorch/raw/master/examples/images/toucan.jpg \
    # -P /content/images

    import matplotlib.pyplot as plt
    import torchvision.models as models
    from flashtorch.utils import apply_transforms, load_image
    from flashtorch.saliency import Backprop

    model = models.alexnet(pretrained=True)
    backprop = Backprop(model)

    image = load_image('/content/images/great_grey_owl.jpg')
    owl = apply_transforms(image)

    target_class = 24
    backprop.visualize(owl, target_class, guided=True, use_gpu=True)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import torchvision.models as models
    from flashtorch.activmax import GradientAscent

    model = models.vgg16(pretrained=True)
    g_ascent = GradientAscent(model.features)

    # specify layer and filter info
    conv5_1 = model.features[24]
    conv5_1_filters = [45, 271, 363, 489]

    g_ascent.visualize(conv5_1, conv5_1_filters, title="VGG16: conv5_1")

使用TensorBoard可视化训练过程

  1. TensorBoard安装
  2. TensorBoard可视化的基本逻辑
  3. TensorBoard的配置与启动
  4. TensorBoard模型结构可视化
    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
    import torch.nn as nn

    class Net(nn.Module):
    def __init__(self):
    super(Net, self).__init__()
    self.conv1 = nn.Conv2d(in_channels=3,out_channels=32,kernel_size = 3)
    self.pool = nn.MaxPool2d(kernel_size = 2,stride = 2)
    self.conv2 = nn.Conv2d(in_channels=32,out_channels=64,kernel_size = 5)
    self.adaptive_pool = nn.AdaptiveMaxPool2d((1,1))
    self.flatten = nn.Flatten()
    self.linear1 = nn.Linear(64,32)
    self.relu = nn.ReLU()
    self.linear2 = nn.Linear(32,1)
    self.sigmoid = nn.Sigmoid()

    def forward(self,x):
    x = self.conv1(x)
    x = self.pool(x)
    x = self.conv2(x)
    x = self.pool(x)
    x = self.adaptive_pool(x)
    x = self.flatten(x)
    x = self.linear1(x)
    x = self.relu(x)
    x = self.linear2(x)
    y = self.sigmoid(x)
    return y

    model = Net()
    print(model)

    writer.add_graph(model, input_to_model = torch.rand(1, 3, 224, 224))
    writer.close()
  5. TensorBoard图像可视化
    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
    import torchvision
    from torchvision import datasets, transforms
    from torch.utils.data import DataLoader

    transform_train = transforms.Compose(
    [transforms.ToTensor()])
    transform_test = transforms.Compose(
    [transforms.ToTensor()])

    train_data = datasets.CIFAR10(".", train=True, download=True, transform=transform_train)
    test_data = datasets.CIFAR10(".", train=False, download=True, transform=transform_test)
    train_loader = DataLoader(train_data, batch_size=64, shuffle=True)
    test_loader = DataLoader(test_data, batch_size=64)

    images, labels = next(iter(train_loader))

    # 仅查看一张图片
    writer = SummaryWriter('./pytorch_tb')
    writer.add_image('images[0]', images[0])
    writer.close()

    # 将多张图片拼接成一张图片,中间用黑色网格分割
    # create grid of images
    writer = SummaryWriter('./pytorch_tb')
    img_grid = torchvision.utils.make_grid(images)
    writer.add_image('image_grid', img_grid)
    writer.close()

    # 将多张图片直接写入
    writer = SummaryWriter('./pytorch_tb')
    writer.add_images("images",images,global_step = 0)
    writer.close()
  6. TensorBoard连续变量可视化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    writer = SummaryWriter('./pytorch_tb')
    for i in range(500):
    x = i
    y = x**2
    writer.add_scalar("x", x, i) #日志中记录x在第step i 的值
    writer.add_scalar("y", y, i) #日志中记录y在第step i 的值
    writer.close()

    writer1 = SummaryWriter('./pytorch_tb/x')
    writer2 = SummaryWriter('./pytorch_tb/y')
    for i in range(500):
    x = i
    y = x*2
    writer1.add_scalar("same", x, i) #日志中记录x在第step i 的值
    writer2.add_scalar("same", y, i) #日志中记录y在第step i 的值
    writer1.close()
    writer2.close()
  7. TensorBoard参数分布可视化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import torch
    import numpy as np

    # 创建正态分布的张量模拟参数矩阵
    def norm(mean, std):
    t = std * torch.randn((100, 20)) + mean
    return t

    writer = SummaryWriter('./pytorch_tb/')
    for step, mean in enumerate(range(-10, 10, 1)):
    w = norm(mean, 1)
    writer.add_histogram("w", w, step)
    writer.flush()
    writer.close()
  8. 服务器端使用TensorBoard
  9. 总结
    TensorBoard的基本逻辑就是文件的读写逻辑,写入想要可视化的数据,然后TensorBoard自己会读出来。

    参考资料

    深入浅出PyTorch

pytorch - 训练技巧

自定义损失函数

  1. 以函数定义
    简单直接
    1
    2
    3
    def my_loss(output, target):
    loss = torch.mean((output - target)**2)
    return loss
  2. 以类定义
    更加常用,继承自nn.Module,可以当成神经网络的一层,使用tensor可以自动求导
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class DiceLoss(nn.Module):
    def __init__(self,weight=None,size_average=True):
    super(DiceLoss,self).__init__()

    def forward(self,inputs,targets,smooth=1):
    inputs = F.sigmoid(inputs)
    inputs = inputs.view(-1)
    targets = targets.view(-1)
    intersection = (inputs * targets).sum()
    dice = (2.*intersection + smooth)/(inputs.sum() + targets.sum() + smooth)
    return 1 - dice

    # 使用方法
    criterion = DiceLoss()
    loss = criterion(input,targets)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class DiceBCELoss(nn.Module):
    def __init__(self, weight=None, size_average=True):
    super(DiceBCELoss, self).__init__()

    def forward(self, inputs, targets, smooth=1):
    inputs = F.sigmoid(inputs)
    inputs = inputs.view(-1)
    targets = targets.view(-1)
    intersection = (inputs * targets).sum()
    dice_loss = 1 - (2.*intersection + smooth)/(inputs.sum() + targets.sum() + smooth)
    BCE = F.binary_cross_entropy(inputs, targets, reduction='mean')
    Dice_BCE = BCE + dice_loss

    return Dice_BCE
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class IoULoss(nn.Module):
    def __init__(self, weight=None, size_average=True):
    super(IoULoss, self).__init__()

    def forward(self, inputs, targets, smooth=1):
    inputs = F.sigmoid(inputs)
    inputs = inputs.view(-1)
    targets = targets.view(-1)
    intersection = (inputs * targets).sum()
    total = (inputs + targets).sum()
    union = total - intersection

    IoU = (intersection + smooth)/(union + smooth)

    return 1 - IoU
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    ALPHA = 0.8
    GAMMA = 2

    class FocalLoss(nn.Module):
    def __init__(self, weight=None, size_average=True):
    super(FocalLoss, self).__init__()

    def forward(self, inputs, targets, alpha=ALPHA, gamma=GAMMA, smooth=1):
    inputs = F.sigmoid(inputs)
    inputs = inputs.view(-1)
    targets = targets.view(-1)
    BCE = F.binary_cross_entropy(inputs, targets, reduction='mean')
    BCE_EXP = torch.exp(-BCE)
    focal_loss = alpha * (1-BCE_EXP)**gamma * BCE

    return focal_loss

动态调整学习率

  1. 使用scheduler
    PyTorch在torch.optim.lr_scheduler封装好了一些动态调整学习率的方法。

    • lr_scheduler.LambdaLR
    • lr_scheduler.MultiplicativeLR
    • lr_scheduler.StepLR
    • lr_scheduler.MultiStepLR
    • lr_scheduler.ExponentialLR
    • lr_scheduler.CosineAnnealingLR
    • lr_scheduler.ReduceLROnPlateau
    • lr_scheduler.CyclicLR
    • lr_scheduler.OneCycleLR
    • lr_scheduler.CosineAnnealingWarmRestarts

    将scheduler.step()放在optimizer.step()后面进行使用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # 选择一种优化器
    optimizer = torch.optim.Adam(...)
    # 选择上面提到的一种或多种动态调整学习率的方法
    scheduler1 = torch.optim.lr_scheduler....
    scheduler2 = torch.optim.lr_scheduler....
    ...
    schedulern = torch.optim.lr_scheduler....
    # 进行训练
    for epoch in range(100):
    train(...)
    validate(...)
    optimizer.step()
    # 需要在优化器参数更新之后再动态调整学习率
    scheduler1.step()
    ...
    schedulern.step()

  2. 自定义scheduler

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    def adjust_learning_rate(optimizer, epoch):
    lr = args.lr * (0.1 ** (epoch // 30)) # 学习率每30轮下降为原来的1/10
    for param_group in optimizer.param_groups:
    param_group['lr'] = lr

    optimizer = torch.optim.SGD(model.parameters(),lr = args.lr,momentum = 0.9)
    for epoch in range(10):
    train(...)
    validate(...)
    adjust_learning_rate(optimizer,epoch)

模型微调-torchvision

  1. 模型微调-torchvision
    • 在源数据集上预训练一个源模型
    • 创建一个新的目标模型,复制源模型除了输出层外的所有结构,其参数学习到了源数据集的知识,假设其同样适用于目标数据集,且源模型输出层和源数据集标签密切相关,所以目标模型不采用它。
    • 为目标层添加目标数据集类别个数的输出层,随机初始化模型参数。
    • 在目标数据集上训练目标模型,从头训练输出层,其他层参数微调。
  2. 使用已有模型结构
    • 实例化网络
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      import torchvision.models as models
      resnet18 = models.resnet18()
      # resnet18 = models.resnet18(pretrained=False) 等价于与上面的表达式
      alexnet = models.alexnet()
      vgg16 = models.vgg16()
      squeezenet = models.squeezenet1_0()
      densenet = models.densenet161()
      inception = models.inception_v3()
      googlenet = models.googlenet()
      shufflenet = models.shufflenet_v2_x1_0()
      mobilenet_v2 = models.mobilenet_v2()
      mobilenet_v3_large = models.mobilenet_v3_large()
      mobilenet_v3_small = models.mobilenet_v3_small()
      resnext50_32x4d = models.resnext50_32x4d()
      wide_resnet50_2 = models.wide_resnet50_2()
      mnasnet = models.mnasnet1_0()
    • 传递pretrained参数
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      import torchvision.models as models
      resnet18 = models.resnet18(pretrained=True)
      alexnet = models.alexnet(pretrained=True)
      squeezenet = models.squeezenet1_0(pretrained=True)
      vgg16 = models.vgg16(pretrained=True)
      densenet = models.densenet161(pretrained=True)
      inception = models.inception_v3(pretrained=True)
      googlenet = models.googlenet(pretrained=True)
      shufflenet = models.shufflenet_v2_x1_0(pretrained=True)
      mobilenet_v2 = models.mobilenet_v2(pretrained=True)
      mobilenet_v3_large = models.mobilenet_v3_large(pretrained=True)
      mobilenet_v3_small = models.mobilenet_v3_small(pretrained=True)
      resnext50_32x4d = models.resnext50_32x4d(pretrained=True)
      wide_resnet50_2 = models.wide_resnet50_2(pretrained=True)
      mnasnet = models.mnasnet1_0(pretrained=True)
      pytorch模型扩展为.pt .pth,模型权重一旦被下载下次就不需要加载,可以将自己的权重下载下来放到同文件夹下,然后再将参数加载网络。如果中途强行停止下载的话,一定要去对应路径下将权重文件删除干净,要不然可能会报错。
      1
      2
      self.model = models.resnet50(pretrained=False)
      self.model.load_state_dict(torch.load('./model/resnet50-19c8e357.pth'))
  3. 训练特定层
    如果我们正在提取特征并且只想为新初始化的层计算梯度,其他参数不进行改变。那我们就需要通过设置requires_grad = False来冻结部分层。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    def set_parameter_requires_grad(model, feature_extracting):
    if feature_extracting:
    for param in model.parameters():
    param.requires_grad = False

    import torchvision.models as models
    # 冻结参数的梯度
    feature_extract = True
    model = models.resnet18(pretrained=True)
    set_parameter_requires_grad(model, feature_extract)
    # 修改模型
    num_ftrs = model.fc.in_features
    model.fc = nn.Linear(in_features=num_ftrs, out_features=4, bias=True)
    之后在训练过程中,model仍会进行梯度回传,但是参数更新则只会发生在fc层。

模型微调 - timm

torchvision的扩充版本

  1. 查看预训练模型种类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import timm
    avail_pretrained_models = timm.list_models(pretrained=True)
    len(avail_pretrained_models)

    # 模糊查询
    all_densnet_models = timm.list_models("*densenet*")
    all_densnet_models

    # 查看模型参数
    model = timm.create_model('resnet34',num_classes=10,pretrained=True)
    model.default_cfg
  2. 使用和修改预训练模型
    通过timm.create_model()的方法来进行模型的创建,传入参数pretrained=True,来使用预训练模型。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import timm
    import torch

    model = timm.create_model('resnet34',pretrained=True)
    x = torch.randn(1,3,224,224)
    output = model(x)
    output.shape

    # 查看某一层模型参数
    model = timm.create_model('resnet34',pretrained=True)
    list(dict(model.named_children())['conv1'].parameters())

    # 修改模型
    model = timm.create_model('resnet34',num_classes=10,pretrained=True)
    x = torch.randn(1,3,224,224)
    output = model(x)
    output.shape

    # 改变输入通道数
    model = timm.create_model('resnet34',num_classes=10,pretrained=True,in_chans=1)
    x = torch.randn(1,1,224,224)
    output = model(x)
  3. 模型保存
    1
    2
    torch.save(model.state_dict(),'./checkpoint/timm_model.pth')
    model.load_state_dict(torch.load('./checkpoint/timm_model.pth'))

半精度训练

GPU的性能主要分为两部分:算力和显存,前者决定了显卡计算的速度,后者则决定了显卡可以同时放入多少数据用于计算。在可以使用的显存数量一定的情况下,每次训练能够加载的数据更多(也就是batch size更大),则也可以提高训练效率。另外,有时候数据本身也比较大(比如3D图像、视频等),显存较小的情况下可能甚至batch size为1的情况都无法实现。因此,合理使用显存也就显得十分重要。

我们观察PyTorch默认的浮点数存储方式用的是torch.float32,小数点后位数更多固然能保证数据的精确性,但绝大多数场景其实并不需要这么精确,只保留一半的信息也不会影响结果,也就是使用torch.float16格式。由于数位减了一半,因此被称为“半精度”。显然半精度能够减少显存占用,使得显卡可以同时加载更多数据进行计算。

  1. 半精度训练的设置
    import autocast
    1
    from torch.cuda.amp import autocast
    在模型定义中,使用python的装饰器方法,用autocast装饰模型中的forward函数。
    1
    2
    3
    4
    @autocast()   
    def forward(self, x):
    ...
    return x
    在训练过程中,只需在将数据输入模型及其之后的部分放入“with autocast():“即可:
    1
    2
    3
    4
    5
    for x in train_loader:
    x = x.cuda()
    with autocast():
    output = model(x)
    ...

数据增强-imgaug

  1. 单张图片处理
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import imageio
    import imgaug as ia
    %matplotlib inline

    # 图片的读取
    img = imageio.imread("./Lenna.jpg")

    # 使用Image进行读取
    # img = Image.open("./Lenna.jpg")
    # image = np.array(img)
    # ia.imshow(image)

    # 可视化图片
    ia.imshow(img)
    imgaug包含了许多从Augmenter继承的数据增强的操作
    1
    2
    3
    4
    5
    6
    7
    8
    9
    from imgaug import augmenters as iaa

    # 设置随机数种子
    ia.seed(4)

    # 实例化方法
    rotate = iaa.Affine(rotate=(-4,45))
    img_aug = rotate(image=img)
    ia.imshow(img_aug)
    对一张图片做多种数据增强处理
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    iaa.Sequential(children=None, # Augmenter集合
    random_order=False, # 是否对每个batch使用不同顺序的Augmenter list
    name=None,
    deterministic=False,
    random_state=None)
    # 构建处理序列
    aug_seq = iaa.Sequential([
    iaa.Affine(rotate=(-25,25)),
    iaa.AdditiveGaussianNoise(scale=(10,60)),
    iaa.Crop(percent=(0,0.2))
    ])
    # 对图片进行处理,image不可以省略,也不能写成images
    image_aug = aug_seq(image=img)
    ia.imshow(image_aug)
  2. 对批次图片进行处理
    对批次的图片以同一种方式处理
    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
    images = [img,img,img,img,]
    images_aug = rotate(images=images)
    ia.imshow(np.hstack(images_aug))

    # 对批次图片进行多种增强
    aug_seq = iaa.Sequential([
    iaa.Affine(rotate=(-25, 25)),
    iaa.AdditiveGaussianNoise(scale=(10, 60)),
    iaa.Crop(percent=(0, 0.2))
    ])

    # 传入时需要指明是images参数
    images_aug = aug_seq.augment_images(images = images)
    #images_aug = aug_seq(images = images)
    ia.imshow(np.hstack(images_aug))

    # 对批次的图片分部分处理
    iaa.Sometimes(p=0.5, # 代表划分比例
    then_list=None, # Augmenter集合。p概率的图片进行变换的Augmenters。
    else_list=None, #1-p概率的图片会被进行变换的Augmenters。注意变换的图片应用的Augmenter只能是then_list或者else_list中的一个。
    name=None,
    deterministic=False,
    random_state=None)

    # 对不同大小的图片进行处理
    # 构建pipline
    seq = iaa.Sequential([
    iaa.CropAndPad(percent=(-0.2, 0.2), pad_mode="edge"), # crop and pad images
    iaa.AddToHueAndSaturation((-60, 60)), # change their color
    iaa.ElasticTransformation(alpha=90, sigma=9), # water-like effect
    iaa.Cutout() # replace one squared area within the image by a constant intensity value
    ], random_order=True)

    # 加载不同大小的图片
    images_different_sizes = [
    imageio.imread("https://upload.wikimedia.org/wikipedia/commons/e/ed/BRACHYLAGUS_IDAHOENSIS.jpg"),
    imageio.imread("https://upload.wikimedia.org/wikipedia/commons/c/c9/Southern_swamp_rabbit_baby.jpg"),
    imageio.imread("https://upload.wikimedia.org/wikipedia/commons/9/9f/Lower_Keys_marsh_rabbit.jpg")
    ]

    # 对图片进行增强
    images_aug = seq(images=images_different_sizes)

    # 可视化结果
    print("Image 0 (input shape: %s, output shape: %s)" % (images_different_sizes[0].shape, images_aug[0].shape))
    ia.imshow(np.hstack([images_different_sizes[0], images_aug[0]]))

    print("Image 1 (input shape: %s, output shape: %s)" % (images_different_sizes[1].shape, images_aug[1].shape))
    ia.imshow(np.hstack([images_different_sizes[1], images_aug[1]]))

    print("Image 2 (input shape: %s, output shape: %s)" % (images_different_sizes[2].shape, images_aug[2].shape))
    ia.imshow(np.hstack([images_different_sizes[2], images_aug[2]]))

使用argparse进行调参

直接在命令行中就可以向程序中传入参数。我们可以使用python file.py来运行python文件。而argparse的作用就是将命令行传入的其他参数进行解析、保存和使用。在使用argparse后,我们在命令行输入的参数就可以以这种形式python file.py –lr 1e-4 –batch_size 32来完成对常见超参数的设置。

argparse的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# demo.py
import argparse

# 创建ArgumentParser()对象
parser = argparse.ArgumentParser()

# 添加参数
parser.add_argument('-o', '--output', action='store_true',
help="shows output")
# action = `store_true` 会将output参数记录为True
# type 规定了参数的格式
# default 规定了默认值
parser.add_argument('--lr', type=float, default=3e-5, help='select the learning rate, default=1e-3')

parser.add_argument('--batch_size', type=int, required=True, help='input batch size')
# 使用parse_args()解析函数
args = parser.parse_args()

if args.output:
print("This is some output")
print(f"learning rate:{args.lr} ")

输入python demo.py –lr 3e-4 –batch_size 32,得到:

This is some output
learning rate: 3e-4

更加高效使用argparse修改超参数

为了使代码更加简洁和模块化,我一般会将有关超参数的操作写在config.py,然后在train.py或者其他文件导入就可以。

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
import argparse  

def get_options(parser=argparse.ArgumentParser()):

parser.add_argument('--workers', type=int, default=0,
help='number of data loading workers, you had better put it '
'4 times of your gpu')

parser.add_argument('--batch_size', type=int, default=4, help='input batch size, default=64')

parser.add_argument('--niter', type=int, default=10, help='number of epochs to train for, default=10')

parser.add_argument('--lr', type=float, default=3e-5, help='select the learning rate, default=1e-3')

parser.add_argument('--seed', type=int, default=118, help="random seed")

parser.add_argument('--cuda', action='store_true', default=True, help='enables cuda')
parser.add_argument('--checkpoint_path',type=str,default='',
help='Path to load a previous trained model if not empty (default empty)')
parser.add_argument('--output',action='store_true',default=True,help="shows output")

opt = parser.parse_args()

if opt.output:
print(f'num_workers: {opt.workers}')
print(f'batch_size: {opt.batch_size}')
print(f'epochs (niters) : {opt.niter}')
print(f'learning rate : {opt.lr}')
print(f'manual_seed: {opt.seed}')
print(f'cuda enable: {opt.cuda}')
print(f'checkpoint_path: {opt.checkpoint_path}')

return opt

if __name__ == '__main__':
opt = get_options()

随后在train.py等其他文件,我们就可以使用下面的这样的结构来调用参数。

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
# 导入必要库
...
import config

opt = config.get_options()

manual_seed = opt.seed
num_workers = opt.workers
batch_size = opt.batch_size
lr = opt.lr
niters = opt.niters
checkpoint_path = opt.checkpoint_path

# 随机数的设置,保证复现结果
def set_seed(seed):
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
random.seed(seed)
np.random.seed(seed)
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True

...


if __name__ == '__main__':
set_seed(manual_seed)
for epoch in range(niters):
train(model,lr,batch_size,num_workers,checkpoint_path)
val(model,lr,batch_size,num_workers,checkpoint_path)

参考资料

深入浅出PyTorch

pytorch - 模型定义

模型定义的方式

基于nn.Module,可以通过Sequential,ModuleList和ModuleDict三种方式定义PyTorch模型。

Sequential

将模型的层按序排列起来,按顺序读取,不用写forward,但丧失灵活性

  1. Sequential
    1
    2
    3
    4
    5
    6
    7
    8
    ## Sequential: Direct list
    import torch.nn as nn
    net1 = nn.Sequential(
    nn.Linear(784, 256),
    nn.ReLU(),
    nn.Linear(256, 10),
    )
    print(net1)
     Sequential(
       (0): Linear(in_features=784, out_features=256, bias=True)
       (1): ReLU()
       (2): Linear(in_features=256, out_features=10, bias=True)
     )
    
  2. Ordered Dict
    1
    2
    3
    4
    5
    6
    7
    8
    import collections
    import torch.nn as nn
    net2 = nn.Sequential(collections.OrderedDict([
    ('fc1', nn.Linear(784, 256)),
    ('relu1', nn.ReLU()),
    ('fc2', nn.Linear(256, 10))
    ]))
    print(net2)
     Sequential(
       (fc1): Linear(in_features=784, out_features=256, bias=True)
       (relu1): ReLU()
       (fc2): Linear(in_features=256, out_features=10, bias=True)
     )
    

ModuleList

ModuleList 接收一个子模块(或层,需属于nn.Module类)的列表作为输入,类似List那样进行append和extend操作。同时,子模块或层的权重也会自动添加到网络中来。

1
2
3
4
net3 = nn.ModuleList([nn.Linear(784, 256), nn.ReLU()])
net3.append(nn.Linear(256, 10)) # # 类似List的append操作
print(net3[-1]) # 类似List的索引访问
print(net3)

ModuleList 并没有定义一个网络,它只是将不同的模块储存在一起。把modellist写到初始化,再定义forward函数明确传输顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Net3(nn.Module):
def __init__(self):
super().__init__()
self.modulelist = nn.ModuleList([nn.Linear(784, 256), nn.ReLU()])
self.modulelist.append(nn.Linear(256, 10))

def forward(self, x):
for layer in self.modulelist:
x = layer(x)
return x
net3_ = Net3()
out3_ = net3_(a)
print(out3_.shape)

ModuleDict

ModuleDict和ModuleList的作用类似,只是ModuleDict能够更方便地为神经网络的层添加名称。同样地,ModuleDict并没有定义一个网络,它只是将不同的模块储存在一起,要定义forward。

1
2
3
4
5
6
7
8
net = nn.ModuleDict({
'linear': nn.Linear(784, 256),
'act': nn.ReLU(),
})
net['output'] = nn.Linear(256, 10) # 添加
print(net['linear']) # 访问
print(net.output)
print(net)

利用模型块快速搭建复杂网络

当模型有很多层的时候,其中很多重复出现的结构可以定义为一个模块,便利模型构建。

如U-Net所示,模型左右对称,每个子层内部有两次卷积,左侧下采样连接,右侧上采样连接,每层模型块和上下模型块连接,同层的左右模型块连接。

  1. 双次卷积
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class DoubleConv(nn.Module):
    """(convolution => [BN] => ReLU) * 2"""

    def __init__(self, in_channels, out_channels, mid_channels=None):
    super().__init__()
    if not mid_channels:
    mid_channels = out_channels
    self.double_conv = nn.Sequential(
    nn.Conv2d(in_channels, mid_channels, kernel_size=3, padding=1, bias=False),
    nn.BatchNorm2d(mid_channels),
    nn.ReLU(inplace=True),
    nn.Conv2d(mid_channels, out_channels, kernel_size=3, padding=1, bias=False),
    nn.BatchNorm2d(out_channels),
    nn.ReLU(inplace=True)
    )

    def forward(self, x):
    return self.double_conv(x)
  2. 下采样
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Down(nn.Module):
    """Downscaling with maxpool then double conv"""

    def __init__(self, in_channels, out_channels):
    super().__init__()
    self.maxpool_conv = nn.Sequential(
    nn.MaxPool2d(2),
    DoubleConv(in_channels, out_channels)
    )

    def forward(self, x):
    return self.maxpool_conv(x)
  3. 上采样
    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
    class Up(nn.Module):
    """Upscaling then double conv"""

    def __init__(self, in_channels, out_channels, bilinear=True):
    super().__init__()

    # if bilinear, use the normal convolutions to reduce the number of channels
    if bilinear: # 插值
    self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)
    self.conv = DoubleConv(in_channels, out_channels, in_channels // 2)
    else:
    self.up = nn.ConvTranspose2d(in_channels, in_channels // 2, kernel_size=2, stride=2)
    self.conv = DoubleConv(in_channels, out_channels)

    def forward(self, x1, x2):
    x1 = self.up(x1)
    # input is CHW
    diffY = x2.size()[2] - x1.size()[2]
    diffX = x2.size()[3] - x1.size()[3]

    x1 = F.pad(x1, [diffX // 2, diffX - diffX // 2,
    diffY // 2, diffY - diffY // 2])
    # if you have padding issues, see
    # https://github.com/HaiyongJiang/U-Net-Pytorch-Unstructured-Buggy/commit/0e854509c2cea854e247a9c615f175f76fbb2e3a
    # https://github.com/xiaopeng-liao/Pytorch-UNet/commit/8ebac70e633bac59fc22bb5195e513d5832fb3bd
    x = torch.cat([x2, x1], dim=1)
    # 连接左侧的数据再卷积
    return self.conv(x)
  4. 输出
    1
    2
    3
    4
    5
    6
    7
    class OutConv(nn.Module):
    def __init__(self, in_channels, out_channels):
    super(OutConv, self).__init__()
    self.conv = nn.Conv2d(in_channels, out_channels, kernel_size=1)

    def forward(self, x):
    return self.conv(x)
  5. 组装
    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
    class UNet(nn.Module):
    def __init__(self, n_channels, n_classes, bilinear=True):
    super(UNet, self).__init__()
    self.n_channels = n_channels
    self.n_classes = n_classes
    self.bilinear = bilinear

    self.inc = DoubleConv(n_channels, 64)
    self.down1 = Down(64, 128)
    self.down2 = Down(128, 256)
    self.down3 = Down(256, 512)
    factor = 2 if bilinear else 1
    self.down4 = Down(512, 1024 // factor)
    self.up1 = Up(1024, 512 // factor, bilinear)
    self.up2 = Up(512, 256 // factor, bilinear)
    self.up3 = Up(256, 128 // factor, bilinear)
    self.up4 = Up(128, 64, bilinear)
    self.outc = OutConv(64, n_classes)

    def forward(self, x):
    x1 = self.inc(x)
    x2 = self.down1(x1)
    x3 = self.down2(x2)
    x4 = self.down3(x3)
    x5 = self.down4(x4)
    x = self.up1(x5, x4)
    x = self.up2(x, x3)
    x = self.up3(x, x2)
    x = self.up4(x, x1)
    logits = self.outc(x)
    return logits
    unet = UNet(3,1)
    unet

模型修改

当有一个现成的模型需要对结构进行修改使用时,我们可以在已有模型上修改。

  1. 修改模型层
    1
    2
    3
    import copy
    unet1 = copy.deepcopy(unet)
    unet1.outc
    先复制,然后修改outc
    1
    2
    3
    b = torch.rand(1,3,224,224)
    out_unet1 = unet1(b)
    print(out_unet1.shape)
    要把输出Chanel变成5,重新实例化outc
    1
    2
    3
    4
    unet1.outc = OutConv(64, 5)
    unet1.outc
    out_unet1 = unet1(b)
    print(out_unet1.shape)
  2. 添加额外输入
    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
    class UNet2(nn.Module):
    def __init__(self, n_channels, n_classes, bilinear=True):
    super(UNet2, self).__init__()
    self.n_channels = n_channels
    self.n_classes = n_classes
    self.bilinear = bilinear

    self.inc = DoubleConv(n_channels, 64)
    self.down1 = Down(64, 128)
    self.down2 = Down(128, 256)
    self.down3 = Down(256, 512)
    factor = 2 if bilinear else 1
    self.down4 = Down(512, 1024 // factor)
    self.up1 = Up(1024, 512 // factor, bilinear)
    self.up2 = Up(512, 256 // factor, bilinear)
    self.up3 = Up(256, 128 // factor, bilinear)
    self.up4 = Up(128, 64, bilinear)
    self.outc = OutConv(64, n_classes)

    def forward(self, x, add_variable):
    x1 = self.inc(x)
    x2 = self.down1(x1)
    x3 = self.down2(x2)
    x4 = self.down3(x3)
    x5 = self.down4(x4)
    x = self.up1(x5, x4)
    x = self.up2(x, x3)
    x = self.up3(x, x2)
    x = self.up4(x, x1)
    x = x + add_variable #修改点
    logits = self.outc(x)
    return logits
    unet2 = UNet2(3,1)

    c = torch.rand(1,1,224,224)
    out_unet2 = unet2(b, c)
    print(out_unet2.shape)
    或用torch.cat实现了tensor的拼接,如x = torch.cat((self.dropout(self.relu(x)), add_variable.unsqueeze(1)),1)。
  3. 添加额外输出
    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
    class UNet3(nn.Module):
    def __init__(self, n_channels, n_classes, bilinear=True):
    super(UNet3, self).__init__()
    self.n_channels = n_channels
    self.n_classes = n_classes
    self.bilinear = bilinear

    self.inc = DoubleConv(n_channels, 64)
    self.down1 = Down(64, 128)
    self.down2 = Down(128, 256)
    self.down3 = Down(256, 512)
    factor = 2 if bilinear else 1
    self.down4 = Down(512, 1024 // factor)
    self.up1 = Up(1024, 512 // factor, bilinear)
    self.up2 = Up(512, 256 // factor, bilinear)
    self.up3 = Up(256, 128 // factor, bilinear)
    self.up4 = Up(128, 64, bilinear)
    self.outc = OutConv(64, n_classes)

    def forward(self, x):
    x1 = self.inc(x)
    x2 = self.down1(x1)
    x3 = self.down2(x2)
    x4 = self.down3(x3)
    x5 = self.down4(x4)
    x = self.up1(x5, x4)
    x = self.up2(x, x3)
    x = self.up3(x, x2)
    x = self.up4(x, x1)
    logits = self.outc(x)
    return logits, x5 # 修改点
    unet3 = UNet3(3,1)

    c = torch.rand(1,1,224,224)
    out_unet3, mid_out = unet3(b)
    print(out_unet3.shape, mid_out.shape)

模型保存和读取

单卡/多卡,整个/部分模型, unet.state_dict()查看模型权重,保存的模型格式: pt pth pkl。

  1. CPU或单卡:保存&读取整个模型

    1
    2
    3
    torch.save(unet, "./unet_example.pth")
    loaded_unet = torch.load("./unet_example.pth")
    loaded_unet.state_dict()
  2. CPU或单卡:保存&读取模型权重

    1
    2
    3
    4
    torch.save(unet.state_dict(), "./unet_weight_example.pth")
    loaded_unet_weights = torch.load("./unet_weight_example.pth")
    unet.load_state_dict(loaded_unet_weights) # 用已经定义好的模型结构加载变量
    unet.state_dict()
  3. 多卡:保存&读取整个模型。注意模型层名称前多了module
    不建议,因为保存模型的GPU_id等信息和读取后训练环境可能不同,尤其是要把保存的模型交给另一用户使用的情况

    1
    2
    3
    4
    5
    6
    os.environ['CUDA_VISIBLE_DEVICES'] = '2,3'
    unet_mul = copy.deepcopy(unet)
    unet_mul = nn.DataParallel(unet_mul).cuda()
    torch.save(unet_mul, "./unet_mul_example.pth")
    loaded_unet_mul = torch.load("./unet_mul_example.pth")
    loaded_unet_mul
  4. 多卡:保存&读取模型权重。

    1
    2
    3
    4
    5
    torch.save(unet_mul.state_dict(), "./unet_weight_mul_example.pth")
    loaded_unet_weights_mul = torch.load("./unet_weight_mul_example.pth")
    unet_mul.load_state_dict(loaded_unet_weights_mul)
    unet_mul = nn.DataParallel(unet_mul).cuda()
    unet_mul.state_dict()

    另外,如果保存的是整个模型,也建议采用提取权重的方式构建新的模型:

    1
    2
    3
    unet_mul.state_dict = loaded_unet_mul.state_dict
    unet_mul = nn.DataParallel(unet_mul).cuda()
    unet_mul.state_dict()

参考资料

深入浅出PyTorch

pytorch - 各个组件和实践

本章介绍pytorch进行深度学习模型训练的各个组件和实践,层层搭建神经网络模型。

神经网络学习机制

  • 数据预处理
    完成一项机器学习任务时的步骤,首先需要对数据进行预处理,其中重要的步骤包括数据格式的统一和必要的数据变换,同时划分训练集和测试集。
  • 模型设计
    选择模型。
  • 损失函数和优化方案设计
    设定损失函数和优化方法,以及对应的超参数(当然可以使用sklearn这样的机器学习库中模型自带的损失函数和优化器)。
  • 前向传播
    用模型去拟合训练集数据,
  • 反向传播
  • 更新参数
  • 模型表现
    在验证集/测试集上计算模型表现。

深度学习在实现上的特殊性

  • 样本量大,需要分批加载
    由于深度学习所需的样本量很大,一次加载全部数据运行可能会超出内存容量而无法实现;同时还有批(batch)训练等提高模型表现的策略,需要每次训练读取固定数量的样本送入模型中训练。
  • 逐层、模块化搭建网络(卷积层、全连接层、LSTM等)
    深度神经网络往往需要“逐层”搭建,或者预先定义好可以实现特定功能的模块,再把这些模块组装起来。
  • 多样化的损失函数和优化器设计
    由于模型设定的灵活性,因此损失函数和优化器要能够保证反向传播能够在用户自行定义的模型结构上实现
  • GPU的使用
    需要把模型和数据“放到”GPU上去做运算,同时还需要保证损失函数和优化器能够在GPU上工作。如果使用多张GPU进行训练,还需要考虑模型和数据分配、整合的问题。
  • 各个模块之间的配合
    深度学习中训练和验证过程最大的特点在于读入数据是按批的,每次读入一个批次的数据,放入GPU中训练,然后将损失函数反向传播回网络最前面的层,同时使用优化器调整网络参数。这里会涉及到各个模块配合的问题。训练/验证后还需要根据设定好的指标计算模型表现。

pytorch深度学习模块

将PyTorch完成深度学习的步骤拆解为几个主要模块,实际使用根据自身需求修改对应模块即可,深度学习->搭积木。

一、 基本配置

首先导入必要的包

1
2
3
4
5
6
import os
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
import torch.optim as optimizer

配置训练环境和超参数

1
2
3
4
5
6
7
8
9
10
11
# 配置GPU,这里有两种方式
## 方案一:使用os.environ,后续用.cuda()
os.environ['CUDA_VISIBLE_DEVICES'] = '0'
# 方案二:使用“device”,后续对要使用GPU的变量用.to(device)即可
device = torch.device("cuda:1" if torch.cuda.is_available() else "cpu")

## 配置其他超参数,如batch_size, num_workers, learning rate, 以及总的epochs
batch_size = 256 # 每次训练读入的数据量
num_workers = 4 # 有多少线程来读入数据,对于Windows用户,这里应设置为0,否则会出现多线程错误
lr = 1e-4 # 参数更新的步长
epochs = 20 # 训练多少轮

二、 数据读入

有两种方式:

  • 下载并使用PyTorch提供的内置数据集
    只适用于常见的数据集,如MNIST,CIFAR10等,PyTorch官方提供了数据下载。这种方式往往适用于快速测试方法(比如测试下某个idea在MNIST数据集上是否有效)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 首先设置数据变换
    from torchvision import transforms

    image_size = 28
    data_transform = transforms.Compose([
    transforms.ToPILImage(), # 这一步取决于后续的数据读取方式,如果使用内置数据集则不需要
    transforms.Resize(image_size),
    transforms.ToTensor()
    ])
    1
    2
    3
    4
    5
    ## 读取方式一:使用torchvision自带数据集,下载可能需要一段时间
    from torchvision import datasets

    train_data = datasets.FashionMNIST(root='./', train=True, download=True, transform=data_transform)
    test_data = datasets.FashionMNIST(root='./', train=False, download=True, transform=data_transform)
  • 从网站下载以csv格式存储的数据,读入并转成预期的格式
    需要自己构建Dataset,这对于PyTorch应用于自己的工作中十分重要,同时,还需要对数据进行必要的变换,比如说需要将图片统一为一致的大小,以便后续能够输入网络训练;需要将数据格式转为Tensor类,等等。
    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
    ## 读取方式二:读入csv格式的数据,自行构建Dataset类
    # csv数据下载链接:https://www.kaggle.com/zalando-research/fashionmnist
    class FMDataset(Dataset):
    def __init__(self, df, transform=None):
    self.df = df
    self.transform = transform
    self.images = df.iloc[:,1:].values.astype(np.uint8)
    self.labels = df.iloc[:, 0].values

    def __len__(self):
    return len(self.images)

    def __getitem__(self, idx):
    image = self.images[idx].reshape(28,28,1) # 1是单一通道
    label = int(self.labels[idx])
    if self.transform is not None:
    image = self.transform(image)
    else:
    image = torch.tensor(image/255., dtype=torch.float) # image/255 把数值归一化
    label = torch.tensor(label, dtype=torch.long)
    return image, label

    train_df = pd.read_csv("./fashion-mnist_train.csv")
    test_df = pd.read_csv("./fashion-mnist_test.csv")
    train_data = FMDataset(train_df, data_transform)
    test_data = FMDataset(test_df, data_transform)
    PyTorch数据读入是通过Dataset+DataLoader的方式完成的,Dataset定义好数据的格式和数据变换形式,DataLoader用iterative的方式不断读入批次数据。我们可以定义自己的Dataset类来实现灵活的数据读取,定义的类需要继承PyTorch自身的Dataset类。主要包含三个函数:
  • _init_: 用于向类中传入外部参数,同时定义样本集
  • _getitem_: 用于逐个读取样本集合中的元素,可以进行一定的变换,并将返回训练/验证所需的数据
  • _len_: 用于返回数据集的样本数
    在构建训练和测试数据集完成后,需要定义DataLoader类,以便在训练和测试时加载数据:
    1
    2
    train_loader = DataLoader(train_data, batch_size=batch_size, shuffle=True, num_workers=num_workers, drop_last=True)
    test_loader = DataLoader(test_data, batch_size=batch_size, shuffle=False, num_workers=num_workers)
    读入后,我们可以做一些数据可视化操作,主要是验证我们读入的数据是否正确:
    1
    2
    3
    4
    import matplotlib.pyplot as plt
    image, label = next(iter(train_loader))
    print(image.shape, label.shape)
    plt.imshow(image[0][0], cmap="gray")

三、 模型构建

我们这里的任务是对10个类别的“时装”图像进行分类,FashionMNIST数据集中包含已经预先划分好的训练集和测试集,其中训练集共60,000张图像,测试集共10,000张图像。每张图像均为单通道黑白图像,大小为32*32pixel,分属10个类别。由于任务较为简单,这里我们手搭一个CNN,而不考虑当下各种模型的复杂结构,模型构建完成后,将模型放到GPU上用于训练。
Module 类是nn模块里提供的一个模型构造类,是所有神经⽹网络模块的基类,我们可以继承它来定义我们想要的模型。

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
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv = nn.Sequential(
nn.Conv2d(1, 32, 5),
nn.ReLU(),
nn.MaxPool2d(2, stride=2),
nn.Dropout(0.3),
nn.Conv2d(32, 64, 5),
nn.ReLU(),
nn.MaxPool2d(2, stride=2),
nn.Dropout(0.3)
)
self.fc = nn.Sequential(
nn.Linear(64*4*4, 512),
nn.ReLU(),
nn.Linear(512, 10)
)

def forward(self, x):
x = self.conv(x)
x = x.view(-1, 64*4*4)
x = self.fc(x)
# x = nn.functional.normalize(x)
return x

model = Net()
model = model.cuda()
# model = nn.DataParallel(model).cuda() # 多卡训练时的写法,之后的课程中会进一步讲解
torch.nn.Conv2d(
  in_channels, 
  out_channels, 
  kernel_size, 
  stride=1, 
  padding=0, 
  dilation=1, 
  groups=1, 
  bias=True, 
  padding_mode='zeros', 
  device=None, 
  dtype=None)

torch.nn.MaxPool2d(
  kernel_size, 
  stride=None, 
  padding=0, 
  dilation=1, 
  return_indices=False, 
  ceil_mode=False)

$d_{out} =(d_{in}−dilation∗(kernelsize−1)−1+2∗padding)/stride+1)$
下面再举一个其他模型MLP:
继承Module类构造多层感知机,这里定义的MLP类重载了Module类的init函数和forward函数。它们分别用于创建模型参数和定义前向计算。前向计算也即正向传播。系统将通过⾃动求梯度⽽自动⽣成反向传播所需的 backward 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import torch
from torch import nn

class MLP(nn.Module):
# 声明带有模型参数的层,这里声明了两个全连接层
def __init__(self, **kwargs):
# 调用MLP父类Block的构造函数来进行必要的初始化。这样在构造实例时还可以指定其他函数
super(MLP, self).__init__(**kwargs)
self.hidden = nn.Linear(784, 256)
self.act = nn.ReLU()
self.output = nn.Linear(256,10)

# 定义模型的前向计算,即如何根据输入x计算返回所需要的模型输出
def forward(self, x):
o = self.act(self.hidden(x))
return self.output(o)

我们可以实例化 MLP 类得到模型变量 net 。下⾯的代码初始化 net 并传入输⼊数据 X 做一次前向计算。其中, net(X) 会调用 MLP 继承⾃自 Module 类的 call 函数,这个函数将调⽤用 MLP 类定义的forward 函数来完成前向计算。

1
2
3
4
X = torch.rand(2,784)
net = MLP()
print(net)
net(X)

注意,这里并没有将 Module 类命名为 Layer (层)或者 Model (模型)之类的名字,这是因为该类是一个可供⾃由组建的部件。它的子类既可以是⼀个层(如PyTorch提供的 Linear 类),⼜可以是一个模型(如这里定义的 MLP 类),或者是模型的⼀个部分。
下面介绍一些神经网络中常见的层:
深度学习的一个魅力在于神经网络中各式各样的层,例如全连接层、卷积层、池化层与循环层等等。虽然PyTorch提供了⼤量常用的层,但有时候我们依然希望⾃定义层。

  1. 不含模型参数的层
    下⾯构造的 MyLayer 类通过继承 Module 类自定义了一个将输入减掉均值后输出的层,并将层的计算定义在了 forward 函数里。这个层里不含模型参数。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import torch
    from torch import nn

    class MyLayer(nn.Module):
    def __init__(self, **kwargs):
    super(MyLayer, self).__init__(**kwargs)
    def forward(self, x):
    return x - x.mean()
    # 测试,实例化该层,然后做前向计算
    layer = MyLayer()
    layer(torch.tensor([1, 2, 3, 4, 5], dtype=torch.float))
  2. 含模型参数的层
    自定义含模型参数的自定义层,其中的模型参数可以通过训练学出。Parameter 类其实是 Tensor 的子类,如果一个 Tensor 是 Parameter ,那么它会⾃动被添加到模型的参数列表里。所以在⾃定义含模型参数的层时,我们应该将参数定义成 Parameter ,除了直接定义成 Parameter 类外,还可以使⽤ ParameterListParameterDict 分别定义参数的列表和字典。
    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
    class MyListDense(nn.Module):
    def __init__(self):
    super(MyListDense, self).__init__()
    self.params = nn.ParameterList([nn.Parameter(torch.randn(4, 4)) for i in range(3)])
    self.params.append(nn.Parameter(torch.randn(4, 1)))

    def forward(self, x):
    for i in range(len(self.params)):
    x = torch.mm(x, self.params[i]) # torch.mm矩阵相乘,两个二维张量相乘
    return x
    net = MyListDense()
    print(net)

    class MyDictDense(nn.Module):
    def __init__(self):
    super(MyDictDense, self).__init__()
    self.params = nn.ParameterDict({
    'linear1': nn.Parameter(torch.randn(4, 4)),
    'linear2': nn.Parameter(torch.randn(4, 1))
    })
    self.params.update({'linear3': nn.Parameter(torch.randn(4, 2))}) # 新增

    def forward(self, x, choice='linear1'):
    return torch.mm(x, self.params[choice])

    net = MyDictDense()
    print(net)
    下面给出常见的神经网络的一些层,比如卷积层、池化层,以及较为基础的AlexNet,LeNet等。
  3. 二维卷积层
    二维卷积层将输入和卷积核做互相关运算,并加上一个标量偏差来得到输出。卷积层的模型参数包括了卷积核和标量偏差。在训练模型的时候,通常我们先对卷积核随机初始化,然后不断迭代卷积核和偏差。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import torch
    from torch import nn

    # 卷积运算(二维互相关)
    def corr2d(X, K):
    h, w = K.shape
    X, K = X.float(), K.float()
    Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
    for i in range(Y.shape[0]):
    for j in range(Y.shape[1]):
    Y[i, j] = (X[i: i + h, j: j + w] * K).sum()
    return Y

    # 二维卷积层
    class Conv2D(nn.Module):
    def __init__(self, kernel_size):
    super(Conv2D, self).__init__()
    self.weight = nn.Parameter(torch.randn(kernel_size))
    self.bias = nn.Parameter(torch.randn(1))

    def forward(self, x):
    return corr2d(x, self.weight) + self.bias
    下面的例子里我们创建一个⾼和宽为3的二维卷积层,然后设输⼊高和宽两侧的填充数分别为1。给定一个高和宽为8的输入,我们发现输出的高和宽也是8。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # 定义一个函数来计算卷积层。它对输入和输出做相应的升维和降维
    def comp_conv2d(conv2d, X):
    # (1, 1)代表批量大小和通道数
    X = X.view((1, 1) + X.shape) # 加上两个维度
    Y = conv2d(X)
    return Y.view(Y.shape[2:]) # 排除不关心的前两维:批量和通道


    # 注意这里是两侧分别填充1⾏或列,所以在两侧一共填充2⾏或列
    conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3,padding=1)

    X = torch.rand(8, 8)
    comp_conv2d(conv2d, X).shape
    当卷积核的高和宽不同时,我们也可以通过设置高和宽上不同的填充数使输出和输入具有相同的高和宽。填充可以增加输出的高和宽。这常用来使输出与输入具有相同的高和宽。步幅可以减小输出的高和宽,例如输出的高和宽仅为输入的高和宽的 ( 为大于1的整数)。
    1
    2
    3
    # 使用高为5、宽为3的卷积核。在⾼和宽两侧的填充数分别为2和1,-5+2*2=-3+2*1
    conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=(5, 3), padding=(2, 1))
    comp_conv2d(conv2d, X).shape
  4. 池化层
    池化层每次对输入数据的一个固定形状窗口(⼜称池化窗口)中的元素计算输出。不同于卷积层里计算输⼊和核的互相关性,池化层直接计算池化窗口内元素的最大值或者平均值。该运算也分别叫做最大池化或平均池化。在二维最⼤池化中,池化窗口从输入数组的最左上方开始,按从左往右、从上往下的顺序,依次在输⼊数组上滑动。当池化窗口滑动到某⼀位置时,窗口中的输入子数组的最大值即输出数组中相应位置的元素。下面把池化层的前向计算实现在pool2d函数里。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    def pool2d(X, pool_size, mode='max'):
    p_h, p_w = pool_size
    Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
    for i in range(Y.shape[0]):
    for j in range(Y.shape[1]):
    if mode == 'max':
    Y[i, j] = X[i: i + p_h, j: j + p_w].max()
    elif mode == 'avg':
    Y[i, j] = X[i: i + p_h, j: j + p_w].mean()
    return Y
    我们可以使用torch.nn包来构建神经网络。我们已经介绍了autograd包,nn包则依赖于autograd包来定义模型并对它们求导。一个nn.Module包含各个层和一个forward(input)方法,该方法返回output。
  5. LeNet模型示例
    这是一个简单的前馈神经网络 (feed-forward network)(LeNet)。它接受一个输入,然后将它送入下一层,一层接一层的传递,最后给出输出。一个神经网络的典型训练过程如下: 定义包含一些可学习参数(或者叫权重)的神经网络 在输入数据集上迭代 通过网络处理输入 计算 loss (输出和正确答案的距离) 将梯度反向传播给网络的参数 更新网络的权重,一般使用一个简单的规则:weight = weight - learning_rate * gradient
    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
    import torch
    import torch.nn as nn
    import torch.nn.functional as F

    class Net(nn.Module):

    def __init__(self):
    super(Net, self).__init__()
    # 输入图像channel:1;输出channel:6;5x5卷积核
    self.conv1 = nn.Conv2d(1, 6, 5)
    self.conv2 = nn.Conv2d(6, 16, 5)
    # an affine operation: y = Wx + b
    self.fc1 = nn.Linear(16 * 5 * 5, 120)
    self.fc2 = nn.Linear(120, 84)
    self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
    # 2x2 Max pooling
    x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
    # 如果是方阵,则可以只使用一个数字进行定义
    x = F.max_pool2d(F.relu(self.conv2(x)), 2)
    x = x.view(-1, self.num_flat_features(x))
    x = F.relu(self.fc1(x))
    x = F.relu(self.fc2(x))
    x = self.fc3(x)
    return x

    def num_flat_features(self, x):
    size = x.size()[1:] # 除去批处理维度的其他所有维度
    num_features = 1
    for s in size:
    num_features *= s
    return num_features

    net = Net()
    print(net)
     Net(
       (conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
       (conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
       (fc1): Linear(in_features=400, out_features=120, bias=True)
       (fc2): Linear(in_features=120, out_features=84, bias=True)
       (fc3): Linear(in_features=84, out_features=10, bias=True)
     )
    

我们只需要定义 forward 函数,backward函数会在使用autograd时自动定义,backward函数用来计算导数。我们可以在 forward 函数中使用任何针对张量的操作和计算。一个模型的可学习参数可以通过net.parameters()返回。

1
2
3
params = list(net.parameters())
print(len(params))
print(params[0].size()) # conv1的权重

尝试随机输入

1
2
3
4
5
6
input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)
# 清零所有参数的梯度缓存,然后进行随机梯度的反向传播:
net.zero_grad()
out.backward(torch.randn(1, 10))

torch.nn只支持小批量处理 (mini-batches)。整个 torch.nn 包只支持小批量样本的输入,不支持单个样本的输入。比如,nn.Conv2d 接受一个4维的张量,即nSamples x nChannels x Height x Width 如果是一个单独的样本,只需要使用input.unsqueeze(0) 来添加一个“假的”批大小维度。
torch.Tensor - 一个多维数组,支持诸如backward()等的自动求导操作,同时也保存了张量的梯度。
nn.Module - 神经网络模块。是一种方便封装参数的方式,具有将参数移动到GPU、导出、加载等功能。
nn.Parameter - 张量的一种,当它作为一个属性分配给一个Module时,它会被自动注册为一个参数。
autograd.Function - 实现了自动求导前向和反向传播的定义,每个Tensor至少创建一个Function节点,该节点连接到创建Tensor的函数并对其历史进行编码。

  1. AlexNet模型示例
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
class AlexNet(nn.Module):
def __init__(self):
super(AlexNet, self).__init__()
self.conv = nn.Sequential(
nn.Conv2d(1, 96, 11, 4), # in_channels, out_channels, kernel_size, stride, padding
nn.ReLU(),
nn.MaxPool2d(3, 2), # kernel_size, stride
# 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
nn.Conv2d(96, 256, 5, 1, 2),
nn.ReLU(),
nn.MaxPool2d(3, 2),
# 连续3个卷积层,且使用更小的卷积窗口。除了最后的卷积层外,进一步增大了输出通道数。
# 前两个卷积层后不使用池化层来减小输入的高和宽
nn.Conv2d(256, 384, 3, 1, 1),
nn.ReLU(),
nn.Conv2d(384, 384, 3, 1, 1),
nn.ReLU(),
nn.Conv2d(384, 256, 3, 1, 1),
nn.ReLU(),
nn.MaxPool2d(3, 2)
)
# 这里全连接层的输出个数比LeNet中的大数倍。使用丢弃层来缓解过拟合
self.fc = nn.Sequential(
nn.Linear(256*5*5, 4096),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(4096, 4096),
nn.ReLU(),
nn.Dropout(0.5),
# 输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
nn.Linear(4096, 10),
)

def forward(self, img):
feature = self.conv(img)
output = self.fc(feature.view(img.shape[0], -1))
return output

net = AlexNet()
print(net)
AlexNet(
  (conv): Sequential(
    (0): Conv2d(1, 96, kernel_size=(11, 11), stride=(4, 4))
    (1): ReLU()
    (2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
    (3): Conv2d(96, 256, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
    (4): ReLU()
    (5): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
    (6): Conv2d(256, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (7): ReLU()
    (8): Conv2d(384, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (9): ReLU()
    (10): Conv2d(384, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (11): ReLU()
    (12): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (fc): Sequential(
    (0): Linear(in_features=6400, out_features=4096, bias=True)
    (1): ReLU()
    (2): Dropout(p=0.5)
    (3): Linear(in_features=4096, out_features=4096, bias=True)
    (4): ReLU()
    (5): Dropout(p=0.5)
    (6): Linear(in_features=4096, out_features=10, bias=True)
  )
)

四、 模型初始化

  1. torch.nn.init使用
    通常使用isinstance来进行判断模块
    1
    2
    3
    4
    5
    6
    7
    8
    import torch
    import torch.nn as nn

    conv = nn.Conv2d(1,3,3)
    linear = nn.Linear(10,1)

    isinstance(conv,nn.Conv2d)
    isinstance(linear,nn.Conv2d)
    查看不同初始化参数
    1
    2
    3
    4
    # 查看随机初始化的conv参数
    conv.weight.data
    # 查看linear的参数
    linear.weight.data
    对不同类型层进行初始化
    1
    2
    3
    4
    5
    6
    # 对conv进行kaiming初始化
    torch.nn.init.kaiming_normal_(conv.weight.data)
    conv.weight.data
    # 对linear进行常数初始化
    torch.nn.init.constant_(linear.weight.data,0.3)
    linear.weight.data
  2. 初始化函数的封装
    人们常常将各种初始化方法定义为一个initialize_weights()的函数并在模型初始后进行使用。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    def initialize_weights(self):
    for m in self.modules():
    # 判断是否属于Conv2d
    if isinstance(m, nn.Conv2d):
    torch.nn.init.xavier_normal_(m.weight.data)
    # 判断是否有偏置
    if m.bias is not None:
    torch.nn.init.constant_(m.bias.data,0.3)
    elif isinstance(m, nn.Linear):
    torch.nn.init.normal_(m.weight.data, 0.1)
    if m.bias is not None:
    torch.nn.init.zeros_(m.bias.data)
    elif isinstance(m, nn.BatchNorm2d):
    m.weight.data.fill_(1)
    m.bias.data.zeros_()
    这段代码流程是遍历当前模型的每一层,然后判断各层属于什么类型,然后根据不同类型层,设定不同的权值初始化方法。我们可以通过下面的例程进行一个简短的演示:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # 模型的定义
    class MLP(nn.Module):
    # 声明带有模型参数的层,这里声明了两个全连接层
    def __init__(self, **kwargs):
    # 调用MLP父类Block的构造函数来进行必要的初始化。这样在构造实例时还可以指定其他函数
    super(MLP, self).__init__(**kwargs)
    self.hidden = nn.Conv2d(1,1,3)
    self.act = nn.ReLU()
    self.output = nn.Linear(10,1)

    # 定义模型的前向计算,即如何根据输入x计算返回所需要的模型输出
    def forward(self, x):
    o = self.act(self.hidden(x))
    return self.output(o)

    mlp = MLP()
    print(list(mlp.parameters()))
    print("-------初始化-------")

    initialize_weights(mlp)
    print(list(mlp.parameters()))
     [Parameter containing:
     tensor([[[[ 0.2103, -0.1679,  0.1757],
               [-0.0647, -0.0136, -0.0410],
               [ 0.1371, -0.1738, -0.0850]]]], requires_grad=True), Parameter containing:
     tensor([0.2507], requires_grad=True), Parameter containing:
     tensor([[ 0.2790, -0.1247,  0.2762,  0.1149, -0.2121, -0.3022, -0.1859,  0.2983,
             -0.0757, -0.2868]], requires_grad=True), Parameter containing:
     tensor([-0.0905], requires_grad=True)]
     "-------初始化-------"
     [Parameter containing:
     tensor([[[[-0.3196, -0.0204, -0.5784],
               [ 0.2660,  0.2242, -0.4198],
               [-0.0952,  0.6033, -0.8108]]]], requires_grad=True),
     Parameter containing:
     tensor([0.3000], requires_grad=True),
     Parameter containing:
     tensor([[ 0.7542,  0.5796,  2.2963, -0.1814, -0.9627,  1.9044,  0.4763,  1.2077,
               0.8583,  1.9494]], requires_grad=True),
     Parameter containing:
     tensor([0.], requires_grad=True)]
    

五、 损失函数

这里使用torch.nn模块自带的CrossEntropy损失,PyTorch会自动把整数型的label转为one-hot型,用于计算CE loss,这里需要确保label是从0开始的,同时模型不加softmax层(使用logits计算),这也说明了PyTorch训练中各个部分不是独立的,需要通盘考虑。

1
criterion = nn.CrossEntropyLoss()

1. 二分类交叉熵损失函数

1
torch.nn.BCELoss(weight=None, size_average=None, reduce=None, reduction='mean')

功能:计算二分类任务时的交叉熵(Cross Entropy)函数。在二分类中,label是{0,1}。对于进入交叉熵函数的input为概率分布的形式。一般来说,input为sigmoid激活层的输出,或者softmax的输出。
主要参数
weight:每个类别的loss设置权值
size_average:数据为bool,为True时,返回的loss为平均值;为False时,返回的各样本的loss之和。
reduce:数据类型为bool,为True时,loss的返回是标量。
计算公式如下:

1
2
3
4
5
6
7
m = nn.Sigmoid()
loss = nn.BCELoss()
input = torch.randn(3, requires_grad=True)
target = torch.empty(3).random_(2)
output = loss(m(input), target)
output.backward()
print('BCELoss损失函数的计算结果为',output)
BCELoss损失函数的计算结果为 tensor(0.5732, grad_fn=<BinaryCrossEntropyBackward>)

2. 交叉熵损失函数

1
torch.nn.CrossEntropyLoss(weight=None, size_average=None, ignore_index=-100, reduce=None, reduction='mean')

功能:计算交叉熵函数
主要参数
weight:每个类别的loss设置权值。
size_average:数据为bool,为True时,返回的loss为平均值;为False时,返回的各样本的loss之和。
ignore_index:忽略某个类的损失函数。
reduce:数据类型为bool,为True时,loss的返回是标量。
计算公式如下:
$
\operatorname{loss}(x, \text { class })=-\log \left(\frac{\exp (x[\text { class }])}{\sum_{j} \exp (x[j])}\right)=-x[\text { class }]+\log \left(\sum_{j} \exp (x[j])\right)
$

1
2
3
4
5
6
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()
print(output)
tensor(2.0115, grad_fn=<NllLossBackward>)

3. L1损失函数

1
torch.nn.L1Loss(size_average=None, reduce=None, reduction='mean')

功能: 计算输出y和真实标签target之间的差值的绝对值。reduction参数决定了计算模式。有三种计算模式可选:none:逐个元素计算。sum:所有元素求和,返回标量。mean:加权平均,返回标量。如果选择none,那么返回的结果是和输入元素相同尺寸的。默认计算方式是求平均。
计算公式如下:
$
L_{n} = |x_{n}-y_{n}|
$

1
2
3
4
5
6
loss = nn.L1Loss()
input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)
output = loss(input, target)
output.backward()
print('L1损失函数的计算结果为',output)
L1损失函数的计算结果为 tensor(1.5729, grad_fn=<L1LossBackward>)

4. MSE损失函数

1
torch.nn.MSELoss(size_average=None, reduce=None, reduction='mean')

功能: 计算输出y和真实标签target之差的平方。

L1Loss一样,MSELoss损失函数中,reduction参数决定了计算模式。有三种计算模式可选:none:逐个元素计算。sum:所有元素求和,返回标量。默认计算方式是求平均。

计算公式如下:

$
l_{n}=\left(x_{n}-y_{n}\right)^{2}
$

1
2
3
4
5
6
loss = nn.MSELoss()
input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)
output = loss(input, target)
output.backward()
print('MSE损失函数的计算结果为',output)
MSE损失函数的计算结果为 tensor(1.6968, grad_fn=<MseLossBackward>)

5. 平滑L1 (Smooth L1)损失函数

1
torch.nn.SmoothL1Loss(size_average=None, reduce=None, reduction='mean', beta=1.0)

功能: L1的平滑输出,其功能是减轻离群点带来的影响

reduction参数决定了计算模式。有三种计算模式可选:none:逐个元素计算。sum:所有元素求和,返回标量。默认计算方式是求平均。

提醒: 之后的损失函数中,关于reduction 这个参数依旧会存在。所以,之后就不再单独说明。

计算公式如下:
$
\operatorname{loss}(x, y)=\frac{1}{n} \sum_{i=1}^{n} z_{i}
$
其中,

1
2
3
4
5
6
loss = nn.SmoothL1Loss()
input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)
output = loss(input, target)
output.backward()
print('SmoothL1Loss损失函数的计算结果为',output)
SmoothL1Loss损失函数的计算结果为 tensor(0.7808, grad_fn=<SmoothL1LossBackward>)

平滑L1与L1的对比

这里我们通过可视化两种损失函数曲线来对比平滑L1和L1两种损失函数的区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
inputs = torch.linspace(-10, 10, steps=5000)
target = torch.zeros_like(inputs)

loss_f_smooth = nn.SmoothL1Loss(reduction='none')
loss_smooth = loss_f_smooth(inputs, target)
loss_f_l1 = nn.L1Loss(reduction='none')
loss_l1 = loss_f_l1(inputs,target)

plt.plot(inputs.numpy(), loss_smooth.numpy(), label='Smooth L1 Loss')
plt.plot(inputs.numpy(), loss_l1, label='L1 loss')
plt.xlabel('x_i - y_i')
plt.ylabel('loss value')
plt.legend()
plt.grid()
plt.show()


可以看出,对于smoothL1来说,在 0 这个尖端处,过渡更为平滑。

6. 目标泊松分布的负对数似然损失

1
torch.nn.PoissonNLLLoss(log_input=True, full=False, size_average=None, eps=1e-08, reduce=None, reduction='mean')

功能: 泊松分布的负对数似然损失函数
主要参数:
log_input:输入是否为对数形式,决定计算公式。
full:计算所有 loss,默认为 False。
eps:修正项,避免 input 为 0 时,log(input) 为 nan 的情况。
数学公式:

  • 当参数log_input=True
    $
    \operatorname{loss}\left(x_{n}, y_{n}\right)=e^{x_{n}}-x_{n} \cdot y_{n}
    $
  • 当参数log_input=False
    $
    \operatorname{loss}\left(x_{n}, y_{n}\right)=x_{n}-y_{n} \cdot \log \left(x_{n}+\text { eps }\right)
    $
1
2
3
4
5
6
loss = nn.PoissonNLLLoss()
log_input = torch.randn(5, 2, requires_grad=True)
target = torch.randn(5, 2)
output = loss(log_input, target)
output.backward()
print('PoissonNLLLoss损失函数的计算结果为',output)
PoissonNLLLoss损失函数的计算结果为 tensor(0.7358, grad_fn=<MeanBackward0>)

7. KL散度

1
torch.nn.KLDivLoss(size_average=None, reduce=None, reduction='mean', log_target=False)

功能: 计算KL散度,也就是计算相对熵。用于连续分布的距离度量,并且对离散采用的连续输出空间分布进行回归通常很有用。
主要参数:
reduction:计算模式,可为 none/sum/mean/batchmean

none:逐个元素计算。
sum:所有元素求和,返回标量。
mean:加权平均,返回标量。
batchmean:batchsize 维度求平均值。

计算公式:

1
2
3
4
5
inputs = torch.tensor([[0.5, 0.3, 0.2], [0.2, 0.3, 0.5]])
target = torch.tensor([[0.9, 0.05, 0.05], [0.1, 0.7, 0.2]], dtype=torch.float)
loss = nn.KLDivLoss()
output = loss(inputs,target)
print('KLDivLoss损失函数的计算结果为',output)
KLDivLoss损失函数的计算结果为 tensor(-0.3335)

8. MarginRankingLoss

1
torch.nn.MarginRankingLoss(margin=0.0, size_average=None, reduce=None, reduction='mean')

功能: 计算两个向量之间的相似度,用于排序任务。该方法用于计算两组数据之间的差异。
主要参数:
margin:边界值,$x_{1}$ 与$x_{2}$ 之间的差异值。
reduction:计算模式,可为 none/sum/mean。
计算公式:
$
\operatorname{loss}(x 1, x 2, y)=\max (0,-y *(x 1-x 2)+\operatorname{margin})
$

1
2
3
4
5
6
7
loss = nn.MarginRankingLoss()
input1 = torch.randn(3, requires_grad=True)
input2 = torch.randn(3, requires_grad=True)
target = torch.randn(3).sign()
output = loss(input1, input2, target)
output.backward()
print('MarginRankingLoss损失函数的计算结果为',output)
MarginRankingLoss损失函数的计算结果为 tensor(0.7740, grad_fn=<MeanBackward0>)

9. 多标签边界损失函数

1
torch.nn.MultiLabelMarginLoss(size_average=None, reduce=None, reduction='mean')

功能: 对于多标签分类问题计算损失函数。
主要参数:
reduction:计算模式,可为 none/sum/mean。
计算公式:
$
\operatorname{loss}(x, y)=\sum_{i j} \frac{\max (0,1-x[y[j]]-x[i])}{x \cdot \operatorname{size}(0)}
$
$
\begin{array}{l}
\text { 其中, } i=0, \ldots, x \cdot \operatorname{size}(0), j=0, \ldots, y \cdot \operatorname{size}(0), \text { 对于所有的 } i \text { 和 } j \text {, 都有 } y[j] \geq 0 \text { 并且 }\
i \neq y[j]
\end{array}
$

1
2
3
4
5
6
loss = nn.MultiLabelMarginLoss()
x = torch.FloatTensor([[0.9, 0.2, 0.4, 0.8]])
# for target y, only consider labels 3 and 0, not after label -1
y = torch.LongTensor([[3, 0, -1, 1]])# 真实的分类是,第3类和第0类
output = loss(x, y)
print('MultiLabelMarginLoss损失函数的计算结果为',output)
MultiLabelMarginLoss损失函数的计算结果为 tensor(0.4500)

10. 二分类损失函数

1
torch.nn.SoftMarginLoss(size_average=None, reduce=None, reduction='mean')torch.nn.(size_average=None, reduce=None, reduction='mean')

功能: 计算二分类的 logistic 损失。
主要参数:
reduction:计算模式,可为 none/sum/mean。
计算公式:
$
\operatorname{loss}(x, y)=\sum_{i} \frac{\log (1+\exp (-y[i] \cdot x[i]))}{x \cdot \operatorname{nelement}()}
$
$

\text { 其中, } x . \text { nelement() 为输入 } x \text { 中的样本个数。注意这里 } y \text { 也有 } 1 \text { 和 }-1 \text { 两种模式。 }

$

1
2
3
4
5
6
inputs = torch.tensor([[0.3, 0.7], [0.5, 0.5]])  # 两个样本,两个神经元
target = torch.tensor([[-1, 1], [1, -1]], dtype=torch.float) # 该 loss 为逐个神经元计算,需要为每个神经元单独设置标签

loss_f = nn.SoftMarginLoss()
output = loss_f(inputs, target)
print('SoftMarginLoss损失函数的计算结果为',output)
SoftMarginLoss损失函数的计算结果为 tensor(0.6764)

11. 多分类的折页损失

1
torch.nn.MultiMarginLoss(p=1, margin=1.0, weight=None, size_average=None, reduce=None, reduction='mean')

功能: 计算多分类的折页损失
主要参数:
reduction:计算模式,可为 none/sum/mean。
p:可选 1 或 2。
weight:各类别的 loss 设置权值。
margin:边界值
计算公式:
$
\operatorname{loss}(x, y)=\frac{\sum_{i} \max (0, \operatorname{margin}-x[y]+x[i])^{p}}{x \cdot \operatorname{size}(0)}
$
$
\begin{array}{l}
\text { 其中, } x \in{0, \ldots, x \cdot \operatorname{size}(0)-1}, y \in{0, \ldots, y \cdot \operatorname{size}(0)-1} \text {, 并且对于所有的 } i \text { 和 } j \text {, }\
\text { 都有 } 0 \leq y[j] \leq x \cdot \operatorname{size}(0)-1, \text { 以及 } i \neq y[j] \text { 。 }
\end{array}
$

1
2
3
4
5
6
inputs = torch.tensor([[0.3, 0.7], [0.5, 0.5]]) 
target = torch.tensor([0, 1], dtype=torch.long)

loss_f = nn.MultiMarginLoss()
output = loss_f(inputs, target)
print('MultiMarginLoss损失函数的计算结果为',output)
MultiMarginLoss损失函数的计算结果为 tensor(0.6000)

12. 三元组损失

1
torch.nn.TripletMarginLoss(margin=1.0, p=2.0, eps=1e-06, swap=False, size_average=None, reduce=None, reduction='mean')

功能: 计算三元组损失。
三元组: 这是一种数据的存储或者使用格式。<实体1,关系,实体2>。在项目中,也可以表示为< anchor, positive examples , negative examples>
在这个损失函数中,我们希望去anchor的距离更接近positive examples,而远离negative examples
主要参数:
reduction:计算模式,可为 none/sum/mean。
p:可选 1 或 2。
margin:边界值
计算公式:

1
2
3
4
5
6
7
triplet_loss = nn.TripletMarginLoss(margin=1.0, p=2)
anchor = torch.randn(100, 128, requires_grad=True)
positive = torch.randn(100, 128, requires_grad=True)
negative = torch.randn(100, 128, requires_grad=True)
output = triplet_loss(anchor, positive, negative)
output.backward()
print('TripletMarginLoss损失函数的计算结果为',output)
TripletMarginLoss损失函数的计算结果为 tensor(1.1667, grad_fn=<MeanBackward0>)

13. HingEmbeddingLoss

1
torch.nn.HingeEmbeddingLoss(margin=1.0, size_average=None, reduce=None, reduction='mean')

功能: 对输出的embedding结果做Hing损失计算
主要参数:
reduction:计算模式,可为 none/sum/mean。
margin:边界值
计算公式:

注意事项: 输入x应为两个输入之差的绝对值。
可以这样理解,让个输出的是正例yn=1,那么loss就是x,如果输出的是负例y=-1,那么输出的loss就是要做一个比较。

1
2
3
4
5
loss_f = nn.HingeEmbeddingLoss()
inputs = torch.tensor([[1., 0.8, 0.5]])
target = torch.tensor([[1, 1, -1]])
output = loss_f(inputs,target)
print('HingEmbeddingLoss损失函数的计算结果为',output)
HingEmbeddingLoss损失函数的计算结果为 tensor(0.7667)

14. 余弦相似度

1
torch.nn.CosineEmbeddingLoss(margin=0.0, size_average=None, reduce=None, reduction='mean')

功能: 对两个向量做余弦相似度
主要参数:
reduction:计算模式,可为 none/sum/mean。
margin:可取值[-1,1] ,推荐为[0,0.5] 。
计算公式:

这个损失函数应该是最广为人知的。对于两个向量,做余弦相似度。将余弦相似度作为一个距离的计算方式,如果两个向量的距离近,则损失函数值小,反之亦然。

1
2
3
4
5
6
loss_f = nn.CosineEmbeddingLoss()
inputs_1 = torch.tensor([[0.3, 0.5, 0.7], [0.3, 0.5, 0.7]])
inputs_2 = torch.tensor([[0.1, 0.3, 0.5], [0.1, 0.3, 0.5]])
target = torch.tensor([[1, -1]], dtype=torch.float)
output = loss_f(inputs_1,inputs_2,target)
print('CosineEmbeddingLoss损失函数的计算结果为',output)
CosineEmbeddingLoss损失函数的计算结果为 tensor(0.5000)

15.CTC损失函数

1
torch.nn.CTCLoss(blank=0, reduction='mean', zero_infinity=False)

功能: 用于解决时序类数据的分类
计算连续时间序列和目标序列之间的损失。CTCLoss对输入和目标的可能排列的概率进行求和,产生一个损失值,这个损失值对每个输入节点来说是可分的。输入与目标的对齐方式被假定为 “多对一”,这就限制了目标序列的长度,使其必须是≤输入长度。
主要参数:
reduction:计算模式,可为 none/sum/mean。
blank:blank label。
zero_infinity:无穷大的值或梯度值为

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
# Target are to be padded
T = 50 # Input sequence length
C = 20 # Number of classes (including blank)
N = 16 # Batch size
S = 30 # Target sequence length of longest target in batch (padding length)
S_min = 10 # Minimum target length, for demonstration purposes

# Initialize random batch of input vectors, for *size = (T,N,C)
input = torch.randn(T, N, C).log_softmax(2).detach().requires_grad_()

# Initialize random batch of targets (0 = blank, 1:C = classes)
target = torch.randint(low=1, high=C, size=(N, S), dtype=torch.long)

input_lengths = torch.full(size=(N,), fill_value=T, dtype=torch.long)
target_lengths = torch.randint(low=S_min, high=S, size=(N,), dtype=torch.long)
ctc_loss = nn.CTCLoss()
loss = ctc_loss(input, target, input_lengths, target_lengths)
loss.backward()


# Target are to be un-padded
T = 50 # Input sequence length
C = 20 # Number of classes (including blank)
N = 16 # Batch size

# Initialize random batch of input vectors, for *size = (T,N,C)
input = torch.randn(T, N, C).log_softmax(2).detach().requires_grad_()
input_lengths = torch.full(size=(N,), fill_value=T, dtype=torch.long)

# Initialize random batch of targets (0 = blank, 1:C = classes)
target_lengths = torch.randint(low=1, high=T, size=(N,), dtype=torch.long)
target = torch.randint(low=1, high=C, size=(sum(target_lengths),), dtype=torch.long)
ctc_loss = nn.CTCLoss()
loss = ctc_loss(input, target, input_lengths, target_lengths)
loss.backward()

print('CTCLoss损失函数的计算结果为',loss)
CTCLoss损失函数的计算结果为 tensor(16.0885, grad_fn=<MeanBackward0>)

六、 优化器

这里使用Adam优化器

1
optimizer = optim.Adam(model.parameters(), lr=0.001)

Pytorch很人性化的给我们提供了一个优化器的库torch.optim,在这里面提供了十种优化器。

  • torch.optim.ASGD
  • torch.optim.Adadelta
  • torch.optim.Adagrad
  • torch.optim.Adam
  • torch.optim.AdamW
  • torch.optim.Adamax
  • torch.optim.LBFGS
  • torch.optim.RMSprop
  • torch.optim.Rprop
  • torch.optim.SGD
  • torch.optim.SparseAdam

而以上这些优化算法均继承于Optimizer,下面我们先来看下所有优化器的基类Optimizer。定义如下:

1
2
3
4
5
class Optimizer(object):
def __init__(self, params, defaults):
self.defaults = defaults
self.state = defaultdict(dict)
self.param_groups = []

Optimizer有三个属性:

  • defaults:存储的是优化器的超参数,例子如下:
1
{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}
  • state:参数的缓存,例子如下:
1
2
3
defaultdict(<class 'dict'>, {tensor([[ 0.3864, -0.0131],
[-0.1911, -0.4511]], requires_grad=True): {'momentum_buffer': tensor([[0.0052, 0.0052],
[0.0052, 0.0052]])}})
  • param_groups:管理的参数组,是一个list,其中每个元素是一个字典,顺序是params,lr,momentum,dampening,weight_decay,nesterov,例子如下:
1
[{'params': [tensor([[-0.1022, -1.6890],[-1.5116, -1.7846]], requires_grad=True)], 'lr': 1, 'momentum': 0, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}]

Optimizer还有以下的方法:

  • zero_grad():清空所管理参数的梯度,PyTorch的特性是张量的梯度不自动清零,因此每次反向传播后都需要清空梯度。
1
2
3
4
5
6
7
8
9
10
11
12
def zero_grad(self, set_to_none: bool = False):
for group in self.param_groups:
for p in group['params']:
if p.grad is not None: #梯度不为空
if set_to_none:
p.grad = None
else:
if p.grad.grad_fn is not None:
p.grad.detach_()
else:
p.grad.requires_grad_(False)
p.grad.zero_()# 梯度设置为0
  • step():执行一步梯度更新,参数更新
1
2
def step(self, closure): 
raise NotImplementedError
  • add_param_group():添加参数组
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
def add_param_group(self, param_group):
assert isinstance(param_group, dict), "param group must be a dict"
# 检查类型是否为tensor
params = param_group['params']
if isinstance(params, torch.Tensor):
param_group['params'] = [params]
elif isinstance(params, set):
raise TypeError('optimizer parameters need to be organized in ordered collections, but '
'the ordering of tensors in sets will change between runs. Please use a list instead.')
else:
param_group['params'] = list(params)
for param in param_group['params']:
if not isinstance(param, torch.Tensor):
raise TypeError("optimizer can only optimize Tensors, "
"but one of the params is " + torch.typename(param))
if not param.is_leaf:
raise ValueError("can't optimize a non-leaf Tensor")

for name, default in self.defaults.items():
if default is required and name not in param_group:
raise ValueError("parameter group didn't specify a value of required optimization parameter " +
name)
else:
param_group.setdefault(name, default)

params = param_group['params']
if len(params) != len(set(params)):
warnings.warn("optimizer contains a parameter group with duplicate parameters; "
"in future, this will cause an error; "
"see github.com/pytorch/pytorch/issues/40967 for more information", stacklevel=3)
# 上面好像都在进行一些类的检测,报Warning和Error
param_set = set()
for group in self.param_groups:
param_set.update(set(group['params']))

if not param_set.isdisjoint(set(param_group['params'])):
raise ValueError("some parameters appear in more than one parameter group")
# 添加参数
self.param_groups.append(param_group)
  • load_state_dict() :加载状态参数字典,可以用来进行模型的断点续训练,继续上次的参数进行训练
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
def load_state_dict(self, state_dict):
r"""Loads the optimizer state.

Arguments:
state_dict (dict): optimizer state. Should be an object returned
from a call to :meth:`state_dict`.
"""
# deepcopy, to be consistent with module API
state_dict = deepcopy(state_dict)
# Validate the state_dict
groups = self.param_groups
saved_groups = state_dict['param_groups']

if len(groups) != len(saved_groups):
raise ValueError("loaded state dict has a different number of "
"parameter groups")
param_lens = (len(g['params']) for g in groups)
saved_lens = (len(g['params']) for g in saved_groups)
if any(p_len != s_len for p_len, s_len in zip(param_lens, saved_lens)):
raise ValueError("loaded state dict contains a parameter group "
"that doesn't match the size of optimizer's group")

# Update the state
id_map = {old_id: p for old_id, p in
zip(chain.from_iterable((g['params'] for g in saved_groups)),
chain.from_iterable((g['params'] for g in groups)))}

def cast(param, value):
r"""Make a deep copy of value, casting all tensors to device of param."""
.....

# Copy state assigned to params (and cast tensors to appropriate types).
# State that is not assigned to params is copied as is (needed for
# backward compatibility).
state = defaultdict(dict)
for k, v in state_dict['state'].items():
if k in id_map:
param = id_map[k]
state[param] = cast(param, v)
else:
state[k] = v

# Update parameter groups, setting their 'params' value
def update_group(group, new_group):
...
param_groups = [
update_group(g, ng) for g, ng in zip(groups, saved_groups)]
self.__setstate__({'state': state, 'param_groups': param_groups})
  • state_dict():获取优化器当前状态信息字典
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def state_dict(self):
r"""Returns the state of the optimizer as a :class:`dict`.

It contains two entries:

* state - a dict holding current optimization state. Its content
differs between optimizer classes.
* param_groups - a dict containing all parameter groups
"""
# Save order indices instead of Tensors
param_mappings = {}
start_index = 0

def pack_group(group):
......
param_groups = [pack_group(g) for g in self.param_groups]
# Remap state to use order indices as keys
packed_state = {(param_mappings[id(k)] if isinstance(k, torch.Tensor) else k): v
for k, v in self.state.items()}
return {
'state': packed_state,
'param_groups': param_groups,
}

实际操作

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
import os
import torch

# 设置权重,服从正态分布 --> 2 x 2
weight = torch.randn((2, 2), requires_grad=True)
# 设置梯度为全1矩阵 --> 2 x 2
weight.grad = torch.ones((2, 2))
# 输出现有的weight和data
print("The data of weight before step:\n{}".format(weight.data))
print("The grad of weight before step:\n{}".format(weight.grad))
# 实例化优化器
optimizer = torch.optim.SGD([weight], lr=0.1, momentum=0.9)
# 进行一步操作
optimizer.step()
# 查看进行一步后的值,梯度
print("The data of weight after step:\n{}".format(weight.data))
print("The grad of weight after step:\n{}".format(weight.grad))
# 权重清零
optimizer.zero_grad()
# 检验权重是否为0
print("The grad of weight after optimizer.zero_grad():\n{}".format(weight.grad))
# 输出参数
print("optimizer.params_group is \n{}".format(optimizer.param_groups))
# 查看参数位置,optimizer和weight的位置一样,我觉得这里可以参考Python是基于值管理
print("weight in optimizer:{}\nweight in weight:{}\n".format(id(optimizer.param_groups[0]['params'][0]), id(weight)))
# 添加参数:weight2
weight2 = torch.randn((3, 3), requires_grad=True)
optimizer.add_param_group({"params": weight2, 'lr': 0.0001, 'nesterov': True})
# 查看现有的参数
print("optimizer.param_groups is\n{}".format(optimizer.param_groups))
# 查看当前状态信息
opt_state_dict = optimizer.state_dict()
print("state_dict before step:\n", opt_state_dict)
# 进行5次step操作
for _ in range(50):
optimizer.step()
# 输出现有状态信息
print("state_dict after step:\n", optimizer.state_dict())
# 保存参数信息
torch.save(optimizer.state_dict(),os.path.join(r"D:\pythonProject\Attention_Unet", "optimizer_state_dict.pkl"))
print("----------done-----------")
# 加载参数信息
state_dict = torch.load(r"D:\pythonProject\Attention_Unet\optimizer_state_dict.pkl") # 需要修改为你自己的路径
optimizer.load_state_dict(state_dict)
print("load state_dict successfully\n{}".format(state_dict))
# 输出最后属性信息
print("\n{}".format(optimizer.defaults))
print("\n{}".format(optimizer.state))
print("\n{}".format(optimizer.param_groups))

输出结果

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
# 进行更新前的数据,梯度
The data of weight before step:
tensor([[-0.3077, -0.1808],
[-0.7462, -1.5556]])
The grad of weight before step:
tensor([[1., 1.],
[1., 1.]])
# 进行更新后的数据,梯度
The data of weight after step:
tensor([[-0.4077, -0.2808],
[-0.8462, -1.6556]])
The grad of weight after step:
tensor([[1., 1.],
[1., 1.]])
# 进行梯度清零的梯度
The grad of weight after optimizer.zero_grad():
tensor([[0., 0.],
[0., 0.]])
# 输出信息
optimizer.params_group is
[{'params': [tensor([[-0.4077, -0.2808],
[-0.8462, -1.6556]], requires_grad=True)], 'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}]

# 证明了优化器的和weight的储存是在一个地方,Python基于值管理
weight in optimizer:1841923407424
weight in weight:1841923407424

# 输出参数
optimizer.param_groups is
[{'params': [tensor([[-0.4077, -0.2808],
[-0.8462, -1.6556]], requires_grad=True)], 'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}, {'params': [tensor([[ 0.4539, -2.1901, -0.6662],
[ 0.6630, -1.5178, -0.8708],
[-2.0222, 1.4573, 0.8657]], requires_grad=True)], 'lr': 0.0001, 'nesterov': True, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0}]

# 进行更新前的参数查看,用state_dict
state_dict before step:
{'state': {0: {'momentum_buffer': tensor([[1., 1.],
[1., 1.]])}}, 'param_groups': [{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False, 'params': [0]}, {'lr': 0.0001, 'nesterov': True, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'params': [1]}]}
# 进行更新后的参数查看,用state_dict
state_dict after step:
{'state': {0: {'momentum_buffer': tensor([[0.0052, 0.0052],
[0.0052, 0.0052]])}}, 'param_groups': [{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False, 'params': [0]}, {'lr': 0.0001, 'nesterov': True, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'params': [1]}]}

# 存储信息完毕
----------done-----------
# 加载参数信息成功
load state_dict successfully
# 加载参数信息
{'state': {0: {'momentum_buffer': tensor([[0.0052, 0.0052],
[0.0052, 0.0052]])}}, 'param_groups': [{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False, 'params': [0]}, {'lr': 0.0001, 'nesterov': True, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'params': [1]}]}

# defaults的属性输出
{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}

# state属性输出
defaultdict(<class 'dict'>, {tensor([[-1.3031, -1.1761],
[-1.7415, -2.5510]], requires_grad=True): {'momentum_buffer': tensor([[0.0052, 0.0052],
[0.0052, 0.0052]])}})

# param_groups属性输出
[{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False, 'params': [tensor([[-1.3031, -1.1761],
[-1.7415, -2.5510]], requires_grad=True)]}, {'lr': 0.0001, 'nesterov': True, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'params': [tensor([[ 0.4539, -2.1901, -0.6662],
[ 0.6630, -1.5178, -0.8708],
[-2.0222, 1.4573, 0.8657]], requires_grad=True)]}]

注意:

  1. 每个优化器都是一个类,我们一定要进行实例化才能使用,比如下方实现:

    1
    2
    3
    4
    5
    class Net(nn.Moddule):
    ···
    net = Net()
    optim = torch.optim.SGD(net.parameters(),lr=lr)
    optim.step()
  2. optimizer在一个神经网络的epoch中需要实现下面两个步骤:

    1. 梯度置零
    2. 梯度更新
      1
      2
      3
      4
      5
      6
      7
      optimizer = torch.optim.SGD(net.parameters(), lr=1e-5)
      for epoch in range(EPOCH):
      ...
      optimizer.zero_grad() #梯度置零
      loss = ... #计算loss
      loss.backward() #BP反向传播
      optimizer.step() #梯度更新
  3. 给网络不同的层赋予不同的优化器参数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    from torch import optim
    from torchvision.models import resnet18

    net = resnet18()

    optimizer = optim.SGD([
    {'params':net.fc.parameters()},#fc的lr使用默认的1e-5
    {'params':net.layer4[0].conv1.parameters(),'lr':1e-2}],lr=1e-5)

    # 可以使用param_groups查看属性

实验

为了更好的了解优化器,对PyTorch中的优化器进行了一个小测试

数据生成

1
2
3
4
a = torch.linspace(-1, 1, 1000)
# 升维操作
x = torch.unsqueeze(a, dim=1)
y = x.pow(2) + 0.1 * torch.normal(torch.zeros(x.size()))

数据分布曲线

网络结构

1
2
3
4
5
6
7
8
9
10
11
12
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.hidden = nn.Linear(1, 20)
self.predict = nn.Linear(20, 1)

def forward(self, x):
x = self.hidden(x)
x = F.relu(x)
x = self.predict(x)
return x

下面这部分是测试图,纵坐标代表Loss,横坐标代表的是Step:

在上面的图片上,曲线下降的趋势和对应的steps代表了在这轮数据,模型下的收敛速度

注意:

优化器的选择是需要根据模型进行改变的,不存在绝对的好坏之分,我们需要多进行一些测试。
后续会添加SparseAdam,LBFGS这两个优化器的可视化结果

七、 训练与评估

关注两者的主要区别:
模型状态设置
是否需要初始化优化器
是否需要将loss传回到网络
是否需要每步更新optimizer
此外,对于测试或验证过程,可以计算分类准确率

1
2
3
4
5
6
7
8
9
10
11
12
13
def train(epoch):
model.train()
train_loss = 0
for data, label in train_loader:
data, label = data.cuda(), label.cuda()
optimizer.zero_grad()
output = model(data) # 前向传播
loss = criterion(output, label)
loss.backward() # 反向传播
optimizer.step() # 优化器更新权重
train_loss += loss.item()*data.size(0)
train_loss = train_loss/len(train_loader.dataset)
print('Epoch: {} \tTraining Loss: {:.6f}'.format(epoch, train_loss))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def val(epoch):       
model.eval() # 测试和训练不一样
val_loss = 0
gt_labels = []
pred_labels = []
with torch.no_grad(): # 不计算梯度
for data, label in test_loader:
data, label = data.cuda(), label.cuda()
output = model(data)
preds = torch.argmax(output, 1) # 得到预测的结果是哪一类
gt_labels.append(label.cpu().data.numpy()) # 拼接起来
pred_labels.append(preds.cpu().data.numpy())
loss = criterion(output, label)
val_loss += loss.item()*data.size(0)
val_loss = val_loss/len(test_loader.dataset)
gt_labels, pred_labels = np.concatenate(gt_labels), np.concatenate(pred_labels)
acc = np.sum(gt_labels==pred_labels)/len(pred_labels) # pre和label相等的次数除上总数
print('Epoch: {} \tValidation Loss: {:.6f}, Accuracy: {:6f}'.format(epoch, val_loss, acc))
1
2
3
for epoch in range(1, epochs+1):
train(epoch)
val(epoch)

八、 可视化

见后续专题

九、 保存模型

训练完成后,可以使用torch.save保存模型参数或者整个模型,也可以在训练过程中保存模型:

1
2
save_path = "./FahionModel.pkl"
torch.save(model, save_path)

参考资料

深入浅出PyTorch

pytorch - 基础知识

由于张量是对数据的描述,在神经网络中通常将数据以张量的形式表示,如零维张量表示标量,一维张量表示向量,二维表示矩阵,三维张量表示图像,四维表示视频,这章首先介绍张量,然后介绍它的运算,再是核心包autograd自动微分。

张量

张量的简介

pytorch的torch.Tensor和Numpy的多维数组非常相似,由于tensor提供GPU的自动求梯度和计算,它更适合深度学习。

  1. 用dtype指定类型创建tensor,detype有tensor.float/long等

    1
    torch.tensor(1,dtype=torch.int8)
  2. 使用指定类型随机初始化

    1
    torch.IntTensor(2,3)

    tensor和numpy array 互相转化,torch.tensor创建的张量是不共享内存的,但torch.from_numpy()和torch.as_tensor()从numpy array创建得到的张量和原数据是共享内存的,修改numpy array会导致对应tensor的改变

    1
    2
    3
    4
    5
    6
    7
    8
    import numpy as np
    array = np.array([[1,2,3],[4,5,6]])
    tensor = torch.tensor(array)
    array2tensor = torch.from_numpy(array)
    tensor2array = tensor.numpy()
    # 修改array,对应的tensor也会改变
    array[0,0] = 100
    print(array2tensor)
  3. 从已存在的tensor创建

    1
    2
    3
    4
    5
    6
    7
    x = torch.tensor([5.5, 3])
    x = x.new_ones(4, 3, dtype=torch.double)
    # 创建一个新的全1矩阵tensor,返回的tensor默认具有相同的torch.dtype和torch.device
    x = torch.randn_like(x, dtype=torch.float)
    # 重置数据类型
    print(x)
    # 结果会有一样的size
  4. 创建tensor的函数
    基础构造 torch.tensor([1,2,3,4])
    随机初始化 torch.rand(2,3) [0,1)的均匀分布, torch.randn(2,3) N(0,1)的正态分布,randperm(10) 随机排列
    正态分布 torch.normal(2,3) 均值为2标准差为3的正态分布
    全1矩阵 torch.ones(2,3)
    全0矩阵 torch.zeros(2,3)
    对角单位阵 torch.eye(2,3)
    有序序列 torch.arange(2,10,2) 从2到10,步长2
    均分序列 torch.linspace(2,10,2) 从2到10,均分成2份

  5. 查看tensor的维度

    1
    2
    3
    4
    import torch
    k = torch.tensor(2,3)
    print(k.shape)
    print(k.size())

张量的操作

  1. 加法

    1
    2
    3
    4
    5
    6
    7
    8
    k = torch.rand(2, 3) 
    l = torch.ones(2, 3)

    print(k+l)

    torch.add(k,l)

    k.add(l) # 原值修改
  2. 索引操作
    与原数据内存共享,用clone()不会修改

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    x = torch.rand(4,3)
    # 取第二列
    print(x[:, 1])

    y = x[0,:]
    y += 1
    print(y)
    print(x[0, :]) # 源tensor也被改了了

    y -= 1
    a = y.clone()
    a += 1
    print(x[0, :])
  3. 维度变换

    1
    2
    3
    4
    5
    x = torch.randn(4,4)
    y = x.view(16)
    z = x.view(-1,2)
    y += 1
    print(x)

    view()共享内存,仅是更改了对张量的观察角度,而reshape()不共享内存,但原始tensor如果不连续,它会返回原值的copy,所以推荐先用clone创建副本再view,用clone能记录到计算图中,梯度回传到副本时也会传到源tensor

    扩展/压缩tensor

    1
    2
    3
    4
    5
    6
    7
    8
    9
    o = torch.rand(2,3)
    print(o)
    r = o.unsqueeze(0)
    print(r)
    print(r.shape)

    s = r.squeeze(0)
    print(s)
    print(s.shape)
  4. 取值操作

    1
    2
    3
    4
    import torch
    x = torch.randn(1)
    print(type(x))
    print(type(x.item()))

    其他操作见官方文档

张量的广播机制

两个形状不同的Tensor按元素运算时,可能会触发广播(broadcasting)机制:先适当复制元素使这两个Tensor形状相同后,再按元素运算。

1
2
3
4
5
p = torch.arange(1, 3).view(1, 2)
print(p)
q = torch.arange(1, 4).view(3, 1)
print(q)
print(p + q)

自动求导

Autograd

它是torch.tensor的核心类,设置它的属性requires_grad=True来追踪张量,完成计算后调用backward()来自动计算所有梯度,导数会自动累积到grad,由链式法则可以计算导数,torch.autograd就是计算雅可比矩阵乘积的。

  • requires_grad
    如果没有指定的话,默认输入的这个标志是 False。
  • grad_fn
    每个张量都有一个grad_fn属性,该属性引用了创建Tensor自身的Function(除非这个张量是用户手动创建的,即这个张量的grad_fn是None)。Tensor 和 Function 互相连接生成了一个无环图 (acyclic graph),它编码了完整的计算历史。
    1
    2
    3
    4
    5
    6
    7
    a = torch.randn(2, 2) # 缺失情况下默认 requires_grad = False
    a = ((a * 3) / (a - 1))
    print(a.requires_grad)
    a.requires_grad_(True)
    print(a.requires_grad)
    b = (a * a).sum()
    print(b.grad_fn)

下面举个例子说明梯度计算过程

  1. 创建一个张量并设置requires_grad=True用来追踪其计算历史
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    x = torch.ones(2, 2, requires_grad=True)
    print(x)

    y = x**2
    print(y)
    print(y.grad_fn)

    z = y * y * 3
    out = z.mean()
    print(z, out)
  2. 现在开始进行反向传播,因为out是一个标量,因此out.backward()和 out.backward(torch.tensor(1.)) 等价。由于grad在反向传播是累加的,所以在backward之前要清零.grad.data.zero_()。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    out.backward()
    print(x.grad)

    # 再来反向传播⼀一次,注意grad是累加的
    out2 = x.sum()
    out2.backward()
    print(x.grad)

    out3 = x.sum()
    x.grad.data.zero_()
    out3.backward()
    print(x.grad)
    雅可比向量积,.data.norm()它对张量y每个元素进行平方,然后对它们求和,最后取平方根,这些操作计算就是所谓的L2或欧几里德范数。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    x = torch.randn(3, requires_grad=True)
    print(x)

    y = x * 2
    i = 0
    while y.data.norm() < 1000:
    y = y * 2
    i = i + 1
    print(y)
    print(i)
    当y不再是标量,torch.autograd不能直接计算完整的雅可比矩阵,但是如果我们只想要雅可比向量积,只需将这个向量作为参数传给backward:
    1
    2
    3
    v = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float)
    y.backward(v)
    print(x.grad)
    若不需要计算梯度,阻止autograd跟踪设置.requires_grad=True的张量的历史记录
    1
    2
    3
    4
    5
    print(x.requires_grad)
    print((x ** 2).requires_grad)

    with torch.no_grad():
    print((x ** 2).requires_grad)
    若想修改tensor的数值,又不希望被autograd记录(即不会影响反向传播), 那么我们可以对tensor.data进行操作
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    x = torch.ones(1,requires_grad=True)

    print(x.data) # 还是一个tensor
    print(x.data.requires_grad) # 但是已经是独立于计算图之外,返回false

    y = 2 * x
    x.data *= 100 # 只改变了值,不会记录在计算图,所以不会影响梯度传播

    y.backward()
    print(x) # 更改data的值也会影响tensor的值,x为100
    print(x.grad) # 不会受到值改变的影响

并行计算简介

PyTorch可以在编写完模型之后,让多个GPU来参与训练,减少训练时间。CUDA是我们使用GPU的提供商——NVIDIA提供的GPU并行计算框架。对于GPU本身的编程,使用的是CUDA语言来实现的。而pytorch编写深度学习代码使用的CUDA是表示开始要求我们的模型或者数据开始使用GPU了。当我们使用了.cuda()时,其功能是让我们的模型或者数据从CPU迁移到GPU(0)当中,通过GPU开始计算。

注:数据在GPU和CPU之间进行传递时会比较耗时,我们应当尽量避免数据的切换。GPU运算很快,但是在使用简单的操作时,我们应该尽量使用CPU去完成。当我们的服务器上有多个GPU,我们应该指明我们使用的GPU是哪一块,如果我们不设置的话,tensor.cuda()方法会默认将tensor保存到第一块GPU上,等价于tensor.cuda(0),这将会导致爆出out of memory的错误。我们可以通过以下两种方式继续设置。

1
2
3
4
 #设置在文件最开始部分
import os
os.environ["CUDA_VISIBLE_DEVICE"] = "2" # 设置默认的显卡
CUDA_VISBLE_DEVICE=0,1 python train.py # 使用0,1两块GPU

常见的并行方法:

  • 网络结构分布到不同的设备中(Network partitioning)
    将一个模型的各个部分拆分,然后将不同的部分放入到GPU来做不同任务的计算。这里遇到的问题就是,不同模型组件在不同的GPU上时,GPU之间的传输就很重要,对于GPU之间的通信是一个考验。但是GPU的通信在这种密集任务中很难办到,所以这个方式慢慢淡出了视野。

  • 同一层的任务分布到不同数据中(Layer-wise partitioning)
    同一层的模型做一个拆分,让不同的GPU去训练同一层模型的部分任务。这样可以保证在不同组件之间传输的问题,但是在我们需要大量的训练,同步任务加重的情况下,会出现和第一种方式一样的问题。

  • 不同的数据分布到不同的设备中,执行相同的任务(Data parallelism)
    它的逻辑是,不再拆分模型,训练的时候模型都是一整个模型。但是我将输入的数据拆分。所谓的拆分数据就是,同一个模型在不同GPU中训练一部分数据,然后再分别计算一部分数据之后,只需要将输出的数据做一个汇总,然后再反传,这种方式可以解决之前模式遇到的通讯问题,是现在主流方式。

    参考资料

    深入浅出PyTorch