代码见:JordanAsh/boostresnet: A PyTorch implementation of BoostResNet
原始论文:Huang F, Ash J, Langford J, et al. Learning deep resnet blocks sequentially using boosting theory[C]//International Conference on Machine Learning. PMLR, 2018: 2058-2067.
代码解析及功能实现
这段代码实现了一个训练ResNet块并逐层训练的过程。这个过程结合了Boosting理论,通过逐层训练ResNet块来提高分类性能。
代码前期准备
import torch
import sys
import pickle
import os
import numpy as np
import torchfile
from torch import nn
from torch.autograd import Variable
import argparse
导入了必要的库,包括PyTorch、NumPy、系统操作、数据加载和命令行参数解析库。
解析命令行参数
parser = argparse.ArgumentParser()
parser.add_argument('--gammaFirst', default=0.5, help='initial gamma')
parser.add_argument('--checkEvery', default=10000, help='how frequently to check gamma requirement - 10k for cifar, 5k for svhn')
parser.add_argument('--data', default='SVHN.t7', help='load data')
parser.add_argument('--gammaThresh',default=-0.0001, help='gamma threshold to stop training layer')
parser.add_argument('--lr', default=0.001, help='learning rate')
parser.add_argument('--maxIters', default=10000, help='maximum iterations before stopping train layer')
parser.add_argument("--transform", help='do CIFAR-style image transformations?', action="store_true")
parser.add_argument('--printEvery', default=100, help='how frequently to print')
parser.add_argument('--batchSize', default=100, help='batch size')
parser.add_argument('--modelPath', default='model.pt', help='output model')
opt = parser.parse_args()
解析命令行参数,设置各种训练超参数,包括初始gamma值、检查gamma的频率、数据集路径、gamma阈值、学习率、最大迭代次数、是否进行数据变换、打印频率、批处理大小和模型保存路径。
加载数据
data = torchfile.load(opt.data) # load dataset in torch format (assuming already normalized)
Xtrain = data.Xtrain
Ytrain = data.Ytrain - 1
Xtest = data.Xtest
Ytest = data.Ytest - 1
cut = int(np.shape(Xtrain)[0] / opt.batchSize * opt.batchSize) # cut off a few samples for simplicity
nTrain = cut
Xtrain = Xtrain[:cut]
Ytrain = Ytrain[:cut]
cut = int(np.shape(Xtest)[0] / opt.batchSize * opt.batchSize)
Xtest = Xtest[:cut]
Ytest = Ytest[:cut]
nTest = cut
numClasses = 10
加载并处理数据集,将训练集和测试集按批处理大小进行裁剪,以确保数据大小是批处理大小的整数倍。
定义打印函数
def printer(print_arr):for v in print_arr: sys.stdout.write(str(v) + '\t')sys.stdout.write('\n')sys.stdout.flush()
定义一个打印函数,用于在控制台打印输出。
加载ResNet模型并构建块
import fbrn
model = fbrn.tmp
model.load_state_dict(torch.load('fbrn.pth'))
allBlocks = {}
allBlocks[0] = nn.Sequential(model[0], model[1], model[2])
for i in range(8): allBlocks[1 + i] = model[3][i]
for i in range(8): allBlocks[9 + i] = model[4][i]
for i in range(8): allBlocks[17+ i] = model[5][i]
criterion = nn.CrossEntropyLoss().cuda()
nFilters = 15; rounds = 25
加载预训练的ResNet模型,并将其分成多个块,分别存储在allBlocks
字典中。定义损失函数为交叉熵损失。
allBlocks[0]
包含model
的第 0 层到第 2 层,使用nn.Sequential
将这些层组合在一起。for i in range(8): allBlocks[1 + i] = model[3][i]
将model
的第 3 层的 8 个子层分别存储在allBlocks
的索引 1 到 8。for i in range(8): allBlocks[9 + i] = model[4][i]
将model
的第 4 层的 8 个子层分别存储在allBlocks
的索引 9 到 16。for i in range(8): allBlocks[17 + i] = model[5][i]
将model
的第 5 层的 8 个子层分别存储在allBlocks
的索引 17 到 24。
这样,allBlocks
字典就包含了模型的所有层,并且这些层被分割成了不同的块,每个块对应一个连续的索引范围。
分块的意义
这段代码将模型分为了三个主要的残差块(residual blocks):
- 第 0 块:
allBlocks[0]
包含模型的第 0 层到第 2 层。 - 第 1 块:
allBlocks[1]
到allBlocks[8]
包含模型的第 3 层的 8 个子层。 - 第 2 块:
allBlocks[9]
到allBlocks[16]
包含模型的第 4 层的 8 个子层。 - 第 3 块:
allBlocks[17]
到allBlocks[24]
包含模型的第 5 层的 8 个子层。
数据增强函数
def transform(X):tmp = np.zeros((np.shape(X)[0], 3, 38, 38))tmp[:, :, 2:34, 2:34] = Xfor i in range(np.shape(X)[0]):r1 = np.random.randint(4)r2 = np.random.randint(4)X[i] = tmp[i, :, r1 : r1 + 32, r2 : r2 + 32]if np.random.uniform() > .5:X[i] = X[i, :, :, ::-1]return X
定义数据增强函数,用于CIFAR样式的图像变换,进行随机裁剪和水平翻转。
模型评估函数
def getPerformance(net, X, Y, n):acc = 0.model.eval()Xoutput = np.zeros((X.shape[0], 10))for batch in range(int(X.shape[0] / opt.batchSize)):start = batch * opt.batchSize; stop = (batch + 1) * opt.batchSize - 1ints = np.linspace(start, stop, opt.batchSize).astype(int)data = Variable(torch.from_numpy(X[ints])).float().cuda()for i in range(n): data = allBlocks[i](data)output = net(data)acc += np.mean(torch.max(output, 1)[1].cpu().data.numpy() == Y[ints])Xoutput[ints] = output.cpu().data.numpy()acc /= (X.shape[0] / opt.batchSize)model.train()return acc, Xoutput
定义模型评估函数,用于在训练和测试数据集上计算模型的准确率。
初始化模型统计数据
a_previous = 0.0
a_current = -1.0
s = np.zeros((nTrain, numClasses))
cost = np.zeros((nTrain, numClasses))
Xoutput_previous = np.zeros((nTrain, numClasses))
Ybatch = np.zeros((opt.batchSize))
YbatchTest = np.zeros((opt.batchSize))
gamma_previous = opt.gammaFirst
totalIterations = 0; tries = 0
初始化一些变量,用于存储模型统计数据、损失、输出和其他辅助数据。
逐层训练模型
for n in range(rounds):gamma = -1Z = 0# create cost function for i in range(nTrain):localSum = 0for l in range(numClasses):if l != Ytrain[i]:cost[i][l] = np.exp(s[i][l] - s[i][int(Ytrain[i])])localSum += cost[i][l]cost[i][int(Ytrain[i])] = -1 * localSumZ += localSum# fetch the correct classification layersbk = allBlocks[n]ci = nn.Sequential(model[6], model[7], model[8])if n < 17: ci = nn.Sequential(allBlocks[17], ci)if n < 9: ci = nn.Sequential(allBlocks[9], ci)modelTmp = nn.Sequential(bk, ci, nn.Softmax(dim=0))modelTmp = modelTmp.cuda()optimizer = torch.optim.Adam(modelTmp.parameters(), lr=opt.lr) tries = 0XbatchTest = torch.zeros(opt.batchSize, nFilters, 32, 32)while (gamma < opt.gammaThresh and ((opt.checkEvery * tries) < opt.maxIters)):accTrain = 0; accTest = 0; err = 0;for batch in range(1, opt.checkEvery + 1):optimizer.zero_grad()# get batch of training samplesints = np.random.random_integers(np.shape(Xtrain)[0] - 1, size=(opt.batchSize))Xbatch = Xtrain[ints]Ybatch = Variable(torch.from_numpy(Ytrain[ints])).cuda().long()# do transformationsif opt.transform: Xbatch = transform(Xbatch)data = Variable(torch.from_numpy(Xbatch)).float().cuda()for i in range(n): data = allBlocks[i](data)# get gradientsoutput = modelTmp(data)loss = torch.exp(criterion(output, Ybatch))loss.backward()err += loss.data[0]# evaluate training accuracyoutput = modelTmp(data)accTrain += np.mean(torch.max(output, 1)[1].cpu().data.numpy() == Ytrain[ints])# get test accuracy model.eval()ints = np.random.random_integers(np.shape(Xtest)[0] - 1, size=(opt.batchSize))Xbatch = Xtest[ints]data = Variable(torch.from_numpy(Xbatch)).float().cuda()for i in range(n): data = allBlocks[i](data)output = modelTmp(data)accTest += np.mean(torch.max(output, 1)[1].cpu().data.numpy() == Ytest[ints])model.train()if batch % opt.printEvery == 0:accTrain /= opt.printEveryaccTest /= opt.printEveryerr /= opt.printEveryprinter([n, rounds, totalIterations + batch + (opt.checkEvery * tries), err, accTrain, accTest])accTrain = 0;accTest = 0; err = 0;optimizer.step()totalIterations += opt.checkEverytries += 1# get performance of new layera_current, Xoutput_previous = getPerformance(modelTmp, Xtrain, Ytrain, n + 1)gamma = (a_current - a_previous) / (1 - a_current)a_previous = a_currentprinter([n, gamma, opt.gammaThresh])if gamma < opt.gammaThresh:breakelse:s += np.log(Xoutput_previous)
逐层训练ResNet块,使用Boosting理论来增强分类性能。具体步骤包括:
- 计算损失函数。
- 加载对应的ResNet块。
- 进行优化迭代,计算训练和测试集上的准确率,并根据gamma值判断是否继续训练。
保存模型
torch.save(model.state_dict(), opt.modelPath)
保存训练好的模型。
核心代码
初始化部分
a_previous = 0.0
a_current = -1.0
s = np.zeros((nTrain, numClasses))
cost = np.zeros((nTrain, numClasses))
Xoutput_previous = np.zeros((nTrain, numClasses))
Ybatch = np.zeros((opt.batchSize))
YbatchTest = np.zeros((opt.batchSize))
gamma_previous = opt.gammaFirst
totalIterations = 0; tries = 0
这里初始化了一些变量,包括模型的统计信息、训练样本的预测输出、批量训练样本等。其中:
a_previous
和a_current
是当前和之前的提升系数。s
是一个保存每个训练样本在每个类别上的累积输出。cost
是样本的代价矩阵。Xoutput_previous
是上一轮的输出。gamma_previous
是上一轮的提升系数。totalIterations
和tries
用于记录总迭代次数和尝试次数。
1.初始化阶段(主循环)
for n in range(rounds):gamma = -1Z = 0
gamma = -1
和Z = 0
初始化了一些用于计算的变量。rounds
是算法的迭代轮数。在代码中设置为25,每轮迭代都会添加一个新的残差块,并调整模型参数。- 每一轮次训练前将
gamma
和Z
置零。
2.计算代价函数
for i in range(nTrain):localSum = 0for l in range(numClasses):if l != Ytrain[i]:cost[i][l] = np.exp(s[i][l] - s[i][int(Ytrain[i])])localSum += cost[i][l]cost[i][int(Ytrain[i])] = -1 * localSumZ += localSum
这个部分计算每个训练样本的代价函数,并将计算所有错误分类的代价,存储在 cost
数组中。
每个训练样本的成本通过计算每个类别的错误分类代价 cost
。如果样本 i
的真实类别为 Ytrain[i]
,则计算该样本在其他类别上的成本之和 localSum
,并将 localSum
赋值给该样本真实类别的成本(取负值)。
cost[i][l] = np.exp(s[i][l] - s[i][int(Ytrain[i])])
计算样本 i i i 在类别 l l l 上的代价。cost[i][int(Ytrain[i])] = -1 * localSum
将当前样本在其真实类别上的代价设为负的localSum
。Z
是所有样本的总代价。
3. 获取当前分类层
bk = allBlocks[n]ci = nn.Sequential(model[6], model[7], model[8])if n < 17: ci = nn.Sequential(allBlocks[17], ci)if n < 9: ci = nn.Sequential(allBlocks[9], ci)modelTmp = nn.Sequential(bk, ci, nn.Softmax(dim=0))modelTmp = modelTmp.cuda()
这段代码的作用是创建一个临时模型 modelTmp
,它由当前迭代的残差块 bk
和一些预定义的分类层 ci
组成。以下是具体解释:
bk
表示当前迭代的残差块。ci
表示后续的分类层,包含模型中的一些层。
根据当前迭代次数 n
,动态调整 ci
的层次结构:
- 如果当前迭代次数
n
小于 17,那么在分类层ci
前面加上第 17 块的残差块。 - 如果当前迭代次数
n
小于 9,那么在分类层ci
前面加上第 9 块的残差块。
这样的设计使得模型在不同的迭代次数 n
下,分类层 ci
的结构不同,逐步增加模型的复杂性。这个结构确保了模型能够逐层学习和调整,从而提高分类性能。
4.1 优化器设置
optimizer = torch.optim.Adam(modelTmp.parameters(), lr=opt.lr) tries = 0XbatchTest = torch.zeros(opt.batchSize, nFilters, 32, 32)
使用 Adam 优化器,并初始化 XbatchTest
用于测试。
Adam 优化器是什么?大概的数学原理是什么?是对应的梯度下降算法吗?还是什么其他的算法?
Adam(Adaptive Moment Estimation)是一种基于一阶梯度的优化算法,结合了动量和自适应学习率两个方法。它在深度学习中被广泛使用,因为它在处理稀疏梯度和非平稳目标上表现良好。
Adam的数学原理:
- 计算每个参数的梯度的一阶动量(平均值)和二阶动量(方差)。
- 对每个参数的更新使用动量和学习率的校正。
具体来说,Adam 优化器的步骤如下:
- 初始化动量和二阶动量。
- 在每次迭代中更新动量和二阶动量。
- 根据动量和二阶动量校正参数更新步长。
具体的更新公式为:
- m t = β 1 m t − 1 + ( 1 − β 1 ) g t m_t = \beta_1 m_{t-1} + (1 - \beta_1) g_t mt=β1mt−1+(1−β1)gt
- v t = β 2 v t − 1 + ( 1 − β 2 ) g t 2 v_t = \beta_2 v_{t-1} + (1 - \beta_2) g_t^2 vt=β2vt−1+(1−β2)gt2
- m ^ t = m t 1 − β 1 t \hat{m}_t = \frac{m_t}{1 - \beta_1^t} m^t=1−β1tmt
- v ^ t = v t 1 − β 2 t \hat{v}_t = \frac{v_t}{1 - \beta_2^t} v^t=1−β2tvt
- θ t = θ t − 1 − η m ^ t v ^ t + ϵ \theta_t = \theta_{t-1} - \eta \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon} θt=θt−1−ηv^t+ϵm^t
其中:
- g t g_t gt 是梯度。
- m t m_t mt 和 v t v_t vt 分别是一阶和二阶动量。
- β 1 \beta_1 β1 和 β 2 \beta_2 β2 是动量系数,通常取 0.9 0.9 0.9 和 0.999 0.999 0.999。
- ϵ \epsilon ϵ 是一个小常数,用于防止分母为零,通常取 1 0 − 8 10^{-8} 10−8。
- η \eta η 是学习率。
4.2 训练循环
while (gamma < opt.gammaThresh and ((opt.checkEvery * tries) < opt.maxIters)):accTrain = 0; accTest = 0; err = 0;for batch in range(1, opt.checkEvery+1):optimizer.zero_grad()
zero_grad()
:在每次反向传播之前,将所有参数的梯度缓存清零。在 PyTorch 中,梯度是累加的,所以需要在每次迭代开始前清零。
在满足 gamma
小于阈值和最大迭代次数限制的条件下,开始训练循环。
关于 opt.gammaThresh
和 gamma
这里的 opt.gammaThresh
对应算法中的 γ t \gamma_t γt,而 gamma
对应的是 γ \gamma γ。代码中的逻辑 while (gamma < opt.gammaThresh)
实际上在检查当前的 γ \gamma γ 是否小于阈值。这是因为我们希望 γ \gamma γ 足够大,代表模型的分类能力足够强,当 γ \gamma γ 小于阈值时,停止训练。
具体来说:
gamma
初始化为 -1,表示最差情况。- 当训练时,更新
gamma
的值,如果gamma
达到或超过opt.gammaThresh
,则说明模型性能达到预期,可以停止训练。
获取训练样本批次
ints = np.random.random_integers(np.shape(Xtrain)[0] - 1, size=(opt.batchSize))Xbatch = Xtrain[ints]Ybatch = Variable(torch.from_numpy(Ytrain[ints])).cuda().long()
从训练集中随机选择一个批次样本,并将其转换为 PyTorch 变量。
数据变换和前向传播
if opt.transform: Xbatch = transform(Xbatch)data = Variable(torch.from_numpy(Xbatch)).float().cuda()for i in range(n): data = allBlocks[i](data)
如果指定了数据变换,则对数据进行变换,然后进行前向传播,通过前 n
个残差块处理数据。
获取梯度和更新权重
output = modelTmp(data)loss = torch.exp(criterion(output, Ybatch))loss.backward()err += loss.data[0]output = modelTmp(data)accTrain += np.mean(torch.max(output,1)[1].cpu().data.numpy() == Ytrain[ints])model.eval()ints = np.random.random_integers(np.shape(Xtest)[0] - 1, size=(opt.batchSize))Xbatch = Xtest[ints]data = Variable(torch.from_numpy(Xbatch)).float().cuda()for i in range(n): data = allBlocks[i](data)output = modelTmp(data)accTest += np.mean(torch.max(output,1)[1].cpu().data.numpy() == Ytest[ints])model.train()
在训练过程中,通过交叉熵+指数损失函数计算并累积损失 err
criterion = nn.CrossEntropyLoss().cuda()
计算训练准确率 accTrain
和测试准确率 accTest
。
打印和梯度裁剪
if batch % opt.printEvery == 0:accTrain /= opt.printEveryaccTest /= opt.printEveryerr /= opt.printEveryprinter([n, rounds, totalIterations + batch + (opt.checkEvery * tries), err, accTrain, accTest])accTrain = 0; accTest = 0; err = 0;for p in modelTmp.parameters(): p.grad.data.clamp_(-.1, .1) optimizer.step()
- 每隔
opt.printEvery
次批量训练输出一次当前状态,包括错误率、训练准确率和测试准确率。 - 进行梯度裁剪,防止梯度爆炸。
step()
:执行一步优化算法,即根据当前的梯度和动量更新模型参数。
梯度裁剪对应的代码是:
for p in modelTmp.parameters(): p.grad.data.clamp_(-.1, .1)
这段代码通过限制梯度的值在 [-0.1, 0.1] 范围内,防止梯度爆炸。这对于稳定训练过程非常重要,尤其是深度神经网络,梯度可能会在反向传播过程中变得非常大。
计算 gamma
值
accTrain, Xoutput = getPerformance(modelTmp, Xtrain, Ytrain, n)gamma_current = -1 * np.sum(Xoutput * cost) / Zgamma = (gamma_current ** 2 - gamma_previous ** 2)/(1 - gamma_previous ** 2) if gamma > 0: gamma = np.sqrt(gamma)else: gamma = -1 * np.sqrt(-1 * gamma)a_current = 0.5 * np.log((1 + gamma_current) / (1 - gamma_current))
更新模型参数,并在每个批量训练后进行一次梯度下降。
- 计算当前模型的性能,得到
gamma_current
。 - 根据
gamma_current
和gamma_previous
更新gamma
。
公式:
γ t ← γ ~ t + 1 2 − γ ~ t 2 1 − γ ~ t 2 \gamma_t \leftarrow \sqrt{\frac{\tilde{\gamma}_{t+1}^2 - \tilde{\gamma}_t^2}{1 - \tilde{\gamma}_t^2}} γt←1−γ~t2γ~t+12−γ~t2
其中:
gamma_current
是当前训练轮次的 γ t \gamma_t γt。gamma_previous
是前一次训练轮次的 γ t \gamma_t γt。
为什么这样处理负数的情况?
当 γ \gamma γ 为负数时,我们使用以下公式进行更新:
γ = − 1 ∗ − 1 ∗ γ ~ t + 1 2 − γ ~ t 2 1 − γ ~ t 2 \gamma = -1 * \sqrt{-1 * \frac{\tilde{\gamma}_{t+1}^2 - \tilde{\gamma}_t^2}{1 - \tilde{\gamma}_t^2}} γ=−1∗−1∗1−γ~t2γ~t+12−γ~t2
这样处理的原因是:
- γ \gamma γ 的计算可能会因为 γ ~ t + 1 2 − γ ~ t 2 \tilde{\gamma}_{t+1}^2 - \tilde{\gamma}_t^2 γ~t+12−γ~t2 的值而导致根号内出现负数。这在数学上是不可接受的。
- 为了避免这种情况,取负号后再开方,以确保 γ \gamma γ 的计算结果是一个实数。负号处理后仍然保持 γ \gamma γ 的真实值,因为 γ \gamma γ 本身可以是负数或正数。
这个处理方式巧妙地避免了计算过程中根号内为负数的问题,同时保持了 γ \gamma γ 的实际含义。
尝试次数和权重更新
tries += 1if (gamma > opt.gammaThresh or ((opt.checkEvery * tries) >= opt.maxIters)):totalIterations = totalIterations + (tries * opt.checkEvery)printer([gamma, gamma_current, gamma_previous])printer(['a_{t+1}:', a_current, 'gamma_t:', gamma]) s += Xoutput * a_current - Xoutput_previous * a_previousaccTest, _ = getPerformance(modelTmp, Xtest, Ytest, n) printer(['t', rounds, 'numBatches:', tries * opt.checkEvery, 'test accuracy:', accTest]) gamma_previous = gamma_current
更新 tries
,如果 gamma
达到阈值或尝试次数达到最大迭代次数,则记录迭代次数并打印信息。最后更新累积输出 s
和 gamma_previous
。