目录
前馈神经网络实现鸢尾花分类与Softmax回归完成鸢尾花分类做对比分析
基于前馈神经网络完成鸢尾花分类
实现代码:
import torch
import torch.utils.data as io
import torch.nn.functional as F
from nndl.dataset import load_data
from torch import nn
from torchmetrics import Metric
import torch.optim as opt
import matplotlib.pyplot as plt
class IrisDataset(io.Dataset):
def __init__(self, mode='train', num_train=120, num_dev=15):
super(IrisDataset, self).__init__()
X, y = load_data(shuffle=True)
if mode == 'train':
self.X, self.y = X[:num_train], y[:num_train]
elif mode == 'dev':
self.X, self.y = X[num_train:num_train + num_dev], y[num_train:num_train + num_dev]
else:
self.X, self.y = X[num_train + num_dev:], y[num_train + num_dev:]
def __getitem__(self, idx):
return self.X[idx], self.y[idx]
def __len__(self):
return len(self.y)
torch.manual_seed(12)
train_dataset=IrisDataset(mode='train')
dev_dataset=IrisDataset(mode='dev')
test_dataset=IrisDataset(mode='test')
print("length of train set:",len(train_dataset))
# 用DataLoader进行封装
# 批量大小
batch_size=16
# 加载数据
train_loader=io.DataLoader(train_dataset,batch_size=batch_size,shuffle=True)
dev_loader=io.DataLoader(dev_dataset,batch_size=batch_size)
test_loader=io.DataLoader(test_dataset,batch_size=batch_size)
# 模型构建
class Model_MLP_L2_V3(nn.Module):
def __init__(self,input_size,output_size,hidden_size):
super(Model_MLP_L2_V3,self).__init__()
# 构建第一个全连接层
self.fc1 = nn.Linear(input_size, hidden_size)
self.fc1.weight.data.normal_(mean=0.0, std=0.01)
self.fc1.bias.data.fill_(1.0)
# 构建第二全连接层
self.fc2 = nn.Linear(hidden_size, output_size)
self.fc2.weight.data.normal_(mean=0.0, std=0.01)
self.fc2.bias.data.fill_(1.0)
self.act=nn.Sigmoid()
def forward(self, inputs):
outputs = self.fc1(inputs)
outputs = self.act(outputs)
outputs = self.fc2(outputs)
return outputs
fnn_model=Model_MLP_L2_V3(input_size=4,output_size=3,hidden_size=6)
class Accuracy(Metric):
def __init__(self,is_logist=True):
"""
输入:
- is_logist: outputs是logist还是激活后的值
"""
# 用于统计正确样本数
self.num_corretc=0
# 用于统计样本个数
self.num_count=0
self.is_logist=is_logist
def update(self,outputs,labels):
"""
输入:
- outputs: 预测值, shape=[N,class_num]
- labels: 标签值, shape=[N,1]
"""
if outputs.shape[1]==1:
outputs=torch.squeeze(outputs,dim=-1)
if self.is_logist:
# logist判断是否大于0
preds = (outputs >= 0).float()
else:
# 如果不是logist,判断每个概率值是否大于0.5,当大于0.5时,类别为1,否则类别为0
preds = (outputs >= 0.5).float()
# 多分类时,使用torch.argmax计算最大元素索引引用类别
else:
preds=torch.argmax(outputs,dim=1)
# 获取本批数据中预测正确的样本个数
labels=torch.squeeze(labels,dim=-1)
batch_correct = torch.sum((preds == labels).float()).item()
batch_count=len(labels)
# 更新num_correct和num_count
self.num_correct+=batch_correct
self.num_count+=batch_count
def compute(self):
# 使用累计的数据,计算总的指标
if self.num_count==0:
return 0
return self.num_correct/self.num_count
def reset(self):
# 重置正确的数目和总数
self.num_correct=0
self.num_count=0
def name(self):
return "Accuracy"
class RunnerV3(object):
def __init__(self, model, optimizer, loss_fn, metric, **kwargs):
self.model = model
self.optimizer = optimizer
self.loss_fn = loss_fn
self.metric = metric # 只用于计算评价指标
# 记录训练过程中的评价指标变化情况
self.dev_scores = []
# 记录训练过程中的损失函数变化情况
self.train_epoch_losses = [] # 一个epoch记录一次loss
self.train_step_losses = [] # 一个step记录一次loss
self.dev_losses = []
# 记录全局最优指标
self.best_score = 0
def train(self, train_loader, dev_loader=None, **kwargs):
# 将模型切换为训练模式
self.model.train()
# 传入训练轮数,如果没有传入值则默认为0
num_epochs = kwargs.get("num_epochs", 0)
# 传入log打印频率,如果没有传入值则默认为100
log_steps = kwargs.get("log_steps", 100)
# 评价频率
eval_steps = kwargs.get("eval_steps", 0)
# 传入模型保存路径,如果没有传入值则默认为"best_model.pdparams"
save_path = kwargs.get("save_path", "best_model.pdparams")
custom_print_log = kwargs.get("custom_print_log", None)
# 训练总的步数
num_training_steps = num_epochs * len(train_loader)
if eval_steps:
if self.metric is None:
raise RuntimeError('Error: Metric can not be None!')
if dev_loader is None:
raise RuntimeError('Error: dev_loader can not be None!')
# 运行的step数目
global_step = 0
# 进行num_epochs轮训练
for epoch in range(num_epochs):
# 用于统计训练集的损失
total_loss = 0
for step, data in enumerate(train_loader):
X, y = data
# 获取模型预测
logits = self.model(X)
loss = self.loss_fn(logits, y) # 默认求mean
total_loss += loss
# 训练过程中,每个step的loss进行保存
self.train_step_losses.append((global_step, loss.item()))
if log_steps and global_step % log_steps == 0:
print(
f"[Train] epoch: {epoch}/{num_epochs}, step: {global_step}/{num_training_steps}, loss: {loss.item():.5f}")
# 梯度反向传播,计算每个参数的梯度值
loss.backward()
if custom_print_log:
custom_print_log(self)
# 小批量梯度下降进行参数更新
self.optimizer.step()
# 梯度归零
self.optimizer.zero_grad()
# 判断是否需要评价
if eval_steps > 0 and global_step > 0 and \
(global_step % eval_steps == 0 or global_step == (num_training_steps - 1)):
dev_score, dev_loss = self.evaluate(dev_loader, global_step=global_step)
print(f"[Evaluate] dev score: {dev_score:.5f}, dev loss: {dev_loss:.5f}")
# 将模型切换为训练模式
self.model.train()
# 如果当前指标为最优指标,保存该模型
if dev_score > self.best_score:
self.save_model(save_path)
print(
f"[Evaluate] best accuracy performence has been updated: {self.best_score:.5f} --> {dev_score:.5f}")
self.best_score = dev_score
global_step += 1
# 当前epoch 训练loss累计值
trn_loss = (total_loss / len(train_loader)).item()
# epoch粒度的训练loss保存
self.train_epoch_losses.append(trn_loss)
print("[Train] Training done!")
# 模型评估阶段,使用'paddle.no_grad()'控制不计算和存储梯度
@torch.no_grad()
def evaluate(self, dev_loader, **kwargs):
assert self.metric is not None
# 将模型设置为评估模式
self.model.eval()
global_step = kwargs.get("global_step", -1)
# 用于统计训练集的损失
total_loss = 0
# 重置评价
self.metric.reset()
# 遍历验证集每个批次
for batch_id, data in enumerate(dev_loader):
X, y = data
# 计算模型输出
logits = self.model(X)
# 计算损失函数
loss = self.loss_fn(logits, y).item()
# 累积损失
total_loss += loss
# 累积评价
self.metric.update(logits, y)
dev_loss = (total_loss / len(dev_loader))
dev_score = self.metric.compute()
# 记录验证集loss
if global_step != -1:
self.dev_losses.append((global_step, dev_loss))
self.dev_scores.append(dev_score)
return dev_score, dev_loss
# 模型评估阶段,使用'paddle.no_grad()'控制不计算和存储梯度
@torch.no_grad()
def predict(self, x, **kwargs):
# 将模型设置为评估模式
self.model.eval()
# 运行模型前向计算,得到预测值
logits = self.model(x)
return logits
def save_model(self, save_path):
torch.save(self.model.state_dict(), save_path)
def load_model(self, model_path):
model_state_dict = torch.load(model_path)
self.model.load_state_dict(model_state_dict)
# 模型训练
lr=0.2
# 定义网络
model=fnn_model
# 定义优化器
optimizer = opt.SGD(model.parameters(), lr=lr)
# 定义损失函数,softmax+交叉熵
loss_fn=F.cross_entropy
# 定义评价指标
metric = Accuracy(is_logist=True)
runner=RunnerV3(model,optimizer,loss_fn,metric)
# 启动训练
log_steps=100
eval_steps=50
runner.train(train_loader,dev_loader,num_epochs=500,log_steps=log_steps,eval_steps=eval_steps,save_path="best_model.pdparams")
# 可视化观察训练集损失和训练集loss变化情况。
# 绘制训练集和验证集的损失变化以及验证集上的准确率变化曲线
def plot_training_loss_acc(runner, fig_name,
fig_size=(16, 6),
sample_step=20,
loss_legend_loc="upper right",
acc_legend_loc="lower right",
train_color="#8E004D",
dev_color='#E20079',
fontsize='x-large',
train_linestyle="-",
dev_linestyle='--'):
plt.figure(figsize=fig_size)
plt.subplot(1, 2, 1)
train_items = runner.train_step_losses[::sample_step]
train_steps = [x[0] for x in train_items]
train_losses = [x[1] for x in train_items]
plt.plot(train_steps, train_losses, color=train_color, linestyle=train_linestyle, label="Train loss")
if len(runner.dev_losses) > 0:
dev_steps = [x[0] for x in runner.dev_losses]
dev_losses = [x[1] for x in runner.dev_losses]
plt.plot(dev_steps, dev_losses, color=dev_color, linestyle=dev_linestyle, label="Dev loss")
# 绘制坐标轴和图例
plt.ylabel("loss", fontsize=fontsize)
plt.xlabel("step", fontsize=fontsize)
plt.legend(loc=loss_legend_loc, fontsize=fontsize)
# 绘制评价准确率变化曲线
if len(runner.dev_scores) > 0:
plt.subplot(1, 2, 2)
plt.plot(dev_steps, runner.dev_scores,
color=dev_color, linestyle=dev_linestyle, label="Dev accuracy")
# 绘制坐标轴和图例
plt.ylabel("score", fontsize=fontsize)
plt.xlabel("step", fontsize=fontsize)
plt.legend(loc=acc_legend_loc, fontsize=fontsize)
plt.savefig(fig_name)
plt.show()
plot_training_loss_acc(runner, 'fw-loss.pdf')
# 加载最优模型
runner.load_model('best_model.pdparams')
# 模型评价
score, loss = runner.evaluate(test_loader)
print("[Test] accuracy/loss: {:.4f}/{:.4f}".format(score, loss))
# 获取测试集中第一条数据
X, label = next(iter(test_loader))
logits = runner.predict(X)
pred_class = torch.argmax(logits[0]).numpy()
label = label[0].numpy()
# 输出真实类别与预测类别
print("The true category is {} and the predicted category is {}".format(label, pred_class))
实验结果:
不同训练轮数实验结果 :
- 训练100轮:
- 训练150轮:
- 训练200轮:
- 训练400轮:
- 训练600轮:
分析:由训练数据,随着训练的轮数的增加,在验证集上,训练的准确率是呈逐渐升高的变化规律,直至稳定在0.933....左右,损失是呈逐渐降低的趋势变化的。在测试集上,准确率和损失是不变的,维持在1和1.0183,应该是对于测试集,100轮训练已经足够对它进行预测了,我试了下40轮训练发现,它的准确率很高并且损失也很小,应该是对于测试集过拟合了。
修改偏置b后的实验结果:
- 随机初始化:
- 初始化为1:
- 初始化为0.5:
- 初始化为0:
根据实验结果来看,在训练集上,随机初始化的结果是最好的,相同条件下能够达到损失最小,但是对于测试集初始化为0损失是最低的,但是根据之前的了解,这个应该只是对于这个数据集好一点,一般情况下如果所有的偏置都初始化为零,可能会导致网络的对称性问题,使得每一层的输出相同。因此,零初始化在实践中很少使用,一般还是使用随机初始化相对好一点,也可以增加多样性。
不同学习率实验结果:
- 学习率为0.1:
- 学习率为0.2:
- 学习率为0.5:
- 学习率为1.0:
- 学习率为3.0:
分析:相同的训练轮数,不同的学习率,对应的验证集损失和准确率都是逐渐优化的,但是在测试集上,损失先降低后升高,说明对于我们的模型来说,学习率的最优值是存在于这个区间的,越接近于最优值,训练收敛情况越好,当学习率太大时,会导致如下图所示的情况,导致网络无法收敛,产生梯度爆炸。学习率的设置还是根据经验设置,经过手动不断的测试,观察拟合情况,达到对于学习率的最优值。
总结实验:
重点:小批量梯度下降法进行训练。
梯度下降法优化模型主要是将整个训练集上的风险函数(即损失函数)降到最低,这样是批量梯度下降法,每次迭代都计算数据集中所有样本的损失函数的梯度求和,当数据集样本容量很大时,将会导致训练的计算复杂度高,每次迭代开销也很大。
小批量梯度下降法就是为了解决这个问题:在每次迭代时都只随机采集含有K个样本(K:批量大小),只计算这一部分样本的损失并更新参数。这种方法收敛快,计算开销小。
实验中新学习的库或方法:
- torch.utils.data.DataLoader:它的功能有对数据的自动批处理、并行加载和数据打乱等功能。接受一个数据集对象作为输入,并根据指定的参数将数据集划分成多个小批次进行加载。在该实验中就是使用它加载minibatch的数据,进行批处理。DataLoader的主要功能包括:
- 自动批处理:根据指定的batch_size参数,将数据集划分成小批次进行加载。
- 并行加载:可以使用多个子进程并行加载数据,加快数据加载速度。
- 数据打乱:可以在每个epoch开始前打乱数据顺序,增加数据的随机性和泛化能力。
- torchmetrics :是一个用于评估模型性能的PyTorch库,其中的Metric类提供了各种常见的评估指标的计算和跟踪功能。它包含了许多常见的分类、回归和聚类任务的评估指标,如准确率、精确率、召回率、F1分数、均方根误差等。Metric类的功能包括:
- 计算指标:Metric类提供了计算各种评估指标的方法,可以根据预测值和真实值计算相应的指标。
- 跟踪指标:Metric类可以在每个批次或每个epoch结束时更新和跟踪指标的值,以便在训练或验证过程中进行监控和记录。
定义一个自定义的Metric类时,继承自Metric类,需要实现相应的计算逻辑和更新方法。
- 计算逻辑:
class CustomMetric(Metric): def __init__(self): super().__init__() # 初始化指标的相关变量 def update(self, preds, target): # 根据预测值和真实值更新指标的值 def compute(self):#(需要注意:在paddle的模型中,这里是accumulate) # 计算并返回指标的最终值
-
更新方法:
metric = CustomMetric() metric.update(preds, target)
- 使用compute方法计算并获取指标的最终值。
score = metric.compute()
- torch.optim:PyTorch中用于优化模型参数的库。它提供了多种优化算法的实现,如随机梯度下降(SGD)、Adam、RMSprop等。torch.optim库的功能:
1.定义优化器:torch.optim库提供了各种优化算法的类,可以通过实例化这些类来创建相应的优化器对象。常用的优化器类包括SGD、Adam、RMSprop等。
SGD:随机梯度下降。更新公式:
parameter = parameter - learning_rate* gradient
Adam:自适应学习率的优化算法
m = beta1 * m + (1 - beta1) * gradient
v = beta2 * v + (1 - beta2) * gradient^2
parameter = parameter - learning_rate * m / (sqrt(v) + epsilon)
其中,m和v分别是梯度的一阶矩估计(mean)和二阶矩估计(uncentered variance),beta1和beta2是衰减率,epsilon是一个很小的常数,用于避免除以零。
RMSprop: 自适应学习率的优化算法。它通过平均梯度的平方根来调整学习率。更新公式:
v = decay_rate * v + (1 - decay_rate) * gradient^2
parameter = parameter - learning_rate * gradient / (sqrt(v) + epsilon)
其中,v是梯度的平方根的移动平均,decay_rate是衰减率,epsilon是一个很小的常数,用于避免除以零。
2.更新模型参数:优化器对象可以通过调用step()方法来更新模型的参数。在每个训练迭代中,通常会先计算损失函数的梯度,然后调用优化器的step()方法来更新模型的参数。
3.调整学习率:优化器对象还提供了调整学习率的功能。可以通过设置学习率参数或使用学习率调度器来动态调整学习率。(如torch.optim.lr_scheduler模块中的类)
调用其中的方法:
1.创建模型对象和优化器对象:
model = ...
optimizer = optim.SGD(model.parameters(), lr=0.01)
2.在训练循环中,计算损失函数的梯度并更新模型参数:
optimizer.zero_grad() # 清零梯度(注:paddle这里是optimizer.clear_grad())
loss = ...
loss.backward() # 计算梯度
optimizer.step() # 更新参数
3.调整学习率:
scheduler=optim.lr_scheduler.StepLR(optimizer,step_size=1,gamma=0.1)
scheduler.step()
使用学习率调度器类StepLR来设置学习率的调整策略。
详细了解torch.argmax
- torch.argmax(input, dim=None, keepdim=False):其中input是输入张量,dim是指定维度,如果不指定则返回整个张量中最大值的索引,keepdim表示是否保留维度。在多分类任务中,可以使用torch.argmax来判断模型对于输入样本的分类结果。
判断步骤:
- 通过模型对输入样本进行前向传播,得到输出张量。输出张量的形状通常(batch_size, num_classes),其中batch_size表示输入样本的数量,num_classes表示分类的类别数量。
- 使用torch.argmax函数在指定维度上找到输出张量中每个样本的最大值的索引。通常,dim参数会设置为1,表示在第二个维度上寻找最大值的索引。这样,得到的结果是一个大小为(batch_size,)的张量,其中每个元素是对应样本的预测类别的索引。
- 根据预测类别的索引,可以将其与真实标签进行比较,从而计算模型在该批次样本上的准确率或其他评估指标。
基于Softmax回归完成鸢尾花分类
实现代码:
from sklearn.datasets import load_iris
import pandas
import numpy as np
iris_features = np.array(load_iris().data, dtype=np.float32)
iris_labels = np.array(load_iris().target, dtype=np.int32)
print(pandas.isna(iris_features).sum())
print(pandas.isna(iris_labels).sum())
import matplotlib.pyplot as plt #可视化工具
# # 箱线图查看异常值分布
# def boxplot(features):
# feature_names = ['sepal_length', 'sepal_width', 'petal_length', 'petal_width']
#
# # 连续画几个图片
# plt.figure(figsize=(5, 5), dpi=200)
# # 子图调整
# plt.subplots_adjust(wspace=0.6)
# # 每个特征画一个箱线图
# for i in range(4):
# plt.subplot(2, 2, i+1)
# # 画箱线图
# plt.boxplot(features[:, i],
# showmeans=True,
# whiskerprops={"color":"#E20079", "linewidth":0.4, 'linestyle':"--"},
# flierprops={"markersize":0.4},
# meanprops={"markersize":1})
# # 图名
# plt.title(feature_names[i], fontdict={"size":5}, pad=2)
# # y方向刻度
# plt.yticks(fontsize=4, rotation=90)
# plt.tick_params(pad=0.5)
# # x方向刻度
# plt.xticks([])
# plt.savefig('ml-vis.pdf')
# # plt.show()
# boxplot(iris_features)
import copy
import torch
# 加载数据集
def load_data(shuffle=True):
"""
加载鸢尾花数据
输入:
- shuffle:是否打乱数据,数据类型为bool
输出:
- X:特征数据,shape=[150,4]
- y:标签数据, shape=[150]
"""
# 加载原始数据
X = np.array(load_iris().data, dtype=np.float32)
y = np.array(load_iris().target, dtype=np.int32)
X = torch.tensor(X)
y = torch.tensor(y)
# 数据归一化
X_min = torch.min(X, 0)[0]
X_max = torch.max(X, 0)[0]
X = (X - X_min) / (X_max - X_min)
# 如果shuffle为True,随机打乱数据
if shuffle:
idx = torch.randperm(X.shape[0])
X = X[idx]
y = y[idx]
return X, y
# 固定随机种子
torch.manual_seed(102)
num_train = 120
num_dev = 15
num_test = 15
X, y = load_data(shuffle=True)
print("X shape: ", X.shape, "y shape: ", y.shape)
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:]
# 打印X_train和y_train的维度
print("X_train shape: ", X_train.shape, "y_train shape: ", y_train.shape)
# 打印前5个数据的标签
print(y_train[:5])
from nndl import op
# 输入维度
input_dim = 4
# 类别数
output_dim = 3
# 实例化模型
model = op.model_SR(input_dim=input_dim, output_dim=output_dim)
from nndl import op, metric, opitimizer, RunnerV2
# 学习率
lr = 0.2
# 梯度下降法
optimizer = opitimizer.SimpleBatchGD(init_lr=lr, model=model)
# 交叉熵损失
loss_fn = op.MultiCrossEntropyLoss()
# 准确率
metric = metric.accuracy
# 实例化RunnerV2
runner = RunnerV2(model, optimizer, metric, loss_fn)
# 启动训练
runner.train([X_train, y_train], [X_dev, y_dev], num_epochs=500, log_epochs=10, save_path="best_model.pdparams")
from nndl import plot
plot(runner,fig_name='linear-acc3.pdf')
# 加载最优模型
runner.load_model('best_model.pdparams')
# 模型评价
score, loss = runner.evaluate([X_test, y_test])
print("[Test] score/loss: {:.4f}/{:.4f}".format(score, loss))
logits = runner.predict(torch.tensor(X_test))
# 观察其中一条样本的预测结果
pred = torch.argmax(logits[0]).item()
# 获取该样本概率最大的类别
label = y_test[0].item()
# 输出真实类别与预测类别
print("The true category is {} and the predicted category is {}".format(label, pred))
实验结果:
不同训练轮数实验结果:
- 训练100轮:
- 训练150轮:
- 训练200轮:
- 训练600轮:
- 训练1000轮:
分析:由训练数据,随着训练的轮数的增加,在验证集上,训练的准确率是呈逐渐升高的变化规律,直至稳定在0.7333....左右,损失是呈逐渐降低的趋势变化的,但是收敛的很慢。在测试集上,也是损失逐渐下降,准确率逐渐升高,差不多训练2000轮才勉强和前馈神经训练100轮差不多,但是没有过拟合的情况,即使训练10000轮也没有过拟合。
前馈神经网络实现鸢尾花分类与Softmax回归完成鸢尾花分类做对比分析
通过这些训练数据比对可以看出:在验证集中,相同训练轮数下对鸢尾花数据集的分类中,基于前馈神经网络要明显比前面基于Softmax回归收敛的快,分类精确度高,损失也少。在测试集中,前馈神经网络的准确率能够达到1,但是损失却也高于Softmax回归。应该是由于前馈神经网络的分类拟合能力太强,导致过拟合,泛化性能降低,损失反而升高。
随着训练轮数的增加,两种方法在验证集上的准确率都有增加,损失都有或多或少的减少,但是明显前馈神经网络要收敛的快,准确。但是因为它收敛的太快了,当训练轮数很大时,它的损失反而会升高,产生过拟合,泛化性能降低。
总而言之,前馈神经网络的多分类训练结果要明显优于基于Softmax回归的训练结果,收敛速度快,准确率高。但是缺点是对于较小,较简单数据集,由于它的训练能力太强,尤其要注意不能训练太多而导致过拟合,泛化性能降低。从实现上说,前馈神经网络也具有更复杂的模型结构和更多的参数,而Softmax回归相对简单,训练时间和计算复杂度较低。