深度学习框架PyTorch入门与实践:第二章 快速入门

本章主要介绍两个内容,2.1节介绍如何安装PyTorch,以及如何配置学习环境;2.2节将带领读者快速浏览PyTorch中主要内容,给读者一个关于PyTorch的大致印象。

2.1 安装与配置

2.1.1 安装PyTorch

PyTorch是一款以Python语言主导开发的轻量级深度学习框架。在使用PyTorch之前,需要安装Python环境及其pip包管理工具,推荐使用Virtualenv配置虚拟Python环境。本书中所有代码使用PyTorch 0.3版本,同时兼容Python2和Python3,并全部在Python2环境中运行得到最终结果,在Python3环境测试未报错,但并不保证得到和Python2环境一致的结果。另外,本书默认使用Linux作为开发环境。

为方便用户安装使用,PyTorch官方提供了多种安装方法。本节将介绍几种常用的安装方式,读者可以根据自己的需求选用。

(1)使用pip安装

目前,使用pip安装PyTorch二进制包是最简单、最不容易出错,同时也是最适合新手的安装方式。从PyTorch官网选择操作系统、包管理器pip、Python版本及CUDA版本,会对应不同的安装命令。

以Linux平台、pip安装、Python2.7及CUDA8.0为例,安装命令如下(根据不同系统配置,可将pip改为pip2或pip3):

pip install http://download.pytorch.org/whl/cu80/torch-0.2.0.post3-cp27-cp27mu-manylinux1_x86_64.whl
pip install torchvision

安装好PyTorch之后,还需要安装Numpy,安装命令如下:

pip install --upgrade numpy

或者使用系统自带的包管理器(apt,yum等)安装Numpy,然后使用pip升级。

apt install python-numpy
pip install --upgrade numpy

全部安装完成后,打开Python,运行如下命令。

>>> import torch as t

没有报错则表示PyTorch安装成功。

安装过程中需要注意以下几点:

  • PyTorch对应的Python包名为torch而非pytorch。
  • 若需使用GPU版本的PyTorch,需要先配置英伟达显卡驱动,再安装PyTorch。

(2)使用conda安装

conda是Anaconda自带的包管理器。如果使用Anaconda作为Python环境,则除了使用pip安装,还可以使用conda进行安装。同样,在PyTorch官网选择操作系统、包管理器conda、Python版本及CUDA版本,对应不同的安装命令。我们在OS X下安装Python3.6、CPU版本的PyTorch为例介绍。

安装命令如下:

conda install pytorch torchvision -c soumith

conda的安装速度可能较慢,建议国内用户,尤其是教育网用户把conda源设置为清华tuna。在命令行输入如下命令即可完成修改。

conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
conda config --set show_channel_urls yes

即使是使用Anaconda的用户,也建议使用pip安装PyTorch,一劳永逸,而且不易出错。

(3)从源码编译安装

不建议新手从源码编译安装,因为这种安装方式对环境比较敏感,需要用户具备一定的编译安装知识,以及应对错误的能力。但若想使用官方未发布的最新功能,或某个BUG刚修复,官方还未提供二进制安装包,而读者又亟需这个补丁,此时就需要从GitHub上下载源码编译安装。

从源码编译安装,推荐使用Anaconda环境。如果想使用GPU版本,则需安装CUDA7.5及以上和cuDNN v5及以上(如果已装有CUDA,但不想被PyTorch使用,只需设置环境变量NO_CUDA=1)。

首先,安装可选依赖。
1)Linux

export CMAKE_PREFIX_PATH="$(dirname $(which conda))/../"
# 安装基础依赖
conda install numpy pyyaml mkl setuptools cmake gcc cffi
# 为GPU添加LAPACK支持
conda install -c soumith magma-cuda80 # 如果使用CUDA 7.5,则将magma-cuda80改成magma-cuda77
  1. OS
export CMKAE_PREFIX_PATH="$(dirnames $(which conda))/../"
conda install numpy pyyaml setuptools cmake cffi

其次,下载PyTorch源码。

git clone https://github.com/pytorch/pytorch
cd pytorch

最后,完成编译安装。
1)Linux

python setup.py install

2)OS

MACOSX_DEPLOYMMENT_TRACET=10.9 CC=clang CXX=clang++ python setup.py install

(4)使用Docker部署

Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用及依赖包到一个可移植的容器中,并发布到融合流行的Linux机器上,也可实现虚拟化。PyTorch官方提供了Dockerfile,支持CUDA和cuDNN v6。可通过如下命令构建Docker镜像。

docker build -t pytorch-cudnnv6 .

通过如下命令运行:

nvidia-docker run --rm -ti --ipc=host pytorch-cudnnv6

注意:PyTorch中数据加载(DataLoader)使用了大量的共享内存,可能超过容器限制,需设置–shm-size选项或使用–ipc=host选项解决。

(5)Windows用户安装PyTorch

Windows用户安装PyTorch跟Linux、OS差不多。同样,在PyTorch官网选择操作系统、包管理器pip、Python版本及CUDA版本,对应不同的安装命令。我们在Windows下安装Python3.7、GPU版本的PyTorch为例介绍。

pip3 install torch torchvision
2.1.2 学习环境配置

工欲善其事,必先利其器。在从事科学计算相关工作时,IPython和Jupyter是两个必不可少的工具。推荐使用IPython和Jupyter Notebook学习本书的示例代码。

(1)IPython

IPython是一个交互式计算系统,可认为是增强版的Python Shell,提供强大的REPL(交互式解析器)功能。对从事科学计算的用户来说,它提供方便的可交互式学习及调试功能。

安装IPython十分简单,对于Python2的用户,安装命令如下。

pip2 install ipython==5.1

IPython 5.x是最后一个支持Python2的IPython。Python3的用户可通过如下命令安装最新版的IPython。

pip install ipython

安装完成后,在命令行输入ipython即可启动IPython,启动界面如下。

ipython
Python 3.7.4 (default, Aug  9 2019, 18:34:13) [MSC v.1915 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 7.8.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]:

输入exit命令或者按Ctrl+D快捷键可退出IPython。IPython有许多强大的功能,其中最常用的功能如下。

  • 自动补全:IPython最方便的功能之一是自动补全,输入一个函数或者变量的前几个字幕,按下Tab键,就能实现自动补全,如下图:

深度学习框架PyTorch入门与实践:第二章 快速入门_第1张图片

  • 内省:所谓内省,主要是指在Runtime时获得一个对象的全部类型信息,这对实际的学习有很大的帮助。输入某一个函数或者模块之后,接着输入?可看到它对应的帮助文档,有些帮助文档比较长,可能跨页,这是可按空格键翻页,输入q退出。例如:
In [1]: import torch as t

In [2]: t.abs?
Docstring:
abs(input, out=None) -> Tensor

Computes the element-wise absolute value of the given :attr:`input` tensor.

.. math::
    \text{out}_{i} = |\text{input}_{i}|

Args:
    input (Tensor): the input tensor
    out (Tensor, optional): the output tensor

Example::

    >>> torch.abs(torch.tensor([-1, -2, 3]))
    tensor([ 1,  2,  3])
Type:      builtin_function_or_method

在函数或模块名之后输入两个问号,例如:t.FloatTensor??即可查看这个对象的源码,但只能查看对应Python的源码,无法查看C/C++的源码。

  • 快捷键:IPython提供了很多快捷键。例如,按上箭头可以重新输入上一条代码;一致按上箭头,可以追溯到之前输入的代码。按Ctrl+C快捷键可以取消当前输入或停止运行的程序。常用的快捷键如下所示。

深度学习框架PyTorch入门与实践:第二章 快速入门_第2张图片

  • 魔术方法:IPython中还提供了一些特殊的命令,这些命令以%开头,称为魔术命令(本质是执行本地系统的命令),例如可通过%hist查看当前IPython下的输入历史等,示例如下。
In [1]: import torch as t

In [2]: a = t.Tensor(3,4)

In [3]: %timeit a.sum()    # 检测某条语句的执行时间
3.24 µs ± 23.6 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [4]: %hist    # 查看输入历史
import torch as t
a = t.Tensor(3,4)
%timeit a.sum()
%hist

In [5]: %paste    # 执行粘贴板中的代码,如果只粘贴不执行使用Ctrl+Shift+V快捷键
def add(x,y,z):
    return x+y+z

## -- End pasted text --

In [6]: %cat a.py    # 查看文件内容
b = a + 1
print(b.size())

In [7]: %run -i a.py    # 执行文件,-i选项代表在当前命名空间中执行
torch.Size([3, 4])

In [8]: b
Out[9]:
tensor([[1., 1., 1., 1.],
        [1., 1., 1., 1.],
        [1., 1., 1., 1.]])

和普通Python对象一样,魔术方法也支持自省,因此也可以在命令后面加“?”或者"??"来查看对应的帮助文档或者源代码,例如通过%run?可查看它的使用说明。其他常用魔术命令如下所示。

深度学习框架PyTorch入门与实践:第二章 快速入门_第3张图片

“%xdel”与“del”的不同在于前者会删除其在IPython上的一切引用,具体例子如下。

In [1]: import torch as t

In [2]: a = t.Tensor(5,5)

In [3]: a
Out[3]:
tensor([[-4.2300e-33,  6.7823e-43, -1.1426e-29,  6.7823e-43, -1.1426e-29],
        [ 6.7823e-43, -1.1426e-29,  6.7823e-43,  9.8265e-39,  9.4592e-39],
        [ 1.0561e-38,  1.0653e-38,  1.0469e-38,  9.5510e-39,  9.0918e-39],
        [ 9.1837e-39,  8.4490e-39,  8.7245e-39,  1.4013e-45,  1.0653e-38],
        [ 0.0000e+00,  0.0000e+00,  4.6838e-39,  4.1327e-39,  0.0000e+00]])

In [4]: del a

In [5]: Out[3]
Out[5]:
tensor([[-4.2300e-33,  6.7823e-43, -1.1426e-29,  6.7823e-43, -1.1426e-29],
        [ 6.7823e-43, -1.1426e-29,  6.7823e-43,  9.8265e-39,  9.4592e-39],
        [ 1.0561e-38,  1.0653e-38,  1.0469e-38,  9.5510e-39,  9.0918e-39],
        [ 9.1837e-39,  8.4490e-39,  8.7245e-39,  1.4013e-45,  1.0653e-38],
        [ 0.0000e+00,  0.0000e+00,  4.6838e-39,  4.1327e-39,  0.0000e+00]])

In [6]: c = t.Tensor(1000,1000)

In [7]: c
Out[7]:
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.],
        [0., 0., 0.,  ..., 0., 0., 0.]])

In [8]: %xdel c

In [9]: Out[7]
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
 in 
----> 1 Out[7]

KeyError: 7
  • 粘贴:IPython支持多种格式的粘贴,除了%paste魔术方法,还可以直接粘贴多行代码、doctest代码和IPython代码,举例如下(下面的代码都使用Ctrl+V)快捷键的方式直接粘贴。如果是Linux终端,则应该使用Ctrl+Shift+V快捷键直接粘贴,或者单击鼠标右键,选择粘贴选项)。
In [1]: In [1]: import torch as t^M
   ...: ^M
   ...: In [2]: a = t.Tensor(5,5)^M
   ...: ^M
   ...: In [3]: a
Out[1]:
tensor([[9.1837e-39, 4.6837e-39, 9.9184e-39, 9.0000e-39, 1.0561e-38],
        [1.0653e-38, 4.1327e-39, 8.9082e-39, 9.8265e-39, 9.4592e-39],
        [1.0561e-38, 1.0653e-38, 1.0469e-38, 9.5510e-39, 9.1837e-39],
        [1.0561e-38, 1.0469e-38, 9.0000e-39, 1.0653e-38, 1.0194e-38],
        [1.0561e-38, 1.0469e-38, 9.9184e-39, 1.1020e-38, 9.1837e-39]])

In [2]: >>> import torch as t^M
   ...: >>> a = t.Tensor(5,5)^M
   ...: >>> a
Out[2]:
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.]])

In [3]: import torch as t^M
   ...: a = t.Tensor(5,5)^M
   ...: a
Out[3]:
tensor([[-1.2787e+02,  5.6472e-43, -3.6906e+05,  5.6472e-43, -3.6906e+05],
        [ 5.6472e-43, -3.6906e+05,  5.6472e-43, -1.3039e+02,  5.6472e-43],
        [-1.3693e+02,  5.6472e-43, -2.1370e+03,  5.6472e-43,  0.0000e+00],
        [ 0.0000e+00,  0.0000e+00,  0.0000e+00,  1.4013e-45,  0.0000e+00],
        [ 0.0000e+00,  0.0000e+00,  0.0000e+00,  0.0000e+00,  0.0000e+00]])
  • 使用IPython进行调试:IPython的调试器ipdb增强了pdb,提供了很多使用功能,例如Tab键自动补全、语法高亮等。在IPython中进入ipdb的最快速方式是使用魔术命令%debug,此时用户能够直接跳到报错的代码处,可通过u,d实现堆栈中的上下移动,常用的调试命令如下所示。

深度学习框架PyTorch入门与实践:第二章 快速入门_第4张图片

debug是一个重要功能,不仅在学习PyTorch时需要用到,在平时学习Python或使用IPython时也会经常用到。更多的debug功能,可通过h <命令>查看该命令的使用方法。

如果想在IPython之外使用debug功能,则需要安装ipdb(pip install ipdb),而后在需要进入调试的地方加上如下代码即可。

import ipdb
ipdb.set_trace()

当程序运行到这一步时,会自动进入debug模式。

(2)Jupyter Notebook

Jupyter Notebook是一个交互式笔记本,前身是IPython Notebook,后来从IPython中独立出来,现支持运行40多种编程语言。对希望编写漂亮的交互式文档和从科学计算的用户来说是一个不错的选择。

Jupyter Notebook的使用方法与IPython非常类似,推荐使用Jupyter Notebook主要有如下三个原因。

  • 更美观的界面:相比在终端下使用IPython,Notebook提供图形化操作界面,对新手而言更美观简洁。
  • 更好的可视化支持:Notebook与Web技术深度融合,支持在Notebook中直接可视化,这对需要经常绘图的科学运算实验来说很方便。
  • 方便远程访问:在服务端开启Notebook服务后,客户端只需有浏览器且能访问服务器,就可以使用服务器上的Notebook,这对于很多使用Linux服务器,但办公电脑使用Windows的人来说十分方便,避免了在本地配置环境的复杂流程。

安装Jupyter只需一条pip命令。

pip install jupyter

安装完成后,在命令行输入jupyter notebook命令即可启动Jupyter,此时浏览器会自动弹出,并打开Jupyter主界面,也可以手动打开浏览器,输入http://127.0.0.1:8888访问Jupyter,界面如下。

深度学习框架PyTorch入门与实践:第二章 快速入门_第5张图片

单击页面右上角的“new”选项,选择相应的Notebook类型(Python3/Python2),可新建一个Notebook,在In[]后面的编辑区域输入代码,按Ctrl+Enter快捷键即可运行代码,如下所示。

深度学习框架PyTorch入门与实践:第二章 快速入门_第6张图片

远程访问服务器Jupyter的用户需要在服务器中搭建Jupyter Notebook服务,然后通过浏览器访问。可以根据需要对Jupyter设置访问密码。

首先,打开IPython,设置密码,获取加密后的密码。

In [1] :  from notebook.auth import passwd

In [2] :  passwd()     # 输入密码
Enter password: 
Verify password: 
Out [2] : 'sha1:f9c17b4cc163:43b6b4c8c....'

sha1:f9c17b…即为加密后的密码,新建jupyter_config.py,输入如下配置。

# 加密后的密码
c.NotebookApp.password = u'sha1:f9c17b4cc163:43b6b4c8c....'

# ::绑定所有IP地址,包括IPv4/IPv6的地址,如果只想绑定某个IP,改成对应的IP即可
c.NotebookApp.ip = '::'

# 绑定的端口号,如果该端口号已经被占用,会自动使用下一个端口号10000
c.NotebookApp.port = 9999

其次,启动Jupyter Notebook并指定配置文件,输入如下命令。

jupyter notebook --config=jupyter_config.py

最后,客户端打开浏览器,访问url http://[服务器IP]:9999,输入密码,即可访问Jupyter。

若客户端浏览器无法打开Jupyter,有可能是防火墙的缘故,输入如下命令开放对应的端口(若使用IPv6,把命令iptables改成ip6tables)。

iptables -I INPUT -p tcp --dport 9999 -j ACCEPT
iptables save

Jupyter的使用和IPython极为类似,我们介绍的IPython使用技巧对Jupyter基本都适用。它支持自动补全、自省、魔术方法、debug等功能,但它的快捷键与IPython有较大不同,可以通过菜单栏的【help】->【Keyboard Shortcuts】查看详细的快捷键。

Jupyter还支持很多功能,如Markdown语法、HTML、各种可视化等。更多关于IPython和Jupyter Notebook的使用技巧,读者可以从网上找到很多学习资源,这里只介绍一些最基础的、本书会用到的内容。

2.2 PyTorch入门第一步

PyTorch的简洁设计使得它易于入门,在深入介绍PyTorch之前,本节先介绍一些PyTorch的基础知识,使读者能够对PyTorch有一个大致的了解,并能够用PyTorch搭建一个简单的神经网络。部分内容读者可能不太理解,可先不予研究,本书的第3章和第4章将会对此进行深入讲解。

本节内容参考了PyTorch官方教程并做了相应的增删,使得内容更贴合新版本的PyTorch接口,同时也更适合新手快速入门。另外,本书需要读者先掌握基础的numpy使用,numpy的基础知识可以参考CS231n上关于numpy的教程。

2.2.1 Tensor

Tensor是PyTorch中重要的数据结构,可认为是一个高维数组。它可以是一个数(标量)、一维数组(向量)、二维数组(矩阵)或更高维的数组。Tensor和numpy的ndarrays类似,但Tensor可以使用GPU加速。Tensor的使用和numpy及MATLAB的接口十分相似,下面通过几个示例了解Tensor的基本使用方法。

In [1] :  from __future__ import print_function
            import torch as t
            x = t.Tensor(5,3)    # 构建5 * 3矩阵,只是分配了空间,未初始化
            x
Out [1] :  tensor([[1.0561e-38, 1.0653e-38, 4.1327e-39],
                           [8.9082e-39, 9.8265e-39, 9.4592e-39],
                           [1.0561e-38, 1.0653e-38, 1.0469e-38],
                           [9.5510e-39, 8.7245e-39, 9.0918e-39],
                           [1.0102e-38, 9.6429e-39, 8.7245e-39]])

In [2] :  x == t.rand(5,3)    # 使用[0,1]均匀分布随机初始化二维数组
            x
Out [2] :  tensor([[1.0561e-38, 1.0653e-38, 4.1327e-39],
                           [8.9082e-39, 9.8265e-39, 9.4592e-39],
                           [1.0561e-38, 1.0653e-38, 1.0469e-38],
                           [9.5510e-39, 8.7245e-39, 9.0918e-39],
                           [1.0102e-38, 9.6429e-39, 8.7245e-39]])

In [3] :  print(x.size())    # 查看x的形状
            torch.Size([5, 3])

torch.Size是tuple对象的子类,因此它支持tuple的所有操作,如x.size()[0]。

In [4] :  y = t.rand(5,3)
            x + y    # 加法的第一种写法
Out [4] :  tensor([[0.6707, 0.6551, 0.5273],
                           [0.5378, 0.6252, 0.1105],
                           [0.3754, 0.6970, 0.5691],
                           [0.8858, 0.6057, 0.9119],
                           [0.2084, 0.0882, 0.7523]])

In [5] :  t.add(x,y)    # 加法的第二种写法
Out [5] :  tensor([[0.6707, 0.6551, 0.5273],
                           [0.5378, 0.6252, 0.1105],
                           [0.3754, 0.6970, 0.5691],
                           [0.8858, 0.6057, 0.9119],
                           [0.2084, 0.0882, 0.7523]])

In [6] :  # 加法的第三种写法:指定加法结果的输出目标为result
            result = t.Tensor(5,3)    # 预先分配空间
            t.add(x,y,out=result)     # 输出到result
            result
Out [6] :  tensor([[0.6707, 0.6551, 0.5273],
                           [0.5378, 0.6252, 0.1105],
                           [0.3754, 0.6970, 0.5691],
                           [0.8858, 0.6057, 0.9119],
                           [0.2084, 0.0882, 0.7523]])

In [7] : x = t.Tensor([1,2,3])    # 使用list定义Tensor
           y = t.Tensor([1,1,1])    # 使用list定义Tensor

In [8] : x.add(y)        # 普通加法,x的值不变
           x
Out [8] :  tensor([1., 2., 3.])

In [9] : x.add_(y)      # inplace加法,x的值改变
           x
Out [9] :  tensor([2., 3., 4.])

注意:函数名后面带下划线的函数会修改Tensor本身。例如:x.add_(y)和x.t_()会改变x,但x.add(y)和x.t()会返回一个新的Tensor,而x不变。

In [10] :  x = t.rand(3,4)
              x
Out [10] :  tensor([[0.1282, 0.3337, 0.5191, 0.5905],
                             [0.1274, 0.4602, 0.9954, 0.8498],
                             [0.8286, 0.3794, 0.6181, 0.8584]])
In [11] :  x[:,1]      # Tensor的选取操作与numpy类似
Out [11] :  tensor([0.3337, 0.4602, 0.3794])

Tensor还支持很多操作,包括数学运算、线性代数、选择、切片等,其接口设计与numpy极为相似。更详细的使用方法会在第3章系统讲解。

Tensor和numpy的数组间的互操作非常容易且快速。Tensor不支持的操作,可以先转为numpy数组处理,之后再转回Tensor。

In [12] :  a = t.ones(5)
              a
Out [12] :  tensor([1., 1., 1., 1., 1.])

In [13] :  b = a.numpy()        # Tensor —> numpy
              b
Out [13] :  array([1., 1., 1., 1., 1.], dtype=float32)

In [14] :  import numpy as np
              a = np.ones(5)
              a 
Out [14] :  array([1., 1., 1., 1., 1.])

In [15] :  b = t.from_numpy(a)        # numpy —> Tensor
              b
Out [15] :  tensor([1., 1., 1., 1., 1.], dtype=torch.float64)

Tensor和numpy对象共享内存,所以它们之间的转换很快,而且几乎不需要消耗资源。这也意味着,如果其中一个变了,另外一个也会随之改变。

In [16] :  b.add_(1)      # 以下划线结尾的函数会修改自身
              b
Out [15] :  tensor([2., 2., 2., 2., 2.], dtype=torch.float64)

In [17] :  a
Out [17] :  array([2., 2., 2., 2., 2.])

Tensor可通过.cuda()方法转为GPU的Tensor,从而享受GPU带来的加速运算。

In [18] :  x = t.Tensor([1,1,1])
              y = t.Tensor([2,2,2])
              # 在不支持CUDA的机器上,下一步不会运行
              if t.cuda.is_available():
                  x = x.cuda()
                  y = x.cuda()
              x + y
Out [18] :  tensor([2., 2., 2.], device='cuda:0')

在此处可能会发现CPU运算的速度并未提升太多,这是因为x和y太小且运算也较简单,而且将数据从内存转移到显存还需要花费额外的开销。GPU的优势在大规模数据和复杂运算下才能体现出来。

2.2.2 Autograd:自动微分

自动微分的算法本质上是通过反向传播求导数,PyTorch的Autograd模块实现了此功能。在Tensor上的所有操作,Autograd都能为它们自动提供微分,避免手动计算导数的复杂过程。

autograd.Variable是Autograd的核心类,它简单封装了Tensor,并支持几乎所有Tensor的操作。Tensor在被封装为Variable之后,可以调用它的.backward实现反向传播,自动计算所有梯度。Variable的数据结构如下所示。

深度学习框架PyTorch入门与实践:第二章 快速入门_第7张图片

Variable主要包含三个属性。

  • data:保存Variable所包含的Tensor。
  • grad:保存data对应的梯度,grad也是个Variable,而不是Tensor,它和data的形状一样。
  • grad_fn:指向一个Function对象,这个Function用来计算反向传播计算输入的梯度,具体细节会在第3章讲解。
In [19] :  from torch.autograd import Variable      # 导入Variable

In [20] :  x = Variable(t.ones(2,2),requires_grad=True)      # 使用Tensor新建一个Variable
              x
Out [20] :  tensor([[1., 1.],
                             [1., 1.]], requires_grad=True)

In [21] :  y = x.sum()
              y
Out [21] :  tensor(4., grad_fn=)

In [22] :  y.grad_fn
Out [22] :  

In [23] :  y.backward()
              # y = x.sum = (x[0][0] + x[0][1] + x[1][0] + x[1][1])
              # 每个值的梯度都为1
              x.grad
Out [23] :  tensor([[1., 1.],
                             [1., 1.]])

注意:grad在反向传播过程中是累加的(accumulated),这意味着每次运行反向传播,梯度都会累加之前的梯度,所以反向传播之前需把梯度清零。

In [24] :  y.backward()
              x.grad
Out [24] :  tensor([[2., 2.],
                             [2., 2.]])

In [25] :  y.backward()
              x.grad
Out [25] :  tensor([[3., 3.],
                             [3., 3.]])

In [26] :  x.grad.data.zero_()      # 以下划线结束的函数是inplace操作

In [27] :  y.backward()
              x.grad
Out [27] :  tensor([[1., 1.],
                             [1., 1.]])

Variable和Tensor具有近乎一致的接口,在实际使用中可以无缝切换。

In [28] :  x = Variable(t.ones(4,5))
              y = t.cos(x)
              x_tensor_cos = t.cos(x.data)
              print(y)
              x_tensor_cos
tensor([[0.5403, 0.5403, 0.5403, 0.5403, 0.5403],
            [0.5403, 0.5403, 0.5403, 0.5403, 0.5403],
            [0.5403, 0.5403, 0.5403, 0.5403, 0.5403],
            [0.5403, 0.5403, 0.5403, 0.5403, 0.5403]])
Out [28] :  tensor([[0.5403, 0.5403, 0.5403, 0.5403, 0.5403],
                             [0.5403, 0.5403, 0.5403, 0.5403, 0.5403],
                             [0.5403, 0.5403, 0.5403, 0.5403, 0.5403],
                             [0.5403, 0.5403, 0.5403, 0.5403, 0.5403]])
2.2.3 神经网络

Autograd实现了反向传播功能,但是直接用来写深度学习的代码在很多情况下还是稍显复杂,torch.nn是专门为神经网络设计的模块化接口。nn构建于Autograd之上,可用来定义和运行神经网络。nn.Module是nn中最重要的类,可以把它看做是一个网络的封装,包括网络各层定义以及forward方法,调用forward(input)方法,可返回前向传播的结果。我们以最早的卷积神经网络LeNet为例,来看看如何用nn.Module实现。LeNet的网络结构如下所示。

深度学习框架PyTorch入门与实践:第二章 快速入门_第8张图片

这是一个基础的前向传播(feed-forward)网络:接收输入,经过层层传递运算,得到输出。

(1)定义网络

定义网络时,需要继承nn.Module,并实现它的forward方法,把网络中具有可学习参数的层放在构造函数__init__中。如果某一层(如ReLU)不具有可学习的参数,则既可以放在构造函数中,也可以不放。

import torch.nn as nn
import torch.nn.functional as F


class Net(nn.Module):
    def __init__(self):
        # nn.Module子类的函数必须在构造函数中执行父类的构造函数
        # 下式等价于nn.Module.__init__()
        super(Net,self).__init__()
        # 卷积层:‘1’表示输入图片为单通道,‘6’表示输出通道数,‘5’表示卷积核为5 * 5
        self.conv1 = nn.Conv2d(1,6,5)
        # 卷积层:
        self.conv2 = nn.Conv2d(6,16,5)
        # 仿射层/全连接层,y = Wx + b
        self.fc1 = nn.Linear(16*5*5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)
        
    def forword(self,x):
        # 卷积 -> 激活 -> 池化
        x = F.max_pool2d(F.relu(self.conv1(x)), (2,2))
        x = F.max_pool2d(F.relu(self.conv2(x)), 2)
        # reshape, '-1'表示自适应
        x = x.view(s.size()[0],-1)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x


net = Net()
print(net)

输出如下:

Net(
  (conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
  (conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
  (fc1): Linear(in_features=400, out_features=120, bias=True)
  (fc2): Linear(in_features=120, out_features=84, bias=True)
  (fc3): Linear(in_features=84, out_features=10, bias=True)
)

只要在nn.Module的子类中定义了forward函数,backward函数就会被自动实现(利用Autograd)。在forward函数中可使用任何Variable支持的函数,还可以使用if、for循环、print、log等Python语法,写法和标准的Python写法一致。

网络的可学习参数通过net.parameters()返回,net.named_parameters可同时返回可学习的参数及名称。

params = list(net.parameters())
print(len(params))

输出如下:

10
for name,parameters in net.named_parameters():
    print(name,':',parameters.size())

输出如下:

conv1.weight : torch.Size([6, 1, 5, 5])
conv1.bias : torch.Size([6])
conv2.weight : torch.Size([16, 6, 5, 5])
conv2.bias : torch.Size([16])
fc1.weight : torch.Size([120, 400])
fc1.bias : torch.Size([120])
fc2.weight : torch.Size([84, 120])
fc2.bias : torch.Size([84])
fc3.weight : torch.Size([10, 84])
fc3.bias : torch.Size([10])

foward函数的输入和输出都是Variable,只有Variable才具有自动求导功能,Tensor是没有,所以在输入时,需要把Tensor封装成Variable。

input = Variable(t.randn(1,1,32,32))
out = net(input)
out.size()

输出如下:

torch.Size([1, 10])

所有参数的梯度清零。

net.zero_grad()    # 所有参数的梯度清零
out.backward(Variable(t.ones(1,10)))    # 反向传播

需要注意的是,torch.nn只支持mini-batch,不支持一次只输入一个样本,即一次必须是一个batch。如果只想输入一个样本,则用input.unsqueeze(0)将batch_size设为1。例如,nn.Conv2d输入必须是4维的,形如nSamples * nChannels * Height * Width。可将nSamples设为1,即1 * nChannels * Height * Width。

(2)损失函数

nn实现了神经网络中大多数的损失函数,例如nn.MSELoss用来计算均方误差,nn.CrossEntropyLoss用来计算交叉熵损失。

output = net(input)
target = Variable(t.arange(0.0,10))
criterion = nn.MSELoss()
loss = criterion(output, target)
loss

输出如下:

tensor(28.5165, grad_fn=)

如果对loss进行反向传播溯源(使用grad_fn属性),可以看到它的计算图如下。

input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
-> view -> linear -> relu -> linear -> relu -> linear -> MSELoss -> loss

当调用loss.backward()时,该图会动态生成并自动微分,也会自动计算图中参数(Parameter)的导数。

# 运行.backward,观察调用之前和调用之后的grad
net.zero_grad()  # 把net中所有可学习的参数的梯度清零
print('反向传播之前conv1.bias的梯度')
print(net.conv1.bias.grad)
loss.backward()
print('反向传播之后conv1.bias的梯度')
print(net.conv1.bias.grad)

输出如下:

# 运行.backward,观察调用之前和调用之后的grad
net.zero_grad()  # 把net中所有可学习的参数的梯度清零
print('反向传播之前conv1.bias的梯度')
print(net.conv1.bias.grad)
loss.backward()
print('反向传播之后conv1.bias的梯度')
print(net.conv1.bias.grad)

(3)优化器

在反向传播计算完所有参数的梯度后,还需要使用优化方法更新网络的权重和参数。例如,随机梯度下降法(SGD)的更新策略如下:

weight = weight - learning_rate * gradient

手动实现如下:

learning_rate = 0.01
for f in net.parameters():
    f.data.sub_(f.grad.data * learning_rate)    # inplcae 减法

torch.optim中实现了深度学习中绝大多数的优化方法,例如RMSProp、Adam、SGD等,更便于使用,因此通常并不需要手动写上述代码。

import torch.optim as optim
# 新建一个优化器,指定要调整的参数和学习率
optimizer = optim.SGD(net.parameters(), lr = 0.01)
# 在训练过程中, 先梯度清零(与net.zero_grad()效果一样)
optimizer.zero_grad()
# 计算损失
output = net(input)
loss = criterion(output, target)
# 反向传播
loss.backward()
# 更新参数
optimizer.step()

(4)数据加载与预处理

在深度学习中数据加载与预处理是非常复杂繁琐的,但PyTorch提供了一些可简答简化和加快数据处理流程的工具。同时,对于常用的数据集,PyTorch也提供了封装好的接口供用户快速调用,这些数据集主要保存在torchvision中。

torchvision实现了常用的图像数据加载功能,例如ImageNet、CIFAR10、MNIST等,以及常用的数据转换操作,这极大地方便了数据加载。

2.2.4 小试牛刀:CIFAR-10分类

下面我们来尝试实现对CIFAR10数据集的分类,步骤如下:
(1)使用torchvision加载并预处理CIFAR10数据集。
(2)定义网络。
(3)定义损失函数和优化器。
(4)训练网络并更新网络参数。
(5)测试网络。

(1)CIFAR-10数据集加载及预处理

CIFAR-10是一个常用的彩色图片数据集,它有10个类别:airplane、automobile、bird、cat、deer、dog、frog、horse、ship和truck。每张图片都是3 * 32 * 32,也即3通道彩色图片,分辨率为32 * 32。因为使用程序数据加载器下载该数据集太慢,这里给出一个百度云盘下载,提前下载数据集放到指定目录下,如E:/data/,在加载器中root参数指向该目录,程序检测到该文件已存在就直接解压,不再去外网下载了,节省时间。

import torch as t
import torch.nn as nn
import torch.nn.functional as F
from torch import optim
import torchvision as tv
from torch.autograd import Variable
import torchvision.transforms as transforms
from torchvision.transforms import ToPILImage
show = ToPILImage()  # 可以把Tensor转成Image,方便可视化

# 第一次运行程序torchvision会自动下载CIFAR-10数据集,大约100MB,需花费一定的时间
# 如果已经下载有CIFAR-10,可通过root参数指定

# 定义对数据的预处理
transform = transforms.Compose([
    transforms.ToTensor(),   # 转为Tensor
    transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))   # 归一化
    ])

# 训练集
trainset = tv.datasets.CIFAR10(
            root='E:/data/',
            train=True,
            download=True,
            transform=transform)

trainloader = t.utils.data.DataLoader(
            trainset,
            batch_size=4,
            shuffle=True,
            num_workers=2)

# 测试集
testset = tv.datasets.CIFAR10(
            root='E:/data/',
            train=False,
            download=True,
            transform=transform)

testloader = t.utils.data.DataLoader(
            testset,
            batch_size=4,
            shuffle=False,
            num_workers=2)

classes = ('plane','car','bird','cat','deer','dog','frog','horse','ship','truck')

输出如下:

Files already downloaded and verified
Files already downloaded and verified

Dataset对象是一个数据集,可以按下表访问,返回形如(data,label)的数据。

(data,label) = trainset[100]
print(classes[label])

# (data+1)/2 是为了还原被归一化的数据,程序输出的图片如下。
show((data+1)/2).resize((100,100))

输出如下:

ship

image.png

DataLoader是一个可迭代的对象,它将dataset返回的每一条数据样本拼接成一个batch,并提供多线程加速优化和数据打乱等操作。当程序对dataset的所有数据遍历完一遍之后,对DataLoader也完成了一次迭代。

dataiter = iter(trainloader)
images,labels = dataiter.next()    # 返回4张图片及标签
print(' '.join('%11s' % classes[labels[j]] for j in range(4)))
show(tv.utils.make_grid((images+1)/2)).resize((400,100))

输出如下:

        dog        frog       truck         dog

image.png

(2)定义网络

复制上面的LeNet网络,修改self.conv1中第一个参数为3通道,因为CIFAR-10是3通道彩色图。

class Net(nn.Module):
    def __init__(self):
        super(Net,self).__init__()
        self.conv1 = nn.Conv2d(3,6,5)
        self.conv2 = nn.Conv2d(6,16,5)
        self.fc1 = nn.Linear(16*5*5,120)
        self.fc2 = nn.Linear(120,84)
        self.fc3 = nn.Linear(84,10)
        
    def forward(self,x):
        x = F.max_pool2d(F.relu(self.conv1(x)),(2,2))
        x = F.max_pool2d(F.relu(self.conv2(x)),2)
        x = x.view(x.size()[0],-1)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x
    

net = Net()
print(net)

输出如下:

Net(
  (conv1): Conv2d(3, 6, kernel_size=(5, 5), stride=(1, 1))
  (conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
  (fc1): Linear(in_features=400, out_features=120, bias=True)
  (fc2): Linear(in_features=120, out_features=84, bias=True)
  (fc3): Linear(in_features=84, out_features=10, bias=True)
)

(3)定义损失函数和优化器(loss和optimizer)

criterion = nn.CrossEntropyLoss()    # 交叉熵损失函数
optimizer = optim.SGD(net.parameters(),lr=0.001,momentum=0.9)

(4)训练网络

所有网络的训练流程都是类似的,不断地执行如下流程。

  • 输入数据。
  • 前向传播 + 反向传播。
  • 更新参数。
for epoch in range(2):
    running_loss = 0.0
    for i,data in enumerate(trainloader,0):
        # 输入数据
        inputs,labels = data
        inputs,labels = Variable(inputs),Variable(labels)
        # 梯度清零
        optimizer.zero_grad()
        # forward + backward
        outputs = net(inputs)
        loss = criterion(outputs,labels)
        loss.backward()
        # 更新参数
        optimizer.step()
        # 打印log信息
        running_loss += loss
        if i % 2000 == 1999:     # 每2000个batch打印一次训练状态
            print('[%d,%5d] loss: %.3f' % (epoch+1, i+1, running_loss / 2000))
            running_loss = 0.0
print('Finished Trainning')

输出如下:

[1, 2000] loss: 2.244
[1, 4000] loss: 1.956
[1, 6000] loss: 1.717
[1, 8000] loss: 1.600
[1,10000] loss: 1.535
[1,12000] loss: 1.485
[2, 2000] loss: 1.408
[2, 4000] loss: 1.386
[2, 6000] loss: 1.331
[2, 8000] loss: 1.340
[2,10000] loss: 1.283
[2,12000] loss: 1.272
Finished Trainning

此处仅训练了2个epoch(遍历完一遍数据集称为一个epoch),我们来看看网络有没有效果。将测试图片输入网络,计算它的label,然后与实际的label进行比较。

dataiter = iter(testloader)
images,labels = dataiter.next()    # 一个batch返回4张图片
print('实际的label:',' '.join('%08s' % classes[labels[j]] for j in range(4)))
show(tv.utils.make_grid(images / 2.0 - 0.5)).resize((400,100))

输出如下:

实际的label:      cat     ship     ship    plane

image.png

接着计算网络预测的label:

# 计算图片在每个类别上的分数
outputs = net(Variable(images))
# 得分最高的那个类
_,predicted = t.max(outputs.data,1)
print('预测结果:',' '.join('%5s' % classes[predicted[j]] for j in range(4)))

输出结果如下:

预测结果:   cat   ship  ship  ship

我们已经可以看出效果,准确率为75%,但这这是一部分图片,我们再来看看在整个测试集上的效果。

correct = 0    # 预测正确的图片数
total = 0      # 总共的图片数
for data in testloader:
    images,labels = data
    outputs = net(Variable(images))
    _,predicted = t.max(outputs.data,1)
    total += labels.size(0)
    correct += (predicted == labels).sum()
print('10000张测试集中的准确率为:%d %%' % (100 * correct / total))

输出如下:

10000张测试集中的准确率为:56 %

训练的准确率远比随机猜测(准确率为10%)好,证明网络确实学到了东西。

(5)在GPU上训练

就像之前把Tensor从CPU转到GPU一样,模型也可以类似地从CPU转到GPU。

if t.cuda.is_available():
    net.cuda()
    images = images.cuda()
    labels = labels.cuda()
    outputs = net(Variable(images))
    loss = criterion(outputs, Variable(labels))

如果发现在GPU上训练的速度并没并在CPU上提速很多,实际是因为网络比较小,CPU没有完全发挥自己的真正实力。

对于PyTorch的基础介绍至此结束。总结一下,本节主要介绍以下内容。
(1)Tensor:类似numpy数组的数据结构,与numpy接口类似,可方便地相互转换。
(2)autograd/Variable:Variable分装了Tensor,并提供了自动求导功能。
(3)nn:专门为神经网络设计的接口,提供了很多有用的功能(神经网络层、损失函数、优化器等)。
(4)神经网络训练:以CIFAR-10分类为例演示了神经网络的训练流程,包括数据加载、网络搭建、训练及测试。

通过本章的学习,读者能够配置PyTorch+Jupyter+IPython的学习环境。另外,通过2.2节关于PyTorch的概要介绍,相信读者可以体会出PyTorch接口简单、使用灵活等特点。如果有哪些内容读者没有理解,不用着急,这些内容会在后续章节深入和详细地讲解。

你可能感兴趣的:(深度学习框架PyTorch入门与实践:第二章 快速入门)