Tensor 与 Autograd
神经网络中重要的内容就是进行参数学习,参数学习就离不开求导。torch.autograd 包来进行自动求导。
自动求导要点
看不懂。。。。为啥别人都说这本书好呢。有好多错别字啊,,
梯度的本意是一个向量(矢量),表示某一函数在该点处的方向导数沿着该方向取得最大值,即函数在该点处沿着该方向(此梯度的方向)变化最快,变化率最大(为该梯度的模)。
看不懂的可以看看下面的在回来看看。。
-
创建叶子节点(Leaf Node) 的Tensor,使用requires_grad参数指定是否记录对其的操作,以便之后利用 backward() 方法进行导数求解,requires_grad 参数默认False,需要对其求导就设置为True,然后与之有关系的节点会自动变为True。
-
可以使用 requires_grad_(bool) 方法修改Tensor 的requires_grad 属性,调用 .detach() or with torch.no_grad(): ,就不会再计算张量的梯度,跟踪张量的历史记录。这点在评估模型,测试模型阶段常用。
-
同运算创建的非叶子节点 的Tensor ,会自动赋予 grad_fn 属性,表示梯度函数。叶子节点的这个属性是None。
-
最后得到的Tensor执行backward()函数,此时自动计算各变量的梯 度,并将累加结果保存到grad属性中。计算完成后,非叶子节点的梯度自动 释放。
-
backward()函数接收参数,该参数应和调用backward()函数的Tensor 的维度相同,或者是可broadcast(广播)的维度。如果求导的Tensor为标量(即一个 数字),则backward中的参数可省略
-
反向传播的中间缓存会被清空,如果需要进行多次反向传播,需要 指定backward中的参数retain_graph=True。多次反向传播时,梯度是累加 的。
-
非叶子节点的梯度backward调用后即被清空。
-
可以通过用torch.no_grad()包裹代码块的形式来阻止autograd去跟踪 那些标记为.requesgrad=True的张量的历史记录。这步在测试阶段经常使 用。
计算图,有向无环图(DAG),表示算子与变量的关系,就是 z = wx + b。其中用户创建的变量就是叶子节点,这里就是 w b x 。为了计算叶子节点的梯度,将 requires_grad 设置为True,然后就可以自动跟踪记录。y z 是计算来的变量,是非叶子节点,z 是根节点。mul add 就是算子(函数)。
我们的目标是更新个叶子节点的梯度。下面就是各叶子节点的梯度的计算:
还记得吴恩达课程里的神经网络嘛,这个反向传播计算的就是代价函数的导数部分,每一个权重的Δ。。然后就可以梯度下降啦,,进行优化,,这里不需要对 x 的导。
使用 backward() 方法反向传播,自动计算个节点的梯度。利用导数的链式法则,计算叶子节点的梯度,梯度值累加到 grad 属性中,对于非叶子节点的函数操作 记录在grad_fn 属性中,叶子节点的grad_fn 值为None。
具体的过程可以看看吴恩达的神经网络的反向传播,感觉那个还是很好理解的。。。
标量的反向传播
# 看着可能很乱,但思路还是很清晰的。。。
x=torch.Tensor([2])
print('输入张量x:',x)
w=torch.randn(1,requires_grad=True) # 可以使用w.requires_grad_(False) 修改这个属性
b=torch.randn(1,requires_grad=True) # 记录求导,并且与之有依赖关系的节点会自动变成True
y=torch.mul(w,x) #等价于w*x
z=torch.add(y,b) #等价于y+b
print('随机的权重参数w,偏移量b:,结果z:\n',w,b,z)
print("x,w,b的require_grad属性分别为:{},{},{}".format(x.requires_grad,w.requires_grad,b.requires_grad))
>>>
输入张量x: tensor([2.])
随机的权重参数w,偏移量b:,结果z:
tensor([0.8357], requires_grad=True) tensor([0.4299], requires_grad=True) tensor([2.1012], grad_fn=<AddBackward0>)
x,w,b的require_grad属性分别为:False,True,True
#查看非叶子节点的requres_grad属性,
print("y,z的requires_grad属性分别为:{},{}".format(y.requires_grad,z.requires_grad))
#因与w,b有依赖关系,故y,z的requires_grad属性也是:True,True
>>> y,z的requires_grad属性分别为:True,True
#查看各节点是否为叶子节点
print("x,w,b,y,z的是否为叶子节点:{},{},{},{},{}".format(x.is_leaf,w.is_leaf,b.is_leaf,y.is_leaf,z.is_leaf))
>>> x,w,b,y,z的是否为叶子节点:True,True,True,False,False
#查看叶子节点的grad_fn属性,这个属性就是梯度函数。
print("x,w,b的grad_fn属性:{},{},{}".format(x.grad_fn,w.grad_fn,b.grad_fn))
#因x,w,b为用户创建的,为通过其他张量计算得到,故x,w,b的grad_fn属性:None,None,None
>>> x,w,b的grad_fn属性:None,None,None
#查看非叶子节点的grad_fn属性
print("y,z的是否为叶子节点:{},{}".format(y.grad_fn,z.grad_fn))
#y,z的是否为叶子节点:<MulBackward0 object at 0x7f923e85dda0>,<AddBackward0 object at 0x7f923e85d9b0>
#基于z张量进行梯度反向传播,执行backward之后计算图会自动清空,
#如果需要多次使用backward,需要修改参数retain_graph为True,此时梯度是累加的
#z.backward(retain_graph=True)
z.backward()
#查看叶子节点的梯度,x是叶子节点但它无需求导,故其梯度为None
print("参数w,b,x的梯度分别为:{},{},{}".format(w.grad,b.grad,x.grad)) # 应该是 z 对 w,b 的导
>>> 参数w,b,x的梯度分别为:tensor([2.]),tensor([1.]),None
#非叶子节点的梯度,执行backward之后,会自动清空
print("非叶子节点y,z的梯度分别为:{},{}".format(y.grad,z.grad))
>>>非叶子节点y,z的梯度分别为:None,None
z.backward(retain_graph=True)
print("参数w,b的梯度分别为:{},{},{}".format(w.grad,b.grad,x.grad))
print("非叶子节点y,z的梯度分别为:{},{}".format(y.retain_grad(),z.retain_grad()))
参数w,b的梯度分别为:tensor([4.]),tensor([2.]),None # 这里经过了累加。应该就是简单的一次两次的累加
非叶子节点y,z的梯度分别为:None,None
脑部链接。。更复杂的。加深对反向传播的理解,,,
非标量的反向传播
一般我们的目标都是标量,比如损失值,但也会碰到非标量的情况,这是就需要对非标量进行反向传播。pytorch 规定只能进行标量与张量的求导。这是就需要给backward() 传入一个grandient。将张量对张量的求导转换为标量对张量的求导。
意思就是:目标 loss=(y1,y2,ym) , 传入的grandient参数v =(v1,vm) , 对Loss的求导就可以转化为对 loss*v^T 标量的求导。
backward(gradient=None, retain_graph=None, create_graph=False)
#定义叶子节点张量x,形状为1x2
x= torch.tensor([[2, 3]], dtype=torch.float, requires_grad=True)
#初始化Jacobian矩阵 雅克比矩阵 就是y 对 x的梯度
J= torch.zeros(2 ,2)
#初始化目标张量,形状为1x2
y = torch.zeros(1, 2)
#定义y与x之间的映射关系:
y[0, 0] = x[0, 0] ** 2 + 3 * x[0 ,1]
y[0, 1] = x[0, 1] ** 2 + 2 * x[0, 0]
手算一下:y 对 x的梯度是一个雅可比矩阵。
这个 J 就是 y 对 x 的梯度。
自动算一下:要分成两步进行,一次 v=(1,0) 得到 y1 对 x 的梯度,然后使 v=(0,1) 得到 y2 对 x 的梯度。这里 x 就是那个张量,y1,y2 就是那个标量嘛。。。感觉就是分开进行。
#生成y1对x的梯度
y.backward(torch.Tensor([[1, 0]]),retain_graph=True)
J[0]=x.grad
#梯度是累加的,故需要对x的梯度清零
x.grad = torch.zeros_like(x.grad)
#生成y2对x的梯度
y.backward(torch.Tensor([[0, 1]])) # 填入 v
J[1]=x.grad
#显示jacobian矩阵的值
print(J)
使用 Numpy 实现机器学习
import numpy as np
from matplotlib import pyplot as plt
np.random.seed(100)
x = np.linspace(-1, 1, 100).reshape(100,1) # y=3x**2 + 2 随便加一些噪声
y = 3*np.power(x, 2) +2+ 0.2*np.random.rand(x.size).reshape(100,1)
plt.scatter(x, y)
plt.show()
# 随机初始化 参数 w,b
w1 = np.random.rand(1,1)
b1 = np.random.rand(1,1)
lr =0.001 # 学习率
for i in range(800):
# 前向传播
y_pred = np.power(x,2)*w1 + b1
# 定义损失函数
loss = 0.5 * (y_pred - y) ** 2
loss = loss.sum()
#计算梯度
grad_w=np.sum((y_pred - y)*np.power(x,2)) # 对 w 的导
grad_b=np.sum((y_pred - y))
#使用梯度下降法,是loss最小
w1 -= lr * grad_w
b1 -= lr * grad_b
plt.plot(x, y_pred,'r-',label='predict')
plt.scatter(x, y,color='blue',marker='o',label='true') # true data
plt.xlim(-1,1)
plt.ylim(2,6)
plt.legend()
plt.show()
print(w1,b1)
>>> [[2.98927619]] [[2.09818307]]
使用 Tensor 及 Antograd 实现机器学习
import torch as t
import matplotlib.pyplot as plt
t.manual_seed(100)
dtype = t.float
x=t.unsqueeze(t.linspace(-1,1,100),dim=1)
y=3*x.pow(2) + 2+0.2 * t.rand(x.size())
w = t.randn(1,1,dtype=dtype,requires_grad = True) # w , b 需要进行学习,属性设置为True
b = t.randn(1,1,dtype=dtype, requeires_grad = True)
lr = 0.001
for ii in range(800):
y_pred = x.pow(2).mm(w) + b
loss = 0.5 * (y_pred -y) **2
loss = loss.sum()
loss.backward() # 自动求得 loss 对 w,b 的梯度
with t.no_grad(): # 手动更新参数,是上下文环境中切断自动求导的计算。
w -= lr * w.grad # 不在计算张量的梯度,跟踪张量的历史记录
b -= lr * b.grad
w.grad.zero_() # 梯度清零,每次下降都要更新的
b.grad.zero_()
plt.plot(x.numpy(), y_pred.detach().numpy(),'r-',label='predict')#predict
plt.scatter(x.numpy(), y.numpy(),color='blue',marker='o',label='true') # true data
plt.xlim(-1,1)
plt.ylim(2,6)
plt.legend()
plt.show()
print(w, b)
>>>
(tensor([[2.9648]], requires_grad=True),
tensor([[2.1145]], requires_grad=True))
这个 with t.no_grad():
感觉好难理解啊。一般来说对 tensor 的操作会默认生成计算图的,以便来反向传播。但是有些操作我们是不需要反向(不生成计算图),可以打印过程中的 y, loss看看他们的 grad_fn 属性,MulBackward0 啥的。如果你不加这个 no_grad() ,后面的 w,b 的减法操作就会生成计算图,然后在反向中就会进行计算。(虽然不会生成计算图,但实际的计算还是会进行的),这个有节约显存的作用吧。。没试过。验证集,我们只是想看一下训练的效果,并不是想通过验证集来更新网络时,就可以使用with torch.no_grad()。
按理来说加不加这句的结果应该相同,但针对这个例子就会报错。。因为进行了一个 w-= 的赋值操作,好像就是为了防止给梯度赋值一个无穷大吧,,好难啊。。。
使用TensorFlow 框架
import tensorflow.compat.v1 as tf # 我的是2.3.1,用第一版的API 就这么搞
tf.disable_v2_behavior()
import numpy as np
np.random.seed(100)
x=np.linspace(-1,1,100).reshape(100,1)
y=3*np.power(x,2) + 2+ 0.2*np.random.rand(x.size).reshape(100,1)
x1 = tf.placeholder(tf.float32,shape = (None,1))
y1 = tf.placeholder(tf.float32,shape = (None,1))
# 创建两个占位符,用来存放输入数据x,目标值 y ,在运行计算图是导入数据
w = tf.Variable(tf.random_uniform([1],0,1.0))
b = tf.Variable(tf.zeros([1]))
# 创建权重变量 w,b 并随机化,tf 的变量在整个计算图中保存其值。
y_pred = np.power(x,2) *w +b # 前向传播,计算预测值
loss = tf.reduce_mean(tf.square(y-y_pred)) # 计算损失值
grad_w,grad_b = tf.gradients(loss,[w,b]) # 计算有关参数 w,b 关于损失函数的梯度
#使用梯度下降法更新参数,执行计算图是给 new_w1, new_w2 赋值,
# 对 tf 来说更新参数是计算图的一部分内容,在pytroch 中这部分属于计算图之外
learning_rate = 0.01
new_w = w.assign(w - learning_rate * grad_w)
new_b = b.assign(b - learning_rate * grad_b)
# 已构建计算图,创建 session 执行计算
with tf.Session() as sess:
# 执行前初始化变量 w,b
sess.run(tf.global_variables_initializer())
for step in range(2000):
# 循环执行计算图,每次把 x1, y1 赋值给 x,y
# 每次执行计算图,需要计算关于 new_w new_b 的损失值,返回numpy的数组
loss_value, v_w,v_b = sess.run([loss,new_w,new_b],feed_dict = {x1:x,y1:y})
if step%200 == 0: # 200次输出一次结果
print("损失值、权重、偏移量分别为{:.4f},{},{}".format(loss_value,v_w,v_b))
>>>
损失值、权重、偏移量分别为9.5711,[0.39555296],[0.05977444]
损失值、权重、偏移量分别为0.1316,[1.816024],[2.5013514]
损失值、权重、偏移量分别为0.0692,[2.1526716],[2.4078143]
损失值、权重、偏移量分别为0.0375,[2.387772],[2.321037]
损失值、权重、偏移量分别为0.0210,[2.556913],[2.2583735]
损失值、权重、偏移量分别为0.0125,[2.6786542],[2.2132683]
损失值、权重、偏移量分别为0.0081,[2.7662807],[2.1808016]
损失值、权重、偏移量分别为0.0058,[2.8293512],[2.1574354]
损失值、权重、偏移量分别为0.0046,[2.8747463],[2.1406155]
损失值、权重、偏移量分别为0.0040,[2.9074223],[2.1285102]