Python使用“MAML”和“Reptile”的深度元学习
Reptile和MAML的metalearning方法是为神经网络提出一个初始化方法,该方法很容易推广到类似的任务。这与“学习通过梯度下降学习梯度下降”不同,我们并未学习初始化,而是优化器。
转移学习
这种方法与传输学习非常相似,在这种方法中,我们在ImageNet上训练一个网络,后来事实证明,微调这个网络可以很容易地学习另一个图像数据集,而且数据少得多。事实上,转移学习可以被看作是一种元学习的形式。事实上,它可以用来从非常小的数据集中学习。
这里的区别在于,初始网络是以明确的易于普遍化的目的进行训练的,而传输学习只是“意外”发生,因此可能无法达到最佳效果。
事实上,找到一个转移学习不能学习一个好的初始化的相当容易。为此,我们需要看一维正弦波回归问题。
在这个K-shot问题中,每个任务都是学习一个修正的正弦函数。具体地,对于每个任务,底层功能将是这样的形式的Y = A的sin(x + b)中,与两个一和b随机选择,而我们的神经网络的目标是学习找到ý给定X仅基于10(x,y)对。
我们来绘制几个示例正弦波任务:
class SineWaveTask:
def __init__(self):
self.a = np.random.uniform(0.1, 5.0)
self.b = np.random.uniform(0, 2*np.pi)
self.train_x = None
def f(self, x):
return self.a * np.sin(x + self.b)
def training_set(self, size=10, force_new=False):
if self.train_x is None and not force_new:
self.train_x = np.random.uniform(-5, 5, size)
x = self.train_x
elif not force_new:
x = self.train_x
else:
x = np.random.uniform(-5, 5, size)
y = self.f(x)
return torch.Tensor(x), torch.Tensor(y)
def test_set(self, size=50):
x = np.linspace(-5, 5, size)
y = self.f(x)
return torch.Tensor(x), torch.Tensor(y)
def plot(self, *args, **kwargs):
x, y = self.test_set(size=100)
return plt.plot(x.numpy(), y.numpy(), *args, **kwargs)
SineWaveTask().plot()
SineWaveTask().plot()
SineWaveTask().plot()
plt.show()
为了理解为什么这将成为转移学习的一个问题,让我们绘制其中的1000个:
看起来每个x值都有很多重叠...
由于在多个任务中每个x有多个可能的值,如果我们训练单个神经网络同时处理多个任务,那么最好的办法就是返回每个x的所有任务的平均 y值。那么每个x的平均y值是多少?
平均值基本上是0,这意味着训练了很多任务的神经网络只需返回0!目前尚不清楚这实际上会有多大帮助,但在这种情况下,这是转移学习方法......
通过实际实现一个简单的模型来解决这些正弦波任务并使用传输学习来训练它,让我们看看它的效果如何。首先,模型本身:
class SineModel(ModifiableModule):
def __init__(self):
super().__init__()
self.hidden1 = GradLinear(1, 40)
self.hidden2 = GradLinear(40, 40)
self.out = GradLinear(40, 1)
def forward(self, x):
x = F.relu(self.hidden1(x))
x = F.relu(self.hidden2(x))
return self.out(x)
def named_submodules(self):
return [('hidden1', self.hidden1), ('hidden2', self.hidden2), ('out', self.out)]
现在,让我们按顺序对一堆不同的随机任务进行一段时间的训练:
SINE_TRANSFER = SineModel()
def sine_fit1(net, wave, optim=None, create_graph=False, force_new=False):
net.train()
if optim is not None:
optim.zero_grad()
x, y = wave.training_set(force_new=force_new)
loss = F.mse_loss(net(V(x[:, None])), V(y))
loss.backward(create_graph=create_graph, retain_graph=True)
if optim is not None:
optim.step()
return loss.data.cpu().numpy()[0]
def fit_transfer(epochs=1):
optim = torch.optim.Adam(SINE_TRANSFER.params())
for _ in range(epochs):
for t in random.sample(SINE_TRAIN, len(SINE_TRAIN)):
sine_fit1(SINE_TRANSFER, t, optim)
fit_transfer()
基本上,它看起来像我们的传输模型学习一个不变的函数,而且很难将它调整到更好的状态。事实上并非如此!随机初始化随着时间的推移最终损失比微调我们的传输模型更好。
MAML
如前所述,我们试图找到一组权重,以便在类似任务上运行梯度下降尽可能快地取得进展。MAML 通过运行一次梯度下降迭代,然后基于一次迭代对真实任务取得的进展更新初始权重,从而极其字面地处理这个问题。更具体地说它:
创建初始化权重的副本
对副本上的随机任务运行渐变下降迭代
通过迭代梯度下降并返回初始权重,在测试集上反向传播损失,以便我们可以更新初始权重,使其更容易更新。
因此,我们需要在这个过程中采用梯度梯度,也就是二阶导数。幸运的是,这是PyTorch现在支持的东西,不幸的是,PyTorch以一种我们仍然可以通过它们运行渐变下降的方式更新模型的参数有些尴尬(我们已经看到这是“通过渐变学习渐变下降下降“),这解释了写模型的怪异方式。
因为我们要使用二阶导数,所以我们需要确保允许我们计算原始梯度的计算图保持在周围,这就是我们传递create_graph=True给 它的原因.backward()。
def maml_sine(model, epochs, lr_inner=0.01, batch_size=1):
optimizer = torch.optim.Adam(model.params())
for _ in range(epochs):
for i, t in enumerate(random.sample(SINE_TRAIN, len(SINE_TRAIN))):
new_model = SineModel()
new_model.copy(model, same_var=True)
loss = sine_fit1(new_model, t, create_graph=not first_order)
for name, param in new_model.named_params():
grad = param.grad
new_model.set_param(name, param - lr_inner * grad)
sine_fit1(new_model, t, force_new=True)
if (i + 1) % batch_size == 0:
optimizer.step()
optimizer.zero_grad()
即使在梯度下降的单个步骤后,正弦形状开始可见,并且经过10个步骤后,波的中心几乎完全正确,但这样更好。这是否反映在学习曲线中?是!
是否有近似的MAML不使用二阶导数?当然!我们可以简单地假设我们用于内部梯度下降的梯度刚刚出现,因此只是在不考虑这些二阶导数的情况下改进初始参数。让我们在我们的MAML训练函数中添加一个first_order参数来处理这个问题:
def maml_sine(model, epochs, lr_inner=0.01, batch_size=1, first_order=False):
optimizer = torch.optim.Adam(model.params())
for _ in range(epochs):
for i, t in enumerate(random.sample(SINE_TRAIN, len(SINE_TRAIN))):
new_model = SineModel()
new_model.copy(model, same_var=True)
loss = sine_fit1(new_model, t, create_graph=not first_order)
for name, param in new_model.named_params():
grad = param.grad
if first_order:
grad = V(grad.detach().data)
new_model.set_param(name, param - lr_inner * grad)
sine_fit1(new_model, t, force_new=True)
if (i + 1) % batch_size == 0:
optimizer.step()
optimizer.zero_grad()
这个一阶近似有多好?事实证明,它几乎和原来的MAML一样好,而且速度确实快了大约33%。
Reptile
现在我们来实现Reptile并将其与MAML进行比较:
def reptile_sine(model, epochs, lr_inner=0.01, lr_outer=0.001, k=3, batch_size=32):
optimizer = torch.optim.Adam(model.params(), lr=lr_outer)
name_to_param = dict(model.named_params())
for _ in range(epochs):
for i, t in enumerate(random.sample(SINE_TRAIN, len(SINE_TRAIN))):
new_model = SineModel()
new_model.copy(model)
inner_optim = torch.optim.SGD(new_model.params(), lr=lr_inner)
for _ in range(k):
sine_fit1(new_model, t, inner_optim)
for name, param in new_model.named_params():
cur_grad = (name_to_param[name].data - param.data) / k / lr_inner
if name_to_param[name].grad is None:
name_to_param[name].grad = V(torch.zeros(cur_grad.size()))
name_to_param[name].grad.data.add_(cur_grad / batch_size)
if (i + 1) % batch_size == 0:
optimizer.step()
optimizer.zero_grad()
看起来Reptile确实可以达到与MAML相似甚至稍好一点的性能,而且算法简单快速!
所有这些都适用于许多问题,而不仅仅是这种正弦波的例子。