2.5 自动微分
2.5.1 一个简单的例子
import torch
X = torch.arange(4, dtype = torch.float)
X
'''
tensor默认是long,只有浮点型才能求梯度
requires_grad_(True)就是说明要求梯度,因为torch是不会自动求梯度的。
'''
X.requires_grad_(True)
print(X.grad)
'''
torch.dot()计算点积,只能用于向量相乘
torch.mv()用于矩阵向量相乘
torch.mm()用于矩阵矩阵相乘
'''
Y = 2 * torch.dot(X, X)
Y
Y.backward()
X.grad
'''
自变量x需要求梯度,x.requires_grad_(True)
因变量Y需要backward()
最后计算出来的梯度存储在x.grad里面
'''
X.grad == 4 * X
'''
默认情况下,PyTorch会累积之前的梯度。
这个也很好理解,因为一个批次中有8条数据,做法是累积这8条数据的梯度然后更新一次参数,
所以梯度累积在训练模型的过程中用的非常多,因此默认就是要累积梯度。
'''
X.grad.zero_()
y = X.sum()
y.backward()
X.grad
2.5.2 非标量变量的反向传播
'''
PyTorch要求backward()的作用对象必须是标量(只有一个数值的张量),而不是张量。
直接对张量使用backward()会报错,
'''
import torch
x = torch.arange(4, dtype = torch.float)
x.requires_grad_(True)
y = x * x
"""
这里必须先sum(),使得y变成只含一个元素的张量,再去计算梯度。
"""
y.sum().backward()
x.grad
2.5.3 分离计算
'''
分离计算本质上就是为了实现对x求偏导时,把y看成常数,而不是把y看成关于x的函数。
detach()的作用就是:切断计算图,停止梯度追踪
'''
import torch
x = torch.arange(4, dtype = torch.float, requires_grad = True)
y = x * x
u = y.detach()
z = u * x
z.sum().backward()
x.grad
x.grad.zero_()
y.sum().backward()
x.grad == 2*x
2.5.4 Python控制流的梯度计算
'''
'''
def f(a):
b = a * 2
# norm() L2范数,b1**2 + b2**2 + .... 然后开根号。范数用来衡量张量的整体大小。
while b.norm() < 1000:
b = b * 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c
'''
size = ()表示标量张量,也就是张量中只有一个元素
'''
import torch
a = torch.randn(size = (), requires_grad = True)
a
d = f(a)
d.backward()
a.grad == d / a
练习
1. 为什么计算二阶导数比一阶导数的开销要更大
单纯计算一阶导数是不需要记录一阶导过程中的计算图的,但是计算二阶导的时候需要把一阶导的计算图记录下来,在此基础之上再求导。
多了两个开销:
- 内存上,得多记录一个图
- 计算上,得多计算一遍。
2. 在运行反向传播函数之后,立即再次运行它,看看会发生什么。
会报错。因为第一次Y.backward()执行完毕的时候,计算图就已经被释放掉了,这是PyTorch的默认机制,及时释放一阶计算过的计算图,以节省内存。
因此当你第二次调用Y.backward()的时候,计算图已不复存在,因此报错。
'''
Y = X * X 直接用称号算出来的是Hardmard积,说人话就是Y = [x1**2, x2**2, ...]
所以Y不是标量,而backward()的作用对象必须是一个标量。
使用backward()只能对一阶进行求导,依赖的是x.grad
但是grad1 = torch.autograd.grad(Y, X, create_graph = True)和X.grad无关,结果存储在grad1中
grad1去不去grad1[0]影响重大。
因为grad1本身是一个tuple, 元组,元组不能求sum(),但是grad1[0]是张量tensor,
就可以使用grad1[0].sum(),从而可以进一步对这个标量进行求导,实现二阶求导。
'''
import torch
X = torch.arange(5, dtype = torch.float, requires_grad = True)
Y = X * X
grad1 = torch.autograd.grad(Y.sum(), X, create_graph = True)
X.grad, grad1, grad1[0]
'''
backward()只能求一阶导,多次调用backward()就是多次进行一阶求导
'''
grad2 = torch.autograd.grad(grad1[0].sum(), X)
X.grad, grad2
``

3. 在控制流的例子中,我们计算d关于a的导数,如果将变量a更改为随机向量或矩阵,会发生什么?
会报错。因为当a是一个矩阵,那么计算出来的d也是矩阵,在对d使用backward()的时候,d并不是一个标量,因此会报错
4. 重新设计一个求控制流梯度的例子,运行并分析结果。
'''
那就写个if else, 再写个for循环
控制流也没什么高大上的,其实就是条件判断,循环,异常处理等。
'''
import torch
x = torch.randn(size = (), requires_grad = True)
y = x
if y > 0:
while y < 10:
y = y * 2
else:
while y > -1:
y = y * 3
y.backward()
x.grad == y / x
'''
'''
%matplotlib inline
import numpy as np
from matplotlib_inline import backend_inline
from d2l import torch as d2l
def use_svg_display(): #@save
backend_inline.set_matplotlib_formats("svg")
def set_figsize(figsize = (8.5, 5.5)): #@save
use_svg_display()
d2l.plt.rcParams['figure.figsize'] = figsize
#@save
def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend):
axes.set_xlabel(xlabel)
axes.set_ylabel(ylabel)
axes.set_xscale(xscale)
axes.set_yscale(yscale)
axes.set_xlim(xlim)
axes.set_ylim(ylim)
if legend:
axes.legend(legend)
axes.grid()
def plot(X, Y = None, xlabel = None, ylabel = None, legend = None, xlim = None, ylim = None,
xscale = "linear", yscale = "linear", fmts = ("-", "m--", "g-.", "r:"), figsize = (8.5, 5.5), axes = None):
if legend == None:
legend = []
set_figsize(figsize)
axes = axes if axes else d2l.plt.gca()
def has_one_axis(X):
return (hasattr(X, "ndim") and X.ndim == 1 or isinstance(X, list) and
not hasattr(X[0], "__len__"))
if has_one_axis(X):
X = [X]
if Y == None:
X, Y = [[]]* len(X), X
elif has_one_axis(Y):
Y = [Y]
'''
直接对列表乘以2,是对列表内部元素的复制
'''
if len(X) != len(Y):
X = X * len(Y)
axes.cla()
'''
循环的单次就绘制了一条线
'''
for x, y, fmt in zip(X, Y, fmts):
if len(x):
axes.plot(x, y, fmt)
else:
axes.plot(y, fmt)
set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
'''
x = np.arange(0, 3, 0.1)
plot(x, [f(x), 2*x-3], "x", "f(x)", legend = ["f(x)", "Tangent line (x=1)"])
numpy里面有的东西,基本可以用torch平替
backward()的作用是对每个变量逐个求偏导,结果存储在x.grad里面,因此dy_dx就是x.grad
'''
x = torch.arange((-2)*np.pi, 2*np.pi, 0.1, dtype = torch.float32, requires_grad = True)
y1 = torch.sin(x)
y1.sum().backward()
dy_dx = x.grad
plot(x.detach().numpy(), [y1.detach().numpy(), dy_dx.detach().numpy()], "x", "f(x)", legend = ["sin(x)", "cos(x)"])
2.6 概率
2.6.1 基本概率论
%matplotlib inline
import torch
from torch.distributions import multinomial
from d2l import torch as d2l
fair_probs = torch.ones([6]) / 6
multinomial.Multinomial(1, fair_probs).sample()
'''
torch.ones(6)和torch.ones([6])效果是一样的
直接对张量整体除以6是对张量中的每个元素除以6
'''
print(fair_probs)
print(torch.ones(6))
print(torch.ones([6]))
multinomial.Multinomial(10, fair_probs).sample()
multinomial.Multinomial(1000000, fair_probs).sample() / 1000000
'''
这个地方的plot完全没有跟上次一样定义一大堆的内容,直接就是用的d2l.plt里面的plot,直接用就行。
问题1:sample((500, ))这个500有什么用,为什么还要加个逗号
由于sample()的参数必须是tuple()类型,因此要用(500, ),直接用(500)那就是整数,不是tuple元组
一共做500次实验,每次实验掷色子10次,
问题2 cumsum有什么用,dim = 0代表什么
dim这个东西之前遇到过,0纵1横,dim = 0就纵向运算,dim = 1就横向运算
cumsum[dim = 0]相当于纵向求前缀和
补充问题1:estimates = cum_counts / cum_counts.sum(dim = 1, keepdims = True) 这里面的keepdims = True有什么用
keepdims = True最大的作用就是保持sum()以后的形状不变。
如果直接用cum_counts.sum(dim = 1), 那么出来的形状就是[500],这是一个一维向量,而被除数是[500, 6],根据广播机制的规则,
从右到左依次比较每个维度,满足以下三个条件之一:1.相等 2.其中一个为1 3. 其中一维不存在
很明显如果不适用keepdims = True, 那么[500, 6] 和[500]是不满足广播机制的。
使用keepdims = 1以后,cum_counts.sum(dim = 1, keepdims = True)出来的形状就是二维的[500, 1],这么就满足广播机制,可以使用广播除法。
问题3:estimates[:, i].numpy()有什么用
这个相当于把estimates这个500行,6列的元素一列一列地取。
补充问题2:
d2l.plt.plot(estimates[:, i].numpy(),
label = ("P(die=" + str(i+1) + ")" ) )
用上述代码在画图的时候,estimates[:, i].numpy()只给出了纵坐标,横坐标的范围是怎么确定的?
如果d2l.plt.plot()在画图的时候没有给出横坐标,那么会自动根据纵坐标的个数去生成横坐标。
问题4:d2l.plt.axhline有什么用,d2l.plt.axhline(y = 0.167, color = "black", linestyle = "dashed")这一行代码有什么用
说白了就是画一条黑色的虚线,看是不是随着实验次数的增加,概率越来越接近1/6
问题5:为什么d2l.plt.legend()没有指定曲线名称,画出来的图中却有名称
plot的时候已经指定过label了,只需要用d2l.plt.legend显示出来就行。
'''
counts = multinomial.Multinomial(10, fair_probs).sample((500, ))
cum_counts = counts.cumsum(dim = 0)
estimates = cum_counts / cum_counts.sum(dim = 1, keepdims = True)
d2l.set_figsize((9, 6))
for i in range(6):
d2l.plt.plot(estimates[:, i].numpy(),
label = ("P(die=" + str(i+1) + ")" ) )
d2l.plt.axhline(y = 0.167, color = "black", linestyle = "dashed")
d2l.plt.gca().set_xlabel("Groups of experiments")
d2l.plt.gca().set_ylabel("Estimated probability")
d2l.plt.legend()
counts = multinomial.Multinomial(10, fair_probs).sample((500, ))
cum_counts = counts.cumsum(dim = 0)
counts[:20], counts.shape, cum_counts[:20], cum_counts.shape+
2.6.2 处理多个随机变量
问题1:贝叶斯定理
这个定理我现在不太能理解它的原理,但是公式可以背住。
问题2:边际化
所谓边际化,说人话就是把所有分组的概率加起来就是该事件的概率。
2.6.3 期望和方差
from torch.distributions import multinomial
import torch
from d2l import torch as d2l
probability = torch.ones(6) / 6
probability
counts = multinomial.Multinomial(10, probability).sample((500, ))
cum_counts = counts.cumsum(dim = 0)
cum_counts[:20]
'''
累积求和,求和,这两者是不一样的。
竖着是累积求和,就应该用counts.cumsum(dim = 0)
横着只是求和,为了使用广播除法应该用keepdims = True, cum_counts.sum(dim = 1, keepdims = True)
'''
cum_counts_probability = cum_counts / cum_counts.sum(dim = 1, keepdims = True)
cum_counts_probability
A并B:
- 上限:P(A) + P(B)
- 下限:Max(P(A), P(B))
A交B
- 上限: Min(P(A), P(B))
- 下限:空集