过去几年已经发布了许多深度学习框架。其中,来自Facebook AI Research的PyTorch非常独特,因其优雅,灵活,速度和简单性而获得广泛采用。如果没有足够的研究支持,大多数深度学习框架对于应用程序开发来说太具体,或者对于没有足够支持应用程序开发的研究来说太具体。
但是,PyTorch通过提供对应用程序开发人员非常友好的API来模糊两者之间的界限,同时提供了轻松定义自定义图层并全面控制训练过程(包括梯度传播)的功能。这使它非常适合开发人员和研究人员。
PyTorch所有功能的主要特点是其逐个运行的方法,这使得可以即时更改神经网络的结构,而不像其他依赖不灵活静态图形的深度学习库。
在这篇文章中,您将从头学习如何使用Pytorch构建完整的图像分类管道。
安装PyTorch
由于预构建的二进制文件在所有系统中都能正常工作,因此安装Pytorch是一件轻而易举的事情(注意Python版本,及对于的GPU)。
安装在WINDOWS上
仅限CPU:
pip3 install http://download.Pytorch.org/whl/cpu/torch-0.4.0-cp35-cp35m-win_amd64.whl
pip3 install torchvision
GPU支持
pip3 install http://download.Pytorch.org/whl/cu80/torch-0.4.0-cp35-cp35m-win_amd64.whl
pip3 install torchvision
安装在LINUX上
仅限CPU:
pip3 install torch torchvision
GPU支持
pip3 install http://download.Pytorch.org/whl/cpu/torch-0.4.0-cp35-cp35m-linux_x86_64.whl
pip3 install torchvision
在OSX上安装
仅限CPU:
pip3 install torch torchvision
GPU支持
有关在OSX上安装gpu支持的说明,请访问Pytorch.org。
卷积神经网络简介
我们将在这篇文章中使用的模型属于一类称为卷积神经网络(CNN)的神经网络。CNN主要是一堆卷积层,通常与标准化层和激活层交织。卷积神经网络的组成部分总结如下。
CNN ?- 一堆卷积层
卷积层 ?- 一个检测某些特征的层。有特定数量的通道。
通道 ?- 检测图像中的特定功能。
内核/过滤器 ?- 每个通道中要检测的功能。它有一个固定的大小,通常是3 x 3。
简要解释一下,卷积层只是一个特征检测层。每个卷积层都有特定数量的通道; 每个通道检测图像中的特定功能。要检测的每个功能通常称为内核或过滤器。内核的大小是固定的,通常使用大小为3 x 3的内核。
例如,具有64个通道和3×3的内核大小的卷积层将检测64个不同的特征,每个大小为3×3。
定义模型结构
模型在PyTorch中由扩展Module类的自定义类定义。torch.nn包中可以找到模型的所有组件。因此,我们只需导入这个包。这里我们将建立一个简单的CNN模型,用于对来自CIFAR 10数据集的RGB图像进行分类。CIFAR10数据集包含50,000个训练图像和10,000个尺寸为32 x 32的测试图像。Python代码如下
# Import needed packages
import torch
import torch.nn as nn
class SimpleNet(nn.Module):
def __init__(self, num_classes=10):
super(SimpleNet, self).__init__()
self.conv1 = nn.Conv2d(in_channels=3, out_channels=12, kernel_size=3, stride=1, padding=1)
self.relu1 = nn.ReLU()
self.conv2 = nn.Conv2d(in_channels=12, out_channels=12, kernel_size=3, stride=1, padding=1)
self.relu2 = nn.ReLU()
self.pool = nn.MaxPool2d(kernel_size=2)
self.conv3 = nn.Conv2d(in_channels=12, out_channels=24, kernel_size=3, stride=1, padding=1)
self.relu3 = nn.ReLU()
self.conv4 = nn.Conv2d(in_channels=24, out_channels=24, kernel_size=3, stride=1, padding=1)
self.relu4 = nn.ReLU()
self.fc = nn.Linear(in_features=16 * 16 * 24, out_features=num_classes)
def forward(self, input):
output = self.conv1(input)
output = self.relu1(output)
output = self.conv2(output)
output = self.relu2(output)
output = self.pool(output)
output = self.conv3(output)
output = self.relu3(output)
output = self.conv4(output)
output = self.relu4(output)
output = output.view(-1, 16 * 16 * 24)
output = self.fc(output)
return output
在上面的Python代码中,我们首先定义一个名为SimpleNet的新类,它扩展了nn.Module类。在这个类的构造函数中,我们指定了网络中的所有层。我们的网络结构为卷积- relu -卷积- relu - pool -卷积- relu -卷积- relu - linear。
为了弄清楚每一层发生了什么,让我们逐一检查。
卷积层
nn.Conv2d(in_channels=3, out_channels=12, kernel_size=3, stride=1, padding=1)
假设我们的输入是具有3个通道(RED-GREEN-BLUE)的RGB图像,我们将数量指定in_channels为3.接下来,我们要将12个特征检测器应用于图像,因此我们指定的数量为out_channels 12。这里我们使用标准的3x3内核大小(简单定义为3)。步幅设置为1,并且应该始终如此,除非您打算缩小图像的尺寸。通过将步幅(stride )设置为1,卷积一次将移动1个像素。最后,我们将padding设置为1:这确保我们的图像填充零以保持输入和输出大小相同。
基本上,你目前不需要太担心步幅和padding。把重点放在in_channels和out_channels。
请注意,out_channels在这一层,作为in_channels下一层,如下所示。
nn.Conv2d(in_channels = 12,out_channels = 12,kernel_size = 3,stride = 1,padding = 1)
Relu
这是标准的Relu激活函数,它基本上将所有传入的特征阈值设为0或更大。简单来说,当将Relu应用于传入特征时,小于0的任何数字都变为零,而其他数保持不变。
MaxPool2d
此图层通过将图像的尺寸设置kernel_size为2来减少图像的尺寸,从而将图像的宽度和高度减少2倍。实质上是使图像的2 x 2区域中的像素达到最大值,并使用该图像代表整个地区; 因此4个像素成为一个。
线性(linear)
我们网络的最后一层几乎总是线性层。它是一个标准的、全连接层,计算我们每个类的分数——在本例中是10个类。
请注意,我们必须在将最后一个conv-relu层的整个feature map传递到图像之前,将其flatten 。最后一层有24个输出通道,由于2×2的max汇聚,此时我们的图像已经变成16 x 16(32/2 = 16)。我们的平面图像是16 x 16 x 24。我们用Python代码来做这个:
output = output.view(-1, 16 * 16 * 24)
在我们的线性层中,我们必须指定数量为input_features16 x 16 x 24,并且数量output_features应该与我们期望的类数相对应。
请注意PyTorch中定义模型的简单规则。在构造函数中定义层,并在forward函数中传入所有输入。
模块化
上面的代码很酷但不够酷 - 如果我们要编写非常深的网络,它看起来很麻烦。更简洁的代码的关键是模块化。在上面的例子中,我们可以将卷积和relu放在一个单独的模块中,并将大部分这个模块堆叠在我们的SimpleNet中。
为此,我们首先定义一个新模块,Python代码如下所示
class Unit(nn.Module):
def __init__(self, in_channels, out_channels):
super(Unit, self).__init__()
self.conv = nn.Conv2d(in_channels=in_channels, kernel_size=3, out_channels=out_channels, stride=1, padding=1)
self.bn = nn.BatchNorm2d(num_features=out_channels)
self.relu = nn.ReLU()
def forward(self, input):
output = self.conv(input)
output = self.bn(output)
output = self.relu(output)
return output
把上面的内容看作是一个迷你网络,它是我们更大的SimpleNet的一部分。
正如您可以看到的,这个单元由卷积-batchnormaliz -relu组成。
不像第一个例子,这里我在ReLU之前包含了BatchNorm2d。
批量归一化将所有输入归一化,以具有零均值和单位方差。这大大提高了CNN模型的准确性。
定义了上面的单元后,我们现在可以将它们堆在一起。
class Unit(nn.Module):
def __init__(self,in_channels,out_channels):
super(Unit,self).__init__()
self.conv = nn.Conv2d(in_channels=in_channels,kernel_size=3,out_channels=out_channels,stride=1,padding=1)
self.bn = nn.BatchNorm2d(num_features=out_channels)
self.relu = nn.ReLU()
def forward(self,input):
output = self.conv(input)
output = self.bn(output)
output = self.relu(output)
return output
class SimpleNet(nn.Module):
def __init__(self,num_classes=10):
super(SimpleNet,self).__init__()
#Create 14 layers of the unit with max pooling in between
self.unit1 = Unit(in_channels=3,out_channels=32)
self.unit2 = Unit(in_channels=32, out_channels=32)
self.unit3 = Unit(in_channels=32, out_channels=32)
self.pool1 = nn.MaxPool2d(kernel_size=2)
self.unit4 = Unit(in_channels=32, out_channels=64)
self.unit5 = Unit(in_channels=64, out_channels=64)
self.unit6 = Unit(in_channels=64, out_channels=64)
self.unit7 = Unit(in_channels=64, out_channels=64)
self.pool2 = nn.MaxPool2d(kernel_size=2)
self.unit8 = Unit(in_channels=64, out_channels=128)
self.unit9 = Unit(in_channels=128, out_channels=128)
self.unit10 = Unit(in_channels=128, out_channels=128)
self.unit11 = Unit(in_channels=128, out_channels=128)
self.pool3 = nn.MaxPool2d(kernel_size=2)
self.unit12 = Unit(in_channels=128, out_channels=128)
self.unit13 = Unit(in_channels=128, out_channels=128)
self.unit14 = Unit(in_channels=128, out_channels=128)
self.avgpool = nn.AvgPool2d(kernel_size=4)
#Add all the units into the Sequential layer in exact order
self.net = nn.Sequential(self.unit1, self.unit2, self.unit3, self.pool1, self.unit4, self.unit5, self.unit6
,self.unit7, self.pool2, self.unit8, self.unit9, self.unit10, self.unit11, self.pool3,
self.unit12, self.unit13, self.unit14, self.avgpool)
self.fc = nn.Linear(in_features=128,out_features=num_classes)
def forward(self, input):
output = self.net(input)
output = output.view(-1,128)
output = self.fc(output)
return output
这是一个完整的15层网络,由14个卷积层、14个Relu层、14个批量归一化层、4个汇集层和1个线性层组成,共计62层。这是通过使用子模块和顺序类来实现的。
上面的代码是由单元的堆栈和中间的池层组成的。
请注意,如何使代码更紧凑,把所有层,除了完全连接层到一个连续的类。这进一步简化了前向函数中的代码。
self.net = nn.Sequential(self.unit1, self.unit2, self.unit3, self.pool1, self.unit4, self.unit5, self.unit6, self.unit7, self.pool2, self.unit8, self.unit9, self.unit10, self.unit11, self.pool3,self.unit12, self.unit13, self.unit14, self.avgpool)
最后一个单元之后的AvgPooling层计算每个通道中所有激活的平均值。该单元的输出具有128个通道,并且在共用3次后,我们的32×32图像变为4×4。我们应用内核大小为4的AvgPool2D,将我们的特征映射为1×1×128。
self.avgpool = nn.AvgPool2d(kernel_size = 4)
因此,线性层将具有1×1×128 = 128个输入特征。
self.fc = nn.Linear(in_features = 128,out_features = num_classes)
我们还将网络的输出平铺为128个特征。
output = output.view(-1,128)
加载和增加数据
由于采用torchvision软件包,PyTorch中的数据加载非常简单。为了演示这一点,我将加载我们将在本教程中使用的CIFAR10数据集。
首先,我们需要三个额外的导入语句
from torchvision.datasets import CIFAR10
from torchvision.transforms import transforms
from torch.utils.data import DataLoader
要加载数据集,我们执行以下操作:
定义要在图像上应用的转换
使用torchvision加载数据集
创建一个DataLoader的实例来保存图像
我们做的训练集如下:
#Define transformations for the training set, flip the images randomly, crop out and apply mean and std normalization
train_transformations = transforms.Compose([
transforms.RandomHorizontalFlip(),
transforms.RandomCrop(32,padding=4),
transforms.ToTensor(),
transforms.Normalize((0.5,0.5,0.5), (0.5,0.5,0.5))
])
#Load the training set
train_set =CIFAR10(root="./data",train=True,transform=train_transformations,download=True)
#Create a loder for the training set
train_loader = DataLoader(train_set,batch_size=32,shuffle=True,num_workers=4)
首先,我们使用transform.Compose传递一系列转换。RandomHorizontalFlip会水平地随机翻转图像。RandomCrop随机裁剪图像。以下是一个水平翻转的例子。
最后,两个最重要的; ToTensor将图像转换为PyTorch可用的格式。使用下面给出的值进行归一化将使我们所有的像素范围在-1到+1之间。请注意,在说明转换时,ToTensor和Normalize必须按照上述定义的顺序排列。其主要原因是其他变换应用于PIL图像的输入,但是,在应用归一化之前,必须将其转换为pytorch张量。
数据增强有助于模型正确分类图像,而不考虑其显示的角度。
接下来,我们使用CIFAR10类加载训练集,最后我们为训练集创建一个加载器,指定批量大小为32的图像。
对于下面的测试集重复该操作,只是转换只包括ToTensor和Normalize。我们不在测试集上应用其他类型的转换。
# Define transformations for the test set
test_transformations = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
# Load the test set, note that train is set to False
test_set = CIFAR10(root="./data", train=False, transform=test_transformations, download=True)
# Create a loder for the test set, note that both shuffle is set to false for the test loader
test_loader = DataLoader(test_set, batch_size=32, shuffle=False, num_workers=4)
您第一次运行此代码时,约170 mb的数据集将被下载到您的系统中
训练模型
使用Pytorch训练神经网络是一个非常明确的过程,可以让您完全控制训练过程中发生的情况。让我们一步一步地完成这个过程。
您应该将Adam优化器导入为:
from torch.optim import Adam
步骤1:实例化模型,创建优化器和损失函数
from torch.optim import Adam
# Check if gpu support is available
cuda_avail = torch.cuda.is_available()
# Create model, optimizer and loss function
model = SimpleNet(num_classes=10)
#if cuda is available, move the model to the GPU
if cuda_avail:
model.cuda()
#Define the optimizer and loss function
optimizer = Adam(model.parameters(), lr=0.001, weight_decay=0.0001)
loss_fn = nn.CrossEntropyLoss()
步骤2:编写一个函数来调整学习率
# Create a learning rate adjustment function that divides the learning rate by 10 every 30 epochs
def adjust_learning_rate(epoch):
lr = 0.001
if epoch > 180:
lr = lr / 1000000
elif epoch > 150:
lr = lr / 100000
elif epoch > 120:
lr = lr / 10000
elif epoch > 90:
lr = lr / 1000
elif epoch > 60:
lr = lr / 100
elif epoch > 30:
lr = lr / 10
for param_group in optimizer.param_groups:
param_group["lr"] = lr
这个功能在每30个epochs之后基本上将学习率分成10倍。
第3步:编写功能来保存和评估模型
def save_models(epoch):
torch.save(model.state_dict(), "cifar10model_{}.model".format(epoch))
print("Chekcpoint saved")
def test():
model.eval()
test_acc = 0.0
for i, (images, labels) in enumerate(test_loader):
if cuda_avail:
images = Variable(images.cuda())
labels = Variable(labels.cuda())
# Predict classes using images from the test set
outputs = model(images)
_, prediction = torch.max(outputs.data, 1)
test_acc += torch.sum(prediction == labels.data)
# Compute the average acc and loss over all 10000 test images
test_acc = test_acc / 10000
return test_acc
为了评估模型在测试集上的精度,我们遍历了测试加载器。在每一步中,我们都会将图像和标签移动到GPU(如果可用)并将其包装在变量中。图像被传递到模型中以获得预测。选取最大预测值,然后与实际类别进行比较以获得精度。最后我们返回平均精度。
第四步:编写训练功能
def train(num_epochs):
best_acc = 0.0
for epoch in range(num_epochs):
model.train()
train_acc = 0.0
train_loss = 0.0
for i, (images, labels) in enumerate(train_loader):
# Move images and labels to gpu if available
if cuda_avail:
images = Variable(images.cuda())
labels = Variable(labels.cuda())
# Clear all accumulated gradients
optimizer.zero_grad()
# Predict classes using images from the test set
outputs = model(images)
# Compute the loss based on the predictions and actual labels
loss = loss_fn(outputs, labels)
# Backpropagate the loss
loss.backward()
# Adjust parameters according to the computed gradients
optimizer.step()
train_loss += loss.cpu().data[0] * images.size(0)
_, prediction = torch.max(outputs.data, 1)
train_acc += torch.sum(prediction == labels.data)
# Call the learning rate adjustment function
adjust_learning_rate(epoch)
# Compute the average acc and loss over all 50000 training images
train_acc = train_acc / 50000
train_loss = train_loss / 50000
# Evaluate on the test set
test_acc = test()
# Save the model if the test acc is greater than our current best
if test_acc > best_acc:
save_models(epoch)
best_acc = test_acc
# Print the metrics
print("Epoch {}, Train Accuracy: {} , TrainLoss: {} , Test Accuracy: {}".format(epoch, train_acc, train_loss,
上面的训练功能是高度注释的; 但是,您仍然可能会因为几件事而感到困惑。仔细检查上面详细说明的内容非常重要。
首先我们循环训练集的加载器:
for i, (images,labels) in enumerate(train_loader):
接下来,如果支持gpu,我们将图像和标签移动到GPU:
if cuda_avail:
images = Variable(images.cuda())
labels = Variable(labels.cuda())
下一行是清除当前所有累积的梯度。
optimizer.zero_grad()
这很重要,因为神经网络中的权重是根据每个批次累积的梯度进行调整的,因此对于每个新批次,梯度必须重置为零,因此之前批次中的图像不会将梯度传播到新批次。
在接下来的步骤中,我们将图像传递到模型中。它返回预测,然后我们将预测和实际标签都传递给损失函数。
我们调用loss.backward() 传播梯度,然后我们调用optimizer.step() 修改我们的模型参数根据传播的梯度。
这些是训练的主要步骤。
其余的代码是计算指标:
train_loss += loss.cpu().data[0] * images.size(0)
_, prediction = torch.max(outputs.data, 1)
train_acc += torch.sum(prediction == labels.data)
在这里,我们检索实际损失,然后获得最大预测等级。最后,我们总结批次中正确预测的数量并将其添加到总数中train_acc。
每个epoch之后,我们称之为学习率调整函数,计算训练损失和训练精度的平均值,找出测试精度并记录结果。
更重要的是,我们追踪最佳精度,并且如果当前测试精度比我们目前最好的精度要高,我们会调用保存模型函数。
推理与保存的模型
模型经过训练后,可以用来对新图像进行推理。
要执行推理,您需要经过以下步骤:
定义并实例化在训练期间构建的相同模型。
将保存的检查点加载到模型中。
从文件系统中选择一个图像。
通过模型运行图像并获取最高的预测。
将预测的类号转换为类名。
为了说明这一点,我们将使用具有预训练ImageNet权重的Squeezenet模型。这使我们能够拍摄几乎任何图像并对其进行预测。由于ImageNet模型有1000个类,因此支持许多不同种类的对象。
Torchvision提供了预定义的模型,涵盖了广泛的流行体系结构。
首先,导入所有需要的包和类,并创建Squeezenet模型的一个实例。
# Import needed packages
import torch
import torch.nn as nn
from torchvision.transforms import transforms
from torch.autograd import Variable
from torchvision.models import squeezenet1_1
import requests
import shutil
from io import open
import os
from PIL import Image
import json
model = squeezenet1_1(pretrained=True)
model.eval()
请注意,在上面的代码中,通过将预训练设置为true,首次运行此功能时将下载Squeezenet模型。模型的大小只有4.7 mb。
接下来,创建一个预测函数,如下所示:
def predict_image(image_path):
print("Prediction in progress")
image = Image.open(image_path)
# Define transformations for the image, should (note that imagenet models are trained with image size 224)
transformation = transforms.Compose([
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
# Preprocess the image
image_tensor = transformation(image).float()
# Add an extra batch dimension since pytorch treats all images as batches
image_tensor = image_tensor.unsqueeze_(0)
if torch.cuda.is_available():
image_tensor.cuda()
# Turn the input into a Variable
input = Variable(image_tensor)
# Predict the class of the image
output = model(input)
index = output.data.numpy().argmax()
return index
上面的代码包含了我们在训练和评估过程中使用的相同组件。为清晰起见,请参阅上述代码中的注释。
最后,在运行我们的预测的主要功能中,我们应该从网上下载一个图像并将其存储在磁盘上。我们还应该下载将所有类索引映射到实际类名的类映射。这是因为我们的模型会返回预测类的索引,这取决于类名是如何编码的,然后将从索引类映射中检索实际名称。
之后,我们使用保存的图像运行预测功能,并使用保存的类图获取确切的类名。
if __name__ == "__main__":
imagefile = "image.png"
imagepath = os.path.join(os.getcwd(), imagefile)
# Donwload image if it doesn't exist
if not os.path.exists(imagepath):
data = requests.get(
"https://github.com/OlafenwaMoses/ImageAI/raw/master/images/3.jpg", stream=True)
with open(imagepath, "wb") as file:
shutil.copyfileobj(data.raw, file)
del data
index_file = "class_index_map.json"
indexpath = os.path.join(os.getcwd(), index_file)
# Donwload class index if it doesn't exist
if not os.path.exists(indexpath):
data = requests.get('https://github.com/OlafenwaMoses/ImageAI/raw/master/imagenet_class_index.json')
with open(indexpath, "w", encoding="utf-8") as file:
file.write(data.text)
class_map = json.load(open(indexpath))
# run prediction function annd obtain prediccted class index
index = predict_image(imagepath)
prediction = class_map[str(index)][1]
print("Predicted Class ", prediction)
这里是完整的python推理代码:
# Import needed packages
import torch
import torch.nn as nn
from torchvision.transforms import transforms
import matplotlib.pyplot as plt
import numpy as np
from torch.autograd import Variable
from torchvision.models import squeezenet1_1
import torch.functional as F
import requests
import shutil
from io import open
import os
from PIL import Image
import json
""" Instantiate model, this downloads tje 4.7 mb squzzene the first time it is called.
To use with your own model, re-define your trained networks ad load weights as below
checkpoint = torch.load("pathtosavemodel")
model = SimpleNet(num_classes=10)
model.load_state_dict(checkpoint)
model.eval()
"""
model = squeezenet1_1(pretrained=True)
model.eval()
def predict_image(image_path):
print("Prediction in progress")
image = Image.open(image_path)
# Define transformations for the image, should (note that imagenet models are trained with image size 224)
transformation = transforms.Compose([
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
# Preprocess the image
image_tensor = transformation(image).float()
# Add an extra batch dimension since pytorch treats all images as batches
image_tensor = image_tensor.unsqueeze_(0)
if torch.cuda.is_available():
image_tensor.cuda()
# Turn the input into a Variable
input = Variable(image_tensor)
# Predict the class of the image
output = model(input)
index = output.data.numpy().argmax()
return index
if __name__ == "__main__":
imagefile = "image.png"
imagepath = os.path.join(os.getcwd(), imagefile)
# Donwload image if it doesn't exist
if not os.path.exists(imagepath):
data = requests.get(
"https://github.com/OlafenwaMoses/ImageAI/raw/master/images/3.jpg", stream=True)
with open(imagepath, "wb") as file:
shutil.copyfileobj(data.raw, file)
del data
index_file = "class_index_map.json"
indexpath = os.path.join(os.getcwd(), index_file)
# Donwload class index if it doesn't exist
if not os.path.exists(indexpath):
data = requests.get('https://github.com/OlafenwaMoses/ImageAI/raw/master/imagenet_class_index.json')
with open(indexpath, "w", encoding="utf-8") as file:
file.write(data.text)
class_map = json.load(open(indexpath))
# run prediction function annd obtain prediccted class index
index = predict_image(imagepath)
prediction = class_map[str(index)][1]
print("Predicted Class ", prediction)
上面的例子中的图片是这只鸟的图片:
该图像来自ImageAI存储库。如果要使用自己的自定义网络(即刚刚创建的SimpleNet)执行推理,则只需用此替换模型加载部分即可。
checkpoint = torch.load("pathtosavemodel")
model = SimpleNet(num_classes=10)
model.load_state_dict(checkpoint)
model.eval()
请注意,如果您的模型是在ImageNet上进行培训的,那么您的模型num_classes 必须是1000而不是10。
代码的所有其他方面保持不变 - 只有一个区别 - 如果我们用cifar10训练模型进行预测,那么在变换中,更改transforms.CenterCrop(224) 为transforms.Resize(32)
但是,如果您的模型是在ImageNet上进行培训的,则不应该执行此更改。