Pytorch
是一个基于python
的科学计算库,致力于为两类用户提供服务:
张量和Numpy
中ndarrays
的概念很相似,有了这个作为基础,张量也可以被运行在GPU
上来加速计算,下面直接可以感受下张量的创建与Numpy
有什么样的区别。
>>> import torch
>>> # 这个是用来生成一个未初始化的5*3的张量,切记不是全零
>>> x = torch.empty(5, 3)
>>> print(x)
tensor([[1.6795e+08, 4.7423e+30, 4.7393e+30],
[9.5461e-01, 4.4377e+27, 1.7975e+19],
[4.6894e+27, 7.9463e+08, 3.2604e-12],
[1.7743e+28, 2.0535e-19, 5.9682e-02],
[7.0374e+22, 3.8946e+21, 4.4650e+30]])
>>> x = torch.rand(5, 3)
>>> print(x)
tensor([[0.6174, 0.5861, 0.0551],
[0.0682, 0.7835, 0.1476],
[0.9313, 0.4109, 0.5852],
[0.5924, 0.8216, 0.5282],
[0.1268, 0.6657, 0.8975]])
>>> # 这个是初始化一个全零张量,可以指定每个元素的类型。
>>> x = torch.zeros(5, 3, dtype=torch.long)
>>> print(x)
tensor([[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]])
>>> #从已有列表转化为张量
>>> x = torch.tensor([5.5, 3])
>>> print(x)
tensor([5.5000, 3.0000])
>>> #新建一个与之前tensor一样shape的tensor
>>> y = torch.randn_like(x, dtype=torch.float32)
>>> print(y)
tensor([-0.1764, -0.4671])
>>> print(x.size)
torch.Size([2])
在这里,先了解下张量的四则运算。包括加、剪、乘、除等具体的实现代码!
>>> import torch
>>> x = torch.rand(5, 3)
>>> y = torch.randn(5, 3)
>>> print(x)
tensor([[ 0.5597, -0.7536, 0.2511],
[-1.8233, -0.0448, 0.8358],
[ 1.6649, -0.2385, -0.5507],
[-2.2102, -0.8068, 0.6075],
[-0.9757, -1.4153, 1.6784]])
>>>print(y)
tensor([[-1.2461, 0.5389, 0.2612],
[-2.3689, -1.8445, 0.6520],
[ 0.0917, 1.0162, 0.0165],
[ 2.0018, -1.1325, -0.2471],
[-0.4413, -0.5396, -1.6062]])
>>> # 加法1
>>> z = x + y
>>> print(z)
tensor([[-0.6864, -0.2147, 0.5123],
[-4.1921, -1.8892, 1.4879],
[ 1.7566, 0.7777, -0.5343],
[-0.2084, -1.9393, 0.3604],
[-1.4170, -1.9549, 0.0722]])
>>> # 加法2
>>> z = torch.add(x, y)
>>> print(z)
tensor([[-0.6864, -0.2147, 0.5123],
[-4.1921, -1.8892, 1.4879],
[ 1.7566, 0.7777, -0.5343],
[-0.2084, -1.9393, 0.3604],
[-1.4170, -1.9549, 0.0722]])
>>> # 加法三,使用一个新的变量来保存
>>> result = torch.empty(5, 3)
>>> torch.add(x, y, out=result)
>>> print(result)
tensor([[-0.6864, -0.2147, 0.5123],
[-4.1921, -1.8892, 1.4879],
[ 1.7566, 0.7777, -0.5343],
[-0.2084, -1.9393, 0.3604],
[-1.4170, -1.9549, 0.0722]])
>>> # 加法四,在本身基础之上进行相加
>>> y.add_(x)
>>> print(y)
tensor([[-0.6864, -0.2147, 0.5123],
[-4.1921, -1.8892, 1.4879],
[ 1.7566, 0.7777, -0.5343],
[-0.2084, -1.9393, 0.3604],
[-1.4170, -1.9549, 0.0722]])
>>> import torch
>>> c = x - y
>>> print(c)
tensor([[ 1.8059, -1.2925, -0.0102],
[ 0.5456, 1.7997, 0.1838],
[ 1.5732, -1.2546, -0.5672],
[-4.2119, 0.3256, 0.8546],
[-0.5344, -0.8758, 3.2846]])
>>> c = torch.sub(x, y)
>>> print(c)
tensor([[ 1.8059, -1.2925, -0.0102],
[ 0.5456, 1.7997, 0.1838],
[ 1.5732, -1.2546, -0.5672],
[-4.2119, 0.3256, 0.8546],
[-0.5344, -0.8758, 3.2846]])
>>> z = x * y
>>> print(z)
tensor([[-0.6975, -0.4061, 0.0656],
[ 4.3191, 0.0825, 0.5450],
[ 0.1526, -0.2423, -0.0091],
[-4.4242, 0.9137, -0.1501],
[ 0.4306, 0.7636, -2.6959]])
>>> z = torch.mul(x, y)
>>> print(z)
tensor([[-0.6975, -0.4061, 0.0656],
[ 4.3191, 0.0825, 0.5450],
[ 0.1526, -0.2423, -0.0091],
[-4.4242, 0.9137, -0.1501],
[ 0.4306, 0.7636, -2.6959]])
>>> d = x / y
>>> print(d)
tensor([[-4.4917e-01, -1.3984e+00, 9.6110e-01],
[ 7.6968e-01, 2.4264e-02, 1.2819e+00],
[ 1.8158e+01, -2.3467e-01, -3.3461e+01],
[-1.1041e+00, 7.1247e-01, -2.4586e+00],
[ 2.2111e+00, 2.6231e+00, -1.0449e+00]])
>>> d = torch.div(x, y)
>>> print(d)
tensor([[-4.4917e-01, -1.3984e+00, 9.6110e-01],
[ 7.6968e-01, 2.4264e-02, 1.2819e+00],
[ 1.8158e+01, -2.3467e-01, -3.3461e+01],
[-1.1041e+00, 7.1247e-01, -2.4586e+00],
[ 2.2111e+00, 2.6231e+00, -1.0449e+00]])
>>> #二维矩阵乘法
>>> # 二维矩阵乘法运算操作包括torch.mm()、torch.matmul()、@,
>>> a = torch.ones(2, 1)
>>> b = torch.ones(1, 2)
>>> print(torch.mm(a, b).shape)
torch.Size([2, 2])
>>> print(torch.matmul(a, b).shape)
torch.Size([2, 2])
>>> print((a @ b).shape)
torch.Size([2, 2])
>>> c = torch.rand(4, 3, 28, 64)
>>> d = torch.rand(4, 3, 64, 32)
>>> print(torch.matmul(c, d).shape)
torch.Size([4, 3, 28, 32])
>>> a = torch.full([2, 2], 3)
>>> print(a)
tensor([[3., 3.],
[3., 3.]])
>>> b = a.pow(2)
>>> print(b)
tensor([[9., 9.],
[9., 9.]])
>>> c = b ** (0.5) # 也可以a**(0.5)
>>> print(c)
tensor([[3., 3.],
[3., 3.]])
>>> import torch
>>> a = torch.exp(torch.ones(2, 2)) # 得到2*2的全是e的Tensor
>>> print(a)
tensor([[2.7183, 2.7183],
[2.7183, 2.7183]])
>>> print(torch.log(a)) # 取自然对数
tensor([[1., 1.],
[1., 1.]])
>>> a = torch.tensor(3.14)
>>> print(a.floor(), a.ceil(), a.trunc(), a.frac()) # 取下,取上,取整数,取小数
>>> tensor(3.) tensor(4.) tensor(3.) tensor(0.1400)
>>> b = torch.tensor(3.49)
>>> c = torch.tensor(3.5)
>>> print(b.round(), c.round()) # 四舍五入
tensor(3.) tensor(4.)
>>> x = torch.randn(4, 4)
>>> y = x.view(16)
>>> z = x.view(-1, 8) # the size -1 is inferred from other dimensions
>>> print(x.size(), y.size(), z.size())
torch.Size([4, 4]) torch.Size([16]) torch.Size([2, 8])
>>> x = torch.randn(1)
>>> print(x)
tensor([-1.4743])
>>> print(x.item())
-1.4742881059646606
>>> a = torch.ones(5)
>>> print(a)
tensor([1., 1., 1., 1., 1.])
>>> b = a.numpy()
>>> print(b)
[1. 1. 1. 1. 1.]
>>> a.add_(1)
>>> print(a)
tensor([2., 2., 2., 2., 2.])
>>> print(b)
[2. 2. 2. 2. 2.]
>>> import numpy as np
>>> a = np.ones(5)
>>> b = torch.from_numpy(a)
>>> np.add(a, 1, out=a)
>>> print(a)
[2. 2. 2. 2. 2.]
>>> print(b)
tensor([2., 2., 2., 2., 2.], dtype=torch.float64)
>>> if torch.cuda.is_available(): #判断机器中有没有GPU
... device = torch.device("cuda") # 初始化一个device的对象
... y = torch.ones_like(x, device=device) # 直接在GPU中初始化一个tensor对象
... x = x.to(device) # 使用.to方法,将tensor移动到GPU中
... z = x + y
... print(z)
... print(z.to("cpu", torch.double)) # 将z张量移动到cpu中
tensor([-0.4743], device='cuda:0')
tensor([-0.4743], dtype=torch.float64)
在其他的文章中你可能会看到说将Tensor
包裹到Variable
中提供自动梯度计算,Variable
这个在 0.41 版中已经被标注为过期了,现在可以直接使用Tensor
。
在这里,我们先回顾下Pytorch
中的Tensor
初始化的实现:
torch.tensor(data, *, dtype=None, device=None, requires_grad=False, pin_memory=False) → Tensor
我们想直接创建一个Tensor
,那么我们需要使用torch.tensor()
,其中的具体参数如下:
data
是张量本身,可以是列表、元组、NumPy
的ndarray
,标量等等。dtype
是数据类型,比如float32
等。device
指定存储该张量的的设备。这个变量主要是针对 CPU/GPU 异构编程,在 CUDA 这种异构编程体系里,CPU 被称作主机(Host),某张 GPU 卡被称作设备(Device)。requires_grad
表示是否需要求梯度(Gradient)或者说是否需要求导。如果设置为 True,表示需要求导。默认这个值是 False。关于梯度的使用可以看下面的代码段示例:
>>> import torch
>>> #如果你设置了它的参数 ‘.requires_grad=true’ 的话,它将会开始去追踪所有的在这个张量上面的运算。
>>> #当你完成你的计算的时候,你可以调用’.backwward()来计算所有的微分。这个向量的梯度将会自动被保存在’grad’这个属性里面。
>>> x = torch.ones(2, 2, requires_grad=True)
>>> print(x)
tensor([[1., 1.],
[1., 1.]], requires_grad=True)
>>> y = x + 2
>>> print(y)
tensor([[3., 3.],
[3., 3.]], grad_fn=<AddBackward0>)
>>> print(y.grad_fn)
<AddBackward0 object at 0x7fc6bd199ac8>
>>> z = y * y * 3
>>> out = z.mean()
>>> print(z, out)
tensor([[27., 27.],
[27., 27.]], grad_fn=<MulBackward0>) tensor(27., grad_fn=<MeanBackward0>)
>>> a = torch.randn(2, 2)
>>> a = ((a * 3) / (a - 1))
>>> print(a.requires_grad)
False
>>> a.requires_grad_(True)
>>> print(a.requires_grad)
True
>>> b = (a * a).sum()
>>> print(b.grad_fn)
<SumBackward0 object at 0x7fc6bd1b02e8>
>>> out.backward()
>>> print(x.grad)
tensor([[4.5000, 4.5000],
[4.5000, 4.5000]])
我们可以看一下线性回归的案例,来学习下,如何使用自动求导来学习参数:在这个例子中,我们随机初始化了输入x
和输出y
,分别作为模型的特征和要拟合的目标值。这个模型有两层,第一层是输入层,第二层为隐藏层,模型的前向传播如下所示:
在案例中,我们需要网络学习到
与
的具体值:
import torch
dtype = torch.float
device = torch.device("cpu") # 使用CPU
# device = torch.device("cuda:0") # 如果使用GPU,请打开注释
# N: batch size
# D_in: 输入维度
# H: 隐藏层
# D_out: 输出维度
N, D_in, H, D_out = 64, 1000, 100, 10
# 初始化随机数x, y
# x, y用来模拟机器学习的输入和输出
x = torch.randn(N, D_in, device=device, dtype=dtype)
y = torch.randn(N, D_out, device=device, dtype=dtype)
# 初始化模型的参数w1和w2
# 均设置为 requires_grad=True
# PyTorch会跟踪w1和w2上的计算,帮我们自动求导
w1 = torch.randn(D_in, H, device=device, dtype=dtype, requires_grad=True)
w2 = torch.randn(H, D_out, device=device, dtype=dtype, requires_grad=True)
learning_rate = 1e-6
for t in range(500):
# 前向传播过程:
# h1 = relu(x * w1)
# y = h1 * w2
y_pred = x.mm(w1).clamp(min=0).mm(w2)
# 计算损失函数loss
# loss是误差的平方和
loss = (y_pred - y).pow(2).sum()
if t % 100 == 99:
print(t, loss.item())
# 反向传播过程:
# PyTorch会对设置了requires_grad=True的Tensor自动求导,本例中是w1和w2
# 执行完backward()后,w1.grad 和 w2.grad 里存储着对于loss的梯度
loss.backward()
# 根据梯度,更新参数w1和w2
with torch.no_grad():
w1 -= learning_rate * w1.grad
w2 -= learning_rate * w2.grad
# 将 w1.grad 和 w2.grad 中的梯度设为零
# PyTorch的backward()方法计算梯度会默认将本次计算的梯度与.grad中已有的梯度加和
# 必须在下次反向传播前先将.grad中的梯度清零
w1.grad.zero_()
w2.grad.zero_()
在这个例子中,我们对
和
设置了requires*grad=True
,损失函数是loss
,PyTorch
会自动跟踪这两个变量上的计算,当执行backward()
时,PyTorch
帮我们计算了loss
对于
和
的导数。每次迭代后,我们都要根据导数,更新
和
。PyTorch
的backward()
方法计算梯度会默认将本次计算的梯度与.grad
中已有的梯度加和,下次迭代前,需要将.grad
中的梯度清零,否则影响下一轮迭代的梯度值。经过多轮训练,模型的loss
不断变小。
在Pytorch
中,PyTorch
通过torch.utils.data
对一般常用的数据加载进行了封装,可以很容易地实现多线程数据预读和批量加载。并且torchvision
已经预先实现了常用图像数据集,包括如CIFAR-10,ImageNet、COCO、MNIST、LSUN
等数据集,可通过torchvision.datasets
方便的调用
Dataset
是一个抽象类, 为了能够方便的读取,需要将要使用的数据包装为Dataset
类。自定义的Dataset
需要继承它并且实现两个成员方法:
len(self)
)获取一条数据或一个样本。数据集展示
如上图所示,是我们常用来作为分类数据集的标签案例,前面是路径,后面为标签的具体值。我们使用Dataset
来进行数据加载。如下代码所示:
from torch.utils.data import Dataset
#定义一个数据集
class MyDataSet(Dataset):
""" 数据集演示 """
def __init__(self, file_name):
"""实现初始化方法,在初始化的时候将数据读载入"""
self.file=[line.strip() for line in open(file_name)]
def __len__(self):
'''
返回df的长度
'''
return len(self.file)
def __getitem__(self, idx):
'''
根据 idx 返回一行数据
'''
return self.file[idx]
执行下面的代码:
data = MyDataSet('./data.txt')
print(len(data))
返回数据集的长度为3
。
DataLoader
为我们提供了对Dataset
的读取操作,常用参数有:batch_size
(每个batch
的大小), shuffle
(是否进行shuffle
操作), num_workers
(加载数据的时候使用几个子进程)。
如下代码所示:
import torch
dataloader = torch.utils.data.DataLoader(data, batch_size=1, shuffle=True, num_workers=0)
#我们可以调整其中的参数,来进行数据部分代码修改
for i, data in enumerate(dataloader):
print(i,data)
返回结果如下:
0 ['/data/3.jpg 0']
1 ['/data/1.jpg 1']
2 ['/data/2.jpg 2']
torchvision.datasets
可以理解为PyTorch
团队自定义的dataset
,这些dataset
帮我们提前处理好了很多的图片数据集,我们拿来就可以直接使用,如下几种数据集:我们可以直接使用,示例如下:
import torchvision.datasets as datasets
trainset = datasets.MNIST(root='./data', # 表示 MNIST 数据的加载的目录
train=True, # 表示是否加载数据库的训练集,false的时候加载测试集
download=True, # 表示是否自动下载 MNIST 数据集
transform=None) # 表示是否需要对数据进行预处理,None为不进行预处理
torchvision.models
torchvision
不仅提供了常用图片数据集,还提供了训练好的模型,可以加载之后,直接使用,或者在进行迁移学习torchvision.models
模块的子模块中包含以下模型结构案例等。
#初始化resnet18模型
import torchvision.models as models
resnet18 = models.resnet18(pretrained=True)
print(resnet18)
这里可以看到输出的网络结构如下:
Downloading: "https://download.pytorch.org/models/resnet18-5c106cde.pth" to C:\Users\lenovo/.cache\torch\checkpoints\resnet18-5c106cde.pth
100%|██████████| 44.7M/44.7M [00:04<00:00, 11.2MB/s]
ResNet(
(conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
(layer1): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(1): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer2): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer3): Sequential(
(0): BasicBlock(
(conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer4): Sequential(
(0): BasicBlock(
(conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(avgpool): AvgPool2d(kernel_size=7, stride=1, padding=0)
(fc): Linear(in_features=512, out_features=1000, bias=True)
)
变换是常见的图像变换。它们可以使用链接在一起Compose
。此外,还有torchvision.transforms.functional
模块。功能转换可以对转换进行细粒度控制。如果您必须构建更复杂的转换管道(例如,在分段任务的情况下),这将非常有用。
这里主要有四类数据增强,如下:
Crop
)
中心裁剪:transforms.CenterCrop
随机裁剪:transforms.RandomCrop
随机长宽比裁剪:transforms.RandomResizedCrop
上下左右中心裁剪:transforms.FiveCrop
上下左右中心裁剪后翻转,transforms.TenCrop
Flip and Rotation
)
依概率p
水平翻转:transforms.RandomHorizontalFlip(p=0.5)
依概率p垂直翻转:transforms.RandomVerticalFlip(p=0.5)
随机旋转:transforms.RandomRotation
transforms.Resize
)
标准化:transforms.Normalize
转为tensor
,并归一化至[0-1]:transforms.ToTensor
填充:transforms.Pad
修改亮度、对比度和饱和度:transforms.ColorJitter
转灰度图:transforms.Grayscale
线性变换:(transforms.LinearTransformation()
)仿射变换:transforms.RandomAffine
依概率p
转为灰度图:transforms.RandomGrayscale
将数据转换为PILImage
:transforms.ToPILImage
transforms
操作,使数据增强更灵活
从给定的一系列transforms
中选一个进行操作:transforms.RandomChoice(transforms)
, 给一个transform
加上概率,依概率进行操作:transforms.RandomApply(transforms, p=0.5)
, 将transforms
中的操作随机打乱:transforms.RandomOrder
。在数据增强的一系列使用中,我们可以按照如下的代码进行:
import os
from PIL import Image
import numpy as np
from torchvision import transforms as T
transform = T.Compose([
T.Resize(224), # 缩放图片(Image),保持长宽比不变,最短边为224像素
T.CenterCrop(224), # 从图片中间切出224*224的图片
T.ToTensor(), # 将图片(Image)转成Tensor,归一化至[0, 1]
T.Normalize(mean=[.5, .5, .5], std=[.5, .5, .5]) # 标准化至[-1, 1],规定均值和标准差
])
img = Image.open('./image.jpg')
img = transform(img)
在前面的部分内容,我们可以使用伴有梯度的来实现深度学习模型,但其抽象程度较低,如果用其来实现深度学习模型,则需要编写的代码量极大。因此,torch.nn
应运而生,其是专门为深度学习而设计的模块。torch.nn
的核心数据结构是Module
,它是一个抽象概念,既可以表示神经网络中的某个层(layer
),也可以表示一个包含很多层的神经网络。在实际使用中,最常见的做法是继承nn.Module
,撰写自己的网络层。
首先了解下,全连接层是个啥,长啥样呢?看下图所示:
全连接层主要在应用中是将中间的特征维度进行调整,调整到类别数的这一操作。
先来看看如何用nn.Module
实现自己的全连接层。
如何用nn.Module
实现自己的全连接层?输出
和输入
满足
,
和
是可学习的参数。
import torch as t
from torch import nn
class Linear(nn.Module): # 继承nn.Module
def __init__(self, in_features, out_features):
super(Linear, self).__init__() # 等价于nn.Module.__init__(self)
self.w = nn.Parameter(t.randn(in_features, out_features)) #nn.Parameter是将其包装成一个参数对象,可以用来进行训练
self.b = nn.Parameter(t.randn(out_features)) #bias,可训练
def forward(self, x):
x = x.mm(self.w)
return x + self.b.expand_as(x) #使用了广播机制
执行代码:
linear = Linear(4,3)
input = t.randn(2,4)
output = linear(input)
print(output)
结果如下:
tensor([[-1.1762, 2.0987, 4.4372],
[-2.3790, -1.8490, 0.5742]], grad_fn=<AddBackward0>)
我们可以遍历下模型的参数:
for name, parameter in linear.named_parameters():
print(name, parameter) # w and b
w Parameter containing:
tensor([[ 0.5792, 0.4161, -1.2512],
[-0.2647, -0.8852, -1.5065],
[-0.5495, 0.2031, -0.5617],
[ 0.4093, 1.0125, 0.7929]], requires_grad=True)
b Parameter containing:
tensor([-1.3909, 0.9145, 0.6379], requires_grad=True)
对于自定义全连接层的实现,可以参考上面的代码,当然后面对于像卷积层或者整个神经网络都是类似的设计,设计学习参数,以及实现forward()
函数。
init
中必须自己定义可学习的参数,并封装成Parameter
,如在本例中我们把和
封装成parameter
。其默认需要求导(requires_grad = True
)。
Module
中的可学习参数可以通过named_parameters()
或者parameters()
返回迭代器,前者会给每个parameter
都附上名字,使其更具有辨识度。Linear
必须继承nn.Module
,并且在其构造函数中需调用nn.Module
的构造函数,即super(Linear, self)._init_()
或nn.Module._init_(self)
,推荐使用第一种用法,尽管第二种写法更直观。forward
函数实现前向传播过程,其输入可以是一个或多个tensor
。对于图像处理中,主要包括卷积层(Conv
)、池化层(Pool
)等,这些层在实际使用中可分为一维(1D
)、二维(2D
)、三维(3D
),池化方式又分为平均池化(AvgPool
)、最大值池化(MaxPool
)、自适应池化(AdaptiveAvgPool
)等。而卷积层除了常用的前向卷积之外,还有转置卷积(TransposeConv
)。
VGG16
第一个FC
的参数量为个。卷积层每次用来遍历图像的这个滤波器的值是共享的,即每个位置都是一样的值,所以它的参数量会比全连接层小很多。如VGG16
的Conv2-1
的参数量为:
。
同时,卷积神经网络还利用了感受野的这一概念:由于图像的空间联系是局部的,每个神经元不需要对全部的图像做感受,只需要感受局部特征即可,然后在更高层将这些感受得到的不同的局部神经元综合起来就可以得到全局的信息了,这样可以减少连接的数目。
卷积过程
对于卷积,我们这里主要是采用的2D
卷积来举例子!
torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)
其中具体的参数如下:
首先我们先看下举例图片:
from PIL import Image
image = Image.open('./slumdunk.jpg')
image.show()
接下来,先将图像转成tensor
格式,再进行卷积操作!
from torchvision.transforms import ToTensor, ToPILImage
to_tensor = ToTensor() # img -> tensor
to_pil = ToPILImage()
from torch import nn
# 输入是一个batch,batch_size=1
input = to_tensor(image).unsqueeze(0)
conv = nn.Conv2d(3, 1, (3, 3), 1, bias=False)
out = conv(input)
ret_img = to_pil(out.data.squeeze(0))
ret_img.show()
CNN
这样不断地进行信息提取,供后面进行分类与检测任务处理。
AvgPool2d()
方法进行讲解。官网中的文档如下:torch.nn.AvgPool2d(kernel_size, stride=None, padding=0, ceil_mode=False, count_include_pad=True, divisor_override=None)
其中的主要参数的具体作用如下:
代码如下:
avg_pool = nn.AvgPool2d(2)
pool_img = avg_pool(input)
ret_img = to_pil(pool_img.squeeze())
ret_img.show()
看看使用代码后的效果:
Pytorch
中,有直接可以用的全连接函数!nn.Linear(in_features, out_features, bias=True)
Batch Normalization
,也叫BN
),分为1D、2D和3D。还有在风格迁移中常用到的Instance Normalizaion
层。对于BN
的作用,主要是为了解决在深度神经网络中,每一层的特征分布不一致,神经网络学习难度大的问题。其运算过程主要如下:
相关库函数:
torch.nn.BatchNorm2d(num_features, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
对于这些超参主要是框架在实现的过程中的一些技巧,如下所示:
BN的具体实现过程
网络训练中以batch_size
为最小单位不断迭代,而由于每一次的batch
有差异,实际是通过变量,以及滑动平均来记录均值与方差。训练完成后,推断阶段时通过
,
,以及记录的均值与方差计算bn
层输出。
举例如何使用:
import torch
feature = torch.randn(2, 128, 14, 14) #(N, C, H, W)
bn = torch.nn.BatchNorm2d(128) #使用默认参数
out = bn(feature)
Dropout
的简单使用。import torch
drop = torch.nn.Dropout(0.5)
feature = torch.randn(12, 256)
out = drop(feature)
在神经网络中,需要激活函数来搭配使用,使得网络是非线性的。如果不用激励函数(其实相当于激励函数是
),在这种情况下你每一层节点的输入都是上层输出的线性函数,很容易验证,无论你神经网络有多少层,输出都是输入的线性组合,与没有隐藏层效果相当,这种情况就是最原始的感知机(Perceptron)了,那么网络的逼近能力就相当有限。正因为上面的原因,我们决定引入非线性函数作为激励函数,这样深层神经网络表达能力就更加强大(不再是输入的线性组合,而是几乎可以逼近任意函数)。
常用的激活函数有sigmoid
、tanh
、ReLU
等。
在这里,我们以ReLU
为样例,目前大多数的网络都会主要选择ReLU
来作为激活函数,因为其避免了使用sigmid
、tanh
梯度消失的问题。
ReLU
的计算公式如下:
代码示例如下:
import torch
relu = torch.nn.ReLU(inplace=True)
input = torch.randn(3, 3)
print(input)
output = relu(input)
print(output) # 小于0的都被截断为0
# 等价于input.clamp(min=0)
tensor([[ 0.4869, 1.3280, -1.1800],
[-0.5563, 0.4759, -1.5502],
[-2.1328, 0.9588, -0.5466]])
tensor([[0.4869, 1.3280, 0.0000],
[0.0000, 0.4759, 0.0000],
[0.0000, 0.9588, 0.0000]])
ReLU
函数有个inplace
参数,如果设为True
,它会把输出直接覆盖到输入中,这样可以节省内存/显存。之所以可以覆盖是因为在计算ReLU
的反向传播时,只需根据输出就能够推算出反向传播的梯度。
对于我们在搭网络的时候,我们通常会将上层的输出放入这层的输入,这样的网络叫做前馈神经网络,而这样写起来有时候回比较折腾。在此,就有两种简化方式,ModuleList
和Sequential
。其中Sequential
是一个特殊的module
,它包含几个子Module
,前向传播时会将输入一层接一层的传递下去。ModuleList
也是一个特殊的module
,可以包含几个子module
,可以像用list
一样使用它,但不能直接把输入传给ModuleList
。下面举例说明:
第一种 :
import torch
from torch import nn
net1 = nn.Sequential()
net1.add_module('conv', nn.Conv2d(3, 3, 3))
net1.add_module('batchnorm', nn.BatchNorm2d(3))
net1.add_module('activation_layer', nn.ReLU())
print(net1)
#输出结果:
Sequential(
(conv): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
(batchnorm): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(activation_layer): ReLU()
)
第二种:
import torch
from torch import nn
net2 = nn.Sequential(
nn.Conv2d(3, 3, 3),
nn.BatchNorm2d(3),
nn.ReLU()
)
print(net2)
#输出结果
net2: Sequential(
(0): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
(1): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
)
第三种:
from collections import OrderedDict
net3= nn.Sequential(OrderedDict([
('conv1', nn.Conv2d(3, 3, 3)),
('bn1', nn.BatchNorm2d(3)),
('relu1', nn.ReLU())
]))
print('net3:', net3)
#输出结果
net3: Sequential(
(conv1): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
(bn1): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu1): ReLU()
)
我们可以使用名字或者序号来取出其中的Module
,如下代码:
print(net1.conv, net2[0], net3.conv1)
#输出:
(Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1)),
Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1)),
Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1)))
在神经网络训练的时候,我们需要使用损失函数来进行网络的拟合。根据不同的任务,我们选择不同的损失函数,比如在目标检测任务中,我们会有坐标回归的损失,以及分类的损失。我们常用的损失函数主要是以下几种:
class torch.nn.BCELoss(weight=None, size_average=None, reduce=None, reduction='mean')
sigmoid
函数和BCELoss
方法结合到一个类中。class torch.nn.BCEWithLogitsLoss(weight=None, size_average=None, reduce=None, reduction='mean', pos_weight=None)
class torch.nn.NLLLoss(weight=None, size_average=None, ignore_index=-100, reduce=None, reduction='mean')
class torch.nn.CrossEntropyLoss(weight=None, size_average=True, ignore_index=-100, reduce=True)
class torch.nn.L1Loss(size_average=None, reduce=None, reduction='mean')
class torch.nn.SmoothL1Loss(size_average=None, reduce=None, reduction='mean')
这里以CrossEntropyLoss
来举例:
import torch
from torch import nn
# batch_size=3,计算对应每个类别的分数(只有两个类别)
score = torch.randn(3, 2)
# 三个样本分别属于1,0,1类,label必须是LongTensor
label = torch.Tensor([1, 0, 0]).long()
# loss与普通的layer无差异
criterion = nn.CrossEntropyLoss()
loss = criterion(score, label)
print(loss)
#输出
tensor(1.0429)
PyTorch
将深度学习中常用的优化方法全部封装在torch.optim
中,其设计十分灵活,能够很方便的扩展成自定义的优化方法。目前常用的优化器主要有以下四个:
所有的优化方法都是继承基类optim.Optimizer
,并实现了自己的优化步骤。下面就以最基本的优化方法——随机梯度下降法(SGD
)举例说明。这里需重点掌握关于优化器的一些参数设置:
optim
的使用方法如下:
#首先定义一个网络,这里以LeNet为例
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 6, 5),
nn.ReLU(),
nn.MaxPool2d(2,2),
nn.Conv2d(6, 16, 5),
nn.ReLU(),
nn.MaxPool2d(2,2)
)
self.classifier = nn.Sequential(
nn.Linear(16 * 5 * 5, 120),
nn.ReLU(),
nn.Linear(120, 84),
nn.ReLU(),
nn.Linear(84, 10)
)
def forward(self, x):
x = self.features(x)
x = x.view(-1, 16 * 5 * 5)
x = self.classifier(x)
return x
net = Net()
#优化器的定义过程
from torch import optim
optimizer = optim.SGD(params=net.parameters(), lr=0.01)
optimizer.zero_grad() # 梯度清零,等价于net.zero_grad(),这一步至关重要!
input = t.randn(1, 3, 32, 32)
output = net(input)
output.backward(output) # fake backward
optimizer.step() # 执行优化
而神经网络的训练,有时候是需要将某些层固定较大的学习率,剩下的层使用较小的学习率,这时候,依旧可以如下设计:
# 只为两个全连接层设置较大的学习率,其余层的学习率较小
special_layers = nn.ModuleList([net.classifier[0], net.classifier[3]])
special_layers_params = list(map(id, special_layers.parameters()))
base_params = filter(lambda p: id(p) not in special_layers_params,
net.parameters())
optimizer = t.optim.SGD([
{'params': base_params},
{'params': special_layers.parameters(), 'lr': 0.01}
], lr=0.001 )
我们同样可以调整不同的学习率,第一种方法:
# 方法1: 新建一个optimizer
old_lr = 0.1
optimizer1 =optim.SGD([
{'params': net.features.parameters()},
{'params': net.classifier.parameters(), 'lr': old_lr*0.1}
], lr=1e-5)
optimizer1
第二种方法则为:
for param_group in optimizer.param_groups:
param_group['lr'] *= 0.1 # 学习率为之前的0.1倍
参数的初始化在神经网络中十分重要,良好的初始化能让模型更快收敛,同时达到更高的水平,而糟糕的初始化则可能模型都难以收敛。PyTorch
中nn.Module
的模块参数都采取了较为合理的初始化策略,因此一般不用我们考虑,当然我们也可以用自定义初始化去代替系统的默认初始化。而当我们在使用Parameter
时,自定义初始化则尤为重要,因torch.Tensor()
返回的是内存中的随机数,很可能会有极大值,这在实际训练网络中会造成溢出或者梯度消失。PyTorch
中nn.init
模块就是专门为初始化而设计,如果某种初始化策略nn.init
不提供,用户也可以自己直接初始化。
对于参数初始化,主要有:
# 默认方法
for m in model.modules():
if isinstance(m, (nn.Conv2d, nn.Linear)):
nn.init.xavier_uniform_(m.weight)
# he initialization
for m in model.modules():
if isinstance(m, (nn.Conv2d, nn.Linear)):
nn.init.kaiming_normal_(m.weight, mode='fan_in')
for m in model.modules():
if isinstance(m, (nn.Conv2d, nn.Linear)):
nn.init.orthogonal(m.weight)
for m in model.modules():
if isinstance(m, nn.BatchNorm2d):
nn.init.constant(m.weight, 1)
nn.init.constant(m.bias, 0)
我们可以使用init
来进行参数初始化:
# 利用nn.init初始化
from torch import nn
from torch.nn import init
import torch
linear = nn.Linear(3, 4)
torch.manual_seed(1)
# 等价于 linear.weight.data.normal_(0, std)
init.xavier_normal_(linear.weight)
同样,我们可以直接初始化:
# 直接初始化
import math
import torch
torch.manual_seed(1)
# xavier初始化的计算公式
std = math.sqrt(2)/math.sqrt(7.)
linear.weight.data.normal_(0,std)
nn
中还有一个很常用的模块:nn.functional
,nn
中的大多数layer
,在functional
中都有一个与之相对应的函数。nn.functional
中的函数和nn.Module
的主要区别在于,用nn.Module
实现的layers
是一个特殊的类,都是由class layer(nn.Module)
定义,会自动提取可学习的参数。而nn.functional
中的函数更像是纯函数,在不需要参数的情况下,可以室友nn.functional
来进行实现。
如ReLU、tanh、sigmoid
、Pool
等没有学习参数的层,可以使用nn.functional
来进行实现。
如:
from torch.nn import functional as F
import torch
feature = torch.randn(1, 3, 12, 12)
out1 = F.relu(feature)
这里,可以使用下上面学习到的Pytorch
技巧来搭建一个ResNet
网络,第一种方法如下:
from torch import nn
import torch as t
from torch.nn import functional as F
class ResidualBlock(nn.Module):
'''
残差模块的实现
'''
def __init__(self, inchannel, outchannel, stride=1, shortcut=None):
super(ResidualBlock, self).__init__()
self.left = nn.Sequential(
nn.Conv2d(inchannel,outchannel,3,stride, 1,bias=False),
nn.BatchNorm2d(outchannel),
nn.ReLU(inplace=True),
nn.Conv2d(outchannel,outchannel,3,1,1,bias=False),
nn.BatchNorm2d(outchannel) )
self.right = shortcut
def forward(self, x):
out = self.left(x)
residual = x if self.right is None else self.right(x)
out += residual
return F.relu(out)
class ResNet(nn.Module):
'''
实现主module:ResNet34
ResNet34 包含多个layer,每个layer又包含多个residual block
用子module来实现residual block,用_make_layer函数来实现layer
'''
def __init__(self, num_classes=1000):
super(ResNet, self).__init__()
# 前几层图像转换
self.pre = nn.Sequential(
nn.Conv2d(3, 64, 7, 2, 3, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(3, 2, 1))
# 重复的layer,分别有3,4,6,3个residual block
self.layer1 = self._make_layer( 64, 64, 3)
self.layer2 = self._make_layer( 64, 128, 4, stride=2)
self.layer3 = self._make_layer( 128, 256, 6, stride=2)
self.layer4 = self._make_layer( 256, 512, 3, stride=2)
#分类用的全连接
self.fc = nn.Linear(512, num_classes)
def _make_layer(self, inchannel, outchannel, block_num, stride=1):
'''
构建layer,包含多个residual block
'''
shortcut = nn.Sequential(
nn.Conv2d(inchannel,outchannel,1,stride, bias=False),
nn.BatchNorm2d(outchannel))
layers = []
layers.append(ResidualBlock(inchannel, outchannel, stride, shortcut))
for i in range(1, block_num):
layers.append(ResidualBlock(outchannel, outchannel))
return nn.Sequential(*layers)
def forward(self, x):
x = self.pre(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = F.avg_pool2d(x, 7)
x = x.view(x.size(0), -1)
return self.fc(x)
model = ResNet()
input = t.randn(1, 3, 224, 224)
out = model(input)
同样,我们可以使用torchvision
来搭建神经网络,如下代码:
from torchvision import models
model = models.resnet34()
Pytorch
在实际使用过程中,往往会使用多卡来进行训练。而多卡训练也包括单机多卡,多机多卡,在这里,我们主要参考单机多卡的使用方式。
#我一般在使用多GPU的时候, 会喜欢使用os.environ['CUDA_VISIBLE_DEVICES']来限制使用的GPU个数, 例如我要使用第0和第3编号的GPU, 那么只需要在程序中这样设置:
import os
os.environ['CUDA_VISIBLE_DEVICES'] = '0,3' #第一块与第四块卡
#但是要注意的是, 这个参数的设定要保证在模型加载到gpu上之前, 我一般都是在程序开始的时候就设定好这个参数, 之后如何将模型加载到多GPU上面呢?
import torch
from torch import nn
#如果是模型, 那么需要执行下面的这几句代码:
model = MyModel() #其中MyModel是自己实现的模型
model = nn.DataParallel(model)
model = model.cuda()
#如果是数据, 那么直接执行下面这几句代码就可以了:
inputs = inputs.cuda()
labels = labels.cuda()
在Pytorch
在训练过程中,如果随机初始化,那么可能导致每次训练的结果不一样。因此,在每次训练的时候,需要固定住这些随机参数!代码如下:
import os
import random
import torch
def seed_torch(seed=1029):
random.seed(seed)
os.environ['PYTHONHASHSEED'] = str(seed) # 为了禁止hash随机化,使得实验可复现
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed) # if you are using multi-GPU.
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True
seed_torch()
Pytorch
最快的学习方法往往是直接上手一个新的完整项目,学习搭建过程中的设计与代码,在后面,我们再进行Pytorch
的项目实战,在实战中,再进行其中的细节讲解,并夯实基础!
[AICAMP——Python入门]:
[AICAMP——Linux入门]:Linux入门
欢迎加我微信,每天16个小时在线