- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍖 原作者:K同学啊
往期内容
深度学习第一周:单通道图像——MNIST手写数字识别
深度学习第二周:多通道图像——CIFAR10彩色图片识别
深度学习第三周:本地数据集——天气识别
深度学习第四周:模型保存和加载——猴痘病识别
一、 前期准备
1. 设置GPU
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
import torchvision
# 设置硬件设备,如果有GPU则使用,没有则使用cpu
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
Output:
device(type='cuda') #代表使用的是GPU
2. 设置随机种子
为了保证实验可以复现,我们通过随机种子控制随机数的生成。
import random
import numpy as np
def setup_seed(seed):
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
np.random.seed(seed)
random.seed(seed)
torch.backends.cudnn.deterministic = True
# 设置随机数种子
setup_seed(2)
3. 导入数据
import os,PIL,random,pathlib
data_dir = './data/运动鞋/train'
data_dir = pathlib.Path(data_dir)
data_paths = list(data_dir.glob('*'))
classeNames = [str(path).split("\\")[3] for path in data_paths]
classeNames
['adidas', 'nike']
pathlib.Path(data_dir)
:它将data_dir
字符串转换成一个Path
对象。这个对象代表了文件系统中的具体路径,无论这个路径是否真实存在。data_dir.glob('*')
:生成一个迭代器。这个迭代器遍历与指定模式匹配的所有路径。当前模式是'*'
,意味着它会匹配data_dir
目录下的所有文件和子目录,不论它们的名字是什么。.split("\\")
:这个方法用于分割路径字符串,使用反斜杠(\)作为分隔符。在Windows操作系统中,路径通常使用反斜杠作为目录分隔符。注意,由于反斜杠在Python字符串中用作转义字符,因此需要双写(即"\")以表示字面上的反斜杠字符。[3]
:代表为path根据反斜杠分割后的第四个元素。
# 关于transforms.Compose的更多介绍可以参考:https://blog.csdn.net/qq_38251616/article/details/124878863
train_transforms = transforms.Compose([
transforms.Resize([224, 224]), # 将输入图片resize成统一尺寸
transforms.RandomHorizontalFlip(), # 数据增强-随机水平翻转
transforms.ToTensor(), # 将PIL Image或numpy.ndarray转换为tensor,并归一化到[0,1]之间
transforms.Normalize( # 标准化处理-->转换为标准正态分布(高斯分布),使模型更容易收敛
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]) # 其中 mean=[0.485,0.456,0.406]与std=[0.229,0.224,0.225] 从数据集中随机抽样计算得到的。
])
test_transform = transforms.Compose([
transforms.Resize([224, 224]), # 将输入图片resize成统一尺寸
transforms.ToTensor(), # 将PIL Image或numpy.ndarray转换为tensor,并归一化到[0,1]之间
transforms.Normalize( # 标准化处理-->转换为标准正态分布(高斯分布),使模型更容易收敛
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]) # 其中 mean=[0.485,0.456,0.406]与std=[0.229,0.224,0.225] 从数据集中随机抽样计算得到的。
])
train_dataset = datasets.ImageFolder("./data/运动鞋/train",transform=train_transforms)
test_dataset = datasets.ImageFolder("./data/运动鞋/test",transform=train_transforms)
datasets.ImageFolder
:torchvision.datasets.ImageFolder
是PyTorch提供的一个类,它用于处理那些目录结构按类别组织的图像数据集。每个类的图像应存储在各自的子目录中。ImageFolder
自动将这些子目录的名称作为类标签。mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]
:mean, std 三通道值是从imagenet训练集中抽样算出来的。
查看数据集大小,
train_size=len(train_dataset)
test_size=len(test_dataset)
train_size,test_size
(502, 76)
通过 torch.utils.data.DataLoader
设置 Loader,
batch_size = 32
train_dl = torch.utils.data.DataLoader(train_dataset,
batch_size=batch_size,
shuffle=True,
num_workers=1)
test_dl = torch.utils.data.DataLoader(test_dataset,
batch_size=batch_size,
shuffle=True,
num_workers=1)
num_workers=1
: 这个参数设置了用于数据加载的子进程数。增加num_workers
的数量可以提高数据加载的速度,特别是当数据集比较大或者数据预处理比较耗时时。设置为2意味着会有两个工作进程并行读取数据。这个参数需要根据本地/服务器性能,进行灵活调整,并非越大越好。
查看数据维数,
for X, y in test_dl:
print("Shape of X [N, C, H, W]: ", X.shape)
print("Shape of y: ", y.shape, y.dtype)
break
Shape of X [N, C, H, W]: torch.Size([32, 3, 224, 224])
Shape of y: torch.Size([32]) torch.int64
对应的是:
- batch number:32
- channel:3
- height:224(图片的长)
- weight:224(图片的宽)
2.构建网络
import torch.nn.functional as F
class Model(nn.Module):
def __init__(self):
super(Model, self).__init__()
self.conv1=nn.Sequential(
nn.Conv2d(3, 12, kernel_size=5, padding=0), # 12*220*220
nn.BatchNorm2d(12), # 对当前batch下所有图片的同一个特征的所有通道,进行归一化
nn.ReLU()) #利用sequential建立小block
self.conv2=nn.Sequential(
nn.Conv2d(12, 12, kernel_size=5, padding=0), # 12*216*216
nn.BatchNorm2d(12),
nn.ReLU())
self.pool3=nn.Sequential(
nn.MaxPool2d(2)) # 12*108*108
self.conv4=nn.Sequential(
nn.Conv2d(12, 24, kernel_size=5, padding=0), # 24*104*104
nn.BatchNorm2d(24),
nn.ReLU())
self.conv5=nn.Sequential(
nn.Conv2d(24, 24, kernel_size=5, padding=0), # 24*100*100
nn.BatchNorm2d(24),
nn.ReLU())
self.pool6=nn.Sequential(
nn.MaxPool2d(2)) # 24*50*50
self.dropout = nn.Sequential(
nn.Dropout(0.2))
self.fc=nn.Sequential(
nn.Linear(24*50*50, len(classeNames)))
def forward(self, x):
batch_size = x.size(0)
x = self.conv1(x) # 卷积-BN-激活
x = self.conv2(x) # 卷积-BN-激活
x = self.pool3(x) # 池化
x = self.conv4(x) # 卷积-BN-激活
x = self.conv5(x) # 卷积-BN-激活
x = self.pool6(x) # 池化
x = self.dropout(x)
x = x.view(batch_size, -1) # flatten 变成全连接网络需要的输入 (batch, 24*50*50) ==> (batch, -1), -1 此处自动算出的是24*50*50
x = self.fc(x)
return x
nn.Conv2d(input_channel, output_channel, kernel_size)
CNN理论核心——卷积层,设置kernel / filter 大小和个数,用于提取数据特征。nn.MaxPool2d(2)
下采样(池化),降低数据维数,表示抽象概念,传入参数为池化核大小。nn.ReLU
激活函数,赋予模型拟合非线性关系的能力。nn.Linear(input_dimension, output_dimension)
全连接层,相当于给数据乘以权重矩阵W,W的size由input_dimension
,output_dimension
和确定。- 卷积核的size为:kernel_size × kernel_size × 上一层channel,因此,卷积核维数 = 数据维数 + 1。
nn.Sequential()
:序列容器,用于搭建神经网络的模块被按照被传入构造器的顺序添加到nn.Sequential()容器中。OrderedDict也可以被传入nn.Sequential()中。
查看网络参数,
from torchinfo import summary
model = Model().to(device) # 将模型转移到GPU中(我们模型运行均在GPU中进行)
# 加载训练好的参数
# PATH = './Model/P5_model.pth'
# model.load_state_dict(torch.load(PATH, map_location=device))
summary(model)
=================================================================
Layer (type:depth-idx) Param #
=================================================================
Model --
├─Sequential: 1-1 --
│ └─Conv2d: 2-1 912
│ └─BatchNorm2d: 2-2 24
│ └─ReLU: 2-3 --
├─Sequential: 1-2 --
│ └─Conv2d: 2-4 3,612
│ └─BatchNorm2d: 2-5 24
│ └─ReLU: 2-6 --
├─Sequential: 1-3 --
│ └─MaxPool2d: 2-7 --
├─Sequential: 1-4 --
│ └─Conv2d: 2-8 7,224
│ └─BatchNorm2d: 2-9 48
│ └─ReLU: 2-10 --
├─Sequential: 1-5 --
│ └─Conv2d: 2-11 14,424
│ └─BatchNorm2d: 2-12 48
│ └─ReLU: 2-13 --
├─Sequential: 1-6 --
│ └─MaxPool2d: 2-14 --
├─Sequential: 1-7 --
│ └─Dropout: 2-15 --
├─Sequential: 1-8 --
│ └─Linear: 2-16 120,002
=================================================================
Total params: 146,318
Trainable params: 146,318
Non-trainable params: 0
=================================================================
三、 训练模型
1. 设置超参数——自定义动态学习率
def adjust_learning_rate(optimizer, epoch, start_lr):
# 每 2 个epoch衰减到原来的 0.98
lr = start_lr * (0.98 ** (epoch // 2)) #//代表÷2之后的商
for param_group in optimizer.param_groups:
param_group['lr'] = lr
learn_rate = 1e-4 # 初始学习率
optimizer = torch.optim.SGD(model.parameters(), lr=learn_rate)
- 然而,尽管
adjust_learning_rate
函数本身不打印或返回学习率的更新结果,学习率实际上是通过修改optimizer
对象的状态来更新的。 这种更新是在内存中对optimizer
对象进行的,不需要通过返回值或打印来“输出”。 代码通过修改optimizer.param_groups
中每个参数组的lr
值来实现学习率的更新。
1. 设置超参数——方式二:PyTorch调用API
learn_rate = 1e-4 # 初始学习率
lambda1 = lambda epoch: (0.98 ** (epoch // 2))
optimizer = torch.optim.SGD(model.parameters(), lr=learn_rate)
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=lambda1) #选定调整方法
lambda1 = lambda epoch: (0.98 ** (epoch // 2))
:定义一个匿名函数lambd1
,使用lambda
关键字(专门用于创建匿名函数),其参数是epoch
,函数体是返回 0.98 ^ [epoch
除以 2 的商]。scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=lambda1)
:LambdaLR
调度器名为scheduler
,它根据lambda1
函数动态调整与optimizer
关联的学习率。LambdaLR
接受两个主要参数:optimizer, lr_lambda
。optimizer
是之前创建的SGD优化器,lr_lambda
是定义好的调整学习率的函数。
2. 训练函数
# 训练循环
def train(dataloader, model, loss_fn, optimizer):
size = len(dataloader.dataset) # 训练集的大小
num_batches = len(dataloader) # 批次数目, (size/batch_size,向上取整)
train_loss, train_acc = 0, 0 # 初始化训练损失和正确率
for X, y in dataloader: # 获取图片及其标签
X, y = X.to(device), y.to(device)
# 计算预测误差
pred = model(X) # 网络输出
loss = loss_fn(pred, y) # 计算网络输出和真实值之间的差距,targets为真实值,计算二者差值即为损失
# 反向传播
optimizer.zero_grad() # grad属性归零
loss.backward() # 反向传播
optimizer.step() # 每一步自动更新
# 记录acc与loss
train_acc += (pred.argmax(1) == y).type(torch.float).sum().item()
train_loss += loss.item()
train_acc /= size
train_loss /= num_batches
return train_acc, train_loss
optimizer.zero_grad()
清空上一次的累计梯度loss.backward()
根据tensor进行过的数学运算来自动计算其对应的梯度。具体来说,torch.tensor
是autograd包的基础类,如果你设置tensor的requires_grads
为True
,就会开始跟踪这个tensor上面的所有运算,如果你做完运算后使用tensor.backward()
,所有的梯度就会自动运算,tensor的梯度将会累加到它的.grad属性里面去。optimizer.step()
step()函数的作用是执行一次反向传播,通过梯度下降法来更新参数的值。optimizer
只负责通过梯度下降进行优化,而不负责产生梯度
3.测试函数
测试函数和训练函数大致相同,但是由于不进行梯度下降对网络权重进行更新,所以不需要传入优化器。
def test (dataloader, model, loss_fn):
size = len(dataloader.dataset) # 测试集的大小,一共10000张图片
num_batches = len(dataloader) # 批次数目,313(10000/32=312.5,向上取整)
test_loss, test_acc = 0, 0
# 当不进行训练时,停止梯度更新,节省计算内存消耗
with torch.no_grad():
for imgs, target in dataloader:
imgs, target = imgs.to(device), target.to(device)
# 计算loss
target_pred = model(imgs)
loss = loss_fn(target_pred, target)
test_loss += loss.item() #累计loss
test_acc += (target_pred.argmax(1) == target).type(torch.float).sum().item() # 累计正确个数
test_acc /= size
test_loss /= num_batches
return test_acc, test_loss
4. 正式训练
# import pdb
loss_fn = nn.CrossEntropyLoss() # 创建损失函数
epochs = 40
train_loss = []
train_acc = []
test_loss = []
test_acc = []
# pdb.set_trace()
for epoch in range(epochs):
# 更新学习率(使用自定义学习率时使用)
# adjust_learning_rate(optimizer, epoch, learn_rate)
model.train()
epoch_train_acc, epoch_train_loss = train(train_dl, model, loss_fn, optimizer)
scheduler.step() # 更新学习率(调用官方动态学习率接口时使用)
model.eval()
epoch_test_acc, epoch_test_loss = test(test_dl, model, loss_fn)
train_acc.append(epoch_train_acc)
train_loss.append(epoch_train_loss)
test_acc.append(epoch_test_acc)
test_loss.append(epoch_test_loss)
# 获取当前的学习率
lr = optimizer.state_dict()['param_groups'][0]['lr']
template = ('Epoch:{:2d}, Train_acc:{:.1f}%, Train_loss:{:.3f}, Test_acc:{:.1f}%, Test_loss:{:.3f}, Lr:{:.2E}')
print(template.format(epoch+1, epoch_train_acc*100, epoch_train_loss,
epoch_test_acc*100, epoch_test_loss, lr))
print('Done')
Epoch: 1, Train_acc:54.0%, Train_loss:0.743, Test_acc:52.6%, Test_loss:0.683, Lr:1.00E-04
Epoch: 2, Train_acc:59.8%, Train_loss:0.699, Test_acc:64.5%, Test_loss:0.662, Lr:9.80E-05
Epoch: 3, Train_acc:57.8%, Train_loss:0.693, Test_acc:61.8%, Test_loss:0.619, Lr:9.80E-05
Epoch: 4, Train_acc:67.5%, Train_loss:0.620, Test_acc:63.2%, Test_loss:0.662, Lr:9.60E-05
Epoch: 5, Train_acc:69.3%, Train_loss:0.588, Test_acc:57.9%, Test_loss:0.603, Lr:9.60E-05
Epoch: 6, Train_acc:71.1%, Train_loss:0.576, Test_acc:63.2%, Test_loss:0.644, Lr:9.41E-05
Epoch: 7, Train_acc:71.1%, Train_loss:0.556, Test_acc:67.1%, Test_loss:0.590, Lr:9.41E-05
Epoch: 8, Train_acc:73.9%, Train_loss:0.549, Test_acc:63.2%, Test_loss:0.605, Lr:9.22E-05
Epoch: 9, Train_acc:73.5%, Train_loss:0.541, Test_acc:69.7%, Test_loss:0.561, Lr:9.22E-05
Epoch:10, Train_acc:74.5%, Train_loss:0.535, Test_acc:63.2%, Test_loss:0.605, Lr:9.04E-05
Epoch:11, Train_acc:77.5%, Train_loss:0.516, Test_acc:68.4%, Test_loss:0.547, Lr:9.04E-05
Epoch:12, Train_acc:79.1%, Train_loss:0.479, Test_acc:67.1%, Test_loss:0.638, Lr:8.86E-05
Epoch:13, Train_acc:77.9%, Train_loss:0.472, Test_acc:65.8%, Test_loss:0.565, Lr:8.86E-05
Epoch:14, Train_acc:80.5%, Train_loss:0.457, Test_acc:75.0%, Test_loss:0.529, Lr:8.68E-05
Epoch:15, Train_acc:80.5%, Train_loss:0.471, Test_acc:73.7%, Test_loss:0.509, Lr:8.68E-05
Epoch:16, Train_acc:80.7%, Train_loss:0.446, Test_acc:71.1%, Test_loss:0.525, Lr:8.51E-05
Epoch:17, Train_acc:81.3%, Train_loss:0.440, Test_acc:72.4%, Test_loss:0.545, Lr:8.51E-05
Epoch:18, Train_acc:83.7%, Train_loss:0.436, Test_acc:67.1%, Test_loss:0.560, Lr:8.34E-05
Epoch:19, Train_acc:84.9%, Train_loss:0.417, Test_acc:67.1%, Test_loss:0.556, Lr:8.34E-05
Epoch:20, Train_acc:81.1%, Train_loss:0.418, Test_acc:69.7%, Test_loss:0.500, Lr:8.17E-05
Epoch:21, Train_acc:84.3%, Train_loss:0.400, Test_acc:67.1%, Test_loss:0.526, Lr:8.17E-05
Epoch:22, Train_acc:84.7%, Train_loss:0.405, Test_acc:77.6%, Test_loss:0.501, Lr:8.01E-05
Epoch:23, Train_acc:86.1%, Train_loss:0.398, Test_acc:68.4%, Test_loss:0.555, Lr:8.01E-05
Epoch:24, Train_acc:85.5%, Train_loss:0.398, Test_acc:73.7%, Test_loss:0.498, Lr:7.85E-05
Epoch:25, Train_acc:87.3%, Train_loss:0.366, Test_acc:73.7%, Test_loss:0.494, Lr:7.85E-05
Epoch:26, Train_acc:85.5%, Train_loss:0.380, Test_acc:71.1%, Test_loss:0.473, Lr:7.69E-05
Epoch:27, Train_acc:88.2%, Train_loss:0.369, Test_acc:65.8%, Test_loss:0.589, Lr:7.69E-05
Epoch:28, Train_acc:88.4%, Train_loss:0.364, Test_acc:72.4%, Test_loss:0.480, Lr:7.54E-05
Epoch:29, Train_acc:85.9%, Train_loss:0.368, Test_acc:68.4%, Test_loss:0.543, Lr:7.54E-05
Epoch:30, Train_acc:87.1%, Train_loss:0.357, Test_acc:69.7%, Test_loss:0.500, Lr:7.39E-05
Epoch:31, Train_acc:87.3%, Train_loss:0.349, Test_acc:72.4%, Test_loss:0.544, Lr:7.39E-05
Epoch:32, Train_acc:90.6%, Train_loss:0.341, Test_acc:76.3%, Test_loss:0.474, Lr:7.24E-05
Epoch:33, Train_acc:89.6%, Train_loss:0.337, Test_acc:78.9%, Test_loss:0.482, Lr:7.24E-05
Epoch:34, Train_acc:89.4%, Train_loss:0.336, Test_acc:72.4%, Test_loss:0.596, Lr:7.09E-05
Epoch:35, Train_acc:91.6%, Train_loss:0.317, Test_acc:69.7%, Test_loss:0.493, Lr:7.09E-05
Epoch:36, Train_acc:88.6%, Train_loss:0.343, Test_acc:76.3%, Test_loss:0.479, Lr:6.95E-05
Epoch:37, Train_acc:90.8%, Train_loss:0.316, Test_acc:73.7%, Test_loss:0.425, Lr:6.95E-05
Epoch:38, Train_acc:89.0%, Train_loss:0.323, Test_acc:76.3%, Test_loss:0.489, Lr:6.81E-05
Epoch:39, Train_acc:90.2%, Train_loss:0.317, Test_acc:73.7%, Test_loss:0.490, Lr:6.81E-05
Epoch:40, Train_acc:90.6%, Train_loss:0.323, Test_acc:75.0%, Test_loss:0.497, Lr:6.68E-05
Done
model.train()
的作用是启用 Batch Normalization 和 Dropout。model.eval()
的作用是关闭 Batch Normalization 和 Dropout。Normalization部分是调用training set中的方差和均值进行。Dropout部分不需要,Dropout部分只是帮助模型训练,防止过拟合。因此我们直接调用模型训练好的参数即可。
四、 结果可视化
import matplotlib.pyplot as plt
#隐藏警告
import warnings
warnings.filterwarnings("ignore") #忽略警告信息
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
plt.rcParams['figure.dpi'] = 100 #分辨率
epochs_range = range(epochs)
plt.figure(figsize=(12, 3))
plt.subplot(1, 2, 1)
plt.plot(epochs_range, train_acc, label='Training Accuracy')
plt.plot(epochs_range, test_acc, label='Test Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')
plt.subplot(1, 2, 2)
plt.plot(epochs_range, train_loss, label='Training Loss')
plt.plot(epochs_range, test_loss, label='Test Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()
2. 指定图片进行预测
补充知识:torch.squeeze() & torch.unsqueeze() 详解
>>> x = torch.zeros(2, 1, 2, 1, 2)
>>> y1 = torch.squeeze(x)
>>> y1.size()
torch.Size([2, 2, 2])
可以发现,第二和第四维度被squeezed,
>>> x = torch.zeros(2, 1, 2, 1, 2)
>>> y2 = torch.squeeze(x, 0)
>>> y2.size()
torch.Size([2, 1, 2, 1, 2])
由于第一维度不为1,因此无法被squeezed,
>>> x = torch.zeros(2, 1, 2, 1, 2)
>>> y3 = torch.squeeze(x, 1)
>>> y3.size()
torch.Size([2, 2, 1, 2])
由于第二维度为1,因此可以被squeezed。现在对指定图片进行预测,
from PIL import Image
classes = list(train_dataset.class_to_idx)
def predict_one_image(image_path, model, transform, classes):
test_img = Image.open(image_path).convert('RGB')
plt.imshow(test_img) # 展示预测的图片
test_img = transform(test_img)
img = test_img.to(device).unsqueeze(0)
model.eval()
output = model(img)
_,pred = torch.max(output,1)
pred_class = classes[pred]
print(f'预测结果是:{pred_class}')
这里需要注意的是,nn.CrossEntropyLoss()
的target
并没有经过独热编码,例如[0 ,3, 4, 1],代表有四个样本,分别属于第0、3、4、1类。但是nn.CrossEntropyLoss()
的output
的 维数 = 类别数,例如 [ 3.4972, -3.2446],代表该样本第1类得分为 3.4972,第2类得分为 -3.2446。由于第1类得分更大,因此该样本将被判定为第1类。
# 预测训练集中的某张照片
predict_one_image(image_path='./data/运动鞋/test/adidas/1.jpg',
model=model,
transform=test_transform,
classes=classes)
预测结果是:adidas
五、保存并加载模型
# 模型保存
PATH = './P5_model.pth' # 保存的参数文件名
torch.save(model.state_dict(), PATH)
# 将参数加载到model当中
model.load_state_dict(torch.load(PATH, map_location=device))
model.state_dict()
:返回模型的状态字典。模型的状态字典是一个包含所有模型参数(如权重和偏置)的Python字典。这使得模型的参数可以被保存。model.load_state_dict(...)
:将一个状态字典加载到指定模型中。状态字典包含了模型参数,如权重和偏置等。torch.load(PATH, map_location=device):
这个函数用于加载之前保存的对象。map_location
用于指定加载状态字典时的设备映射。device可以是’cpu’或者CUDA设备如’cuda:0’。这个参数确保了即使在不同的设备间移动模型时,参数也会被正确地加载。
个人总结:
为什么需要动态学习率 ?
- 加速收敛:在训练初期,使用较大的学习率可以帮助模型快速接近最优解。
- 提高精度:在训练后期,减小学习率可以帮助模型细致调整参数,避免过度震荡,从而更精确地找到最优解。
- 适应数据:对于不同的数据集和不同阶段的训练过程,模型可能需要不同的学习速度。动态调整学习率可以根据模型的当前状态和过去的表现来优化学习过程。
动态学习率调整是优化深度学习模型训练过程中不可或缺的一环,正确的学习率调整策略能显著提高模型训练的效率和效果。