🤵 Author :Horizon Max
✨ 编程技巧篇:各种操作小结
🏆 神经网络篇:经典网络模型
Pytorch 炼丹小结
🚀 优化器(Optimizer)
🎨 SGD (Stochastic Gradient Descent)
- 介绍:随机梯度下降是最基本的优化算法,通过计算每个样本的梯度来更新模型的参数;(20世纪50年代)
- 公式: θ t + 1 = θ t − lr × ∇ θ t J ( θ t ) \theta_{t+1} = \theta_t - \text{lr} \times \nabla_{\theta_t} J(\theta_t) θt+1=θt−lr×∇θtJ(θt),其中 ∇ θ t J ( θ t ) \nabla_{\theta_t} J(\theta_t) ∇θtJ(θt) 表示损失函数 J J J 对模型参数 θ t \theta_t θt 的梯度;
- 优点:计算简单,易于实现;对于小型数据集和浅层网络,效果可能不错;
- 缺点:容易陷入局部最小值;学习率需要手动调整,不适应复杂的优化问题;收敛速度较慢;
- 用法:
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, momentum=momentum,
weight_decay=weight_decay)
model.parameters()
:需要更新的模型参数;lr
:学习率,控制每次更新的步长;momentum
:动量(可选),增加了在更新中考虑先前步骤的方向;通常设置为0.9;weight_decay
:权重衰减(可选),用于对模型参数进行L2正则化;通过减小参数值来防止过拟合;- 正则后的表达式: θ t + 1 = θ t − lr × ∇ θ t J ( θ t ) − (lr × weight_decay × θ t ) \theta_{t+1} = \theta_t - \text{lr} \times \nabla_{\theta_t} J(\theta_t) - (\text{lr} \times \text{weight\_decay} \times \theta_t) θt+1=θt−lr×∇θtJ(θt)−(lr×weight_decay×θt) ;
🎨 Adagrad (Adaptive Gradient Algorithm)
- 介绍:Adagrad是一种自适应学习率优化算法,根据参数的历史梯度进行调整;它根据每个参数的梯度平方和的累积来缩放学习率;(2011年)
- 公式:
E [ g 2 ] t = E [ g 2 ] t − 1 + g t 2 E[g^2]_t = E[g^2]_{t-1} + g^2_t E[g2]t=E[g2]t−1+gt2
l t = η E [ g 2 ] t + ϵ l_t = \frac{\eta}{\sqrt{E[g^2]_t + \epsilon}} lt=E[g2]t+ϵη
p t = p t − 1 − l t ⋅ g t p_t = p_{t-1} - l_t \cdot g_t pt=pt−1−lt⋅gt - 其中, E [ g 2 ] t E[g^2]_t E[g2]t表示参数梯度平方的累积和; g t 2 g^2_t gt2是当前参数的梯度平方;
- l t l_t lt表示当前参数的学习率; η \eta η是初始学习率, ϵ \epsilon ϵ是一个小的常数;
- p t p_t pt表示更新后的参数; g t g_t gt表示当前参数的梯度;
- 优点:自适应调整学习率,适应性强;对于稀疏梯度的处理较好;不需要手动调整学习率;
- 缺点:学习率逐渐减小,可能导致学习速度过慢;在长时间训练中可能会出现学习率衰减过大的问题;
- 用法:
optimizer = torch.optim.Adagrad(model.parameters(), lr=learning_rate, lr_decay=lr_decay,
weight_decay=weight_decay)
model.parameters()
:需要更新的模型参数;lr
:学习率;lr_decay
:学习率衰减(可选),用于降低学习率。通常设置为0;weight_decay
:权重衰减(可选),用于对模型参数进行L2正则化;
🎨 RMSprop(Root Mean Square Propagation)
- 介绍:RMSprop是一种基于梯度平方的自适应学习率优化算法;它使用指数移动平均来估计梯度平方的移动平均,并使用这个估计来更新参数;(2012年)
- 公式:
E [ g 2 ] t = β ⋅ E [ g 2 ] t − 1 + ( 1 − β ) ⋅ g t 2 E[g^2]_t = \beta \cdot E[g^2]_{t-1} + (1 - \beta) \cdot g^2_t E[g2]t=β⋅E[g2]t−1+(1−β)⋅gt2
l t = η E [ g 2 ] t + ϵ l_t = \frac{\eta}{\sqrt{E[g^2]_t + \epsilon}} lt=E[g2]t+ϵη
p t = p t − 1 − l t ⋅ g t p_t = p_{t-1} - l_t \cdot g_t pt=pt−1−lt⋅gt - 其中, E [ g 2 ] t E[g^2]_t E[g2]t表示参数梯度平方的移动平均值; β \beta β是衰减率,用于控制历史梯度对当前梯度的影响;
- l t l_t lt表示当前参数的学习率; η \eta η是初始学习率, ϵ \epsilon ϵ是一个小的常数;
- p t p_t pt表示更新后的参数; g t g_t gt表示当前参数的梯度;
- 优点:自适应调整学习率,适应性强;相对于Adagrad,对学习率的衰减进行了限制,减少了学习率过早衰减的问题;
- 缺点:可能会受到初始学习率和衰减率的敏感性影响;可能会在一些情况下无法找到全局最优解;
- 用法:
optimizer = torch.optim.RMSprop(model.parameters(), lr=learning_rate, alpha=alpha, eps=epsilon,
weight_decay=weight_decay, momentum=momentum, centered=centered)
model.parameters()
:需要更新的模型参数;lr
:学习率;alpha
:平滑常数(可选),用于计算梯度平方的移动平均。通常设置为0.99;eps
:数值稳定性项(可选),用于防止除以零。通常设置为一个很小的值,如1e-8;weight_decay
:权重衰减(可选),用于对模型参数进行L2正则化;momentum
:动量(可选),增加了在更新中考虑先前步骤的方向。通常设置为0;centered
:指示是否使用中心化的RMSprop(可选)。如果设置为True,它会计算中心化的梯度方差;- 注:Adagrad 和 RMSprop 的学习率调整方式非常相似,它们都是基于梯度平方的累积信息来计算学习率;但 Adagrad 使用累积和,而 RMSprop 使用指数加权移动平均(使用 β \beta β 来控制);
🎨 Adam (Adaptive Moment Estimation)
- 介绍:Adam是一种自适应学习率的优化算法,结合了动量和自适应学习率的特性;它使用指数移动平均来估计梯度的一阶矩和二阶矩,并使用这些估计来更新参数;(2014年)
- 公式:
m = β 1 ⋅ m + ( 1 − β 1 ) ⋅ ∇ θ t m = \beta_1 \cdot m + (1 - \beta_1) \cdot \nabla_{\theta_t} m=β1⋅m+(1−β1)⋅∇θt
v = β 2 ⋅ v + ( 1 − β 2 ) ⋅ ∇ θ t 2 v = \beta_2 \cdot v + (1 - \beta_2) \cdot {\nabla_{\theta_t}}^2 v=β2⋅v+(1−β2)⋅∇θt2
m ^ = m 1 − β 1 t \hat{m} = \frac{m}{1 - \beta_1^t} m^=1−β1tm
v ^ = v 1 − β 2 t \hat{v} = \frac{v}{1 - \beta_2^t} v^=1−β2tv
θ t + 1 = θ t − lr ⋅ m ^ v ^ + ϵ \theta_{t+1} = \theta_t - \text{lr} \cdot \frac{\hat{m}}{\sqrt{\hat{v}} + \epsilon} θt+1=θt−lr⋅v^+ϵm^ - 其中, β 1 和 β 2 \beta_1 \text{和} \beta_2 β1和β2是一阶矩估计和二阶矩估计的指数衰减率, ∇ θ t \nabla_{\theta_t} ∇θt是对模型参数 θ t \theta_t θt 的梯度;
- m 和 v m \text{和} v m和v是一阶矩估计和二阶矩估计的变量, m ^ 和 v ^ \hat{m} \text{和} \hat{v} m^和v^是对偏差校正后的一阶矩估计和二阶矩估计的变量;
- lr \text{lr} lr是学习率, ϵ \epsilon ϵ是用于数值稳定性的小常数, θ t + 1 和 θ t \theta_{t+1} \text{和} \theta_t θt+1和θt是模型参数的更新和当前值, t t t 是迭代次数 ;
- 优点:自适应调整学习率,适用于大多数问题;具有较快的收敛速度;能够处理稀疏梯度和非平稳目标函数;
- 缺点:对于某些问题可能过度依赖一阶矩和二阶矩估计;对学习率的选择比较敏感;内存使用较高;
- 用法:
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate, betas=(beta1, beta2),
eps=epsilon, weight_decay=weight_decay)
model.parameters()
:需要更新的模型参数;lr
:学习率;betas
:动量项的衰减系数(可选),一阶矩和二阶矩的权重。通常设置为(0.9, 0.999);eps
:数值稳定性项(可选),用于防止除以零。通常设置为一个很小的值,如1e-8;weight_decay
:权重衰减(可选),用于对模型参数进行L2正则化;
🎨 AdamW(Adam with Weight Decay)
- 介绍:AdamW是Adam算法的一种改进版本,通过引入权重衰减(weight decay)来解决参数正则化问题;与Adam不同,AdamW在权重衰减项的计算中不使用偏差校正;这样在参数更新的过程中,既考虑了梯度信息,又考虑了权重衰减的影响;(2017年)
- 公式:(参考 Adam )
θ t + 1 = θ t − lr ⋅ m ^ v ^ + ϵ − (lr ⋅ weight_decay ⋅ θ t ) \theta_{t+1} = \theta_t - \text{lr} \cdot \frac{\hat{m}}{\sqrt{\hat{v}} + \epsilon} - (\text{lr} \cdot \text{weight\_decay} \cdot \theta_t) θt+1=θt−lr⋅v^+ϵm^−(lr⋅weight_decay⋅θt) - weight_decay \text{weight\_decay} weight_decay 是权重衰减系数,用于控制权重衰减的强度;
- 优点:修正了Adam算法中权重衰减计算不准确的问题;能够更准确地控制模型参数的正则化程度;适用于各种深度学习任务;
- 缺点:在某些情况下可能导致性能下降;学习率和权重衰减的选择仍然需要仔细调整;
- 用法:
optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate, betas=(beta1, beta2),
eps=epsilon, weight_decay=weight_decay)
model.parameters()
:需要更新的模型参数;lr
:学习率;betas
:动量项的衰减系数(可选),一阶矩和二阶矩的权重。通常设置为(0.9, 0.999);eps
:数值稳定性项(可选),用于防止除以零。通常设置为一个很小的值,如1e-8;weight_decay
:权重衰减,用于对模型参数进行L2正则化;- 注:与 Adam 对比,在 AdamW 中权重衰减被直接纳入到参数更新公式中,而不是作为一个独立的超参数;这种方式可以更准确地控制权重衰减的影响,并避免了权重衰减对梯度的不合理抑制;
🚀 学习率(Learning Rate Scheduler)
🎨 StepLR
- 介绍:StepLR函数根据给定的步数来衰减学习率;
- 公式:
l
r
=
l
r
0
×
γ
⌊
num_epochs
step_size
⌋
lr = lr_0 \times \gamma^{\left\lfloor \frac{\text{num\_epochs}}{\text{step\_size}} \right\rfloor}
lr=lr0×γ⌊step_sizenum_epochs⌋
(if epoch % step_size == 0)
- 用法:
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.5)
optimizer
:优化器对象;step_size
:学习率衰减的步数;gamma
:学习率衰减的乘法因子;
🎨 MultiStepLR
- 介绍:MultiStepLR函数在指定的里程碑(milestones)处衰减学习率;
- 公式:
l
r
=
l
r
0
×
g
a
m
m
a
i
d
x
lr = lr_0 \times gamma^{idx}
lr=lr0×gammaidx
(if epoch in milestones[idx])
- 用法:
scheduler = torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones=[50, 80, 90], gamma=0.1)
optimizer
:优化器对象;milestones
:里程碑列表,表示在哪些步数下衰减学习率;gamma
:学习率衰减的乘法因子;
🎨 ExponentialLR
- 介绍:ExponentialLR函数按指数方式衰减学习率;
- 公式: l r = l r ∗ g a m m a e p o c h lr = lr * gamma^{epoch} lr=lr∗gammaepoch
- 用法:
scheduler = torch.optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.9)
optimizer
:优化器对象;gamma
:学习率衰减的乘法因子,较大的值会使学习率下降得更快,而较小的值则会使学习率下降得更慢;
🎨 CosineAnnealingLR
- 介绍:CosineAnnealingLR函数使用余弦退火算法以余弦函数的形式在每个周期内逐渐降低学习率;
- 公式: l r = l r m a x ∗ 0.5 ∗ ( 1 + c o s ( e p o c h / T m a x ∗ π ) ) lr = lr_{max} * 0.5 * (1 + cos(epoch / T_{max} * \pi)) lr=lrmax∗0.5∗(1+cos(epoch/Tmax∗π))
- 用法:
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=num_epochs//3, eta_min=0)
optimizer
:优化器对象;T_max
:学习率下降的周期长度,较小的值会使学习率快速下降,而较大的值会使学习率缓慢下降;eta_min
:学习率的最小值,当学习率达到最小值后,余弦退火算法将不再改变学习率;
🎨 CosineAnnealingWarmRestarts
- 介绍:它根据余弦退火函数调整学习率的值,与标准的余弦退火
CosineAnnealingLR
不同的是,它在退火周期内进行了额外的重启操作,以增加模型的鲁棒性和训练效果; - 公式: l r = l r m i n + 0.5 ∗ ( l r m a x − l r m i n ) ∗ ( 1 + c o s ( e p o c h / T ) ) lr = lr_{min} + 0.5 * (lr_{max} - lr_{min}) * (1 + cos(epoch / T)) lr=lrmin+0.5∗(lrmax−lrmin)∗(1+cos(epoch/T))
- 用法:
scheduler = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=10, T_mult=2, eta_min=0)
optimizer
:优化器对象;T_0
:周期长度的初始值;T_mult
:每次重启后周期长度的倍数,即 T = T _ 0 ∗ T _ m u l t T = T\_0 * T\_mult T=T_0∗T_mult;eta_min
:学习率的最小值,当学习率达到最小值后,余弦退火算法将不再改变学习率;
🎨 ReduceLROnPlateau
- 介绍:ReduceLROnPlateau函数在验证损失不再下降时按照一定的因子减小学习率;
- 公式: l r n e w = l r o l d ∗ f a c t o r lr_{new} = lr_{old} * factor lrnew=lrold∗factor
- 用法:
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=10)
optimizer
:优化器对象;mode
:监测指标的模式,可以是’min’或’max’;factor
:学习率衰减的乘法因子;patience
:忍受没有改善的epoch数量;
🚀 混合精度(GradScaler)
🎨 介绍
2017年,NVIDIA研究了一种用于混合精度训练的方法,在训练网络时将单精度(FP32)与半精度(FP16)结合在一起,并使用相同的超参数实现了与FP32几乎相同的结果;
单精度 (FP32)
的 Tensor 格式:torch.float32 ;半精度 (FP16)
的 Tensor 格式:torch.float16 ;
使用单精度 (FP32)主要问题:
- 模型尺寸大对显存要求高;
- 模型训练速度慢;
- 模型推理速度慢;
🎨 代码
from tqdm import tqdm
from torch.cuda.amp import GradScaler
scaler = GradScaler(enabled=True)
for imgs, labels in tqdm(dataloader):
imgs = imgs.to(device)
labels = labels.to(device)
optimizer.zero_grad()
with autocast():
pre_labels = model(imgs)
loss = criterion(pre_labels, labels)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
🚀 梯度累加(Add Grad)
🎨 介绍
batch_size 因显存太小的原因导致只能设置为较小值,可以采取梯度累加的方法解决 ;
采用参数延迟更新的方法,可以实现与之前大 batch_size 相近的实验效果 ;
🎨 代码
from tqdm import tqdm
add_steps = 2
for idx, (imgs, labels) in enumerate(tqdm(train_loader)):
imgs = imgs.to(device)
labels = labels.to(device)
pre_labels = model(images)
loss = criterion(pre_labels, labels)
loss = loss / add_steps
loss.backward()
# update gradient
if((idx + 1) % add_steps) == 0:
optimizer.step()
optimizer.zero_grad()
附录一:学习率绘制
import torch
import torch.optim as optim
from torchvision import models
import matplotlib as mlp
import matplotlib.pyplot as plt
mlp.use('TkAgg')
# model
model = models.resnet50(pretrained=True)
# optimizer
optimizer = optim.SGD(model.parameters(), lr=0.1)
# scheduler
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.5)
# lr log list
lr_values = []
num_epochs = 100
# train
for epoch in range(num_epochs):
# lr log epoch
lr_values.append(optimizer.param_groups[0]['lr'])
# model training ...
# update lr
scheduler.step()
# plot epoch-lr
epoch_values = list(range(num_epochs))
plt.plot(epoch_values, lr_values)
plt.xlabel('Epoch')
plt.ylabel('Learning Rate')
plt.title('Learning Rate Schedule')
plt.show()
附录二:完整程序
import torch
import torch.nn as nn
import torch.optim as optim
import torch.optim.lr_scheduler as lr_scheduler
import torchvision.transforms as transforms
import torchvision.datasets as datasets
from torchvision.models import resnet50
from torch.cuda.amp import autocast, GradScaler
def main():
random.seed(42)
np.random.seed(42)
torch.manual_seed(42)
torch.cuda.manual_seed(42)
torch.cuda.manual_seed_all(42)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
# transform
transform_train = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
transform_val = transforms.Compose([
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
# dataloader
train_dataset = datasets.ImageNet(root='path/to/imagenet/train', transform=transform_train)
test_dataset = datasets.ImageNet(root='path/to/imagenet/val', transform=transform_val)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=128, shuffle=True, num_workers=8)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=128, shuffle=False, num_workers=8)
# model
model = resnet50(pretrained=False)
model.fc = nn.Linear(2048, 1000)
optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=1e-4)
scheduler = lr_scheduler.CosineAnnealingLR(optimizer, T_max=10)
criterion = nn.CrossEntropyLoss()
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
criterion = criterion.to(device)
scaler = GradScaler(enabled=True)
best_test_loss = float('inf')
best_test_acc = float('inf')
for epoch in range(10):
train_loss, train_acc = train(model, train_loader, criterion, optimizer, scaler, device)
test_loss, test_acc = test(model, test_loader, criterion, device)
print('lr: ', optimizer.param_groups[0]['lr'])
scheduler.step()
if test_acc < best_test_acc:
best_test_acc = test_acc
best_test_loss = test_loss
print('Epoch {}'.format(epoch + 1))
print('Train Loss: {:.4f} | Train Acc: {:.2f}%'.format(train_loss, train_acc))
print('Test Loss: {:.4f} | Test Acc: {:.2f}%'.format(test_loss, test_acc))
print('Best Test Loss: {:.4f} | Best Test Acc: {:.4f}'.format(best_test_loss, best_test_loss))
def train(model, loader, criterion, optimizer, scaler, device):
model.train()
running_loss = 0.0
correct = 0
total = 0
for inputs, targets in loader:
inputs, targets = inputs.to(device), targets.to(device)
optimizer.zero_grad()
with autocast():
outputs = model(inputs)
loss = criterion(outputs, targets)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
running_loss += loss.item()
_, predicted = outputs.max(1)
total += targets.size(0)
correct += predicted.eq(targets).sum().item()
train_loss = running_loss / len(loader)
train_acc = 100.0 * correct / total
return train_loss, train_acc
def test(model, loader, criterion, device):
model.eval()
running_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for inputs, targets in loader:
inputs, targets = inputs.to(device), targets.to(device)
outputs = model(inputs)
loss = criterion(outputs, targets)
running_loss += loss.item()
_, predicted = outputs.max(1)
total += targets.size(0)
correct += predicted.eq(targets).sum().item()
test_loss = running_loss / len(loader)
test_acc = 100.0 * correct / total
return test_loss, test_acc
if __name__ == '__main__':
main()