diff --git a/docs/tutorial.ipynb b/docs/tutorial.ipynb
new file mode 100644
index 0000000..ab03686
--- /dev/null
+++ b/docs/tutorial.ipynb
@@ -0,0 +1,814 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "Nj5SX4ilokxS"
+ },
+ "source": [
+ "# Tutorial\n",
+ "\n",
+ "Let's consider a toy problem. Here we consider a signal $\\mathsf{x^\\star \\in \\mathbb{R}^n}$ where $\\mathsf{x^\\star = M s}$ with $\\mathsf{M \\in \\mathbb{R}^{n\\times r}}$ and $\\mathsf{s\\in\\mathbb{R}^r}$. The vector $\\mathsf{s}$ is generated as the element-wise product of a Gaussian random vector and a Bernoulli vector ($\\mathsf{p}$ the probability that each entry is nonzero), each vector with i.i.d. entries.\n",
+ "\n",
+ "\n",
+ "Access is given to linear measurements $ \\mathsf{d = A x^\\star}, $ where $\\mathsf{A\\in\\mathbb{R}^{m\\times n}}$ is a matrix with normalized columns (up to numerical tolerance). The task at hand is to\n",
+ "\n",
+ "$$\\mathsf{ Find\\ x^\\star\\ given\\ d\\ and\\ A}.$$\n",
+ "\n",
+ "Using the fact that $\\mathsf{p\\cdot r\\ll n}$, we know $\\mathsf{x^\\star}$ admits a sparse representation. Thus, we estimate\n",
+ "\n",
+ "$$ \\mathsf{x^\\star \\approx argmin_{x} \\ \\|K x\\|_1 \\ \\ \\mbox{s.t.}\\ \\ Ax=d.} $$\n",
+ "\n",
+ "In this case, the implicit L2O model takes as input $\\mathsf{d}$ and outputs an inference via\n",
+ "\n",
+ "$$ \\mathsf{{N_{\\theta}}(d) = argmin_{x} \\ \\|K x\\|_1 \\ \\ \\mbox{s.t.}\\ \\ Ax=d}.$$\n",
+ "\n",
+ "
\n",
+ "\n",
+ "Throughout, we take $\\mathsf{m=100}$, $\\mathsf{n=250}$, $\\mathsf{r=50}$, and $\\mathsf{p=0.1}$.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "First, we import various utilities and mount Google drive (where this notebook was executed)."
+ ],
+ "metadata": {
+ "id": "PWpjPG3qvPmR"
+ }
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "id": "hoWeV9idVIe0",
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "outputId": "18d44a10-66a1-4bf4-e60b-f8fc8c0137a4"
+ },
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "Mounted at /content/drive\n",
+ "+-------------------+--------------+\n",
+ "| Network Component | # Parameters |\n",
+ "+-------------------+--------------+\n",
+ "| K | 62500 |\n",
+ "| TOTAL | 62500 |\n",
+ "+-------------------+--------------+\n"
+ ]
+ }
+ ],
+ "source": [
+ "import os\n",
+ "import sys\n",
+ "from google.colab import drive\n",
+ "drive.mount('/content/drive')\n",
+ "sys.path.append('/content/drive/MyDrive/xai-l2o/src/')\n",
+ "save_dir = './drive/MyDrive/xai-l2o/'\n",
+ "\n",
+ "# from certificate import CertificateModel, CertificateEnsemble\n",
+ "from utils import solve_least_squares, create_dict_loaders\n",
+ "from utils import plot_dict_signal, print_model_params\n",
+ "from models import ImpDictModel\n",
+ "import scipy.io\n",
+ "import numpy as np\n",
+ "import torch\n",
+ "import torch.nn as nn\n",
+ "from torch.utils.data import TensorDataset, DataLoader\n",
+ "from torch.utils.data.dataset import random_split\n",
+ "\n",
+ "torch.manual_seed(31415)\n",
+ "\n",
+ "loader_train, loader_test, A = create_dict_loaders()\n",
+ "\n",
+ "max_epoch = 75\n",
+ "device = 'cuda:0'\n",
+ "model = ImpDictModel(A)\n",
+ "model = model.to(device=device)\n",
+ "criterion = nn.MSELoss()\n",
+ "file_name = save_dir + 'weights/dictionary_model_weights.pth'\n",
+ "\n",
+ "loss_best = 1.0e10\n",
+ "MSE_ave = 0.0\n",
+ "learning_rate = 4.0e-5\n",
+ "max_depth_train = 400\n",
+ "max_depth_test = 2000\n",
+ "optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate, weight_decay=1.0e-5)\n",
+ "training_msg = '[{:5d}] train loss = {:2.3e} | depth = {:3.0f} | lr = {:2.3e}'\n",
+ "training_msg += ' | K 2-norm = {:2.3e}'\n",
+ "\n",
+ "model.to(device)\n",
+ "\n",
+ "print_model_params(model)\n",
+ "\n",
+ "load_weights = False\n",
+ "if load_weights:\n",
+ " state = torch.load(file_name, map_location=torch.device(device))\n",
+ " model.load_state_dict(state['model_state_dict'])\n",
+ " print('Loaded model from file.')\n",
+ " epochs_adm = 0"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Model Training\n",
+ "With the model loaded, we next train it to predict $\\mathsf{x^\\star}$ from $\\mathsf{d}$. We use the Adam optimizer and print samples from test data every few epochs to give intuition for how well the parameters are tuned."
+ ],
+ "metadata": {
+ "id": "BFF3WvCiDHE9"
+ }
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "id": "_VND6CPwVOYh",
+ "colab": {
+ "base_uri": "https://localhost:8080/",
+ "height": 1000
+ },
+ "outputId": "54d541c3-a903-4933-ba7a-0c762b07846b"
+ },
+ "outputs": [
+ {
+ "output_type": "display_data",
+ "data": {
+ "text/plain": [
+ "