Moon数据集 前馈神经网络.二分类任务 [HBU]

1数据集构建

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

代码(提前将弯月数据集导入进项目中):

#深度学习
import matplotlib.pyplot as plt
from nndl.dataset import make_moons #导入Moon1000数据集

# 采样1000个样本
n_samples = 1000

#make_moons函数接受三个参数 :n_samples设定生成的数据样本量
    #shuffle决定了是否要随机打乱数据 noise设定数据的噪声比例
X, y = make_moons(n_samples=n_samples, shuffle=True, noise=0.1)

#训练集640条,验证集160条,测试集200条
num_train = 640
num_dev = 160
num_test = 200

#切分数据集,将输入x 输出y分开
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:]

#用reshape方法将三个集转换成列向量(从默认的二维数组转化成[-1,1]的形状)
y_train = y_train.reshape([-1, 1])
y_dev = y_dev.reshape([-1, 1])
y_test = y_test.reshape([-1, 1])

plt.figure(figsize=(5, 5))
#绘制散点图。X[: , 0]和Y[: ,1]是散点图x和y的坐标
#marker参数用于指定散点的形状,此处用*表示
#c参数用于指定颜色,c是y的tolist()形式,意味着散点图中的每一个点的颜色都将根据y中的对应值来决定
plt.scatter(x=X[:, 0].tolist(), y=X[:, 1].tolist(), marker='*', c=y.tolist())

#设置x,y轴的范围,都是-3 到 4
plt.xlim(-3, 4)
plt.ylim(-3, 4)
plt.savefig('linear-dataset-vis.pdf')
plt.show()

结果:
当noise = 0.5时                                                                      当noise = 0.1时:

更新一下弯月数据集,需要注意,此弯月数据集使用了paddle框架:

import math
import copy
import paddle
import numpy as np
from sklearn.datasets import load_iris

#新增make_moons函数
def make_moons(n_samples=1000, shuffle=True, noise=None):
    """
    生成带噪音的弯月形状数据
    输入:
        - n_samples:数据量大小,数据类型为int
        - shuffle:是否打乱数据,数据类型为bool
        - noise:以多大的程度增加噪声,数据类型为None或float,noise为None时表示不增加噪声
    输出:
        - X:特征数据,shape=[n_samples,2]
        - y:标签数据, shape=[n_samples]
    """
    n_samples_out = n_samples // 2
    n_samples_in = n_samples - n_samples_out

    #采集第1类数据,特征为(x,y)
    #使用'paddle.linspace'在0到pi上均匀取n_samples_out个值
    #使用'paddle.cos'计算上述取值的余弦值作为特征1,使用'paddle.sin'计算上述取值的正弦值作为特征2
    outer_circ_x = paddle.cos(paddle.linspace(0, math.pi, n_samples_out))
    outer_circ_y = paddle.sin(paddle.linspace(0, math.pi, n_samples_out))

    inner_circ_x = 1 - paddle.cos(paddle.linspace(0, math.pi, n_samples_in))
    inner_circ_y = 0.5 - paddle.sin(paddle.linspace(0, math.pi, n_samples_in))
    
    print('outer_circ_x.shape:', outer_circ_x.shape, 'outer_circ_y.shape:', outer_circ_y.shape)
    print('inner_circ_x.shape:', inner_circ_x.shape, 'inner_circ_y.shape:', inner_circ_y.shape)
    
    #使用'paddle.concat'将两类数据的特征1和特征2分别延维度0拼接在一起,得到全部特征1和特征2
    #使用'paddle.stack'将两类特征延维度1堆叠在一起
    X = paddle.stack(
        [paddle.concat([outer_circ_x, inner_circ_x]),
        paddle.concat([outer_circ_y, inner_circ_y])],
        axis=1
    )

    print('after concat shape:', paddle.concat([outer_circ_x, inner_circ_x]).shape)
    print('X shape:', X.shape)

    #使用'paddle. zeros'将第一类数据的标签全部设置为0
    #使用'paddle. ones'将第一类数据的标签全部设置为1
    y =paddle.concat(
        [paddle.zeros(shape=[n_samples_out]), paddle.ones(shape=[n_samples_in])]
    )

    print('y shape:', y.shape)

    #如果shuffle为True,将所有数据打乱
    if shuffle:
        #使用'paddle.randperm'生成一个数值在0到X.shape[0],随机排列的一维Tensor做索引值,用于打乱数据
        idx = paddle.randperm(X.shape[0])
        X = X[idx]
        y = y[idx]

    #如果noise不为None,则给特征值加入噪声
    if noise is not None:
        #使用'paddle.normal'生成符合正态分布的随机Tensor作为噪声,并加到原始特征上
        X += paddle.normal(mean=0.0, std=noise, shape=X.shape)

    return X, y


#加载数据集
def load_data(shuffle=True):
    """
    加载鸢尾花数据
    输入:
        - shuffle:是否打乱数据,数据类型为bool
    输出:
        - X:特征数据,shape=[150,4]
        - y:标签数据, shape=[150,3]
    """
    #加载原始数据
    X = np.array(load_iris().data, dtype=np.float32)
    y = np.array(load_iris().target, dtype=np.int64)

    X = paddle.to_tensor(X)
    y = paddle.to_tensor(y)

    #数据归一化
    X_min = paddle.min(X, axis=0)
    X_max = paddle.max(X, axis=0)
    X = (X-X_min) / (X_max-X_min)

    #如果shuffle为True,随机打乱数据
    if shuffle:
        idx = paddle.randperm(X.shape[0])
        X_new = copy.deepcopy(X)
        y_new = copy.deepcopy(y)
        for i in range(X.shape[0]):
            X_new[i] = X[idx[i]]
            y_new[i] = y[idx[i]]
        X = X_new
        y = y_new

    return X, y


2 模型构建

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

Z^{(l)}=A^{(l-1)}W^{^{l}}+b^{l}

我将代码注释全部写在代码中,请大家在运行代码的时候仔细看看注释~

从代码中可以看到类Linear继承自Op类。在Linear中,定义了两个方法:__init__forward

线性层算子 代码:

#模型构建
import torch
from nndl.op import Op

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

        #初始化权重的torch张量和偏置的张量 存储在self.params字典中
        self.params = {}
        # 初始化权重
        self.params['W'] = weight_init(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

Logistic算子 代码:

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

    #forward()接受一个参数inputs,返回输出outputs
    def forward(self,inputs):
        outputs = 1.0 / (1.0 + torch.exp(-inputs))
        self.outputs = outputs
        return outputs

层的串行组合 

在定义了神经层的线性层算子和激活函数算子之后,我们可以不断交叉重复使用它们来构建一个多层的神经网络。下面我们实现一个两层的用于二分类任务的前馈神经网络,选用Logistic作为激活函数,可以利用上面实现的线性层和激活函数算子来组装。代码实现如下:​​​​​​

#层的串行组合
#实现两层前馈神经网络 L2正则化
class Model_MLP_L2(Op):
    def __init__(self,input_size,hidden_size,output_size):
        #input_size :输入维度
        #hidden_size:隐藏层神经元数量
        #output_size:输出维度
        self.function1 =Linear(input_size, hidden_size, name = 'fc1')
        self.act_fn1 = Logistic()
        self.function2 = 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.function1(X)
        a1 = self.act_fn1(z1)
        z2 = self.function2(a1)
        a2 = self.act_fn2(z2)

        return a2

有了线性层、Logistic、层的串行组合算子,接下来对模型进行测试:

我们设置输入层维度为5,隐藏层维度为10,输出层维度为1. 使用Logistic函数作为激活函数。

使用torch.rand()生成维度为1行5列的二维张量,生成的值在0-1之间。观察结果。

# 实例化模型
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)

结果: 张量的形状为1行1列


3 损失函数

二分类交叉熵损失函数(见第三章)。分类任务的损失函数一般使用交叉熵损失函数,而不使用平方损失函数,具体原因 可见我之前的博客:
[23-24 秋学期] NNDL-作业2 HBU-CSDN博客

使用算子定义交叉熵损失函数,代码:(代码解释、注释都已写清)

#交叉熵损失函数
#类定义,B_EntirpyLoss类继承了Op类
class B_EntirpyLoss(Op):
    #__init__方法是一个特殊方法,对象被创建时,该方法被自动调用
    def __init__(self,model):
        #在该方法中 定义了三个实例变量,初始值都为None
        self.predicts = None
        self.labels = None
        self.num = None

    #call方法使得一个对象可以像函数那样被调用,它接受预测值和标签作为参数,并传递给forward方法
    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] #获取预测值predicts张量的第一维大小
        loss = -1/self.num*(torch.matmul(self.labels.t(), torch.log(self.predicts)) + torch.matmul((1-self.labels.t())
                                                                                    , torch.log(1-self.predicts)))

        #torch.squeeze()压缩维度。用于将输入张量形状中的1去掉,然后返回新张量。
        #dim = 1指定了要在哪个维度上去掉1
        loss = torch.squeeze(loss,dim = 1) #为何要压缩维度?
        return loss

    def backward(self):
        # 计算损失
        loss = torch.nn.functional.binary_cross_entropy_with_logits(self.predicts, self.labels)
        # 自动微分
        loss.backward()

4 模型优化

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

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

(1)反向传播算法

第1步是前向计算,可以利用算子的forward()方法来实现;

第2步是反向计算梯度,可以利用算子的backward()方法来实现;

第3步中的计算参数梯度也放到backward()中实现,更新参数放到另外的优化器中专门进行。

(2)损失函数

二分类交叉熵损失函数;实现损失函数的backward(),forward()和backward()代码如下:

#交叉熵损失函数
#类定义,B_EntirpyLoss类继承了Op类
class B_EntirpyLoss(Op):
    #__init__方法是一个特殊方法,对象被创建时,该方法被自动调用
    def __init__(self,model):
        #在该方法中 定义了三个实例变量,初始值都为None
        self.predicts = None
        self.labels = None
        self.num = None

    #call方法使得一个对象可以像函数那样被调用,它接受预测值和标签作为参数,并传递给forward方法
    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] #获取预测值predicts张量的第一维大小
        loss = -1/self.num*(torch.matmul(self.labels.t(), torch.log(self.predicts)) + torch.matmul((1-self.labels.t())
                                                                                    , torch.log(1-self.predicts)))

        #torch.squeeze()压缩维度。用于将输入张量形状中的1去掉,然后返回新张量。
        #dim = 1指定了要在哪个维度上去掉1
        loss = torch.squeeze(loss,dim = 1) #为何要压缩维度?
        return loss

    def backward(self):
        # 计算损失
        loss = torch.nn.functional.binary_cross_entropy_with_logits(self.predicts, self.labels)
        # 自动微分
        loss.backward()

在PyTorch中,损失函数对模型预测的导数通常通过自动微分(autograd)来计算,功能精确、灵活。binary_cross_entropy_with_logits函数会内部计算梯度,然后通过调用loss.backward(),系统会自动计算损失对模型参数的梯度。

(3)Logistic算子

为Logistic算子增加反向函数,详细注释和参数、方法说明都写在代码中,代码如下:

#logistic算子
#使用Logistic激活函数,为Logistic算子增加反向函数
class Logistic(Op):
    '''
    初始化三个属性
    inputs表示  输入数据
    outputs表示 输出数据
    params表示 参数
    '''
    def __init__(self):
        self.inputs = None
        self.outputs = None
        self.params = None

    #forward方法用来实现Logistic函数的前向传播
    #将outputs存储在self.outputs中输出
    def forward(self,inputs):
        outputs = 1.0/(1.0+torch.exp(-inputs))
        self.outputs = outputs
        return outputs

    #实现反向传播。接收参数grads,输出对某个变量的梯度
    def backward(self, grads):
        #计算Logistic激活函数对输入的导数
        outputs_grads_inputs = torch.multiply(self.outputs,(1.0-self.outputs))
        return torch.multiply(grads,outputs_grads_inputs)

(4)线性层

线性层输入的梯度;计算线性层参数的梯度;

这段代码定义了一个名为Linear的类,表示一个线性操作,它是Op类的子类。这个线性操作对应于神经网络中的全连接层(也称为线性层)。代码如下:

#线性层 定义名为Linear的类,表示一个线性操作,时Op的子类
class Linear(Op):
    #初始化方法__init__
    def __init__(self,input_size,output_size,name,weight_init=torch.rand,bias_init=torch.zeros):
        '''
        input_size: 输入大小
        output_size: 输出大小
        name: 定义此层的名称
        weight_init:使用随机数torch.rand()初始化权重
        bias_init:使用零向量torch.zeros初始化偏置项
        '''
        self.params = {} #是一个字典,用于存储权重W和偏置b
        self.params['W'] = weight_init(size=[input_size,output_size])
        self.params['b'] = bias_init(size=[1,output_size])

        self.inputs = None
        self.grads = {} #是一个字典,用于存储每个参数的梯度
        self.name = name

    #前向传播方法 接收一个输入inputs,并计算输出
    def forward(self,inputs):
        self.inputs = inputs
        #self.inputs = torch.tensor(inputs)

        # 前向传播 W*X+b
        outputs = torch.matmul(self.inputs,self.params['W'])+self.params['b']
        return outputs

    #反向传播算法 输入:损失函数对当前层输出的导数(误差梯度)
    def backward(self, grads):
        """
        输入: grads:损失函数对当前层输出的导数
        输出: 损失函数对当前层输入的导数
        """

        #计算损失函数对当前层输出的导数(误差梯度),计算w b的梯度并存储在self.grads字典中
        self.grads['W'] = torch.matmul(self.inputs.T , grads)
        self.grads['b'] = torch.sum(grads,dim = 0)

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

(5)整个网络

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

输入层-隐藏层-输出层。代码如下:

#实现一个两层前馈神经网络
class Model_MLP_L2(Op):
    def __init__(self,input_size,hidden_size,output_size):
        #输入层至隐层
        #线性层
        self.function1 = Linear(input_size,hidden_size,name='fc1')
        #激活函数层 Logistic
        self.act_fn1 = Logistic()

        #隐层至输出层
        self.function2 = Linear(hidden_size,output_size,name='fc2')
        self.act_fn2 = Logistic()

        self.layers=[self.function1,self.act_fn1,self.function2,self.act_fn2]

    def __call__(self, X):
        #调用前向传播函数
        return self.forward(X)

    #前向传播
    def forward(self, X):
        z1 = self.function1(X) #净活性值z1
        a1 = self.act_fn1(z1) #激活后的活性值a1
        z2 = self.function2(X)
        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.function2.backward(loss_grad_z2)
        loss_grad_z1 = self.act_fn1.backward(loss_grad_a1)
        loss_grad_inputs = self.function1.backward(loss_grad_z1) #输入

(6)优化器

在计算好神经网络参数的梯度之后,我们将梯度下降法中参数的更新过程实现在优化器中。与第3章中实现的梯度下降优化器SimpleBatchGD不同的是,此处的优化器需要遍历每层,对每层的参数分别做更新。

#优化器
from nndl.opitimizer import Optimizer

#定义优化器类,继承了Optimizer类,用于实现批量梯度下降法
class BatchGD(Optimizer):
    #BatchGD是批量梯度下降的简称,是一种常用的优化算法,用于在每个训练步骤中更新模型的参数
    def __init__(self, init_lr, model):
        #初始学习率init_lr   要优化的模型model
        super(BatchGD, self).__init__(init_lr=init_lr, model=model)

    def step(self): #用以更新模型参数
        # 参数更新
        for layer in self.model.layers:  # 遍历所有层
            #self.model是BatchGD类优化的模型,model.layers是模型的所有层

            #检查当前层的参数是否是字典类型
            if isinstance(layer.params, dict):
                #若当前层的参数是字典类型,这个循环遍历此字典的所有键(即参数的名称)
                for key in layer.params.keys():
                    #实现参数更新
                    #通过计算当前参数的梯度layers.grads[key]与学习率的乘积,然后将差值从当前数值中减去,得到新参数值
                    layer.params[key] = layer.params[key] - self.init_lr * layer.grads[key]

BatchGD类的功能就是使用批量梯度下降算法来更新模型的每个层的参数


5 完善Runner类:RunnerV2_1

支持自定义算子的梯度计算,在训练过程中调用self.loss_fn.backward()从损失函数开始反向计算梯度;

每层的模型保存和加载,将每一层的参数分别进行保存和加载。

#完善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)

6 模型训练

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

from 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)

metric包中的accuary代码为:

import torch
 
 
def accuracy(preds, labels):
    """
    输入:
        - preds:预测值,二分类时,shape=[N, 1],N为样本数量,多分类时,shape=[N, C],C为类别数量
        - labels:真实标签,shape=[N, 1]
    输出:
        - 准确率:shape=[1]
    """
    # 判断是二分类任务还是多分类任务,preds.shape[1]=1时为二分类任务,preds.shape[1]>1时为多分类任务
    if preds.shape[1] == 1:
        data_float = torch.randn(preds.shape[0], preds.shape[1])
        # 二分类时,判断每个概率值是否大于0.5,当大于0.5时,类别为1,否则类别为0
        # 使用'torch.cast'将preds的数据类型转换为float32类型
        preds = (preds>=0.5).type(torch.float32)
    else:
        # 多分类时,使用'torch.argmax'计算最大元素索引作为类别
        data_float = torch.randn(preds.shape[0], preds.shape[1])
        preds = torch.argmax(preds,dim=1, dtype=torch.int32)
    return torch.mean(torch.eq(preds, labels).type(torch.float32))
 
 

训练结果:

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

# 打印训练集和验证集的损失
plt.figure()
plt.plot(range(epoch_num), runner.train_loss, color="#8E004D", label="Train loss")
plt.plot(range(epoch_num), runner.dev_loss, color="#E20079", linestyle='--', label="Dev loss")
plt.xlabel("epoch", fontsize='x-large')
plt.ylabel("loss", fontsize='x-large')
plt.legend(fontsize='large')
plt.savefig('fw-loss2.pdf')
plt.show()

     可见,随着训练epoch数量的增加和训练次数的增多,Train loss训练误差和验证集误差都在减小,初期下降的速度快,到了后期(约400次之后)损失值逐渐平稳。


7 性能评价

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

# 加载训练好的模型
runner.load_model(model_saved_dir)
# 在测试集上对模型进行评价
score, loss = runner.evaluate([X_test, y_test])
 
print("[Test] score/loss: {:.4f}/{:.4f}".format(score, loss))

【思考题】

对比“基于Logistic回归的二分类任务”与“基于前馈神经网络的二分类任务”,谈谈自己的看法。

答:

1.模型复杂度:

Logistic回归:是一个线性模型,其模型复杂度相对较低。它只能学习线性的决策边界,对于复杂的非线性问题可能效果有限。

前馈神经网络:可以学习复杂的非线性决策边界,对于复杂的输入数据有更好的适应性。但随着网络深度的增加,模型复杂度也会显著提高。

2.特征处理:

Logistic回归:直接在原始特征上进行建模,需要手动选择和工程化特征。

前馈神经网络:可以自动学习特征,从原始输入中学习到更高级别的特征表示。

3.训练方法:

Logistic回归:通常使用梯度下降法进行优化,目标是最小化损失函数。

前馈神经网络:也使用梯度下降法进行优化,但训练过程通常更加复杂,需要反向传播和参数更新。

4.泛化能力:

Logistic回归:由于其模型的简单性,更容易过拟合训练数据,泛化能力可能较差。

前馈神经网络:具有更强的表示能力,但也更容易过拟合。为了提高泛化能力,通常需要使用正则化等技术。

5.计算需求:

Logistic回归:模型简单,需要的计算资源相对较少,可以更快地训练和预测。

前馈神经网络:模型复杂,需要的计算资源相对较多,特别是当网络深度或宽度很大时。

6.灵活性:

Logistic回归:是一种经典的机器学习方法,具有较高的稳定性和可解释性。

前馈神经网络:具有更高的灵活性,可以适应各种复杂的模式和关系。但这也意味着其结构、设计和训练过程通常更加复杂。

可视化对比推荐去看此篇博客,能够更直观的感受两种回归的性能与解决问题的能力:
HBU-NNDL 实验五 前馈神经网络(1)二分类任务_二分类任务模型选择-CSDN博客


错误总结与反思:

1.
在书写代码时,我提前将老师分享的NNDL包导入到了python项目中,但是当我在main主程序中去调用自定义包时,发生了报错,报错内容显示我的项目中并不存在这些自定义包,下图是我的python工程目录:

在本次实验中,涉及调用了opitimizer包(优化器)和metric包,结果都显示报错:

在终端里尝试导入包,也会报错:

导致这种错误 常有的原因如下:

  1. 路径问题:检查自定义包的路径是否正确。如果自定义包位于不同的目录下,你可能需要在导入语句中使用相对路径或绝对路径。例如,如果自定义包位于项目根目录的子目录中,可以使用from 子目录.包名 import 模块的方式来导入。
  2. 拼写错误:检查导入语句中的包名和模块名是否拼写正确。Python对大小写敏感,因此确保拼写与实际文件名完全一致。
  3. 初始化文件:确保自定义包的目录中包含一个名为__init__.py的文件。这个文件可以是空的,但它的存在告诉Python解释器该目录是一个包。
  4. 环境问题:如果你在虚拟环境中运行Python项目,确保你的虚拟环境已经激活,并且自定义包已经安装到该虚拟环境中。
  5. 循环导入:如果自定义包之间存在循环依赖的情况,可能会导致导入错误。尝试重新组织代码以避免循环依赖,或者在适当的地方使用局部导入。
  6. 语法错误:检查自定义包中的代码是否存在语法错误,这可能会导致导入失败。
  7. 自定义包的调用方式:确保在main程序中以正确的方式调用自定义包中的模块或函数。例如,如果自定义包中包含一个名为my_module的模块,你可以通过import my_module来导入该模块,然后使用my_module.some_function()的方式来调用其中的函数。

   检查路径,发现了自己的路径问题:opitimizer包和metric包都是在nndl目录下的,所以要将nndl这一级目录加上:

此时编译器正确,ModuleNotFoundError被解决。我的问题就是调用包时的路径写错。

2.

torch.seed()和paddle.seed()。

二者都是用来设置随机数生成器的种子,以确保随机过程能够产生相同的随机序列。

需要注意的区别是,torch.seed()不接收函数,使用当前系统时间作为种子来初始化随机数生成器。在PyTorch中,还可以使用torch.manual_seed()为CPU设置随机数种子,以及使用torch.cuda.manual_seed()和torch.cuda.manual_seed_all()为GPU设置随机数种子。而paddle.seed()接收一个整数函数

这是我报错的截图,将torch.seed()中的参数10去除即可解决问题。


本文章内容借鉴出处见下,在此鸣谢:

HBU-NNDL 实验五 前馈神经网络(1)二分类任务_二分类任务模型选择-CSDN博客

NNDL实验: Moon1000数据集 - 弯月消失之谜_读取弯月数据集-CSDN博客

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

洛杉矶县牛肉板面

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值