Pytorch - HongkuanZhang/Technique-Notes GitHub Wiki

torch.nn和torch.nn.functional的关系

torch.nn中大多数layer在torch.nn.funtional中都有一个与之对应的函数。二者的区别在于:

  1. torch.nn.Module中实现layer的都是一个特殊的类,可以去查阅,他们都是以class xxxx来定义的,会自动提取可学习的参数。而nn.functional中的函数,更像是纯函数,由def function( )定义,只是进行简单的数学运算而已。
  2. functional中的函数是一个确定的不变的运算公式,输入数据产生输出就ok,而深度学习中会有很多权重是在不断更新的,不可能每进行一次forward就用新的权重重新来定义一遍函数来进行计算,所以说就会采用类的方式,以确保能在参数发生变化时仍能使用我们之前定好的运算步骤。
  3. 从这个分析就可以看出什么时候改用nn.Module中的layer了:如果模型有可学习的参数,最好使用nn.Module对应的相关layer,否则二者都可以使用,没有什么区别。比如Relu没有可学习的参数,只是进行一个运算而已,所以使用的就是functional中的relu函数,而卷积层和全连接层都有可学习的参数,所以用的是nn.Module中的类。不具备可学习参数的层,将它们用函数代替,这样可以不用放在构造函数中进行初始化。

torchvision.transform

1、torchvision.transforms.Compose(transforms) 功能:将多个transform组合起来使用 transforms: 由transform构成的列表 使用例子: transforms.Compose([ transforms.CenterCrop(10), transforms.ToTensor(), ])

2、CenterCrop(size) 功能:将给定的PIL.Image进行中心切割,得到给定的size。 size可以是tuple,(target_height, target_width)。 size也可以是一个Integer,在这种情况下,切出来的图片的形状是正方形。

3、RandomCrop(size, padding=0) 功能:切割中心点的位置随机选取。size可以是tuple也可以是Integer。

4、RandomHorizontalFlip 功能:随机水平翻转给定的PIL.Image,概率为0.5。即:一半的概率翻转,一半的概率不翻转。

5、RandomSizedCrop(size, interpolation=2) 功能:先将给定的PIL.Image随机切,然后再resize成给定的size大小。

6、Pad(padding, fill=0) 功能:将给定的PIL.Image的所有边用给定的pad value填充。

7、Normalize(mean, std) 功能:给定均值:(R,G,B) 方差:(R,G,B),将会把Tensor正则化。即:Normalized_image=(image-mean)/std

8、ToTensor 功能:把一个取值范围是[0,255]的PIL.Image或者shape为(H,W,C)的numpy.ndarray,转换成形状为[C,H,W],取值范围是[0,1.0] torch.FloadTensor

9、ToPILImage 功能:将shape为(C,H,W)的Tensor或shape为(H,W,C)的numpy.ndarray转换成PIL.Image,值不变。

10、Lambda(lambd) 功能:使用lambd作为转换器。

11、Scale(size, interpolation=2) 功能:将输入的Image重新改变大小成给定的size,size是最小边的边长。举个例子,如果原图的height>width,那么改变大小后的图片大小是(size*height/width, size)

torch.where & torch.full_like

torch.where用于寻找目标tensor中满足条件的元素并且赋值,它包含三个参数:判定条件,满足条件元素的设定值,不满足条件元素的设定值,后两个参数常常用torch.full_like来设置,具体如下:

import torch

x = torch.linspace(1, 27, steps=27).view(9, 3)
y = torch.where(x > 5, torch.full_like(x, 5), torch.full_like(x,8))

x >>>
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., 27.]])

y >>>
tensor([[8., 8., 8.],
        [8., 8., 5.],
        [5., 5., 5.],
        [5., 5., 5.],
        [5., 5., 5.],
        [5., 5., 5.],
        [5., 5., 5.],
        [5., 5., 5.],
        [5., 5., 5.]])

nn.LSTM

LSTM中的参数及意义如下:

  1. input_size 输入数据的特征数量.
  2. hidden_size 输出数据的特征数量.
  3. num_layer 构建的神经网络层数(向上延伸的层数),默认为1.
  4. batch_first 默认为False,这种时候输入必须为(sentence_len, batch_size, input_size),若为True,则输入维度变为我们熟知的(batch_size,sentence_len,input_size).
  5. dropout 除了最后一层之外都引入一个dropout.
  6. bidirectional 默认为Flase,True的时候会前后方向各算一次,最后得到两倍的输出(sentence_len,batch_size,hidden_size*2).

输入和输出的维度如下:

  1. 输入input的维度: (sentence_len, batch_size, input_size)
    c0 & h0的维度: (layer_num * direction_num, batch_size, hidden_size)
  2. 输出output的维度: (sentence_len,batch_size,hidden_size * direction_num)
    c0 & h0的维度: (layer_num * direction_num, batch_size, hidden_size)

一个例子:

input = t.randn(2, 3, 4) # batch_size=3, seq_len=2, input_size=4    
lstm = nn.LSTM(4, 3, 1) # input_size=4, hidden_size=3, layer_num=1        
h0 = t.randn(1, 3, 3) # c0 & h0: batch_size=3, layer_num=1, hidden_size=3 
c0 = t.randn(1, 3, 3)

out, (hout,cout) = lstm(input, (h0, c0)) # out维度为(2,3,3) hout & cout维度为(1,3,3)
                                         # 若为双向则out的最后一个维度都乘以2,即为(2,3,6) 

nn.Embedding

参数如下:

  1. num_embeddings : 词典大小
  2. embedding_dim : 每个词的维度
  3. padding_idx : 整数,输出遇到此下标时用0填充(暂时没懂)

输入输出:

  1. 输入:input=t.Tensor(batch_size, sentence_len)
  2. 输出:output=t.Tensor(batch_size,sentence_len,embedding_dim)

tensor.expand & torch.full:

  1. tensor.expand是将tensor按照给出的size进行扩展,而且只能进行0维度扩展
tensor.expand(size) # size is like (3,2) that you want to expand to.
  1. torch.full是将某个字母按照指定形状全部填充
torch.full(size, number) # size is that you want to expand to and the number is the number that tensor is filled with.

.data()和.detach()

tensor.data和tensor.detach()都可以把tensor的梯度消去,只保留tensor本身,如下:

a = torch.tensor([1,2,3.], requires_grad =True)
out = a.sigmoid()
k = out.sum()
f = k.data
c = k.detach()

print(k.requires_grad,f.requires_grad,c.requires_grad) # True,Flase,False
print(k,f) # tensor(2.5644, grad_fn=<SumBackward0>) tensor(2.5644)

两者的区别如下:

# tensor.data

>>> a = torch.tensor([1,2,3.], requires_grad =True)
>>> out = a.sigmoid()
>>> c = out.data
>>> c.zero_()
tensor([ 0., 0., 0.])  

>>> out                   
tensor([ 0., 0., 0.])     # 对out的值进行改变

>>> out.sum().backward()  #  反向传播
>>> a.grad                #  这里由于out被改变了,导致反向传播的值也和原来out的反向传播值不一样,而这个严重的错误不会报错
tensor([ 0., 0., 0.])     #  (可以试试out不变的时候反向传播的值,即c.zero_()不操作,直接out.sum().backward())

# tensor.detach()

>>> a = torch.tensor([1,2,3.], requires_grad =True)
>>> out = a.sigmoid()
>>> c = out.detach()
>>> c.zero_()
tensor([ 0., 0., 0.])

>>> out                   #  out的值仍然被c.zero_()修改
tensor([ 0., 0., 0.])

>>> out.sum().backward()  #  需要原来out的值,但是已经被c.zero_()覆盖了,所以不进行反向传播,结果会报错(好,不会引起重大错误)
RuntimeError: one of the variables needed for gradient
computation has been modified by an

contiguous

  • 对于torch.tensor如果在view之前用了transpose, permute等,需要用contiguous()来返回一个contiguous copy。如下:
x = torch.rand(3,4,5,6)
x = x.transpose(1,2).view(3,5,-1) # 会报错,因为需要用contiguous
x = x.transpose(1,2).contiguous().view(3,5,-1) # 正确 形状为(3,5,24)

nn.Conv2d:

  1. 格式为 nn.Conv2d(in_channel, out_channel, kernel_size, stride, padding)
    其中kernel_size和stride可以为tuple, 分别指定高和宽
    padding在pytorch中默认为valid padding, 即输出size一般比原来小,eg: padding = (2,4) 意味着高上下各pad2 宽左右各pad4
  2. input : (batch_size, in_channel, H_in, W_in)
    output : (batch_size, out_channel, H_out, W_out)
  3. 对于H_out W_out 计算方法如下:
    无padding:original_size-(kernal_size-1)/stride
    有padding:original_size-(kernal_size-1)+2*padding/stride
    Original_size 为 H_in或W_in, 计算时结果若为小数则向上取整 如 23.5 -> 24
  4. Pooling哪个维度哪个维度就变成1
  5. 基本上pooling都是操作在feature map内部的, 比如说30个feature map每个维度为1 * 6, 则pooling是在6这个feature map的size上进行的, pooling之后就是1 * 30
  6. 关于Pytorch的CNN, 一些有用的Links: Link1, link2

Bilstm char embedding example

Generating the char-level representation for "God"

对tensor指定条件返回满足条件的布尔函数tensor/元素index

  1. 返回布尔函数tensor
  1. 返回满足条件元素的index

保存和加载模型

torch.save(model_object,'model.pth')
model = torch.load('model.pth')

Softmax, Log_softmax, NLLLoss, CrossEntropyLoss的关系

  • Softmax: torch.nn.functional.softmax() 只是单纯的对tensor的值进行归一化处理(概率分布和为1)
  • Log_softmax: torch.nn.functional.log_softmax() / torch.nn.LogSoftmax() 比上面一步多进行一个log运算
  • NLLLoss: torch.nn.functional.nll_loss() / torch.nn.NLLLoss() 输入是对数概率向量和一个目标标签(常常接在上面的Log_softmax之后),输出是根据crossentropy来算的loss,例子如下:
import torch.nn.functional as F
input = torch.tensor([-1,-0.7,-0.15](/HongkuanZhang/Technique-Notes/wiki/-1,-0.7,-0.15))
target = torch.tensor([1](/HongkuanZhang/Technique-Notes/wiki/1)) # 假设正确标签为1号
output = F.nll_loss(input,target) # output为0.7
  • CrossEntropyLoss: torch.nn.functional.cross_entropy() / torch.nn.CrossEntropyLoss() 输入为最原始的模型输出(无需softmax), 输出为交叉熵损失, 即: nn.CrossEntropyLoss() = nn.LogSoftmax() + nn.NLLLoss()

优化器

torch.optim.optimizer_name(params,lr)

pytorch中创建优化器yongtorch.optim.优化器名称(eg., Adam)。用法如下:
optimizer = torch.optim.Adam(model.parameters(),lr=0.01)
关于两个基础参数:

  • 一个是要优化的网络的参数params。根据需求,它可以是网络的全体参数model.parameters(),也可以是模型的一部分参数如[{'params': model.layer1.parameters(),'lr'=0.1},{'params':model.layer2.parameters(),'lr'=0.2}],这样的每个字典叫做一个params_groups参数组, 且根据需要每组学习率可以设置为不相同。如果只对某两个层的参数进行更新优化,且lr相同,则可以令params = nn.ModuleList([model.layer1,model.layer2]).parameters()或者params = [*model.layer1.parameters(),*model.layer2.parameters()],然后统一分配默认的学习率(lr参数指定)。
  • 另一个参数是lr,这个参数值给定了默认的学习率,如果在params_group中没有指定lr则参数组被赋予默认的lr值。
  • optimizer的两个属性
  1. optimizer.defaults: 字典,存放这个优化器的一些初始参数,有:'lr', 'betas', 'eps', 'weight_decay', 'amsgrad'。事实上这个属性继承自torch.optim.Optimizer父类;
  2. optimizer.param_groups:列表,每个元素都是一个字典,每个元素包含的关键字有:'params', 'lr', 'betas', 'eps', 'weight_decay', 'amsgrad',params类是各个网络的参数放在了一起。这个属性也继承自torch.optim.Optimizer父类。

torch.optim.lr_scheduler:调整学习率

  • torch.optim.lr_scheduler中大部分调整学习率的方法都是根据epoch训练次数进行调整的,这里主要介绍LambdaLR,语法如下:
    torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda, last_epoch=-1)
  • 更新策略:new_lr=λ×initial_lr
  • 参数:
  1. optimizer (Optimizer):要更改学习率的优化器;
  2. lr_lambda(function or list):根据epoch计算λ的函数,或者是一个list的这样的function,分别计算各个parameter groups的学习率更新用到的λ
  3. last_epoch (int):最后一个epoch的index,如果是训练了很多个epoch后中断了,继续训练,这个值就等于加载的模型的epoch。默认为-1表示从头开始训练,即从epoch=1开始。
  • 另外一个常用的scheduler是StepLR,用法如下:
    scheduler = lr_scheduler.StepLR(optimizer, step_size = 10, gamma = 0.1)
    这里的step_size表示scheduler.step()进行step_size次计数后对学习率进行调整。一般来说scheduler.step()都放在epoch中,因此表示step_size次epoch之后调整学习率,如果和optimizer.step()一样放在mini-batch中(比较少放在mini-batch中),则表示step_size次iteration后调整学习率。

应用optimizer及调整学习率的代码实例:

import torch
import torch.nn as nn
from torch.optim.lr_scheduler import LambdaLR

initial_lr = 0.1

class model(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels=3, out_channels=3, kernel_size=3)

    def forward(self, x):
        pass

net_1 = model()

optimizer_1 = torch.optim.Adam(net_1.parameters(), lr = initial_lr)
scheduler_1 = LambdaLR(optimizer_1, lr_lambda=lambda epoch: 1/(epoch+1))

print("初始化的学习率:", optimizer_1.defaults['lr'])

for epoch in range(1, 11):

    optimizer_1.zero_grad()
    optimizer_1.step()
    print("第%d个epoch的学习率:%f" % (epoch, optimizer_1.param_groups[0]['lr'])) #这里是每个epoch都更新第一组参数组的学习率,实际上很多时候设置一个条件如`epoch % 5 == 0`来更新学习率。scheduler有计epoch数的模块,所以不依赖于epoch这个参数。
    scheduler_1.step()
'''
结果如下:
初始化的学习率: 0.1
第1个epoch的学习率:0.100000
第2个epoch的学习率:0.050000
第3个epoch的学习率:0.033333
第4个epoch的学习率:0.025000
第5个epoch的学习率:0.020000
第6个epoch的学习率:0.016667
第7个epoch的学习率:0.014286
第8个epoch的学习率:0.012500
第9个epoch的学习率:0.011111
第10个epoch的学习率:0.010000
'''
  • 总结下来,优化参数的总体流程为:
  1. 清空梯度 optimizer.zero_grad() (放在mini-batch中)
  2. 向前传播 output = model(....) (放在mini-batch中)
  3. 计算loss loss = nn.Loss_function(output,golden_labels) (放在mini-batch中)
  4. 反向传播 loss.backward() (放在mini-batch中)
  5. 更新参数 optimizer.step() (放在mini-batch中)
  6. 调整学习率(根据条件) scheduler.step() (放在epoch中)

nn.Layernorm

这个函数的shape参数应该等于要正则化的维度值(一般是输入tensor最后一维的维度),具体可以参考此链接