本文参考的是《动手学深度学习》(PyTorch版),链接在下面。由于照着网站上的代码敲一遍自己印象也不是很深刻,所以我整理了该书本中的内容,整理了自己的思路梳理了一遍。希望该文章能够对初学者的你来说有所帮助。同时由于我也是第一次用torch写代码,可能会有许多疏漏,如果有错误,希望各位能够指正。
目录
0 代码目的
本项目是实现了原书中的第3.2节,实现线性回归。其网络结构图如下:
输入有两个特征,输出只有一个数据。输入层与输出层之间是线性的。
1 数据集创建
数据集的创建与原书的创建方式相同。只是我将样本数更改为了10000个,并分为了训练集与测试集。训练集占比70%,测试集占比30%。真实权重与偏置与原书相同,真实权重为
[
2
,
−
3.4
]
[2, -3.4]
[2,−3.4],真实偏置为
4.2
4.2
4.2。并且将数据保存到了data
文件夹下。为了偷懒,我将数据封装成TensorDataset
后用pickle
进行的保存。代码如下:
import torch
import torch.utils.data as Data
import numpy as np
import pickle
from sklearn.model_selection import train_test_split
def create_data():
num_inputs = 2
num_examples = 10000
true_w = [2, -3.4]
true_b = 4.2
features = torch.randn(num_examples, num_inputs, dtype=torch.float32)
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b
# print(labels.size())
labels += torch.tensor(np.random.normal(0, 0.01, size=labels.size()),
dtype=torch.float32)
X_train, X_test, y_train, y_test = train_test_split(features, labels, test_size=0.3,
random_state=0)
train_dataset = Data.TensorDataset(X_train, y_train)
test_dataset = Data.TensorDataset(X_test, y_test)
# dataset = Data.TensorDataset(features, labels)
with open('./data/train_dataset.pkl', 'wb') as f:
pickle.dump(train_dataset, f)
with open('./data/test_dataset.pkl', 'wb') as f:
pickle.dump(test_dataset, f)
create_data()
这串代码实质上就是使用了真实的权重与偏置,加上一个服从均值为0,标准差为0.01的正态分布的干扰项,生成了10000条数据:
y = X w + b + ϵ {\boldsymbol y} = {\boldsymbol X}{\boldsymbol w} + \boldsymbol b + \epsilon y=Xw+b+ϵ
2 神经网络搭建流程
这一部分是我根据作者的思路,整理出来的自己的思路,详细的内容见下图:
当然,由于我本人用torch也没写过几个神经网络,所以这张思维导图可能不是特别完善,如果后续有新的理解,会重新更改。
3 从0搭建一个线性回归神经网络
3.1 参数定义
根据上图最上面的部分,我们需要考虑的参数有num_epoch(epoch数)
,batch_size
,num_inputs(输入层数目)
,num_outputs(输出层数目)
,lr(学习率)
,w(第一层权重)
,b(第一层偏置)
。由于还有输入的训练数据与测试数据,所以整个类的构造方法为:
def __init__(self, train_dataset, test_dataset, num_epochs=10,
batch_size=16, num_inputs=2, num_outputs=1, lr=0.03):
self.train_dataset = train_dataset
self.test_dataset = test_dataset
self.num_epochs = num_epochs
self.batch_size = batch_size
self.num_inputs = num_inputs
self.num_outputs = num_outputs
self.lr = lr
self.w = torch.tensor(np.random.normal(0, 0.01, (num_inputs, num_outputs)),
dtype=torch.float32)
self.b = torch.zeros(num_outputs, dtype=torch.float32)
self.w.requires_grad_(True)
self.b.requires_grad_(True)
这里在定义w
和b
的时候,就设置其为可学习的参数。
3.2 模块定义
3.2.1 神经网络构建
由于我们只是个线性的神经网络,其公式为:
y ^ = X w + b \hat \boldsymbol y = \boldsymbol X \boldsymbol w + \boldsymbol b y^=Xw+b
于是神经网络的构建如下:
def net(self, X, w, b):
"""
神经网络, y_hat = Xw + b
:param X: tensor
输入的样本数据, 大小为(batch_size, num_inputs)
:param w: tensor
权重, 大小为(num_inputs, num_outputs)
:param b: tensor
偏置, 大小为(batch_size, num_outputs)
:return y_hat: tensor
输出层的输出, 大小为(batch_size, num_outputs)
"""
y_hat = torch.mm(X, w) + b
return y_hat
3.2.2 损失函数定义
由于是回归问题,所以这里损失函数就使用均方误差
。
def get_loss(self):
"""
获得损失函数
:return loss: Object
均方误差损失函数
"""
loss = nn.MSELoss()
return loss
3.2.3 优化器定义
这里采用SGD
优化器。
def get_optimizer(self):
"""
获得优化器
:return optimizer: Object
SGD优化器
"""
optimizer = optim.SGD([self.w, self.b], self.lr)
return optimizer
优化器传入的parameters
是[self.w, self.b]
,也就是说在之后的梯度下降过程中,修改的是self.w, self.b
。
3.3 模型训练
3.3.1 将数据封装到DataLoader()
由于在xmind中也写到了,每一个epoch
开始的时候需要将样本数据给打乱,所以这里将数据放入DataLoader()
中进行数据的打乱。
def get_data_loader(self):
"""
获得数据集的DataLoader实例化对象
:return train_iter: Object
训练集
:return test_iter: Object
测试集
"""
train_iter = Data.DataLoader(self.train_dataset, self.batch_size, shuffle=True)
test_iter = Data.DataLoader(self.test_dataset, self.batch_size, shuffle=False)
return train_iter, test_iter
由于测试数据不进行训练,所以这里没有必要每一个epoch
都打乱顺序(毕竟打乱顺序也是需要花费时间与性能的)。同时,虽然我没有仔细研究过DataLoader
这个类,但是根据实验证明,只要设置了shuffle=True
,那么在后续遍历这个数据的时候,每一个epoch
都是会打乱一次的。
3.2.2 训练
def train(self):
"""
模型训练
"""
train_iter, test_iter = self.get_data_loader()
loss = self.get_loss()
optimizer = self.get_optimizer()
for epoch in range(self.num_epochs):
for X, y in train_iter:
output = self.net(X, self.w, self.b)
train_loss = loss(output, y.view(-1, 1))
optimizer.zero_grad() # 清空梯度
train_loss.backward()
optimizer.step()
# print('training w: {0}, training b: {1}'.format(self.w, self.b))
for X, y in test_iter:
test_output = self.net(X, self.w, self.b)
test_loss = loss(test_output, y.view(-1, 1))
# print('test w: {0}, test b: {1}'.format(self.w, self.b))
print('epoch %d, train loss: %f, test loss: %f' %
(epoch + 1, train_loss.item(), test_loss.item()))
这里首先调用之前定义的get_data_loader()
方法,得到训练数据与测试数据的DataLoader()
。接着调用get_loss()
和get_optimizer()
得到损失函数与优化函数。
第三步就是训练的过程,这里每一个epoch
都遍历一遍全部样本数据。而batch_size
的使用就是在train_iter
和test_iter
这两个实例化对象里面。在遍历这两个实例化对象的过程中,每一轮吐出来的X
与y
都是一个batch
的大小。而且也就是在for X, y in xxx_iter:
这个语句中,大家可以观测到数据是被打乱了的。
再然后就是按着思维导图上的逻辑来,先通过前向传播获得网络输出的
y
^
\hat \boldsymbol y
y^,接着将
y
^
\hat \boldsymbol y
y^ 与
y
\boldsymbol y
y 通过均方误差求得
l
o
s
s
loss
loss,清空梯度后反向传播,最后通过优化器更改构造函数中定义的self.w
与self.b
。
这里不得不提一嘴,torch的代码看上去确实比 tf 的简洁且流畅的多……
对于测试,我们在测试集上面验证训练情况。由于是回归问题,所以我们依旧用每个epoch
的损失来作为衡量标准。测试的方法就是我们将每个epoch
训练后的w, b
与测试集数据
重新代入到网络中,并通过计算出的
y
^
t
e
s
t
\hat \boldsymbol y_{test}
y^test 与
y
t
e
s
t
\boldsymbol y_{test}
ytest 用同样的损失函数计算损失,求得测试集上的性能。以下是10个epoch
的输出情况:
epoch 1, train loss: 0.000130, test loss: 0.000100
epoch 2, train loss: 0.000101, test loss: 0.000103
epoch 3, train loss: 0.000060, test loss: 0.000100
epoch 4, train loss: 0.000115, test loss: 0.000097
epoch 5, train loss: 0.000177, test loss: 0.000098
epoch 6, train loss: 0.000138, test loss: 0.000096
epoch 7, train loss: 0.000075, test loss: 0.000096
epoch 8, train loss: 0.000075, test loss: 0.000097
epoch 9, train loss: 0.000069, test loss: 0.000096
epoch 10, train loss: 0.000160, test loss: 0.000103
当然,可能会有同学问,在一个epoch
中,在训练集上使用self.w, self.b
,又在测试集上使用self.w, self.b
,会不会出现在测试的时候更改权重与偏置的情况。实验证明,只要不调用optimizer.step()
就不会出现这个情况。如果想要自己验证的同学,将上面代码的两个注释给取消即可(i.e. 训练结束后打印一遍self.w, self.b
,测试结束后再打印一遍self.w, self.b
,或者直接print是否相等),最后的结果是两者相同。
4 完整代码
注:以下代码仅限神经网络的代码,不包括数据集创建的代码。数据集创建的完整代码在第一节中。
import torch
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as Data
import numpy as np
import pickle
class LinearRegression:
"""
线性回归类
"""
def __init__(self, train_dataset, test_dataset, num_epochs=10,
batch_size=16, num_inputs=2, num_outputs=1, lr=0.03):
self.train_dataset = train_dataset
self.test_dataset = test_dataset
self.num_epochs = num_epochs
self.batch_size = batch_size
self.num_inputs = num_inputs
self.num_outputs = num_outputs
self.lr = lr
self.w = torch.tensor(np.random.normal(0, 0.01, (num_inputs, num_outputs)),
dtype=torch.float32)
self.b = torch.zeros(num_outputs, dtype=torch.float32)
self.w.requires_grad_(True)
self.b.requires_grad_(True)
def get_data_loader(self):
"""
获得数据集的DataLoader实例化对象
:return train_iter: Object
训练集
:return test_iter: Object
测试集
"""
train_iter = Data.DataLoader(self.train_dataset, self.batch_size, shuffle=True)
test_iter = Data.DataLoader(self.test_dataset, self.batch_size, shuffle=False)
return train_iter, test_iter
def net(self, X, w, b):
"""
神经网络, y_hat = Xw + b
:param X: tensor
输入的样本数据, 大小为(batch_size, num_inputs)
:param w: tensor
权重, 大小为(num_inputs, num_outputs)
:param b: tensor
偏置, 大小为(batch_size, num_outputs)
:return y_hat: tensor
输出层的输出, 大小为(batch_size, num_outputs)
"""
y_hat = torch.mm(X, w) + b
return y_hat
def get_loss(self):
"""
获得损失函数
:return loss: Object
均方误差损失函数
"""
loss = nn.MSELoss()
return loss
def get_optimizer(self):
"""
获得优化器
:return optimizer: Object
SGD优化器
"""
optimizer = optim.SGD([self.w, self.b], self.lr)
return optimizer
def train(self):
"""
模型训练
"""
train_iter, test_iter = self.get_data_loader()
loss = self.get_loss()
optimizer = self.get_optimizer()
for epoch in range(self.num_epochs):
for X, y in train_iter:
output = self.net(X, self.w, self.b)
train_loss = loss(output, y.view(-1, 1))
optimizer.zero_grad() # 清空梯度
train_loss.backward()
optimizer.step()
# print('training w: {0}, training b: {1}'.format(self.w, self.b))
for X, y in test_iter:
test_output = self.net(X, self.w, self.b)
test_loss = loss(test_output, y.view(-1, 1))
# print('test w: {0}, test b: {1}'.format(self.w, self.b))
print('epoch %d, train loss: %f, test loss: %f' %
(epoch + 1, train_loss.item(), test_loss.item()))
with open('./data/train_dataset.pkl', 'rb') as f:
train_dataset = pickle.load(f)
with open('./data/test_dataset.pkl', 'rb') as f:
test_dataset = pickle.load(f)
linear = LinearRegression(train_dataset=train_dataset, test_dataset=test_dataset)
linear.train()
5 参考
[1] Aston Zhang and Zachary C. Lipton and Mu Li and Alexander J. Smola. Dive into Deep Learning[M]. 2020: http://www.d2l.ai
[2] wang xiang. pytorch里面的Optimizer和optimizer.step()用法[EB/OL]. (2019-08-21)[2021-09-16]. https://blog.csdn.net/qq_40178291/article/details/99963586
[3] Doodlera. PyTorch dataloader里的shuffle=True[EB/OL]. (2020-11-05)[2021-09-16]. https://blog.csdn.net/qq_35248792/article/details/109510917