“读取代码的次数多于写入代码的次数。”+ +牢记前面的引用,向您展示了如何使用各种 API 实现相同的模型。 任何最新算法实现的开放源代码都将是这些 API 的组合。 接下来,我们将从 Keras 实现开始。 + +# 准备数据集 + +Keras 提供`MNIST`数据。 首先,导入`tensorflow`。 然后定义一些常量,例如批处理大小,类和时期数。 可以根据计算机上可用的 RAM 选择批次大小。 批处理大小越大,所需的 RAM 越多。 批次大小对准确性的影响很小。 此处的类数等于 10,并且针对不同的问题而有所不同。 时期的数量决定了训练必须经过整个数据集的次数。 如果在所有时期结束时减少损失,则可以将其设置为较高的数字。 在某些情况下,训练时间较长可以提高准确性。 现在让我们看一下创建数据集的步骤: + +1. 设置输入图像的尺寸,如下所示: + +```py + batch_size = 128 + no_classes = 10 + epochs = 2 + image_height, image_width = 28, 28 +``` + +2. 使用 Keras 实用程序将数据从磁盘加载到内存: + +```py + (x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data() +``` + +3. 将向量重塑为图像格式,并使用给定的代码定义卷积的输入尺寸: + +```py + x_train = x_train.reshape(x_train.shape[0], image_height, image_width, 1) + x_test = x_test.reshape(x_test.shape[0], image_height, image_width, 1) + input_shape = (image_height, image_width, 1) +``` + +4. 如下将数据类型转换为`float`: + +```py + x_train = x_train.astype('float32') + x_test = x_test.astype('float32') +``` + +5. 通过减去数据均值来归一化数据: + +```py + x_train /= 255 + x_test /= 255 +``` + +6. 将分类标签转换为一次性编码: + +```py + y_train = tf.keras.utils.to_categorical(y_train, no_classes) + y_test = tf.keras.utils.to_categorical(y_test, no_classes) +``` + +这与 TensorFlow 编写代码的方式非常不同。 数据已加载到内存中,此处`Placeholders`的概念均不存在。 + +# 建立模型 + +在本节中,我们将使用一些卷积层,然后是全连接层,以训练前面的数据集。 构造一个简单的顺序模型,该模型具有两个卷积层,然后是池化层,丢失层和密集层。 顺序模型具有`add`方法,可以将多个层堆叠在一起。 第一层具有 64 个过滤器,第二层具有 128 个过滤器。 所有过滤器的内核大小均为 3。 在卷积层之后应用最大池。 卷积层的输出被展平,并通过丢包连接连接到一对完全连接的层。 + +最后一层连接到 softmax,因为这是一个多类分类问题。 以下代码显示了如何定义模型: + +```py +def simple_cnn(input_shape): + model = tf.keras.models.Sequential() + model.add(tf.keras.layers.Conv2D( + filters=64, + kernel_size=(3, 3), + activation='relu', + input_shape=input_shape + )) + model.add(tf.keras.layers.Conv2D( + filters=128, + kernel_size=(3, 3), + activation='relu' + )) + model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2))) + model.add(tf.keras.layers.Dropout(rate=0.3)) + model.add(tf.keras.layers.Flatten()) + model.add(tf.keras.layers.Dense(units=1024, activation='relu')) + model.add(tf.keras.layers.Dropout(rate=0.3)) + model.add(tf.keras.layers.Dense(units=no_classes, activation='softmax')) + model.compile(loss=tf.keras.losses.categorical_crossentropy, + optimizer=tf.keras.optimizers.Adam(), + metrics=['accuracy']) + return model +simple_cnn_model = simple_cnn(input_shape) +``` + +该模型刚刚定义,必须进行编译。 在编译丢失期间,必须定义优化器和指标。 损失将是交叉熵,并通过 Adam 算法进行了优化,我们将以准确性作为度量标准。 使用加载的数据,训练和评估数据。 使用训练参数加载训练数据并拟合模型: + +```py +simple_cnn_model.fit(x_train, y_train, batch_size, epochs, (x_test, y_test)) +train_loss, train_accuracy = simple_cnn_model.evaluate( + x_train, y_train, verbose=0) +print('Train data loss:', train_loss) +print('Train data accuracy:', train_accuracy) +``` + +使用 Keras A PI 时,不会创建会话。 然后按以下方式评估测试数据: + +```py +test_loss, test_accuracy = simple_cnn_model.evaluate( + x_test, y_test, verbose=0) +print('Test data loss:', test_loss) +print('Test data accuracy:', test_accuracy) +``` + +评估也可以在不显式创建会话的情况下创建。 运行完成后,结果应类似于以下内容: + +```py +Loss for train data: 0.0171295607952 +Accuracy of train data: 0.995016666667 +Loss for test data: 0.0282736890309 +Accuracy of test data: 0.9902 +``` + +这样可以使测试数据的准确度达到 99%。 请注意,训练精度高于测试数据,并且始终打印它们都是一个好习惯。 精度的差异是由于迭代次数造成的。 由于数据集的差异,准确性比 TensorFlow 中创建的先前模型要高一些。 + +# 其他流行的图像测试数据集 + +`MNIST`数据集是用于测试算法的最常用数据集。 但是还有其他数据集可用于测试图像分类算法。 + +# CIFAR 数据集 + +加拿大高级研究机构**( **CIFAR** )-10 数据集包含 60,000 张图像,其中 50,000 张图像用于训练,10,000 张图像用于测试。 类的数量是 10。图像尺寸是 32 像素 x 32 像素。 以下是从每个类别中随机选择的图像:** + +`![](img/50a4ca9c-f6dc-413c-9cb1-1691ccc0680c.png)` + +这些图像很小,仅包含一个对象。 `CIFAR-100`数据集包含相同数量的图像,但具有 100 个类别。 因此,每个类别只有 600 张图像。 每个图像都带有一个超级标签和一个精美标签。 如果您想进行实验,可以在`tf.keras.datasets`上找到此数据集。 + +# Fashion-MNIST 数据集 + +`Fashion-MNIST`是替代`MNIST`数据集而创建的数据集。 创建为`MNIST`的此数据集被认为太简单了,可以直接用`MNIST`代替。 + +以下是在执行**主成分分析**( **PCA** )之后从数据集中随机选择的示例: + +![](img/da904c50-2447-4537-833a-d31218cbfe5a.png) + +数据集大小,标签数量和图像大小类似于`MNIST`。 可以在 [https://github.com/zalandoresearch/fashion-mnist](https://github.com/zalandoresearch/fashion-mnist) 上找到更多详细信息。 您可以运行先前学习的模型并检查准确性。 + +# ImageNet 数据集和竞争 + +ImageNet 是具有 14,197,122 图像,21,841 个同义词集索引的计算机视觉数据集。 同义词集是 WordNet 层次结构中的一个节点,而节点又是一组同义词。 每年都会举办一次比赛,其中有 1000 个此类数据集。 它已成为评估图像分类算法性能的标准基准。 + +在 2013 年,基于深度学习的计算机视觉模型获得了第一名。 从那时起,只有深度学习模型赢得了竞争。 以下是多年来在比赛中排名前五位的错误率: + +![](img/06f1198b-fb2d-44ab-baed-b2cff9e113ed.png) + +您会注意到,多年来精度以及层的深度一直在增加。 接下来,我们将了解该图中存在的模型。 + +# 更大的深度学习模型 + +我们将审视几种模型定义,这些模型定义在 ImageNet 竞赛中取得了最新的成果。 我们将在以下主题中单独研究它们。 + +# AlexNet 模型 + +**AlexNet** 是第一本引起人们对计算机视觉深度学习的广泛兴趣的出版物。 Krizhevsky 等。 ( [https://papers.nips.cc/paper/4824-imagenet-classification-with-deep-convolutional-neural-networks.pdf](https://papers.nips.cc/paper/4824-imagenet-classification-with-deep-convolutional-neural-networks.pdf) )提出了 AlexNet,它一直是先驱和影响力 在这个领域里。 该模型赢得了 ImageNet 2013 挑战。 错误率是 15.4%,明显优于下一个。 该模型是具有五个卷积层的相对简单的体系结构。 面临的挑战是对 1,000 种对象进行分类。 图像和数据包含 1500 万条带注释的图像,其中包含 22,000 多个类别。 其中,只有 1,000 个类别用于比赛。 AlexNet 使用 ReLU 作为激活功能,发现它的训练速度比其他激活功能快几倍。 该模型的架构如下所示: + +![](img/ee056ac4-3d0d-47df-ac96-5e2f6988c91b.png) + +复制自 Krizhevsky 等人。 + +本文还使用了数据增强技术,例如图像翻译,水平翻转和随机裁剪。 漏失层防止过度拟合 。 该模型使用香草**随机梯度下降**( **SGD** )进行训练。 仔细选择 SGD 的参数进行训练。 学习率在一组固定的训练迭代中变化。 动量和重量衰减采用固定值进行训练。 本文介绍了一种称为**本地响应规范化** ( **LRN** )的概念。 LRN 层对滤镜上的每个像素进行归一化,以避免在特定滤镜中发生巨大的激活。 + +不再使用该层,因为最近的研究表明,由于 LRN,没有太大的改进。 AlexNet 总共有 6000 万个参数。 + +# VGG-16 模型 + +**VGG** 模型代表牛津大学的, **视觉几何组** 。 该模型非常简单,并且比 AlexNet 具有更大的深度。 该纸有两个模型,深度分别为 16 和 19 层。 所有的 CNN 层都使用 3 x 3 步幅的滤镜和 1 尺寸的垫,以及 2 步幅的最大合并尺寸 2。这导致参数数量减少。 尽管由于最大池化而减小了大小,但过滤器的数量却随着层的增加而增加。 16 层深度模型的体系结构如下: + +![](img/776596c6-d2f7-4656-bb91-85d84253f33c.png) + +该模型具有 1.38 亿个参数,是此处描述的所有模型中最大的。 但是参数的一致性很好。 其特征是,随着网络的深入,图像的尺寸越小,滤波器的数量就越多。 所使用的数据增强技术之一是规模抖动。 比例抖动是一种增强技术,其中具有随机大小的一侧被认为会改变比例。 + +# Google Inception-V3 模型 + +**Inception-V3** 是 Szegedy 等人提出的。 ( [https://arxiv.org/pdf/1409.4842.pdf](https://arxiv.org/pdf/1409.4842.pdf) ),并介绍了具有更好泛化方法的初始概念。 该架构在 2014 年赢得了 ImageNet 竞赛的冠军。它旨在提高速度和尺寸的效率。 它的参数比 AlexNet 小 12 倍。 初始阶段是构建宏体系结构的微体系结构。 每个隐藏层都有一个较高级别的图像表示。 在每一层,我们可以选择使用池化或其他层。 初始使用多个内核,而不是使用一种类型的内核。 平均池之后是各种大小的卷积,然后将它们合并在一起。 + +可以基于数据学习内核参数。 使用多个内核,该模型可以检测较小的特征以及较高的抽象度。 1 x 1 卷积将减少特征,从而减少计算量。 这将在推理过程中占用较少的 RAM。 以下是最简单形式的启动模块,其中包含具有各种内核大小和池化的卷积选项: + +![](img/e38ca5d2-f9c5-4581-b5be-1a5a30c5d57b.png) + +请注意,与 AlexNet 或 VGG 相反,操作是并行进行的。 输出量巨大,因此引入了 1 x 1 的滤波器以降低尺寸。 将缩小的尺寸添加到体系结构后,它将变为: + +![](img/cc7e385f-0826-4d07-99d7-9116a649e402.png) + +该模型的整个体系结构如下,包括所有的风吹草动: + +![](img/bef2324f-3a22-4f92-8af9-a6e90300e66c.png) + +该图说明了 Google Inception V3 模型架构[经 Szegedy 等人的许可复制] + +有 9 个初始模块,共 100 层,它们具有良好的性能。 + +# Microsoft ResNet-50 模型 + +**ResNet** 是 He 等人提出的。 ( [https://arxiv.org/pdf/1512.03385.pdf](https://arxiv.org/pdf/1512.03385.pdf) ),并在 2015 年赢得了 ImageNet 竞赛。此方法表明可以训练更深的网络。 网络越深,精度变得越饱和。 这甚至不是由于过拟合或由于存在大量参数,而是由于减少了训练误差。 这是由于无法反向传播梯度。 可以通过以下方法将梯度直接发送到带有残差块的更深层来克服: + +![](img/ff1c06f1-c254-4602-9b50-91cf9402df05.png) + +每两层相连,形成一个残留块。 您可以看到训练是在各层之间传递的。 通过这种技术,反向传播会将错误带到较早的层。 + +可以从 [https://github.com/tensorflow/tensorflow/tree/r1.4/tensorflow/python/keras/_impl/keras/applications](https://github.com/tensorflow/tensorflow/tree/r1.4/tensorflow/python/keras/_impl/keras/applications) 使用模型定义。 定义了模型中的每一层,并且`ImageNet`数据集上的预训练权重可用。 + +# SqueezeNet 模型 + +Iandola 等人介绍了 **SqueezeNet** 模型。 ( [https://arxiv.org/pdf/1602.07360.pdf](https://arxiv.org/pdf/1602.07360.pdf) ),以减少模型尺寸和参数数量。 + +通过使用 1 x 1 过滤器替换 3 x 3 过滤器,使网络变得更小,如下所示: + +![](img/61941eba-0b03-4a6d-93ce-69132bedbe96.png) + +经 Iandola 等人许可复制。 + +3 x 3 滤波器的输入数量也减少了在较高级别发生时各层的下采样,从而提供了较大的激活图: + +![](img/12771464-5c49-48ae-b9e4-f512ed8f42aa.png) + +经 Iandola 许可复制 等。 + +# 空间变压器网络 + +Jaderberg 等人提出的**空间变压器网络**。 ( [https://arxiv.org/pdf/1506.02025.pdf](https://arxiv.org/pdf/1506.02025.pdf) )尝试在传递到 CNN 之前对图像进行转换。 这与其他网络不同,因为它尝试在卷积之前修改图像。 该网络学习参数以变换图像。 学习用于 **仿射变换**的参数。 通过应用仿射变换,可以实现 **空间不变性** 。 在以前的网络中,空间不变性是通过最大池化层实现的。 空间变压器网络的位置如下所示: + +![](img/177e14e5-4bc8-4b5c-9e0f-754188739941.png) + +经 Jaderberg 许可复制 等。 + +# DenseNet 模型 + +DenseNet 是 Huang 等人提出的 ResNet 的扩展。 ( [https://arxiv.org/pdf/1608.06993.pdf](https://arxiv.org/pdf/1608.06993.pdf) )。 在 ResNet 块中,上一层通过求和合并到下一层。 在 DenseNet 中,上一层通过串联合并到下一层。 DenseNet 将所有层连接到上一层,将当前层连接到下一层。 + +在下图中,可以看出要素图是如何作为输入提供给其他层的: + +![](img/2ff8e417-48ae-491e-9abc-78bccdcdc61a.png) + +经 Huang 许可复制 等。 + +这样,它提供了多个优点,例如更平滑的渐变,特征变换等。 这也减少了参数的数量: + +![](img/feec4f72-5ef2-4eb0-a647-21f56d254fef.png) + +经 Huang 许可复制 等。 + +我们已经介绍了图像分类任务的所有最新算法。 任何体系结构均可用于图像分类任务。 在下一节中,我们将看到如何使用这些先进的体系结构训练模型来预测宠物,并提高准确性。 + +# 训练猫与狗的模型 + +在本部分中,我们将准备和训练用于预测猫与狗的模型,并了解一些可提高准确性的技术。 大多数图像分类问题都属于这种范例。 本节介绍的技术,例如扩充和迁移学习,对于一些问题很有用。 + +# 准备数据 + +为了进行分类,我们将从 **kaggle** 下载数据并以适当的格式存储。 注册并登录 [www.kaggle.com](http://www.kaggle.com) 并转到 [https://www.kaggle.com/c/dogs-vs-cats/data](https://www.kaggle.com/c/dogs-vs-cats/data) 。 从该页面下载`train.zip`和`test1.zip`文件。 `train.zip`文件包含 25,000 张宠物数据图像。 我们将仅使用部分数据来训练模型。 具有更多计算能力的读者,例如**图形处理单元** ( **GPU** ),可以使用比建议的更多的数据。 运行以下脚本以重新排列图像并创建必要的文件夹: + +```py +import os +import shutil + +work_dir = '' # give your correct directory +image_names = sorted(os.listdir(os.path.join(work_dir, 'train'))) + +def copy_files(prefix_str, range_start, range_end, target_dir): + image_paths = [os.path.join(work_dir, 'train', prefix_str + '.' + str(i) + '.jpg') + for i in range(range_start, range_end)] + dest_dir = os.path.join(work_dir, 'data', target_dir, prefix_str) + os.makedirs(dest_dir) + for image_path in image_paths: + shutil.copy(image_path, dest_dir) + +copy_files('dog', 0, 1000, 'train') +copy_files('cat', 0, 1000, 'train') +copy_files('dog', 1000, 1400, 'test') +copy_files('cat', 1000, 1400, 'test') +``` + +对于我们的实验,我们将仅使用 1000 张猫和狗的图像。 因此,将图像 0–999 从下载的文件夹复制到`cats`下新创建的`train` 文件夹。 同样,将 1,000–1,400 复制到`data/test/cat`,将`train/dogs`中的 1 0–999 和`data/test/dog`中的 1,000–1,400 复制,这样我们每个班级都有 1,000 个训练示例 每个类别 400 个验证示例。 + +# 使用简单的 CNN 进行基准测试 + +让我们在该数据集上运行先前的`simple_cnn`模型,并查看其性能。 该模型的性能将成为我们判断其他技术的基本基准。 我们将为数据加载和训练定义一些变量,如下所示: + +```py +image_height, image_width = 150, 150 +train_dir = os.path.join(work_dir, 'train') +test_dir = os.path.join(work_dir, 'test') +no_classes = 2 +no_validation = 800 +epochs = 2 +batch_size = 200 +no_train = 2000 +no_test = 800 +input_shape = (image_height, image_width, 3) +epoch_steps = no_train // batch_size +test_steps = no_test // batch_size +``` + +该常数用于本节中的训练猫和狗模型的讨论中讨论的技术。 在这里,我们正在使用 2,800 张图像进行训练和测试,这对于个人计算机的 RAM 是合理的。 但这对于更大的数据集是不可持续的。 最好一次只加载一批图像进行培训和测试。 为此,`tf.keras`具有称为`ImageDataGenerator`的类,可在必要时读取图像。 假定从上一节中导入了`simple_cnn`模型。 以下是使用生成器加载图像的示例: + +```py +generator_train = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1\. / 255) +generator_test = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1\. / 255) +``` + +加载时,此定义还会重新缩放图像。 接下来,我们可以使用`flow_from_directory`方法从目录中读取图像,如下所示: + +```py +train_images = generator_train.flow_from_directory( + train_dir, + batch_size=batch_size, + target_size=(image_width, image_height)) + +test_images = generator_test.flow_from_directory( + test_dir, + batch_size=batch_size, + target_size=(image_width, image_height)) +``` + +加载图像的目录,批量大小和图像的目标大小作为参数传递。 此方法执行重新缩放,并分批传递数据以拟合模型。 该生成器可直接用于拟合模型。 该模型的方法 `fit_generator`可以按以下方式使用: + +```py +simple_cnn_model.fit_generator( + train_images, + steps_per_epoch=epoch_steps, + epochs=epochs, + validation_data=test_images, + validation_steps=test_steps) +``` + +该模型适合来自训练图像生成器的数据。 从训练中定义时期数,并传递验证数据以获取模型过度训练的性能。 该`fit_generator`支持并行处理数据和模型训练。 CPU 执行重新缩放,而 GPU 可以执行模型训练。 这使得计算资源的效率很高。 经过 50 个纪元后,该模型的准确度应为 60%。 接下来,我们将看到如何扩充数据集以获得改进的性能。 + +# 扩充数据集 + +数据扩充提供了增加数据集大小的方法。 数据扩充会在训练期间引入噪声,从而在模型中为各种输入生成鲁棒性。 该技术在数据集较小且可以组合并与其他技术一起使用的情况下很有用。 接下来,我们将看到不同类型的扩充。 + +# 技术提升 + +可以通过多种方式来增强图像,如下所述: + +* **翻转**:图像在水平或垂直方向上被镜像或翻转 +* **随机裁剪**:裁剪随机部分,因此该模型可以处理遮挡 +* **剪切**:图像变形以影响物体的形状 +* **缩放**:训练图像的缩放部分以处理不同比例的图像 +* **旋转**:旋转对象以处理对象中各种程度的变化 +* **增白**:增白是通过仅保留重要数据的主成分分析完成的 +* **归一化**:通过标准化均值和方差来归一化像素 +* **通道偏移**:更改颜色通道以使模型对各种伪像引起的颜色变化具有鲁棒性 + +所有这些技术都在`ImageDataGenerator`中实现,以增加数据集的大小。 以下是`generator_train`的修改版本,其中包含前面讨论的一些增强技术: + +```py +generator_train = tf.keras.preprocessing.image.ImageDataGenerator( + rescale=1\. / 255, + horizontal_flip=True, + zoom_range=0.3, + shear_range=0.3,) +``` + +替换前面代码中的`generator_train`将使精度提高到 90%。 更改扩充的参数,并注意更改。 在下一节中,我们将讨论一种称为转移学习的技术,该技术有助于以更少的数据训练更大的模型。 + +# 转移模型的学习或微调 + +转移学习是从预先训练的模型中学习的过程,该模型在较大的数据集上进行了训练。 用随机初始化训练模型通常需要时间和精力才能获得结果。 使用预训练的模型初始化模型可以加快收敛速度,并节省时间和能源。 这些经过预训练的模型通常使用精心选择的超参数进行训练。 + +可以直接使用预训练模型的几层,而无需进行任何修改,也可以对其进行位训练以适应变化。 在本节中,我们将学习如何对在`ImageNet`数据集上具有数百万个类别的模型进行调整或转移学习。 + +# 瓶颈功能培训 + +上一节中介绍的模型很简单,因此准确性可能较低。 应该从它们构建复杂的模型。 它们不能从头开始构建。 因此,提取瓶颈特征并对它们进行分类器训练。 瓶颈功能是训练数百万张图像的复杂体系结构所产生的功能。 图像是通过前进完成的,并存储了最终图层的特征。 从这些中,训练了一个简单的逻辑分类器进行分类。 提取瓶颈层,如下所示: + +```py +generator = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1\. / 255) + +model = tf.keras.applications.VGG16(include_top=False) + +train_images = generator.flow_from_directory( + train_dir, + batch_size=batch_size, + target_size=(image_width, image_height), + class_mode=None, + shuffle=False +) +train_bottleneck_features = model.predict_generator(train_images, epoch_steps) + +test_images = generator.flow_from_directory( + test_dir, + batch_size=batch_size, + target_size=(image_width, image_height), + class_mode=None, + shuffle=False +) + +test_bottleneck_features = model.predict_generator(test_images, test_steps) +``` + +将采用 VGG 模型并将其用于预测图像。 标签分配如下: + +```py +train_labels = np.array([0] * int(no_train / 2) + [1] * int(no_train / 2)) +test_labels = np.array([0] * int(no_test / 2) + [1] * int(no_test / 2)) +``` + +使用瓶颈功能构建,编译和训练具有两层的顺序模型,并且可以使用以下给出的代码来实现: + +```py +model = tf.keras.models.Sequential() +model.add(tf.keras.layers.Flatten(input_shape=train_bottleneck_features.shape[1:])) +model.add(tf.keras.layers.Dense(1024, activation='relu')) +model.add(tf.keras.layers.Dropout(0.3)) +model.add(tf.keras.layers.Dense(1, activation='softmax')) +model.compile(loss=tf.keras.losses.categorical_crossentropy, + optimizer=tf.keras.optimizers.Adam(), + metrics=['accuracy']) +``` + +使用以下所示的代码对这些瓶颈特征进行模型训练: + +```py +model.fit( + train_bottleneck_features, + train_labels, + batch_size=batch_size, + epochs=epochs, + validation_data=(test_bottleneck_features, test_labels)) +``` + +这提供了一种不同的方法来训练模型,并且在训练数据较少时很有用。 这通常是训练模型的更快方法。 仅使用预训练模型的最终激活来适应新任务。 这个想法可以扩展为微调几层,如下所示: + +# 在深度学习中微调几层 + +可以加载预训练的模型,并且仅可以训练几层。 当给定的问题与模型所训练的图像非常不同时,此方法会更好地工作。 **微调**是深度学习中的常见做法。 当数据集较小时,这具有优势。 优化也可以更快地获得。 + +在小型数据集上训练深度网络会导致过度拟合。 使用微调程序也可以避免这种过拟合。 在较大的数据集上训练的模型也应该相似,因为我们希望激活和特征与较小的数据集相似。 您可以从存储的权重路径开始,如下所示: + +```py +top_model_weights_path = 'fc_model.h5' +``` + +加载**视觉几何组**( **VGG** )模型,并将初始层设置为不可训练。 下一部分将详细介绍 VGG 模型。 目前,将 VGG 视为适用于图像数据的大型深度学习模型。 使用以下给出的代码,用新的可训练层替换完全连接的层: + +```py +model = tf.keras.applications.VGG16(include_top=False) +``` + +可以在 VGG 模型的顶部构建一个小型的两层前馈网络,通常具有隐藏的单元,激活和退出,如下所示: + +```py +model_fine_tune = tf.keras.models.Sequential() +model_fine_tune.add(tf.keras.layers.Flatten(input_shape=model.output_shape)) +model_fine_tune.add(tf.keras.layers.Dense(256, activation='relu')) +model_fine_tune.add(tf.keras.layers.Dropout(0.5)) +model_fine_tune.add(tf.keras.layers.Dense(no_classes, activation='softmax')) +``` + +顶级型号还必须装有经过充分培训的砝码。 然后可以将顶级模型添加到卷积基础中: + +```py +model_fine_tune.load_weights(top_model_weights_path) +model.add(model_fine_tune) +``` + +我们可以将前 25 个层设置为不可训练,直到最后一个卷积块,这样它们的权重才会被更新。 仅其余层将被更新: + +```py +for vgg_layer in model.layers[:25]: + vgg_layer.trainable = False +``` + +使用梯度下降优化器以缓慢的学习速率(4 量级)编译模型: + +```py +model.compile(loss='binary_crossentropy', + optimizer=tf.keras.optimizers.SGD(lr=1e-4, momentum=0.9), + metrics=['accuracy']) +``` + +我们可以将之前介绍的增强技术与剪切,缩放和翻转结合使用。 可以从目录中将流与火车和验证数据集一起添加到生成器。 现在可以将模型与数据增强结合起来进行微调。 这种训练方式比以前的所有方法都具有更好的准确性。 以下是转学的指南: + +| **数据大小** | **相似数据集** | **不同的数据集** | +| 较小的数据 | 微调输出层 | 微调更深层 | +| 更大的数据 | 微调整个模型 | 从头开始训练 | + +根据数据大小,可以确定要微调的层数。 数据越少,需要调整的层数就越少。 我们已经看到了如何使用转移学习技术来提高模型的准确性。 + +# 开发实际应用 + +识别猫和狗是一个很酷的问题,但不太可能是重要的问题。 产品中使用的图像分类的实际应用可能会有所不同。 您可能有不同的数据,目标等。 在本节中,您将学习解决这些不同设置的提示和技巧。 解决新问题时应考虑的因素如下: + +* 目标数量。 是 10 类问题还是 10,000 类问题? +* 类内差异有多大? 例如,是否必须在一个类别标签下标识不同类型的猫? +* 类间差异有多大? 例如,是否需要识别不同的猫? +* 数据有多大? +* 数据的平衡程度如何? +* 是否已经有一个训练有很多图像的模型? +* 部署推断时间和模型大小需要什么? 在 iPhone 上是 50 毫秒还是在 Google Cloud Platform 上是 10 毫秒? 可以消耗多少 RAM 来存储模型? + +处理图像分类问题时,请尝试回答这些问题。 根据答案,您可以设计训练体系结构并提高准确性,如下一节所述。 + +# 选择合适的模型 + +体系结构有很多选择。 根据部署的灵活性,可以选择模型。 请记住,卷积较小且较慢,但是密集层较大且较快。 在大小,运行时间和准确性之间需要权衡。 建议在最终决定之前测试所有架构。 根据应用程序,某些模型可能比其他模型更好。 您可以减小输入大小以加快推理速度。 可以根据以下部分所述的指标来选择体系结构。 + +# 解决欠拟合和过拟合的方案 + +对于该问题,模型有时可能太大或太小。 可以将其分别分类为欠拟合或过度拟合。 当模型太小时会发生拟合不足,而在训练精度较低时可以进行测量。 当模型太大并且训练和测试精度之间存在较大差距时,就会发生过度拟合。 拟合不足可以通过以下方法解决: + +* 获取更多数据 +* 尝试更大的模型 +* 如果数据很小,请尝试使用转移学习技术或进行数据扩充 + +过度拟合可以通过以下方法解决: + +* 使用辍学和批处理规范化等技术进行正则化 +* 扩充数据集 + +时刻提防损失。 损耗应随着迭代次数的减少而减少。 如果损失没有减少,则表明训练已停止。 一种解决方案是尝试使用其他优化器。 类别失衡可以通过加权损失函数来解决。 始终使用 **TensorBoard** 观看摘要。 很难估计需要多少数据。 本部分是培训任何深度学习模型的最佳课程。 接下来,我们将介绍一些特定于应用程序的指南。 + +# 面部性别和年龄检测 + +应用程序可能需要从面部检测性别和年龄。 脸部图像可以是通过脸部检测器获取的 。 可以将经过裁剪的脸部图像作为训练数据提供,并且应该给出相似的经过裁剪的脸部以进行推断。 根据所需的推理时间,可以选择 OpenCV 或 CNN 面部检测器。 对于培训,可以使用 Inception 或 ResNet。 如果由于是视频而所需的推理时间要少得多,则最好使用三个卷积,然后是两个完全连接的层。 请注意,年龄数据集通常存在巨大的类别失衡,因此使用不同的度量标准(如准确性和召回率)将有所帮助。 + +# 服装模型的微调 + +服装模型的微调是一个不错的选择。 在这里,具有多个对属性进行分类的 softmax 层将很有用。 这些属性可以是图案,颜色等。 + +# 品牌安全 + +使用**支持向量机**( **SVM** )来训练瓶颈层是一个不错的选择,因为各个类别的图像可能会完全不同。 通常将其用于内容审核,以帮助避免显示露骨的图像。 您已经了解了如何解决图像分类中的新问题。 + +# 摘要 + +我们已经介绍了用于训练分类任务的基本但有用的模型。 我们看到了使用 Keras 和 TensorFlow API 的 MNIST 数据集的简单模型。 我们还看到了如何利用 TensorBoard 观看培训过程。 然后,我们讨论了一些特定应用程序的最新体系结构。 还介绍了几种提高准确性的方法,例如数据增强,瓶颈层训练和微调预训练模型。 还介绍了为新模型训练模型的提示和技巧。 + +在下一章中,我们将看到如何可视化深度学习模型。 我们还将在本章中部署经过训练的模型以进行推断。 我们还将看到如何将训练有素的图层用于通过应用程序进行图像搜索。 然后,我们将了解自动编码器的概念并将其用于特征的维数。 \ No newline at end of file diff --git a/docs/dl-cv/03.md b/docs/dl-cv/03.md new file mode 100644 index 00000000..67743619 --- /dev/null +++ b/docs/dl-cv/03.md @@ -0,0 +1,792 @@ +# 图像检索 + +深度学习也可以称为**表示学习**,因为模型的特征或表示是在训练期间学习的。 在隐藏层的训练过程中生成的**视觉特征**可用于计算距离度量。 这些模型学习如何根据分类任务在各个层上检测边缘,图案等。 在本章中,我们将研究以下内容: + +* 如何从经过分类训练的模型中提取特征 +* 如何使用 TensorFlow Serving 在生产系统中进行更快的推断 +* 如何使用这些功能计算查询图像和目标集之间的相似度 +* 使用分类模型进行排名 +* 如何提高检索系统的速度 +* 从整体上看系统的架构 +* 当目标图像过多时,使用自动编码器学习紧凑的描述符 +* 训练去噪自动编码器 + +# 了解视觉特征 + +深度学习模型经常因无法解释而受到批评。 基于神经网络的模型通常被认为像黑匣子,因为人类很难推理出深度学习模型的工作原理。 由于激活函数,深度学习模型对图像进行的层转换是非线性的,因此不容易可视化。 已经开发出了通过可视化深层网络的层来解决对不可解释性的批评的方法。 在本节中,我们将研究可视化深层的尝试,以便了解模型的工作原理。 + +可视化可以使用模型的激活和渐变来完成。 可以使用以下技术可视化激活: + +* **最近的邻居**:可以对图像进行层激活,并且可以一起看到该激活的最近图像。 +* **降维**:激活的尺寸可以通过**主成分分析**( **PCA** )或 **t 分布随机邻居** **嵌入**( **t-SNE** ),可在二维或三维中可视化。 PCA 通过将值投影到最大方差方向来减小尺寸。 t-SNE 通过将最接近的点映射到三个维度来减小维度。 降维的使用及其技术超出了本书的范围。 建议您参考基本的机器学习材料,以了解有关降维的更多信息。 + +Wikipedia is a good source for understanding dimensionality reduction techniques. Here are a few links that you can refer to: + +* [https://zh.wikipedia.org/wiki/Dimensionality_reduction](https://en.wikipedia.org/wiki/Dimensionality_reduction) +* [https://zh.wikipedia.org/wiki/Principal_component_analysis](https://en.wikipedia.org/wiki/Principal_component_analysis) +* [https://zh.wikipedia.org/wiki/T-distributed_stochastic_neighbor_embedding](https://en.wikipedia.org/wiki/T-distributed_stochastic_neighbor_embedding) +* [https://en.wikipedia.org/wiki/Locality-sensitive_hashing](https://en.wikipedia.org/wiki/Locality-sensitive_hashing) + +* **最大补丁**:激活一个神经元,并捕获最大激活的相应补丁。 +* **遮挡**:在各个位置遮挡(遮挡)图像,并且激活以热图显示,以了解图像的哪些部分很重要。 + +在以下各节中,我们将看到如何实现这些功能的可视化。 + +# 可视化深度学习模型的激活 + +任何层的过滤器都可以可视化任何模型架构。 使用该技术只能理解初始层。 最后一层对于最近的邻居方法很有用。 当`ImageNet`数据集与最近的邻居排列在一起时,其外观如下: + +![](img/f13aef48-ee95-48ba-a9d8-87149edc5ea7.jpg) + +查看此图像,您可以看到相同的对象一起出现。 有趣的事情之一是,诸如狗,猴子和猎豹之类的动物虽然没有经过一个标签的训练却同时出现。 当对象相似时,图像的最近邻居可视化非常有用,因此,我们可以了解模型的预测。 最后一层也可以通过降维技术(例如主成分分析和 t-SNE)进行可视化。 在下一节中,我们将看到使用降维的可视化实现。 + +# 嵌入可视化 + +可以使用 TensorBoard 以二维或三维可视化嵌入层(即预最终层)。 假定本节中的代码段位于图像分类一章中训练的卷积神经网络模型之后。 首先,我们需要一个元数据文件,它是一个制表符分隔的文件。 元数据文件的每一行都应具有将要可视化的图像标签。 需要一个新变量来存储在会话创建和初始化之间定义的嵌入,如以下代码所示: + +```py +no_embedding_data = 1000 embedding_variable = tf.Variable(tf.stack( + mnist.test.images[:no_embedding_data], axis=0), trainable=False) +``` + +我们将获取 MNIST 测试数据,并创建用于可视化的元数据文件,如下所示: + +```py +metadata_path = '/tmp/train/metadata.tsv' with open(metadata_path, 'w') as metadata_file: + for i in range(no_embedding_data): + metadata_file.write('{}\n'.format( + np.nonzero(mnist.test.labels[::1])[1:][0][i])) +``` + +如上代码所示,应通过设置参数使嵌入变量不可训练。 接下来,必须定义投影仪配置。 它必须具有`tensor_name`,它是嵌入变量名称,元数据文件的路径和子画面图像。 子画面图像是一个带有小图像的图像,表示要通过嵌入可视化的标签。 以下是用于定义嵌入投影的代码: + +```py +from tensorflow.contrib.tensorboard.plugins import projector +projector_config = projector.ProjectorConfig() +embedding_projection = projector_config.embeddings.add() +embedding_projection.tensor_name = embedding_variable.name +embedding_projection.metadata_path = metadata_path +embedding_projection.sprite.image_path = os.path.join(work_dir + '/mnist_10k_sprite.png') +embedding_projection.sprite.single_image_dim.extend([28, 28]) +``` + +必须指定子画面图像尺寸。 然后,可以使用投影机通过摘要编写器和配置来可视化嵌入,如以下代码所示: + +```py +projector.visualize_embeddings(train_summary_writer, projector_config) +tf.train.Saver().save(session, '/tmp/train/model.ckpt', global_step=1) +``` + +然后,将模型与会话一起保存。 然后转到 TensorBoard 查看以下可视化效果: + +![](img/12a15ca8-cf56-4557-8f9c-6ee86903ed4f.png) + +TensorBoard 说明了代码的输出 + +您必须通过按钮选择 T-SNE 和颜色,如屏幕截图所示,以获得类似的可视化效果。 您可以看到数字如何一起出现。 该可视化对于检查数据和经过训练的嵌入非常有用。 这是 TensorBoard 的另一个强大功能。 在下一部分中,我们将实现可视化的引导反向传播。 + +# 引导反向传播 + +直接将特征可视化可能会减少信息量。 因此,我们使用反向传播的训练过程来激活滤镜以实现更好的可视化。 由于我们选择了要激活的神经元以进行反向传播,因此称为引导反向传播。 在本节中,我们将实现引导式反向传播以可视化功能。 + +我们将定义大小并加载 VGG 模型,如下所示: + +```py +image_width, image_height = 128, 128 vgg_model = tf.keras.applications.vgg16.VGG16(include_top=False) +``` + +图层由以图层名称作为键的字典组成,模型中的图层以权重作为键值,以方便访问。 现在,我们将从第五个块 `block5_conv1` 中获取第一卷积层,以计算 可视化效果。 输入和输出在此处定义: + +```py +input_image = vgg_model.input +vgg_layer_dict = dict([(vgg_layer.name, vgg_layer) for vgg_layer in vgg_model.layers[1:]]) +vgg_layer_output = vgg_layer_dict['block5_conv1'].output +``` + +我们必须定义损失函数。 损失函数将最大化特定层的激活。 这是一个梯度上升过程,而不是通常的梯度下降过程,因为我们正在尝试使损失函数最大化。 对于梯度上升,平滑梯度很重要。 因此,在这种情况下,我们通过归一化像素梯度来平滑梯度。 该损失函数快速收敛而不是。 + +应该对图像的输出进行归一化以可视化,在优化过程中使用 g 辐射上升来获得函数的最大值。 现在,我们可以通过定义评估器和梯度来开始梯度上升优化,如下所示。 现在,必须定义损失函数,并要计算的梯度。 迭代器通过迭代计算损耗和梯度值,如下所示: + +```py +filters = [] +for filter_idx in range(20): + loss = tf.keras.backend.mean(vgg_layer_output[:, :, :, filter_idx]) + gradients = tf.keras.backend.gradients(loss, input_image)[0] + gradient_mean_square = tf.keras.backend.mean(tf.keras.backend.square(gradients)) + gradients /= (tf.keras.backend.sqrt(gradient_mean_square) + 1e-5) + evaluator = tf.keras.backend.function([input_image], [loss, gradients]) +``` + +输入是随机的灰度图像,并添加了一些噪声。 如此处所示,将生成随机图像并完成缩放。 + +```py + gradient_ascent_step = 1. + input_image_data = np.random.random((1, image_width, image_height, 3)) + input_image_data = (input_image_data - 0.5) * 20 + 128 +``` + +现在开始对损失函数进行优化,对于某些滤波器,损失值可能为 0,应将其忽略,如下所示: + +```py + for i in range(20): + loss_value, gradient_values = evaluator([input_image_data]) + input_image_data += gradient_values * gradient_ascent_step + # print('Loss :', loss_value) + if loss_value <= 0.: + break +``` + +优化之后,通过均值减去并调整标准偏差来完成归一化。 然后,可以按比例缩小滤镜并将其裁剪到其渐变值,如下所示: + +```py + if loss_value > 0: + filter = input_image_data[0] + filter -= filter.mean() + filter /= (filter.std() + 1e-5) + filter *= 0.1 + filter += 0.5 + filter = np.clip(filter, 0, 1) + filter *= 255 + filter = np.clip(filter, 0, 255).astype('uint8') + filters.append((filter, loss_value)) +``` + +这些过滤器是随机选择的,并在此处可视化: + +![](img/f741f405-d2e9-4598-822b-d269d004e882.png) + +如图所示,用于缝合图像并产生输出的代码与代码束一起提供。 由于修道院的接受区域变大,因此可视化在以后的层变得复杂。 一些滤镜看起来很相似,但只是旋转而已。 在这种情况下,可视化的层次结构可以清楚地看到,如 Zeiler 等人所示。 ( [https://arxiv.org/pdf/1412.6572.pdf](https://arxiv.org/pdf/1412.6572.pdf) )。 下图显示了不同层的直接可视化: + +![](img/810e2adf-2cf5-44d2-b729-91dfd56cadb1.png) + +经 Zeiler 等人许可复制。 + +前两层看起来像边缘和角落检测器。 类似于 Gabor 的滤镜仅出现在第三层中。 Gabor 滤波器是线性的,传统上用于纹理分析。 我们已经直接通过引导反向传播看到了特征的可视化。 接下来,我们将看到如何实现 DeepDream 进行可视化。 + +# 深梦 + +可以在网络中的某些层上放大神经元激活,而不是合成图像。 放大原始图像以查看特征效果的概念称为 **DeepDream** 。 创建 DeepDream 的步骤是: + +1. 拍摄图像并从 CNN 中选择一个图层。 +2. 在特定的层进行激活。 +3. 修改渐变,以使渐变和激活相等。 +4. 计算图像和反向传播的梯度。 +5. 必须使用正则化对图像进行抖动和归一化。 +6. 像素值应修剪。 +7. 为了实现分形效果,对图像进行了多尺度处理。 + +让我们从导入相关的包开始: + +```py +import os +import numpy as np +import PIL.Image +import urllib.request +from tensorflow.python.platform import gfile +import zipfile +``` + +初始模型在`Imagenet`数据集和 Google 提供的模型文件上进行了预训练。 我们可以下载该模型并将其用于本示例。 模型文件的 ZIP 归档文件已下载并解压缩到一个文件夹中,如下所示: + +```py +model_url = 'https://storage.googleapis.com/download.tensorflow.org/models/inception5h.zip' file_name = model_url.split('/')[-1] + +file_path = os.path.join(work_dir, file_name) + +if not os.path.exists(file_path): + file_path, _ = urllib.request.urlretrieve(model_url, file_path) + +zip_handle = zipfile.ZipFile(file_path, 'r') +zip_handle.extractall(work_dir) +zip_handle.close() +``` + +这些命令应该在工作目录中创建了三个新文件。 可以将此预训练的模型加载到会话中,如下所示: + +```py +graph = tf.Graph() +session = tf.InteractiveSession(graph=graph) +model_path = os.path.join(work_dir, 'tensorflow_inception_graph.pb') +with gfile.FastGFile(model_path, 'rb') as f: + graph_defnition = tf.GraphDef() + graph_defnition.ParseFromString(f.read()) +``` + +会话从图形初始化开始。 然后,将下载的模型的图形定义加载到内存中。 作为预处理步骤,必须从输入中减去`ImageNet`平均值,如下所示。 预处理后的图像随后被馈送到该图,如下所示: + +```py +input_placeholder = tf.placeholder(np.float32, name='input') +imagenet_mean_value = 117.0 preprocessed_input = tf.expand_dims(input_placeholder-imagenet_mean_value, 0) +tf.import_graph_def(graph_defnition, {'input': preprocessed_input}) +``` + +现在,会话和图形已准备好进行推断。 双线性插值需要`resize_image`功能。 可以添加`resize`函数方法,该函数通过 TensorFlow 会话来调整图像的大小,如下所示: + +```py +def resize_image(image, size): + resize_placeholder = tf.placeholder(tf.float32) + resize_placeholder_expanded = tf.expand_dims(resize_placeholder, 0) + resized_image = tf.image.resize_bilinear(resize_placeholder_expanded, size)[0, :, :, :] + return session.run(resized_image, feed_dict={resize_placeholder: image}) +``` + +可以将工作目录中的图像加载到内存中并转换为浮点值,如下所示: + +```py +image_name = 'mountain.jpg' image = PIL.Image.open(image_name) +image = np.float32(image) +``` + +此处显示了已加载的图像,供您参考: + +![](img/a6cf4c5d-de28-4bec-995d-07b57bfbe560.jpg) + +音阶空间的八度音阶数,大小和音阶在此处定义: + +```py +no_octave = 4 scale = 1.4 window_size = 51 +``` + +这些值在此处显示的示例中效果很好,因此需要根据其大小调整其他图像。 可以选择一个层来做梦,该层的平均平均值将是`objective`函数,如下所示: + +```py +score = tf.reduce_mean(objective_fn) +gradients = tf.gradients(score, input_placeholder)[0] +``` + +计算图像的梯度以进行优化。 可以通过将图像调整为各种比例并找到差异来计算八度图像,如下所示: + +```py +octave_images = [] +for i in range(no_octave - 1): + image_height_width = image.shape[:2] + scaled_image = resize_image(image, np.int32(np.float32(image_height_width) / scale)) + image_difference = image - resize_image(scaled_image, image_height_width) + image = scaled_image + octave_images.append(image_difference) +``` + +现在可以使用所有八度图像运行优化。 窗口在图像上滑动,计算渐变激活以创建梦,如下所示: + +```py +for octave_idx in range(no_octave): + if octave_idx > 0: + image_difference = octave_images[-octave_idx] + image = resize_image(image, image_difference.shape[:2]) + image_difference + + for i in range(10): + image_heigth, image_width = image.shape[:2] + sx, sy = np.random.randint(window_size, size=2) + shifted_image = np.roll(np.roll(image, sx, 1), sy, 0) + gradient_values = np.zeros_like(image) + + for y in range(0, max(image_heigth - window_size // 2, window_size), window_size): + for x in range(0, max(image_width - window_size // 2, window_size), window_size): + sub = shifted_image[y:y + window_size, x:x + window_size] + gradient_windows = session.run(gradients, {input_placeholder: sub}) + gradient_values[y:y + window_size, x:x + window_size] = gradient_windows + + gradient_windows = np.roll(np.roll(gradient_values, -sx, 1), -sy, 0) + image += gradient_windows * (1.5 / (np.abs(gradient_windows).mean() + 1e-7)) +``` + +现在,创建 DeepDream 的优化已完成,可以通过剪切值来保存,如下所示: + +```py +image /= 255.0 image = np.uint8(np.clip(image, 0, 1) * 255) +PIL.Image.fromarray(image).save('dream_' + image_name, 'jpeg') +``` + +在本节中,我们已经看到了创建 DeepDream 的过程。 结果显示在这里: + +![](img/1c9b8a0a-3621-4644-97e5-1c0158a051dd.jpg) + +如我们所见,狗到处都被激活。 您可以尝试其他各种层并查看结果。 这些结果可用于艺术目的。 类似地,可以激活其他层以产生不同的伪像。 在下一节中,我们将看到一些对抗性示例,这些示例可能会欺骗深度学习模型。 + +# 对抗性例子 + +在几个数据集上,图像分类算法已达到人类水平的准确性。 但是它们可以被对抗性例子轻易地欺骗。 对抗示例是合成图像,它们使模型无法产生所需的结果。 拍摄任何图像,然后选择不正确的随机目标类别。 可以用噪声修改该图像,直到网络被 Goodfellow 等人所欺骗。 ( [https://arxiv.org/pdf/1412.6572.pdf](https://arxiv.org/pdf/1412.6572.pdf) )。 该模型的对抗攻击示例如下所示: + +![](img/5fc3a7dd-4e49-4215-a821-e026811ca3fd.png) + +经 Goodfellow 等人许可复制。 + +在此图中,左侧显示的图像具有特定标签的 58%可信度。 左边的图像与中间显示的噪声结合在一起时,在右边形成图像。 对于人来说,带有噪点的图像看起来还是一样。 但是带有噪点的图像可以通过具有 97%置信度的其他标签来预测。 尽管图像具有非常不同的对象,但仍将高置信度分配给特定示例。 这是深度学习模型的问题,因此,您应该了解这在哪里适用: + +* 甚至可以在不访问模型的情况下生成对抗性示例。 您可以训练自己的模型,生成对抗性示例,但仍然可以欺骗其他模型。 +* 在实践中这种情况很少发生,但是当有人试图欺骗系统来发送垃圾邮件或崩溃时,这将成为一个真正的问题。 +* 所有机器学习模型都容易受到此问题的影响,而不仅仅是深度学习模型。 + +您应该考虑对抗性示例,了解在安全关键系统上部署深度学习模型的后果。 在下一节中,我们将看到如何利用 TensorFlow Serving 获得更快的推断。 + +# 模型推论 + +任何新数据都可以传递给模型以获取结果。 从图像获取分类结果或特征的过程称为 **推论**。 训练和推理通常在不同的计算机上和不同的时间进行。 我们将学习如何存储模型,运行推理以及如何使用 TensorFlow Serv 作为具有良好延迟和吞吐量的服务器。 + +# 导出模型 + +训练后的模型必须导出并保存。 权重,偏差和图形都存储用于推断。 我们将训练 MNIST 模型并将其存储。 首先使用以下代码定义所需的常量: + +```py +work_dir = '/tmp' model_version = 9 training_iteration = 1000 input_size = 784 no_classes = 10 batch_size = 100 total_batches = 200 +``` + +`model_version`可以是一个整数,用于指定我们要导出以供服务的模型。 `feature config`存储为具有占位符名称及其对应数据类型的字典。 应该映射预测类及其标签。 身份占位符可与 API 配合使用: + +```py +tf_example = tf.parse_example(tf.placeholder(tf.string, name='tf_example'), + {'x': tf.FixedLenFeature(shape=[784], dtype=tf.float32), }) +x_input = tf.identity(tf_example['x'], name='x') +``` + +可以使用以下代码使用权重,偏差,对数和优化器定义一个简单的分类器: + +```py +y_input = tf.placeholder(tf.float32, shape=[None, no_classes]) +weights = tf.Variable(tf.random_normal([input_size, no_classes])) +bias = tf.Variable(tf.random_normal([no_classes])) +logits = tf.matmul(x_input, weights) + bias +softmax_cross_entropy = tf.nn.softmax_cross_entropy_with_logits(labels=y_input, logits=logits) +loss_operation = tf.reduce_mean(softmax_cross_entropy) +optimiser = tf.train.GradientDescentOptimizer(0.5).minimize(loss_operation) +``` + +训练模型,如以下代码所示: + +```py +mnist = input_data.read_data_sets('MNIST_data', one_hot=True) +for batch_no in range(total_batches): + mnist_batch = mnist.train.next_batch(batch_size) + _, loss_value = session.run([optimiser, loss_operation], feed_dict={ + x_input: mnist_batch[0], + y_input: mnist_batch[1] + }) + print(loss_value) +``` + +定义预测签名,并导出模型。 将模型保存到持久性存储中,以便可以在以后的时间点进行推理。 这将通过反序列化导出数据,并将其存储为其他系统可以理解的格式。 具有不同变量和占位符的多个图可用于导出。 它还支持`signature_defs` 和资产。 `signature_defs`指定了输入和输出,因为将从外部客户端访问输入和输出。 资产是将用于推理的非图形组件,例如词汇表等。 + +分类签名使用对 TensorFlow 分类 API 的访问权限。 输入是强制性的,并且有两个可选输出(预测类别和预测概率),其中至少一个是强制性的。 预测签名提供输入和输出数量的灵活性。 可以定义多个输出并从客户端显式查询。 `signature_def`显示在此处: + +```py +signature_def = ( + tf.saved_model.signature_def_utils.build_signature_def( + inputs={'x': tf.saved_model.utils.build_tensor_info(x_input)}, + outputs={'y': tf.saved_model.utils.build_tensor_info(y_input)}, + method_name="tensorflow/serving/predict")) +``` + +最后,使用预测签名将元图和变量添加到构建器中: + +```py +model_path = os.path.join(work_dir, str(model_version)) +saved_model_builder = tf.saved_model.builder.SavedModelBuilder(model_path) +saved_model_builder.add_meta_graph_and_variables( + session, [tf.saved_model.tag_constants.SERVING], + signature_def_map={ + 'prediction': signature_def + }, + legacy_init_op=tf.group(tf.tables_initializer(), name='legacy_init_op')) +saved_model_builder.save() +``` + +该构建器已保存,可以由服务器使用。 所示示例适用于任何模型,并可用于导出。 在下一部分中,我们将服务并查询导出的模型。 + +# 服务训练过的模型 + +可以使用以下命令通过 TensorFlow Serving 服务上一节中导出的模型: + +```py +tensorflow_model_server --port=9000 --model_name=mnist --model_base_path=/tmp/mnist_model/ +``` + +`model_base_path` 指向导出模型的目录。 现在可以与客户端一起测试服务器。 请注意,这不是 HTTP 服务器,因此需要此处显示的客户端而不是 HTTP 客户端。 导入所需的库: + +```py +from grpc.beta import implementations +import numpy +import tensorflow as tf +from tensorflow.examples.tutorials.mnist import input_data +from tensorflow_serving.apis import predict_pb2 +from tensorflow_serving.apis import prediction_service_pb2 +``` + +添加并发常数,测试数量和工作目录。 定义了一个类,用于对返回的结果进行计数。 定义了**远程过程调用**( **RPC** )回调,并带有用于对预测计数的计数器,如下所示: + +```py +concurrency = 1 num_tests = 100 host = '' port = 8000 work_dir = '/tmp' def _create_rpc_callback(): + def _callback(result): + response = numpy.array( + result.result().outputs['y'].float_val) + prediction = numpy.argmax(response) + print(prediction) + return _callback +``` + +根据您的要求修改 `host` 和 `port` 。 `_callback`方法定义了从服务器返回响应时所需的步骤。 在这种情况下,将计算最大概率。 通过调用服务器来运行推断: + +```py +test_data_set = mnist.test +test_image = mnist.test.images[0] + +predict_request = predict_pb2.PredictRequest() +predict_request.model_spec.name = 'mnist' predict_request.model_spec.signature_name = 'prediction' predict_channel = implementations.insecure_channel(host, int(port)) +predict_stub = prediction_service_pb2.beta_create_PredictionService_stub(predict_channel) + +predict_request.inputs['x'].CopyFrom( + tf.contrib.util.make_tensor_proto(test_image, shape=[1, test_image.size])) +result = predict_stub.Predict.future(predict_request, 3.0) +result.add_done_callback( + _create_rpc_callback()) +``` + +反复调用推理以评估准确性,延迟和吞吐量。 推断错误率应该在 90%左右,并且并发性应该很高。 导出和客户端方法可用于任何模型,以从模型获得结果和特征。 在下一节中,我们将构建检索管道。 + +# 基于内容的图像检索 + +**基于内容的图像检索**( **CBIR** )的技术将查询图像作为输入,并对目标图像数据库中的图像进行排名,从而产生输出。 CBIR 是具有特定目标的图像到图像搜索引擎。 要检索需要目标图像数据库。 返回距查询图像最小距离的目标图像。 我们可以直接将图像用于相似性,但是问题如下: + +* 图像尺寸巨大 +* 像素中有很多冗余 +* 像素不携带语义信息 + +因此,我们训练了一个用于对象分类的模型,并使用该模型中的特征进行检索。 然后,我们通过相同的模型传递查询图像和目标数据库以获得特征。 这些模型也可以称为**编码器**,因为它们对特定任务的图像信息进行编码。 编码器应该能够捕获全局和局部特征。 我们可以使用我们在图像分类一章中研究过的模型,这些模型经过训练可以进行分类任务。 由于强力扫描或线性扫描速度较慢,因此图像搜索可能会花费大量时间。 因此,需要一些用于更快检索的方法。 以下是一些加快匹配速度的方法: + +* **局部敏感哈希**( **LSH** ):L SH 将要素投影到其子空间,并可以向候选对象提供列表,并在以后进行精细特征排名。 这也是我们本章前面介绍的降维技术,例如 PCA 和 t-SNE。 它具有较小尺寸的铲斗。 +* **多索引哈希**:此方法对功能进行哈希处理,就像信鸽拟合一样,可以使其更快。 它使用汉明距离来加快计算速度。 汉明距离不过是以二进制表示的数字的位置差异的数量。 + +这些方法更快,需要更少的内存,但要权衡准确性。 这些方法也没有捕获语义上的差异。 可以根据查询对匹配结果进行重新排名以获得更好的结果。 重新排序可以通过对返回的目标图像重新排序来改善结果。 重新排序可以使用以下技术之一: + +* **几何验证**:此方法将几何图形和目标图像与仅返回相似几何图形的目标图像进行匹配。 +* **查询扩展**:这将扩展目标图像列表并详尽搜索它们。 +* **相关性反馈**:此方法从使用中获取反馈并返回结果。 根据用户输入,将进行重新排名。 + +这些技术已针对文本进行了很好的开发,可用于图像。 在本章中,我们将重点介绍提取特征并将其用于 CBIR。 在下一节中,我们将学习如何进行模型推断。 + +# 建立检索管道 + +从查询图像的目标图像中获得最佳匹配的步骤序列称为**检索管道**。 检索管道具有多个步骤或组件。 图像数据库的功能必须脱机提取并存储在数据库中。 对于每个查询图像,必须提取特征并且必须在所有目标图像之间计算相似度。 然后,可以对图像进行排名以最终输出。 检索管道如下所示: + +![](img/c9ea2c8e-b149-4890-8a5e-ba4d03ade21e.png) + +特征提取步骤必须快速,为此可以使用 TensorFlow Serving。 您可以根据应用程序选择使用哪些功能。 例如,当需要基于纹理的匹配时可以使用初始层,而当必须在对象级别进行匹配时可以使用更高的层。 在下一部分中,我们将看到如何从预训练的初始模型中提取特征。 + +# 提取图像的瓶颈特征 + +瓶颈要素是在预分类层中计算的值。 在本节中,我们将看到如何使用 TensorFlow 从预训练的模型中提取 b ottleneck 特征。 首先,使用以下代码导入所需的库: + +```py +import os +import urllib.request +from tensorflow.python.platform import gfile +import tarfile +``` + +然后,我们需要下载带有图形定义及其权重的预训练模型。 TensorFlow 已使用初始架构在`ImageNet`数据集上训练了一个模型,并提供了该模型。 我们将使用以下代码下载该模型并将其解压缩到本地文件夹中: + +```py +model_url = 'http://download.tensorflow.org/models/image/imagenet/inception-2015-12-05.tgz' file_name = model_url.split('/')[-1] +file_path = os.path.join(work_dir, file_name) + +if not os.path.exists(file_path): + file_path, _ = urllib.request.urlretrieve(model_url, file_path) +tarfile.open(file_path, 'r:gz').extractall(work_dir) +``` + +仅当模型不存在时,这会创建一个文件夹并下载模型。 如果重复执行代码,则不会每次都下载模型。 该图形以**协议缓冲区**( **protobuf** )格式存储在文件中。 必须将其读取为字符串,然后传递给`tf.GraphDef()`对象以将其带入内存: + +```py +model_path = os.path.join(work_dir, 'classify_image_graph_def.pb') +with gfile.FastGFile(model_path, 'rb') as f: + graph_defnition = tf.GraphDef() + graph_defnition.ParseFromString(f.read()) +``` + +在初始模型中,瓶颈层名为`pool_3/_reshape:0`,并且该层的尺寸为 2,048 。 输入的占位符名称为`DecodeJpeg/contents:0`,调整大小张量名称为`ResizeBilinear:0`。 我们可以使用`tf.import_graph_def`和所需的返回张量导入图形定义,以进行进一步的操作: + +```py +bottleneck, image, resized_input = ( + tf.import_graph_def( + graph_defnition, + name='', + return_elements=['pool_3/_reshape:0', + 'DecodeJpeg/contents:0', + 'ResizeBilinear:0']) +) +``` + +进行查询和目标图像并将其加载到内存中。 `gfile`功能提供了一种更快的方式将图像加载到内存中。 + +```py +query_image_path = os.path.join(work_dir, 'cat.1000.jpg') +query_image = gfile.FastGFile(query_image_path, 'rb').read() +target_image_path = os.path.join(work_dir, 'cat.1001.jpg') +target_image = gfile.FastGFile(target_image_path, 'rb').read() +``` + +让我们定义一个使用`session`和图像从图像中提取瓶颈特征的函数: + +```py +def get_bottleneck_data(session, image_data): + bottleneck_data = session.run(bottleneck, {image: image_data}) + bottleneck_data = np.squeeze(bottleneck_data) + return bottleneck_data +``` + +启动会话,并传递图像以运行前向推理,以从预先训练的模型中获取瓶颈值: + +```py +query_feature = get_bottleneck_data(session, query_image) +print(query_feature) +target_feature = get_bottleneck_data(session, target_image) +print(target_feature) +``` + +运行上面的代码应显示如下: + +```py +[ 0.55705792 0.36785451 1.06618118 ..., 0.6011821 0.36407694 + 0.0996572 ] +[ 0.30421323 0.0926369 0.26213276 ..., 0.72273785 0.30847171 + 0.08719242] +``` + +该计算特征的过程可以按比例缩放以获取更多目标图像。 使用这些值,可以在查询图像和目标数据库之间计算相似度,如以下部分所述。 + +# 计算查询图像与目标数据库之间的相似度 + +NumPy 的`linalg.norm`可用于计算**欧几里德距离**。 可以通过计算要素之间的欧几里得距离来计算查询图像与目标数据库之间的相似度,如下所示: + +```py +dist = np.linalg.norm(np.asarray(query_feature) - np.asarray(target_feature)) +print(dist) +``` + +运行此命令应打印以下内容: + +```py +16.9965 +``` + +这是可用于相似度计算的度量。 查询与目标图像之间的欧几里得距离越小,图像越相似。 因此,计算欧几里得距离是相似度的量度。 使用特征来计算欧几里得距离是基于这样的假设:在训练模型的过程中学习了这些特征。 将这种计算扩展成数百万个图像效率不高。 在生产系统中,期望以毫秒为单位返回结果。 在下一节中,我们将看到如何提高检索效率。 + +# 高效检索 + +检索可能很慢,因为它是蛮力方法。 使用近似最近的邻居可以使匹配更快。 维度的诅咒也开始出现,如下图所示: + +![](img/d3c91098-f5f8-42ed-9fac-3ee18ba2baee.png) + +随着维数的增加,复杂度也从二维维增加到三个维。 距离的计算也变慢。 为了使距离搜索更快,我们将在下一部分中讨论一种近似方法。 + +# 使用近似最近的邻居更快地匹配 + +**近似最近邻居**( **ANNOY** )是一种用于更快进行最近邻居搜索的方法。 ANNOY 通过随机投影来构建树。 树结构使查找最接近的匹配更加容易。 您可以创建`ANNOYIndex`以便快速检索,如下所示: + +```py +def create_annoy(target_features): + t = AnnoyIndex(layer_dimension) + for idx, target_feature in enumerate(target_features): + t.add_item(idx, target_feature) + t.build(10) + t.save(os.path.join(work_dir, 'annoy.ann')) + +create_annoy(target_features) +``` + +创建索引需要要素的尺寸。 然后将项目添加到索引并构建树。 树木的数量越多,在时间和空间复杂度之间进行权衡的结果将越准确。 可以创建索引并将其加载到内存中。 可以查询 ANNOY,如下所示: + +```py +annoy_index = AnnoyIndex(10) +annoy_index.load(os.path.join(work_dir, 'annoy.ann')) +matches = annoy_index.get_nns_by_vector(query_feature, 20) +``` + +匹配项列表可用于检索图像详细信息。 项目的索引将被返回。 + +请访问 [https://github.com/spotify/annoy](https://github.com/spotify/annoy) 以获取`ANNOY`的完整实现,以及其在准确性和速度方面与其他近似最近邻算法的基准比较。 + +# ANNOY 的优点 + +使用 ANNOY 的原因很多。 主要优点如下: + +* 具有内存映射的数据结构,因此对 RAM 的占用较少。 因此,可以在多个进程之间共享同一文件。 +* 可以使用曼哈顿,余弦或欧几里得等多种距离来计算查询图像和目标数据库之间的相似度。 + +# 原始图像的自动编码器 + +自动编码器是一种用于生成有效编码的无监督算法。 输入层和目标输出通常相同。 减少和增加之间的层以下列方式: + +![](img/2cc688ac-9e9f-447e-b46e-295eea60e0e9.png) + +**瓶颈**层是尺寸减小的中间层。 瓶颈层的左侧称为**编码器**,右侧称为**解码器**。 编码器通常减小数据的尺寸,而解码器增大尺寸。 编码器和解码器的这种组合称为自动编码器。 整个网络都经过重建错误训练。 从理论上讲,可以存储瓶颈层,并可以通过解码器网络重建原始数据。 如下所示,这可以减小尺寸并易于编程。 使用以下代码定义卷积,解卷积和完全连接的层: + +```py +def fully_connected_layer(input_layer, units): + return tf.layers.dense( + input_layer, + units=units, + activation=tf.nn.relu + ) + +def convolution_layer(input_layer, filter_size): + return tf.layers.conv2d( + input_layer, + filters=filter_size, + kernel_initializer=tf.contrib.layers.xavier_initializer_conv2d(), + kernel_size=3, + strides=2 + ) + +def deconvolution_layer(input_layer, filter_size, activation=tf.nn.relu): + return tf.layers.conv2d_transpose( + input_layer, + filters=filter_size, + kernel_initializer=tf.contrib.layers.xavier_initializer_conv2d(), + kernel_size=3, + activation=activation, + strides=2 + ) +``` + +定义具有五层卷积的会聚编码器,如以下代码所示: + +```py +input_layer = tf.placeholder(tf.float32, [None, 128, 128, 3]) +convolution_layer_1 = convolution_layer(input_layer, 1024) +convolution_layer_2 = convolution_layer(convolution_layer_1, 512) +convolution_layer_3 = convolution_layer(convolution_layer_2, 256) +convolution_layer_4 = convolution_layer(convolution_layer_3, 128) +convolution_layer_5 = convolution_layer(convolution_layer_4, 32) +``` + +通过展平第五个卷积层来计算瓶颈层。 再次将瓶颈层重新成形为卷积层,如下所示: + +```py +convolution_layer_5_flattened = tf.layers.flatten(convolution_layer_5) +bottleneck_layer = fully_connected_layer(convolution_layer_5_flattened, 16) +c5_shape = convolution_layer_5.get_shape().as_list() +c5f_flat_shape = convolution_layer_5_flattened.get_shape().as_list()[1] +fully_connected = fully_connected_layer(bottleneck_layer, c5f_flat_shape) +fully_connected = tf.reshape(fully_connected, + [-1, c5_shape[1], c5_shape[2], c5_shape[3]]) +``` + +计算可以重建图像的发散或解码器部分,如以下代码所示: + +```py +deconvolution_layer_1 = deconvolution_layer(fully_connected, 128) +deconvolution_layer_2 = deconvolution_layer(deconvolution_layer_1, 256) +deconvolution_layer_3 = deconvolution_layer(deconvolution_layer_2, 512) +deconvolution_layer_4 = deconvolution_layer(deconvolution_layer_3, 1024) +deconvolution_layer_5 = deconvolution_layer(deconvolution_layer_4, 3, + activation=tf.nn.tanh) +``` + +该网络经过培训,可以快速收敛。 传递图像特征时可以存储瓶颈层。 这有助于减少可用于检索的数据库的大小。 仅需要编码器部分即可为特征建立索引。 自动编码器是一种有损压缩算法。 它与其他压缩算法不同,因为它从数据中学习压缩模式。 因此,自动编码器模型特定于数据。 自动编码器可以与 t-SNE 结合使用以获得更好的可视化效果。 自动编码器学习的瓶颈层可能对其他任务没有用。 瓶颈层的大小可以大于以前的层。 在这种分叉和收敛连接的情况下,稀疏的自动编码器就会出现。 在下一节中,我们将学习自动编码器的另一种应用。 + +# 使用自动编码器进行降噪 + +自动编码器也可以用于图像去噪。 去噪是从图像中去除噪点的过程。 去噪编码器可以无监督的方式进行训练。 可以在正常图像中引入噪声,并针对原始图像训练自动编码器。 以后,可以使用完整的自动编码器生成无噪声的图像。 在本节中,我们将逐步说明如何去噪 MNIST 图像。 导入所需的库并定义占位符,如下所示: + +```py +x_input = tf.placeholder(tf.float32, shape=[None, input_size]) +y_input = tf.placeholder(tf.float32, shape=[None, input_size]) +``` + +`x_input`和`y_input`的形状与自动编码器中的形状相同。 然后,定义一个密集层,如下所示,默认激活为`tanh`激活功能。 `add_variable_summary`方法是从图像分类章节示例中导入的。 密集层的定义如下所示: + +```py +def dense_layer(input_layer, units, activation=tf.nn.tanh): + layer = tf.layers.dense( + inputs=input_layer, + units=units, + activation=activation + ) + add_variable_summary(layer, 'dense') + return layer +``` + +接下来,可以定义自动编码器层。 该自动编码器仅具有完全连接的层。 编码器部分具有减小尺寸的三层。 解码器部分具有增加尺寸的三层。 编码器和解码器都是对称的,如下所示: + +```py +layer_1 = dense_layer(x_input, 500) +layer_2 = dense_layer(layer_1, 250) +layer_3 = dense_layer(layer_2, 50) +layer_4 = dense_layer(layer_3, 250) +layer_5 = dense_layer(layer_4, 500) +layer_6 = dense_layer(layer_5, 784) +``` + +隐藏层的尺寸是任意选择的。 接下来,定义`loss`和`optimiser`。 这里我们使用 Sigmoid 代替 softmax 作为分类,如下所示: + +```py +with tf.name_scope('loss'): + softmax_cross_entropy = tf.nn.sigmoid_cross_entropy_with_logits( + labels=y_input, logits=layer_6) + loss_operation = tf.reduce_mean(softmax_cross_entropy, name='loss') + tf.summary.scalar('loss', loss_operation) + +with tf.name_scope('optimiser'): + optimiser = tf.train.AdamOptimizer().minimize(loss_operation) +``` + +TensorBoard 提供了另一种称为`image,`的摘要,可用于可视化图像。 我们将使用输入`layer_6`并将其重塑形状以将其添加到摘要中,如下所示: + +```py +x_input_reshaped = tf.reshape(x_input, [-1, 28, 28, 1]) +tf.summary.image("noisy_images", x_input_reshaped) + +y_input_reshaped = tf.reshape(y_input, [-1, 28, 28, 1]) +tf.summary.image("original_images", y_input_reshaped) + +layer_6_reshaped = tf.reshape(layer_6, [-1, 28, 28, 1]) +tf.summary.image("reconstructed_images", layer_6_reshaped) +``` + +图像数量默认限制为三张,并且可以更改。 这是为了限制其将所有图像都写入摘要文件夹。 接下来,合并所有摘要,并将图添加到摘要编写器,如下所示: + +```py +merged_summary_operation = tf.summary.merge_all() +train_summary_writer = tf.summary.FileWriter('/tmp/train', session.graph) +``` + +可以将正常的随机噪声添加到图像中并作为输入张量馈入。 添加噪声后,多余的值将被裁剪。 目标将是原始图像本身。 此处显示了噪声和训练过程的附加信息: + +```py +for batch_no in range(total_batches): + mnist_batch = mnist_data.train.next_batch(batch_size) + train_images, _ = mnist_batch[0], mnist_batch[1] + train_images_noise = train_images + 0.2 * np.random.normal(size=train_images.shape) + train_images_noise = np.clip(train_images_noise, 0., 1.) + _, merged_summary = session.run([optimiser, merged_summary_operation], + feed_dict={ + x_input: train_images_noise, + y_input: train_images, + }) + train_summary_writer.add_summary(merged_summary, batch_no) +``` + +开始此培训后,可以在 TensorBoard 中查看结果。 损失显示在此处: +![](img/2ed9d697-3908-4169-927f-99769d2f7bdf.png) + +Tensorboard 说明了输出图 + +损耗稳步下降,并将在迭代过程中保持缓慢下降。 这显示了自动编码器如何快速收敛。 接下来,原始图像显示三位数: + +![](img/4e4b465b-0039-41c7-826e-67ba07141257.png) + +以下是添加了噪点的相同图像: + +![](img/16852acb-354f-43a2-832c-78fb55edb1ff.png) + +您会注意到有很大的噪音,这是作为输入给出的。 接下来,是使用去噪自动编码器重建的相同编号的图像: + +![](img/fcaa79e6-c455-46a6-8d49-1788aa7be7b6.png) + +您会注意到,去噪自动编码器在消除噪声方面做得非常出色。 您可以在测试图像上运行它,并可以看到质量得到保持。 对于更复杂的数据集,可以使用卷积神经网络以获得更好的结果。 该示例展示了计算机视觉深度学习的强大功能,因为它是在无人监督的情况下进行训练的。 + +# 摘要 + +在本章中,您学习了如何从图像中提取特征并将其用于 CBIR。 您还学习了如何使用 TensorFlow Serving 来推断图像特征。 我们看到了如何利用近似最近邻或更快的匹配而不是线性扫描。 您了解了散列如何仍可以改善结果。 引入了自动编码器的概念,我们看到了如何训练较小的特征向量以进行搜索。 还显示了使用自动编码器进行图像降噪的示例。 我们看到了使用基于位的比较的可能性,该比较可以将其扩展到数十亿张图像。 + +在下一章中,我们将看到如何训练对象检测问题的模型。 我们将利用开源模型来获得良好的准确性,并了解其背后的所有算法。 最后,我们将使用所有想法来训练行人检测模型。 \ No newline at end of file diff --git a/docs/dl-cv/04.md b/docs/dl-cv/04.md new file mode 100644 index 00000000..cd6633f5 --- /dev/null +++ b/docs/dl-cv/04.md @@ -0,0 +1,500 @@ +# 物体检测 + +对象检测是在图像中找到对象位置的动作。 在本章中,我们将通过了解以下主题来学习对象检测技术和实现行人检测: + +* 基础知识以及定位和检测之间的区别 +* 各种数据集及其描述 +* 用于对象定位和检测的算法 +* TensorFlow API 用于对象检测 +* 训练新的物体检测模型 +* 基于 YOLO 算法的移动汽车行人检测 + +# 检测图像中的物体 + +近年来,对象检测在应用和研究方面都出现了爆炸式增长。 对象检测是计算机视觉中的重要问题。 与图像分类任务相似,更深的网络在检测方面表现出更好的性能。 目前,这些技术的准确性非常好。 因此,它被用于许多应用中。 + +图像分类将图像标记为一个整体。 除了标记对象外,找到对象的位置也称为**对象定位**。 通常,对象的位置由直角坐标定义。 在图像中使用直角坐标查找多个对象称为检测。 这是对象检测的示例: + +![](img/07fc811c-af39-4bb1-b78d-16a2914667c2.png) + +该图显示了带有边界框的四个对象。 我们将学习可以执行查找框任务的算法。 这些应用在自动驾驶汽车和工业物体等机器人视觉领域具有巨大的应用前景。 我们可以将定位和检测任务概括为以下几点: + +* 本地化检测标签内图像中的一个对象 +* 检测可找到图像中的所有对象以及标签 + +区别在于对象的数量。 在检测中,存在可变数量的对象。 在设计与定位或检测有关的深度学习模型的体系结构时,此小差异会带来很大的不同。 接下来,我们将看到可用于任务的各种数据集。 + +# 探索数据集 + +可用于对象定位和检测的数据集很多。 在本节中,我们将探索研究社区用来评估算法的数据集。 有些数据集带有不同数量的对象,这些对象中标注的范围从 20 到 200 不等,这使得对象检测变得困难。 与其他数据集(每个图像仅包含一个对象)相比,某些数据集在一个图像中包含的对象太多。 接下来,我们将详细查看数据集。 + +# ImageNet 数据集 + +ImageNet 具有用于评估分类,本地化和检测任务的数据。 [第 2 章](../Text/02.html),*图像分类*详细讨论了分类数据集。 与分类数据类似,本地化任务有 1,000 个类别。 准确度是根据前五次检测得出的。 所有图像中至少会有一个边界框。 有 470,000 张图像的检测问题有 200 个对象,每个图像平均有 1.1 个对象。 + +# PASCAL VOC 挑战 + +PASCAL VOC 挑战赛于 2005 年至 2012 年进行。该挑战赛被认为是物体检测技术的基准。 数据集中有 20 个类别。 该数据集包含用于训练和验证的 11,530 张图像,以及针对感兴趣区域的 27,450 条注释。 以下是数据集中存在的二十个类: + +* 人: P 人 +* 动物: B ird,猫,牛,狗,马,绵羊 +* 车辆: A 飞机,自行车,轮船,公共汽车,汽车,摩托车,火车 +* 室内: B 水壶,椅子,餐桌,盆栽,沙发,电视/显示器 + +您可以从 [http://host.robots.ox.ac.uk/pascal/VOC/voc2012/VOCtrainval_11-May-2012.tar](http://host.robots.ox.ac.uk/pascal/VOC/voc2012/VOCtrainval_11-May-2012.tar) 下载数据集。 每个图像平均有 2.4 个对象。 + +# 可可物体检测挑战 + +上下文( **COCO** )数据集中的**通用对象具有 200,000 张图像,其中 80 个类别的注释超过 500,000 个。 它是最广泛的公开可用的对象检测数据库。 下图显示了数据集中存在的对象的列表:** + +![](img/277bcdb3-8b17-4c4a-829a-e79e857a354c.png) + +每个图像的平均对象数为 7.2。 这些是物体检测挑战的著名数据集。 接下来,我们将学习如何针对这些数据集评估算法。 + +# 使用指标评估数据集 + +指标对于深度学习任务中的理解至关重要。 由于人工注释,对象检测和定位的度量是特殊的。 人类可能已经注释了一个名为 **Ground-truth** 的框。 真实性不一定是绝对真理。 此外,盒子的像素可能因人而异。 因此,算法很难检测到人类绘制的确切边界框。 **联合交叉口**( **IoU** )用于评估定位任务。 **平均精确度平均值**( **mAP** )用于评估检测任务。 我们将在下一部分中看到指标的描述。 + +# 联合路口 + +IoU 是**地面实况**与预测面积的重叠面积与总面积之比。 这是该指标的直观说明: + +![](img/6d0305cb-779a-42f0-8bfe-6834eecdca89.png) + +这两个正方形代表地面实况和预测的边界框。 IoU 计算为重叠面积与并集面积之比。 这是给定地面真理和预测边界框的 IoU 计算脚本: + +```py +def calculate_iou(gt_bb, pred_bb): + ''' + :param gt_bb: ground truth bounding box :param pred_bb: predicted bounding box ''' gt_bb = tf.stack([ + gt_bb[:, :, :, :, 0] - gt_bb[:, :, :, :, 2] / 2.0, + gt_bb[:, :, :, :, 1] - gt_bb[:, :, :, :, 3] / 2.0, + gt_bb[:, :, :, :, 0] + gt_bb[:, :, :, :, 2] / 2.0, + gt_bb[:, :, :, :, 1] + gt_bb[:, :, :, :, 3] / 2.0]) + gt_bb = tf.transpose(gt_bb, [1, 2, 3, 4, 0]) + pred_bb = tf.stack([ + pred_bb[:, :, :, :, 0] - pred_bb[:, :, :, :, 2] / 2.0, + pred_bb[:, :, :, :, 1] - pred_bb[:, :, :, :, 3] / 2.0, + pred_bb[:, :, :, :, 0] + pred_bb[:, :, :, :, 2] / 2.0, + pred_bb[:, :, :, :, 1] + pred_bb[:, :, :, :, 3] / 2.0]) + pred_bb = tf.transpose(pred_bb, [1, 2, 3, 4, 0]) + area = tf.maximum( + 0.0, + tf.minimum(gt_bb[:, :, :, :, 2:], pred_bb[:, :, :, :, 2:]) - + tf.maximum(gt_bb[:, :, :, :, :2], pred_bb[:, :, :, :, :2])) + intersection_area= area[:, :, :, :, 0] * area[:, :, :, :, 1] + gt_bb_area = (gt_bb[:, :, :, :, 2] - gt_bb[:, :, :, :, 0]) * \ + (gt_bb[:, :, :, :, 3] - gt_bb[:, :, :, :, 1]) + pred_bb_area = (pred_bb[:, :, :, :, 2] - pred_bb[:, :, :, :, 0]) * \ + (pred_bb[:, :, :, :, 3] - pred_bb[:, :, :, :, 1]) + union_area = tf.maximum(gt_bb_area + pred_bb_area - intersection_area, 1e-10) + iou = tf.clip_by_value(intersection_area / union_area, 0.0, 1.0) + return iou +``` + +地面真相和预测的边界框堆叠在一起。 然后在处理负面积的情况下计算面积。 当边界框坐标不正确时,可能会出现负区域。 框的右侧坐标很多发生在从左到左的坐标上。 由于没有保留边界框的结构,因此必然会出现负区域。 计算联合和交叉区域,然后进行最终的 IoU 计算,该计算是与 **地面 真相** 的预测面积与 总面积。 IoU 计算可以与算法结合使用,以训练定位问题。 + +# 平均平均精度 + +mAP 用于评估检测算法。 mAP 度量是检测到的边界框的精度和召回率的乘积。 mAP 值的范围是 0 到 100。数字越大,则越好。 可以通过分别为每个类别计算**平均精度**( **AP** ),然后计算该类别的平均值来计算 mAP。 仅当 mAP 高于 0.5 时,检测结果才被视为真正的阳性。 通过绘制每个类别的绘制精度/召回曲线,可以合并来自测试图像的所有检测。 曲线下的最终区域可用于算法比较。 mAP 是衡量网络灵敏度的一种很好的方法,同时不会引发很多错误警报。 我们已经了解了数据集的评估算法。 接下来,我们将研究本地化任务的算法。 + +# 本地化算法 + +定位算法是在[第 2 章](../Text/02.html),*图像分类*和[第 3 章](../Text/03.html),*图像检索*中学习的材料的扩展。 在图像分类中,图像经过 CNN(卷积神经网络)的多层。 CNN 的最后一层输出属于每个标签的概率值。 可以扩展它以定位对象。 我们将在以下各节中看到这些想法。 + +# 使用滑动窗口定位对象 + +定位的一种直观方法是使用对象预测图像的多个裁剪部分。 可以通过在图像上移动一个窗口并为每个窗口进行预测来完成图像的裁剪。 移动比图像小的窗口并根据窗口大小裁剪图像的方法称为**滑动窗口**。 可以对图像的每个裁剪窗口进行预测,这称为滑动窗口对象检测。 + +可以通过针对紧密裁剪的图像进行图像分类问题训练的深度学习模型来完成预测。 近距离修剪意味着在整个图像中只会找到一个对象。 窗口在整个图像上的移动必须一致。 图像的每个部分都会通过模型以找到分类。 这种方法有两个问题。 + +* 它只能找到与窗口大小相同的对象。 如果对象大小大于窗口大小,则滑动窗口将丢失对象。 为了克服这个问题,我们将使用**标度空间** 的概念。 +* 另一个问题是,将窗口移到像素上方可能会导致丢失一些对象。 在每个像素上移动窗口会导致大量额外的计算,因此会降低系统速度。 为避免这种情况,我们将在卷积层中加入一个技巧。 + +在下一节中,我们将介绍这两种技术。 + +# 比例空间概念 + +比例空间是使用各种大小的图像的概念。 图像会缩小为较小的尺寸,因此可以在相同尺寸的窗口中检测到较大的物体。 可以使用减小的尺寸将图像调整为某些尺寸。 通过删除替代像素或插值来调整图像大小可能会留下一些伪像。 因此,图像被平滑并迭代调整大小。 通过平滑和调整大小获得的图像是比例空间。 + +窗口在每个刻度上滑动以定位对象。 运行多个比例相当于使用更大的窗口运行图像。 在多个规模上运行的计算复杂度很高。 可以通过以准确性为代价进行快速移动来加快本地化速度。 复杂性使解决方案无法在生产中使用。 滑动窗口的思想可以通过完全卷积的滑动窗口实现而变得高效。 + +# 将完全连接的层训练为卷积层 + +滑动窗口的问题是计算复杂度。 复杂性是因为对每个窗口都进行了预测。 已针对重叠区域的每个窗口计算了深度学习功能。 可以减少裁剪窗口中重叠区域的特征计算。 解决方案是使用仅计算一次特征的全卷积网络。 为了理解全卷积网络,让我们首先看看如何将全连接层转换为`convolution_layer`。 内核更改为相同的大小,并使用与神经元数量相同的过滤器数量。 也可以对其他层重复此操作。 更改内核大小是将完全连接的层转换为`convolution_layer`的简便方法: + +```py +convolution_layer_1 = convolution_layer(x_input_reshape, 64) +pooling_layer_1 = pooling_layer(convolution_layer_1) +convolution_layer_2 = convolution_layer(pooling_layer_1, 128) +pooling_layer_2 = pooling_layer(convolution_layer_2) +dense_layer_bottleneck = convolution_layer(pooling_layer_2, 1024, [5, 5]) +logits = convolution_layer(dense_layer_bottleneck, no_classes, [1, 1]) +logits = tf.reshape(logits, [-1, 10]) +``` + +密集层表示为卷积层。 这个想法在各种情况下都是强大而有用的。 我们将扩展此思想,以将滑动窗口表示为完整的卷积网络。 + +# 滑动窗口的卷积实现 + +在这种技术中,最终目标不是滑动,而是变成一些需要深度的目标,并需要多个框作为窗口。 Sermanet 等。 ( [https://arxiv.org/pdf/1312.6229.pdf](https://arxiv.org/pdf/1312.6229.pdf) )使用完全卷积实现来克服滑动窗口的这一问题。 这是滑动窗口的这种卷积实现的说明: + +![](img/615888e7-8aac-425c-83be-0802095b2cd8.png) + +经 Sermanet 等人许可复制。 + +在示例的上部,常规分类表示为完全卷积层。 在该图的下部,相同的内核应用于更大的图像,最后生成 2x2 而不是 1。最后一层表示这些边界框的输出中的四个。 具有用于预测的体积可以提高效率,但是盒子仍然存在准确定位的问题。 因此不需要滑动窗口,因此解决了复杂性。 纵横比总是在变化,必须在多个比例尺上看到。 通过完全卷积方法生成的边界框不是很准确。 仅针对额外区域进行额外计算。 可以想像,这些盒子仅限于经过训练的盒子的数量。 接下来,我们将看到一种更准确地检测边界框位置的方法。 + +# 将本地化视为回归问题 + +思考本地化的一种基本方法是将问题建模为回归问题。 边界框是四个数字,因此可以通过回归设置以直接方式进行预测。 我们还需要预测标签,这是一个分类问题。 + +有不同的参数化可用于定义边界框。 边界框通常有四个数字。 表示形式之一是坐标的中心以及边界框的高度和宽度。 通过删除完全连接的层并用回归编码器替换它,可以使用预训练的模型。 必须使用 L2 损失对回归进行正则化,而 L2 损失在异常值方面表现不佳。 L1 的损失比 L1 好。 用平滑化的正则化交换回归更好。 对模型进行微调可提供良好的准确性,而对整个网络进行训练只会带来微不足道的性能改善。 在训练时间和准确性之间进行权衡。 接下来,我们将看到使用卷积网络进行回归的不同应用。 + +# 将回归应用于其他问题 + +回归图像坐标适用于其他几种应用程序,例如**姿态检测**和**基准点检测**。 姿势检测是在人体中发现关节位置的动作,如下所示: + +![](img/9e37fff6-eae3-4729-8cf6-c015a4e0b85f.png) + +在上一个图像中,检测到多个位置,例如头部,颈部,肩膀,脚踝和手。 这可以扩展到所有人类部分。 我们了解到的回归可以用于此应用程序。 这是基准点检测的示例: + +![](img/6bfd58bb-e261-4dee-8b30-25c3f884e729.png) + +基准点是脸部相对于眼睛,鼻子和嘴唇的位置的地标。 找到这些地标对于基于面部的增强现实应用至关重要。 人脸识别中还有更多地标可用,将在[第 6 章](../Text/06.html),*相似性学习*中详细介绍。 + +# 结合回归与滑动窗口 + +为滑动窗口方法或完全卷积方法中的每个窗口计算分类分数,以了解该窗口中存在哪些对象。 代替预测用于检测对象的每个窗口的分类得分,可以利用分类得分来预测每个窗口本身。 结合滑动窗口,比例空间,完全卷积和回归之类的所有想法,比任何单独的方法都具有更好的结果。 以下是各种网络使用回归方法在`ImageNet`数据集上获得的前五个定位错误率: + +![](img/e4493c04-62ab-4564-9465-cb134209a626.png) + +上图显示网络越深,结果越好。 对于 AlexNet,本地化方法未在本文中描述。 OverFeat 使用带有框合并的多尺度卷积回归。 VGG 使用了本地化,但比例尺和位置较少。 这些收益归因于深层特征。 ResNet 使用不同的本地化方法和更深入的功能。 + +回归编码器和分类编码器独立运行。 因此,有可能预测边界框的标签不正确。 通过在不同的图层上附加回归编码器可以解决此问题。 该方法也可以用于多个物体,从而解决了物体检测问题。 给定一个图像,找到其中的所有实例。 很难将检测视为回归,因为输出的数量是可变的。 一个图像可能有两个对象,而另一个可能有三个或更多。 在下一节中,我们将看到更有效地处理检测问题的算法。 + +# 检测物体 + +对象检测算法有多种变体。 这里讨论了对象检测 API 附带的一些算法。 + +# 卷积神经网络(R-CNN)的区域 + +该系列的第一个工作是 Girshick 等人提出的 CNN 区域( [https://arxiv.org/pdf/1311.2524.pdf](https://arxiv.org/pdf/1311.2524.pdf) )。 它提出了一些框,并检查是否有任何框对应于基本事实。 **选择性搜索**用于这些地区提案。 选择性搜索通过对各种大小的窗口的颜色/纹理进行分组来建议区域。 选择性搜索寻找斑点样的结构。 它以一个像素开始,并在更大范围内产生斑点。 它产生了大约 2,000 个区域提案。 与所有可能的滑动窗口相比,该区域建议更少。 + +调整提案的大小并通过标准的 CNN 体系结构,例如 Alexnet / VGG / Inception / ResNet。 CNN 的最后一层是通过 SVM 进行训练的,该 SVM 使用无对象类来标识对象。 通过拉紧图像周围的框可以进一步改善框。 使用对象区域建议训练用于预测更近边界框的线性回归模型。 R-CNN 的架构如下所示: + +![](img/4f45a709-739b-41a7-969e-56589fecb96f.png) + +经 Girshick 等人许可复制。 + +编码器可以是标准深度学习模型的预训练模型。 从训练数据为所有区域计算特征。 存储功能,然后训练 SVM。 接下来,用标准化坐标训练边界框。 在图像坐标之外可能会有一些建议,因此将其标准化以进行训练和推理。 + +这种方法的缺点是: + +* 通过选择性搜索形成了多个建议,因此必须计算许多推论,通常约为 2,000 +* 必须对三个分类器进行训练,这会增加参数的数量 +* 没有端到端的培训 + +# 快速 R-CNN + +Girshick 等人提出的 Fast R-CNN。 ( [https://arxiv.org/pdf/1504.08083.pdf](https://arxiv.org/pdf/1504.08083.pdf) )方法仅运行一次 CNN 推理,因此减少了计算量。 CNN 的输出用于建议网络并选择边界框。 它介绍了一种称为**感兴趣区域池**的技术。 感兴趣区域池采用 CNN 功能,并根据区域将它们合并在一起。 合并使用 CNN 进行推理后获得的特征,并选择区域,如下图所示: + +![](img/9607861d-014a-4574-980d-90fb23f91ae4.png) + +经 Girshick 等人许可复制。 + +这样,执行端到端训练,避免了多个分类器。 请注意,SVM 被 softmax 层替换,并且框回归器由边界框回归器代替。 仍然存在的缺点是选择性搜索,这需要一些时间。 + +# 更快的 R-CNN + +Ren 等人提出了更快的 R-CNN。 ( [https://arxiv.org/pdf/1506.01497.pdf](https://arxiv.org/pdf/1506.01497.pdf) )。 Faster R-CNN 和 Fast R-CNN 方法之间的区别在于,Faster R-CNN 使用诸如 VGG 和 Inception 等体系结构的 CNN 功能来提案而不是选择性搜索。 CNN 功能进一步通过区域提议网络传递。 滑动窗口通过具有潜在边界框和分数的要素传递,并输出一些直观的长宽比,模型输出边界框和分数: + +![](img/ea103e8f-b553-4d9d-89cd-077888fb7db0.png) + +经 Ren 等人许可复制。 + +更快的 R-CNN 比快速 R-CNN 更快,因为它通过仅计算一次功能来节省计算量。 + +# 单发多盒探测器 + +SSD(单发多盒)是所有方法中最快的。 此方法同时预测对象并找到边界框。 在培训期间,可能会有很多负面因素,因此很难否定地挖掘班级失衡。 CNN 的输出具有各种大小的功能。 这些被传递到 3x3 卷积滤波器以预测边界框。 + +此步骤将预测对象和边界框: + +![](img/28e92ddf-38ea-4b41-80e6-add3753d03d2.png) + +经 Liu 等人许可复制。 + +这些是可用于对象检测的算法,我们将在下一节中学习如何实现它们。 + +# 对象检测 API + +Google 发布了经过预先训练的模型,并在`COCO`数据集上对各种算法进行了训练,以供公众使用。 该 API 建立在 TensorFlow 之上,旨在用于构建,训练和部署对象检测模型。 这些 API 支持对象检测和本地化任务。 预训练模型的可用性可对新数据进行微调,从而加快训练速度。 这些不同的模型在速度和准确性之间进行权衡。 + +# 安装与设定 + +使用以下命令安装协议缓冲区 ***(*** **protobuf)**编译器。 为 protobuf 创建目录并直接下载该库: + +```py +mkdir protoc_3.3 +cd protoc_3.3 +wget [https://github.com/google/protobuf/releases/download/v3.3.0/protoc-3.3.0-linux-x86_64.zip](https://github.com/google/protobuf/releases/download/v3.3.0/protoc-3.3.0-linux-x86_64.zip) +``` + +更改文件夹的权限并提取内容,如下所示: + +```py +chmod 775 protoc-3.3.0-linux-x86_64.zip +unzip protoc-3.3.0-linux-x86_64.zip +``` + +协议缓冲区(protobuf)是 Google 的语言无关,平台无关的可扩展机制,用于序列化结构化数据。 它用于 XML 的使用,但是更加简单快捷。 模型通常在 TensorFlow 中导出为这种格式。 一个人可以定义一次数据结构,但可以用多种语言进行读写。 然后运行以下命令来编译 protobuf。 返回到工作文件夹,然后 c 从 [https://github.com/tensorflow/models.git](https://github.com/tensorflow/models.git) 克隆存储库,并将它们移至以下文件夹: + +```py +git clone https://github.com/tensorflow/models.git +``` + +现在,使用以下代码将模型移至研究文件夹: + +```py +cd models/research/ +~/protoc_3.3/bin/protoc object_detection/protos/*.proto --python_out=. +``` + +TensorFlow 对象检测 API 使用 protobuf 导出模型权重和训练参数。 TensorFlow ,模型,研究和苗条目录应通过以下命令附加到`PYTHONPATH`: + +```py +export PYTHONPATH=.:./slim/ +``` + +使用前面的命令添加到 python 路径仅一次。 对于下一个,该命令必须再次运行。 可以通过运行以下代码来测试安装: + +```py +python object_detection/builders/model_builder_test.py +``` + +此代码的输出在此处给出: + +```py +Ran 7 tests in 0.022s + +OK +``` + +可以从 [https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/installation.md](https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/installation.md) 获得有关安装的更多信息。 现在安装已完成并经过测试。 + +# 预训练模型 + +有几种模型已经过预先训练并可以使用。 所有这些模型都在`COCO`数据集上进行了训练,可用于检测`COCO`数据集中可用的对象,例如人和汽车。 这些模型对于新任务(例如交通标志检测)的转移学习也很有用。 此处显示了经过预训练的模型的表格,其中包含`COCO`数据集上的相对速度和 mAP。 使用不同的 CNN 训练了各种算法,并在名称中进行了描述: + +| **型号名称** | **速度** | **可可地图** | +| `ssd_mobilenet_v1_coco` | 快速 | 21 | +| `ssd_inception_v2_coco` | 快速 | 24 | +| `rfcn_resnet101_coco` | 中 | 30 | +| `faster_rcnn_resnet101_coco` | 中 | 32 | +| `faster_rcnn_inception_resnet_v2_atrous_coco` | 慢 | 37 | + +根据需求,可以从模型中进行选择。 下载在 Mobilenet 上训练的 SSD 模型,并通过转到工作目录将其提取,如下所示: + +```py +mkdir Chapter04 && cd Chapter04 +wget http://download.tensorflow.org/models/object_detection/ssd_mobilenet_v1_coco_11_06_2017.tar.gz +tar -xzvf ssd_mobilenet_v1_coco_11_06_2017.tar.gz +``` + +`Chapter04`文件夹中将包含各种文件,这些文件在此处列出: + +* 这是图形的原始定义-`graph.pbtxt` +* 图的权重已冻结,可以用于推断-`frozen_inference_graph.pb` +* 检查点文件 + * `model.ckpt.data-00000-of-00001` + * `model.ckpt.meta` + * `model.ckpt.index` + +下一部分将使用此模型执行检测任务。 + +# 重新训练物体检测模型 + +使用相同的 API,我们可以为自定义数据集重新训练模型。 定制数据的训练涉及数据集的准备,选择算法以及执行微调。 整个管道可以作为参数传递给训练脚本。 训练数据必须转换为 TensorFlow 记录。 TensorFlow 记录是 Google 提供的一种文件格式,可以使数据读取比常规文件更快。 现在,我们将逐步进行培训。 + +# Pet 数据集的数据准备 + +本示例使用 Oxford-IIIT `Pet`数据集。 使用这些命令从`Chapter04`目录下载图像和注释。 + +```py +wget http://www.robots.ox.ac.uk/~vgg/data/pets/daimg.tar.gz +wget http://www.robots.ox.ac.uk/~vgg/data/pets/data/annotations.tar.gz +``` + +提取图像和注释,如下所示: + +```py +tar -xvf images.tar.gz +tar -xvf annotations.tar.gz +``` + +创建`pet_tf`记录文件以在`tf`记录中创建数据集,因为它们是对象检测训练器的必需输入。 可在`object_detection/data/pet_label_map.pbtxt`找到`Pet`数据集的`label_map`。 移至`research`文件夹并运行以下命令: + +```py +python object_detection/create_pet_tf_record.py \ + --label_map_path=object_detection/data/pet_label_map.pbtxt \ + --data_dir=~/chapter4/. \ + --output_dir=~/chapter4/. +``` + +您可以在研究目录`pet_train.record`和`pet_val.record`中看到两个`.record`文件。 + +# 目标检测训练流水线 + +培训 protobuf 必须配置为进行培训。 在此过程中,以下五件事很重要: + +* 具有型号类型的型号配置 +* `train_config`用于标准训练参数 +* 必须报告的指标的`eval_config` +* 数据集的`train_input_`配置 +* 评估数据集的`eval_input_`配置 + +我们将使用 [https://github.com/tensorflow/models/blob/master/research/object_detection/samples/configs/ssd_mobilenet_v1_pets.config](https://github.com/tensorflow/models/blob/master/research/object_detection/samples/configs/ssd_mobilenet_v1_pets.config) 中的配置文件。 通过运行以下命令将其下载到`Chapter04`文件夹。 打开`config`文件并编辑以下行: + +```py +fine_tune_checkpoint: "~/Chapter04/ssd_mobilenet_v1_coco_11_06_2017/model.ckpt" + +train_input_reader: { + tf_record_input_reader { + input_path: "~/Chapter04/pet_train.record" + } + label_map_path: "~/model/research/object_detection/data/pet_label_map.pbtxt" +} + +eval_input_reader: { + tf_record_input_reader { + input_path: "~/Chapter04/pet_val.record" + } + label_map_path: "~/model/research/object_detection/data/pet_label_map.pbtxt" +} +``` + +保存`config`文件。 文件中有各种参数会影响模型的准确性。 + +# 训练模型 + +现在,API,数据和配置文件已准备好进行重新培训。 可以通过以下命令触发训练: + +```py +PYTHONPATH=.:./slim/. python object_detection/train.py \ + --logtostderr \ + --pipeline_config_path=~/chapter4/ssd_mobilenet_v1_pets.config \ + --train_dir=~/Chapter04 +``` + +培训将从大约 140 的损失开始,并将持续减少。 培训将永远进行,必须使用 *Ctrl* + *C* 命令手动将其终止。 训练期间创建的检查点可在以后用于推理。 + +# 使用 TensorBoard 监控损失和准确性 + +训练损失和准确性可以使用 TensorBoard 进行监视。 使用以下命令运行 TensorBoard: + +```py +tensorboard --logdir=/home/ubuntu/Chapter04 +``` + +培训和评估都可以在 TensorBoard 中可视化。 + +# 训练自动驾驶汽车的行人检测 + +可以在 [http://pascal.inrialpes.fr/data/human/](http://pascal.inrialpes.fr/data/human/) 中找到用于训练行人物体检测的数据集。 可以在 [https://github.com/diegocavalca/machine-learning/blob/master/supervisioned/object.detection_tensorflow/simple.detection.ipynb](https://github.com/diegocavalca/machine-learning/blob/master/supervisioned/object.detection_tensorflow/simple.detection.ipynb) 中找到检测行人的步骤。 可以从 [http://www.vision.ee.ethz.ch/~timofter/traffic_signs/](http://www.vision.ee.ethz.ch/~timofter/traffic_signs/) 和 [http://btsd.ethz.ch/下载用于训练符号检测器的数据集。 shareddata /](http://btsd.ethz.ch/shareddata/) 。 对于无人驾驶汽车,图像中将有四个类别用于标记:行人,汽车,摩托车和背景。 当不存在任何后台类时,必须检测到该后台类。 训练深度学习分类模型的一个假设是,至少一个对象将出现在图像中。 通过添加`background`类,我们克服了这个问题。 神经网络还可以根据标签生成对象的边界框。 + +# YOLO 对象检测算法 + +最近的对象检测算法是**。您只看过一次**( **YOLO** )。 图像分为多个网格。 图像的每个网格单元都运行相同的算法。 让我们通过定义带有初始化器的层来开始实现: + +```py +def pooling_layer(input_layer, pool_size=[2, 2], strides=2, padding='valid'): + layer = tf.layers.max_pooling2d( + inputs=input_layer, + pool_size=pool_size, + strides=strides, + padding=padding + ) + add_variable_summary(layer, 'pooling') + return layer + +def convolution_layer(input_layer, filters, kernel_size=[3, 3], padding='valid', + activation=tf.nn.leaky_relu): + layer = tf.layers.conv2d( + inputs=input_layer, + filters=filters, + kernel_size=kernel_size, + activation=activation, + padding=padding, + weights_initializer=tf.truncated_normal_initializer(0.0, 0.01), + weights_regularizer=tf.l2_regularizer(0.0005) + ) + add_variable_summary(layer, 'convolution') + return layer + +def dense_layer(input_layer, units, activation=tf.nn.leaky_relu): + layer = tf.layers.dense( + inputs=input_layer, + units=units, + activation=activation, + weights_initializer=tf.truncated_normal_initializer(0.0, 0.01), + weights_regularizer=tf.l2_regularizer(0.0005) + ) + add_variable_summary(layer, 'dense') + return layer +``` + +可以注意到,激活层为`leaky_relu`,权重以截断的正态分布初始化。 这些修改的图层可用于构建模型。 该模型如下创建: + +```py +yolo = tf.pad(images, np.array([[0, 0], [3, 3], [3, 3], [0, 0]]), name='pad_1') +yolo = convolution_layer(yolo, 64, 7, 2) +yolo = pooling_layer(yolo, [2, 2], 2, 'same') +yolo = convolution_layer(yolo, 192, 3) +yolo = pooling_layer(yolo, 2, 'same') +yolo = convolution_layer(yolo, 128, 1) +yolo = convolution_layer(yolo, 256, 3) +yolo = convolution_layer(yolo, 256, 1) +yolo = convolution_layer(yolo, 512, 3) +yolo = pooling_layer(yolo, 2, 'same') +yolo = convolution_layer(yolo, 256, 1) +yolo = convolution_layer(yolo, 512, 3) +yolo = convolution_layer(yolo, 256, 1) +yolo = convolution_layer(yolo, 512, 3) +yolo = convolution_layer(yolo, 256, 1) +yolo = convolution_layer(yolo, 512, 3) +yolo = convolution_layer(yolo, 256, 1) +yolo = convolution_layer(yolo, 512, 3) +yolo = convolution_layer(yolo, 512, 1) +yolo = convolution_layer(yolo, 1024, 3) +yolo = pooling_layer(yolo, 2) +yolo = convolution_layer(yolo, 512, 1) +yolo = convolution_layer(yolo, 1024, 3) +yolo = convolution_layer(yolo, 512, 1) +yolo = convolution_layer(yolo, 1024, 3) +yolo = convolution_layer(yolo, 1024, 3) +yolo = tf.pad(yolo, np.array([[0, 0], [1, 1], [1, 1], [0, 0]])) +yolo = convolution_layer(yolo, 1024, 3, 2) +yolo = convolution_layer(yolo, 1024, 3) +yolo = convolution_layer(yolo, 1024, 3) +yolo = tf.transpose(yolo, [0, 3, 1, 2]) +yolo = tf.layers.flatten(yolo) +yolo = dense_layer(yolo, 512) +yolo = dense_layer(yolo, 4096) + +dropout_bool = tf.placeholder(tf.bool) +yolo = tf.layers.dropout( + inputs=yolo, + rate=0.4, + training=dropout_bool + ) +yolo = dense_layer(yolo, output_size, None) +``` + +堆叠了几个卷积层,生成了 YOLO 网络。 该网络用于创建用于实时检测的对象检测算法。 + +# 摘要 + +在本章中,我们了解了对象定位和检测任务之间的区别。 讨论了几个数据集和评估标准。 讨论了各种解决定位问题和算法的方法,例如用于检测的 R-CNN 和 SSD 模型的变体。 涵盖了在开源存储库中执行检测的过程。 我们使用该技术训练了行人检测模型。 我们还了解了在训练此类模型时需要进行的各种取舍。 + +在下一章中,我们将学习语义分割算法。 我们将使用该知识来实现医学成像和卫星成像问题的分割算法。 \ No newline at end of file diff --git a/docs/dl-cv/05.md b/docs/dl-cv/05.md new file mode 100644 index 00000000..9c846222 --- /dev/null +++ b/docs/dl-cv/05.md @@ -0,0 +1,535 @@ +# 语义分割 + +在本章中,我们将学习各种语义分割技术并为其训练模型。 分割是逐像素分类任务。 解决分割问题的思想是对对象检测问题的扩展。 分割在医学和卫星图像理解等应用中非常有用。 + +本章将涵盖以下主题: + +* 学习语义分割和实例分割之间的区别 +* 细分数据集和指标 +* 语义分割算法 +* 分割在医学和卫星图像中的应用 +* 实例分割算法 + +# 预测像素 + +图像分类是预测标签或类别的任务。 对象检测是预测几种基于深度学习的算法及其相应边界框的列表的任务。 边界框可能在其中包含除检测到的对象以外的其他对象。 在某些应用中,将每个像素标记到标签很重要,而不是可能包含多个对象的边框。 **语义分割**是预测逐像素标签的任务。 + +这是图像及其对应的语义分割的示例: + +![](img/ecd4a853-47b0-4b64-959d-e659b7a506a8.jpeg) ![](img/f3d07c15-d6fe-4677-9b43-9628b38bc9d4.jpg) > + +如图中所示,使用每个像素的标签预测输入图像。 标签可以是天空,树木,人,山和桥。 标签不是分配给整个图像,而是分配给每个像素。 语义分割独立标记像素。 您会注意到每个人都没有区别。 图像中的所有人员均以相同的方式标记。 + +这是区分相同标签的每个实例的示例: + +![](img/1c6a1b46-8d59-4882-99a5-cdf1ddfc4ef0.jpg) + +用像素标记分割每个实例的任务称为**实例分割**。 实例分割可以被认为是具有像素级标签的对象检测的扩展。 语义分段和实例分段的应用非常广泛,下一部分将提供一些应用。 + +# 诊断医学影像 + +可以使用分割技术来诊断医学图像。 现代医学成像技术,例如**磁共振成像**( **MRI** ),**计算机断层扫描**( **CT** )和**视网膜病变**创建高质量的图像。 可以将通过这种技术生成的图像划分为多个区域,以检测来自脑部扫描的肿瘤或来自视网膜扫描的斑点。 一些设备提供体积图像,这些图像也可以通过分段进行分析。 分割视频以进行机器人手术,使医生能够在机器人协助的手术中仔细查看区域。 在本章的后面,我们将看到如何分割医学图像。 + +# 通过卫星图像了解地球 + +卫星图像最近变得越来越丰富。 卫星捕获的图像提供了地球整个表面的高分辨率视图。 通过分析卫星图像,我们可以了解有关地球的几件事,例如: + +* 衡量与经济增长相关的国家的建筑率 +* 测量油箱 +* 规划和组织交通 +* 计算森林砍伐及其影响 +* 通过对动物进行计数并跟踪其运动来帮助保护野生动植物 +* 发现考古遗址 +* 绘制自然灾害造成的损坏区域 + +卫星图像还有更多的应用可能。 对于上述大多数问题,解决方案始于卫星图像的分割。 在本章的后面,我们将看到如何分割卫星图像。 + +# 使机器人能够看到 + +分割场景对于机器人看清周围世界并与之互动至关重要。 工业和家用机器人必须处理这些物体。 一旦根据对象跨越了机器人的视野,就可以进行处理。 还有更多值得一提的应用程序: + +* 对缺陷进行细分的工具的工业检查 +* 时装行业的色彩诊断; 可以将图像与各种时尚对象进行分割并将其用于颜色解析 +* 区分背景与前景以应用人像效果 + +在下一部分中,我们将学习一些用于评估细分算法的公共数据集。 + +# 数据集 + +[第 4 章](../Text/04.html),*对象检测*中提到的`PASCAL`和`COCO`数据集也可以用于分割任务。 注释是不同的,因为它们是按像素标记的。 新算法通常以`COCO`数据集为基准。 `COCO`还具有诸如草,墙和天空之类的东西数据集。 像素精度属性可用作评估算法的指标。 + +除了上面提到的那些以外,在医学影像和卫星影像领域还有其他几个数据集。 这里提供了指向其中一些链接的供您参考: + +* [http://www.cs.bu.edu/~betke/BiomedicalImageSegmentation](http://www.cs.bu.edu/~betke/BiomedicalImageSegmentation/) +* [https://www.kaggle.com/c/intel-mobileodt-cervical-cancer-screening/data](https://www.kaggle.com/c/intel-mobileodt-cervical-cancer-screening/data) +* [https://www.kaggle.com/c/diabetic-retinopathy-detection](https://www.kaggle.com/c/diabetic-retinopathy-detection) +* [https://grand-challenge.org/all_challenges](https://grand-challenge.org/all_challenges/) +* [http://www.via.cornell.edu/databases](http://www.via.cornell.edu/databases/) +* [https://www.kaggle.com/c/dstl-satellite-imagery-feature-detection](https://www.kaggle.com/c/dstl-satellite-imagery-feature-detection) +* [https://aws.amazon.com/public-datasets/spacenet](https://aws.amazon.com/public-datasets/spacenet/) +* [https://www.iarpa.gov/challenges/fmow.html](https://www.iarpa.gov/challenges/fmow.html) +* [https://www.kaggle.com/c/planet-understanding-the-amazon-from-space](https://www.kaggle.com/c/planet-understanding-the-amazon-from-space) + +为分割任务创建训练数据非常昂贵。 有在线工具可用于注释数据集。 麻省理工学院 **MIT 大学**提供的 **LabelMe** 移动应用程序非常适合注释,可以从[下载 http://labelme.csail.mit.edu/Release3.0](http://labelme.csail.mit.edu/Release3.0) 。 + +# 语义分割算法 + +提出了几种基于深度学习的算法来解决图像分割任务。 可以在像素级别应用滑动窗口方法进行分割。 滑动窗口方法会拍摄图像并将图像分成较小的作物。 图像的每种裁剪都被分类为标签。 这种方法昂贵且效率低下,因为它不会重用重叠补丁之间的共享功能。 在以下各节中,我们将讨论一些可以克服此问题的算法。 + +# 完全卷积网络 + +**全卷积网络**( **FCN** )引入了端到端卷积网络的思想。 通过删除完全连接的层,可以将任何标准的 CNN 架构用于 FCN,其实现在[第 4 章](../Text/04.html),*对象检测*中进行了显示。 完全连接的层被卷积层代替。 最终层的深度较高,尺寸较小。 因此,可以执行一维卷积以达到所需的标签数量。 但是对于分割,必须保留空间尺寸。 因此,构建完整的卷积网络时没有最大池,如下所示: + +![](img/3b75d8a1-168a-4625-b727-169ebb0425f4.png) + +该网络的损耗是通过平均每个像素和小批量的交叉熵损耗来计算的。 最后一层的深度等于类数。 FCN 与对象检测相似,只是保留了空间尺寸。 由于某些像素可能会被错误预测,因此该架构产生的输出将很粗糙。 计算量很大,在下一节中,我们将看到如何解决此问题。 + +# SegNet 架构 + +**SegNet** 具有编码器和解码器方法。 编码器具有各种卷积层,而解码器具有各种解卷积层。 SegNet 改进了 FCN 产生的粗略输出。 因此,它的内存占用较少。 当特征尺寸减小时,通过反卷积将其再次上采样至图像大小,从而反转了卷积效果。 反卷积学习用于上采样的参数。 由于池层中信息的丢失,这种体系结构的输出将很粗糙。 + +![](img/0bca20d8-04c9-48cb-8029-e8af3258c09f.png) + +现在,让我们学习几个新概念,这些概念称为上采样,无规则卷积和转置卷积,它们将帮助我们更好地理解该网络。 + +# 通过合并对图层进行上采样 + +在[第 1 章](../Text/01.html)和*入门*中,我们讨论了最大池化。 最大池化是一种从窗口中选取最大值的采样策略。 对于上采样,可以相反。 每个值都可以用零包围,以对该层进行上采样,如下所示: + +![](img/6c6fa429-432a-4bc4-af17-015e872d5490.png) + +将零添加到与上采样数字相同的位置。 通过记住下采样的位置并将其用于上采样,可以改善解池功能,如下所示: + +![](img/7793c888-12eb-40b1-944f-85470767c43c.png) + +从索引角度来看,上采样比附加零产生更好的结果。 这种通过池对层进行上采样的方法是无法学到的,并且可以按原样工作。 接下来,我们将看到如何使用可学习的参数进行上采样和下采样。 + +# 通过卷积对图层进行采样 + +可以使用卷积直接对图层进行升采样或降采样。 可以增加用于卷积的步幅以引起下采样,如下所示: + +![](img/d2cd8c90-7707-4786-a5bc-e40b08a8a35d.png) + +通过卷积的下采样被称为**无孔卷积**或**扩张卷积**或**大卷积**。 类似地,可以通过学习内核将其反转为升采样,如下所示: + +![](img/9e542e78-5c13-4d7f-8d96-e5cedc35bdec.png) + +直接使用卷积的上采样可以称为**转置卷积**。 其他一些同义词是**反卷积**或**分数步卷积**或**上卷积**。 现在,了解了升采样的过程。 这是描述先前算法的代码片段: + +```py +input_height = 360 input_width = 480 kernel = 3 filter_size = 64 pad = 1 pool_size = 2 +``` + +输入之后,它遵循大小逐渐减小的通常的卷积神经网络,可以称为编码器。 以下代码可用于定义编码器: + +```py +model = tf.keras.models.Sequential() +model.add(tf.keras.layers.Layer(input_shape=(3, input_height, input_width))) + +# encoder model.add(tf.keras.layers.ZeroPadding2D(padding=(pad, pad))) +model.add(tf.keras.layers.Conv2D(filter_size, kernel, kernel, + border_mode='valid')) +model.add(tf.keras.layers.BatchNormalization()) +model.add(tf.keras.layers.Activation('relu')) +model.add(tf.keras.layers.MaxPooling2D(pool_size=(pool_size, pool_size))) + +model.add(tf.keras.layers.ZeroPadding2D(padding=(pad, pad))) +model.add(tf.keras.layers.Conv2D(128, kernel, kernel, border_mode='valid')) +model.add(tf.keras.layers.BatchNormalization()) +model.add(tf.keras.layers.Activation('relu')) +model.add(tf.keras.layers.MaxPooling2D(pool_size=(pool_size, pool_size))) + +model.add(tf.keras.layers.ZeroPadding2D(padding=(pad, pad))) +model.add(tf.keras.layers.Conv2D(256, kernel, kernel, border_mode='valid')) +model.add(tf.keras.layers.BatchNormalization()) +model.add(tf.keras.layers.Activation('relu')) +model.add(tf.keras.layers.MaxPooling2D(pool_size=(pool_size, pool_size))) + +model.add(tf.keras.layers.ZeroPadding2D(padding=(pad, pad))) +model.add(tf.keras.layers.Conv2D(512, kernel, kernel, border_mode='valid')) +model.add(tf.keras.layers.BatchNormalization()) +model.add(tf.keras.layers.Activation('relu')) +``` + +可以使用以下代码将编码器的输出以增大的尺寸馈入解码器: + +```py +# decoder model.add(tf.keras.layers.ZeroPadding2D(padding=(pad, pad))) +model.add(tf.keras.layers.Conv2D(512, kernel, kernel, border_mode='valid')) +model.add(tf.keras.layers.BatchNormalization()) + +model.add(tf.keras.layers.UpSampling2D(size=(pool_size, pool_size))) +model.add(tf.keras.layers.ZeroPadding2D(padding=(pad, pad))) +model.add(tf.keras.layers.Conv2D(256, kernel, kernel, border_mode='valid')) +model.add(tf.keras.layers.BatchNormalization()) + +model.add(tf.keras.layers.UpSampling2D(size=(pool_size, pool_size))) +model.add(tf.keras.layers.ZeroPadding2D(padding=(pad, pad))) +model.add(tf.keras.layers.Conv2D(128, kernel, kernel, border_mode='valid')) +model.add(tf.keras.layers.BatchNormalization()) + +model.add(tf.keras.layers.UpSampling2D(size=(pool_size, pool_size))) +model.add(tf.keras.layers.ZeroPadding2D(padding=(pad, pad))) +model.add(tf.keras.layers.Conv2D(filter_size, kernel, kernel, border_mode='valid')) +model.add(tf.keras.layers.BatchNormalization()) + +model.add(tf.keras.layers.Conv2D(nClasses, 1, 1, border_mode='valid', )) +``` + +解码图像的大小与输入的大小相同,并且可以使用以下代码来训练整个模型: + +```py +model.outputHeight = model.output_shape[-2] +model.outputWidth = model.output_shape[-1] + +model.add(tf.keras.layers.Reshape((nClasses, model.output_shape[-2] * model.output_shape[-1]), + input_shape=(nClasses, model.output_shape[-2], model.output_shape[-1]))) + +model.add(tf.keras.layers.Permute((2, 1))) +model.add(tf.keras.layers.Activation('softmax')) + +model.compile(loss="categorical_crossentropy", optimizer=tf.keras.optimizers.Adam, metrics=['accuracy']) +``` + +这种对图像进行编码和解码的方式克服了基于 FCN 的模型的缺点。 接下来,我们将看到具有膨胀卷积的不同概念。 + +# 跳过连接以进行更好的培训 + +分割输出的粗糙程度可以通过跳过体系结构来限制,并且可以获得更高的分辨率。 另一种替代方法是按比例放大最后三层并将其平均,如下所示: + +![](img/1cf06efe-233a-40d0-9fa5-7f53ab8f18dc.png) + +此算法将在后面的部分中用作卫星图像的示例。 + +# 膨胀卷积 + +逐像素分类和图像分类在结构上不同。 因此,减少信息的池化层将产生粗略的分段。 但是请记住,池化对于拥有更广阔的视野并允许采样至关重要。 引入了一种名为**扩张卷积**的新想法,以解决该问题,从而在进行更广阔视野的同时减少损耗。 扩大的卷积本质上是通过跳过窗口中的每个像素来进行卷积,如下所示: + +![](img/1caea247-6635-401e-a844-b023c12f9366.png) + +膨胀距离随层而变化。 这样的分割结果的输出被放大以得到更精细的分辨率。 训练了一个单独的网络以进行多尺度聚合。 + +# 深度实验室 + +Chen 等人提出的 DeepLab。 ( [https://arxiv.org/pdf/1606.00915.pdf](https://arxiv.org/pdf/1606.00915.pdf) )在多个尺度上执行卷积,并使用来自各种尺度的特征获得分数图。 然后对得分图进行插值,并通过**条件随机字段**( **CRF** )进行最终分割。 图像的这种比例处理可以通过使用其自己的 CNN 处理各种大小的图像,或者通过具有不同水平的卷积卷积的并行卷积来执行。 + +![](img/c958122d-5ce1-4d41-89a1-d2203bb2c7f5.png) + +经 Chen 等人许可复制。 + +# 互联网 + +膨胀卷积需要更大的输入,因此占用大量内存。 使用高分辨率图片时,这会带来计算问题。 里德等。 ( [https://arxiv.org/pdf/1611.06612.pdf](https://arxiv.org/pdf/1611.06612.pdf) )提出了一种称为 RefiNet 的方法来克服此问题,如下所示: + +![](img/db354e6d-4923-4bbc-947a-44c5ba5be7c7.png) + +经 Reid 等人许可复制。 + +RefiNet 使用编码器,然后使用解码器。 CNN 的编码器输出。 解码器连接各种大小的功能: + +![](img/836b137d-c3de-4c2c-baa2-c6e86dd1b6fc.png) + +经 Reid 等人许可复制。 + +串联完成后会放大低维特征。 + +# PSP 网 + +Zhoa 等人介绍的 PSPnet 中使用了全局内容。 (https://arxiv.org/pdf/1612.01105.pdf),方法是增加池化层的内核大小。 汇集以金字塔的方式进行。 金字塔同时覆盖图像的各个部分和大小。 架构之间存在损失,因此无法进行适当的监管。 + +![](img/1a1c9433-d75c-4c5d-add8-1a1c9fc9ec8b.png) + +经 Zhao 等人许可复制。 + +# 大内核很重要 + +Peng 等。 ( [https://arxiv.org/pdf/1703.02719.pdf](https://arxiv.org/pdf/1703.02719.pdf) )展示了大内核的重要性。 大内核比小内核具有更大的接受范围。 这些大内核的计算复杂度可用于以较小的内核来克服。 最后有一个边界优化网络。 + +![](img/3f5d488c-04a0-4396-a2a8-c1fb2bcbf1aa.png) + +经 Peng 等人许可复制。 + +# DeepLab v3 + +Chen 等人在论文中使用批处理归一化。 ( [https://arxiv.org/pdf/1706.05587.pdf](https://arxiv.org/pdf/1706.05587.pdf) )以提高性能。 功能的多尺度以级联方式编码,以提高性能: + +![](img/301dc866-0bfd-4010-a5f1-df4e33e12e34.png) + +经 Chen 等人许可复制。 + +![](img/65958e56-9dfc-4075-8de4-d31f9bd2662e.png) + +经 Chen 等人许可复制。 + +我们已经看到了几种架构可以使用深度学习提高图像分割的准确性。 接下来,我们将看到在医学成像中的应用。 + +# 超神经分割 + +Kaggler 是一个组织进行预测建模和分析竞赛的组织。 Kagglers 曾经受到挑战,要从颈部超声图像中分割神经结构。 可以从 [https://www.kaggle.com/c/ultrasound-nerve-segmentation](https://www.kaggle.com/c/ultrasound-nerve-segmentation) 下载有关该数据的数据。 Ronneberger 等人提出的 UNET 模型。 ( [https://arxiv.org/pdf/1505.04597.pdf](https://arxiv.org/pdf/1505.04597.pdf) )类似于自动编码器,但具有卷积而不是完全连接的层。 这里有一个编码部分,其卷积减小,而解码器部分的卷积增大,如下所示: + +![](img/d5f0233d-03ef-4826-9cba-8a3bbf2f4eb3.png) + +该图说明了 UNET 模型的体系结构[经 Ronneberger 等人许可复制] + +相似大小的编码器和解码器部分的卷积通过跳过连接来学习。 模型的输出是一个介于 0 到 1 之间的掩码。让我们从导入函数开始,借助以下代码: + +```py +import os +from skimage.transform import resize +from skimage.io import imsave +import numpy as np +from data import load_train_data, load_test_data +``` + +在所有导入之后,我们现在将使用以下代码定义大小: + +```py +image_height, image_width = 96, 96 smoothness = 1.0 work_dir = '' +``` + +现在我们将定义`dice_coefficient`及其损失函数。 在这种情况下,`dice_coefficient`也是度量标准: + +```py +def dice_coefficient(y1, y2): + y1 = tf.flatten(y1) + y2 = tf.flatten(y2) + return (2\. * tf.sum(y1 * y2) + smoothness) / (tf.sum(y1) + tf.sum(y2) + smoothness) + +def dice_coefficient_loss(y1, y2): + return -dice_coefficient(y1, y2) +``` + +UNET 模型可以定义如下: + +```py +def preprocess(imgs): + imgs_p = np.ndarray((imgs.shape[0], image_height, image_width), dtype=np.uint8) + for i in range(imgs.shape[0]): + imgs_p[i] = resize(imgs[i], (image_width, image_height), preserve_range=True) + imgs_p = imgs_p[..., np.newaxis] + return imgs_p + +def covolution_layer(filters, kernel=(3,3), activation='relu', input_shape=None): + if input_shape is None: + return tf.keras.layers.Conv2D( + filters=filters, + kernel=kernel, + activation=activation) + else: + return tf.keras.layers.Conv2D( + filters=filters, + kernel=kernel, + activation=activation, + input_shape=input_shape) + +def concatenated_de_convolution_layer(filters): + return tf.keras.layers.concatenate([ + tf.keras.layers.Conv2DTranspose( + filters=filters, + kernel=(2, 2), + strides=(2, 2), + padding='same' + )], + axis=3 + ) + +``` + +所有层都已连接并使用,如以下代码所示: + +```py +unet = tf.keras.models.Sequential() +inputs = tf.keras.layers.Input((image_height, image_width, 1)) +input_shape = (image_height, image_width, 1) +unet.add(covolution_layer(32, input_shape=input_shape)) +unet.add(covolution_layer(32)) +unet.add(pooling_layer()) + +unet.add(covolution_layer(64)) +unet.add(covolution_layer(64)) +unet.add(pooling_layer()) + +unet.add(covolution_layer(128)) +unet.add(covolution_layer(128)) +unet.add(pooling_layer()) + +unet.add(covolution_layer(256)) +unet.add(covolution_layer(256)) +unet.add(pooling_layer()) + +unet.add(covolution_layer(512)) +unet.add(covolution_layer(512)) +``` + +这些层是连接在一起的,并使用了反卷积层: + +```py +unet.add(concatenated_de_convolution_layer(256)) +unet.add(covolution_layer(256)) +unet.add(covolution_layer(256)) + +unet.add(concatenated_de_convolution_layer(128)) +unet.add(covolution_layer(128)) +unet.add(covolution_layer(128)) + +unet.add(concatenated_de_convolution_layer(64)) +unet.add(covolution_layer(64)) +unet.add(covolution_layer(64)) + +unet.add(concatenated_de_convolution_layer(32)) +unet.add(covolution_layer(32)) +unet.add(covolution_layer(32)) + +unet.add(covolution_layer(1, kernel=(1, 1), activation='sigmoid')) + +unet.compile(optimizer=tf.keras.optimizers.Adam(lr=1e-5), + loss=dice_coefficient_loss, + metrics=[dice_coefficient]) + +``` + +接下来,可以通过使用以下代码对模型进行图像训练: + +```py +x_train, y_train_mask = load_train_data() + +x_train = preprocess(x_train) +y_train_mask = preprocess(y_train_mask) + +x_train = x_train.astype('float32') +mean = np.mean(x_train) +std = np.std(x_train) + +x_train -= mean +x_train /= std + +y_train_mask = y_train_mask.astype('float32') +y_train_mask /= 255. unet.fit(x_train, y_train_mask, batch_size=32, epochs=20, verbose=1, shuffle=True, + validation_split=0.2) + +x_test, y_test_mask = load_test_data() +x_test = preprocess(x_test) + +x_test = x_test.astype('float32') +x_test -= mean +x_test /= std + +y_test_pred = unet.predict(x_test, verbose=1) + +for image, image_id in zip(y_test_pred, y_test_mask): + image = (image[:, :, 0] * 255.).astype(np.uint8) + imsave(os.path.join(work_dir, str(image_id) + '.png'), image) +``` + +图像可以进行预处理和使用。 现在可以进行图像的训练和测试了。 训练模型后,分割会产生良好的结果,如下所示: + +![](img/92227668-9c13-4160-b879-f011ded4bb4c.png) + +我们已经训练了可以分割医学图像的模型。 该算法可以在几种用例中使用。 在下一节中,我们将看到如何分割卫星图像。 + +# 分割卫星图像 + +在本节中,我们将使用**国际摄影测量与遥感学会**( **ISPRS** )提供的数据集。 数据集包含 5 毫米分辨率的德国波茨坦的卫星图像。 这些图像带有红外和图像高度轮廓的附加数据。 与图像相关联的六个标签是: + +* 建造 +* 植被 +* 树木 +* 驾驶室 +* 杂乱无章 +* 不透水的 + +总共提供了 8,000 x 6,000 色块的 38 张图像。 请转到页面 [http://www2.isprs.org/commissions/comm3/wg4/data-request-form2.html](http://www2.isprs.org/commissions/comm3/wg4/data-request-form2.html) 并填写表格。 之后,在表单上选择以下选项: + +![](img/3272ab24-2a2e-4ebe-bf40-91d3fd5d9eb0.png) + +发布表格后,将向您发送电子邮件,从中可以下载数据。 + +# 为细分建模 FCN + +导入库并获得输入的形状。 标签数定义为`6`: + +```py +from .resnet50 import ResNet50 +nb_labels = 6 img_height, img_width, _ = input_shape +input_tensor = tf.keras.layers.Input(shape=input_shape) +weights = 'imagenet' +``` + +在 ImageNet 上预先训练的`ResNet`模型将用作基本模型。 以下代码可用于使用`ResNet`定义基本模型: + +```py +resnet50_model = ResNet50( + include_top=False, weights='imagenet', input_tensor=input_tensor) +``` + +现在,我们将使用以下代码从`ResNet`中获取最后三层: + +```py +final_32 = resnet50_model.get_layer('final_32').output +final_16 = resnet50_model.get_layer('final_16').output +final_x8 = resnet50_model.get_layer('final_x8').output +``` + +必须压缩每个跳过连接以匹配等于标签数的通道: + +```py + +c32 = tf.keras.layers.Conv2D(nb_labels, (1, 1))(final_32) +c16 = tf.keras.layers.Conv2D(nb_labels, (1, 1))(final_16) +c8 = tf.keras.layers.Conv2D(nb_labels, (1, 1))(final_x8) +``` + +可以使用双线性插值来调整压缩跳过连接的输出大小。 可以通过使用可以计算 TensorFlow 操作的`Lambda`层来实现插值。 以下代码段可用于使用 lambda 层进行插值: + +```py +def resize_bilinear(images): + return tf.image.resize_bilinear(images, [img_height, img_width]) + +r32 = tf.keras.layers.Lambda(resize_bilinear)(c32) +r16 = tf.keras.layers.Lambda(resize_bilinear)(c16) +r8 = tf.keras.layers.Lambda(resize_bilinear)(c8) +``` + +使用以下代码,可以通过添加三个值来合并我们定义的三层: + +```py +m = tf.keras.layers.Add()([r32, r16, r8]) +``` + +可以使用 softmax 激活来应用模型的概率。 在应用 softmax 之前和之后调整模型大小: + +```py +x = tf.keras.ayers.Reshape((img_height * img_width, nb_labels))(m) +x = tf.keras.layers.Activation('img_height')(x) +x = tf.keras.layers.Reshape((img_height, img_width, nb_labels))(x) + +fcn_model = tf.keras.models.Model(input=input_tensor, output=x) +``` + +已经定义了一个简单的 FCN 层,经过训练后,它会产生以下结果: + +![](img/c09f3fc7-b5d0-4eb1-9b22-e68707be3f96.png) + +您会看到六个标签的预测是合理的。 接下来,我们将学习分割实例。 + +# 分割实例 + +在分析图像时,我们的兴趣只会吸引到图像中的某些实例。 因此,它不得不从图像的其余部分中分割这些实例。 从其余信息中分离所需信息的过程被广泛称为**分割实例**。 在此过程中,首先拍摄输入图像,然后将边界框与对象一起定位,最后,将为每个类别预测逐像素掩码。 对于每个对象,都将计算像素级精度。 有几种用于分割实例的算法。 最近的算法之一是 He 等人提出的 **Mask RCNN** 算法。 ( [https://arxiv.org/pdf/1703.06870.pdf](https://arxiv.org/pdf/1703.06870.pdf) )。 下图描绘了 Mask R-CNN 的体系结构: + +![](img/608bc4e7-8b4b-46b9-98d9-1626226bc859.png) + +经 He 等人许可复制。 + +该架构看起来与 R-CNN 类似,但增加了分段功能。 这是一个具有端到端培训的多阶段网络。 学习了区域提案。 该网络分为两个部分,一个用于检测,另一个用于分类评分。 结果非常好,如下所示: + +![](img/a0697d7c-9d03-46fd-9ae2-ff00ff587825.png) + +该图说明了分割实例的过程,请注意,准确地检测了对象并进行了相应的分割。 + +同一网络还可以预测人的姿势。 分割和检测这两个任务是并行处理的。 + +# 摘要 + +在本章中,我们学习了各种分割算法。 我们还看到了用于基准测试的数据集和指标。 我们将学到的技术应用于卫星和医学图像的细分。 最后,我们谈到了 Mask R-CNN 算法的实例分割。 + +在下一章中,我们将学习相似性学习。 相似性学习模型学习两个图像之间的比较机制。 对于面部识别等多种应用很有用。 我们将学习几种可用于相似性学习的模型架构。 \ No newline at end of file diff --git a/docs/dl-cv/06.md b/docs/dl-cv/06.md new file mode 100644 index 00000000..89b5435e --- /dev/null +++ b/docs/dl-cv/06.md @@ -0,0 +1,594 @@ +# 相似学习 + +在本章中,我们将学习相似性学习并学习相似性学习中使用的各种损失函数。 当每个班级的数据集都很小时,相似性学习对我们很有用。 我们将了解可用于面部分析的不同数据集,并建立用于面部识别,界标检测的模型。 我们将在本章介绍以下主题: + +* 相似性学习的不同算法 +* 用于相似度学习的各种损失函数 +* 可以使用此类模型的各种方案 +* 人脸识别的完整过程 + +# 相似性学习算法 + +**相似性学习**是训练度量以计算两个实体之间的相似性的过程。 由于学习了相似性,这也可以称为度量学习。 度量可以是欧几里得或余弦或其他自定义距离函数。 实体可以是任何数据,例如图像,视频,文本或表格。 为了计算度量,需要图像的矢量表示。 此表示可以是 CNN 计算的特征,如[第 3 章](../Text/03.html),*图像检索*中所述。 为对象分类而学习的 CNN 可以用作计算度量的向量。 为图像分类而获得的特征向量将不是手头任务的最佳表示。 在相似性学习中,我们发现有关 CNN 的信息,这些 CNN 会为相似性学习任务生成经过训练的特征。 这里给出了相似学习的一些应用: + +* 用于生物识别的人脸验证以比较两个人脸 +* 视觉搜索现实世界中的物体以在线查找类似产品 +* 视觉推荐某些属性相似的产品 + +在本章中,我们将详细了解人脸验证。 因此,让我们从可用于相似性学习的算法开始。 + +# 连体网络 + +顾名思义,暹罗网络是一种神经网络模型,其中训练该网络以区分两个输入。 暹罗网络可以训练 CNN,以通过两个编码器产生嵌入。 每个编码器被馈送正对或负对中的一个图像。 暹罗网络所需的数据少于其他深度学习算法。 最初引入暹罗网络来比较签名。 下图显示了一个暹罗网络。 权重在网络之间共享: + +![](img/451420ac-8f1b-415c-b8d7-f893cf701fee.png) + +连体网络的另一种用途是一次性学习。 **单次学习**是仅举一个示例的学习技术。 在这种情况下,可以显示图像,并判断它们是否相似。 对于大多数相似性学习任务,需要一对正负对进行训练。 可以将此类数据集与可用于分类任务的任何数据集一起形成,前提是它们是欧几里得距离。 这些算法与前几章中的算法之间的主要区别在于,这些编码器试图将一个编码器与另一个编码器区分开。 + +# 对比损失 + +对比损失通过相似度区分图像。 使用相似性度量比较特征或潜在层,并与目标一起训练相似性得分。 在正对的情况下,目标将为 0,因为两个输入相同。 对于负数对,在余弦距离或正则欧几里得距离的情况下,潜对之间的距离最大为 0。 损耗可以由`contrastive_loss`定义,在以下代码中进行解释: + +```py +def contrastive_loss(model_1, model_2, label, margin=0.1): + distance = tf.reduce_sum(tf.square(model_1 - model_2), 1) + loss = label * tf.square( + tf.maximum(0., margin - tf.sqrt(distance))) + (1 - label) * distance + loss = 0.5 * tf.reduce_mean(loss) + return loss +``` + +比较两个模型的距离并计算损失。 现在,我们将定义和训练一个暹罗网络。 对于暹罗网络,我们将需要两个相同的模型。 接下来,借助以下代码,为具有给定输入的简单 CNN 定义一个函数: + +```py +def get_model(input_): + input_reshape = tf.reshape(input_, [-1, 28, 28, 1], + name='input_reshape') + convolution_layer_1 = convolution_layer(input_reshape, 64) + pooling_layer_1 = pooling_layer(convolution_layer_1) + convolution_layer_2 = convolution_layer(pooling_layer_1, 128) + pooling_layer_2 = pooling_layer(convolution_layer_2) + flattened_pool = tf.reshape(pooling_layer_2, [-1, 5 * 5 * 128], + name='flattened_pool') + dense_layer_bottleneck = dense_layer(flattened_pool, 1024) + return dense_layer_bottleneck +``` + +定义的模型将使用两次来定义暹罗网络所需的编码器。 接下来,定义两个模型的占位符。 对于每一对,输入的相似性也作为输入提供。 定义的模型相同。 还可以定义模型,以便共享权重。 此处定义了左右两个模型: + +```py +left_input = tf.placeholder(tf.float32, shape=[None, input_size]) +right_input = tf.placeholder(tf.float32, shape=[None, input_size]) +y_input = tf.placeholder(tf.float32, shape=[None, no_classes]) +left_bottleneck = get_model(left_input) +right_bottleneck = get_model(right_input) +``` + +瓶颈层是从模型中获取的,并被串联在一起。 这对于相似性学习问题至关重要。 可以创建任意数量的模型,并且可以连接最后的图层,如下所示: + +```py +dense_layer_bottleneck = tf.concat([left_bottleneck, right_bottleneck], 1) +``` + +接下来,添加一个辍学层,并从级联层中计算出 logit。 然后,该过程类似于任何其他网络,如下所示: + +```py +dropout_bool = tf.placeholder(tf.bool) +dropout_layer = tf.layers.dropout( + inputs=dense_layer_bottleneck, + rate=0.4, + training=dropout_bool + ) +logits = dense_layer(dropout_layer, no_classes) + +with tf.name_scope('loss'): + softmax_cross_entropy = tf.nn.softmax_cross_entropy_with_logits( + labels=y_input, logits=logits) + loss_operation = tf.reduce_mean(softmax_cross_entropy, name='loss') + tf.summary.scalar('loss', loss_operation) + +with tf.name_scope('optimiser'): + optimiser = tf.train.AdamOptimizer().minimize(loss_operation) + +with tf.name_scope('accuracy'): + with tf.name_scope('correct_prediction'): + predictions = tf.argmax(logits, 1) + correct_predictions = tf.equal(predictions, tf.argmax(y_input, 1)) + with tf.name_scope('accuracy'): + accuracy_operation = tf.reduce_mean( + tf.cast(correct_predictions, tf.float32)) +tf.summary.scalar('accuracy', accuracy_operation) + +session = tf.Session() +session.run(tf.global_variables_initializer()) + +merged_summary_operation = tf.summary.merge_all() +train_summary_writer = tf.summary.FileWriter('/tmp/train', session.graph) +test_summary_writer = tf.summary.FileWriter('/tmp/test') + +test_images, test_labels = mnist_data.test.images, mnist_data.test.labels +``` + +数据必须分别输入左右模型,如下所示: + +```py +for batch_no in range(total_batches): + mnist_batch = mnist_data.train.next_batch(batch_size) + train_images, train_labels = mnist_batch[0], mnist_batch[1] + _, merged_summary = session.run([optimiser, merged_summary_operation], + feed_dict={ + left_input: train_images, + right_input: train_images, + y_input: train_labels, + dropout_bool: True + }) + train_summary_writer.add_summary(merged_summary, batch_no) + if batch_no % 10 == 0: + merged_summary, _ = session.run([merged_summary_operation, + accuracy_operation], feed_dict={ + left_input: test_images, + right_input: test_images, + y_input: test_labels, + dropout_bool: False + }) + test_summary_writer.add_summary(merged_summary, batch_no) +``` + +我们已经看到了如何定义一个暹罗网络。 定义了两个编码器,并连接了潜在空间以形成训练损失。 左右模型分别提供数据。 接下来,我们将看到如何在单个网络中执行相似性学习。 + +# 面对网 + +Schroff 等人提出的 FaceNet 模型。 ( [https://arxiv.org/pdf/1503.03832.pdf](https://arxiv.org/pdf/1503.03832.pdf) )解决了面部验证问题。 它学习一个深层的 CNN,然后将面部图像转换为嵌入图像。 嵌入可用于比较人脸以查看其相似程度,并可通过以下三种方式使用: + +* **人脸验证**考虑两个人脸,并确定它们是否相似。 面部验证可以通过计算距离度量来完成。 +* **人脸识别**是用于用名字标记人脸的分类问题。 嵌入向量可用于训练最终标签。 +* **人脸聚类**将相似的人脸分组,就像照片应用程序将同一个人的照片聚在一起的方式一样。 诸如 K-means 之类的聚类算法用于对人脸进行分组。 + +下图显示了 FaceNet 架构: + +![](img/b1d11aa0-8cd0-498c-873c-edc53897f6c9.png) + +经 Schroff 等人许可复制。 + +FaceNet 会获取一批面部图像并进行训练。 在那一批中,将有几个正对。 在计算损耗时,考虑正对和最接近的几个负对。 挖掘选择性对可实现平稳训练。 如果所有负面因素一直都被推开,则训练不稳定。 比较三个数据点称为**三重态损失**。 在计算损耗时,图像被认为具有正负匹配。 底片仅被推动一定的幅度。 在此详细说明三重态损失。 + +# 三重损失 + +三元组损失学习图像的得分向量。 人脸描述符的得分向量可用于验证欧几里得空间中的人脸。 在学习投影的意义上,三元组损失类似于度量学习,因此可以区分输入。 这些投影或描述符或分数矢量是紧凑的表示形式,因此可以视为降维技术。 一个三元组由一个锚点,正负面组成。 锚可以是任何人的面孔,正面是同一个人的图像。 负片图像可能来自另一个人。 显然,对于给定的锚点,将会有很多负面的面孔。 通过选择当前更靠近锚点的底片,编码器将很难区分面部,从而使其学习效果更好。 此过程称为**硬底开采**。 可以在欧氏空间中使用阈值获得更接近的负值。 下图描述了三重态损失模型: + +![](img/861242f9-1c52-49ef-a891-0377251b7745.png) + +经 Schroff 等人许可复制。 + +TensorFlow 中的损失计算如下所示: + +```py +def triplet_loss(anchor_face, positive_face, negative_face, margin): + def get_distance(x, y): + return tf.reduce_sum(tf.square(tf.subtract(x, y)), 1) + + positive_distance = get_distance(anchor_face, positive_face) + negative_distance = get_distance(anchor_face, negative_face) + total_distance = tf.add(tf.subtract(positive_distance, negative_distance), margin) + return tf.reduce_mean(tf.maximum(total_distance, 0.0), 0) +``` + +三胞胎的开采是一项艰巨的任务。 每个点都必须与其他点进行比较,以获得适当的锚点和正对。 三元组的挖掘如下所示: + +```py +def mine_triplets(anchor, targets, negative_samples): + distances = cdist(anchor, targets, 'cosine') + distances = cdist(anchor, targets, 'cosine').tolist() + QnQ_duplicated = [ + [target_index for target_index, dist in enumerate(QnQ_dist) if dist == QnQ_dist[query_index]] + for query_index, QnQ_dist in enumerate(distances)] + for i, QnT_dist in enumerate(QnT_dists): + for j in QnQ_duplicated[i]: + QnT_dist.itemset(j, np.inf) + + QnT_dists_topk = QnT_dists.argsort(axis=1)[:, :negative_samples] + top_k_index = np.array([np.insert(QnT_dist, 0, i) for i, QnT_dist in enumerate(QnT_dists_topk)]) + return top_k_index +``` + +由于距离计算发生在 CPU 中,因此这可能会使在 GPU 机器上的训练变慢。 FaceNet 模型是训练人脸相似模型的最新方法。 + +# DeepNet 模型 + +DeepNet 模型用于学习用于面部验证任务(例如 FaceNet)的面部嵌入。 这是对上一部分中讨论的 FaceNet 方法的改进。 它需要对同一张脸进行多次裁剪,并通过多个编码器才能获得更好的嵌入效果。 与 FaceNet 相比,此方法具有更高的准确性,但需要更多时间进行处理。 面部裁切在相同区域进行,并通过其各自的编码器。 然后将所有层连接起来以进行三重态损失的训练。 + +# 深排 + +Wang 等人提出的 DeepRank。 ( [https://users.eecs.northwestern.edu/~jwa368/pdfs/deep_ranking.pdf](https://users.eecs.northwestern.edu/~jwa368/pdfs/deep_ranking.pdf) )用于根据相似度对图像进行排名。 图像通过不同的模型传递,如下所示: + +![](img/85e5d360-602e-4bf4-b596-09e44f152e27.png) + +经王等人许可转载。 + +在此也计算了三重态损耗,并且反向传播更加顺利。 然后可以将图像转换为线性嵌入以进行排名,如下所示: + +![](img/e87e214e-cc3c-45bd-8629-75a2beeef0ea.png) + +经 Wang 等人许可复制。 + +该算法对于排名目的非常有用。 + +# 视觉推荐系统 + +视觉推荐系统非常适合获取给定图像的推荐。 推荐模型提供具有相似属性的图像。 根据 Shankar 等人提出的以下模型。 ( [https://arxiv.org/pdf/1703.02344.pdf](https://arxiv.org/pdf/1703.02344.pdf) ),您可以了解相似图像的嵌入,并提供以下建议: + +![](img/7337cf27-f5a5-4292-b4b2-d967bd5382f9.png) + +图(a)显示了深度排名架构,图(b)显示了 VisNet 架构[经 Shankar 等人许可复制。 + +这些是用于相似性学习的一些算法。 在下一节中,我们将看到如何将这些技术应用于面部。 + +# 人脸分析 + +可以使用计算机视觉以多种方式分析人脸。 为此,需要考虑以下几个因素: + +* **人脸检测**:找到人脸位置的边界框 +* **面部标志检测**:查找鼻子,嘴巴等面部特征的空间点 +* **人脸对齐**:将人脸转换成正面人脸以进行进一步分析 +* **属性识别**:查找诸如性别,微笑等属性 +* **情绪分析**:分析人的情绪 +* **人脸验证**:查找两个图像是否属于同一个人 +* **人脸识别**:为人脸识别 +* **人脸聚类**:将同一个人的脸部分组在一起 + +在以下各节中,让我们详细了解这些任务的数据集和实现。 + +# 人脸检测 + +人脸检测类似于对象检测,我们在[第 4 章](../Text/04.html),*对象检测*中讨论过。 必须从图像中检测出面部的位置。 可以从 [http://vis-www.cs.umass.edu/fddb/](http://vis-www.cs.umass.edu/fddb/) 下载名为**人脸检测数据集和基准**( **FDDB** )的数据集。 。 它具有 2,845 张带有 5,171 张脸的图像。 可以从 Yang 等人提出的 [http://mmlab.ie.cuhk.edu.hk/projects/WIDERFace/](http://mmlab.ie.cuhk.edu.hk/projects/WIDERFace/) 下载另一个称为**宽脸**的数据集。 它具有 32,203 张图像和 393,703 张面孔。 这是来自更广泛的面部数据集的图像示例: + +![](img/92e946e7-2fb7-468a-aacd-77039c30fbb4.jpg) + +由杨等人提出。 并转载自 http://mmlab.ie.cuhk.edu.hk/projects/WIDERFace/support/intro.jpg + +数据集的比例,姿势,遮挡,表情,妆容和照明度都有很好的变化。 另一个名为 **多属性标签的面孔**( **MALF** )的数据集包含 5,250 张图像,其中包含 11,931 张面孔。 可以从 [http://www.cbsr.ia.ac.cn/faceevaluation/](http://www.cbsr.ia.ac.cn/faceevaluation/) 链接访问 MALF。 在对象检测中使用的相同技术也可以应用于面部检测。 + +# 面对地标和属性 + +脸部地标是人脸的空间点。 空间点对应于各种面部特征的位置,例如眼睛,眉毛,鼻子,嘴巴和下巴。 点数可能会从 5 到 78 不等,具体取决于注释。 脸部界标也称为**基准点**,**脸部关键点**或**脸部姿势**。 人脸标志具有许多应用,如下所示: + +* 脸部对齐以更好地进行脸部验证或识别 +* 跟踪视频中的人脸 +* 面部表情或情绪可以测量 +* 有助于诊断疾病 + +接下来,我们将看到一些带有基准点注释的数据库。 + +# 多任务面部地标(MTFL)数据集 + +`MTFL`数据集由 Zhang 等人提出。 并带有五个面部标志以及性别,微笑,眼镜和头部姿势注释。 数据库中存在 12,995 张面孔。 可以从 [http://mmlab.ie.cuhk.edu.hk/projects/TCDCN/data/MTFL.zip](http://mmlab.ie.cuhk.edu.hk/projects/TCDCN/data/MTFL.zip) 下载`MTFL`。 + +这是`MTFL`中存在的图像的示例: + +![](img/de1113bf-8f62-4752-a656-e0f79ca4b1a4.jpg) + +由张等人提出。 并转载自 http://mmlab.ie.cuhk.edu.hk/projects/TCDCN/img/1.jpg + +面部在年龄,照度,情绪等方面有很多变化。 **头姿势**是脸部方向的角度,以度为单位。 眼镜,微笑,性别属性等都用二进制标签注释。 + +# Kaggle 关键点数据集 + +Kaggle 关键点数据集带有 15 个面部标志。 数据集中存在 8,832 张图像。 可以从链接 [https://www.kaggle.com/c/facial-keypoints-detection/data](https://www.kaggle.com/c/facial-keypoints-detection/data) 下载。 图像尺寸为 96 像素 x 96 像素。 + +# 多属性人脸地标(MAFL)数据集 + +Zhang 等人提出的`MAFL`数据集。 带有 5 种具有 40 种不同面部属性的面部地标。 数据库中存在 20,000 张面孔。 可以从 [https://github.com/zhzhanp/TCDCN-face-alignment](https://github.com/zhzhanp/TCDCN-face-alignment) 下载`MAFL`。 这是`MAFL`中存在的图像的示例: + +![](img/0c8f1dd3-a580-402f-a9e2-66ccff7c19b1.png) + +由 Liu 等人提出。 并转载自 http://mmlab.ie.cuhk.edu.hk/projects/celeba/overview.png + +注释的属性包括尖头,带子,小胡子,卷发,戴帽子等。 这些图像也包含在`CelebA`数据集中,稍后将详细讨论。 + +# 学习面部关键点 + +如先前主题中所述,在计算关键面部点时,需要定义一些参数。 我们将使用以下代码来定义这些参数: + +```py +image_size = 40 no_landmark = 10 no_gender_classes = 2 no_smile_classes = 2 no_glasses_classes = 2 no_headpose_classes = 5 batch_size = 100 total_batches = 300 +``` + +接下来,为各种输入保留一些占位符。 + +```py +image_input = tf.placeholder(tf.float32, shape=[None, image_size, image_size]) +landmark_input = tf.placeholder(tf.float32, shape=[None, no_landmark]) +gender_input = tf.placeholder(tf.float32, shape=[None, no_gender_classes]) +smile_input = tf.placeholder(tf.float32, shape=[None, no_smile_classes]) +glasses_input = tf.placeholder(tf.float32, shape=[None, no_glasses_classes]) +headpose_input = tf.placeholder(tf.float32, shape=[None, no_headpose_classes]) +``` + +接下来,使用四个卷积层构造主模型,如以下代码所示: + +```py +image_input_reshape = tf.reshape(image_input, [-1, image_size, image_size, 1], + name='input_reshape') + +convolution_layer_1 = convolution_layer(image_input_reshape, 16) +pooling_layer_1 = pooling_layer(convolution_layer_1) +convolution_layer_2 = convolution_layer(pooling_layer_1, 48) +pooling_layer_2 = pooling_layer(convolution_layer_2) +convolution_layer_3 = convolution_layer(pooling_layer_2, 64) +pooling_layer_3 = pooling_layer(convolution_layer_3) +convolution_layer_4 = convolution_layer(pooling_layer_3, 64) +flattened_pool = tf.reshape(convolution_layer_4, [-1, 5 * 5 * 64], + name='flattened_pool') +dense_layer_bottleneck = dense_layer(flattened_pool, 1024) +dropout_bool = tf.placeholder(tf.bool) +dropout_layer = tf.layers.dropout( + inputs=dense_layer_bottleneck, + rate=0.4, + training=dropout_bool + ) +``` + +接下来,我们将使用以下代码为所有不同的任务创建一个 logit 分支: + +```py +landmark_logits = dense_layer(dropout_layer, 10) +smile_logits = dense_layer(dropout_layer, 2) +glass_logits = dense_layer(dropout_layer, 2) +gender_logits = dense_layer(dropout_layer, 2) +headpose_logits = dense_layer(dropout_layer, 5) +``` + +损耗是针对所有面部特征单独计算的,如以下代码所示: + +```py +landmark_loss = 0.5 * tf.reduce_mean( + tf.square(landmark_input, landmark_logits)) + +gender_loss = tf.reduce_mean( + tf.nn.softmax_cross_entropy_with_logits( + labels=gender_input, logits=gender_logits)) + +smile_loss = tf.reduce_mean( + tf.nn.softmax_cross_entropy_with_logits( + labels=smile_input, logits=smile_logits)) + +glass_loss = tf.reduce_mean( + tf.nn.softmax_cross_entropy_with_logits( + labels=glasses_input, logits=glass_logits)) + +headpose_loss = tf.reduce_mean( + tf.nn.softmax_cross_entropy_with_logits( + labels=headpose_input, logits=headpose_logits)) + +loss_operation = landmark_loss + gender_loss + \ + smile_loss + glass_loss + headpose_loss +``` + +现在,我们将初始化优化器并开始训练,如以下代码所示: + +```py +optimiser = tf.train.AdamOptimizer().minimize(loss_operation) +session = tf.Session() +session.run(tf.initialize_all_variables()) +fiducial_test_data = fiducial_data.test + +for batch_no in range(total_batches): + fiducial_data_batch = fiducial_data.train.next_batch(batch_size) + loss, _landmark_loss, _ = session.run( + [loss_operation, landmark_loss, optimiser], + feed_dict={ + image_input: fiducial_data_batch.images, + landmark_input: fiducial_data_batch.landmarks, + gender_input: fiducial_data_batch.gender, + smile_input: fiducial_data_batch.smile, + glasses_input: fiducial_data_batch.glasses, + headpose_input: fiducial_data_batch.pose, + dropout_bool: True + }) + if batch_no % 10 == 0: + loss, _landmark_loss, _ = session.run( + [loss_operation, landmark_loss], + feed_dict={ + image_input: fiducial_test_data.images, + landmark_input: fiducial_test_data.landmarks, + gender_input: fiducial_test_data.gender, + smile_input: fiducial_test_data.smile, + glasses_input: fiducial_test_data.glasses, + headpose_input: fiducial_test_data.pose, + dropout_bool: False + }) +``` + +此过程可用于检测面部特征以及界标。 + +# 人脸识别 + +**面部识别**或**面部识别**是从数字图像或视频中识别人物的过程。 让我们在下一部分中了解可用于面部识别的数据集。 + +# 野生(LFW)数据集中的带标签的面孔 + +`LFW`数据集包含 13,233 张面孔和 5,749 位独特的人,被视为评估面孔验证数据集的标准数据集。 精度度量可用于评估算法。 可以在链接 [http://vis-www.cs.umass.edu/lfw/](http://vis-www.cs.umass.edu/lfw/) 中访问数据集。 + +# YouTube 面孔数据集 + +YouTube `faces`数据集包含 3,425 个视频剪辑,其中包含 1,595 个独特的人。 这些视频是从 YouTube 收集的。 数据集每人至少有两个视频。 该数据集被视为视频中人脸验证的标准数据集。 可以在链接 [https://www.cs.tau.ac.il/~wolf/ytfaces/](https://www.cs.tau.ac.il/~wolf/ytfaces/) 中访问数据集。 + +# CelebFaces 属性数据集(CelebA) + +`CelebA` 数据集带有人物身份以及 5 个面部标志和 40 个属性的注释。 数据库中有 10,177 位独特的人,拥有 202,599 张面部图像。 它是可用于面部验证,检测,界标和属性识别问题的大型数据集之一。 图像具有带有各种注释的良好人脸变化。 可以在链接 [http://mmlab.ie.cuhk.edu.hk/projects/CelebA.html](http://mmlab.ie.cuhk.edu.hk/projects/CelebA.html) 中访问数据集。 + +# CASIA 网络人脸数据库 + +`CASIA`数据集带有 1 个 0,575 个独特的人注释,总共有 494,414 张图像。 该数据集可以从 [http://www.cbsr.ia.ac.cn/english/CASIA-WebFace-Database.html](http://www.cbsr.ia.ac.cn/english/CASIA-WebFace-Database.html) 获得。 这是可用于人脸验证和识别问题的第二大公共数据集。 + +# VGGFace2 数据集 + +Cao 等人提出的`VGGFace2`数据集。 被 9,131 位独特的人注解,具有 331 万张图片。 数据集可从 [http://www.robots.ox.ac.uk/~vgg/data/vgg_face2/](http://www.robots.ox.ac.uk/~vgg/data/vgg_face2/) 获得。 变化包括年龄,种族,姿势,职业和照度。 这是可用于面部验证的最大数据集。 + +这是数据集中存在的图像的示例: + +![](img/d2c45b27-6fc0-409c-8aa6-196b8569aec2.png) + +由曹等人提出。 并转载自 http://www.robots.ox.ac.uk/~vgg/data/vgg_face2/web_page_img.png + +每个唯一的人的最小,平均和最大图像数分别是 87、362.6 和 843。 + +# 计算脸部之间的相似度 + +人脸相似度的计算是一个多步骤问题。 必须检测面部,然后找到基准点。 面可以与基准点对齐。 对齐的面可以用于比较。 如前所述,人脸检测类似于物体检测。 因此,为了找到面孔之间的相似性,我们将首先通过以下代码导入所需的库,以及`facenet`库: + +```py +from scipy import misc +import tensorflow as tf +import numpy as np +import os +import facenet +print facenet +from facenet import load_model, prewhiten +import align.detect_face +``` + +可以如下所示加载和对齐图像: + +```py +def load_and_align_data(image_paths, image_size=160, margin=44, gpu_memory_fraction=1.0): + minsize = 20 + threshold = [0.6, 0.7, 0.7] + factor = 0.709 print('Creating networks and loading parameters') + with tf.Graph().as_default(): + gpu_options = tf.GPUOptions(per_process_gpu_memory_fraction=gpu_memory_fraction) + sess = tf.Session(config=tf.ConfigProto(gpu_options=gpu_options, log_device_placement=False)) + with sess.as_default(): + pnet, rnet, onet = align.detect_face.create_mtcnn(sess, None) + + nrof_samples = len(image_paths) + img_list = [None] * nrof_samples + for i in range(nrof_samples): + img = misc.imread(os.path.expanduser(image_paths[i]), mode='RGB') + img_size = np.asarray(img.shape)[0:2] + bounding_boxes, _ = align.detect_face.detect_face(img, minsize, pnet, rnet, onet, threshold, factor) + det = np.squeeze(bounding_boxes[0, 0:4]) + bb = np.zeros(4, dtype=np.int32) + bb[0] = np.maximum(det[0] - margin / 2, 0) + bb[1] = np.maximum(det[1] - margin / 2, 0) + bb[2] = np.minimum(det[2] + margin / 2, img_size[1]) + bb[3] = np.minimum(det[3] + margin / 2, img_size[0]) + cropped = img[bb[1]:bb[3], bb[0]:bb[2], :] + aligned = misc.imresize(cropped, (image_size, image_size), interp='bilinear') + prewhitened = prewhiten(aligned) + img_list[i] = prewhitened + images = np.stack(img_list) + return images +``` + +现在,我们将处理图像路径以获取嵌入。 相同的代码在这里给出: + +```py +def get_face_embeddings(image_paths, model='/20170512-110547/'): + images = load_and_align_data(image_paths) + with tf.Graph().as_default(): + with tf.Session() as sess: + load_model(model) + images_placeholder = tf.get_default_graph().get_tensor_by_name("input:0") + embeddings = tf.get_default_graph().get_tensor_by_name("embeddings:0") + phase_train_placeholder = tf.get_default_graph().get_tensor_by_name("phase_train:0") + feed_dict = {images_placeholder: images, phase_train_placeholder: False} + emb = sess.run(embeddings, feed_dict=feed_dict) + + return emb +``` + +现在,我们将使用以下代码来计算嵌入之间的距离: + +```py +def compute_distance(embedding_1, embedding_2): + dist = np.sqrt(np.sum(np.square(np.subtract(embedding_1, embedding_2)))) + return dist +``` + +此函数将计算嵌入之间的**欧几里德**距离。 + +# 寻找最佳阈值 + +使用前面的功能,可以计算出该系统的精度。 以下代码可用于计算最佳阈值: + +```py +import sys +import argparse +import os +import re +from sklearn.metrics import classification_report +from sklearn.metrics import accuracy_score +``` + +现在,使用以下代码从文件夹中获取图像路径: + +```py +def get_image_paths(image_directory): + image_names = sorted(os.listdir(image_directory)) + image_paths = [os.path.join(image_directory, image_name) for image_name in image_names] + return image_paths +``` + +通过嵌入时,将获得图像的距离,如以下代码所示: + +```py +def get_labels_distances(image_paths, embeddings): + target_labels, distances = [], [] + for image_path_1, embedding_1 in zip(image_paths, embeddings): + for image_path_2, embedding_2 in zip(image_paths, embeddings): + if (re.sub(r'\d+', '', image_path_1)).lower() == (re.sub(r'\d+', '', image_path_2)).lower(): + target_labels.append(1) + else: + target_labels.append(0) + distances.append(compute_distance(embedding_1, embedding_2)) # Replace distance metric here + return target_labels, distances +``` + +阈值随以下代码所示而变化,并相应打印各种度量: + +```py +def print_metrics(target_labels, distances): + accuracies = [] + for threshold in range(50, 150, 1): + threshold = threshold/100. + predicted_labels = [1 if dist <= threshold else 0 for dist in distances] + print("Threshold", threshold) + print(classification_report(target_labels, predicted_labels, target_names=['Different', 'Same'])) + accuracy = accuracy_score(target_labels, predicted_labels) + print('Accuracy: ', accuracy) + accuracies.append(accuracy) + print(max(accuracies)) +``` + +现在,借助以下代码将图像路径传递给嵌入: + +```py +def main(args): + image_paths = get_image_paths(args.image_directory) + embeddings = get_face_embeddings(image_paths) # Replace your embedding calculation here + target_labels, distances = get_labels_distances(image_paths, embeddings) + print_metrics(target_labels, distances) +``` + +最后,图像目录作为这些方法的主要参数传递,如以下代码所示: + +```py +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('image_directory', type=str, help='Directory containing the images to be compared') + parsed_arguments = parser.parse_args(sys.argv[1:]) + main(parsed_arguments) +``` + +在此示例中,我们采用了预先训练的模型并将其用于构建人脸验证方法。 ; + +# 人脸聚类 + +**人脸聚类**是将同一个人的图像分组在一起以形成相册的过程。 可以提取人脸的嵌入,并且可以使用诸如 K 均值的聚类算法将同一个人的人脸合并在一起。 TensorFlow 为 K-means 算法提供了一个称为 `tf.contrib.learn.KmeansClustering` 的 API。 K-均值算法将数据点分组在一起。 借助这种 K-means 算法,可以提取专辑的嵌入内容,并且可以一起找到个人的面孔,或者换句话说,可以将聚在一起。 + +# 摘要 + +在本章中,我们介绍了相似性学习的基础知识。 我们研究了度量学习,暹罗网络和 FaceNet 等算法。 我们还介绍了损失函数,例如对比损失和三重损失。 还涵盖了两个不同的领域,即排名和推荐。 最后,通过理解几个步骤(包括检测,基准点检测和相似性评分)涵盖了面部识别的分步演练。 + +在下一章中,我们将了解递归神经网络及其在自然语言处理问题中的使用。 稍后,我们将使用语言模型和图像模型来对图像进行字幕。 我们将针对该问题访问几种算法,并查看两种不同类型数据的实现。 \ No newline at end of file diff --git a/docs/dl-cv/07.md b/docs/dl-cv/07.md new file mode 100644 index 00000000..66750c16 --- /dev/null +++ b/docs/dl-cv/07.md @@ -0,0 +1,310 @@ +# 图片字幕 + +在本章中,我们将处理字幕图像的问题。 这涉及到检测对象,并且还提出了图像的文本标题。 图像字幕也可以称为**图像文本转换**。 曾经被认为是一个非常棘手的问题,我们现在在此方面取得了相当不错的成绩。 对于本章,需要具有相应标题的图像数据集。 在本章中,我们将详细讨论图像字幕的技术和应用。 + +我们将在本章介绍以下主题: + +* 了解用于评估它们的不同数据集和指标 +* 了解用于自然语言处理问题的一些技巧 +* 向量模型的不同词 +* 几种用于图像字幕的算法 +* 不良结果和改进范围 + +# 了解问题和数据集 + +自动生成图像标题的过程是一项重要的深度学习任务,因为它结合了语言和视觉这两个世界。 该问题的独特性使其成为计算机视觉中的主要问题之一。 用于图像字幕的深度学习模型应该能够识别图像中存在的对象,并能够以自然语言生成表示对象与动作之间关系的文本。 此问题的数据集很少。 其中最著名的数据集是[第 4 章](../Text/04.html),*对象检测*中对象检测中涵盖的 COCO 数据集的扩展。 + +# 了解用于图像字幕的自然语言处理 + +由于必须从图像中生成自然语言,因此熟悉**自然语言处理**( **NLP** )变得很重要。 NLP 的概念是一个广泛的主题,因此我们将范围限制为与图像字幕相关的主题。 自然语言的一种形式是**文本**。 文本是单词或字符的序列。 文本的原子元素称为**令牌**,它是**字符**的序列。 字符是文本的原子元素。 + +为了处理文本形式的任何自然语言,必须通过删除标点符号,方括号等对文本进行预处理。 然后,必须通过将文本分隔为空格来将文本标记为单词。 然后,必须将单词转换为向量。 接下来,我们将看到矢量转换如何提供帮助。 + +# 以矢量形式表达单词 + +矢量形式的单词可以帮助自己执行算术运算。 向量必须紧凑,尺寸较小。 同义词应具有相似的向量,而反义词应具有不同的向量。 可以将单词转换为向量,以便可以如下所示比较关系: + +![](img/d4061008-c857-4130-bb3e-6c1f418ea379.png) + +该向量算法使得能够在不同实体之间的语义空间中进行比较。 接下来,我们将看到如何创建可将单词转换为矢量表示的深度学习模型。 + +# 将单词转换为矢量 + +通过在大型文本语料库上训练模型,可以将单词转换为向量。 训练模型,使得给定一个单词,该模型可以预测附近的单词。 在预测附近单词的单次热编码之前,首先对单词进行单次热编码,然后进行隐藏层。 以这种方式进行训练将创建单词的紧凑表示。 可以通过两种方式获得单词的上下文,如下所示: + +* **跳过图**:给定一个单词,尝试预测几个接近的单词 +* **连续词袋**( **CBOW** ):通过给定一组词来预测一个词,从而跳过跳跃语法 + +下图说明了这些过程: + +![](img/2b34c3e0-28a8-4d55-8c1b-70ea045357c5.png) + +两种方法均显示出良好的结果。 单词在嵌入空间中转换为向量。 接下来,我们将看到训练嵌入空间的详细信息。 + +# 训练嵌入 + +可以使用如下所示的模型来训练嵌入: + +![](img/82b25c21-48e2-4de6-b9c0-03e6e245a825.png) + +如上图所示,目标词是根据上下文或历史预测的。 该预测基于 **Softmax 分类器**。 隐藏层将嵌入作为紧凑的表示形式学习。 请注意,这不是完整的深度学习模型,但它仍然可以正常工作。 这是嵌入的低维可视化: + +![](img/1dc5ed0c-353d-4884-8251-522e97b6bc8e.png) + +使用 Softmax 分类器的嵌入的低维可视化 + +该可视化使用 TensorBoard 生成。 具有相似语义或不同词性的单词会一起出现。 + +我们已经学习了如何训练用于生成文本的紧凑表示。 接下来,我们将看到图像字幕的方法。 + +# 图像字幕处理方法及相关问题 + +已经提出了几种对图像进行字幕的方法。 直观地,将图像转换为视觉特征,并从这些特征生成文本。 生成的文本将采用词嵌入的形式。 生成文本的一些主要方法涉及 LSTM 和关注。 让我们从使用旧的生成文本的方法开始。 + +# 使用条件随机字段链接图像和文本 + +Kulkarni 等人在论文 [http://www.tamaraberg.com/papers/generation_cvpr11.pdf](http://www.tamaraberg.com/papers/generation_cvpr11.pdf) 中,提出了一种从图像中查找对象和属性并使用它来生成文本的方法。 **条件随机字段**( **CRF** )。 传统上,CRF 用于结构化预测,例如文本生成。 生成文本的流程如下所示: + +![](img/114a59b6-d1fe-4f78-b9c6-32c7d416cd3a.png) + +该图说明了使用 CRF 生成文本的过程[摘自 Kulkarni 等人] + +CRF 的使用在以适当的前置词以连贯的方式生成文本方面存在局限性。 结果显示在这里: + +![](img/29a03016-9c2c-4712-9dc8-cf412fb617b7.png) + +复制自 Kulkarni 等。 + +结果对对象和属性具有正确的预测,但无法生成良好的描述。 + +# 在 CNN 功能上使用 RNN 生成字幕 + +Vinyals 等人在论文 [https://arxiv.org/pdf/1411.4555.pdf](https://arxiv.org/pdf/1411.4555.pdf) 中提出了一种端到端可训练的深度学习用于图像字幕的方法,该方法将 CNN 和 RNN 背靠背地堆叠在一起。 这是一个端到端的可训练模型。 结构如下所示: + +![](img/5bf3b8f9-fa55-4a3f-a84c-b0818fcc856a.png) + +转载自 Vinyals 等。 (2015 年) + +该模型可以生成以自然语言完成的句子。 CNN 和 **LSTM** 的展开图如下所示: + +![](img/04f54858-0741-427c-9c36-4c627f70ba34.png) + +该图说明了 CNN 和 LSTM 架构[摘自 Vinyals 等人] + +这是 **LSTM** 的展开视图。 此处显示了一组选择性的结果: + +![](img/bc22e56f-e384-499e-be63-4e036405ee7c.png) + +转载自 Vinyals 等。 (2015 年) + +在此过程中,CNN 将图像编码为特征,RNN 从中生成句子。 + +# 使用图像排名创建字幕 + +Ordonez 等人在论文 [http://papers.nips.cc/paper/4470-im2text-describing-images-using-1-million-captioned-photographs.pdf](http://papers.nips.cc/paper/4470-im2text-describing-images-using-1-million-captioned-photographs.pdf) 中,提出了一种方法 对图像进行排名,然后生成标题。 此过程的流程如下所示: + +![](img/5b285990-9992-4773-833a-720904a28188.png) + +复制自 Ordonez 等。 (2015) + +从排名图像中提取的高级信息可用于生成文本。 下图显示,可用于排名的图像越多,结果将越好: + +![](img/fd60d31e-2040-4634-8f2b-245e258740ea.png) + +复制自 Ordonez 等。 (2015) + +# 从图像检索字幕和从字幕检索图像 + +Chen 等人在论文 [https://www.cs.cmu.edu/~xinleic/papers/cvpr15_rnn.pdf](https://www.cs.cmu.edu/~xinleic/papers/cvpr15_rnn.pdf) 中,提出了一种从文本中检索图像和从图像中检索文本的方法。 这是双向映射。 下图显示了一个用自然语言解释图像的人和另一个在视觉上思考它的人: + +![](img/226bae31-12c2-4ec6-aa6a-5f54db2f7d6e.png) + +转载自 Chen 等。 (2015) + +检索字幕可以通过以下方式通过潜在空间连接图像和文本的编码器来实现: + +![](img/f4ca10b9-38aa-46a9-9839-d7844a8401c1.png) + +转载自 Chen 等。 (2015) + +图像中的第一个模型是用于训练的完整模型。 如图中所示,视觉功能也可以用于生成句子,反之亦然。 + +# 密集字幕 + +Johnson 等人在论文 [https://www.cv-foundation.org/openaccess/content_cvpr_2016/papers/Johnson_DenseCap_Fully_Convolutional_CVPR_2016_paper.pdf](https://www.cv-foundation.org/openaccess/content_cvpr_2016/papers/Johnson_DenseCap_Fully_Convolutional_CVPR_2016_paper.pdf) 中,提出了一种用于密集字幕的方法。 首先,让我们看一些结果,以了解任务: + +![](img/29744e79-12f1-49c0-a1bb-cd99c44a87d2.png) + +转载自 Johnson 等。 + +如您所见,为图像中的对象和动作生成了单独的标题; 由此得名; **密集字幕**。 这是 Johnson 等人提出的架构: + +![](img/b4d4bca7-5ea6-48a2-ad5c-c5063af9ebe5.png) + +转自 Johnson 等。 + +该体系结构实质上是 Faster-RCNN 和 **LSTM** 的组合。 产生该区域以产生对象检测结果,并且使用该区域的视觉特征来产生字幕。 + +# 使用 RNN 字幕 + +Donahue 等人在论文 [https://arxiv.org/pdf/1411.4389.pdf](https://arxiv.org/pdf/1411.4389.pdf) 中,提出了**长期递归卷积架构**( **LRCN** ) 用于图像字幕的任务。 此模型的体系结构如下所示: + +![](img/b0a60d74-0013-443b-b167-4cc77a74bfcb.png) + +转载自 Donahue 等。 + +图中显示了 CNN 和 LSTM 在整个时间上的权重,这使得该方法可扩展到任意长序列。 + +# 使用多峰度量空间 + +Mao 等人在论文 [https://arxiv.org/pdf/1412.6632.pdf](http://papers.nips.cc/paper/4470-im2text-describing-images-using-1-million-captioned-photographs.pdf) 中提出了一种使用**多模态嵌入空间**生成字幕的方法。 下图说明了这种方法: + +![](img/11930a42-1972-4cbb-a1d7-417207484a6c.png) + +转自毛等人。 + +Kiros 等人在论文 [https://arxiv.org/pdf/1411.2539.pdf](https://arxiv.org/pdf/1411.2539.pdf) 中提出了另一种生成字幕的多模态方法,该方法可以将图像和文本嵌入同一多模态空间。 下图说明了这种方法: + +![](img/c7396971-eb91-40ba-8e8e-9ea6ed8eef77.png) + +复制自 Kiros 等。 + +两种多模式方法都给出了良好的结果。 + +# 使用注意力网络进行字幕 + +Xu 等人在论文 [https://arxiv.org/pdf/1502.03044.pdf](https://arxiv.org/pdf/1502.03044.pdf) 中,提出了一种使用**注意机制**进行图像字幕的方法。 注意机制对图像的某些区域比其他区域赋予更多权重。 注意还可以实现可视化,向我们展示模型生成下一个单词时所关注的位置。 建议的模型如下所示: + +![](img/16daa7fa-b24c-4855-b677-fdb975865573.png) + +转载自徐等人。 + +首先,从图像中提取 CNN 特征。 然后,将关注的 RNN 应用于生成单词的图像。 + +# 知道什么时候看 + +Lu 等。 ( [https://arxiv.org/pdf/1612.01887.pdf](https://arxiv.org/pdf/1612.01887.pdf) )提出了一种引起关注的方法,可提供出色的结果。 知道何时看待注意力捕获的区域会产生更好的结果。 流程如下所示: + +![](img/94576f82-a74c-4394-bc55-78c027c88424.png) + +摘自 Lu 等。 + +注意机制如下所示: + +![](img/15065730-7c08-4996-9a4d-efd4eaa6b377.png) + +摘自 Lu 等。 + +结果重点突出的区域如下: + +![](img/aeb23046-5281-48a0-8f43-19852a14bc89.png) + +摘自 Lu 等。 + +生成字幕时注意力的释放在此处可视化: + +![](img/5dc70bd2-6e0f-4e12-a79b-564d36959461.png) + +摘自 Lu 等。 + +我们已经看到,用于生成字幕几种方法。 接下来,我们将看到一个实现。 + +# 实施基于注意力的图像字幕 + +让我们使用以下代码从 VGG 和 LSTM 模型定义 CNN: + +```py +vgg_model = tf.keras.applications.vgg16.VGG16(weights='imagenet', + include_top=False, + input_tensor=input_tensor, + input_shape=input_shape) + +word_embedding = tf.keras.layers.Embedding( + vocabulary_size, embedding_dimension, input_length=sequence_length) +embbedding = word_embedding(previous_words) +embbedding = tf.keras.layers.Activation('relu')(embbedding) +embbedding = tf.keras.layers.Dropout(dropout_prob)(embbedding) + +cnn_features_flattened = tf.keras.layers.Reshape((height * height, shape))(cnn_features) +net = tf.keras.layers.GlobalAveragePooling1D()(cnn_features_flattened) + +net = tf.keras.layers.Dense(embedding_dimension, activation='relu')(net) +net = tf.keras.layers.Dropout(dropout_prob)(net) +net = tf.keras.layers.RepeatVector(sequence_length)(net) +net = tf.keras.layers.concatenate()([net, embbedding]) +net = tf.keras.layers.Dropout(dropout_prob)(net) +``` + +现在,我们已经定义了 CNN,接下来使用以下代码定义关注层: + +```py +h_out_linear = tf.keras.layers.Convolution1D( + depth, 1, activation='tanh', border_mode='same')(h) +h_out_linear = tf.keras.layers.Dropout( + dropout_prob)(h_out_linear) +h_out_embed = tf.keras.layers.Convolution1D( + embedding_dimension, 1, border_mode='same')(h_out_linear) +z_h_embed = tf.keras.layers.TimeDistributed( + tf.keras.layers.RepeatVector(num_vfeats))(h_out_embed) + +Vi = tf.keras.layers.Convolution1D( + depth, 1, border_mode='same', activation='relu')(V) + +Vi = tf.keras.layers.Dropout(dropout_prob)(Vi) +Vi_emb = tf.keras.layers.Convolution1D( + embedding_dimension, 1, border_mode='same', activation='relu')(Vi) + +z_v_linear = tf.keras.layers.TimeDistributed( + tf.keras.layers.RepeatVector(sequence_length))(Vi) +z_v_embed = tf.keras.layers.TimeDistributed( + tf.keras.layers.RepeatVector(sequence_length))(Vi_emb) + +z_v_linear = tf.keras.layers.Permute((2, 1, 3))(z_v_linear) +z_v_embed = tf.keras.layers.Permute((2, 1, 3))(z_v_embed) + +fake_feat = tf.keras.layers.Convolution1D( + depth, 1, activation='relu', border_mode='same')(s) +fake_feat = tf.keras.layers.Dropout(dropout_prob)(fake_feat) + +fake_feat_embed = tf.keras.layers.Convolution1D( + embedding_dimension, 1, border_mode='same')(fake_feat) +z_s_linear = tf.keras.layers.Reshape((sequence_length, 1, depth))(fake_feat) +z_s_embed = tf.keras.layers.Reshape( + (sequence_length, 1, embedding_dimension))(fake_feat_embed) + +z_v_linear = tf.keras.layers.concatenate(axis=-2)([z_v_linear, z_s_linear]) +z_v_embed = tf.keras.layers.concatenate(axis=-2)([z_v_embed, z_s_embed]) + +z = tf.keras.layers.Merge(mode='sum')([z_h_embed,z_v_embed]) +z = tf.keras.layers.Dropout(dropout_prob)(z) +z = tf.keras.layers.TimeDistributed( + tf.keras.layers.Activation('tanh'))(z) +attention= tf.keras.layers.TimeDistributed( + tf.keras.layers.Convolution1D(1, 1, border_mode='same'))(z) + +attention = tf.keras.layers.Reshape((sequence_length, num_vfeats))(attention) +attention = tf.keras.layers.TimeDistributed( + tf.keras.layers.Activation('softmax'))(attention) +attention = tf.keras.layers.TimeDistributed( + tf.keras.layers.RepeatVector(depth))(attention) +attention = tf.keras.layers.Permute((1,3,2))(attention) +w_Vi = tf.keras.layers.Add()([attention,z_v_linear]) +sumpool = tf.keras.layers.Lambda(lambda x: K.sum(x, axis=-2), + output_shape=(depth,)) +c_vec = tf.keras.layers.TimeDistributed(sumpool)(w_Vi) +atten_out = tf.keras.layers.Merge(mode='sum')([h_out_linear,c_vec]) +h = tf.keras.layers.TimeDistributed( + tf.keras.layers.Dense(embedding_dimension,activation='tanh'))(atten_out) +h = tf.keras.layers.Dropout(dropout_prob)(h) + +predictions = tf.keras.layers.TimeDistributed( + tf.keras.layers.Dense(vocabulary_size, activation='softmax'))(h) +``` + +在前面的代码的帮助下,我们定义了一个深度学习模型,该模型将 CNN 功能与 RNN 结合在一起,并借助注意力机制。 目前,这是生成字幕的最佳方法。 + +# 摘要 + +在本章中,我们已经了解了与图像标题相关的问题。 我们看到了一些涉及自然语言处理和各种`word2vec`模型(例如`GLOVE`)的技术。 我们了解了`CNN2RNN`,度量学习和组合目标等几种算法。 后来,我们实现了一个结合了 CNN 和 LSTM 的模型。 + +在下一章中,我们就来了解生成模型。 我们将从头开始学习和实现样式算法,并介绍一些最佳模型。 我们还将介绍很酷的**生成对抗网络**( **GAN** )及其各种应用。 \ No newline at end of file diff --git a/docs/dl-cv/08.md b/docs/dl-cv/08.md new file mode 100644 index 00000000..87f214d6 --- /dev/null +++ b/docs/dl-cv/08.md @@ -0,0 +1,748 @@ +# 生成模型 + +生成模型已经成为计算机视觉中的重要应用。 与前几章讨论的应用程序根据图像进行预测不同,生成模型可以为特定目标创建图像。 在本章中,我们将了解: + +* 生成模型的应用 +* 样式转移算法 +* 训练超分辨率图像模型 +* 生成模型的实施和培训 +* 当前模型的缺点 + +在本章的最后,您将能够实现一些出色的应用程序来传递样式,并理解与生成模型相关的可能性和困难。 + +# 生成模型的应用 + +让我们从生成模型的可能应用开始本章。 应用程序是巨大的。 我们将看到其中一些应用程序,以了解动机和可能性。 + +# 艺术风格转移 + +艺术风格转移是将艺术风格转移到任何图像的过程。 例如,可以使用一幅图像的艺术风格和另一幅图像的内容来创建图像。 Gatys 等人在此显示了一个结合了几种不同样式的图像示例。 ( [https://www.cv-foundation.org/openaccess/content_cvpr_2016/papers/Gatys_Image_Style_Transfer_CVPR_2016_paper.pdf](https://www.cv-foundation.org/openaccess/content_cvpr_2016/papers/Gatys_Image_Style_Transfer_CVPR_2016_paper.pdf) )。 图像 **A** 是应用了样式的照片,其结果显示在其他图像中: + +![](img/0e36da01-457e-4388-8cec-85994bf97af2.png) + +转载自盖蒂斯等。 + +此应用程序引起了公众的注意,并且市场上有几种提供此功能的移动应用程序。 + +# 预测视频中的下一帧 + +可以使用生成模型从合成视频集中预测未来的帧。 在下面由 Lotter 等人提出的图像中。 ( [https://arxiv.org/pdf/1511.06380.pdf](https://arxiv.org/pdf/1511.06380.pdf) )左侧的图像是前一帧的模型,而右侧的图像相对于前一帧有两种算法 基本事实: + +![](img/af300a68-236d-43de-b849-f7fbad8dd6b8.png) + +复制自 Lotter 等。 + +生成模型生成的框架将是现实的。 + +# 图像超分辨率 + +**超分辨率**是从较小的图像创建高分辨率图像的过程。 传统上,插值用于创建更大的图像。 但是插值通过提供平滑效果而错过了高频细节。 经过训练的生成模型针对此超分辨率的特定目的而创建的图像具有出色的细节。 以下是 Ledig 等人提出的此类模型的示例。 ( [https://arxiv.org/pdf/1609.04802.pdf](https://arxiv.org/pdf/1609.04802.pdf) )。 左侧是通过 **4 倍缩放**生成的,看起来与右侧的原件没有区别: + +![](img/96bebdcb-ce44-486b-9b87-f3f48a39b789.png) + +转自 Ledig 等。 + +超分辨率对于在高质量的显示器或打印件上呈现低分辨率的图像很有用。 另一个应用可能是重建高质量的压缩图像。 + +# 交互式图像生成 + +生成模型可用于通过**交互作用**创建图像。 用户可以添加编辑内容,并且可以生成图像,以反映编辑内容,如 Zhu 等人建议的那样。 ( [https://arxiv.org/pdf/1609.03552v2.pdf](https://arxiv.org/pdf/1609.03552v2.pdf) ): + +![](img/76957f5e-d2e5-4573-a6c7-3aa162253546.png) + +复制自 Zhu 等。 + +如图所示,图像是根据编辑的形状和颜色生成的。 底部的绿色笔触创建了草原,矩形创建了摩天大楼,依此类推。 图像将被生成并通过用户的进一步输入进行微调。 生成的图像还可以用于检索可以利用的最相似的真实图像。 提供交互式图像生成是一种直观搜索图像的全新方法。 + +# 图像到图像的翻译 + +图像可用于生成具有特定目标的其他图像,因此此过程称为**图像到图像的转换**。 此处显示了此类翻译的一些示例,以及由 Isola 等人提出的相应标准。 ( [https://arxiv.org/pdf/1611.07004.pdf](https://arxiv.org/pdf/1611.07004.pdf) ): + +![](img/a289fe70-1518-455e-91f5-22ae6a0a1de4.png) + +复制自 Isola 等。 + +带有标签的图形可以转换为逼真的图像以用于创意目的。 黑白图像可以转换为彩色图像。 这样的翻译对于照片编辑应用程序,为旧电影着色,服装设计等非常有用。 + +# 文字到图像的生成 + +可以从文本描述中生成图像,其步骤类似于图像到图像的翻译。 以下是一些由 Reed 等人展示的自然文本描述生成的示例。 ( [https://arxiv.org/pdf/1605.05396.pdf](https://arxiv.org/pdf/1605.05396.pdf) ): + +![](img/5f66cccf-b9c9-4609-b224-06e95897407b.png) + +复制自 Reed 等。 + +当前,此模型仅适用于少数几个对象。 从文本生成图像还不够实际,无法在应用程序中使用。 + +# 修补 + +修复是填充图像中的间隙的过程,如下所示: + +![](img/c3919cea-ec35-4cff-b352-15975acdb6f9.jpg) + +资料来源:https://www.flickr.com/photos/littleredelf/4756059924/ + +左侧的图像是正常图像,右侧的图像是经过处理的图像。 从图像中可以看到,不需要的东西已从图片中删除。 修补对于从图像中删除不需要的对象以及填充扫描图稿的空间很有用。 + +# 调和 + +融合是将图像的一部分平滑地粘贴到另一个图像上而没有任何伪影的过程。 此处显示的图像**和**表示一种图像放置在另一图像上的情况,给人留下不好的印象。 图像 **b** 和 **c** 代表传统的混合技术,例如**修正的 Poisson 方法**和**多样条方法[** 。 + +最终图像或图像 **d** 显示了混合生成方法的结果,该方法比 Wu 等人的其他方法提供了更好的结果。 ( [https://arxiv.org/pdf/1703.07195.pdf](https://arxiv.org/pdf/1703.07195.pdf) ): + +![](img/85081b77-9fc2-436d-8205-13ffb13b374a.png) + +摘自 Wu 等。 + +混合对于照片编辑和电影行业中的特殊效果非常有用。 + +# 转换属性 + +可以使用生成模型来更改图像的属性。 兰珀(Lample)等人在此显示,可以修改人的脸部以反映不同的属性,例如性别,眼镜,年龄等。 ( [https://research.fb.com/wp-content/uploads/2017/11/fader_networks__conditional_attribute_based_image_generation_by_disentangling_in_latent_space.pdf](https://research.fb.com/wp-content/uploads/2017/11/fader_networks__conditional_attribute_based_image_generation_by_disentangling_in_latent_space.pdf) ): + +![](img/e700b8c7-e5a1-4914-8f0a-afded0129609.png) + +转载于 Lample 等人。 + +更改属性既可以用于创意应用程序,也可以用于娱乐,也可以用于生成更多具有变化的训练数据。 + +# 创建训练数据 + +生成模型可用于大规模生成训练,甚至可用于完善为训练而创建的合成图像。 这是使用 Wang 等人的生成模型为交通标志识别创建的合成图像。 ( [https://arxiv.org/pdf/1707.03124.pdf](https://arxiv.org/pdf/1707.03124.pdf) ) + +![](img/b4123a9e-ccd5-4c45-9348-0935c45e318f.png) + +转载自 Wang 等。 + +使用这些图像可以使分类更加准确。 + +# 创建新的动画角色 + +生成模型可用于创建具有各种条件的新动画角色,例如面部表情,发型,服装等,如 Jin 等人所示。 ( [https://arxiv.org/pdf/1708.05509.pdf](https://arxiv.org/pdf/1708.05509.pdf) ): + +![](img/e92bfe6e-601b-49e4-a8df-72cb19f7c73e.png) + +转载自 Jin 等。 + +创建具有不同属性的新角色可以彻底改变动画产业。 + +# 照片的 3D 模型 + +我们可以使用生成模型从 2D 图像创建 3D 模型,如 Wu 等人所示。 ( [https://arxiv.org/pdf/1610.07584.pdf](https://arxiv.org/pdf/1610.07584.pdf) ): + +![](img/08cdcdde-c328-4fec-84bc-42c38bd0a7d7.png) + +摘自 Wu 等。 + +从图像创建 3D 模型对于机器人技术,增强现实和动画行业很有用。 在以下各节中,我们将学习它们背后的算法。 在下一节中,我们将实现神经艺术风格的转换。 + +# 神经艺术风格的转移 + +我们将要实现的第一个应用程序是**神经艺术风格转换**。 在这里,我们将 **Van Gogh** 艺术的样式转移到图像上。 图像可以视为样式和内容的组合。 艺术风格转换技术将图像转换为看起来像具有特定绘画风格的绘画。 我们将看到如何编写这个想法。 `loss`功能将比较生成的图像与照片内容和绘画风格。 因此,针对图像像素而不是针对网络权重执行优化。 通过将照片的内容与生成的图像相比较,然后是绘画风格和生成的图像,可以计算出两个值。 + +# 内容丢失 + +由于像素不是一个好的选择,我们将使用各个图层的 CNN 功能,因为它们可以更好地表示内容。 如 [第 3 章](../Text/03.html),*图像检索*, 所示,初始层具有高频,例如边缘,拐角和纹理。 后面的层代表对象,因此更适合内容。 后者可以比像素更好地将*对象与*对象进行比较。 但是为此,我们需要先使用以下代码导入所需的库: + +```py +import numpy as np +from PIL import Image +from scipy.optimize import fmin_l_bfgs_b +from scipy.misc import imsave +from vgg16_avg import VGG16_Avg +from keras import metrics +from keras.models import Model +from keras import backend as K +``` + +现在,让我们使用以下命令加载所需的图像: + +```py +content_image = Image.open(work_dir + 'bird_orig.png') +``` + +我们将在此实例中使用以下图片: + +![](img/c5f130bb-71bd-45ba-8bea-d224f13820de.png) + +当我们使用 VGG 架构提取特征时,必须从所有图像中减去所有`ImageNet`图像的均值,如以下代码所示: + +```py +imagenet_mean = np.array([123.68, 116.779, 103.939], dtype=np.float32) + +def subtract_imagenet_mean(image): + return (image - imagenet_mean)[:, :, :, ::-1] +``` + +请注意,通道是不同的。 `preprocess`函数拍摄生成的图像并减去平均值,然后反转通道。 `deprocess`函数由于进行了预处理步骤而使效果相反,如以下代码所示: + +```py +def add_imagenet_mean(image, s): + return np.clip(image.reshape(s)[:, :, :, ::-1] + imagenet_mean, 0, 255) +``` + +首先,我们将了解如何使用其他图像中的内容创建图像。 这是根据**随机噪声** 创建图像的过程。 此处使用的内容是某层中**激活**的总和。 我们将使随机噪声和图像之间的内容损失最小化,这被称为内容损失。 该损耗类似于逐像素损耗,但应用于层激活,因此将捕获内容而没有噪声。 任何 CNN 体系结构都可以用来转发内容图像和随机噪声。 比较这两个输出的激活,进行激活并计算均方误差。 + +冻结 CNN 权重时,将更新随机图像的像素。 在这种情况下,我们将冻结 VGG 网络。 现在,可以加载 VGG 模型。 生成图像对子采样技术(例如 **max pooling** )非常敏感。 无法从最大池中取回像素值。 因此,**平均池**比最大池更平滑。 使用平均池转换 VGG 模型的功能用于加载模型,如下所示: + +```py +vgg_model = VGG16_Avg(include_top=False) +``` + +请注意,即使合并类型已更改,此模型的权重也与原始模型相同。 ResNet 和 Inception 模型不适合此操作,因为它们无法提供各种抽象。 我们将从模型冻结的最后一个 VGG 模型的卷积层`block_conv1`中获取激活。 这是 VGG 的第三层,具有广阔的接收范围。 这里给出了相同的代码供您参考: + +```py +content_layer = vgg_model.get_layer('block5_conv1').output +``` + +现在,使用截断的 VGG 创建新模型,直到具有良好功能的图层。 因此,该图像现在可以加载,并且可以用于执行前向推断,以获得**实际激活的层**。 使用以下代码创建 TensorFlow 变量以捕获激活: + +```py +content_model = Model(vgg_model.input, content_layer) +content_image_array = subtract_imagenet_mean(np.expand_dims(np.array(content_image), 0)) +content_image_shape = content_image_array.shape +target = K.variable(content_model.predict(content_image_array)) +``` + +让我们定义一个评估器类,以计算图像的损耗和梯度。 下列类在迭代的任意点返回损耗和梯度值: + +```py +class ConvexOptimiser(object): + def __init__(self, cost_function, tensor_shape): + self.cost_function = cost_function + self.tensor_shape = tensor_shape + self.gradient_values = None def loss(self, point): + loss_value, self.gradient_values = self.cost_function([point.reshape(self.tensor_shape)]) + return loss_value.astype(np.float64) + + def gradients(self, point): + return self.gradient_values.flatten().astype(np.float64) +``` + +损失函数可以定义为特定卷积层的激活值之间的均方误差。 损失将在生成的图像和原始内容照片的层之间进行计算,如下所示: + +```py +mse_loss = metrics.mean_squared_error(content_layer, target) +``` + +可以通过考虑模型的输入来计算损耗的梯度,如下所示: + +```py +grads = K.gradients(mse_loss, vgg_model.input) +``` + +函数的输入是模型的输入,输出将是损耗和梯度值的数组,如下所示: + +```py +cost_function = K.function([vgg_model.input], [mse_loss]+grads) +``` + +此函数是确定性的要优化的,因此不需要 **SGD** : + +```py +optimiser = ConvexOptimiser(cost_function, content_image_shape) +``` + +可以使用简单的优化程序来优化此功能,因为它是凸的,因此是确定性的。 我们还可以在迭代的每个步骤中保存图像。 我们将以可访问渐变的方式进行定义,就像我们使用 scikit-learn 的优化程序进行最终优化一样。 注意,该损失函数是凸的,因此,简单的优化器足以满足计算要求。 可以使用以下代码定义优化器: + +```py +def optimise(optimiser, iterations, point, tensor_shape, file_name): + for i in range(iterations): + point, min_val, info = fmin_l_bfgs_b(optimiser.loss, point.flatten(), + fprime=optimiser.gradients, maxfun=20) + point = np.clip(point, -127, 127) + print('Loss:', min_val) + imsave(work_dir + 'gen_'+file_name+'_{i}.png', add_imagenet_mean(point.copy(), tensor_shape)[0]) + return point +``` + +优化器采用`loss`函数,点和渐变,然后返回更新。 需要使用以下代码生成随机图像,以使内容损失最小化: + +```py +def generate_rand_img(shape): + return np.random.uniform(-2.5, 2.5, shape)/1 generated_image = generate_rand_img(content_image_shape) +``` + +这是创建的随机图像: + +![](img/bb7d0bd4-7167-43d7-938f-163330b4bfa5.png) + +该优化可以运行 10 次迭代以查看结果,如下所示: + +```py +iterations = 10 generated_image = optimise(optimiser, iterations, generated_image, content_image_shape, 'content') +``` + +如果一切顺利,那么在迭代过程中,损失应如下图所示: + +```py +Current loss value: 73.2010421753 +Current loss value: 22.7840042114 +Current loss value: 12.6585302353 +Current loss value: 8.53817081451 +Current loss value: 6.64649534225 +Current loss value: 5.56395864487 +Current loss value: 4.83072710037 +Current loss value: 4.32800722122 +Current loss value: 3.94804215431 +Current loss value: 3.66387653351 +``` + +这是生成的图像,现在,它看起来几乎像只鸟。 可以运行优化以进行进一步的迭代以完成此操作: + +![](img/6f9d1c9b-34ce-4c4b-ab82-4a67f76fb906.png) + +优化器拍摄图像并更新像素,以使内容相同。 虽然效果较差,但可以在一定程度上重现图像内容。 通过迭代获得的所有图像都很好地说明了图像的生成方式。 此过程不涉及批处理。 在下一节中,我们将看到如何以绘画风格创建图像。 + +# 使用 Gram 矩阵的样式损失 + +创建具有原始图像内容的图像后,我们将看到如何仅使用样式创建图像。 样式可以认为是图像颜色和纹理的混合。 为此,我们将定义样式丢失。 首先,我们将覆盖图像并将其转换为数组,如以下代码所示: + +```py +style_image = Image.open(work_dir + 'starry_night.png') +style_image = style_image.resize(np.divide(style_image.size, 3.5).astype('int32')) +``` + +这是我们加载的样式图像: + +![](img/1f67b6de-e5e1-461d-b515-a96915351fd5.png) + +现在,我们将使用以下代码通过更改通道对该图像进行预处理: + +```py +style_image_array = subtract_imagenet_mean(np.expand_dims(style_image, 0)[:, :, :, :3]) +style_image_shape = style_image_array.shape +``` + +为此,我们将考虑以下几层,就像我们在以下代码中所做的那样: + +```py +model = VGG16_Avg(include_top=False, input_shape=shp[1:]) +outputs = {l.name: l.output for l in model.layers} +``` + +现在,我们将使用以下代码将多层作为前四个块的数组输出: + +```py +layers = [outputs['block{}_conv1'.format(o)] for o in range(1,3)] +``` + +现在创建一个新模型,该模型可以使用以下代码输出所有这些层并分配目标变量: + +```py +layers_model = Model(model.input, layers) +targs = [K.variable(o) for o in layers_model.predict(style_arr)] +``` + +使用 **Gram 矩阵**计算样式损失。 革兰氏矩阵是矩阵及其转置的乘积。 激活值可以简单地转置和相乘。 然后将此矩阵用于计算样式和随机图像之间的误差。 克矩阵会丢失位置信息,但会保留纹理信息。 我们将使用以下代码定义 Gram 矩阵: + +```py +def grammian_matrix(matrix): + flattened_matrix = K.batch_flatten(K.permute_dimensions(matrix, (2, 0, 1))) + matrix_transpose_dot = K.dot(flattened_matrix, K.transpose(flattened_matrix)) + element_count = matrix.get_shape().num_elements() + return matrix_transpose_dot / element_count +``` + +您可能现在已经知道,它是一对列之间的相关性的度量。 高度和宽度尺寸被展平。 这不包括任何本地信息,因为坐标信息被忽略。 样式损失计算输入图像的 Gram 矩阵与目标之间的均方误差,如以下代码所示: + +```py +def style_mse_loss(x, y): + return metrics.mse(grammian_matrix(x), grammian_matrix(y)) +``` + +现在,我们使用以下代码通过汇总各层的所有激活来计算损失: + +```py +style_loss = sum(style_mse_loss(l1[0], l2[0]) for l1, l2 in zip(style_features, style_targets)) +grads = K.gradients(style_loss, vgg_model.input) +style_fn = K.function([vgg_model.input], [style_loss]+grads) +optimiser = ConvexOptimiser(style_fn, style_image_shape) +``` + +然后,通过创建随机图像,以与以前相同的方式解决它。 但是这次,我们还将应用高斯滤波器,如以下代码所示: + +```py +generated_image = generate_rand_img(style_image_shape) +``` + +生成的随机图像如下所示: + +![](img/6bf68978-98b1-449a-a813-639c4bda8b31.png) + +优化可以运行 10 次迭代以查看结果,如下所示: + +```py +generated_image = optimise(optimiser, iterations, generated_image, style_image_shape) +``` + +如果一切顺利,求解器应打印类似于以下的损耗值: + +```py +Current loss value: 5462.45556641 +Current loss value: 189.738555908 +Current loss value: 82.4192581177 +Current loss value: 55.6530838013 +Current loss value: 37.215713501 +Current loss value: 24.4533748627 +Current loss value: 15.5914745331 +Current loss value: 10.9425945282 +Current loss value: 7.66888141632 +Current loss value: 5.84042310715 +``` + +这是生成的图像: + +![](img/097eec9d-b9f7-42b3-81db-c4d68781126e.png) + +在这里,我们从随机噪声中创建了具有特定绘画风格的图像,而没有任何位置信息。 在下一节中,我们将看到如何结合使用-内容损失和样式损失。 + +# 风格转移 + +现在,我们知道了如何重建图像,以及如何构建捕获原始图像样式的图像。 显而易见的想法可能是通过加权并添加两个`loss`函数来将这两种方法结合起来,如以下代码所示: + +```py +w,h = style.size +src = img_arr[:,:h,:w] +``` + +和以前一样,我们将获取一系列图层输出以计算样式损失。 但是,我们仍然只需要一层输出来计算内容损失。 我们如何知道要抓哪一层? 如前所述,层越低,内容重构将越精确。 在将内容重建与样式相结合时,我们可以预期,对内容进行更宽松的重建将为样式带来更大的影响空间(例如:灵感)。 此外,即使没有相同的细节,后面的图层也可以确保图像看起来像相同的主题。 以下代码用于此过程: + +```py +style_layers = [outputs['block{}_conv2'.format(o)] for o in range(1,6)] +content_name = 'block4_conv2' +content_layer = outputs[content_name] +``` + +现在,使用以下代码使用所需的输出层创建一个单独的样式模型: + +```py +style_model = Model(model.input, style_layers) +style_targs = [K.variable(o) for o in style_model.predict(style_arr)] +``` + +我们还将使用以下代码为具有内容层的内容创建另一个模型: + +```py +content_model = Model(model.input, content_layer) +content_targ = K.variable(content_model.predict(src)) +``` + +现在,两种方法的合并就像合并它们各自的损失函数一样简单。 请注意,与我们之前的功能相反,此功能将产生三种不同类型的输出: + +* 一个用于原始图像 +* 一个用于模仿我们风格的图片 +* 一个用于训练像素的随机图像 + +我们调整重建方式的一种方法是更改内容损失系数,此处为 1/10。 如果增加分母,则样式将对图像产生较大影响,而如果太大,则非结构化样式将掩盖图像的原始内容。 同样,如果它太小,则图像将没有足够的样式。 我们将在此过程中使用以下代码: + +```py +style_wgts = [0.05,0.2,0.2,0.25,0.3] +``` + +`loss`函数同时包含样式和内容层,如下所示: + +```py +loss = sum(style_loss(l1[0], l2[0])*w + for l1,l2,w in zip(style_layers, style_targs, style_wgts)) +loss += metrics.mse(content_layer, content_targ)/10 +grads = K.gradients(loss, model.input) +transfer_fn = K.function([model.input], [loss]+grads) evaluator = Evaluator(transfer_fn, shp) +``` + +我们将使用以下代码像以前一样运行求解器 10 次迭代: + +```py +iterations=10 +x = rand_img(shp) x = solve_image(evaluator, iterations, x) +``` + +损耗值应按如下所示打印: + +```py +Current loss value: 2557.953125 +Current loss value: 732.533630371 +Current loss value: 488.321166992 +Current loss value: 385.827178955 +Current loss value: 330.915924072 +Current loss value: 293.238189697 +Current loss value: 262.066864014 +Current loss value: 239.34185791 +Current loss value: 218.086700439 +Current loss value: 203.045211792 +``` + +这些结果是惊人的。 他们每个人都以艺术家的风格来完成原始图像的复制工作。 生成的图像如下所示: + +![](img/fe01f255-d682-4f67-8612-b692af48dd95.png) + +现在,我们将结束样式转换部分。 此操作确实很慢,但可以处理任何图像。 在下一节中,我们将看到如何使用类似的想法来创建超分辨率网络。 有几种方法可以改善这种情况,例如: + +* 将 高斯 滤镜添加到随机图像 +* 为图层添加不同的权重 +* 可以使用不同的图层和权重来满足 +* 初始化图像而不是随机图像 +* 颜色可以保存 +* 掩码可以用于 的,用于指定所需的内容 +* 任何草图都可以转换为绘画 +* 绘制草图并创建图像 + +通过训练 CNN 输出任何图像,都可以将其转换为艺术风格。 + +# 生成对抗网络 + +**生成对抗网络**( **GAN** )由 **Ian Goodfellow** 于 2014 年发明。这是一种无监督算法,其中两个神经网络被训练为鉴别器和生成器。 , 同时。 该技术可以根据随机噪声生成图像,鉴别器可以评估是否为原始图像。 经过进一步培训后,生成器网络可以生成逼真的图像。 生成器网络通常是反卷积神经网络,而鉴别器是卷积神经网络。 + +理解这一点的一个很好的类比是,将生成器看作是伪造钱币的人,而将鉴别器看作是确定钱币是否为假币的警察。 生成器会根据警察的反馈不断提高伪钞的质量,直到警察无法区分伪钞和伪钞。 现在,让我们从实现开始。 + +# 甘香草 + +原始 GAN 称为**香草 GAN** 。 在构建模型之前,让我们定义一些对本章其余部分有用的层。 以下是`convolutional_layers`,其中添加了泄漏激活和正则化: + +```py +def convolution_layer(input_layer, + filters, + kernel_size=[4, 4], + activation=tf.nn.leaky_relu): + layer = tf.layers.conv2d( + inputs=input_layer, + filters=filters, + kernel_size=kernel_size, + activation=activation, + kernel_regularizer=tf.nn.l2_loss, + bias_regularizer=tf.nn.l2_loss, + ) + add_variable_summary(layer, 'convolution') + return layer +``` + +接下来,我们将使用以下代码定义与带有正则化的`convolution_layer`相反的`transpose_convolution_layer`: + +```py +def transpose_convolution_layer(input_layer, + filters, + kernel_size=[4, 4], + activation=tf.nn.relu, + strides=2): + layer = tf.layers.conv2d_transpose( + inputs=input_layer, + filters=filters, + kernel_size=kernel_size, + activation=activation, + strides=strides, + kernel_regularizer=tf.nn.l2_loss, + bias_regularizer=tf.nn.l2_loss, + ) + add_variable_summary(layer, 'convolution') + return layer +``` + +接下来,我们将使用以下代码定义一个具有非线性激活的密集层: + +```py +def dense_layer(input_layer, + units, + activation=tf.nn.relu): + layer = tf.layers.dense( + inputs=input_layer, + units=units, + activation=activation + ) + add_variable_summary(layer, 'dense') + return layer +``` + +现在,我们将定义一个生成器,该生成器将噪声作为输入并变为图像。 发生器由几个完全连接的层组成,然后是转置卷积层以对噪声进行上采样。 最后,提出了卷积层以使噪声成为单个通道。 每层之间都有批量归一化层,以使梯度平滑流动。 我们将使用以下代码定义生成器: + +```py +def get_generator(input_noise, is_training=True): + generator = dense_layer(input_noise, 1024) + generator = tf.layers.batch_normalization(generator, training=is_training) + generator = dense_layer(generator, 7 * 7 * 256) + generator = tf.layers.batch_normalization(generator, training=is_training) + generator = tf.reshape(generator, [-1, 7, 7, 256]) + generator = transpose_convolution_layer(generator, 64) + generator = tf.layers.batch_normalization(generator, training=is_training) + generator = transpose_convolution_layer(generator, 32) + generator = tf.layers.batch_normalization(generator, training=is_training) + generator = convolution_layer(generator, 3) + generator = convolution_layer(generator, 1, activation=tf.nn.tanh) + return generator +``` + +现在,我们将定义 GAN 的**鉴别器**部分,该部分可拍摄图像并尝试区分假冒商品和真实形象。 鉴别器是一个规则的卷积网络,上面有几个`convolutional_layers`,其后是致密层。 批归一化层位于层之间。 我们将使用以下代码来定义鉴别符: + +```py +def get_discriminator(image, is_training=True): + x_input_reshape = tf.reshape(image, [-1, 28, 28, 1], + name='input_reshape') + discriminator = convolution_layer(x_input_reshape, 64) + discriminator = convolution_layer(discriminator, 128) + discriminator = tf.layers.flatten(discriminator) + discriminator = dense_layer(discriminator, 1024) + discriminator = tf.layers.batch_normalization(discriminator, training=is_training) + discriminator = dense_layer(discriminator, 2) + return discriminator +``` + +创建鉴别器后,我们将使用以下代码创建一个噪声矢量,该噪声矢量将作为生成器的输入: + +```py +input_noise = tf.random_normal([batch_size, input_dimension]) +``` + +可以使用 TensorFlow 中的`tf.contrib.gan`模块创建 GAN 模型。 它采用了 generator 和 discriminator 方法及其相应的输入,如下所示: + +```py +gan = tf.contrib.gan.gan_model( + get_generator, + get_discriminator, + real_images, + input_noise) +``` + +现在,可以使用以下代码从`gan_train`方法开始训练,该方法将`gan_train_ops`方法带给生成器和鉴别器以损失,并对优化器进行优化,并使用以下代码: + +```py +tf.contrib.gan.gan_train( + tf.contrib.gan.gan_train_ops( + gan, + tf.contrib.gan.gan_loss(gan), + tf.train.AdamOptimizer(0.001), + tf.train.AdamOptimizer(0.0001))) +``` + +通过运行此命令,将创建可从随机矢量输出图像的 GAN 模型。 生成的图像不受限制,可以来自任何标签。 在下一节中,我们将使用条件 GAN 生成所需的输出。 + +# Conditional GAN + +有条件的 GAN 生成带有所需标签的图像。 例如,我们可以要求模型生成数字 8,而模型将生成数字 8。为此,需要标签以及使用模型训练的噪声,如下所示: + +```py +gan = tf.contrib.gan.gan_model( + get_generator, + get_discriminator, + real_images, + (input_noise, labels)) +``` + +其余的培训与香草 GAN 相似。 接下来,我们将使用 GAN 压缩图像。 + +# 对抗损失 + +对抗性损失是来自发电机的损失。 该损失可以与伪图像和真实图像之间的逐像素损失相结合,以形成组合的对抗性损失。 GAN 模型必须随`real_images`一起提供给生成器和鉴别器,如下所示: + +```py +gan = tf.contrib.gan.gan_model( + get_autoencoder, + get_discriminator, + real_images, + real_images) +``` + +生成器是一个自动编码器。 可以在[第 3 章](../Text/03.html),*图像检索*中找到该实现。 此后,我们将使用以下代码定义损失: + +```py +loss = tf.contrib.gan.gan_loss( + gan, gradient_penalty=1.0) + +l1_pixel_loss = tf.norm(gan.real_data - gan.generated_data, ord=1) + +loss = tf.contrib.gan.losses.combine_adversarial_loss( + loss, gan, l1_pixel_loss, weight_factor=1) +``` + +GAN 损失的梯度是不利的。 然后,计算逐像素损失并将其添加到损失的损失中。 训练此模型将创建一个功能强大的自动编码器,可用于图像压缩。 + +# 图片翻译 + +正如我们在应用程序部分中所了解的,可以将一个图像转换为另一个图像。 输入图像被提供给鉴别器,而目标图像被提供给生成器,同时创建 GAN 模型,如下所示: + +```py +gan = tf.contrib.gan.gan_model( + get_generator, + get_discriminator, + real_images, + input_images) +``` + +除像素级损失外,最小二乘损失也用于训练模型。 可以使用以下代码进行计算: + +```py +loss = tf.contrib.gan.gan_loss( + gan, + tf.contrib.gan.losses.least_squares_generator_loss, + tf.contrib.gan.losses.least_squares_discriminator_loss) + +l1_loss = tf.norm(gan.real_data - gan.generated_data, ord=1) + +gan_loss = tf.contrib.gan.losses.combine_adversarial_loss( + loss, gan, l1_loss, weight_factor=1) +``` + +使用此技术,可以将一个图像转换为另一个图像。 + +# InfoGAN + +InfoGAN 无需任何明确的监督培训即可生成所需标签的图像。 `infogan_model`接受非结构化和结构化的输入,如以下代码所示: + +```py +info_gan = tf.contrib.gan.infogan_model( + get_generator, + get_discriminator, + real_images, + unstructured_input, + structured_input) + +loss = tf.contrib.gan.gan_loss( + info_gan, + gradient_penalty_weight=1, + gradient_penalty_epsilon=1e-10, + mutual_information_penalty_weight=1) +``` + +由于训练不稳定,因此将损失定义为罚款。 增加罚分可以在训练过程中提供更大的稳定性。 + +# GAN 的缺点 + +GAN 生成的图像具有一些缺点,例如计数,透视图和全局结构。 当前正在广泛研究以改进模型。 + +# 视觉对话模型 + +**视觉对话模型**( **VDM** )可以基于图像进行聊天。 VDM 应用了计算机视觉,**自然语言处理**( **NLP** )和聊天机器人的技术。 它发现了主要的应用程序,例如向盲人解释图像,向医生解释医学扫描,虚拟伴侣等。 接下来,我们将看到解决这一难题的算法。 + +# VDM 算法 + +**Lu et al** ( [https://research.fb.com/wp-content/uploads/2017/11/camera_ready_nips2017.pdf)](https://research.fb.com/wp-content/uploads/2017/11/camera_ready_nips2017.pdf))提出了此处讨论的算法。 Lu 等人提出了基于 GAN 的 VDM。 生成器生成答案,鉴别器对这些答案进行排名。 以下是该过程的示意图: + +![](img/d64d01f5-a996-4063-aac0-a64fec3d0e15.png) + +基于 GAN 技术的 VDM 架构[摘自 Lu 等人] + +聊天历史,当前问题和图像将作为输入提供给生成器。 接下来,我们将看到生成器如何工作。 + +# 发电机 + +生成器具有编码器和解码器。 编码器将图像,问题和历史记录作为输入。 编码器首先关注 **LSTM** 的历史记录,并关注图像的输出。 流程如下所示: + +![](img/f1aed34d-d664-451a-a18e-50148eb5990b.png) + +转载自 Lu 等。 + +整个历史记录都可用,并且 **LSTM** 记录了聊天的历史记录。 输出伴随有产生嵌入的图像。 编码器生成的嵌入被解码器用来创建答案。 解码器由 RNN 制成。 编码器和解码器一起形成生成器,生成可能的答案。 接下来,我们将了解鉴别器的工作原理。 + +# 鉴别器 + +鉴别器从生成器获取生成的序列并对其进行排序。 排名是通过对 n 对损失学习的嵌入完成的。 n 对损失类似于三重态损失,不同之处在于使用几对正负对进行比较。 这是该模型产生的一些结果。 + +![](img/981124a1-6d73-4233-a453-f44e7861a98e.png) + +转载自 Lu 等。 + +结果是合理的,并且比简单的鉴别器产生的结果更好。 + +# 摘要 + +在本章中,我们了解了生成模型和大量应用程序。 我们实施它们是为了在保留内容的同时将样式从一种转换为另一种。 我们看到了 GAN 背后的直觉和经过训练的模型可以做到这一点。 最后,我们了解了视觉对话系统。 + +在下一章中,我们将学习用于视频分析的深度学习方法。 我们将看到如何通过摄像机,文件等访问视频内容。 我们将通过在帧级别和整个视频上应用分类来实现视频分类。 稍后,我们将看到如何跟踪视频中的对象。 \ No newline at end of file diff --git a/docs/dl-cv/09.md b/docs/dl-cv/09.md new file mode 100644 index 00000000..29c43939 --- /dev/null +++ b/docs/dl-cv/09.md @@ -0,0 +1,378 @@ +# 视频分类 + +在本章中,我们将看到如何训练视频数据的深度学习模型。 我们将开始按帧对视频进行分类。 然后,我们将使用时间信息以获得更好的准确性。 稍后,我们将图像的应用扩展到视频,包括姿势估计,字幕和生成视频。 + +在本章中,我们将涵盖的以下主题: + +* 视频分类的数据集和算法 +* 将视频分成帧并分类视频 +* 在单个框架级别上训练视觉特征模型 0 +* 了解 3D 卷积及其在视频中的使用 +* 在视频上合并运动矢量 +* 利用时间信息进行目标跟踪 +* 人体姿势估计和视频字幕等应用 + +# 了解视频并对其分类 + +视频不过是一系列图像。 视频沿时间方向为图像带来了新的维度。 图像的空间特征和视频的时间特征可以放在一起,比仅图像提供更好的结果。 额外的维度还导致大量空间,因此增加了训练和推理的复杂性。 用于处理视频的计算需求非常高。 视频还改变了深度学习模型的架构,因为我们必须考虑时间特征。 + +视频分类是用类别标记视频的任务。 类别可以在帧级别,也可以在整个视频中。 视频中可能有执行的动作或任务。 因此,视频分类可以标记视频中存在的对象或标记视频中发生的动作。 在下一部分中,我们将看到用于视频分类任务的可用数据集。 + +# 探索视频分类数据集 + +视频分类是视频数据研究的主要问题。 拍摄了几个视频,并标记了与数据相关的各种对象或动作。 数据集根据大小,质量和标签类型而有所不同。 有些甚至包括多个视频标签。 这些视频通常很短。 长视频可能会执行各种操作,因此可以在分别对剪切的视频片段或摘要进行分类之前在时间上进行分割。 接下来,我们将考虑一些特定数据集的细节。 + +# UCF101 + +佛罗里达中部**大学**( **UCF101** )是用于动作识别的数据集。 这些视频是在 YouTube 上收集的,由逼真的动作组成。 此数据集中有 101 个操作类别。 还有另一个名为 **UCF50** 的数据集,它具有 50 个类别。 整个动作中该数据集中有 13,320 个视频。 这些视频具有背景,比例,姿势,遮挡和照明条件的多种变化。 动作类别分为 25 个,它们具有相似的变化,例如背景,姿势,比例,视点,照明等。 + +动作和每个动作的视频数显示如下: + +![](img/fa184ce6-4c4f-4e4e-9eb0-46906bd0b009.png) + +来源:http://crcv.ucf.edu/data/UCF101/Number%20of%20Videos%202.jpg + +所有 101 个动作都分为五种类型的动作,如下所示:人与物体的交互,身体动作,人与人的交互,演奏乐器和运动。 数据集和注释可从 [http://crcv.ucf.edu/data/UCF101.php](http://crcv.ucf.edu/data/UCF101.php) 下载。 + +接下来,我们将了解 YouTube-8M 数据集。 + +# YouTube-8M + +**YouTube-8M** 数据集用于视频分类问题。 数据集包含带有标签和视觉功能的视频 URL。 以下是有关数据集的一些统计信息: + +* **视频 URL 的数量**:700 万 +* **影片剪辑的时长**:450,000 +* **类标签的数量**:4,716 +* **每个视频的平均标签数**:3.4 + +以下是各种类型的数据集摘要: + +![](img/53ee0eb1-cef3-4906-84ef-e972b7792a36.png) + +来源:https://research.google.com/youtube8m/vertical-videos.png + +前面的图像可以让您一眼看出数据集中可用的标签类型。 视频数据很大,因此视觉特征被计算并随数据集一起提供。 可以通过以下链接访问数据集: [https://research.google.com/youtube8m/。](https://research.google.com/youtube8m/.) + +# 其他数据集 + +还有更多的数据集可用于视频分类问题。 以下是更多数据集的详细信息: + +* **Sports-1M** ( **Sports-1 百万**):拥有 1,133,158 个具有 487 个课程的视频。 注释是自动完成的。 数据集可以从以下位置下载: [http://cs.stanford.edu/people/karpathy/deepvideo/](http://cs.stanford.edu/people/karpathy/deepvideo/) 。 +* **UCF-11** (**中佛罗里达大学-11 动作**):拥有 1,600 部视频,包含 11 动作。 视频的速度为 29.97 fps(每秒帧数)。 数据集可以与`UCF101`一起下载。 +* **HMDB-51** (**人体运动数据库-51 个动作**):包含 5,100 个具有 51 个动作的视频。 数据集链接为: [http://serre-lab.clps.brown.edu/resource/hmdb-a-large-human-motion-database](http://serre-lab.clps.brown.edu/resource/hmdb-a-large-human-motion-database) 。 +* **Hollywood2** :拥有 12 个动作的 1,707 个视频。 数据集链接为: [http://www.di.ens.fr/~laptev/actions/hollywood2](http://www.di.ens.fr/~laptev/actions/hollywood2) 。 + +我们已经看到了可用于视频分类任务的数据集,以及描述和访问链接。 接下来,我们将看到如何加载视频并将其拆分为帧以进行进一步处理。 + +# 将视频分成帧 + +视频可以转换为帧并保存在目录中以备将来使用。 分成帧可以通过在训练过程之前对视频进行解压缩来帮助我们节省时间。 首先,让我们看一下将视频转换为帧的代码片段: + +```py +import cv2 +video_handle = cv2.VideoCapture(video_path) +frame_no = 0 while True: + eof, frame = video_handle.read() + if not eof: + break + cv2.imwrite("frame%d.jpg" % frame_no, frame) + frame_no += 1 +``` + +使用此代码段,所有前面的数据集都可以转换为帧。 请注意,这将需要大量的硬盘空间。 + +# 视频分类方法 + +视频必须针对几种应用进行分类。 由于视频中包含大量数据,因此还必须考虑训练和推理计算。 所有视频分类方法均受图像分类算法启发。 VGG,Inception 等标准体系结构用于帧级别的特征计算,然后进行进一步处理。 诸如 **CNN** , **注意** ,先前章节中学习的和 **LSTM** 之类的概念将在此处有用。 直观地,以下方法可用于视频分类: + +* 提取帧并使用在[第 2 章](../Text/02.html),*图像分类*中学习的模型,以帧为基础进行分类。 +* 提取在[第 3 章](../Text/03.html),*图像检索*中学习的图像特征,并且可以按照[第 7 章](../Text/07.html),*图像说明中的描述,使用这些特征训练 RNN。* 。 +* 在整个视频上训练 **3D 卷积**网络。 3D 卷积是 2D 卷积的扩展; 我们将在以下各节中详细了解 3D 卷积的工作原理。 +* 使用视频的**光流**可以进一步提高精度。 光流是物体运动的模式,我们将在接下来的部分中详细介绍。 + +我们将看到几种算法,它们在各种计算复杂性上都具有良好的准确性。 可以通过将数据集转换为帧并将其子采样为相同的长度来准备它。 一些预处理会有所帮助,例如减去 Imagenet 的均值。 + +# 融合并行 CNN 进行视频分类 + +就帧而言,由于图像的下采样,视频的预测可能不会产生良好的结果,从而丢失了精细的细节。 使用高分辨率的 CNN 将增加推理时间。 因此,Karpathy 等。 ( [https://static.googleusercontent.com/media/research.google.com/zh-CN//pubs/archive/42455.pdf](https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/42455.pdf) )建议融合两个流 并行运行进行视频分类。 进行逐帧预测有两个问题,即: + +* 由于较大的 CNN 架构,预测可能需要很长时间 +* 独立的预测会沿时间维度丢失信息 + +使用更少的参数和两个并行运行的较小编码器可以简化体系结构。 视频同时通过两个 CNN 编码器传递。 一个编码器需要较低的分辨率并要处理高分辨率。 编码器具有交替的卷积,规范化和合并层。 两个编码器的最后一层通过完全连接的层连接。 另一个编码器具有相同的大小,但仅进行中心裁剪,如下所示: + +![](img/1f34b3d0-4e01-4ed2-a5fa-da513c7a1155.png) + +转载自 Karpathy 等。 + +帧的并行处理通过对视频进行下采样来加快运行时间。 CNN 体系结构的参数减半,同时保持相同的精度。 这两个流称为,,**中央凹**,,和**上下文**。 以下代码段显示了流: + +```py +high_resolution_input = tf.placeholder(tf.float32, shape=[None, input_size]) +low_resolution_input = tf.placeholder(tf.float32, shape=[None, input_size]) +y_input = tf.placeholder(tf.float32, shape=[None, no_classes]) +high_resolution_cnn = get_model(high_resolution_input) +low_resolution_cnn = get_model(low_resolution_input) +dense_layer_1 = tf.concat([high_resolution_cnn, low_resolution_cnn], 1) +dense_layer_bottleneck = dense_layer(dense_layer_1, 1024) +logits = dense_layer(dense_layer_bottleneck, no_classes) +``` + +下图显示了跨时间维度进行处理的帧: + +![](img/d2211390-ff79-43d4-9b7e-842c29b2b66c.png) + +转自 Karpathy 等。 + +可以在不同的时间观看视频,而不是通过固定大小的剪辑。 在前面的图像中介绍了连接时间信息的三种方式。 后期融合需要更长的时间框架,而早期融合则需要几个帧。 慢速融合将后期融合和早期融合结合在一起,可获得良好效果。 该模型在`Sports1M`数据集上进行了训练,该数据集具有 487 个类别,并达到了 50%的准确性。 将同一模型应用于`UCF101`时,可达到 60%的精度。 + +# 长时间对视频进行分类 + +融合方法适用于短视频片段。 分类较长的视频很困难,因为必须计算和记住很多帧。 Ng 等。 ( [https://www.cv-foundation.org/openaccess/content_cvpr_2015/papers/Ng_Beyond_Short_Snippets_2015_CVPR_paper.pdf](https://www.cv-foundation.org/openaccess/content_cvpr_2015/papers/Ng_Beyond_Short_Snippets_2015_CVPR_paper.pdf) )提出了两种对较长视频进行分类的方法: + +* 第一种方法是在时间上合并卷积特征。 最大池用作功能`aggregation`方法。 +* 第二种方法是使用 LSTM 连接处理各种可变长度视频的卷积功能。 + +下图显示了这两种方法: + +![](img/1fc508d4-5f43-4908-aa11-13d9e0a2b336.png) + +摘自 Ng 等。 + +可以提取 CNN 功能并将其馈送到小型 LSTM 网络,如以下代码所示: + +```py +net = tf.keras.models.Sequential() +net.add(tf.keras.layers.LSTM(2048, + return_sequences=False, + input_shape=input_shape, + dropout=0.5)) +net.add(tf.keras.layers.Dense(512, activation='relu')) +net.add(tf.keras.layers.Dropout(0.5)) +net.add(tf.keras.layers.Dense(no_classes, activation='softmax')) +``` + +添加 LSTM 进行功能池可提供更好的性能。 功能以各种方式合并,如下图所示: + +![](img/72c1430c-6604-4c17-abe6-133fe2d4daa9.png) + +摘自 Ng 等。 + +如图所示,卷积特征可以几种不同的方式聚合。 池在完全连接的层之前完成。 该方法在`Sports1M`数据集和`UCF101`数据集中的准确率分别为 73.1%和 88.6%。 下图显示了 LSTM 方法: + +![](img/bc029064-479f-4788-8aec-077b933fe468.png) + +摘自 Ng 等。 + +该模型的计算量很高,因为使用了多个 LSTM。 + +# 流式传输两个 CNN 以进行动作识别 + +视频中对象的运动具有有关视频中执行的动作的非常好的信息。 物体的运动可以通过光流来量化。 Simonyan 和 Zisserman( [http://papers.nips.cc/paper/5353-two-stream-convolutional-networks-for-action-recognition-in-videos.pdf](http://papers.nips.cc/paper/5353-two-stream-convolutional-networks-for-action-recognition-in-videos.pdf) 提出了一种用于动作识别的方法,该方法使用来自图像和光流的两个流。 + +光流通过量化观察者与场景之间的相对运动来测量运动。 可以在 [https://www.youtube.com/watch?v=5VyLAH8BhF8](https://www.youtube.com/watch?v=5VyLAH8BhF8) 上找到有关光流的详细讲座。 通过运行以下命令可以获得光流: + +```py +p1, st, err = cv2.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lk_params) +``` + +一个流采用单个帧并使用常规 CNN 预测动作。 另一个流获取多个帧并计算光流。 光流通过 CNN 进行预测。 下图显示了这两个预测: + +![](img/ef8e4e07-e018-48ba-b82d-53613dddb8e8.png) + +转载自 Simonyan 和 Zisserman + +两种预测都可以与最终预测结合。 + +# 使用 3D 卷积进行时间学习 + +可以使用 3D 卷积对视频进行分类。 3D 卷积运算将体积作为输入并输出,而 2D 卷积可以将 2D 或体积输出并输出 2D 图像。 区别如下所示: + +![](img/0e8faa5c-11ae-4631-b717-bcab34d2e075.png) + +复制自 Tran 等。 + +前两个图像属于 2D 卷积。 输出始终是图像。 同时,3D 卷积输出一个体积。 区别在于内核在 3 个方向上进行卷积运算。 Tran 等。 ( [https://www.cv-foundation.org/openaccess/content_iccv_2015/papers/Tran_Learning_Spatiotemporal_Features_ICCV_2015_paper.pdf](https://www.cv-foundation.org/openaccess/content_iccv_2015/papers/Tran_Learning_Spatiotemporal_Features_ICCV_2015_paper.pdf) )将 3D 卷积用于视频分类。 3D 卷积模型如下所示: + +![](img/f2f9bf9d-7ffd-4229-b5cf-0b90cbf076a1.png) + +复制自 Tran 等。 + +以下是使用 3D 卷积的模型代码片段: + +```py +net = tf.keras.models.Sequential() +net.add(tf.keras.layers.Conv3D(32, + kernel_size=(3, 3, 3), + input_shape=(input_shape))) +net.add(tf.keras.layers.Activation('relu')) +net.add(tf.keras.layers.Conv3D(32, (3, 3, 3))) +net.add(tf.keras.layers.Activation('softmax')) +net.add(tf.keras.layers.MaxPooling3D()) +net.add(tf.keras.layers.Dropout(0.25)) + +net.add(tf.keras.layers.Conv3D(64, (3, 3, 3))) +net.add(tf.keras.layers.Activation('relu')) +net.add(tf.keras.layers.Conv3D(64, (3, 3, 3))) +net.add(tf.keras.layers.Activation('softmax')) +net.add(tf.keras.layers.MaxPool3D()) +net.add(tf.keras.layers.Dropout(0.25)) + +net.add(tf.keras.layers.Flatten()) +net.add(tf.keras.layers.Dense(512, activation='sigmoid')) +net.add(tf.keras.layers.Dropout(0.5)) +net.add(tf.keras.layers.Dense(no_classes, activation='softmax')) +net.compile(loss=tf.keras.losses.categorical_crossentropy, + optimizer=tf.keras.optimizers.Adam(), metrics=['accuracy']) +``` + +3D 卷积需要大量的计算能力。 3D 卷积在`Sports1M`数据集上达到 90.2%的精度。 + +# 使用轨迹进行分类 + +Wang 等。 ( [https://www.cv-foundation.org/openaccess/content_cvpr_2015/papers/Wang_Action_Recognition_With_2015_CVPR_paper.pdf](https://www.cv-foundation.org/openaccess/content_cvpr_2015/papers/Wang_Action_Recognition_With_2015_CVPR_paper.pdf) )使用身体各部分的轨迹对所执行的动作进行分类。 这项工作结合了手工制作和深度学习的功能,可以进行最终预测。 以下是分类的表示形式: + +![](img/fa390d0d-4a3e-48af-8fe6-4896c092d2e9.png) + +转载自 Wang 等。 + +手工制作的特征是 **Fisher 向量**,这些特征来自 CNN。 下图演示了轨迹和特征图的提取: + +![](img/389aa340-de61-4803-baaf-6e3b78b659f3.png) + +转载自 Wang 等。 + +轨迹和特征图都在时间上组合在一起,以形成关于时间片段的最终预测。 + +# 多峰融合 + +杨等。 ( [http://research.nvidia.com/sites/default/files/pubs/2016-10_Multilayer-and-Multimodal/MM16.pdf](http://research.nvidia.com/sites/default/files/pubs/2016-10_Multilayer-and-Multimodal/MM16.pdf) )提出了一种具有 4 个模型的多模态视频融合 分类。 这四个模型分别是 3D 卷积特征,2D 光流,3D 光流和 2D 卷积特征。 + +该方法的数据流如下所示: + +![](img/9b6f98f2-2e75-4ebb-b40e-ac265467545f.png) + +转载自 Yang 等。 + +现在,让我们了解 Convlet 。 Convlet 是来自单个内核的小卷积输出。 下图显示了 convlet 对卷积层中空间权重的学习: + +![](img/e258e5ea-f039-40f2-b79f-4fa0df3a7b55.png) + +转载自 Yang 等。 + +空间权重指示卷积层中局部空间区域的区分度或重要性。 下图是在多层卷积层和完全连接层上完成的多层表示融合的图示: + +![](img/b4160338-5686-48dd-a31a-f6c07404edcc.png) + +转载自 Yang 等。 + +增强机制用于组合预测。 **Boosting** 是一种可以将多个模型预测组合为最终预测的机制。 + +# 参加区域分类 + +注意机制可以用于分类。 注意机制复制了人类专注于识别活动区域的行为。 注意机制赋予某些区域比其他区域更多的权重。 训练时从数据中学习权重方法。 注意机制主要有两种,即: + +* **柔和的注意力**:性格确定,因此可以通过反向传播来学习。 +* **刻苦关注**:性质随机,这需要复杂的学习机制。 由于需要采样数据,因此也很昂贵。 + +以下是软关注的可视化: + +![](img/5c514fb5-7f01-4436-90e9-65ced3ea1f90.png) + +转载自 Sharma 等。 + +根据注意,计算并加权 **CNN** 功能。 对某些区域的关注或权重可以用于可视化。 Sharma 等。 ( [https://arxiv.org/pdf/1511.04119.pdf](https://arxiv.org/pdf/1511.04119.pdf) )使用此想法对视频进行分类。 **LSTM** 被用作卷积特征。 **LSTM** 通过注意以下帧来预测区域,如下图所示: + +![](img/c835ef24-72c0-4144-9fd8-890500f2c8ac.png) + +转自 Sharma 等人。 + +每个 **LSTM** 堆栈都会预测位置和标签。 每个堆栈具有三个 **LSTM** 。 **LSTM** 堆栈的输入是卷积特征立方体和位置。 位置概率是注意权重。 注意的使用提高了准确性以及可视化预测的方法。 + +我们已经看到了各种视频分类方法。 接下来,我们将学习视频中的其他应用程序。 + +# 将基于图像的方法扩展到视频 + +图像可用于姿势估计,样式转换,图像生成,分割,字幕等等。 同样,这些应用程序也在视频中找到位置。 使用时间信息可以改善来自图像的预测,反之亦然。 在本节中,我们将看到如何将这些应用程序扩展到视频。 + +# 退缩人体姿势 + +人体姿势估计是视频数据的重要应用,可以改善其他任务,例如动作识别。 首先,让我们看一下可用于姿势估计的数据集的描述: + +* **摆在野生的** **数据集**中:包含 30 个带有人体姿势注释的视频。 数据集链接为: [https://lear.inrialpes.fr/research/posesinthewild/](https://lear.inrialpes.fr/research/posesinthewild/) 。 该数据集带有人类上半身关节的注释。 +* **电影院中标记的帧**( **FLIC** ):从 30 部电影中获得的人体姿势数据集,可在以下位置找到: [https://bensapp.github.io/flic-dataset.html](https://bensapp.github.io/flic-dataset.html) 。 + +Pfister 等。 ( [https://www.cv-foundation.org/openaccess/content_iccv_2015/papers/Pfister_Flowing_ConvNets_for_ICCV_2015_paper.pdf](https://www.cv-foundation.org/openaccess/content_iccv_2015/papers/Pfister_Flowing_ConvNets_for_ICCV_2015_paper.pdf) )提出了一种预测视频中人体姿势的方法。 以下是回归人体姿势的管道: + +![](img/59d379eb-94cf-453c-aeac-28b1a04e6e44.png) + +复制自 Pfister 等。 + +视频中的帧被获取并通过卷积网络。 融合图层,并获得姿势热图。 姿势热图与光流结合以获得扭曲的热图。 合并时间范围内的扭曲热图,以生成合并的热图,得到最终姿势。 + +# 跟踪面部标志 + +视频中的人脸分析需要人脸检测,界标检测,姿势估计,验证等。 计算地标对于捕获面部动画,人机交互和人类活动识别尤其重要。 除了在帧上进行计算外,还可以在视频上进行计算。 Gu 等。 ( [http://research.nvidia.com/sites/default/files/pubs/2017-07_Dynamic-Facial-Analysis/rnnface.pdf](http://research.nvidia.com/sites/default/files/pubs/2017-07_Dynamic-Facial-Analysis/rnnface.pdf) )提出了一种使用联合估计的检测和跟踪方法 使用 RNN 的视频中的面部地标。 结果优于逐帧预测和其他先前模型。 地标由 CNN 计算,时间方面在 RNN 中编码。 综合数据用于训练。 + +# 分割影片 + +使用时间信息时,可以更好地分割视频。 加德(Gadde)等人。 ( [https://ps.is.tuebingen.mpg.de/uploads_file/attachment/attachment/386/gadde2017videocnns.pdf](https://ps.is.tuebingen.mpg.de/uploads_file/attachment/attachment/386/gadde2017videocnns.pdf) )提出了一种通过扭曲来组合时间信息的方法。 下图演示了该解决方案,该方法将两个帧分段并且结合了变形: + +![](img/0d1ae328-95c8-4794-b8a4-196046e5f578.png) + +转载自 Gadde 等。 + +下图显示了翘曲网: + +![](img/8c759f74-da66-4844-a5d5-66369605ba25.png) + +转载自 Gadde 等。 + +在两个帧之间计算光流,将它们与变形结合在一起。 变形模块获取光流,对其进行转换,然后将其与变形的表示相结合。 + +# 字幕视频 + +[第 7 章](../Text/07.html),*图像字幕*说明了几种组合文本和图像的方法。 同样,可以为视频生成字幕,以描述上下文。 让我们看一下可用于字幕视频的数据集列表: + +* **Microsoft Research-视频转文本**( **MSR-VTT** )具有 200,000 个视频剪辑和句子对。 可以从以下网站获取更多详细信息: [https://www.microsoft.com/zh-cn/research/publication/msr-vtt-a-large-video-description-dataset-for-bridging-video-and- 语言/](https://www.microsoft.com/en-us/research/publication/msr-vtt-a-large-video-description-dataset-for-bridging-video-and-language/) 。 +* **MPII 电影描述语料库**( **MPII-MD** )可以从以下网站获取: [https://www.mpi-inf.mpg.de/departments/computer-vision-and 多模态计算/研究/视觉和语言/ mpii 电影描述数据集](https://www.mpi-inf.mpg.de/departments/computer-vision-and-multimodal-computing/research/vision-and-language/mpii-movie-description-dataset)。 它有 68,000 个句子和 94 部电影。 +* **蒙特利尔视频注释数据集**( **M-VAD** )可从以下网站获得: [https://mila.quebec/zh/publications/public-datasets / m-vad /](https://mila.quebec/en/publications/public-datasets/m-vad/) 和有 49,000 个剪辑。 +* **YouTube2Text** 包含 1,970 个视频,包含 80,000 个描述。 + +姚等。 ( [https://www.cv-foundation.org/openaccess/content_iccv_2015/papers/Yao_Describing_Videos_by_ICCV_2015_paper.pdf](https://www.cv-foundation.org/openaccess/content_iccv_2015/papers/Yao_Describing_Videos_by_ICCV_2015_paper.pdf) )提出了一种为视频添加字幕的方法。 经过训练以进行动作识别的 3D 卷积网络用于提取局部时间特征。 然后在特征上使用注意力机制以使用 RNN 生成文本。 该过程如下所示: + +![](img/71cfe3d2-2787-4234-a683-8ca292510133.png) + +转载自 Yao 等。 + +Donahue 等。 ( [https://www.cv-foundation.org/openaccess/content_cvpr_2015/papers/Donahue_Long-Term_Recurrent_Convolutional_2015_CVPR_paper.pdf](https://www.cv-foundation.org/openaccess/content_cvpr_2015/papers/Donahue_Long-Term_Recurrent_Convolutional_2015_CVPR_paper.pdf) )提出了另一种视频字幕或描述方法,该方法将 **LSTM** 与 卷积功能。 + +这类似于前面的方法,除了我们在此处使用 2D 卷积功能,如下图所示: + +![](img/fe273a12-ded0-4027-b3d1-0657dee35178.png) + +摘自 Donahue 等。 + +我们有几种将文本与图像结合起来的方法,例如活动识别,图像描述和视频描述技术。 下图说明了这些技术: + +![](img/37ba89c5-80e9-4655-8e8f-9e664ddd5b29.png) + +摘自 Donahue 等。 + +Venugopalan 等。 ( [https://www.cv-foundation.org/openaccess/content_iccv_2015/papers/Venugopalan_Sequence_to_Sequence_ICCV_2015_paper.pdf](https://www.cv-foundation.org/openaccess/content_iccv_2015/papers/Venugopalan_Sequence_to_Sequence_ICCV_2015_paper.pdf) )提出了一种使用编码器-解码器方法进行视频字幕的方法。 以下是他提出的技术的可视化: + +![](img/6935b3c7-f3bb-4876-a3d5-f6f6559d7e05.png) + +复制自 Venugopalan 等。 + +对于此方法,可以在图像的帧或光流上计算 **CNN** 。 + +# 产生影片 + +可以使用生成模型以无监督的方式生成视频。 可以使用当前帧预测未来的帧。 Ranzato 等。 ( [https://arxiv.org/pdf/1412.6604.pdf](https://arxiv.org/pdf/1412.6604.pdf) )提出了一种受语言模型启发的视频生成方法。 RNN 模型用于拍摄图像补丁并预测下一个补丁。 + +# 摘要 + +在本章中,我们涵盖了与视频分类有关的各种主题。 我们看到了如何将视频拆分为帧,以及如何将图像中的深度学习模型用于各种任务。 我们介绍了一些特定于视频的算法,例如跟踪对象。 我们看到了如何将基于视频的解决方案应用于各种场景,例如动作识别,手势识别,安全应用程序和入侵检测。 + +在下一章中,我们将学习如何将上一章中训练有素的模型部署到各种云和移动平台上的生产环境中。 我们将看到不同的硬件如何影响延迟和吞吐量方面的性能。 \ No newline at end of file diff --git a/docs/dl-cv/10.md b/docs/dl-cv/10.md new file mode 100644 index 00000000..99b87ab6 --- /dev/null +++ b/docs/dl-cv/10.md @@ -0,0 +1,242 @@ +# 部署方式 + +在本章中,我们将学习如何在各种平台上部署经过训练的模型,以实现最大吞吐量和最小延迟。 我们将了解 GPU 和 CPU 等各种硬件的性能。 我们将遵循在 Amazon Web Services,Google Cloud Platform 等平台以及 Android,iOS 和 Tegra 等移动平台上部署 TensorFlow 的步骤。 + +我们将在本章介绍以下主题: + +* 了解影响深度学习模型训练和推理性能的因素 +* 通过各种方法提高性能 +* 查看各种硬件的基准并学习调整它们以实现最佳性能的步骤 +* 使用各种云平台进行部署 +* 使用各种移动平台进行部署 + +# 模型表现 + +性能对于深度学习模型的培训和部署都很重要。 由于大数据或大模型架构,培训通常需要更多时间。 结果模型可能更大,因此在 RAM 受限的移动设备中使用时会出现问题。 更多的计算时间导致更多的基础架构成本。 推理时间在视频应用中至关重要。 由于前面提到了性能的重要性,因此在本节中,我们将研究提高性能的技术。 降低模型复杂度是一个简单的选择,但会导致精度降低。 在这里,我们将重点介绍一些方法,这些方法可以提高性能,而准确性却没有明显的下降。 在下一节中,我们将讨论量化选项。 + +# 量化模型 + +深度学习模型的权重具有 32 位浮点值。 当权重量化为 8 位时,精度下降很小,因此在部署中不会注意到。 结果权重的精度似乎对深度学习模型的精度性能影响较小。 这个想法对深度学习很有趣,并且在模型大小变得至关重要时很有用。 通过用 8 位值替换 32 位浮点值,可以显着减小模型大小并提高推理速度。 实施模型量化时有很多选择。 权重可以存储在 8 位中,但推理操作可以以 32 位浮点值执行。 架构的每个组件在量化大小上的行为可能有所不同,因此,取决于层,可以选择 32 或 16 或 8 位值。 + +量化工作有多种原因。 通常,深度学习模型经过训练可以解决图像中的噪声,因此可以被认为是健壮的。 推理计算可以具有冗余信息,并且可以由于量化而去除冗余信息。 + +最新的 CPU 和 RAM 硬件已针对浮点计算进行了调整,因此在此类硬件中量化效果可能不太明显。 随着为此目的引入越来越多的硬件,这种情况正在改变。 在 GPU 中,由于内存和速度现已适应较低的精确浮点运算,因此它们在内存和速度上存在明显差异。 还有其他特殊硬件可用于运行不太精确的浮动操作。 + +# 移动网 + +霍华德(Howard)和其他人( [https://arxiv.org/pdf/1704.04861.pdf](https://arxiv.org/pdf/1704.04861.pdf) )引入了一种称为 **MobileNets** 的新型模型,可用于移动和嵌入式应用程序。 MobileNets 可以用于不同的应用程序,例如对象检测,地标识别,人脸属性,细粒度分类,如下所示: + +![](img/f363f9cf-4786-41e0-9812-d49b4c26039a.png) + +转载自霍华德等人 + +MobileNets 通过用深度( **b** )和点向卷积( **c** )替换标准卷积滤波器( **a** )和点卷积( **c** )来减少模型的大小和计算量,如下所示: + +![](img/dab9c6a5-cbd9-485e-8e96-832c17bedf0a.png) + +转载自霍华德等人 + +批量归一化和激活层被添加到深度和点积卷积中,如下所示: + +![](img/4ee8ae67-9658-480e-a775-5b6f45033be8.png) + +转载自霍华德等人 + +有两个参数会影响模型的选择: + +* **乘法和加法次数**:精度和多加法之间的权衡如下所示: + +![](img/9f6fad9e-9ab5-405c-a5ac-8958cb342906.png) + +转载自霍华德等人 + +* **模型**中的参数数量:此处显示权衡: + +![](img/5a370687-9b2b-4f57-80fd-87792fdfae85.png) + +转载自霍华德等人 + +MobileNets 已显示,可以在移动和嵌入式设备上使用的精度有所降低的情况下,可以减少模型的计算和尺寸。 在霍华德等人的文章中可以看到模型与精度之间的确切权衡。 + +# 云端部署 + +必须将这些模型部署在云中以用于多个应用程序。 我们将为此目的寻找主要的云服务提供商。 + +# AWS + +Amazon Web Services(AWS)将支持扩展到基于 TensorFlow 的模型的开发和部署。 在 [https://aws.amazon.com/](https://aws.amazon.com/) 上注册 AWS,然后选择 **Amazon Machine Images** ( **AMI** )之一。 AMI 是安装了所有必需软件的计算机的映像。 您不必担心安装软件包。 **AWS 提供了深度学习 AMI** ( **DLAMI** ),以简化培训和部署深度学习模型。 有几种选择。 在这里,我们将使用 Conda,因为它带有运行 TensorFlow 所需的几个软件包。 Python 有两个选项:版本 2 和版本 3。以下代码将在 CUDA 8 的 Python 3 上使用 Keras 2 激活 TensorFlow: + +```py +source activate tensorflow_p36 +``` + +以下代码将在 CUDA 8 的 Python 2 上使用 Keras 2 激活 TensorFlow: + +```py +source activate tensorflow_p27 +``` + +您可以访问 [https://aws.amazon.com/tensorflow/](https://aws.amazon.com/tensorflow/) 了解更多详细信息和教程。 + +还可以通过执行以下给定的步骤来启动**虚拟机**( **VM** ): + +1. 转到 [aws.amazon.com](https://aws.amazon.com/) ,然后使用您的 Amazon 帐户登录。 +2. 从登录页面选择启动虚拟机: + +![](img/d8d35771-0127-4f5f-9f7a-37edc6eb0c1c.png) + +3. 在下一个窗口中,单击入门,选择 EC2 实例,如下所示: + +![](img/cf4ce914-ae52-4a17-968d-206854461304.png) + +4. 为 EC2 实例命名: + +![](img/afc41e92-9559-4fb0-9214-91b3d9902bf4.png) + +5. 选择操作系统的类型: + +![](img/5d91face-5671-4648-93fd-69708c41002b.png) + +6. 选择实例类型。 实例类型指示 RAM 和 CPU 大小不同的配置类型。 也有两个选项可供选择。 选择实例类型,然后单击“下一步”按钮: + +![](img/39eeaa24-3a0a-47d7-8e72-87048ae2d5f3.png) + +7. 创建一个隐私增强型邮件安全证书(PEM)文件,该文件将用于登录,如下所示: + +![](img/25ab45d2-9f98-4720-a642-03263e11295b.png) + +8. 创建实例将花费一些时间,最后,将显示完成状态: + +![](img/06ba2432-c401-46c9-9046-5097bad8cbfc.png) + +9. 接下来,单击进入 EC2 控制台按钮: + +![](img/973f1291-4651-47fd-b924-e999c5f583a6.png) + +10. 现在将创建实例; 单击连接按钮,如下所示: + +![](img/9c23756d-5135-44af-9f23-d59513e9d581.png) + +11. 接下来,必须将实例连接到虚拟机的命令提示符。 连接所需的说明在此步骤中给出。 您需要在之前的步骤中下载“ pem”文件。 按照显示的说明连接到系统: + +![](img/13ef9b43-6bd6-4ef3-a9e9-526f0a69c84e.png) + +12. 完成后,通过单击操作|实例状态|终止来终止实例: + +![](img/c7b106a4-3f86-44a5-94fb-e8795643e0d2.png) + +安装和执行步骤可以遵循[第 1 章](../Text/01.html),*入门*。 + +# Google Cloud Platform + +Google Cloud Platform(GCP)是 Google 提供的云平台,具有与 AWS 类似的功能。 通过执行以下步骤,可以使用一个简单的虚拟机来训练诸如 AWS 之类的模型: + +1. 使用 [cloud.google.com](https://cloud.google.com/) 转到 Google Cloud Platform,然后使用您的 Gmail 帐户登录到该平台。 +2. 现在,通过单击转到控制台按钮进入控制台: + +![](img/b38e13e2-2b2a-45e4-841c-fc5feed34fcd.png) + +3. 进入控制台后,通过单击 Compute Engine |进入 **VM 创建页面**。 右上角菜单中的 VM 实例,如以下屏幕截图所示: + +![](img/0a5622e5-c108-4b28-8960-1eab8e3d9574.png) + +4. 然后单击 CREATE INSTANCE 按钮,以创建所需的实例: + +![](img/0671e492-4e4b-4edd-8622-8208b096056e.png) + +5. 接下来,可以通过配置选择实例类型。 Zone 参数通知区域将部署实例。 通过选择靠近用户的区域,可以节省等待时间。 可以使用所需的 RAM 和 CPU 定制机器类型。 还可以选择 GPU,以进行更快的训练。 选择实例的大小,然后单击“创建”按钮,如以下屏幕截图所示: + +![](img/ae5b0924-3d17-4afa-9e63-baa8859245ba.png) + +6. 创建实例将需要几分钟。 然后,单击实例的 SSH 下拉列表,然后选择“在浏览器窗口中打开”选项,如下所示,以在浏览器中打开控制台: + +![](img/783c512d-4692-4eeb-914d-9ff3f2d430b1.png) + +使用该外壳,您可以安装 TensorFlow 并可以训练或部署模型。 有许多选项可从虚拟机的配置中选择。 根据成本和时间的权衡,可以选择配置。 + +GCP 具有**云机器学习引擎**,可在使用 TensorFlow 时为我们提供帮助。 GCP 的三个组件可以一起用于构建培训和部署基础架构: + +1. 用于 的 Cloud DataFlow 预处理图像 +2. 用于 和 培训和部署模型的云机器学习引擎 +3. Google Cloud Storage 用于存储培训数据,代码和结果 + +可以在 [https://cloud.google.com/ml-engine/docs 上找到 使用云机器学习引擎建立自定义图像分类模型的出色教程。 / flowers-tutorial](https://cloud.google.com/ml-engine/docs/flowers-tutorial) 。 + +# 在设备中部署模型 + +TensorFlow 模型也可以部署在移动设备中。 移动设备包括智能手机,无人机,家用机器人等。 数十亿智能手机可以具有可以使用深度学习的计算机视觉应用程序。 可以拍照并搜索,流化带有标记场景的视频等。 在移动设备中进行部署意味着深度学习模型存在于设备上,并且推断发生在设备上。 设备上部署的模型有助于解决隐私问题。 在以下主题中,我们将讨论如何在各种移动平台上部署它们。 + +# 杰特逊 TX2 + +Jetson TX2 是由 NVIDIA 提供的嵌入式设备,专门用于高效 AI 计算。 Jetson TX2 轻巧,紧凑,因此适合在无人机,公共场所等中部署。 它还附带预装的 TensorRT,这是 TensorFlow 的运行时。 您可以购买 Jetson 并在安装 TensorFlow 之前快速安装 Ubuntu,CUDA,CUDNN。 克隆 [https://github.com/jetsonhacks/installTensorFlowTX2](https://github.com/jetsonhacks/installTensorFlowTX2) ,然后在命令提示符下输入以下命令。 + +1. 首先,在以下代码的帮助下安装必备组件: + +```py + ./installPrerequisites.sh +``` + +2. 现在,使用以下代码克隆 TensorFlow: + +```py + ./cloneTensorFlow.sh +``` + +3. 接下来,使用 以下代码设置所需的环境变量: + +```py + ./setTensorFlowEV.sh +``` + +4. 现在我们将使用 以下代码构建 TensorFlow : + +```py + ./buildTensorFlow.sh +``` + +5. 现在,我们将使用以下代码 使用 将打包文件处理为 Wheel 文件: + +```py + ./packageTensorFlow.sh +``` + +6. 现在,我们将使用以下代码安装 Tensorflow: + +```py + pip install $HOME/tensorflow-1.0.1-cp27-cp27mu-linux_aarch64.whl +``` + +借助这些步骤,我们可以在 Jetson TX2 中安装 TensorFlow。 + +# 安卓系统 + +任何 Android 应用程序都可以使用 TensorFlow,其构建细节可以在 [https://www.tensorflow.org/mobile/android_build](https://www.tensorflow.org/mobile/android_build) 中找到。 关于此的官方示例可以在 [https://github.com/tensorflow/tensorflow/tree/master/tensorflow/examples/android](https://github.com/tensorflow/tensorflow/tree/master/tensorflow/examples/android) 中找到。 假设读者具有 Android 编程经验,则在 Android 设备中实现 Tensorflow 的步骤如下: + +1. 使用[第 3 章](../Text/03.html)和*图像检索*中介绍的步骤,将 TensorFlow 模型导出到`.pb`文件。 +2. 生成二进制文件`.so`和`.jar`。 +3. 编辑`gradle`文件以启用库加载。 +4. 加载并运行 Android 应用文件 + +# 苹果手机 + +苹果使用 CoreML 框架将机器学习集成到 iPhone 应用程序中。 Apple 提供了可以直接集成到应用程序中的标准模型列表。 您可以使用 TensorFlow 训练自定义深度学习模型并将其在 iPhone 中使用。 为了部署自定义模型,您必须在 CoreML 框架模型中隐藏 TensorFlow。 谷歌发布了 [https://github.com/tf-coreml/tf-coreml](https://github.com/tf-coreml/tf-coreml) ,用于将 TensorFlow 模型转换为 CoreML 模型。 可以使用以下代码安装 TFcoreML: + +```py +pip install -U tfcoreml +``` + +可以使用以下代码导出模型: + +```py +import tfcoreml as tf_converter +tf_converter.convert(tf_model_path='tf_model_path.pb', + mlmodel_path='mlmodel_path.mlmodel', + output_feature_names=['softmax:0'], + input_name_shape_dict={'input:0': [1, 227, 227, 3]}) +``` + +iPhone 可以使用导出的模型进行预测。 + +# 摘要 + +在本章中,我们了解了如何在各种平台和设备上部署经过训练的深度学习模型。 我们已经介绍了为这些平台获得最佳性能的步骤和准则。 我们已经看到了 MobileNets 的优势,它以很小的精度权衡来减少推理时间。 \ No newline at end of file diff --git a/docs/dl-cv/cover.jpg b/docs/dl-cv/cover.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dbbc8fc396101bfad352d09eb62a638e6400f8c2 GIT binary patch literal 24302 zcmV(#K;*xPP)
#US>>!tgVg@ym@8=AE@ZNaiU*oHqg&S|H{hYA%n zlUAJ1X-3PiKo}5CO{fWD$$oOFu&g@b4o9}24*#GWv}spzfqfEK7UtNyL6gGW!-$n3 zu+(*(=OhD&aLEkSMWNmFnph`VfHC*Fzz5*x6uiz1VWC*KPIYl-J#y-$uM17*8l4!p zNi`_y;f}xXF088W@r;P8JyYODua=Z3F&NXcUO?o8YME64y=n}CB`ffW2_r}|LFlSR z8GXl@5gbEZpQMHSeN73?gjO@8j$BFCF)U0MH@ZmZTJGtdrn;<;^{Jp6&GA~gmofO} zO&9zKM9&SrImMv(Xi^>VsH7i~3LjhGVd`%q91?{!j;3exGc20iwS*9Jpk#wS=Gi*m zQqBiKL7BVEv*HCw1XW3Xp!4$8o#RRm3umTMJ;T$pt)@glstMUywnA*D^E|_A@%nVV z&Njs^%kZYLiqfC!^;2o|8gE*l96;R=Q!DP|h)1@?F-OV3k!rV_!hWz^032TU$(p3| z<__`vH>;d{qj5HLwWd#5j?YC^vRs}lXT`4IH^hr6W}>dX>pPXw>2z&|Y?#s)r@xT~ z3Vid|%-)n>LDDWGz0Q_@RebR_#^;u-E3(ZI$0X9VDW({*)IBWBB`HsMtkRiRo(6b& z@C<%}nbR3|djPq7`CYz2j2Y6EPjPh0o1-Y< z`B9|v*30ZW4NpDzYLvmA7@U|ASSS`qGqluEa&%H{1tk6LhYY!X?gd`9UOBvoOjb_w zW>&n%0;Rn+ z0f88S>1_v$cy3+d7$d@K4W5| zbxBAyagslkMY0nX5G3V!p?TEVHWzYYSYlvU))`G5p^=`HB}tVtEWY;Wyd5vFNE(!> zMbo`W%+q)n6Nfk^xscrn5g!sm?7J*Pou&;e!1GYx#e5HyZc*6BG%O^dnGhRFe`df$ z%grn-hGEIHo(qH(J-slj{l1$J8wSSqh-mQQD4$^1#HjE7roS|}MBm)Z=`0>L5|5Vg z+e*)dUu0ZbsC5gmiuH@RPh@f!7IZ9R)Em^GyB;j3qSQ3{w`4|u#W7#?ofD%L027V3 zTl26eh9x^B^n@Z#i8fY5X@3=nmqb{Jr8cT+n(LKVf@x!qG132pqbw34E2Ind@{l5* z5HX(FtlA}vaReW?I1Bv_x^=}4!j zU_`Vc;qhsAgh-l)B1KDJ-NDOueFL{d&CIkbVt9J FHz$6&FX>y^UV z!IIel9jwSQGM=Y9QT2iUa&Kj-2&4O%zu59ViwGvfJDibum&R6TW8Uwa09p};5HX{= zG1e&s-p*1AGkd$k1^i;o%qnmJaw|V&aEDl6Q8HamcWn9Jgs`kbSc;!~57C?iOLlUw zY{=~G!NP`@;DrSb-kptv*q4pSwvQv5_l7cAWcqw*$%?jx(2O97Zscz)lZjt=JlP#j zmh;8vS+` *G%ci-0UF5DmccSL}|Gy!g(rFqd(&dvT5TQpy66O{|9@ zOb?4 M2?AcWglGd zG6V~)-MA50SnvSwCxivOK<2WX;iY_n7jm#tl36%41Pc#8(MfvGRy0c41djke{fT4| zz4;3QenCKKF9TL3Nr9K5yM}V=iEi^p!!HgNnsW=w%@AHlA)rTKSO+1E{eQ|WAvQi3 zJi;&d@U&nnQnQKiJwbE7I22&WT0uApawUy=5Y;V@SVfH-I~56z{)MI^qcT!-OU?Zs ztY0 ;Kp=XpPS%VkS;DQ%8ix&Z6TOY+v zP9MRz?r_m~|5~ z22s3W?~ClO-O}MDW0%e0 >x^8SlE8mIHd2Y7mZd^TZ& z@v+UWo4Dmt%o3&jT{4K$SrBhhq5onRjD@f~#l_A?bPuMv5z6E)UdCjya8YcmN4TMh zc>Xbwe1(k;1ME8?Us0tYEL+X;-?@@&e6&e^+7CI^zIwbjAz&545^D$Q#tI>P{Y`gH z94rjthGft~Mpg~;%ZsHkW5Wjq47+uCNj6LsEgntO^7Mx~4Jqwm$+iWvz+Ji%n`J}8 z)yy17EC>9Oo^x6w$*uE~3jsIC%1~I`3uM{T;STJiJk1y{NHl^M8sv)cLYnMO9oe)) zN0PjMjw#T2=)`XGZ9z`p)60+N#pAS{uj|J72-aKK{0nt^S=hSad%eq%mpA&EQcaqp z7f%J5@T4mbBnMCj8V^gpUbC5yZIK929%d9XBB85my!W=8t@EvkDIWF4upF29hOXoz zM&SUq3XkIaQ2Fgcj`K#Xc2KsL49ljRPA0eQq+1Ub#d}=2SFFLdKEPu$)P66rc{f3$ zr9PTau+q9ZQAE)Ncw*h$f`WZ|&esyPCRAh1FGR5X;WIQ7Gyer6AL(ey6}EIAgoRc! zq%{emP4&3n?^~68{Y;aQyclX}R2kX_I2E8*8B4ziDx$gP#22rT6~0W97X!ZagG!T< zBdvCA=RMjBF^5eka7mZaSlKV*N2F?>Gg)AW=M-ZetQHwb$tj7em*mn&O0H$>w?g z_;uQj*7p@9cW0;F`b_K>|4P2bG#%M_{feY-tj!N54tAbUwsz1~xG7ojV#}$4d-HP& z-|%itJPvRPWEM`gOSiaJ %OhOZJ?r$?bF!b}5BEkr~G{E`Q{bG(NuCq(r&e zfoaTy@;%5WfD2*;ReN^fjtQ$~Yy&Y~Eo3`Do2D$pwoybaw|*+?jBSeN=hw$?Z7e`{ zk)xfxFmK;usW3SJKI9Uv=kl(&Q9ec3OH-;|3RKyta=AR0^OCC2)TbU%?Vl#o+m@ fL$#RyebrOl4i2o>!g7!I*1M@r z4b*6%GObL}^HXv ;MCMoKV2D9bgwP0Kyi$fV(QWzoxPSh!-`|F7ETgO2!ou0= z*k7w#1k+btFn#>`W30xEH-%9tJidOL{FvM#L;&H!I5V;jfI7QcU^os9t&+PdI!*WD z*=n-?`uO||?b4HW-L!)0cJlRh|GcMGq}?103~09~y0ikL-?tAsLKloVTcy?4#AZ6V zy&z*#5nv$B_ z@2O^kcSa0Gy6@u#ZCNU(=$2ZxyB?U40x%U+B(>{;pshrpr%EF7upam3EZp2ob;2us zRT~q7EpEApgS?`kV8{06YtahTu*8uuXSWx5k90%od49b;$EnKL4LQIW{1BK}1EaXn z)tX8m>>h34^l!IV67PJ^0r%fp-PGXC+}<*coj;qK7(n1!HJvHG^l6?7xNMAMAfFjT zpTwPh_d>0(vahAt(L6!ZWOBB^^Nm+;f^k)(`=;X71Rjh#Xv0_s7@9&auZ$@Y+AF=# z;Vx&sM(oV&dX`cq=#i<=(=N;XNx}3&6Y0>tS5dR|ndx-}R7NUTvSLu~x`8W|RMwqf zx7;o~x1F)=@@NwUV|E5IG}gbHUcRqN%Ifq?S5Lx*hfV_+<#+rGq z444JVwE?1-j}1stRi?eaiCP#}UKV HYviUjgaJhTClD-~Bw`AC)(amU)VErE;)Cm@QtwVRD$OLSw7oHv(e>{Zh{$;TdO zU!e|O2 m;Ei8`AuH`(e)e|dwJkdsV^EyKAQ zWA~_ F4N~!YN*IDXTM)ppoGd0d^ChoW4 zF1Mkn;2d3q8~jOy1@7SX$~x@6-5O69XGDgbF#qZ-}VHvF0`)7p_BejLHFN;J(0 zMd1=#oth*%R!L%gTTWeM85c5S#9Bthx` 9V}87*yKp_pJ&9w@9!1$}X9;vxmN`T%+nm#ONs*R+M9Tph9^wP8|S*V~$- ztx75@tGbY|$efZW8%kmxIlE;Hes{z(IN4uQL8h+rZK*-&(%pnAEK7~Q4+$ya2mDC5 zzM;xW;OWV@j1z%N=oY29qY7U n&$gpEb*u6lO=f z;N5b%HPu_L?o!G|WMzq{HM*oijcilV9OYDZxn8g08*rP;_G;2$KhQhMNn}*}MY+yg z0iWa(PRC{T#yc@^#_bf7(~{0sF_MYR8LdmCCN9DdhI%Tk=3Tu=g=H;ywUw6SO$<`? zwsIwcS^lE9Sd3Lyg|HA%e{(LzcHWvgrRZB{Q6F;E+bvW^$!RY4Zkc_*lPGx2mJQwV zo(uJ4 c}Y5`l$EjX4A6%N=p%xLS@&R*H(PkiTd$ z;=8>ovuf=}$bE58GWSglhZm$4AN0%%@Q>o^7bE3tmMT*NK{9RAE-(E4hkc`M4Ql2} zH~E&-14J@lZZ@(aEi!l7DUMEZO|~)*Rv}CWRQRB`kJ?wtK6pa2j8b^(m2$A;T!6&c z !>r&Ot*g6Op+sjnrc2I<<_9gM7IHHb9tU4rJ-H?bp}ej{nVc}bSqv@L z78c6RGr)hemr_unS%&b_
loz9Nh*K;yX7O?-GPz8d+eO?{ zIe;k`lmk_)IA*^Tp!CM~jljb8)^`*$TqSx)ZMBH4Jem1YQ7PwHOAQTIM{ER^0~b)P z2_`*nVEJC40;Ux5ihujso|tptR8+*wh#Dv*{H)Scvc5@tYZRQJF*V@ca*^{YwdE>c zm=sn#iQB^ZkW6E-alO2`5b7^de_T7mRj~vbO9aXV7hDL;x5U%nj!S9_%lpuRMG0#b zRC2@hZz+>+c?WL24i&!)hOD~1$q5!otn<`UZ=8hZ<$>#9oZs|^&%5w+q%6NsnlKWn z*n+rLNjWCrmT{a4m!Yns6INRIMX4LEN&xdy3fB3w@>JC|*S|@vA}s|sc9ZTu4Yvn} zCSfIegAYY6_(3F0&>f>>frAB=b9=dtZxpF**`>K}U?Gh@mx>?Nz>>XiSQv->S0cJn zYI{5^#xEbp)?%=8$(M;_18C{3FD+s0PVq+8I;nnF6%*hki`gJ#SR`u{1?31Umn8!d z$r+V_^1``{rxLY~Xb`O;TZzPj b^k4%p_ui&RjhLS+fJG922z3fu ( zLURuxUZQd>^uYFcQ?qJm%;H68OhXGVCFi%-4liW?Xlt$sjma7<(x_REYQ <*cSb{7TwA;tDL;*@SXr@5J}8$SCBJET(nPyya w*aqIBHI_l2h<&Fo5vd~VCmM6rqNIUVxVp}?RpvsT5 zK*1qp^iA}`NV_g*k}~7*IR{H3i)K<*(uY*g $>UeLp2_hOT9eJYyKDSsO*6mHd6xW4vE^|o#xlDrp$usw## zn@ugv g!?fKm0M=*lK4 z83dJ(0&gn6df^ls;TOx}ER!1>#utLbk=Y4Oo|wsmLs^s+YgZIm18GvULxbv|hwD@B zf)|Pm`Q?Nt-j(o+U(y(*esv#vNj^+KC7O%|1mV`vm~QyRmOGM}3WD2gwU%P1)W)%I z?q}=9p=HbS#k4)hb7{)3fWeUCMD~fcHKVmgsztzPnPVfm)5I>nqu4mn+}bWI2Sv*~ zw XiF-c(oDxf61+|x=6VB7MP z{GC(WjBJKf(}@LDR=yE?;m0*X3Fr@28TznP*k|ZwC)19nt1FZ5zAl6?LrW5f8Wb-? zkK3Dl+saG|N%{{ofl@#TQf;WkK1B%&Vvi+!jqJI|_owf4!rI&354dDA(Qzu(n29z6 zc`fL;UupyxS<52>v@Vyn<&S|y%|o>fwv9cR!PMeQ0|i};1f#WG3tRxbH`1q{2hSJU zWq V(X>LJbvEQ^C2K0(Etz$C(*|v-9S7zq7Z<+4?5c z-`7p;;T;sFegTYY5GStcXsr;mN=urXxjpz?np-u5a+&y4Tk=A~8}RP!;RAaR%p6r_ z0fQ{rX*bhue`Z>||La96oD+b7wpH&hYoyFw& znu*p2rtD$b!A*)}blrHG-3($ !tDU1qqvt=XgR zN~Ym{Qcre~rtdV7-?e&k@)0#GdIuE}jECbITMbO(Ypb#ClVR-Wz(+&bpj_IbF?GG9 z$vUjxh`1Y1wz~OXWltNxOq=bUVEiW6>v_qI$TGkqC+yOyE}&T?#t5qmo*|G;d2CTG z?;2yI_DP*tpfLROsVt}kP+pXc1X|#-ptL25oUOW};Yd?6lWIy&f$U??<-2%9E8}|o zOVo(BQ^Ipn2@qY}!bAltRF#yts0$E76s)6!^KJ`Bf$P$CY~Tdh_d8RR9F2@QFc-fV zsu<%Ile~nxpytG+%P9JIr*sh)iQ9u5t|f*jy6ul^GGf$^N5VQ6NOKNuE$f;kQI+H! zB~0t>Heczx$W+H86-t7XMb^5wqq>CSmJ2eEHR&hSe~>zhDAnVT=P|s%bwrD2>YuL1 z9fuljs3e|>^7M>KAT#QWM%}7tg}u#eZ4pagsE+c)X$}(U>OBHsNW81Q>7yEvI80$( zx(3mWV8vLRD(+^7wCDOEzHqIY=7J*IyRx9NOMG9s_K(9Uw=+hd>bPt;mA9&kR25y> ze%-<6+f=38!b!>xsh3LTY6!XOoNme)Gs@6$Lc-RMZsYf_h#6S4HK?der>y#nn~HZx zY;nN7v iquVYA%67 zK}Na0Yy~OzB)a1cZ*_gjs0xZZVcE2vT`QBeU^%=KFWrS?Fs3?q5%e$+gQRC`(OHbW z*b>*6a*NdDsmu@VNXp%aib5(ur4>uI+AWu1S?PdMyOQ1&yg}bL4_6_=d16)8bj(UF zx}tVAmZIj;JFJ+;Rw!_d93Co?rr3o_KaNZ3MD{y&jg2$_bl0eZ>b3JfpXB2ZHgg3h zg&7Oh&McDDuw+}x;7@lkw^z%gq2qUb$CWx `mRIs$5&w03EbDu3kmm3Ktfz4F`u>pZ5dl7z66^Mkmk+Mpi6| zD@v3xbBW}VT1^|E3PXWKXzAWpf)=!xg`9@CV?3!#dPNQCR@1vuwq+0E8Er`WE)!D~ zHwvVBDJj*18@rGqFCZ0I{w9S_Ubz%BbD3FmV@uj#&;S`Di{ZK6_j)`Vzj6Ff=0y$g zmvQwGPh^W&T2jf*tvzU9dE(_pTsOg$)6W1mskEsYu=&+kx)3^keA69uz+*EV@43Oe zYS~M6Hn}tEPhl6S>x(JPE||$;f#sg#Ghi282#ry~Tk!G`L4fCOxJq#mL25>lkVD}| zDebiNVn%Ej+OJi!6^vdpTWB1ONzeme)pJN}!kb$M=64?uevkUA-?`_NmRMw;e$9aJ z7l%?BB8yC3q9YdH>5}Qp!ot1)74q7C&m(wIqfVl_yscE1P>uFgCczG9XVn*LKU+5L z^vlgDE;I@1tVnH7Yc3wN(ptMh1a*PN`s>YM%mA4}C;Q}kqnVM=*%0B)Y|S>fJycLv z(z1<&6k%b#0A6QB#iLE3+ly^w?O;)D3w~Udjd=HMi4Mm1ZXNba2PBKK9rylXKi;}z za|!{nuzGQ}B`>ioxVUW H(yuhzV{!~CSEun0JK7a (XK^_0vUK085{Sl^Sa;kv6bX zO`!()L&!Bkw|7CsB^OKDM+ha{nTDI;{VE@vN8BqMTEAN2uSfjd-ySq7u7X@!UcR)lN2h^OW+N>~fEi7ymr6>X4 zWyAHQfT*)$X*YJME|VJc`|xz+SU( QMkDqPLlFfOcnesoL!nGxQX z_tW?19DgI*`o2rSh4f*7VdBW|?b%E@M15-b;1^s9w?w0gsNHMJG>GXLmrWPf&j6uo zCtq`7+YT{Pr^qePrZHjqc7hrZ>ni%pm%1d^T5`;^qT`Lt@7p0a%}YT+^w}@y7+GV& zc%X{IbUW31BI^}mwT$G?2kI9d5iA{Q`^a3!)HTWA_aFE>(oCICWOVZr)@m|MW27L= zH&Q(c<6GVj@DyCNvhEt*>Bcf2R3~!D3#kpLc-r2&O7+r&=kiw *o@mh)R#SZUp|e&FI0E}SuL zbf3#xs581iMQ^6ZLE!ck3L=3YAdl4;Nk(V8dS1o%c5>8^Gyr9nD}e8@Hn73qM@qBb zzjv=Aw(p-PjnJArfEp; EE&4D^G9_bW9uCb7 &Jb%kDg4|Csh zSsLCksX;sbdKDdCRjdPNFB)A`0K>MWjgGhdU>^r0Dw{@Du !l)Eg*P=!Z2s$;28Hg&?b%aB;X 3a6n1x*GRNTCTOT0hFw?zi1G>2R$kpEQK7I zkg@T%?)@;zFC3yZKGI~OvunqHFTSNN2Sn8P#DFj2>n2&kOQ*8FJhkz%3uHN{ywzn4 zTqOanIJuQX?rgQ-w1P@)T;`IGsM-=(4Qr&)<;mA9hk`S77}$b-3#R7gvKXEsO>uYK zV#gzU*Q~3v@=#q9xKtPLLUnL&-Pk1Xszv?2x?8x!L04FxjX+V?31`)883?uoTvxNv zLM&r0uhLi7eA}1Y5dA{%69!nn6*%G~)?3S;F;Yu !I6I-imJCE~ z`~71JW{JpjyAZ1@f>!N8UgMJdg39VPdEN({L3=8+%SpjT(1x%)^mx_gY`8+#zms-H zf zv5_w=4%^v^{e6qoRkW}6O;Yv>$L3`Q2?NcERMAX zxROucvItj0am?`*SnUQCK|#s4=_~cMG%d|%pgG4c!-g?$ kKd+I zr)LC8x;M9FijD_nO?3i@ol?W`%x^m|I6ml0Q9i#3ie!^WLhYmHZ`0?;N7{%h5LnPG zTvomsLtXF~)%8)av(=#&Q4*>Jm_&9zkjV`^KR %`BtpGU+x6|+{-v>n$?7*c`uwi7b}m92-cnYLn_F&LZv*-v0+j3N zr3U?vw&!+1zQS2zfot$JX2QVaR#uF1UjpT PSv47nW{ziMV-Gz6ut@^|3uXBd9s zBdSM|0Fww*6A0(5o?j$yrsq`f)lKBihwBGYpBFV`C{9fD2*Liz4OI!*`TeT~*TTY> z*Lz8Db)0*6@613n+z06S@tHg>Gi_VFlzU0RZJ^xhB&M43C9w5j%GPbGU>UITC)k>9 z`4(Iggyu5&t=twzZtX2J=gAY@x!|t`Uu8+N{)p_tETW8R?*c0;?yYoSl_MEJYTvCu z_J-s30!mtI8P`7t>^tgWsoV$NMUXoYxFdW41mDZI;1Ah5(FQ~EL|&Q4{R_DBzML#C zWj8Y0(<7(uAr=s@yDG_ Ppv0=9>G|D~F@U_RglgU?WE_}M6eZ+TZGGd#rG=+s RqV6=Z`><7)~&;xiB#-EQvvBQzu#(Yldh7xPW3xm(YNZmgM-i;uZ zTWG!;PmK)IgA^jwgUnCS_6G7~hBOvJuv5Z^-Q=bAoYYQBEW=7;hb4h8wo{fmM9B#6 zB0uqI>Sodc)}CxPhuS?#3#4Os(H3hBDB8_A!$&jfX2lYb7KrSRdx}MceXa#2wF5~V z$oLOoiF*T2<*C|DY0Q)9i7vHhx3uI>6Xfe=>}l||Osq_219os%`zzLIa4ZeTovADh zXDqc-Z%-wDiCdg6r0WqcB$zsqGjVgP*io?uMy9}G-Pp&=bfzRuWD>Mx&5(IgPhVO) z!cINJo_gId?7UcD?FVXIlLq+o&$o +72*onv3T^d^!Yt*!MT-h)x z%P|L)XE6ti?rRu2ZW-48O&i(jb&ZoIajF}^5~qRcanh{;#H&Ovft^%uxSJM<2Uu<9 z+s@`)WRV9`g9cjtR1k@M=-L+qt99okUg6YAz* z?3u@@1Hwyc8JICS-@Lr&aF*!?X f^RZ>dm~WPf1l5 Xn{v3%%QJlH+zg<=H!~9qztN9G)J)tj; z_3DV{R(X*W__4x!<@jfl>m=E2b#gr56x8}q7rR>Ta2VB5;{%dojucTM497!J%?lif zuId&rPQa!XF9DE*m-%XTm~D24*&)|?o@`64x8;00E9RSBlFe86c9pDVtKDiw=bmOo zj-;1t3OrOIl@_~vbxbyR%7$+DMEBbvEc4Z|+$1@Elk3CZMXt9M;$^nVi(Qc+T62BO z=^RAHY=c8s{jHDXyuf?lrz3uOsPWoOr2`w&YhjO<*juRcJU{5|4(BFWZT0N1)dg|J zR__YEIWoU&`R6plGYA$slQwZej=;AE`HQqbupFutGtLeN^hbfjpH%pDIbS6>^x5n{ z{8A;e!$!}F6+UEzgoSYHVpf!V<0DU2g7}-^dtKv=4uSpLD4PH;PT}CJ;y7k_Z=C0R zn;*U-97&qiNIKiqiDB8yOZ?f;qjt%BmFM{?Uoj^=X}uxL?h?8Y!Ga% zY*sZBffqREvx1R|^UN{~$&Fx40vbCef|p$)LM+)ZVKQFmufxlHRWe@mW`k$V=R{*T zwIn4D1?|yAG?vgEjzo9`(HQ>mupCz?K^%<{h0#bnR^rAnVZ3;YfcRy*-3qVn2pXC( zx@VpwTVnQO;$XoKIZizN%vSgv4hF|~BpGn{8!wohN`VF07|%Gal0R^?^Y6?rc#FRe z$6_ 9S<*45B)=sQX5!sCTbNVeO$ zO7v+~<6NPLJtev#HbF^=vNS&+ET;tN4w?M8!oi(RdbiOv(ld$BdPSR@p{Uk)Im%=@ zUFRp1- u+%dYt|;(#Clve%F~@d=0=mYtlVfopenGWBkH_cE35Uo<30_~} zr{l>Rb^$1sUmEYWF^7^_sP8(u%)eO`dhnIRLzfnVcXKM9?^+b?Gul=mj M zByHeL!U#_4UHQPra`u>*$>M0G?dXBGY{giLeb}ObBDKHb{&a^ j0 zNKND2QXEub2WVn9X{n7L+EZM-Cw3O)o&9SBKI1}W&A-(7KbBvdx5*P6d;i4*GaWk^ zblL Zj4ADW4&*qec;3cBQq`6#oiio{xgUi zb9mwVm@tGD1f-h4VVfVd4x&^+cOB7W*87W_aFliT2{HVzCS1v5HGw-S<8>$5Fp7oW zTggse26)lB2~(uV*1|WbJ|2&?kEs|LPLk9$sTu7@l%~#Us!9E!PK_mgkh-X?m7Hm; zgWzXueQ6)aae-ZH|2FXy7Xoqtf!Ku*W9M=={Nf3N9}>7R^%1S0Aizt%Q6bbdpA&oZ zCHC4admXtOr*^JZ?9C2`-!M32@zTWGQV)|0le!Q=+5{tF?Y@Xxa;)R`H9{WE&7>`x zoNJH_DHk$JbYZ_s?R{K)-FbdNfY%d!8tWzw8U*@8EW}NiH}%o^&{SrA3FERBilU%) zV>s8Oo6sR4U}z845)aZ>Y=WXaUpQ#)@o<7)<2j6%87n?g>ZM@J#7)8=4M5R8z~)0% zapNAQ&0tf1auCJ|P31V`my~#HPVQiRKx=4wJj}Ob|DpwTtk40gSMyD+x9D3Q>xA(_ z-=hPC=AfRPwmL^sW_v=zf5zr pK8mmnVKl14hnDd^bWfd2(}VYDz_W+!%&lg-~uD|7Tj>f`(` zGL8#$@^)<4C*&<9dizJYVe5QOaG=+e5G?2f&oD6S`AEV>vnV |co(IJiJ|Kp8=LC-8Wi zp?VdN24+hLZrZsBBFjuJ^)f$ItEwdbwnWCMPX$Afmw33SD+F?bNIOaBX*ECX4(#$F zUaBqOCD~SWf(}D*)Z4NqSjwZWs)G(VzBS-r{5nXDdWKFt23jPSwpc~Z7rf|3IU_x+ zDw=Af{MD|+v9be|3ytMPF*{mEm@!p5_qobvnj#*%4SP~Vm^sC~=6HO!)q$F8D5FbR zEifBrzJ BfaoM)1{rx#+phk-(C z&o>!r)5;9e`@7h=GeI_@#qep9`|p0!h=QiPBGzry5== nNVspx1^nvExGZwRFWT-`(v zybste8}LOYsSg`!pLp~i( pr7?4=r%S(M3N9;Ygv~`9Rj4nADH?=%2`>3131s4~I<0k0SaB+aJIJD~mV^#1H z#BjVr76 R7@WmADa~ z@(ihmX@WEEeoY-#i@%!ChZ8v7@NEjV0zWo%XT$moJ4JCzv-@I@pkycZ$ZmjZ>M)we zr(AdOY1N1vn|bHm%|LaeiQ`szppjEtEJnw!0t$F*%}@HT-*q~UlIuVRzKeC|#F(l@ zL-6ST=hnf-A9SB|;7-cKT-BTU+FphQjMQ2yS9JXmZ^Fvt^H%_$A!W%@7A;5ZcfgHk z 2SA`~5f|YW1J}L_nkyf>8h;0E>cw9Ui3U z-Y>BJ+?SBU2dq*;(%TOlo%R|(d@?Zac+m9h%JkliXx$4fu-wBx1j;)Wq@7tZWMF_+ zWn&u?MdlxXMV3(fTH}l^ISs&KX!*V|AGdz*GQ+%M)}|xFQsyD-oDlR>r?${W{Z>1H zt3?x}px{0I28R!tP>eAsysQUaq{lU=&xS>D$Gw&WIxjq_Y30byC($5!P0&r5G%9>D z!%!o-ckJ&zER*3aj{A2}4i=-L&(-?8w2jDPs;*_3(Y+6#2=^B8r@L|i!y5xckNeLh zGqNxC>bf}t1;hR76xWIQaga?yNl zn{IBfuV8weknw#|`m}eGrqCDoE~q7j^Jm&J(CFuBbd_BOgHV&sS9k5zt-Lq0+pU8Z zTZ^OcX`Iqt;d_-WtA_V+hbb=$jhf9kNNP=Q9bS+n_sU5!zpsOGmZ$sIJ9OPS1M>~N zRoLM#ccL{ERoIY$F!0bU5*OcWintEs80`=LslHh7ZmO>0f72|14i;UTeH^)6$`yC+ z+Dq^8)WTx8uX_5A82@O3Ox4*oVb-Qn(>^J(EB!?o?mECL-5CdDAg8ulzjes(J|8Rc zX&;%(JOeP) qXudH`p71*?N$Qa9vezM+)kdqv3(-|HGMc5AGvXrLTRE} z{WqX>sCQuSoZ)6Nzy?jwQ_Tsh!2dvoG0OEHYLNPEI-NxO>15EfmyYm#w@ WrmfDa7?hf~FPG^!CU8 zdA}c#>LM=8XIuSUo|#_k+YBb^>>)zr_T!d_Y(x%%SOXnc)TfDL$48w_HTsY%b=wUe zp-+5!JDH66Hso}Q78>5x@;A9Z8h>olK4chl?rqg+A_y;2dNnYrftX+9W%5Tx|DbPo z1wQaTXcG$krst$#dax)Eml4iT01V<~plQvxB)fL!bMWji;Fv*MbO&SNnT8u6Q*x}$ zmh`z(y`Ej_eav&V-!%-o!PDrRT*e_yp^X?LdqJo(2C0k5cIEU3EBwwua?I4Zu5n6r z_t>F1H)A_Z*p8Y;MPu%Mn}kN0f8d X#d!{{qRWu=lK?Hg#qUy*(cWxF^@S z44WFmG7Q-sEqZ~W9kA3zxKv0s`>^F6H$w% 4Yd^wSw>C1$dLa0 z!v81jai(8n*&csy{xn}&U;jz}zECa0pAbbp$n`1Fq%qTe+UyGkldm80dK?(S$H?Pz z%tJHx=wq2`^7wV~!La G;8Y!tkPWj>lVby#TlMt#`P~R0456O&kdVsA zkNhM#2EWF~zi37iFaV#27YCBMVq{}Z$fHuDBA_SE%?IP zjn1A6_-Q)n^y(j{WD5U(lCE@7s!_{iI4RNVuD(~UiS{KiV4@!%Nn_9% znz|I_9@wp=dkQ-!<~qsodAtwTn*U?O;~Z84*KEIzF5&0G(g;m}_j@i}?CBB|m*t`P zG&@=L!*DPt)rxhDw~mLd;xIlB-!Nh{kX3+>*89*^+g(Qx;EH@ecn+l|TYH?S{{dd2 zsGGbBQyc1>mTZOI=_Y&Gn(aJ!s!!n;Xp&RmE-StCPc*1opi6YXJY$1jXGHo(;-zb= zGeMa+7CI-IS7R}y3vP^*j$Nwi`3zmju|qB~yXAH-QZ!?Gqxh{c-ep} h zgT`zO@#dK4S-z#(Q(8Hf)H%NDgNaQvU}wQa%Xr_9z;}e+C-{~01RDH$y R-ooCeH3RFsmv!UAUF@vlf)t{bSrP9L7H{#BG zdwLy#<@I`g(fKJ^#+=GT)GP@pl&DI7Zpw9Gt_|lwOGFS-_NJZ32;YbLW%1uUTP~OD z^&l|2>{?jpm97s-E)vTajl;JUfcYlL3+NkR;B2i+lDbK_w)8C*QMQJQl81!PvK)eC z?O|DzSHfb=TZBb6TxM9-#bDTJ(#!isgZAovK$_7athgOc-mQ_Emk7_(5G>1WLvH_f zDLzK9tYb^{se>g-^HqbFke1pB?NM1Rz(CJ}QCw-<8Sd#+*6Zcd>mV$z<6>YGFOkPf zG=P_IP|tA1T1~3*B)?g8>O;8JIOLbz@;g$`;<(HPVet@t%QvrMuqf!mlJfA+;f2k| zanmt4kqSX 3ucGD`Z$E6r7^kBhPxn94b&O}{fkCW1|04x^#c1v35^-N5ugO``+EMB;z z=1ukD`7lr@B(5ft8JAHYQof02(1s=ZKY?ZW )5RH}PIzys69q}#F-a2nM<{fy+i?IwQ408*Nx|*%b4s>)Szj7p?Y =a3TJx zbrXhl-n@flThkl&8Pm*#HLk#oCO57lP%x zb6W^6!bRRRTl`%1VF~cEjBS`T^~1kekK!d gG7=zo_vwt?+)lus!BH%w_rdm32#wKV&i!v7t~yDf}vO z>@{z^vq5_-b33McSfcc}8pR7*%D imO*o%k?>zzaTrJc3o>7hOSZ={G{)9wTe3Zdrp>Fx z3%Q@KSNO ukw%g(@`9S)if)Ut52_%(Knw zdAw-5w{5|&P+zQqc>e{4Jw|n5M`*7@;R=31yttqxA%+a)EkQ_domfFGexh-?_(rh& zmwD%`4a2K^5gAEd8l|{5B;#dx6Y(G;sIDk8f%E)QHTD(9Ft=6CMgZj+N`VDY@ O}Pu5z+W|+r2N_;qTIO07aH1xNP5% z&rh!ZD@Otrt&@lG!iH*1*`#$*=#s+9h+jPhiyG}8a%EkB>4LJL5#vY~3m*MKK%w#D z&%(k)puCdMUUXnlp?<0JjKcc7omp}e_cT8h%NO35d$8QdjB|@WRgF46Exeq{Q-#We z0)Az6!ar4oW3Q?j-=6GeJdU672wI_abw 6eTz6His*Aanyd^a7)0p^xvXnmNZWRb6lEExzg_{yFmRExoDhBfhr$7r&*ijx`@2 z*OdhJwyusiKkFsU3tqm>2>zGB(E%;1cgYpFr0!uT&e6&6E#yUSURe;lQV6qLESfJk zb-AFt#iI05xnn0Q+KSWl<*sms+=m1|yujFO@40i%Xw6xZYf;=Qjs-{q)cHK}tN#_E z505P5&1l@xP%~@aA=7sGkfxh@bWu4(-3Cx_mfVzGGRaB_KDchYRWy|HDj{l{Y0`UI z*~F{0Oq#}KWUZeH3z^PEonR6@N`Ful7TtVs-32sh66qL7MJx<~;X0DB)_a}_rp; B4O>=~VVd`Xuz#cjh(H)ru#Yk4~E z<2h@q*JkKkh0K2c$k^ewq+mY0eoO^vw~_jZb2`L~$6q@dRPH0_k(GuYikEj;hQpCn zSr2+kT_ZQn|vamPp&nv+kbG1UR3JGkD$ zJmZ!BRs5%n?I3DCavdMMgS^xzQvp+%g5KiyGbwXP5&Sdy<$a!SP{0qPxul_EAIvZC z&T;oClEm$PBpy4q_|;m>{{&v(%;7;hYbxU-{n1-7(gh5Lb4kAq3;Y^U4b8MD!`PTJ z{rNg!v(Ko5{cU)pU!swXC!CFX*+*9YI^Gv2hPoi|?i0rLHNK=}KNuGHFi=P=8{Ynd h`XF3g!SGSA{QsbyRV(DnbY%bl002ovPDHLkV1gP{Y#9Im literal 0 HcmV?d00001 diff --git a/docs/dl-cv/img/04d8485e-8078-4516-8203-27c99d3f2e66.png b/docs/dl-cv/img/04d8485e-8078-4516-8203-27c99d3f2e66.png new file mode 100644 index 0000000000000000000000000000000000000000..176f726f6f281dec19010e7d24052cdcaaf9b6a0 GIT binary patch literal 5718 zcmV-c7OClpP) RC)a&+ySeU@X6LFCbgnqNlB;o&@bAB>2DDKS zlARw>sw4*S2l}zP`&(tv7{(CI+nYglz$SPbdcNn4d}4FN<~R2@*K-%{^ZaHt3^t?r z{QpLyXR>*OKYQc!dFa&^{+x}G!CjGip<=`# xL)USQ`hL(B|IWY#g?+H^2rrIIwMNg8#f>4zhup4m@=B*(n~M z?bzh`lat=t=_71r@G@V_jgvmX)$BEo;T$dkhN|$?!`?8{0VJ^e?pUdTY_5<^YH#ih zU}HZT1)CY%?HE^veP`q23pV!5dCGuI&WuNg*(>n96+9w^r`oTW4GiBnh65V!G#kTt zZy_7!u4LorYz#ZEv3Y}jW89Z)97FZRsc}7fbPP+eG4Gw%&IYv&qJisE53^I%evl0` z1Z(Fv*v^m*e0t}-#zx&R?n*XyhU#zD_(VWBLpJc(*Ul!~$>R4t`eJ0W!Ieh``&-o) zD>kO;gWv4fz>V?7a9F|SzVsV>5$uaU=bPW)shd~MrmvX{m)*KyxN1?W0A_>t*B*~o z$nFXL?*@B(5I%CWG4?Nasb-$K)46YT!gT2Ih||_g?cXR^BO@atBO@atBO|+Dv;HCm zZT6Oa%4%oh6N6S`grL=CgrL=^FD5p`=G)pIgrL>_mk_ktgb=ivAOytb1;z B^ ztrijjVnb|*&5vTkh(Rlx88K*OLxFfZ8w$jU%}dz4CkDjk1#EsN2CZxYVnA$Oz=nSG zZD&Kp0>p;c5F29i3fM4$(8^{;5L(&%MG#uqqy(Xr4F%%F<|S qOO}qA^1Wp^9kP PFSsW0X*1q_6V$))l{GAdwE%wF_D1mc|&0qV_b5_k^Z4odt<`o+}j|t9# zo~m1~`b-o*Q2YI%); ?cf8Q0&xb+v)FAEzrs)plX3r zZ2lk)jcg)8hkLYG9{n>VHqCa8!6vb3_8VezicLgpTG)s%p$eR4yAxbe1x};=5(Ius z6*!Id)(-+n37kgzRRn?(I4$<&K?q{gY}1D*_(E)&*?f(HDJ5_kErLM&3$bY)!+{OW z*U>VDZ+Y-1C2(5&hX3;~1yqfeK(P5Yv1v4UO@VmpI0bB`G*NFWn`laGn%P9*zh5YU z)69nZk|ua=GBdvMgTE<()2MI!X%Iw|z-h8Kz()K@lZrN(6;A`{|4VEd`(jMs{7r0{ z*?gS_|4@>m$)+6GOzDg{jef(oVGvG z{Y$y rf&a z@5A2+CnhMR{UUg9!wWWGL`S!Ti;w<}jfc^L7f`QL=$k-bk^(#8t)de|M5l_UFoDB` z!cxHT{skxU oUVM*x8$F>Pt5p7c7#Bjqhm& zfIiIKx|wHwsoQAOr_;EV5&!DRz*9j3HmvKyhx~Z(eGa${-*W7XwKi4R!1;jN1ltId z@pg}7t0Ce8# !{m{sxe#SjVoA~1oY*c6RQ`OLOjSG~{9g9bLNzvEnB z0%tn)4o0fBTO7vqp~T?=6FB} n!x>!W~#*@{j z{IGC;hOzH_ITXh8`OL6RvQd5`Bmc&iniJH9YsSnb!vG7fiv=9CxIQbtv4PF$ET@z_ z`1m#Qho^@v=x gBg*` zJ`&!FWuy`~rQ#d-uTls7jpQP_(PIWld@45JHqi|i^>AuNV8CKb;CyV*ej=We-@rii zu|*g2Ve>i3=AwUrS 3juHxNlZ3{Mr?Y?+-tY4I;Y z4O9<)rTT`$CR=ypi;e%0Tu$XzO&9wVG?aFWx)=|I8Q8B3J=sKDHfujF7lURKonTW{ z1GocalSoW%T}OSApYgyb$pi**eiMcL<(`y_%m1|jf8mc~7^uRVF#HcmVkM1BVO*3& zXy_O&crlII6{{)@cFW=IZ@K724vh8qX7OXFoT&FPoV!1%xMZJ5H^0v=%!XH1y12W) zNp1tlKmNB&IxGJ!%Tz8sQfvmLiu)MGQ=t7m`a4m)ASk8%IuS@G{92QI6f_k54X}ZM z>ftz(PEo_{1S`|Mht}ef0W-Q4Wpe9u-NXgDQMT@^y^dqh#r&avdC7&;mzb&1RePOC zj2Xn&=hv2SRb7Ho_$*z@T6fl7Lp+6b6x(OZ&3Uz)qm4$)PEY2Gv0!=;L023~Puq54 z*;iYafl~2=E;~3 zM3Uc7@lE46r4l%UFD7n`8Df>dX;3nQFTlB-w6NJ#5)U_MNG7QL_{TnC4->`iu=*R+ z#UG!QAy$FI!o6fps1TDF(>L;F`_j5}3?5WAy|kj*WVEx!nv=r{lkYr*iOF74du_y0 zsszqvP(6v;D+Y`cp>%QnL}T_5n@FH8#(Z&}KasGhvB5!HmU9DL=Xo|GCY8w|X3%O( z08qYN6B#@PI!-tyDe6Z-=LjepcoT~x9?_s`O#Fr+I#-shF7JbzF0(rNk4W6!s)0&b z=zF1eTsClBAj&^vmlI;$<(oY3U=v3eGepX3N-F+IwO4^EZ2OdD*;$4XZ#MUz+wWyA zV^Mr71((r_YfQZSg|-DL{w4cCwyG=fEK3cCZ9aGS%_33>oS)U|nfpT)cC3qYqmxaU z+!~&&$&LF?HX^xI`C?xTs)WMX7@d9b?F6)6#LFY429@eFJM>!=Vaxz*!d*EirPFuX zuSnovCdS`O8%{+g$VPR=Rqb`CiyMDnb8{TAnItN?rRly9?f6)``0Nk0dp;XkB)2eF z@cT_Ufz=c4AlTfZ+sK#C&{1TJ)hr#ceq^F~)?C#5eiInPL&?8&5y$JUG3ilz%`gs+ z;{O^m6tjYuMtz1q_PXZ#Q}Yp)SAE-*kTQYpW_U3}A2uQ>^TnJFwmV?M%a}n8QY4D& zF++S?< frbuq-esdrKRq6G`X+5*ErVZE>$*qEi zkDlXKq1D0vvVLNI8KHx!g=8K5O(N8xcZX`X{MEs4t-KaR(Zt9-PgXV?cg}mwZznOA zQ8go9qWzB(TpBGd{^Gdx5v?|w_p HKGQF{&jjl?>NaCoY(&p0)~n1Rzg*G+yifnYP@zOunh zQQYM>w?$4AmETVNabkpVIIu}L2I9(b!ZK`7 9MKX(VeQOPYDv77oE9^c9&Boa-2 zQzW+_W+2ITq>H0OE-;r4Mu#IF`c0w^gu%fY Xu!FArsHrY3^Zb3Qi`g_Cg~iM?J2|^E>f+?Ny7&U3 zC8pX`2OBOX@SP7#7o#`Z;5W-ozqyq|j2U|L#o#x$NitMjJntB%1Sn&U{i;v8_|R_z z=KZRHD5OMj&2LZ_^N?54m@7m>IpN=Fzu|jbtn RiM;k?!0bx$}|w z=k8Z<0>4pl4V&he?4Iuy#iC4qkBVvYM&~iVUw7?wQDY)7ffF67MrEp6gw;0Ht-4RU zTcxaD-z?Zi;Z)E5CXor{(p&nQ+c+lG*Wx#~3yHm~pu+28zmZ8K)kst%V-|#H?>BM* z+7CRZ3>wtP98*Glr;E8?Q#CtN>CcOl#TK(OBkRcDU~)_DG#&Old@qt!+@RW;Q0KlL zU5YNoe6cl3*Ij-S$vD*DR9z3}N-WnM>A-|`!f>Vh&PEqo%vjpS^0Y4I6Nso5iK?Ym z< +l;6x)_IR3LB362D=-<%Ol`P@*VmO zXg{ugXs8rlJkT`gQGXMaF@pxD)GqOmQlQO#BclYxuo8P=nG!v;vU*}rwb$6+;1A-y z0<%NW3`hr6h~bfYnVwbYV)&?u{MHu7@d=Gwq@(BVPt?U-y0*v{M=*$Adi$syk=S$I zYvng0i43yl{zfb)m);gnO+enzDZ?A}4Nhf3s<=7tCn`2oE}c9`^BXRkGJ5gQYp-!) z=Lt`$3V_C}=S5ae^vI=imE4lNzlo}F;;bdHnXxhO$xizX9 H=i(RuLU#u*a!(xLTZL_%n9Zt93h~bSw*rq!AVmt*wG|-q~ zW{iwZzmXAD6}R;_#TZUUBR2U>T^9>VrSJ6(b~wC65zmK9snEqD=_&EzGd^SZ%knpv z+ x6;-vao$0=EsPf*}c_Zy+67SL48u+D_tiHBRvt=;BD9prbh8?yQbS#QNfn+Uvq^7SEr*Z+CSxvERum z>{p_URqgf4K3#iV^fy0V`?XET^4)~EZn(w9`6xQ_#W+s!0qSCl;S4ydZhbRUT|Da6 z#dPYw6XTSiba87uCF)1okrQdUc!GlEFaf$4ct*V(rzS FuL99I-rCqS<@iYt^U**3Fvz z`x`#M8k636KTfKz)o& O< `y9tByH`3=O(#~Gch!64XrFynp0z8S-w%&xShliZ6`LOK(|pnU zG}@u}_&&@ReII>yWie)rTXX5d^&4EVPu5-!+i#4Mwb#S;8#1!*tP=NbHcqjl(x>Og zEg5!QJIfe|CnM9U5b#c0@7v&h!_F*ii@n0r=-S#`MRsb~%Vp=TjQXx@XiDo^H@bko zRNoc08hd5w(;~s0%URcHY72An)VRux{P8Mh)}ww;YNKe>RIimT0M=T6ceAxIHqumo zzxbi_Q$Z}P8FX8=_U_hjBem`Nt_-YZsPD?qD?iNia937_CP^L#X<$0Q$kMmMOec zX7_Rw=W1i*PPWN&Cv_eTxBiB0HrKyh8{Y7GrELKW@P^l{#>N_3+7<*gpV=e*g;R4q zv*-0)S?O|K-xW&_cD00EX&-zHn9a4eD|4-0OCNNQgl#W$#)iqVaj&^juWVprzc-!? zXLk3fK=0l#eBErZ{~GL>t _HwShumzW~eYh)Q zS>F}3je}j;%QPL=*sRQTdeER#d%mu 3Ks)50a9I&zVT`3nev@NBJ9_-3k>n~iescl&ouaVVz7pCtD z{RW?z_F;omoHRF_&6_-D+LyT+Lr=`sHrv1K3fyz9f0?V*a%t84#!XiSxWqo@A7k6F zZOv~odz9Aw26qKa4|X+MX}f|g%dtJv{RVtA16NuZweJD^M%$8FtMx+n8`v^RGi_HV z`{Mh}=X{*!*ZIbHG{$ 39~ZHt|)m%0 ;}qPV+G~BNfLrOfc77Ug z6JVonSz^7=?DVe2&{pePu>Gd>QOR0onU(4Njm5f8r(V9T+}zl?o#%Io4H;RVtk;MR zI&3j@A`1F7p~>92noXEIr=dX=v&hZqx4MQF<&H`?#G+>Z4=>2_oV92EN&o-=07*qo IM6N<$f@XwJbN~PV literal 0 HcmV?d00001 diff --git a/docs/dl-cv/img/04f54858-0741-427c-9c36-4c627f70ba34.png b/docs/dl-cv/img/04f54858-0741-427c-9c36-4c627f70ba34.png new file mode 100644 index 0000000000000000000000000000000000000000..940cb9d52f13e33a8588bfc4ebd0981a5d89f384 GIT binary patch literal 11293 zcmZ{KXHZjJxV3 Z?+O=zd7tb{eu3e)9 zUAsond!6cPN6c@)^lFjO*EQ6 ?4WR}MVU#N`CyB#67?pKkd|!CZ>T4aC#P{=%_Lu$}nW*+2)`{5#t&ElKx> z$!~U4q2#b@KPxcrKAdNO1yvw{D8<(Al6z1Ri2h*k%0(bMGdO}nd{zn(uX9iy{puR` z6wDVEpKvK_Ex_*l-{rX vVC7G*>mg;FNCyDZ^yM_!Fi(Y9~A&^JW%G{}I` z;%JMP3)S+ae~XD!e6pDJb%UI>Pp=dVZaxydV;T_{R3MwnaD8X&pbWZ$e;yH@VEKKJ zFZ3JT7FVBADZ>FWWXMGQH-iwi(yEcQdmN+^1r}>GrDYp^GS+(Iw5w k0NViD^H{WNzoT1gKT~`6Kuhp=oQ-QnawpQ3lis-5I4Ax;u zD6>zZiO0AMkK0&mbrStek-6A_XIR{d>Ye-bFs_JO!$sj^-$<;W8;FbQimrd6c~P!V zmGZ4{UE%xk!xOxWi%O;I_=Ygr#Yr6 dT)s~*S&{T%-*;1)*jBL;mpes{E|=Ul!KR+!XTfsgZj-j<3AuhD|z)wFgqL| zO-wmmSX@*AE&OSVU4x%MAcxJjckWL~G8D2RN(iF DW3AgV7ma geXT^+bJ*-JJ0+njyTYSdL*VTaZQD<=wyzpx7h&nb_`2 zY@PN6spId$R@*cDqwt()f(zeZaWME{VLt&GLRC-pmjbcX#98kmi82Ea^gh16vhq@` z?uQT2lSTbq&5fK0K#^I^%&wlpB%0qRFzm+W#wEu?OP2R5X@Q%y1Pi8(ANQTMFI0=2 z3Y3Dpy#dHXEn-zZx4_;R#wz74PJX!m@P2HYeD}DON66_e7c6D|qs}}guKYz8BsiB% z!PuW)_A74^)qkfGNG#pKrtCrLJHN_jENaq{_YH9_ NUX6E$VATmub;QMLp2%~+fc _Yg>kGT}&i> z8(vPQYswgpI^Am_^SzMS{=&3lNS@2Q4f?!It!Ybrs`Z+(^d915!^)$jO^S`zEd4Gb zICfMRbo1<$L6LWCqNRr`c1P#3OP@TONds!5!aP|#mRYz|jMNQVYDfL}l^E%&7X)F+ zVMOTvnQY${Bvy@SV4pa$3wMvQ@g8~yKvk8@l0!U%iF7w`IV8*R;8OALQQ7^Jz)oRG z1{2&_NU3<(d_rXl9->K6+ns8Z2EwCX h g |*8;hp@XGyPvED`pa3bb56st1rwq25cwX ziu!Rle5iRs-sxQ0!*|>+c|f8=lQk%Te>ca?7(ulKzroGp7P$YGV%z=^W~eWB!{qLc z*G$V@f#6SP+0Cxq@DtM|n KteoObw#{s`lQ z^yqXsx`y=OhdXFbv3Iy?Kj#LiLn#pM%_a%pN$JrNS75--V-IRwfqBmKRRir|<5i}l zp(S>wTh*Zix$KG)jSMi+FU oKw&XI5Dj0UmNbh|a6D z=42enbG?%)35ViQxBm{r!$M5$TBfS_t+05!ij%Ly8w}AbSt?~&S4f#xfp)e!b!{j_ zfo0&BUlT~?+TfoUGQ|G-FqipL9DXL8(dMv068qXFyfWTI60r*5EG!AF3X659oKQ|B zRd!(0#arznPInKTtBioRBBMqn$MWUp&zTJAkY?F_ML!bEMt<<*h5Rj&n2FG~V M_f`Ep5e#&c;U!Y;n~Tj|;m6C5Wo>z)`g|L#8v zI Y=@U#pCe^%17605v-{D_LK{J_UtxU|5&H@ek)oZ61M z# z^Xxz-zwW?jGs6Si-{VEG+0gS;=SE z|Jx5+nOi!Jh%>LleN%`ObG!kJRVWn6(3kKOx&JsMn1*M}IMxow8x)DRd8%-S-+Cb- z6m+303xYWD@<$cRI(0-a#~uf0EKEOMp*!lly~KIrFSkK(?A|!h!DW;=c_%vBR5@o% z*B6(D#?(I0`bZQlv~ic(*U;QALP|a4_Lt1c <>t*lv%j8=suA-n>qeDW@?{WH6@q!wEEo zEdJ2iKY!T=?8 Bo|7iR?#TexapBpt{Gddv@iD zZ19tUUv^DNtu!Bi)j`W@wkOp7ui!ubMvv9dJh8-m26X7<#6qII%I-U+4^`c7m5Oa^ z7Dsqv+ `7}r zS_|=Kw1Td-uG}RWa _a@Ryp>ELJbOR-v$@$ba#vP?u@GSgbj830M=Ea_4GH-UhDS53@X zTp7#Ee@0o|JrNSEWJ)CTC_qYnK}^W9GyRxa!n^@*BVT;Ew@;`Lt|LqNJZ^N14|S}2 z_0Wt4bXU&n;DkCQH74aEw$g{Mn;JA~JV$T}a8i&SJ);0w-38?cLhU<%gfOw4>BpHV zz8>Y?QBz{@rZu%@f&~Tea@b~-ay<#%N`9ZJT>cfk*?+8W@p>9Qg3(F8y^MZ|O%-y4 zqNQ411{HpHv_1{uZvP2@OA%kXd1mH3*{)C{UO|6NO&P;IicuyI8@yn5=kP;`l}FG8 z0j>K=gf7wCmztP)7jY{z)p7l$NqaZL%6DUx>UO*Ki9IfF|GsB=V=?ERi&yN$!Q5te zdiFN_`$^qIp{;B5lii&YS&M5RUOH4qr%3n6jb&5`r*~u6I?h`)jL*j$9zNSKF`7tc zL3(*_71y5hsM$c7b3yv0IMklZR!Icz0PJLXt@S+@3HXU0G{L&c{ !1sSOVTRijfWMUaU#G6TX6fGLF4PKaNM&evWN!a@XU7o>Y892$Lb!R`SNT zi}qukXG!GbFe0DG5eJ+l#ht?tCf-6_LR5;MC43m4ZZw(B&02-hZj7+_%n4r|`*?}> zGUtz_V@4>2Xf~+4@DO2&t>#s~2Rd=bW^f@C3dYt-_BuS*$=YusAf-EtHna`zipQC- zL2{_DqF1qi-C9vG`2kF_j?XZkuBP0L&j?Se7w)fIt}0MiIE~4{AV9V;jNY;1P@Q$N zZ^4#U$SUo+vsp~k4efm*I}c7^*SlxR+(0s_Eh~Rp>@a#<{q6HT>iTjOx6V!cW+PTy z?;%LPPTYCsRQ4}(ZzmK_<`(U+t1EPK+5 croJJVfj1O|#<-$1 zn?f%D&rdW!*+V6el*0D#phtWL$mXTRZ!+6+4+c(UA(^2w*KT2E4gRzo%Gb32$Oc~+ zWho^$C{DaZTZlkqDrGD>X1AFKhiqk;89{w-OxFA&eVyhpufJwZe5liQsjJ>my|G94 zuHG}8fD--!aOy*aaUo`A@f%0r!4prDuUQ$=k1Pe+-cd`-vBnQXlEba;2wJCG^H o_xVvE|lXZnM6 zHTI2W=&A>c$}4yYK!nDsT!`wI?Q^DD6QBFAwsZ}zjAJlLyj{(WODZ| ZdwxBr>T{RhYm!xa za1uWgJ0-bQyDF7 ?Cg+fxGUnJPbhAv^-k0Ony|tX$zLfyA%! zhZPj?S>dZ#fTFYZGZ<08oTVZWGwZPQ&w&EepeN|Q^6D>9Bg^gzB~3gEpB&PT)BGd& zh{0*o8(J$%EOO)#HM;={5Kw`_pu<4;$J)= nhXf{_$=y^Q+M zJ1>cHXif+r3jDY8%+Q+odXh-O(I_|Pbs;Z_ludXmP@dFKkp|{G@1C%{Yq50-{=A2N zioT9rS#;6obylvYYNhPjs$Y6Y+}bm);no{}glN@Z1y=QFoS2NS_*AunSk#)h_YT-xz;C zP<#;G_?r0sy^&nCpXJ>>DE9YQ6u4uvE5|wgWT4X R~8@;8e)pnd?$HJm7HLYbTm_chDPghy$|b>!$- zFS3{qGkQD6)Yue#v%u;WG=GU=<3f$8V+OT-<%?^evA)oWC?WM@aFx=j$+u`!2NaVZ zG=`gn0+2y`ieh9w_^oda$H!xFxbY=IDK0&c>fE|&-(^fta=BHODxV7NPlXPilKtaw z{f~KO8doUcAlFVw)O}~<@69*IV4AXrDW#K>u(?8)>48~eZ{r)Nu)W!0wGUxcvj04U z_1T``!*65KmzIbOLa^{!_y(KGDB_VlOpW^sU?Qr(6jdnLiT4)f(35o(>iZzT0)3gj z_+)LLXkr#*p<8n{mj#PGz@#INI%tY@DZv|!tBR91yZ|)&I2+YPIR!G3k&E?f!HA{B z #OYE-g|2B0#a=xMoIq6h3rlT4jQ=;E_O)~0Z>fiuvb_X`gPGSV z))a`NP}aPHb0~{5dAgo{$M^w5*fHuBs=UCD9uJF)4}mB@2<+}=`7V$xXE$0%i((=j z&3HqMcd|r9oXSPQK I6`nNe@}^u(z)8Y-P-M-DWc;#(UXItuMhKsT!1To zLgyypZ9^{bgTK=cd_R#cHr4_9vdklNDF5CE#bj5DEH{6f@_4$Fx}~X=TPr*$M%N!U z&2Og!(6Kg`Z`J%X^~$9n6N@dH?S|!X#N4M*`|Z-U#kx&yJm|WJ#Bh`%VLPYECKhx~ zbXDmefc-G 0>ACm&g!6^v_HI*0v~i6++sE;8d2$8+ z?l}q21a}o6UN5l`#n9mp-#RlTm9XQ6lCGpTsW^A3Bv@sGoMSK`r@XYC`l^v2d$!7d zA5o@L%7ri`RX`<}cVrxCfK%Dsy-DGj*&B$Q3iAP=h=uM3%{kCTO!y)Cy0N%MIC$0N z$jK1kDQl70ud6sUX<;;g3Qis2?d=+I-yPhSN0Ky3tOPdL_TVQTeNPF-Cw**m@n&$I zLuBRk*ZnU`NMla9i14dE4@DaxyKO=KxX+L(^Wxf&*dJT9(S-bMYV8@Ul*DWc<$4F4 z0a!^3v;i#GtYUoy8ZA6tPuuQW3l9JpL2bSBR~)QqhfedAQFej2a3hg!XBO-DiGW;2 zK}ZXl?@6o!cOnx9{o)ys@R&B6q7*PDJ;h&e)#IJi?dX}d#9HX9;?|Yohy$^2uBl93 zSf%!tnjIA{bd*5dt35+Xw913aqQVha1HjAjsXaudKYS>@_OUCV d^GFGLIQ9kMTeU2^ei?7FHTqJ0Y62P6|rpz+{? zZ%f0!{m_C{6OY3lxt$~(4$y5AbbN_~#T7M<_gcx&x^p@5HlnSzR5JPR91xt7s`>r^ z<5u!8{rlg)!W!OAskEsEpIPY-P4INmYs{Rh<>O7cnOSc^mrqxJlf!dx?f=3WUaU5p zf+iK&QPHKTxa%V1B~r(t677fUpoJmd%Fh7p<8+a`$JRgIqNVc9Ke(Odh8ChZQ95O* z+0y|PX8A6+frJkR5>U2$#IE}2ADBFL{liLc!2KPqQz~Dobczxt)DBw>RCR7-1h)5# zC}!R*Rr(7L{4cq%cP^Ik=!tZ3JNh(vBWZ7IAlLno>E4M2INKUL_Fp=K{o!u%220=P z(?7(2kJQoqhSPP936IlDXyM bAh#K-T(4_&fxfY~|HD?^ZC)A&3 zco(HH1YUh(jBC$%LhO^xzZ!FiZIE#UA?$DAc{OF?L%1ijMAs}1cRaH Xo1;gmFpZxgcqnwM-(h4qFGS4zZ5Koj uWCuU>o-}yfcU*C?BhoeU1`n0t?RPoC(xP6s_DZK7$}jhXr@}(EO$v3*^hpd+ zs?CIFz3Nsh)_XS>&eLYStvU6 3x4uYL*R}*%@qWQ-e{@U6eEI9&D`;0?*Yj9XK z<=tc8A2}~8q#D}(Lo%`%0{`&j#hygD*c!LVB38Cn<&eeF +RY8rZ_wd84t+bX359^wYm-W%gQLVGvrgUMF>u>`JF z=beX75eP)t_HZ#?n$!@~j4Ev7aePlj{viaHRQ;M#Sj!K=9_e}E>XN`+PS&PYLLVik zfb+tV&O_exG0fXn-0zXZluzx~JmPht9B|vb)5QGqwRp9+1|9r?jzE+jVw`Jexf1Cp z)szJ8^KrD)b6Xd0< zo;KeNYx5s#wnZk6WrYRangj~Q0!H?dc$x=wie=*4sf9*38%r$b(|Br{dtvI)RHG{r zw%PrP!Un``PUa!0+qCIhOFg^ca?=b~;E+Wc(QO!#>+q}F7c+aIiSy+JvSA!&W)*g$ zt831zwb8z>8D9m1GjyVnSBAj{Q=SJt?|z{>(!NDn(A92IDqMRh1UkL02k-sYnTiLB z>Im@awSF1U|KyGHMK$-p)C&W3y03KJg UA7Sr7FhP~@ z820?1-&xa{;4$*E5W($=gDb}A<;{l9x%#=z48Y0f=#~wt`KH&e-^D;i;=y_l49x&F zkQ^oaVxQ+5EostR5CpN+#?)R}MX8Yxbu+&XG%FB%Ysljly4r6ClfIS8VBZ;YR!CGJ z0}PKM#Dtdq$CE&0SkDK4OiYkKXgk^M2CFu8*AurZ=OC250#2C7F9+xPBZArPj?P44 zY&qk$@FQhA#Z)piV1uRS@Hl}L=^+Ie#LI?g%_{>hmbSInnXiB-u&Mu9GBIi3^JO|Q z=_nxYPq>OLF*mn#LuT;jhN_Yn;zB;B`H$tW+I67mSVN Gzv z-m|thJH6l_ve)PeJWYRV2+Mn8E8{z@Neuln;N)|^1+Zy!;8h$_-pb_ixBUJrz@Hmh z!U0OTgUZkpI>jQ3+zJ^wsb9vFAtLVp16LRIQR)XE`OROA=Uq3pKolcjyP^jWid| zLJSg~k7x>9zBI>rEP*bS-A>Rh@ z@3;#+6+_UG($~E%4E>=gc|TG1zkhp9AD;@p-MEQ$3uFQu)0vE*9M3fRi!;E=TtQq< zZZ N z8SnAj?<&62JcACgxYoe7qD9?k>Q4kJvE9^8t3=BHgZoxg+1sbu%CY|-QcyKVA<$XX z$Ofn>c?NKOZJ3wTf|1)~0zA*k+TBuEYi&>`sT?+@qSWKss7Vtc^Wx|;Ldj;o@~dxI z-}NW+-OZWfiNi<3*`YYATAilNszepe@LJ{P*pX(dg nPF5|H$r>1zkIZ1a*ju@gjCY5R;>l$jSaoA zh#t j!;S z1DCH`fy=sdF}x`QZ424`Uy1DGW^$iwUgOj1Io=OBN@gwmsIG0aw+mDAe`W=w4+(={ zJ}u1QH-?+sd@`(Uhn=pkRc`><0GpqF$j%jhb@2974a4|9 g<6tVIRA${tX<#k0rhk+%C4`@G@AnM*fjpASx*>uOFFStu$}A9*_OxneZhyS> z<>4iG&9C4(>K-5c=kPW3xofj98MEbeZt*h-oWDQhOm2(z^^kCl2t{qW`@^n%$Lobd z)Z8Az?ZD$0gK755ux6OH2PfLINBO_s;NvkMh=jWNU$pEdU1N9FW%kRSe~@`D&Dps# z6N?*2wFnl3zR9(%f6Vd-W^Nwq9eOw3uBNu{j)$oUi0Fzj5W%lRhDhz)hBY#G`pK z$#yXBVafvJT(xcJ2Yz>auF0?bug+1 lzzt$<&UOWZF=W17F2}W@ zkN?8iOK4*xki7ThgBe|acf+;rJkhn b 5z3wb{kY_lJGetTCwT-~BLdU)h@ zEC31-#*ZGPE*@6w2cXQ1bI7lV5yn+-3R 6PgF zjBChg4lDc_vCgY6)tUEQ(%EtM`TNlQ@MD=dXgseuHnz{8EBp7$4URWAgl B9V}%*HlF|dfY;RN!tHVp01;klV?4H z&_<#!yB?<}=r8BXM62SFC(UMX8CO1$QO0-Z>xYr3F>6rqXwjThr=SHj^G4@NkKqo5 z7MXh94Mm)^lq^%9_)jDXU57lLezs3fFal-(RwK0_%3x-OtxqwXRn {`hjC@Qe5sAt?a4MPLIcSXCQP}wb`R261R%@)s;>7zs zd^LAz|7>U6a@fx7`DB{2p7}KQ X%Kk^c4H zLNSkqVgDy77Cj5e`|$n6S-$V^vL)Zdgr!Ecks345zbxL$ 4SFG`>DuuMkOdpVz1L2D4c2!hj<{+zY5Bk+E8 zl_#4OHRV{=(4h3CF{^- =Xx(F3GM2;}1%4V^C3b=b$SSaH%eH QjyiOv157E=qo0~j(Ki4Rc! zvm3rLr=J1twzK!b9!qUavyT#6AK{z*`*i5q?5IQ$Ay?wZI_@p0Wp(lHJxwe{$UtGS z(X{7tDtH6ER1IIiGbr7I?W30+hhk?bqKkkZa1MS(-rV2Pqi>epxRuTsegkZ=dJ*>; zfPPaSg+Mg8JXRjXQSK~yCe`0EFN@=x)5qV}8m$fPu)u8CkTvI0%zBy_Zh=m%bc@VS zA477Z @vFpp0B+{|Bv*4rJ}C%Q#kx;*Jk2@!4F=GP3x z5We?K50=x#z?#_E6XALJrD1wmKhcXQ!pQ2fpW462VhBsBm@wu<<0Q5n>XD$adr`=? z`KrEOS(HE;lAsc(!B$SuL^1rPGAN{lA$iyCR@ktcr4yK277+}m0=~vl J}6u1+PdYx2kg&a8>pdSNyF*JQfay zj7*57^>k!CMQdPpa`&>VRrVe1`nYUBk$uMU(J=mRh!o;vMP23zR=eu+$H;2^Uf}yo z>Vy+&F8#CGtCu4X0&qO&NgV?Bk6I%Re5LZAjCCHQ@x@Dac=uq^RoLWJZlGK=uvV;o z8>fZQ1-T&UY8q$}S*=b!aSRMKuM4fe=Kb6^K?>RfRJ2pJ;S`MDLG1QNA>~8g3Sxz& zKmL ^@Ii0kXf;PgaaoYySZLL zasnW{+uT`O8Qj$NGhFYa{EOcETiuxP?~FxTSo<6p8kK>K`z 3e)=0_-W&Tb?binIc8L2&E)PI$Al5hrH;CC% zqP2&>Pd4nIOc02RZor%HDB9HL?PRV;G2g0} @J6E1;JBh=r@|20Vqnz*Eb Zm!L{c7OxLq<$bTc(0r*;`qVP`{{b_4trh?P literal 0 HcmV?d00001 diff --git a/docs/dl-cv/img/0671e492-4e4b-4edd-8622-8208b096056e.png b/docs/dl-cv/img/0671e492-4e4b-4edd-8622-8208b096056e.png new file mode 100644 index 0000000000000000000000000000000000000000..6e50e6f7c0f2037445a848c5cdb260d22e1cddc4 GIT binary patch literal 932 zcmV;V16%xwP) KfF%y}^{e?u(#i>&tk+Ns zogWiB>(S`bXfz`?jY_3bsZ=VJN~Ka=r=OJ6-5%Q)D!RMfz!ACW>G8*PUOykdXa=fO z9)?Id;EmkaG` tX5J-mhvZYd z4;5VCzq4>%f`k3Gf;+_jfHE*>pIr27;A~tsL)nm^3Y_IR=ivy^W-E%>GE8s*DbvEo zgFrb>qeXD#4Kf1OH^DX5N9Uer6$VL6n lhWEv>M>oNlWHblYCAhmrGBCk$fJS*600(kU{R%i_eTK6&+#zn)@Bti6aHfJY zk`yiSYy&trTLTvuz}|m<4;M&| 4+4lQq4tX^RAm)M@x`TmbjxgRBdWoSc78 zuSe7+at7`)Tz5PkhY9Y4BZ|x%YB*2N$rX{nb$Lz2!^Loco{u3;=u?S~zQk{alY&s- zUZRU!N**h?AdjO_%(DmH%>q|EEQPBm38|dnIGOO}4BQy_mA;s^-y--U8*TV?6 `mEVdZT4ZsyRliEi#iZtd7u6S5}Aj$`aW0ntur(b`< zbPC@ch?w9m4fZtExd-aL`DoB0B~NELchzY%61kz;=DT?~STxNZbY&4fHHvbi)9}-{ zw$8Ou=USoyXG=Spg+r~w_|3G4)W%>WwT{-;xw8%ta3BV$rJNz;K7ca<*C!`djERS( zW) )wh i1 Dz! zlr&pJ1e;cu5=6DYd2FBDN)nvZ?YPZ=Gt*Vc)gCUp&uyhcMA9L_;h-RX^KgdpeXf)e zS(D)_))tX}cb~fmF5Z*m;wb4n3fxyQ;6Tj7S( )#J)FxwVBO8ZJ>??(^hvc=sZ=VJN~Kb%R4SF~zt&$YEwAuZx$6M{0000 MJ#}tXTR1p4I5|1FxVX5vxp{baczJpG`1ttw`Q=5=ZDU|yp5p1^7*cWT z?X9zUj|>FZKO8i)WzXE2o}Dx$drH~+xO{nr)c6H^zsQ8%{(fIl-66x zd>)~7QvLQ!f{%Ps6dHJQ5}2iL9AGos(8$XKBH(;Sh#-(ZoAt(?oVbL~nRjwcm~!ln zeO}frV>Uni>?Nb;MzfjEKKpI|&%W%~*;8qyTW;F))| }%X=TR+C>XYRdNw_(lHpR+E%V4A(Vv72xG#`m{gJYCkjq~~F*TCU^8 zZ0+!*&9x?W9Bk$F#v3Aev(IKNdf>75;_kJVOH|}8<#ndc`KVVEpMP!5VjsJ$Nx|X9 z8$IXN9CEWgb1!Z6$tA7oyWZ`pds4bkIVp1DrgSkg|CK*{ZkqhwG|8xW`;^?vJf&L- zPrBXD$-m?FJ?P5vUa|S>Z=C2kqW8GiOf>!9wcJlrZ2W(p+u !P3!J zLgGJXJ!hJ&m&lh{9Ju#n+1#_W9#dPlSjp#hr6?EM9 1N?=wjW<$^H8Ow361pp1mZgMP;k$<%4ZIF5KL`nR9!5 zKAU@tu-Uq3neSiK)^==N%VsuHZ>Hv2uJskOc9rnSubWUjCwK0RCmp8dV`enPTn zLh-KaXZSNWE8f^+z1U*OS@Rov>~Ej?o;msOv%_hhD+~P-|9y}A^(bfG2cEP-vn?mK z?7WcHn!aywwbq}ogc3%Jxu#+Mf1(ymKF )+(c zT{iWXS+gPPZS=;8e9QR{FUs-qoqWP?R)F-gcL9&W6Y{=F?DVa?`aybk+wBwHn--S5 z&)Hj;b@|^r`G(huhPQ*HB;E3FExc3}cV)( ;1Af-k-lo|5)4WOCmXYG@qz$yEDIG-Q|Ly+44(& zE}q-9b@K|ts~aL*;?r_0Uaj0ECY`4_?aIvz+e`9%7MYtiMNT@KvUc9XTl{7m(5wK> z1&pw)0p=?pvI>aL0#WqLEBBpXI@8NSyMIg!7xt(o?D*RO%-al}u6{1-oD!M<62j^M literal 0 HcmV?d00001 diff --git a/docs/dl-cv/img/06f1198b-fb2d-44ab-baed-b2cff9e113ed.png b/docs/dl-cv/img/06f1198b-fb2d-44ab-baed-b2cff9e113ed.png new file mode 100644 index 0000000000000000000000000000000000000000..2b271e8830777033a7388ab45d8f8ae19722cb77 GIT binary patch literal 4953 zcmYjVc{r5a` YxEQ7H<_G#=YLYASj8@sZF%uKR{tQ9kq zeMmy~B^qS!H@)BYdavL4<2m>9ocn(6>;BxI&pFq1Voi 65!F)%Q~ z85oY`Gc(b@7??mi=@Zn{5PAL2pFec7xw-k?y?azD6^FyEt*vQlYO=Glb8v8Qa&mHU zae+V}Zf 9p4#lxhmG?axV_+1v(z0XB*1T-{W=&(r z^)Rp3$o&S(iROu;eQzmytrTSCkVSUT>m}6L ~3`UQrUFjCM2x}Bw+t&Tw)1y}T}s#|LJhN4jZAsn#K zD4M`Q$mxkaw#kx&c=5gSRNR-k8;SWgs!Mw#smqh8kuK)ASFUGI@8U0 m=zQ+_mjzh+2FE;LBHNaHmZ|QVTJ@_>96Yd5l87b4w2R2MW3py?X*U#!QQIYH7 z+j8Ox7I_Mzf|ly $~?3PK%+{9S10nHQx$^nQAcPAyDutBEuZzuC8)UA!}3sdhml?D5<*=G zp)tiOM;xXR{?RaTjUBn^6A!Pd%98pq;aiJ2tLSf22~z>dh&&i6Bh^}3@*Ls=sqJIv z*~q%53$El0vP;zW9q8zwNQZfxu|Y3q>*TT7tl{#o$1Vet4(;{Dx5vnci#q6{q}ag3 zYkkq@uWv;v%L^*|zyh_!Ne=M8y~8sSRh;2_hS_uzTuuFH7^<52`)Fu6i3t0hiKFdv z=H{Xvez=HM6o0AvnY+Xu7uCW6-`6nZ*wI0Dy>#URw#SA3D4p^CG7|cyCRVd%GQG4y zYbm%&r8quKM6l)f{ly2M(V{Lgx=SoUyQ8>6FrG?IG&eC>-xsLVQxQccLylCA&rhlA zwF29ZC>o?MeP+63L{FCsT-8kx66RCiGSZO_(1Za-wE26(&>27kP3ygy3dK*eJixm! z{f~a_Kn6F7Y61@lF$(1ZqR9yFD<$M&9}>T{y(0`i?L2Mmzz2imtSn>p;G-Gj=ou9H zpAb3(#je2i<~;k`HnfT2MrAc21J`TQ+;~h z|4sG}c>Fk{e_nQx@YN?&SCUa49mMvGR;n=FxgAiJL;nLYgl_Z1&|NoybLb-H8AZq{ zh=(NeS+-Mvnymo2gnvj~=s>|)IDOuJoQwW{8PHnPii7 t1bBX_nHsbcWfs_I=GpzUJ48c>nr;Lqd8XP>^8 zLQ(|CC8ckyf$fWYlW<5Lb>RgCz=rd>z(dkv&Xc3(y)%raM&EhG_x#01Mvm^W;wRNi z!qQQu#3*b$p)NSF4Rfu?LXznSB~{ZIr%D+0Qg*Tb4 -Esg1X}Mu-zB-FLUX|dZ5b2k-xKWH8OC^#mY;TCVy5h;f*L} zDOK;o3&C%{TEihd9W5@kx %Z3XhP}u!5XF&LqUgP|*ETbPHy!f++pBye zuLoW|>f*F#;3me+s-`E4nA(J9O*{?h_4Q3uw>_StMV8VFzm@0LMV7h(l*zAp@!*V@ z a{h9*-q4eaRwzgbLY_*H|b0FQ|f}^hB;lJvMi)8BAd!P z jqHla%fr?qGNY0s4_&M)<=g{_&^Y!cbddZQfna};~$XoiV@w9O}20s`+|A+% rGkYQ9-IR8cxeopuDwAtoSQc`JTTBLm#{AIGo zneV|ceBA1hC)~YKX|&%KxT2TW+I8b`W6DI`dM))1LA2- ~k z9o!gtVl=`ae!-D6!8YPIKWf7#&tz +^2&sKVS z{$vKRHF4zUY|@q$UPJy|qe@kskEl+-bm2r`a*$ocTcMp{Nq9i2<1dxhJ*&LdlRZ>|6=0@thys%(XtWt-ndSvKmZhz$;0b zjN|Wl@3e|+P(`>+o~)B&T@QiK!XIb!P=0 1DRb-gr`p~S<*s?KOB*mF Bh9XCfHA=$(2 zhI-;lXOfM&Lg-9#d@E}Krp&8pLRRR5>S`fDpBq7dX~_Fd@dW56#OK)tU5EE6UctM} z 3iYNAy( z<3*Q7UpFic3&IzO0~TUO*Ct|)HdTlHfbOl-L!3TP^&t+os9H7iC_NMoQF0M%x%t^E z;bx|sfXX*rVSD7e+OVZw%I&C~o@~qZeWT=(iI<}yEEkEerr0E4?AwpHuCh-ZdQDBZ zInb4z-_F6UPCLVb=@+Uht12$_NU`^ZEY3cO0)n}OdQWS`3y{gkgUTP~q%Rv>5Qq-P zirdGodj9w31cy@D%FT5qw{#A70S zo*VNap4tk%oSF!2D$r~yCY1#3(6*LiEhXW-l`+Szt8>VQ3N| cTU;+~^=CHBJ(Iy%lSu+QXs=*~;>apQ~V zwXy(-wkrtIofU7JNGo^EpQ$bN+sKuG0HAcGr)Wz-vZnt+66N(`a4riF-h>(*)}Xpu zzV?_CdUHxX&YPH;I?qLA_6NnZx)ECda(ooQ&beU;a}3|Dt|R=ucRQW(fQwq8sOOu9 z`?XIku`dFk?B^PNZ5sqgnKixX+J`xkleD2U@Izc&ezN6maDWOtz9;N79^7#vPwD*9 z8#|KK_I6e0tR{}qFgu(l&l53rp*G->oTW6}YPbPeV~}!@7ZMk^lwyNPFT=+AT}Lr} z^9^WV2!5`zNRquNk)$*My(es|Aq|(OhqV`1-TGOaf6Xd*eai1Rzg6@jEa!~QY@CEP zG%v!vNAw)N6MO=j=6vU@{ yy7++YCni73LK=3S G|;beSJ&8YvVFg1)9B7Jm-68 zfqWt4g3BIeg<7q2sf6dzZ0TLa2=5o)#^=>FocLbU4H1#`4nL%~>}@q<1vS#!9nWLl zWk0NQDbo81%1UG2_$>1HtgzU3m+H2;Wk0j;1}!Gu*FbwG_9CUyUQvz=1y5FI9R@P+ z{$@Ko)z @4C&$k=WrL_CCyg)2Fa@2#c)Ojyx@K;tJvB*Yob**RYDA z10Z*QW!G4CMCs`?*Ub(4L!m9%p#-#DVK9^Ih}qKa!`c0CO?+Cvh4Rk)Z|#nLP;0pL zZBgVkFc`_lq4dQ0oF@{R?HD;Z0Fk+xlyIEUeYDLdk-~0enp%mey)C-ef9qtE^GwN7 znHEsFGDTp1Vg)Y^=MGY;4nAfCjUlja@b?k4dkHsQRXhuRJL&e~ ?Zap z1x!>>W1s