pytroch学习笔记五————利用pytorch工具实现之前的简单的模型
上一次我们基本上算手敲了一个可以训练的线性模型出来,了解了学习的基本过程,很明显这略有些许麻烦,如果每个学习过程都需要我们去手动的求导,手动的调参则需要非常大的工作量,pytorch的部分功能可以帮我们省去这些工作,比如说自动求导
自动求导
若要自动求导,则在required_grad
属性设置为True即可,此时pytorch将会跟踪这个张量,任何对此张量进行操作都会对其的导数进行计算,并存入grad
属性里面,其本质操作相当于一个链式操作,即对每个计算操作产生的导数过程都储存在相应的grad
属性下
如下:
params=torch.tensor([1.0,0.0],requires_grad=True)
t_p=model(t_u,*params)
loss=loss_fn(t_p,t_c)
loss.backward()
print(params.grad)
backward()
操作即是对loss进行反向遍历,计算出所有对应参数的梯度值
需要注意的是,backward()
操作会将计算的结果与grad
属性的值进行累加,所以在开始运算时需要对其进行显式的清零
利用grad.zero_()
方法可以对其进行清零
所以自动训练的从头到尾的代码如下:
import torch
t_c = [0.5, 14.0, 15.0, 28.0, 11.0, 8.0, 3.0, -4.0, 6.0, 13.0, 21.0]
t_u = [35.7, 55.9, 58.2, 81.9, 56.3, 48.9, 33.9, 21.8, 48.4, 60.4, 68.4]
t_c = torch.tensor(t_c)
t_u = torch.tensor(t_u)
t_un=t_u*0.1
def model(t_u,w,b):
return w*t_u+b
def loss_fn(t_p,t_c):
squared_diffs=(t_p-t_c)**2
return squared_diffs.mean()
def training_loop(n_epochs,learning_rate,params,t_u,t_c):
for epoch in range(1,n_epochs+1):
if params.grad is not None:
params.grad.zero_()
t_p=model(t_u,*params)
loss=loss_fn(t_p,t_c)
loss.backward()
with torch.no_grad():
params-=learning_rate*params.grad
if(epoch%500==0):
print('Epoch %d,Loss %f'%(epoch,float(loss)))
return params
params=training_loop(n_epochs=5000,learning_rate=1e-2,
params=torch.tensor([1.0,0.0],requires_grad=True),
t_u=t_un,t_c=t_c)
print(params)
有时候在进行操作的时候我们不像让梯度产生更新,这时候就用with torch.no_grad():
更新封装在非梯度的上下文中,这意味着在with
模块,自动求导将不会起作用
优化器
上个代码范例中,我们手动敲了梯度下降的代码,就是with
里的代码,这种批量梯度下降的优化方法的代码似乎挺简单,但是有的优化方法的代码就比较复杂,pytorch对其进行了封装,并统称为优化器,存在torch.optim
模块当中
创建一个优化器的方法如下:
params=torch.tensor([1.0,0.0],requires_grad=True)
learning_rate=1e-2
optimizer=optim.SGD([params],lr=learning_rate)
优化器里的参数params是对张量对象的引用,所以对优化器里的参数梯度清零即是对整个参数梯度清零,对应的方法是optimizer.zero_grad()
;此外,让参数按照优化器里的方法进行更新的方法则是optimizer.step()
,用这两个方法,则就可以大大简化上面的代码,简化完成后的代码如下:
import torch
import torch.optim as optim
t_c = [0.5, 14.0, 15.0, 28.0, 11.0, 8.0, 3.0, -4.0, 6.0, 13.0, 21.0]
t_u = [35.7, 55.9, 58.2, 81.9, 56.3, 48.9, 33.9, 21.8, 48.4, 60.4, 68.4]
t_c = torch.tensor(t_c)
t_u = torch.tensor(t_u)
t_un=t_u*0.1
def model(t_u,w,b):
return w*t_u+b
def loss_fn(t_p,t_c):
squared_diffs=(t_p-t_c)**2
return squared_diffs.mean()
params=torch.tensor([1.0,0.0],requires_grad=True)
learning_rate=1e-2
optimizer=optim.SGD([params],lr=learning_rate)
def training_loop(n_epochs,optimizer,params,t_u,t_c):
for epoch in range(1,n_epochs+1):
t_p=model(t_u,*params)
loss=loss_fn(t_p,t_c)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if(epoch%500==0):
print('Epoch %d,Loss %f'%(epoch,float(loss)))
return params
params=training_loop(n_epochs=5000,optimizer=optimizer,
params=params,
t_u=t_un,t_c=t_c)
print(params)
这里我们用的是SGD,表示随机梯度下降方法,当然我们也可以尝试其他的优化器,比如说Adam,对于Adam优化器,不介绍过多的细节,它的学习率是自适应设置的,此外,它对参数放缩不太敏感,以至于我们可以使用原始的输入t_u,甚至可以将学习率提高到1e-1,代码如下:
import torch
import torch.optim as optim
t_c = [0.5, 14.0, 15.0, 28.0, 11.0, 8.0, 3.0, -4.0, 6.0, 13.0, 21.0]
t_u = [35.7, 55.9, 58.2, 81.9, 56.3, 48.9, 33.9, 21.8, 48.4, 60.4, 68.4]
t_c = torch.tensor(t_c)
t_u = torch.tensor(t_u)
t_un=t_u*0.1
def model(t_u,w,b):
return w*t_u+b
def loss_fn(t_p,t_c):
squared_diffs=(t_p-t_c)**2
return squared_diffs.mean()
params=torch.tensor([1.0,0.0],requires_grad=True)
learning_rate=1e-1
optimizer=optim.Adam([params],lr=learning_rate)
def training_loop(n_epochs,optimizer,params,t_u,t_c):
for epoch in range(1,n_epochs+1):
t_p=model(t_u,*params)
loss=loss_fn(t_p,t_c)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if(epoch%500==0):
print('Epoch %d,Loss %f'%(epoch,float(loss)))
return params
params=training_loop(n_epochs=5000,optimizer=optimizer,
params=params,
t_u=t_u,t_c=t_c)
print(params)
可以看出优化器的设计使得我们改变训练策略将会变得异常容易
训练验证和过拟合
课本上我们可以得到数据其实分为训练集和验证集,这里就不对其进行赘述了,这里就讲述一下分割数据集的方法
n_sample=t_u.shape[0]
n_val=int(0.2*n_sample)
shuffled_indices=torch.randperm(n_samples)
train_indices=shuffled_indices[:-n_val]
val_indices=shuffled_indices[-n_val:]
train_t_u=t_u[train_indices]
train_t_c=t_c[train_indices]
val_t_u=t_u[val_indices]
val_t_c=t_c[val_indices]
train_t_un=0.1*train_t_u
val_t_un=0.1*val_t_u
torch.randperm(n):将0~n-1(包括0和n-1)随机打乱后获得的数字序列,函数名是random permutation缩写
因此我们有了训练集和测试集,即可判断是否过拟合(训练集的loss小于测试集的loss),修改过后的代码如下:
def training_loop(n_epochs, optimizer, params, train_t_u, val_t_u,
train_t_c, val_t_c):
for epoch in range(1, n_epochs + 1):
train_t_p = model(train_t_u, *params)
train_loss = loss_fn(train_t_p, train_t_c)
val_t_p = model(val_t_u, *params)
val_loss = loss_fn(val_t_p, val_t_c)
optimizer.zero_grad()
train_loss.backward()
optimizer.step()
if epoch <= 3 or epoch % 500 == 0:
print(f"Epoch {epoch}, Training loss {train_loss.item():.4f},"
f" Validation loss {val_loss.item():.4f}")
return params
params = torch.tensor([1.0, 0.0], requires_grad=True)
learning_rate = 1e-2
optimizer = optim.SGD([params], lr=learning_rate)
training_loop(
n_epochs = 3000,
optimizer = optimizer,
params = params,
train_t_u = train_t_un,
val_t_u = val_t_un,
train_t_c = train_t_c,
val_t_c = val_t_c)
正如教科书上那班,我们希望只在训练集上对参数进行训练而不是在测试集上,上述代码虽然不会在测试集上对参数进行训练,但是所对应的grad
属性毫无疑问会依旧进行计算,我们想人为的关闭这个功能以释放额外的开销,就要用到之前用过的上下文管理器torch.no_grad()
进行处理,代码如下:
def training_loop(n_epochs, optimizer, params, train_t_u, val_t_u,
train_t_c, val_t_c):
for epoch in range(1, n_epochs + 1):
train_t_p = model(train_t_u, *params)
train_loss = loss_fn(train_t_p, train_t_c)
with torch.no_grad():
val_t_p = model(val_t_u, *params)
val_loss = loss_fn(val_t_p, val_t_c)
assert val_loss.requires_grad == False
optimizer.zero_grad()
train_loss.backward()
optimizer.step()
还有set_grad_enabled()
方法,可以使用一个布尔表达式设定代码运行时启用或禁用求导的条件,典型的条件是我们是在训练模式下运行还是推理条件下运行,其使用例子如下:
def calc_forward(t_u, t_c, is_train):
with torch.set_grad_enabled(is_train):
t_p = model(t_u, *params)
loss = loss_fn(t_p, t_c)
return loss