Pytorch学习笔记(1)——手把手教你从0开始搭建个自己的神经网络

15 篇文章 4 订阅
11 篇文章 0 订阅

本文参考的是《动手学深度学习》(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_sizenum_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)

这里在定义wb的时候,就设置其为可学习的参数。

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_itertest_iter这两个实例化对象里面。在遍历这两个实例化对象的过程中,每一轮吐出来的Xy都是一个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.wself.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

  • 5
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值