{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "8aa08564",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"torch : 1.10.1\n",
"pytorch_lightning: 1.6.0.dev0\n",
"torchmetrics : 0.6.2\n",
"matplotlib : 3.3.4\n",
"coral_pytorch : 1.2.0\n",
"\n"
]
}
],
"source": [
"%load_ext watermark\n",
"%watermark -p torch,pytorch_lightning,torchmetrics,matplotlib,coral_pytorch"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "d8c56c2e",
"metadata": {},
"outputs": [],
"source": [
"%load_ext pycodestyle_magic\n",
"%flake8_on --ignore W291,W293,E703"
]
},
{
"cell_type": "markdown",
"id": "a2ea3b1f",
"metadata": {},
"source": [
"
\n",
"\n",
"# Binary extension MLP for ordinal regression and deep learning -- cement strength dataset"
]
},
{
"cell_type": "markdown",
"id": "4b7fe9ae",
"metadata": {},
"source": [
"This tutorial explains how to train a deep neural network (here: multilayer perceptron) with the binary extension method by Niu at al. 2016 for ordinal regression. \n",
"\n",
"**Paper reference:**\n",
"\n",
"- Niu, Zhenxing, Mo Zhou, Le Wang, Xinbo Gao, and Gang Hua. \"[Ordinal regression with multiple output cnn for age estimation](https://openaccess.thecvf.com/content_cvpr_2016/papers/Niu_Ordinal_Regression_With_CVPR_2016_paper.pdf).\" In Proceedings of the IEEE conference on computer vision and pattern recognition."
]
},
{
"cell_type": "markdown",
"id": "1a91c5cb",
"metadata": {},
"source": [
"**Note:**\n",
" \n",
"To keep the notation lean and minimal, this notebook only contains \"Squared-error reformulation\"-specific comments. For more comments on the PyTorch Lightning use, please see the cross-entropy baseline notebook [baseline-light_cement.ipynb](./baseline-light_cement.ipynb)"
]
},
{
"cell_type": "markdown",
"id": "a8f10f9e",
"metadata": {},
"source": [
"## General settings and hyperparameters"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "558765e6",
"metadata": {},
"outputs": [],
"source": [
"BATCH_SIZE = 32\n",
"NUM_EPOCHS = 200\n",
"LEARNING_RATE = 0.01\n",
"NUM_WORKERS = 0\n",
"\n",
"DATA_BASEPATH = \".\""
]
},
{
"cell_type": "markdown",
"id": "455a429b",
"metadata": {},
"source": [
"## Converting a regular classifier into a Niu et al. ordinal regression model"
]
},
{
"cell_type": "markdown",
"id": "e9607c53",
"metadata": {},
"source": [
"Changing a classifier to a binary extension model (as proposed by Niu et al.) for ordinal regression is actually really simple and only requires a few changes:\n",
"\n",
"**1)**\n",
"We replace the output layer \n",
"\n",
"```python\n",
"output_layer = torch.nn.Linear(hidden_units[-1], num_classes)\n",
"```\n",
"\n",
"by a CORAL layer (available through `coral_pytorch`):\n",
"\n",
"```python\n",
"output_layer = torch.nn.Linear(size_in=hidden_units[-1], num_classes=num_classes-1*2)`\n",
"```\n",
"\n",
"**2)**\n",
"\n",
"Convert the integer class labels into the extended binary label format using the `levels_from_labelbatch` provided via `coral_pytorch`:\n",
"\n",
"```python\n",
"levels = levels_from_labelbatch(class_labels, \n",
" num_classes=num_classes)\n",
"```\n",
"\n",
"**3)** \n",
"\n",
"Swap the cross entropy loss from PyTorch,\n",
"\n",
"```python\n",
"torch.nn.functional.cross_entropy(logits, true_labels)\n",
"```\n",
"\n",
"with a new loss function:\n",
"\n",
"```python\n",
"def niu_et_al_loss(logits, levels):\n",
" val = (-torch.sum((F.log_softmax(logits, dim=2)[:, :, 1]*levels\n",
" + F.log_softmax(logits, dim=2)[:, :, 0]*(1-levels)), dim=1))\n",
" return torch.mean(val)\n",
"\n",
"loss = niu_et_al_loss(logits, levels)\n",
"```\n",
"\n",
"**4)**\n",
"\n",
"In a regular classifier, we usually obtain the predicted class labels as follows:\n",
"\n",
"```python\n",
"predicted_labels = torch.argmax(logits, dim=1)\n",
"```\n",
"\n",
"Replace this with the following code to convert the predicted probabilities into the predicted labels:\n",
"\n",
"```python\n",
"\n",
"def niu_logits_to_labels(logits):\n",
" probas = F.softmax(logits, dim=2)[:, :, 1]\n",
" predict_levels = probas > 0.5\n",
" predicted_labels = torch.sum(predict_levels, dim=1)\n",
" return predicted_labels\n",
"\n",
"\n",
"predicted_labels = niu_logits_to_labels(logits)\n",
"```"
]
},
{
"cell_type": "markdown",
"id": "4c569076",
"metadata": {},
"source": [
"## Implementing a `MultiLayerPerceptron` using PyTorch Lightning's `LightningModule`"
]
},
{
"cell_type": "markdown",
"id": "a14c5681",
"metadata": {},
"source": [
"\n",
"- Given a multilayer perceptron classifier with cross entropy loss, it is very easy to change this classifier into a ordinal regression model using the extended binary method as explained in the previous section. In the code example below, we just change the output layer:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "7c907a9c",
"metadata": {},
"outputs": [],
"source": [
"import torch\n",
"\n",
"\n",
"# Regular PyTorch Module\n",
"class MultiLayerPerceptron(torch.nn.Module):\n",
" def __init__(self, input_size, hidden_units, num_classes):\n",
" super().__init__()\n",
"\n",
" # num_classes is used by the CORAL loss function\n",
" self.num_classes = num_classes\n",
" \n",
" # Initialize MLP layers\n",
" all_layers = []\n",
" for hidden_unit in hidden_units:\n",
" layer = torch.nn.Linear(input_size, hidden_unit)\n",
" all_layers.append(layer)\n",
" all_layers.append(torch.nn.ReLU())\n",
" input_size = hidden_unit\n",
"\n",
" # Modify output layer -------------------------------------------\n",
" # Regular classifier would use the following output layer:\n",
" # output_layer = torch.nn.Linear(hidden_units[-1], num_classes)\n",
" output_layer = torch.nn.Linear(hidden_units[-1], (num_classes-1)*2)\n",
" # ----------------------------------------------------------------\n",
" \n",
" all_layers.append(output_layer)\n",
" self.model = torch.nn.Sequential(*all_layers)\n",
" \n",
" def forward(self, x):\n",
" x = self.model(x)\n",
" \n",
" # Reshape logits for Niu et al. loss\n",
" x = x.view(-1, (self.num_classes-1), 2)\n",
" # -------------------------------------------------------------------### \n",
" return x"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "a5247f4d",
"metadata": {},
"outputs": [],
"source": [
"from coral_pytorch.dataset import levels_from_labelbatch\n",
"\n",
"import torch.nn.functional as F\n",
"import pytorch_lightning as pl\n",
"import torchmetrics\n",
"\n",
"\n",
"def niu_et_al_loss(logits, levels):\n",
" val = -torch.sum(\n",
" (\n",
" F.log_softmax(logits, dim=2)[:, :, 1] * levels\n",
" + F.log_softmax(logits, dim=2)[:, :, 0] * (1 - levels)\n",
" ),\n",
" dim=1,\n",
" )\n",
" return torch.mean(val)\n",
"\n",
"\n",
"def niu_logits_to_labels(logits):\n",
" probas = F.softmax(logits, dim=2)[:, :, 1]\n",
" predict_levels = probas > 0.5\n",
" predicted_labels = torch.sum(predict_levels, dim=1)\n",
" return predicted_labels\n",
"\n",
"\n",
"class LightningMLP(pl.LightningModule):\n",
" def __init__(self, model, learning_rate):\n",
" super().__init__()\n",
"\n",
" self.learning_rate = learning_rate\n",
" self.model = model\n",
"\n",
" self.save_hyperparameters(ignore=['model'])\n",
"\n",
" self.train_mae = torchmetrics.MeanAbsoluteError()\n",
" self.valid_mae = torchmetrics.MeanAbsoluteError()\n",
" self.test_mae = torchmetrics.MeanAbsoluteError()\n",
" \n",
" def forward(self, x):\n",
" return self.model(x)\n",
" \n",
" def _shared_step(self, batch):\n",
" features, true_labels = batch\n",
" \n",
" # Convert class labels to the extended binary labels ---\n",
" levels = levels_from_labelbatch(\n",
" true_labels, num_classes=self.model.num_classes)\n",
" # -------------------------------------------------------\n",
"\n",
" logits = self(features)\n",
"\n",
" # Custom Loss --------------------------------------------\n",
" # A regular classifier uses:\n",
" # loss = torch.nn.functional.cross_entropy(logits, true_labels)\n",
" loss = niu_et_al_loss(logits, levels.type_as(logits))\n",
" # -------------------------------------------------------\n",
"\n",
" # Prediction to label -----------------------------------\n",
" # A regular classifier uses:\n",
" # predicted_labels = torch.argmax(logits, dim=1)\n",
" predicted_labels = niu_logits_to_labels(logits)\n",
" # -------------------------------------------------------\n",
" return loss, true_labels, predicted_labels\n",
"\n",
" def training_step(self, batch, batch_idx):\n",
" loss, true_labels, predicted_labels = self._shared_step(batch)\n",
" self.log(\"train_loss\", loss)\n",
" self.train_mae(predicted_labels, true_labels)\n",
" self.log(\"train_mae\", self.train_mae, on_epoch=True, on_step=False)\n",
" return loss\n",
"\n",
" def validation_step(self, batch, batch_idx):\n",
" loss, true_labels, predicted_labels = self._shared_step(batch)\n",
" self.log(\"valid_loss\", loss)\n",
" self.valid_mae(predicted_labels, true_labels)\n",
" self.log(\"valid_mae\", self.valid_mae,\n",
" on_epoch=True, on_step=False, prog_bar=True)\n",
"\n",
" def test_step(self, batch, batch_idx):\n",
" _, true_labels, predicted_labels = self._shared_step(batch)\n",
" self.test_mae(predicted_labels, true_labels)\n",
" self.log(\"test_mae\", self.test_mae, on_epoch=True, on_step=False)\n",
"\n",
" def configure_optimizers(self):\n",
" optimizer = torch.optim.Adam(self.parameters(), lr=self.learning_rate)\n",
" return optimizer"
]
},
{
"cell_type": "markdown",
"id": "9083eeb8",
"metadata": {},
"source": [
"---\n",
"\n",
"# Note: There Are No Changes Compared To The Baseline Below\n",
"\n",
"---"
]
},
{
"cell_type": "markdown",
"id": "f7959fab",
"metadata": {},
"source": [
"## Setting up the dataset"
]
},
{
"cell_type": "markdown",
"id": "cb6a5846",
"metadata": {},
"source": [
"### Inspecting the dataset"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "a4056461",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"
| \n", " | response | \n", "V1 | \n", "V2 | \n", "V3 | \n", "V4 | \n", "V5 | \n", "V6 | \n", "V7 | \n", "V8 | \n", "
|---|---|---|---|---|---|---|---|---|---|
| 0 | \n", "4 | \n", "540.0 | \n", "0.0 | \n", "0.0 | \n", "162.0 | \n", "2.5 | \n", "1040.0 | \n", "676.0 | \n", "28 | \n", "
| 1 | \n", "4 | \n", "540.0 | \n", "0.0 | \n", "0.0 | \n", "162.0 | \n", "2.5 | \n", "1055.0 | \n", "676.0 | \n", "28 | \n", "
| 2 | \n", "2 | \n", "332.5 | \n", "142.5 | \n", "0.0 | \n", "228.0 | \n", "0.0 | \n", "932.0 | \n", "594.0 | \n", "270 | \n", "
| 3 | \n", "2 | \n", "332.5 | \n", "142.5 | \n", "0.0 | \n", "228.0 | \n", "0.0 | \n", "932.0 | \n", "594.0 | \n", "365 | \n", "
| 4 | \n", "2 | \n", "198.6 | \n", "132.4 | \n", "0.0 | \n", "192.0 | \n", "0.0 | \n", "978.4 | \n", "825.5 | \n", "360 | \n", "
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n",
"┃ Test metric ┃ DataLoader 0 ┃\n",
"┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n",
"│ test_mae │ 0.3199999928474426 │\n",
"└───────────────────────────┴───────────────────────────┘\n",
"\n"
],
"text/plain": [
"┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n",
"┃\u001b[1m \u001b[0m\u001b[1m Test metric \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1m DataLoader 0 \u001b[0m\u001b[1m \u001b[0m┃\n",
"┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n",
"│\u001b[36m \u001b[0m\u001b[36m test_mae \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35m 0.3199999928474426 \u001b[0m\u001b[35m \u001b[0m│\n",
"└───────────────────────────┴───────────────────────────┘\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/plain": [
"[{'test_mae': 0.3199999928474426}]"
]
},
"execution_count": 15,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"trainer.test(model=lightning_model, datamodule=data_module, ckpt_path='best')"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"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.8.12"
}
},
"nbformat": 4,
"nbformat_minor": 5
}