多类别图像分类一直是深度学习研究的核心。多类图像分类的目标是从一组固定的类别中为图像指定一个标签。 在本文中,我们将学习如何创建一个算法来识别STL-10数据集中的10类对象。我们将使用在ImageNet数据集曾经表现SOTA的模型并且在STL-10数据集上微调。 本文讲涉及以下内容:
- 导入与与处理数据
- 构建模型
- 定义损失函数
- 定义优化器
- 迁移学习
- 模型部署
导入与数据处理
我们将使用PyTorch的torchvision包提供的STL-10数据集。类别分别为飞机(0)、鸟(1)、轿车(2)、猫(3)、鹿(4)、狗(5)、马(6)、猴子(7)、船(8)和卡车(9)。图像为RGB颜色,尺寸为96*96。该数据集包含5000张训练图像和8000张测试图像。在训练和测试数据集中,每个类分别有500和800张图像。
from torchvision import datasets
import torchvision.transforms as transforms
import os
path2data = "./data"
if not os.path.exists(path2data):
os.mkdirs(path2data)
data_transformer = transforms.Compose([transforms.ToTensor()])
train_ds = datasets.STL10(path2data, split="train", download=True, transform=data_transformer)
print(train_ds.data.shape)
import collections
y_train=[y for _,y in train_ds]
counter_train = collections.Counter(y_train)
print(counter_train)
test0_ds = datasets.STL10(path2data, split="test", download=True, transform=data_transformer)
print(test0_ds.data.shape)
from sklearn.model_selection import StratifieldShuffleSplit
sss = StratifieldShuffleSplit(n_splits=1, test_size=0.2, random_state=0)
indices=list(range(len(test0_ds)))
y_test0 = [y for _,y in test0_ds]
for test_index, val_index in sss.split(indices, y_test0):
print("test:", test_index, "val:", val_index)
print(len(val_index), len(test_index))
from torch.utils.data import Subset
val_ds = Subset(test0_ds, val_index)
test_ds = Subset(test0_ds, test_index)
import collections
import numpy as np
y_test = [ y for _,y in test_ds]
y_val = [y for _,y in val_ds]
counter_test = collections.Counter(y_test)
counter_val = collections.Counter(y_val)
print(counter_test)
print(counter_val)
from torchvision import utils
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(0)
def show(img, y=None, color=True):
npimg = img.numpy()
npimg_tr = np.transpose(npimg, (1, 2, 0))
plt.imshow(npimg_tr)
if y is not None:
plt.title("label: " + str(y))
grid_size=4
rnd_inds = np.random.randint(0, len(train_ds),grid_size)
print("image indices:", rnd_inds)
x_grid = [train_ds[i][0] for i in rnd_inds]
y_grid = [train_ds[i][1] for i in rnd_inds]
x_grid = utils.make_grid(x_grid, nrow=4, padding=1)
print(x_grid.shape)
plt.figure(figsize=(10, 10))
show(x_grid,y_grid)
np.random.seed(0)
grid_size=4
rnd_inds=np.random.randint(0,len(val_ds),grid_size)
print("image indices:",rnd_inds)
x_grid=[val_ds[i][0] for i in rnd_inds]
y_grid=[val_ds[i][1] for i in rnd_inds]
x_grid=utils.make_grid(x_grid, nrow=4, padding=2)
print(x_grid.shape)
plt.figure(figsize=(10,10))
show(x_grid,y_grid)
import numpy as np
meanRGB = [np.mean(x.numpy(), axis=(1,2)) for x,_ in train_ds]
stdRGB = [np.std(x.numpy(),axis=(1, 2) for x,_ in train_ds]
meanR = np.mean([m[0] for m in meanRGB])
meanG = np.mean([m[1] for m in meanRGB])
meanB = np.mean([m[2] for m in meanRGB])
stdR = np.mean([s[0] for s in stdRGB])
stdG = np.mean(s[1] for s in stdRGB])
stdB = np.mean(s[2] for s in stdRGB])
print(meanR, meanG, meanB)
print(stdR, stdG, stdB)
train_transformer =
transforms.Compose([transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomVerticalFlip(p=0.5),
transforms.ToTensor(),
transforms.Normalize([meanR, meanG, meanB], [stdR, stdG, stdB],
])
train_ds.transform = train_transformer
test0_ds.transform = test0_transformer
import torch
np.random.seed(0)
torch.manual_seed(0)
grid_size=4
rnd_inds = np.random.randint(0, len(train_ds), grid_size)
print("image indices:", rnd_inds)
x_grid = [train_ds[i][0] for i in rnd_inds]
y_grid = [train_ds[i][1] for i in rnd_inds]
x_grid = utils.make_grid(x_grid, nrow=4, padding=2)
print(x_grid.shape)
plt.figure(figsize=(10,10))
show(x_grid, y_grid)
from torch.utils.data import DataLoader
train_dl = DataLoader(train_ds, batch_size=32, shuffle=True)
val_dl = DataLoader(val_ds, batch_size=64, shuffle=False)
for x, y in train_dl:
print(x.shape)
print(y.shape)
break
for x, y in val_dl:
print(x.shape)
print(y.shape)
break
代码解析: 在步骤1中,我们从torchvision.datasets加载训练数据集。训练数据集有5000张3x96x96大小的图像。变换函数转换PIL图像为张量并且归一化像素值在[0,1]范围。确保第一次运行代码时,download= True。它将自动下载数据集并将其存储在指定文件夹中。下载一次后,可以设置download= False。在步骤2中,我们计算了每个类的图像数量。在每个类中有相同数量的图像(一个平衡的数据集)。 在步骤3中,我们加载了原始测试数据集,并将其命名为test0_ds。test0_ds中有8000张图像。因为没有正式的验证数据集,所以我们将test0_ds索引分成两个组,val_index和test_index。在步骤4中,我们使用了来自sklearn.model_selection的StratifiedShuffleSplit。这个函数返回分层的随机索引。在第5步中,我们使用val_index和test_index创建两个数据集:val_ds和test_ds。val_ds中有1600张图象,test_ds中有6400张图象。我们将在训练期间使用val_ds对模型进行评估。 因为我们使用了分层随机索引,所以我们可以在步骤6中看到val_ds和test_ds都是平衡的。在步骤7和步骤8中,我们显示了来自train_ds和val_ds的示例图像。我们使用np.random.seed(0)固定了随机种子,以便每次运行中得到相同的随机索引。如果您想在每次运行中看到不同的随机图像,请注释掉这一行。在第9步中,我们计算了train_ds中像素值的均值和标准差。我们将使用这些值对数据进行归一化。 在步骤10中,我们定义了图像变换。对于train_ds,我们添加了RandomHorizontalFlip和RandomVerticalFlip来增加训练数据集。此外,我们通过使用transforms.Normalize实现零均值单位方差的归一化。这个函数的参数是第9步中计算的每个通道的平均值和标准差。对于test0_ds,我们只添加了标准化函数,因为我们不需要对验证和测试数据集进行数据扩充。 在第11步中,我们更新了train_ds和test0_ds的转换函数。注意,当我们更新test0_ds时。转换函数val_ds和test_ds都将被更新,因为它们是test0_ds的子集。在第12步中,我们使用更新的转换显示了来自train_ds的示例图像。注意,由于归一化,转换图像的像素值在显示时被剪辑。 在第13步中,我们创建了PyTorch数据加载器train_dl和val_dl,以便能够自动从每个数据集获取批量数据。在步骤14和步骤15中,我们使用数据加载器提取一批数据。正如所见,提取的数据大小取决于批处理的大小。 在torchvision包中还包含其他多分类数据集,例如,FashionMNIST。要使用该数据集,使用以下代码:
from torchvision import datasets
fashion_train = datasets.FashionMNIST(path2data, train_true, download=True)
构建模型
接下来,我们将为我们的多分类任务构建一个模型。我们将使用torchvision内置的模型,而不是构建自定义模型。torchvision包为图像多分类提供了多种深度学习模型的实现。这些包括AlexNet,VGG,ResNet,SqueezeNet,DenseNet,Inception,GoogleNet,ShuffleNet。这些模型是在ImageNet数据集上训练的结果。ImageNet数据集拥有1000个类,共计1400万张图像。我们既可以使用随机初始化权重也可以使用预先训练的权重。
from torchvision import models
import torch
model_resnet18 = models.resnet18(pretrained=False)
print(model_resnet18)
from torch import nn
num_classes = 10
num_ftrs = model_resnet18.fc.in_features
model_resnet18.fc = nn.Linear(num_ftrs, num_classes)
device = torch.device("cuda:0")
model_resnet18.to(device)
from torchsummary import summary
summary(model_resnet18, input_size=(3, 224, 224), device=device.type)
for w in model_resnet18.parameters():
w = w.data.cpu()
print(w.shape)
min_w = torch.min(w)
w1 = (-1/(2*min_w))*w + 0.5
print(torch.min(w1).item(), torch.max(w1).item())
grid_size = len(w1)
x_grid = [w1[i] for i in range(grid_size)]
x_grid = utils.make_grid(x_grid, nrow=8, padding=1)
print(x_grid.shape)
plt.figure(figsize=(10, 10))
show(x_grid)
break
from torchvision import models
import torch
resnet18_pretrained = models.resnet18(pretrained=True)
num_classes=10
num_ftrs = resnet18_pretrained.fc.in_features
resnet18_pretrained.fc = nn.Linear(num_ftrs, num_classes)
device = torch.device("cuda:0")
resnet18_pretrained.to(device)
for w in resnet18_pretrained.parameters():
w = w.data.cpu()
print(w.shape)
min_w = torch.min(w)
w1 = (-1/(2*min_w))*w + 0.5
print(torch.min(w1).item(), torch.max(w1).item())
grid_size = len(w1)
x_grid = [w1[i] for i in range(grid_size)]
x_grid = utils.make_grid(x_grid, nrow=8, padding=1)
print(x_grid.shape)
plt.figure(figsize=(10, 10))
show(x_grid)
break
代码解析: 在步骤1中,我们加载了resnet18模型。这将自动下载模型并将其存储在本地以备将来使用。加载模型时的一个重要参数是预先训练。如果设置为False,模型权重将被随机初始化。 在步骤2中,我们打印了模型。最后一层是一个线性层,有1000个输出。resnet18模型是为拥有1000个类的ImageNet数据集开发的。在步骤3中,我们将最后一层更改为num_classes = 10的分类输出。当我们得到模型summary时,可以在第4步中看到这个更改。即使原始图像的大小是9696,我们需要调整它们的大小224224,与resnet18模型预训练时的尺寸相同。 在步骤5中,我们可视化了第一层的卷积滤波器。由于滤波器是随机初始化的,它们显示的是随机初始化滤波器的结果。 在步骤6中,我们用pretraining = True加载了resnet18模型。这将使用ImageNet数据集上预先训练的权重加载模型。在第7步中,我们显示了第一层的滤波器。正如所见,滤波器代表一些模式和结构,如边缘。 此外,你也可以使用torchvision.models包提供的更多模型。例如vgg19,代码如下:
num_classes=10
vgg19 = models.vgg19(pretrained=True)
vgg19.classifier[6] = nn.Linear(4096, num_classes)
定义损失函数
定义损失函数的目标是优化模型。分类任务的标准损失函数是交叉熵损失或log损失。但是,在定义损失函数时,我们需要考虑模型输出的数量及其激活函数。对于多类分类任务,输出的数量被设置为类的数量。输出激活函数然后决定损失函数。 下表为不同激活函数对应的损失函数: 在构建模型部分中定义的resnet18模型,因为是没有激活函数的线性输出,因此,我们选择nn.CrossEntropyLoss 作为损失函数的。这个损失函数将nn.LogSoftmax()和nn.NLLLoss()合并在一个类中。
loss_func = nn.CrossEntropyLoss(reduction="sum")
torch.manual_seed(0)
n,c=4,5
y = torch.randn(n, c, requires_grad=True)
print(y.shape)
loss_func = nn.CrossEntropyLoss(reduction="sum")
target = torch.randint(c, size=(n,))
print(target.shape)
loss = loss_func(y, target)
print(loss.item())
loss.backup()
print(y.data)
代码解析: 在步骤1中,我们定义了损失函数。resnet18模型使用线性输出。因此,我们使用nn.CrossEntropyLoss 作为损失函数。定义损失函数时要注意的点是reduction参数,它指定了应用于输出的reduction方式。有三个选项可供选择:none, sum, and mean。我们选择reduction= sum,将输出损失求和。由于我们将分批处理数据,这将返回每批数据的损失值之和。 在步骤2中,我们以n=4个样本,c=5个类别为例计算损失。 在步骤3中,我们为步骤2中的例子计算了梯度。稍后,我们将使用反向传播算法来计算相对于模型参数的损失梯度。
定义优化器
torch.optim包提供了通用优化器的实现。优化器将保存当前状态,并根据计算出的梯度更新参数。对于分类任务,随机梯度下降(SGD)和Adam优化器是常用的优化器。为模型选择一个优化器被认为是一个超参数。通常需要尝试多个优化器来找到性能最好的一个。Adam optimizer在速度和准确性方面通常优于SGD,所以我们在这里选择Adam optimizer。 torch.optim包中另一个不错的工具是学习率策略。学习策略是在训练过程中自动调整学习率以提高模型性能的有效工具。 在本教程中,我们将学习如何定义优化器、获取当前学习速率以及定义学习策略。
from torch import optim
opt=optim.Adam(model_resnet18.parameters(), lr=1e-4)
def get_lr(opt):
for param_group in opt.param_groups:
return param_group["lr"]
current_lr = get_lr(opt)
print("current lr={}".format(current_lr))
from torch.optim.lr_scheduler import CosineAnnelingLR
lr_scheduler = CosineAnnelingLR(opt, T_max=2, eta_min=1e-5)
for i in range(10):
lr_scheduler.step()
print("epoch %s, lr: %.1e" %(i, get_lr(opt)))
代码解析: 在步骤1中,我们定义了Adam优化器。如前所述,Adam优化器在分类任务方面的性能在大多数情况下优于其他优化器。然而,这不应被视为一项规则。考虑将优化器作为超参数,并尝试几种不同的优化器,看看哪一种性能更好。优化器类的重要参数是模型参数和学习率。model_resnet18.parameters()返回传递给优化器的模型参数的迭代器。学习速率将决定更新的大小。我们为所有层设置一个学习速率。在步骤2中,我们开发了一个返回当前学习率值的辅助函数。 在步骤3中,我们使用了来自torch.optim.lr_scheduler包中的CosineAnnealingLR方法。该优化策略基于余弦退火策略调整学习率。学习速率从优化器中的设定值lr=1e-4开始。然后逐渐向eta_min=1e-5递减,在2*T_max=4次迭代后返回到原来的设定值lr=1e-4。 下面的图描绘了10个epochs的学习率值: 稍后,我们将看到如何在训练中调用学习率策略。
从头开始训练与迁移学习
到目前为止,我们已经创建了数据集并定义了模型、损失函数和优化器。在本教程中,我们将实现训练和验证脚本。我们首先用随机初始化的权值训练模型。然后,我们将使用预先训练的权重训练模型。这种方法也被称为迁移学习。在迁移学习中,我们尝试将从一个问题中学到的知识(权重)用于其他类似的问题。训练和验证脚本可能很长且重复。为了更好的代码可读性和避免代码重复,我们将首先构建几个辅助函数。
def metrics_batch(output, target):
pred = output.argmax(dim=1, keepdim=True)
corrects = pred.eq(target.view_as(pred)).sum().item()
return corrects
def loss_batch(loss_func, output, target, opt=True):
loss = loss_func(output, target)
metric_b = metrics_batch(output, target)
if opt is not None:
opt.zero_grad()
loss.backward()
opt.step()
return loss.item(), metric_b
def loss_epoch(model, loss_func, dataset_dl, sanity_check=False, opt=None):
running_loss = 0.0
running_metric = 0.0
len_data = len(dataset_dl.dataset)
for xb, yb in dataset_dl:
xb = xb.to(device)
yb = yb.to(device)
output = model(xb)
loss_b, metric_b = loss_batch(loss_func, output, yb, opt)
running_loss += loss_b
if metric_b is not None:
running_metric += metric_b
if sanity_check is True:
break
loss = running_loss/float(len_data)
metric = running_metric/float(len_data)
return loss, metric
def train_val(model, params):
num_epochs = params["num_epochs"]
loss_func = params["loss_func"]
opt = params["optimizer"]
train_dl = params["train_dl"]
val_dl = params["val_dl"]
sanity_check = params["sanity_check"]
lr_scheduler = params["lr_scheduler"]
path2weights = params["path2weights"]
loss_history = {"train":[], "val":[]}
metric_history = {"train":[], "val":[]}
best_model_wts = copy.deepcopy(model.state_dict())
best_loss = float("inf")
for epoch in range(num_epochs):
current_lr = get_lr(opt)
print("Epoch {}/{}, current lr={}".format(epoch, num_epochs-1, current_lr))
model.train()
train_loss,train_metric=loss_epoch(model, loss_func, train_dl, sanity_check, opt)
loss_history["train"].append(train_loss)
metric_history["train"].append(train_metric)
model.eval()
with torch.no_grad():
val_loss, val_metric = loss_epoch(model, loss_func, val_dl, sanity_check)
loss_history["val"].append(val_loss)
metric_history["val"].append(val_metric)
if val_loss < best_loss:
best_loss = val_loss
best_model_wts = copy.deepcopy(model.state_dict())
torch.save(model.state_dict(), path2weights)
print("Copied best model weights")
lr_scheduler.step()
print("train loss:%.6f, dev loss: %.6f, accuracy:%.2f"%(train_loss, val_loss, 100*val_metric)
model.load_state_dict(best_model_wts)
return model, loss_history, metric_history
import copy
loss_func = nn.CrossEntropyLoss(reduction="sum")
opt = optim.Adam(model_resnet18.parameters(), lr=1e-4)
lr_scheduler = CosineAnnelingLR(opt, T_max=5, eta_min=1e-6)
os.makedirs("./model", exist_ok=True)
params_train = {
"num_epochs":100,
"optimizer":opt,
"loss_func":loss_func,
"train_dl":train_dl,
"val_dl":val_dl,
"sanity_check":False,
"lr_scheduler":lr_scheduler,
"path2weights":"./models/resnet18.pt",
}
model_resnet18, loss_hist, metric_hist = train_val(model_resnet18, params_train)
num_epochs = params_train["num_epochs"]
plt.title("Train-Val Loss")
plt.plot(range(1, num_epochs+1), loss_hist["train"], label="train")
plt.plot(range(1, num_epochs+1), loss_hist["val"], label="val")
plt.ylabel("Loss")
plt.xlabel("Training Epochs")
plt.legend()
plt.show()
plt.title("Train-Val Accuracy")
plt.plot(range(1, num_epochs+1), metric_hist["train"], label="train")
plt.plot(range(1, num_epochs+1), metric_hist["val"], label="val")
plt.ylabel("Accuracy")
plt.xlabel("Training Epochs")
plt.legend()
plt.show()
import copy
loss_func = nn.CrossEntropyLoss(reduction="sum")
opt = optim.Adam(model_resnet18.parameters(), lr=1e-4)
lr_scheduler = CosineAnnelingLR(opt, T_max=5, eta_min=1e-6)
params_train = {
"num_epochs":100,
"optimizer":opt,
"loss_func":loss_func,
"train_dl":train_dl,
"val_dl":val_dl,
"sanity_check":False,
"lr_scheduler":lr_scheduler,
"path2weights":"./models/resnet18_pretrained.pt",
}
model_resnet18, loss_hist, metric_hist = train_val(resnet18_pretrained, params_train)
代码解析 在step1中,定义一个获取每个batch中正确数目的函数 在step2中,我们实现loss_batch函数,这个函数输入损失函数,优化器,模型以及真值。 在step3中,loss_epoch函数输入模型对象,损失函数,数据加载器和优化器。我们使用数据加载器获取一个batch的数据,然后这个batch的数据一道GPU设备上,并获得模型输出。然后把损失以及度量值保存在之前定义好的变量里面。 在step4中,我们传入Python字典参数,可以提高代码的可读性 为了避免过拟合,在训练期间需要跟踪模型的性能。每训练一个epoch后,需要评估模型在验证集上的性能。验证集上的loss与最好的loss作比较。如果val_loss<best_loss, 将当前的权重拷贝给best_model_wts。在step5中,开始训练模型。每10个epochs,学习率策略经过一个周期。 在step6中,绘制损失和正确率的图形。可以发现,使用resnet18从头开始训练,不能得到很好的结果,那是因为训练集数据太少。 在step7中,使用预训练模型进行微调,效果显著。
部署模型
from torch import nn
from torchvision import models
model_resnet18 = models.resnet18(pretrained=False)
num_ftrs = model_resnet18.fc.in_features
num_classes=10
model_resnet18.fc=nn.Linear(num_ftrs, num_classes)
import torch
path2weights = "./models/resnet18_pretrained.pt"
model_resnet18.load_state_dict(torch.load(path2weights))
model_resnet18.eval()
if torch.cuda.is_available():
device = torch.device("cuda")
model_resnet18=model_resnet18.to(device)
def deploy_model(model, dataset, device, num_classes=10, sanity_check=False):
len_data = len(dataset)
y_out = torch.zeros(len_data, num_classes)
y_gt = np.zeros((len_data), dtype="uint8")
model = model.to(device)
elapsed_time=[]
with torch.no_grad():
for i in range(len_data):
x,y = dataset[i]
y_gt[i] = y
start = time.time()
yy = model(x.unsqueeze(0).to(device))
y_out[i] = torch.softmax(yy, dim=1)
elapsed = time.time()-start
elapsed_time.append(elapsed)
if sanity_check is True:
break
inference_time = np.mean(elapsed_time)*1000
print("average inference time per image on %s: %,2f ms" % (device, inference_time))
return y_out.numpy(), y_gt
import time
import numpy as np
y_out, y_gt = deploy_model(cnn_model, val_ds, device=device, sanity_check=False)
print(y_out.shape, y_gt.shape)
from sklearn.metrics import accuracy_score
y_pred = np.argmax(y_out, axis=1)
print(y_pred.shape, y_gt.shape)
acc = accuracy_score(y_pred, y_gt)
print("accuracy: %.2f" % acc)
y_out, y_gt = deploy_model(cnn_model, test_ds, device=device, sanity_check=False)
y_pred = np.argmax(y_out, axis=1)
acc = accuracy_score(y_pred, y_gt)
print("accuracy: %.2f" % acc)
from torchvision import utils
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(1)
def show(inp, title=None):
mean = [0.447, 0.440, 0.407]
std = [0.224, 0.221, 0.224]
inp = inp.numpy().transpose((1, 2, 0))
mean = np.array(mean)
std = np.array(std)
inp = std * inp + mean
inp = np.clip(inp, 0, 1)
plt.show(inp)
if title is not None:
plt.title(title)
plt.pause(0.001)
grid_size = 4
rnd_inds = np.random.randint(0, len(test_ds), grid_size)
print("image indices:", rnd_inds)
x_grid_test = [test_ds[i][0] for i in rnd_inds]
y_grid_test = [test_ds[i][1] for i in rnd_inds]
x_grid_test = utils.make_grid(x_grid_test, nrow=4, padding=2)
print(x_grid_test.shape)
plt.rcParams["figure.figsize"]=(10, 5)
imshow(x_grid_test, y_grid_test)
device_cpu = torch.device("cpu")
y_out, y_gt = deploy_model(cnn_model, val_ds, device=device, sanity_check=False)
print(y_out.shape, y_gt.shape)
代码解析: 在步骤1中,我们从torchvision.models加载了内置模型。在步骤2中,我们加载了state_dict,它包含模型中的模型权重。对于部署,需要将模型设置为评估模式。这一点很重要,因为某些层(如dropout和BatchNorm)在训练和部署模式中表现不同。如果CUDA设备可用,我们应该把模型移到CUDA设备上。模型已经准备好部署了。 在第5步中,辅助函数以numpy数组的形式返回模型输出和ground truth标签。此外,CUDA设备上每个图像的推断时间大概为3.53 ms。稍后,我们将看到CPU设备上的推断时间。 在第6步中,我们通过在验证数据集中部署保存的模型来验证它。在深度学习模型的开发过程中,很多事情都可能出错。我们建议通过在已知数据集(例如,验证数据集)上部署模型来验证存储模型的性能。 在步骤7中,我们使用scikit-learn包来计算我们的二分类模型的准确性。验证数据集的准确性为0.94。这是对我们模型的一个很好的验证。 在步骤8中,我们将模型部署在test_ds上。我们没有使用test_ds进行验证,因此可以认为它是模型的一个隐藏数据集。幸运的是,我们也有test_ds的标签,我们可以在test_ds上评估模型的性能。准确性为0.95,接近val_ds的精度。 在第9步中,我们显示了一些来自test_ds的示例图像和预测标签。 在步骤10中,我们希望看到部署在CPU上的推理时间。如果我们想要在没有GPU的设备上部署模型,这将是有用的信息。每张图像的推理时间大约是25毫秒。这仍然是一个很低的数字,可以用于许多实时应用程序。
|