我们在学一门编程语言的时候,写的第一个程序往往是hello world。同样,手写数字识别是机器学习中,最入门的项目。

完整代码在文末,中间是代码每个部分的分析

机器学习的基本思路

我们需要两个东西:

  • 数据集
  • 模型

调用的包

1
2
3
4
5
import torch
from torch import nn, optim
from torch.autograd import Variable
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

三层全连接神经网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
"""
定义了一个简单的三层全连接神经网络,每一层都是线性的
"""
class simpleNet(nn.Module):
"""
初始化函数
:param param1: 类实例对象本身
:param param2: 输入层
:param param3: 隐藏层
:param param4: 输出层
"""
def __init__(self, in_dim, n_hidden_1, out_dim):
super(simpleNet, self).__init__()
self.layer1 = nn.Sequential(nn.Linear(in_dim, n_hidden_1), nn.BatchNorm1d(n_hidden_1), nn.ReLU(True))
self.layer2 = nn.Sequential(nn.Linear(n_hidden_1, out_dim))

def forward(self, x):
x = self.layer1(x)
x = self.layer2(x)
return x
  • super(simpleNet, self)

    • 首先找到 simpleNet 的父类(就是类 nn.Module ).
    • 然后把类 simpleNet 的对象转换为类 nn.Module 的对象。
  • nn.Sequential

    • 一个有序的容器。
    • 在构建数个网络层之后会自动调用 forward()方法
    • 简单来说就是通过 Squential 将网络层和激活函数结合起来。
  • nn.Linear

    1
    2
    3
    4
    5
    6
    class torch.nn.Linear(in_features, out_features, bias=True)
    """
    :param param1: size of each input sample
    :param param2: size of each output sample
    :param param3: if set to False, the layer wiil not learn an additive bias. Default: True
    """
    • 用来设置神经网络中的全连接层。注意⚠️:全连接层的输入与输出都是二维张量。

    • 一般形状为[batch_size, size]

    • in_feature 指的是输入的二维张量大小。个人喜欢理解成输入层的神经元数目。
    • out_feature 指的是输出的二维张量大小。个人喜欢理解成输出层的神经元数目。
    • 改变形状的角度看,通过一个 nn.Linear 相当于把一个输入为 [batch_size, in_features] 的张量变换成 [batch_size, out_features] 的输出张量。

    nn_Linear_examplecode

    nn_Linear_result

  • nn.BatchNorm1d(n_hidden_1)

    • n_hidden_1 进行归一化
  • nn.ReLU(True)

    • 在运算过程中直接覆盖原变量(如果 False 就不直接覆盖)
    • 比如我们要实现对变量 x 的自增。如果是 True就相当于 x=x+1; ,如果是 False 就相当于 y=x+1;x=y;
    • 可见这样能够节省运算内存
  • forward(x)

    • 在后面的代码我们创建了一个神经网络模型 model=simpleNet(28*28,128,10),然后利用这个模型去训练图片 model(img)
    • 这里的model(img)会调用基类的 __call__方法,__call__ 方法又会调用类里面的 forward函数
    • 在这里,相当于我们重写了forward() 函数,使输入的 img 先经过了 layer1 ,再经过 layer2 , 最后被 return

超参数定义

1
2
3
4
# 定义一些超参数
BATCH_SIZE = 128
LEARNING_RATE = 0.02
NUM_EPOCHS = 20

以我们这个三层全连接神经网络为例,我们怎么确定隐藏层应该有几个节点呢?这里就需要我们在一堆模型中找出比较好的一款(比如隐藏层是10个节点的那款)。而这个10,其实也是我们定义的一种参数,但是他不会在学习的过程中被修改。就像是一种 框架参数 一样。所以我们把他们叫做超参数(超越参数的参数)。

  • BATCH_SIZE 我们把一堆数据打成一个包(batch),每次训练的时候就训练一整个包。这个参数是由于深度学习中尝使用SGD(随机梯度下降)产生。batch_size的大小取值和GPU内存会有关系,数值越大一次性载入数据越多,占用的GPU内存越多。适当增加 batch_size 能够增加训练速度和训练精度(因为梯度下降时震动较小),过小会导致模型收敛困难。
  • LEARNING_RATE 学习率,也就是每一轮学习后,对模型的修正程度,太大容易修正过头。
  • NUM_EPOCH 把所有的训练数据全部迭代遍历的次数。

数据预处理

1
2
3
4
# 数据预处理。transforms.ToTensor()将图片转换成PyTorch中处理的对象Tensor,并且进行标准化(数据在0~1之间)
# transforms.Normalize()做归一化。它进行了减均值,再除以标准差。两个参数分别是均值和标准差
# transforms.Compose()函数则是将各种预处理的操作组合到了一起
data_tf = transforms.Compose([transforms.ToTensor(), transforms.Normalize([0.5], [0.5])])

数据集下载

1
2
3
4
5
# 数据集的下载器
train_dataset = datasets.MNIST(root='./data', train=True, transform=data_tf, download=True)
test_dataset = datasets.MNIST(root='./data', train=False, transform=data_tf)
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)

选用的数据集是MNIST

选择模型

1
2
3
4
# 选择模型
model = simpleNet(28 * 28, 128, 10)
if torch.cuda.is_available():
model = model.cuda()

torch.cuda.is_available() 正如其名,判断GPU可不可以使用。如果可以就用GPU加速运算。

定义损失函数

1
2
3
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=LEARNING_RATE, weight_decay = 1e-4)
  • nn.CrossEntropyLoss() 交叉熵损失。公式为:

​ 后面就是一堆数学公式推导了,埋个烂坑吧,以后再细说。

  • optim.SGD() SGD(随机梯度下降)会把数据拆分成batch_size 大小的 batch 进行计算。如果整套丢入神经网络训练会消耗很大的计算资源。SGD就像是抽样一样,达成训练优化的目的。
  • model.parameters() 为该实例中可优化的参数。
  • lr 学习率。
  • weight_decay 权重衰减,也叫L2正则化,目的就是为了让权重衰减到更小的值,在一定程度上减少模型过拟合的问题。

训练模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 训练模型
for i in range(NUM_EPOCHS):
total_loss=0
for data in train_loader:
img, label = data
img = img.view(img.size(0), -1)

if torch.cuda.is_available():
img = img.cuda()
label = label.cuda()

out = model(img)
loss = criterion(out, label)
total_loss += loss.data.item()
optimizer.zero_grad() # 梯度清空
loss.backward()
optimizer.step()
print('epoch %d: lost %f' % (i+1, total_loss))

img = img.view(img.size(0), -1) 说白了就是把多行的 Tensor 拼接成一行。demo:

1
2
3
4
5
6
7
8
9
import torch

a = torch.Tensor(2,3)
print(a)
# tensor([[0.0000, 0.0000, 0.0000],
# [0.0000, 0.0000, 0.0000]])

print(a.view(1,-1))
# tensor([[0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000]])

loss.data.item() .data会返回一个Tensor,.item()返回一个数。demo:

1
2
3
4
5
6
7
8
9
10
11
import torch 
a = torch.ones([1,3])
print(a)
# tensor([[1., 1., 1.]])
print(a.data)
# tensor([[1., 1., 1.]])
print(a.data[0,1])
# tensor(1.)
print(a.data[0,1].item())
# 1.0
# print(a.item()) 运行该行代码会报错

loss.backward() 反向传播。计算出loss以后,我们要修改每个节点的权重,用这个函数来计算层之间节点的新权重。

optimizer.step() 更新所有参数。

模型评估

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 模型评估
model.eval()
eval_loss = 0
eval_acc = 0
for data in test_loader:
img, label = data
img = img.view(img.size(0), -1)
if torch.cuda.is_available():
img = img.cuda()
label = label.cuda()

out = model(img)
loss = criterion(out, label)
eval_loss += loss.data.item()*label.size(0)
_, pred = torch.max(out, 1)
num_correct = (pred == label).sum()
eval_acc += num_correct.item()
print('Test Loss: {:.6f}, Acc: {:.6f}'.format(
eval_loss / (len(test_dataset)),
eval_acc / (len(test_dataset))
))

附录(python)

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
# !/usr/bin/python
# coding: utf8

import torch
from torch import nn, optim
from torch.autograd import Variable
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

class simpleNet(nn.Module):
"""
定义了一个简单的三层全连接神经网络,每一层都是线性的
"""
def __init__(self, in_dim, n_hidden_1, out_dim):
super(simpleNet, self).__init__()
# 第一层
self.layer1 = nn.Sequential(nn.Linear(in_dim, n_hidden_1), nn.BatchNorm1d(n_hidden_1), nn.ReLU(True))
# 第二层
self.layer2 = nn.Sequential(nn.Linear(n_hidden_1, out_dim))

def forward(self, x):
x = self.layer1(x)
x = self.layer2(x)
return x

# 定义一些超参数
BATCH_SIZE = 128
LEARNING_RATE = 0.02
NUM_EPOCHS = 20

# 数据预处理。transforms.ToTensor()将图片转换成PyTorch中处理的对象Tensor,并且进行标准化(数据在0~1之间)
# transforms.Normalize()做归一化。它进行了减均值,再除以标准差。两个参数分别是均值和标准差
# transforms.Compose()函数则是将各种预处理的操作组合到了一起
data_tf = transforms.Compose([transforms.ToTensor(), transforms.Normalize([0.5], [0.5])])

# 数据集的下载器
train_dataset = datasets.MNIST(root='./data', train=True, transform=data_tf, download=True)
test_dataset = datasets.MNIST(root='./data', train=False, transform=data_tf)
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)

# 选择模型
model = simpleNet(28 * 28, 128, 10)
if torch.cuda.is_available():
model = model.cuda()

# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=LEARNING_RATE, weight_decay = 1e-4)

# 训练模型
for i in range(NUM_EPOCHS):
total_loss=0
for data in train_loader:
img, label = data
img = img.view(img.size(0), -1)

if torch.cuda.is_available():
img = img.cuda()
label = label.cuda()

out = model(img)
loss = criterion(out, label)
total_loss += loss.data.item()
optimizer.zero_grad() # 梯度清空
loss.backward()
optimizer.step()
print('epoch %d: lost %f' % (i+1, total_loss))

# 模型评估
model.eval()
eval_loss = 0
eval_acc = 0
for data in test_loader:
img, label = data
img = img.view(img.size(0), -1)
if torch.cuda.is_available():
img = img.cuda()
label = label.cuda()

out = model(img)
loss = criterion(out, label)
eval_loss += loss.data.item()*label.size(0)
_, pred = torch.max(out, 1)
num_correct = (pred == label).sum()
eval_acc += num_correct.item()
print('Test Loss: {:.6f}, Acc: {:.6f}'.format(
eval_loss / (len(test_dataset)),
eval_acc / (len(test_dataset))
))