Unverified Commit a78dae5b authored by peastman's avatar peastman Committed by GitHub
Browse files

Merge pull request #1636 from peastman/maml

Created new MAML API 
parents 516fb0a8 b2f0404f
Loading
Loading
Loading
Loading
+149 −129
Original line number Diff line number Diff line
"""Model-Agnostic Meta-Learning (MAML) algorithm for low data learning."""

from deepchem.models.tensorgraph.layers import Layer
from deepchem.models.tensorgraph.optimizers import Adam, GradientDescent
import numpy as np
import os
import shutil
import tempfile
@@ -17,23 +17,32 @@ class MetaLearner(object):
  data for training it on a large (possibly infinite) set of different tasks.
  """

  @property
  def loss(self):
    """Get the model's loss function, represented as a Layer or Tensor."""
  def compute_model(self, inputs, variables, training):
    """Compute the model for a set of inputs and variables.

    Parameters
    ----------
    inputs: list of tensors
      the inputs to the model
    variables: list of tensors
      the values to use for the model's variables.  This might be the actual
      variables (as returned by the MetaLearner's variables property), or
      alternatively it might be the values of those variables after one or more
      steps of gradient descent for the current task.
    training: bool
      indicates whether the model is being invoked for training or prediction

    Returns
    -------
    (loss, outputs) where loss is the value of the model's loss function, and
    outputs is a list of the model's outputs
    """
    raise NotImplemented("Subclasses must implement this")

  @property
  def variables(self):
    """Get the list of Tensorflow variables to train.

    The default implementation returns all trainable variables in the graph.  This is usually
    what you want, but subclasses can customize it if needed.
    """
    loss = self.loss
    if isinstance(loss, Layer):
      loss = loss.out_tensor
    with loss.graph.as_default():
      return tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES)
    """Get the list of Tensorflow variables to train."""
    raise NotImplemented("Subclasses must implement this")

  def select_task(self):
    """Select a new task to train on.
@@ -47,8 +56,8 @@ class MetaLearner(object):
  def get_batch(self):
    """Get a batch of data for training.

    This should return the data in the form of a Tensorflow feed dict, that is, a dict
    mapping tensors to values.  This will usually be called twice for each task, and should
    This should return the data as a list of arrays, one for each of the model's
    inputs.  This will usually be called twice for each task, and should
    return a different batch on each call.
    """
    raise NotImplemented("Subclasses must implement this")
@@ -85,9 +94,9 @@ class MAML(object):
    ----------
    learner: MetaLearner
      defines the meta-learning problem
    learning_rate: float, Layer, or Tensor
    learning_rate: float or Tensor
      the learning rate to use for optimizing each task (not to be confused with the one used
      for meta-learning).  This can optionally be made a variable (represented as a Layer or
      for meta-learning).  This can optionally be made a variable (represented as a
      Tensor), in which case the learning rate will itself be learnable.
    optimization_steps: int
      the number of steps of gradient descent to perform for each task
@@ -101,21 +110,10 @@ class MAML(object):
    """
    # Record inputs.

    raise Exception(
        'MAML does not currently work correctly.  It needs to be rewritten to be compatible with modern TensorFlow'
    )
    self.learner = learner
    if isinstance(learner.loss, Layer):
      self._loss = learner.loss.out_tensor
    else:
      self._loss = learner.loss
    if isinstance(learning_rate, Layer):
      self._learning_rate = learning_rate.out_tensor
    else:
    self._learning_rate = learning_rate
    self.meta_batch_size = meta_batch_size
    self.optimizer = optimizer
    self._graph = self._loss.graph

    # Create the output directory if necessary.

@@ -128,37 +126,42 @@ class MAML(object):
      self._model_dir_is_temp = True
    self.model_dir = model_dir
    self.save_file = "%s/%s" % (self.model_dir, "model")
    with self._graph.as_default():
      # Create duplicate placeholders for meta-optimization.

    learner.select_task()
      self._meta_placeholders = {}
      for p in learner.get_batch().keys():
        name = 'meta/' + p.name.split(':')[0]
        self._meta_placeholders[p] = tf.placeholder(p.dtype, p.shape, name)
    example_inputs = learner.get_batch()
    self._input_shapes = [(None,) + i.shape[1:] for i in example_inputs]
    self._input_dtypes = [x.dtype for x in example_inputs]
    self._input_placeholders = [
        tf.placeholder(dtype=tf.as_dtype(t), shape=s)
        for s, t in zip(self._input_shapes, self._input_dtypes)
    ]
    self._meta_placeholders = [
        tf.placeholder(dtype=tf.as_dtype(t), shape=s)
        for s, t in zip(self._input_shapes, self._input_dtypes)
    ]
    variables = learner.variables
    self._loss, self._outputs = learner.compute_model(self._input_placeholders,
                                                      variables, False)
    loss, _ = learner.compute_model(self._input_placeholders, variables, True)

      # Create the loss function for meta-optimization.
    # Build the meta-learning model.

      updated_loss = self._loss
      updated_variables = learner.variables
    updated_variables = variables
    for i in range(optimization_steps):
        gradients = tf.gradients(updated_loss, updated_variables)
      gradients = tf.gradients(loss, updated_variables)
      updated_variables = [
          v if g is None else v - self._learning_rate * g
          for v, g in zip(updated_variables, gradients)
      ]
        replacements = dict(
            (tf.convert_to_tensor(v1), v2)
            for v1, v2 in zip(learner.variables, updated_variables))
      if i == optimization_steps - 1:
        # In the final loss, use different placeholders for all inputs so the loss will be
        # computed from a different batch.

          for p in self._meta_placeholders:
            replacements[p] = self._meta_placeholders[p]
        updated_loss = tf.contrib.graph_editor.graph_replace(
            self._loss, replacements)
      self._meta_loss = updated_loss
        inputs = self._meta_placeholders
      else:
        inputs = self._input_placeholders
      loss, outputs = learner.compute_model(inputs, updated_variables, True)
    self._meta_loss = loss

    # Create variables for accumulating the gradients.

@@ -169,9 +172,7 @@ class MAML(object):
        del variables[i]
        del gradients[i]
    zero_gradients = [tf.zeros(g.shape, g.dtype) for g in gradients]
      summed_gradients = [
          tf.Variable(z, trainable=False) for z in zero_gradients
      ]
    summed_gradients = [tf.Variable(z, trainable=False) for z in zero_gradients]
    self._clear_gradients = tf.group(
        *[s.assign(z) for s, z in zip(summed_gradients, zero_gradients)])
    self._add_gradients = tf.group(
@@ -187,6 +188,7 @@ class MAML(object):
    self._task_train_op = task_optimizer._create_optimizer(
        self._global_step).minimize(self._loss)
    self._session = tf.Session()
    self._session.run(tf.global_variables_initializer())

    # Create a Checkpoint for saving.

@@ -214,11 +216,9 @@ class MAML(object):
    checkpoint_interval: float
      the time interval at which to save checkpoints, measured in seconds
    restore: bool
      if True, restore the model from the most recent checkpoint and continue training
      from there.  If False, retrain the model from scratch.
      if True, restore the model from the most recent checkpoint before training
      it further
    """
    with self._graph.as_default():
      self._session.run(tf.global_variables_initializer())
    if restore:
      self.restore()
    manager = tf.train.CheckpointManager(self._checkpoint, self.model_dir,
@@ -231,17 +231,18 @@ class MAML(object):
      self._session.run(self._clear_gradients)
      for j in range(self.meta_batch_size):
        self.learner.select_task()
          feed_dict = self.learner.get_batch()
        inputs = self.learner.get_batch()
        feed_dict = {}
        feed_dict[self._global_step] = i
          for key, value in self.learner.get_batch().items():
            feed_dict[self._meta_placeholders[key]] = value
        for k in range(len(inputs)):
          feed_dict[self._input_placeholders[k]] = inputs[k]
          feed_dict[self._meta_placeholders[k]] = inputs[k]
        self._session.run(self._add_gradients, feed_dict=feed_dict)
      self._session.run(self._meta_train_op)

      # Do checkpointing.

        if i == steps - 1 or time.time(
        ) >= checkpoint_time + checkpoint_interval:
      if i == steps - 1 or time.time() >= checkpoint_time + checkpoint_interval:
        with self._session.as_default():
          manager.save()
        checkpoint_time = time.time()
@@ -251,7 +252,6 @@ class MAML(object):
    last_checkpoint = tf.train.latest_checkpoint(self.model_dir)
    if last_checkpoint is None:
      raise ValueError('No checkpoint found')
    with self._graph.as_default():
    self._checkpoint.restore(last_checkpoint).run_restore_ops(self._session)

  def train_on_current_task(self, optimization_steps=1, restore=True):
@@ -266,7 +266,27 @@ class MAML(object):
    """
    if restore:
      self.restore()
    with self._graph.as_default():
      feed_dict = self.learner.get_batch()
    inputs = self.learner.get_batch()
    feed_dict = {}
    for p, v in zip(self._input_placeholders, inputs):
      feed_dict[p] = v
    for i in range(optimization_steps):
      self._session.run(self._task_train_op, feed_dict=feed_dict)

  def predict_on_batch(self, inputs):
    """Compute the model's outputs for a batch of inputs.

    Parameters
    ----------
    inputs: list of arrays
      the inputs to the model

    Returns
    -------
    (loss, outputs) where loss is the value of the model's loss function, and
    outputs is a list of the model's outputs
    """
    feed_dict = {}
    for p, v in zip(self._input_placeholders, inputs):
      feed_dict[p] = v
    return self._session.run([self._loss, self._outputs], feed_dict=feed_dict)
+97 −90
Original line number Diff line number Diff line
from flaky import flaky

import deepchem as dc
from deepchem.models.tensorgraph.layers import Feature, Label, Dense, L2Loss
import numpy as np
import tensorflow as tf
import unittest

# class TestMAML(unittest.TestCase):
#
#   @flaky
#   def test_sine(self):
#     """Test meta-learning for sine function."""
#
#     # This is a MetaLearner that learns to generate sine functions with variable
#     # amplitude and phase.
#
#     class SineLearner(dc.metalearning.MetaLearner):
#
#       def __init__(self):
#         self.batch_size = 10
#         self.tg = dc.models.TensorGraph(use_queue=False)
#         self.features = Feature(shape=(None, 1))
#         self.labels = Label(shape=(None, 1))
#         hidden1 = Dense(
#             in_layers=self.features, out_channels=40, activation_fn=tf.nn.relu)
#         hidden2 = Dense(
#             in_layers=hidden1, out_channels=40, activation_fn=tf.nn.relu)
#         output = Dense(in_layers=hidden2, out_channels=1)
#         loss = L2Loss(in_layers=[output, self.labels])
#         self.tg.add_output(output)
#         self.tg.set_loss(loss)
#         with self.tg._get_tf("Graph").as_default():
#           self.tg.build()
#
#       @property
#       def loss(self):
#         return self.tg.loss
#
#       def select_task(self):
#         self.amplitude = 5.0 * np.random.random()
#         self.phase = np.pi * np.random.random()
#
#       def get_batch(self):
#         x = np.random.uniform(-5.0, 5.0, (self.batch_size, 1))
#         feed_dict = {}
#         feed_dict[self.features.out_tensor] = x
#         feed_dict[self.labels.out_tensor] = self.amplitude * np.sin(
#             x + self.phase)
#         return feed_dict
#
#     # Optimize it.
#
#     learner = SineLearner()
#     maml = dc.metalearning.MAML(learner, meta_batch_size=4)
#     maml.fit(12000)
#
#     # Test it out on some new tasks and see how it works.
#
#     loss1 = []
#     loss2 = []
#     for i in range(50):
#       learner.select_task()
#       feed_dict = learner.get_batch()
#       for key, value in learner.get_batch().items():
#         feed_dict[maml._meta_placeholders[key]] = value
#       loss1.append(
#           np.average(
#               np.sqrt(maml._session.run(maml._loss, feed_dict=feed_dict))))
#       loss2.append(
#           np.average(
#               np.sqrt(maml._session.run(maml._meta_loss, feed_dict=feed_dict))))
#
#     # Initially the model should do a bad job of fitting the sine function.
#
#     assert np.average(loss1) > 1.0
#
#     # After one step of optimization it should do much better.
#
#     assert np.average(loss2) < 1.0
#
#     # Verify that we can create a new MAML object, reload the parameters from the first one, and
#     # get the same result.
#
#     new_maml = dc.metalearning.MAML(learner, model_dir=maml.model_dir)
#     new_maml.restore()
#     new_loss = np.average(
#         np.sqrt(new_maml._session.run(new_maml._loss, feed_dict=feed_dict)))
#     assert new_loss == loss1[-1]
#
#     # Do the same thing, only using the "restore" argument to fit().
#
#     new_maml = dc.metalearning.MAML(learner, model_dir=maml.model_dir)
#     new_maml.fit(0, restore=True)
#     new_loss = np.average(
#         np.sqrt(new_maml._session.run(new_maml._loss, feed_dict=feed_dict)))
#     assert new_loss == loss1[-1]

class TestMAML(unittest.TestCase):

  @flaky
  def test_sine(self):
    """Test meta-learning for sine function."""

    # This is a MetaLearner that learns to generate sine functions with variable
    # amplitude and phase.

    class SineLearner(dc.metalearning.MetaLearner):

      def __init__(self):
        self.batch_size = 10
        self.w1 = tf.Variable(np.random.normal(size=[1, 40], scale=1.0))
        self.w2 = tf.Variable(
            np.random.normal(size=[40, 40], scale=np.sqrt(1 / 40)))
        self.w3 = tf.Variable(
            np.random.normal(size=[40, 1], scale=np.sqrt(1 / 40)))
        self.b1 = tf.Variable(np.zeros(40))
        self.b2 = tf.Variable(np.zeros(40))
        self.b3 = tf.Variable(np.zeros(1))

      def compute_model(self, inputs, variables, training):
        x, y = inputs
        w1, w2, w3, b1, b2, b3 = variables
        dense1 = tf.nn.relu(tf.matmul(x, w1) + b1)
        dense2 = tf.nn.relu(tf.matmul(dense1, w2) + b2)
        output = tf.matmul(dense2, w3) + b3
        loss = tf.reduce_mean(tf.square(output - y))
        return loss, [output]

      @property
      def variables(self):
        return [self.w1, self.w2, self.w3, self.b1, self.b2, self.b3]

      def select_task(self):
        self.amplitude = 5.0 * np.random.random()
        self.phase = np.pi * np.random.random()

      def get_batch(self):
        x = np.random.uniform(-5.0, 5.0, (self.batch_size, 1))
        return [x, self.amplitude * np.sin(x + self.phase)]

    # Optimize it.

    learner = SineLearner()
    optimizer = dc.models.tensorgraph.optimizers.Adam(learning_rate=5e-3)
    maml = dc.metalearning.MAML(learner, meta_batch_size=4, optimizer=optimizer)
    maml.fit(9000)

    # Test it out on some new tasks and see how it works.

    loss1 = []
    loss2 = []
    for i in range(50):
      learner.select_task()
      batch = learner.get_batch()
      feed_dict = {}
      for j in range(len(batch)):
        feed_dict[maml._input_placeholders[j]] = batch[j]
        feed_dict[maml._meta_placeholders[j]] = batch[j]
      loss1.append(
          np.average(
              np.sqrt(maml._session.run(maml._loss, feed_dict=feed_dict))))
      loss2.append(
          np.average(
              np.sqrt(maml._session.run(maml._meta_loss, feed_dict=feed_dict))))

    # Initially the model should do a bad job of fitting the sine function.

    assert np.average(loss1) > 1.0

    # After one step of optimization it should do much better.

    assert np.average(loss2) < 1.0

    # If we train on the current task, the loss should go down.

    maml.train_on_current_task()
    assert np.average(
        np.sqrt(maml._session.run(maml._loss, feed_dict=feed_dict))) < loss1[-1]

    # Verify that we can create a new MAML object, reload the parameters from the first one, and
    # get the same result.

    new_maml = dc.metalearning.MAML(learner, model_dir=maml.model_dir)
    new_maml.restore()
    loss, outputs = new_maml.predict_on_batch(batch)
    assert np.sqrt(loss) == loss1[-1]

    # Do the same thing, only using the "restore" argument to fit().

    new_maml = dc.metalearning.MAML(learner, model_dir=maml.model_dir)
    new_maml.fit(0, restore=True)
    loss, outputs = new_maml.predict_on_batch(batch)
    assert np.sqrt(loss) == loss1[-1]
+2 −2
Original line number Diff line number Diff line
@@ -73,13 +73,13 @@ class TestPPO(unittest.TestCase):
        TestPolicy(),
        max_rollout_length=20,
        optimizer=Adam(learning_rate=0.003))
    ppo.fit(50000)
    ppo.fit(80000)

    # It should have learned that the expected value is very close to zero, and that the best
    # action is to walk away.

    action_prob, value = ppo.predict([[0]])
    assert -0.5 < value[0] < 0.5
    assert -0.8 < value[0] < 0.5
    assert action_prob.argmax() == 37
    assert ppo.select_action([[0]], deterministic=True) == 37

+32 −29
Original line number Diff line number Diff line
@@ -19,7 +19,7 @@ n_tasks = y.shape[1]
# Toxcast has data on 6874 molecules and 617 tasks.  However, the data is very
# sparse: most tasks do not include data for most molecules.  It also is very
# unbalanced: there are many more negatives than positives.  For each task,
# create a list of alternating postives and negatives so each batch will have
# create a list of alternating positives and negatives so each batch will have
# equal numbers of both.

task_molecules = []
@@ -28,16 +28,9 @@ for i in range(n_tasks):
  negatives = [j for j in range(n_molecules) if w[j, i] > 0 and y[j, i] == 0]
  np.random.shuffle(positives)
  np.random.shuffle(negatives)
  mols = sum((list(x) for x in zip(positives, negatives)), [])
  mols = sum((list(m) for m in zip(positives, negatives)), [])
  task_molecules.append(mols)

# Create the model to train.  We use a simple fully connected network with
# one hidden layer.

model = dc.models.MultitaskClassifier(
    1, n_features, layer_sizes=[1000], dropouts=[0.0])
model.build()

# Define a MetaLearner describing the learning problem.


@@ -48,10 +41,26 @@ class ToxcastLearner(dc.metalearning.MetaLearner):
    self.batch_size = 10
    self.batch_start = [0] * n_tasks
    self.set_task_index(0)
    self.w1 = tf.Variable(
        np.random.normal(size=[n_features, 1000], scale=0.02), dtype=tf.float32)
    self.w2 = tf.Variable(
        np.random.normal(size=[1000, 1], scale=0.02), dtype=tf.float32)
    self.b1 = tf.Variable(np.ones(1000), dtype=tf.float32)
    self.b2 = tf.Variable(np.zeros(1), dtype=tf.float32)

  def compute_model(self, inputs, variables, training):
    x, y = [tf.cast(i, tf.float32) for i in inputs]
    w1, w2, b1, b2 = variables
    dense1 = tf.nn.relu(tf.matmul(x, w1) + b1)
    logits = tf.matmul(dense1, w2) + b2
    output = tf.sigmoid(logits)
    loss = tf.reduce_mean(
        tf.nn.sigmoid_cross_entropy_with_logits(logits=logits, labels=y))
    return loss, [output]

  @property
  def loss(self):
    return model.loss
  def variables(self):
    return [self.w1, self.w2, self.b1, self.b2]

  def set_task_index(self, index):
    self.task = index
@@ -63,18 +72,13 @@ class ToxcastLearner(dc.metalearning.MetaLearner):
    task = self.task
    start = self.batch_start[task]
    mols = task_molecules[task][start:start + self.batch_size]
    labels = np.zeros((self.batch_size, 1, 2))
    labels[np.arange(self.batch_size), 0, y[mols, task].astype(np.int64)] = 1
    weights = np.ones((self.batch_size, 1))
    feed_dict = {}
    feed_dict[model.features[0].out_tensor] = x[mols, :]
    feed_dict[model.labels[0].out_tensor] = labels
    feed_dict[model.task_weights[0].out_tensor] = weights
    labels = np.zeros((self.batch_size, 1))
    labels[np.arange(self.batch_size), 0] = y[mols, task]
    if start + 2 * self.batch_size > len(task_molecules[task]):
      self.batch_start[task] = 0
    else:
      self.batch_start[task] += self.batch_size
    return feed_dict
    return [x[mols, :], labels]


# Run meta-learning on 80% of the tasks.
@@ -93,16 +97,15 @@ def compute_scores(optimize):
  y_true = []
  y_pred = []
  losses = []
  with model._get_tf("Graph").as_default():
    prediction = tf.nn.softmax(model.outputs[0].out_tensor)
  for task in range(learner.n_training_tasks, n_tasks):
    learner.set_task_index(task)
    if optimize:
        maml.train_on_current_task()
      feed_dict = learner.get_batch()
      y_true.append(feed_dict[model.labels[0].out_tensor][:, 0, 0])
      y_pred.append(maml._session.run(prediction, feed_dict=feed_dict)[:, 0, 0])
      losses.append(maml._session.run(model.loss, feed_dict=feed_dict))
      maml.train_on_current_task(restore=True)
    inputs = learner.get_batch()
    loss, prediction = maml.predict_on_batch(inputs)
    y_true.append(inputs[1])
    y_pred.append(prediction[0][:, 0])
    losses.append(loss)
  y_true = np.concatenate(y_true)
  y_pred = np.concatenate(y_pred)
  print()