前文介绍的线性回归和 softmax回归,在模型结构上都属于单层神经网络(只有一个输入层和一个输出层,输入层不计入层数),网络内部本质上只做了一次仿射变换
为了提升模型的表示能力和性能,深度学习主要关注多层模型,并且在神经元中增加激活函数来引入非线性成分。本节以多层感知机(multilayer perceptron,MLP)为例,介绍多层神经网络的概念
Note:
多层感知机
其实一个历史遗留词汇,它的前身是单层感知机
或直接称感知机模型
,是 Frank Rosenblatt 在1957年所发明的一种二元线性分类器,感知机是第一个能根据每个类别的输入样本来学习权重的模型,引发了神经网络相关研究的第一次浪潮,由于当时没有激活函数的概念,这个纯线性模型无法学习异或函数,这次浪潮也由此衰退。目前我们正处于以“深度学习”为代表的第三波神经网络浪潮中。关于感知机的详细介绍可以参考 经典机器学习方法(4)—— 感知机
Note:几十年的研究历程中,先后有 “控制论”、“联结主义”、“神经网络”直到现在的“深度学习”等多个术语指代神经网络相关的研究,现代术语“深度学习”超越了目前机器学习模型的神经科学观点。它诉诸于学习多层次组合这一更普遍的原理,这一原理也可以应用于那些并非受神经科学启发的机器学习框架
理清概念
感知机
= 受生物神经元启发设计的一种二元线性分类器,是神经网络和支持向量机的前身多层感知机MLP
= 至少带一个隐藏层的,深度至少两层的神经网络BP神经网络
= 采用 BP 算法进行训练的神经网络多层感知机在单层神经网络的输入层和输出层之间引入了一到多个隐藏层(hidden layer)
,其中的神经元称为隐藏单元(hidden unit)
,其输出称为隐藏层变量/隐藏变量
。如下图例
这里输入输出和隐藏层的尺寸分别为 4、3、5,由于输入层不涉及计算,以上多层感知机的层数为2,注意多层感知机中的隐藏层和输出层都是全连接层
具体来说,给定由 n n n 个 d d d 维特征样本组成的 batch data X ∈ R n × d \pmb{X}\in\mathbb{R}^{n\times d} XX∈Rn×d,假设多层感知机设输出个数为 q q q,只有一个含 h h h 个隐藏单元的隐藏层,记其输出的隐藏变量为 H ∈ R n × h \pmb{H}\in\mathbb{R}^{n\times h} HH∈Rn×h,由于都是全连接层,设
则 batch data 的输出 O ∈ R n × q \pmb{O}\in\mathbb{R}^{n\times q} OO∈Rn×q 如下计算(其中加法用到广播机制)
H n × h = X n × d W h d × h + b h 1 × h O n × q = H n × h W o h × q + b o 1 × q \pmb{H}_{n\times h} = \pmb{X}_{n\times d}\pmb{W_h}_{d\times h}+\pmb{b_h}_{1\times h}\\ \pmb{O}_{n\times q} = \pmb{H}_{n\times h}\pmb{W_o}_{h\times q}+\pmb{b_o}_{1\times q} HHn×h=XXn×dWhWhd×h+bhbh1×hOOn×q=HHn×hWoWoh×q+bobo1×q 注意到我们直接将隐藏层的输出作为输出层的输入。上述式子可以合并为
O = ( X W h + b h ) W o + b o = X W h W o + b h W o + b o \pmb{O} = (\pmb{XW_h}+\pmb{b_h})\pmb{W_o}+\pmb{b_o} = \pmb{XW_hW_o}+\pmb{b_hW_o}+\pmb{b_o} OO=(XWhXWh+bhbh)WoWo+bobo=XWhWoXWhWo+bhWobhWo+bobo 从这个式子可以看出,如果仅仅引入隐藏层,不管多少层都依然等价于一个单层神经网络
激活函数(activate function)
Note:人工神经网络模型只是生物脑的极简模型,有一派研究人员专门从生物视角研究智能及其人工复现,对应的学科称为“认知神经科学”,那边有很多更贴近生物脑的神经网络模型
ReLU (rectified linear unit 线性整流单元)
,它是一个很简单的线性变换,给定元素 x x x,该函数定义为
ReLU ( x ) = max ( x , 0 ) = { 0 , x ≤ 0 x , x > 0 \text{ReLU}(x) = \max(x,0) = \left\{ \begin{aligned} 0 && ,x\leq 0\\ x && ,x >0 \end{aligned} \right. ReLU(x)=max(x,0)={0x,x≤0,x>0 可见 ReLU 就是输入为负数时输出零,否则保持原样输出的一个分段函数
Sigmoid
函数可以将元素的值变换到 ( 0 , 1 ) (0,1) (0,1) 范围,该函数定义为
sigmoid ( x ) = 1 1 + exp ( − x ) \text{sigmoid}(x) = \frac{1}{1+\exp(-x)} sigmoid(x)=1+exp(−x)1 sigmoid 函数在早期的神经网络中较为普遍,但它目前逐渐被更简单的ReLU函数取代。它的特点是输出值域在0到1之间,在 RNN 网络中利用此特性来控制信息在神经网络中的流动。其导数为
sigmoid ′ ( x ) = sigmoid ( x ) ( 1 − sigmoid ( x ) ) \text{sigmoid}'(x) = \text{sigmoid}(x)(1-\text{sigmoid}(x)) sigmoid′(x)=sigmoid(x)(1−sigmoid(x)) 如下图所示,当输入为0时,sigmoid 函数的导数达到最大值0.25;当输入越偏离0时,sigmoid 函数的导数越接近0
tanh(双曲正切)
函数可以加将元素的值变换到 ( − 1 , 1 ) (-1,1) (−1,1) 范围,该函数定义为
tanh ( x ) = 1 − exp ( − 2 x ) 1 + exp ( − 2 x ) \text{tanh}(x) = \frac{1-\exp(-2x)}{1+\exp(-2x)} tanh(x)=1+exp(−2x)1−exp(−2x) tanh 和 sigmoid 函数的形状很像,但 tanh 函数在坐标系的原点上对称。其导数为
tanh ′ ( x ) = 1 − tanh 2 ( x ) \text{tanh}'(x) = 1-\text{tanh}^2(x) tanh′(x)=1−tanh2(x) 如下图所示,当输入为0时,tanh 函数的导数达到最大值1;当输入越偏离0时,sigmoid函数的导数越接近0
import matplotlib.pyplot as plt
import torch
# 这两行代码解决 plt 中文显示的问题
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
funcs = [x.relu(),x.sigmoid(),x.tanh()]
names = ['ReLU', 'sigmoid', 'tanh']
fig = plt.figure(figsize = (12,9))
for i in range(3):
p1 = fig.add_subplot(3,2,1+i*2,label='a{}'.format(1+i*2))
p2 = fig.add_subplot(3,2,2+i*2,label='a{}'.format(2+i*2))
name = names[i]
y = funcs[i]
if x.grad != None: # 清除上次计算的梯度
x.grad.zero_()
y.sum().backward() # 这样会计算x中各元素梯度,存在x.grad中
p1.plot(x.detach().numpy(), y.detach().numpy(), "r-",linewidth=2,c='r')
p1.set_title(name)
p1.grid(which='major',alpha=0.8)
p2.plot(x.detach().numpy(), x.grad, "r-",linewidth=2,c='r')
p2.set_title(name+'梯度')
p2.grid(which='major',alpha=0.8)
plt.tight_layout() # 防止子图title和轴标签重叠
多层感知机就是含有至少一个隐藏层的由全连接层组成的神经网络,且每个隐藏层的输出通过激活函数进行变换。其网络层数和各隐藏层中隐藏单元个数都是超参数。以单隐藏层为例,输出可以如下计算:
H = ϕ ( X W h + b h ) O = H W o + b o \begin{aligned} \pmb{H} &= \phi(\pmb{XW_h}+\pmb{b_h}) \\ \pmb{O} &= \pmb{HW_o}+\pmb{b_o} \end{aligned} HHOO=ϕ(XWhXWh+bhbh)=HWoHWo+bobo 其中 ϕ \phi ϕ 表示激活函数
有些时候,神经网络最后一个隐藏层的隐藏单元数目比输入维度更小(CV 相关的任务中常有这种情况)这时神经网络的作用就不是升维了,而是提取特征,比如在情感辨析任务中提取人物神态神态
每一个隐藏层都是对上一层的一次抽象/概括/整合,当隐藏层很多时,靠前的隐藏层会提取出更底层/基础的特征,靠后的隐藏层会得到更复杂一点的特征,并在这个过程中实现基础特征的复用,比如下面这时手写数字识别任务
靠前的层提取出各种笔画的特征,靠后的层将这些基础特征整合成圆圈、折线等复杂特征(整合过程会复用上一层的基础特征),最后输出层的每个神经元都代表一个具体数字的特征(所谓 “某个神经元代表一个特征”,是指输入含有整个特征时,这个神经元的输出会变大)
Note: 纯全连接网络不具有 “平移不变性”,注意构成数字 “8” 的上下两个圆形特征,对于全连接网络而言,即时这两个圆一模一样,再图像中的位置不同也会导致网络将其识别为不同的特征,而图片中物体位置移动是很常见的。卷积神经网络具有 “平移不变性”,因此在 CV 中特别常用
import torch
import torchvision
import torchvision.transforms as transforms
import numpy as np
def load_data_fashion_mnist(batch_size, num_workers=0, root='./Datasets/FashionMNIST'):
mnist_train = torchvision.datasets.FashionMNIST(root=root, train=True, download=True,transform=transforms.ToTensor())
mnist_test = torchvision.datasets.FashionMNIST(root=root, train=False, download=True,transform=transforms.ToTensor())
train_iter = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, num_workers=num_workers)
test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False, num_workers=num_workers)
return train_iter, test_iter
# 数据读取迭代器
batch_size = 256
train_iter, test_iter = load_data_fashion_mnist(batch_size, 4)
模型参数初始化:Fashion-MNIST 数据集中图像形状为 28×28,类别数为10。我们将图像拉平为 28 × 28 = 784 28\times 28=784 28×28=784 的向量作为输入,即
num_inputs, num_outputs, num_hiddens = 784, 10, 256
W1 = torch.tensor(np.random.normal(0, 0.01, (num_inputs, num_hiddens)), dtype=torch.float)
b1 = torch.zeros(num_hiddens, dtype=torch.float)
W2 = torch.tensor(np.random.normal(0, 0.01, (num_hiddens, num_outputs)), dtype=torch.float)
b2 = torch.zeros(num_outputs, dtype=torch.float)
params = [W1, b1, W2, b2]
for param in params:
param.requires_grad_(requires_grad=True)
定义激活函数:这里使用 ReLU 激活函数,用 max
函数手动实现之
def relu(X):
return torch.max(input=X, other=torch.tensor(0.0))
定义模型: O = ReLU ( X W h + b h ) W o + b o \pmb{O} = \text{ReLU}(\pmb{XW_h}+\pmb{b_h})\pmb{W_o}+\pmb{b_o} OO=ReLU(XWhXWh+bhbh)WoWo+bobo
def net(X):
X = X.view((-1, num_inputs)) # 原始图像拉平为一维向量输入
H = relu(torch.matmul(X, W1) + b1) # 隐藏层计算
return torch.matmul(H, W2) + b2 # 输出层计算并返回
定义损失函数:直接用 Pytorch 提供的 CrossEntropyLoss
方法实现交叉熵损失,它内部是用 logsoftmax + NLLLoss 实现的,可以避免数据溢出,保证数据稳定性
loss = torch.nn.CrossEntropyLoss()
手动编写小批量随机梯度下降(sgd)方法来优化参数
Note:PyTorch在计算
torch.nn.CrossEntropyLoss
时除过一次batch_size
,因此学习率设得比较大
num_epochs = 5 # 训练轮数
lr = 100.0 # 学习率
batch_size = 256 # batch容量
# 小批量随机梯度下降
def sgd(params, lr, batch_size):
for param in params:
param.data -= lr * param.grad / batch_size # 注意这里更改 param 时用的param.data,这样不会影响梯度计算
# 评估模型准确率
def evaluate_accuracy(data_iter, net):
acc_sum = 0.0 # 所有样本总准确率
n = 0 # 总样本数量
for X, y in data_iter:
acc_sum += (net(X).argmax(dim=1) == y).float().sum().item() # 注意这里中间的 mean() 改成 sum() 了
n += y.shape[0]
return acc_sum / n
def train(net, train_iter, test_iter, loss, num_epochs, batch_size, params=None, lr=None):
# 训练执行 num_epochs 轮
for epoch in range(num_epochs):
train_l_sum = 0.0 # 本 epoch 总损失
train_acc_sum = 0.0 # 本 epoch 总准确率
n = 0 # 本 epoch 总样本数
# 逐小批次地遍历训练数据
for X, y in train_iter:
# 计算小批量损失
y_hat = net(X)
l = loss(y_hat, y).sum()
# 梯度清零
if params is not None and params[0].grad is not None:
for param in params:
param.grad.data.zero_()
# 小批量的损失对模型参数求梯度
l.backward()
# 做小批量随机梯度下降进行优化
sgd(params, lr, batch_size) # 手动实现优化算法
# 记录训练数据
train_l_sum += l.item()
train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
n += y.shape[0]
# 训练完成一个 epoch 后,评估测试集上的准确率
test_acc = evaluate_accuracy(test_iter, net)
# 打印提示信息
print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f'
% (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc))
# 进行训练
train(net, train_iter, test_iter, loss, num_epochs, batch_size, params, lr)
'''
epoch 1, loss 0.0030, train acc 0.719, test acc 0.735
epoch 2, loss 0.0019, train acc 0.826, test acc 0.807
epoch 3, loss 0.0017, train acc 0.844, test acc 0.793
epoch 4, loss 0.0015, train acc 0.855, test acc 0.842
epoch 5, loss 0.0015, train acc 0.863, test acc 0.852
'''
以下代码可以直接复制到 vscode 运行
import torch
import torchvision
import torchvision.transforms as transforms
import numpy as np
# 数据集相关 --------------------------------------------------------------------------------------------------
# 加载数据集
def load_data_fashion_mnist(batch_size, num_workers=0, root='./Datasets/FashionMNIST'):
mnist_train = torchvision.datasets.FashionMNIST(root=root, train=True, download=True,transform=transforms.ToTensor())
mnist_test = torchvision.datasets.FashionMNIST(root=root, train=False, download=True,transform=transforms.ToTensor())
train_iter = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, num_workers=num_workers)
test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False, num_workers=num_workers)
return train_iter, test_iter
# 模型相关 --------------------------------------------------------------------------------------------------------
# 激活函数
def relu(X):
return torch.max(input=X, other=torch.tensor(0.0))
# 定义模型
def net(X):
X = X.view((-1, num_inputs)) # 原始图像拉平为一维向量输入
H = relu(torch.matmul(X, W1) + b1) # 隐藏层计算
return torch.matmul(H, W2) + b2 # 输出层计算并返回
# 小批量随机梯度下降
def sgd(params, lr, batch_size):
for param in params:
param.data -= lr * param.grad / batch_size # 注意这里更改 param 时用的param.data,这样不会影响梯度计算
# 评估模型准确率
def evaluate_accuracy(data_iter, net):
acc_sum = 0.0 # 所有样本总准确率
n = 0 # 总样本数量
for X, y in data_iter:
acc_sum += (net(X).argmax(dim=1) == y).float().sum().item() # 注意这里中间的 mean() 改成 sum() 了
n += y.shape[0]
return acc_sum / n
# 进行训练
def train(net, train_iter, test_iter, loss, num_epochs, batch_size, params=None, lr=None):
# 训练执行 num_epochs 轮
for epoch in range(num_epochs):
train_l_sum = 0.0 # 本 epoch 总损失
train_acc_sum = 0.0 # 本 epoch 总准确率
n = 0 # 本 epoch 总样本数
# 逐小批次地遍历训练数据
for X, y in train_iter:
# 计算小批量损失
y_hat = net(X)
l = loss(y_hat, y).sum()
# 梯度清零
if params is not None and params[0].grad is not None:
for param in params:
param.grad.data.zero_()
# 小批量的损失对模型参数求梯度
l.backward()
# 做小批量随机梯度下降进行优化
sgd(params, lr, batch_size) # 手动实现优化算法
# 记录训练数据
train_l_sum += l.item()
train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
n += y.shape[0]
# 训练完成一个 epoch 后,评估测试集上的准确率
test_acc = evaluate_accuracy(test_iter, net)
# 打印提示信息
print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f'
% (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc))
if __name__ == '__main__':
# 模型参数
num_inputs, num_outputs, num_hiddens = 784, 10, 256
W1 = torch.tensor(np.random.normal(0, 0.01, (num_inputs, num_hiddens)), dtype=torch.float)
b1 = torch.zeros(num_hiddens, dtype=torch.float)
W2 = torch.tensor(np.random.normal(0, 0.01, (num_hiddens, num_outputs)), dtype=torch.float)
b2 = torch.zeros(num_outputs, dtype=torch.float)
params = [W1, b1, W2, b2]
for param in params:
param.requires_grad_(requires_grad=True) # 全部设为允许梯度追踪
# 数据读取迭代器
batch_size = 256
train_iter, test_iter = load_data_fashion_mnist(batch_size, 4)
# 交叉熵损失
loss = torch.nn.CrossEntropyLoss()
# 训练
num_epochs = 5 # 训练轮数
lr = 100.0 # 学习率
batch_size = 256 # batch容量
train(net, train_iter, test_iter, loss, num_epochs, batch_size, params, lr)
和之前 softmax 回归实验一样,按照深度学习习惯把数据拉平这件事定义成神经网络的一个层,然后用 Sequential
容器搭建网络模型
相比之前的 softmax 回归网络,唯一的区别就是多加了一个全连接层作为隐藏层。它的隐藏单元个数为256,并使用ReLU函数作为激活函数
class FlattenLayer(nn.Module):
def __init__(self):
super(FlattenLayer, self).__init__()
def forward(self, x): # x shape: (batch, *, *, ...)
return x.view(x.shape[0], -1)
num_inputs, num_outputs, num_hiddens = 784, 10, 256
net = nn.Sequential(
FlattenLayer(),
nn.Linear(num_inputs, num_hiddens),
nn.ReLU(),
nn.Linear(num_hiddens, num_outputs),
)
for params in net.parameters():
init.normal_(params, mean=0, std=0.01) # 用 nn.init 进行参数初始化
训练步骤和 softmax 回归几乎相同
Note:这里使用了 PyTorch 的 SGD,里面没有除
batch_size
,所以学习率不用设得太大了
num_epochs = 5
batch_size = 256
loss = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(net.parameters(), lr=0.5)
def train(net, train_iter, test_iter, loss, num_epochs, batch_size, params=None, lr=None, optimizer=None):
# 训练执行 num_epochs 轮
for epoch in range(num_epochs):
train_l_sum = 0.0 # 本 epoch 总损失
train_acc_sum = 0.0 # 本 epoch 总准确率
n = 0 # 本 epoch 总样本数
# 逐小批次地遍历训练数据
for X, y in train_iter:
# 计算小批量损失
y_hat = net(X)
l = loss(y_hat, y).sum()
# 梯度清零
optimizer.zero_grad()
# 小批量的损失对模型参数求梯度
l.backward()
# 做小批量随机梯度下降进行优化
optimizer.step()
# 记录训练数据
train_l_sum += l.item()
train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
n += y.shape[0]
# 训练完成一个 epoch 后,评估测试集上的准确率
test_acc = evaluate_accuracy(test_iter, net)
# 打印提示信息
print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f'
% (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc))
train(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)
'''
epoch 1, loss 0.0033, train acc 0.690, test acc 0.754
epoch 2, loss 0.0019, train acc 0.824, test acc 0.809
epoch 3, loss 0.0016, train acc 0.845, test acc 0.803
epoch 4, loss 0.0015, train acc 0.857, test acc 0.804
epoch 5, loss 0.0014, train acc 0.865, test acc 0.823
'''
以下代码可以直接复制到 vscode 运行
import torch
import torchvision
import torchvision.transforms as transforms
from torch import nn
from torch.nn import init
# 数据集相关 --------------------------------------------------------------------------------------------------
# 加载数据集
def load_data_fashion_mnist(batch_size, num_workers=0, root='./Datasets/FashionMNIST'):
mnist_train = torchvision.datasets.FashionMNIST(root=root, train=True, download=True,transform=transforms.ToTensor())
mnist_test = torchvision.datasets.FashionMNIST(root=root, train=False, download=True,transform=transforms.ToTensor())
train_iter = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, num_workers=num_workers)
test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False, num_workers=num_workers)
return train_iter, test_iter
# 模型相关 --------------------------------------------------------------------------------------------------------
class FlattenLayer(nn.Module):
def __init__(self):
super(FlattenLayer, self).__init__()
def forward(self, x): # x shape: (batch, *, *, ...)
return x.view(x.shape[0], -1)
# 评估模型准确率
def evaluate_accuracy(data_iter, net):
acc_sum = 0.0 # 所有样本总准确率
n = 0 # 总样本数量
for X, y in data_iter:
acc_sum += (net(X).argmax(dim=1) == y).float().sum().item() # 注意这里中间的 mean() 改成 sum() 了
n += y.shape[0]
return acc_sum / n
# 进行训练
def train(net, train_iter, test_iter, loss, num_epochs, batch_size, params=None, lr=None, optimizer=None):
# 训练执行 num_epochs 轮
for epoch in range(num_epochs):
train_l_sum = 0.0 # 本 epoch 总损失
train_acc_sum = 0.0 # 本 epoch 总准确率
n = 0 # 本 epoch 总样本数
# 逐小批次地遍历训练数据
for X, y in train_iter:
# 计算小批量损失
y_hat = net(X)
l = loss(y_hat, y).sum()
# 梯度清零
optimizer.zero_grad()
# 小批量的损失对模型参数求梯度
l.backward()
# 做小批量随机梯度下降进行优化
optimizer.step()
# 记录训练数据
train_l_sum += l.item()
train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
n += y.shape[0]
# 训练完成一个 epoch 后,评估测试集上的准确率
test_acc = evaluate_accuracy(test_iter, net)
# 打印提示信息
print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f'
% (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc))
if __name__ == '__main__':
# 模型参数
num_inputs, num_outputs, num_hiddens = 784, 10, 256
# 数据读取迭代器
batch_size = 256
train_iter, test_iter = load_data_fashion_mnist(batch_size, 4)
# 定义模型网络结构
net = nn.Sequential(
FlattenLayer(),
nn.Linear(num_inputs, num_hiddens),
nn.ReLU(),
nn.Linear(num_hiddens, num_outputs),
)
# 初始化模型参数
for params in net.parameters():
init.normal_(params, mean=0, std=0.01)
# 交叉熵损失 & 优化器
loss = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(net.parameters(), lr=0.5)
# 训练
num_epochs = 5 # 训练轮数
batch_size = 256 # batch容量
train(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)