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

论文阅读 - code2vec Learning Distributed Representations of Code

泛读

我们提出了一个神经网络模型,将代码片段表示为连续分布向量(代码嵌入)。其主要思想是将一个代码片段表示为一个固定长度的代码向量,可用于预测代码片段的语义属性。为此,首先将代码分解为其抽象语法树中的路径集合。然后,网络学习每条路径的原子表示,同时学习如何聚合其中的一组路径。通过用从它身上的向量表示来预测方法名称,来证明我们方法的有效性。

论文阅读 - Contrastive Code Representation Learning

Motivation

先前工作从上下文重建tokens来学习源代码上下文表示,对下游的语义理解任务,如代码克隆检测,这些表示应该理想的捕捉程序的功能。但是,我们发现流行的基于重构的RoBERTa模型对代码编辑很敏感,甚至当编辑保留了语义。

ContraCode

我们提出ContraCode:一个对比的预训练任务,学习代码的功能,不只是形式。ContraCode 预训练一个神经网络在许多非等效的干扰项中识别功能相似的程序变体。我们使用一种自动源到源的编译器作为数据扩充的一种形式,伸缩的生成这些变体。对比性预训练在逆序代码克隆检测基准上的表现优于RoBERTa 39%AUROC。令人惊讶的是,改进的对抗性鲁棒性会比自然代码更准确;与竞争基线相比,ContraCode将汇总和TypeScript类型推理精度提高了2到13个百分点。code,paper

像GitHub这样的大型代码库是学习机器辅助编程工具的有力资源。然而,大多数当前的代码表示学习方法都需要标签,而像RoBERTa这样流行的无标签自我监督方法对对抗性输入并不鲁棒。我们不是像BERT这样重建tokens学习代码说什么,而是学习代码做什么。我们提出ContraCode,一种对比性的自我监督算法,它通过基于编译器的数据预测来学习表示不变性。在实验中,ContraCode学习功能的有效表示,并对对抗性代码编辑具有鲁棒性。我们发现,Contra Code显著提高了三个下游JavaScript代码理解任务的性能。

NLP - 01Introduction and Word Vectors

词向量

将词编码为向量可以在词空间中表示为一个点,词空间的维度实际上可能比词数量要小,就足以编码所有单词的语义。one-hot vector:将每个词表示为含有0和1的向量,$R^{|V|·1}$,V为词汇表的大小。这样的词表示没给我们直接的相似性概念,每个词都是线性无关的,所以尝试减小空间来找到一个子空间编码单词之间的关系。

  • Denotational semantics: 用符号(独热向量)表示,是稀疏的不能捕获相似性。

  • Distributional semantics: 根据上下文表示单词的含义,密度大可以很好的捕捉相似性。

SVD based methods

这类的方法是找到一个词嵌入,我们先循环一遍数据集,用矩阵X累计单词共现的频率,然后在X上进行奇异值分解,得到$USV^T$,用U的行作为字典里所有单词的词嵌入。下面讨论几种X的选择。

  • Using Word-Word Co-occurrence
    Matrix:
    • Generate |V| × |V| co-occurrence
      matrix, X.
    • Apply SVD on X to get X = $USV^T$.
    • Select the first k columns of U to get a k-dimensional word vectors.
    • $\frac{\sum^{k}{i=1}\sigma{i}}{\sum^{|V|}{i=1}\sigma{i}}$ indicates the amount of variance captured by the first k dimensions.

      Word-Document Matrix

猜测:有联系的单词经常出现在相同的文档里

构建X:循环非常多的文档,每次单词i出现在文档j里,我们将它添加到$X_{ij}$,维度为$|V|·M$,随着文档数增加。

Window based Co-occurrence Matrix

计算每个单词出现在感兴趣的单词的周围的特定大小滑动窗口中的次数。

对共现矩阵使用SVD

对X执行SVD,观察奇异值,S矩阵的对角项,根据捕获的期望百分比方差,在某个索引k处切断它们。

  • 基于统计的方法有很多问题:矩阵的维数经常发生变化;矩阵稀疏;高维;计算代价;单词频率不平衡

Iteration Based Methods - Word2vec

我们尝试一种新的方法,不是计算存储关于大数据集的全局信息,我们尝试构建一个模型,有能力一次学习一个迭代,最终能够对给定上下文的单词的概率进行编码。

  • Iteration-based methods:每次捕捉单词的共现,而不是SVD直接捕获所有的共现

Word2vec is a software package that actually includes :

  • 2 algorithms: continuous bag-of-words (CBOW) and skip-gram.

    CBOW aims to predict a center word from the surrounding context in terms of word vectors. Skip-gram does the opposite, and predicts the distribution (probability) of context words from a center word.

  • 2 training methods: negative sampling and hierarchical softmax.

    Negative sampling defines an objective by sampling negative examples, while hierarchical softmax defines an objective using an efficient tree structure to compute probabilities for all the vocabulary.

语言模型

论文阅读 - Associating Natural Language Comment and Source Code Entities

Paper,AAAI-2020

摘要

comment 是与 source code elements 相关联的自然语言描述,理解明确的关联可以提高代码的可理解性,并保持代码和注释之间的一致性。为了初步实现这个目标,我们解决了实体对齐的任务,关联 Javadoc comments 的实体和 Java source code elements 的实体。我们提出一种方法,从开源项目的修正历史自动提取监督数据,并为这个任务提出了一个手动注释的评估数据集。我们提出二分类和序列标注模型,通过构建一个丰富的特征集,包括了代码、注释、它们之间的关系。实验表明,我们的系统优于所提出的监督学习的几个基线。


疑问1:为什么要实体对齐?怎么对齐?

疑问2:数据集提取过程?规模?影响力?

疑问3:模型的细节,特征集怎么构造,基线是什么?

疑问4:这个工作带来的影响,现在的进展如何?

介绍

自然语言元素用于记录源代码的各个方面,summaries 提供了给定代码片段功能的高级概述, commit messages 描述了在软件项目的两个版本之间进行的代码更改,API comments 定义了代码块的特定属性(如先决条件和返回值)。 每一个都为开发者之间的沟通提供了一种重要的模式,对开发过程的高效性至关重要。这些自然语言元素越来越流行,在自然语言处理社区的 code summarization,commit message generation,code generation 的研究中。

特别的是,人们对结合自然语言注释和源代码的跨模态任务越来越感兴趣。要完成这些任务,必须了解注释中的元素如何与相应的代码中的元素关联。之前的研究中,检测代码和注释之间不一致的工作,合并对特定任务的规则来连接注释组件到代码的各个方面。最近在自动注释生成方面的工作,依赖一种注意力机制隐式的逼近注释中生成某种术语应注意的代码部分。

与这些方法相反,我们制定了一个任务,目的是学习注释中的实体和相应源代码中的元素之间的显式关联。我们相信显式关联会帮助系统为下游应用带来改进。比如在代码和注释生成任务,它们可以作为监督注意机制,并使用显示知识来增强神经网络模型,这往往带来性能的显著提高。此外,这提供了一种进行更加细粒度的代码注释不一致检测的方法,而不是识别完整注释是否与代码体不一致的常见方法。这样的系统可以成为自动化代码注释维护的有价值组件,目的在于使注释与它所描述的代码保持一致。通过提供注释中哪些元素被注释中的给定实体引用的信号,系统可以自动检测到注释中的实体是否与代码不一致。

学习这种关联的第一步,我们关注在 Javadoc @return comments,它用于描述返回类型和依赖于给定方法中各种条件的潜在返回值。我们观察到 @return comments 往往比其他形式的注释更加结构化,使其成为更干净的数据源,因此是所提任务的合理开头。此外,我们观察到注释通常描述给定代码体中的实体和动作,它们映射到自然语言中的名词短语和动词短语。至于 @return comments,它们描述的返回值通常是实体和与实体相关的条件(如输入参数、程序状态)。因为我们在本文关注与这样的评论,我们针对 @return comments 中的名词短语实体作为第一步,如图1、2,在注释中给定名词短语,任务是识别与它们关联的 code tokens

然而,学习自动解决注释和代码之间的关联,在数据收集方面具有挑战性。为 code/language tasks 获取注释数据是困难的,因为它需要理解特定编程语言的源代码的专业知识。此外,收集包含源代码和自然语言的高质量并行语料库具有挑战性,因为大型在线代码库中的数据本身就存在噪声。我们提出了一种新的方法,不需要人工注释,利用该平台的提交历史特性,从 GitHub 获得该任务的噪声监督。我们证明这种噪声监督提供了有价值的训练信号。

为今后的研究奠定基础,我们和相对简单的模型设计了一组高度显著的特征。我们提出了两种模型,在噪声数据上训练,在人工标注数据集上评估。第一个是二分类模型,单独的对给定的代码块的每一个元素进行分类,判断它是否与相关注释中的指定名词短语相关联。第二个是序列标记模型,特别是一个条件随机场 CRF 模型,它共同为代码中的元素分配标签,其中标签表示一个元素是否与指定的名词短语相关联。我们设计了一套新颖的特征来捕获上下文表示、余弦相似度以及与编程语言相关的 API 和语法。

在噪声数据上训练,两种模型的表现大大优于基线,二值分类器获得 F1 score 0.677,CRF 获得 F1 score 0.618,分别比基线提高了39.6%和27.4%。我们通过随着噪声训练数据量的增加,模型的性能提升来证明噪声数据的价值性。此外,通过消融研究,我们强调了模型所用的特征的实用性。本文的主要贡献如下:

  • 新任务:关联自然语言的注释和源代码的元素,结合一个人工标注的评估数据集。
  • 一种从软件改变历史和利用监督形式的机器学习系统中获取噪声监督的技术。
  • 一个新的特征集,捕捉代码和注释的特征和它们之间的关系,被用于模型中,可以作为未来工作的 baseline。

任务

给定注释中的一个名词短语(NP),其任务是将它和相应代码中的每个候选 code token 之间的关系分类为关联的和不关联的。候选项是code tokens,不包括Java关键字(如try,public,throw),运算符(如=),符号(如[,{)这些元素与编程语言语法相关,通常不会在注释中进行描述,如图中 token int,opcode,currentBC 和NP中的“the current bytecode”相关,但是int,setBCI,_nextBCT不是。该任务和自然语言文本的 anaphora resolution 回指解析有相似之处,包括明确地提到 antecedents 先行词(coreference 共引用)以及关联关系(bridge anaphora 桥接回指)。在这种设置下,注释中被选择的名词短语是 anaphora 隐喻,属于源代码的 tokens 是 candidate antecedents 候选先行词。然而,我们的任务与两者不同,因为它需要对两种不同的模式进行推理。如图1,“problem”显式关联e,但我们需要知道 InterruptedException 是它的类型,这是Exception的一种,Exception是“problem”的一种编程术语。此外,在我们的设置中,注释中的一个NP可以关联源代码中不属于同一个co-reference “chain” 共同引用链的多个不同的元素。由于这些问题,我们将我们的任务广泛的定义为将自然语言注释中的一个名词短语与相应代码体中的单个 code token 关联起来。

在这项工作中,我们使用Java编程语言和Javadoc注释,即 @return 注释。然而,这项任务和方法可以扩展到其他编程语言。例如,Python Docstring和c#XML文档注释也有类似的目的。

数据

我们使用Java中结构最好的注释类型,即带有 @return 的注释,它是Javadoc 文档的一部分,如图1、2。

@return 注释描述了输出,输出是由组成方法的各种语句计算的。相比之下,其他Javadoc 标签中的内容通常更加窄,非结构注释往往本质上更长和高级,这使得很难映射到代码中的元素,如补充材料。我们未来的工作将把提出的任务扩展到其他类型的评论。在此,我们提到评论时,指的是@return标签后的内容。

我们通过从Github上流行的开源项目的所有commits中提取示例来构造数据集。我们根据stars量来排序项目,使用了前1000个项目,因为它们被认为质量更高。我们提取的每个示例都包括对方法体的代码更改以及相应的@return注释的更改。

噪声监督

我们的噪声监督提取方法的核心思想是利用软件版本控制系统(如Git)的修改历史,基于先前研究表明源代码和评论共同进化。本质上,如果注释中的实体和源代码中的实体同时被edit,则它们相关联的概率会更高,即可近似为同时commit。因此,挖掘这样的共编辑让我们为这个任务获得噪声监督:我们用版本控制系统Git来隔离一起添加和删除的部分代码和注释。

  • 监督设定

    • 添加

      部分添加的代码可能和部分同时添加的注释有关联,基于这样的直觉,我们为代码tokens分配了有噪声的标签。也就是说,我们将在给定提交中添加的任何代码tokens标记为与同一提交中的注释中引入的NP相关联的代码,并将所有其他代码tokens标记为与NP无关的代码。这些正标签是有噪声的,因为一个开发者可能同时做其他的与添加的NP无关的代码更改。另一方面,负标签(未关联)具有最小的噪声,因为从以前版本中保留的代码tokens不太可能与以前版本注释中不存在的NP关联。我们从添加的数据中收集的这一组示例构成了我们的主要数据集。

    • 删除

      理论上,如果我们假设被删除的代码tokens与从注释中删除的NP相关,我们可以从每个提交中提取一个示例。然而,删除的NPs在这方面比添加的NPs要微妙得多。如上面所说,由于添加的NP在以前的版本中不存在,因此以前存在的代码tokens不太可能与之相关联。但由于被删除的NP确实存在于以前的版本中,因此我们不能完全地声称一个在代码中的不同版本之间保持不变的token与删掉的NP没有关联。这将可能会导致更多的负标签噪声,除了固有存在的正标签噪声。如图3,nextBCI被自动标记为与被删除的NP“下一个字节码”无关,即使它可以说是关联的。因此,我们将这些示例从我们的主数据集中分离出来,并形成另一组我们称为删除数据集的示例。

  • 数据处理

    我们在提交中检查代码和注释的两个版本:提交之前和提交之后。使用spaCy,我们从两个版本的注释中提取NPs,并使用javalang库,对代码的两个版本进行tokenize。使用difflib库,我们计算了两个版本的注释中的NPs之间的差异,以及两个版本的tokenized代码序列之间的差异。这些差异的每一行变化都用正负号标记,如图3所示。

    从不同的结果中,我们分别识别了之前和之后的注释和代码版本中唯一的NPs和code tokens,允许我们构建两对(NPs,相关的code tokens)。一个是删除的案例,删除部分的注释和代码只出现在先前的版本中。一个是添加的案例,添加部分的注释和代码只出现在新版本中。

    如果提取的NPs或相关code tokens列表为空,我们丢弃这对。此外,我们丢弃由多个NP组成的对,以获得不模糊的训练数据,以确定哪些code tokens应该与哪个NP相关联。因此,最后的对形式是(NP,相关的code tokens)。注意,对于关联的code tokens中的任何token,如果它不是一个通用的Java类型(例如,int,String),我们会用关联到相同文字字符串来处理code tokens中任何其他token。

    然后,我们回到前后版本的代码(不包括Java关键字、操作符和符号,参照第2节)。我们将代码序列tokenize,并将任何不存在在关联code tokens中的tokens标记为不关联。按照这个过程,每个示例都由一个NP和一个被标记的code tokens序列组成。从以前版本(以前)提取的示例将添加到删除数据集中,从新版本(之后)提取的示例被添加到主(添加)数据集中。

  • 数据过滤

    虽然大型代码基地如Github和StackOverflow提供了大量数据,为源代码和自然语言任务获取海量高质量的并行数据仍是一个挑战,由于显著的噪声和代码重复等原因。先前的工作通过使用人工标记数据训练的分类器来过滤低质量的数据来解决这个问题。但是,手动获取数据是很困难的,我们选择用启发式,如之前的工作,我们施加约束来过滤掉噪声数据,包括重复、细碎的场景,和不相干的代码与注释更改组成的示例数据。

    我们定义细碎场景为涉及到由几个都关联到这个NP的code tokens组成的单行方法的示例,还有那些用简单字符串匹配工具就可以解决关联的示例。

    此外,在手动检查了大约200个示例的样本后,我们建立了启发式来最小化不相干代码注释更改的示例数量:

    1. 那些有冗长的方法或大量代码更改,这些更改可能不都与注释相关。
    2. 与重新格式化、更正错误修复和简单改述的相关代码和注释变动的示例。
    3. 注释变化包括动词短语的示例,因为相关的代码变化可能与这些短语有关,而不是NP。

    此外,由于我们关注的是描述Java方法返回值的@return标记,因此我们消除了不包括返回类型更改或至少一个返回语句的代码更改示例。具体参数和每个启发式丢弃的示例数量见补充材料。

    应用这种启发式方法大大减少了数据集的大小。然而,我们在手动检查200个例子并观察到显著的噪声后,确定这种过滤是必要的,并发现这与上述之前的工作一致,表明了在大的代码库中,如果没有积极的过滤和预处理,就无法从中学习。

    经过过滤后,我们将主数据集划分为训练集、测试集和验证集,如表1所示。基于训练集,NP的中位数是2,四分位数范围(IQR,差异在25%和75%的百分位)1,code token的中位数25,IQR 21,相关的code token是10,IQR 13。我们报告了IQR,因为这些分布不是正态的。

  • 测试集

    测试集中的117个示例是由一位有7年Java使用经验的作者进行注释的。在试验研究中,两个注释者在集中专注于注释的标准之前,共同检查了一个用于注释的方法/注释对的样本集。用于识别相关的code tokens的标准包括:它是否由NP直接引用;它是与NP引用的实体对应的属性、类型或方法;它被设置为等于NP引用的实体;如果令牌被更改,则需要更新NP。有关注释的示例,请参见补充材料。为了评估注释的质量,我们询问了一个不是作者之一且有5年Java经验的研究生,注释286个代码标记(来自测试集中的25个示例),这些标记在嘈杂的监督下被标记为相关的。两组注释之间的Cohen’s kappa得分为0.713,表明一致性令人满意。

表示和特征

我们设计了一组特征,包括表面特征,单词表示,代码标记表示,余弦相似性、代码结构和java api。我们的模型利用通过连接这些特征而得到的1852维特征向量。

  • surface feature 表面特征

    我们合并了两个binary features,subtokens matching 和返回语句中的 presence,这些也被用于下节讨论的baseline。subtoken matching feature 表示一个候选code token完全匹配给定名词短语的组件,在给定token-level或者subtoken-level。subtokenization 是指java中常用的分裂驼峰式大小写,返回语句的presence feature表示候选code token是否出现返回语句还是完全匹配在返回语句出现的任何token。

  • 单词和代码表示

    为了在注释和代码中获得术语表示,我们预先训练了注释的character-level和word-level嵌入表示,代码的character-level、subtoken-level和token-level的嵌入表示。这些128维度的嵌入在大语料库上训练,由GitHub中提取的128,168个@return标签/Java方法对组成。训练前的任务是使用单层、单向的SEQ2SEQ模型为Java方法生成@return注释。我们使用平均嵌入来获得NP和候选代码标记的表示。此外,为了提供一个有意义的上下文,我们对完整@return注释对应的嵌入以及候选令牌出现的同一行中标记对应的嵌入取平均值。

  • 余弦相似性

    最近的工作使用代码和自然语言描述组成的对的联合向量空间,表明一个代码体和它相关描述有相似的向量。由于@return注释的内容经常在代码中提到实体,不是建立一个联合的向量空间,而是通过计算关于java代码训练的嵌入向量表示,我们将NP投影到代码的相同向量空间。然后我们计算NP和候选code token的余弦相似性,在token-level,subtoken-level,character-level。类似的计算NP和候选code tokens出现的代码行的余弦相似性。

  • 代码结构

    抽象语法树捕捉给定代码体在树形式的语法结构,由Java语法定义。使用javalang的AST解析器,我们获得了该method对应的AST。为了表示候选code token相对于该method的整体结构属性,我们提取其父节点和祖父节点的节点类型,用one-hot编码来表示它们。这在method的更广泛的内容的候选code token作用,提供了更深的了解,通过传递详细的信息,如是否在方法调用中、变量声明、循环、参数、try/catch block等等。

  • java api

    我们用one-hot编码来表示与通用Java类型和java.util包(一个实用程序类的集合,如List,我们往往会经常使用)。我们假设这些特征可以揭示这些经常出现的tokens的显示形式。为了捕捉本地文本,我们还包括了与候选code token相邻的code token的java相关特性,例如它是通用的Java类型还是Java关键字之一。

模型

我们开发了代表不同的方式来解决我们提出的任务的两种模型:二进制分类和序列标记。我们还制定了多个基于规则的baseline。

  • 二分类器

    给定一个code token序列和注释中的一个NP,我们独立地将每个标记分类为关联或不关联。我们的分类器是一个前馈神经网络,有4个全连接层和最后一个最终输出层。作为输入,网络接受一个与候选code token对应的特征向量(在前一节中讨论),并且模型输出对该token的二值预测。在实验中,我们还用了逻辑回归模型作为分类器,但它的表现不如神经网络。

  • 序列标记

    给定一个code token序列和注释中的一个NP,我们对代码序列的tokens是否与NP相关联进行一起分类。以这种方式构建问题背后的直觉是,对给定代码标记的分类通常依赖于对附近标记的分类。例如,在图3c中,表示next()函数的返回类型的int的token与指定的NP没有关联,而与opcode相邻的int的token被认为是关联的,因为opcode是关联的,而int是它的类型。

    为了重新建立原始序列的连续顺序,我们将已删除的Java关键字和符号注入回序列中,并引入第三个类作为这些插入标记的黄金标签。具体来说,我们预测了这三个标签:关联的、非关联的和一个伪标签Java。请注意,我们在评估过程中忽略了这些令牌的分类,也就是说,如果在测试时预测了任何code token标记为伪标签,我们会自动将其分配为不关联(平均而言,这种情况的约为1%)。我们构建一个CRF模型,通过在前馈神经网络的前面加上神经CRF层,类似于二元分类器的结构,让网络接受一个由method中的所有tokens的特征向量组成的矩阵。在实验中,我们还用了非神经网络的CRF,采用sklearn-crfsuite,但它的表现不如神经网络。

  • 模型参数

    四个全连接层有512,384,256,126个units,每个被dropout的概率是0.2。如果在5个连续epochs(10epochs之后),F1 score没有改进,我们就终止训练。我们使用最高F1 score的模型。我们用tensorflow实现了这两种模型。

  • baselines

    • Random

      基于均匀分布的code token的随机分类。

    • weighted random

      根据从训练集中观察到的相关类和非相关类的概率分别为42.8%和57.2%,对代码标记进行随机分类。

    • subtoken matching

      将subtoken matching表面特征(在上一节中介绍)设置为true的任何token都被分类为关联,而所有其他token都被分类为不关联。请注意,永远不会有这种情况出现,所有相关的code token在token-level或subtoken-level与NP匹配。在过滤过程中,我们从数据集中删除了这些琐碎的例子,因为它们可以用简单的字符串匹配工具来解决,而不是本工作的重点。

    • presence in return statement

      将返回语句表面特征(在上一节中讨论)设置为true的任何token都被分类为关联,所有其他token都被分类为未关联。

结果

采用micro-level precision,recall,F1 metrics评估模型。在token-level上,测试集中有3592NP-code token pairs。所有报告的分数都是三次运行取平均。下面讨论在主训练集上训练的结果,将删除数据集纳入训练的结果,二值分类器和CRF模型使用的特征的消融研究结果。

  • 主训练集上训练结果

    三个baselines和我们的模型的结果如表2所示。我们的分析主要基于注释测试集上的结果,为了完整性,我们展示了来自未注释集的结果。相对于未注释集的分数,模型通过注释集获得较低的精度分数和较高的召回分数。这是意料之中的,因为在注释过程中,关联有黄金标签的令牌数量减少了。

    我们的两个模型的表现都大大优于baseline。从二进制分类器中获得的样本输出见补充材料。虽然CRF的召回率得分略高于二值分类器,但很明显,二值分类器总体上优于F1得分。这可能是由于CRF需要额外的参数来建模依赖关系,这可能无法准确设置,因为我们的实验设置中示例级数据数量有限。此外,虽然我们期望CRF比二值分类器更对上下文敏感,但我们确实将二值分类器结合了许多上下文特征(周围和邻近标记的嵌入、上下文与NP的相似性以及相邻标记的Java API知识)。我们发现有错误分析,CRF模型倾向于在Java关键字之后的令牌以及在稍后出现的方法中的令牌上犯错误。这表明CRF模型可能难以对更长范围的依赖关系和更长范围的序列进行推理。此外,与二进制分类设置相比,Java关键字出现在序列标记设置,因此CRF模型必须比二进制分类器推理更多的code tokens。

  • 使用删除功能来增强训练

    我们通过从删除数据集中分阶段添加数据来增加训练集。在这些新的补充数据集上训练二值分类器和CRF的结果如表3所示。对于二值分类器,添加500和867个被删除的示例似乎对F1有显著的提升,而对于CRF模型,添加任何数量的被删除的示例都会提高性能。这表明,我们的模型可以从我们认为比我们收集的主要训练集更有噪声的数据中学习。由于我们能够在添加的情况以及对应的删除的情况下找到价值,我们能够大大增加可以收集来训练执行我们提议的任务的模型的数据量的上限。考虑到为这项任务获得大量高质量的数据是多么困难,这尤其令人鼓舞。尽管我们从超过1,000个项目的所有提交的源代码文件中的方法中提取了示例,在过滤噪声后,我们只从添加的案例中获得了总共970个例子。通过包括已删除案例中的867个例子,我们将这个数字增加到1837个。虽然这仍然是一个相对较小的数字,但我们预计随着任务范围扩展到本文中关注的@return之外的其他评论,潜在的规模将大幅增加。

  • 消融研究

    我们对在主数据集上训练的二元分类器进行了消融研究,以分析我们所引入的特征的影响。我们消除了余弦相似性,嵌入,以及和java相关的特性。嵌入特性包括代码嵌入(即对应于候选代码标记的嵌入和方法行中的标记)和注释嵌入(即对应于NP和@retern注释的嵌入)。6根据表4所示的结果,相对于f1度量,所有这些特征都对完整模型的性能做出了积极的贡献。

相关工作

之前的工作研究了一项任务,包括将对话系统中的名词短语接地到编程环境中(Li和Boyer 2015;Li和Boyer2016)。名词短语是从学生和导师之间的互动中提取出来的,而编程环境承载着学生的代码。他们的工作性质类似于共引用解析,因为目标是识别编程环境中被给定名词短语引用的实体。在对话中,学生和导师讨论与代码中特定实体相关的实现细节,这使得共同引用解析成为框架任务的适当方式。相比之下,他们的工作主题——伴随源代码的注释——通常描述高级功能,而不是实现细节。由于代码中的多个组件相互作用以组成功能,因此代码中可能存在由注释中的给定元素直接或间接引用的实体。由于它们的数据和实现无法公开获得,因此我们无法对这些任务和方法进行任何进一步的比较。

Fluri、W¨ursch和Gall(2007)研究了一个变体任务,即基于距离度量和其他简单启发式方法将单个源代码组件(如类、方法、语句)映射到行或块注释。相比之下,我们的方法在更细粒度的级别上处理代码和注释——我们在令牌级别上对代码进行建模,并在注释中考虑NPs。此外,在我们的框架下,多个代码标记可以映射到同一个NP,并且这些映射是从从更改中提取的数据中学习到的。

Liu等人(2018)介绍了一项任务,该任务涉及将单个提交消息中包含的不同更改意图链接到在提交中发生更改的软件项目中的源代码文件。虽然这需要将自然语言消息中的组件与源代码关联起来,就像我们提出的任务一样,但我们感兴趣的关联占据了更高的粒度。也就是说,我们关注评论中的NPs,而他们的工作是关注提交消息中的句子和子句,在我们的例子中,分类单位是每个单独的代码标记,而在他们的工作中是每个文件。此外,在它们的工作中构建的数据集提取已经更改并提交消息的源代码文件,这些文件是为每个提交新编写的。相反,对于我们的任务,我们收集了包含源代码和注释中的更改的例子,这些源代码和注释是共同发展的。

我们从Git版本控制系统中提取示例的过程与Faruqui等人(2018)基于维基百科的编辑历史建立一个维基百科编辑语料库的方法类似。他们从维基百科文章中的句子中连续文本的插入中提取样本,这些例子有望展示自然语言文本通常是如何被编辑的。相比之下,我们不仅仅限制插入或要求编辑是连续的。此外,我们努力收集一些演示两种模式如何一起编辑的例子。

总结

在本文中,我们制定了将Javadoc注释中的实体与Java源代码中的元素关联起来的任务。我们提出了一种新的方法来获得有噪声的监督,并提出了一组丰富的特性,旨在捕获代码、注释和它们之间的关系等方面。基于在一个手动标记的测试集上进行的评估,我们表明,在这样的噪声数据上训练的两个不同的模型可以显著优于多个基线。此外,我们通过展示增加有噪声训练数据的大小如何提高性能,证明了从有噪声数据中学习的潜力。我们还通过消融研究强调了我们的特征集的价值。

Code and datasets

env:py2.7,tnsorflow-gpu=1.15.0
待改进:

  • 数据集太小
  • 模型太简单,baseline太弱