🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎
📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝
📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】 深度学习【DL】
🖍foreword
✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。
如果你对这个系列感兴趣的话,可以关注订阅哟👋
文章目录
在上一章中,我们了解到,随着训练数据集中可用图像数量的增加,模型的分类准确率不断提高,以至于包含 8000 张图像的训练数据集在验证数据集上的准确率高于包含 1,000 张图像的训练数据集。然而,为了训练模型,我们并不总是可以选择数百或数千张图像以及它们相应类别的基本事实。这就是迁移学习的用武之地。
迁移学习是一种将通用数据集上的模型学习迁移到感兴趣的特定数据集的技术。通常,用于执行迁移学习的预训练模型在数百万张图像上进行训练(这些图像是通用的,而不是我们感兴趣的数据集),并且这些预训练模型现在已经针对我们感兴趣的数据集进行了微调。
在本章中,我们将了解两种不同的迁移学习架构家族——VGG架构的变体和ResNet 架构的变体。
除了了解架构外,我们还将了解它们在两个不同的用例中的应用,即ge 和性别分类,我们将在其中学习如何同时优化交叉熵和平均绝对误差损失,以及面部关键点检测,我们将学习如何利用神经网络在单个预测中生成多个(136 个,而不是 1 个预测)连续输出。最后,我们将了解一个新的库,该库有助于在其余章节中显着降低代码复杂性。
总之,本章涵盖了以下主题:
- 介绍迁移学习
- 了解 VGG16 和 ResNet 架构
- 实现面部关键点检测
- 多任务学习:实现年龄估计和性别分类
- 介绍 torch_snippets 库
介绍迁移学习
迁移学习是一种利用从一项任务中获得的知识来解决另一项类似任务的技术。
想象一个模型,它在数百万张图像上训练,涵盖数千种对象(不仅仅是猫和狗)。模型的各种过滤器(内核)将为图像中的各种形状、颜色和纹理激活。现在可以重复使用这些过滤器来学习一组新图像的特征。学习特征后,它们可以在最终分类层之前连接到隐藏层,以自定义新数据。
ImageNet ( ImageNet ) 是一项旨在将大约 1400 万张图像分类为 1000 个不同类别的竞赛。它在数据集中有多种类,包括印度象、狮子鱼、硬盘、发胶和吉普车。
我们将在本章中介绍的深度神经网络架构已经在 ImageNet 数据集上进行了训练。此外,考虑到要在 ImageNet 中分类的对象的种类和数量,模型非常深,以便捕获尽可能多的信息。
让我们通过一个假设的场景来理解迁移学习的重要性:
考虑一种情况,我们正在处理道路图像,试图根据它们包含的对象对它们进行分类。从头开始构建模型可能会导致次优结果,因为图像的数量可能不足以学习数据集中的各种变化(正如我们在之前的用例中看到的那样,对 8,000 张图像进行训练会导致更高的在验证数据集上的准确度比在 2,000 张图像上的训练要好)。在 ImageNet 上训练的预训练模型在这种情况下会派上用场。在对大型 ImageNet 数据集进行训练期间,它已经了解了很多与交通相关的类别,例如汽车、道路、树木和人类。因此,利用已经训练的模型将导致更快和更准确的训练,因为模型已经知道通用形状,现在必须将它们拟合到特定图像。有了直觉,现在让我们理解迁移学习的高级流程如下:
1.对输入图像进行归一化,使用与预训练模型训练期间相同的均值和标准差进行归一化。
2.获取预训练模型的架构。获取由于在大型数据集上进行训练而产生的此架构的权重。
3.丢弃预训练模型的最后几层。
4.将截断的预训练模型连接到一个新初始化的层(或多个层),其中权重是随机初始化的。确保最后一层的输出具有与我们想要预测的类/输出一样多的神经元
5.确保预训练模型的权重不可训练(换句话说,在反向传播期间冻结/未更新),但新初始化层的权重以及将其连接到输出层的权重是可训练的:
- 我们不训练预训练模型的权重,因为我们假设这些权重已经为任务很好地学习了,因此利用了大型模型的学习。总之,我们只为我们的小数据集学习新初始化的层。
6.在越来越多的时期更新可训练参数以适应模型。
现在我们已经了解了如何实现迁移学习,让我们了解各种架构、它们是如何构建的,以及将迁移学习应用于后续部分的猫对狗用例时的结果。首先,我们将详细介绍VGG产生的一些各种架构。
了解 VGG16 架构
VGG代表Visual Geometry Group,它基于牛津大学,16 代表模型中的层数。VGG16 模型在 ImageNet 比赛中被训练用于分类对象,并在 2014 年获得亚军架构。我们之所以研究这种架构而不是获胜架构(GoogleNet)是因为它的简单性和更大的愿景接受度社区通过在其他几个任务中使用它。让我们了解 VGG16 的架构以及如何在 PyTorch 中访问和表示 VGG16 预训练模型。
-
import torchvision
-
import torch.nn
as nn
-
import torch
-
import torch.nn.functional
as F
-
from torchvision
import transforms,models,datasets
-
!pip install torch_summary
-
from torchsummary
import summary
-
device = 'cuda'
if torch.cuda.is_available()
else 'cpu '
包中的models模块torchvision托管 PyTorch 中可用的各种预训练模型。
2.加载 VGG16 模型并在设备内注册模型:
model = models.vgg16(pretrained=True).to(device)
在前面的代码中,我们调用了类中的vgg16方法models。此外,通过提及pretrained = True,我们指定加载用于在 ImageNet 比赛中对图像进行分类的权重,然后将模型注册到设备。
3.获取模型的摘要:
summary(model, torch.zeros(1,3,224,224));
上述代码的输出如下:
在前面的总结中,我们提到的 16 层分组如下:
{1,2},{3,4,5},{6,7},{8,9,10},{11,12},{13,14},{15,16,17},{18,19},{20,21},{22,23,24},{25,26},{27,28},{29,30,31,32},{33,34,35},{36,37,38],{39}
同样的总结也可以这样可视化:
请注意,该网络中有约 1.38 亿个参数(其中约 1.22 亿是网络末端的线性层 - 102 + 16 + 400 万个参数),其中包括 13 层卷积和/或池化,随着增加过滤器的数量和 3 个线性层。
理解 VGG16 模型组件的另一种方法是简单地打印如下:
model
这将产生以下输出:
features请注意,模型中有三个主要的子模块—— avgpool、 和classifier。通常,我们会冻结features和avgpool模块。删除classifier模块(或仅底部的几层)并在其位置创建一个新模块,以预测与我们的数据集对应的所需类数(而不是现有的 1,000 个)。
现在让我们了解如何在实践中使用 VGG16 模型,在以下代码中使用猫对狗数据集(考虑每个类中仅 500 张图像进行训练):
1.安装所需的软件包:
-
import torch
-
import torchvision
-
import torch.nn
as nn
-
import torch.nn.functional
as F
-
from torchvision
import transforms,models,datasets
-
import matplotlib.pyplot
as plt
-
from
PIL
import Image
-
from torch
import optim
-
device = 'cuda'
if torch.cuda. is_available()
else 'cpu'
-
import cv2, glob, numpy
as np, pandas
as pd
-
from glob
import glob
-
import torchvision.transforms
as transforms
-
from torch.utils.
data import DataLoader, Dataset
2.下载数据集并指定训练和测试目录:
- 下载数据集。假设我们正在使用 Google Colab,我们执行以下步骤,我们提供身份验证密钥并将其放置在 Kaggle 可以使用该密钥对我们进行身份验证并下载数据集的位置:
-
!pip install -q kaggle
-
from google.colab import files
-
files.upload()
-
!mkdir -p ~
/.kaggle
-
!cp kaggle.json ~
/.kaggle
/
-
!ls ~
/.kaggle
-
!chmod
600
/root
/.kaggle
/ kaggle.json
- 下载数据集并解压:
-
!kaggle datasets download -d tongpython/
cat-and-dog
-
!unzip
cat-and-dog.zip
- 指定训练和测试图像文件夹:
-
train_
data_dir
=
'training_set/training_set'
-
test_
data_dir
=
'test_set/test_set'
3.提供返回猫和狗数据集的输入-输出对的类,就像我们在第 4 章介绍卷积神经网络中所做的那样。请注意,在这种情况下,我们仅从每个文件夹中获取前 500 张图像:
-
class CatsDogs(Dataset):
-
def __init__(
self, folder):
-
cats
= glob(folder
+
'/cats/*.jpg')
-
dogs
= glob(folder
+
'/dogs/*.jpg')
-
self.fpaths
= cats[:
500]
+ dogs[:
500]
-
self.normalize
= transforms.Normalize(mean
=[
0.485,
-
0.456,
0.406],std
=[
0.229,
0.224,
0.225])
-
from
random import shuffle, seed; seed(
10);
-
shuffle(
self.fpaths)
-
self.targets
=[fpath.split(
'/')[-
1].startswith(
'dog') \
-
for fpath
in
self.fpaths]
-
def __len__(
self):
return len(
self.fpaths)
-
def __getitem__(
self, ix):
-
f
=
self.fpaths[ix]
-
target
=
self.targets[ix]
-
im
= (cv
2.imread(f)[:,:,
::-
1])
-
im
= cv
2.resize(im, (
224,224))
-
im
= torch.tensor(im
/
255)
-
im
= im.permute(
2,0,1)
-
im
=
self.normalize(im)
-
return im.float().
to(device),
-
torch.tensor([target]).float().
to(device)
cats_dogs本节中的类与第 4 章中的类之间的主要区别在于normalize我们使用模块中的Normalize函数应用的函数transforms。
4.获取图像及其标签:
data = CatsDogs(train_data_dir)
现在让我们检查一个示例图像及其对应的类:
-
im, label
=
data[
200]
-
plt.imshow(im.permute(
1,2,0).cpu())
-
print(label)
前面的代码产生以下输出:
5.定义模型。下载预训练的 VGG16 权重,然后冻结features模块并使用avgpool和classifier模块进行训练:
- 首先,我们从班级下载预训练的 VGG16 模型models:
-
def
get_model():
-
model
= models.vgg
16(pretrained
=
True)
- 指定我们要冻结之前下载的模型中的所有参数:
-
for param
in model.parameters():
-
param.requires_grad
=
False
在前面的代码中,我们通过指定在反向传播期间param.requires_grad = False冻结参数更新。
- 替换avgpool模块以返回大小为 1 x 1 而不是 7 x 7 的特征图,换句话说,现在的输出将是batch_size x 512 x 1 x 1:
model.avgpool = nn.AdaptiveAvgPool2d(output_size=(1,1))
- 定义classifier模型的模块,这里我们首先将avgpool模块的输出展平,将 512 个单元连接到 128 个单元,并在连接到输出层之前执行激活:
-
model.classifier
= nn.
Sequential(nn.Flatten(),
-
nn.Linear(
512,
128),
-
nn.ReLU(),
-
nn.Dropout(
0.2),
-
nn.Linear(
128,
1),
-
nn.Sigmoid() )
- 定义损失函数 ( loss_fn),optimizer并将它们与定义的模型一起返回:
-
loss_fn
= nn.BCELoss()
-
optimizer
= torch.optim.Adam(model.parameters(),lr
=
1e-
3)
-
return model.
to(device), loss_fn, optimizer
请注意,在前面的代码中,我们首先冻结了预训练模型的所有参数,然后覆盖了avgpool和classifier模块。现在,其余代码看起来与我们在上一章中看到的相似。
模型总结如下:
-
!pip install torch_summary
-
from torchsummary import summary
-
model, criterion, optimizer
=
get_model()
-
summary(model, torch.
zeros(
1,3,224,224))
前面的代码产生以下输出:
请注意,在总共 1470 万个参数中,可训练参数的数量只有 65,793 个,因为我们已经冻结了features模块并覆盖了avgpool和classifier模块。现在,只有classifier模块具有要学习的权重。
6.定义一个函数来训练批次,计算准确度,并像我们在Chapter 4介绍卷积神经网络中所做的那样获取数据:
- 训练一批数据:
-
def
train_batch(x, y, model, opt, loss_fn):
-
model.
train()
-
prediction =
model(x)
-
batch_loss =
loss_fn(prediction, y)
-
batch_loss.
backward()
-
optimizer.
step()
-
optimizer.
zero_grad()
-
return batch_loss.
item()
- 定义一个函数来计算一批数据的准确性:
-
@torch.
no_grad()
-
def accuracy(x, y, model):
-
model.eval()
-
prediction
= model(x)
-
is_correct
= (prediction
>
0.5)
=
= y
-
return
is_correct.cpu().numpy().tolist( )
- 定义一个函数来获取数据加载器:
-
def
get_dat
a():
-
train
= CatsDogs(train_
data_dir)
-
trn_dl
= DataLoader(train, batch_
size
=
32, shuffle
=
True, \
-
drop_
last
=
True)
-
val
= CatsDogs(
test_
data_dir)
-
val_dl
= DataLoader(val, batch_
size
=
32, shuffle
=
True, \
-
drop_
last
=
True)
-
return trn_dl, val_dl
- 初始化get_data和get_model函数:
-
trn_dl, val_dl
=
get_dat
a()
-
model, loss_fn, optimizer
=
get_model()
7.就像我们在第 4 章介绍卷积神经网络中所做的那样,在越来越多的时期训练模型:
-
train_losses, train_accuracies
= [], []
-
val_accuracies
= []
-
for epoch
in range(
5):
-
print(f
" epoch {epoch + 1}/5")
-
train_epoch_losses, train_epoch_accuracies
= [], []
-
val_epoch_accuracies
= []
-
-
for ix, batch
in enumerate(iter(trn_dl)):
-
x, y
= batch
-
batch_loss
= train_batch(x, y, model, optimizer, \
-
loss_fn)
-
train_epoch_losses.append(batch_loss)
-
train_epoch_loss
= np.array(train_epoch_losses).mean()
-
-
for ix, batch
in enumerate(iter(trn_dl)):
-
x, y
= batch
-
is_correct
= accuracy(x, y, model)
-
train_epoch_accuracies.
extend(
is_correct)
-
train_epoch_accuracy
= np.mean(train_epoch_accuracies)
-
-
for ix, batch
in enumerate(iter(val_dl)):
-
x, y
= batch
-
val_
is_correct
= accuracy(x, y, model)
-
val_epoch_accuracies.
extend(val_
is_correct)
-
val_epoch_accuracy
= np.mean(val_epoch_accuracies)
-
-
train_losses.append(train_epoch_loss)
-
train_accuracies.append(train_epoch_accuracy)
-
val_accuracies.append(val_epoch_accuracy)
8.绘制增加时期的训练和测试准确度值:
-
epochs
= np.arange(
5)
+
1
-
import matplotlib.ticker
as mtick
-
import matplotlib.pyplot
as plt
-
import matplotlib.ticker
as mticker
-
%matplotlib inline
-
plt.plot(epochs, train_accuracies,
'bo',
-
label
=
'Training accuracy')
-
plt.plot(epochs, val_accuracies,
'r',
-
label
=
'Validation accuracy')
-
plt.gc
a().xaxis.
set_major_locator(mticker.MultipleLocator(
1))
-
plt.title(
'Training and validation accuracy \
-
with VGG16 \nand 1K training data points')
-
plt.xlabel(
'Epochs')
-
plt.ylabel(
'Accuracy')
-
plt.ylim(
0.95,1)
-
plt.gc
a().
set_yticklabels([
'{:.0f}%'.
format(x
*
100) \
-
for x
in plt.gc
a().
get_yticks()])
-
plt.legend()
-
plt.grid(
'off')
-
plt.show()
这将产生以下输出:
请注意,我们能够在第一个 epoch 内获得 98% 的准确率,即使在包含 1,000 张图像(每个类别 500 张图像)的小型数据集上也是如此。
我们使用 VGG11 和 VGG19 代替 VGG16 预训练模型时的训练和验证准确率如下:
请注意,虽然基于 VGG19 的模型的准确度略高于基于 VGG16 的模型,在验证数据上的准确率为 98%,但基于 VGG11 的模型的准确度略低,为 97%。
从 VGG16 到 VGG19,我们增加了层数,一般来说,神经网络越深,它的准确率就越高。
然而,如果仅仅增加层数是诀窍,那么我们可以继续向模型添加更多层(同时注意避免过度拟合)以在 ImageNet 上获得更准确的结果,然后针对感兴趣的数据集对其进行微调. 不幸的是,事实并非如此。
有多种原因导致它不那么容易。随着我们在架构方面的深入,以下任何一种情况都可能发生:
- 我们必须学习更多的特征。
- 消失的梯度出现。
- 更深层次的信息修改太多。
ResNet 用于解决识别何时不学习的特定场景,我们将在下一节中学习。
了解 ResNet 架构
在构建太深的网络时,有两个问题。在前向传播中,网络的最后几层几乎没有关于原始图像是什么的信息。在反向传播中,由于梯度消失(换句话说,它们几乎为零),靠近输入的前几层几乎没有任何梯度更新。为了解决这两个问题,残差网络 (ResNet) 使用类似高速公路的连接,将原始信息从前几层传输到后面的层。理论上,由于这个高速公路网络,即使是最后一层也会拥有原始图像的全部信息。并且由于跳过层,反向梯度将在几乎没有修改的情况下自由地流向初始层。
残差网络中的术语残差是模型期望从前一层学习的附加信息,需要传递到下一层。
一个典型的残差块如下所示:
如您所见,到目前为止,我们一直对提取 F(x) 值感兴趣,其中 x 是来自前一层的值,在残差网络的情况下,我们不仅提取通过后的值通过权重层,即 F(x),但也将 F(x) 与原始值相加,即 x。
到目前为止,我们一直在使用标准层来执行线性或卷积变换F(x)以及一些非线性激活。这两种操作在某种意义上都会破坏输入信息。我们第一次看到一个层不仅可以转换输入,还可以通过将输入直接添加到转换中来保留它F(x) + x。这样,在某些场景下,层在记住输入是什么方面的负担很小,并且可以专注于学习任务的正确转换。
让我们通过构建残差块的代码更详细地看一下残差层:
1.在方法中定义一个带有卷积操作的类(上图中的权重层)__init__:
-
class ResLayer(nn.Module):
-
def __init__(
self,ni,
no,kernel_
size,stride
=
1):
-
super(ResLayer,
self).__init__()
-
padding
= kernel_
size
-
2
-
self.conv
= nn.
Sequential(
-
nn.Conv
2d(ni,
no, kernel_
size, stride,
-
padding
=padding),
-
nn.ReLU()
-
)
请注意,在前面的代码中,我们定义padding了通过卷积时输出的维度,如果我们将两者相加,输入的维度应该保持不变。
2.定义forward方法:
-
def
forward(
self, x):
-
x =
self.conv(x) + x
-
return x
在前面的代码中,我们得到的输出是通过卷积操作的输入和原始输入的总和。
现在我们已经了解了残差块的工作原理,让我们了解残差块在预训练的基于残差块的网络 ResNet18 中是如何连接的:
如您所见,该架构有 18 层,因此被称为 ResNet18 架构。此外,请注意跳过连接是如何通过网络进行的。它不是在每个卷积层都进行,而是在每两层之后进行。
现在我们了解了 ResNet 架构的组成,让我们构建一个基于 ResNet18 架构的模型来对狗和猫进行分类,就像我们在上一节中使用 VGG16 所做的那样。
为了构建分类器,VGG16 部分第3 步的代码与处理导入包、获取数据和检查它们的代码相同。因此,我们将从了解预训练的 ResNet18 模型的组成开始:
1.加载预训练的 ResNet18 模型并检查加载模型中的模块:
-
model
= models.resnet
18(pretrained
=
True).
to(device)
-
model
ResNet18 模型的结构包含以下组件:
- 卷积
- 批量标准化
- 恢复
- 最大池化
- 四层 ResNet 块
- 平均池化(avgpool)
- 全连接层 (fc)
正如我们在 VGG16 中所做的那样,我们将冻结所有不同的模块,但在下一步中更新avgpool和模块中的参数。fc
2.定义模型架构、损失函数和优化器:
-
def
get_model():
-
model
= models.resnet
18(pretrained
=
True)
-
for param
in model.parameters():
-
param.requires_grad
=
False
-
model.avgpool
= nn.AdaptiveAvgPool
2d(
output_
size
=(
1,1))
-
model.fc
= nn.
Sequential(nn.Flatten(),
-
nn.Linear(
512,
128),
-
nn.ReLU(),
-
nn.Dropout(
0.2),
-
nn.Linear(
128,
1),
-
nn.Sigmoid())
-
loss_fn
= nn.BCELoss()
-
optimizer
= torch.optim.Adam(model.parameters(),lr
=
1e-
3)
-
return model.
to(device), loss_fn, optimizer
在前面的模型中,fc模块的输入形状为 512,因为输出avgpool的形状为批量大小 x 512 x 1 x 1。
现在我们已经定义了模型,让我们按照 VGG 部分执行步骤 5和6 。训练模型(以下每个图表的模型分别为 ResNet18、ResNet 34、ResNet 50、ResNet 101 和ResNet 152)随着 epoch 的增加,训练和验证准确度的变化如下:
我们看到模型的准确率在仅对 1000 张图像进行训练时,在 97% 和 98% 之间变化,其中准确率随着 ResNet 中层数的增加而增加。
现在我们已经了解了如何利用预训练模型来预测二元类,在接下来的部分中,我们将学习如何利用预训练模型来解决涉及以下内容的实际用例:
- 多重回归:给定图像作为输入预测多个值 - 面部关键点检测
- 多任务学习:单次预测多个项目——年龄估计和性别分类
实现面部关键点检测
到目前为止,我们已经了解了预测二元类(猫对狗)或多标签(fashionMNIST)的类。现在让我们学习一个回归问题,在此过程中,我们预测的不是一个而是几个连续输出的任务。想象一个场景,要求您预测面部图像上的关键点,例如眼睛、鼻子和下巴的位置。在这种情况下,我们需要采用新的策略来构建模型来检测关键点。
在我们进一步深入之前,让我们通过下图了解我们想要实现的目标:
如上图所示,面部关键点表示包含人脸的图像上的各个关键点的标记。
要解决这个问题,我们首先要解决几个问题:
- 图像可以是不同的形状:
- 这保证了在调整图像以使它们全部达到标准图像尺寸的同时调整关键点位置。
- 面部关键点类似于散点图上的点,但这次基于某种模式散布:
- 这意味着如果将图像调整为 224 x 224 x 3 的形状,则这些值介于 0 和 224 之间。
- 根据图像的大小对因变量(面部关键点的位置)进行归一化:
- 如果我们考虑它们相对于图像尺寸的位置,则关键点值始终介于 0 和 1 之间。
- 鉴于因变量值始终介于 o 和 1 之间,我们可以在最后使用 sigmoid 层来获取介于 0 和 1 之间的值。
让我们制定解决这个用例的流程:
1.导入相关包。
2.导入数据。
3.定义准备数据集的类:
- 确保对输入图像进行适当的预处理以执行迁移学习。
- 确保以这样一种方式处理关键点的位置,即我们获取它们相对于处理后的图像的相对位置。
4.定义模型、损失函数和优化器:
- 损失函数是平均绝对误差,因为输出是 0 到 1 之间的连续值。
5.在越来越多的时期训练模型。
现在让我们实现前面的步骤:
1.导入相关包和数据集:
-
import torchvision
-
import torch.nn
as nn
-
import torch
-
import torch.nn.functional
as F
-
from torchvision
import transforms, models, datasets
-
from torchsummary
import summary
-
import numpy
as np, pandas
as pd, os, glob, cv2
-
from torch.utils.
data import TensorDataset,DataLoader,Dataset
-
from copy
import deepcopy
-
from mpl_toolkits.mplot3d
import Axes3D
-
import matplotlib.pyplot
as plt
-
%matplotlib inline
-
from sklearn
import cluster
-
device = 'cuda'
if torch.cuda.is_available()
else 'cpu'
2.下载并导入相关数据。您可以下载包含图像及其对应的面部关键点的相关数据:
-
!git clone https:
/
/github.com
/udacity
/P
1_Facial_Keypoints.git
-
!cd P
1_Facial_Keypoints
-
root_dir
=
'P1_Facial_Keypoints/data/training/'
-
all_img_paths
= glob.glob(os.path.join(root_dir,
'*.jpg'))
-
data
= pd.
read_csv(\
-
'P1_Facial_Keypoints/data/training_frames_keypoints.csv')
导入数据集的示例如下:
在前面的输出中,第 1 列代表图像的名称,偶数列代表人脸 68 个关键点中每个关键点对应的x轴值,其余奇数列(第一列除外)代表y -axis 值对应于 68 个关键点中的每一个。
3.定义FacesData为数据加载器提供输入和输出数据点的类:
class FacesData(Dataset):
- 现在让我们定义__init__方法,它将文件 ( df) 的数据框作为输入:
-
def
__init__(
self, df):
-
super(
FacesData).__init__()
-
self.df = df
- 定义用于预处理图像的均值和标准差,以便它们可以被预训练的 VGG16 模型使用:
-
self.normalize
= transforms.Normalize(
-
mean
=[
0.485,
0.456,
0.406],
-
std
=[
0.229,
0.224,
0.225]
)
- 现在,定义__len__ 方法:
def __len__(self): return len(self.df)
接下来,我们定义__getitem__方法,我们获取与给定索引对应的图像,对其进行缩放,获取与给定索引对应的关键点值,对关键点进行归一化,以便我们将关键点的位置作为比例图像的大小,并对图像进行预处理。
- 定义方法并获取与给定索引 ( )__getitem__对应的图像的路径:ix
-
def __getitem__(
self, ix):
-
img_path
=
'P1_Facial_Keypoints/data/training/'
+ \
-
self.df.iloc[ix,
0]
- 缩放图像:
img = cv2.imread(img_path)/255。
- 将预期输出值(关键点)归一化为原始图像大小的比例:
-
kp
= deepcopy(
self.df.iloc[ix,
1:].tolist())
-
kp_x
= (np.array(kp[
0
::
2])
/img.shape[
1]).tolist()
-
kp_y
= ( np.array(kp[
1
::
2])
/img.shape[
0]).tolist()
在前面的代码中,我们确保关键点按原始图像大小的比例提供。这样做是为了当我们调整原始图像的大小时,关键点的位置不会改变,因为关键点是作为原始图像的一部分提供的。此外,通过将关键点作为原始图像的一部分,我们期望输出值介于 0 和 1 之间。
- 对图像进行预处理后返回关键点(kp2)和图像( ):img
-
kp
2
= kp_x
+ kp_y
-
kp
2
= torch.tensor(kp
2)
-
img
=
self.preprocess_
input(img)
-
return img, kp
2
- 定义预处理图像的函数 ( preprocess_input):
-
def preprocess_
input(
self, img):
-
img
= cv
2.resize(img, (
224,224))
-
img
= torch.tensor(img).permute(
2,0,1)
-
img
=
self.normalize(img).float()
-
return img.
to(device)
- 定义一个函数来加载图像,当我们想要可视化测试图像和测试图像的预测关键点时,这将很有用:
-
def load_img(
self, ix):
-
img_path
=
'P1_Facial_Keypoints/data/training/'
+ \
-
self.df.iloc[ix,
0]
-
img
= cv
2.imread(img_path)
-
img
= cv
2.cvtColor(img, cv
2.COLOR_BGR
2RGB)
/
255.
-
img
= cv
2.resize(img, (
224,224))
-
return img
4.现在让我们创建一个训练和测试数据拆分,并建立训练和测试数据集和数据加载器:
-
from sklearn.model_selection import train_
test_split
-
-
train,
test
= train_
test_split(
data,
test_
size
=
0.2, \
-
random_state
=
101)
-
train_dataset
= FacesData(train.
reset_
index(drop
=
True))
-
test_dataset
= FacesData(
test.
reset_
index(drop
=
True))
-
-
train_loader
= DataLoader(train_dataset, batch_
size
=
32)
-
test_loader
= DataLoader(
test_dataset, batch_
size
=
32)
在前面的代码中,我们在输入数据框中按人名拆分了训练和测试数据集,并获取了它们对应的对象。
5.现在让我们定义我们将用来识别图像中关键点的模型:
- 加载预训练的 VGG16 模型:
-
def
get_model():
-
model
= models.vgg
16(pretrained
=
True)
- 确保先冻结预训练模型的参数:
-
for param
in model.parameters():
-
param.requires_grad
=
False
覆盖并解冻模型最后两层的参数:
-
model.avgpool
= nn.
Sequential(nn.Conv
2d(
512,512,3),
-
nn.MaxPool
2d(
2),
-
nn.Flatten())
-
model.classifier
= nn.
Sequential(
-
nn.Linear(
2048,
512),
-
nn.ReLU (),
-
nn.Dropout(
0.5),
-
nn.Linear(
512,
136),
-
nn.Sigmoid()
-
)
请注意,classifier模块中模型的最后一层是一个 sigmoid 函数,它返回一个介于 o 和 1 之间的值,并且预期输出将始终介于 0 和 1 之间,因为关键点位置是原始图像尺寸的一小部分:
- 定义损失函数和优化器并将它们与模型一起返回:
-
criterion
= nn.L
1Loss()
-
optimizer
= torch.optim.Adam(model.parameters(), lr
=
1e-
4)
-
return model.
to(device), criterion, optimizer
请注意,损失函数是L1Loss,换句话说,我们正在对面部关键点位置的预测(将被预测为图像宽度和高度的百分比)进行平均绝对误差减少。
6.获取模型、损失函数和对应的优化器:
model, criterion, optimizer = get_model()
7.定义函数以在一批数据点上进行训练并在测试数据集上进行验证:
- 正如我们之前所做的那样,训练批次涉及获取通过模型传递输入的输出、计算损失值以及执行反向传播以更新权重:
-
def
train_batch(img, kps, model, optimizer, criterion):
-
model.
train()
-
optimizer.
zero_grad()
-
_kps =
model(img.
to(device))
-
loss =
criterion(_kps, kps.
to(device))
-
loss.
backward()
-
optimizer.
step()
-
return loss
- 构建一个函数,返回测试数据的损失和预测的关键点:
-
def
validate_batch(img, kps, model, criterion):
-
model.
eval()
-
with torch.
no_grad():
-
_kps =
model(img.
to(device))
-
loss =
criterion(_kps, kps.
to(device))
-
return _kps, loss
8.在训练数据加载器的基础上训练模型,并在测试数据上对其进行测试,正如我们在前面几节中所做的那样:
-
train_loss,
test_loss
= [], []
-
n_epochs
=
50
-
-
for epoch
in range(n_epochs):
-
print(f
" epoch {epoch+ 1} : 50")
-
epoch_train_loss, epoch_
test_loss
=
0,
0
-
for ix, (img,kps)
in enumerate(train_loader):
-
loss
= train_batch(img, kps, model, optimizer, \
-
criterion)
-
epoch_train_loss
+
= loss.item()
-
epoch_train_loss
/
= (ix
+
1)
-
-
for ix,(img,kps)
in enumerate(
test_loader):
-
ps, loss
=
validate_batch(img, kps, model, criterion)
-
epoch_
test_loss
+
= loss.item()
-
epoch_
test_loss
/
= (ix
+
1)
-
-
train_loss.append(epoch_train_loss)
-
test_loss.append(epoch_
test_loss)
9. 绘制增加时期的训练和测试损失:
-
epochs
= np.arange(
50)
+
1
-
import matplotlib.ticker
as mtick
-
import matplotlib.pyplot
as plt
-
import matplotlib.ticker
as mticker
-
%matplotlib inline
-
plt.plot(epochs, train_loss,
'bo', label
=
'Training loss')
-
plt.plot(epochs,
test_loss,
'r', label
=
'Test loss')
-
plt.title(
'Training and Test loss over increasing epochs')
-
plt.xlabel(
'Epochs')
-
plt.ylabel(
'Loss')
-
plt .legend()
-
plt.grid(
'off')
-
plt.show()
前面的代码产生以下输出:
10.在随机测试图像的索引上测试我们的模型,假设为 0。请注意,在以下代码中,我们利用了之前创建的类中的load_img方法:FacesData
-
ix
=
0
-
plt.figure(figsize
=(
10,10))
-
plt.subplot(
221)
-
plt.title(
'Original image')
-
im
=
test_dataset.load_img(ix)
-
plt.imshow(im)
-
plt.grid(
False)
-
plt.subplot(
222)
-
plt.title(
'Image with facial keypoints')
-
x, _
=
test_dataset[ix]
-
plt.imshow(im)
-
kp
= model(x[None]).flatten().detach().cpu()
-
plt.scatter(kp[:
68]
*
224, kp[
68:]
*
224, c
=
'r')
-
plt.grid(
False)
-
plt.show()
前面的代码产生以下输出:
从前面的图像中,我们看到模型能够相当准确地识别面部关键点,给定图像作为输入。
在本节中,我们从头开始构建面部关键点检测器模型。但是,有一些为 2D 和 3D 点检测而构建的预训练模型。在下一节中,我们将学习如何利用人脸对齐库来获取人脸的 2D 和 3D 关键点。
2D和3D面部关键点检测
在本节中,我们将利用一个预训练模型,该模型可以通过几行代码检测人脸中存在的 2D 和 3D 关键点。
为此,我们将利用该face-alignment库:
1.安装所需的软件包:
-
!pip install -qU face-alignment
-
import face_alignment, cv2
2.导入图像:
!wget https://www.dropbox.com/s/2s7xjto7rb6q7dc/Hema.JPG
3.定义人脸对齐方法,在这里我们指定是要获取 2D 还是 3D 的关键点地标:
-
fa
= face_alignment.FaceAlignment(\
-
face_alignment.LandmarksType._
2D, \
-
flip_
input
=
False, device
=
'cpu')
4.读取输入图像并将其提供给get_landmarks方法:
-
input
= cv
2.imread(
'Hema.JPG')
-
preds
= fa.
get_landmarks(
input)[
0]
-
print(preds.shape)
-
# (
68,2)
在前面的代码行中,我们利用类中的get_landmarks方法fa来获取与面部关键点对应的 68个 x和y坐标。
5.用检测到的关键点绘制图像:
-
import matplotlib.pyplot
as plt
-
%matplotlib inline
-
fig,ax
= plt.subplots(figsize
=(
5,5))
-
plt.imshow(cv
2.cvtColor(cv
2.imread(
'Hema.JPG'), \
-
cv
2.COLOR_BGR
2RGB))
-
ax.scatter(preds[:,
0], preds[:,
1], marker
=
'+', c
=
'r')
-
plt.show()
前面的代码产生以下输出:
注意 60 个可能的面部关键点周围的 + 符号散点图。
以类似的方式,获得面部关键点的 3D 投影如下:
-
fa
= face_alignment.FaceAlignment(
-
face_alignment.LandmarksType._
3D , flip_
input
-
=
False, device
=
'cpu')
-
input
= cv
2.imread(
'Hema.JPG')
-
preds
= fa.
get_landmarks(
input)[
0]
-
import pandas
as pd
-
df
= pd.DataFrame(preds)
-
df.
columns
= [
'x',
'y',
'z']
-
import plotly.express
as px
-
fig
= px.scatter_
3d(df, x
=
'x', y
=
'y', z
=
'z')
-
fig.show()
请注意,与 2D 关键点场景中使用的代码的唯一变化是我们指定LandmarksType为 3D 代替 2D
前面的代码产生以下输出:
通过利用该face_alignment库的代码,我们看到我们能够利用预先训练的面部关键点检测模型在预测新图像时具有高精度。
到目前为止,在不同的用例中,我们已经了解了以下内容:
- 猫与狗:预测二元分类
- FashionMNIST:预测 10 个可能类别中的标签
- 面部关键点:预测给定图像的 0 到 1 之间的多个值
在下一节中,我们将学习如何使用单个网络一次性预测二元类和回归值。
多任务学习——实现年龄估计和性别分类
多任务学习是研究的一个分支,其中一个/几个输入用于预测几个不同但最终连接的输出。例如,在自动驾驶汽车中,模型需要识别障碍物、规划路线、提供适量的油门/制动和转向,仅举几例。它需要通过考虑相同的输入集(将来自多个传感器)在瞬间完成所有这些
从到目前为止我们已经解决的各种用例中,我们能够训练神经网络并估计给定图像的人的年龄或预测给定图像的人的性别,一次一项任务。但是,我们还没有研究过这样一种场景,即我们能够在一张图像的单张照片中预测年龄和性别。在一个镜头中预测两个不同的属性很重要,因为相同的图像用于两个预测(当我们在第 7 章,对象检测基础知识中执行对象检测时,我们将进一步理解这一点)。.
在本节中,我们将学习如何在单个前向传递中预测属性、连续预测和分类预测。
我们采用的策略如下:
1.导入相关包。
2.获取包含人物图像、性别和年龄信息的数据集。
3.通过执行适当的预处理来创建训练和测试数据集。
4.建立一个适用于以下情况的模型:
- 模型的所有层都与我们迄今为止构建的模型相似,除了最后一部分。
- 在最后一部分中,创建两个从前一层分支出来的独立层,其中一层对应于年龄估计,另一层对应于性别分类。
- 确保每个输出分支都有不同的损失函数,因为年龄是一个连续值(需要计算 mse 或 mae 损失),而性别是一个分类值(需要计算交叉熵损失)。
- 对年龄估计损失和性别分类损失进行加权求和。
- 通过执行优化权重值的反向传播来最小化整体损失。
5.训练模型并预测新图像。
有了前面的策略,让我们编写用例:
1.导入相关包:
-
import torch
-
import numpy
as np, cv2, pandas
as pd, glob, time
-
import matplotlib.pyplot
as plt
-
%matplotlib inline
-
import torch.nn
as nn
-
from torch
import optim
-
import torch.nn.functional
as F
-
from torch.utils.
data import Dataset, DataLoader
-
import torchvision
-
from torchvision
import transforms, models, datasets
-
device = 'cuda'
if torch.cuda.is_available()
else 'cpu'
2.获取数据集:
-
from pydrive.auth import GoogleAuth
-
from pydrive.drive import GoogleDrive
-
from google.colab import auth
-
from oauth
2client.client import GoogleCredentials
-
-
auth.authenticate_user()
-
gauth
= GoogleAuth()
-
gauth.credentials
=GoogleCredentials.
get_application_
default()
-
drive
= GoogleDrive(gauth)
-
-
def getFile_
from_drive(
file_id, name ):
-
downloaded
= drive.CreateFile({
'id':
file_id})
-
downloaded.GetContentFile(name)
-
-
getFile_
from_drive(
'1Z1RqRo0_JiavaZw2yzZG6WETdZQ8qX86',
-
'fairface-img-margin025-trainval.zip')
-
getFile_
from_drive(
'1k5vvyREmHDW5TSM9QgB04Bvc8C8_7dl-',
-
'fairface-label-train.csv')
-
getFile_
from_drive(
'1_rtz1M1zhvS0d5vVoXUamnohB6cJ02iJ',
-
'fairface-label-val.csv')
-
-
!unzip -qq fairface-img-margin
025-trainval.zip
3.我们下载的数据集可以加载并按以下方式构建:
-
trn_df
= pd.
read_csv(
'fairface-label-train.csv')
-
val_df
= pd.
read_csv(
'fairface-label-val.csv')
-
trn_df.head()
前面的代码产生以下输出:
4.构建将GenderAgeClass文件名作为输入并返回相应图像、性别和缩放年龄的类。我们缩放年龄,因为它是一个连续的数字,正如我们在第 3 章中所见,使用 PyTorch 构建深度神经网络,最好缩放数据以避免梯度消失,然后在后处理期间重新缩放它:
- 在方法中提供图像的文件路径 ( fpaths) __init__:
-
IMAGE_
SIZE
=
224
-
class GenderAgeClass(Dataset):
-
def __init__(
self, df, tfms
=None):
-
self.df
= df
-
self.normalize
= transforms.Normalize(
-
mean
=[
0.485,
0.456,
0.406],
-
std
=[
0.229,
0.224,
0.225])
- 将方法定义__len__为返回输入中图像数量的方法:
def __len__(self): return len(self.df)
- 定义__getitem__在给定位置获取图像信息的方法ix:
-
def __getitem__(
self, ix):
-
f
=
self.df.iloc[ix].squeeze()
-
file
= f.
file
-
gen
= f.gender
=
=
'Female'
-
age
= f.age
-
im
= cv
2.imread(
file)
-
im
= cv
2.cvtColor(im, cv
2.COLOR_BGR
2RGB)
-
return im, age, gen
- 编写一个预处理图像的函数,包括调整图像大小、排列通道以及对缩放图像执行归一化:
-
def preprocess_image(
self, im):
-
im
= cv
2.resize(im, (IMAGE_
SIZE, IMAGE_
SIZE))
-
im
= torch.tensor(im).permute(
2,0,1)
-
im
=
self.normalize(im
/
255.)
-
return im[None]
- 创建该collate_fn方法,该方法获取一批数据,其中数据点经过如下预处理:
- process_image使用该方法处理每个图像。
- 将年龄缩放 80(数据集中存在的最大年龄值),以便所有值都介于 0 和 1 之间。
- 将性别转换为浮点值。
- 图像、年龄和性别分别转换为火炬对象并返回:
-
def
collate_fn(self, batch):
-
'preprocess images, ages and genders'
-
ims, ages, genders = [], [], []
-
for im, age, gender in batch:
-
im = self.
preprocess_image(im)
-
ims.
append(im)
-
-
ages.
append(
float(
int(age)/
80))
-
genders.
append(
float(gender))
-
-
ages, genders = [torch.
tensor(x).
to(device).
float() \
-
for x in [ages, genders]]
-
ims = torch.
cat(ims).
to(device)
-
-
return ims, ages, genders
5.我们现在定义训练和验证数据集和数据加载器:
- 创建数据集:
-
trn = GenderAgeClass(
trn_df)
-
val = GenderAgeClass(
val_df)
- 指定数据加载器:
-
device
=
'cuda'
if torch.cuda.
is_available()
else
'cpu'
-
train_loader
= DataLoader(trn, batch_
size
=
32, shuffle
=
True, \
-
drop_
last
=
True,collate_fn
=trn.collate_fn)
-
test_loader
= DataLoader(val, batch_
size
=
32,
-
collate_fn
=val.collate_fn)
-
a,b,c,
=
next(iter(train_loader))
-
print(a.shape, b.shape, c.shape)
6.定义模型、损失函数和优化器:
- 首先,在函数中,我们加载预训练的 VGG16 模型:
-
def
get_model():
-
model
= models.vgg
16(pretrained
=
True)
- 接下来,冻结加载的模型(通过指定param.requires_grad = False):
-
for param
in model.parameters():
-
param.requires_grad
=
False
- 用我们自己的层覆盖该avgpool层:
-
model.avgpool
= nn.
Sequential(
-
nn.Conv
2d(
512,512, kernel_
size
=
3),
-
nn.MaxPool
2d(
2),
-
nn.ReLU(),
-
nn.Flatten()
-
)
现在是关键部分。我们通过创建两个输出分支来偏离我们迄今为止所学的内容。执行如下:
- 在方法中构建一个class命名ageGenderClassifier为以下的神经网络__init__:
-
class ageGenderClassifier(nn.Module):
-
def __init__(self):
-
super(ageGenderClassifier, self).__init__()
- 定义intermediate层计算:
-
self.intermediate
= nn.
Sequential(
-
nn.Linear(
2048,512),
-
nn.ReLU(),
-
nn.Dropout(
0.4),
-
nn.Linear(
512,128),
-
nn.ReLU(),
-
nn.Dropout(
0.4),
-
nn.Linear(
128,64),
-
nn.ReLU(),
-
)
- 定义age_classifier和gender_classifier:
-
self.age_classifier
= nn.
Sequential(
-
nn.Linear(
64,
1),
-
nn.Sigmoid()
-
)
-
self.gender_classifier
= nn.
Sequential(
-
nn.Linear(
64,
1),
-
nn.Sigmoid()
-
)
请注意,在前面的代码中,最后一层有一个 sigmoid 激活,因为年龄输出将是一个介于 0 和 1 之间的值(因为它被缩放了 80)并且性别有一个 sigmoid,因为输出是0或1 .
- forward将层叠层的 pass 方法定义为intermediatefirst ,然后是age_classifierthen gender_classifier:
-
def forward(
self, x):
-
x
=
self.intermediate(x)
-
age
=
self.age_classifier(x)
-
gender
=
self.gender_classifier(x)
-
return gender, age
- 用我们之前定义的类覆盖classifier模块:
model.classifier = ageGenderClassifier()
- 定义性别(二元交叉熵损失)和年龄(L1 损失)预测的损失函数。定义优化器并返回模型、损失函数和优化器,如下所示:
-
gender_criterion
= nn.BCELoss()
-
age_criterion
= nn.L
1Loss()
-
loss_functions
= gender_criterion, age_criterion
-
optimizer
= torch.optim.Adam(model.parameters(),lr
=
1e-
4)
-
return model.
to(device), loss_functions, optimizer
- 调用get_model函数来初始化变量中的值:
model, criterion, optimizer = get_model()
7.定义函数以对一批数据进行训练并在一批数据集上进行验证。
该train_batch方法将图像、性别、年龄、模型、优化器和损失函数的实际值作为输入来计算总损失,如下所示:
- train_batch使用适当的输入参数定义方法:
def train_batch(data, model, optimizer, criteria):
- 指定我们正在训练模型,将优化器重置为zero_grad,并计算年龄和性别的预测值:
-
model.train()
-
ims, age, gender
=
data
-
optimizer.
zero_grad()
-
pred_gender, pred_age
= model(ims)
- 在计算年龄估计和性别分类对应的损失之前,获取年龄和性别的损失函数:
-
gender_criterion, age_criterion
= criteria
-
gender_loss
= gender_criterion(pred_gender.squeeze(), \
-
gender)
-
age_loss
= age_criterion(pred_age.squeeze(), age)
- 通过求和gender_loss和age_loss执行反向传播来计算整体损失,通过优化模型的可训练权重来减少整体损失并返回整体损失:
-
total_loss
= gender_loss
+ age_loss
-
total_loss.backward()
-
optimizer.step()
-
return total_loss
该validate_batch方法以图像、模型和损失函数以及年龄和性别的实际值作为输入,计算年龄和性别的预测值以及损失值,如下所示:
- 使用适当的输入参数定义vaidate_batch 函数:
def validate_batch(data, model, criteria):
- 指定我们要评估模型,因此在通过模型传递图像来预测年龄和性别值之前不需要进行梯度计算:
-
model
.eval()
-
with torch
.no_grad():
-
pred_gender, pred_age =
model(img)
- 计算与年龄和性别预测对应的损失值(gender_loss和age_loss)。我们压缩预测(具有 (batch size, 1) 的形状,以便将其重新整形为与原始值相同的形状(具有批量大小的形状):
-
gender_criterion, age_criterion
= criteria
-
gender_loss
= gender_criterion(pred_gender.squeeze(), \
-
gender)
-
age_loss
= age_criterion(pred_age.squeeze(), age)
- 计算整体损失,最终预测性别类别 ( pred_gender),并返回预测性别、年龄和总损失:
-
total_loss
= gender_loss
+ age_loss
-
pred_gender
= (pred_gender
>
0.5).squeeze()
-
gender_acc
= (pred_gender
=
= gender).float().
sum()
-
age_mae
= torch.abs(age
- pred_age).float().
sum()
-
return total_loss, gender_acc, age_mae
8.在五个时期训练模型:
- 定义占位符来存储训练和测试损失值,并指定 epoch 的数量:
-
import
time
-
model, criteria, optimizer
=
get_model()
-
val_gender_accuracies
= []
-
val_age_maes
= []
-
train_losses
= []
-
val_losses
= []
-
-
n_epochs
=
5
-
best_
test_loss
=
1000
-
start
=
time.
time()
- 循环遍历不同的 epoch 并在每个 epoch 开始时重新初始化训练和测试损失值:
-
for epoch
in range(n_epochs):
-
epoch_train_loss, epoch_
test_loss
=
0,
0
-
val_age_mae, val_gender_acc, ctr
=
0,
0,
0
-
_n
= len(train_loader)
- 循环训练数据加载器 ( train_loader) 并训练模型:
-
for ix,
data in enumerate(train_loader):
-
loss = train_batch(
data, model, optimizer, criteria)
-
epoch_train_loss += loss.item()
- 循环通过测试数据加载器并计算性别准确性以及年龄:
-
for ix,
data
in enumerate(
test_loader):
-
loss, gender_acc, age_mae
=
validate_batch(
data, \
-
model, criteria)
-
epoch_
test_loss
+
= loss.item()
-
val_age_mae
+
= age_mae
-
val_gender_acc
+
= gender_acc
-
ctr
+
= len(
data[
0])
- 计算年龄预测和性别分类的整体准确率:
-
val_age_mae
/
= ctr
-
val_gender_acc
/
= ctr
-
epoch_train_loss
/
= len(train_loader)
-
epoch_
test_loss
/
= len(
test_loader)
- 记录每个时期的指标:
-
elapsed
=
time.
time()-
start
-
best_
test_loss
= min(best_
test_loss, epoch_
test_loss)
-
print(
'{}/{} ({:.2f}s - {:.2f}s remaining)'.
format(\
-
epoch
+
1, n_epochs,
time.
time()-
start, \
-
(n_epochs-epoch)
*(elapsed
/(epoch
+
1))))
-
info
= f
''
'Epoch: {epoch+1:03d}
-
\tTrain Loss: {epoch_train_loss:.3f}
-
\tTest:\{epoch_test_loss:.3f}
-
\tBest Test Loss: {best_test_loss:.4f}'
''
-
info
+
= f
'\nGender Accuracy:
-
{val_gender_acc*100:.2f}%\tAge MAE: \
-
{val_age_mae:.2f}\n'
-
print(info)
- 存储每个 epoch 中测试数据集的年龄和性别准确度:
-
val_gender_accuracies
.append(val_gender_acc)
-
val_age_maes
.append(val_age_mae)
9.绘制年龄估计和性别预测在不断增加的时期的准确性:
-
epochs
= np.arange(
1,(n_epochs
+
1))
-
fig,ax
= plt.subplots(
1,2,figsize
=(
10,5))
-
ax
= ax.flat
-
ax[
0].plot(epochs, val_gender_accuracies,
'bo')
-
ax[
1].plot(epochs, val_age_maes,
'r')
-
ax[
0].
set_xlabel(
'Epochs') ; ax[
1].
set_xlabel(
'Epochs')
-
ax[
0].
set_ylabel(
'Accuracy'); ax[
1].
set_ylabel(
'MAE')
-
ax[
0].
set_title(
'Validation Gender Accuracy')
-
ax[
0].
set_title(
'Validation Age Mean-Absolute-Error')
-
plt.show()
前面的代码产生以下输出:
我们在年龄预测方面落后了 6 年,在预测性别方面的准确率约为 84%。
10.在随机测试图像上预测年龄和性别:
- 获取图像:
!wget https://www.dropbox.com/s/6kzr8l68e9kpjkf/5_9.JPG
- 加载图像并将其传递给我们之前创建的对象中的preprocess_image方法:trn
-
im
= cv
2.imread(
'/content/5_9.JPG')
-
im
= trn.preprocess_image(im).
to(device)
- 通过训练好的模型传递图像:
-
gender, age
= model(im)
-
pred_gender
= gender.
to(
'cpu').detach().numpy()
-
pred_age
= age.
to(
'cpu').detach().numpy()
- 绘制图像并打印原始值和预测值:
-
im
= cv
2.imread(
'/content/5_9.JPG')
-
im
= cv
2.cvtColor(im, cv
2.COLOR_BGR
2RGB)
-
plt.imshow(im)
-
print(
'predicted gender:',np.where(pred_gender[
0][
0]
<
0.5, \
-
'Male',
'Female'),
-
'; Predicted age', int(pred_age[
0][
0]
*
80))
前面的代码产生以下输出:
综上所述,我们可以看到,我们能够一次性预测年龄和性别。但是,我们需要注意,这是非常不稳定的,并且年龄值会随着图像的不同方向和光照条件而有很大差异。在这种情况下,数据增强会派上用场。
到目前为止,我们已经了解了迁移学习、预训练架构以及如何在两个不同的用例中利用它们。您还会注意到代码稍长,我们手动导入大量包,创建空列表以记录指标,并不断读取/显示图像以进行调试。在下一节中,我们将了解作者为避免此类冗长代码而构建的库。
介绍 torch_snippets 库
您可能已经注意到,我们在几乎所有部分中都使用了相同的功能。一遍又一遍地编写相同的函数行是浪费我们的时间。为方便起见,本书的作者编写了一个名为 的 Python 库,以便我们的代码看起来简洁明了。 torch_snippets
读取图像、显示图像和整个训练循环等实用程序非常重复。我们希望通过将它们包装在最好是单个函数调用的代码中来避免一遍又一遍地编写相同的函数。例如,要读取彩色图像,我们不需要每次都写cv2.imread(...)后面跟着。cv2.cvtColor(...)相反,我们可以简单地调用read(...). 同样,对于plt.imshow(...),也有很多麻烦,包括图像的大小应该是最佳的,通道尺寸应该是最后的(记住 PyTorch 有它们)。这些将始终由单个函数来处理,show. read与and类似show,我们将在本书中使用超过 20 个便利函数和类。我们将使用torch_snippets从现在开始,以便更多地专注于实际的深度学习而不会分心。让我们通过使用这个库进行训练 来深入了解突出的功能,age-and-gender 以便我们可以学习使用这些功能并获得最大的收益。
1.安装并加载库:
-
!pip install torch_snippets
-
from torch_snippets
import *
该库一开始就允许我们加载所有重要的 Torch 模块和实用程序,例如 NumPy、pandas、Matplotlib、Glob、Os 等。
2.下载数据并创建数据集,如上一节所述。创建一个数据集类 ,GenderAgeClass并进行一些更改,这些更改在以下代码中以粗体显示:
-
class GenderAgeClass(Dataset):
-
...
-
def __getitem__(
self, ix):
-
...
-
age
= f.age
-
im
=
read(
file,
1)
-
return im, age, gen
-
-
def preprocess_image(
self, im):
-
im
= resize(im, IMAGE_
SIZE)
-
im
= torch.tensor(im).permute(
2,0,1)
-
...
在前面的代码块中,该行im = read(file, 1)被换行cv2.imread并cv2.COLOR_BGR2RGB成为一个函数调用。“1”代表“读取为彩色图像”,如果没有给出,将默认加载黑白图像。还有一个resize包装cv2.resize. 接下来,我们看一下show 函数。
3.指定训练和验证数据集并查看示例图像:
-
trn
= GenderAgeClass(trn_df)
-
val
= GenderAgeClass(val_df)
-
train_loader
= DataLoader(trn, batch_
size
=
32, shuffle
=
True, \
-
drop_
last
=
True, collate_fn
=trn.collate_fn)
-
test_loader
= DataLoader(val, batch_
size
=
32, \ collate_fn
-
= val.collate_fn)
-
-
im, gen, age
= trn[
0]
-
show(im, title
=f
'Gender: {gen}\nAge: {age}', sz
=
5)
当我们在整本书中处理图像时,将其包装成一个函数是有意义的。import matplotlib.pyplot as pltplt.imshow调用show(<2D/3D-Tensor>)将完全做到这一点。与 Matplotlib 不同的是,它可以绘制存在于 GPU 上的火炬阵列,而不管图像是否包含通道作为第一维或最后一维。关键字title将绘制带有图像的标题,关键字sz(size 的缩写)将根据传递的整数值绘制更大/更小的图像(如果未通过,sz将根据图像分辨率选择合理的默认值)。在对象检测章节中,我们也将使用相同的函数来显示边界框。查看help(show)更多论据。让我们在这里创建一些数据集并检查第一批图像及其目标。
4.创建数据加载器并检查张量。检查张量的数据类型、最小值、平均值、最大值和形状是一种常见的活动,它被包装为一个函数。它可以接受任意数量的张量输入:
-
train_loader
= DataLoader(trn, batch_
size
=
32, shuffle
=
True, \
-
drop_
last
=
True, collate_fn
=trn.collate_fn)
-
test_loader
= DataLoader(val, batch_
size
=
32, \
-
collate_fn
=val.collate_fn)
-
-
ims, gens, ages
=
next(iter(train_loader))
-
inspect(ims, gens, ages)
输出将inspect如下所示:
-
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
-
Tensor Shape: torch.
Size([
32,
3,
224,
224]) Min: -
2.118 Max:
2.640 Mean:
0.133 dtype: torch.float
32
-
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
-
Tensor Shape: torch.
Size([
32]) Min:
0.000 Max:
1.000 Mean:
0.594 dtype: torch.float
32
-
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
-
Tensor Shape: torch.
Size([
32]) Min:
0.087 Max:
0.925 Mean:
0.400 dtype: torch.float
32
-
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
5.像往常一样创建model, optimizer, loss_functions, train_batch, 和。validate_batch由于每个深度学习实验都是独一无二的,因此这一步没有任何包装函数。
6.最后,我们需要加载所有组件并开始训练。记录增加时期的指标。
这是一个高度重复的循环,所需的更改最少。我们将始终循环固定数量的 epoch,首先是训练数据加载器,然后是验证数据加载器。每次您必须创建空的指标列表并在训练/验证后跟踪它们时,都会使用train_batch或调用每个批次。validate_batch在一个时期结束时,您必须打印所有这些指标的平均值并重复该任务。知道每个 epoch /batch 将要训练多长时间(以秒为单位)也很有帮助。最后,在训练结束时,通常使用 绘制相同的指标matplotlib。所有这些都封装在一个名为Report. 它是一个 Python 类,有不同的理解方法。以下代码中的粗体部分突出显示的功能 Report:
-
model, criterion, optimizer
=
get_model()
-
n_epochs
=
5
-
log
=
Report(n_epochs)
-
for epoch
in range(n_epochs):
-
N
= len(train_loader)
-
for ix,
data
in enumerate(train_loader):
-
total_loss,gender_loss,age_loss
= train_batch(
data, \
-
model, optimizer, criterion)
-
log.
record(epoch
+(ix
+
1)
/N, trn_loss
=total_loss, \
-
end
=
'\r')
-
-
N
= len(
test_loader)
-
for ix,
data
in enumerate(
test_loader):
-
total_loss,gender_acc,age_mae
=
validate_batch(
data, \
-
model, criterion)
-
gender_acc
/
= len(
data[
0])
-
age_mae
/
= len(
data[
0])
-
log.
record(epoch
+(ix
+
1)
/N, val_loss
=total_loss, \
-
val_gender_acc
=gender_acc, \
-
val_age_mae
=age_mae,
end
=
'\r')
-
log.
report_avgs(epoch
+
1)
-
log.plot_epochs()
该类Report使用唯一的参数进行实例化,即要训练的 epoch 数,并在训练开始之前实例化。
在每个训练/验证步骤中,我们可以Report.record只使用一个位置参数调用该方法,该位置参数是我们所处的训练/验证位置(以批号表示)(通常是( epoch_number + (1+batch number)/(total_N_batches) )。在位置参数之后,我们传递一个一堆我们可以自由选择的关键字参数。如果需要捕获训练损失,关键字参数可以是trn_loss。在前面,我们记录了四个指标,trn_loss,val_loss,val_gender_acc和val_age_mae,而没有创建一个空列表。
它不仅会记录,还会在输出中打印相同的损失。作为结束参数的使用'\r'是一种特殊的说法,即下次要记录一组新的损失时替换该行。此外,Report将自动计算训练和验证的剩余时间并打印出来。
Report将记住何时记录指标并在Report.report_avgs调用函数时打印该时期的所有平均指标。这将是一个永久的印刷品。
最后,相同的平均指标在函数调用中绘制为折线图Report.plot_epochs,无需格式化(您也可以Report.plot用于绘制整个训练的每个批次指标,但这可能看起来很乱)。如果需要,相同的功能可以选择性地绘制指标。举个例子,在前面的例子中,如果您只对绘制trn_loss和val_loss指标感兴趣,这可以通过调用log.plot_epochs(['trn_loss, 'val_loss'])或什至简单地完成log.plot_epochs('_loss')。它将搜索与所有指标匹配的字符串,并找出我们要求的指标。
训练完成后,上述代码段的输出应如下所示: