深度学习进阶,多个输出和多个损失实现多标签分类
Posted AI浩
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深度学习进阶,多个输出和多个损失实现多标签分类相关的知识,希望对你有一定的参考价值。
Keras:多个输出和多个损失
几周前,我们讨论了如何使用 Keras 和深度学习执行多标签分类。 今天,我们将讨论一种更高级的技术,称为多输出分类。 那么,两者有什么区别呢?
在多标签分类中,您的网络在负责分类的网络末端只有一组全连接层(即“头”)。 但是在多输出分类中,您的网络至少分支两次(有时更多),在网络末端创建多组完全连接的头——然后您的网络可以为每个头预测一组类标签,从而有可能学习不相交的标签组合。 您甚至可以将多标签分类与多输出分类结合起来,这样每个全连接的头部都可以预测多个输出!
今天的教程来指导你使用 Keras 进行多输出分类,要了解如何通过 TensorFlow 和 Keras 使用多个输出和多个损失,请继续阅读!
在今天的博客文章中,我们将学习如何使用:
- 多重损失函数
- 多路输出
多输出深度学习数据集
数据集地址:链接:https://pan.baidu.com/s/1eANXTnWl2nf853IEiLOvWg
提取码:jo4h
我们的数据集由5547张图片组成,它们来自12个不同的种类,包括:
- black_dress(333张图片)
- black_jeans(344张图片)
- black_shirt(436张图片)
- black_shoe(534张图片)
- blue_dress(386张图片)
- blue_jeans(356张图片)
- blue_shirt(369张图片)
- red_dress(384张图片)
- red_shirt(332张图片)
- red_shoe(486张图片)
- white_bag(747张图片)
- white_shoe(840张图片)
我们的卷积神经网络的目标是同时预测颜色和服饰类别。代码使用Tensorflow2.0以上版本编写。下面对我实现算法的代码作讲解:
项目结构
$ tree --filelimit 10 --dirsfirst
.
├── dataset
│ ├── black_jeans [344 entries]
│ ├── black_shoes [358 entries]
│ ├── blue_dress [386 entries]
│ ├── blue_jeans [356 entries]
│ ├── blue_shirt [369 entries]
│ ├── red_dress [380 entries]
│ └── red_shirt [332 entries]
├── examples
│ ├── black_dress.jpg
│ ├── black_jeans.jpg
│ ├── blue_shoes.jpg
│ ├── red_shirt.jpg
│ └── red_shoes.jpg
├── output
│ ├── fashion.model
│ ├── category_lb.pickle
│ ├── color_lb.pickle
│ ├── output_accs.png
│ └── output_losses.png
├── model
│ ├── __init__.py
│ └── fashionnet.py
├── train.py
└── classify.py
在上面你可以找到我们的项目结构,但在我们继续之前,让我们先回顾一下内容。 有 3 个值得注意的 Python 文件:
- model/fashionnet.py :我们的多输出分类网络文件包含由三种方法组成的 FashionNet 架构类: build_category_branch 、 build_color_branch 和 build 。我们将在下一节详细回顾这些方法。
- train.py :此脚本将训练 FashionNet 模型并在此过程中生成输出文件夹中的所有文件。
- category.py :此脚本加载我们训练好的网络并使用多输出分类对示例图像进行分类。
我们还有 4 个顶级目录:
- dataset/ :我们的时尚数据集,是使用他们的 API 从 Bing Image Search 中抓取的。我们在上一节中介绍了数据集。要以与我相同的方式创建您自己的数据集,请参阅如何(快速)构建深度学习图像数据集。
- examples/ :我们有一些示例图像,我们将在本博文的最后一节中将它们与我们的分类.py 脚本结合使用。
- output/ :我们的 train.py 脚本生成了一些输出文件:
- fashion.model :我们的序列化 Keras 模型。
- category_lb.pickle :服装类别的序列化 LabelBinarizer 对象由 scikit-learn 生成。这个文件可以通过我们的classify.py 脚本加载(并调用标签
- color_lb.pickle :颜色的 LabelBinarizer 对象。
- output_accs.png :精度训练图图像。
- output_losses.png :损失训练图图像。
- model/ :这是一个包含 FashionNet 类的 Python 模块。
快速回顾我们的多输出 Keras 架构
为了使用 Keras 执行多输出预测,我们将实现一个名为 FashionNet 的特殊网络架构(我为这篇博文而创建)。
FashionNet 架构包含两个特殊组件,包括:
- 网络早期的一个分支,将网络分成两个“子网络”——一个负责服装类型分类,另一个负责颜色分类。
- 网络末端的两个(不相交)全连接头,每个头负责各自的分类职责。
在我们开始实现 FashionNet 之前,让我们可视化这些组件中的每一个,第一个是分支:
在这个网络架构图中,您可以看到我们的网络接受 96 x 96 x 3 的输入图像。
然后我们立即创建两个分支:
- 左边的分支负责对服装类别进行分类。
- 右侧的分支处理颜色分类。
每个分支执行其各自的一组卷积、激活、批量归一化、池化和 dropout 操作,直到我们达到最终输出:
图 5:我们的深度学习 Keras 多输出分类网络可以学习不相交的标签组合。
请注意这些全连接 (FC) 头集如何与我们在本博客中研究过的其他架构中的 FC 层相似——但现在有两个,每个都负责其给定的分类任务。
网络右侧的分支明显比左侧分支浅(没有那么深)。 预测颜色比预测服装类别要容易得多,因此颜色分支相对较浅。
为了了解我们如何实现这样的架构,让我们继续下一节。
实施我们的“FashionNet”架构
图 6:Keras 深度学习库具有执行多输出分类所需的所有功能。
新建fashionnet.py 在里面增加:
# import the necessary packages
from tensorflow.keras.models import Model
from tensorflow.keras.layers import BatchNormalization
from tensorflow.keras.layers import Conv2D
from tensorflow.keras.layers import MaxPooling2D
from tensorflow.keras.layers import Activation
from tensorflow.keras.layers import Dropout
from tensorflow.keras.layers import Lambda
from tensorflow.keras.layers import Dense
from tensorflow.keras.layers import Flatten
from tensorflow.keras.layers import Input
import tensorflow as tf
我们首先从 Keras 库导入模块,然后导入 TensorFlow 本身。
由于我们的网络由两个子网络组成,我们将定义两个函数来负责构建各自的分支。
第一个, build_category_branch ,用于分类服装类型,定义如下:
class FashionNet:
@staticmethod
def build_category_branch(inputs, numCategories,
finalAct="softmax", chanDim=-1):
# utilize a lambda layer to convert the 3 channel input to a
# grayscale representation
x = Lambda(lambda c: tf.image.rgb_to_grayscale(c))(inputs)
# CONV => RELU => POOL
x = Conv2D(32, (3, 3), padding="same")(x)
x = Activation("relu")(x)
x = BatchNormalization(axis=chanDim)(x)
x = MaxPooling2D(pool_size=(3, 3))(x)
x = Dropout(0.25)(x)
build_category_branch 具有三个值得注意的参数:
- 输入:我们类别分支子网络的输入量。
- numCategories :类别的数量,例如“连衣裙”、“鞋子”、“牛仔裤”、“衬衫”等。
- finalAct :最终激活层类型,默认为 softmax 分类器。如果您同时执行多输出和多标签分类,您可能希望将此激活更改为 sigmoid。
我们使用 Lambda 层将图像从 RGB 转换为灰度。
为什么要这样做? 嗯,不管是红色、蓝色、绿色、黑色还是紫色,裙子都是裙子,对吧? 因此,我们决定丢弃任何颜色信息,转而关注图像中的实际结构成分,确保我们的网络不会学习将特定颜色与服装类型联合关联。
然后我们继续构建我们的 CONV => RELU => POOL 块和 dropout。请注意,我们使用的是 TensorFlow/Keras 的函数式 API;我们需要功能性 API 来创建我们的分支网络结构。
我们的第一个 CONV 层有 32 个滤波器,带有 3 x 3 内核和 RELU 激活(整流线性单元)。我们应用批量归一化、最大池化和 25% dropout。 Dropout是将节点从当前层随机断开到下一层的过程。这种随机断开的过程自然有助于网络减少过度拟合,因为层中没有一个节点负责预测某个类、对象、边缘或角。
接下来是我们的两组 (CONV => RELU) * 2 => POOL 块:
# (CONV => RELU) * 2 => POOL
x = Conv2D(64, (3, 3), padding="same")(x)
x = Activation("relu")(x)
x = BatchNormalization(axis=chanDim)(x)
x = Conv2D(64, (3, 3), padding="same")(x)
x = Activation("relu")(x)
x = BatchNormalization(axis=chanDim)(x)
x = MaxPooling2D(pool_size=(2, 2))(x)
x = Dropout(0.25)(x)
# (CONV => RELU) * 2 => POOL
x = Conv2D(128, (3, 3), padding="same")(x)
x = Activation("relu")(x)
x = BatchNormalization(axis=chanDim)(x)
x = Conv2D(128, (3, 3), padding="same")(x)
x = Activation("relu")(x)
x = BatchNormalization(axis=chanDim)(x)
x = MaxPooling2D(pool_size=(2, 2))(x)
x = Dropout(0.25)(x)
此代码块中过滤器、内核和池大小的变化协同工作,以逐渐减小空间大小但增加深度。
让我们将它与 FC => RELU 层结合起来:
# define a branch of output layers for the number of different
# clothing categories (i.e., shirts, jeans, dresses, etc.)
x = Flatten()(x)
x = Dense(256)(x)
x = Activation("relu")(x)
x = BatchNormalization()(x)
x = Dropout(0.5)(x)
x = Dense(numCategories)(x)
x = Activation(finalAct, name="category_output")(x)
# return the category prediction sub-network
return x
最后一个激活层是完全连接的,并且具有与我们的 numCategories 相同数量的神经元/输出。
请注意,我们将最终激活层命名为“category_output”。这很重要,因为我们稍后将在 train.py 中按名称引用该层。
让我们定义用于构建多输出分类网络的第二个函数。 这个名为 build_color_branch ,顾名思义,它负责对图像中的颜色进行分类:
@staticmethod
def build_color_branch(inputs, numColors, finalAct="softmax",
chanDim=-1):
# CONV => RELU => POOL
x = Conv2D(16, (3, 3), padding="same")(inputs)
x = Activation("relu")(x)
x = BatchNormalization(axis=chanDim)(x)
x = MaxPooling2D(pool_size=(3, 3))(x)
x = Dropout(0.25)(x)
# CONV => RELU => POOL
x = Conv2D(32, (3, 3), padding="same")(x)
x = Activation("relu")(x)
x = BatchNormalization(axis=chanDim)(x)
x = MaxPooling2D(pool_size=(2, 2))(x)
x = Dropout(0.25)(x)
# CONV => RELU => POOL
x = Conv2D(32, (3, 3), padding="same")(x)
x = Activation("relu")(x)
x = BatchNormalization(axis=chanDim)(x)
x = MaxPooling2D(pool_size=(2, 2))(x)
x = Dropout(0.25)(x)
我们的 build_color_branch 参数与 build_category_branch 基本相同。
我们用 numColors (不同于 numCategories )来区分最后一层的激活次数。
这一次,我们不会应用 Lambda 灰度转换层,因为我们实际上关心的是网络这个区域的颜色。 如果我们转换为灰度,我们将丢失所有颜色信息!
网络的这个分支比服装类别分支浅得多,因为手头的任务要简单得多。 我们要求子网络完成的只是对颜色进行分类——子网络不必那么深。
就像我们的类别分支一样,我们有第二个完全连接的头部。 让我们构建 FC => RELU 块来完成:
# define a branch of output layers for the number of different
# colors (i.e., red, black, blue, etc.)
x = Flatten()(x)
x = Dense(128)(x)
x = Activation("relu")(x)
x = BatchNormalization()(x)
x = Dropout(0.5)(x)
x = Dense(numColors)(x)
x = Activation(finalAct, name="color_output")(x)
# return the color prediction sub-network
return x
为了区分颜色分支的最终激活层,我提供了 name=“color_output” 关键字参数。我们将在训练脚本中引用名称。 我们构建 FashionNet 的最后一步是将我们的两个分支放在一起并构建最终架构:
@staticmethod
def build(width, height, numCategories, numColors,
finalAct="softmax"):
# initialize the input shape and channel dimension (this code
# assumes you are using TensorFlow which utilizes channels
# last ordering)
inputShape = (height, width, 3)
chanDim = -1
# construct both the "category" and "color" sub-networks
inputs = Input(shape=inputShape)
categoryBranch = FashionNet.build_category_branch(inputs,
numCategories, finalAct=finalAct, chanDim=chanDim)
colorBranch = FashionNet.build_color_branch(inputs,
numColors, finalAct=finalAct, chanDim=chanDim)
# create the model using our input (the batch of images) and
# two separate outputs -- one for the clothing category
# branch and another for the color branch, respectively
model = Model(
inputs=inputs,
outputs=[categoryBranch, colorBranch],
name="fashionnet")
# return the constructed network architecture
return model
定义build函数,有5个参数。 build 函数假设我们使用的是 TensorFlow 和最后一次排序的通道。
inputShape 元组是明确排序的 (height, width, 3) ,其中 3 代表 RGB 通道。 如果您想使用 TensorFlow 以外的后端,您需要修改代码以:(1) 正确地为您的后端设置正确的通道顺序,以及 (2) 实现一个自定义层来处理 RGB 到灰度的转换。 从那里,我们定义了网络的两个分支,然后将它们放在一个模型中。 关键是我们的分支有一个共同的输入,但有两个不同的输出(服装类型和颜色分类)。
实现多输出和多损失训练脚本
现在我们已经实现了我们的 FashionNet 架构,让我们训练它! 准备好后,打开 train.py 并深入研究:
# set the matplotlib backend so figures can be saved in the background
import matplotlib
matplotlib.use("Agg")
# import the necessary packages
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.preprocessing.image import img_to_array
from sklearn.preprocessing import LabelBinarizer
from sklearn.model_selection import train_test_split
from model.fashionnet import FashionNet
from imutils import paths
import matplotlib.pyplot as plt
import numpy as np
import argparse
import random
import pickle
import cv2
import os
我们首先为脚本导入必要的包。
从那里我们解析我们的命令行参数:
# construct the argument parser and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-d", "--dataset", required=True,
help="path to input dataset (i.e., directory of images)")
ap.add_argument("-m", "--model", required=True,
help="path to output model")
ap.add_argument("-l", "--categorybin", required=True,
help="path to output category label binarizer")
ap.add_argument("-c", "--colorbin", required=True,
help="path to output color label binarizer")
ap.add_argument("-p", "--plot", type=str, default="output",
help="base filename for generated plots")
args = vars(ap.parse_args())
我们很快就会看到如何运行训练脚本。 现在,只知道 --dataset 是我们数据集的输入文件路径, --model 、 --categorybin 、 --colorbin 都是三个输出文件路径。
或者,您可以使用 --plot 参数为生成的精度/损失图指定基本文件名。
现在,让我们建立四个重要的训练变量:
# initialize the number of epochs to train for, initial learning rate,
# batch size, and image dimensions
EPOCHS = 50
INIT_LR = 1e-3
BS = 32
IMAGE_DIMS = (96, 96, 3)
我们设置以下变量:
- EPOCHS : epoch 数设置为 50 。通过实验,我发现 50 个 epoch 生成的模型具有低损失并且没有过拟合到训练集(或尽可能不过拟合)。
- INIT_LR :我们的初始学习率设置为 0.001 。学习率控制着我们沿着梯度所做的“步骤”。较小的值表示较小的步长,较大的值表示较大的步长。我们很快就会看到我们将使用 Adam 优化器,同时随着时间的推移逐渐降低学习率。
- BS:我们将以 32 的批量大小训练我们的网络。
- IMAGE_DIMS :所有输入图像都将调整为 96 x 96,具有 3 个通道 (RGB)。我们正在使用这些维度进行训练,我们的网络架构输入维度也反映了这些维度。当我们在后面的部分中使用示例图像测试我们的网络时,测试维度必须与训练维度匹配。
我们的下一步是抓取我们的图像路径并随机打乱它们。我们还将初始化列表以分别保存图像本身以及服装类别和颜色:
# grab the image paths and randomly shuffle them
print("[INFO] loading images...")
imagePaths = sorted(list(paths.list_images(args["dataset"])))
random.seed(42)
random.shuffle(imagePaths)
# initialize the data, clothing category labels (i.e., shirts, jeans,
# dresses, etc.) along with the color labels (i.e., red, blue, etc.)
data = []
categoryLabels = []
colorLabels = []
随后,我们将遍历 imagePaths 、预处理并填充 data 、 categoryLabels 和 colorLabels 列表:
# loop over the input images
for imagePath in imagePaths:
# load the image, pre-process it, and store it in the data list
image = cv2.imread(imagePath)
image = cv2.resize(image, (IMAGE_DIMS[1], IMAGE_DIMS[0]))
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
image = img_to_array(image)
data.append(image)
# extract the clothing color and category from the path and
# update the respective lists
(color, cat) = imagePath.split(os.path.sep)[-2].split("_")
categoryLabels.append(cat)
colorLabels.append(color)
我们开始遍历我们的 imagePaths。
在循环内部,我们加载图像并将其调整为 IMAGE_DIMS 。 我们还将图像从 BGR 排序转换为 RGB。 我们为什么要进行这种转换? 回想一下我们在 build_category_branch 函数中的 FashionNet 类,我们在 Lambda 函数/层中使用了 TensorFlow 的 rgb_to_grayscale 转换。 因此,我们首先转换为 RGB,并最终将预处理后的图像附加到数据列表中。
接下来,仍然在循环内部,我们从当前图像所在的目录名称中提取颜色和类别标签。
要查看此操作,只需在终端中启动 Python,并提供一个示例 imagePath 进行实验,如下所示:
$ python
>>> import os
>>> imagePath = "dataset/red_dress/00000000.jpg"
>>> (color, cat) = imagePath.split(os.path.sep)[-2].split("_")
>>> color
'red'
>>> cat
'dress'
您当然可以按您希望的任何方式组织您的目录结构(但您必须修改代码)。 我最喜欢的两种方法包括 (1) 为每个标签使用子目录或 (2) 将所有图像存储在单个目录中,然后创建 CSV 或 JSON 文件以将图像文件名映射到它们的标签。 让我们将三个列表转换为 NumPy 数组,对标签进行二值化,并将数据划分为训练和测试分割:
# scale the raw pixel intensities to the range [0, 1] and convert to
# a NumPy array
data = np.array(data, dtype="float") / 255.0
print("[INFO] data matrix: {} images ({:.2f}MB)".format(
len(imagePaths), data.nbytes / (1024 * 1000.0)))
# convert the label lists to NumPy arrays prior to binarization
categoryLabels = np.array(categoryLabels)
colorLabels = np.array(colorLabels)
# binarize both sets of labels
print("[INFO] binarizing labels...")
categoryLB = LabelBinarizer()
colorLB = LabelBinarizer()
categoryLabels = categoryLB.fit_transform(categoryLabels)
colorLabels = colorLB.fit_transform(colorLabels)
# 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, categoryLabels, colorLabels,
test_size=0.2, random_state=42)
(trainX, testX, trainCategoryY, testCategoryY,
trainColorY, testColorY) = split
我们的最后一个预处理步骤——转换为 NumPy 数组并将原始像素强度缩放为 [0, 1]。
我们还将 categoryLabels 和 colorLabels 转换为 NumPy 数组。 这是必要的,因为在我们的下一个中,我们将使用我们之前导入的 scikit-learn 的 LabelBinarizer对标签进行二值化。 由于我们的网络有两个独立的分支,我们可以使用两个独立的标签二值化器——这与我们使用 MultiLabelBinarizer(也来自 scikit-learn)的多标签分类不同。
接下来,我们对数据集执行典型的 80% 训练/20% 测试拆分。
让我们构建网络,定义我们的独立损失,并编译我们的模型:
# initialize our FashionNet multi-output network
model = FashionNet.build(96, 96,
numCategories=len(categoryLB.classes_),
numColors=len(colorLB.classes_),
finalAct="softmax")
# define two dictionaries: one that specifies the loss method for
# each output of the network along with a second dictionary that
# specifies the weight per loss
losses = {
"category_output": "categorical_crossentropy",
"color_output": "categorical_crossentropy",
}
lossWeights = {"category_output": 1.0, "color_output": 1.0}
# initialize the optimizer and compile the model
print("[INFO] compiling model...")
opt = Adam(lr=INIT_LR, decay=INIT_LR / EPOCHS)
model.compile(optimizer=opt, loss=losses, loss_weights=lossWeights,
metrics=["accuracy"])
实例化多输出 FashionNet 模型。我们在创建 FashionNet 类并在其中构建函数时剖析了参数,因此请务必查看我们在此处实际提供的值。
接下来,我们需要为每个完全连接的头部定义两个损失。
定义多个损失是通过使用每个分支激活层的名称的字典来完成的——这就是我们在 FashionNet 实现中命名输出层的原因!每个损失都将使用分类交叉熵,这是训练网络进行大于 2 类分类时使用的标准损失方法。
我们还在单独字典(具有相同值的同名键)中定义了相等的 lossWeights。在您的特定应用程序中,您可能希望对一个损失进行比另一个更重的加权。
现在我们已经实例化了我们的模型并创建了 loss + lossWeights 字典,让我们用学习率衰减初始化 Adam 优化器并编译我们的模型。
我们的下一个块只是开始训练过程:
以上是关于深度学习进阶,多个输出和多个损失实现多标签分类的主要内容,如果未能解决你的问题,请参考以下文章