模型创建与nn.Module

1. 网络模型创建步骤

前几次已经学完了数据部分,现在开始学习模型部分。

image-20200727224448921

模型又分为模型创建和权值初始化。其中,模型创建还可以进一步分为构建网络层和拼接网络层。这些工作都可以用nn.Module来完成。

image-20200727204822556

本节我们用7层结构的LeNet来学习模型创建和nn.Module。

image-20200727204943120

实验

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import os
import random
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
import torchvision.transforms as transforms
import torch.optim as optim
from matplotlib import pyplot as plt
from model.lenet import LeNet
from tools.my_dataset import RMBDataset


def set_seed(seed=1):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)


set_seed() # 设置随机种子
rmb_label = {"1": 0, "100": 1}

# 参数设置
MAX_EPOCH = 10
BATCH_SIZE = 16
LR = 0.01
log_interval = 10
val_interval = 1

# ============================ step 1/5 数据 ============================

split_dir = os.path.join("data", "rmb_split")
train_dir = os.path.join(split_dir, "train")
valid_dir = os.path.join(split_dir, "valid")

norm_mean = [0.485, 0.456, 0.406]
norm_std = [0.229, 0.224, 0.225]

train_transform = transforms.Compose([
transforms.Resize((32, 32)),
transforms.RandomCrop(32, padding=4),
transforms.ToTensor(),
transforms.Normalize(norm_mean, norm_std),
])

valid_transform = transforms.Compose([
transforms.Resize((32, 32)),
transforms.ToTensor(),
transforms.Normalize(norm_mean, norm_std),
])

# 构建MyDataset实例
train_data = RMBDataset(data_dir=train_dir, transform=train_transform)
valid_data = RMBDataset(data_dir=valid_dir, transform=valid_transform)

# 构建DataLoder
train_loader = DataLoader(dataset=train_data, batch_size=BATCH_SIZE, shuffle=True)
valid_loader = DataLoader(dataset=valid_data, batch_size=BATCH_SIZE)

# ============================ step 2/5 模型 ============================
net = LeNet(classes=2)
# print(net)
net.initialize_weights()

# ============================ step 3/5 损失函数 ============================
criterion = nn.CrossEntropyLoss() # 选择损失函数

# ============================ step 4/5 优化器 ============================
optimizer = optim.SGD(net.parameters(), lr=LR, momentum=0.9) # 选择优化器
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1) # 设置学习率下降策略

# ============================ step 5/5 训练 ============================
train_curve = list()
valid_curve = list()

for epoch in range(MAX_EPOCH):

loss_mean = 0.
correct = 0.
total = 0.

net.train()
for i, data in enumerate(train_loader):

# forward
inputs, labels = data
outputs = net(inputs)

# backward
optimizer.zero_grad()
loss = criterion(outputs, labels)
loss.backward()

# update weights
optimizer.step()

# 统计分类情况
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).squeeze().sum().numpy()

# 打印训练信息
loss_mean += loss.item()
train_curve.append(loss.item())
if (i+1) % log_interval == 0:
loss_mean = loss_mean / log_interval
print("Training:Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format(
epoch, MAX_EPOCH, i+1, len(train_loader), loss_mean, correct / total))
loss_mean = 0.

scheduler.step() # 更新学习率

# validate the model
if (epoch+1) % val_interval == 0:

correct_val = 0.
total_val = 0.
loss_val = 0.
net.eval()
with torch.no_grad():
for j, data in enumerate(valid_loader):
inputs, labels = data
outputs = net(inputs)
loss = criterion(outputs, labels)

_, predicted = torch.max(outputs.data, 1)
total_val += labels.size(0)
correct_val += (predicted == labels).squeeze().sum().numpy()

loss_val += loss.item()

valid_curve.append(loss_val)
print("Valid:\t Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format(
epoch, MAX_EPOCH, j+1, len(valid_loader), loss_val, correct / total))


train_x = range(len(train_curve))
train_y = train_curve

train_iters = len(train_loader)
valid_x = np.arange(1, len(valid_curve)+1) * train_iters*val_interval # 由于valid中记录的是epochloss,需要对记录点进行转换到iterations
valid_y = valid_curve

plt.plot(train_x, train_y, label='Train')
plt.plot(valid_x, valid_y, label='Valid')

plt.legend(loc='upper right')
plt.ylabel('loss value')
plt.xlabel('Iteration')
plt.show()

# ============================ inference ============================

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
test_dir = os.path.join(BASE_DIR, "test_data")

test_data = RMBDataset(data_dir=test_dir, transform=valid_transform)
valid_loader = DataLoader(dataset=test_data, batch_size=1)

for i, data in enumerate(valid_loader):
# forward
inputs, labels = data
outputs = net(inputs)
_, predicted = torch.max(outputs.data, 1)

rmb = 1 if predicted.numpy()[0] == 0 else 100
print("模型获得{}元".format(rmb))
  1. 设置断点,进去LeNet

    image-20200727220912817

  2. 跳转到lenet.py的class LeNet(nn.Module)__init__(self, classes)内,构建网络层(如卷积层)就是在模型的__init__中完成的。

    image-20200727221123998

  3. 单步运行后会跳出__init__回到主函数,完成了模型的初始化。我们再设置断点,运行至断点处。

    image-20200727221659818

  4. 进入net(inputs),跳转到module.py的class Module(object)__call__函数。运行到self.forward,进入。

    image-20200727222338701

  5. 跳转到lenet.py的class LeNet(nn.Module)forward(self, x)内。单步运行,最后会返回out。

    image-20200727222242664

  6. 然后会返回到module.py,得到result,最后返回result。从下图可知,前向传播计算得到result靠的就是self.forward,也就是你自定义网络中的forward函数。

    image-20200727222751404

  7. 单步运行,返回result后跳回主程序。返回的result就是我们一次迭代计算的output。

    image-20200727223113685

于是我们知道了,首先定义一个模型需要继承nn.module,然后重写__init____forward__函数。其中,__init__构建网络层,在模型初始化时需要运行__init__函数;在数据前向传播过程中,需要用到__forward__函数,因此需要在__forward__函数中拼接网络层。

image-20200727215859011

2. nn.Module属性

所有的模型都是nn.Module的子类,就像在数据部分中,我们自定义的数据集都得继承dataset这个类一样。在本节的例子中,LeNet就需要继承nn.Module,因此LeNet也是nn.Module类的,包括LeNet中的网络层也是nn.Module类。

下图展示了pytorch的神经网络包torch.nn,本节重点研究nn.Module

image-20200727224518359

nn.module类中,有8个有序字典属性用于管理模型。这里我们只需要关注parameters和modules。

image-20200727224904004

下面我们观察nn.module的创建以及属性管理的机制。

实验

  1. 设置断点,进去LeNet

    image-20200727220912817

  2. 可以看到,LeNet是继承于nn.Module的子类,进入super(LeNet, self).__init__()

    image-20200727225311640

  3. 跳转到module.py的class Module(object)__init__(self)中。可以看到,在这里进行了8个有序字典属性的初始化。

    image-20200727225603035

  4. 跳出函数,回到lenet.py,可以看到LeNet类的初始化完成,已经具有了nn.Module的8个属性,现在暂时还是空的。

    image-20200727230207259

  5. 进入nn.Conv2d(3, 6, 5)观察网络层的构建。

    image-20200727230341772

  6. 跳转到conv.py的class Conv2d(_ConvNd)__init__函数。Conv2d是继承于_ConvNd的。运行到super(Conv2d, self).__init__,然后进入。

    image-20200727230618259

  7. 进入会跳转到utils.py,跳出再进入就能到达conv.py的class _ConvNd中,可以发现_ConvNd也是继承于nn.Module的,所以网络层也都是nn.Module的子类。

    image-20200727231123064

  8. 进入super(_ConvNd, self).__init__,就又跳转到module.py的class Module(object)__init__(self)中,这里进行了8个有序字典属性的初始化。跳出函数,回到lenet.py的__init__,此时已经完成了conv1网络层的初始化。

    image-20200727225603035

  9. 现在再看LeNet类的属性,发现_modules多了一个元素,就是我们刚刚加入的网络层conv1。键是conv1,值是nn.Conv2d(3, 6, 5)

    image-20200728003854943

  10. conv1同样也是nn.Module的子类,也有这8个属性,我们同样可以查看。发现_parameters有元素,而_modules没有。这是因为conv1没有嵌套的网络层了,并且conv1有许多的可训练参数。而_parameters是继承于tensor类的,所以有tensor的属性。

    image-20200728005036248

  11. 当lenet.py的__init__全部完成以后,跳转回主函数可以发现,全部网络层都已添加到属性_modules中。

    image-20200728012348636

3. nn.Module总结

  • 一个module可以包含多个子module

  • 一个module相当于一个运算,必须实现forward()函数

  • 每个module都有8个有序字典管理它的属性