Pytorch学习笔记(2)基础概念

1.1 PyTorch 简介与安装

PyTorch 的诞生

2017 年 1 月,FAIR(Facebook AI Research)发布了 PyTorch。PyTorch 是在 Torch 基础上用 python 语言重新打造的一款深度学习框架。Torch 是采用 Lua 语言为接口的机器学习框架,但是因为 Lua 语言较为小众,导致 Torch 学习成本高,因此知名度不高。

PyTorch 的发展

2017 年 1 月正式发布 PyTorch。

2018 年 4 月更新 0.4.0 版,支持 Windows 系统,caffe2 正式并入 PyTorch。

2018 年 11 月更新 1.0 稳定版,已成为 Github 上增长第二快的开源项目。

2019 年 5 月更新 1.1.0 版,支持 TensorBoard,增强可视化功能。

2019 年 8 月更新 1.2.0 版,更新 Torchvision,torchaudio 和torchtext,支持更多功能。

目前 PyTorch 有超越 Tensorflow 的趋势。

PyTorch 优点

上手快,掌握 Numpy 和基本深度学习概念即可上手。

代码简洁灵活,使用 nn.Module 封装使得网络搭建更加方便 。基于动态图机制,更加灵活。

资源多,arXiv 中新论文的算法大多有 PyTorch 实现。

开发者多,Github 上贡献者(Contributors)已经超过 1100+

PyTorch 实现模型训练的 5 大要素

数据:包括数据读取,数据清洗,进行数据划分和数据预处理,比如读取图片如何预处理及数据增强。

模型:包括构建模型模块,组织复杂网络,初始化网络参数,定义网络层。

损失函数:包括创建损失函数,设置损失函数超参数,根据不同任务选择合适的损失函数。

优化器:包括根据梯度使用某种优化器更新参数,管理模型参数,管理多个参数组实现不同学习率,调整学习率。

迭代训练:组织上面 4 个模块进行反复训练。包括观察训练效果,绘制 Loss/ Accuracy 曲线,用 TensorBoard 进行可视化分析。

整个系列的学习都会围绕着这 5 个方面来展开。

安装

在开发过程中可能会有多个项目同时进行,不同项目之间使用的 Python 版本和一些库的版本不一样,这就会导致冲突。因此这里使用 Anaconda 来管理多个 Python 虚拟环境。Anaconda 是为了方便使用 Python 而建立的一个软件包,包含常用 的 250 多个工具包,多个版本的 Python 解释器和强大的虚拟环境管理工具。各个环境之间相互独立,可任意切换。

安装 Anaconda

到官网 https://www.anaconda.com/products/individual 选择适合自己系统的 64 位安装包,注意选择 Python3 以上的版本。

安装时记得勾选Add Anaconda to my PATH environment variable 添加到环境变量中。

安装完成后打开cmd,输入conda回车出现如下信息,即为安装成功。

接着添加中科大镜像或者清华镜像,在安装库的时候实现加速下载。

安装 PyTorch

检查是否有支持 CUDA 的 GPU,若有,需要安装 CUDA 和CuDNN。

进入 PyTorch 官方网站 https://pytorch.org/get-started/locally/选择自己需要安装的 PyTorch 对应的命令,在本地 cmd 中输入安装命令即可。这里我本机选择使用 conda 安装不支持 GPU 的 1.5 版本:conda install pytorch torchvision cpuonly -c pytorch

如果 conda 或者 pip 安装很慢,也可以直接进入 https://download.pytorch.org/whl/torch_stable.html 下载 whl 包到本地安装。该页面包含了所有历史版本和各个平台的 PyTorch,需要根据文件名选择自己需要的版本进行下载。文件命名规则如下所示:

第一部分是 cuda 版本或者 cpu,第二部分是 PyTorch 版本号,第三部分是 Python 版本号,第四部分是操作系统。

cu92/torch-1.5.0%2Bcu92-cp37-cp37m-linux_x86_64.whl

Pycharm 使用 Anaconda 环境

在 Pycharm 中新建项目后,需要在 File -> Settings -> Project -> Python Interpreter 中选择 Anaconda 环境。

首先点击齿轮图标,点击弹出的add

接着在弹出的窗口中选择Conda EnvironmentConda execute 选择你安装好的 Anaconda 的文件夹下的Scripts\conda.exe

最后在Python Interpreter给当前项目选择刚刚创建的 Python 环境即可。

如果安装的是 GPU 版本,安装完成之后可以使用print(torch.cuda.is_available())语句来查看安装好的 PyTorch 是否支持 GPU。这里我是用的是 CPU 版本。

1.2 Tensor(张量)介绍

本章代码:

https://github.com/zhangxiann/PyTorch_Practice/blob/master/lesson1/tensor_introduce1.py

https://github.com/zhangxiann/PyTorch_Practice/blob/master/lesson1/tensor_introduce1.py

Tensor 的概念

Tensor 中文为张量。张量的意思是一个多维数组,它是标量、向量、矩阵的高维扩展。

标量可以称为 0 维张量,向量可以称为 1 维张量,矩阵可以称为 2 维张量,RGB 图像可以表示 3 维张量。你可以把张量看作多维数组。

img

Tensor 与 Variable

在 PyTorch 0.4.0 之前,torch.autograd 包中存在 Variable 这种数据类型,主要是用于封装 Tensor,进行自动求导。Variable 主要包含下面几种属性。

data: 被包装的 Tensor。

grad: data 的梯度。

grad_fn: 创建 Tensor 所使用的 Function,是自动求导的关键,因为根据所记录的函数才能计算出导数。

requires_grad: 指示是否需要梯度,并不是所有的张量都需要计算梯度。

is_leaf: 指示是否叶子节点(张量),叶子节点的概念在计算图中会用到,后面详细介绍。

在 PyTorch 0.4.0 之后,Variable 并入了 Tensor。在之后版本的 Tensor 中,除了具有上面 Variable 的 5 个属性,还有另外 3 个属性。

dtype: 张量的数据类型,如 torch.FloatTensor,torch.cuda.FloatTensor。

shape: 张量的形状。如 (64, 3, 224, 224)

device: 张量所在设备 (CPU/GPU),GPU 是加速计算的关键

关于 dtype,PyTorch 提供了 9 种数据类型,共分为 3 大类:float (16-bit, 32-bit, 64-bit)、integer (unsigned-8-bit ,8-bit, 16-bit, 32-bit, 64-bit)、Boolean。模型参数和数据用的最多的类型是 float-32-bit。label 常用的类型是 integer-64-bit。

Tensor 创建的方法

直接创建 Tensor

torch.tensor()

torch.tensor(data, dtype=None, device=None, requires_grad=False, pin_memory=False)

data: 数据,可以是 list,numpy

dtype: 数据类型,默认与 data 的一致

device: 所在设备,cuda/cpu

requires_grad: 是否需要梯度

pin_memory: 是否存于锁页内存

代码示例:

arr = np.ones((3, 3))

print(“ndarray的数据类型:”, arr.dtype)

# 创建存放在 GPU 的数据

# t = torch.tensor(arr, device=‘cuda’)

t= torch.tensor(arr)

print(t)

输出为:

ndarray的数据类型: float64

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

​ [1., 1., 1.],

​ [1., 1., 1.]], dtype=torch.float64)

torch.from_numpy(ndarray)

从 numpy 创建 tensor。利用这个方法创建的 tensor 和原来的 ndarray 共享内存,当修改其中一个数据,另外一个也会被改动。

代码示例:

arr = np.array([[1, 2, 3], [4, 5, 6]])

t = torch.from_numpy(arr)

# 修改 array,tensor 也会被修改

# print(“\n修改arr”)

# arr[0, 0] = 0

# print("numpy array: ", arr)

# print("tensor : ", t)

# 修改 tensor,array 也会被修改

print(“\n修改tensor”)

t[0, 0] = -1

print("numpy array: ", arr)

print("tensor : ", t)

输出为:

修改tensor

numpy array: [[-1 2 3]

[ 4 5 6]]

tensor : tensor([[-1, 2, 3],

​ [ 4, 5, 6]], dtype=torch.int32)

根据数值创建 Tensor

torch.zeros()

torch.zeros(*size, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)

功能:根据 size 创建全 0 张量

size: 张量的形状

out: 输出的张量,如果指定了 out,那么torch.zeros()返回的张量和 out 指向的是同一个地址

layout: 内存中布局形式,有 strided,sparse_coo 等。当是稀疏矩阵时,设置为 sparse_coo 可以减少内存占用。

device: 所在设备,cuda/cpu

requires_grad: 是否需要梯度

代码示例:

out_t = torch.tensor([1])

# 这里制定了 out

t = torch.zeros((3, 3), out=out_t)

print(t, ‘\n’, out_t)

# id 是取内存地址。最终 t 和 out_t 是同一个内存地址

print(id(t), id(out_t), id(t) == id(out_t))

输出是:

tensor([[0, 0, 0],

​ [0, 0, 0],

​ [0, 0, 0]])

tensor([[0, 0, 0],

​ [0, 0, 0],

​ [0, 0, 0]])

2984903203072 2984903203072 True

torch.zeros_like

torch.zeros_like(input, dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format)

功能:根据 input 形状创建全 0 张量

input: 创建与 input 同形状的全 0 张量

dtype: 数据类型

layout: 内存中布局形式,有 strided,sparse_coo 等。当是稀疏矩阵时,设置为 sparse_coo 可以减少内存占用。

同理还有全 1 张量的创建方法:torch.ones()torch.ones_like()

torch.full(),torch.full_like()

torch.full(size, fill_value, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)

功能:创建自定义数值的张量

size: 张量的形状,如 (3,3)

fill_value: 张量中每一个元素的值

代码示例:

t = torch.full((3, 3), 1)

print(t)

输出为:

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

​ [1., 1., 1.],

​ [1., 1., 1.]])

torch.arange()

torch.arange(start=0, end, step=1, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)

功能:创建等差的 1 维张量。注意区间为[start, end)。

start: 数列起始值

end: 数列结束值,开区间,取不到结束值

step: 数列公差,默认为 1

代码示例:

t = torch.arange(2, 10, 2)

print(t)

输出为:

tensor([2, 4, 6, 8])

torch.linspace()

torch.linspace(start, end, steps=100, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)

功能:创建均分的 1 维张量。数值区间为 [start, end]

start: 数列起始值

end: 数列结束值

steps: 数列长度 (元素个数)

代码示例:

# t = torch.linspace(2, 10, 5)

t = torch.linspace(2, 10, 6)

print(t)

输出为:

tensor([ 2.0000, 3.6000, 5.2000, 6.8000, 8.4000, 10.0000])

torch.logspace()

torch.logspace(start, end, steps=100, base=10.0, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)

功能:创建对数均分的 1 维张量。数值区间为 [start, end],底为 base。

start: 数列起始值

end: 数列结束值

steps: 数列长度 (元素个数)

base: 对数函数的底,默认为 10

代码示例:

# t = torch.logspace(2, 10, 5)

t = torch.logspace(2, 10, 6)

print(t)

输出为:

tensor([ 2.0000, 3.6000, 5.2000, 6.8000, 8.4000, 10.0000])

torch.eye()

torch.eye(n, m=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)

功能:创建单位对角矩阵( 2 维张量),默认为方阵

n: 矩阵行数。通常只设置 n,为方阵。

m: 矩阵列数

根据概率创建 Tensor

torch.normal()

torch.normal(mean, std, *, generator=None, out=None)

功能:生成正态分布 (高斯分布)

mean: 均值

std: 标准差

有 4 种模式:

    mean 为标量,std 为标量。这时需要设置 size。

    代码示例:

    # mean:标量 std: 标量

    # 这里需要设置 size

    t_normal = torch.normal(0., 1., size=(4,))

    print(t_normal)

    输出为:

    tensor([0.6614, 0.2669, 0.0617, 0.6213])

    mean 为标量,std 为张量

    mean 为张量,std 为标量

    代码示例:

    # mean:张量 std: 标量

    mean = torch.arange(1, 5, dtype=torch.float)

    std = 1

    t_normal = torch.normal(mean, std)

    print(“mean:{}\nstd:{}”.format(mean, std))

    print(t_normal)

    输出为:

    mean:tensor([1., 2., 3., 4.])

    std:1

    tensor([1.6614, 2.2669, 3.0617, 4.6213])

    这 4 个数采样分布的均值不同,但是方差都是 1。

    mean 为张量,std 为张量

    代码示例:

    # mean:张量 std: 张量

    mean = torch.arange(1, 5, dtype=torch.float)

    std = torch.arange(1, 5, dtype=torch.float)

    t_normal = torch.normal(mean, std)

    print(“mean:{}\nstd:{}”.format(mean, std))

    print(t_normal)

    输出为:

    mean:tensor([1., 2., 3., 4.])

    std:tensor([1., 2., 3., 4.])

    tensor([1.6614, 2.5338, 3.1850, 6.4853])

    其中 1.6614 是从正态分布 N ( 1 , 1 ) N(1,1) N(1,1) 中采样得到的,其他数字以此类推。

torch.randn() 和 torch.randn_like()

torch.randn(*size, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)

功能:生成标准正态分布。

size: 张量的形状

torch.rand() 和 torch.rand_like()

torch.rand(*size, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)

功能:在区间 [0, 1) 上生成均匀分布。

torch.randint() 和 torch.randint_like()

randint(low=0, high, size, *, generator=None, out=None,

dtype=None, layout=torch.strided, device=None, requires_grad=False)

功能:在区间 [low, high) 上生成整数均匀分布。

size: 张量的形状

torch.randperm()

torch.randperm(n, out=None, dtype=torch.int64, layout=torch.strided, device=None, requires_grad=False)

功能:生成从 0 到 n-1 的随机排列。常用于生成索引。

n: 张量的长度

torch.bernoulli()

torch.bernoulli(input, *, generator=None, out=None)

功能:以 input 为概率,生成伯努利分布 (0-1 分布,两点分布)

input: 概率值

1.3 张量操作与线性回归

本章代码:https://github.com/zhangxiann/PyTorch_Practice/blob/master/lesson1/linear_regression.py

张量的操作

拼接
torch.cat()

torch.cat(tensors, dim=0, out=None)

功能:将张量按照 dim 维度进行拼接

tensors: 张量序列

dim: 要拼接的维度

代码示例:

t = torch.ones((2, 3))

t_0 = torch.cat([t, t], dim=0)

t_1 = torch.cat([t, t], dim=1)

print(“t_0:{} shape:{}\nt_1:{} shape:{}”.format(t_0, t_0.shape, t_1, t_1.shape))

输出是:

t_0:tensor([[1., 1., 1.],

​ [1., 1., 1.],

​ [1., 1., 1.],

​ [1., 1., 1.]]) shape:torch.Size([4, 3])

t_1:tensor([[1., 1., 1., 1., 1., 1.],

​ [1., 1., 1., 1., 1., 1.]]) shape:torch.Size([2, 6])

torch.stack()

torch.stack(tensors, dim=0, out=None)

功能:将张量在新创建的 dim 维度上进行拼接

tensors: 张量序列

dim: 要拼接的维度

代码示例:

t = torch.ones((2, 3))

# dim =2

t_stack = torch.stack([t, t, t], dim=2)

print(“\nt_stack.shape:{}”.format(t_stack.shape))

# dim =0

t_stack = torch.stack([t, t, t], dim=0)

print(“\nt_stack.shape:{}”.format(t_stack.shape))

输出为:

t_stack.shape:torch.Size([2, 3, 3])

t_stack.shape:torch.Size([3, 2, 3])

第一次指定拼接的维度 dim =2,结果的维度是 [2, 3, 3]。后面指定拼接的维度 dim =0,由于原来的 tensor 已经有了维度 0,因此会把tensor 往后移动一个维度变为 [1,2,3],再拼接变为 [3,2,3]。

切分
torch.chunk()

torch.chunk(input, chunks, dim=0)

功能:将张量按照维度 dim 进行平均切分。若不能整除,则最后一份张量小于其他张量。

input: 要切分的张量

chunks: 要切分的份数

dim: 要切分的维度

代码示例:

a = torch.ones((2, 7)) # 7

list_of_tensors = torch.chunk(a, dim=1, chunks=3) # 3

for idx, t in enumerate(list_of_tensors):

print(“第{}个张量:{}, shape is {}”.format(idx+1, t, t.shape))

输出为:

第1个张量:tensor([[1., 1., 1.],

​ [1., 1., 1.]]), shape is torch.Size([2, 3])

第2个张量:tensor([[1., 1., 1.],

​ [1., 1., 1.]]), shape is torch.Size([2, 3])

第3个张量:tensor([[1.],

​ [1.]]), shape is torch.Size([2, 1])

由于 7 不能整除 3,7/3 再向上取整是 3,因此前两个维度是 [2, 3],所以最后一个切分的张量维度是 [2,1]。

torch.split()

torch.split(tensor, split_size_or_sections, dim=0)

功能:将张量按照维度 dim 进行平均切分。可以指定每一个分量的切分长度。

tensor: 要切分的张量

split_size_or_sections: 为 int 时,表示每一份的长度,如果不能被整除,则最后一份张量小于其他张量;为 list 时,按照 list 元素作为每一个分量的长度切分。如果 list 元素之和不等于切分维度 (dim) 的值,就会报错。

dim: 要切分的维度

代码示例:

t = torch.ones((2, 5))

list_of_tensors = torch.split(t, [2, 1, 2], dim=1)

for idx, t in enumerate(list_of_tensors):

print(“第{}个张量:{}, shape is {}”.format(idx+1, t, t.shape))

结果为:

第1个张量:tensor([[1., 1.],

​ [1., 1.]]), shape is torch.Size([2, 2])

第2个张量:tensor([[1.],

​ [1.]]), shape is torch.Size([2, 1])

第3个张量:tensor([[1., 1.],

​ [1., 1.]]), shape is torch.Size([2, 2])

索引
torch.index_select()

torch.index_select(input, dim, index, out=None)

功能:在维度 dim 上,按照 index 索引取出数据拼接为张量返回。

input: 要索引的张量

dim: 要索引的维度

index: 要索引数据的序号

代码示例:

# 创建均匀分布

t = torch.randint(0, 9, size=(3, 3))

# 注意 idx 的 dtype 不能指定为 torch.float

idx = torch.tensor([0, 2], dtype=torch.long)

# 取出第 0 行和第 2 行

t_select = torch.index_select(t, dim=0, index=idx)

print(“t:\n{}\nt_select:\n{}”.format(t, t_select))

输出为:

t:

tensor([[4, 5, 0],

​ [5, 7, 1],

​ [2, 5, 8]])

t_select:

tensor([[4, 5, 0],

​ [2, 5, 8]])

torch.mask_select()

torch.masked_select(input, mask, out=None)

功能:按照 mask 中的 True 进行索引拼接得到一维张量返回。

要索引的张量

mask: 与 input 同形状的布尔类型张量

代码示例:

t = torch.randint(0, 9, size=(3, 3))

mask = t.le(5) # ge is mean greater than or equal/ gt: greater than le lt

# 取出大于 5 的数

t_select = torch.masked_select(t, mask)

print("t:\n{}\nmask:\n{}\nt_select:\n{} ".format(t, mask, t_select))

结果为:

t:

tensor([[4, 5, 0],

​ [5, 7, 1],

​ [2, 5, 8]])

mask:

tensor([[ True, True, True],

​ [ True, False, True],

​ [ True, True, False]])

t_select:

tensor([4, 5, 0, 5, 1, 2, 5])

最后返回的是一维张量。

变换
torch.reshape()

torch.reshape(input, shape)

功能:变换张量的形状。当张量在内存中是连续时,返回的张量和原来的张量共享数据内存,改变一个变量时,另一个变量也会被改变。

input: 要变换的张量

shape: 新张量的形状

代码示例:

# 生成 0 到 8 的随机排列

t = torch.randperm(8)

# -1 表示这个维度是根据其他维度计算得出的

t_reshape = torch.reshape(t, (-1, 2, 2))

print(“t:{}\nt_reshape:\n{}”.format(t, t_reshape))

结果为:

t:tensor([5, 4, 2, 6, 7, 3, 1, 0])

t_reshape:

tensor([[[5, 4],

​ [2, 6]],

​ [[7, 3],

​ [1, 0]]])

在上面代码的基础上,修改原来的张量的一个元素,新张量也会被改变。

代码示例:

# 修改张量 t 的第 0 个元素,张量 t_reshape 也会被改变

t[0] = 1024

print(“t:{}\nt_reshape:\n{}”.format(t, t_reshape))

print(“t.data 内存地址:{}”.format(id(t.data)))

print(“t_reshape.data 内存地址:{}”.format(id(t_reshape.data)))

结果为:

t:tensor([1024, 4, 2, 6, 7, 3, 1, 0])

t_reshape:

tensor([[[1024, 4],

​ [ 2, 6]],

​ [[ 7, 3],

​ [ 1, 0]]])

t.data 内存地址:2636803119936

t_reshape.data 内存地址:2636803119792

torch.transpose()

torch.transpose(input, dim0, dim1)

功能:交换张量的两个维度。常用于图像的变换,比如把c*h*w变换为h*w*c

input: 要交换的变量

dim0: 要交换的第一个维度

dim1: 要交换的第二个维度

代码示例:

#把 c * h * w 变换为 h * w * c

t = torch.rand((2, 3, 4))

t_transpose = torch.transpose(t, dim0=1, dim1=2) # chw hwc

print(“t shape:{}\nt_transpose shape: {}”.format(t.shape, t_transpose.shape))

结果为:

t shape:torch.Size([2, 3, 4])

t_transpose shape: torch.Size([2, 4, 3])

torch.t()

功能:2 维张量转置,对于 2 维矩阵而言,等价于torch.transpose(input, 0, 1)

torch.squeeze()

torch.squeeze(input, dim=None, out=None)

功能:压缩长度为 1 的维度。

dim: 若为 None,则移除所有长度为 1 的维度;若指定维度,则当且仅当该维度长度为 1 时可以移除。

代码示例:

​ # 维度 0 和 3 的长度是 1

​ t = torch.rand((1, 2, 3, 1))

​ # 可以移除维度 0 和 3

​ t_sq = torch.squeeze(t)

​ # 可以移除维度 0

​ t_0 = torch.squeeze(t, dim=0)

​ # 不能移除 1

​ t_1 = torch.squeeze(t, dim=1)

​ print(“t.shape: {}”.format(t.shape))

​ print(“t_sq.shape: {}”.format(t_sq.shape))

​ print(“t_0.shape: {}”.format(t_0.shape))

​ print(“t_1.shape: {}”.format(t_1.shape))

结果为:

t.shape: torch.Size([1, 2, 3, 1])

t_sq.shape: torch.Size([2, 3])

t_0.shape: torch.Size([2, 3, 1])

t_1.shape: torch.Size([1, 2, 3, 1])

torch.unsqueeze()

torch.unsqueeze(input, dim)

功能:根据 dim 扩展维度,长度为 1。

张量的数学运算

主要分为 3 类:加减乘除,对数,指数,幂函数 和三角函数。

这里介绍一下常用的几种方法。

torch.add()

torch.add(input, other, out=None)

torch.add(input, other, *, alpha=1, out=None)

功能:逐元素计算 input + alpha * other。因为在深度学习中经常用到先乘后加的操作。

input: 第一个张量

alpha: 乘项因子

other: 第二个张量

torch.addcdiv()

torch.addcdiv(input, tensor1, tensor2, *, value=1, out=None)

计算公式为:out ∗ i = input ⁡ ∗ i + *{i}=\operatorname{input}*{i}+ i=inputi+ value ×  tensor  1 ∗ i  tensor  2 ∗ i \times \frac{\text { tensor } 1*{i}}{\text { tensor } 2*{i}} × tensor 2i tensor 1i

torch.addcmul()

torch.addcmul(input, tensor1, tensor2, *, value=1, out=None)

计算公式为:out ∗ i = *{i}= i= input ∗ i + *{i}+ i+ value × \times × tensor 1 ∗ i × 1*{i} \times 1i× tensor 2 ∗ i 2*{i} 2i

线性回归

线性回归是分析一个变量 ( y y y) 与另外一 (多) 个变量 ( x x x) 之间的关系的方法。一般可以写成 y = w x + b y=wx+b y=wx+b。线性回归的目的就是求解参数 w , b w, b w,b

线性回归的求解可以分为 3 步:

    确定模型: y = w x + b y=wx+b y=wx+b

    选择损失函数,一般使用均方误差 MSE: 1 m ∑ ∗ i = 1 m ( y ∗ i − y ^ ∗ i ) 2 \frac{1}{m} \sum*{i=1}^{m}\left(y*{i}-\hat{y}*{i}\right)^{2} m1i=1m(yiy^i)2。其中 $ \hat{y}*{i} $ 是预测值, y y y 是真实值。

    使用梯度下降法求解梯度 (其中 l r lr lr 是学习率),并更新参数:

    w = w − l r ∗ w . g r a d w = w - lr * w.grad w=wlrw.grad

    b = b − l r ∗ b . g r a d b = b - lr * b.grad b=blrb.grad

代码如下:

import torch

import matplotlib.pyplot as plt

torch.manual_seed(10)

lr = 0.05 # 学习率

# 创建训练数据

x = torch.rand(20, 1) * 10 # x data (tensor), shape=(20, 1)

# torch.randn(20, 1) 用于添加噪声

y = 2*x + (5 + torch.randn(20, 1)) # y data (tensor), shape=(20, 1)

# 构建线性回归参数

w = torch.randn((1), requires_grad=True) # 设置梯度求解为 true

b = torch.zeros((1), requires_grad=True) # 设置梯度求解为 true

# 迭代训练 1000 次

for iteration in range(1000):

​ # 前向传播,计算预测值

​ wx = torch.mul(w, x)

​ y_pred = torch.add(wx, b)

​ # 计算 MSE loss

​ loss = (0.5 * (y - y_pred) ** 2).mean()

​ # 反向传播

​ loss.backward()

​ # 更新参数

​ b.data.sub_(lr * b.grad)

​ w.data.sub_(lr * w.grad)

​ # 每次更新参数之后,都要清零张量的梯度

​ w.grad.zero_()

​ b.grad.zero_()

​ # 绘图,每隔 20 次重新绘制直线

​ if iteration % 20 == 0:

​ plt.scatter(x.data.numpy(), y.data.numpy())

​ plt.plot(x.data.numpy(), y_pred.data.numpy(), ‘r-’, lw=5)

​ plt.text(2, 20, ‘Loss=%.4f’ % loss.data.numpy(), fontdict={‘size’: 20, ‘color’: ‘red’})

​ plt.xlim(1.5, 10)

​ plt.ylim(8, 28)

​ plt.title(“Iteration: {}\nw: {} b: {}”.format(iteration, w.data.numpy(), b.data.numpy()))

​ plt.pause(0.5)

​ # 如果 MSE 小于 1,则停止训练

​ if loss.data.numpy() < 1:

​ break

训练的直线的可视化如下:

在 80 次的时候,Loss 已经小于 1 了,因此停止了训练。

1.4 计算图与动态图机制

本章代码:https://github.com/zhangxiann/PyTorch_Practice/blob/master/lesson1/computational_graph.py

计算图

深度学习就是对张量进行一系列的操作,随着操作种类和数量的增多,会出现各种值得思考的问题。比如多个操作之间是否可以并行,如何协同底层的不同设备,如何避免冗余的操作,以实现最高效的计算效率,同时避免一些 bug。因此产生了计算图 (Computational Graph)。

计算图是用来描述运算的有向无环图,有两个主要元素:节点 (Node) 和边 (Edge)。节点表示数据,如向量、矩阵、张量。边表示运算,如加减乘除卷积等。

用计算图表示: y = ( x + w ) ∗ ( w + 1 ) y=(x+w)*(w+1) y=(x+w)(w+1),如下所示:

可以看作, y = a × b y=a \times b y=a×b ,其中 a = x + w a=x+w a=x+w b = w + 1 b=w+1 b=w+1

计算图与梯度求导

这里求 y y y w w w 的导数。根复合函数的求导法则,可以得到如下过程。

∂ y ∂ w = ∂ y ∂ a ∂ a ∂ w + ∂ y ∂ b ∂ b ∂ w   = b ∗ 1 + a ∗ 1   = b + a   = ( w + 1 ) + ( x + w )   = 2 ∗ w + x + 1   = 2 ∗ 1 + 2 + 1 = 5 \begin{aligned} \frac{\partial y}{\partial w} &=\frac{\partial y}{\partial a} \frac{\partial a}{\partial w}+\frac{\partial y}{\partial b} \frac{\partial b}{\partial w} \ &=b *1+a* 1 \ &=b+a \ &=(w+1)+(x+w) \ &=2 *w+x+1 \ &=2* 1+2+1=5\end{aligned} wy=aywa+bywb =b1+a1 =b+a =(w+1)+(x+w) =2w+x+1 =21+2+1=5

体现到计算图中,就是根节点 y y y 到叶子节点 w w w 有两条路径 y -> a -> wy ->b -> w。根节点依次对每条路径的孩子节点求导,一直到叶子节点w,最后把每条路径的导数相加即可。

代码如下:

import torch

w = torch.tensor([1.], requires_grad=True)

x = torch.tensor([2.], requires_grad=True)

# y=(x+w)*(w+1)

a = torch.add(w, x) # retain_grad()

b = torch.add(w, 1)

y = torch.mul(a, b)

# y 求导

y.backward()

# 打印 w 的梯度,就是 y 对 w 的导数

print(w.grad)

结果为tensor([5.])

我们回顾前面说过的 Tensor 中有一个属性is_leaf标记是否为叶子节点。

在上面的例子中, x x x w w w 是叶子节点,其他所有节点都依赖于叶子节点。叶子节点的概念主要是为了节省内存,在计算图中的一轮反向传播结束之后,非叶子节点的梯度是会被释放的。

代码示例:

# 查看叶子结点

print(“is_leaf:\n”, w.is_leaf, x.is_leaf, a.is_leaf, b.is_leaf, y.is_leaf)

# 查看梯度

print(“gradient:\n”, w.grad, x.grad, a.grad, b.grad, y.grad)

结果为:

is_leaf:

True True False False False

gradient:

tensor([5.]) tensor([2.]) None None None

非叶子节点的梯度为空,如果在反向传播结束之后仍然需要保留非叶子节点的梯度,可以对节点使用retain_grad()方法。

而 Tensor 中的 grad_fn 属性记录的是创建该张量时所用的方法 (函数)。而在反向传播求导梯度时需要用到该属性。

示例代码:

# 查看梯度

print("w.grad_fn = ", w.grad_fn)

print("x.grad_fn = ", x.grad_fn)

print("a.grad_fn = ", a.grad_fn)

print("b.grad_fn = ", b.grad_fn)

print("y.grad_fn = ", y.grad_fn)

结果为

w.grad_fn = None

x.grad_fn = None

a.grad_fn = <AddBackward0 object at 0x000001D8DDD20588>

b.grad_fn = <AddBackward0 object at 0x000001D8DDD20588>

y.grad_fn = <MulBackward0 object at 0x000001D8DDD20588>

PyTorch 的动态图机制

PyTorch 采用的是动态图机制 (Dynamic Computational Graph),而 Tensorflow 采用的是静态图机制 (Static Computational Graph)。

动态图是运算和搭建同时进行,也就是可以先计算前面的节点的值,再根据这些值搭建后面的计算图。优点是灵活,易调节,易调试。PyTorch 里的很多写法跟其他 Python 库的代码的使用方法是完全一致的,没有任何额外的学习成本。

静态图是先搭建图,然后再输入数据进行运算。优点是高效,因为静态计算是通过先定义后运行的方式,之后再次运行的时候就不再需要重新构建计算图,所以速度会比动态图更快。但是不灵活。TensorFlow 每次运行的时候图都是一样的,是不能够改变的,所以不能直接使用 Python 的 while 循环语句,需要使用辅助函数 tf.while_loop 写成 TensorFlow 内部的形式。

1.5 autograd 与逻辑回归

本章代码:

https://github.com/zhangxiann/PyTorch_Practice/blob/master/lesson1/autograd.py

https://github.com/zhangxiann/PyTorch_Practice/blob/master/lesson1/logistic-regression.py

自动求导 (autograd)

在深度学习中,权值的更新是依赖于梯度的计算,因此梯度的计算是至关重要的。在 PyTorch 中,只需要搭建好前向计算图,然后利用torch.autograd自动求导得到所有张量的梯度。

torch.autograd.backward()

torch.autograd.backward(tensors, grad_tensors=None, retain_graph=None, create_graph=False, grad_variables=None)

功能:自动求取梯度

tensors: 用于求导的张量,如 loss

retain_graph: 保存计算图。PyTorch 采用动态图机制,默认每次反向传播之后都会释放计算图。这里设置为 True 可以不释放计算图。

create_graph: 创建导数计算图,用于高阶求导

grad_tensors: 多梯度权重。当有多个 loss 混合需要计算梯度时,设置每个 loss 的权重。

retain_graph 参数

代码示例

w = torch.tensor([1.], requires_grad=True)

x = torch.tensor([2.], requires_grad=True)

# y=(x+w)*(w+1)

a = torch.add(w, x)

b = torch.add(w, 1)

y = torch.mul(a, b)

# 第一次执行梯度求导

y.backward()

print(w.grad)

# 第二次执行梯度求导,出错

y.backward()

其中y.backward()方法调用的是torch.autograd.backward(self, gradient, retain_graph, create_graph)。但是在第二次执行y.backward()时会出错。因为 PyTorch 默认是每次求取梯度之后不保存计算图的,因此第二次求导梯度时,计算图已经不存在了。在第一次求梯度时使用y.backward(retain_graph=True)即可。如下代码所示:

​ w = torch.tensor([1.], requires_grad=True)

​ x = torch.tensor([2.], requires_grad=True)

​ # y=(x+w)*(w+1)

​ a = torch.add(w, x)

​ b = torch.add(w, 1)

​ y = torch.mul(a, b)

​ # 第一次求导,设置 retain_graph=True,保留计算图

​ y.backward(retain_graph=True)

​ print(w.grad)

​ # 第二次求导成功

​ y.backward()

grad_tensors 参数

代码示例:

w = torch.tensor([1.], requires_grad=True)

x = torch.tensor([2.], requires_grad=True)

a = torch.add(w, x)

b = torch.add(w, 1)

y0 = torch.mul(a, b) # y0 = (x+w) * (w+1)

y1 = torch.add(a, b) # y1 = (x+w) + (w+1) dy1/dw = 2

# 把两个 loss 拼接都到一起

loss = torch.cat([y0, y1], dim=0) # [y0, y1]

# 设置两个 loss 的权重: y0 的权重是 1,y1 的权重是 2

grad_tensors = torch.tensor([1., 2.])

loss.backward(gradient=grad_tensors) # gradient 传入 torch.autograd.backward()中的grad_tensors

# 最终的 w 的导数由两部分组成。∂y0/∂w * 1 + ∂y1/∂w * 2

print(w.grad)

结果为:

tensor([9.])

该 loss 由两部分组成: y ∗ 0 y*{0} y0 y ∗ 1 y*{1} y1。其中 ∂ y ∗ 0 ∂ w = 5 \frac{\partial y*{0}}{\partial w}=5 wy0=5 ∂ y ∗ 1 ∂ w = 2 \frac{\partial y*{1}}{\partial w}=2 wy1=2。而 grad*tensors 设置两个 loss 对 w 的权重分别为 1 和 2。因此最终 w 的梯度为: ∂ y ∗ 0 ∂ w × 1 + ∂ y 1 ∂ w × 2 = 9 \frac{\partial y*{0}}{\partial w} \times 1+ \frac{\partial y_{1}}{\partial w} \times 2=9 wy0×1+wy1×2=9

torch.autograd.grad()

torch.autograd.grad(outputs, inputs, grad_outputs=None, retain_graph=None, create_graph=False, only_inputs=True, allow_unused=False)

功能:求取梯度。

outputs: 用于求导的张量,如 loss

inputs: 需要梯度的张量

create_graph: 创建导数计算图,用于高阶求导

retain_graph:保存计算图

grad_outputs: 多梯度权重计算

torch.autograd.grad()的返回结果是一个 tunple,需要取出第 0 个元素才是真正的梯度。

下面使用torch.autograd.grad()求二阶导,在求一阶导时,需要设置 create_graph=True,让一阶导数 grad_1 也拥有计算图,然后再使用一阶导求取二阶导:

x = torch.tensor([3.], requires_grad=True)

y = torch.pow(x, 2) # y = x**2

# 如果需要求 2 阶导,需要设置 create_graph=True,让一阶导数 grad_1 也拥有计算图

grad_1 = torch.autograd.grad(y, x, create_graph=True) # grad_1 = dy/dx = 2x = 2 * 3 = 6

print(grad_1)

# 这里求 2 阶导

grad_2 = torch.autograd.grad(grad_1[0], x) # grad_2 = d(dy/dx)/dx = d(2x)/dx = 2

print(grad_2)

输出为:

(tensor([6.], grad_fn=),)

(tensor([2.]),)

需要注意的 3 个点:

在每次反向传播求导时,计算的梯度不会自动清零。如果进行多次迭代计算梯度而没有清零,那么梯度会在前一次的基础上叠加。

代码示例:

w = torch.tensor([1.], requires_grad=True)

x = torch.tensor([2.], requires_grad=True)

# 进行 4 次反向传播求导,每次最后都没有清零

for i in range(4):

​ a = torch.add(w, x)

​ b = torch.add(w, 1)

​ y = torch.mul(a, b)

​ y.backward()

​ print(w.grad)

结构如下:

tensor([5.])

tensor([10.])

tensor([15.])

tensor([20.])

每一次的梯度都比上一次的梯度多 5,这是由于梯度不会自动清零。使用w.grad.zero_()将梯度清零。

for i in range(4):

​ a = torch.add(w, x)

​ b = torch.add(w, 1)

​ y = torch.mul(a, b)

​ y.backward()

​ print(w.grad)

​ # 每次都把梯度清零

​ # w.grad.zero_()

依赖于叶子节点的节点,requires_grad 属性默认为 True。

叶子节点不可执行 inplace 操作。

以加法来说,inplace 操作有a += xa.add_(x),改变后的值和原来的值内存地址是同一个。非inplace 操作有a = a + xa.add(x),改变后的值和原来的值内存地址不是同一个。

代码示例:

print(“非 inplace 操作”)

a = torch.ones((1, ))

print(id(a), a)

# 非 inplace 操作,内存地址不一样

a = a + torch.ones((1, ))

print(id(a), a)

print(“inplace 操作”)

a = torch.ones((1, ))

print(id(a), a)

# inplace 操作,内存地址一样

a += torch.ones((1, ))

print(id(a), a)

结果为:

非 inplace 操作

2404827089512 tensor([1.])

2404893170712 tensor([2.])

inplace 操作

2404827089512 tensor([1.])

2404827089512 tensor([2.])

如果在反向传播之前 inplace 改变了叶子 的值,再执行 backward() 会报错

w = torch.tensor([1.], requires_grad=True)

x = torch.tensor([2.], requires_grad=True)

# y = (x + w) * (w + 1)

a = torch.add(w, x)

b = torch.add(w, 1)

y = torch.mul(a, b)

# 在反向传播之前 inplace 改变了 w 的值,再执行 backward() 会报错

w.add_(1)

y.backward()

这是因为在进行前向传播时,计算图中依赖于叶子节点的那些节点,会记录叶子节点的地址,在反向传播时就会利用叶子节点的地址所记录的值来计算梯度。比如在 y = a × b y=a \times b y=a×b ,其中 a = x + w a=x+w a=x+w b = w + 1 b=w+1 b=w+1 x x x w w w 是叶子节点。当求导 ∂ y ∂ a = b = w + 1 \frac{\partial y}{\partial a} = b = w+1 ay=b=w+1,需要用到叶子节点 w w w

逻辑回归 (Logistic Regression)

逻辑回归是线性的二分类模型。模型表达式 y = f ( z ) = 1 1 + e − z y=f(z)=\frac{1}{1+e^{-z}} y=f(z)=1+ez1,其中 z = W X + b z=WX+b z=WX+b f ( z ) f(z) f(z) 称为 sigmoid 函数,也被称为 Logistic 函数。函数曲线如下:(横坐标是 z z z,而 z = W X + b z=WX+b z=WX+b,纵坐标是 y y y)

分类原则如下:class KaTeX parse error: Expected '}', got '\right' at position 63: …eq y\end{array}\̲r̲i̲g̲h̲t̲.。当 y < 0.5 y<0.5 y<0.5 时,类别为 0;当 0.5 ≤ y 0.5 \leq y 0.5y 时,类别为 1。

其中 z = W X + b z=WX+b z=WX+b 就是原来的线性回归的模型。从横坐标来看,当 z < 0 z<0 z<0 时,类别为 0;当 0 ≤ z 0 \leq z 0z 时,类别为 1,直接使用线性回归也可以进行分类。逻辑回归是在线性回归的基础上加入了一个 sigmoid 函数,这是为了更好地描述置信度,把输入映射到 (0,1) 区间中,符合概率取值。

逻辑回归也被称为对数几率回归 ln ⁡ y 1 − y = W X + b \ln \frac{y}{1-y}=W X+b ln1yy=WX+b,几率的表达式为: y 1 − y \frac{y}{1-y} 1yy y y y 表示正类别的概率, 1 − y 1-y 1y 表示另一个类别的概率。根据对数几率回归可以推导出逻辑回归表达式:

ln ⁡ y 1 − y = W X + b \ln \frac{y}{1-y}=W X+b ln1yy=WX+b y 1 − y = e W X + b \frac{y}{1-y}=e^{W X+b} 1yy=eWX+b y = e W X + b − y ∗ e W X + b y=e^{W X+b}-y * e^{W X+b} y=eWX+byeWX+b y ( 1 + e W X + b ) = e W X + b y\left(1+e^{W X+b}\right)=e^{W X+b} y(1+eWX+b)=eWX+b y = e W X + b 1 + e W X + b = 1 1 + e − ( W X + b ) y=\frac{e^{W X+b}}{1+e^{W X+b}}=\frac{1}{1+e^{-(W X+b)}} y=1+eWX+beWX+b=1+e(WX+b)1

PyTorch 实现逻辑回归

PyTorch 构建模型需要 5 大步骤:

数据:包括数据读取,数据清洗,进行数据划分和数据预处理,比如读取图片如何预处理及数据增强。

模型:包括构建模型模块,组织复杂网络,初始化网络参数,定义网络层。

损失函数:包括创建损失函数,设置损失函数超参数,根据不同任务选择合适的损失函数。

优化器:包括根据梯度使用某种优化器更新参数,管理模型参数,管理多个参数组实现不同学习率,调整学习率。

迭代训练:组织上面 4 个模块进行反复训练。包括观察训练效果,绘制 Loss/ Accuracy 曲线,用 TensorBoard 进行可视化分析。

代码示例:

import torch

import torch.nn as nn

import matplotlib.pyplot as plt

import numpy as np

torch.manual_seed(10)

# ============================ step 1/5 生成数据 ============================

sample_nums = 100

mean_value = 1.7

bias = 1

n_data = torch.ones(sample_nums, 2)

# 使用正态分布随机生成样本,均值为张量,方差为标量

x0 = torch.normal(mean_value * n_data, 1) + bias # 类别0 数据 shape=(100, 2)

# 生成对应标签

y0 = torch.zeros(sample_nums) # 类别0 标签 shape=(100, 1)

# 使用正态分布随机生成样本,均值为张量,方差为标量

x1 = torch.normal(-mean_value * n_data, 1) + bias # 类别1 数据 shape=(100, 2)

# 生成对应标签

y1 = torch.ones(sample_nums) # 类别1 标签 shape=(100, 1)

train_x = torch.cat((x0, x1), 0)

train_y = torch.cat((y0, y1), 0)

# ============================ step 2/5 选择模型 ============================

class LR(nn.Module):

​ def init(self):

​ super(LR, self).init()

​ self.features = nn.Linear(2, 1)

​ self.sigmoid = nn.Sigmoid()

​ def forward(self, x):

​ x = self.features(x)

​ x = self.sigmoid(x)

​ return x

lr_net = LR() # 实例化逻辑回归模型

# ============================ step 3/5 选择损失函数 ============================

loss_fn = nn.BCELoss()

# ============================ step 4/5 选择优化器 ============================

lr = 0.01 # 学习率

optimizer = torch.optim.SGD(lr_net.parameters(), lr=lr, momentum=0.9)

# ============================ step 5/5 模型训练 ============================

for iteration in range(1000):

​ # 前向传播

​ y_pred = lr_net(train_x)

​ # 计算 loss

​ loss = loss_fn(y_pred.squeeze(), train_y)

​ # 反向传播

​ loss.backward()

​ # 更新参数

​ optimizer.step()

​ # 清空梯度

​ optimizer.zero_grad()

​ # 绘图

​ if iteration % 20 == 0:

​ mask = y_pred.ge(0.5).float().squeeze() # 以0.5为阈值进行分类

​ correct = (mask == train_y).sum() # 计算正确预测的样本个数

​ acc = correct.item() / train_y.size(0) # 计算分类准确率

​ plt.scatter(x0.data.numpy()[:, 0], x0.data.numpy()[:, 1], c=‘r’, label=‘class 0’)

​ plt.scatter(x1.data.numpy()[:, 0], x1.data.numpy()[:, 1], c=‘b’, label=‘class 1’)

​ w0, w1 = lr_net.features.weight[0]

​ w0, w1 = float(w0.item()), float(w1.item())

​ plot_b = float(lr_net.features.bias[0].item())

​ plot_x = np.arange(-6, 6, 0.1)

​ plot_y = (-w0 * plot_x - plot_b) / w1

​ plt.xlim(-5, 7)

​ plt.ylim(-7, 7)

​ plt.plot(plot_x, plot_y)

​ plt.text(-5, 5, ‘Loss=%.4f’ % loss.data.numpy(), fontdict={‘size’: 20, ‘color’: ‘red’})

​ plt.title(“Iteration: {}\nw0:{:.2f} w1:{:.2f} b: {:.2f} accuracy:{:.2%}”.format(iteration, w0, w1, plot_b, acc))

​ plt.legend()

​ # plt.savefig(str(iteration / 20)+“.png”)

​ plt.show()

​ plt.pause(0.5)

​ # 如果准确率大于 99%,则停止训练

​ if acc > 0.99:

​ break

训练的分类直线的可视化如下:

  • 17
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值