目录
回归问题
数据生成(包含数据集划分)
梯度下降
步骤一:计算损失
步骤二:计算梯度
步骤三:更新参数
步骤四:重复上述步骤(针对不同情况的每个epoch的参数更新量N/一次训练传入的数据集大小)
线性回归的Numpy实现(手动计算梯度+手动更新参数+手动定义损失函数)
PyTorch
Tensor(张量)
数据载入、设备和CUDA
创建参数
Autograd(自动计算梯度包)
动态计算图
Optimizer(自动更新参数)
损失(自动调用损失函数)
模型定义(init定义操作+foward计算输出)
嵌套模型
序贯模型
单步训练函数(fullbatch)
Dataset(元组列表:特征,标签;方便进行数据划分dataloader-minibatch;在CPU上的处理)
DataLoader(训练集/验证集数据切片-minibatch;元组:特征,标签)
验证(设定验证模式;不需要计算梯度)
完整的代码实现
转载
本文翻译自towardsdatascience上非常火爆的PyTorch介绍
https://towardsdatascience.com/understanding-pytorch-with-an-example-a-step-by-step-tutorial-81fc5f8c4e8etowardsdatascience.com
作者同时也正在出书
Deep Learning with PyTorch Step-by-Step: A Beginner’s Guideleanpub.com
希望能帮助到有需要的人。
本篇文章主要涉及以下方面内容:
考虑一个只有一个特征 x 的回归问题
假设模型参数的真实值为 a=1,b=2 ,噪声为高斯白噪声,生成 100个样本数据
np.random.seed(42)
x = np.random.rand(100,1)
y = 1 + 2*x + .1*np.random.rand(100,1)
将数据集分为训练集和验证集,将次序打乱并将前 80 个样本用于训练
# 打乱次序
idx = np.arange(100)
np.random.shuffle(idx)
# 前80个样本用于训练
train_idx = idx[:80]
val_idx = idx[80:]
# 创建训练集和验证集
x_train, y_train = x[train_idx], y[train_idx]
x_val, y_val = x[val_idx], y[val_idx]
接下来我们来看看如果利用梯度下降法来学习模型参数。
需要注意:如果我们将所有( N个)训练数据都用于计算损失,则是批(batch)梯度下降;如果只用一个点来计算损失,则是随机梯度下降(SGD);如果数据点介于 1 到N 则是最小(mini-batch)梯度下降。
带入模型后
一个梯度就是关于一个参数的偏导数,因为我们这里有两个参数 a和 b,因此需要计算两个偏导数。这里需要利用到链式法则
我们利用梯度来更新参数,由于我们要最小化损失,所以参数的更新方向需要往负梯度方向进行。
其中 n 为学习率。
采用更新后的参数并回到步骤一重复以上过程。
首先随机初始化参数 a,b
np.random.seed(42)
a = np.random.randn(1)
b = np.random.randn(1)
设置学习率和epoch数目
lr = 1e-1
n_epochs = 1000 # 全部数据需要用来训练几次
在每个epoch中执行
for epoch in range(n_epochs):
# 计算模型的预测
yhat = a + b * x_train
# 计算模型的预测误差
error = (y_train - yhat)
# 计算损失(MSE),对应公式(3)
loss = (error ** 2).mean()
# 计算梯度,对应公式(4)
a_grad = -2 * error.mean()
b_grad = -2 * (x_train * error).mean()
# 更新模型参数,对应公式(5)
a = a - lr * a_grad
b = b - lr * b_grad
我们也可以调用 Scikit-Learn 的线性回归来拟合模型然后比较我们梯度下降得到的参数和 Scikit-Learn 得到的是否一致
from sklearn.linear_model import LinearRegression
linr = LinearRegression()
linr.fit(x_train, y_train)
print(a, b)
print(linr.intercept_, linr.coef_[0])
是时候进入PyTorch了!
首先我们要熟悉一些基础的概念,比如说
张量其实就是任意维度的矩阵,比如标量就是零维张量,向量就是一维张量,矩阵就是二维张量。
我们可以直接将numpy中的array转化为张量,并定义为是GPU中的张量,还是CPU中的张量(这里的设备指的是CPU或GPU)
import torch
import torch.optim as optim
import torch.nn as nn
from torchviz import make_dot
# 如果有GPU则用GPU,没有则用CPU
device = 'cuda' if torch.cuda.is_available() else 'cpu'
# 将Numpy的array转化为tensor并指定设备
x_train_tensor = torch.from_numpy(x_train).float().to(device)
y_train_tensor = torch.from_numpy(y_train).float().to(device)
# 观察它们类别的不同
print(type(x_train), type(x_train_tensor), x_train_tensor.type())
可以看到第一个的类别是numpy.ndarray,第二个的类别是torch.Tensor。
同样也可以将张量转化为Numpy中的array,使用指令x_train_tensor.numpy()即可,需要注意的是如果是GPU中的张量需要先(用"cpu()")转为CPU型张量再进一步转化。
了解张量后我们需要将需要学习的参数创建成张量的形式,并声明是否需要计算梯度,同时也可以声明设备。
# 随机初始化参数,并声明需要计算梯度:REQUIRES_GRAD = TRUE,tensor的位置。
torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
print(a, b)
Autograd 是 PyTorch 的自动微分包,可以帮我们自动计算出所有梯度。
lr = 1e-1
n_epochs = 1000
torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
for epoch in range(n_epochs):
yhat = a + b * x_train_tensor
error = y_train_tensor - yhat
loss = (error ** 2).mean()
# 不需要再手动计算梯度了
# a_grad = -2 * error.mean()
# b_grad = -2 * (x_tensor * error).mean()
# 直接在loss后调用计算梯度指令
loss.backward()
# 计算出来的梯度
print(a.grad)
print(b.grad)
# 参数的更新
# 需要声明不需要涉及动态图的运算
with torch.no_grad():
a -= lr * a.grad
b -= lr * b.grad
# 将梯度清空
a.grad.zero_()
b.grad.zero_()
print(a, b)
PyTorchViz 工具包的 make_dot(variable) 方法能够让我们看到相应变量的计算图。
torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
yhat = a + b * x_train_tensor
error = y_train_tensor - yhat
loss = (error ** 2).mean()
如果我们调用 make_dot(yhat),可以看到
其中
动态图的一个优点是我们可以根据自己的需求设计梯度的计算方向,比如
yhat = a + b * x_train_tensor
error = y_train_tensor - yhat
loss = (error ** 2).mean()
# 有两个分叉
if loss > 0:
yhat2 = b * x_train_tensor
error2 = y_train_tensor - yhat2
loss += error2.mean()
对应的计算图为
现在你能够利用 PyTorch 自动计算出来梯度并手动更新参数了,但是如果有一堆的参数,我们可以利用 PyTorch 的优化器,比如 SGD 或者 Adam,来更加高效地更新参数。
torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
print(a, b)
lr = 1e-1
n_epochs = 1000
# 定义随机梯度下降优化器用来优化参数
optimizer = optim.SGD([a, b], lr=lr)
for epoch in range(n_epochs):
yhat = a + b * x_train_tensor
error = y_train_tensor - yhat
loss = (error ** 2).mean()
loss.backward()
# 不需要再手动更新了
# with torch.no_grad():
# a -= lr * a.grad
# b -= lr * b.grad
optimizer.step()
# 不需要再清零梯度
# a.grad.zero_()
# b.grad.zero_()
optimizer.zero_grad()
print(a, b)
PyTorch覆盖了大部分我们可能用到的损失函数,对于回归问题我们采用的是MSE,定义了损失函数我们就没必要自己计算损失了。
torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
print(a, b)
lr = 1e-1
n_epochs = 1000
# 定义MSE损失函数
loss_fn = nn.MSELoss(reduction='mean')
optimizer = optim.SGD([a, b], lr=lr)
for epoch in range(n_epochs):
yhat = a + b * x_train_tensor
# 不需要自己计算损失了
# error = y_tensor - yhat
# loss = (error ** 2).mean()
loss = loss_fn(y_train_tensor, yhat)
loss.backward()
optimizer.step()
optimizer.zero_grad()
print(a, b)
接下来我们再看看如何利用模型进行预测。
在 PyTorch 中模型用 python 中的类(class)表示
最基本的方法是
class ManualLinearRegression(nn.Module):
def __init__(self):
super().__init__()
# 在模型里我们将参数包装为 nn.Parameter
self.a = nn.Parameter(torch.randn(1, requires_grad=True, dtype=torch.float))
self.b = nn.Parameter(torch.randn(1, requires_grad=True, dtype=torch.float))
def forward(self, x):
# 计算预测
return self.a + self.b * x
注意:我们需要将模型和数据放在同一设备中(GPU或CPU)。
torch.manual_seed(42)
# 创建模型并指定设备
model = ManualLinearRegression().to(device)
# 使用 state_dict 看参数
print(model.state_dict())
lr = 1e-1
n_epochs = 1000
loss_fn = nn.MSELoss(reduction='mean')
optimizer = optim.SGD(model.parameters(), lr=lr)
for epoch in range(n_epochs):
# 将模型设置为训练模式!
model.train()
# 不需要人工进行预测
# yhat = a + b * x_tensor
yhat = model(x_train_tensor)
loss = loss_fn(y_train_tensor, yhat)
loss.backward()
optimizer.step()
optimizer.zero_grad()
print(model.state_dict())
注意:上面的 model.train() 并不是用来训练模型的,它只是将模型设置为训练模式。因为有的模型在训练和评价阶段采用的机制不一样,比如训练过程有 Dropout 操作。
上面的模型中我们自己定义了两个参数作为线性回归的参数,我们也可以使用 PyTorch 的 Linear 模型作为我们模型的属性。在 __init__ 方法中我们需要增加一个属性来表示线性模型
class LayerLinearRegression(nn.Module):
def __init__(self):
super().__init__()
# 创建一个单入单出的线性层
self.linear = nn.Linear(1, 1)
def forward(self, x):
# 计算输出时只需要调用这一层,不需要用forward
return self.linear(x)
对于一些深度模型,一层的输出作为下一层的输入,我们可以采用序贯 Sequential 模型,比如线性模型可以看作只有一个模型组成的序贯模型
model = nn.Sequential(nn.Linear(1, 1)).to(device)
目前为止我们定义了优化器、损失函数和模型。可以将它们整合成一个函数进行一步训练。
def make_train_step(model, loss_fn, optimizer):
# 定义单步训练函数
def train_step(x, y):
# 将模型设置为训练模式
model.train()
# 进行预测
yhat = model(x)
# 计算损失
loss = loss_fn(y, yhat)
# 计算梯度
loss.backward()
# 更新参数并清零梯度
optimizer.step()
optimizer.zero_grad()
# 返回损失
return loss.item()
# 返回单步训练函数
return train_step
# 根据模型、损失函数和优化器创建单步训练函数
train_step = make_train_step(model, loss_fn, optimizer)
losses = []
# For each epoch...
for epoch in range(n_epochs):
# 单步训练并返回损失
loss = train_step(x_train_tensor, y_train_tensor)
losses.append(loss)
# 检验模型参数
print(model.state_dict())
到目前为止我们使用的数据是从 Numpy 的数组转化过来的张量,其实在 PyTorch 中我们可以使用 Dataset 这个类。
可以将其视为一种 Python 元组列表,每个元组对应一个点(特征,标签)。最基本的方法是
__init__(self)
: 它包含构建元组列表所需的任何参数-它可能是将被加载和处理的CSV文件的名称;它可能是两个张量,一个用于特征,另一个用于标签;或其他任何东西,取决于待解决的任务。__get_item__(self, index)
:它允许数据集索引__len__(self)
:返回数据集的大小from torch.utils.data import Dataset, TensorDataset
class CustomDataset(Dataset):
def __init__(self, x_tensor, y_tensor):
self.x = x_tensor
self.y = y_tensor
def __getitem__(self, index):
return (self.x[index], self.y[index])
def __len__(self):
return len(self.x)
# 这里的张量是在CPU中的,不占用GPU
x_train_tensor = torch.from_numpy(x_train).float()
y_train_tensor = torch.from_numpy(y_train).float()
train_data = CustomDataset(x_train_tensor, y_train_tensor)
print(train_data[0])
# 如果数据集里只是一些张量可以直接使用自带的TensorDataset类
train_data = TensorDataset(x_train_tensor, y_train_tensor)
print(train_data[0])
使用 Dataset 的好处在于它可以使用 数据加载器 DataLoader,从而对数据进行划分训练。
目前为止我们单步训练使用的是全部数据,即批梯度下降。当数据样本很多时通常采用小批次梯度下降,也就是单步训练只采用部分数据。使用 DataLoader 能够帮我们对数据进行切片!
from torch.utils.data import DataLoader
# 函数的输入为:训练数据集、批次数量、是否打乱次序
train_loader = DataLoader(dataset=train_data, batch_size=16, shuffle=True)
使用 DataLoader 后我们的代码变为
losses = []
train_step = make_train_step(model, loss_fn, optimizer)
for epoch in range(n_epochs):
for x_batch, y_batch in train_loader:
# 由于数据是存在CPU的,因此我们需要将训练数据送到模型所在位置
x_batch = x_batch.to(device)
y_batch = y_batch.to(device)
loss = train_step(x_batch, y_batch)
losses.append(loss)
print(model.state_dict())
注意:使用 DataLoader 后有两点不同的地方,
同理我们需要对验证数据建立一个 DataLoader
from torch.utils.data.dataset import random_split
x_tensor = torch.from_numpy(x).float()
y_tensor = torch.from_numpy(y).float()
dataset = TensorDataset(x_tensor, y_tensor)
# 数据集划分
train_dataset, val_dataset = random_split(dataset, [80, 20])
train_loader = DataLoader(dataset=train_dataset, batch_size=16)
val_loader = DataLoader(dataset=val_dataset, batch_size=20)
接下来我们来看看验证部分,有两点需要注意的:
losses = []
val_losses = []
train_step = make_train_step(model, loss_fn, optimizer)
for epoch in range(n_epochs):
for x_batch, y_batch in train_loader:
x_batch = x_batch.to(device)
y_batch = y_batch.to(device)
loss = train_step(x_batch, y_batch)
losses.append(loss)
with torch.no_grad():
for x_val, y_val in val_loader:
x_val = x_val.to(device)
y_val = y_val.to(device)
# 验证模式
model.eval()
yhat = model(x_val)
val_loss = loss_fn(y_val, yhat)
val_losses.append(val_loss.item())
print(model.state_dict())
import numpy as np
import torch
import torch.optim as optim
import torch.nn as nn
from torchviz import make_dot
from torch.utils.data import Dataset, TensorDataset, DataLoader
from torch.utils.data.dataset import random_split
device = 'cuda' if torch.cuda.is_available() else 'cpu'
# 数据的生成
np.random.seed(42)
x = np.random.rand(100, 1)
true_a, true_b = 1, 2
y = true_a + true_b*x + 0.1*np.random.randn(100, 1)
# 将Numpy数据转为张量
x_tensor = torch.from_numpy(x).float()
y_tensor = torch.from_numpy(y).float()
# 自定义Dataset
class CustomDataset(Dataset):
def __init__(self, x_tensor, y_tensor):
self.x = x_tensor
self.y = y_tensor
def __getitem__(self, index):
return (self.x[index], self.y[index])
def __len__(self):
return len(self.x)
# 创建 Dataset
dataset = TensorDataset(x_tensor, y_tensor) # dataset = CustomDataset(x_tensor, y_tensor)
# 划分训练数据和测试数据8:2
train_dataset, val_dataset = random_split(dataset, [80, 20])
# 创建数据加载器
train_loader = DataLoader(dataset=train_dataset, batch_size=16)
val_loader = DataLoader(dataset=val_dataset, batch_size=20)
# 定义模型
class ManualLinearRegression(nn.Module):
def __init__(self):
super().__init__()
self.linear = nn.Linear(1, 1)
def forward(self, x):
return self.linear(x)
# 定义单步训练函数
def make_train_step(model, loss_fn, optimizer):
def train_step(x, y):
model.train() #训练模式
yhat = model(x) #计算输出
loss = loss_fn(y, yhat) #计算损失
loss.backward() #计算梯度
optimizer.step() #更新参数
optimizer.zero_grad() #清零梯度
return loss.item()
return train_step
# Estimate a and b
torch.manual_seed(42)
# 模型、损失函数、优化器
model = ManualLinearRegression().to(device) # model = nn.Sequential(nn.Linear(1, 1)).to(device)
loss_fn = nn.MSELoss(reduction='mean')
optimizer = optim.SGD(model.parameters(), lr=1e-1)
# 创建单步训练函数
train_step = make_train_step(model, loss_fn, optimizer)
n_epochs = 100
training_losses = []
validation_losses = []
print(model.state_dict())
for epoch in range(n_epochs):
batch_losses = []
for x_batch, y_batch in train_loader:
x_batch = x_batch.to(device)
y_batch = y_batch.to(device)
loss = train_step(x_batch, y_batch)
batch_losses.append(loss)
training_loss = np.mean(batch_losses)
training_losses.append(training_loss)
with torch.no_grad():
val_losses = []
for x_val, y_val in val_loader:
x_val = x_val.to(device)
y_val = y_val.to(device)
model.eval()
yhat = model(x_val)
val_loss = loss_fn(y_val, yhat).item()
val_losses.append(val_loss)
validation_loss = np.mean(val_losses)
validation_losses.append(validation_loss)
print(f"[{epoch+1}] Training loss: {training_loss:.3f}\t Validation loss: {validation_loss:.3f}")
print(model.state_dict())
Python数据处理入门教程!
开源项目地址:https://github.com/datawhalechina/powerful-numpy