Repository: cornellius-gp/gpytorch
Pull request #471 (2019-01-23 05:34:02)

Multi-output GP model
A draft for a multi-output GP model (+ associated Likelihood + MLL). Little more than a convenient container of a list of models. In particular evaluation is done sequentially internally. The better way to do this would be to specify a structured block-diagonal kernel, with a block for each output. This however will require some more work on the internals of how inference is handled (in terms of properly sub-partitioning the full covariance matrix).

Will add some examples and unit tests for this soon.


Patch:
diff --git a/examples/11_ModelList_GP_Regression/ModelList_GP_Regression.ipynb b/examples/11_ModelList_GP_Regression/ModelList_GP_Regression.ipynb
new file mode 100644
index 00000000..a5893b21
--- /dev/null
+++ b/examples/11_ModelList_GP_Regression/ModelList_GP_Regression.ipynb
@@ -0,0 +1,323 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# ModelList (Multi-Output) GP Regression\n",
+    "\n",
+    "## Introduction\n",
+    "\n",
+    "This notebook demonstrates how to wrap independent GP models into a convenient Multi-Output GP model using a ModelList. \n",
+    "\n",
+    "Unlike in the Multitask case, this do not model correlations between outcomes, but treats outcomes independently. This is equivalent to setting up a separate GP for each outcome, but can be much more convenient to handle, in particular it does not require manually looping over models when fitting or predicting.\n",
+    "\n",
+    "This type of model is useful if \n",
+    "- when the number of training / test points is different for the different outcomes\n",
+    "- using different covariance modules and / or likelihoods for each outcome\n",
+    "\n",
+    "For block designs (i.e. when the above points do not apply), you should instead use a batch mode GP as described in the [batch simple regression example](../01_Simple_GP_Regression/Simple_Batch_Mode_GP_Regression.ipynb). This will be much faster because it uses additional parallelism."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "import math\n",
+    "import torch\n",
+    "import gpytorch\n",
+    "from matplotlib import pyplot as plt\n",
+    "\n",
+    "%matplotlib inline\n",
+    "%load_ext autoreload\n",
+    "%autoreload 2"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Set up training data\n",
+    "\n",
+    "In the next cell, we set up the training data for this example. We'll be using a different number of training examples for the different GPs."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "train_x1 = torch.linspace(0, 0.95, 50) + 0.05 * torch.rand(50)\n",
+    "train_x2 = torch.linspace(0, 0.95, 25) + 0.05 * torch.rand(25)\n",
+    "\n",
+    "train_y1 = torch.sin(train_x1 * (2 * math.pi)) + 0.2 * torch.randn_like(train_x1)\n",
+    "train_y2 = torch.cos(train_x2 * (2 * math.pi)) + 0.2 * torch.randn_like(train_x2)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Set up the sub-models\n",
+    "\n",
+    "Each individual model uses the `ExactGP` model from the [simple regression example](../01_Simple_GP_Regression/Simple_GP_Regression.ipynb)."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "class ExactGPModel(gpytorch.models.ExactGP):\n",
+    "    def __init__(self, train_x, train_y, likelihood):\n",
+    "        super().__init__(train_x, train_y, likelihood)\n",
+    "        self.mean_module = gpytorch.means.ConstantMean()\n",
+    "        self.covar_module = gpytorch.kernels.ScaleKernel(gpytorch.kernels.RBFKernel())\n",
+    "    \n",
+    "    def forward(self, x):\n",
+    "        mean_x = self.mean_module(x)\n",
+    "        covar_x = self.covar_module(x)\n",
+    "        return gpytorch.distributions.MultivariateNormal(mean_x, covar_x)\n",
+    "    \n",
+    "\n",
+    "likelihood1 = gpytorch.likelihoods.GaussianLikelihood()\n",
+    "model1 = ExactGPModel(train_x1, train_y1, likelihood1)\n",
+    "\n",
+    "likelihood2 = gpytorch.likelihoods.GaussianLikelihood()\n",
+    "model2 = ExactGPModel(train_x2, train_y2, likelihood2)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "We now collect the submodels in an `IndependentMultiOutputGP`, and the respective likelihoods in a `MultiOutputLikelihood`. These are container modules that make it easy to work with multiple outputs. In particular, they will take in and return lists of inputs / outputs and delegate the data to / from the appropriate sub-model (it is important that the order of the inputs / outputs corresponds to the order of models with which the containers were instantiated)."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "model = gpytorch.models.IndependentModelList(model1, model2)\n",
+    "likelihood = gpytorch.likelihoods.LikelihoodList(model1.likelihood, model2.likelihood)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Set up overall Marginal Log Likelihood\n",
+    "\n",
+    "Assuming independence, the MLL for the container model is simply the sum of the MLLs for the individual models. `SumMarginalLogLikelihood` is a convenient container for this (by default it uses an `ExactMarginalLogLikelihood` for each submodel)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "from gpytorch.mlls import SumMarginalLogLikelihood\n",
+    "\n",
+    "mll = SumMarginalLogLikelihood(likelihood, model)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Train the model hyperparameters\n",
+    "\n",
+    "With the containers in place, the models can be trained in a single loop on the container (note that this means that optimization is performed jointly, which can be an issue if the individual submodels require training via very different step sizes)."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {
+    "scrolled": false
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Iter 1/50 - Loss: 1.052\n",
+      "Iter 2/50 - Loss: 1.020\n",
+      "Iter 3/50 - Loss: 0.983\n",
+      "Iter 4/50 - Loss: 0.942\n",
+      "Iter 5/50 - Loss: 0.898\n",
+      "Iter 6/50 - Loss: 0.851\n",
+      "Iter 7/50 - Loss: 0.803\n",
+      "Iter 8/50 - Loss: 0.754\n",
+      "Iter 9/50 - Loss: 0.707\n",
+      "Iter 10/50 - Loss: 0.663\n",
+      "Iter 11/50 - Loss: 0.624\n",
+      "Iter 12/50 - Loss: 0.589\n",
+      "Iter 13/50 - Loss: 0.557\n",
+      "Iter 14/50 - Loss: 0.523\n",
+      "Iter 15/50 - Loss: 0.493\n",
+      "Iter 16/50 - Loss: 0.460\n",
+      "Iter 17/50 - Loss: 0.424\n",
+      "Iter 18/50 - Loss: 0.395\n",
+      "Iter 19/50 - Loss: 0.362\n",
+      "Iter 20/50 - Loss: 0.331\n",
+      "Iter 21/50 - Loss: 0.304\n",
+      "Iter 22/50 - Loss: 0.261\n",
+      "Iter 23/50 - Loss: 0.231\n",
+      "Iter 24/50 - Loss: 0.202\n",
+      "Iter 25/50 - Loss: 0.182\n",
+      "Iter 26/50 - Loss: 0.153\n",
+      "Iter 27/50 - Loss: 0.138\n",
+      "Iter 28/50 - Loss: 0.121\n",
+      "Iter 29/50 - Loss: 0.104\n",
+      "Iter 30/50 - Loss: 0.092\n",
+      "Iter 31/50 - Loss: 0.078\n",
+      "Iter 32/50 - Loss: 0.067\n",
+      "Iter 33/50 - Loss: 0.058\n",
+      "Iter 34/50 - Loss: 0.060\n",
+      "Iter 35/50 - Loss: 0.046\n",
+      "Iter 36/50 - Loss: 0.055\n",
+      "Iter 37/50 - Loss: 0.046\n",
+      "Iter 38/50 - Loss: 0.053\n",
+      "Iter 39/50 - Loss: 0.063\n",
+      "Iter 40/50 - Loss: 0.059\n",
+      "Iter 41/50 - Loss: 0.057\n",
+      "Iter 42/50 - Loss: 0.067\n",
+      "Iter 43/50 - Loss: 0.086\n",
+      "Iter 44/50 - Loss: 0.070\n",
+      "Iter 45/50 - Loss: 0.067\n",
+      "Iter 46/50 - Loss: 0.081\n",
+      "Iter 47/50 - Loss: 0.075\n",
+      "Iter 48/50 - Loss: 0.068\n",
+      "Iter 49/50 - Loss: 0.071\n",
+      "Iter 50/50 - Loss: 0.060\n"
+     ]
+    }
+   ],
+   "source": [
+    "# Find optimal model hyperparameters\n",
+    "model.train()\n",
+    "likelihood.train()\n",
+    "\n",
+    "# Use the Adam optimizer\n",
+    "optimizer = torch.optim.Adam([\n",
+    "    {'params': model.parameters()},  # Includes all submodel and all likelihood parameters\n",
+    "], lr=0.1)\n",
+    "\n",
+    "n_iter = 50\n",
+    "for i in range(n_iter):\n",
+    "    optimizer.zero_grad()\n",
+    "    output = model(*model.train_inputs)\n",
+    "    loss = -mll(output, model.train_targets)\n",
+    "    loss.backward()\n",
+    "    print('Iter %d/%d - Loss: %.3f' % (i + 1, n_iter, loss.item()))\n",
+    "    optimizer.step()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Make predictions with the model"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "image/png": "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\n",
+      "text/plain": [
+       "<Figure size 576x216 with 2 Axes>"
+      ]
+     },
+     "metadata": {
+      "bento_obj_id": "139977174656392"
+     },
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "# Set into eval mode\n",
+    "model.eval()\n",
+    "likelihood.eval()\n",
+    "\n",
+    "# Initialize plots\n",
+    "f, axs = plt.subplots(1, 2, figsize=(8, 3))\n",
+    "\n",
+    "# Make predictions (use the same test points)\n",
+    "with torch.no_grad(), gpytorch.settings.fast_pred_var():\n",
+    "    test_x = torch.linspace(0, 1, 51)\n",
+    "    # This contains predictions for both outcomes as a list\n",
+    "    predictions = likelihood(*model(test_x, test_x))\n",
+    "    \n",
+    "for submodel, prediction, ax in zip(model.models, predictions, axs):\n",
+    "    mean = prediction.mean\n",
+    "    lower, upper = prediction.confidence_region()\n",
+    "    \n",
+    "    tr_x = submodel.train_inputs[0].detach().numpy()\n",
+    "    tr_y = submodel.train_targets.detach().numpy()\n",
+    "    \n",
+    "    # Plot training data as black stars\n",
+    "    ax.plot(tr_x, tr_y, 'k*')\n",
+    "    # Predictive mean as blue line\n",
+    "    ax.plot(test_x.numpy(), mean.numpy(), 'b')\n",
+    "    # Shade in confidence \n",
+    "    ax.fill_between(test_x.numpy(), lower.detach().numpy(), upper.detach().numpy(), alpha=0.5)\n",
+    "    ax.set_ylim([-3, 3])\n",
+    "    ax.legend(['Observed Data', 'Mean', 'Confidence'])\n",
+    "    ax.set_title('Observed Values (Likelihood)')\n",
+    "\n",
+    "None"
+   ]
+  }
+ ],
+ "metadata": {
+  "anaconda-cloud": {},
+  "bento_stylesheets": {
+   "bento/extensions/flow/main.css": true,
+   "bento/extensions/kernel_selector/main.css": true,
+   "bento/extensions/kernel_ui/main.css": true,
+   "bento/extensions/new_kernel/main.css": true,
+   "bento/extensions/system_usage/main.css": true
+  },
+  "kernelspec": {
+   "display_name": "ae_lazarus (local)",
+   "language": "python",
+   "name": "ae_lazarus_local"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.6.3rc1+"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 1
+}
diff --git a/examples/11_ModelList_GP_Regression/README.md b/examples/11_ModelList_GP_Regression/README.md
new file mode 100644
index 00000000..23635654
--- /dev/null
+++ b/examples/11_ModelList_GP_Regression/README.md
@@ -0,0 +1,3 @@
+# ModelList (Multi-Output) GP Regression (independent GPs)
+
+A way of conveniently wrapping independent GPs into a single model.
diff --git a/examples/11_ModelList_GP_Regression/index.rst b/examples/11_ModelList_GP_Regression/index.rst
new file mode 100644
index 00000000..b0c29899
--- /dev/null
+++ b/examples/11_ModelList_GP_Regression/index.rst
@@ -0,0 +1,8 @@
+.. mdinclude:: README.md
+
+.. toctree::
+   :glob:
+   :maxdepth: 1
+   :hidden:
+
+   ModelList_GP_Regression.ipynb
diff --git a/gpytorch/likelihoods/__init__.py b/gpytorch/likelihoods/__init__.py
index d0634be7..f83092cd 100644
--- a/gpytorch/likelihoods/__init__.py
+++ b/gpytorch/likelihoods/__init__.py
@@ -3,6 +3,7 @@
 from .likelihood import Likelihood
 from .bernoulli_likelihood import BernoulliLikelihood
 from .gaussian_likelihood import GaussianLikelihood, _GaussianLikelihoodBase
+from .likelihood_list import LikelihoodList
 from .multitask_gaussian_likelihood import (
     MultitaskGaussianLikelihood,
     MultitaskGaussianLikelihoodKronecker,
@@ -19,6 +20,7 @@ __all__ = [
     "GaussianLikelihood",
     "HeteroskedasticNoise",
     "Likelihood",
+    "LikelihoodList",
     "MultitaskGaussianLikelihood",
     "MultitaskGaussianLikelihoodKronecker",
     "SoftmaxLikelihood",
diff --git a/gpytorch/likelihoods/likelihood_list.py b/gpytorch/likelihoods/likelihood_list.py
new file mode 100644
index 00000000..49f21373
--- /dev/null
+++ b/gpytorch/likelihoods/likelihood_list.py
@@ -0,0 +1,21 @@
+#! /usr/bin/env python3
+
+from gpytorch.likelihoods import Likelihood
+from torch.nn import ModuleList
+
+
+def _get_tuple_args_(*args):
+    for arg in args:
+        if isinstance(arg, tuple):
+            yield arg
+        else:
+            yield (arg,)
+
+
+class LikelihoodList(Likelihood):
+    def __init__(self, *likelihoods):
+        super().__init__()
+        self.likelihoods = ModuleList(likelihoods)
+
+    def forward(self, *args):
+        return [likelihood.forward(*args_) for likelihood, args_ in zip(self.likelihoods, _get_tuple_args_(*args))]
diff --git a/gpytorch/mlls/__init__.py b/gpytorch/mlls/__init__.py
index a3e6644e..b232b068 100644
--- a/gpytorch/mlls/__init__.py
+++ b/gpytorch/mlls/__init__.py
@@ -1,19 +1,21 @@
 #!/usr/bin/env python3
 
-from .marginal_log_likelihood import MarginalLogLikelihood
-from .exact_marginal_log_likelihood import ExactMarginalLogLikelihood
-from .variational_marginal_log_likelihood import VariationalMarginalLogLikelihood
-from .variational_elbo import VariationalELBO, VariationalELBOEmpirical
 from .added_loss_term import AddedLossTerm
+from .exact_marginal_log_likelihood import ExactMarginalLogLikelihood
 from .inducing_point_kernel_added_loss_term import InducingPointKernelAddedLossTerm
+from .marginal_log_likelihood import MarginalLogLikelihood
+from .sum_marginal_log_likelihood import SumMarginalLogLikelihood
+from .variational_elbo import VariationalELBO, VariationalELBOEmpirical
+from .variational_marginal_log_likelihood import VariationalMarginalLogLikelihood
 
 
 __all__ = [
-    "MarginalLogLikelihood",
+    "AddedLossTerm",
     "ExactMarginalLogLikelihood",
-    "VariationalMarginalLogLikelihood",
+    "InducingPointKernelAddedLossTerm",
+    "MarginalLogLikelihood",
+    "SumMarginalLogLikelihood",
     "VariationalELBO",
     "VariationalELBOEmpirical",
-    "AddedLossTerm",
-    "InducingPointKernelAddedLossTerm",
+    "VariationalMarginalLogLikelihood",
 ]
diff --git a/gpytorch/mlls/sum_marginal_log_likelihood.py b/gpytorch/mlls/sum_marginal_log_likelihood.py
new file mode 100644
index 00000000..9a5dd1d9
--- /dev/null
+++ b/gpytorch/mlls/sum_marginal_log_likelihood.py
@@ -0,0 +1,38 @@
+#! /usr/bin/env python3
+
+from gpytorch.mlls import ExactMarginalLogLikelihood, MarginalLogLikelihood
+from torch.nn import ModuleList
+
+
+class SumMarginalLogLikelihood(MarginalLogLikelihood):
+    """Sum of marginal log likelihoods, to be used with Multi-Output models.
+
+    Args:
+        likelihood: A MultiOutputLikelihood
+        model: A MultiOutputModel
+        mll_cls: The Marginal Log Likelihood class (default: ExactMarginalLogLikelihood)
+
+    In case the model outputs are independent, this provives the MLL of the multi-output model.
+
+    """
+
+    def __init__(self, likelihood, model, mll_cls=ExactMarginalLogLikelihood):
+        super().__init__(model.likelihood, model)
+        self.mlls = ModuleList([mll_cls(mdl.likelihood, mdl) for mdl in model.models])
+
+    def forward(self, outputs, targets, *params):
+        """
+        Args:
+            outputs: (Iterable[MultivariateNormal]) - the outputs of the latent function
+            targets: (Iterable[Tensor]) - the target values
+            params: (Iterable[Iterable[Tensor]]) - the arguments to be passed through
+                (e.g. parameters in case of heteroskedastic likelihoods)
+        """
+        if len(params) == 0:
+            sum_mll = sum(mll(output, target) for mll, output, target in zip(self.mlls, outputs, targets))
+        else:
+            sum_mll = sum(
+                mll(output, target, *iparams)
+                for mll, output, target, iparams in zip(self.mlls, outputs, targets, params)
+            )
+        return sum_mll.div_(len(self.mlls))
diff --git a/gpytorch/models/__init__.py b/gpytorch/models/__init__.py
index 98277e3d..449ed88c 100644
--- a/gpytorch/models/__init__.py
+++ b/gpytorch/models/__init__.py
@@ -1,11 +1,13 @@
 #!/usr/bin/env python3
 
 from .gp import GP
+from .abstract_variational_gp import AbstractVariationalGP
+from .additive_grid_inducing_variational_gp import AdditiveGridInducingVariationalGP
 from .exact_gp import ExactGP
-from .variational_gp import VariationalGP
 from .grid_inducing_variational_gp import GridInducingVariationalGP
-from .additive_grid_inducing_variational_gp import AdditiveGridInducingVariationalGP
-from .abstract_variational_gp import AbstractVariationalGP
+from .model_list import AbstractModelList, IndependentModelList
+from .variational_gp import VariationalGP
+
 
 try:
     from .pyro_variational_gp import PyroVariationalGP
@@ -17,11 +19,13 @@ except ImportError:
 
 
 __all__ = [
-    "GP",
+    "AbstractModelList",
+    "AbstractVariationalGP",
+    "AdditiveGridInducingVariationalGP",
     "ExactGP",
-    "VariationalGP",
+    "GP",
     "GridInducingVariationalGP",
-    "AdditiveGridInducingVariationalGP",
-    "AbstractVariationalGP",
+    "IndependentModelList",
     "PyroVariationalGP",
+    "VariationalGP",
 ]
diff --git a/gpytorch/models/model_list.py b/gpytorch/models/model_list.py
new file mode 100644
index 00000000..af97d2a2
--- /dev/null
+++ b/gpytorch/models/model_list.py
@@ -0,0 +1,67 @@
+#! /usr/bin/env python3
+
+from abc import ABC, abstractproperty
+
+import torch
+from gpytorch.likelihoods import LikelihoodList
+from gpytorch.models import GP
+from torch.nn import ModuleList
+
+
+class AbstractModelList(GP, ABC):
+    @abstractproperty
+    def num_outputs(self):
+        """The model's number of outputs"""
+        pass
+
+    def forward_i(self, i, *args, **kwargs):
+        """Forward restricted to the i-th output only"""
+        raise NotImplementedError
+
+    def likelihood_i(self, i, *args):
+        """Evaluate likelihood of the i-th output only"""
+        raise NotImplementedError
+
+
+class IndependentModelList(AbstractModelList):
+    def __init__(self, *models):
+        super().__init__()
+        self.models = ModuleList(models)
+        for m in models:
+            if not hasattr(m, "likelihood"):
+                raise ValueError(
+                    "IndependentModelList currently only supports models that have a likelihood (e.g. ExactGPs)"
+                )
+        self.likelihood = LikelihoodList(*[m.likelihood for m in models])
+
+    @property
+    def num_outputs(self):
+        return len(self.models)
+
+    def forward_i(self, i, *args, **kwargs):
+        return self.models[i].forward(*args, **kwargs)
+
+    def likelihood_i(self, i, *args):
+        return self.likelihood.likelihoods[i](*args)
+
+    def forward(self, *args, **kwargs):
+        [model.forward(*args_, **kwargs) for model, args_ in zip(self.models, _get_tensor_args(*args))]
+
+    def __call__(self, *args, **kwargs):
+        return [model.__call__(*args_, **kwargs) for model, args_ in zip(self.models, _get_tensor_args(*args))]
+
+    @property
+    def train_inputs(self):
+        return [model.train_inputs for model in self.models]
+
+    @property
+    def train_targets(self):
+        return [model.train_targets for model in self.models]
+
+
+def _get_tensor_args(*args):
+    for arg in args:
+        if torch.is_tensor(arg):
+            yield (arg,)
+        else:
+            yield arg
diff --git a/gpytorch/module.py b/gpytorch/module.py
index 805c9748..7aa1044f 100644
--- a/gpytorch/module.py
+++ b/gpytorch/module.py
@@ -18,26 +18,9 @@ class Module(nn.Module):
 
     def __call__(self, *inputs, **kwargs):
         outputs = self.forward(*inputs, **kwargs)
-
-        if isinstance(outputs, tuple):
-            if not all(
-                torch.is_tensor(output) or isinstance(output, Distribution) or isinstance(output, LazyTensor)
-                for output in outputs
-            ):
-                raise RuntimeError(
-                    "All outputs must be a Distribution, torch.Tensor, or LazyTensor. "
-                    "Got {}".format([output.__class__.__name__ for output in outputs])
-                )
-            if len(outputs) == 1:
-                outputs = outputs[0]
-            return outputs
-
-        elif torch.is_tensor(outputs) or isinstance(outputs, Distribution) or isinstance(outputs, LazyTensor):
-            return outputs
-        else:
-            raise RuntimeError(
-                "Output must be a Distribution, torch.Tensor, or LazyTensor. Got {}".format(outputs.__class__.__name__)
-            )
+        if isinstance(outputs, list):
+            return [_validate_module_outputs(output) for output in outputs]
+        return _validate_module_outputs(outputs)
 
     def _get_module_and_name(self, parameter_name):
         """Get module and name from full parameter name."""
@@ -232,6 +215,27 @@ class Module(nn.Module):
                 raise e
 
 
+def _validate_module_outputs(outputs):
+    if isinstance(outputs, tuple):
+        if not all(
+            torch.is_tensor(output) or isinstance(output, Distribution) or isinstance(output, LazyTensor)
+            for output in outputs
+        ):
+            raise RuntimeError(
+                "All outputs must be a Distribution, torch.Tensor, or LazyTensor. "
+                "Got {}".format([output.__class__.__name__ for output in outputs])
+            )
+        if len(outputs) == 1:
+            outputs = outputs[0]
+        return outputs
+    elif torch.is_tensor(outputs) or isinstance(outputs, Distribution) or isinstance(outputs, LazyTensor):
+        return outputs
+    else:
+        raise RuntimeError(
+            "Output must be a Distribution, torch.Tensor, or LazyTensor. Got {}".format(outputs.__class__.__name__)
+        )
+
+
 def _extract_named_added_loss_terms(module, memo=None, prefix=""):
     if memo is None:
         memo = set()


Test patch:
diff --git a/test/examples/test_model_list_gp_regression.py b/test/examples/test_model_list_gp_regression.py
new file mode 100644
index 00000000..c4ccd870
--- /dev/null
+++ b/test/examples/test_model_list_gp_regression.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python3
+
+import math
+import unittest
+
+import gpytorch
+import torch
+from gpytorch.distributions import MultivariateNormal
+from gpytorch.kernels import RBFKernel, ScaleKernel
+from gpytorch.likelihoods import GaussianLikelihood, LikelihoodList
+from gpytorch.means import ConstantMean
+from gpytorch.mlls import SumMarginalLogLikelihood
+from gpytorch.models import IndependentModelList
+from gpytorch.priors import SmoothedBoxPrior
+
+
+class ExactGPModel(gpytorch.models.ExactGP):
+    def __init__(self, train_inputs, train_targets, likelihood):
+        super(ExactGPModel, self).__init__(train_inputs, train_targets, likelihood)
+        self.mean_module = ConstantMean(prior=SmoothedBoxPrior(-1, 1))
+        self.covar_module = ScaleKernel(
+            RBFKernel(lengthscale_prior=SmoothedBoxPrior(math.exp(-3), math.exp(3), sigma=0.1))
+        )
+
+    def forward(self, x):
+        mean_x = self.mean_module(x)
+        covar_x = self.covar_module(x)
+        return MultivariateNormal(mean_x, covar_x)
+
+
+class TestModelListGPRegression(unittest.TestCase):
+    def test_simple_model_list_gp_regression(self, cuda=False):
+        train_x1 = torch.linspace(0, 0.95, 25) + 0.05 * torch.rand(25)
+        train_x2 = torch.linspace(0, 0.95, 15) + 0.05 * torch.rand(15)
+
+        train_y1 = torch.sin(train_x1 * (2 * math.pi)) + 0.2 * torch.randn_like(train_x1)
+        train_y2 = torch.cos(train_x2 * (2 * math.pi)) + 0.2 * torch.randn_like(train_x2)
+
+        likelihood1 = GaussianLikelihood()
+        model1 = ExactGPModel(train_x1, train_y1, likelihood1)
+
+        likelihood2 = GaussianLikelihood()
+        model2 = ExactGPModel(train_x2, train_y2, likelihood2)
+
+        model = IndependentModelList(model1, model2)
+        likelihood = LikelihoodList(model1.likelihood, model2.likelihood)
+
+        if cuda:
+            model = model.cuda()
+
+        model.train()
+        likelihood.train()
+
+        mll = SumMarginalLogLikelihood(likelihood, model)
+
+        optimizer = torch.optim.Adam([{"params": model.parameters()}], lr=0.1)
+
+        for _ in range(10):
+            optimizer.zero_grad()
+            output = model(*model.train_inputs)
+            loss = -mll(output, model.train_targets)
+            loss.backward()
+            optimizer.step()
+
+        model.eval()
+        likelihood.eval()
+
+        with torch.no_grad(), gpytorch.settings.fast_pred_var():
+            test_x = torch.linspace(0, 1, 10, device=torch.device("cuda") if cuda else torch.device("cpu"))
+            outputs_f = model(test_x, test_x)
+            predictions_obs_noise = likelihood(*outputs_f)
+
+        self.assertIsInstance(outputs_f, list)
+        self.assertEqual(len(outputs_f), 2)
+        self.assertIsInstance(predictions_obs_noise, list)
+        self.assertEqual(len(predictions_obs_noise), 2)
+
+    def test_simple_model_list_gp_regression_cuda(self):
+        if torch.cuda.is_available():
+            self.test_simple_model_list_gp_regression(cuda=True)
+
+
+if __name__ == "__main__":
+    unittest.main()