作者归档:feiguyun

1. Pandas基础篇

Python有了NumPy的Pandas,用Python处理数据就像使用Exel或SQL一样简单方便。
Pandas是基于NumPy的Python 库,它被广泛用于快速分析数据,以及数据清洗和准备等工作。可以把 Pandas 看作是 Python版的Excel或Table。Pandas 有两种数据结构:
Series和DataFrame,Pandas经过几个版本的更新,目前已经成为数据清洗、处理和分析的不二选择。

1.1 问题:Pandas有哪些优势?

科学计算方面NumPy是优势,但NumPy中没有标签,数据清理、数据处理就不是其强项了。而DataFrame有标签,就像SQL中的表一样,所以在数据处理方面DataFrame就更胜一筹了,具体包含以下几方面:
(1)读取数据方面
Pandas提供强大的IO读取工具,csv格式、Excel文件、数据库等都可以非常简便地读取,对于大数据,pandas也支持大文件的分块读取。
(2)在数据清洗方面
面对数据集,我们遇到最多的情况就是存在缺失值,Pandas把各种类型数据类型的缺失值统一称为NaN,Pandas提供许多方便快捷的方法来处理这些缺失值NaN。
(3)分析建模阶段
在分析建模阶段,Pandas自动且明确的数据对齐特性,非常方便地使新的对象可以正确地与一组标签对齐,由此,Pandas就可以非常方便地将数据集进行拆分-重组操作。
(4)结果可视化方面
结果展示方面,我们都知道Matplotlib是个数据视图化的好工具,Pandas与Matplotlib搭配,不用复杂的代码,就可以生成多种多样的数据视图。

1.2 Pandas数据结构

Pandas中两个最常用的对象是Series和DataFrame。使用pandas前,需导入以下内容:

Pandas主要采用Series和DataFrame两种数据结构。Series是一种类似一维数据的数据结构,由数据(values)及索引(indexs)组成,而DataFrame是一个表格型的数据结构,它有一组序列,每列的数据可以为不同类型(NumPy数据组中数据要求为相同类型),它既有行索引,也有列索引。

图1-1 DataFrame结构

1.3 Series

上章节我们介绍了多维数组(ndarray),当然,它也包括一维数组,Series类似一维数组,为啥还要介绍Series呢?或Series有哪些特点?
Series一个最大特点就是可以使用标签索引,序列及ndarray也有索引,但都是位置索引或整数索引,这种索引有很多局限性,如根据某个有意义标签找对应值,切片时采用类似[2:3]的方法,只能取索引为2这个元素等等,无法精确定位。
Series的标签索引(它位置索引自然保留)使用起来就方便多了,且定位也更精确,不会产生歧义。以下通过实例来说明。
(1)使用Series

0 1
1 3
2 6
3 -1
4 2
5 8
dtype: int64
(2)使用Series的索引

a 1
c 3
d 6
e -1
b 2
g 8
dtype: int64
(3)根据索引找对应值

1.4 DataFrame

DataFrame除了索引有位置索引也有标签索引,而且其数据组织方式与MySQL的表极为相似,除了形式相似,很多操作也类似,这就给操作DataFrame带来极大方便。这些是DataFrame特色的一小部分,它还有比数据库表更强大的功能,如强大统计、可视化等等。
DataFrame有几个要素:index、columns、values等,columns就像数据库表的列表,index是索引,values就是值。

图1-2 DataFrame结果

1.4.1 生成DataFrame

生成DataFrame有很多,比较常用的有导入等长列表、字典、numpy数组、数据文件等。

1.4.2 获取数据

获取DataFrame结构中数据可以采用obj[]操作、obj.iloc[]、obj.loc[]等命令。
(1)使用obj[]来获取列或行

(2)使用obj.loc[] 或obj.iloc[]获取行或列数据。
loc通过行标签获取行数据,iloc通过行号获取行数据。
loc 在index的标签上进行索引,范围包括start和end.
iloc 在index的位置上进行索引,不包括end.
这两者的主要区别可参考如下示例:

【说明】
除使用iloc及loc外,早期版本还有ix格式。pandas0.20.0及以上版本,ix已经丢弃,请尽量使用loc和iloc;

1.4.3 修改数据

我们可以像操作数据库表一样操作DataFrame,删除数据、插入数据、修改字段名、索引名、修改数据等,以下通过一些实例来说明。

图1-3 数据结构

1.4.4 汇总统计

Pandas有一组常用的统计方法,可以根据不同轴方向进行统计,当然也可按不同的列或行进行统计,非常方便。
常用的统计方法有:
表1-1 Pandas统计方法

以下通过实例来说明这些方法的使用
(1)把csv数据导入pandas

(2)查看df的统计信息

【说明】

即各项-均值的平方求和后再除以N 。
std:表示标准差,是var的平方根。

1.4.5选择部分列

这里选择学生代码、课程代码、课程名称、程程成绩,注册日期等字段

1.4.6删除重复数据

如果有重复数据(对df1的所有列),则删除最后一条记录。

1.4.7补充缺省值

(1)用指定值补充NaN值
这里要求把stat_date的缺省值(NaN)改为'2018-09-01'

(2)可视化,并在图形上标准数据

结果为:

(3)导入一些库及支持中文的库

(4)画图

运行结果


图1-4 可视化结果

1.4.8 Pandas操作MySQL数据库

(1)从MySQL数据库中获取学生基本信息表

(2)查看df_info前3行数据

(3)选择前两个字段

(4)df2 与df_info1 根据字段stud_code 进行内关联

(5)对df3 根据字段stud_code,sub_code进行分组,并求平均每个同学各科的平均成绩。

【备注】
如果需要合计各同学的成绩,可用如下语句。

(6)选择数学分析课程,并根据成绩进行降序。

(7)取前5名

注:DataFrame数据结构的函数或方法有很多,大家可以通过df.[Tab键]方式查看,具体命令的使用方法,如df.count(),可以在Ipython命令行下输入:?df.count() 查看具体使用,退出帮助界面,按q即可。

1.4.9 Pandas操作excel

把DataFrame数据写入excel中的多个sheet中

第4章 PyTorch数据处理工具箱

在3.5节我们利用PyTorch的torchvision、data等包,下载及预处理MNIST数据集。数据下载和预处理是机器学习、深度学习实际项目中耗时又重要的任务,尤其是数据预处理,关系到数据质量和模型性能,往往要占据项目的大部分时间。好在PyTorch为此提供了专门的数据下载、数据处理包,使用这些包,可极大提高我们的开发效率及数据质量。
本章将介绍以下内容:
 简单介绍PyTorch相关的数据处理工具箱
 utils.data简介
 torchvision简介
 TensorBoard简介及实例

4.1 数据处理工具箱概述

通过第3章,读者应该对torchvision、data等数据处理包有了初步的认识,但可能理解还不够深入,接下来我们将详细介绍。PyTorch涉及数据处理(数据装载、数据预处理、数据增强等)主要工具包及相互关系如图4-1所示。

图4-1 PyTorch主要数据处理工具
图4-1 的左边是torch.utils.data工具包,它包括以下4个类:
1) Dataset:是一个抽象类,其他数据集需要继承这个类,并且覆写其中的两个方法(__getitem__、__len__)。
2) DataLoader:定义一个新的迭代器,实现批量(batch)读取,打乱数据(shuffle)并提供并行加速等功能。
3) random_split:把数据集随机拆分为给定长度的非重叠新数据集。
4) *sampler:多种采样函数。
图4-1中间是PyTorch可视化处理工具(torchvision),它是PyTorch的一个视觉处理工具包,独立于PyTorch,需要另外安装,使用pip或conda安装即可:

4.2 utils.data简介

utils.data包括Dataset和DataLoader。torch.utils.data.Dataset为抽象类。自定义数据集需要继承这个类,并实现两个函数,即__len__和__getitem__。前者提供数据的大小(size),后者通过给定索引获取数据和标签或一个样本。 __getitem__一次只能获取一个样本,所以通过torch.utils.data.DataLoader来定义一个新的迭代器,实现batch读取。首先我们来定义一个简单的数据集,然后具体使用Dataset及DataLoader,给读者一个直观的认识。 1)导入需要的模块。

2)定义获取数据集的类。
该类继承基类Dataset,自定义一个数据集及对应标签。

3)获取数据集中数据。

以上数据以元组格式返回,每次只返回一个样本。实际上,Dateset只负责数据的抽取,一次调用__getitem__只返回一个样本。如果希望批量处理(batch),同时还要进行shuffle和并行加速等操作,可选择DataLoader。DataLoader的格式为:

主要参数说明如下。
 dataset: 加载的数据集。
 batch_size: 批大小。
 shuffle:是否将数据打乱。
 sampler:样本抽样。
 num_workers:使用多进程加载的进程数,0代表不使用多进程。
 collate_fn:如何将多个样本数据拼接成一个batch,一般使用默认的拼接方式即可。
 pin_memory:是否将数据保存在锁页内存(pin memory区,pin memory中的数据转到GPU会快一些。
 drop_last:dataset 中的数据个数可能不是 batch_size的整数倍,drop_last为True会将多出来不足一个batch的数据丢弃。
使用函数DataLoader加载数据。

运行结果如下:
i: 0
data: tensor([[1, 2],
[3, 4]])
Label: tensor([0, 1])
i: 1
data: tensor([[2, 1],
[3, 4]])
Label: tensor([0, 1])
i: 2
data: tensor([[4, 5]])
Label: tensor([2])
从这个结果可以看出,这是批量读取。我们可以像使用迭代器一样使用它,如对它进行循环操作。不过它不是迭代器,我们可以通过iter命令转换为迭代器。

一般用data.Dataset处理同一个目录下的数据。如果数据在不同目录下,不同目录代表不同类别(这种情况比较普遍),使用data.Dataset来处理就不很方便。不过,可以使用PyTorch提供的另一种可视化数据处理工具(即torchvision)就非常方便,不但可以自动获取标签,还提供很多数据预处理、数据增强等转换函数。

4.3 torchvision简介

torchvision有4个功能模块,model、datasets、transforms和utils。其中model后续章节将介绍,利用datasets下载一些经典数据集,3.5小节有实例,读者可以参考一下。本节主要介绍如何使用datasets的ImageFolder处理自定义数据集,如何使用transforms对源数据进行预处理、增强等。下面重点介绍transforms及ImageFolder。

4.3.1 transforms

transforms提供了对PIL Image对象和Tensor对象的常用操作。
1)对PIL Image的常见操作如下。
 Scale/Resize: 调整尺寸,长宽比保持不变。
 CenterCrop、RandomCrop、RandomSizedCrop:裁剪图像,CenterCrop和RandomCrop在crop时是固定size,RandomResizedCrop则是random size的crop。
 Pad: 填充。
 ToTensor: 把一个取值范围是[0,255]的PIL.Image 转换成 Tensor。形状为(H,W,C)的numpy.ndarray,转换成形状为[C,H,W],取值范围是[0,1.0]的torch.FloatTensor。
 RandomHorizontalFlip:图像随机水平翻转,翻转概率为0.5。
 RandomVerticalFlip: 图像随机垂直翻转。
 ColorJitter: 修改亮度、对比度和饱和度。
2)对Tensor的常见操作如下。
 Normalize: 标准化,即减均值,除以标准差。
 ToPILImage:将Tensor转为PIL Image。
如果要对数据集进行多个操作,可通过Compose将这些操作像管道一样拼接起来,类似于nn.Sequential。以下为示例代码。

还可以自己定义一个python lambda表达式,如将每个像素值加10,可表示为:transforms.Lambda(lambda x: x.add(10))。
更多内容可参考官网,地址为https://pytorch.org/docs/stable/torchvision/transforms.html。

4.3.2 ImageFolder

当文件依据标签处于不同文件下时,如:
─── data
├── zhangliu
│ ├── 001.jpg
│ └── 002.jpg
├── wuhua
│ ├── 001.jpg
│ └── 002.jpg
.................
我们可以利用 torchvision.datasets.ImageFolder 来直接构造出 dataset,代码如下:

ImageFolder 会将目录中的文件夹名自动转化成序列,那么DataLoader载入时,标签自动就是整数序列了。
下面我们利用ImageFolder读取不同目录下图像数据,然后使用transorms进行图像预处理,预处理有多个,我们用compose把这些操作拼接在一起。然后使用DataLoader加载。
将处理后的数据用torchvision.utils中的save_image保存为一个png格式文件,然后用Image.open打开该png文件,详细代码如下:

运行结果如下,结果如图4-2所示。
tensor([2, 2, 0, 0, 0, 1, 2, 2])

图4-2 make_grid拼接在一起的图形
打开test01.png文件:

运行结果如图4-3所示。

图4-3 用Image查看png文件

4.4 可视化工具

TensorBoard是Google TensorFlow 的可视化工具,可以记录训练数据、评估数据、网络结构、图像等,并且可以在Web上展示,对于观察神经网路训练的过程非常有帮助。PyTorch支持tensorboard_logger、 visdom等可视化工具。

4.4.1 TensorBoard简介

TensorBoard功能很强大,支持scalar、image、figure、histogram、audio、text、graph、onnx_graph、embedding、pr_curve、videosummaries等可视化方式。
使用TensorBoard的一般步骤如下。
1)导入tensorboard,实例化SummaryWriter类,指明记录日志路径等信息。

【说明】
(1)其中logs指生成日志文件路径,如果是在Windows环境下,需要注意其logs路径格式与Linux环境不同,需要使用转义字符或在字符串前加r,如
writer = SummaryWriter(log_dir=r'D:\myboard\test\logs')
(2)SummaryWriter的格式为:

(3)如果不写log_dir,系统将在当前目录创建一个runs的目录。

2)调用相应的API接口,接口一般格式为:

3)启动tensorboard服务。cd到logs目录所在的同级目录,在命令行输入如下命令,logdir等式右边可以是相对路径或绝对路径。

4)Web展示。在浏览器输入:

便可看到logs目录保存的各种图形,如图4-4所示。

图4-4 TensorBoard示例图形
鼠标在图形上移动,还可以看到对应位置的具体数据。

4.4.2用TensorBoard可视化神经网络

4.4.1节介绍了TensorBoard的主要内容,为帮助大家更好地理解,下面我们将介绍几个实例。实例内容涉及如何使用TensorBoard可视化神经网络模型、可视化损失值、图像等。
1)导入需要的模块。

2)构建神经网络。

3)把模型保存为graph。

打开浏览器,便可看到图4-5所示的可视化计算图。
图4-5 TensorBoard可视化计算图

4.4.3用TensorBoard可视化损失值

可视化损失值,使用add_scalar函数,这里利用一层全连接神经网络,训练一元二次函数的参数。

运行结果如图4-6所示。

图4-6 可视化损失值与迭代步的关系

4.4.4用TensorBoard可视化特征图

利用TensorBoard对特征图进行可视化,不同卷积层的特征图的抽取程度是不一样的。
x从cifair10数据集获取。注意:因PyTorch1.7 utils有一个bug,这里使用了PyTorch1.10版的utils。

运行结果如图4-7、图4-8所示。

图4-7 conv1的特征图

图4-8 conv2的特征图

4.5 小结

本章详细介绍了PyTorch有关数据下载、预处理方面的一些常用包,以及可视化计算结果的TensorBoard工具,并通过一些实例详细说明如何使用这些包或工具。第1-4章介绍了有关NumPy及PyTorch的基础知识,这有助于读者更好理解和使用接下来的深度学习方面的基本概念、原理和算法等内容。

第3章 PyTorch神经网络工具箱

前面已经介绍了PyTorch的数据结构及自动求导机制,充分运行这些技术可以大大提高我们的开发效率。这章将介绍PyTorch的另一利器:神经网络工具箱。利用这个工具箱,设计一个神经网络就像搭积木一样,可以极大简化我们构建模型的任务。
本章主要讨论如何使用PyTorch神经网络工具箱来构建网络,主要内容如下:
 介绍神经网络核心组件
 如何构建一个神经网络
 构建神经网络的主要工具
 如何训练模型
 实现神经网络实例

3.1 神经网络核心组件

神经网络看起来很复杂,节点很多,层数多,参数更多。但核心部分或组件不多,把这些组件确定后,这个神经网络基本就确定了。这些核心组件包括:
 层:神经网络的基本结构,将输入张量转换为输出张量。
 模型:层构成的网络。
 损失函数:参数学习的目标函数,通过最小化损失函数来学习各种参数。
 优化器:如何是损失函数最小,这就涉及到优化器。
当然这些核心组件不是独立的,它们之间、它们与神经网络其他组件之间有密切关系。为便于大家理解,我们把这些关键组件及相互关系用图3-1表示。

图3-1 神经网络关键组件及相互关系示意图
多个层链接在一起构成一个模型或网络,输入数据通过这个模型转换为预测值。预测值与真实值共同构成损失函数的输入,损失函数输出损失值(损失值可以是距离、概率值等),该损失值用于衡量预测值与目标结果的匹配或相似程度。优化器利用损失值更新权重参数,目标是使损失值越来越小。这是一个循环过程,当损失值达到一个阀值或循环次数到达指定次数时,循环结束。
接下来利用PyTorch的nn工具箱,构建一个神经网络实例。nn中对这些组件都有现成包或类,可以直接使用,非常方便。

3.2 PyTorch构建神经网络的主要工具

使用PyTorch构建神经网络使用的主要工具(或类)及相互关系,如图3-2所示。

图3-2 PyTorch实现神经网络主要工具及相互关系
从图3-2可知,可以基于Module类或函数(nn.functional)构建网络层。nn中的大多数层(layer)在functional中都有与之对应的函数。nn.functional中的函数与nn.Module中的layer的主要区别是后者继承自Module类,可自动提取可学习的参数,而nn.functional更像是纯函数。两者功能相同,性能也没有很大区别,那么如何选择呢?卷积层、全连接层、dropout层等含有可学习参数,一般使用nn.Module,而激活函数、池化层不含可学习参数,可以使用nn.functional中对应的函数。

3.2.1 nn.Module

前面我们使用autograd及Tensor实现机器学习实例时,需要做不少设置,如对叶子节点的参数requires_grad设置为True,然后调用backward,再从grad属性中提取梯度。对于大规模的网络,autograd太过于底层和烦琐。为了简单、有效解决这个问题,nn是一个有效工具。它是专门为深度学习设计的一个模块,而nn.Module是nn的一个核心数据结构。nn.Module可以是神经网络的某个层,也可以是包含多层的神经网络。在实际使用中,最常见的做法是继承nn.Module,生成自己的网络/层,如3.4节实例中,我们定义的Net类就采用这种方法(class Net(torch.nn.Module))。nn中已实现了绝大多数层,包括全连接层、损失层、激活层、卷积层、循环层等。这些层都是nn.Module的子类,能够自动检测到自己的参数,并将其作为学习参数,且针对GPU运行进行了CuDNN优化。

3.2.2 nn.functional

nn中的层,一类是继承了nn.Module,其命名一般为nn.Xxx(第一个是大写),如nn.Linear、nn.Conv2d、nn.CrossEntropyLoss等。另一类是nn.functional中的函数,其名称一般为nn.funtional.xxx,如nn.funtional.linear、nn.funtional.conv2d、nn.funtional.cross_entropy等。从功能来说两者相当,基于nn.Mudle能实现的层,也可以基于nn.funtional实现,反之亦然,而且性能方面两者也没有太大差异。不过在具体使用时,两者还是有区别的,主要区别如下。
1) nn.Xxx继承于nn.Module,nn.Xxx 需要先实例化并传入参数,然后以函数调用的方式调用实例化的对象并传入输入数据。它能够很好的与nn.Sequential结合使用,而nn.functional.xxx无法与nn.Sequential结合使用。
2) nn.Xxx不需要自己定义和管理weight、bias参数;而nn.functional.xxx需要你自己定义weight、bias,每次调用的时候都需要手动传入weight、bias等参数, 不利于代码复用。
3) dropout操作在训练和测试阶段是有区别的,使用nn.Xxx方式定义dropout,在调用model.eval()之后,自动实现状态的转换,而使用nn.functional.xxx却无此功能。
总的来说,两种功能都是相同的,但PyTorch官方推荐:具有学习参数的(例如,conv2d、 linear、batch_norm、dropout等)情况采用nn.Xxx方式,没有学习参数的(例如,maxpool, loss func, activation func)等情况选择使用nn.functional.xxx或者nn.Xxx方式。3.5节中使用激活层,我们采用无学习参数的F.relu方式来实现,即nn.functional.xxx方式。

3.3 构建模型

第2章介绍使用PyTorch实现机器学习任务的几个实例,具体步骤好像不少,但关键就是选择网络层,构建网络,然后选择损失和优化器。在nn工具箱中,可以直接引用的网络很多,有全连接层、卷积层、循环层、正则化层、激活层等。接下来将介绍PyTorch的主要工具或模块,采用不同方法构建如图3-3所示的神经网络。

图3-3 神经网络结构
如图3-3所示,先把28x28的图像展平为784向量,layer1和layer2分别包括一个全连接层、一个批量归一化层,激活函数都是ReLU,输出层的激活函数为softmax。
PyTorch构建模型大致有以下3种方式。
1) 继承nn.Module基类构建模型。
2) 使用nn.Sequential按层顺序构建模型。
3) 继承nn.Module基类构建模型,又使用相关模型容器(如nn.Sequential,nn.ModuleList,nn.ModuleDict等)进行封装。
在这3种方法中,第1种方式最为常见;第2种方式比较简单,非常适合与初学者;第3种方式较灵活但复杂一些。

3.3.1 继承nn.Module基类构建模型

利用这种方法构建模型,先定义一个类,使之继承nn.Module基类。把模型中需要用到的层放在构造函数__init__()中,在forward方法中实现模型的正向传播。具体代码如下。
1)导入模块。

2) 构建模型。

3)查看模型。

运行结果如下:

3.3.2 使用nn.Sequential按层顺序构建模型

使用nn.Sequential构建模型,因其内部实现了forward函数,因此可以不用写forward函数。nn.Sequential里面的模块按照先后顺序进行排列的,所以必须确保前一个模块的输出大小和下一个模块的输入大小是一致的。使用这种方法一般构建较简单的模型。 以下是使用nn.Sequential搭建模型的几种等价方法。
1.利用可变参数
Python中的函数参数个数是可变(或称为不定长参数),PyTorch中的有些函数也类似,如nn.Sequential(*args)就是一例。
1)导入模块。

2)构建模型。

3)查看模型。

运行结果如下:
Sequential(
(0): Flatten(start_dim=1, end_dim=-1)
(1): Linear(in_features=784, out_features=300, bias=True)
(2): BatchNorm1d(300, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(3): ReLU()
(4): Linear(in_features=300, out_features=100, bias=True)
(5): BatchNorm1d(100, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(6): ReLU()
(7): Linear(in_features=100, out_features=10, bias=True)
(8): Softmax(dim=1)
)
这种方式构建时不能给每个层指定名称,如果需要给每个层指定名称,可使用add_module方法或OrderedDict方法。
2.使用add_module方法
1)构建模型。

2)查看模型。

运行结果如下:
Sequential(
(flatten): Flatten(start_dim=1, end_dim=-1)
(linear1): Linear(in_features=784, out_features=300, bias=True)
(bn1): BatchNorm1d(300, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu1): ReLU()
(linear2): Linear(in_features=300, out_features=100, bias=True)
(bn2): BatchNorm1d(100, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu2): ReLU()
(out): Linear(in_features=100, out_features=10, bias=True)
(softmax): Softmax(dim=1)
)
3.使用OrderedDict
1)导入模块。

2)构建模型。

3)查看模型。

运行结果如下:
Sequential(
(flatten): Flatten(start_dim=1, end_dim=-1)
(linear1): Linear(in_features=784, out_features=300, bias=True)
(bn1): BatchNorm1d(300, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu1): ReLU()
(linear2): Linear(in_features=300, out_features=100, bias=True)
(bn2): BatchNorm1d(100, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu2): ReLU()
(out): Linear(in_features=100, out_features=10, bias=True)
(softmax): Softmax(dim=1)
)

3.3.3 继承nn.Module基类并应用模型容器来构建模型

当模型的结构比较复杂时,可以应用模型容器(如nn.Sequential,nn.ModuleList,
nn.ModuleDict)对模型的部分结构进行封装,以增强模型的可读性,或减少代码量。
1.使用nn.Sequential模型容器
1)导入模块。

2)构建模型。

3)查看模型。

运行结果如下:
Model_lay(
(flatten): Flatten(start_dim=1, end_dim=-1)
(layer1): Sequential(
(0): Linear(in_features=784, out_features=300, bias=True)
(1): BatchNorm1d(300, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(layer2): Sequential(
(0): Linear(in_features=300, out_features=100, bias=True)
(1): BatchNorm1d(100, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(out): Sequential(
(0): Linear(in_features=100, out_features=10, bias=True)
)
)
2.使用nn.ModuleList模型容器
1)导入模块。

2)构建模型。

3)查看模型。

运行结果如下:
Model_lst(
(layers): ModuleList(
(0): Flatten(start_dim=1, end_dim=-1)
(1): Linear(in_features=784, out_features=300, bias=True)
(2): BatchNorm1d(300, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(3): ReLU()
(4): Linear(in_features=300, out_features=100, bias=True)
(5): BatchNorm1d(100, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(6): ReLU()
(7): Linear(in_features=100, out_features=10, bias=True)
(8): Softmax(dim=1)
)
)
3.使用nn.ModuleDict模型容器
1)导入模块。

2)构建模型。

其中激活函数ReLU在模型中应该出现2次,但函数相同,故在定义字典时,只需定义一次,但在定义forward函数的列表中需要出现2次。
3)查看模型。

运行结果如下:
Model_dict(
(layers_dict): ModuleDict(
(flatten): Flatten(start_dim=1, end_dim=-1)
(linear1): Linear(in_features=784, out_features=300, bias=True)
(bn1): BatchNorm1d(300, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU()
(linear2): Linear(in_features=300, out_features=100, bias=True)
(bn2): BatchNorm1d(100, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(out): Linear(in_features=100, out_features=10, bias=True)
(softmax): Softmax(dim=1)
)
)

3.3.4 自定义网络模块

利用以上方法,自定义一些典型的网络模块,如残差网络(ResNet18)中的残差块,如图3-4所示。

图3-4 残差块网络结构
残差块有两种,一种是正常的模块方式,如图3-4左图,将输入与输出相加,然后应用激活函数ReLU。 另一种是为使输入与输出形状一致,需添加通过1×1卷积调整通道和分辨率,如图3-4中的右图所示。这些模块中用到卷积层、批量规范化层,具体将在第6章详细介绍,这里我们只需要了解这些是网络层即可。
1)定义图3-4左图的残差模块。

2)定义图3-4右图的残差模块。

3)组合这两个模块得到现代经典RetNet18网络结构。

3.4 训练模型

构建模型(假设为model)后,接下来就是训练模型。PyTorch训练模型主要包括加载数据集、损失计算、定义优化算法、反向传播、参数更新等主要步骤。
1.加载预处理数据集
加载和预处理数据集,可以使用PyTorch的数据处理工具,如torch.utils和torchvision等,这些工具将在第4章详细介绍。
2.定义损失函数
定义损失函数可以通过自定义方法或使用PyTorch内置的损失函数,如回归使用的losss_fun=nn.MSELoss(),分类使用的nn.BCELoss等损失函数,更多内容可参考本书5.2.4节。
3.定义优化方法
Pytoch常用的优化方法都封装在torch.optim里面,其设计很灵活,可以扩展为自定义的优化方法。所有的优化方法都是继承了基类optim.Optimizer,并实现了自己的优化步骤。
最常用的优化算法就是梯度下降法及其各种变种,具体将在5.4节详细介绍,这些优化算法大多使用梯度更新参数。
如使用SGD优化器时,可设置为optimizer = torch.optim.SGD(params,lr = 0.001)。
4.循环训练模型
1)设置为训练模式:
model.train()
调用model.train()会把所有的module设置为训练模式。
2)梯度清零:
optimizer. zero_grad()
在默认情况下梯度是累加的,需要手工把梯度初始化或清零,调用optimizer.zero_grad() 即可。
3)求损失值:
y_prev=model(x)
loss=loss_fun(y_prev,y_true)
4)自动求导,实现梯度的反向传播:
loss.backward()
5)更新参数:
optimizer.step()
5.循环测试或验证模型
1)设置为测试或验证模式:
model.eval()
调用model.eval()会把所有的training属性设置为False。
2)在不跟踪梯度模式下计算损失值、预测值等:
with.torch.no_grad():
6.可视化结果
下面我们通过实例来说明如何使用nn来构建网络模型、训练模型。
【说明】model.train()与model.eval()的使用
如果模型中有BN (Batch Normalization)层和Dropout,需要在训练时添加model.train(),
在测试时添加model.eval()。其中model.train()是保证BN层用每一批数据的均值和方差,而model.eval()是保证BN用全部训练数据的均值和方差;而对于Dropout,model.train()是随机取一部分网络连接来训练更新参数,而model.eval()是利用到了所有网络连接。

3.5实现神经网络实例

前面我们介绍了使用PyTorch构建神经网络的一些组件、常用方法和主要步骤等,本节通过一个构建神经网络的实例把这些内容有机结合起来。

3.5.1背景说明

本节将利用神经网络完成对手写数字进行识别的实例,来说明如何借助nn工具箱来实现一个神经网络,并对神经网络有个直观了解。在这个基础上,后续我们将对nn的各模块进行详细介绍。实例环境使用PyTorch1.5+,GPU或CPU,源数据集为MNIST。
主要步骤如下。
 利用PyTorch内置函数mnist下载数据。
 利用torchvision对数据进行预处理,调用torch.utils建立一个数据迭代器。
 可视化源数据。
 利用nn工具箱构建神经网络模型。
 实例化模型,并定义损失函数及优化器。
 训练模型。
 可视化结果。
神经网络的结构如图3-5所示。

图3-5 神经网络结构图
使用两个隐含层,每层使用ReLU激活函数,输出层使用softmax激活函数,最后使用torch.max(out,1)找出张量out最大值对应索引作为预测值。

3.5.2准备数据

1)导人必要的模块。

2)定义一些超参数。

3)下载数据并对数据进行预处理。

【说明】
1) transforms.Compose可以把一些转换函数组合在一起。
2) Normalize([0.5], [0.5])对张量进行归一化,这里两个0.5分别表示对张量进行归一化的全局平均值和方差。因图像是灰色的只有一个通道,如果有多个通道,需要有多个数字,如三个通道,应该是Normalize([m1,m2,m3], [n1,n2,n3])。
3) download参数控制是否需要下载,如果./data目录下已有MNIST,可选择False。
4) 用DataLoader得到生成器,这可节省内存。
5) torchvision及data的使用第4章将详细介绍。

3.5.3可视化源数据

对数据集中部分数据进行可视化。

运行结果如图3-6所示。

图3-6 MNIST源数据示例

3.5.4 构建模型

数据预处理之后,我们开始构建网络,创建模型。
1)构建网络。

2)实例化网络。

3.5.5 训练模型

训练模型,这里使用for循环进行迭代。其中包括对训练数据的训练模型,然后用测试数据验证模型。
1)训练模型。

最后5次迭代的结果如下:
学习率:0.006561
epoch: 15, Train Loss: 1.4681, Train Acc: 0.9950, Test Loss: 1.4801, Test Acc: 0.9830
epoch: 16, Train Loss: 1.4681, Train Acc: 0.9950, Test Loss: 1.4801, Test Acc: 0.9833
epoch: 17, Train Loss: 1.4673, Train Acc: 0.9956, Test Loss: 1.4804, Test Acc: 0.9826
epoch: 18, Train Loss: 1.4668, Train Acc: 0.9960, Test Loss: 1.4798, Test Acc: 0.9835
epoch: 19, Train Loss: 1.4666, Train Acc: 0.9962, Test Loss: 1.4795, Test Acc: 0.9835
这个神经网络的结构比较简单,只用了两层,也没有使用dropout层,迭代20次,测试准确率达到98%左右,效果还可以。不过,还是有提升空间,如果采用cnn,dropout等层,应该还可以提升模型性能。
2)可视化训练及测试损失值。

运行结果如图3-7所示。

图3-7 MNIST数据集训练的损失值

3.6 小结

本章我们首先介绍了神经网络的核心组件,即层、模型、损失函数及优化器。然后,从一个完整实例开始,看PyTorch是如何使用其包、模块等来搭建、训练、评估、优化神经网络。最后详细剖析了PyTorch的工具箱nn以及基于nn的一些常用类或模块等,并用相关实例演示这些模块的功能。这章介绍了神经网络工具箱,下一章将介绍PyTorch的另一个强大工具箱,即数据处理工具箱。

第2 章 向量基本运算

2.1转置运算

向量的转置(Transpose)将列向量变成行向量,或将行向量变成列向量。
向量X的转置记为X^T
例1:X=\left[\begin{matrix} 2\cr 0 \cr 3\end{matrix}\right]X^T=[2,0,3]
用Python表示:

2.2 两个向量的点积

两个向量(如X、Y,它们的维数相同)的点积(或称为内积)定义为它们对应元素乘积之和,记为:X^T Y
例2:X=\left[\begin{matrix} 2\cr 0 \cr 3\end{matrix}\right]Y=\left[\begin{matrix} 1\cr 5 \cr 9\end{matrix}\right]
X^T Y=[2,0,3]\cdot \left[\begin{matrix} 1\cr 5 \cr 9\end{matrix}\right]=2\times 1+0\times 5+3\times 9=29
向量与自身的点积为所有元素的平方和:
X^T X=\sum_{i=1}^n x_i^2
如果两个向量的点积为0,则称它们正交。
点积运算满足如下规律:
X^TY=Y^TX
(kX^T)Y=kX^TY
(X+Y)^T)Z=X^TZ+Y^TZ
Z^T(X+Y)=Z^TX+Z^TY
利用点积可以简化线性函数的表述,这种方法在机器学习中经常可以看到。
如表示权重(\omega_i)与输入(x_i)、偏置项(b)的线性模型预测函数:
 \omega_1 x_1+\omega_2 x_2+\dots+\omega_n x_n+b \tag{2.5}
设权重向量w=[\omega_1, \omega_2,\dots, \omega_n] ,输入向量X=[x_1, x_2,\dots, x_n] ,式(2.5)可写成:
WX^T+b
点积运算用于在机器学习的正向传播过程。

2.3 两个向量的阿达马 (Hadamard) 积

两个向量的阿达马积或称为遂元乘积、对应元素的乘积,是它们对应元素相乘。
记为:X*Y X\bigodot Y
例3:X=[1 2 3] Y=[2 2 1]
X*Y=[1×2 2×2 3×1]=[2 4 3]
当向量X、Y的元素个数相同时,还可进行对应元素的加、减、乘、除等算术运算。
X+Y=[3 4 4 ]
X/Y=[0.5 1 3]
用Python代码实现

经过阿达马运算的向量或矩阵维度不变,如:

运行结果:
A向量运行后形状:(3,),B矩阵运行后形状:(2, 3)
阿达马积在机器学习中的正向传播和反向传播、梯度下降法中经常出现。

2.4向量的范数

数有大小,向量也有大小,向量的大小我们通过范数(Norm)来衡量。范数在机器学习、深度学习中运用非常广泛,特别在限制模型复杂度、提升模型的泛化能力方面效果不错。p范数的定义如下:
||x||_p=(\sum_i |x_i| ^p)^{\frac 1p}\tag{2.6}
其中p\in R,P\geq 1
直观上来看,向量x的范数是度量从原点到点x的距离,范数是将向量映射到非负值的函数,如果从广义来说,任意一个满足以下三个条件的函数,都可称为范数:
(1)非负性:f(x)≥0,且当f(x)=0时,必有x=0;
(2)三角不等式性:f(x+y)≤f(x)+f(y);
(3)齐次性:\forall \alpha\in R,\forall x \in R^n f(\alpha x)=| \alpha|f(x)

当p=1时,即L^1范数,也称为绝对值范数,大小等于向量的每个元素绝对值之和,即:
||x||=\sum_i|x_i|
当p=2时,即L^2范数,也称为欧几里得范数,其大小表示从原点到当前点的欧几里得距离,即:
||x||_2=\sqrt{(x_1^2+x_2^2+\cdots+x_n^2)}\tag{2.7}
当p为\infty时,即L^{\infty}范数,也称为最大范数,它的值等于向量中每个元素的绝对值的最大值,即:
||x||_{\infty}=max_i (|x_i|)\tag{2.8}
前面主要介绍了利用范数来度量向量的大小,矩阵的大小如何度量呢?我们可以用类似的方法。在深度学习中,常用Frobenius范数来描述,即:
||A||_F=\sqrt{(\sum_{i,j}A_{i,j}^2 )}
它有点类似向量的L^2范数。
两个向量的点积可以用范数来表示,即:
x^T y=||x||_2 ||y||_2 \cos\theta \tag{2.9}
其中\theta表示x与y之间的夹角。 以上说了向量一种度量方式,即通过范数来度量向量或矩阵的大小,并有具体公式,在实际编程中如何计算向量的范数呢?这里我们还是以Python为例进行说明。

打印结果如下:
[ 0. 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9]
4.5
1.68819430161
0.9
由此看出利用Python求向量的范数还是很方便的。
例3:点积与范数的应用实例:
在平面解析几何中,点(x_0,y_0)到直线ax+by+c=0的距离为:
d=\frac{|ax_0+by_0+c|}{a^2+b^2}
在空间解析几何中,点(x_0,y_0,z_0)到平面ax+by+cz+d=0的距离为:
l=\frac{|ax_0+by_0+cz_0+d|}{a^2+b^2+c^2}
将其推广到n维空间,点(x_1,x_2,\cdots,x_n)到超平面\omega_1 x_1+\omega_2 x_2+\cdots+\omega_n x_n+b=0(或w^T X+b=0)的距离为:
d=\frac{|w^T X+b|}{||w||_2}

2.5线性相关性

前面我们介绍了向量、矩阵等概念,接下来我们将介绍向量组、线性组合、线性相关性、秩等重要概念。
由多个同维度的列向量构成的集合称为向量组,矩阵可以看成是由行向量或列向量构成的向量组。

2.5.1线性组合

给定向量组X:x_1,x_2,\cdots,x_n其中x_i\in R^m,对任何一组实数k_1,k_2,\cdots,k_n,构成的表达式:
k_1 x_1+k_2 x_2+\cdots+k_n x_n\tag{2.10}
称为向量组X的一个线性组合,k_1,k_2,\cdots,k_n称为向量组的系数。
对于任意一个m维向量b,如果存在一组实数k_1,k_2,\cdots,k_n,使得:
k_1 x_1+k_2 x_2+\cdots+k_n x_n=b
成立,则称向量b可以被向量组X:x_1,x_2,\cdots,x_n线性表示。
对于任意实数集\{k_1,k_2,\cdots,k_n\},由(2.10)式构成的所有向量集合,称为向量空间\{k_1 x_1+k_2 x_2+\cdots+k_n x_n\},其中k_i\in R
向量空间的概念有点抽象,我们举一个简单实例来说明这个概念,比如由三个向量构成的向量组\{(1,0,0),(0,1,0),(0,0,1)\} 和任何一组实数\{k_1,k_2,k_3\}就构成了一个三维空间。

2.5.2线性相关

线性相关性主要分析向量之间的关系,一组向量他们之间有哪些关系呢?这些关系有何重要应用?
向量组x_1,x_2,\cdots,x_n线性相关 <=> 存在不全为0的实数k_1,k_2,\cdots,k_n,使得:
k_1 x_1+k_2 x_2+\cdots+k_n x_n=0\tag{2.11}

如果不存在一组不全为0的数使k_1 x_1+k_2 x_2+\cdots+k_n x_n=0<=>向量组x_1,x_2,\cdots,x_n线性无关或线性独立。

例1:x_1=\binom{1}{0}, x_2=\binom{0}{1}线性无关,因为k_1 x_1+k_2 x_2=k_1 \binom{1}{0}+k_2 \binom{0}{1}=\binom{k_1}{k_2}=0,只有k_1=k_2=0,所以x_1,x_2线性无关。
例2:x_1=\binom{1}{0}, x_2=\binom{0}{1},x_3=\binom{1}{1}线性有关,因为x_1+x_2-x_3=0,k_1=1,k_2=1,k_3=-1
都不为0(至少一个不为0),所以x_1,x_2,x_3线性相关。
线性相关和线性无关的等价定义:利用矩阵乘法公式,式(2.11)可表示为:
\left(x_1,\cdots,x_n\right)\left(\begin{matrix}k_1 \cr \vdots \cr k_n\end {matrix}\right)=0,\Rightarrow Ax=0
这里假设:A=(a_1,\cdots,a_n),x=\left(\begin{matrix}x_1 \cr \vdots \cr x_n\end {matrix}\right),由线性方程组的理论,可得:
Ax=0只有零解,\Rightarrow a_1,\cdots,a_n线性无关;
Ax=0有非零解,\Rightarrow a_1,\cdots,a_n线性相关;
由线性方程组解的存在性与矩阵的秩的关系,有如下定理:
R(A)=n,\Rightarrow a_1,\cdots,a_n线性无关;
R(A)<n, \Rightarrow a_1,\cdots,a_n线性相关;

2.5.3向量组的秩

假设在原向量组X:x_1,x_2,\cdots,x_n存在一个子向量组,不妨设为X_0:x_1,x_2,\cdots,x_r,r<n,满足: (1)x_1,x_2,\cdots,x_r线性无关; (2)向量组X的中任意r+1个向量构成的子向量组都是线性相关的。 那么,称向量组X_0:x_1,x_2,\cdots,x_r,r 秩是一个重要概念,运用非常广泛,实际上矩阵我们可以看成是一个向量组。如果把矩阵看成是由所有行向量构成的向量组,这样矩阵的行秩就等于行向量组的秩;如果把矩阵看成是由所有列向量构成的向量组,这样矩阵的列秩就等于列向量组的秩。矩阵的行秩与列秩相等,因此,把矩阵的行秩和列秩统称为矩阵的秩。

2.5.4.向量空间

如果在n维向量集合S上的加法、数乘运算封闭,则称S为向量空间或线性空间。
即,对任意x,y\in S,都有x+y\in S,kx\in S,其中k是任意实数.
如通常看到的2维向量构建的集合R^2,3维向量构成的集合R^3,就是向量空间或线性空间。

第1章NumPy基础

请参考《机器学习的数学》中NumPy基础

NumPy基础

第2章 PyTorch基础

PyTorch是Facebook团队于2017年1月发布的一个深度学习框架,虽然晚于TensorFlow、Keras等框架,但自发布之日起,其关注度就在不断上升,目前在GitHub上的热度已超过Theano、Caffe、MXNet等框架。
与PyTorch 1.0之前的版本相比,PyTorch 1.0版本增加了很多新功能,对原有内容进行了优化,并整合了caffe2,使用更方便,也大大增强其生产性,所以其热度在迅速上升。
PyTorch采用Python语言接口来实现编程,非常容易上手。它就像带GPU的NumPy,而且与Python一样都属于动态框架。PyTorch继承了Torch灵活、动态的编程环境和用户友好等特点,支持以快速和灵活的方式构建动态神经网络,还允许在训练过程中快速更改代码而不妨碍其性能,支持动态图形等尖端AI模型的能力,是快速实验的理想选择。本章主要介绍PyTorch的一些基础且常用的概念和模块,具体包括如下内容:
 为何选择PyTorch
 PyTorch环境的安装与配置
 NumPy与Tensor
 Tensor与Autograd
 使用NumPy实现机器学习
 使用Tensor及antograd实现机器学习
 使用优化器自动微分等实现机器学习
 使用TensorFlow2架构实现机器学习

2.1 为何选择PyTorch

PyTorch是一个建立在Torch库之上的Python包,旨在加速深度学习应用。它提供一种类似NumPy的抽象方法来表征张量(或多维数组),可以利用GPU来加速训练。由于 PyTorch 采用了动态计算图(Dynamic Computational Graph)结构,且基于tape的autograd 系统的深度神经网络。其他很多框架,比如 TensorFlow(TensorFlow2.0也加入了动态网络的支持)、Caffe、CNTK、Theano 等,采用静态计算图。通过PyTorch一种称之为反向模式自动微分(Reverse-mode auto-differentiation)的技术,你可以非常方便地构建网络。
torch是PyTorch中的一个重要包,它包含了多维张量的数据结构以及基于其上的多种数学操作。
自2015 年谷歌开源 TensorFlow以来,深度学习框架之争越来越激烈,全球多个看重 AI 研究与应用的科技巨头均在加大这方面的投入。从 2017 年年初发布以来,PyTorch 可谓是异军突起,在短时间内就取得了一系列成果,成为其中的明星框架。之后PyTorch进行了一些较大的版本更新,如0.4版本把Varable与Tensor进行了合并,增加了Windows的支持;1.0版本增加了JIT(全称Just-in-time compilation,即时编译,它弥补了研究与生产的部署的差距)、更快的分布式、C++扩展等。
目前PyTorch 1.0 稳定版已发布,它 从 Caffe2 和 ONNX 移植了模块化和产品导向的功能,并将它们和 PyTorch 已有的灵活、专注研究的特性相结合。PyTorch 1.0 中的技术已经让很多 Facebook 的产品和服务变得更强大,包括每天执行 60 亿次文本翻译。
PyTorch由4个主要包组成,具体如下。
 torch:类似于NumPy的通用数组库,可将张量类型转换为torch.cuda.TensorFloat,并在GPU上进行计算。
 torch.autograd:用于构建计算图形并自动获取梯度的包。
 torch.nn:具有共享层和损失函数的神经网络库。
 torch.optim:具有通用优化算法(如SGD,Adam等)的优化包。

2.2 安装配置

在安装PyTorch时,请先核查当前环境是否有GPU,如果没有,则安装CPU版PyTorch;如果有,则安装GPU版PyTorch。

2.2.1 安装CPU版PyTorch

安装CPU版PyTorch的方法比较简单。PyTorch是基于Python开发的,所以如果没有安装Python则需要先安装Python,再安装PyTorch。具体步骤如下。
1. 下载Python
安装Python建议采用anaconda方式安装,先从Anaconda的官网:https://www.anaconda.com/distribution, 如图2-1 所示。

图2-1 下载Anaconda界面
下载Anaconda3的最新版本,如Anaconda3-2021.11-Linux-x86_64.sh,建议使用3系列,3系列代表未来发展。另外,下载时根据自己环境,选择操作系统等。
2. 安装Python
在命令行,执行如下命令,开始安装Python:
Anaconda3-2021.11-Linux-x86_64.sh
根据安装提示,直接按回车即可。其间会提示选择安装路径,如果没有特殊要求,可以按回车使用默认路径(~/ anaconda3),然后就开始安装。安装完成后,程序提示是否把anaconda3的binary路径加入到当前用户的.bashrc配置文件中,建议添加。添加以后,就可以使用python、ipython命令时自动使用Anaconda3的python环境。
3. 安装PyTorch
登录PyTorch官网(https://pytorch.org/),登录后,可看到如图2-2 所示界面,然后选择对应项。

图2-2 安装CPU版PyTorch

把第⑥项内容复制到命令行,执行即可。
conda install pytorch-cpu torchvision-cpu -c pytorch
(6)验证安装是否成功
启动Python,然后执行如下命令,如果没有报错,说明安装成功!

2.2.2 安装GPU版PyTorch

安装GPU版本的PyTorch稍微复杂一点,除需要安装Python、PyTorch,还需要安装GPU的驱动(如英伟达的Nvidia)及cuda、cuDNN计算框架,主要步骤如下。
1. 安装NVIDIA驱动
下载地址为https://www.nvidia.cn/Download/index.aspx?lang=cn。 登录可以看到如图2-3所示的界面。

图2-3 NVIDIA的下载界面
选择产品类型、操作系统等,然后点击搜索按钮,进入下载界面。
安装完成后,在命令行输入nvidia-smi,用来显示GPU卡的基本信息,如果出现如图2-4所示信息,则说明安装成功。如果报错,则说明安装失败,请搜索其他安装驱动的方法。

图2-4 显示GPU卡的基本信息
2. 安装CUDA
CUDA(Compute Unified Device Architecture,统一计算设备架构),是英伟达公司推出的一种基于新的并行编程模型和指令集架构的通用计算架构,它能利用英伟达GPU的并行计算引擎,比CPU更高效地解决许多复杂计算任务。安装CUDA 驱动时,需保证该驱动与NVIDIA GPU 驱动的版本一致,这样CUDA才能找到显卡。
3. 安装cuDNN
NVIDIA cuDNN是用于深度神经网络的GPU加速库。注册NVIDIA并下载cuDNN包,地址为https://developer.nvidia.com/rdp/cudnn-archive。
4. 安装Python及PyTorch
这步与2.2.1节安装CPU版PyTorch的步骤相同,只是选择CUDA时,不是选择None,而是选择对应CUDA的版本号,如图2-5所示。

图2-5 安装GPU版PyTorch
5. 验证
验证PyTorch安装是否成功的方法与2.2.1节一样,如果想进一步验证PyTorch是否在使用GPU,可以运行以下这段测试GPU的程序test_gpu.py。

在命令行运行以下脚本:
python test_gpu.py
如果可以看到如图2-6所示的结果,说明安装GPU版PyTorch成功!

图2-6 运行test_gpu.py的结果
在命令行运行nvidia-smi,可以看到如图2-7所示界面。

图2-7 含GPU进程的显卡信息

2.3 Jupyter Notebook环境配置

Jupyter Notebook是目前Python比较流行的开发、调试环境,此前被称为 IPython Notebook。它以网页的形式打开,可以在网页页面中直接编写代码和运行代码,代码的运行结果(包括图形)也会直接显示,如在编程过程中添加注释、目录、图像或公式等内容。Jupyter Notebook具有以下特点。
 编程时具有语法高亮、缩进、tab补全的功能。
 可直接通过浏览器运行代码,同时在代码块下方展示运行结果。
 以富媒体格式展示计算结果。富媒体格式包括:HTML,LaTeX,PNG,SVG等。
 对代码编写说明文档或语句时,支持Markdown语法。
 支持使用LaTeX编写数学性说明。
接下来介绍配置Jupyter Notebook的主要步骤。
1)生成配置文件。

将在当前用户目录下生成文件:.jupyter/jupyter_notebook_config.py
2)生成当前用户登录jupyter密码。打开ipython, 创建一个密文密码:

3)修改配置文件。

进行如下修改:

4)启动Jupyter Notebook。

在浏览器上,输入IP:port,即可看到如图2-8所示界面。

图2-8 Jupyter notebook主页界面
接下来就可以在浏览器进行开发调试PyTorch、Python等任务了。

2.4 NumPy与Tensor

第1章我们介绍了NumPy,知道其读取数据非常方便,而且还拥有大量的函数,所以深得数据处理、机器学习者喜爱。这节我们将介绍PyTorch的Tensor,它可以是零维(又称为标量或一个数)、一维、二维及多维的数组。其自称为神经网络界的NumPy, 它与NumPy相似,它们共享内存,它们之间的转换非常方便和高效。不过它们也有不同之处,最大的区别就是NumPy 会把 ndarray 放在 CPU 中加速运算,而由Torch 产生的 Tensor 会放在 GPU 中进行加速运算(假设当前环境有GPU)。

2.4.1 Tensor概述

对Tensor的操作很多,从接口的角度来划分,可以分为两类:
1)torch.function,如torch.sum、torch.add等,
2)tensor.function,如tensor.view、tensor.add等。
这些操作对大部分Tensor都是等价的,如torch.add(x,y)与x.add(y)等价。在实际使用时,可以根据个人爱好选择。
如果从修改方式的角度,可以分为以下两类。
1)不修改自身数据,如x.add(y),x的数据不变,返回一个新的tensor。
2)修改自身数据,如x.add_(y)(运行符带下划线后缀),运算结果存在x中,x被修改。
以下代码说明add与add_的区别。

运行结果如下:
tensor([4, 6])
tensor([1, 2])
tensor([4, 6])

2.4.2 创建Tensor

新建Tensor的方法很多,可以把列表或ndarray等数据对象直接转换为Tensor,也可以根据指定的形状构建。常见的构建Tensor的方法,可参考表2-1。
表2-1 常见的新建Tensor方法

函数 功能
Tensor(*size) 直接从参数构造一个的张量,支持list、numpy数组
eye(row, column) 创建指定行数,列数的二维单位tensor
linspace(start,end,steps) 从step到end,均匀切分成steps份
logspace(start,end,steps) 从10^step, 到10^end,均匀切分成steps份
rand/randn(*size) 生成[0,1)均匀分布/标准正态分布数据
ones(*size) 返回指定shape的张量,元素初始为1
zeros(*size) 返回指定shape的张量,元素初始为0
ones_like(t) 返回与t的shape相同的张量,且元素初始为1
zeros_like(t) 返回与t的shape相同的张量,且元素初始为0
arange(start,end,step) 在区间[start,end)上以间隔step生成一个序列张量
from_numpy(ndarray) 从ndarray创建一个tensor

下面举例说明。

【说明】注意torch.Tensor与torch.tensor的几点区别
1)torch.Tensor是torch.empty和torch.tensor之间的一种混合,但是,当传入数据时,torch.Tensor使用全局默认dtype(FloatTensor),torch.tensor从数据中推断数据类型。
2)torch.tensor(1)返回一个固定值1,而torch.Tensor(1)返回一个大小为1的张量,它是随机初始化的值。
举例如下。

运行结果如下:
t1的值tensor([3.5731e-20]),t1的数据类型torch.FloatTensor
t2的值1,t2的数据类型torch.LongTensor

下面来看一些根据一定规则,自动生成tensor的例子。

2.4.3 改变Tensor形状

在处理数据、构建网络层等过程中,我们经常需要了解Tensor的形状、改变Tensor的形状。与改变NumPy的形状类似,改变tenor的形状也有很多类似函数,具体可参考表2-2。 表2-2 为tensor常用修改形状的函数。

函数 说明
size() 返回张量的shape属性值,与函数shape(0.4版新增)等价
numel(input) 计算tensor的元素个数
view(*shape) 修改tensor的shape,与reshape(0.4版新增)类似,但view返回的对象与源tensor共享内存,修改一个另一个同时修改。Reshape将生成新的tensor,而且不要求源tensor是连续的。View(-1)展平数组。
resize 类似于view,但在size超出时会重新分配内存空间
item 若tensor为单元素,则返回pyton的标量
unsqueeze 在指定维度增加一个"1"
squeeze 在指定维度压缩一个"1"

下面来看一些实例。

【说明】torch.view与torch.reshape的异同。
1)reshape()可以由torch.reshape(),也可由torch.Tensor.reshape()调用。view()只可由torch.Tensor.view()来调用。
2)对于一个将要被view的Tensor,新的size必须与原来的size与stride兼容。否则,在view之前必须调用contiguous()方法。
3)同样也是返回与input数据量相同,但形状不同的tensor。若满足view的条件,则不会copy,若不满足,则会copy。
4)如果你只想重塑张量,请使用torch.reshape。 如果您还关注内存使用情况并希望确保两个张量共享相同的数据,请使用torch.view。

2.4.4 索引操作

Tensor的索引操作与NumPy类似,一般情况下索引结果与源数据共享内存。从tensor获取元素除了可以通过索引,也可借助一些函数,常用的选择函数可参考表2-3。
表2-3 常用选择操作函数

函数 说明
index_select(input,dim,index) 在指定维度上选择一些行或列
nonzero(input) 获取非0元素的下标
masked_select(input,mask) 使用二元值进行选择
gather(input,dim,index) 在指定维度上选择数据,输出的形状与index(index的类型必须是LongTensor类型的)一致
scatter_( input, dim, index, src) 为gather的反操作,根据指定索引补充数据

以下为部分函数的实现代码:

2.4.5 广播机制

前文1.8节介绍了NumPy的广播机制,它是向量运算的重要技巧。PyTorch也支持广播规则,下面通过几个示例进行说明。

2.4.6 逐元素操作

与NumPy一样,tensor也有逐元素操作,操作内容相似,但使用函数可能不尽相同。大部分数学运算都属于逐元操作,逐元素操作输入与输出的形状相同。,常见的逐元素操作,可参考表2-4。
表2-4常见逐元素操作

函数 说明
abs/add 绝对值/加法
addcdiv(t,t1,t2,value=1) t1与t2的按元素除后,乘value加t
addcmul(t,t1,t2, value=1) t1与t2的按元素乘后,乘value加t
ceil/floor 向上取整/向下取整
clamp(t, min, max) 将张量元素限制在指定区间
exp/log/pow 指数/对数/幂
mul(或*)/neg 逐元素乘法/取反
sigmoid/tanh/softmax 激活函数
sign/sqrt 取符号/开根号

【说明】这些操作均创建新的tensor,如果需要就地操作,可以使用这些方法的下划线版本,例如abs_。
以下为部分逐元素操作代码实例。

2.4.7 归并操作

归并操作,顾名思义,就是对输入进行归并或合计等操作,这类操作的输入输出形状一般不相同,而且往往是输入大于输出形状。归并操作可以对整个tensor进行归并,也可以沿着某个维度进行归并。常见的归并操作可参考表2-5。
表2-5 常见的归并操作

函数 说明
cumprod(t, axis) 在指定维度对t进行累积
cumsum 在指定维度对t进行累加
dist(a,b,p=2) 返回a,b之间的p阶范数
mean/median 均值/中位数
std/var 标准差/方差
norm(t,p=2) 返回t的p阶范数
prod(t)/sum(t) 返回t所有元素的积/和

【说明】
归并操作一般涉及一个dim参数,指定沿哪个维进行归并。另一个参数是keepdim,说明输出结果中是否保留维度1,默认情况是False,即不保留。
以下为归并操作的部分代码。

2.4.8 比较操作

比较操作一般进行逐元素比较,有些是按指定方向比较。常用的比较函数可参考表2-6。
表2-6 常用的比较函数

函数 说明
eq 比较tensor是否相等,支持broadcast
equal 比较tensor是否有相同的shape与值
ge/le/gt/lt 大于/小于比较/大于等于/小于等于比较
max/min(t,axis) 返回最值,若指定axis,则额外返回下标
topk(t,k,axis) 在指定的axis维上取最高的K个值

以下是部分函数的代码实现。

2.4.9 矩阵操作

机器学习和深度学习中存在大量的矩阵运算,用的比较多的有两种,一种是逐元素乘法,另外一种是点积乘法。PyTorch中常用的矩阵函数可参考表2-7。
表2-7 常用矩阵函数

函数 说明
dot(t1, t2) 计算张量(1D)的内积或点积
mm(mat1, mat2)/bmm(batch1,batch2) 计算矩阵乘法/含batch的3D矩阵乘法
mv(t1, v1) 计算矩阵与向量乘法
t 转置
svd(t) 计算t的SVD分解

【说明】
1)torch的dot与NumPy的dot有点不同,torch中dot对两个为1维张量进行点积运算,NumPy中的dot无此限制。
2)mm是对2维矩阵进行点积运算,bmm对含batch的3维矩阵进行点积运算。
3)转置运算会导致存储空间不连续,需要调用contiguous方法转为连续。

2.4.10 PyTorch与NumPy比较

PyTorch与NumPy有很多类似的地方,并且有很多相同的操作函数名称,或虽然函数名称不同但含义相同;当然也有一些虽然函数名称相同,但含义不尽相同。对此,有时很容易混淆,下面我们把一些主要的区别进行汇总,具体可参考表2-8。
表2-8 PyTorch与NumPy函数对照表

操作类别 NumPy PyTorch
数据类型 np.ndarray torch.Tensor
np.float32 torch.float32; torch.float
np.float64 torch.float64; torch.double
np.int64 torch.int64; torch.long
从已有数据构建 np.array([3.2, 4.3], dtype=np.float16) torch.tensor([3.2, 4.3],
dtype=torch.float16)
x.copy() x.clone()
np.concatenate torch.cat
线性代数 np.dot torch.mm
属性 x.ndim x.dim()
x.size x.nelement()
形状操作 x.reshape x.reshape; x.view
x.flatten x.view(-1)
类型转换 np.floor(x) torch.floor(x); x.floor()
比较 np.less x.lt
np.less_equal/np.greater x.le/x.gt
np.greater_equal/np.equal/np.not_equal x.ge/x.eq/x.ne
随机种子 np.random.seed torch.manual_seed

2.5 Tensor与Autograd

在神经网络中,一个重要内容就是进行参数学习,而参数学习离不开求导,PyTorch是如何进行求导的呢?
现在大部分深度学习架构都有自动求导的功能,PyTorch也不列外,torch.autograd包就是用来自动求导的。autograd包为张量上所有的操作提供了自动求导功能,而torch.Tensor和torch.Function为autograd包的两个核心类,它们相互连接并生成一个有向非循环图。接下来我们先简单介绍tensor如何实现自动求导,然后介绍计算图,最后用代码实现这些功能。

2.5.1 自动求导要点

autograd包为对tensor进行自动求导,为实现对tensor自动求导,需考虑如下事项。
1)创建叶子节点(leaf node)的tensor,使用requires_grad参数指定是否记录对其的操作,以便之后利用backward()方法进行梯度求解。requires_grad参数默认值为False,如果要对其求导需设置为True,与之有依赖关系的节点自动变为True。
2)可利用requires_grad_()方法修改tensor的requires_grad属性。可以调用.detach()或with torch.no_grad():将不再计算张量的梯度,跟踪张量的历史记录。这点在评估模型、测试模型阶段常常使用。
3)通过运算创建的tensor(即非叶子节点),会自动被赋于grad_fn属性。该属性表示梯度函数。叶子节点的grad_fn为None。
4)最后得到的tensor执行backward()函数,此时自动计算各变在量的梯度,并将累加结果保存grad属性中。计算完成后,非叶子节点的梯度自动释放。
5)backward()函数接受参数,该参数应和调用backward()函数的Tensor的维度相同,或者是可broadcast的维度。如果求导的tensor为标量(即一个数字),backward中参数可省略。
6)反向传播的中间缓存会被清空,如果需要进行多次反向传播,需要指定backward中的参数retain_graph=True。多次反向传播时,梯度是累加的。
7)非叶子节点的梯度backward调用后即被清空。
8)可以通过用torch.no_grad()包裹代码块来阻止autograd去跟踪那些标记为.requesgrad=True的张量的历史记录。这步在测试阶段经常使用。
在整个过程中,PyTorch采用计算图的形式进行组织,该计算图为动态图,它的计算图在每次正向传播时,将重新构建。其他深度学习架构,如TensorFlow、Keras一般为静态图。接下来我们介绍计算图,用图的形式来描述就更直观了,该计算图为有向无环图(DAG)。

2.5.2计算图

计算图是一种有向无环图像,用图形方式表示算子与变量之间的关系,直观高效。如图2-9所示,圆形表示变量,矩形表示算子。如表达式z=wx+b可写成两个表示式:如果y=wx,则z=y+b。其中x、w、b为变量,是用户创建的变量,不依赖于其他变量,故又称为叶子节点。为计算各叶子节点的梯度,需要把对应的张量参数requires_grad属性设置为True,这样就可自动跟踪其历史记录。y、z是计算得到的变量,非叶子节点,z为根节点。mul和add是算子(或操作或函数)。这些变量及算子就构成一个完整的计算过程(或正向传播过程)。

图2-9正向传播计算图
我们的目标是更新各叶子节点的梯度,根据复合函数导数的链式法则,不难算出各叶子节点的梯度。
\frac{\partial z}{\partial x}=\frac{\partial z}{\partial y}\frac{\partial y}{\partial x}=w \tag{2.1}
 \frac{\partial z}{\partial w}=\frac{\partial z}{\partial y}\frac{\partial y}{\partial w}=x \tag{2.2}
 \frac{\partial z}{\partial b}=b \tag{2.3}
PyTorch调用backward(),将自动计算各节点的梯度,这是一个反向传播过程,这个过程可用图2-9表示。在反向传播过程中,autograd沿着图2-10,从当前根节点z反向溯源,利用导数链式法则,计算所有叶子节点的梯度,并梯度值将累加到grad属性中。对非叶子节点的计算操作(或function)记录在grad_fn属性中,叶子节点的grad_fn值为None。

图2-10 梯度反向传播计算图
下面我们用代码实现这个计算图。

2.5.3 标量反向传播

PyTorch使用torch.autograd.backward来实现反向传播,backward函数的具体格式如下:

参数说明如下。
 tensor: 用于计算梯度的tensor。
 grad_tensors: 在计算非标量的梯度时会用到。其形状一般需要和前面的tensor保持一致。
 retain_graph: 通常在调用一次backward后,pytorch会自动把计算图销毁,如果要想对某个变量重复调用backward,则需要将该参数设置为True
 create_graph: 当设置为True的时候可以用来计算更高阶的梯度
 grad_variables:这个参数后面版本中应该会丢弃,直接使用grad_tensors就好了。
假设x、w、b都是标量,z=wx+b,对标量z调用backward(),我们无须对backward()传入参数。以下是实现自动求导的主要步骤。
1)定义叶子节点及算子节点。

运行结果如下:
x,w,b的require_grad属性分别为:False,True,True
2)查看叶子节点、非叶子节点的其他属性。

3)自动求导,实现梯度方向传播,即梯度的反向传播。

2.5.4 非标量反向传播

2.5.3小节我们介绍了当目标张量为标量时,调用backward()无须传入参数。目标张量一般是标量,如我们经常使用的损失值Loss,一般都是一个标量。但也有非标量的情况,后面我们介绍的Deep Dream的目标值就是一个含多个元素的张量。如何对非标量进行反向传播呢?PyTorch有个简单的原则,不让张量对张量求导,只允许标量对张量求导,因此,如果目标张量对一个非标量调用backward(),需要传入一个gradient参数,该参数也是张量,而且其形状需要与调用backward()的张量形状相同。
为什么要传入一个张量gradient?这是为了把张量对张量求导转换为标量对张量求导。这有点拗口,我们举一个例子来说,假设目标值为loss=(y_1,y_2,\cdots,y_m)传入的参数为v=(v_1,v_2,\cdots,v_m),那么就可把对loss的求导,转换为对loss*v^T标量的求导。即把原来\frac {\partial {loss}}{\partial X}得到雅可比矩阵(Jacobian)乘以张量v^T,便可得到我们需要的梯度矩阵。
1、 非标量简单示例
我们先看目标张量为非标量的简单实例。

运行后会报错:RuntimeError: grad can be implicitly created only for scalar outputs。这是因为张量y为非标量所致。
如何避免类似错误呢?我们手工计算Y的导数。已知:
X=[x_1,x_2]
Y=[x_1^2+3,x_2^2+3]
如何求\frac {\partial Y}{\partial X}呢?
Y为一个向量,如果我们想办法把这个向量转变成一个标量不就好了?比如我们可以对Y求和,然后用求和得到的标量在对X求导,这样不会对结果有影响,例如:
Y_{sum}=\sum y_i =x_1^2+x_2^2+6
\frac {\partial Y_{sum}}{\partial x_1}=2x_1,\frac {\partial Y_{sum}}{\partial x_2}=2x_2
这个过程可写成如下代码。

可以看到对y求和后再计算梯度没有报错,结果也与预期一样。
实际上,对Y求和就是等价于Y点积一个的全为1的向量或矩阵。即,而这个向量矩阵V也就是我们需要传入的grad_tensors参数。(点积只是相对于一维向量而言的,对于矩阵或更高为的张量,可以看做是对每一个维度做点积。)
2.非标量复杂实例
(1)定义叶子叶子节点及计算节点

(2)手工计算y对x的梯度
我们先手工计算一下y对x的梯度,为了验证PyTorch的backward的结果是否正确。
y对x的梯度是一个雅可比矩阵,各项的值,我们可通过以下方法进行计算。
假设x=(x_1=2,x_2=3),y=(y_1=x_1^2+3x_2,y_2=x_2^2+2x_1),不难得到:

x_1=2,x_2=3时,
(3)调用backward获取y对x的梯度
这里我们可以分成两步的计算。首先让v=(1,0)得到y_1对x的梯度,然后使v=(0,1),得到y_2对x的梯度。这里因需要重复使用backward(),需要使参数retain_graph=True,具体代码如下:

运行结果如下:
tensor([[4., 3.],[2., 6.]])
这个结果与手工运行的式(2.5)结果一致。
(4)如果V值不对,将导致错误结果。
如果取v=[1,1]将导致错误结果,代码示例如下:

这个结果与我们手工运算的不符,显然这个结果是错误的,错在哪里呢?这个结果的计算过程是:
J^T\cdot v^T=\left(\begin{matrix} 4 & 2\cr 3 & 6 \end{matrix}\right)\left(\begin{matrix} 1\cr 1\end{matrix}\right)=\left(\begin{matrix} 6\cr 9\end{matrix}\right)\tag{2.7}
由此,错在v的取值错误,通过这种方式得的到并不是y对x的梯度。
3.小结
1)PyTorch不允许张量对张量求导,只允许标量对张量求导,求导结果是和自变量同型的张量。
2)为避免直接对张量求导,可以利用torch.autograd.backward()函数中的参数grad_tensors, 把它转换标量来求导。 y.backward(v) 的含义是:先计算 loss = torch.sum(y * v),然后求 loss 对(能够影响到 y 的)所有变量 x 的导数。这里,y和 v是同型 Tensor。也就是说,可以理解成先按照 v对y的各个分量加权,加权求和之后得到真正的 loss,再计算这个 loss 对于所有相关变量的导数。
3)PyTorch中的计算图是动态计算图,动态计算图有两个特点:正向传播是立即执行的;反向传播后计算图立即销毁。我们把PyTorch使用自动微分的计算图的生命周期用图2-11来表示。
图2-11 PyTorch计算图的生命周期

2.5.5切断一些分支的反向传播

训练网络时,有时候我们希望保持一部分的网络参数不变,只对其中一部分的参数进行调整;或者只训练部分分支网络,并不让其梯度对主网络的梯度造成影响,这时候可以使用detach()函数来切断一些分支的反向传播。
detach_()将张量从创建它的计算图(Graph)中分离,把它作为叶子节点,其grad_fn=None且requires_grad=False。
假设y是作为x的函数,而z则是y和x的函数。如果我们想计算z关于x的梯度,但由于某种原因,我们希望将y视为一个常数。为此,我们可以分离y来返回一个新变量c,c变量与y具有相同的值, 但丢弃计算图中如何计算y的任何信息。 换句话说,梯度不会向后流经c到x。 因此,下面的反向传播函数计算z=c*x关于x的偏导数,同时将c作为常数处理,即有\frac {\partial z}{\partial X}=c,而不是把z=x^3+3关于x的偏导数,\frac {\partial z}{\partial X}\neq 3x^2

由于变量c记录了y的计算结果,在y上调用反向传播, 将得到y= x**2+3关于的x的导数,即2*x。

2.6 使用NumPy实现机器学习

前面我们介绍了NumPy、Tensor的基础内容,对如何用NumPy、Tensor操作数组有了一定认识。为了加深大家对PyTorch的谅解,本章剩余章节将分别用NumPy、Tensor、autograd、nn及optimal实现同一个机器学习任务,比较它们的异同及优缺点,从而加深对PyTorch的理解。
首先,我们用最原始的NumPy实现一个有关回归的机器学习任务,不用PyTorch中的包或类。这种方法的代码可能会多一点,但每一步都是透明的,有利于理解每步的工作原理。
主要步骤分析如下。
首先,是给出一个数组x,然后基于表达式:y=3x^2+2,加上一些噪声数据到达另一组数据y。
然后,构建一个机器学习模型,学习表达式y=wx^2+b的两个参数w,b。利用数组x,y的数据训练模型。
最后,采用梯度下降法,通过多次迭代,学习到w、b的值。
1)导入需要的库。

2)生成输入数据x及目标数据y。设置随机数种子,生成同一个份数据,以便用多种方法进行比较。

3)查看x,y数据分布情况。

运行结果如图2-12所示。

图2-12 NumPy实现的源数据
4)初始化权重参数。

5)训练模型。
定义损失函数,假设批量大小为100:
用代码实现上面这些表达式:

6)查看可视化结果。

运行结果如图2-13所示。

图2-13 可视化NumPy学习结果
[[2.98927619]] [[2.09818307]]
从结果看来,学习效果还是比较理想的。

2.7 使用Tensor及Autograd实现机器学习

2.6节可以说是纯手工完成一个机器学习任务,数据用NumPy表示,梯度学习是自己定义并构建学习模型。这种方法适合于比较简单的情况, 如果稍微复杂一些, 代码量将几何级增加。 是否有更方便的方法呢? 这节我们将使用PyTorch的自动求导的一个
包——autograd,利用这个包及对应的Tensor, 便可利用自动反向传播来求梯度,无须手工计算梯度。以下是具体实现代码。
1)导入需要的库。

2)生成训练数据,并可视化数据分布情况。

运行结果如图2-14所示。

图2-14 可视化输入数据
3)初始化权重参数。

4)训练模型。

5)查看可视化训练结果。

运行结果如图2-15所示。

图2-15 使用 autograd的结果
tensor([[2.9645]], requires_grad=True) tensor([[2.1146]], requires_grad=True)。
这个结果与使用NumPy机器学习的差不多。

2.8 使用优化器及自动微分

使用PyTorch内置的损失函数、优化器和自动微分机制等,可大大简化整个机器学习过程。梯度更新可简化为optimizer.step(),梯度清零可使用optimizer.zero_grad()。详细代码如下。导入模块与生成数据代码与2.7小节的基本相同,只需添加导入nn模块(这个模块第3章将介绍),这里就重写了。
1)定义损失函数及优化器。

2)训练模型。

3)查看可视化运行结果。

运行结果如图2-16所示。

图2-16 使用优化器及自动微分(autograd)的结果
tensor([[2.6369]], requires_grad=True) tensor([[2.2360]], requires_grad=True)
由此可知,使用内置损失函数、优化器及自动微分实现机器学习比较简洁,这也是深度学习普遍采用的方式。

2.9 把数据集转换带批量的迭代器

把数据集转换为带批量的迭代器,这样训练时就可进行批量处理。如果数据量比较大,采用批量处理可提升训练模型的效率及性能。
1)构建数据迭代器。

2)训练模型。

3)查看可视化运行结果。

运行结果如图2-17所示。

图2-17 使用数据迭代器、优化器和自动微分(autograd)的结果
tensor([[2.6370]], requires_grad=True) tensor([[2.2360]], requires_grad=True)

2.10 使用TensorFlow2架构实现机器学习

2.6节用NumPy实现了回归分析,2.7节用PyTorch的autograd及Tensor实现了这个任务。这节我们用深度学习的另一个框架TensorFlow实现该回归分析任务,大家可比较一下不同架构之间的区别。为便于比较,这里使用TensorFlow 2实现这个任务。
1)导入库及生成训练数据。

2)生成训练数据,并初始化参数。

3)构建模型。

4)训练模型。

5)查看可视化运行结果。

运行结果如图2-18所示。

图2-18 使用Tensorflow的结果

2.11 小结

本章主要介绍PyTorch的基础知识,这些内容是后续章节的重要支撑。首先介绍了PyTorch的安装配置,然后介绍了PyTorch的重要数据结构Tensor。Tensor类似于NumPy的数据结构,但Tensor提供GPU加速及自动求导等技术。最后分别用NumPy、Tensor、autograd、Optimizer和TensorFlow2等技术分别实现同一个机器学习任务。

第10章 可视化

俗话说得好,“一图胜千言”,可见图像给我们带来的震撼效果。生活如此,机器学习也如此,图的直观、简单明了同样给我不一样的感觉和理解。那么,如何把数据变成图?如何把一些比较隐含的规则通过图像展示出来呢?
本章主要介绍几个基于Python、TensorFlow开发的可视化的强大工具,具体包括:
 matplotlib
 pyecharts

10.1 matplotlib

matplotlib 是 Python 中最著名的2D绘图库,它提供了与 matlab 相似的 API,十分适合交互式绘图,简单明了,功能强大,而且可以方便地作为绘图控件,嵌入 GUI 应用程序中。下面我们进入matplotlib的世界,开始我们的数据可视化之旅。

10.1.1 matplotlib的基本概念

在介绍matplotlib前,首先要保证环境中安装了Python。建议使用Anaconda安装,因为Anaconda安装包中包含很多常用的工具包,如matplotlib、NumPy、Pandas、Sklearn等,并且后续的更新维护也非常方便。
在绘制我们的第一个图形之前,我们先来了解几个matplotlib的非常重要的概念,以帮助我们更快地理解matplotlib的各种API,以及能让你和你的同事使用一种大家都能听得懂的语言以及术语进行沟通。
matplotlib设置坐标主要参数配置详细说明及示例说明如下。
1)导入绘图相关模块;
2)生成数据;
3)plot绘制图形,(选 - 线条设置)设置线linestyle或标记marker;
4)(选 - 坐标轴设置 - 添加坐标标签)给x轴添加标签xlabel和y轴添加标签ylabel;
5)(选 - 坐标轴设置 - 添加坐标刻度)设置x轴的刻度xlim()和y轴的刻度ylim();
6)(选 - 图例设置label)设置图例legend();
7)输出图形show()。
下面来看一个使用matplotlib绘图的实例,具体如下:

使用matplotlib对数据进行可视化的示例的运行结果如图10-1所示。

图10-1 使用matplotlib对数据进行可视化
也可以把图10-1拆成两个图,代码如下。

把图10-1拆成两个图的运行结果如图10-2所示。

图10-2 把图5-1拆成两个图

10.1.2 使用matplotlib绘制图表

matplotlib能绘制出各种各样的图表,所以开发人员可根据需要展示的数据格式、内容以及要用图表来达到的效果来选择合适的图形种类。下面我们通过日常工作中最常用的4种图表来做一个演示。
1.柱状图
柱状图是指用一系列高度不等的纵向条纹或者线段直观地显示统计报告来帮助人们理解数据的分布情况。在绘制柱状图时,我们可以使用plt.bar(x,y,tick_label),给出x,y坐标值,同时给出x坐标轴上对应刻度的含义等,示例如下。

绘制出的柱状图如图10-3所示。

图10-3 柱状图
2. 折线图
折线图通常用来显示随时间变化而变化的连续的数据,它非常适用于展示在相等的时间间隔下的数据的变化趋势。比如,使用折线图展示一个系统从2010年到2020年的每年的注册人数。在绘制折线图时,我们可以使用plt.plot()。 下面我们用折线图来显示系统注册人数的变化情况。

绘制出的折线图如图10-4所示。

图10-4 折线图
从图10-4中我们可以直观地看到,系统的注册人数在2011年进入了一个谷值,而2014是峰值。
3. 饼图
饼图常常用来显示一个数据系列中各项的大小及其在整体中的占比。比如我们可以用下面的饼图来展示每个人的月收入,并显示他们的月收入占总体收入的比例。

绘制出的饼图如图10-5所示。

图10-5 饼图
4. 散点图
散点图是指在回归分析中数据点在坐标系平面上的分布图,用于表示因变量随自变量变化而变化的大致趋势,从而帮助我们根据其中的关系选择合适的函数对数据点进行拟合。下面我们绘制一张身高和体重关系的散点图。

绘制出的散点图如图10-6所示。

图10-6 散点图
除了上述介绍的4种图形,matplotlib还可以绘制其他图形,比如线箱图、极限图、气泡图等。感兴趣的读者可以自行查阅matplotlib的网站或者源代码,以了解更多内容。

10.1.3 使用rcParams

rcParams用于存放matplotlib的图表全局变量,我们可以用它来设置全局的图表属性,当然在进行具体图表绘制的时候,我们也可以对全局变量进行覆盖。下面介绍几个常用的全局变量。注意,如果想在图表中显示中文内容,比如显示中文标题,则需要在matplotlib的全局变量rcParams里进行设置。
1)没设置rcParams属性。

运行结果如图10-7所示。

图10-7 没有设置rcParams属性的情况
如图10-7所示,中文标题没有正确显示,而是随机变成几个方框。此时,通过rcParams设置文字属性即可使标题正确显示。

运行结果如图10-8所示。

图10-8 设置rcParams属性的情况
更多关于rcParams的设置问题,请参照matplotlib官网(https://matplotlib.org/stable/api/matplotlib_configuration_api.html#matplotlib.RcParams)。

10.2 pyecharts

我们接下来要介绍的pyecharts正是Python版本的eCharts。
相较于经典的matplotlib,pyecharts可以在保证易用、简洁、交互性的基础上让开发人员绘制出种类更加丰富(比如3D,和地图模块的集成)、样式更加新颖的图表。下面我们先来看如何安装pyecharts 。

10.2.1 pyecharts安装

pyecharts 是一个用于生成 ECharts 图表的类库,官网为https://pyecharts.org/。
pyecharts有两个大的版本,v0.5.x 以及 v1.x。其中, v0.5.x 支持 Python 2.7 以及Python 3.4, v1.x 支持Python 3.6及以上版本。考虑到v0.5版本已经不再维护,而且大多数公司已经升级到Python 3.7及以上版本,所以本节只介绍1.x版本,并且以最新版v1.9为基础进行讲解。pyecharts安装主要有两种方式,通过源码或者pip安装,这里以pip安装为例进行讲解:

【说明】安装pyecharts时,可改用国内的安装源,如清华安装源,以提高下载速度,具体代码如下:

10.2.2 使用pyecharts绘制图表

我们先来用一个简单的例子直观地了解如何使用pyecharts绘图,体会它的便利性和优雅。

绘制出的pyecharts的柱状图如图10-9所示。
图10-9 pyecharts的柱状图
上述代码显示了苏州XX渔具店在2020年和2021年各种子品类的销售金额。 首先我们创建了一个Bar类型的图表,添加了X轴(add_xaxis)来代表各种品类,之后添加了两个Y轴的数据(add_yaxis)来代表2020年以及2021年的业绩。为了让图表更加容易理解,我们增加了标题以及副标题(title以及subtitle)。
用Pyecharts画的柱状图非常优雅,当然,用它画其他图形同样如此。绘制出的图形如下:
1. 仪表盘(Gauge)
我们第一个例子来模拟汽车的仪表盘,仪表盘上显示这辆汽车的最高时速,以及当前行驶速度,汽车仪表盘还会使用醒目的红色提醒驾驶员不要超速行驶,我们把这些信息一并添加到我们需要绘制的图形里面。

运行结果:

图10-10 仪表盘
大家可以从上图看到,仪表盘图形(Gauge)非常适合展示进度或者占比信息,通常我们会把几个仪表盘图形组合成一个组合图表进行展示,这样能让使用者对全局的信息有个快速的了解。不如,我们可以用几个仪表盘图形展示我们集群里面各个节点的健康状态,它们的CPU的使用率,IO的吞吐是不是在一个可承受的范围内等等。
2、地理坐标系(Geo)
这几年,各大app推出一个显示用户出行轨迹的应用广受各位旅游达人以及飞人的喜欢,在一张中国地图或者世界地图上,用箭头代表自己的飞行路径,线段的粗细代表了飞行这条航线的频率,让用户对自己过去一年的行踪有个直观的认识,也当做是在朋友圈凡尔赛的资料。接下来,我们用pyecharts来大概模拟这个功能。

pyecharts内嵌了中国以及各个省份的矢量图,可以方便的绘制出你想要的区域,使用者可以通过使用坐标或者城市名称的形式标定出具体的位置,进而用不同的颜色代表特殊的含义。

10.2.3 从上海出发的航线图

用于带有起点和终点信息的线数据的绘制,主要用于地图上的航线、路线的可视化。

打开shanghai-out.html

图10-11 从上海到各城市的航线图

10.3 实例:词云图

词云图又叫文字云,是对文本数据中出现频率较高的关键词予以视觉上的突出,形成"关键词的渲染"就类似云一样的彩色图片,从而过滤掉大量次要信息,使人一眼就可以领略文章的核心要义。

10.3.1 实例概况

实例环境:windows或linux,Python3.6+,jieba (中文分词),PIL(图像处理),wordcloud (词云表现)、matplotlib(图像显示)等。其中jieba、wordcloud需要用pip安装。具体安装方法如下:

文本信息:使用环球时报上一篇文章,题为《“中国芯”亟待顶层设计》

10.3.2 代码实现

【说明】大家可以从网络上随便采取一段文章作为输入文本(本例的输入文本为:chinese-core.txt),背景图片可以从网上随便下载一个作为词云背景图(本例的背景图像:back.jpg)。

图10-12 词云图

10.4 练习

1、尝试用其他主题文章进行词云展示。
2、尝试使用pyecharts的WordCloud画一下词云图,然后比较一下与有何区别。

第8章 文件处理和异常处理

8.1 问题:Python如何获取文件数据?

Python处理文件的步骤包括打开、读写、关闭。第一步当然就是先要打开文件。要以读文件的模式打开一个文件对象,使用Python内置的open()函数,传入文件名和其他参数。open() 函数常用形式是接收两个参数:文件名(file)和模式(mode),如:

完整的语法格式为:

其中:
 file: 必需,文件路径(相对或者绝对路径)。如果是linux环境,路径一般表示为'./data/file_name',如果是windows环境,一般表示为'.\data\file_name',因反斜杠"\"在Python中被视为转义字符,为确保正确,应以原字符串的方式指定路径,即在开头的单引号前加上r。
 mode: 可选,文件打开模式
 buffering: 可选,设置缓冲
 encoding: 可选,一般使用utf8
 errors: 可选,报错级别
 newline: 可选,区分换行符,如\n,\r\n等
 closefd: 可选,传入的file参数类型
 opener: 可选,可以通过调用*opener*来自定义opener。
用open()函数打开文件具体代码如下:

运行结果:
Python,java
PyTorch,TensorFlow,Keras
在操作系统中对文件的操作划分了很多权限,比如读权限、写权限、追加方式写和覆盖方式写等等。Python打开文件的常用语法格式是open(file,mode=’r’),第二个参数是字符,其值有规定的内容和含义,如表9-1所示:
表9-1 mode参数取值和含义

参数值 含义
‘r’ 以只读方式打开已存在的文件
‘w’ 以写入方式打开文件,如不存在则自动创建
‘x’ 以可写入方式打开文件
‘a’ 以追加方式打开文件,新写入的内容会附加在文件末尾
‘b’ 以二进制方式打开文件
‘t’ 以文本方式打开文件
‘+’ 以读写方式打开文件
‘U’ 通用换行符模式(不建议使用)

上面的参数值可以配合使用,比如open(file,’ab’)就是以追加方式打开二进制文件。如果open()方法不写mode参数,mode的默认值是’rt’,即只读方式打开文本文件。
如果要打开的文件并不存在,open方法会报错。如下所示:

这种报错信息叫做异常,如何捕捉异常、如何处理异常等9.4章节将介绍。

8.2基本文件操作

对文件的常用操作包括读取文件,写入文件。读取文件又可以根据文件的大小选择不同的读取方式,如按字节读取、逐行读取、读取整个文件等方式。

8.2.1 读取文件

打开文件后,读取文件使用read()方法。一个文本文件由多行字符串组成,而一行字符串又由多个字符组成。read(size)方法是以字节为单位读取文件内容。比如read(1)就是从当前文件指针位置开始,读取1个字节的内容。如果read()括号中没有数字或是负数,则读取整个文件内容。
(1)按字节读取
下面代码每次从文件中读取固定的1个字节。每次读完后,文件指针会指向下一个字节的位置,就好比用瓢从水缸中舀水,每次都盛出相同的水量。

(2)读取整个文件
不指定read()括号中的参数,会读取整个文件内容。

8.2.2读取文件使用with语句

无论使用哪种高级语言来读取文件,都是先打开磁盘上的一个物理文件,获得一个文件句柄,通过这个句柄(或称作文件对象)来读取,最后再关闭。如果代码中忘记了关闭文件对象,这个文件对象会一直存在于内存中,除非使用close()方法来释放这个文件对象所占用的空间。Python语言为了避免忘记关闭文件,提供了with关键字来自动关闭文件。即使用with格式,就不需要再写close语句了。

8.2.3 逐行读取文件

使用read()方法要么读取整个文件,要么读取固定字节数,总归不太方便。文本文件都是由多行字符串组成,Python也可以逐行读取文件,使用readline()方法。

(1)逐行读取文件内容并打印

运行结果如下:
no,name,age,gender

01,李康,15,M

02,张平,14,F

03,刘畅,16,M
(2)从上面的打印结果可以看出,行之间多了一个空行。为何出现这种情况?这是因为在文件中,每行的末尾都有一个不可见的换行符(如\n),print语句会加上这个换行符。如何去掉这些空行?只要在print中使用rstrip()或strip()即可:

运行结果:
no,name,age,gender
01,李康,15,M
02,张平,14,F
03,刘畅,16,M
(3)使用readline()可以每次读取一行
使用readline()也会把文件中每行末尾的回车符读进来,如果需要去掉这些空行,同样可以使用rstrip或strip函数。

8.2.4 读取文件所有内容

使用readline()方法虽然可以一次读一行,比使用read(size)方法一次读一个字节方便了不少,但每次运行readline()方法后,文件指针会自动指向下一行,仍然要再调用一次readline()方法,才能读取下一行内容。还是不方便。
(1)使用readlines()读取文件所有内容
Python还提供了readlines()方法一次把文件所有行都读出来,放入到一个列表中。

下面定义一个类Stu,该类实现利用readlines()函数返回的列表中,并处理每行的每列数据,并打印出每列属性值。
(2)定义类Stu

(3)处理文件中每列数据

#上面代码运行的结果如下:
学号:01,姓名:李康, 年龄:15,性别:M
学号:02,姓名:张平, 年龄:14,性别:F
学号:03,姓名:刘畅, 年龄:16,性别:M
上面介绍的三种读文件的方法,都是从文件头开始读,直到遇到文件结束符(EOF)。这种读取方式称作顺序读取。如果一个文件有几个G大小,我想读出其中的一小部分内容,可以采取随机读取方式,使用seek()或tell()方法,有兴趣的读者可以参考Python文档资料。

8.2.5写入文件

write(str)方法把str字符串写入文件,返回值是str字符串的长度。写文件前要先使用追加或写入模式打开文件。

上面代码中的文件名newfile,如果不存在将自动创建。写入的方式是"a",即追加的方式,如果存在将往里追加记录没有指定扩展名。。但写入的是字符串,仍然是一个文本文件,可以使用记事本查看。write方法写入的字符串最后不会加上回车键\n。
如果要把多行内容写文件,可以每行都调用write方法,Python也提供了writelines(seq)方法一次性写入多行内容。参数seq是一个列表或元祖。

以'w'模式写入文件时,如果文件已存在,会直接覆盖(相当于删掉后新写入一个文件)。
上面写入文件的字符串要加入回车键,否则即使调用多次writelines()方法,Python执行时也不会自动加上回车。
【说明】
要读取非UTF-8编码的文本文件,需要给open()函数传入encoding参数,例如,读取GBK编码的文件:

更多信息可参考:
https://blog.csdn.net/xrinosvip/article/details/82019844

8.3 异常处理

8.3.1 如何使你的程序更可靠?

写出程序能运行不是我们的目的,写出能运行且不出错的程序才是本事。代码的健壮性和稳定性是衡量一个软件好坏的指标之一。大多数高级语言都提供了异常处理机制来确保代码的健壮性。Python的异常处理语法简单且功能实用,是必须要掌握的要点。

8.3.2 捕获异常

异常处理有两个关键字:try和except。这两个关键字把程序分成两个代码块。try中放置程序正常运行代码,except中是处理程序出错后的代码。其语句结构如下:

try..except代码执行过程类似于 if...else,但后者仅限于可以预知的错误,而使用except是来捕获隐藏的错误。下面代码演示除数为零的异常。

在进行文件操作时,也会出各种异常情况,同样适用try..except语法格式。以下代码中要打开的文件并不存在,程序捕捉到这种异常后,会进入except模块

8.3.3 捕获多种异常

异常的种类有多种,针对不同类型的异常可以做区别处理。Python中定义的异常类型有很多种,常见的几种类型可参考表9-3:
表9-3 常见异常种类

异常类名 含义
AttributeError 对象缺少属性
IOError 输入/输出操作失败
ImportError 导入模块/对象失败
KeyError 集合中缺少键值错误
NameError 未声明或初始化变量
OSError 操作系统错误
StopIteration 迭代器没有更多的值
ZeroDivisionError 除数为0或用0取模
Exception 常规异常的基类

捕获多种异常的语法格式为:

我们把上一节的两种异常代码合并处理:

多个except并列时,try中的代码最先遇到哪个异常种类,就会进入对应的except代码块,而忽略其他的异常种类。except..as 后面的变量名e是为该异常类创建的实例,可以拿到具体的异常信息。

8.3.4 捕获所有异常

既然有那么多的异常种类,我需要每个都捕获么?那样代码写起来太冗长了。Python的每个常规异常类型被定义成了一个类,这些类都有一个共同的父类,就是Exception类。在不需要区分异常类型的情况下,把所有异常都归入Exception类也是通用的做法。

另外需要注意,如果多个except并列出现,要把Exception基类放在最下面,否则会出现某个异常种类捕捉不到的情况。以下代码是错误的:

8.3.5 清理操作

异常处理中还有一个关键字是finally。final是最终的意思,finally代码块放在所有except代码的后面,无论是否执行了异常代码,finally中的代码都会被执行。

finally关键字只能出现一次,里面的代码主要完成清理工作。比如关闭文件、关闭数据库链接、记录运行日志等。如下代码把关闭文件放在finally中。

由于try、except、finally分属三个代码块,myfile变量需要定义在外面,以便在代码块中可以引用。

8.3.6 练习

编写一个脚本,实现以下功能:
(1)把用户名、用户登录密码写人文件,至少3条记录,文件名为login.txt
(2)文件login.txt列之间用逗号分割。
(3)用input函数作为一个登录界面,输入用户名、用户密码
(4)用input输入中的用户名及用户密码与文件login.txt中的用户名及密码进行匹对,如果两项都对,提示登录成功,否则提示具体错误,如用户名不存在或密码错误等。

第9章 正则化

正则表达式是处理字符串的强大工具,它有自己特定的语法结构,有了它,实现字符串的检索、替换、匹配验证都不在话下。当然,对于爬虫来说,有了它,从HTML里提取想要的信息就非常方便了。

9.1 简单实例

打开开源中国提供的正则表达式测试工具http://tool.oschina.net/regex/,输入待匹配的文本,然后选择常用的正则表达式,就可以得出相应的匹配结果了。例如,这里输入待匹配的文本如下:
my email is wumg3000 and my website is http://feiguyunai.com
在下图的输入框输入以上语句,然后点击测试匹配,则可得到匹配结果。

【结果解释】

a-z代表匹配任意的小写字母,\s表示匹配任意的空白字符(等价于\t \n \r\f),[^\s]表示不是非空白字符,*就代表匹配前面的字符任意多个,这一长串的正则表达式就是这么多匹配规则的组合。
[a-zA-Z]+://[^\s]* 结果为:
.*[a-zA-Z]+://[^\s]* 结果为:

9.2 常用匹配规则

以下是常用匹配规则

模式 描述
\w 匹配字母、数字及下划线, 注意 Unicode 正则表达式会匹配中文字符.
\W 匹配不是字母、数字及下划线的字符
\s 匹配任意空白字符,包括空格,制表符等,价于[ \t\n\r\f]  \r回车,\f换页
\S 匹配任意非空字符,等价于[^\f\n\r\t]
\d 匹配任意数字,等价于[0-9]
\D 匹配任意非数字的字符
\A 匹配字符串开头
\Z 匹配字符串结尾,如果存在换行,只匹配到换行前的结束字符串
\z 匹配字符串结尾,如果存在换行,同时还会匹配换行符
\G 匹配最后匹配完成的位置
\n 匹配一个换行符
\t 匹配一个制表符
^ 匹配一行字符串的开头
$ 匹配一行字符串的结尾
. 匹配任意字符,除了换行符,当re.DOTALL标记被指定时,则可以匹配包括换行符的任意字符
[...] 用来表示一组字符,单独列出,比如[amk]匹配a、m或k
[^...] 不在[]中的字符,比如[^abc]匹配除了a、b、c之外的字符
* 匹配前一个字符0个或多个
+ 匹配前一个字符1个或多个
? 匹配0个或1个前面的正则表达式定义的字符,非贪婪方式
{n} 精确匹配n个前面的表达式
{n, m} 匹配n到m次由前面正则表达式定义的片段,贪婪方式
a|b 匹配a或b
( ) 匹配括号内的表达式,也表示一个组

说明:
*、+、? {n}、{n,m}等为数量限定。

正则表达式特殊字符优先级:

优先级 符号
最高 \
“()” “(?:)” “(?=)” “[]”
中(数量限定) “*”“+” “?”“{n}” “{n,}” “{n,m}”
“^” “$” “中介字符”
次最低 串接,即相邻字符连接在一起
最低 “|”

9.3 常用函数

正则表达式常用函数

match() 决定正则表达式对象是否在字符串最开始的位置匹配。注意:该方法不是完全匹配。当模式结束时若 原字符串还有剩余字符,仍然视为成功。想要完全匹配,可以在表达式末尾加上边界匹配符“$”
search() 在字符串内查找模式匹配,只要找到第一个匹配然后返回,如果字符串没有匹配,则返回“None”
findall() 遍历匹配,可以获取字符串中所有匹配的字符串,返回一个列表
sub() 替换原字符串中每一个匹配的子串后返回替换后的字符串

9.3.1match()

这里首先介绍re的第一个常用的匹配方法——match(),向它传入要匹配的字符串以及正则表达式,就可以检测这个正则表达式是否匹配字符串。其格式为:
re.match(pattern, string, flags=0)
参数说明:
 Pattern:匹配的正则表达式
 String:匹配的字符串
 Flags:标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。
match()方法会尝试从字符串的起始位置匹配正则表达式,如果匹配,就返回匹配成功的结果;如果不匹配,就返回None。示例如下:

运行结果
1

Hello 123 4567 World_This
(0, 25)
【结果说明】
用它来匹配这个长字符串。
开头的^是匹配字符串的开头,也就是以Hello开头;
然后\s匹配空白字符,用来匹配目标字符串的空格;
\d匹配数字,3个\d匹配123;
然后再写1个\s匹配空格;
后面还有4567,我们其实可以依然用4个\d来匹配,但是这么写比较烦琐,所以后面可以跟{4}以代表匹配前面的规则4次,也就是匹配4个数字;
然后后面再紧接1个空白字符,
最后\w{10}匹配10个字母及下划线。
而在match()方法中,第一个参数传入了正则表达式,第二个参数传入了要匹配的字符串。
打印输出结果,可以看到结果是SRE_Match对象,这证明成功匹配。该对象有两个方法:group()方法可以输出匹配到的内容,结果是Hello 123 4567 World_This,这恰好是正则表达式规则所匹配的内容;span()方法可以输出匹配的范围,结果是(0, 25),这就是匹配到的结果字符串在原字符串中的位置范围。
如果想从字符串中提取一部分内容,该怎么办呢?
可以使用()括号将想提取的子字符串括起来。()实际上标记了一个子表达式的开始和结束位置,被标记的每个子表达式会依次对应每一个分组,调用group()方法传入分组的索引即可获取提取的结果。示例如下:

运行结果

Hello 1234567 World
1234567
(0, 19)
【结果说明】
可以看到,我们成功得到了1234567。这里用的是group(1),它与group()有所不同,后者会输出完整的匹配结果,group(1)输出第一个被()包围的匹配结果。假如正则表达式后面还有()包括的内容,那么可以依次用group(2)、group(3)等来获取,如下例

运行结果为

Hello 1234567 World_This is
1234567
is
(0, 27)
【练习】
1、用简单的正则表达式,输出如下结果:
Hello 1234567 World_This is
2、用简单的正则表达式,输出如下结果:
Hello 1234567 World_This

1、通配符
刚才我们写的正则表达式其实比较复杂,出现空白字符我们就写\s匹配,出现数字我们就用\d匹配,这样的工作量非常大。其实完全没必要这么做,因为还有一个万能匹配可以用,那就是.*(点星)。其中.(点)可以匹配任意字符(除换行符),*(星)代表匹配前面的字符无限次,所以它们组合在一起就可以匹配任意字符了。有了它,我们就不用挨个字符地匹配了。

接着上面的例子,我们可以改写一下正则表达式:

运行结果

Hello 1234567 World_This is a Regex Demo
7
(0, 40)
【结果说明】
这里为何是7而不是1234567?
这里就涉及一个贪婪匹配与非贪婪匹配的问题了。在贪婪匹配下,.*会匹配尽可能多的字符。正则表达式中.*后面是\d+,也就是至少一个数字,并没有指定具体多少个数字,因此,.*从开始处抓取满足模式的最长字符,这里就把123456匹配了,给\d+留下一个可满足条件的数字7,最后得到的内容就只有数字7了。
2、贪婪与非贪婪
为了达到我们预期的效果,我们可以采用非贪婪的方式。
非贪婪匹配的写法是.*?,多了一个?,那么它可以达到怎样的效果?我们再用实例看一下:

运行结果

Hello 1234567 World_This is a Regex Demo
1234567
(0, 40)

【结果说明】
此时就可以成功获取1234567了。原因可想而知,贪婪匹配是尽可能匹配多的字符,非贪婪匹配就是尽可能匹配少的字符。当.*?匹配到Hello后面的空白字符时,再往后的字符就是数字了,而\d+恰好可以匹配,那么这里.*?就不再进行匹配,;留给\d+去匹配后面的数字。所以这样.*?匹配了尽可能少的字符,\d+的结果就是1234567了。
所以说,在做匹配的时候,字符串中间尽量使用非贪婪匹配,也就是用.*?来代替.*,以免出现匹配结果缺失的情况。
但这里需要注意,如果匹配的结果在字符串结尾,.*?就有可能匹配不到任何内容了,因为它会匹配尽可能少的字符。例如:

运行结果
result1匹配结果:
result2匹配结果: kEraCN
【结果说明】
因.*?为非贪婪模式---即匹配尽可能少的字符,故.*?没有匹配到任何结果,而.*则尽量匹配多的内容,成功得到了匹配结果
3、标志符
在re.match函数中,有一个flags参数,缺省值为0,如果不为0,它有哪些作用呢?

运行结果:
报错,报错信息如下:
AttributeError Traceback (most recent call last)
in ()
5 '''
6 result = re.match('^He.*?(\d+).*?Demo$', content)
----> 7 print(result.group(1))

AttributeError: 'NoneType' object has no attribute 'group'
【结果说明】
运行直接报错,也就是说正则表达式没有匹配到这个字符串,返回结果为None,而我们又调用了group()方法导致AttributeError。
那么,为什么加了一个换行符,就匹配不到了呢?这是因为\.匹配的是除换行符之外的任意字符,当遇到换行符时,.*?就不能匹配了,所以导致匹配失败。这里只需加一个修饰符re.S,即可修正这个错误:

运行结果
1234567

这个re.S在网页匹配中经常用到。因为HTML节点经常会有换行,加上它,就可以匹配节点与节点之间的换行了。另外,还有一些标识符,在必要的情况下也可以使用,如下表。

标识符 描述
re.I 使匹配对大小写不敏感
re.L 做本地化识别(locale-aware)匹配
re.M 多行匹配,影响^和$
re.S 使.匹配包括换行在内的所有字符
re.U 根据Unicode字符集解析字符。这个标志影响\w、\W、 \b和\B
re.X 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解

4、转义字符
我们知道正则表达式定义了许多匹配模式,如.匹配除换行符以外的任意字符,但是如果目标字符串里面就包含.,那该怎么办呢?
这里就需要用到转义匹配了,示例如下:

运作结果

当遇到用于正则匹配模式的特殊字符时,在前面加反斜线(\)转义一下即可。

9.3.2 search()

match()方法是从字符串的开头开始匹配的,一旦开头不匹配,那么整个匹配就失败了。我们看下面的例子:

运算结果
None

【结果说明】
在匹配时,search()方法扫描整个字符串,返回第一个匹配字符串,如果搜索完了还没有找到,就返回None。

因此,为了匹配方便,我们可以尽量使用search()方法。下面再用几个实例来看看search()方法的用法。
首先,这里有一段待匹配的HTML文本,接下来写几个正则表达式实例来实现相应信息的提取

从以上HTML文件可知,ul节点里有许多li节点,其中li节点中有的包含a节点,有的不包含a节点,a节点还有一些相应的属性——超链接和歌手名、歌曲名。

以下我们从这个HTML文件中提取歌手名和歌名,正则表达式该如何写呢?
首先,我们尝试提取class为active的li节点内部的超链接包含的歌手名和歌名,此时需要提取第三个li节点下a节点的singer属性和文本。

此时正则表达式可以以li开头,然后寻找一个data-view为7,中间的部分可以用.*?来匹配。接下来,要提取singer这个属性值,所以还需要写入singer="(.*?)",这里需要提取的部分用小括号括起来,以便用group()方法提取出来,它的两侧边界是双引号。
然后还需要匹配a节点的文本,其中它的左边界是>,右边界是。然后目标内容依然用(.*?)来匹配,所以最后的正则表达式就变成了:

然后再调用search()方法,它会搜索整个HTML文本,找到符合正则表达式的第一个内容返回。另外,由于代码有换行,所以这里第三个参数需要传入re.S。整个匹配代码如下

运行结果
任贤齐沧海一声笑
【注意】
由于绝大部分的HTML文本都包含了换行符,所以尽量都需要加上re.S修饰符,以免出现匹配不到的问题。

9.3.3findall()

前面我们介绍了match()、search()方法,
match()从字符串的起始位置匹配正则表达式,如果匹配,就成功返回;如果不匹配,就返回None。
search()方法可以返回匹配正则表达式的第一个内容,如果还有匹配内容,不会返回。
如果想要获取匹配正则表达式的所有内容,那该怎么办呢?这时就要借助findall()方法了。该方法会搜索整个字符串,然后返回匹配正则表达式的所有内容。
还是上面的HTML文本,如果想获取所有a节点的超链接、歌手和歌名,就可以将search()方法换成findall()方法。如果有返回结果的话,就是列表类型,所以需要遍历一下来依次获取每组内容。代码如下:

运行结果
('/2.mp3', '任贤齐', '沧海一声笑')
/2.mp3 任贤齐沧海一声笑
('/3.mp3', '齐秦', '往事随风')
/3.mp3 齐秦往事随风
('/4.mp3', 'beyond', '光辉岁月')
/4.mp3 beyond 光辉岁月
('/5.mp3', '陈慧琳', '记事本')
/5.mp3 陈慧琳记事本
('/6.mp3', '邓丽君', '但愿人长久')
/6.mp3 邓丽君但愿人长久

这个结果不是很完美,其中还带有 * 的内容,我们有方法去除这些内容吗?有的。使用sub()函数就可简单实现。

9.3.4 sub()

除了使用正则表达式提取信息外,有时候还需要借助它来修改文本。比如,想要把一串文本中的所有数字都去掉,可以借助sub()方法。示例如下:

运行结果
aKyroiRixLg
如果我们要去除<i.*?>,可采用如下方法:

运行结果

正则表达式如果比较长,如果要多次引用,就比较繁琐,有更简洁的方法吗?我们可以采用compile()的方法,通过这个方法把正则表达式编译为一个正则表达式对象,以后引用这个对象即可,这样写起来就简洁多了,如代码:

运行结果
2018-09-15 2018-09-17 2018-09-22