一个简单的回归模型可以写作:
$y = X * W + b$
其中 $W$ 和 $b$ 作为模型的参数,需要通过给定的训练数据学习而得,训练的过程就是尽可能找到最优的 $W$ 和 $b$ 。我们使用经典的回归数据——波士顿房价数据集来演示如何使用 PyTorch 构建神经网络。
1 | import torch,random |
波士顿房价的数据是美国人口普查局收集的美国马萨诸塞州波士顿住房价格的有关信息,该数据集很小,只有 506 个样本,每个样本有 14 个属性或特征(使用 sklearn 内置的该数据只有 13 个特征,少了最后一个“中位数”)。我们首先将数据集划分为 训练集 和 测试集,并转换成张量数据结构:
1 | from sklearn.model_selection import train_test_split |
我们先写一版从零实现的线性回归,旨在掌握神经网络中张量运算的维度对应关系,训练一个神经网络基本思想,包括正向与反向传播、参数更新等原理。所以,包括读取数据,我们也尽量不使用 PyTorch 的高级 API,而是自己封装:1
2
3
4
5
6
7def data_iter(batch_size, features, labels):
num_examples = len(features)
indices = list(range(num_examples))
random.shuffle(indices) # 把数据打乱
for i in range(0, num_examples, batch_size):
batch_indices = torch.tensor(indices[i:min(i + batch_size, num_examples)])
yield features[batch_indices], labels[batch_indices]
在上面的代码中,我们定义一个 data_iter 函数实现接收批量大小、特征矩阵和标签向量作为输入,生成大小为 batch_size 的小批量。每个小批量包含打乱后的一组特征和标签。
初始化线性回归模型中的参数 $W$ 和 $b$,需要注意的是这些参数需设置为记录梯度的:1
2
3
4# W在0轴上的维度需要与输入数据在1轴上的维度对应上(13个特征),进行矩阵乘法后根据需要设置输出的维度,这里设置为1
W = torch.randn((len(boston.feature_names), 1), requires_grad=True)
# 偏置是一个标量
b = torch.zeros(1, requires_grad=True)
我们可以定义线性回归模型为 $y = X * W + b$,即进行一个矩阵乘法再加上偏置,偏置虽然是标量,但会进行广播处理以完成相加的运算:1
2
3def linreg(X, W, b):
""" 线性回归模型 """
return torch.matmul(X, W) + b
定义损失函数,即模型预测值与真实值的差距,一般使用 平方误差 来衡量线性回归任务的损失,之所以乘以 1/2 是为了求导后的结果更规整:1
2
3def squared_loss(y_pred, y):
""" 均方损失 """
return ((y_pred - y.reshape(y_pred.shape))**2).mean() / 2
梯度下降是最基本的优化算法,其原理是在每一步中,使用从数据集中随机抽取的一个小批量样本,然后根据参数计算损失的梯度;接下来,朝着减少损失的方向更新我们的参数。每一步更新的大小有学习率 lr 决定:1
2
3
4
5
6def sgd(params, lr, batch_size):
""" 小批量随机梯度下降 """
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size # 注意要用批量大小来归一化步长
param.grad.zero_() # 注意更新参数后要将累计的梯度清零
至此,我们可以开始训练模型了,一般的模型训练步骤如下:
- 初始化参数
- 重复,直到一定的循环次数(或终止)
- 计算模型输出 out,并计算与真实值之间的误差 loss
- 反向传播计算梯度(调用 backward( ))
- 更新参数,并清空梯度
在每个迭代周期(epoch)中,我们使用 data_iter 函数遍历整个数据集,并将训练数据集中所有样本都使用一次,其中迭代周期个数 num_epochs 和学习率 lr 都属于超参数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24lr = 0.0001 # 数据太少不能设置太大,否则梯度会消失或爆炸,loss输出nan/inf
num_epochs = 4
net = linreg
loss = squared_loss
batch_size = 32
for epoch in range(num_epochs):
for X, y in data_iter(batch_size, X_train, y_train):
l = loss(net(X, W, b), y) # 计算损失误差
# 因为 l 形状(`batch_size`, 1)不是一个标量,只有标量才能进行反向传播,
# 所以将 l 中的所有元素加到一起,并以此计算关于[W, b]的梯度
l.sum().backward()
sgd([W, b], lr, batch_size) # 使用参数的梯度更新参数
with torch.no_grad(): # 测试时不需要跟踪梯度
train_l = loss(net(X_train, W, b), y_train)
print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')
'''
epoch 1, loss 3456.732666
epoch 2, loss 802.957031
epoch 3, loss 530.158081
epoch 4, loss 480.403564
'''
可以看到输出的 loss 随着迭代周期的增进而不断减小。但是由于我们的模型过于简单,输出的损失实在是太大了点。我们接下来使用PyTorch内置的很多神经网络的数据、模型、损失、优化API来实现一版简介但结构更复杂的神经网络。
首先,我们加载数据就是用Dataset与DataLoader来完成:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15from torch.utils.data import Dataset
class BostonDataset(Dataset):
def __init__(self, X_train, y_train):
self.data = X_train
self.labels = y_train
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
return self.data[idx], self.labels[idx]
dataset = BostonDataset(X_train, y_train)
dl = torch.utils.data.DataLoader(dataset, batch_size=32, shuffle=True, num_workers=0)
接下来,我们来构建模型,$y = X * W + b$ 的操作其实就是一个线性变换,PyTorch中有已经实现好的线性网络torch.nn.Linear( ),其具体的使用方法如下:
init(self, in_features, out_features, bias=True)
- in_features:前一层网络神经元的个数
- out_features: 该网络层神经元的个数
以上两者决定了weight的形状[out_features , in_features]- bias: 网络层是否有偏置,默认存在,且维度为[out_features ];若bias=False,则该网络层无偏置。
输入该网络层的形状(N, *, in_features),其中N为批量处理过成中每批数据的数量,*表示单个样本数据存在若干轴,但最后一个轴上的维度一定是in_features。可以看到,不管输入数据的形状多么复杂,我们只需要关心特征对应的轴在输入输出中的维度变化,一下子简单了很多。
所以,我们可以定义一个相对复杂一点的网络(包含隐藏层),并且增加一个非线性变换(激活函数),这样就避免了两个线性变换直接连接(加深网络就没有意义了):1
2
3
4
5
6
7
8
9
10
11
12
13class Net(torch.nn.Module):
def __init__(self, n_feature, n_output):
super(Net, self).__init__()
self.hidden = torch.nn.Linear(n_feature, 100) # 增加一个隐藏层
self.predict = torch.nn.Linear(100, n_output)
def forward(self, x):
out = self.hidden(x)
out = torch.relu(out) # 两个线性网络中间增加非线性变换
out = self.predict(out)
return out
net = Net(13, 1) # 实例化网络,输入有13个特征,输出只有1维
接下来定义损失函数和优化算法,PyTorch已经为我们定义好了一切:1
2loss_func = torch.nn.MSELoss() # 均方误差作为损失
optimizer = torch.optim.Adam(net.parameters(), lr=0.01) # 使用Adam优化算法
最后,我们可以开始训练了:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24loss_train_list = []
num_epochs = 25
for i in range(num_epochs):
train_loss = []
for _, data in enumerate(dl):
pred = net.forward(data[0])
pred = torch.squeeze(pred) # 预测结果需要删除掉维度为1的轴,才和样本值形状一致
loss_train = loss_func(pred, data[1])
train_loss.append(loss_train.item()) # 我们需要记录每个batch的损失,平均后作为一个epoch的损失值
optimizer.zero_grad() # 将梯度设为0
loss_train.backward() # 反向传播
optimizer.step()
epoch_loss = np.mean(train_loss) # 一个epoch训练结束
loss_train_list.append(epoch_loss)
print(f'epoch {i + 1}, loss {epoch_loss:f}')
'''
epoch 1, loss 555.314002
epoch 2, loss 89.066934
epoch 3, loss 60.860289
'''
可以看出,误差相比简单的网络小了很多,而且迭代多个epoch以后会下降到10左右。我们将每个epoch的误差都保存下来,并作图来观察其变化,这是一种常用的分析方法。
1 | import matplotlib.pyplot as plt |