前言
在用Pytorch包编写人工智能代码时最常用的操作就是用梯度去更新参数,也就是
w
=
w
−
l
r
∗
w
.
g
r
a
d
w=w-lr*w.grad
w=w−lr∗w.grad
这个赋值操作很有可能会导致变量的id发生变化,进而给Pytorch中的loss函数的计算带来困惑。还好在Pytorch(一)中我们已经了解到了tensor变量的绝缘与不绝缘,所以接下来用代码来演示变量id发生变化时的处理办法以及如何保持参数id不变。
用Pytorch实现线性回归
假设我们有生成的线性回归训练数据,从数据中学习出最佳的参数,这就是线性回归。接下来从tensor变量的id变化与不变化两种角度给出对应的Pytorch代码实现指导。
参数id发生变化
#导入模块
import torch
from matplotlib import pyplot as plt
torch.set_default_tensor_type(torch.cuda.FloatTensor)
#随机数生成种子
torch.manual_seed(1)
#生成训练用数据
w_0=torch.randn(2)
b_0=torch.randn(1)
print(f"w_0,w_0.dtype,w_0.shape,b_0,b_0.dtype,b_0.shape:{w_0,w_0.dtype,w_0.shape,b_0,b_0.dtype,b_0.shape}")
print("-----------------------------------------------------------")
x_0=torch.randn(10,2)
y_0=torch.mv(x_0,w_0)+b_0
print(f"x_0,x_0.dtype,x_0.shape,y_0.dtype,y_0,y_0.shape:{x_0,x_0.dtype,x_0.shape,y_0.dtype,y_0,y_0.shape}")
print("-----------------------------------------------------------")
#设置模型:步长、待训练参数等
lr=torch.tensor(0.01)
w=torch.randn(2)
w.requires_grad_(True)
b=torch.randn(1)
b.requires_grad_(True)
print(f"w,b:{w,b}")
print("-----------------------------------------------------------")
#先训练十轮,看看效果,后面一千轮再逐渐达到精度要求。
for i in range(10):
loss=((torch.mv(x_0,w)+b-y_0)**2).sum()
print(f"loss:{loss}")
loss.backward()
print(f"w.grad,b.grad:{w.grad,b.grad}")
print(id(w),id(b),id(w.data),id(b.data),id(w.detach()),id(b.detach()))
w=w.data-lr*w.grad.data
b=b.data-lr*b.grad.data
print(f"w,b:{w},{b}")
w.requires_grad_(True)
b.requires_grad_(True)
print("-----------------------------------------------------------")
#这一千轮静默输出,最终结果单独查看
for i in range(1000):
loss=((torch.mv(x_0,w)+b-y_0)**2).sum()
loss.backward()
w=w.data-lr*w.grad.data
b=b.data-lr*b.grad.data
w.requires_grad_(True)
b.requires_grad_(True)
print(f"w,b,w_0,b_0:{w,b,w_0,b_0}")
print(f"loss:{loss}")
在上述代码中tensor变量的id会发生变化,所以我们重新计算loss的表达式的值。同时由于参数变量每次迭代中都是一个新的tensor变量,所以需要手动再加上求导标志requires_grad
。
参数id不变化
#导入模块
import torch
from matplotlib import pyplot as plt
torch.set_default_tensor_type(torch.cuda.FloatTensor)
#随机数生成种子
torch.manual_seed(1)
#生成训练用数据
w_0=torch.randn(2)
b_0=torch.randn(1)
print(f"w_0,w_0.dtype,w_0.shape,b_0,b_0.dtype,b_0.shape:{w_0,w_0.dtype,w_0.shape,b_0,b_0.dtype,b_0.shape}")
print("-----------------------------------------------------------")
x_0=torch.randn(10,2)
y_0=torch.mv(x_0,w_0)+b_0
print(f"x_0,x_0.dtype,x_0.shape,y_0.dtype,y_0,y_0.shape:{x_0,x_0.dtype,x_0.shape,y_0.dtype,y_0,y_0.shape}")
print("-----------------------------------------------------------")
#设置模型:步长、待训练参数等
lr=torch.tensor(0.01)
w=torch.randn(2)
w.requires_grad_(True)
b=torch.randn(1)
b.requires_grad_(True)
print(f"w,b:{w,b}")
print("-----------------------------------------------------------")
for i in range(10):
loss=((torch.mv(x_0,w)+b-y_0)**2).sum()
print(f"loss:{loss}")
loss.backward()
print(f"w.grad,b.grad:{w.grad,b.grad}")
print(id(w),id(b),id(w.data),id(b.data),id(w.detach()),id(b.detach()))
w.data=w.data-lr*w.grad.detach()
b.data=b.data-lr*b.grad.detach()
#也可以使用
#w.data=w-lr*w.grad
#b.data=b-lr*b.grad
#也可以使用
#w.data=w.data-lr*w.grad
#b.data=b.data-lr*b.grad
#也可以使用
#w.data=w.clone()-lr*w.grad.clone()
#b.data=b.clone()-lr*b.grad.clone()
print(f"w,b:{w},{b}")
w.grad.data.zero_()
b.grad.data.zero_()
print("-----------------------------------------------------------")
for i in range(1000):
loss=((torch.mv(x_0,w)+b-y_0)**2).sum()
loss.backward()
w.data=w.data-lr*w.grad.data
b.data=b.data-lr*b.grad.data
#也可以使用
#w.data=w-lr*w.grad
#b.data=b-lr*b.grad
#也可以使用
#w.data=w.data-lr*w.grad
#b.data=b.data-lr*b.grad
#也可以使用
#w.data=w.clone()-lr*w.grad.clone()
#b.data=b.clone()-lr*b.grad.clone()
w.grad.data.zero_()
b.grad.data.zero_()
print(f"w,b:{w,b}")
print(f"loss:{loss}")
其实总结一下就是当通过.data属性更新参数时,参数id不变,此时需要手动将参数的梯度归零,避免累积。当通过直接赋值来更新参数时,参数id变化,此时需要对参数再设置一下需要梯度。至于等号右边的赋值表达式是什么倒是无关紧要,不管什么表达式,都是绝缘的。都不会产生映射现象。
用Pytorch实现感知机
#导入模块
import torch
from matplotlib import pyplot as plt
torch.set_default_tensor_type(torch.cuda.FloatTensor)
#生成数据
w_0=torch.randn(3)
b_0=torch.randn(1)
x_0=torch.randn(10,3)
y_0=torch.where(torch.mv(x_0,w_0)+b_0>0,1,-1)
print(f"w_0,b_0,x_0,y_0:{w_0,b_0,x_0,y_0}")
#使用感知机进行分类。
lr=torch.tensor(0.01)
w=torch.randn(3)
b=torch.randn(1)
w.requires_grad_(True)
b.requires_grad_(True)
loss=torch.exp(-(torch.mv(x_0,w)+b)*y_0).sum()
while (loss>0.1):
loss=torch.exp(-(torch.mv(x_0,w)+b)*y_0).sum()
loss.backward()
w.data=w-lr*w.grad
b.data=b-lr*b.grad
w.grad.data.zero_()
b.grad.data.zero_()
print(f"w,b,loss:{w,b,loss}")
print(f"w_0,b_0,w,b,loss:{w_0,b_0,w,b,loss}")
print(f"y_0,(torch.mv(x_0,w)+b):{y_0,torch.mv(x_0,w)+b}")
其实这个loss函数设置的有问题,并且本程序中没有考虑参数向量的长度问题,因为当w,b
确定以后,2w,2b
也一定适合原问题。再说回loss函数的问题,假设所有10个样本都分类正确,也就是
−
(
w
∗
x
0
+
b
)
∗
y
0
≤
0
-(w*x_0+b)*y_0\le0
−(w∗x0+b)∗y0≤0
此时loss函数的值一定小于10,而我设置的停止目标是0.1,呜呜呜,我是大恶人,刚开始我还设置的是0.0001,我说我电脑怎么呜呜叫呢,那不是风扇的声音,是它在哭😭,本表情来自ubuntu系统自带的字符应用中。还有更多。所以loss只要小于10就可以了。而我加大了很多工作量。其实10也不是很够用,因为可能会有边沿情况,能小尽小最好。
那么问题来了,损失函数写多少能够确保无错误分类的情况存在呢?下面的只是粗略估计,并非严格论证。我们知道错误分类是由于有些数据对
(
x
,
y
)
(x,y)
(x,y)所对应的损失过于小,所以才给那些错误分类的点留足了错误空间,而我们只要把所有的错误空间考虑在内一定不会误分类。假设其他带来的损失都足够小,接近为0,不妨设其他九个损失都是0,那剩余的一个点容许的误分类损失为1。
通过分析发现当误差趋于稳定时,所得的分类面基本就是正确的了,所以我修改了代码。
#导入模块
import torch
from matplotlib import pyplot as plt
torch.set_default_tensor_type(torch.cuda.FloatTensor)
#生成数据
w_0=torch.randn(3)
b_0=torch.randn(1)
x_0=torch.randn(10,3)
y_0=torch.where(torch.mv(x_0,w_0)+b_0>0,1,-1)
print(f"w_0,b_0,x_0,y_0:{w_0,b_0,x_0,y_0}")
#使用感知机进行分类。
lr=torch.tensor(0.01)
w=torch.randn(3)
b=torch.randn(1)
w.requires_grad_(True)
b.requires_grad_(True)
loss=torch.exp(-(torch.mv(x_0,w)+b)/torch.sqrt((w*w).sum())*y_0).sum()
temp=loss.clone()+1
while True:
loss=torch.exp(-((torch.mv(x_0,w)+b)/torch.sqrt((w*w).sum()))*y_0).sum()
loss.backward()
if(temp-loss.clone()<0.001):
break
temp=loss.clone()
w.data=w-lr*w.grad
b.data=b-lr*b.grad
w.grad.data.zero_()
b.grad.data.zero_()
print(f"w,b,loss:{w,b,loss}")
print(f"w_0,b_0,w,b,loss:{w_0,b_0,w,b,loss}")
print(f"y_0,(torch.mv(x_0,w)+b):{y_0,torch.mv(x_0,w)+b}")