{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Concise Implementation of Softmax Regression\n",
"\n",
"Just as Gluon made it much easier to implement [linear regression](linear-regression-gluon.md), we'll find it similarly (or possibly more) convenient for implementing classification models. \n",
"Again, we begin with our import ritual."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"attributes": {
"classes": [],
"id": "",
"n": "1"
}
},
"outputs": [],
"source": [
"import sys\n",
"sys.path.insert(0, '..')\n",
"\n",
"%matplotlib inline\n",
"import d2l\n",
"from mxnet import gluon, init\n",
"from mxnet.gluon import loss as gloss, nn"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's stick with the Fashion-MNIST dataset and keep the batch size at $256$ as in the last section."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"attributes": {
"classes": [],
"id": "",
"n": "2"
}
},
"outputs": [],
"source": [
"batch_size = 256\n",
"train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Initialize Model Parameters\n",
"\n",
"As [mentioned previously](softmax-regression.md), the output layer of softmax regression is a fully connected (`Dense`) layer. Therefore, to implement our model, we just need to add one `Dense` layer with 10 outputs to our `Sequential`. Again, here, the `Sequential` isn't really necessary, but we might as well form the habit since it will be ubiquitous when implementing deep models. Again, we initialize the weights at random with zero mean and standard deviation 0.01."
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"attributes": {
"classes": [],
"id": "",
"n": "3"
}
},
"outputs": [],
"source": [
"net = nn.Sequential()\n",
"net.add(nn.Dense(10))\n",
"net.initialize(init.Normal(sigma=0.01))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## The Softmax\n",
"\n",
"In the previous example, we calculated our model's output and then ran this output through the cross-entropy loss. At its heart it uses `-nd.pick(y_hat, y).log()`. Mathematically, that's a perfectly reasonable thing to do. However, computationally, things can get hairy when dealing with exponentiation due to numerical stability issues, a matter we've already discussed a few times (e.g. in when covering [Naive Bayes](../chapter_crashcourse/naive-bayes.md) and in the problem set of the previous chapter). Recall that the softmax function calculates $\\hat y_j = \\frac{e^{z_j}}{\\sum_{i=1}^{n} e^{z_i}}$, where $\\hat y_j$ is the j-th element of ``yhat`` and $z_j$ is the j-th element of the input ``y_linear`` variable, as computed by the softmax.\n",
"\n",
"If some of the $z_i$ are very large (i.e. very positive), \n",
"$e^{z_i}$ might be larger than the largest number \n",
"we can have for certain types of ``float`` (i.e. overflow). \n",
"This would make the denominator (and/or numerator) ``inf`` and we get zero, \n",
"or ``inf``, or ``nan`` for $\\hat y_j$. \n",
"In any case, we won't get a well-defined return value for ``cross_entropy``. This is the reason we subtract $\\text{max}(z_i)$ \n",
"from all $z_i$ first in ``softmax`` function. \n",
"You can verify that this shifting in $z_i$ \n",
"will not change the return value of ``softmax``.\n",
"\n",
"After the above subtraction/ normalization step, \n",
"it is possible that $z_j$ is very negative. \n",
"Thus, $e^{z_j}$ will be very close to zero \n",
"and might be rounded to zero due to finite precision (i.e underflow), \n",
"which makes $\\hat y_j$ zero and we get ``-inf`` for $\\text{log}(\\hat y_j)$. \n",
"A few steps down the road in backpropagation, \n",
"we start to get horrific not-a-number (``nan``) results printed to screen.\n",
"\n",
"Our salvation is that even though we're computing these exponential functions, we ultimately plan to take their log in the cross-entropy functions. \n",
"It turns out that by combining these two operators \n",
"``softmax`` and ``cross_entropy`` together, \n",
"we can escape the numerical stability issues \n",
"that might otherwise plague us during backpropagation. \n",
"As shown in the equation below, we avoided calculating $e^{z_j}$ \n",
"but directly used $z_j$ due to $\\log(\\exp(\\cdot))$.\n",
"\n",
"$$\n",
"\\begin{aligned}\n",
"\\log{(\\hat y_j)} & = \\log\\left( \\frac{e^{z_j}}{\\sum_{i=1}^{n} e^{z_i}}\\right) \\\\\n",
"& = \\log{(e^{z_j})}-\\text{log}{\\left( \\sum_{i=1}^{n} e^{z_i} \\right)} \\\\\n",
"& = z_j -\\log{\\left( \\sum_{i=1}^{n} e^{z_i} \\right)}\n",
"\\end{aligned}\n",
"$$\n",
"\n",
"We'll want to keep the conventional softmax function handy \n",
"in case we ever want to evaluate the probabilities output by our model. \n",
"But instead of passing softmax probabilities into our new loss function, \n",
"we'll just pass $\\hat{y}$ and compute the softmax and its log \n",
"all at once inside the softmax_cross_entropy loss function, \n",
"which does smart things like the log-sum-exp trick ([see on Wikipedia](https://en.wikipedia.org/wiki/LogSumExp))."
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"attributes": {
"classes": [],
"id": "",
"n": "4"
}
},
"outputs": [],
"source": [
"loss = gloss.SoftmaxCrossEntropyLoss()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Optimization Algorithm\n",
"\n",
"We use the mini-batch random gradient descent \n",
"with a learning rate of $0.1$ as the optimization algorithm. \n",
"Note that this is the same choice as for linear regression \n",
"and it illustrates the general applicability of the optimizers."
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"attributes": {
"classes": [],
"id": "",
"n": "5"
}
},
"outputs": [],
"source": [
"trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.1})"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Training\n",
"\n",
"Next, we use the training functions defined in the last section to train a model."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"attributes": {
"classes": [],
"id": "",
"n": "6"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"epoch 1, loss 0.7876, train acc 0.749, test acc 0.799\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"epoch 2, loss 0.5741, train acc 0.811, test acc 0.817\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"epoch 3, loss 0.5306, train acc 0.823, test acc 0.832\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"epoch 4, loss 0.5049, train acc 0.830, test acc 0.842\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"epoch 5, loss 0.4893, train acc 0.835, test acc 0.840\n"
]
}
],
"source": [
"num_epochs = 5\n",
"d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None,\n",
" None, trainer)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Just as before, this algorithm converges to a solution \n",
"that achieves an accuracy of 83.7%, \n",
"albeit this time with a lot fewer lines of code than before. \n",
"Note that in many cases, Gluon takes specific precautions \n",
"in addition to the most well-known tricks for ensuring numerical stability. \n",
"This saves us from many common pitfalls that might befall us \n",
"if we were to code all of our models from scratch.\n",
"\n",
"## Exercises\n",
"\n",
"1. Try adjusting the hyper-parameters, such as batch size, epoch, and learning rate, to see what the results are.\n",
"1. Why might the test accuracy decrease again after a while? How could we fix this?\n",
"\n",
"## Scan the QR Code to [Discuss](https://discuss.mxnet.io/t/2337)\n",
"\n",
"![](../img/qr_softmax-regression-gluon.svg)"
]
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 2
}