[入门]3. 神经网络

神经网络

我们一般使用torch.nn模块来构建一个神经网络。

1
2
3
4
import torch
import torch.nn as nn
from sklearn import datasets
import numpy as np
1
X, y = datasets.load_breast_cancer(return_X_y=True)

Pytorch要求训练的数据必须是float类型,导入的X是np.float64也就是double类型。

1
2
3
4
5
6
7
X = X.astype(np.float32)
# MSE要求y必须是float
y = y.astype(np.float32)
# 神经网络的输入是二维的,要保持一致
y = np.reshape(y, (-1, 1))
X = torch.from_numpy(X)
y = torch.from_numpy(y)
1
X.shape
torch.Size([569, 30])

1. 构建一个神经网络

1
2
3
4
5
6
7
8
9
10
11
12
13
class MLPNet(nn.Module):
def __init__(self, inDim, hidDim, outDim):
super(MLPNet, self).__init__()
# y = Wx + b
self.hidLayer = nn.Linear(inDim, hidDim, bias=True)
self.outLayer = nn.Linear(hidDim, outDim, bias=True)

def forward(self, X):
z1 = self.hidLayer(X)
a1 = torch.tanh(z1)
z2 = self.outLayer(a1)
a2 = torch.sigmoid(z2)
return a2
1
2
feature = X.shape[1]
net = MLPNet(feature, 25, 1)
1
output = net(X)
1
print(output[0:5, :])
tensor([[0.2746],
        [0.2853],
        [0.2849],
        [0.2925],
        [0.3158]], grad_fn=<SliceBackward>)

2. Loss 函数

1
loss = nn.MSELoss()
1
2
lossVal = loss(output, y)
print(lossVal)
tensor(0.2712, grad_fn=<MseLossBackward>)

3. 反向传播

实现了前向传播,有了损失函数,我们就可以进行反向传播了。其实很简单:

1
2
3
4
net.zero_grad()
lossVal = loss(output, y)
lossVal.backward()
# 然后更新参数

要注意zero_grad这一条,绝对不可以漏了。因为根据pytorch中的backward()函数的计算,当网络参量进行反馈时,梯度是被积累的而不是被替换掉;但是在每一个batch时毫无疑问并不需要将两个batch的梯度混合起来累积,因此这里就需要每个batch设置一遍zero_grad

1
net.zero_grad()
1
lossVal.backward()
1
print(net.hidLayer.bias.grad)
tensor([ 0.0000e+00,  0.0000e+00,  0.0000e+00,  2.6967e-06, -8.7211e-05,
         0.0000e+00,  0.0000e+00,  0.0000e+00,  0.0000e+00,  0.0000e+00,
         0.0000e+00,  0.0000e+00,  8.8216e-06,  0.0000e+00,  0.0000e+00,
         0.0000e+00,  0.0000e+00,  0.0000e+00,  4.9632e-12,  0.0000e+00,
        -3.9861e-05,  3.5545e-04,  0.0000e+00,  0.0000e+00, -5.6574e-03])

4. 更新参数

有了grad,就可以更新参数了: \[ \theta = \theta - \alpha \cdot \frac{dJ}{d\theta} \] 关于更新参数有很多种方法,最简单的当然是手动更新了:

1
2
3
learning_rate = 0.01
for f in net.parameters():
f.data.sub_(f.grad.data * learning_rate)

但不建议这么做,在torch.optim中实现了很多更新的算法如SGD, Nesterov-SGD, Adam, RMSProp等等。一般选择 Adam 是最好的。 使用的方法如下:

  1. 首先构建一个Optimizer对象: opt = optim.XXX(net.parameters, lr=...)
  2. 在需要更新的时候调用 opt.step() 更新一次参数。
1
2
import torch.optim as optim
optimizer = optim.SGD(net.parameters(), lr=1e-3)
1
2
# 更新参数
optimizer.step()

另外说明一下,opt有一个.zero_grad方法,它的效用和net.zero_grad()是一样的,在每次backward之前都必须调用一次zero_grad

5. 总流程

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
model = MLPNet(feature, 25, 1)
# 定义损失函数
criterion = nn.MSELoss()
# 迭代次数
epoches = 1000
# Adam比SGD好很多
optimiser = optim.Adam(model.parameters(), lr=1e-3)
# 开启训练模式,一般没什么用,但是在有如Dropout层时就会起作用
# 处于良好的习惯,最好还是写上
model.train()
# 开始迭代训练
for i in range(1, epoches + 1):
# forward
out = model(X)
# 损失
loss = criterion(out, y)
# 梯度清零, 一定要在backward之前
model.zero_grad()
# 反向传播
loss.backward()
# 更新清零
optimiser.step()
if i % 100 == 0:
msg = 'Iter: {0}, Loss = {1}'.format(i, loss.item())
print(msg)
Iter: 100, Loss = 0.17146091163158417
Iter: 200, Loss = 0.10728909820318222
Iter: 300, Loss = 0.08248171955347061
Iter: 400, Loss = 0.06832777708768845
Iter: 500, Loss = 0.05633820965886116
Iter: 600, Loss = 0.0479607917368412
Iter: 700, Loss = 0.04309488832950592
Iter: 800, Loss = 0.03938402235507965
Iter: 900, Loss = 0.036379750818014145
Iter: 1000, Loss = 0.03386558219790459

看一下效果如何:

1
2
3
4
5
6
7
8
9
with torch.no_grad():
# 打开预测模式,这里其实没卵用,但是是个好习惯
model.eval()
out = model(X)
res = (out > 0.5).to(dtype=torch.int)
y = y.to(dtype=torch.int)
eq = torch.sum(y == res).item()
l = len(res)
print('准确率:', eq / l)
准确率: 0.9525483304042179

注意到我们使用了:

1
2
with torch.no_grad():
model.eval()

调用no_grad()会构建一个上下文环境,在这个块当中进行计算时不会将梯度记录下来,从而大大提高运行速度,节省内存

eval则是对应了train。在当你需要用到 dropout 或者 batch normalization 的时候,调用 train 方法可以在 forward 的时候打开 dropout 或归一化操作,而eval则会关闭。