原文:https:///huggingface/from-zero-to-research-an-introduction-to-meta-learning-8e16e677f78ahttps://blog.csdn.net/weixin_39653948/article/details/109279826
元学习(Meta-learning)是机器学习领域一个令人兴奋的研究趋势,它解决了学会学习(learning to learn)的问题。机器学习研究的传统模式是获取特定任务的庞大数据集,并利用该数据集从头开始训练模型。显然,这与人类如何利用过去的经验快速学习新任务相去甚远。那是因为人类 学会了学习(learn to learn)。
What’s learning in the first place?看一下当训练一个简单的神经网络对狗和猫的图像进行分类时会发生什么。假设有一个单独的猫的训练图像以及猫的标签。
Single step of the training process of a neural network. The neural net is trained to classify an image as representing a dog or a cat反向传播(backprop)是训练神经网络的关键步骤。由于神经网络执行的计算和损失是可微函数[3],可以计算应该应用于神经网络的每个参数的梯度,以减少神经网络当前预测的标记和真实/目标标记之间的差异(该差异由损失函数测量)。反向传播之后是优化器,它为模型计算更新的参数。这就是为什么训练神经网络更像是一门艺术而不是一门科学,因为有太多可能的优化器和优化设置(超参数)。
训练图像现在是一只猫,指示图片代表猫的标签是红色三角形。大的三角形是神经网络,有■(正方形)个参数和梯度。损失函数是标有L的梯形,优化器是标有O的梯形。然后,学习过程只是重复应用优化步骤,直到收敛到神经网络的良好参数为止。
Let’s turn to meta-learning元学习的思想是学习学习过程(learn the learning process)。 有几种方法可以实现元学习[4],但我想在这里描述的两种方法是关于学习一个类似于我们刚刚看到的学习过程。 在我们的训练过程中,特别需要学习两件事:
神经网络的初始参数(蓝色■) 优化程序的参数(粉红色★) 我将描述这两种情况的组合,但是每种情况本身也非常有趣,可以导致简化、加速和合理的理论结果。
How do we learn these meta-parameters?事实证明,我们可以沿着训练过程本身反向传播元损失梯度,回到模型的初始权重和/或优化器的参数。 我们现在有两个嵌套的训练过程:优化器/元学习器的元训练过程,其中(元)向前传递包括模型的几个训练步骤(前面已经看到了向前、向后和优化步骤)。 A meta-training step (training the optimizer O) comprising with 3 steps of training the model M)这里,元训练过程的单个步骤被水平表示。它包括模型训练过程的两个步骤(纵向图的元前向传播和元后向传播)。模型的训练过程和我们刚才看到的训练过程一模一样。 如我们所见,元前向传播(meta-forward)过程的输入是在模型训练过程中连续使用的示例/标签列表(或批次列表)。
The input of a meta-training step is a list of examples (🐈, 🐕) with associated labels (🔺,🔻)可以用什么元损失来训练元学习器?在模型训练时,可以简单地将模型预测与目标标签进行比较,以获得误差信号。对于元学习器,我们想要一个元损失,该损失表明元学习器执行任务的能力:训练模型。一种可能性是在一些训练数据上计算模型的损失,损失越小,训练效果越好。我们可以在最后计算出元损失,甚至可以将训练期间已经计算出的模型损失相结合(例如将它们相加)。我们还需要一个元优化器来更新优化器的权重。在这里,它开始变得非常元化,因为我们可以使用另一个元学习器来优化元学习器,依此类推,但最终,我们将需要一个手动定义的优化器,例如SGD或ADAM(不能将所有下降)。关于实现的一些重要说明,我们现在也可以讨论: 二阶导数(Second-order derivatives):通过模型的梯度反向传播元损失涉及计算导数的导数,即二阶导数(当绿色▲经过我们上一个动画的元后向传递时的绿色■时)。我们可以在Tensorflow或PyTorch等现代框架中进行计算,但实际上,我们通常会丢弃二阶导数,并且仅通过模型权重(元后向传递的黄色■)进行反向传播以降低复杂性。 坐标共享(Coordinate sharing):最近的深度学习模型可以包含大量参数(在NLP中大约为30-200百万)。使用当前的GPU内存,不可能有如此多的参数作为优化器的单独输入。我们通常做的称为坐标共享,这意味着我们为模型的单个参数设计优化器,并为所有参数复制优化器(即,沿与模型参数相关联的输入维度分配权重)。这样,元学习器的参数数量与模型的参数数量无关。当元学习器是一个具有像RNN那样的记忆的网络时,我们仍然可以允许每个模型参数具有单独的隐藏状态,以保持每个模型参数的演化的单独记忆。
Meta-learning in PyTorch让我们尝试一些代码,看看实际情况如何。 因此,我们有一个带有权重的模型,我们希望对其进行训练并将其用于两个任务: 在PyTorch中,最简单的方法是有两个代表模型的重复模块,每个任务一个。让我们调用前向模型负责存储元前向传递期间使用的模型梯度的模块,以及后向模型负责将参数作为元后向传递期间反向传播优化器梯度的连续路径的模块。这两个模块将共享其张量,以避免重复存储(张量是内存中的真实内容),但将保留单独的变量,以完全分隔模型的梯度和用于元学习器的梯度。A simple meta-learner class in PyTorch在PyTorch中共享张量非常简单:只需要更新Variable类中的指针以指向相同的张量即可。当模型已经是内存优化模型,例如具有共享张量(输入和输出嵌入)的AWD-LSTM或AWD-QRNN模型时,就会遇到一个难题。然后,当我们更新两个模块的模型参数时,需要注意保持正确的指针。def get_params(module, memo=None, pointers=None): ''' Returns an iterator over PyTorch module parameters that allows to update parameters (and not only the data). ! Side effect: update shared parameters to point to the first yield instance (i.e. you can update shared parameters and keep them shared) Yields: (Module, string, Parameter): Tuple containing the parameter's module, name and pointer ''' if memo is None: memo = set() pointers = {} for name, p in module._parameters.items(): if p not in memo: memo.add(p) pointers[p] = (module, name) yield module, name, p elif p is not None: prev_module, prev_name = pointers[p] module._parameters[name] = prev_module._parameters[prev_name] # update shared parameter pointer for child_module in module.children(): for m, n, p in get_params(child_module, memo, pointers): yield m, n, p
使用此函数,我们可以直接插入任何模型并在元学习器中循环遍历模型参数[8]。现在,让我们编写一个简单的元学习器类。我们的优化程序是一个模块,该模块将在正向传播过程中作为输入,正向模型(带有渐变)和后向模型将循环其参数,以允许元梯度向后传播的方式更新向后模型参数(通过更新参数指针而不仅仅是张量)。class MetaLearner(nn.Module): ''' Bare Meta-learner class Should be added: intialization, hidden states, more control over everything ''' def __init__(self, model): super(MetaLearner, self).__init__() self.weights = Parameter(torch.Tensor(1, 2))
def forward(self, forward_model, backward_model): ''' Forward optimizer with a simple linear neural net Inputs: forward_model: PyTorch module with parameters gradient populated backward_model: PyTorch module identical to forward_model (but without gradients) updated at the Parameter level to keep track of the computation graph for meta-backward pass ''' f_model_iter = get_params(forward_model) b_model_iter = get_params(backward_model) for f_param_tuple, b_param_tuple in zip(f_model_iter, b_model_iter): # loop over parameters # Prepare the inputs, we detach the inputs to avoid computing 2nd derivatives (re-pack in new Variable) (module_f, name_f, param_f) = f_param_tuple (module_b, name_b, param_b) = b_param_tuple inputs = Variable(torch.stack([param_f.grad.data, param_f.data], dim=-1)) # Optimization step: compute new model parameters, here we apply a simple linear function dW = F.linear(inputs, self.weights).squeeze() param_b = param_b + dW # Update backward_model (meta-gradients can flow) and forward_model (no need for meta-gradients). module_b._parameters[name_b] = param_b param_f.data = param_b.data
现在,我们可以像在第一部分中看到的那样训练该优化器。这是一个简单的要点,说明了我们已经描述的元训练过程:def train(forward_model, backward_model, optimizer, meta_optimizer, train_data, meta_epochs): ''' Train a meta-learner Inputs: forward_model, backward_model: Two identical PyTorch modules (can have shared Tensors) optimizer: a neural net to be used as optimizer (an instance of the MetaLearner class) meta_optimizer: an optimizer for the optimizer neural net, e.g. ADAM train_data: an iterator over an epoch of training data meta_epochs: meta-training steps To be added: intialization, early stopping, checkpointing, more control over everything ''' for meta_epoch in range(meta_epochs): # Meta-training loop (train the optimizer) optimizer.zero_grad() losses = [] for inputs, labels in train_data: # Meta-forward pass (train the model) forward_model.zero_grad() # Forward pass inputs = Variable(inputs) labels = Variable(labels) output = forward_model(inputs) loss = loss_func(output, labels) # Compute loss losses.append(loss) loss.backward() # Backward pass to add gradients to the forward_model optimizer(forward_model, # Optimizer step (update the models) backward_model) meta_loss = sum(losses) # Compute a simple meta-loss meta_loss.backward() # Meta-backward pass meta_optimizer.step() # Meta-optimizer step
Avoid memory blow-up — Hidden State Memorization有时我们想学习一个优化器,该优化器可以在具有数千万参数的超大型模型上运行,同时我们想通过大量步骤来展开元训练,以获得高质量的梯度,例如我们在工作中做到了。实际上,这意味着我们要在元前传过程中包括很长的训练过程,其中包含许多时间步长,并且我们必须将参数保留在内存中(黄色■)和渐变(绿色■)的数据用于元后向传递。一种方法是通过使用梯度检查点,也称为隐藏状态记忆,来交换一些内存进行计算[10]。在我们的例子中,梯度检查点包括将元前向和元后向路径分割成我们连续计算的片段。OpenAI 的 Yaroslav Bulatov 的博客文章很好地介绍了梯度检查点(gradient checkpointing)。如果你对此感兴趣,可以点此查看。这篇文章已经很长了,所以我不会包含梯度检查点代码的全部要点。我更愿意介绍TSHadley的PyTorch实现,以及当前在PyTorch中包含梯度检查点的工作。
Other approaches in Meta-learning 🐣我还没有时间来探讨元学习方面的其他两个研究趋势,但它们也非常有希望。我只给一些指示,以便在了解一般思路后可以自己检查一下: 递归网络(Recurrent networks):我们已经建立了神经网络的标准训练过程。另一种方法是将任务序列视为一系列连续的输入,并构建一个循环模型,该模型可以为新任务摄取并构建该序列的表示。在这种情况下,我们通常有一个带有记忆或注意力的循环网络的单一训练过程。这种方法也给出了很好的结果,特别是当嵌入是为任务充分设计的时候。一个很好的例子是最近的 SNAIL paper。 强化学习(Reinforcement learning):优化器在元转发过程中进行的计算非常类似于递归网络的计算:对一系列输入(学习过程中模型的连续权重和梯度)重复应用相同的参数。在实践中,这意味着我们遇到了一个关于递归网络的常见问题:模型在出错时很难回到安全路径,因为它们没有被训练来从训练错误中恢复,并且模型很难推广到比元训练期间使用的序列更长的序列。为了解决这些问题,可以求助于强化学习方法,其中模型学习与当前培训状态相关联的行动策略。
Meta-learning in Natural Language Processing 🗣元学习和自然语言处理(NLP)中使用的神经网络模型之间有一个有趣的相似之处,如我们在上一段中刚刚提到的递归神经网络(RNN):元学习器优化神经网络模型的行为类似于递归神经网络。像RNN一样,元学习器在训练过程中吸收模型的一系列参数和梯度作为输入序列,并根据该输入序列计算顺序输出(更新模型参数的系列)。我们在论文中发展了这一类比,并研究了元学习器如何在神经网络语言模型中实现中期记忆:元学习器学习用标准RNN(如LSTM)的权重对中期记忆进行编码(除了短期记忆在LSTM的隐藏状态下的传统编码方式)。
我们的元学习语言模型有三个层次的记忆,从下到上:一个标准的LSTM,一个元学习器更新LSTM的权重来存储中期记忆和长期静态记忆。 我们发现,元学习语言模型可以被训练成对最近输入的记忆进行编码,比如维基百科文章的开头,这将有助于预测文章的结尾。 曲线表明该模型在预测 Wikipedia 文章开头的单词方面有多好(A、...、H 是连续的 Wikipedia 文章),彩色单词表示单个单词相同,蓝色更好,红色更差。当模型通读一篇文章时,它会从头开始学习并更好地预测结尾。若觉得还不错的话,请点个 “赞” 或 “在看” 吧
|