第25章 Pytorch 如何高效使用GPU

深度学习涉及很多向量或多矩阵运算,如矩阵相乘、矩阵相加、矩阵-向量乘法等。深层模型的算法,如BP,Auto-Encoder,CNN等,都可以写成矩阵运算的形式,无须写成循环运算。然而,在单核CPU上执行时,矩阵运算会被展开成循环的形式,本质上还是串行执行。GPU(Graphic Process Units,图形处理器)的众核体系结构包含几千个流处理器,可将矩阵运算并行化执行,大幅缩短计算时间。随着NVIDIA、AMD等公司不断推进其GPU的大规模并行架构,面向通用计算的GPU已成为加速可并行应用程序的重要手段。得益于GPU众核(many-core)体系结构,程序在GPU系统上的运行速度相较于单核CPU往往提升几十倍乃至上千倍。
目前,GPU已经发展到了较为成熟的阶段。利用GPU来训练深度神经网络,可以充分发挥其数以千计计算核心的能力,在使用海量训练数据的场景下,所耗费的时间大幅缩短,占用的服务器也更少。如果对适当的深度神经网络进行合理优化,一块GPU卡相当于数十甚至上百台CPU服务器的计算能力,因此GPU已经成为业界在深度学习模型训练方面的首选解决方案。
如何使用GPU?现在很多深度学习工具都支持GPU运算,使用时只要简单配置即可。Pytorch支持GPU,可以通过to(device)函数来将数据从内存中转移到GPU显存,如果有多个GPU还可以定位到哪个或哪些GPU。Pytorch一般把GPU作用于张量(Tensor)或模型(包括torch.nn下面的一些网络模型以及自己创建的模型)等数据结构上。

25.1 单GPU加速

使用GPU之前,需要确保GPU是可以使用,可通过torch.cuda.is_available()的返回值来进行判断。返回True则具有能够使用的GPU。
通过torch.cuda.device_count()可以获得能够使用的GPU数量。
如何查看平台GPU的配置信息?在命令行输入命令nvidia-smi即可 (适合于Linux或Windows环境)。图5-13是GPU配置信息样例,从中可以看出共有2个GPU。

图5-13 GPU配置信息

把数据从内存转移到GPU,一般针对张量(我们需要的数据)和模型。
对张量(类型为FloatTensor或者是LongTensor等),一律直接使用方法.to(device)或.cuda()即可。

对于模型来说,也是同样的方式,使用.to(device)或.cuda来将网络放到GPU显存。

25.2 多GPU加速

这里我们介绍单主机多GPUs的情况,单机多GPUs主要采用的DataParallel函数,而不是DistributedParallel,后者一般用于多主机多GPUs,当然也可用于单机多GPU。
使用多卡训练的方式有很多,当然前提是我们的设备中存在两个及以上的GPU。
使用时直接用model传入torch.nn.DataParallel函数即可,如下代码:

这时,默认所有存在的显卡都会被使用。
如果你的电脑有很多显卡,但只想利用其中一部分,如只使用编号为0、1、3、4的四个GPU,那么可以采用以下方式:

或者

其中CUDA_VISIBLE_DEVICES 表示当前可以被Pytorch程序检测到的GPU。
下面为单机多GPU的实现代码。
(1)背景说明
这里使用波士顿房价数据为例,共506个样本,13个特征。数据划分成训练集和测试集,然后用data.DataLoader转换为可批加载的方式。采用nn.DataParallel并发机制,环境有2个GPU。当然,数据量很小,按理不宜用nn.DataParallel,这里只是为了说明使用方法。
(2)加载数据

(2)把数据转换为批处理加载方式
批次大小为128,打乱数据。

(3)定义网络

(4)把模型转换为多GPU并发处理格式

运行结果
Let's use 2 GPUs
DataParallel(
(module): Net1(
(layer1): Sequential(
(0): Linear(in_features=13, out_features=16, bias=True)
)
(layer2): Sequential(
(0): Linear(in_features=16, out_features=32, bias=True)
)
(layer3): Sequential(
(0): Linear(in_features=32, out_features=1, bias=True)
)
)
)
(5)选择优化器及损失函数

(6)模型训练,并可视化损失值。

运行的部分结果
In Model: input size torch.Size([64, 13]) output size torch.Size([64, 1])
In Model: input size torch.Size([64, 13]) output size torch.Size([64, 1])
Outside: input size torch.Size([128, 13]) output_size torch.Size([128, 1])
In Model: input size torch.Size([64, 13]) output size torch.Size([64, 1])
In Model: input size torch.Size([64, 13]) output size torch.Size([64, 1])
Outside: input size torch.Size([128, 13]) output_size torch.Size([128, 1])
从运行结果可以看出,一个批次数据(batch-size=128)拆分成两份,每份大小为64,分别放在不同的GPU上。此时用GPU监控也可发现,两个GPU都同时在使用。

(7)通过web查看损失值的变化情况

图5-16 并发运行训练损失值变化情况
图形中出现较大振幅,是由于采用批次处理,而且数据没有做任何预处理,对数据进行规范化应该更平滑一些,大家可以尝试一下。

单机多GPU也可使用DistributedParallel,它多用于分布式训练,但也可以用在单机多GPU的训练,配置比使用nn.DataParallel稍微麻烦一点,但是训练速度和效果更好一点。具体配置为:

单机运行时使用下面方法启动

25.3使用GPU注意事项

使用GPU可以提升我们训练的速度,如果使用不当,可能影响使用效率,具体使用时要注意以下几点:
(1)GPU的数量尽量为偶数,奇数的GPU有可能会出现异常中断的情况;
(2)GPU很快,但数据量较小时,效果可能没有单GPU好,甚至还不如CPU;
(3)如果内存不够大,使用多GPU训练的时候可通过设置pin_memory为False,当然使用精度稍微低一点的数据类型有时也效果。

第24章 轻松掌握深度学习的核心-误差反向传播法

24.1 链式法则

假设z=f(t)
t=g(y)
则,根据复合函数的求导规则(即链式法则),可得:

24.1.1 计算图的方向传播

反向传播的计算顺序是将信号E乘以节点的局部导数(∂y/∂x),然后将结果传递给下一个节点。

24.1.2 链式法则

如果某个函数为复合函数,则该复合函数的导数可以用构成复合函数的各个函数的导数的乘积表示。

24.1.3 链式法则和计算图

反向传播是上游传过来的导数乘以本节点导数。

24.2 反向传播

反向传播是上游传过来的导数乘以本节点对应输入的导数。

24.2.1 加法节点的反向传播

 

代码实现如下

24.2.2 乘法法节点的反向传播

24.2.3 激活函数层的反向传播

24.3 激活函数层的反向传播

24.3.1 ReLU激活函数

24.3.2 Sigmoid激活函数


根据导数的链式规则,上游的值乘以本节点输出对输入的导数.
代码实现

24.4 Affine/softmax层的反向传播


DY=np.array([[1,2,3],[4,5,6]])
dB=np.sum(DY,axis=0)
Affine层的代码实现

24.4.1 Softmax-with loss 层

输入一张手写5的图片,经过多层(这里假设为2层)神经网络转换后,对输出10个节点,在各个输出节点的得分或概率是不同的,其中对应标签为5的节点(转换为one
-hot后为[0,0,0,0,1,0,0,0,0],得分或概率最大。

我们看一下带softmax及loss的反向传播,如何计算梯度。以下为示意图。

代码实现

24.5 损失反向传播法的实现

24.5.1 神经网络学习的基本步骤

利用随机梯度下降法,求梯度并更新权重和偏置参数,整个过程是个循环过程。
步骤1
从训练数据中随机选择一部分数据
步骤2
构建网络,利用前向传播,求出输出值。然后利用输出值与目标值得到损失函数,利用损失函数,利用反向传播方法,求各参数的梯度。
步骤3
将权重参数沿梯度方向进行微小更新
步骤4
重复以上1、2、3步骤

24.5.2 神经网络学习的反向传播法的实现

神经网络结构图如下

下面用代码实现
1)概述
为定义和保存以上神经网络架构,需要先定义几个实例变量:
保存权重参数的字典型变量params。
保存各层的信息的顺序字典layers,这里的顺序是插入数据的先后顺序。
神经网络的最后一层lastlayer
除了以上三个实例变量,还需要定义一些方法
构造函数,以初始化变量和权重等
预测方法,根据神经网络各层的前向传播得到最后的输出值
损失函数,根据输出值与目标值,得到交叉熵作为衡量两个分布的距离。
评估指标,这里使用精度来衡量模型性能
最后就是计算梯度,这里使用反向传播方法的得到,具体是利用导数的链式法则,从后往前,获取各层的梯度作为前层梯度往前传递(往输入端)
当然,这里需要先定义好各层类,各类中各层的权重参数、包括前向传播结果,反向传播的梯度等。
2)定义各层类
①softmax 函数及Sigmoid类

②Affine类或称为sumweigt

③最后一层

3)定义损失函数

4)定义神经网络类

5)使用误差反向传播法训练模型

.09000000000000001
train acc, test acc | 0.9837833333333333, 0.9724
0.09000000000000001
train acc, test acc | 0.9842666666666666, 0.9722
0.08100000000000002
train acc, test acc | 0.98475, 0.9716
0.08100000000000002
train acc, test acc | 0.9853166666666666, 0.9733
0.08100000000000002
train acc, test acc | 0.9859666666666667, 0.9726
0.08100000000000002
train acc, test acc | 0.9861166666666666, 0.9707
0.08100000000000002
train acc, test acc | 0.9873, 0.9737
0.08100000000000002
train acc, test acc | 0.9873833333333333, 0.9744
0.08100000000000002
train acc, test acc | 0.9881, 0.973
0.08100000000000002
train acc, test acc | 0.9886666666666667, 0.9747
0.08100000000000002
train acc, test acc | 0.9888833333333333, 0.9743

6)利用各种算法对MNIST数据集的影响

第23章 神经风格迁移

本章数据集下载地址(提取码是:7kct)

神经风格迁移是指将参考图像的风格应用于目标图像,同时保留目标图形的内容,如下图所示:

实现风格迁移核心思想就是定义损失函数,然后最小化损失。这里的损失包括风格损失和内容损失。
用公式来表示就是:

具体内容如下图

如图,假设初始化图像x(Input image)是一张随机图片,我们经过fw(image Transform Net)网络进行生成,生成图片y。
此时y需要和风格图片ys进行特征的计算得到一个loss_style,与内容图片yc进行特征的计算得到一个loss_content,假设loss=loss_style+loss_content,便可以对fw的网络参数进行训练。

23.1 内容损失

内容损失一般选择靠近的某层激活的差平方或L2范数。
写成代码就是

23.2 风格损失

格拉姆矩阵(Gram Matrix),即某一层特征图的内积。这个内积可以理解为表示该层特征之间相互关系的映射。损失函数的定义主要考虑以下因素:
①在目标内容图像和生成图像之间保持相似的较高层激活,从而能保留内容。卷积神经网络应该能够看到目标图像和生成图像包含相同的内容。
②在较低层和较高层的激活中保持类似的相互关系,从而能保留风格。特征相互关系捕zu到的是纹理,生成图像和风格参考图像在不同的空间尺度上应该具有相同纹理。

Gram Matrices的计算过程
假设输入图像经过卷积后,得到的feature map为[ch, h, w]。我们经过flatten和矩阵转置操作,可以变形为[ ch, h*w]和[h*w, ch]的矩阵。再对两矩阵做内积得到[ch, ch]大小的矩阵,这就是我们所说的Gram Matrices,如下图所示:

 

比如我们假设输入图像经过卷积后得到的[b, ch, h*w]的feature map,其中我们用fm表示第m个通道的特征层,fn为第n通道特征层。则Gram Matrices中元素fm∗fn代表的就是m通道和n通道特征flatten后按位相乘(内积)
具体实现代码

关于Gram矩阵还有以下三点值得注意:
1 Gram矩阵的计算采用了累加的形式,抛弃了空间信息。一张图片的像素随机打乱之后计算得到的Gram Matrix和原图的Gram Matrix一样。所以认为Gram Matrix所以认为Gram Matrix抛弃了元素之间的空间信息。
2 Gram Matrix的结果与feature maps F 的尺寸无关,只与通道个数有关,无论H,W的大小如何,最后Gram Matrix的形状都是CxC
3 对于一个C x H x W的feature maps,可以通过调整形状和矩阵乘法运算快速计算它的Gram Matrix。即先将F调整到 C x (H x W)的二维矩阵,然后再计算F 和F的转置。结果就为Gram Matrix

Gram Matrix的特点:
通过相乘运算,它将特征之间的区别进行扩大或者缩小,由此可一定程度反应向量本身及向量之间的一些特征或关系,它注重风格纹理,忽略空间信息

23.3 用keras实现神经风格迁移

https://ypw.io/style-transfer/(神经风格迁移 pytorch 0.4)
1)导入目标、风格图像

2)定义图像处理辅助函数
对进出VGG19神经网络的图像进行加载、预处理和后处理等处理。

【说明】
keras中preprocess_input()函数的作用是对样本执行 逐样本均值消减 的归一化,即在每个维度上减去样本的均值,对于维度顺序是channels_last的数据,keras中每个维度上的操作如下:

3)加载VGG19网络,并将其应用于三张图像
三张图像是目标图像、风格图像、生成图像,把这三张图像作为一个批量。其中生成图像将改变,以占位符的形式存储。而目标图像、风格图像在整个过程中是不变的,故以constant方式存储。

Downloading data from https://github.com/fchollet/deep-learning-models/releases/download/v0.1/vgg19_weights_tf_dim_ordering_tf_kernels_notop.h5
80142336/80134624 [==============================] - 155s 2us/step
Model loaded.

4)查看VGG19的网络结构图

_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) (None, None, None, 3) 0
_________________________________________________________________
block1_conv1 (Conv2D) (None, None, None, 64) 1792
_________________________________________________________________
block1_conv2 (Conv2D) (None, None, None, 64) 36928
_________________________________________________________________
block1_pool (MaxPooling2D) (None, None, None, 64) 0
_________________________________________________________________
block2_conv1 (Conv2D) (None, None, None, 128) 73856
_________________________________________________________________
block2_conv2 (Conv2D) (None, None, None, 128) 147584
_________________________________________________________________
block2_pool (MaxPooling2D) (None, None, None, 128) 0
_________________________________________________________________
block3_conv1 (Conv2D) (None, None, None, 256) 295168
_________________________________________________________________
block3_conv2 (Conv2D) (None, None, None, 256) 590080
_________________________________________________________________
block3_conv3 (Conv2D) (None, None, None, 256) 590080
_________________________________________________________________
block3_conv4 (Conv2D) (None, None, None, 256) 590080
_________________________________________________________________
block3_pool (MaxPooling2D) (None, None, None, 256) 0
_________________________________________________________________
block4_conv1 (Conv2D) (None, None, None, 512) 1180160
_________________________________________________________________
block4_conv2 (Conv2D) (None, None, None, 512) 2359808
_________________________________________________________________
block4_conv3 (Conv2D) (None, None, None, 512) 2359808
_________________________________________________________________
block4_conv4 (Conv2D) (None, None, None, 512) 2359808
_________________________________________________________________
block4_pool (MaxPooling2D) (None, None, None, 512) 0
_________________________________________________________________
block5_conv1 (Conv2D) (None, None, None, 512) 2359808
_________________________________________________________________
block5_conv2 (Conv2D) (None, None, None, 512) 2359808
_________________________________________________________________
block5_conv3 (Conv2D) (None, None, None, 512) 2359808
_________________________________________________________________
block5_conv4 (Conv2D) (None, None, None, 512) 2359808
_________________________________________________________________
block5_pool (MaxPooling2D) (None, None, None, 512) 0
=================================================================
Total params: 20,024,384
Trainable params: 20,024,384
Non-trainable params: 0
_________________________________________________________________
None

VGG19网络的结构图

5)定义内容损失

内容损失最小化,以保证目标图像和生成图像在VGG19卷积神经网络的顶层(即block5-conv2)具有相似结果。

6)定义风格损失函数
使用一个辅助函数来计算输入矩阵的格拉姆矩阵,即原始特征矩阵中相互关系的映射。

假设输入图像经过卷积后,得到的feature map为[ch, h, w]。我们经过flatten和矩阵转置操作,可以变形为[ ch, h*w]和[h*w, ch]的矩阵。再对两矩阵做内积得到[ch, ch]大小的矩阵,这就是我们所说的Gram Matrices,如下图所示:

7)定义总变差损失函数
除了以上两个损失函数,还需要一个总变差损失,它对生成的图像的像素进行正则化等操作,它促使生成图像具有空间的连续性,以避免结果过度像素化。

8)定义总损失函数
总损失是内容损失、风格损失、总变差损失的加权损失。网络顶层包含更加全局、更加抽象的信息,所以内容损失只使用一个顶层,即block5_conv2层;每层对都有不同风格,所以对风格损失需要使用一系列的层(block1_conv1、block2_conv1、block3_conv1、block4_conv1、block5_conv1)

9)L_BFGS算法简介
这里使用scipy中L_BFGS算法进行最优化。为便于大家理解该优化器,这里我们先简单介绍一下L_BFGS算法对应的函数格式及示例。
fmin_l_bfgs_b函数格式:

使用示例:

10)定义生成图像的优化器
通过优化器获取梯度、损失等

11)训练模型

12)可视化目标图、参考风格图、生成图等。

 

第22 章 神经网络黑箱不黑

人们常说,神经网络模型就像一个“黑盒”,这对一些神经网络模型确实如此,不过卷积神经网络,在可视化方面取得长足进步,我们可以看到卷积的中间结果、可视化不同的卷积核、可视化图像中类激活的热力图(决定类分类的关键区域)。
这三种方法的具体内容为:
1、卷积核输出的可视化(Visualizing intermediate convnet outputs (intermediate activations),即可视化卷积核经过激活之后的结果。能够看到图像经过卷积之后结果,帮助理解卷积核的作用
2、卷积核的可视化(Visualizing convnets filters),帮助我们理解卷积核是如何感受图像的。
3、热度图可视化(Visualizing heatmaps of class activation in an image),通过热度图,了解图像分类问题中图像哪些部分起到了关键作用,同时可以定位图像中物体的位置。

22.1 可视化中间结果

可视化中间结果,是指对于给定输入,展示网络中各个卷积层和池化层输出的特征图(层的输出通常是激活函数的输出,故又称为该层的激活)。
每个通道上特征图是相对独立的,我们可以将这些特征图可视化的正确方法是将每个通道的内容分别绘制成二维图像。
1)可视化下例模型的中间输出
本章模型cats_and_dogs_small_2.h5、图像cat.1700.jpg、creative_commons_elephant.jpg
下载地址

运行结果
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_13 (Conv2D) (None, 148, 148, 32) 896
_________________________________________________________________
max_pooling2d_13 (MaxPooling (None, 74, 74, 32) 0
_________________________________________________________________
conv2d_14 (Conv2D) (None, 72, 72, 64) 18496
_________________________________________________________________
max_pooling2d_14 (MaxPooling (None, 36, 36, 64) 0
_________________________________________________________________
conv2d_15 (Conv2D) (None, 34, 34, 128) 73856
_________________________________________________________________
max_pooling2d_15 (MaxPooling (None, 17, 17, 128) 0
_________________________________________________________________
conv2d_16 (Conv2D) (None, 15, 15, 128) 147584
_________________________________________________________________
max_pooling2d_16 (MaxPooling (None, 7, 7, 128) 0
_________________________________________________________________
flatten_5 (Flatten) (None, 6272) 0
_________________________________________________________________
dropout_5 (Dropout) (None, 6272) 0
_________________________________________________________________
dense_13 (Dense) (None, 512) 3211776
_________________________________________________________________
dense_14 (Dense) (None, 1) 513
=================================================================
Total params: 3,453,121
Trainable params: 3,453,121
Non-trainable params: 0

2)获取测试数据中的一张图像

运行结果
(1, 150, 150, 3)
转换为3D

(150, 150, 3)

3)可视化这个图像

4)抽取前8层的特征图或激活输出

5) 返回8个Numpy数组组成的列表,每个激活输出对应一个Numpy数组

(1, 148, 148, 32)
6) 查看第一层,第4个通道的激活输出图像

第4通道似乎是对角边缘检测器。
查看第7个通道的输出图像

第7通道似乎是圆点检测器,这对寻找猫眼睛非常有帮助。

7)把各通道组合成一个完整图形

上图从第1层到第8层,各通道的拼接图,从这些拼接图可以看出:
①第一层是各种边缘探测器的集合。在这一阶段,激活几乎保留了原始图像中的所有信息。
②随着层数的加深,激活变得越来越抽象,并且越来越难以直观理解。层数越深,关于图像视觉内容的信息越少,而关于类别的信息就越多。
③激活的稀疏性随着层数的加深而增大。

22.2 可视化卷积网络的过滤器

参考:https://blog.csdn.net/weiwei9363/article/details/79112872
https://www.jianshu.com/p/fb3add126da1
卷积核到底是如何识别物体的呢?想要解决这个问题,有一个方法就是去了解卷积核最感兴趣的图像是怎样的。我们知道,卷积的过程就是特征提取的过程,每一个卷积核代表着一种特征。如果图像中某块区域与某个卷积核的结果越大,那么该区域就越“像”该卷积核。
基于以上的推论,如果我们找到一张图像,能够使得这张图像对某个卷积核的输出最大,那么我们就说找到了该卷积核最感兴趣的图像。
具体思路:输入一张随机内容的图像I, 求某个卷积核F对图像的梯度 G=∂F/∂I,用梯度上升的方法迭代更新图像 I=I+η∗G,η为学习率。
我们可以从空白输入图像开始,将梯度下降应用于卷积神经网络输入图像的值,让某个过滤器的响应最大化。这样得到的输入图像就是选定过滤器具有最大响应的图形。
具体过程,
1)先构建一个损失函数,让某个卷积层的某个过滤器作用输入图像的激活值最大化;
2)使用随机梯度下降来调节输入图像的值,以便让这个激活值最大化。
以下我们以VGG16网络的block3_conv1层的第0个过滤器为例,
(1)首先构建有关过滤器激活值的损失函数。

(2)为了求相对于模型输入loss的梯度,可以使用keras的backend模块内置的gradients函数。

为便于计算梯度,使梯度用L2进行标准化处理

(3)利用keras后端函数计算loss及梯度
利用keras后端函数,可以根据一个输入图像,计算损失张量和梯度张量的值。

利用一个循环进行随机梯度下降,从而更新梯度

为便于可视化,对输入图像进行预处理。

(4)将以上代码整合到一个函数中

(5)可视化每一层前64个卷积核

block1_conv1 层前64个过滤器模式

block2_conv1 前64个过滤器模式

block3-conv1 前64过滤器模式

block4-conv1 前64个过滤器模式
结论:
低层的卷积核似乎对颜色,边缘信息感兴趣。
越高层的卷积核,感兴趣的内容越抽象(非常魔幻啊),也越复杂。
高层的卷积核感兴趣的图像越来越难通过梯度上升获得(block5_conv1有很多还是随机噪声的图像)

22.3 可视化类激活的热力图

1)CAM
在介绍Grad-CAM,Grad- Class Activation Mapping)之前,我们先介绍一下CAM。
我们日常生活中讲的热力图,是根据动物散发热量而形成的图形,图1中动物或人因为散发出热量,所以能够清楚的被看到。

图1
这次我们讲的深度学习中的类激活的热力图与此类似。
对一个深层的卷积神经网络而言,通过多次卷积和池化以后,它的最后一层卷积层包含了最丰富的空间和语义信息,再往下就是全连接层和softmax层了,如图2,其中所包含的信息都是难以理解的,难以可视化的方式展示出来。如果要让卷积神经网络的对其分类结果给出一个合理解释,充分利用好最后一个卷积层是关键。

图2
CAM借鉴了很著名的论文Network in Network中的思路,利用GAP(Global Average Pooling)替换掉了全连接层。可以把GAP视为一个特殊的average pool层,只不过其pool size和整个特征图一样大,其实就是求每张特征图所有像素的均值。具体可参考图3

图3

图4
GAP(参考图4)的优点在NIN的论文中说的很明确了:由于没有了全连接层,输入就不用固定大小了,因此可支持任意大小的输入;此外,引入GAP更充分的利用了空间信息,且没有了全连接层的各种参数,鲁棒性强,也不容易产生过拟合;还有很重要的一点是,在最后的 mlpconv层(也就是最后一层卷积层)强制生成了和目标类别数量一致的特征图,经过GAP以后再通过softmax层得到结果,这样做就给每个特征图赋予了很明确的意义。
我们重点看下经过GAP之后与输出层的连接关系(暂不考虑softmax层),实质上也是就是个全连接层,只不过没有了偏置项,如图4所示:

图5
从图5中可以看到,经过GAP之后,我们得到了最后一个卷积层每个特征图的均值,通过加权和得到输出(实际中是softmax层的输入)。需要注意的是,对每一个类别C,每个特征图k的均值都有一个对应的w,记为ω_k^c。CAM的基本结构就是这样了,下面就是和普通的CNN模型一样训练就可以了。训练完成后才是重头戏:我们如何得到一个用于解释分类结果的热力图呢?其实非常简单,比如说我们要解释为什么分类的结果是羊驼,我们把羊驼这个类别对应的所有ω_k^c取出来,求出它们与自己对应的特征图的加权和即可。由于这个结果的大小和特征图是一致的,我们需要对它进行上采样,叠加到原图上去,如图6所示。

图6
这样,CAM以热力图的形式告诉了我们,模型通过哪些像素确定这个图片是羊驼了
2)Grad-CAM
前面我们简单介绍了CAM,CAM的解释效果已经很不错了,但是它有一个不足,就是它要求修改原模型的结构,导致需要重新训练该模型,这大大限制了它的使用场景。如果模型已经上线了,或着训练的成本非常高,我们几乎是不可能为了它重新训练的。为了解决这个问题,人们就提出了Grad-CAM。
Grad-CAM的基本思路和CAM是一致的,也是通过得到每对特征图对应的权重,最后求一个加权和。但是它与CAM的主要区别在于求权重ω_k^c的过程。CAM通过替换全连接层为GAP层,重新训练得到权重,而Grad-CAM另辟蹊径,用梯度的全局平均来计算权重。事实上,经过严格的数学推导,Grad-CAM与CAM计算出来的权重是等价的。为了和CAM的权重做区分,定义Grad-CAM中第k个特征图对类别c的权重为ω_k^c,可通过下面的公式计算:

其中,Z为特征图的像素个数,y^c是对应类别c的分数(在代码中一般用logits表示,是输入softmax层之前的值),A_ij^k表示第k个特征图中,(i,j)位置处的像素值。求得类别对所有特征图的权重后,求其加权和就可以得到热力图。

Grad-CAM的整体结构如下图所示:

图7
注意这里和CAM的另一个区别是,Grad-CAM对最终的加权和加了一个ReLU,加这么一层ReLU的原因在于我们只关心对类别c有正影响的那些像素点,如果不加ReLU层,最终可能会带入一些属于其它类别的像素,从而影响解释的效果。使用Grad-CAM对分类结果进行解释的效果如下图所示:

图8
3)用Keras如何实现Grad-CAM?
①加载带有预训练权重的VGG16网络

Downloading data from https://github.com/fchollet/deep-learning-models/releases/download/v0.1/vgg16_weights_tf_dim_ordering_tf_kernels.h5
553467904/553467096 [==============================] - 2601s 5us/step
②预处理一张图片

查看预训练的VGG16网络,并将其预测向量解码为人们可读的格式。

redicted: [('n02504458', 'African_elephant', 0.90942115), ('n01871265', 'tusker', 0.08618273), ('n02504013', 'Indian_elephant', 0.004354583)]

从上面运行结果可以看出:
非洲象(African_elephant),占90%
长牙动物(tusker),占8%
印度象(Indian_elephant),占0.4%
网络识别出图像中包含数据量不确定的非洲象。预测向量中被最大激活的元素是对应“非洲象”类别元素(即类别概率最大项),索引编号为386

386
④ 实现Grad-CAM算法

⑥可视化类激活图

图9
⑦把原始图叠加在刚生成的热力图上

图10

参考资料:
《Python深度学习》弗朗索瓦•肖莱著
https://bindog.github.io/blog/2018/02/10/model-explanation/
http://spytensor.com/index.php/archives/20/(包括keras、pytorch实现Grad-CAM算法,class activation map)
http://spytensor.com/index.php/archives/19/(介绍GAP)

第21章 TensorFlow又一利器Tensorflow.js(基于浏览器开发测试机器学习)

21.1 TensorFlow.js主要功能

TensorFlow.js是一个JavaScript库,它可以将机器学习功能添加到任何Web应用程序中。使用TensorFlow.js,可以从头开发机器学习脚本。你可以使用API在浏览器或Node.js服务器应用程序中构建和训练模型。并且,你可以使用TensorFlow.js在JavaScript环境中运行现有模型。
甚至,你可以使用TensorFlow.js用自己的数据再训练预先存在的机器学习模型,这些其中包括浏览器中客户端可用的数据。例如,你可以使用网络摄像头中的图像数据。如果你是一名机器学习、深度学习爱好者,那么TensorFlow.js是学习的好方法!
TensorFlow.js利用 WebGL 来进行加速的机器学习类库,它基于浏览器,提供了高层次的 JavaScript API 接口。它将高性能机器学习构建块带到您的指尖,使您能够在浏览器中训练神经网络或在推理模式下运行预先训练的模型。
TensorFlow.js的主要功能包括:利用用js开发机器学习、运行已有模型、重新训练已有模型等,具体请看下图:

有关安装/配置 TensorFlow.js 的指南,请参阅:https://js.tensorflow.org/index.html#getting-started。

21.2 安装Node.js和NPM

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。 Node.js 的使用包管理器 npm来管理所有模块的安装、配置、删除等操作,使用起来非常方便。但是想要配置好npm的使用环境还是稍微有点复杂,下面跟着我一起来学习在windows系统上配置NodeJS和NPM吧。
具体安装大家可参考:
https://jingyan.baidu.com/article/48b37f8dd141b41a646488bc.html

21.3 Javascript(js)简介

1.JavaScript历史
要了解JavaScript,我们首先要回顾一下JavaScript的诞生。
在上个世纪的1995年,当时的网景公司正凭借其Navigator浏览器成为Web时代开启时最著名的第一代互联网公司。
由于网景公司希望能在静态HTML页面上添加一些动态效果,于是叫Brendan Eich这哥们在两周之内设计出了JavaScript语言。你没看错,这哥们只用了10天时间。
为什么起名叫JavaScript?原因是当时Java语言非常红火,所以网景公司希望借Java的名气来推广,但事实上JavaScript除了语法上有点像Java,其他部分基本上没啥关系。
2.ECMAScript
因为网景开发了JavaScript,一年后微软又模仿JavaScript开发了JScript,为了让JavaScript成为全球标准,几个公司联合ECMA(European Computer Manufacturers Association)组织定制了JavaScript语言的标准,被称为ECMAScript标准。
所以简单说来就是,ECMAScript是一种语言标准,而JavaScript是网景公司对ECMAScript标准的一种实现。
那为什么不直接把JavaScript定为标准呢?因为JavaScript是网景的注册商标。
不过大多数时候,我们还是用JavaScript这个词。如果你遇到ECMAScript这个词,简单把它替换为JavaScript就行了。
3.JavaScript版本
JavaScript的标准——ECMAScript在不断发展,最新版ECMAScript 6标准(简称ES6)已经在2015年6月正式发布了,所以,讲到JavaScript的版本,实际上就是说它实现了ECMAScript标准的哪个版本。想更详细了解JavaScript,大家可参考:
http://www.runoob.com/js/js-howto.html

21.4 TensorFlow.JS基础

21.4.1 主要概念

TensorFlow.js的主要概念,包括张量(Tensor)、变量(Variables)、操作(operations)、模型和层(models and layers)等,如下图所示。

21.4.2 张量

张量(Tensor)是TensorFlow中的主要数据单位。张量包含一组数值,可以是任何形状:一维或多维。当你创建新的张量时,你还需要定义形状(shape)。你可以通过使用tensor函数并传入第二个参数来定义形状,如下所示:

这是定义具有两行四列形状的张量。产生的张量如下所示

也可以让TensorFlow推断出张量的形状,如下列代码。

我们可以使用input.shape来检索张量的大小。

这里的形状为[2]。我们还可以创建具有特定大小的张量。例如,下面我们创建一个形状为[2,2]的零值张量。

这行代码创建了以下张量
[[0,0,0],
[0,0,0]]
此外,你可以使用以下函数来增强代码可读性:

tf.scalar:只有一个值的张量
tf.tensor1d:具有一个维度的张量
tf.tensor2d:具有两个维度的张量
tf.tensor3d:具有三维的张量
tf.tensor4d:具有四个维度的张量,
下图为常见的几种张量示意图:

如表示标量:

如表示二维张量:

在TensorFlow.js中,所有张量都是不可变的。这意味着张量一旦创建,之后就无法改变。如果你执行一个更改量值的操作,总是会创建一个新的张量并返回结果值。

21.4.3 变量

张量(Tensors) 是不可变的,一旦创建,不能改变其值;而变量(variables) 则可以动态改变其值,主要用于在模型训练期间存储和更新值。您可以使用assign方法为现有变量指定新值:

变量主要用于在模型训练期间存储然后更新参数值。

21.4.4 操作

通过使用TensorFlow操作,你可以操纵张量的数据。由于张量运算的不变性,结果值总是返回一个新的张量。
TensorFlow.js提供了许多有用的操作,如square,add,sub和mul。你可以直接应用操作,如下所示:。

执行此代码后,新张量包含以下值:
[[1, 4 ],
[9, 16]]

21.4.5 内存管理

由于TensorFlow.js使用GPU来加速数学运算,因此在使用张量和变量时需要管理GPU内存。
TensorFlow.js提供了两个函数来帮助解决这个问题:dispose和tf.tidy。
 dispose
我们可以在张量或变量上调用dispose来清除它并释放其GPU内存:

在进行大量张量操作时,使用dispose会很麻烦。 TensorFlow.js提供了另一个函数tf.tidy,它与JavaScript中的常规作用域起着类似的作用,但是对于GPU支持的张量。
tf.tidy执行一个函数并清除所创建的任何中间张量,释放它们的GPU内存。 它不会清除内部函数的返回值。

使用tf.tidy将有助于防止应用程序中的内存泄漏。 它还可以用于更加谨慎地控制何时回收内存。
【注意】
 传递给tf.tidy的函数应该是同步的,也不会返回Promise。 我们建议保留更新UI的代码或在tf.tidy之外发出远程请求。
 tf.tidy不会清理变量。 变量通常持续到机器学习模型的整个生命周期,因此TensorFlow.js即使它们是在tf.tidy的情况下创建的,也不会清理它们。 但是,您可以手动调用dispose来清理。

21.4.6 模型和层

从概念上讲,模型是一种函数,给定一些输入将产生一些所需的输出。在TensorFlow.js中,有两种方法可以创建模型。 您可以直接使用ops来表示模型所做的事情。 例如:

我们也可以使用高级API tf.model来构建层中的模型,这是深度学习中的流行方法。 以下代码构造了一个tf.sequential模型:

其中units. 激活输出的数量。由于这是最后一层,这里涉及20个类别的分类任务。units的数据量指节点数。
TensorFlow.js中有许多不同类型的层。 如tf.layers.simpleRNN,tf.layers.gru和tf.layers.lstm等。
【注意】
利用TensorFlow.js构建网络时,第一层必须明确指定输入形状,其余的层默认从前面的层输入。如下示例代码:
(1)首先,用tf.sequential()实例化构建模型model
将使用Sequential模型(最简单的模型类型),其中张量将连续地从一层传递到下一层。

(2)添加一个卷积层

 nputShape.将流入模型第一层的数据的形状。这里,我们的MNIST样本是28x28像素的黑白图像。图像数据的规范格式是[row,column,depth],所以我们在这里配置的形状是[28,28,1]——每个维度有28rowX28column个像素,而depth为1是因为我们的图像只有1个颜色通道。
 kernelSize. 应用于输入数据的滑动卷积滤波器窗口的大小。在这里,我们设置kernelSize为5,它表示一个5x5的正方形卷积窗口。
 filters. 应用于输入数据,大小为kernelSize的滤波器窗口的数量。在这里,我们将对数据应用8个过滤器。
 strides. 滑动窗口的“步长” - 即每次在图像上移动时,滤波器将移动多少个像素。在这里,我们指定步幅为1,这意味着过滤器将以1像素为单位滑过图像。
 activation.卷积完成后应用于数据的 激活函数。这里,我们使用了 Rectified Linear Unit (ReLU)函数,这是ML模型中非常常见的激活函数。
 kernelInitializer. 用于随机初始化模型权重的方法,这对于训练动态是非常重要的。我们不会详细介绍初始化的细节,这里VarianceScaling是一个很不错的初始化器。

(2)添加一个池化层

 poolSize. 应用于输入数据的滑动窗口大小。在这里,我们设置poolSize为[2,2],这意味着池化层将对输入数据应用2x2窗口。
 stride. 滑动窗口的“步长” - 即每次在输入数据上移动时,窗口将移动多少个像素。在这里,我们指定[2,2]的步长,这意味着滤波器将在水平和垂直两个方向上以2个像素为单位滑过图像。
【注意】
由于poolSize和strides都是2×2,所以池窗口将完全不重叠。这意味着池化层会将前一层的激活图的大小减半。
(3)再添加一个卷积层
重复使用层结构是神经网络中的常见模式。我们添加第二个卷积层到模型,并在其后添加池化层。请注意,在我们的第二个卷积层中,我们将滤波器数量从8增加到16。还要注意,我们没有指定inputShape,因为它可以从前一层的输出形状中推断出来。

(4)添加一个展平层
我们添加一个 flatten层,将前一层的输出平铺到一个向量中。

【注意】
展平层,既没有说明输入形状,也没有说明输出形状,这些形状都是从前层输出自动获取。
(5)输出层
最后,让我们添加一个 dense层(也称为全连接层),它将执行最终的分类。 在dense层前先对卷积+池化层的输出执行flatten也是神经网络中的另一种常见模式

 units. 激活输出的数量。由于这是最后一层,我们正在做10个类别的分类任务(数字0-9),因此我们在这里使用10个units。 (有时units被称为神经元的数量)
 kernelInitializer. 我们将对dense层使用与卷积层相同的VarianceScaling初始化策略。
 activation. 分类任务的最后一层的激活函数通常是 softmax。 Softmax将我们的10维输出向量归一化为概率分布,使得我们10个类中的每个都有一个概率值。

21.4.7 优化问题

这一部分,我们将学习如何解决优化问题。给定函数f(x),我们要求求得x=a使得f(x)最小化。为此,我们需要一个优化器。优化器是一种沿着梯度来最小化函数的算法。文献中有许多优化器,如SGD,Adam等等,这些优化器的速度和准确性各不相同。Tensorflowjs支持大多数重要的优化器。
我们将举一个简单的例子:f(x)=x⁶+2x⁴+3x²+x+1。函数的曲线图如下所示。可以看到函数的最小值在区间[-0.5,0]。我们将使用优化器来找出确切的值。

首先,我们定义要最小化的函数:

现在我们可以迭代地最小化函数以找到最小值。我们将以a=2的初始值开始,学习率定义了达到最小值的速度。我们将使用Adam优化器:

使用值为0.9的学习速率,我们发现200次迭代后的最小值对应的y为-0.16092407703399658。

更多内容可参考:
https://js.tensorflow.org/tutorials/core-concepts.html

21.5设置项目(使用npm)

(1)在第一步中,我们需要设置项目。创建一个新的空目录。

(2)切换到新创建的项目文件夹

以下操作,都在该文件夹下
(3)创建一个package.json文件
在文件夹中,我们现在准备创建一个package.json文件,以便我们能够通过使用Node.js包管理器来管理依赖项:

(4)安装Parcel捆绑器
因为我们将在项目文件夹中本地安装依赖项(例如Tensorflow.js库),所以我们需要为Web应用程序使用模块捆绑器(bundler)。为了尽可能简单,我们将使用Parcel Web应用程序捆绑器,因为Parcel不需要进行配置。让我们通过在项目目录中执行以下命令来安装Parcel捆绑器:

(5)创建两个空文件
接下来,让我们为我们的实现创建两个新的空文件:

(6)安装Bootstrap库
我们将Bootstrap库添加为依赖项,因为我们将为我们的用户界面元素使用一些Bootstrap CSS类:

(7)修改两个空文件
在index.html中,让我们插入以下基本html页面的代码:

另外,将以下代码添加到index.js

我们将文本Hello World写入具有ID输出的元素,以在屏幕上查看第一个结果并获得正确处理JS代码的确认。

(8)启动程序及web服务
最后,让我们通过使用parcel命令启动构建程序和开发的Web服务:

你现在应该可以在浏览器中通过URL http://localhost:1234打开网站。结果应与你在以下截图中看到的内容对应:

【注意】
以上步骤我们也可用yarn执行,npm与yarn的对应关系及优缺点,可参考:
https://juejin.im/entry/5a73ca7d6fb9a063435ea9ad
http://www.fly63.com/article/detial/554

21.6设置项目(使用yarn)

(1)在第一步中,我们需要设置项目。创建一个新的空目录。

(2)切换到新创建的项目文件夹

以下操作,都在该文件夹下
(3)创建一个package.json文件
在文件夹中,我们现在准备创建一个package.json文件,以便我们能够通过使用Node.js包管理器来管理依赖项:

(4)安装Parcel捆绑器
Parcel 是一个 web 应用打包工具, 与其他工具的区别在于开发者的使用体验。它利用多核处理器提供了极快的速度, 并且不需要任何配置。
因为我们将在项目文件夹中本地安装依赖项(例如Tensorflow.js库),所以我们需要为Web应用程序使用模块捆绑器(bundler)。为了尽可能简单,我们将使用Parcel Web应用程序捆绑器,因为Parcel不需要进行配置。让我们通过在项目目录中执行以下命令来安装Parcel捆绑器:

(5)创建两个空文件
接下来,让我们为我们的实现创建两个新的空文件:

(6)安装Bootstrap库
我们将Bootstrap库添加为依赖项,因为我们将为我们的用户界面元素使用一些Bootstrap CSS类:

将生成一个文件(yarn.lock)和一个文件夹(node_modules)
(7)修改两个空文件
在index.html中,让我们插入以下基本html页面的代码:

另外,将以下代码添加到index.js

我们将文本Hello World写入具有ID输出的元素,以在屏幕上查看第一个结果并获得正确处理JS代码的确认。

(8)启动程序及web服务
最后,让我们通过使用parcel命令启动构建程序和开发的Web服务:

执行该命令,将生成一个dist文件夹,同时更新相关文件。
你现在应该可以在浏览器中通过URL http://localhost:1234打开网站。当文件改变时它仍然会自动重建并支持热替换。结果应与你在以下截图中看到的内容对应:

【说明】

21.7实例详解

本实例利用tensorflow.js定义一个模型,该模型模拟一条直线(y=2x-1),然后,根据训练好的模型,在浏览器上,输入一个值,实时预测值,新体验,很不错哦!
(1)添加ensorflow.js
为了Tensorflow.js添加到项目中,我们再次使用NPM并在项目目录中执行以下命令

这将下载并将其安装到node_modules文件夹中。成功执行此命令后,我们现在可以通过在文件顶部添加以下import语句来导入index.js中的Tensorflow.js库:

当我们将TensorFlow.js导入为tf后,我们就可以通过在代码中使用tf对象来访问TensorFlow.js API。
(2)定义模型
现在TensorFlow.js已经可用,让我们从一个简单的机器学习练习开始。下面的示例应用程序涵盖的机器学习脚本是公式Y = 2X-1,这是个线性回归。
此函数返回给定X对应的Y值。如果绘制点(X,Y),你将得到一条直线,如下所示:

接下来我们将使用来自该函数的输入数据(X,Y)并使用这些数字训练模型。然后使用训练好的模型,根据新的X值来预测Y值。期望从模型返回的Y结果接近函数返回的精确值。

让我们创建一个非常简单的神经网络来实现。此模型只需处理一个输入值和一个输出值:

首先,我们通过调用tf.sequential方法创建一个新的模型实例。得到一个新的序列模型。其中一层的输出是下一层的输入,即模型是层的简单“堆叠”,没有分支或跳过。
创建好模型后,我们准备通过调用model.add来添加第一层。通过调用tf.layers.dense将新层传递给add方法。这会创建一个稠密层或全连接层。在稠密层中,层中的每个节点都连接到前一层中的每个节点。对于我们的示例,只需向神经网络添加一个具有一个输入和输出形状的密集层就足够了。

在下一步中,我们需要为模型指定损失函数和优化函数。

通过将配置对象传递给模型实例的编译方法来完成。配置对象包含两个属性:
 loss:这里我们使用meanSquaredError损失函数。通常,损失函数用于将一个或多个变量的值映射到表示与该值相关联的一些“损失”的实数上。如果训练模型,它会尝试最小化损失函数的结果。估计量的均方误差是误差平方的平均值 - 即估计值与估计值之间的平均平方差。
 optimizer:要使用的优化器函数。我们的线性回归机器学习任务使用的是sgd函数。Sgd代表Stochastic Gradient Descent,它是一个适用于线性回归任务的优化器函数。
现在模型已配置完成,接下来将训练模型。
(3)训练模型
为了用函数Y=2X-1的值训练模型,我们定义了两个形状为6,1的张量。第一张量xs包含x值,第二张量ys包含相应的y值:

把这两个张量传递给调用的model.fit方法来训练模型

对于第三个参数,我们传递一个对象,该对象包含一个名为epochs的属性,该属性设置为值500。此处指定的数字是指定TensorFlow.js通过训练集的次数。
fit方法的结果是一个Promise,所以我们注册一个回调函数,该函数在训练结束时被激活。
(4)预测
现在让我们在这个回调函数中执行最后一步,并根据给定的x值预测y值

使用model.predict方法完成预测。该方法以张量的形式接收输入值作为参数。在这个特定情况下,我们在内部创建一个只有一个值(5)的张量并将其传递给预测。通过调用print函数,我们确保将结果值打印到控制台,如下所示:

输出显示预测值为8.9962864并且非常接近9(如果x设置为5,函数Y=2X-1的Y值为9)
(5)优化界面
已上面经实现的示例是使用固定输入值进行预测(5)并将结果输出到浏览器控制台。让我们引入一个更复杂的用户界面,让用户能够输入用于预测的值。在index.html中添加以下代码:

这里我们使用各种Bootstrap CSS类,向页面添加输入和按钮元素,并定义用于输出结果的区域。

我们还需要在index.js中做一些更改:

注册了预测按钮的click事件的事件处理程序。在此函数内部,读取input元素的值(通过number函数把输入值转换为数据类型)并调用model.predict方法。此方法返回的结果将插入具有id输出的元素中。

现在的结果应该如下所示:

现在我们根据训练好的模型,输入值(x),就可实时预测Y值了!。单击“ Predict ”按钮完成预测。结果会直接显示在网站上。

(6)查看运行情况
通过管理平台(console),查看运行情况。

参考文档:
https://js.tensorflow.org/
https://cloud.tencent.com/developer/article/1346798

第20章 深度学习框架Pytorch快速入门

20.1 Pytorch简介

PyTorch 是 Torch 在 Python 上的衍生. 因为 Torch 是一个使用 Lua 语言的神经网络库,由于 PyTorch 采用了动态计算图(dynamic computational graph)结构,PyTorch 有一种独特的神经网络构建方法:使用和重放 tape recorder。而不是大多数开源框架,比如 TensorFlow、Caffe、CNTK、Theano 等采用的静态计算图。 使用 PyTorch,通过一种我们称之为「Reverse-mode auto-differentiation(反向模式自动微分)」的技术,你可以零延迟或零成本地任意改变你的网络的行为。
torch 产生的 tensor 放在 GPU 中加速运算 (前提是你有合适的 GPU), 就像 Numpy 会把 array 放在 CPU 中加速运。
torch是一个支持 GPU 的 Tensor 库,如果你使用 numpy,那么你就使用过 Tensor(即 ndarray)。PyTorch 提供了支持 CPU 和 GPU 的 Tensor。
pytorch版本变化

从 2015 年谷歌开源 TensorFlow 开始,深度学习框架之争越来越越激烈,全球多个看重 AI 研究与应用的科技巨头均在加大这方面的投入。从 2017 年年初发布以来,PyTorch 可谓是异军突起,短短时间内取得了一系列成果,成为了其中的明星框架。
PyTorch 1.0 预览版已出,稳定版发布在即,全新的版本融合了 Caffe2 和 ONNX 支持模块化、面向生产的功能,并保留了 PyTorch 现有的灵活、以研究为中心的设计。PyTorch 1.0 从 Caffe2 和 ONNX 移植了模块化和产品导向的功能,并将它们和 PyTorch 已有的灵活、专注研究的设计结合,已提供多种 AI 项目的从研究原型制作到产品部署的快速、无缝路径。利用 PyTorch 1.0,AI 开发者可以通过混合前端快速地实验和优化性能,该前端可以在命令式执行和声明式执行之间无缝地转换。PyTorch 1.0 中的技术已经让很多 Facebook 的产品和服务变得更强大,包括每天执行 60 亿次文本翻译。
pytorch的组成:
PyTorch由4个主要包装组成:
①.torch:类似于Numpy的通用数组库,可以在将张量类型转换为(torch.cuda.TensorFloat)并在GPU上进行计算。
②.torch.autograd:用于构建计算图形并自动获取渐变的包。
③.torch.nn:具有共同层和成本函数的神经网络库。
④.torch.optim:具有通用优化算法(如SGD,Adam等)的优化包。

20.2Pytorch安装配置

本章的环境:python3.6,pytorch0.4.1,windows
windows下安装pytorch0.4.1的方法如下:


登录pytorch官网(https://pytorch.org/),选择安装配置内容,具体如下:

在命令行输入以下安装命令:

验证pytorch安装是否成功及安装版本号:

运行结果
0.4.1

20.3Pytorch实例

我觉得入门最快、最有效的方法就是实战,通过一些实际案例的学习,收获往往要好于从简单概念入手。本节内容安排大致如下:
先从我们熟知的numpy开始,看如何使用numpy实现正向传播和反向传播开始,接着介绍与pytorch中与Numpy相似的Tensor,如何实现同样功能,然后依次介绍如何使用autograd、nn、optim等等模块实现正向传播和反向传播。最后介绍两个完整实例:一个是回归分析、一个是卷积神经网络。

20.3.1利用Numpy实现正向和反向传播(简单实例)

利用numpy进行正向传播和反向传播,我们先介绍一个简单实例,然后由此推广到一般情况,最后用Pytorch实现自动反向传播。
简单实例主要信息如下:
数据量(N)为1,输入数据维度或节点数(D_in)为2,隐含层节点(H)为2,输出层维度或节点数(D_out)为2,输入到隐含层的权值矩阵为w1,隐含层到输出层的权值矩阵为w2,隐含层的激活函数为Relu(np.maximum(h, 0)),网络结构及矩阵、向量信息如下:
简单示例的神经网络结构图为:


1、正向传播
正向传播示意图(看蓝色细线部分):

具体实现步骤如下:
(1)导入需要的库或模块
这个简单实例,主要用到numpy及相关运算。

(2)生成输入数据、权重初始值等

运行结果
x的值:[[1 2]],
y的值:[[1 2]],
w1的初始值:[[0. 0.1]
[0.2 0.3]],
w2的初始值:[[0. 0.1]
[0.2 0.3]]
(3)前向传播并计算预测值

运行结果
h的值:[[0.4 0.7]],
h_relu的值:[[0.4 0.7]],
y_pred的初始值:[[0.14 0.25]]

其中dot是numpy实现内积运算,该运算规则示意图如下:

 

(4)计算损失值

3.8021

2、反向传播
反向传播示意图(看绿色粗线部分)


(1)具体求导步骤如下

运行结果
对y_pred求导:[[-1.72 -3.5 ]],
对W2求导:[[-0.688 -1.4 ]
[-1.204 -2.45 ]],
对h_relu求导:[[-0.35 -1.394]]
h的值:[[-0.35 -1.394]],
对h求导:[[-0.688 -1.4 ]
[-1.204 -2.45 ]],
对w1求导:[[-0.35 -1.394]
[-0.7 -2.788]]
(2)根据梯度更新权重参数

运行结果

更新w1:[[3.50000000e-07 1.00001394e-01]
[2.00000700e-01 3.00002788e-01]],
更新w2:[[6.88000000e-07 1.00001400e-01]
[2.00001204e-01 3.00002450e-01]]
其中涉及对向量或矩阵求导公式推导可参考:
https://blog.csdn.net/DawnRanger/article/details/78600506
至此利用numpy求正向和反向传播就结束了,接下来,我们看一般情况,即包含批量数据、一般维度、对权重进行多次迭代运算。

20.3.2利用Numpy实现正向和反向传播(一般情况)

上节我们用一个简单实例,说明如何利用numpy实现正向和反向传播,有了这个基础之后,我们接下来介绍利用numpy实现一般情况的正向和反向传播,具体代码如下:

20.3.3Pytorch的Tensor实现正向和反向传播

Numpy是一个很棒的框架,但它不能利用GPU来加速其数值计算。 对于现代深度神经网络,GPU通常提供50倍或更高的加速,所以不幸的是,numpy对于现代深度学习来说还不够。
在这里,我们介绍最基本的PyTorch概念:Tensor。 PyTorch Tensor在概念上与numpy数组相同:Tensor是一个n维数组,PyTorch提供了许多用于在这些Tensors上运算的函数。 Tensors可以跟踪计算图和梯度,也可用作科学计算的通用工具。
与numpy不同,PyTorch Tensors可以利用GPU加速其数值计算。 要在GPU上运行PyTorch Tensor,只需将其转换为新的数据类型即可。
在这里,我们使用PyTorch Tensors将双层网络与随机数据相匹配。 像上面的numpy示例一样,我们需要手动实现通过网络的正向和反向传播:

20.3.4利用Tensor和autograd实现自动反向传播

在上面的例子中,我们不得不手动实现神经网络的前向和后向传递。手动实现反向传递对于小型双层网络来说并不是什么大问题,但对于大型复杂网络来说,很快就会变得非常繁琐。
是否有更高效的方法呢?我们可以使用自动微分来自动计算神经网络中的反向传播。 PyTorch中的autograd包提供了这个功能。使用autograd时,网络的正向传递将定义计算图形;图中的节点将是张量,边将是从输入张量产生输出张量的函数。通过此图反向传播,您可以轻松计算梯度。
这听起来很复杂,在实践中使用起来非常简单。每个Tensor代表计算图中的节点。如果x是具有x.requires_grad = True的Tensor,则x.grad是另一个Tensor,相对于某个标量值保持x的梯度。
在这里,我们使用PyTorch Tensors和autograd来实现我们的双层网络;现在我们不再需要手动实现通过网络的反向传播了!

如果不用with torch.no_grad来更新权重参数,我们可以使用优化器来实现,具体可参考优化器(optim部分)。

20.3.5拓展autograd

我们可以用autograd实现自动反向求导,如果我们想要自己写函数,而又不用自动求导,该如何实现?
实现的方式就是自己定义函数,实现它的正向和反向求导。
在PyTorch中,我们可以通过定义torch.autograd.Function的子类并实现前向和后向函数来轻松定义我们自己的autograd运算符。 然后我们可以使用我们的新autograd运算符,通过构造一个实例并像函数一样调用它,传递包含输入数据的Tensors。
在下面例子中,我们定义了自己的自定义autograd函数来执行ReLU,并使用它来实现我们的双层网络:

【小知识】
Function与Module都可以对pytorch进行自定义拓展,使其满足网络的需求,但这两者还是有十分重要的不同:

(1)Function一般只定义一个操作,因为其无法保存参数,因此适用于激活函数、pooling等操作;Module是保存了参数,因此适合于定义一层,如线性层,卷积层,也适用于定义一个网络
(2)Function需要定义三个方法:__init__, forward, backward(需要自己写求导公式);Module:只需定义__init__和forward,而backward的计算由自动求导机制构成
(3)可以不严谨的认为,Module是由一系列Function组成,因此其在forward的过程中,Function和Tensor组成了计算图,在backward时,只需调用Function的backward就得到结果,因此Module不需要再定义backward。
(4)Module不仅包括了Function,还包括了对应的参数,以及其他函数与变量,这是Function所不具备的。

20.3.6对比TensorFlow

PyTorch autograd看起来很像TensorFlow:在两个框架中我们定义了一个计算图,并使用自动微分来计算梯度。两者之间最大的区别是TensorFlow的计算图是静态的,PyTorch使用动态计算图。
在TensorFlow中,我们定义计算图一次,然后一遍又一遍地执行相同的图,可能将不同的输入数据提供给图。在PyTorch中,每个前向传递定义了一个新的计算图。
静态图很好,因为你可以预先优化图形;例如,框架可能决定融合某些图形操作以提高效率,或者提出一种策略,用于在多个GPU或许多机器上分布图形。如果您反复使用相同的图表,那么可以分摊这个代价可能高昂的前期优化,因为相同的图表会反复重新运行。
静态和动态图表不同的一个方面是控制流程。对于某些模型,我们可能希望对每个数据点执行不同的计算;例如,可以针对每个数据点针对不同数量的时间步长展开循环网络;这种展开可以作为循环实现。使用静态图形,循环结构需要是图形的一部分;因此,TensorFlow提供了诸如tf.scan之类的运算符,用于将循环嵌入到图中。使用动态图形情况更简单:因为我们为每个示例动态构建图形,我们可以使用常规命令流程控制来执行每个输入不同的计算。
与上面的PyTorch autograd示例相比,这里我们使用TensorFlow来拟合一个简单的双层网:

20.3.7高级封装(nn模块)

计算图和autograd是一个非常强大的范例,用于定义复杂的运算符并自动获取导数;然而,对于大型神经网络,原始autograd封装级别较低,需要编写很多代码。
在构建神经网络时,我们经常考虑将计算安排到层中,其中一些层具有可学习的参数,这些参数将在学习期间进行优化。
在TensorFlow中,像Keras,TensorFlow-Slim和TFLearn这样的软件包提供了对构建神经网络有用的原始计算图形的更高级别的抽象。
在PyTorch中也有更高一级的封装,nn包服务于同样的目的。 nn包定义了一组模块,它们大致相当于神经网络层。模块接收输入张量并计算输出张量,但也可以保持内部状态,例如包含可学习参数的张量。 nn包还定义了一组在训练神经网络时常用的有用损失函数。
有关torch.nn的进一步介绍,大家可参考:
http://blog.leanote.com/post/1556905690@qq.com/torch.nn
在这个例子中,我们使用nn包来实现我们的双层网络:

20.3.8 优化器(optim)

到目前为止,我们通过手动改变持有可学习参数的Tensors来更新模型的权重(使用torch.no_grad()或.data以避免在autograd中跟踪历史记录)。 对于像随机梯度下降这样的简单优化算法来说,这不是一个巨大的负担,但在实践中,我们经常使用更复杂的优化器如AdaGrad,RMSProp,Adam等来训练神经网络。
PyTorch中的optim包抽象出优化算法的思想,并提供常用优化算法的实现。
在这个例子中,我们将使用nn包像以前一样定义我们的模型,但我们将使用optim包提供的Adam算法优化模型: