PyTorch多分类问题

多分类问题(刘二大人PyTorch第九讲)

之前的糖尿病预测案例是二分类问题,这一讲使用MNIST数据集讲解了多分类问题。介绍了softmax的概念和作用,数据集加载中的transform(转为张量、归一化等),设计了五层的线性变换层模型,使用MNIST进行模型训练与测试。
二分类本质上是特殊的多分类,其区别在于:
(1)模型输出:
     二分类模型通常输出一个表示属于某一类别的概率值,一般是经过sigmoid等激活函数处理后得到0-1之间的概率数值。假设概率值为P,则表示该数据属于类别1的概率为P,属于类别2的概率为1-P;
    多分类模型的最后一层不使用激活函数,而是使用softmax函数将原始的未激活的得分转换成概率分布。输出一个概率分布向量,其长度等于类别数,每个元素表示样本属于对应类别的概率,确定数据类别时,取这个概率分布中数值最大的那个。
(2)损失函数:二分类通常使用BCELoss,多分类通常使用CrossEntropyLoss。
     BCELoss计算方式:BCELoss=-(y(log\hat{y}+(1-y)log(1-\hat{y})),其中y\hat{y}分别表示真是标签和预测标签。
     CrossEntropyLoss应用softmax将原始得分转换为概率分布\sigma(z)_j=\frac{e^{z_j}}{\sum_{k=1}^{K}e^{z_k}},其中z是输入的logits(原始得分),K是类别总数,j表示第j个类别,得到单个样本属于各个类别的概率分布;再根据真实标签对应的概率分量进行交叉熵损失的计算,如对于某个样本,已知真实标签类别索引为1,预测的概率分布p=[0.628,0.231,0.141],根据交叉熵损失公式计算损失为-log(0.231)≈1.469,则对于该样本,经过softmax函数处理和交叉熵损失计算后得到交叉熵损失约为1.469。即CrossEntropyLoss <==> LogSoftmax+NLLoss(负对数似然函数)
说明:
softmax的输入为原始得分,不需要做非线性激活变换。softmax的作用:通过指数变换将输入的负数转换为正数,使得K个分类的输出的值都是大于等于0的;所有类的概率和为1。

过程与代码

1. 加载数据集

  • MNIST数据集说明:手写数字识别组成,包含0-9这10个数字的手写样本(10分类)。训练集60000张图片、测试集10000张图片。
    数据特点:每张图像都是28*28像素的灰度图像,图像大小统一;单通道;每张图片对应一个标签,标签是0-9之间的一个数字。
  • 数据集的加载:
    (1)指定路径
    (2)做数据预处理
    (3)使用datasets和DataLoader相互配合进行数据加载

代码:

class CustomMNIST(datasets.MNIST):
    mirrors = [
        'https://ossci-datasets.s3.amazonaws.com/mnist/',
    ]


batch_size = 64
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.1307),(0.3081,))
])
train_dataset = CustomMNIST(root=r'F:\MyPracticeProject\刘二大人\dataset/mnist/', train=True, download=True, transform=transform)
train_loader = DataLoader(train_dataset, shuffle=True, batch_size=batch_size)
test_dataset = CustomMNIST(root=r'F:\MyPracticeProject\刘二大人\dataset/mnist/', train=False, download=True, transform=transform)
test_loader = DataLoader(test_dataset, shuffle=False, batch_size=batch_size)

说明:

  • 关于数据集路径:
    可以手动下载后放在本地进行加载,也可以直接选择在线下载,我们选择在线下载。但运行时显示下载失败,原因是datasets.MNIST中默认的下载路径“http://yann.lecun.com/exdb/mnist/”不行,因此重新定义类,重写了MNIST的下载路径。

  • 关于预处理:
    在进行数据集加载时,需要对图片做一定的处理,如将训练数据集调整为统一大小,做随机裁剪、随机旋转、随机水平翻转等数据增强操作,转化为张量,做归一化等(这里只做了最重要的两个操作:转为张量、归一化)。这些预处理方法在torchvision中的datasets中有提供:
    (1)transforms.ToTensor()
    作用:将图像数据从常见的PIL图像格式或Numpy数组格式转换为PyTorch能够处理的张量格式,以便训练和推理。
    转换过程:对于PIL图像(PyTorch读取图像时用的是PIL,现在用pillow),原始输入图像是像素值在0-255之间的整数,神经网络的输入希望是-1—+1之间的数,最好遵从正态分布,transforms.ToTensor()能将原始的图像转换为图像张量,存储形式为0.0-1.0之间的浮点数。同时,会对图像的通道顺序进行调整,将PIL图像的通道顺序(H,W,Channel)转换为PyTorch中Tensor的通道顺序(Channel,H,W)。(本案例中,将输入的(1,28,28)转换为(28,28,1))。
    (2)transforms.Normalize((每个通道的均值),(每个通道的标准差))归一化
    作用:把图像像素值调整到特定范围,有助于提升深度学习模型的训练效果与收敛速度。

  • 关于数据集的加载:
    (1)定义好transform后,放入MNIST数据集里,这样在读取第i个数据样本的时候,直接用transform处理数据。
    (2)使用torch.utils.data中的DataLoader进行数据加载。
    DataLoader的参数包括dataset(指定要加载的数据集)、batch_size、shuffle(true或false,训练集打乱、测试集不打乱)、num_workers(制定用于数据加载的子进程数量)、worker_init_fn(指定每个子进程初始化时调用的函数,可以用于每个子进程中设置随机种子等操作)等。

注意:如果是在线下载数据集,路径最好使用绝对路径(我使用了相对路径,运行代码后,代码跑通了,但是找不到下载的数据集在哪存放着)

2. 模型设计

使用5层的全连接层进行图像特征提取,除了分类层,前面四层都使用relu激活函数进行非线性变换。模型的输入为一组图片的特征(N,1,28,28)(N表示batch_size,1表示通道数为1,28和28为图片的高和宽),输出为计算得到的图片属于各类别的概率(N,10)(输入是一个batch_size的图片tensor,但实际是一张图片一张图片进行处理)

class Net(torch.nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.l1 = torch.nn.Linear(784, 512)
        self.l2 = torch.nn.Linear(512, 256)
        self.l3 = torch.nn.Linear(256, 128)
        self.l4 = torch.nn.Linear(128, 64)
        self.l5 = torch.nn.Linear(64, 10)
    
    def forward(self, x):
        x = x.view(-1, 784)   # 将输入的大小为28*28的图片张量展平为一维向量输入到全连接神经网络中
        x = F.relu(self.l1(x))
        x = F.relu(self.l2(x))
        x = F.relu(self.l3(x))
        x = F.relu(self.l4(x))
        return self.l5(x)    # 下一步:对x进行softmax归一化与分类操作

model = Net()  # 模型实例化

说明:
x=x.view(-1,784):
对于全连接神经网络,其输入数据需要展平为一维向量,这个一维向量可以看作是一个形状为 (1, n) 的矩阵(n维特征数量),或者批量输入时形状为(batch_size, n)的矩阵。在该案例中,将28×28大小的手写数字图片张量展平为长度为784(28×28)的一维向量作为输入。因为设定的batch_size为64,则输入矩阵的形状为(64, 784)。
对于CNN,输入为多通道矩阵,即三维张量(单张图像)或四维张量(批量图像)。RNN、Transformer输入为序列形式的数据。图神经网络的输入为图结构数据。

3. 损失函数和优化器

criterion = torch.nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)

说明:
二分类任务使用BCELoss,多分类使用CrossEntropyLoss损失函数。

  • 二分类最后一层经过softmax激活后得到的是一个0-1之间的概率,表示属于类别1的概率,损失使用BCELoss进行计算。BCELoss需要的参数值有两个,模型预测得到的\hat{y}(0-1之间的概率值)与真实标签y的值(0或1),计算的实质,是取预测值的log对数值的负数形式(log函数在x为0-1时值小于0)。
  • 多分类的最后一层不需要激活函数(避免重复计算,CrossEntropyLoss内部已经集成了类似softmax的操作,如果在模型最后一层使用 softmax 激活函数,会将 logits 转换为概率分布,之后在计算损失时又要基于这些概率进行对数运算。;反向传播中对logits直接计算梯度比先经过激活函数再算梯度更加简洁高效,激活函数的引入会增加计算图的复杂度,而直接用 logits 配合专门设计的损失函数,能让梯度计算更顺畅,更有效地更新模型参数)。
    CrossEntropyLoss的计算过程是,对于模型最后一层的输出(模型计算的该输入数据属于各个类别的原始数值组成的张量,如[0.2, 0.1, -0.1]),先使用softmax将得到的得分转换为样本属于各个类别的概率分布(如[0.38,0.34,0.28])。然后根据真实标签的索引取出预测概率分布中对应的值,计算它交叉熵(-log\hat{y},取对数的负数),如对于某个样本,已知真实标签类别索引为1,对于上述步骤计算得到的[0.38,0.34,0.28],计算-log(0.34),得到损失值。

4. 训练与测试

训练单元:前馈(计算模型对数据预测的概率分布、损失),反向(损失反向传播、计算梯度、更新权重参数)
测试单元:前馈(计算模型对数据预测的概率分布),计算模型预测的准确率 并进行输出。

def train(epoch):
    running_loss = 0.0
    for batch_idx, data in enumerate(train_loader, 0):
        inputs,targets = data
        optimizer.zero_grad()  # 优化器梯度清零

        # 前馈,反向,更新
        outputs = model(inputs)
        loss = criterion(outputs, targets)
        loss.backward()
        optimizer.step()

        # 计算损失
        running_loss = running_loss + loss.item()        # 注意:loss是个张量,要取其数值,应该用.item()
        if batch_idx % 300 == 299:
            print('epoch:%d, batch:%5d, loss:%.3f'%(epoch+1, batch_idx+1, running_loss/300))

# 测试
# 不需要反向传播,只需要算正向
def test():
    correct = 0
    total = 0
    with torch.no_grad():    # 测试过程不需要梯度更新
        for data in test_loader:     # 对于test_loader中的每条数据
            images, labels = data    # 取数据及其标签
            outputs = model(images)  # 传入数据进行预测
            # 预测类别最大值的索引
            _,predicted = torch.max(outputs.data, dim=1)  # 从维度1((batch_size,N),从维度1取值最大的那个为预测值)
            total = total + labels.size(0)
            correct = correct + (predicted==labels).sum().item()
    print('Accuracy on test set: %d %%' %(100*correct/total))   # 跑完所有测试集后输出正确率

说明:
(1)images, labels = data
当使用torchvision.datasets中的数据集类(如MNIST、CIFAR10等)或自定义数据集类时,这些数据集类会按照一定的规则读取和处理原始数据,将其整理成特征(对应inputs)和标签(对应labels)的形式。然后DataLoader会对数据集进行封装,按照设定的batch_size等参数将数据分成批次。在训练或测试循环中,通过迭代DataLoader,每次取出一个批次的数据,这个批次的数据就包含了inputs和targets,并通过inputs,targets=data进行解包赋值。
(2)_, predicted = torch.max(outputs.data, dim=1)
output为(batch_size, N)的张量,从维度1取出取值最大的预测值,返回值为最大值的值、索引(代表类别),我们需要的是最大值的索引。
(3)total = total + labels.size(0)
计算测试数据的总数。labels是(batch_size,1)的张量,labels.size得到的是元组(N,1),labels.size(0)得数值N。
(4)correct = correct + (predicted==labels).sum().item()
这行代码将当前批次中预测正确的样本数量累加到 correct 变量中。
predicted是预测的类别索引(N个一维向量组成的张量(向量中存储的是数据所属类别的索引)),labels是真实类别索引(是形状与predicted形状一致的torch.Tensor对象,里面存储的是每条数据属于某个类别的索引 )。
predicted==labels会对predicted和labels中的对应元素逐元素比较,返回一个布尔类型的torch.Tensor,True表示相同、False表示不同。
(predicted==labels).sum() 计算的是预测正确的样本数量,返回的结果仍然是一个 torch.Tensor 对象。
item() 方法用于将只包含一个元素的 torch.Tensor 转换为 Python 的标量(如整数或浮点数)。

整体代码

import torch 
from torchvision import transforms   # 图像处理
from torchvision import datasets    # 数据集
from torch.utils.data import DataLoader  # 数据加载器
import torch.nn.functional as F
import torch.optim as optim 

class CustomMNIST(datasets.MNIST):
    mirrors = [
        'https://ossci-datasets.s3.amazonaws.com/mnist/',
    ]


batch_size = 64
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.1307,),(0.3081,))
])
train_dataset = CustomMNIST(root=r'F:\MyPracticeProject\刘二大人\dataset/mnist/', train=True, download=True, transform=transform)
train_loader = DataLoader(train_dataset, shuffle=True, batch_size=batch_size)
test_dataset = CustomMNIST(root=r'F:\MyPracticeProject\刘二大人\dataset/mnist/', train=False, download=True, transform=transform)
test_loader = DataLoader(test_dataset, shuffle=False, batch_size=batch_size)

# 2. 模型设计
class Net(torch.nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.l1 = torch.nn.Linear(784, 512)
        self.l2 = torch.nn.Linear(512, 256)
        self.l3 = torch.nn.Linear(256, 128)
        self.l4 = torch.nn.Linear(128, 64)
        self.l5 = torch.nn.Linear(64, 10)
    
    def forward(self, x):
        x = x.view(-1, 784)
        x = F.relu(self.l1(x))
        x = F.relu(self.l2(x))
        x = F.relu(self.l3(x))
        x = F.relu(self.l4(x))
        return self.l5(x)  # 下一步:对x进行softmax归一化与分类操作

model = Net()


# 3.设计损失函数和优化器
criterion = torch.nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)

# 4. 训练
def train(epoch):
    running_loss = 0.0
    for batch_idx, data in enumerate(train_loader, 0):
        inputs,targets = data
        optimizer.zero_grad()  # 优化器梯度清零

        # 前馈,反向,更新
        outputs = model(inputs)
        loss = criterion(outputs, targets)
        loss.backward()
        optimizer.step()

        # 计算损失
        running_loss = running_loss + loss.item()        # 注意:loss是个张量,要取其数值,应该用.item()
        if batch_idx % 300 == 299:
            print('epoch:%d, batch:%5d, loss:%.3f'%(epoch+1, batch_idx+1, running_loss/300))

# 测试
# 不需要反向传播,只需要算正向
def test():
    correct = 0
    total = 0
    with torch.no_grad():    # 测试过程不需要梯度更新
        for data in test_loader:     # 对于test_loader中的每条数据
            images, labels = data    # 取数据及其标签
            outputs = model(images)  # 传入数据进行预测
            # 预测类别最大值的索引
            _,predicted = torch.max(outputs.data, dim=1)  # 从维度1((batch_size,N),从维度1取值最大的那个为预测值)
            total = total + labels.size(0)
            correct = correct + (predicted==labels).sum().item()
    print('Accuracy on test set: %d %%' %(100*correct/total))   # 跑完所有测试集后输出正确率

if __name__=="__main__":
    for epoch in range(10):
        train(epoch)
        test()

运行结果:

epoch:1, batch:  300, loss:2.263
epoch:1, batch:  600, loss:3.486
epoch:1, batch:  900, loss:3.947
Accuracy on test set: 89 %
epoch:2, batch:  300, loss:0.339
...
Accuracy on test set: 97 %

结果说明:
准确率在97%后就上不去了,原因是,使用全连接神经网络做图像处理时,忽略了一些对局部信息的处理,把所有元素之间做了全连接,处理图像时更关系更高级别的抽象特征,全连接是非常原始的特征,如果用特征提取做分类训练,可能效果会更好一点,所以自动提取特征,如CNN。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容