{
"cells": [
{
"cell_type": "markdown",
"id": "196ef2df",
"metadata": {},
"source": [
"\n",
"\n",
"\n",
"\n",
"\n",
"\n",
"\n",
"\n",
"\n",
"\n",
"\n",
"\n",
"\n",
"\n",
"\n",
"\n",
"\n",
"# Loss functions\n",
"\n",
"Loss functions are used to train neural networks and to compute the difference between output and target variable. A critical component of training neural networks is the loss function. A loss function is a quantative measure of how bad the predictions of the network are when compared to ground truth labels. Given this score, a network can improve by iteratively updating its weights to minimise this loss. Some tasks use a combination of multiple loss functions, but often you'll just use one. MXNet Gluon provides a number of the most commonly used loss functions, and you'll choose certain loss functions depending on your network and task. Some common task and loss function pairs include:\n",
"\n",
"- regression: [L1Loss](/api/python/docs/api/gluon/loss/index.html#mxnet.gluon.loss.L1Loss), [L2Loss](/api/python/docs/api/gluon/loss/index.html#mxnet.gluon.loss.L2Loss) \n",
"- classification: [SigmoidBinaryCrossEntropyLoss](/api/python/docs/api/gluon/loss/index.html#mxnet.gluon.loss.SigmoidBinaryCrossEntropyLoss), [SoftmaxCrossEntropyLoss](/api/python/docs/api/gluon/loss/index.html#mxnet.gluon.loss.SoftmaxCrossEntropyLoss)\n",
"- embeddings: [HingeLoss](/api/python/docs/api/gluon/_autogen/mxnet.gluon.loss.HingeLoss.html)\n",
"\n",
"We'll first import the modules, where the `mxnet.gluon.loss` module is imported as `gloss` to avoid the commonly used name `loss`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "fe3ffebd",
"metadata": {},
"outputs": [],
"source": [
"from IPython import display\n",
"from matplotlib import pyplot as plt\n",
"import mxnet as mx\n",
"from mxnet import nd, autograd\n",
"from mxnet.gluon import nn, loss as gloss"
]
},
{
"cell_type": "markdown",
"id": "3de90bb5",
"metadata": {},
"source": [
"## Basic Usages\n",
"\n",
"Now let's create an instance of the $\\ell_2$ loss, which is commonly used in regression tasks."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b1e9fbce",
"metadata": {},
"outputs": [],
"source": [
"loss = gloss.L2Loss()"
]
},
{
"cell_type": "markdown",
"id": "5575e405",
"metadata": {},
"source": [
"And then feed two inputs to compute the elementwise loss values."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6d7f2284",
"metadata": {},
"outputs": [],
"source": [
"x = nd.ones((2,))\n",
"y = nd.ones((2,)) * 2\n",
"loss(x, y)"
]
},
{
"cell_type": "markdown",
"id": "7a275fff",
"metadata": {},
"source": [
"These values should be equal to the math definition: $0.5\\|x-y\\|^2$."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c80193d1",
"metadata": {},
"outputs": [],
"source": [
".5 * (x - y)**2"
]
},
{
"cell_type": "markdown",
"id": "338242b5",
"metadata": {},
"source": [
"Next we show how to use a loss function to compute gradients."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e64bdb89",
"metadata": {},
"outputs": [],
"source": [
"X = nd.random.uniform(shape=(2, 4))\n",
"net = nn.Dense(1)\n",
"net.initialize()\n",
"with autograd.record():\n",
" l = loss(net(X), y)\n",
"print(l)"
]
},
{
"cell_type": "markdown",
"id": "95c8c134",
"metadata": {},
"source": [
"We can compute the gradients w.r.t. the loss function."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8c28d02e",
"metadata": {},
"outputs": [],
"source": [
"l.backward()\n",
"print(net.weight.grad())"
]
},
{
"cell_type": "markdown",
"id": "93b0d3d3",
"metadata": {},
"source": [
"## Loss functions\n",
"\n",
"Most commonly used loss functions can be divided into 2 categories: regression and classification.\n",
"\n",
"Let's first visualize several regression losses. We visualize the loss values versus the predicted values with label values fixed to be 0."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a13fc4e7",
"metadata": {},
"outputs": [],
"source": [
"def plot(x, y):\n",
" display.set_matplotlib_formats('svg')\n",
" plt.plot(x.asnumpy(), y.asnumpy())\n",
" plt.xlabel('x')\n",
" plt.ylabel('loss')\n",
" plt.show()\n",
"\n",
"def show_regression_loss(loss):\n",
" x = nd.arange(-5, 5, .1)\n",
" y = loss(x, nd.zeros_like(x))\n",
" plot(x, y)\n"
]
},
{
"cell_type": "markdown",
"id": "d4309623",
"metadata": {},
"source": [
"Then plot the classification losses with label values fixed to be 1."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3b4c22e0",
"metadata": {},
"outputs": [],
"source": [
"def show_classification_loss(loss):\n",
" x = nd.arange(-5, 5, .1)\n",
" y = loss(x, nd.ones_like(x))\n",
" plot(x, y)"
]
},
{
"cell_type": "markdown",
"id": "9199780a",
"metadata": {},
"source": [
"#### [L1 Loss](https://mxnet.apache.org/api/python/gluon/loss.html#mxnet.gluon.loss.L1Loss)\n",
"\n",
"L1 Loss, also called Mean Absolute Error, computes the sum of absolute distance between target values and the output of the neural network. It is defined as:\n",
"\n",
"$$ L = \\sum_i \\vert {label}_i - {pred}_i \\vert. $$\n",
"\n",
"It is a non-smooth function that can lead to non-convergence. It creates the same gradient for small and large loss values, which can be problematic for the learning process."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5a351506",
"metadata": {},
"outputs": [],
"source": [
"show_regression_loss(gloss.L1Loss())"
]
},
{
"cell_type": "markdown",
"id": "dfbac65f",
"metadata": {},
"source": [
"#### [L2 Loss](https://mxnet.apache.org/api/python/gluon/loss.html#mxnet.gluon.loss.L2Loss)\n",
"\n",
"L2Loss, also called Mean Squared Error, is a regression loss function that computes the squared distances between the target values and the output of the neural network. It is defined as:\n",
"\n",
"$$ L = \\frac{1}{2} \\sum_i \\vert {label}_i - {pred}_i \\vert^2. $$\n",
"\n",
"Compared to L1, L2 loss it is a smooth function and it creates larger gradients for large loss values. However due to the squaring it puts high weight on outliers."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f7454143",
"metadata": {},
"outputs": [],
"source": [
"show_regression_loss(gloss.L2Loss())"
]
},
{
"cell_type": "markdown",
"id": "09a2b6db",
"metadata": {},
"source": [
"#### [Huber Loss](https://mxnet.apache.org/api/python/gluon/loss.html#mxnet.gluon.loss.HuberLosss)\n",
"\n",
"HuberLoss combines advantages of L1 and L2 loss. It calculates a smoothed L1 loss that is equal to L1 if the absolute error exceeds a threshold $$\\rho$$, otherwise it is equal to L2. It is defined as:\n",
"$$\n",
"\\begin{split}L = \\sum_i \\begin{cases} \\frac{1}{2 {rho}} ({label}_i - {pred}_i)^2 &\n",
" \\text{ if } |{label}_i - {pred}_i| < {rho} \\\\\n",
" |{label}_i - {pred}_i| - \\frac{{rho}}{2} &\n",
" \\text{ otherwise }\n",
" \\end{cases}\\end{split}\n",
"$$"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c6cb9f14",
"metadata": {},
"outputs": [],
"source": [
"show_regression_loss(gloss.HuberLoss(rho=1))"
]
},
{
"cell_type": "markdown",
"id": "9140d87c",
"metadata": {},
"source": [
"An example of where Huber Loss is used can be found in [Deep Q Network](https://openai.com/blog/openai-baselines-dqn/).\n",
"\n",
"#### [Cross Entropy Loss with Sigmoid](https://mxnet.apache.org/api/python/gluon/loss.html#mxnet.gluon.loss.SigmoidBinaryCrossEntropyLoss)\n",
"\n",
"Binary Cross Entropy is a loss function used for binary classification problems e.g. classifying images into 2 classes. Cross entropy measures the difference between two probability distributions and it is defined as:\n",
"$$\\sum_i -{(y\\log(p) + (1 - y)\\log(1 - p))} $$\n",
"Before the loss is computed a sigmoid activation is applied per default. If your network has `sigmoid` activation as last layer, then you need set ```from_sigmoid``` to False, to avoid applying the sigmoid function twice."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "91bdaa3e",
"metadata": {},
"outputs": [],
"source": [
"show_classification_loss(gloss.SigmoidBinaryCrossEntropyLoss())"
]
},
{
"cell_type": "markdown",
"id": "4bb6811b",
"metadata": {},
"source": [
"#### [Cross Entropy Loss with Softmax](https://mxnet.apache.org/api/python/gluon/loss.html#mxnet.gluon.loss.SoftmaxCrossEntropyLoss)\n",
"\n",
"In classification, we often apply the\n",
"softmax operator to the predicted outputs to obtain prediction probabilities,\n",
"and then apply the cross entropy loss against the true labels:\n",
"\n",
"$$ \\begin{align}\\begin{aligned}p = \\text{softmax}({pred})\\\\L = -\\sum_i \\sum_j {label}_j \\log p_{ij}\\end{aligned}\\end{align}\n",
"$$\n",
"\n",
"Running these two steps one-by-one, however, may lead to numerical instabilities. The `loss` module provides a single operators with softmax and cross entropy fused to avoid such problem."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "04310ca7",
"metadata": {},
"outputs": [],
"source": [
"loss = gloss.SoftmaxCrossEntropyLoss()\n",
"x = nd.array([[1, 10], [8, 2]])\n",
"y = nd.array([0, 1])\n",
"loss(x, y)"
]
},
{
"cell_type": "markdown",
"id": "bdb96f92",
"metadata": {},
"source": [
"#### [Hinge Loss](https://mxnet.apache.org/api/python/gluon/loss.html#mxnet.gluon.loss.HingeLoss)\n",
"\n",
"Commonly used in Support Vector Machines (SVMs), Hinge Loss is used to additionally penalize predictions that are correct but fall within a margin between classes (the region around a decision boundary). Unlike `SoftmaxCrossEntropyLoss`, it's rarely used for neural network training. It is defined as:\n",
"\n",
"$$\n",
"L = \\sum_i max(0, {margin} - {pred}_i \\cdot {label}_i)\n",
"$$"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "26362e58",
"metadata": {},
"outputs": [],
"source": [
"show_classification_loss(gloss.HingeLoss())"
]
},
{
"cell_type": "markdown",
"id": "88b4c69d",
"metadata": {},
"source": [
"#### [Logistic Loss](/api/python/docs/api/gluon/loss/index.html#mxnet.gluon.loss.LogisticLoss)\n",
"\n",
"The Logistic Loss function computes the performance of binary classification models.\n",
"$$\n",
"L = \\sum_i \\log(1 + \\exp(- {pred}_i \\cdot {label}_i))\n",
"$$\n",
"The log loss decreases the closer the prediction is to the actual label. It is sensitive to outliers, because incorrectly classified points are penalized more."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "02bf6393",
"metadata": {},
"outputs": [],
"source": [
"show_classification_loss(gloss.LogisticLoss())"
]
},
{
"cell_type": "markdown",
"id": "b1e6e099",
"metadata": {},
"source": [
"#### [Kullback-Leibler Divergence Loss](/api/python/docs/api/gluon/loss/index.html#mxnet.gluon.loss.KLDivLoss)\n",
"\n",
"The Kullback-Leibler divergence loss measures the divergence between two probability distributions by calculating the difference between cross entropy and entropy. It takes as input the probability of predicted label and the probability of true label.\n",
"\n",
"$$\n",
"L = \\sum_i {label}_i * \\big[\\log({label}_i) - {pred}_i\\big]\n",
"$$\n",
"\n",
"The loss is large, if the predicted probability distribution is far from the ground truth probability distribution. KL divergence is an asymmetric measure. KL divergence loss can be used in Variational Autoencoders (VAEs), and reinforcement learning policy networks such as Trust Region Policy Optimization (TRPO)\n",
"\n",
"\n",
"For instance, in the following example we get a KL divergence of 0.02. We set ```from_logits=False```, so the loss functions will apply ```log_softmax``` on the network output, before computing the KL divergence."
]
},
{
"cell_type": "markdown",
"id": "9239e03c",
"metadata": {},
"source": [
"```python\n",
"output = mx.nd.array([[0.39056206, 1.3068528, 0.39056206, -0.30258512]])\n",
"print('output.softmax(): {}'.format(output.softmax().asnumpy().tolist()))\n",
"target_dist = mx.nd.array([[0.3, 0.4, 0.1, 0.2]])\n",
"loss_fn = gloss.KLDivLoss(from_logits=False)\n",
"loss = loss_fn(output, target_dist)\n",
"print('loss (kl divergence): {}'.format(loss.asnumpy().tolist()))\n",
"```\n"
]
},
{
"cell_type": "markdown",
"id": "e76b6c0c",
"metadata": {},
"source": [
"#### [Triplet Loss](/api/python/docs/api/gluon/loss/index.html#mxnet.gluon.loss.TripletLoss)\n",
"\n",
"Triplet loss takes three input arrays and measures the relative similarity. It takes a positive and negative input and the anchor.\n",
"\n",
"$$\n",
"L = \\sum_i \\max(\\Vert {pos_i}_i - {pred} \\Vert_2^2 -\n",
" \\Vert {neg_i}_i - {pred} \\Vert_2^2 + {margin}, 0)\n",
"$$\n",
"\n",
"The loss function minimizes the distance between similar inputs and maximizes the distance between dissimilar ones.\n",
"In the case of learning embeddings for images of characters, the network may get as input the following 3 images:\n",
"\n",
"![triplet_loss](triplet_loss.png)\n",
"\n",
"The network would learn to minimize the distance between the two `A`'s and maximize the distance between `A` and `Z`.\n",
"\n",
"#### [CTC Loss](/api/python/docs/api/gluon/loss/index.html#mxnet.gluon.loss.CTCLoss)\n",
"\n",
"CTC Loss is the [connectionist temporal classification loss](https://distill.pub/2017/ctc/) . It is used to train recurrent neural networks with variable time dimension. It learns the alignment and labelling of input sequences. It takes a sequence as input and gives probabilities for each timestep. For instance, in the following image the word is not well aligned with the 5 timesteps because of the different sizes of characters. CTC Loss finds for each timestep the highest probability e.g. `t1` presents with high probability a `C`. It combines the highest probapilities and returns the best path decoding. For an in-depth tutorial on how to use CTC-Loss in MXNet, check out this [example](https://github.com/apache/incubator-mxnet/tree/master/example/ctc).\n",
"\n",
"![ctc_loss](ctc_loss.png)\n",
"\n",
"#### [Cosine Embedding Loss](/api/python/docs/api/gluon/loss/index.html#mxnet.gluon.loss.CosineEmbeddingLoss)\n",
"The cosine embedding loss computes the cosine distance between two input vectors.\n",
"\n",
"$$\n",
"\\begin{split}L = \\sum_i \\begin{cases} 1 - {cos\\_sim({input1}_i, {input2}_i)} & \\text{ if } {label}_i = 1\\\\\n",
" {cos\\_sim({input1}_i, {input2}_i)} & \\text{ if } {label}_i = -1 \\end{cases}\\\\\n",
"cos\\_sim(input1, input2) = \\frac{{input1}_i.{input2}_i}{||{input1}_i||.||{input2}_i||}\\end{split}\n",
"$$\n",
"\n",
"Cosine distance measures the similarity between two arrays given a label and is typically used for learning nonlinear embeddings.\n",
"For instance, in the following code example we measure the similarity between the input vectors `x` and `y`. Since they are the same the label equals `1`. The loss function returns $$ \\sum_i 1 - {cos\\_sim({input1}_i, {input2}_i)} $$ which is equal `0`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9bfad8f4",
"metadata": {},
"outputs": [],
"source": [
"x = mx.nd.array([1,0,1,0,1,0])\n",
"y = mx.nd.array([1,0,1,0,1,0])\n",
"label = mx.nd.array(1)\n",
"loss = gloss.CosineEmbeddingLoss()\n",
"print(loss(x,y,label))"
]
},
{
"cell_type": "markdown",
"id": "2ad3771b",
"metadata": {},
"source": [
"Now let's make `y` the opposite of `x`, so we set the label `-1` and the function will return $$ \\sum_i cos\\_sim(input1, input2) $$"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "79e7687e",
"metadata": {},
"outputs": [],
"source": [
"x = mx.nd.array([1,0,1,0,1,0])\n",
"y = mx.nd.array([0,1,0,1,0,1])\n",
"label = mx.nd.array(-1)\n",
"loss = gloss.CosineEmbeddingLoss()\n",
"print(loss(x,y,label))"
]
},
{
"cell_type": "markdown",
"id": "f764c47f",
"metadata": {},
"source": [
"#### [PoissonNLLLoss](/api/python/docs/api/gluon/loss/index.html#mxnet.gluon.loss.PoissonNLLLoss)\n",
"Poisson distribution is widely used for modelling count data. It is defined as:\n",
"\n",
"$$\n",
"f(x) = \\frac{\\mu ^ {\\kern 0.08 em x} e ^ {-\\mu}} {x!} \\qquad \\qquad x = 0,1,2 , \\ldots \\,.\n",
"$$\n",
"\n",
"\n",
"For instance, the count of cars in road traffic approximately follows a Poisson distribution. Using an ordinary least squares model for Poisson distributed data would not work well because of two reasons:\n",
" - count data cannot be negative\n",
" - variance may not be constant\n",
"\n",
"Instead we can use a Poisson regression model, also known as log-linear model. Thereby the Poisson incident rate $$\\mu$$ is\n",
"modelled by a linear combination of unknown parameters.\n",
"We can then use the PoissonNLLLoss which calculates the negative log likelihood for a target that follows a Poisson distribution.\n",
"\n",
"$$ L = \\text{pred} - \\text{target} * \\log(\\text{pred}) +\\log(\\text{target!}) $$\n",
"\n",
"## Advanced: Weighted Loss\n",
"\n",
"Some examples in a batch may be more important than others. We can apply weights to individual examples during the forward pass of the loss function using the `sample_weight` argument. All examples are weighted equally by default."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "28270c5a",
"metadata": {},
"outputs": [],
"source": [
"x = nd.ones((2,))\n",
"y = nd.ones((2,)) * 2\n",
"loss = gloss.L2Loss()\n",
"loss(x, y, nd.array([1, 2]))"
]
},
{
"cell_type": "markdown",
"id": "f18bfb97",
"metadata": {},
"source": [
"## Conclusion\n",
"\n",
"In this tutorial we saw an example of how to evaluate model performance using loss functions (during the forward pass). Crucially, we then saw how calculate parameter gradients (using `backward`) which would minimise this loss. You should now have a better understanding of when to apply different loss functions, especially for regression vs classification tasks.\n",
"\n",
"## Recommended Next Steps\n",
"\n",
"In addition to loss functions, which are used for explicit optimization, you might want to look at metrics that give useful evaluation feedback even if they're not explicitly optimized for in the same way as the loss. You might also want to learn more about the mechanics of the backpropagation stage in the autograd tutorial."
]
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}