PaperSpace-博客中文翻译-五-

PaperSpace 博客中文翻译(五)

原文:PaperSpace Blog

协议:CC BY-NC-SA 4.0

理解 GauGAN 第 1 部分:揭开 Nvidia 的风景画 GANs

原文:https://blog.paperspace.com/nvidia-gaugan-introduction/

2019 年在 CVPR 提交的最有趣的论文之一是英伟达的 *语义图像合成与空间自适应归一化。*这是他们的新算法 GauGAN 的特色,它可以有效地将涂鸦变成现实。这项技术已经在工作了一段时间,从 Nvidia 在 2017 年首次推出 Pix2PixHD 和 2018 年推出 Vid2Vid 开始。最后,2019 年给了我们印象深刻的 GauGAN 的加入。看看 Nvidia 为 2020 年准备了什么会很有趣。

GauGAN can turn doodles into lifelike images. This image, courtesy of Nvidia, demonstrates how GauGAN can produce realistic versions of different elements such as "road," "tree," etc.

在本文中,我们将看到 GauGAN 算法如何在粒度级别上工作。我们还将深入了解为什么 Nvidia 在这些算法的使用上投资如此之大。

我还想指出,在这篇文章中,我将主要关注 GauGAN,并避免在这篇文章中过于深入 Pix2PixHD。这两种算法都生成图像,并且有很多共同点,GauGAN 是最近开发的。因此,我将把重点放在 GauGAN 上,只在出现与 GauGAN 显著不同的特性时才提到 Pix2PixHD,主要是为了避免重复。

这是四部分系列的第一部分。我们还将介绍:

你也可以在 ML Showcase 上查看 GauGAN,并在免费的 GPU 上运行模型。

我们开始吧!

条件 gan

gan 通常用于生成数据。你通常提供一个噪声输入,网络将利用它产生一个相关的输出。这种 GAN 非常有用,因为除了随机噪声之外,它不需要任何东西来产生数据,任何数字软件都可以产生随机噪声。

另一方面,有条件的 gan 通常采用特定的输入来确定要生成的数据(换句话说,生成的数据是以我们提供的输入为条件的)。例如,在 GauGAN 中,输入是语义分割图,GAN 根据输入图像生成真实图像,如下例所示。

GauGAN takes a Semantic Segmentation Map (left) as input and produces a photo-realistic image as the output (right). Image Credits: Nvidia

类似地,其他条件生成网络可以创建:

  1. 以前一帧为条件的视频帧。
  2. 以平面地图图像为条件的地形地图图像。
  3. 以文本描述为条件的图像。

还有更多。

高根建筑

像任何其他生成性对抗网络一样,GauGAN 包含一个鉴别器和一个生成器。具体到 GauGAN,Nvidia 引入了一种新的标准化技术,称为空间自适应标准化,或通过使用特殊的铲块进行铲标准化。此外,GauGAN 具有一个用于多模态合成的编码器(如果这没有意义,不要担心,我们将在后面讨论)。

发电机的输入包括:

  1. 一个一键编码的语义分割图
  2. 边缘贴图(可选)
  3. 编码的特征向量(可选)

第一个是必需的,而第二个和第三个输入是可选的。无论使用哪一组输入,它们在发送到生成器之前都会进行深度连接。

语义分割图基本上是一次性编码的,因此我们对每个类别都有一个一次性编码的图。这些地图然后被深度连接。

Diagram illustrating how semantic maps are one-hot encoded before being sent to the generator in GauGAN

只有当图像中的对象具有实例 id 时,边缘图才是可能的。在分段标签中,我们只关心对象的类别。如果两辆汽车重叠,那么汽车标签将是由两辆重叠汽车组成的斑点。当两个对象重叠时,这可能会混淆算法并导致其性能下降。

为了克服这个问题,在 Pix2PixHD 中引入了语义标签映射的使用。这是一个 0-1 二进制映射,其中除了四个相邻像素不属于同一个实例的像素之外,每个像素都是零。

通过将用作风格指南的图像传递通过编码器来产生编码的特征向量。

发电机

GauGAN 的生成器是一个由 SPADE 块组成的全卷积解码器。SPADE 代表空间自适应归一化块。当我们完成架构的高级概述后,我们将详细讨论这个组件。

GauGAN Generator. A miniature version of the Pix2PixHD generator has been shown for comparison. Pix2PixHD has convolutional layers, residual blocks and transpose convolutions in that order. Source: https://arxiv.org/pdf/1903.07291.pdf

GauGAN 生成器和 Pix2PixHD 的架构有很大的不同。

首先,不涉及下采样。缩减像素采样层构建语义信息。相反,作者选择直接提供语义分割图作为每个 SPADE 块(对应于网络中的不同级别)的输入。

第二,与 Pix2PixHD 不同,上采样是通过最近邻调整大小来执行的,而不是通过使用转置卷积层。转置卷积层失去了很多支持,因为它们容易在图像中产生棋盘状伪像。从业者已经开始转向不可学习的上采样,然后是卷积层。作者只是跟随了这一趋势。

铲归一化层

在论文中,作者认为无条件规范化会导致语义信息的丢失。这种形式的规范化包括批量规范化和实例规范化。

深度学习中的规范化通常包括三个步骤:

  1. 计算相关统计数据(如平均值和标准偏差)。
  2. 通过减去平均值并将该数字除以标准偏差来标准化输入。
  3. 通过使用可学习的参数$ \gamma,\beta \(对输入\) y = \gamma x + \beta $进行重新缩放

批次定额和实例定额在步骤 1 中有所不同,即如何计算统计数据。在批处理规范中,我们计算图像批处理的统计数据。在实例规范中,我们计算每个图像。

这里有一个例子可以帮助你发挥想象力。

In batch normalization, the statistics are computed over feature maps across all batches. In instance normalization, the statistics are computed over feature maps across a single image.

然而,在 SPADE 中,我们以这样的方式修改批范数(注意,我们仍然跨每个特征图计算小批的统计),即我们学习特征图中每个像素的不同参数集,而不是学习每个通道的单个参数集。我们直接通过增加批范数参数的数量来达到像素的数量。

铲形发生器模块

作者介绍了一个 SPADE 生成器模块,这是一个小的残差卷积网络,它产生两个特征映射:一个对应于逐像素的$\ beta $,另一个对应于逐像素的$\ gamma$。这些图的每个像素代表用于重新缩放特征图中相应像素的值的参数。

Design of a SPADE Unit. Note, this describes the SPADE unit in the SPADEResBlk demonstrated in the diagram above.

上图可能会让一些读者感到困惑。我们有一个叫做 Batch Norm 的模块,它只执行统计数据的计算。SPADE 中统计量的计算类似于批量定额。稍后进行重新缩放。

在多 GPU 系统上如何实现规范化的上下文中,同步规范化被称为同步规范化。通常,如果您的批处理大小为 32,并且您有 8 个 GPU,PyTorch's nn.BatchNorm2d layer 将跨每个 GPU 分别计算 4 个批处理的统计数据,并更新参数。在同步批处理规范中,统计数据是在全部 32 幅图像上计算的。当每个 GPU 的批处理大小很小时,比如 1 或 2,同步规范化很有用。小批量计算统计数据可能会产生非常嘈杂的估计值,导致训练紧张。

作为 out of the SPADE 模块获得的特征映射被逐元素相乘并添加到归一化的输入映射中。

虽然没有在本文的图表中显示,但每个卷积层都遵循谱范数。首先,频谱范数限制了卷积滤波器的 Lipschitz 常数。关于谱范数的讨论超出了本文的范围。我已经给出了一篇讨论谱范数的文章的链接。

这是描述高根的方程式。

SPADE 为什么有用?

第一个原因是 GauGAN 的输入是一个语义图,它被进一步一次性编码。

这意味着 GAN 必须采用统一值的区域,精确地说是 1,并产生具有不同值的像素,以便它们看起来像真实的物体。与特征图中每个通道的单组批范数参数相比,为每个像素设置不同组的批范数参数有助于更好地处理这项任务。

作者还声称 SPADE 导致更多的区别性语义信息。为了支持他们的观点,他们拍摄了两幅图像,这两幅图像都只有一个标签,一幅是天空,另一幅是草地。虽然我发现下面的推理很弱,但为了明智地涵盖这篇论文,我还是要指出来。

作者给出了一个例子来支持他们的说法,他们拍摄了两张图像,只包含一个标签。一个是天空,一个是草地。对这两个图像应用卷积会产生不同的值,但值是一致的。作者接着指出,应用实例范数将把两个不同值但一致的特征映射变成一个只含零点的同值特征映射。这导致语义信息的丢失。

然后,他们继续展示 SPADE 和 Instance Norm 的输出如何在一个包含完全相同标签的语义图中有所不同。

然而,这似乎不像是苹果之间的比较。首先,作者声称标准化的结果是信息被抽取。然而,黑桃和实例规范中的标准化步骤是相同的。它们不同的地方是重新缩放步骤。

第二,在 Pix2PixHD 中,实例范数层的参数是不可学习的,实例范数仅仅是执行归一化($ \ gamma \(设置为 1,\)\beta$设置为 0)。然而,在 GauGAN,SPADE 有可学习的参数。

第三,比较批量规范和 SPADE 是比实例规范和 SPADE 更好的比较。这是因为实例 Norm 的有效批处理大小为 1,而 SPADE 和 Batch Norm 都可以利用更大的批处理大小(更大的批处理大小导致更少的噪声统计估计)。

鉴别器

通常,鉴别器是一个分类器网络,末端是完全连接的层,根据鉴别器对图像的逼真程度,产生 0 到 1 之间的单一输出。

多尺度 PatchGAN 鉴别器是一个完全卷积的神经网络。它输出一个特征图,然后对其进行平均,以获得图像的“逼真度*”*得分。完全卷积有助于 GAN 工艺尺寸不变。

文章认为鉴别器和发生器一样使用谱范数。然而,看一下实现就知道使用了实例规范。这也反映在图表中。

编码器

与普通的 GANs 不同,GauGAN 不采用随机噪声向量,而只采用语义图。这意味着给定一个单一的输入语义图,输出总是确定的。这违背了图像合成的精神,因为生成不同输出的能力是非常重要的。仅重构输入的 GAN 与单位函数一样好。我们进行合成以生成超出我们训练数据的数据,而不仅仅是使用神经网络来重建它。

为此,作者设计了一种编码器。编码器主要获取一幅图像,将图像编码成两个向量。这两个向量用作正态高斯分布的均值和标准差。然后,从该分布中采样一个随机向量,然后与输入语义图一起连接,作为生成器的输入。

当我们对不同的向量进行采样时,合成结果也随之多样化。

风格引导的图像合成

在推断时,该编码器可以用作要生成的图像的样式指南。我们通过编码器传递要用作样式向导的图像。然后,生成的随机向量与输入语义图连接。

当我们从分布(其平均值和标准偏差由编码器预测)中采样不同的值时,我们将能够探索数据的不同模式。例如,每个随机向量将产生具有相同语义布局但不同模态特征(如颜色、亮度等)的图像。

损失函数和训练

GauGAN 的损失函数由以下损失项组成。当我们经过的时候,我会逐一检查。

多尺度对抗性损失

GauGAN 合并了一个铰链损耗,这也见于 SAGAN 和 Geometric GAN 等论文。下面是损失函数

给定生成器生成的图像,我们创建一个图像金字塔,将生成的图像调整到多个比例。然后,我们使用每个尺度的鉴别器计算真实度得分,并反向传播损失。

特征匹配损失

这种损失促使 GAN 产生不仅能够欺骗发生器的图像,而且所产生的图像还应该具有与真实图像相同的统计特性。为了实现这一点,我们惩罚真实图像的鉴别器特征图和伪图像的鉴别器特征图之间的 L1 距离。

为生成的图像的所有尺度计算特征匹配损失。

$$ L_ (G,D_k) = \mathop{\mathbb} {s,x } :\sum \frac{1}{n_}[||d{(i)}{(s,x)}-d^{(i)}{(s,g(s))}||_1]$ $

这里$k$代表我们使用的图像比例。我们使用来自鉴别器的$T$特征图,并且$N_i$是每个特征图的归一化常数,使得每对特征图之间的 L1 差具有相同的比例,尽管每个特征图中的滤波器数量不同。

VGG 损失

这种损失与上述损失类似,唯一的区别是,我们不是使用鉴别器来计算特征图,而是使用在 Imagenet 上预先训练的 VGG-19 来计算真实和伪造图像的特征图。然后我们惩罚这些地图之间的 L1 距离。

$$L_ (G,D_k) = \mathop{\mathbb}{s,x } :\sum{5} \frac{1}{2i}[||vgg{(x,m_)}-vgg(g(s),m _ I)| | _ 1]$ $ $ $其中: VGG(x,m):is :the :feature :map :m :of :vgg 19 :when :x :is :the :input。\ \ \ \ and :M = \ { " relu 1 \ _ 1 "、" relu2_1 "、" relu3_1 "、" relu4_1 "、" relu5_1"} $$

编码器损耗

作者对编码器使用 KL 发散损失
$ $ L _ = D _ (q(z | x)| | p(z))$ $

在上面的 loss 中,$q(z|x)$被称为变分分布,根据给定的实像$x$我们从中抽取出随机向量$z$而$p(z)$是标准的高斯分布

请注意,虽然我们可以在推断过程中使用样式图像,但分割图的基础事实在训练过程中充当我们的样式图像。这是有意义的,因为地面真相的风格和我们试图合成的图像是一样的。

你可以把上面的损失函数看作是自动编码器损失变化的正则化损失项。对于编码器,GauGAN 表现为一种变化的自动编码器,GauGAN 扮演解码器的角色。

对于熟悉变分自动编码器的本笔记,KL 发散损失项充当编码器的正则项。这种损失不利于我们的编码器预测的分布和零均值高斯分布之间的 KL 散度。

如果没有这种损失,编码器可能会通过为我们数据集中的每个训练示例分配不同的随机向量来作弊,而不是实际学习捕获我们数据形态的分布。如果你对这个解释不清楚,我推荐你阅读更多关于可变自动编码器的内容,我在下面提供了链接。

结论

所以,这就结束了我们对 GauGAN 架构及其目标函数的讨论。

在下一部分中,我们将讨论 GauGAN 是如何训练的,以及它与竞争对手的算法相比表现如何,尤其是它的前身 Pix2PixHD。在那之前,你可以查看 GauGAN 的网络演示,它允许你使用一个类似绘画的应用程序创建随机的风景。

了解 GauGAN 系列

进一步阅读

  1. 批量定额
  2. VAE
  3. 高根演示
  4. FID
  5. 光谱范数

基于 Keras 的有向掩模 R-CNN 的目标检测

原文:https://blog.paperspace.com/object-detection-directed-mask-r-cnn-keras/

对象检测是一种重要的计算机视觉应用,它使计算机能够看到图像内部的内容。由于对象在图像中的位置不可预测,所以存在密集的计算来处理整个图像以定位对象。

随着卷积神经网络(CNN)的出现,检测物体的时间已经减少,因此物体可能在几秒钟内被检测到(就像你只看一次( YOLO )模型)。但是即使 YOLO 速度很快,它的准确性也不如它的速度令人印象深刻。一个更精确的模型,但不幸的是需要更多的时间,是基于地区的 CNN (R-CNN)

本教程是对 R-CNN 的扩展,称为遮罩 R-CNN ,将它指向物体的候选位置。由此产生的模型被称为导向面具 R-CNN 。当有关于物体位置的先验信息时,它工作得很好。

本教程的大纲如下:

  1. R-CNN 模型概述
  2. 区域提议
  3. 操纵由区域提议网络(RPN)产生的区域
  4. 定向屏蔽 R-CNN
  5. 参考

R-CNN 模型概述

根据我以前的一个名为更快的 R-CNN 的 Paperspace 博客教程,针对对象检测任务进行了解释,下图给出了对象检测模型涵盖的三个主要步骤。

第一步负责提出候选区域。这一步的输出只是一些可能包含对象的区域。在第二步中,独立处理每个区域以确定是否存在对象。这是通过提取一组足以描述该区域的特征来完成的。最后,来自每个区域的特征被馈送到一个分类器,该分类器预测是否存在对象。如果有一个对象,那么分类器预测它的类别标签。

这个管道是根据正在使用的模型定制的。R-CNN 模型在本文中有描述。下图给出了它的流程图。

使用选择性搜索算法生成区域建议。该算法返回大量区域,其中图像中的对象很可能在这些区域之一中。

在 ImageNet 数据集上预先训练的 CNN 用于从每个区域提取特征。在 R-CNN 模型的末尾,区域特征向量被馈送到特定于类别的支持向量机(SVM ),以预测对象的类别标签。

R-CNN 模型的一个缺点是该模型不能被端到端地训练,并且之前讨论的 3 个步骤是独立的。为了解决这个问题,R-CNN 架构被修改为一个新的扩展。这种更新的架构被称为更快的 R-CNN(在本文的中介绍),其中:

  • 区域提议网络(RPN)取代了选择性搜索算法。
  • RPN 连接到预训练的 CNN,在那里它们两者可以被端到端地训练。
  • SVM 分类器由预测每个类别概率的 Softmax 层代替。

更快的 R-CNN 只是一个由 3 部分组成的大网络,其中训练从 RPN 开始端到端地进行,直到 Softmax 层。

如需了解更多信息,请查看此 Paperspace 博客教程:更快的 R-CNN 解释对象检测任务

Mask R-CNN 型号是更快 R-CNN 的扩展版本。除了预测对象位置之外,还返回对象的二进制掩码。这有助于将对象从背景中分割出来。

下一节集中在区域提案,解释本教程的主要贡献之一;即使用静态而不是动态的区域提议。

地区提案

区域建议生成器(如选择性搜索或 RPN)生成大量可能有对象的区域。目标检测模型消耗的大部分时间只是检查每个区域是否包含目标。它以许多被拒绝的区域和少数包含对象的被接受的区域结束。

下面的 Python 代码( source )使用 OpenCV 生成区域建议,并在图像上绘制它们。它调整图像的大小,使其高度为 400 像素。代码只显示 1000 个地区。您可以更改这些数字,并检查结果如何变化。

import sys
import cv2

img_path = "img.jpg"

im = cv2.imread(img_path)
newHeight = 400
newWidth = int(im.shape[1]*400/im.shape[0])
im = cv2.resize(im, (newWidth, newHeight))    

ss = cv2.ximgproc.segmentation.createSelectiveSearchSegmentation()
ss.setBaseImage(im)

# High recall Selective Search
ss.switchToSelectiveSearchQuality()

rects = ss.process()
print('Total Number of Region Proposals: {}'.format(len(rects)))

numShowRects = 1000
imOut = im.copy()

for i, rect in enumerate(rects):
  if (i < numShowRects):
    x, y, w, h = rect
    cv2.rectangle(imOut, (x, y), (x+w, y+h), (0, 255, 0), 1, cv2.LINE_AA)
  else:
    break

cv2.imshow("Output", imOut)

cv2.imwrite("SS_out.jpg", imOut)

在将一个样本图像输入该代码后,总共找到了 18,922 个区域建议。下图显示了前 1000 个区域的边界框。

在一些对象检测应用中,您可能具有关于对象可能存在的候选位置的先验信息。这样,对象检测模型将通过只查看您提供的位置来节省时间。

在下一张图片中( source ),我们已经有了马的位置信息,因为这些位置不会改变。只有 7 个地区值得关注。因此,与其要求对象检测模型浪费时间搜索区域提议,不如将这 7 个区域提供给模型要容易得多。模型只会决定一匹马是否存在。

不幸的是,像 Mask R-CNN 这样的目标检测模型无法明确告诉他们候选区域。因此,该模型从零开始,并花时间搜索所有可能存在对象的区域方案。它可能会也可能不会搜索您希望它搜索的地区。

选择性搜索算法检测到 20K 个区域建议,其中只有 1000 个区域显示在下图中。因此,该模型将花费时间在所有区域上工作,而实际上只有 7 个区域是感兴趣的。

屏蔽 R-CNN Keras 示例

名为 matterport/Mask_RCNN 的现有 GitHub 项目提供了使用 TensorFlow 1 的 Mask R-CNN 模型的 Keras 实现。为了使用 TensorFlow 2,该项目在 ahmedgad/Mask-RCNN-TF2 项目中进行了扩展,该项目将在本教程中用于构建 Mask R-CNN 和 定向 Mask R-CNN

本节仅给出一个使用掩码 R-CNN 模型加载和进行预测的示例。下面的代码加载模型的预训练权重,然后检测对象。该模型在 COCO 对象检测数据集上进行训练。

该项目有一个名为model的模块,其中有一个名为MaskRCNN的类。创建该类的一个实例来加载 Mask R-CNN 模型的架构。使用load_weights()方法加载预训练的模型权重。

加载模型后,它就可以进行预测了。图像被读取并输入到detect()方法。此方法返回 4 个输出,分别代表以下内容:

  1. 检测对象的感兴趣区域(ROI)。
  2. 预测的类别标签。
  3. 班级成绩。
  4. 分段遮罩。

最后,调用visualize模块中的display_instances()函数来突出显示检测到的对象。

import mrcnn
import mrcnn.config
import mrcnn.model
import mrcnn.visualize
import cv2
import os

CLASS_NAMES = ['BG', 'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light', 'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow', 'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard', 'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple', 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', 'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush']

class SimpleConfig(mrcnn.config.Config):
    NAME = "coco_inference"
    GPU_COUNT = 1
    IMAGES_PER_GPU = 1

    NUM_CLASSES = len(CLASS_NAMES)

model = mrcnn.model.MaskRCNN(mode="inference", 
                             config=SimpleConfig(),
                             model_dir=os.getcwd())

model.load_weights(filepath="mask_rcnn_coco.h5", 
                   by_name=True)

image = cv2.imread("test.jpg")
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

r = model.detect([image])

r = r[0]

mrcnn.visualize.display_instances(image=image, 
                                  boxes=r['rois'], 
                                  masks=r['masks'], 
                                  class_ids=r['class_ids'], 
                                  class_names=CLASS_NAMES, 
                                  scores=r['scores'])

下面的图像是在前面的代码中提供的,以显示事情是如何工作的。

下图显示了带标签的对象。每个对象都有一个边界框、遮罩、类别标签和一个预测分数。

要了解更多有关使用 TensorFlow 1 和 2 在 Keras 中使用 Mask R-CNN 的信息,请查看以下资源:

为了达到本教程的目的,即构建一个只调查一组预定义区域的定向掩码 R-CNN,下一节将讨论如何操作区域建议网络(RPN)来检索和编辑区域建议。

操纵由区域提议网络(RPN)产生的区域

Mask R-CNN 架构中负责产生区域提议的部分是(正如您可能已经猜到的)区域提议网络(RPN)。它使用锚的概念,这有助于产生不同比例和长宽比的区域建议。

Mask_RCNN 项目中,有一个被称为ProposalLayer的层,它接收锚,过滤它们,并产生许多区域提议。用户可以定制返回区域的数量。

默认情况下,调用detect()方法后返回的模型输出是 ROI、类标签、预测分数和分段掩码。该部分编辑模型以返回由ProposalLayer返回的区域建议,然后调查这些区域。

本质上,MaskRCNN类被编辑,因此它的最后一层变成了ProposalLayer。下面是返回地区建议的MaskRCNN类的简化代码。

class MaskRCNN():

    def __init__(self, mode, config, model_dir):
        assert mode in ['training', 'inference']
        self.mode = mode
        self.config = config
        self.model_dir = model_dir
        self.set_log_dir()
        self.keras_model = self.build(mode=mode, config=config)

    def build(self, mode, config):
        assert mode in ['training', 'inference']

        h, w = config.IMAGE_SHAPE[:2]
        if h / 2**6 != int(h / 2**6) or w / 2**6 != int(w / 2**6):
            raise Exception("Image size must be dividable by 2 at least 6 times "
                            "to avoid fractions when downscaling and upscaling."
                            "For example, use 256, 320, 384, 448, 512, ... etc. ")

        input_image = KL.Input(
            shape=[None, None, config.IMAGE_SHAPE[2]], name="input_image")
        input_image_meta = KL.Input(shape=[config.IMAGE_META_SIZE],
                                    name="input_image_meta")

        input_anchors = KL.Input(shape=[None, 4], name="input_anchors")

        if callable(config.BACKBONE):
            _, C2, C3, C4, C5 = config.BACKBONE(input_image, stage5=True,
                                                train_bn=config.TRAIN_BN)
        else:
            _, C2, C3, C4, C5 = resnet_graph(input_image, config.BACKBONE,
                                             stage5=True, train_bn=config.TRAIN_BN)
        P5 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c5p5')(C5)
        P4 = KL.Add(name="fpn_p4add")([
            KL.UpSampling2D(size=(2, 2), name="fpn_p5upsampled")(P5),
            KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c4p4')(C4)])
        P3 = KL.Add(name="fpn_p3add")([
            KL.UpSampling2D(size=(2, 2), name="fpn_p4upsampled")(P4),
            KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c3p3')(C3)])
        P2 = KL.Add(name="fpn_p2add")([
            KL.UpSampling2D(size=(2, 2), name="fpn_p3upsampled")(P3),
            KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c2p2')(C2)])

        P2 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p2")(P2)
        P3 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p3")(P3)
        P4 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p4")(P4)
        P5 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p5")(P5)

        P6 = KL.MaxPooling2D(pool_size=(1, 1), strides=2, name="fpn_p6")(P5)

        rpn_feature_maps = [P2, P3, P4, P5, P6]

        anchors = input_anchors

        rpn = build_rpn_model(config.RPN_ANCHOR_STRIDE,
                              len(config.RPN_ANCHOR_RATIOS), config.TOP_DOWN_PYRAMID_SIZE)

        layer_outputs = [] 
        for p in rpn_feature_maps:
            layer_outputs.append(rpn([p]))

        output_names = ["rpn_class_logits", "rpn_class", "rpn_bbox"]
        outputs = list(zip(*layer_outputs))
        outputs = [KL.Concatenate(axis=1, name=n)(list(o))
                   for o, n in zip(outputs, output_names)]

        rpn_class_logits, rpn_class, rpn_bbox = outputs

        proposal_count = config.POST_NMS_ROIS_INFERENCE
        rpn_rois = ProposalLayer(
            proposal_count=proposal_count,
            nms_threshold=config.RPN_NMS_THRESHOLD,
            name="ROI",
            config=config)([rpn_class, rpn_bbox, anchors])

        model = KM.Model([input_image, input_image_meta, input_anchors], 
                         rpn_rois, 
                         name='mask_rcnn')

        if config.GPU_COUNT > 1:
            from mrcnn.parallel_model import ParallelModel
            model = ParallelModel(model, config.GPU_COUNT)

        return model

因为detect()方法期望模型返回关于检测对象的信息,如 ROI,所以必须编辑该方法,因为模型现在返回区域建议。下面给出了这个方法的新代码。

def detect(self, images, verbose=0):
    assert self.mode == "inference", "Create model in inference mode."
    assert len(images) == self.config.BATCH_SIZE, "len(images) must be equal to BATCH_SIZE"

    if verbose:
        log("Processing {} images".format(len(images)))
        for image in images:
            log("image", image)
    molded_images, image_metas, windows = self.mold_inputs(images)
    image_shape = molded_images[0].shape
    for g in molded_images[1:]:
        assert g.shape == image_shape, "After resizing, all images must have the same size. Check IMAGE_RESIZE_MODE and image sizes."

    anchors = self.get_anchors(image_shape)
    anchors = np.broadcast_to(anchors, (self.config.BATCH_SIZE,) + anchors.shape)

    if verbose:
        log("molded_images", molded_images)
        log("image_metas", image_metas)
        log("anchors", anchors)

    rois = self.keras_model.predict([molded_images, image_metas, anchors], verbose=0)

    return rois

要获得新的 model.py 文件的完整代码,该文件应用了ProposalLayer层和detect()方法中的更改,请点击这里

下一个代码块给出了一个示例,它使用新的MaskRCNN类来返回区域建议,并将它们绘制在图像上。你可以在这里找到剧本

与之前的示例相比,模块名称从mrcnn更改为mrcnn_directed

import mrcnn_directed
import mrcnn_directed.config
import mrcnn_directed.model
import mrcnn_directed.visualize
import cv2
import os

CLASS_NAMES = ['BG', 'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light', 'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow', 'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard', 'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple', 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', 'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush']

class SimpleConfig(mrcnn_directed.config.Config):
    NAME = "coco_inference"

    GPU_COUNT = 1
    IMAGES_PER_GPU = 1

    NUM_CLASSES = len(CLASS_NAMES)

model = mrcnn_directed.model.MaskRCNN(mode="inference", 
                                      config=SimpleConfig(),
                                      model_dir=os.getcwd())

model.load_weights(filepath="mask_rcnn_coco.h5", 
                   by_name=True)

image = cv2.imread("test.jpg")
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

r = model.detect([image], verbose=0)

r = r[0]

r2 = r.copy()

r2[:, 0] = r2[:, 0] * image.shape[0]
r2[:, 2] = r2[:, 2] * image.shape[0]
r2[:, 1] = r2[:, 1] * image.shape[1]
r2[:, 3] = r2[:, 3] * image.shape[1]

mrcnn_directed.visualize.display_instances(image=image, 
                                           boxes=r2)

代码运行后,输入图像将显示区域建议的边界框。区域建议的数量是 1000,可以通过设置SimpleConfig类中的POST_NMS_ROIS_INFERENCE属性来更改。

以下是模型返回的前 4 个区域建议:

r[0]
Out[8]: array([0.49552074, 0.0, 0.53763664, 0.09105143], dtype=float32)

r[1]
Out[9]: array([0.5294977, 0.39210293, 0.63644147, 0.44242138], dtype=float32)

r[2]
Out[10]: array([0.36204672, 0.40500385, 0.6706183 , 0.54514766], dtype=float32)

r[3]
Out[11]: array([0.48107424, 0.08110721, 0.51513755, 0.17086479], dtype=float32)

区域的坐标范围从 0.0 到 1.0。为了将它们返回到原始比例,区域坐标乘以图像的宽度和高度。以下是新的值:

r2[0]
array([144.19653, 0.0, 156.45226, 40.517887], dtype=float32)

r2[1]
Out[5]: array([154.08383, 174.48581, 185.20447, 196.87752], dtype=float32)

r2[2]
Out[6]: array([105.3556, 180.22672, 195.14992, 242.59071], dtype=float32)

r2[3]
Out[7]: array([139.9926, 36.09271, 149.90503, 76.03483], dtype=float32)

请注意,有大量的区域需要处理。如果这个数字减少了,那么模型就会快很多。下一节将讨论如何指导 Mask R-CNN 模型,其中用户告诉模型要在哪些区域中搜索对象。

定向屏蔽 R-CNN

与使用 RPN 搜索区域提议的掩码 R-CNN 模型相比,定向掩码 R-CNN 仅在一些用户定义的区域内搜索。因此,定向掩模 R-CNN 适用于对象可以位于一些预定义的区域集合中的应用。定向屏蔽 R-CNN 的代码可在这里获得。

我们可以从一个实验开始,在这个实验中,模型被迫只保留前 4 个区域建议。这里的想法是将保存区域提议的张量乘以掩码,对于前 4 个区域提议的坐标,该掩码被设置为 1.0,否则为 0.0。

ProposalLayer结束时,正好在return proposals行之前,可以使用以下 3 行。

zeros = np.zeros(shape=(1, self.config.POST_NMS_ROIS_INFERENCE, 4), dtype=np.float32)
zeros[0, :4, :] = 1.0

proposals = KL.Multiply()([proposals, tf.convert_to_tensor(zeros)])

这里是只使用了 4 个区域的ProposalLayer的新代码。

class ProposalLayer(KE.Layer):
    def __init__(self, proposal_count, nms_threshold, config=None, **kwargs):
        super(ProposalLayer, self).__init__(**kwargs)
        self.config = config
        self.proposal_count = proposal_count
        self.nms_threshold = nms_threshold

    def call(self, inputs):
        scores = inputs[0][:, :, 1]
        deltas = inputs[1]
        deltas = deltas * np.reshape(self.config.RPN_BBOX_STD_DEV, [1, 1, 4])
        anchors = inputs[2]

        pre_nms_limit = tf.minimum(self.config.PRE_NMS_LIMIT, tf.shape(anchors)[1])
        ix = tf.nn.top_k(scores, pre_nms_limit, sorted=True,
                         name="top_anchors").indices
        scores = utils.batch_slice([scores, ix], lambda x, y: tf.gather(x, y),
                                   self.config.IMAGES_PER_GPU)
        deltas = utils.batch_slice([deltas, ix], lambda x, y: tf.gather(x, y),
                                   self.config.IMAGES_PER_GPU)
        pre_nms_anchors = utils.batch_slice([anchors, ix], lambda a, x: tf.gather(a, x),
                                    self.config.IMAGES_PER_GPU,
                                    names=["pre_nms_anchors"])

        boxes = utils.batch_slice([pre_nms_anchors, deltas],
                                  lambda x, y: apply_box_deltas_graph(x, y),
                                  self.config.IMAGES_PER_GPU,
                                  names=["refined_anchors"])

        window = np.array([0, 0, 1, 1], dtype=np.float32)
        boxes = utils.batch_slice(boxes,
                                  lambda x: clip_boxes_graph(x, window),
                                  self.config.IMAGES_PER_GPU,
                                  names=["refined_anchors_clipped"])

        def nms(boxes, scores):
            indices = tf.image.non_max_suppression(
                boxes, scores, self.proposal_count,
                self.nms_threshold, name="rpn_non_max_suppression")
            proposals = tf.gather(boxes, indices)
            # Pad if needed
            padding = tf.maximum(self.proposal_count - tf.shape(proposals)[0], 0)
            proposals = tf.pad(proposals, [(0, padding), (0, 0)])
            return proposals
        proposals = utils.batch_slice([boxes, scores], nms,
                                      self.config.IMAGES_PER_GPU)

        zeros = np.zeros(shape=(1, self.config.POST_NMS_ROIS_INFERENCE, 4), dtype=np.float32)
        zeros[0, :4, :] = 1.0

        proposals = KL.Multiply()([proposals, tf.convert_to_tensor(zeros)])

        return proposals

    def compute_output_shape(self, input_shape):
        return (None, self.proposal_count, 4)

下图显示了 4 个区域。

也可以通过硬编码它们的值来为模型提供您自己的自定义坐标,但是它们必须在 0.0 和 1.0 之间缩放。

以下代码分配 5 个区域建议的静态坐标值。

zeros = np.zeros(shape=(1, self.config.POST_NMS_ROIS_INFERENCE, 4), dtype=np.float32)

zeros[0, 0, :] = [0.0,  0.0 ,  0.2,   0.3]
zeros[0, 1, :] = [0.42, 0.02,  0.8,   0.267]
zeros[0, 2, :] = [0.12, 0.52,  0.55,  0.84]
zeros[0, 3, :] = [0.61, 0.71,  0.87,  0.21]
zeros[0, 4, :] = [0.074, 0.83, 0.212, 0.94]

proposals = tf.convert_to_tensor(zeros)

ProposalLayer的代码现在可以简化为以下内容。

class ProposalLayer(KE.Layer):
    def __init__(self, proposal_count, nms_threshold, config=None, **kwargs):
        super(ProposalLayer, self).__init__(**kwargs)
        self.config = config

    def call(self, inputs):
        zeros = np.zeros(shape=(1, self.config.POST_NMS_ROIS_INFERENCE, 4), dtype=np.float32)

        zeros[0, 0, :] = [0.0,  0.0 ,  0.2,   0.3]
        zeros[0, 1, :] = [0.42, 0.02,  0.8,   0.267]
        zeros[0, 2, :] = [0.12, 0.52,  0.55,  0.84]
        zeros[0, 3, :] = [0.61, 0.71,  0.87,  0.21]
        zeros[0, 4, :] = [0.074, 0.83, 0.212, 0.94]

        proposals = tf.convert_to_tensor(zeros)

        return proposals

下图显示了用户指定的 5 个区域的边界框。这样,用户迫使模型在预定义的区域内搜索对象。

注意,Config类中的POST_NMS_ROIS_INFERENCE属性的默认值是 1000,这意味着前面的代码在 1000 个区域中搜索对象。如果您想在特定数量的区域中搜索对象,您可以在Config类中设置POST_NMS_ROIS_INFERENCE属性,如下面的代码所示。这迫使模型只能在 5 个区域上工作。

class SimpleConfig(mrcnn_directed.config.Config):
    NAME = "coco_inference"

    GPU_COUNT = 1
    IMAGES_PER_GPU = 1

    NUM_CLASSES = len(CLASS_NAMES)

    POST_NMS_ROIS_INFERENCE = 5

下一节将讨论与屏蔽 R-CNN 相比,定向屏蔽 R-CNN 的 Python 实现中的变化。

定向屏蔽 R-CNN 的变化

在这里你可以找到所有实现定向屏蔽 R-CNN 的文件。最重要的文件是 model.py

mrcnn 目录相比,有 4 个变化:

  1. config.Config级。
  2. ProposalLayer级。
  3. MaskRCNN级。
  4. visualize.display_instances()法。

第一次更改

第一个变化是 mrcnn_directed.config.Config 类有了一个新的属性叫做 REGION_PROPOSALS 。该属性的默认值是None,这意味着区域提议将由 RPN 生成。 REGION_PROPOSALS 属性在 mrcnn.config.Config 类中不存在。

用户可以设置该属性的值,以传递一些预定义的区域建议,模型将在这些区域中查看。这就是用户如何指导 Mask R-CNN 模型查看某些特定区域。

要使用 REGION_PROPOSALS 属性,首先创建一个形状为(1, POST_NMS_ROIS_INFERENCE, 4)的 NumPy 数组,其中POST_NMS_ROIS_INFERENCE是区域建议的数量。

在下一个代码块中,POST_NMS_ROIS_INFERENCE属性被设置为 5,以便只使用 5 个区域建议。创建新的 NumPy 个零数组,然后指定区域建议的坐标。

POST_NMS_ROIS_INFERENCE = 5

REGION_PROPOSALS = numpy.zeros(shape=(1, POST_NMS_ROIS_INFERENCE, 4), dtype=numpy.float32)

REGION_PROPOSALS[0, 0, :] = [0.49552074, 0\.        , 0.53763664, 0.09105143]
REGION_PROPOSALS[0, 1, :] = [0.5294977 , 0.39210293, 0.63644147, 0.44242138]
REGION_PROPOSALS[0, 2, :] = [0.36204672, 0.40500385, 0.6706183 , 0.54514766]
REGION_PROPOSALS[0, 3, :] = [0.48107424, 0.08110721, 0.51513755, 0.17086479]
REGION_PROPOSALS[0, 4, :] = [0.45803332, 0.15717855, 0.4798005 , 0.20352092]

之后,创建一个 mrcnn_directed.config.Config 类的实例,将 2 个属性POST_NMS_ROIS_INFERENCEREGION_PROPOSALS 设置为前面代码中创建的 2 个属性。这样,模型被迫使用上面定义的区域建议。

注意,如果 REGION_PROPOSALS 设置为None,那么区域建议将由 RPN 生成。这是第一个变化的结束。

class SimpleConfig(mrcnn_directed.config.Config):
    NAME = "coco_inference"
    GPU_COUNT = 1
    IMAGES_PER_GPU = 1
    NUM_CLASSES = len(CLASS_NAMES)

    POST_NMS_ROIS_INFERENCE = POST_NMS_ROIS_INFERENCE
    # If REGION_PROPOSALS is None, then the region proposals are produced by the RPN.
    # Otherwise, the user-defined region proposals are used.
    REGION_PROPOSALS = REGION_PROPOSALS
    # REGION_PROPOSALS = None

第二次变化

第二个变化是除了 mrcnn/model.py 脚本中的 mrcnn/ProposalLayer 类之外, mrcnn_directed/model.py 脚本有了一个新的类,命名为 ProposalLayerDirected

ProposalLayerDirected 类处理在 REGION_PROPOSALS 属性中传递的用户定义的区域建议。 ProposalLayer 类照常处理由 RPN 生成的区域提议。

根据 REGION_PROPOSALS 属性是否设置为None,根据下一个代码决定使用哪一层。如果属性是None,则使用 ProposalLayer 类。否则,使用 ProposalLayerDirected 类。

if type(config.REGION_PROPOSALS) != type(None):
            proposal_count = config.POST_NMS_ROIS_TRAINING if mode == "training"\
                else config.POST_NMS_ROIS_INFERENCE
            rpn_rois = ProposalLayerDirected(proposal_count=proposal_count,
                                             nms_threshold=config.RPN_NMS_THRESHOLD,
                                             name="ROI",
                                             config=config)([rpn_class, rpn_bbox, anchors])
        else:
            proposal_count = config.POST_NMS_ROIS_TRAINING if mode == "training"\
                else config.POST_NMS_ROIS_INFERENCE
            rpn_rois = ProposalLayer(proposal_count=proposal_count,
                                     nms_threshold=config.RPN_NMS_THRESHOLD,
                                     name="ROI",
                                     config=config)([rpn_class, rpn_bbox, anchors])

第三次变化

第三个变化是 mrcnn/model.py 脚本中的 mrcnn/MaskRCNN 类被替换为 mrcnn_directed/model.py 脚本中的 2 个类:

  1. MaskRCNNDirectedRPN :这个类用于只返回区域提议层的输出。根据 REGION_PROPOSALS 属性是否为None,MaskRCNNDirectedRPN类既可以返回由 RPN 生成的区域建议,也可以返回用户定义的区域建议(为了确保一切正常)。
  2. MaskRCNNDirected :该类用于检测返回的区域建议中的对象。根据 REGION_PROPOSALS 属性是否为None,MaskRCNNDirected类可以检测 RPN 生成的区域建议或用户定义的区域建议中的对象。

根据以下代码,两个类 MaskRCNNDirectedRPNMaskRCNNDirected 都以相同的方式实例化:

model = mrcnn_directed.model.MaskRCNNDirectedRPN(mode="inference", 
                                                 config=SimpleConfig(),
                                                 model_dir=os.getcwd())

model = mrcnn_directed.model.MaskRCNNDirected(mode="inference", 
                                              config=SimpleConfig(),
                                              model_dir=os.getcwd())

关于这两个类,真正重要的是detect()方法的输出。对于 MaskRCNNDirectedRPN 类,detect()方法返回一个形状数组(1, POST_NMS_ROIS_INFERENCE, 4)

对于 MaskRCNNDirected 类,detect()方法返回以下形状的 4 个数组:

  1. (POST_NMS_ROIS_INFERENCE, 4)
  2. (ImgHeight, ImgWidth, POST_NMS_ROIS_INFERENCE)
  3. (POST_NMS_ROIS_INFERENCE)
  4. (POST_NMS_ROIS_INFERENCE)

其中ImgHeightImgWidth分别是输入图像的高度和宽度。

第四次变化

第四个变化是,除了visualize.py脚本中的visualize.display_instances()方法,还有一个额外的方法叫做 display_instances_RPN()

display_instances() 方法通过在每个对象上显示边界框、类别标签、预测分数和分段掩码,对检测到的对象进行注释,就像我们之前所做的一样。这里有一个例子:

r = model.detect([image])
r = r[0]

mrcnn_directed.visualize.display_instances(image=image, 
                                           boxes=r['rois'], 
                                           masks=r['masks'], 
                                           class_ids=r['class_ids'], 
                                           class_names=CLASS_NAMES, 
                                           scores=r['scores'])

display_instances_RPN() 方法只是显示每个区域提议上的边界框。与 display_instances() 方法相比,它的争论更少。这里有一个例子:

r = model.detect([image])
r = r[0]

r2 = r.copy()

mrcnn_directed.visualize.display_instances_RPN(image=image, 
                                               boxes=r2)

使用定向屏蔽 R-CNN 的示例

定向屏蔽 R-CNN 模型有两个主要用途:

  1. 仅使用MaskRCNNDirectedRPN类处理区域提议。
  2. 使用区域建议来注释使用MaskRCNNDirected类检测到的对象。

下一个代码块给出了一个使用了MaskRCNNDirectedRPN类的用户定义区域建议的例子。也可以将SimpleConfig类中的REGION_PROPOSALS属性设置为None,以强制模型使用 RPN 生成区域提议。

import mrcnn_directed
import mrcnn_directed.config
import mrcnn_directed.model
import mrcnn_directed.visualize
import cv2
import os
import numpy

CLASS_NAMES = ['BG', 'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light', 'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow', 'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard', 'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple', 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', 'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush']

POST_NMS_ROIS_INFERENCE = 5

REGION_PROPOSALS = numpy.zeros(shape=(1, POST_NMS_ROIS_INFERENCE, 4), dtype=numpy.float32)

REGION_PROPOSALS[0, 0, :] = [0.0,  0.0 ,  0.2,   0.3]
REGION_PROPOSALS[0, 1, :] = [0.42, 0.02,  0.8,   0.267]
REGION_PROPOSALS[0, 2, :] = [0.12, 0.52,  0.55,  0.84]
REGION_PROPOSALS[0, 3, :] = [0.61, 0.71,  0.87,  0.21]
REGION_PROPOSALS[0, 4, :] = [0.074, 0.83, 0.212, 0.94]

class SimpleConfig(mrcnn_directed.config.Config):
    NAME = "coco_inference"
    GPU_COUNT = 1
    IMAGES_PER_GPU = 1
    NUM_CLASSES = len(CLASS_NAMES)

    POST_NMS_ROIS_INFERENCE = POST_NMS_ROIS_INFERENCE
    # If REGION_PROPOSALS is None, then the region proposals are produced by the RPN.
    # Otherwise, the user-defined region proposals are used.
    REGION_PROPOSALS = REGION_PROPOSALS
    # REGION_PROPOSALS = None

model = mrcnn_directed.model.MaskRCNNDirectedRPN(mode="inference", 
                                                 config=SimpleConfig(),
                                                 model_dir=os.getcwd())

model.load_weights(filepath="mask_rcnn_coco.h5", 
                   by_name=True)

image = cv2.imread("test.jpg")
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

r = model.detect([image], verbose=0)

r = r[0]

r2 = r.copy()

r2[:, 0] = r2[:, 0] * image.shape[0]
r2[:, 2] = r2[:, 2] * image.shape[0]
r2[:, 1] = r2[:, 1] * image.shape[1]
r2[:, 3] = r2[:, 3] * image.shape[1]

mrcnn_directed.visualize.display_instances_RPN(image=image, 
                                               boxes=r2)

下一个代码块给出了一个示例,除了处理区域建议之外,还注释了检测到的对象。注意使用了 MaskRCNNDirected 类。此代码可在的处获得。

也可以将SimpleConfig类中的REGION_PROPOSALS属性设置为None,以强制模型使用 RPN 生成区域建议。

import mrcnn_directed
import mrcnn_directed.config
import mrcnn_directed.model
import mrcnn_directed.visualize
import cv2
import os
import numpy

CLASS_NAMES = ['BG', 'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light', 'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow', 'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard', 'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple', 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', 'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush']

POST_NMS_ROIS_INFERENCE = 5

REGION_PROPOSALS = numpy.zeros(shape=(1, POST_NMS_ROIS_INFERENCE, 4), dtype=numpy.float32)

REGION_PROPOSALS[0, 0, :] = [0.49552074, 0\.        , 0.53763664, 0.09105143]
REGION_PROPOSALS[0, 1, :] = [0.5294977 , 0.39210293, 0.63644147, 0.44242138]
REGION_PROPOSALS[0, 2, :] = [0.36204672, 0.40500385, 0.6706183 , 0.54514766]
REGION_PROPOSALS[0, 3, :] = [0.48107424, 0.08110721, 0.51513755, 0.17086479]
REGION_PROPOSALS[0, 4, :] = [0.45803332, 0.15717855, 0.4798005 , 0.20352092]

class SimpleConfig(mrcnn_directed.config.Config):
    NAME = "coco_inference"
    GPU_COUNT = 1
    IMAGES_PER_GPU = 1
    NUM_CLASSES = len(CLASS_NAMES)

    POST_NMS_ROIS_INFERENCE = POST_NMS_ROIS_INFERENCE
    # If REGION_PROPOSALS is None, then the region proposals are produced by the RPN.
    # Otherwise, the user-defined region proposals are used.
    REGION_PROPOSALS = REGION_PROPOSALS
    # REGION_PROPOSALS = None

model = mrcnn_directed.model.MaskRCNNDirected(mode="inference", 
                                              config=SimpleConfig(),
                                              model_dir=os.getcwd())

model.load_weights(filepath="mask_rcnn_coco.h5", 
                   by_name=True)

image = cv2.imread("test.jpg")
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

r = model.detect([image])

r = r[0]

mrcnn_directed.visualize.display_instances(image=image, 
                                           boxes=r['rois'], 
                                           masks=r['masks'], 
                                           class_ids=r['class_ids'], 
                                           class_names=CLASS_NAMES, 
                                           scores=r['scores'])
print(r['rois'].shape)
print(r['masks'].shape)
print(r['class_ids'].shape)
print(r['scores'].shape)

结论

在本教程中,我们看到了如何编辑掩模 R-CNN 来建立一个有向网络,其中用户指定模型应该寻找对象的一些区域。减少模型处理的区域提议的数量减少了模型的计算时间。只有当对象在一些预定义的位置总是可见时,才能应用这一工作,例如车辆停靠站。

参考文献

使用 PyTorch 和 Detectron2 进行对象检测

原文:https://blog.paperspace.com/object-detection-segmentation-with-detectron2-on-paperspace-gradient/

[2021 年 12 月 2 日更新:本文包含关于梯度实验的信息。实验现已被弃用,渐变工作流已经取代了它的功能。请参见工作流程文档了解更多信息。]

在这篇文章中,我们将向你展示如何在渐变上训练 Detectron2 来检测自定义对象,例如渐变上的花朵。我们将向您展示如何标记自定义数据集以及如何重新训练您的模型。在我们训练它之后,我们将尝试在 Gradient 上推出一个带有 API 的推理服务器。

参考回购https://github.com/Paperspace/object-detection-segmentation

文章大纲

  • 检测器 2 概述
  • 构建自定义数据集概述
  • 如何标注自定义数据集
  • 注册自定义检测器 2 对象检测数据
  • 在坡度上运行探测器 2 训练
  • 对梯度运行检测器 2 推理

检测器 2 概述

Detectron2 是一个流行的基于 PyTorch 的模块化计算机视觉模型库。这是 Detectron 的第二个版本,最初是用 Caffe2 编写的。Detectron2 系统允许您将最先进的计算机视觉技术插入到您的工作流程中。引用 Detectron2 发布博客:

Detectron2 包括原始 Detectron 中可用的所有型号,如更快的 R-CNN、Mask R-CNN、RetinaNet 和 DensePose。它还具有几个新的模型,包括级联 R-CNN,全景 FPN 和张量掩模,我们将继续添加更多的算法。我们还增加了一些功能,如同步批处理规范和对 LVIS 等新数据集的支持

构建自定义数据集概述

下载数据集

https://public . robo flow . com/classification/flowers _ classification

Roboflow: Computer Vision dataset management tool

我们将在 Roboflow 免费托管的公共花卉检测数据上培训我们的 custom Detectron2 检测器。

花卉数据集是各种花卉种类(如蒲公英和雏菊)的分类检测数据集。值得注意的是,血细胞检测不是 Detectron2 中可用的功能——我们需要训练底层网络来适应我们的定制任务。

使用标注对数据集进行标注

https://github.com/tzutalin/labelImg

准确标记的数据对于成功的机器学习至关重要,计算机视觉也不例外。

在这一节中,我们将演示如何使用 LabelImg 开始为对象检测模型标记您自己的数据。

关于标签制作

LabelImg 是一个免费的开源工具,用于图形化标记图像。它是用 Python 编写的,图形界面使用 QT。这是一个简单、免费的方法,可以标记几百张图片来尝试你的下一个项目。

安装标签

sudo apt-get install pyqt5-dev-tools
sudo pip3 install -r requirements/requirements-linux-python3.txt
make qt5py3
python3 labelImg.py
python3 labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE] 

用标签标记图像

LabelImg 支持 VOC XML 或 YOLO 文本文件格式的标签。在 Paperspace,我们强烈建议您使用默认的 VOC XML 格式来创建标签。多亏了 ImageNet,VOC XML 成为了一个更通用的标准,因为它与对象检测相关,而各种 YOLO 实现具有稍微不同的文本文件格式。

通过选择 LabelImg 左侧的“打开目录”打开您想要的图像集

要启动标签,请键入 w,并绘制所需的标签。然后,键入 ctrl(或 command) S 保存标签。键入 d 转到下一幅图像(键入 a 返回一幅图像)。

将 VOX XML 保存到 coco 数据集 JSON 中

在我们的https://github.com/Paperspace/object-detection-segmentation中的 数据集 中,你会发现一个 coco_labelme.py 脚本,它将从我们之前生成的 xml 文件中创建一个 coco 数据集 json。

为了运行它:

python3 数据集/coco _ label me . py/path _ to _ your _ images/"-output = " trainval . JSON "

上传您的自定义数据集

现在,我们必须将自定义数据集上传到 s3 bucket。

在这里您可以找到如何使用 AWS-CLI 或 web 控制台将您的数据上传到 S3 的说明

注册数据集

为了让 detectron2 知道如何获得名为“my_dataset”的数据集,用户需要实现一个函数来返回数据集中的项目,然后告诉 detectron2 这个函数:

def my_dataset_function():
...
return list[dict] in the following format
from detectron2.data import DatasetCatalog
DatasetCatalog.register("my_dataset", my_dataset_function)
# later, to access the data:
data: List[Dict] = DatasetCatalog.get("my_dataset") 

这里,代码片段将名为“my_dataset”的数据集与返回数据的函数相关联。如果多次调用,该函数必须返回相同的数据。注册将一直有效,直到进程退出。

如何将 my_dataset 数据集注册到 detectron2,遵循 detectron2 自定义数据集教程

from detectron2.data.datasets import register_coco_instances
register_coco_instances("my_dataset", {}, "./data/trainval.json", "./data/images") 

每个数据集都与一些元数据相关联。在我们的例子中,可以通过调用my _ dataset _ metadata = metadata catalog . get(" my _ dataset ")来访问它,您将得到

Metadata(evaluator_type='coco', image_root='./data/images', json_file='./data/trainval.json', name=my_dataset,
thing_classes=[‘type1’, ‘type2’, ‘type3’], thing_dataset_id_to_contiguous_id={1: 0, 2: 1, 3: 2}) 

要获得关于数据集的目录存储信息的实际内部表示以及如何获得它们,可以调用 dataset _ dicts = dataset catalog . get("my _ dataset")。内部格式使用一个 dict 来表示一个图像的注释。

为了验证数据加载是否正确,让我们可视化数据集中随机选择的样本的注释:

import random
from detectron2.utils.visualizer import Visualizer
for d in random.sample(dataset_dicts, 3):
img = cv2.imread(d["file_name"])
visualizer = Visualizer(img[:, :, ::-1], metadata=my_dataset_metadata, scale=0.5)
vis = visualizer.draw_dataset_dict(d)
cv2_imshow(vis.get_image()[:, :, ::-1]) 

训练模型

现在,让我们在 my_dataset 数据集上微调 coco 预训练的 R50-FPN 掩模 R-CNN 模型。根据数据集的复杂程度和大小,可能需要 5 分钟到几小时。

预训练模型可从以下网址下载:

detectron2://COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x/137849600/model_final_f10217.pkl 
from detectron2.engine import DefaultTrainer
from detectron2.config import get_cfg
import os
cfg = get_cfg()
cfg.merge_from_file(
"./detectron2_repo/configs/COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml"
)
cfg.DATASETS.TRAIN = ("my_dataset",)
cfg.DATASETS.TEST = ()
cfg.DATALOADER.NUM_WORKERS = 2
cfg.MODEL.WEIGHTS = "detectron2://COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x/137849600/model_final_f10217.pkl"  # initialize from model zoo
cfg.SOLVER.IMS_PER_BATCH = 2
cfg.SOLVER.BASE_LR = 0.02
cfg.SOLVER.MAX_ITER = (
300
)  # 300 iterations seems good enough, but you can certainly train longer
cfg.MODEL.ROI_HEADS.BATCH_SIZE_PER_IMAGE = (
128
)  # faster, and good enough for this toy dataset
cfg.MODEL.ROI_HEADS.NUM_CLASSES = 3  # 3 classes (data, fig, hazelnut)
os.makedirs(cfg.OUTPUT_DIR, exist_ok=True)
trainer = DefaultTrainer(cfg)
trainer.resume_or_load(resume=False)
trainer.train() 

在坡度上跑训练

渐变 CLI 安装

如何安装渐变 CLI - docs

pip install gradient
Then make sure to obtain an API Key, and then:
gradient apiKey XXXXXXXXXXXXXXXXXXX 

在单个 GPU 上训练

注意:单人训练需要很长时间,所以做好等待的准备!

gradient experiments run singlenode \
--name mask_rcnn \
--projectId <some project> \
--container devopsbay/detectron2-cuda:v0 \
--machineType P4000 \
--command "sudo python training/train_net.py --config-file training/configs/mask_rcnn_R_50_FPN_1x.yaml --num-gpus 1 SOLVER.IMS_PER_BATCH 2 SOLVER.BASE_LR 0.0025 MODEL.WEIGHTS https://dl.fbaipublicfiles.com/detectron2/COCO-Detection/faster_rcnn_R_50_FPN_1x/137257794/model_final_b275ba.pkl  OUTPUT_DIR /artifacts/models/detectron" \
--workspace https://github.com/Paperspace/object-detection-segmentation.git \
--datasetName my_dataset \
--datasetUri <Link_to_your_dataset> \
--clusterId <cluster id> 

数据集被下载到。/data/ <你的数据集>目录。模型结果存储在。/models 目录。

如果一切设置正确,您应该会看到如下内容:

张量板支持

梯度支持 Tensorboard 开箱即用

OverviewVisualize and compare experiments with TensorBoardsGradient Docs

在实验页面上,您可以创建新的 Tensorboard,只需点击“添加到 Tensorboard”即可实时查看数据,即使在训练仍在进行中。

在 Tensorboard 中,您可以查看精度:

Live metrics during training

你也可以在他们训练的时候现场对比多个模特!

多 GPU 训练

规模至关重要

为了加快这个过程,我们必须在多 GPU 设置中运行训练。

为此,让我们在梯度专用集群上运行一个实验,为此我们需要添加几个额外的参数:

gradient experiments run multinode \
  --name mask_rcnn_multinode \
  --projectId <some project> \
  --workerContainer devopsbay/detectron2:v1 \
  --workerMachineType P4000 \
  --workerCount 7 \
  --parameterServerContainer devopsbay/detectron2:v1 \
  --parameterServerMachineType P4000 \
  --parameterServerCount 1 \
  --experimentType GRPC \
  --workerCommand "python training/train_net.py --config-file training/configs/mask_rcnn_R_50_FPN_3x.yaml MODEL.WEIGHTS https://dl.fbaipublicfiles.com/detectron2/COCO-Detection/faster_rcnn_R_50_FPN_1x/137257794/model_final_b275ba.pkl  OUTPUT_DIR /artifacts/models/detectron" \
  --parameterServerCommand "python training/train_net.py --config-file training/configs/mask_rcnn_R_50_FPN_3x.yaml MODEL.WEIGHTS https://dl.fbaipublicfiles.com/detectron2/COCO-Detection/faster_rcnn_R_50_FPN_1x/137257794/model_final_b275ba.pkl  OUTPUT_DIR /artifacts/models/detectron" \
  --workspace https://github.com/Paperspace/object-detection-segmentation.git \
  --datasetName small_coco \
  --datasetUri s3://paperspace-tiny-coco/small_coco.zip \
  --clusterId <cluster id> 

这将在 8x P4000 上启动多 GPU 分布式培训。

python training/train_net.py --config-file training/configs/mask_rcnn_R_50_FPN_3x.yaml --num-gpus 1 MODEL.WEIGHTS https://dl.fbaipublicfiles.com/detectron2/COCO-Detection/faster_rcnn_R_50_FPN_1x/137257794/model_final_b275ba.pkl  OUTPUT_DIR /artifacts/models/detectron 

必须在培训中涉及的所有机器上调用该函数。它将在每台机器上产生子进程(由num-GPU定义)。

参数

  • num-GPU(int)–每台机器的 GPU 数量
  • args(tuple)–传递给 main_func 的参数

如何扩展您的配置以获得最佳加速?

当配置是为不同于当前使用的工作线程数的特定工作线程数(根据**cfg.SOLVER.REFERENCE_WORLD_SIZE**)定义时,返回一个新的 cfg,其中总批处理大小被缩放,以便每个 GPU 的批处理大小与原始的**IMS_PER_BATCH // REFERENCE_WORLD_SIZE**保持相同。

其他配置选项也相应调整:*训练步数和热身步数成反比。*学习率按比例缩放。

例如,原始配置如下所示:

IMS_PER_BATCH: 16
BASE_LR: 0.1
REFERENCE_WORLD_SIZE: 8
MAX_ITER: 5000
STEPS: (4000,)
CHECKPOINT_PERIOD: 1000 

当在 16 个 GPU 上使用此配置而不是参考数字 8 时,调用此方法将返回一个新的配置,其中包含:

IMS_PER_BATCH: 32
BASE_LR: 0.2
REFERENCE_WORLD_SIZE: 16
MAX_ITER: 2500
STEPS: (2000,)
CHECKPOINT_PERIOD: 500 

请注意,原始配置和这个新配置都可以在 16 个 GPU 上进行训练。由用户决定是否启用该功能(通过设置**REFERENCE_WORLD_SIZE**)。

比较起来怎么样?

在本例中,我们比较了单个 P4000 和 4 个 p 4000。

你可以看到在重新训练模型时的速度差异——由于模型已经在更早的时候被训练过,开始时的准确率非常高,约为 90%。

部署训练模型

如果在训练部分结束时一切运行正常,我们应该在梯度仪表板中看到一个训练好的模型

Successfully trained model

如何在梯度上部署模型

此示例将加载之前训练的模型,并启动一个具有简单界面的 web app 应用程序。在我们解释它如何工作之前,让我们在平台上启动它!

deployments create /
--name paperspace-detectron-demo-app /
--instanceCount 1 /
--imageUrl devopsbay/detectron2-cuda:v0 /
--machineType V100 /
--command "pip3 install -r demo/requirements.txt && python demo/app.py" /
--workspace https://github.com/Paperspace/object-detection-segmentation.git 
--deploymentType Custom 
--clusterId <cluster id> 
--modelId <model id> 
--ports 8080

Successfully deployed App

因此,在这篇文章中,我们将为 detectron2 的实例分段创建一个 web 应用程序。

后端

首先,我们将创建机器学习后端。这将使用基本的烧瓶。我们将从一些相当标准的样板代码开始。

import io
from flask import Flask, render_template, request, send_from_directory, send_file
from PIL import Image
import requests
import os
import urllib.request

app = Flask(__name__)

@app.route("/")
def index():

	# render the index.html template
	return render_template('index.html')

if __name__ == "__main__":

	# get port. Default to 8080
	port = int(os.environ.get('PORT', 8080))

	# set debug level
	logging.basicConfig(level=logging.DEBUG)

	# run app
	app.run(host='0.0.0.0', port=port) 

这个应用程序将简单地呈现模板index.html。我已经手动指定了端口。

接下来,我们将添加函数来获取图像。我们希望能够上传图像到网站。我们也希望能够提供一个网址和图像将自动下载的网站。我已经创建了如下代码。

@app.route("/detect", methods=['POST', 'GET'])
def upload():
    if request.method == 'POST':

        try:

            # open image
            file = Image.open(request.files['file'].stream)

            # remove alpha channel
            rgb_im = file.convert('RGB')
            rgb_im.save('file.jpg')

        # failure
        except:

            return render_template("failure.html")

    elif request.method == 'GET':

        # get url
        url = request.args.get("url")

        # save
        try:
            # save image as jpg
            # urllib.request.urlretrieve(url, 'file.jpg')
            rgb_im = load_image_url(url)
            rgb_im = rgb_im.convert('RGB')
            rgb_im.save('file.jpg')

        # failure
        except:
            return render_template("failure.html")

    # run inference
    # result_img = run_inference_transform()
    try:
        result_img = run_inference('file.jpg')

        # create file-object in memory
        file_object = io.BytesIO()

        # write PNG in file-object
        result_img.save(file_object, 'PNG')

        # move to beginning of file so `send_file()` it will read from start
        file_object.seek(0)
    except Exception as e:
        app.logger.critical(str(e))
        return render_template("failure.html")

    return send_file(file_object, mimetype='image/jpeg') 

这段代码允许我们将图像上传到后端(POST 请求)。或者我们可以提供一个 url 的后端,它会自动下载图像(获取请求)。该代码还将图像转换成一个jpg。我无法使用 detectron2 对一张png图像进行推断。所以我们必须转换成一个jpg

如果代码因为某种原因不能下载图像,它将返回failure.html模板。这基本上只是一个简单的html页面,说明在检索图像时出现了错误。

另外,我指定了一个不同的@app.route。这将需要反映在index.html文件中。

前端

现在我将创建前端html代码。该界面允许用户上传图像或指定图像的 URL。

<h2>Detectron2 Instance Segmentation</h2>
<p>Upload your file</p>
<form action="/model-serving/{{ model_id}}/detect" method="POST" enctype="multipart/form-data">

<div class="form-group">
<label for="file1">File</label>

Select Image
</div>
Submit
</form> 

模型

在这一部分中,我们将得到一个之前训练的 detectron2 模型来对图像进行推理。然后我们将它链接到我们现有的后端。

这部分稍微复杂一点。我们将创建一个名为Detector的新类。我们将创建探测器 2 所需的cfg。然后,我们将创建另一个函数来对图像进行推理。

首先,我们必须从 repo 加载模型配置:

# obtain detectron2's default config
self.cfg = get_cfg() 

# Load Model Config
self.model = os.getenv('MODEL_CONFIG', 'mask_rcnn_R_50_FPN_3x.yaml')
# load values from a file
self.cfg.merge_from_file(model_zoo.get_config_file("COCO-InstanceSegmentation/"+self.model)) 

然后我们要设置是要在 CPU 还是 GPU 上运行推理。请注意,在 GPU 上训练的模型在 CPU 上无法正常工作。

# Additional Info when using cuda
if torch.cuda.is_available():
    self.cfg.MODEL.DEVICE = "cuda"
else:
# set device to cpu
    self.cfg.MODEL.DEVICE = "cpu" 

下一阶段是加载我们之前训练过的训练模型文件:

# get Model from paperspace trained model directory
model_path = os.path.abspath('/models/model/detectron/model_final.pth')
if os.path.isfile(model_path):
    print('Using Trained Model {}'.format(model_path), flush=True)
else:
    # Load default pretrained model from Model Zoo
    print('No Model Found at {}'.format(model_path), flush=True)
    model_path = model_zoo.get_checkpoint_url("COCO-InstanceSegmentation/"+self.model)

self.cfg.MODEL.WEIGHTS = model_path

就是这样!

现在让我们创建推理函数

# detectron model
def inference(self, file):

    predictor = DefaultPredictor(self.cfg)
	im = cv.imread(file)
	rgb_image = im[:, :, ::-1]
	outputs = predictor(rgb_image)

	# get metadata
	metadata = MetadataCatalog.get(self.cfg.DATASETS.TRAIN[0])
	# visualise
	v = Visualizer(rgb_image[:, :, ::-1], metadata=metadata, scale=1.2)
	v = v.draw_instance_predictions(outputs["instances"].to("cpu"))

	# get image 
	img = Image.fromarray(np.uint8(v.get_image()[:, :, ::-1]))

	return img 

我们还想跟踪我们的服务收到了多少请求——我们使用了 Paperspace Utils 的内置度量系统

if logger:
    # Push Metrics
    logger["inference_count"].inc()
    logger.push_metrics()
    print('Logged Inference Count', flush=True)

Service ready for action

最终结果:

使用 PyTorch 进行对象定位,第 2 部分

原文:https://blog.paperspace.com/object-localization-pytorch-2/

图像定位对我来说是一个有趣的应用,因为它正好介于图像分类和对象检测之间。这是使用 PyTorch 的对象定位系列的第二部分,所以如果您还没有阅读过前一部分,请务必阅读。

一定要跟着 Gradient 上的 IPython 笔记本走,叉出自己的版本试试看!

数据集可视化

在进入本教程的机器学习部分之前,让我们用边界框来可视化数据集。这里我们可以看到如何通过与图像大小相乘来检索坐标。我们使用 OpenCV 来显示图像。这将打印出 20 个图像及其边界框的样本。

# Create a Matplotlib figure
plt.figure(figsize=(20,20));

# Generate a random sample of images each time the cell is run 
random_range = random.sample(range(1, len(img_list)), 20)

for itr, i in enumerate(random_range, 1):

    # Bounding box of each image
    a1, b1, a2, b2 = boxes[i];
    img_size = 256

    # Rescaling the boundig box values to match the image size
    x1 = a1 * img_size
    x2 = a2 * img_size
    y1 = b1 * img_size
    y2 = b2 * img_size

    # The image to visualize
    image = img_list[i]

    # Draw bounding boxes on the image
    cv2.rectangle(image, (int(x1),int(y1)),
          (int(x2),int(y2)),
                  (0,255,0),
                  3);

    # Clip the values to 0-1 and draw the sample of images
    img = np.clip(img_list[i], 0, 1)
    plt.subplot(4, 5, itr);
    plt.imshow(img);
    plt.axis('off');

Visualization of the dataset

数据集分割

我们已经在 img_list、labels 和 box 中获得了数据集,现在我们必须在进入数据加载器之前分割数据集。像往常一样,我们将使用 sklearn 库中的 train_test_split 方法来完成这个任务。

# Split the data of images, labels and their annotations
train_images, val_images, train_labels, \
val_labels, train_boxes, val_boxes = train_test_split( np.array(img_list), 
                np.array(labels), np.array(boxes), test_size = 0.2, 
                random_state = 43)

print('Training Images Count: {}, Validation Images Count: {}'.format(
    len(train_images), len(val_images) ))

现在,我们已经通过可视化快速浏览了数据集,并完成了数据集拆分。让我们继续为数据集构建自定义 PyTorch 数据加载器,目前数据集分散在变量中。

Pytorch 中的自定义数据加载器

顾名思义,DataLoaders 返回一个对象,该对象将在我们训练模型时处理整个数据提供系统。它在创建对象时提供了像 shuffle 这样的功能,它有一个“getitem”方法,可以处理每次迭代中应该输入的数据,所有这些东西都可以让你按照你希望的方式设计一切,而不会在训练部分使代码变得混乱。这使您可以更专注于其他优化。让我们从进口开始 PyTorch 部分。

from PIL import Image
import torch
import torchvision
from torchvision.transforms import ToTensor
import torchvision.transforms as transforms
import numpy as np
import matplotlib.pyplot as plt

import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import os
import pickle
import random
import time

如果可以的话,重要的事情之一是使用 GPU 来训练 ML 模型,尤其是当目标很大的时候。如果在 Paperspace Gradient 中运行,请选择带有 GPU 的机器。

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
device 

设备(type='cuda ')

上面的输出表明你有一个 GPU,而设备可以用来将数据和模型转换成 GPU 以便利用。继续讨论数据加载器。

class Dataset():
    def __init__(self, train_images, train_labels, train_boxes):
        self.images = torch.permute(torch.from_numpy(train_images),(0,3,1,2)).float()
        self.labels = torch.from_numpy(train_labels).type(torch.LongTensor)
        self.boxes = torch.from_numpy(train_boxes).float()

    def __len__(self):
        return len(self.labels)

    # To return x,y values in each iteration over dataloader as batches.

    def __getitem__(self, idx):
        return (self.images[idx],
              self.labels[idx],
              self.boxes[idx])

# Inheriting from Dataset class

class ValDataset(Dataset):

    def __init__(self, val_images, val_labels, val_boxes):

        self.images = torch.permute(torch.from_numpy(val_images),(0,3,1,2)).float()
        self.labels = torch.from_numpy(val_labels).type(torch.LongTensor)
        self.boxes = torch.from_numpy(val_boxes).float() 

这里我们将创建 Dataset 类,首先加载图像、标签和盒子坐标,缩放到类变量的范围[0-1]。然后,我们使用“getitem”来设计每次迭代中的加载器输出。类似地,我们将构建 ValDataset(验证数据集)数据加载器类。由于数据的结构和性质是相同的,我们将从上面的类继承。

现在我们有了数据加载器的类,让我们马上从各自的类创建数据加载器对象。

dataset = Dataset(train_images, train_labels, train_boxes)
valdataset = ValDataset(val_images, val_labels, val_boxes)

现在我们已经完成了准备数据的旅程,让我们转到教程的机器学习模型部分。我们将使用一组相当简单的卷积神经网络来实现目标定位,这将有助于我们根据自己的理解来调整概念。我们可以尝试这种架构的各个方面,包括使用像 Alexnet 这样的预训练模型。我鼓励你学习更多关于优化器、损失函数和超调模型的知识,以掌握未来项目的架构设计技巧。

模型架构

因此,为了理解我们必须如何设计架构,我们必须首先理解输入和输出。这里输入的是一批图片,所以会像(BS, C, H, W)一样风格化。BS批量大小,然后是通道、高度和重量。顺序很重要,因为在 PyTorch 中,图像就是这样存储的。在 TensorFlow 中,对于每幅图像是(H,W,C)。

说到输出,我们有两个输出,正如我们在上一篇博客开始时所讨论的。首先是你的分类输出,大小为(1, N)N是类的数量。第二个输出的大小为(1, 4),为 xmin、ymin、xmax 和 ymax,但在(0,1)的范围内。这有助于您以后根据图像大小缩放坐标。因此输出将是(CLF, BOX),第一个是上面讨论的分类,另一个是坐标。

现在,从机器学习部分解释,CLF将用于通过在输出上使用 argmax 来获得类索引,并且我们可以在末尾添加 softmax 激活函数用于概率输出。BOX输出将在通过 sigmoid 激活函数后产生,该函数使范围(0,1)。也就是说,其他一切都类似于传统的图像分类模型。让我们看看代码。

class Network(nn.Module):
    def __init__(self):
        super(Network, self).__init__()

        # CNNs for rgb images
        self.conv1 = nn.Conv2d(in_channels=3, out_channels=6, kernel_size=5)
        self.conv2 = nn.Conv2d(in_channels=6, out_channels=12, kernel_size=5)
        self.conv3 = nn.Conv2d(in_channels=12, out_channels=24, kernel_size=5)
        self.conv4 = nn.Conv2d(in_channels=24, out_channels=48, kernel_size=5)
        self.conv5 = nn.Conv2d(in_channels=48, out_channels=192, kernel_size=5)

        # Connecting CNN outputs with Fully Connected layers for classification
        self.class_fc1 = nn.Linear(in_features=1728, out_features=240)
        self.class_fc2 = nn.Linear(in_features=240, out_features=120)
        self.class_out = nn.Linear(in_features=120, out_features=2)

        # Connecting CNN outputs with Fully Connected layers for bounding box
        self.box_fc1 = nn.Linear(in_features=1728, out_features=240)
        self.box_fc2 = nn.Linear(in_features=240, out_features=120)
        self.box_out = nn.Linear(in_features=120, out_features=4)

    def forward(self, t):
        t = self.conv1(t)
        t = F.relu(t)
        t = F.max_pool2d(t, kernel_size=2, stride=2)

        t = self.conv2(t)
        t = F.relu(t)
        t = F.max_pool2d(t, kernel_size=2, stride=2)

        t = self.conv3(t)
        t = F.relu(t)
        t = F.max_pool2d(t, kernel_size=2, stride=2)

        t = self.conv4(t)
        t = F.relu(t)
        t = F.max_pool2d(t, kernel_size=2, stride=2)

        t = self.conv5(t)
        t = F.relu(t)
        t = F.avg_pool2d(t, kernel_size=4, stride=2)

        t = torch.flatten(t,start_dim=1)

        class_t = self.class_fc1(t)
        class_t = F.relu(class_t)

        class_t = self.class_fc2(class_t)
        class_t = F.relu(class_t)

        class_t = F.softmax(self.class_out(class_t),dim=1)

        box_t = self.box_fc1(t)
        box_t = F.relu(box_t)

        box_t = self.box_fc2(box_t)
        box_t = F.relu(box_t)

        box_t = self.box_out(box_t)
        box_t = F.sigmoid(box_t)

        return [class_t,box_t]

接下来让我们实例化这个模型,并让它使用 GPU(如果可用的话)。这确实可以加速训练过程,特别是对于像图像定位这样的大目标。之后我们还将看到模型摘要。

model = Network()
model = model.to(device)
model

我们可以在训练后使用 ONNX 格式和 netron 网站可视化模型,以评估复杂的细节,如重量等。现在,我们可以编写一个小函数来计算每批训练的正确预测数。

def get_num_correct(preds, labels):
    return torch.round(preds).argmax(dim=1).eq(labels).sum().item()

现在,我们将为训练和验证数据集创建数据加载器,以便在训练时输入成批的图像。

dataloader = torch.utils.data.DataLoader(
       dataset, batch_size=32, shuffle=True)
valdataloader = torch.utils.data.DataLoader(
       valdataset, batch_size=32, shuffle=True)

在这里,我们可以在 Dataloader 方法中有更多的参数,如 num_workers ,这可以帮助我们改善加载时间,其中 2 通常是使数据加载过程流水线化的最佳值。现在我们要创建训练函数,这是每个 ML 项目中最重要的部分。我们将保持流程简单,便于所有人理解培训的每个部分。我们将在整个过程中专注于提高验证的准确性。

模型训练和验证

def train(model):
    # Defining the optimizer
    optimizer = optim.SGD(model.parameters(),lr = 0.1)
    num_of_epochs = 30
    epochs = []
    losses = []
    # Creating a directory for storing models
    os.mkdir('models')
    for epoch in range(num_of_epochs):
        tot_loss = 0
        tot_correct = 0
        train_start = time.time()
        model.train()
        for batch, (x, y, z) in enumerate(dataloader):
        	# Converting data from cpu to GPU if available to improve speed
            x,y,z = x.to(device),y.to(device),z.to(device)
            # Sets the gradients of all optimized tensors to zero
            optimizer.zero_grad()
            [y_pred,z_pred]= model(x)
            # Compute loss (here CrossEntropyLoss)
            class_loss = F.cross_entropy(y_pred, y)
            box_loss = F.mse_loss(z_pred, z)
            (box_loss + class_loss).backward()
            # class_loss.backward()
            optimizer.step()
            print("Train batch:", batch+1, " epoch: ", epoch, " ",
                  (time.time()-train_start)/60, end='\r')

        model.eval()
        for batch, (x, y,z) in enumerate(valdataloader):
        	# Converting data from cpu to GPU if available to improve speed	
            x,y,z = x.to(device),y.to(device),z.to(device)
            # Sets the gradients of all optimized tensors to zero
            optimizer.zero_grad()
            with torch.no_grad():
                [y_pred,z_pred]= model(x)

                # Compute loss (here CrossEntropyLoss)
                class_loss = F.cross_entropy(y_pred, y)
                box_loss = F.mse_loss(z_pred, z)
                # Compute loss (here CrossEntropyLoss)

            tot_loss += (class_loss.item() + box_loss.item())
            tot_correct += get_num_correct(y_pred, y)
            print("Test batch:", batch+1, " epoch: ", epoch, " ",
                  (time.time()-train_start)/60, end='\r')
        epochs.append(epoch)
        losses.append(tot_loss)
        print("Epoch", epoch, "Accuracy", (tot_correct)/2.4, "loss:",
              tot_loss, " time: ", (time.time()-train_start)/60, " mins")
        torch.save(model.state_dict(), "model_ep"+str(epoch+1)+".pth")

让我们详细讨论一下这个问题。我们将使用的优化器是随机梯度下降,但是如果您愿意,您可以尝试其他优化技术,如 Adam。

我们将分别处理培训和验证。为了在训练期间跟踪时间,我们可以使用 train_start 变量。这在计算训练时间的付费 GPU 实例上非常有用,这将帮助您在多次再训练之前提前计划。

默认情况下,PyTorch 模型设置为 train (self.training = True)。当我们到达代码的验证部分时,我们将讨论为什么切换状态及其影响。

从我们创建的数据加载器中,我们访问由 x、y 和 z 组成的每批数据,它们分别是图像、标签和边界框。然后,我们将它们转换成我们喜欢的设备,例如,如果 GPU 可用的话。优化器处理深度学习中的反向传播,因此在训练之前,我们通过 optimizer.zero_grad()将每批的梯度设置为零。

将输入(x)输入到模型后,我们进入损耗计算。这是一个重要的部分,因为这里涉及两种类型的损失:分类问题的交叉熵损失,以及寻找边界框坐标的回归部分的均方误差。如果我们在这里观察,我们可以看到我们如何将损失的总和发送给反向传播,而不是个体治疗。

之后,代码的评估部分类似于训练部分,除了我们不做反向传播。一旦我们转移到验证部分,我们使用 model.eval() 将模型状态切换到 eval。正如我们前面所讨论的,默认情况下,模型处于训练状态,切换非常重要。一些层在训练/和评估期间具有不同的行为(如 BatchNorm、Dropout ),从而影响整体性能。这是一个有趣的点,因为当模型处于训练状态时,BatchNorm 使用每批统计数据,并且删除层被激活。但是当模型处于评估(推断)模式时,BatchNorm 层使用运行统计,而 Dropout 层被停用。这里需要注意的是,这两个函数调用都不运行向前/向后传递。它们告诉模型在运行时如何行动。这一点很重要,因为一些模块(层)被设计为在训练和推理期间表现不同,如果在错误的模式下运行,模型将产生意想不到的结果。在这里,我们使用之前编写的函数来计算准确性,以找到正确预测的数量。

在这个例子中,我们将训练 30 个历元,但是这是任意的,我们可以自由地试验更长或更短的历元值。

关于培训过程,我们必须选择的最后一件事是如何保存模型。在 PyTorch 中,我们可以用两种方式保存模型。一个是我们保存整个模型,包括重量、结构等等;如果我们确定 PyTorch 的生产版本与开发版本匹配,这是完美的。在某些情况下,当大小限制在 500MB 左右时,以这种方式保存的 PyTorch 模型对于部署来说可能太重了。我们保存模型的另一种方法是通过创建一个 state_dict(),,它只将模型的可学习参数的状态保存为 python 字典。这使得它在形式上更加模块化,就像在生产中一样,为了复活模型,您必须提供模型的 state_dict()和模型架构(在我们的例子中是网络)。这几乎就像提供肌肉和骨骼来复活一个生命,这是方便和灵活的,因为我们可以决定 PyTorch 版本,如果需要,可以使用 PyTorch cpu 专用版本,这将减少整个包的大小,这种方法非常有用。我鼓励你多读一些关于的文章来拓宽你的理解。也就是说,如果没有内存存储限制,保存所有模型数据是最安全的方法。否则选择像提前停止这样的技术会帮助你得到最好的模型而不会错过它。

一旦您理解了培训功能,我们就开始培训吧。

train(model)

这里需要理解的一件重要事情是,如果您希望重新训练,您必须重新初始化模型。没有该步骤的重新训练只会增加现有的模型参数。因此,处理这个潜在问题的一个好方法是在训练之前初始化模型,以便再次重复相同的过程。

验证的准确性取决于很多因素,第一次尝试可能得不到好的结果。我提供的用于测试实现的数据集是最小的数据集,您必须有一个更加精细和通用的数据集来对您的模型进行真正的测试。其他一些改进模型的方法是通过数据扩充技术等。我已经写了一篇关于提高精确度的详细文章,所以如果你的模型仍然需要改进的话,可以读一读。

模型测试/推理

当谈到图像定位时,预测不仅仅是从模型中获得输出。我们还必须处理边界框坐标,以生成一个实际的边界框来可视化结果,这甚至可能有助于生产。本节将有 3 个主要组件,预测脚本、预处理和后处理脚本,所以让我们开始吧。

预处理

在处理机器学习产品时,输入模型的数据必须与训练时输入的数据进行完全相同的预处理,这一点很重要。在推理阶段,调整大小是任何图像相关模型最常见的预处理之一。这里我们的图像尺寸是 256。

def preprocess(img, image_size = 256):

    image = cv2.resize(img, (image_size, image_size))
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    image = image.astype("float") / 255.0 

    # Expand dimensions as predict expect image in batches
    image = np.expand_dims(image, axis=0) 
    return image

后加工

一旦我们得到输出,用更简单的术语来说,它们将是[CLF, BOX]的形式,我们将不得不使用边界框值来创建可视化的结果。边界框输入被缩放到范围[0,1],并且,当我们在最后使用 sigmoid 激活函数时,我们的预测也在范围[0,1]内。我们将不得不重新调整它们以得到 xmin,ymin 等。为此,我们只需将这些值乘以图像大小(此处为 256)。

def postprocess(image, results):

    # Split the results into class probabilities and box coordinates
    [class_probs, bounding_box] = results

    # First let's get the class label

    # The index of class with the highest confidence is our target class
    class_index = torch.argmax(class_probs)

    # Use this index to get the class name.
    class_label = num_to_labels[class_index]

    # Now you can extract the bounding box too.

    # Get the height and width of the actual image
    h, w = 256,256

    # Extract the Coordinates
    x1, y1, x2, y2 = bounding_box[0]

    # # Convert the coordinates from relative (i.e. 0-1) to actual values
    x1 = int(w * x1)
    x2 = int(w * x2)
    y1 = int(h * y1)
    y2 = int(h * y2)

    # return the lable and coordinates
    return class_label, (x1,y1,x2,y2),torch.max(class_probs)*100

现在我们已经有了预处理和后处理,让我们进入预测脚本。

预测脚本

在预测脚本中,我们首先从早期的网络中获得模型架构,然后将模型移动到我们首选的设备:Gradient 笔记本中的 GPU。一旦完成,我们加载模型的 state_dict() ,就像我们之前在验证中讨论的那样。我们将模型设置为 eval 状态,并使用预处理功能将图像准备好输入模型。然后我们可以使用 PyTorch 中的 permute 函数将图像数组从[N,H,W,C]重新制作成[N,C,H,W]。结果被提供给后处理函数,后者返回实际的坐标和标签。最后,我们使用 matplotlib 绘制带有边界框的结果图像。

# We will use this function to make prediction on images.
def predict(image,  scale = 0.5):
  model = Network()
  model = model.to(device)
  model.load_state_dict(torch.load("models/model_ep29.pth"))
  model.eval()

  # Reading Image
  img  = cv2.imread(image)

  # # Before we can make a prediction we need to preprocess the image.
  processed_image = preprocess(img)

  result = model(torch.permute(torch.from_numpy(processed_image).float(),(0,3,1,2)).to(device))

  # After postprocessing, we can easily use our results
  label, (x1, y1, x2, y2), confidence = postprocess(image, result)

  # Now annotate the image
  cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 100), 2)
  cv2.putText(
      img, 
      '{}, CONFIDENCE: {}'.format(label, confidence), 
      (30, int(35 * scale)), 
      cv2.FONT_HERSHEY_COMPLEX, scale,
      (200, 55, 100),
      2
      )

  # Show the Image with matplotlib
  plt.figure(figsize=(10,10))
  plt.imshow(img[:,:,::-1])

让我们继续在一幅图像上做推论。

image = '< IMAGE PATH >'
predict(image)

下面给出了输出,你可以看到我们是如何在图像上得到一个边界框的。

Resulting Image with Bounding Box

ONNX 和模型架构可视化

正如我们之前提到的,我们可以使用 netron 来可视化我们的模型及其重量等等。为此,我们可以将模型转换为 ONNX 格式。ONNX 格式是 Snap Lens Studio 等平台接受的通用机器学习模型存储格式,也是 Gradient 上可部署的框架之一。基于 TensorFlow 和 PyTorch 构建的模型可以转换为 ONNX,两者实现相同的形式和功能。以下是我们将如何做。获得'后。ONNX '文件,你可以访问 netron 进行更深入的模型可视化。

model = Network()
model = model.to(device)
model.load_state_dict(torch.load('models/model_ep29.pth'))
model.eval()
random_input = torch.randn(1, 3, 256, 256, dtype=torch.float32).to(device)
# you can add however many inputs your model or task requires

input_names = ["input"]
output_names = ["cls","loc"]

torch.onnx.export(model, random_input, 'localization_model.onnx', verbose=False, 
                  input_names=input_names, output_names=output_names, 
                  opset_version=11)
print("DONE")

结论

正如我们一直在讨论的,图像定位是一个有趣的研究领域,与图像分类问题相比,在这方面获得准确性要困难一些。在这里,我们不仅要得到正确的分类,还要得到一个紧密而正确的包围盒。处理这两个问题会使模型变得复杂,因此改进数据集和体系结构会有所帮助。我希望这篇由两部分组成的关于实现图像本地化的详细文章对您有所帮助,并且我鼓励您阅读更多相关内容。

使用 PyTorch 进行对象定位,第 1 部分

原文:https://blog.paperspace.com/object-localization-using-pytorch-1/

随着图像分类模型的流行,深度学习领域引起了人们的注意,剩下的就是历史了。今天,我们仅仅从一个单一的图像输入就能产生未来的技术。GAN 模型生成图像,图像分割模型精确标记区域,对象检测模型检测正在发生的一切,就像通过普通的闭路电视摄像机识别繁忙街道上的人一样。这些成就真正为进一步的研究铺平了道路,也为随着可解释人工智能的到来,基本建模实践进入医学领域等严肃领域铺平了道路。图像定位对我来说是一个有趣的应用,因为它正好介于图像分类和对象检测之间。每当一个项目需要在检测到的类别上获得边界框时,人们经常会跳到对象检测和 YOLO,这很好,但在我看来,如果你只是进行分类和定位单个类别,那就太过了。让我们深入研究图像本地化:概念、PyTorch 中的实现和可能性。

图像分类

图像分类是基本的深度学习问题之一,使用卷积神经网络以及基于它们构建的架构 ResNet、SqueezeNet 等来解决。随着时间的推移,在很大程度上改进了此类模型的性能。根据项目的不同,并不总是需要构建一个超过 250MB 的大型模型来提供 95%以上的验证准确性。有时候在开发过程中,比如像 Lens Studio 这样的移动平台,重要的是在尺寸和计算方面得到一个更小的模型,具有合理的准确性,并且推理时间非常短。在这个更小的模型空间中,我们有像 MobileNet、SqueezeNet 等模型。

架构和用例的多样性导致了分类的改进和对象的本地化。模型不仅可以将图像分类为 x 或 y,还可以使用强大的边界框在图像中定位 x 或 y,这种想法成倍地增加了可以围绕它构建的用例。定位的方法非常类似于图像分类,所以如果你不熟悉 CNN,可能很难理解。在深入本地化领域之前,让我们先探讨一下它们。

卷积神经网络

如果你是神经网络的新手,请读一下这个。

假设你理解深度学习模型如何在基本的全连接层模型上工作,随着参数的不断增加和大规模全连接层的增加,它们的计算量会变得很大。

Source: Building A Deep Learning Model

在这里,如果你看上面的图像,集中在红色和第一个蓝色层,它们之间的计算将采取(不准确,只是为了说明)3*4 操作。现在,即使对于 CPU 来说,这也是相当轻量级的。但是当我们处理图像时,让我们取一个典型的 64X64 大小的图像。图像大小为 64x64,每个有 3 个通道。每幅图像的 RGB 通道总共有 12,288 个像素。现在,这听起来很大,但当你想到用与上面完全连接的层相同的方式处理图像时,计算量会变得更大。当你将 12288 个神经元中的每一个与下一层的其他神经元相乘时,从计算的角度来看这是非常昂贵的,成本会急剧上升。

为了补救这一点,我们可以使用 CNN:它以一种计算量更轻的方式来完成许多更重的模型架构的任务,同时提供更好的准确性。在卷积神经网络中,我们在张量上进行计算,核在通道上,并不断减少张量的大小,同时增加通道的数量,其中每个通道都有助于学习一些独特的东西,直到我们最终将通道堆栈展平为完全连接的层,如下所示。这是对 CNN 的一个简单的解释,你必须深入这个架构才能掌握它的每一个方面。

Source: Comprehensive guide to CNN

在进行物体定位之前,先关注上面的红点层。这作为图像分类问题的最后一层,结果是具有最大值的神经元的索引。那就是预测的类索引。在对象定位中,我们将不只有一个输出,而是两个不同的输出,一个是类,另一个是具有 4 个边界框坐标值的回归输出。

目标定位

与目标检测模型相比,定位的迷人之处在于它的简单性。毫无疑问,对象检测模型处理更大的问题,并且必须对某些架构中的初始区域选择层所建议的所有区域进行分类。尽管如此,我们可以像回归问题一样预测坐标的想法还是很有趣的。因此,我们的通用架构(类似于图像分类模型)将具有额外的 1 个回归输出(具有 4 个坐标),并行训练以获得坐标。

本地化数据集

对于对象定位,我们不仅需要图像和标签,还需要包含对象的边界框的坐标。我们必须使用图像注释/对象标记工具来准备这样的数据集,而 XML 是用于存储每个图像的坐标值的流行文件格式。

一个比较流行的对象标记工具是微软的开源对象标记工具,VOTT。它是轻量级和简单的,这使得它非常适合本地化问题。这是项目和文档的链接

不一定非要将数据存储为 XML,您可以选择任何您能轻松处理的文件类型来读取数据,但是 XML 非常有用。我们的输入将是图像,它的标签,以及对应于我们正在标记的坐标的四个值。对于这个项目,我提取了流行的狗与猫数据集的一部分,并使用 VOTT 和生成的 XML 文件创建了边界框。我们将详细讨论如何利用 python 和一些库来读取这些 XML 文件以获得相应图像的输入坐标。

PyTorch 实现

像往常一样,我们从导入库开始,包括 pandas、CSV 和 XML,这将帮助我们处理输入。

import numpy as np

from sklearn.model_selection import train_test_split

import matplotlib.pyplot as plt
import cv2
import random
import os

from PIL import Image

import pandas as pd
from xml.dom import minidom
import csv

您可以从这里下载用于演示该对象本地化实现的数据集。为您的个人项目实现时,更改数据集并用更多图像和注释填充它,以获得可靠的模型性能。因为我必须为演示创建边界框,所以我将样本保持在最少。

首先,我们将解压数据集开始。上面给出了数据集链接。

!unzip localization_dataset.zip

让我们在读取和处理输入之前分析 XML 文件。理解这种结构将有助于您理解在遇到新的 XML 时如何阅读它。

<annotation verified="yes">
    <folder>Annotation</folder>
    <filename>cat.0.jpg</filename>
    <path>Cat-PascalVOC-export/Annotations/cat.0.jpg</path>
    <source>
        <database>Unknown</database>
    </source>
    <size>
        <width>256</width>
        <height>256</height>
        <depth>3</depth>
    </size>
    <segmented>0</segmented>
    <object>
    <name>cat</name>
    <pose>Unspecified</pose>
    <truncated>0</truncated>
    <difficult>0</difficult>
    <bndbox>
        <xmin>55.35820533192091</xmin>
        <ymin>10.992090947210452</ymin>
        <xmax>197.38757944915255</xmax>
        <ymax>171.24521098163842</ymax>
    </bndbox>
</object>
</annotation> 

接下来,让我们开始阅读和处理图像和注释。我们将使用 XML minidom 来读取 XML 文件。

def extract_xml_contents(annot_directory, image_dir):

        file = minidom.parse(annot_directory)

        # Get the height and width for our image
        height, width = cv2.imread(image_dir).shape[:2]

        # Get the bounding box co-ordinates 
        xmin = file.getElementsByTagName('xmin')
        x1 = float(xmin[0].firstChild.data)

        ymin = file.getElementsByTagName('ymin')
        y1 = float(ymin[0].firstChild.data)

        xmax = file.getElementsByTagName('xmax')
        x2 = float(xmax[0].firstChild.data)

        ymax = file.getElementsByTagName('ymax')
        y2 = float(ymax[0].firstChild.data)

        class_name = file.getElementsByTagName('name')

        if class_name[0].firstChild.data == "cat":
          class_num = 0
        else:
          class_num = 1

        files = file.getElementsByTagName('filename')
        filename = files[0].firstChild.data

        # Return the extracted attributes
        return filename,  width, height, class_num, x1,y1,x2,y2

让我们创建一个 num_to_labels 字典来从预测中检索标签。

num_to_labels= {0:'cat',1:'dog'}

一旦我们获得了所有这些数据,我们就可以将其存储为一个 CSV 文件,这样我们就不必为每次训练读取 XML 文件。我们将使用 Pandas 数据帧来存储数据,稍后我们将把它保存为 CSV。

# Function to convert XML files to CSV
def xml_to_csv():

  # List containing all our attributes regarding each image
  xml_list = []

  # We loop our each class and its labels one by one to preprocess and augment 
  image_dir = 'dataset/images'
  annot_dir = 'dataset/annot'

  # Get each file in the image and annotation directory
  mat_files = os.listdir(annot_dir)
  img_files = os.listdir(image_dir)

  # Loop over each of the image and its label
  for mat, image_file in zip(mat_files, img_files):

      # Full mat path
      mat_path = os.path.join(annot_dir, mat)

      # Full path Image
      img_path = os.path.join(image_dir, image_file)

      # Get Attributes for each image 
      value = extract_xml_contents(mat_path, img_path)

      # Append the attributes to the mat_list
      xml_list.append(value)

  # Columns for Pandas DataFrame
  column_name = ['filename', 'width', 'height', 'class_num', 'xmin', 'ymin', 
                 'xmax', 'ymax']

  # Create the DataFrame from mat_list
  xml_df = pd.DataFrame(xml_list, columns=column_name)

  # Return the dataframe
  return xml_df

# The Classes we will use for our training
classes_list = sorted(['cat',  'dog'])

# Run the function to convert all the xml files to a Pandas DataFrame
labels_df = xml_to_csv()

# Saving the Pandas DataFrame as CSV File
labels_df.to_csv(('dataset.csv'), index=None)

现在我们已经在 CSV 中获得了数据,让我们加载相应的图像和标签来构建数据加载器。我们将对图像进行预处理,包括归一化像素。这里需要注意的一点是我们如何存储坐标。我们将该值存储为一个 0 到 1 的范围值,方法是将它除以图像大小(这里是 256),以便模型输出可以在以后缩放到任何图像(我们将在可视化部分看到这一点)。

def preprocess_dataset():
  # Lists that will contain the whole dataset
  labels = []
  boxes = []
  img_list = []

  h = 256
  w = 256
  image_dir = 'dataset/images'

  with open('dataset.csv') as csvfile:
      rows = csv.reader(csvfile)
      columns = next(iter(rows))
      for row in rows:
        labels.append(int(row[3]))
        #Scaling Coordinates to the range of [0,1] by dividing the coordinate with image size, 256 here.
        arr = [float(row[4])/256,  
               float(row[5])/256,
               float(row[6])/256,
               float(row[7])/256]
        boxes.append(arr)
        img_path = row[0]
        # Read the image
        img  = cv2.imread(os.path.join(image_dir,img_path))

        # Resize all images to a fix size
        image = cv2.resize(img, (256, 256))

        # # Convert the image from BGR to RGB as NasNetMobile was trained on RGB images
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

        # Normalize the image by dividing it by 255.0 
        image = image.astype("float") / 255.0

        # Append it to the list of images
        img_list.append(image)

  return labels, boxes, img_list

现在这个函数已经准备好了,让我们调用它并加载输入数据。作为数据预处理的最后一步,我们将混洗数据。

# All images will resized to 300, 300 
image_size = 256

# Get Augmented images and bounding boxes
labels, boxes, img_list = preprocess_dataset()

# Now we need to shuffle the data, so zip all lists and shuffle
combined_list = list(zip(img_list, boxes, labels))
random.shuffle(combined_list)

# Extract back the contents of each list
img_list, boxes, labels = zip(*combined_list)

到目前为止,我们已经完成了数据预处理,包括从 XML 文件中读取数据注释,现在我们准备在 PyTorch 和模型架构设计中构建定制的数据加载器。实现将在本文的下一部分继续,我们将详细讨论上述方法来完成实现。感谢阅读:)

如何在渐变笔记本上用 JoJoGAN 进行一次脸部风格化

原文:https://blog.paperspace.com/one-shot-face-stylization-with-jojogan/

风格转移是最近围绕深度学习媒体最热门的话题之一。这有许多原因,包括该方法易于出版的可论证性,以及对照片进行快速风格编辑的潜在效用。这种实用性和易于演示的结合使 style transfer 成为许多数据科学家、ML 工程师和人工智能爱好者从事的最受欢迎的第一个计算机视觉项目之一,例如将文森特·梵高的“星空”的风格赋予以前平凡的风景照片。

也就是说,这是一门粗糙的科学。像许多计算机视觉任务一样,将风格转移到图像的粗糙和较大区域的挑战远比将相同的风格转移到面部的精细特征容易。特别是像眼睛和嘴巴这样的区域很难让人工智能正确地近似生成。

An example of JoJoGAN (trained on faces from the tv show Arcane) applying its stylization to randomly sampled faces.

在本教程中,我们将看看 JoJoGAN -一种新颖的方法进行一次性风格的面部图像转移。这个 PyTorch 编写的架构旨在捕捉历史上难以解释的风格细节,例如传递保留眼睛形状或嘴巴细节等面部细节的风格效果。JoJoGAN 旨在解决这个问题,首先近似成对的训练数据集,然后微调 StyleGAN 以执行一次性人脸风格化。

JoJoGAN 能够摄取任何一张人脸图像(理想情况下是某种高质量的头像),使用 GAN 反演来近似成对的真实数据,并使用这些数据来精确调整预先训练的 StyleGAN2 模型。然后使 StyleGAN2 模型可推广,以便赋予的样式可以随后应用于新图像。以前的一次和几次拍摄尝试已经接近成功的水平,但 JoJoGAN 已经成功地实现了其生成的图像的极高质量水平。

按照下面的步骤,看看如何在渐变笔记本上运行 JoJoGAN!

要求和设置

JoJoGAN 是一个基于 PyTorch 的包,它利用许多库来实现它的功能。当您为 JoJoGAN 创建笔记本时,请确保选择 PyTorch 图块以及 GPU 实例。完成后,滚动到页面底部,选择高级选项切换。对于你的工作区网址,一定要输入 https://github.com/gradient-ai/JoJoGAN 。一旦完成,您的实例已经启动,继续打开stylize.ipynb文件。这是我们将做大部分工作的地方。

!pip install gdown scikit-learn==0.22 scipy lpips dlib opencv-python-headless tensorflow
!wget https://github.com/ninja-build/ninja/releases/download/v1.8.2/ninja-linux.zip
!unzip ninja-linux.zip -d /usr/local/bin/
!update-alternatives --install /usr/bin/ninja ninja /usr/local/bin/ninja 1 --force 

第一个代码单元包含我们用来创建实例的官方 PyTorch 映像上需要但没有安装的库。请务必先运行此单元,以确保一切正常运行。

#imports
import torch
torch.backends.cudnn.benchmark = True
from torchvision import transforms, utils
from util import *
from PIL import Image
import math
import random
import os

import numpy as np
from torch import nn, autograd, optim
from torch.nn import functional as F
from tqdm import tqdm
import lpips
import wandb
from model import *
from e4e_projection import projection as e4e_projection
from copy import deepcopy

os.makedirs('inversion_codes', exist_ok=True)
os.makedirs('style_images', exist_ok=True)
os.makedirs('style_images_aligned', exist_ok=True)
os.makedirs('models', exist_ok=True)

下一个单元将包导入到笔记本中,因为它们已经安装在机器上了。值得注意的是,我们同时使用本地和 python 安装包。确保不要更改.ipynb文件的位置,以确保其正常工作。然后,下面的os.makedirs()语句创建并检查我们将用于 JoJoGAN 的目录是否包含在内。

!gdown https://drive.google.com/uc?id=1s-AS7WRUbL3MzEALxM8y4_XO3n3panxH
!tar -xf pretrained_models.tar.gz
!mv pretrained_models/stylegan2-ffhq-config-f.pt ~/../notebooks
!gdown https://drive.google.com/uc?id=1O8OLrVNOItOJoNGMyQ8G8YRTeTYEfs0P
!mv e4e_ffhq_encode.pt models/

运行这些导入之后的单元也很重要,因为这是我们将获得 StyleGAN2 和 e4e 模型的检查点的地方,我们将使用它们作为生成器的基础。

#Finish setup
device = 'cuda' #@param ['cuda', 'cpu']

latent_dim = 512

# Load original generator
original_generator = Generator(1024, latent_dim, 8, 2).to(device)
ckpt = torch.load('stylegan2-ffhq-config-f.pt')
original_generator.load_state_dict(ckpt["g_ema"], strict=False)
mean_latent = original_generator.mean_latent(10000)

# to be finetuned generator
generator = deepcopy(original_generator)

transform = transforms.Compose(
    [
        transforms.Resize((1024, 1024)),
        transforms.ToTensor(),
        transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
    ]
)

为了结束设置,我们需要实例化我们的生成器。我们将设备指定为 cuda,因为我们使用的是 GPU。我们还将两个生成器的潜在维度设置为 512。

对于设置,我们首先实例化一个未经训练的生成器,以便在整个过程中进行微调。它与来自 ffhq StyleGAN2 模型检查点的状态字典相匹配,因此我们可以更新它的副本,以反映我们希望通过训练传递的风格。然后,副本可用于将输出与原始版本进行比较。最后,我们定义了一个在图像上使用的转换,以帮助简化样式转换本身。

对齐面

%matplotlib inline

filename = 'iu.jpeg' #@param {type:"string"}
filepath = f'test_input/{filename}'

# uploaded = files.upload()
# filepath = list(uploaded.keys())[0]
name = strip_path_extension(filepath)+'.pt'

# aligns and crops face
aligned_face = align_face(filepath)

# my_w = restyle_projection(aligned_face, name, device, n_iters=1).unsqueeze(0)
my_w = e4e_projection(aligned_face, name, device).unsqueeze(0)

display_image(aligned_face, title='Aligned face')

在我们继续训练模型或生成图像之前,让我们检查一下这里提供的辅助函数。最重要的是align_face功能。它将拍摄任何合适大小的照片,检查图像中是否有清晰的人脸,然后旋转它,使眼线与图像的底部平面平行。这确保了用于训练或风格转换的每个提交的照片都是合适的类型。

使用预训练的样式模型检查点生成图像

加载更多模型

!gdown https://drive.google.com/uc?id=15V9s09sgaw-zhKp116VHigf5FowAy43f

# To download more pretrained style models, use the gdown script above with the corresponding id's (the values)
# drive_ids = {
#     "stylegan2-ffhq-config-f.pt": "1Yr7KuD959btpmcKGAUsbAk5rPjX2MytK",
#     "e4e_ffhq_encode.pt": "1o6ijA3PkcewZvwJJ73dJ0fxhndn0nnh7",
#     "restyle_psp_ffhq_encode.pt": "1nbxCIVw9H3YnQsoIPykNEFwWJnHVHlVd",
#     "arcane_caitlyn.pt": "1gOsDTiTPcENiFOrhmkkxJcTURykW1dRc",
#     "arcane_caitlyn_preserve_color.pt": "1cUTyjU-q98P75a8THCaO545RTwpVV-aH",
#     "arcane_jinx_preserve_color.pt": "1jElwHxaYPod5Itdy18izJk49K1nl4ney",
#     "arcane_jinx.pt": "1quQ8vPjYpUiXM4k1_KIwP4EccOefPpG_",
#     "arcane_multi_preserve_color.pt": "1enJgrC08NpWpx2XGBmLt1laimjpGCyfl",
#     "arcane_multi.pt": "15V9s09sgaw-zhKp116VHigf5FowAy43f",
#     "disney.pt": "1zbE2upakFUAx8ximYnLofFwfT8MilqJA",
#     "disney_preserve_color.pt": "1Bnh02DjfvN_Wm8c4JdOiNV4q9J7Z_tsi",
#     "jojo.pt": "13cR2xjIBj8Ga5jMO7gtxzIJj2PDsBYK4",
#     "jojo_preserve_color.pt": "1ZRwYLRytCEKi__eT2Zxv1IlV6BGVQ_K2",
#     "jojo_yasuho.pt": "1grZT3Gz1DLzFoJchAmoj3LoM9ew9ROX_",
#     "jojo_yasuho_preserve_color.pt": "1SKBu1h0iRNyeKBnya_3BBmLr4pkPeg_L",
#     "supergirl.pt": "1L0y9IYgzLNzB-33xTpXpecsKU-t9DpVC",
#     "supergirl_preserve_color.pt": "1VmKGuvThWHym7YuayXxjv0fSn32lfDpE",
#     "art.pt": "1a0QDEHwXQ6hE_FcYEyNMuv5r5UnRQLKT",
# }

这个部分的第一个单元包含一个 gdown 命令和一个对应的预训练 StyleGAN2 模型的字典,以及它们的 Google drive ids。使用上面的脚本下载任何不同的可用样式,方法是在命令``gdown https://drive.google.com/uc?id= {id} 后替换粘贴 id。在下面的部分中,使用这些其他模型将它们的各种风格转换为您选择的图像。

将“神秘 _ 多重”风格转移到提供的样本图像中

plt.rcParams['figure.dpi'] = 150
pretrained = 'arcane_multi' #@param ['art', 'arcane_multi', 'supergirl', 'arcane_jinx', 'arcane_caitlyn', 'jojo_yasuho', 'jojo', 'disney']
#@markdown Preserve color tries to preserve color of original image by limiting family of allowable transformations. Otherwise, the stylized image will inherit the colors of the reference images, leading to heavier stylizations.
preserve_color = False #@param{type:"boolean"}

ckpt = torch.load('arcane_multi.pt')
generator.load_state_dict(ckpt["g"], strict=False) 

为了便于理解,下一个单元格已被拆分。在第一小节中,我们从检查点实例化新模型,并将状态字典加载到生成器中。这将设置我们的生成器使用该样式创建图像。

#@title Generate results
n_sample =  5#@param {type:"number"}
seed = 3000 #@param {type:"number"}

torch.manual_seed(seed)
with torch.no_grad():
    generator.eval()
    z = torch.randn(n_sample, latent_dim, device=device)

    original_sample = original_generator([z], truncation=0.7, truncation_latent=mean_latent)
    sample = generator([z], truncation=0.7, truncation_latent=mean_latent)

    original_my_sample = original_generator(my_w, input_is_latent=True)
    my_sample = generator(my_w, input_is_latent=True)

# display reference images
if pretrained == 'arcane_multi':
    style_path = f'style_images_aligned/arcane_jinx.png'
else:   
    style_path = f'style_images_aligned/{pretrained}.png'
style_image = transform(Image.open(style_path)).unsqueeze(0).to(device)
face = transform(aligned_face).unsqueeze(0).to(device)

my_output = torch.cat([style_image, face, my_sample], 0)
display_image(utils.make_grid(my_output, normalize=True, range=(-1, 1)), title='My sample')
plt.show()

output = torch.cat([original_sample, sample], 0)
display_image(utils.make_grid(output, normalize=True, range=(-1, 1), nrow=n_sample), title='Random samples')
plt.show()

在下一小节中,我们使用我们的原始生成器和新训练的奥术多重生成器,从原始面部输入和样式对图像生成的影响的组合中创建图像。我们对所提供的人脸图像iu.jpeg以及在 seed 3000 中生成的 StyleGAN2 人脸的随机采样都这样做。

The original style template, the original photo, and the style transferred photo

正如你所看到的,JoJoGAN 能够在照片中表现出很多训练对象的特征。值得注意的是,蓝色的眼睛,浓眉,人中的轻微隆起,脸颊变色,较暗的调色板,和较重的阴影都被传递到新的图像上。让我们看看随机生成的面孔,看看是否存在相同的特征:

The style image of the type the model was trained on, the original photo, and the input photo after the style transfer is applied.

从上面的照片中我们可以看到,风格转移的效果非常一致。这表明 JoJoGAN 可以有效地将风格转移到单张照片上,并且它高度通用化,能够在各种各样的面部、肤色和面部结构上工作。现在我们已经确认了 JoJoGAN 的功效,让我们看看下一节,看看我们如何在自己的图像上训练 JoJoGAN。

为 JoJoGAN 训练一个新的风格模型

为了训练一个新的 JoJoGAN 生成器,我们首先需要获得一个好的图像数据集。对于这个例子,我们将使用神秘电视节目中提供的图像来训练模型。

但在此之前,我们需要迈出关键的一步。早期的安装导致了 libjpeg 库的冲突。导航到~/usr/lib并通过在终端中运行以下命令删除冲突的库:

cd ../usr/lib/
rm libturbojpeg.a
rm libturbojpeg.la
rm libturbojpeg.so
rm libturbojpeg.so.0
rm libturbojpeg.so.1.0
rm libjpeg.a
rm libjpeg.la
rm libjpeg.so
rm libturbojpeg.so.0     
rm libturbojpeg.so.0.1.0

既然已经解决了这个问题,剩下的就是运行最后两个单元。这将运行一个 python 脚本Run_Stylizer.py,该脚本根据第 110 行的 num_iters 变量指定的迭代次数,训练我们的生成器。另外,一定要在第 75 行的style_images/后面加上你的文件夹名。在运行单元之前,在脚本文件中为训练序列设置这些参数。如果遇到 OOM 错误,请尝试减小训练图像集的大小。

The results of my personal training run using the Arcane images

结论

本教程向我们展示了如何使用 JoJoGAN 生成高质量的图像,并将输入样式转换到输入图像上。这个过程很容易调整,只需要在运行时将文件名添加到 python 脚本中。一定要在 Gradient 的免费 GPU 笔记本上免费亲自试用。

JoJoGAN 在 Gradient 上运行所需的笔记本和脚本都可以在这里找到

深度学习中的梯度下降和优化

原文:https://blog.paperspace.com/optimization-in-deep-learning/

许多深度学习模型训练管道下面最常见的方法是梯度下降。但是普通梯度下降会遇到几个问题,比如陷入局部极小值或者爆炸和消失梯度的问题。为了解决这些问题,随着时间的推移,已经设计了梯度下降的几种变体。我们将在本文中查看最常见的一些,并针对一些优化问题对它们进行基准测试。

您可以跟随本教程中的代码,并从 ML Showcase 中免费运行它。

梯度下降

在深入研究优化器之前,我们先来看看梯度下降。梯度下降是一种优化算法,通过在与最陡上升相反的方向上移动来迭代地减少损失函数。在给定初始点的情况下,任何曲线上最陡上升的方向都是通过计算该点的坡度来确定的。与之相反的方向会把我们带到最小最快的地方。

在数学上,这是一种最小化目标函数 J (𝜃)的方法,其中𝜃代表模型的参数。深度架构通过遵循前馈机制进行预测,其中每一层都将前一层的输出作为输入,并使用由𝜃表示的参数(或者许多熟悉神经网络优化的人会称之为权重偏差),并最终输出传递到下一层的转换后的特征。将最终层的输出与我们期望的真实输出进行比较,并计算损失函数。然后使用反向传播来更新这些参数,反向传播使用梯度下降来找到应该更新参数的确切方式。这些参数的更新取决于优化算法的梯度和学习速率。

基于梯度下降的参数更新遵循以下规则:

θ=θ∏j(θ)

其中 η 为学习率。

1D 函数相对于其输入的梯度的数学公式如下:

虽然这对于连续函数是准确的,但是在计算神经网络的梯度时,我们将主要处理离散函数,并且计算极限并不像上面显示的那样简单。

上述方法(前向差分)被证明是不太准确的,因为截断误差的量级是 O(h) 。相反,使用中心差分方案,如下所示:

在中心差分法中,截断误差的量级为 *O(h2)。*截断误差基于两个公式的泰勒展开式。这里的和这里的可以很好地解释这一点。

梯度下降变体

梯度下降优于其他迭代优化方法,如 Newton Rhapson 方法,因为 Newton 方法在每个时间步使用一阶和二阶导数,这使得其在规模上的操作效率低下。

有几种梯度下降尝试解决香草算法的某些限制,如随机梯度下降和允许在线学习的小批量梯度下降。普通梯度下降法计算整个数据集的梯度,而批量梯度下降法允许我们在处理几批数据的同时更新梯度,从而在处理大型数据集时提高内存效率。

香草梯度下降

让我们从如何实现香草和动量梯度下降开始,看看算法是如何工作的。然后,我们将在等高线图上可视化 2D 函数的梯度更新,以更好地理解算法。

更新将不会基于一个损失函数,而只是朝着与最陡上升相反的方向迈出一步。为了得到最陡上升的方向,我们将首先编写函数来计算函数的梯度,给出需要计算梯度的点。我们还需要另一个参数来定义我们的数值微分步骤的大小,用 h 表示。

采用多个坐标作为输入的函数的数值微分的中心差分方案可以如下实现。

import numpy as np

def gradient(f, X, h):
    grad = []
    for i in range(len(X)):
        Xgplus = np.array([x if not i == j else x + h for j, x in enumerate(X)])
        Xgminus = np.array([x if not i == j else x - h for j, x in enumerate(X)])
        grad.append(f(*Xgplus) - f(*Xgminus) / (2 * h))
    return np.array(grad) 

普通梯度下降更新将如下所示:

def vanilla_update(epoch, X, f, lr, h):
    grad = gradient(f, X, h)
    X1 = np.zeros_like(X)
    for i in range(len(X)):
        X1[i] = X[i] - lr * grad[i]
    print('epoch: ', epoch, 'point: ', X1, 'gradient: ', grad)
    return X1 

你可以把学习率想象成梯度更新的步长。

我们将在 Ackley 函数上测试我们的算法,Ackley 函数是测试优化算法的流行函数之一。阿克利的函数看起来像这样。

import numpy as np

def ackleys_function(x, y):
    return - 20 * np.exp(- 0.2 * np.sqrt(0.5 * (x ** 2 + y ** 2))) \
           - np.exp(0.5 * (np.cos(2 * np.pi * x) + np.cos(2 * np.pi * y))) \
           + np.e + 20 

现在,为了最终测试我们的香草梯度下降:

if __name__ == '__main__':
    h = 1e-3
    f = ackleys_function
    point = np.array([-2., -2.])
    i = 0
    lr = 0.00001
    while True:
        new_point = vanilla_update(i+1, point, f, lr, h)
        plt.plot(*point, 'ro', ms=1)
        if np.sum(abs(new_point - point)) < h:
            print('Converged.')
            break
        point = new_point
        i += 1 

我们使用的收敛标准很简单。如果该点的坐标的绝对值没有显著变化,如由 h 的值所确定的,我们停止该算法。

动量梯度下降

对于陡坡,动量梯度下降比普通梯度下降更快地帮助我们加速下坡。这是通过使用动量项来实现的。你可以认为它是根据梯度的大小和方向来调整梯度步长的速度。梯度更新中的动量因子是直到最后一个时间步长的梯度移动平均值,乘以一个小于 1 的常数,该常数保证整个速度项在极高的斜率下收敛。这有助于我们在更新参数时避免极端跳跃。

为了实现动量更新,除了计算当前点的梯度之外,还必须存储先前步骤的梯度,用于动量步骤的计算。参数 m 定义为动量,函数可以实现如下。

def momentum_update(epoch, X, f, lr, m, h, vel=[]):
    grad = gradient(f, X, h)
    X1 = np.zeros_like(X)
    for i in range(len(X)):
        vel[i] = m * vel[i] + lr * grad[i]
        X1[i] = X[i] - vel[i]
    print('epoch: ', epoch, 'point: ', X1, 'gradient: ', grad, 'velocity: ', vel)
    return X1, vel 

最终的循环将如下所示:

if __name__ == '__main__':
    h = 1e-3
    f = ackleys_function
    point = np.array([-2., -2.])
    vel = np.zeros_like(point)
    i = 0
    lr = 0.00001
    m = 0.9
    grads = []
    while True:
        new_point, vel = momentum_update(i+1, point, f, lr, m, h, vel=vel)
        plt.plot(*point, 'bo', ms=1)
        if np.sum(abs(new_point - point)) < h:
            print('Converged.')
            break
        point = new_point
        i += 1 

梯度下降可视化

获取 3D 函数的绘图:

from matplotlib import pyplot as plt
from mpl_toolkits import mplot3d

def get_scatter_plot(X, Y, function):
    Z = function(X, Y)
    fig = plt.figure()
    cm = plt.cm.get_cmap('viridis')
    plt.scatter(X, Y, c=Z, cmap=cm)
    plt.show()
    return fig

def get_contours(X, Y, function):
    Z = function(X, Y)
    fig = plt.figure()
    contours = plt.contour(X, Y, Z, colors='black',
                           linestyles='dashed',
                           linewidths=1)
    plt.clabel(contours, inline=1, fontsize=10)
    plt.contourf(X, Y, Z)
    plt.xlabel('X')
    plt.ylabel('Y')
    plt.show()
    return fig

def get_3d_contours(X, Y, function):
    Z = function(X, Y)
    fig = plt.figure()
    ax = plt.axes(projection='3d')
    cm = plt.cm.get_cmap('viridis')
    ax.contour3D(X, Y, Z, 100, cmap=cm)
    ax.set_xlabel('x')
    ax.set_ylabel('y')
    ax.set_zlabel('z')
    plt.show()
    return fig

def get_surface_plot(X, Y, function):
    Z = function(X, Y)
    fig = plt.figure()
    ax = plt.axes(projection='3d')
    cm = plt.cm.get_cmap('viridis')
    ax.plot_surface(X, Y, Z, rstride=1,
                    cstride=1, cmap=cm)
    ax.set_xlabel('x')
    ax.set_ylabel('y')
    ax.set_zlabel('z')
    plt.show()
    return fig

if __name__ == '__main__':
    x = np.linspace(-1, 1, 1000)
    X, Y = np.meshgrid(x, x)
    get_scatter_plot(X, Y, ackleys_function)
    get_contours(X, Y, ackleys_function)
    get_3d_contours(X, Y, ackleys_function)
    get_surface_plot(X, Y, ackleys_function) 

可视化看起来像这样。

2D Ackley's function

2D contours of Ackley's function

Surface plot for Ackley's function

3D contours for Ackley's function

这里, XY 恰好是一个网格而不是 1D 阵列。我们可以使用 numpy 函数np.meshgrid创建一个 meshgrid。

您还可以利用令人惊叹的 VisPy 库快速创建 3D 可视化效果,并实时研究它们。

import sys
from vispy import app, scene

def get_vispy_surface_plot(x, y, function):
    canvas = scene.SceneCanvas(keys='interactive', bgcolor='w')
    view = canvas.central_widget.add_view()
    view.camera = scene.TurntableCamera(up='z', fov=60)
    X, Y = np.meshgrid(x, y)
    Z = function(X, Y)
    p1 = scene.visuals.SurfacePlot(x=x, y=y, z=Z, color=(0.3, 0.3, 1, 1))
    view.add(p1)
    scene.Axis(font_size=16, axis_color='r',
                     tick_color='r', text_color='r',
                     parent=view.scene)
    scene.Axis(font_size=16, axis_color='g',
                     tick_color='g', text_color='g',
                     parent=view.scene)
    scene.visuals.XYZAxis(parent=view.scene)
    canvas.show()
    if sys.flags.interactive == 0:
        app.run()
    return scene, app 

这应该给你一个画布来玩这个情节。

要在等高线图上显示普通梯度下降的梯度下降更新,请使用以下代码。

if __name__ == '__main__':

    x = np.linspace(-2, 2, 1000)

    h = 1e-3

    f = ackleys_function

    a, b = np.meshgrid(x, x)
    Z = f(a, b)
    contours = plt.contour(a, b, Z, colors='black',
                           linestyles='dashed',
                           linewidths=1)
    plt.clabel(contours, inline=1, fontsize=10)
    plt.contourf(a, b, Z)
    plt.xlabel('X')
    plt.ylabel('Y')

    point = np.array([-2., -2.])

    i = 0
    lr = 0.00001
    while True:
        new_point = vanilla_update(i+1, point, f, lr, h)
        plt.plot(*point, 'ro', ms=1)
        if np.sum(abs(new_point - point)) < h:
            print('Converged.')
            break
        point = new_point
        i += 1

    plt.show() 

该算法需要 139 个历元才能收敛,梯度更新如下所示:

对于应用动量更新和绘图,您可以使用以下代码:

if __name__ == '__main__':

    x = np.linspace(-2, 2, 1000)

    h = 1e-3

    f = ackleys_function

    a, b = np.meshgrid(x, x)
    Z = f(a, b)
    contours = plt.contour(a, b, Z, colors='black',
                           linestyles='dashed',
                           linewidths=1)
    plt.clabel(contours, inline=1, fontsize=10)
    plt.contourf(a, b, Z)
    plt.xlabel('X')
    plt.ylabel('Y')

    point = np.array([-2., -2.])
    vel = np.zeros_like(point)

    i = 0
    lr = 0.00001
    m = 0.1
    grads = []
    while True:
        new_point, vel = momentum_update(i+1, point, f, lr, m, h, vel=vel)
        plt.plot(*point, 'bo', ms=1)
        if np.sum(abs(new_point - point)) < h:
            print('Converged.')
            break
        point = new_point
        i += 1
    plt.show() 

在相同的学习速率和动量为 0.1 的情况下,上述更新方案在 127 个时期内收敛。

流行的梯度下降实现

梯度下降算法有几种实现,它们都有解决特定问题的小调整。

一些流行的梯度下降算法包括:

  1. 内斯特罗夫动量 -正如我们所讨论的,在基于动量的梯度下降中,速度项由一个移动平均值组成,直到前一个时间步长。在内斯特罗夫动量法中,考虑了当前的时间步长,为算法提供了一种如何调整下一个时间步长的更新的预测能力。下一步的梯度是通过考虑动量在下一次更新之后找到近似位置来计算的。
  2. Adam -自适应矩估计,也称为 Adam optimizer,通过查看从梯度和常数参数计算的一阶和二阶矩来计算每个优化步骤的自适应学习率。它的一部分类似于动量,但 Adam 在基于动量的优化速度较高的情况下表现更好,因为它根据二阶矩为梯度步长提供了相反的力。该算法使用去偏差机制来确保它不会总是收敛到微不足道的值。
  3. 与频繁出现的特征相比,AdaGrad 通过向不频繁出现的特征分配更高的权重来计算自适应学习率。它累积平方梯度的方式与动量累积梯度的方式相同。这使得优化器能够更好地处理稀疏数据。Adagrad 用于训练手套单词向量。
  4. AdaMax-Adam 算法可以修改为根据 L2 范数值来缩放二阶矩,而不是使用原始值。但是参数也是平方的。不使用平方项,可以使用任何指数 n 。虽然对于更大的值,这样的梯度更新往往是不稳定的,但是如果参数 n 趋于无穷大,那么它为我们提供了稳定的解决方案。根据梯度的这种正则化获得的速度项然后被用于更新模型的权重。
  5. AdamW 优化器本质上是 Adam,它使用 L2 正则化权重。L2 正则化的常见实现用衰减的权重修改梯度值,而在 AdamW 实现中,正则化在梯度更新步骤期间完成。这个轻微的变化似乎以一种显著的方式改变了结果。
  6. 与 Adagrad 一样,AdaDelta 使用累积的平方梯度,但只在一组步骤窗口中使用。渐变不是存储的,而是动态计算的。因此,在每一步中,由于仅依赖于先前的平均值和当前的梯度,存储器的利用被优化。

了解流行的梯度下降算法更多数学细节的资源可以在这里找到。其中一些的实现可以在这里找到。

基准优化器

让我们看看这些优化器是如何相互竞争的。我们将使用 CIFAR10 数据集进行基准测试。你也可以跟随来自 ML Showcase 的代码,并在 Gradient 上免费运行它。

让我们导入开始我们的培训脚本所需的所有东西。我们将使用 PyTorch 优化器及其 ResNet18 实现。我们将使用 Matplotlib 来可视化我们的结果。

import torch
from torch import nn
from torch import optim
from torch.utils.data import RandomSampler, DataLoader

from torchvision import models
from torchvision import transforms
from torchvision.datasets import CIFAR10 as cifar
from torchvision import datasets

import time
import pickle
import random
import numpy as np
from tqdm import tqdm
from matplotlib import pyplot as plt

为了确保我们的结果是可重复的,让我们为 torch、NumPy 和 Python random 模块设置 PRNG 种子。

然后,我们创建一个扩充和规范化的数据集。CIFAR 数据集用于创建我们的训练和测试数据加载器。

torch.manual_seed(0)
torch.cuda.manual_seed(0)
np.random.seed(0)
random.seed(0)

DATA_PATH = 'cifar'

trans = transforms.Compose([            [
                transforms.RandomHorizontalFlip(),
                transforms.RandomCrop(32, padding=4),
                transforms.ToTensor(),
                transforms.Normalize(
                    mean=[n/255\. for n in [129.3, 124.1, 112.4]], 
                    std=[n/255\. for n in [68.2,  65.4,  70.4]]
                )
        ])

train = cifar(DATA_PATH, train=True, transform=trans, download=False)
test = cifar(DATA_PATH, train=False, transform=trans, download=False)

batch_size = 64
train_size = len(train)
test_size = len(test)

train_dataloader = DataLoader(train, shuffle=True, batch_size=batch_size)
test_dataloader = DataLoader(test, shuffle=False, batch_size=batch_size)

我们将为此任务训练一个 ResNet18 模型。ResNet18 模型默认输出 1000 个特征。为了使其适用于我们的数据集,我们添加了一个具有 1000 个输入要素和 10 个输出要素的线性图层。

class Cifar10_Resnet18(nn.Module):

    def __init__(self,):
        super(Cifar10_Resnet18, self).__init__()
        self.base = models.resnet18(pretrained=True)
        self.classification = nn.Linear(in_features=1000, out_features=10)

    def forward(self, inputs):
        out = self.base(inputs)
        out = self.classification(out)
        return out

如果使用 GPU,请将设备设置为 CUDA 类型。

device = torch.device(type='cuda')

让我们定义一个所有优化器的字典,这样我们就可以创建一个循环来遍历所有优化器。字典的值是定义放入字符串的优化器的命令。稍后我们将使用eval函数来实现优化器。

optimizers = {
        'SGD': 'optim.SGD(model.parameters(), lr=0.01, momentum=0.9)',
        'Adam': 'optim.Adam(model.parameters())',
        'Adadelta': 'optim.Adadelta(model.parameters())',
        'Adagrad': 'optim.Adagrad(model.parameters())',
        'AdamW': 'optim.AdamW(model.parameters())',
        'Adamax': 'optim.Adamax(model.parameters())',
        'ASGD': 'optim.ASGD(model.parameters())',
    }

主训练循环训练每个优化器 50 个时期,并通知我们关于训练准确性、验证准确性、训练损失和测试损失的信息。我们使用CrossEntropyLoss作为我们的损失标准,最后我们将所有的度量保存为 pickle 文件。对于每个优化器,都会初始化一个新的模型,然后我们使用eval函数根据未训练的模型参数定义优化器。

epochs = 50

optim_keys = list(optimizers.keys())

train_losses = []
train_accuracies = []
test_losses = []
test_accuracies = []

for i, optim_key in enumerate(optim_keys):
    print('-------------------------------------------------------')
    print('Optimizer:', optim_key)
    print('-------------------------------------------------------')
    print("{:<8} {:<25} {:<25} {:<25} {:<25} {:<25}".format('Epoch', 'Train Acc', 'Train Loss', 'Val Acc', 'Val Loss', 'Train Time'))

    model = Cifar10_Resnet18()
    model.to(device)

    optimizer = eval(optimizers[optim_key])
    criterion = nn.CrossEntropyLoss()

    model.train()

    optim_train_acc = []
    optim_test_acc = []
    optim_train_loss = []
    optim_test_loss = []

    for epoch in range(epochs):

        start = time.time()

        epoch_loss = []
        epoch_accuracy = []

        for step, batch in enumerate(train_dataloader):

            optimizer.zero_grad()

            batch = tuple(t.to(device) for t in batch)
            images, labels = batch

            out = model(images)

            loss = criterion(out, labels)

            confidence, predictions = out.max(dim=1)
            truth_values = predictions == labels
            acc = truth_values.sum().float().detach().cpu().numpy() / truth_values.shape[0]

            epoch_accuracy.append(acc)
            epoch_loss.append(loss.float().detach().cpu().numpy().mean())

            loss.backward()
            optimizer.step()

        optim_train_loss.append(np.mean(epoch_loss))
        optim_train_acc.append(np.mean(epoch_accuracy))

        test_epoch_loss = []
        test_epoch_accuracy = []

        end = time.time()

        model.eval()
        for step, batch in enumerate(test_dataloader):

            batch = tuple(t.to(device) for t in batch)
            images, labels = batch

            out = model(images)

            loss = criterion(out, labels)

            confidence, predictions = out.max(dim=1)
            truth_values = predictions == labels
            acc = truth_values.sum().float().detach().cpu().numpy() / truth_values.shape[0]

            test_epoch_accuracy.append(acc)
            test_epoch_loss.append(loss.float().detach().cpu().numpy().mean())

        optim_test_loss.append(np.mean(test_epoch_loss))
        optim_test_acc.append(np.mean(test_epoch_accuracy))

        print("{:<8} {:<25} {:<25} {:<25} {:<25} {:<25}".format(epoch+1, 
                                                                np.mean(epoch_accuracy), 
                                                                np.mean(epoch_loss), 
                                                                np.mean(test_epoch_accuracy), 
                                                                np.mean(test_epoch_loss), 
                                                                end-start))

    train_losses.append(optim_train_loss)
    test_losses.append(optim_test_loss)
    train_accuracies.append(optim_train_acc)
    test_accuracies.append(optim_train_acc)

train_accuracies = dict(zip(optim_keys, train_accuracies))
test_accuracies = dict(zip(optim_keys, test_accuracies))
train_losses = dict(zip(optim_keys, train_losses))
test_losses = dict(zip(optim_keys, test_losses))

with open('train_accuracies', 'wb') as f:
    pickle.dump(train_accuracies, f)
with open('train_losses', 'wb') as f:
    pickle.dump(train_losses, f)
with open('test_accuracies', 'wb') as f:
    pickle.dump(test_accuracies, f)
with open('test_losses', 'wb') as f:
    pickle.dump(test_losses, f)

我们可以使用下面的代码来绘制结果。

x = np.arange(epochs) + 1

for optim_key in optim_keys:
    plt.plot(x, train_accuracies[optim_key], label=optim_key)

plt.title('Training Accuracies')
plt.legend()
plt.show()

for optim_key in optim_keys:
    plt.plot(x, train_losses[optim_key], label=optim_key)

plt.title('Training Losses')
plt.legend()
plt.show()

for optim_key in optim_keys:
    plt.plot(x, test_accuracies[optim_key], label=optim_key)

plt.title('Testing Accuracies')
plt.legend()
plt.show()

for optim_key in optim_keys:
    plt.plot(x, test_losses[optim_key], label=optim_key)

plt.title('Testing Losses')
plt.legend()
plt.show()

我们看到,对于我们选择的任务,Adamax 始终比所有其他优化器执行得更好。其次是 SGD、Adam 和 AdamW,分别是训练精度。

对于看不见的数据,在 50 个时期之后,Adamax、SGD、Adam 和 AdamW 的模型性能是相似的。Adamax 和 SGD 在最初几个时期的改进最大。

SGD 和 Adamax 再次看到了持续的强劲表现,这也反映在训练精度上。

然而,验证集的失败让亚当和亚当成为了胜利者。对于 SGD 和 Adamax,损失在接近结束时增加,表明需要早期停止机制。

从图中可以清楚地看到,对于我们选择的任务,Adagrad 和 Adadelta 线性提高,但性能不如 Adammax、SGD、Adam 或 AdamW。

优化器的更多基准测试

其他鲜为人知的优化器包括:

  1. 准双曲 Adam 将动量更新和平方梯度更新从更新机制中分离出来,而是使用准双曲公式。这在分母中使用当前和先前梯度的加权平均值,减去一个常数,以及当前和先前平方梯度的另一个加权平均值,减去另一个常数。
  2. YellowFin -在 YellowFin 中,通过最小化局部二次函数,在每次迭代中调整学习速率和动量值,调整后的超参数有助于保持恒定的收敛速率。
  3. -衰减动量是一种动量法则,可以应用于任何带动量的梯度下降算法。衰减动量规则通过衰减梯度转移到未来时间步长的“能量”来降低速度值,方法是用当前时间步长与最终时间步长的比率对其进行加权。通常不需要超参数调整,因为动量通常在最终优化迭代时衰减到 0。当衰减被延迟时,该算法执行得更好。

要更深入地研究这些稍微不太流行的梯度下降算法,请查看本文。为了比较上面提到的一些优化器,他们使用了 6 个测试问题:

  1. CIFAR10 - ResNet18
  2. CIFAR100 - VGG16
  3. STL100 - Wide ResNet 16-8
  4. 时尚主义者的帽子
  5. PTB - LSTM
  6. MNIST - VAE

并测试了以下自适应学习率优化器:

  1. 圣经》和《古兰经》传统中)亚当(人类第一人的名字
  2. 阿姆斯特拉德
  3. 亚当斯
  4. 柴达木
  5. 黄鳍金枪鱼
  6. 恶魔

结果可通过下表进行总结:

它们还在下面的图中展示了几个时期的训练和验证损失。

对于相同的任务,他们还测试了非自适应学习率优化器,例如:

  1. SGDM
  2. 阿格米
  3. QHM
  4. 恶魔 SGDM

对于 CIFAR10 - ResNet18 任务,结果如下:

而对于其他任务:

损失图如下所示:

还有其他几种优化方法,如基于遗传算法的方法或概率优化方法,如模拟退火,它们可以在某些情况下很好地替代梯度下降。例如,在强化学习中,需要通过几种策略优化算法进行离散优化。一些建立在经典强化算法基础上的算法可以在这里找到。

摘要

我们研究了梯度下降,并在 Python 中从头开始实现了香草和动量更新机制。我们还将 Ackley 函数的梯度更新可视化为沿等高线图的移动。我们使用 CIFAR10 数据集对图像分类任务的几个优化器进行了基准测试,并为此训练了一个 ResNet18。结果表明,Adamax、SGD、AdamW 和 Adam 表现良好,而 Adagrad 和 Adadelta 表现不佳。然后,我们看了几个不太流行的基于梯度下降的优化器,它们目前正在深度学习中使用。最后,我们看了我们讨论的几个优化器在不同任务上的表现,包括为卷积架构、LSTMs 和可变自动编码器调整权重。

用 Python 中基于角度的技术检测异常值

原文:https://blog.paperspace.com/outlier-detection-with-abod/

Photo by Greg Rakozy / Unsplash

基于角度的异常值检测 (ABOD)是一种用于检测给定数据集中异常值或异常值的流行技术,通常在多变量环境中使用。它是检测异常值的几何方法的一部分。这方面的其他方法包括基于深度的技术和基于凸包概念的技术。对于这个博客,我们将只关注 ABOD,它的理论和一个简短的教程,使用 PyOD python 包来实现它。但在此之前,让我们定义一个离群值。任何具有与数据集中的大多数观察值完全不同的属性的观察值或数据点都称为异常值/异常值。数据集中的异常值可能由于许多因素而出现,例如仪器记录误差、人为误差、多数群体的自然变化等。

当处理用例时,例如银行领域的欺诈交易检测、电子商务领域的销售峰谷分析、识别网络中的恶意节点/数据包等,您可能希望检测数据集中的异常。除了不一定要求将检测异常值作为最终目标的用例之外,在尝试拟合学习大多数数据点所拥有的一般模式的模型时,您可能希望考虑处理数据集中的这些数据点。通常,剔除异常值的决定取决于开发人员考虑的因素,如潜在的异常值是否是群体的自然组成部分,或者它是否是由一些仪器误差等因素导致的。数据中的异常会导致学习扭曲的数据表示,提供底层数据的误导性表示。因此,在处理这些点时,有必要通过坚持机器学习模型的假设来更好地拟合机器学习模型。

此外,根据部署这些系统的域的敏感性,保守一点通常是一个好的做法,与假阴性 (FN)相比,有较高的假阳性 (FP)。这主要是因为在现实世界中,你可以通过加入一个人来监督最终的决定。但是,这可能会增加总周转时间,并导致很少的询问。但是这仍然比跳过一个异常现象然后后悔要好的多。例如,最好保守一点,停止任何不寻常的高额付款进出银行账户,然后通过 IVR 或与客户的正常电话确认来解决,而不是被骗。

误报——模型说这是个异常值。但不是离群值。

假阴性——模型表示这不是异常值。但这是个例外。

现在让我们深入了解 ABOD 及其变体的细节-

基于角度的异常检测(ABOD)

该技术基于关注由多变量特征空间中任意三个数据点的集合形成的角度的思想。对于异常值和正常点,角包围区大小的变化是不同的。通常,内侧点的观察到的方差高于外侧点,因此这种测量有助于我们对正常点和外侧点进行不同的聚类。基于角度的离群值(ABOD)技术在高维空间中工作得非常好,不像其他基于距离的测量方法那样受到“维数灾难”的困扰。高维空间中任意两点之间的距离几乎是相似的。在这种情况下,角度可以更好地描述亲密度。

该算法非常简单,描述如下-

  1. 迭代每个数据点,计算它 (pivot) 与所有其他数据对形成的角度,并将它们存储在角度列表中。
  2. 计算在步骤 1 中创建的角度列表的方差。
  3. 小于特定阈值的方差值可以被标记为潜在异常。 ( 低方差 表示支点是一个 异常点 高方差 表示支点是一个 正常点 )

让我们也从视觉上来理解这一点

****ABOD for Anomaly Detection

ABOD illustration****

如左图所示,我们可以看到两个集群,一个是正常点,另一个是异常点,即单个蓝点。如果我们选择红点作为兴趣点( pivot ,我们想看看这个点是不是离群点;我们将计算这个点和空间中任何其他两点所围成的角度。在对所有的对进行迭代并计算这个枢轴所包含的角度时,我们可能会观察到角度的许多变化。这样的模式表明支点是具有高内聚力的集群的一部分。

同样,如果我们现在观察右边的图形,将注意力集中在绿点上,重复选择任何两个其他点的过程,并观察它与枢轴所成的角度,我们可能会观察到非常小的变化。这种模式表明关注点远离多数群集,并且可能是异常值。使用基于角度的技术检测异常的整个过程具有 O(n)的相当高的复杂度,因为每次我们都需要用一个枢轴做一个三元组,然后在所有对上循环以计算包含角度的方差。

一个更快的版本叫做快速 ABOD。它使用K-最近邻来近似方差,而不是从给定枢轴的所有可能对中计算方差。随着“K”的值在 KNN 增长,该算法收敛到真实方差,并且像它的旧版本一样工作。下图显示了 ABOD 逼近异常值的速度。

****Fast ABOD

Approximate (Fast) ABOD****

**从这两个例子中可以看出,在计算包围角时,只考虑了枢轴点 c (左边红色,右边绿色)的 k 个最近邻居,这使得计算速度快得惊人。

****## 密码

在博客的这一部分,我们将快速浏览一个例子,并使用PyOD包检测合成数据集中的异常。PyOD 是用于检测多元数据中异常值的最全面和可伸缩的 Python 工具包之一。它提供了 40 多种异常检测算法,从传统技术到使用邻近、集成和基于神经网络的方法的目标检测领域的最新发展。

您可以使用如下所示的 pip 安装 PyOD-

$> pip install pyod

接下来,我们可以使用 PyOD 的 generate_data 方法在 2-D 空间中生成 150 个随机样本。我特别选择了 2-D 空间,而不是更高的值,因为它易于可视化。我还将污染率设置为 10%,即 150 个数据点中的 15 个数据点是异常值。如文档中所述,正常点通过多元高斯分布生成,异常点通过均匀分布生成

from pyod.utils.data import generate_data

X_train, Y_train = generate_data(   
                                    n_train=150, 
                                    n_features=2,
                                    train_only=True,
                                    contamination=0.1,
                                    random_state=42
                                 )

x1, x2 = X_train[:,0], X_train[:,1]

让我们看看数据分布是怎样的-

import matplotlib.pyplot as plt
%matplotlib inline

for idx, i in enumerate(Y_train):
  if i==0.0: 
    color='blue'
  else: 
    color='red'
  plt.scatter(x1[idx], x2[idx], c=color)

Synthetic Dataset with Outliers

Synthetic Data with Outlier(marked in red)

接下来,我们通过将 N^3 搜索空间缩小到仅计算 10 个邻居形成的角度来快速拟合 ABOD。然后我们继续计算误差%。

from pyod.models.abod import ABOD

abod_model = ABOD(contamination=0.1, method='fast', n_neighbors=10)
abod_model.fit(X_train)

pred = abod_model.predict(X_train)

error = (pred != Y_train).sum()
print (f'Error % = {(error/len(pred))*100}')
>> Error % = 2.6666

接下来,我们绘制内点和外点的预测。

import matplotlib.pyplot as plt
%matplotlib inline

for idx, i in enumerate(pred):
  if i==0.0: 
    color='purple'
  else: 
    color='orange'
  plt.scatter(x1[idx], x2[idx], c=color)

ABOD predictions for Outlier Detection

ABOD predictions for Outlier Detection

从上图可以看出,橙色和紫色点分别是预测的异常值和内嵌值。我们的模型总共产生 4 个错误(2 个假阳性,2 个假阴性)。

总结想法

因此,我们以此结束这篇博客。请随意检查 PyOD 库提供的其他算法,并尝试将其中一些算法组合起来以获得最佳结果。人们经常使用的另一种流行的无监督异常检测技术称为隔离林。请随意查看这个博客,获得这个概念的精彩演示。

谢谢大家!****

神经结构搜索第 1 部分:概述

原文:https://blog.paperspace.com/overview-of-neural-architecture-search/

对于经典的机器学习算法,超参数优化问题已经以许多不同的方式得到解决。一些例子包括使用网格搜索、随机搜索、贝叶斯优化、元学习等等。但是当考虑深度学习架构时,这个问题变得更加难以处理。在本文中,我们将涵盖神经架构搜索的问题和当前的艺术状态。本文假设不同的神经网络和深度学习架构的基本知识。

这是一个系列的第 1 部分,将带您了解什么是神经架构搜索(NAS)问题,以及如何使用 Keras 实现各种有趣的 NAS 方法。在这一部分中,我们将涉及控制器(RNNs)、加强梯度、遗传算法、控制搜索空间和设计搜索策略等主题。这将包括该领域的最新论文的文献综述。

介绍

深度学习工程师应该对什么架构可能最适合什么情况有直观的理解,但这种情况很少发生。一个人可以创造的可能架构是无穷无尽的。想想我们用于迁移学习的所有在 ImageNet 上训练的卷积骨干。以 ResNet 为例,它具有 50、100 和 150 层的变体。有许多方法可以调整这些架构——添加额外的跳过连接,删除卷积块等。

神经结构搜索的目的是在给定数据集的情况下,自动执行寻找最佳模型结构的过程。

使这一领域成为热门研究领域的第一篇论文来自 Zoph 等人。(2016).这个想法是使用一个控制器(一个递归神经网络)生成一个架构,训练它,记录它的准确性,根据计算的梯度训练控制器,最后确定哪个架构表现最好。换句话说,它将评估所有(或者至少许多)可能的架构,并找到一个给出最佳验证准确性的架构。

这种对整个搜索空间的探索只对那些拥有大量计算资源的人有意义。令人欣慰的是,随着时间的推移,已经开发出了几种更快、更有效的执行神经结构搜索任务的方法。

神经结构搜索的基础

为了理解应用于这项任务的主要方法,我们需要一些深度学习、优化和计算机科学的跨领域知识。人们解决这个问题的方法从强化学习到进化算法。

神经架构搜索的早期解决方案可以被视为一个多步循环,大致如下:

换句话说:

  1. 使用控制器生成架构
  2. 为几个时期训练生成的架构
  3. 评估生成的架构
  4. 了解所述架构的表现如何
  5. 相应地更新您的控制器
  6. 重复一遍。

这种广泛的概述让我们对我们试图解决的问题有了一个概念,但留下了几个未回答的问题,例如:

  1. 设计控制器的最好方法是什么?
  2. 我们如何优化更好的架构生成?
  3. 我们如何有效地缩小搜索空间?

要回答这些问题,我们应该先讨论一些基础理论。

控制器:循环网络

阅读本文的大多数人可能已经知道什么是 rnn,但是为了完整起见,我们仍然会触及一些基本概念。递归神经网络接受顺序输入,并根据训练数据预测序列中的下一个元素。普通的递归网络将基于所提供的输入,处理它们先前的隐藏状态,并输出下一个隐藏状态和顺序预测。将该预测与地面真实值进行比较,以使用反向传播来更新权重。

我们也知道 rnn 容易受到消失和爆炸梯度的影响。为了解决这个问题,LSTMs 应运而生。LSTMs 使用不同的门来管理序列中每个先前元素的重要性。还有 LSTMs 的双向变体,其从左到右以及从右到左学习不同元素的顺序依赖性。

在神经结构搜索的背景下,这种或那种形式的递归网络将会派上用场,因为它们可以作为控制器来创建顺序输出。这些顺序输出将被解码,以创建神经网络架构,我们将反复训练和测试这些架构,以实现更好的架构建模。

加固坡度

强化学习流程看起来像这样。给定环境、当前状态和一组可能的操作:

  1. 我们的代理根据策略采取行动
  2. 环境被操纵,新的状态被创建
  3. 基于奖励函数,我们的代理更新它的策略
  4. 使用新的状态和更新的策略重复上述步骤

“策略梯度”指的是更新此策略的不同方式。加强是一种策略梯度算法,最初用于 NAS 上,以优化控制器的体系结构搜索过程。强化策略梯度试图通过最大化每个搜索步骤的对数似然性与每个步骤的报酬的乘积,以最大化代理的总报酬的方式优化目标函数。奖励可以是由控制器创建的每个架构的验证准确性的某个函数。

对于神经结构搜索,我们将发现我们的控制器可以通过使用增强梯度来优化以创建更好的结构。这里,动作是您的架构中可能的层或连接,策略是每个控制器步骤中这些层的 softmax 分布。

遗传算法

同样的优化过程可以使用遗传算法来完成,这是一类旨在复制种群在自然界中如何进化以优化函数的算法。整个工作流程看起来像这样:给定一组初始的解决方案...

  1. **评估每个解决方案的适用性。**这是专门为你的优化问题设计的。根据你的目标,你必须找到一个合适的适应度函数。
  2. 选择最适合的种群来创造后代。我们可以使用多种选择方案来挑选父母——从排序的适应值中挑选最适合的;轮盘赌选择,考虑机会;锦标赛选择,允许锦标赛的获胜者被挑选出来进行进一步发展,等等。
  3. 创造后代和变异。在数字进化中,后代不需要只有双亲。每种表型中的基因或每种溶液中的不同变量可以以几种方式混合和匹配来产生后代。未来的一代也是由以各种方式变异的某一部分人口创造的。
  4. 使用新一代解决方案重复该过程。

执行这些步骤有几种方法。遗传算法变得越来越复杂,包括寿命、多目标优化、精英主义、多样性、好奇心、存档前代等。

关于进化算法,另一个重要的概念是帕累托最优。帕累托阵线是资源配置的一种状态,这种状态使得不可能以有利于一方而不损害另一方的方式偏离这种状态。当试图平衡多目标权衡时,这个概念很方便,就像在神经架构搜索中,为了可伸缩性的目的,在寻找最优解决方案时必须平衡准确性和效率。

神经结构搜索

在 2017 年由 Zoph et al. 发表的论文中,遵循的方法是生成层超参数(例如,卷积层将需要步幅、内核大小、内核数量、是否具有跳过连接等。)依次。这些超参数就是我们所说的搜索空间。生成的架构在 CIFAR-10 数据集上训练了一定数量的时期。所使用的奖励函数是在最后 5 个立方历元中发现的最大验证准确度,并且使用加强策略梯度来更新控制器。

至少在最初的几个时代,这看起来像是创建架构的盲目尝试。

控制搜索空间

NAS 算法设计一个特定的搜索空间,并在搜索空间中搜寻更好的体系结构。上面提到的论文中卷积网络设计的搜索空间可以在下图中看到。

如果层数超过最大值,算法将停止。在他们后来的实验中,他们还在搜索空间中添加了跳过连接、批量标准化和重新激活。类似地,他们通过使用如下所示的搜索空间创建不同的递归单元架构来创建 RNN 架构。

上面提到的方法的最大缺点是,在得出明确的解决方案之前,在搜索空间中导航要花费时间。他们用了 28 天 800 个 GPU 在整个搜索空间中导航,然后提出了最好的架构。显然需要一种方法来设计能够更智能地导航搜索空间的控制器。

为此, Zoph 等人(2017) 试图通过将其分解为两个步骤来解决这个问题:

  1. 为卷积块设计单元
  2. 堆叠一定数量的块来创建架构

他们使用扩展的搜索空间,包括各种内核大小的卷积、池化和深度可分离卷积,首先创建:

  1. **正常细胞。**返回与输入相同大小的特征图的卷积单元
  2. **一个还原细胞。**返回初始输入的一半宽度和高度的特征图的卷积单元

还开发了约束搜索空间的新方法,如由 Ghiasi 等人(2019) 采用 MobileNet 框架并调整更好的特征金字塔网络,然后将 FPN 架构堆叠一定次数以生成最终架构。陈等(2019) 对不同目标检测算法的主干架构也是如此。正如 Elsken et al. (2019) 所指出的,这些方法也带来了设计宏观架构的问题,即考虑需要堆叠多少个电池块以及它们如何相互连接。

设计搜索策略

进入神经架构搜索的大多数工作都是针对这部分问题的创新:找出哪些优化方法效果最好,以及如何改变或调整它们,以使搜索过程更快地产生更好的结果,并保持一致的稳定性。已经尝试了几种方法,包括贝叶斯优化、强化学习、神经进化、网络变形和博弈论。我们将逐一研究所有这些方法。

1.贝叶斯优化

贝叶斯优化方法在神经结构搜索的早期工作中取得了很大成功。贝叶斯优化通过优化代理模型的获取函数来指导下一个评估点的选择。步骤(如本条中所述)如下:

Initialization
      - Place a Gaussian process prior on f
      - Observe f at n_0 points according to an initial space-filling
        experimental design
      - Set n to n_0

While n ≤ N do
      - Update the posterior probability distribution of f using all
	available data
      - Identify the maximiser x_n of the acquisition function over the valid
        input domain choices for parameters, where the acquisition function is
        calculated using the current posterior distribution
      - Observe y_n = f(x_n)
      - Increment n
End while

Return either the point evaluated with the largest f(x) or the point
with the largest posterior mean.

Kandasamy 等人(2018) 创建了 NASBOT,这是一种基于高斯过程的方法,用于多层感知器和卷积网络的神经架构搜索。他们通过最优传输程序计算距离度量来导航搜索空间。周等(2019) 提出 BayesNAS,将经典贝叶斯学习应用于一次架构搜索(在最后一节有更多关于一次架构搜索的内容)。

2.强化学习

强化学习已经成功地用于推动更好架构的搜索过程。如前所述,NAS 的初始方法主要是使用增强梯度作为搜索策略(例如 Zoph 等人(2016)Pham 等人(2018) )。其他方法,如最近策略优化和 Q 学习,分别由 Zoph et al. (2018)Baker et al. (2016) 应用于相同的问题。你可以在这里了解更多关于 PPO 算法的知识,在这里了解更多关于 Q 学习的知识。

有效导航搜索空间以节省宝贵的计算和存储资源的能力通常是 NAS 算法中的主要瓶颈。通常,以高验证准确性为唯一目标的模型最终会变得非常复杂——这意味着更多的参数、更多的内存需求和更长的推理时间。增强学习对解决这些问题的一个重要贡献是由 Hsu 等人(2018) 做出的,他们提出了 MONAS:使用增强学习的多目标神经架构搜索。MONAS 试图通过构建一个新的优化目标来优化可伸缩性;它不仅具有良好的验证精度,而且功耗极低。

混合奖励函数被定义为:

R = α∗Accuracy − (1−α)∗Energy

另一个减少推理延迟的有趣努力是由郭等人(2018) 进行的,他们试图以模仿人类设计架构的方式创建一种搜索方法:通过训练一个代理来学习人类设计的搜索网络结构的拓扑结构。

代理智能地在搜索空间中导航,使用反向强化学习 (IRL)构建由人类设计的拓扑引导的架构。IRL 是一种依赖于马尔可夫决策过程(MDPs)的范式,其中学徒代理的目标是从专家的演示中找到可以解释专家行为的奖励函数。强化学习试图通过优化其策略来最大化回报,而在反向强化学习中,我们得到了一个专家策略,我们试图通过找到最佳回报函数来解释它。

他们提出了一个受生物认知理论启发的镜像刺激函数来提取专家人类设计网络(ResNet)的抽象拓扑知识。他们在工作中回答了两个主要问题:

  1. 如何以代理能够理解的方式对拓扑进行编码?
  2. 如何使用这些信息来指导我们的搜索过程?

我们来看看他们是如何回答这两个问题的。

如何以代理能够理解的方式对拓扑进行编码?

作者使用网络前两层的超参数(内核大小、操作类型和索引)来编码他们所谓的“状态特征码”。它们使用特征计数将这些特征代码编码到嵌入中,如下式所述。

其中 φ 是状态特征函数, s 是环境的状态, t 代表迭代(因为在给定时间的每个状态是由我们的代理创建的架构),从 1 到 T 变化(可以是上界或根据一些收敛标准计算,在本文中不清楚); γ 表示考虑时间相关性的折扣标量。

如何使用这些信息来指导我们的搜索过程?

在这里,他们面临着经典的探索-利用权衡,其中算法需要创建拓扑结构,类似于专家网络,同时还有效地探索搜索空间。政策模仿会导致我们的网络提高强先验并抑制搜索过程。为了避免这一点,他们设计了一个镜像刺激函数:

优化的问题变得类似于寻找一个时间步长的奖励函数 *r(st) =wT φ(st)。*这是通过由吴恩达和皮特阿比尔提出的匹配函数来完成的。最终的奖励函数计算如下:

他们应用 Q 学习,使用上面显示的奖励的标准化版本来指导他们的搜索。IRLAS 能够在 ImageNet 和移动环境中实现最先进的结果。

3.神经进化

Floreano 等人 (2008)声称,对于神经网络权重的优化,基于梯度的方法优于进化方法,并且进化方法应仅用于优化架构本身。除了决定正确的遗传进化参数,如突变率、死亡率等。,也有必要评估神经网络的拓扑结构在我们用于数字进化的基因型中的确切表现。

这种表示可以是:

  1. **Direct,**我们要优化的网络的每个属性都将直接与一个基因相关联
  2. **间接,**基因型和表型之间的联系不是一对一的映射,而是一组描述如何创造个体的规则

Stanley et al. (2002) 提出了 NEAT,它使用直接编码策略来表示神经网络中的不同节点和连接。它们的基因型表示包括节点基因和连接基因,每个基因都直接描述了你的网络中不同的节点是如何连接的,它们是否活跃等等。NEAT 中的变异包括修改一个连接或添加一个新的连接。本文还讨论了竞争惯例问题——由于盲目交叉而产生更差网络的可能性。

NEAT 通过使用历史标记来解决这个问题(如上所示)。通过用历史数字标记新的进化,当需要交叉两个个体时,这可以以低得多的机会创建无功能的个体来完成。采取的另一项措施是使用物种形成,或者根据拓扑结构将不同的生成架构分组。每个新的架构只需要和它自己的“物种”竞争。

另一方面,组合模式产生网络(CPPNs)提供了一个强大的间接编码,可以用 NEAT 进化以获得更好的结果。你可以在这里了解更多关于 CPPNs 的信息,在这里大卫·哈的一篇文章中可以找到它的实现和可视化。NEAT 的另一个变种叫做 HyperNEAT ,也使用 CPPNs 进行编码,并随着 NEAT 算法发展。 Irwin-Harris et al. (2019) 提出了一种间接编码方法,使用有向无环图对不同的神经网络架构进行编码,用于进化。

Stanley 等人 (2019)强调了遗传算法中考虑多样性的能力,这增加了发现新架构的机会,也允许大规模并行探索。考虑多样性对于多重局部最优的问题也很重要,并且可以避免遗传漂移。为了帮助多样性,早期的许多工作试图扩大遗传空间,其思想是,如果进化过程被引导远离局部最优解,这将有助于探索和鼓励创新。它们包括小生境方法,如拥挤,其中一个新个体替换与其基因最相似的个体,以及适应度共享,其中个体根据基因距离聚集,并根据其集群中成员的数量进行惩罚。

尽管有时有效,但这些方法也会失败,因为这些解决方案通常不是最优的,并且适应性低。为了解决这个问题, Stanton et al. (2018) 将好奇心纳入强化学习算法中,以进行更好的探索。 Gravina 等人(2016) 引入惊喜世代元素,鼓励多元化。在进化算法中结合多样性的其他方法可以在这篇论文中找到。

正如 Elsken 等人(2018) 所指出的,使用进化算法优于其他优化方法的一个优势是它们在性能函数设计中提供的灵活性。适应度函数的范围可以从非常简单到非常复杂,与错误率或对数似然函数相比,它给了我们很大的实验空间。 Elsken et al. (2019) 通过提出柠檬水解决可扩展性、高精度、低资源消耗。这是一种用于多目标架构搜索的进化算法,允许在该方法的单次运行中,在多个目标下逼近架构的整个 Pareto 前沿,例如预测性能和参数数量。

为了减少运行神经进化方法本身的计算资源,他们为柠檬水添加了一个拉马克继承机制,该机制使用网络变形操作从其父网络中产生子网络。

4.网络变形

金等(2019) 构建了 AutoKeras,这是一个开源的 NAS 引擎,功能有点像 AutoML。他们的框架利用贝叶斯优化来指导网络形态。为了引导变形操作通过搜索空间,构建了基于编辑距离的核(因为架构表示避开了欧几里德表示)。编辑距离是指将一个网络转换成另一个网络需要多少次变形操作,核函数定义如下:

其中 d 表示建筑之间的编辑距离 *f[a] 和 f[b] ,*和 p 是将编辑距离映射到新空间的映射函数。这个新空间基于使用布尔增益定理将原始度量嵌入到一个新的空间中。对于这个 NP 难题,编辑距离近似如下。

他们通过为树形结构空间设计新的获取函数,将贝叶斯优化应用于网络变形。它们不仅改变了树结构的叶子,还改变了内部节点,允许它们变成更小的架构,避免迭代地构建更大更复杂的架构。提出了 A*搜索和模拟退火来平衡勘探和开采。他们在分类任务上取得了令人印象深刻的结果。

Kwasigroch 等人(2019) 针对检测恶性黑色素瘤的问题,提出了一种使用进化算法和爬山算法进行神经架构搜索的网络变形方法。爬山策略通过反复扰动初始解来创建新的解,直到找到最佳解。应用于 NAS 的爬山策略和进化算法启发的变形操作如下图所示。

Hill climbing algorithm for NAS

初始网络的结构是四个块[Conv、巴奇诺姆、雷卢、最大池],后面是最后一个块[Conv、巴奇诺姆、雷卢、西格蒙德]。卷积层包含 128 个跨度为 1 的 3×3 内核的过滤器,而最大池层包含跨度为 2 的 2×2 窗口。网络用热重启随机梯度下降法训练,学习率下降。

5.博弈论

刘等(2019) 提出 DARTS,一种利用博弈论概念优化神经架构的可区分架构搜索方法。他们将架构搜索问题描述为双层优化,其中一个优化问题嵌入到另一个优化问题中。 Nayman 等人(2019) 提出了一种基于最小化遗憾的决策科学概念的架构搜索方法。他们重新制定了 DARTS 方法,通过利用豌豆理论(专家建议的预测)直接优化架构权重,并提出了 XNAS。PEA 框架使用预测者/决策者来尝试顺序地做出决策,在我们的例子中,使用专家的建议来构建神经架构。

这些专家网络在每个时间步做出预测,指导预测者的决策。他们使用指数梯度规则进行优化。然后,优化器删除弱专家,并有效地将权重重新分配给剩余的专家。

模型压缩

虽然不完全在体系结构搜索的领域中,但是设计神经体系结构的一个重要考虑是模型压缩。随着时间的推移,已经采用了几种方法:量化、修剪、知识提炼等。值得一提的是施蒂尔等人(2019) 的工作,他们提出了一种基于沙普利值的博弈论方法来创建高效的网络拓扑。Shapley 值是一个博弈论问题的主要解决方案:如何在拥有不同技能的玩家联盟中最好地分配集体收益?对于玩家的子集,Shapley 值可以被认为是玩家在集体收益中的预期边际贡献。它的计算如下所示。

Notation: |F| is the size of the full coalition. S represents any subset of the coalition that doesn’t include player i, and |S| is the size of that subset. The bit at the end is just “how much bigger is the payoff when we add player i to this particular subset S”. [source]

Shapley 值和边际收益如何工作的解释可以在这里找到。

参与者可以被视为网络的结构组件。例如,在这个联盟游戏中,单一隐藏层网络中的每一个神经元都会扮演一个玩家的角色。对于收益函数,存在几种不同的可能选择,如训练损失、验证准确性等。在这项工作中,交叉熵准确性被用作支付函数,因为它的有界性质。他们从评估精度中减去基线(回归精度)来获得收益。Shapley 值通过随机采样来近似,并用于最终修剪模型,以根据哪些玩家具有最低的 Shapley 值来减少玩家的数量。

绩效评估策略

Elsken 等人(2019) 提到需要加快 NAS 方法的性能评估。测量生成的架构的性能的一个简单方法是测量验证的准确性。但是对于大的搜索空间、大的数据集和几层深度网络,这变成了一个耗时且计算量大的任务。为了避免这种情况,他们提出了几个策略,如使用通过训练获得的较少时期的低保真度估计值,或像 Real 等人(2019) 所做的那样,对一小部分数据进行训练。如果我们能够确定架构的相对等级不会因为低保真度评估而改变,那么这种方法是可行的。但是最近的研究表明事实并非如此。

另一种策略是基于学习曲线的外推。从最初几个时期的学习曲线来看,预测性能较差的架构可以终止,以加快搜索过程。刘等(2018) 另一方面,不使用学习曲线,而是建议训练一个代理模型,根据从其他新颖架构推断的属性来预测架构的性能。

Pham et al. (2018) 推广的 One-shot architecture search 是 NAS 方法,试图训练一个包含搜索空间中所有其他架构的超级架构。这是通过在由搜索空间导航策略创建的所有网络之间共享参数来实现的。一次性方法允许快速评估,因为超图的权重可以被继承,以通过训练它们来评估各种不同的架构。虽然一次性 NAS 减少了神经架构搜索所需的计算资源,但尚不清楚超图的权重继承如何影响子图的性能以及它引入架构的偏差种类。

摘要

在这篇文章中,我们回顾了神经结构搜索的问题,将它分为三个活跃的研究领域。我们看了如何设计搜索空间,如何设计搜索策略(使用贝叶斯优化、强化学习、进化算法、网络形态或博弈论),最后我们看了加速架构性能评估的不同方法(即低保真度评估、学习曲线外推和一次性学习)。

我们跳过了许多关于使用剪枝、量化和知识提取来压缩神经网络的文献,因为这更属于如何在给定父架构的情况下构建高效架构的讨论。另一篇长文可以很容易地专门讨论这个话题。我仍然觉得有必要简单提一下这个话题,因为这些技术目前正受到越来越多的关注,它们为机器学习的民主化铺平了道路。

我希望这篇文章对你有用。

在本系列的下一部分,我们将开始研究如何为多层感知器实现 NAS,以便了解这里讨论的核心概念是如何工作的。

强化学习的机器学习实践者指南:强化学习领域概述

原文:https://blog.paperspace.com/overview-of-reinforcement-learning-universe/

这是我们关于强化学习(RL)概念系列的第 2 部分,面向已经有一些机器学习经验的人。在第 1 部分中,我们主要关注马尔可夫决策过程(或 MDP),这是一种数学框架,用于对强化学习中的问题进行建模。

在这一部分中,我们将对 RL 世界进行一次简短的游览,包括基本术语、RL 和其他形式的机器学习之间的相似性和不同性、RL 算法的剖析等等。

这一部分假设您熟悉马尔可夫决策过程(MDP ),我们已经在第 1 部分的中广泛讨论过。如果您不熟悉 MDP 或者需要复习,我强烈建议您在继续阅读本文之前先阅读第 1 部分。

以下是我们将在本帖中涉及的内容。

  • 为什么要强化学习?为什么不是监督学习?
    • 优化奖励函数
    • 带标签的监督学习
    • 分配转移
  • RL 算法剖析
  • 收集数据
    • 政策外与政策内
    • 探索与开发
    • 为什么不符合政策?
    • 为什么是政策上的?
  • 计算回报
    • 在无模型设置中估计预期收益
    • 贴现因素
    • 价值函数
    • n 步返回
  • 更新政策
    • q 学习
    • 政策梯度

那么,我们开始吧。

为什么要强化学习?为什么不是监督学习?

强化学习的中心目标是最大化累积回报,或回报。为了做到这一点,代理应该找到一个策略$ \pi(s) \(来预测一个动作\) a \(,给定状态\) s $,使回报最大化。

优化奖励函数

就像我们在标准 ML 中使用梯度下降来最小化损失函数一样,我们也可以使用梯度上升来最大化回报吗?(梯度上升不是找到最小值,而是收敛到最大值。)还是有问题?

考虑回归问题的均方损失。损失的定义如下:

$$ L(X,Y;\ theta)= \ frac { 1 } { 2 }(y-\thetatx) 2 $ $

这里$ Y \(是标签,\) X $是输入,$θ$是神经网络的权重。它的梯度很容易计算。

另一方面,回报是通过增加奖励来构建的。奖励是由环境的奖励函数产生的,很多情况下我们可能不知道(无模型设置)。如果我们不知道函数,就不能计算梯度。所以,这个办法行不通!

带标签的监督学习

如果我们不知道回报函数,也许我们能做的是构造我们自己的损失函数,如果代理人采取的行动不是导致最大回报的行动,它基本上惩罚代理人。这样,代理预测的动作就变成了预测$ \theta^TX$,最优动作就变成了标签。

为了确定每一步的最佳行动,我们需要一个专家/软件。在这个方向上所做的工作的例子包括模仿学习和行为克隆,在这两种情况下,代理应该通过专家的示范来学习(请阅读相关链接)。

然而,这种方法所需的演示/标记数据的数量可能被证明是良好训练的瓶颈。人类标记是一项昂贵的业务,考虑到深度学习算法往往需要大量数据,收集如此多的专家标记数据可能根本不可行。另一方面,如果一个人有一个专家软件,那么他需要知道为什么要用人工智能来构建另一个软件来解决一个我们已经有专家软件的任务!

此外,使用专家训练数据进行监督的方法通常受到专家专业知识上限的限制。专家的政策可能是目前最好的,而不是最优的。理想情况下,我们希望 RL 代理发现最优策略(使回报最大化)。

此外,这种方法对于我们没有专家的未解决的问题不起作用。

分配转移

用专家标注的数据解决问题的另一个问题是分布变化。让我们考虑一个专家代理玩 SeaQuest 的例子,在这个游戏中,你要从海底接潜水员。该试剂具有耗尽的氧气源,并且必须定期到表面重新收集氧气。

现在假设专家代理的策略是永远不会让代理处于氧气低于 30%的状态。因此,遵循此策略收集的数据不会有氧气低于 30%的状态。

现在想象一个代理从使用上述专家的策略创建的数据集被训练。如果训练有素的代理人不完美(100%的性能是不现实的),并发现自己处于氧气约为 20%的状态,它可能没有经历过在这种状态下应采取的最佳行动。这种现象称为分布偏移,可以通过下图来理解。

模仿学习中的一个著名作品被用来对抗分配转移匕首,我在下面提供了一个链接。然而,这种方法也因花费的金钱和时间方面的高数据获取成本而受到损害。

RL 算法的剖析

在 RL 中,代理不是使用标签来学习,而是通过**试错来学习。**代理人尝试了一堆东西,观察什么能带来好的回报,什么能带来坏的回报。使用各种技术,代理人想出一个策略,该策略应该执行给出最大或接近最大回报的动作。

RL 算法有各种形状和大小,但大多数都倾向于遵循一个模板,我们在这里称之为 RL 算法的剖析。

一般来说,任何 RL 算法都将学习过程分为三个步骤。

  1. 收集数据
  2. 估计回报
  3. 完善政策。

这些步骤重复多次,直到达到期望的性能。我避免使用收敛这个词,因为对于许多深度 RL 算法来说,没有收敛的理论保证,只是它们在实践中似乎对广泛的问题都很有效。

本文的其余部分将深入探讨上述每一个问题。

收集数据

与标准的机器学习设置不同,RL 设置中没有静态数据集,我们可以重复迭代以改进我们的策略。

相反,我们必须通过与环境互动来收集数据。运行策略并存储数据。数据通常存储为四元组值$ \乐浪 s_t,a_t,r_t,s_{t+1} \rangle $其中,

  • $ s_t $是初始状态
  • $ a_t $是采取的行动
  • $ r_t \(是以\) s_t \(的形式获得\) a_t $的奖励
  • $ s_{t+1} \(是在\) s_t \(中提取\) a_t $后座席转换到的新状态

政策外与政策内

RL 算法通常在收集数据和改进策略之间交替。我们用来收集数据的策略可能与我们正在培训的策略不同。这被称为非政策学习。

收集数据的策略被称为行为策略,而我们正在训练的策略(我们计划使用测试时间)被称为目标策略。

在另一种情况下,我们可能会使用我们正在培训的相同策略来收集数据。这被称为**政策学习。**在这里,行为策略和目标策略是相同的。

让我们了解一个例子的帮助之间的区别。假设我们正在训练一个目标策略,它为我们提供了每个行动的估计回报(考虑行动是离散的),我们必须选择最大化这个估计的行动*。换句话说,我们根据行动分数贪婪地行动。*

$ $ \ pi(s)= \ under set { A \ in \ mathcal } { \ arg \ max } R _ (A)$ $

这里,$ R_ $是返回估计值。

在数据收集阶段,我们选择随机行动来与环境互动,不考虑回报估计。当我们更新我们的目标策略时,我们假设超过$\乐浪 s_t,a_t,s _ { t+1 } \ rangle$的策略所采取的轨迹将通过贪婪地选择行为 w.r.t 到估计的回报来确定。相反,当我们收集数据时,我们选择随机行动。

因此,我们的行为政策不同于我们的训练政策,即贪婪政策。因此,这是非政策培训

如果我们也使用贪婪策略进行数据收集,这将成为一个策略上的设置*。*

探索与开发

除了学分分配问题,这是 RL 中最大的挑战之一;如此之多,保证它自己的职位,但我会尝试在这里给出一个简短的概述。

让我们再举一个例子。你是一名编写生产级代码的数据科学家。尽可能使用矢量化来编写 NumPy 代码。现在,你有一个非常重要的项目,你非常想完成。

你会想,我是否应该继续在我的代码中严格使用矢量化来优化它?或者,我是否应该花些时间在我的武器库中添加更多的技术,比如广播和整形,以编写甚至更优化的代码,而这仅仅通过矢量化是可能的。如果你正朝这个方向思考,你可以阅读我们关于 NumPy 优化的 4 部分系列文章,它涵盖了上面提到的所有内容,甚至更多!

NumPy Optimization: Vectorization and Broadcasting | Paperspace BlogIn Part 1 of our series on writing efficient code with NumPy we cover why loops are slow in Python, and how to replace them with vectorized code. We also dig deep into how broadcasting works, along with a few practical examples.Paperspace BlogAyoosh Kathuria

我们以前都遇到过这种情况。我目前的工作流程是否足够好,或者我是否应该探索 Docker?我现在的 IDE 够好吗,还是应该学 Vim?所有这些场景基本上都是在我们是否想要探索更多以追求新工具,还是利用我们现有的知识之间的选择!

在 RL 算法的世界中,代理从探索所有行为开始。在适当的训练过程中,通过反复试验,它在某种程度上认识到什么可行,什么不可行。

一旦它的表现达到了一个不错的水平,随着训练的进一步进行,它应该利用它所学到的东西变得更好吗?或者,它应该探索更多,也许发现新的方法,最大限度地提高回报。这就是 exploration v/s 剥削困境的核心所在。

RL 算法必须平衡这两者,以确保良好的性能。最好的解决方案是在完全探索和剥削行为的中间。有多种方法可以将这些行为的平衡融入数据收集策略中。

在非策略设置中,数据收集策略可能完全随机,导致过度探索性行为。或者,一个人可以根据训练策略采取一个步骤,或者以某种概率随机地采取一个随机行动*。这是在深度 RL 中普遍使用的技术。*

p = random.random()

if p < 0.9:
	# act according to policy 
else:
	# pick a random action

在基于策略的设置中,我们可以使用一些噪声来干扰操作,或者预测概率分布的参数,并从中进行采样以确定要采取的操作。

为什么不符合政策?

非政策学习的最大好处是它帮助我们探索。目标政策在本质上可能会变得非常具有剥削性,不能很好地探索环境,只能满足于次优政策。探索性的数据收集策略将提供从环境到训练策略的不同状态。由于这个原因,行为政策也被称为探索性政策。

此外,偏离策略的学习允许代理使用来自旧迭代的数据,而对于基于策略的学习,每次迭代都必须收集新数据。然而,如果数据收集操作是昂贵的,则 on-policy 可能增加训练过程的计算时间。

为什么是政策上的?

如果非政策学习在鼓励探索方面如此伟大,为什么我们还要使用政策学习呢?嗯,原因与策略如何更新的数学原理有关(步骤 3)。

对于非策略更新,我们基本上是使用另一个策略生成的轨迹来更新目标策略的行为。给定类似的初始状态和行动,我们的目标政策可能会导致不同的轨迹和回报。如果这些差异太大,更新可能会以意想不到的方式修改策略。

正是因为这个原因,我们并不总是进行非政策学习。即使我们这样做了,只有当两个策略之间的差异不是很大时,更新才能很好地工作,因此,我们定期同步我们的目标和行为策略,并在进行偏离策略的学习更新时使用重要性采样等技术。我把关于精确数学和重要性抽样的讨论留到另一篇文章中。

计算回报

RL 算法学习循环的下一部分是计算回报。强化学习的核心目标是最大化预期累积回报,或者简单地说是回报。**预期这个词基本上考虑了政策和环境的可能随机性。

当我们处理环境在本质上是偶发的情况时(即代理的生命最终终止。就像马里奥这样的游戏,代理人可以完成游戏,或者因摔倒/被咬而死亡),回报被称为有限期限回报。在处理连续环境(无终端状态)的情况下,该回报被称为无限期回报。

让我们只考虑偶发的环境。我们的策略可能是随机的,即给定一个状态$ s $ ,它可能采取不同的轨迹$ \tau \(到达最终状态\) s_T $。

我们可以定义这些轨迹的概率分布,使用

  1. 政策(给我们各种行动的可能性),以及
  2. 环境的状态转移函数(它给出了给定状态和动作的未来状态的分布)。

从初始状态$s _ 1$开始并在终止状态$s _ T$结束的任何轨迹的概率由下式给出

一旦我们有了轨迹的概率分布,我们就可以计算所有可能轨迹的预期收益,如下所示。

RL 算法旨在最大化这个量。


注意,奖励和回报是非常容易混淆的。环境为代理人的每一步提供奖励,而回报是这些个人奖励的总和。

我们可以将时间$ t \(的回报\) G_t $定义为该时间的奖励总和。

\(G_t = \sum_{t}^{T} r(s_t,a_t)\)

一般来说,当我们谈论最大化期望回报作为解决 RL 任务的目标时,我们谈论最大化$ R_1 $即从初始状态的回报。

在数学上,我们优化算法以最大化每$ R_t \(。如果算法能够最大化每\) R_t \(的值,那么\) R_1 \(会自动最大化。相反,试图最大化\) R _ { 1 是荒谬的...t-1} \(使用在时间\) t $采取的行动。该策略不能改变以前采取的操作。

为了区分$ R_1 \(return 和一般 return\) R_t $,我们称前者为测试 return ,而称后者为 return。

The return for any state is sum of cumulative reward from that state to the terminal state.

在无模型设置中估计预期收益

回想一下上一篇文章,很多时候我们试图直接从经验中学习,但是我们无法使用状态转换函数。这些设置是无模型 RL 的一部分。在其他情况下,状态空间可能如此之大,以至于计算所有轨迹都很困难。

在这种情况下,我们基本上会多次运行该策略,并对单个回报进行平均,以获得回报的估计值。下面的表达式估计了预期收益。

$$ \ frac { 1 } \sum_^ \ sum _ ^ r(s _ { I,t },a_{i,t }),$ $

其中$ N \(是运行次数,而\) r(s_{i,t},a_{i,t}) \(是在\) i^ \(运行中在时间\) t $获得的奖励

我们运行的次数越多,我们的估计就越准确。

贴现因素

上述预期回报公式有几个问题。

  1. 在没有终结状态的继续任务的情况下,期望收益是无穷和。从数学上讲,这个总和可能不会收敛于任意的奖励结构。
  2. 即使在有限范围的情况下,一个轨迹的回报也包括从一个状态开始的所有回报,比如$ s \(一直到最终状态\) s_T $。我们平等地权衡所有的回报,不管它们是早出现还是晚出现。这与现实生活中的事情完全不同。

第二点基本上问的是“在计算回报的同时,有必要把未来的所有回报都加起来吗?”也许我们可以只做下一个 10 或 100?这也将解决持续任务带来的问题,因为我们不必担心无穷和。

假设你正试图教一辆汽车如何在 RL 中行驶。这辆车每走一步都会得到 0 到 1 之间的奖励,这取决于它的速度。一旦我们记录了 10 分钟或更早的驾驶时间,情节就会终止,以防发生碰撞。

想象一下,汽车与车辆靠得太近,必须减速以避免碰撞和本集的结束。请注意,碰撞可能仍在几步之外。因此,在碰撞发生之前,我们仍然会因为撞车前的高速行驶而获得奖励。如果我们只考虑立即或 3 立即回报的奖励,那么我们将获得高回报,即使我们在碰撞过程中!

假设,10 分钟的一集分成 100 个时间步。汽车全速前进,直到第 80 个时间步,之后为 0,因为它在第 80 个时间步发生碰撞。理想情况下,它应该减速以避免碰撞。

Using Instantaneous Rewards as Return.

现在让我们用未来的三个回报来计算回报。

Using three future rewards as return.

橙色线代表 75 时间步长标记。车辆至少应该在这里开始减速。换句话说,返回应该更少,因此不鼓励这种行为(碰撞路径上的速度)。只有在第 78 个时间步之后,回报才开始下降,这时停止已经太晚了。第 75 个时间步超速的回报应该也会少。

我们想要激励远见的回报。也许我们应该加入更多的未来回报来计算我们的回报。

如果我们把所有的未来收益像原始公式一样加起来。这样,我们的回报随着我们的前进而不断下降,即使是在时间步长 1-50。注意,在 T = 1-50 时采取的措施对导致 T = 80 时碰撞的措施几乎没有影响。

Adding all future rewards to the return

最佳时机介于两者之间。为了做到这一点,我们引入了一个新的参数$ \gamma $来做这件事。我们将预期收益改写如下。

$ \(\ frac { 1 } { n } \sum_{i}^{n} \ sum _ { t = t ' } ^ { T} \gamma^{t-t ' } r(s _ { I,t },a_{i,t})\)

这是贴现收益$ G_1 $的样子。

$ $ g _ 1 = r _ 1+\ gamma r _ { 2 }+∞r _ 3+\ gamma = 3r _ 4+……$ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $

这通常被称为贴现预期回报。$ \gamma $称为折扣系数

当使用奖励来计算我们的回报时,参数$ \gamma \(让我们控制我们对未来的展望。更高的\) \gamma \(值会让我们看到更远的未来。\) \gamma \(的值介于 0 和 1 之间。奖励的权重随着每个时间步呈指数下降。实践中常用的值是 0.9、0.99 和 0.999。下图显示了\) \gamma $如何影响我们的汽车示例的回报。

A higher \(\gamma\) means that the the agent looks much further into the future. Conversely, the effect of collision on the return is seen on much earlier time steps with a higher \(\gamma\)

在合理的假设下,贴现无限和可以被证明是收敛的,这使得它们在没有终态的连续环境的情况下是有用的。

价值函数

虽然我们可以用上面提到的方法定义计算贴现回报,但这里有一个问题。计算任何一个州的回报都需要你先完成这一集或者执行大量的步骤,然后才能计算回报。

计算回报的另一种方法是提出一个函数,它将州$ s \(和保单\) \pi $作为输入,并直接估计贴现回报的价值,而不是我们必须将未来的回报相加。

策略$ \pi \(的值函数写为\) V^\pi (s)\(。如果我们从状态\) s \(开始遵循策略\) \pi $的话,它的值等于预期收益。在 RL 文献中,以下符号用于描述价值函数。

$ $ v^\pi(s)= \ mathbb [g _ t | s _ t = s]$ $

注意,我们没有把$ G_t \(写成州和政策的函数。这是因为我们把\) G_t $仅仅当作环境回报的总和。我们不知道(在无模型环境中)这些奖励是如何计算的。

然而,$ V^\pi(s ) $是一个函数,我们用神经网络这样的估计器来预测自己。在这里,它被写成符号中的一个函数。

价值函数的一个微小修改是 Q 函数,它估计如果我们在州$ s \(采取行动\) a \(在政策\) \pi $下的预期贴现回报。

$ \(q^\pi(s,a)= \ mathbb { e }[g _ t | s _ t = s,A_t = a]\)

最大化所有状态的价值函数的策略被称为最优策略。

在 RL 中有相当多的学习价值函数的方法。如果我们处于基于模型的 RL 环境中,并且状态空间不是非常大,那么我们可以通过动态编程来学习值函数。这些方法使用价值函数的递归公式,称为贝尔曼方程来学习它们。我们将在后面关于价值函数学习的文章中深入讨论贝尔曼方程。

在状态空间太大的情况下,或者我们处于无模型设置中,我们可以使用监督学习来估计价值函数,其中标签是通过平均许多回报来估计的。

n 步返回

我们可以混合回报和价值函数,得出所谓的 n 步回报。

$ \(g^{n}_t = r _ { t }+\伽马 r_{t+1} +...+\伽马^{n-1} r _ { t+n-1 }+\伽马^ 3 V(s_{t+n})\)

这里有一个 3 步返回的例子。

$ \(g^{3}_t = r _ { t }+\伽马 r _ { t+1 }+\gamma^2 r _ { t+2 }+\伽马^ 3 V(s_{t+3})\)

在文献中,使用价值函数来计算剩余的回报(在 n 步之后)也被称为自举。

更新政策

在第三部分,我们更新政策,使其采取行动最大化贴现期望报酬(DER)

在 Deep RL 中有几种流行的方法来学习最优策略。这两种方法都包含可以使用反向传播进行优化的目标。

q 学习

在 Q 学习中,我们学习一个 Q 函数,并通过对每个动作的 Q 值表现贪婪来选择动作。我们优化损失,目的是学习 Q 函数。

政策梯度

我们还可以通过对其进行梯度上升来最大化预期回报。这个梯度被称为政策梯度。

在这种方法中,我们直接修改策略,使得在所收集的经验中导致更多奖励的行为更有可能发生,而在策略的概率分布中导致不好的奖励的行为不太可能发生。

另一类算法叫做行动者-批评家算法,结合了 Q 学习和策略梯度。

结论

这是我们结束这一部分的地方。在这一部分中,我们讨论了使用 RL 方法的动机和 RL 算法的基本结构。

如果你觉得我对最后一部分,也就是更新政策的那一部分不够重视,那也是应该的。我把这一节写得很短,因为提到的每种方法都有它自己的位置!在接下来的几周里,我将为每一种方法撰写专门的博客文章,并附上您可以运行的代码。在那之前,代码速度!

进一步阅读

  1. 模仿学习概述
  2. 数据集聚合或 Dagger 方法上的幻灯片。
  3. RL -重要性抽样
  4. 贝尔曼方程

卷积神经网络中的填充

原文:https://blog.paperspace.com/padding-in-convolutional-neural-networks/

填充是卷积神经网络中的一个基本过程。虽然不是强制性的,但这是一个在许多 CNN 架构中经常使用的过程。在这篇文章中,我们将探讨为什么和如何做到这一点。

卷积的机制

图像处理/计算机视觉上下文中的卷积是一个过程,通过该过程,图像被滤波器“扫描”,以便以某种方式对其进行处理。让我们在细节上稍微技术性一点。

对计算机来说,图像只是一组数字类型(整数或浮点数),这些数字类型被恰当地称为像素。事实上,1920 像素乘 1080 像素(1080p)的高清图像只是一个具有 1080 行和 1920 列的数字类型的表格/数组。另一方面,滤波器本质上是相同的,但通常尺寸较小,常见的(3,3)卷积滤波器是 3 行 3 列的阵列。

当对图像进行卷积时,对图像的连续片应用滤波器,其中在滤波器的元素和该片中的像素之间发生逐元素乘法,然后累积和作为其自己的新像素返回。例如,当使用(3,3)滤波器执行卷积时,9 个像素被聚集以产生单个像素。由于这种聚集过程,一些像素丢失了。

The convolution process

Filter scanning over an image to generate a new image via convolution.

丢失的像素

要理解像素丢失的原因,请记住,如果卷积滤波器在扫描图像时超出界限,则特定的卷积实例将被忽略。举例来说,考虑一个 6×6 像素的图像被一个 3×3 的滤波器卷积。如下图所示,前 4 个卷积落在图像内,为第一行生成 4 个像素,而第 5 个和第 6 个卷积落在边界外,因此被忽略。同样,如果滤波器向下移动 1 个像素,则对于第二行,同样的模式重复,同时损失 2 个像素。当该过程完成时,6×6 像素图像变成 4×4 像素图像,因为它在 dim 0 (x)中丢失了 2 列像素,在 dim 1 (y)中丢失了 2 行像素。

Convolution instances using a 3x3 filter.

同样,如果使用 5×5 滤波器,则在 dim 0 (x)和 dim 1 (y)中分别丢失 4 列和 4 行像素,从而产生 2×2 像素图像。

Convolution instances using a 5x5 filter.

不要相信我的话,试试下面的函数,看看是不是真的如此。随意调整论点。

import numpy as np
import torch
import torch.nn.functional as F
import cv2
import torch.nn as nn
from tqdm import tqdm
import matplotlib.pyplot as plt

def check_convolution(filter=(3,3), image_size=(6,6)):
    """
    This function creates a pseudo image, performs
    convolution and returns the size of both the pseudo
    and convolved image
    """
    #  creating pseudo image
    original_image = torch.ones(image_size)
    #  adding channel as typical to images (1 channel = grayscale)
    original_image = original_image.view(1, 6, 6)

    #  perfoming convolution
    conv_image = nn.Conv2d(1, 1, filter)(original_image)

    print(f'original image size: {original_image.shape}')
    print(f'image size after convolution: {conv_image.shape}')
    pass

check_convolution()

像素丢失的方式似乎有一种模式。似乎每当使用 m×n 滤波器时,m-1 列像素在 dim 0 中丢失,n-1 行像素在 dim 1 中丢失。让我们更数学一点...

图像大小= (x,y)
滤波器大小= (m,n)
卷积后的图像大小= (x-(m-1),y-(n-1)) = (x-m+1,y-n+1)

每当使用大小为(m,n)的滤波器对大小为(x,y)的图像进行卷积时,就会产生大小为(x-m+1,y-n+1)的图像。

虽然这个等式可能看起来有点复杂(没有双关的意思),但它背后的逻辑很容易理解。由于大多数普通滤波器在尺寸上是正方形的(在两个轴上尺寸相同),所有要知道的是一旦使用(3,3)滤波器完成卷积,2 行和 2 列像素丢失(3-1);如果使用(5,5)过滤器,则丢失 4 行和 4 列像素(5-1);如果使用(9,9)过滤器,你猜对了,8 行和 8 列像素丢失(9-1)。

丢失像素的含义

丢失 2 行和 2 列像素可能看起来没有那么大的影响,特别是在处理大图像时,例如,would 图像(3840,2160)在被(3,3)滤波器卷积成(3838,2158)时,看起来不受丢失 2 行和 2 列像素的影响,丢失了其总像素的大约 0.1%。当涉及多层卷积时,问题就开始出现了,这在当前 CNN 体系结构中是很典型的。以 RESNET 128 为例,该架构具有大约 50 (3,3)个卷积层,这将导致大约 100 行和 100 列像素的损失,将图像大小减小到(3740,2060),图像总像素的大约 7.2%的损失,所有这些都没有考虑下采样操作。

即使是浅架构,丢失像素也会产生巨大的影响。在大小为(28,28)的 MNIST 数据集中的图像上使用仅应用了 4 个卷积层的 CNN 将导致 8 行和 8 列像素的损失,将其大小减小到(20,20),损失了其总像素的 57.1%,这是相当可观的。

由于卷积运算是从左到右、从上到下进行的,因此最右边和最下边的像素会丢失。因此,可以有把握地说 卷积导致边缘像素 的丢失,这些像素可能包含手头的计算机视觉任务所必需的特征。

填充作为解决方案

因为我们知道像素在卷积后必然会丢失,所以我们可以通过预先添加像素来抢占先机。例如,如果要使用(3,3)滤波器,我们可以预先向图像添加 2 行和 2 列像素,以便在进行卷积时,图像大小与原始图像相同。

让我们再来一点数学...

图像尺寸= (x,y)
滤波器尺寸= (m,n)

填充后的图像大小= (x+2,y+2)

使用等式==> (x-m+1,y-n+1)

卷积后的图像大小(3,3) = (x+2-3+1,y+2-3+1) = (x,y)

分层填充

因为我们处理的是数字数据类型,所以额外像素的值也应该是数字。通常采用的值是像素值零,因此经常使用术语“零填充”。

抢先将像素的行和列添加到图像阵列的问题是,必须在两侧均匀地完成。例如,当添加 2 行和 2 列像素时,它们应该被添加为顶部一行、底部一行、左侧一列和右侧一列。

看下面的图像,已经添加了 2 行和 2 列来填充左边的 6 x 6 数组,同时在右边添加了 4 行和 4 列。如前一段所述,附加的行和列沿所有边缘均匀分布。

仔细观察左边的数组,似乎 6×6 的 1 数组被一层 0 包围起来,所以 padding=1。另一方面,右边的数组似乎被两层零包围,因此 padding=2。

Layers of zeros added via padding.

将所有这些放在一起,可以有把握地说,当一个人在准备(3,3)卷积时想要添加 2 行和 2 列像素时,他需要一个单层填充。在相同的叶片中,如果需要添加 6 行和 6 列像素以准备(7,7)卷积,则需要 3 层填充。用更专业的术语来说,

给定大小为(m,n)的滤波器,需要(m-1)/2 层填充以在卷积后保持图像大小不变;假设 m=n 并且 m 是奇数。

浸轧工艺

为了演示填充过程,我编写了一些普通代码来复制填充和卷积的过程。

首先,让我们看看下面的填充函数,该函数接受一个图像作为参数,默认填充层为 2。当 display 参数保留为 True 时,该函数通过显示原始图像和填充图像的大小来生成迷你报告;还会返回两个图像的绘图。

def pad_image(image_path, padding=2, display=True, title=''):
      """
      This function performs zero padding using the number of 
      padding layers supplied as argument and return the padded
      image.
      """

      #  reading image as grayscale
      image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

      #  creating an array of zeros
      padded = arr = np.zeros((image.shape[0] + padding*2, 
                               image.shape[1] + padding*2))

      #  inserting image into zero array
      padded[int(padding):-int(padding), 
             int(padding):-int(padding)] = image

      if display:
        print(f'original image size: {image.shape}')
        print(f'padded image size: {padded.shape}')

        #  displaying results
        figure, axes = plt.subplots(1,2, sharey=True, dpi=120)
        plt.suptitle(title)
        axes[0].imshow(image, cmap='gray')
        axes[0].set_title('original')
        axes[1].imshow(padded, cmap='gray')
        axes[1].set_title('padded')
        axes[0].axis('off')
        axes[1].axis('off')
        plt.show()
        print('image array preview:')  
      return padded

Padding function.

为了测试填充功能,考虑下面的图片大小(375,500)。将此图像通过 padding=2 的 padding 函数应该会产生相同的图像,在左右边缘有两列零,在顶部和底部有两行零,从而将图像大小增加到(379,504)。我们来看看是不是这样...

Image of size (375, 500)

pad_image('image.jpg')

输出:
原始图像尺寸:(375,500)
填充图像尺寸:(379,504)

Notice the thin line of black pixels along the edges of the padded image.

有用!请随意在您可能找到的任何图像上尝试该功能,并根据需要调整参数。下面是复制卷积的普通代码。

def convolve(image_path, padding=2, filter, title='', pad=False):
      """
      This function performs convolution over an image 
      """

      #  reading image as grayscale
      image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

      if pad:
        original_image = image[:]
        image = pad_image(image, padding=padding, display=False)
      else:
        image = image

      #  defining filter size
      filter_size = filter.shape[0]

      #  creating an array to store convolutions
      convolved = np.zeros(((image.shape[0] - filter_size) + 1, 
                        (image.shape[1] - filter_size) + 1))

      #  performing convolution
      for i in tqdm(range(image.shape[0])):
        for j in range(image.shape[1]):
          try:
            convolved[i,j] = (image[i:(i+filter_size), j:(j+filter_size)] * filter).sum()
          except Exception:
            pass

      #  displaying results
      if not pad:
        print(f'original image size: {image.shape}')
      else:
        print(f'original image size: {original_image.shape}')
      print(f'convolved image size: {convolved.shape}')

      figure, axes = plt.subplots(1,2, dpi=120)
      plt.suptitle(title)
      if not pad:
        axes[0].imshow(image, cmap='gray')
        axes[0].axis('off')
      else:
        axes[0].imshow(original_image, cmap='gray')
        axes[0].axis('off')
      axes[0].set_title('original')
      axes[1].imshow(convolved, cmap='gray')
      axes[1].axis('off')
      axes[1].set_title('convolved')
      pass

Convolution function

对于过滤器,我选择了值为 0.01 的(5,5)数组。这背后的想法是,在求和以产生单个像素之前,过滤器将像素强度降低 99%。简单来说,这个滤镜应该对图像有模糊效果。

filter_1 = np.ones((5,5))/100

filter_1
[[0.01, 0.01, 0.01, 0.01, 0.01]
 [0.01, 0.01, 0.01, 0.01, 0.01]
 [0.01, 0.01, 0.01, 0.01, 0.01]
 [0.01, 0.01, 0.01, 0.01, 0.01]
 [0.01, 0.01, 0.01, 0.01, 0.01]]

(5, 5) Convolution Filter

在没有填充的原始图像上应用过滤器应该产生大小为(371,496)的模糊图像,丢失 4 行和 4 列。

convolve('image.jpg', filter=filter_1)

Performing convolution without padding

输出:
原始图像尺寸:(375,500)
卷积图像尺寸:(371,496)

(5, 5) convolution without padding

但是,当 pad 设置为 true 时,图像大小保持不变。

convolve('image.jpg', pad=True, padding=2, filter=filter_1)

Convolution with 2 padding layers.

输出:
原始图像尺寸:(375,500)
卷积图像尺寸:(375,500)

(5, 5) convolution with padding

让我们重复相同的步骤,但这次使用(9,9)过滤器...

filter_2 = np.ones((9,9))/100
filter_2

filter_2
[[0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01],
 [0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01],
 [0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01],
 [0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01],
 [0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01],
 [0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01],
 [0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01],
 [0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01],
 [0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01]])

(9, 9) filter

如果没有填充,生成的图像会缩小...

convolve('image.jpg', filter=filter_2)

输出:
原始图像尺寸:(375,500)
卷积图像尺寸:(367,492)

(9, 9) convolution without padding

使用(9,9)过滤器,为了保持图像大小不变,我们需要指定填充层为 4 (9-1/2),因为我们将向原始图像添加 8 行和 8 列。

convolve('image.jpg', pad=True, padding=4, filter=filter_2)

输出:
原始图像尺寸:(375,500)
卷积图像尺寸:(375,500)

(9, 9) convolution with padding

从 PyTorch 的角度来看

为了便于说明,我在上一节中选择使用普通代码来解释这些过程。可以在 PyTorch 中复制相同的过程,但是要记住,由于 PyTorch 会随机初始化不是为任何特定目的而设计的过滤器,因此生成的图像很可能很少或没有经过变换。

为了演示这一点,让我们修改前面一节中定义的 check_convolution()函数...

def check_convolution(image_path, filter=(3,3), padding=0):
    """
    This function performs convolution on an image and 
    returns the size of both the original and convolved image
    """

    image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

    image = torch.from_numpy(image).float()

    #  adding channel as typical to images (1 channel = grayscale)
    image = image.view(1, image.shape[0], image.shape[1])

    #  perfoming convolution
    with torch.no_grad():
      conv_image = nn.Conv2d(1, 1, filter, padding=padding)(image)

    print(f'original image size: {image.shape}')
    print(f'image size after convolution: {conv_image.shape}')
    pass

Function perform convolution using the default PyTorch convolution class

注意,在函数中我使用了默认的 PyTorch 2D 卷积类,函数的填充参数直接提供给卷积类。现在让我们尝试不同的过滤器,看看产生的图像大小...

check_convolution('image.jpg', filter=(3, 3))

(3, 3) convolution without padding

输出:
原图尺寸:火炬。Size(1,375,500)
卷积后的图像大小:torch。尺寸(1,373,498)


check_convolution('image.jpg', filter=(3, 3), padding=1) 

(3, 3) convolution with one padding layer.-

输出:
原图尺寸:火炬。Size(1,375,500)
卷积后的图像大小:torch。尺寸(1,375,500)


check_convolution('image.jpg', filter=(5, 5))

(5, 5) convolution without padding-

输出:
原图尺寸:火炬。Size(1,375,500)
卷积后的图像大小:torch。尺寸(1,371,496)


check_convolution('image.jpg', filter=(5, 5), padding=2)

(5, 5) convolution with 2 layers of padding-

输出:
原图尺寸:火炬。Size(1,375,500)
卷积后的图像大小:torch。尺寸(1,375,500)

从上面的例子可以明显看出,当卷积在没有填充的情况下完成时,得到的图像尺寸减小了。但是,当使用正确数量的填充层进行卷积时,生成的图像与原始图像大小相等。

结束语

在这篇文章中,我们已经能够确定卷积过程确实会导致像素丢失。我们还能够证明,在卷积之前,在一个称为填充的过程中,抢先向图像添加像素,可以确保图像在卷积之后保持其原始大小。

欢迎回来申请 ATG 奖学金!

原文:https://blog.paperspace.com/paperspace-advanced-technology-group-fellowships/

在全球疫情期间中断了一年之后,我们非常激动地宣布,我们将重新召集纸张空间高级技术集团

https://www.youtube.com/embed/4cuz9cgQ1sc?feature=oembed

Paperspace CEO Dillon Erb shares the critical role ATG Fellows play at Paperspace

ATG 研究员承担与机器学习和深度学习相关的重要研究项目,并在更大的 Paperspace R&D 组织中发挥重要作用。

ATG 奖学金为期 10-15 周,是带薪的全职职位。该计划旨在吸引希望在 Paperspace engineering org 的支持和合作下从事机器学习和深度学习研究的研究生和研究生。

我们依靠 ATG 研究员获得广泛的专业知识。我们与 ATG 研究员一起设计有意义的研究项目,以探索尖端的机器学习技术和库,并推动机器学习的可能性。

今年,我们希望支持三个研究方向:

  • 深度技术 -这一研究方向旨在推动当前机器学习算法、应用或基础知识的极限。过去的研究员承担了 GPU 内核开发、对抗性自动编码器、自动 ml 参数空间探索策略和深度学习领域的其他高级主题。
  • 工具/界面 -该研究方向旨在推动 ML 界面(GUI、CLI 等)的发展。诸如实验跟踪、可视化技术、新的分布式训练体系结构以及复杂数据和交互建模的新方法等主题都属于该研究领域的范围。过去的研究员一直致力于通过新的抽象和接口使预训练的深度学习模型更容易被更广泛的受众访问。各种推理架构和设备约束的模型优化也是一个感兴趣的领域。
  • 教育/可及性 -这一研究方向旨在通过教育和宣传来扩大现有 ML 和深度学习技术的可及性。随着越来越多的高级课题来自学术界,专家和新手之间的鸿沟越来越大。此外,向新受众和用户开放深度学习的最佳方式是什么,而不仅仅是领域专家?公平、偏见、解释和公开的问题是这次谈话的中心。

Advanced Technologies GroupPaperspace ATG is the internal R&D arm of Paperspace and is tasked with exploring advanced topics in machine learning, data engineering, and UI/UX for developing intelligent applications.

Paperspace 上次支持 ATG 奖学金是在 2018 年和 2019 年。ATG 研究员来自广泛的学术背景。我们接待了来自领先的人工智能和人工智能机构的研究人员,包括 NYU 大学、佐治亚理工学院和哈佛大学。前两批人继续攻读博士学位,成为领先公司的研究科学家,并建立了风险投资支持的机器学习公司,如 T2 跑道 T3。

https://www.youtube.com/embed/XRVjZHpe4iQ?feature=oembed

Past ATG Fellows discuss their time at Paperspace

Paperspace ATG 项目的一个显著特点是,在 Paperspace 世界级工程组织的全力支持下,可以自由探索研究课题。

对于一家软件公司来说,我们拥有异常广阔的表面积,因此我们在从低延迟流到分布式系统架构、加速计算和生产级机器学习系统编排的所有领域都拥有主题专业知识。

作为一名 ATG 研究员,你的工作很有可能会提供给近 50 万用户。您将能够影响 Paperspace 产品的未来,同时还能进行有意义的合作研究。

我们邀请您今天就申请!

Advanced Technologies GroupPaperspace ATG is the internal R&D arm of Paperspace and is tasked with exploring advanced topics in machine learning, data engineering, and UI/UX for developing intelligent applications.

纸空间+ RDP

原文:https://blog.paperspace.com/paperspace-and-rdp/

几十年来,微软的同名远程桌面协议(RDP)一直是访问远程桌面的支柱,我们很高兴地宣布, RDP 现已集成到 Paperspace 平台中。

虽然不像 Paperspace 原生协议那样“简洁”,但 RDP 是稳定的、功能齐全的,并且为大多数 IT 专业人员所熟悉。RDP 通过第三方应用和微软自己提供的版本在 Windows、Mac 和 Linux 上得到支持。

为什么这很重要

RDP 提供的最重要的好处之一是能够从目前市场上的大多数瘦客户机上访问纸张空间。另一个好处是,RDP 是作为安卓和苹果智能手机/平板电脑上的移动应用程序提供的。

RDP 的主要特色:

  • 多显示器支持
  • 音频和视频重定向
  • 打印机转发
  • USB 和本地磁盘重定向
  • 复制/粘贴同步

要开始设置您自己的瘦客户机,请在此注册。

Paperspace 关闭 1300 万美元,推动增长

原文:https://blog.paperspace.com/paperspace-closes-13m-to-fuel-growth/

我们很高兴地宣布,我们已经完成了来自正弦波风险投资电池风险投资英特尔投资的 1300 万美元首轮投资、以及来自初始资本的后续投资。最新一轮融资使我们的资金总额达到 1900 万美元。

我们在 2014 年秋季创立了 Paperspace,目标很简单:让云计算变得更容易实现。当时,公共云对于托管网站和服务很有用,但对于其他类型的应用程序来说并不是理想的环境。我们认为,有大量新的应用程序也可以从利用公共云的巨大规模和灵活性中受益。我们认为 GPU 加速和简化用户体验是我们的主要优势。

在 2015 年完成 Y Combinator 后,我们侵入了几个版本,并决定采用 GPU 驱动的虚拟机来支持各种新兴的用例。最引人注目的是当时有点小众的观众,这是机器学习中一个名为深度学习的新兴领域。这对我们来说是一个完美的受众,原因有二:我们在公共云中有最大的 GPU 实例选择,该领域的其他参与者使 IaaS 对这种新型开发人员来说太复杂了。

在过去的几年里,只有少数几个工程师,我们已经能够达到一些惊人的里程碑。我们拥有超过 150,000 个活跃用户帐户,每天都依赖于 Paperspace 的企业数量也在快速增长。我们发布了迄今为止最重要的产品 T1,我们与 T2 一些令人难以置信的机构 T3 合作,我们建立了一个专注于 ML 的开发者社区。

下一章

我们将继续专注于构建世界上最好的深度学习平台。我们强烈地感觉到,训练和部署模型的过程对于个人开发者和大规模运营的公司来说都过于昂贵和复杂。我们将努力消除瓶颈,简化这项技术的投资过程。

多亏你

我们因社区的支持而感到谦卑。您的反馈推动了我们在 Paperspace 的产品开发。我们也有很好的机会直接与开发者合作,在我们的博客上创建原创教程内容(在此提交您的作品!).最后,我们最近推出了一个社区,用户和爱好者可以在公共论坛上发布教程和分享相关内容。

这个新的资本将允许我们参加更多的会议,举办更多的聚会,支持更多的研究人员,并与我们的客户和社区进行更多的接触。

来自整个 Paperspace 团队:

谢谢!

ps:我们正在全面招聘。你可以点击查看我们的职业页面

图纸空间部署指南

原文:https://blog.paperspace.com/paperspace-deployment-guide/

云中的完整 VDI 实施

Paperspace 在云中提供了一个完整的虚拟桌面解决方案,没有内部 VDI 带来的任何麻烦。它易于设置、易于管理,并且可以大幅削减 It 成本。Paperspace 使员工能够从世界上任何地方的任何设备安全地访问他们的应用程序和文件。

了解如何在几分钟内开始:
下载部署指南

paperspace 渐变与 amazon pagemaker

原文:https://blog.paperspace.com/paperspace-gradient-vs-amazon-sagemaker/

介绍

Paperspace Gradient 和 Amazon SageMaker 是两个最受欢迎的端到端机器学习平台。

端到端机器学习平台是指支持从研究或原型阶段到大规模部署的机器学习模型开发的工具集。

平台意味着存在某种程度的自动化,使得执行机器学习任务变得更加容易。

Paperspace Gradient 和 Amazon SageMaker 之间的相似之处

Paperspace Gradient 和 Amazon SageMaker 使机器学习模型更容易从研究走向生产。

以前,需要将工具和服务组合在一起才能生产一个机器学习模型,而现在,通过您选择的框架和软件包,可以在一个平台上编写、管理和编排模型。

结果是模型开发过程比拼凑的工作流更健壮、可靠、可预测、可再现,并且随着时间的推移对大多数应用程序更有用。

使用 Gradient 或 SageMaker 等完全托管服务的优势有很多:

  • 协作:机器学习平台包括一个云 IDE 或笔记本环境,可以轻松共享和部署代码
  • 可见性:模型会随着时间而变化,可见的测试和训练数据和结果是确保模型持续改进的唯一方法
  • 版本化:版本化是提高故障隔离和模型效率的关键
  • 再现性:与团队合作需要重现实验的能力

Gradient 和 SageMaker 都具有加快机器学习模型开发周期的功能。

而非 使用托管服务开发机器学习模型的缺点还包括其他几个因素:

  • 拼凑起来的工具很难扩展,尤其是在与团队合作时
  • 手动流程更容易引入错误
  • 成本可能会失控,因为拼凑起来的工作流的每一部分都可能有相关的成本
  • 兼容性成为一个复杂的问题
  • 随着时间的推移,模型的改进越来越少,这代表了业务效率的对数损失

随着您的机器学习部署的规模和复杂性的增长,ML 平台将对您的团队变得更加有用。以下文档介绍了选择 ML 平台时需要考虑的一些重要因素。

Paperspace Gradient 和 Amazon SageMaker 之间的差异

Paperspace Gradient 和亚马逊 SageMaker 的主要区别在于,Gradient 是一个易于使用的编排工具,可以大规模编写和部署机器学习模型,而 SageMaker 是一系列令人困惑的工业产品,从数据标签到预建的算法市场。

差异解释如下:

认知开销和设计哲学

轻松的 ML 意味着编写机器学习代码,而不是排除基础设施工具的故障。

使用 Amazon SageMaker,您需要具备丰富的 DevOps 知识。您需要使用 IAM 管理用户权限。您将需要能够执行应用程序规模的工具来设置 ML 管道。

使用 Paperspace Gradient,有一个直观的 GUI 和一个 GradientCI 命令行工具。大多数工具都是现成的。计算分配、包依赖性和模型版本化也是现成可用的。

如果你已经拥有丰富的 AWS 和/或 SageMaker 专业知识,你会发现 Gradient 让许多重复性的编排任务变得更加容易。

从哲学上讲,Gradient 让您可以访问整个基础架构堆栈。关于整个部署管道的信息浮出水面,例如,从作业运行器到应用层,再到底层计算硬件。

激励调整

Paperspace Gradient 是一种多云解决方案。Gradient 背后的动机是为每个机器学习团队提供最好的工具——不管他们住在哪里。

许多 AWS 应用程序(包括 SageMaker 中的应用程序)旨在鼓励最终用户采用其他 AWS 应用程序或托管服务。例如,SageMaker 已经有了一个托管服务,用于预建算法,部署到边缘,以及为模型预测添加人工审查。

如果你完全围绕 AWS 进行组织,你可能知道 AWS 托管服务可能是一个黑洞——使用单一云 ML 平台可能会将你推向云的昂贵托管服务。

梯度适合您的生产力。它不会通过架构锁定来限制您,不会推动您采用托管服务,也不会通过固执己见的特定于平台的设计来强行限制您的 ML 能力。

CI/CD 方法和反馈回路

CI/CD for Machine Learning

许多高功能软件团队在他们的工作流程中使用一些 CI/CD 版本。优势是多方面的:

  • 更短的开发周期
  • 更大的团队可见性和协作
  • 较小的代码更改改进了故障识别
  • 更快的代码改进速度

机器学习过程受益于同样的原因。使用像 Paperspace Gradient 这样的 CI/CD 工具可以提高模型开发和部署周期的效率。

Amazon SageMaker 也有用于调整的工具(例如,一键超参数优化),但你会发现这些功能在渐变中与标准功能相同。

基础设施管理

众所周知,AWS 应用程序很难管理,但在规模上非常强大。许多 AWS 服务非常复杂,以至于它们有独立的认证程序!

为了大规模部署机器学习模型,Amazon SageMaker 需要了解权限(例如 IAM)、计算资源的分配(例如 EC2),以及对通过 AWS 属性的数据流的深入了解。

基于 Gradient 的工具基础设施要简单得多。Gradient 具有集成的作业运行器,能够以最经济高效的方式自动调配计算资源。

部署速度

借助 Paperspace Gradient,您可以在 5 分钟或更短时间内从 Jupyter 笔记本进入生产阶段。

Gradient Model view

以基础设施性能著称的亚马逊 SageMaker 需要付出更多努力才能获得洞察力——无论是第一次还是每次。Paperspace 更加直观,尤其是对于不习惯编排计算负载的数据科学家来说。

Gradient 和 SageMaker 都提供“一键式部署”,但在 Gradient 中,这包括计算资源的供应(使用无服务器作业运行器),而在 SageMaker 中,供应仍然是一个工具挑战。

换句话说,尽管 SageMaker 可以被微调以达到最大性能,但是如果部署一个模型要花费很长时间,那么基础设施性能是没有意义的。

成本(按使用付费)

Paperspace Gradient 和 Amazon SageMaker 都有某种形式的按实例计费。

SageMaker 将其服务称为 托管现场培训,使用 EC2 现场实例。这些实例来自共享实例池,这些实例在 AWS 云上的给定时间和给定的实时供应和需求下可用。然后,AWS 从这一桶可用资源中满足 ML 作业的计算需求,并执行您的计算操作。

spot 实例的问题是,你的工作可能会被另一个客户抢占,这大大延长了给定实验的培训时间。而且如果真的使用 EC2 机器学习实例,成本是很难控制的。

另一方面,Gradient 使您可以轻松地为您需要的实例付费——无论是本地、AWS 还是 GCP。这种按需计费被称为 无服务器 ML 。这意味着有一个集成的作业运行器可以为给定的作业自动配置计算资源。你是 总是 只对你实际使用的资源计费。这防止了由于用户错误或糟糕的系统设计导致的大量成本超支。

The Gradient Job Runner eliminates the need to provision an entire machine for model training or deployment.

拓扑学

许多机器学习和深度学习应用需要在边缘部署或与物联网框架集成的训练模型。模型通常为推理而优化。执行修剪或量化。模型会缩小以符合部署标准。

因为 Gradient 是一个 Kubernetes 应用程序,所以很容易将训练分布到处理中心,处理中心可以很容易地上下旋转。可以按照适合业务需求的方式设置集群,并且有一个单一平台来管理多云计算工作负载。

相比之下,SageMaker 在默认情况下将您限制在特定的数据中心区域,并使其难以分发。

供应商锁定和可扩展性

亚马逊 SageMaker 只在 AWS 上运行。如果你在 AWS 上运营业务或者对 AWS 生态系统非常熟悉,这就不是问题。然而,一个额外的问题出现了,不是因为供应商锁定,而是因为可扩展性。

Paperspace Gradient 是安装在任何云或本地基础设施之上的软件层。该应用程序本身是一个 Kubernetes 应用程序。梯度可以跨多个云环境进行编排,这意味着模型可以在单一控制台下进行管理。

有了 SageMaker,机器学习工作流就受制于 AWS 生态系统。如果新产品或工具在其他公共云上提供,就没有办法访问它们。

相比之下,Gradient 将允许您将任何云上的任何新功能连接到您的机器学习工作流。最终,锁定的缺乏和可扩展性保护了关键的商业知识产权和竞争优势。

深度学习的灵活性和兼容性

随着你的机器学习越来越复杂,你的算法工作也可能越来越复杂。

同样,经典的机器学习技术可能不足以满足您的应用——尤其是如果您的应用涉及音频或视频,或者如果某个特定的经典算法限制了您的结果的准确性。

当需要深度学习时,Paperspace Gradient 可以更轻松地将模型部署到 GPU。

原因是多方面的:

  • 通过 Gradient job runner,在 GPU 加速的机器上进行分布式培训非常简单
  • GPU 定价在 Gradient 上比在 AWS EC2 上更有吸引力
  • Gradient 与 PyTorch 和 TensorFlow 等最流行的深度学习框架具有开箱即用的兼容性
  • 从单节点部署到大规模分布式培训都很容易
  • 易于搭载、易于开发、易于扩展

Gradient 通常能够更快地从研究到生产开发深度学习模型,从而随着时间的推移加快深度学习模型的训练、部署和改进速度。

至关重要的是,Gradient 还使得将模型扩展到大规模分布式训练变得异常容易。

最后一点:花在工具上的时间

正如 2019 年 ka ggle State of Data Science and Machine Learning 的数据所揭示的那样,数据科学家的大部分时间都花在了数据基础设施和服务运营上。

一流的数据科学团队通常还包括开发人员和/或基础架构工程师。

因为任何数据科学团队的目标都应该是在单位时间内返回更多的洞察力,所以数据科学家仍然在工具上花费如此多的时间是没有意义的。

渐变让我们更容易忘记工具,回到真正的工作中。

From Kaggle's State of Data Science and Machine Learning 2019

结论:机器学习的统一平台

轻松的机器学习意味着编写 ML 代码,而不是排除基础设施工具的故障。CI/CD 平台提供对整个体系的访问——从计算层到管理层——同时抽象出不可扩展的任务。

最终,用于机器学习的 CI/CD 工作流的成功实施将意味着更好的可靠性、稳定性、确定性和更快的创新步伐。

Paperspace 与 Graphcore 合作推出 IPU 云服务

原文:https://blog.paperspace.com/paperspace-graphcore-partnership/

人工智能模型的规模和复杂性正以令人震惊的速度增长。训练最先进模型所需的计算周期需求通常超过数百亿个参数,这种需求确实非常迅速,使现有芯片的供应和能力紧张。

Compute Trends Across Three eras of Machine Learning

作为一个旨在支持每一盎司性能都很重要的尖端加速应用的云平台,我们与基础设施的接近让我们对令人兴奋和快速增长的人工智能芯片领域有了独特的见解。特别是,我们已经看到 Graphcore 成为一个关键角色,通过其智能处理单元IPU 实现其提高人工智能系统效率和性能的愿景。

正如 Graphcore 首席执行官 Nigel Toon 在最近的一次采访中讨论的,需要一个设计良好的人工智能软件栈来真正释放专用人工智能芯片提供的原始性能提升。

你可以构建各种奇异的硬件,但是如果你不能实际构建一个软件,将一个人在非常简单的水平上的描述能力转化为硬件,你就不能真正产生一个解决方案。
–奈杰尔·图恩,Graphcore 首席执行官

开发人员需要一个简单的访问层来抽象出机器学习基础设施管理中涉及的许多复杂性,这一观点得到了 Paperspace 的热烈赞同。事实上,我们的 MLOps 平台 Gradient 就是为此而打造的。

今天,我们激动地宣布 Graphcore IPUs 集成到渐变笔记本中。这种新的梯度机器类型提供了免费访问 IPU-POD16 经典机器提供 4 petaFLOPS 的人工智能计算。

任何 Paperspace 用户现在都可以毫不费力地在 Graphcore IPUs 上运行最先进的模型,包括与 HuggingFace 合作开发的预配置变压器示例项目。你可以在这里的公告中了解更多。

TRY NOW

还会有更多

Paperspace 和 Graphcore 将继续合作,将 IPUs 集成到完整的端到端梯度平台中,提供 ML 管道和推理功能,随着人工智能行业的成熟,这些功能越来越普遍。

在 iOS 中管理您的 Paperspace 核心计算机

原文:https://blog.paperspace.com/paperspace-ios-shortcuts/

‌If:您和我一样,越来越依赖移动设备,但您仍然需要一台运行完整操作系统的工作站来访问您的工作所需的功能强大但过时的工具套件。输入:Paperspace +您选择的 RDP/VNC 客户(我使用跳转桌面)。从 Paperspace 提供的众多核心机器中选择一台,几分钟后,您就可以在任何移动设备上访问一个完全托管的超级桌面。问题变成了:你如何在移动中以最小的摩擦管理这台机器?当然,你可以登录在线仪表盘,但你和我一样,我们都很懒。

什么是 iOS 快捷方式&为什么?

快捷方式 是一款运行复杂任务的免费(也经常被忽视)iOS 设备应用。与paper space API相结合,快捷方式成为一个强大的工具,让您只需轻点几下就能管理您的机器。

快捷方式应该预装在你的 iOS 设备上,但是如果你删除了它,你可以在这里重新下载。

我开始开发 Paperspace iOS 快捷方式主要是为了一个目的:快速启动/停止/重启虚拟机的能力。如果你的机器是按小时计费的,那么当你不使用它的时候,你很可能会关闭机器*。这意味着每次你想实际使用这台机器时,你必须先把它的转到。如果您使用本机 Paperspace 客户端,这将自动发生。然而,目前还没有原生的 iOS 或 Android Paperspace 客户端。相反,您可以简单地通过 RDP/VNC 访问机器(点击此处此处如果您想了解有关设置的更多信息)。*

总而言之:

*问题# 1: RDP/VNC 只有当你的 Paperspace 机器是ON + 问题#2: 如果你选择按小时计费 您可能会让您的机器处于 关闭 状态 + 问题#3: 没有本地 iOS/Android Paperspace 应用程序来轻松管理您的移动机器


解决方案: 一个与 Paperspace 的 API 交互的快捷方式,只需点击几下鼠标即可管理您的机器*

图纸空间 iOS 快捷方式

*paper space iOS 快捷方式将根据您的反馈不断改进。
要报告 bug,请求新功能,或者其他一般询问,请随时联系我

这里下载 1.0 版本:
https://www . I cloud . com/shortcut s/712 c 756367644 E3 f 81636512 a6 FCE 44 a

请注意,与应用程序不同,快捷方式不会通过 app store 自动更新,因此请务必返回此处下载最新版本。

版本 1.0 (04.20.20)

管理多台机器,每台机器:

  • 查看当前机器状态(关闭、启动、就绪等。)
  • 启动/停止/重启机器
  • 查看机器规格
    • 名字
    • 身份证明
    • 地区
    • 公共知识产权(如果适用)
    • 随机存取存储
    • 中央处理器
    • 国家政治保卫局。参见 OGPU
    • 可用存储空间(%和 GB)
  • 查看当月账单
    • 每月总数
    • 计费周期
    • 使用类型
    • 小时率
    • 使用小时数
    • 存储速率
    • 公共知识产权(如果适用)
  • 更新机器名称
  • 将公共 IP 复制到剪贴板(如果适用)

In a few taps, you can quickly manage your Paperspace Core Machine from your iOS device. No login required.

装置

按照以下步骤开始:

  1. 导航到设置->快捷方式,打开*“允许不受信任的快捷方式”*

注意:只有 Apple 快捷方式被认为是“可信的”,所有第三方快捷方式都需要此设置。如果你的 iOS 设备上没有快捷方式应用,在这里下载。

如果这是您第一次使用快捷方式,您将无法更改此设置,除非您已经运行了至少一个快捷方式。打开快捷方式应用程序,尝试一下。然后,您将能够返回到步骤#1。

  1. 通过 Paperspace Web 控制台创建一个新的 Paperspace API 密钥。
    复制你创建的 API 密匙,因为它需要在设置时添加。

  2. 获取您的机器 ID,也称为主机名。这也可以通过 Paperspace Web 控制台找到。复制主机名供以后参考。

  3. 下载 Paperspace iOS 快捷方式的最新版本
    你需要滚动快捷方式的所有动作。
    在底部选择**“添加不可信快捷方式”**。

  4. 按照添加 API 密钥和机器 id 中的说明进行操作。确保您的机器 id 中没有空行。

设置完成!

可选步骤:

  • 将您的快捷方式添加到小部件页面,以便快速访问。

    1. 在主屏幕上,向右滑动以显示您的小工具。
    2. 滚动到底部并选择“编辑”。
    3. 将“快捷方式”添加到您的小部件列表中。
    4. 您可以通过小组件屏幕上的“自定义快捷方式”选择显示哪些快捷方式。
  • 在主屏幕上添加图标。

    1. 将所需的 Paperspace 图标图像下载到 iOS 照片库中。点击此处查看来自 Paperspace 的 Github 的示例
    2. 在 iOS 设备上打开快捷方式应用程序。
    3. 选择三个点“...”对于图纸空间快捷方式。
    4. 再次选择三个点“...”。
    5. 点击“添加到主屏幕”。
    6. 点击小图标,选择“选择照片”。选择您下载的照片。如果愿意,您可以重命名该应用程序。
    7. 点击右上角的“添加”完成此过程。
    8. 现在,您将在主屏幕上看到应用程序图标。

限制

  • 计费:
    • 并非所有的附加组件目前都支持计费计算。
      • 支持公共 IP 地址,但不支持快照、模板或共享驱动器。
  • API 调用:

结论

Paperspace iOS 快捷方式是在旅途中快速管理您的 Paperspace 核心机器的便捷工具。

这里下载 1.0 版本:
https://www . I cloud . com/shortcut s/712 c 756367644 E3 f 81636512 a6 FCE 44 a

Paperspace iOS 快捷方式将根据您的反馈随着时间的推移而不断发展。要报告错误、请求新功能或其他一般问题,请随时联系我

注意:与应用程序不同,快捷方式不会通过 app store 自动更新。请务必回来这里下载最新版本。*

Paperspace 加入 NVIDIA 云服务提供商计划,加速云 GPU 的采用和远程工作

原文:https://blog.paperspace.com/paperspace-joins-nvidia-cloud-service-provider-program-accelerates-cloud-gpu-adoption-remote-work/

纽约市,2020 年 9 月 10 日——paper space 今天宣布,它已经加入了 NVIDIA 合作伙伴网络(NPN) 的云服务提供商计划,将 GPU 加速带到云中,以应对远程工作的人们。这一指定紧随 Paperspace 最近宣布加入英伟达 DGX 就绪软件计划,获得英伟达 DGX 系统的 Gradient 认证。Paperspace 重视与英伟达在不断发展的云服务生态系统中的合作。

在过去的六个月中,对支持远程工作的灵活、安全的解决方案的需求变得至关重要,并且有望持续下去。企业越来越多地转向基于云的解决方案,这种解决方案提供了支持随处远程工作所需的灵活性和简化的管理。

作为 NPN 云服务提供商计划的合格合作伙伴,Paperspace 与企业合作,为现代工作负载部署 NVIDIA GPU 加速解决方案,包括 AI、数据科学、HPC 和虚拟工作站,这些工作站采用最新的 Quadro 技术和计算机图形,用于 CAD/CAM、数字内容创建和渲染。

共同的愿景

NVIDIA GPUs 用于构建下一代应用,如人工智能驱动的癌症检测和自动驾驶汽车,以及技术和创意专业人士使用的图形密集型应用。数据科学家等领域专家从位于原始基础设施之上的软件抽象层中受益匪浅。Paperspace 开发软件来协调和扩展基于 GPU 的应用程序,提供一种无缝方式来利用 GPU 计算能力,而不需要专业的开发人员和基础架构知识。这种硬件和软件的强大组合对于采用基于 GPU 的应用程序至关重要。

Paperspace 的联合创始人丹尼尔·科布兰(Daniel Kobran)表示:“Paperspace 早期押注于云 GPU,这实际上是由 GPU 技术的领导者英伟达实现的。“我们已经与 NVIDIA 密切合作了几年,我们很高兴能够继续作为 CSP 合作伙伴进行合作。”

NVIDIA 云计算和战略合作伙伴全球业务开发总监 Matt McGrigg 表示:“基于云的解决方案提供了企业所需的灵活性和简化的管理,支持在任何地方工作的新常态。“NVIDIA GPUs 和 Paperspace 应用的结合有助于拓展远程工作的可能性。”

Paperspace 加入英伟达 DGX 计划

原文:https://blog.paperspace.com/paperspace-joins-nvidia-dgx-program/

今天,我们很高兴终于向您展示了我们在过去几个月里与 NVIDIA 的同事们一起工作的成果。

有了 Gradient ,我们的开发者第一 ML 平台,指导方法一直是:我们如何让开发 ML 模型像构建现代 web 应用程序一样简单?以及我们可以建立什么工具来使来自不同领域的软件开发人员在机器学习和深度学习方面取得成功?

Layers of abstraction within a machine learning workflow.

如果你关注 Gradient 有一段时间了,你会知道我们也支持在机器学习中使用敏捷方法。我们相信像 CI/CD 这样的最佳实践对于机器学习的大规模采用是必不可少的,并且已经提出了像 MLOps 这样的其他实用概念

迄今为止,我们已经帮助数万名研究人员、数据科学家和人工智能工程师在商用云资源的基础上构建确定性和可再现的工作流。我们的工具为数千名设计师、工程师、研究人员等开启了基于云的 GPU 和尖端分布式处理。今年早些时候,我们启动了第百万台虚拟机!

Gradient 的核心任务之一是为机器学习工程师抽象出复杂的计算基础设施。我们的用户喜欢 Gradient 的可移植性,并要求更多的方法来将 ML 工作负载从单个工作站扩展到云。

云原生方法意味着 Gradient 可以轻松支持 GCP、AWS 和 Azure,我们刚刚推出了一个开源安装程序 ,可以在你能想象的任何其他环境上运行 Gradient——从你的本地计算机到云。

今天,我们很高兴地宣布与 NVIDIA 正式合作,这是由我们在企业中实现人工智能的共同愿景推动的。

通过这种合作关系,我们已经为 NVIDIA 的任何 DGX 系统开发了开箱即用的一流梯度支持。

这是一件大事,原因如下:

  1. 英伟达制造了许多当今最强大的人工智能芯片,DGX 是一个世界级的严肃人工智能计算平台
  2. Gradient 现已完全支持开箱即用,并已通过 NVIDIA 的仔细测试,以确保从安装到生产推断的一流体验
  3. 此版本可让您轻松地将云资源连接到任何内部部署的 DGX 系统,为您的数据带来强大的计算能力,并通过统一的管理体验释放整个组织的协作和可见性

作为 DGX 就绪软件计划的创始成员,我们很高兴能够为英伟达 DGX 深度学习架构提供全面验证的支持。

英伟达 DGX 软件产品管理高级总监 John Barco 说:

“我们开发了 NVIDIA DGX 就绪软件程序,以加速企业中的人工智能开发。Paperspace 开发了一种独特的 CI/CD 方法来构建机器学习模型,简化了过程,并利用了英伟达 DGX 系统的强大功能。”

如果您想了解更多信息,我们很乐意与您聊天。我们的 Gradient Enterprise 支持团队为 DGX 客户提供设置帮助、解决方案架构和增强支持,因此请联系

这是一项鼓舞人心的努力,旨在打造能够让一代建设者受益的技术,我们怀着极大的自豪感承担这项工作。我们很高兴与 NVIDIA 发展这种合作关系,我们迫不及待地想向您展示我们的合作成果。

Paperspace 加入 TensorFlow AI 服务合作伙伴

原文:https://blog.paperspace.com/paperspace-joins-tensorflow-ai-service-partners/

[2021 年 12 月 2 日更新:本文包含关于梯度实验的信息。实验现已被弃用,渐变工作流已经取代了它的功能。请参见工作流程文档了解更多信息。]

我们很高兴地宣布,我们已经与 TensorFlow 团队合作,成为官方人工智能服务合作伙伴。作为世界上最受欢迎的开源机器学习框架之一,下载量超过 1 亿次,TensorFlow 宣布了这一合作伙伴关系,旨在通过基于人工智能的系统帮助更多企业解决最具挑战性的业务问题。

作为 TensorFlow 人工智能服务合作伙伴,我们的 Gradient 团队将与 TensorFlow 团队密切合作,帮助更多团队加速他们的 ML 项目。这包括定期分享见解和产品反馈,以及共同努力进行增强和改进,专门解决企业的 ML 需求。

今天,世界上数百万家公司正在投资机器学习,以解决几乎每个行业的问题。TensorFlow 是 Google Brain 团队创建的一个开源项目,是一个非常受欢迎的 ML 框架,支持从训练到模型服务的端到端工作流。TensorFlow 对分布式培训和其他一些高级功能提供了本机支持,这使得该框架在交付生产应用程序时尤其有价值。

TensorFlow 与 Gradient、Paperspace 的 MLOps 平台紧密集成,极大地简化了开发 ML 模型的研究和生产阶段。Gradient 支持任何版本的 TensorFlow 用于笔记本实验作业(部署训练好的模型参见 TensorFlow Serving )。尽管客户也可以自带自己定制的 TensorFlow 容器,但开箱后会提供一组预建的 TensorFlow 容器。请参见我们的集成页面,了解 Gradient 中 TensorFlow 支持的更多示例,包括自动模型解析和内置分布式训练功能。

Selecting a TensorFlow container in Gradient

Gradient 客户将 TensorFlow 用于广泛的应用,如计算机视觉、自然语言处理(NLP)、推荐系统、异常检测等。例如, Spectrum Labs 正在使用 Gradient 向提供有毒聊天检测模型给互联网约会、游戏、市场和社交媒体社区。

我们的 ML ShowcaseblogGitHub repo 有许多基于 TensorFlow 的入门级和生产就绪样本项目,涵盖了机器学习领域的许多热门领域。

这种合作将开启新的可能性,我们非常高兴正式确定这种关系。

首先,你可以创建一个免费账户或者给我们发一封短信

介绍渐变社区笔记本:在免费 GPU 上轻松运行 ML 笔记本

原文:https://blog.paperspace.com/paperspace-launches-gradient-community-notebooks/

无论你是机器学习爱好者、研究人员还是专业人士,设置和管理你的工作环境都可能是一个复杂和令人分心的过程。即使您已经解决了安装和版本兼容性问题,您仍将面临克隆最新模型、与他人共享您的成果以及简单地跟踪您的工作的挑战。更糟糕的是,许多云 GPU 的价格高得惊人。

我们正在通过发布 渐变社区笔记本 正面解决这些问题,这是一项基于 Jupyter 笔记本的免费云 GPU 服务,从根本上简化了 ML/AI 开发的过程。现在,任何使用 PyTorch、TensorFlow 和 Keras 等流行深度学习框架的开发人员都可以轻松启动强大的免费 GPU 实例,并在他们的 ML 项目上进行合作。这标志着我们的使命的最新努力,使云 GPU 资源更容易为社区所用。

推出免费笔记本

Gradient Community 笔记本是公共的、可共享的 Jupyter 笔记本,运行在免费的云 GPU 和 CPU 上。笔记本可以在任何 DL 或 ML 框架上运行,预先配置为开箱即用。使用你自己的容器或者从大量模板中选择,包括流行的驱动程序和依赖项,比如 CUDA 和 cuDNN。

Gradient 让您专注于构建模型,而不是排除环境故障。

免费笔记本可用的实例类型包括:

  • 空闲 CPU-C4 CPU 实例
  • 免费-GPU+ — NVIDIA M4000 GPU
  • 免费-P5000 — NVIDIA P5000 GPU

5 GB 的永久存储也是免费的。

机器学习展示:现成的 ML 项目

Gradient - ML ShowcaseA collection of interactive Machine Learning projects curated by Paperspace Gradient.ML Showcase

ML Showcase 是一个现成的、完整的 ML 项目库,可以开箱即用。现在有了免费的 CPU 和 GPU,你可以轻松地选择一个项目,并将其作为自己工作或探索的基础。

您可以根据项目的类型和类别来浏览项目。一旦你找到一个你感兴趣的项目,在 Gradient 中打开它浏览代码。

通过克隆并在免费的 GPU 上运行该项目,让它成为你自己的项目。调整参数,交换数据集,或者将其作为自己工作的基础。

如何在跑步中迅速起步

开始使用您的第一台免费 GPU 笔记本电脑非常简单。

  1. 创建免费账户
  2. 选择您的实例(M4000 或 P5000 云 GPU,或 C4 云 CPU)
  3. 启动您自己的笔记本,或者从 ML showcase 或社区获得一个现成的项目

您可以运行无限数量的会话,一次最多可运行 6 个小时。您的笔记本将保持完整版本,并且您可以重新启动您的实例来运行另外 6 个小时,次数不限。

渐变社区笔记本关注的正是这个社区。所有笔记本都设置为公共的,可以由其他社区成员共享和分享。

要了解更多信息请查看文档或免费运行自己的笔记本。

图纸空间学习

原文:https://blog.paperspace.com/paperspace-learn/

大家好!我们很高兴地宣布 Paperspace Learn ,这是一个完全免费的资源,面向任何想要进入云计算领域或想要了解深度学习和 MLOps 等专业主题的人。它包括几十个分类的新视频(外加一个关于高级机器学习的新课程,我们将继续开发这些视频。我们的使命是让每个人都能在云中构建和使用密集型应用程序,无论他们是否有 DevOps 背景。

我们创建了几种方法来过滤图书馆的主题,如按产品,按标签,或按类别,如初学者,高级等。还有一个探索页面用于查看特色内容和最新内容列表。

要了解最新内容,请随时订阅电子邮件更新。你会在主页的工具条上看到一个添加你的电子邮件地址的选项。

我们希望你喜欢!

图纸空间安全性介绍

原文:https://blog.paperspace.com/paperspace-security/

概观

安全性和隐私是您业务的核心

图纸空间的设计以安全性为首要考虑因素。我们知道安全性是所有业务的基石,我们致力于提供世界上最值得信赖的虚拟桌面环境。在当今的环境中,了解您公司的数据是安全的,权限是受管理的,并且与可能的攻击者完全隔离,这是迁移到云的基本要求。Paperspace 在各方面都超过了,可以成为您安全 IT 基础架构的主要支柱。

数据安全

零知识平台的技术基础

Paperspace 的理念是只有您才能访问您的数据,我们孜孜不倦地设计符合这一目标的解决方案。这发生在应用层、网络层和物理数据中心(对于我们的托管产品)。

进出您的 Paperspace 虚拟机的所有通信都通过完全加密的通道进行保护。

  • 客户端和远程服务器之间的加密流(SSL/TLS)独立于平台— web、桌面或移动。
  • 我们的数据库、web 服务器、API 和内部网络之间的流量也是加密的(SSL/TLS)
  • 使用 256 位 AES 或更高版本保护数据库
  • 我们在证书中使用 2048 位公钥,并且只支持高强度对称密码。

网络安全性

确保数据的安全传输

当您将 Paperspace 虚拟机放在网络上时,Paperspace 很可能是该网络上最安全的机器。

  • 100%网络隔离
  • 可配置的防火墙(或运行您自己的防火墙)
  • 从 VPN 到 VPN 的加密通道(Paperspace 和您的办公室之间的 IPSec/OpenVPN 加密通道)
  • 802.1q VLANs,这个专用连接可以划分成多个虚拟接口

数据中心标准和合规性

保护和监控企业服务器

我们的数据中心采用了各种安全机制,包括严格的访问策略以及安全的保险库和保险箱。

  • Paperspace 数据中心符合 ISO 和 SSAE16 标准(经独立审计机构和第三方机构认证)。
  • 我们的数据中心采用 24x7 现场安全措施,包括人员、移动侦测、门禁系统和闭路视频监控。
  • 对包含公司服务器的区域的访问仅限于通过徽章访问系统授予的提升角色的授权人员。
  • 不间断电源和备用系统,以及火灾/洪水检测和预防。

用户和身份管理

知道谁,何时,何地

Paperspace 提供工具来集中管理身份和协作者,具有强身份验证和细粒度权限。

  • 强大的基于角色的许可系统有助于保持对传统本地系统的更严格控制(包括机器和驱动器)。
  • 用于访问控制的活动目录集成(可选)
  • 高级帐户管理(远程注销所有会话、警报等)
  • 登录监控和访问日志,为您帐户中的所有内容、用户、设备和活动提供智能和可见性。

信用卡安全

Paperspace 不存储信用卡信息

信用卡处理由 Stripe 处理。Stripe 符合 PCI 标准,所有与其 API 交互的流量都在安全通道(HTTPS)上运行。存储在他们服务器上的信用卡信息使用 AES-256 加密。

流动性

防止数据被盗和丢失

Paperspace 独特的“零本地存储”模式是最安全的虚拟桌面交付系统。由于端点仅渲染从数据中心流出的像素,因此无法提取虚拟环境中的信息(从机器或共享驱动器)。我们的零本地存储政策在所有平台(web、桌面、移动)上实施,不受设备限制。

这里有一个可下载的版本(PDF):
Paperspace 安全白皮书

介绍 Gradient 的下一次迭代:在本地、在您的云中或混合环境中运行

原文:https://blog.paperspace.com/paperspace-unveils-next-iteration-of-gradient/

更新:这次发布会在 TechCrunch 上被特别报道

在过去的几年里,我们处理了无数在现有基础设施上运行我们的 MLOps SaaS 平台 Gradient 的请求,从本地裸机服务器到 AWS 虚拟机,再到 Kubernetes 集群。今天,我们很高兴地与大家分享,最新版本的 Gradient 现在可以跨多种云、本地和混合环境运行,并且完全不受基础架构限制。新版本还引入了 GradientCI,这是业界第一个用于构建、训练和部署深度学习模型的全面 CI/CD 引擎。

如今,公司部署大量资源来管理需要持续支持和维护的内部 ML 管道。Gradient 通过提供生产就绪平台即服务来加速他们的人工智能应用程序的开发,从而消除了这一令人头痛的问题。平台能力包括 ML 团队运行 Jupyter 笔记本、分布式培训、超参数调优、将模型部署为 RESTful APIs 的能力。Gradient 使开发人员能够构建复杂的端到端管道,跨越异构基础设施,所有这些都从一个中心开始。

核心优势包括:

  • 用于开发、培训和部署模型的端到端平台
  • 他们的 git 存储库和 Gradient 之间的持续集成
  • 借助强大的流水线和确定性流程实现工作流自动化
  • 跨团队协作:添加团队成员,控制权限,并提高整个组织的可见性
  • 利用全面管理和优化的英特尔 nerv ana NNP-T 加速器或将现有基础设施转变为强大的 MLOps 平台

Paperspace 很自豪能够与英特尔合作,为他们的新人工智能芯片提供软件抽象层。

“Paperspace 有助于激励下一代人工智能开发者;它们还将帮助释放即将推出的英特尔 Nervana 神经网络处理器的强大功能。这些新芯片将提供突破性的性能,Paperspace 将帮助公司快速高效地运营这一令人惊叹的新人工智能硬件。”
—英特尔人工智能软件和人工智能产品事业部总经理卡洛斯·莫拉莱斯。

有关此版本渐变的更多信息,请联系我们的销售团队。

Paperspace 与 Citrix:它们有何不同?

原文:https://blog.paperspace.com/paperspace-vs-citrix-how-are-they-different/

我们经常遇到的一个问题是,我们与 Citrix 有何不同?有两个主要区别:

(在本文中,我们将使用 Citrix 作为示例,但这同样适用于所有传统的 VDI 提供商)。

接近

VDI 提供商销售的是软件,而不是机器。由专业 IT 团队对服务器进行机架安装、虚拟化和管理是当今利用虚拟桌面的先决条件。这是一个极其昂贵和复杂的系统,难以配置和管理,因此对于大多数没有大型 It 团队/预算的公司来说是力所不及的。随着无处不在的带宽和公共云的出现,VDI 最终有可能摆脱企业被迫管理自己的服务器的内部模式。

Paperspace 正在将 VDI 迁移到它所属的云中。所有固有的复杂性都包装在一个简单的层中,使任何人都可以从虚拟桌面中受益。

我们喜欢用 Dropbox、box 和 Drive 之前的各种文件同步技术进行类比。当时,这项技术非常复杂,只有拥有复杂 it 部门的公司才能使用。将它转移到公共云,并将一切都放在直观的界面之后,这使得该技术面向更广泛的受众。

简而言之,VDI 仅适用于拥有大量 IT 预算的财富 500 强公司。有了 Paperspace,每个人都可以使用同样的技术。

技术

  • **GPU:**我们正在利用云游戏技术来提供更高性能的桌面(GPU 加速的操作系统体验)和更快的流(GPU 的编码速度比 CPU 快得多)。我们能够在更远的距离进行流式传输,同时支持图形密集型应用,如 3D CAD、Photoshop 等。第一次在虚拟桌面上。为什么这很重要?鉴于目前非 GPU 加速 VDI 的局限性,该技术不适用于工程、建筑、设计和动画等整个行业。

  • **流:**仅仅访问 GPU 是不够的。协议本身必须能够实时适应变化的网络条件,同时针对任何端点进行优化。我们正在研究协议层的一些真正有趣的东西,以解决低端设备、数据包丢失、高延迟连接等问题。这包括像我们的抖动缓冲和动态比特率调整。举个例子,我们从视频游戏行业了解到,延迟抖动对人眼的影响比延迟本身更大。考虑到这个,我们可以达到更高的延迟数字(甚至高达 100 毫秒),而用户不会感觉到延迟。

  • Web: Paperspace 是一种“Web 优先”的技术。大多数虚拟桌面产品要求每个平台都有笨重的客户端应用程序。我们在网络浏览器中直接获得接近原生的性能,这为最终用户带来了更加顺畅/流畅的体验,并使我们能够从大多数企业中出现的 BYOD 计划中获得独特的好处。

用 Python 实现梯度下降,第 1 部分:向前和向后传递

原文:https://blog.paperspace.com/part-1-generic-python-implementation-of-gradient-descent-for-nn-optimization/

通过一系列教程,梯度下降(GD)算法将在 Python 中从头实现,用于优化人工神经网络(ANN)在反向传播阶段的参数。GD 实现将是通用的,可以与任何人工神经网络架构一起工作。教程将遵循一个简单的路径来完全理解如何实现 GD。每个教程将涵盖所需的理论,然后在 Python 中应用。

在本教程中,这是系列的第 1 部分,我们将通过为一个特定的 ANN 架构实现 GD 来启动 worm,其中有一个具有 1 个输入的输入层和一个具有 1 个输出的输出层。本教程将不使用任何隐藏层。为了简单起见,在开始时不使用偏差。

1 个输入–1 个输出

通用实现 GD 算法的第一步是实现一个非常简单的架构,如下图所示。只有 1 个输入和 1 个输出,根本没有隐藏层。在考虑在向后传递中使用 GD 算法之前,让我们从向前传递开始,看看如何从输入开始直到计算误差。

前进传球

根据下图,输入 X1 乘以其权重 W 返回结果 X1*W 。在前向传递中,通常已知每个输入乘以其相关联的权重,然后对所有输入与其权重之间的乘积求和。这叫做积和(SOP)。例如,有 2 个输入 X1X2 ,它们的权重分别为 W1W2 ,那么 SOP 将为 X1W1+X2W2 。在本例中,只有 1 个输入,因此 SOP 没有意义。

计算完 SOP 后,下一步是将其馈送到输出层神经元中的激活函数。这种函数有助于捕捉输入和输出之间的非线性关系,从而提高网络的精度。在本教程中,将使用 sigmoid 函数。下图给出了它的公式。

假设本例中的输出范围为 0 到 1,则从 sigmoid 返回的结果可以被视为预测输出。这个示例是一个回归示例,但是通过将 sigmoid 返回的分数映射到类标签,可以很容易地将其转换为分类示例。

计算预测输出后,下一步是使用下面定义的平方误差函数测量预测误差。

此时,向前传球完成。基于计算的误差,我们可以返回并计算用于更新当前权重的权重梯度。

偶数道次

在后向过程中,我们通过改变网络权重来了解误差是如何变化的。因此,我们想建立一个方程,其中误差和重量都存在。怎么做呢?

根据上图,误差是用两项计算的,它们是:

  1. 预测
  2. 目标

不要忘记预测值是作为 sigmoid 函数的输出计算的。因此,我们可以将 sigmoid 函数代入误差方程,结果如下所示。但是在这一点上,误差和重量不包括在这个等式中。

这是正确的,但还要记住,sop 是作为输入 X1 与其重量 W 之间的乘积计算的。因此,我们可以删除 sop,并使用其等效物 X1*W ,如下所示。

此时,我们可以开始计算误差相对于重量的梯度,如下图所示。使用以下公式计算梯度可能会很复杂,尤其是当存在更多输入和权重时。作为一种选择,我们可以使用简化计算的链式法则。

链式法则

当梯度的两个参与者,即本例中的误差W 不直接与单个方程相关时,我们可以遵循从误差开始直到到达 W 的导数链。回顾误差函数,我们可以发现预测是误差和权重之间的纽带。因此,我们可以计算一阶导数,即误差对预测输出的导数,如下所示。

之后,我们可以通过计算 sigmoid 函数的导数,根据下图计算预测到 sop 的导数。

最后,我们可以计算 sop 和重量之间的导数,如下图所示。

经过一系列导数后,我们可以通过将所有导数相乘,将误差与重量联系起来,如下所示。

Python 实现

在理论上理解了这个过程的工作原理之后,我们就可以很容易地应用它了。下面列出的代码经历了前面讨论的步骤。输入 X1 值为 0.1,目标值为 0.3。使用**numpy . rand()**随机初始化权重,返回 0 到 1 之间的数字。之后,输入和权重被传播到向前传递。这是通过计算输入和重量之间的乘积,然后调用 sigmoid() 函数实现的。请记住,sigmoid()函数的输出被视为预测输出。计算出预测输出后,最后一步是使用 error() 函数计算误差。这样,向前传球就完成了。

import numpy

def sigmoid(sop):
    return 1.0 / (1 + numpy.exp(-1 * sop))

def error(predicted, target):
    return numpy.power(predicted - target, 2)

def error_predicted_deriv(predicted, target):
    return 2 * (predicted - target)

def activation_sop_deriv(sop):
    return sigmoid(sop) * (1.0 - sigmoid(sop))

def sop_w_deriv(x):
    return x

def update_w(w, grad, learning_rate):
    return w - learning_rate * grad

x = 0.1
target = 0.3
learning_rate = 0.001
w = numpy.random.rand()
print("Initial W : ", w)

# Forward Pass
y = w * x
predicted = sigmoid(y)
err = error(predicted, target)

# Backward Pass
g1 = error_predicted_deriv(predicted, target)

g2 = activation_sop_deriv(predicted)

g3 = sop_w_deriv(x)

grad = g3 * g2 * g1
print(predicted)

w = update_w(w, grad, learning_rate) 

在反向传递中,使用 error_predicted_deriv() 函数计算误差对预测输出的导数,结果存储在变量 g1 中。之后,使用 activation_sop_deriv() 函数计算预测(激活)输出到 sop 的导数。结果存储在变量 g2 中。最后,使用 sop_w_deriv() 函数计算 sop 对重量的导数,并将结果存储在变量 g3 中。

计算完链中的所有导数后,下一步是通过将所有导数 g1、g2 和 g3 相乘来计算误差对重量的导数。这将返回权重值可以更新的梯度。使用 update_w() 函数更新权重。它接受 3 个参数:

  1. w
  2. 毕业生
  3. 学习率

这将返回替换旧权重的更新后的权重。注意,前面的代码没有使用更新的权重重复重新训练网络。我们可以进行几次迭代,其中梯度下降算法可以根据下面修改的代码达到更好的权重值。请注意,您可以更改学习速率和迭代次数,直到网络做出正确的预测。

import numpy

def sigmoid(sop):
    return 1.0 / (1 + numpy.exp(-1 * sop))

def error(predicted, target):
    return numpy.power(predicted - target, 2)

def error_predicted_deriv(predicted, target):
    return 2 * (predicted - target)

def activation_sop_deriv(sop):
    return sigmoid(sop) * (1.0 - sigmoid(sop))

def sop_w_deriv(x):
    return x

def update_w(w, grad, learning_rate):
    return w - learning_rate * grad

x = 0.1
target = 0.3
learning_rate = 0.01
w = numpy.random.rand()
print("Initial W : ", w)

for k in range(10000):
    # Forward Pass
    y = w * x
    predicted = sigmoid(y)
    err = error(predicted, target)

    # Backward Pass
    g1 = error_predicted_deriv(predicted, target)

    g2 = activation_sop_deriv(predicted)

    g3 = sop_w_deriv(x)

    grad = g3 * g2 * g1
    print(predicted)

    w = update_w(w, grad, learning_rate) 

下图显示了网络预测如何通过迭代增强。网络可以在 50000 次迭代后达到期望的输出。请注意,通过改变学习速率,您可以通过更少的迭代次数达到期望的输出。

当学习率为 0.5 时,网络仅经过 10000 次迭代就达到了期望的输出。

下图显示了当学习率为 0.5 时,网络误差如何随着迭代而变化。

在构建了对于具有 1 个输入和 1 个输出的基本架构能够有效工作的 GD 算法之后,我们可以在下一节中将输入的数量从 1 增加到 2。请注意,理解前面的实现是如何工作的非常重要,因为接下来的部分将高度依赖于它。

结论

到目前为止,我们已经成功实现了 GD 算法,可以处理 1 个输入或 2 个输入。在下一个教程中,前面的实现将被扩展,以允许算法处理更多的输入。使用将在下一个教程中讨论的例子,将推导出允许 GD 算法处理任意数量的输入的一般规则。

用 Python 实现梯度下降,第 2 部分:针对任意数量的输入进行扩展

原文:https://blog.paperspace.com/part-2-generic-python-implementation-of-gradient-descent-for-nn-optimization/

在 Python 中实现通用梯度下降(GD)算法以优化反向传播阶段的人工神经网络(ANN)参数的系列教程中,再次向您问好。GD 实现将是通用的,可以与任何人工神经网络架构一起工作。这是本系列的第二篇教程,讨论扩展第 1 部分 的 实现,允许 GD 算法在输入层处理任意数量的输入。

本教程是本系列的第 2 部分,有两个部分。每一节都讨论了为具有不同数量输入的体系结构构建 GD 算法。第一种架构,输入神经元数量为 2。第二个将包括 10 个神经元。通过这些例子,我们可以推导出实现 GD 算法的一些通用规则,该算法可以处理任意数量的输入。

2 个输入–1 个输出

本节扩展了 第 1 部分 中 GD 算法的实现,使其能够处理具有 2 个输入的输入层,而不是只有 1 个输入。下图给出了具有 2 个输入和 1 个输出的人工神经网络图。现在,每个输入都有不同的权重。对于第一个输入 X1 ,有一个权重 W1 。对于第二个输入 X2 ,其权重为 W2 。如何让 GD 算法与这两个参数一起工作?将误差链写成 W1 和 W2 的导数后,答案就简单多了。

下图给出了 W1 和 W2 误差的导数链。有什么区别?区别在于如何计算 SOP 和重量之间的最后一个导数。W1 和 W2 的前两个导数是相同的。

下面列出的代码给出了计算上述导数的实现。与第 1 部分的实施相比,有 3 个主要区别。

第一个是使用**numpy . rand()**初始化 2 个权重有 2 行代码。

第二个变化是 SOP 计算为每个输入与其相关权重的乘积之和( X1W1+X2W2 )。

第三个变化是计算 SOP 对两个砝码的导数。在第 1 部分中,只有一个重量,因此计算的是一个导数。在这个例子中,你可以认为它只是将代码行翻倍。变量 g3w1 计算 w1 的导数,变量 g3w2 计算 w2 的导数。最后,在两个变量 gradw1gradw2 中计算每个权重更新的梯度。最后,两次调用 update_w() 函数来更新每个权重。

import numpy

def sigmoid(sop):
    return 1.0/(1+numpy.exp(-1*sop))

def error(predicted, target):
    return numpy.power(predicted-target, 2)

def error_predicted_deriv(predicted, target):
    return 2*(predicted-target)

def activation_sop_deriv(sop):
    return sigmoid(sop)*(1.0-sigmoid(sop))

def sop_w_deriv(x):
    return x

def update_w(w, grad, learning_rate):
    return w - learning_rate*grad

x1=0.1
x2=0.4
target = 0.3
learning_rate = 0.1
w1=numpy.random.rand()
w2=numpy.random.rand()
print("Initial W : ", w1, w2)

# Forward Pass
y = w1*x1 + w2*x2
predicted = sigmoid(y)
err = error(predicted, target)

# Backward Pass
g1 = error_predicted_deriv(predicted, target)

g2 = activation_sop_deriv(predicted)

g3w1 = sop_w_deriv(x1)
g3w2 = sop_w_deriv(x2)

gradw1 = g3w1*g2*g1
gradw2 = g3w2*g2*g1

w1 = update_w(w1, gradw1, learning_rate)
w2 = update_w(w2, gradw2, learning_rate)

print(predicted)

前面的代码只适用于 1 次迭代。我们可以使用一个循环来进行多次迭代,其中权重可以更新为一个更好的值。这是新的代码。

import numpy

def sigmoid(sop):
    return 1.0/(1+numpy.exp(-1*sop))

def error(predicted, target):
    return numpy.power(predicted-target, 2)

def error_predicted_deriv(predicted, target):
    return 2*(predicted-target)

def activation_sop_deriv(sop):
    return sigmoid(sop)*(1.0-sigmoid(sop))

def sop_w_deriv(x):
    return x

def update_w(w, grad, learning_rate):
    return w - learning_rate*grad

x1=0.1
x2=0.4
target = 0.3
learning_rate = 0.1
w1=numpy.random.rand()
w2=numpy.random.rand()
print("Initial W : ", w1, w2)

for k in range(80000):
    # Forward Pass
    y = w1*x1 + w2*x2
    predicted = sigmoid(y)
    err = error(predicted, target)

    # Backward Pass
    g1 = error_predicted_deriv(predicted, target)

    g2 = activation_sop_deriv(predicted)

    g3w1 = sop_w_deriv(x1)
    g3w2 = sop_w_deriv(x2)

    gradw1 = g3w1*g2*g1
    gradw2 = g3w2*g2*g1

    w1 = update_w(w1, gradw1, learning_rate)
    w2 = update_w(w2, gradw2, learning_rate)

    print(predicted)

下图显示了人工神经网络的预测值如何变化,直到达到所需的输出值 0.3。在大约 5000 次迭代之后,网络能够做出正确的预测。

下图显示了迭代次数对误差的影响。经过 5000 次迭代后,误差为 0.0。

到目前为止,我们已经成功实现了 GD 算法,可以处理 1 个输入或 2 个输入。在下一节中,前面的实现将被扩展,以允许该算法处理 10 个输入。

10 个输入–1 个输出

具有 10 个输入和 1 个输出的网络架构如下所示。有 10 个输入 X1 至 X10 和 10 个权重 W1 至 W10,每个输入都有其权重。为训练这样一个网络建立 GD 算法类似于前面的例子,但是只使用 10 个输入而不是 2 个。只需重复计算 SOP 和每个重量之间的导数的代码行。

在编写计算导数的代码之前,最好列出要计算的必要导数,这些导数在下图中进行了总结。很明显,所有重量的前两个导数是固定的,但最后一个导数(重量导数的 SOP)是每个重量的变化。

在前面的示例中,只有两个权重,因此有以下内容:

  1. 2 行代码,用于指定 2 个输入中每个输入的值。
  2. 用于初始化 2 个权重的 2 行代码。
  3. 通过对 2 个输入和 2 个权重之间的乘积求和来计算 SOP。
  4. 用于计算 SOP 和 2 个权重导数的 2 行代码。
  5. 用于计算 2 个权重的梯度的 2 行。
  6. 2 行用于更新 2 个权重。

在本例中,10 行将替换 2 行,因此将存在以下内容:

  1. 10 行代码,用于指定 10 个输入中每个输入的值。
  2. 初始化 10 个权重的 10 行代码。
  3. 通过对 10 个输入和 10 个重量的乘积求和来计算 SOP。
  4. 计算 10 个重量导数的 SOP 的 10 行代码。
  5. 10 行用于计算 10 个重量的梯度。
  6. 用于更新 10 个权重的 10 行代码。

下面给出了用于实现具有 10 个输入的网络的 GD 算法的代码。

import numpy

def sigmoid(sop):
    return 1.0/(1+numpy.exp(-1*sop))

def error(predicted, target):
    return numpy.power(predicted-target, 2)

def error_predicted_deriv(predicted, target):
    return 2*(predicted-target)

def sigmoid_sop_deriv(sop):
    return sigmoid(sop)*(1.0-sigmoid(sop))

def sop_w_deriv(x):
    return x

def update_w(w, grad, learning_rate):
    return w - learning_rate*grad

x1=0.1
x2=0.4
x3=4.1
x4=4.3
x5=1.8
x6=2.0
x7=0.01
x8=0.9
x9=3.8
x10=1.6

target = 0.3
learning_rate = 0.01

w1=numpy.random.rand()
w2=numpy.random.rand()
w3=numpy.random.rand()
w4=numpy.random.rand()
w5=numpy.random.rand()
w6=numpy.random.rand()
w7=numpy.random.rand()
w8=numpy.random.rand()
w9=numpy.random.rand()
w10=numpy.random.rand()

print("Initial W : ", w1, w2, w3, w4, w5, w6, w7, w8, w9, w10)

# Forward Pass
y = w1*x1 + w2*x2 + w3*x3 + w4*x4 + w5*x5 + w6*x6 + w7*x7 + w8*x8 + w9*x9 + w10*x10
predicted = sigmoid(y)
err = error(predicted, target)

# Backward Pass
g1 = error_predicted_deriv(predicted, target)

g2 = sigmoid_sop_deriv(y)

g3w1 = sop_w_deriv(x1)
g3w2 = sop_w_deriv(x2)
g3w3 = sop_w_deriv(x3)
g3w4 = sop_w_deriv(x4)
g3w5 = sop_w_deriv(x5)
g3w6 = sop_w_deriv(x6)
g3w7 = sop_w_deriv(x7)
g3w8 = sop_w_deriv(x8)
g3w9 = sop_w_deriv(x9)
g3w10 = sop_w_deriv(x10)

gradw1 = g3w1*g2*g1
gradw2 = g3w2*g2*g1
gradw3 = g3w3*g2*g1
gradw4 = g3w4*g2*g1
gradw5 = g3w5*g2*g1
gradw6 = g3w6*g2*g1
gradw7 = g3w7*g2*g1
gradw8 = g3w8*g2*g1
gradw9 = g3w9*g2*g1
gradw10 = g3w10*g2*g1

w1 = update_w(w1, gradw1, learning_rate)
w2 = update_w(w2, gradw2, learning_rate)
w3 = update_w(w3, gradw3, learning_rate)
w4 = update_w(w4, gradw4, learning_rate)
w5 = update_w(w5, gradw5, learning_rate)
w6 = update_w(w6, gradw6, learning_rate)
w7 = update_w(w7, gradw7, learning_rate)
w8 = update_w(w8, gradw8, learning_rate)
w9 = update_w(w9, gradw9, learning_rate)
w10 = update_w(w10, gradw10, learning_rate)

print(predicted)

通常,前面的代码只进行一次迭代。我们可以使用一个循环在多次迭代中训练网络。

import numpy

def sigmoid(sop):
    return 1.0/(1+numpy.exp(-1*sop))

def error(predicted, target):
    return numpy.power(predicted-target, 2)

def error_predicted_deriv(predicted, target):
    return 2*(predicted-target)

def sigmoid_sop_deriv(sop):
    return sigmoid(sop)*(1.0-sigmoid(sop))

def sop_w_deriv(x):
    return x

def update_w(w, grad, learning_rate):
    return w - learning_rate*grad

x1=0.1
x2=0.4
x3=4.1
x4=4.3
x5=1.8
x6=2.0
x7=0.01
x8=0.9
x9=3.8
x10=1.6

target = 0.3
learning_rate = 0.01

w1=numpy.random.rand()
w2=numpy.random.rand()
w3=numpy.random.rand()
w4=numpy.random.rand()
w5=numpy.random.rand()
w6=numpy.random.rand()
w7=numpy.random.rand()
w8=numpy.random.rand()
w9=numpy.random.rand()
w10=numpy.random.rand()

print("Initial W : ", w1, w2, w3, w4, w5, w6, w7, w8, w9, w10)

for k in range(1000000000):
    # Forward Pass
    y = w1*x1 + w2*x2 + w3*x3 + w4*x4 + w5*x5 + w6*x6 + w7*x7 + w8*x8 + w9*x9 + w10*x10
    predicted = sigmoid(y)
    err = error(predicted, target)

    # Backward Pass
    g1 = error_predicted_deriv(predicted, target)

    g2 = sigmoid_sop_deriv(y)

    g3w1 = sop_w_deriv(x1)
    g3w2 = sop_w_deriv(x2)
    g3w3 = sop_w_deriv(x3)
    g3w4 = sop_w_deriv(x4)
    g3w5 = sop_w_deriv(x5)
    g3w6 = sop_w_deriv(x6)
    g3w7 = sop_w_deriv(x7)
    g3w8 = sop_w_deriv(x8)
    g3w9 = sop_w_deriv(x9)
    g3w10 = sop_w_deriv(x10)

    gradw1 = g3w1*g2*g1
    gradw2 = g3w2*g2*g1
    gradw3 = g3w3*g2*g1
    gradw4 = g3w4*g2*g1
    gradw5 = g3w5*g2*g1
    gradw6 = g3w6*g2*g1
    gradw7 = g3w7*g2*g1
    gradw8 = g3w8*g2*g1
    gradw9 = g3w9*g2*g1
    gradw10 = g3w10*g2*g1

    w1 = update_w(w1, gradw1, learning_rate)
    w2 = update_w(w2, gradw2, learning_rate)
    w3 = update_w(w3, gradw3, learning_rate)
    w4 = update_w(w4, gradw4, learning_rate)
    w5 = update_w(w5, gradw5, learning_rate)
    w6 = update_w(w6, gradw6, learning_rate)
    w7 = update_w(w7, gradw7, learning_rate)
    w8 = update_w(w8, gradw8, learning_rate)
    w9 = update_w(w9, gradw9, learning_rate)
    w10 = update_w(w10, gradw10, learning_rate)

    print(predicted)

下图显示了迭代次数对预测输出的影响。

下图显示了误差和迭代次数之间的关系。大约 26,000 次迭代后,误差为 0.0。

至此,用于优化具有 10 个输入的网络的 GD 算法的实现完成。您可能想知道如果有 10 个以上的输入会怎么样。我们必须为每个输入神经元添加更多的线吗?使用当前的实现,我们必须复制这些行,但这不是唯一的方法。我们可以改进前面的代码,这样就根本不需要修改代码来处理任何数量的输入。

处理任意数量的输入

目前实现 GD 算法所遵循的策略是为每个新输入复制一些行代码。尽管这是一种遗留的方法,但理解每一小步是如何工作的是有帮助的。在本节中,将对前面的实现进行改进,这样当输入数量增加或减少时,我们就不必编辑代码了。我们要做的是检查之前的实现,寻找为每个输入神经元重复的线。之后,这些线将被一条适用于所有输入的单线所取代。在前面的代码中,有 6 个部分需要改进:

  1. 指定输入值。
  2. 权重初始化。
  3. 计算 SOP。
  4. 计算重量衍生产品的 SOP。
  5. 计算权重的梯度。
  6. 更新权重。

让我们研究每一部分,看看我们能做些什么。

指定输入值

上面代码中用于指定所有输入值的部分如下所示。如果要添加更多的输入,将会写入更多的行。

x1=0.1
x2=0.4
x3=1.1
x4=1.3
x5=1.8
x6=2.0
x7=0.01
x8=0.9
x9=0.8
x10=1.6

我们可以使用一种更好的方法,将所有这些行替换为下面给出的一行。NumPy 数组保存所有这些输入。使用索引,我们可以返回所有单独的输入。例如,如果要检索第一个输入,那么索引 0 用于索引数组 x。

x = numpy.array([0.1, 0.4, 1.1, 1.3, 1.8, 2.0, 0.01, 0.9, 0.8, 1.6])

权重初始化

下面给出了前面用于初始化权重的代码部分。如果要初始化更多的权重,将会写入更多的行。

w1=numpy.random.rand()
w2=numpy.random.rand()
w3=numpy.random.rand()
w4=numpy.random.rand()
w5=numpy.random.rand()
w6=numpy.random.rand()
w7=numpy.random.rand()
w8=numpy.random.rand()
w9=numpy.random.rand()
w10=numpy.random.rand()

我们可以用下面的行代替所有这些行,而不是添加单独的行来初始化每个权重。这将返回一个有 10 个值的 NumPy 数组,每个值对应一个权重。同样,使用索引,我们可以检索单个权重。

w = numpy.random.rand(10)

计算 SOP

先前代码中的 SOP 计算如下。对于每个输入,我们必须在下面的等式中添加一个新项,用于乘以其权重。

y = w1*x1 + w2*x2 + w3*x3 + w4*x4 + w5*x5 + w6*x6 + w7*x7 + w8*x8 + w9*x9 + w10*x10

我们可以使用更好的方法,而不是用这种方法将每个输入乘以其权重。请记住,SOP 的计算方法是将每个输入乘以其权重。另外,记住 xw 现在都是 NumPy 数组,并且每个都有 10 个值,其中数组 w 中索引 i 处的权重对应于数组 x 处索引 i 处的输入。我们需要的是将每个输入乘以其权重,并返回这些乘积的总和。好消息是 NumPy 支持数组逐值相乘。因此,写入 w*x 将返回一个新数组,其中的 10 值表示每个输入的权重乘积。我们可以将所有这些产品相加,并使用下面给出的行返回 SOP。

y = numpy.sum(w*x)

计算重量衍生产品的 SOP

需要编辑的下一部分代码是负责计算重量导数的 SOP 的部分。下面给出。

g3w1 = sop_w_deriv(x1)
g3w2 = sop_w_deriv(x2)
g3w3 = sop_w_deriv(x3)
g3w4 = sop_w_deriv(x4)
g3w5 = sop_w_deriv(x5)
g3w6 = sop_w_deriv(x6)
g3w7 = sop_w_deriv(x7)
g3w8 = sop_w_deriv(x8)
g3w9 = sop_w_deriv(x9)
g3w10 = sop_w_deriv(x10)

不用调用 sop_w_deriv() 函数来计算每个权重的导数,我们可以简单地将数组 x 传递给这个函数,如下所示。当这个函数接收一个 NumPy 数组时,它独立地处理该数组中的每个值,并返回一个新的 NumPy 数组和结果。

g3 = sop_w_deriv(x)

计算权重的梯度

下面给出了负责计算权重梯度的代码部分。

gradw1 = g3w1*g2*g1
gradw2 = g3w2*g2*g1
gradw3 = g3w3*g2*g1
gradw4 = g3w4*g2*g1
gradw5 = g3w5*g2*g1
gradw6 = g3w6*g2*g1
gradw7 = g3w7*g2*g1
gradw8 = g3w8*g2*g1
gradw9 = g3w9*g2*g1
gradw10 = g3w10*g2*g1

我们可以简单地使用下面的行,而不是添加一个新的行来乘以链中每个权重的所有导数。记住 g1g2 都是保存单个值的数组,但是 g3 是保存 10 个值的 Numpy 数组。这可以看作是一个数组乘以一个标量值。

grad = g3*g2*g1

更新权重

下面列出了要编辑的最终代码部分,它负责更新权重。

w1 = update_w(w1, gradw1, learning_rate)
w2 = update_w(w2, gradw2, learning_rate)
w3 = update_w(w3, gradw3, learning_rate)
w4 = update_w(w4, gradw4, learning_rate)
w5 = update_w(w5, gradw5, learning_rate)
w6 = update_w(w6, gradw6, learning_rate)
w7 = update_w(w7, gradw7, learning_rate)
w8 = update_w(w8, gradw8, learning_rate)
w9 = update_w(w9, gradw9, learning_rate)
w10 = update_w(w10, gradw10, learning_rate)

不用为每个权重调用 update_w() 函数,我们可以简单地将上一步计算的 grad 数组和权重数组 w 一起传递给这个函数,如下所示。在函数内部,将为每个权重及其梯度调用权重更新等式。它将返回一个新的 10 值数组,代表可以在下一次迭代中使用的新权重。

w = update_w(w, grad, learning_rate)

完成所有编辑后,最终优化的代码如下所示。它显示了前面的代码,但是作为一种映射每个部分及其编辑的方式进行了注释。

import numpy

def sigmoid(sop):
    return 1.0/(1+numpy.exp(-1*sop))

def error(predicted, target):
    return numpy.power(predicted-target, 2)

def error_predicted_deriv(predicted, target):
    return 2*(predicted-target)

def sigmoid_sop_deriv(sop):
    return sigmoid(sop)*(1.0-sigmoid(sop))

def sop_w_deriv(x):
    return x

def update_w(w, grad, learning_rate):
    return w - learning_rate*grad

#x1=0.1
#x2=0.4
#x3=4.1
#x4=4.3
#x5=1.8
#x6=2.0
#x7=0.01
#x8=0.9
#x9=3.8
#x10=1.6
x = numpy.array([0.1, 0.4, 1.1, 1.3, 1.8, 2.0, 0.01, 0.9, 0.8, 1.6])
target = numpy.array([0.2])

learning_rate = 0.1

#w1=numpy.random.rand()
#w2=numpy.random.rand()
#w3=numpy.random.rand()
#w4=numpy.random.rand()
#w5=numpy.random.rand()
#w6=numpy.random.rand()
#w7=numpy.random.rand()
#w8=numpy.random.rand()
#w9=numpy.random.rand()
#w10=numpy.random.rand()
w = numpy.random.rand(10)

#print("Initial W : ", w1, w2, w3, w4, w5, w6, w7, w8, w9, w10)
print("Initial W : ", w)

for k in range(1000000000):
    # Forward Pass
#    y = w1*x1 + w2*x2 + w3*x3 + w4*x4 + w5*x5 + w6*x6 + w7*x7 + w8*x8 + w9*x9 + w10*x10
    y = numpy.sum(w*x)
    predicted = sigmoid(y)
    err = error(predicted, target)

    # Backward Pass
    g1 = error_predicted_deriv(predicted, target)

    g2 = sigmoid_sop_deriv(y)

#    g3w1 = sop_w_deriv(x1)
#    g3w2 = sop_w_deriv(x2)
#    g3w3 = sop_w_deriv(x3)
#    g3w4 = sop_w_deriv(x4)
#    g3w5 = sop_w_deriv(x5)
#    g3w6 = sop_w_deriv(x6)
#    g3w7 = sop_w_deriv(x7)
#    g3w8 = sop_w_deriv(x8)
#    g3w9 = sop_w_deriv(x9)
#    g3w10 = sop_w_deriv(x10)
    g3 = sop_w_deriv(x)
#    g3 = numpy.array([sop_w_deriv(x_) for x_ in x])

#    gradw1 = g3w1*g2*g1
#    gradw2 = g3w2*g2*g1
#    gradw3 = g3w3*g2*g1
#    gradw4 = g3w4*g2*g1
#    gradw5 = g3w5*g2*g1
#    gradw6 = g3w6*g2*g1
#    gradw7 = g3w7*g2*g1
#    gradw8 = g3w8*g2*g1
#    gradw9 = g3w9*g2*g1
#    gradw10 = g3w10*g2*g1
    grad = g3*g2*g1

#    w1 = update_w(w1, gradw1, learning_rate)
#    w2 = update_w(w2, gradw2, learning_rate)
#    w3 = update_w(w3, gradw3, learning_rate)
#    w4 = update_w(w4, gradw4, learning_rate)
#    w5 = update_w(w5, gradw5, learning_rate)
#    w6 = update_w(w6, gradw6, learning_rate)
#    w7 = update_w(w7, gradw7, learning_rate)
#    w8 = update_w(w8, gradw8, learning_rate)
#    w9 = update_w(w9, gradw9, learning_rate)
#    w10 = update_w(w10, gradw10, learning_rate)
    w = update_w(w, grad, learning_rate)
#    w = numpy.array([update_w(w_, grad_, learning_rate) for (w_, grad_) in [(w[i], grad[i]) for i in range(10)]])

    print(predicted)

删除注释后,代码如下。代码现在非常清晰。

import numpy

def sigmoid(sop):
    return 1.0/(1+numpy.exp(-1*sop))

def error(predicted, target):
    return numpy.power(predicted-target, 2)

def error_predicted_deriv(predicted, target):
    return 2*(predicted-target)

def sigmoid_sop_deriv(sop):
    return sigmoid(sop)*(1.0-sigmoid(sop))

def sop_w_deriv(x):
    return x

def update_w(w, grad, learning_rate):
    return w - learning_rate*grad

x = numpy.array([0.1, 0.4, 1.1, 1.3, 1.8, 2.0, 0.01, 0.9, 0.8, 1.6])
target = numpy.array([0.2])

learning_rate = 0.1

w = numpy.random.rand(10)

print("Initial W : ", w)

for k in range(1000000000):
    # Forward Pass
    y = numpy.sum(w*x)
    predicted = sigmoid(y)
    err = error(predicted, target)

    # Backward Pass
    g1 = error_predicted_deriv(predicted, target)

    g2 = sigmoid_sop_deriv(y)

    g3 = sop_w_deriv(x)

    grad = g3*g2*g1

    w = update_w(w, grad, learning_rate)

    print(predicted)

假设我们要创建一个有 5 个输入的网络,我们可以简单地做两个改变:

  1. 准备输入数组 x 只有 5 个值。
  2. 在**numpy . rand()**中用 5 替换 10。

下面给出了适用于 5 个输入的代码。

import numpy

def sigmoid(sop):
    return 1.0/(1+numpy.exp(-1*sop))

def error(predicted, target):
    return numpy.power(predicted-target, 2)

def error_predicted_deriv(predicted, target):
    return 2*(predicted-target)

def sigmoid_sop_deriv(sop):
    return sigmoid(sop)*(1.0-sigmoid(sop))

def sop_w_deriv(x):
    return x

def update_w(w, grad, learning_rate):
    return w - learning_rate*grad

x = numpy.array([0.1, 0.4, 1.1, 1.3, 1.8])
target = numpy.array([0.2])

learning_rate = 0.1

w = numpy.random.rand(5)

print("Initial W : ", w)

for k in range(1000000000):
    # Forward Pass
    y = numpy.sum(w*x)
    predicted = sigmoid(y)
    err = error(predicted, target)

    # Backward Pass
    g1 = error_predicted_deriv(predicted, target)

    g2 = sigmoid_sop_deriv(y)

    g3 = sop_w_deriv(x)

    grad = g3*g2*g1

    w = update_w(w, grad, learning_rate)

    print(predicted)

结论

在这一点上,我们成功地实现了 GD 算法,用于处理具有输入层和输出层的 ANN,其中输入层可以包括任意数量的输入。在下一个教程中,这个实现将被扩展为在 ANN 中添加一个单独的隐藏层,并使用 GD 算法进行优化。

用 Python 实现梯度下降,第 3 部分:添加隐藏层

原文:https://blog.paperspace.com/part-3-generic-python-implementation-of-gradient-descent-for-nn-optimization/

在 Python 中实现通用梯度下降(GD)算法以优化反向传播阶段的人工神经网络(ANN)参数的系列教程中,再次向您问好。GD 实现将是通用的,可以与任何人工神经网络架构一起工作。

在第 2 部分中,实现了 GD 算法,以便它可以处理任意数量的输入神经元。在第 3 部分中,这是本系列的第三篇教程,第 2 部分的实现将被扩展,以允许 GD 算法处理具有 2 个神经元的单个隐藏层。本教程有两个部分。在第一部分,人工神经网络将有 3 个输入,1 个具有 3 个神经元的隐藏层,和一个具有 1 个神经元的输出层。在第二部分中,输入数量将从 3 个增加到 10 个。

1 个具有 2 个神经元的隐藏层

本节扩展了第 2 部分中 GD 算法的实现,使其能够处理一个包含两个神经元的隐藏层。第 2 部分使用了 10 个输入,但为了简单起见,本部分将只使用 3 个输入。下图给出了具有 3 个输入、1 个具有 2 个神经元的隐藏层和 1 个输出神经元的 ANN 图。

现在,3 个输入中的每个输入都连接到 2 个隐藏的神经元。对于每个连接,都有不同的权重。输入层和隐藏层之间的权重被标记为 Wzy ,其中 z 是指输入层神经元索引, y 是指隐藏神经元的索引。

第一个输入 X1 和第一个隐神经元之间连接的权重是 W11 。同样,权重 W12 用于 X1 和第二个隐藏神经元之间的连接。关于 X2 ,权重 W21W22 分别用于第一和第二隐藏神经元的连接。同样, X3 有两个权重 W31W32

除了输入层和隐藏层之间的权重之外,还有 2 个权重将 2 个隐藏神经元连接到输出神经元,它们是 W41W42

如何让 GD 算法与所有这些参数一起工作?在写出从误差开始直到达到每个单独重量的导数链之后,答案会简单得多。通常,在考虑 GD 算法更新权重的向后传递之前,我们必须从向前传递开始。

前进传球

在前向传递中,隐藏层中的神经元除了接受它们的权重之外,还接受来自输入层的输入。然后,计算输入与其权重之间的乘积之和( SOP )。关于第一个隐藏神经元,它分别接受 3 个输入 X1X2X3 以及它们的权重 W11W21W31 。该神经元的 SOP 通过对每个输入与其权重之间的乘积求和来计算,因此结果是:

SOP1=X1*W11+X2*W21+X3*W31

第一个隐藏神经元的 SOP 在图中标为 SOP1 以供参考。对于第二个隐藏神经元,其 SOP 标记为 SOP2 ,如下所示:

SOP2=X1*W12+X2*W22+X3*W32

在计算了所有隐藏神经元的 SOP 之后,接下来是将这样的 SOP 馈送到激活函数。此系列中使用的函数是 sigmoid 函数,其计算方法如下图中的等式所示。

通过将 SOP1 输入到 sigmoid 函数,结果是由下式计算的 Activ1 :

由下式计算出的 SOP2Activ2 :

请记住,在前向传递中,一层的输出被视为下一层的输入。隐藏层的输出 Activ1Activ2 被视为输出层的输入。重复该过程以计算输出层神经元中的 SOP。输出神经元的每个输入都有一个权重。对于第一个输入 Activ1 ,其权重为 W41 。第二输入 Activ2 的重量为 W42 。输出神经元的 SOP 标记为 SOP3 ,计算如下:

SOP3=Activ1*W41+Activ2*W42

SOP3 被馈送到 sigmoid 函数以返回 Activ3 ,如下式所示:

在本教程中,激活函数的输出被视为网络的预测输出。网络做出预测后,下一步是使用下面给出的平方误差函数计算误差。

此时,向前传球完成,我们准备好通过向后传球。

偶数道次

在反向传递中,目标是计算更新网络中每个权重的梯度。因为我们从正向传递中结束的地方开始,所以首先计算最后一层的梯度,然后移动直到到达输入层。让我们开始计算隐藏层和输出层之间的权重梯度。

因为没有包含误差和权重(W41 和 W42)的显式方程,所以最好使用链式法则。计算这些权重的梯度所需的导数链是什么?

从第一个权重开始,我们需要找到误差对 W41 的导数。误差方程有两项,如下所示:

  1. 预测
  2. 目标

在这两项中,哪一项将误差与重量 W41 联系起来?确定它是预测的,因为它是使用 sigmoid 函数计算的,该函数接受包含 W41 的 SOP3。因此,要计算的一阶导数是预测输出导数的误差,其计算如下式所示。

之后,接下来是通过用 SOP3 代入 sigmoid 函数的导数来计算预测的SOP3 的导数,如下式所示。

接下来是计算 SOP3 对 W41 的导数。还记得包含 SOP3 和 W41 的等式吗?下面重复一遍。

SOP3 = Activ1*W41 + Activ2*W42

SOP3 对 W41 的导数由下式给出。

通过计算链中从误差到 W41 的所有导数,我们可以通过将所有这些导数相乘来计算误差W41 的导数,如下式所示。

类似于计算误差W41 的导数,我们可以很容易地计算出误差W42 的导数。与上一个等式不同的唯一一项是最后一项。现在,我们不是计算 SOP3 至 W41 的导数,而是计算 SOP3 至 W42 的导数,如下式所示。

最后,根据下式计算 W42 导数的误差。

此时,我们成功地计算了隐藏层和输出层之间所有权重的梯度。接下来是计算输入层和隐藏层之间的权重梯度。这两层之间的误差和权重之间的导数链是什么?当然,前两个导数是前一个链中使用的前两个导数,如下所示:

  1. 预测导数的误差。
  2. 预测到 SOP3 导数。

我们需要计算 SOP3 对 Activ1 和 Activ2 的导数,而不是计算 SOP3 对 W41 和 W4s 的导数。SOP3 到 Activ1 导数有助于计算连接到第一个隐藏神经元的权重梯度,即 W11、W21 和 W31。SOP3 到 Activ2 的导数有助于计算连接到第二个隐藏神经元的权重的梯度,即 W12、W22 和 W32。

从 Activ1 开始,将 SOP3 与 Activ1 相关联的等式重复如下:

SOP3=Activ1*W41+Activ2*W42

SOP3 对 Activ1 的导数计算如下式所示:

类似地,SOP3 对 Activ2 的导数计算如下式所示:

之后,我们可以计算链中的下一个导数,即 Activ1 对 SOP1 的导数,SOP1 是通过在 sigmoid 函数的导数方程中代入 so P1 计算的,如下所示。这将用于更新权重 W11、W21 和 W31。

类似地,Activ2 至 SOP2 的导数计算如下。这将用于更新权重 W12、W22 和 W32。

为了更新权重 W11、W21 和 W31,要计算的最后一个导数是 SOP1 与所有这些权重之间的导数。首先,我们必须记住将 SOP1 与所有这些权重相关联的等式。下面重复一遍。

SOP1=X1*W11+X2*W21+X3*W31

SOP1 对所有这 3 个权重的导数在下面的等式中给出。

类似地,我们必须记住将 SOP2 与权重 W12、W22 和 W32 相关联的等式,这就是下面再次重复该等式的原因。

SOP2=X1*W12+X2*W22+X3*W32

下图给出了 SOP2 至 W12、W22 和 W32 的导数。

在计算了从误差到输入层和隐藏层之间的所有权重的链中的所有导数之后,接下来是将它们相乘以计算梯度,通过该梯度来更新这些权重。

对于连接到第一个隐藏神经元(W11、W21 和 W31)的权重,将使用下面的链来计算它们的梯度。注意,所有这些链共享所有导数,除非最后一个导数。

对于连接到第二个隐藏神经元 W12、W22 和 W32 的权重,将使用下面的链来计算它们的梯度。注意,所有这些链共享所有导数,除非最后一个导数。

此时,我们已经成功准备好了计算整个网络中所有权重梯度的链。我们可以在下图中总结所有这些链。

理解了为当前网络实现 GD 算法背后的理论之后,接下来是开始用 Python 实现这样的算法。请注意,该实现高度依赖于本系列前面部分中开发的实现。

Python 实现

下面列出了实现具有 3 个输入、1 个具有 2 个神经元的隐藏层和 1 个输出神经元的 ANN 并使用 GD 算法对其进行优化的完整代码。将讨论该代码的各个部分。

import numpy

def sigmoid(sop):
    return 1.0 / (1 + numpy.exp(-1 * sop))

def error(predicted, target):
    return numpy.power(predicted - target, 2)

def error_predicted_deriv(predicted, target):
    return 2 * (predicted - target)

def sigmoid_sop_deriv(sop):
    return sigmoid(sop) * (1.0 - sigmoid(sop))

def sop_w_deriv(x):
    return x

def update_w(w, grad, learning_rate):
    return w - learning_rate * grad

x = numpy.array([0.1, 0.4, 4.1])
target = numpy.array([0.2])

learning_rate = 0.001

w1_3 = numpy.random.rand(3)
w2_3 = numpy.random.rand(3)
w3_2 = numpy.random.rand(2)
w3_2_old = w3_2
print("Initial W : ", w1_3, w2_3, w3_2)

# Forward Pass
# Hidden Layer Calculations
sop1 = numpy.sum(w1_3 * x)
sop2 = numpy.sum(w2_3 * x)

sig1 = sigmoid(sop1)
sig2 = sigmoid(sop2)

# Output Layer Calculations
sop3 = numpy.sum(w3_2 * numpy.array([sig1, sig2]))

predicted = sigmoid(sop3)
err = error(predicted, target)

# Backward Pass
g1 = error_predicted_deriv(predicted, target)

### Working with weights between hidden and output layer
g2 = sigmoid_sop_deriv(sop3)

g3 = numpy.zeros(w3_2.shape[0])
g3[0] = sop_w_deriv(sig1)
g3[1] = sop_w_deriv(sig2)

grad_hidden_output = g3 * g2 * g1

w3_2 = update_w(w3_2, grad_hidden_output, learning_rate)

### Working with weights between input and hidden layer
# First Hidden Neuron
g3 = sop_w_deriv(w3_2_old[0])
g4 = sigmoid_sop_deriv(sop1)

g5 = sop_w_deriv(x)

grad_hidden1_input = g5 * g4 * g3 * g2 * g1

w1_3 = update_w(w1_3, grad_hidden1_input, learning_rate)

# Second Hidden Neuron
g3 = sop_w_deriv(w3_2_old[1])
g4 = sigmoid_sop_deriv(sop2)

g5 = sop_w_deriv(x)

grad_hidden2_input = g5 * g4 * g3 * g2 * g1

w2_3 = update_w(w2_3, grad_hidden2_input, learning_rate)

w3_2_old = w3_2
print(predicted)

首先,使用这两行代码准备输入和输出:

x = numpy.array([0.1, 0.4, 4.1])
target = numpy.array([0.2])

之后,根据这些线准备网络权重。请注意, w1_3 是一个数组,包含将 3 个输入连接到第一个隐藏神经元的 3 个权重。 w2_3 是一个包含 3 个权重的数组,将 3 个输入连接到第二个隐藏神经元。最后,w3_2 是一个具有 2 个权重的数组,用于连接隐含层神经元和输出神经元。

w1_3 = numpy.random.rand(3)
w2_3 = numpy.random.rand(3)
w3_2 = numpy.random.rand(2)

准备好输入和权重后,接下来是根据下面的代码进行正向传递。它首先计算两个隐藏神经元的乘积之和,然后将它们提供给 sigmoid 函数。sigmoid 函数的 2 个输出乘以连接到输出神经元的 2 个权重,以返回 sop3 。这也被用作 sigmoid 函数的输入,以返回预测输出。最后计算误差。

# Forward Pass
# Hidden Layer Calculations
sop1 = numpy.sum(w1_3 * x)
sop2 = numpy.sum(w2_3 * x)

sig1 = sigmoid(sop1)
sig2 = sigmoid(sop2)

# Output Layer Calculations
sop3 = numpy.sum(w3_2 * numpy.array([sig1, sig2]))

predicted = sigmoid(sop3)
err = error(predicted, target)

向前传球完成后,接下来是向后传球。下面给出了负责更新隐藏层和输出层之间的权重的代码部分。预测输出导数的误差被计算并保存在变量 g1 中。 g2 保存对 SOP3 导数的预测输出。最后,计算 SOP3 到 W41 和 W42 的导数,并保存在变量 g3 中。在计算 W41 和 W41 的梯度所需的所有导数后,梯度被计算并保存在 grad_hidden_output 变量中。最后,通过传递旧的权重、梯度和学习率,使用 update_w() 函数更新这些权重。

# Backward Pass
g1 = error_predicted_deriv(predicted, target)

### Working with weights between hidden and output layer
g2 = sigmoid_sop_deriv(sop3)

g3 = numpy.zeros(w3_2.shape[0])
g3[0] = sop_w_deriv(sig1)
g3[1] = sop_w_deriv(sig2)

grad_hidden_output = g3 * g2 * g1

w3_2 = update_w(w3_2, grad_hidden_output, learning_rate)

更新隐藏层和输出层之间的权重后,接下来是处理输入层和隐藏层之间的权重。下面是更新连接到第一个隐藏神经元的权重所需的代码。 g3 代表 SOP3 对 Activ1 的导数。因为这种导数是使用隐藏层和输出层之间的旧的权重值而不是更新的权重值来计算的,所以旧的权重被保存到 w3_2_old 变量中,以便在该步骤中使用。 g4 代表 Activ1SOP1 的导数。最后, g5 代表 SOP1 对权重( W11W21W31 )的导数。

当计算这 3 个权重的梯度时,g3、g4 和 g5 彼此相乘。在更新隐藏层和输出层之间的权重时,它们还会乘以 g2 和 g1。基于计算的梯度,将 3 个输入连接到第一个隐藏神经元的权重被更新。

### Working with weights between input and hidden layer
# First Hidden Neuron
g3 = sop_w_deriv(w3_2_old[0])
g4 = sigmoid_sop_deriv(sop1)

g5 = sop_w_deriv(x)

grad_hidden1_input = g5*g4*g3*g2*g1

w1_3 = update_w(w1_3, grad_hidden1_input, learning_rate)

类似于处理连接到第一个隐藏神经元的 3 个权重,连接到第二个隐藏神经元的其他 3 个权重根据下面的代码进行更新。

# Second Hidden Neuron
g3 = sop_w_deriv(w3_2_old[1])
g4 = sigmoid_sop_deriv(sop2)

g5 = sop_w_deriv(x)

grad_hidden2_input = g5*g4*g3*g2*g1

w2_3 = update_w(w2_3, grad_hidden2_input, learning_rate)

在代码结束时, w3_2_old 变量被设置为等于 w3_2

w3_2_old = w3_2

到了这一步,我们示例中实现 GD 算法的全部代码就完成了。剩下的编辑是使用一个循环来进行多次迭代,以更新权重,从而做出更好的预测。下面是更新后的代码。

import numpy

def sigmoid(sop):
    return 1.0/(1+numpy.exp(-1*sop))

def error(predicted, target):
    return numpy.power(predicted-target, 2)

def error_predicted_deriv(predicted, target):
    return 2*(predicted-target)

def sigmoid_sop_deriv(sop):
    return sigmoid(sop)*(1.0-sigmoid(sop))

def sop_w_deriv(x):
    return x

def update_w(w, grad, learning_rate):
    return w - learning_rate*grad

x = numpy.array([0.1, 0.4, 4.1])
target = numpy.array([0.2])

learning_rate = 0.001

w1_3 = numpy.random.rand(3)
w2_3 = numpy.random.rand(3)
w3_2 = numpy.random.rand(2)
w3_2_old = w3_2
print("Initial W : ", w1_3, w2_3, w3_2)

for k in range(80000):
    # Forward Pass
    # Hidden Layer Calculations
    sop1 = numpy.sum(w1_3*x)
    sop2 = numpy.sum(w2_3*x)

    sig1 = sigmoid(sop1)
    sig2 = sigmoid(sop2)

    # Output Layer Calculations
    sop3 = numpy.sum(w3_2*numpy.array([sig1, sig2]))

    predicted = sigmoid(sop3)
    err = error(predicted, target)

    # Backward Pass
    g1 = error_predicted_deriv(predicted, target)

    ### Working with weights between hidden and output layer
    g2 = sigmoid_sop_deriv(sop3)

    g3 = numpy.zeros(w3_2.shape[0])
    g3[0] = sop_w_deriv(sig1)
    g3[1] = sop_w_deriv(sig2)

    grad_hidden_output = g3*g2*g1

    w3_2 = update_w(w3_2, grad_hidden_output, learning_rate)

    ### Working with weights between input and hidden layer
    # First Hidden Neuron
    g3 = sop_w_deriv(w3_2_old[0])
    g4 = sigmoid_sop_deriv(sop1)

    g5 = sop_w_deriv(x)

    grad_hidden1_input = g5*g4*g3*g2*g1

    w1_3 = update_w(w1_3, grad_hidden1_input, learning_rate)

    # Second Hidden Neuron
    g3 = sop_w_deriv(w3_2_old[1])
    g4 = sigmoid_sop_deriv(sop2)

    g5 = sop_w_deriv(x)

    grad_hidden2_input = g5*g4*g3*g2*g1

    w2_3 = update_w(w2_3, grad_hidden2_input, learning_rate)

    w3_2_old = w3_2
    print(predicted)

迭代完成后,下图显示了迭代的预测输出是如何变化的。

下图显示了迭代过程中误差的变化。

使用 10 个输入

之前的实现使用只有 3 个输入的输入层。如果使用更多的输入会怎样?是否需要对代码做大量修改?答案是否定的,因为有两处小改动:

  1. 编辑输入数组 x 以添加更多输入。
  2. 编辑权重数组的大小以返回 10 个权重,而不是 3 个。

下面列出了使用 10 个输入的实现。除了保存 10 个值的输入数组 x 之外,代码中的所有内容都与上一节中的内容相同。同样,使用**numpy . rand()**函数返回 10 个权重。这就是你需要做的一切。

import numpy

def sigmoid(sop):
    return 1.0 / (1 + numpy.exp(-1 * sop))

def error(predicted, target):
    return numpy.power(predicted - target, 2)

def error_predicted_deriv(predicted, target):
    return 2 * (predicted - target)

def sigmoid_sop_deriv(sop):
    return sigmoid(sop) * (1.0 - sigmoid(sop))

def sop_w_deriv(x):
    return x

def update_w(w, grad, learning_rate):
    return w - learning_rate * grad

x = numpy.array([0.1, 0.4, 4.1, 4.3, 1.8, 2.0, 0.01, 0.9, 3.8, 1.6])
target = numpy.array([0.2])

learning_rate = 0.001

w1_10 = numpy.random.rand(10)
w2_10 = numpy.random.rand(10)
w3_2 = numpy.random.rand(2)
w3_2_old = w3_2
print("Initial W : ", w1_10, w2_10, w3_2)

for k in range(80000):
    # Forward Pass
    # Hidden Layer Calculations
    sop1 = numpy.sum(w1_10 * x)
    sop2 = numpy.sum(w2_10 * x)

    sig1 = sigmoid(sop1)
    sig2 = sigmoid(sop2)

    # Output Layer Calculations
    sop3 = numpy.sum(w3_2 * numpy.array([sig1, sig2]))

    predicted = sigmoid(sop3)
    err = error(predicted, target)

    # Backward Pass
    g1 = error_predicted_deriv(predicted, target)

    ### Working with weights between hidden and output layer
    g2 = sigmoid_sop_deriv(sop3)

    g3 = numpy.zeros(w3_2.shape[0])
    g3[0] = sop_w_deriv(sig1)
    g3[1] = sop_w_deriv(sig2)

    grad_hidden_output = g3 * g2 * g1

    w3_2[0] = update_w(w3_2[0], grad_hidden_output[0], learning_rate)
    w3_2[1] = update_w(w3_2[1], grad_hidden_output[1], learning_rate)

    ### Working with weights between input and hidden layer
    # First Hidden Neuron
    g3 = sop_w_deriv(w3_2_old[0])
    g4 = sigmoid_sop_deriv(sop1)

    g5 = numpy.zeros(w1_10.shape[0])
    g5 = sop_w_deriv(x)

    grad_hidden1_input = g5 * g4 * g3 * g2 * g1

    w1_10 = update_w(w1_10, grad_hidden1_input, learning_rate)

    # Second Hidden Neuron
    g3 = sop_w_deriv(w3_2_old[1])
    g4 = sigmoid_sop_deriv(sop2)

    g5 = numpy.zeros(w2_10.shape[0])
    g5 = sop_w_deriv(x)

    grad_hidden2_input = g5 * g4 * g3 * g2 * g1

    w2_10 = update_w(w2_10, grad_hidden2_input, learning_rate)

    w3_2_old = w3_2
    print(predicted)

用 Python 实现梯度下降,第 4 部分:适用于任意数量的神经元

原文:https://blog.paperspace.com/part-4-generic-python-implementation-of-gradient-descent-for-nn-optimization/

在本教程中,我们将梯度下降的实现扩展到具有任意数量神经元的单个隐藏层。

第 4 部分分为两节。首先,我们将扩展第 3 部分的实现,允许在一个隐藏层中有 5 个神经元,而不是只有 2 个。第二部分将解决使梯度下降(GD)算法神经元不可知的问题,因为任意数量的隐藏神经元可以被包括在单个隐藏层中。

这是教程系列的第四部分,专门向您展示如何用 Python 实现一般的梯度下降算法。这可以为任何神经网络架构实现,以优化其参数。在第 2 部分中,我们看到了如何为任意数量的输入神经元实现 GD 算法。在第 3 部分中,我们扩展了这个实现,使其适用于一个额外的具有 2 个神经元的单层。在这部分教程的最后,将会有一个用 Python 实现的梯度下降算法,它可以处理任意数量的输入,以及一个具有任意数量神经元的单个隐藏层

步骤 1: 1 个有 5 个神经元的隐藏层

我们将从扩展前面的实现开始,允许在隐藏层中有 5 个神经元。下图示意性地显示了这一点。扩展算法的一个简单方法就是重复我们已经写好的几行代码,现在针对所有 5 个神经元。

在查看向后传递之前,值得回忆的是,在向前传递中使用了 sigmoid 激活函数(定义如下)。注意 SOP 代表之和。

使用标准平方误差函数计算误差。

在反向传递中,用于更新隐藏层和输出层之间的权重的梯度被简单地计算,如第 3 部分中所讨论的,没有任何改变。一阶导数是下面给出的预测输出导数的误差。

二阶导数是预测输出对 SOP6 的导数。

第三个也是最后一个导数是隐藏层和输出层之间权重的 SOP6 。因为有 5 个权重将 5 个隐藏神经元连接到输出神经元,所以将有 5 个导数,每个权重一个。记住 SOP6 是根据下面的等式计算的:

SOP6 = Activ1*W41 + Activ2*W42 + Activ3*W43 + Activ4*W44 + Activ5*W45

比如 SOP6W41 的导数等于 Activ1, SOP6W42 的导数等于 Activ2 等等。

为了计算这 5 个权重的梯度,将前面 3 个导数的链相乘。所有梯度都是根据下图中的等式计算的。所有这些梯度共享链中的前两个导数。

计算隐藏层和输出层之间的权重梯度后,接下来是计算输入层和隐藏层之间的权重梯度。

用于计算此类梯度的导数链将从之前计算的前两个导数开始,它们是:

  1. 预测输出导数的误差。
  2. SOP6 导数的预测输出。

链中的三阶导数将是 sigmoid 函数( Activ1Activ5 )输出的 SOP6。基于下面再次给出的将 SOP6 和 Activ1 与 Activ2 相关联的等式,SOP6 对 Activ1 的导数等于 W41,SOP6 对 Activ2 的导数等于 W42,以此类推。

SOP6 = Activ1*W41 + Activ2*W42 + Activ3*W43 + Activ4*W44 + Activ5*W45

链中的下一个导数是 sigmoid 函数对隐藏层中的 SOP 的导数。例如,Activ1 对 SOP1 的导数根据下式计算。要计算 Activ2 对 SOP2 的导数,只需将 SOP1 替换为 SOP2。这适用于所有其他衍生品。

链中的最后一个导数是计算每个隐藏神经元的 SOP 相对于与其相连的权重的导数。为简单起见,下图显示了 ANN 体系结构,除了与第一个隐藏神经元的连接之外,输入层和隐藏层之间的所有连接都被删除。

为了计算 SOP1 对其 3 个权重(W11、W21 和 W31)的导数,我们必须记住下面给出的与所有权重相关的等式。因此,SOP1 对 W11 的导数为 X1,SOP2 对 W21 的导数为 X2,依此类推。

SOP1 = X1*W11 + X2*W21 + X3*W31

如果将输入神经元连接到第二个隐藏神经元的权重是 W12、W22 和 W32,则 SOP2 计算如下。因此,SOP2 至 W12 的导数是 X1,SOP2 至 W22 的导数是 X2,依此类推。对于所有其他隐藏的神经元,该过程继续。

SOP2 = X1*W12 + X2*W22 + X3*W32

你可能会注意到,任何 SOP 对其 3 个权重的导数的结果将是 X1、X2 和 X3。

在计算了从误差到输入层权重的链中的所有导数之后,我们可以计算梯度。例如,连接到第一个隐藏神经元的 3 个权重的 3 个梯度是根据下面列出的等式计算的。注意,除了最后一个导数,所有的链都有相同的导数。

对于第二个隐藏神经元,每个 Activ1 由 Activ2 代替,每个 SOP1 由 SOP2 代替。这对于处理其他隐藏神经元也是有效的。

此时,我们成功地准备了用于计算网络中所有权重的梯度的所有导数链。下一步是用 Python 实现它。

Python 实现

下面列出了用于实现 GD 算法的 Python 脚本,该算法用于优化具有 3 个输入和 5 个神经元的隐藏层的 ANN。我们将讨论这段代码的每一部分。

import numpy

def sigmoid(sop):
    return 1.0 / (1 + numpy.exp(-1 * sop))

def error(predicted, target):
    return numpy.power(predicted - target, 2)

def error_predicted_deriv(predicted, target):
    return 2 * (predicted - target)

def sigmoid_sop_deriv(sop):
    return sigmoid(sop) * (1.0 - sigmoid(sop))

def sop_w_deriv(x):
    return x

def update_w(w, grad, learning_rate):
    return w - learning_rate * grad

x = numpy.array([0.1, 0.4, 4.1])
target = numpy.array([0.2])

learning_rate = 0.001

w1_3 = numpy.random.rand(3)
w2_3 = numpy.random.rand(3)
w3_3 = numpy.random.rand(3)
w4_3 = numpy.random.rand(3)
w5_3 = numpy.random.rand(3)
w6_5 = numpy.random.rand(5)
w6_5_old = w6_5
print("Initial W : ", w1_3, w2_3, w3_3, w4_3, w5_3, w6_5)

# Forward Pass
# Hidden Layer Calculations
sop1 = numpy.sum(w1_3 * x)
sop2 = numpy.sum(w2_3 * x)
sop3 = numpy.sum(w3_3 * x)
sop4 = numpy.sum(w4_3 * x)
sop5 = numpy.sum(w5_3 * x)

sig1 = sigmoid(sop1)
sig2 = sigmoid(sop2)
sig3 = sigmoid(sop3)
sig4 = sigmoid(sop4)
sig5 = sigmoid(sop5)

# Output Layer Calculations
sop_output = numpy.sum(w6_5 * numpy.array([sig1, sig2, sig3, sig4, sig5]))

predicted = sigmoid(sop_output)
err = error(predicted, target)

# Backward Pass
g1 = error_predicted_deriv(predicted, target)

### Working with weights between hidden and output layer
g2 = sigmoid_sop_deriv(sop_output)

g3 = numpy.zeros(w6_5.shape[0])
g3[0] = sop_w_deriv(sig1)
g3[1] = sop_w_deriv(sig2)
g3[2] = sop_w_deriv(sig3)
g3[3] = sop_w_deriv(sig4)
g3[4] = sop_w_deriv(sig5)

grad_hidden_output = g3 * g2 * g1

w6_5[0] = update_w(w6_5[0], grad_hidden_output[0], learning_rate)
w6_5[1] = update_w(w6_5[1], grad_hidden_output[1], learning_rate)
w6_5[2] = update_w(w6_5[2], grad_hidden_output[2], learning_rate)
w6_5[3] = update_w(w6_5[3], grad_hidden_output[3], learning_rate)
w6_5[4] = update_w(w6_5[4], grad_hidden_output[4], learning_rate)

### Working with weights between input and hidden layer
# First Hidden Neuron
g3 = sop_w_deriv(w6_5_old[0])
g4 = sigmoid_sop_deriv(sop1)
g5 = sop_w_deriv(x)
grad_hidden1_input = g5 * g4 * g3 * g2 * g1
w1_3 = update_w(w1_3, grad_hidden1_input, learning_rate)

# Second Hidden Neuron
g3 = sop_w_deriv(w6_5_old[1])
g4 = sigmoid_sop_deriv(sop2)
g5 = sop_w_deriv(x)
grad_hidden2_input = g5 * g4 * g3 * g2 * g1
w2_3 = update_w(w2_3, grad_hidden2_input, learning_rate)

# Third Hidden Neuron
g3 = sop_w_deriv(w6_5_old[2])
g4 = sigmoid_sop_deriv(sop3)
g5 = sop_w_deriv(x)
grad_hidden3_input = g5 * g4 * g3 * g2 * g1
w3_3 = update_w(w3_3, grad_hidden3_input, learning_rate)

# Fourth Hidden Neuron
g3 = sop_w_deriv(w6_5_old[3])
g4 = sigmoid_sop_deriv(sop4)
g5 = sop_w_deriv(x)
grad_hidden4_input = g5 * g4 * g3 * g2 * g1
w4_3 = update_w(w4_3, grad_hidden4_input, learning_rate)

# Fourth Hidden Neuron
g3 = sop_w_deriv(w6_5_old[4])
g4 = sigmoid_sop_deriv(sop5)
g5 = sop_w_deriv(x)
grad_hidden5_input = g5 * g4 * g3 * g2 * g1
w5_3 = update_w(w5_3, grad_hidden5_input, learning_rate)

w6_5_old = w6_5
print(predicted)

根据下面的代码行,准备输入及其输出是这段代码中要做的第一件事。因为输入层有 3 个输入,所以只存在一个有 3 个值的数组。它实际上不是一个数组,而是一个向量。目标被指定为单个值。

x = numpy.array([0.1, 0.4, 4.1])
target = numpy.array([0.2])

下一步是准备网络权重,如下所示。每个隐藏神经元的权重在一个单独的变量中创建。例如,第一个隐藏神经元的权重存储在 w1_3 变量中。变量 w6_5 保存将 5 个隐藏神经元连接到输出神经元的 5 个权重。

w1_3 = numpy.random.rand(3)
w2_3 = numpy.random.rand(3)
w3_3 = numpy.random.rand(3)
w4_3 = numpy.random.rand(3)
w5_3 = numpy.random.rand(3)
w6_5 = numpy.random.rand(5)

变量 w6_5_old 保存 w6_5 变量中的权重,作为计算 SOP6 到 Activ1-Activ5 导数时使用的备份。

w6_5_old = w6_5

准备好输入、输出和权重后,下一步是开始向前传递。第一个任务是计算每个隐藏神经元的 SOP,如下所示。这是通过将 3 个输入乘以 3 个权重来实现的。

# Forward Pass
# Hidden Layer Calculations
sop1 = numpy.sum(w1_3 * x)
sop2 = numpy.sum(w2_3 * x)
sop3 = numpy.sum(w3_3 * x)
sop4 = numpy.sum(w4_3 * x)
sop5 = numpy.sum(w5_3 * x)

之后,sigmoid 函数应用于所有这些乘积的和。

sig1 = sigmoid(sop1)
sig2 = sigmoid(sop2)
sig3 = sigmoid(sop3)
sig4 = sigmoid(sop4)
sig5 = sigmoid(sop5)

sigmoid 函数的输出被视为输出神经元的输入。这种神经元的 SOP 使用下面的线计算。

# Output Layer Calculations
sop_output = numpy.sum(w6_5 * numpy.array([sig1, sig2, sig3, sig4, sig5]))

输出神经元的 SOP 被馈送到 sigmoid 函数,以返回预测输出。计算出预测输出后,接下来使用 error() 函数计算误差。误差计算是正向传递的最后一步。接下来是开始向后传球。

predicted = sigmoid(sop_output)
err = error(predicted, target)

在后向传递中,根据下面的线,计算的一阶导数是预测输出导数的误差。结果保存在变量 g1 中以备后用。

g1 = error_predicted_deriv(predicted, target)

下一个导数是根据下一行对 SOP6 导数的预测输出。结果保存在变量 g2 中以备后用。

g2 = sigmoid_sop_deriv(sop_output)

为了计算隐藏层和输出层之间的权重梯度,剩余的导数是 SOP6 到 W41-W45 的导数。它们在变量 g3 中根据下一行进行计算。

g3 = numpy.zeros(w6_5.shape[0])
g3[0] = sop_w_deriv(sig1)
g3[1] = sop_w_deriv(sig2)
g3[2] = sop_w_deriv(sig3)
g3[3] = sop_w_deriv(sig4)
g3[4] = sop_w_deriv(sig5)

在准备了计算权重 W41 至 W45 的梯度所需的所有导数之后,接下来是使用下一条线来计算梯度。

grad_hidden_output = g3 * g2 * g1

之后,可以使用下面给出的 update_w() 函数来更新这 5 个权重。它接受旧的权重、梯度和学习率,并返回新的权重。

w6_5 = update_w(w6_5, grad_hidden_output, learning_rate)

更新隐藏层和输出层之间的权重后,接下来是计算输入层和隐藏层之间的权重梯度。通过我们的讨论,我们将一次研究一个隐藏的神经元。

对于第一个隐藏神经元,为其权重准备梯度所需的计算如下所示。在变量 g3 中,计算 SOP6Activ1 的导数。在 g4 中,计算 Activ1SOP1 的导数。最后的导数是保存在 g5 变量中的 SOP1W11-W31 导数。注意 g5 有 3 个导数,每个重量一个,而 g4g3 只有一个导数。

在计算链中的所有导数之后,接下来是通过乘以变量 g1 至 g5 来计算梯度,用于更新将 3 个输入神经元连接到第一个隐藏神经元的 3 个权重。结果保存在 grad_hidden1_input 变量中。最后,使用 update_w() 函数更新 3 个权重。

# First Hidden Neuron
g3 = sop_w_deriv(w6_5_old[0])
g4 = sigmoid_sop_deriv(sop1)
g5 = sop_w_deriv(x)
grad_hidden1_input = g5 * g4 * g3 * g2 * g1
w1_3 = update_w(w1_3, grad_hidden1_input, learning_rate)

对其他隐藏神经元的处理与上面的代码非常相似。从上面的 5 行来看,只需对前 2 行进行修改。对于第二个隐藏神经元的工作,使用索引 1 为 w6_5_old 计算 g3 。计算 g4 时,使用 sop2 而不是 sop1 。下面列出了负责更新第二个隐藏神经元的权重的代码部分。

# Second Hidden Neuron
g3 = sop_w_deriv(w6_5_old[1])
g4 = sigmoid_sop_deriv(sop2)
g5 = sop_w_deriv(x)
grad_hidden2_input = g5 * g4 * g3 * g2 * g1
w2_3 = update_w(w2_3, grad_hidden2_input, learning_rate)

对于第三个隐藏神经元,使用索引 2 来计算 g3w6_5_old 。为了计算 g4 ,使用 sop3 。其代码如下。

# Third Hidden Neuron
g3 = sop_w_deriv(w6_5_old[2])
g4 = sigmoid_sop_deriv(sop3)
g5 = sop_w_deriv(x)
grad_hidden3_input = g5 * g4 * g3 * g2 * g1
w3_3 = update_w(w3_3, grad_hidden3_input, learning_rate)

对于第四个隐藏神经元,使用索引 3 来计算 g3w6_5_old 。为了计算 g4 ,使用 sop4 。其代码如下。

# Fourth Hidden Neuron
g3 = sop_w_deriv(w6_5_old[3])
g4 = sigmoid_sop_deriv(sop4)
g5 = sop_w_deriv(x)
grad_hidden4_input = g5 * g4 * g3 * g2 * g1
w4_3 = update_w(w4_3, grad_hidden4_input, learning_rate)

对于第五个也是最后一个隐藏神经元,使用索引 4 为 w6_5_old 计算 g3 。为了计算 g4 ,使用 sop5 。其代码如下。

# Fifth Hidden Neuron
g3 = sop_w_deriv(w6_5_old[4])
g4 = sigmoid_sop_deriv(sop5)
g5 = sop_w_deriv(x)
grad_hidden5_input = g5 * g4 * g3 * g2 * g1
w5_3 = update_w(w5_3, grad_hidden5_input, learning_rate)

此时,计算所有网络权重的梯度,并更新权重。只要记得在最后将 w6_5_old 变量设置为新的 w6_5 即可。

w6_5_old = w6_5

在为正在使用的架构实现了 GD 算法之后,我们可以允许使用一个循环在多次迭代中应用该算法。这在下面列出的代码中实现。

import numpy

def sigmoid(sop):
    return 1.0/(1+numpy.exp(-1*sop))

def error(predicted, target):
    return numpy.power(predicted-target, 2)

def error_predicted_deriv(predicted, target):
    return 2*(predicted-target)

def sigmoid_sop_deriv(sop):
    return sigmoid(sop)*(1.0-sigmoid(sop))

def sop_w_deriv(x):
    return x

def update_w(w, grad, learning_rate):
    return w - learning_rate*grad

x = numpy.array([0.1, 0.4, 4.1])
target = numpy.array([0.2])

learning_rate = 0.001

w1_3 = numpy.random.rand(3)
w2_3 = numpy.random.rand(3)
w3_3 = numpy.random.rand(3)
w4_3 = numpy.random.rand(3)
w5_3 = numpy.random.rand(3)
w6_5 = numpy.random.rand(5)
w6_5_old = w6_5
print("Initial W : ", w1_3, w2_3, w3_3, w4_3, w5_3, w6_5)

for k in range(80000):
    # Forward Pass
    # Hidden Layer Calculations
    sop1 = numpy.sum(w1_3*x)
    sop2 = numpy.sum(w2_3*x)
    sop3 = numpy.sum(w3_3*x)
    sop4 = numpy.sum(w4_3*x)
    sop5 = numpy.sum(w5_3*x)

    sig1 = sigmoid(sop1)
    sig2 = sigmoid(sop2)
    sig3 = sigmoid(sop3)
    sig4 = sigmoid(sop4)
    sig5 = sigmoid(sop5)

    # Output Layer Calculations
    sop_output = numpy.sum(w6_5*numpy.array([sig1, sig2, sig3, sig4, sig5]))

    predicted = sigmoid(sop_output)
    err = error(predicted, target)

    # Backward Pass
    g1 = error_predicted_deriv(predicted, target)

    ### Working with weights between hidden and output layer
    g2 = sigmoid_sop_deriv(sop_output)

    g3 = numpy.zeros(w6_5.shape[0])
    g3[0] = sop_w_deriv(sig1)
    g3[1] = sop_w_deriv(sig2)
    g3[2] = sop_w_deriv(sig3)
    g3[3] = sop_w_deriv(sig4)
    g3[4] = sop_w_deriv(sig5)

    grad_hidden_output = g3*g2*g1

    w6_5 = update_w(w6_5, grad_hidden_output, learning_rate)

    ### Working with weights between input and hidden layer
    # First Hidden Neuron
    g3 = sop_w_deriv(w6_5_old[0])
    g4 = sigmoid_sop_deriv(sop1)
    g5 = sop_w_deriv(x)
    grad_hidden1_input = g5*g4*g3*g2*g1
    w1_3 = update_w(w1_3, grad_hidden1_input, learning_rate)

    # Second Hidden Neuron
    g3 = sop_w_deriv(w6_5_old[1])
    g4 = sigmoid_sop_deriv(sop2)
    g5 = sop_w_deriv(x)
    grad_hidden2_input = g5*g4*g3*g2*g1
    w2_3 = update_w(w2_3, grad_hidden2_input, learning_rate)

    # Third Hidden Neuron
    g3 = sop_w_deriv(w6_5_old[2])
    g4 = sigmoid_sop_deriv(sop3)
    g5 = sop_w_deriv(x)
    grad_hidden3_input = g5*g4*g3*g2*g1
    w3_3 = update_w(w3_3, grad_hidden3_input, learning_rate)

    # Fourth Hidden Neuron
    g3 = sop_w_deriv(w6_5_old[3])
    g4 = sigmoid_sop_deriv(sop4)
    g5 = sop_w_deriv(x)
    grad_hidden4_input = g5*g4*g3*g2*g1
    w4_3 = update_w(w4_3, grad_hidden4_input, learning_rate)

    # Fifth Hidden Neuron
    g3 = sop_w_deriv(w6_5_old[4])
    g4 = sigmoid_sop_deriv(sop5)
    g5 = sop_w_deriv(x)
    grad_hidden5_input = g5*g4*g3*g2*g1
    w5_3 = update_w(w5_3, grad_hidden5_input, learning_rate)

    w6_5_old = w6_5
    print(predicted)

下图显示了预测输出与每次迭代之间的关系。

下图给出了误差和迭代之间的关系。

之前的 GD 算法实现不仅适用于单个隐藏层,还适用于该层中特定数量的神经元。为了推广该算法,我们可以继续编辑之前的实现,以便它可以在单个隐藏层内对任意数量的神经元起作用。随后,可以添加更多的隐藏层,并且该算法将不依赖于固定数量的隐藏层。

步骤 2:使用任意数量的隐藏神经元

根据前面的实现,每个神经元的计算几乎相同。使用了相同的代码,但只是输入了适当的输入。使用循环,我们可以编写一次这样的代码,并在每次迭代中使用不同的输入。新代码如下所示。

import numpy

def sigmoid(sop):
    return 1.0/(1+numpy.exp(-1*sop))

def error(predicted, target):
    return numpy.power(predicted-target, 2)

def error_predicted_deriv(predicted, target):
    return 2*(predicted-target)

def sigmoid_sop_deriv(sop):
    return sigmoid(sop)*(1.0-sigmoid(sop))

def sop_w_deriv(x):
    return x

def update_w(w, grad, learning_rate):
    return w - learning_rate*grad

x = numpy.array([0.1, 0.4, 4.1])
target = numpy.array([0.2])

learning_rate = 0.001

# Number of inputs, number of neurons per each hidden layer, number of output neurons
network_architecture = numpy.array([x.shape[0], 5, 1])

# Initializing the weights of the entire network
w = []
w_temp = []
for layer_counter in numpy.arange(network_architecture.shape[0]-1):
    for neuron_nounter in numpy.arange(network_architecture[layer_counter+1]):
        w_temp.append(numpy.random.rand(network_architecture[layer_counter]))
    w.append(numpy.array(w_temp))
    w_temp = []
w = numpy.array(w)
w_old = w
print("Initial W : ", w)

for k in range(10000000000000):
    # Forward Pass
    # Hidden Layer Calculations
    sop_hidden = numpy.matmul(w[0], x)

    sig_hidden = sigmoid(sop_hidden)

    # Output Layer Calculations
    sop_output = numpy.sum(w[1][0]*sig_hidden)

    predicted = sigmoid(sop_output)
    err = error(predicted, target)

    # Backward Pass
    g1 = error_predicted_deriv(predicted, target)

    ### Working with weights between hidden and output layer
    g2 = sigmoid_sop_deriv(sop_output)
    g3 = sop_w_deriv(sig_hidden)
    grad_hidden_output = g3*g2*g1
    w[1][0] = update_w(w[1][0], grad_hidden_output, learning_rate)

    ### Working with weights between input and hidden layer
    g5 = sop_w_deriv(x)
    for neuron_idx in numpy.arange(w[0].shape[0]):
        g3 = sop_w_deriv(w_old[1][0][neuron_idx])
        g4 = sigmoid_sop_deriv(sop_hidden[neuron_idx])
        grad_hidden_input = g5*g4*g3*g2*g1
        w[0][neuron_idx] = update_w(w[0][neuron_idx], grad_hidden_input, learning_rate)

    w_old = w
    print(predicted)

如前所述,输入和目标被指定。有一个名为 network_architecture 的变量用于保存 ANN 架构。对于所使用的架构,输入的数量等于 x.shape[0],在本例中为 3,隐藏神经元的数量为 5,输出神经元的数量为 1。

network_architecture = numpy.array([x.shape[0], 5, 1])

使用遍历架构中指定的每一层的 for 循环,可以在名为 w 的单个数组中创建网络的权重。代码如下所示。与使用单个变量保存每个单独图层的权重相比,这是一种更好的构建网络权重的方法。

# Initializing the weights of the entire network
w = []
w_temp = []
for layer_counter in numpy.arange(network_architecture.shape[0]-1):
    for neuron_nounter in numpy.arange(network_architecture[layer_counter+1]):
        w_temp.append(numpy.random.rand(network_architecture[layer_counter]))
    w.append(numpy.array(w_temp))
    w_temp = []
w = numpy.array(w)

对于这个例子,数组 w 的形状是(2),这意味着其中只有 2 个元素。第一个元素的形状是(5,3),它保存有 3 个输入的输入层和有 5 个神经元的隐藏层之间的权重。数组 w 中第二个元素的形状是(1,5 ),它保存了具有 5 个神经元的隐藏层和只有一个神经元的输出层之间的权重。

以这种方式准备重量有利于向前和向后传球。所有乘积的总和都是使用一条直线计算的,如下所示。请注意,w[0]表示输入层和隐藏层之间的权重。

sop_hidden = numpy.matmul(w[0], x)

类似地,sigmoid 函数被调用一次,以应用于所有乘积和,如下所示。

sig_hidden = sigmoid(sop_hidden)

隐藏层和输出层之间的乘积之和是根据这条单线计算的。请注意,w[1]返回这两个层之间的权重。

sop_output = numpy.sum(w[1][0]*sig_hidden)

通常,预测输出和误差计算如下。

predicted = sigmoid(sop_output)
err = error(predicted, target)

这是向前传球的终点。在后向传递中,因为在输出层中只有一个神经元,所以它的权重将以先前使用的相同方式更新。

# Backward Pass
g1 = error_predicted_deriv(predicted, target)

### Working with weights between hidden and output layer
g2 = sigmoid_sop_deriv(sop_output)
g3 = sop_w_deriv(sig_hidden)
grad_hidden_output = g3*g2*g1
w[1][0] = update_w(w[1][0], grad_hidden_output, learning_rate)

当更新输入层和隐藏层之间的权重时,使用下面给出的循环的**。它遍历隐藏层中的每个神经元,并将适当的输入用于函数 sop_w_deriv()sigmoid_sop_deriv() 。**

### Working with weights between input and hidden layer
g5 = sop_w_deriv(x)
for neuron_idx in numpy.arange(w[0].shape[0]):
    g3 = sop_w_deriv(w_old[1][0][neuron_idx])
    g4 = sigmoid_sop_deriv(sop_hidden[neuron_idx])
    grad_hidden_input = g5*g4*g3*g2*g1
    w[0][neuron_idx] = update_w(w[0][neuron_idx], grad_hidden_input, learning_rate)

通过这样做,我们成功地最小化了 GD 算法代码,并将其推广到单个隐藏层中的任意数量的隐藏神经元。在用不同数量的隐藏神经元测试代码之前,让我们确保它像前面的实现一样正确工作。下图显示了预测的输出如何随着迭代而变化。它与先前获得的结果相同,这意味着实现是正确的。

下图显示了误差是如何随着迭代而变化的,这也与上一个实现中的情况相同。

在确保代码正确工作后,下一步是使用不同数量的隐藏神经元。唯一需要的改变是在 network_architecture 变量中指定所需的隐藏神经元数量。下一个代码使用了 8 个隐藏的神经元。

import numpy
import matplotlib.pyplot

def sigmoid(sop):
    return 1.0/(1+numpy.exp(-1*sop))

def error(predicted, target):
    return numpy.power(predicted-target, 2)

def error_predicted_deriv(predicted, target):
    return 2*(predicted-target)

def sigmoid_sop_deriv(sop):
    return sigmoid(sop)*(1.0-sigmoid(sop))

def sop_w_deriv(x):
    return x

def update_w(w, grad, learning_rate):
    return w - learning_rate*grad

x = numpy.array([0.1, 0.4, 4.1])
target = numpy.array([0.2])

learning_rate = 0.001

# Number of inputs, number of neurons per each hidden layer, number of output neurons
network_architecture = numpy.array([x.shape[0], 8, 1])

# Initializing the weights of the entire network
w = []
w_temp = []
for layer_counter in numpy.arange(network_architecture.shape[0]-1):
    for neuron_nounter in numpy.arange(network_architecture[layer_counter+1]):
        w_temp.append(numpy.random.rand(network_architecture[layer_counter]))
    w.append(numpy.array(w_temp))
    w_temp = []
w = numpy.array(w)
w_old = w
print("Initial W : ", w)

for k in range(80000):
    # Forward Pass
    # Hidden Layer Calculations
    sop_hidden = numpy.matmul(w[0], x)

    sig_hidden = sigmoid(sop_hidden)

    # Output Layer Calculations
    sop_output = numpy.sum(w[1][0]*sig_hidden)

    predicted = sigmoid(sop_output)
    err = error(predicted, target)

    # Backward Pass
    g1 = error_predicted_deriv(predicted, target)

    ### Working with weights between hidden and output layer
    g2 = sigmoid_sop_deriv(sop_output)
    g3 = sop_w_deriv(sig_hidden)
    grad_hidden_output = g3*g2*g1
    w[1][0] = update_w(w[1][0], grad_hidden_output, learning_rate)

    ### Working with weights between input and hidden layer
    g5 = sop_w_deriv(x)
    for neuron_idx in numpy.arange(w[0].shape[0]):
        g3 = sop_w_deriv(w_old[1][0][neuron_idx])
        g4 = sigmoid_sop_deriv(sop_hidden[neuron_idx])
        grad_hidden_input = g5*g4*g3*g2*g1
        w[0][neuron_idx] = update_w(w[0][neuron_idx], grad_hidden_input, learning_rate)

    w_old = w
    print(predicted)

下图显示了预测输出和迭代次数之间的关系,证明了 GD 算法能够成功地训练 ANN。

下图给出了误差和迭代次数之间的关系。

结论

到本系列的这一部分结束时,我们已经成功地实现了 GD 算法,可以在单个隐藏层中处理可变数量的隐藏神经元。它也可以接受可变数量的输入。在下一部分中,实现将被扩展,以允许 GD 算法处理不止一个隐藏层。

使用 Pix4D 和 Core 进行摄影测量

原文:https://blog.paperspace.com/photogrammetry-gpu-cloud-pix4d/

在测绘中使用摄影来测量物体之间的距离的概念被称为摄影测量。这是通过从不同角度和距离拍摄的一系列照片中观察同一物体来实现的。通过对一些物体上的这些距离进行三角测量,有可能创建物体、土地或空间的三维表示,然后我们可以利用 3D 模型进行大量不同的任务,如进行测量。这个过程在许多不同的用例中被大量利用,如土地测量、医学或体育。例如,对一块土地进行航空摄影测量可以极大地提高工程师、建筑师和施工团队对地块工作做出明智决策的能力。

毫无疑问,最受欢迎的摄影测量软件是 Pix4D,它针对航空摄影测量进行了优化,使用无人机拍摄的照片用于各种目的,如土地测量、建筑和农业规划。Pix4D 的主要产品是 Pix4Dmapper 和 Pix4Dmatic。Pix4Dmapper 是专业无人机测绘的主要软件,Pix4Dmatic 旨在更大规模的项目中做同样的工作。在这篇博客文章中,我们将先看一下 Pix4Dmapper ,然后深入了解一下 Pix4Dmatic 。然后,我们将学习如何在 Paperspace Core 上设置 Pix4Dmatic 来处理样本数据集。

让我们看看如何使用强大的 GPU 来处理摄影测量任务所需的大量数据。

为什么要使用摄影测量?

Pix4D 可以说是帮助大规模土地勘测和无人机测绘的首要服务,但这项技术的实际功能用例是什么?

Pix4Dmapper 是 Pix4D 的较小规模无人机测绘软件。使用 Pix4Dmapper,可以用任何相机或无人机捕捉 RGB、热或多光谱图像,在应用程序中将地面或空中图像数字化为可用的 3D 和数字表示,然后使用这些表示创建感兴趣数据的精确测量和检查。然后,这些测量值可用于许多任务,如指导施工、土地测量等。然而,Pix4Dmapper 在快速处理多少数据方面受到限制。

Source

Pix4Dmatic 是 Pix4D 处理大数据的解决方案。它支持下一代陆地、走廊和大比例制图的摄影测量。Pix4Dmatic 在其他方面的功能与 Pix4Dmapper 非常相似,但在摄影测量的处理步骤上有显著的加速。因为我们可以访问大型示例数据集和核心 GPU,所以我们将在本教程中使用 Pix4Dmatic。

设置:核心

在开始之前,在 Paperspace 上加载一个 Windows 核心机器。我建议您选择 Ampere 机器类型(A4000、A5000、A6000 和 A100)中的一种,如果它们可用的话,因为运行 Pix4D 的计算成本非常高。建议至少有 32 GB(20 MP 时有 2,000-5,000 个图像)到 64 GB(20mp 时有 5,000-10,000 个图像)的 CPU RAM,以及至少 100 GB - 200 GB 的可用内存空间(20mp 时有 2,000-5,000 个图像)。您可以在“机器类型”部分的“核心机器创建”页面中更改可用的存储量。

一旦你完成了这些步骤,点击底部的“创建”来创建核心机器。然后,单击 GUI 中的“Launch”按钮访问您的 Windows 桌面。

设置:Pix4D

现在,我们在我们的核心机器里。我们需要做的第一件事是用 Pix4D 创建一个帐户并下载软件。Pix4D 是一个商业产品,因此您需要注册一个付费帐户来使用该软件套件。但是,如果您愿意,您可以获得 15 天的免费试用期来演示他们的产品。

要开始创建您的帐户,请转到该页面,向下滚动到标有 Pix4Dmatic 的部分,如果您还没有帐户,请单击“开始 15 天试用”。按照页面上的说明完成帐户设置。

https://blog.paperspace.com/content/media/2022/06/pix4d--2.mp4

现在您已经登录并拥有了许可证,请再次转到 Pix4Dmatic 页面,并下载该应用程序。运行安装程序以完成设置。接下来,我们需要确保 Pix4DMatic 使用我们提供的 GPU。为此,首先通过单击屏幕左下角的 Windows 按钮进入系统设置,然后单击“齿轮”符号进入设置。从窗口中,搜索“图形设置”并导航到该页面。从那里,转到“添加应用程序”部分,单击“浏览”,然后在 Program Files/Pix4dmatic 中找到您的“pix4dmatic.exe”。添加到自定义图形设置菜单图形用户界面与电子选择“高性能”,以确保您的应用程序利用您的 GPU。

设置我们的演示需要完成的第二步是获取一些用于示例项目的示例数据。幸运的是,Pix4D 为它的用户提供了一个强大的样本数据集和项目选择,我们可以利用它来达到这个目的。打开核心机中的链接,滚动到 Pix4Dmatic 部分。对于今天的例子,我们将下载并使用城市区域数据集。城市区域数据集是最初更大的 10615 图像数据集的 100 图像子集,并且“整个数据集的图像采集是使用 4 架同时飞行的 eBee X - senseFly 无人机完成的”【来源】。下载并解压缩数据集。

载入数据

现在我们已经有了数据和软件设置,我们可以开始我们的摄影测量任务。加载应用程序,并单击 File > New 在应用程序中启动一个新项目。您可以将路径保留为默认文件夹,但最好重命名项目以代表作品的示例性质。

https://blog.paperspace.com/content/media/2022/06/ezgif.com-gif-maker--2-.mp4

现在,在您的新项目中,我们可以通过简单地将包含数据集 zip 文件内容的文件拖放到窗口中来加载我们的数据。这将自动用按地理位置组织的 100 幅图像填充地图特征空间。

https://blog.paperspace.com/content/media/2022/06/samp.mp4

接下来,我们有一个分配地面控制点(GCP)的可选步骤。“控制点是坐标已知的特征点。他们的坐标是通过传统的测量方法或其他来源(激光雷达、该地区的旧地图或网络地图服务)获得的。GCP 用于对项目进行地理参考并减少噪音。”【Source】虽然它们对于摄影测量任务的成功不是绝对必要的,但对于提高我们输出对象的质量却非常有用。

处理数据

对于下一步,请确保您已激活 Pix4Dmatic 的许可证。否则,只有前两个处理选项可用,您将无法输出报告。

要处理我们现在上传和设置的城市区域数据集,请选择屏幕右上角带有播放按钮的按钮。这将打开处理菜单。在顶部,您可以更改处理模板,以反映不同类型的无人机飞行和使用的图像捕捉模式。在这种情况下,我们将使用默认的“最低点”。

然后,它给你 5 个处理选项:校准,增密,网格,DSM 和正射镶嵌。您可以单击其中每一项的切换按钮,将它们添加到我们的流程工作流中。让我们来讨论一下它们各自的功能:

校准

“校准处理”选项有助于为其他处理选项准备 Pix4Dmatic 数据,并通过进一步优化数据为未来运行服务。在实践中,这意味着分配校准模板以决定将使用什么处理选项,分配管道以允许用户控制相机的内部和外部参数,分配图像比例以定义用于提取关键点的图像大小,分配设置以确定提取的关键点的数量。和内部置信度度量来设置初始摄像机校准的置信度。置信度决定了摄像机在校准过程中可以重新校准和调整的程度。

增密

The dense point cloud for the urban area dataset

增密处理步骤根据提交的数据创建密集点云。密集点云是 3D 对象建模的常见起点,并且由关于特征空间中许多点的位置的一系列测量来表示。在摄影测量任务中,通过测量图像表面上的许多点来计算它们,然后使用这些距离来推断物体彼此相对位置的三维理解。

网格

The image mesh for the urban area dataset

在 3D 计算机图形和实体建模中,多边形网格是定义多面体对象形状的顶点、边和面的集合。在网格处理阶段,Pix4Dmatic 生成并提取特征空间中对象的网格表示。这个网格(一个. obj 文件)然后可以在许多第三方应用程序中使用,如 SculptGL 或 Blender。

数字表面模型

dsm.jpg

A sample DSM from Pix4D - Source

DSM 步骤将密集点云作为输入,用于定义分辨率、启用表面平滑和启用插值以创建数字表面模型(DSM)。DSM 被标记为感兴趣区域的 2.5 D 模型。它可以导出为两种不同类型的文件:栅格地理标记或点云(。xyz,。las,。laz)。栅格 geotiff 文件的每个像素和点云中的每个点都包含三维(X,Y,Z)的位移信息,但它们没有第四维来表示捕获的颜色。对于每个(X,Y)位置,DSM 只有 1 个 Z 值(该(X,Y)位置最高点的高度)。

正镶嵌

The orthomosaic for the urban area dataset.

正射镶嵌是所采集影像的 2D 表示法,其中包含有关映射区域的 X、Y 和颜色值的信息。它不同于照相拼接,因为正射校正方法使用 DSM 从图像中移除透视变形。然而,这需要大量的匹配/关键点(超过 1000 个)来生成模型。实际上,正射镶嵌会校正相机的视角,并根据对象/地面的每个点与相机的距离来增强制图表达以使用不同的比例。这使得正射镶嵌在测量中非常有用,因为物体是根据拍摄的物体彼此之间的相对距离和大小来创建的。

把所有的放在一起

在单击 start 运行处理之前,请注意在屏幕底部还有一个标题为“exports”的附加部分。这些输出将决定我们之后可以检查的内容。如果你需要一个输出。例如,obj 文件或 DSM GeoTIFF,确保选择那些框。否则,您现在可以只输出质量报告,然后单击“开始”

https://blog.paperspace.com/content/media/2022/06/ortho4.mp4

处理后,我们可以开始查看我们新创建的城市区域数据集的 3D 表示。通过这样做,我们可以更好地理解这个模型是如何创建的。通过点击每个连接点,我们可以看到每个不同角度的“摄像机”是如何连接到这些点的。这些不同的视线中的每一条都为 Pix4D 提供了不同的信息来创建这个 3D 表示。

然而,上面的例子并不完美,当我们放大时,我们可以看到模型的大部分是黑色或空白的。原因有二:第一,即使有广泛的摄像机网络也不可能捕捉到每一点信息,第二,Pix4Dmatic 的能力与有多少台摄像机以及 GCP 连接点的标记有多好直接相关。通过单击窗口右上角的条形图符号,我们可以访问评估信息提取质量的报告。正如我们所见,在所有使用的图像中,平均 GCP 均方根误差为 0.36 米。这是一个相当大的错误,我们可以通过后退一步,更准确地标注 GCP 的所有连接点,来大幅改进这个模型的输出。

检查我们处理过的数据

现在,我们的 3D 表示已经处理完毕,可以开始使用提供的摄影测量工具来测量我们的图像。这是 Pix4D 摄影测量的真正用途。通过软件进行测量,可以节省亲自进行物理测量的时间和精力。它还允许像我们这样的人,在不同国家的计算机上,仅使用无人机和 Pix4D 的强大软件套件,在全球各地进行相同的工作。

https://blog.paperspace.com/content/media/2022/06/Screen-Recording-2022-06-16-at-5.32.29-PM.mp4

您可以通过选择菜单栏下方屏幕左上角的标尺图标进行简单的测量。然后,只需将鼠标拖过有问题的位置,即可测量两点之间的距离。

结束语

Pix4dmatic 是进行大规模摄影测量的一个非常强大的工具。用户可以期待该应用程序能够处理数百到数千个相机输入,以比 Pix4dmapper 更高的速度创建大型 3D 场景模型。如果你打算进行大规模的摄影测量,那么 Pix4dmatic 绝对是与你的核心机器一起运行的首选软件。

具有深度强化学习的物理控制任务

原文:https://blog.paperspace.com/physics-control-tasks-with-deep-reinforcement-learning/

在本教程中,我们将实现论文 深度强化学习的连续控制 ,该论文由 Google DeepMind 发布,并在 2016 年 ICRL 大会上作为会议论文提交。网络将在 PyTorch 中使用 OpenAI gym 实现。该算法结合了深度学习和强化学习技术来处理高维度,即连续的动作空间。

Deep-Q 学习算法的成功使 Google DeepMind 在玩 Atari 游戏时胜过人类之后,他们将同样的想法扩展到物理任务,其中的动作空间相对于前述游戏要大得多。事实上,在物理任务中,目标通常是使刚体学习某种运动,可以施加到致动器的动作是连续的,即它们可以在一个间隔内从最小值跨越到最大值。

人们可以简单地问:为什么我们不把行动空间离散化?

是的,我们可以,但考虑一个 3 自由度系统,其中每个动作,跨越其自己的区间,被离散化为,比方说,10 个值:动作空间将具有 1000 (10^3)的维度,这将导致两个大问题:维度的诅咒和连续控制任务的棘手方法,其中每个动作的 10 个样本的离散化不会导致良好的解决方案。考虑一个机械臂:一个致动器并不只有几个可用的扭矩/力值来产生旋转/平移操作的速度和加速度。

深度 Q 学习可以很好地处理高维状态空间(图像作为输入),但是它仍然不能处理高维动作空间(连续动作)。Deep-Q 学习的一个很好的例子就是实现了一个可以玩 Dino Run 的 AI,其中动作空间的集合简单来说就是:{ jumpget_down,do_nothing }。如果你想知道强化学习的基础和如何实现 Q-网络,前面提到的教程是一个很好的开始,如果你不熟悉强化学习的概念,我强烈建议你先浏览一下。

我们将涵盖的内容

在本教程中,我们将经历以下步骤:

  • 解释政策网络的概念
  • 在所谓的演员兼评论家架构中结合Q-网络政策网络
  • 查看参数是如何更新的,以便最大化和最小化性能目标功能
  • 整合内存缓冲冻结目标网络概念,了解在 DDPG 采取的探索策略是什么。
  • 使用 PyTorch 实现算法:在一些为连续控制任务创建的 OpenAI gym 环境上进行训练,比如 山地车连续 。更复杂的环境,如漏斗(使一条腿向前跳跃而不摔倒)和双倒立摆(通过沿水平轴施加力来保持摆的平衡)需要 MuJoCo 许可证,如果你有学术或机构联系,你必须购买或申请它。尽管如此,您可以申请 30 天的免费许可证。

DDPG 入门

作为概述,本文介绍的算法称为深度确定性策略梯度(DDPG)。它延续了之前成功的 DeepMind 论文使用深度强化学习经验重放缓冲 的概念玩雅达利,其中通过对经验批次进行采样来偏离策略地训练网络,以及 冻结目标网络 ,其中制作网络的副本,目的是在目标函数中使用,以避免复杂和非线性函数逼近器(如神经网络)的发散和不稳定性。

由于本教程的目的不是展示强化学习的基础知识,如果你不熟悉这些概念,我强烈建议你首先阅读前面提到的可以玩 Dino Run Paperspace 教程的 AI。一旦你熟悉了 环境代理奖励Q 值函数 (这是在复杂任务中由深度神经网络近似的函数,因此称为 Q 网络)的概念,你就准备好投入到更复杂的深度强化学习架构中去了,像行动者-批评家架构,它涉及到了【T2

简而言之,强化学习

强化学习是机器学习的一个子领域。它不同于经典的有监督和无监督学习范式,因为它是一种试错法。这意味着代理实际上不是在数据集上训练的,而是通过与环境交互来训练的,环境被认为是我们希望代理作用的整个系统(像游戏或机械臂)。线索点是环境必须提供一个奖励来回应代理人的行为。这种奖励是根据任务设计的,必须经过深思熟虑,因为它对整个学习过程至关重要。

试错法的基本要素是值函数,通过离散场景中的贝尔曼方程求解,其中我们有低维状态和动作空间。当我们处理高维状态空间或动作空间时,我们必须引入复杂的非线性函数逼近器,例如深度神经网络。为此,在文献中引入了深度强化学习的概念。

现在,让我们从简要描述 DDPG 带来的主要创新开始,这些创新用于在强化学习框架中处理连续的、因此是高维的动作空间。

DDPG 积木

政策网络

除了使用神经网络来参数化 Q 函数,就像发生在 DQN 身上的那样,在更复杂的演员-评论家架构(DDPG 的核心)中,它被称为“评论家”,我们还有政策网络,被称为“演员”。然后引入该神经网络来参数化策略函数。

策略基本上是代理行为,是从状态到动作的映射(在 确定性策略 的情况下)或动作的分配(在 随机策略 的情况下)。这两种策略的存在是因为它们适用于某些特定的任务:确定性策略非常适用于物理控制问题,而随机策略是解决博弈问题的一个很好的选择。

在这种情况下,策略网络的输出是对应于要对环境采取的动作的值。

目标和损失函数

我们有两个网络,因此要更新两组参数:策略网络的参数必须被更新,以便最大化在策略梯度定理中定义的性能测量***【J】***;同时更新 critic 网络的参数,以便最小化时间差损失 L

基本上,我们需要改进性能度量 J 以便遵循 Q 值函数的最大化,同时最小化时间差异损失,就像在玩 Atari 游戏的深度 Q 网络中发生的那样。

演员-评论家建筑

Actor 将状态作为输入,给出动作作为输出,而 critic 将状态和动作都作为输入,给出 Q 函数的值作为输出。评论家使用梯度时间差学习,而演员的参数学习遵循政策梯度定理。这种体系结构背后的主要思想是,策略网络动作产生动作,而 Q 网络批评该动作。

集成体验回放和冻结目标网络

与 Q 学习一样,使用非线性函数逼近器(如神经网络)意味着不再保证收敛,而神经网络是在大的状态空间上进行推广所必需的。由于这个原因,需要使用经验重放来制作独立且相同分布的样本。此外,需要使用冻结的目标网络,以便在更新 critic 网络时避免发散。与每 C 步更新一次目标网络的 DQN 不同,在 DDPG 的情况下,在“软”更新之后,在每个时间步更新目标网络的参数:

其中τ << 1,w 和θ分别是目标 Q 网络和目标策略网络的权重。通过“软”更新,目标网络的权重被限制为缓慢变化,从而提高了学习和收敛结果的稳定性。然后,在时间差异损失中使用目标网络,而不是 Q 网络本身。

探测

像 DDPG 这样的算法中的探索问题可以用一种非常简单的方式来解决,并且独立于学习算法。然后,通过将从噪声进程 N 采样的噪声添加到行动者策略来构建探索策略。勘探政策因此变成:

$\pi/center>(S[t]) = \(\pi\)(S[t], \(\theta\)) + \(\nu\)

其中$\nu$是一个奥恩斯坦-乌伦贝克过程,即一个随机过程,它能产生时间相关的动作,保证物理控制问题中的顺利探索。

DDPG 算法综述

连续控制问题:综述

我们现在来看看可以用来运行 DDPG 算法的一些环境。这些环境可以通过 gym 包获得,正如前面提到的,其中一些环境需要 MuJoCo(这是一个物理引擎)许可才能运行。我们将看看不需要 MuJoCo 的钟摆环境,以及需要 MuJoCo 的 Hopper 环境。

钟摆

任务概述

目的是在中央致动器上施加扭矩,以保持摆锤在垂直轴上的平衡。该问题具有三维状态空间,即角度的余弦和正弦以及角度的导数。动作空间是一维的,它是施加到关节上的力矩,被限制在$[-2,2]$。

奖励函数

奖励的精确等式:

-(theta^2 + 0.1*theta_dt^2 + 0.001*action^2) 

θ在-π和π之间归一化。所以成本最低的是-(pi^2 + 0.1*8^2 + 0.001*2^2) = -16.2736044,成本最高的是0。本质上,目标是保持零度角(垂直),最小的旋转速度和最少的努力。

有关钟摆环境的更多细节,请查看 GitHub 或 T2 的 open ai env 页面。

料斗

漏斗的任务是使一个有三个关节和四个身体部分的漏斗尽可能快地向前跳跃。健身房也有,但需要 MuJoCo 许可证,所以你必须申请并安装它,健身房才能工作。

任务的概述

这个问题有一个 11 维的状态向量,包括:位置(如果是旋转或棱柱关节,则以辐射或米为单位),位置的导数以及旋转关节角度相对于其相对参考系的正弦和余弦函数。动作空间对应于一个三维空间,其中每个动作都是一个连续值,其范围在$[1,1]\(。因此网络架构应该有 3 个输出神经元,具有***【tanh】***激活功能。这些扭矩施加在位于**大腿**关节、**腿**关节和**脚**关节的致动器上,这些动作的范围归一化为\)[1,1]$。

奖励函数

由于任务的目标是使料斗向前移动,因此定义奖励函数时考虑了活着的奖励、向前速度的正贡献(通过对每一步的位移求导来计算)以及动作控制空间中欧几里德范数的负贡献。

其中 a 是动作(即网络的输出), vx 是前进速度, b 是活着的奖励。当至少一个故障条件发生时,情节终止,它们是:

其中θ是物体的前倾角。

有关 Hopper 环境的更多详细信息,请查看 GitHubOpenAI env 页面。

其他可以玩的健身房环境

有几个健身房环境适合连续控制,因为它们有连续的行动空间。有些需要 MuJoCo,有些不需要。

其中不需要 MuJoCo 的,可以在月球着陆器两足步行器或者推车上试代码。请注意,赛车具有高维状态(图像像素),因此您不能使用与低维状态空间环境一起使用的全连接层,但也可以使用包含卷积层的架构。

代码实现

设置

在 Paperspace 上设置实例:

公共容器“Paperspace + Fast。“人工智能很适合我们的实验。

**配置:**打开终端安装健身房,升级火炬版本。

pip install gym 
pip install --upgrade torch 

实验将在“钟摆-v0”健身房环境下进行。一些环境需要 MuJoCo 许可证(“HalfCheetah-v1”、“Hopper-v1”、“Ant-v1”或“Humanoid-v1”),而其他环境需要 PyBox2d 才能运行(“LunarLanderContinuous-v2”、“CarRacing-v0”或“BipedalWalker-v2”)。

一旦你为你想玩的环境安装了 MuJoCo 或者 PyBox2d(《钟摆-v0》不需要这些中的任何一个,只需要 gym 包),你就可以打开一个 Jupyter 笔记本开始编码了。

常规设置

该配置遵循 DDPG 文件补充信息章节中描述的设置,您可以在第 11 页找到。

如文中所述,我们必须设置缓冲区大小为100 万个 条目,从内存中采样的批量大小等于 64 ,演员和评论家网络的学习速率分别等于 0.00010.001 ,用于软更新的 tau 参数等于 0.001

BUFFER_SIZE=1000000
BATCH_SIZE=64  #this can be 128 for more complex tasks such as Hopper
GAMMA=0.9
TAU=0.001       #Target Network HyperParameters
LRA=0.0001      #LEARNING RATE ACTOR
LRC=0.001       #LEARNING RATE CRITIC
H1=400   #neurons of 1st layers
H2=300   #neurons of 2nd layers

MAX_EPISODES=50000 #number of episodes of the training
MAX_STEPS=200    #max steps to finish an episode. An episode breaks early if some break conditions are met (like too much
                  #amplitude of the joints angles or if a failure occurs)
buffer_start = 100
epsilon = 1
epsilon_decay = 1./100000 #this is ok for a simple task like inverted pendulum, but maybe this would be set to zero for more
                     #complex tasks like Hopper; epsilon is a decay for the exploration and noise applied to the action is 
                     #weighted by this decay. In more complex tasks we need the exploration to not vanish so we set the decay
                     #to zero.
PRINT_EVERY = 10 #Print info about average reward every PRINT_EVERY

ENV_NAME = "Pendulum-v0" # For the hopper put "Hopper-v2" 
#check other environments to play with at https://gym.openai.com/envs/ 

体验回放缓冲区

使用优先体验回放会很有趣。你曾经和 DDPG 一起尝试过优先体验回放吗?如果你想分享你的结果,请留下你的评论。

不管怎样,下面是一个没有优先级的简单重放缓冲区的实现。

from collections import deque
import random
import numpy as np

class replayBuffer(object):
    def __init__(self, buffer_size, name_buffer=''):
        self.buffer_size=buffer_size  #choose buffer size
        self.num_exp=0
        self.buffer=deque()

    def add(self, s, a, r, t, s2):
        experience=(s, a, r, t, s2)
        if self.num_exp < self.buffer_size:
            self.buffer.append(experience)
            self.num_exp +=1
        else:
            self.buffer.popleft()
            self.buffer.append(experience)

    def size(self):
        return self.buffer_size

    def count(self):
        return self.num_exp

    def sample(self, batch_size):
        if self.num_exp < batch_size:
            batch=random.sample(self.buffer, self.num_exp)
        else:
            batch=random.sample(self.buffer, batch_size)

        s, a, r, t, s2 = map(np.stack, zip(*batch))

        return s, a, r, t, s2

    def clear(self):
        self.buffer = deque()
        self.num_exp=0 

网络架构

我们在这里定义网络。该结构遵循论文的描述:actor 由三个完全连接的层组成,并具有双曲正切作为输出激活函数,以处理[-1,1]值范围。critic 将状态和动作作为输入,并在三个完全连接的层之后输出 Q 值。

def fanin_(size):
    fan_in = size[0]
    weight = 1./np.sqrt(fan_in)
    return torch.Tensor(size).uniform_(-weight, weight)

class Critic(nn.Module):
    def __init__(self, state_dim, action_dim, h1=H1, h2=H2, init_w=3e-3):
        super(Critic, self).__init__()

        self.linear1 = nn.Linear(state_dim, h1)
        self.linear1.weight.data = fanin_(self.linear1.weight.data.size())

        self.linear2 = nn.Linear(h1+action_dim, h2)
        self.linear2.weight.data = fanin_(self.linear2.weight.data.size())

        self.linear3 = nn.Linear(h2, 1)
        self.linear3.weight.data.uniform_(-init_w, init_w)

        self.relu = nn.ReLU()

    def forward(self, state, action):
        x = self.linear1(state)
        x = self.relu(x)
        x = self.linear2(torch.cat([x,action],1))

        x = self.relu(x)
        x = self.linear3(x)

        return x

class Actor(nn.Module): 
    def __init__(self, state_dim, action_dim, h1=H1, h2=H2, init_w=0.003):
        super(Actor, self).__init__()        
        self.linear1 = nn.Linear(state_dim, h1)
        self.linear1.weight.data = fanin_(self.linear1.weight.data.size())

        self.linear2 = nn.Linear(h1, h2)
        self.linear2.weight.data = fanin_(self.linear2.weight.data.size())

        self.linear3 = nn.Linear(h2, action_dim)
        self.linear3.weight.data.uniform_(-init_w, init_w)

        self.relu = nn.ReLU()
        self.tanh = nn.Tanh()

    def forward(self, state):
        x = self.linear1(state)
        x = self.relu(x)
        x = self.linear2(x)
        x = self.relu(x)
        x = self.linear3(x)
        x = self.tanh(x)
        return x

    def get_action(self, state):
        state  = torch.FloatTensor(state).unsqueeze(0).to(device)
        action = self.forward(state)
        return action.detach().cpu().numpy()[0] 

探索

正如论文中所描述的,为了保证探索,我们必须在动作中加入噪音。选择奥恩斯坦-乌伦贝克过程是因为它以平滑的方式添加噪声,适合于连续的控制任务。关于这个随机过程的更多细节在维基百科上有简单的描述。

# Based on http://math.stackexchange.com/questions/1287634/implementing-ornstein-uhlenbeck-in-matlab
class OrnsteinUhlenbeckActionNoise:
    def __init__(self, mu=0, sigma=0.2, theta=.15, dt=1e-2, x0=None):
        self.theta = theta
        self.mu = mu
        self.sigma = sigma
        self.dt = dt
        self.x0 = x0
        self.reset()

    def __call__(self):
        x = self.x_prev + self.theta * (self.mu - self.x_prev) * self.dt + self.sigma * np.sqrt(self.dt) * np.random.normal(size=self.mu.shape)
        self.x_prev = x
        return x

    def reset(self):
        self.x_prev = self.x0 if self.x0 is not None else np.zeros_like(self.mu)

    def __repr__(self):
        return 'OrnsteinUhlenbeckActionNoise(mu={}, sigma={})'.format(self.mu, self.sigma) 

设置培训

我们通过初始化环境、网络、目标网络、重放存储器和优化器来设置训练。

torch.manual_seed(-1)

env = NormalizedEnv(gym.make(ENV_NAME))

state_dim = env.observation_space.shape[0]
action_dim = env.action_space.shape[0]

print("State dim: {}, Action dim: {}".format(state_dim, action_dim))

noise = OrnsteinUhlenbeckActionNoise(mu=np.zeros(action_dim))

critic  = Critic(state_dim, action_dim).to(device)
actor = Actor(state_dim, action_dim).to(device)

target_critic  = Critic(state_dim, action_dim).to(device)
target_actor = Actor(state_dim, action_dim).to(device)

for target_param, param in zip(target_critic.parameters(), critic.parameters()):
    target_param.data.copy_(param.data)

for target_param, param in zip(target_actor.parameters(), actor.parameters()):
    target_param.data.copy_(param.data)

q_optimizer  = opt.Adam(critic.parameters(),  lr=LRC)
policy_optimizer = opt.Adam(actor.parameters(), lr=LRA)

MSE = nn.MSELoss()

memory = replayBuffer(BUFFER_SIZE)
writer = SummaryWriter() #initialise tensorboard writer 

遍历剧集

MAX _ EPISODES 和 MAX_STEPS 参数可以根据我们将要培训代理的环境类型进行调整。在单摆的情况下,我们没有每集的失败条件,所以它将总是通过每集的最大步骤;在存在失败条件的任务中,代理不会经历所有的步骤(至少在开始时,当它还没有学会如何完成任务时)。

plot_reward = []
plot_policy = []
plot_q = []
plot_steps = []

best_reward = -np.inf
saved_reward = -np.inf
saved_ep = 0
average_reward = 0
global_step = 0
#s = deepcopy(env.reset())

for episode in range(MAX_EPISODES):
    #print(episode)
    s = deepcopy(env.reset())
    #noise.reset()

    ep_reward = 0.
    ep_q_value = 0.
    step=0

    for step in range(MAX_STEPS):
        #loss=0
        global_step +=1
        epsilon -= epsilon_decay
        #actor.eval()
        a = actor.get_action(s)
        #actor.train()

        a += noise()*max(0, epsilon)
        a = np.clip(a, -1., 1.)
        s2, reward, terminal, info = env.step(a)

        memory.add(s, a, reward, terminal,s2)

        #keep adding experiences to the memory until there are at least minibatch size samples

        if memory.count() > buffer_start:
            s_batch, a_batch, r_batch, t_batch, s2_batch = memory.sample(BATCH_SIZE)

            s_batch = torch.FloatTensor(s_batch).to(device)
            a_batch = torch.FloatTensor(a_batch).to(device)
            r_batch = torch.FloatTensor(r_batch).unsqueeze(1).to(device)
            t_batch = torch.FloatTensor(np.float32(t_batch)).unsqueeze(1).to(device)
            s2_batch = torch.FloatTensor(s2_batch).to(device)

            #compute loss for critic
            a2_batch = target_actor(s2_batch)
            target_q = target_critic(s2_batch, a2_batch)
            y = r_batch + (1.0 - t_batch) * GAMMA * target_q.detach() #detach to avoid backprop target
            q = critic(s_batch, a_batch)

            q_optimizer.zero_grad()
            q_loss = MSE(q, y) 
            q_loss.backward()
            q_optimizer.step()

            #compute loss for actor
            policy_optimizer.zero_grad()
            policy_loss = -critic(s_batch, actor(s_batch))
            policy_loss = policy_loss.mean()
            policy_loss.backward()
            policy_optimizer.step()

            #soft update of the frozen target networks
            for target_param, param in zip(target_critic.parameters(), critic.parameters()):
                target_param.data.copy_(
                    target_param.data * (1.0 - TAU) + param.data * TAU
                )

            for target_param, param in zip(target_actor.parameters(), actor.parameters()):
                target_param.data.copy_(
                    target_param.data * (1.0 - TAU) + param.data * TAU
                )

        s = deepcopy(s2)
        ep_reward += reward

        #if terminal:
        #    noise.reset()
        #    break

    try:
        plot_reward.append([ep_reward, episode+1])
        plot_policy.append([policy_loss.data, episode+1])
        plot_q.append([q_loss.data, episode+1])
        plot_steps.append([step+1, episode+1])
    except:
        continue
    average_reward += ep_reward

    if ep_reward > best_reward:
        torch.save(actor.state_dict(), 'best_model_pendulum.pkl') #Save the actor model for future testing
        best_reward = ep_reward
        saved_reward = ep_reward
        saved_ep = episode+1

    if (episode % PRINT_EVERY) == (PRINT_EVERY-1):    # print every print_every episodes
        subplot(plot_reward, plot_policy, plot_q, plot_steps)
        print('[%6d episode, %8d total steps] average reward for past {} iterations: %.3f'.format(PRINT_EVERY) %
              (episode + 1, global_step, average_reward / PRINT_EVERY))
        print("Last model saved with reward: {:.2f}, at episode {}.".format(saved_reward, saved_ep))
        average_reward = 0 #reset average reward 

结论

For Pendulum-v0 task, the objective is to maintain the pendulum in a vertical position, so the total cumulative reward over all the steps must be as close as possible to zero; after 200 steps the agent learned to reach and stay in that condition.

本教程中的代码片段是一个更完整的笔记本的一部分,你可以在 GitHub 的这个链接找到。那个笔记本用的网络适合低维状态空间;如果你想处理图像输入,你必须增加卷积层,如研究论文第 11 页所述。

请随意与其他环境或方法分享您的经验,以改进整体培训流程!

pix2pix 生成对抗网络

原文:https://blog.paperspace.com/pix2pix-gan/

Anime Latte | Instagram: @timmossholder

Photo by Tim Mossholder / Unsplash

除了生成对抗网络(GANs)可以实现的无数奇迹之外,例如从零开始生成全新的实体,它们还有另一个用于图像到图像翻译的奇妙用例。在之前的博客中,我们了解了条件 GANs 的实现。这些条件 gan 的特征在于执行任务以获得提供给训练网络的特定输入所需的输出的能力。在我们继续本文中这些条件 GAN 的一个变体之前,我建议从这个链接检查 CGAN 的实现。

在本文中,我们将探讨 GAN 的一种变体:pix2pix GANs,这种变体近年来因其能够高精度地将图像从源域转换到目标域而广受欢迎。在我们开始理解 pix2pix GAN 的研究论文和体系结构分析之前,我们将首先简要介绍阅读这篇博客所需的基础知识。最后,我们将从头开始使用 pix2pix GANs 开发一个卫星图像到地图的翻译项目。

通过使用以下链接的 repo 作为“工作区 URL”来创建 Paperspace Gradient 笔记本,可以利用 Paperspace Gradient 平台来运行以下项目通过切换笔记本创建页面上的高级选项按钮,可以找到该字段。

简介:

条件 GANs 的主要应用之一是这些网络执行高精度图像到图像翻译的能力。图像到图像的翻译是一项任务,其中我们从一个特定的域获取图像,并通过根据特定任务的需要对其进行变换,将其转换为另一个域中的图像。有各种各样的图像翻译项目可供使用,包括将黑白图像转换为彩色图像,将动画草图转换为逼真的人类图片,以及许多其他类似的想法。

先前已经利用了许多方法来高精度地执行图像到图像的转换。然而,一种简单的 CNN 方法来最小化预测像素和真实像素之间的欧几里德距离往往会产生模糊的结果。模糊效应的主要原因是通过平均所有似是而非的输出来最小化欧几里德距离。pix2pix 条件 GAN 解决了大部分这些潜在问题。模糊图像将被鉴别器网络确定为假样本,解决了以前 CNN 方法的一个主要问题。在下一节中,我们将对这些 pix2pix 条件 gan 有一个更概念性的理解。


了解 pix2pix GANs:

Image Source

先前已经使用了许多方法来执行各种图像转换任务。然而,历史上大多数原始方法都失败了,包括一些流行的深度学习框架。当大多数方法在生成任务中挣扎时,生成对抗网络在大多数情况下成功了。最好的图像翻译网络之一是 pix2pix GANs。在本节中,我们将分解这些 pix2pix GAN 的程序工作,并尝试理解 pix 2 pix GAN 架构的发生器和鉴别器网络的复杂细节。

生成器架构利用了 U-Net 架构设计。在我之前的一篇博客中,我们已经非常详细地讨论了这个话题,你可以通过这个链接查看。U-Net 架构使用编码器-解码器类型的结构,其中我们在架构的前半部分对卷积层进行下采样,后半部分涉及使用卷积转置等分层上采样来调整大小,以实现更高的图像缩放。传统的 U-Net 架构略显陈旧,因为它最初是在 2015 年开发的,自那以来神经网络已经取得了巨大的进步。

因此,pix2pix GAN 的发电机网络中使用的 U-Net 架构是原始 U-Net 架构的略微修改版本。而编码器-解码器结构以及跳跃连接是两个网络的关键方面;有一些关键的值得注意的差异。原始 U-Net 架构中的图像大小从原始尺寸变为新的较小高度和宽度。在 pix2pix GAN 网络中,发生器网络保留图像大小和尺寸。在向下步进之前,我们在 pix2pix 生成器网络中仅使用一个卷积层模块,而不是最初使用的两个模块。最后,U-Net 网络缩小到大约 32 x 32 的最大值,而发电机网络一直缩小到 1 x 1。

至于所提出的鉴别器架构,pix2pix GAN 使用了一种分片方法,该方法仅在分片尺度上惩罚结构。虽然 GAN 架构中的大多数复杂鉴别器利用整个图像来建立伪或实(0 或 1)值,但是块 GAN 试图对图像中的每个 N ×N 块是真还是假进行分类。对于每个特定的任务,N×N 小块的大小可以不同,但是最终的输出是所考虑的小块的所有响应的平均值。贴片 GAN 鉴别器的主要优势在于它们具有更少的训练参数、运行更快,并且可以应用于任意大的图像。

在 pix2pix GANs 的帮助下,我们可以进行许多实验。其中一些实验包括城市景观照片翻译的语义标签,立面数据集照片的建筑标签,黑白图像到彩色图像,动画草图到现实人类图片,等等!在这个项目中,我们将把重点放在卫星地图到航空照片的转换,从谷歌地图刮数据训练。


使用 pix2pix GANs 将卫星图像转换为地图;

Image Source

在本文的这一部分中,我们将着重于从头开始开发一个 pix2pix GAN 架构,用于将卫星图像转换为各自的地图。在开始本教程之前,我强烈建议查看一下我之前在 TensorFlow 文章和 Keras 文章上的博客。这两个库将是我们用于构建以下项目的主要深度学习框架。该项目被分成许多更小的子部分,以便更容易理解完成预期任务所需的所有步骤。让我们从导入所有必要的库开始。

导入基本库

如前所述,我们将利用的两个主要深度学习框架是 TensorFlow 和 Keras。最有用的层包括卷积层、泄漏 ReLU 激活函数、批量归一化、漏失层和其他一些基本层。我们还将导入 NumPy 库来处理数组,并相应地生成真实和虚假的图像。Matplotlib 库用于绘制所需的图形和必要的绘图。请查看下面的代码块,了解我们将在这个项目的构造中使用的所有必需的库和导入。

from tensorflow.keras.optimizers import Adam
from tensorflow.keras.initializers import RandomNormal
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Conv2D, Conv2DTranspose, LeakyReLU, Activation
from tensorflow.keras.layers import BatchNormalization, Concatenate, Dropout
from keras.preprocessing.image import img_to_array
from keras.preprocessing.image import load_img
from tensorflow.keras.utils import plot_model
from tensorflow.keras.models import load_model
from os import listdir
from numpy import asarray, load
from numpy import vstack
from numpy import savez_compressed
from matplotlib import pyplot
import numpy as np
from matplotlib import pyplot as plt
from numpy.random import randint
from numpy import zeros
from numpy import ones

准备数据:

通过查看和分析我们的数据,我们可以注意到,我们有一个完整的图像,其中包含地图和它们各自的卫星图像。在接下来的代码块中,我们将定义数据集的路径。我推荐那些想和教程一起实验的读者从 Kaggle 下载这个数据集。以下数据集旨在作为图像到图像翻译问题的通用解决方案。他们有四个数据集,我们可以用来开发 Pix2Pix GANs。这些数据集包括门面、城市景观、地图和鞋的边缘。

您还可以下载任何其他想要测试模型工作过程的下载内容。对于本文,我们将使用地图数据集。在下面的代码块中,我定义了到我的目录的特定路径,该目录包含带有训练和验证目录的地图数据。请随意相应地设置自己的路径位置。我们还将定义一些基本参数,使用这些参数可以更轻松地完成一些编码过程。由于图像包含卫星图像及其各自的地图,我们可以将它们平均分割,因为它们的尺寸都是 256 x 256,如下面的代码块所示。

# load all images in a directory into memory
def load_images(path, size=(256,512)):
    src_list, tar_list = list(), list()
    for filename in listdir(path):
        # load and resize the image
        pixels = load_img(path + filename, target_size=size)
        # convert to numpy array
        pixels = img_to_array(pixels)
        # split into satellite and map
        sat_img, map_img = pixels[:, :256], pixels[:, 256:]
        src_list.append(sat_img)
        tar_list.append(map_img)
    return [asarray(src_list), asarray(tar_list)]

# dataset path
path = 'maps/train/'
# load dataset
[src_images, tar_images] = load_images(path)
print('Loaded: ', src_images.shape, tar_images.shape)

n_samples = 3
for i in range(n_samples):
    pyplot.subplot(2, n_samples, 1 + i)
    pyplot.axis('off')
    pyplot.imshow(src_images[i].astype('uint8'))

# plot target image
for i in range(n_samples):
    pyplot.subplot(2, n_samples, 1 + n_samples + i)
    pyplot.axis('off')
    pyplot.imshow(tar_images[i].astype('uint8'))
pyplot.show()
Loaded:  (1096, 256, 256, 3) (1096, 256, 256, 3) 

u 网发电机网络:

为了构建 pix2pix GAN 架构的发生器网络,我们将把该结构分成几个部分。我们将从编码器模块开始,这里我们将定义步长为 2 的卷积层,然后是泄漏 ReLU 激活函数。大多数卷积层之后还会有批量标准化层,如下面的代码块所示。一旦我们返回了发生器网络的编码器模块,我们可以如下构建网络的前半部分-C64-C128-C256-C512-C512-C512-C512-C512-C512。

# Encoder Block
def define_encoder_block(layer_in, n_filters, batchnorm=True):
    init = RandomNormal(stddev=0.02)
    g = Conv2D(n_filters, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(layer_in)
    if batchnorm:
        g = BatchNormalization()(g, training=True)
    g = LeakyReLU(alpha=0.2)(g)
    return g

我们将定义的发生器网络的下一部分是解码器模块。在该功能中,我们将对所有先前下采样的图像进行上采样,并添加(连接)从编码器到解码器网络必须进行的必要跳跃连接,类似于 U-Net 架构。对于模型的上采样,我们可以利用卷积转置层和批量归一化层以及可选的丢弃层。发生器网络的解码器模块包含如下架构-CD 512-CD 512-CD 512-C512-C256-C128-C64。下面是以下结构的代码块。

# Decoder Block
def decoder_block(layer_in, skip_in, n_filters, dropout=True):
    init = RandomNormal(stddev=0.02)
    g = Conv2DTranspose(n_filters, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(layer_in)
    g = BatchNormalization()(g, training=True)
    if dropout:
        g = Dropout(0.5)(g, training=True)
    # merge with skip connection
    g = Concatenate()([g, skip_in])
    g = Activation('relu')(g)
    return g

现在,我们已经构建了前面的两个主要函数,即编码器和解码器模块,我们可以继续多次调用它们,根据必要的要求调整网络。我们将遵循本节之前讨论的编码器和解码器网络架构,并相应地构建这些模块。大部分结构都是按照下面的研究论文搭建的。最终输出激活函数 tanh 生成-1 到 1 范围内的图像。通过修改一些参数,您可以随意尝试任何其他可能产生更好结果的较小修改。发电机网络的代码块如下所示。

# Define the overall generator architecture
def define_generator(image_shape=(256,256,3)):
    # weight initialization
    init = RandomNormal(stddev=0.02)

    # image input
    in_image = Input(shape=image_shape)

    # encoder model: C64-C128-C256-C512-C512-C512-C512-C512
    e1 = define_encoder_block(in_image, 64, batchnorm=False)
    e2 = define_encoder_block(e1, 128)
    e3 = define_encoder_block(e2, 256)
    e4 = define_encoder_block(e3, 512)
    e5 = define_encoder_block(e4, 512)
    e6 = define_encoder_block(e5, 512)
    e7 = define_encoder_block(e6, 512)

    # bottleneck, no batch norm and relu
    b = Conv2D(512, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(e7)
    b = Activation('relu')(b)

    # decoder model: CD512-CD512-CD512-C512-C256-C128-C64
    d1 = decoder_block(b, e7, 512)
    d2 = decoder_block(d1, e6, 512)
    d3 = decoder_block(d2, e5, 512)
    d4 = decoder_block(d3, e4, 512, dropout=False)
    d5 = decoder_block(d4, e3, 256, dropout=False)
    d6 = decoder_block(d5, e2, 128, dropout=False)
    d7 = decoder_block(d6, e1, 64, dropout=False)

    # output
    g = Conv2DTranspose(image_shape[2], (4,4), strides=(2,2), padding='same', kernel_initializer=init)(d7) #Modified 
    out_image = Activation('tanh')(g)  #Generates images in the range -1 to 1\. So change inputs also to -1 to 1

    # define model
    model = Model(in_image, out_image)
    return model

贴片 GAN 鉴频器网络:

一旦构建了发生器网络,我们就可以继续研究鉴别器架构。在继续构建鉴别器结构之前,我们将初始化我们的权重并合并输入源图像和目标图像。鉴别器结构将遵循 C64-C128-C256-C512 的模式构建。在最后一层之后,应用卷积来映射到 1 维输出,随后是 sigmoid 函数。下面代码块中使用的鉴别器网络允许大小低至 16 x 16。最后,我们可以编译用一个图像的批量大小和具有小学习率和 0.5 beta 值的 Adam 优化器训练的模型。对于每次模型更新,鉴别器的损耗被加权 50%。查看以下代码片段,了解完整的 GAN 鉴频器网络补丁。

def define_discriminator(image_shape):

    # weight initialization
    init = RandomNormal(stddev=0.02)

    # source image input
    in_src_image = Input(shape=image_shape)  

    # target image input
    in_target_image = Input(shape=image_shape)  

    # concatenate images, channel-wise
    merged = Concatenate()([in_src_image, in_target_image])

    # C64: 4x4 kernel Stride 2x2
    d = Conv2D(64, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(merged)
    d = LeakyReLU(alpha=0.2)(d)

    # C128: 4x4 kernel Stride 2x2
    d = Conv2D(128, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(d)
    d = BatchNormalization()(d)
    d = LeakyReLU(alpha=0.2)(d)

    # C256: 4x4 kernel Stride 2x2
    d = Conv2D(256, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(d)
    d = BatchNormalization()(d)
    d = LeakyReLU(alpha=0.2)(d)

    # C512: 4x4 kernel Stride 2x2 
    d = Conv2D(512, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(d)
    d = BatchNormalization()(d)
    d = LeakyReLU(alpha=0.2)(d)

    # second last output layer : 4x4 kernel but Stride 1x1 (Optional)
    d = Conv2D(512, (4,4), padding='same', kernel_initializer=init)(d)
    d = BatchNormalization()(d)
    d = LeakyReLU(alpha=0.2)(d)

    # patch output
    d = Conv2D(1, (4,4), padding='same', kernel_initializer=init)(d)
    patch_out = Activation('sigmoid')(d)

    # define model
    model = Model([in_src_image, in_target_image], patch_out)
    opt = Adam(lr=0.0002, beta_1=0.5)

    # compile model
    model.compile(loss='binary_crossentropy', optimizer=opt, loss_weights=[0.5])
    return model

定义完整的 GAN 架构:

既然我们已经定义了发生器和鉴频器网络,我们可以根据需要继续训练整个 GAN 架构。鉴别器中的重量是不可训练的,但是独立的鉴别器是可训练的。因此,我们将相应地设置这些参数。然后,我们将把源图像作为输入传递给模型,而模型的输出将包含生成的结果以及鉴别器输出。总损失计算为对抗性损失(BCE)和 L1 损失(MAE)的加权和,权重比为 1:100。我们可以用这些参数和 Adam 优化器来编译模型,以返回最终的模型。

# define the combined GAN architecture
def define_gan(g_model, d_model, image_shape):
    for layer in d_model.layers:
        if not isinstance(layer, BatchNormalization):
            layer.trainable = False

    in_src = Input(shape=image_shape)
    gen_out = g_model(in_src)
    dis_out = d_model([in_src, gen_out])
    model = Model(in_src, [dis_out, gen_out])
    # compile model
    opt = Adam(lr=0.0002, beta_1=0.5)

    model.compile(loss=['binary_crossentropy', 'mae'], 
               optimizer=opt, loss_weights=[1,100])

    return model

定义所有基本参数:

下一步,我们将定义训练 pix2pix GAN 模型所需的所有基本函数和参数。首先,让我们定义生成真样本和假样本的函数。下面提供了执行以下操作的代码片段。

def generate_real_samples(dataset, n_samples, patch_shape):
    trainA, trainB = dataset
    ix = randint(0, trainA.shape[0], n_samples)
    X1, X2 = trainA[ix], trainB[ix]
    y = ones((n_samples, patch_shape, patch_shape, 1))
    return [X1, X2], y

def generate_fake_samples(g_model, samples, patch_shape):
    X = g_model.predict(samples)
    y = zeros((len(X), patch_shape, patch_shape, 1))
    return X, y

在下一个代码块中,我们将创建一个函数来总结模型的性能。生成的图像将与它们的原始对应物进行比较,以获得期望的响应。我们可以为源图像、生成的图像和目标输出图像绘制三幅图。我们可以保存绘图和发电机模型,以便以后根据需要进行进一步计算。

#save the generator model and check how good the generated image looks. 
def summarize_performance(step, g_model, dataset, n_samples=3):
    [X_realA, X_realB], _ = generate_real_samples(dataset, n_samples, 1)
    X_fakeB, _ = generate_fake_samples(g_model, X_realA, 1)

    # scale all pixels from [-1,1] to [0,1]
    X_realA = (X_realA + 1) / 2.0
    X_realB = (X_realB + 1) / 2.0
    X_fakeB = (X_fakeB + 1) / 2.0

    # plot real source images
    for i in range(n_samples):
        plt.subplot(3, n_samples, 1 + i)
        plt.axis('off')
        plt.imshow(X_realA[i])

    # plot generated target image
    for i in range(n_samples):
        plt.subplot(3, n_samples, 1 + n_samples + i)
        plt.axis('off')
        plt.imshow(X_fakeB[i])

    # plot real target image
    for i in range(n_samples):
        plt.subplot(3, n_samples, 1 + n_samples*2 + i)
        plt.axis('off')
        plt.imshow(X_realB[i])

    # save plot to file
    filename1 = 'plot_%06d.png' % (step+1)
    plt.savefig(filename1)
    plt.close()

    # save the generator model
    filename2 = 'model_%06d.h5' % (step+1)
    g_model.save(filename2)
    print('>Saved: %s and %s' % (filename1, filename2))

最后,让我们定义训练函数,通过它我们可以训练模型并调用所有适当的函数来生成样本、总结模型性能,以及根据需要训练批处理函数。一旦我们为 pix2pix 模型创建了训练函数,我们就可以继续训练模型并为卫星图像到地图图像转换任务生成结果。下面是 train 函数的代码块。

# train function for the pix2pix model
def train(d_model, g_model, gan_model, dataset, n_epochs=100, n_batch=1):
    n_patch = d_model.output_shape[1]
    trainA, trainB = dataset
    bat_per_epo = int(len(trainA) / n_batch)
    n_steps = bat_per_epo * n_epochs

    for i in range(n_steps):
        [X_realA, X_realB], y_real = generate_real_samples(dataset, n_batch, n_patch)
        X_fakeB, y_fake = generate_fake_samples(g_model, X_realA, n_patch)

        d_loss1 = d_model.train_on_batch([X_realA, X_realB], y_real)
        d_loss2 = d_model.train_on_batch([X_realA, X_fakeB], y_fake)
        g_loss, _, _ = gan_model.train_on_batch(X_realA, [y_real, X_realB])

        # summarize model performance
        print('>%d, d1[%.3f] d2[%.3f] g[%.3f]' % (i+1, d_loss1, d_loss2, g_loss))
        if (i+1) % (bat_per_epo * 10) == 0:
            summarize_performance(i, g_model, dataset)

训练 pix2pix 模型:

一旦我们构建了整个发生器和鉴别器网络,并将它们组合到 GAN 架构中,并完成了所有基本参数和值的声明,我们就可以开始最终训练 pix2pix 模型并观察其性能。我们将传递源的镜像形状,并根据发生器和鉴频器网络构建 GAN 架构。

我们将定义输入源图像和目标图像,然后根据输出双曲正切函数的执行情况,对这些图像进行相应的归一化处理,使其在-1 到 1 的理想范围内缩放。我们最终可以开始模型的训练,并在十个时期后评估性能。为每个时期的每批(总共 1096 个)报告参数。对于十个纪元,我们应该注意到总数为 10960。下面是训练模型的代码片段。

image_shape = src_images.shape[1:]
d_model = define_discriminator(image_shape)
g_model = define_generator(image_shape)
gan_model = define_gan(g_model, d_model, image_shape)

data = [src_images, tar_images]

def preprocess_data(data):
    X1, X2 = data[0], data[1]
    # scale from [0,255] to [-1,1]
    X1 = (X1 - 127.5) / 127.5
    X2 = (X2 - 127.5) / 127.5
    return [X1, X2]

dataset = preprocess_data(data)

from datetime import datetime 
start1 = datetime.now() 

train(d_model, g_model, gan_model, dataset, n_epochs=10, n_batch=1) 

stop1 = datetime.now()
#Execution time of the model 
execution_time = stop1-start1
print("Execution time is: ", execution_time)
>10955, d1[0.517] d2[0.210] g[8.743]
>10956, d1[0.252] d2[0.693] g[5.987]
>10957, d1[0.243] d2[0.131] g[12.658]
>10958, d1[0.339] d2[0.196] g[6.857]
>10959, d1[0.010] d2[0.125] g[4.013]
>10960, d1[0.085] d2[0.100] g[10.957]
>Saved: plot_010960.png and model_010960.h5
Execution time is:  0:39:10.933599 

在我的系统上,用于模型训练的程序的执行花费了大约 40 分钟。根据您的 GPU 和设备功能,培训时间可能会有所不同。Paperspace 上的梯度平台是这种培训机制的一个很好的可行选择。训练完成后,我们有了一个模型和一个可用的图。可以加载模型,并相应地对其进行必要的预测。下面是用于加载模型以及进行基本预测和相应绘图的代码块。

# Plotting the Final Results
model = load_model('model_010960.h5')

# plot source, generated and target images
def plot_images(src_img, gen_img, tar_img):
    images = vstack((src_img, gen_img, tar_img))
    # scale from [-1,1] to [0,1]
    images = (images + 1) / 2.0
    titles = ['Source', 'Generated', 'Expected']

    # plot images row by row
    for i in range(len(images)):
        pyplot.subplot(1, 3, 1 + i)
        pyplot.axis('off')
        pyplot.imshow(images[i])
        pyplot.title(titles[i])
    pyplot.show()

[X1, X2] = dataset
# select random example
ix = randint(0, len(X1), 1)
src_image, tar_image = X1[ix], X2[ix]
# generate image from source
gen_image = model.predict(src_image)
# plot all three images
plot_images(src_image, gen_image, tar_image)

我强烈推荐查看下面的网站,其中大部分代码都被考虑到了。作为下一步,我们强烈建议观众尝试各种可能有助于产生更好效果的组合。观众也可以选择对更多的时期进行训练,以尝试获得更理想的结果。除了这个项目,pix2pix GANs 在各种项目中都有很高的实用性。我建议尝试许多项目,以便更好地掌握这些生成网络的能力。


结论:

A little astronaut in the moon

Photo by Kobby Mendez / Unsplash

从一幅图像到另一幅图像的图像到图像转换是一项非常复杂的任务,因为简单的卷积网络由于缺乏特征提取能力而无法以最理想的方式完成这项任务。另一方面,GANs 在生成一些高精度和准确度的最佳图像方面做得非常出色。它们还有助于避免简单卷积网络的一些暗淡效果,如输出清晰、逼真的图像等。因此,推出的 pix2pix GAN 架构是解决此类问题的最佳 GAN 版本之一。艺术家和多个用户甚至通过互联网使用 pix2pix GAN 软件来实现高质量的结果。

在本文中,我们主要关注 pix2pix GANs 中用于图像翻译的最重要的条件类型 gan 之一。我们了解了更多关于图像翻译的主题,并理解了与 pix2pix GANs 及其功能机制相关的大多数基本概念。一旦我们介绍了 pix2pix GAN 的基本方面,包括生成器和鉴别器架构,我们就开始构建卫星图像转换为地图项目。强烈建议观众尝试其他类似项目,并尝试 pix2pix GAN 架构的发生器和鉴频器网络的各种可能变化。

在以后的文章中,我们将关注用 pix2pix GANs 构建更多的项目,因为这些条件网络有许多可能性。我们还将探索其他类型的 gan,如 Cycle GANs,以及关于 BERT 变换器和从头构建神经网络的其他教程和项目(第 2 部分)。在那之前,享受编码和构建新项目吧!

卷积神经网络中的池化

原文:https://blog.paperspace.com/pooling-in-convolutional-neural-networks/

Photo by Lee Jeffs / Unsplash

任何熟悉卷积神经网络的人都会对术语“汇集”感到熟悉,因为它是每个卷积层之后通常使用的过程。在本文中,我们将探索 CNN 架构中这一基本过程背后的原因和方法。

汇集过程

与卷积类似,池化过程也利用过滤器/内核,尽管它没有任何元素(有点像空数组)。它本质上包括在图像的连续补丁上滑动这个过滤器,并以某种方式处理内核中捕获的像素;基本上与卷积运算相同。

计算机视觉的飞跃

在深度学习框架中,有一个不太流行但非常基本的参数,它决定了卷积和池类的行为。从更一般的意义上来说,它控制任何由于某种原因使用滑动窗口的类的行为。该参数被称为'步距'它被称为滑动窗口,因为用滤镜扫描图像类似于在图像像素上滑动一个小窗口)。

跨距参数决定了在执行卷积和池化等滑动窗口操作时,mxuch 滤波器如何在任一维度上移动。

Strides in a sliding window operation using a kernel/filter of size (2, 2)

在上面的图像中,滤镜在(6,6)图像上的第 0 维度(水平)和第 1 维度(垂直)滑动。当 stride=1 时,滤波器滑动一个像素。然而,当步幅=2 时,滤波器滑动两个像素;步幅=3 时为三个像素。当通过滑动窗口过程生成新图像时,这具有有趣的效果;因为在两个维度上的步幅 2 基本上生成了一个图像,该图像是其原始图像的一半大小。同样,步幅为 3 将产生大小为其参考图像的三分之一的图像,依此类推。

当步幅> 1 时,产生的表示是其参考图像大小的一部分。

在执行池操作时,一定要注意默认情况下 stride 总是等于过滤器的大小。例如,如果要使用(2,2)滤波器,stride 的默认值为 2。

联营的类型

在细胞神经网络中主要使用两种类型的池操作,它们是最大池和平均池。这两种池操作的全局变体也存在,但是它们超出了本文的范围(全局最大池和全局平均池)。

最大池化

最大池需要使用过滤器扫描图像,并在每个实例中返回过滤器中捕捉的最大像素值,作为新图像中它自己的像素。

The max pooling operation

从图中可以看出,空的(2,2)滤波器在(4,4)图像上滑动,跨距为 2,如上一节所述。每个实例的最大像素值作为其自身的独特像素返回,以形成新的图像。所得到的图像被认为是原始图像的最大合并表示(注意,由于前面章节中讨论的默认步幅为 2,所得到的图像是原始图像的一半大小)。

平均池

就像最大池一样,空过滤器也在图像上滑动,但在这种情况下,过滤器中捕获的所有像素的平均值将返回,以形成原始图像的平均池表示,如下图所示。

The average pooling operation

最大池与平均池

从上一节中的插图可以清楚地看到,与平均混合表示相比,最大混合表示中的像素值要大得多。用更简单的话来说,这仅仅意味着从最大池产生的表示通常比从平均池产生的表示更清晰。

联营的本质

在我以前的一篇文章中,我提到了卷积神经网络如何通过卷积过程从图像中提取边缘特征。这些提取的特征被称为特征图。然后,池化作用于这些特征图,并作为一种主成分分析(允许我对这一概念相当自由),通过浏览特征图并在一个称为下采样的过程中产生一个小规模的摘要。

用不太专业的术语来说,汇集生成小尺寸的图像,这些图像保留了参考图像的所有基本属性(像素)。基本上,可以产生汽车的(25,25)像素图像,通过使用(2,2)核迭代汇集 4 次,该图像将保留大小为(400,400)的参考图像的所有一般细节和组成。它通过利用大于 1 的步幅来做到这一点,允许产生是原始图像的一部分的表示。

回到 CNN,随着卷积层变得更深,特征图(由卷积产生的表示)的数量增加。如果特征图的大小与提供给网络的图像的大小相同,由于网络中存在大量数据,特别是在训练期间,计算速度将受到严重阻碍。通过逐步对这些特征地图进行下采样,即使特征地图的数量增加,网络中的数据量也能得到有效控制。这意味着网络将逐步处理合理数量的数据,而不会丢失由先前卷积层提取的任何基本特征,从而提高计算速度。

池的另一个效果是,它允许卷积神经网络变得更加健壮,因为它们变得平移不变。这意味着该网络将能够从感兴趣的对象中提取特征,而不管该对象在图像中的位置如何(在未来的文章中会有更多相关内容)。

引擎盖下。

在本节中,我们将使用一个手动编写的池函数来可视化池过程,以便更好地理解实际发生了什么。提供了两个函数,一个用于最大池,另一个用于平均池。使用这些函数,我们将尝试合并以下大小为(446,550)像素的图像。

Reference image.

#  import these dependencies
import torch
import numpy as np
import matplotlib.pyplot as plt
import torch.nn.functional as F
from tqdm import tqdm

Don't forget to import these dependencies

幕后最大池

def max_pool(image_path, kernel_size=2, visualize=False, title=''):
      """
      This function replicates the maxpooling
      process
      """

      #  assessing image parameter
      if type(image_path) is np.ndarray and len(image_path.shape)==2:
        image = image_path
      else:
        image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

      #  creating an empty list to store convolutions
      pooled = np.zeros((image.shape[0]//kernel_size, 
                        image.shape[1]//kernel_size))

      #  instantiating counter
      k=-1
      #  maxpooling
      for i in tqdm(range(0, image.shape[0], kernel_size)):
        k+=1
        l=-1
        if k==pooled.shape[0]:
          break
        for j in range(0, image.shape[1], kernel_size):
          l+=1
          if l==pooled.shape[1]:
            break
          try:
            pooled[k,l] = (image[i:(i+kernel_size), 
                                j:(j+kernel_size)]).max()
          except ValueError:
            pass

      if visualize:
        #  displaying results
        figure, axes = plt.subplots(1,2, dpi=120)
        plt.suptitle(title)
        axes[0].imshow(image, cmap='gray')
        axes[0].set_title('reference image')
        axes[1].imshow(pooled, cmap='gray')
        axes[1].set_title('maxpooled')
      return pooled

Max Pooling Function.

上面的函数复制了最大池化过程。使用函数,让我们尝试使用(2,2)内核最大化参考图像池。

max_pool('image.jpg', 2, visualize=True)

Producing a max pooled representation

查看每个轴上的数字线,可以清楚地看到图像的尺寸缩小了,但所有细节都保持不变。这几乎就像该过程已经提取了最显著的像素,并且产生了大小为参考图像一半的概括表示(一半是因为使用了(2,2)核)。

下面的函数允许最大池化过程的多次迭代的可视化。

def visualize_pooling(image_path, iterations, kernel=2):
      """
      This function helps to visualise several
      iterations of the pooling process
      """
      image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

      #  creating empty list to hold pools
      pools = []
      pools.append(image)

      #  performing pooling
      for iteration in range(iterations):
        pool = max_pool(pools[-1], kernel)
        pools.append(pool)

      #  visualisation
      fig, axis = plt.subplots(1, len(pools), dpi=700)
      for i in range(len(pools)):
        axis[i].imshow(pools[i], cmap='gray')
        axis[i].set_title(f'{pools[i].shape}', fontsize=5)
        axis[i].axis('off')
      pass

Pooling visualization function

使用此函数,我们可以使用(2,2)过滤器可视化 3 代最大池表示,如下所示。图像从(446,450)像素的大小变为(55,56)像素的大小(本质上是 1.5%的总和),同时保持其总体构成。

visualize_pooling('image.jpg', 3)

Reference image through 3 progressive iterations of max pooling using a (2, 2) kernel.

使用更大的核(3,3)的效果如下所示,正如预期的那样,对于每次迭代,参考图像减小到其先前大小的 1/3。到第三次迭代,产生像素化的(16,16)下采样表示(0.1%总和)。虽然是像素化的,但图像的整体概念仍然保持不变。

visualize_pooling('image.jpg', 3, kernel=3)

Reference image through 3 iterations of max pooling using a (3, 3) kernel.

为了正确地尝试模拟卷积神经网络中的最大池化过程,让我们使用 Prewitt 算子对图像中检测到的垂直边缘进行几次迭代。

Max pooling over detected edges.

到第三次迭代时,虽然图像的尺寸减小了,但是可以看到它的特征(边缘)逐渐变得清晰。

幕后平均池

def average_pool(image_path, kernel_size=2, visualize=False, title=''):
      """
      This function replicates the averagepooling
      process
      """

      #  assessing image parameter
      if type(image_path) is np.ndarray and len(image_path.shape)==2:
        image = image_path
      else:
        image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

      #  creating an empty list to store convolutions
      pooled = np.zeros((image.shape[0]//kernel_size, 
                        image.shape[1]//kernel_size))

      #  instantiating counter
      k=-1
      #  averagepooling
      for i in tqdm(range(0, image.shape[0], kernel_size)):
        k+=1
        l=-1
        if k==pooled.shape[0]:
          break
        for j in range(0, image.shape[1], kernel_size):
          l+=1
          if l==pooled.shape[1]:
            break
          try:
            pooled[k,l] = (image[i:(i+kernel_size), 
                                j:(j+kernel_size)]).mean()
          except ValueError:
            pass

      if visualize:
        #  displaying results
        figure, axes = plt.subplots(1,2, dpi=120)
        plt.suptitle(title)
        axes[0].imshow(image, cmap='gray')
        axes[0].set_title('reference image')
        axes[1].imshow(pooled, cmap='gray')
        axes[1].set_title('averagepooled')
      return pooled

Average pooling function.

上面的函数复制了平均的池化过程。请注意,这是与 max pooling 函数相同的代码,不同之处在于在内核滑过图像时使用了 mean()方法。下面是我们参考图像的平均合并表示。

average_pool('image.jpg', 2, visualize=True)

Producing an average pooled representation.

与 max pooling 类似,可以看到图像已经缩小到一半大小,同时保留了其最重要的属性。这非常有趣,因为与最大池不同,平均池不直接使用参考图像中的像素,而是将它们组合起来,基本上创建新的属性(像素),而参考图像中的细节仍会保留。

让我们使用下面的可视化功能来看看平均池化过程是如何通过 3 次迭代进行的。

def visualize_pooling(image_path, iterations, kernel=2):
      """
      This function helps to visualise several
      iterations of the pooling process
      """
      image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

      #  creating empty list to hold pools
      pools = []
      pools.append(image)

      #  performing pooling
      for iteration in range(iterations):
        pool = average_pool(pools[-1], kernel)
        pools.append(pool)

      #  visualisation
      fig, axis = plt.subplots(1, len(pools), dpi=700)
      for i in range(len(pools)):
        axis[i].imshow(pools[i], cmap='gray')
        axis[i].set_title(f'{pools[i].shape}', fontsize=5)
        axis[i].axis('off')
      pass

Pooling visualization function.

同样,即使图像大小在每次迭代后逐渐减少一半,其细节仍然保持,尽管逐渐像素化(如小尺寸图像中所预期的)。

visualize_pooling('image.jpg', 3)

Reference image through 3 iterations of average pooling using a (2, 2) kernel.

使用(3,3)内核的平均池产生以下结果。不出所料,图像大小被缩小到之前值的 1/3。就像在 max pooling 中一样,在第 3 次迭代中出现大量像素,但图像的整体属性相当完整。

visualize_pooling('image.jpg', 3, kernel=3)

Reference image through 3 iterations of average pooling using a (3, 3) kernel.

使用 Prewitt 算子在检测到的垂直边缘上运行(2,2)平均汇集产生以下结果。正如在最大池中一样,图像特征(边缘)在渐进平均池中变得更加明显。

Average pooling over detected edges.

最大池还是平均池?

在了解了最大和平均池过程之后,一个自然的反应是想知道对于计算机视觉应用来说,哪一个更优越。事实是,两种观点都有可能。

一方面,由于 max pooling 选择内核中捕获的最高像素值,它会产生更清晰的表示。

Comparing representations produced using both methods.

在卷积神经网络环境中,这意味着它可以更好地将检测到的边缘聚焦到特征地图中,如下图所示。

Comparing effect on edges.

另一方面,也可以支持平均池的观点,即平均池可以生成更一般化的要素地图。考虑我们的大小为(444,448)的参考图像,当与大小为(2,2)的核合并时,其合并表示的大小为(222,224),基本上是参考图像中总像素的 25%。因为 max pooling 基本上选择像素,一些人认为它会导致数据丢失,这可能对网络性能有害。相反,平均池不是选择像素,而是通过计算它们的平均值将像素合并成一个像素,因此一些人认为平均池只是将像素压缩 75%,而不是显式地移除像素,这将产生更一般化的特征图,从而在对抗过拟合方面做得更好。

我属于分界线的哪一边?我个人认为 max pooling 在特征地图中进一步突出边缘的能力使其在计算机视觉/深度学习应用中具有优势,因此它更受欢迎。这并不是说使用平均池会严重降低网络性能,只是个人观点。

结束语

在本文中,我们对卷积神经网络环境下的池化有了直观的认识。我们已经研究了两种主要的池化类型,以及每种类型所产生的池化表示的差异。

对于 CNN 中关于池化的所有讨论,请记住,目前大多数架构都倾向于使用步长卷积层,而不是用于下采样的池化层,因为它们可以降低网络的复杂性。无论如何,池仍然是卷积神经网络的一个重要组成部分。

流行深度学习架构综述:AlexNet、VGG16 和 GoogleNet

原文:https://blog.paperspace.com/popular-deep-learning-architectures-alexnet-vgg-googlenet/

从图像识别到图像生成和标记的问题已经从各种深度学习(DL)架构进步中受益匪浅。理解不同 DL 模型的复杂性将有助于您理解该领域的发展,并找到适合您试图解决的问题的方法。

在过去的几年中,出现了许多体系结构,它们在许多方面都有所不同,比如层的类型、超参数等。在这个系列中,我们将回顾几个最著名的 DL 架构,它们定义了这个领域,并重新定义了我们解决关键问题的能力。

在本系列的第一部分,我们将介绍 2012 年至 2014 年发布的“早期”车型。这包括:

  • AlexNet
  • VGG16
  • 谷歌网

在第 2 部分中,我们将介绍 ResNet、InceptionV3 和 SqueezeNet 。第 3 部分将介绍 DenseNet、ResNeXt、MnasNet 和 ShuffleNet v2。

AlexNet (2012 年)

AlexNet 是迄今为止最流行的神经网络架构之一。它是由 Alex Krizhevsky 为 ImageNet 大规模视觉识别挑战(ils RV)提出的,基于卷积神经网络。ILSVRV 评估用于对象检测和图像分类的算法。2012 年,Alex Krizhevsky 等人发表了 用深度卷积神经网络 进行 ImageNet 分类。这是第一次听说 AlexNet 的时候。

面临的挑战是开发一个深度卷积神经网络,将 ImageNet LSVRC-2010 数据集中的 120 万幅高分辨率图像分类为 1000 多个不同的类别。该架构实现了 15.3%的前 5 个错误率(在模型的前 5 个预测中找不到给定图像的真实标签的比率)。第二好的成绩是 26.2%,远远落后。

AlexNet 架构

该架构总共由八层组成,其中前五层是卷积层,后三层是全连接的。前两个卷积层连接到重叠的最大池层,以提取最大数量的特征。第三、第四和第五卷积层直接连接到全连接层。卷积和全连接层的所有输出都连接到 ReLu 非线性激活函数。最终输出层连接到 softmax 激活层,生成 1000 个类别标签的分布。

AlexNet Architecture

网络的输入维度为(256 × 256 × 3),这意味着 AlexNet 的输入是(256 × 256)像素的 RGB (3 通道)图像。该架构涉及超过 6000 万个参数和 65 万个神经元。为了减少训练过程中的过拟合,网络使用了脱落层。被“放弃”的神经元对正向传递没有贡献,也不参与反向传播。这些层存在于前两个完全连接的层中。

AlexNet 培训和结果

该模型使用随机梯度下降优化函数,批量、动量和重量衰减分别设置为 128、0.9 和 0.0005。所有层都使用 0.001 的相等学习率。为了解决训练过程中的过度拟合问题,AlexNet 同时使用了数据扩充层和数据删除层。在两个 GTX 580 3GB GPU 上训练 90 个周期大约需要 6 天时间。

以下是使用 AlexNet 架构获得的结果截图:

Results Using AlexNet on the ImageNet Dataset

关于在 ILSVRC-2010 数据集上的结果,AlexNet 在比赛进行时取得了 37.5%和 17.0%的 top-1 和 top-5 测试集错误率。

PyTorch 和 TensorFlow 等流行的深度学习框架现在已经有了 AlexNet 等架构的基本实现。下面是几个相关的链接,帮助你自己实现它。

  1. PyTorch AlexNet 型号
  2. Tensorflow AlexNet 模型
  3. AlexNet 的 Keras 实现
  4. 其他参考:了解 AlexNet
  5. 原文:https://papers.nips.cc/paper/4824-imagenet-classification-with-deep-convolutional-neural-networks.pdf

VGG16 (2014 年)

VGG 是由牛津大学的卡伦·西蒙扬和安德鲁·齐泽曼提出的一种流行的神经网络架构。它也基于 CNN,并应用于 2014 年的 ImageNet 挑战赛。作者在他们的论文中详细介绍了他们的工作, 用于大规模图像识别的非常深的卷积网络 。该网络在 ImageNet 数据集上取得了 92.7%的前 5 名测试准确率。

与 AlexNet 相比,VGG 的主要改进包括相继使用大内核大小的滤波器(在第一和第二卷积层中分别为 11 和 5)和多个(3×3)内核大小的滤波器。

VGG 体系结构

架构的输入尺寸固定为图像尺寸(244 × 244)。在预处理步骤中,从图像中的每个像素中减去平均 RGB 值。

Source: Step by step VGG16 implementation in Keras for beginners

预处理完成后,图像被传递到一堆卷积层,这些卷积层带有尺寸为(3×3)的小型感受野滤波器。在一些配置中,滤波器大小设置为(1 × 1),这可视为输入通道的线性变换(后跟非线性)。

卷积运算的步距固定为 1。空间池由五个最大池层执行,这五个最大池层跟随几个卷积层。最大池在(2 × 2)像素窗口上执行,步长设置为 2。

全连接层的配置总是相同的;前两层各有 4096 个通道,第三层执行 1000 路 ILSVRC 分类(因此包含 1000 个通道,每个类别一个通道),最后一层是 softmax 层。VGG 网络的所有隐藏层之后是 ReLu 激活函数。

VGG 配置、培训和结果

VGG 网络有五种配置,分别命名为 A 到 e。配置的深度从左(A)到右(B)增加,并增加了更多的层。下表描述了所有可能的网络架构:

所有配置都遵循架构中的通用模式,只是深度不同;从网络 A 中的 11 个权重层(8 个卷积层和 3 个全连接层),到网络 E 中的 19 个权重层(16 个卷积层和 3 个全连接层)。卷积层的信道数量相当少,从第一层的 64 开始,然后在每个最大汇集层之后增加 2 倍,直到达到 512。下图显示了参数的总数(以百万计):

在 VGG 网络上训练图像使用类似于前面提到的 Krizhevsky 等人的技术(即 AlexNet 的训练)。当涉及多尺度训练图像时,只有少数例外。整个训练过程通过使用基于反向传播的小批量梯度下降优化多项式逻辑回归目标来执行。批量大小和动量分别设置为 256 和 0.9。为前两个完全连接的层添加了下降正则化,将下降率设置为 0.5。网络的学习率最初设置为 0.001,然后当验证集精度停止提高时,学习率降低 10 倍。总的来说,学习率降低了 3 倍,并且在 370,000 次迭代(74 个时期)之后停止学习。

在 ILSVRC-2012 和 ILSVRC-2013 比赛中,VGG16 都明显优于上一代车型。关于单网性能,VGG16 架构取得了最好的结果(7.0%测试误差)。下表显示了错误率。

关于硬件和训练时间,VGG 网络使用英伟达的泰坦黑色 GPU 进行了数周的训练。

如果你使用 VGG 网络,有两个主要缺点值得注意。第一,训练要花很多时间。第二,网络架构权重相当大。由于其深度和全连接节点的数量,训练的 VGG16 模型超过 500MB。VGG16 用于很多深度学习图像分类问题;然而,更小的网络架构往往更可取(如 SqueezeNet、GoogleNet 等。)

PyTorch 和 TensorFlow 等流行的深度学习框架都有 VGG16 架构的基本实现。下面是几个相关链接。

  1. PyTorch VGG 实现
  2. 张量流 VGG 实现
  3. 链接到原始研究论文

谷歌网(2014 年)

初始网络是神经网络领域的重大突破之一,尤其是对 CNN 而言。迄今为止,盗梦网络有三个版本,分别命名为盗梦版本 1、2 和 3。第一个版本于 2014 年进入该领域,正如其名称“GoogleNet”所示,它是由谷歌的一个团队开发的。该网络负责在 ILSVRC 中建立新的分类和检测技术。这个初始网络的第一个版本被称为 GoogleNet

如果网络由许多深层构成,它可能会面临过度拟合的问题。为了解决这个问题,研究论文中的作者提出了 GoogleNet 架构,其想法是拥有可以在同一级别上操作的多种大小的过滤器。有了这个想法,网络实际上变得更宽而不是更深。下图显示了一个简单的初始模块。

从上图可以看出,卷积运算是对三种滤波器大小的输入执行的:(1 × 1)、(3 × 3)和(5 × 5)。还对卷积执行最大池操作,然后将其发送到下一个初始模块。

由于训练神经网络既耗时又昂贵,作者通过在(3 × 3)和(5 × 5)卷积之前添加额外的(1 × 1)卷积来限制输入通道的数量,以降低网络的维数并执行更快的计算。下面的图片展示了一个简单的 Inception 模块。

这些是谷歌网的组成部分。下面是关于其架构的详细报告。

谷歌网络架构

GoogleNet 架构有 22 层,包括 27 个池层。总共有 9 个初始模块线性堆叠。初始模块的末端连接到全局平均池层。下面是整个 GoogleNet 架构的缩小图。

The Orange Box in the architecture is the stem that has few preliminary convolutions. The purple boxes are the auxiliary classes. (Image Credits: A Simple Guide to the Versions of the Inception Network).

下图解释了详细的架构和参数。

GoogleNet 培训和结果

GoogleNet 使用分布式机器学习系统进行训练,具有适度的模型和数据并行性。训练使用动量为 0.9 的异步随机梯度下降和每 8 个时期将学习率降低 4%的固定学习率时间表。下面是参加 ILSVRC 2014 的团队的成绩图像。谷歌网以 6.67%的错误率位居第一。

下面是几个相关链接,如果你对使用或实现 GoogleNet 感兴趣,我鼓励你去看看。

  1. 链接到原始研究论文
  2. GoogleNet 的 PyTorch 实现
  3. Google net 的 Tensorflow 实现

在本系列的下一部分,我们将回顾 2015 年至 2016 年发布的流行深度学习架构,包括 ResNet、InceptionV3 和 SqueezeNet

流行深度学习架构综述:DenseNet、ResNeXt、MnasNet 和 ShuffleNet v2

原文:https://blog.paperspace.com/popular-deep-learning-architectures-densenet-mnasnet-shufflenet/

这个由三部分组成的系列的目的是揭示深度学习模型的前景和发展,这些模型定义了该领域并提高了我们解决挑战性问题的能力。在第 1 部分中,我们介绍了 2012-2014 年开发的模型,即 AlexNet、VGG16 和 GoogleNet 。在第二部分中,我们看到了 2015-2016 年更近的模型: ResNet,InceptionV3,和 SqueezeNet 。既然我们已经介绍了过去流行的架构和模型,我们将继续介绍最新的技术。

我们将在此讨论的架构包括:

  • DenseNet
  • ResNeXt
  • MnasNet
  • ShuffleNet v2

让我们开始吧。

DenseNet (2016)

“DenseNet”这个名字指的是密集连接的卷积网络。这是由黄高、刘庄和他们的团队在 2017 年 CVPR 会议上提出的。它获得了最佳论文奖,并被引用了 2000 多次。

传统的 n 层的卷积网络有 n 个连接;每层与其后续层之间有一个。在 DenseNet 中,每一层都以前馈方式连接到其他每一层,这意味着 DenseNet 总共有 n ( n +1)/2 个连接。对于每一层,所有先前层的特征图被用作输入,并且它自己的特征图被用作所有后续层的输入。

密集块

DenseNet 比传统的深度 CNN 有一个很大的优势:通过许多层传递的信息在到达网络末端时不会被洗掉或消失。这是通过一个简单的连接模式实现的。为了理解这一点,人们必须知道普通 CNN 中的层是如何连接的。

这是一个简单的 CNN,其中各层是顺序连接的。但是,在密集块中,每一层都从所有前面的层获得额外的输入,并将自己的要素地图传递给所有后面的层。下图描绘了致密的块状物。

随着网络中的图层从所有先前图层接收要素地图,网络将变得更细、更紧凑。下面是一个通道数设置为 4 的 5 层密集块。

DenseNet 建筑

DenseNet 已经应用于各种不同的数据集。基于输入的维度,使用不同类型的密集块。下面是对这些层的简要描述。

  • **基本 DenseNet 组合层:**在这种类型的密集块中,每一层后面都有一个预激活的批量标准化层、ReLU 激活函数和一个 3×3 卷积。下面是一个快照。

  • 瓶颈 DenseNet (DenseNet-B): 由于每一层都会产生 k 输出特征图,因此每一层的计算都会更加困难。因此,作者提出了一种瓶颈结构,其中在 3×3 卷积层之前使用 1×1 卷积,如下所示。

  • **DenseNet 压缩:**为了提高模型的紧凑性,作者尝试减少过渡层的特征图。因此,如果密集块由 m 个特征图组成,并且过渡层生成 i 个输出特征图,其中 0<I= 1,这个 i 也表示压缩因子。如果 I 的值等于 1(I = 1),则过渡层上的特征图数量保持不变。如果 i < 1,则该架构被称为 DenseNet-C,并且 i 的值将变为 0.5。当瓶颈层和带有 i < 1 的过渡层都被使用时,我们称我们的模型为 DenseNet-BC。
  • **带过渡层的多个密集块:**架构中的密集块后面是 1×1 卷积层和 2×2 平均池层。由于要素地图大小相同,因此很容易连接过渡图层。最后,在密集块的末尾,执行附加到 softmax 分类器的全局平均池。

DenseNet 培训和结果

原始研究论文中定义的 DenseNet 架构应用于三个数据集:CIFAR、SVHN 和 ImageNet。所有架构都使用随机梯度下降优化器进行训练。CIFAR 和 SVHN 的训练批次大小分别为 64、300 和 40 个时期。初始学习率设置为 0.1,并进一步降低。以下是在 ImageNet 上培训的 DenseNet 的指标:

  • 批量:256 个
  • 纪元:90 年
  • 学习率:0.1,在 30 岁和 60 岁时下降了 10 倍
  • 重量衰减和动量:0.00004 和 0.9

以下是详细的结果,显示了 DenseNet 的不同配置与 CIFAR 和 SVHN 数据集上的其他网络相比的差异。蓝色数据表示最佳结果。

以下是 ImageNet 上不同大小的 DenseNet 的前 1 名和前 5 名错误。

如果您想了解原始论文、其实现或如何自己实现 DenseNet,下面是一些相关链接:

  1. 本综述中的大部分图像摘自 Sik-Ho Tsang 的原始研究论文( DenseNet )和文章【T2 综述:Dense net-密集卷积网络(图像分类)】
  2. 原始纸张对应的代码
  3. dense net 的 TensorFlow 实现
  4. PyTorch 实现 DenseNet

ResNeXt (2017)

ResNeXt 是一个同质神经网络,它减少了传统 ResNet 所需的超参数数量。这是通过使用“基数”来实现的,基数是 ResNet 的宽度和深度之上的一个额外维度。基数定义了转换集的大小。

在此图中,最左边的图是一个传统的 ResNet 块;最右边的是 ResNeXt 块,基数为 32。相同的转换应用了 32 次,最后汇总结果。这项技术是在 2017 年的论文 中提出的,该论文题为《深度神经网络 的聚合残差变换》,由谢赛宁、罗斯·吉希克、彼得·多拉尔、·涂和何合著,他们都在人工智能研究所工作。

VGG 网络雷斯网和初始网络已经在特征工程领域获得了很大的发展势头。尽管他们表现出色,但仍面临一些限制。这些模型非常适合几个数据集,但由于涉及许多超参数和计算,使它们适应新的数据集不是一件小事。为了克服这些问题,考虑了 VGG/ResNet(从 VGG 发展而来的 ResNet)和 Inception 网络的优点。一言以蔽之,ResNet 的重复策略与 Inception Network 的拆分-转换-合并策略相结合。换句话说,网络块分割输入,将其转换为所需的格式,并将其合并以获得输出,其中每个块遵循相同的拓扑结构。

ResNeXt 架构

ResNeXt 的基本架构由两条规则定义。首先,如果块产生相同维度的空间图,它们共享相同的超参数集,并且如果空间图以因子 2 被下采样,则块的宽度乘以因子 2。

如表中所示,ResNeXt-50 的基数 32 重复了 4 次(深度)。[]中的尺寸表示剩余块结构,而写在它们旁边的数字表示堆叠块的数量。32 精确地表示在分组卷积中有 32 个组。

上述网络结构解释了什么是分组卷积,以及它如何胜过其他两种网络结构。

  • (a)表示以前已经见过的普通 ResNeXt 块。它的基数为 32,遵循分割-转换-合并策略。
  • (b)看起来确实像是《盗梦空间》的一片叶子。然而,Inception 或 Inception-ResNet 没有遵循相同拓扑的网络块。
  • (c)与 AlexNet 架构中提出的分组卷积相关。如(a)和(b)所示,32*4 已被替换为 128。简而言之,这意味着分裂是由分组卷积层完成的。类似地,该变换由进行 32 组卷积的另一分组卷积层来完成。后来,连接发生了。

在上述三种方法中,(c)被证明是最好的,因为它易于实现。

resnet 培训和结果

ImageNet 已经被用来展示当基数而不是宽度/深度被考虑时准确性的提高。

当基数较高时,ResNeXt-50 和 ResNeXt-101 都不太容易出错。此外,与 ResNet 相比,ResNeXt 表现良好。

下面是一些重要的链接,

  1. 链接到原始研究论文
  2. PyTorch 实现 ResNext
  3. ResNext 的 Tensorflow 实现

ShuffleNet v2 (2018)

ShuffleNet v2 考虑直接指标,如速度或内存访问成本,来衡量网络的计算复杂性(除了 FLOPs,它还充当间接指标)。此外,直接指标也在目标平台上进行评估。ShuffleNet v2 就是这样在 2018 年出版的论文 ShuffleNet V2:高效 CNN 架构设计实用指南 中介绍的。它是由马宁宁、张翔宇、郑海涛和孙健共同撰写的。

FLOPs 是衡量网络计算性能的常用指标。然而,一些研究证实了这样一个事实,即失败并不能完全挖掘出潜在的真理;具有类似 FLOPs 的网络在速度上有所不同,这可能是因为内存访问成本、并行度、目标平台等。所有这些都不属于失败,因此,被忽视。ShuffleNet v2 通过提出建模网络的四个准则克服了这些麻烦。

ShuffleNet v2 架构

在了解网络体系结构之前,网络构建所依据的指导原则应简要介绍如何考虑各种其他直接指标:

  1. 等通道宽度最小化内存访问成本:当输入通道和输出通道的数量同比例(1:1)时,内存访问成本变低。
  2. 过多的组卷积增加了访存成本:组数不宜过高,否则访存成本有增加的趋势。
  3. 网络碎片降低了并行度:碎片降低了网络执行并行计算的效率。
  4. 基于元素的操作是不可忽略的:基于元素的操作有小的触发器,但是会增加内存访问时间。

所有这些都集成在 ShuffleNet v2 架构中,以提高网络效率。

信道分割操作符将信道分为两组,其中一组作为身份保留(第 3 条准则)。另一个分支沿着三个回旋具有相同数量的输入和输出通道(第一条准则)。1x1 卷积不是分组的(第二条准则)。像 ReLU、Concat、深度卷积这样的基于元素的操作被限制在一个分支中(4 准则)。

ShuffleNet v2 的整体架构列表如下:

结果是关于输出声道的不同变化。

ShuffleNet v2 培训和结果

Imagenet 已被用作数据集,以获得各种数据集的结果。

复杂度、错误率、GPU 速度和 ARM 速度已经被用于在预期的模型中导出稳健且有效的模型。虽然 ShuffleNet v2 缺乏 GPU 速度,但它记录了最低的 top-1 错误率,这超过了其他限制。

下面是几个额外的链接,你可能会对自己实现 ShuffleNet 感兴趣,或者深入阅读原文。

  1. 链接到原始研究论文
  2. shuffle net v2 的 Tensorflow 实现
  3. PyTorch 实现 ShuffleNet V2

多边核安全网络(2019 年)

MnasNet 是一个自动化的移动神经架构搜索网络,用于使用强化学习来构建移动模型。它结合了 CNN 的基本精髓,从而在提高准确性和减少延迟之间取得了正确的平衡,以在模型部署到移动设备上时描绘高性能。这个想法是在 2019 年出的论文 MnasNet:平台感知的神经架构搜索移动 中提出的。该论文由谭明星、陈博、庞若明、维杰·瓦苏德万、马克·桑德勒、安德鲁·霍华德和安德鲁·霍华德共同撰写,他们都属于谷歌大脑团队。

到目前为止开发的传统移动 CNN 模型,当考虑等待时间和准确性时,不能产生正确的结果;他们在这两方面都有所欠缺。通常使用 FLOPS 来估计延迟,这不会输出正确的结果。然而,在 MnasNet 中,模型被直接部署到移动设备上,并且对结果进行估计;不涉及代理。移动设备通常是资源受限的,因此,诸如性能、成本和延迟等因素是需要考虑的重要指标。

MnasNet 架构

该架构通常由两个阶段组成-搜索空间和强化学习方法。

  • 分解的分层搜索空间:搜索空间支持在整个网络中包含不同的层结构。CNN 模型被分解成各种块,其中每个块具有唯一的层架构。连接的选择应使输入和输出相互兼容,从而产生良好的结果以保持较高的准确率。下面是搜索空间的样子:

可以注意到,搜索空间由几个块组成。所有层都根据其尺寸和过滤器大小进行隔离。每个块都有一组特定的层,在这些层中可以选择操作(如蓝色所示)。如果输入或输出维度不同,则每个块中的第一层的跨度为 2,其余层的跨度为 1。从第二层开始到第 N 层重复相同的一组操作,其中 N 是块号。

  • 强化搜索算法:因为我们有两个主要目标要实现——延迟和准确性,所以我们采用了一种强化学习方法,其中回报是最大化的(多目标回报)。在搜索空间中定义的每个 CNN 模型将被映射到由强化学习代理执行的一系列动作。

这就是搜索算法中存在的东西——控制器是递归神经网络(RNN),训练器训练模型并输出精度。该模型被部署到移动电话上以估计延迟。准确性和等待时间被合并成一个多目标的奖励。该奖励被发送到 RNN,使用该奖励更新 RNN 的参数,以最大化总奖励。

MnasNet 培训和结果

与其他传统的移动 CNN 模型相比,Imagenet 已被用于描述 MnasNet 模型所实现的准确性。这是一个代表相同情况的表格:

MnasNet 无疑减少了延迟,提高了准确性。

如果您想查看原始论文或在 PyTorch 中自己实现 MnasNet,请查看以下链接:

  1. 链接到原始研究论文
  2. PyTorch 实施 MnasNet

这就结束了我们的三部分系列,涵盖了定义该领域的流行深度学习架构。如果你还没有,请随意查看第一部分和第二部分的,它们涵盖了 ResNet、Inception v3、AlexNet 等模型。我希望这个系列对你有用。

流行深度学习架构回顾:ResNet、InceptionV3 和 SqueezeNet

原文:https://blog.paperspace.com/popular-deep-learning-architectures-resnet-inceptionv3-squeezenet/

之前我们看了 2012-2014 年定义领域的深度学习模型,即 AlexNet、VGG16 和 GoogleNet 。这一时期的特点是大型模型,长时间的培训,以及延续到生产的困难。

在本文中,我们将深入探讨这一时期(2015-2016 年)之后出现的模型。仅在这两年中就取得了巨大的进步,导致了精确度和性能的提高。具体来说,我们将了解:

  • ResNet
  • Wide ResNet
  • InceptionV3
  • 斯奎泽尼

让我们开始吧。

ResNet (2015)

由于深度神经网络的训练既耗时又容易过度拟合,微软的一个团队引入了一个残差学习框架,以改善比以前使用的网络更深的网络训练。这项研究发表在 2015 年题为 图像识别的深度残差学习 的论文中。就这样,著名的 ResNet(残余网络的简称)诞生了。

当训练深度网络时,深度的增加会导致精度饱和,然后迅速下降。这被称为“退化问题”这突出了并非所有的神经网络架构都同样容易优化。

ResNet 使用一种称为“残差映射”的技术来解决这个问题。残差网络明确地让这些层拟合残差映射,而不是希望每几个堆叠层直接拟合所需的底层映射。下面是剩余网络的构建模块。

F(x)+x 的公式可以通过具有快捷连接的前馈神经网络来实现。

许多问题可以使用 ResNets 解决。当网络深度增加时,它们易于优化并实现更高的精度,产生比以前的网络更好的结果。像我们在第一部分中介绍的前辈一样,ResNet 首先在 ImageNet 的 120 多万张属于 1000 个不同类别的训练图像上进行训练和测试。

ResNet 架构

与传统的神经网络结构相比,ResNets 相对容易理解。下图是一个 VGG 网络、一个普通的 34 层神经网络和一个 34 层残差神经网络。在平面网络中,对于相同的输出要素地图,图层具有相同数量的过滤器。如果输出特征的大小减半,则过滤器的数量加倍,使得训练过程更加复杂。

同时,在残差神经网络中,正如我们可以看到的,在关于 VGG 的训练期间,有少得多的滤波器和较低的复杂度。添加了一个快捷连接,将网络转换为其对应的剩余版本。这种快捷连接执行标识映射,并为增加的维度填充额外的零条目。该选项不引入额外的参数。投影快捷方式在数学上表示为 F(x+x),用于匹配由 1×1 卷积计算的维度。

下表显示了不同 ResNet 架构中的层和参数。

每个 ResNet 块要么是两层深(用于 ResNet 18 或 34 等小型网络),要么是三层深(ResNet 50、101 或 152)。

ResNet 培训和结果

来自 ImageNet 数据集的样本被重新缩放到 224 × 224,并通过每像素均值减法进行归一化。随机梯度下降用于最小批量为 256 的优化。学习率从 0.1 开始,误差增大时除以 10,模型训练到 60 × 104 次迭代。重量衰减和动量分别设定为 0.0001 和 0.9。不使用脱落层。

ResNet 在更深层次的架构中表现非常好。下图显示了两个 18 层和 34 层神经网络的错误率。左边的图显示了普通网络,而右边的图显示了它们的 ResNet 等价物。图像中细的红色曲线表示训练错误,粗的曲线表示验证错误。

下表显示了 ImageNet 验证中最大的错误(%),10 次裁剪测试。

正如我们今天所知,ResNet 在定义深度学习领域方面发挥了重要作用。

如果您对自己实现 ResNet 感兴趣,下面是一些重要的链接:

  1. PyTorch ResNet 实现
  2. Tensorflow ResNet 实现
  3. 链接到原始研究论文

Wide ResNet (2016)

宽残差网络是对原始深残差网络的更新改进。研究表明,在不影响网络性能的情况下,可以将网络做得更浅更宽,而不是依靠增加网络的深度来提高其精度。这种思想在论文 广残网 中提出,该论文于 2016 年发表(并由 Sergey Zagoruyko 和 Nikos Komodakis 于 2017 年更新。

深度残差网络已经显示出显著的准确性,有助于相对容易地执行图像识别等任务。尽管如此,深层网络仍然面临着网络退化以及爆炸或消失梯度的挑战。深度残余网络也不能保证所有残余块的使用;只有几个可以被跳过,或者只有几个可以进入更大的贡献块(即提取有用的信息)。这个问题可以通过禁用随机块来解决——这是广义的辍学。从这个想法得到启示,Wide ResNet 的作者已经证明了宽残差网络甚至可以比深残差网络表现得更好。

宽 ResNet 架构

宽 ResNet 有一组堆叠在一起的 ResNet 块,其中每个 ResNet 块都遵循 BatchNormalization-ReLU-Conv 结构。该结构描述如下:

有五个组组成了一个大范围的搜索网。这里的块是指残差块 B(3,3)。Conv1 在任何网络中都保持不变,而 conv2、conv3 和 conv4 则根据 k 、定义宽度的值而变化。卷积层之后是平均池层和分类层。

可以调整以下可变度量,以得出残差块的各种表示:

  • **卷积类型:**上图 B(3,3)在残差块中有 3 × 3 个卷积层。也可以探索其他可能性,例如集成 3 × 3 卷积层和 1 × 1 卷积。
  • **每个块的卷积:**块的深度必须通过估计该度量对模型性能的依赖性来确定。
  • **残差块的宽度:**必须一致地检查宽度和深度,以关注计算复杂度和性能。
  • Dropout: 在每一个残差块的卷积之间都要增加一个 Dropout 层。这可以防止过度拟合。

广泛的 ResNet 培训和结果

CIFAR-10 上训练了 Wide ResNet。以下指标导致了最低的错误率:

  • 卷积类型:B(3,3)
  • 每个残差块的卷积层数:2。因此,B(3,3)是首选维度(而不是 B(3,3,3)或 B(3,3,3,3)。
  • 剩余块的宽度:深度为 28,宽度为 10 似乎不太容易出错。
  • 辍学:当辍学被包括在内时,错误率进一步降低。比较结果如下图所示。

下表比较了 CIFAR-10 和 CIFAR-100 上的 Wide ResNet 与其他几个型号(包括原始 ResNet)的复杂性和性能:

以下是几个你自己实现 Wide ResNet 的重要环节:

  1. 链接到原论文
  2. PyTorch 实现宽 ResNet
  3. Tensorflow 实现宽 ResNet

盗梦空间第三版(2015)

Inception v3 主要关注通过修改以前的 Inception 架构来消耗更少的计算能力。这个想法是在 2015 年发表的论文 重新思考计算机视觉 的盗梦架构中提出的。该书由克里斯蒂安·塞格迪、文森特·范霍克、谢尔盖·约菲和黄邦贤·史伦斯合著。

与 VGGNet 相比,Inception Networks(Google net/Inception v1)已证明在网络生成的参数数量和产生的经济成本(内存和其他资源)方面计算效率更高。如果要对初始网络进行任何更改,需要注意确保计算优势不会丧失。因此,由于新网络效率的不确定性,针对不同用例的初始网络的适应变成了一个问题。在 Inception v3 模型中,已经提出了几种用于优化网络的技术来放松约束,以便更容易地适应模型。这些技术包括分解卷积、正则化、降维以及并行计算。

Inception v3 架构

Inception v3 网络的架构是逐步构建的,如下所述:

**1。因式分解卷积:**这有助于降低计算效率,因为它减少了网络中涉及的参数数量。它还保持对网络效率的检查。

2。更小的卷积:用更小的卷积替换更大的卷积肯定会导致更快的训练。假设一个 5 × 5 的滤波器有 25 个参数;代替 5 × 5 卷积的两个 3 × 3 滤波器只有 18 (33 + 33)个参数。

In the middle we see a 3x3 convolution, and below a fully-connected layer. Since both 3x3 convolutions can share weights among themselves, the number of computations can be reduced.

**3。不对称卷积:**一个 3 × 3 卷积可以被一个 1 × 3 卷积和一个 3 × 1 卷积代替。如果用 2 × 2 卷积代替 3 × 3 卷积,参数的数量将比提出的非对称卷积略高。

**4。辅助分类器:**辅助分类器是训练时插在层间的小 CNN,产生的损耗加到主网损耗上。在 GoogLeNet 中,辅助分类器用于更深的网络,而在 Inception v3 中,辅助分类器充当正则化器。

**5。网格尺寸缩减:**网格尺寸缩减通常通过池化操作来完成。然而,为了克服计算成本的瓶颈,提出了一种更有效的技术:

上述所有概念都被整合到最终的架构中。

Inception v3 培训和结果

Inception v3 在 ImageNet 上进行了训练,并与其他当代模型进行了比较,如下所示。

如表中所示,当使用辅助分类器、卷积因子分解、RMSProp 和标签平滑进行增强时,Inception v3 可以实现与其同时代产品相比最低的错误率。

如果你想自己实现 Inception v3,下面是一些相关的链接:

  1. 链接到原始研究论文
  2. TensorFlow 实现 Inception v3
  3. PyTorch 实施 Inception v3

squeeze et(2016 年)

SqueezeNet 是一个较小的网络,被设计为 AlexNet 的更紧凑的替代品。它的参数比 AlexNet 少了近 50 倍,但执行速度却快了 3 倍。这个架构是由 DeepScale、加州大学伯克利分校和斯坦福大学的研究人员在 2016 年提出的。它最早发表在他们题为 SqueezeNet: AlexNet 级别的精度,参数少 50 倍,< 0.5MB 模型大小 的论文中。

以下是 SqueezeNet 背后的关键理念:

  • **策略一:**用 1 × 1 的滤镜代替 3 × 3 的滤镜
  • **策略二:**将输入通道的数量减少到 3 × 3 个滤波器
  • **策略三:**在网络后期向下采样,以便卷积层具有大的激活图

SqueezeNet 架构和结果

SqueezeNet 架构由“挤压”和“扩展”层组成。一个压缩卷积层只有 1 × 1 个滤波器。这些信号被送入一个扩展层,该层混合了 1 × 1 和 3 × 3 卷积滤波器。如下所示。

A "Fire Module"

本文作者使用术语“火灾模块”来描述挤压层和膨胀层。

输入图像首先被发送到独立的卷积层。根据上面的策略一,这一层之后是 8 个“消防模块”,它们被命名为“消防 2-9”。下图显示了生成的挤压网。

From left to right: SqueezeNet, SqueezeNet with simple bypass, and SqueezeNet with complex bypass

按照策略二,每个火灾模块的过滤器通过“简单旁路”增加最后,SqueezeNet 在层 conv1、fire4、fire8 和 conv10 之后以 2 的步长执行最大池化。根据策略三,池被给予相对较晚的位置,导致 SqueezeNet 具有“复杂旁路”(上图中最右边的架构)

下图显示了 SqueezeNet 与原版 AlexNet 的对比。

正如我们所观察到的,AlexNet 的压缩模型的权重是 240MB,并且达到了 80.3%的准确率。同时,深度压缩 SqueezeNet 消耗 0.47MB 的内存并实现相同的性能。

以下是网络中使用的其他参数的详细信息:

  • ReLU 激活应用于 fire 模块内部的所有挤压和扩展层之间。
  • 在 fire9 模块之后,添加了 Dropout 层以减少过度拟合,概率为 0.5。
  • 网络中没有使用完全连接的层。这一设计选择的灵感来自于(林等,2013)提出的网络(NIN) 架构中的网络。
  • 以 0.04 的学习率训练 SqueezeNet,该学习率在整个训练过程中线性下降。
  • 训练的批量大小是 32,并且网络使用了 Adam 优化器。

SqueezeNet 由于体积小,使得部署过程更加容易。最初,这个网络是在 Caffe 中实现的,但这个模型后来越来越流行,并被许多不同的平台采用。

下面是几个相关的链接,可以让你自己实现 SqueezeNet,或者进一步研究最初的实现:

  1. 链接到 SqueezeNet 的原始实现
  2. 链接到研究论文
  3. 在 tensorlow中的 squeezenet
  4. pytorch 中的 squeezenet】

在本系列的第三和最后部分,我们将涵盖 2017 年至 2019 年最新发布的模型:DenseNet、ResNeXt、MnasNet 和 ShuffleNet v2。

用 postnet 构建一个眼睛过滤器的应用程序

原文:https://blog.paperspace.com/posenet-keypoint-detection-android-app/

姿态估计是用于检测物体姿态(即方向和位置)的计算机视觉任务。它通过检测一些关键点来工作,这样我们就可以了解物体的主要部分,并估计其当前的方向。基于这样的关键点,我们将能够在 2D 或 3D 中形成物体的形状。

本教程介绍了如何构建一个 Android 应用程序,使用预训练的 TFLite PoseNet 模型来估计独立 RGB 图像中的人体姿势。该模型预测了人体 17 个关键点的位置,包括眼睛、鼻子、肩膀等的位置。通过估计关键点的位置,我们将在第二个教程中看到如何使用该应用程序来制作特殊效果和滤镜,就像你在 Snapchat 上看到的那样。

本教程的大纲如下:

  • 基本 Android 工作室项目
  • 加载图库图像
  • 裁剪和缩放图像
  • 利用 PoseNet 估计人体姿态
  • 获取关于关键点的信息
  • PosenetActivity.kt的完整代码

让我们开始吧。

Base Android Studio 项目

我们将使用tensor flow Lite pose net Android 演示作为节省时间的起点。我们先来讨论一下这个项目是如何运作的。然后我们会根据自己的需要进行编辑。

该项目使用预训练的 PoseNet 模型,这是 MobileNet 的转换版本。PoseNet 模型可在此链接下载。该模型接受大小为(257, 257)的图像,并返回以下 17 个关键点的位置:

  1. 鼻子
  2. 左眼,左眼
  3. 正确地
  4. 左耳
  5. 战士
  6. 左肩膀
  7. 右肩
  8. 左肘
  9. 右肘
  10. 左手腕
  11. 右手腕
  12. 左臀
  13. 权利
  14. 左膝
  15. 右膝盖
  16. 左脚踝
  17. 右脚踝

对于每个关键点,都有一个表示置信度的相关值,范围从 0.0。到 1.0。因此,该模型返回两个列表:一个表示关键点位置,另一个包含每个关键点的置信度。由您设置置信度阈值,将候选关键点分类为接受或拒绝。通常,当阈值为 0.5 或更高时,就做出了好的决定。

该项目是用 Kotlin 编程语言实现的,并访问 Android 摄像头来捕捉图像。对于每个捕捉的图像,该模型预测关键点的位置,并显示这些关键点重叠的图像。

在本教程中,我们将尽可能地简化这个项目。首先,该项目将被编辑,以处理从图库中选择的单个图像,而不是用相机拍摄的图像。一旦我们有了单幅图像的结果,我们将在眼睛上添加一个遮罩,这是 Snapchat 等图像编辑应用程序中的一个已知效果。

下一节讨论从项目中移除不必要的代码。

删除不必要的代码

该项目被配置为处理从摄像机捕获的图像,这不是我们当前的目标。因此,任何与访问或捕捉图像相关的内容都应该删除。有三个文件需要编辑:

  • PosetnetActivity.kt活动文件。
  • activity_posenet.xml布局PosetnetActivity.kt活动的资源文件。
  • AndroidManifest.xml

PosenetActivity.kt文件开始,下面是要删除的代码行列表:

  • CameraDevice.StateCallback() 161:179
  • CameraCaptureSession.CaptureCallback() 184:198
  • onViewCreated() 216:219
  • onResume() 221:224
  • onPause() 232:236
  • requestCameraPermission() 243:249
  • openCamera() 327:345
  • closeCamera() 350:368
  • startBackgroundThread() 373:376
  • stopBackgroundThread() 381:390
  • fillBytes() 393:404
  • OnImageAvailableListener 407:451
  • createCameraPreviewSession() 598:655
  • setAutoFlash() 657:664

由于删除了前面的代码,因此不再需要以下变量:

  • PREVIEW_WIDTH : 97
  • PREVIEW_HEIGHT : 98
  • 从第 104 行到第 158 行定义的所有变量:cameraIdsurfaceViewcaptureSessioncameraDevicepreviewSizepreviewWidth等等。

完成前面的更改后,也不再需要这三行代码:

  • 第 228 行onStart()里面的方法:openCamera()
  • 第 578 行draw()方法:surfaceHolder!!.unlockCanvasAndPost(canvas)

下面是做出这些改变后的PosenetActivity.kt的当前形式。

package org.tensorflow.lite.examples.posenet

import android.Manifest
import android.app.AlertDialog
import android.app.Dialog
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.Rect
import android.os.Bundle
import android.support.v4.app.ActivityCompat
import android.support.v4.app.DialogFragment
import android.support.v4.app.Fragment
import android.util.Log
import android.util.SparseIntArray
import android.view.LayoutInflater
import android.view.Surface
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import kotlin.math.abs
import org.tensorflow.lite.examples.posenet.lib.BodyPart
import org.tensorflow.lite.examples.posenet.lib.Person
import org.tensorflow.lite.examples.posenet.lib.Posenet

class PosenetActivity :
  Fragment(),
  ActivityCompat.OnRequestPermissionsResultCallback {

  /** List of body joints that should be connected.    */
  private val bodyJoints = listOf(
    Pair(BodyPart.LEFT_WRIST, BodyPart.LEFT_ELBOW),
    Pair(BodyPart.LEFT_ELBOW, BodyPart.LEFT_SHOULDER),
    Pair(BodyPart.LEFT_SHOULDER, BodyPart.RIGHT_SHOULDER),
    Pair(BodyPart.RIGHT_SHOULDER, BodyPart.RIGHT_ELBOW),
    Pair(BodyPart.RIGHT_ELBOW, BodyPart.RIGHT_WRIST),
    Pair(BodyPart.LEFT_SHOULDER, BodyPart.LEFT_HIP),
    Pair(BodyPart.LEFT_HIP, BodyPart.RIGHT_HIP),
    Pair(BodyPart.RIGHT_HIP, BodyPart.RIGHT_SHOULDER),
    Pair(BodyPart.LEFT_HIP, BodyPart.LEFT_KNEE),
    Pair(BodyPart.LEFT_KNEE, BodyPart.LEFT_ANKLE),
    Pair(BodyPart.RIGHT_HIP, BodyPart.RIGHT_KNEE),
    Pair(BodyPart.RIGHT_KNEE, BodyPart.RIGHT_ANKLE)
  )

  /** Threshold for confidence score. */
  private val minConfidence = 0.5

  /** Radius of circle used to draw keypoints.  */
  private val circleRadius = 8.0f

  /** Paint class holds the style and color information to draw geometries,text and bitmaps. */
  private var paint = Paint()

  /** An object for the Posenet library.    */
  private lateinit var posenet: Posenet

  /**
   * Shows a [Toast] on the UI thread.
   *
   * @param text The message to show
   */
  private fun showToast(text: String) {
    val activity = activity
    activity?.runOnUiThread { Toast.makeText(activity, text, Toast.LENGTH_SHORT).show() }
  }

  override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
  ): View? = inflater.inflate(R.layout.activity_posenet, container, false)

  override fun onStart() {
    super.onStart()
    posenet = Posenet(this.context!!)
  }

  override fun onDestroy() {
    super.onDestroy()
    posenet.close()
  }

  override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<String>,
    grantResults: IntArray
  ) {
    if (requestCode == REQUEST_CAMERA_PERMISSION) {
      if (allPermissionsGranted(grantResults)) {
        ErrorDialog.newInstance(getString(R.string.request_permission))
          .show(childFragmentManager, FRAGMENT_DIALOG)
      }
    } else {
      super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    }
  }

  private fun allPermissionsGranted(grantResults: IntArray) = grantResults.all {
    it == PackageManager.PERMISSION_GRANTED
  }

  /** Crop Bitmap to maintain aspect ratio of model input.   */
  private fun cropBitmap(bitmap: Bitmap): Bitmap {
    val bitmapRatio = bitmap.height.toFloat() / bitmap.width
    val modelInputRatio = MODEL_HEIGHT.toFloat() / MODEL_WIDTH
    var croppedBitmap = bitmap

    // Acceptable difference between the modelInputRatio and bitmapRatio to skip cropping.
    val maxDifference = 1e-5

    // Checks if the bitmap has similar aspect ratio as the required model input.
    when {
      abs(modelInputRatio - bitmapRatio) < maxDifference -> return croppedBitmap
      modelInputRatio < bitmapRatio -> {
        // New image is taller so we are height constrained.
        val cropHeight = bitmap.height - (bitmap.width.toFloat() / modelInputRatio)
        croppedBitmap = Bitmap.createBitmap(
          bitmap,
          0,
          (cropHeight / 2).toInt(),
          bitmap.width,
          (bitmap.height - cropHeight).toInt()
        )
      }
      else -> {
        val cropWidth = bitmap.width - (bitmap.height.toFloat() * modelInputRatio)
        croppedBitmap = Bitmap.createBitmap(
          bitmap,
          (cropWidth / 2).toInt(),
          0,
          (bitmap.width - cropWidth).toInt(),
          bitmap.height
        )
      }
    }
    return croppedBitmap
  }

  /** Set the paint color and size.    */
  private fun setPaint() {
    paint.color = Color.RED
    paint.textSize = 80.0f
    paint.strokeWidth = 8.0f
  }

  /** Draw bitmap on Canvas.   */
  private fun draw(canvas: Canvas, person: Person, bitmap: Bitmap) {
    canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
    // Draw `bitmap` and `person` in square canvas.
    val screenWidth: Int
    val screenHeight: Int
    val left: Int
    val right: Int
    val top: Int
    val bottom: Int
    if (canvas.height > canvas.width) {
      screenWidth = canvas.width
      screenHeight = canvas.width
      left = 0
      top = (canvas.height - canvas.width) / 2
    } else {
      screenWidth = canvas.height
      screenHeight = canvas.height
      left = (canvas.width - canvas.height) / 2
      top = 0
    }
    right = left + screenWidth
    bottom = top + screenHeight

    setPaint()
    canvas.drawBitmap(
      bitmap,
      Rect(0, 0, bitmap.width, bitmap.height),
      Rect(left, top, right, bottom),
      paint
    )

    val widthRatio = screenWidth.toFloat() / MODEL_WIDTH
    val heightRatio = screenHeight.toFloat() / MODEL_HEIGHT

    // Draw key points over the image.
    for (keyPoint in person.keyPoints) {
      Log.d("KEYPOINT", "" + keyPoint.bodyPart + " : (" + keyPoint.position.x.toFloat().toString() + ", " + keyPoint.position.x.toFloat().toString() + ")");
      if (keyPoint.score > minConfidence) {
        val position = keyPoint.position
        val adjustedX: Float = position.x.toFloat() * widthRatio + left
        val adjustedY: Float = position.y.toFloat() * heightRatio + top
        canvas.drawCircle(adjustedX, adjustedY, circleRadius, paint)
      }
    }

    for (line in bodyJoints) {
      if (
        (person.keyPoints[line.first.ordinal].score > minConfidence) and
        (person.keyPoints[line.second.ordinal].score > minConfidence)
      ) {
        canvas.drawLine(
          person.keyPoints[line.first.ordinal].position.x.toFloat() * widthRatio + left,
          person.keyPoints[line.first.ordinal].position.y.toFloat() * heightRatio + top,
          person.keyPoints[line.second.ordinal].position.x.toFloat() * widthRatio + left,
          person.keyPoints[line.second.ordinal].position.y.toFloat() * heightRatio + top,
          paint
        )
      }
    }

    canvas.drawText(
      "Score: %.2f".format(person.score),
      (15.0f * widthRatio),
      (30.0f * heightRatio + bottom),
      paint
    )
    canvas.drawText(
      "Device: %s".format(posenet.device),
      (15.0f * widthRatio),
      (50.0f * heightRatio + bottom),
      paint
    )
    canvas.drawText(
      "Time: %.2f ms".format(posenet.lastInferenceTimeNanos * 1.0f / 1_000_000),
      (15.0f * widthRatio),
      (70.0f * heightRatio + bottom),
      paint
    )
  }

  /** Process image using Posenet library.   */
  private fun processImage(bitmap: Bitmap) {
    // Crop bitmap.
    val croppedBitmap = cropBitmap(bitmap)

    // Created scaled version of bitmap for model input.
    val scaledBitmap = Bitmap.createScaledBitmap(croppedBitmap, MODEL_WIDTH, MODEL_HEIGHT, true)

    // Perform inference.
    val person = posenet.estimateSinglePose(scaledBitmap)
    val canvas: Canvas = surfaceHolder!!.lockCanvas()
    draw(canvas, person, scaledBitmap)
  }

  /**
   * Shows an error message dialog.
   */
  class ErrorDialog : DialogFragment() {

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
      AlertDialog.Builder(activity)
        .setMessage(arguments!!.getString(ARG_MESSAGE))
        .setPositiveButton(android.R.string.ok) { _, _ -> activity!!.finish() }
        .create()

    companion object {

      @JvmStatic
      private val ARG_MESSAGE = "message"

      @JvmStatic
      fun newInstance(message: String): ErrorDialog = ErrorDialog().apply {
        arguments = Bundle().apply { putString(ARG_MESSAGE, message) }
      }
    }
  }

  companion object {
    /**
     * Conversion from screen rotation to JPEG orientation.
     */
    private val ORIENTATIONS = SparseIntArray()
    private val FRAGMENT_DIALOG = "dialog"

    init {
      ORIENTATIONS.append(Surface.ROTATION_0, 90)
      ORIENTATIONS.append(Surface.ROTATION_90, 0)
      ORIENTATIONS.append(Surface.ROTATION_180, 270)
      ORIENTATIONS.append(Surface.ROTATION_270, 180)
    }

    /**
     * Tag for the [Log].
     */
    private const val TAG = "PosenetActivity"
  }
}

文件activity_posenet.xml中的所有元素也应该被删除,因为不再需要它们了。因此,该文件应该如下所示:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

</RelativeLayout>

对于AndroidManifest.xml文件,因为我们不再访问摄像机,所以应删除以下 3 行:

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />

从三个文件PosenetActivity.ktactivity_posenet.xmlAndroidManifest.xml中删除所有不必要的代码后,我们仍然需要做一些修改来处理单个图像。下一节讨论编辑activity_posenet.xml文件,以便能够加载和显示图像。

编辑活动布局

下面列出了活动布局文件的内容。它只有两个元素:ButtonImageView。该按钮将用于在单击后加载图像。它被赋予一个 IDselectImage,以便在活动内部访问。

ImageView将有两个用途。第一个是显示选中的图像。第二个是显示应用眼睛过滤器后的结果。ImageView被赋予 ID imageView,以便从活动中访问。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>

下图显示了活动布局的样子。

在实现按钮 click listener 之前,有必要在AndroidManifest.xml文件中添加下一行来请求访问外部存储器的许可。

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

下一节将讨论如何实现按钮点击监听器来从图库中加载图像。

加载图库图像

下面给出了onStart()回调方法的当前实现。如果您还没有这样做,请删除对openCamera()方法的调用,因为不再需要它了。onStart()方法只是创建了一个PoseNet类的实例,这样以后就可以用它来预测关键点的位置。变量posenet保存创建的实例,稍后将在processImage()方法中使用。

override fun onStart() {
  super.onStart()
  posenet = Posenet(this.context!!)
}

onStart()方法中,我们可以将一个点击监听器绑定到selectImage按钮。下面是这种方法的新实现。使用Intent,画廊将被打开,要求用户选择一个图像。调用startActivityForResult()方法,请求代码存储在设置为100REQUEST_CODE变量中。

override fun onStart() {
    super.onStart()

    posenet = Posenet(this.context!!)

    selectImage.setOnClickListener(View.OnClickListener {
        val intent = Intent(Intent.ACTION_PICK)
        intent.type = "image/jpg"
        startActivityForResult(intent, REQUEST_CODE)
    })
}

一旦用户返回到应用程序,就会调用onActivityResult()回调方法。下面是它的实现。使用一个if语句,检查结果以确保图像被成功选择。如果结果不成功(例如,用户没有选择图像),则显示 toast 消息。

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_CODE) {
        imageView.setImageURI(data?.data)

        val imageUri = data?.getData()
        val bitmap = MediaStore.Images.Media.getBitmap(context?.contentResolver, imageUri)

        processImage(bitmap)

    } else {
        Toast.makeText(context, "No image is selected.", Toast.LENGTH_LONG).show()
    }
}

如果结果成功,则使用setImageURI()方法在ImageView上显示所选图像。

为了能够处理选定的图像,它需要作为一个Bitmap可用。因此,图像根据其 URI 被读取为位图。首先使用getData()方法返回 URI,然后使用getBitmap()方法返回位图。位图可用后,调用processImage()方法准备图像并估计人体姿态。这将在接下来的两节中讨论。

下面显示了将在整个教程中使用的图像。

Image source: Fashion Kids. Here is the direct image URL.

从图库中选择这样的图像后,它将显示在ImageView上,如下图所示。

在图像作为位图加载之后,在估计人体姿态之前,需要两个额外的步骤:裁剪和缩放图像。我们现在将讨论这些。

裁剪和缩放图像

本节介绍在应用 PoseNet 模型之前准备映像。

在项目内部有一个名为processImage()的方法,它调用必要的方法来完成四个重要任务:

  1. 通过调用cropBitmap()方法裁剪图像。
  2. 通过调用Bitmap.createScaledBitmap()方法缩放图像。
  3. 通过调用estimateSinglePose()方法估计姿态。
  4. 通过调用draw()方法在图像上绘制关键点。

下面列出了processImage()方法的实现。在这一节中,我们将关注前两个任务,裁剪和缩放图像。

private fun processImage(bitmap: Bitmap) {
  // Crop bitmap.
  val croppedBitmap = cropBitmap(bitmap)

  // Created scaled version of bitmap for model input.
  val scaledBitmap = Bitmap.createScaledBitmap(croppedBitmap, MODEL_WIDTH, MODEL_HEIGHT, true)

  // Perform inference.
  val person = posenet.estimateSinglePose(scaledBitmap)

  // Draw keypoints over the image.
  val canvas: Canvas = surfaceHolder!!.lockCanvas()
  draw(canvas, person, scaledBitmap)
}

为什么我们需要裁剪或缩放图像?是否需要同时应用裁剪和缩放操作,或者只应用一种操作就足够了?大家讨论一下。

PoseNet 模型接受大小为(257, 257)的图像。在Constants.kt文件中定义了两个变量MODEL_WIDTHMODEL_HEIGHT,分别代表模型输入的宽度和高度。两者都被设定为257

如果一个图像要传递给PoseNet模型,那么它的大小必须是(257, 257)。否则,将引发异常。例如,如果从图库中读取的图像大小为(547, 783),那么必须将其调整到模型输入的大小(257, 257)

基于此,似乎只需要scale(即调整大小)操作就可以将图像转换为所需的大小。Bitmap.createScaledBitmap()方法接受输入位图、所需的宽度和高度,并返回所需大小的新位图。那么,为什么还要应用裁剪操作呢?答案是保留模型的长宽比。否则我们很容易出现图像质量问题。下图显示了应用裁剪和调整大小操作后的结果。

由于裁剪图像,图像顶部的一些行会丢失。只要人体出现在图像的中心,这就不是问题。您可以检查cropBitmap()方法的实现是如何工作的。

在讨论了processImage()方法的前两个任务的目的之后,现在让我们讨论剩下的两个:姿态估计和关键点绘制。

使用 PoseNet 估计人体姿态

要估计所选图像的人体姿态,您需要做的就是调用estimateSinglePose()方法,如下所示。该方法接受缩放后的图像作为输入,并在保存模型预测的person变量中返回一个对象。

val person = posenet.estimateSinglePose(scaledBitmap)

基于模型预测,关键点将被绘制在图像上。为了能够在图像上绘制,必须首先创建一个画布。下面的代码行(在processImage()方法中)使用了surfaceHolder来绘制画布,但是我们将删除它:

val canvas: Canvas = surfaceHolder!!.lockCanvas()

用这个替换它:

val canvas = Canvas(scaledBitmap)

现在我们准备调用draw()方法在图像上绘制关键点。不要忘记从draw()方法的末尾删除这一行:surfaceHolder!!.unlockCanvasAndPost(canvas)

draw(canvas, person, scaledBitmap)

既然我们已经讨论了processImage()方法中的所有方法调用,下面是它的实现。

private fun processImage(bitmap: Bitmap) {
  // Crop bitmap.
  val croppedBitmap = cropBitmap(bitmap)

  // Created scaled version of bitmap for model input.
  val scaledBitmap = Bitmap.createScaledBitmap(croppedBitmap, MODEL_WIDTH, MODEL_HEIGHT, true)

  // Perform inference.
  val person = posenet.estimateSinglePose(scaledBitmap)

  // Draw keypoints over the image.
  val canvas = Canvas(scaledBitmap)
  draw(canvas, person, scaledBitmap)
}

下图显示了在画出模型确信的关键点后的结果。这些点被画成圆圈。这是来自draw()方法的代码部分,负责在图像上画圆。您可以编辑变量circleRadius的值来增加或减小圆的大小。

if (keyPoint.score > minConfidence) {
    val position = keyPoint.position
    val adjustedX: Float = position.x.toFloat() * widthRatio
    val adjustedY: Float = position.y.toFloat() * heightRatio
    canvas.drawCircle(adjustedX, adjustedY, circleRadius, paint)
}

请注意,所绘制的关键点的置信度大于设置为0.5minConfidence变量中指定的值。你可以把它改成最适合你的。

下一节将展示如何打印一些关于关键点的信息。

获取关于关键点的信息

estimateSinglePose()方法返回的对象person保存了一些关于检测到的关键点的信息。这些信息包括:

  1. 位置
  2. 信心
  3. 关键点表示的身体部位

接下来的代码创建了一个for循环,用于遍历所有的关键点,并在日志消息中打印每个关键点的前三个属性。

for (keyPoint in person.keyPoints) {
    Log.d("KEYPOINT", "Body Part : " + keyPoint.bodyPart + ", Keypoint Location : (" + keyPoint.position.x.toFloat().toString() + ", " + keyPoint.position.y.toFloat().toString() + "), Confidence" + keyPoint.score);
}

下面是运行循环的结果。请注意,LEFT_EYERIGHT_EYERIGHT_SHOULDER等身体部位的置信度大于0.5,这也是它们被绘制在图像上的原因。

D/KEYPOINT: Body Part : NOSE, Keypoint Location : (121.0, 97.0), Confidence : 0.999602
D/KEYPOINT: Body Part : LEFT_EYE, Keypoint Location : (155.0, 79.0), Confidence : 0.996097
D/KEYPOINT: Body Part : RIGHT_EYE, Keypoint Location : (99.0, 78.0), Confidence : 0.9952989
D/KEYPOINT: Body Part : LEFT_EAR, Keypoint Location : (202.0, 96.0), Confidence : 0.9312741
D/KEYPOINT: Body Part : RIGHT_EAR, Keypoint Location : (65.0, 105.0), Confidence : 0.3558412
D/KEYPOINT: Body Part : LEFT_SHOULDER, Keypoint Location : (240.0, 208.0), Confidence : 0.18282844
D/KEYPOINT: Body Part : RIGHT_SHOULDER, Keypoint Location : (28.0, 226.0), Confidence : 0.8710659
D/KEYPOINT: Body Part : LEFT_ELBOW, Keypoint Location : (155.0, 160.0), Confidence : 0.008276528
D/KEYPOINT: Body Part : RIGHT_ELBOW, Keypoint Location : (-22.0, 266.0), Confidence : 0.009810507
D/KEYPOINT: Body Part : LEFT_WRIST, Keypoint Location : (196.0, 161.0), Confidence : 0.012271293
D/KEYPOINT: Body Part : RIGHT_WRIST, Keypoint Location : (-7.0, 228.0), Confidence : 0.0037742765
D/KEYPOINT: Body Part : LEFT_HIP, Keypoint Location : (154.0, 101.0), Confidence : 0.0043469984
D/KEYPOINT: Body Part : RIGHT_HIP, Keypoint Location : (255.0, 259.0), Confidence : 0.0035778792
D/KEYPOINT: Body Part : LEFT_KNEE, Keypoint Location : (157.0, 97.0), Confidence : 0.0024392735
D/KEYPOINT: Body Part : RIGHT_KNEE, Keypoint Location : (127.0, 94.0), Confidence : 0.003601794
D/KEYPOINT: Body Part : LEFT_ANKLE, Keypoint Location : (161.0, 194.0), Confidence : 0.0022431263
D/KEYPOINT: Body Part : RIGHT_ANKLE, Keypoint Location : (92.0, 198.0), Confidence : 0.0021493114

**PosenetActivity.kt**的完整代码

这里是PosenetActivity.kt的完整代码。

package org.tensorflow.lite.examples.posenet

import android.app.Activity
import android.app.AlertDialog
import android.app.Dialog
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.*
import android.os.Bundle
import android.support.v4.app.ActivityCompat
import android.support.v4.app.DialogFragment
import android.support.v4.app.Fragment
import android.util.Log
import android.util.SparseIntArray
import android.view.LayoutInflater
import android.view.Surface
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_posenet.*
import kotlin.math.abs
import org.tensorflow.lite.examples.posenet.lib.BodyPart
import org.tensorflow.lite.examples.posenet.lib.Person
import org.tensorflow.lite.examples.posenet.lib.Posenet
import android.provider.MediaStore
import android.graphics.Bitmap

class PosenetActivity :
    Fragment(),
    ActivityCompat.OnRequestPermissionsResultCallback {

    /** List of body joints that should be connected.    */
    private val bodyJoints = listOf(
        Pair(BodyPart.LEFT_WRIST, BodyPart.LEFT_ELBOW),
        Pair(BodyPart.LEFT_ELBOW, BodyPart.LEFT_SHOULDER),
        Pair(BodyPart.LEFT_SHOULDER, BodyPart.RIGHT_SHOULDER),
        Pair(BodyPart.RIGHT_SHOULDER, BodyPart.RIGHT_ELBOW),
        Pair(BodyPart.RIGHT_ELBOW, BodyPart.RIGHT_WRIST),
        Pair(BodyPart.LEFT_SHOULDER, BodyPart.LEFT_HIP),
        Pair(BodyPart.LEFT_HIP, BodyPart.RIGHT_HIP),
        Pair(BodyPart.RIGHT_HIP, BodyPart.RIGHT_SHOULDER),
        Pair(BodyPart.LEFT_HIP, BodyPart.LEFT_KNEE),
        Pair(BodyPart.LEFT_KNEE, BodyPart.LEFT_ANKLE),
        Pair(BodyPart.RIGHT_HIP, BodyPart.RIGHT_KNEE),
        Pair(BodyPart.RIGHT_KNEE, BodyPart.RIGHT_ANKLE)
    )

    val REQUEST_CODE = 100

    /** Threshold for confidence score. */
    private val minConfidence = 0.5

    /** Radius of circle used to draw keypoints.  */
    private val circleRadius = 8.0f

    /** Paint class holds the style and color information to draw geometries,text and bitmaps. */
    private var paint = Paint()

    /** An object for the Posenet library.    */
    private lateinit var posenet: Posenet

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? = inflater.inflate(R.layout.activity_posenet, container, false)

    override fun onStart() {
        super.onStart()

        posenet = Posenet(this.context!!)

        selectImage.setOnClickListener(View.OnClickListener {
            val intent = Intent(Intent.ACTION_PICK)
            intent.type = "image/jpg"
            startActivityForResult(intent, REQUEST_CODE)
        })
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_CODE) {
            imageView.setImageURI(data?.data) // handle chosen image

            val imageUri = data?.getData()
            val bitmap = MediaStore.Images.Media.getBitmap(context?.contentResolver, imageUri)

            processImage(bitmap)
        } else {
            Toast.makeText(context, "No image is selected.", Toast.LENGTH_LONG).show()
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        posenet.close()
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults: IntArray
    ) {
        if (requestCode == REQUEST_CAMERA_PERMISSION) {
            if (allPermissionsGranted(grantResults)) {
                ErrorDialog.newInstance(getString(R.string.request_permission))
                    .show(childFragmentManager, FRAGMENT_DIALOG)
            }
        } else {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        }
    }

    private fun allPermissionsGranted(grantResults: IntArray) = grantResults.all {
        it == PackageManager.PERMISSION_GRANTED
    }

    /** Crop Bitmap to maintain aspect ratio of model input.   */
    private fun cropBitmap(bitmap: Bitmap): Bitmap {
        val bitmapRatio = bitmap.height.toFloat() / bitmap.width
        val modelInputRatio = MODEL_HEIGHT.toFloat() / MODEL_WIDTH
        var croppedBitmap = bitmap

        // Acceptable difference between the modelInputRatio and bitmapRatio to skip cropping.
        val maxDifference = 1e-5

        // Checks if the bitmap has similar aspect ratio as the required model input.
        when {
            abs(modelInputRatio - bitmapRatio) < maxDifference -> return croppedBitmap
            modelInputRatio < bitmapRatio -> {
                // New image is taller so we are height constrained.
                val cropHeight = bitmap.height - (bitmap.width.toFloat() / modelInputRatio)
                croppedBitmap = Bitmap.createBitmap(
                    bitmap,
                    0,
                    (cropHeight / 5).toInt(),
                    bitmap.width,
                    (bitmap.height - cropHeight / 5).toInt()
                )
            }
            else -> {
                val cropWidth = bitmap.width - (bitmap.height.toFloat() * modelInputRatio)
                croppedBitmap = Bitmap.createBitmap(
                    bitmap,
                    (cropWidth / 5).toInt(),
                    0,
                    (bitmap.width - cropWidth / 5).toInt(),
                    bitmap.height
                )
            }
        }
        Log.d(
            "IMGSIZE",
            "Cropped Image Size (" + croppedBitmap.width.toString() + ", " + croppedBitmap.height.toString() + ")"
        )
        return croppedBitmap
    }

    /** Set the paint color and size.    */
    private fun setPaint() {
        paint.color = Color.RED
        paint.textSize = 80.0f
        paint.strokeWidth = 5.0f
    }

    /** Draw bitmap on Canvas.   */
    private fun draw(canvas: Canvas, person: Person, bitmap: Bitmap) {
        setPaint()

        val widthRatio = canvas.width.toFloat() / MODEL_WIDTH
        val heightRatio = canvas.height.toFloat() / MODEL_HEIGHT

        // Draw key points over the image.
        for (keyPoint in person.keyPoints) {
            Log.d(
                "KEYPOINT",
                "Body Part : " + keyPoint.bodyPart + ", Keypoint Location : (" + keyPoint.position.x.toFloat().toString() + ", " + keyPoint.position.x.toFloat().toString() + "), Confidence" + keyPoint.score
            );

            if (keyPoint.score > minConfidence) {
                val position = keyPoint.position
                val adjustedX: Float = position.x.toFloat() * widthRatio
                val adjustedY: Float = position.y.toFloat() * heightRatio
                canvas.drawCircle(adjustedX, adjustedY, circleRadius, paint)
            }
        }

        for (line in bodyJoints) {
            if (
                (person.keyPoints[line.first.ordinal].score > minConfidence) and
                (person.keyPoints[line.second.ordinal].score > minConfidence)
            ) {
                canvas.drawLine(
                    person.keyPoints[line.first.ordinal].position.x.toFloat() * widthRatio,
                    person.keyPoints[line.first.ordinal].position.y.toFloat() * heightRatio,
                    person.keyPoints[line.second.ordinal].position.x.toFloat() * widthRatio,
                    person.keyPoints[line.second.ordinal].position.y.toFloat() * heightRatio,
                    paint
                )
            }
        }
    }

    /** Process image using Posenet library.   */
    private fun processImage(bitmap: Bitmap) {
        // Crop bitmap.
        val croppedBitmap = cropBitmap(bitmap)

        // Created scaled version of bitmap for model input.
        val scaledBitmap = Bitmap.createScaledBitmap(croppedBitmap, MODEL_WIDTH, MODEL_HEIGHT, true)
        Log.d(
            "IMGSIZE",
            "Cropped Image Size (" + scaledBitmap.width.toString() + ", " + scaledBitmap.height.toString() + ")"
        )

        // Perform inference.
        val person = posenet.estimateSinglePose(scaledBitmap)

        // Making the bitmap image mutable to enable drawing over it inside the canvas.
        val workingBitmap = Bitmap.createBitmap(croppedBitmap)
        val mutableBitmap = workingBitmap.copy(Bitmap.Config.ARGB_8888, true)

        // There is an ImageView. Over it, a bitmap image is drawn. There is a canvas associated with the bitmap image to draw the keypoints.
        // ImageView ==> Bitmap Image ==> Canvas

        val canvas = Canvas(mutableBitmap)

        draw(canvas, person, mutableBitmap)
    }

    /**
     * Shows an error message dialog.
     */
    class ErrorDialog : DialogFragment() {

        override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
            AlertDialog.Builder(activity)
                .setMessage(arguments!!.getString(ARG_MESSAGE))
                .setPositiveButton(android.R.string.ok) { _, _ -> activity!!.finish() }
                .create()

        companion object {

            @JvmStatic
            private val ARG_MESSAGE = "message"

            @JvmStatic
            fun newInstance(message: String): ErrorDialog = ErrorDialog().apply {
                arguments = Bundle().apply { putString(ARG_MESSAGE, message) }
            }
        }
    }

    companion object {
        /**
         * Conversion from screen rotation to JPEG orientation.
         */
        private val ORIENTATIONS = SparseIntArray()
        private val FRAGMENT_DIALOG = "dialog"

        init {
            ORIENTATIONS.append(Surface.ROTATION_0, 90)
            ORIENTATIONS.append(Surface.ROTATION_90, 0)
            ORIENTATIONS.append(Surface.ROTATION_180, 270)
            ORIENTATIONS.append(Surface.ROTATION_270, 180)
        }

        /**
         * Tag for the [Log].
         */
        private const val TAG = "PosenetActivity"
    }
}

结论

本教程讨论了使用预训练的 PoseNet 来构建一个估计人体姿势的 Android 应用程序。该模型能够预测人体中 17 个关键点的位置,如眼睛、鼻子和耳朵。

下图总结了该项目中应用的步骤。我们首先从图库中加载一张图片,裁剪并调整大小到(257, 257)。在加载 PoseNet 模型之后,图像被馈送给它,用于预测关键点的位置。最后,将检测到的置信度在0.5以上的关键点绘制在图像上。

下一个教程继续这个项目,在关键点的位置上放置滤镜,应用类似 Snapchat 中看到的效果。

使用 PoseNet 为 Android 创建一个类似 Snapchat 的眼睛过滤器

原文:https://blog.paperspace.com/posenet-snapchat-eye-filter-android/

PoseNet 是一个计算机视觉深度学习模型,用于估计一个人的物理位置。基于 MobileNet,它可以部署到移动设备上,响应输入图像所需的时间甚至更少。

在上一个教程中,我们看到了如何使用 PoseNet 模型来检测人体的各个关键点的位置,例如眼睛、耳朵和鼻子。基于这些检测到的关键点,我们可以应用像 Snapchat 这样的流行应用程序中可用的效果。在本教程中,我们将继续在图像上应用眼睛过滤器的项目。

大纲如下:

  • 项目管道的快速概述
  • 准备过滤器
  • 定位左眼和右眼的关键点
  • 加载和绘制眼睛过滤器
  • 动态设置眼睛过滤器的大小
  • 完成PosenetActivity.kt的实施

让我们开始吧。

项目管道快速概览

这个项目由两部分组成:使用 PoseNet 的关键点检测(我们在之前的教程中已经介绍过了),并将其应用到创建眼睛过滤器的用例中。

下图总结了我们在上一个教程中完成的内容。我们首先加载、裁剪和调整图像大小,使其大小与模型输入的大小相匹配。该图像然后被馈送到 PoseNet 模型,用于预测关键点位置。

在某些情况下,模型可能无法准确检测身体部位,或者图像可能根本不包含人。这就是为什么每个关键点都有一个相关的分数(在0.01.0之间),代表模型的置信度。例如,如果分数高于0.5,则关键点将被接受以进行进一步处理。在上一个教程中,我们看到了如何用高于0.5的置信度在检测到的关键点上画圆。

当前教程在上一个教程停止的地方继续;检测完所有的关键点后,我们将会锁定眼睛的位置。加载并准备好过滤器图像后,将其放置在目标关键点上。这将在下面演示。

我们将从下载和准备过滤器图像开始。

准备眼部滤镜

眼睛过滤器只是一个放在我们的目标图像上的图像。您可以使用任何您想要的滤镜图像,但是您需要遵循我们将在这里讨论的准备它的说明。

我们将使用的过滤器可以从CleanPNG.com下载。它不仅仅是一个单目过滤器,而是如下所示的九个不同潜在过滤器的集合。

让我们从左上角开始。

左眼和右眼看起来是相连的,所以为了方便起见,我们将它们分开。这是左眼:

这是正确的:

准备好过滤器后,我们将回顾如何定位由 PoseNet 模型返回的左眼和右眼的关键点。

定位左眼和右眼的关键点

在之前教程中讨论的 Android Studio 项目中,活动PosenetActivity.kt有一个名为processImage()的方法,负责准备图库图像(即裁剪和调整大小)、预测关键点位置以及绘制它们的所有工作。它的实现如下所示。

private fun processImage(bitmap: Bitmap) {
  // Crop bitmap.
  val croppedBitmap = cropBitmap(bitmap)

  // Created scaled version of bitmap for model input.
  val scaledBitmap = Bitmap.createScaledBitmap(croppedBitmap, MODEL_WIDTH, MODEL_HEIGHT, true)

  // Perform inference.
  val person = posenet.estimateSinglePose(scaledBitmap)

  // Draw keypoints over the image.
  val canvas = Canvas(scaledBitmap)
  draw(canvas, person, scaledBitmap)
}

对象person保存了关于检测到的关键点的信息,包括它们的位置、置信度得分以及它们所代表的身体部位。这个对象作为参数与画布和位图一起提供给了draw()方法。

这里是draw()方法在关键点位置画圆的最小代码。变量MODEL_WIDTHMODEL_HEIGHT分别代表模型的输入宽度和高度。在 Android Studio 项目的Constants.kt文件中,两者都被赋予了值257

private fun draw(canvas: Canvas, person: Person, bitmap: Bitmap) {
    setPaint()

    val widthRatio = canvas.width.toFloat() / MODEL_WIDTH
    val heightRatio = canvas.height.toFloat() / MODEL_HEIGHT

    // Draw key points over the image.
    for (keyPoint in person.keyPoints) {
        if (keyPoint.score > minConfidence) {
            val position = keyPoint.position
            val adjustedX: Float = position.x.toFloat() * widthRatio
            val adjustedY: Float = position.y.toFloat() * heightRatio
            canvas.drawCircle(adjustedX, adjustedY, circleRadius, paint)
        }
    }
}

下图显示了绘制关键点后的图像。

Source: Fashion Kids. Image direct link is here.

person对象中,我们可以检索模型返回的所有17关键点的以下信息:

  1. 身体部位:keyPoint.bodyPart
  2. 地点:keyPoint.position.x
  3. 置信度:keyPoint.position.y

下一个for循环将此信息打印为日志消息。

for (keyPoint in person.keyPoints) {
    Log.d("KEYPOINT", "Body Part : " + keyPoint.bodyPart + ", Keypoint Location : (" + keyPoint.position.x.toFloat().toString() + ", " + keyPoint.position.y.toFloat().toString() + "), Confidence" + keyPoint.score);
}

我们对所有的关键点都不感兴趣,只对眼睛感兴趣。在这个循环中,我们可以使用下面的if语句来检查左眼:

if (keyPoint.bodyPart == BodyPart.LEFT_EYE) {
}

然后是右眼。

if (keyPoint.bodyPart == BodyPart.RIGHT_EYE) {
}

注意,前面两个if语句只是检查身体部分,忽略了置信度得分。下面的代码也考虑了置信度得分。请注意,minConfidence变量在PosenetActivity.kt中定义,其值为0.5,这意味着关键点必须具有0.5或更高的置信度才能被接受。

for (keyPoint in person.keyPoints) {
    if (keyPoint.bodyPart == BodyPart.LEFT_EYE && keyPoint.score > minConfidence) {
    }

    if (keyPoint.bodyPart == BodyPart.RIGHT_EYE && keyPoint.score > minConfidence) {
    }
}

现在我们可以定位眼睛,我们将看到如何加载过滤器。

加载并绘制眼图滤镜

两只眼睛的滤镜将作为资源图像添加到 Android Studio 项目中。只需复制两张图片并粘贴到项目的drawable文件夹中。为了方便定位图像,选择Android视图,然后导航到app/res/drawable目录,如下图所示。在我的例子中,我将这两个图像命名为left.pngright.png

下一行使用decodeResource()方法加载图像。要加载左眼滤镜,只需将当前 ID R.drawable.right替换为R.drawable.left。记得根据您为两个图像选择的名称替换单词rightleft

var filterImage = BitmapFactory.decodeResource(context?.getResources(), R.drawable.right)

加载左眼和右眼滤镜后,在将它们绘制到图像上之前,我们需要调整它们的大小。下一行使用Bitmap.createScaledBitmap()方法将每个调整到(100, 100)

filterImage = Bitmap.createScaledBitmap(filterImage, 100, 100, true)

最后,根据下面的代码在画布上绘制过滤器。从xy坐标中减去的值50用于将尺寸为(100, 100)的滤光器置于眼睛的中心。

canvas.drawBitmap(
    filterImage,
    keyPoint.position.x.toFloat() * widthRatio - 50,
    keyPoint.position.y.toFloat() * heightRatio - 50,
    null
)

我们现在已经在图像上加载、调整和绘制了滤镜。下面列出了draw()方法的完整实现,包括if语句。

private fun draw(canvas: Canvas, person: Person, bitmap: Bitmap) {
    setPaint()

    val widthRatio = canvas.width.toFloat() / MODEL_WIDTH
    val heightRatio = canvas.height.toFloat() / MODEL_HEIGHT

    // Draw key points over the image.
    for (keyPoint in person.keyPoints) {
        if (keyPoint.bodyPart == BodyPart.LEFT_EYE) {
            var filterImage = BitmapFactory.decodeResource(context?.getResources(), R.drawable.left)
            filterImage = Bitmap.createScaledBitmap(filterImage, 100, 100, true)
            canvas.drawBitmap(
                filterImage,
                keyPoint.position.x.toFloat() * widthRatio - 50,
                keyPoint.position.y.toFloat() * heightRatio - 50,
                null
             )
         }

         if (keyPoint.bodyPart == BodyPart.RIGHT_EYE) {
             var filterImage = BitmapFactory.decodeResource(context?.getResources(), R.drawable.right)
             filterImage = Bitmap.createScaledBitmap(filterImage, 100, 100, true)
             canvas.drawBitmap(
                 filterImage,
                 keyPoint.position.x.toFloat() * widthRatio - 50,
                 keyPoint.position.y.toFloat() * heightRatio - 50,
                 null
             )
        }
    }
}

下图显示了滤镜如何查看图像。结果看起来相当不错。

让我们用下面的图片再试一次。

Source: Fashion Kids. Direct link to the image is here..

下图显示了如何应用过滤器。在这种情况下,过滤器太大了。

问题是滤镜的大小永远是(100, 100),与眼睛大小无关。在某些情况下,就像上图中的一个,眼睛可能远离相机,因此它们的尺寸远小于(100, 100)。因此,滤镜不仅覆盖眼睛,还可能覆盖整张脸。

我们将在下一节通过根据两眼之间的距离调整眼睛滤镜的大小来解决这个问题。

动态设置眼睛滤镜大小

为了相对于眼睛大小来调整过滤器图像的大小,两只眼睛的X位置将被保存到一个数组中。下一行创建了一个名为eyesXLocation的数组来保存眼睛的X位置。

var eyesXLocation = FloatArray(2)

使用下面给出的for循环,与两只眼睛相关的两个关键点的X位置将被提取并保存到数组中。

for (keyPoint in person.keyPoints) {
    if (keyPoint.bodyPart == BodyPart.LEFT_EYE) {
        eyesXLocation[0] = keyPoint.position.x.toFloat() * widthRatio
    }

    if (keyPoint.bodyPart == BodyPart.RIGHT_EYE) {
        eyesXLocation[1] = keyPoint.position.x.toFloat() * widthRatio
    }
}

基于存储在eyesXLocation数组中的值,将根据下一行计算两眼之间的水平绝对距离。

var eyeFilterSize = abs(eyesXLocation[1] - eyesXLocation[0])

下一张图应该有助于阐明距离是如何计算的。关键点位置用红色圆圈标记。关键点之间的距离是连接它们的红线的长度。如果线的长度是L,那么眼睛过滤器的尺寸将是(L, L)。在这里,L是变量eyeFilterSize。每个眼睛过滤器以关键点为中心。

eyeFilterSize中的值将被提供给Bitmap.createScaledBitmap()方法,以调整加载的过滤器图像的大小,如下所示。

filterImage = Bitmap.createScaledBitmap(
    filterImage,
    eyeFilterSize.toInt(),
    eyeFilterSize.toInt(),
    true
)

下面显示了draw()方法的新实现。

private fun draw(canvas: Canvas, person: Person, bitmap: Bitmap) {
    setPaint()

    val widthRatio = canvas.width.toFloat() / MODEL_WIDTH
    val heightRatio = canvas.height.toFloat() / MODEL_HEIGHT

    var eyesXLocation = FloatArray(2)
    for (keyPoint in person.keyPoints) {
        if (keyPoint.bodyPart == BodyPart.LEFT_EYE) {
            eyesXLocation[0] = keyPoint.position.x.toFloat() * widthRatio
        }

        if (keyPoint.bodyPart == BodyPart.RIGHT_EYE) {
            eyesXLocation[2] = keyPoint.position.x.toFloat() * widthRatio
        }
    }

    var eyeFilterSize = abs(eyesXLocation[1] - eyesXLocation[0])

    // Draw key points over the image.
    for (keyPoint in person.keyPoints) {
        if (keyPoint.bodyPart == BodyPart.LEFT_EYE) {
            var filterImage = BitmapFactory.decodeResource(context?.getResources(), R.drawable.left)
            filterImage = Bitmap.createScaledBitmap(
                filterImage,
                eyeFilterSize.toInt(),
                eyeFilterSize.toInt(),
                true
            )
            canvas.drawBitmap(
                filterImage,
                keyPoint.position.x.toFloat() * widthRatio - eyeFilterSize / 2,
                keyPoint.position.y.toFloat() * heightRatio - eyeFilterSize / 2,
                null
            )
        }

        if (keyPoint.bodyPart == BodyPart.RIGHT_EYE) {
            var filterImage = BitmapFactory.decodeResource(context?.getResources(), R.drawable.right)
            filterImage = Bitmap.createScaledBitmap(
                filterImage,
                eyeFilterSize.toInt(),
                eyeFilterSize.toInt(),
                true
            )
            canvas.drawBitmap(
                filterImage,
                keyPoint.position.x.toFloat() * widthRatio - eyeFilterSize / 2,
                keyPoint.position.y.toFloat() * heightRatio - eyeFilterSize / 2,
                null
            )
        }
    }
}

这是用之前的图片测试应用程序的结果。过滤器的大小现在更适合眼睛的大小。

注意,基于眼睛关键点之间的距离动态计算过滤器尺寸也有助于使过滤器完全覆盖眼睛。这可以在下图中看到。

现在,我们可以自动调整任何过滤器,以适应图像。在我们结束教程之前,让我们看看如何使用其他眼睛过滤器。

使用不同的眼睛过滤器

我们可以很容易地改变我们使用的过滤器。您需要做的就是将图像作为资源文件添加到 Android Studio 项目中,并将其加载到draw()方法中。让我们使用下面的心脏图像代替前面的过滤器。

Image source: Clipart Library. The direct link of the image is here.

下载完图片后,只需按照下图将其作为可绘制资源添加到 Android Studio 项目中即可。我将资源文件命名为heart.png

现在,根据下一行,我们将使用draw()方法来加载它,以代替前面的过滤器。

var filterImage = BitmapFactory.decodeResource(context?.getResources(), R.drawable.heart)

如果您想使用另一个过滤器,只需将其作为资源添加,并将其 ID 提供给decodeResource()方法。

下图显示了使用心脏过滤器后的结果。

完成PosenetActivity.kt的实施

下面列出了PosenetActivity.kt的完整代码。你也可以在这里下载项目

package org.tensorflow.lite.examples.posenet

import android.app.Activity
import android.app.AlertDialog
import android.app.Dialog
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.*
import android.os.Bundle
import android.support.v4.app.ActivityCompat
import android.support.v4.app.DialogFragment
import android.support.v4.app.Fragment
import android.util.Log
import android.util.SparseIntArray
import android.view.LayoutInflater
import android.view.Surface
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_posenet.*
import kotlin.math.abs
import org.tensorflow.lite.examples.posenet.lib.BodyPart
import org.tensorflow.lite.examples.posenet.lib.Person
import org.tensorflow.lite.examples.posenet.lib.Posenet
import android.provider.MediaStore
import android.graphics.Bitmap

class PosenetActivity :
    Fragment(),
    ActivityCompat.OnRequestPermissionsResultCallback {

    val REQUEST_CODE = 100

    /** Threshold for confidence score. */
    private val minConfidence = 0.5

    /** Radius of circle used to draw keypoints.  */
    private val circleRadius = 8.0f

    /** Paint class holds the style and color information to draw geometries,text and bitmaps. */
    private var paint = Paint()

    /** An object for the Posenet library.    */
    private lateinit var posenet: Posenet

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? = inflater.inflate(R.layout.activity_posenet, container, false)

    override fun onStart() {
        super.onStart()

        posenet = Posenet(this.context!!)

        selectImage.setOnClickListener(View.OnClickListener {
            val intent = Intent(Intent.ACTION_PICK)
            intent.type = "image/jpg"
            startActivityForResult(intent, REQUEST_CODE)
        })
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_CODE) {
            imageView.setImageURI(data?.data) // handle chosen image

            val imageUri = data?.getData()
            val bitmap = MediaStore.Images.Media.getBitmap(context?.contentResolver, imageUri)

            processImage(bitmap)
        } else {
            Toast.makeText(context, "No image is selected.", Toast.LENGTH_LONG).show()
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        posenet.close()
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults: IntArray
    ) {
        if (requestCode == REQUEST_CAMERA_PERMISSION) {
            if (allPermissionsGranted(grantResults)) {
                ErrorDialog.newInstance(getString(R.string.request_permission))
                    .show(childFragmentManager, FRAGMENT_DIALOG)
            }
        } else {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        }
    }

    private fun allPermissionsGranted(grantResults: IntArray) = grantResults.all {
        it == PackageManager.PERMISSION_GRANTED
    }

    /** Crop Bitmap to maintain aspect ratio of model input.   */
    private fun cropBitmap(bitmap: Bitmap): Bitmap {
        val bitmapRatio = bitmap.height.toFloat() / bitmap.width
        val modelInputRatio = MODEL_HEIGHT.toFloat() / MODEL_WIDTH
        var croppedBitmap = bitmap

        // Acceptable difference between the modelInputRatio and bitmapRatio to skip cropping.
        val maxDifference = 1e-5

        // Checks if the bitmap has similar aspect ratio as the required model input.
        when {
            abs(modelInputRatio - bitmapRatio) < maxDifference -> return croppedBitmap
            modelInputRatio < bitmapRatio -> {
                // New image is taller so we are height constrained.
                val cropHeight = bitmap.height - (bitmap.width.toFloat() / modelInputRatio)
                croppedBitmap = Bitmap.createBitmap(
                    bitmap,
                    0,
                    (cropHeight / 5).toInt(),
                    bitmap.width,
                    (bitmap.height - cropHeight / 5).toInt()
                )
            }
            else -> {
                val cropWidth = bitmap.width - (bitmap.height.toFloat() * modelInputRatio)
                croppedBitmap = Bitmap.createBitmap(
                    bitmap,
                    (cropWidth / 5).toInt(),
                    0,
                    (bitmap.width - cropWidth / 5).toInt(),
                    bitmap.height
                )
            }
        }
        Log.d(
            "IMGSIZE",
            "Cropped Image Size (" + croppedBitmap.width.toString() + ", " + croppedBitmap.height.toString() + ")"
        )
        return croppedBitmap
    }

    /** Set the paint color and size.    */
    private fun setPaint() {
        paint.color = Color.RED
        paint.textSize = 80.0f
        paint.strokeWidth = 5.0f
    }

    private fun draw(canvas: Canvas, person: Person, bitmap: Bitmap) {
        setPaint()

        val widthRatio = canvas.width.toFloat() / MODEL_WIDTH
        val heightRatio = canvas.height.toFloat() / MODEL_HEIGHT

        var eyesXLocation = FloatArray(2)
        for (keyPoint in person.keyPoints) {
            if (keyPoint.bodyPart == BodyPart.LEFT_EYE) {
                eyesXLocation[0] = keyPoint.position.x.toFloat() * widthRatio
            }

            if (keyPoint.bodyPart == BodyPart.RIGHT_EYE) {
                eyesXLocation[2] = keyPoint.position.x.toFloat() * widthRatio
            }
        }

        var eyeFilterSize = abs(eyesXLocation[1] - eyesXLocation[0])

        // Draw key points over the image.
        for (keyPoint in person.keyPoints) {
            if (keyPoint.bodyPart == BodyPart.LEFT_EYE) {
                var filterImage = BitmapFactory.decodeResource(context?.getResources(), R.drawable.left)
                filterImage = Bitmap.createScaledBitmap(
                    filterImage,
                    eyeFilterSize.toInt(),
                    eyeFilterSize.toInt(),
                    true
                )
                canvas.drawBitmap(
                    filterImage,
                    keyPoint.position.x.toFloat() * widthRatio - eyeFilterSize / 2,
                    keyPoint.position.y.toFloat() * heightRatio - eyeFilterSize / 2,
                    null
                )
            }

            if (keyPoint.bodyPart == BodyPart.RIGHT_EYE) {
                var filterImage = BitmapFactory.decodeResource(context?.getResources(), R.drawable.right)
                filterImage = Bitmap.createScaledBitmap(
                    filterImage,
                    eyeFilterSize.toInt(),
                    eyeFilterSize.toInt(),
                    true
                )
                canvas.drawBitmap(
                    filterImage,
                    keyPoint.position.x.toFloat() * widthRatio - eyeFilterSize / 2,
                    keyPoint.position.y.toFloat() * heightRatio - eyeFilterSize / 2,
                    null
                )
            }
        }
    }

    /** Process image using Posenet library.   */
    private fun processImage(bitmap: Bitmap) {
        // Crop bitmap.
        val croppedBitmap = cropBitmap(bitmap)

        // Created scaled version of bitmap for model input.
        val scaledBitmap = Bitmap.createScaledBitmap(croppedBitmap, MODEL_WIDTH, MODEL_HEIGHT, true)
        Log.d(
            "IMGSIZE",
            "Cropped Image Size (" + scaledBitmap.width.toString() + ", " + scaledBitmap.height.toString() + ")"
        )

        // Perform inference.
        val person = posenet.estimateSinglePose(scaledBitmap)

        // Making the bitmap image mutable to enable drawing over it inside the canvas.
        val workingBitmap = Bitmap.createBitmap(croppedBitmap)
        val mutableBitmap = workingBitmap.copy(Bitmap.Config.ARGB_8888, true)

        // There is an ImageView. Over it, a bitmap image is drawn. There is a canvas associated with the bitmap image to draw the keypoints.
        // ImageView ==> Bitmap Image ==> Canvas

        val canvas = Canvas(mutableBitmap)

        draw(canvas, person, mutableBitmap)
    }

    /**
     * Shows an error message dialog.
     */
    class ErrorDialog : DialogFragment() {

        override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
            AlertDialog.Builder(activity)
                .setMessage(arguments!!.getString(ARG_MESSAGE))
                .setPositiveButton(android.R.string.ok) { _, _ -> activity!!.finish() }
                .create()

        companion object {

            @JvmStatic
            private val ARG_MESSAGE = "message"

            @JvmStatic
            fun newInstance(message: String): ErrorDialog = ErrorDialog().apply {
                arguments = Bundle().apply { putString(ARG_MESSAGE, message) }
            }
        }
    }

    companion object {
        /**
         * Conversion from screen rotation to JPEG orientation.
         */
        private val ORIENTATIONS = SparseIntArray()
        private val FRAGMENT_DIALOG = "dialog"

        init {
            ORIENTATIONS.append(Surface.ROTATION_0, 90)
            ORIENTATIONS.append(Surface.ROTATION_90, 0)
            ORIENTATIONS.append(Surface.ROTATION_180, 270)
            ORIENTATIONS.append(Surface.ROTATION_270, 180)
        }

        /**
         * Tag for the [Log].
         */
        private const val TAG = "PosenetActivity"
    }
}

结论

本教程延续了上一教程中开始的项目,允许在图像上添加眼睛滤镜。

基于使用 PoseNet 深度学习模型检测到的关键点,可以定位左眼和右眼。在被定位之后,加载过滤器图像并准备在被检测的眼睛上绘制。

为了使过滤器图像的大小取决于眼睛的大小,计算两只眼睛之间的距离并用于调整过滤器的大小。

自然语言处理中使用预训练单词嵌入的指南

原文:https://blog.paperspace.com/pre-trained-word-embeddings-natural-language-processing/

与其他问题相比,处理文本数据的过程略有不同。这是因为数据通常是文本形式的。因此,你必须弄清楚如何以机器学习模型可以理解的数字形式来表示数据。在本文中,我们将看看如何做到这一点。最后,您将使用 TensorFlow 建立一个深度学习模型,对给定的文本进行分类。

让我们开始吧。请注意,您可以从渐变社区笔记本的免费 GPU 上运行本教程中的所有代码。

加载数据

第一步是下载和加载数据。我们将使用的数据是一个情感分析数据集。它有两列;一个有情感,另一个有标签。让我们下载并加载它。

!wget --no-check-certificate \    https://drive.google.com/uc?id=13ySLC_ue6Umt9RJYSeM2t-V0kCv-4C-P -O /tmp/sentiment.csv \    -O /tmp/sentiment.csv
import pandas as pd
df = pd.read_csv('/tmp/sentiment.csv')

这是一个数据样本。

现在让我们选择特性和目标,然后将数据分成训练集和测试集。

X = df['text']
y = df['sentiment']
from sklearn.model_selection import train_test_split
X_train, X_test , y_train, y_test = train_test_split(X, y , test_size = 0.20)

数据预处理

由于这是文本数据,有几件事你必须清理它。这包括:

  • 将所有句子转换成小写
  • 删除所有引号
  • 用一些数字形式表示所有的单词
  • 删除特殊字符,如@%

以上都可以在 TensorFlow 中使用Tokenizer实现。该类需要几个参数:

  • num_words:希望包含在单词索引中的最大单词数
  • oov_token:用来表示在单词字典中找不到的单词的标记。这通常发生在处理训练数据时。数字 1 通常用于表示“词汇表之外”的令牌(“oov”令牌)

一旦用首选参数实例化了训练集上的Tokenizer,就用fit_on_texts函数来拟合它。

from keras.preprocessing.text import Tokenizer
vocab_size = 10000
oov_token = "<OOV>"
tokenizer = Tokenizer(num_words = vocab_size, oov_token=oov_token)
tokenizer.fit_on_texts(X_train)

word_index可以用来显示单词到数字的映射。

word_index = tokenizer.word_index

将文本转换为序列

下一步是将每种情绪表示为一系列数字。这可以使用texts_to_sequences功能来完成。

X_train_sequences = tokenizer.texts_to_sequences(X_train)

这是这些序列的样子。

让我们对测试集做同样的事情。当您检查序列样本时,您可以看到不在词汇表中的单词由1表示。

X_test_sequences = tokenizer.texts_to_sequences(X_test) 

填充序列

目前,这些序列有不同的长度。通常,您会将相同长度的序列传递给机器学习模型。因此,您必须确保所有序列的长度相同。这是通过填充序列来完成的。较长的序列将被截断,而较短的序列将用零填充。因此,您必须声明截断和填充类型。

让我们从定义每个序列的最大长度、填充类型和截断类型开始。填充和截断类型“post”意味着这些操作将发生在序列的末尾。

max_length = 100
padding_type='post'
truncation_type='post'

有了这些,让我们开始填充X_test_sequences。这是在传递上面定义的参数时使用pad_sequences函数完成的。

from keras.preprocessing.sequence import pad_sequences

X_test_padded = pad_sequences(X_test_sequences,maxlen=max_length, 
                               padding=padding_type, truncating=truncation_type)

X_train_sequences也应该这样做。

X_train_padded = pad_sequences(X_train_sequences,maxlen=max_length, padding=padding_type, 
                       truncating=truncation_type)

打印最终结果显示,在序列的末尾添加了零,使它们具有相同的长度。

使用手套词嵌入

TensorFlow 使您能够训练单词嵌入。然而,这一过程不仅需要大量数据,而且可能是时间和资源密集型的。为了应对这些挑战,你可以使用预先训练的单词嵌入。让我们用斯坦福大学的 GloVe(全局向量)单词嵌入来说明如何做到这一点。这些嵌入是从表示在同一向量空间中相似的单词中获得的。也就是说,负面的词会聚集在一起,正面的词也是如此。

第一步是获得单词嵌入,并将它们添加到字典中。之后,您需要为训练集中的每个单词创建一个嵌入矩阵。让我们从下载手套单词嵌入开始。

!wget --no-check-certificate \
     http://nlp.stanford.edu/data/glove.6B.zip \
     -O /tmp/glove.6B.zip

下一步是将它们提取到一个临时文件夹中。

import os
import zipfile
with zipfile.ZipFile('/tmp/glove.6B.zip', 'r') as zip_ref:
    zip_ref.extractall('/tmp/glove')

接下来,用这些嵌入创建字典。让我们使用glove.6B.100d.tx嵌入。名称中的100与为序列选择的最大长度相同。

import numpy as np
embeddings_index = {}
f = open('/tmp/glove/glove.6B.100d.txt')
for line in f:
    values = line.split()
    word = values[0]
    coefs = np.asarray(values[1:], dtype='float32')
    embeddings_index[word] = coefs
f.close()

print('Found %s word vectors.' % len(embeddings_index))

下一步是为前面获得的单词索引中的每个单词创建单词嵌入矩阵。如果一个单词在 GloVe 中没有嵌入,它将呈现一个零矩阵。

embedding_matrix = np.zeros((len(word_index) + 1, max_length))
for word, i in word_index.items():
    embedding_vector = embeddings_index.get(word)
    if embedding_vector is not None:
        # words not found in embedding index will be all-zeros.
        embedding_matrix[i] = embedding_vector

下面是单词“shop”的单词 embedding 的样子。

创建 Keras 嵌入层

下一步是使用上面获得的嵌入作为 Keras 嵌入层的权重。你还必须将该层的trainable参数设置为False,这样就不会被训练。如果再次进行训练,权重将被重新初始化。这将类似于从零开始训练单词嵌入。还有几件其他事情需要注意:

  • 嵌入层将第一个参数作为词汇表的大小。添加1是因为0通常保留用于填充
  • input_length是输入序列的长度
  • output_dim是密集嵌入的维数
from tensorflow.keras.layers import Embedding, LSTM, Dense, Bidirectional

embedding_layer = Embedding(input_dim=len(word_index) + 1,
                            output_dim=max_length,
                            weights=[embedding_matrix],
                            input_length=max_length,
                            trainable=False)

创建张量流模型

下一步是在 Keras 模型中使用嵌入层。让我们将模型定义如下:

  • 作为第一层的嵌入层
  • 两个双向 LSTM 层确保信息双向流动
  • 完全连接的层,以及
  • 负责最终输出的最终层
from tensorflow.keras.models import Sequential
model = Sequential([
    embedding_layer,
    Bidirectional(LSTM(150, return_sequences=True)), 
    Bidirectional(LSTM(150)),
    Dense(128, activation='relu'),
   Dense(1, activation='sigmoid')
])

训练模型

下一步是编译和训练模型。

model.compile(loss='binary_crossentropy',optimizer='adam',metrics=['accuracy']) 

当模型正在训练时,您可以设置一个EarlyStopping回调,以便在模型停止改进时停止训练过程。还可以设置 TensorBoard 回调,以便稍后快速查看模型的性能。

from tensorflow.keras.callbacks import EarlyStopping, TensorBoard
%load_ext tensorboard
rm -rf logs

log_folder = 'logs'
callbacks = [
            EarlyStopping(patience = 10),
            TensorBoard(log_dir=log_folder)
            ]
num_epochs = 600
history = model.fit(X_train_padded, y_train, epochs=num_epochs, validation_data=(X_test_padded, y_test),callbacks=callbacks)

可以使用evaluate方法快速检查模型的性能。

loss, accuracy = model.evaluate(X_test_padded,y_test)
print('Test accuracy :', accuracy)

形象化

从日志目录启动 TensorBoard 可以看到模型的性能。

%tensorboard --logdir logs

您也可以使用 TensorBoard 的图形部分来深入可视化模型。

最后的想法

在本文中,您已经通过一个示例了解了如何在自然语言处理问题中使用预先训练的单词嵌入。您可以尝试通过以下方式改进这一模式:

您也可以通过在 Gradient Community 笔记本中的免费 GPU 上运行这个示例来尝试一下。

新闻稿:使用 CoreSite 扩展公共云

原文:https://blog.paperspace.com/press-release-public-cloud-expansion/

http://www . coresite . com/about/news-events/press-releases/paper space-expands-public-cloud-with coresite

PAPERSPACE 通过 CORESITE 扩展公共云

**科罗拉多州丹佛市—2016 年 6 月 21 日—**CoreSite Realty Corporation(NYSE:COR)是美国安全、可靠、高性能数据中心和互连解决方案的主要提供商,该公司今天宣布,Paperspace 是一家技术公司,为其客户提供一台完整的个人电脑,该电脑位于云中,可从任何网络浏览器访问,该公司刚刚完成了 CoreSite 在纽约和圣克拉拉园区的数据中心部署。

Paperspace 与其 IT 服务提供商 COLOpeople 合作,寻找可扩展的高性能数据中心解决方案,以满足其新推出的云计算平台的需求。基于客户对 Paperspace 云解决方案的强烈初始需求,Paperspace 选择 CoreSite 来访问领先企业、云提供商和互联网对等交换,以及灵活和可扩展的数据中心解决方案。对于 Paperspace 来说,在由具有良好可靠性记录的提供商运营的高密度、可扩展的国家数据中心平台内进行部署至关重要。

Paperspp ace 的联合创始人 Daniel Kobran 表示:“我们对数据中心合作伙伴的主要需求是灵活地适应我们在高密度电力环境中的快速增长,并为美国各地强大的企业客户以及网络和云服务合作伙伴提供连接。" CoreSite 继续超越我们所有的标准."

Paperspace 为企业提供了一种创新的解决方案,以易于使用的界面按需提供额外的处理能力。通过在 CoreSite 平台的东西两岸部署,Paperspace 可以在 CoreSite 强大的客户群体中获得更广泛的受众,包括不断增长的企业需求基础。

CoreSite 在纽约的园区包括位于曼哈顿和新泽西州 Secaucus 的数据中心设施,包括超过 280,000 平方英尺的互连、可靠、高性能数据中心容量。CoreSite 的纽约园区托管了超过 45 家网络服务提供商,可直接访问一些世界领先的云服务提供商,所有这些提供商均可通过稳定、低延迟的网络访问曼哈顿,从而在支持提升客户 IT 性能的同时降低客户成本。

CoreSite 的硅谷数据中心市场由位于圣克拉拉、圣何塞和米尔皮塔斯的运营设施组成,位于圣克拉拉的另外两个数据中心目前正在建设中。竣工后,这七处设施将提供近 780,000 平方英尺的数据中心容量。包括企业、国际和国内运营商、社交媒体公司、云计算提供商以及媒体和娱乐公司在内的近 200 家客户在 CoreSite 的硅谷数据中心开展业务。

CoreSite 销售和营销高级副总裁史蒂夫·史密斯表示:“我们很高兴欢迎 Paperspace 加入我们在全国平台上不断壮大的云服务社区。“我们很高兴能与 Paperspace 合作,推出他们的创新云计算产品。我们期待通过可靠且可扩展的数据中心解决方案来支持他们的快速增长,同时提供无缝、高度接触的客户体验。”

关于图纸空间

Paperspace 是为云时代设计的虚拟桌面平台。我们提供对基于订阅的桌面的访问,可以在任何地方、任何设备上进行访问。Paperspace 是首款能够运行高端富媒体应用和 3D 图形的 GPU 加速虚拟桌面。我们的专有协议是目前最快的流媒体技术,可直接在网络浏览器中提供近乎原生的性能。Paperspace 包括一个简单的基于 web 的界面,通过强大的企业功能(如一键备份、登录监控、自定义模板和共享驱动器)来管理您的团队。

关于一致性

CoreSite Realty Corporation(NYSE:COR)为北美八个主要市场中不断增长的客户生态系统提供安全、可靠、高性能的数据中心和互连解决方案。超过 900 家全球领先企业、网络运营商、云提供商和支持服务提供商选择 CoreSite 来连接、保护和优化其性能敏感型数据、应用和计算工作负载。我们可扩展、灵活的解决方案和 350 多名敬业的员工始终如一地提供无与伦比的数据中心选项,所有这些都带来了一流的客户体验和持久的关系。欲了解更多信息,请访问 www.CoreSite.com。

🎁产品更新:代码差异视图

原文:https://blog.paperspace.com/product-update-code-diff-view/

[2021 年 12 月 2 日更新:本文包含关于梯度实验的信息。实验现已被弃用,渐变工作流已经取代了它的功能。请参见工作流程文档了解更多信息。]

Gradient experiments 中的代码查看器现在包括一个 visual diff 工具,面向开发人员,用于更好地可视化实验中的代码更改。

它是如何工作的

diff 视图帮助您比较 Gradient 中的文件、目录和版本控制项目。它提供了两个文件的双向比较,使理解代码更改更加容易。

Gradient code diffing tool

切换并排视图

新的比较查看器支持“并排”和“逐行”的可视化表示。使用切换按钮在两者之间切换,如下图所示:

🎁产品更新:热键

原文:https://blog.paperspace.com/product-update-hotkeys/

我们很高兴分享热键快捷键的到来!

我们经常从用户那里听到的一件事是,他们希望在使用 Paperspace 产品时能够更快地切换上下文。

我们希望响应这一号召,简化在 Paperspace 上切换不同资源的体验。例如,如果你正在浏览你的笔记本寻找上周你正在工作的拥抱脸的变形金刚笔记本,你可能也想检查作业页面,看看你最后一次使用渐变作业运行器是什么时候——现在只需一次按键就可以实现!

我们邀请您使用这些新的漂亮热键在 Paperspace 控制台上操作:

  • shift + t -切换控制台菜单
  • m -切换侧边栏菜单
  • c -导航至核心
  • g -导航至梯度
  • n -导航至笔记本
  • j -导航至工作
  • p -导航至项目

如果您对希望实现的其他快捷方式有任何建议,请告诉我们!

一如既往,让我们知道你在 Twitter 上的想法,或者通过 hello@paperspace.com 联系我们。

ProGAN:渐进增长的生成性对抗网络

原文:https://blog.paperspace.com/progan/

faces

Photo by Andrew Seaman / Unsplash

人类学习的过程是一个渐进的曲线。作为婴儿,我们在几个月的时间里慢慢地从坐、爬、站、走、跑等等进步。大多数对概念的理解也是从初学者逐渐进步,并继续学习到高级/大师水平。即使是语言的学习也是一个渐进的过程,从学习字母表、理解单词开始,最后发展形成句子的能力。这些例子提供了人类如何理解最基本概念的要点。我们慢慢适应并学习新的主题,但这样的场景在机器和深度学习模型的情况下如何工作?

在以前的博客中,我们已经研究了几种不同类型的生成性敌对网络,它们都有自己独特的方法来获得特定的目标。之前的一些作品包括循环甘pix-2-pix 甘、【their 甘,以及其他多个生成网络,都有各自独特的特质。然而,在这篇文章中,我们将重点关注一个名为GANsProgressive Growing 的生成性对抗网络,它以大多数人都会采用的方式学习模式,从最低级开始,到更高级的理解。本文提供的代码可以有效地运行在 Paperspace Gradient 平台上,利用其大规模、高质量的资源来实现预期的结果。

简介:

在 ProGANs 之前建立的大多数生成网络使用独特的技术,主要涉及损失函数的修改以获得期望的结果。这些体系结构的生成器和鉴别器中的层总是同时被训练。此时,大多数生成网络都在改进其他基本特征和参数,以提高结果,但并不真正涉及渐进增长。然而,随着逐渐增长的生成性对抗网络的引入,训练程序的重点是逐渐增长网络,一次一层。

对于这种渐进增长的训练过程,直观的想法是经常人为地将训练图像缩小和收缩到最小的像素化尺寸。一旦我们有了最低分辨率的图像,我们就可以开始训练程序,随着时间的推移获得稳定性。在本文中,我们将更详细地探讨 ProGANs。在接下来的部分中,我们将学习对 ProGANs 如何工作有一个概念性理解的大部分要求。然后从头开始构建网络以生成面部结构。事不宜迟,让我们深入了解这些创意网络。


理解程序:

Image Source

ProGAN 网络的主要思想是从最低层到最高层构建。在接下来的研究论文中,作者描述了生成器和鉴别器逐步学习的方法。如上图所示,我们从一个 4 x 4 的低分辨率图像开始,然后开始添加额外的微调和更高的参数变量,以获得更有效的结果。网络首先学习并理解 4 x 4 图像的工作原理。一旦完成,我们开始教它 8 x 8 的图像,16 x 16 的分辨率,等等。上述示例中使用的最高分辨率为 1024 x 1024。

这种渐进式训练的方法使模型在整个训练过程中获得了前所未有的结果,整体稳定性更高。我们知道,这一革命性网络的主要理念之一是利用不断发展的技术。我们之前也注意到,它没有像其他架构一样过多地考虑这些网络的损失函数。在本实验中使用了默认的 Wasserstein 损失,但是也可以使用其他类似的损失函数,如最小平方损失。观众可以从我之前的一篇关于沃瑟斯坦生成对抗网络(WGAN)的文章中,通过这个链接了解更多关于这种损失的信息。

除了渐进生长网络的概念之外,本文还介绍了其他一些有意义的主题,即最小批量标准偏差、新层中的褪色、像素归一化和均衡学习速率。在我们着手实现这些概念之前,我们将在本节中更详细地探索和理解每一个概念。

由于只考虑小批量,所以小批量标准偏差鼓励生成网络在生成的图像中产生更多变化。由于只考虑小批量,鉴别器更容易区分图像的真假,迫使生成器生成更多种类的图像。这种简单的技术解决了生成网络的一个主要问题,与各自的训练数据相比,生成的图像通常变化较小。

Image Source

研究论文中讨论的另一个重要概念是在新层中引入淡化。当从一个阶段过渡到另一个阶段时,即从较低分辨率切换到下一个较高分辨率时,新的层被平滑地淡入。这防止了在添加这个新层时先前的层受到突然的“冲击”。参数$\alpha$用于控制衰落。该参数在多次训练迭代中线性插值。如上图所示,最终公式可以写成如下形式:

$$(1-\ alpha)\倍上采样\,层+(\ alpha)\倍输出\,层$ $

我们将在本节简要介绍的最后两个概念是均衡学习率和像素归一化。通过均衡的学习速率,我们可以相应地调整每一层的权重。公式类似于明凯初始化或 he 初始化。但是均衡学习率在每次向前传递时都使用它,而不是把它作为一个单独的初始化器。最后,使用像素归一化代替批量归一化,因为注意到内部协变量移位的问题在 GANs 中并不突出。像素归一化将每个像素中的特征向量归一化为单位长度。有了对这些基本概念的理解,我们可以着手构建用于生成面部图像的 ProGAN 网络架构。


从零开始构建 ProGAN 架构网络:

在本节中,我们将涵盖从零开始构建渐进增长的生成性对抗网络所需的大多数必要元素。我们将致力于利用这些网络架构生成面部图像。完成这个编码构建的主要要求是一个像样的用于训练的 GPU(或 Paperspace Gradient Platform)以及 TensorFlow 和 Keras 深度学习框架的一些基础知识。如果你不熟悉这两个库,我推荐你看看 TensorFlow 的这个链接和 Keras 的下面这个链接。让我们从导入必要的库开始。

导入基本库:

第一步,我们将导入有效计算 ProGAN 网络所需的所有基本库。我们将导入 TensorFlow 和 Keras 深度学习框架,用于构建最佳鉴别器和生成器网络。NumPy 库将用于大多数需要执行的数学运算。我们也将利用一些计算机视觉库来相应地处理图像。此外,可以使用简单的 pip install 命令安装 mtcnn 库。下面是代表这个项目所需的所有库的代码片段。

from math import sqrt
from numpy import load, asarray, zeros, ones, savez_compressed
from numpy.random import randn, randint
from skimage.transform import resize
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.models import Sequential, Model
from tensorflow.keras.layers import Input, Dense, Flatten, Reshape, Conv2D
from tensorflow.keras.layers import UpSampling2D, AveragePooling2D, LeakyReLU, Layer, Add
from keras.constraints import max_norm
from keras.initializers import RandomNormal
import mtcnn
from mtcnn.mtcnn import MTCNN
from keras import backend
from matplotlib import pyplot
import cv2
import os
from os import listdir
from PIL import Image
import cv2

数据预处理:

本项目数据集可从以下 网站下载。名人面孔属性(CelebA)数据集是用于面部检测和识别相关任务的更受欢迎的数据集之一。在本文中,我们将主要使用这些数据,在 ProGAN 网络的帮助下生成新的独特面孔。我们将在下一个代码块中定义的一些基本函数将帮助我们以更合适的方式处理项目。首先,我们将定义一个函数来加载图像,将其转换为 RGB 图像,并以 numpy 数组的形式存储它。

在接下来的几个函数中,我们将利用预训练的多任务级联卷积神经网络(MTCNN),它被认为是用于人脸检测的最先进的精度深度学习模型。使用这个模型的主要目的是确保我们只考虑这个名人数据集中可用的人脸,而忽略一些不必要的背景特征。因此,在将图像调整到所需的大小之前,我们将使用之前安装在本地系统上的 mtcnn 库来执行面部检测和提取。

# Loading the image file
def load_image(filename):
    image = Image.open(filename)
    image = image.convert('RGB')
    pixels = asarray(image)
    return pixels

# extract the face from a loaded image and resize
def extract_face(model, pixels, required_size=(128, 128)):
    # detect face in the image
    faces = model.detect_faces(pixels)
    if len(faces) == 0:
        return None

    # extract details of the face
    x1, y1, width, height = faces[0]['box']
    x1, y1 = abs(x1), abs(y1)

    x2, y2 = x1 + width, y1 + height
    face_pixels = pixels[y1:y2, x1:x2]
    image = Image.fromarray(face_pixels)
    image = image.resize(required_size)
    face_array = asarray(image)

    return face_array

# load images and extract faces for all images in a directory
def load_faces(directory, n_faces):
    # prepare model
    model = MTCNN()
    faces = list()

    for filename in os.listdir(directory):
        # Computing the retrieval and extraction of faces
        pixels = load_image(directory + filename)
        face = extract_face(model, pixels)
        if face is None:
            continue
        faces.append(face)
        print(len(faces), face.shape)
        if len(faces) >= n_faces:
            break

    return asarray(faces)

根据您的系统功能,下一步可能需要一些时间来完全计算。数据集中有大量可用的数据。如果读者有更多的时间和计算资源,最好提取整个数据,并在完整的 CelebA 数据集上进行训练。然而,出于本文的目的,我将只利用 10000 张图片进行个人培训。使用下面的代码片段,我们可以提取数据并以. npz 压缩格式保存,以备将来使用。

# load and extract all faces
directory = 'img_align_celeba/img_align_celeba/'
all_faces = load_faces(directory, 10000)
print('Loaded: ', all_faces.shape)

# save in compressed format
savez_compressed('img_align_celeba_128.npz', all_faces)

可以加载保存的数据,如下面的代码片段所示。

# load the prepared dataset
from numpy import load
data = load('img_align_celeba_128.npz')
faces = data['arr_0']
print('Loaded: ', faces.shape)

构建基本功能:

在这一节中,我们将着重于构建我们之前在理解 ProGANs 如何工作时讨论过的所有函数。我们将首先构造像素归一化函数,该函数允许我们将每个像素中的特征向量归一化为单位长度。下面的代码片段可用于计算相应的像素归一化。

# pixel-wise feature vector normalization layer
class PixelNormalization(Layer):
    # initialize the layer
    def __init__(self, **kwargs):
        super(PixelNormalization, self).__init__(**kwargs)

    # perform the operation
    def call(self, inputs):
        # computing pixel values
        values = inputs**2.0
        mean_values = backend.mean(values, axis=-1, keepdims=True)
        mean_values += 1.0e-8
        l2 = backend.sqrt(mean_values)
        normalized = inputs / l2
        return normalized

    # define the output shape of the layer
    def compute_output_shape(self, input_shape):
        return input_shape

在我们之前讨论的下一个重要方法中,我们将确保模型在小批量标准偏差上训练。小批量功能仅用于鉴频器网络的输出层。我们利用 minibatch 标准偏差来确保模型考虑较小的批次,从而使生成的各种图像更加独特。下面是计算迷你批次标准差的代码片段。

# mini-batch standard deviation layer
class MinibatchStdev(Layer):
    # initialize the layer
    def __init__(self, **kwargs):
        super(MinibatchStdev, self).__init__(**kwargs)

    # perform the operation
    def call(self, inputs):
        mean = backend.mean(inputs, axis=0, keepdims=True)
        squ_diffs = backend.square(inputs - mean)
        mean_sq_diff = backend.mean(squ_diffs, axis=0, keepdims=True)
        mean_sq_diff += 1e-8
        stdev = backend.sqrt(mean_sq_diff)

        mean_pix = backend.mean(stdev, keepdims=True)
        shape = backend.shape(inputs)
        output = backend.tile(mean_pix, (shape[0], shape[1], shape[2], 1))

        combined = backend.concatenate([inputs, output], axis=-1)
        return combined

    # define the output shape of the layer
    def compute_output_shape(self, input_shape):
        input_shape = list(input_shape)
        input_shape[-1] += 1
        return tuple(input_shape)

下一步,我们将计算加权和并定义 Wasserstein 损失函数。如前所述,加权和类将用于层中的平滑衰落。我们将使用$\alpha$值计算输出,正如我们在上一节中所述。下面是以下操作的代码块。

# weighted sum output
class WeightedSum(Add):
    # init with default value
    def __init__(self, alpha=0.0, **kwargs):
        super(WeightedSum, self).__init__(**kwargs)
        self.alpha = backend.variable(alpha, name='ws_alpha')

    # output a weighted sum of inputs
    def _merge_function(self, inputs):
        # only supports a weighted sum of two inputs
        assert (len(inputs) == 2)
        # ((1-a) * input1) + (a * input2)
        output = ((1.0 - self.alpha) * inputs[0]) + (self.alpha * inputs[1])
        return output

# calculate wasserstein loss
def wasserstein_loss(y_true, y_pred):
    return backend.mean(y_true * y_pred)

最后,我们将定义为图像合成项目创建 ProGAN 网络架构所需的一些基本功能。我们将定义函数来为发生器和鉴别器网络生成真实和虚假样本。然后,我们将更新淡入值,并相应地缩放数据集。所有这些步骤都在它们各自的函数中定义,这些函数可以从下面定义的代码片段中获得。

# load dataset
def load_real_samples(filename):
    data = load(filename)
    X = data['arr_0']
    X = X.astype('float32')
    X = (X - 127.5) / 127.5
    return X

# select real samples
def generate_real_samples(dataset, n_samples):
    ix = randint(0, dataset.shape[0], n_samples)
    X = dataset[ix]
    y = ones((n_samples, 1))
    return X, y

# generate points in latent space as input for the generator
def generate_latent_points(latent_dim, n_samples):
    x_input = randn(latent_dim * n_samples)
    x_input = x_input.reshape(n_samples, latent_dim)
    return x_input

# use the generator to generate n fake examples, with class labels
def generate_fake_samples(generator, latent_dim, n_samples):
    x_input = generate_latent_points(latent_dim, n_samples)
    X = generator.predict(x_input)
    y = -ones((n_samples, 1))
    return X, y

# update the alpha value on each instance of WeightedSum
def update_fadein(models, step, n_steps):
    alpha = step / float(n_steps - 1)
    for model in models:
        for layer in model.layers:
            if isinstance(layer, WeightedSum):
                backend.set_value(layer.alpha, alpha)

# scale images to preferred size
def scale_dataset(images, new_shape):
    images_list = list()
    for image in images:
        new_image = resize(image, new_shape, 0)
        images_list.append(new_image)
    return asarray(images_list)

创建发电机网络:

Image Source

生成器架构包括一个潜在向量空间,我们可以在其中初始化初始参数,以生成所需的图像。一旦我们定义了潜在向量空间,我们将获得 4×4 的维度,这将允许我们处理初始输入图像。然后,我们将继续添加上采样和卷积层,以及带有泄漏 ReLU 激活功能的几个块的像素归一化层。最后,我们将添加 1 x 1 卷积来映射 RGB 图像。

除了一些小的例外,开发的生成器将利用研究论文中的大多数特性。我们将使用 128 个过滤器,而不是使用 512 个或更多的过滤器,因为我们正在用更小的图像尺寸构建架构。代替均衡的学习速率,我们将利用高斯随机数和最大范数权重约束。我们将首先定义一个生成器块,然后开发整个生成器模型网络。下面是生成器块的代码片段。

# adding a generator block
def add_generator_block(old_model):
    init = RandomNormal(stddev=0.02)
    const = max_norm(1.0)
    block_end = old_model.layers[-2].output

    # upsample, and define new block
    upsampling = UpSampling2D()(block_end)
    g = Conv2D(128, (3,3), padding='same', kernel_initializer=init, kernel_constraint=const)(upsampling)
    g = PixelNormalization()(g)
    g = LeakyReLU(alpha=0.2)(g)
    g = Conv2D(128, (3,3), padding='same', kernel_initializer=init, kernel_constraint=const)(g)
    g = PixelNormalization()(g)
    g = LeakyReLU(alpha=0.2)(g)

    out_image = Conv2D(3, (1,1), padding='same', kernel_initializer=init, kernel_constraint=const)(g)
    model1 = Model(old_model.input, out_image)
    out_old = old_model.layers[-1]
    out_image2 = out_old(upsampling)

    merged = WeightedSum()([out_image2, out_image])
    model2 = Model(old_model.input, merged)
    return [model1, model2]

下面是完成每个连续层的生成器架构的代码片段。

# define generator models
def define_generator(latent_dim, n_blocks, in_dim=4):
    init = RandomNormal(stddev=0.02)
    const = max_norm(1.0)
    model_list = list()
    in_latent = Input(shape=(latent_dim,))
    g  = Dense(128 * in_dim * in_dim, kernel_initializer=init, kernel_constraint=const)(in_latent)
    g = Reshape((in_dim, in_dim, 128))(g)

    # conv 4x4, input block
    g = Conv2D(128, (3,3), padding='same', kernel_initializer=init, kernel_constraint=const)(g)
    g = PixelNormalization()(g)
    g = LeakyReLU(alpha=0.2)(g)

    # conv 3x3
    g = Conv2D(128, (3,3), padding='same', kernel_initializer=init, kernel_constraint=const)(g)
    g = PixelNormalization()(g)
    g = LeakyReLU(alpha=0.2)(g)

    # conv 1x1, output block
    out_image = Conv2D(3, (1,1), padding='same', kernel_initializer=init, kernel_constraint=const)(g)
    model = Model(in_latent, out_image)
    model_list.append([model, model])

    for i in range(1, n_blocks):
        old_model = model_list[i - 1][0]
        models = add_generator_block(old_model)
        model_list.append(models)

    return model_list

创建鉴别器网络:

Image Source

对于鉴别器架构,我们将对生成器网络的构建方式进行逆向工程。我们将从一幅 RGB 图像开始,通过一系列卷积层进行下采样。一堆模块将重复这种模式,但在最后,在输出模块,我们将添加一个 minibatch 标准差层,与之前的输出连接。最后,在两个额外的卷积层之后,我们将使鉴别器输出单个输出,这决定了生成的图像是假的还是真的。下面是添加鉴别器块的代码片段。

# adding a discriminator block
def add_discriminator_block(old_model, n_input_layers=3):
    init = RandomNormal(stddev=0.02)
    const = max_norm(1.0)
    in_shape = list(old_model.input.shape)

    # define new input shape as double the size
    input_shape = (in_shape[-2]*2, in_shape[-2]*2, in_shape[-1])
    in_image = Input(shape=input_shape)

    # define new input processing layer
    d = Conv2D(128, (1,1), padding='same', kernel_initializer=init, kernel_constraint=const)(in_image)
    d = LeakyReLU(alpha=0.2)(d)

    # define new block
    d = Conv2D(128, (3,3), padding='same', kernel_initializer=init, kernel_constraint=const)(d)
    d = LeakyReLU(alpha=0.2)(d)
    d = Conv2D(128, (3,3), padding='same', kernel_initializer=init, kernel_constraint=const)(d)
    d = LeakyReLU(alpha=0.2)(d)
    d = AveragePooling2D()(d)
    block_new = d

    # skip the input, 1x1 and activation for the old model
    for i in range(n_input_layers, len(old_model.layers)):
        d = old_model.layers[i](d)
    model1 = Model(in_image, d)

    model1.compile(loss=wasserstein_loss, optimizer=Adam(lr=0.001, beta_1=0, beta_2=0.99, epsilon=10e-8))

    downsample = AveragePooling2D()(in_image)

    block_old = old_model.layers[1](downsample)
    block_old = old_model.layers[2](block_old)
    d = WeightedSum()([block_old, block_new])

    for i in range(n_input_layers, len(old_model.layers)):
        d = old_model.layers[i](d)

    model2 = Model(in_image, d)

    model2.compile(loss=wasserstein_loss, optimizer=Adam(lr=0.001, beta_1=0, beta_2=0.99, epsilon=10e-8))
    return [model1, model2]

下面是完成每个连续层的鉴别器架构的代码片段。

# define the discriminator models for each image resolution
def define_discriminator(n_blocks, input_shape=(4,4,3)):
    init = RandomNormal(stddev=0.02)
    const = max_norm(1.0)
    model_list = list()
    in_image = Input(shape=input_shape)

    d = Conv2D(128, (1,1), padding='same', kernel_initializer=init, kernel_constraint=const)(in_image)
    d = LeakyReLU(alpha=0.2)(d)
    d = MinibatchStdev()(d)

    d = Conv2D(128, (3,3), padding='same', kernel_initializer=init, kernel_constraint=const)(d)
    d = LeakyReLU(alpha=0.2)(d)
    d = Conv2D(128, (4,4), padding='same', kernel_initializer=init, kernel_constraint=const)(d)
    d = LeakyReLU(alpha=0.2)(d)

    d = Flatten()(d)
    out_class = Dense(1)(d)

    model = Model(in_image, out_class)
    model.compile(loss=wasserstein_loss, optimizer=Adam(lr=0.001, beta_1=0, beta_2=0.99, epsilon=10e-8))
    model_list.append([model, model])

    for i in range(1, n_blocks):
        old_model = model_list[i - 1][0]
        models = add_discriminator_block(old_model)
        model_list.append(models)

    return model_list

开发 ProGAN 模型体系结构:

一旦我们完成了对单个发生器和鉴别器网络的定义,我们将创建一个整体的复合模型,将它们结合起来以创建 ProGAN 模型架构。一旦我们组合了生成器模型,我们就可以编译它们并相应地训练它们。让我们定义创建复合程序网络的函数。

# define composite models for training generators via discriminators

def define_composite(discriminators, generators):
    model_list = list()
    # create composite models
    for i in range(len(discriminators)):
        g_models, d_models = generators[i], discriminators[i]
        # straight-through model
        d_models[0].trainable = False
        model1 = Sequential()
        model1.add(g_models[0])
        model1.add(d_models[0])
        model1.compile(loss=wasserstein_loss, optimizer=Adam(lr=0.001, beta_1=0, beta_2=0.99, epsilon=10e-8))
        # fade-in model
        d_models[1].trainable = False
        model2 = Sequential()
        model2.add(g_models[1])
        model2.add(d_models[1])
        model2.compile(loss=wasserstein_loss, optimizer=Adam(lr=0.001, beta_1=0, beta_2=0.99, epsilon=10e-8))
        # store
        model_list.append([model1, model2])
    return model_list

最后,一旦我们创建了整体的复合模型,我们就可以开始训练过程了。训练网络的大部分步骤与我们之前训练 GANs 的方式相似。然而,在训练过程中还引入了渐强层和渐进生长 GAN 的渐进更新。下面是创建训练时期的代码块。

# train a generator and discriminator
def train_epochs(g_model, d_model, gan_model, dataset, n_epochs, n_batch, fadein=False):
    bat_per_epo = int(dataset.shape[0] / n_batch)
    n_steps = bat_per_epo * n_epochs
    half_batch = int(n_batch / 2)

    for i in range(n_steps):
        # update alpha for all WeightedSum layers when fading in new blocks
        if fadein:
            update_fadein([g_model, d_model, gan_model], i, n_steps)
        # prepare real and fake samples
        X_real, y_real = generate_real_samples(dataset, half_batch)
        X_fake, y_fake = generate_fake_samples(g_model, latent_dim, half_batch)

        # update discriminator model
        d_loss1 = d_model.train_on_batch(X_real, y_real)
        d_loss2 = d_model.train_on_batch(X_fake, y_fake)

        # update the generator via the discriminator's error
        z_input = generate_latent_points(latent_dim, n_batch)
        y_real2 = ones((n_batch, 1))
        g_loss = gan_model.train_on_batch(z_input, y_real2)

        # summarize loss on this batch
        print('>%d, d1=%.3f, d2=%.3f g=%.3f' % (i+1, d_loss1, d_loss2, g_loss))

# train the generator and discriminator
def train(g_models, d_models, gan_models, dataset, latent_dim, e_norm, e_fadein, n_batch):
    g_normal, d_normal, gan_normal = g_models[0][0], d_models[0][0], gan_models[0][0]
    gen_shape = g_normal.output_shape
    scaled_data = scale_dataset(dataset, gen_shape[1:])
    print('Scaled Data', scaled_data.shape)

    # train normal or straight-through models
    train_epochs(g_normal, d_normal, gan_normal, scaled_data, e_norm[0], n_batch[0])
    summarize_performance('tuned', g_normal, latent_dim)

    # process each level of growth
    for i in range(1, len(g_models)):
        # retrieve models for this level of growth
        [g_normal, g_fadein] = g_models[i]
        [d_normal, d_fadein] = d_models[i]
        [gan_normal, gan_fadein] = gan_models[i]

        # scale dataset to appropriate size
        gen_shape = g_normal.output_shape
        scaled_data = scale_dataset(dataset, gen_shape[1:])
        print('Scaled Data', scaled_data.shape)

        # train fade-in models for next level of growth
        train_epochs(g_fadein, d_fadein, gan_fadein, scaled_data, e_fadein[i], n_batch[i], True)
        summarize_performance('faded', g_fadein, latent_dim)

        # train normal or straight-through models
        train_epochs(g_normal, d_normal, gan_normal, scaled_data, e_norm[i], n_batch[i])
        summarize_performance('tuned', g_normal, latent_dim)

在培训过程中,我们还将定义一个自定义函数,帮助我们相应地评估我们的结果。我们可以总结我们的性能,并绘制一些从每次迭代中生成的图,以查看我们对结果的改进有多好。下面是总结我们整体模型性能的代码片段。

# generate samples and save as a plot and save the model
def summarize_performance(status, g_model, latent_dim, n_samples=25):
    gen_shape = g_model.output_shape
    name = '%03dx%03d-%s' % (gen_shape[1], gen_shape[2], status)

    X, _ = generate_fake_samples(g_model, latent_dim, n_samples)
    X = (X - X.min()) / (X.max() - X.min())

    square = int(sqrt(n_samples))
    for i in range(n_samples):
        pyplot.subplot(square, square, 1 + i)
        pyplot.axis('off')
        pyplot.imshow(X[i])

    # save plot to file
    filename1 = 'plot_%s.png' % (name)
    pyplot.savefig(filename1)
    pyplot.close()

    filename2 = 'model_%s.h5' % (name)
    g_model.save(filename2)
    print('>Saved: %s and %s' % (filename1, filename2))

最后,一旦相应地定义了所有必要的功能,我们就可以开始训练过程了。为了提高稳定性,我们将对小图像使用较大的批量大小和较小的时期,同时减少批量大小和增加时期的数量以实现更大的图像缩放。为图像合成训练 ProGAN 网络的代码片段如下所示。

# number of growth phases where 6 blocks == [4, 8, 16, 32, 64, 128]
n_blocks = 6
latent_dim = 100

d_models = define_discriminator(n_blocks)
g_models = define_generator(latent_dim, n_blocks)
gan_models = define_composite(d_models, g_models)

dataset = load_real_samples('img_align_celeba_128.npz')
print('Loaded', dataset.shape)

n_batch = [16, 16, 16, 8, 4, 4]
n_epochs = [5, 8, 8, 10, 10, 10]

train(g_models, d_models, gan_models, dataset, latent_dim, n_epochs, n_epochs, n_batch)

结果和进一步讨论:

>12500, d1=1756536064.000, d2=8450036736.000 g=-378913792.000
Saved: plot_032x032-faded.png and model_032x032-faded.h5 

下图显示了我在多次训练后进行 32 x 32 的上采样后获得的结果。

Image by Author

理想情况下,如果您有更多的时间、图像和计算资源来训练,我们将能够获得与下图类似的结果。

Ideal Results from research paper

大部分代码来自机器学习知识库网站,我强烈推荐通过下面的链接查看。可以对下面的项目进行一些改进和补充。我们可以通过使用更高的图像质量、增加训练迭代次数和整体计算能力来改进数据集。另一个想法是将 ProGAN 网络与 SRGAN 架构合并,以创建独特的组合可能性。我建议感兴趣的读者尝试各种可能的结果。


结论:

The River Severn at Shrewsbury, Shropshire, 1770 by Paul Sandby

Photo by Birmingham Museums Trust / Unsplash

生物学习的一切都是循序渐进的,要么从以前的错误中适应,要么从零开始学习,同时慢慢发展任何特定概念、想法或想象的每个方面。我们不是直接创造大句子,而是先学习字母、小单词等等,直到我们能够创造更长的句子。类似地,ProGAN 网络使用类似的直观方法,其中模型从最低的像素分辨率开始学习。然后,它逐渐学习更高分辨率的递增模式,以在光谱的末端实现高质量的结果。

在本文中,我们讨论了获得对用于高质量图像生成的 ProGAN 网络的基本直观理解所需的大部分主题。我们从 ProGAN 网络的基本介绍开始,然后继续理解其作者在其研究论文中介绍的大多数独特方面,以实现前所未有的结果。最后,我们使用获得的知识从零开始构建一个 ProGAN 网络,用于使用 Celeb-A 数据集生成面部图像。虽然训练是针对有限的分辨率进行的,但是读者可以进行进一步的实验,一直到更高质量的分辨率。这些生成网络有无限的发展空间。

在接下来的文章中,我们将涵盖更多的生成性对抗网络的变体,比如 StyleGAN 架构等等。我们也将获得对可变自动编码器的概念性理解,并参与更多的信号处理项目。在那之前,继续尝试和编码更多独特的深度学习和 AI 项目!

强化学习项目

原文:https://blog.paperspace.com/projects-with-reinforcement-learning/

H Y P E R S P A C E

Photo by Samuele Errico Piccarini / Unsplash

在强化系列文章的第二部分中,我们将进一步了解实现强化学习算法和策略的许多方法,以获得可能的最佳结果。如果您还没有,我强烈建议您查看一下,因为我们已经介绍了理解与这些强化学习方法相关的理论知识所需的一些基本知识。我们已经介绍了初级强化学习的概念,理解了一些有用的算法和策略,最后,实现了一个简单的深度强化学习项目。在本文中,我们的目标是进一步扩展这方面的知识。

在本文中,我们将了解与项目结构的实现相关的完整细节,主要是众多概念,如 OpenAI gym,其他必要的安装要求,如 PyTorch 和稳定基线,以及其他基本要求。我们还将查看在执行这些项目的过程中可能遇到的潜在错误修复。然后我们将继续进行两个项目的工作,即 cart pole 项目和自动驾驶汽车。我们将在 PyTorch 虚拟环境中使用不同的方法来解决 cart pole 项目,并学习如何构建一个经过训练的模型来处理具有强化学习的自动驾驶问题。查看目录,进一步理解我们将在下一篇文章中处理的众多主题和概念。

目录:

  1. 介绍
  2. 项目结构入门
    1。了解 OpenAI 健身房和
    2。安装程序
    3。修复潜在的错误
  3. 用强化学习开发一个撑杆跳项目
    1。导入基本库
    2。创造环境
    3。训练一个 RL 算法
    4。保存和加载模型
    5。评估和测试模型
  4. 用强化学习开发自动驾驶汽车项目
    1。导入基本库
    2。测试环境
    3。训练模型
    4。拯救训练有素的模特
    5。评估和测试模型
  5. 结论

简介:

Screenshot by Author

从机器人模拟到在游戏中训练 AI 机器人,再到构建高级象棋引擎,强化学习在所有这些巨大的应用中都有用处。上面显示的图包括我在不同类型的机器人模拟和项目中使用的一个软件的截图。在将这些模型部署到工业规模之前,对其进行检查和测试是至关重要的。因此,我们可以利用 OpenAI gym 环境来构建我们的强化算法,以便在有效测试特定项目的同时有效地训练模型。我们的目标是帮助机器人(或有意义的特定对象)高精度地执行复杂的任务。

一旦您在特定环境中成功地测试了您的开发模型,您就可以部署机器人模型及其各自经过训练的强化学习模型。除了工业机器人应用,这些强化学习模型也用于分析和监控交易数据、商业智能和其他类似的实践。在本文中,我们的主要焦点将是在主要两个程序上学习和训练这些 RL 模型,即,手推车杆游戏和自动驾驶自动驾驶汽车模拟。在我们直接进入这些精彩的项目之前,让我们探索一下 OpenAI gym 环境以及它们的其他基本需求和依赖性。


项目结构入门:

假设观众已经阅读了上一篇关于开始强化学习的文章的开始部分,那么你应该对我们构建深度学习模型所需的依赖类型有一个简单的想法。我们将利用 OpenAI 健身房来创建必要的环境,在那里我们可以训练我们的强化学习模型。我们将在虚拟环境中使用 PyTorch 安装,而不是以前的 TensorFlow 和 Keras 支持。因此,我建议查看《PyTorch终极指南》,以熟悉 py torch 深度学习框架。最后,我们将利用 Stable-Baselines3 开发工具包,它是实现强化学习算法和策略的可靠方法。

稳定基线 3 的灵感来自最初由 OpenAI 开发的原始基线版本。它是 PyTorch 中强化学习算法的一组可靠实现。它是稳定基线的下一个主要版本。为了进一步研究这个神奇的强化学习库,我建议查看下面的文档入门指南。下面的库安装起来很简单。确保您的虚拟环境中安装了 PyTorch,然后继续运行以下命令。

pip install stable-baselines3[extra] 

OpenAI gym 是探索各种强化学习算法的最佳工具包之一。由于其众多环境的大集合,对其类型没有预先假设的环境为用户提供了对不同类型的问题进行实验的基础平台。它高度兼容任何数值计算库和深度学习框架,如 TensorFlow、PyTorch 或 Theano。稳定基线 3 为用户提供了一套改进的算法和策略,以高度简化的方式训练强化学习模型。他们帮助研究团体容易地复制、提炼和识别新的想法。

第二个自动驾驶项目需要的另一个主要安装是特定平台(Windows、Mac 或 Linux)的 SWIG 安装。从这个网站下载 swig 包,并确保将路径添加到您的环境变量中。简化的包装器和接口生成器(SWIG)是最好的开源工具之一,它允许你把用 C 或 C++代码编写的计算机脚本包装成其他脚本语言。一旦下载完成,并且您已经安装了它,关闭命令提示符并重新打开它进行任何额外的安装。我下载安装的版本是 swigwin-4.0.2 和 swigwin-3.0.12。请核实并检查哪个版本最适合您,以及是否有任何未来的更新。

运行第二个项目需要的另外两个最终安装是 2D 盒子。盒子-2D 环境是在 OpenAI 健身房进行自驾实验的必备环境。另一个主要的安装是 Pyglet 库,它对游戏开发非常有用。在包含所有其他依赖项的虚拟环境中运行下面提到的命令,您应该能够相应地下载这两个需求。如果您像我一样在安装过程中遇到错误,请查看下面的堆栈溢出网站以获得解决方案。最好下载、安装各种版本的 SWIG,并将其添加到环境变量中,以避免此类问题。

!pip install gym[box2d] pyglet==1.3.2 

用强化学习开发推车杆项目:

Image Source

我们已经在上一篇强化学习入门文章中讨论了 cart pole 项目。作为一个快速回顾,让我们理解这个任务如何工作的内在细节。推车杆项目基本定义如下-

一根杆子通过一个非驱动关节连接到一辆小车上,小车沿着一条无摩擦的轨道移动。通过对推车施加+1 或-1 的力来控制该系统。钟摆开始直立,目标是防止它翻倒。杆保持直立的每个时间步长提供+1 的奖励。当柱子偏离垂直方向超过 15 度,或者手推车偏离中心超过 2.4 个单位时,该集结束。
-来源

在这个项目中,我们的目标是利用强化学习模型在接下来的项目中取得良好的结果,这样手推车的杆子可以长时间保持平衡,并且该模型能够在测试该模型的每集中获得至少 200 分。让我们通过理解所有的基本概念并相应地开发我们的强化学习模型来开始这个项目。

导入基本库:

在第一步中,我们将导入一些访问环境所需的基本库,并为 cart pole 项目开发所需的强化学习模型。我们将在这个实验中使用 PPO 算法。近似策略优化算法结合了 A2C(拥有多个工作者)和 TRPO(使用信任区域来改进参与者)的思想。观众可以相应地选择各自的算法。下一次导入有助于根据需要包装环境,而评估策略将帮助我们分析所需的指标。

import gym 
from stable_baselines3 import PPO
from stable_baselines3.common.vec_env import DummyVecEnv
from stable_baselines3.common.evaluation import evaluate_policy
import os

创造环境:

下一步,我们将继续使用 OpenAI 健身房支持系统创建我们的训练环境。在这一步中,我们将测试 cart pole 的工作机制,并在没有任何模型训练的情况下相应地评估分数,以了解它的表现如何。前一篇文章中也提到了这一步,我建议查看一下,以便更好地理解所达到的低分数。

environment_name = "CartPole-v0"
env = gym.make(environment_name)

episodes = 5

for episode in range(1, episodes+1):
    state = env.reset()
    done = False
    score = 0 

    while not done:
        env.render()
        action = env.action_space.sample()
        n_state, reward, done, info = env.step(action)
        score+=reward
    print('Episode:{} Score:{}'.format(episode, score))

env.close()

训练 RL 算法:

在这一步中,我们将选择首选的强化学习算法和策略来完成这项任务。对于本文中的任务和项目,我们将利用近似策略优化强化学习算法。请随意试验由稳定基线 3 库提供的不同类型的算法和策略。其他一些选项包括 A2C、DQN、SAC 和其他类似的算法。

在我们开始训练我们的强化学习模型之前,我们将专注于添加和创建一些必要的回调,包括用于查看各自日志的 Tensorboard 回调。对于这一步,我们需要创建适当的目录来存储 tensorboard 生成的日志。使用 Python 代码或直接在各自的平台中创建文件夹。一旦创建了这些文件夹,我们就可以加入日志和模型的路径。

training_log_path = os.path.join(log_path, 'PPO_3')
!tensorboard --logdir={training_log_path}

在下一步中,一旦达到我们要求的阈值,我们将利用一些其他回调来停止火车。我们还将为经过训练的模型以及它们生成的日志定义保存路径和日志路径。为了训练模型,我们将使用 MLP 策略定义 PPO 算法,并定义张量板路径。然后,我们将使用定义的回调对模型进行总计 20000 个时间步长的训练。我们的目标是在每一集的推车杆子问题上取得至少 200 分。最后,用下面提供的代码片段训练模型。

from stable_baselines3.common.callbacks import EvalCallback, StopTrainingOnRewardThreshold
import os

save_path = os.path.join('Training', 'Saved Models')
log_path = os.path.join('Training', 'Logs')

env = gym.make(environment_name)
env = DummyVecEnv([lambda: env])

stop_callback = StopTrainingOnRewardThreshold(reward_threshold=190, verbose=1)
eval_callback = EvalCallback(env, 
                             callback_on_new_best=stop_callback, 
                             eval_freq=10000, 
                             best_model_save_path=save_path, 
                             verbose=1)

model = PPO('MlpPolicy', env, verbose = 1, tensorboard_log=log_path)

model.learn(total_timesteps=20000, callback=eval_callback)

现在,您可以继续直接评估模型,或者继续保存模型以加载并在以后重用它。您可以执行以下操作,如下所示。我们将在接下来的小节中更详细地讨论这几个步骤。

model_path = os.path.join('Training', 'Saved Models', 'best_model')
model = PPO.load(model_path, env=env)

evaluate_policy(model, env, n_eval_episodes=10, render=True)
env.close()

请注意,您还可以使用不同的算法,并针对您正在处理的特定项目相应地更改策略。我建议试验和尝试各种组合,看看模型收敛得有多快,以及哪个模型更适合特定的问题。

保存和加载模型:

如前所述,我们可以创建保存路径,一旦训练完成,我们将在该路径中保存已训练的模型。这个操作可以用 model.save()函数来执行。然而,因为我们已经为下面的操作使用了回调,所以我们不必担心这一步。然而,下面的代码显示了保存您想要的模型的另一种方法。

PPO_path = os.path.join('Training', 'Saved Models', 'PPO_model')
model.save(PPO_path)

如果您想要加载相同的模型,那么您可以使用下面显示的代码。

model = PPO.load('Training/Saved Models/PPO_model', env=env)

评估和测试模型:

在这个项目的最后一部分,我们将评估和测试训练好的模型。以下代码显示了如何评估总共十集的特定策略。

from stable_baselines3.common.evaluation import evaluate_policy
evaluate_policy(model, env, n_eval_episodes=10, render=True)
env.close()

最后,我们需要测试和评估我们训练好的模型的性能。对于这一步,我们将利用评估策略并测试总共十集的模型。渲染环境,并在渲染的环境上测试模型。理想情况下,训练好的模型应该能够在我们运行训练好的模型的每个间隔(情节)中获得 200 分。评估完成后,我们可以继续关闭测试环境。

obs = env.reset()
while True:
    action, _states = model.predict(obs)
    obs, rewards, done, info = env.step(action)
    env.render()
    if done: 
        print('info', info)
        break

 env.close()

如果您想要测试已训练的模型并运行已保存的模型,下面提供了以下内容的完整代码。我们基本上是导入所有需要的库,为 cart pole 问题创建体育馆环境,加载训练好的模型,并执行评估来检查我们在训练好的模型上取得的结果。

import gym 
from stable_baselines3 import PPO
from stable_baselines3.common.vec_env import DummyVecEnv
from stable_baselines3.common.evaluation import evaluate_policy
import os

environment_name = "CartPole-v0"
env = gym.make(environment_name)

model = PPO.load('Training/Saved Models/PPO_model')

obs = env.reset()
while True:
    action, _states = model.predict(obs)
    obs, rewards, done, info = env.step(action)
    env.render()
    if done: 
        print('info', info)
        break

env.close()

完成后,让我们继续本文的下一部分。我们现在将为自动驾驶汽车项目构建一个强化学习模型。


用强化学习开发自动驾驶汽车项目:

Screenshot by Author on a partially trained model

在本文的第二个项目中,我们将了解如何为自动驾驶汽车开发强化学习模型。正如我们从上面的图像中看到的,我们可以训练我们的强化学习模型,以实现赛车在赛道上的每一个时刻都获得理想的分数。然而,当赛车偏离轨道时,我们会否定它的一些进步。利用这条学习曲线,我们可以相应地训练汽车,以达到所需的结果,并确保汽车沿着正确的轨道行驶,而不会跑偏。请注意,当我们在这个项目中工作时,会有几种情况,赛车偏离了轨道,并持续烧毁。这通常是好的,即使它可能会持续几秒钟。

导入基本库:

在导入基本库之前,请确保您已经阅读了本文前面的项目结构入门一节。确保您已经安装了 box-2D、Pyglet、SWIG 和项目的其他必要需求。一旦成功安装了所有的依赖项,我们将继续调用库来完成这个任务。OpenAI gym 创建我们的环境,稳定的基线来导入我们的策略,向量化环境,并评估策略。查看下面的代码片段,了解程序。

import gym 
from stable_baselines3 import PPO
from stable_baselines3.common.vec_env import VecFrameStack
from stable_baselines3.common.evaluation import evaluate_policy
import os
gym.logger.set_level(40)

测试环境:

奖励是每帧-0.1,每个被访问的轨迹块+1000/N,其中 N 是轨迹中被访问的块的总数。比如你完成了 732 帧,你的奖励就是 1000 - 0.1*732 = 926.8 分。当代理人持续获得 900+点时,游戏被认为已解决。但是,每集生成的轨迹完全是随机的。为了达到最佳效果,我们将需要训练强化学习模型相当长的时间,这取决于您拥有的系统组件的类型。因此,为了达到这项任务的最佳效果,耐心是必需的,因为在训练过程中你将会面临几次汽车烧毁。

在接下来的代码片段中,我们将测试自动驾驶汽车所需的环境。类似于我们如何测试 cart pole 实验,我们将为自动驾驶汽车问题构建测试环境。我们将使用 OpenAI gym 构建环境,并继续渲染环境以评估默认汽车的分数和性能。我们会注意到汽车不能如预期的那样运行,我们的任务是使用强化学习来提高它的整体性能。

environment_name = "CarRacing-v0"

env = gym.make(environment_name)

episodes = 5
for episode in range(1, episodes+1):
    state = env.reset()
    done = False
    score = 0 

    while not done:
        env.render()
        action = env.action_space.sample()
        n_state, reward, done, info = env.step(action)
        score+=reward
    print('Episode:{} Score:{}'.format(episode, score))

env.close()

样本输出:

Track generation: 1207..1513 -> 306-tiles track
Episode:1 Score:-34.426229508197295
Track generation: 1180..1479 -> 299-tiles track
Episode:2 Score:-36.2416107382556
Track generation: 1170..1467 -> 297-tiles track
Episode:3 Score:-32.43243243243292
Track generation: 1012..1269 -> 257-tiles track
Episode:4 Score:-25.781250000000323
Track generation: 1181..1482 -> 301-tiles track
Episode:5 Score:-33.333333333333876 

如果您想了解关于环境的更多信息,可以查看如下操作示例。

env.action_space.sample()

和观察空间如下。

env.observation_space.sample()

完整详细标注的源代码也是 OpenAI 针对自动驾驶汽车问题提供的。在这个链接的帮助下,查看者可以访问源代码。

训练模型:

在下一步中,我们将训练近似策略优化强化学习算法,以解决我们在前面几节中定义的自动驾驶汽车的任务。第一步是确保您已经创建了一个包含 logs 文件夹的培训目录。您可以使用 os.makedirs 命令来执行此操作,或者直接在平台中创建新目录。我们将指定这些日志的路径来存储张量板的评估结果。然后,我们将继续使用 CNN 策略创建 PPO 算法,并为总共 20000 个步骤训练该模型。如果您的计算资源系统允许您在期望的时间内训练更多的时间步长,建议相应地延长时间步长。训练的越多,对模型的学习和适应越好。下面的代码块说明了如何运行下面的代码。

log_path = os.path.join('Training', 'Logs')
model = PPO("CnnPolicy", env, verbose=1, tensorboard_log=log_path)
model.learn(total_timesteps=20000)

保存已训练的模型:

一旦您完成了模型的训练,强烈建议您相应地保存这些模型。为保存的模型创建目录,您将在其中存储您已经成功训练的模型。这种模型的保存不仅允许您重用模型,还允许您在需要时为更多的时期重建和重新训练模型。这一步很有帮助,尤其是在以下场景中,您将需要多个时间步长来完善模型,以始终获得 900+的分数。下面是保存训练模型的代码片段。

ppo_path = os.path.join('Training', 'Saved Models', 'PPO_Driving_model')
model.save(ppo_path)

评估和测试模型:

一旦我们训练并保存了强化学习模型,我们将继续评估策略并进行相应的测试。下面是评估训练模型的代码片段。

evaluate_policy(model, env, n_eval_episodes=10, render=True)
env.close()

在下一个代码块中,我们将通过测试来验证定型模型的性能。我们将在模型上使用 predict 函数来同时处理渲染环境上的动作。检查下面用于分析模型的代码。

obs = env.reset()
while True:
    action, _states = model.predict(obs)
    obs, rewards, dones, info = env.step(action)
    env.render()

env.close()

如果您只想在不同的笔记本或 python 文件中运行已保存的已训练模型,则可以在训练完成后使用下面的代码片段。类似于前面的导入,导入我们在这个任务中需要的所有基本库。然后,我们将使用 OpenAI gym 创建赛车环境,加载我们之前训练的近似策略优化(PPO)模型,并相应地评估该模型。评估过程完成后,我们可以关闭环境。

import gym 
from stable_baselines3 import PPO
from stable_baselines3.common.vec_env import VecFrameStack
from stable_baselines3.common.evaluation import evaluate_policy
import os
gym.logger.set_level(40)

environment_name = "CarRacing-v0"
env = gym.make(environment_name)

model = PPO.load('Training/Saved Models/PPO_Driving_model_2')

evaluate_policy(model, env, n_eval_episodes=10, render=True)
env.close()

注意,为了使训练模型具有更好的性能,建议运行大量时间步长的 RL 模型,以便训练模型可以适应多种情况,并在每次执行评估时获得高分。随着时间步长的降低,很容易遇到大量的汽车烧毁,这是完全没有问题的。确保您在每个培训间隔都保存模型,然后在有时间和资源时继续重新培训。请随意尝试各种不同的项目,尝试不同的实验来加深对强化学习的理解。


结论:

Roadtrip

Photo by Hannes Egler / Unsplash

有了这两篇关于强化学习的文章,我们可以用各种 RL 算法和策略执行几乎任何类型的项目。OpenAI gym 环境有更多的环境,如 Atari 游戏、机器人模拟等等,这个领域的研究人员和爱好者可以探索和了解更多。我会鼓励去看看其他与我们可以通过强化学习开发的各种可能性相关的东西和项目。我建议重新访问 OpenAI gym 和 stable-baselines3 库,以获得关于这些主题的更多信息,并尝试更多的实验。我也强烈推荐看看下面的 YouTube 频道,它深入地介绍了强化学习。

在本文中,我们简要介绍了强化学习的一些有用的应用。然后我们理解了 OpenAI gym 的重要性和 PyTorch 环境中第三版稳定基线的效用。我们还研究了完成本博客中提到的项目所需的其他一些主要设备,如 SWIG、box-2D 和 Pyglet。然后,我们继续构建 cart pole 项目,类似于上一篇文章中介绍的项目。然而,我们注意到一种不同的实现方法,使用 PyTorch 代替 TensorFlow。最后,我们解决了如何用强化学习训练自动驾驶汽车的问题。

在以后的文章中,我们将使用 transformers 和 BERT 进行一些自然语言处理项目。我们也将理解自动编码器的概念和从零开始构建神经网络。在那之前,继续探索强化学习的世界,以及你可以用它构建的众多令人惊叹的项目!

自然语言处理中基于提示的学习范式——第一部分

原文:https://blog.paperspace.com/prompt-based-learning-in-natural-language-processing/

基于提示的自然语言处理是最近人们讨论的自然语言处理领域最热门的话题之一。基于提示的学习通过利用预先训练的语言模型在大量文本数据上获得的知识来解决各种类型的下游任务,例如文本分类、机器翻译、命名实体检测、文本摘要等,这是有充分理由的。而那也是在放松了 的约束下,首先没有任何任务特定的数据 *。*与传统的监督学习范式不同,在传统的监督学习范式中,我们训练一个模型来学习一个将输入 x 映射到输出 y 的函数,这里的想法是基于直接对文本的概率进行建模的语言模型。

你可以在这里问一些有趣的问题,我可以用 GPT 做机器翻译吗? *可以用 BERT 做情感分类吗?*所有这些都不需要专门训练他们完成这些任务。这正是基于提示的自然语言处理的优势所在。因此,在这篇博客中,我们将尝试从这篇详尽且文笔优美的论文中总结出一些初始片段——预训练、提示和预测:自然语言处理中提示方法的系统调查 *。*在这篇博客中,我们讨论了 NLP 中存在的各种类型的学习范例、基于提示的范例中经常使用的符号、基于提示的学习的演示应用,并讨论了在设计提示环境时需要考虑的一些设计问题。这个博客是 3 个博客系列的第 1 部分,将很快讨论论文的其他细节,如这样一个系统的挑战,学习自动设计提示等。

自然语言处理学习空间的演化

  • 范例 1: 完全监督(非神经网络) —这是最初的日子,那时 TF-IDF 和其他人工设计的支持向量机、决策树、K 近邻等功能被认为是时尚的
  • 范式二: 全监督(神经网络) —计算得到了一点便宜,神经网络的研究终于有了进展。那时,使用具有长短期记忆的单词 2 vec(T7)、LSTM(T8)、其他深度神经网络架构等变得流行起来。
  • 范式 3: 预训练,微调——切到 2017 年左右至今,这些日子里,在特定任务上微调预训练模型像 BERTCNN 等是最流行的方法论。
  • 范式四: 预训练、提示、预测 —从去年开始,大家都在谈论基于提示的 NLP。不像以前的学习模式,我们试图推动模型以适应数据,这里我们试图调整数据以适应预先训练的模型。

Learning paradigm in NLP

Paradigms in NLP Learning Space| Source: https://arxiv.org/pdf/2107.13586v1.pdf

直到 **范式3:**预训练、微调 ,使用语言模型作为几乎每个任务的基础模型都不存在。这就是为什么我们在上图的方框中没有看到“任务关系”列下的箭头。同样,如上所述,利用 **基于提示的学习,其思想是设计输入以适合模型 。**上表中用传入箭头描述了同样的情况,指向 LM(语言模型),其中的任务是 CLS(分类)、TAG(标记)、GEN(生成)。

提示符号

从下图可以看出,我们从输入 (x) (比方说一个电影评论)和输出预期 (y)开始。第一个任务是使用提示函数(图中提到的Fprompt***)重新格式化这个输入,其输出表示为*(x’)。现在,我们的语言模型的任务是预测代替占位符 Z.z 值,然后对于槽 Z 被答案填充的提示,我们称之为填充提示,,如果答案为真,我们称之为已回答提示**。

prompt learning notations

Terminology in Prompting | Source: https://arxiv.org/pdf/2107.13586v1.pdf

应用程序

这个范例的一些流行应用是文本生成、问题回答、推理、命名实体识别、关系提取、文本分类等。

  • 文本生成 - 文本生成涉及到生成文本,通常以其他一些信息为条件。随着在自回归设置中训练的模型的使用,文本生成的任务变得自然。通常,所设计的提示在本质上以触发器标记为前缀,作为模型开始生成过程的提示。
  • 问答 - 问答(QA)旨在回答给定的输入问题,通常基于上下文文档。例如,给定一篇输入文章,如果我们想得到文章中提到的所有名字,我们可以将提示表述为“生成上面文章中提到的所有人名”。我们的模型现在的行为类似于文本生成,其中问题成为前缀。
  • 命名实体识别——命名实体识别(NER)是在给定的句子中识别命名实体(如人名、地点)的任务。例如- 如果输入是“Prakhar 喜欢打板球”,要确定“Prakhar”是什么类型的实体,我们可以将提示公式化为**“prak har 是一个 Z 实体”**,预先训练好的语言模型生成的答案空间 Z 应该是 person、organization 等,其中“person”的概率最高。
  • 关系抽取——关系抽取是预测给定句子中两个实体之间关系的任务。这个视频讲解讲的是在零镜头设置下,使用预先训练的语言模型,将关系提取任务建模为自然语言推理任务。

https://www.youtube.com/embed/eUGc0BHUmNI?feature=oembed

  • 文本分类 - 文本分类是给给定的文本片段分配一个预定义的标签的任务。该任务的可能提示可以是 “本文档的主题是 z”,然后将其馈入 mask 预训练语言模型进行槽填充。

随时查看 GPT 3 号游乐场并测试您的提示-

OpenAI GPT-3 Playground | GPT-3 DemoAvailable after getting access to the GPT-3 API On November 18, 2021, OpenAI announced the broadened availability of its OpenAI API service, which enabl...GPT-3 DemoGPT-3 Demo

GPT-3 Playground Demo

演示

我们将使用 OpenPrompt - 一个用于提示学习的开源框架 来编码一个基于提示的文本分类用例。它支持来自 huggingface transformers 的预训练语言模型和记号化器。

您可以使用如下所示的简单 pip 命令安装该库

>> pip install openprompt

我们模拟一个 2 类问题,其中体育健康。我们还定义了三个输入示例,我们对这些示例的分类标签感兴趣。

from openprompt.data_utils import InputExample
classes = [ 
    "Sports",
    "Health"
]
dataset = [
    InputExample(
        guid = 0,
        text_a = "Cricket is a really popular sport in India.",
    ),
    InputExample(
        guid = 1,
        text_a = "Coronavirus is an infectious disease.",
    ),
    InputExample(
        guid = 2,
        text_a = "It's common to get hurt while doing stunts.",
    )
]

Defining Input Examples

接下来,我们加载我们的语言模型,我们选择 RoBERTa 来实现我们的目的。

from openprompt.plms import load_plm
plm, tokenizer, model_config, WrapperClass = load_plm("roberta", "roberta-base")

Loading Pre-trained Language Models

接下来,我们定义了我们的 模板 ,它允许我们动态地将我们的输入示例存储在“text_a”变量中。{"mask"}标记是模型填充的内容。随意查看如何写模板?了解设计您的产品的更多详细步骤。

from openprompt.prompts import ManualTemplate
promptTemplate = ManualTemplate(
    text = '{"placeholder":"text_a"} It was {"mask"}',
    tokenizer = tokenizer,
)

Defining Templates

接下来,我们定义描述器,它允许我们将模型的预测投射到预定义的类别标签上。随意结账怎么写大字报?了解设计您的产品的更多详细步骤。

from openprompt.prompts import ManualVerbalizer
promptVerbalizer = ManualVerbalizer(
    classes = classes,
    label_words = {
        "Health": ["Medicine"],
        "Sports": ["Game", "Play"],
    },
    tokenizer = tokenizer,
)

Defining Verbalizer

接下来,我们通过传入必要的参数(如模板、语言模型和描述符)来创建我们的提示模型用于分类。

from openprompt import PromptForClassification
promptModel = PromptForClassification(
    template = promptTemplate,
    plm = plm,
    verbalizer = promptVerbalizer,
)

接下来,我们创建我们的数据加载器,用于从数据集中进行小批量采样。

from openprompt import PromptDataLoader
data_loader = PromptDataLoader(
        dataset = dataset,
        tokenizer = tokenizer,
        template = promptTemplate,
        tokenizer_wrapper_class=WrapperClass,
    )

Defining Dataloader

接下来,我们将我们的模型设置为评估模式,并以屏蔽语言模型 (MLM)的方式对每个输入示例进行预测。

import torch

promptModel.eval()
with torch.no_grad():
     for batch in data_loader:
         logits = promptModel(batch)
         preds = torch.argmax(logits, dim = -1)
         print(tokenizer.decode(batch['input_ids'][0], skip_special_tokens=True), classes[preds])

Making predictions

下面的代码片段显示了每个输入示例的输出。

>> Cricket is a really popular sport in India. The topic is about Sports
>> Coronavirus is an infectious disease. The topic is about Health
>> It's common to get hurt while doing stunts. The topic is about Health

Output predictions

提示的设计考虑

这里我们讨论一些基本的 设计考虑 在设计提示环境时可以使用

  1. 预训练模型的选择 —这是设计整个提示系统非常重要的步骤之一。预先训练的目标和训练风格推导出下游任务的任何模型的适用性。例如,类似 BERT 的目标可以用于分类任务,但不太适合文本生成任务,而基于像 GPT 这样的自回归训练策略的模型非常适合自然语言生成任务。
  2. 设计提示 —一旦预先训练的模型被固定,设计提示/信号并以返回期望答案的方式格式化输入文本又是一项非常重要的任务。它对系统的整体精度有很大的影响。一种显而易见的方法是手工制作这些提示,但是考虑到这种方法的局限性,即劳动强度大且耗时的过程,已经进行了广泛的研究来自动化提示生成过程。一个提示的例子可以是,比如说— X 总的来说,这是一部 Z 电影 。这里,X 是回顾(原始输入),Z 是我们的模型预测的,整个 【粗体】 序列被称为提示。
  3. 设计答案*——每个任务都会有自己的一组标记,这些标记通常出现在特定任务的语料库中。提出这样一个集合也很重要,然后有一个 映射函数将这些标记翻译成实际的答案/标签 是我们必须设计的另一件事。比如——“*我爱这部电影。总的来说,这是一部 Z 电影。**在这个句子中,模型可能会预测很棒、棒极了、非常好、很好等,种单词来代替 z。假设我们的任务是检测情绪,那么我们需要将这些单词(非常好、很棒等)映射到它们相应的标签,即非常积极的标签。
  4. 基于提示的训练策略 :可能会出现我们有训练数据可用于下游任务的情况。在那些情况下,我们可以 派生方法来训练参数,或者是提示符,或者是 LM,或者是两者都是

有趣的是,在 NLP 中出现了一股新的研究潮流,处理最少的训练数据,并利用大型预训练语言模型。在本博客的后续部分,我们将扩展到上述每个设计考虑事项。

参考

  1. 预训练、提示和预测:自然语言处理中提示方法的系统调查

PyTorch 101,第 3 部分:深入 PyTorch

原文:https://blog.paperspace.com/pytorch-101-advanced/

读者们好,这是我们正在做的 PyTorch 系列的又一篇文章。这篇文章的目标读者是 PyTorch 用户,他们熟悉 PyTorch 的基础知识,并且希望进入中级水平。虽然我们已经在之前的帖子中介绍了如何实现基本的分类器,但在这篇帖子中,我们将讨论如何使用 PyTorch 实现更复杂的深度学习功能。这篇文章的一些目的是让你明白。

  1. PyTorch 类像nn.Modulenn.Functionalnn.Parameter有什么区别,什么时候用哪个
  2. 如何定制您的培训选项,例如不同层次的不同学习率,不同的学习率计划
  3. 自定义重量初始化

在我们开始之前,让我提醒您这是我们 PyTorch 系列的第 3 部分。

  1. 理解图形,自动微分和亲笔签名
  2. 建立你的第一个神经网络
  3. 深入 PyTorch
  4. 内存管理和使用多个 GPU
  5. 理解挂钩

你可以在 Github repo 这里获得这篇文章(以及其他文章)中的所有代码。


那么,我们开始吧。

你可以在 Github repo 这里获得这篇文章(以及其他文章)中的所有代码。

nn。模块 vs 神经网络。功能的

这是经常发生的事情,尤其是当你阅读开源代码的时候。在 PyTorch 中,层通常被实现为torch.nn.Module对象或torch.nn.Functional函数。用哪个?哪个更好?

正如我们在第 2 部分中所介绍的,torch.nn.Module基本上是 PyTorch 的基石。它的工作方式是首先定义一个nn.Module对象,然后调用它的forward方法来运行它。这是一种面向对象的做事方式。

另一方面,nn.functional以函数的形式提供了一些层/激活,这些函数可以在输入端直接调用,而不是定义一个对象。例如,为了重新缩放图像张量,您在图像张量上调用torch.nn.functional.interpolate

那么我们如何选择什么时候用什么呢?当我们实现的层/激活/损耗有损耗时。

理解有状态性

正常情况下,任何一层都可以看成一个函数。例如,卷积运算只是一堆乘法和加法运算。所以,我们把它作为一个函数来实现是有意义的,对吗?但是等等,这个层包含了我们训练时需要存储和更新的权重。因此,从编程的角度来看,层不仅仅是功能。它还需要保存数据,这些数据会随着我们对网络的训练而变化。

我现在想让你强调这样一个事实,卷积层保存的数据会改变。这意味着该层有一个随着我们训练而变化的状态。为了实现执行卷积运算的函数,我们还需要定义一个数据结构来独立于函数本身保存层的权重。然后,将这个外部数据结构作为函数的输入。

或者为了避免麻烦,我们可以定义一个类来保存数据结构,并将卷积运算作为成员函数。这将真正简化我们的工作,因为我们不必担心函数外部存在有状态变量。在这些情况下,我们更喜欢使用nn.Module对象,这里我们有权重或其他可能定义层行为的状态。例如,在训练和推断过程中,辍学/批量范数层的行为会有所不同。

另一方面,在不需要状态或权重的情况下,可以使用nn.functional。例如,调整大小(nn.functional.interpolate)、平均池(nn.functional.AvgPool2d)。

尽管有上面的推理,大多数的nn.Module类都有它们的nn.functional对应物。然而,在实际工作中要尊重上述推理。

nn。参数

PyTorch 中的一个重要类是nn.Parameter类,令我惊讶的是,它在 PyTorch 的介绍性文本中几乎没有涉及到。考虑下面的情况。

class net(nn.Module):
  def __init__(self):
    super().__init__()
    self.conv = nn.Linear(10,5)

  def forward(self, x):
    return self.linear(x)

myNet = net()

#prints the weights and bias of Linear Layer
print(list(myNet.parameters())) 

每个nn.Module都有一个parameters()函数,它返回可训练的参数。我们必须隐式定义这些参数是什么。在nn.Conv2d的定义中,PyTorch 的作者将权重和偏差定义为层的参数。然而,请注意,当我们定义net时,我们不需要将nn.Conv2dparameters添加到netparameters。这是通过将nn.Conv2d对象设置为net对象的成员而隐式发生的。

这是由nn.Parameter类在内部实现的,它是Tensor类的子类。当我们调用一个nn.Module对象的parameters()函数时,它返回所有属于nn.Parameter对象的成员。

事实上,nn.Module类的所有训练权重都被实现为nn.Parameter对象。每当一个nn.Module(在我们的例子中为nn.Conv2d)被指派为另一个nn.Module的成员时,被指派对象的“参数”(即nn.Conv2d的权重)也被添加到被指派对象的“参数”(net对象的参数)中。这被称为注册nn.Module的“参数”

如果你试图给nn.Module对象分配一个张量,它不会出现在parameters()中,除非你将其定义为nn.Parameter对象。这样做是为了方便可能需要缓存一个不可微张量的场景,例如在 RNNs 的情况下缓存以前的输出。

class net1(nn.Module):
  def __init__(self):
    super().__init__()
    self.conv = nn.Linear(10,5)
    self.tens = torch.ones(3,4)                       # This won't show up in a parameter list 

  def forward(self, x):
    return self.linear(x)

myNet = net1()
print(list(myNet.parameters()))

##########################################################

class net2(nn.Module):
  def __init__(self):
    super().__init__()
    self.conv = nn.Linear(10,5) 
    self.tens = nn.Parameter(torch.ones(3,4))                       # This will show up in a parameter list 

  def forward(self, x):
    return self.linear(x)

myNet = net2()
print(list(myNet.parameters()))

##########################################################

class net3(nn.Module):
  def __init__(self):
    super().__init__()
    self.conv = nn.Linear(10,5) 
    self.net  = net2()                      # Parameters of net2 will show up in list of parameters of net3

  def forward(self, x):
    return self.linear(x)

myNet = net3()
print(list(myNet.parameters())) 

nn。ModuleList 和 nn。参数列表()

我记得当我在 PyTorch 中实现 YOLO v3 时,我不得不使用一个nn.ModuleList。我必须通过解析包含架构的文本文件来创建网络。我将所有对应的nn.Module对象存储在一个 Python 列表中,然后将该列表作为我的代表网络的nn.Module对象的成员。

简单来说,大概是这样。

layer_list = [nn.Conv2d(5,5,3), nn.BatchNorm2d(5), nn.Linear(5,2)]

class myNet(nn.Module):
  def __init__(self):
    super().__init__()
    self.layers = layer_list

  def forward(x):
    for layer in self.layers:
      x = layer(x)

net = myNet()

print(list(net.parameters()))  # Parameters of modules in the layer_list don't show up.

正如你所看到的,不像我们注册单个模块,分配一个 Python 列表并不注册列表中模块的参数。为了解决这个问题,我们用nn.ModuleList类包装我们的列表,然后将它指定为 network 类的成员。

layer_list = [nn.Conv2d(5,5,3), nn.BatchNorm2d(5), nn.Linear(5,2)]

class myNet(nn.Module):
  def __init__(self):
    super().__init__()
    self.layers = nn.ModuleList(layer_list)

  def forward(x):
    for layer in self.layers:
      x = layer(x)

net = myNet()

print(list(net.parameters()))  # Parameters of modules in layer_list show up.

类似地,可以通过将列表包装在一个nn.ParameterList类中来注册一个张量列表。

重量初始化

重量初始化会影响您的训练结果。此外,对于不同种类的层,你可能需要不同的权重初始化方案。这可以通过modulesapply功能来完成。modulesnn.Module类的成员函数,它返回一个迭代器,包含一个nn.Module函数的所有成员nn.Module成员对象。然后使用apply函数可以在每个 nn 上调用。用于设置其初始化的模块。

import matplotlib.pyplot as plt
%matplotlib inline

class myNet(nn.Module):

  def __init__(self):
    super().__init__()
    self.conv = nn.Conv2d(10,10,3)
    self.bn = nn.BatchNorm2d(10)

  def weights_init(self):
    for module in self.modules():
      if isinstance(module, nn.Conv2d):
        nn.init.normal_(module.weight, mean = 0, std = 1)
        nn.init.constant_(module.bias, 0)

Net = myNet()
Net.weights_init()

for module in Net.modules():
  if isinstance(module, nn.Conv2d):
    weights = module.weight
    weights = weights.reshape(-1).detach().cpu().numpy()
    print(module.bias)                                       # Bias to zero
    plt.hist(weights)
    plt.show() 

Histogram of weights initialised with Mean = 1 and Std = 1

torch..nn.init模块中有大量的原位初始化功能。

模块()和子模块()

modules非常相似的功能是children。这是一个微小但重要的区别。我们知道,nn.Module对象可以包含其他nn.Module对象作为它的数据成员。

children()将只返回nn.Module对象的列表,这些对象是调用children的对象的数据成员。

另一方面,nn.Modules递归地进入每个nn.Module对象,创建一个每个nn.Module对象的列表,直到没有nn.module对象。注意,modules()也返回它被调用的nn.Module作为列表的一部分。

注意,上面的陈述对于从nn.Module类继承的所有对象/类都是正确的。

class myNet(nn.Module):
  def __init__(self):
    super().__init__()
    self.convBN =  nn.Sequential(nn.Conv2d(10,10,3), nn.BatchNorm2d(10))
    self.linear =  nn.Linear(10,2)

  def forward(self, x):
    pass

Net = myNet()

print("Printing children\n------------------------------")
print(list(Net.children()))
print("\n\nPrinting Modules\n------------------------------")
print(list(Net.modules())) 

因此,当我们初始化权重时,我们可能想要使用modules()函数,因为我们不能进入nn.Sequential对象并为其成员初始化权重。

打印网络信息

我们可能需要打印关于网络的信息,无论是为了用户还是为了调试目的。PyTorch 使用它的named_*函数提供了一种非常简洁的方式来打印大量关于我们网络的信息。有 4 个这样的函数。

  1. named_parameters。返回一个迭代器,该迭代器给出一个元组,该元组包含参数的名称(如果一个卷积层被指定为self.conv1,那么它的参数将是conv1.weightconv1.bias)以及由nn.Parameter__repr__函数返回的值

2.named_modules。同上,但是迭代器像modules()函数一样返回模块。

3.named_children同上,但是迭代器返回模块像children()返回

4.named_buffers返回缓冲张量,如批次范数层的移动平均。

for x in Net.named_modules():
  print(x[0], x[1], "\n-------------------------------")

不同层次的学习率不同

在本节中,我们将学习如何对不同的层使用不同的学习率。总的来说,我们将讨论如何为不同的参数组设置不同的超参数,无论是不同层的不同学习率,还是偏差和权重的不同学习率。

实现这种东西的想法相当简单。在我们之前的文章中,我们实现了一个 CIFAR 分类器,我们将网络的所有参数作为一个整体传递给优化器对象。

class myNet(nn.Module):
  def __init__(self):
    super().__init__()
    self.fc1 = nn.Linear(10,5)
    self.fc2 = nn.Linear(5,2)

  def forward(self, x):
    return self.fc2(self.fc1(x))

Net = myNet()
optimiser = torch.optim.SGD(Net.parameters(), lr = 0.5)

然而,torch.optim类允许我们以字典的形式提供具有不同学习速率的不同参数集。

optimiser = torch.optim.SGD([{"params": Net.fc1.parameters(), 'lr' : 0.001, "momentum" : 0.99},
                             {"params": Net.fc2.parameters()}], lr = 0.01, momentum = 0.9)

在上面的场景中,“fc1”的参数使用 0.01 的学习率和 0.99 的动量。如果没有为一组参数指定超参数(如“fc2 ”),它们将使用该超参数的默认值,作为优化器函数的输入参数。您可以使用上面提到的named_parameters()函数,基于不同的层创建参数列表,或者无论参数是权重还是偏差。

学习率调度

安排你的学习率是你想要调整的一个主要的超参数。PyTorch 通过其torch.optim.lr_scheduler模块提供了对学习率计划的支持,该模块有多种学习率计划。下面的例子演示了这样一个例子。

scheduler = torch.optim.lr_scheduler.MultiStepLR(optimiser, milestones = [10,20], gamma = 0.1)

每当我们到达包含在milestones列表中的时期时,上面的调度程序将学习速率乘以gamma。在我们的例子中,学习率在第 10 个和第 20 个时期乘以 0.1。您还必须在代码的循环中编写一行scheduler.step,遍历各个时期,以便更新学习率。

通常,训练循环由两个嵌套循环组成,其中一个循环遍历历元,而嵌套循环遍历该历元中的批次。确保在纪元循环开始时调用scheduler.step,以便更新你的学习率。注意不要写在批处理循环中,否则你的学习率可能会在第 10 个批处理而不是第 10 个历元更新。

还要记住,scheduler.step不能代替optim.step,而且每次你后退的时候都必须调用optim.step。(这将在“批处理”循环中)。

保存您的模型

您可能想要保存您的模型,以便以后用于推理,或者可能只是想要创建训练检查点。在 PyTorch 中保存模型时,有两种选择。

首先是使用torch.save。这相当于使用 Pickle 序列化整个nn.Module对象。这会将整个模型保存到磁盘上。你可以用torch.load将这个模型载入内存。

torch.save(Net, "net.pth")

Net = torch.load("net.pth")

print(Net)

以上将保存整个模型的权重和架构。如果您只需要保存权重,而不是保存整个模型,您可以只保存模型的state_dictstate_dict基本上是一个字典,它将网络的nn.Parameter对象映射到它们的值。

如上所述,可以将一个现有的state_dict加载到一个nn.Module对象中。请注意,这并不涉及保存整个模型,而只是保存参数。在你加载状态字典之前,你必须创建一个分层的网络。如果网络架构与我们保存的state_dict不完全相同,PyTorch 将抛出一个错误。

for key in Net.state_dict():
  print(key, Net.state_dict()[key])

torch.save(Net.state_dict(), "net_state_dict.pth")

Net.load_state_dict(torch.load("net_state_dict.pth"))

来自torch.optim的优化器对象也有一个state_dict对象,用于存储优化算法的超参数。它可以像我们在上面做的那样,通过在一个优化器对象上调用load_state_dict来保存和加载。

结论

这就完成了我们对 PyTorch 的一些更高级特性的讨论。我希望你在这篇文章中读到的东西将帮助你实现你可能已经想到的复杂的深度学习想法。如果你感兴趣的话,这里有进一步研究的链接。

  1. py torch 中的学习率计划选项列表
  2. 保存和加载模型 PyTorch 官方教程
  3. torch . nn 到底是什么?

PyTorch 101,第 2 部分:构建您的第一个神经网络

原文:https://blog.paperspace.com/pytorch-101-building-neural-networks/

在本文中,我们将讨论如何使用 PyTorch 构建定制的神经网络架构,以及如何配置您的训练循环。我们将实现一个 ResNet 来对来自 CIFAR-10 数据集的图像进行分类。

在我们开始之前,让我说一下,本教程的目的不是为了在任务中达到尽可能好的精确度,而是向您展示如何使用 PyTorch。

让我提醒您,这是 PyTorch 系列教程的第 2 部分。强烈建议阅读第一部分,尽管这不是本文所必需的。

  1. 理解图形,自动微分和亲笔签名
  2. 建立你的第一个神经网络
  3. 深入 PyTorch
  4. 内存管理和使用多个 GPU
  5. 理解挂钩

你可以在 Github repo 这里获得这篇文章(以及其他文章)中的所有代码。


在本帖中,我们将介绍

  1. 如何使用nn.Module类构建神经网络
  2. 如何使用DatasetDataloader类构建带有数据扩充的定制数据输入管道。
  3. 如何使用不同的学习率计划来配置您的学习率
  4. 训练 Resnet bases 图像分类器对来自 CIFAR-10 数据集的图像进行分类。

先决条件

  1. 链式法则
  2. 对深度学习的基本理解
  3. PyTorch 1.0
  4. 本教程的第 1 部分

你可以在 Github repo 这里获得这篇文章(以及其他文章)中的所有代码。

一个简单的神经网络

在本教程中,我们将实现一个非常简单的神经网络。

Diagram of the Network

构建网络

torch.nn模块是 PyTorch 中设计神经网络的基石。这个类可用于实现一个层,如全连接层、卷积层、池层、激活函数,还可以通过实例化一个torch.nn.Module对象来实现整个神经网络。(从现在开始,我将简称它为nn.module)

多个nn.Module对象可以串在一起形成一个更大的nn.Module对象,这就是我们如何使用许多层来实现一个神经网络。事实上,nn.Module可以用来表示 PyTorch 中的任意函数 f

nn.Module类有两个您必须覆盖的方法。

  1. __init__功能。当您创建一个nn.Module的实例时,这个函数被调用。在这里,您将定义层的各种参数,如过滤器、卷积层的内核大小、丢失层的丢失概率。
  2. forward功能。这是您定义如何计算输出的地方。这个函数不需要显式调用,只需调用nn.Module实例就可以运行,就像一个以输入作为参数的函数一样。
# Very simple layer that just multiplies the input by a number
class MyLayer(nn.Module):
  def __init__(self, param):
    super().__init__()
    self.param = param 

  def forward(self, x):
    return x * self.param

myLayerObject = MyLayer(5)
output = myLayerObject(torch.Tensor([5, 4, 3]) )    #calling forward inexplicitly 
print(output)

另一个广泛使用且重要的类是nn.Sequential类。当初始化这个类时,我们可以以特定的顺序传递一个nn.Module对象的列表。nn.Sequential返回的对象本身就是一个nn.Module对象。当这个对象是带输入的 run 时,它会按照我们传递给它的顺序,依次通过所有的nn.Module对象运行输入。

combinedNetwork = nn.Sequential(MyLayer(5), MyLayer(10))

output = combinedNetwork([3,4])

#equivalent to..
# out = MyLayer(5)([3,4])
# out = MyLayer(10)(out)

让我们现在开始实施我们的分类网络。我们将利用卷积层和池层,以及自定义实现的残差块。

Diagram of the Residual Block

虽然 PyTorch 用它的torch.nn模块提供了许多现成的层,但我们必须自己实现剩余的块。在实现神经网络之前,我们先实现 ResNet 块。

class ResidualBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1):
        super(ResidualBlock, self).__init__()

        # Conv Layer 1
        self.conv1 = nn.Conv2d(
            in_channels=in_channels, out_channels=out_channels,
            kernel_size=(3, 3), stride=stride, padding=1, bias=False
        )
        self.bn1 = nn.BatchNorm2d(out_channels)

        # Conv Layer 2
        self.conv2 = nn.Conv2d(
            in_channels=out_channels, out_channels=out_channels,
            kernel_size=(3, 3), stride=1, padding=1, bias=False
        )
        self.bn2 = nn.BatchNorm2d(out_channels)

        # Shortcut connection to downsample residual
        # In case the output dimensions of the residual block is not the same 
        # as it's input, have a convolutional layer downsample the layer 
        # being bought forward by approporate striding and filters
        self.shortcut = nn.Sequential()
        if stride != 1 or in_channels != out_channels:
            self.shortcut = nn.Sequential(
                nn.Conv2d(
                    in_channels=in_channels, out_channels=out_channels,
                    kernel_size=(1, 1), stride=stride, bias=False
                ),
                nn.BatchNorm2d(out_channels)
            )

    def forward(self, x):
        out = nn.ReLU()(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        out += self.shortcut(x)
        out = nn.ReLU()(out)
        return out

如您所见,我们在__init__函数中定义了网络的层或组件。在forward函数中,我们如何将这些组件串在一起,从我们的输入计算输出。

现在,我们可以定义我们的完整网络。

class ResNet(nn.Module):
    def __init__(self, num_classes=10):
        super(ResNet, self).__init__()

        # Initial input conv
        self.conv1 = nn.Conv2d(
            in_channels=3, out_channels=64, kernel_size=(3, 3),
            stride=1, padding=1, bias=False
        )

        self.bn1 = nn.BatchNorm2d(64)

        # Create blocks
        self.block1 = self._create_block(64, 64, stride=1)
        self.block2 = self._create_block(64, 128, stride=2)
        self.block3 = self._create_block(128, 256, stride=2)
        self.block4 = self._create_block(256, 512, stride=2)
        self.linear = nn.Linear(512, num_classes)

    # A block is just two residual blocks for ResNet18
    def _create_block(self, in_channels, out_channels, stride):
        return nn.Sequential(
            ResidualBlock(in_channels, out_channels, stride),
            ResidualBlock(out_channels, out_channels, 1)
        )

    def forward(self, x):
	# Output of one layer becomes input to the next
        out = nn.ReLU()(self.bn1(self.conv1(x)))
        out = self.stage1(out)
        out = self.stage2(out)
        out = self.stage3(out)
        out = self.stage4(out)
        out = nn.AvgPool2d(4)(out)
        out = out.view(out.size(0), -1)
        out = self.linear(out)
        return out

输入格式

现在我们有了网络对象,我们把焦点转向输入。在处理深度学习时,我们会遇到不同类型的输入。图像、音频或高维结构数据。

我们正在处理的数据类型将决定我们使用什么样的输入。一般来说,在 PyTorch 中,你会发现批处理始终是第一维。由于我们在这里处理的是图像,所以我将描述图像所需的输入格式。

图像的输入格式为[B C H W]。其中B是批量,C是通道,H是高度,W是宽度。

我们的神经网络的输出现在是乱码,因为我们使用了随机权重。让我们现在训练我们的网络。

加载数据

现在让我们加载数据。为此,我们将使用torch.utils.data.Datasettorch.utils.data.Dataloader类。

我们首先将 CIFAR-10 数据集下载到与代码文件相同的目录中。

启动终端,cd到您的代码目录并运行以下命令。

wget http://pjreddie.com/media/files/cifar.tgz
tar xzf cifar.tgz

如果你在 macOS 上,你可能需要使用curl或者如果你在 windows 上,你可能需要手动下载它。

我们现在读取 CIFAR 数据集中存在的类的标签。

data_dir = "cifar/train/"

with open("cifar/labels.txt") as label_file:
    labels = label_file.read().split()
    label_mapping = dict(zip(labels, list(range(len(labels))))) 

我们将使用PIL库读取图像。在编写加载数据的功能之前,我们先编写一个预处理函数,它完成以下工作。

  1. 随机水平移动图像,概率为 0.5
  2. 使用 CIFAR 数据集的平均值和标准偏差归一化图像
  3. 将其从W  H  C重塑为C  H  W
def preprocess(image):
    image = np.array(image)

    if random.random() > 0.5:
        image = image[::-1,:,:]

    cifar_mean = np.array([0.4914, 0.4822, 0.4465]).reshape(1,1,-1)
    cifar_std  = np.array([0.2023, 0.1994, 0.2010]).reshape(1,1,-1)
    image = (image - cifar_mean) / cifar_std

    image = image.transpose(2,1,0)
    return image 

通常,PyTorch 提供了两个与构建输入管道以加载数据相关的类。

  1. torch.data.utils.dataset,我们现在称之为dataset类。
  2. torch.data.utils.dataloader,我们现在称之为dataloader类。

torch.utils.data.dataset

dataset是一个加载数据并返回生成器的类,这样你就可以迭代它。它还允许您将数据扩充技术整合到输入管道中。

如果你想为你的数据创建一个dataset对象,你需要重载三个函数。

  1. __init__功能。在这里,您可以定义与数据集相关的内容。最重要的是,你的数据的位置。您还可以定义想要应用的各种数据扩充。
  2. __len__功能。这里,您只需返回数据集的长度。
  3. __getitem__功能。该函数将索引i作为参数,并返回一个数据示例。在我们的训练循环中,这个函数会在每次迭代时被dataset对象用不同的i调用。

这是 CIFAR 数据集的dataset对象的一个实现。

class Cifar10Dataset(torch.utils.data.Dataset):
    def __init__(self, data_dir, data_size = 0, transforms = None):
        files = os.listdir(data_dir)
        files = [os.path.join(data_dir,x) for x in files]

        if data_size < 0 or data_size > len(files):
            assert("Data size should be between 0 to number of files in the dataset")

        if data_size == 0:
            data_size = len(files)

        self.data_size = data_size
        self.files = random.sample(files, self.data_size)
        self.transforms = transforms

    def __len__(self):
        return self.data_size

    def __getitem__(self, idx):
        image_address = self.files[idx]
        image = Image.open(image_address)
        image = preprocess(image)
        label_name = image_address[:-4].split("_")[-1]
        label = label_mapping[label_name]

        image = image.astype(np.float32)

        if self.transforms:
            image = self.transforms(image)

        return image, label 

我们还使用了__getitem__函数来提取编码在文件名中的图像标签。

类允许我们结合惰性数据加载原则。这意味着不是一次将所有数据加载到内存中(这可以通过在__init__函数中加载内存中的所有图像来完成,而不仅仅是地址),而是只在需要的时候加载一个数据示例(当__getitem__被调用时)。

当您创建一个Dataset类的对象时,您基本上可以像遍历任何 python iterable 一样遍历该对象。每次迭代,__getitem__用递增的索引i作为它的输入参数。

数据扩充

我还在__init__函数中传递了一个transforms参数。这可以是任何进行数据扩充的 python 函数。虽然可以在预处理代码中进行数据扩充,但是在__getitem__中进行只是个人喜好的问题。

在这里,我们还可以添加数据增强。这些数据扩充可以作为函数或类来实现。你只需要确保你能够在__getitem__功能中将它们应用到你想要的结果中。

我们有大量的数据扩充库可以用来扩充数据。

对于我们的例子,torchvision library 提供了许多预构建的转换,以及将它们组合成一个更大的转换的能力。但是我们将把我们的讨论限制在 PyTorch。

torch.utils.data.Dataloader

Dataloader类促进了

  1. 数据批处理
  2. 数据混洗
  3. 使用线程一次加载多个数据
  4. 预取,即在 GPU 处理当前批次的同时,Dataloader可以同时将下一批次加载到内存中。这意味着 GPU 不必等待下一批,它加快了训练速度。

用一个Dataset对象实例化一个Dataloader对象。然后你可以迭代一个Dataloader对象实例,就像你迭代一个dataset实例一样。

但是,您可以指定各种选项,以便更好地控制循环选项。

trainset = Cifar10Dataset(data_dir = "cifar/train/", transforms=None)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=128, shuffle=True, num_workers=2)

testset = Cifar10Dataset(data_dir = "cifar/test/", transforms=None)
testloader = torch.utils.data.DataLoader(testset, batch_size=128, shuffle=True, num_workers=2)

trainsettrainloader对象都是 python 生成器对象,可以通过以下方式迭代。

for data in trainloader:   # or trainset
	img, label = data

但是,Dataloader类比Dataset类方便多了。在每次迭代中,Dataset类只会返回给我们__getitem__函数的输出,Dataloader做的远不止这些。

  1. 请注意,trainset的方法__getitem__返回了一个形状为3 x 32 x 32的 numpy 数组。Dataloader将图像批量化为形状张量 128 x 3 x 32 x 32。(因为batch_size = 128 在我们的代码中)。
  2. 还要注意,当我们的__getitem__方法输出一个 numpy 数组时,Dataloader类自动将其转换成一个Tensor
  3. 即使__getitem__方法返回一个非数字类型的对象,Dataloader类也会将它转换成一个大小为B的列表/元组(在我们的例子中是 128)。假设__getitem__也返回一个字符串,即标签字符串。如果我们在实例化 dataloader 时设置 batch = 128,那么每次迭代,Dataloader都会给我们一个 128 个字符串的元组。

加上预取,多线程加载到上面的好处,几乎每次都首选使用Dataloader类。

培训和评估

在开始编写训练循环之前,我们需要确定超参数和优化算法。PyTorch 通过其torch.optim为我们提供了许多预置的优化算法。

火炬. optim

torch.optim模块为您提供多种与培训/优化相关的功能,例如。

  1. 不同的优化算法(如optim.SGDoptim.Adam)
  2. 能够安排学习速度(使用optim.lr_scheduler)
  3. 对于不同的参数有不同的学习率的能力(我们不会在这篇文章中讨论)。

我们使用交叉熵损失和基于动量的 SGD 优化算法。我们的学习率在第 150 和 200 个纪元时衰减了 0.1 倍。

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")     #Check whether a GPU is present.

clf = ResNet()
clf.to(device)   #Put the network on GPU if present

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(clf.parameters(), lr=0.1, momentum=0.9, weight_decay=5e-4)
scheduler = torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones=[150, 200], gamma=0.1)

在第一行代码中,如果 GPU 编号为 0(如果存在的话),那么device将被设置为cuda:0,否则将被设置为cpu

默认情况下,当我们初始化一个网络时,它驻留在 CPU 上。clf.to(device)将网络移动到 GPU(如果有)。我们将在另一部分更详细地讨论如何使用多个 GPU。我们也可以使用clf.cuda(0)将我们的网络clf转移到 GPU 0。(一般情况下用 GPU 的索引代替0

criterion基本上是一个nn.CrossEntropy类对象,顾名思义,实现了交叉熵损失。它基本上是nn.Module的子类。

然后我们将变量optimizer定义为一个optim.SGD对象。optim.SGD的第一个参数是clf.parameters()。一个nn.Module对象的parameters()函数返回它所谓的parameters(实现为nn.Parameter对象,我们将在下一部分学习这个类,在那里我们将探索高级 PyTorch 功能。现在,把它想象成一个相关的Tensors列表,它们是可以学习的。 clf.parameters()基本上都是我们神经网络的权重。

正如您将在代码中看到的,我们将在代码中对optimizer调用step()函数。当调用step()时,优化器使用梯度更新规则等式更新clf.parameters()中的每个Tensor。使用每个Tensorgrad属性访问渐变

一般来说,对于任何优化器,不管是 SGD、Adam 还是 RMSprop,第一个参数都是它应该更新的Tensors列表。其余的参数定义了各种超参数。

scheduler顾名思义,可以调度optimizer的各种超参数。optimizer用于实例化scheduler。每次我们调用scheduler.step()时,它都会更新超参数

编写训练循环

我们最终训练了 200 个纪元。您可以增加纪元的数量。这在 GPU 上可能需要一段时间。同样,本教程的目的是展示 PyTorch 是如何工作的,而不是达到最佳精度。

我们评估每个时期的分类准确性。

for epoch in range(10):
    losses = []
    scheduler.step()
    # Train
    start = time.time()
    for batch_idx, (inputs, targets) in enumerate(trainloader):
        inputs, targets = inputs.to(device), targets.to(device)

        optimizer.zero_grad()                 # Zero the gradients

        outputs = clf(inputs)                 # Forward pass
        loss = criterion(outputs, targets)    # Compute the Loss
        loss.backward()                       # Compute the Gradients

        optimizer.step()                      # Updated the weights
        losses.append(loss.item())
        end = time.time()

        if batch_idx % 100 == 0:
          print('Batch Index : %d Loss : %.3f Time : %.3f seconds ' % (batch_idx, np.mean(losses), end - start))

          start = time.time()
    # Evaluate
    clf.eval()
    total = 0
    correct = 0

    with torch.no_grad():
      for batch_idx, (inputs, targets) in enumerate(testloader):
          inputs, targets = inputs.to(device), targets.to(device)

          outputs = clf(inputs)
          _, predicted = torch.max(outputs.data, 1)
          total += targets.size(0)
          correct += predicted.eq(targets.data).cpu().sum()

      print('Epoch : %d Test Acc : %.3f' % (epoch, 100.*correct/total))
      print('--------------------------------------------------------------')
    clf.train() 

上面是一大段代码。我没有把它分成更小的部分,以免影响连续性。虽然我已经在代码中添加了注释来通知读者发生了什么,但我现在将解释代码中不那么琐碎的部分。

我们首先在 epoch 开始时调用scheduler.step(),以确保optimizer将使用正确的学习速率。

我们在循环中做的第一件事是将我们的inputtarget移动到 GPU 0。这应该是我们的模型所在的设备,否则 PyTorch 将会抛出错误并停止。

注意我们在向前传球之前调用了optimizer.zero_grad()。这是因为叶子Tensor s(权重是)将保留先前过程的梯度。如果在丢失时再次调用backward,新的渐变将简单地添加到由grad属性包含的早期渐变中。使用 RNNs 时,此功能很方便,但现在,我们需要将梯度设置为零,这样梯度就不会在后续过程之间累积。

我们还将评估代码放在torch.no_grad上下文中,这样就不会为评估创建图形。如果您觉得这令人困惑,您可以回到第 1 部分来更新您亲笔签名的概念。

还要注意,我们在评估之前调用模型上的clf.eval(),然后在评估之后调用clf.train()。PyTorch 中的模型有两种状态eval()train()。状态之间的差异源于状态层,如批处理范数(训练中的批处理统计与推理中的总体统计)和丢弃,它们在推理和训练期间表现不同。eval告诉nn.Module将这些层置于推理模式,而训练告诉nn.Module将其置于训练模式。

结论

这是一个详尽的教程,我们向你展示了如何建立一个基本的训练分类器。虽然这只是一个开始,但是我们已经涵盖了可以让您开始使用 PyTorch 开发深层网络的所有构件。

在本系列的下一部分,我们将研究 PyTorch 中的一些高级功能,这些功能将增强您的深度学习设计。这些包括创建更复杂架构的方法,如何定制培训,例如不同参数有不同的学习率。

进一步阅读

  1. PyTorch 文档
  2. 更多 PyTorch 教程
  3. 如何配合 PyTorch 使用 Tensorboard】

PyTorch 101,第 1 部分:理解图形、自动微分和自动签名

原文:https://blog.paperspace.com/pytorch-101-understanding-graphs-and-automatic-differentiation/

PyTorch 是最重要的 python 深度学习库之一。这是深度学习研究的首选,随着时间的推移,越来越多的公司和研究实验室正在采用这个库。

在这一系列教程中,我们将向您介绍 PyTorch,以及如何最好地利用这些库以及围绕它构建的工具生态系统。我们将首先讨论基本的构建模块,然后讨论如何快速构建定制架构的原型。最后,我们将以几篇关于如何调整代码,以及在出现问题时如何调试代码的文章来结束。

这是我们 PyTorch 101 系列的第一部分。

  1. 理解图形,自动微分和亲笔签名
  2. 建立你的第一个神经网络
  3. 深入 PyTorch
  4. 内存管理和使用多个 GPU
  5. 理解挂钩

你可以在 Github repo 这里获得这篇文章(以及其他文章)中的所有代码。


先决条件

  1. 链式法则
  2. 对深度学习的基本理解
  3. PyTorch 1.0

你可以在 Github repo 这里获得这篇文章(以及其他文章)中的所有代码。

自动微分

PyTorch 上的许多系列教程都是从基本结构的初步讨论开始的。然而,我想先从讨论自动微分开始。

自动微分不仅是 PyTorch 的构造块,也是所有 DL 库的构造块。在我看来,PyTorch 的自动微分引擎,称为亲笔签名的是一个了解自动微分如何工作的出色工具。这不仅有助于你更好地理解 PyTorch,也有助于你理解其他的 DL 库。

现代神经网络架构可以有数百万个可学习的参数。从计算的角度来看,训练神经网络包括两个阶段:

  1. 计算损失函数值的正向传递。
  2. 向后传递以计算可学习参数的梯度。

向前传球非常直接。一层的输出是下一层的输入,依此类推。

反向传递稍微复杂一些,因为它要求我们使用链式法则来计算权重相对于损失函数的梯度。

玩具的例子

让我们以一个非常简单的仅由 5 个神经元组成的神经网络为例。我们的神经网络看起来如下。

A Very Simple Neural Network

下面的等式描述了我们的神经网络。

$ \(b = w _ 1 * a\) $ \(c = w _ 2 * a\) $ \(d = w _ 3 * b+w _ 4 * c\) $ \(L = 10-d\) $

让我们计算每个可学习参数$w$的梯度。

$ \(\ frac { \ partial { L } } { \ partial { w _ 4 } } = \ frac { \ partial { L } } * \ frac { \ partial { d } } { \ partial { w _ 4 } }\) $ $ \ frac { \ partial } { \ partial { w _ 3 } } = \ frac { \ partial } { \ partial } * \ frac { \ partial } { \ partial { w _ 3 } }。

所有这些梯度都是应用链式法则计算出来的。注意,由于梯度的分子分母的显式函数,所以上述等式右侧的所有单个梯度都可以直接计算。


计算图表

我们可以手动计算网络的梯度,因为这非常简单。想象一下,如果你有一个 152 层的网络会怎么样。或者,如果网络有多个分支。

当我们设计软件来实现神经网络时,我们希望找到一种方法,使我们能够无缝地计算梯度,而不管架构类型如何,这样当网络发生变化时,程序员就不必手动计算梯度。

我们以一种叫做计算图的数据结构的形式激发了这个想法。计算图看起来非常类似于我们在上图中制作的图表。然而,计算图中的节点基本上是操作符。这些操作符基本上是数学操作符,但有一种情况除外,在这种情况下,我们需要表示用户定义变量的创建。

注意,为了清楚起见,我们在图中还表示了叶变量$ a,w_1,w_2,w_3,w_4$。然而,应该注意,它们不是计算图的一部分。在我们的图表中,它们所代表的是用户定义变量的特殊情况,我们刚刚将其作为例外情况进行了介绍。

Computation Graph for our very simple Neural Network

变量 b、cd 作为数学运算的结果被创建,而变量 a、w1、w2、w3w4 由用户自己初始化。因为它们不是由任何数学运算符创建的,所以与它们的创建相对应的节点由它们的名称本身来表示。对于图中的所有节点都是如此。


计算梯度

现在,我们准备描述我们将如何使用计算图来计算梯度。

除了叶节点之外,计算图的每个节点都可以被认为是接受一些输入并产生一个输出的函数。考虑从$ w_4c$和$w_3b$产生变量 d 的图的节点。因此我们可以写,

\(d = f(w_3b,w_4c)\)

d is output of function f(x,y) = x + y

现在,我们可以很容易地计算出$f$相对于其输入值$ \ frac { \ partial } { \ partial { w _ 3b } } \(和\) \ frac { \ partial } { \ partial { w _ 4c } } $(都是 1 )的梯度。现在,给进入节点的边标上它们各自的渐变,如下图所示。

Local Gradients

我们对整个图形都这样做。图表看起来像这样。

Backpropagation in a Computational Graph

接下来,我们描述计算该图中任何节点相对于损失$L$的导数的算法。假设我们要计算导数,\(\ frac { \ partial { f } } { \ partial { w _ 4 } }\)

  1. 我们首先追踪从 d 到$ w_4 $的所有可能路径。
  2. 只有一条这样的路。
  3. 我们沿着这条路径乘以所有的边。

如果你看到,这个乘积就是我们用链式法则推导出的表达式。如果从 L 到变量有多条路径,那么我们沿着每条路径乘边,然后把它们加在一起。例如,$ \ frac { \ partial } { \ partial } $的计算方法如下

$ \(\ frac { \ partial { f } } { \ partial { w _ 4 } } = \ frac { \ partial { L } } * \ frac { \ partial { d } } * \ frac { \ partial { b } } { \ partial { a } }+\ frac { \ partial { L } } { \ partial { d } } * \ frac { \ partial { d } } * \ frac { \ partial { c } } { \ partial { a }\) $

我的天啊

现在我们知道了什么是计算图,让我们回到 PyTorch 并理解上面的内容是如何在 PyTorch 中实现的。

张量

Tensor是一种数据结构,是 PyTorch 的基本构建模块。与 numpy 阵列非常相似,除了与 numpy 不同,tensors 被设计为利用 GPU 的并行计算能力。许多张量语法类似于 numpy 数组的语法。

In [1]:  import torch

In [2]: tsr = torch.Tensor(3,5)

In [3]: tsr
Out[3]: 
tensor([[ 0.0000e+00,  0.0000e+00,  8.4452e-29, -1.0842e-19,  1.2413e-35],
        [ 1.4013e-45,  1.2416e-35,  1.4013e-45,  2.3331e-35,  1.4013e-45],
        [ 1.0108e-36,  1.4013e-45,  8.3641e-37,  1.4013e-45,  1.0040e-36]]) 

一个它自己的,Tensor就像一个 numpy ndarray。一个可以让你快速做线性代数选项的数据结构。如果您希望 PyTorch 创建一个对应于这些操作的图形,您必须将Tensorrequires_grad属性设置为 True。

这里的 API 可能有点混乱。PyTorch 中有多种初始化张量的方法。虽然有些方法可以让你在构造函数本身中显式定义requires_grad,但其他方法需要你在创建张量后手动设置它。

>> t1 = torch.randn((3,3), requires_grad = True) 

>> t2 = torch.FloatTensor(3,3) # No way to specify requires_grad while initiating 
>> t2.requires_grad = True

requires_grad具有传染性。这意味着当一个Tensor通过操作其他Tensor而被创建时,如果至少一个用于创建的张量的requires_grad被设置为True,那么结果Tensorrequires_grad将被设置为True

每个Tensor都有一个叫做grad_fn、*、*的属性,它是指创建变量的数学运算符。如果requires_grad设置为假,则grad_fn为无。

在我们的例子中,其中$ d = f(w_3b,w_4c) $, d 的 grad 函数将是加法运算符,因为 f 将其与输入相加。注意,加法运算符也是我们图中的节点,它输出的是 d 。如果我们的Tensor是一个叶节点(由用户初始化),那么grad_fn也是 None。

import torch 

a = torch.randn((3,3), requires_grad = True)

w1 = torch.randn((3,3), requires_grad = True)
w2 = torch.randn((3,3), requires_grad = True)
w3 = torch.randn((3,3), requires_grad = True)
w4 = torch.randn((3,3), requires_grad = True)

b = w1*a 
c = w2*a

d = w3*b + w4*c 

L = 10 - d

print("The grad fn for a is", a.grad_fn)
print("The grad fn for d is", d.grad_fn) 

如果您运行上面的代码,您会得到下面的输出。

The grad fn for a is None
The grad fn for d is <AddBackward0 object at 0x1033afe48>

可以使用成员函数is_leaf来确定变量是否是叶子Tensor

功能

PyTorch 中的所有数学运算都由torch . nn . autograded . function类实现。这个类有两个我们需要了解的重要成员函数。

第一个是它的 forward 函数,它使用输入简单地计算输出。

backward 函数获取来自其前方网络部分的输入梯度。如你所见,从函数 f 反向传播的梯度基本上是从其前面的层反向传播到 f梯度乘以f 的输出相对于其输入的局部梯度**。这正是backward函数的作用。**

让我们再次用$$ d = f(w_3b,w_4c) $$的例子来理解

  1. d 就是我们这里的Tensor。它的grad_fn 就是<ThAddBackward> *。*这基本上是加法运算,因为创建 d 的函数将输入相加。
  2. it's grad_fnforward函数接收输入$w_3b$ $w_4c$并将它们相加。这个值基本上存储在 d
  3. <ThAddBackward>backward函数基本上以来自更远层的输入梯度作为输入。这基本上是$ \ frac { \ partial } { \ partial } $沿着从 Ld. 的边缘而来。该梯度也是 L w.r.t 到 d 的梯度,并存储在dgrad 属性中。可以通过调用d.grad 进入。
  4. 然后计算局部梯度$ \ frac { \ partial } { \ partial { w _ 4c } } \(和\) \ frac { \ partial } { \ partial { w _ 3b } }。
  5. 然后,backward 函数将传入的梯度分别与本地计算的梯度相乘,并且 " 通过调用其输入的grad_fn的 backward 方法将 " 梯度发送到其输入。
  6. 比如与 d 关联的<ThAddBackward>backward函数调用$w_4c$的 grad_fn 的反向函数(这里,$w_4c$是中间张量,它的 grad_fn<ThMulBackward>。调用backward函数时,梯度$ \ frac { \ partial } { \ partial } * \ frac { \ partial } { \ partial { w _ 4c } } $作为输入传递。
  7. 现在,对于变量$w_4c$,$ \ frac { \ partial } { \ partial } * \ frac { \ partial } { \ partial { w _ 4c } } \(成为引入的梯度,就像在步骤 3 中\) \ frac { \ partial } { \ partial } \(对于 *\) d $ 是,并且该过程重复。

从算法上来说,这是计算图的反向传播过程。(非实际执行,仅具有代表性)

def backward (incoming_gradients):
	self.Tensor.grad = incoming_gradients

	for inp in self.inputs:
		if inp.grad_fn is not None:
			new_incoming_gradients = //
			  incoming_gradient * local_grad(self.Tensor, inp)

			inp.grad_fn.backward(new_incoming_gradients)
		else:
			pass 

这里的self.Tensor基本就是亲笔签名创造的Tensor。函数,在我们的例子中是 d

上面已经描述了输入梯度和局部梯度。


为了在我们的神经网络中计算导数,我们通常在代表我们损失的Tensor上调用backward。然后,我们从代表我们损失的grad_fn的节点开始回溯图。

如上所述,当我们回溯时,backward函数在图中被递归调用。有一次,我们到达一个叶节点,由于grad_fn 都不存在,而是停止原路返回。

这里需要注意的一点是,如果你在向量值张量上调用backward(),PyTorch 会给出一个错误。这意味着你只能在标量值张量上调用backward。在我们的例子中,如果我们假设a是一个向量值张量,并在 L 上调用backward,它将抛出一个错误。

import torch 

a = torch.randn((3,3), requires_grad = True)

w1 = torch.randn((3,3), requires_grad = True)
w2 = torch.randn((3,3), requires_grad = True)
w3 = torch.randn((3,3), requires_grad = True)
w4 = torch.randn((3,3), requires_grad = True)

b = w1*a 
c = w2*a

d = w3*b + w4*c 

L = (10 - d)

L.backward()

运行上面的代码片段会导致以下错误。

RuntimeError: grad can be implicitly created only for scalar outputs 

这是因为根据定义,梯度可以相对于标量值来计算。你不能精确区分一个矢量和另一个矢量。用于这种情况的数学实体称为**雅可比矩阵,**关于它的讨论超出了本文的范围。

有两种方法可以克服这一点。

如果你只是对上面的代码设置L做一个小小的改动,使之成为所有错误的总和,我们的问题就解决了。

import torch 

a = torch.randn((3,3), requires_grad = True)

w1 = torch.randn((3,3), requires_grad = True)
w2 = torch.randn((3,3), requires_grad = True)
w3 = torch.randn((3,3), requires_grad = True)
w4 = torch.randn((3,3), requires_grad = True)

b = w1*a 
c = w2*a

d = w3*b + w4*c 

# Replace L = (10 - d) by 
L = (10 -d).sum()

L.backward() 

一旦完成,您就可以通过调用Tensorgrad属性来访问渐变。

第二种方法是,由于某种原因,必须绝对调用向量函数上的backward,你可以传递一个张量形状大小的torch.ones,你试图用它向后调用。

# Replace L.backward() with 
L.backward(torch.ones(L.shape))

注意backward过去是如何将引入的渐变作为输入的。这样做使得backward认为引入的梯度只是与 L 大小相同的张量,并且它能够反向传播。

这样,我们可以为每个Tensor设置梯度,并且我们可以使用我们选择的优化算法来更新它们。

w1 = w1 - learning_rate * w1.grad 

诸如此类。

PyTorch 图与张量流图有何不同

PyTorch 创建了一个叫做的动态计算图,这意味着这个图是动态生成的。

在变量的forward函数被调用之前,图中的Tensor (grad_fn ) 没有节点。

`a = torch.randn((3,3), requires_grad = True)   #No graph yet, as a is a leaf

w1 = torch.randn((3,3), requires_grad = True)  #Same logic as above

b = w1*a   #Graph with node `mulBackward` is created.` 

该图是调用多个张量forward函数的结果。只有这样,为图形和中间值分配的非叶节点的缓冲区(用于以后计算梯度。当您调用backward时,随着梯度的计算,这些缓冲区(用于非叶变量)基本上被释放,并且图被破坏(在某种意义上,您不能通过它反向传播,因为保存值以计算梯度的缓冲区已经不在了)。

下一次,您将在同一个张量集上调用forward上一次运行的叶节点缓冲区将被共享,而非叶节点缓冲区将被再次创建。

如果在有非叶节点的图上多次调用backward,您会遇到下面的错误。

`RuntimeError: Trying to backward through the graph a second time, but the buffers have already been freed. Specify retain_graph=True when calling backward the first time.`

这是因为非叶子缓冲区在第一次调用backward()时被破坏,因此,当第二次调用backward时,没有路径导航到叶子。您可以通过向backward函数添加retain_graph = True参数来撤销这种非叶缓冲区破坏行为。

`loss.backward(retain_graph = True)`

如果您执行上述操作,您将能够通过同一图形再次反向传播,并且梯度将被累积,即,下一次您反向传播时,梯度将被添加到先前反向传递中已存储的梯度中。


这与 TensorFlow 使用的 静态计算图 不同,tensor flow 在运行程序的 之前声明了 。然后,通过向预定义的图形输入值来“运行”图形。

动态图范例允许您在运行时对网络架构进行更改,因为只有在运行一段代码时才会创建一个图。

这意味着一个图可以在程序的生命周期中被重新定义,因为你不必预先定义它。

然而,这对于静态图形是不可能的,在静态图形中,图形是在运行程序之前创建的,只是在以后执行。

动态图也使调试更容易,因为它更容易定位错误的来源。

一些贸易技巧

要求 _grad

这是Tensor类的一个属性。默认情况下,它是 False。当你不得不冻结一些层,并阻止他们在训练时更新参数时,这是很方便的。您可以简单地将requires_grad设置为 False,这些Tensors不会参与计算图形。

因此,没有梯度会传播到这些层,或那些依赖于这些层的梯度流requires_grad。当设置为真时,requires_grad具有传染性,即使运算的一个操作数的requires_grad设置为真,结果也是如此。

火炬号 grad()

当我们计算梯度时,我们需要缓存输入值和中间要素,因为稍后可能需要它们来计算梯度。

\(b = w_1*a\) w.r.t 其输入$w_1$和$a$的梯度分别为$a$和$w_1$。我们需要存储这些值,以便在反向过程中进行梯度计算。这会影响网络的内存占用。

当我们执行推理时,我们不计算梯度,因此,不需要存储这些值。事实上,在推理过程中不需要创建图,因为这将导致无用内存消耗。

PyTorch 提供了一个上下文管理器,为此称为torch.no_grad

`with torch.no_grad:
	inference code goes here` 

没有为在此上下文管理器下执行的操作定义图形。

结论

理解亲笔签名的和计算图形是如何工作的,可以让 PyTorch 的使用变得更加容易。我们的基础坚如磐石,接下来的帖子将详细介绍如何创建定制的复杂架构,如何创建定制的数据管道和更多有趣的东西。

进一步阅读

  1. 链式法则
  2. 反向传播

PyTorch 101,第 5 部分:理解钩子

原文:https://blog.paperspace.com/pytorch-hooks-gradient-clipping-debugging/

读者们好。欢迎来到我们的 PyTorch 调试和可视化教程。至少现在,这是我们 PyTorch 系列的最后一部分,从对图形的基本理解开始,一直到本教程。

在本教程中,我们将介绍 PyTorch 钩子,以及如何使用它们来调试我们的反向传递,可视化激活和修改渐变。

在我们开始之前,让我提醒您这是我们 PyTorch 系列的第 5 部分。

  1. 理解图形,自动微分和亲笔签名
  2. 建立你的第一个神经网络
  3. 深入 PyTorch
  4. 内存管理和使用多个 GPU
  5. 理解挂钩

你可以在 Github repo 这里获得这篇文章(以及其他文章)中的所有代码。

了解 PyTorch 挂钩

PyTorch 中的钩子为桌面带来的功能性严重不足。就像超级英雄的医生命运一样。没听说过他?没错。这才是重点。

我如此喜欢钩子的一个原因是它们让你在反向传播过程中做一些事情。钩子就像是许多英雄留在恶棍巢穴中获取所有信息的工具之一。

你可以在Tensornn.Module登记一个钩子。钩子基本上是一个函数,当forwardbackward被调用时执行。

当我说forward的时候,我不是指一个nn.Moduleforwardforward函数在这里指的是张量的grad_fn对象torch.Autograd.Functionforward函数。你觉得最后一行是胡言乱语吗?我推荐你在 PyTorch 中查看我们关于计算图的文章。如果你只是在偷懒,那么要明白每个张量都有一个grad_fn,它是创建张量的torch.Autograd.Function对象。例如,如果一个张量是由tens = tens1 + tens2创建的,那么它的grad_fn就是AddBackward。还是说不通?你一定要回去看看这篇文章。

注意,像nn.Linear一样的nn.Module有多个forward调用。它的输出由两个操作创建,(Y = W * X + B),加法和乘法,因此将有两个forward调用。这可能会把事情弄糟,并可能导致多个输出。我们将在本文后面更详细地讨论这一点。

PyTorch 提供了两种类型的挂钩。

  1. 向前的钩拳
  2. 向后的钩子

前向钩子是在向前传递的过程中执行的,而后向钩子是在调用backward函数时执行的。再次提醒你,这些是一个Autograd.Function对象的forwardbackward函数。

张量挂钩

一个钩子基本上是一个函数,有一个非常具体的签名。当我们说一个钩子被执行时,实际上,我们说的是这个函数被执行。

对于张量来说,后弯的特征是,

hook(grad) -> Tensor or None 

张量没有forward钩子。

grad基本上就是调用 backward后张量grad属性中包含的值。函数不应该修改它的参数。它必须返回None或一个张量,该张量将代替grad用于进一步的梯度计算。下面我们提供一个例子。

import torch 
a = torch.ones(5)
a.requires_grad = True

b = 2*a

b.retain_grad()   # Since b is non-leaf and it's grad will be destroyed otherwise.

c = b.mean()

c.backward()

print(a.grad, b.grad)

# Redo the experiment but with a hook that multiplies b's grad by 2\. 
a = torch.ones(5)

a.requires_grad = True

b = 2*a

b.retain_grad()

b.register_hook(lambda x: print(x))  

b.mean().backward() 

print(a.grad, b.grad)

如上所述,功能有多种用途。

  1. 您可以打印梯度的值用于调试。您也可以记录它们。这对于梯度被释放的非叶变量尤其有用,除非您对它们调用retain_grad。做后者可以增加记忆保持力。钩子提供了更简洁的方式来聚集这些值。
  2. 您可以在反向过程中修改梯度**。这一点非常重要。虽然您仍然可以访问网络中张量的grad变量,但您只能在完成整个向后传递后才能访问它。例如,让我们考虑一下我们在上面做了什么。我们将b的梯度乘以 2,现在后续的梯度计算,如a(或任何依赖于b梯度的张量)的梯度计算,使用 2 * grad(b)代替 grad(b)。相比之下,如果我们在backward之后单独更新参数**,我们必须将b.grada.grad(或者实际上,所有依赖于b梯度的张量)乘以 2。****
a = torch.ones(5)

a.requires_grad = True
b = 2*a

b.retain_grad()

b.mean().backward() 

print(a.grad, b.grad)

b.grad *= 2

print(a.grad, b.grad)       # a's gradient needs to updated manually 

钩子为 nn。模块对象

对于nn.Module对象,钩子函数的签名,

hook(module, grad_input, grad_output) -> Tensor or None 

对于后弯钩,以及

hook(module, input, output) -> None 

向前勾拳。

在我们开始之前,让我澄清一下,我不喜欢在nn.Module对象上使用钩子。首先,因为它们迫使我们打破抽象。nn.Module应该是一个表示层的模块化对象。然而,一个hook被赋予一个forward和一个backward,其中在一个nn.Module对象中可以有任意数量。这就需要我知道模块化对象的内部结构。

例如,一个nn.Linear在其执行期间包含两个forward调用。乘法和加法(y = w ***** x + b)。这就是为什么钩子函数的input可以是一个包含两个不同的forward调用的输入和output前向调用的输出的元组。

grad_inputnn.Module对象 w.r.t 的输入对损耗的梯度(dL / dx,dL / dw,dL / b)。grad_outputnn.Module对象的输出相对于梯度的梯度。由于在一个nn.Module对象中的多次调用,这些可能会非常不明确。

考虑下面的代码。

import torch 
import torch.nn as nn

class myNet(nn.Module):
  def __init__(self):
    super().__init__()
    self.conv = nn.Conv2d(3,10,2, stride = 2)
    self.relu = nn.ReLU()
    self.flatten = lambda x: x.view(-1)
    self.fc1 = nn.Linear(160,5)

  def forward(self, x):
    x = self.relu(self.conv(x))
    return self.fc1(self.flatten(x))

net = myNet()

def hook_fn(m, i, o):
  print(m)
  print("------------Input Grad------------")

  for grad in i:
    try:
      print(grad.shape)
    except AttributeError: 
      print ("None found for Gradient")

  print("------------Output Grad------------")
  for grad in o:  
    try:
      print(grad.shape)
    except AttributeError: 
      print ("None found for Gradient")
  print("\n")
net.conv.register_backward_hook(hook_fn)
net.fc1.register_backward_hook(hook_fn)
inp = torch.randn(1,3,8,8)
out = net(inp)

(1 - out.mean()).backward()

产生的输出是。

Linear(in_features=160, out_features=5, bias=True)
------------Input Grad------------
torch.Size([5])
torch.Size([5])
------------Output Grad------------
torch.Size([5])

Conv2d(3, 10, kernel_size=(2, 2), stride=(2, 2))
------------Input Grad------------
None found for Gradient
torch.Size([10, 3, 2, 2])
torch.Size([10])
------------Output Grad------------
torch.Size([1, 10, 4, 4])

在上面的代码中,我使用了一个钩子来打印grad_inputgrad_output的形状。现在我对这方面的知识可能是有限的,如果你有其他选择,请做评论,但出于对平克·弗洛伊德的爱,我不知道什么grad_input应该代表什么?

conv2d中你可以通过形状来猜测。尺寸[10, 3, 3, 2]grad_input是重量等级。那个[10]可能是bias。但是输入特征图的梯度呢?None?除此之外,Conv2d使用im2col或它的同类来展平图像,这样整个图像上的卷积可以通过矩阵计算来完成,而不是循环。那里有电话吗?所以为了得到 x 的梯度,我必须调用它后面的层的grad_output

linear令人费解。两个grad_inputs都是大小[5]但是线性层的权重矩阵不应该是160 x 5吗?

对于这样的混乱,我不喜欢用钩子来处理nn.Modules。你可以做像 ReLU 这样简单的事情,但是对于复杂的事情呢?不是我喜欢的。

正确使用钩子的方法:一个观点

所以,我完全赞成在张量上使用钩子。通过使用named_parameters函数,我已经成功地用 PyTorch 完成了我所有的渐变修改/裁剪需求。named_parameters允许我们更多地控制要修补的渐变。这么说吧,我想做两件事。

  1. 反向传播时将线性偏差的梯度变为零。
  2. 确保没有梯度去 conv 层小于 0。
import torch 
import torch.nn as nn

class myNet(nn.Module):
  def __init__(self):
    super().__init__()
    self.conv = nn.Conv2d(3,10,2, stride = 2)
    self.relu = nn.ReLU()
    self.flatten = lambda x: x.view(-1)
    self.fc1 = nn.Linear(160,5)

  def forward(self, x):
    x = self.relu(self.conv(x))
    x.register_hook(lambda grad : torch.clamp(grad, min = 0))     #No gradient shall be backpropagated 
                                                                  #conv outside less than 0

    # print whether there is any negative grad
    x.register_hook(lambda grad: print("Gradients less than zero:", bool((grad < 0).any())))  
    return self.fc1(self.flatten(x))

net = myNet()

for name, param in net.named_parameters():
  # if the param is from a linear and is a bias
  if "fc" in name and "bias" in name:
    param.register_hook(lambda grad: torch.zeros(grad.shape))

out = net(torch.randn(1,3,8,8)) 

(1 - out).mean().backward()

print("The biases are", net.fc1.bias.grad)     #bias grads are zero 

产生的输出是:

Gradients less than zero: False
The biases are tensor([0., 0., 0., 0., 0.])

用于可视化激活的向前挂钩

如果你注意到了,Tensor没有前向钩子,而nn.Module有,当调用forward时执行。尽管我已经强调了将钩子附加到 PyTorch 的问题,但我已经看到许多人使用前向钩子通过将特征映射保存到钩子函数外部的 python 变量来保存中间特征映射。类似这样的。

visualisation = {}

inp = torch.randn(1,3,8,8)

def hook_fn(m, i, o):
  visualisation[m] = o 

net = myNet()

for name, layer in net._modules.items():
  layer.register_forward_hook(hook_fn)

out = net(inp) 

一般来说,一个nn.Moduleoutput是最后一个forward的输出。但是,不使用钩子也可以安全地复制上述功能。只需将nn.Module对象的forward函数中的中间输出添加到一个列表中。不过,打印nn.Sequential内部模块的中间激活可能会有点问题。为了解决这个问题,我们需要将一个钩子注册到 Sequential 的子模块,而不是注册到Sequential本身。

import torch 
import torch.nn as nn

class myNet(nn.Module):
  def __init__(self):
    super().__init__()
    self.conv = nn.Conv2d(3,10,2, stride = 2)
    self.relu = nn.ReLU()
    self.flatten = lambda x: x.view(-1)
    self.fc1 = nn.Linear(160,5)
    self.seq = nn.Sequential(nn.Linear(5,3), nn.Linear(3,2))

  def forward(self, x):
    x = self.relu(self.conv(x))
    x = self.fc1(self.flatten(x))
    x = self.seq(x)

net = myNet()
visualisation = {}

def hook_fn(m, i, o):
  visualisation[m] = o 

def get_all_layers(net):
  for name, layer in net._modules.items():
    #If it is a sequential, don't register a hook on it
    # but recursively register hook on all it's module children
    if isinstance(layer, nn.Sequential):
      get_all_layers(layer)
    else:
      # it's a non sequential. Register a hook
      layer.register_forward_hook(hook_fn)

get_all_layers(net)

out = net(torch.randn(1,3,8,8))

# Just to check whether we got all layers
visualisation.keys()      #output includes sequential layers

最后,您可以将这个张量转换成 numpy 数组并绘制激活图。

结论

这就结束了我们对 PyTorch 的讨论,py torch 是可视化和调试回传的一个非常有效的工具。希望这篇文章能帮助你更快地解决你的问题。

纸空间上的 PyTorch 闪电

原文:https://blog.paperspace.com/pytorch-lightning-on-paperspace/

PyTorch Lightning 是一个使用 PyTorch 进行研究的框架,它简化了我们的代码,而没有带走原始 PyTorch 的功能。它抽象出样板代码,并将我们的工作组织成类,例如,使数据处理和模型训练分离,否则它们会很快混合在一起,难以维护。

通过这种方式,它能够在高级项目上构建和快速迭代,并获得其他方式难以达到的结果。

结合 Paperspace 的易用性及其随时可用的 GPU 硬件,这为希望在实际项目中领先的数据科学家提供了一个绝佳的途径,而不必成为代码和基础设施的全职维护者。

闪电是为了谁?

在其创造者关于闪电的博客中,他们写道

PyTorch Lightning 是为从事人工智能研究的专业研究人员和博士生创建的

这是公平的,要从它为我们的代码提供的结构中获得全部好处,需要做真实大小的项目。反过来,这要求用户已经熟悉 PyTorch、Python 类和深度学习概念。

然而,它并不比那些工具更难使用,而且在某些方面更容易。因此,对于那些正在成为数据科学家的人来说,这是一条很好的路线。

鉴于它的文档,这一点尤其正确,其中包括对 Lightning 能做什么的出色概述,还包括一组介绍 PyTorch 和 Lightning 的教程。

闪电是如何形成的

Lightning 将我们端到端数据科学工作流的主要部分分成几个类。这将数据准备和模型训练分离开来,使事情可以重用,而不必一行一行地检查代码。例如,在新数据集上训练模型所需的更改变得更加清晰。

Lightning 中的主要类别有

  • 照明模块
  • 数据模块
  • 运动鞋

LightningModule 通过使用 PyTorch nn.Module,使深度学习模型能够在 Lightning 中进行训练。这可以包括作为类中独立方法的训练、验证和测试。

DataModule 允许我们将特定数据集所需的所有处理放在一个地方。在现实世界的项目中,从我们的原始数据到模型就绪的东西可能是获得结果所需的全部代码和工作的很大一部分。因此,以这种方式组织它,使数据准备不与模型混合,是非常有价值的。

然后,培训师允许我们一起使用来自上述两个类的数据集和模型,而不必编写更多的工程或样板代码。

在图纸空间上运行闪电

要在 Paperspace 上运行 Lightning,一旦在登录,只需旋转渐变笔记本,选择一台机器,指向我们的回购包含示例.ipynb笔记本,我们就可以开始了。就这么简单!

Paperspace Gradient Notebook creation

或者,我们也可以使用下面的梯度运行按钮,只需点击一下就可以启动笔记本电脑

有关在 Gradient 上运行笔记本的更多信息,请访问我们的笔记本教程

被简化的代码示例

这两张截图来自作者介绍 Lightning 的博客条目,展示了代码被简化的典型例子。

在第一幅图中,数据准备和拆分成训练/验证/测试集的特定线性排列封装在一个数据模块中。每个准备步骤都有自己的方法。

Organization and modularization of data preparation via the Lightning DataModule (from the blog entry introducing Lightning)

在第二幅图中,一屏实例化模型训练循环的样板代码被简化为不到一半长度的 LightningModule,训练和分类步骤依次变成类方法,而不是 for 和 with 循环。

Simplification of model training loop via the LightningModule (from the blog entry introducing Lightning)

虽然对于简单的例子来说,开销可能会很大,但是当全尺寸真实项目的数据准备开始占用许多屏幕的代码时,这种组织以及随之而来的模块化和可重用性只会变得更有价值。

Lightning 文档也有很棒的动画示例,通过典型的数据准备和模型训练过程更详细地展示了代码安排和简化。

还有很多

这篇博客文章对 Lightning 及其在 Paperspace 上的运行做了一个简单而基本的概述。有更多的功能等待着我们去探索,这些功能要么是免费的,要么是无需编写更多代码就可以轻松激活的。

  • 检查点保存和加载
  • 历元和批量迭代
  • 多 GPU 支持
  • 将实验记录到 TensorBoard
  • 优化器是否单步执行、反向传播和零化梯度调用
  • 模型评估期间禁用渐变
  • TPU 和 Graphcore IPU 硬件加速器
  • 16 位自动混合精度(AMP)
  • 导出到 ONNX 或 Torchscript 以进行模型部署
  • 与 DeepSpeed 集成
  • 探查器发现代码瓶颈

最后,Lightning 的一个在线伙伴 Lightning Bolts ( 网站文档回购)包含一系列扩展和模型,可以与 Lightning 一起使用,以进一步增加其功能。

文档和教程

作为一名拥有 20 多年经验的数据科学家,您的作者已经看到了许多产品和网站的文档。Lightning 的文档和教程肯定是我用过的质量最高、最全面的。所有内容都覆盖了高质量的内容,他们“获得”了我们作为数据科学家希望看到的内容。

PyTorch Lightning documentation at readthedocs

这不是一个完全面向初学者的网站,因为在第一个快速入门页面上,我们直接进入了 PyTorch 代码、Python 类和 autoencoder 深度学习模型。你不需要成为所有这些方面的专家,也可以了解正在发生的事情,但是有一些熟悉感是有帮助的。

话虽如此,还是有一套动手的例子和教程,它们和文档一样全面且呈现良好。标有 1-13 的是基于阿姆斯特丹大学深度学习课程的 2020 年版本。

教程 1-13 从介绍 PyTorch 开始,然后转到 Lightning,所以如果你对 py torch-classes-deep learning trifecta 不太熟悉,这些将是一个好去处。

所有的教程都是 Jupyter 笔记本的形式,因此可以在 Paperspace Gradient 上运行,不需要任何设置。

页面上的全套文档是

  • 开始
  • 升级:基础、中级、高级和专家技能
  • 核心 API
  • API 参考
  • 通用工作流程(逐级重新排列)
  • 词汇表
  • 实践示例

结论和后续步骤

我们已经介绍了 PyTorch Lightning,并展示了如何使用它来简化和组织我们现实世界的深度学习项目。

它假设您对 PyTorch、Python 类和深度学习有一定的了解,但是除了 Lightning 之外,它还包含了帮助我们学习 PyTorch 的优秀文档和教程。

结合 Paperspace 在开始编码方面的易用性和可用的 GPU 硬件,这让我们可以直接解决我们的数据科学问题,避免需要时间来设置和编写样板代码。

对于后续步骤,尝试运行 Paperspace 上的教程,或访问以下网站了解更多信息:

除了闪电本身:

PyTorch 损失函数

原文:https://blog.paperspace.com/pytorch-loss-functions/

损失函数是 ML 模型训练中的基础,并且在大多数机器学习项目中,如果没有损失函数,就没有办法驱动您的模型做出正确的预测。通俗地说,损失函数是一个数学函数或表达式,用于衡量模型在某些数据集上的表现。了解模型在特定数据集上的表现有多好,可以让开发人员在训练期间深入了解许多决策,例如使用新的、更强大的模型,甚至将损失函数本身更改为不同的类型。说到损失函数的类型,有几种损失函数是多年来发展起来的,每一种都适用于特定的训练任务。

在本文中,我们将探讨这些不同的损失函数,它们是 PyTorch 神经网络模块的一部分。我们将进一步深入研究 PyTorch 如何通过构建一个自定义的模块 API 向用户公开这些损失函数,作为其 nn 模块 API 的一部分。

现在我们已经对损失函数有了一个高层次的理解,让我们探索一些关于损失函数如何工作的更多技术细节。

什么是损失函数?

我们之前说过,损失函数告诉我们模型在特定数据集上的表现如何。从技术上讲,它是通过测量预测值与实际值的接近程度来做到这一点的。当我们的模型在训练和测试数据集上做出非常接近实际值的预测时,这意味着我们有一个非常稳健的模型。

虽然损失函数为我们提供了关于模型性能的关键信息,但这不是损失函数的主要功能,因为有更稳健的技术来评估我们的模型,如准确性和 F 分数。损失函数的重要性主要是在训练过程中实现的,在训练过程中,我们将模型的权重向损失最小化的方向推动。通过这样做,我们增加了我们的模型做出正确预测的概率,如果没有损失函数,这可能是不可能的。

不同的损失函数适合不同的问题,每个损失函数都由研究人员精心制作,以确保训练期间稳定的梯度流。

有时,损失函数的数学表达式可能有点令人生畏,这导致一些开发人员将它们视为黑盒。我们稍后将揭示 PyTorch 的一些最常用的损失函数,但在此之前,让我们先看看在 PyTorch 的世界中我们是如何使用损失函数的。

PyTorch 中的损失函数

PyTorch 开箱即用,提供了许多规范的损失函数和简单的设计模式,允许开发人员在培训期间快速轻松地迭代这些不同的损失函数。PyTorch 的所有损失函数都封装在 nn 模块中,nn 模块是 PyTorch 用于所有神经网络的基类。这使得在项目中添加损失函数就像添加一行代码一样简单。让我们看看如何在 PyTorch 中添加均方误差损失函数。

import torch.nn as nn
MSE_loss_fn = nn.MSELoss()

上述代码返回的函数可用于计算预测值与实际值之间的差距,格式如下。

#predicted_value is the prediction from our neural network
#target is the actual value in our dataset
#loss_value is the loss between the predicted value and the actual value
Loss_value = MSE_loss_fn(predicted_value, target)

现在我们已经了解了如何在 PyTorch 中使用损失函数,让我们深入到 PyTorch 提供的几个损失函数的幕后。

PyTorch 中有哪些损失函数?

PyTorch 附带的许多损失函数大致分为 3 类——回归损失、分类损失和排序损失。

回归损失主要与连续值有关,它可以取两个极限之间的任何值。这方面的一个例子是社区房价的预测。

分类损失函数处理离散值,例如将对象分类为盒子、笔或瓶子。

排名损失预测值之间的相对距离。这种情况的一个例子是面部验证,其中我们想要知道哪些面部图像属于特定的面部,并且可以通过经由它们与目标面部扫描的相对近似程度对哪些面部属于和不属于原始面部持有者进行排序来做到这一点。

L1 损失函数

L1 损失函数计算预测张量中的每个值和目标张量之间的平均绝对误差。它首先计算预测张量中的每个值与目标张量中的每个值之间的绝对差,并计算从每个绝对差计算返回的所有值的总和。最后,它计算这个和值的平均值,以获得平均绝对误差( MAE )。L1 损失函数对于处理噪声非常稳健。

Mean Average Error Formula

import torch.nn as nn

#size_average and reduce are deprecated

#reduction specifies the method of reduction to apply to output. Possible values are 'mean' (default) where we compute the average of the output, 'sum' where the output is summed and 'none' which applies no reduction to output

Loss_fn = nn.L1Loss(size_average=None, reduce=None, reduction='mean')

input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)
output = loss_fn(input, target)
print(output) #tensor(0.7772, grad_fn=<L1LossBackward>)

返回的单个值是维数为 3 乘 5 的两个张量之间的计算损失。

均方误差

均方差与平均绝对误差有一些惊人的相似之处。与平均绝对误差的情况不同,它不是计算预测张量和目标张量的值之间的绝对差,而是计算预测张量和目标张量的值之间的平方差。通过这样做,相对较大的差异被罚得更多,而相对较小的差异被罚得更少。然而,在处理异常值和噪声方面,MSE 被认为不如 MAE 稳健。

Mean Squared Error Formula

import torch.nn as nn

loss = nn.MSELoss(size_average=None, reduce=None, reduction='mean')
#L1 loss function parameters explanation applies here.

input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)
output = loss(input, target)
print(output) #tensor(0.9823, grad_fn=<MseLossBackward>)

交叉熵损失

交叉熵损失用于涉及许多离散类的分类问题。它测量一组给定随机变量的两个概率分布之间的差异。通常,在使用交叉熵损失时,我们的网络的输出是一个 Softmax 层,这确保了神经网络的输出是一个概率值(0-1 之间的值)。

softmax 层由两部分组成-特定类的预测指数。

yi 是特定类的神经网络的输出。这个函数的输出是一个接近于零的数,但如果 yi 大且为负,则永远不会为零,如果 yi 为正且非常大,则更接近于 1。

import numpy as np

np.exp(34) #583461742527454.9
np.exp(-34) #1.713908431542013e-15

第二部分是归一化值,用于确保 softmax 层的输出始终是概率值。

这是通过将每个类值的所有指数相加得到的。softmax 的最终等式如下所示:

在 PyTorch 的 nn 模块中,交叉熵损失将 log-softmax 和负 Log-Likelihood 损失组合成一个损失函数。

请注意打印输出中的梯度函数是如何成为负对数似然损失(NLL)的。这实际上揭示了交叉熵损失将遮光罩下的 NLL 损失与 log-softmax 层相结合。

负对数似然损失

NLL 损失函数的工作方式与交叉熵损失函数非常相似。如前面交叉熵部分所述,交叉熵损失将 log-softmax 图层和 NLL 损失相结合,以获得交叉熵损失的值。这意味着通过使神经网络的最后一层是 log-softmax 层而不是正常的 softmax 层,可以使用 NLL 损失来获得交叉熵损失值。

m = nn.LogSoftmax(dim=1)
loss = nn.NLLLoss()
# input is of size N x C = 3 x 5
input = torch.randn(3, 5, requires_grad=True)
# each element in target has to have 0 <= value < C
target = torch.tensor([1, 0, 4])
output = loss(m(input), target)
output.backward()
# 2D loss example (used, for example, with image inputs)
N, C = 5, 4
loss = nn.NLLLoss()
# input is of size N x C x height x width
data = torch.randn(N, 16, 10, 10)
conv = nn.Conv2d(16, C, (3, 3))
m = nn.LogSoftmax(dim=1)
# each element in target has to have 0 <= value < C
target = torch.empty(N, 8, 8, dtype=torch.long).random_(0, C)
output = loss(m(conv(data)), target)
print(output) #tensor(1.4892, grad_fn=<NllLoss2DBackward>)

#credit NLLLoss — PyTorch 1.9.0 documentation

二元交叉熵损失

二进制交叉熵损失是一类特殊的交叉熵损失,用于将数据点仅分类为两类的特殊问题。这类问题的标签通常是二进制的,因此我们的目标是推动模型来预测一个接近零的零标签数和一个接近一的一个一标签数。通常当使用 BCE 损失进行二进制分类时,神经网络的输出是 Sigmoid 层,以确保输出是接近零的值或接近一的值。

import torch.nn as nn

m = nn.Sigmoid()
loss = nn.BCELoss()
input = torch.randn(3, requires_grad=True)
target = torch.empty(3).random_(2)
output = loss(m(input), target)
print(output) #tensor(0.4198, grad_fn=<BinaryCrossEntropyBackward>)

具有对数的二元交叉熵损失

我在上一节中提到,二进制交叉熵损失通常作为 sigmoid 层输出,以确保输出介于 0 和 1 之间。具有 Logits 的二进制交叉熵损失将这两层组合成一层。根据 PyTorch 文档,这是一个在数值上更加稳定的版本,因为它利用了 log-sum exp 技巧。

import torch
import torch.nn as nn

target = torch.ones([10, 64], dtype=torch.float32)  # 64 classes, batch size = 10
output = torch.full([10, 64], 1.5)  # A prediction (logit)
pos_weight = torch.ones([64])  # All weights are equal to 1
criterion = torch.nn.BCEWithLogitsLoss(pos_weight=pos_weight)
loss = criterion(output, target)  # -log(sigmoid(1.5))
print(loss) #tensor(0.2014)

平滑 L1 损耗

平滑 L1 损失函数通过试探值β结合了 MSE 损失和 MAE 损失的优点。这个标准是在 Fast R-CNN 论文中介绍的。当实际值和预测值之间的绝对差值低于β时,该标准使用平方差,非常类似于 MSE 损失。MSE 损失图是一条连续的曲线,这意味着每个损失值的梯度不同,可以在任何地方得到。此外,随着损失值减小,梯度减小,这在梯度下降期间是方便的。然而,对于非常大的损失值,梯度爆炸,因此当绝对差变得大于β且潜在的梯度爆炸被消除时,标准切换到平均绝对误差,其梯度对于每个损失值几乎是恒定的。

import torch.nn as nn

loss = nn.SmoothL1Loss()
input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)
output = loss(input, target)

print(output) #tensor(0.7838, grad_fn=<SmoothL1LossBackward>)

铰链嵌入损耗

铰链嵌入损失主要用于半监督学习任务,以衡量两个输入之间的相似性。当有一个输入张量和一个包含值 1 或-1 的标签张量时使用。它主要用于涉及非线性嵌入和半监督学习的问题。

import torch
import torch.nn as nn

input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)

hinge_loss = nn.HingeEmbeddingLoss()
output = hinge_loss(input, target)
output.backward()

print('input: ', input)
print('target: ', target)
print('output: ', output)

#input:  tensor([[ 1.4668e+00,  2.9302e-01, -3.5806e-01,  1.8045e-01,  #1.1793e+00],
#       [-6.9471e-05,  9.4336e-01,  8.8339e-01, -1.1010e+00,  #1.5904e+00],
#       [-4.7971e-02, -2.7016e-01,  1.5292e+00, -6.0295e-01,  #2.3883e+00]],
#       requires_grad=True)
#target:  tensor([[-0.2386, -1.2860, -0.7707,  1.2827, -0.8612],
#        [ 0.6747,  0.1610,  0.5223, -0.8986,  0.8069],
#        [ 1.0354,  0.0253,  1.0896, -1.0791, -0.0834]])
#output:  tensor(1.2103, grad_fn=<MeanBackward0>)

利润排名损失

差值排序损失属于排序损失,与其他损失函数不同,其主要目标是测量数据集中一组输入之间的相对距离。边际排名损失函数采用两个输入和一个仅包含 1 或-1 的标签。如果标签为 1,则假设第一输入应该具有比第二输入更高的等级,如果标签为-1,则假设第二输入应该具有比第一输入更高的等级。下面的等式和代码显示了这种关系。

import torch.nn as nn

loss = nn.MarginRankingLoss()
input1 = torch.randn(3, requires_grad=True)
input2 = torch.randn(3, requires_grad=True)
target = torch.randn(3).sign()
output = loss(input1, input2, target)
print('input1: ', input1)
print('input2: ', input2)
print('output: ', output)

#input1:  tensor([-1.1109,  0.1187,  0.9441], requires_grad=True)
#input2:  tensor([ 0.9284, -0.3707, -0.7504], requires_grad=True)
#output:  tensor(0.5648, grad_fn=<MeanBackward0>)

三重边际损失

该标准通过使用训练数据样本的三元组来测量数据点之间的相似性。所涉及的三元组是锚定样本、阳性样本和阴性样本。目标是 1)使正样本和锚之间的距离尽可能小,以及 2)使锚和负样本之间的距离大于一个差值加上正样本和锚之间的距离。通常情况下,正样本与主播属于同一类,负样本则不是。因此,通过使用该损失函数,我们旨在使用三元组边际损失来预测锚和阳性样本之间的高相似性值以及锚和阴性样本之间的低相似性值。

import torch.nn as nn

triplet_loss = nn.TripletMarginLoss(margin=1.0, p=2)
anchor = torch.randn(100, 128, requires_grad=True)
positive = torch.randn(100, 128, requires_grad=True)
negative = torch.randn(100, 128, requires_grad=True)
output = triplet_loss(anchor, positive, negative)
print(output)  #tensor(1.1151, grad_fn=<MeanBackward0>)

余弦嵌入损失

余弦嵌入损失测量给定输入 x1、x2 和包含值 1 或-1 的标签张量 y 的损失。它用于测量两个输入相似或不相似的程度。

该标准通过计算空间中两个数据点之间的余弦距离来测量相似性。余弦距离与两点之间的角度相关,这意味着角度越小,输入越接近,因此它们越相似。

import torch.nn as nn

loss = nn.CosineEmbeddingLoss()
input1 = torch.randn(3, 6, requires_grad=True)
input2 = torch.randn(3, 6, requires_grad=True)
target = torch.randn(3).sign()
output = loss(input1, input2, target)
print('input1: ', input1)
print('input2: ', input2)
print('output: ', output)

#input1:  tensor([[ 1.2969e-01,  1.9397e+00, -1.7762e+00, -1.2793e-01, #-4.7004e-01,
#         -1.1736e+00],
#        [-3.7807e-02,  4.6385e-03, -9.5373e-01,  8.4614e-01, -1.1113e+00,
#          4.0305e-01],
#        [-1.7561e-01,  8.8705e-01, -5.9533e-02,  1.3153e-03, -6.0306e-01,
#          7.9162e-01]], requires_grad=True)
#input2:  tensor([[-0.6177, -0.0625, -0.7188,  0.0824,  0.3192,  1.0410],
#        [-0.5767,  0.0298, -0.0826,  0.5866,  1.1008,  1.6463],
#        [-0.9608, -0.6449,  1.4022,  1.2211,  0.8248, -1.9933]],
#       requires_grad=True)
#output:  tensor(0.0033, grad_fn=<MeanBackward0>)

库尔巴克-莱布勒发散损失

给定两种分布,P 和 Q,Kullback Leibler 散度(KLD)损失测量当 P(假定为真实分布)被 Q 替换时损失了多少信息。通过测量当我们使用 Q 逼近 P 时损失了多少信息,我们能够获得 P 和 Q 之间的相似性,从而驱动我们的算法产生非常接近真实分布 P 的分布。当使用 Q 逼近 P 时的信息损失与使用 P 逼近 Q 时的信息损失不同,因此 KL 散度是不对称的。

import torch.nn as nn

loss = nn.KLDivLoss(size_average=None, reduce=None, reduction='mean', log_target=False)
input1 = torch.randn(3, 6, requires_grad=True)
input2 = torch.randn(3, 6, requires_grad=True)
output = loss(input1, input2)

print('output: ', output) #tensor(-0.0284, grad_fn=<KlDivBackward>)

构建您自己的定制损失函数

PyTorch 为我们提供了两种流行的方法来建立我们自己的损失函数,以适应我们的问题;即使用类实现和使用函数实现。让我们从函数实现开始,看看如何实现这两种方法。

函数的自定义丢失

这是编写自定义损失函数最简单的方法。这就像创建一个函数一样简单,向它传递所需的输入和其他参数,使用 PyTorch 的核心 API 或函数 API 执行一些操作,然后返回值。让我们来看一个自定义均方误差的演示。

def custom_mean_square_error(y_predictions, target):
  square_difference = torch.square(y_predictions - target)
  loss_value = torch.mean(square_difference)
  return loss_value

在上面的代码中,我们定义了一个自定义损失函数来计算给定预测张量和目标传感器的均方误差

y_predictions = torch.randn(3, 5, requires_grad=True);
target = torch.randn(3, 5)
pytorch_loss = nn.MSELoss();
p_loss = pytorch_loss(y_predictions, target)
loss = custom_mean_square_error(y_predictions, target)
print('custom loss: ', loss)
print('pytorch loss: ', p_loss)

#custom loss:  tensor(2.3134, grad_fn=<MeanBackward0>)
#pytorch loss:  tensor(2.3134, grad_fn=<MseLossBackward>)

我们可以使用我们的自定义损失函数和 PyTorch 的 MSE 损失函数来计算损失,观察我们已经获得了相同的结果。

Python 类的自定义丢失

这种方法可能是 PyTorch 中定义自定义损耗的标准和推荐方法。通过对 nn 模块进行子类化,损失函数被创建为神经网络图中的节点。这意味着我们的自定义损失函数是 PyTorch 层,与卷积层完全相同。让我们来看一个演示,看看这是如何处理定制 MSE 损失的。

class Custom_MSE(nn.Module):
  def __init__(self):
    super(Custom_MSE, self).__init__();

  def forward(self, predictions, target):
    square_difference = torch.square(predictions - target)
    loss_value = torch.mean(square_difference)
    return loss_value

  # def __call__(self, predictions, target):
  #   square_difference = torch.square(y_predictions - target)
  #   loss_value = torch.mean(square_difference)
  #   return loss_value

我们可以在“forward”函数调用或“call”内部定义损失的实际实现。查看 IPython 笔记本上的 Gradient,了解实践中使用的自定义 MSE 函数

最后的想法

我们已经讨论了很多 PyTorch 中可用的损失函数,并且深入研究了这些损失函数的内部工作原理。为特定问题选择正确的损失函数可能是一项艰巨的任务。希望本教程和 PyTorch 官方文档可以作为理解哪种损失函数更适合您的问题的指南。

PyTorch 101,第 4 部分:内存管理和使用多个 GPU

原文:https://blog.paperspace.com/pytorch-memory-multi-gpu-debugging/

图片来源:cryptocurrency360.com

你好。这是我们 PyTorch 101 系列的第 4 部分,我们将在这篇文章中讨论多种 GPU 的使用。

在这一部分中,我们将讨论,

  1. 如何为您的网络使用多个 GPU,无论是使用数据并行还是模型并行。
  2. 如何在创建新对象时自动选择 GPU?
  3. 如何诊断和分析可能出现的内存问题。

那么,我们开始吧。

在我们开始之前,让我提醒您这是我们 PyTorch 系列的第 4 部分。

  1. 理解图形,自动微分和亲笔签名
  2. 建立你的第一个神经网络
  3. 深入 PyTorch
  4. 内存管理和使用多个 GPU
  5. 理解挂钩

你可以在 Github repo 这里获得这篇文章(以及其他文章)中的所有代码。


围绕 CPU/GPU 移动张量

PyTorch 中的每个张量都有一个to()成员函数。它的工作是把调用它的张量放到某个设备上,不管是 CPU 还是 GPU。to功能的输入是一个torch.device对象,可通过以下任一输入进行初始化。

  1. cpu为 CPU
  2. cuda:0用于放在 0 号 GPU 上。类似地,如果你想把张量放上去

一般来说,每当你初始化一个张量,它就被放在 CPU 上。然后就可以把它移到 GPU 上了。您可以通过调用torch.cuda.is_available函数来检查 GPU 是否可用。

if torch.cuda.is_available():
	dev = "cuda:0"
else:
	dev = "cpu"

device = torch.device(dev)

a = torch.zeros(4,3)   
a = a.to(device)       #alternatively, a.to(0)

你也可以通过将张量的索引作为to函数的参数,将张量移动到某个 GPU。

重要的是,上面这段代码是设备不可知的,也就是说,您不必单独更改它就可以在 GPU 和 CPU 上工作。

cuda()函数

将张量放在 GPU 上的另一种方法是对它们调用cuda(n)函数,其中n是 GPU 的索引。如果你只是调用cuda,那么张量放在 GPU 0 上。

torch.nn.Module类也有tocuda功能,将整个网络放在一个特定的设备上。不像,Tensorsnn.Module对象上调用to就足够了,不需要分配to函数的返回值。

clf = myNetwork()
clf.to(torch.device("cuda:0")    # or clf = clf.cuda() 

GPU 的自动选择

虽然能够明确决定张量在哪个 GPU 上运行是件好事,但一般来说,我们在运算过程中会创建很多张量。我们希望它们在某个设备上自动创建,以减少跨设备传输,这会降低我们代码的速度。在这方面,PyTorch 为我们提供了一些功能来实现这一点。

首先,是torch.get_device函数。只支持 GPU 张量。它返回张量所在的 GPU 的索引。我们可以用这个函数来确定张量的设备,这样我们就可以把一个创建的张量自动移动到这个设备上。

#making sure t2 is on the same device as t2

a = t1.get_device()
b = torch.tensor(a.shape).to(dev)

我们还可以在创建新张量的同时调用cuda(n)。默认情况下,由cuda调用创建的所有张量都放在 GPU 0 上,但这可以通过以下语句来更改。

torch.cuda.set_device(0)   # or 1,2,3 

如果一个张量是由同一设备上的两个操作数之间的运算产生的,那么结果张量也是如此。如果操作数在不同的设备上,就会导致错误。

新 _*函数

你也可以使用 py torch 1.0 版本中的一堆new_函数。当在Tensor上调用类似new_ones的函数时,它返回一个相同数据类型的新张量 cof,并且与调用new_ones函数的张量在同一设备上。

ones = torch.ones((2,)).cuda(0)

# Create a tensor of ones of size (3,4) on same device as of "ones"
newOnes = ones.new_ones((3,4)) 

randTensor = torch.randn(2,4) 

在 PyTorch 文档中可以找到new_函数的详细列表,我在下面提供了该文档的链接。

使用多个 GPU

我们可以通过两种方式来利用多个 GPU。

  1. 数据并行,这里我们将批次划分成更小的批次,在多个 GPU 上并行处理这些更小的批次。
  2. 模型并行,我们将神经网络分成更小的子网络,然后在不同的 GPU 上执行这些子网络。

数据并行性

PyTorch 中的数据并行是通过nn.DataParallel类实现的。你用一个代表你的网络的nn.Module对象和一个 GPU IDs 列表初始化一个nn.DataParallel对象,批处理必须通过它们被并行化。

parallel_net = nn.DataParallel(myNet, gpu_ids = [0,1,2]) 

现在,您可以像执行nn.Module一样简单地执行nn.DataParallel对象。

predictions = parallel_net(inputs)           # Forward pass on multi-GPUs
loss = loss_function(predictions, labels)     # Compute loss function
loss.mean().backward()                        # Average GPU-losses + backward pass
optimizer.step() 

然而,有几件事我想说明一下。尽管我们的数据必须在多个 GPU 上并行处理,但我们最初必须将它存储在单个 GPU 上。

我们还需要确保DataParallel对象也在那个特定的 GPU 上。语法仍然类似于我们之前对nn.Module所做的。

input        = input.to(0)
parallel_net = parellel_net.to(0)

实际上,下图描述了nn.DataParallel是如何工作的。

Working of nn.DataParallel. Source

DataParallel获取输入,将其分成更小的批次,在所有设备上复制神经网络,执行传递,然后在原始 GPU 上收集输出。

DataParallel的一个问题是它可能会给一个 GPU(主节点)带来不对称的负载。通常有两种方法来避免这些问题。

  1. 首先是计算向前传递时的损耗。这确保至少损失计算阶段是并行的。
  2. 另一种方法是实现并行损失功能层。这超出了本文的范围。不过,对于那些感兴趣的人,我在本文末尾给出了一个链接,链接到一篇详细介绍这种层的实现的中型文章。

模型并行性

模型并行意味着将网络分成更小的子网,然后放在不同的 GPU 上。这样做的主要动机是,您的网络可能太大,无法容纳在单个 GPU 中。

请注意,模型并行性通常比数据并行性慢,因为将单个网络分成多个 GPU 会在 GPU 之间引入依赖性,从而阻止它们以真正并行的方式运行。从模型并行性中获得的优势不是速度,而是运行网络的能力,这些网络的规模太大,无法在单个 GPU 上运行。

正如我们在图 b 中看到的,子网 2 在正向传递期间等待子网 1,而子网 1 在反向传递期间等待子网 2。

Model Parallelism with Dependencies


只要记住两件事,实现模型并行是非常容易的。

  1. 输入和网络应该总是在同一设备上。
  2. tocuda函数具有自动签名支持,因此您的渐变可以在反向传递期间从一个 GPU 复制到另一个 GPU。

我们将使用下面这段代码来更好地理解这一点。

class model_parallel(nn.Module):
	def __init__(self):
		super().__init__()
		self.sub_network1 = ...
		self.sub_network2 = ...

		self.sub_network1.cuda(0)
		self.sub_network2.cuda(1)

	def forward(x):
		x = x.cuda(0)
		x = self.sub_network1(x)
		x = x.cuda(1)
		x = self.sub_network2(x)
		return x 

init函数中,我们将子网分别放在 GPUs 0 和 1 上。

注意,在forward函数中,我们将中间输出从sub_network1传送到 GPU 1,然后再传送给sub_network2。由于cuda有亲笔签名的支持,从sub_network2反向传播的丢失将被复制到sub_network1的缓冲区,以供进一步反向传播。

排除内存不足错误

在这一节中,我们将介绍如何诊断内存问题,以及如果您的网络使用的内存超过需求时可能的解决方案。

虽然用尽内存可能需要减少批处理大小,但是可以进行某些检查来确保内存的使用是最佳的。

使用 GPUtil 跟踪内存使用情况

跟踪 GPU 使用情况的一种方法是在控制台中使用nvidia-smi命令监控内存使用情况。这种方法的问题是,GPU 使用高峰和内存不足发生得如此之快,以至于您无法确定是哪部分代码导致了内存溢出。

为此,我们将使用一个名为GPUtil的扩展,您可以通过运行以下命令用 pip 安装它。

pip install GPUtil

用法也很简单。

import GPUtil
GPUtil.showUtilization()

只需将第二行放在您想要查看 GPU 利用率的地方。通过将该语句放在代码的不同位置,您可以找出是哪个部分导致了网络崩溃。


现在让我们来谈谈纠正 OOM 错误的可能方法。

使用 del 关键字处理记忆缺失

PyTorch 有一个相当激进的垃圾收集器。一旦变量超出范围,垃圾收集就会释放它。

需要记住的是,Python 并不像其他语言(如 C/C++)那样强制执行作用域规则。一个变量只有在没有指向它的指针时才会被释放。(这与变量不需要在 Python 中声明的事实有关)

因此,即使你退出训练循环,保持你的inputoutput张量的张量所占用的内存仍然不能被释放。考虑下面的代码块。

for x in range(10):
	i = x

print(i)   # 9 is printed

运行上面的代码片段将打印出i的值,即使我们在初始化i的循环之外。同样,持有lossoutput的张量可以活出训练圈。为了真正释放这些张量占据的空间,我们使用del关键字。

del out, loss

事实上,根据一般的经验法则,如果你完成了一个张量,你应该del因为它不会被垃圾收集,除非没有对它的引用。

使用 Python 数据类型代替一维张量

通常,我们在训练循环中聚合值来计算一些指标。最大的例子是我们在每次迭代中更新运行损失。但是,如果在 PyTorch 中不小心的话,这样的事情可能会导致内存的过度使用。

考虑下面的代码片段。

total_loss = 0

for x in range(10):
  # assume loss is computed 
  iter_loss = torch.randn(3,4).mean()
  iter_loss.requires_grad = True     # losses are supposed to differentiable
  total_loss += iter_loss            # use total_loss += iter_loss.item) instead

我们期望在随后的迭代中,对iter_loss的引用被重新分配给新的iter_loss,并且从早期表示中表示iter_loss的对象将被释放。但这不会发生。为什么?

由于iter_loss是可微的,所以线total_loss += iter_loss创建了具有一个AddBackward功能节点的计算图。在随后的迭代中,AddBackward节点被添加到这个图中,并且不释放保存有iter_loss值的对象。通常,当调用backward时,分配给计算图的内存被释放,但是这里没有调用backward的范围。

The computation graph created when you keep adding the loss tensor to the variable loss

这个问题的解决方案是添加一个 python 数据类型,而不是添加一个张量到total_loss中,这会阻止任何计算图的创建。

我们只是用total_loss += iter_loss.item()替换了行total_loss += iter_lossitem从包含单个值的张量返回 python 数据类型。

清空 Cuda 缓存

虽然 PyTorch 积极地释放内存,但 pytorch 进程可能不会将内存归还给操作系统,即使在您完成 tensors 之后。此内存被缓存,以便可以快速分配给正在分配的新张量,而无需向操作系统请求新的额外内存。

当您在工作流中使用两个以上的流程时,这可能会成为一个问题。

第一个进程可以占用 GPU 内存,即使它的工作已经完成,当第二个进程启动时会导致 OOM。要解决这个问题,您可以在代码末尾编写命令。

torch.cuda.empy_cache()

这将确保进程占用的空间被释放。

import torch
from GPUtil import showUtilization as gpu_usage

print("Initial GPU Usage")
gpu_usage()                             

tensorList = []
for x in range(10):
  tensorList.append(torch.randn(10000000,10).cuda())   # reduce the size of tensor if you are getting OOM

print("GPU Usage after allcoating a bunch of Tensors")
gpu_usage()

del tensorList

print("GPU Usage after deleting the Tensors")
gpu_usage()  

print("GPU Usage after emptying the cache")
torch.cuda.empty_cache()
gpu_usage()

在 Tesla K80 上执行此代码时,会产生以下输出

Initial GPU Usage
| ID | GPU | MEM |
------------------
|  0 |  0% |  5% |
GPU Usage after allcoating a bunch of Tensors
| ID | GPU | MEM |
------------------
|  0 |  3% | 30% |
GPU Usage after deleting the Tensors
| ID | GPU | MEM |
------------------
|  0 |  3% | 30% |
GPU Usage after emptying the cache
| ID | GPU | MEM |
------------------
|  0 |  3% |  5% |

使用 torch.no_grad()进行推理

默认情况下,PyTorch 将在向前传递期间创建一个计算图形。在创建此图的过程中,它将分配缓冲区来存储梯度和中间值,这些值用于在反向传递过程中计算梯度。

在向后传递期间,除了分配给叶变量的缓冲区之外,所有这些缓冲区都被释放。

然而,在推理过程中,没有向后传递,这些缓冲区也不会被释放,导致内存堆积。因此,每当你想执行一段不需要反向传播的代码时,把它放在一个torch.no_grad()上下文管理器中。

with torch.no_grad()
	# your code 

使用 CuDNN 后端

您可以利用cudnn基准来代替普通基准。CuDNN 可以提供很多优化,可以降低您的空间使用,特别是当输入到您的神经网络是固定大小。在代码顶部添加以下代码行,以启用 CuDNN 基准测试。

torch.backends.cudnn.benchmark = True
torch.backends.cudnn.enabled = True

使用 16 位浮点

nVidia 的新 RTX 和沃尔特卡支持 16 位训练和推理。

model = model.half()     # convert a model to 16-bit
input = input.half()     # convert a model to 16-bit

然而,16 位培训选项必须有所保留。

虽然使用 16 位张量可以将 GPU 的使用量减少近一半,但它们也存在一些问题。

  1. 在 PyTorch 中,批处理规范层在半精度浮点运算时存在收敛问题。如果你是这种情况,确保批量定额层是float32
model.half()  # convert to half precision
for layer in model.modules():
  if isinstance(layer, nn.BatchNorm2d):
    layer.float()

此外,您需要确保当输出通过forward函数中的不同层时,批处理规范层的输入从float16转换为float32,然后输出需要转换回float16

你可以在 PyTorch 这里找到关于 16 位训练的很好的讨论。

2.16 位浮点数可能会有溢出问题。有一次,我记得在试图将两个边界框的合并区域(用于计算借据)存储在一个float16中时,出现了这样的溢出。所以要确保你有一个现实的界限来限制你试图存进 float16 的值。

Nvidia 最近发布了一个名为 Apex 的 PyTorch 扩展,它有助于 PyTorch 中的数字安全混合精度训练。我在文章的最后提供了链接。

结论

以上是关于 PyTorch 中内存管理和多 GPU 使用的讨论。以下是你可能想跟进这篇文章的重要链接。

进一步阅读

  1. PyTorch new功能
  2. 并行损失层:在较大批量上训练神经网络:单 GPU、多 GPU &分布式设置的实用技巧
  3. GPUtil Github 页面
  4. PyTorch 中半精确训练的讨论
  5. Nvidia Apex Github 页面
  6. 英伟达 Apex 教程

用 Pytorch 进行矢量化和广播

原文:https://blog.paperspace.com/pytorch-vectorization-and-broadcasting/

在 GPU 上运行机器学习代码可以获得巨大的性能提升。但是 GPU 针对需要并行执行数千次相同操作的代码进行了优化。因此,我们以这种方式编写代码也很重要。

本周早些时候,我在训练一些单词嵌入。回想一下,单词嵌入是密集向量,其被认为捕获单词含义,并且如果单词在含义上相似,则两个单词嵌入之间的距离(余弦距离或欧几里德距离)应该更小。

我想通过对单词相似度数据集(如斯坦福稀有单词相似度数据集)进行评估来评估我训练的单词嵌入的质量。单词相似度数据集收集人类对单词之间距离的判断。词汇的词语相似度数据集 V 可以表示为 |V| x |V| 矩阵 S ,其中 S[i][j] 表示词语 V[i]V[j] 之间的相似度。

我需要写一些 Pytorch 代码来计算每对嵌入之间的余弦相似度**,从而产生一个单词嵌入相似度矩阵,我可以将它与进行比较。**

下面是我的第一次尝试:
loopy-1
来源

我们遍历嵌入矩阵E,并计算每对嵌入的余弦相似度ab。这给了我们一个浮点数列表。然后我们使用torch.cat将每个子列表转换成一个张量,然后我们torch.stack将整个列表转换成一个单一的 2D(n×n)张量。

好吧,让我们看看这个愚蠢的代码是如何执行的!我们将生成 20,000 个一维单词嵌入的随机矩阵,并计算余弦相似度矩阵。
loopy_test-1
我们正在 PaperSpace 的一台强大的 P6000 机器上运行该基准测试,但是快速浏览一下nvidia-smi的输出显示 GPU 利用率为 0%,而top显示 CPU 正在努力工作。离节目结束还有 5 个小时。

现在,我们用矢量化的形式重写函数:
vectorized
来源

在 P6000 上的快速性能测试表明,该函数从 20,000 个 100 维嵌入中计算相似性矩阵仅需 3.779 秒!

让我们浏览一下代码。关键的想法是,我们将余弦相似度函数分解成它的分量运算,这样我们就可以并行处理 10,000 次计算,而不是按顺序进行。

两个向量的余弦相似度就是它们之间夹角的余弦值:

cosine-similarity

  1. 首先,我们将 E 与其转置矩阵相乘。
    dot
    这产生一个(嵌入数,嵌入数)矩阵dot。如果你想一想矩阵乘法是如何工作的(先相乘再求和),你会发现每个dot[i][j]现在存储的是E[i]E[j]的点积。

  2. 然后,我们计算每个嵌入向量的大小。
    norm
    2表示我们正在计算每个向量的 L-2(欧氏)范数。1告诉 Pytorch 我们的嵌入矩阵的布局是(num _ embedding,vector_dimension)而不是(vector_dimension,num _ embedding)。
    norm现在是一个行矢,那里norm[i] = ||E[i]||

  3. 我们将每个(E[i] dot E[j])除以||E[j]||。
    div1
    在这里,我们正在开发一种叫做的广播。请注意,我们将一个矩阵(num _ embedding,num _ embedding)除以一个行向量(num _ embedding,)。在不分配更多内存的情况下,Pytorch 将向下广播行向量,这样我们可以想象我们被一个由 num_embeddings 行组成的矩阵分割,每个行包含原始的行向量。结果是,我们的原始矩阵中的每个单元现在都被除以||E[j]||,嵌入的幅度对应于它的列数。

  4. 最后,我们除以||E[i]||:
    div2
    再次,我们使用广播,但这次我们先将norm转换成列向量,这样广播将复制列而不是行。结果是每个单元格 x[i][j]除以||E[i]||,即第 I 次嵌入的幅度。

**就是这样!**我们已经计算了包含每对嵌入之间的成对余弦相似性的矩阵,并从矢量化和广播中获得了巨大的性能提升!

结论
下次当你想知道为什么你的机器学习代码运行缓慢时,即使是在 GPU 上,也要考虑对任何有圈圈的代码进行矢量化!

如果你想了解更多我们在这篇博文中提到的东西,请点击以下链接:

问答模型的比较

原文:https://blog.paperspace.com/question-answering-models-a-comparison/

自然语言处理中的问题回答子域由于在用参考文档回答问题方面的广泛应用而受到欢迎。这个问题的解决方案是通过使用由输入文本、查询和包含查询答案的输入文本的文本片段或范围组成的数据集来解决的。在深度学习模型的帮助下,从数据中实现人类水平的预测有了显著的提高。

在本教程中,我们将涵盖:

  • 变压器架构
  • 流行的数据集和评估指标
  • 来自变压器的双向编码器表示
  • 艾伯特:一点伯特
  • 伊利克特拉
  • BART
  • 使用标准模型的长文档问答的问题
  • LONGFORMER:长文档转换器

您可以从 ML Showcase 的免费 GPU 上运行本教程的完整代码。

变压器架构

论文中提出了变压器架构你所需要的就是注意力。编码器对输入文本进行编码,解码器处理编码以理解序列背后的上下文信息。堆栈中的每个编码器和解码器使用注意机制来处理每个输入以及每个其他输入,以衡量它们的相关性,并在解码器的帮助下生成输出序列。这里的注意机制能够动态突出显示和理解输入文本中的特征。《变形金刚》中的自我关注机制可以用一个例句来说明。

A dog came running at me. It had cute eyes

这里自我注意机制允许单词It与单词dog相关联,并且理解It并不指代单词me。这是因为序列中的其他输入被加权,并且为这两个单词分配了更高的相关性概率。

流行的数据集和评估指标

在本文中,我们将遵循最常见的问答模型和数据集,它们是在 transformer 架构的帮助下预先训练的。最受欢迎的问答数据集包括小队CoQA 等。这些数据集得到了很好的维护和定期更新,从而使它们适合通过最先进的模型进行训练。数据集由输入问题、参考文本和目标组成。

在 F1 和 EM 分数的帮助下完成评估。这些是使用精度和召回率计算的。精度是真阳性的数量除以真阳性和假阳性的数量。召回率是真阳性的数量除以真阳性和假阴性的数量。F1 的分数是:

 2*((precision*recall)/(precision+recall))

EM(精确匹配)分数衡量与任何一个基本事实答案精确匹配的预测的百分比。这里的基本事实是数据集中的目标标注。

来自变压器的双向编码器表示

huggingface transformers库提供了包装良好的实现供我们试验。让我们开始我们的实验,首先进入一个 BERT 模型来理解它的性能。

Google Research 的 BERT 基于 transformer 架构,具有在维基百科和图书语料库上训练的编码器-解码器堆栈,后者是一个包含 10,000 多本不同流派图书的数据集。

SQuAD 数据集仅包含大约 150,000 个问题。我们将对这些方面的预训练模型进行微调,因为迁移学习被证明可以在有限的数据下提高性能。当 BERT 在 SQuAD 上接受训练时,输入问题和参考文本使用一个[sep]标记分开。概率分布用于从参考文本中确定startend 记号,其中包含答案的跨度由startend记号的位置确定。

Tokenization in BERT (source)

!pip install torch
!pip install transformers
!pip install sentencepiece 
from transformers import BertTokenizer,AlbertTokenizer,AutoTokenizer, AutoModelForQuestionAnswering ,BertForQuestionAnswering
import torch

model_name='bert-large-uncased-whole-word-masking-finetuned-squad'
model = BertForQuestionAnswering.from_pretrained(model_name) 

这里我们加载了标准的bert-large-uncased-whole-word-masking-finetuned-squad,大小在 1.34 GB 左右。接下来,我们将看到相同标记的标记器如何标记我们的输入。

from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained(model_name)
question = "How heavy is Ever Given?"
answer_text = "The Ever Given is 400m-long (1,312ft) and weighs 200,000 tonnes, with a maximum capacity of 20,000 containers. It is currently carrying 18,300 containers."
input_ids = tokenizer.encode(question, answer_text)
tokens = tokenizer.convert_ids_to_tokens(input_ids)
print(tokens)
Output:

['[CLS]', 'how', 'heavy', 'is', 'ever', 'given', '?', '[SEP]', 'the', 'ever', 'given', 'is', '400', '##m', '-', 'long', '(', '1', ',', '312', '##ft', ')', 'and', 'weighs', '200', ',', '000', 'tonnes', ',', 'with', 'a', 'maximum', 'capacity', 'of', '20', ',', '000', 'containers', '.', 'it', 'is', 'currently', 'carrying', '18', ',', '300', 'containers', '.', '[SEP]']

问题和答案文本由一个[sep]标记分隔,"##"表示标记的其余部分应该附加到前一个标记上,没有空格(用于解码或标记化的反转)。"##"的使用确保了带有这个符号的记号与它前面的记号直接相关。

接下来,我将把我们的问答实现封装在一个函数中,这样我们就可以重用它来尝试这个例子中的不同模型,只需要对代码做很小的修改。

def qa(question,answer_text,model,tokenizer):
  inputs = tokenizer.encode_plus(question, answer_text, add_special_tokens=True, return_tensors="pt")
  input_ids = inputs["input_ids"].tolist()[0]

  text_tokens = tokenizer.convert_ids_to_tokens(input_ids)
  print(text_tokens)
  outputs = model(**inputs)
  answer_start_scores=outputs.start_logits
  answer_end_scores=outputs.end_logits

  answer_start = torch.argmax(
      answer_start_scores
  )  # Get the most likely beginning of answer with the argmax of the score
  answer_end = torch.argmax(answer_end_scores) + 1  # Get the most likely end of answer with the argmax of the score

  answer = tokenizer.convert_tokens_to_string(tokenizer.convert_ids_to_tokens(input_ids[answer_start:answer_end]))

  # Combine the tokens in the answer and print it out.""
  answer = answer.replace("#","")

  print('Answer: "' + answer + '"')
  return answer 
`qa(question,answer_text,model,tokenizer)`

Output:
Answer: "200 , 000 tonnes"

1.1 班的 BERT 的 F1 和 EM 分数分别为 91.0 和 84.3 左右。

艾伯特:一点伯特

对于需要更低内存消耗和更快训练速度的任务,我们可以使用 ALBERT 。这是一个精简版的 BERT,它使用了参数减少技术,因此在运行训练和推理时减少了参数的数量。这也有助于模型的可伸缩性。

tokenizer=AlbertTokenizer.from_pretrained('ahotrod/albert_xxlargev1_squad2_512')
model=AlbertForQuestionAnswering.from_pretrained('ahotrod/albert_xxlargev1_squad2_512')
qa(question,answer_text,model,tokenizer)

Output:
Answer: "200,000 tonnes"

ALBERT 中的输入嵌入由相对低维度(例如 128 美元)的嵌入矩阵组成,而隐藏层维度更高(如 BERT 情况下的 768 美元或更高)。随着矩阵尺寸的减小,投影参数也减小,即可以观察到参数减小了 80%。这也伴随着性能的微小下降;一个 80.3 的小队 2.0 的分数,从 80.4 下降。

伊利克特拉

ELECTRA(有效学习准确分类令牌替换的编码器)是另一个提供预训练方法的模型,其中它通过新颖的方法破坏了 MLM(掩蔽语言建模);被称为替换令牌检测(RTD)。该方法训练两个转换器模型:生成器和鉴别器,它们都试图胜过对方。生成器试图通过替换序列中的标记来欺骗鉴别器,而鉴别器的工作是找到生成器替换的标记。由于任务是在所有记号上定义的,而不是在被屏蔽的部分上定义的,所以这种方法更有效,并且在特征提取方面优于 BERT。

tokenizer = AutoTokenizer.from_pretrained("valhalla/electra-base-discriminator-finetuned_squadv1")
model = AutoModelForQuestionAnswering.from_pretrained("valhalla/electra-base-discriminator-finetuned_squadv1")

question="What is the discriminator of ELECTRA similar to?"

answer_text='As mentioned in the original paper: ELECTRA is a new method for self-supervised language representation learning. It can be used to pre-train transformer networks using relatively little compute. ELECTRA models are trained to distinguish "real" input tokens vs "fake" input tokens generated by another neural network, similar to the discriminator of a GAN. At small scale, ELECTRA achieves strong results even when trained on a single GPU. At large scale, ELECTRA achieves state-of-the-art results on the SQuAD 2.0 dataset.'

qa(question,answer_text,model,tokenizer)
Output:
Answer: "a gan"

根据论文,ELECTRA-base 在 SQuAD 2.0 上实现了 EM 84.5 和 F1-90.8 的成绩。与 BERT 相比,预训练也更快,并且需要更少的例子来获得相同水平的性能。

巴特

脸书研究公司的 BART 结合了双向(如 BERT)和自回归(如 GPT 和 GPT-2)模型的能力。自回归模型从给定上下文的一组单词中预测未来单词。通过破坏一些记号或其他记号,对输入文本进行噪声处理。LM(语言模型)试图通过预测用原始标记替换损坏的标记。

tokenizer = AutoTokenizer.from_pretrained("valhalla/bart-large-finetuned-squadv1")

model = AutoModelForQuestionAnswering.from_pretrained("valhalla/bart-large-finetuned-squadv1")

question="Upto how many tokens of sequences can BART handle?"

answer_text="To use BART for question answering tasks, we feed the complete document into the encoder and decoder, and use the top hidden state of the decoder as a representation for each word. This representation is used to classify the token. As given in the paper bart-large achives comparable to ROBERTa on SQuAD. Another notable thing about BART is that it can handle sequences with upto 1024 tokens."

qa(question,answer_text,model,tokenizer)
Output:
Answer: " 1024"

通过结合两个世界的优点,即双向和自回归模型的特征,BART 提供了比 BERT 更好的性能(尽管参数增加了 10%)。在这里,BART-large 的 EM 得分为 88.8,F1 得分为 94.6。

使用标准模型的长文档问答的问题

正如本文开头所讨论的,变形金刚模型利用自我关注操作的帮助来提供有意义的结果。随着序列长度的增加,这种机制的计算规模急剧扩大。例如,看看下面给出的问题和文本:

question="how many pilots did the ship have?"
answer_text="""
Tug boats had spent several hours on Monday working to free the bow of the massive vessel after dislodging the stern earlier in the day.
Marine traffic websites showed images of the ship away from the banks of the Suez Canal for the first time in seven days following an around-the-clock international effort to reopen the global shipping lane.
There are still 422 ships waiting to go through the Suez Canal, Rabie said, adding that the canal's authorities decided the ships will be able to cross the canal on a first come first serve basis, though the ships carrying livestock were permitted to cross in the first convoy of the day.
The average number of ships that transited through the canal on a daily basis before the accident was between 80 to 90 ships, according to Lloyds List; however, the head of the Suez Canal Authority said that the channel will work over 24 hours a day to facilitate the passage of almost 400 ships carrying billions of dollars in freight.
The journey to cross the canal takes 10 to 12 hours and in the event the channel operates for 24 hours, two convoys per day will be able to successfully pass through.
Still, shipping giant Maersk issued an advisory telling customers it could take "6 days or more" for the line to clear. The company said that was an estimate and subject to change as more vessels reach the blockage or are diverted.
The rescue operation had intensified in both urgency and global attention with each day that passed, as ships from around the world, carrying vital fuel and cargo, were blocked from entering the canal during the crisis, raising alarm over the impact on global supply chains.
What it&#39;s really like steering the world&#39;s biggest ships
What it's really like steering the world's biggest ships
Promising signs first emerged earlier on Monday when the rear of the vessel was freed from one of the canal's banks.
People at the canal cheered as news of Monday's progress came in.
The Panama Maritime Authority said that according to preliminary reports Ever Given suffered mechanical problems that affected its maneuverability.

The ship had two pilots on board during the transit.

However, the owner of the vessel, Japanese shipping company Shoe Kisen insists that there had been no blackout resulting in loss of power prior to the ship’s grounding.
Instead, gusting winds of 30 knots and poor visibility due to a dust storm have been identified as the likely causes of the grounding, which left the boxship stuck sideways in a narrow point of the waterway.

The incident has not resulted in any marine pollution ot injuries to the crew, only some structural damage to the ship, which is yet to be determined.
"""

如果我们像前面一样使用标准的 BERT 模型,将会观察到一个错误,表明长的输入序列不能被处理。

from transformers import BertTokenizer,BertForQuestionAnswering
model_name='bert-large-uncased-whole-word-masking-finetuned-squad'
tokenizer = BertTokenizer.from_pretrained(model_name)
model = BertForQuestionAnswering.from_pretrained(model_name)

qa(question,answer_text,model,tokenizer)
Output:
RuntimeError: The size of tensor a (548) must match the size of tensor b (512) at non-singleton dimension 1

我们需要另一个模型,它已经过预训练,包括一个更长的输入文档序列,也可以处理相同的架构。

Longformer:长文档转换器

如果我们使用 Longformer 而不是标准的基于 transformer 的模型,较长序列的问题可以得到解决(在一定程度上)。标准的自我注意被放弃,取而代之的是局部窗口注意和任务驱动的整体注意被结合使用。预训练模型可以处理多达 4096 个标记的序列,而 BERT 中只有 512 个标记。在长文档任务中,Longformer 优于大多数其他模型,并且可以观察到在内存和时间复杂度方面的显著减少。

对于上面的问答对,我们来看看 longformer 的表现如何。

tokenizer = AutoTokenizer.from_pretrained("valhalla/longformer-base-4096-finetuned-squadv1")

model = AutoModelForQuestionAnswering.from_pretrained("valhalla/longformer-base-4096-finetuned-squadv1")
qa(question,answer_text,model,tokenizer) 
Output:
Answer: " two"

将模型转换为 Longformer

一个标准的变压器模型可以被转换成它的“长”版本。这些步骤包括将位置嵌入从$512$扩展到$4096$以及用 longformer 注意层替换标准的自我注意层。代码和解释可以在这里找到

结论

在本文中,我们介绍了各种基于 transformer 的问答模型,以及用于预训练它们的数据集。我们还看到了每个模型在 F1 和 EM 分数方面的不同基线。这些年来体系结构的改进导致了基线性能的提高。如果我们将这种模型转换为 Longformer 模型,那么它对于长文档的性能仍然可以得到改善。

随机森林指南:巩固决策树

原文:https://blog.paperspace.com/random-forests/

随机森林算法是最流行的机器学习算法之一,用于分类和回归。执行这两项任务的能力使它独一无二,并增强了它在无数应用程序中的广泛使用。它还在大多数情况下保证了高精度,使其成为最受欢迎的分类算法之一。随机森林由决定组成。它的树越多,算法就越复杂。它从树汇集的投票中选择最佳结果,使它变得健壮。让我们看看随机森林工作的内部细节,然后使用 scikit-learn 库用 Python 编写同样的代码。

在本文中,我们将了解以下模块:

  • 为什么是随机森林?
    • 决策树的缺点
    • 随机森林的诞生
  • 一个实时例子
  • 决策树和随机森林的区别
  • 随机森林的应用
  • 挖掘和理解逻辑
    • 随机森林,一块一块的
    • 计算特征重要性(特征工程)
    • 解码超参数
  • 算法编码
  • 优点和缺点
  • 总结和结论

为什么是随机森林?

随机森林是一种监督机器学习分类算法。在监督学习中,算法是用带标签的数据训练的,这些数据指导你完成训练过程。使用随机森林算法的主要优势是它支持分类和回归的能力。

如前所述,随机森林使用许多决策树来给你正确的预测。人们普遍认为,由于许多树木的存在,这可能会导致过度适应。然而,这看起来并不像是一个障碍,因为只有最好的预测(投票最多的)会从可能的输出类中挑选出来,从而确保平滑、可靠和灵活的执行。现在,让我们看看随机森林是如何创建的,以及它们是如何克服决策树中存在的缺点而进化的。

决策树的缺点

决策树是一个分类模型,它制定了一些特定的规则集,这些规则表明了数据点之间的关系。我们基于一个属性分割观察值(数据点),使得结果组尽可能不同,并且每个组中的成员(观察值)尽可能相似。换句话说,类间距离需要低,类内距离需要高。这是通过各种技术实现的,如信息增益、基尼指数等。

有一些差异会阻碍决策树的顺利实现,包括:

  • 当决策树非常深时,可能会导致过度拟合。随着分割节点决策的进展,每个属性都被考虑在内。它试图做到尽善尽美,以便准确地拟合所有的训练数据,因此学习了太多关于训练数据的特征,降低了它的概括能力。
  • 决策树是贪婪的,倾向于寻找局部最优解,而不是考虑全局最优解。在每一步,它都使用某种技术来寻找最佳分割。但是,本地的最佳节点可能不是全局的最佳节点。

为了解决这些问题,兰登森林公司来了。

随机森林的诞生

创建一个这些树的组合似乎是解决上述缺点的一种补救措施。随机森林最初是由田锦浩于 1995 年在贝尔实验室提出的。

大量的采油树可以通过减少在考虑单个采油树时通常出现的误差来超越单个采油树。当一棵树出错时,另一棵树可能表现良好。这是随之而来的一个额外的优势,这种组合被称为随机森林。

随机分割的数据集分布在所有树中,其中每个树集中于它已经被提供的数据。从每棵树上收集投票,选择最受欢迎的类作为最终输出,这是为了分类。在回归中,对所有输出取平均值,并将其视为最终结果。

与决策树不同,在决策树中,性能最佳的功能被作为分割节点,在随机森林中,这些功能是随机选择的。只考虑选择的特征包,并使用随机阈值来创建决策树。

Random Forest

算法是装袋的扩展。在 Bagging 技术中,从给定的数据集创建几个数据子集。这些数据集被分离并单独训练。在随机森林中,随着数据的划分,特征也被划分,并且不是所有的特征都用于生长树。这种技术被称为特征装袋。每棵树都有自己的一组特征。

一个实时例子

考虑一个案例,你想建立一个网站,有一个特定的主题供你选择。比方说,你联系了你的一个朋友来评论这个问题,你决定继续下去。然而,这个结果偏向于你所依赖的一个决定,并且没有探索各种其他的可能性。这就是使用决策树的地方。如果你就同样的问题咨询了一些人,并让他们投票选出最佳主题,那么之前产生的偏见就不会在这里出现了。这是因为在前一种情况下,最推荐的主题优先于唯一可用的选项。这似乎是一个偏差较小且最可靠的输出,是典型的随机森林方法。在下一节中,让我们来看看决策树和随机森林之间的区别。

决策树和随机森林的区别

与基于给定数据生成规则的决策树不同,随机森林分类器随机选择特征来构建几个决策树,并对观察到的结果进行平均。此外,过拟合问题是通过获取几个随机的数据子集并将它们提供给各种决策树来解决的。

然而,与随机森林相比,决策树的计算速度更快,因为它易于生成规则。在随机森林分类器中,需要考虑几个因素来解释数据点之间的模式。

随机森林的应用

随机森林分类器被用于跨越不同部门的若干应用中,如银行、医药、电子商务等。由于其分类的准确性,其使用逐年增加。

  • 顾客行为的识别。
  • 遥感。
  • 研究股票市场趋势。
  • 通过找出共同的模式来识别病理。
  • 对欺诈行为和非欺诈行为进行分类。

挖掘和理解逻辑

和任何机器学习算法一样,随机森林也包括两个阶段,训练测试。一个是森林的创建,另一个是从输入模型的测试数据中预测结果。让我们看看构成伪代码主干的数学。

随机森林,一块一块的

训练:对于中的B1,2,… B, ( B 为随机森林中的决策树数量)

  • 首先,应用 bagging 生成随机数据子集。给定训练数据集, XY ,通过替换采样来完成打包,来自 *X,Y,*的 n 个训练示例通过将它们表示为, X_bY_b

  • 从提供的所有功能中随机选择 N 个功能。

  • N 个特征中计算最佳分割节点 n

  • 使用考虑的分割点分割节点。

  • 重复以上 3 个步骤,直到生成 l 个节点。

  • 重复上述 4 个步骤,直到生成 B 个树。

  • 测试:训练阶段完成后,通过平均所有回归树的输出来预测未知样本的输出,x’

或者在分类的情况下,收集每棵树的投票,将投票最多的类作为最终预测。

可以基于数据集的大小、交叉验证或外差来选择随机森林中的最佳树数( B )。让我们来理解这些术语。

交叉验证一般用于减少机器学习算法中的过拟合。它获取训练数据,并在 k 表示的多次迭代中使用各种测试数据集对其进行测试,因此得名 k 倍交叉验证。这可以告诉我们基于 k 值的树的数量。

Cross-validation (Source: https://en.wikipedia.org/wiki/Cross-validation_(statistics))

出袋误差是在每个训练样本 x_i 上的平均预测误差,仅使用那些在其引导样本中没有 x_i 的树。这类似于留一交叉验证方法。

计算特征重要性(特征工程)

从这里开始,让我们了解如何使用 Python 中的 scikit-learn 库对 Random Forest 进行编码。

首先,测量特征的重要性可以更好地概述哪些特征实际上会影响预测。Scikit-learn 提供了一个很好的特性指示器,表示所有特性的相对重要性。这是使用基尼指数平均杂质减少量 (MDI) 计算的,该指数衡量使用该特征的树节点在一个森林中的所有树中减少杂质的程度。

它描述了每个特性在训练阶段所做的贡献,并对所有分数进行了缩放,使其总和为 1。反过来,这有助于筛选出重要的特性,并删除那些对模型构建过程影响不大(没有影响或影响较小)的特性。仅考虑几个特征的原因是为了减少过度拟合,这通常在有大量属性时实现。

解码超参数

Scikit-learn 提供了一些与随机森林分类器一起使用的功能或参数,以提高模型的速度和准确性。

  • n_estimators: 表示随机森林中树木的数量。树的数量越多,消耗更高计算能力的结果越稳定和可靠。默认值在 0.20 版中为 10,在 0.22 版中为 100。
  • **标准:**衡量分割质量的函数(基尼/熵)。默认值是基尼。
  • max_depth: 树的最大深度。这样一直持续到所有的叶子都是纯净的。默认值为无。
  • max_features: 每次分割时要寻找的特征数量。默认值为自动,即 sqrt(特征数量)。
  • min_samples_leaf: 出现在叶节点的最小样本数。默认值为 1。
  • **n _ jobs:**fit 和 predict 函数并行运行的作业数量。默认值为无,即只有 1 个作业。
  • oob_score: 是否使用 OOB (out-of-bag)抽样来提高泛化精度。默认值为 False。

算法编码

第一步:探索数据

首先从 sklearn 包中的数据集库中,导入 MNIST 数据。

from sklearn import datasets

mnist = datasets.load_digits()
X = mnist.data
Y = mnist.target

然后通过打印数据集的数据(输入)和目标(输出)来浏览数据。

Output:

[[ 0\.  0\.  5\. 13\.  9\.  1\.  0\.  0\.  0\.  0\. 13\. 15\. 10\. 15\.  5\.  0\.  0\.  3.
  15\.  2\.  0\. 11\.  8\.  0\.  0\.  4\. 12\.  0\.  0\.  8\.  8\.  0\.  0\.  5\.  8\.  0.
   0\.  9\.  8\.  0\.  0\.  4\. 11\.  0\.  1\. 12\.  7\.  0\.  0\.  2\. 14\.  5\. 10\. 12.
   0\.  0\.  0\.  0\.  6\. 13\. 10\.  0\.  0\.  0.]]
[0] 

输入有 64 个值,表示数据中有 64 个属性,输出类标签为 0。为了证明这一点,观察存储数据和目标的 Xy 的形状。

print(mnist.data.shape)
print(mnist.target.shape)

Output:

(1797, 64)
(1797,) 

数据集中有 1797 个数据行和 64 个属性。

第二步:数据预处理

这一步包括使用熊猫创建一个数据帧目标数据分别存储在 yX 变量中。 pd。序列用于获取 int 数据类型的 1D 数组。这些是属于类别数据的一组有限的值。 pd。DataFrame 将数据转换成一组表格值。 head() 返回数据帧的前五个值。让我们打印它们。

import pandas as pd

y = pd.Series(mnist.target).astype('int').astype('category')
X = pd.DataFrame(mnist.data)

print(X.head())
print(y.head())

Output:

   0    1    2     3     4     5    6    7    8    9  ...    54   55   56  \
0  0.0  0.0  5.0  13.0   9.0   1.0  0.0  0.0  0.0  0.0 ...   0.0  0.0  0.0   
1  0.0  0.0  0.0  12.0  13.0   5.0  0.0  0.0  0.0  0.0 ...   0.0  0.0  0.0   
2  0.0  0.0  0.0   4.0  15.0  12.0  0.0  0.0  0.0  0.0 ...   5.0  0.0  0.0   
3  0.0  0.0  7.0  15.0  13.0   1.0  0.0  0.0  0.0  8.0 ...   9.0  0.0  0.0   
4  0.0  0.0  0.0   1.0  11.0   0.0  0.0  0.0  0.0  0.0 ...   0.0  0.0  0.0   

    57   58    59    60    61   62   63  
0  0.0  6.0  13.0  10.0   0.0  0.0  0.0  
1  0.0  0.0  11.0  16.0  10.0  0.0  0.0  
2  0.0  0.0   3.0  11.0  16.0  9.0  0.0  
3  0.0  7.0  13.0  13.0   9.0  0.0  0.0  
4  0.0  0.0   2.0  16.0   4.0  0.0  0.0  

[5 rows x 64 columns]
0    0
1    1
2    2
3    3
4    4
dtype: category
Categories (10, int64): [0, 1, 2, 3, ..., 6, 7, 8, 9] 

使用从 sklearn 下的 model_selection 包导入的 train_test_split 将输入( X )和输出( y )分离成训练和测试数据。 test_size 表示 70%的数据点属于训练数据,30%属于测试数据。

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3) 

X_train 是训练数据中的输入。

X_test 是测试数据中的输入。

y_train 是训练数据中的输出。 y_test 是测试数据中的输出。

步骤 3:创建分类器

使用从 sklearn 中的系综包中获取的 RandomForestClassifier 在训练数据上训练模型。 n_estimators 参数表示随机森林中要包含 100 棵树。 fit() 方法是通过在 X_trainy_train 上训练模型来拟合数据。

from sklearn.ensemble import RandomForestClassifier

clf=RandomForestClassifier(n_estimators=100)
clf.fit(X_train,y_train) 

使用应用于 X_test 数据的 predict() 方法预测输出。这给出了存储在 y_pred 中的预测值。

y_pred=clf.predict(X_test)

使用从 sklearn 中的度量包导入的 accuracy_score 方法检查准确性。根据实际值( y_test )和预测值( y_pred )来估计精度。

from sklearn.metrics import accuracy_score

print("Accuracy: ", accuracy_score(y_test, y_pred))

Output:

Accuracy:  0.9796296296296296 

这给出了 97.96% 作为训练的随机森林分类器的估计精度。的确是个好成绩!

步骤 4:估计特征重要性

在前面的章节中,已经提到特征重要性是随机森林分类器的一个重要特征。现在让我们来计算一下。

feature_importances_sklearn 库提供,作为 RandomForestClassifier 的一部分。提取值,然后按降序排序,首先打印最重要的值。

feature_imp=pd.Series(clf.feature_importances_).sort_values(ascending=False)
print(feature_imp[:10])

Output:

21    0.049284
43    0.044338
26    0.042334
36    0.038272
33    0.034299
dtype: float64 

左栏表示属性标签,即第 26 个属性、第 43 个属性等等,右栏是表示特征重要性的数字。

步骤 5:可视化特征重要性

导入库 matplotlib.pyplotseaborn 来可视化上述特征重要性输出。给出输入和输出值,其中 x 由特征重要性值给出, y 分别是 64 个属性中最重要的 10 个特征。

import matplotlib.pyplot as plt
import seaborn as sns

%matplotlib inline
sns.barplot(x=feature_imp, y=feature_imp[:10].index)
plt.xlabel('Feature Importance Score')
plt.ylabel('Features')
plt.title("Visualizing Important Features")
plt.legend()
plt.show() 

Features vs Feature Importance Score

优点和缺点

虽然随机森林可用于分类和回归,但它在以下方面优于其他算法:

  • 这是一种稳健且通用的算法。
  • 它可用于处理给定数据中缺失的值。
  • 它可以用来解决无监督的最大似然问题。
  • 理解算法是一件轻而易举的事。
  • 默认超参数用于给出良好的预测。
  • 解决了过拟合问题。
  • 它可以用作特征选择工具。
  • 它能很好地处理高维数据。

一些不利因素伴随着优势,

  • 这在计算上是昂贵的。
  • 这很难解释。
  • 大量的树木需要大量的时间。
  • 创建预测通常很慢。

总结和结论

Random Forest 的简单性、多样性、健壮性和可靠性胜过其他可用的算法。通过调整超参数和选择关键特征,它为提高模型的精度提供了很大的空间。

本文从描述决策树如何经常充当绊脚石,以及随机森林分类器如何拯救开始。继续,差异和实时应用的探索。后来,通过同时探索其中的数学味道,伪代码被分解成不同的阶段。在下一节中,我们将介绍实际的编码经验。

还有很多,随机森林是解决机器学习可解决问题的一种方式。

参考

https://towards data science . com/understanding-random-forest-58381 e 0602d 2

https://synced review . com/2017/10/24/how-random-forest-algorithm-works-in-machine-learning/

https://builtin.com/data-science/random-forest-algorithm

https://www . data camp . com/community/tutorials/random-forests-classifier-python

https://en.wikipedia.org/wiki/Random_forest

https://en.wikipedia.org/wiki/Out-of-bag_error

https://sci kit-learn . org/stable/modules/generated/sk learn . ensemble . randomforestclassifier . html

基于递归神经网络的递归语言模型

原文:https://blog.paperspace.com/recurrent-neural-networks-part-1-2/

建立直觉

有大量的数据本质上是有序的,例如语音、时间序列(天气、金融等)。)、传感器数据、视频和文本,仅举几个例子。递归神经网络(RNNs)是专门为顺序数据处理设计的神经网络家族。在本文中,我们将通过探索文本理解、表示和生成的特殊性来了解 RNNs。

机器理解自然语言意味着什么?有许多不同的方法试图回答这个问题。我们期望从这种机器中得到的一种能力是告诉我们一个给定句子的可能性有多大的能力。在这种方法中,我们假设一个句子的可能性可以从语言的日常使用中确定。

神经语言模型试图解决确定一个句子在现实世界中的可能性的问题。尽管从表面上看,这似乎不是世界上最令人兴奋的任务,但这种类型的建模是理解自然语言的基本构件,也是自然语言处理(NLP)中的基本任务。一旦我们有了这样的系统,我们就可以用它来生成新的文本,产生一些奇妙的结果。一旦我们用这个模型武装起来,我们也可以在其他任务中获得重要的洞察力,比如机器翻译和对话生成。

关于语言单位的说明:我们大概可以认同一个事实,一个句子可以理解为一个序列,但是序列是什么呢?人物?文字?中间的东西?这是一个活跃的研究领域,对于什么是书面语的基本单位还没有达成共识。现在,我们将简单地使用单词作为我们的语言单位。

在本文中,我们将首先尝试了解语言模型的基础知识什么是递归神经网络我们如何使用它们来解决语言建模的问题。有了这些,我们将试着理解为什么这种方法会起作用以及如何起作用,并加以推广。最后,我们描述了如何将所有这些工具放在一起,以便自动生成文本

由于这篇文章旨在介绍自然语言理解和文本生成,我们将大部分实用和实现细节放在下一篇文章中,在下一篇文章中,我们将采取更实际的方法以不同的形式生成文本。

单词级语言模型

我们现在可以开始形式化我们的想法,让我们考虑一个由$T$个单词组成的句子$S$,这样

\(S = (w_1,w _2,...,w _T)\)

同时,每个符号$w_i$是包含所有可能单词的词汇表 $V$的一部分,

\(V = \{ v_1,v _2,...,v _{|V|} \}\)

其中$|V|$表示词汇表的大小。

如果我们想计算一个句子的概率,我们可以使用链规则来得到

\(p(S) = p(w_1,w _2,...,w _ T)= p(w _ 1)\ cdot p(w _ 2 | w _ 1)\ cdot p(w _ 3 | w _ 2,w _ 1)\ cdot \ cdot \ cdot p(w _ T | w _ { T-1 },w _{T-2},...,w _1 )\)

或者,更明智地说,

$ $ p(s)= \prod_^t p(w _ t | w _ { \ lt t })$ $

其中$w _{\lt t}$是指时间$t$之前的所有单词。

单词在时间$t$的概率取决于句子的前面的单词。为了计算一个句子的概率,我们只需要计算单个项$p(w _t | w _{\lt t})$并将它们相乘。

一般来说,语言模型试图在每个时间步长$t$上预测下一个单词$w_{t+1}$给定前面的单词$w _{\lt t}。

递归神经网络

人类的大部分理解都基于上下文。我们来考虑以下顺序——巴黎是 _ _ _ _ _ _ _ _最大的城市。用填空很容易。这意味着在序列的前面的元素中有关于编码的最后一个字的信息。

这种架构背后的想法是利用数据的这种顺序结构。这种神经网络的名字来源于它们以循环方式运行的事实。这意味着对序列的每个元素执行相同的操作,其输出取决于当前的输入和前面的操作。

这是通过将网络在时间$t$的输出与网络在时间$t+1$的输入进行循环来实现的。这些循环允许信息从一个时间步持续到下一个时间步。

rnn001Figure 1. Left: Circuit diagram. The black square represents a time delay of a single time step. Right: Same network as an unfolded computational graph, each node is associated with a particular time.

这种带有循环的结构可能有点令人困惑,但当我们看到展开计算图时形成的链时,它就变得直观了。现在我们有了一个体系结构,它可以在每个时间步长$x_t$接收不同的输入,具有在每个时间步长$o_t$产生输出的能力,并保持一个存储状态$h_t$包含关于直到时间$t$网络中所发生的信息。

重要的是要注意,我们可以像输入序列中的元素一样多次展开这个网络。此外,RNN 单元的每个“实现”的参数是相同的,使得模型的参数数量与序列的长度无关。

这种模式的成功关键在于我们在 RNN 单位内部执行的操作。在本系列的下一篇文章中,我们将研究这个细胞,特别是我们将介绍门控循环单位(GRU ),一种特殊类型的 RNN。现在,我们可以把 RNN 单元想象成一种计算,在这种计算中,我们更新存储向量$h$来决定,在每一个时间步,哪些信息我们想要保留,哪些信息不再相关,哪些信息我们想要忘记,以及哪些信息要从新输入中添加。RNN 单元还创建与当前隐藏状态(或记忆向量)紧密相关的输出向量。

值得注意的是,递归神经网络可以用于各种场景,这取决于如何输入和解释输出。这些场景可以分为三个主要的不同类别:

  • 顺序输入到顺序输出。机器翻译/词性标注和语言建模任务属于这一类。
  • 顺序输入到单输出。这个属性的一个任务是情感分析,在这个任务中,我们输入一个句子,我们想把它分为积极的、中性的或消极的。
  • 单输入到顺序输出。例如,图像标题的情况就是这样:我们向 RNN 提供了一张图片,并希望生成对它的描述。

递归语言模型

在这一节中,我们来看看如何使用这个架构来完成前面定义的语言建模任务。假设我们要计算句子的可能性,老板是一只叫 Joe 的猫。为此,我们需要估计以下概率,

$$ p(\text),p(\text| \text),p(\ text | \ text )...,p(\ text | \ text {和老板是一只名叫})的猫$$

单词嵌入

我们首先要考虑的是如何定义语言建模函数的输入。很明显,输入是$T - 1$个单词的序列,但是,这些单词中的每一个应该如何表示呢?正如在大多数深度学习系统中一样,我们希望尽可能少地用先验知识来约束模型。满足该标准的一种编码方案是 1-of-K 编码(one-hot-encoding)。

在这种方案下,词汇表$V$中的每个单词$i$都表示为一个二进制向量$\mathbf _i$。为了用向量$\mathbf _i$表示第$i$,我们将它的第$ I $-个元素设置为 1,所有其他元素设置为 0。

\(w _i = [0,0,...,1 (i\text{-th element}),0,...,0]^t \在\{ 0,1\}^{|V|}\)

在任何两个字向量之间的距离在两个字不同的情况下等于 1,而在两个字是同一个字的情况下等于 0 的意义上,嵌入在这种编码方案中的先验知识是最小的。

现在,我们模型的输入是一个$ T-1 \(one-hot vectors\)(\ mathbf _ 1,\mathbf _2,...,\mathbf _)\(。然后,将这些向量中的每一个乘以权重矩阵\)\mathbf\(,以获得一系列的*个连续的*个向量\)(\mathbf _1,\mathbf _2,...,\mathbf _)$这样,

\(\mathbf{x} _j = \mathbf{E}^T \mathbf{w} _j\)

性能提示:实际上不执行这个矩阵向量乘法。由于 vector $\mathbf _j$只有一个元素等于 1(第 I \(-个元素),其余的都是零,所以乘法运算相当于只取\)\mathbf$的第 I $-行。因为对矩阵进行切片比执行乘法要快,所以这就是实际操作的方式。

正向通路

图 2 说明了我们如何处理这个问题。首先,我们将内存向量$h$初始化为零。在第一时间步(用零表示)中,RNN 单元的输入是特殊标记,它表示句子的开始。作为输出,我们得到词汇表中每个可能单词的概率,给出句子标记的开始。内存向量在相同的操作中得到更新,并被发送到下一个时间步。现在我们重复时间步骤 1 的过程,其中是单元的输入,$h_1$是包含过去信息的存储状态,$p(w_2|\text{ < \s >和})$是输出。

rnn001Figure 2. RNN for language modelling.

一般来说,在每一个时间步,我们试图估计词汇表$V$中所有可能的下一个单词的概率分布。然后,RNN 的输出层是 softmax 层,它返回大小为$|V|\(的向量,其第\) I $-个元素指示单词$V_i$成为句子中出现的下一个单词的预测概率。

对于时间$t$处的输出是所计算的存储器状态$h_t$的仿射变换的情况,我们有

$ \(p(w _ t = k | w _ { \ lt t })= \ frac { exp(\ mathbf { v } _k^t h _ t+b _ k)} { \ sum _ { k ' } exp(\ mathbf { v } _{k'}^t h _ t+b _ { k ' })}\) $

偶数道次

我们已经有了一个架构,可以潜在地学习利用递归神经网络对序列进行评分。唯一缺少的部分是为网络定义一个适当的损失函数,以实际学习我们期望它学习的东西。

给定序列的损失是模型分配给正确输出的负对数概率。这是$L = - log(p(w _1,w _2,...,w _T))\(。使用链式法则和乘积的对数等于对数之和的事实,对于句子\)\mathbf$,我们得到,

$ $ L(\ mathbf )=-\ sum _ t log \ p _ (w _ t = x _ { t+1 })=-\ sum _ t log \ \ mathbf _ t[x _ { t+1 }]$ $

其中$\mathbf _t [x _{t+1}]$是对应于真实单词$x _{t+1}$的输出 softmax 的元素。

定义了损失,并假定整个系统是可微的,我们可以通过所有先前的 RNN 单元和嵌入矩阵反向传播损失,并相应地更新其权重。

推广到看不见的 n-grams

既然我们已经描述了我们的语言模型,让我们看看内部发生了什么,以便更好地理解为什么这种方法如此强大。特别是,我们关注模型如何推广到看不见的 \(n\)-grams(由$n$个连续单词组成的序列)。这是神经语言模型与经典的 NLP 方法(如$n$-gram 语言模型)相比的主要优势之一。

我们的模型可以被认为是两个函数(\(f \circ g\))的组合。第一个函数$f$将前面的单词序列(在$n-1$个单词之前)映射到一个连续的向量空间。结果向量$\mathbf$是内存状态或上下文向量
$ \(f:\{0,1\}^{|v| \次(n-1)} \右箭头\mathbb R^d\)

由$g$表征的第二阶段通过应用仿射变换(与矩阵相乘并与偏置向量相加),然后是 softmax 归一化(将输出转换成有效的概率分布),将连续向量$h$映射到目标词概率。

\(g(\mathbf{h}) = softmax(\mathbf{U}^T \mathbf{h} + \mathbf{c})\)

让我们更仔细地看看$g$执行的操作。我们可以忽略偏差项的影响。我们可以将矩阵向量乘法表示如下,

rnn001

其中输出向量的每个元素是$\mathbf\(与\)\mathbf^T$.的相应行的向量乘积你可以用这个形象化来说服自己这个事实。

这意味着字典的第$i$个单词的模型的预测概率与$\mathbf^T$的第$i$列与上下文向量$\mathbf$的对齐程度成比例。

现在,如果我们有两个上下文单词序列,后面通常跟有一组相似的单词,那么上下文向量$\mathbf _1$和$\mathbf _2$必须相似。为什么?因为一旦我们把它们乘以$\mathbf^T$,我们需要相同的一组单词来获得高概率。

这最终意味着,神经语言模型必须将后面跟有相同单词的$n$-grams 投射到上下文向量空间中的附近点。如果不满足该属性,并且两个$ n \(-gram 被映射到完全不同的向量,那么当将每个向量乘以\)\mathbf^T$时,我们将获得下一个单词的非常不同的概率,从而导致糟糕的语言模型。

让我们试着用一个例子来说明这一点,在不失一般性的情况下,我们将假设每个单词只有依赖于出现在句子中的前一个单词。换句话说,我们考虑一个上下文长度为 1 的二元模型语言模型。我们的训练语料由以下三个句子组成 1 :

  • 资格赛还剩三支队伍。
  • 四支队伍已经通过了第一轮。
  • 四组正在场地上比赛。

我们将把重点放在粗体短语上。每个二元模型的第一个单词是上下文单词,语言模型必须预测下一个单词的概率。

从我们之前的分析中,我们注意到模型必须将上下文单词“three”和“four”映射到上下文空间中的附近点。这是因为我们需要它们给单词“teams”(训练集的第一句和第二句)一个相似的概率。

目标单词向量$\mathbf _\(和\)\mathbf _\(也必然彼此接近,因为否则“teams”给定“four”(\) p(\ text | \ text )\ propto \ mathbf _ \ mathbf _ \()的概率和“groups”给定“four”(\) p(\ text | \ text )\ propto \ mathbf { 1

现在,假设我们用这个微小的语料库训练我们的语言模型,我们可以要求模型从未见过的二元组“三个组”的概率。我们的语言模型会将上下文词“三”投射到上下文空间中一个接近“四”的点上。根据该上下文向量,模型将必须向单词“groups”分配高概率,因为上下文向量$\mathbf _\(和目标单词向量\)\mathbf _$被很好地对齐。所以,即使没有见过二元模型“三组”,这种方法也能分配一个合理的概率。

这种方法的强大之处就在于此。这里的关键属性是使用单词和上下文的分布式表示,即我们使用单词和上下文的连续矢量表示。该方法基于语言的分布假设:

分布假说认为,出现在相同上下文中的单词往往有相似的意思。潜在的含义是“一个词的特点是由它所保持的公司”。 ACLWeb

这种方法不仅在两个主要方面利用了语言的概率性质。一方面,它学习给定上下文的一个单词的概率,另一方面,它在上下文空间中用相似向量表示相似上下文,在单词空间中用相似向量表示相似单词。

文本生成

我们现在如何使用这种语言模型来生成新的文本呢?它是相对直接的。如果我们想要生成一个新的句子,我们只需要随机初始化上下文向量$\mathbf _0$,然后在每个时间步长从输出单词概率分布中展开 RNN 采样一个单词,并将这个单词反馈到下一个时间 RNN 单元的输入。

我们来举个具体的例子。假设不只是想生成通用文本,但我们希望它有某些特征。例如,我们希望生成的句子就像是谢尔顿·库珀写的一样。一种方法是获取《生活大爆炸》的所有脚本,经谢尔顿过滤,然后根据这些数据训练我们的语言模型。

这种方法的问题是,观察的数量可能不足以训练我们的系统。我们的替代方案是使用预训练,即我们首先用通用文本训练我们的语言模型(例如,我们可以使用古腾堡语料库)。然后,一旦我们的模型对单词、上下文和文本结构有了一定的理解,我们就可以继续使用谢尔顿的台词来学习他说话的特定方式。

在本系列的下一篇文章中,我们将进一步研究这类模型的实现,并做一些实验。现在,如果你对这种技术的能力感兴趣,我会推荐你看看硅谷的自动脚本生成,在深度写作博客中。

后续步骤

在下一篇文章中,我们将更进一步。我们将更深入地研究这些模型的实现细节,并提及一些常见问题以及它们在实践中是如何解决的。我们还将建议一些开放数据集,并就我们可以使用哪种训练数据给出一些想法。最后,我们还将尝试使用 PyTorch 从头实现我们的第一个文本生成软件,并运行一些实验。保持联系!

进一步阅读

从 Kyunghyun Cho 教授关于分布式表示的自然语言理解的讲义中借用的例子。纽约大学,2015 年。链接

要开始使用您自己的 ML-in-a-box 设置,在此注册。

给你的朋友 10 美元,得到 15 美元

原文:https://blog.paperspace.com/referral-codes/

引入推荐代码

我们非常高兴地宣布一项新功能,它允许您与他人分享您对 Paperspace 的热情-推荐代码。

与任何人分享 10 美元

您推荐给 Paperspace 并使用您的唯一推荐代码注册的任何人,在添加有效的支付方式后,都会立即收到 $10 的点数。

收到 15 美元

作为帮助我们传播消息的回报,我们将为每个注册的朋友提供 15 美元的积分,一旦他们作为 Paperspace 客户被收取 25 美元。您可以推荐的朋友数量和获得的积分没有限制。

我该如何开始?

您可以从控制台的帐户信息页面访问您的唯一推荐代码。您可以与任何人共享此代码。

Referral Code in Account Info page

请随意用这些例子来分享爱:

在推特和脸书上分享

只需点击屏幕右上角的共享:

机器学习实践者强化学习指南:关于马尔可夫决策过程

原文:https://blog.paperspace.com/reinforcement-learning-for-machine-learning-folks/

深度强化学习是目前深度学习发展最快的子学科之一。在不到十年的时间里,研究人员已经使用深度 RL 来训练在各种游戏中表现超过专业人类玩家的代理人,从围棋等棋盘游戏到雅达利游戏和 Dota 等视频游戏。然而,强化学习的学习障碍可能会有点令人生畏,即使是对于那些之前涉足深度学习的其他子学科(如计算机视觉和自然语言处理)的人来说。

事实上,当我开始目前的工作时,那正是我发现自己的地方。有过 3 年左右从事计算机视觉工作的经历,突然投入到深度强化学习中。这看起来像是使用神经网络来做事情,但标准深度学习的大多数方法都不起作用。

差不多一年过去了,我终于开始欣赏强化学习(RL)的微妙本质,并认为写一篇帖子向来自标准机器学习经验的人介绍 RL 的概念会很好。我想以这样一种方式写这篇文章,如果有人(大约一年前,我就是这样)偶然看到它,这篇文章将成为一个很好的起点,并有助于缩短过渡时间。

所以让我们开始吧。

在本帖中,我们将介绍:

  • 环境
  • 国家
    • 马尔可夫性质
    • 现实生活中对马尔可夫性质的违背
    • 部分可观测性
  • 马尔可夫决策过程
    • 国家
    • 行动
    • 转移函数
    • 奖励函数
  • 部分可观测马尔可夫决策过程
  • 基于模型的学习与无模型的学习
    • 基于模型的学习
    • 无模型学习

环境

切换到 RL 时遇到的第一个差异也是最基本的一个差异是,神经网络是通过与动态环境而不是静态数据集的交互来训练的。通常你会有一个由图像/句子/音频文件组成的数据集,你会一次又一次地迭代它,以更好地完成你训练的任务。

在 RL 中,你有一个采取行动的环境。每一个行动都会从环境中获得回报。神经网络(或任何其他用于确定动作的功能)被称为代理。代理的寿命可能是有限的(例如,像游戏终止的马里奥这样的游戏),也可能是无限的(一个永远持续的游戏)。现在,我们将只关注终止的 RL 任务,这些任务也被称为情节任务。代理具有无限生命周期的 RL 任务将在本系列的下一部分中解决。

代理人在整个剧集中累积的累积报酬被称为回报。强化学习的目标就是让这个回报最大化。解决这个问题包括解决信用分配问题。它得名于这样一个事实,即在我们可以采取的所有可能的行动中,我们必须给每一个行动分配信用(回报),以便我们可以选择产生最大回报的行动。

从技术上讲,RL 代理的目标是最大化*预期收益。*但是在这里,我们不会在这方面停留太久。我们将在的下一部分更详细地讨论 RL 算法。在这一部分,我们仅限于讨论 RL 的中心问题。


一个环境的例子是 Atari 游戏,Breakout。

在中断中,座席可以向左、向右移动滑块,也可以不做任何操作。这些是代理可以执行的动作。当一个砖块被球击中时,环境会奖励代理人。做 RL 的目标是训练代理,让所有砖块都被摧毁。

国家

环境的状态基本上给了我们关于环境的信息。它可以是游戏的一个截图,一系列的截图,或者一堆支配环境如何运作的方程。一般来说,在每个时间步提供给代理的任何信息都有助于它采取行动。

马尔可夫性质

如果状态提供的信息足以确定给定任何动作的环境的未来状态,那么该状态被认为具有马尔可夫性质(或者在某些文献中,该状态被认为是*马尔可夫)。这是因为我们在进行强化学习时处理的环境被建模为马尔可夫决策过程。*我们稍后会详细介绍这些,但是现在,请理解这些环境的未来状态(对于任何操作)可以使用当前状态来确定。

想想雅达利游戏的突破。如果我们把游戏任意时刻的一张截图当做状态,是马尔科夫吗?

答案是否定的,因为根据刚才的截图,没有办法确定球的方向!考虑下面的图像。

球可能会朝两个白色箭头标记的任何一个方向运动,但我们无法判断是哪个方向。所以也许我们需要更多的信息来使状态马尔可夫化。我们把过去的 4 帧作为我们的状态,怎么样,这样我们至少对球的方向有个概念?

给定这 4 帧,我们可以计算球的速度和方向来预测未来的 4 帧(其中的后 3 帧与当前状态的前 3 帧相同)。

环境在本质上也可能是随机的。也就是说,假设相同的动作应用于相同的状态,结果仍然可能是不同的状态。如果所有动作的未来状态的概率分布可以仅使用当前状态和当前状态来确定,则该状态也被称为马尔可夫。当我们过一会儿看马尔可夫决策过程的数学公式时,这一点会变得更清楚。

现实生活中对马尔可夫性质的违背

考虑我们必须预测未来帧的情况。这里,我们必须预测球会以什么角度弹开。

当然,给定球的速度和角度,我们可以预测球反弹时的角度和速度。然而,只有当我们假设系统是理想的,球和圆盘之间的碰撞是纯弹性的,这才是正确的。

如果我们在现实生活中模拟这样的场景,碰撞的非弹性本质将违反状态的马尔可夫性质。然而,如果我们可以为状态添加更多细节,例如恢复系数(这将允许我们在计算中考虑碰撞的非弹性性质),状态将再次变为马尔可夫状态,因为我们可以精确地预测球的未来位置。谢天谢地,在突破中,碰撞是纯弹性的。

为现实生活环境构建马尔可夫状态是非常困难的。第一个大障碍是传感器总是在读数中加入一些噪音,这使得正确预测(甚至测量)环境的真实状态变得不可能。第二,有许多国家的元素可能根本不为我们所知。

考虑一个经典的强化学习任务,包括平衡一根垂直的柱子。下面是真实任务的视频。

https://www.youtube.com/embed/5Q14EjnOJZc?feature=oembed

下面的摘录摘自巴尔托和萨顿的名著《强化学习:导论》中关于马尔可夫性质的讨论,该书谈到了极点平衡实验中状态的马尔可夫性质。

在前面介绍的杆平衡任务中,如果状态信号精确地指定了小车沿轨道的位置和速度、小车和杆之间的角度以及该角度变化的速率(角速度),或者使得精确地重建该位置和速度成为可能,则该状态信号将是马尔可夫的。在理想的车-杆系统中,给定控制器采取的行动,该信息将足以准确预测车和杆的未来行为。然而,在实践中,永远不可能准确地知道这一信息,因为任何真实的传感器都会在其测量中引入一些失真和延迟。此外,在任何真实的车杆系统中,总会有其他影响,如杆的弯曲、车轮和杆轴承的温度以及各种形式的反冲,这些都会轻微影响系统的行为。如果状态信号仅仅是小车和杆子的位置和速度,这些因素将导致对马尔可夫性质的破坏。

部分可观测性

虽然很难有具有马尔可夫性质的状态,特别是在现实生活中,但好消息是,具有近似马尔可夫性质的近似在解决强化学习任务的实践中已经足够好了。

考虑极点平衡实验的情况。一种将车的位置分为三个区域的状态——右、左和中间——并使用类似的内在状态变量的粗略量化值(如车的速度、杆的角速度等)。),对于通过 RL 方法解决平衡问题来说足够好。这样的状态与马尔科夫状态相去甚远,但可能会通过迫使代理忽略对解决任务毫无用处的细微差别来加速学习。

在对这种环境进行理论分析时,我们假设确实存在一种状态,通常仅称为“状态”或“真实状态”,具有我们无法观察到的马尔可夫性质。我们所能观察到的只是真实状态的一部分或更嘈杂的版本,这就叫做观察。这被称为部分可观测性,这样的环境被建模为部分可观测的马尔可夫决策过程(或简称为 POMDPs)。

请注意,在如何表示状态和观察值的文献中存在一些差异。一些文献使用单词“状态”来表示具有马尔可夫属性的真实隐藏状态,而其他文献将神经网络的输入称为状态,而将具有马尔可夫属性的隐藏状态称为“真实状态”。

现在我们已经建立了对马尔可夫性质的理解,让我们正式定义马尔可夫决策过程。

马尔可夫决策过程

几乎强化学习中的所有问题在理论上都被建模为在马尔可夫决策过程中最大化回报,或者简称为 MDP。MDP 有四个特点:

  1. \(\mathcal{S}\):代理在与环境交互时经历的状态集。假设状态具有马尔可夫性质。
  2. \(\mathcal{A}\):代理可以在环境中执行的一组合法操作。
  3. \(\mathcal{P}\):在给定初始状态和应用于该状态的操作的情况下,控制环境将转换到哪个状态的概率分布的转换函数。
  4. \(\mathcal{R}\):奖励函数,确定当代理使用特定动作从一种状态转换到另一种状态时将获得什么奖励。

马尔可夫决策过程通常表示为$ \ mathcal = \乐浪\mathcal、\mathcal、\mathcal、\mathcal \rangle $。现在让我们更详细地研究一下它们。

国家

这组状态$ \mathcal \(,对应于我们在上一节中讨论的*状态*。状态可以是有限的,也可以是无限的。它们可以是离散的,也可以是连续的。\) \mathcal $描述了 MDP 的状态可以采用的合法值,因此也被称为状态空间。

有限离散状态空间的一个例子是我们用于突围游戏的状态,即过去的 4 帧。该状态由 4 个210 x 160图像按通道连接而成,因此该状态是一个由4 x 210 x 160个整数组成的数组。每个像素可以取从0255的值。因此,状态空间中可能状态的数量是$ 255^{4 * 210 * 160}。这是一个巨大但有限的状态空间。

连续状态空间的一个例子是 OpenAI Gym 中的CartPole-v0环境,它基于解决倒立摆任务。

CartPole-v0 environment from OpenAI Gym

该状态由 4 个内在状态变量组成:小车位置、速度、极点角度和尖端的极点速度。以下是状态变量的合法值。

State Space of the CartPole Environment of Gym

状态空间是无限的,因为连续变量可以在边界之间取任意值。

行动

操作空间$ \mathcal $描述代理可以在 MDP 中执行的合法操作。这些动作将环境转换到一个不同的状态,在这种转换之后,代理会得到一个奖励。

就像状态空间一样,动作空间基本上是合法动作的范围。它们既可以像突围赛的动作空间一样离散,也就是[NO-OP, FIRE, RIGHT, LEFT],让滑球什么都不做,发射球,分别向右或向左移动。连续动作空间可以是赛车游戏的空间,其中加速度是代理可以执行的动作。这个加速度可以是一个连续变量,范围从比如说$ -3 m/s2 美元到 3 m/s2 美元。

转移函数

转移函数给出了状态空间上的概率,该概率描述了给定动作$ a \(应用于处于状态\) s \(的环境时,环境转移到各种状态的可能性。函数\) \ mathcal :\ mathcal \ times \ mathcal \ right arrow \ mathcal $给出未来状态的概率分布。

\(P_{ss'}^a = P(s' \vert s,a)= \ mathbb { p }[s _ { t+1 } = s ' \ vert s _ t = s,A_t = a]\)

雅达利游戏突破的未来状态的概率分布将在$ 255^{4 * 210 * 160} $个状态上定义。在实践中,计算这样的分布通常是难以处理的,我们将在这篇文章的后面看到如何处理这种情况。

转移函数的形式赋予了 MDP 状态的马尔可夫性质。该函数被定义为在给定当前状态$ s \(和仅应用\) a $ 的操作的情况下,产生未来状态$ s' $的分布。如果转移函数需要状态和动作的历史,那么状态将不再具有马尔可夫属性。

因此,马尔可夫属性也用数学术语表示如下:

$ \(\ mathbb { P }[S _ { t+1 } = S ' \ vert S _ t = S,A_t = A]= \ mathbb { P }[S _ { t+1 } = S ' \ vert S _ t,A _ t,S_{t-1},a_{t-1}....S_0,a_0]\)

也就是说,您只需要当前状态和动作来定义未来状态的分布,而不考虑代理访问的状态和采取的动作的历史。

奖励函数

奖励函数告诉我们,如果代理在状态$ s_t $中采取行动$a$将会得到什么奖励。函数$ \ mathcal :\ mathcal \ times \ mathcal \ right arrow \ mathbb $为我们提供了代理在特定状态下执行特定操作的报酬。

\(R(s,A)= \ mathbb { E }[R _ { t+1 } \ vert S _ t = S,A_t = a]\)

部分可观测马尔可夫决策过程

不难扩展 MDPs 的理论框架来定义部分可观测的 MDP,或 POMDP。POMDP 被定义为$ \ mathcal = \乐浪\mathcal、\mathcal、\mathcal、\mathcal、\Omega、\mathcal \rangle $。

除了$\mathcal、\mathcal、\mathcal、\mathcal、$我们还定义了:

  1. \(\Omega\),描述观察值的合法值的集合。
  2. 条件观察函数$ O (s ',a) \(告诉我们,如果代理采取行动\) a \(转换到新状态\) s' $时,代理将接收到的观察分布。

模型 v/s 模型自由学习

正如我们所讨论的,对于游戏的突破,我们的状态是一个非常高的维度,维度为 255 美元^ {4 * 210 * 160}。对于如此高的状态,转换函数需要输出这些状态的分布。正如您可能已经发现的那样,在当前的硬件能力下,做到这一点是很困难的,记住,每一步都需要计算转移概率。

有几种方法可以避开这个问题。

基于模型的学习

我们可以努力学习突破游戏本身的模型。这将涉及使用深度学习来学习转换函数$ /mathcal \(和奖励函数\) \mathcal $。我们可以将状态建模为一个4 x 210 x 160维的连续空间,其中每个维度的范围从 0 到 255,并提出一个生成模型。

当我们学习这些东西时,据说我们正在学习环境的模型。我们使用这个模型来学习一个名为策略的函数,\(\ pi:\ mathcal { S } \ times \ mathcal { A } \ right arrow[0,1]\),它基本上为我们提供了代理在经历状态$ s \(时要采取的操作\) a$的概率分布$ \pi(s,a) $。

一般来说,当你有一个环境模型时,学习一个最优策略(一个最大化回报的策略)被称为计划。学习环境模型以达到最优策略的 RL 方法被归类于基于模型的强化学习

无模型学习

或者,我们可能会发现底层环境太难建模,也许直接从经验中学习比之前试图学习环境的模型更好。

这与人类学习大部分任务的方式相似。汽车驾驶员实际上并没有学到控制汽车的物理定律,而是学会了根据他对汽车如何对不同动作做出反应的经验做出决定(如果你试图加速太快,汽车就会打滑等等)。

我们也可以这样训练我们的策略$ \pi $。这样的任务被称为控制任务。试图直接从经验中学习控制任务的最优策略的 RL 方法被归类为无模型强化学习。

包扎

这部分到此为止。这部分是关于设置问题,试图给你一个我们在强化学习中处理问题的框架的感觉,首先通过直觉,然后通过数学。

下一部分中,我们将探讨如何解决这些任务,以及在应用从标准深度学习中借鉴的技术时我们面临的挑战。在那之前,这里有一些资源可以用来进一步深化这篇文章中的内容!

  1. 第三章, 强化学习:巴尔托&萨顿简介。
  2. David Silver 的 RL 课程——第 2 讲:马尔可夫决策过程

大规模的可靠性和性能

原文:https://blog.paperspace.com/reliability-and-performance-at-scale/

在过去几年中,纸张空间云已经大规模增长。我们现在支持 60 多万用户,为我们的用户提供近 1 亿小时的 GPU 计算服务。

随着我们的发展,我们遇到了一些扩展障碍。我们充分意识到停机和错误是不可接受的,尤其是当我们越来越多的用户群在生产中运行时。

我们已经在幕后进行了一些改进,以增强虚拟机运行状况检查和警报。我们看到错误率下降,内核和梯度的加速和减速持续时间更快,梯度笔记本的内核性能更好。在 Paperspace 控制台中,我们已经超过了 99.85%的持续无崩溃会话率,而且这一趋势还在继续。

Console data from May 2022

我们正夜以继日地努力正面解决剩余的可靠性和性能问题。在接下来的几个月里,在大大小小的问题上,期待着有影响的变化。我们将在硬件和软件方面进行改进,包括重新编写计费引擎,这是我们一段时间以来一直困扰的问题。

在接下来的几个版本中,我们将继续在幕后进行这些改进。感谢您的支持,我们正在扩展世界上最好的云来加速计算。

💜PS 工程

使用 PyTorch 和棉被的可重复机器学习

原文:https://blog.paperspace.com/reproducible-data-with-pytorch-and-quilt/

在本文中,我们将训练 PyTorch 模型来执行超分辨率成像,这是一种优雅地放大图像的技术。我们将使用棉被数据注册库将训练数据和模型快照为版本数据包

side-by-side
超分辨率成像(右)从低分辨率图像(左)推断像素值。

再现性危机

机器学习项目通常从获取数据、清理数据和将数据转换成模型原生格式开始。这样的手动数据管道创建起来很乏味,并且很难随着时间的推移、在合作者之间以及在机器之间重现。此外,经过训练的模型经常被随意存储,没有版本控制。综合起来,上述挑战被称为 机器学习中的再现性危机

这太糟糕了,有时感觉就像回到了没有源代码控制的时候。
—皮特·沃顿

作为开发人员,我们有大量的工具来对代码进行版本控制。GitHub、Docker 和 PyPI 就是三个例子。我们使用这些服务来共享和发现应用程序的构建模块。构建块是版本化的和可部署的,这使得它们具有高度的可复制性。

但是可重用的数据呢?在本文中,我们将创建像 PyPI 包一样部署的可重用数据单元:

$ quilt install akarve/BSDS300 

在 GitHub 上存储数据怎么样?

如果你曾经试图在 GitHub 上存储数据,你可能会发现大数据是不受欢迎的。GitHub 将文件限制为 100MB,将存储库限制为 1GB。GitHub LFS 放宽了这些限制,但幅度不大。

相比之下,Quilt 存储库可以保存数万亿字节的数据和数千个文件,如本例中的 Allen Cell Explorer 所示。包直接从 blob 存储中流出。因此,客户可以像从亚马逊 S3 读取数据一样快地获取数据。此外,Quilt 将数据序列化为列格式,比如 Apache Parquet。序列化加快了 I/O 并提高了网络吞吐量。

示例:使用 PyTorch 和棉被的超分辨率成像

将培训数据版本化

在这一部分,我们将打包我们的测试和训练集。如果您已经熟悉数据包,或者渴望训练模型,请跳到下一节,将数据部署到任何机器

我们将在伯克利分割数据集和基准测试[1], BSDS300 上训练我们的超分辨率模型。首先,从伯克利下载数据 (22 MB)。将内容解压到一个干净的目录中,并打开BSDS300文件夹。您将看到以下内容:

$ ls
iids_test.txt  iids_train.txt  images 

可选地,添加一个README.md文件,以便您的数据包是自文档化的:

# Berkeley Segmentation Dataset (BDS300)
See [BSDS on the web](https://www2.eecs.berkeley.edu/Research/Projects/CS/vision/bsds/).

# Citations
    ```
@InProceedings{MartinFTM01,
  author = {D. Martin and C. Fowlkes and D. Tal and J. Malik},
  title = {A Database of Human Segmented Natural Images and its
           Application to Evaluating Segmentation Algorithms and
           Measuring Ecological Statistics},
  booktitle = {Proc. 8th Int'l Conf. Computer Vision},
  year = {2001},
  month = {July},
  volume = {2},
  pages = {416--423}
}
    ```py 

要将上述文件转换成版本化的数据包,我们需要安装 Quilt:

$ pip install quilt 

Windows 用户,先安装 Visual C++可再发行版 for Visual Studio 2015 。要从 Jupyter 单元安装被子,请参见附录 1

接下来,从当前工作目录的内容构建一个数据包:

$ quilt build YOUR_USERNAME/BSDS300 . 

此时,包驻留在您的机器上。如果你想把这个包部署到其他机器上,你需要在quiltdata.com上有一个免费账户。

$ quilt login
$ quilt push YOUR_USERNAME/BSDS300 --public 

现在,世界上任何人都可以精确地复制相同的数据:

quilt install akarve/BSDS300 -x e472cf0 

-x参数指定了包实例的 SHA-256 摘要。

正如 PyPI 托管可重用的软件包(如 pandas、numpy 和 torch),Quilt 托管可重用的数据包。

每个包都有一个登录页面,其中显示了文档、修订历史等内容。下面我们看到包的大小(22.2 MB),文件统计(300。jpg 文件),和包哈希(e472cf0…)。

newlanding
被子上的伯克利分割数据集。

将数据部署到任何机器

在远程机器上安装 Quilt,然后安装 BSDS300 包。

$ pip install quilt[img]
$ quilt install akarve/BSDS300 

(要自定义数据包的存储位置,请参见附录 2 。)

我们现在准备探索 Python 中的 BSDS300:

In [1]:
from quilt.data.akarve import BSDS300 as bsds
bsds.images

Out [1]:
<GroupNode>
test/
train/ 

包是可浏览的,就像文件系统一样。bsds.images.test包含图片:

In [2]: bsds.images.test
Out[2]:
<GroupNode>
n101085
n101087
n102061
n103070
n105025
… 

我们可以使用 Quilt 的asa=回调来显示bsds.images.test“作为一个情节”。

%matplotlib inline
from quilt.asa.img import plot
bsds.images.test(asa=plot(figsize=(20,20))) 

bsd-asa-plot

在引擎罩下,quilt.asa.img.plot()对每张图片做如下处理:

from matplotlib import image, pyplot

pyplot.imshow(
    image.imread(
        bsd['images']['test']['n101085']()
    )) 

bsd['images']['test']['n101085']()代表文件bimg/test/101085.jpg。Quilt 将n添加到文件名前面,这样每个包节点都是一个有效的 Python 标识符,可以用 Python 的点操作符或括号访问。后面的括号()指示 Quilt 返回底层 数据片段 的路径。

用被子包裹训练 PyTorch 模型

超分辨率成像优雅地推断出测试实例中缺失的像素值。为了让模型推断分辨率,它需要高分辨率图像的训练语料库(在我们的例子中,是 BSDS300 训练集)。

那么,我们如何将包中的数据放入 PyTorch 呢?

Quilt 提供了一个高阶函数asa.pytorch.dataset(),它将打包的数据转换成一个torch.utils.data.Dataset对象:

from quilt.data.akarve import BSDS300 as bsds
from quilt.asa.pytorch import dataset

return bsds.images.train(
    asa=dataset(
        include=is_image,
        node_parser=node_parser,
        input_transform=input_transform(...),
        target_transform=target_transform(...)
    )) 

要获得完整的代码示例,请参见 pytorch 的这个分支-示例。fork 包含训练和应用我们的超分辨率模型所需的所有代码。叉子有什么特别之处?代码更少。

有了 Quilt 管理数据,几十行样板代码就消失了。不再需要下载、解包和加载数据的一次性功能。

运行培训作业

储存库quilt data/py torch-examples包含一个入口点脚本train_super_resolution.sh,它调用main.py来安装依赖项,训练模型,并将模型检查点保存到磁盘:

#!/usr/bin/bash
export QUILT_PRIMARY_PACKAGE_DIR='/storage/quilt_packages'
cd super_resolution
pip install -r requirements.txt
mkdir -p /storage/models/super_resolution/
N_EPOCHS=$1
echo "Training for ${N_EPOCHS:=10} epochs\n"
# train
python main.py \
	--upscale_factor 3 \
	--batchSize 4 \
	--testBatchSize 100 \
	--nEpochs $N_EPOCHS \
	--lr 0.001 \
        --cuda 

您可以克隆这个 Paperspace 作业来训练您自己帐户中的模型。在 NVIDIA P4000 上,培训大约在 12 分钟内完成。如果你想改变模型的存储位置,请参见 main.py

快照 PyTorch 模型

除了数据之外,还可以在 Quilt 中存储模型及其完整的修订历史。

$ quilt build USR/PKG /storage/models/super_resolution/
$ quilt push USR/PKG 

有了上面创建的包,任何人都可以恢复过去的训练时间。

推论:加大我的分辨率

现在我们的模型已经训练好了,我们可以重新水合 epoch 500 并超解析测试集中的图像:

$ bash infer_super_resolution.sh 500 304034 

下面是结果。

panther-side-by-side-1
经过 500 个历元的训练推断出的超分辨率(右图)。

这里有一个推理任务。为了使推理工作,确保您的模型检查点保存在/storage/models/super_resolution中(如上面的训练脚本所示),或者更新代码以使用不同的目录。此外,如果你和--cuda一起训练,你需要打电话给super_resolve.py --cuda

结论

我们已经使用 Quilt 打包数据,将数据部署到远程机器,然后训练 PyTorch 模型。

我们可以把可复制的机器学习想象成一个三变量的方程:
代码+数据+模型=可复制性

通过将版本化数据和版本化模型添加到我们的工作流中,我们使开发人员能够更轻松地获得跨机器、跨合作者的一致结果。

承认

感谢最初的 super_resolution 示例的开发者,感谢 BSDS300 的策展人和创作者,感谢 Dillon ErbAdam Sah 审阅本文草稿,感谢 Paperspace 提供计算资源。

附录

1:在 Jupyter 笔记本中安装被子

由于杰克·范德普拉斯详述的原因,从笔记本内部安装软件包很复杂。以下是正确的做法:

import sys
!{sys.executable} -m pip install quilt 

2:在特定目录下安装软件包

如果你希望你的数据包存在于一个特定的目录中,例如在一个共享的驱动器上,创建一个quilt_packages目录。如果您使用 Paperspace Gradient,持久化的/storage目录是数据包的理想之家。

$ mkdir -p /storage/quilt_packages 

使用环境变量告诉 Quilt 在哪里搜索包:

%env QUILT_PRIMARY_PACKAGE_DIR=/storage/quilt_packages 

3:给我看文件

Quilt 对文件进行重复数据删除,将其序列化为高速格式,并将其存储在quilt_packages中的唯一标识符(文件的 SHA-256 哈希)下。重复数据消除的结果是,跨数据包重复的文件在磁盘上只存储一次。包含相同文件的数据包,例如foo.csv,使用对quilt_packagesfoo.csv片段的单一引用。

以上所有这些都提高了性能并减少了磁盘空间,但有时您希望使用底层文件。为此,使用quilt.export:

quilt.export("akarve/BSDS300", "SOME_DIRECTORY") 

你现在可以将你的机器学习代码指向SOME_DIRECTORY,一切都正常了。这里有一个用 PyTorch 模型导出图像来推断其分辨率的例子。


  1. BSDS300 数据集来自@InProceedings{MartinFTM01, author = {D. Martin and C. Fowlkes and D. Tal and J. Malik}, title = {A Database of Human Segmented Natural Images and its Application to Evaluating Segmentation Algorithms and Measuring Ecological Statistics}, booktitle = {Proc. 8th Int'l Conf. Computer Vision}, year = {2001}, month = {July}, volume = {2}, pages = {416--423} } ↩︎

如何用 GFP-GAN 修复受损照片

原文:https://blog.paperspace.com/restoring-old-photos-using-gfp-gan/

2022 年 7 月 7 日:这篇博客文章及其相关的笔记本和回购已经更新,可以与 GFPGAN 1.3 一起使用。


自摄影发明以来的近 200 年里,我们一直面临着同样的问题:我们如何防止损害的积累和破坏图像的质量。印在胶片及其前体介质上的照片会因暴露于自然环境和老化而受损,更不用说材料本身的脆弱性和对急性损伤的敏感性了。

虽然数码照片已经消除了很多潜在的问题,包括储存和保护,但数码照片仍然存在一种内在的模糊性,这种模糊性在胶片中是没有的。这在很大程度上是因为一张 35 毫米的胶片能够捕捉的信息是 4k 数字图像捕捉设备的几倍。

因此,这两种摄影媒介各自的缺点都有一个相似的问题:如何恢复或提升这些图像的分辨率和质量。

绿色荧光蛋白-氮化镓

GFP-GAN 是一种新的 GAN 架构,旨在提升受损、老化和其他低分辨率照片中人脸的质量,由作者研究人员在他们的论文“利用生成性面部先验信息进行真实世界的盲人脸恢复”中介绍,,,张宏伦和。在实践中,这对于图像质量具有恢复和升级效果,并且可以与其他模型结合使用,以显著提高图像质量。

Source

GFP-GAN 的组成如下:

首先,退化去除模块(在这种情况下,是普通的 U-Net)获取受损照片,并在提取潜在特征的同时去除退化。该模块特别提取两种类型的特征:将输入图像映射到最接近的潜在 StyleGAN2 码的潜在特征,以及用于调制 StyleGAN2 特征 的多分辨率空间特征。

接下来,预训练的 StyleGAN2 模型充当生成面部先验。在 GAN 和 DRM 之间,潜在特征由几个多层感知器转换成风格向量。这些向量然后被用来产生中间卷积特征,目的是使用空间特征来进一步调制最终输出。

通道分割要素变换允许空间要素用于预测变换参数,这些变换参数可用于缩放和置换生成器中要素地图中的要素。这种情况只发生在某些通道中,因此如果模型认为没有必要更改某些特征,则允许这些特征不加更改地通过。

最后,使用所生成图像的生成器感知重建损失、对抗性损失、ID 损失和面部成分损失来进一步细化所生成的图像,直到训练完成。

在实践中,这允许 GFP-GAN 从根本上恢复和升级受损图像的面部质量。当结合作者之前的作品 REAL-ESRGAN 时,我们可以使用这些模型来增强照片,远远超过过去在相同挑战中尝试的水平。

在斜坡上跑步

建立

由于图像生成的成本很高,建议您在本地或远程机器上使用这个带有 GPU 的包。我们现在将浏览一个快速教程,使用 GFP-GAN repo 的 Gradient 预制 fork 在远程实例上运行包。

登录到 Gradient,并在 Gradient 中导航到您想要工作的项目空间。然后,使用右上角的按钮创建一个新笔记本。

因为这个包是用 PyTorch 编写的,所以根据您的目的选择 PyTorch 运行时和合适的 GPU。这应该在我们提供给所有用户的免费 GPU 上运行良好,这取决于供应商。

最后一步是切换页面底部的高级选项。确保将 GFP-GAN repo 的预制分支的 url 粘贴到“工作区 URL”框中。现在你可以启动笔记本了。

An example of the photo restoration in practice. Notice how the effect is more pronounced on faces.

梯度运行 GFP-GAN

一旦你的笔记本准备好了,打开笔记本“Run-GFPGAN.ipynb”

您可以使用这个笔记本来运行一个简单的演示,使用 repo 的创建者提供的预训练 GFP-GAN 模型实例。你可以运行所有现在看到提供的样本图像演示工作,但如果你想使用自己的图像:他们需要直接上传到梯度。

# Install basicsr - https://github.com/xinntao/BasicSR
# We use BasicSR for both training and inference
!pip install basicsr

# Install facexlib - https://github.com/xinntao/facexlib
# We use face detection and face restoration helper in the facexlib package
!pip install facexlib

# If you want to enhance the background (non-face) regions with Real-ESRGAN,
# you also need to install the realesrgan package
!pip install realesrgan

当您点击 run all 时,它将首先安装所需的库依赖项。第一个细胞中的细胞都来自同一个研究团队,他们互相促进。BasicSR 是一个用于图像和视频恢复的开源工具包,facexlib 打包了一组现成的算法来处理面部特征,Real-ESRGAN 用于增强受损图像的背景,就像 GFP-GAN 恢复面部一样。

!pip install -r requirements.txt
!pip install opencv-python==4.5.5.64 

您可能还需要在终端中输入以下命令。这需要在终端本身中运行,因为在安装过程中需要肯定的“是”才能在终端中进行安装。

apt-get update && apt-get install libgl1

下一个代码单元包含确保我们的环境可以运行 GFP-GAN 所需的剩余包。

!python setup.py develop
!wget https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.3.pth -P experiments/pretrained_models 

最后,我们可以运行 setup.py 脚本来完成运行生成器的环境设置。我们还使用一个 wget 来获得作者提供的预训练 GFP-GAN 模型以供使用。

!python inference_gfpgan.py -i inputs/whole_imgs -o results -v 1.3 -s 2

要实际运行生成器,请运行笔记本中包含该命令的最后一个单元格。它会将您新恢复的图像直接输出到新创建的结果目录中。

An example i made using a random image I found on Reddit

结论

本教程分解了 GFP-GAN 的基本架构,并演示了如何使用 GFP-GAN 及其堂兄包 REAL-esrGAN 来大幅恢复老化和损坏的照片。虽然许多人将照片修复作为一种爱好,但这可能很快会使这种努力变得更加复杂,耗时更少。

感谢您的阅读!

来源和参考资料:

Towards Real-World Blind Face Restoration with Generative Facial PriorBlind face restoration usually relies on facial priors, such as facialgeometry prior or reference prior, to restore realistic and faithful details.However, very low-quality inputs cannot offer accurate geometric prior whilehigh-quality references are inaccessible, limiting the applicability inre…arXiv.orgXintao WangCasual GAN Papers: GFP-GAN ExplainedTowards Real-World Blind Face Restoration with Generative Facial Prior by Xintao Wang et al. explained in 5 minutes.Kirill Demochkin’s Picture

Paperspace 上的 RTX GPU

原文:https://blog.paperspace.com/rtx-gpus-on-paperspace/

我们很高兴地宣布,我们正在添加 NVIDIA RTX 卡到我们的 GPU 实例阵容。NVIDIA 的 RTX 一代引入了用于 3D 加速的专用光线跟踪(RT)核心和用于人工智能处理的张量核心的概念。

NVIDIA 对新光线追踪技术的描述:

支持 RTX 的 GPU 包括专用的光线跟踪加速硬件,使用先进的加速结构,并实施全新的 GPU 渲染管道,以实现游戏和其他图形应用程序中的实时光线跟踪。开发人员通过 NVIDIA OptiX、使用 NVIDIA 光线跟踪库增强的 Microsoft DXR 以及即将推出的 Vulkan 光线跟踪 API 来利用光线跟踪加速。

英伟达关于 RTX 人工智能功能的简介:

NVIDIA NGX 功能利用张量内核来最大限度地提高其运行效率,并需要支持 RTX 的 GPU。NGX 使开发人员可以通过预先训练的网络轻松地将人工智能功能集成到他们的应用程序中。

除了这些新的板载技术,RTX 一代还包括新一代 GPU 的典型性能提升,因此将 RTX 加入我们的 GPU 阵容是显而易见的。

从今天起,我们提供 RTX 4000 芯片,但我们也计划提供其他卡。

我们希望你喜欢!

如何将你的 Chromebook 变成一台超级电脑

原文:https://blog.paperspace.com/run-a-virtual-windows-or-linux-machine-on-chromebook/

Chromebooks 是低端的超便携“笔记本电脑”,非常适合需要比智能手机更多屏幕空间的高度移动人士。谷歌一直在补贴 Chromebooks(更多的人=更多的广告),所以它们是传统笔记本电脑的一个非常实惠的替代品。Chromebooks 重量如此之轻,加上其 200 美元的价格标签,让人们感受到了独特的数字自由。突然,带着一台 12 磅重的外星人“笔记本电脑”感觉不太像未来...

便携性很好,但这是一种权衡

那么 Chromebooks 不擅长什么呢?基本上任何比加载你的 Twitter feed 需要更多能量的东西。Chromebooks 是第一台挑战摩尔定律(计算能力每两年翻一番的概念)的计算机。大多数 Chromebooks(也有例外)的内存约为 2GB,比目前大多数智能手机的内存少 2GB 左右。这禁止他们做任何超过加载一个基本网页的事情——12 个 Chrome 标签之后,你几乎不能做任何事情。在移动性和性能之间有一个真正的权衡,在现实世界中,这很重要。

一种新的模式出现了

如果你能拥有不可思议的便携性和无限的能力,那会怎么样?这是 20 世纪 50 年代大型机/瘦客户机模型的前提,也是我们称之为 Paperspace 的新技术的前提。瘦客户机模式失败的原因有很多,最明显的是因为这个叫做互联网的小东西不存在。如果除了直接插入超级计算机之外,你不能在任何地方使用终端,那么它对超级计算机有什么好处呢?Chromebooks 和 Paperspace 都受益于几乎无处不在的高速互联网的普及。除了 Paperspace,在可移植性和性能之间没有权衡,因为所有繁重的工作都在云中远程完成。你的 Chromebook 成为了一个轻量级的入口,可以根据你的需求无限扩展。

你可以做一些很酷的事情

下面是我在 Chromebook 上运行 Revit 的视频:
https://s3.amazonaws.com/ps.public.resources/video/Revit-video.mp4

这是一个被大多数建筑师和工程师用来设计和建模复杂建筑的程序。正如你所想象的,它需要大量的电力来运行(例如,他们推荐 16 GB RAM),这意味着比 Chromebook 多了很多的电力。在 Paperspace 的支持下,这款 Chromebook 被“改造”成了一台 64GB 内存的机器。它有 1gb 的互联网,只需点击一个按钮,它的存储就可以随时升级。查看您可以使用 Paperspace 做的其他一些很酷的事情:

要开始在 Chromebook 上运行 Windows:

SIGN UP HERE

在 Windows 上运行 TensorFlow

原文:https://blog.paperspace.com/running-tensorflow-on-windows-with-paperspace/

以前,可以通过使用 Docker 容器在 Windows 环境中运行 TensorFlow 。这种方法有很多缺点,其中最重要的是缺乏 GPU 支持。由于 GPU 通常比 CPU 的性能提升超过10 倍,难怪人们对在完全 GPU 支持下本地运行 TensorFlow 感兴趣。截至 2016 年 12 月,这已经成为可能。最棒的是,设置只需 5 分钟:

先决条件:

GPU+机器

TensorFlow 依赖于 NVIDIA 开发的一项名为 CUDA 的技术。GPU+机器包括一个支持 CUDA 的 GPU,非常适合 TensorFlow 和一般的机器学习。不使用 GPU(使用 CPU)也可以运行 TensorFlow,但是您将在下面看到使用 GPU 的性能优势。

库达

下载链接
推荐版本: Cuda 工具包 8.0

安装将提供安装 NVIDIA 驱动程序。这已经安装,所以取消选中此框跳过这一步。

需要重新启动才能完成安装。

cuDNN

下载链接
推荐版本: cuDNN v5.1

在 Windows 上,cuDNN 以 zip 存档的形式分发。提取它并添加 Windows 路径。我将它提取到C:\tools\cuda\bin并运行:

set PATH=%PATH%;C:\tools\cuda\bin 

计算机编程语言

下载链接

如果您还没有安装 Python,Anaconda 的 Python 3.5 很容易安装。这是一个相当大的安装,所以需要几分钟。TensorFlow 目前需要 Python 2.7、3.4 或 3.5。

安装 TensorFlow

首先,我们将为我们的项目创建一个虚拟环境:

conda create --name tensorflow-gpu python=3.5 

然后激活或切换到这个虚拟环境:

activate tensorflow-gpu 

最后,安装带 GPU 支持的 TensorFlow:

pip install tensorflow-gpu 

测试 TensorFlow 安装

python
...
>>> import tensorflow as tf
>>> hello = tf.constant('Hello, TensorFlow!')
>>> sess = tf.Session()
>>> print(sess.run(hello))
Hello, TensorFlow!
>>> a = tf.constant(10)
>>> b = tf.constant(32)
>>> print(sess.run(a + b))
42
>>> 

安装完成后,我们就可以运行我们的第一个模型了。


让我们运行一个模型!

运行 TensorFlow 演示模型

现在是有趣的部分。TensorFlow 附带了一些演示模型。我们将导航到它们所在的目录,并运行一个简单的模型来对来自 MNIST 数据集的手写数字进行分类:

cd C:\Users\Paperspace\Anaconda3\envs\tensorflow-gpu\Lib\site-packages\tensorflow\models\image\mnist
python convolutional.py 

如果一切都配置正确,您应该在您的窗口中看到类似的内容:

您可以看到每一行大约需要 11-12 毫秒来运行。真是令人印象深刻。为了了解 GPU 带来的巨大差异,我将停用它并运行相同的模型。

conda create --name tensorflow python=3.5
activate tensorflow
pip install tensorflow 


如您所见,每一行大约需要 190 毫秒。利用 GPU 可以实现 17 倍的性能提升!

值得一提的是,我们是在强大的 8 核英特尔至强处理器上运行的 GPU 加速通常会超过这些结果。


总结:

监控 GPU 利用率

最后,我有两种方法可以监控我的 GPU 使用情况:

英伟达-SMI

NVIDIA-SMI 是一个内置在 NVIDIA 驱动程序中的工具,可以直接在命令提示符下显示 GPU 的使用情况。导航到它的位置并运行它。

cd C:\Program Files\NVIDIA Corporation\NVSMI
nvidia-smi.exe 

GPU-Z

TechPowerUp 开发了一个非常流行的 GPU 监控工具,叫做 GPU-Z,使用起来更加友好。在这里下载

NVIDIA-SMI 和 GPU-Z 并行运行

就是这样。让我们知道你的想法!

在 Mac 上运行 Windows 的更好方式

原文:https://blog.paperspace.com/running-windows-on-a-mac/

在 Mac 上运行 Windows 已经有一段时间了,但现有的虚拟化解决方案有许多缺点。以下是一些例子:

资源有限

使用 ParallelsFusionVirtualBox 意味着你同时运行两个操作系统,所以你需要一个相当强大的机器,以便从你的虚拟机获得像样的性能。换句话说,如果你愿意使用 Mac 一半的资源,这些解决方案是很棒的。[1]

其次,大多数虚拟化软件会限制您可以分配给 Windows 操作系统的资源数量。借助 Paperspace,您可以运行高达 192 GB RAM、32 个 vCPUs、无限存储和 4 GB GPU。最后,您可以在 Mac 上运行强大的 Windows 专用应用程序,如 Revit、SolidWorks 和 3D Studio Max。

成本

接下来是您的 Windows 许可证和虚拟化软件本身的成本问题。Paperspace 允许您按月甚至按小时支付 Windows 许可证和您需要的任何硬件资源。这更灵活,也更实惠。

任何设备

Paperspace 的一大优势是,您的桌面可以在任何设备上使用,并且您不会被某个物理硬件所束缚。本地虚拟化并非如此,您需要找到第三方解决方案来保持文件同步,并且当您使用新设备时,您将无法访问 Windows 应用程序。

复杂度

最后,在本地虚拟化 Windows 是一件令人头疼的事情。您需要研究、购买并安装虚拟化软件的副本,找到 Windows 的 DVD 或 ISO 副本,购买 Windows 产品密钥,并完成操作系统安装过程。有了 Paperspace,你不到三分钟就能开始工作。我们负责 Windows 许可,不需要安装任何东西,您需要的只是一个 web 浏览器。


  1. Boot Camp 没有这种限制,但代价是你缺乏在 Mac 和 Windows 之间切换的灵活性,因为它们不会同时运行。 ↩︎

比较:SageMaker Studio 笔记本和 Paperspace 渐变笔记本

原文:https://blog.paperspace.com/sagemaker-studio-notebooks-alternative-comparison/

亚马逊 SageMaker 是一个基于云的机器学习平台,与谷歌的 AI 平台和微软的 Azure 机器学习工作室竞争。

The AWS SageMaker Studio console

作为 SageMaker Studio 的一部分,AWS 提供了可以在 Jupyter 或 JupyterLab 中启动的笔记本,而无需公共资源。

一个区别是 SageMaker Studio 笔记本不同于常规的 SageMaker 笔记本实例。文档中描述的差异是 SageMaker Studio 笔记本的启动速度快 5-10 倍,可以通过 link 共享,并很好地集成到 SageMaker studio 的其他部分,在那里它们可以利用共享存储和其他资源。

让我们看看 SageMaker Studio 笔记本的优缺点,然后看看它们与 Paperspace 渐变笔记本的对比。

TL;博士;医生

SageMaker Studio 笔记本是对旧 SageMaker 笔记本的重大改进。Studio 简化了访问控制和共享(允许您授权 SSO 访问笔记本资源),并提供了一种通过 SageMaker Studio 为用户创建共享数据资源的方法。

也就是说,来自 Paperspace 的 Gradient 笔记本电脑提供了几乎相同的生产级(就笔记本电脑可以说是生产级)可靠性,但在协作、实例选择、计量定价简单性等方面更易于使用。

SageMaker Studio 笔记本电脑最适合那些已经购买了 SageMaker 并且对笔记本电脑供应和访问有公司要求的用户。渐变笔记本更适合希望完成快速探索和协作并尽快启动和运行的团队。

pagemaker studio 笔记本简介

亚马逊在 2017 年推出了 SageMaker,为需要完全托管环境来完成机器学习任务的机器学习工程师提供一站式商店。

SageMaker Studio Notebook Launcher

SageMaker Studio 是 SageMaker 的一部分,专注于构建和训练 ML 模型。正如文档所描述的,SageMaker Studio 用于在 Jupyter 笔记本上建立和训练模型,部署和建模它们的预测,然后跟踪和调试 ML 实验。

AWS 对 SageMaker Studio 笔记本电脑进行了如下描述:

下一代 SageMaker 笔记本电脑包括 AWS 单点登录(AWS SSO)集成、快速启动时间和单击共享。

SageMaker 可惜没有宣传的那么简单。第一次开始非常困难——无论是在提供资源还是添加合作者方面。

我们建议,如果您确实在探索 SageMaker Studio 笔记本,请确保您的 AWS 权限足够大,可以创建您不可避免地需要的所有资源!

特征比较

让我们来看看这两家笔记本电脑提供商的一些共同特点。对于新用户来说,最引人注目的比较点是 SageMaker 只需要一张信用卡和一个 AWS 实例就可以启动一个笔记本。

与此同时,Gradient 将允许您使用自由层 CPU 和 GPU 实例,无需信用卡即可开始使用。

由于可以选择拥有公共(只读)笔记本以及可能被分叉的笔记本,因此在 Gradient 上与其他用户共享笔记本也更加简单。

aws pagemaker studio 笔记本 图纸空间渐变笔记本
成本 为实例付费 免费的 CPU 和 GPU 笔记本电脑
资源 任何 AWS 实例 任何图纸空间实例
从零要求开始 信用卡,GPU 批准 免费的 CPU 和 GPU,无需信用卡或批准
启动时间 计算需要几分钟来初始化 计算需要几秒钟来初始化
自动关机
Jupyter 笔记本选项
JupyterLab Option
从容器构建

IDE vs JupyterLab

需要注意的一点是,AWS SageMaker Studio 笔记本和 Gradient 笔记本都有两种笔记本查看选项:笔记本的自定义 IDE 版本以及传统的 JupyterLab 笔记本选项。

这里我们有 SageMaker 选项:

[Left] SageMaker Studio Notebook custom IDE, [Right] Same notebook but viewed in the traditional JupyterLab IDE

这里我们有渐变选项:

[Left] Gradient Notebooks custom IDE, [Right] Same notebook but viewed in the traditional JupyterLab IDE

这是值得注意的,因为许多基于云的笔记本电脑不提供这两个选项!例如——Google Colab 和 Kaggle 内核因不允许完全访问 JupyterLab 而臭名昭著。

这两款产品都将在您需要时实现完整的 JupyterLab 体验,这太棒了——您永远不知道何时会需要访问 JupyterLab 的一些不常用功能!

成本比较

AWS 计量计费是出了名的复杂。SageMaker Studio 笔记本没有什么不同!

同时,在选择可用实例(包括自由实例)时,Gradient 提供了更多的易用性,这在任何 Gradient 笔记本的侧面板实例选择器中都是可能的。

此外,Gradient 还通过简单的自动关闭计时器、当(未使用的)实例运行一段时间后的电子邮件通知以及控制台中提供的简单计量账单发票来帮助您管理成本。

Gradient offers a large number of instances in the instance selector accessible from any notebook

让我们来看一些实例定价:

实例类型 图纸空间渐变笔记本 实例类型 aws pagemaker studio 笔记本
免费(M4000) 每小时 0.00 美元 ml . p 3.2x 大 每小时 3.82 美元
免费(P5000) 每小时 0.00 美元 ml.p3.8xlarge 14.688 美元/小时
P4000* 每小时 0.51 美元 ml . p 3.16 x 大号 28.152 美元/小时
P5000* 每小时 0.78 美元 ml.g4dn.xlarge 0.7364 美元/小时
P6000* 每小时 1.10 美元 ml.g4dn.2xlarge 1.0528 美元/小时
V100* 每小时 2.30 美元 ml.g4dn.4xlarge 1.6856 美元/小时
P5000 x4* 每小时 3.12 美元 ml.g4dn.8xlarge 每小时 3.0464 美元
P6000 x4* 每小时 4.40 美元 ml.g4dn.12xlarge 5.4768 美元/小时
- - ml.g4dn.16xlarge 6.0928 美元/小时

Paperspace 的付费实例需要订阅计划,而 SageMaker 机器学习笔记本不需要订阅。梯度订购层级如下:

| 梯度订阅类型 | 费用 | 细节 |
| --- | --- | --- |
| 自由的 | 0 美元/月 | -仅免费实例
-笔记本是公共的
-限制 1 台并发笔记本
-每次会话最多限制 12 小时

  • 5GB 永久存储 |
    | G1(个人) | 8 美元/月 | -免费和付费实例
    -私人笔记本
    -限制 5 个并发笔记本
    -无限会话长度
  • 200GB 永久存储 |
    | G2(个人) | 24 美元/月 | -免费和付费实例
    -私人笔记本
    -限制 10 个并发笔记本
    -无限会话长度
  • 1TB 永久存储 |
    | T1(团队) | 12 美元/用户/月 | -免费和付费实例
    -私有笔记本
    -限制 10 个并发笔记本
    -无限会话长度
  • 500GB 持久存储
    -私有团队协作
    -私有托管集群 |
    | T2(团队) | 49 美元/用户/月 | -免费和付费实例
    -私有笔记本
    -限制 50 个并发笔记本
    -无限会话长度
  • 1TB 持久存储
    -私有团队协作
    -私有托管集群 |

虽然 Gradient 需要订阅层(和信用卡)来启用付费实例,但您始终可以免费使用免费层 CPU 和 GPU 笔记本电脑。

与此同时,AWS 只需要信用卡授权来探索产品!

入门指南

使用 AWS SageMaker Studio 设置 Jupyter 笔记本

  • 如果需要,创建一个 AWS 帐户
  • 输入个人信息并同意条款
  • 通过输入信用卡信息激活帐户
  • 导航到 AWS 控制台
  • 选择 pagemaker 产品
  • 从 SageMaker Studio 控制面板创建一个新的 SageMaker Studio 工作区
  • 加载 SageMaker Studio 后,从 SageMaker Studio 小程序中启动一个新的笔记本

在图纸空间渐变中设置 Jupyter 笔记本

若要开始使用渐变笔记本:

  • 创建一个 Paperspace 帐户(链接)
  • 导航到渐变>笔记本,并选择创建笔记本
  • 输入笔记本的名称、运行时(可选),并选择一个实例
  • 如果你选择了一个自由的 CPU 或自由的 GPU 实例,选择开始笔记本,就是这样!(付费实例需要信用卡。)
  • 注意:Paperspace 提供对自由层 CPU 和 GPU 支持的笔记本电脑的无限制使用

启动时间

任何云提供商都需要一些时间来启动 CPU 或 GPU 实例。AWS SageMaker Studio 需要很长时间进行配置,可能需要 5 分钟左右,而 Paperspace 大约需要 30 秒。请注意,相对于 SageMaker 上提供的普通笔记本实例,AWS 最近减少了 SageMaker Studio 笔记本的启动时间。

支持

众所周知,很难获得 AWS 的支持。与此同时,Paperspace 拥有一支出色的支持团队,提供基于票证的支持,平均响应时间为几个小时。

结论

您或您的公司很可能已经在使用 AWS 生态系统中的资源。不幸的是,这并没有让在 AWS SageMaker 上开始使用笔记本变得更加容易。

您仍然需要为您和您的队友创建一个 SageMaker Studio 工作区。您需要通过 IAM 提供实例以供您的笔记本使用,并且您需要为您希望能够访问您正在创建的笔记本资源的任何人创建 IAM 角色。

也就是说,一旦你在 AWS SageMaker Studio 中设置了笔记本,这肯定是对 AWS 旧笔记本功能的改进,它主要只是 JupyterLab 的托管版本。现在,您可以从 AWS(如 S3)连接其他资源,这很有用,尽管在我们的测试中,我们无法正确集成。

如果您重视易用性而不是企业级访问控制和资源管理,那么 Paperspace Gradient 是一个不错的选择。Gradient 将帮助您和您的团队在各种资源上设置和运行——比 AWS 快得多,但代价是配置选项更少。

这是在一般情况下使用 AWS 产品时的一个常见问题——因此,请确保了解风险和好处,并做出最适合您团队当前需求的选择!

向生产就绪的 Graphcore IPUs 问好

原文:https://blog.paperspace.com/say-hello-to-graphcore-ipus-in-production/

当我们第一次引入对 Graphcore IPUs 的支持时,目标是为 ML 工程师提供一个免费使用的沙盒环境,以测试这款突破性的新芯片。对这项服务的反应非常热烈。今天,成千上万的工作负载已经运行,为从 NLP 到 GNNs 的各种项目提供动力。

Gradient 面向处于 ML 开发周期任何阶段的机器学习开发者。该免费计划旨在测试最先进的模型和原型新想法。当到了超越概念阶段的时候,Gradient 提供了对更大的机器类型、增加的并发性、协作和其他工具的访问,使得生产模型成为可能。在成功发布免费 IPUs 的基础上,下一步显然是让用户在构建和扩展生产应用程序时可以利用它们。

从今天开始,我们很高兴能够分享 Graphcore IPUs,它现在不受自由层的限制。您可以访问全系列的 IPU 系统,包括提供 5.6 千万亿次人工智能计算的 Bow Pod16。最受欢迎的功能之一是在团队工作区访问 IPUs,现在已经公开提供,因此任何组织都可以在他们支持 IPU 的模型上进行协作。

要了解关于 IPUs 的更多信息,包括关于样本模型、Poplar SDK 或系统规范的信息,请访问 Graphcore 网页。您可以通过创建或访问您的帐户开始:

Get Started

此外,为了快速无缝地加入 IPU,我们向合格的组织提供工程支持。例如,我们的团队可以帮助您将模型从 CPU 或 GPU 迁移到 IPUs。联系我们的解决方案团队了解更多信息。

凭借海量数据集和尖端的自然语言处理技术,SciSpace 的一组 ML 研究人员正在为科学家和出版商构建下一代工具

原文:https://blog.paperspace.com/scispace-nlp-tools-for-researchers-and-publishers/

SciSpace(原名 Typeset)是一家软件公司,致力于让科学研究更具协作性和可访问性。

除了为研究人员提供有价值的工具来追踪科学文章的谱系之外,SciSpace 还是一个端到端的集成研究平台,具有现代化的工作流程,可以帮助学者毫不费力地撰写和发表研究。

我们很高兴有机会与 SciSpace 的高级研究科学家 Rohan Tondulkar 坐在一起,深入研究他的团队正在进行的一些机器学习工作。

我们开始吧!

paper space:SciSpace 是研究人员和出版商的平台。为了帮助我们的读者更好地理解产品,你能告诉我们一些你的用户的常见用例吗?

:SciSpace 背后的想法是将研究生态系统中的每一个利益相关者——读者、研究人员、教授、出版商和大学——聚集在一个屋檐下,并为他们提供一套互联的应用程序,使科学知识和数据无缝地流动。

科学爱好者和研究人员依靠我们的 Discover 模块来搜索、访问和阅读科学手稿。我们的平台上有超过 2.7 亿篇来自不同领域的研究论文。结合强大的搜索过滤器和个性化推荐,我们使文献搜索变得更加简单快捷。

除此之外,学生和研究人员使用我们的 Write 解决方案来创建、编辑、校对、排版和格式化他们的手稿。我们提供 30,000 多个期刊模板,使研究人员能够在几秒钟内根据最新的期刊指南格式化他们的手稿。

出版商使用我们内置的 OJS 模块和自动化的 XML 优先制作工作流程来简化制作流程并提高其文章的可见性。

*

SciSpace (formerly Typeset) is building ML-assisted tools for researchers and publishers.*

Paperspace :使用科学文献的一个令人畏惧的事情是,至少可以说,语料库是……庞大的。我们真正谈论的是什么样的规模?您每天都要处理什么样的数据集?

Tondulkar :我们每天都要处理海量数据集。我们允许用户搜索 2.7 亿篇研究文章以及每篇文章的元数据,如参考文献、引文等。总数达到数十亿个数据点。我们有近 5000 万个开放存取 pdf,每个任务都必须处理它们。我们开发的每一个功能都必须迎合这个庞大的数据。

Paperspace :你的职位是高级研究科学家——能告诉我们更多关于你的工作吗?你的团队正在应对什么样的挑战,这与公司更广泛的使命有什么关系?

:我们有一个由 Tirthankar Ghosal 博士领导的六人小型 ML 研究团队,他是来自 IIT 巴特那的博士,也是学术文档处理方面的专家。该团队旨在探索和部署各种 NLP 功能,以提高可发现性并使科学文章更容易消化。然后,研究团体能够从这些论文中获得更多价值,并更快地将这些点联系起来。

作为一名高级研究科学家,我有一个更广泛的角色,涵盖端到端的 ML 堆栈。我的职责包括研究和试验各种问题陈述,协调团队内部的工作,建立部署管道,解决可伸缩性问题,检查模型输出的质量,并为我们的模型提供服务。

我们的团队致力于许多独特而有趣的问题,包括生成研究文章的提取和抽象摘要。我们还建立语义搜索工具、推荐系统,并进行大规模引用分析。还有更多正在筹备中!

**

Articles on SciSpace reference related works, citations, and more.**

Paperspace :作为一家 GPU 云提供商,我们注意到人们对 NLP 用例及应用有着浓厚的兴趣。关于使用加速 GPU 计算来训练 NLP 模型,您能告诉我们些什么?

Tondulkar :自从用于机器学习用例的 GPU 问世以来,我们也看到了 NLP 领域创新的激增。近年来,随着 GPU 在培训和部署过程中的易用性,这一数字才开始增加。各种 GPU 提供商,如 Paperspace、AWS 和 Google Cloud 等,在帮助研究人员以超快的速度训练和部署他们的模型方面发挥了重要作用。

在开发更强大的 GPU 的同时,NLP 模型(例如 GPT-3 )的大小和性能也在增长。低成本的 GPU 使得大公司和小公司比以往任何时候都更容易使用和开发高性能的模型。这已经帮助各种基于人工智能/自然语言处理的初创公司成长起来,并且有着巨大的未来潜力,因为我们看到越来越多的用例以及问题陈述被 ML 解决。

**

“我们的团队需要对各种问题陈述进行实验。这涉及到使用 PyTorch、TensorFlow、拥抱脸等训练大型深度学习模型。数百万个数据点。因此,我们需要一个可以扩展到多个用户和项目的平台。我们探索了许多最适合我们进行实验的选项——paper space、Google Colab、AWS Sagemaker 和 Vast.ai。

我们发现,对于我们这些研究人员来说,Paperspace 是最方便、最具成本效益的选择。Paperspace 通过允许选择 GPU/CPU 机器以及按需使用 GPU 来提供更多控制,这有助于显著优化成本。"

Rohan Tondulkar,SciSpace 高级研究科学家**

纸空间 :纸空间是如何进入画面的?您的团队开始使用 Paperspace 机器有什么特别的原因吗?

Tondulkar :我们团队需要对各种问题语句进行实验。这涉及到使用 PyTorch、TensorFlow、拥抱脸等训练大型深度学习模型。数百万个数据点。因此,我们需要一个可以扩展到多个用户和项目的平台。我们探索了许多最适合我们进行实验的选项——paper space、Google Colab、AWS Sagemaker 和 Vast.ai。

我们发现,对于我们这些研究人员来说,Paperspace 是最方便、最具成本效益的选择。Paperspace 通过允许选择 GPU/CPU 机器以及按需使用 GPU 来提供更多控制,这有助于显著优化成本。

其他功能,如创建数据集和与 Git 集成,对我们也有好处。我们发现 Paperspace 中的环境稳定且易于设置。多亏了 Paperspace,我们可以可靠而系统地进行这些实验。

Paperspace :在构建生产规模的 ML 应用程序时,你的团队遇到了哪些意想不到或意料之中的挑战?

:我们最大的挑战之一是获得良好的数据集来训练我们的模型。由于我们在一个利基空间中运作,很少有合适的数据集是公开可用的。此外,我们的模型必须在来自不同领域的研究文章上表现同样出色,这些领域包括人工智能、数学、物理、化学、医学等,这是一项非常具有挑战性的任务。模型经过严格的质量检查,以确保这一点。

从大型 ML 模型生成输出可能有点耗时。快速且经济高效地生成数百万篇研究文章的产出是一个巨大的挑战。

Paperspace :如果我们的读者想看看 SciSpace 并参与到社区中来,你会建议他们如何开始?

Tondulkar :随意访问我们的网站, typeset.io (我们的前身是 typeset.io)。在那里,您可以了解我们的所有产品。如果你想阅读一些关于你最喜欢的研究主题的最新研究,你可以从主题页面开始。到目前为止,我们在这个平台上有大约 5000 万个全文 pdf。

您可以在 Twitter 上关注我们,地址为 @Scispace_ 。我们定期分享趋势研究论文的精选列表、开创性文章的摘要、研究写作和发布最佳实践,并积极与我们的研究人员和教授社区互动。您也可以查看我们的 LinkedIn 页面,了解更多关于我们的工作文化、未来计划和其他计划。

**

The Trace feature surfaces valuable information about scholarly articles.**

Paperspace :还有什么你想大声喊出来的吗?

:一定要看看我们的痕迹功能。Trace 是我们的纸面文档发现引擎的新增功能,在业界是直观和独特的。你将能够深入钻研一个话题,而不会忘记你的进展。点击这里阅读最近一篇关于伯特的文章!

要了解更多关于 SciSpace 的信息,请务必查看 https://typeset.io/.

SCNet (CVPR 2020)

原文:https://blog.paperspace.com/scnet-cvpr-2020/

虽然近年来计算机视觉中的注意力方法受到了相当多的关注,但它们被严格限制在这些方法集成到普通卷积神经网络(CNN)的方式上。大多数情况下,这些方法被用作插件模块,可以插入不同类型的 CNN 的结构中。然而,在这篇博客文章中,我们将看看 2020 的一篇论文,题为用自校准卷积改进卷积网络,作者刘。et。艾尔。提出了一种新形式的卷积运算,称为自校准卷积(SC-Conv ),它具有与注意机制非常相似的自校准特性。

首先,我们将看看这篇论文背后的动机,然后与 GhostNet (CVPR 2020)进行一些微妙的比较。然后,我们将对 SC-Conv 的结构进行研究,并提供 PyTorch 代码,然后以结果和一些批评意见结束帖子。

目录

  1. 动机
  2. 自校准卷积
  3. 密码
  4. 结果
  5. 结论
  6. 参考

摘要

细胞神经网络的最新进展主要致力于设计更复杂的结构来增强其表征学习能力。在本文中,我们考虑在不调整模型结构的情况下,改进细胞神经网络的基本卷积特征变换过程。为此,我们提出了一种新颖的自校准卷积,通过内部通信显式扩展每个卷积层的视野,从而丰富输出特征。特别是,与使用小核(例如,3 × 3)融合空间和通道信息的标准卷积不同,我们的自校准卷积通过一种新颖的自校准操作,自适应地围绕每个空间位置建立长程空间和通道间相关性。因此,它可以通过明确地结合更丰富的信息来帮助 CNN 生成更具区分性的表示。我们的自校准卷积设计简单而通用,可以很容易地应用于增强标准卷积层,而不会引入额外的参数和复杂性。大量实验表明,当将我们的自校准卷积应用于不同的主干时,基线模型可以在各种视觉任务中得到显著改善,包括图像识别、对象检测、实例分割和关键点检测,而无需改变网络架构。我们希望这项工作可以为未来的研究提供一个有前途的方法来设计新的卷积特征变换,以改善卷积网络。

动机

虽然大多数研究方向都是人工设计架构或组件,如注意机制或非局部块,以丰富经典卷积神经网络的特征表示,但这是次优的,并且非常迭代。

在本文中,我们没有设计复杂的网络架构来加强特征表示,而是引入自校准卷积作为一种有效的方法,通过增加每层的基本卷积变换来帮助卷积网络学习区分表示。类似于分组卷积,它将特定层的卷积滤波器分成多个部分,但是不均匀地,每个部分内的滤波器以不同的方式被利用。具体来说,自校准卷积首先通过下采样将输入转换为低维嵌入,而不是在原始空间中均匀地对输入执行所有卷积。由一个滤波器部分变换的低维嵌入被用来校准另一部分内的滤波器的卷积变换。受益于这种异构卷积和滤波器间通信,每个空间位置的感受野可以被有效地扩大。

作为标准卷积的增强版本,我们的自校准卷积有两个优点。首先,它使每个空间位置能够自适应地编码来自长距离区域的信息上下文,打破了在小区域(例如,3× 3)内卷积运算的传统。这使得由我们的自校准卷积产生的特征表示更具鉴别性。第二,所提出的自校准卷积是通用的,并且可以容易地应用于标准卷积层,而不引入任何参数和复杂度开销或者改变超参数。

自校准卷积

上面的示意图展示了自校准回旋(SC-Conv)的结构设计。乍一看,该结构似乎是鬼卷积(CVPR 2020)和常规注意机制的交叉。在深入了解 SCConv 之前,让我们快速浏览一下 GhostNet。

幽灵网(CVPR 2020)

GhostNet 在 CVPR 2020 上发表,通过用 ghost 层取代标准卷积层,提供了一种降低大型卷积神经网络的参数和计算复杂性的简单方法。对于给定的输入$X \in \mathbb{C \ast H \ast W}\(和预期的输出\) \ tilde \ in \mathbb{\hat \ ast h \ ast w } \(,ghost layer 首先使用标准卷积生成\)\frac{\hat}{2}\(通道。然后,剩余的\)\frac{\hat}{2}$通道通过使第一组通道通过深度方向卷积核来生成,该卷积核基本上将参数复杂度降低了近一半。为了更深入地了解 GhostNet,请阅读我在同一个这里的帖子。

好的,现在回到 SC Conv,在 SC conv 有两个不同的分支,一个是身份分支,类似于固有残差分支,而另一个分支负责自校准。给定一个输入张量$X \in \mathbb^{C \ast H \ast W}\(和一个相同形状的期望输出张量\)\hat\(,该张量首先被分成每个\)\frac{2}$通道的两个集合,第一个分支简单地对其中一个集合应用 2D 卷积运算。在第二个分支中,集合通过三个并行操作。第一个操作是简单的信道保持 2D 卷积,而在第二个操作中,空间维度以因子$r$下采样,然后对其应用 2D 卷积,其输出随后以因子$r$上采样以保持相同的形状。最后一个操作是一个单位函数,它按元素添加到来自第二个操作的上采样特征映射中。合成张量通过 sigmoid 激活,然后与从第一个张量获得的张量按元素相乘。最后,这个张量通过另一个保持 2D 卷积的通道。然后,将来自两个分支的两个张量连接起来,形成完整的集合。

密码

import torch
import torch.nn as nn
import torch.nn.functional as F

class SCConv(nn.Module):
    def __init__(self, inplanes, planes, stride, padding, dilation, groups, pooling_r, norm_layer):
        super(SCConv, self).__init__()
        self.k2 = nn.Sequential(
                    nn.AvgPool2d(kernel_size=pooling_r, stride=pooling_r), 
                    nn.Conv2d(inplanes, planes, kernel_size=3, stride=1,
                                padding=padding, dilation=dilation,
                                groups=groups, bias=False),
                    norm_layer(planes),
                    )
        self.k3 = nn.Sequential(
                    nn.Conv2d(inplanes, planes, kernel_size=3, stride=1,
                                padding=padding, dilation=dilation,
                                groups=groups, bias=False),
                    norm_layer(planes),
                    )
        self.k4 = nn.Sequential(
                    nn.Conv2d(inplanes, planes, kernel_size=3, stride=stride,
                                padding=padding, dilation=dilation,
                                groups=groups, bias=False),
                    norm_layer(planes),
                    )

    def forward(self, x):
        identity = x

        out = torch.sigmoid(torch.add(identity, F.interpolate(self.k2(x), identity.size()[2:]))) # sigmoid(identity + k2)
        out = torch.mul(self.k3(x), out) # k3 * sigmoid(identity + k2)
        out = self.k4(out) # k4

        return out

您需要下载 ImageNet 数据集来训练 SCNet。你可以按照这里的说明下载。下载后,您可以在上面 paperspace gradient 中链接的 jupyter 环境中使用以下命令训练 ImageNet。

usage: train_imagenet.py  [-h] [-j N] [--epochs N] [--start-epoch N] [-b N]
                          [--lr LR] [--momentum M] [--weight-decay W] [--print-freq N]
                          [--resume PATH] [-e] [--pretrained] [--world-size WORLD_SIZE]
                          [--rank RANK] [--dist-url DIST_URL]
                          [--dist-backend DIST_BACKEND] [--seed SEED] [--gpu GPU]
                          [--multiprocessing-distributed]
                          DIR

PyTorch ImageNet Training

positional arguments:
  DIR                   path to dataset

optional arguments:
  -h, --help            show this help message and exit
  -j N, --workers N     number of data loading workers (default: 4)
  --epochs N            number of total epochs to run
  --start-epoch N       manual epoch number (useful on restarts)
  -b N, --batch-size N  mini-batch size (default: 256), this is the total
                        batch size of all GPUs on the current node when using
                        Data Parallel or Distributed Data Parallel
  --lr LR, --learning-rate LR
                        initial learning rate
  --momentum M          momentum
  --weight-decay W, --wd W
                        weight decay (default: 1e-4)
  --print-freq N, -p N  print frequency (default: 10)
  --resume PATH         path to latest checkpoint (default: none)
  -e, --evaluate        evaluate model on validation set
  --pretrained          use pre-trained model
  --world-size WORLD_SIZE
                        number of nodes for distributed training
  --rank RANK           node rank for distributed training
  --dist-url DIST_URL   url used to set up distributed training
  --dist-backend DIST_BACKEND
                        distributed backend
  --seed SEED           seed for initializing training.
  --gpu GPU             GPU id to use.
  --multiprocessing-distributed
                        Use multi-processing distributed training to launch N
                        processes per node, which has N GPUs. This is the
                        fastest way to use PyTorch for either single node or
                        multi node data parallel training

注意:您需要一个权重&偏差账户来启用 WandB 仪表板日志。

结果

以下是论文中展示的一些 SCNets 结果:

结论

SCNet 提供了一种在卷积神经网络中嵌入注意机制的新方法。与作为附加模块应用的传统注意机制不同,SCConv 可以用来取代传统的卷积层。尽管这种方法在参数/ FLOPs 方面很便宜,并提供了很好的性能提升,但唯一的问题是操作数量的增加导致了运行时间的增加。

参考

  1. 用自校准卷积改进卷积网络
  2. SCConv 官方 GitHub 资源库

并行空间和信道挤压与激发(scSE)网络

原文:https://blog.paperspace.com/scse-nets/

从一开始,挤压和激励网络就是计算机视觉中应用注意力机制的重要方法。然而,它们确实有某些缺点,这些缺点已经被其他研究人员以某种形式不断地解决了。在这篇文章中,我们将谈论 Roy 等人接受的一篇这样的论文。艾尔。题目为“ 全卷积网络中并发空间和信道压缩&激励 ”。该论文围绕将空间注意力分支引入挤压和激发模块,该模块类似于卷积块注意力模块(CBAM),然而,在聚集通道和空间注意力的方式上略有不同。

事不宜迟,让我们深入研究 scSE 的动机,然后分析该模块的结构设计,最后通过研究观察到的结果及其 PyTorch 代码进行总结。

目录

  1. 动机
  2. scSE 模块
  3. 密码
  4. 结果
  5. 结论
  6. 参考

摘要

全卷积神经网络(F-CNNs)已经为大量应用建立了图像分割的最新技术。F-CNN 的架构创新主要集中在改善空间编码或网络连接以帮助梯度流。在本文中,我们探索了另一个自适应地重新校准特征图的方向,以增强有意义的特征,同时抑制弱的特征。我们从最近提出的挤压和激发(SE)模块中获得灵感,用于图像分类的特征图的通道重新校准。为此,我们介绍了用于图像分割的 SE 模块的三种变体,(I)空间压缩和通道方式激发(cSE),(ii)通道方式压缩和空间激发(sSE)以及(iii)并行空间和通道压缩和激发(scSE)。我们将这些 se 模块有效地整合到三种不同的先进 F-CNN(dense Net、SD-Net、U-Net)中,并观察到所有架构的性能持续改善,同时对模型复杂性的影响最小。对两个具有挑战性的应用进行评估:MRI 扫描上的全脑分割和全身对比增强 CT 扫描上的器官分割。

动机

挤压和激励模块已经在计算机视觉领域发挥了作用。一种配备挤压和激励模块的深度卷积神经网络架构在 ImageNet 数据集上的 ILSVRC 2017 分类竞赛中获得了第一名,为进一步加强探索更多渠道注意力模块变体奠定了基础。一般来说,在关注场景中更重要的物体的范围内,挤压和激发模块或通道注意力背后的直觉在某种程度上继承了人类的视觉认知能力。在这篇论文中,作者受到启发,利用全卷积神经网络(F-CNNs)的压缩和激励模块的高性能进行图像分类和图像分割,主要是在医学图像分割领域。由于医学图像分割也需要对空间域的关注,作者通过包括并行空间关注块来扩展 SE 的结构设计(其被表示为 cSE ,其中 c 代表“通道”,因为 SE 是通道专用激励算子)。

作为挤压和激发通道描述符成功的必然结果,作者首先提出了 se 模块的替代版本,它沿通道“挤压”并在空间上“激发”,称为 空间 SE(sSE)。随后,作者引入了一种传统 SE 和 sSE 块的混合,称为并发空间和通道 SE 块(scSE) ,它们“沿着通道和空间分别重新校准特征图,然后组合输出”。目标是基本上最大限度地将信息传播到注意机制中,以便焦点可以同时处于像素级和通道级。虽然 scSE 块在本文中主要是针对全卷积神经网络(CNN ),如 UNets 和 DenseNets 介绍的,但是该方法可以应用/插入到传统的基于 CNN 的架构中,如残差网络。该论文还深入研究了这些基于 se 的块在医学图像分割的情况下的作用,这是提供空间每像素级关注的主要动机,因为在医学图像分割的情况下,由于数据的通常格式,每像素信息与通道级信息相比极其重要。dicom)。

scSE 模块

这里有三个组件需要分析,如上面摘自论文的图表所示:

  • 空间压缩和通道激发(cSE)
  • 通道挤压和空间激发(sSE)
  • 并行空间和信道挤压和信道激发(scSE)

空间压缩和通道激发(cSE)

本质上,这表示传统的挤压和激励模块。SE 模块由 3 个模块组成,即挤压模块、激励模块和缩放模块。让我们假设此 cSE 块的输入是\mathbb{N 中的四维特征映射张量$ \ textbf \其中$N \ast C \ast H \ast W$表示批量大小、通道数和空间维度(分别为各个特征映射/通道的高度和宽度),然后,挤压模块将$ \ textbf \减少到\mathbb{N 中的$ \ tilde { \ textbf } \ 1 这样做是为了确保与在全尺寸输入张量$\textbf\(上计算注意力权重相比,计算复杂度显著降低。此外,\)\tilde{\textbf}\(作为输入被传递到*激励模块*,该模块将这个简化的张量传递通过多层感知器(MLP)瓶颈。这将输出合成张量\)\hat{\tilde{\textbf}}\(与\)\tilde{\textbf}\(的维数相同。最后,*缩放模块*将 sigmoid 激活单元应用于该张量\)\hat{\tilde{\textbf}}\(然后该张量按元素乘以原始输入张量\)\textbf$。

为了更详细地理解挤压和激励网络的功能,请阅读我们关于 SENets 的详细文章。

通道挤压和空间激发(sSE)

cSEsSE 互补,本质上是一种与它的对应物相反的东西,它在空间维度上减少特征张量,在通道维度上激发。与之相反,sSE 挤压通道,在空间维度上激发。类似于 cSE,我们假设这个 cSE 块的输入是一个 4 维特征映射张量$ \ textbf \ in \mathbb{n \ ast c \ ast h \ ast w } \(。首先,输入张量\)\textbf$通过将$C$通道减少到 1 的 2D 逐点卷积核被减少到\mathbb{N 中的$\tilde{\textbf}。最后,该压缩张量$\tilde{\textbf}\(通过 sigmoid 激活单元,然后与原始输入张量\)\textbf$逐元素相乘。

并行空间和信道挤压和信道激发(scSE)

简单来说, scSE 是之前讨论的 cSEsSE 区块的合并。首先,类似于 cSE 和 sSE,让我们假设这个 cSE 块的输入是一个 4 维特征映射张量$ \ textbf \ in \mathbb^{n \ ast c \ ast h \ ast w } \(。这个张量\)\textbf$并行通过 cSE 和 sSE 块。然后将两个结果输出逐元素相加,以提供最终输出。然而,有一些扩展,研究人员和用户都发现,计算两个张量上的元素最大值也是一种理想的策略,而不是进行元素求和,并且与作者在论文中描述的原始 scSE 变体相比,提供了有竞争力和可比较的结果。

密码

下面的片段提供了用于 scSE、cSE 和 sSE 块的 PyTorch 代码,这些代码可以很容易地插入到计算机视觉领域中的任何传统卷积神经网络架构的块中。

*`import torch
import torch.nn as nn
import torch.nn.functional as F

class ChannelSELayer(nn.Module):
    """
    Re-implementation of Squeeze-and-Excitation (SE) block described in:
        *Hu et al., Squeeze-and-Excitation Networks, arXiv:1709.01507*
    """

    def __init__(self, num_channels, reduction_ratio=2):
        """
        :param num_channels: No of input channels
        :param reduction_ratio: By how much should the num_channels should be reduced
        """
        super(ChannelSELayer, self).__init__()
        num_channels_reduced = num_channels // reduction_ratio
        self.reduction_ratio = reduction_ratio
        self.fc1 = nn.Linear(num_channels, num_channels_reduced, bias=True)
        self.fc2 = nn.Linear(num_channels_reduced, num_channels, bias=True)
        self.relu = nn.ReLU()
        self.sigmoid = nn.Sigmoid()

    def forward(self, input_tensor):
        """
        :param input_tensor: X, shape = (batch_size, num_channels, H, W)
        :return: output tensor
        """
        batch_size, num_channels, H, W = input_tensor.size()
        # Average along each channel
        squeeze_tensor = input_tensor.view(batch_size, num_channels, -1).mean(dim=2)

        # channel excitation
        fc_out_1 = self.relu(self.fc1(squeeze_tensor))
        fc_out_2 = self.sigmoid(self.fc2(fc_out_1))

        a, b = squeeze_tensor.size()
        output_tensor = torch.mul(input_tensor, fc_out_2.view(a, b, 1, 1))
        return output_tensor

class SpatialSELayer(nn.Module):
    """
    Re-implementation of SE block -- squeezing spatially and exciting channel-wise described in:
        *Roy et al., Concurrent Spatial and Channel Squeeze & Excitation in Fully Convolutional Networks, MICCAI 2018*
    """

    def __init__(self, num_channels):
        """
        :param num_channels: No of input channels
        """
        super(SpatialSELayer, self).__init__()
        self.conv = nn.Conv2d(num_channels, 1, 1)
        self.sigmoid = nn.Sigmoid()

    def forward(self, input_tensor, weights=None):
        """
        :param weights: weights for few shot learning
        :param input_tensor: X, shape = (batch_size, num_channels, H, W)
        :return: output_tensor
        """
        # spatial squeeze
        batch_size, channel, a, b = input_tensor.size()

        if weights is not None:
            weights = torch.mean(weights, dim=0)
            weights = weights.view(1, channel, 1, 1)
            out = F.conv2d(input_tensor, weights)
        else:
            out = self.conv(input_tensor)
        squeeze_tensor = self.sigmoid(out)

        # spatial excitation
        squeeze_tensor = squeeze_tensor.view(batch_size, 1, a, b)
        output_tensor = torch.mul(input_tensor, squeeze_tensor)
        return output_tensor

class ChannelSpatialSELayer(nn.Module):
    """
    Re-implementation of concurrent spatial and channel squeeze & excitation:
        *Roy et al., Concurrent Spatial and Channel Squeeze & Excitation in Fully Convolutional Networks, MICCAI 2018, arXiv:1803.02579*
    """

    def __init__(self, num_channels, reduction_ratio=2):
        """
        :param num_channels: No of input channels
        :param reduction_ratio: By how much should the num_channels should be reduced
        """
        super(ChannelSpatialSELayer, self).__init__()
        self.cSE = ChannelSELayer(num_channels, reduction_ratio)
        self.sSE = SpatialSELayer(num_channels)

    def forward(self, input_tensor):
        """
        :param input_tensor: X, shape = (batch_size, num_channels, H, W)
        :return: output_tensor
        """
        #output_tensor = torch.max(self.cSE(input_tensor), self.sSE(input_tensor))
        output_tensor = self.cSE(input_tensor) + self.sSE(input_tensor)
        return output_tensor`*

结果

本文中提供的以下结果展示了在用于医学图像分割的 MALC 和内脏数据集上的不同 F-CNN 架构(如 U-Net、SD-Net 和 DenseNets)中使用的 scSE 块的功效。

**

结论

scSE 模块是第一个成功展示在 F-CNNs 模型中应用通道方式和每像素注意力算子的重要性和益处的方法。展示的结果是有希望的,但是,由于应用了 sSE 分支,计算量相当大。然而,这归结于用户在自己的数据集上尝试,以了解与增加的计算开销相比,该块在精度提升方面的实际效率。

参考

  1. 全卷积网络中并发的空间和信道“压缩&激励
  2. SCS 码
  3. 引导注意力和挤压激励网络(SENet),Paperspace 博客

新:更好的机密管理在纸张空间梯度中实现

原文:https://blog.paperspace.com/secrets-land-in-paperspace-gradient/

[2021 年 12 月 2 日更新:本文包含关于梯度实验的信息。实验现已被弃用,渐变工作流已经取代了它的功能。请参见工作流程文档了解更多信息。]

我们很高兴地宣布 Gradient 中新的秘密管理的到来!

Secrets are now available in Gradient Projects!

Secrets 是一种在团队、集群或项目级别本地范围密钥和凭证的方法——它们现在可用于在私有集群上运行的所有梯度实验!

秘密通常用于存储 API 密钥、S3 凭证、SSH 密钥或运行机器学习工作负载所需的任何其他安全文本。它们尤其适用于需要安全方法来认证项目或实验中使用的外部资源的协作团队。

一旦创建了一个秘密,只需使用语法secret:<name>将该秘密作为环境变量注入即可。

可以在渐变控制台中或通过渐变 CLI 创建密码。

有关 CLI 用法的更多信息,请阅读文档!

PyTorch 闪电的句子嵌入

原文:https://blog.paperspace.com/sentence-embeddings-pytorch-lightning/

在本文中,您将了解 NLP 世界中句子嵌入的相关性,并学习如何使用 PyTorch 的 lightning-flash;一个快速有效的工具,帮助您轻松构建和缩放人工智能模型。

在本文中,我们将简要介绍以下内容:

  • 两个向量之间的余弦相似性
  • 单词嵌入与句子嵌入
  • 变形金刚 API
  • PyTorch 闪电框架

两个向量之间的余弦相似性

假设你有两个向量,每个向量都有不同的方向和大小。如果这是真实世界的物理学,你可以通过取向量之间角度的余弦来计算向量之间的相似性。在计算机科学的上下文中,向量是由整数值或浮点值的数组组成的表示。为了计算这种阵列之间的相似性,我们可以使用余弦相似性度量。

The equation for cosine similarity (source)

输出是范围在 0 和 1 之间的相似性分数。下面是一个示例 python 函数,其中有两个向量xy作为输入,返回输入的余弦相似性得分作为结果。

import numpy as np

def cos_sim(x, y):
  """
  input: Two numpy arrays, x and y
  output: similarity score range between 0 and 1
  """"
	 #Taking dot product for obtaining the numerator
    numerator = np.dot(x, y)

	#Taking root of squared sum of x and y
    x_normalised = np.sqrt(np.sum(x**2))
    y_normalised = np.sqrt(np.sum(y**2))

    denominator = x_normalised * y_normalised
    cosine_similarity = numerator / denominator
    return cosine_similarity 

单词嵌入与句子嵌入

自然语言处理领域从单词嵌入的出现中获益良多。在解决自然语言处理问题时,单词嵌入的使用有助于更好地理解自然语言的语境,并有助于其在各种监督和非监督任务中的使用。

单词嵌入被定义为一个单词的固定大小的向量,使得一种语言中的每个单词都可以根据自然语言空间中的语义上下文来更好地表示。这种表示允许单词嵌入用于数学计算、训练神经网络等任务。Word2Vec 和 Glove 是两种最流行的早期单词嵌入模型。

后来,当基于 BERT 的模型随着 Huggingface API 流行起来时,对上下文理解的标准甚至上升到了更高的水平。但是这也导致了缩放形式的另一个问题。由于基于 BERT 的模型提供了更复杂的向量,计算速度发生了巨大的变化。此外,在需要理解句子意图的任务中,以向量表示形式对整个句子进行更广泛的理解被证明更有用。一个这样的任务的例子是句子相似性(STS),其目标是预测两个句子是否在语义上彼此相似。当这两个句子被输入到 BERT 提供的复杂神经网络模型中时,就会产生巨大的计算过载。据发现,一个具有 10,000 个句子对的数据集的任务将需要近 65 个小时的时间,因为它需要大约 5000 万次推理计算()。这将是扩展深度学习模型用于 STS 和其他无监督任务(如聚类)时的一个主要缺点。让我们看一个示例代码,看看 Huggingface 的基于 BERT 的单词嵌入模型如何解决 STS 任务:

from transformers import BertTokenizer, TFBertModel
model_name="prajjwal1/bert-small"
tokenizer=BertTokenizer.from_pretrained(model_name)
model = TFBertModel.from_pretrained(model_name,from_pt=True)
def encode_sentences(sentences):
  encoded = tokenizer.batch_encode_plus(
                      [sentences],
                      max_length=512,
                      pad_to_max_length=False,
                      return_tensors="tf",
                  )

  input_ids = np.array(encoded["input_ids"], dtype="int32")
  output = model(
      input_ids
  )
  sequence_output, pooled_output = output[:2]
  return pooled_output[0]

sentence1="There is a cat playing with a ball"
sentence2="Can you see a cat with a ball over the fence?"
embed1=encode_sentences(sentence1)
embed2=encode_sentences(sentence2)

cosine_similarity= cos_sim(embed1,embed2)
print("Cosine similarity Score {}".format(cosine_similarity))

句子转换器在这里提供了一个易于使用的 API 来为输入生成有意义的句子嵌入,以这种方式,两个句子对之间的关系可以通过常见的度量标准(如余弦相似度)轻松计算出来。基于句子变形器的 BERT 嵌入可以将上述类似任务的时间从 65 小时减少到仅仅 5 秒。除了 STS 任务之外,这些嵌入还被证明对其他任务有用,如自然语言推理(NLI),下一句预测等。

变形金刚 API

句子转换器是一个 Python API,其中有来自 100 多种语言的句子嵌入。代码针对快速计算进行了很好的优化。API 中还提供了不同的度量标准来计算和查找相似的句子,进行释义挖掘,并帮助进行语义搜索。

让我们看看如何在句子转换器 API 中编码句子,并使用其内置的 API 计算句子对之间的余弦相似度。

在我们开始之前,我们需要从 pip 安装这个包:

pip install sentence_transformers

现在让我们编码:

from sentence_transformers import SentenceTransformer, util
model = SentenceTransformer('all-MiniLM-L6-v2') #using a relatively smaller size model from the api

#two columns with indexes corresponding to pairs
col1=["I like to watch tv","The cat was running after the butterfly"]
col2=["Watching Television is my favorite time pass","It is so windy today"]

#Compute encodings for both lists
vectors1 = model.encode(col1, convert_to_tensor=True)
vectors2 = model.encode(col2, convert_to_tensor=True)

#Computing the cosine similarity for every pair
cosine_scores = util.cos_sim(vectors1, vectors2)

#Display cosine similarity score for the computed embeddings

for i,(sent1,sent2) in enumerate(zip(col1,col2)):
    if cosine_scores[i][i]>=0.5:
      label="Similar"
    else:
      label="Not Similar"
    print("sentence 1:{} | sentence 2:{}| prediction: {}".format(sent1,sent2,label)) 

输出:

sentence 1:I like to watch tv | sentence 2:Watching Television is my favorite time pass| prediction: Similar

sentence1:The cat was running after the butterfly | sentence 2:It is so windy today| prediction: Not Similar

正如您从输出中看到的,句子转换 API 能够快速准确地评估示例字符串之间的相似性,第一个正确地被声明为相似,第二个正确地被声明为不相似。

Pytorch-lightning 框架

到目前为止,我们已经了解了如何编写代码来计算句子相似度。但是当涉及到扩展模型或在生产中使用它时,编写笔记本风格的代码通常是不够的。如果您使用 PyTorch,您将处理一个训练循环、一个验证循环、一个测试循环、优化器和其他配置。Pytorch-lightning 通过简单地提供一个框架,可以轻松地以一种可伸缩、易于使用的方式包装所有这些模块,为您省去所有这些麻烦。

让我们熟悉一些在 lightning 模块上使用的概念和工具包,以进一步理解如何处理我们的文本数据。

数据模块

Datamodule从云/本地存储中获取数据,应用其他预处理/转换,如清理、标记化等,并将其包装在DataLoader对象中。这有助于为数据创建有组织的管理,而不是读取和预处理分布在几个文件或位置的数据。Dataloader也有助于将数据分割成列车测试和验证。

运动鞋

Trainer通过培训所需的必要功能,帮助您实现管道自动化。这意味着处理整个训练循环,管理超参数,加载模型和数据加载器,处理批处理和回调,根据给定的测试数据进行预测,最后保存模型检查点。培训师为您抽象出所有这些方面,无需任何额外的 PyTorch 代码。

闪电

随着人工智能迎接在每个领域工作的挑战,不同的框架每天都在涌现,在某个时间点,我们可能希望所有这些都在一个框架中可用。这就是闪电的本质目标。

Flash 是 PyTorch Lightning 团队提供给你的一个子项目,作为你大部分机器学习问题的一站式工具包。Flash 将其任务包装在一个 lightning 模块中,适当使用TrainerDatamodule来利用 PyTorch 必须提供的每一个功能。可以用这种方式分析的一些流行的领域和任务包括音频、文本、图像、图形、结构化数据等。

让我们试用一个来自 lightning-flash 的文本分类示例来回顾一下上面的概念,TrainerDataloader,以及它们的实现。训练数据来自 Kaggle 的垃圾短信分类任务。

如果您在创建笔记本时输入这个 Github 链接作为您的工作区 URL,它将与运行该演示所需的笔记本和 Python 脚本一起预上传到您的渐变实例中。您还可以通过该链接访问和分叉该笔记本的公共版本。

 import torch

import flash
from flash.text import TextClassificationData, TextClassifier
#using the SPAM text message classification from kaggle: https://www.kaggle.com/team-ai/spam-text-message-classification
datamodule = TextClassificationData.from_csv(
    "Message", #source column
    "Category", #target column : The data does not even need to be integer encoded!
    train_file="/content/SPAM text message 20170820 - Data.csv",
    batch_size=8,
)

# The Model is  loaded with a huggingface backbone
model = TextClassifier(backbone="prajjwal1/bert-small", num_classes=datamodule.num_classes)

# A trainer object is created with the help of pytorch-lightning module and the task is finetuned
trainer = flash.Trainer(max_epochs=2, gpus = 1)
trainer.finetune(model, datamodule=datamodule, strategy="freeze")

# few data for predictions
datamodule = TextClassificationData.from_lists(
    predict_data=[
        "Can you spill the secret?",
        "Camera - You are awarded a SiPix Digital Camera! call 09061221066 fromm landline. Delivery within 28 days.",
        "How are you? We have reached India.",
    ],
    batch_size=8,
)
predictions = trainer.predict(model, datamodule=datamodule)
print(predictions)
# >>>[['ham', 'spam', 'ham']]

# Finally, we save the model
trainer.save_checkpoint("spam_classification.pt")

上面的示例显示了一个下游任务,其中模型根据给定的垃圾邮件与垃圾邮件数据进行了微调。当涉及到使用句子嵌入时,当涉及到与句子相似性相关的任务时,我们不需要微调,即,生成的句子嵌入可以直接与来自句子转换器 API 的度量一起使用,以容易地计算相似性。

让我们看一个例子,看看 lightning-flash 如何帮助我们计算句子嵌入,并尝试解决句子相似性任务,而实际上不需要对下游任务进行微调,并使用无监督的方法。

 import torch

import flash
from flash.text import TextClassificationData, TextEmbedder
from sentence_transformers import util
predict_data=["I like to watch tv","Watching Television is my favorite time pass","The cat was running after the butterfly","It is so windy today"]
# Wrapping the prediction data inside a datamodule
datamodule = TextClassificationData.from_lists(
    predict_data=predict_data,
    batch_size=8,
)

# We are loading a pre-trained SentenceEmbedder
model = TextEmbedder(backbone="sentence-transformers/all-MiniLM-L6-v2")

trainer = flash.Trainer(gpus=1)

#Since this task is tackled unsupervised, the predict method generates sentence embeddings using the prediction input
embeddings = trainer.predict(model, datamodule=datamodule)

for i in range(0,len(predict_data),2):
  embed1,embed2=embeddings[0][i],embeddings[0][i+1]
  # we are using cosine similarity to compute the similarity score
  cosine_scores = util.cos_sim(embed1, embed2)
  if cosine_scores>=0.5:
      label="Similar"
  else:
      label="Not Similar"
  print("sentence 1:{} | sentence 2:{}| prediction: {}".format(predict_data[i],predict_data[i+1],label)) 

如您所见,我们不需要像之前的文本分类任务那样进行微调。Lightning-flash 的功能将有助于通过Datamodule输入数据,并借助来自Trainer对象的predict方法生成嵌入。

结论

在本文中,我们已经介绍了句子转换器的基础知识以及使用句子转换器解决句子相似性问题。对于这样的任务,我们还评估了句子嵌入相对于单词嵌入的优势。通过查看 pytorch-lightning 实现句子转换器的示例,我们学会了为生产就绪的应用程序扩展代码,现在我们可以通过避免样板代码来简化编写 pytorch 训练循环所需的管道。

具有 Keras 的递归神经网络的注意机制

原文:https://blog.paperspace.com/seq-to-seq-attention-mechanism-keras/

在本教程中,我们将介绍 RNNs 中的注意机制:它们如何工作,网络架构,它们的应用,以及如何使用 Keras 实现注意机制。

具体来说,我们将涵盖:

  • 用于神经机器翻译的序列到序列模型的问题
  • 注意力机制导论
  • 注意机制的类别
  • 注意机制的应用
  • 使用具有注意机制的 RNN 的神经机器翻译
  • 结论

你可以从 Gradient 社区笔记本的免费 GPU 上运行本教程中的所有代码。

我们开始吧!

注意:本系列中的所有例子(高级 RNNs)都是在 TensorFlow 2.x 上训练的。

用于神经机器翻译的序列到序列模型的问题

机器翻译问题促使我们发明了“注意力机制”。机器翻译是从一种语言到另一种语言的自动转换。这种转换必须使用计算机程序进行,该程序必须具有将文本从一种语言转换成另一种语言的智能。当神经网络执行这项工作时,它被称为“神经机器翻译”。

由于人类语言的模糊性和复杂性,机器翻译是人工智能中最具挑战性的问题之一。

尽管很复杂,我们已经看到了许多解决这个问题的方法。

神经网络在设计机器翻译过程自动化的方法中发挥了至关重要的作用。第一个适合这种应用的神经网络是序列对序列模型。

如 *编码器-解码器序列到序列模型介绍(Seq2Seq) ,*所示,序列到序列模型包括编码器和解码器,其中编码器产生上下文向量(编码表示)作为副产品。这个向量被提供给解码器,然后开始产生输出。

有趣的是,这就是通常的翻译过程。

A sequence-to-sequence model with ‘c’ as the encoded (context) vector

编码器-解码器序列到序列模型本身类似于当前的翻译过程。它包括将源语言编码成合适的表示形式,然后将其解码成目标语言,其中输入和输出向量不必大小相同。然而,这种模式也存在一些问题:

  • 上下文向量具有固定长度。假设有一个很长的序列需要被编码。由于编码向量的大小不变,网络很难定义长序列的编码表示。通常,它可能会忘记序列的早期部分,导致重要信息的丢失。
  • 序列到序列模型将 编码器的最终状态视为要传递给解码器的上下文向量。换句话说,它不检查编码过程中生成的中间状态。如果涉及长序列的输入数据,这也会导致信息丢失。

这两个因素会成为提高序列到序列模型性能的瓶颈。为了根除这个问题,我们可以通过使模型能够软搜索输入来过滤其中的相关位置,从而扩展这个架构。然后,它可以基于相对上下文向量和所有先前生成的输出单词来预测输出。

这正是注意力机制所做的!

注意力机制导论

按照典型的英语词汇,“注意力”指的是将你的注意力集中在某件事情上。如果我们考虑神经机器翻译的例子,你认为“注意力”在哪里?

注意机制旨在解决我们在用序列到序列模型训练神经机器翻译模型时讨论的两个问题。首先,当存在注意力整合时,该模型不需要将编码输出压缩到单个上下文向量中。相反,它将输入序列编码成向量序列,并根据解码器的隐藏状态选择这些向量的子集。简而言之,注意力被用来选择什么是必要的,而不会放过其他必要的信息。

这就是带有注意力机制的 RNN 模型的样子:

An RNN Model with “Attention Mechanism”

下面是如何使用注意力机制解决机器翻译问题的一步一步的过程:

  • 首先,输入序列$ x1,x2,x3 \(被提供给编码器 LSTM。矢量\) h1,H2,H3 $由编码器从给定的输入序列中计算出来。这些向量是给予注意力机制的输入。随后,解码器输入第一状态向量$s_0$,该向量也作为注意机制的输入。我们现在有$s_0$和$h_1,h_2,h_3$作为输入。
  • 注意机制模式(用红框表示)接受输入,并通过全连接网络和 softmax 激活功能传递它们,从而生成“注意权重”。
  • 然后计算编码器输出向量的加权和,得到上下文向量$c_1$。这里,向量根据注意力权重进行缩放。
  • 现在,解码器的工作是处理状态和上下文向量,以生成输出向量$y_1$。
  • 解码器还产生结果状态向量$s_1$,它与编码器的输出一起再次提供给注意机制模型。
  • 这产生了加权和,产生了上下文向量$c_2$。
  • 这个过程继续进行,直到所有的解码器都产生了输出向量$y_1,y_2,y_3$。

注意机制模型的问题在于,上下文向量使解码器能够只关注其输入的某些部分(事实上,上下文向量是从编码器的输出中生成的)。通过这种方式,模型会关注它认为对确定输出至关重要的所有输入。

注意机制的类别

我们可以将注意机制大致分为三类:自我注意、软注意和硬注意机制。

自我关注

自我关注有助于模型内部的互动。用于机器阅读论文的长短期记忆网络使用自我关注。学习过程如下例所示:

The word in red is the current word being read. The blue colour indicates the activation level (memories). [Source]

这里的注意力是在相同的序列内计算的。换句话说,自我注意使输入元素能够相互作用。

柔和的注意力

软注意“柔和地”将注意力权重放在输入(图像/句子)的所有块上,即,它采用加权平均机制。它测量关于输入的各种组块的注意力,并输出加权的输入特征。它通过给与手头任务无关的区域分配较低的权重来抹黑它们。这样,软注意就不会把注意力局限在图像或句子的特定部分;而是不断学习。

软注意是一种完全可微分的注意机制,其中梯度可以在反向传播期间自动传播。

强烈的关注

“硬”,顾名思义,只针对图像/句子的特定部分。在反向传播过程中,为了估计所有其他状态的梯度,我们需要使用蒙特卡罗方法进行采样并对结果进行平均。

Soft (top row) vs Hard (bottom row) Attention [Source]

注意机制的应用

注意力机制的几个应用包括:

  • 图像字幕
  • 语音识别
  • 机器翻译
  • 无人驾驶汽车
  • 文档摘要

Image Captioning Model using Attention Mechanism [Source]

使用具有注意机制的 RNN 的神经机器翻译

RNN 可以用来实现机器翻译。一般来说,一个简单的 RNN 加上一个编码器-解码器序列到序列模型就可以完成这项工作。然而,如章节“神经机器翻译的序列到序列模型的问题中所述,编码输出的压缩表示可能会忽略翻译过程所需的重要特征。为了根除这个问题,当我们用编码器-解码器序列到序列模型嵌入注意机制时,当涉及长的文本序列时,我们不必在信息损失上妥协。

注意力机制集中在所有的输入上,这些输入是产生输出所真正需要的。这里不涉及压缩。相反,它会考虑编码器的所有输出,并根据解码器的隐藏状态为它们分配重要性。

下面是一个使用 RNN 模型(带注意机制的编码器-解码器序列到序列)进行法语到英语翻译的逐步过程。

不要忘记,你可以跟随代码,并从渐变社区笔记本的免费 GPU 上运行它。

步骤 1:导入数据集

首先,导入英语到法语的数据集(下载链接)。它有大约 185,583 个语言翻译对。

解包数据集并存储txt文件路径。

# Untar the dataset
!unzip 'fra-eng.zip'

# Get the txt file which has English -> French translation
path_to_file = "fra.txt"

步骤 2:预处理数据集

数据集包含 Unicode 字符,必须对其进行规范化。

此外,必须使用正则表达式库清理序列中的所有标记。

删除不需要的空格,在每个单词和其后的标点符号之间包含一个空格(以区分两者),用空格替换不需要的字符,并添加<start><end>标记来指定序列的开始和结束。

将 unicode 转换封装在函数unicode_to_ascii()中,将序列预处理封装在函数preprocess_sentence()中。

import unicodedata

import re

# Convert the unicode sequence to ascii
def unicode_to_ascii(s):

  # Normalize the unicode string and remove the non-spacking mark
  return ''.join(c for c in unicodedata.normalize('NFD', s)
      if unicodedata.category(c) != 'Mn')

# Preprocess the sequence
def preprocess_sentence(w):

  # Clean the sequence
  w = unicode_to_ascii(w.lower().strip())

  # Create a space between word and the punctuation following it
  w = re.sub(r"([?.!,¿])", r" \1 ", w)
  w = re.sub(r'[" "]+', " ", w)

  # Replace everything with space except (a-z, A-Z, ".", "?", "!", ",")
  w = re.sub(r"[^a-zA-Z?.!,¿]+", " ", w)

  w = w.strip()

  # Add a start and stop token to detect the start and end of the sequence
  w = '<start> ' + w + ' <end>'
  return w

步骤 3:准备数据集

接下来,从我们拥有的原始数据中准备一个数据集。将英语序列和它们相关的法语序列组合起来,创建单词对。

import io

# Create the Dataset
def create_dataset(path, num_examples):
  lines = io.open(path, encoding='UTF-8').read().strip().split('\n')

  # Loop through lines (sequences) and extract the English and French sequences. Store them as a word-pair
  word_pairs = [[preprocess_sentence(w) for w in l.split('\t', 2)[:-1]]  for l in lines[:num_examples]]
  return zip(*word_pairs)

检查数据集是否已正确创建。

en, fra = create_dataset(path_to_file, None)
print(en[-1])
print(fra[-1])
# Output
<start> if someone who doesn t know your background says that you sound like a native speaker , it means they probably noticed something about your speaking that made them realize you weren t a native speaker . in other words , you don t really sound like a native speaker . <end>
<start> si quelqu un qui ne connait pas vos antecedents dit que vous parlez comme un locuteur natif , cela veut dire qu il a probablement remarque quelque chose a propos de votre elocution qui lui a fait prendre conscience que vous n etes pas un locuteur natif . en d autres termes , vous ne parlez pas vraiment comme un locuteur natif . <end> 

现在标记序列。记号化是创建包括英语和法语记号(即单词)的内部词汇表、将记号(或者一般来说,序列)转换成整数、并填充它们以使序列具有相同长度的机制。总而言之,标记化促进了模型训练过程。

创建一个函数tokenize()来封装上述所有需求。

import tensorflow as tf

# Convert sequences to tokenizers
def tokenize(lang):
  lang_tokenizer = tf.keras.preprocessing.text.Tokenizer(
      filters='')

  # Convert sequences into internal vocab
  lang_tokenizer.fit_on_texts(lang)

  # Convert internal vocab to numbers
  tensor = lang_tokenizer.texts_to_sequences(lang)

  # Pad the tensors to assign equal length to all the sequences
  tensor = tf.keras.preprocessing.sequence.pad_sequences(tensor,
                                                         padding='post')

  return tensor, lang_tokenizer

通过调用create_dataset()tokenize()函数加载标记化的数据集。

# Load the dataset
def load_dataset(path, num_examples=None):

  # Create dataset (targ_lan = English, inp_lang = French)
  targ_lang, inp_lang = create_dataset(path, num_examples)

  # Tokenize the sequences
  input_tensor, inp_lang_tokenizer = tokenize(inp_lang)
  target_tensor, targ_lang_tokenizer = tokenize(targ_lang)

  return input_tensor, target_tensor, inp_lang_tokenizer, targ_lang_tokenizer

减少训练模型所需的数据样本数量。使用整个数据集将消耗更多的时间来训练模型。

# Consider 50k examples
num_examples = 50000
input_tensor, target_tensor, inp_lang, targ_lang = load_dataset(path_to_file, num_examples)

# Calculate max_length of the target tensors
max_length_targ, max_length_inp = target_tensor.shape[1], input_tensor.shape[1]

输入和目标张量的max_length对于确定每个序列的最大填充长度至关重要。

步骤 4:创建数据集

分离训练和验证数据集。

!pip3 install sklearn

from sklearn.model_selection import train_test_split

# Create training and validation sets using an 80/20 split
input_tensor_train, input_tensor_val, target_tensor_train, target_tensor_val = train_test_split(input_tensor, target_tensor, test_size=0.2)

print(len(input_tensor_train), len(target_tensor_train), len(input_tensor_val), len(target_tensor_val))
# Output
40000 40000 10000 10000

验证在序列的标记和索引之间创建的映射。

# Show the mapping b/w word index and language tokenizer
def convert(lang, tensor):
  for t in tensor:
    if t != 0:
      print ("%d ----> %s" % (t, lang.index_word[t]))

print ("Input Language; index to word mapping")
convert(inp_lang, input_tensor_train[0])
print ()
print ("Target Language; index to word mapping")
convert(targ_lang, target_tensor_train[0])
# Output
Input Language; index to word mapping
1 ----> <start>
140 ----> quel
408 ----> idiot
3 ----> .
2 ----> <end>

Target Language; index to word mapping
1 ----> <start>
33 ----> what
86 ----> an
661 ----> idiot
36 ----> !
2 ----> <end> 

步骤 5:初始化模型参数

有了数据集,开始初始化模型参数。

  • BUFFER_SIZE:输入/目标样本总数。在我们的模型中,它是 40,000。
  • BATCH_SIZE:训练批次的长度。
  • steps_per_epoch:每个历元的步数。通过将BUFFER_SIZE除以BATCH_SIZE来计算。
  • embedding_dim:嵌入层的节点数。
  • units:网络中的隐藏单元。
  • vocab_inp_size:输入(法语)词汇的长度。
  • vocab_tar_size:输出(英语)词汇的长度。
# Essential model parameters
BUFFER_SIZE = len(input_tensor_train)
BATCH_SIZE = 64
steps_per_epoch = len(input_tensor_train)//BATCH_SIZE
embedding_dim = 256
units = 1024
vocab_inp_size = len(inp_lang.word_index) + 1
vocab_tar_size = len(targ_lang.word_index) + 1

接下来,调用tf.data.Dataset API 并创建一个合适的数据集。

dataset = tf.data.Dataset.from_tensor_slices((input_tensor_train, target_tensor_train)).shuffle(BUFFER_SIZE)
dataset = dataset.batch(BATCH_SIZE, drop_remainder=True)

验证新创建的数据集的输入批次和目标批次的形状。

# Size of input and target batches
example_input_batch, example_target_batch = next(iter(dataset))
example_input_batch.shape, example_target_batch.shape
# Output
(TensorShape([64, 19]), TensorShape([64, 11])) 

1911表示输入(法语)和目标(英语)序列的最大填充长度。

步骤 6:编码器类

创建编码器-解码器序列到序列模型(具有注意机制)的第一步是创建编码器。对于手头的应用程序,创建一个编码器,其嵌入层后跟一个 GRU(门控递归单元)层。输入首先通过嵌入层,然后进入 GRU 层。GRU 层输出编码器网络输出和隐藏状态。

将模型的__init__()call()方法放在一个类Encoder中。

在方法__init__()中,初始化批量大小和编码单位。添加一个嵌入层,接受vocab_size作为输入维度,接受embedding_dim作为输出维度。另外,添加一个接受units(输出空间的维度)和第一个隐藏维度的 GRU 层。

在方法call()中,定义必须通过编码器网络发生的正向传播。

此外,定义一个方法initialize_hidden_state(),用维度batch_sizeunits初始化隐藏状态。

添加以下代码作为您的Encoder类的一部分。

# Encoder class
class Encoder(tf.keras.Model):
  def __init__(self, vocab_size, embedding_dim, enc_units, batch_sz):
    super(Encoder, self).__init__()
    self.batch_sz = batch_sz
    self.enc_units = enc_units

    # Embed the vocab to a dense embedding 
    self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)

    # GRU Layer
    # glorot_uniform: Initializer for the recurrent_kernel weights matrix, 
    # used for the linear transformation of the recurrent state
    self.gru = tf.keras.layers.GRU(self.enc_units,
                                   return_sequences=True,
                                   return_state=True,
                                   recurrent_initializer='glorot_uniform')

  # Encoder network comprises an Embedding layer followed by a GRU layer
  def call(self, x, hidden):
    x = self.embedding(x)
    output, state = self.gru(x, initial_state=hidden)
    return output, state

  # To initialize the hidden state
  def initialize_hidden_state(self):
    return tf.zeros((self.batch_sz, self.enc_units))

调用 encoder 类来检查编码器输出的形状和隐藏状态。

encoder = Encoder(vocab_inp_size, embedding_dim, units, BATCH_SIZE)

sample_hidden = encoder.initialize_hidden_state()
sample_output, sample_hidden = encoder(example_input_batch, sample_hidden)

print ('Encoder output shape: (batch size, sequence length, units) {}'.format(sample_output.shape))
print ('Encoder Hidden state shape: (batch size, units) {}'.format(sample_hidden.shape))
# Output
Encoder output shape: (batch size, sequence length, units) (64, 19, 1024)
Encoder Hidden state shape: (batch size, units) (64, 1024) 

第七步:注意机制类

这一步抓住了注意力机制。

  • 计算编码器输出和解码器状态的和(或积)。
  • 通过完全连接的网络传递生成的输出。
  • 对输出应用 softmax 激活。这赋予了注意力权重。
  • 通过计算注意力权重和编码器输出的加权和来创建上下文向量。

到目前为止,所有的东西都需要被捕获到一个类BahdanauAttention中。 Bahdanau 注意力也被称为“加法注意力”,一种软注意力技术。由于这是附加注意,我们做编码器的输出和解码器隐藏状态的总和(如第一步所述)。

这个类必须有__init__()call()方法。

__init__()方法中,初始化三个Dense层:一个用于解码器状态(“单位”是大小),另一个用于编码器的输出(“单位”是大小),另一个用于全连接网络(一个节点)。

call()方法中,通过获取最终的编码器隐藏状态来初始化解码器状态(\(s_0\))。将生成的解码器隐藏状态通过一个密集层。此外,通过另一个密集层插入编码器的输出。将两个输出相加,将它们封装在一个tanh激活中,并将它们插入全连接层。这个全连接层有一个节点;因此,最终输出的尺寸为batch_size * max_length of the sequence * 1

稍后,对全连接网络的输出应用 softmax 以生成注意力权重。

通过对注意力权重和编码器的输出进行加权求和来计算context_vector

# Attention Mechanism
class BahdanauAttention(tf.keras.layers.Layer):
  def __init__(self, units):
    super(BahdanauAttention, self).__init__()
    self.W1 = tf.keras.layers.Dense(units)
    self.W2 = tf.keras.layers.Dense(units)
    self.V = tf.keras.layers.Dense(1)

  def call(self, query, values):
    # query hidden state shape == (batch_size, hidden size)
    # values shape == (batch_size, max_len, hidden size)

    # we are doing this to broadcast addition along the time axis to calculate the score
    # query_with_time_axis shape == (batch_size, 1, hidden size)
    query_with_time_axis = tf.expand_dims(query, 1)

    # score shape == (batch_size, max_length, 1)
    # we get 1 at the last axis because we are applying score to self.V
    # the shape of the tensor before applying self.V is (batch_size, max_length, units)
    score = self.V(tf.nn.tanh(
        self.W1(query_with_time_axis) + self.W2(values)))

    # attention_weights shape == (batch_size, max_length, 1)
    attention_weights = tf.nn.softmax(score, axis=1)

    # context_vector shape after sum == (batch_size, hidden_size)
    context_vector = attention_weights * values
    context_vector = tf.reduce_sum(context_vector, axis=1)

    return context_vector, attention_weights

验证注意力权重的形状及其输出。

attention_layer = BahdanauAttention(10)
attention_result, attention_weights = attention_layer(sample_hidden, sample_output)

print("Attention result shape: (batch size, units) {}".format(attention_result.shape))
print("Attention weights shape: (batch_size, sequence_length, 1) {}".format(attention_weights.shape))
# Output
Attention result shape: (batch size, units) (64, 1024)
Attention weights shape: (batch_size, sequence_length, 1) (64, 19, 1) 

sample_hidden这里是编码器的隐藏状态,sample_output表示编码器的输出。

步骤 8:解码器类

这一步封装了解码机制。这个Decoder类必须有两个方法:__init__()call()

__init__()方法中,初始化批量大小、解码器单元、嵌入维度、GRU 层和密集层。另外,创建一个BahdanauAttention类的实例。

call()方法中:

  • 调用注意力向前传播并捕获上下文向量和注意力权重。
  • 通过嵌入层发送目标令牌。
  • 连接嵌入的输出和上下文向量。
  • 将输出插入 GRU 层,然后插入全连接层。

添加以下代码来定义Decoder类。

# Decoder class
class Decoder(tf.keras.Model):
  def __init__(self, vocab_size, embedding_dim, dec_units, batch_sz):
    super(Decoder, self).__init__()
    self.batch_sz = batch_sz
    self.dec_units = dec_units
    self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
    self.gru = tf.keras.layers.GRU(self.dec_units,
                                   return_sequences=True,
                                   return_state=True,
                                   recurrent_initializer='glorot_uniform')
    self.fc = tf.keras.layers.Dense(vocab_size)

    # Used for attention
    self.attention = BahdanauAttention(self.dec_units)

  def call(self, x, hidden, enc_output):
    # x shape == (batch_size, 1)
    # hidden shape == (batch_size, max_length)
    # enc_output shape == (batch_size, max_length, hidden_size)

    # context_vector shape == (batch_size, hidden_size)
    # attention_weights shape == (batch_size, max_length, 1)
    context_vector, attention_weights = self.attention(hidden, enc_output)

    # x shape after passing through embedding == (batch_size, 1, embedding_dim)
    x = self.embedding(x)

    # x shape after concatenation == (batch_size, 1, embedding_dim + hidden_size)
    x = tf.concat([tf.expand_dims(context_vector, 1), x], axis=-1)

    # passing the concatenated vector to the GRU
    output, state = self.gru(x)

    # output shape == (batch_size * 1, hidden_size)
    output = tf.reshape(output, (-1, output.shape[2]))

    # output shape == (batch_size, vocab)
    x = self.fc(output)

    return x, state, attention_weights

验证解码器输出形状。

decoder = Decoder(vocab_tar_size, embedding_dim, units, BATCH_SIZE)

sample_decoder_output, _, _ = decoder(tf.random.uniform((BATCH_SIZE, 1)),
                                      sample_hidden, sample_output)

print ('Decoder output shape: (batch_size, vocab size) {}'.format(sample_decoder_output.shape))
# Output
Decoder output shape: (batch_size, vocab size) (64, 5892)

步骤 9:优化器和损失函数

定义优化器和损失函数。

由于输入序列是用零填充的,所以当real值为零时,可以消除损失。

# Initialize optimizer and loss functions
optimizer = tf.keras.optimizers.Adam()

loss_object = tf.keras.losses.SparseCategoricalCrossentropy(
    from_logits=True, reduction='none')

# Loss function
def loss_function(real, pred):

  # Take care of the padding. Not all sequences are of equal length.
  # If there's a '0' in the sequence, the loss is being nullified
  mask = tf.math.logical_not(tf.math.equal(real, 0))
  loss_ = loss_object(real, pred)

  mask = tf.cast(mask, dtype=loss_.dtype)
  loss_ *= mask

  return tf.reduce_mean(loss_)

步骤 10:训练模型

在训练期间检查你的模型的重量。这有助于在评估模型时自动检索权重。

import os

checkpoint_dir = './training_checkpoints'
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt")
checkpoint = tf.train.Checkpoint(optimizer=optimizer,
                                 encoder=encoder,
                                 decoder=decoder)

接下来,定义培训程序。首先,调用编码器类并获取编码器输出和最终隐藏状态。初始化解码器输入,使<start>令牌分布在所有输入序列上(使用BATCH_SIZE指示)。使用教师强制技术通过将目标作为下一个输入来迭代所有解码器状态。这个循环一直持续到目标序列(英语)中的每个标记都被访问。

用解码器输入、解码器隐藏状态和编码器输出调用解码器类。获取解码器输出和隐藏状态。通过比较目标的实际值和预测值来计算损失。获取目标令牌并将其馈送到下一个解码器状态(关于后续目标令牌)。此外,请注意,目标解码器隐藏状态将是下一个解码器隐藏状态。

教师强制技术完成后,计算批量损失,并运行优化器来更新模型的变量。

@tf.function
def train_step(inp, targ, enc_hidden):
  loss = 0

  # tf.GradientTape() -- record operations for automatic differentiation
  with tf.GradientTape() as tape:
    enc_output, enc_hidden = encoder(inp, enc_hidden)

    # dec_hidden is used by attention, hence is the same enc_hidden
    dec_hidden = enc_hidden

    # <start> token is the initial decoder input
    dec_input = tf.expand_dims([targ_lang.word_index['<start>']] * BATCH_SIZE, 1)

    # Teacher forcing - feeding the target as the next input
    for t in range(1, targ.shape[1]):

      # Pass enc_output to the decoder
      predictions, dec_hidden, _ = decoder(dec_input, dec_hidden, enc_output)

      # Compute the loss
      loss += loss_function(targ[:, t], predictions)

      # Use teacher forcing
      dec_input = tf.expand_dims(targ[:, t], 1)

  # As this function is called per batch, compute the batch_loss
  batch_loss = (loss / int(targ.shape[1]))

  # Get the model's variables
  variables = encoder.trainable_variables + decoder.trainable_variables

  # Compute the gradients
  gradients = tape.gradient(loss, variables)

  # Update the variables of the model/network
  optimizer.apply_gradients(zip(gradients, variables))

  return batch_loss

现在初始化实际的训练循环。运行指定次数的循环。首先,使用方法initialize_hidden_state()初始化编码器隐藏状态。一次循环一批数据集(每个历元)。每批调用train_step()方法,计算损耗。继续进行,直到覆盖了所有的时期。

import time

EPOCHS = 30

# Training loop
for epoch in range(EPOCHS):
  start = time.time()

  # Initialize the hidden state
  enc_hidden = encoder.initialize_hidden_state()
  total_loss = 0

  # Loop through the dataset
  for (batch, (inp, targ)) in enumerate(dataset.take(steps_per_epoch)):

    # Call the train method
    batch_loss = train_step(inp, targ, enc_hidden)

    # Compute the loss (per batch)
    total_loss += batch_loss

    if batch % 100 == 0:
      print('Epoch {} Batch {} Loss {:.4f}'.format(epoch + 1,
                                                   batch,
                                                   batch_loss.numpy()))
  # Save (checkpoint) the model every 2 epochs
  if (epoch + 1) % 2 == 0:
    checkpoint.save(file_prefix = checkpoint_prefix)

  # Output the loss observed until that epoch
  print('Epoch {} Loss {:.4f}'.format(epoch + 1,
                                      total_loss / steps_per_epoch))

  print('Time taken for 1 epoch {} sec\n'.format(time.time() - start))
# Output
Epoch 1 Batch 0 Loss 0.0695
Epoch 1 Batch 100 Loss 0.0748
Epoch 1 Batch 200 Loss 0.0844
Epoch 1 Batch 300 Loss 0.0900
Epoch 1 Batch 400 Loss 0.1104
Epoch 1 Batch 500 Loss 0.1273
Epoch 1 Batch 600 Loss 0.1203
Epoch 1 Loss 0.0944
Time taken for 1 epoch 113.93592977523804 sec

Epoch 2 Batch 0 Loss 0.0705
Epoch 2 Batch 100 Loss 0.0870
Epoch 2 Batch 200 Loss 0.1189
Epoch 2 Batch 300 Loss 0.0995
Epoch 2 Batch 400 Loss 0.1375
Epoch 2 Batch 500 Loss 0.0996
Epoch 2 Batch 600 Loss 0.1054
Epoch 2 Loss 0.0860
Time taken for 1 epoch 115.66511249542236 sec

Epoch 3 Batch 0 Loss 0.0920
Epoch 3 Batch 100 Loss 0.0709
Epoch 3 Batch 200 Loss 0.0667
Epoch 3 Batch 300 Loss 0.0580
Epoch 3 Batch 400 Loss 0.0921
Epoch 3 Batch 500 Loss 0.0534
Epoch 3 Batch 600 Loss 0.1243
Epoch 3 Loss 0.0796
Time taken for 1 epoch 114.04204559326172 sec

Epoch 4 Batch 0 Loss 0.0847
Epoch 4 Batch 100 Loss 0.0524
Epoch 4 Batch 200 Loss 0.0668
Epoch 4 Batch 300 Loss 0.0498
Epoch 4 Batch 400 Loss 0.0776
Epoch 4 Batch 500 Loss 0.0614
Epoch 4 Batch 600 Loss 0.0616
Epoch 4 Loss 0.0734
Time taken for 1 epoch 114.43488264083862 sec

Epoch 5 Batch 0 Loss 0.0570
Epoch 5 Batch 100 Loss 0.0554
Epoch 5 Batch 200 Loss 0.0731
Epoch 5 Batch 300 Loss 0.0668
Epoch 5 Batch 400 Loss 0.0510
Epoch 5 Batch 500 Loss 0.0630
Epoch 5 Batch 600 Loss 0.0809
Epoch 5 Loss 0.0698
Time taken for 1 epoch 114.07995843887329 sec

Epoch 6 Batch 0 Loss 0.0842
Epoch 6 Batch 100 Loss 0.0489
Epoch 6 Batch 200 Loss 0.0540
Epoch 6 Batch 300 Loss 0.0809
Epoch 6 Batch 400 Loss 0.0807
Epoch 6 Batch 500 Loss 0.0590
Epoch 6 Batch 600 Loss 0.1161
Epoch 6 Loss 0.0684
Time taken for 1 epoch 114.42468786239624 sec
…
Epoch 29 Batch 0 Loss 0.0376
Epoch 29 Batch 100 Loss 0.0478
Epoch 29 Batch 200 Loss 0.0489
Epoch 29 Batch 300 Loss 0.0251
Epoch 29 Batch 400 Loss 0.0296
Epoch 29 Batch 500 Loss 0.0385
Epoch 29 Batch 600 Loss 0.0638
Epoch 29 Loss 0.0396
Time taken for 1 epoch 114.00363779067993 sec

Epoch 30 Batch 0 Loss 0.0196
Epoch 30 Batch 100 Loss 0.0246
Epoch 30 Batch 200 Loss 0.0296
Epoch 30 Batch 300 Loss 0.0204
Epoch 30 Batch 400 Loss 0.0269
Epoch 30 Batch 500 Loss 0.0598
Epoch 30 Batch 600 Loss 0.0290
Epoch 30 Loss 0.0377
Time taken for 1 epoch 114.20779871940613 sec 

步骤 11:测试模型

现在定义您的模型评估过程。首先,考虑用户给出的句子。这必须用法语给出。该模型现在必须将句子从法语转换成英语。

用 Y 轴上的max_length_target和 X 轴上的max_length_input初始化稍后要绘制的空白注意力图。

对句子进行预处理,转换成张量。

然后把句子代入模型。

初始化一个空的隐藏状态,该状态将在初始化编码器时使用。通常,编码器类中的initialize_hidden_state()方法给出了维度为batch_size * hidden_units的隐藏状态。现在,由于批处理大小为$1$,初始隐藏状态必须手动初始化。

调用编码器类并获得编码器输出和最终隐藏状态。

通过循环遍历max_length_targ,调用解码器类,其中dec_input<start>令牌,dec_hidden状态是编码器隐藏状态,enc_out是编码器的输出。获取解码器输出、隐藏状态和注意力权重。

使用注意力权重创建一个图。获取最受关注的预测令牌。将标记附加到结果中,继续执行,直到到达<end>标记。

下一个解码器输入将是先前预测的索引(关于令牌)。

添加以下代码作为evaluate()函数的一部分。

import numpy as np

# Evaluate function -- similar to the training loop
def evaluate(sentence):

  # Attention plot (to be plotted later on) -- initialized with max_lengths of both target and input
  attention_plot = np.zeros((max_length_targ, max_length_inp))

  # Preprocess the sentence given
  sentence = preprocess_sentence(sentence)

  # Fetch the indices concerning the words in the sentence and pad the sequence
  inputs = [inp_lang.word_index[i] for i in sentence.split(' ')]
  inputs = tf.keras.preprocessing.sequence.pad_sequences([inputs],
                                                         maxlen=max_length_inp,
                                                         padding='post')
  # Convert the inputs to tensors
  inputs = tf.convert_to_tensor(inputs)

  result = ''

  hidden = [tf.zeros((1, units))]
  enc_out, enc_hidden = encoder(inputs, hidden)

  dec_hidden = enc_hidden
  dec_input = tf.expand_dims([targ_lang.word_index['<start>']], 0)

  # Loop until the max_length is reached for the target lang (ENGLISH)
  for t in range(max_length_targ):
    predictions, dec_hidden, attention_weights = decoder(dec_input,
                                                         dec_hidden,
                                                         enc_out)

    # Store the attention weights to plot later on
    attention_weights = tf.reshape(attention_weights, (-1, ))
    attention_plot[t] = attention_weights.numpy()

    # Get the prediction with the maximum attention
    predicted_id = tf.argmax(predictions[0]).numpy()

    # Append the token to the result
    result += targ_lang.index_word[predicted_id] + ' '

    # If <end> token is reached, return the result, input, and attention plot
    if targ_lang.index_word[predicted_id] == '<end>':
      return result, sentence, attention_plot

    # The predicted ID is fed back into the model
    dec_input = tf.expand_dims([predicted_id], 0)

  return result, sentence, attention_plot

第十二步:计划和预测

定义plot_attention()函数来绘制注意力统计数据。

import matplotlib.pyplot as plt
import matplotlib.ticker as ticker

# Function for plotting the attention weights
def plot_attention(attention, sentence, predicted_sentence):
  fig = plt.figure(figsize=(10,10))
  ax = fig.add_subplot(1, 1, 1)
  ax.matshow(attention, cmap='viridis')

  fontdict = {'fontsize': 14}

  ax.set_xticklabels([''] + sentence, fontdict=fontdict, rotation=90)
  ax.set_yticklabels([''] + predicted_sentence, fontdict=fontdict)

  ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
  ax.yaxis.set_major_locator(ticker.MultipleLocator(1))

  plt.show()

定义一个函数translate(),它在内部调用evaluate()函数。

# Translate function (which internally calls the evaluate function)
def translate(sentence):
  result, sentence, attention_plot = evaluate(sentence)

  print('Input: %s' % (sentence))
  print('Predicted translation: {}'.format(result))

  attention_plot = attention_plot[:len(result.split(' ')), :len(sentence.split(' '))]
  plot_attention(attention_plot, sentence.split(' '), result.split(' '))

将保存的检查点恢复到model

# Restore the latest checkpoint in checkpoint_dir
checkpoint.restore(tf.train.latest_checkpoint(checkpoint_dir))
# Output
<tensorflow.python.training.tracking.util.CheckpointLoadStatus at 0x7fb0916e32d0> 

通过输入几个法语句子来调用translate()函数。

translate(u"As tu lu ce livre?")
# Output
Input: <start> as tu lu ce livre ? <end>
Predicted translation: did you read this book ? <end> 

实际翻译过来就是“你读过这本书吗?”

translate(u"Comment as-tu été?")
# Output
Input: <start> comment as tu ete ? <end>
Predicted translation: how were you ? <end> 

实际的翻译是“你过得怎么样?”

可以推断,预测的翻译接近实际翻译。

结论

这是我的 RNNs 系列的最后一部分。在本教程中,你已经了解了注意力机制是怎么回事。您已经了解了它如何优于一般的编码器-解码器序列到序列模型。你还训练了一个神经机器翻译模型,将句子从法语翻译成英语。您可以进一步调整模型的超参数来衡量模型的表现。

我希望你喜欢阅读这个系列!

参考: TensorFlow 教程

py torch seq 2 seq 翻译器简介

原文:https://blog.paperspace.com/seq2seq-translator-pytorch/

神经机器翻译是使用深度学习来生成从一种语言到另一种语言的文本的准确翻译的实践。这意味着训练一个深度神经网络来预测一个单词序列作为正确翻译的可能性。

这种技术的用途几乎是无限的。今天,我们有翻译人员能够对用其他语言编写的整个网页进行几乎即时且相对准确的翻译。我们可以将相机对准一段文本,并使用增强现实来用翻译代替文本。我们甚至可以动态地将现场演讲翻译成其他语言的文本。这种能力在很大程度上实现了技术全球化,如果没有序列到序列神经机器翻译的概念,这是不可能的。

谷歌的研究人员在 2014 年推出了第一个 Seq2Seq(序列到序列)翻译器。他们的发明从根本上改变了翻译领域,像谷歌翻译这样的热门服务已经发展到了巨大的准确性和可访问性水平,以满足互联网的需求。

在这篇博文中,我们将分解 Seq2Seq 翻译的理论和设计。然后,我们将从头开始浏览 Seq2Seq 翻译的官方 PyTorch 指南的增强版本,其中我们将首先改进原始框架,然后演示如何使其适应新的数据集。

Seq2Seq 译者:他们是如何工作的?

对于深度学习,Seq2Seq 翻译器以相对简单的方式工作。这类模型的目标是将固定长度的字符串输入映射到固定长度的成对字符串输出,其中这两个长度可以不同。如果输入语言中的一个字符串有 8 个单词,而目标语言中的同一个句子有 4 个单词,那么高质量的翻译者应该推断出这一点,并缩短输出的句子长度。

设计理论:

Source

Seq2Seq 翻译器通常共享一个公共框架。任何 Seq2Seq 转换器的三个主要组件是编码器和解码器网络以及它们之间的中间矢量编码。这些网络通常是递归神经网络(RNN),但它们通常是由更专业的门控递归单元(GRU)和长短期记忆(LSTM)组成的。这是为了限制潜在的消失梯度影响平移。

编码器网络是一系列这些 RNN 单元。它使用这些来对编码器向量的输入中的元素进行顺序编码,最终的隐藏状态被写入中间向量。

许多 NMT 模型利用注意力的概念来改进这种上下文编码。注意是通过一组权重迫使解码器关注编码器输出的某些部分的做法。这些注意力权重乘以编码器输出向量。这产生了组合矢量编码,其将增强解码器理解其正在生成的输出的上下文的能力,并因此改进其预测。计算这些注意力权重是通过前馈注意力层完成的,前馈注意力层使用解码器输入和隐藏状态作为输入。

编码器向量包含来自编码器的输入的数字表示。如果一切顺利,它会从最初的输入句子中捕获所有信息。然后,这个编码向量充当解码器网络的初始隐藏状态。

解码器网络本质上与编码器相反。它将编码的矢量中间体作为隐藏状态,并顺序生成翻译。输出中的每个元素通知解码器对下一个元素的预测。

实际上:

Source

实际上,一个 NMT 将接受一种语言的输入字符串,并创建一个表示句子中每个元素(单词)的嵌入序列。编码器中的 RNN 单元将先前的隐藏状态和原始输入嵌入的单个元素作为输入,并且每一步可以通过访问前一步的隐藏状态来通知预测的元素,从而顺序地改进前一步。值得一提的是,除了对句子进行编码之外,句子结束标记表示也作为一个元素包含在序列中。这种句尾标记有助于翻译者知道翻译语言中的哪些单词将触发解码器退出解码并输出翻译的句子。

最终的隐藏状态嵌入被编码在中间编码器矢量中。编码捕获尽可能多的关于输入句子的信息,以便于解码器将它们解码成翻译。这可以通过被用作解码器网络的初始隐藏状态来实现。

使用来自编码器向量的信息,解码器中的每个递归单元接受来自前一个单元的隐藏状态,并产生输出以及它自己的隐藏状态。隐藏状态通知解码器对序列进行预测,对于每个顺序预测,解码器使用来自前一个隐藏状态的信息预测序列的下一个实例。因此,最终输出是翻译句子中每个元素的逐步预测的最终结果。由于句尾标签,这个句子的长度与输入句子的长度无关,它告诉解码器何时停止向句子添加术语。

在下一节中,我们将展示如何使用定制函数和 PyTorch 实现每个步骤。

实现 Seq2Seq 转换器

PyTorch 网站上有一个关于从头开始创建 Seq2Seq 翻译器的精彩教程。下一节将修改那里的大部分代码,因此在开始实现这些更新之前,浏览一下他们的教程笔记本可能是值得的。

我们将以两种方式扩展本教程:添加一个全新的数据集,并进行调整以优化翻译功能。首先,我们将展示如何获取和准备 WMT2014 英法翻译数据集,以便在渐变笔记本中与 Seq2Seq 模型一起使用。由于大部分代码与 PyTorch 教程中的相同,我们将只关注编码器网络注意力解码器网络训练代码

准备数据

获取和准备 WMT2014 Europarl v7 英语-法语数据集

WMT2014 Europarl v7 英法文数据集收集了欧洲议会内部的发言,并被翻译成多种不同的语言。你可以在 https://www.statmt.org/europarl/免费参观。

要将数据集放到 Gradient 上,只需进入终端并运行

wget https://www.statmt.org/europarl/v7/fr-en.tgz
tar -xf fre-en.tgz

您还需要下载 Torch 提供的教程数据集。

wget https://download.pytorch.org/tutorial/data.zip
unzip data.zip

一旦你有了数据集,我们可以使用来自机器学习大师的杰森·布朗利创建的一些代码来快速准备和组合它们用于我们的 NMT。这个代码在笔记本里data_processing.ipynb:

# load doc into memory
def load_doc(filename):
	# open the file as read only
	file = open(filename, mode='rt', encoding='utf-8')
	# read all text
	text = file.read()
	# close the file
	file.close()
	return text

# split a loaded document into sentences
def to_sentences(doc):
	return doc.strip().split('\n')

# clean a list of lines
def clean_lines(lines):
	cleaned = list()
	# prepare regex for char filtering
	re_print = re.compile('[^%s]' % re.escape(string.printable))
	# prepare translation table for removing punctuation
	table = str.maketrans('', '', string.punctuation)
	for line in lines:
		# normalize unicode characters
		line = normalize('NFD', line).encode('ascii', 'ignore')
		line = line.decode('UTF-8')
		# tokenize on white space
		line = line.split()
		# convert to lower case
		line = [word.lower() for word in line]
		# remove punctuation from each token
		line = [word.translate(table) for word in line]
		# remove non-printable chars form each token
		line = [re_print.sub('', w) for w in line]
		# remove tokens with numbers in them
		line = [word for word in line if word.isalpha()]
		# store as string
		cleaned.append(' '.join(line))
	return cleaned

# save a list of clean sentences to file
def save_clean_sentences(sentences, filename):
	dump(sentences, open(filename, 'wb'))
	print('Saved: %s' % filename)

# load English data
filename = 'europarl-v7.fr-en.en'
doc = load_doc(filename)
sentences = to_sentences(doc)
sentences = clean_lines(sentences)
save_clean_sentences(sentences, 'english.pkl')
# spot check
for i in range(10):
	print(sentences[i])

# load French data
filename = 'europarl-v7.fr-en.fr'
doc = load_doc(filename)
sentences = to_sentences(doc)
sentences = clean_lines(sentences)
save_clean_sentences(sentences, 'french.pkl')
# spot check
for i in range(1):
	print(sentences[i])

这将获取我们的 WMT2014 数据集,并清除其中的任何标点符号、大写字母、不可打印的字符以及包含数字的标记。然后它将这些文件分开放供以后使用。

with open('french.pkl', 'rb') as f:
    fr_voc = pickle.load(f)

with open('english.pkl', 'rb') as f:
    eng_voc = pickle.load(f)

data = pd.DataFrame(zip(eng_voc, fr_voc), columns = ['English', 'French'])
data

我们可以使用pickle.load()来加载现在保存的文件,然后我们可以使用方便的 Pandas DataFrame 来合并这两个文件。

结合我们的两个数据集

为了给翻译人员创建一个更完整的数据集,让我们将现有的两个数据集结合起来。

data2 = pd.read_csv('eng-fra.txt', '\t', names = ['English', 'French']) 

我们需要从规范的 PyTorch 教程中加载原始数据集。有了这两个数据帧,我们现在可以将它们连接起来,并以 PyTorch 示例数据集使用的原始格式保存它们。

data = pd.concat([data,data2], ignore_index= True, axis = 0)

data.to_csv('eng-fra.txt')

现在,我们的数据集可以应用于我们的代码,就像规范的 PyTorch 教程!但首先,让我们看看准备数据集的步骤,看看我们可以做出哪些改进。打开笔记本seq2seq_translation_combo.ipynb并运行第一个单元格,以确保 matplotlib inline 正在工作并且导入已经完成。

from __future__ import unicode_literals, print_function, division
from io import open
import unicodedata
import string
import re
import random

import torch
import torch.nn as nn
from torch import optim
import torch.nn.functional as F

import torchtext
from torchtext.data import get_tokenizer

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

数据集准备辅助函数

SOS_token = 0
EOS_token = 1

class Lang:
    def __init__(self, name):
        self.name = name
        self.word2index = {}
        self.word2count = {}
        self.index2word = {0: "SOS", 1: "EOS"}
        self.n_words = 2  # Count SOS and EOS

    def addSentence(self, sentence):
        for word in sentence.split(' '):
            self.addWord(word)

    def addWord(self, word):
        if word not in self.word2index:
            self.word2index[word] = self.n_words
            self.word2count[word] = 1
            self.index2word[self.n_words] = word
            self.n_words += 1
        else:
            self.word2count[word] += 1

为了给翻译者处理数据集,我们可以使用这个 Lang 类为我们的 language 类提供有用的功能,比如word2indexindex2wordword2count。下一个像元也将包含用于清理原始数据集的有用函数。

def readLangs(lang1, lang2, reverse=False):
    print("Reading lines...")

    # Read the file and split into lines
    lines = open('%s-%s2.txt' % (lang1, lang2), encoding='utf-8').\
        read().strip().split('\n')
    # Split every line into pairs and normalize
    pairs = [[normalizeString(s) for s in l.split('\t')] for l in lines]

    # Reverse pairs, make Lang instances
    if reverse:
        pairs = [list(reversed(p)) for p in pairs]
        input_lang = Lang(lang2)
        output_lang = Lang(lang1)
    else:
        input_lang = Lang(lang1)
        output_lang = Lang(lang2)

    return input_lang, output_lang, pairs

接下来,readLangs 函数接收我们的 csv 来创建input_langoutput_lang,并配对我们将用来准备数据集的变量。这个函数使用助手函数来清理文本并规范化字符串。

MAX_LENGTH = 12

eng_prefixes = [
    "i am ", "i m ",
    "he is", "he s ",
    "she is", "she s ",
    "you are", "you re ",
    "we are", "we re ",
    "they are", "they re ", "I don t", "Do you", "I want", "Are you", "I have", "I think",
       "I can t", "I was", "He is", "I m not", "This is", "I just", "I didn t",
       "I am", "I thought", "I know", "Tom is", "I had", "Did you", "Have you",
       "Can you", "He was", "You don t", "I d like", "It was", "You should",
       "Would you", "I like", "It is", "She is", "You can t", "He has",
       "What do", "If you", "I need", "No one", "You are", "You have",
       "I feel", "I really", "Why don t", "I hope", "I will", "We have",
       "You re not", "You re very", "She was", "I love", "You must", "I can"]
eng_prefixes = (map(lambda x: x.lower(), eng_prefixes))
eng_prefixes = set(eng_prefixes)

def filterPair(p):
    return len(p[0].split(' ')) < MAX_LENGTH and \
        len(p[1].split(' ')) < MAX_LENGTH and \
        p[1].startswith(eng_prefixes)

def filterPairs(pairs):
    return [pair for pair in pairs if filterPair(pair)]
eng_prefixes

在 Torch 教程的另一个变化中,我扩展了英语前缀列表,以包括现在合并的数据集最常见的起始前缀。我还将max_length延长到了 12,以创建一组更健壮的数据点,但是这可能会引入更多的混杂因素。尝试将max_length降低回 10,看看性能如何变化。

def prepareData(lang1, lang2,reverse = False):
    input_lang, output_lang, pairs = readLangs(lang1, lang2, reverse)
    print("Read %s sentence pairs" % len(pairs))
    pairs = filterPairs(pairs)
    print("Trimmed to %s sentence pairs" % len(pairs))
    print("Counting words...")
    for pair in pairs:
        input_lang.addSentence(pair[0])
        output_lang.addSentence(pair[1])
    print("Counted words:")
    print(input_lang.name, input_lang.n_words)
    print(output_lang.name, output_lang.n_words)
    return input_lang, output_lang, pairs

input_lang, output_lang, pairs = prepareData('eng', 'fra', True)
print(random.choice(pairs))

最后,prepareData 函数将所有的辅助函数放在一起,筛选并最终确定 NMT 培训的语言对。现在我们的数据集已经准备好了,让我们直接进入翻译器本身的代码。

翻译

编码器

class EncoderRNN(nn.Module):
    def __init__(self, input_size, hidden_size):
        super(EncoderRNN, self).__init__()
        self.hidden_size = hidden_size

        self.embedding = nn.Embedding(input_size, hidden_size)
        self.gru = nn.GRU(hidden_size, hidden_size)

    def forward(self, input, hidden):
        embedded = self.embedding(input).view(1, 1, -1)
        output = embedded
        output, hidden = self.gru(output, hidden)
        return output, hidden

    def initHidden(self):
        return torch.zeros(1, 1, self.hidden_size, device=device)

我们使用的编码器与本教程基本相同,可能是我们在本文中要剖析的最简单的代码。我们可以从 forward 函数中看到,对于每个输入元素,编码器如何输出一个输出向量和一个隐藏状态。然后返回隐藏状态,因此可以在接下来的步骤中与输出一起使用。

注意力解码器

class AttnDecoderRNN(nn.Module):
    def __init__(self, hidden_size, output_size, dropout_p=0.1, max_length=MAX_LENGTH):
        super(AttnDecoderRNN, self).__init__()
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.dropout_p = dropout_p
        self.max_length = max_length

        self.embedding = nn.Embedding(self.output_size, self.hidden_size)
        self.attn = nn.Linear(self.hidden_size * 2, self.max_length)
        self.attn_combine = nn.Linear(self.hidden_size * 2, self.hidden_size)
        self.dropout = nn.Dropout(self.dropout_p)
        self.gru = nn.GRU(self.hidden_size, self.hidden_size)
        self.out = nn.Linear(self.hidden_size, self.output_size)

    def forward(self, input, hidden, encoder_outputs):
        embedded = self.embedding(input).view(1, 1, -1)
        embedded = self.dropout(embedded)

        attn_weights = F.softmax(
            self.attn(torch.cat((embedded[0], hidden[0]), 1)), dim=1)
        attn_applied = torch.bmm(attn_weights.unsqueeze(0),
                                 encoder_outputs.unsqueeze(0))

        output = torch.cat((embedded[0], attn_applied[0]), 1)
        output = self.attn_combine(output).unsqueeze(0)

        output = F.relu(output)
        output, hidden = self.gru(output, hidden)

        output = F.log_softmax(self.out(output[0]), dim=1)
        return output, hidden, attn_weights

    def initHidden(self):
        return torch.zeros(1, 1, self.hidden_size, device=device)

由于我们在示例中使用了注意力,让我们看看如何在解码器中实现它。显而易见,编码器和解码器网络之间存在一些关键差异,远远不止是它们行为的简单反转。

首先,init()函数有另外两个参数:max_lengthdropout_pmax_length是一个句子所能容纳的最大元素数量。我们这样设置是因为两个配对数据集中句子长度的差异很大。dropout_p用于帮助调节和防止神经元的共同适应。

第二,我们有注意力层本身。在每一步,注意层接收注意输入、解码器状态和所有编码器状态。它用这个来计算注意力分数。对于每个编码器状态,attention 计算其与该解码器状态的“相关性”。它应用注意函数,该函数接收一个解码器状态和一个编码器状态,并返回标量值。注意力分数用于计算注意力权重。这些权重是通过将softmax应用于注意力分数而创建的概率分布。最后,它将注意力输出计算为编码器状态与注意力权重的加权和。 (1)

这些额外的参数和注意机制使得解码器需要少得多的训练和全部信息来理解序列中所有单词的关系。

培养

teacher_forcing_ratio = 0.5

def train(input_tensor, target_tensor, encoder, decoder, encoder_optimizer, decoder_optimizer, criterion, max_length=MAX_LENGTH):
    encoder_hidden = encoder.initHidden()

    encoder_optimizer.zero_grad()
    decoder_optimizer.zero_grad()

    input_length = input_tensor.size(0)
    target_length = target_tensor.size(0)

    encoder_outputs = torch.zeros(max_length, encoder.hidden_size, device=device)

    loss = 0

    for ei in range(input_length):
        encoder_output, encoder_hidden = encoder(
            input_tensor[ei], encoder_hidden)
        encoder_outputs[ei] = encoder_output[0, 0]

    decoder_input = torch.tensor([[SOS_token]], device=device)

    decoder_hidden = encoder_hidden

    use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False

    if use_teacher_forcing:
        # Teacher forcing: Feed the target as the next input
        for di in range(target_length):
            decoder_output, decoder_hidden, decoder_attention = decoder(
                decoder_input, decoder_hidden, encoder_outputs)
            loss += criterion(decoder_output, target_tensor[di])
            decoder_input = target_tensor[di]  # Teacher forcing

    else:
        # Without teacher forcing: use its own predictions as the next input
        for di in range(target_length):
            decoder_output, decoder_hidden, decoder_attention = decoder(
                decoder_input, decoder_hidden, encoder_outputs)
            topv, topi = decoder_output.topk(1)
            decoder_input = topi.squeeze().detach()  # detach from history as input

            loss += criterion(decoder_output, target_tensor[di])
            if decoder_input.item() == EOS_token:
                break

    loss.backward()

    encoder_optimizer.step()
    decoder_optimizer.step()

    return loss.item() / target_length

我们使用的训练函数接受几个参数。input_tensortarget_tensor分别是句子对的第 0 和第 1 个索引。编码器是上述的编码器。解码器是上述注意力解码器。我们正在将编码器和解码器优化器从随机梯度下降切换到 Adagrad,因为我们发现使用 Adagrad 时,翻译具有更低的损失。最后,我们使用交叉熵损失作为我们的标准,而不是教程中使用的神经网络。NLLLoss()。

还要看老师逼比。该值设置为 0.5,用于帮助提高模型的功效。在. 5 处,它随机确定是否将目标作为下一个输入提供给解码器或者使用解码器自己的预测。这可以帮助平移更快地收敛,但也可能导致不稳定。例如,过度使用教师强制可能会创建一个输出具有准确语法但与输入没有翻译关系的模型。

def trainIters(encoder, decoder, n_iters, print_every=1000, plot_every=100):
    start = time.time()
    plot_losses = []
    print_loss_total = 0  # Reset every print_every
    plot_loss_total = 0  # Reset every plot_every

    encoder_optimizer = optim.Adagrad(encoder.parameters())
    decoder_optimizer = optim.Adagrad(decoder.parameters())
    training_pairs = [tensorsFromPair(random.choice(pairs))
                      for i in range(n_iters)]
    criterion = nn.CrossEntropyLoss()

    for iter in range(1, n_iters + 1):
        training_pair = training_pairs[iter - 1]
        input_tensor = training_pair[0]
        target_tensor = training_pair[1]

        loss = train(input_tensor, target_tensor, encoder,
                     decoder, encoder_optimizer, decoder_optimizer, criterion)
        print_loss_total += loss
        plot_loss_total += loss

        if iter % print_every == 0:
            print_loss_avg = print_loss_total / print_every
            print_loss_total = 0
            print('%s (%d %d%%) %.4f' % (timeSince(start, iter / n_iters),
                                         iter, iter / n_iters * 100, print_loss_avg))

        if iter % plot_every == 0:
            plot_loss_avg = plot_loss_total / plot_every
            plot_losses.append(plot_loss_avg)
            plot_loss_total = 0

    showPlot(plot_losses)

TrainIters 实际上实现了培训过程。对于预设的迭代次数,它将计算损失。该函数继续保存损失值,以便在训练完成后可以有效地绘制它们。

要更深入地了解这个翻译器的代码,请务必查看包含所有这些信息的 Gradient 笔记本演示以及 Github 页面。

翻译

hidden_size = 256
encoder = EncoderRNN(input_lang.n_words, hidden_size).to(device)
attn_decoder = AttnDecoderRNN(hidden_size, output_lang.n_words, dropout_p=0.1).to(device)

trainIters(encoder1, attn_decoder1, 75000, print_every=5000)

现在我们已经设置了我们的翻译器,我们需要做的就是实例化我们的编码器和注意力解码器模型用于训练并执行 trainIters 函数。确保在训练单元之前运行笔记本中的所有单元,以启用辅助功能。

我们将使用 256 的隐藏大小,并确保您的设备设置为device(type='cuda')。这将确保 RNN 使用 GPU 训练。

当您运行此单元时,您的模型将训练 75,000 次迭代。培训完成后,使用提供的评估功能来评估您的新翻译模型的质量性能。这里有一个例子,说明我们为演示而训练的模型如何在随机抽样的翻译上表现。

结束语

现在,您应该能够获取任何适当的翻译数据集,并将其插入到这个翻译器代码中。我推荐从其他的 WMT 欧帕尔配对开始,但是有无限的选择。一定要在 Gradient 上运行,以获得强大的 GPU!

如果您克隆 Github repo 或将其用作 Gradient 中的工作区 URL,您可以访问包含本文代码的笔记本。有三本笔记本。数据处理是你首先要进入并运行的笔记本。然后是本文中用于在 Europarl 法语-英语数据集上实现 Seq2Seq 翻译的代码,

使用 Paperspace + Parsec 设置您的云游戏平台

原文:https://blog.paperspace.com/setting-up-your-cloud-gaming-rig-with-paperspace-parsec/

5 分钟设置指南

**1。**创建新的 Paperspace 机器

登录到 Paperspace 后,创建一台新机器。

  • 选择 Parsec 模板
  • 选择您的机器类型
  • 选择您想要的区域
  • 选择您的存储空间 —您可以随时增加存储空间
  • 点击创建

**2。**在新机器上启动 Parsec 应用程序

  • 当机器显示就绪时,在浏览器中打开新的 Paperspace 机器。
  • 登录 Parsec 应用程序。单击以保存您的 Parsec 密码,用于自动登录。
  • 出现提示时,选择允许连接到这台计算机

注意:使用 Parsec 时,不要同时使用 Paperspace 流。

**3。**在本地安装 Parsec 客户端

关闭 Paperspace,在本地计算机上下载 Parsec 客户端parsec.tv/downloads

**就是这样!**与您的朋友共享 Paperspace 并获得免费积分。您唯一的推荐代码可以在控制台的 账号标签下找到。


安装说明

Parsec 客户端系统要求

Parsec 可用于任何运行 macOS 10.9+的 Mac,任何运行 Windows 7+或 Raspberry Pi 3 的具有该硬件的 PC。

机器类型

Parsec 只会在专用 GPU 机器类型上工作。

深度卷积神经网络(SA-Net)的混洗注意

原文:https://blog.paperspace.com/shuffle-attention-sanet/

如果一位来自前十年的深度学习研究人员穿越时间来到今天,并询问当前大多数研究都集中在什么主题上,可以很有把握地说注意力机制将位于该列表的首位。注意机制在自然语言处理(NLP)和计算机视觉(CV)中都占据了主导地位。在 NLP 中,重点是让类似 transformer 的架构更加高效,而在 CV 中,则更多的是通过即插即用模块来推动 SOTA。

这篇文章的主题也是为了同样的目的。我们正在谈论 ICASSP 2021 年题为 SA-Net:深度卷积神经网络的 Shuffle Attention。

该论文提出了一种称为 混洗注意力 的新颖注意力机制,该机制可用于传统骨干网,以最小的计算成本提高性能。在本文中,我们将通过讨论随机注意力背后的动机来开始这一仪式,然后是对网络的结构性剖析,最后以论文中展示的结果及其代码来结束本文。

目录

  1. 动机
  2. 转移注意力
  3. 密码
  4. 结果
  5. 结论
  6. 参考

论文摘要

注意机制使神经网络能够准确地关注输入的所有相关元素,已经成为提高深度神经网络性能的必要组件。在计算机视觉研究中,主要有两种广泛使用的注意机制,空间注意和通道注意,它们分别旨在捕捉像素级的成对关系和通道依赖性。虽然将它们融合在一起可能比它们单独的实现获得更好的性能,但这将不可避免地增加计算开销。本文提出了一种有效的混洗注意模块来解决这个问题,该模块采用混洗单元来有效地结合两种注意机制。具体来说,SA 首先将通道维度分组为多个子特征,然后并行处理它们。然后,对于每个子特征,SA 利用混洗单元来描述空间和通道维度上的特征依赖性。之后,所有子特征被聚集,并且采用“信道混洗”算子来实现不同子特征之间的信息通信。所提出的 SA 模块是高效且有效的,例如,针对主干 ResNet50 的 SA 的参数和计算分别是 300 对 25.56M 和 2.76e-3 GFLOPs 对 4.12 GFLOPs,并且就 Top-1 准确度而言,性能提升超过 1.34%。在常用基准(包括用于分类的 ImageNet-1k、用于对象检测的 MS COCO 和实例分割)上的大量实验结果表明,所提出的模拟退火算法通过实现更高的精度和更低的模型复杂度而显著优于当前的 SOTA 方法。

动机

在过去的几年里,注意力机制吸引了深度学习领域许多研究人员的注意,并在今天的研究社区中占据了主导地位。在计算机视觉领域(主要在图像分类和物体识别领域),像挤压和激励网络这样的即插即用模块已经成为更多关注机制的基础。从 CBAMECA-Net ,目标一直是提供一个低成本的模块来增加更具代表性的能力,以提高深度卷积神经网络的性能。这些注意机制大多分为两个不同的类别:通道注意和空间注意。前者在特征映射张量的通道维度上聚集特征,而后者在特征映射张量中在每个通道的空间维度上聚集信息。然而,这些类型的注意机制没有充分利用空间和通道注意之间的相关性,这使得它们的效率较低。

本文旨在回答以下问题:

我们能不能以一种更轻但更有效的方式融合不同的注意力模块?

作者从三个基本概念中获得灵感,回答了上述问题:

  1. 多分支架构
  2. 分组特征
  3. 注意机制

多分支架构

作者从流行的 ShuffleNet v2 架构中获得灵感,该架构有效地构建了多分支结构并并行处理不同的分支。确切地说,输入被分成两个分支,每个分支有$\frac{2}$个通道,其中$c$是通道总数。然后,这两个分支在通过级联合并形成最终输出之前,经过后续卷积层。

分组特征

作者依赖于空间分组增强(SGE) 注意机制的思想,该机制引入了一种分组策略,该策略将输入特征图张量沿通道维度划分成组。然后,通过一系列操作并行增强这些组。

注意机制

与其说是灵感,不如说是文献综述,作者们依次回顾了现有的将通道和空间注意相结合的注意机制,如 CBAM 、GCNet 和 SGE。

转移注意力

把无序注意力看作是来自 SE 通道注意力、来自 CBAM 的空间注意力和来自 SGE 的通道分组的混合体。随机注意力有四个组成部分:

  1. 特征分组
  2. 渠道关注
  3. 空间注意力
  4. 聚合

特征分组

随机注意力(SA)中的功能分组属性是两级层次结构。假设注意模块的输入张量是$\ textbf \ in \mathbb{c \ ast h \ ast w } $,其中$c$表示通道维度,而$h \ ast w$表示每个特征图的空间维度,则 SA 首先沿着通道维度将$\ textbf $分成$g$组,使得每个组现在变成$\ tilde { \ textbf } \ in \mathbb{\frac \ ast h \ ast w }$。这些特征组然后被传递到注意模块,在那里它们沿着通道维度被进一步分成两组,每组用于空间和通道注意分支。因此,通过每个空间或通道注意分支的子特征组可以用$\ hat { \ textbf } \ in \mathbb^{\frac{2g} \ ast h \ ast w }$表示。

渠道关注

对于渠道关注分支,SA 首先通过应用全球平均池(GAP)层将$\hat{\textbf}\(减少到\){\frac{2G} \ast 1 \ast 1}。简单的选通机制使用一个紧凑的功能来实现精确和自适应选择的指导,然后是一个 sigmoid 激活功能。这可以用下面的数学公式来表示:

\(= \ sigma(\ \ mathematical { f } _ { c })\ CDO \ hat { \ textf { x } = \ sigma(w _ { 1s }+b _ { 1 })\ CDO \ hat { \ textf { x }\)

其中,$ w _ { 1 } \在\mathbb{\frac{2g} \ ast 1 \ ast 1 } \(和\) b _ { 1 } \在\mathbb{\frac{2g} \ ast 1 \ ast 1 } \(是用于缩放和移动间隙的参数(\)\hat{\textbf}$)。

空间注意力

对于空间注意力,输入$\hat{\textbf}\(被减少组范数以获得空间方面的统计。随后,\)\mathcal_(\cdot)$用于增强约化张量的表示。这可以用下面的数学公式来表示:

= \ sigma(w _ { 2 } \ CDO gn(\ \ hat { \ textf )+b _ 2)\ CDO \ hat { \ textf $)

聚合

空间注意和通道注意分支的输出首先被连接。与 ShuffleNet v2 类似,在级联之后,采用信道混洗策略来实现沿着信道维度的跨组信息流。因此,最终输出与 SA 层的输入张量具有相同的维数。

密码

以下代码片段提供了 PyTorch 中 SA 层的结构定义。

import torch
import torch.nn as nn
from torch.nn.parameter import Parameter

class sa_layer(nn.Module):
    """Constructs a Channel Spatial Group module.
    Args:
        k_size: Adaptive selection of kernel size
    """

    def __init__(self, channel, groups=64):
        super(sa_layer, self).__init__()
        self.groups = groups
        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        self.cweight = Parameter(torch.zeros(1, channel // (2 * groups), 1, 1))
        self.cbias = Parameter(torch.ones(1, channel // (2 * groups), 1, 1))
        self.sweight = Parameter(torch.zeros(1, channel // (2 * groups), 1, 1))
        self.sbias = Parameter(torch.ones(1, channel // (2 * groups), 1, 1))

        self.sigmoid = nn.Sigmoid()
        self.gn = nn.GroupNorm(channel // (2 * groups), channel // (2 * groups))

    @staticmethod
    def channel_shuffle(x, groups):
        b, c, h, w = x.shape

        x = x.reshape(b, groups, -1, h, w)
        x = x.permute(0, 2, 1, 3, 4)

        # flatten
        x = x.reshape(b, -1, h, w)

        return x

    def forward(self, x):
        b, c, h, w = x.shape

        x = x.reshape(b * self.groups, -1, h, w)
        x_0, x_1 = x.chunk(2, dim=1)

        # channel attention
        xn = self.avg_pool(x_0)
        xn = self.cweight * xn + self.cbias
        xn = x_0 * self.sigmoid(xn)

        # spatial attention
        xs = self.gn(x_1)
        xs = self.sweight * xs + self.sbias
        xs = x_1 * self.sigmoid(xs)

        # concatenate along channel axis
        out = torch.cat([xn, xs], dim=1)
        out = out.reshape(b, -1, h, w)

        out = self.channel_shuffle(out, 2)
        return out

结果

ImageNet-1k 分类

MS-COCO 对象检测

MS-COCO 实例分割

GradCAM 可视化

结论

无序注意力可能是最接近于在注意力机制提供的计算开销和性能提升之间实现正确平衡的方式。该文件提供了坚实的重要结果以及良好的背景直觉,以支持设计选择。看到 SA 层在更复杂和困难的任务中接受测试将会很有趣。

参考

  1. SA-Net:深度卷积神经网络的注意力转移
  2. SA-Net 官方 GitHub 资源库

用搅拌机制作 SMPL 模型

原文:https://blog.paperspace.com/smpl-models-with-blender/

Photo by Ellen Qin / Unsplash

对于不同类型的图形设计,人体模型有很多解释。为现有的软件和图形管道构建类似于更人性化的模型的 3d 结构是设计的主要方面之一。蒙皮多人线性(SMPL)模型是一个基于蒙皮顶点的模型,它精确地表示自然人体姿态中的各种身体形状。SMPL 模型代表了现实人类模型的一些最佳作品,因为它们与当前用于图形生成的大多数流行平台兼容,如 Blender、Maya、Unreal Engine 和 Unity。这种高度的兼容性使得许多成功的工作有可能用这些 SMPL 模型来完成。

在之前关于使用 Blender 进行 3d 建模的文章中,我们了解了 Blender 的大部分基本方面,例如众多的工具、代码、功能、属性和其他重要主题。我们还探索了向特定模型分配和添加背景图像的领域。除了添加各自的背景,我们还学习了添加理想纹理的过程,以使我们的模型看起来更具视觉吸引力和美感。我们将利用这些先前概念的知识,并将它们应用到当前的 SMPL 模型理论中。

在上一篇关于 3d 建模的文章中,我们也详细讨论了模型的动画。然而,我们不会把重点放在 SMPL 模型的动画方面。我们将学习导入皮肤的多人线性模型到 Blender 环境中,并做相应的添加。下面是我们将在本文中涉及的主题类型的目录列表。查看它以理解我们将在这篇文章中学习的关于使用*“带搅拌机的 SMPL 模型”*的基本概念。

目录:

  • 介绍
  • 创建和生成 SMPL 模型
  • 了解如何在 Blender 中使用 SMPL 模型
  • 使用 Python 和 Blender
    1 执行大量任务。进口 SMPL 车型
    2。给相应的模型
    3 添加背景图片。添加理想的纹理
    4。渲染图像
    5。添加多摄像机视图
  • 在 Blender 中添加多个纹理
  • 结论

简介:

蒙皮多人线性(SMPL)是一个真实的三维人体模型,它基于从数千次三维人体扫描中学习到的蒙皮和形状混合。构建 SMPL 模型的主要想法是确保有一个公共平台,这些现实的人类模型可以在这个平台上找到它们的效用,即在 Blender、Maya、Unreal Engine 和 Unity 等流行软件中。为了构建这些模型,使用了混合蒙皮、自动装配、学习姿势模型、学习姿势形状和学习混合形状的概念,以确保产生最佳结果。关于这些主题和 SMPL 模型的更多信息,我建议从下面的链接查看关于“ SMPL:一个带皮肤的多人线性模型”的官方研究论文。

在本文中,我们将重点介绍如何在 GitHub 参考的帮助下创建和生成 SMPL 模型,然后学习将这些模型导入 Blender 环境的过程。一旦我们完成了这些基本程序,我们将开始调整我们从以前的文章中获得的知识,并开始实现 SMPL 模型的一些背景细节,以及尝试给下面的模型添加一些纹理。我们将尝试只使用 Python 代码来执行我们讨论过的所有期望的操作。添加多个纹理的最后一个动作可以在 Blender 工具的帮助下进行,以获得更高的精度,并添加不同类型的颜色。最后,我们将看看这些模型可能实现的一些现有和未来的工作。


创建和生成 SMPL 模型:

要使用 SMPL 模型,您可以做的第一步是通过此链接访问以下官方网站。如果您尚未注册,请务必注册并登录网站。在这里,您将有机会下载具有不同姿势和参数的蒙皮多人线性模型的数据集。最好是找到。obj 文件或将它们从现有格式转换为。obj 格式,以确保 Blender 可以更轻松地访问这些文件。如果您在系统上成功下载或安装这些模型的过程中遇到问题,我建议您尝试本节中提到的下一种方法。

对于创建和生成 SMPL 模型,我的建议是利用以下 GitHub 链接,这些链接来自开发人员,他们在解释多种格式的 SMPL 模型方面做了大量工作,使其在 numpy 版本以及 TensorFlow 深度学习框架和 PyTorch 深度学习框架中可用,用于 GPU 计算。大部分代码都是在 Ubuntu 平台上测试的。如果你使用 Windows 平台进行大部分的 Blender 操作,你可以使用一个虚拟的盒子来测试 Ubuntu 环境。GitHub 链接中提到的大多数步骤都是不言自明的,但是您需要确保您的测试设备上有 Python 的要求和 GitHub 存储库的副本。

一旦您完成了所有的安装、创建和提取 GitHub 存储库,您就可以继续创建一个虚拟环境来存储这个任务的所有基本需求。一旦创建了虚拟环境并在其中安装了所有必需的依赖项,请确保为特定任务激活了虚拟环境。将目录更改为下载 GitHub repo,首先运行预处理 Python 脚本。如果您在特定平台上没有 GPU 支持,您可以运行 smpl_np.py 脚本来相应地生成男性或女性模型。模型将在。obj 格式,现在可以导入 Blender 平台了。

source ./ss_h_mesh_venv/bin/activate (In the previous folder or the directory you created your venv)

cd SMPL-master/

python preprocess.py

python smpl_np.py 

如果您的平台上安装了 GPU,您可以运行 TensorFlow 脚本,而不是 Numpy 变体。请注意,您可以在脚本中做一些小的改动,以获得 SMPL 模特的不同姿势。

python smpl_tf.py 

如果你在破译这一步有困难,并且不能生成你想要的模型,将会有一个默认的男性和女性模型的附件,这样你就可以根据需要继续完成文章的其余部分。


了解如何在 Blender 中使用 SMPL 模型:

现在我们已经简单了解了如何创建和生成不同姿态的 SMPL 模型,我们可以继续了解如何导入 SMPL 模型。我们知道这些文件保存为。系统中的 obj 文件。在 Windows 平台上,您会注意到这些文件被保存为 3D 对象,单击属性时,特定格式显示为. obj 文件。本文的大部分内容将集中在如何使用 Python 脚本来处理 SMPL 模型。然而,对于导入模型和其他类似的步骤,我们还将介绍如何通过使用 Blender 平台及其提供的众多功能来完成以下步骤。

首先,让我们了解借助 Python 脚本将 SMPL 模型导入 Blender 平台的代码结构。下面的代码块解释了如何实现下面的任务。在文本编辑器部分,我们用格式。py 并继续导入必要的库,如 bpy 和数学库,它们将帮助我们完成本文中几乎所有的必要任务。第一步是删除搅拌机屏幕的默认立方体。这个过程有两种可能。注释行根据需要执行操作,但是如果您试图多次运行该脚本,您更愿意检查是否有要删除的多维数据集,并且只在存在多维数据集时才删除它。导入所需 SMPL 模型的最后一步非常简单,因为您需要指定将代码连接到男性或女性模型的存储位置的路径。

import bpy
from bpy import context, data, ops
import math

###  Remove The Default Cude Object
# bpy.ops.object.delete(use_global=False)

for o in bpy.context.scene.objects:
    if o.name == "Cube":
        bpy.ops.object.delete(use_global=False)

### Import The SMPL Model
file_loc = 'Your Path to smpl_male.obj or smpl_female.obj'
imported_object = bpy.ops.import_scene.obj(filepath=file_loc)
obj_object = bpy.context.selected_objects[0] 
print('Imported name: ', obj_object.name)

如果您想将模型直接导入到您的 Blender 环境中,那么这个过程也非常简单,只要您知道存储特定模型的位置或路径。进入 Blender 平台并选择一个常规项目后,删除默认立方体。在以前的文章中已经多次讨论了删除的过程。请随意选择最适合你的方法。删除默认多维数据集后,从主菜单栏中选择该文件。选择要导入的选项,并从各种可用选项中选择波前(。obj)选项。浏览并选择您想要的型号,它应该会出现在 Blender 屏幕上,如下图所示。

导入的模型将需要一些重新调整和缩放。我们将在下一节介绍如何有效地执行以下步骤。


使用 Python 和 Blender 执行大量任务:

在本文的这一部分,我们将重点关注在 SMPL 模型上执行各种任务。首先,我们将从将 SMPL 模型导入 Blender 平台的过程开始。一旦我们成功地导入了 SMPL 模型,我们将开始为下面的模型添加一些重要的特性。我们将添加一个背景图像到相应的模型,然后继续添加一个理想的纹理。纹理很可能代表典型的肤色。

一旦我们完成了这些任务,我们就可以从多个角度来观察我们最终的渲染图像。在多个摄像机视图的帮助下,这一任务是可能的。本文的这一部分更侧重于在代码的帮助下完成预期的任务。然而,我将简单地介绍一下如何利用现有的 Blender 工具来完成这些任务。但是问题是你不能复制和构建许多不同结构的模型,因为没有代码的工作的复杂性会显著增加。

导入 SMPL 模型:

在上一节中,我们已经详细介绍了如何将 SMPL 模型导入 Blender 平台。代码摘要如下面的代码片段所示。

import bpy
from bpy import context, data, ops
import math

###  Remove The Default Cude Object
# bpy.ops.object.delete(use_global=False)

for o in bpy.context.scene.objects:
    if o.name == "Cube":
        bpy.ops.object.delete(use_global=False)

### Import The SMPL Model
file_loc = 'Your Path to smpl_male.obj or smpl_female.obj'
imported_object = bpy.ops.import_scene.obj(filepath=file_loc)
obj_object = bpy.context.selected_objects[0] 
print('Imported name: ', obj_object.name)

下一个重要步骤是调整模型的位置,相应地旋转模型以匹配相机的最佳视图,最后将模型缩放到合适的大小,以便我们可以清楚地查看模型。在 Python 脚本的帮助下,可以轻松执行以下操作。需要选择分配给模型的创建对象,我们可以相应地改变所有需要的参数。位置、旋转欧拉角和缩放参数在 x 轴、y 轴和 z 轴上都是可变的。一旦您在特定的轴上改变了这些属性,您将会得到一个更加令人满意的 SMPL 的修改,这对于我们在本文中试图完成的大多数任务来说都是非常有用的。

# Locations
obj_object.location.x = 0
obj_object.location.y = 0
obj_object.location.z = 0.5

# Rotations
obj_object.rotation_euler[0] = math.radians(90)
obj_object.rotation_euler[1] = math.radians(-0)
obj_object.rotation_euler[2] = math.radians(60)

# Scale
obj_object.scale.x = 2
obj_object.scale.y = 2
obj_object.scale.z = 2

如果你试图用 Blender 中分配的工具来完成下面的步骤,这个过程非常简单。在屏幕上选择 SMPL 模型后,转到属性布局。默认情况下,应选择对象属性。但是,如果不是,您可以相应地调整位置、旋转和缩放的所有参数。您可以借助鼠标手动调整模型的位置,借助键盘上的' R 键沿所需轴调整旋转角度,或借助键盘上的' S 键调整模型的比例,并沿所有轴均匀缩放。

向相应的模型添加背景图像:

我们将关注的下一步是给相应的模型添加一个背景图像。完成以下过程的步骤在我之前的一篇文章中有详细介绍,这篇文章讲述了向 3d 模型添加背景图像和纹理的基础知识。即使是 SMPL 模式,过程也基本相同。让我们重温下面的代码片段,以便更好地理解这个概念。

为了给图像添加所需的背景,第一步是选择我们将用来观看 SMPL 模型的摄像机。一旦选择了摄像机,我们就可以指定包含男性或女性模特位置的路径。一旦我们将背景图像赋给一个变量,我们就可以开始堆肥的过程了。我们已经在以前的一篇关于 3d 建模的文章中广泛地讨论了这个主题。将 UI 类型的区域设置为合成器节点树。这一步将布局更改到我们可以构建节点来相应地连接我们想要的功能的地方。删除所有默认节点,并开始构建具有适当连接的新节点。每个节点的位置不是强制性的。然而,当观察任何不匹配时,下面的放置看起来更美观。观察下面的代码片段,以防您陷入编码过程。

### Background Image
cam = bpy.context.scene.camera
filepath = "background image path"

img = bpy.data.images.load(filepath)
cam.data.show_background_images = True
bg = cam.data.background_images.new()
bg.image = img
bpy.context.scene.render.film_transparent = True

### Composting

bpy.context.area.ui_type = 'CompositorNodeTree'

#scene = bpy.context.scene
#nodetree = scene.node_tree
bpy.context.scene.use_nodes = True
tree = bpy.context.scene.node_tree

for every_node in tree.nodes:
    tree.nodes.remove(every_node)

RenderLayers_node = tree.nodes.new('CompositorNodeRLayers')   
RenderLayers_node.location = -300,300

comp_node = tree.nodes.new('CompositorNodeComposite')   
comp_node.location = 400,300

AplhaOver_node = tree.nodes.new(type="CompositorNodeAlphaOver")
AplhaOver_node.location = 150,450

Scale_node = tree.nodes.new(type="CompositorNodeScale")
bpy.data.scenes["Scene"].node_tree.nodes["Scale"].space = 'RENDER_SIZE'
Scale_node.location = -150,500

Image_node = tree.nodes.new(type="CompositorNodeImage")
Image_node.image = img  
Image_node.location = -550,500

links = tree.links
link1 = links.new(RenderLayers_node.outputs[0], AplhaOver_node.inputs[2])
link2 = links.new(AplhaOver_node.outputs[0], comp_node.inputs[0])
link3 = links.new(Scale_node.outputs[0], AplhaOver_node.inputs[1])
link4 = links.new(Image_node.outputs[0], Scale_node.inputs[0])

bpy.context.area.ui_type = 'TEXT_EDITOR'

一旦所有的连接都按讨论的那样完成,就必须将 UI 布局的区域从合成部分转换回文本编辑器,以继续本部分中所有剩余任务的 Python 脚本编写过程。您可以按键盘上的“ Numpad 0 ”在各自的相机视图中查看 SMPL 模型。借助 Python 脚本执行的所有步骤也可以用必要的 Blender 工具复制,以获得理想的结果。然而,每次您想要构建一个模型而不是复制粘贴一个脚本或代码片段时,构建这些步骤的复杂性更加麻烦。让我们继续分析如何给我们的 SMPL 模型添加纹理。

添加理想的纹理:

既然我们已经成功地导入了模型并添加了背景,那么给模型添加一个新的纹理使它看起来更真实就变得非常重要了。借助 Python 脚本添加单个纹理的过程非常简单。您可以创建材质的名称并将其存储为所需的变量,然后继续创建节点来定义您的特定属性。您还可以选择下载您认为最适合您的 SMPL 模型的自定义皮肤颜色,并创建相应的节点链接来映射它们。整个模型将使用下面代码片段中提到的代码块导入您在本节中添加的纹理。您可以相应地随意修改、更改和试验代码!

### Adding Textures

mat = bpy.data.materials.new(name="New_Mat")
mat.use_nodes = True
bsdf = mat.node_tree.nodes["Principled BSDF"]
texImage = mat.node_tree.nodes.new('ShaderNodeTexImage')
texImage.image = bpy.data.images.load("Any texture like a skin color")
mat.node_tree.links.new(bsdf.inputs['Base Color'], texImage.outputs['Color'])

#ob = context.view_layer.objects.active

# Assign it to object
if obj_object.data.materials:
    obj_object.data.materials[0] = mat
else:
    obj_object.data.materials.append(mat)

# Change the ViewPort Shading to RENDERED    
for area in bpy.context.screen.areas: 
    if area.type == 'VIEW_3D':
        for space in area.spaces: 
            if space.type == 'VIEW_3D':
                space.shading.type = 'RENDERED'

要在不使用任何代码的情况下完成以下步骤,您可以保持对象处于选中状态,并将属性部分更改为材质属性布局,然后通过指定所需的材质来添加所需的皮肤位置或您选择的任何其他颜色。要查看指定的颜色,需要将视口视图从实心状态切换到材质预览模式或渲染模式。所有其他属性都可以根据用户的需要进行操作和更改。然而,正如上一节所讨论的,我们可以注意到,对于创建多个模型来说,手动计算这些步骤而不是使用 Python 脚本可能非常复杂。现在让我们继续了解如何渲染 SMPL 模型,并将其保存在所需的路径位置。

渲染图像:

创建模型并将其保存在所需位置的最后一步是渲染过程。在之前的文章中,我们已经多次讨论过渲染过程。定义渲染上下文,并在相应的变量中缩放分辨率百分比。在下一步中,我们可以定义保存相应模型的路径。确保图像的格式是常用的格式之一,如。png 或. jpg。当我们完成图像渲染后,我们可以将图像保存为所需的格式,并恢复之前的路径,以便进一步编写脚本和进行计算。

### Rendering Procedure
render = bpy.context.scene.render
scale = render.resolution_percentage / 100

FILE_NAME = "figure2.png"
FILE_PATH = "Save Path"

# Save Previous Path
previous_path = bpy.context.scene.render.filepath

# Render Image
bpy.context.scene.render.filepath = FILE_PATH
bpy.ops.render.render(write_still=True)

# Restore Previous Path
bpy.context.scene.render.filepath = previous_path

您也可以选择使用 Blender 平台中提供的工具直接渲染图像,方法是在主菜单栏中选择要渲染的选项,然后单击渲染图像选项。确保选择相应的摄像机来执行渲染操作。也可以直接点击键盘上的 F12 按钮渲染图像。但是,如果您有多个摄像机,这个过程会非常复杂,因为您在摄像机切换过程中的工作量会增加。要在多台摄像机之间切换并拥有多摄像头视图,请选择所需的摄像机,然后单击键盘上的“ctrl”+“numpad 0”。值得注意的是,创建多个这样的项目的脚本过程非常简单。

添加多摄像机视图:

在本文的最后一节,我们将学习如何创建多摄像头视图,并查看 SMPL 模型以及相应的背景细节和纹理。要创建新的摄像机,选择摄像机角度,并给它一个您选择的名称。根据所需的值设置镜头,并创建您的对象。您可以设置位置、旋转角度,并将所有对象链接到创建的相机。渲染过程也可以再次完成,类似于上一节中提到的步骤。您可以在不同的位置创建多个这样的相机,并捕捉模型的许多视图和透视图。按照下面显示的代码块,可以相应地更改摄像机的变量和名称。

### Creating A New Camera Angle
scn = bpy.context.scene

# create the second camera
cam2 = bpy.data.cameras.new("Camera 2")
cam2.lens = 50

# create the second camera object
cam_obj2 = bpy.data.objects.new("Camera 2", cam2)

# Set Location 
cam_obj2.location.x = -1.43
cam_obj2.location.y = -11
cam_obj2.location.z = 6.46

# Set Angles
cam_obj2.rotation_euler[0] = math.radians(62)
cam_obj2.rotation_euler[1] = math.radians(-0.01)
cam_obj2.rotation_euler[2] = math.radians(-6.89)

scn.collection.objects.link(cam_obj2)

### Rendering Procedure
render = bpy.context.scene.render
scale = render.resolution_percentage / 100

FILE_NAME = "24.png"
FILE_PATH = "Save Path"

# Save Previous Path
previous_path = bpy.context.scene.render.filepath

# Render Image
bpy.context.scene.render.filepath = FILE_PATH
bpy.ops.render.render(write_still=True)

# Restore Previous Path
bpy.context.scene.render.filepath = previous_path

# Set the Camera 2 to active camera
bpy.context.scene.camera = bpy.data.objects["Camera 2"]

cam2 = bpy.context.scene.camera
filepath = "Background Path"

img = bpy.data.images.load(filepath)
cam2.data.show_background_images = True
bg = cam2.data.background_images.new()
bg.image = img
bpy.context.scene.render.film_transparent = True

现在,我们已经介绍了如何在 Blender 中处理 SMPL 模型的几乎所有重要的基本方面,您可以发现和探索更多的实验,以更好地理解如何使用这些模型。在本文的下一节中,我们将研究这样一种改进,您可以通过向模型添加多种纹理来对 SMPL 模型进行改进,以获得更好的美学外观。


在 Blender 中添加多个纹理:

在本文的这一部分中,我们将研究一些可以添加到我们的 SMPL 模型中的额外改进。我们可以做的一个改进是添加许多不同类型的纹理。我们可以将多种皮肤颜色(如上图所示)或不同种类的纹理结合到 SMPL 模型中,以达到多种目的。以下任务也可以通过 UV 映射在编码和 Python 脚本的帮助下完成,但您也可以在 Blender 工具的帮助下完成以下任务。

为了精确计算添加多个纹理的后续部分,我建议切换 Blender 布局屏幕右上角的 x 射线模式,并从实体视口材质预览切换到渲染状态。在左上角的屏幕上,从对象模式切换到编辑模式,这样你就可以开始对你的 SMPL 模型的纹理做一些改变。下一步是在材质属性布局中添加你选择的多种纹理和颜色阴影。你可以使用默认的颜色阴影,或者你已经下载的皮肤/其他纹理。

现在,您可以继续用鼠标选择特定区域,并为这些特定区域和部分指定您选择的纹理或颜色。确保放大 SMPL 模型,以便可以更近距离地看到这些区域,然后可以更精确地选择所需的区域。这种多纹理模型的一个例子如上图所示。您可以使用这些模型进行许多实验,强烈建议您尝试它们的各种变体。


结论:

The Blue-faced Parakeet

Photo by The New York Public Library / Unsplash

为了获得适用于众多软件设计平台的最佳架构类型,已经有了一些人类模型的创造。对于生成逼真的人体模型,诸如混合蒙皮、自动装配、学习姿势模型、学习姿势形状和学习混合形状等主题具有重要意义。有了这些概念的绝对理论和实践知识,就可以生成现实的人体模型结构。这些生成的人体模型有助于创建各种逼真和似是而非的人体形状和结构,这些形状和结构在许多作品和项目中有很大的用途。

在本文中,我们讨论了与多人线性(SMPL)模型相关的大部分次要方面。我们理解创建和生成具有真实人体结构的 SMPL 模型的概念,以便它们可以用于许多训练过程。然后我们理解了用代码或者直接通过 Blender 中可用的工具将 SMPL 模型导入 Blender 平台的基本过程。我们还探索了为这些 SMPL 模型添加背景和添加独特纹理的基础知识,以使模型看起来更符合人体。最后,我们探索了在同一个模型中添加多种纹理的领域,并对这些模型未来可能的工作类型进行了简短的讨论。

最后一步,我再次强烈推荐阅读更多的研究论文,自己探索这个话题。在未来的文章中,我们将回到更多的深度学习内容,并致力于与 GANs 相关的众多项目和与神经网络相关的其他主题。

Spectrum Labs 与 Paperspace 合作,推动基于 NLP 的有毒聊天建模工作

原文:https://blog.paperspace.com/spectrum-labs-collaborates-with-paperspace-to-boost-nlp-based-toxic-chat-modeling-efforts/

总部位于旧金山的技术公司 Spectrum Labs 提供情境人工智能、自动化和服务,以帮助消费者品牌识别和应对有毒行为,该公司与 Paperspace 合作,向互联网约会、游戏、市场和社交媒体社区提供有毒聊天检测模型。

Guadian is Spectrum Labs' content moderation platform. It allows customers to manage a moderation queue, build automated responses, analyze overall health, and train models.

随着世界越来越依赖于基于互联网的服务、应用和游戏,在线社区继续呈指数级增长,每天都有数十亿条消息和交易。这些社区中的骚扰、仇恨言论、歧视和其他有害行为正在加速,需要一种超越外包调节、关键词列表或内部遗留技术的解决方案,这些技术无法应对如此大规模的问题。

幸运的是,在线毒性是一个非常适合机器学习的问题。社区生成大量的文本数据和上下文元数据,这些数据和元数据形成了应用基于 NLP(自然语言处理)的机器学习技术来检测有毒和非法行为的坚实基础。

Spectrum Labs 以其高度精确、可调的上下文感知模型及其工作流工具 Guardian 而闻名。该公司在多种输入(如文本数据和各种形式的元数据)中使用基本的 NLP 技术(如矢量化)来创建关于用户行为的可信信号。然后,Spectrum 将这些模型作为端点提供给合作伙伴,以检测诈骗企图、厌女症、严重威胁、不受欢迎的性挑逗、仇恨言论、骚扰等。

Spectrum Labs 正在快速发展其机器学习团队和客户群。由于他们的每个最终用户都通过唯一的端点使用模型(每个端点代表一个经过调整以适应特定应用的基础模型),该团队面临着扩大团队规模和增加唯一模型部署数量的双重挑战。这些需求为 Gradient 建立了一个强大的用例:帮助内部团队协作,并通过 CI/CD、可追溯性和确定性帮助扩展模型部署。

“保护用户体验免受在线毒害需要一种最先进的机器学习方法。Paperspace 有助于团队高效运作。”

Josh Newman,Spectrum Labs 联合创始人兼首席技术官

Paperspace 首席运营官公司的丹·科布兰(Dan Kobran)表示:“与 Spectrum 实验室及其才华横溢的机器学习团队合作,我们感到无比兴奋。Spectrum Labs 的技术正在使互联网成为一个更安全、更包容、更友好的地方,我们期待帮助他们成功完成这一重要使命。"

欲了解更多关于频谱实验室的信息,请访问:www.spectrumlabsai.com

NumPy 优化的基本要素第 2 部分:将 K-Means 聚类速度提高 70 倍

原文:https://blog.paperspace.com/speed-up-kmeans-numpy-vectorization-broadcasting-profiling/

在关于如何使用 NumPy 编写高效代码的系列文章的第 1 部分中,我们讨论了矢量化和广播的重要主题。在这一部分中,我们将通过使用 NumPy 实现 K-Means 聚类算法的有效版本来实践这些概念。我们将把它与一个完全使用 Python 循环实现的简单版本进行比较。最后我们会看到 NumPy 版本比简单循环版本快大约 70 倍。

确切地说,在本帖中,我们将涉及:

  1. 了解 K-均值聚类
  2. 实现 K-意味着使用循环
  3. 使用 cProfile 查找代码中的瓶颈
  4. 使用 NumPy 优化 K-Means

我们开始吧!

了解 K-均值聚类

在本帖中,我们将优化 k-means 聚类算法的实现。因此,我们必须至少对算法的工作原理有一个基本的了解。当然,详细的讨论也超出了本文的范围;如果你想深入研究 k-means,你可以在下面找到几个推荐链接。

K-Means 聚类算法有什么作用?

简而言之,k-means 是一种无监督的学习算法,它根据相似性将数据分成不同的组。由于这是一个无监督的算法,这意味着我们没有数据标签。

k-means 算法最重要的超参数是聚类数,即 k. 一旦我们决定了 k 的值,算法的工作方式如下。

  1. 从数据中随机初始化 k 个点(对应于 k 个簇)。我们称这些点为形心。
  2. 对于每个数据点,测量距质心的 L2 距离。将每个数据点分配给距离最短的质心。换句话说,给每个数据点分配最近的质心。
  3. 现在,分配给质心的每个数据点形成一个单独的聚类。对于 k 个质心,我们将有 k 个簇。通过该特定聚类中存在的所有数据点的平均值来更新每个聚类的质心值。
  4. 重复步骤 1-3,直到每次迭代的质心的最大变化低于阈值,或者聚类误差收敛。

这是算法的伪代码。

Pseudo-code for the K-Means Clustering Algorithm

我要离开 K-Means。这足以帮助我们编写算法。然而,还有更多,如如何选择一个好的值 *k,*如何评估性能,可以使用哪些距离度量,预处理步骤,以及理论。如果你想深入研究,这里有几个链接供你进一步研究。

CS221Stanford CS221K-means Clustering: Algorithm, Applications, Evaluation Methods, and DrawbacksClustering is one of the most common exploratory data analysis technique used to get an intuition about the structure of the data. It can be defined as the task of identifying subgroups in the data…Towards Data ScienceImad Dabbura

现在,让我们继续算法的实现。

实现 K-意味着使用循环

在本节中,我们将使用 Python 和 loops 实现 K-Means 算法。我们不会为此使用 NumPy。这段代码将作为我们优化版本的基准。

生成数据

要执行聚类,我们首先需要我们的数据。虽然我们可以从多个在线数据集进行选择,但让事情保持简单和直观。我们将通过从多个高斯分布中采样来合成一个数据集,这样对我们来说可视化聚类就很容易了。

如果你不知道什么是高斯分布,那就去看看吧!

[](https://www.sciencedirect.com/topics/biochemistry-genetics-and-molecular-biology/gaussian-distribution#:~:text=Gaussian%20distribution%20(also%20known%20as,and%20below%20the%20mean%20value.)[Gaussian Distribution - an overview | ScienceDirect TopicsScienceDirect TopicsScienceDirect](https://www.sciencedirect.com/topics/biochemistry-genetics-and-molecular-biology/gaussian-distribution#:~:text=Gaussian%20distribution%20(also%20known%20as,and%20below%20the%20mean%20value.)

我们将从四个具有不同平均值和分布的高斯分布中创建数据。

import numpy as np 
# Size of dataset to be generated. The final size is 4 * data_size
data_size = 1000
num_iters = 50
num_clusters = 4

# sample from Gaussians 
data1 = np.random.normal((5,5,5), (4, 4, 4), (data_size,3))
data2 = np.random.normal((4,20,20), (3,3,3), (data_size, 3))
data3 = np.random.normal((25, 20, 5), (5, 5, 5), (data_size,3))
data4 = np.random.normal((30, 30, 30), (5, 5, 5), (data_size,3))

# Combine the data to create the final dataset
data = np.concatenate((data1,data2, data3, data4), axis = 0)

# Shuffle the data
np.random.shuffle(data) 

为了有助于我们的可视化,让我们在三维空间中绘制这些数据。

import matplotlib.pyplot as plt 
from mpl_toolkits.mplot3d import Axes3D 

fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')

ax.scatter(data[:,0], data[:,1], data[:,2], s= 0.5)
plt.show()

3-D Visualization of the Dataset

在上图中很容易看到四组数据。首先,这使得我们可以轻松地为我们的实现选择一个合适的值 k 。这符合尽可能保持算法细节简单的精神,因此我们可以专注于实现。

助手功能

我们首先初始化我们的质心,以及一个记录每个数据点被分配到哪个质心的列表。

# Set random seed for reproducibility 
random.seed(0)

# Initialize the list to store centroids
centroids = []

# Sample initial centroids
random_indices = random.sample(range(data.shape[0]), 4)
for i in random_indices:
    centroids.append(data[i])

# Create a list to store which centroid is assigned to each dataset
assigned_centroids = [0] * len(data)

在实现我们的循环之前,我们将首先实现几个助手函数。

compute_l2_distance取两个点(比如说[0, 1, 0][4, 2, 3])并根据以下公式计算它们之间的 L2 距离。

$$ L2(X_1,x _ 2)= \sum_^{dimensions(x_1)}(x _ 1[I]-x_2[i])^2 $ $

def compute_l2_distance(x, centroid):
    # Initialize the distance to 0
    dist = 0

    # Loop over the dimensions. Take squared difference and add to 'dist' 
    for i in range(len(x)):
        dist += (centroid[i] - x[i])**2

    return dist

我们实现的另一个助手函数叫做get_closest_centroid,这个名字不言自明。该函数接受一个输入x和一个列表centroids,并返回与最接近x的质心相对应的列表centroids的索引。

def get_closest_centroid(x, centroids):
    # Initialize the list to keep distances from each centroid
    centroid_distances = []

    # Loop over each centroid and compute the distance from data point.
    for centroid in centroids:
        dist = compute_l2_distance(x, centroid)
        centroid_distances.append(dist)

    # Get the index of the centroid with the smallest distance to the data point 
    closest_centroid_index =  min(range(len(centroid_distances)), key=lambda x: centroid_distances[x])

    return closest_centroid_index

然后我们实现函数compute_sse,它计算 SSE 或误差平方和。这个度量用于指导我们必须做多少次迭代。一旦这个值收敛,我们就可以停止训练了。

def compute_sse(data, centroids, assigned_centroids):
    # Initialise SSE 
    sse = 0

    # Compute the squared distance for each data point and add. 
    for i,x in enumerate(data):
    	# Get the associated centroid for data point
        centroid = centroids[assigned_centroids[i]]

        # Compute the distance to the centroid
        dist = compute_l2_distance(x, centroid)

        # Add to the total distance
        sse += dist

    sse /= len(data)
    return sse

主循环

现在,让我们写主循环。参考上面提到的伪代码,以供参考。我们仅仅循环 50 次迭代,而不是循环直到收敛。

# Number of dimensions in centroid
num_centroid_dims = data.shape[1]

# List to store SSE for each iteration 
sse_list = []

tic = time.time()

# Loop over iterations
for n in range(num_iters):

    # Loop over each data point
    for i in range(len(data)):
        x = data[i]

        # Get the closest centroid
        closest_centroid = get_closest_centroid(x, centroids)

        # Assign the centroid to the data point.
        assigned_centroids[i] = closest_centroid

    # Loop over centroids and compute the new ones.
    for c in range(len(centroids)):
        # Get all the data points belonging to a particular cluster
        cluster_data = [data[i] for i in range(len(data)) if assigned_centroids[i] == c]

        # Initialize the list to hold the new centroid
        new_centroid = [0] * len(centroids[0])

        # Compute the average of cluster members to compute new centroid
        # Loop over dimensions of data
        for dim in range(num_centroid_dims): 
            dim_sum = [x[dim] for x in cluster_data]
            dim_sum = sum(dim_sum) / len(dim_sum)
            new_centroid[dim] = dim_sum

        # assign the new centroid
        centroids[c] = new_centroid

    # Compute the SSE for the iteration
    sse = compute_sse(data, centroids, assigned_centroids)
    sse_list.append(sse)

完整的代码可以在下面看到。

基于样式的重新校准模块(SRM)通道注意

原文:https://blog.paperspace.com/srm-channel-attention/

计算机视觉中的通道注意机制的主题可能是最广泛研究的,相当于自然语言处理(NLP)领域中的变形金刚。自 2018 年发表的开创性工作以来,已经有了一些改进,称为挤压和激励网络,我们在本文的中讨论过。挤压和激励等渠道关注方法是打破排行榜基准的核心。例如,在 ImageNet 分类中广泛流行的最先进(SOTA)模型称为高效网络,将挤压和激励模块列为其架构设计中的关键组件。频道关注机制的价值不仅仅是从性能提升的角度来看,还从可解释性的角度来看。

目录

  1. 为什么要考虑渠道关注机制?
  2. SRM 动机
  3. 主要贡献
  4. 风格再校准模块(SRM)
    a .风格池
    b .风格整合
    c .复杂度
    d .与 SENets 的比较
    e .与 ECANets 的比较
  5. 密码
  6. 结果
  7. 批评意见
  8. 结论
  9. 参考

为什么要考虑渠道关注方式?

让我们确保我们在如何评估注意力机制(在这种情况下,属于基于渠道的注意力方法的类别)方面达成一致。术语“通道”(称为$C$)被定义为(通常)输入到任何深度卷积神经网络(CNN)中的任何中间卷积层的四维张量或其输出中的各个特征映射。关注这些“渠道”实质上意味着指定哪一个渠道更重要或更不重要,随后给它打分或用学习到的关注程度来衡量它。因此,频道关注方法本质上指定了在训练过程中网络“关注什么”。通过使用 GradCAM 和 GradCAM++从可解释性的角度对这些模块进行了评估,这揭示了配备有通道注意方法的网络在图像中的标签特定对象上具有更紧的界限。

From Rotate to Attend: Convolutional Triplet Attention Module (WACV 2021)

考虑信道注意方法的第二个原因是,可以使用信道注意进行动态信道修剪或选通,以减少有效网络大小或动态计算复杂性,同时保持接近恒定的性能。在这个方向上有许多方法,最近和最重要的一个是高通的 ICLR 2020 论文,题为学习条件通道门控网络的批量整形

这一领域的主要缺点之一是这些频道关注模块增加的计算开销。虽然这些方法中的大多数是简单的即插即用模块,可以插入到深度 CNN 中的任何位置,但它们通常会向模型架构添加大量的附加参数和触发器。这也会导致延迟增加和吞吐量降低。然而,已经有几种方法来降低由这些模块引入的开销成本,这些方法已经成功地证明了利用简单的低成本模块化频道关注方法可以实现有竞争力的性能。

鉴于上述信息,让我们进入这篇文章的主要议程。我们将回顾另一种受流行的挤压和激发网络(SENetsTPAMI 和 CVPR 2018),称为李等人在论文中提出的风格再校准模块(SRM)。艾尔。名为 SRM:卷积神经网络的基于风格的重新校准模块。事不宜迟,我们将深入探讨 SRM 的动机,然后分析该模块的结构设计,最后通过研究结果及其 PyTorch 代码结束本文。

摘要

随着卷积神经网络风格转移的发展,风格在卷积神经网络中的作用越来越受到广泛关注。在本文中,我们的目的是充分利用风格的潜力,以改善一般视觉任务的细胞神经网络的表现。我们提出了一个基于风格的重新校准模块(SRM),一个简单而有效的架构单元,它通过利用中间特征图的风格来自适应地重新校准中间特征图。SRM 首先通过样式池从特征图的每个通道中提取样式信息,然后通过独立于通道的样式集成来估计每个通道的重新校准权重。通过将各个风格的相对重要性结合到特征图中,SRM 有效地增强了 CNN 的表现能力。所提出的模块以可忽略的开销直接馈入现有的 CNN 架构。我们对一般图像识别以及与风格相关的任务进行了全面的实验,验证了 SRM 优于最近的方法,如挤压和激励(SE)。为了解释 SRM 和 SE 之间的内在差异,我们对它们的代表性属性进行了深入的比较。

动机

生成建模中一个深入探索的领域是图像风格化。一些有影响力的作品显示了利用风格背景进行基于图像的风格转换的潜力。虽然这些图像可能看起来更像是一种审美享受,但其背后的基础对于理解卷积网络的工作方式至关重要。conv 网络中潜在的风格属性也在不同的背景下进行了研究。很少有工作表明,被限制为仅依赖风格信息而不考虑空间上下文的 conv 网络在图像分类任务上做得很好。

风格属性是这项工作的两个激励因素之一,正如作者所说:

在这项工作中,我们进一步促进了 CNN 架构设计中风格的利用。我们的方法通过突出或抑制与任务相关的风格来动态丰富特征表示。

除了风格属性,注意力/特征再校准机制也是拼图的另一块。承认以前的注意力机制如聚集激发、挤压激发等的缺点。提出了一种新的轻量级高效通道注意机制:

与先前的努力相反,我们在利用风格信息方面重新制定了通道方式的重新校准,而没有通道关系或空间注意力的帮助。我们提出了一种风格池方法,它优于我们设置中的标准全局平均或最大池,以及一种独立于通道的风格集成方法,它实质上比完全连接的对应方更轻量级,但在各种场景中更有效。

贡献

本文的主要贡献如下:

我们提出了一个基于风格的特征再校准模块,它通过将风格合并到特征图中来增强 CNN 的表示能力。

尽管它的开销很小,但所提出的模块显著提高了网络在一般视觉任务以及风格相关任务中的性能。

通过深入分析以及消融研究,我们检查了我们的方法的内部行为和有效性。

样式重新校准模块

在本节中,我们将剖析本文中提出的 SRM 模块的精确结构设计,并将其与传统的挤压激励模块进行比较。

样式重新校准模块(SRM)由两个集成组件组成,称为样式池样式集成。样式池负责从维度$(C \ast H \ast W)$的输入张量$X$生成维度$(C \ast d)$的样式特征$T$。然后,这些样式特征被传递到样式集成阶段,以生成 shape $(C \ast 1)$的样式权重$G$。然后,通过简单地按元素将$X$乘以$G$来使用样式权重重新校准输入张量$X$中的通道。

在接下来的部分中,我们将分析 SRM 的两个组件,即样式池和样式集成。我们将通过重新考察挤压和激发网络来总结,并观察 SRM 和 SE 通道注意力之间的区别点。

Generalized Representation of SRM

样式池

SRM 由两个不同的子模块组成,分别负责特征分解和特征再校准。样式池是负责特征分解的两个组件之一。在理想和最佳设置中,必须处理所有可用信息以改进空间建模。然而,为张量中的每个像素计算注意力图所引入的计算开销是巨大的。输入张量通常是三维的,\((C \ast H \ast W)\)(不包括批量大小)。因此,计算每个像素的注意力图将导致大小为$(CHW) \ast H \ast W$的张量,这是一个巨大的数字,并导致巨大的内存开销。这实际上是自我注意的基础,它被认为是自然语言处理领域中自我注意生成对抗网络(SAGAN)和转换器的一个组成部分。

从数学上讲,样式池接受\mathbb^{N 的输入要素映射$ \ textbf \ ast c \ ast h \ ast w } \(并通过以下操作提供\mathbb{R}^{N 的样式池要素\) \ textbf \ ast c \ ast 2 } $:

$ \ mu _ = \frac{1}\sum^\sum^{x_}$

$ \ sigma _ = \sqrt{\frac{1}\sum^\sum^{(x_-\mu_)}^{2}}$

\(t_{nc}=[\mu_{nc},\sigma_{nc}]\)

\mathbb^{2}\(中的结果\) t _ \被表示为样式向量,用作批次$n$和频道$c$中每个图像的样式信息的汇总。$\mu_$通常被称为全局平均池(GAP),也用作其他标准注意力机制中的首选压缩形式,如挤压和激励(SENets)和卷积块注意力模块( CBAM )。作者确实研究了其他形式的风格池,比如在附录中计算最大池,它与 CBAM 的平均池一起使用(这将在本文后面的结果部分讨论)。在风格解开的情况下,通常计算通道之间的相关性,但是,在这种情况下,作者的重点是关注通道方面的统计,以提高效率和概念的清晰度,因为计算通道相关性矩阵需要$C \ast C$计算预算。

风格整合

SRM Structural Design

样式集成模块的目标是模拟与各个通道相关联的样式的重要性,以相应地强调或抑制它们。本质上,在特征映射张量中,总是存在通道重要性的不平衡分布。一些渠道可能对模型的性能有更大的影响,而其他渠道可能仅仅是多余的。然而,普通卷积神经网络的设计方式是,它们固有地同等地优先考虑每个信道,并给它们一个信道权重系数 1。风格整合模块本质上告诉网络在完整的特征映射张量中哪些通道更重要。

上图展示了样式集成模块的架构设计。它由三个模块组成:

  1. 通道式全连接层(CFC)
  2. 批量标准化层(BN)
  3. 乙状结肠激活单位

数学上,给定样式池的输出,其在\mathbb{N 中被表示为样式表示$ \ textbf \ ast c \ ast 2 } \(,样式集成操作符使用可学习的参数\) \ textbf \在\mathbb{C 中编码通道:

\(z _ { NC } = \ textbf { w } _ c \ cdot \ textbf { t } _ { NC }\)

其中$\ textbf \ in \mathbb^{n \ ast c } $表示编码的样式特征。这可以解释为具有两个输入节点和一个输出的通道独立全连接层,其中偏置项被合并到随后的 BN 层中。

然后,CFC 的输出被传递到批量标准化层以改进训练。随后,BN 层的输出被传递到作为门控机制的 s 形激活单元,如以下数学等式所示:

≤表示

\(\sigma_c^{(z)} = \sqrt{\frac{1}{n}\sum^{n}{n=1}(z_{nc}-\mu_c^{(z)})^{2}}\)

$\tilde=\gamma(\frac{z_-\mu_c^{(z)}}{\sigma_c^{(z)}})+ \ beta _ c 美元

\(g_{nc}=\frac{1}{1+e^{-\tilde{z}_{nc}}}\)

\mathbbC$中的$\gamma$和$ \ beta \表示仿射变换参数,而\mathbb{N 中的$ \ textbf \ ast c } $表示学习的每通道风格权重。

请注意,BN 在推理时使用均值和方差的固定近似值,这允许 BN 层合并到前面的 CFC 层中。

更简单地说,输入特征张量中每个通道的样式集成是一个 CFC 层$ f _ :\mathbb^{2} \ to \ mathbb $后跟一个激活函数$f_ : \mathbb \to [0,1]\(。最后,sigmoid 激活的每通道风格权重然后与输入特征张量中的各个对应通道逐元素相乘。因此,\mathbb{R}^{N 的输出\) \ hat { \ textbf } \是通过以下方式获得的:

\(\ tilde { \ textbf { x } } _ { NC } = g _ { NC } \ cdot \ textbf { x } _ { NC }\)

复杂性

作者的目标之一是在内存和计算复杂性方面设计一个轻量级和高效的模块。造成参数开销的两个模块是 CFC 和 BN 层。每个项的参数数量分别为$ \ sum N _ \ cdot C _ \ cdot 2 \(和\) \ sum \ cdot C _ \ cdot 4 $,其中$S$表示体系结构第$ \ textit $-级中重复模块的数量,$C_s$表示第$ \ textit $-级的输出通道尺寸。SRM 提供的参数总数为:

$6\sum^_N_s \cdot C_s 美元

如果将 Resnet-50 视为基准架构,那么将 SRM 添加到该架构的每个模块会导致额外的 0.02 GFLOPs 和 0.06M 参数。这比 SENets 等其他标准注意机制便宜得多,在相同的 ResNet-50 架构中使用时,SENets 增加了 253 万个参数。

与挤压和激励网络(SENets)的比较

Squeeze-and-Excitation Module

挤压-激发网络是计算机视觉领域中最基本的注意机制之一。大多数使用注意力模块的现代架构都以这样或那样的方式受到了杰出人物的启发,SRM 也不例外。SRM 模块和 SE 模块之间有一些关键区别。虽然两个模块都有特征压缩和特征加权方法,但 SE 和 SRM 涉及的确切过程是不同的。SRM 保留了 SE 的 squeeze 模块中使用的平均池,但是,它添加了一个标准偏差池,以在压缩时提供更丰富的功能表示。

此外,另一个主要区别在于 SRM 的特征加权模块。在 se 的情况下,特征加权模块(称为“激励”模块)使用多层感知器瓶颈,它首先降低维度,然后再次扩展回来。这是 SE 在主干架构中引入参数开销的主要原因。然而,对于 SRM,它使用的 CFC 比 se 中使用的 MLP 便宜得多,这也是 SRM 比 SE 便宜的主要原因。唉,SRM 和 SE 都使用 sigmoid 激活单元来缩放学习到的每通道权重,然后将这些缩放后的权重按元素乘以输入特征张量中的相应通道。

为了理解 SE 和 SRM 之间的代表性差异,作者将 SRM 和 SE 通过导致最高通道权重的图像学习的特征可视化。他们通过使用在 DTD 数据集上训练的基于 SE 和 SRM 的 ResNet-56 来做到这一点。如下图所示,SE 导致通道间高度重叠的图像,而 SRM 在顶部激活的图像中表现出更高的多样性,这意味着与 SE 相比,SRM 允许通道权重之间的相关性更低。作者进一步暗示,SE 和 SRM 的代表性表达能力之间的差异可能是未来的研究领域。尽管差别很大,但与 SE 相比,SRM 中不同模块的影响并不一定完全清晰,因为两种结构在设计方面极其相似。

引用作者的话:

SE 相关矩阵中明显的网格模式意味着通道组同步开启或关闭,而 SRM 则倾向于促进通道间的去相关。我们在 SE 和 SRM 之间的比较表明,它们针对完全不同的特征表示角度来提高性能,这值得进一步研究。

与高效渠道关注网(SENets)的比较

理论上,ECANets 似乎是 SRM 的复制品,但最多只提供微小的修改,这还没有得到很好的证明。ECA 非常便宜,而且比 SRM 便宜,主要有两个原因。在 ECA 的情况下,特征压缩方法仅涉及 GAP(全局平均池)的方法,类似于 se 的方法,而 SRM 同时使用 GAP 和标准差池。此外,ECA 不依赖于批处理规范化层,而 SRM 在 CFC 层之后立即使用一个。对于 ECA 的结构还有一个警告,因为它对用于特征加权的一维 conv 层使用自适应核大小公式。该公式包含两个超参数$\gamma$和$b$,这两个超参数是为获得最佳性能而预定义的,并基于输入特征映射张量中的通道数$C$进行自适应调整。

密码

以下是 SRM 模块的 PyTorch 代码,可插入标准卷积骨干架构:

import torch
from torch import nn

class SRMLayer(nn.Module):
    def __init__(self, channel, reduction=None):
        # Reduction for compatibility with layer_block interface
        super(SRMLayer, self).__init__()

        # CFC: channel-wise fully connected layer
        self.cfc = nn.Conv1d(channel, channel, kernel_size=2, bias=False,
                             groups=channel)
        self.bn = nn.BatchNorm1d(channel)

    def forward(self, x):
        b, c, _, _ = x.size()

        # Style pooling
        mean = x.view(b, c, -1).mean(-1).unsqueeze(-1)
        std = x.view(b, c, -1).std(-1).unsqueeze(-1)
        u = torch.cat((mean, std), -1)  # (b, c, 2)

        # Style integration
        z = self.cfc(u)  # (b, c, 1)
        z = self.bn(z)
        g = torch.sigmoid(z)
        g = g.view(b, c, 1, 1)

        return x * g.expand_as(x)

对于张量流,SRM 可定义如下:

import tensorflow as tf

def SRM_block(x, channels, use_bias=False, is_training=True, scope='srm_block'):
    with tf.variable_scope(scope) :
        bs, h, w, c = x.get_shape().as_list() # c = channels

        x = tf.reshape(x, shape=[bs, -1, c]) # [bs, h*w, c]

        x_mean, x_var = tf.nn.moments(x, axes=1, keep_dims=True) # [bs, 1, c]
        x_std = tf.sqrt(x_var + 1e-5)

        t = tf.concat([x_mean, x_std], axis=1) # [bs, 2, c]

        z = tf.layers.conv1d(t, channels, kernel_size=2, strides=1, use_bias=use_bias)
        z = tf.layers.batch_normalization(z, momentum=0.9, epsilon=1e-05, center=True, scale=True, training=is_training, name=scope)
        # z = tf.contrib.layers.batch_norm(z, decay=0.9, epsilon=1e-05, center=True, scale=True, updates_collections=None, is_training=is_training, scope=scope)

        g = tf.sigmoid(z)

        x = tf.reshape(x * g, shape=[bs, h, w, c])

        return x

在这两种情况下,在架构层的初始化过程中,一定要将正确的通道号作为参数传递给该层。

结果

在这一节中,我们将看看作者展示的各种结果,从 ImageNet 和 CIFAR-10/100 上的图像分类到风格转换和纹理分类。

ImageNet-1k

如上表所示,在 ImageNet 分类任务上,SRM 的性能大大优于 vanilla、GE 和 SE 网络,同时在参数开销方面也很便宜。基于 ResNet-50 的模型的训练曲线如下图所示:

作者还在风格化的 ImageNet 上测试了 SRM,这是 ImageNet 的一种变体,使用由 数字画师数据集中的随机绘画风格化的图像创建。关于数据集和训练设置的更多细节可以在论文中找到。

与之前在 ImageNet 上的结果相似,SRM 在风格化 ImageNet 的情况下占据了重要位置,如上表所示。

CIFAR 10/100

SRM 还能够在 CIFAR-10 和 CIFAR-100 图像分类基准测试中轻松领先于 Vanilla、SE 和 GE (Gather Excite)变体,如下表所示:

Office Home 数据集

在平均超过 5 倍交叉验证的情况下,SRM 在 Office Home 数据集上的多域分类上也始终表现出比基线和基于 SE 的 ResNet-18 更好的性能。结果如下表所示:

纹理分类

继续上述基准测试中展示的强大性能,SRM 提高了 ResNet-32 和 ResNet-56 模型在基线和 SE 变体上的得分,可描述纹理数据集上的纹理分类基准平均超过 5 倍交叉验证,如下所示:

风格转移

作者最后使用 MS-COCO 数据集探索了 SRM 在风格转换任务上的表现。定量分析的训练图和样本输出如下所示:

还有一些例子:

消融研究

为了进一步了解 SRM 中使用的不同组件的重要性,作者对 SRM 中使用的池化方法和风格集成方法进行了广泛的研究,如下表所示:

最后,为了验证 SRM 产生相关的信道权重,作者使用 SRM 的信道权重来修剪模型,并观察稀疏(修剪)模型的性能。如下图所示,与基线相比,基于 SRM 的修剪网络表现出更好的性能:

批评意见

总的来说,这篇论文写得非常透彻。由于作者的功劳,他们提供了广泛的实验结果以及广泛的消融研究和与 SENets 的比较。然而,随着计算机视觉中注意力机制的快速流动,大多数方法都被每年出现的新变体所掩盖。虽然这篇论文不一定展示任何缺点或不足,但与每种注意力方法一样,它涉及到计算成本的增加。遗憾的是,这仍然取决于用户在自己的任务/数据集上进行尝试的判断。

结论

风格再校准模块(SRM)是一个高效和轻量级的通道注意力模块,使用深度卷积神经网络在广泛的任务范围内提供显著改善的和有竞争力的性能。SRM 可以被认为是目前在不同技术水平(SOTA)网络(如高效网络)中使用的 evergreen 挤压和激励模块的强大替代品。

参考

  1. SRM:基于风格的卷积神经网络再校准模块
  2. 挤压和激励网络
  3. ECA-Net:深度卷积神经网络的高效信道关注
  4. Gather-Excite:利用卷积神经网络中的特征上下文
  5. SRM 的 PyTorch 正式实施

从渐变部署运行稳定扩散 Web UI 第 2 部分:更新容器以访问新功能

原文:https://blog.paperspace.com/stable-diffusion-webui-deployment-2/

上个月,我们讨论了 AUTOMATIC1111 的稳定扩散 Web UI 和稳定扩散开源社区的其他贡献者的一些主要功能。Web UI 是一个流行的 Gradio web 应用程序,它允许用户从任何主要的稳定扩散管道生成图像,包括图像到图像、文本到图像等等,并且还提供工具,其中许多工具是自上一篇文章以来添加的,包括训练文本反转嵌入和超网络、用 GFPGAN 和 CodeFormer 升级照片,以及添加扩展。

在本文中,我们将从另一个角度看一下 11 月底的 Web UI。首先,我们将逐步完成从 Paperspace 渐变部署中快速、轻松地启动 Web UI 的步骤。然后,我们将分解一些最好的扩展和新功能,并通过工作示例展示它们可以为您的工作流添加什么。

从渐变部署启动 Web UI

有两种方法可以从渐变中运行 Web UI:在渐变笔记本中或从渐变部署中。可以通过创建一个选择了稳定扩散运行时的笔记本来访问笔记本版本的部署。然后,我们可以将稳定的扩散 v1-5 模型公共文件挂载到我们的笔记本上,并启动 Web UI。

如果我们想要运行一个部署,我们需要一个合适的 Docker 容器,其中包含运行应用程序所需的所有先决组件和包。在下一节中,我们将浏览用于创建该容器的 Dockerfile 文件。如果我们想在将来更新容器,我们可以使用这个 Dockerfile 文件,使用 Web UI 的最新更新来创建容器的新版本。

第一部分将回顾前一篇文章中描述的设置过程,但是该过程的重大更新使得我们有必要在深入研究如何使用扩展之前重新回顾一下。

文档文件

让我们从我们将要托管 Web UI 的容器开始。它包含了我们运行稳定扩散所需要的许多包,尽管为了简单起见,其他包实际上是在发布时安装的。Web UI 中包含的launch.py脚本自动化了很多过程,但是我们仍然需要将环境设置到最小。

以下是 docker 文件中的示例代码:

# Paperspace base container as our baseline
FROM paperspace/gradient-base:pt112-tf29-jax0314-py39-20220803

# Upgrade pip to prevent install errors
RUN pip3 install --upgrade pip

# Clone Web UI
RUN git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui

# Make it working directory 
WORKDIR /stable-diffusion-webui

## pip installs
RUN pip install transformers scipy ftfy ipywidgets msgpack rich einops omegaconf pytorch_lightning basicsr optax facexlib realesrgan kornia imwatermark invisible-watermark piexif fonts font-roboto gradio
RUN pip install git+https://github.com/crowsonkb/k-diffusion.git
RUN pip install -e git+https://github.com/CompVis/taming-transformers.git@master#egg=taming-transformers
RUN pip install git+https://github.com/openai/CLIP.git
RUN pip install diffusers

# Get model ckpt
RUN apt update
RUN apt install -y aria2
RUN aria2c --seed-time=0 --dir models/Stable-diffusion "magnet:?xt=urn:btih:2daef5b5f63a16a9af9169a529b1a773fc452637&dn=v1-5-pruned-emaonly.ckpt"

# Finish setup
RUN pip install -r requirements.txt
RUN mkdir repositories
RUN git clone https://github.com/CompVis/stable-diffusion /repositories/stable-diffusion
RUN git clone https://github.com/TencentARC/GFPGAN.git /repositories/GFPGAN

EXPOSE 7860

请注意,此容器的另一个版本具有相同的 Dockerfile,但没有安装和下载模型检查点的 aria2。这个容器小得多。我们将在后面的部分更详细地讨论如何使用它。

更新 Web UI 部署容器

如果我们想要创建这个容器的更新版本,我们可以简单地用这个 Docker 文件重新运行 Docker build 命令。然后,它会在新的容器中为我们提供最新版本的 Web UI。然后可以上传到 Dockerhub,在那里 Gradient 可以将它用于笔记本、工作流和部署。

要运行这个过程,我们可以在自己的终端中使用下面的代码片段。

git clone https://github.com/gradient-ai/stable-diffusion
cd stable-dffusion
docker build -t <your account>/stable-diffusion-webui-deployment:<tag>
docker push <your account>/stable-diffusion-webui-deployment:<tag>

然后,我们可以用新容器的名称替换下一节中显示的部署规范的第一行。如果我们想做任何改变,比如添加额外的模型检查点或图像嵌入,我们也可以修改 docker 文件本身。如果需要的话,这将允许更加定制化的体验。

Paperspace 将努力自己定期更新容器,所以请回到我们的项目页面查看更新。

加快 Web 用户界面的渐变部署

既然我们已经看到了容器中的内容,我们可以看看稳定扩散 Web UI 的渐变部署的步骤。到本节结束时,用户应该能够根据需要使用自己的定制模型和嵌入来运行自己的 Web UI 容器。

The Deployments tab

首先,确保您已经注册了一个 Paperspace 帐户,并在文件中记录了一张信用卡。与该流程的笔记本版本不同,该部署没有免费的 GPU 选项。如果您打算通过这种方法使用 Web UI,请记住这一点。继续登录到您的帐户,并前往任何项目的部署选项卡。

Sample set up for the Deployment in the Console

接下来,点击“创建”按钮打开创建页面。这是我们可以为部署填写信息规范的地方。适当地命名部署。我们选择了‘稳定扩散 Web UI 部署’。

然后,我们可以为我们的机器选择一个 GPU 选项。我们的建议是 A4000 或 A5000,因为它们的性价比非常高。有关选择 GPU 的更多信息,请参见我们对 Paperspace GPU 定价的评论。

在这一部分,我们可以开始为我们的部署做更多的定制选择。这个容器现在有两个版本:一个预先下载了预先训练好的 v1-5 模型,另一个假设模型作为模型工件上传到 Paperspace。在下一节中,我们将看看如何使用模型工件来运行带有我们自己上传的模型文件的 Web UI,使我们能够访问类似于 Dreambooth 模型或者文本反转嵌入的东西。现在,我们将使用paperspace/stable-diffusion-webui-deployment:v1.1-model-included。这个容器带有已经下载的v1-5-pruned-email only . ckpt

最后,我们将我们的端口设置为暴露的“7860”,并在启动时输入命令来启动 Web UI:

python launch.py --listen --autolaunch --enable-insecure-extension-access --port 7860

Click the Endpoint URI to open the Web UI in your browser

这是完整的规范:

image: paperspace/stable-diffusion-webui-deployment:v1.1-model-included
port: 7860
command:
  - python
  - launch.py
  - '--autolaunch'
  - '--listen'
  - '--enable-insecure-extension-access'
  - '--port'
  - '7860'
resources:
  replicas: 1
  instanceType: A4000

然后我们可以点击 Deploy 来启动容器和应用程序。这大约需要 2 分钟的启动时间,可以通过单击“部署详细信息”页面中的 API 端点链接来访问。

如果一切启动成功,当你进入链接时,会看到稳定扩散 Web UI 的 txt2img“主页”,现在可以开始合成图像了!

备选方案:使用渐变 CLI 启动 Web UI 部署

或者,我们可以使用渐变包从本地终端启动 Web UI。您可以安装软件包并使用下面的代码片段登录。您只需要在 API keys 选项卡的 team settings 页面中创建一个 API key,并在代码片段中提示的地方填充它。

pip install gradient
gradient apiKey <your API key here>

接下来,打开您的终端并导航到您可以工作的目录。然后,用touch yaml.spec创建一个新的 YAML 规格文件。然后在规范中填入以下内容:

image: paperspace/stable-diffusion-webui-deployment:v1.1-model-included
port: 7860
command:
  - python
  - launch.py
  - '--autolaunch'
  - '--listen'
  - '--enable-insecure-extension-access'
  - '--port'
  - '7860'
resources:
  replicas: 1
  instanceType: A4000

最后,我们可以使用以下命令创建部署:

gradient deployments create --name [Deployment name] --projectId [your Project ID] --spec [path to your deployment spec file i.e. spec.yaml]

然后,当您使用完部署时,您可以通过返回到您的规范文件并将其更改为 0 个副本(规范的倒数第二行)来删除它。这将停止部署运行,但不会删除它。然后使用以下终端命令更新您的部署:

gradient deployments update --id <your deployment id> --spec <path to updated spec>

使用上传的模型工件启动 Web UI

容器paperspace/stable-diffusion-webui-deployment:v1.1-model-included有 11.24 GB 大。这在很大程度上是由于模型检查点,它仅占用大约 5 GB 的内存。

为了改善容器大小的问题,我们还创建了一个没有下载模型的版本:paperspace/stable-diffusion-webui-deployment:v1.1。这个打火机容器被设计成连接到上传到 Gradient 的模型工件。

Sample model upload page

为此,我们需要首先上传一个模型检查点到我们正在工作的项目中。在渐变控制台中,导航到“模型”选项卡,然后单击“上传模型”。然后从您的本地机器上选择您的模型检查点(尽管 v2 目前不支持 Web UI ),并上传它。

Sample model details page

现在,可以通过梯度部署或工作流使用顶部列出的 ID 值来访问上传的模型。我们只需要适当地修改我们的 YAML 规范来连接它们。从上图中我们可以看到,示例文件v1-5-pruned-emaonly.ckpt与 id mo4a5gkc13ccrl相关联,可以通过模型工件的名称在顶部找到。

当我们将部署连接到这个工件时,它将自动使用这个 ID 作为子目录名,因此新文件被装载到opt/<your model id>/目录中。因此,当部署处于活动状态时,我们需要启动带有--ckpt标志的 Web UI 来获取这些名称。

opt/可以在任何梯度部署的根目录中找到,所以我们可以使用这个新信息来推断检查点文件的位置:../opt/mo4a5gkc13ccrl/v1-5-pruned-emaonly.ckpt

除了 CMD 之外,我们还需要设置环境变量,并声明要连接的容器的文件 id 和路径。我们可以使用下面的示例 YAML 规范,通过填充缺少的值来启动带有任何上传的扩散模型的 Web UI。

image: paperspace/stable-diffusion-webui-deployment:v1.1
port: 7860
command:
  - python
  - launch.py
  - '--share'
  - '--autolaunch'
  - '--listen'
  - '--enable-insecure-extension-access'
  - '--port'
  - '7860'
  - '--ckpt'
  - ../opt/< model id >/<checkpoint file name>
env:
  - name: MODEL_NAME
    value: <name you gave to the model artifact in console>
  - name: MODEL_FILE
    value: <file name for model i.e. v1-5-pruned-emaonly.ckpt>
  - name: MODEL_DIR
    value: /opt/< model id >
models:
  - id: <model id>
    path: /opt/<model id>
resources:
  replicas: 1
  instanceType: A4000 # <-- we recommend the A4000 or A5000

当输入到渐变部署创建页面时,我们将得到如下所示的内容:

我们必须正确地填充环境变量,这样才能工作,但是如果我们成功了,我们将会使用我们自己的模型文件部署一个 5 GB 的模型简化版本!

Web Ui 部署中的新特性

自从我们上次更新 Web UI 部署以来,在升级应用程序的各种功能方面已经取得了重大进展。由于强大的 Gradio 框架,它被证明是开源社区为稳定传播而开发的累积努力的一个非常有能力的吸收器。在本节中,我们将详细了解这些新特性。

扩展ˌ扩张

如果在启动时应用了正确的命令行标志,Web UI 很早就具备了接收用户脚本的能力。这些用户脚本允许用户以重要的方式修改应用程序和扩展功能。为了简化这些脚本的使用并适应低代码用户,“扩展”标签被添加到 Web UI 中。这些扩展打包了来自社区的用户脚本,然后可以添加到 Web UI 上。这些工具从强大的插件和工具到对用户界面的有用编辑,再到风格灵感工具。这些扩展的一些示例包括但不限于:

梦想小屋

Dreambooth tab

这个扩展允许用户通过输入大量图像来训练一个新的、微调的模型,以便模型进行自我调整。这是创建稳定扩散模型的定制版本的最流行的方法之一,并且该扩展使得它在 Web UI 中使用简单。

如果您尝试这样做,请务必将您的结果与使用渐变 Dreambooth 笔记本的结果进行比较!

图像浏览器

Image Browser tab

Image Browser 插件可能是最有用的实用程序扩展,它允许用户在 Web UI 中检查整个会话中生成的图像。这对部署用户特别有用,因为它提供了一种简单的方法来下载在会话早期的任何时间生成的图像,并比较不同生成规格的照片。

嵌入编辑

文本反转是另一种用于微调稳定扩散的流行方法,尽管与 Dreambooth 不同,它专注于为训练图像的特征创建最佳的单词模拟表示。通过嵌入编辑器,用户可以修改和编辑现有的嵌入。目前这还只是初步的,但是细心的用户可以使用它来调整他们的文本反转嵌入,效果很好。

请务必将您的结果与梯度文本反演笔记本中的结果进行比较!

美学渐变

美学梯度是一种被设计成通过引导生成过程朝向由用户从一组图像定义的定制美学来个性化裁剪条件扩散模型的方法。使用这个扩展,用户可以使用一些图像创建一个美学渐变。这种新的样式可以应用于以后生成的任何图像。

研究艺术家

https://blog.paperspace.com/content/media/2022/12/Screen-Recording-2022-11-30-at-6.07.07-PM.mp4

除了上面提到的更多功能扩展,还有许多工具可以帮助用户从自己的工作中获得灵感。这方面的一个例子是 Artists to Study extension,它从大量不同的风格和美学中快速生成样本图像,以帮助用户决定使用哪种风格。

本地化及更多

最有用的扩展之一是本地化扩展。这允许用户将 Web UI 的语言更改为他们的母语,并且很可能会给许多非英语用户提供运行这个 Web UI 的能力,否则他们将无法充分利用这个能力。本地化包括繁体中文、韩语、西班牙语、意大利语、日语、德语等!

这种对扩展的观察并不全面。请务必查看 Wiki 和相关的 repos,了解关于每个感兴趣的扩展的详细信息。

检查点合并

以前,可以使用命令行合并检查点(Glid-3-XL-stable 有一个很好的脚本),但这绝不是一个直观的过程。对于低代码用户来说,合并两个模型可能非常困难。

检查点合并是 Web UI 中最有用的工具之一。有了这个工具,在组合两个模型时可以更加细致。滑块允许用户确定将模型权重的大致“百分比”转移到输出模型,并允许用户根据需要迭代测试不同的模型组合。

火车

训练选项卡增加了使用文件选择在 Web UI 中训练超网络和图像嵌入的能力。这些功能仍在更新,以匹配外部工具的功能,但它们仍然有用,特别是如果我们想在相对低功耗的 GPU 上训练图像嵌入。

新计划程序

在 Web UI 中使用 Img2Img 或 Txt2Img 脚本生成图像时,用户可以选择使用哪种调度程序。根据 Huggingface 文档,“调度函数,在库中表示为调度器,接收训练模型的输出,扩散过程迭代的样本,以及返回去噪样本的时间步长。这就是为什么调度器在其他扩散模型实现中也可能被称为采样器来源】。

Prompt: "New york city in the 1920s" generated at 12 and 50 steps with DPM++ SDE

自从这个部署容器的最后一次迭代以来,Web UI 中添加了许多新的调度程序。特别是,我们想提醒大家注意 DPM++ 2M、DPM ++ 2M·卡拉斯和 DPM++ SDE 计划程序。这些都能够以极低的采样步长值产生高质量的输出。上面是包含在 10 个扩散步骤和 50 个扩散步骤生成的图像的样本。如我们所见,图形保真度损失相对较小,左侧网格中的图像在细节和清晰度方面与使用 5 倍步数生成的图像在美学上几乎相同。

结束语

在本文中,我们查看了为稳定扩散 Web UI 创建和更新容器的步骤,详细介绍了使用渐变部署 Web UI 的步骤,并讨论了自上一篇文章以来稳定扩散 Web UI 中添加到应用程序中的新特性。

将来,请回到本系列文章中查看有关稳定扩散 Web UI 容器的更新。

posted @ 2024-11-02 15:52  绝不原创的飞龙  阅读(28)  评论(0编辑  收藏  举报