pytorch tensor查找0_集智俱乐部——教你快速上手PyTorch

10d19195e3dffa1e449992c908dbc340.png

本书由国内最早研究人工智能、复杂系统的科学社区——集智俱乐部众包创作而成!

本书是一本系统介绍深度学习及开源框架PyTorch的入门书。全书注重实战,每章围绕一个有意思的实战案例展开,不仅循序渐进地讲解了PyTorch的基本使用、神经网络的搭建、卷积神经网络和循环神经网络的实现,而且全面深入地介绍了计算机视觉、自然语言处理、迁移学习,以及最新的对抗学习和深度强化学习等前沿技术。

本书含有10个经典实战项目,还配有12节入门视频课,43个高频问题答疑,名师手把手教学,零基础也能学,每天只花十分钟,轻松入门深度学习。

另外在B站还有作者视频讲解,请点击下方链接:

https://www.bilibili.com/video/av50239976。

分享文章试读:

第二章:PyTorch简介

PyTorch具有悠久的历史,它的前身Torch是用Lua写的机器学习框架,后来受到Facebook、NVIDIA(著名显卡生产厂商)、Uber等大公司以及斯坦福大学、卡内基梅隆大学等著名高校的支持。下面,就让我们走进PyTorch的世界。

2.1 PyTorch安装

PyTorch的安装非常简单。按照PyTorch官网的说明,我们只需要选择操作系统、Python的版本,以及显卡CUDA的版本,该网页就会提供一行命令,只需要在终端(Terminal,Windows系统就是命令行程序)输入相应的安装语句就可以安装了。例如,如果选择的操作系统是Linux,包管理器是Conda(需要事先安装Anancoda),Python版本3.6,Cuda版本8.0,则在命令行(终端)输入如下语句,就可以轻松安装好PyTorch:

conda install pytorch torchvision -c pytorch

另外,PyTorch官方提供了在Windows操作系统上安装PyTorch的方法,对于大多数Windows用户来说,这无疑提供了很大的便利。

注意,本书的代码都是在PyTorch 0.4.0版本中测试通过的,请不要使用0.4.0以下的PyTorch来运行本书的代码,否则可能会出现一些问题。另外本书的代码全部编写在Jupyter Notebook文档中。Jupyter Notebook是一个非常简单方便的IPython交互环境,强烈建议大家安装。

2.2 初识PyTorch

按照官方的说法,PyTorch具有如下3个最关键的特性:

□ 与Python完美融合(Pythonic);

□ 支持张量计算(Tensor computation);

□ 动态计算图(dynamic computation graph)。

下面,我们分别对这些特性进行说明。

2.2.1 与Python完美融合

与Python完美融合是指PyTorch是一个全面面向Python的机器学习框架,使用PyTorch与使用其他Python程序包没有任何区别。

与此形成鲜明对比的是TensorFlow,使用过的人都知道,TensorFlow会将一个深度学习任务分为定义计算图和执行计算的过程,而定义计算图的过程就好像在使用一套全新的语言。PyTorch就没有这个缺点,从定义计算图到执行计算是一气呵成的,用户丝毫感受不到使用Python和PyTorch的区别。

2.2.2 张量计算

PyTorch的运算单元叫作张量(tensor)。我们可以将张量理解为一个多维数组,一阶张量即为一维数组,通常叫作向量(vector);二阶张量即为二维数组,通常叫作矩阵(matrix);三阶张量即为三维数组;n 阶张量即为n维数组,有n 个下标。

图2.1所示为一个三阶张量。我们可以理解为三个矩阵,每个矩阵相当于一个二阶张量,都具有8行6列,其中每个单元都存储了一个实数,这些实数可能相同也可能不同。

710af2d096f615a4b9bb66fa44ac3040.png

我们将一个张量每个维度的大小称为张量在这个维度的尺寸(size)。例如,在图2.1所示的张量中,它首先由三个矩阵构成,所以它第一个维度的尺寸就是3;接下来,在每一个维度上,它又是一个矩阵,这个矩阵有8行,因此它第二个维度的尺寸就是8;对于任意一行,它又是一个长度为6的向量,因此它第三个维度的尺寸就是6。如果我们将这个三阶张量看作一个立方体,那么3、8、6就分别是这个立方体的长、宽、高。图2.1中最后一行(3, 8, 6)就标出了该张量在各个维度的尺寸。

1. 定义张量

下面,让我们来看看如何利用PyTorch定义一个张量。首先,需要导入PyTorch的包,我们只需要在Jupyter Notebook中输入以下命令即可:

import torch

接下来,我们可以创建一个尺寸为(5, 3)的二阶张量(也就是5行3列的矩阵)。我们希望其中每个元素的数值是随机赋予的一个[0, 1]区间中的实数,则只需要输入:

x=torch.rand(5,3) 
x

其中,第二行的x表示打印输出x的数值。系统的返回值如下(每次执行数值都会不同):

tensor([[0.3297, 0.7021, 0.1119],
        [0.6668, 0.6904, 0.1953],
        [0.6683, 0.4260, 0.2950],
        [0.0899, 0.4099, 0.0882],
        [0.4675, 0.8369, 0.1926]])

可以看到系统打印输出了一个5×3的矩阵。

下面再看一个例子,创建一个尺寸为(5, 3)、内容全是1的张量,并打印输出:

y=torch.ones(5,3) 
y

于是,系统输出如下:

tensor([[1., 1., 1.],
        [1., 1.,  1.],
        [1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.]])

接下来,让我们再创建一个三维(阶)的张量,尺寸为(2, 5, 3),内容全是0:

z=torch.zeros(2,5,3) 
z

系统输出如下:

tensor([[[0., 0., 0.],
         [0., 0., 0.],
         [0., 0., 0.],
         [0., 0., 0.],
         [0., 0., 0.]],

         [0., 0., 0.],
         [0., 0., 0.],
         [0., 0., 0.],
         [0., 0., 0.],
         [0., 0., 0.]]])

可以看到,系统输出的实际上是两个尺寸为(5, 3)、全是0的张量,这就是PyTorch表示二维以上张量的方法。按照这样的方法,我们可以构造出任意维度的张量。

2. 访问张量

接下来,让我们看看如何访问定义好的张量。其实上一个例子的输出已经提示我们可以使用如下方式访问张量中的元素:

z[0]

这表示访问z张量中的第一个元素(注意,张量的下标是从0开始的)。系统返回如下:

tensor([[0., 0., 0.],
        [0., 0., 0.],
        [0., 0., 0.],
        [0., 0., 0.],
        [0., 0., 0.]])

可以看到,它是一个尺寸为(5, 3)的张量。那么,如果想访问x张量中第2行第3列的数字,则可以使用下列Python指令:

x[1,2]

系统返回:

tensor(0.1119)

最后,我们还可以使用切片(slicing)的方法来访问张量,例如如果希望访问x中第3列的全部元素,则可以输入:

x[:,2]

系统返回:

tensor([0.1119, 0.1953, 0.2950, 0.0882, 0.1926])

第一个维度下标用“:”表示所有的行,“:”相当于一个通配符。其实,熟悉Python的读者应该已经发现了,PyTorch中的张量定义和访问方法与Python中NumPy数组的定义和访问没有什么区别,因此参考NumPy的各种语法和技巧来操作PyTorch的张量就可以了。

3. 张量的运算

PyTorch中的张量还可像NumPy的多维数组一样完成各种各样的运算。例如,张量可以相加:

z=x+y
z

返回结果:

tensor([[1.3297, 1.7021, 1.1119],
        [1.6668, 1.6904, 1.1953],
        [1.6683, 1.4260, 1.2950],
        [1.0899, 1.4099, 1.0882],
        [1.4675, 1.8369, 1.1926]])

当然,要保证x和y的尺寸一模一样才能相加。下面再来看看两个二维张量的矩阵乘法,其实这与两个矩阵相乘没有任何区别。这里需要调用PyTorch的mm(matrix multiply)命令,它的作用就是矩阵相乘。

q=x.mm(y.t())
q

返回如下:

tensor([[1.1993, 1.1993, 1.1993, 1.1993, 1.1993],
        [2.0680, 2.0680, 2.0680, 2.0680, 2.0680],
        [1.6001, 1.6001, 1.6001, 1.6001, 1.6001],
        [0.8946, 0.8946, 0.8946, 0.8946, 0.8946],
        [1.9811, 1.9811, 1.9811, 1.9811, 1.9811]])

注意,根据矩阵乘法的基本规则,输出的张量尺寸大小转变为(5, 5)。其中,y.t表示矩阵y的转置。因为x的尺寸为(5, 3),y的尺寸为(5, 3),而根据矩阵的乘法规则,第一个张量第二个维度的尺寸必须和第二个张量第一个维度的尺寸相等才能相乘,因此,将y转置之后得到y.t的尺寸是(3, 5),才可以和x相乘。

张量的基本运算还有很多,包括换位、索引、切片、数学运算、线性算法和随机数等,详见PyTorch官方文档,在此不再赘述。

4. 张量与NumPy数组之间的转换

既然PyTorch的张量与NumPy的多维数组如此之像,那么它们之间应该可以自由转换。PyTorch提供了NumPy和张量之间简单的转换语句。

从NumPy到张量的转换为from_numpy(a),其中a为一个NumPy数组。反过来,从张量到Numpy的转换可以使用a.numpy(),其中a为一个PyTorch张量。

例如,如果有:

import numpy as np

x_tensor = torch.randn(2,3) 
y_numpy = np.random.randn(2,3)

x_tensor是一个尺寸为(2, 3)的随机张量,y_numpy是一个尺寸为(2, 3)的随机矩阵。randn的意思是创建一个满足正态分布的随机张量或矩阵(rand是均匀分布的随机张量或矩阵)。

那么,我们可以将张量转化为NumPy:

x_numpy = x_tensor.numpy()

也可以将NumPy转化为张量:

y_tensor = torch.from_numpy(y_numpy)

除了这种直接转换,大多数时候还需要按照类型进行转换。例如,如果a是一个float类型的NumPy数组,那么可以用torch.FloatTensor(a)将a转化为一个float类型的张量。与此类似,如果a是一个int类型的NumPy数组,那么我们可以用torch.LongTensor(a)将a转化为一个整数类型的张量。

5. GPU上的张量运算

很多PyTorch中的张量运算都与NumPy中的数组运算一样,那么PyTorch为什么还要发明张量而不直接用NumPy的数组呢?答案就在于,PyTorch中的张量是可以在GPU中计算的,这大大提高了运算速度,而NumPy数组却不能。

首先,要完成GPU上的运算,需要确认你的计算机已经安装了GPU并且可以正常操作。可以用如下方法进行验证:

torch.cuda.is_available()

如果返回True就表明你的GPU已经正常安装,否则将无法使用GPU。当确认可以使用之后,你只需要将定义的张量放置到GPU上即可。例如,将x、y放到GPU上,你只需要输入如下代码即可:

if torch.cuda.is_available():
      x=x.cuda()
      y=y.cuda() 
      print(x+y)

返回如下:

tensor([[1.3297, 1.7021, 1.1119],
        [1.6668, 1.6904, 1.1953],
        [1.6683, 1.4260, 1.2950],
        [1.0899, 1.4099, 1.0882],
        [1.4675, 1.8369, 1.1926]], device='cuda:0')

注意,最后一行多出来的cuda:0表明当前这个输出结果x+y是存储在GPU上的。

当然,你的计算机版本设置也可能不支持GPU,如果你希望获得GPU资源,可以考虑在云服务器平台上运行。

我们也可以将已存储在GPU上的变量再“卸载”到CPU上,只需要输入以下命令即可:

x = x.cpu()

如果你的计算机本来就没有GPU,那么执行上述语句就会出错,所以最好是在前面加上if torch.cuda.is_available()进行判断。

2.2.3 动态计算图

人工神经网络之所以在诸多机器学习算法中脱颖而出,就是因为它可以利用反向传播算法来更新内在的计算单元,从而更加精准地解决问题。反向传播算法能够精确地计算出网络中每一个单元对于网络表现的贡献(即所谓的梯度信息),利用这种技术,就大大提高了神经网络的训练效率,从而避免了大量无效学习。

在深度学习框架出现之前,针对不同的神经网络架构,需要编写不同的反向传播算法,这就增加了难度和工作量。现在大多数深度学习框架都采用了计算图(computational graph)技术,于是我们就有了通用的解决方案,不需要为每一种架构的网络定制不同的反向传播算法,只需要关注如何实现神经网络的前馈运算即可。当前馈运算步骤完成之后,深度学习框架就会自动搭建一个计算图,通过这个图,就可以让反向传播算法自动进行。因此,计算图技术的出现大幅提升了构建神经计算系统的效率。这就是我们必须采用深度学习框架的原因。

计算图解决这一问题的基本思想就是将正向的计算过程步骤都用计算图记录下来。只要这些运算步骤是可微分(differentialable)的,那么我们就可以沿着计算图的路径进行任意变量的求导,于是便可以自动计算每个变量的梯度。因此,动态计算图是一种数值运算和符号运算(体现为微分)的综合,它是整套深度学习技术和框架中最重要的核心。

那么,究竟什么是计算图呢?它实际上是一种描述和记录张量运算过程的抽象网络。一张计算图包括两类节点,分别是变量(variable)和运算(computation)。计算图上的有向连边表示各个节点之间的因果联系或依赖关系,如图2.2所示。

6cfda9661e2ff952f8144961f4cc54b5.png

图中的方框节点为变量,椭圆节点为运算操作,它们彼此相连构成了一个有向无环图(directed acyclic graph,一种每条边都有方向且图中并不存在环路的特殊图)。箭头的方向表示该节点计算输入的来源方向,换句话说,沿着箭头的反方向前进就是一个多步计算进行的方向。

传统的深度学习框架(例如TensorFlow、Theano等)都采用了静态计算图技术,也就是将计算图的构建过程和利用该图进行计算的过程明确地分离开。这样做的好处是静态计算图可以反复地利用。而PyTorch既可以动态地构建计算图,又可以同时在计算图上执行计算,执行完计算后还可以继续构建计算图,而不必将构建计算图和图上的计算过程分离开。这样一来,PyTorch构造计算图的过程就像写普通的Python语句一样方便、快捷,而且,这种动态计算图技术也让代码的调试和追踪更加简便。

1. 自动微分变量

PyTorch是借助自动微分变量(autograd variable)来实现动态计算图的。从表面上看,自动微分变量与普通的张量没有什么区别,都可以进行各种运算,但是它的内部数据结构却比张量更复杂。在PyTorch 0.4中,自动微分变量已经与张量完全合并了,所以,任何一个张量都是一个自动微分变量。但是,为了与之前的PyTorch版本兼容,本书仍会使用自动微分变量,而非直接采用张量进行各种运算。PyTorch 0.4自动向下兼容,也就是说,它仍支持自动微分变量的使用。在采用自动微分变量以后,无论一个计算过程多么复杂,系统都会自动构造一张计算图来记录所有的运算过程。在构建好动态计算图之后,我们就可以非常方便地利用.backward()函数自动进行反向传播算法,从而计算每一个自动微分变量的梯度信息。

它是怎么做到的呢?自动微分变量是通过3个重要的属性data、grad以及grad_fn来实现的。

data是一个伴随着自动微分变量的张量,专门存储计算结果。我们平时可以将一个自动微分变量当成普通的张量来使用,几乎没有什么区别。这样,PyTorch会将计算的结果张量存储到自动微分变量的data分量里面。

此外,当采用自动微分变量计算的时候,系统会自动构建计算图,也就是存储计算的路径。对此,我们可以通过访问一个自动微分变量的grad_fn(旧版本是creator)来获得计算图中的上一个节点,从而知道是哪个运算导致了现在这个自动微分变量的出现。所以,每个节点的grad_fn其实就是计算图中的箭头。我们完全可以利用grad_fn来回溯每一个箭头,从而重构出整张计算图。

最后,当进行反向传播算法的时候,我们需要计算计算图中每一个变量节点的梯度值(grandient,即该变量需要被更新的增量)。我们只需要调用.backward()这个函数,就可以计算所有变量的梯度信息,并将叶节点的导数值存储在.grad中。

2. 动态计算图实例

下面我们举例说明如何构建计算图。比如,通过PyTorch的自动微分变量来计算y=x+2。

首先,导入PyTorch中自动微分变量的包:

from torch.autograd import Variable

然后,创建一个叫作x的自动微分变量,它包裹了一个尺寸为(2, 2)的张量,取值全为1。

x = Variable(torch.ones(2, 2), requires_grad=True) 
x

requires_grad=True是为了保证它可以在反向传播算法的过程中获得梯度信息。执行该语句的输出结果是:

tensor([[1.0, 1.0],
       [1.0, 1.0]])

可以看到,这个自动微分变量x与张量torch.ones(2, 2)没有太大的区别。这个x相当于是在张量torch.ones(2, 2)外面套了一层变量的外衣。这也是为什么在PyTorch 0.4版本后就已经不需要自动微分变量的概念了,普通的张量就是自动微分变量。在做运算时,PyTorch会自动开始构建计算图,目前这个计算图仅仅有一个x节点,如图2.3所示。

f9d9a10981281c072379da9c8a18025f.png

我们可以像对张量一样对自动微分变量进行各类运算,例如:

y = x + 2 
y

系统会完成张量加法的运算:

tensor([[3.0, 3.0],
        [3.0, 3.0]])

这时可以通过y.data查看y中存储的数值:

tensor([[3.0, 3.0],
        [3.0, 3.0]])

可以看到,这个返回值和y的返回值在0.4版本中没有任何区别。

接下来,我们可以尝试查看y的grad_fn:

y.grad_fn

返回的是:

<AddBackward0 at 0x7f7f796710f0>

从返回结果可知,y.grad_fn储存的是运算(AddBackward0)的信息,AddBackward0表示加上一个常数的运算,它是计算图上的一个运算节点。

所以,在执行完y=x+2之后,我们得到的计算图如图2.4所示。

29bb95d0782530523983602c0cdc0299.png

注意这里有两类节点,一类是方框形的表示自动微分变量的节点,另一类是椭圆形的运算节点。

接着,我们再进行运算y∗y:

z = y * y

*表示的是自动微分变量或张量之间的按元素乘法(两个张量在对应位置上进行数值相乘,这与矩阵运算mm是完全不一样的)。

接下来,通过z.grad_fn查看Variable z的grad_fn属性:

<MulBackward1 at 0x7f7f796711d0>

由上可知,z.grad_fn存储的是乘法运算的信息,Mul表示进行的是乘法运算。此时,动态计算图更新如图2.5所示。

3312c887be9a3d3dd005180518514111.png

注意,这时计算图将两个运算节点连接到了一起,而没有将变量z和y相连,也没有将*运算连接到z的后面。一般而言,计算图每增加一次计算就将相邻的两个运算节点连接在一起。
图2.5中每个变量节点右侧的数字对应的是它们data属性里存储的值,所以z当前的data就是一个张量[[9, 9], [9, 9]]。

我们再加一步计算:

t = torch.mean(z)

torch.mean(z)表示对z求平均,而z相当于一个矩阵,于是对矩阵的每个元素求和再除以元素的个数,就得到了平均值9。

在完成所有计算后,最终的计算图如图2.6所示。

1524de74cba4ea2c6f9f8f61fdacb59d.png

由此,关于计算图,我们可以总结如下。

□ 通过自动微分变量,PyTorch可以将运算过程全部通过.grad_fn属性记录下来,构成一个计算图;

□ 自动微分变量的运算结果存储在data中;

□ 每进行一步运算,PyTorch就将一个新的运算节点添加到动态计算图中,并与上一步的计算节点相连。

3. 自动微分与梯度计算

图2.6其实就是一个广义的神经网络,我们同样可以将反向传播算法应用到这个广义神经网络上,从而计算出每个变量节点的梯度信息。接下来,就让我们在这张图上完成简化版的反向传播算法,即计算每个节点的梯度信息。所谓的求梯度就是高等数学中的求导运算,梯度信息就是导数数值。

实际上,图2.6所代表的是一个函数:

ae01723aec992d6d8ab81fd2d593bc57.png

c94696e110e40073c2b4348809f61bce.png

我们当然可以手动计算导数,但是PyTorch提供了一个非常方便的数值计算方案,即通过.backward()进行自动求导计算:

t.backward()

执行完这个命令之后,PyTorch就会自动在图2.6上执行反向传播算法,从tx进行梯度(导数)的计算。这里所说的反向传播算法是指沿着计算图从下往上计算每个变量节点梯度信息的过程。之后,我们可以用.grad来查看每个节点的梯度:

print(z.grad)
print(y.grad)
print(x.grad)

PyTorch规定,只有计算图上的叶节点才可以通过.backward()获得梯度信息。zy不是叶节点,所以都没有梯度信息。于是,我们得到的输出如下:

None
None
tensor([[1.5000, 1.5000],
        [1.5000, 1.5000]])

55ab1187ece21c15db268e944a046156.png

注意,因为x 是一个张量,所以tx的求导也是一个同维度的张量(不熟悉的读者可以参考多变量微积分)。

由此可见,梯度或导数的计算可以自动化地进行,非常方便。无论函数依赖关系多么复杂,无论神经网络有多深,我们都可以通过backward()函数来完成梯度的自动计算,这就是PyTorch的优势。

为了说明backward()的厉害之处,也为了进一步理解动态计算图,让我们再来看一个例子。

首先,创建一个1×2的自动微分变量(1维向量)s:

s = Variable(torch.FloatTensor([[0.01, 0.02]]), requires_grad = True)

再创建一个2×2的矩阵型自动微分变量x:

x = Variable(torch.ones(2, 2), requires_grad = True)

用s反复乘以x(矩阵乘法)10次,注意s始终是自动微分变量:

for i in range(10):
     s = s.mm(x)

对s中的各个元素求均值,得到一个尺寸为(1, 1)的张量(也称为标量,即1×1张量):

z = torch.mean(s)

这个过程的动态计算图如图2.7所示。

a8811251819700310116ab55615d7820.png

同样,我们可以很轻松地计算叶节点变量的梯度信息:

z.backward() 
print(x.grad) 
print(s.grad)

得到的结果是:

tensor([[37.1200, 37.1200],
        [39.6800, 39.6800]])

None

所以,zx的偏导数为上述张量。由于s不是叶节点,所以梯度信息是None。xz在这里出现了多次,我们在计算梯度时,会为每一个节点计算梯度信息,之后再把同样命名的节点的梯度信息累加起来,作为该变量最终的梯度信息。

注意 只有叶节点才能计算grad信息,非叶节点不能计算。这是因为非叶节点大多都是计算单元,而有些非叶节点并不能对计算结果造成影响,因此也不能计算梯度信息。

2.3 PyTorch实例:预测房价

为了更好地理解前面所讲的概念,本节将引入一个实例问题:根据历史数据预测未来的房价。我们将实现一个线性回归模型,并用梯度下降算法求解该模型,从而给出预测直线。

这个实例问题是:假如有历史房价数据,我们应如何预测未来某一天的房价?针对这个问题,我们的求解步骤包括:准备数据、模型设计、训练和预测。

2.3.1 准备数据

按理说,我们需要找到真实的房价数据来进行拟合和预测。简单起见,我们也可以人为编造一批数据,从而重点关注方法和流程。

首先,我们编造一批假的时间数据。假设我们每隔一个月能获得一次房价数据,那么时间数据就可以为0, 1, …,表示第0、1、2……个月份,我们可以由PyTorch的linspace来构造0~100之间的均匀数字作为时间变量x:

x = Variable(torch.linspace(0, 100).type(torch.FloatTensor))

然后,我们再来编造这些时间点上的历史房价数据。假设它就是在x的基础上加上一定的噪声:

rand = Variable(torch.randn(100)) * 10 
y = x + rand

这里的rand是一个随机数,满足均值为0、方差为10的正态分布。torch.randn(100)这个命令可以生成100个标准正态分布随机数(均值0,方差1)。于是,我们就编造好了历史房价数据:y。现在我们有了100个时间点xi和每个时间点对应的房价yi。其中,每一个xi, yi称为一个样本点。

之后,我们将数据集切分成训练集和测试集两部分。所谓训练集是指训练一个模型的所有数据,所谓测试集则是指用于检验这个训练好的模型的所有数据。注意,在训练的过程中,模型不会接触到测试集的数据。因此,模型在测试集上运行的效果模拟了真实的房价预测环境。

在下面这段代码中,:-10是指从x变量中取出倒数第10个元素之前的所有元素;而-10:是指取出x中最后面的10个元素。所以,我们就把第0到90个月的数据当作训练集,把后10个月的数据当作测试集。

x_train = x[: -10]
x_test = x[-10 :]
y_train = y[: -10]
y_test = y[-10 :]

接下来,让我们对训练数据点进行可视化:

import matplotlib.pyplot as plt #导入画图的程序包

plt.figure(figsize=(10,8)) #设定绘制窗口大小为10*8 inch
#绘制数据,由于x和y都是Variable,需要用data获取它们包裹的Tensor,并转成Numpy plt.plot(x_train.data.numpy(), y_train.data.numpy(), 'o') 
plt.xlabel('X') #添加X轴的标注
plt.ylabel('Y') #添加Y轴的标注
plt.show() #画出图形

最终得到的输出图像如图2.8所示。

76eba3548c49805b9e848a3ee1afbe85.png

通过观察散点图,可以看出走势呈线性,所以可以用线性回归来进行拟合。

2.3.2 模型设计

b2af81233558e005720f1ed37ee2439e.png

并让它尽可能地小。其中N为所有数据点的个数,也就是100。由于xi, yi都是固定的数,而只有ab是变量,那么L本质上就是ab的函数。所以,我们要寻找最优的ab组合,让L最小化。

我们可以利用梯度下降法来反复迭代ab,从而让L越变越小。梯度下降法是一种常用的数值求解函数最小值的基本方法,它的基本思想就像是盲人下山。这里要优化的损失函数L(a, b)就是那座山。假设有一个盲人站在山上的某个随机初始点(这就对应了ab的初始随机值),他会在原地转一圈,寻找下降最快的方向来行进。所谓下降的快慢,其实就是Lab在这一点的梯度(导数);所谓的行进,就是更新ab的值,让盲人移动到一个新的点。于是,每到一个新的点,盲人就会依照同样的方法行进,最终停留在让L最小的那个点。

我们可以通过下面的迭代计算来实现盲人下山的过程:

ed35823adf54f7df8a6a49cd4b74f51a.png

为一个参数,叫作学习率,它可以调节更新的快慢,相当于盲人每一步的步伐有多大。

a越大,ab更新得越快,但是计算得到的最优值L 就有可能越不准。

在计算的过程中,我们需要计算出Lab的偏导数,利用PyTorch的backward()可以非常方便地将这两个偏导数计算出来。于是,我们只需要一步一步地更新ab的数值就可以了。当达到一定的迭代步数之后,最终的ab的数值就是我们想要的最优数值,y=ax+b这条直线就是我们希望寻找的尽可能拟合所有数据点的直线。

2.3.3 训练

接下来,就让我们将上述思路转化为PyTorch代码。首先,我们需要定义两个自动微分变量a和b:

a = Variable(torch.rand(1), requires_grad = True) 
b = Variable(torch.rand(1), requires_grad = True)

可以看到,在初始的时候,a和b都是随机取值的。设置学习率:

learning_rate = 0.0001

然后,完成对a和b的迭代计算:

a99566c1b7dd8dc9d033f5a4da44e60d.png

整个计算过程其实是利用自动微分变量 a和b来完成动态计算图的构建,然后在其上进行梯度反传的过程。所以,整个计算过程就是在训练一个广义的神经网络,a和b就是神经网络的参数,一次迭代就是一次训练。

另外,有以下几点技术细节值得说明。

□ 在计算predictions时,为了让abx的维度相匹配,我们对ab进行了扩维。我们知道,x_train的尺寸是(90, 1),而ab的尺寸都是1,它们并不匹配。我们可以通过使用expand_as提升ab的尺寸,与x_train一致。a.expand_as(x_train)的作用是将a的维度调整为和x_train一致,所以函数的结果是得到一个尺寸为(90, 1)的张量,数值全为a的数值。

□ 不能直接对一个自动微分变量进行数值更新,只能对它的data属性进行更新。所以在更新a的时候,我们是在更新a.data,也就是a所包裹的张量。

□ 在PyTorch中,如果某个函数后面加上了“_”,就表明要用这个函数的计算结果更新当前的变量。例如,a.data.add_(3)的作用是将a.data的数值更新为a.data加上3。

最后,将原始的数据散点联合拟合的直线通过图形画出来,如下所示:

x_data = x_train.data.numpy() #将x中的数据转换成NumPy数组
 plt.figure(figsize = (10, 7)) #定义绘图窗口
 xplot, = plt.plot(x_data, y_train.data.numpy(), 'o') #绘制x, y散点图
 yplot, = plt.plot(x_data, a.data.numpy() * x_data +b.data.numpy())  #绘制拟合直线图
 plt.xlabel('X') #给横坐标轴加标注
 plt.ylabel('Y') #给纵坐标轴加标注
 str1 = str(a.data.numpy()[0]) + 'x +' + str(b.data.numpy()[0]) #将拟合直线的参数a、b显示出来
 plt.legend([xplot, yplot],['Data', str1]) #绘制图例
 plt.show() #将图形画出来

最后得到的图像如图2.9所示。

6e936b29bcae1c55d318bd488e858f16.png

看来我们得到的拟合结果还不错。

2.3.4 预测

最后一步就是进行预测。在测试数据集上应用我们的拟合直线来预测出对应的y,也就是房价。只需要将测试数据的x值带入我们拟合好的直线即可:

predictions = a.expand_as(x_test) * x_test + b.expand_as(x_test) #计算模型的预测结果
predictions #输出

最终输出的预测结果为:

Variable containing:
 89.4647
 90.4586
 91.4525
 92.4465
 93.4404
 94.4343
 95.4283
 96.4222
 97.4162
 98.4101
[torch.FloatTensor of size 10]

那么,预测的结果到底准不准呢?我们不妨把预测数值和实际数值绘制在一起,如下所示:

x_data = x_train.data.numpy() #获得x包裹的数据
x_pred = x_test.data.numpy() #获得包裹的测试数据的自变量
plt.figure(figsize = (10, 7)) #设定绘图窗口大小
plt.plot(x_data, y_train.data.numpy(), 'o') #绘制训练数据
plt.plot(x_pred, y_test.data.numpy(), 's') #绘制测试数据
x_data = np.r_[x_data, x_test.data.numpy()]
plt.plot(x_data, a.data.numpy() * x_data + b.data.numpy()) #绘制拟合数据
plt.plot(x_pred, a.data.numpy() * x_pred + b.data.numpy(), 'o') #绘制预测数据
plt.xlabel('X') #更改横坐标轴标注
plt.ylabel('Y') #更改纵坐标轴标注
str1 = str(a.data.numpy()[0]) + 'x +' + str(b.data.numpy()[0]) #图例信息
plt.legend([xplot, yplot],['Data', str1]) #绘制图例
plt.show()

最终得到的图像如图2.10所示。

22fe25cd9af2d7ceeb20c1f19824c0d8.png

图中的方块点表示测试集中实际的房价数据,直线上的圆点则表示在测试集上预测的房价数据。我们还可以计算测试集上的损失函数,来检验模型预测的效果,在此不再赘述。

2.3.5 术语汇总

本节通过房价预测的线型回归模型说明了PyTorch的工作原理。实际上,这个简单的模型是各种复杂的神经网络模型的雏形。这个实例中使用的一些基本概念在后续的章节中会反复出现,因此,在此进行汇总,以便读者参考。

□ 模型:在本例中就是那条直线,这是我们对数据预测原理的基本假设。面对一组数据,我们可以假设它符合一条直线,也可以假设它符合一条更复杂的曲线,或者是一个复杂的神经网络,总之基本假设就是模型。

□ 拟合:将模型应用到训练数据上,并试图达到最佳匹配的过程。

□ 特征变量:在本例中,x(月份)就是特征变量,在一般情况下,特征变量可以是多个,它们构成了模型的自变量集合。也就是说,我们是根据数据中的特征变量来进行预
测的。

□ 目标变量:在本例中,y(房价)就是目标变量,它是模型要去拟合的目标。

□ 参数:在本例中,ab就是参数。在后续的例子中,神经网络的权重(weight)、偏置(bias)就是模型的参数。我们通过调整参数来达到最好的拟合效果。参数越多,往往拟合得越准,但是也越容易引起过拟合现象(将在下一章讲解)。

□ 损失函数:也就是本例中直线与数据点的平均距离L。在一般的机器学习中,我们总需要定义一个衡量模型好坏的损失函数(有时用平均误差,有时用交叉熵或似然函数),它通常是目标变量和模型预测值的函数,我们会根据这个函数来对模型进行优化,求出最优的参数组合。

□ 训练:反复调整模型中参数的过程。

□ 测试:检验训练好的模型的过程。

□ 样本:每一个数据点就叫作一个样本。

□ 训练集:用于训练模型的数据集合。

□ 测试集:用于检验模型的数据集合。

□ 梯度下降算法:在本例中体现为迭代计算ab的过程。根据梯度信息更新参数的算法,简单有效。

□ 训练迭代:反复利用梯度下降算法的循环过程。

除了这些在本例中用到的概念外,还有一个重要的概念,这就是超参数。在本例中没有用到超参数,但后面的章节会反复提及这个术语。

de7e283d41d6636cf3f3b36f574410b6.png

2.4 小结

本章我们对PyTorch进行了简介,讲解了一些最基本的概念,包括张量、自动微分变量、动态计算图等,这些概念是理解后续章节的重要基石。

PyTorch是与TensorFlow、MXNet、Caffe等平行的深度学习开源框架。然而,PyTorch最大的特点就在于简单易用,特别适合新手快速掌握。我们通过线性回归以及其他的简单示例代码展示了PyTorch的迷人优点:支持张量的计算和动态计算图,具有Python化的编程风格。

总结来看,PyTorch可以利用自动微分变量将一般的计算过程全部自动转化为动态计算图。所以,一个完整的算法就可以搭建一个计算图,也就是一个广义的神经网络。之后可以利用PyTorch强大的backward功能自动求导,利用复杂的计算图进行梯度信息的反传,从而计算出每个叶节点对应的自动微分变量的梯度信息值。有了这种值,我们就可以利用梯度下降算法来进行参数更新,也就是广义的训练或学习的过程。因此,PyTorch可以非常方便地将学习问题自动化。这种思想将贯穿本书所有的运算实例。

然而,这仅仅是一个开始,在本书后续章节中,我们还会利用PyTorch编写各种各样的深度学习程序,让我们拭目以待。

更多图书咨询请关注图灵社区。

f6cc250b1a84d14ea4636c70e3a37aa9.png
深度学习原理与PyTorch实战
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值