快速上手深度学习项目(三)

Yizumi Konata Lv3

全连接神经网络(pytorch)

pytorch以及更底层的conda、cuda怎么安装就不介绍了,要说我也不是很装的来,反正有什么问题就上网搜吧。

在PyTorch中,所有参与运算的张量都用同一个类表示,其类型名叫做Tensor,也就是torch.Tensor。而在构建张量时,我们一般要用torch.tensor这个函数,其可以传入一个列表。Tensornp.ndarray用法几乎一模一样。两者之间可以互相转换

1
2
3
##  下面两种方法都不创建新的内存空间
array2tensor=torch.from_numpy(numpy_data)#numpy array->torch tensor,其参数必须是数组形式
tensor2array=torch_data.numpy() #torch tensor->numpy array

这里有一个要注意的点是torch.Tensor是一个类型名,意味着他也可以是一个构造函数,而torch.tensortorch.from_numpy等是一个工厂函数。构造函数在构造一个张量时的类型使用全局默认值,而工厂函数则根据输入推断数据类型,所以创建tensor建议统一使用torch.tensor,这会创建一块内存区域,如果不想创建内存区域的话可以使用torch.as_tensor

那既然pytorch的核心数据结构与numpy这么像,为什么还要pytorch呢?这个问题的答案网上一搜一大堆,最根本的点在于pytorch会维护计算图,并根据为每一个(需要梯度计算的)Tensor自动计算梯度,这也就是我们为什么说他是框架,框架就是干一些脏活累活的嘛,就像你用springboot的时候就几乎不需要你手动监听管理socket了,你使用pytorch,就不需要自己动手计算梯度了。

用pytorch实现多分类的神经网络(矩阵乘)

其实这一节只是将ndarray换成了Tensor,其他几乎没区别,本节作者使用的是点集分类任务,我这边直接贴作者的代码,然后在关键地方解释一下

本项目中,我们要用到一个平面点数据集。在平面上,有三种颜色不同的点。我们希望用PyTorch编写的神经网络能够区分这三种点。这个点是作者随机生成的,有兴趣可以看源代码 。使用下面的代码生成400个点,x的形状为[2,400],y的形状为[1,400]

1
train_X, train_Y = generate_points(400)

在计算损失函数时,PyTorch默认标签Y是一个一维整形数组。而我们之前都会把Y预处理成[1, m]的张量。因此,这里要先做一个维度转换,再转张量。当然你先转张量,然后再调用squeeze(0)也是一样的,Tensor的删除单维度的方法也是squeeze,并且也可以用方法和全局函数两种方式调用,即t.squeeze()torch.squeeze(t)是一样的。不过这里提一点,Tensor增加维度的函数名称叫做unsqueeze而不是numpy的expand_dim

1
train_Y_pt = torch.tensor(train_Y.squeeze(0), dtype=torch.long)

然后定义网络。初始化函数如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MulticlassClassificationNet():
def __init__(self, neuron_cnt: List[int]):
self.num_layer = len(neuron_cnt) - 1
self.neuron_cnt = neuron_cnt
self.W = []
self.b = []
for i in range(self.num_layer):
new_W = torch.empty(neuron_cnt[i + 1], neuron_cnt[i]) #torch.empty创建未初始化的Tensor
new_b = torch.empty(neuron_cnt[i + 1], 1)
torch.nn.init.kaiming_normal_(new_W, nonlinearity='relu')#torch内置He Initialization函数,
torch.nn.init.kaiming_normal_(new_b, nonlinearity='relu')
#torch.nn.Parameter将传入的参数设置为可学习更新的参数,也就是让new_W和new_b参与梯度的计算
#同时将张量注册为模型的参数
#你可以理解为torch.nn.Parameter就是将参数设置为需要计算梯度的Tensor
self.W.append(torch.nn.Parameter(new_W))
self.b.append(torch.nn.Parameter(new_b))
self.trainable_vars = self.W + self.b
self.loss_fn = torch.nn.CrossEntropyLoss()#torch内置交叉熵损失函数,

接下来编写前向传播函数和损失函数

1
2
3
4
5
6
7
8
9
10
11
12
13
def forward(self, X):
A = X
for i in range(self.num_layer):
Z = torch.matmul(self.W[i], A) + self.b[i]
if i == self.num_layer - 1:
A = F.softmax(Z, 0)
else:
A = F.relu(Z)

return A

def loss(self, Y, Y_hat):
return self.loss_fn(Y_hat.T, Y)

注意前向传播函数里面不需要缓存那些计算梯度用到的变量了,这些脏活pytorch会帮我们做的,这个F是作者导入的torch.nn.functional,我代码是直接抄的,保留作者的写法。常用的一些函数在torch.nn.functional中基本都实现了,可以直接调用。

关于这个torch.nn.functionaltorch.nn两个模块,其实也可以说道说道,基本上torch.nn中有的模块,torch.nn.functional中都有其函数实现,通常情况下torch.nn用于创建可学习的模块,也就是神经网络的参数,torch.nn.functional只是参与一下计算,里面没有可学习的参数。具体的区别有兴趣可以去搜一下。

最后是训练代码

1
2
3
4
5
6
7
8
9
10
11
12
13
def train(model: MulticlassClassificationNet,
X,
Y,
step,
learning_rate,
print_interval=100):
optimizer = torch.optim.Adam(model.trainable_vars, learning_rate)
for s in range(step):
Y_hat = model.forward(X)
cost = model.loss(Y, Y_hat)
optimizer.zero_grad()
cost.backward()
optimizer.step()

对比上一节的训练代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for e in range(total_epoch):
for mini_batch_X, mini_batch_Y in mini_batch_XYs:
mini_batch_Y_hat = model.forward(mini_batch_X)
model.backward(mini_batch_Y)
optimizer.zero_grad()
optimizer.add_grad(model.get_grad_dict())
optimizer.step()

currrent_epoch = optimizer.epoch

if currrent_epoch % print_interval == 0:
# print loss
...
optimizer.increase_epoch()

可以看到整体流程是差不多的,对每一步(当然这里没有batch了,引入batch无非就是你循环改一下,不影响训练流程)先前向传播,然后反向得到梯度,最后使用优化器更新参数,不过pytorch的写法里梯度是通过loss回传的,这样写我认为更直观。

执行完cost = model.loss(Y, Y_hat),整个计算图就已经构造完成了。我们调用optimizer.zero_grad()清空优化器,用cost.backward()自动完成反向传播并记录梯度,之后用optimizer.step()完成一步梯度下降。

主函数里的训练代码就很简单了,不多说

1
2
3
4
n_x = 2
neuron_list = [n_x, 10, 10, 3]
model = MulticlassClassificationNet(neuron_list)
train(model, train_X_pt, train_Y_pt, 5000, 0.001, 1000)

用pytorch实现多分类的神经网络(模块化)

上面用pytorch,使用numpy手搓的思路实现了一个多分类神经网络。实际上我们一般写pytorch的思路并不是抄公式,而是使用现成的模块搭积木。比如上面的人物,不就是要搭建len(neuron_cnt) - 1个线性层吗,所以正常人写的代码应该是下面的

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
import torch
import torch.nn as nn
import torch.nn.functional as F

class MulticlassClassificationNet(nn.Module):
def __init__(self, neuron_cnt: list[int]):
super(MulticlassClassificationNet, self).__init__()
layers = []
self.num_layer = len(neuron_cnt) - 1
# 创建线性层
for i in range(self.num_layer):
layers.append(nn.Linear(neuron_cnt[i], neuron_cnt[i + 1]))
#包装为一个nn.ModuleList,方便管理(比如直接注册,不需要torch.nn.Parameter,方便pytorch对参数序列化,不需要我们手动遍历列表,方便统一将参数移动到某个设备上等等)
self.layers = nn.ModuleList(layers)
self.loss_fn = nn.CrossEntropyLoss()

def forward(self, X):
A = X
for i, layer in enumerate(self.layers):
A = layer(A)
if i == self.num_layer - 1:
A = F.softmax(A, dim=1)
else:
A = F.relu(A)
return A

def loss(self, Y, Y_hat):
return self.loss_fn(Y_hat, Y)

def evaluate(self, X, Y, return_loss=False):
Y_hat = self.forward(X)
Y_hat_predict = torch.argmax(Y_hat, dim=1)
accuracy = (Y == Y_hat_predict).float().mean()
if return_loss:
loss = self.loss(Y, Y_hat)
return accuracy, loss
else:
return accuracy

def train(model: MulticlassClassificationNet, X, Y, step, learning_rate, print_interval=100):
optimizer = torch.optim.Adam(model.parameters(), learning_rate)
for s in range(step):
Y_hat = model(X)
cost = model.loss(Y, Y_hat)
optimizer.zero_grad()
cost.backward()
optimizer.step()
if s % print_interval == 0:
accuracy, loss = model.evaluate(X, Y, return_loss=True)
print(f'Step: {s}')
print(f'Accuracy: {accuracy.item()}')
print(f'Train loss: {loss.item()}')
#保存模型
torch.save(model.state_dict(), save_path)

这里只用了最简单的线性层,其只需要输入和输出向量的维度即可。其实对于其他的模块,最重要的参数都是输入和输出的形状。pytorch中预先实现了很多神经网络模块,他们都是nn.Module的子类,我们自定义的神经网络模块也应该继承自nn.Module

  • Title: 快速上手深度学习项目(三)
  • Author: Yizumi Konata
  • Created at : 2024-08-06 16:49:23
  • Updated at : 2024-09-05 15:08:06
  • Link: https://zz12138zz.github.io/2024/08/06/dl3/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments