NNDL 实验五 前馈神经网络(1)二分类任务

目录

4.1 神经元

4.1.1 净活性值

4.1.2 激活函数

4.2 基于前馈神经网络的二分类任务

4.2.1 数据集构建

4.2.2 模型构建

4.2.3 损失函数 

4.2.4 模型优化

4.2.5 完善Runner类:RunnerV2_1

4.2.6 模型训练

4.2.7 性能评价

总结


4.1 神经元

4.1.1 净活性值

使用pytorch计算一组输入的净活性值z

净活性值z经过一个非线性函数f(·)后,得到神经元的活性值a

(借用老师一个图)

假设一个神经元接收的输入为x\in

使用pytorch计算一组输入的净活性值,代码参考paddle例题:

import torch

# 2个特征数为5的样本
X = torch.rand(size=[2, 5])

# 含有5个参数的权重向量
w = torch.rand(size=[5, 1])
# 偏置项
b = torch.rand(size=[1, 1])

# 使用'torch.matmul'实现矩阵相乘
z = torch.matmul(X, w) + b
print("input X:", X)
print("weight w:", w, "\nbias b:", b)
print("output z:", z)

在飞桨中,可以使用nn.Linear完成输入张量的上述变换。

在pytorch中学习相应函数torch.nn.Linear(features_in, features_out, bias=False)。

实现上面的例子,完成代码,进一步深入研究torch.nn.Linear()的使用。

torch.nn.Linear的原理:从名称就可以看出来,nn.Linear表示的是线性变换,原型就是初级数学里学到的线性函数:y=kx+b,不过在深度学习中,变量都是多维张量,乘法就是矩阵乘法,加法就是矩阵加法,因此在nn.Linear中运行的是矩阵的相关运行算。

torch.nn.Linear的使用:常用头文件:import torch.nn as nn

nn.Linear()的初始化: 

nn.Linear(in_feature,out_feature,bias)

in_feature: int型, 在forward中输入Tensor最后一维的通道数

out_feature: int型, 在forward中输出Tensor最后一维的通道数

bias: bool型, Linear线性变换中是否添加bias偏置

 nn.Linear()的执行:(即执行forward函数)

out=nn.Linear(input)

 input: 表示输入的Tensor,可以有多个维度
output: 表示输入的Tensor,可以有多个维度

如果想更加详细了解torch.nn.Linear可参考以下博客:

torch.nn.Linear详解

【思考题】加权相加仿射变换之间有什么区别和联系?

 最开始看到加权相加与仿射变换时,各自分开看它们的时候好像都懂了,可是让我写它们之间的区别与联系时,却不知从何说起,于是我就查了查资料,我觉得加权相加就是指的是线性变换而仿射变换简单来说就是线性变换+平移。

线性变换有三个特点:

  1. 变换前是直线,变换依然是直线;
  2. 直线比例保持不变
  3. 变换前是圆点,变换后依然是圆点

仿射变换有两个特点:

  1. 变换前是直线,变换后依然是直线;
  2. 直线比例保持不变 

如果想更加详细的了解可以参考以下博客:

仿射变换 

4.1.2 激活函数

激活函数通常为非线性函数,可以增强神经网络的表示能力和学习能力。

常用的激活函数有S型函数ReLU函数

4.1.2.1 Sigmoid 型函数

 常用的 Sigmoid 型函数有 Logistic 函数和 Tanh 函数。

Logistic函数:

\sigma (z)=\frac{1}{1+exp(-z)}

Tanh函数:

yanh(z)=\frac{exp(z)-exp(-z)}{exp(z)+exp(-z)}

  1. 使用python实现并可视化“Logistic函数、Tanh函数“
  2. 在飞桨中,可以通过调用paddle.nn.functional.sigmoidpaddle.nn.functional.tanh实现对张量的Logistic和Tanh计算。在pytorch中找到相应函数并测试

 Logistic函数和Tanh函数的代码实现和可视化如下:

#Logistic函数和Tanh函数的代码实现和可视化如下:

import matplotlib.pyplot as plt

# Logistic函数
def logistic(z):
    return 1.0 / (1.0 + torch.exp(-z))

# Tanh函数
def tanh(z):
    return (torch.exp(z) - torch.exp(-z)) / (torch.exp(z) + torch.exp(-z))

# 在[-10,10]的范围内生成10000个输入值,用于绘制函数曲线
z = torch.linspace(-10, 10, 10000)

plt.figure()
plt.plot(z.tolist(), logistic(z).tolist(), color='blue', label="Logistic Function")
plt.plot(z.tolist(), tanh(z).tolist(), color='yellow', linestyle ='--', label="Tanh Function")

ax = plt.gca() # 获取轴,默认有4个
# 隐藏两个轴,通过把颜色设置成none
ax.spines['top'].set_color('none')
ax.spines['right'].set_color('none')
# 调整坐标轴位置   
ax.spines['left'].set_position(('data',0))
ax.spines['bottom'].set_position(('data',0))
plt.legend(loc='lower right', fontsize='large')

plt.savefig('fw-logistic-tanh.pdf')
plt.show()

运行结果

 NNDL 实验五 前馈神经网络(1)二分类任务_第1张图片

 4.1.2.2 ReLU型函数

常见的ReLU函数有ReLU和带泄露的ReLU(Leaky ReLU)

ReLU(z)=max(0,z)

LeakyReLU(z)=max(0,z)+\lambdamin(0,z)

其中\lambda为超参数

  1. 使用python实现并可视化可视化“ReLU、带泄露的ReLU的函数”
  2. 在飞桨中,可以通过调用paddle.nn.functional.relupaddle.nn.functional.leaky_relu完成ReLU与带泄露的ReLU的计算。在pytorch中找到相应函数并测试

可视化ReLU和带泄露的ReLU的函数的代码实现和可视化如下:

#可视化ReLU和带泄露的ReLU的函数的代码实现和可视化如下:
# ReLU
def relu(z):
    return torch.maximum(z, torch.tensor(0.))

# 带泄露的ReLU
def leaky_relu(z, negative_slope=0.1):
    # 当前版本torch暂不支持直接将bool类型转成int类型,因此调用了torch的cast函数来进行显式转换
    a1 = ((z > 0).to(dtype=torch.float32) * z)
    a2 = ((z <= 0).to(dtype=torch.float32) * (negative_slope * z))
    return a1 + a2

# 在[-10,10]的范围内生成一系列的输入值,用于绘制relu、leaky_relu的函数曲线
z = torch.linspace(-10, 10, 10000)

plt.figure()
plt.plot(z.tolist(), relu(z).tolist(), color="yellow", label="ReLU Function")
plt.plot(z.tolist(), leaky_relu(z).tolist(), color="blue", linestyle="--", label="LeakyReLU Function")

ax = plt.gca()
ax.spines['top'].set_color('none')
ax.spines['right'].set_color('none')
ax.spines['left'].set_position(('data',0))
ax.spines['bottom'].set_position(('data',0))
plt.legend(loc='upper left', fontsize='large')
plt.savefig('fw-relu-leakyrelu.pdf')
plt.show()

运行结果:

NNDL 实验五 前馈神经网络(1)二分类任务_第2张图片

动手实现《神经网络与深度学习》4.1节中提到的其他激活函数:

Hard-Logistic、Hard-Tanh、ELU、Softplus、Swish等。(选做)(改为做查询其对应的函数。)

Hard-Logistic函数:

NNDL 实验五 前馈神经网络(1)二分类任务_第3张图片

Hard-Tanh函数:

在这里插入图片描述

 

 ELU函数:

在这里插入图片描述
其中γ≥0是一个超参数,决定x≤0时的饱和曲线,并调整输出均值在0附近

 Softplus函数:

在这里插入图片描述

 Swish函数:

在这里插入图片描述

4.2 基于前馈神经网络的二分类任务

4.2.1 数据集构建

使用第3.1.1节中构建的二分类数据集:Moon1000数据集,其中训练集640条、验证集160条、测试集200条。该数据集的数据是从两个带噪音的弯月形状数据分布中采样得到,每个样本包含2个特征。

注(这里的数据集和上个实验中用到的弯月数据集出现错误,所以这次我就直接修改了数据集,具体的原因可以参考下面的博客链接)

弯月数据集之谜

修改后代码实现:

from nndl.dataset import make_moons

# 采样1000个样本
n_samples = 1000
X, y = make_moons(n_samples=n_samples, shuffle=True, noise=0.1)

num_train = 640
num_dev = 160
num_test = 200

X_train, y_train = X[:num_train], y[:num_train]
X_dev, y_dev = X[num_train:num_train + num_dev], y[num_train:num_train + num_dev]
X_test, y_test = X[num_train + num_dev:], y[num_train + num_dev:]

y_train = y_train.reshape([-1,1])
y_dev = y_dev.reshape([-1,1])
y_test = y_test.reshape([-1,1])

运行结果:

NNDL 实验五 前馈神经网络(1)二分类任务_第4张图片 

 

4.2.2 模型构建

 为了更高效的构建前馈神经网络,我们先定义每一层的算子,然后再通过算子组合构建整个前馈神经网络。

假设网络的第L层的输入为第L−1层的神经元活性值a(L−1),经过一个仿射变换,得到该层神经元的净活性值z,再输入到激活函数得到该层神经元的活性值a。

在实践中,为了提高模型的处理效率,通常将N个样本归为一组进行成批地计算。假设网络第L层的输入为A^{(l-1)}\inR^{N*M_{l-1}},其中每一行为一个样本,则前馈网络中第L层的计算公式为

Z^{(l)}=A^{(l-1)}W^{(l)}+b^{(l)}\in \mathbb{R}^{N\times M_{l}},(4.8)

A^{(l)}=f_{l}(Z^{(l)})\in \mathbb{R}^{N\times M_{l}},(4.9)

其中Z^{(l)}N个样本第l层神经元的净活性值,A^{(l)N个样本第l层神经元的活性值W^{(l)}\in \mathbb{R}^{M_{l-1}\times M_{l}}为第l层的权重矩阵,b^{(l)}\in \mathbb{R}^{1\times M_{l}}为第l层的偏置。

 为了和代码的实现保存一致性,这里使用形状为(样本数量×特征维度)(样本数量×特征维度)的张量来表示一组样本。样本的矩阵XX是由NN个xx的行向量组成。而《神经网络与深度学习》中xx为列向量,因此这里的权重矩阵WW和偏置bb和《神经网络与深度学习》中的表示刚好为转置关系。

为了使后续的模型搭建更加便捷,我们将神经层的计算,即公式(4.8)和(4.9),都封装成算子,这些算子都继承Op基类。

4.2.2.1 线性层算子

公式(4.8)对应一个线性层算子,权重参数采用默认的随机初始化,偏置采用默认的零初始化。代码实现如下:

#线性层算子
from nndl.op import Op
import  torch

# 实现线性层算子
class Linear(Op):
    def __init__(self, input_size, output_size, name, weight_init=torch.normal, bias_init=torch.zeros):
        """
        输入:
            - input_size:输入数据维度
            - output_size:输出数据维度
            - name:算子名称
            - weight_init:权重初始化方式,默认使用'torch.standard_normal'进行标准正态分布初始化
            - bias_init:偏置初始化方式,默认使用全0初始化
        """

        self.params = {}
        # 初始化权重
        self.params['W'] = weight_init(0,1,size=[input_size, output_size])
        # 初始化偏置
        self.params['b'] = bias_init(size=[1, output_size])
        self.inputs = None

        self.name = name

    def forward(self, inputs):
        """
        输入:
            - inputs:shape=[N,input_size], N是样本数量
        输出:
            - outputs:预测值,shape=[N,output_size]
        """
        self.inputs = inputs

        outputs = torch.matmul(self.inputs, self.params['W']) + self.params['b']
        return outputs

4.2.2.2 Logistic算子(激活函数)

本节我们采用Logistic函数来作为公式(4.9)中的激活函数。这里也将Logistic函数实现一个算子,代码实现如下:

#Logistic算子
class Logistic(Op):
    def __init__(self):
        self.inputs = None
        self.outputs = None

    def forward(self, inputs):
        """
        输入:
            - inputs: shape=[N,D]
        输出:
            - outputs:shape=[N,D]
        """
        outputs = 1.0 / (1.0 + torch.exp(-inputs))
        self.outputs = outputs
        return outputs

4.2.2.3 层的串行组合

在定义了神经层的线性层算子和激活函数算子之后,我们可以不断交叉重复使用它们来构建一个多层的神经网络。

实现一个两层的用于二分类任务的前馈神经网络,选用Logistic作为激活函数,可以利用上面实现的线性层和激活函数算子来组装。代码如下:

#层的串行组合
# 实现一个两层前馈神经网络
class Model_MLP_L2(Op):
    def __init__(self, input_size, hidden_size, output_size):
        """
        输入:
            - input_size:输入维度
            - hidden_size:隐藏层神经元数量
            - output_size:输出维度
        """
        self.fc1 = Linear(input_size, hidden_size, name="fc1")
        self.act_fn1 = Logistic()
        self.fc2 = Linear(hidden_size, output_size, name="fc2")
        self.act_fn2 = Logistic()

    def __call__(self, X):
        return self.forward(X)

    def forward(self, X):
        """
        输入:
            - X:shape=[N,input_size], N是样本数量
        输出:
            - a2:预测值,shape=[N,output_size]
        """
        z1 = self.fc1(X)
        a1 = self.act_fn1(z1)
        z2 = self.fc2(a1)
        a2 = self.act_fn2(z2)
        return a2

实例化一个两层的前馈网络,令其输入层维度为5,隐藏层维度为10,输出层维度为1。
并随机生成一条长度为5的数据输入两层神经网络,观察输出结果。

#测试
# 实例化模型
model = Model_MLP_L2(input_size=5, hidden_size=10, output_size=1)
# 随机生成1条长度为5的数据
X = torch.rand(size=[1, 5])
result = model(X)
print ("result: ", result)

运行结果

4.2.3 损失函数 

二分类交叉熵损失函数见第三章

4.2.4 模型优化

神经网络的层数通常比较深,其梯度计算和上一章中的线性分类模型的不同的点在于:

线性模型通常比较简单可以直接计算梯度,而神经网络相当于一个复合函数,需要利用链式法则进行反向传播来计算梯度。

4.2.4.1 反向传播算法

  1. 第1步是前向计算,可以利用算子的forward()方法来实现;
  2. 第2步是反向计算梯度,可以利用算子的backward()方法来实现;
  3. 第3步中的计算参数梯度也放到backward()中实现,更新参数放到另外的优化器中专门进行。

这样,在模型训练过程中,我们首先执行模型的forward(),再执行模型的backward(),就得到了所有参数的梯度,之后再利用优化器迭代更新参数。

以我们这节中构建的两层全连接前馈神经网络Model_MLP_L2为例,下图给出了其前向和反向计算过程:

NNDL 实验五 前馈神经网络(1)二分类任务_第5张图片

下面我们按照反向的梯度传播顺序,为每个算子添加backward()方法,并在其中实现每一层参数的梯度的计算。 

4.2.4.2 损失函数

二分类交叉熵损失函数

NNDL 实验五 前馈神经网络(1)二分类任务_第6张图片

实现损失函数的backward(),代码实现如下:

# 实现交叉熵损失函数
class BinaryCrossEntropyLoss(Op):
    def __init__(self, model):
        self.predicts = None
        self.labels = None
        self.num = None

        self.model = model

    def __call__(self, predicts, labels):
        return self.forward(predicts, labels)

    def forward(self, predicts, labels):
        """
        输入:
            - predicts:预测值,shape=[N, 1],N为样本数量
            - labels:真实标签,shape=[N, 1]
        输出:
            - 损失值:shape=[1]
        """
        self.predicts = predicts
        self.labels = labels
        self.num = self.predicts.shape[0]
        loss = -1. / self.num * (paddle.matmul(self.labels.t(), paddle.log(self.predicts)) 
                + paddle.matmul((1-self.labels.t()), paddle.log(1-self.predicts)))

        loss = paddle.squeeze(loss, axis=1)
        return loss

    def backward(self):
        # 计算损失函数对模型预测的导数
        loss_grad_predicts = -1.0 * (self.labels / self.predicts - 
                       (1 - self.labels) / (1 - self.predicts)) / self.num
        
        # 梯度反向传播
        self.model.backward(loss_grad_predicts)

4.2.4.3 Logistic算子

为Logistic算子增加反向函数

Logistic算子的前向过程表示为A=\sigma (Z),其中\sigma为Logistic函数,Z\in R^{N\times D}A\in R^{N\times D}的每一行表示一个样本。

为了简便起见,我们分别用向量a\in R^{D} 和z\in R^{D}表示同一个样本在激活函数前后的表示,则az的偏导数为:

 \frac{\partial a}{\partial z} =diag(a\odot (1-a))\in R^{D\times D}

按照反向传播算法,令\delta _{a}=\frac{\partial R}{\partial a}\in R^{D}表示最终损失R对Logistic算子的单个输出a的梯度,则

 

\delta _{z}=\frac{\partial a}{\partial z}\delta _{a}=diag(a\odot (1-a))\delta _{(a)}

=a\odot (1-a)\odot \delta _{(a)}

 将上面公式利用批量数据表示的方式重写,令\delta _{A}=\frac{\partial R}{\partial A} \in R^{N\times D}表示最终损失R对Logistic算子输出A的梯度,损失函数对Logistic函数输入Z的导数为

 \delta _{Z}=A\odot (1-A)\odot \delta _{A}\in R^{N\times D}

\delta _{Z}为Logistic算子反向传播的输出。

由于Logistic函数中没有参数,这里不需要在backward()方法中计算该算子参数的梯度。

class Logistic(Op):
    def __init__(self):
        self.inputs = None
        self.outputs = None
        self.params = None

    def forward(self, inputs):
        outputs = 1.0 / (1.0 + torch.exp(-inputs))
        self.outputs = outputs
        return outputs

    def backward(self, grads):
        # 计算Logistic激活函数对输入的导数
        outputs_grad_inputs = torch.multiply(self.outputs, (1.0 - self.outputs))
        return torch.multiply(grads,outputs_grad_inputs)

4.2.4.4 线性层

线性层算子Linear的前向过程表示为Y=XW+b,其中输入为X\in R^{N\times M},输出为Y\in R^{N\times D},参数为权重矩阵W\in R^{M\times D}和偏置b\in R^{1\times D}XY中的每一行表示一个样本。

为了简便起见,我们用向量x\in R^{M}y\in R^{D}表示同一个样本在线性层算子中的输入和输出,则有y=W^{T}x+b^{T}y对输入x的偏导数为

\frac{\partial y}{\partial x} =W\in R^{D\times M} 

线性层输入的梯度按照反向传播算法,令\delta _{y}=\frac{\partial R}{\partial y} \in R^{D}表示最终损失R对线性层算子的单个输出y的梯度,则

\delta _{x}=\frac{\partial R}{\partial x}=W\delta _{y}

将上面公式利用批量数据表示的方式重写,令\delta_{Y}=\frac{\partial R}{\partial Y}\in\mathbb{R}^{N\times D}表示最终损失R对线性层算子输出Y的梯度,公式可以重写为

\delta_{X}=\delta_{Y}W^{T}

 其中\delta_{X}为线性层算子反向函数的输出。 

计算线性层参数的梯度:由于线性层算子中包含有可学习的参数Wb,因此backward()除了实现梯度反传外,还需要计算算子内部的参数的梯度。

\delta_{y}=\frac{\partial R}{\partial y}\in \mathbb{R}^{D}表示最终损失R对线性层算子的单个输出y的梯度,则

 

\delta _{W}=\frac{\partial R}{\partial W}=x\delta _{y}^{T}

\delta_{b}=\frac{\partial R}{\partial b}=\delta _{y}^{T}

 将上面公式利用批量数据表示的方式重写,令\delta_{Y}=\frac{\partial R}{\partial Y}\in \mathbb{R}^{N \times D}表示最终损失R对线性层算子输出Y的梯度,则公式可以重写为

\delta_{W}=X^{T}\delta_{Y}

\delta_{b}=1^{T}\delta_{Y}

具体实现代码如下:

#线性层
class Linear(Op):
    def __init__(self, input_size, output_size, name, weight_init=torch.normal, bias_init=torch.zeros):
        self.params = {}
        self.params['W'] = weight_init(0,1,size=[input_size, output_size])
        self.params['b'] = bias_init(size=[1, output_size])

        self.inputs = None
        self.grads = {}

        self.name = name

    def forward(self, inputs):
        self.inputs = inputs
        outputs = torch.matmul(self.inputs, self.params['W']) + self.params['b']
        return outputs

    def backward(self, grads):
        """
        输入:
            - grads:损失函数对当前层输出的导数
        输出:
            - 损失函数对当前层输入的导数
        """
        self.grads['W'] = torch.matmul(self.inputs.T, grads)
        self.grads['b'] = torch.sum(grads, axis=0)

        # 线性层输入的梯度
        return torch.matmul(grads, self.params['W'].T)

4.2.4.5 整个网络

实现完整的两层神经网络的前向和反向计算

代码实现下:

#整个网络
class Model_MLP_L2(Op):
    def __init__(self, input_size, hidden_size, output_size):
        # 线性层
        self.fc1 = Linear(input_size, hidden_size, name="fc1")
        # Logistic激活函数层
        self.act_fn1 = Logistic()
        self.fc2 = Linear(hidden_size, output_size, name="fc2")
        self.act_fn2 = Logistic()

        self.layers = [self.fc1, self.act_fn1, self.fc2, self.act_fn2]

    def __call__(self, X):
        return self.forward(X)

    # 前向计算
    def forward(self, X):
        z1 = self.fc1(X)
        a1 = self.act_fn1(z1)
        z2 = self.fc2(a1)
        a2 = self.act_fn2(z2)
        return a2

    # 反向计算
    def backward(self, loss_grad_a2):
        loss_grad_z2 = self.act_fn2.backward(loss_grad_a2)
        loss_grad_a1 = self.fc2.backward(loss_grad_z2)
        loss_grad_z1 = self.act_fn1.backward(loss_grad_a1)
        loss_grad_inputs = self.fc1.backward(loss_grad_z1)

4.2.4.6 优化器

在计算好神经网络参数的梯度之后,我们将梯度下降法中参数的更新过程实现在优化器中。

与第3章中实现的梯度下降优化器SimpleBatchGD不同的是,此处的优化器需要遍历每层,对每层的参数分别做更新。

代码如下:

#优化器
from nndl.opitimizer import Optimizer

class BatchGD(Optimizer):
    def __init__(self, init_lr, model):
        super(BatchGD, self).__init__(init_lr=init_lr, model=model)

    def step(self):
        # 参数更新
        for layer in self.model.layers: # 遍历所有层
            if isinstance(layer.params, dict):
                for key in layer.params.keys():
                    layer.params[key] = layer.params[key] - self.init_lr * layer.grads[key]

4.2.5 完善Runner类:RunnerV2_1

基于3.1.6实现的 RunnerV2 类主要针对比较简单的模型。而在本章中,模型由多个算子组合而成,通常比较复杂,因此本节继续完善并实现一个改进版: RunnerV2_1类,其主要加入的功能有:

  1. 支持自定义算子的梯度计算,在训练过程中调用self.loss_fn.backward()从损失函数开始反向计算梯度;
  2. 每层的模型保存和加载,将每一层的参数分别进行保存和加载。

代码如下:

#完善runner类
import os


class RunnerV2_1(object):
    def __init__(self, model, optimizer, metric, loss_fn, **kwargs):
        self.model = model
        self.optimizer = optimizer
        self.loss_fn = loss_fn
        self.metric = metric

        # 记录训练过程中的评估指标变化情况
        self.train_scores = []
        self.dev_scores = []

        # 记录训练过程中的评价指标变化情况
        self.train_loss = []
        self.dev_loss = []

    def train(self, train_set, dev_set, **kwargs):
        # 传入训练轮数,如果没有传入值则默认为0
        num_epochs = kwargs.get("num_epochs", 0)
        # 传入log打印频率,如果没有传入值则默认为100
        log_epochs = kwargs.get("log_epochs", 100)

        # 传入模型保存路径
        save_dir = kwargs.get("save_dir", None)

        # 记录全局最优指标
        best_score = 0
        # 进行num_epochs轮训练
        for epoch in range(num_epochs):
            X, y = train_set
            # 获取模型预测
            logits = self.model(X)
            # 计算交叉熵损失
            trn_loss = self.loss_fn(logits, y)  # return a tensor

            self.train_loss.append(trn_loss.item())
            # 计算评估指标
            trn_score = self.metric(logits, y).item()
            self.train_scores.append(trn_score)

            self.loss_fn.backward()

            # 参数更新
            self.optimizer.step()

            dev_score, dev_loss = self.evaluate(dev_set)
            # 如果当前指标为最优指标,保存该模型
            if dev_score > best_score:
                print(f"[Evaluate] best accuracy performence has been updated: {best_score:.5f} --> {dev_score:.5f}")
                best_score = dev_score
                if save_dir:
                    self.save_model(save_dir)

            if log_epochs and epoch % log_epochs == 0:
                print(f"[Train] epoch: {epoch}/{num_epochs}, loss: {trn_loss.item()}")

    def evaluate(self, data_set):
        X, y = data_set
        # 计算模型输出
        logits = self.model(X)
        # 计算损失函数
        loss = self.loss_fn(logits, y).item()
        self.dev_loss.append(loss)
        # 计算评估指标
        score = self.metric(logits, y).item()
        self.dev_scores.append(score)
        return score, loss

    def predict(self, X):
        return self.model(X)

    def save_model(self, save_dir):
        # 对模型每层参数分别进行保存,保存文件名称与该层名称相同
        for layer in self.model.layers:  # 遍历所有层
            if isinstance(layer.params, dict):
                torch.save(layer.params, os.path.join(save_dir, layer.name + ".pdparams"))

    def load_model(self, model_dir):
        # 获取所有层参数名称和保存路径之间的对应关系
        model_file_names = os.listdir(model_dir)
        name_file_dict = {}
        for file_name in model_file_names:
            name = file_name.replace(".pdparams", "")
            name_file_dict[name] = os.path.join(model_dir, file_name)

        # 加载每层参数
        for layer in self.model.layers:  # 遍历所有层
            if isinstance(layer.params, dict):
                name = layer.name
                file_path = name_file_dict[name]
                layer.params = torch.load(file_path)

 

4.2.6 模型训练

使用训练集和验证集进行模型训练,共训练2000个epoch。评价指标为accuracy

代码实现如下:

#模型训练
from nndl.metric import accuracy
torch.random.manual_seed(123)
epoch_num = 1000

model_saved_dir = "model"

# 输入层维度为2
input_size = 2
# 隐藏层维度为5
hidden_size = 5
# 输出层维度为1
output_size = 1

# 定义网络
model = Model_MLP_L2(input_size=input_size, hidden_size=hidden_size, output_size=output_size)

# 损失函数
loss_fn = BinaryCrossEntropyLoss(model)

# 优化器
learning_rate = 0.2
optimizer = BatchGD(learning_rate, model)

# 评价方法
metric = accuracy

# 实例化RunnerV2_1类,并传入训练配置
runner = RunnerV2_1(model, optimizer, metric, loss_fn)

runner.train([X_train, y_train], [X_dev, y_dev], num_epochs=epoch_num, log_epochs=50, save_dir=model_saved_dir)

运行结果

 NNDL 实验五 前馈神经网络(1)二分类任务_第7张图片

 NNDL 实验五 前馈神经网络(1)二分类任务_第8张图片

 

 可视化观察训练集与验证集的损失函数变化情况。

#可视化观察训练集与验证集的损失函数变化情况
# 打印训练集和验证集的损失
import matplotlib.pyplot as plt

plt.figure()
plt.plot(range(epoch_num), runner.train_loss, color="#e4007f", label="Train loss")
plt.plot(range(epoch_num), runner.dev_loss, color="#f19ec2", linestyle='--', label="Dev loss")
plt.xlabel("epoch", fontsize='large')
plt.ylabel("loss", fontsize='large')
plt.legend(fontsize='x-large')
plt.savefig('fw-loss2.pdf')
plt.show()

运行结果

NNDL 实验五 前馈神经网络(1)二分类任务_第9张图片 

4.2.7 性能评价

使用测试集对训练中的最优模型进行评价,观察模型的评价指标。

#性能评价
# 加载训练好的模型
runner.load_model(model_saved_dir)
# 在测试集上对模型进行评价
score, loss = runner.evaluate([X_test, y_test])

print("[Test] score/loss: {:.4f}/{:.4f}".format(score, loss))

运行结果

 

 从结果来看,模型在测试集上取得了较高的准确率。

下面对结果进行可视化:

代码实现:

import math

# 均匀生成40000个数据点
x1, x2 = torch.meshgrid(torch.linspace(-math.pi, math.pi, 200), torch.linspace(-math.pi, math.pi, 200))
x = torch.stack([torch.flatten(x1), torch.flatten(x2)], axis=1)

# 预测对应类别
y = runner.predict(x)
y = torch.squeeze((y>=0.5).to(dtype=torch.float32),axis=-1)

# 绘制类别区域
plt.ylabel('x2')
plt.xlabel('x1')
plt.scatter(x[:,0].tolist(), x[:,1].tolist(), c=y.tolist(), cmap=plt.cm.Spectral)

plt.scatter(X_train[:, 0].tolist(), X_train[:, 1].tolist(), marker='*', c=torch.squeeze(y_train,axis=-1).tolist())
plt.scatter(X_dev[:, 0].tolist(), X_dev[:, 1].tolist(), marker='*', c=torch.squeeze(y_dev,axis=-1).tolist())
plt.scatter(X_test[:, 0].tolist(), X_test[:, 1].tolist(), marker='*', c=torch.squeeze(y_test,axis=-1).tolist())
plt.show()

运行结果

NNDL 实验五 前馈神经网络(1)二分类任务_第10张图片

这里从结果可以看出,按着给的参考代码修改后跑完一遍代码发现最后的分类结果却是一条直线并没有将弯月形数据集完美的完成二分类,这是由于上面我们跑的代码,我们只设置了一层神经网络,所以导致没有完美分类好,对于如何让它可以分类好,其实在参考代码的后面就有,那部分属于是下个实验的内容,所以先留个疑问。

【思考题】对比

3.1 基于Logistic回归二分类任务 4.2 基于前馈神经网络二分类任务

谈谈自己的看法

 Logistic回归的二分类任务比较简单,并且它属于是线性的,对于一些普通简单的数据集可能有很好的分类效果但是对于一些类似于弯月数据集复杂的数据集可能就不太适用了。

前馈神经网络二分类任务相较于前者,它是属于非线性的,因为神经网络含有激活函数,激活函数可以将线性的转化为非线性,对于像弯月型复杂的数据集有很好的分类效果。

总结

这次实验是前馈神经网络对二分类问题,上次实验是用Logistic回归的二分类任务,当时我们在使用弯月数据集时,我们班好多数据集都发生了错误,建立的弯月形数据集没有形成弯月,后来被我们班里大牛发现,同时老师也指出,这次我才及时做出了修改,并且这次前馈神经网络,我做到最后发现跑完代码有了一些小问题,按理来说最后的结果应为

NNDL 实验五 前馈神经网络(1)二分类任务_第11张图片

,这次留一个小疑问,自己慢慢琢磨并询问班级里的大佬,下次实验解决它。这次实验还是收获了很多,机器学习学过,但是自己当时学的太迷糊,这次自己改代码,跑代码,遇到问题,解决问题,这种感觉确实让人很满足, 并且让我对前馈神经网络有了更加深入理解和了解。

参考资料和博客

ref

NNDL 实验4(上) - HBU_DAVID - 博客园 (cnblogs.com)

torch.nn.Linear详解

仿射变换 

弯月数据集之谜

你可能感兴趣的:(深度学习,神经网络,分类,人工智能)