目录

  • 梯度
  • nn.Embedding
  • dataset和dataloader
  • 随机数

梯度

实验数据:

x1 = torch.tensor([1, 2], dtype=torch.float, requires_grad=True)
x2 = torch.tensor([3, 4], dtype=torch.float, requires_grad=True)
x3 = torch.tensor([5, 6], dtype=torch.float, requires_grad=True)
y = (torch.pow(x1, 3) + torch.pow(x2, 2) + x3).sum()
y.backward()
x4 = x2.clone()
print(x1.grad, x2.grad, x3.grad, x4.grad, x4.requires_grad)
# 梯度分别是 3x^2, 2x, 1, None
# tensor([ 3., 12.]) tensor([6., 8.]) tensor([1., 1.]) None True

求导是通过backward()来实现的,最后对象一定是一个scalar,比如y.backward()。这里的y是一个和一些需要求导的tensor相关的数值。
则可以通过x.grad去查看tensor x的梯度。

tensor有一个属性requires_grad,决定是否求梯度。
可以通过detach()或者detach_()放弃求导。注意,在pytorch中_代表是否修改自身。比如x.detach()只是返回一个放弃求导的tensor,x本身并没有放弃。但是需要注意下文所说的浅拷贝问题,即使y=x.detach()返回了一个放弃求导的tensor,此时x可求导y不可求,但是对y做修改仍然会影响到x。
注意python中的=是浅拷贝,如果用a = b去构造a的话,a的变化会在b上面进行修改。

a = torch.tensor([1, 2], dtype=torch.float, requires_grad=True)
b = a.detach()  # detach 后 requires_grad=False
b[0] = 100
print(a, b)  
# 浅拷贝 还是会影响彼此 tensor([100.,   2.], requires_grad=True) tensor([100.,   2.])
a.detach_()
print(a, b)  
# tensor([100.,   2.]) tensor([100.,   2.])

所以如果想构造一个相同的tensor,可以通过clone()来实现,如a = b.clone()。需要注意两点:
1.clone出来的tensor的requires_grad是根据原来的tensor决定的
1.1 如果原tensor是True就True,并且会保留clone的求导关系。即,假设a = b.clone(),那么最后b的梯度里会加上a的梯度;并且对a求梯度始终是None,因为此时a不是一个leaf variables。但是只会对叶子变量求梯度。参见后文的x1,x3的梯度情况
1.2 如果原来tensor是False,比如a = b.detach().clone(),那么a也是False。但是可以修改a的requires_grad为True,并且之后可以正常求梯度。参见后文的x1,x4,x5梯度情况。
2.clone后的新tensor不会复制原来的tensor的grad,并且丢失自己原来的grad。

# 原来的x3和x1都有梯度值
x3 = x1.clone()  # requires_grad为True 但是和x1有clone的梯度关系
x4 = x1.detach().clone()  # requires_grad为True 和x1无关
x5 = x1.detach().clone()
print(x3, x3.grad, x3.requires_grad)  
# tensor([1., 2.], grad_fn=<CloneBackward>) None True 注意是True可求导,并且有clone的关系
print(x4, x4.grad, x4.requires_grad)  
# tensor([1., 2.]) None False 注意是False不可求导

x3.requires_grad = True
# 会报错 RuntimeError: you can only change requires_grad flags of leaf variables.

对于求出来的梯度grad是不清空的,如果多次求导,梯度会累加。如果想清空某个tensor的梯度,可以使用grad.zero(),比如x.grad.zero() 举例:

x2.grad.zero_()  # 清空梯度
x5.requires_grad = True
z = (torch.pow(x1, 3) + torch.pow(x2, 2) + x3 + x4 + x5).sum()
z.backward()
print(x1.grad, x2.grad, x3.grad, x4.grad, x5.grad)
# tensor([ 7., 25.]) tensor([6., 8.]) None None tensor([1., 1.])
# x1梯度是原来梯度的两倍+1,因为累加了一次自己的梯度,然后加了一次clone后的x3的梯度 = 2x3+1 = 7
# x2梯度清空了 所以只有z求导后的梯度
# x3是非叶子变量 虽然requires_grad为True但是无梯度 是None
# x4是detach后的clone, requires_grad=False
# x5虽然是detach后的clone, 但是重新设置了requires_grad=True 所以可以正常求梯度
print(x1.requires_grad, x2.requires_grad, x3.requires_grad, x4.requires_grad, x5.requires_grad)
# True True True False True

那么如何做到,修改当前tensor a的数据为tensor b,并且a和b之间没有clone的梯度关系,并且a保持requires_grad为True,并且可以无视a原来的grad呢?
答:令a=b.detach().clone()获取数据,然后设置a.requires_grad = True来确保可以求梯度
举例如下,令x1的数据为gr,并且可以继续求导。

x1 = torch.tensor([1, 2], dtype=torch.float, requires_grad=True)
x2 = torch.tensor([3, 4], dtype=torch.float, requires_grad=True)
gr = torch.tensor([5, 0], dtype=torch.float, requires_grad=True)
y = (torch.pow(x1, 3) + torch.pow(x2, 2)).sum()
y.backward()
# 3x^2, 2x
print(x1.grad, x2.grad)

x1 = gr.detach().clone()
x1.requires_grad = True

y = (torch.pow(x1, 3) + torch.pow(x2, 2)).sum()
y.backward()
# 3x^2, 2x+2x
print(x1.grad, x2.grad)

其他:copy_函数也是会有copy的梯度路径的。


nn.Embedding

nn.Embedding 常用初始化emb = nn.Embedding(num, dim)。num是个题个数,dim是嵌入维度。意思是每个个体用一个长度为dim的向量来表示,一共有num个个体。想找第i个个题的表示就是找第i行。
里面存在变量weight,初始化服从标准正态分布\(\mathcal{N}(0,1)\)。官网说是the learnable weights of the module ,是一个Tensor。
通过代码查看信息:

emb = nn.Embedding(1, 2)
print(emb.weight, type(emb.weight))  # tensor([[0.0935, 0.2543]], requires_grad=True) <class 'torch.nn.parameter.Parameter'>
print(emb.weight.requires_grad)  # True
print(emb.weight.data, type(emb.weight.data))  # tensor([[0.0935, 0.2543]]) <class 'torch.Tensor'>
print(emb.weight.data.requires_grad)  # False

可以看到weight和weight.data都是tensor,但是一个可以求梯度,另一个不可以。也就是说weight在计算图上,但是weight.data不在计算图上也不需要detach()。

为了后续求梯度方便,使用nn.Embedding.from_pretrained设置其他tensor为嵌入。需要注意的是,from_pretrained即使其他tensor是可求梯度的,新的嵌入的weight和weight.data的requires_grad都是False。所以只能手动设置。但是weight.data的requires_grad即使手动设置也无法成功。

pre_emb = torch.tensor([[1, 2]], dtype=torch.float, requires_grad=True)
emb = nn.Embedding.from_pretrained(pre_emb)
print(emb.weight.requires_grad, emb.weight.data.requires_grad)
# False False 两个都不能求梯度
emb.weight.requires_grad = True
emb.weight.data.requires_grad = True
print(emb.weight, type(emb.weight))
print(emb.weight.requires_grad)
print(emb.weight.data, type(emb.weight.data))
print(emb.weight.data.requires_grad)  # 仍然是False

一个例子:

y = torch.pow(emb.weight, 2).sum()
y.backward()
print(emb.weight.grad)  # 梯度是2x  tensor([[2., 4.]])

fs = emb.weight.data.clone().pow(2)  # 直接data.clone() data不在计算图上 所以clone后不会有梯度关系
fs.requires_grad = True

emb.weight.grad.zero_()
y = (torch.pow(fs, 2) + torch.pow(emb.weight, 2)).sum()
y.backward()
print(emb.weight.grad)  # tensor([[2., 4.]]) 和原来没有变化
print(fs.grad)  # tensor([[2., 8.]])

在学习一些代码给过程中我遇到一个需求:每轮epoch开始的时候,对emb规范化,除以自身的2范数,但是又不能影响梯度。
向量的p范数就是 \((|x_1|^p + \cdots, |x_n|^p)^{\frac{1}{p}}\),所以2范数就是所有元素的平方的和,再求开方。
则规范化后,每行向量都需要除以当前向量的2范数。
实际上为了方便,很多时候会去掉开方操作,认为2范数就是所有元素的平方的和

pre_emb = torch.tensor([[1, 2], [3, 4]], dtype=torch.float, requires_grad=True)
emb = nn.Embedding.from_pretrained(pre_emb)
emb.weight.requires_grad = True

tmp = emb.weight.data.clone()
# tmp.requires_grad = True  # 因为最后还要把tmp clone 到emb.weight.data里面 所以不需要管requires_grad
sum = torch.sum(torch.pow(tmp, 2), dim=1, keepdim=True)
print(sum)  # tensor([ 5., 25.], grad_fn=<SumBackward2>)
print(tmp.size(), sum.size())  # torch.Size([2, 2]) torch.Size([2, 1])
tmp = tmp / sum
print(tmp)  # 得到规范化后的数据
emb.weight.data = tmp.clone()
print('output emb.weight & emb.weight.data --------------')
print(emb.weight)
print(emb.weight.data)
print(emb.weight.requires_grad, emb.weight.data.requires_grad)

注意是把tmp clone到weight.data里面而不是weight里面。因为weight是一个Parameter,但是tmp只是一个普通的tensor。无注释做法就是:

tmp = emb.weight.data.clone()
tmp = tmp / torch.sum(torch.pow(tmp, 2), dim=1, keepdim=True)
emb.weight.data = tmp.clone()

通过查阅资料,看到了另一种方便的写法:

tmp = emb.weight.data.clone()
tmp = tmp / torch.sum(torch.pow(tmp, 2), dim=1, keepdim=True)
emb.weight.data.copy_(tmp)

注意这里是data.copy_,因为copy_会记录copy的梯度关系,但是data不在计算图中记录了也没用。

dataset和dataloader

import的内容:from torch.utils.data import Dataset, DataLoader

Dataset,数据集,自定义三个函数。

class MyDataset(Dataset):
    def __init__(self, data):
        self.data = data

    def __getitem__(self, index):
        return self.data[index]

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

注意,进去后,getitem返回的还是原来的数据类型(numpy、list etc)。(DataLoader出来的就都是tensor了)

DataLoader
用法:

train_set = DataLoader(dataset, batch_size=batch_size, shuffle=True)

之后根据batch_size划分,每个batch分为多个tensor。假如self.data[index]返回的是[a, b, c],那么batch就会是[tensor1, tensor2, tensor3]
如果觉得tensor麻烦可以转化:

for batch in train_set:
  h, r, t = [tri.tolist() for tri in batch]

随机数

random.random()用于生成一个0到1的随机浮点数:\(0 \leq x < 1\)
random.randint(a, b)用于生成一个[a,b]范围内的整数