supervised_reptile/reptile.py (150 lines of code) (raw):
"""
Supervised Reptile learning and evaluation on arbitrary
datasets.
"""
import random
import tensorflow as tf
from .variables import (interpolate_vars, average_vars, subtract_vars, add_vars, scale_vars,
VariableState)
class Reptile:
"""
A meta-learning session.
Reptile can operate in two evaluation modes: normal
and transductive. In transductive mode, information is
allowed to leak between test samples via BatchNorm.
Typically, MAML is used in a transductive manner.
"""
def __init__(self, session, variables=None, transductive=False, pre_step_op=None):
self.session = session
self._model_state = VariableState(self.session, variables or tf.trainable_variables())
self._full_state = VariableState(self.session,
tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES))
self._transductive = transductive
self._pre_step_op = pre_step_op
# pylint: disable=R0913,R0914
def train_step(self,
dataset,
input_ph,
label_ph,
minimize_op,
num_classes,
num_shots,
inner_batch_size,
inner_iters,
replacement,
meta_step_size,
meta_batch_size):
"""
Perform a Reptile training step.
Args:
dataset: a sequence of data classes, where each data
class has a sample(n) method.
input_ph: placeholder for a batch of samples.
label_ph: placeholder for a batch of labels.
minimize_op: TensorFlow Op to minimize a loss on the
batch specified by input_ph and label_ph.
num_classes: number of data classes to sample.
num_shots: number of examples per data class.
inner_batch_size: batch size for every inner-loop
training iteration.
inner_iters: number of inner-loop iterations.
replacement: sample with replacement.
meta_step_size: interpolation coefficient.
meta_batch_size: how many inner-loops to run.
"""
old_vars = self._model_state.export_variables()
new_vars = []
for _ in range(meta_batch_size):
mini_dataset = _sample_mini_dataset(dataset, num_classes, num_shots)
for batch in _mini_batches(mini_dataset, inner_batch_size, inner_iters, replacement):
inputs, labels = zip(*batch)
if self._pre_step_op:
self.session.run(self._pre_step_op)
self.session.run(minimize_op, feed_dict={input_ph: inputs, label_ph: labels})
new_vars.append(self._model_state.export_variables())
self._model_state.import_variables(old_vars)
new_vars = average_vars(new_vars)
self._model_state.import_variables(interpolate_vars(old_vars, new_vars, meta_step_size))
def evaluate(self,
dataset,
input_ph,
label_ph,
minimize_op,
predictions,
num_classes,
num_shots,
inner_batch_size,
inner_iters,
replacement):
"""
Run a single evaluation of the model.
Samples a few-shot learning task and measures
performance.
Args:
dataset: a sequence of data classes, where each data
class has a sample(n) method.
input_ph: placeholder for a batch of samples.
label_ph: placeholder for a batch of labels.
minimize_op: TensorFlow Op to minimize a loss on the
batch specified by input_ph and label_ph.
predictions: a Tensor of integer label predictions.
num_classes: number of data classes to sample.
num_shots: number of examples per data class.
inner_batch_size: batch size for every inner-loop
training iteration.
inner_iters: number of inner-loop iterations.
replacement: sample with replacement.
Returns:
The number of correctly predicted samples.
This always ranges from 0 to num_classes.
"""
train_set, test_set = _split_train_test(
_sample_mini_dataset(dataset, num_classes, num_shots+1))
old_vars = self._full_state.export_variables()
for batch in _mini_batches(train_set, inner_batch_size, inner_iters, replacement):
inputs, labels = zip(*batch)
if self._pre_step_op:
self.session.run(self._pre_step_op)
self.session.run(minimize_op, feed_dict={input_ph: inputs, label_ph: labels})
test_preds = self._test_predictions(train_set, test_set, input_ph, predictions)
num_correct = sum([pred == sample[1] for pred, sample in zip(test_preds, test_set)])
self._full_state.import_variables(old_vars)
return num_correct
def _test_predictions(self, train_set, test_set, input_ph, predictions):
if self._transductive:
inputs, _ = zip(*test_set)
return self.session.run(predictions, feed_dict={input_ph: inputs})
res = []
for test_sample in test_set:
inputs, _ = zip(*train_set)
inputs += (test_sample[0],)
res.append(self.session.run(predictions, feed_dict={input_ph: inputs})[-1])
return res
class FOML(Reptile):
"""
A basic implementation of "first-order MAML" (FOML).
FOML is similar to Reptile, except that you use the
gradient from the last mini-batch as the update
direction.
There are two ways to sample batches for FOML.
By default, FOML samples batches just like Reptile,
meaning that the final mini-batch may overlap with
the previous mini-batches.
Alternatively, if tail_shots is specified, then a
separate mini-batch is used for the final step.
This final mini-batch is guaranteed not to overlap
with the training mini-batches.
"""
def __init__(self, *args, tail_shots=None, **kwargs):
"""
Create a first-order MAML session.
Args:
args: args for Reptile.
tail_shots: if specified, this is the number of
examples per class to reserve for the final
mini-batch.
kwargs: kwargs for Reptile.
"""
super(FOML, self).__init__(*args, **kwargs)
self.tail_shots = tail_shots
# pylint: disable=R0913,R0914
def train_step(self,
dataset,
input_ph,
label_ph,
minimize_op,
num_classes,
num_shots,
inner_batch_size,
inner_iters,
replacement,
meta_step_size,
meta_batch_size):
old_vars = self._model_state.export_variables()
updates = []
for _ in range(meta_batch_size):
mini_dataset = _sample_mini_dataset(dataset, num_classes, num_shots)
mini_batches = self._mini_batches(mini_dataset, inner_batch_size, inner_iters,
replacement)
for batch in mini_batches:
inputs, labels = zip(*batch)
last_backup = self._model_state.export_variables()
if self._pre_step_op:
self.session.run(self._pre_step_op)
self.session.run(minimize_op, feed_dict={input_ph: inputs, label_ph: labels})
updates.append(subtract_vars(self._model_state.export_variables(), last_backup))
self._model_state.import_variables(old_vars)
update = average_vars(updates)
self._model_state.import_variables(add_vars(old_vars, scale_vars(update, meta_step_size)))
def _mini_batches(self, mini_dataset, inner_batch_size, inner_iters, replacement):
"""
Generate inner-loop mini-batches for the task.
"""
if self.tail_shots is None:
for value in _mini_batches(mini_dataset, inner_batch_size, inner_iters, replacement):
yield value
return
train, tail = _split_train_test(mini_dataset, test_shots=self.tail_shots)
for batch in _mini_batches(train, inner_batch_size, inner_iters - 1, replacement):
yield batch
yield tail
def _sample_mini_dataset(dataset, num_classes, num_shots):
"""
Sample a few shot task from a dataset.
Returns:
An iterable of (input, label) pairs.
"""
shuffled = list(dataset)
random.shuffle(shuffled)
for class_idx, class_obj in enumerate(shuffled[:num_classes]):
for sample in class_obj.sample(num_shots):
yield (sample, class_idx)
def _mini_batches(samples, batch_size, num_batches, replacement):
"""
Generate mini-batches from some data.
Returns:
An iterable of sequences of (input, label) pairs,
where each sequence is a mini-batch.
"""
samples = list(samples)
if replacement:
for _ in range(num_batches):
yield random.sample(samples, batch_size)
return
cur_batch = []
batch_count = 0
while True:
random.shuffle(samples)
for sample in samples:
cur_batch.append(sample)
if len(cur_batch) < batch_size:
continue
yield cur_batch
cur_batch = []
batch_count += 1
if batch_count == num_batches:
return
def _split_train_test(samples, test_shots=1):
"""
Split a few-shot task into a train and a test set.
Args:
samples: an iterable of (input, label) pairs.
test_shots: the number of examples per class in the
test set.
Returns:
A tuple (train, test), where train and test are
sequences of (input, label) pairs.
"""
train_set = list(samples)
test_set = []
labels = set(item[1] for item in train_set)
for _ in range(test_shots):
for label in labels:
for i, item in enumerate(train_set):
if item[1] == label:
del train_set[i]
test_set.append(item)
break
if len(test_set) < len(labels) * test_shots:
raise IndexError('not enough examples of each class for test set')
return train_set, test_set