赞
踩
本文参考的是《动手学深度学习》(PyTorch版),链接在下面。由于照着网站上的代码敲一遍自己印象也不是很深刻,所以我整理了该书本中的内容,整理了自己的思路梳理了一遍。希望该文章能够对初学者的你来说有所帮助。同时由于我也是第一次用torch写代码,可能会有许多疏漏,如果有错误,希望各位能够指正。
本项目是实现了原书中的第3.2节,实现线性回归。其网络结构图如下:
输入有两个特征,输出只有一个数据。输入层与输出层之间是线性的。
数据集的创建与原书的创建方式相同。只是我将样本数更改为了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+ϵ
这一部分是我根据作者的思路,整理出来的自己的思路,详细的内容见下图:
当然,由于我本人用torch也没写过几个神经网络,所以这张思维导图可能不是特别完善,如果后续有新的理解,会重新更改。
根据上图最上面的部分,我们需要考虑的参数有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
的时候,就设置其为可学习的参数。
由于我们只是个线性的神经网络,其公式为:
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
由于是回归问题,所以这里损失函数就使用均方误差
。
def get_loss(self):
"""
获得损失函数
:return loss: Object
均方误差损失函数
"""
loss = nn.MSELoss()
return loss
这里采用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
。
由于在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
都是会打乱一次的。
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是否相等),最后的结果是两者相同。
注:以下代码仅限神经网络的代码,不包括数据集创建的代码。数据集创建的完整代码在第一节中。
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()
[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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。