赞
踩
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 模型构建
为了更高效的构建前馈神经网络,我们先定义每一层的算子,然后再通过算子组合构建整个前馈神经网络。
我将代码注释全部写在代码中,请大家在运行代码的时候仔细看看注释~
从代码中可以看到类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包,结果都显示报错:
在终端里尝试导入包,也会报错:
导致这种错误 常有的原因如下:
from 子目录.包名 import 模块
的方式来导入。__init__.py
的文件。这个文件可以是空的,但它的存在告诉Python解释器该目录是一个包。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去除即可解决问题。
本文章内容借鉴出处见下,在此鸣谢:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。