一.绪论
pytorch是用于python深度学习的神经网络包,我们会使用pytorch来编程神经网络
pytorch既是一个深度学习框架也是一个科学计算包,科学计算方面主要是pytorch张量库和相关张量库运算的结果
张量——是一个n维数组或一个n-D数组
对于python来说,最流行的科学计算包之一是Numpy(n维数组的转换包)
pytorch是一个张量库,紧密地反映了Numpy多维数组的功能,并且它与Numpy本身具有高度的互操作性
pytorch张量及其相关操作与Numpy中N维数组非常相似
pytorch包(主要使用到的):
1.torch 包含所有其他包和张量库(顶级包);
2.torch.nn 、 torch.autograd 主要的工作包。
前者代表神经网络,包含类和模块,比如图层、权重、前向函数。神经网络是建立在torch.nn的基础上的;
后者是一个子包,负责处理在核心处优化我们的神经网络权重所需的导数计算。
3.torch.nn.functional 、 torch.optim 典型的深度学习函数和优化算法。
前者是功能接口,它让我们能够访问像损失函数、激活函数、卷积运算。
后者使我们能够访问典型的优化(像SGD和Adam这样的算法)。
4.torch.utils 是一个子包,它包含像数据集和数据加载器这样的实用应用程序类,是数据预处理更加容易。
5.torchvision 是一个单独的包,它能让我们访问流行的数据集、计算机视觉的模型架构和图像转换。
<所有深度学习的框架都有2个特性:一个张量库和一个用于计算导数的包;对于pytorch来说,这两个一个是torch一个是torch.autograd>
二.张量
深度学习中使用张量时最关注的三个张量属性——秩、轴、形状;这些概念是相互建立的:秩->轴->形状,所有的这些都与索引(Indexes)的概念有本质上的联系
a.张量的秩:
张量的秩是指张量中存在的维数,一个张量的秩告诉我们一个张量有多少个轴(个人理解:写在书面上表现为访问到最后一个具体数据时有多少个列,arr[0][0][0]——>三阶)
假设我们有一个秩为2的张量,意味着——>我们有一个矩阵;我们有一个二维数组;我们有一个二维张量
一个张量的秩告诉我们需要多少个索引来访问或引用张量数据和结构中包含的特定数据元素
如果我们有一个二阶张量,我们需要两个指标来访问张量的单个元素 (arr[0][0])
b.张量的轴:
一个张量的轴时一个张量的一个特定的维数,对于张量,最后一个轴的元素总是数字
如果我们说一个张量是一个二阶张量,意为张量有两个维度,或者说,我们的意思是张量有两个轴;元素被认为是存在的或者沿着一个轴运动,运动会受到每个轴长度的限制,每个轴的长度告诉我们沿轴有多少个索引
假设我们有一个张量t,第一个轴长度是3->我们可以在第一个轴上标上3个位置,第二个轴长度是4->我们可以在第二个轴上标上4个位置
例:
存在一个张量:
dd=[
[1,2,3],
[4,5,6],
[7,8,9]
]
dd[0] -> 第一个轴上的元素 是数组 [1,2,3] dd[0][0] -> 第二个轴上的元素 是数字 1
//每一个轴都包含一个n维数组 最后一个轴的元素总是数字 ——————>拓展到n维张量
c.张量的形状:
张量的形状是由每个轴的长度决定的,如果我们知道一个给定张量的形状,那么我们就知道了每个轴的长度,这告诉我们每个轴上有多少个索引
张量的形状编码了所有关于张量、秩、轴和索引的相关信息
*把dd Python列表传递给torch.tensor函数来得到一个torch.tensor对象
t = torch.tensor(dd)
t -> ([[1,2,3],
[4,5,6],
[7,8,9]]) //python列表已经变成了一个torch张量
type(t) //查看其类型
-> torch.Tensor
t.shape //查看其形状
-> torch.Size([3, 3]) //这个张量的形状是3x3 ————> 这个秩为2的张量的每一个轴的长度是3,意味着每个轴上都有3个索引
//在pytorch中,一个张量的大小(size)和一个张量的形状(shape)是一样的
形状还对所有有关秩、轴、索引的相关信息进行编码,我们在编程时必须经常执行的一种操作在神经网络中被称为重塑
当我们的张量在我们的网络中流动时,在网络中不同的点会有特定的形状,我们的工作是理解即将到来的形状,并有能力根据需要进行重塑
*在pytorch中重塑张量:
假设我们想要把我们的张量t重塑成1x9 ——> 沿着第一个轴的数组和第二个轴上的9个数字
使用reshape函数,对张量t进行重塑,为每个轴指定我们想要的长度
t.reshape(1,9) //运行这个代码可以看到这个代码返回了一个新的形状
——> tensor([[1, 2, 3, 4, 5, 6, 7, 8, 9]]) //注意:在重塑时分量值的乘积必须等于张量中元素的总数,这使得张量数据结构中有足够的位置来包含重组重组后的所有原始元素
t.reshape(1,9).shape
——> torch.Size([1, 9]) //形状变成了1x9
重塑改变的是形状,而不是底层的数据元素
例:(演示张量概念的使用)
把图像输入看作是CNN的张量,CNN输入的形状通常长度为4 ——> [?, ?, ?, ?] ——> 我们有一个秩为4的张量,它有四个轴
[ ?, ?, ?, ? ]
A0 A1 A2 A3
如果我们沿A3轴运行,我们停下来检查那里的一个元素,我们会看到一个数字;如果我们沿着其他轴运行,这里的元素将是多维数组
对于图像,原始数据以像素的形式出现:这些像素由一个数字组成,并使用高度和宽度来表示两个维度,所以图像的高度(Height)和宽度(Width)在最后两个轴上表示
A1代表的是颜色通道(C):对于RGB(Red、Green、Blue)图像,其值为3;灰度图像(在黑色与白色之间还有许多级的颜色深度),其值为1
在访问数据方面,我们需要三个索引,选择A1、A2、A3来找到特定的像素值
A0代表一个批量大小,在神经网络中,我们通常使用批量的样本而不是单一样本,所以这个轴的长度告诉我们在我们的批当中由多少个样本,一整批图像都是用一个张量来表示的
给定一个张量,形状为[3, 1, 28, 28], 这给了我们一个四阶张量,它最终会经过我们的卷积神经网络,给定一个像这样的图像张量,我们可以使用四个索引在特定图像的特定颜色通道中导航到特定的像素
张量被卷积层转化后,颜色通道轴会变化
*假设我们有一个张量,它包含来自一个28x28灰度图像的数据,其形状为[1, 1, 28, 28],现将这个图形传递到我们的CNN并通过第一个卷积层,这时我们的张量和基础数据的形状将由卷积运算来改变
卷积改变了高度和宽度维度以及颜色通道的数量,通道的数量根据在层中使用的滤波器的数量而变化
假设我们有3个卷积滤波器(Filters),因此我们会从卷积层得到三个通道输出,这些通道是卷积层的输出,因此叫做输出通道;
这三个滤波器中的每一个都可以包含原始的单个输入通道,产生三个输出通道
输出通道仍然由像素组成,但是像素值已经被卷积运算修改了;
基于滤波器得大小,输出的高度和宽度也会变,日后详说;
通过输出通道,我们不再有彩色通道(可以看作是经过修改的颜色通道),我们称这些通道 ——> 特征图
所谓特征图是由输入颜色通道和卷积滤波器所产生卷积得结果,所以我们把一个输入颜色通道和一个卷积滤波器结合起来,再对其进行卷积运算,我们得到一个输出通道的结果,我们称之为 ——> 特征映射
它输出了代表图像的特定特征(我的理解是,不同滤波器专注的特征不同,因此会得到不同的结果),比如边缘;当网络在训练过程中学习时,这些映射就会出现
三.通过探索pytorch张量来探索pytorch本身
pytorch张量是在pytorch中编程神经网络时使用的数据结构,必须编写的第一行代码通常是数据预处理程序,数据预处理的最终目标是将我们正在处理的任何数据转换成能够感知我们神经网络的张量
创建一个torch张量:
import torch
import numpy as np
t = torch.Tensor()
# 我们有数据类型、一个设备和一个布局
print(t.dtype) # torch.float32 数据类型 指定在张量中包含的数据类型
print(t.device) # cpu
print(t.layout) # torch.strided
*张量包含下列类型之一的统一数值数据:torch.float32、torch.float64、torch.float16、torch.uint8、torch.int8、torch.int16、torch.int32、torch.int64
每种数据类型都有一个CPU和一个GPU版本,张量之间进行的张量计算必须是相同数据类型的张量
*可以看到张量指定了一个设备,它要么是CPU要么是GPU,这是数据被分配的位置,这决定了给定张量的张量计算的位置,pytorch支持多种设备的使用,我们可以用索引来指定
device = torch.device('cuda: 0') 我们可以用torch.device来创建一个设备,然后指定我们的第一个GPU,我们就可以得到这个设备的输出
输出:device(type = 'cuda', index = 0) 我们可以看到它的类型是cuda,意味这个设备是个GPU,0的索引告诉我们它是我们拥有的第一个GPU
使用多设备时,张量之间进行张量操作必须是存在于同一设备上的张量发生的
*我们的布局是strided,strided告诉我们张量数据是如何在内存中布局的,这是默认值,一般无需修改
*用数据创建pytorch张量(将数据转换成pytorch张量)的一些常用方法:
先用numpy数组(array)来创建一些数据
data = np.array([1, 2, 3])
type(data) # numpy.ndarray
现在我们有一个numpy数组,我们来创建张量:
在pytorch中创建张量对象主要有4种方法(他们都接受像numpy数组这样的数据结构,并给我们一个pytoch张量类的实例):
·1·torch.Tensor 类构造函数,在构造一个张量时使用全局缺省值
torch.get_default_dtype() # torch.float32 --> 默认的dtype
·2·torch.tensor 为我们构建张量对象的工厂函数(接收参数输入并返回特定类型对象的函数;允许更多的动态对象创建)
torch.tensor(data) # tensor([1, 2, 3], dtype=torch.int32) 我们的数据类型与numpy数组中的输入数据相匹配
·3·torch.as_tensor 工厂函数
torch.as_tensor(data) # tensor([1, 2, 3], dtype=torch.int32)
·4·torch.from_numpy 工厂函数
torch.from_numpy(data) # tensor([1, 2, 3], dtype=torch.int32)
工厂函数能力相对更大,更推荐使用
工厂函数类型判断( 根据输入推断数据类型,根据传入的数据选择一个dtype即'类型判断' ):
torch.tensor(np.array([1, 2, 3])) 整型进入
# tensor([1, 2, 3], dtype=torch.int32) 整型输出
torch.tensor(np.array([1., 2., 3.])) 浮点型进入
# tensor([1., 2., 3.], dtype=torch.float64) 浮点型输出
也可以显式地设置数据类型(所有工厂函数都允许):
torch.tensor(np.array([1, 2, 3]), dtype=torch.float64)
# tensor([1., 2., 3.], dtype=torch.float64)
*没有数据的情况下可以使用的创建方法:
·1·torch.eye 返回一个单位张量,或更广为人知的单位矩阵;指定我们想要的行数n,可以得到一个n维张量——>一个对角线上是1其余为0的张
torch,eye(2) # tensor([[1., 0.]
[0., 1.]])
·2·torch.zeros 可以调用的零函数,我们指定每个轴的长度,m x n ——> 有m个坐标轴的秩为n的张量,所有元素都是以相同方式排列的0(全0张量)
torch.zeros(2, 4) # tensor([[0., 0., 0., 0.],
[0., 0., 0., 0.]])
·3·torch.ones 大致内容同上(全1张量)
torch.ones(2, 4) # tensor([[1., 1., 1., 1.],
[1., 1., 1., 1.]])
·4·torch.rand 具有随机的值(随机张量)
torch.rand(2, 4) # 元素随机产生 可自行在编译器上试验
如果我们将现有的数据转换成一个pytorch张量,数据必须是数字的,很多时候可能会使用一个numpy数组,特别是在处理python中的数字数据时。
重新初始化我们的数据和张量:
data = np.array([1,2,3])
data # array([1, 2, 3])
t1 = torch.Tensor(data) # 创建张量时修改numpy数组并把它作为输入
t2 = torch.tensor(data)
t3 = torch.as_tensor(data)
t4 = torch.from_numpy(data)
data[0] = 4 # 修改了数组内的值,注意:没有修改我们的张量!
data[1] = 5
data[2] = 6
print(t1) # tensor([1., 2., 3.])
print(t2) # tensor([1, 2, 3], dtype=torch.int32)
* 前两个张量t1和t2包含来自输入阵列的原始数据值,在之后改变数组并不影响张量数据
print(t3) # tensor([4, 5, 6], dtype=torch.int32)
print(t4) # tensor([4, 5, 6], dtype=torch.int32)
* 后两个张量包含了更改后的数组中的数据,可以说是它们映射数组的情况
* 这种差异是由在每个创建选项中分配内存的方式造成的,前两个选项在内存中创建一个额外的输入数据副本,后两个选项是用数字数组在内存中共享数据,不同之处就在于对性能的共享
* 共享数据比复制数据更有效,使用更少的内存 ——> torch.as_tensor调用和torch.from_numpy调用,输入数据时是共享内存
最好的选择:
首先是torch.tensor(),带有小写t的工厂函数,它是十分常用的;
现在,如果我们要对性能进行调优,我们还需要第二种方法——内存共享,torch.as_tensor工厂函数,它可以接受任何像python数据结构这样的数组(torch.from_numpy只接受numpy数组)