D2L系列-第3篇-2.5自动微分and2.6概率

103 阅读8分钟

2.5 自动微分

2.5.1 一个简单的例子

import torch

X = torch.arange(4, dtype = torch.float)
X

image.png

''' 
tensor默认是long,只有浮点型才能求梯度 
requires_grad_(True)就是说明要求梯度,因为torch是不会自动求梯度的。

'''

X.requires_grad_(True)
print(X.grad)

image.png

''' 
torch.dot()计算点积,只能用于向量相乘
torch.mv()用于矩阵向量相乘
torch.mm()用于矩阵矩阵相乘

'''

Y = 2 * torch.dot(X, X)
Y

image.png

Y.backward()

X.grad

image.png

''' 
自变量x需要求梯度,x.requires_grad_(True)
因变量Y需要backward()
最后计算出来的梯度存储在x.grad里面
'''

X.grad == 4 * X

image.png

''' 
默认情况下,PyTorch会累积之前的梯度。
这个也很好理解,因为一个批次中有8条数据,做法是累积这8条数据的梯度然后更新一次参数,
所以梯度累积在训练模型的过程中用的非常多,因此默认就是要累积梯度。

'''
X.grad.zero_()
y = X.sum()
y.backward()

X.grad

image.png

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

image.png

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

image.png

x.grad.zero_()

y.sum().backward()

x.grad == 2*x

image.png

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

image.png

d = f(a)
d.backward()
a.grad == d / a

image.png

练习

1. 为什么计算二阶导数比一阶导数的开销要更大

单纯计算一阶导数是不需要记录一阶导过程中的计算图的,但是计算二阶导的时候需要把一阶导的计算图记录下来,在此基础之上再求导。

多了两个开销:

  1. 内存上,得多记录一个图
  2. 计算上,得多计算一遍。

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]

image.png

''' 
backward()只能求一阶导,多次调用backward()就是多次进行一阶求导
'''
grad2 = torch.autograd.grad(grad1[0].sum(), X)

X.grad, grad2
``

![image.png](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/59be37c92bc8452ab53ae504c8d4428d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgWm5vcno=:q75.awebp?rk3s=f64ab15b&x-expires=1772425780&x-signature=ZSVlHX8bJcud%2FNsZMiTcfMoBt6A%3D)

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

image.png

image.png

''' 


'''
%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)"])

image.png

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()

image.png

''' 
torch.ones(6)和torch.ones([6])效果是一样的
直接对张量整体除以6是对张量中的每个元素除以6
'''

print(fair_probs)
print(torch.ones(6))
print(torch.ones([6]))

image.png

multinomial.Multinomial(10, fair_probs).sample()

image.png

multinomial.Multinomial(1000000, fair_probs).sample() / 1000000

image.png

''' 
这个地方的plot完全没有跟上次一样定义一大堆的内容,直接就是用的d2l.plt里面的plot,直接用就行。


问题1sample((500, ))这个500有什么用,为什么还要加个逗号
  由于sample()的参数必须是tuple()类型,因此要用(500, ),直接用(500)那就是整数,不是tuple元组
  一共做500次实验,每次实验掷色子10次,

问题2 cumsum有什么用,dim = 0代表什么
  dim这个东西之前遇到过,01横,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()

image.png

counts = multinomial.Multinomial(10, fair_probs).sample((500, ))
cum_counts = counts.cumsum(dim = 0)

counts[:20], counts.shape, cum_counts[:20], cum_counts.shape+

image.png

2.6.2 处理多个随机变量

问题1:贝叶斯定理
这个定理我现在不太能理解它的原理,但是公式可以背住。

问题2:边际化
所谓边际化,说人话就是把所有分组的概率加起来就是该事件的概率。

2.6.3 期望和方差

image.png

from torch.distributions import multinomial 
import torch 
from d2l import torch as d2l 

probability = torch.ones(6) / 6
probability

image.png

counts = multinomial.Multinomial(10, probability).sample((500, ))
cum_counts = counts.cumsum(dim = 0)
cum_counts[:20]

image.png

''' 
累积求和,求和,这两者是不一样的。
竖着是累积求和,就应该用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

image.png

image.png

A并B:

  • 上限:P(A) + P(B)
  • 下限:Max(P(A), P(B))

A交B

  • 上限: Min(P(A), P(B))
  • 下限:空集

image.png

image.png