嗨!朋友,带你来预测深度学习模型的性能
点击上方关注,All in AI中国
人们普遍认为,最近深度学习的成功很大程度上取决于大量数据的可用性。Vision是实现DL承诺的第一个领域,这可能是因为ImageNet等大型数据集的可用性。最近RL模拟器的激增进一步说明,随着我们进一步将这些技术应用到实际问题中,数据稀缺可能很快成为瓶颈。
但是有多少数据是足够的呢?
在商业环境中,这个问题经常出现。当时间和金钱处于紧要关头时,能够对模型体系架构的改进与收集更多的数据进行权衡的具体说明是有用的。我们应该为一个工程师团队支付6个月的时间来精细化我们的模型,还是应该向一个众包团队支付6个月的费用来整理我们所需要的数据呢?
我们不能轻易的去回答这个问题,这件事反映了深度学习作为一个领域的不成熟,这一缺陷导致Ali Rahimi(阿里•拉希米)在他2018年的NIPS演讲中宣称"机器学习已经变成炼金术"(至少在某种意义上,他错了;炼金术从来没有让任何人赚到钱,而深度学习确实让一些人变得非常的富有)。Yann LeCunn(杨立昆)曾在Facebook上发表了一篇广受关注的帖子,并在回应中提出了挑战:"如果你对我们对于你每天的使用方法的理解不满意,那么就去改正它吧"。
Ali Rahimi曾表达过"深度学习是'炼金术'"的观点。
来自百度的一篇题为《Deep Learning Scaling is Predictable, Empirically》的论文在一定程度上回答了这一挑战。正如书名所示,他们对这个问题的回答是实证的,而不是理论的。这篇论文还附有一篇优秀的博客文章,我希望您参考这篇文章,以便对研究结果进行更详细的讨论,我将在这里进行总结。
论文传送门:https://arxiv.org/abs/1712.00409
在我们深入研究这个问题之前,先来讲一个题外话:长期以来,尺度效应的研究一直吸引着生物学家。这张1947年Max Kleiber的图也显示了动物的代谢率(每天产生的热量)以双对数的函数方式与动物的体重成比例(更多见下文)。
事实上,它似乎是
这就是为什么红线比标记的面更陡的原因——标记的面更陡
但是却比标签上的权重要轻。有趣的是,没有人真的知道为什么这条定律会成立,尽管它看起来非常有道理。
回到百度和人工智能的世界,70年后,可能我们正在制作类似的情节:(可能我们也出现了类似的情节)
从本质上讲,在幂律分布相同的情况下,增加数据的纸质文档在测试集损失上产生下降,当以双对数标度绘制时,最终会成为一条直线。
让人值得注意的是,这种关系的指数——线性标度上直线的斜率——对于你所要解决的问题的任何架构来说都差不多。因此数据集本身定义了这个指数:模型只是移动了截距。要强调这一点:对于给定数据集的任何模型,添加更多数据的效果本质上是相同的,这很不寻常。
他们没有为这篇论文提供任何代码,所以我在PyTorch中汇总了一些实验来探究他们的结论。
代码
你可以在这里下载完整的Jupyter笔记本,或者继续阅读一些专家的文章。
我在PyTorch教程中提供的代码基础上生成了一个简单的CNN来测试CIFAR数据集(这是一个包含10个类的小型图像分类任务)。我用一个超参数字典来配置它,因为最佳的超参数对数据集的大小非常敏感-正如我们将要看到的,这对于复制百度结果是很重要的。
class Net(nn.Module): """ A simple 5 layer CNN, configurable by passing a hyperparameter dictionary at initialization. Based upon the one outlined in the Pytorch intro tutorial (http://pytorch.org/tutorials/beginner/blitz/neural_networks_tutorial.html#define-the-network) """ def __init__(self, hyperparam_dict=None): super(Net, self).__init__() if not hyperparam_dict : hyperparam_dict = self.standard_hyperparams() self.hyperparam_dict = hyperparam_dict self.conv1 = nn.Conv2d(3, hyperparam_dict['conv1_size'], 5) self.pool = nn.MaxPool2d(2, 2) self.conv2 = nn.Conv2d(hyperparam_dict['conv1_size'], hyperparam_dict['conv2_size'], 5) self.fc1 = nn.Linear(hyperparam_dict['conv2_size'] * 5 * 5, hyperparam_dict['fc1_size']) self.fc2 = nn.Linear(hyperparam_dict['fc1_size'], hyperparam_dict['fc2_size']) self.fc3 = nn.Linear(hyperparam_dict['fc2_size'], 10) def forward(self, x): x = self.pool(F.relu(self.conv1(x))) x = self.pool(F.relu(self.conv2(x))) x = x.view(-1, self.hyperparam_dict['conv2_size'] * 5 * 5) x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = self.fc3(x) return x def standard_hyperparams(self): hyperparam_dict = {} hyperparam_dict['conv1_size'] = 6 hyperparam_dict['conv2_size'] = 16 hyperparam_dict['fc1_size'] = 120 hyperparam_dict['fc2_size'] = 84 return hyperparam_dict
我将训练数据分成了训练集和验证集,并参照了论文中的建议对训练集进行二次抽样。
def get_dataset_size(start=0.5, end=100, base=2): """ Returns exponentially distributed dataset size vector""" dataset_size=[start] while True: dataset_size.append(dataset_size[-1]*base) if dataset_size[-1] > end: dataset_size[-1] = end break return dataset_size transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]) trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform) testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform) val_size = 0.2 num_train = len(trainset) indices = list(range(num_train)) split = int(np.floor(val_size * num_train)) np.random.shuffle(indices) train_idx, val_idx = indices[split:], indices[:split] total_train = len(train_idx) # For each of our train sets, we want a subset of the true train set dataset_size = np.array(get_dataset_size()) dataset_size /=100 # Convert to fraction of original dataset size train_set_samplers = dict() trainset_loaders = dict() for ts in dataset_size: train_set_samplers[ts]=np.random.choice(train_idx, int(ts*total_train)) trainset_loaders[ts]=torch.utils.data.DataLoader(trainset, batch_size=4, sampler=train_set_samplers[ts], num_workers=2) val_sampler = SubsetRandomSampler(val_idx) valloader = torch.utils.data.DataLoader(trainset, batch_size=4, sampler=val_sampler, num_workers=2) testloader = torch.utils.data.DataLoader(testset, batch_size=4, shuffle=False, num_workers=2)
然后,我训练了9个模型,每个模型对应一个数据集大小,通过连续增加3个周期的验证错误来定义一个停止条件(最初的论文对验证的细节有些模糊)。然后我根据测试集对它们进行了评估。
test_acc = {} val_acc = {} train_acc = {} test_loss = {} for train_size in dataset_size: print('Training with subset %1.4f, which is %d images'%(train_size, train_size*total_train)) net = Net() # Train model with an early stopping criterion - terminates after 4 epochs of non-improving val loss net, loss_list, val_list = train_model(net, trainset_loaders[train_size], valloader, 1000, n_epochs=10) test_accuracy, loss = test_model(net, testloader) print('Accuracy of the network on the 10000 test images: %d %%' % ( 100 * accuracy)) test_acc[train_size] = accuracy test_loss[train_size] = loss val_acc[train_size] = val_list train_acc[train_size] = loss_list torch.save(net, 'trainset_%1.2f_%d_images.model'%(train_size, train_size*total_train))
正如您所期望的那样,测试集的精准度随着训练集的大小而增加。而且,它看起来有点幂律。
损失也以类似的方式减少。
然而,无论是准确性还是损失的双对数的图看起来都不像百度论文中的那个那样可爱。事实上,它们各自都表现出某种模糊的对数形式,表明我们有次幂定律关系。
原因很明显:我没有像他们在每个训练集大小上那样进行着详尽的超参数的搜索。因此,我们并没有为每个数据集大小找到最佳的模型。最有可能的是,我们的模型缺乏完全捕获更大数据集的能力,我们没有充分利用这些数据。
添加超参数调整
您将会记得,在模型定义中,我们使用超参数字典设置了图层的大小,从而可以通过超参数的调优轻松地调整网络的形状。因此,对我们来说,实现一些随机搜索相对简单:
def random_hyperparamters(): """ Returns randomly drawn hyperparamters for our CNN" hyperparam_dict = {} hyperparam_dict['lr'] = 10 ** np.random.uniform(-6, -1) hyperparam_dict['weight_decay'] = 10 ** np.random.uniform(-6, -3) hyperparam_dict['momentum'] = 10 ** np.random.uniform(-1, 0) hyperparam_dict['conv1_size'] = int(np.random.uniform(10,100)) hyperparam_dict['conv2_size'] = int(np.random.uniform(10,100)) hyperparam_dict['fc1_size'] = int(np.random.uniform(30,200)) hyperparam_dict['fc2_size'] = int(np.random.uniform(30,200)) return hyperparam_dict
我们现在可以对每个数据集大小重复训练循环,随机抽样参数:
n_searches = 20 n_epochs = 15 n_val = 500 for train_size in dataset_size: print('Training with subset %1.4f, which is %d images'%(train_size, train_size*total_train)) test_acc[train_size] = [] test_loss[train_size] = [] val_acc[train_size] = [] train_acc[train_size] = [] # Perform random search for that dataset size for trial in range(n_searches): hyperparam_dict = random_hyperparamters() print(hyperparam_dict) net = Net(hyperparam_dict) net, loss_list, val_list = train_model(net, trainset_loaders[train_size], valloader, n_val, n_epochs=n_epochs, lr=hyperparam_dict['lr'], momentum=hyperparam_dict['momentum'], weight_decay=hyperparam_dict['weight_decay'] ) test_acc[train_size].append((hyperparam_dict, accuracy)) test_loss[train_size].append((hyperparam_dict, loss)) val_acc[train_size].append((hyperparam_dict, val_list)) train_acc[train_size].append((hyperparam_dict, loss_list)) torch.save(net, 'trainset_%d_images_trial%d_val_loss_%1.2f.model'% ((train_size*total_train), trial, val_list[-1])) torch.save(hyperparam_dict, 'trainset_%d_images_trial%d_val_loss_%1.2f.hparams'%((train_size*total_train), trial, val_list[-1]))
并使用它为每个数据集大小训练一组网络,保持在验证集上表现最好的网络。我在不使用GPU的情况下在MacBook上进行这种调优,所以我把自己对每个数据集大小的搜索限制在10次以内,希望我能证明这一点,而不需要请求AWS实例。
然后我们可以再次寻找幂律,果然,它们看起来整洁多了:
不像克莱伯的那么好,但也不坏。
结论:
最初的论文在不同的任务中测试了不同的模型——与这里执行的最接近的是带有ResNets的ImageNet。令人高兴的是,这些结果很容易在不同的网络、不同的数据集上复制。
在他们的讨论中,作者指出
我们还没有找到影响幂律指数的因素。当我们增加数据集的大小时,模型需要学习更多的概念,而数据则依次减少。
这正是你在人类身上看到的,你知道的越多,就越容易获得新知识。
我之前写过关于量化超级智能的进展的困难。似乎超越幂律指数的模型的出现——它们在学习过程中获得了更高的数据效率——这也可能是这条道路上一个重要的经验里程碑。
编译出品