目录
前言:
本博客内容用来学习深度学习,自用为主,主要借鉴、复现两位学长和老师的宝贵思路和代码实现,用于加深自己的理解。感谢学长们的博客:
NNDL 实验五 前馈神经网络(3)鸢尾花分类-CSDN博客
【精选】HBU-NNDL 实验五 前馈神经网络(3)鸢尾花分类_鸢尾花分类 隐藏层取多大_不是蒋承翰的博客-CSDN博客
鸢尾花数据集
Iris 鸢尾花数据集内包含 3 类:
山鸢尾(Iris-setosa)、变色鸢尾(Iris-versicolor)、维吉尼亚鸢尾(Iris-virginica)。
共 150 条记录,每类各 50 个数据,每条记录都有 4 项特征:花萼长度、花萼宽度、花瓣长度、花瓣宽度。
sepallength:萼片长度
sepalwidth:萼片宽度
petallength:花瓣长度
petalwidth:花瓣宽度
四个特征的单位都是厘米(cm)
画出数据集中150个数据的前两个特征的散点分布图:
#鸢尾花数据集
import pandas as pd #Pandas是流行数据处理库,用于数据处理和分析
import matplotlib.pyplot as plt
# 导入数据集 usecols参数指定要导入的列,这里表示导入1-5列
df = pd.read_csv('iris.csv', usecols=[1, 2, 3, 4, 5])
#绘制训练集基本散点图,观察数据集的线性可分性
#表示绘制图形的画板尺寸为10*8
plt.figure(figsize=(10, 8))
#绘制了三种不同类型鸢尾花的散点图。分别设置了x坐标、y坐标、标签
plt.scatter(df[:50]['SepalLengthCm'], df[:50]['SepalWidthCm'], label='Iris-setosa')
plt.scatter(df[50:100]['SepalLengthCm'], df[50:100]['SepalWidthCm'], label='Iris-versicolor')
plt.scatter(df[100:150]['SepalLengthCm'], df[100:150]['SepalWidthCm'], label='Iris-virginica')
plt.xlabel('SepalLength(Cm)')#萼片长度
plt.ylabel('SepalWidth(Cm)')#萼片宽度
#标题 '鸢尾花萼片的长度与宽度的散点分布'
plt.title('Scattered distribution of length and width of iris sepals.')
plt.legend() #显示标签
plt.show()
结果:
三种颜色的散点分布情况如图所示,可以看到山鸢尾(蓝色点)聚集在一个区域,更容易被分类出来;而变色鸢尾和维吉尼亚鸢尾花(橙色点和绿色点)相容性较高,不容易区分开。
基于前馈神经网络完成鸢尾花分类
小批量梯度下降法:
在梯度下降法中,目标函数是整个训练集上的风险函数,这种方式称为批量梯度下降法(Batch Gradient Descent,BGD)。 批量梯度下降法在每次迭代时需要计算每个样本上损失函数的梯度并求和。当训练集中的样本数量N很大时,空间复杂度比较高,每次迭代的计算开销也很大。
为减少每次迭代的计算复杂度,我们可以在每次迭代时只采集一小部分样本,计算在这组样本上损失函数的梯度并更新参数,这种优化方式称为:小批量梯度下降法(Mini-Batch Gradient Descent,Mini-Batch GD)。
为了小批量梯度下降法,我们需要对数据进行随机分组。目前,机器学习中通常做法是构建一个数据迭代器,每个迭代过程中从全部数据集中获取一批指定数量的数据。
拓展:
梯度下降法有着三种不同的形式,分别是批量梯度下降、随机梯度下降和小批量梯度下降。
批量梯度下降:批量梯度下降法是最原始的形式。每一次迭代更新权值时,都使用所有样本来计算偏导数。采用这种方法由所有样本确定梯度方向,可以保证每一步都是准确地向着极值点的方向趋近,收敛的速度最快,所需要的迭代次数最少。当目标函数是凸函数时,一定能够收敛于全局最小值,如果目标函数是非凸函数,则会收敛到某个局部极小值点。(对所有样本的计算,可以利用向量运算进行并行计算来提升运算速度)
对于小规模数据集,通常采用这种批量梯度下降法进行训练,在前面的实验使用的都是这种方法,但是在神经网络和深度学习中,样本的数量往往非常大,每个样本中,属性的个数也可能非常的大,采用批量梯度下降法,在每一步迭代时,都需要用到所有的样本,计算量会非常大,即使使用向量运算,也需要花费大量的时间。
并且在大规模数据集中,通常会有大量冗余数据,也没有必要使用整个训练集来计算梯度,因此,批量梯度下降法并不适合大规模数据集。为了实现更快的计算,可以使用随机梯度下降法。
随机梯度下降法:在这种方法中,每次迭代时只使用一个样本来训练模型,也就是说每次只使用一个样本去计算代价函数的梯度并迭代更新模型的参数,使模型的输出值尽可能逼近这个样本真实的标签值。
当训练误差足够小时,结束本次训练,再输入下一个新的样本,显然使用前面样本训练出的网络参数,不一定能够使得后面的新样本误差最小,所以这个新样本需要再重新训练网络,这个样本训练结束之后,再输入下一个样本,再次训练网络,直到使用所有样本训练一遍为止,这个过程也被称为一轮。
随机梯度下降法虽然每次训练只使用一个样本,单次迭代的速度很快,但是通过单个样本计算出的梯度不能够很好的体现全体样本的梯度。各个样本各自为政,横冲直撞,不同样本的训练结果,往往会互相抵消,导致参数更新非常的频繁,因此,可能会走很多的弯路,在最优点附近晃来晃去,却无法快速收敛,即使损失函数是凸函数,也无法做到线性收敛,而且采用这种方法,每次只使用一个样本,也不利于实现并行计算。
实际上这种方法很少使用,现在我们所说的随机梯度下降通常是指小批量梯度下降算法。
小批量梯度下降法:小批量梯度下降算法是前面两种的折中方案,也称为小批量随机梯度下降算法。这种算法把梯度称为若干个小批量,也叫做小批量。也就是每次迭代只使用其中一个小批量来训练模型。
在小批量梯度下降法中,每个批中的所有样本共同决定了本次迭代中梯度的方向,这样训练起来就不会跑偏,也就减少了随机性。
将所有的批次都执行一遍,就称之为一轮。因为各个批的样本之间也会存在训练结果互相抵消的问题,因此通常也需要经过多轮训练才能够收敛。
使用这种方法的好处是,无论整个训练集的样本数量有多少,每次迭代所使用的训练样本数量都是固定的。和批量梯度下降法相比,这样显然可以大大的加快训练速度,另外,和批量梯度下降法一样,这种方法也可以实现并行计算。因此。在训练大规模数据集时,通常首选小批量梯度下降算法。
数据分组
为了小批量梯度下降法,我们需要对数据进行随机分组。目前,机器学习中通常做法是构建一个数据迭代器,每个迭代过程中从全部数据集中获取一批指定数量的数据。
数据迭代器的实现原理如下图所示:
- 首先,将数据集封装为Dataset类,传入一组索引值,根据索引从数据集合中获取数据;
- 其次,构建DataLoader类,需要指定数据批量的大小和是否需要对数据进行乱序,通过该类即可批量获取数据。
数据处理
构造IrisDataset类进行数据读取,继承自torch.utils.data.Dataset
类。torch.utils.data.Dataset
是用来封装 Dataset的方法和行为的抽象类,通过一个索引获取指定的样本,同时对该样本进行数据处理。
import numpy as np
import torch
import torch.utils.data as io #用于创建数据加载器
from nndl.dataset import load_data #自定义库,用于加载数据集
#该类继承torch.utils.data.Dataser。此类用于获取和处理鸢尾花数据集
class IrisDataset(io.Dataset):
#初始化函数接受三个参数
#mode 用于区分数据集类型,训练集train,验证集dev
#初始化训练集的大小为120,验证集大小为15
def __init__(self, mode='train', num_train=120, num_dev=15):
#super 这行代码调用父类的初始化函数。在子类中重用父类的初始化代码
super(IrisDataset, self).__init__()
# 调用第三章中的数据读取函数,其中不需要将标签转成one-hot类型
#调用名为load_data的函数,此函数负责加载鸢尾花数据集
#shuffle=True表示在加载数据时会被随机打乱,确保每个epoch的顺序是随机的。
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:]
#从数据集中获取一个样本,返回一个样本的输入X和标签y
def __getitem__(self, idx):
return self.X[idx], self.y[idx]
#返回数据集标签的数量(即数据集的大小)
def __len__(self):
return len(self.y)
#设置随机种子以保证结果的可重复性
torch.random.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进行封装
#定义了一个批量大小变量,值为16.批量大小指一次训练中传递给模型的样本数量
batch_size = 16
# 加载数据
#训练迭代时对训练数据进行随机排序。 设置shuffle=True
train_loader = io.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
#后两行未设置shuffle=True,因此验证集和测试集的数据保持原始顺序
dev_loader = io.DataLoader(dev_dataset, batch_size=batch_size)
test_loader = io.DataLoader(test_dataset, batch_size=batch_size)
模型构建
构建一个简单的前馈神经网络进行鸢尾花分类实验。
其中输入层神经元个数为4,输出层神经元个数为3,隐含层神经元个数为6。代码实现如下:
#模型构建
from torch import nn # 引入torch.nn as nn
# 实现一个两层前馈神经网络
class Model_MLP_L2_V3(torch.nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(Model_MLP_L2_V3, self).__init__()
self.fc1 = torch.nn.Linear(input_size, hidden_size)
w_ = torch.normal(0, 0.01, size=(hidden_size, input_size), requires_grad=True)
self.fc1.weight = nn.Parameter(w_)
self.fc1.bias = torch.nn.init.constant_(self.fc1.bias, val=1.0)
self.fc2 = torch.nn.Linear(hidden_size, output_size )
w2 = torch.normal(0, 0.01, size=(output_size, hidden_size), requires_grad=True)
self.fc2.weight = torch.nn.Parameter(w2)
self.fc2.bias = torch.nn.init.constant_(self.fc2.bias, val=1.0)
self.act = torch.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, hidden_size=6,output_size=3)
完善Runner类
复习--Runner类的介绍:
机器学习方法流程包括数据集构建、模型构建、损失函数定义、优化器、模型训练、模型评价、模型预测等环节。
为了更方便地将上述环节规范化,我们将机器学习模型的基本要素封装成一个Runner类。
除上述提到的要素外,再加上模型保存、模型加载等功能。
Runner类的成员函数定义如下:
__init__函数:实例化Runner类,需要传入模型、损失函数、优化器和评价指标等;
train函数:模型训练,指定模型训练需要的训练集和验证集;
evaluate函数:通过对训练好的模型进行评价,在验证集或测试集上查看模型训练效果;
predict函数:选取一条数据对训练好的模型进行预测;
save_model函数:模型在训练过程和训练结束后需要进行保存;
load_model函数:调用加载之前保存的模型。
完善Runner类,RunnerV3代码如下:
#完善Runner类
import torch.nn.functional as F
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!")
# 模型评估阶段,使用'torch.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.accumulate()
# 记录验证集loss
if global_step != -1:
self.dev_losses.append((global_step, dev_loss))
self.dev_scores.append(dev_score)
return dev_score, dev_loss
# 模型评估阶段,使用'torch.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)
其中,Accuracy的代码如下:
#Accuracy代码
class Accuracy(object):
def __init__(self, is_logist=True):
# 用于统计正确的样本个数
self.num_correct = 0
# 用于统计样本的总数
self.num_count = 0
self.is_logist = is_logist
def update(self, outputs, labels):
# 判断是二分类任务还是多分类任务,shape[1]=1时为二分类任务,shape[1]>1时为多分类任务
if outputs.shape[1] == 1: # 二分类
outputs = torch.squeeze(outputs, axis=-1)
if self.is_logist:
# logist判断是否大于0
preds = (outputs>=0).to(torch.float32)
else:
# 如果不是logist,判断每个概率值是否大于0.5,当大于0.5时,类别为1,否则类别为0
preds = (outputs>=0.5).to(torch.float32)
else:
# 多分类时,使用'torch.argmax'计算最大元素索引作为类别
preds = torch.argmax(outputs, dim=1).int()
# 获取本批数据中预测正确的样本个数
labels = torch.squeeze(labels, axis=-1)
#batch_correct = torch.sum(torch.tensor(preds==labels, dtype=torch.float32)).numpy()
batch_correct = torch.sum((preds == labels).clone().detach()).numpy()
batch_count = len(labels)
# 更新num_correct 和 num_count
self.num_correct += batch_correct
self.num_count += batch_count
def accumulate(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"
模型训练
使用训练集和验证集进行模型训练,共训练150个epoch。在实验中,保存准确率最高的模型作为最佳模型。代码实现如下:
#模型训练
import torch.optim as opt #导入优化库
lr = 0.2 #定义学习率
# 定义网络
model = fnn_model #模型fnn_model已在上面代码定义
# 定义优化器SGD 随机梯度下降优化器,将模型参数传给优化器
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)
# 启动训练 训练轮数为150轮,每隔100步记录一次,每隔50步进行一次评估
log_steps = 100
eval_steps = 50
runner.train(train_loader, dev_loader,
num_epochs=150, log_steps=log_steps, eval_steps = eval_steps,
save_path="best_model.pdparams")
训练结果为:
可视化:
#可视化
import matplotlib.pyplot as plt
# 绘制训练集和验证集的损失变化以及验证集上的准确率变化曲线
def plot_training_loss_acc(runner, fig_name,
fig_size=(16, 6),
sample_step=20, #取数据的步长,每隔20取一个
#设置图例的位置
loss_legend_loc="upper right",
acc_legend_loc="lower right",
#设置曲线的颜色
train_color="b",
dev_color='r',
fontsize='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='x-large')#图例
# 绘制评价准确率变化曲线
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='x-large')
plt.savefig(fig_name)#保存图像
plt.show()
plot_training_loss_acc(runner, 'fw-loss.pdf') #调用绘制图形的函数
分析结果,左图是训练集与验证集上损失函数值随着训练次数增加的变化情况,在训练集上,损失值下降稍有波动,但是总体趋势下降;在验证集上,损失值的变化波动较小,总体趋势也是在下降。
右图则显示了验证集上随着训练次数增加,评价指标(准确率)的变化情况。可见准确率的总体趋势是随着训练次数的增加而增加。中间过程会经历波折,但是最终准确率的值可以达到平稳,收敛到了一个值。
模型评价
#模型评价--加载最优模型,从runner类中调取
runner.load_model('best_model.pdparams')
# 进行评价,评价指标有score准确率 和 loss损失函数值
score, loss = runner.evaluate(test_loader) #调取runner类中的评价函数
print("[Test] accuracy/loss: {:.4f}/{:.4f}".format(score, loss))
运行结果:
结果分析:(插个眼) 不太理解为什么最优模型训练出来的loss值是1.0183,这个值未免太大了,而且和上面运行出的可视化曲线对应不上啊? 下节课问问老师
模型预测
代码:
#模型预测
#获取测试集中第一条数据
X, label = train_dataset[0] #x是特征 label是对应的标签
logits = runner.predict(X) #将预测结果存储在logits中
#找到logits中概率最大的类别索引,并将其转换为numpy数组并存储在pred_class中
pred_class = torch.argmax(logits[0]).numpy()
label = label.numpy() #将标签转换为numpy数组 存储在label中
# 输出真实类别与预测类别
print("The true category is {} and the predicted category is {}".format(label, pred_class))
结果:
(插个眼) 我的真实类别为1 预测类别却为0。 是模型出问题了还是数据集有问题呢?之前的训练结果和数据基本没问题,为什么真实类别和预测类别还是不同呢?
对比Softmax分类和前馈神经网络分类
-
模型结构:
- )Softmax分类:Softmax是一种概率模型,它将每个类别的概率计算为一个指数函数。它通常在输出层上使用,以输出每个类别的概率。Softmax函数将神经网络的输出转换为一个概率分布,使得每个输出单元的输出值在0和1之间,并且所有输出单元的输出值之和为1。
- )前馈神经网络(Feedforward Neural Network,FNN):FNN是一种多层感知器,它包含一个输入层、一个或多个隐藏层和一个输出层。在训练过程中,FNN通过反向传播算法调整权重,以最小化损失函数。FNN可以处理多分类问题,也能够适应不同的特征数量。
-
训练算法:
- )Softmax分类:Softmax分类使用随机梯度下降(SGD)或批量梯度下降(Batch Gradient Descent,BGD)等优化算法来调整权重,以最小化损失函数。Softmax分类的训练过程简单且易于实现。
- )前馈神经网络:FNN使用反向传播算法进行训练。在每个训练批次中,FNN首先向前传播输入数据,计算输出和预期结果的误差,然后反向传播误差,更新权重。这个过程需要更多的计算资源和时间。
-
适用场景:
- )Softmax分类:适用于小规模数据集和线性可分的问题。Softmax分类的优点在于其简单性和易于理解性。Softmax模型在训练时不需要使用反向传播算法,这使得它在某些计算资源有限的环境中更为适用。
- )前馈神经网络:适用于大规模高维数据集和复杂的非线性问题。FNN可以处理多分类问题,也能够适应不同的特征数量。对于类别之间存在复杂的非线性关系的问题,FNN通过复杂的网络结构和非线性激活函数能够更好地捕捉这些关系。
-
性能:
- )Softmax分类:在处理小规模、线性可分的问题时,Softmax分类具有较好的性能。其优点在于训练过程简单、易于实现和计算效率高。当处理大规模、复杂的非线性问题时,Softmax分类的性能会下降。
- )前馈神经网络:在处理大规模高维数据集和复杂的非线性问题时,前馈神经网络具有较好的性能。通过使用多层感知器和反向传播算法,FNN可以学习复杂的非线性关系并提高分类精度。然而FNN的训练过程相对复杂,需要更多的计算资源和时间。
以下是代码生成的可视化部分,更加直观:
这些代码是我借鉴了学长的博客和网上搜索过来的代码,自己在pycharm上运行了,然后仔细分析了得到的结果。
现在来看一下softmax函数下的进阶弯月数据集分类可视化结果图:
红蓝绿三色的散点分别代表了三种不同类别的点。而背景色蓝色、红色、黄色则代表了分类出来的区域。由于softmax分类适用于解决线性问题,可见在此数据集下的分类效果很差,分类的效果是三个线性集合分出的区域,有很多散点没有被分隔开,表达性差,准确率低。
使用FNN前馈神经网络做分类的效果:
FNN是一个非线性分类模型,可以处理复杂数据集模型,相对于softmax分类具有更强的表达能力,可视化图中的三类散点基本上都被非线性的分割器分离成三个部分。由此可见,神经网络在非线性数据分类和复杂模型分类问题中的效果比softmax分类要良好许多。
本节实验全部代码(自存):
#鸢尾花数据集 原版
import pandas as pd #Pandas是流行数据处理库,用于数据处理和分析
import matplotlib.pyplot as plt
# 导入数据集 usecols参数指定要导入的列,这里表示导入1-5列
df = pd.read_csv('iris.csv', usecols=[1, 2, 3, 4, 5])
#绘制训练集基本散点图,观察数据集的线性可分性
#表示绘制图形的画板尺寸为10*8
plt.figure(figsize=(10, 8))
#绘制了三种不同类型鸢尾花的散点图。分别设置了x坐标、y坐标、标签
plt.scatter(df[:50]['SepalLengthCm'], df[:50]['SepalWidthCm'], label='Iris-setosa')
plt.scatter(df[50:100]['SepalLengthCm'], df[50:100]['SepalWidthCm'], label='Iris-versicolor')
plt.scatter(df[100:150]['SepalLengthCm'], df[100:150]['SepalWidthCm'], label='Iris-virginica')
plt.xlabel('SepalLength(Cm)')#萼片长度
plt.ylabel('SepalWidth(Cm)')#萼片宽度
#标题 '鸢尾花萼片的长度与宽度的散点分布'
plt.title('Scattered distribution of length and width of iris sepals.')
plt.legend() #显示标签
plt.show()
import numpy as np
import torch
import torch.utils.data as io #用于创建数据加载器
from nndl.dataset import load_data #自定义库,用于加载数据集
#该类继承torch.utils.data.Dataser。此类用于获取和处理鸢尾花数据集
class IrisDataset(io.Dataset):
#初始化函数接受三个参数
#mode 用于区分数据集类型,训练集train,验证集dev
#初始化训练集的大小为120,验证集大小为15
def __init__(self, mode='train', num_train=120, num_dev=15):
#super 这行代码调用父类的初始化函数。在子类中重用父类的初始化代码
super(IrisDataset, self).__init__()
# 调用第三章中的数据读取函数,其中不需要将标签转成one-hot类型
#调用名为load_data的函数,此函数负责加载鸢尾花数据集
#shuffle=True表示在加载数据时会被随机打乱,确保每个epoch的顺序是随机的。
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:]
#从数据集中获取一个样本,返回一个样本的输入X和标签y
def __getitem__(self, idx):
return self.X[idx], self.y[idx]
#返回数据集标签的数量(即数据集的大小)
def __len__(self):
return len(self.y)
#设置随机种子以保证结果的可重复性
torch.random.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))
#封装
#定义了一个批量大小变量,值为16.批量大小指一次训练中传递给模型的样本数量
batch_size = 16
# 加载数据
#训练迭代时对训练数据进行随机排序。 设置shuffle=True
train_loader = io.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
#后两行未设置shuffle=True,因此验证集和测试集的数据保持原始顺序
dev_loader = io.DataLoader(dev_dataset, batch_size=batch_size)
test_loader = io.DataLoader(test_dataset, batch_size=batch_size)
#模型构建
from torch import nn # 引入torch.nn as nn
# 实现一个两层前馈神经网络
class Model_MLP_L2_V3(torch.nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(Model_MLP_L2_V3, self).__init__()
self.fc1 = torch.nn.Linear(input_size, hidden_size)
#创建了正态分布,均值为0,方差为0.01 requires=True表示
w_ = torch.normal(0, 0.01, size=(hidden_size, input_size), requires_grad=True)
self.fc1.weight = nn.Parameter(w_)
self.fc1.bias = torch.nn.init.constant_(self.fc1.bias, val=1.0)
self.fc2 = torch.nn.Linear(hidden_size, output_size )
w2 = torch.normal(0, 0.01, size=(output_size, hidden_size), requires_grad=True)
self.fc2.weight = torch.nn.Parameter(w2)
self.fc2.bias = torch.nn.init.constant_(self.fc2.bias, val=1.0)
self.act = torch.sigmoid #激活函数
def forward(self, inputs):
outputs = self.fc1(inputs)
outputs = self.act(outputs)
outputs = self.fc2(outputs)
#outputs = self.act(outputs) #最后一层需要激活函数激活再输出
return outputs
fnn_model =Model_MLP_L2_V3(input_size=4, hidden_size=6,output_size=3)
#完善Runner类
import torch.nn.functional as F
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!")
# 模型评估阶段,使用'torch.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.accumulate()
# 记录验证集loss
if global_step != -1:
self.dev_losses.append((global_step, dev_loss))
self.dev_scores.append(dev_score)
return dev_score, dev_loss
# 模型评估阶段,使用'torch.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)
#Accuracy代码
class Accuracy(object):
def __init__(self, is_logist=True):
# 用于统计正确的样本个数
self.num_correct = 0
# 用于统计样本的总数
self.num_count = 0
self.is_logist = is_logist
def update(self, outputs, labels):
#使用的是对数几率lotigs 将概率分布转化为对数形式
# 判断是二分类任务还是多分类任务,shape[1]=1时为二分类任务,shape[1]>1时为多分类任务
if outputs.shape[1] == 1: # 二分类
outputs = torch.squeeze(outputs, axis=-1)
if self.is_logist:
# logist判断是否大于0
preds = (outputs>=0).to(torch.float32)
else:
# 如果不是logist,判断每个概率值是否大于0.5,当大于0.5时,类别为1,否则类别为0
preds = (outputs>=0.5).to(torch.float32)
else:
# 多分类时,使用'torch.argmax'计算最大元素索引作为类别
preds = torch.argmax(outputs, dim=1).int()
# 获取本批数据中预测正确的样本个数
labels = torch.squeeze(labels, axis=-1)
#batch_correct = torch.sum(torch.tensor(preds==labels, dtype=torch.float32)).numpy()
batch_correct = torch.sum((preds == labels).clone().detach()).numpy()
batch_count = len(labels)
# 更新num_correct 和 num_count
self.num_correct += batch_correct
self.num_count += batch_count
def accumulate(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"
#模型训练
import torch.optim as opt #导入优化库
lr = 0.2 #定义学习率
# 定义网络
model = fnn_model #模型fnn_model已在上面代码定义
# 定义优化器SGD 随机梯度下降优化器,将模型参数传给优化器
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)
# 启动训练 训练轮数为150轮,每隔100步记录一次,每隔50步进行一次评估
log_steps = 100
eval_steps = 50
runner.train(train_loader, dev_loader,
num_epochs=150, log_steps=log_steps, eval_steps = eval_steps,
save_path="best_model.pdparams")
#可视化
import matplotlib.pyplot as plt
# 绘制训练集和验证集的损失变化以及验证集上的准确率变化曲线
def plot_training_loss_acc(runner, fig_name,
fig_size=(16, 6),
sample_step=20, #取数据的步长,每隔20取一个
#设置图例的位置
loss_legend_loc="upper right",
acc_legend_loc="lower right",
#设置曲线的颜色
train_color="b",
dev_color='r',
fontsize='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='x-large')#图例
# 绘制评价准确率变化曲线
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='x-large')
plt.savefig(fig_name)#保存图像
plt.show()
plot_training_loss_acc(runner, 'fw-loss.pdf') #调用绘制图形的函数
#模型评价--加载最优模型,从runner类中调取
runner.load_model('best_model.pdparams')
# 进行评价,评价指标有score准确率 和 loss损失函数值
score, loss = runner.evaluate(test_loader) #调取runner类中的评价函数
print("[Test] accuracy/loss: {:.4f}/{:.4f}".format(score, loss))
#模型预测
#获取测试集中第一条数据
X, label = train_dataset[0] #x是特征 label是对应的标签
logits = runner.predict(X) #将预测结果存储在logits中,对数几率
#找到logits中概率最大的类别索引,并将其转换为numpy数组并存储在pred_class中
pred_class = torch.argmax(logits[0]).numpy()
label = label.numpy() #将标签转换为numpy数组 存储在label中
# 输出真实类别与预测类别
print("The true category is {} and the predicted category is {}".format(label, pred_class))
错误与反思
1.进行模型训练时,在Accuracy类的代码中(416行)出现了警告Warning:
Warning的全内容为:
UserWarning: To copy construct from a tensor, it is recommended to use sourceTensor.clone().detach() or sourceTensor.clone().detach().requires_grad_(True), rather than torch.tensor(sourceTensor).
翻译一下就是:要从张量复制构造,建议使用sourceTensor.cone().detach() 或者 sourceTensor.clone().detch().requires_grad_(True)。
最好不要使用torch.tensor(sourceTensor)。
我修改了这行代码(417行),错误就解决了:
为什么呢?我去搜索了一下sourceTensor.cone().detach() 和 sourceTensor.clone().detch().requires_grad_(True)的作用、用法。
2.
在进行第一次模型训练时,发现自己居然运行出了这样荒谬的结果:
训练集和验证集上的损失值波动超级大,不仅没有随着训练次数的增加减小,反而还收敛在1.095-1.100之间。 验证集上的评估准则 精确率也有很大的波动,在极大和极小值跳跃。
这种训练情况把我整慌了,我决定重新训练,修改一下之前定义过的类。但是并没有发现代码的问题,于是我将这个问题搁置到第二天了,第二天再次打开pycharm运行了一遍代码(未改动),居然又运行出了正确的结果了。所以问题就在于pycharm,不在代码。
这次涨经验了,原来pycharm编译器也会出问题,当不知道代码中的错误到底出在哪里时,可以重启电脑或者pycharm,说不定问题就解决了。
3.
定义一个类时,接受的参数中的*kwargs是什么?
在Python中,kwargs(关键字参量)是一种用于接受任意数量和类型参数的方法。这些参数被包含在一个名为kwargs的特殊字典中。在类定义中,可以使用**kwargs来定义一个或多个接受任意关键字参数的类方法。这些参数可以在方法内部使用,并在调用时传递任意数量的参数。