当前位置:   article > 正文

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

moon数据集

1数据集构建

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

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

  1. #深度学习
  2. import matplotlib.pyplot as plt
  3. from nndl.dataset import make_moons #导入Moon1000数据集
  4. # 采样1000个样本
  5. n_samples = 1000
  6. #make_moons函数接受三个参数 :n_samples设定生成的数据样本量
  7. #shuffle决定了是否要随机打乱数据 noise设定数据的噪声比例
  8. X, y = make_moons(n_samples=n_samples, shuffle=True, noise=0.1)
  9. #训练集640条,验证集160条,测试集200条
  10. num_train = 640
  11. num_dev = 160
  12. num_test = 200
  13. #切分数据集,将输入x 输出y分开
  14. X_train, y_train = X[:num_train], y[:num_train]
  15. X_dev, y_dev = X[num_train:num_train + num_dev], y[num_train:num_train + num_dev]
  16. X_test, y_test = X[num_train + num_dev:], y[num_train + num_dev:]
  17. #用reshape方法将三个集转换成列向量(从默认的二维数组转化成[-1,1]的形状)
  18. y_train = y_train.reshape([-1, 1])
  19. y_dev = y_dev.reshape([-1, 1])
  20. y_test = y_test.reshape([-1, 1])
  21. plt.figure(figsize=(5, 5))
  22. #绘制散点图。X[: , 0]和Y[: ,1]是散点图x和y的坐标
  23. #marker参数用于指定散点的形状,此处用*表示
  24. #c参数用于指定颜色,c是y的tolist()形式,意味着散点图中的每一个点的颜色都将根据y中的对应值来决定
  25. plt.scatter(x=X[:, 0].tolist(), y=X[:, 1].tolist(), marker='*', c=y.tolist())
  26. #设置x,y轴的范围,都是-3 到 4
  27. plt.xlim(-3, 4)
  28. plt.ylim(-3, 4)
  29. plt.savefig('linear-dataset-vis.pdf')
  30. plt.show()

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

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

  1. import math
  2. import copy
  3. import paddle
  4. import numpy as np
  5. from sklearn.datasets import load_iris
  6. #新增make_moons函数
  7. def make_moons(n_samples=1000, shuffle=True, noise=None):
  8. """
  9. 生成带噪音的弯月形状数据
  10. 输入:
  11. - n_samples:数据量大小,数据类型为int
  12. - shuffle:是否打乱数据,数据类型为bool
  13. - noise:以多大的程度增加噪声,数据类型为None或float,noise为None时表示不增加噪声
  14. 输出:
  15. - X:特征数据,shape=[n_samples,2]
  16. - y:标签数据, shape=[n_samples]
  17. """
  18. n_samples_out = n_samples // 2
  19. n_samples_in = n_samples - n_samples_out
  20. #采集第1类数据,特征为(x,y)
  21. #使用'paddle.linspace'在0到pi上均匀取n_samples_out个值
  22. #使用'paddle.cos'计算上述取值的余弦值作为特征1,使用'paddle.sin'计算上述取值的正弦值作为特征2
  23. outer_circ_x = paddle.cos(paddle.linspace(0, math.pi, n_samples_out))
  24. outer_circ_y = paddle.sin(paddle.linspace(0, math.pi, n_samples_out))
  25. inner_circ_x = 1 - paddle.cos(paddle.linspace(0, math.pi, n_samples_in))
  26. inner_circ_y = 0.5 - paddle.sin(paddle.linspace(0, math.pi, n_samples_in))
  27. print('outer_circ_x.shape:', outer_circ_x.shape, 'outer_circ_y.shape:', outer_circ_y.shape)
  28. print('inner_circ_x.shape:', inner_circ_x.shape, 'inner_circ_y.shape:', inner_circ_y.shape)
  29. #使用'paddle.concat'将两类数据的特征1和特征2分别延维度0拼接在一起,得到全部特征1和特征2
  30. #使用'paddle.stack'将两类特征延维度1堆叠在一起
  31. X = paddle.stack(
  32. [paddle.concat([outer_circ_x, inner_circ_x]),
  33. paddle.concat([outer_circ_y, inner_circ_y])],
  34. axis=1
  35. )
  36. print('after concat shape:', paddle.concat([outer_circ_x, inner_circ_x]).shape)
  37. print('X shape:', X.shape)
  38. #使用'paddle. zeros'将第一类数据的标签全部设置为0
  39. #使用'paddle. ones'将第一类数据的标签全部设置为1
  40. y =paddle.concat(
  41. [paddle.zeros(shape=[n_samples_out]), paddle.ones(shape=[n_samples_in])]
  42. )
  43. print('y shape:', y.shape)
  44. #如果shuffle为True,将所有数据打乱
  45. if shuffle:
  46. #使用'paddle.randperm'生成一个数值在0到X.shape[0],随机排列的一维Tensor做索引值,用于打乱数据
  47. idx = paddle.randperm(X.shape[0])
  48. X = X[idx]
  49. y = y[idx]
  50. #如果noise不为None,则给特征值加入噪声
  51. if noise is not None:
  52. #使用'paddle.normal'生成符合正态分布的随机Tensor作为噪声,并加到原始特征上
  53. X += paddle.normal(mean=0.0, std=noise, shape=X.shape)
  54. return X, y
  55. #加载数据集
  56. def load_data(shuffle=True):
  57. """
  58. 加载鸢尾花数据
  59. 输入:
  60. - shuffle:是否打乱数据,数据类型为bool
  61. 输出:
  62. - X:特征数据,shape=[150,4]
  63. - y:标签数据, shape=[150,3]
  64. """
  65. #加载原始数据
  66. X = np.array(load_iris().data, dtype=np.float32)
  67. y = np.array(load_iris().target, dtype=np.int64)
  68. X = paddle.to_tensor(X)
  69. y = paddle.to_tensor(y)
  70. #数据归一化
  71. X_min = paddle.min(X, axis=0)
  72. X_max = paddle.max(X, axis=0)
  73. X = (X-X_min) / (X_max-X_min)
  74. #如果shuffle为True,随机打乱数据
  75. if shuffle:
  76. idx = paddle.randperm(X.shape[0])
  77. X_new = copy.deepcopy(X)
  78. y_new = copy.deepcopy(y)
  79. for i in range(X.shape[0]):
  80. X_new[i] = X[idx[i]]
  81. y_new[i] = y[idx[i]]
  82. X = X_new
  83. y = y_new
  84. return X, y

2 模型构建

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

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

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

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

线性层算子 代码:

  1. #模型构建
  2. import torch
  3. from nndl.op import Op
  4. # 实现线性层算子
  5. class Linear(Op):
  6. def __init__(self, input_size, output_size, name, weight_init=torch.randn, bias_init=torch.zeros):
  7. """
  8. 输入:
  9. - input_size:输入数据维度
  10. - output_size:输出数据维度
  11. - name:算子名称
  12. - weight_init:权重初始化方式,默认使用'torch.randn'进行标准正态分布初始化
  13. - bias_init:偏置初始化方式,默认使用全0初始化
  14. """
  15. #初始化权重的torch张量和偏置的张量 存储在self.params字典中
  16. self.params = {}
  17. # 初始化权重
  18. self.params['W'] = weight_init(size=[input_size, output_size])
  19. # 初始化偏置
  20. self.params['b'] = bias_init(size=[1, output_size])
  21. self.inputs = None
  22. self.name = name
  23. #定义该层在前向传播时的行为 此方法使用初始化好的权重和偏置,对出入进行线性变换
  24. def forward(self, inputs):
  25. """
  26. 输入:- inputs:shape=[N,input_size], N是样本数量
  27. 输出:- outputs:预测值,shape=[N,output_size]
  28. """
  29. self.inputs = inputs
  30. outputs = torch.matmul(self.inputs, self.params['W']) + self.params['b']
  31. return outputs

Logistic算子 代码:

  1. #定义Logistic层算子
  2. class Logistic(Op):
  3. def __init__(self):
  4. self.inputs = None
  5. self.outputs = None
  6. #forward()接受一个参数inputs,返回输出outputs
  7. def forward(self,inputs):
  8. outputs = 1.0 / (1.0 + torch.exp(-inputs))
  9. self.outputs = outputs
  10. return outputs

层的串行组合 

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

  1. #层的串行组合
  2. #实现两层前馈神经网络 L2正则化
  3. class Model_MLP_L2(Op):
  4. def __init__(self,input_size,hidden_size,output_size):
  5. #input_size :输入维度
  6. #hidden_size:隐藏层神经元数量
  7. #output_size:输出维度
  8. self.function1 =Linear(input_size, hidden_size, name = 'fc1')
  9. self.act_fn1 = Logistic()
  10. self.function2 = Linear(hidden_size, output_size, name="fc2")
  11. self.act_fn2 = Logistic()
  12. def __call__(self, X):
  13. return self.forward(X)
  14. def forward(self, X):
  15. """
  16. 输入:
  17. - X:shape=[N,input_size], N是样本数量
  18. 输出:
  19. - a2:预测值,shape=[N,output_size]
  20. """
  21. z1 = self.function1(X)
  22. a1 = self.act_fn1(z1)
  23. z2 = self.function2(a1)
  24. a2 = self.act_fn2(z2)
  25. return a2

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

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

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

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

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


3 损失函数

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

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

  1. #交叉熵损失函数
  2. #类定义,B_EntirpyLoss类继承了Op类
  3. class B_EntirpyLoss(Op):
  4. #__init__方法是一个特殊方法,对象被创建时,该方法被自动调用
  5. def __init__(self,model):
  6. #在该方法中 定义了三个实例变量,初始值都为None
  7. self.predicts = None
  8. self.labels = None
  9. self.num = None
  10. #call方法使得一个对象可以像函数那样被调用,它接受预测值和标签作为参数,并传递给forward方法
  11. def __call__(self, predicts, labels):
  12. return self.forward(predicts, labels)
  13. def forward(self,predicts,labels):
  14. '''
  15. 输入:
  16. :predicts: 预测值,shape=[N,1] N为样本数量
  17. :labels: 真实标签,shape = [N,1]
  18. 输出:
  19. :损失:shape = [1]
  20. '''
  21. self.predicts = predicts
  22. self.labels = labels
  23. self.num = self.predicts.shape[0] #获取预测值predicts张量的第一维大小
  24. loss = -1/self.num*(torch.matmul(self.labels.t(), torch.log(self.predicts)) + torch.matmul((1-self.labels.t())
  25. , torch.log(1-self.predicts)))
  26. #torch.squeeze()压缩维度。用于将输入张量形状中的1去掉,然后返回新张量。
  27. #dim = 1指定了要在哪个维度上去掉1
  28. loss = torch.squeeze(loss,dim = 1) #为何要压缩维度?
  29. return loss
  30. def backward(self):
  31. # 计算损失
  32. loss = torch.nn.functional.binary_cross_entropy_with_logits(self.predicts, self.labels)
  33. # 自动微分
  34. loss.backward()

4 模型优化

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

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

(1)反向传播算法

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

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

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

(2)损失函数

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

  1. #交叉熵损失函数
  2. #类定义,B_EntirpyLoss类继承了Op类
  3. class B_EntirpyLoss(Op):
  4. #__init__方法是一个特殊方法,对象被创建时,该方法被自动调用
  5. def __init__(self,model):
  6. #在该方法中 定义了三个实例变量,初始值都为None
  7. self.predicts = None
  8. self.labels = None
  9. self.num = None
  10. #call方法使得一个对象可以像函数那样被调用,它接受预测值和标签作为参数,并传递给forward方法
  11. def __call__(self, predicts, labels):
  12. return self.forward(predicts, labels)
  13. def forward(self,predicts,labels):
  14. '''
  15. 输入:
  16. :predicts: 预测值,shape=[N,1] N为样本数量
  17. :labels: 真实标签,shape = [N,1]
  18. 输出:
  19. :损失:shape = [1]
  20. '''
  21. self.predicts = predicts
  22. self.labels = labels
  23. self.num = self.predicts.shape[0] #获取预测值predicts张量的第一维大小
  24. loss = -1/self.num*(torch.matmul(self.labels.t(), torch.log(self.predicts)) + torch.matmul((1-self.labels.t())
  25. , torch.log(1-self.predicts)))
  26. #torch.squeeze()压缩维度。用于将输入张量形状中的1去掉,然后返回新张量。
  27. #dim = 1指定了要在哪个维度上去掉1
  28. loss = torch.squeeze(loss,dim = 1) #为何要压缩维度?
  29. return loss
  30. def backward(self):
  31. # 计算损失
  32. loss = torch.nn.functional.binary_cross_entropy_with_logits(self.predicts, self.labels)
  33. # 自动微分
  34. loss.backward()

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

(3)Logistic算子

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

  1. #logistic算子
  2. #使用Logistic激活函数,为Logistic算子增加反向函数
  3. class Logistic(Op):
  4. '''
  5. 初始化三个属性
  6. inputs表示 输入数据
  7. outputs表示 输出数据
  8. params表示 参数
  9. '''
  10. def __init__(self):
  11. self.inputs = None
  12. self.outputs = None
  13. self.params = None
  14. #forward方法用来实现Logistic函数的前向传播
  15. #将outputs存储在self.outputs中输出
  16. def forward(self,inputs):
  17. outputs = 1.0/(1.0+torch.exp(-inputs))
  18. self.outputs = outputs
  19. return outputs
  20. #实现反向传播。接收参数grads,输出对某个变量的梯度
  21. def backward(self, grads):
  22. #计算Logistic激活函数对输入的导数
  23. outputs_grads_inputs = torch.multiply(self.outputs,(1.0-self.outputs))
  24. return torch.multiply(grads,outputs_grads_inputs)

(4)线性层

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

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

  1. #线性层 定义名为Linear的类,表示一个线性操作,时Op的子类
  2. class Linear(Op):
  3. #初始化方法__init__
  4. def __init__(self,input_size,output_size,name,weight_init=torch.rand,bias_init=torch.zeros):
  5. '''
  6. input_size: 输入大小
  7. output_size: 输出大小
  8. name: 定义此层的名称
  9. weight_init:使用随机数torch.rand()初始化权重
  10. bias_init:使用零向量torch.zeros初始化偏置项
  11. '''
  12. self.params = {} #是一个字典,用于存储权重W和偏置b
  13. self.params['W'] = weight_init(size=[input_size,output_size])
  14. self.params['b'] = bias_init(size=[1,output_size])
  15. self.inputs = None
  16. self.grads = {} #是一个字典,用于存储每个参数的梯度
  17. self.name = name
  18. #前向传播方法 接收一个输入inputs,并计算输出
  19. def forward(self,inputs):
  20. self.inputs = inputs
  21. #self.inputs = torch.tensor(inputs)
  22. # 前向传播 W*X+b
  23. outputs = torch.matmul(self.inputs,self.params['W'])+self.params['b']
  24. return outputs
  25. #反向传播算法 输入:损失函数对当前层输出的导数(误差梯度)
  26. def backward(self, grads):
  27. """
  28. 输入: grads:损失函数对当前层输出的导数
  29. 输出: 损失函数对当前层输入的导数
  30. """
  31. #计算损失函数对当前层输出的导数(误差梯度),计算w b的梯度并存储在self.grads字典中
  32. self.grads['W'] = torch.matmul(self.inputs.T , grads)
  33. self.grads['b'] = torch.sum(grads,dim = 0)
  34. #线性层输入的梯度
  35. return torch.matmul(grads, self.params['W'].T)

(5)整个网络

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

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

  1. #实现一个两层前馈神经网络
  2. class Model_MLP_L2(Op):
  3. def __init__(self,input_size,hidden_size,output_size):
  4. #输入层至隐层
  5. #线性层
  6. self.function1 = Linear(input_size,hidden_size,name='fc1')
  7. #激活函数层 Logistic
  8. self.act_fn1 = Logistic()
  9. #隐层至输出层
  10. self.function2 = Linear(hidden_size,output_size,name='fc2')
  11. self.act_fn2 = Logistic()
  12. self.layers=[self.function1,self.act_fn1,self.function2,self.act_fn2]
  13. def __call__(self, X):
  14. #调用前向传播函数
  15. return self.forward(X)
  16. #前向传播
  17. def forward(self, X):
  18. z1 = self.function1(X) #净活性值z1
  19. a1 = self.act_fn1(z1) #激活后的活性值a1
  20. z2 = self.function2(X)
  21. a2 = self.act_fn2(z2)
  22. return a2
  23. #反向传播计算
  24. def backward(self,loss_grad_a2):
  25. loss_grad_z2 = self.act_fn2.backward(loss_grad_a2) #求导
  26. loss_grad_a1 = self.function2.backward(loss_grad_z2)
  27. loss_grad_z1 = self.act_fn1.backward(loss_grad_a1)
  28. loss_grad_inputs = self.function1.backward(loss_grad_z1) #输入

(6)优化器

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

  1. #优化器
  2. from nndl.opitimizer import Optimizer
  3. #定义优化器类,继承了Optimizer类,用于实现批量梯度下降法
  4. class BatchGD(Optimizer):
  5. #BatchGD是批量梯度下降的简称,是一种常用的优化算法,用于在每个训练步骤中更新模型的参数
  6. def __init__(self, init_lr, model):
  7. #初始学习率init_lr 要优化的模型model
  8. super(BatchGD, self).__init__(init_lr=init_lr, model=model)
  9. def step(self): #用以更新模型参数
  10. # 参数更新
  11. for layer in self.model.layers: # 遍历所有层
  12. #self.model是BatchGD类优化的模型,model.layers是模型的所有层
  13. #检查当前层的参数是否是字典类型
  14. if isinstance(layer.params, dict):
  15. #若当前层的参数是字典类型,这个循环遍历此字典的所有键(即参数的名称)
  16. for key in layer.params.keys():
  17. #实现参数更新
  18. #通过计算当前参数的梯度layers.grads[key]与学习率的乘积,然后将差值从当前数值中减去,得到新参数值
  19. layer.params[key] = layer.params[key] - self.init_lr * layer.grads[key]

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


5 完善Runner类:RunnerV2_1

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

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

  1. #完善Runner类
  2. import os
  3. class RunnerV2_1(object):
  4. def __init__(self, model, optimizer, metric, loss_fn, **kwargs):
  5. self.model = model
  6. self.optimizer = optimizer
  7. self.loss_fn = loss_fn
  8. self.metric = metric
  9. # 记录训练过程中的评估指标变化情况
  10. self.train_scores = []
  11. self.dev_scores = []
  12. # 记录训练过程中的评价指标变化情况
  13. self.train_loss = []
  14. self.dev_loss = []
  15. def train(self, train_set, dev_set, **kwargs):
  16. # 传入训练轮数,如果没有传入值则默认为0
  17. num_epochs = kwargs.get("num_epochs", 0)
  18. # 传入log打印频率,如果没有传入值则默认为100
  19. log_epochs = kwargs.get("log_epochs", 100)
  20. # 传入模型保存路径
  21. save_dir = kwargs.get("save_dir", None)
  22. # 记录全局最优指标
  23. best_score = 0
  24. # 进行num_epochs轮训练
  25. for epoch in range(num_epochs):
  26. X, y = train_set
  27. # 获取模型预测
  28. logits = self.model(X)
  29. # 计算交叉熵损失
  30. trn_loss = self.loss_fn(logits, y) # return a tensor
  31. self.train_loss.append(trn_loss.item())
  32. # 计算评估指标
  33. trn_score = self.metric(logits, y).item()
  34. self.train_scores.append(trn_score)
  35. self.loss_fn.backward()
  36. # 参数更新
  37. self.optimizer.step()
  38. dev_score, dev_loss = self.evaluate(dev_set)
  39. # 如果当前指标为最优指标,保存该模型
  40. if dev_score > best_score:
  41. print(f"[Evaluate] best accuracy performence has been updated: {best_score:.5f} --> {dev_score:.5f}")
  42. best_score = dev_score
  43. if save_dir:
  44. self.save_model(save_dir)
  45. if log_epochs and epoch % log_epochs == 0:
  46. print(f"[Train] epoch: {epoch}/{num_epochs}, loss: {trn_loss.item()}")
  47. def evaluate(self, data_set):
  48. X, y = data_set
  49. # 计算模型输出
  50. logits = self.model(X)
  51. # 计算损失函数
  52. loss = self.loss_fn(logits, y).item()
  53. self.dev_loss.append(loss)
  54. # 计算评估指标
  55. score = self.metric(logits, y).item()
  56. self.dev_scores.append(score)
  57. return score, loss
  58. def predict(self, X):
  59. return self.model(X)
  60. def save_model(self, save_dir):
  61. # 对模型每层参数分别进行保存,保存文件名称与该层名称相同
  62. for layer in self.model.layers: # 遍历所有层
  63. if isinstance(layer.params, dict):
  64. torch.save(layer.params, os.path.join(save_dir, layer.name + ".pdparams"))
  65. def load_model(self, model_dir):
  66. # 获取所有层参数名称和保存路径之间的对应关系
  67. model_file_names = os.listdir(model_dir)
  68. name_file_dict = {}
  69. for file_name in model_file_names:
  70. name = file_name.replace(".pdparams", "")
  71. name_file_dict[name] = os.path.join(model_dir, file_name)
  72. # 加载每层参数
  73. for layer in self.model.layers: # 遍历所有层
  74. if isinstance(layer.params, dict):
  75. name = layer.name
  76. file_path = name_file_dict[name]
  77. layer.params = torch.load(file_path)

6 模型训练

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

  1. from metric import accuracy
  2. torch.random.manual_seed(123)
  3. epoch_num = 1000
  4. model_saved_dir = "model"
  5. # 输入层维度为2
  6. input_size = 2
  7. # 隐藏层维度为5
  8. hidden_size = 5
  9. # 输出层维度为1
  10. output_size = 1
  11. # 定义网络
  12. model = Model_MLP_L2(input_size=input_size, hidden_size=hidden_size, output_size=output_size)
  13. # 损失函数
  14. loss_fn = BinaryCrossEntropyLoss(model)
  15. # 优化器
  16. learning_rate = 0.2
  17. optimizer = BatchGD(learning_rate, model)
  18. # 评价方法
  19. metric = accuracy
  20. # 实例化RunnerV2_1类,并传入训练配置
  21. runner = RunnerV2_1(model, optimizer, metric, loss_fn)
  22. runner.train([X_train, y_train], [X_dev, y_dev], num_epochs=epoch_num, log_epochs=50, save_dir=model_saved_dir)

metric包中的accuary代码为:

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

训练结果:

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

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

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


7 性能评价

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

  1. # 加载训练好的模型
  2. runner.load_model(model_saved_dir)
  3. # 在测试集上对模型进行评价
  4. score, loss = runner.evaluate([X_test, y_test])
  5. 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博客

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/354966
推荐阅读
相关标签
  

闽ICP备14008679号