提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章链接
https://arxiv.org/pdf/1711.05769
摘要
本博客介绍了论文《PackNet: Adding Multiple Tasks to a Single Network by Iterative Pruning》针对多个任务进行持续学习中所遇到的灾难性遗忘问题,提出了一种基于网络剪枝技术创建自由参数,并将多个任务添加到单个深度神经网络的方法PackNet。博客首先介绍了PackNet如何对任务进行网络剪枝和训练,然后介绍了研究者们对各种网络架构和大规模数据集进行了广泛的实验,与提出的PackNet方法来进行对。结果表明比PackNet以前的工作更好的针对灾难性遗忘的鲁棒性。
Abstract
This blog introduces the paper “PackNet: Adding Multiple Tasks to a Single Network by Iterative Pruning”, which aims at the catastrophic forgetting problem encountered in the continuous learning of multiple tasks, and proposes a method to create free parameters based on network pruning technology and add multiple tasks to a single deep neural network PackNet. The blog first introduces how PackNet prunes and trains the network for tasks, and then introduces the extensive experiments that researchers have conducted on various network architectures and large-scale datasets to match with the proposed PackNet method. The results show better robustness to catastrophic forgetting than PackNet’s previous work.
一、方法介绍
1.训练过程
研究者所提出的PackNet方法的基本思想是使用网络剪枝枝术来创建自由参数,然后可以将其用于学习新任务,而无需添加额外的网络容量。在添加新任务时,采用权重 - 幅度 - 基于的剪枝方法,对网络各层的合格权重按一定比例进行一次性剪枝(论文中提到增量剪枝效果更好但为简单采用一次性剪枝),将权重绝对值最小的部分设为零。剪枝后重新训练网络以恢复精度,并且在训练新任务时,只允许被剪枝的权重更新,属于先前任务的权重保持固定,不修改先前任务的权重。在推理阶段,根据所选任务对网络参数进行掩码操作,使网络状态与训练时一致,且无额外运行时开销。具体过程如下图所示:
任务 I 的网络初始训练学习密集滤波器,如 (a) 所示。 经过 60% 的修剪并重新训练后,获得了任务 I 的稀疏滤波器。如 (b) 所示,其中白色圆圈表示 0 值权重。 为任务 I 保留的权重在该方法的其余部分中保持固定,并且不符合进一步修剪的条件。任务 I 和新任务 II进行对接,因为研究者允许更新任务 II 的剪枝权重,便产生了过滤器 ©,它共享为任务 I 学习的权重。另一轮 33% 的剪枝和重新训练生成的过滤器 (d), 这是用于评估任务 II 的过滤器。此后,任务 II 的权重(以橙色表示)保持固定。若还有新的任务,则需要再次修剪网络,释放一些仅用于任务 II 的参数,并重新训练任务 II 以从修剪中恢复, 这便生成了如 (d) 所示的滤波器。 从此时开始,任务 I 和 II 的权重保持固定。 然后,使用可用的修剪参数来学习另一个新任务,从而产生如 (e) 所示的绿色权重。重复此过程,直到添加了所有必需的任务或没有更多可用的自由参数。 在实验中,剪枝和重新训练比简单的微调长约 1.5 倍,因为研究者会重新训练一半的训练周期。
2.剪枝程序和推理
在每一轮剪枝中,研究者从每个卷积层和全连接层中删除固定百分比的合格权重。 层中的权重按其绝对大小排序,并选择最低的 50% 或 75% 进行删除。从图 中的过滤器 © 到 (d),仅修剪属于任务 II 的橙色权重,而任务 I 的灰色权重保持固定。 这可确保添加新任务时先前任务的性能不会发生变化。
在使用批量归一化的网络中,在第一轮修剪和重新训练之后,不会更新参数(增益、偏差)或运行平均值(均值、方差), 这种选择有助于减少额外的每任务开销。
添加多个任务的唯一开销是存储稀疏掩码,指示哪些参数对于特定任务是活动的。 通过遵循迭代训练过程,对于特定任务 K,获得一个过滤器,该过滤器是为该特定任务学习的权重和为所有先前任务 1,···,K − 1 学习的权重的叠加。
二、实验分析
1.实验代码
# this code is modified from the pytorch example code: https://github.com/pytorch/examples/blob/master/imagenet/main.py
# after the model is trained, you might use convert_model.py to remove the data parallel module to make the model as standalone weight.
#
# Bolei Zhou
import argparse
import os
import shutil
import time
import torch
import torch.nn as nn
import torch.nn.parallel
import torch.backends.cudnn as cudnn
import torch.optim
import torch.utils.data
import torchvision.transforms as transforms
import torchvision.datasets as datasets
import torchvision.models as models
import wideresnet
import pdb
# 获取所有可用的模型名称(仅包括小写字母且不是以"__"开头的模型)
model_names = sorted(name for name in models.__dict__
if name.islower() and not name.startswith("__")
and callable(models.__dict__[name]))
# 创建 ArgumentParser 对象,用于处理命令行输入的参数
parser = argparse.ArgumentParser(description='PyTorch Places365 Training')
parser.add_argument('data', metavar='DIR',
help='path to dataset')# 定义数据集路径参数
parser.add_argument('--arch', '-a', metavar='ARCH', default='resnet18',
help='model architecture: ' +
' | '.join(model_names) +
' (default: resnet18)')# 定义模型架构参数,默认使用 resnet18
parser.add_argument('-j', '--workers', default=6, type=int, metavar='N',
help='number of data loading workers (default: 4)')# 定义数据加载线程数(默认为 6)
parser.add_argument('--epochs', default=90, type=int, metavar='N',
help='number of total epochs to run')#训练的总轮次,默认为 90
parser.add_argument('--start-epoch', default=0, type=int, metavar='N',
help='manual epoch number (useful on restarts)')
parser.add_argument('-b', '--batch-size', default=256, type=int,
metavar='N', help='mini-batch size (default: 256)')# 定义每次训练的批量大小,默认为 256
# 定义初始学习率,默认为 0.1
parser.add_argument('--lr', '--learning-rate', default=0.1, type=float,
metavar='LR', help='initial learning rate')
parser.add_argument('--momentum', default=0.9, type=float, metavar='M',
help='momentum')# 动量默认为 0.9
parser.add_argument('--weight-decay', '--wd', default=1e-4, type=float,
metavar='W', help='weight decay (default: 1e-4)')# 权重衰减(L2 正则化),默认为 1e-4
# 定义打印频率(每多少个 batch 打印一次)
parser.add_argument('--print-freq', '-p', default=10, type=int,
metavar='N', help='print frequency (default: 10)')
# 定义检查点路径,用于恢复训练(如果需要的话)
parser.add_argument('--resume', default='', type=str, metavar='PATH',
help='path to latest checkpoint (default: none)')
# 定义是否仅进行模型评估(如果设置此选项,则不会进行训练)
parser.add_argument('-e', '--evaluate', dest='evaluate', action='store_true',
help='evaluate model on validation set')
# 定义是否使用预训练模型(默认为 False)
parser.add_argument('--pretrained', dest='pretrained', action='store_false',
help='use pre-trained model')
# 定义模型的类别数,默认为 365(适用于 Places365 数据集)
parser.add_argument('--num_classes', default=365, type=int, help='num of class in the model')
# 定义使用的数据集,默认为 'places365'
parser.add_argument('--dataset', default='places365', help='which dataset to train')
best_prec1 = 0
def main():
global args, best_prec1
args = parser.parse_args()
print(args)
# 创建模型
print("=> creating model '{}'".format(args.arch))
if args.arch.lower().startswith('wideresnet'):
# 如果是WideResNet模型,使用自定义的ResNet50架构,最后特征图大小为14x14,便于类激活映射
model = wideresnet.resnet50(num_classes=args.num_classes)
else:
model = models.__dict__[args.arch](num_classes=args.num_classes)
if args.arch.lower().startswith('alexnet') or args.arch.lower().startswith('vgg'):
model.features = torch.nn.DataParallel(model.features)
model.cuda()
else:
model = torch.nn.DataParallel(model).cuda()
print(model)
# 从检查点恢复模型
if args.resume:
if os.path.isfile(args.resume):
print("=> loading checkpoint '{}'".format(args.resume))
checkpoint = torch.load(args.resume)
args.start_epoch = checkpoint['epoch']
best_prec1 = checkpoint['best_prec1']
model.load_state_dict(checkpoint['state_dict'])
print("=> loaded checkpoint '{}' (epoch {})"
.format(args.resume, checkpoint['epoch']))
else:
print("=> no checkpoint found at '{}'".format(args.resume))
# 开启cudnn的优化,加速卷积运算
cudnn.benchmark = True
# 数据加载
traindir = os.path.join(args.data, 'train')
valdir = os.path.join(args.data, 'val')
normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
# 训练数据加载器
train_loader = torch.utils.data.DataLoader(
datasets.ImageFolder(traindir, transforms.Compose([
transforms.RandomSizedCrop(224), # 随机裁剪224x224的图像
transforms.RandomHorizontalFlip(), # 随机水平翻转
transforms.ToTensor(), # 将图像转换为Tensor
normalize, # 标准化
])),
batch_size=args.batch_size, shuffle=True,
num_workers=args.workers, pin_memory=True)
# 验证数据加载器
val_loader = torch.utils.data.DataLoader(
datasets.ImageFolder(valdir, transforms.Compose([
transforms.Scale(256), # 将图像缩放到256
transforms.CenterCrop(224), # 中心裁剪224x224的图像
transforms.ToTensor(), # 将图像转换为Tensor
normalize, # 标准化
])),
batch_size=args.batch_size, shuffle=False,
num_workers=args.workers, pin_memory=True)
# 定义损失函数(交叉熵损失)
criterion = nn.CrossEntropyLoss().cuda()
# 定义优化器(使用SGD)
optimizer = torch.optim.SGD(model.parameters(), args.lr,
momentum=args.momentum,
weight_decay=args.weight_decay)
# 如果只是进行评估,不进行训练
if args.evaluate:
validate(val_loader, model, criterion)
return
# 训练和验证过程
for epoch in range(args.start_epoch, args.epochs):
# 调整学习率
adjust_learning_rate(optimizer, epoch)
# 训练一个epoch
train(train_loader, model, criterion, optimizer, epoch)
# 在验证集上评估模型
prec1 = validate(val_loader, model, criterion)
# 如果当前的准确率更高,保存检查点
is_best = prec1 > best_prec1
best_prec1 = max(prec1, best_prec1)
save_checkpoint({
'epoch': epoch + 1,
'arch': args.arch,
'state_dict': model.state_dict(), # 保存模型状态字典
'best_prec1': best_prec1,
}, is_best, args.arch.lower()) # 保存检查点,如果是最佳模型的话
def train(train_loader, model, criterion, optimizer, epoch):
# 创建用于记录训练过程中的指标的对象
batch_time = AverageMeter() # 记录每个batch的时间
data_time = AverageMeter() # 记录数据加载时间
losses = AverageMeter() # 记录损失
top1 = AverageMeter() # 记录Top-1准确率
top5 = AverageMeter() # 记录Top-5准确率
# 切换到训练模式
model.train()
end = time.time() # 记录开始时间
for i, (input, target) in enumerate(train_loader):
# 计算数据加载时间
data_time.update(time.time() - end)
target = target.cuda(async=True) # 将目标标签转移到GPU上
input_var = torch.autograd.Variable(input) # 将输入数据转为Variable对象
target_var = torch.autograd.Variable(target) # 将目标标签转为Variable对象
# 计算模型输出
output = model(input_var)
loss = criterion(output, target_var) # 计算损失值
# 计算Top-1和Top-5准确率,并更新相关指标
prec1, prec5 = accuracy(output.data, target, topk=(1, 5))
losses.update(loss.data[0], input.size(0)) # 更新损失的统计信息
top1.update(prec1[0], input.size(0)) # 更新Top-1准确率的统计信息
top5.update(prec5[0], input.size(0)) # 更新Top-5准确率的统计信息
# 计算梯度并进行SGD优化步骤
optimizer.zero_grad() # 清空先前的梯度
loss.backward() # 反向传播计算梯度
optimizer.step() # 更新参数
# 计算并更新每个batch的时间
batch_time.update(time.time() - end)
end = time.time()
# 每隔一定的迭代次数打印一次训练进度
if i % args.print_freq == 0:
print('Epoch: [{0}][{1}/{2}]\t'
'Time {batch_time.val:.3f} ({batch_time.avg:.3f})\t'
'Data {data_time.val:.3f} ({data_time.avg:.3f})\t'
'Loss {loss.val:.4f} ({loss.avg:.4f})\t'
'Prec@1 {top1.val:.3f} ({top1.avg:.3f})\t'
'Prec@5 {top5.val:.3f} ({top5.avg:.3f})'.format(
epoch, i, len(train_loader), batch_time=batch_time,
data_time=data_time, loss=losses, top1=top1, top5=top5))
def validate(val_loader, model, criterion):
# 初始化一些用于统计的指标:时间、损失、top1准确率、top5准确率
batch_time = AverageMeter() # 记录每个批次的时间
losses = AverageMeter() # 记录每个批次的损失
top1 = AverageMeter() # 记录top1准确率
top5 = AverageMeter() # 记录top5准确率
# 切换到评估模式(禁用dropout、batch norm等)
model.eval()
# 记录开始时间
end = time.time()
# 遍历验证集中的每个批次
for i, (input, target) in enumerate(val_loader):
# 将目标标签移到GPU上
target = target.cuda(async=True)
# 将输入和目标转化为torch.autograd.Variable,并设置volatile=True(表示不需要计算梯度)
input_var = torch.autograd.Variable(input, volatile=True)
target_var = torch.autograd.Variable(target, volatile=True)
# 计算模型输出
output = model(input_var)
# 计算损失值
loss = criterion(output, target_var)
# 计算准确率(top1 和 top5)
prec1, prec5 = accuracy(output.data, target, topk=(1, 5))
# 更新损失值
losses.update(loss.data[0], input.size(0))
# 更新top1准确率
top1.update(prec1[0], input.size(0))
# 更新top5准确率
top5.update(prec5[0], input.size(0))
# 计算每个批次的耗时
batch_time.update(time.time() - end)
end = time.time()
# 每隔一定批次打印一次当前进度和统计信息
if i % args.print_freq == 0:
print('Test: [{0}/{1}]\t'
'Time {batch_time.val:.3f} ({batch_time.avg:.3f})\t'
'Loss {loss.val:.4f} ({loss.avg:.4f})\t'
'Prec@1 {top1.val:.3f} ({top1.avg:.3f})\t'
'Prec@5 {top5.val:.3f} ({top5.avg:.3f})'.format(
i, len(val_loader), batch_time=batch_time, loss=losses,
top1=top1, top5=top5))
# 打印最终的top1和top5准确率
print(' * Prec@1 {top1.avg:.3f} Prec@5 {top5.avg:.3f}'
.format(top1=top1, top5=top5))
return top1.avg
def save_checkpoint(state, is_best, filename='checkpoint.pth.tar'):
torch.save(state, filename + '_latest.pth.tar')
if is_best:
shutil.copyfile(filename + '_latest.pth.tar', filename + '_best.pth.tar')
class AverageMeter(object):
"""Computes and stores the average and current value"""
def __init__(self):
self.reset()
def reset(self):
self.val = 0
self.avg = 0
self.sum = 0
self.count = 0
def update(self, val, n=1):
self.val = val
self.sum += val * n
self.count += n
self.avg = self.sum / self.count
def adjust_learning_rate(optimizer, epoch):
"""Sets the learning rate to the initial LR decayed by 10 every 30 epochs"""
lr = args.lr * (0.1 ** (epoch // 30))
for param_group in optimizer.param_groups:
param_group['lr'] = lr
def accuracy(output, target, topk=(1,)):
"""Computes the precision@k for the specified values of k"""
maxk = max(topk)
batch_size = target.size(0)
_, pred = output.topk(maxk, 1, True, True)
pred = pred.t()
correct = pred.eq(target.view(1, -1).expand_as(pred))
res = []
for k in topk:
correct_k = correct[:k].view(-1).float().sum(0)
res.append(correct_k.mul_(100.0 / batch_size))
return res
if __name__ == '__main__':
main()
研究者对网络所做的改变是为每个新任务添加一个新的输出层。 修剪初始 ImageNet 训练的网络后,在 ImageNet 数据集上对其进行了 10 个 epoch 的微调,学习率为 1e-3,在 5 个 epoch 后衰减了 10 倍。 为了添加细粒度数据集,研究者使用相同的初始学习率,在 10 个 epoch 后衰减,并总共训练 20 个 epoch。 对于较大的 Places365 数据集,研究者总共微调了 10 个时期,学习率在 5 个时期后衰减。 当网络在新任务训练后被修剪时,我们以 1e-4 的恒定学习率进一步微调网络 10 个时期。 我们在所有网络上使用 32 的批量大小和默认的 dropout 率。
2.数据集和训练设置
研究者在两个大规模图像数据集和三个细粒度分类数据集上评估我们的方法,如下图所示:
对于Stanford Cars和 CUBS 数据集,研究者从输入图像中裁剪出对象边界框,并将其大小调整为 224 × 224。对于其他数据集,研究者将输入图像大小调整为 256 × 256,并随机裁剪大小 224 × 224 作为输入。 对于所有数据集,执行左右翻转以进行数据增强。在实验中,所有的训练都是从ImageNet开始,因为拥有一组良好的起始参数至关重要。
修剪初始 ImageNet 训练的网络后,研究者在 ImageNet 数据集上对其进行了 10 个 epoch 的微调,学习率为 1e-3,在 5 个 epoch 后衰减了 10 倍。 为了添加细粒度数据集,使用相同的初始学习率,在 10 个 epoch 后衰减,并总共训练 20 个 epoch。 对于较大Places365 数据集,研究者总共微调了 10 个时期,学习率在 5 个时期后衰减。 当网络在新任务训练后被修剪时,以 1e-4 的恒定学习率进一步微调网络 10 个时期,在所有网络上使用批量大小 32 和默认的 dropout 率。
2.基线设置
第一个基线方法研究者采用的是Classifier Only,是从初始网络中提取 fc7 或预分类器特征,并仅为每个特定任务训练一个新的分类器,这意味着 ImageNet 上的性能保持不变。 为了训练每个新的分类器层,研究者使用 1e-3 的恒定学习率进行 20 个时期。
第二个基线称为Individual Networks,为每个任务训练单独的模型,通过将网络的所有资源专用于该单个任务来实现尽可能高的准确性。 为了获得单个细粒度任务的模型,从 ImageNet 训练的网络开始,并对相应任务进行总共 20 个时期的微调,学习率为 1e-3,在 10 个时期后衰减了 10 倍。
3.实验过程与结果
可以看到,通过比较“Classifier Only”和“Individual Networks”列,可以清楚地看到,通过允许较低的卷积层发生变化,细粒度任务受益匪浅,汽车和鸟类分类的 top-1 错误率从 56.42% 下降到 13.97% 、 和 分别从 36.76% 降至 22.57%。
通过对 ImageNet 训练的 VGG-16 网络进行 50% 和 75% 的剪枝和重新训练,top-1 误差从最初的 28.42% 分别略有增加到 29.33% 和 30.87%,top-5 误差略有增加 从9.61%到9.99%和10.93%。 当三个任务添加到 75% 修剪的初始网络中时,得到 CUBS、Stanford Cars 和 Flowers 只比个体网络最佳情况差 2.38%、1.78% 和 1.10%。 同时,与仅分类器基线相比,误差分别减少了 11.04%、30.41% 和 10.41%。从最初按较高比率修剪的网络开始,可以在细粒度任务上获得更好的性能,因为它为任务提供更多参数。 这尤其有助于具有挑战性的汽车分类,随着初始剪枝率从 50% 增加到 75%,将 top-1 错误从 18.08% 减少到 15.75%。
此外,PackNet在这四个数据集上的错误率均小于LWF方法,LwF 的方法中,旧任务的错误不断增加,而对于PackNet来说,错误保持不变。 LwF 旧任务精度的不可预测的变化是一个缺陷,特别是当想要保证特定水平的性能时。
最后,如表最后一行所示,基于剪枝的模型比每个任务训练单独的网络要小得多(595 MB vs 2,173 MB),并且仅比仅分类器基线大 33 MB。
随后,研究员添加了一个大型数据集作为对比来验证PackNet方法:
通过将比 ImageNet 更大的 Places365添加到经过 75% 修剪的 ImageNet 训练网络中,实现了 0.64% 以内的 top-1 错误和 0.10% 以内的 top-5 错误。 单独训练的网络。 相比之下,联合训练的基线获得的性能比 ImageNet 的单个网络差得多。
为了使实验结果更加具有普遍性,研究员便在其他结构更加复杂的大型网络上对PackeNet方法来进行检验,结果如下图所示:
更深的 ResNet 和 DenseNet 网络分别具有 50 层和 121 层,对于剪枝非常稳健,在 ImageNet 上分别仅损失 0.45% 和 0.04% 的 top-1 准确率。 ResNet 的 Top-5 错误增加了 0.05%,DenseNet 的 Top-5 错误减少了 0.13%。 在 Flowers 分类的情况下,PackNet的表现比单个网络更好,因为训练整个网络导致它过度拟合最小的 Flowers 数据集。
除了在一系列网络中获得良好的性能之外,基于剪枝的方法的另一个好处是,对于给定的任务,可以迭代地对网络进行少量剪枝,以便在当前任务准确性的损失和可以实现为后续任务提供自由参数。
此外,研究员观察到准确度随着训练顺序的变化而降低,如下图所示:
随着任务的不断添加,PackNet在CUBS上的误差率从24.23%一直提升到27.19%,当添加顺序从第一更改为第三时,误差平均增加 3%。这些发现表明,如果可以提前决定任务的顺序,则应首先添加最具挑战性或不相关的任务。
除了任务顺序,剪枝比率同样对于实验的结果有很大的影响,如下图所示:
在上图中,研究者测量了当任务首次添加到 50% 修剪的 VGG-16 网络时修剪和重新训练的效果。 考虑这种特定情况,以便将修剪的影响与上面讨论的训练顺序分开。 最后观察到,由于网络连接的突然变化,任务的错误在修剪(⋆标记)后立即增加。 然而,在重新训练后,错误会减少,甚至可能降至原始未剪枝错误以下。该图表明重新训练是必要的,尤其是当剪枝率很大时。
总结
在论文中,研究者所提出的基于网络剪枝技术创建自由参数,将多个任务打包到单个网络的方法PackNet方法允许修改网络的所有层并影响大量的过滤器和特征,以获得与每个任务的单独训练的网络相当的精度。 它不仅适用于相对“宽敞”的 VGG-16 架构,还适用于更紧凑的参数高效网络,例如 ResNets 和 DenseNets。在一些大型数据集的任务表现中,PackNet所表现出的性能与单独训练任务所创建的个体网络相差无几,并且PackNet方法有着相较于个体任务网络更低的存储消耗。在使用PackNet时需要注意尽量将重要任务或者不相关的任务优先选择来进行训练,避免由于剪枝而影响新加入任务的性能,并且当剪枝率特别大时,需要对任务进行重新训练。在未来的研究方向中,可以尝试在单个网络进行多任务学习的框架中将PackNet与类似于学习二进制权重网络的技术相结合以提高任务的准确度。