{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.6.4","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":3004,"databundleVersionId":861823,"sourceType":"competition"}],"dockerImageVersionId":46,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Visualizing MNIST with a Deep Variational Autoencoder\n\n1. Introduction  \n    A. What is autoencoding?  \n    B. Autoencoders  \n    C. The Variational Variety  \n2. Data preparation  \n    A. Load data  \n    B. Combine train & test  \n    C. Split into new training & validation sets  \n    D. Reshape & normalize  \n3. Model construction  \n    A. Encoder network  \n    B. Sampling function  \n    C. Decoder network  \n    D. Loss\n4. Train the VAE\n5. Results  \n    A. Clustering of digits in the latent space  \n    B. Sample digits","metadata":{"_uuid":"5bff66d63dca5b4b08f9c69e554a6177895dc670","_cell_guid":"b14df16e-0a02-49a8-a477-7cd84f80fc0e"}},{"cell_type":"markdown","source":"## *1. Introduction*\nI spent a rainy weekend recently learning about autoencoders using TensorFlow and Keras in Python. I learned that variational autoencoders (VAEs) can be used to visualize high-dimensional data in a meaningful, lower-dimensional space. In this kernel, I go over some details about autoencoding and autoencoders, especially VAEs, before constructing and training a deep VAE on the MNIST data from the Digit Recognizer competition. We'll see how the data cluster in the lower-dimensional space according to their digit class. Plotting the test set data in this space shows where the images with unknown digit classes fall with respect to the known digit classes.\n\nThe code here borrows heavily from Fran&ccedil;ois Chollet's example VAE from his book [Deep Learning with Python](https://www.amazon.com/Deep-Learning-Python-Francois-Chollet/dp/1617294438/ref=sr_1_1?ie=UTF8&qid=1520470984&sr=8-1&keywords=francois+chollet). You can find a repo of examples from the book (including the one that inspired this kernel) [here on GitHub](https://github.com/brilliantFire/deep-learning-with-python-notebooks).\n\n### *A. What is autoencoding?*\nAutoencoding is much like what it sounds in the sense that the input and 'output' are essentially the same. It's an algorithm for data compression where the functions for compression and decompression are *learned from the data*. It's considered more of a *semi-supervised* learning method as opposed to a truly *unsupervised* one since it's not entirely 'targetless'. Instead it learns the targets from the data itself.\n\nDespite all this talk of data compression, autoencoders aren't typically used for that purpose. In practice, you're much more likely to see them being used to preprocess data (as in denoising - think images but it doesn't have to be ;) ) or for dimensionality reduction. In fact, the hidden layers of simple autoencoders are doing something like principal component analysis (PCA), another method traditionally used for dimensionality reduction.\n\n### *B. Autoencoders*\nGenerally autoencoders have three parts: an encoder, a decoder, and a 'loss' function that maps one to the other. For the simplest autoencoders - the sort that compress and then reconstruct the original inputs from the compressed representation - we can think of the 'loss' as describing the amount of information lost in the process of reconstruction. Typically when people are talking about autoencoders, they're talking about ones where the encoders and decoders are neural networks (in our case deep convnets). In training the autoencoder, we're optimizing the parameters of the neural networks to minimize the 'loss' (or distance) and we do that by stochastic gradient descent (yet another topic for another post). \n\n### *C. The Variational Variety*\nThere's a bunch of different kinds of autoencoders but for this post I'm going to concentrate on one type called a *variational autoencoder*. Variational autoencoders (VAEs) don't learn to morph the data in and out of a compressed representation of itself like the 'vanilla' autoencoders I described above. Instead, they learn the parameters of the probability distribution that the data came from. These types of autoencoders have much in common with latent factor analysis (if you know something about that). The encoder and decoder learn models that are in terms of underlying, unobserved *latent* variables. It's essentially an inference model and a generative model daisy-chained together.\n\n![A variational autoencoder](https://i.imgur.com/ZN6MyTx.png)\n\nVAEs have received a lot of attention because of their *generative* ability (though they seem to be falling out of fashion in favor of general adversarial networks, or GANs, in that regard). Since they learn about the distribution the inputs came from, we can sample from that distribution to generate novel data. As we'll see, VAEs can also be used to cluster data in useful ways.","metadata":{"_uuid":"9184c1178ebfec6552bcc5ced94b98a22df9a24b","_cell_guid":"9232ab89-1026-4c9e-93d1-dd9a7e3f8f8f"}},{"cell_type":"markdown","source":"## 2. *Data preparation*  \n### *A. Load Data*","metadata":{"_uuid":"517052cecc729f77b352d0d6e74f35e8489aa474","_cell_guid":"af039539-b12e-4573-a78f-b550f44c7332"}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport matplotlib\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as mpatches\n%matplotlib inline\n\nfrom scipy.stats import norm\n\nimport keras\nfrom keras import layers\nfrom keras.models import Model\nfrom keras import metrics\nfrom keras import backend as K   # 'generic' backend so code works with either tensorflow or theano\n\nK.clear_session()\n\nnp.random.seed(237)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":false,"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_orig = pd.read_csv('../input/train.csv')\ntest_orig = pd.read_csv('../input/test.csv')\n\ntrain_orig.head()","metadata":{"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","trusted":false,"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### *B. Combine Train & Test*\nLet's add a placeholder 'label' column to the test dataset. We'll give all the test images a label of '11' for now (\"This data goes to 11.\") ","metadata":{"_uuid":"ee191fc918081c56e4f99a76450112e06ec298bb","_cell_guid":"dfca3ccc-c861-40da-a416-08c0cb526a52"}},{"cell_type":"code","source":"# create 'label' column in test dataset; rearrange so that columns are in the same order as in train\ntest_orig['label'] = 11\ntestCols = test_orig.columns.tolist()\ntestCols = testCols[-1:] + testCols[:-1]\ntest_orig = test_orig[testCols]","metadata":{"_uuid":"6155d58768e796e39afe51e405d60dbb14f50105","_cell_guid":"d64517e9-3c29-46ca-803a-ee7d13a5bb53","collapsed":true,"trusted":false,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"We want to train the autoencoder with as many images as possible. Also, since we don't need the labels for building the model (remember: *semi-supervised*), it makes sense to combine the train and test data into one combined dataframe.","metadata":{"_uuid":"0d37d4883719be67a82ebbc3d11f2a1193f38a10","_cell_guid":"8d468e80-9a7f-488e-98e4-945b3a9a3744"}},{"cell_type":"code","source":"# combine original train and test sets\ncombined = pd.concat([train_orig, test_orig], ignore_index = True)\n\ncombined.head()","metadata":{"_uuid":"4d00ef2261bf7414b8fdff34a27ceb8cc31c6a32","_cell_guid":"4736824b-f96d-4276-a997-8155e3e4d13a","trusted":false,"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"combined.tail()","metadata":{"_uuid":"f8ba5aea0756b4cf0f75add60af08abfce601194","_cell_guid":"4869da15-7183-4568-b09f-0a299d0f4786","trusted":false,"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### *C. Split into training & validation sets*\nDespite being trained in a semi-supervised way, the VAE algorithm entails minimizing a 'loss' function. As we'll see shortly, that loss is actually two different 'losses' combined, one that describes the difference between the input images and the images reconstructed from samples from the latent distribution, and another that is the difference between the latent distribution and the prior (the inputs). We can calculate this loss on a validation set with each training epoch as an estimate of how the model describes data it was not trained on.  \n\nRemember that we've combined the original train and test data so the *new* train and validation sets that we make below will each have some images with missing ('11') labels.","metadata":{"_uuid":"fd183a862b09ee0ec3581768ce857eb778be3c49","_cell_guid":"15b23079-70ac-40fb-96a1-ad4d4dbe1d29"}},{"cell_type":"code","source":"# Hold out 5000 random images as a validation/test sample\nvalid = combined.sample(n = 5000, random_state = 555)\ntrain = combined.loc[~combined.index.isin(valid.index)]\n\n# free up some space and delete test and combined\ndel train_orig, test_orig, combined\n\nvalid.head()","metadata":{"_uuid":"83f48e2fb61b3c738d14c2e9575d08ca8d40a9be","_cell_guid":"a073bdce-1609-4542-b46c-6604db42ffe2","trusted":false,"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### *4. Reshape & normalize*\nOur encoder and decoder are deep convnets constructed using the Keras Functional API. We'll need to separate the inputs from the labels, normalize them by dividing the max pixel value, and reshape them into 28x28 pixel images.","metadata":{"_uuid":"9caef978723aa28443dc33602e5e096f4535d750","_cell_guid":"012e0e0b-8f35-4722-b6d3-22fd2619c8dd"}},{"cell_type":"code","source":"# X's\nX_train = train.drop(['label'], axis = 1)\nX_valid = valid.drop(['label'], axis = 1)\n\n# labels\ny_train = train['label']\ny_valid = valid['label']\n\n# Normalize and reshape\nX_train = X_train.astype('float32') / 255.\nX_train = X_train.values.reshape(-1,28,28,1)\n\nX_valid = X_valid.astype('float32') / 255.\nX_valid = X_valid.values.reshape(-1,28,28,1)","metadata":{"_uuid":"f91d57425cd06cde12375a700a423895da9c7c3e","_cell_guid":"f1e92f03-2b6c-4830-8a3e-e062f9411fdb","collapsed":true,"trusted":false,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"We can take a look at a few random images. The bottom right panel shows one of the more difficult-to-classify digits (even for humans!).","metadata":{"_uuid":"b89e64ddca2b0a11ae4fe9ddfe4b928f60594c14","_cell_guid":"fef54f40-7d3e-4767-bf72-d3b94509b5da"}},{"cell_type":"code","source":"plt.figure(1)\nplt.subplot(221)\nplt.imshow(X_train[13][:,:,0])\n\nplt.subplot(222)\nplt.imshow(X_train[690][:,:,0])\n\nplt.subplot(223)\nplt.imshow(X_train[2375][:,:,0])\n\nplt.subplot(224)\nplt.imshow(X_train[42013][:,:,0])\nplt.show()","metadata":{"_uuid":"c5f9fd1d7d3e7c4e01d73f039b90309499300b41","_cell_guid":"98261ef0-11de-48f3-add1-e914a62674fd","trusted":false,"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. *Model construction*\n### *A. Encoder network*\nA VAE has three basic parts:  \n\n1. An encoder that that learns the parameters (mean and variance) of the underlying latent distribution;  \n2. A means of sampling from that distribution; and,  \n3. A decoder that can turn the sample from #2 back into an image.  \n\nIn this example, both the encoder and decoder networks are deep convnets. You'll notice that the encoder below has two output layers, one for the latent distribution mean (z_mu) and the other for its variance (z_log_sigma).","metadata":{"_uuid":"5252a23c95a1ad720fe6e0681d10d91624547882","_cell_guid":"2c02e436-c1c0-4d03-ae48-88253a871e10"}},{"cell_type":"code","source":"img_shape = (28, 28, 1)    # for MNIST\nbatch_size = 16\nlatent_dim = 2  # Number of latent dimension parameters\n\n# Encoder architecture: Input -> Conv2D*4 -> Flatten -> Dense\ninput_img = keras.Input(shape=img_shape)\n\nx = layers.Conv2D(32, 3,\n                  padding='same', \n                  activation='relu')(input_img)\nx = layers.Conv2D(64, 3,\n                  padding='same', \n                  activation='relu',\n                  strides=(2, 2))(x)\nx = layers.Conv2D(64, 3,\n                  padding='same', \n                  activation='relu')(x)\nx = layers.Conv2D(64, 3,\n                  padding='same', \n                  activation='relu')(x)\n# need to know the shape of the network here for the decoder\nshape_before_flattening = K.int_shape(x)\n\nx = layers.Flatten()(x)\nx = layers.Dense(32, activation='relu')(x)\n\n# Two outputs, latent mean and (log)variance\nz_mu = layers.Dense(latent_dim)(x)\nz_log_sigma = layers.Dense(latent_dim)(x)","metadata":{"_uuid":"55fea787c00da3a81b87fc655fa405dca882802a","_cell_guid":"e2a1d4a9-dc41-4765-b52b-36219e13eef2","collapsed":true,"trusted":false,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### *B. Sampling function*\nNext, we create a function to sample from the distribution we just learned the parameters of. `epsilon` is a tensor of small random normal values. One of the assumptions underlying a VAE like this is that our data arose from a random process and is normally distributed in the latent space.\n\nWith Keras, everything has to be in a 'layer' to compile correctly. This goes for our sampling function. The `Lambda` layer wrapper let's us do this.","metadata":{"_uuid":"168c43129e87eec53c6af72dba48c7a761a79063","_cell_guid":"93b37058-c9d3-4bfc-b3d3-a0d1aa200965"}},{"cell_type":"code","source":"# sampling function\ndef sampling(args):\n    z_mu, z_log_sigma = args\n    epsilon = K.random_normal(shape=(K.shape(z_mu)[0], latent_dim),\n                              mean=0., stddev=1.)\n    return z_mu + K.exp(z_log_sigma) * epsilon\n\n# sample vector from the latent distribution\nz = layers.Lambda(sampling)([z_mu, z_log_sigma])","metadata":{"_uuid":"b8a145b6db334a68977c89639a08ef371e54cdd6","_cell_guid":"c8e1ba8f-a88a-4687-80f3-68f6815bb032","collapsed":true,"trusted":false,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### *C. Decoder network*\nThe decoder is basically the encoder in reverse.","metadata":{"_uuid":"d6f83916df89ac4afeee046fc2d3f817fc16c249","_cell_guid":"3442d1be-3e02-44db-b615-59b010089776"}},{"cell_type":"code","source":"# decoder takes the latent distribution sample as input\ndecoder_input = layers.Input(K.int_shape(z)[1:])\n\n# Expand to 784 total pixels\nx = layers.Dense(np.prod(shape_before_flattening[1:]),\n                 activation='relu')(decoder_input)\n\n# reshape\nx = layers.Reshape(shape_before_flattening[1:])(x)\n\n# use Conv2DTranspose to reverse the conv layers from the encoder\nx = layers.Conv2DTranspose(32, 3,\n                           padding='same', \n                           activation='relu',\n                           strides=(2, 2))(x)\nx = layers.Conv2D(1, 3,\n                  padding='same', \n                  activation='sigmoid')(x)\n\n# decoder model statement\ndecoder = Model(decoder_input, x)\n\n# apply the decoder to the sample from the latent distribution\nz_decoded = decoder(z)","metadata":{"_uuid":"0df3d7471211f7403735a4e923c47a2294a0a349","_cell_guid":"68322de7-9675-4788-9c60-9044cb3172db","collapsed":true,"trusted":false,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### *D. Loss*\nWe need one more thing and that's something that will calculate the unique loss function the VAE requires. Recall that the VAE is trained using a loss function with two components:  \n\n1. 'Reconstruction loss' - This is the cross-entropy describing the errors between the decoded samples from the latent distribution and the original inputs.  \n2. The Kullback-Liebler divergence between the latent distribution and the prior (this acts as a sort of regularization term).  \n\nWe define a custom layer class that calculates the loss. ","metadata":{"_uuid":"705e156f13e4b89c73c2d5c06f0b0965d74a182f","_cell_guid":"f93644c4-28cd-43dc-8e1c-f8c11b60be90"}},{"cell_type":"code","source":"# construct a custom layer to calculate the loss\nclass CustomVariationalLayer(keras.layers.Layer):\n\n    def vae_loss(self, x, z_decoded):\n        x = K.flatten(x)\n        z_decoded = K.flatten(z_decoded)\n        # Reconstruction loss\n        xent_loss = keras.metrics.binary_crossentropy(x, z_decoded)\n        # KL divergence\n        kl_loss = -5e-4 * K.mean(1 + z_log_sigma - K.square(z_mu) - K.exp(z_log_sigma), axis=-1)\n        return K.mean(xent_loss + kl_loss)\n\n    # adds the custom loss to the class\n    def call(self, inputs):\n        x = inputs[0]\n        z_decoded = inputs[1]\n        loss = self.vae_loss(x, z_decoded)\n        self.add_loss(loss, inputs=inputs)\n        return x\n\n# apply the custom loss to the input images and the decoded latent distribution sample\ny = CustomVariationalLayer()([input_img, z_decoded])","metadata":{"_uuid":"289b40920037fbdb305d3e76a5a08495a143693c","_cell_guid":"394127bc-7beb-406a-8e2c-7aae3b436d85","collapsed":true,"trusted":false,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Now we can instantiate the model and take a look at its summary.","metadata":{"_uuid":"80350c12746fdab9892122af1b60248f7567f0fe","_cell_guid":"b56e4d33-5d1a-49c2-aa3e-c00692862889"}},{"cell_type":"code","source":"# VAE model statement\nvae = Model(input_img, y)\nvae.compile(optimizer='rmsprop', loss=None)\nvae.summary()","metadata":{"_uuid":"91449af9f139e7080ddaa8b0a8152b66b9d02d95","_cell_guid":"b189196e-deb3-429d-bf3b-1c8c6d4a4afb","trusted":false,"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. *Train the VAE*\nFinally, we fit the model.","metadata":{"_uuid":"a838351a7a4aa74c8a1a50d09a4a4a3594aaaac1","_cell_guid":"cf0514db-2761-40b7-b174-7f40d8112d91"}},{"cell_type":"code","source":"vae.fit(x=X_train, y=None,\n        shuffle=True,\n        epochs=7,\n        batch_size=batch_size,\n        validation_data=(X_valid, None))","metadata":{"_uuid":"e052652709cbd5fcb9e4a836306245edaaf0ab51","_cell_guid":"d44843b2-3e0d-4319-bc6e-8089871ec1ea","trusted":false,"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. *Results*  \n### *A. Clustering of digits in the latent space*\nWe can make predictions on the validation set using the encoder network. This has the effect of translating the images from the 784-dimensional input space into the 2-dimensional latent space. When we color-code those translated data points according to their *known* digit class, we can see how the digits cluster together.","metadata":{"_uuid":"3e03487ab62f3f8807441ca6501438b74a5c95c6","_cell_guid":"8a3a7177-31bc-462b-a43f-d79dfd79f654"}},{"cell_type":"code","source":"# Isolate original training set records in validation set\nvalid_noTest = valid[valid['label'] != 11]\n\n# X's and Y's\nX_valid_noTest = valid_noTest.drop('label', axis=1)\ny_valid_noTest = valid_noTest['label']\n\n# Reshape and normalize\nX_valid_noTest = X_valid_noTest.astype('float32') / 255.\nX_valid_noTest = X_valid_noTest.values.reshape(-1,28,28,1)","metadata":{"_uuid":"ecd9333a0861a6f7aabaccf508412e9056f46035","_cell_guid":"dc72b908-77e0-4ef6-859d-79f0e83d97ef","collapsed":true,"trusted":false,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Translate into the latent space\nencoder = Model(input_img, z_mu)\nx_valid_noTest_encoded = encoder.predict(X_valid_noTest, batch_size=batch_size)\nplt.figure(figsize=(10, 10))\nplt.scatter(x_valid_noTest_encoded[:, 0], x_valid_noTest_encoded[:, 1], c=y_valid_noTest, cmap='brg')\nplt.colorbar()\nplt.show()","metadata":{"_uuid":"0d8c22694d59990b52efe2c1a6cac66b9d899412","_cell_guid":"afe4a541-814e-4def-88cd-cbee97e18dfe","trusted":false,"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Including the original test set data lets us see where they fall with respect to the known digit clusters.","metadata":{"_uuid":"1c36beb9eae66ebc216ba9a8e7ee9994e776a530","_cell_guid":"508868ea-ee14-4a56-9207-d4ac2607697d"}},{"cell_type":"code","source":"# set colormap so that 11's are gray\ncustom_cmap = matplotlib.cm.get_cmap('brg')\ncustom_cmap.set_over('gray')\n\nx_valid_encoded = encoder.predict(X_valid, batch_size=batch_size)\nplt.figure(figsize=(10, 10))\ngray_marker = mpatches.Circle(4,radius=0.1,color='gray', label='Test')\nplt.legend(handles=[gray_marker], loc = 'best')\nplt.scatter(x_valid_encoded[:, 0], x_valid_encoded[:, 1], c=y_valid, cmap=custom_cmap)\nplt.clim(0, 9)\nplt.colorbar()","metadata":{"_uuid":"5a1bb733976c79bf74a3a9cafa262d73e1b93865","_cell_guid":"e53af49f-8591-403d-a909-5bb54947d040","trusted":false,"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### *B. Sample digits*\nAnother fun thing we can do is to use the decoder network to take a peak at what samples from the latent space look like as we change the latent variables. What we end up with is a smoothly varying space where each digit transforms into the others as we dial the latent variables up and down.","metadata":{"_uuid":"277873274ca79a7e5abc4243609c0a169ca6c87b","_cell_guid":"f592181f-07a4-4fc8-aee7-2f85922b1a1b"}},{"cell_type":"code","source":"# Display a 2D manifold of the digits\nn = 20  # figure with 20x20 digits\ndigit_size = 28\nfigure = np.zeros((digit_size * n, digit_size * n))\n\n# Construct grid of latent variable values\ngrid_x = norm.ppf(np.linspace(0.05, 0.95, n))\ngrid_y = norm.ppf(np.linspace(0.05, 0.95, n))\n\n# decode for each square in the grid\nfor i, yi in enumerate(grid_x):\n    for j, xi in enumerate(grid_y):\n        z_sample = np.array([[xi, yi]])\n        z_sample = np.tile(z_sample, batch_size).reshape(batch_size, 2)\n        x_decoded = decoder.predict(z_sample, batch_size=batch_size)\n        digit = x_decoded[0].reshape(digit_size, digit_size)\n        figure[i * digit_size: (i + 1) * digit_size,\n               j * digit_size: (j + 1) * digit_size] = digit\n\nplt.figure(figsize=(10, 10))\nplt.imshow(figure, cmap='gnuplot2')\nplt.show()  ","metadata":{"_uuid":"ab1bcf744f65020eef6665b2166d32820842b824","_cell_guid":"8b6e0f3e-eb67-446d-aa17-492b1c68077a","trusted":false,"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Thanks for reading!","metadata":{"_uuid":"8316c7cf88dc54ba59c793732102faa36b6aaab0","_cell_guid":"36998846-4a02-43cc-b04f-4dceb90cbac0"}}]}