欢迎光临散文网 会员登陆 & 注册

Pytorch教程——全连接神经网络

2023-02-24 22:54 作者:远-方-的-风  | 我要投稿

一、全连接神经网络简介

        人工神经网络(ANN)简称神经网络,可以对一组输入信号和一组输出信号之间的关系进行建模,是机器学习和认知科学领域中一种模仿生物神经网络的结构和功能的数学模型。用于对函数进行估计或近似,其灵感来源于动物的中枢神经系统,特别是大脑。神经网络由大量的人工神经元(或节点)联结进行计算,大多数情况下人工神经网络能在外界信息的基础上改变内部结构,是一种自适应系统。

图1

       具有n个输入一个输出的单一的神经元模型的结构如图1所示。在这个模型中,神经元接收到来自 n个其他神经元传递过来的输入信号,这些输入信号通过带权重的连接进行传递,神经元收到的总输入值将经过激活函数f处理后产生神经元的输出

       全连接神经网络(Multi-Layer Perception, MLP)或者叫多层感知机,是一种连接方式较为简单的人工神经网络,属于前馈神经网络的一种,主要由输入层、隐藏层和输出层构成,并且在每个隐藏层中可以有多个神经元。MLP网络是可以应用于几乎所有任务的多功能学习方法,包括分类、回归,甚至是无监督学习。

       神经网络的学习能力主要来源于网络结构,而且根据层的数量不同、每层神经元数量的多少,以及信息在层之间的传播方式,可以组合成多种神经网络模型。全连接神经网络主要由输入层、隐藏层和输出层构成。输入层仅接收外界的输入,不进行任何函数处理,所以输入层的神经元个数往往和输入的特征数量相同,隐藏层和输出层神经元对信号进行加工处理,最终结果由输出层神经元输出。根据隐藏层的数量可以分为单隐藏层MLP和多隐藏层MLP,它们的网络拓扑结构如图2所示。

图2

       针对单隐藏层MLP和多隐藏层MLP,每个隐藏层的神经元数量是可以变化的,通常没有一个很好的标准用于确定每层神经元的数量和隐藏层的个数。根据经验,更多的神经元就会有更强的 表达能力,同时更容易造成网络的过拟合,所以在使用全连接神经网络时,对模型泛化能力的测试很重要,最好的方式是在训练模型时使用验证集来验证模型的泛化能力,且尽可能地去尝试多种网络结构,以寻找更好的模型,但这往往需要耗费大量的时间。

       下面使用Pytorch中的相关模块搭建多隐藏层的全连接神经网络,使用真实数据集探索MLP在分类中的应用。学习如何利用Pytorch搭建、训练、验证建立的MLP网络及相关网络可视化和训练技巧。在分析之前先导入所需要的库和相关模块,程序如下:

import numpy as np

import pandas as pd

from sklearn.preprocessing import StandardScaler,MinMaxScaler

from sklearn.model_selection import train_test_split

from sklearn.metrics import accuracy_score,confusion_matrix,classification_report

from sklearn.manifold import TSNE

import torch

import torch.nn as nn

from torch.optim import SGD,Adam

import torch.utils.data as Data

import matplotlib.pyplot as plt

import seaborn as sns

import hiddenlayer as hl

from torchviz import make_dot

       在上面导入的库和模块中,sklearn.preprocessing模块用于数据标准化预处理,sklearn.model_selection模块用于数据集的切分,sklearn.metrics模块用于评价模型的预测效果,sklearn.manifold模块用于数据的降维及可视化,torch库则是用于全连接网络的搭建和训练

二、MLP分类模型

       为了比较数据标准化是否对模型的训练过程有影响,通常使用相同的网络结构,分别对标准化和未标准化的数据训练,并将结果和训练过程进行比较。例如使用MLP进行分类,其分析流程如图3所示。

图3

2.1 数据准备和探索

       本节使用一个垃圾邮件数据介绍如何使用Pytorch建立MLP分类模型,该数据集可以从UCI机器学习数据库进行下载,网址为:http://archive.ics.uci.edu/ml/datasets/Spambase

       在该数据集中,包含57个邮件内容的统计特征,其中有48个特征是关键词出现频率 x 100的取值,范围为[0, 100],变量名使用word_freq_WORD命名,WORD表示该特征统计的词语;6个特征为关键字符出现的频率 x 100取值,范围为[0, 100],变量名使用char_freq_CHAR命名;1个变量为capital_run_length_average,表示大写字母不间断的平均长度;1个变量为capital_run_length_longest,表示大写字母不间断的最大长度;1个变量capital_run_length_total表示邮件中大写字母的数量。数据集中最后一个变量是待预测目标变量(0、1),表示电子邮件被认为是垃圾邮件(1)或不是(0)。

       垃圾邮件数据下载后保存为spambase.csv,使用pandas包将其读入Python工作环境中,程序如下所示:

##读取并显示数据

spam = pd.read_csv("data/spambase.csv")

spam

统计两种类型的邮件样本数,使用pd.value_counts()函数进行计算,程序如下:

#计算垃圾邮件和非垃圾邮件的数量

pd.value_counts(spam.label)


        发现数据集中垃圾邮件有1813个样本,非垃圾邮件有2788个样本。为了验证训练好的MLP网络的性能,需要将数据集spam切分为训练集和测试集,其中使用75%的数据作为训练集,剩余25%的数据作为测试集,以测试训练好的模型的泛化能力。数据集切分可以使用train_test_split()函数,程序如下:

#将数据随机切分为训练集和测试集

X = spam.iloc[:,0:57].values

y = spam.label.values

X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.25,random_state=123)

       切分好数据后,需要对数据进行标准化处理。此处采用MinMaxScaler()将数据进行最大—最小值标准化,将数据集中的每个特征取值范围转化到0 ~ 1之间

#对数据的前57列特征进行数据的标准化处理

scales = MinMaxScaler(feature_range=(0,1))

X_train_s = scales.fit_transform(X_train)

X_test_s = scales.fit_transform(X_test)

       得到标准化数据后,将训练数据集的每个特征变量使用箱线图进行显示,对比不同类型的邮件(垃圾邮件和非垃圾邮件)在每个特征变量上的数据分布情况

#得到标准化数据后使用箱线图将训练集的每个特征变量进行显示

colname = spam.columns.values[:-1]   #:-1表示除了最后一个取全部

plt.figure(figsize=(20,14))

for ii in range(len(colname)):

    plt.subplot(7,9,ii+1)

    sns.boxplot(x = y_train,y = X_train_s[:,ii])   #[:,i]表示取第i列的所有行

    plt.title(colname[ii])

plt.subplots_adjust(hspace=0.5)

plt.show()

       上面程序使用sns.boxplot()函数将数据集X_train_s中的57个特征变量进行了可视化,得到的图像如图4所示。

图4

        通过图像发现,有些特征在两种类型的邮件上分布有较大的差异,如word_freq_all、word_freq_our、word_freq_your、word_freq_you、word_freq_000等。

下面将使用全部特征作为MLP网络的输入,因为有些特征尽管在两种类型的邮件上差异并不明显,但是全连接神经网络包含多个神经元,而且每个神经元都是接受前面层的所有神经元的输出,所以MLP网络的每个神经元都有变换、筛选、综合输入的能力,故用于输入的特征越多,网络获取的信息就会越充分,从而网络就会越有效。虽然有些特征在两种类型的邮件上差异并不明显,但是如果将多个特征综合考虑,差异可能就会非常明显。所以在下面建立的MLP分类器中,将使用全部数据特征作为网络的输入。

2.2 搭建网络并可视化

       在数据准备、探索和可视化分析后,下面搭建需要使用的全连接神经网络分类器。网络的每个全连接隐藏层由nn.Linear()函数和nn.ReLU()函数构成,其中nn.ReLU()表示使用激活函数ReLU。构建全连接层分类网络的程序如下所示:

##全连接网络

class MLPclassifica(nn.Module):

    def __init__(self):

        super(MLPclassifica,self).__init__()

        ##定义第一个隐藏层

        self.hidden1 = nn.Sequential(

                        nn.Linear(in_features=57,  #第一个隐藏层的输入,数据的特征数

                                 out_features=30,  #第一个隐藏层的输出,神经元的数量

                                 bias=True),  #默认会有偏置

                        nn.ReLU())

        ##定义第二个隐藏层

        self.hidden2 = nn.Sequential(

                        nn.Linear(in_features=30,

                                 out_features=10,

                                 bias=True),

                        nn.ReLU())

        ##分类层

        self.classifica = nn.Sequential(

                        nn.Linear(in_features=10,

                                 out_features=2,

                                 bias=True),

                        nn.Sigmoid())

    ##定义网络的前向传播途径

    def forward(self,x):

            fc1 = self.hidden1(x)

            fc2 = self.hidden2(fc1)

            output = self.classifica(fc2)

            #输出为两个隐藏层和输出层

            return fc1,fc2,output

       上面的程序中定义了一个MLPclassifica函数类,其网络结构中含有hidden1和hidden2两个隐藏层,分别包含30和10个神经元以及1个分类层classifica,并且分类层使用Sigmoid函数作为激活函数。由于数据有57个特征,所以第一个隐藏层的输入特征为57,而且该数据为二分类问题,所以分类层有2个神经元。在定义完网络结构后,需要定义网络的正向传播过程,分别输出了网络的两个隐藏层fc1、fc2和分类层的输出output。

       针对定义好的MLPclassifica()函数类,使用mlpc = MLPclassifica()得到全连接网络mlpc,并利用torchviz库中的make_dot函数,将网络结构进行可视化,程序如下:

##输出网络结构

mlpc = MLPclassifica()

##使用make_dot可视化网络

x = torch.randn(1,57).requires_grad_(True)

y = mlpc(x)

Mymlpvis = make_dot(y,params=dict(list(mlpc.named_parameters())+[('x',x)]))

Mymlpvis

图5

        得到的网络结构传播过程的可视化结构如图5所示。

2.3 使用未预处理的数据训练模型

       在网络搭建完毕后,首先使用未标准化的训练数据训练模型,然后利用未标准化的测试数据验证模型的泛化能力,分析网络在未标准化的数据集中是否也能很好地拟合数据。首先将未标准化的数据转化为张量,并将张量处理为数据加载器,程序如下:

#将数据转化成张量

X_train_nots = torch.from_numpy(X_train_s.astype(np.float32))

y_train_t = torch.from_numpy(y_train.astype(np.int64))

X_test_nots = torch.from_numpy(X_test_s.astype(np.float32))

y_test_t = torch.from_numpy(y_test.astype(np.int64))

#将训练集转化为张量后,使用TensorDataset将X和Y整理到一起

train_data_nots = Data.TensorDataset(X_train_nots,y_train_t)

#定义数据加载器,将训练集进行批量处理

train_nots_loader = Data.DataLoader(

    dataset=train_data_nots,  #使用的数据集

    batch_size=64,  #批处理样本大小

    shuffle=True,  #每次迭代前打乱数据

    num_workers=1,

)

       在上面的程序中,为了模型正常的训练,需要将数据集X转化为32位浮点型张量,以及将Y转化为64位整型张量,将数据设置为加载器,将Data.TensorDataset()函数和Data.DataLoader()结合在一起使用。在上述数据加载器中每个batch包含64个样本。

       数据准备完毕后,需要使用训练集对全连接神经网络mlpc进行训练和测试。在优化模型时,优化函数使用torch.optim.Adam(),损失函数使用交叉熵损失函数nn.CrossEntropyLoss()。为了观察网络在训练过程中损失的变化情况以及在测试集上预测精度的变化,可以使用HiddenLayer库,将相应数值的变化进行可视化。模型的训练和相关可视化程序如下所示:

#定义优化器

optimizer = torch.optim.Adam(mlpc.parameters(),lr=0.01)

loss_func = nn.CrossEntropyLoss()  #交叉熵损失函数

#记录训练过程的指标

history1 = hl.History()

#使用canvas进行可视化

canvas1 = hl.Canvas()

print_step = 25   #25次迭代后输出损失 

#对模型进行迭代处理 所有数据训练30轮

for epoch in range(30):

    #对训练数据的加载器进行迭代计算

    for step,(b_x,b_y) in enumerate(train_nots_loader):

        #计算每个batch的损失

        _,_,output = mlpc(b_x)   #MLP在训练batch的输出

        train_loss = loss_func(output,b_y)   #二分类交叉熵损失函数

        optimizer.zero_grad()  #每个迭代步的梯度初始化为0

        train_loss.backward()  #损失的后向传播,计算梯度

        optimizer.step()  #使用梯度优化

        #计算迭代次数

        niter = epoch*len(train_nots_loader)+step+1

        #计算每经过print_step次迭代后的输出(25)

        if niter % print_step == 0:

            _,_,output = mlpc(X_test_nots)

            _,pre_lab = torch.max(output,1)

            test_accuracy = accuracy_score(y_test_t,pre_lab)

            #为history添加epoch,损失和精度

            history1.log(niter,train_loss = train_loss,test_accuracy = test_accuracy)

            #使用两个图像可视化损失函数和精度

            with canvas1:

                canvas1.draw_plot(history1["train_loss"])

                canvas1.draw_plot(history1["test_accuracy"])

       上面的程序对训练数据集进行了30个epoch训练,在网络训练过程中,每经过25次迭代就对测试集进行一次预测,并且将迭代次数、训练集上损失函数的取值、模型在测试集上的识别精度都使用history1.log()函数进行保存,再使用canvas1.draw_plot()函数将损失函数大小、预测精度实时可视化出来,得到的模型训练过程如图6所示。

图6

       从图6中可以看出,损失函数一直在波动,并没有收敛到一个平稳的数值区间,在测试集上的精度也具有较大的波动范围,而且最大精度低于72%。说明使用未标准化的数据集训练的模型并没有训练效果,即MLP分类器没有收敛。

导致这样结果的原因可能很多,例如:

(1)数据没有经过标准化处理,所以网络没有收敛。

(2)使用的训练数据样本太少,导致网络没有收敛。

(3)搭建的MLP网络使用的神经元太多或太少,所以网络没有收敛。

2.4 使用预处理后的数据训练模型

       MLP分类器没有收敛的原因可能有多个,但是最可能的原因是数据没有进行标准化预处理,为了验证猜想的正确性,使用标准化数据集重新对上面的MLP网络进行训练,观察训练集和测试集在网络训练过程中的表现,查看网络是否收敛。

       下面使用标准化后的数据进行训练,探索是否会得到收敛的模型。首先对标准化后的数据进行预处理,得到数据加载器。

#将数据转化成张量

X_train_t = torch.from_numpy(X_train_s.astype(np.float32))

y_train_t = torch.from_numpy(y_train.astype(np.int64))

X_test_t = torch.from_numpy(X_test_s.astype(np.float32))

y_test_t = torch.from_numpy(y_test.astype(np.int64))

#将训练集转化为张量后,使用TensorDataset将X和Y整理到一起

train_data = Data.TensorDataset(X_train_t,y_train_t)

#定义数据加载器,将训练数据集进行批量处理

train_loader = Data.DataLoader(

    dataset=train_data,  #使用的数据集

    batch_size=64,  #批处理样本大小

    shuffle=True,  #每次迭代前打乱数据

    num_workers=1,

)

       在训练数据和测试数据准备好后,使用与2.3节相似的程序,训练全连接神经网络分类器,程序如下:

#定义优化器

optimizer = torch.optim.Adam(mlpc.parameters(),lr=0.01)

loss_func = nn.CrossEntropyLoss()  #交叉熵损失函数

#记录训练过程的指标

history1 = hl.History()

#使用canvas进行可视化

canvas1 = hl.Canvas()

print_step = 25  #25次迭代后输出损失

#对模型进行迭代处理 所有数据训练30轮

for epoch in range(30):

    #对训练数据的加载器进行迭代计算

    for step,(b_x,b_y) in enumerate(train_loader):

        #计算每个batch的损失

        _,_,output = mlpc(b_x)   #MLP在训练batch上的输出

        train_loss = loss_func(output,b_y)   #二分类交叉熵损失

        optimizer.zero_grad()  #每个迭代步的梯度初始化为0

        train_loss.backward()  #损失的后向传播,计算梯度

        optimizer.step()  #使用梯度优化

        #计算迭代次数

        niter = epoch*len(train_loader)+step+1

        #计算每经过print_step次迭代后的输出(25)

        if niter % print_step == 0:

            _,_,output = mlpc(X_test_t)

            _,pre_lab = torch.max(output,1)

            test_acc = accuracy_score(y_test_t,pre_lab)

            #为history添加epoch,损失和精度

            history1.log(niter,train_loss = train_loss,test_acc = test_acc)

            #使用两个图像可视化损失函数和精度

            with canvas1:

                canvas1.draw_plot(history1["train_loss"])

                canvas1.draw_plot(history1["test_acc"])

       在上面的程序中,利用标准化处理后的数据集对网络进行训练,并且在网络训练过程中,每经过25次迭代就对标准化的测试集进行一次预测。同样将迭代次数、训练集上的损失函数取值、模型在测试集上的识别精度都使用history1.log()函数进行保存,并且使用canvas1.draw_plot()函数将损失函数大小、预测精度实时可视化出来,得到如图7所示的模型训练过程。

图7

       从图7中得到了标准化数据训练的分类器,并且损失函数最终收敛到一个平稳的数值区间,在测试集上的精度也得到了收敛,预测精度稳定在90%以上。说明模型在使用标准化数据后得到有效的训练。即数据标准化预处理对MLP网络非常重要。

       在获得收敛的模型后,可以在测试集上计算模型的垃圾邮件识别最终精度,程序代码如下所示:

_,_,output = mlpc(X_test_t)

_,pre_lab = torch.max(output,1)

test_acc = accuracy_score(y_test_t,pre_lab)

print("test_acc:",test_acc)

Out[11]:test_acc:0.9209383145091226

2.5 获取中间层的输出并可视化

       在全连接神经网络训练好后,为了更好地理解全连接神经网络的计算过程,以获取网络在计算过程中中间隐藏层的输出,可以使用两种方法:

(1)如果在网络的前向过程想要输出隐藏层,可以使用单独的变量进行命名,然后在输出时输出该变量,例如:

def forward(self,x):

      fc1 = self.hidden1(x)

      fc2 = self.hidden2(fc1)

      output = self.classifica(fc2)

      #输出为两个隐藏层和输出层

      return fc1,fc2,output

       在上面的forward()函数中,fc1、fc2分别是第一隐藏层和第二隐藏层的输出,output则为分类层的输出,最后使用return fc1,fc2,output同时将三个变量输出,便于在调用模型时,轻松获得隐藏层的输出。

(2)如果在网络的前向过程中只输出了最后一层的输出,并没有输出中间变量,这是想要获取中间层的输出,则使用钩子(hook)技术。钩子技术可以理解为对一个完整的业务流程,使用钩子可以在不修改原始网络代码的情况下,将额外的功能依附于业务流程,并获取想要的输出。

       接下来针对上述已经训练好的网络,分别利用两种方法介绍如何从网络中获取中间隐藏层的输出,并对相关输出进行可视化。
1.使用中间层的输出

       在上述定义的全连接网络中,已经输出了隐藏层的输出,可以直接使用mlpc()作用于测试集时,输出相关内容,程序如下:

#计算最终模型在测试集上的第二个隐藏层的输出

_,test_fc2,_ = mlpc(X_test_t)

print("test_fc2.shape:",test_fc2.shape)

Out[13]: test_fc2.shape: torch.Size([1151, 10])

      在上述程序和输出中,可以通过“_,test_fc2,_ = mlpc(X_test_t)”获取mlpc网络,让测试集在第二个隐藏层输出test_fc2。test_fc2的尺寸为[1151,10],表明有1151个样本,每个样本包含10个特征输出

      下面对10个特征进行降维,然后使用散点图进行可视化,程序如下:

#对输出进行降维并可视化

test_fc2_tsne = TSNE(n_components = 2).fit_transform(test_fc2.data.numpy())

#将特征进行可视化

plt.figure(figsize = (8,6))

#可视化前设置坐标系的取值范围

plt.xlim([min(test_fc2_tsne[:,0]-1),max(test_fc2_tsne[:,0])+1])

plt.ylim([min(test_fc2_tsne[:,1]-1),max(test_fc2_tsne[:,1])+1])

plt.plot(test_fc2_tsne[y_test == 0,0],test_fc2_tsne[y_test == 0,1],"bo",label = "0")

plt.plot(test_fc2_tsne[y_test == 1,0],test_fc2_tsne[y_test == 1,1],"rd",label = "1")

plt.legend()

plt.title("test_fc2_tsne")

plt.show()

        上述程序隐形后,可得到如图8所示的散点图。

图8

         在散点图中,两种点分别代表垃圾邮件和正常邮件在空间中的分布情况

2.使用钩子获取中间层的输出

         首先定义一个辅助函数,以方便获取和保存中间层的输出。

 #定义一个辅助函数,来获取指定层名称的特征

activation = {}  #保存不同层的输出

def get_activation(name):

    def hook(model, input, output):

        activation[name] = output.detach()

    return hook

       在定义好get_activation()函数后,获取中间层的输出,以字典的形式保存在activation字典中。获取分类层的输出可以使用如下所示的代码:

#全连接网络获取分类层的输出

mlpc.classifica.register_forward_hook(get_activation("classifica"))

_,_,_ = mlpc(X_test_t)

classifica = activation["classifica"].data.numpy()

print("classifica.shape:",classifica.shape)

       上述程序先使用mlpc.classifica.register_forward_hook(get_activation("classifica"))操作(该操作主要用于获取classifica层的输出结果),然后将训练好的网络mlpc作用于测试集X_test_t上,这样在activation字典中,键值classifica对应的结果即为想要获取的中间层特征。从输出中可以发现,其每个样本包含两个特征输出。下面同样使用散点图将其可视化。

#将特征进行可视化

plt.figure(figsize = (8,6))

#可视化每类的散点图

plt.plot(classifica[y_test == 0,0],classifica[y_test == 0,1],"bo",label = "0")

plt.plot(classifica[y_test == 1,0],classifica[y_test == 1,1],"rd",label = "1")

plt.legend()

plt.title("classifica")

plt.show()

       在散点图9中,两种点分别代表垃圾邮件和正常邮件在空间中的分布情况。

图9


Pytorch教程——全连接神经网络的评论 (共 条)

分享到微博请遵守国家法律