目录
1 小批量梯度下降法
1.0 展开聊一聊~
1.1 数据分组
1.2 用DataLoader进行封装
1.3 模型构建
1.4 完善Runner类
1.5 模型训练
1.6 模型评价
1.7 模型预测
思考
总结
参考文献
首先基础知识铺垫~
继续使用第三章中的鸢尾花分类任务,将Softmax分类器替换为前馈神经网络。
- 损失函数:交叉熵损失;
- 优化器:随机梯度下降法;
- 评价指标:准确率;
1 小批量梯度下降法
1.0 展开聊一聊~
在梯度下降法中,目标函数是整个训练集上的风险函数,这种方式称为批量梯度下降法(Batch Gradient Descent,BGD)。批量梯度下降法在每次迭代时需要计算每个样本上损失函数的梯度并求和。为了减少每次迭代的计算复杂度,我们可以在每次迭代时只采集一小部分样本,计算在这组样本上损失函数的梯度并更新参数,这种优化方式称为小批量梯度下降法(Mini-Batch Gradient Descent,Mini-Batch GD)。
梯度下降算法一般情况下主要说的是三种,批量梯度下降、随机梯度下降、小批量梯度下降。
知道大家跟我一样不爱看一大段一大段的定义,巴拉巴拉一堆的,所以我大概总结了一下,快拿出你们的小本本!!!
首先,要明确梯度下降算法都是优化算法,用于求解目标函数的最优参数:
- 批量梯度下降(Batch Gradient Descent,BGD)
- 随机梯度下降(Stochastic Gradient Descent,SGD)
- 小批量梯度下降(Mini-Batch Gradient Descent,MBGD)
批量梯度下降(BGD):最早出现的梯度下降方法是批量梯度下降。BGD在每一次迭代中使用所有训练样本来计算梯度,并更新模型参数,步骤如下:
- 对于每个训练样本,计算梯度。
- 将所有梯度求平均,得到一个全局梯度。
- 根据学习率和全局梯度更新模型参数。
优点:
- 收敛性较好,能够达到全局最优(目标函数是凸函数)。
- 梯度计算相对准确,参数更新稳定。
- 收敛速度最快,可以保证每一步都是准确地向着极值点的方向趋近,所需要的迭代次数最少。
缺点:
- 计算梯度时需要处理大量数据,计算开销较大。
- 参数更新只能在整个训练集上进行,但大规模数据集通常会有大量冗余数据,所以不适用于大规模数据集。
- 容易陷入局部最优(目标函数是非凸函数)。
这里需要提一下~对所有样本的计算,可以利用向量运算进行并行计算来提升运算速度。
随机梯度下降(SGD):为了解决批量梯度下降在处理大规模数据集时的计算开销问题,随机梯度下降方法被提出。SGD在每一次迭代中仅使用一个训练样本来计算梯度,并更新模型参数。具体步骤如下:
- 随机选择一个训练样本。
- 计算该样本的梯度。
- 根据学习率和该样本的梯度更新模型参数。
优点:
- 计算开销较小,适用于大规模数据集。
- 参数更新频繁,可能更容易逃离局部最优。
缺点:
- 每次迭代只使用一个样本,但是单个样本计算出的梯度不能够很好的体现全体样本的梯度。
- 参数更新的方向较不稳定,可能会产生参数震荡。
- 参数更新非常的频繁,在最优点附近晃来晃去,收敛速度大大降低。
小批量梯度下降(MBGD):为了兼顾批量梯度下降和随机梯度下降的优点,小批量梯度下降方法被引入。小批量梯度下降算法又被叫做小批量随机梯度下降算法。MBGD在每一次迭代中使用一小部分训练样本(通常称为mini-batch)来计算梯度,并更新模型参数。具体步骤如下:
- 随机选择一小部分训练样本(mini-batch)。
- 计算这些样本的梯度。
- 根据学习率和这些样本的梯度更新模型参数。
优点:
- 兼具BGD和SGD的优点,计算开销适中,参数更新相对稳定。
- 可以利用矩阵运算的高效性,提高计算效率。
- 较容易并行化处理,适用于大规模数据集。
缺点:
- 学习率选择较为敏感,需要进行合适的调参。
总结:三种梯度下降方法各有优劣。批量梯度下降收敛性好,但计算开销大;随机梯度下降计算开销小,但更新不稳定;小批量梯度下降在两者之间取得平衡,并且对于大规模数据集有较好的适应性。在实际应用中,根据具体问题的规模和特点选择合适的梯度下降方法。
1.1 数据分组
为了小批量梯度下降法,我们需要对数据进行随机分组。目前,机器学习中通常做法是构建一个数据迭代器,每个迭代过程中从全部数据集中获取一批指定数量的数据。原理图展示一下:
首先,将数据集封装为Dataset类,传入一组索引值,根据索引从数据集合中获取数据;
其次,构建DataLoader类,需要指定数据批量的大小和是否需要对数据进行乱序,通过该类即可批量获取数据。
import torch.utils.data as Data
class IrisDataset(Data.Dataset):
def __init__(self, mode='train', num_train=120, num_dev=15):
super(IrisDataset, self).__init__()
# 调用第三章中的数据读取函数,其中不需要将标签转成one-hot类型
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)
__getitem__:根据给定索引获取数据集中指定样本,并对样本进行数据处理;
__len__:返回数据集样本个数。
train_dataset = IrisDataset(mode='train')
dev_dataset = IrisDataset(mode='dev')
test_dataset = IrisDataset(mode='test')
print("length of train set: ", len(train_dataset))
1.2 用DataLoader进行封装
batch_size = 16
# 加载数据
train_loader = Data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
dev_loader = Data.DataLoader(dev_dataset, batch_size=batch_size)
test_loader = Data.DataLoader(test_dataset, batch_size=batch_size)
1.3 模型构建
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.fc2 = nn.Linear(hidden_size, output_size)
# 定义网络使用的激活函数
self.act = nn.Sigmoid()
nn.init.normal_(self.fc1.weight, mean=0., std=0.01)
nn.init.constant_(self.fc1.bias, 1.0)
nn.init.normal_(self.fc2.weight, mean=0., std=0.01)
nn.init.constant_(self.fc2.bias, 1.0)
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)
1.4 完善Runner类
class Accuracy(object):
def __init__(self, is_logist=True):
"""
输入:
- is_logist: outputs是logist还是激活后的值
"""
# 用于统计正确的样本个数
self.num_correct = 0
# 用于统计样本的总数
self.num_count = 0
self.is_logist = is_logist
def update(self, outputs, labels):
"""
输入:
- outputs: 预测值, shape=[N,class_num]
- labels: 标签值, shape=[N,1]
"""
# 判断是二分类任务还是多分类任务,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:
# 多分类时,使用'paddle.argmax'计算最大元素索引作为类别
preds = torch.argmax(outputs, dim=1).int()
# 获取本批数据中预测正确的样本个数
labels = torch.squeeze(labels, dim=-1)
batch_correct = float((preds == labels).sum())
# batch_correct = torch.sum(torch.tensor(preds == labels, dtype=torch.float32)).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"
完善RunnerV3类
import torch
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() #无clear_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.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
# 模型评估阶段,使用'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)
注意torch环境中没有clear_grad方法,这儿需要调用zero_grad方法。
1.5 模型训练
这里把RunnerV3放入runner.py存储在nndl文件夹中
import torch.optim as opt
from nndl.runner import RunnerV3
import torch.nn.functional as F
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)
使用训练集和验证集进行模型训练,共训练150个epoch。在实验中,保存准确率最高的模型作为最佳模型。
# 启动训练
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")
可视化观察训练集损失和训练集loss变化情况,代码如下:
import matplotlib.pyplot as plt
# 绘制训练集和验证集的损失变化以及验证集上的准确率变化曲线
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="#e4007f",
dev_color='#f19ec2',
fontsize='large',
train_linestyle="-",
dev_linestyle='--'):
global dev_steps
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')
1.6 模型评价
使用测试数据对在训练过程中保存的最佳模型进行评价,观察模型在测试集上的准确率以及Loss情况,代码如下:
# 加载最优模型
runner.load_model('best_model.pdparams')
# 模型评价
score, loss = runner.evaluate(test_loader)
print("[Test] accuracy/loss: {:.4f}/{:.4f}".format(score, loss))
1.7 模型预测
同样地,也可以使用保存好的模型,对测试集中的某一个数据进行模型预测,观察模型效果。代码如下
# 获取测试集中第一条数据
X, label = train_dataset[0]
logits = runner.predict(X)
pred_class = torch.argmax(logits[0]).numpy()
label = label.numpy()
# 输出真实类别与预测类别
print("The true category is {} and the predicted category is {}".format(label, pred_class))
思考
softmax、svm、前馈神经网络三种进行比较,svm代码如下:
from sklearn.svm import SVC
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import accuracy_score
def SVC_split(x_train, y_train, x_test, y_test):
# 定义SVM分类器
svm = SVC()
# 定义参数空间
param_grid = {
'C': [0.01, 0.1, 1, 10],
'kernel': ['linear', 'rbf', 'poly'],
'gamma': ['scale', 'auto']
}
# 使用GridSearchCV进行交叉验证和参数选择
grid_search = GridSearchCV(svm, param_grid=param_grid, cv=5)
grid_search.fit(x_train, y_train.ravel())
# 在测试集上评估模型性能
y_pred = grid_search.predict(x_test)
accuracy = accuracy_score(y_test, y_pred)
print("Accuracy on test set: {:.2f}%".format(accuracy * 100))
SVC_split(train_dataset.X,train_dataset.y ,test_dataset.X, test_dataset.y)
调用实验四的softmax函数的结果如下:
很明显我们发现softmax回归的准确率远远低于前馈神经网络和svm,简单总结一下都有什么原因吧:
softmax回归是一个线性模型,其只能学习到线性关系。对于复杂的非线性分类问题,Softmax回归的表达能力可能不足以捕捉到数据中的更复杂模式。相比之下,前馈神经网络具有更强大的非线性建模能力,可以通过多个隐藏层和非线性激活函数来学习到更复杂的特征表示。SVM也可以通过核函数将低维输入映射到高维空间,从而进行非线性分类。
由此得出结论,但需要注意的是,Softmax回归在所有二分类任务上表现都会远远低于前馈神经网络和SVM。对于一些简单的线性可分问题或数据分布较为简单的情况下,Softmax回归可能表现得很好。然而,对于更复杂的问题和数据集,使用更复杂的模型如前馈神经网络和SVM通常能够获得更好的性能。
但是有个疑问前馈神经网络和SVM究竟哪个更好一点?
简而言之,神经网络是个“黑匣子”,优化目标是基于经验风险最小化,易陷入局部最优,训练结果不太稳定,一般需要大样本;
而支持向量机有严格的理论和数学基础,基于结构风险最小化原则, 泛化能力优于前者,算法具有全局最优性, 是针对小样本统计的理论。
就目前我的理论知识,好像想搞明白哪个好哪个坏可能有点难,而且模型好像很难说哪个好哪个坏,可能针对不同的数据集表现也会不一样,害,浅浅插个眼,等之后,对深度学习有了一定程度的了解的时候再回来,看看有没有一个答案吧。
总结
到此为止前馈神经网络结束啦,有了一点搭建神经网络的经验了吧,大概流程如下:
-
定义网络结构:首先需要确定网络的结构,包括输入层、隐藏层和输出层的大小和数量。
-
初始化参数:对于每个神经元,需要初始化权重和偏置值。权重和偏置值通常是随机初始化的,以避免初始状态过于相似导致模型收敛缓慢。
-
定义损失函数:损失函数用来衡量预测值和真实值之间的误差。二分类问题通常使用交叉熵损失函数,回归问题可以使用均方误差损失函数。
-
定义优化器:优化器用于更新模型的参数,使损失函数最小化。常见的优化器包括随机梯度下降 (SGD)、Adam等。
-
训练模型:通过传递数据进行前向传播和反向传播,更新模型的参数。在训练过程中,需要将数据分为训练集、验证集和测试集。
-
模型评价:在训练完成之后,可以通过输入新数据并进行前向传播来得到预测结果。也可以进行可视化等等。
本次实验对前馈神经网络的应用有更为明确的理解,同时针对softmax,svm,CNN的对比,在实验和搜索资料的过程中,也明白了什么模型更适合应用在什么范围内,softmax和CNN的具体的应用区别还没弄明白算是个小小的遗憾吧,等有更多深度学习经验的时候看看能不能再回来解答这个问题吧~
参考文献
torch.nn.Module所有方法总结及其使用举例_torch.nn.module cuda-CSDN博客
torch.nn — PyTorch master documentation
NNDL 实验五 前馈神经网络(3)鸢尾花分类-CSDN博客
神经网络 VS SVM_svm和神经网络的区别-CSDN博客