Commit e18e2076 authored by peastman's avatar peastman
Browse files

OntologyModel supports classification

parent 0ba3560c
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ from deepchem.models.tensorgraph.models.gan import GAN, WGAN
from deepchem.models.tensorgraph.models.text_cnn import TextCNNModel
from deepchem.models.tensorgraph.sequential import Sequential
from deepchem.models.tensorgraph.models.sequence_dnn import SequenceDNN
from deepchem.models.tensorgraph.models.ontology import OntologyModel, OntologyNode

#################### Compatibility imports for renamed TensorGraph models. Remove below with DeepChem 3.0. ####################

+85 −12
Original line number Diff line number Diff line
@@ -2,8 +2,10 @@

from deepchem.models import TensorGraph
from deepchem.models.tensorgraph import layers
from deepchem.metrics import to_one_hot
import tensorflow as tf


class OntologyNode(object):

  def __init__(self, id, n_outputs, feature_ids=[], children=[]):
@@ -12,6 +14,7 @@ class OntologyNode(object):
    self.feature_ids = feature_ids
    self.children = children


class OntologyModel(TensorGraph):
  """Implements ontology based models.

@@ -27,14 +30,23 @@ class OntologyModel(TensorGraph):
  much easier to interpret.
  """

  def __init__(self, n_tasks, feature_ids, root_node, mode="regression", n_classes=2, intermediate_loss_weight=0.3, weight_decay_penalty=0.0, **kwargs):
  def __init__(self,
               n_tasks,
               feature_ids,
               root_node,
               mode="regression",
               n_classes=2,
               intermediate_loss_weight=0.3,
               weight_decay_penalty=0.0,
               **kwargs):
    super(OntologyModel, self).__init__(**kwargs)
    self.n_tasks = n_tasks
    self.feature_ids = feature_ids
    self.mode = mode
    self.n_classes = n_classes
    self._feature_index = dict((f, i) for i, f in enumerate(feature_ids))
    self._features = layers.Transpose((1, 0), in_layers=layers.Feature(shape=(None, len(feature_ids))))
    self._features = layers.Transpose(
        (1, 0), in_layers=layers.Feature(shape=(None, len(feature_ids))))
    self.output_for_node = {}
    self.prediction_for_node = {}
    if mode not in ('regression', 'classification'):
@@ -46,13 +58,19 @@ class OntologyModel(TensorGraph):
    self._build_layers(root_node)
    for id in self.output_for_node:
      if mode == 'regression':
        prediction = layers.Dense(in_layers=self.output_for_node[id], out_channels=n_tasks)
        prediction = layers.Dense(
            in_layers=self.output_for_node[id], out_channels=n_tasks)
      else:
        logits = layers.Reshape(shape=(-1, n_tasks, n_classes), in_layers=layers.Dense(in_layers=self.output_for_node[id], out_channels=n_tasks*n_classes))
        logits = layers.Reshape(
            shape=(-1, n_tasks, n_classes),
            in_layers=layers.Dense(
                in_layers=self.output_for_node[id],
                out_channels=n_tasks * n_classes))
        prediction = layers.SoftMax(logits)
        logits_for_node[id] = logits
      self.prediction_for_node[id] = prediction
    self.add_output(self.prediction_for_node[root_node.id])
      self.add_output(self.prediction_for_node[id])
    self.set_default_outputs([self.prediction_for_node[root_node.id]])

    # Create the loss function.

@@ -62,13 +80,21 @@ class OntologyModel(TensorGraph):
    if mode == 'regression':
      labels = layers.Label(shape=(None, n_tasks))
      for id in self.prediction_for_node:
        losses.append(layers.ReduceSum(layers.L2Loss([labels, self.prediction_for_node[id], weights])))
        loss_weights.append(1.0 if id == root_node.id else intermediate_loss_weight)
        losses.append(
            layers.ReduceSum(
                layers.L2Loss([labels, self.prediction_for_node[id], weights])))
        loss_weights.append(1.0
                            if id == root_node.id else intermediate_loss_weight)
    else:
      labels = layers.Label(shape=(None, n_tasks, n_classes))
      for id in self.prediction_for_node:
        losses.append(layers.WeightedError([layers.SoftMaxCrossEntropy([labels, logits_for_node[id]]), weights]))
        loss_weights.append(1.0 if id == root_node.id else intermediate_loss_weight)
        losses.append(
            layers.WeightedError([
                layers.SoftMaxCrossEntropy([labels, logits_for_node[id]]),
                weights
            ]))
        loss_weights.append(1.0
                            if id == root_node.id else intermediate_loss_weight)
    loss = layers.Add(in_layers=losses, weights=loss_weights)
    if weight_decay_penalty != 0.0:
      loss = layers.WeightDecay(weight_decay_penalty, 'l2', in_layers=loss)
@@ -86,7 +112,11 @@ class OntologyModel(TensorGraph):
          indices.append([self._feature_index[f]])
        else:
          raise ValueError('Unknown feature "%s"' % f)
      inputs.append(layers.Transpose((1, 0), in_layers=layers.Gather(in_layers=self._features, indices=indices)))
      inputs.append(
          layers.Transpose(
              (1, 0),
              in_layers=layers.Gather(
                  in_layers=self._features, indices=indices)))

    # Create inputs for the children.

@@ -107,6 +137,49 @@ class OntologyModel(TensorGraph):

    # Create the output.

    dense = layers.Dense(node.n_outputs, in_layers=inputs, activation_fn=tf.tanh)
    dense = layers.Dense(
        node.n_outputs, in_layers=inputs, activation_fn=tf.tanh)
    output = layers.BatchNorm(dense)
    self.output_for_node[node.id] = output

  def default_generator(self,
                        dataset,
                        epochs=1,
                        predict=False,
                        deterministic=True,
                        pad_batches=True):
    for epoch in range(epochs):
      for (X_b, y_b, w_b, ids_b) in dataset.iterbatches(
          batch_size=self.batch_size,
          deterministic=deterministic,
          pad_batches=pad_batches):
        feed_dict = dict()
        if y_b is not None and not predict:
          if self.mode == 'regression':
            feed_dict[self.labels[0]] = y_b
          else:
            feed_dict[self.labels[0]] = to_one_hot(y_b.flatten(),
                                                   self.n_classes).reshape(
                                                       -1, self.n_tasks,
                                                       self.n_classes)
        if X_b is not None:
          feed_dict[self.features[0]] = X_b
        if w_b is not None and not predict:
          feed_dict[self.task_weights[0]] = w_b
        yield feed_dict

  def create_estimator_inputs(self, feature_columns, weight_column, features,
                              labels, mode):
    tensors = {}
    for layer, column in zip(self.features, feature_columns):
      tensors[layer] = tf.feature_column.input_layer(features, [column])
    if weight_column is not None:
      tensors[self.task_weights[0]] = tf.feature_column.input_layer(
          features, [weight_column])
    if labels is not None:
      if self.mode == 'regression':
        tensors[self.labels[0]] = tf.cast(labels, self.labels[0].dtype)
      else:
        tensors[self.labels[0]] = tf.one_hot(
            tf.cast(labels, tf.int32), self.n_classes)
    return tensors
+20 −12
Original line number Diff line number Diff line
@@ -61,6 +61,7 @@ class TensorGraph(Model):
    self.features = list()
    self.labels = list()
    self.outputs = list()
    self.default_outputs = self.outputs
    self.variances = list()
    self.task_weights = list()
    self.submodels = list()
@@ -342,7 +343,7 @@ class TensorGraph(Model):
      may be tensors, numpy arrays, or anything else that can be converted to
      tensors of the correct shape.
    outputs: list of Layers
      the output layers to compute.  If this is omitted, self.outputs is used
      the output layers to compute.  If this is omitted, self.default_outputs is used
      (that is, all outputs that have been added by calling add_output()).

    Returns
@@ -357,7 +358,7 @@ class TensorGraph(Model):
    if 'outputs' in kwargs:
      outputs = kwargs['outputs']
    else:
      outputs = self.outputs
      outputs = self.default_outputs
    feed_dict = dict(zip(self.features, inputs))
    results = self._run_graph(outputs, feed_dict, False)
    if len(results) == 1:
@@ -378,7 +379,7 @@ class TensorGraph(Model):
    transformers: list
      List of dc.trans.Transformers.
    outputs: object
      If outputs is None, then will assume outputs = self.outputs.
      If outputs is None, then will assume outputs = self.default_outputs.
      If outputs is a Layer/Tensor, then will evaluate and return as a
      single ndarray. If outputs is a list of Layers/Tensors, will return a list
      of ndarrays.
@@ -392,7 +393,7 @@ class TensorGraph(Model):
    if not self.built:
      self.build()
    if outputs is None:
      outputs = self.outputs
      outputs = self.default_outputs
    elif not isinstance(outputs, collections.Sequence):
      outputs = [outputs]
    if uncertainty:
@@ -462,7 +463,7 @@ class TensorGraph(Model):
    transformers: list
      List of dc.trans.Transformers.
    outputs: object
      If outputs is None, then will assume outputs = self.outputs.
      If outputs is None, then will assume outputs = self.default_outputs.
      If outputs is a Layer/Tensor, then will evaluate and return as a
      single ndarray. If outputs is a list of Layers/Tensors, will return a list
      of ndarrays.
@@ -527,7 +528,7 @@ class TensorGraph(Model):
    transformers: list
      List of dc.trans.Transformers.
    outputs: object
      If outputs is None, then will assume outputs=self.outputs. If outputs is
      If outputs is None, then will assume outputs=self.default_outputs. If outputs is
      a Layer/Tensor, then will evaluate and return as a single ndarray. If
      outputs is a list of Layers/Tensors, will return a list of ndarrays.

@@ -568,7 +569,7 @@ class TensorGraph(Model):
    for i in range(masks):
      generator = self.default_generator(
          dataset, predict=True, pad_batches=False)
      results = self._predict(generator, [], self.outputs, True)
      results = self._predict(generator, [], self.default_outputs, True)
      if len(sum_pred) == 0:
        for p, v in results:
          sum_pred.append(p)
@@ -746,6 +747,13 @@ class TensorGraph(Model):
    self._add_layer(layer)
    self.outputs.append(layer)

  def set_default_outputs(self, outputs):
    """Set the default outputs to be computed by predict() and evaluate().

    If this has not been called, all outputs are computed by default.
    """
    self.default_outputs = outputs

  def add_variance(self, layer):
    """Add a layer that computes the variance in an output.

@@ -889,8 +897,8 @@ class TensorGraph(Model):

    if labels is None:
      raise ValueError
    n_tasks = len(self.outputs)
    n_classes = self.outputs[0].out_tensor.get_shape()[-1].value
    n_tasks = len(self.default_outputs)
    n_classes = self.default_outputs[0].out_tensor.get_shape()[-1].value
    evaluator = GeneratorEvaluator(
        self,
        feed_dict_generator,
@@ -1029,7 +1037,7 @@ class TensorGraph(Model):
      saver.restore(self.session, checkpoint)

  def get_num_tasks(self):
    return len(self.outputs)
    return len(self.default_outputs)

  def get_pre_q_input(self, input_layer):
    layer_name = input_layer.name
@@ -1204,12 +1212,12 @@ class TensorGraph(Model):

      if mode == tf.estimator.ModeKeys.PREDICT:
        predictions = {}
        for i, output in enumerate(self.outputs):
        for i, output in enumerate(self.default_outputs):
          predictions[i] = create_tensors(output, tensors, 0)
        return tf.estimator.EstimatorSpec(mode, predictions=predictions)
      if mode == tf.estimator.ModeKeys.EVAL:
        loss = create_tensors(self.loss, tensors, 0)
        predictions = create_tensors(self.outputs[0], tensors, 0)
        predictions = create_tensors(self.default_outputs[0], tensors, 0)
        if len(self.task_weights) == 0:
          weights = None
        else:
+68 −10
Original line number Diff line number Diff line
@@ -28,7 +28,8 @@ class TestOntology(test_util.TensorFlowTestCase):
    node1 = dc.models.OntologyNode('leaf1', 5, feature_ids[:(n_features // 2)])
    node2 = dc.models.OntologyNode('leaf2', 5, feature_ids[(n_features // 2):])
    node3 = dc.models.OntologyNode('root', 5, children=[node1, node2])
    model = dc.models.OntologyModel(n_tasks, feature_ids, node3, learning_rate=0.02)
    model = dc.models.OntologyModel(
        n_tasks, feature_ids, node3, learning_rate=0.02)

    # Train the model on the datase.

@@ -42,8 +43,65 @@ class TestOntology(test_util.TensorFlowTestCase):
    # In addition, it should be able to predict the first task based only on the
    # first leaf node, and the second task based only on the second leaf node.

    leaf1_error = np.mean((model.predict_on_batch(x, outputs=model.prediction_for_node['leaf1'])-y)**2, axis=0)
    leaf2_error = np.mean((model.predict_on_batch(x, outputs=model.prediction_for_node['leaf2'])-y)**2, axis=0)
    leaf1_pred = model.predict_on_batch(
        x, outputs=model.prediction_for_node['leaf1'])
    leaf2_pred = model.predict_on_batch(
        x, outputs=model.prediction_for_node['leaf2'])
    leaf1_error = np.mean((leaf1_pred - y)**2, axis=0)
    leaf2_error = np.mean((leaf2_pred - y)**2, axis=0)
    assert leaf1_error[0] < 0.01
    assert leaf2_error[1] < 0.01
    assert np.mean([leaf1_error[1], leaf1_error[2], leaf2_error[0], leaf2_error[2]]) > 0.01
    assert np.mean(
        [leaf1_error[1], leaf1_error[2], leaf2_error[0], leaf2_error[2]]) > 0.01

  def test_ontology_classifier(self):
    """Test training an OntologyModel for classification."""

    # Create a dataset with three tasks.  The first two tasks each depend only
    # on half the features.  The third task depends on all of them.

    n_features = 8
    n_tasks = 3
    n_classes = 2
    n_samples = 50
    x = np.random.random((n_samples, n_features))
    y = np.zeros((n_samples, n_tasks))
    y[:, 0] = np.sum(x[:, :(n_features // 2)], axis=1) < n_features / 4
    y[:, 1] = np.sum(x[:, (n_features // 2):], axis=1) < n_features / 4
    y[:, 2] = np.sum(x, axis=1) < n_features / 2
    dataset = dc.data.NumpyDataset(x, y)

    # Create an OntologyModel.  Two leaf nodes contain half the features.

    feature_ids = [str(i) for i in range(n_features)]
    node1 = dc.models.OntologyNode('leaf1', 5, feature_ids[:(n_features // 2)])
    node2 = dc.models.OntologyNode('leaf2', 5, feature_ids[(n_features // 2):])
    node3 = dc.models.OntologyNode('root', 5, children=[node1, node2])
    model = dc.models.OntologyModel(
        n_tasks, feature_ids, node3, mode='classification', learning_rate=0.02)

    # Train the model on the datase.

    model.fit(dataset, nb_epoch=1000)

    # It should have learned to predict all of the tasks accurately.

    pred = np.argmax(model.predict_on_batch(x), axis=2)
    pred_error = np.mean(np.abs(pred - y), axis=0)
    assert np.all(pred_error == 0)

    # In addition, it should be able to predict the first task based only on the
    # first leaf node, and the second task based only on the second leaf node.

    leaf1_pred = np.argmax(
        model.predict_on_batch(x, outputs=model.prediction_for_node['leaf1']),
        axis=2)
    leaf2_pred = np.argmax(
        model.predict_on_batch(x, outputs=model.prediction_for_node['leaf2']),
        axis=2)
    leaf1_error = np.mean(np.abs(leaf1_pred - y), axis=0)
    leaf2_error = np.mean(np.abs(leaf2_pred - y), axis=0)
    assert leaf1_error[0] == 0
    assert leaf2_error[1] == 0
    assert np.mean(
        [leaf1_error[1], leaf1_error[2], leaf2_error[0], leaf2_error[2]]) > 0.01