Pytorch 基础

Pytorch 简介

什么是 Pytorch

Python优先的深度学习框架,支持GPU加速和动态神经网络。

为什么使用 PyTorch

  • 动态图机制,更加灵活
  • 与Python紧密结合,更加方便
  • 支持GPU加速,更加快速
  • 支持分布式训练,更加强大

PyTorch 基础

Tensor 张量

Tensor 相当于多维的矩阵。

Tensor 的数据类型有:(32位浮点型)torch.FloatTensor,(64位浮点型)torch.DoubleTensor,(16位整型)torch.ShortTensor,(32位整型)torch.IntTensor,(64位整型)torch.LongTensor

导入 PyTorch

1
import torch

创建一个没有初始化的 5×3 矩阵

1
2
x = torch.empty(5, 3)
print(x)

创建一个随机初始化的矩阵

1
2
3
4
5
6
7
# 均匀分布[0,1], rand
x = torch.rand(5, 3)
print(x)

# 正态分布, randn
x = torch.randn(5, 3)
print(x)

创建一个全0,全1,或者对角线为1的矩阵,且数据类型为 long

1
2
3
4
5
6
7
8
9
10
11
# 全0
x = torch.zeros(5, 3, dtype=torch.long)
print(x)

# 全1
x = torch.ones(5, 3, dtype=torch.long)
print(x)

# 对角线为1
x = torch.eye(5, 3, dtype=torch.long)
print(x)

从数据中直接创建 Tensor

1
2
3
4
5
6
7
8
# 从列表中创建
x = torch.tensor([5.5, 3])
print(x)

# 从numpy数组中创建
import numpy as np
x = torch.from_numpy(np.ones(5))
print(x)

创建一个和已有 Tensor 一样大小的 Tensor

1
2
x = torch.randn_like(x)
print(x)

获取 Tensor 的形状

1
print(x.size())

torch.Size 本质上是一个元组,所以支持元组的各种操作。

转化为 numpy 数组

1
2
x = x.numpy()
print(x)

绝对值

1
2
x = torch.abs(x)
print(x)

加法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
y = torch.rand(5, 3)
# 形式一
print(x + y)

# 形式二
print(torch.add(x, y))

# 形式三
result = torch.empty(5, 3)
torch.add(x, y, out=result)
print(result)

# 形式四
y.add_(x)
print(y)

add_ 是原地操作,即改变了 y 的值。所有的inplace操作都有一个 _ 后缀,表示原地操作。
其他运算:sub 减法,mul 乘法,div 除法,pow 幂运算,mm 矩阵乘法,mv 矩阵向量乘法。

限定范围

1
2
x = torch.clamp(x, min=0, max=1)
print(x)

改变形状

1
2
3
4
x = torch.randn(4, 4)
y = x.view(16)
z = x.view(-1, 8) # -1 表示自适应
print(x.size(), y.size(), z.size())

获取单元素 Tensor 中的值

1
2
x = torch.randn(1)
print(x.item())

使用GPU

1
2
3
4
5
6
7
# 判断是否有GPU
print(torch.cuda.is_available())

# 创建一个 Tensor 并放到 GPU 上
x = torch.randn(5, 3)
x = x.cuda()
print(x)

Variable 变量

Autograd 自动求导

创建一个张量并设置 requires_grad = True 用来追踪其计算历史

1
2
x = torch.ones(2, 2, requires_grad=True)
print(x)

对这个张量做一次运算

1
2
3
4
5
6
7
8
9
y = x + 2
print(y)
# y 是计算结果,所以它有 grad_fn 属性
print(y.grad_fn)

# 对 y 做更多操作
z = y * y * 3
out = z.mean()
print(z, out)

.requires_grad_() 会改变现有张量的 requires_grad 标志。如果没有指定的话,默认输入的这个标志是 False

1
2
3
4
5
6
7
8
9
a = torch.randn(2, 2)
a = ((a * 3) / (a - 1))
print(a.requires_grad)

a.requires_grad_(True)
print(a.requires_grad)

b = (a * a).sum()
print(b.grad_fn)

梯度

1
2
3
4
5
6
7
8
9
10
x = torch.ones(2,2,requires_grad=True)
y = x + 2
z = y * y * 3
out = z.mean()

# 现在开始反向传播,因为 out 是一个标量,则 out.backward() 和 out.backward(torch.tensor(1.)) 等价。
out.backward()

# 打印梯度 d(out)/dx
print(x.grad)

out=14iziout=\frac{1}{4}\sum_iz_i

zi=3(xi+2)2z_i=3(x_i+2)^2

因为zixi=1=27z_i\bigr\rvert_{x_i=1}=27,所以

outxi=32(xi+2)\frac{\partial_{out}}{\partial_{x_i}}=\frac{3}{2}(x_i+2)

所以

outxixi=1=92=4.5\frac{\partial_{out}}{\partial_{x_i}}\bigr\rvert_{x_i=1}=\frac{9}{2}=4.5

雅可比矩阵

数学上,若有向量值函数 y = f(x),那么 y 相对于 x 的梯度是一个雅可比矩阵。

J=[y1x1y1xnymx1ymxn]J= \begin{bmatrix} \frac{\partial y_1}{\partial x_1} & \cdots & \frac{\partial y_1}{\partial x_n}\\ \vdots & \ddots & \vdots\\ \frac{\partial y_m}{\partial x_1} & \cdots & \frac{\partial y_m}{\partial x_n} \end{bmatrix}

通常来说,torch.autograd 是计算雅可比向量积的一个引擎。也就是说,给定任意向量 v,计算乘积JvJ·v。如果 v 恰好是标量函数 l = g(y) 的梯度,也即v=(ly1,,lym)Tv=(\frac{\partial l}{\partial y_1}, \cdots ,\frac{\partial l}{\partial y_m})^T,那么根据链式法则,雅可比向量积的计算刚好就是 lx 的导数:

Jv=[y1x1y1xnymx1ymxn][ly1lym]=[lx1lxn]J·v= \begin{bmatrix} \frac{\partial y_1}{\partial x_1} & \cdots & \frac{\partial y_1}{\partial x_n}\\ \vdots & \ddots & \vdots\\ \frac{\partial y_m}{\partial x_1} & \cdots & \frac{\partial y_m}{\partial x_n} \end{bmatrix} \begin{bmatrix} \frac{\partial l}{\partial y_1}\\ \vdots\\ \frac{\partial l}{\partial y_m} \end{bmatrix}=\begin{bmatrix} \frac{\partial l}{\partial x_1}\\ \vdots\\ \frac{\partial l}{\partial x_n} \end{bmatrix}

雅可比向量积的这一特性使得将外部梯度输入到具有非标量输出的模型中变得非常方便。

1
2
3
4
5
x = torch.randn(3, requires_grad=True)
y = x * 2
while y.data.norm() < 1000:
y = y * 2
print(y)

下面这种情况,y 不再是标量。torch.autograd 不能直接计算完整的雅可比矩阵,但是如果我们只想要雅可比向量积,只需将这个向量作为参数传给 backward

1
2
3
v = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float)
y.backward(v)
print(x.grad)

也可以通过将代码块包装在 with torch.no_grad(): 中,来阻止 autograd 跟踪设置了 .requires_grad=True 的张量的历史记录。

1
2
3
4
5
print(x.requires_grad)
print((x ** 2).requires_grad)

with torch.no_grad():
print((x ** 2).requires_grad)

Variable

VariableTensor 的区别是 Variable 会被放入计算图中,然后进行前向传播,反向传播,自动求导。

Variable 是在 torch.autograd.Variable 中的,使用Variable 需要导入 torch.autograd.Variable

1
2
3
4
5
6
7
8
9
10
11
12
13
from torch.autograd import Variable

x = Variable(torch.Tensor([1]),requires_grad=True)
w = Variable(torch.Tensor([2]),requires_grad=True)
b = Variable(torch.Tensor([3]),requires_grad=True)

y = w * x + b

y.backward()

print(x.grad)
print(w.grad)
print(b.grad)

搭建一个简单的神经网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import torch

batch_n = 100 # 一个批次中输入数据的数量
hidden_layer = 100 # 经过隐藏层后保留的数据特征的个数
input_data = 1000 # 每个数据包含的数据量
output_data = 10 # 每个输出的数据包含的数据量

x = torch.randn(batch_n, input_data) # 100*1000
y = torch.randn(batch_n, output_data) # 100*10

w1 = torch.randn(input_data, hidden_layer) # 1000*100
w2 = torch.randn(hidden_layer, output_data) # 100*10

epoch_n = 20 # 训练的次数
learning_rate = 1e-6 # 学习率

for epoch in range(epoch_n):
h1 = x.mm(w1) # 100*100
h1 = h1.clamp(min=0) # if x < 0 ,x=0
y_pred = h1.mm(w2) # 100*10,前向传播预测结果

loss = (y_pred - y).pow(2).sum() # 损失函数,即均方误差
print("Epoch:{}, Loss:{:.4f}".format(epoch, loss))
grad_y_pred = 2*(y_pred-y) # dloss/dy
grad_w2 = h1.t().mm(grad_y_pred) # dloss/dy * dy/dw2

grad_h = grad_y_pred.clone() # 复制
grad_h = grad_h.mm(w2.t()) # dloss/dy * dy/dh1
grad_h.clamp_(min=0) # if x < 0, x = 0
grad_w1 = x.t().mm(grad_h)

w1 -= learning_rate*grad_w1 # 梯度下降
w2 -= learning_rate*grad_w2

使用 Variable 搭建一个自动计算梯度的神经网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import torch
from torch.autograd import Variable

batch_n = 100 # 一个批次中输入数据的数量
hidden_layer = 100 # 经过隐藏层后保留的数据特征的个数
input_data = 1000 # 每个数据包含的数据量
output_data = 10 # 每个输出的数据包含的数据量

x = Variable(torch.randn(batch_n, input_data),
requires_grad=False) # requires_grad = False不保留梯度
y = Variable(torch.randn(batch_n, output_data), requires_grad=False)
w1 = Variable(torch.randn(input_data, hidden_layer),
requires_grad=True) # requires_grad = True自动保留梯度
w2 = Variable(torch.randn(hidden_layer, output_data), requires_grad=True)

epoch_n = 20
learning_rate = 1e-6

for epoch in range(epoch_n):
y_pred = x.mm(w1).clamp(min=0).mm(w2) # y_pred = w2 * (w1 * x)
loss = (y_pred-y).pow(2).sum() # 损失函数
print("Epoch:{},Loss:{:.4f}".format(epoch, loss))

loss.backward() # 后向传播计算

w1.data -= learning_rate*w1.grad.data
w2.data -= learning_rate*w2.grad.data

w1.grad.data.zero_() # 置0
w2.grad.data.zero_()

使用 nn.Module 自定义传播函数来搭建神经网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import torch
from torch.autograd import Variable

batch_n = 100
hidden_layer = 100
input_data = 1000
output_data = 10


class Model(torch.nn.Module):
def __init__(self):
super(Model, self).__init__()

def forward(self, input_n, w1, w2):
x = torch.mm(input_n, w1)
x = torch.clamp(x, min=0)
x = torch.mm(x, w2)
return x

def backward(self):
pass


model = Model()

x = Variable(torch.randn(batch_n, input_data),
requires_grad=False) # requires_grad = False不保留梯度
y = Variable(torch.randn(batch_n, output_data), requires_grad=False)
w1 = Variable(torch.randn(input_data, hidden_layer),
requires_grad=True) # requires_grad = True自动保留梯度
w2 = Variable(torch.randn(hidden_layer, output_data), requires_grad=True)

epoch_n = 20
learning_rate = 1e-6

for epoch in range(epoch_n):
y_pred = model(x, w1, w2)
loss = (y_pred-y).pow(2).sum()
print("Epoch:{},Loss:{:.4f}".format(epoch, loss))
loss.backward() # 后向传播计算

w1.data -= learning_rate*w1.grad.data
w2.data -= learning_rate*w2.grad.data

w1.grad.data.zero_() ## 置0
w2.grad.data.zero_()

Dataset 数据集

torch.utils.data.Dataset是代表这一数据的抽象类,可以自己定义数据类继承和重写这个抽象类,只需要定义__len____getitem__函数即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from torch.utils.data import Dataset


class myDataset(Dataset):
def __init__(self, csv_file, txt_file, root_dir, other_file):
self.csv_data = pd.read_csv(csv_file)
with open(txt_file, 'r') as f:
data_list = f.readlines()
self.txt_data = data_list
self.root_dir = root_dir

def __len__(self):
return len(self.csv_data)

def __getitem__(self, idx):
data = (self.csv_data[idx], self.txt_data[idx])
return data

通过上面的方式,可以定义需要的数据类,可以通过迭代的方法取得每一个数据,但是这样很难实现取 batchshuffle 或者多线程去读取数据,所以 Pytorch 中提供了 torch.utils.data.DataLoader 来定义一个新迭代器。

1
2
from torch.utils.data import DataLoader
dataiter = DataLoader(myDataset, batch_size=32)

nn.Module 模组

所有的层结构和损失函数来自 torch.nn

1
2
3
4
5
6
7
8
9
10
11
from torch import nn


class net_name(nn.Module):
def __init__(self, other_arguments):
super(net_name, self).__init__()
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size)

def forward(self, x):
x = self.conv1(x)
return x

一个神经网络的典型训练过程如下:

  • 定义包含一些可学习参数(或者叫权重)的神经网络
  • 在输入数据集上迭代
  • 通过网络处理输入
  • 计算 loss (输出和正确答案的距离)
  • 将梯度反向传播给网络的参数
  • 更新网络的权重,一般使用一个简单的规则:weight = weight - learning_rate * gradient

使用 torch.nn 内的序列容器 Sequential

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import torch

batch_n = 100
hidden_layer = 100
input_data = 1000
output_data = 10


model = torch.nn.Sequential(
torch.nn.Linear(input_data, hidden_layer),
torch.nn.ReLU(),
torch.nn.Linear(hidden_layer, output_data)
)


print(model)

使用 nn.Module 定义一个神经网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import torch
import torch.nn as nn
import torch.nn.functional as F


class Net(nn.Module):

def __init__(self):
super(Net, self).__init__()
# 输入图像channel:1, 输出channel:6, 5x5卷积核
self.conv1 = nn.Conv2d(1, 6, 5)
self.conv2 = nn.Conv2d(6, 16, 5)
# an affine operation: y = Wx + b
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)

def forward(self, x):
# 2x2 Max pooling
x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
# 如果是方阵,则可以只使用一个数字进行定义
x = F.max_pool2d(F.relu(self.conv2(x)), 2)
x = x.view(-1, self.num_flat_features(x))
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x

def num_flat_features(self, x):
size = x.size()[1:] # 除去批处理维度的其他所有维度
num_features = 1
for s in size:
num_features *= s
return num_features


net = Net()
print(net)

torch.optim 优化

优化算法分为两大类:

(1)一阶优化算法
使用各个参数的梯度值来更新参数,最常用的是梯度下降。梯度下降的功能是通过寻找最小值,控制方差,更新模型参数,最终使模型收敛,网络的参数更新公式:

w=wηLww = w - \eta \frac{\partial L}{\partial w}

其中,η\eta是学习率,Lw\frac{\partial L}{\partial w}是损失函数关于参数ww的梯度。

(2)二阶优化算法
二阶优化算法使用了二阶导数(Hessian方法)来最小化或最大化损失函数,主要是基于牛顿法:

w=wηH1Lww = w - \eta H^{-1} \frac{\partial L}{\partial w}

其中,HH是损失函数关于参数ww的Hessian矩阵。

1
2
# 优化器(SGD)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)

模型的保存和加载

1
2
3
4
5
# 保存完整模型
torch.save(model,path)

# 保存模型的状态
torch.save(model.state_dict(),path)
1
2
3
4
5
# 加载完整模型
model = torch.load(path)

# 加载模型的状态,需要先定义模型结构
model.load_state_dict(torch.load(path))