Pytorch

Pytorch和Tensorflow是两个在深度学习领域占绝对主流地位的框架
两者互有优势,例如pytorch采用动态计算图,在某些情况如RNN的time step上更灵活,而tensorflow在分布式训练上能力更强
总的来说就是两者完全靠个人选择,个人认为还是pytorch更易于理解些

线性回归

线性回归即在二维平面上给定若干个点,求解其线性关系
线性回归问题非常适合加深对神经网络基本理论的理解以及入门任意一个深度学习框架
下面就以这个例子作为Pytorch(1.8.2)的入门

构造数据集

首先利用numpy构造数据集,可视化如下图所示

1
2
3
4
5
6
7
8
9
10
def createData():
X = np.linspace(-1, 1, 200)
np.random.shuffle(X)
Y = 0.5 * X + 2 + np.random.normal(0, 0.05, (200,))

X = np.expand_dims(X, 1) # 使X.shape为(200, 1),表示200个一维数据
Y = np.expand_dims(Y, 1)
return X, Y

X, Y = createData()

2_point

然后将numpy对象转换为pytorch运算所要求的Tensor(张量)对象

1
2
3
4
5
import torch

# from_numpy将ndarray转换为Tensor,to转换数据类型
X_ten = torch.from_numpy(X).to(torch.float32)
Y_ten = torch.from_numpy(Y).to(torch.float32)

网络搭建

我们已知线性回归的目标函数形如$y=wx+b$
令w对应神经元的连接权,b对应偏置,易知只需要输入层和输出层各一个神经元

pytorch搭建网络需要构造一个继承torch.nn.Module的类
覆写forward方法定义自己的前向传播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import torch
import torch.nn as nn

''' 网络对象,需继承torch.nn.Module'''
class Regressor(torch.nn.Module):
def __init__(self):
super().__init__()

'''定义网络需要的层'''
# 线性全连接层,两个参数分别表示 输入维度、输出维度 (也即上一层和当前层神经元个数)
self.layer = nn.Linear(1, 1)

'''定义前向传播'''
def forward(self, input):
output = self.layer(input)
return output

regressor = Regressor() # 实例化网络

搭建好网络后定义optimizer和损失函数

1
2
3
4
5
6
from torch.optim import SGD
import torch.nn as nn

optimizer = SGD(regressor.parameters(), lr=0.2)
# 学习率为0.2的SGD,regressor.parameters()表示需要应用梯度更新的参数
loss_func = nn.MSELoss() # 均方误差

网络训练与测试

之后开始训练网络(此处没有使用mini-batch学习)

1
2
3
4
5
6
7
8
for _ in range(100):  # 训练次数
pred = regressor(X_ten) # 正向传播

loss = loss_func(pred, Y_ten) # 计算误差

optimizer.zero_grad() # 清空上一步的残余更新参数值
loss.backward() # 反向传播, 计算各层梯度
optimizer.step() # 将梯度更新应用到optimizer对应的参数上

接下来可以看看网络训练的效果了
我们可以使用模型对象的state_dict()方法得到各层参数的字典

1
2
3
4
5
6
7
# regressor.state_dict()
# >>> OrderedDict([('layer.weight', tensor([[0.4949]])), ('layer.bias', tensor([2.0030]))])

w = regressor.state_dict()['layer.weight'] # 获取训练后得到的w,b
b = regressor.state_dict()['layer.bias']

Y_pred = w * X + b

或者直接进行一次前向传播
注意这里因为Y_pred需要计算梯度,若直接转换回numpy将会破坏计算图,pytorch禁止了这种做法
因此这里调用了detach方法,目的是将tensor强制剥离计算图,注意剥离后内存仍指向原址

1
2
Y_pred = regressor(X_ten)
Y_pred = Y_pred.detach().numpy()

最后进行结果可视化

1
2
3
4
# 结果可视化
plt.scatter(X, Y)
plt.plot(X, Y_pred, color='red')
plt.show()

2_regress

完整代码

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import numpy as np
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
from torch.optim import SGD

def createData():
X = np.linspace(-1, 1, 200)
np.random.shuffle(X)
Y = 0.5 * X + 2 + np.random.normal(0, 0.05, (200,))

X = np.expand_dims(X, 1)
Y = np.expand_dims(Y, 1)

return X, Y

'''定义网络'''
class Regressor(torch.nn.Module):
def __init__(self):
super().__init__()

'''定义网络需要的层'''
self.layer = nn.Linear(1, 1) # 线性全连接层,Linear(in_features, out_features, bias=True)

'''定义前向传播'''
def forward(self, input):
output = self.layer(input)
return output

X, Y = createData()
X_ten = torch.from_numpy(X).to(torch.float32)
Y_ten = torch.from_numpy(Y).to(torch.float32) # Linear只支持float32

regressor = Regressor() # 实例化网络

optimizer = SGD(regressor.parameters(), lr=0.2) # 定义优化器
loss_func = nn.MSELoss() # 定义误差计算公式

'''训练'''
for _ in range(100):
pred = regressor(X_ten) # 预测

loss = loss_func(pred, Y_ten) # 计算误差

optimizer.zero_grad() # 清空上一步的残余更新参数值
loss.backward() # 反向传播, 计算梯度
optimizer.step() # 将梯度更新应用到optimizer对应的参数上

'''获取训练后的网络参数'''
# .state_dict()返回各层参数的字典
# >>> OrderedDict([('layer.weight', tensor([[0.4949]])), ('layer.bias', tensor([2.0030]))])

w = regressor.state_dict()['layer.weight']
b = regressor.state_dict()['layer.bias']

Y_pred = w * X + b
# Y_pred = regressor(X_ten)
# Y_pred = Y_pred.detach().numpy()

# 结果可视化
plt.scatter(X, Y)
plt.plot(X, Y_pred, color='red')
plt.show()

分类

获取Mnist数据集及预处理

mnist是一个经典的手写数字图像数据集,torchvision已内置了mnist
包含大小为60000训练集和大小为10000的测试集
其中的图片尺寸均为28x28,且为单通道灰度图像

调用torchvision内置的mnist时可同时作一系列预处理
获取mnist数据后将其送入torch的数据迭代器中方便后续训练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import torch.utils.data as Data
import torchvision

def getData():
data_train = torchvision.datasets.MNIST(
root='./data/', # 数据保存位置
train=True, # 使用训练数据or测试数据
transform=torchvision.transforms.ToTensor(), # 将PIL或numpy转换为Tensor(送入dataloader后自动归一化到[0,1])
download=True, # 是否下载(若已下载则忽略)
)

data_test = torchvision.datasets.MNIST(root='./data/', train=False)

return data_train, data_test

data_train, data_test = getData()
loader = Data.DataLoader(dataset=data_train, batch_size=32, shuffle=True)
# 数据迭代器,shuffle=True则每个epoch后随机打乱数据集

网络搭建

这里使用了pytorch的nn.Sequential类搭建网络,也即顺序模型
顾名思义,其中的前向传播将按照层的传参顺序执行

网络中第一个全连接层后使用了relu激活
输出层神经元有十个是因为分类任务需要输出one-hot编码标签
例如标签5转化为one-hot为[0, 0, 0, 0, 0, 1, 0, 0, 0, 0]

同时这里使用了一个新的optimizer,名称为Adam
它使用了动量思想和动态学习率,是现在比较常用的optimizer

因为是多分类任务,loss函数使用了交叉熵损失
这个损失要求输出值为one-hot编码,且网络最后一层要有softmax激活
但pytorch中使用交叉熵时会帮我们自动完成这两个要求,因此网络定义中没有softmax

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import torch
import torch.nn as nn
from torch.optim import Adam

class Classifier(nn.Module):
def __init__(self):
super().__init__()

self.model = nn.Sequential(
nn.Flatten(),
nn.Linear(784, 32),
nn.ReLU(),
nn.Linear(32, 10)
)

'''定义前向传播'''

def forward(self, input):
output = self.model(input)
return output

classifier = Classifier()
optimizer = Adam(classifier.parameters(), lr=1e-3)
loss_func = nn.CrossEntropyLoss() # 使用交叉熵损失时会自动将label转化为one-hot编码,同时也自动对输入求softmax

网络训练与测试

和线性回归的例子不同,这里由于数据量大而采用了mini-batch学习
dataloader就是为了方便获取小批数据而存在的

1
2
3
4
5
6
7
8
9
for epoch in range(1):
for step, (X, Y) in enumerate(loader): # 获取一个batch的数据
pred = classifier(X)
loss = loss_func(pred, Y)
optimizer.zero_grad()
loss.backward()
optimizer.step()

print('Epoch:', epoch+1, ' Step:', step, ' loss:', loss.item())

最后验证网络效果
首先将下载来的mnist测试集转换为Tensor对象
这里torch.unsqueeze用于在增加某一维度

1
2
test_X = torch.unsqueeze(data_test.data, dim=1).type(torch.float32) / 255.  
test_Y = data_test.targets

然后测试

1
2
3
4
5
output = classifier(test_X)
label_pred = torch.argmax(output, dim=1) # 按行求最大值索引

print('accuracy:', np.sum((test_Y==label_pred).tolist()) / test_Y.shape[0])
# >>> accuracy: 0.9299

启动GPU加速

随着数据量越来越大,CPU计算神经网络会越来越吃力
在上面代码中加入以下部分可利用GPU加速神经网络的计算

1
2
3
4
5
cuda_on = torch.cuda.is_available()  # 获取当前GPU加速是否可用

if cuda_on: # 开启GPU模式
classifier.cuda()
loss_func.cuda()
1
2
3
4
5
6
7
8
9
10
11
12
13
for epoch in range(1):
for step, (X, Y) in enumerate(loader):
if cuda_on: # 开启GPU模式
X = X.cuda()
Y = Y.cuda()

pred = classifier(X)
loss = loss_func(pred, Y)
optimizer.zero_grad()
loss.backward()
optimizer.step()

print('Epoch:', epoch+1, ' Step:', step, ' loss:', loss.item())
1
2
3
if cuda_on:  # 开启GPU模式
test_X = test_X.cuda()
test_Y = test_Y.cuda()

完整代码

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import torch
import torch.nn as nn
from torch.optim import Adam
import torch.utils.data as Data
import torchvision
import numpy as np

class Classifier(nn.Module):
def __init__(self):
super().__init__()

self.model = nn.Sequential(
nn.Flatten(),
nn.Linear(784, 32),
nn.ReLU(),
nn.Linear(32, 10)
)

'''定义前向传播'''

def forward(self, input):
output = self.model(input)
return output

def getData():
data_train = torchvision.datasets.MNIST(
root='./data/', # 数据保存位置
train=True, # 使用训练数据or测试数据
transform=torchvision.transforms.ToTensor(), # 将PIL或numpy转换为Tensor,送入dataloader后自动归一化到[0,1]
download=True, # 是否下载(若已下载则忽略)
)

data_test = torchvision.datasets.MNIST(root='./data/', train=False)

return data_train, data_test


cuda_on = torch.cuda.is_available()

classifier = Classifier()
optimizer = Adam(classifier.parameters(), lr=1e-3)
loss_func = nn.CrossEntropyLoss() # 使用交叉熵损失时会自动将label转化为one-hot编码,同时也自动对输入求softmax

if cuda_on: # 开启GPU模式
classifier.cuda()
loss_func.cuda()

data_train, data_test = getData()
loader = Data.DataLoader(dataset=data_train, batch_size=32, shuffle=True)

for epoch in range(1):
for step, (X, Y) in enumerate(loader):
if cuda_on: # 开启GPU模式
X = X.cuda()
Y = Y.cuda()

pred = classifier(X)
loss = loss_func(pred, Y)
optimizer.zero_grad()
loss.backward()
optimizer.step()

print('Epoch:', epoch+1, ' Step:', step, ' loss:', loss.item())

'''测试'''
test_X = torch.unsqueeze(data_test.data, dim=1).type(torch.float32) / 255.
test_Y = data_test.targets

if cuda_on: # 开启GPU模式
test_X = test_X.cuda()
test_Y = test_Y.cuda()

output = classifier(test_X)
label_pred = torch.argmax(output, dim=1) # 按行求最大值索引

# print(test_Y)
# print(label_pred)
print('accuracy:', np.sum((test_Y==label_pred).tolist()) / test_Y.shape[0])

Pytorch的其他常用功能

模型保存

1
2
3
4
5
torch.save(regressor, 'net.pkl')  # 保存整个网络
torch.save(regressor.state_dict(), 'net_params.pkl') # 只保存参数,需要先实例化完全相同(包括层名称/参数等)的网络对象

regressor = torch.load('net.pkl') # 读取整个网络
regressor.load_state_dict('net_params.pkl') # 只读取参数