Loading... 使用Pytorch,结合cifar数据集,对其中的鸟和飞机进行二分 文章的废话内容可能会有点多,主要是刚上手Pytorch,以及刚刚接触深度学习(bushi) 总之,其实是对Pytorch上手的一个。。。历程 代码部分是书上的实例教程。 关于这个代码,其实,真的虽然和经典,但是未免太不对劲了。 等你到后面就知道了 本文会带给你一个几乎从零开始上手Pytorch进行深度学习的体验。 <div class="tip inlineBlock success"> 已经自己配置好环境和工具?直接上手! <button class=" btn m-b-xs btn-info btn-addon" onclick="window.open('https://blog.a152.top/usr/uploads/2023/11/3658467521.zip','_blank')"><i class="icon-emoji">💖</i>下载ipynb(zip)</button> </div> ## 首先导入我们的武器 ```python %matplotlib inline import numpy as np import torch from matplotlib import pyplot as plt torch.set_printoptions(edgeitems=2) torch.manual_seed(123) ``` 先来看看分类的标签数据,索引和分类标签一一对应,我们使用的是cifar10的数据集 可以说是经典中的经典 ```python class_names = [ "airplane", "automobile", "bird", "cat", "deer", "dog", "frog", "horse", "ship", "truck", ] ``` ## 准备数据集 ```python from torchvision import datasets, transforms data_path = "../data-unversioned/p1ch7/" cifar10 = datasets.CIFAR10( data_path, train=True, download=False, transform=transforms.Compose( [ transforms.ToTensor(), transforms.Normalize((0.4915, 0.4823, 0.4468), (0.2470, 0.2435, 0.2616)), ] ), ) ``` 因为我们之前已经下载过了,所以这里download设置为False,如果你没有下载,改成True就可以了 train=True时,我们加载的是训练数据集 transforms.Compose就跟sklearn的管道类似,我们把操作组合在一起,对数据集进行预处理,在这里我们可以看到 首先我们进行ToTensor,转换为张量数据,然后我们进行归一化,计算公式是 $(v[c]-mean[c])/stdev[c]$ 这个就是transforms.Normalize()做的操作,其中前三个是=mean,后三个是std,之所以是3个事因为有3个通道, 需要注意的是,在我们这个例子中,我们使用的是通道在前,也就是3 x 32 x 32的图片数据 而在一般情况下,我们会遇到通道在后的情况(Tensorflow标准),但是请记住,因为我们这个太过于简单和基础,就依着它把! 主要为什么需要通道在前呢,因为我们后面是简单的使用线性层(哈哈哈哈) 所以我们要把32*32\*3转换成3072 同理,我们来处理我们的验证集(只需要把train设置为False即可): ```python cifar10_val = datasets.CIFAR10( data_path, train=False, download=False, transform=transforms.Compose( [ transforms.ToTensor(), transforms.Normalize((0.4915, 0.4823, 0.4468), (0.2470, 0.2435, 0.2616)), ] ), ) ``` ## 描述一下要做什么? 我们要做一个鸟 飞机的一个二分任务,区分鸟和飞机的图片 对比MNIST的十分(划分数字0-9),我们的这个例子的图像是有通道的(3) 接下来就是要重新整理我们的数据集 0就是代表我么你的飞机,2就是代表我们的鸟 我们用简单的方式来筛选一下我们的数据集 ```python label_map = {0: 0, 2: 1} class_names = ["airplane", "bird"] cifar2 = [(img, label_map[label]) for img, label in cifar10 if label in [0, 2]] cifar2_val = [(img, label_map[label]) for img, label in cifar10_val if label in [0, 2]] ``` 我们可以通过 `len(cifar2)`来看看我们的数据集有多少数量, 执行结果自行查看,是10000 现在的思路,就是把图片当成一个一维向量作为输入, 一个图片 是 32 32 3,也就是有3072个特征 我们的模型就是有3072个输入,然后有一些隐藏特征,输出一个2维的 代表 飞机 还是 鸟 来看一个非常简单的model!!! ```python import torch.nn as nn n_out = 2 model = nn.Sequential( nn.Linear( 3072, # <1> 输入特征 512, # <2> 隐藏层的大小 ), nn.Tanh(), nn.Linear( 512, # <2> 隐藏层的大小 n_out, # <3> 输出特征· ), ) ``` 我们仅仅输出两个特征,很显然,我们使用简单的线性层,激活函数使用Tanh,在第二层直接输出分类结果(bushi) 下面就来介绍一下我们的分类器——Softmax 这个玩意通常用于多分类单标签的最后一层 ## 我们的输出 分类器,softmax 这是一个可微的函数,它获取一个向量并且输出另一个相同维度的向量 我们的约束条件是什么,假设我们是一个二维的 [0,1] 我们要求各项在0-1之间,并且总和加起来是1 $ \text{softmax}(x)_i = \frac{e^{x_i}}{\sum_{j=1}^{N} e^{x_j}} $ $\text{softmax}(x_1, x_2, x_3) = \left( \frac{e^{x_1}}{e^{x_1} + e^{x_2} + e^{x_3}}, \frac{e^{x_2}}{e^{x_1} + e^{x_2} + e^{x_3}}, \frac{e^{x_3}}{e^{x_1} + e^{x_2} + e^{x_3}} \right)$ $\text{softmax}(x_1, x_2, x_3, \ldots, x_n) = \left( \frac{e^{x_1}}{\sum_{i=1}^{n} e^{x_i}}, \frac{e^{x_2}}{\sum_{i=1}^{n} e^{x_i}}, \ldots, \frac{e^{x_n}}{\sum_{i=1}^{n} e^{x_i}} \right)$ 先来看看我们自己实现的Softmax ```python def softmax(x): return torch.exp(x) / torch.exp(x).sum() ``` 我们使用nn里面的Softmax,加到model里面: ```python model = nn.Sequential( nn.Linear(3072, 512), nn.Tanh(), nn.Linear(512, 2), nn.Softmax(dim=1) ) ``` 需要注意的是,Softmax是一个单调函数,但是值之间的比率没有被保留。 下面,我们需要思考我们的损失函数。(抽象的东西开始了)我们需要最大化的,是与正确的类 相关的概率,也就是 飞机是0,鸟是1,与正确类别相关的概率,这被我们称之为我们的模型给定参数的似然, 当正确的概率很低的时候,损失很高 但是概率高于其他选项的时候,损失应该很低, 因为我们想要的损失,是概率分类上的,而不是数值达到1 Negative Log Likelihood 它的表达式 是 $NLL=-sum(Log(out\_i[c\_i]))$ c\_i是样本i的目标类别,sum对N个样本进行求和 有点抽象? 综上,就是以下步骤: 运行正向传播,得到最后的线性层的输出 计算他们的Softmax 获得概率 取得与目标类别对应的预测概率(参数的似然值) 计算它的对数,在前面加一个符号,添加到损失中。 来看看具体代码: ```python model = nn.Sequential( nn.Linear(3072, 512), nn.Tanh(), nn.Linear(512, 2), nn.LogSoftmax(dim=1) ) loss = nn.NLLLoss() img, label = cifar2[0] out = model(img.view(-1).unsqueeze(0)) loss(out, torch.tensor([label])) ``` 可以看到,这个损失函数将一个批次的LogSoftmax的输出座位第一个参数,将类别索引的张量0或者1作为第二个参数 等等?为什么这里使用的是LogSoftmax函数? 因为计算模型的NLL时候,输入的背后约定是接近0的时候,取概率的对数是一件非常棘手的事情,使用LogSoftmax,可以在数值上保证稳定。 之后的不久,我们就正式开始训练了! 在训练之前,来改进一下model吧 我们采用小批量,在小批量上面进行训练 借助于DataLoader类,我们可以打乱数据和组织数据,这个函数接受至少一个数据集对象作为输入,以及一个batch_size和一个shuffle布尔值,这个shuffle表示是否在每个迭代周期开始时,重新打乱 ```python train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64, shuffle=True) ``` ```python import torch import torch.nn as nn import torch.optim as optim train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64, shuffle=True) model = nn.Sequential( nn.Linear(3072, 128), nn.Tanh(), nn.Linear(128, 2), nn.LogSoftmax(dim=1) ) learning_rate = 1e-2 optimizer = optim.SGD(model.parameters(), lr=learning_rate) loss_fn = nn.NLLLoss() n_epochs = 100 for epoch in range(n_epochs): for imgs, labels in train_loader: outputs = model(imgs.view(imgs.shape[0], -1)) loss = loss_fn(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() print("Epoch: %d, Loss: %f" % (epoch, float(loss))) ``` 经过简单的训练,我们的loss大概在0.0几左右 ``` Epoch: 98, Loss: 0.018574 Epoch: 99, Loss: 0.013330 ``` 我们加载一下我们的验证集,进行测试一下 测试的代码如下: ```python train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64, shuffle=False) correct = 0 total = 0 with torch.no_grad(): for imgs, labels in train_loader: outputs = model(imgs.view(imgs.shape[0], -1)) _, predicted = torch.max(outputs, dim=1) total += labels.shape[0] correct += int((predicted == labels).sum()) print("Accuracy: %f" % (correct / total)) val_loader = torch.utils.data.DataLoader(cifar2_val, batch_size=64, shuffle=False) correct = 0 total = 0 with torch.no_grad(): for imgs, labels in val_loader: outputs = model(imgs.view(imgs.shape[0], -1)) _, predicted = torch.max(outputs, dim=1) total += labels.shape[0] correct += int((predicted == labels).sum()) print("Accuracy: %f" % (correct / total)) ``` 上半部分,是我们的训练精度 下半部分,是我们的验证精度。 在我本地得到的结果,差不多是 ``` Accuracy: 0.999500 Accuracy: 0.810500 ``` 很明显,比较严重的过拟合。 我们尝试修改一下模型的大小和层数,简单随意的做法可能是: ```python model = nn.Sequential( nn.Linear(3072, 1024), nn.Tanh(), nn.Linear(1024, 512), nn.Tanh(), nn.Linear(512, 128), nn.Tanh(), nn.Linear(128, 2), ) loss_fn = nn.CrossEntropyLoss() ``` 在这里我们可以看到,我们试图添加中间层来逐步减少特征数量,期望中间层更好的将信息压缩。 使得中间层输出越来越短。 而原本模型中 ,nn.LogSoftmax()和nn.NLLLoss()的组合就相当于使用nn.CrossEntropyLoss() 而这个函数你应该耳熟能详,它有一个名字,叫做交叉熵损失函数 现在就让我们来试试替换掉最后的LosSoftmax 请注意,替换之后,我们需要显式的通过Softmax传递出我们想要的概率(然而Softmax是单调的,所以我们可以通过大小判断最后的分类结果) 让我们看看修改后的代码: ```python import torch import torch.nn as nn import torch.optim as optim train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64, shuffle=True) model = nn.Sequential( nn.Linear(3072, 1024), nn.Tanh(), nn.Linear(1024, 512), nn.Tanh(), nn.Linear(512, 128), nn.Tanh(), nn.Linear(128, 2), # nn.Softmax(dim=1) ) learning_rate = 1e-2 optimizer = optim.SGD(model.parameters(), lr=learning_rate) loss_fn = nn.CrossEntropyLoss() n_epochs = 100 for epoch in range(n_epochs): for imgs, labels in train_loader: outputs = model(imgs.view(imgs.shape[0], -1)) loss = loss_fn(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() print("Epoch: %d, Loss: %f" % (epoch, float(loss))) ``` ``` Epoch: 94, Loss: 0.001689 Epoch: 95, Loss: 0.004958 Epoch: 96, Loss: 0.002165 Epoch: 97, Loss: 0.001450 Epoch: 98, Loss: 0.009242 Epoch: 99, Loss: 0.002103 ``` 可以看到,我们的loss已经很低了 ```python train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64, shuffle=False) correct = 0 total = 0 with torch.no_grad(): for imgs, labels in train_loader: outputs = model(imgs.view(imgs.shape[0], -1)) _, predicted = torch.max(outputs, dim=1) total += labels.shape[0] correct += int((predicted == labels).sum()) print("Accuracy: %f" % (correct / total)) val_loader = torch.utils.data.DataLoader(cifar2_val, batch_size=64, shuffle=False) correct = 0 total = 0 with torch.no_grad(): for imgs, labels in val_loader: outputs = model(imgs.view(imgs.shape[0], -1)) _, predicted = torch.max(outputs, dim=1) total += labels.shape[0] correct += int((predicted == labels).sum()) print("Accuracy: %f" % (correct / total)) ``` 训练精度上高的离谱,这也就意味着过拟合,精度大概有0.99 验证集上 精度只有0.8 这说明了什么? 在两种情况下,模型都过拟合了。即使我们选择了一个更大的模型 接下来我们就看看我们的模型参数 ```python [p.numel() for p in model.parameters()] ``` 返回一个列表,结果是这样的 ``` [3145728, 1024, 524288, 512, 65536, 128, 256, 2] ``` ```python sum([p.numel() for p in model.parameters() if p.requires_grad == True]) ``` 这里使用requires_grad==True来判断是不是可训练的 sum()求和表示一共有多少个参数 ## 总结 好了,到现在为止,我们直接上手了Pytorch,并简单的训练了一个全连接分类器, 不管图片的平移不变形,单纯从像素分布上,来判断鸟还是飞机。 后面,我们将介绍,卷积神经网络 我们这个实例有很多缺陷,或许你应该已经知道了。 我们把一个二维图像展开来看成一个一维的。(这是后面的内容了) Last modification:November 12, 2023 © Allow specification reprint Support Appreciate the author AliPayWeChat Like 5 如果觉得我的内容对你有用,请随意赞赏
One comment
OωO