目录
- 梯度
- 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]范围内的整数