PyImgSearch 博客中文翻译(七十六)

原文:PyImgSearch Blog

协议:CC BY-NC-SA 4.0

在 PyTorch 训练 DCGAN

原文:https://pyimagesearch.com/2021/10/25/training-a-dcgan-in-pytorch/

在本教程中,我们将学习如何使用 PyTorch 生成图像来训练我们的第一个 DCGAN 模型。

本课是高级 PyTorch 技术 3 部分系列的第 1 部分:

  1. 在 PyTorch 中训练 DCGAN(今天的教程)
  2. 在 PyTorch 中从头开始训练物体探测器(下周课)
  3. U-Net:在 PyTorch 中训练图像分割模型(两周内)

到 2014 年,机器学习的世界已经取得了相当大的进步。几个新概念(如关注和 R-CNN)被引入。然而,机器学习的想法已经成型,因此它们都以某种方式被期待。

那是直到 Ian Goodfellow 等人写了一篇论文改变了机器学习的世界;生成性对抗网络。

深度卷积网络在图像领域取得成功后,没过多久,这个想法就和 GANs 融合在一起了。于是,DCGANs 诞生了。

学习如何使用 PyTorch 编写的 DCGAN 生成图像, 继续阅读。

在 PyTorch 中训练 DCGAN

教程的结构:

  • gan 和 DCGANs 介绍
  • 了解 DCGAN 架构
  • PyTorch 实现和演练
  • 关于下一步尝试什么的建议

生成性对抗网络

GANs 的突出因素是它们能够生成真实的图像,类似于您可能使用的数据分布。

GANs 的概念简单而巧妙。让我们用一个简单的例子来理解这个概念(图 1 )。

你最近报了一个艺术班,那里的艺术老师极其苛刻和严格。当你交第一幅画时,美术老师惊呆了。他威胁要把你开除,直到你能创作出一幅壮观的杰作。

不用说,你心烦意乱。这项任务非常困难,因为你还是个新手。唯一对你有利的是,你讨厌的艺术老师说,这幅杰作不必是他的收藏的直接复制品,但它必须看起来像是属于它们的。

你急切地开始改进你的作品。在接下来的几天里,你提交了几份试用版,每一份都比上一次好,但还不足以让你通过这次测试。

与此同时,你的美术老师也开始成为展示给他的画作的更好的评判者。只需一瞥,他就能说出你试图复制的艺术家和艺术品的名字。最后,清算日到了,你提交你的最终作品(图 2 )。

你画了一幅非常好的画,你的美术老师把它放在他的收藏中。他夸你,收你为全日制学生(但到了那个时候,你意识到你已经不需要他了)。

GAN 的工作原理也是如此。“你”是试图生成模拟给定输入数据集的图像的生成者。而“美术老师”是鉴别者,他的工作是判断您生成的图像是否可以与输入数据集进行分组。上述示例与 GAN 的唯一区别在于,发生器和鉴别器都是从零开始一起训练的。

这些网络相互提供反馈,当我们训练 GAN 模型时,两者都得到了改善,我们得到了更好的输出图像质量。

关于在 Keras 和 Tensorflow 中实现 GAN 模型的完整教程,我推荐 Adrian Rosebrock 的教程。

什么是 DCGANs?

拉德福德等人(2016) 发表了一篇关于深度卷积生成对抗网络(DCGANs)的论文。

当时的 DCGANs 向我们展示了如何在没有监督的情况下有效地使用 GANs 的卷积技术来创建与我们的数据集中的图像非常相似的图像。

在这篇文章中,我将解释 DCGANs 及其关键研究介绍,并带您在 MNIST 数据集上完成相同的 PyTorch 实现。顺便说一下,这真的很酷,因为论文的合著者之一是 Soumith Chintala,PyTorch 的核心创建者!

DCGANs 架构

让我们深入了解一下架构:

图 3 包含了 DCGAN 中使用的发生器的架构,如文中所示。

图 3 所示,我们将一个随机噪声向量作为输入,并将一幅完整的图像作为输出。让我们看看图 4 中的鉴别器架构。

鉴别器充当正常的确定性模型,其工作是将输入图像分类为真或假。

该论文的作者创建了一个不同的部分,解释他们的方法和香草甘之间的差异。

  • 普通 GAN 的池层由分数阶卷积(在发生器的情况下)和阶卷积(在鉴别器的情况下)代替。对于前者,我绝对推荐塞巴斯蒂安·拉什卡的这个视频教程。分数步长卷积是标准向上扩展的替代方案,允许模型学习自己的空间表示,而不是使用不可训练的确定性池图层。
  • 与传统 GANs 的第二个最重要的区别是,它排除了完全连接的层,而支持更深层次的架构。
  • 第三, Ioffe 和 Szegedy (2015) 强调了批量标准化的重要性,以确保更深网络中梯度的正确流动。
  • 最后,拉德福德等人解释了 ReLU 和 leaky ReLU 在其架构中的使用,引用了有界函数的成功,以帮助更快地了解训练分布。

让我们看看这些因素是如何影响结果的!

配置您的开发环境

要遵循这个指南,您需要在您的系统上安装 OpenCV 库。

幸运的是,OpenCV 可以通过 pip 安装:

$ pip install opencv-contrib-python

如果你需要帮助为 OpenCV 配置开发环境,我强烈推荐阅读我的 pip 安装 OpenCV 指南——它将在几分钟内让你启动并运行。

在配置开发环境时遇到了问题?

说了这么多,你是:

  • 时间紧迫?
  • 了解你雇主的行政锁定系统?
  • 想要跳过与命令行、包管理器和虚拟环境斗争的麻烦吗?
  • 准备好在您的 Windows、macOS 或 Linux 系统上运行代码***?***

*那今天就加入 PyImageSearch 大学吧!

获得本教程的 Jupyter 笔记本和其他 PyImageSearch 指南,这些指南是 预先配置的 **,可以在您的网络浏览器中运行在 Google Colab 的生态系统上!**无需安装。

最棒的是,这些 Jupyter 笔记本可以在 Windows、macOS 和 Linux 上运行!

项目结构

我们首先需要回顾我们的项目目录结构。

首先访问本教程的 “下载” 部分,检索源代码和示例图像。

从这里,看一下目录结构:

!tree .
.
├── dcgan_mnist.py
├── output
│   ├── epoch_0002.png
│   ├── epoch_0004.png
│   ├── epoch_0006.png
│   ├── epoch_0008.png
│   ├── epoch_0010.png
│   ├── epoch_0012.png
│   ├── epoch_0014.png
│   ├── epoch_0016.png
│   ├── epoch_0018.png
│   └── epoch_0020.png
├── output.gif
└── pyimagesearch
    ├── dcgan.py
    └── __init__.py

pyimagesearch目录中,我们有两个文件:

  • dcgan.py:包含完整的 DCGAN 架构
  • __init__.py:将pyimagesearch转换成 python 目录

在父目录中,我们有dcgan_mnist.py脚本,它将训练 DCGAN 并从中得出推论。

除此之外,我们还有output目录,它包含 DCGAN 生成器生成的图像的按时间顺序的可视化。最后,我们有output.gif,它包含转换成 gif 的可视化效果。

在 PyTorch 中实现 DCGAN

我们的第一个任务是跳转到pyimagesearch目录并打开dcgan.py脚本。该脚本将包含完整的 DCGAN 架构。

# import the necessary packages
from torch.nn import ConvTranspose2d
from torch.nn import BatchNorm2d
from torch.nn import Conv2d
from torch.nn import Linear
from torch.nn import LeakyReLU
from torch.nn import ReLU
from torch.nn import Tanh
from torch.nn import Sigmoid
from torch import flatten
from torch import nn

class Generator(nn.Module):
	def __init__(self, inputDim=100, outputChannels=1):
		super(Generator, self).__init__()

		# first set of CONVT => RELU => BN
		self.ct1 = ConvTranspose2d(in_channels=inputDim,
			out_channels=128, kernel_size=4, stride=2, padding=0,
			bias=False)
		self.relu1 = ReLU()
		self.batchNorm1 = BatchNorm2d(128)

		# second set of CONVT => RELU => BN
		self.ct2 = ConvTranspose2d(in_channels=128, out_channels=64,
					kernel_size=3, stride=2, padding=1, bias=False)
		self.relu2 = ReLU()
		self.batchNorm2 = BatchNorm2d(64)

		# last set of CONVT => RELU => BN
		self.ct3 = ConvTranspose2d(in_channels=64, out_channels=32,
					kernel_size=4, stride=2, padding=1, bias=False)
		self.relu3 = ReLU()
		self.batchNorm3 = BatchNorm2d(32)

		# apply another upsample and transposed convolution, but
		# this time output the TANH activation
		self.ct4 = ConvTranspose2d(in_channels=32,
			out_channels=outputChannels, kernel_size=4, stride=2,
			padding=1, bias=False)
		self.tanh = Tanh()

在这里,我们创建了生成器类(第 13 行)。在我们的__init__构造函数中,我们有 2 个重要的事情要记住(第 14 行):

  • inputDim:通过发生器的噪声矢量的输入大小。
  • outputChannels:输出图像的通道数。因为我们使用的是 MNIST 数据集,所以图像将是灰度的。因此它只有一个频道。

由于 PyTorch 的卷积不需要高度和宽度规格,所以除了通道尺寸之外,我们不必指定输出尺寸。然而,由于我们使用的是 MNIST 数据,我们需要一个大小为1×28×28的输出。

记住,生成器将随机噪声建模成图像。记住这一点,我们的下一个任务是定义生成器的层。我们将使用CONVT(转置卷积)、ReLU(整流线性单元)、BN(批量归一化)(第 18-34 行)。最终的转置卷积之后是一个tanh激活函数,将我们的输出像素值绑定到1-1 ( 行 38-41 )。

	def forward(self, x):
		# pass the input through our first set of CONVT => RELU => BN
		# layers
		x = self.ct1(x)
		x = self.relu1(x)
		x = self.batchNorm1(x)

		# pass the output from previous layer through our second
		# CONVT => RELU => BN layer set
		x = self.ct2(x)
		x = self.relu2(x)
		x = self.batchNorm2(x)

		# pass the output from previous layer through our last set
		# of CONVT => RELU => BN layers
		x = self.ct3(x)
		x = self.relu3(x)
		x = self.batchNorm3(x)

		# pass the output from previous layer through CONVT2D => TANH
		# layers to get our output
		x = self.ct4(x)
		output = self.tanh(x)

		# return the output
		return output

在生成器的forward通道中,我们使用了三次CONVT = >、ReLU = > BN图案,而最后的CONVT层之后是tanh层(行 46-65 )。

class Discriminator(nn.Module):
	def __init__(self, depth, alpha=0.2):
		super(Discriminator, self).__init__()

		# first set of CONV => RELU layers
		self.conv1 = Conv2d(in_channels=depth, out_channels=32,
				kernel_size=4, stride=2, padding=1)
		self.leakyRelu1 = LeakyReLU(alpha, inplace=True)

		# second set of CONV => RELU layers
		self.conv2 = Conv2d(in_channels=32, out_channels=64, kernel_size=4,
				stride=2, padding=1)
		self.leakyRelu2 = LeakyReLU(alpha, inplace=True)

		# first (and only) set of FC => RELU layers
		self.fc1 = Linear(in_features=3136, out_features=512)
		self.leakyRelu3 = LeakyReLU(alpha, inplace=True)

		# sigmoid layer outputting a single value
		self.fc2 = Linear(in_features=512, out_features=1)
		self.sigmoid = Sigmoid()

请记住,当生成器将随机噪声建模到图像中时,鉴别器获取图像并输出单个值(确定它是否属于输入分布)。

在鉴别器的构造函数__init__中,只有两个参数:

  • depth:决定输入图像的通道数
  • alpha:给予架构中使用的泄漏 ReLU 函数的值

我们初始化一组卷积层、漏 ReLU 层、两个线性层,然后是最终的 sigmoid 层(行 75-90 )。这篇论文的作者提到,泄漏 ReLU 允许一些低于零的值的特性有助于鉴别器的结果。当然,最后的 sigmoid 层是将奇异输出值映射到 0 或 1。

	def forward(self, x):
		# pass the input through first set of CONV => RELU layers
		x = self.conv1(x)
		x = self.leakyRelu1(x)

		# pass the output from the previous layer through our second
		# set of CONV => RELU layers
		x = self.conv2(x)
		x = self.leakyRelu2(x)

		# flatten the output from the previous layer and pass it
		# through our first (and only) set of FC => RELU layers
		x = flatten(x, 1)
		x = self.fc1(x)
		x = self.leakyRelu3(x)

		# pass the output from the previous layer through our sigmoid
		# layer outputting a single value
		x = self.fc2(x)
		output = self.sigmoid(x)

		# return the output
		return output

在鉴别器的forward通道中,我们首先添加一个卷积层和一个漏 ReLU 层,并再次重复该模式(第 94-100 行)。接下来是一个flatten层、一个全连接层和另一个泄漏 ReLU 层(线 104-106 )。在最终的 sigmoid 层之前,我们添加另一个完全连接的层(行 110 和 111 )。

至此,我们的 DCGAN 架构就完成了。

训练 DCGAN

dcgan_mnist.py不仅包含 DCGAN 的训练过程,还将充当我们的推理脚本。

# USAGE
# python dcgan_mnist.py --output output

# import the necessary packages
from pyimagesearch.dcgan import Generator
from pyimagesearch.dcgan import Discriminator
from torchvision.datasets import MNIST
from torch.utils.data import DataLoader
from torchvision.transforms import ToTensor
from torchvision import transforms
from sklearn.utils import shuffle
from imutils import build_montages
from torch.optim import Adam
from torch.nn import BCELoss
from torch import nn
import numpy as np
import argparse
import torch
import cv2
import os

# custom weights initialization called on generator and discriminator
def weights_init(model):
	# get the class name
	classname = model.__class__.__name__

	# check if the classname contains the word "conv"
	if classname.find("Conv") != -1:
		# intialize the weights from normal distribution
		nn.init.normal_(model.weight.data, 0.0, 0.02)

	# otherwise, check if the name contains the word "BatcnNorm"
	elif classname.find("BatchNorm") != -1:
		# intialize the weights from normal distribution and set the
		# bias to 0
		nn.init.normal_(model.weight.data, 1.0, 0.02)
		nn.init.constant_(model.bias.data, 0)

在第 23-37 行上,我们定义了一个名为weights_init的函数。这里,我们根据遇到的层初始化自定义权重。稍后,在推断步骤中,我们将看到这改进了我们的训练损失值。

对于卷积层,我们将0.00.02作为该函数中的平均值和标准差。对于批量标准化图层,我们将偏差设置为0,并将1.00.02作为平均值和标准偏差值。这是论文作者提出的,并认为最适合理想的训练结果。

# construct the argument parse and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-o", "--output", required=True,
	help="path to output directory")
ap.add_argument("-e", "--epochs", type=int, default=20,
	help="# epochs to train for")
ap.add_argument("-b", "--batch-size", type=int, default=128,
	help="batch size for training")
args = vars(ap.parse_args())

# store the epochs and batch size in convenience variables
NUM_EPOCHS = args["epochs"]
BATCH_SIZE = args["batch_size"]

在第 40-47 行上,我们构建了一个扩展的参数解析器来解析用户设置的参数并添加默认值。

我们继续将epochsbatch_size参数存储在适当命名的变量中(第 50 行和第 51 行)。

# set the device we will be using
DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

# define data transforms
dataTransforms = transforms.Compose([
	transforms.ToTensor(),
	transforms.Normalize((0.5), (0.5))]
)

# load the MNIST dataset and stack the training and testing data
# points so we have additional training data
print("[INFO] loading MNIST dataset...")
trainData = MNIST(root="data", train=True, download=True,
	transform=dataTransforms)
testData = MNIST(root="data", train=False, download=True,
	transform=dataTransforms)
data = torch.utils.data.ConcatDataset((trainData, testData))

# initialize our dataloader
dataloader = DataLoader(data, shuffle=True,
	batch_size=BATCH_SIZE)

由于 GAN 训练确实涉及更多的复杂性,如果有合适的 GPU 可用,我们将默认设备设置为cuda(Line 54)。

为了预处理我们的数据集,我们简单地在第 57-60 行的上定义了一个torchvision.transforms实例,在这里我们将数据集转换成张量并将其归一化。

PyTorch 托管了许多流行的数据集供即时使用。它省去了在本地系统中下载数据集的麻烦。因此,我们从先前从torchvision.datasets ( 第 65-69 行)导入的 MNIST 包中准备训练和测试数据集实例。MNIST 数据集是一个流行的数据集,包含总共 70,000 个手写数字。

在连接训练和测试数据集(第 69 行)之后,我们创建一个 PyTorch DataLoader实例来自动处理输入数据管道(第 72 行和第 73 行)。

# calculate steps per epoch
stepsPerEpoch = len(dataloader.dataset) // BATCH_SIZE

# build the generator, initialize it's weights, and flash it to the
# current device
print("[INFO] building generator...")
gen = Generator(inputDim=100, outputChannels=1)
gen.apply(weights_init)
gen.to(DEVICE)

# build the discriminator, initialize it's weights, and flash it to
# the current device
print("[INFO] building discriminator...")
disc = Discriminator(depth=1)
disc.apply(weights_init)
disc.to(DEVICE)

# initialize optimizer for both generator and discriminator
genOpt = Adam(gen.parameters(), lr=0.0002, betas=(0.5, 0.999),
	weight_decay=0.0002 / NUM_EPOCHS)
discOpt = Adam(disc.parameters(), lr=0.0002, betas=(0.5, 0.999),
	weight_decay=0.0002 / NUM_EPOCHS)

# initialize BCELoss function
criterion = BCELoss()

因为我们已经将BATCH_SIZE值提供给了DataLoader实例,所以我们在行 76 上计算每个时期的步数。

第 81-83 行,我们初始化生成器,应用自定义权重初始化,并将其加载到我们当前的设备中。正如在dcgan.py中提到的,我们在初始化期间传递适当的参数。

类似地,在第 87-90 行,我们初始化鉴别器,应用自定义权重,并将其加载到我们当前的设备上。唯一传递的参数是depth(即输入图像通道)。

我们选择Adam作为生成器和鉴别器的优化器(第 88-96 行),传递

  • **模型参数:**标准程序,因为模型权重将在每个时期后更新。
  • **学习率:**控制模型自适应的超参数。
  • **β衰变变量:**初始衰变率。
  • **权重衰减值,通过历元数进行调整:**一种正则化方法,增加一个小的惩罚来帮助模型更好地泛化。

最后,我们损失函数的二元交叉熵损失(第 99 行)。

# randomly generate some benchmark noise so we can consistently
# visualize how the generative modeling is learning
print("[INFO] starting training...")
benchmarkNoise = torch.randn(256, 100, 1, 1, device=DEVICE)

# define real and fake label values
realLabel = 1
fakeLabel = 0

# loop over the epochs
for epoch in range(NUM_EPOCHS):
	# show epoch information and compute the number of batches per
	# epoch
	print("[INFO] starting epoch {} of {}...".format(epoch + 1,
		NUM_EPOCHS))

	# initialize current epoch loss for generator and discriminator
	epochLossG = 0
	epochLossD = 0

线 104 上,我们使用torch.randn给发电机馈电,并在可视化发电机训练期间保持一致性。

对于鉴别器,真标签和假标签值被初始化(行 107 和 108 )。

随着必需品的离开,我们开始在行 111 上的历元上循环,并初始化逐历元发生器和鉴别器损耗(行 118 和 119 )。

	for x in dataloader:
		# zero out the discriminator gradients
		disc.zero_grad()

		# grab the images and send them to the device
		images = x[0]
		images = images.to(DEVICE)

		# get the batch size and create a labels tensor
		bs =  images.size(0)
		labels = torch.full((bs,), realLabel, dtype=torch.float,
			device=DEVICE)

		# forward pass through discriminator
		output = disc(images).view(-1)

		# calculate the loss on all-real batch
		errorReal = criterion(output, labels)

		# calculate gradients by performing a backward pass
		errorReal.backward()

启动前,使用zero_grad ( 线 123 )冲洗电流梯度。

DataLoader实例(第 121 行)中获取数据,我们首先倾向于鉴别器。我们将并发批次的所有图像发送至设备(第 126 和 127 行)。由于所有图像都来自数据集,因此它们被赋予了realLabel ( 第 131 和 132 行)。

行 135 上,使用图像执行鉴别器的一次正向通过,并计算误差(行 138 )。

backward函数根据损失计算梯度(行 141 )。

		# randomly generate noise for the generator to predict on
		noise = torch.randn(bs, 100, 1, 1, device=DEVICE)

		# generate a fake image batch using the generator
		fake = gen(noise)
		labels.fill_(fakeLabel)

		# perform a forward pass through discriminator using fake
		# batch data
		output = disc(fake.detach()).view(-1)
		errorFake = criterion(output, labels)

		# calculate gradients by performing a backward pass
		errorFake.backward()

		# compute the error for discriminator and update it
		errorD = errorReal + errorFake
		discOpt.step()

现在,我们继续讨论发电机的输入。在线 144 上,基于发电机输入大小的随机噪声被产生并馈入发电机(线 147 )。

由于生成器生成的所有图像都是假的,我们用fakeLabel值(第 148 行)替换标签张量的值。

行 152 和 153 上,伪图像被馈送到鉴别器,并且计算伪预测的误差。

假图像产生的误差随后被送入backward函数进行梯度计算(行 156 )。然后根据两组图像产生的总损失更新鉴别器(第 159 和 160 行)。

		# set all generator gradients to zero
		gen.zero_grad()

		# update the labels as fake labels are real for the generator
		# and perform a forward pass  of fake data batch through the
		# discriminator
		labels.fill_(realLabel)
		output = disc(fake).view(-1)

		# calculate generator's loss based on output from
		# discriminator and calculate gradients for generator
		errorG = criterion(output, labels)
		errorG.backward()

		# update the generator
		genOpt.step()

		# add the current iteration loss of discriminator and
		# generator
		epochLossD += errorD
		epochLossG += errorG

继续发电机的训练,首先使用zero_grad ( 线 163 )冲洗梯度。

现在在第 168-173 行,我们做了一件非常有趣的事情:由于生成器必须尝试生成尽可能真实的图像,我们用realLabel值填充实际标签,并根据鉴别器对生成器生成的图像给出的预测来计算损失。生成器必须让鉴别器猜测其生成的图像是真实的。因此,这一步非常重要。

接下来,我们计算梯度(行 174 )并更新生成器的权重(行 177 )。

最后,我们更新发生器和鉴别器的总损耗值(行 181 和 182 )。

	# display training information to disk
	print("[INFO] Generator Loss: {:.4f}, Discriminator Loss: {:.4f}".format(
		epochLossG / stepsPerEpoch, epochLossD / stepsPerEpoch))

	# check to see if we should visualize the output of the
	# generator model on our benchmark data
	if (epoch + 1) % 2 == 0:
		# set the generator in evaluation phase, make predictions on
		# the benchmark noise, scale it back to the range [0, 255],
		# and generate the montage
		gen.eval()
		images = gen(benchmarkNoise)
		images = images.detach().cpu().numpy().transpose((0, 2, 3, 1))
		images = ((images * 127.5) + 127.5).astype("uint8")
		images = np.repeat(images, 3, axis=-1)
		vis = build_montages(images, (28, 28), (16, 16))[0]

		# build the output path and write the visualization to disk
		p = os.path.join(args["output"], "epoch_{}.png".format(
			str(epoch + 1).zfill(4)))
		cv2.imwrite(p, vis)

		# set the generator to training mode
		gen.train()

这段代码也将作为我们的训练可视化和推理片段。

对于某个纪元值,我们将生成器设置为评估模式(行 190-194 )。

使用之前初始化的benchmarkNoise,我们让生成器产生图像(行 195 和 196 )。然后首先对图像进行高度整形,并按比例放大到其原始像素值(第 196 和 197 行)。

使用一个名为build_montages的漂亮的imutils函数,我们显示每次调用期间生成的批处理图像(第 198 和 199 行)。build_montages函数接受以下参数:

  • 形象
  • 正在显示的每个图像的大小
  • 将显示可视化效果的网格的大小

行 202-207 上,我们定义了一个输出路径来保存可视化图像并将生成器设置回训练模式。

至此,我们完成了 DCGAN 培训!

DCGAN 训练结果和可视化

让我们看看 DCGAN 在损耗方面的划时代表现。

$ python dcgan_mnist.py --output output
[INFO] loading MNIST dataset...
[INFO] building generator...
[INFO] building discriminator...
[INFO] starting training...
[INFO] starting epoch 1 of 20...
[INFO] Generator Loss: 4.6538, Discriminator Loss: 0.3727
[INFO] starting epoch 2 of 20...
[INFO] Generator Loss: 1.5286, Discriminator Loss: 0.9514
[INFO] starting epoch 3 of 20...
[INFO] Generator Loss: 1.1312, Discriminator Loss: 1.1048
...
[INFO] Generator Loss: 1.0039, Discriminator Loss: 1.1748
[INFO] starting epoch 17 of 20...
[INFO] Generator Loss: 1.0216, Discriminator Loss: 1.1667
[INFO] starting epoch 18 of 20...
[INFO] Generator Loss: 1.0423, Discriminator Loss: 1.1521
[INFO] starting epoch 19 of 20...
[INFO] Generator Loss: 1.0604, Discriminator Loss: 1.1353
[INFO] starting epoch 20 of 20...
[INFO] Generator Loss: 1.0835, Discriminator Loss: 1.1242

现在,在没有初始化自定义权重的情况下重新进行整个训练过程后,我们注意到损失值相对较高。因此,我们可以得出结论自定义重量初始化确实有助于改善训练过程*。*

让我们看看我们的生成器在图 6-9 中的一些改进图像。

图 6 中,我们可以看到,由于生成器刚刚开始训练,生成的图像几乎是胡言乱语。在图 7 中,我们可以看到随着图像慢慢成形,生成的图像略有改善。

图 8 和 9 中,我们看到完整的图像正在形成,看起来就像是从 MNIST 数据集中提取出来的,这意味着我们的生成器学习得很好,最终生成了一些非常好的图像!

总结

GANs 为机器学习打开了一扇全新的大门。我们不断看到 GANs 产生的许多新概念和许多旧概念以 GANs 为基础被复制。这个简单而巧妙的概念足够强大,在各自的领域胜过大多数其他算法。

在训练图中,我们已经看到,到 20 世纪本身,我们的 DCGAN 变得足够强大,可以产生完整和可区分的图像。但是我鼓励您将您在这里学到的编码技巧用于各种其他任务和数据集,并亲自体验 GANs 的魔力。

通过本教程,我试图使用 MNIST 数据集解释 gan 和 DCGANs 的基本本质。我希望这篇教程能帮助你认识到甘是多么的伟大!

引用信息

Chakraborty,d .“在 PyTorch 训练一名 DCGAN”, PyImageSearch ,2021 年,https://PyImageSearch . com/2021/10/25/Training-a-DCGAN-in-py torch/

@article{
    
    Chakraborty_2021_Training_DCGAN,
   author = {
    
    Devjyoti Chakraborty},
   title = {
    
    Training a {
    
    DCGAN} in {
    
    PyTorch}},
   journal = {
    
    PyImageSearch},
   year = {
    
    2021},
   note = {
    
    https://pyimagesearch.com/2021/10/25/training-a-dcgan-in-pytorch/},
}

要下载这篇文章的源代码(并在未来教程在 PyImageSearch 上发布时得到通知),只需在下面的表格中输入您的电子邮件地址!*

在 PyTorch 中从头开始训练对象检测器

原文:https://pyimagesearch.com/2021/11/01/training-an-object-detector-from-scratch-in-pytorch/

在本教程中,您将学习如何使用 PyTorch 从头开始训练自定义对象检测器。

本课是高级 PyTorch 技术 3 部分系列的第 2 部分:

  1. 在 PyTorch 训练一个 DCGAN(上周教程)
  2. 在 PyTorch 中从头开始训练一个物体探测器(今天的教程)
  3. U-Net:在 PyTorch 中训练图像分割模型(下周博文)

从童年开始,人工智能(AI)的想法就吸引了我(就像其他孩子一样)。但是,当然,我对人工智能的概念与它的实际情况有很大的不同,毫无疑问是由于流行文化。直到我少年时代的末期,我坚信 AI 不受抑制的成长会导致 T-800(终结者中的终结者)之类的东西。幸运的是,使用图 1 可以更好地解释实际场景:

不过,不要误会我的意思。机器学习可能是一堆矩阵和微积分结合在一起,但我们可以用一个词来最好地描述这些东西的数量:无限

我一直感兴趣的一个这样的应用是对象检测。注入图像数据来获取标签是一回事,但是让我们的模型知道标签在哪里呢?那是完全不同的一场球赛,就像一些间谍电影一样。这正是我们今天要经历的!

在今天的教程中,我们将学习如何在 PyTorch 中从头开始训练我们自己的物体检测器。这个博客将帮助你:

  • 理解物体检测背后的直觉
  • 了解构建您自己的对象检测器的逐步方法
  • 了解如何微调参数以获得理想的结果

要学习如何在 Pytorch 中从头开始训练物体检测器,继续阅读。

在 PyTorch 中从头开始训练物体检测器

在今天强大的深度学习算法存在之前,物体检测是一个历史上广泛研究的领域。从 20 世纪 90 年代末到 21 世纪 20 年代初,提出了许多新的想法,这些想法至今仍被用作深度学习算法的基准。不幸的是,在那个时候,研究人员没有太多的计算能力可供他们支配,所以大多数这些技术都依赖于大量的额外数学来减少计算时间。谢天谢地,我们不会面临这个问题。

我们的目标检测方法

我们先来了解一下物体检测背后的直觉。我们将要采用的方法非常类似于训练一个简单的分类器。分类器的权重不断变化,直到它为给定数据集输出正确的标签并减少损失。对于今天的任务,我们将做与完全相同的事情,除了我们的模型将输出 5 个值4 个是围绕我们对象的边界框坐标。第 5 个值是被检测对象的标签。注意图 2的架构。

主模型将分为两个子集:回归器和分类器。前者将输出边界框的起始和结束坐标,而后者将输出对象标签。由这些产生的组合损耗将用于我们的反向传播。很简单的开始方式,不是吗?

当然,多年来,一些强大的算法接管了物体检测领域,如 R-CNNYOLO 。但是我们的方法将作为一个合理的起点,让你的头脑围绕物体检测背后的基本思想!

配置您的开发环境

要遵循这个指南,首先需要在系统中安装 PyTorch。要访问 PyTorch 自己的视觉计算模型,您还需要在您的系统中安装 Torchvision。对于一些数组和存储操作,我们使用了numpy。我们也使用imutils包进行数据处理。对于我们的情节,我们将使用matplotlib。为了更好地跟踪我们的模型训练,我们将使用tqdm,最后,我们的系统中需要 OpenCV!

幸运的是,上面提到的所有包都是 pip-installable!

$ pip install opencv-contrib-python
$ pip install torch
$ pip install torchvision
$ pip install imutils
$ pip install matplotlib
$ pip install numpy
$ pip install tqdm

如果你需要帮助为 OpenCV 配置开发环境,我强烈推荐阅读我的 pip 安装 OpenCV 指南——它将在几分钟内让你启动并运行。

在配置开发环境时遇到了问题?

说了这么多,你是:

  • 时间紧迫?
  • 了解你雇主的行政锁定系统?
  • 想要跳过与命令行、包管理器和虚拟环境斗争的麻烦吗?
  • 准备好在您的 Windows、macOS 或 Linux 系统上运行代码***?***

*那今天就加入 PyImageSearch 大学吧!

获得本教程的 Jupyter 笔记本和其他 PyImageSearch 指南,这些指南是 预先配置的 **,可以在您的网络浏览器中运行在 Google Colab 的生态系统上!**无需安装。

最棒的是,这些 Jupyter 笔记本可以在 Windows、macOS 和 Linux 上运行!

项目结构

我们首先需要回顾我们的项目目录结构。

首先访问本教程的 “下载” 部分,检索源代码和示例图像。

从这里,看一下目录结构:

!tree .
.
├── dataset.zip
├── output
│   ├── detector.pth
│   ├── le.pickle
│   ├── plots
│   │   └── training.png
│   └── test_paths.txt
├── predict.py
├── pyimagesearch
│   ├── bbox_regressor.py
│   ├── config.py
│   ├── custom_tensor_dataset.py
│   └── __init__.py
└── train.py

目录中的第一项是dataset.zip。这个 zip 文件包含完整的数据集(图像、标签和边界框)。在后面的章节中会有更多的介绍。

接下来,我们有了output目录。这个目录是我们所有保存的模型、结果和其他重要需求被转储的地方。

父目录中有两个脚本:

  • 用于训练我们的目标探测器
  • predict.py:用于从我们的模型中进行推断,并查看运行中的对象检测器

最后,我们有最重要的目录,即pyimagesearch目录。它包含了 3 个非常重要的脚本。

  • bbox_regressor.py:容纳完整的物体检测器架构
  • config.py:包含端到端训练和推理管道的配置
  • custom_tensor_dataset.py:包含数据准备的自定义类

我们的项目目录审查到此结束。

配置物体检测的先决条件

我们的第一个任务是配置我们将在整个项目中使用的几个超参数。为此,让我们跳到pyimagesearch文件夹并打开config.py脚本。

# import the necessary packages
import torch
import os

# define the base path to the input dataset and then use it to derive
# the path to the input images and annotation CSV files
BASE_PATH = "dataset"
IMAGES_PATH = os.path.sep.join([BASE_PATH, "images"])
ANNOTS_PATH = os.path.sep.join([BASE_PATH, "annotations"])

# define the path to the base output directory
BASE_OUTPUT = "output"

# define the path to the output model, label encoder, plots output
# directory, and testing image paths
MODEL_PATH = os.path.sep.join([BASE_OUTPUT, "detector.pth"])
LE_PATH = os.path.sep.join([BASE_OUTPUT, "le.pickle"])
PLOTS_PATH = os.path.sep.join([BASE_OUTPUT, "plots"])
TEST_PATHS = os.path.sep.join([BASE_OUTPUT, "test_paths.txt"])

我们首先定义几个路径,我们稍后会用到它们。然后在第 7-12 行,我们为数据集(图像和注释)和输出定义路径。接下来,我们为我们的检测器和标签编码器创建单独的路径,然后是我们的绘图和测试图像的路径(第 16-19 行)。

# determine the current device and based on that set the pin memory
# flag
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
PIN_MEMORY = True if DEVICE == "cuda" else False

# specify ImageNet mean and standard deviation
MEAN = [0.485, 0.456, 0.406]
STD = [0.229, 0.224, 0.225]

# initialize our initial learning rate, number of epochs to train
# for, and the batch size
INIT_LR = 1e-4
NUM_EPOCHS = 20
BATCH_SIZE = 32

# specify the loss weights
LABELS = 1.0
BBOX = 1.0

由于我们正在训练一个对象检测器,建议在 GPU 上训练,而不是在 CPU 上,因为计算更复杂。因此,如果我们的系统中有兼容 CUDA 的 GPU,我们就将 PyTorch 设备设置为 CUDA(第 23 行和第 24 行)。

当然,我们将在数据集准备期间使用 PyTorch 变换。因此,我们指定平均值和标准偏差值(第 27 行和第 28 行)。这三个值分别代表通道方向、宽度方向和高度方向的平均值和标准偏差。最后,我们为我们的模型初始化超参数,如学习速率、时期、批量大小和损失权重(第 32-38 行)。

创建自定义对象检测数据处理器

让我们看看我们的数据目录。

!tree . 
.
├── dataset
│   ├── annotations
│   └── images
│       ├── airplane
│       ├── face
│       └── motorcycle

数据集细分为两个文件夹:annotations(包含边界框起点和终点的 CSV 文件)和 images(进一步分为三个文件夹,每个文件夹代表我们今天将使用的类)。

因为我们将使用 PyTorch 自己的数据加载器,所以以数据加载器能够接受的方式预处理数据是很重要的。custom_tensor_dataset.py脚本将完全做到这一点。

# import the necessary packages
from torch.utils.data import Dataset

class CustomTensorDataset(Dataset):
	# initialize the constructor
	def __init__(self, tensors, transforms=None):
		self.tensors = tensors
		self.transforms = transforms

我们创建了一个定制类CustomTensorDataset,它继承自torch.utils.data.Dataset类(第 4 行)。这样,我们可以根据需要配置内部函数,同时保留torch.utils.data.Dataset类的核心属性。

的第 6-8 行,构造函数__init__被创建。构造函数接受两个参数:

  • tensors:三个张量的元组,即图像、标签和边界框坐标。
  • transforms:将用于处理图像的torchvision.transforms实例。
	def __getitem__(self, index):
		# grab the image, label, and its bounding box coordinates
		image = self.tensors[0][index]
		label = self.tensors[1][index]
		bbox = self.tensors[2][index]

		# transpose the image such that its channel dimension becomes
		# the leading one
		image = image.permute(2, 0, 1)

		# check to see if we have any image transformations to apply
		# and if so, apply them
		if self.transforms:
			image = self.transforms(image)

		# return a tuple of the images, labels, and bounding
		# box coordinates
		return (image, label, bbox)

因为我们使用的是自定义类,所以我们将覆盖父类(Dataset)的方法。因此,根据我们的需要改变了__getitem__方法。但是,首先,tensor元组被解包到它的组成元素中(第 12-14 行)。

图像张量最初的形式是Height × Width × Channels。然而,所有 PyTorch 模型都需要他们的输入成为**“通道优先”**相应地,image.permute方法重新排列图像张量(行 18 )。

我们在第 22 行和第 23 行上为torchvision.transforms实例添加了一个检查。如果检查结果为true,图像将通过transform实例传递。此后,__getitem__方法返回图像、标签和边界框。

	def __len__(self):
		# return the size of the dataset
		return self.tensors[0].size(0)

我们要覆盖的第二个方法是__len__方法。它返回图像数据集张量的大小(第 29-31 行)。这就结束了custom_tensor_dataset.py脚本。

构建异议检测架构

谈到这个项目需要的模型,我们需要记住两件事。首先,为了避免额外的麻烦和有效的特征提取,我们将使用一个预先训练的模型作为基础模型。其次,基础模型将被分成两部分;盒式回归器和标签分类器。这两者都是独立的模型实体。

要记住的第二件事是,只有盒子回归器和标签分类器具有可训练的权重。预训练模型的重量将保持不变,如图图 4 所示。

考虑到这一点,让我们进入bbox_regressor.py

# import the necessary packages
from torch.nn import Dropout
from torch.nn import Identity
from torch.nn import Linear
from torch.nn import Module
from torch.nn import ReLU
from torch.nn import Sequential
from torch.nn import Sigmoid

class ObjectDetector(Module):
	def __init__(self, baseModel, numClasses):
		super(ObjectDetector, self).__init__()

		# initialize the base model and the number of classes
		self.baseModel = baseModel
		self.numClasses = numClasses

对于定制模型ObjectDetector,我们将使用torch.nn.Module作为父类(第 10 行)。对于构造函数__init__,有两个外部参数;基本型号和标签数量(第 11-16 行)。

		# build the regressor head for outputting the bounding box
		# coordinates
		self.regressor = Sequential(
			Linear(baseModel.fc.in_features, 128),
			ReLU(),
			Linear(128, 64),
			ReLU(),
			Linear(64, 32),
			ReLU(),
			Linear(32, 4),
			Sigmoid()
		)

继续讨论回归变量,记住我们的最终目标是产生 4 个单独的值:起始 x 轴值,起始 y 轴值,结束 x 轴值,以及结束 y 轴值。第一个Linear层输入基本模型的全连接层,输出尺寸设置为 128 ( 行 21 )。

接下来是几个LinearReLU层(行 22-27 ),最后以输出 4 值Linear层结束,接下来是Sigmoid层(行 28 )。

		# build the classifier head to predict the class labels
		self.classifier = Sequential(
			Linear(baseModel.fc.in_features, 512),
			ReLU(),
			Dropout(),
			Linear(512, 512),
			ReLU(),
			Dropout(),
			Linear(512, self.numClasses)
		)

		# set the classifier of our base model to produce outputs
		# from the last convolution block
		self.baseModel.fc = Identity()

下一步是对象标签的分类器。在回归器中,我们获取基本模型完全连接层的特征尺寸,并将其插入第一个Linear层(第 33 行)。接着重复ReLUDropoutLinear层(行 34-40 )。Dropout层通常用于帮助扩展概化和防止过度拟合。

初始化的最后一步是将基本模型的全连接层变成一个Identity层,这意味着它将镜像其之前的卷积块产生的输出(第 44 行)。

 	def forward(self, x):
		# pass the inputs through the base model and then obtain
		# predictions from two different branches of the network
		features = self.baseModel(x)
		bboxes = self.regressor(features)
		classLogits = self.classifier(features)

		# return the outputs as a tuple
		return (bboxes, classLogits)

接下来是forward步骤(第 46 行)。我们简单地将基本模型的输出通过回归器和分类器(第 49-51 行)。

这样,我们就完成了目标检测器的架构设计。

训练目标检测模型

在我们看到物体探测器工作之前,只剩下最后一步了。因此,让我们跳到train.py脚本并训练模型!

# USAGE
# python train.py

# import the necessary packages
from pyimagesearch.bbox_regressor import ObjectDetector
from pyimagesearch.custom_tensor_dataset import CustomTensorDataset
from pyimagesearch import config
from sklearn.preprocessing import LabelEncoder
from torch.utils.data import DataLoader
from torchvision import transforms
from torch.nn import CrossEntropyLoss
from torch.nn import MSELoss
from torch.optim import Adam
from torchvision.models import resnet50
from sklearn.model_selection import train_test_split
from imutils import paths
from tqdm import tqdm
import matplotlib.pyplot as plt
import numpy as np
import pickle
import torch
import time
import cv2
import os

# initialize the list of data (images), class labels, target bounding
# box coordinates, and image paths
print("[INFO] loading dataset...")
data = []
labels = []
bboxes = []
imagePaths = []

导入必要的包后,我们为数据、标签、边界框和图像路径创建空列表(第 29-32 行)。

现在是进行一些数据预处理的时候了。

# loop over all CSV files in the annotations directory
for csvPath in paths.list_files(config.ANNOTS_PATH, validExts=(".csv")):
	# load the contents of the current CSV annotations file
	rows = open(csvPath).read().strip().split("\n")

	# loop over the rows
	for row in rows:
		# break the row into the filename, bounding box coordinates,
		# and class label
		row = row.split(",")
		(filename, startX, startY, endX, endY, label) = row

		# derive the path to the input image, load the image (in
		# OpenCV format), and grab its dimensions
		imagePath = os.path.sep.join([config.IMAGES_PATH, label,
			filename])
		image = cv2.imread(imagePath)
		(h, w) = image.shape[:2]

		# scale the bounding box coordinates relative to the spatial
		# dimensions of the input image
		startX = float(startX) / w
		startY = float(startY) / h
		endX = float(endX) / w
		endY = float(endY) / h

		# load the image and preprocess it
		image = cv2.imread(imagePath)
		image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
		image = cv2.resize(image, (224, 224))

		# update our list of data, class labels, bounding boxes, and
		# image paths
		data.append(image)
		labels.append(label)
		bboxes.append((startX, startY, endX, endY))
		imagePaths.append(imagePath)

在第 35 行的处,我们开始遍历目录中所有可用的 CSV。打开 CSV,然后我们开始遍历这些行来分割数据(第 37-44 行)。

在将行值分割成一组单独的值之后,我们首先挑选出图像路径(行 48 )。然后,我们使用 OpenCV 读取图像并获得其高度和宽度(第 50 行和第 51 行)。

然后使用高度和宽度值将边界框坐标缩放到01的范围内(第 55-58 行)。

接下来,我们加载图像并做一些轻微的预处理(第 61-63 行)。

然后用解包后的值更新空列表,并且随着每次迭代的进行重复该过程(行 67-70 )。

# convert the data, class labels, bounding boxes, and image paths to
# NumPy arrays
data = np.array(data, dtype="float32")
labels = np.array(labels)
bboxes = np.array(bboxes, dtype="float32")
imagePaths = np.array(imagePaths)

# perform label encoding on the labels
le = LabelEncoder()
labels = le.fit_transform(labels)

为了更快地处理数据,列表被转换成numpy数组(第 74-77 行)。由于标签是字符串格式,我们使用 scikit-learn 的LabelEncoder 将它们转换成各自的索引(第 80 行和第 81 行)。

# partition the data into training and testing splits using 80% of
# the data for training and the remaining 20% for testing
split = train_test_split(data, labels, bboxes, imagePaths,
	test_size=0.20, random_state=42)

# unpack the data split
(trainImages, testImages) = split[:2]
(trainLabels, testLabels) = split[2:4]
(trainBBoxes, testBBoxes) = split[4:6]
(trainPaths, testPaths) = split[6:]

使用另一个方便的 scikit-learn 工具train_test_split,我们将数据分成训练集和测试集,保持一个80-20比率(第 85 行和第 86 行)。由于拆分将应用于传递给train_test_split函数的所有数组,我们可以使用简单的行切片将它们解包为元组(第 89-92 行)。

# convert NumPy arrays to PyTorch tensors
(trainImages, testImages) = torch.tensor(trainImages),\
	torch.tensor(testImages)
(trainLabels, testLabels) = torch.tensor(trainLabels),\
	torch.tensor(testLabels)
(trainBBoxes, testBBoxes) = torch.tensor(trainBBoxes),\
	torch.tensor(testBBoxes)

# define normalization transforms
transforms = transforms.Compose([
	transforms.ToPILImage(),
	transforms.ToTensor(),
	transforms.Normalize(mean=config.MEAN, std=config.STD)
])

解包的训练和测试数据、标签和边界框然后从 numpy 格式转换成 PyTorch 张量(第 95-100 行)。接下来,我们继续创建一个torchvision.transforms实例来轻松处理数据集(第 103-107 行)。这样,数据集也将使用config.py中定义的平均值和标准偏差值进行标准化。

# convert NumPy arrays to PyTorch datasets
trainDS = CustomTensorDataset((trainImages, trainLabels, trainBBoxes),
	transforms=transforms)
testDS = CustomTensorDataset((testImages, testLabels, testBBoxes),
	transforms=transforms)
print("[INFO] total training samples: {}...".format(len(trainDS)))
print("[INFO] total test samples: {}...".format(len(testDS)))

# calculate steps per epoch for training and validation set
trainSteps = len(trainDS) // config.BATCH_SIZE
valSteps = len(testDS) // config.BATCH_SIZE

# create data loaders
trainLoader = DataLoader(trainDS, batch_size=config.BATCH_SIZE,
	shuffle=True, num_workers=os.cpu_count(), pin_memory=config.PIN_MEMORY)
testLoader = DataLoader(testDS, batch_size=config.BATCH_SIZE,
	num_workers=os.cpu_count(), pin_memory=config.PIN_MEMORY)

记住,在custom_tensor_dataset.py脚本中,我们创建了一个定制的Dataset类来满足我们的确切需求。截至目前,我们所需的实体只是张量。因此,为了将它们转换成 PyTorch DataLoader 接受的格式,我们创建了CustomTensorDataset类的训练和测试实例,将图像、标签和边界框作为参数传递(第 110-113 行)。

行 118 和 119 上,使用数据集的长度和config.py中设置的批量值计算每个时期的步骤值。

最后,我们通过DataLoader传递CustomTensorDataset实例,并创建训练和测试数据加载器(第 122-125 行)。

# write the testing image paths to disk so that we can use then
# when evaluating/testing our object detector
print("[INFO] saving testing image paths...")
f = open(config.TEST_PATHS, "w")
f.write("\n".join(testPaths))
f.close()

# load the ResNet50 network
resnet = resnet50(pretrained=True)

# freeze all ResNet50 layers so they will *not* be updated during the
# training process
for param in resnet.parameters():
	param.requires_grad = False

因为我们稍后将使用测试映像路径进行评估,所以它被写入磁盘(行 129-132 )。

对于我们架构中的基本模型,我们将使用一个预先训练好的 resnet50 ( Line 135 )。然而,如前所述,基本模型的重量将保持不变。因此,我们冻结了权重(第 139 和 140 行)。

# create our custom object detector model and flash it to the current
# device
objectDetector = ObjectDetector(resnet, len(le.classes_))
objectDetector = objectDetector.to(config.DEVICE)

# define our loss functions
classLossFunc = CrossEntropyLoss()
bboxLossFunc = MSELoss()

# initialize the optimizer, compile the model, and show the model
# summary
opt = Adam(objectDetector.parameters(), lr=config.INIT_LR)
print(objectDetector)

# initialize a dictionary to store training history
H = {
    
    "total_train_loss": [], "total_val_loss": [], "train_class_acc": [],
	 "val_class_acc": []}

模型先决条件完成后,我们创建我们的定制模型实例,并将其加载到当前设备中(第 144 行和第 145 行)。对于分类器损失,使用交叉熵损失,而对于箱式回归器,我们坚持均方误差损失(第 148 和 149 行)。在 153Adam被设置为目标探测器优化器。为了跟踪训练损失和其他度量,字典H行 157 和 158 上被初始化。

# loop over epochs
print("[INFO] training the network...")
startTime = time.time()
for e in tqdm(range(config.NUM_EPOCHS)):
	# set the model in training mode
	objectDetector.train()

	# initialize the total training and validation loss
	totalTrainLoss = 0
	totalValLoss = 0

	# initialize the number of correct predictions in the training
	# and validation step
	trainCorrect = 0
	valCorrect = 0

对于训练速度评估,记录开始时间(行 162 )。循环多个时期,我们首先将对象检测器设置为训练模式(第 165 行,并初始化正确预测的损失和数量(第 168-174 行)。

	# loop over the training set
	for (images, labels, bboxes) in trainLoader:
		# send the input to the device
		(images, labels, bboxes) = (images.to(config.DEVICE),
			labels.to(config.DEVICE), bboxes.to(config.DEVICE))

		# perform a forward pass and calculate the training loss
		predictions = objectDetector(images)
		bboxLoss = bboxLossFunc(predictions[0], bboxes)
		classLoss = classLossFunc(predictions[1], labels)
		totalLoss = (config.BBOX * bboxLoss) + (config.LABELS * classLoss)

		# zero out the gradients, perform the backpropagation step,
		# and update the weights
		opt.zero_grad()
		totalLoss.backward()
		opt.step()

		# add the loss to the total training loss so far and
		# calculate the number of correct predictions
		totalTrainLoss += totalLoss
		trainCorrect += (predictions[1].argmax(1) == labels).type(
			torch.float).sum().item()

在列车数据加载器上循环,我们首先将图像、标签和边界框加载到正在使用的设备中(行 179 和 180 )。接下来,我们将图像插入我们的对象检测器,并存储预测结果( Line 183 )。最后,由于模型将给出两个预测(一个用于标签,一个用于边界框),我们将它们索引出来并分别计算这些损失(第 183-185 行)。

这两个损失的组合值将作为架构的总损失。我们将在config.py中定义的边界框损失和标签损失的各自损失权重乘以损失,并将它们相加(第 186 行)。

在 PyTorch 的自动梯度功能的帮助下,我们简单地重置梯度,计算由于产生的损失而产生的权重,并基于当前步骤的梯度更新参数(第 190-192 行)。重置梯度是很重要的,因为backward函数一直在累积梯度。因为我们只想要当前步骤的梯度,所以opt.zero_grad清除了先前的值。

在第行第 196-198 行,我们更新损失值并修正预测。

	# switch off autograd
	with torch.no_grad():
		# set the model in evaluation mode
		objectDetector.eval()

		# loop over the validation set
		for (images, labels, bboxes) in testLoader:
			# send the input to the device
			(images, labels, bboxes) = (images.to(config.DEVICE),
				labels.to(config.DEVICE), bboxes.to(config.DEVICE))

			# make the predictions and calculate the validation loss
			predictions = objectDetector(images)
			bboxLoss = bboxLossFunc(predictions[0], bboxes)
			classLoss = classLossFunc(predictions[1], labels)
			totalLoss = (config.BBOX * bboxLoss) + \
				(config.LABELS * classLoss)
			totalValLoss += totalLoss

			# calculate the number of correct predictions
			valCorrect += (predictions[1].argmax(1) == labels).type(
				torch.float).sum().item()

转到模型评估,我们将首先关闭自动渐变并切换到对象检测器的评估模式(行 201-203 )。然后,循环测试数据,除了更新权重之外,我们将重复与训练中相同的过程(第 212-214 行)。

以与训练步骤相同的方式计算组合损失(行 215 和 216 )。因此,总损失值和正确预测被更新(第 217-221 行)。

	# calculate the average training and validation loss
	avgTrainLoss = totalTrainLoss / trainSteps
	avgValLoss = totalValLoss / valSteps

	# calculate the training and validation accuracy
	trainCorrect = trainCorrect / len(trainDS)
	valCorrect = valCorrect / len(testDS)

	# update our training history
	H["total_train_loss"].append(avgTrainLoss.cpu().detach().numpy())
	H["train_class_acc"].append(trainCorrect)
	H["total_val_loss"].append(avgValLoss.cpu().detach().numpy())
	H["val_class_acc"].append(valCorrect)

	# print the model training and validation information
	print("[INFO] EPOCH: {}/{}".format(e + 1, config.NUM_EPOCHS))
	print("Train loss: {:.6f}, Train accuracy: {:.4f}".format(
		avgTrainLoss, trainCorrect))
	print("Val loss: {:.6f}, Val accuracy: {:.4f}".format(
		avgValLoss, valCorrect))
endTime = time.time()
print("[INFO] total time taken to train the model: {:.2f}s".format(
	endTime - startTime))

在一个时期之后,在行 224 和 225 上计算平均分批训练和测试损失。我们还使用正确预测的数量来计算历元的训练和测试精度(行 228 和 229 )。

在计算之后,所有的值都记录在模型历史字典H ( 第 232-235 行)中,同时计算结束时间以查看训练花费了多长时间以及退出循环之后(第 243 行)。

# serialize the model to disk
print("[INFO] saving object detector model...")
torch.save(objectDetector, config.MODEL_PATH)

# serialize the label encoder to disk
print("[INFO] saving label encoder...")
f = open(config.LE_PATH, "wb")
f.write(pickle.dumps(le))
f.close()

# plot the training loss and accuracy
plt.style.use("ggplot")
plt.figure()
plt.plot(H["total_train_loss"], label="total_train_loss")
plt.plot(H["total_val_loss"], label="total_val_loss")
plt.plot(H["train_class_acc"], label="train_class_acc")
plt.plot(H["val_class_acc"], label="val_class_acc")
plt.title("Total Training Loss and Classification Accuracy on Dataset")
plt.xlabel("Epoch #")
plt.ylabel("Loss/Accuracy")
plt.legend(loc="lower left")

# save the training plot
plotPath = os.path.sep.join([config.PLOTS_PATH, "training.png"])
plt.savefig(plotPath)

因为我们将使用对象检测器进行推理,所以我们将它保存到磁盘中( Line 249 )。我们还保存了已创建的标签编码器,因此模式保持不变(第 253-255 行

为了评估模型训练,我们绘制了存储在模型历史字典H ( 第 258-271 行)中的所有指标。

模型训练到此结束。接下来,我们来看看物体探测器训练的有多好!

评估物体探测训练

由于模型的大部分重量保持不变,训练不会花很长时间。首先,让我们来看看一些训练时期。

[INFO] training the network...
  0%|          | 0/20 [00:00<?,  
  5%|| 1/20 [00:16<05:08, 16.21s/it][INFO] EPOCH: 1/20
Train loss: 0.874699, Train accuracy: 0.7608
Val loss: 0.360270, Val accuracy: 0.9902
 10%|| 2/20 [00:31<04:46, 15.89s/it][INFO] EPOCH: 2/20
Train loss: 0.186642, Train accuracy: 0.9834
Val loss: 0.052412, Val accuracy: 1.0000
 15%|█▌        | 3/20 [00:47<04:28, 15.77s/it][INFO] EPOCH: 3/20
Train loss: 0.066982, Train accuracy: 0.9883
...
 85%|████████▌ | 17/20 [04:27<00:47, 15.73s/it][INFO] EPOCH: 17/20
Train loss: 0.011934, Train accuracy: 0.9975
Val loss: 0.004053, Val accuracy: 1.0000
 90%|█████████ | 18/20 [04:43<00:31, 15.67s/it][INFO] EPOCH: 18/20
Train loss: 0.009135, Train accuracy: 0.9975
Val loss: 0.003720, Val accuracy: 1.0000
 95%|█████████▌| 19/20 [04:58<00:15, 15.66s/it][INFO] EPOCH: 19/20
Train loss: 0.009403, Train accuracy: 0.9982
Val loss: 0.003248, Val accuracy: 1.0000
100%|██████████| 20/20 [05:14<00:00, 15.73s/it][INFO] EPOCH: 20/20
Train loss: 0.006543, Train accuracy: 0.9994
Val loss: 0.003041, Val accuracy: 1.0000
[INFO] total time taken to train the model: 314.68s

我们看到,该模型在训练和验证时分别达到了惊人的精度 0.99941.0000 。让我们看看训练图上划时代的变化图 5

该模型在训练值和验证值方面都相当快地达到了饱和水平。现在是时候看看物体探测器的作用了!

从物体检测器得出推论

这个旅程的最后一步是predict.py脚本。这里,我们将逐个循环测试图像,并用我们的预测值绘制边界框。

# USAGE
# python predict.py --input datasimg/face/image_0131.jpg

# import the necessary packages
from pyimagesearch import config
from torchvision import transforms
import mimetypes
import argparse
import imutils
import pickle
import torch
import cv2

# construct the argument parser and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--input", required=True,
	help="path to input image/text file of image paths")
args = vars(ap.parse_args())

argparse模块用于编写用户友好的命令行界面命令。在的第 15-18 行,我们构建了一个参数解析器来帮助用户选择测试图像。

# determine the input file type, but assume that we're working with
# single input image
filetype = mimetypes.guess_type(args["input"])[0]
imagePaths = [args["input"]]

# if the file type is a text file, then we need to process *multiple*
# images
if "text/plain" == filetype:
	# load the image paths in our testing file
	imagePaths = open(args["input"]).read().strip().split("\n")

我们按照参数解析的步骤来处理用户给出的任何类型的输入。在行 22 和 23 上,imagePaths变量被设置为处理单个输入图像,而在行 27-29 上,处理多个图像的事件。

# load our object detector, set it evaluation mode, and label
# encoder from disk
print("[INFO] loading object detector...")
model = torch.load(config.MODEL_PATH).to(config.DEVICE)
model.eval()
le = pickle.loads(open(config.LE_PATH, "rb").read())

# define normalization transforms
transforms = transforms.Compose([
	transforms.ToPILImage(),
	transforms.ToTensor(),
	transforms.Normalize(mean=config.MEAN, std=config.STD)
])

使用train.py脚本训练的模型被调用进行评估(第 34 和 35 行)。类似地,使用前述脚本存储的标签编码器被加载(第 36 行)。因为我们需要再次处理数据,所以创建了另一个torchvision.transforms实例,其参数与训练中使用的参数相同。

# loop over the images that we'll be testing using our bounding box
# regression model
for imagePath in imagePaths:
	# load the image, copy it, swap its colors channels, resize it, and
	# bring its channel dimension forward
	image = cv2.imread(imagePath)
	orig = image.copy()
	image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
	image = cv2.resize(image, (224, 224))
	image = image.transpose((2, 0, 1))

	# convert image to PyTorch tensor, normalize it, flash it to the
	# current device, and add a batch dimension
	image = torch.from_numpy(image)
	image = transforms(image).to(config.DEVICE)
	image = image.unsqueeze(0)

循环测试图像,我们读取图像并对其进行一些预处理(第 50-54 行)。这样做是因为我们的图像需要再次插入对象检测器。

我们继续将图像转换成张量,对其应用torchvision.transforms实例,并为其添加批处理维度(第 58-60 行)。我们的测试图像现在可以插入到对象检测器中了。

	# predict the bounding box of the object along with the class
	# label
	(boxPreds, labelPreds) = model(image)
	(startX, startY, endX, endY) = boxPreds[0]

	# determine the class label with the largest predicted
	# probability
	labelPreds = torch.nn.Softmax(dim=-1)(labelPreds)
	i = labelPreds.argmax(dim=-1).cpu()
	label = le.inverse_transform(i)[0]

首先,从模型中获得预测(行 64 )。我们继续从boxPreds变量(**第 65 行)**解包边界框值。

标签预测上的简单 softmax 函数将为我们提供对应于类的值的更好的图片。为此,我们在69 线上使用 PyTorch 自己的torch.nn.Softmax。用argmax隔离索引,我们将它插入标签编码器le,并使用inverse_transform(索引到值)来获得标签的名称(第 69-71 行)。

	# resize the original image such that it fits on our screen, and
	# grab its dimensions
	orig = imutils.resize(orig, width=600)
	(h, w) = orig.shape[:2]

	# scale the predicted bounding box coordinates based on the image
	# dimensions
	startX = int(startX * w)
	startY = int(startY * h)
	endX = int(endX * w)
	endY = int(endY * h)

	# draw the predicted bounding box and class label on the image
	y = startY - 10 if startY - 10 > 10 else startY + 10
	cv2.putText(orig, label, (startX, y), cv2.FONT_HERSHEY_SIMPLEX,
		0.65, (0, 255, 0), 2)
	cv2.rectangle(orig, (startX, startY), (endX, endY),
		(0, 255, 0), 2)

	# show the output image 
	cv2.imshow("Output", orig)
	cv2.waitKey(0)

第 75 行,我们已经调整了原始图像的大小以适应我们的屏幕。然后存储尺寸调整后的图像的高度和宽度,以基于图像缩放预测的边界框值(第 76-83 行)。之所以这样做,是因为我们在将注释与模型匹配之前,已经将它们缩小到了范围01。因此,出于显示目的,所有输出都必须放大。

显示边界框时,标签名称也将显示在它的顶部。为此,我们为第 86 行的文本设置了 y 轴的值。使用 OpenCV 的putText函数,我们设置了显示在图像上的标签(第 87 行和第 88 行)。

最后,我们使用 OpenCV 的rectangle方法在图像上创建边界框(第 89 行和第 90 行)。因为我们有起始 x 轴、起始 y 轴、结束 x 轴和结束 y 轴的值,所以很容易从它们创建一个矩形。这个矩形将包围我们的对象。

我们的推理脚本到此结束。让我们看看结果吧!

动作中的物体检测

让我们看看我们的对象检测器的表现如何,使用来自每个类的一个图像。我们首先使用一个飞机的图像(图 6 ),然后是一个人脸下的图像(图 7 ),以及一个属于摩托车类的图像(图 8 )。

事实证明,我们的模型的精度值没有说谎。我们的模型不仅正确地猜出了标签,而且生成的包围盒也几乎是完美的!

有了如此精确的检测和结果,我们都可以认为我们的小项目是成功的,不是吗?

摘要

在写这个物体检测教程的时候,回想起来我意识到了几件事。

老实说,我从来不喜欢在我的项目中使用预先训练好的模型。它会觉得我的工作不再是我的工作了。显然,这被证明是一个愚蠢的想法,事实上我的第一个一次性人脸分类器说我和我最好的朋友是同一个人(相信我,我们看起来一点也不相似)。

我认为本教程是一个很好的例子,说明了当你有一个训练有素的特征提取器时会发生什么。我们不仅节省了时间,而且最终的结果也是辉煌的。以图 6 和图 8 为例。预测的边界框具有最小的误差。

当然,这并不意味着没有改进的空间。在图 7 中,图像有许多元素,但物体探测器已经设法捕捉到物体的大致区域。然而,它可以更紧凑。我们强烈建议您修改参数,看看您的结果是否更好!

也就是说,物体检测在当今世界中扮演着重要的角色。自动交通、人脸检测、无人驾驶汽车只是物体检测蓬勃发展的现实世界应用中的一部分。每年,算法都被设计得更快更紧凑。我们已经达到了一个阶段,算法可以同时检测视频场景中的所有对象!我希望这篇教程激起了你对揭示这个领域复杂性的好奇心。

要下载这篇文章的源代码(并在未来教程在 PyImageSearch 上发布时得到通知),只需在下面的表格中输入您的电子邮件地址!*

在自定义数据集上训练 YOLOv5 对象检测器

原文:https://pyimagesearch.com/2022/06/20/training-the-yolov5-object-detector-on-a-custom-dataset/


目录


在自定义数据集上训练 YOLOv5 物体检测器

在深度学习的帮助下,我们都知道计算机视觉领域在最近十年里激增。因此,许多流行的计算机视觉问题,如图像分类、对象检测和具有真实工业用例的分割,开始实现前所未有的准确性。从 2012 年开始,每年都会设定一个新的基准。今天,我们将从实用的角度来看物体检测。

对象检测具有各种最先进的架构,可以在现实世界的数据集上使用现成的架构,以合理的准确度检测对象。唯一的条件是测试数据集与预训练的检测器具有相同的类。

但是,您经常构建的应用程序可能有不同于预训练对象检测器类的对象。例如,数据集分布与训练数据集检测器的地方非常不同。在这种情况下,我们经常使用迁移学习的概念,其中我们使用预先训练的检测器,并在较新的数据集上对其进行微调。在今天的教程中,您将学习在自定义数据集上训练预训练的 YOLOv5 对象检测器,而无需编写太多代码。

我们将不深入 YOLOv5 物体探测器的理论细节;然而,你可以查看我们的介绍 YOLO 家族的博客帖子,在那里我们涵盖了一些相关的内容。

这是我们关于 YOLO 的 7 集系列的最后一课:

  1. YOLO 家族简介
  2. 了解一个实时物体检测网络:你只看一次(YOLOv1)
  3. 更好、更快、更强的物体探测器(YOLOv2)
  4. 使用 COCO 评估器 平均精度(mAP)
  5. 用 Darknet-53 和多尺度预测的增量改进(YOLOv3)
  6. 【yolov 4】
  7. 在自定义数据集上训练 YOLOv5 物体检测器 (今日教程)

要了解如何在自定义数据集 上训练 YOLOv5 物体检测器,只需继续阅读。


在自定义数据集上训练 YOLOv5 物体检测器

2020 年, Ultralytics 的创始人兼 CEO 格伦·约彻(Glenn Jocher)在 GitHub 上发布了其 YOLOv5 的开源实现。YOLOv5 提供了一系列在 MS COCO 数据集上预先训练的对象检测架构。

今天,YOLOv5 是官方最先进的模型之一,拥有巨大的支持,并且在生产中更容易使用。最好的部分是 YOLOv5 是在 PyTorch 中原生实现的,消除了 Darknet 框架的限制(基于 C 编程语言)。

从 YOLO 到 PyTorch 框架的巨大变化使得开发人员更容易修改架构并直接导出到许多部署环境中。别忘了,YOLOv5 是火炬中心展示区的官方最先进车型之一。

表 1 显示了 Volta 100 GPU 上 640×640 图像分辨率下 MS COCO 验证数据集上五个 YOLOv5 变体的性能(mAP)和速度(FPS)基准。所有五个模型都是在 MS COCO 训练数据集上训练的。型号性能指标评测从 YOLOv5n 开始按升序显示(即,具有最小型号尺寸的 nano 变体到最大型号 YOLOv5x)。

| | | | | 速度 | 速度 | 速度 | | |
| | 大小 | | | CPU b1 | V100 b1 | V100 b32 | 参数 | 一偏 |
| 型号 | 【像素】 | 0.5:0.95 | 0.5 | (ms) | (ms) | (ms) | 【米】 | @ 640(B) |
| 约洛夫 5n | 640
| 28.4 | 46.0 | 45
| 6.3 | 0.6 | 1.9 | 4.5 |
| 约洛夫 5s | 640
| 37.2 | 56.0 | 98
| 6.4 | 0.9
| 7.2 | 16.5 |
| yolov 5m
| 640
| 45.2 | 63.9 | 224
| 8.2 | 1.7 | 21.2 | 49
|
| 约洛夫 5l | 640
| 48.8 | 67.2 | 430
| 10.1 | 2.7 | 46.5 | 109.1 |
| YOLOv5x | 640
| 50.7 | 68.9 | 766 | 12.1 | 4.8 | 86.7 | 205.7 |
| **表 1:**MS COCO 数据集上五个 YOLOv5 变种的性能和速度基准。
|

今天,我们将通过在自定义数据集上迁移学习来学习如何在 PyTorch 框架中利用 YOLOv5 的强大功能!


配置您的开发环境

为了遵循这个指南,您需要克隆 Ultralytics YOLOv5 存储库并从requirements.txt安装所有必需的包pip

幸运的是,要运行 YOLOv5 培训,您只需在requirements.txt文件上进行 pip 安装,这意味着所有的库都可以通过 pip 安装!

$ git clone https://github.com/ultralytics/yolov5.git #clone repo
$ cd yolov5/
$ pip install -r requirements.txt #install dependencies

在配置开发环境时遇到了问题?

说了这么多,你是:

  • 时间紧迫?
  • 了解你雇主的行政锁定系统?
  • 想要跳过与命令行、包管理器和虚拟环境斗争的麻烦吗?
  • 准备好在您的 Windows、macOS 或 Linux 系统上运行代码***?***

*那今天就加入 PyImageSearch 大学吧!

获得本教程的 Jupyter 笔记本和其他 PyImageSearch 指南,这些指南是 预先配置的 **,可以在您的网络浏览器中运行在 Google Colab 的生态系统上!**无需安装。

最棒的是,这些 Jupyter 笔记本可以在 Windows、macOS 和 Linux 上运行!


关于数据集

对于今天的实验,我们将在两个不同的数据集上训练 YOLOv5 模型,即 Udacity 自动驾驶汽车数据集Vehicles-OpenImages 数据集

这些数据集是公开的,但我们从 Roboflow 下载它们,它提供了一个很好的平台,可以用计算机视觉领域的各种数据集来训练你的模型。更有趣的是,你可以下载多种格式的数据集,如 COCO JSON、YOLO Darknet TXT 和 YOLOv5 PyTorch。这节省了编写助手函数的时间,以便将基本事实注释转换成模型所需的格式。


YOLOv5 标签格式

因为我们将训练 YOLOv5 PyTorch 模型,所以我们将以 YOLOv5 格式下载数据集。YOLOv5 的基本事实注释格式非常简单(图 2中显示了一个例子),因此您可以自己编写一个脚本来完成这项工作。每个图像的每个边界框都有一个单行的text文件。例如,如果一个图像中有四个对象,text文件将有四行包含类标签和边界框坐标。每行的格式是

class_id center_x center_y width height

其中字段以空格分隔,坐标从01标准化。要从像素值转换为标准化的xywh,将 x &框的宽度除以图像的宽度,将 y &框的高度除以图像的高度。


车辆-OpenImages 数据集

该数据集仅包含用于对象检测的各种车辆类别的 627 幅图像,如汽车公共汽车救护车摩托车卡车。这些图像来源于开放图像开源计算机视觉数据集。该数据集受知识共享许可的保护,它允许你共享和改编数据集,甚至将其用于商业用途。

图 3 显示了来自数据集的一些样本图像,带有用绿色标注的地面实况边界框。

在 627 幅图像中有 1194 个感兴趣区域(对象),这意味着每幅图像至少有 1.9 个对象。基于图 4 中所示的启发,汽车类贡献了超过 50%的对象。相比之下,其余的类别:公共汽车、卡车、摩托车和救护车,相对于小汽车类别来说代表性不足。


Udacity 自动驾驶汽车数据集

请注意,我们不会在此数据集上训练 YOLOv5 模型。相反,我们为你们所有人确定了这个伟大的数据集作为练习,以便一旦你们从本教程中学习完,你们可以使用这个数据集来训练对象检测器。

这个数据集来源于原始的 Udacity 自动驾驶汽车数据集。不幸的是,原始数据集缺少数以千计的行人、骑车人、汽车和交通灯的标注。因此,Roboflow 设法重新标记了数据集,以纠正错误和遗漏。

图 5 显示了 Roboflow 标注的数据集和原始数据集中缺失的标签的一些例子。如果你使用过自动驾驶城市场景数据集,如 CityscapesApolloScapeBerkeley DeepDrive ,你会发现这个数据集与那些数据集非常相似。

该数据集包含 11 个类别的 97,942 个标签和 15,000 幅图像。该数据集在 Roboflow 上以两种不同的方式提供:具有1920x1200(下载大小~3.1 GB)的图像和适合大多数人的具有512x512(下载大小~580 MB)的降采样版本。我们将使用下采样版本,因为它的尺寸更小,符合我们对网络的要求。

像前面的 Vehicles-OpenImages 数据集一样,这个数据集拥有最多属于car类的对象(超过总对象的 60%)。图 6 显示了 Udacity 自动驾驶汽车数据集中的类别分布:


选择型号

图 7 显示了五种 YOLOv5 变体,从为在移动和嵌入式设备上运行而构建的最小 YOLOv5 nano 模型开始,到另一端的 YOLOv5 XLarge。对于今天的实验,我们将利用基本模型 YOLOv5s,它提供了准确性和速度之间的良好平衡。


约洛夫 5 训练

**这一部分是今天教程的核心,我们将涵盖大部分内容,从

  • 下载数据集
  • 创建数据配置
  • 培训 YOLOv5 模型
  • 可视化 YOLOv5 模型工件
  • 定量结果
  • 冻结初始图层并微调剩余图层
  • 结果

下载车辆-打开图像数据集

# Download the vehicles-open image dataset
!mkdir vehicles_open_image
%cd vehicles_open_image
!curl -L "https://public.roboflow.com/ds/2Tb6yXY8l8?key=Eg82WpxUEr" > vehicles.zip 
!unzip vehicles.zip
!rm vehicles.zip

的第 2 行和第 3 行,我们创建了vehicles_open_image目录,并将cd放入我们下载数据集的目录中。然后,在的第 4 行,我们使用curl命令,并将从获得的数据集 URL 传递到这里。最后,我们解压缩数据集并删除第 5 行和第 6 行上的 zip 文件。

让我们看看vehicles_open_image文件夹的内容:

$tree /content/vehicles_open_image -L 2
/content/vehicles_open_image
├── data.yaml
├── README.dataset.txt
├── README.roboflow.txt
├── test
│   ├── images
│   └── labels
├── train
│   ├── images
│   └── labels
└── valid
    ├── images
    └── labels

9 directories, 3 files

父目录有三个文件,其中只有data.yaml是必需的,还有三个子目录:

  • data.yaml:有数据相关的配置,如列车和有效数据目录路径,数据集中的类总数,每个类的名称
  • train:带有训练标签的训练图像
  • valid:带注释的验证图像
  • test:测试图像和标签。如果带有标签的测试数据可用,评估模型的性能就变得容易了。

配置设置

接下来,我们将编辑data.yaml文件,为trainvalid图像设置path和绝对路径。

# Create configuration
import yaml
config = {
    
    'path': '/content/vehicles_open_image',
         'train': '/content/vehicles_open_image/train',
         'val': '/content/vehicles_open_image/valid',
         'nc': 5,
         'names': ['Ambulance', 'Bus', 'Car', 'Motorcycle', 'Truck']}

with open("data.yaml", "w") as file:
   yaml.dump(config, file, default_flow_style=False)

行 2 上,我们导入yaml模块,这将允许我们以.yaml格式保存data.yaml配置文件。然后从第 3-7 行开始,我们在一个config变量中定义数据路径、训练、验证、类的数量和类名。所以config是一本字典。

最后,在的第 9 行和第 10 行,我们打开与数据集一起下载的现有data.yaml文件,用config中的内容覆盖它,并将其存储在磁盘上。


【yolo V5】训练超参数和模型配置

YOLOv5 有大约 30 个用于各种训练设置的超参数。这些在hyp.scratch-low.yaml中定义,用于从零开始的低增强 COCO 训练,放置在/data目录中。训练数据超参数如下所示,它们对于产生良好的结果非常重要,因此在开始训练之前,请确保正确初始化这些值。对于本教程,我们将简单地使用默认值,这些值是为 YOLOv5 COCO 培训从头开始优化的。

如您所见,它有learning rateweight_decayiou_t (IoU 训练阈值),以及一些数据增强超参数,如translatescalemosaicmixupcopy_pastemixup:0.0表示不应应用混合数据增强。

lr0: 0.01  # initial learning rate (SGD=1E-2, Adam=1E-3)
lrf: 0.01  # final OneCycleLR learning rate (lr0 * lrf)
momentum: 0.937  # SGD momentum/Adam beta1
weight_decay: 0.0005  # optimizer weight decay 5e-4
warmup_epochs: 3.0  # warmup epochs (fractions ok)
warmup_momentum: 0.8  # warmup initial momentum
warmup_bias_lr: 0.1  # warmup initial bias lr
box: 0.05  # box loss gain
cls: 0.5  # cls loss gain
cls_pw: 1.0  # cls BCELoss positive_weight
obj: 1.0  # obj loss gain (scale with pixels)
obj_pw: 1.0  # obj BCELoss positive_weight
iou_t: 0.20  # IoU training threshold
anchor_t: 4.0  # anchor-multiple threshold
# anchors: 3  # anchors per output layer (0 to ignore)
fl_gamma: 0.0  # focal loss gamma (efficientDet default gamma=1.5)
hsv_h: 0.015  # image HSV-Hue augmentation (fraction)
hsv_s: 0.7  # image HSV-Saturation augmentation (fraction)
hsv_v: 0.4  # image HSV-Value augmentation (fraction)
degrees: 0.0  # image rotation (+/- deg)
translate: 0.1  # image translation (+/- fraction)
scale: 0.5  # image scale (+/- gain)
shear: 0.0  # image shear (+/- deg)
perspective: 0.0  # image perspective (+/- fraction), range 0-0.001
flipud: 0.0  # image flip up-down (probability)
fliplr: 0.5  # image flip left-right (probability)
mosaic: 1.0  # image mosaic (probability)
mixup: 0.0  # image mixup (probability)
copy_paste: 0.0  # segment copy-paste (probability)

接下来,您可以简单地查看一下YOLOv5s网络架构的结构,尽管您几乎不会修改模型配置文件,这与训练数据超参数不同。对于 COCO 女士数据集,它将nc设置为80,将backbone设置为特征提取,然后将head设置为检测。

# Parameters
nc: 80  # number of classes
depth_multiple: 0.33  # model depth multiple
width_multiple: 0.50  # layer channel multiple
anchors:
  - [10,13, 16,30, 33,23]  # P3/8
  - [30,61, 62,45, 59,119]  # P4/16
  - [116,90, 156,198, 373,326]  # P5/32

# YOLOv5 v6.0 backbone
backbone:
  # [from, number, module, args]
  [[-1, 1, Conv, [64, 6, 2, 2]],  # 0-P1/2
   [-1, 1, Conv, [128, 3, 2]],  # 1-P2/4
   [-1, 3, C3, [128]],
   [-1, 1, Conv, [256, 3, 2]],  # 3-P3/8
   [-1, 6, C3, [256]],
   [-1, 1, Conv, [512, 3, 2]],  # 5-P4/16
   [-1, 9, C3, [512]],
   [-1, 1, Conv, [1024, 3, 2]],  # 7-P5/32
   [-1, 3, C3, [1024]],
   [-1, 1, SPPF, [1024, 5]],  # 9
  ]

# YOLOv5 v6.0 head
head:
  [[-1, 1, Conv, [512, 1, 1]],
   [-1, 1, nn.Upsample, [None, 2, 'nearest']],
   [[-1, 6], 1, Concat, [1]],  # cat backbone P4
   [-1, 3, C3, [512, False]],  # 13

   [-1, 1, Conv, [256, 1, 1]],
   [-1, 1, nn.Upsample, [None, 2, 'nearest']],
   [[-1, 4], 1, Concat, [1]],  # cat backbone P3
   [-1, 3, C3, [256, False]],  # 17 (P3/8-small)

   [-1, 1, Conv, [256, 3, 2]],
   [[-1, 14], 1, Concat, [1]],  # cat head P4
   [-1, 3, C3, [512, False]],  # 20 (P4/16-medium)

   [-1, 1, Conv, [512, 3, 2]],
   [[-1, 10], 1, Concat, [1]],  # cat head P5
   [-1, 3, C3, [1024, False]],  # 23 (P5/32-large)

   [[17, 20, 23], 1, Detect, [nc, anchors]],  # Detect(P3, P4, P5)
  ]

训练约洛夫 5s 模型

我们几乎已经准备好训练 YOLOv5 模型了,正如上面讨论的,我们将训练 YOLOv5s 模型。但是,在我们运行培训之前,让我们定义几个参数:

SIZE = 640
BATCH_SIZE = 32
EPOCHS = 20
MODEL = "yolov5s"
WORKERS = 1
PROJECT = "vehicles_open_image_pyimagesearch"
RUN_NAME = f"{
      
      MODEL}_size{
      
      SIZE}_epochs{
      
      EPOCHS}_batch{
      
      BATCH_SIZE}_small"

我们定义了几个标准模型参数:

  • SIZE:训练时的图像尺寸或网络输入。图像在被传送到网络之前将被调整到这个值。预处理管道会将它们的大小调整为640像素。
  • BATCH_SIZE:作为单个批次送入网络进行正向传送的图像数量。可以根据可用的 GPU 内存进行修改。我们已经将其设置为32
  • EPOCHS:我们想要在完整数据集上训练模型的次数。
  • MODEL:我们希望用于训练的基础模型。我们使用 YOLOv5 系列的小型型号yolov5s
  • WORKERS:要使用的最大数据加载器工作进程。
  • PROJECT:这将在当前目录下创建一个项目目录(yolov5)。
  • RUN_NAME:每次运行这个模型时,它都会在项目目录下创建一个子目录,其中会有很多关于模型的信息,比如权重、样本输入图像、一些验证预测输出、度量图等。
!python train.py --img {
    
    SIZE}\
               --batch {
    
    BATCH_SIZE}\
               --epochs {
    
    EPOCHS}\
               --data ../vehicles_open_image/data.yaml\
               --weights {
    
    MODEL}.pt\
               --workers {
    
    WORKERS}\
               --project {
    
    PROJECT}\
               --name {
    
    RUN_NAME}\
               --exist-ok

如果没有错误,培训将如下所示开始。日志表明 YOLOv5 模型将在特斯拉 T4 GPU 上使用 Torch 版进行训练;除此之外,它还显示了初始化的hyperparameters

下载了yolov5s.pt权重,这意味着用 MS COCO 数据集训练的参数来初始化 YOLOv5s 模型。最后,我们可以看到两个纪元已经用一个[email protected]=0.237完成了。

github: up to date with https://github.com/ultralytics/yolov5 ✅
YOLOv5 ? v6.1-236-gdcf8073 Python-3.7.13 torch-1.11.0+cu113 CUDA:0 (Tesla T4, 15110MiB)
hyperparameters: lr0=0.01, lrf=0.01, momentum=0.937, weight_decay=0.0005, warmup_epochs=3.0, warmup_momentum=0.8, warmup_bias_lr=0.1, box=0.05, cls=0.5, cls_pw=1.0, obj=1.0, obj_pw=1.0, iou_t=0.2, anchor_t=4.0, fl_gamma=0.0, hsv_h=0.015, hsv_s=0.7, hsv_v=0.4, degrees=0.0, translate=0.1, scale=0.5, shear=0.0, perspective=0.0, flipud=0.0, fliplr=0.5, mosaic=1.0, mixup=0.0, copy_paste=0.0
Weights & Biases: run 'pip install wandb' to automatically track and visualize YOLOv5 ? runs (RECOMMENDED)
TensorBoard: Start with 'tensorboard --logdir parking_lot_pyimagesearch', view at http://localhost:6006/
Downloading https://ultralytics.com/assets/Arial.ttf to /root/.config/Ultralytics/Arial.ttf...
100% 755k/755k [00:00<00:00, 18.0MB/s]
YOLOv5 temporarily requires wandb version 0.12.10 or below. Some features may not work as expected.
Downloading https://github.com/ultralytics/yolov5/releases/download/v6.1/yolov5s.pt to yolov5s.pt...
100% 14.1M/14.1M [00:00<00:00, 125MB/s]

Overriding model.yaml nc=80 with nc=5
Logging results to parking_lot_pyimagesearch/yolov5s_size640_epochs20_batch32_simple
Starting training for 20 epochs...

     Epoch   gpu_mem       box       obj       cls    labels  img_size
      0/19     7.36G   0.09176   0.03736   0.04355        31       640: 100% 28/28 [00:28<00:00,  1.03s/it]
               Class     Images     Labels          P          R     mAP@.5 mAP@.5:.95: 100% 4/4 [00:03<00:00,  1.04it/s]
                 all        250        454      0.352      0.293      0.185      0.089

     Epoch   gpu_mem       box       obj       cls    labels  img_size
      1/19     8.98G   0.06672   0.02769   0.03154        45       640: 100% 28/28 [00:25<00:00,  1.09it/s]
               Class     Images     Labels          P          R     mAP@.5 mAP@.5:.95: 100% 4/4 [00:05<00:00,  1.45s/it]
                 all        250        454      0.271      0.347      0.237     0.0735

瞧啊。这样,您已经学会了在从 Roboflow 下载的自定义数据集上训练对象检测器。是不是很神奇?

更令人兴奋的是,YOLOv5 将模型工件记录在runs目录中,我们将在下一节中看到这一点。

培训完成后,您将看到类似于下图的输出:

​​    Epoch   gpu_mem       box       obj       cls    labels  img_size
     19/19     7.16G   0.02747   0.01736  0.004772        46       640: 100% 28/28 [01:03<00:00,  2.27s/it]
               Class     Images     Labels          P          R     mAP@.5 mAP@.5:.95: 100% 4/4 [00:05<00:00,  1.42s/it]
                 all        250        454      0.713      0.574      0.606      0.416

20 epochs completed in 0.386 hours.
Optimizer stripped from runs/train/exp/weights/last.pt, 14.5MB
Optimizer stripped from runs/train/exp/weights/best.pt, 14.5MB

Validating runs/train/exp/weights/best.pt...
Fusing layers... 
Model summary: 213 layers, 7023610 parameters, 0 gradients, 15.8 GFLOPs
               Class     Images     Labels          P          R     mAP@.5 mAP@.5:.95: 100% 4/4 [00:08<00:00,  2.23s/it]
                 all        250        454      0.715      0.575      0.606      0.416
           Ambulance        250         64      0.813      0.814      0.853      0.679
                 Bus        250         46      0.771      0.652      0.664       0.44
                 Car        250        238      0.653      0.496      0.518      0.354
          Motorcycle        250         46      0.731      0.478      0.573      0.352
               Truck        250         60      0.608      0.433      0.425      0.256

以上结果表明,YOLOv5s 模型在所有类中都实现了[email protected] IoU 和[email protected]:0.95 IoU 的映射。它还指示类-明智的地图,并且该模型获得了救护车类的最佳分数(即0.853 [email protected] IoU)。该模型花了23.16分钟在特斯拉 T4 或特斯拉 K80 上完成了20时代的培训。


可视化模型构件

现在我们已经完成了模型的训练,让我们看看在yolov5/vehicles_open_pyimagesearch_model目录中生成的结果。

默认情况下,所有训练结果都记录到yolov5/runs/train中,并为每次运行创建一个新的递增目录,如runs/train/expruns/train/exp1等。然而,在训练模型时,我们通过了PROJECTRUN_NAME,所以在这种情况下,它不会创建默认目录来记录训练结果。因此,在这个实验中runsparking_lot_pyimagesearch

接下来,让我们看看实验中创建的文件。

$tree parking_lot_pyimagesearch/yolov5s_size640_epochs20_batch32_small/
parking_lot_pyimagesearch/yolov5s_size640_epochs20_batch32_small/
├── confusion_matrix.png
├── events.out.tfevents.1652810418.f70b01be1223.864.0
├── F1_curve.png
├── hyp.yaml
├── labels_correlogram.jpg
├── labels.jpg
├── opt.yaml
├── P_curve.png
├── PR_curve.png
├── R_curve.png
├── results.csv
├── results.png
├── train_batch0.jpg
├── train_batch1.jpg
├── train_batch2.jpg
├── val_batch0_labels.jpg
├── val_batch0_pred.jpg
├── val_batch1_labels.jpg
├── val_batch1_pred.jpg
├── val_batch2_labels.jpg
├── val_batch2_pred.jpg
└── weights
    ├── best.pt
    └── last.pt

1 directory, 23 files

线 1 上,我们使用tree命令,后跟PROJECTRUN_NAME,显示训练对象检测器的各种评估指标和权重文件。正如我们所看到的,它有一个精度曲线、召回曲线、精度-召回曲线、混淆矩阵、验证图像预测,以及 PyTorch 格式的权重文件。

现在让我们看一些来自runs目录的图片。

图 8 显示了具有镶嵌数据增强的训练图像批次。有 16 个图像聚集在一起;如果我们从第 3 行第 1 列中选择一个图像,那么我们可以看到该图像是四个不同图像的组合。我们在 YOLOv4 的文章中解释了镶嵌数据增强的概念,所以如果你还没有的话,一定要去看看。

接下来,我们看一下results.png,它包括边界框、对象和分类的训练和验证损失。它还具有用于训练的度量:精确度、召回率、[email protected][email protected]:0.95(图 9 )。

图 10 显示了 Vehicles-OpenImages 数据集上的地面实况图像和 YOLOv5s 模型预测。从下面的两张图片来看,很明显这个模型在检测物体方面做得很好。不幸的是,该模型在第二幅图像中没有检测到自行车,在第六幅图像中没有检测到汽车。在第一张图片中,把一辆卡车误归类为一辆汽车,但这是一个很难破解的问题,因为人类甚至很难正确预测它。但总的来说,它在这些图像上做得很好。

  • 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
  • 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Figure 10: Ground-truth images (top) and YOLOv5s model prediction (bottom) fine-tuned with all layers.


冻结初始图层,微调剩余图层

我们学习了如何训练在 MS COCO 数据集上预先训练的对象检测器,这意味着我们微调了 Vehicles-OpenImages 数据集上的模型参数(所有层)。但问题是,我们需要在新的数据集上训练所有的模型层吗?也许不会,因为预训练模型已经在一个大型的、精确的 MS COCO 数据集上进行了训练。

在自定义数据集上经常微调模型时,冻结图层的好处是可以减少训练时间。如果自定义数据集不太复杂,那么即使不相同,也可以达到相当的精度。当我们比较这两个模型的训练时间时,你会自己看到。

在本节中,我们将再次在 Vehicles-OpenImages 数据集上训练或微调 YOLOv5s 模型,但冻结网络的初始11层,这与之前我们微调所有检测器层不同。感谢 YOLOv5 的创造者,冻结模型层非常容易。但是,首先,我们必须通过--freeze参数传递我们想要在模型中冻结的层数。

现在让我们通过执行train.py脚本来训练模型。首先我们把--name,也就是运行名改成freeze_layers,传递--freeze参数,其他参数都一样。

!python train.py --img {
    
    SIZE}\
               --batch {
    
    BATCH_SIZE}\
               --epochs {
    
    EPOCHS}\
               --data ../vehicles_open_image/data.yaml\
               --weights {
    
    MODEL}.pt\
               --workers {
    
    WORKERS}\
               --project {
    
    PROJECT}\
               --name freeze_layers\
               --exist-ok\
               --freeze 0 1 2 3 4 5 6 7 8 9 10

运行培训脚本时会产生以下输出;如您所见,网络的前 11 层显示有一个freezing前缀,这意味着这些层的参数(权重和偏差)将保持不变。同时,剩余的 15 个图层将在自定义数据集上进行微调。

话虽如此,现在还是让我们来看看结果吧!

freezing model.0.conv.weight
freezing model.0.bn.weight
freezing model.0.bn.bias
freezing model.1.conv.weight
freezing model.1.bn.weight
freezing model.1.bn.bias
.
.
.
.
freezing model.10.conv.weight
freezing model.10.bn.weight
freezing model.10.bn.bias

我们可以从下面的输出中观察到,20个时期仅用了0.158个小时(即9.48分钟)就完成了,而我们在没有层冻结的情况下进行微调的模型用了23.16分钟。哇!这将时间缩短了近 2.5 倍。

但是等等,让我们来看看这个模型的映射,它是为所有类和类级显示的。

在冻结 11 层的情况下,模型实现了0.551 [email protected] IoU 和0.336 [email protected]:0.95 IoU。这两个模型的精确度之间肯定有差异,但不太显著。

20 epochs completed in 0.158 hours.
Optimizer stripped from parking_lot_pyimagesearch/freeze_layers/weights/last.pt, 14.5MB
Optimizer stripped from parking_lot_pyimagesearch/freeze_layers/weights/best.pt, 14.5MB

Validating parking_lot_pyimagesearch/freeze_layers/weights/best.pt...
Fusing layers... 
Model summary: 213 layers, 7023610 parameters, 0 gradients, 15.8 GFLOPs
               Class     Images     Labels          P          R     mAP@.5 mAP@.5:.95: 100% 4/4 [00:04<00:00,  1.25s/it]
                 all        250        454      0.579      0.585      0.551      0.336
           Ambulance        250         64      0.587      0.688      0.629      0.476
                 Bus        250         46      0.527      0.696      0.553      0.304
                 Car        250        238      0.522      0.462      0.452      0.275
          Motorcycle        250         46      0.733      0.478      0.587      0.311
               Truck        250         60      0.527        0.6      0.533      0.313

最后,在图 11 中,我们可以在验证图像上看到探测器预测。结果清楚地表明,它们不如用所有层训练的检测器好。例如,它错过了第一幅图像中的对象,在第二幅图像中,它将一个motorcycle误归类为一个car,在第四幅图像中未能检测到car,甚至在第六幅图像中,它只看到了三个cars。也许多训练一点时间或者少冻结几层可以提高探测器的性能。



汇总

恭喜你走到这一步!让我们快速总结一下我们在本教程中学到的内容。

  1. 我们首先简要介绍了 YOLOv5,并讨论了 YOLOv5 变体的性能和速度基准。
  2. 然后我们讨论了两个数据集:Vehicles-OpenImages 数据集和 Udacity 自动驾驶汽车数据集。除此之外,我们还介绍了 YOLOv5 基本事实注释格式。
  3. 在最终确定 YOLOv5 模型变体用于训练之后,我们进入了本教程的实践部分,在这里我们讨论了一些方面,比如下载数据集、为给定数据创建configuration.yaml,以及训练和可视化 YOLOv5 模型工件。
  4. 最后,我们第二次训练 YOLOv5 模型,但是模型的最初 11 层被冻结,并且将结果与完全训练的 YOLOv5 模型进行比较。

引用信息

Sharma,A. “在自定义数据集上训练 YOLOv5 对象检测器”, PyImageSearch ,D. Chakraborty,P. Chugh,A. R. Gosthipaty,S. Huot,K. Kidriavsteva,R. Raha 和 A. Thanki 编辑。,2022 年,【https://pyimg.co/fq0a3

@incollection{
    
    Sharma_2022_Custom_Dataset,
  author = {
    
    Aditya Sharma},
  title = {
    
    Training the {
    
    YOLOv5} Object Detector on a Custom Dataset},
  booktitle = {
    
    PyImageSearch},
  editor = {
    
    Devjyoti Chakraborty and Puneet Chugh and Aritra Roy Gosthipaty and Susan Huot and Kseniia Kidriavsteva and Ritwik Raha and Abhishek Thanki},
  year = {
    
    2022},
  note = {
    
    https://pyimg.co/fq0a3},
}

要下载这篇文章的源代码(并在未来教程在 PyImageSearch 上发布时得到通知),只需在下面的表格中输入您的电子邮件地址!***

利用 Keras 和深度学习进行迁移学习

原文:https://pyimagesearch.com/2019/05/20/transfer-learning-with-keras-and-deep-learning/

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在本教程中,您将学习如何使用 Keras、深度学习和 Python 在您自己的自定义数据集上执行迁移学习。

想象一下:

你刚刚被 Yelp 雇佣到他们的计算机视觉部门工作。

Yelp 刚刚在其网站上推出了一项新功能,允许评论者为他们的食物/菜肴拍照,然后将它们与餐厅菜单上的特定项目相关联。

这是一个很好的功能…

…但是他们收到了很多垃圾图片。

某些邪恶的用户没有给他们的菜肴拍照…相反,他们在给…(嗯,你大概可以猜到)拍照。

你的任务?

弄清楚如何创建一个自动化的计算机视觉应用程序,可以区分“食物”和“非食物”,从而允许 Yelp 继续推出他们的新功能,并为他们的用户提供价值。

那么,您将如何构建这样一个应用程序呢?

答案在于通过深度学习进行迁移学习。

今天标志着使用 Keras 进行 迁移学习的一套全新教程的开始。迁移学习是这样的过程:

  1. 在数据集上对网络进行预训练
  2. 并且利用它来识别它没有被训练过的图像/对象类别

从本质上讲,我们可以利用最先进的网络在具有挑战性的数据集(如 ImageNet 或 COCO)上学习到的强大、有鉴别能力的过滤器,然后应用这些网络来识别模型从未训练过的对象。

一般来说,在深度学习的背景下,有两种类型的迁移学习:

  1. 通过特征提取转移学习
  2. 通过微调转移学习

在执行特征提取时,我们将预训练的网络视为一个任意的特征提取器,允许输入图像向前传播,在预先指定的层停止,并将该层的输出作为我们的特征。

*另一方面,*微调要求我们通过移除之前的全连接层头,提供新的、刚初始化的层头,然后训练新的 FC 层来预测我们的输入类,从而更新模型架构本身。

我们将在 PyImageSearch 博客上讨论本系列的两种技术,但今天我们将重点讨论特征提取

要了解如何使用 Keras 通过特征提取进行迁移学习,继续阅读!

利用 Keras 和深度学习进行迁移学习

***2020-05-13 更新:*此博文现已兼容 TensorFlow 2+!

***注意:**我将在本系列教程中介绍的许多迁移学习概念也出现在我的书《用 Python 进行计算机视觉的深度学习》中。**在这本书里,我进行了更详细的讨论(包括更多我的技巧、建议和最佳实践)。*如果你想在阅读完本指南后了解更多关于迁移学习的细节,请务必看看我的书

在本教程的第一部分,我们将回顾迁移学习的两种方法:特征提取和微调。

然后,我将详细讨论如何通过特征提取进行迁移学习(本教程的主要焦点)。

从那里,我们将回顾 Food-5k 数据集,该数据集包含分为两类的 5000 张图像:“食物”和“非食物”。

在本教程中,我们将通过特征提取利用迁移学习来识别这两个类。

一旦我们很好地处理了数据集,我们将开始编码。

我们将回顾许多 Python 文件,每个文件完成一个特定的步骤,包括:

  1. 创建配置文件。
  2. 构建我们的数据集(即,将图像放入正确的目录结构中)。
  3. 使用 Keras 和预训练的 CNN 从我们的输入图像中提取特征。
  4. 在提取的特征之上训练逻辑回归模型。

我们今天将在这里复习的部分代码也将在迁移学习系列的剩余部分中使用——如果你打算跟随教程,现在花点时间来确保你理解代码

两种迁移学习:特征提取微调

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Figure 1: Via “transfer learning”, we can utilize a pre-existing model such as one trained to classify dogs vs. cats. Using that pre-trained model we can break open the CNN and then apply “transfer learning” to another, completely different dataset (such as bears). We’ll learn how to apply transfer learning with Keras and deep learning in the rest of this blog post.

***注:*以下章节改编自我的书,用 Python 进行计算机视觉的深度学习。关于迁移学习的全套章节,请参考课文。

考虑一个传统的机器学习场景,其中我们面临两个分类挑战。

在第一个挑战中,我们的目标是训练一个卷积神经网络来识别图像中的狗和猫。

然后,在第二个项目中,我们的任务是识别三种不同的熊:灰熊北极熊大熊猫

使用机器学习/深度学习中的标准实践,我们可以将这些挑战视为两个独立的问题:

  • 首先,我们将收集足够的狗和猫的标记数据集,然后在数据集上训练模型
  • 然后,我们将重复这个过程第二次,只是这一次,收集我们的熊品种的图像,然后在标记的数据集上训练一个模型。

迁移学习提出了一种不同的范式如果我们可以利用现有的预训练分类器作为新分类、对象检测或实例分割任务的起点,会怎么样?

在上述挑战的背景下使用迁移学习,我们将:

  • 首先训练一个卷积神经网络来识别狗和猫
  • 然后,使用对狗和猫数据训练的相同的 CNN,并使用它来区分熊类,即使在初始训练期间没有熊数据与狗和猫数据混合

这听起来好得令人难以置信吗?

其实不是。

在 ImageNet 和 COCO 等大规模数据集上训练的深度神经网络已经被证明在迁移学习的任务上优秀

这些网络学习一组丰富的、有辨别能力的特征,能够识别 100 到 1000 个对象类别——只有这些过滤器可以重复用于 CNN 最初训练以外的任务才有意义。

一般来说,当应用于计算机视觉的深度学习时,有两种类型的迁移学习:

  1. 将网络视为任意特征提取器。
  2. 移除现有网络的全连接层,在 CNN 上放置一组新的 FC 层,然后微调这些权重(以及可选的先前层)以识别新的对象类。

在这篇博文中,我们将主要关注迁移学习的第一种方法,将网络视为特征提取器。

我们将在本系列关于深度学习的迁移学习的后面讨论微调网络。

通过特征提取进行迁移学习

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Figure 2: Left: The original VGG16 network architecture that outputs probabilities for each of the 1,000 ImageNet class labels. Right: Removing the FC layers from VGG16 and instead of returning the final POOL layer. This output will serve as our extracted features.

***注:*以下章节改编自我的书,用 Python 进行计算机视觉的深度学习。有关特征提取的完整章节,请参考正文。

通常,您会将卷积神经网络视为端到端图像分类器:

  1. 我们将图像输入网络。
  2. 图像通过网络向前传播。
  3. 我们在网络的末端获得最终的分类概率。

然而,没有“规则”说我们必须允许图像通过整个 T2 网络向前传播。

相反,我们可以:

  1. 在任意但预先指定的层(如激活层或池层)停止传播。
  2. 从指定图层中提取值。
  3. 将这些值视为特征向量。

例如,让我们考虑本节顶部的图 2 ( )中 Simonyan 和 Zisserman 的 VGG16 网络。

除了网络中的层,我还包括了每个层的输入和输出体积形状。

当将网络视为特征提取器时,我们本质上是在预先指定的层处“切断”网络(通常在完全连接的层之前,但这实际上取决于您的特定数据集)。

如果我们在 VGG16 的全连接层之前停止传播,网络中的最后一层将成为最大池层(图 2),其输出形状将为 7 x 7 x 512 。将该体积展平成特征向量,我们将获得一列 7 x 7 x 512 = 25,088 值— 该数字列表用作我们的特征向量,用于量化输入图像。

然后,我们可以对整个图像数据集重复这个过程。

给定我们网络中总共的 N 幅图像,我们的数据集现在将被表示为一列 N 个向量,每个向量有 25,088 维。

一旦我们有了我们的特征向量,我们就可以在这些特征的基础上训练现成的机器学习模型,如线性 SVM、逻辑回归、决策树或随机森林,以获得可以识别新类别图像的分类器。

也就是说,通过特征提取进行迁移学习的两种最常见的机器学习模型是:

  1. 逻辑回归
  2. 线性 SVM

为什么是那两种型号?

首先,记住我们的特征提取器是 CNN。

CNN 是能够学习非线性特征的非线性模型——我们假设 CNN 所学习的特征已经健壮且有辨别能力。

第二个原因,也可能是更重要的原因,是我们的特征向量往往非常大,而且维数很高。

因此,我们需要一个可以在特征之上训练的快速模型— 线性模型往往训练起来非常快。

例如,我们的 5000 幅图像的数据集,每幅图像由 25088 维的特征向量表示,可以在几秒钟内使用逻辑回归模型进行训练。

作为这一部分的总结,我希望你记住,CNN 本身而不是能够识别这些新的阶级。

相反,我们使用 CNN 作为中间特征提取器。

下游机器学习分类器将负责学习从 CNN 提取的特征的潜在模式。

Foods-5K 数据集

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Figure 3: We will apply transfer learning to the Foods-5K dataset using Python, Keras, and Deep Learning.

我们今天将在这里使用的数据集是由瑞士联邦理工学院的多媒体信号处理小组(MSPG) 策划的 Food-5K 数据集

顾名思义,该数据集由 5000 幅图像组成,分为两类:

  1. 食物
  2. 非食品

我们的目标是训练一个分类器,这样我们就可以区分这两个类。

MSPG 为我们提供了拆分前培训、验证和测试拆分。我们将在本指南中通过抽取进行迁移学习,并在其余的特征抽取教程中使用这些拆分。

下载 Food-5K 数据集

由于 不可靠 Food-5K 数据集下载方法最初发布在以下带密码的 FTP 站点:

…我现在已经将数据集直接包含在这里:

下载 Food-5K 数据集

项目结构

在继续之前,请继续:

  1. 找到这篇博文的 【下载】 部分,抓取代码。
  2. 使用上面的链接下载 Food-5K 数据集

然后你会有两个。压缩文件。首先,提取transfer-learning-keras.zip。在里面,您会发现一个名为Food-5K/的空文件夹。第二,把Food-5K.zip文件放在那个文件夹里,然后解压。

完成这些步骤后,您将看到以下目录结构:

$ tree --dirsfirst --filelimit 10
.
├── Food-5K
│   ├── evaluation [1000 entries]
│   ├── training [3000 entries]
│   ├── validation [1000 entries]
│   └── Food-5K.zip
├── dataset
├── output
├── pyimagesearch
│   ├── __init__.py
│   └── config.py
├── build_dataset.py
├── extract_features.py
└── train.py

7 directories, 6 files

如你所见,Food-5K/包含了evaluation/training/validation/子目录(这些将出现在你提取Food-5K.zip的 之后的*)。每个子目录包含 1000 个.jpg图像文件。*

我们的dataset/目录虽然现在是空的,但很快就会以更有组织的形式包含 Food-5K 图像(将在章节*“为特征提取构建数据集”*中讨论)。

成功执行今天的 Python 脚本后,output/目录将存放我们提取的特征(存储在三个独立的.csv文件中)以及我们的标签编码器和模型(两者都是.cpickle格式)。这些文件有意不包含在中。zip 您必须按照本教程来创建它们。

我们的 Python 脚本包括:

  • 我们的定制配置文件将帮助我们管理数据集、类名和路径。它是直接用 Python 编写的,所以我们可以使用os.path直接在脚本中构建特定于操作系统的格式化文件路径。
  • 使用这个配置,这个脚本将在磁盘上创建一个有组织的数据集,使得从其中提取特征变得容易。
  • extract_features.py:迁移学习魔法从这里开始。这个 Python 脚本将使用预先训练的 CNN 来提取原始特征,并将结果存储在一个.csv文件中。标签编码器.cpickle文件也将通过该脚本输出。
  • 我们的训练脚本将在先前计算的特征之上训练一个逻辑回归模型。我们将评估生成的模型并将其保存为.cpickle

config.pybuild_dataset.py脚本将在迁移学习系列的剩余部分重复使用,所以请务必密切关注它们!

我们的配置文件

让我们从查看配置文件开始。

打开pyimagesearch子模块中的config.py,插入以下代码:

# import the necessary packages
import os

# initialize the path to the *original* input directory of images
ORIG_INPUT_DATASET = "Food-5K"

# initialize the base path to the *new* directory that will contain
# our images after computing the training and testing split
BASE_PATH = "dataset"

我们从单个导入开始。我们将在这个配置中使用os模块( Line 2 )来正确地连接路径。

ORIG_INPUT_DATASET是到原始输入数据集的路径(即,您下载和解压缩 Food-5K 数据集的位置)。

下一个路径BASE_PATH,将是我们的数据集被组织的地方(执行build_dataset.py的结果)。

注意:目录结构对于这篇文章来说不是特别有用,但是一旦我们开始微调,它放在系列文章的后面。同样,我认为以这种方式组织数据集是“最佳实践”,原因您将在本系列文章中看到。

让我们指定更多数据集配置以及我们的类标签和批量大小:

# define the names of the training, testing, and validation
# directories
TRAIN = "training"
TEST = "evaluation"
VAL = "validation"

# initialize the list of class label names
CLASSES = ["non_food", "food"]

# set the batch size
BATCH_SIZE = 32

输出训练、评估和验证目录的路径在第 13-15 行中指定。

在第 18 行的上以列表形式指定了CLASSES。如前所述,我们将使用"food""non_food"图像。

在提取特征时,我们将把数据分成称为批次的小块。BATCH_SIZE指定在线 21 上。

最后,我们可以建立其余的路径:

# initialize the label encoder file path and the output directory to
# where the extracted features (in CSV file format) will be stored
LE_PATH = os.path.sep.join(["output", "le.cpickle"])
BASE_CSV_PATH = "output"

# set the path to the serialized model after training
MODEL_PATH = os.path.sep.join(["output", "model.cpickle"])

我们的标签编码器路径连接在第 25 行的上,其中连接路径的结果是 Linux/Mac 上的output/le.cpickle或 Windows 上的output\le.cpickle

提取的特征将存在于BASE_CSV_PATH中指定路径的 CSV 文件中。

最后,我们在MODEL_PATH中组装导出模型文件的路径。

构建用于特征提取的数据集

在我们从输入图像集中提取特征之前,让我们花点时间在磁盘上组织我们的图像。

我更喜欢将磁盘上的数据集组织成以下格式:

dataset_name/class_label/example_of_class_label.jpg

维护此目录结构:

  • 不仅在磁盘上组织我们的数据集…
  • …但是使我们能够利用 Keras 的flow_from_directory函数,当我们在本系列教程的后面进行微调时。

由于 Food-5K 数据集还提供了*预先提供的数据分割,*我们最终的目录结构将具有以下形式:

dataset_name/split_name/class_label/example_of_class_label.jpg

现在让我们继续构建我们的数据集+目录结构。

打开build_dataset.py文件并插入以下代码:

# import the necessary packages
from pyimagesearch import config
from imutils import paths
import shutil
import os

# loop over the data splits
for split in (config.TRAIN, config.TEST, config.VAL):
	# grab all image paths in the current split
	print("[INFO] processing '{} split'...".format(split))
	p = os.path.sep.join([config.ORIG_INPUT_DATASET, split])
	imagePaths = list(paths.list_images(p))

我们的包装是在2-5 线进口的。我们将在整个脚本中使用我们的config ( Line 2 )来调用我们的设置。其他三个导入——pathsshutilos——将允许我们遍历目录、创建文件夹和复制文件。

在第 8 行,我们开始循环我们的培训、测试和验证部分。

第 11 行和第 12 行创建了一个所有imagePaths在 split 中的列表。

从这里开始,我们将继续遍历imagePaths:

	# loop over the image paths
	for imagePath in imagePaths:
		# extract class label from the filename
		filename = imagePath.split(os.path.sep)[-1]
		label = config.CLASSES[int(filename.split("_")[0])]

		# construct the path to the output directory
		dirPath = os.path.sep.join([config.BASE_PATH, split, label])

		# if the output directory does not exist, create it
		if not os.path.exists(dirPath):
			os.makedirs(dirPath)

		# construct the path to the output image file and copy it
		p = os.path.sep.join([dirPath, filename])
		shutil.copy2(imagePath, p)

对于分割中的每个imagePath,我们继续:

  • 从文件名中提取类label(第 17 行和第 18 行)。
  • 根据BASE_PATHsplitlabel ( 第 21 行)构建输出目录的路径。
  • 通过线 24 和 25 创建dirPath(如有必要)。
  • 将图像复制到目标路径(第 28 行和第 29 行)。

既然build_dataset.py已经被编码,使用教程的 “下载” 部分下载源代码的档案。

然后,您可以使用以下命令执行build_dataset.py:

$ python build_dataset.py
[INFO] processing 'training split'...
[INFO] processing 'evaluation split'...
[INFO] processing 'validation split'...

在这里,您可以看到我们的脚本成功执行。

要验证磁盘上的目录结构,请使用ls命令:

$ ls dataset/
evaluation  training  validation

在数据集目录中,我们有培训、评估和验证部分。

在每个目录中,我们都有类别标签目录:

$ ls dataset/training/
food  non_food

使用 Keras 和预训练的 CNN 从我们的数据集中提取特征

让我们转到迁移学习的实际特征提取部分。

使用预先训练的 CNN 进行特征提取的所有代码将存在于extract_features.py中—打开该文件并插入以下代码:

# import the necessary packages
from sklearn.preprocessing import LabelEncoder
from tensorflow.keras.applications import VGG16
from tensorflow.keras.applications.vgg16 import preprocess_input
from tensorflow.keras.preprocessing.image import img_to_array
from tensorflow.keras.preprocessing.image import load_img
from pyimagesearch import config
from imutils import paths
import numpy as np
import pickle
import random
import os

# load the VGG16 network and initialize the label encoder
print("[INFO] loading network...")
model = VGG16(weights="imagenet", include_top=False)
le = None

第 2-12 行,提取特征所需的所有包都被导入。最值得注意的是这包括VGG16

VGG16 是我们用来进行迁移学习的卷积神经网络(CNN)(第 3 行)。

在第 16 行的上,我们加载model,同时指定两个参数:

  • weights="imagenet":加载预训练的 ImageNet 权重用于迁移学习。
  • include_top=False:我们不包括 softmax 分类器的全连接头部。换句话说,我们砍掉了网络的头。

随着重量的拨入和无头部模型的加载,我们现在准备好进行迁移学习了。我们将直接使用网络*的输出值,*将结果存储为特征向量。

最后,我们的标签编码器在行 17 被初始化。

让我们循环一下我们的数据分割:

# loop over the data splits
for split in (config.TRAIN, config.TEST, config.VAL):
	# grab all image paths in the current split
	print("[INFO] processing '{} split'...".format(split))
	p = os.path.sep.join([config.BASE_PATH, split])
	imagePaths = list(paths.list_images(p))

	# randomly shuffle the image paths and then extract the class
	# labels from the file paths
	random.shuffle(imagePaths)
	labels = [p.split(os.path.sep)[-2] for p in imagePaths]

	# if the label encoder is None, create it
	if le is None:
		le = LabelEncoder()
		le.fit(labels)

	# open the output CSV file for writing
	csvPath = os.path.sep.join([config.BASE_CSV_PATH,
		"{}.csv".format(split)])
	csv = open(csvPath, "w")

每个split(训练、测试和验证)的循环从第 20 行开始。

首先,我们为split ( 第 23 行和第 24 行)抓取所有的imagePaths

路径通过行 28 被随机打乱,从那里,我们的类labels被从路径本身中提取出来(行 29 )。

如果有必要,我们的标签编码器被实例化和安装(第 32-34 行),确保我们可以将字符串类标签转换为整数。

接下来,我们构建输出 CSV 文件的路径(第 37-39 行)。我们将有三个 CSV 文件—每个数据分割一个。每个 CSV 将有 N 个行数——数据分割中的每个图像一行。

下一步是循环遍历BATCH_SIZE块中的imagePaths:

	# loop over the images in batches
	for (b, i) in enumerate(range(0, len(imagePaths), config.BATCH_SIZE)):
		# extract the batch of images and labels, then initialize the
		# list of actual images that will be passed through the network
		# for feature extraction
		print("[INFO] processing batch {}/{}".format(b + 1,
			int(np.ceil(len(imagePaths) / float(config.BATCH_SIZE)))))
		batchPaths = imagePaths[i:i + config.BATCH_SIZE]
		batchLabels = le.transform(labels[i:i + config.BATCH_SIZE])
		batchImages = []

为了创建我们的批处理imagePaths,我们使用 Python 的range函数。该函数接受三个参数:startstopstep。你可以在这篇详解中读到更多关于range的内容。

我们的批处理将遍历整个列表imagePathsstep是我们的批量大小(32,除非您在配置设置中调整它)。

行 48 和 49 上,使用数组切片提取当前一批图像路径和标签。然后我们的batchImages列表在第 50 行被初始化。

现在让我们继续填充我们的batchImages:

		# loop over the images and labels in the current batch
		for imagePath in batchPaths:
			# load the input image using the Keras helper utility
			# while ensuring the image is resized to 224x224 pixels
			image = load_img(imagePath, target_size=(224, 224))
			image = img_to_array(image)

			# preprocess the image by (1) expanding the dimensions and
			# (2) subtracting the mean RGB pixel intensity from the
			# ImageNet dataset
			image = np.expand_dims(image, axis=0)
			image = preprocess_input(image)

			# add the image to the batch
			batchImages.append(image)

循环遍历batchPaths ( 第 53 行,我们将加载每个image,对其进行预处理,并将其聚集到batchImages

image本身装载在线 56 上。

预处理包括:

  • 通过行 56 上的target_size参数调整到 224×224 像素。
  • 转换成数组格式(第 57 行)。
  • 添加批次尺寸(行 62 )。
  • 均值减法(第 63 行)。

如果这些预处理步骤出现外来,请参考用 Python 进行计算机视觉的深度学习

最后,通过线 66image添加到批次中。

现在,我们将通过我们的网络将该批图像传递给以提取特征:

		# pass the images through the network and use the outputs as
		# our actual features, then reshape the features into a
		# flattened volume
		batchImages = np.vstack(batchImages)
		features = model.predict(batchImages, batch_size=config.BATCH_SIZE)
		features = features.reshape((features.shape[0], 7 * 7 * 512))

我们的一批图像经由线 71 和 72 通过网络发送。

请记住,我们已经删除了网络的全连接层头。相反,正向传播在最大池层停止。我们将把 max-pooling 层的输出视为一个列表features,也称为“特征向量”。

最大池层的输出尺寸为 (batch_size,7 x 7 x 512) 。因此,我们可以将features转换成一个形状为(batch_size, 7 * 7 * 512)的 NumPy 数组,将 CNN 的输出视为一个特征向量。

让我们总结一下这个脚本:

		# loop over the class labels and extracted features
		for (label, vec) in zip(batchLabels, features):
			# construct a row that exists of the class label and
			# extracted features
			vec = ",".join([str(v) for v in vec])
			csv.write("{},{}\n".format(label, vec))

	# close the CSV file
	csv.close()

# serialize the label encoder to disk
f = open(config.LE_PATH, "wb")
f.write(pickle.dumps(le))
f.close()

为了保持我们的批处理效率,features和相关的类标签被写入我们的 CSV 文件(第 76-80 行)。

在 CSV 文件中,类label是每一行中的第一个字段(使我们能够在训练期间轻松地从该行中提取它)。特征vec如下。

每个 CSV 文件将通过行 83 关闭。回想一下,在完成后,我们将为每个数据分割创建一个 CSV 文件。

最后,我们可以将标签编码器转储到磁盘中(第 86-88 行)。


让我们使用在 ImageNet 上预先训练的 VGG16 网络从数据集中提取特征。

使用本教程的 “下载” 部分下载源代码,并从那里执行以下命令:

$ python extract_features.py
[INFO] loading network...
[INFO] processing 'training split'...
...
[INFO] processing batch 92/94
[INFO] processing batch 93/94
[INFO] processing batch 94/94
[INFO] processing 'evaluation split'...
...
[INFO] processing batch 30/32
[INFO] processing batch 31/32
[INFO] processing batch 32/32
[INFO] processing 'validation split'...
...
[INFO] processing batch 30/32
[INFO] processing batch 31/32
[INFO] processing batch 32/32

在 NVIDIA K80 GPU 上,从 Food-5K 数据集中的 5000 张图像中提取特征需要 2m55s。

你可以用 CPU 来代替,但是它会花费更多的时间。

实施我们的培训脚本

通过特征提取进行迁移学习的最后一步是实现一个 Python 脚本,该脚本将从 CNN 中提取特征,然后在这些特征的基础上训练一个逻辑回归模型。

**再次提醒,请记住我们的 CNN 没有预测任何事情!相反,CNN 被视为一个任意的特征提取器。

我们向网络输入一幅图像,它被正向传播,然后我们从最大池层提取层输出——这些输出作为我们的特征向量。

要了解我们如何在这些特征向量上训练一个模型,打开train.py文件,让我们开始工作:

# import the necessary packages
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report
from pyimagesearch import config
import numpy as np
import pickle
import os

def load_data_split(splitPath):
	# initialize the data and labels
	data = []
	labels = []

	# loop over the rows in the data split file
	for row in open(splitPath):
		# extract the class label and features from the row
		row = row.strip().split(",")
		label = row[0]
		features = np.array(row[1:], dtype="float")

		# update the data and label lists
		data.append(features)
		labels.append(label)

	# convert the data and labels to NumPy arrays
	data = np.array(data)
	labels = np.array(labels)

	# return a tuple of the data and labels
	return (data, labels)

2-7 行,我们导入我们需要的包。值得注意的是,我们将使用LogisticRegression作为我们的机器学习分类器。与提取特征相比,我们的训练脚本需要更少的导入。这部分是因为训练脚本本身实际上更简单。

让我们在第 9 行上定义一个名为load_data_split的函数。该函数负责加载给定数据分割 CSV 文件路径的所有数据和标签(参数splitPath)。

在函数内部,我们首先初始化我们的datalabels列表(第 11 行和第 12 行)。

从那里,我们打开 CSV,并从第 15 行的开始循环所有行。在循环中,我们:

  • row中所有逗号分隔的值加载到一个列表中(第 17 行)。
  • 通过行 18 抓取类label(列表中的第一个值)。
  • 提取该行中的所有features(行 19 )。这些都是列表中的值,除了类标签。结果就是我们的特征向量
  • 从那里,我们将特征向量和label分别添加到datalabels列表中(第 22 和 23 行)。

最后,datalabels返回到调用函数(第 30 行)。

随着load_data_spit函数准备就绪,让我们通过加载我们的数据来让它工作:

# derive the paths to the training and testing CSV files
trainingPath = os.path.sep.join([config.BASE_CSV_PATH,
	"{}.csv".format(config.TRAIN)])
testingPath = os.path.sep.join([config.BASE_CSV_PATH,
	"{}.csv".format(config.TEST)])

# load the data from disk
print("[INFO] loading data...")
(trainX, trainY) = load_data_split(trainingPath)
(testX, testY) = load_data_split(testingPath)

# load the label encoder from disk
le = pickle.loads(open(config.LE_PATH, "rb").read())

第 33-41 行从磁盘加载我们的训练和测试特征数据。我们使用前面代码块中的函数来处理加载过程。

第 44 行加载我们的标签编码器。

有了内存中的数据,我们现在准备好训练我们的机器学习分类器:

# train the model
print("[INFO] training model...")
model = LogisticRegression(solver="lbfgs", multi_class="auto",
	max_iter=150)
model.fit(trainX, trainY)

# evaluate the model
print("[INFO] evaluating...")
preds = model.predict(testX)
print(classification_report(testY, preds, target_names=le.classes_))

# serialize the model to disk
print("[INFO] saving model...")
f = open(config.MODEL_PATH, "wb")
f.write(pickle.dumps(model))
f.close()

48-50 线负责初始化和训练我们的逻辑回归 model

***注:*要详细了解逻辑回归和其他机器学习算法,一定要参考 PyImageSearch 大师,我的旗舰计算机视觉课程和社区。

线 54 和 55 便于在测试机上评估model并在终端打印分类统计。

最后,model以 Python 的 pickle 格式输出(第 59-61 行)。

我们的培训脚本到此结束!如您所知,在要素数据的基础上编写用于训练逻辑回归模型的代码非常简单。在下一部分中,我们将运行培训脚本。

如果你想知道我们如何处理如此多的特征数据,以至于它不能一次全部放入内存,请继续关注下周的教程。

注意:**本教程已经够长了,所以我还没有介绍如何调整逻辑回归模型的超参数,这是我肯定推荐的,以确保获得尽可能高的精度。如果您有兴趣了解更多关于迁移学习的知识,以及如何在特征提取过程中调整超参数,请务必参考使用 Python 进行计算机视觉的深度学习,其中我将更详细地介绍这些技术。

在提取的特征上训练模型

此时,我们已经准备好通过 Keras 的特征提取来执行迁移学习的最后一步。

让我们简要回顾一下到目前为止我们所做的工作:

  1. 下载了 Food-5K 数据集(分别属于“食物”和“非食物”两类的 5000 张图片)。
  2. 以一种更适合迁移学习的格式重构了数据集的原始目录结构(特别是微调,我们将在本系列的后面讨论)。
  3. 使用在 ImageNet 上预先训练的 VGG16 从图像中提取特征。

现在,我们将在这些提取特征的基础上训练一个逻辑回归模型。

请再次记住,VGG16 没有经过训练,无法识别“食物”和“非食物”类别。相反,它被训练识别 1000 个 ImageNet 类。

但是,通过利用:

  1. 用 VGG16 进行特征提取
  2. 并且在那些提取的特征之上应用逻辑回归分类器

我们将能够识别新的职业,尽管 VGG16 从未被训练来识别它们!

继续使用本教程的 【下载】 部分下载本指南的源代码。

从那里,打开一个终端并执行以下命令:

$ python train.py
[INFO] loading data...
[INFO] training model...
[INFO] evaluating...
              precision    recall  f1-score   support

        food       0.99      0.98      0.98       500
    non_food       0.98      0.99      0.99       500

    accuracy                           0.98      1000
   macro avg       0.99      0.98      0.98      1000
weighted avg       0.99      0.98      0.98      1000

[INFO] saving model...

在我的机器上的训练只花了 27 秒,正如你可以从我们的输出中看到的,我们在测试集上获得了 98-99%的准确率!

什么时候应该使用迁移学习和特征提取?

通过特征提取进行迁移学习通常是在你自己的项目中获得基线准确度的最简单的方法之一。

每当我面对一个新的深度学习项目时,我经常用 Keras 进行特征提取,只是为了看看会发生什么:

  • 在某些情况下,精确度是足够的。
  • 在其他情况下,它需要我根据我的逻辑回归模型调整超参数,或者尝试另一个预先训练好的 CNN。
  • 在其他情况下,我需要探索微调,甚至用定制的 CNN 架构从头开始训练。

无论如何,在最好的情况下通过特征提取的迁移学习给了我很好的准确性,项目可以完成。

在最糟糕的情况下,我将获得一个基线,在未来的实验中超越它。

摘要

今天标志着我们关于 Keras 和深度学习的迁移学习系列的开始。

通过深度学习进行特征提取的两种主要形式是:

  1. 特征抽出
  2. 微调

今天教程的重点是特征提取,将预先训练好的网络视为任意特征提取器的过程。

通过特征提取执行迁移学习的步骤包括:

  1. 从预先训练的网络开始(通常在诸如 ImageNet 或 COCO 的数据集上;大到足以让模型学习区分滤波器)。
  2. 允许输入图像向前传播到任意(预先指定的)层。
  3. 获取该层的输出,并将其作为 f 特征向量。
  4. 在提取特征的数据集上训练“标准”机器学习模型。

通过特征提取执行迁移学习的好处是,我们不需要训练(或重新训练)我们的神经网络。

相反,网络充当黑盒特征提取器。

那些被提取的特征被假设为本质上是非线性的(因为它们是从 CNN 中提取的),然后被传递到线性模型中用于分类。

如果你有兴趣学习更多关于迁移学习、特征提取和微调的知识,一定要参考我的书, 用 Python 进行计算机视觉的深度学习 ,我在那里更详细地讨论了这个主题。

我希望你喜欢今天的帖子!下周我们将讨论当我们的数据集太大而无法放入内存时如何进行特征提取,敬请关注。

要下载这篇文章的源代码(并在未来教程在 PyImageSearch 上发布时得到通知),只需在下面的表格中输入您的电子邮件地址!

猜你喜欢

转载自blog.csdn.net/wizardforcel/article/details/143583074