zl程序教程

您现在的位置是:首页 >  工具

当前栏目

【Pytorch学习笔记】5.总结一下一个深度学习模型的实现过程(以Softmax回归从零实现为例)

笔记学习PyTorch 实现 一个 总结 模型 过程
2023-09-27 14:19:57 时间

一些简单的深度学习模型从零实现看着教程看似简单,敲着代码就过去了,但还是做一下自己的初步总结。里面的思维过程无论放在今后什么问题里都是适用的,包括实现的基本步骤、模型的构思如何反映到代码,定义训练的过程需要哪些参数等等。

本文以Softmax回归从零实现为例,总结一下一个深度学习模型的实现过程,每个步骤都附上自己的总结思考。

首先考虑解决什么问题

比如我们想要用Softmax回归模型,去实现一个图片分类的问题。
后面是目的,前面是手段。
然后脑子里需要浮现出解决问题的几个基本步骤:

  • 获取和读取数据
  • 定义模型:①根据模型初始化模型参数,②定义模型、损失函数
  • 定义优化算法(如SGD)用于学习参数
  • 训练模型
  • 评估模型

1 获取和读取数据

获取数据

  • 我们首先需要创建一个数据集对象Dataset()

对于获取数据来说,一般初学者都是从一些公开数据集获取数据的,获取数据一般会用到pytorch相关库的一些方法。
比如我们想要读取 FashionMNIST 数据集,用于通过Softmax模型解决该数据集的分类问题(或者评估Softmax模型的分类性能,Whatever,不同表述而已)。

我们通常使用torchvision.datasets下的类创建数据集,这个类下有许多公开数据集可以直接下载获取,如我们需要FashionMNIST数据集:

# 对于机器学习问题,一般都会生成一个训练集,一个测试集
mnist_train = torchvision.datasets.FashionMNIST(
    root='~/Datasets/FashionMNIST', train=True, download=True, 
    transform=transforms.ToTensor())
mnist_test = torchvision.datasets.FashionMNIST(
    root='~/Datasets/FashionMNIST', train=False, download=True, 
    transform=transforms.ToTensor())

注意其中的transform参数,需要传入一个数据转换器(位于torchvision.transforms下),常用的有ToTensor()方法,将图片数据转换为尺寸为(C x H x W)且大小位于[0.0, 1.0]的float32数据类型的Tensor。
创建的数据集对象属于torch.utils.data.Dataset的子类。

读取数据

  • 然后将创建的数据集对象Dataset()传入数据加载器Dataloader()

我们创建一个Dataset后,一般直接传入torch.utils.data.Dataloader() 生成一个Dataloader对象。

batch_size = 256
train_iter = torch.utils.data.DataLoader(
    mnist_train, batch_size=batch_size, shuffle=True, num_workers=0)
test_iter = torch.utils.data.DataLoader(
    mnist_test, batch_size=batch_size, shuffle=False, num_workers=0)
# 这里的num_workers表示数据读取的线程数,Windows系统一般默认设0,这里更多信息可以自行搜索学习。

生成Dataloader后,就可以使用for循环读取批量数据了,一次循环是一个batch_size数量的数据。
batch_size是一个重要的超参数,横贯机器学习过程的始终,不仅方便计算机按批次读取数据减少内存开销,并且在计算梯度时,使用一个batch_size的数据进行迭代更新,大大减少计算量。

我们也可以使用next(iter(dataloader))手工读取一个批次的数据。

Dataloader是一个可迭代对象,它通过生成迭代器,来读取批量数据。
对于一些数据,我们还可以构建生成器来读取批量数据,这部分扩展阅读可以参考我这篇文章

2 构建模型

  • 将模型的数学表达转换成代码表达

我们选择Softmax回归模型时,心中需要构思好这个模型的数学表达,见下。
需要注意的是,softmax回归本身是一个单层神经网络,并且和线性回归一样是全连接的:
在这里插入图片描述这里提示我们要有一个思维:把一个数学模型用深度神经网络去构建解释。

我们的Softmax回归数学模型是(以4像素图片,3分类标签为例):
o ( i ) = x ( i ) W + b y ^ ( i ) = softmax ⁡ ( o ( i ) ) p = arg max ⁡ p y ^ p \begin{aligned} \boldsymbol{o}^{(i)} &=\boldsymbol{x}^{(i)} \boldsymbol{W}+\boldsymbol{b} \\ \hat{\boldsymbol{y}}^{(i)} &=\operatorname{softmax}\left(\boldsymbol{o}^{(i)}\right) \end{aligned} \\ p = \underset{p}{\argmax } \hat{y}_{p} o(i)y^(i)=x(i)W+b=softmax(o(i))p=pargmaxy^p
x ( i ) = [ x 1 ( i ) x 2 ( i ) x 3 ( i ) x 4 ( i ) ] , o ( i ) = [ o 1 ( i ) o 2 ( i ) o 3 ( i ) ] , y ^ ( i ) = [ y ^ 1 ( i ) y ^ 2 ( i ) y ^ 3 ( i ) ] \boldsymbol{x}^{(i)}=\left[x_{1}^{(i)} \quad x_{2}^{(i)} \quad x_{3}^{(i)} \quad x_{4}^{(i)}\right], \boldsymbol{o}^{(i)}=\left[\begin{array}{lll} o_{1}^{(i)} & o_{2}^{(i)} & o_{3}^{(i)} \end{array}\right], \\ \hat{\boldsymbol{y}}^{(i)}=\left[\begin{array}{lll} \hat{y}_{1}^{(i)} & \hat{y}_{2}^{(i)} & \hat{y}_{3}^{(i)} \end{array}\right] x(i)=[x1(i)x2(i)x3(i)x4(i)],o(i)=[o1(i)o2(i)o3(i)],y^(i)=[y^1(i)y^2(i)y^3(i)]
参数:
W = [ w 11 w 12 w 13 w 21 w 22 w 23 w 31 w 32 w 33 w 41 w 42 w 43 ] , b = [ b 1 b 2 b 3 ] \boldsymbol{W}=\left[\begin{array}{lll}w_{11} & w_{12} & w_{13} \\ w_{21} & w_{22} & w_{23} \\ w_{31} & w_{32} & w_{33} \\ w_{41} & w_{42} & w_{43}\end{array}\right], \quad \boldsymbol{b}=\left[\begin{array}{lll}b_{1} & b_{2} & b_{3}\end{array}\right] W=w11w21w31w41w12w22w32w42w13w23w33w43,b=[b1b2b3]

初始化模型参数

本例中,我们需要学习的模型系数是 矩阵W和偏倚项b
我们将28281大小的图片拉伸为28×28=784长度的向量(输入的是一个batch的X,即X形状为256×784),输出的是10分类,因此W的形状为:784×10
偏倚项b的形状为10×1

然后,我们一般使用(0,0.01)的正态分布去初始化参数的数值:

num_inputs = 784
num_outputs = 10
W = torch.tensor(
    np.random.normal(loc=0, scale=0.01, size=(num_inputs, num_outputs)), 
    dtype=torch.float)
b = torch.zeros(num_outputs , dtype=torch.float)

最后,最重要的一步,W和b设上梯度,因为我们需要学习这个参数!

# 设上梯度
W.requires_grad_(requires_grad=True)
b.requires_grad_(requires_grad=True)

定义模型、损失函数

先定义softmax:

def softmax(X):
    X_exp = X.exp()
    partition = X_exp.sum(dim=1, keepdim=True)
    return X_exp / partition  # 这里使用了广播机制

定义模型

def net(X):
    return softmax(torch.mm(X.view((-1, num_inputs)), W) + b)

定义交叉熵损失函数

def cross_entropy(y_hat, y):
    return -torch.log(y_hat.gather(dim=1, index=y.view(-1,1)))
# y就是待传入的那个批量的label数据

(定义交叉熵损失函数的细节可以参考我这篇文章

我们在定义模型和损失函数时,在心中对模型的计算图最好有个大致的把握。
我们可以先看看本模型损失函数的梯度节点可视化:
(可视化可参考我的这篇文章
在这里插入图片描述
可以看到在这个损失函数的计算图里,我们要求的是顶端的两个参数W,b,整个损失函数是关于参数的函数。DivBackward0节点及以上部分是模型计算结果y_hat,节点以下部分流向损失函数的计算。

3 定义优化算法

  • 定义优化器以优化参数

本例中我们依然可使用随机梯度下降法(SGD)作为Optimizer,届时在训练时每个batch数据计算完后梯度后,利用当前梯度迭代更新一次参数。
优化器除了需要传入的待学习参数(本例为W,b)外,还需要传入一些超参数。

# lr是学习率。作为超参数。
def sgd(params, lr, batch_size):
    for param in params:
        param.data -= lr * param.grad / batch_size

4 训练模型

接下去就是重头戏的训练环节了,我们一般定义一个训练函数train()作为一套训练的过程的打包。

定义训练函数注意的要素

  1. 超参数:lr(学习率),num_epochs(训练轮数),batch_size
    超参数一般根据经验设置,比如本例设lr=0.1,num_epochs=5
    在深度学习模型中,一般需要训练多轮epoch才有比较好的效果
  2. 常规参数
    训练集的Dataloader:train_iter
    测试集的Dataloader:test_iter(视情况非必须)
    模型名net:net
    损失函数loss:cross_entropy
    训练参数params:[W, b]
    优化器Optimizer:None(我们直接在函数中封入SGD作为默认优化器,就可以不用再手动传)

定义步骤

  1. 从训练集Dataloader获得一个X,y批次;
    通过模型net算出预测值y_hat
    通过损失函数loss算出损失值l

记得回顾上面那张计算图,只有待学习的参数W,b带梯度,是届时需要传入优化器更新的。
同时注意损失函数定义时一般返回的是一个batch_size长度的向量,对齐求sum()转换成标量以方便求导(见下方代码)。

  1. 对参数W,b梯度清零
  2. l.backward()针对这一批量的数据结果,反向传播,求出当前梯度
  3. 之后W,b中就带有了梯度信息,这时传入优化器sgd对参数进行一次迭代更新:

W . d a t a = W . d a t a − l r ∗ W . g r a d / b a t c h s i z e W.data = W.data -lr * W.grad / batchsize W.data=W.datalrW.grad/batchsize
b . d a t a = b . d a t a − l r ∗ b . g r a d / b a t c h s i z e b.data = b.data -lr * b.grad / batchsize b.data=b.datalrb.grad/batchsize

以上完成一个批次,参数就迭代更新一次。
完成所有批次,一轮训练就完成了。
完成设定的训练轮次(num_epochs),训练结束。

下面附上代码:

num_epochs, lr = 5, 0.1

def train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size,
    params = None, lr = None, optimizer = None):

    for epoch in range(num_epochs):
        train_l_sum, train_acc_sum, n = 0.0, 0.0, 0  # 显示总体损失值、准确率用
        for X, y in train_iter:
            print(X.shape)
            y_hat = net(X)
            print(y_hat.shape)
            l = loss(y_hat, y).sum()

            # 梯度清零
            if optimizer is not None:
                # 在这个例子中,optimizer没传入,用默认的sgd,这里不会被执行
                optimizer.zero_grad()
            elif params is not None and params[0].grad is not None:
                for param in params:
                    param.grad.data.zero_()  # 参数的梯度数据清零

            l.backward()   # 小批量的损失对模型参数求梯度

            if optimizer is None:
                sgd(params, lr, batch_size)
                # 传入优化器sgd对参数进行一次迭代更新
            else:
                optimizer.step()
                # 在这个例子中,optimizer没传入,用默认的sgd,这里不会被执行

            train_l_sum += l.item()
            train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()  # 计算训练准确率
            n += y.shape[0]
        print('epoch %d, loss %.4f, train acc %0.3f' % (epoch +1, train_l_sum / n, train_acc_sum / n))

5 评估模型

可以定义一个函数评估测试集上的准确率,这里就不详述了。
具体直接参考下一节的完整版代码,并对train_ch3训练函数补充了测试集评估的内容。

完整版代码(不用装d2l)

代码是自己修订的个人笔记版,可配合我的博文食用。
不需要安装 d2l、d2lzh_pytorch库。

Github版:
3.6Softmax从零实现笔记.ipynb

直接运行版:

# %%
# 导包
import torch
import torchvision
import torchvision.transforms as transforms
import numpy as np
import sys

# %% [markdown]
# ### 获取和读取数据

# %%
mnist_train = torchvision.datasets.FashionMNIST(
    root='~/Datasets/FashionMNIST', train=True, download=True, transform=transforms.ToTensor())
mnist_test = torchvision.datasets.FashionMNIST(
    root='~/Datasets/FashionMNIST', train=False, download=True, transform=transforms.ToTensor())

batch_size = 256

if sys.platform.startswith('win'):
    num_workers = 0  # 0表示不用额外的进程来加速读取数据
else:
    num_workers = 4

train_iter = torch.utils.data.DataLoader(
    mnist_train, batch_size=batch_size, shuffle=True, num_workers=num_workers)
test_iter = torch.utils.data.DataLoader(
    mnist_test, batch_size=batch_size, shuffle=False, num_workers=num_workers)

# %% [markdown]
# ### 初始化模型参数
# 我们使用向量表示每个样本,已知每个样本输入是高和宽均为28像素的图像,向量长度:28*28=784<br>
# 图像有10个类别,单层神经网络输出层的输出个数为10,so,softmax回归的权重w和偏差b参数的矩阵形状为:784×10和1×10(还是个线性模型)

# %%
num_inputs = 784
num_outputs = 10
W = torch.tensor(
    np.random.normal(loc=0, scale=0.01, size=(num_inputs, num_outputs)), 
    dtype=torch.float)
b = torch.zeros(num_outputs , dtype=torch.float)  # 这里直接定义为了 shape 为 10 的矩阵,利用后面的广播原则可扩展维度
print(W.shape, b.shape)

# %%
# 设上梯度
W.requires_grad_(requires_grad=True)
b.requires_grad_(requires_grad=True)

# %% [markdown]
# ### 构建模型
# 实现softmax运算<br>
# 首先描述一下,如何对多维Tensor按维度操作。<br>
# 比如,给定一个矩阵X,可以对其中同一列(dim=0)或同一行(dim=1)的元素求和,并在结果中保留行和列这两个维度(keepdim=True)

# %% [markdown]
# #### 定义softmax运算
# 设矩阵X为一个批次的数据,行数是样本数,列数是特征数。<br>
# 先对每个元素进行exp运算,再对运算好的矩阵进行同行元素求和,最后令矩阵每行各元素与该行元素之和相除。得到每行的概率分布。<br>
# 即,softmax运算的输出矩阵中,任意一行元素代表了一个样本在各个输出类别上的预测概率。

# %%
def softmax(X):
    X_exp = X.exp()
    partition = X_exp.sum(dim=1, keepdim=True)
    return X_exp / partition  # 这里使用了广播机制

# %% [markdown]
# #### 定义模型
# 通过view函数将每张原始图像改成长度为num_inputs的向量。

# %%
def net(X):
    return softmax(torch.mm(X.view((-1, num_inputs)), W) + b)

# %% [markdown]
# #### 定义损失函数

# %%
# 定义交叉熵损失函数
def cross_entropy(y_hat, y):
    return -torch.log(y_hat.gather(dim=1, index=y.view(-1,1)))
# y就是待传入的那个批量的label数据

# %% [markdown]
# ### 计算分类准确率
# 给定一个类别的概率分布y_hat,如果它与真实类别(索引矩阵)y一致,说明预测正确。<br>
# 准确率:正确预测数量 / 总预测数量<br>
# 我们定义accuracy函数。使用argmax()方法,y_hat.argmax(dim=1)返回y_hat每行中最大元素的索引,其与(索引矩阵)y形状相同。<br><br>
# 在pytorch中,相等条件判断式 (y_hat.argmax(dim=1) == y) 是一个类型为 ByteTensor 的Tensor,里面元素为布尔变量,可用float()将其转换为值为0或1(相等为真) 的浮点型Tensor

# %%
# # 定义准确率函数
# y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
# y = torch.LongTensor([0, 2])

# def accuracy(y_hat, y):
#     return (y_hat.argmax(dim=1) == y).float().mean().item()

# print(accuracy(y_hat, y))

# %% [markdown]
# #### 评价模型net在数据集 data_iter 上的准确率

# %%
# net即上面定义的模型。即每张转换成长向量后,赋予线性参数W,b,然后softmax,得到一个batch的y_hat
def evaluate_accuracy(data_iter, net):
    acc_sum, n = 0.0, 0
    for X, y in data_iter:
        acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
        n += y.shape[0]
    return acc_sum / n

# %%
# 因为随机初始化了参数 W,b ,模型net也初始化了,现在已经可以这个求未训练过的随机模型的准确率了。
# 随机模型的准确率应该与10分类的自然概率0.1相近
print(evaluate_accuracy(test_iter, net))

# %% [markdown]
# ### 训练模型
# **我们同样使用小批量随机梯度下降来优化模型的损失函数。**<br>
# 训练模型时,迭代周期数 num_epochs 和学习率 lr 都是可调超参数。

# %%
num_epochs, lr = 5, 0.1

def sgd(params, lr, batch_size):
    for param in params:
        param.data -= lr * param.grad / batch_size

def train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size,
    params = None, lr = None, optimizer = None):

    for epoch in range(num_epochs):
        train_l_sum, train_acc_sum, n = 0.0, 0.0, 0
        for X, y in train_iter:
            # print(X.shape)
            y_hat = net(X)
            # print(y_hat.shape)
            l = loss(y_hat, y).sum()

            # 梯度清零
            if optimizer is not None:
                # 在这个例子中,optimizer没传入,所以用默认的sgd,这里不会被执行
                optimizer.zero_grad()
            elif params is not None and params[0].grad is not None:
                for param in params:
                    param.grad.data.zero_()    # 参数的梯度数据清零

            l.backward()   # 小批量的损失对模型参数求梯度

            if optimizer is None:
                sgd(params, lr, batch_size)
            else:
                optimizer.step()
                # 在这个例子中,optimizer没传入,所以就用默认的sgd,这里不会被执行

            train_l_sum += l.item()
            train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
            n += y.shape[0]
        test_acc = evaluate_accuracy(test_iter, net)
        print('epoch %d, loss %.4f, train acc %0.3f, test acc %.3f' % (epoch +1, train_l_sum / n, train_acc_sum / n, test_acc))

# %%
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, batch_size, [W, b], lr)

# %% [markdown]
# ### 评估模型

# %%
from matplotlib import pyplot as plt

def get_fashion_mnist_labels(labels):
    text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',
                   'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
    return [text_labels[int(i)] for i in labels]

def show_fashion_mnist(images, labels):
    _, figs = plt.subplots(1, len(images), figsize=(12, 12))
    for f, img, lbl in zip(figs, images, labels):
        f.imshow(img.view((28, 28)).numpy())
        f.set_title(lbl)
        f.axes.get_xaxis().set_visible(False)
        f.axes.get_yaxis().set_visible(False)
    plt.show()

X, y = iter(test_iter).next()

true_labels = get_fashion_mnist_labels(y.numpy())
pred_labels = get_fashion_mnist_labels(net(X).argmax(dim=1).numpy())
titles = [true + '\n' + pred for true, pred in zip(true_labels, pred_labels)]

show_fashion_mnist(X[0:9], titles[0:9])