前言

注:以后我的文章会写在个人博客网站上,本站文章也已被搬运。本文地址:
https://xiaodongfan.com/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%E7%B3%BB%E5%88%97-%E4%BA%8C-%EF%BC%9A%E5%85%A8%E8%BF%9E%E6%8E%A5%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%92%8CBP%E7%AE%97%E6%B3%95.html

上篇介绍了深度学习框架pytorch的安装以及神经网络的基本单元:感知机。本文将介绍全连接神经网络(FCNet)的结构和训练方法,全连接神经网络是一种典型的前馈网络。感知机解决不了非线性分类问题,但是多层神经元叠加在一起理论上可以拟合任意的非线性连续函数映射。

全连接网络

全连接网络是一种前馈网络,由输入层、输出层和若干个隐层组成。如下图所示,输入层由 d d d个神经元组成,用于输入样本的各个特征值;网络可以存在若干个隐层,每个隐层的神经元个数也是不确定的;输出层由 l l l个神经元组成, l l l就是最后要分类的类别数。因此神经网络由很多层构成。
神经元之间的连接方式为:同一层之间的神经元没有连接关系,每一层的神经元和下一层的所有神经元连接。每两个连接的神经元之间都有一个连接权重,这里记第 i i i个神经元和下一层第 j j j个神经元的权重为 ω i j \omega_{ij} ωij
在这里插入图片描述
以上图中只有一个隐层的神经网络为例,隐层的第 h h h个神经元的输入可以表示为:
α h = ∑ i = 1 d v i h x i \alpha_h = \sum_{i=1}^d v_{ih}x_i αh=i=1dvihxi
其输出则是输入经过激活函数 f f f作用在输入上,这里取激活函数为Sigmoid函数:
s i g m o i d ( x ) = 1 1 + e − x sigmoid(x) = \frac{1}{1+e^{-x}} sigmoid(x)=1+ex1
之前我们使用的是阶跃函数,Sigmoid也是一个非线性函数,它有一个很好的性质就是它的导数可以用自己本身来表示:
y ′ = y ( 1 − y ) y'=y(1-y) y=y(1y)
阶跃函数和Sigmoid的函数图如下:
在这里插入图片描述
这样全连接网络的输出计算也就是前向传播的过程为:首先通过输入层计算得到第一个隐层的输出,第 i i i个神经元至第 h h h个神经元的计算公式为:
o u t p u t = f ( ∑ i = 1 d v i h x i ) output = f( \sum_{i=1}^d v_{ih}x_i) output=f(i=1dvihxi)
然后通过第一个隐层计算下一个隐层的值,最后传播到输出层,最后得到神经网络的输出 y ^ = ( y ^ 1 , y ^ 2 , . . . , y ^ l ) \mathbf {\hat y}=(\hat y_1, \hat y_2, ... ,\hat y_l) y^=(y^1,y^2,...,y^l)

神经网络训练 BP算法

对上面介绍的神经网络应该怎么训练呢?应该怎么找到最适合一个数据集分类的各个神经元之间连接的权重 ω i j \omega_{ij} ωij呢?反向传播算法(Back Propagation)提供了解决方法。
训练的思路同样是梯度下降算法,我们定义一个损失函数 L L L,通过朝着损失函数下降最快的方向也就是梯度方向去调整我们的权重系数。损失函数可以为均方误差:
E = 1 2 ∑ j = 1 m ( y ^ j − y j ) 2 E=\frac{1}{2} \sum_{j=1}^m (\hat y_j - y_j)^2 E=21j=1m(y^jyj)2
接下来的问题就是求损失函数 E E E对需要训练的权重系数的梯度 ∂ E ∂ ω i j \frac {\partial E}{\partial \omega_{ij}} ωijE

输出层权重训练

首先从隐层至输出层的连接权重 ω h j \omega _{hj} ωhj为例进行推导。这个求梯度的过程就是链式求导法则,首先我们分析一下 ω h j \omega_ {hj} ωhj是如何影响到我们的损失函数 E E E的, ω h j \omega_ {hj} ωhj首先影响了第 j j j个输出层神经元的输入值 β j \beta_j βj,然后进而通过激励函数Sigmoid影响到其输出值 y ^ j \hat y_j y^j,然后影响到 E E E
这个求导过程为:
∂ E ∂ ω h j = ∂ E ∂ y ^ j × ∂ y ^ j ∂ β j × ∂ β j ∂ ω h j \frac {\partial E}{\partial \omega_{hj}} = \frac {\partial E}{\partial {\hat y_j}} \times \frac{\partial {\hat y_j}}{\partial \beta_j} \times \frac{\partial \beta_j}{\partial \omega_{hj}} ωhjE=y^jE×βjy^j×ωhjβj
我们分别来分析这三项:
第一项:
将上面 E E E的表达式代入 ∂ E ∂ y ^ j \frac {\partial E}{\partial {\hat y_j}} y^jE
∂ E ∂ y ^ j = ∂ y ^ j 1 2 ∑ j = 1 m ( y ^ j − y j ) 2 = − ( y j − y ^ j ) \frac {\partial E}{\partial {\hat y_j}} = \frac{\partial}{{\hat y_j}} \frac{1}{2} \sum_{j=1}^m (\hat y_j - y_j)^2 = -(y_j - \hat y_j) y^jE=y^j21j=1m(y^jyj)2=(yjy^j)
第二项:
这一项是神经元输出对输入求导,实际上就是Sigmoid求导:
∂ y ^ j ∂ β j = y j ( 1 − y j ) \frac{\partial {\hat y_j}}{\partial \beta_j} = y_j(1-y_j) βjy^j=yj(1yj)
第三项:
这一项是神经元的输入对权重求导,实际上就等于上一个神经元的值 b h b_h bh:
∂ β j ∂ ω h j = b h \frac{\partial \beta_j}{\partial \omega_{hj}} = b_h ωhjβj=bh
所以根据梯度下降规则更新权重过程为:
ω h j ← ω h j − η ∂ E ∂ ω h j \omega_{hj} \gets \omega_{hj} - \eta \frac {\partial E}{\partial \omega_{hj}} ωhjωhjηωhjE
= ω j i + η ( y j − y ^ j ) y j ( 1 − y j ) b h = η δ j b h =\omega_ji + \eta (y_j - \hat y_j) y_j(1-y_j) b_h = \eta \delta_j b_h =ωji+η(yjy^j)yj(1yj)bh=ηδjbh
上式中的 δ j \delta_j δj我们定义为:
δ j = ( y j − y ^ j ) y j ( 1 − y j ) \delta_j = (y_j - \hat y_j) y_j(1-y_j) δj=(yjy^j)yj(1yj)

隐层权重训练

隐层神经元权系数 v i h v_{ih} vih首先影响 b h b_h bh神经元的输入 α h \alpha_h αh,进而影响输出。
∂ E ∂ v i h = ∂ E ∂ b h × ∂ b h ∂ α h = ∑ j = 1 l ∂ E ∂ β j × ∂ β j ∂ b h × b h ( 1 − b h ) \frac{\partial E}{\partial{v_{ih}}} = \frac{\partial E}{\partial b_h} \times \frac{\partial b_h}{\partial \alpha_h} = \sum_{j=1}^l \frac{\partial E}{\partial \beta_j} \times \frac{\partial \beta_j}{\partial b_h} \times b_h(1-b_h) vihE=bhE×αhbh=j=1lβjE×bhβj×bh(1bh)
= b h ( 1 − b h ) ∑ j = 1 l ω h j δ j =b_h(1-b_h) \sum_{j=1}^l \omega_{hj} \delta_j =bh(1bh)j=1lωhjδj
至此,我们求出了损失函数对输出层权重系数的梯度和对隐层权重系数的梯度,然后就可以根据梯度下降算法对我们的网络进行训练了。
反向传播算法原理比较简单,推到起来由于标号复杂显得繁琐,后面我们训练网络不怎么关心反向传播的内部求解过程,因为pytorch提供了自动求导的功能,这一点让使用者着重于自己的网络结构构建和参数调节,十分方便!!
花这么大功夫敲公式推导BP算法只是为了让读者对训练的过程有个清楚的理解,接下来在pytorch中实战一个简单的全连接网络。

Pytorch 全连接网络实现

Pytorch 上手非常容易,这里有个翻译版的60min入门:https://www.jianshu.com/p/889dbc684622
使用的数据集为Mnist手写数字,训练集有60000个样本,测试集有10000个样本,首先我们建立一个工程并下载数据集如下:

import torch
from torchvision import datasets, transforms

if __name__ == '__main__':
    # Pytorch自带Mnist数据集,可以直接下载,分为测试集和训练集
    train_dataset = datasets.MNIST(root='./data/', train=True, transform=transforms.ToTensor(), download=True)
    test_dataset = datasets.MNIST(root='./data/', train=False, transform=transforms.ToTensor(), download=True)
    # DataLoader类可以实现数据集的分批和打乱等
    train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=16, shuffle=False)
    test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=16, shuffle=False)
    for i, data in enumerate(train_loader, 0):
        image, label = data
        print(image.shape)

torch里面有MNIST数据集,所以直接调用datasets.MNIST下载就行了,然后将得到的数据集用DataLoader类装起来,这个对象参数中的batch_size为每一批的样本个数,也就是训练时一次性装载进内存的数据,shuffle是将数据集顺序打乱的操作。
这段代码的运行输出:
在这里插入图片描述
可以看到打印出的Tensor是四维的一个数组,以后我们进入神经网络的都是一个四维的Tensor,第一维为batch_size,后面三维为图像的CWH,也就是颜色通道数和图像的长宽。MNIST是黑白的数据集,所以颜色通道为1,彩图为3.
装载完数据就可以进行神经网络的构建了。

import torch
import torch.nn as nn
from torch.optim import optimizer
from torchvision import datasets, transforms

# 优先选择gpu
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")


class FCNet(nn.Module):
    def __init__(self):
        super(FCNet, self).__init__()
        # 一共三层神经网络,一个隐层
        self.features = nn.Sequential(
            nn.Linear(784, 100),
            nn.Sigmoid(),
            nn.Linear(100, 10)
        )

    # 前向传播
    def forward(self, x):
        # 输入为16*1*28*28,这里转换为16*784
        x = x.view(16, -1)
        output = self.features(x)
        return output


# 训练网络
def train(train_loader):
    # 损失函数值
    running_loss = 0.0
    for i, data in enumerate(train_loader, 0):
        inputs, labels = data
        # 如果有gpu,则使用gpu
        inputs, labels = inputs.to(device), labels.to(device)

        # 梯度置零
        optimizer.zero_grad()
        # 前向传播
        output = net(inputs)
        # 损失函数
        loss = criterion(output, labels)
        # 反向传播,权值更新
        loss.backward()
        optimizer.step()

        running_loss += loss.item()
        # 每50个batch_size后打印一次损失函数值
        if i % 100 == 99:
            print('%5d loss: %.3f' %
                  (i + 1, running_loss / 100))
            running_loss = 0.0


# 训练完1个或几个epoch之后,在测试集上测试以下准确率,防止过拟合
def test(test_loader):
    correct = 0
    total = 0
    # 不进行autograd
    with torch.no_grad():
        for data in test_loader:
            images, labels = data
            images, labels = images.to(device), labels.to(device)
            outputs = net(images)
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()

    print('Accuracy of the network on test images: %d %%' % (
            100 * correct / total))
    return correct / total


if __name__ == '__main__':
    # Pytorch自带Mnist数据集,可以直接下载,分为测试集和训练集
    train_dataset = datasets.MNIST(root='./data/', train=True, transform=transforms.ToTensor(), download=True)
    test_dataset = datasets.MNIST(root='./data/', train=False, transform=transforms.ToTensor(), download=True)
    # DataLoader类可以实现数据集的分批和打乱等
    train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=16, shuffle=False)
    test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=16, shuffle=False)

    net = FCNet().to(device)
    # 准则函数使用交叉熵函数,可以尝试其他
    criterion = nn.CrossEntropyLoss()
    # 优化方法为带动量的随机梯度下降
    optimizer = torch.optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

    for epoch in range(20):
        print('Start training: epoch {}'.format(epoch+1))
        train(train_loader)
        test(test_loader)

不熟悉pytorch建议先看看上面的教程,上手很快,这个框架也给了我们很多便利,搭建神经网络十分简单。
上述代码搭建的是一个最简单的三层的全连接网络,输入层神经元为28*28也就是每张图的像素个数,有一个隐层为100个神经元,输出层为10个神经元对应10类数字。代码注释比较详细,这里不细说。
最后训练的结果:
在这里插入图片描述
这里可以看到,经过10轮的训练之后,网络对测试集的准确率达到了0.92,这还仅仅是一个最简单的三层全连接网络!!可见神经网络的强大。
这里要注意的就是,网络的训练都是前几轮损失函数值下降的很快,准确率上升也快,后面损失函数就不怎么下降了,这也意味着我们的模型正在逐渐收敛。由于网络简单且图片较小,网络的训练很快,特别是使用GPU的话。
我这里20轮训练之后,准确率达到了94%,但是一直训练下去的话会发现网络准确率不再上升,这是因为网络的结构本身比较简单,学习能力有限,之后我们会使用卷积神经网络对这个数据集进行分类,能够达到更高的准确率。

总结

本篇主要介绍了全连接神经网络的基本结构以及著名的反向传播算法(BP)的原理推导,最后使用pytorch实现了一个最简单的全连接神经网络对MNIST手写数据集进行分类,实例中的代码已经上传至github:https://github.com/Fanxiaodon/nn/tree/master/FCNetMnist
全连接神经网络存在一些缺陷,后面我们会提到,下篇介绍卷积神经网络CNN,CNN相比全连接网络有一些较大的优点,广泛应用于图像处理。
本文中的理论推导部分参考:周志华-《机器学习》

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐