{
  "source_file": "0930",
  "source_platform": "个人博客",
  "author": "François Chollet",
  "title_listed": "Building Autoencoders in Keras",
  "keywords": "自编码器,Keras,无监督",
  "domain": "机器学习",
  "content_type": "教程",
  "is_paid_flag": "否",
  "url": "https://blog.keras.io/building-autoencoders-in-keras.html",
  "publish_date": "2016",
  "corpus_stage": "机器学习",
  "notes": "自编码器教程",
  "id": "d8f51c6555cb",
  "collected_at": "2026-10-02T15:18:56",
  "title": "Building Autoencoders in Keras",
  "body_markdown": "This post was written in early 2016. It is therefore badly outdated.\nIn this tutorial, we will answer some common questions about autoencoders, and we will cover code examples of the following models:\na simple autoencoder based on a fully-connected layer\na sparse autoencoder\na deep fully-connected autoencoder\na deep convolutional autoencoder\nan image denoising model\na sequence-to-sequence autoencoder\na variational autoencoder\nNote: all code examples have been updated to the Keras 2.0 API on March 14, 2017. You will need Keras version 2.0.0 or higher to run them.\nWhat are autoencoders?\n\"Autoencoding\" is a data compression algorithm where the compression and decompression functions are 1) data-specific, 2) lossy, and 3)\nlearned automatically from examples\nrather than engineered by a human. Additionally, in almost all contexts where the term \"autoencoder\" is used, the compression and decompression functions are implemented with neural networks.\n1) Autoencoders are data-specific, which means that they will only be able to compress data similar to what they have been trained on. This is different from, say, the MPEG-2 Audio Layer III (MP3) compression algorithm, which only holds assumptions about \"sound\" in general, but not about specific types of sounds. An autoencoder trained on pictures of faces would do a rather poor job of compressing pictures of trees, because the features it would learn would be face-specific.\n2) Autoencoders are lossy, which means that the decompressed outputs will be degraded compared to the original inputs (similar to MP3 or JPEG compression). This differs from lossless arithmetic compression.\n3) Autoencoders are learned automatically from data examples, which is a useful property: it means that it is easy to train specialized instances of the algorithm that will perform well on a specific type of input. It doesn't require any new engineering, just appropriate training data.\nTo build an autoencoder, you need three things: an encoding function, a decoding function, and a distance function between the amount of information loss between the compressed representation of your data and the decompressed representation (i.e. a \"loss\" function). The encoder and decoder will be chosen to be parametric functions (typically neural networks), and to be differentiable with respect to the distance function, so the parameters of the encoding/decoding functions can be optimize to minimize the reconstruction loss, using Stochastic Gradient Descent. It's simple! And you don't even need to understand any of these words to start using autoencoders in practice.\nAre they good at data compression?\nUsually, not really. In picture compression for instance, it is pretty difficult to train an autoencoder that does a better job than a basic algorithm like JPEG, and typically the only way it can be achieved is by restricting yourself to a very specific type of picture (e.g. one for which JPEG does not do a good job). The fact that autoencoders are data-specific makes them generally impractical for real-world data compression problems: you can only use them on data that is similar to what they were trained on, and making them more general thus requires\nlots\nof training data. But future advances might change this, who knows.\nWhat are autoencoders good for?\nThey are rarely used in practical applications. In 2012 they briefly found an application in greedy layer-wise pretraining for deep convolutional neural networks [1], but this quickly fell out of fashion as we started realizing that better random weight initialization schemes were sufficient for training deep networks from scratch. In 2014, batch normalization [2] started allowing for even deeper networks, and from late 2015 we could train arbitrarily deep networks from scratch using residual learning [3].\nToday two interesting practical applications of autoencoders are\ndata denoising\n(which we feature later in this post), and\ndimensionality reduction for data visualization\n. With appropriate dimensionality and sparsity constraints, autoencoders can learn data projections that are more interesting than PCA or other basic techniques.\nFor 2D visualization specifically,\nt-SNE\n(pronounced \"tee-snee\") is probably the best algorithm around, but it typically requires relatively low-dimensional data. So a good strategy for visualizing similarity relationships in high-dimensional data is to start by using an autoencoder to compress your data into a low-dimensional space (e.g. 32-dimensional), then use t-SNE for mapping the compressed data to a 2D plane. Note that a nice parametric implementation of t-SNE in Keras was developed by Kyle McDonald and\nis available on Github\n. Otherwise\nscikit-learn\nalso has a simple and practical implementation.\nSo what's the big deal with autoencoders?\nTheir main claim to fame comes from being featured in many introductory machine learning classes available online. As a result, a lot of newcomers to the field absolutely love autoencoders and can't get enough of them. This is the reason why this tutorial exists!\nOtherwise, one reason why they have attracted so much research and attention is because they have long been thought to be a potential avenue for solving the problem of unsupervised learning, i.e. the learning of useful representations without the need for labels. Then again, autoencoders are not a true unsupervised learning technique (which would imply a different learning process altogether), they are a\nself-supervised\ntechnique, a specific instance of\nsupervised learning\nwhere the targets are generated from the input data. In order to get self-supervised models to learn interesting features, you have to come up with an interesting synthetic target and loss function, and that's where problems arise: merely learning to reconstruct your input in minute detail might not be the right choice here. At this point there is significant evidence that focusing on the reconstruction of a picture at the pixel level, for instance, is not conductive to learning interesting, abstract features of the kind that label-supervized learning induces (where targets are fairly abstract concepts \"invented\" by humans such as \"dog\", \"car\"...). In fact, one may argue that the best features in this regard are those that are the\nworst\nat exact input reconstruction while achieving high performance on the main task that you are interested in (classification, localization, etc).\nIn self-supervized learning applied to vision, a potentially fruitful alternative to autoencoder-style input reconstruction is the use of toy tasks such as jigsaw puzzle solving, or detail-context matching (being able to match high-resolution but small patches of pictures with low-resolution versions of the pictures they are extracted from). The following paper investigates jigsaw puzzle solving and makes for a very interesting read: Noroozi and Favaro (2016)\nUnsupervised Learning of Visual Representations by Solving Jigsaw Puzzles\n. Such tasks are providing the model with built-in assumptions about the input data which are missing in traditional autoencoders, such as\n\"visual macro-structure matters more than pixel-level details\"\n.\nLet's build the simplest possible autoencoder\nWe'll start simple, with a single fully-connected neural layer as encoder and as decoder:\nimport\nkeras\nfrom\nkeras\nimport\nlayers\n# This is the size of our encoded representations\nencoding_dim\n=\n32\n# 32 floats -> compression of factor 24.5, assuming the input is 784 floats\n# This is our input image\ninput_img\n=\nkeras\n.\nInput\n(\nshape\n=\n(\n784\n,))\n# \"encoded\" is the encoded representation of the input\nencoded\n=\nlayers\n.\nDense\n(\nencoding_dim\n,\nactivation\n=\n'relu'\n)(\ninput_img\n)\n# \"decoded\" is the lossy reconstruction of the input\ndecoded\n=\nlayers\n.\nDense\n(\n784\n,\nactivation\n=\n'sigmoid'\n)(\nencoded\n)\n# This model maps an input to its reconstruction\nautoencoder\n=\nkeras\n.\nModel\n(\ninput_img\n,\ndecoded\n)\nLet's also create a separate encoder model:\n# This model maps an input to its encoded representation\nencoder\n=\nkeras\n.\nModel\n(\ninput_img\n,\nencoded\n)\nAs well as the decoder model:\n# This is our encoded (32-dimensional) input\nencoded_input\n=\nkeras\n.\nInput\n(\nshape\n=\n(\nencoding_dim\n,))\n# Retrieve the last layer of the autoencoder model\ndecoder_layer\n=\nautoencoder\n.\nlayers\n[\n-\n1\n]\n# Create the decoder model\ndecoder\n=\nkeras\n.\nModel\n(\nencoded_input\n,\ndecoder_layer\n(\nencoded_input\n))\nNow let's train our autoencoder to reconstruct MNIST digits.\nFirst, we'll configure our model to use a per-pixel binary crossentropy loss, and the Adam optimizer:\nautoencoder\n.\ncompile\n(\noptimizer\n=\n'adam'\n,\nloss\n=\n'binary_crossentropy'\n)\nLet's prepare our input data. We're using MNIST digits, and we're discarding the labels (since we're only interested in encoding/decoding the input images).\nfrom\nkeras.datasets\nimport\nmnist\nimport\nnumpy\nas\nnp\n(\nx_train\n,\n_\n),\n(\nx_test\n,\n_\n)\n=\nmnist\n.\nload_data\n()\nWe will normalize all values between 0 and 1 and we will flatten the 28x28 images into vectors of size 784.\nx_train\n=\nx_train\n.\nastype\n(\n'float32'\n)\n/\n255.\nx_test\n=\nx_test\n.\nastype\n(\n'float32'\n)\n/\n255.\nx_train\n=\nx_train\n.\nreshape\n((\nlen\n(\nx_train\n),\nnp\n.\nprod\n(\nx_train\n.\nshape\n[\n1\n:])))\nx_test\n=\nx_test\n.\nreshape\n((\nlen\n(\nx_test\n),\nnp\n.\nprod\n(\nx_test\n.\nshape\n[\n1\n:])))\nprint\n(\nx_train\n.\nshape\n)\nprint\n(\nx_test\n.\nshape\n)\nNow let's train our autoencoder for 50 epochs:\nautoencoder\n.\nfit\n(\nx_train\n,\nx_train\n,\nepochs\n=\n50\n,\nbatch_size\n=\n256\n,\nshuffle\n=\nTrue\n,\nvalidation_data\n=\n(\nx_test\n,\nx_test\n))\nAfter 50 epochs, the autoencoder seems to reach a stable train/validation loss value of about\n0.09\n. We can try to visualize the reconstructed inputs and the encoded representations. We will use Matplotlib.\n# Encode and decode some digits\n# Note that we take them from the *test* set\nencoded_imgs\n=\nencoder\n.\npredict\n(\nx_test\n)\ndecoded_imgs\n=\ndecoder\n.\npredict\n(\nencoded_imgs\n)\n# Use Matplotlib (don't ask)\nimport\nmatplotlib.pyplot\nas\nplt\nn\n=\n10\n# How many digits we will display\nplt\n.\nfigure\n(\nfigsize\n=\n(\n20\n,\n4\n))\nfor\ni\nin\nrange\n(\nn\n):\n# Display original\nax\n=\nplt\n.\nsubplot\n(\n2\n,\nn\n,\ni\n+\n1\n)\nplt\n.\nimshow\n(\nx_test\n[\ni\n]\n.\nreshape\n(\n28\n,\n28\n))\nplt\n.\ngray\n()\nax\n.\nget_xaxis\n()\n.\nset_visible\n(\nFalse\n)\nax\n.\nget_yaxis\n()\n.\nset_visible\n(\nFalse\n)\n# Display reconstruction\nax\n=\nplt\n.\nsubplot\n(\n2\n,\nn\n,\ni\n+\n1\n+\nn\n)\nplt\n.\nimshow\n(\ndecoded_imgs\n[\ni\n]\n.\nreshape\n(\n28\n,\n28\n))\nplt\n.\ngray\n()\nax\n.\nget_xaxis\n()\n.\nset_visible\n(\nFalse\n)\nax\n.\nget_yaxis\n()\n.\nset_visible\n(\nFalse\n)\nplt\n.\nshow\n()\nHere's what we get. The top row is the original digits, and the bottom row is the reconstructed digits. We are losing quite a bit of detail with this basic approach.\nAdding a sparsity constraint on the encoded representations\nIn the previous example, the representations were only constrained by the size of the hidden layer (32). In such a situation, what typically happens is that the hidden layer is learning an approximation of\nPCA (principal component analysis)\n. But another way to constrain the representations to be compact is to add a sparsity contraint on the activity of the hidden representations, so fewer units would \"fire\" at a given time. In Keras, this can be done by adding an\nactivity_regularizer\nto our\nDense\nlayer:\nfrom\nkeras\nimport\nregularizers\nencoding_dim\n=\n32\ninput_img\n=\nkeras\n.\nInput\n(\nshape\n=\n(\n784\n,))\n# Add a Dense layer with a L1 activity regularizer\nencoded\n=\nlayers\n.\nDense\n(\nencoding_dim\n,\nactivation\n=\n'relu'\n,\nactivity_regularizer\n=\nregularizers\n.\nl1\n(\n10e-5\n))(\ninput_img\n)\ndecoded\n=\nlayers\n.\nDense\n(\n784\n,\nactivation\n=\n'sigmoid'\n)(\nencoded\n)\nautoencoder\n=\nkeras\n.\nModel\n(\ninput_img\n,\ndecoded\n)\nLet's train this model for 100 epochs (with the added regularization the model is less likely to overfit and can be trained longer). The models ends with a train loss of\n0.11\nand test loss of\n0.10\n. The difference between the two is mostly due to the regularization term being added to the loss during training (worth about 0.01).\nHere's a visualization of our new results:\nThey look pretty similar to the previous model, the only significant difference being the sparsity of the encoded representations.\nencoded_imgs.mean()\nyields a value\n3.33\n(over our 10,000 test images), whereas with the previous model the same quantity was\n7.30\n. So our new model yields encoded representations that are twice sparser.\nDeep autoencoder\nWe do not have to limit ourselves to a single layer as encoder or decoder, we could instead use a stack of layers, such as:\ninput_img\n=\nkeras\n.\nInput\n(\nshape\n=\n(\n784\n,))\nencoded\n=\nlayers\n.\nDense\n(\n128\n,\nactivation\n=\n'relu'\n)(\ninput_img\n)\nencoded\n=\nlayers\n.\nDense\n(\n64\n,\nactivation\n=\n'relu'\n)(\nencoded\n)\nencoded\n=\nlayers\n.\nDense\n(\n32\n,\nactivation\n=\n'relu'\n)(\nencoded\n)\ndecoded\n=\nlayers\n.\nDense\n(\n64\n,\nactivation\n=\n'relu'\n)(\nencoded\n)\ndecoded\n=\nlayers\n.\nDense\n(\n128\n,\nactivation\n=\n'relu'\n)(\ndecoded\n)\ndecoded\n=\nlayers\n.\nDense\n(\n784\n,\nactivation\n=\n'sigmoid'\n)(\ndecoded\n)\nLet's try this:\nautoencoder\n=\nkeras\n.\nModel\n(\ninput_img\n,\ndecoded\n)\nautoencoder\n.\ncompile\n(\noptimizer\n=\n'adam'\n,\nloss\n=\n'binary_crossentropy'\n)\nautoencoder\n.\nfit\n(\nx_train\n,\nx_train\n,\nepochs\n=\n100\n,\nbatch_size\n=\n256\n,\nshuffle\n=\nTrue\n,\nvalidation_data\n=\n(\nx_test\n,\nx_test\n))\nAfter 100 epochs, it reaches a train and validation loss of ~0.08, a bit better than our previous models. Our reconstructed digits look a bit better too:\nConvolutional autoencoder\nSince our inputs are images, it makes sense to use convolutional neural networks (convnets) as encoders and decoders. In practical settings, autoencoders applied to images are always convolutional autoencoders --they simply perform much better.\nLet's implement one. The encoder will consist in a stack of\nConv2D\nand\nMaxPooling2D\nlayers (max pooling being used for spatial down-sampling), while the decoder will consist in a stack of\nConv2D\nand\nUpSampling2D\nlayers.\nimport\nkeras\nfrom\nkeras\nimport\nlayers\ninput_img\n=\nkeras\n.\nInput\n(\nshape\n=\n(\n28\n,\n28\n,\n1\n))\nx\n=\nlayers\n.\nConv2D\n(\n16\n,\n(\n3\n,\n3\n),\nactivation\n=\n'relu'\n,\npadding\n=\n'same'\n)(\ninput_img\n)\nx\n=\nlayers\n.\nMaxPooling2D\n((\n2\n,\n2\n),\npadding\n=\n'same'\n)(\nx\n)\nx\n=\nlayers\n.\nConv2D\n(\n8\n,\n(\n3\n,\n3\n),\nactivation\n=\n'relu'\n,\npadding\n=\n'same'\n)(\nx\n)\nx\n=\nlayers\n.\nMaxPooling2D\n((\n2\n,\n2\n),\npadding\n=\n'same'\n)(\nx\n)\nx\n=\nlayers\n.\nConv2D\n(\n8\n,\n(\n3\n,\n3\n),\nactivation\n=\n'relu'\n,\npadding\n=\n'same'\n)(\nx\n)\nencoded\n=\nlayers\n.\nMaxPooling2D\n((\n2\n,\n2\n),\npadding\n=\n'same'\n)(\nx\n)\n# at this point the representation is (4, 4, 8) i.e. 128-dimensional\nx\n=\nlayers\n.\nConv2D\n(\n8\n,\n(\n3\n,\n3\n),\nactivation\n=\n'relu'\n,\npadding\n=\n'same'\n)(\nencoded\n)\nx\n=\nlayers\n.\nUpSampling2D\n((\n2\n,\n2\n))(\nx\n)\nx\n=\nlayers\n.\nConv2D\n(\n8\n,\n(\n3\n,\n3\n),\nactivation\n=\n'relu'\n,\npadding\n=\n'same'\n)(\nx\n)\nx\n=\nlayers\n.\nUpSampling2D\n((\n2\n,\n2\n))(\nx\n)\nx\n=\nlayers\n.\nConv2D\n(\n16\n,\n(\n3\n,\n3\n),\nactivation\n=\n'relu'\n)(\nx\n)\nx\n=\nlayers\n.\nUpSampling2D\n((\n2\n,\n2\n))(\nx\n)\ndecoded\n=\nlayers\n.\nConv2D\n(\n1\n,\n(\n3\n,\n3\n),\nactivation\n=\n'sigmoid'\n,\npadding\n=\n'same'\n)(\nx\n)\nautoencoder\n=\nkeras\n.\nModel\n(\ninput_img\n,\ndecoded\n)\nautoencoder\n.\ncompile\n(\noptimizer\n=\n'adam'\n,\nloss\n=\n'binary_crossentropy'\n)\nTo train it, we will use the original MNIST digits with shape\n(samples, 3, 28, 28)\n, and we will just normalize pixel values between 0 and 1.\nfrom\nkeras.datasets\nimport\nmnist\nimport\nnumpy\nas\nnp\n(\nx_train\n,\n_\n),\n(\nx_test\n,\n_\n)\n=\nmnist\n.\nload_data\n()\nx_train\n=\nx_train\n.\nastype\n(\n'float32'\n)\n/\n255.\nx_test\n=\nx_test\n.\nastype\n(\n'float32'\n)\n/\n255.\nx_train\n=\nnp\n.\nreshape\n(\nx_train\n,\n(\nlen\n(\nx_train\n),\n28\n,\n28\n,\n1\n))\nx_test\n=\nnp\n.\nreshape\n(\nx_test\n,\n(\nlen\n(\nx_test\n),\n28\n,\n28\n,\n1\n))\nLet's train this model for 50 epochs. For the sake of demonstrating how to visualize the results of a model during training, we will be using\nthe TensorFlow backend\nand the TensorBoard callback.\nFirst, let's open up a terminal and start a TensorBoard server that will read logs stored at\n/tmp/autoencoder\n.\ntensorboard --logdir\n=\n/tmp/autoencoder\nThen let's train our model. In the\ncallbacks\nlist we pass an instance of the\nTensorBoard\ncallback. After every epoch, this callback will write logs to\n/tmp/autoencoder\n, which can be read by our TensorBoard server.\nfrom\nkeras.callbacks\nimport\nTensorBoard\nautoencoder\n.\nfit\n(\nx_train\n,\nx_train\n,\nepochs\n=\n50\n,\nbatch_size\n=\n128\n,\nshuffle\n=\nTrue\n,\nvalidation_data\n=\n(\nx_test\n,\nx_test\n),\ncallbacks\n=\n[\nTensorBoard\n(\nlog_dir\n=\n'/tmp/autoencoder'\n)])\nThis allows us to monitor training in the TensorBoard web interface (by navighating to\nhttp://0.0.0.0:6006\n):\nThe model converges to a loss of 0.094, significantly better than our previous models (this is in large part due to the higher entropic capacity of the encoded representation, 128 dimensions vs. 32 previously). Let's take a look at the reconstructed digits:\ndecoded_imgs\n=\nautoencoder\n.\npredict\n(\nx_test\n)\nn\n=\n10\nplt\n.\nfigure\n(\nfigsize\n=\n(\n20\n,\n4\n))\nfor\ni\nin\nrange\n(\n1\n,\nn\n+\n1\n):\n# Display original\nax\n=\nplt\n.\nsubplot\n(\n2\n,\nn\n,\ni\n)\nplt\n.\nimshow\n(\nx_test\n[\ni\n]\n.\nreshape\n(\n28\n,\n28\n))\nplt\n.\ngray\n()\nax\n.\nget_xaxis\n()\n.\nset_visible\n(\nFalse\n)\nax\n.\nget_yaxis\n()\n.\nset_visible\n(\nFalse\n)\n# Display reconstruction\nax\n=\nplt\n.\nsubplot\n(\n2\n,\nn\n,\ni\n+\nn\n)\nplt\n.\nimshow\n(\ndecoded_imgs\n[\ni\n]\n.\nreshape\n(\n28\n,\n28\n))\nplt\n.\ngray\n()\nax\n.\nget_xaxis\n()\n.\nset_visible\n(\nFalse\n)\nax\n.\nget_yaxis\n()\n.\nset_visible\n(\nFalse\n)\nplt\n.\nshow\n()\nWe can also have a look at the 128-dimensional encoded representations. These representations are 8x4x4, so we reshape them to 4x32 in order to be able to display them as grayscale images.\nencoder\n=\nkeras\n.\nModel\n(\ninput_img\n,\nencoded\n)\nencoded_imgs\n=\nencoder\n.\npredict\n(\nx_test\n)\nn\n=\n10\nplt\n.\nfigure\n(\nfigsize\n=\n(\n20\n,\n8\n))\nfor\ni\nin\nrange\n(\n1\n,\nn\n+\n1\n):\nax\n=\nplt\n.\nsubplot\n(\n1\n,\nn\n,\ni\n)\nplt\n.\nimshow\n(\nencoded_imgs\n[\ni\n]\n.\nreshape\n((\n4\n,\n4\n*\n8\n))\n.\nT\n)\nplt\n.\ngray\n()\nax\n.\nget_xaxis\n()\n.\nset_visible\n(\nFalse\n)\nax\n.\nget_yaxis\n()\n.\nset_visible\n(\nFalse\n)\nplt\n.\nshow\n()\nApplication to image denoising\nLet's put our convolutional autoencoder to work on an image denoising problem. It's simple: we will train the autoencoder to map noisy digits images to clean digits images.\nHere's how we will generate synthetic noisy digits: we just apply a gaussian noise matrix and clip the images between 0 and 1.\nfrom\nkeras.datasets\nimport\nmnist\nimport\nnumpy\nas\nnp\n(\nx_train\n,\n_\n),\n(\nx_test\n,\n_\n)\n=\nmnist\n.\nload_data\n()\nx_train\n=\nx_train\n.\nastype\n(\n'float32'\n)\n/\n255.\nx_test\n=\nx_test\n.\nastype\n(\n'float32'\n)\n/\n255.\nx_train\n=\nnp\n.\nreshape\n(\nx_train\n,\n(\nlen\n(\nx_train\n),\n28\n,\n28\n,\n1\n))\nx_test\n=\nnp\n.\nreshape\n(\nx_test\n,\n(\nlen\n(\nx_test\n),\n28\n,\n28\n,\n1\n))\nnoise_factor\n=\n0.5\nx_train_noisy\n=\nx_train\n+\nnoise_factor\n*\nnp\n.\nrandom\n.\nnormal\n(\nloc\n=\n0.0\n,\nscale\n=\n1.0\n,\nsize\n=\nx_train\n.\nshape\n)\nx_test_noisy\n=\nx_test\n+\nnoise_factor\n*\nnp\n.\nrandom\n.\nnormal\n(\nloc\n=\n0.0\n,\nscale\n=\n1.0\n,\nsize\n=\nx_test\n.\nshape\n)\nx_train_noisy\n=\nnp\n.\nclip\n(\nx_train_noisy\n,\n0.\n,\n1.\n)\nx_test_noisy\n=\nnp\n.\nclip\n(\nx_test_noisy\n,\n0.\n,\n1.\n)\nHere's what the noisy digits look like:\nn\n=\n10\nplt\n.\nfigure\n(\nfigsize\n=\n(\n20\n,\n2\n))\nfor\ni\nin\nrange\n(\n1\n,\nn\n+\n1\n):\nax\n=\nplt\n.\nsubplot\n(\n1\n,\nn\n,\ni\n)\nplt\n.\nimshow\n(\nx_test_noisy\n[\ni\n]\n.\nreshape\n(\n28\n,\n28\n))\nplt\n.\ngray\n()\nax\n.\nget_xaxis\n()\n.\nset_visible\n(\nFalse\n)\nax\n.\nget_yaxis\n()\n.\nset_visible\n(\nFalse\n)\nplt\n.\nshow\n()\nIf you squint you can still recognize them, but barely. Can our autoencoder learn to recover the original digits? Let's find out.\nCompared to the previous convolutional autoencoder, in order to improve the quality of the reconstructed, we'll use a slightly different model with more filters per layer:\ninput_img\n=\nkeras\n.\nInput\n(\nshape\n=\n(\n28\n,\n28\n,\n1\n))\nx\n=\nlayers\n.\nConv2D\n(\n32\n,\n(\n3\n,\n3\n),\nactivation\n=\n'relu'\n,\npadding\n=\n'same'\n)(\ninput_img\n)\nx\n=\nlayers\n.\nMaxPooling2D\n((\n2\n,\n2\n),\npadding\n=\n'same'\n)(\nx\n)\nx\n=\nlayers\n.\nConv2D\n(\n32\n,\n(\n3\n,\n3\n),\nactivation\n=\n'relu'\n,\npadding\n=\n'same'\n)(\nx\n)\nencoded\n=\nlayers\n.\nMaxPooling2D\n((\n2\n,\n2\n),\npadding\n=\n'same'\n)(\nx\n)\n# At this point the representation is (7, 7, 32)\nx\n=\nlayers\n.\nConv2D\n(\n32\n,\n(\n3\n,\n3\n),\nactivation\n=\n'relu'\n,\npadding\n=\n'same'\n)(\nencoded\n)\nx\n=\nlayers\n.\nUpSampling2D\n((\n2\n,\n2\n))(\nx\n)\nx\n=\nlayers\n.\nConv2D\n(\n32\n,\n(\n3\n,\n3\n),\nactivation\n=\n'relu'\n,\npadding\n=\n'same'\n)(\nx\n)\nx\n=\nlayers\n.\nUpSampling2D\n((\n2\n,\n2\n))(\nx\n)\ndecoded\n=\nlayers\n.\nConv2D\n(\n1\n,\n(\n3\n,\n3\n),\nactivation\n=\n'sigmoid'\n,\npadding\n=\n'same'\n)(\nx\n)\nautoencoder\n=\nkeras\n.\nModel\n(\ninput_img\n,\ndecoded\n)\nautoencoder\n.\ncompile\n(\noptimizer\n=\n'adam'\n,\nloss\n=\n'binary_crossentropy'\n)\nLet's train it for 100 epochs:\nautoencoder\n.\nfit\n(\nx_train_noisy\n,\nx_train\n,\nepochs\n=\n100\n,\nbatch_size\n=\n128\n,\nshuffle\n=\nTrue\n,\nvalidation_data\n=\n(\nx_test_noisy\n,\nx_test\n),\ncallbacks\n=\n[\nTensorBoard\n(\nlog_dir\n=\n'/tmp/tb'\n,\nhistogram_freq\n=\n0\n,\nwrite_graph\n=\nFalse\n)])\nNow let's take a look at the results. Top, the noisy digits fed to the network, and bottom, the digits are reconstructed by the network.\nIt seems to work pretty well. If you scale this process to a bigger convnet, you can start building document denoising or audio denoising models.\nKaggle has an interesting dataset to get you started\n.\nSequence-to-sequence autoencoder\nIf you inputs are sequences, rather than vectors or 2D images, then you may want to use as encoder and decoder a type of model that can capture temporal structure, such as a LSTM. To build a LSTM-based autoencoder, first use a LSTM encoder to turn your input sequences into a single vector that contains information about the entire sequence, then repeat this vector\nn\ntimes (where\nn\nis the number of timesteps in the output sequence), and run a LSTM decoder to turn this constant sequence into the target sequence.\nWe won't be demonstrating that one on any specific dataset. We will just put a code example here for future reference for the reader!\ntimesteps\n=\n...\n# Length of your sequences\ninput_dim\n=\n...\nlatent_dim\n=\n...\ninputs\n=\nkeras\n.\nInput\n(\nshape\n=\n(\ntimesteps\n,\ninput_dim\n))\nencoded\n=\nlayers\n.\nLSTM\n(\nlatent_dim\n)(\ninputs\n)\ndecoded\n=\nlayers\n.\nRepeatVector\n(\ntimesteps\n)(\nencoded\n)\ndecoded\n=\nlayers\n.\nLSTM\n(\ninput_dim\n,\nreturn_sequences\n=\nTrue\n)(\ndecoded\n)\nsequence_autoencoder\n=\nkeras\n.\nModel\n(\ninputs\n,\ndecoded\n)\nencoder\n=\nkeras\n.\nModel\n(\ninputs\n,\nencoded\n)\nVariational autoencoder (VAE)\nVariational autoencoders are a slightly more modern and interesting take on autoencoding.\nWhat is a variational autoencoder, you ask? It's a type of autoencoder with added constraints on the encoded representations being learned. More precisely, it is an autoencoder that learns a\nlatent variable model\nfor its input data. So instead of letting your neural network learn an arbitrary function, you are learning the parameters of a probability distribution modeling your data. If you sample points from this distribution, you can generate new input data samples: a VAE is a \"generative model\".\nHow does a variational autoencoder work?\nFirst, an encoder network turns the input samples\nx\ninto two parameters in a latent space, which we will note\nz_mean\nand\nz_log_sigma\n. Then, we randomly sample similar points\nz\nfrom the latent normal distribution that is assumed to generate the data, via\nz = z_mean + exp(z_log_sigma) * epsilon\n, where\nepsilon\nis a random normal tensor. Finally, a decoder network maps these latent space points back to the original input data.\nThe parameters of the model are trained via two loss functions: a reconstruction loss forcing the decoded samples to match the initial inputs (just like in our previous autoencoders), and the KL divergence between the learned latent distribution and the prior distribution, acting as a regularization term. You could actually get rid of this latter term entirely, although it does help in learning well-formed latent spaces and reducing overfitting to the training data.\nBecause a VAE is a more complex example, we have made the code available on Github as\na standalone script\n. Here we will review step by step how the model is created.\nFirst, here's our encoder network, mapping inputs to our latent distribution parameters:\noriginal_dim\n=\n28\n*\n28\nintermediate_dim\n=\n64\nlatent_dim\n=\n2\ninputs\n=\nkeras\n.\nInput\n(\nshape\n=\n(\noriginal_dim\n,))\nh\n=\nlayers\n.\nDense\n(\nintermediate_dim\n,\nactivation\n=\n'relu'\n)(\ninputs\n)\nz_mean\n=\nlayers\n.\nDense\n(\nlatent_dim\n)(\nh\n)\nz_log_sigma\n=\nlayers\n.\nDense\n(\nlatent_dim\n)(\nh\n)\nWe can use these parameters to sample new similar points from the latent space:\nfrom\nkeras\nimport\nbackend\nas\nK\ndef\nsampling\n(\nargs\n):\nz_mean\n,\nz_log_sigma\n=\nargs\nepsilon\n=\nK\n.\nrandom_normal\n(\nshape\n=\n(\nK\n.\nshape\n(\nz_mean\n)[\n0\n],\nlatent_dim\n),\nmean\n=\n0.\n,\nstddev\n=\n0.1\n)\nreturn\nz_mean\n+\nK\n.\nexp\n(\nz_log_sigma\n)\n*\nepsilon\nz\n=\nlayers\n.\nLambda\n(\nsampling\n)([\nz_mean\n,\nz_log_sigma\n])\nFinally, we can map these sampled latent points back to reconstructed inputs:\n# Create encoder\nencoder\n=\nkeras\n.\nModel\n(\ninputs\n,\n[\nz_mean\n,\nz_log_sigma\n,\nz\n],\nname\n=\n'encoder'\n)\n# Create decoder\nlatent_inputs\n=\nkeras\n.\nInput\n(\nshape\n=\n(\nlatent_dim\n,),\nname\n=\n'z_sampling'\n)\nx\n=\nlayers\n.\nDense\n(\nintermediate_dim\n,\nactivation\n=\n'relu'\n)(\nlatent_inputs\n)\noutputs\n=\nlayers\n.\nDense\n(\noriginal_dim\n,\nactivation\n=\n'sigmoid'\n)(\nx\n)\ndecoder\n=\nkeras\n.\nModel\n(\nlatent_inputs\n,\noutputs\n,\nname\n=\n'decoder'\n)\n# instantiate VAE model\noutputs\n=\ndecoder\n(\nencoder\n(\ninputs\n)[\n2\n])\nvae\n=\nkeras\n.\nModel\n(\ninputs\n,\noutputs\n,\nname\n=\n'vae_mlp'\n)\nWhat we've done so far allows us to instantiate 3 models:\nan end-to-end autoencoder mapping inputs to reconstructions\nan encoder mapping inputs to the latent space\na generator that can take points on the latent space and will output the corresponding reconstructed samples.\nWe train the model using the end-to-end model, with a custom loss function: the sum of a reconstruction term, and the KL divergence regularization term.\nreconstruction_loss\n=\nkeras\n.\nlosses\n.\nbinary_crossentropy\n(\ninputs\n,\noutputs\n)\nreconstruction_loss\n*=\noriginal_dim\nkl_loss\n=\n1\n+\nz_log_sigma\n-\nK\n.\nsquare\n(\nz_mean\n)\n-\nK\n.\nexp\n(\nz_log_sigma\n)\nkl_loss\n=\nK\n.\nsum\n(\nkl_loss\n,\naxis\n=-\n1\n)\nkl_loss\n*=\n-\n0.5\nvae_loss\n=\nK\n.\nmean\n(\nreconstruction_loss\n+\nkl_loss\n)\nvae\n.\nadd_loss\n(\nvae_loss\n)\nvae\n.\ncompile\n(\noptimizer\n=\n'adam'\n)\nWe train our VAE on MNIST digits:\n(\nx_train\n,\ny_train\n),\n(\nx_test\n,\ny_test\n)\n=\nmnist\n.\nload_data\n()\nx_train\n=\nx_train\n.\nastype\n(\n'float32'\n)\n/\n255.\nx_test\n=\nx_test\n.\nastype\n(\n'float32'\n)\n/\n255.\nx_train\n=\nx_train\n.\nreshape\n((\nlen\n(\nx_train\n),\nnp\n.\nprod\n(\nx_train\n.\nshape\n[\n1\n:])))\nx_test\n=\nx_test\n.\nreshape\n((\nlen\n(\nx_test\n),\nnp\n.\nprod\n(\nx_test\n.\nshape\n[\n1\n:])))\nvae\n.\nfit\n(\nx_train\n,\nx_train\n,\nepochs\n=\n100\n,\nbatch_size\n=\n32\n,\nvalidation_data\n=\n(\nx_test\n,\nx_test\n))\nBecause our latent space is two-dimensional, there are a few cool visualizations that can be done at this point. One is to look at the neighborhoods of different classes on the latent 2D plane:\nx_test_encoded\n=\nencoder\n.\npredict\n(\nx_test\n,\nbatch_size\n=\nbatch_size\n)\nplt\n.\nfigure\n(\nfigsize\n=\n(\n6\n,\n6\n))\nplt\n.\nscatter\n(\nx_test_encoded\n[:,\n0\n],\nx_test_encoded\n[:,\n1\n],\nc\n=\ny_test\n)\nplt\n.\ncolorbar\n()\nplt\n.\nshow\n()\nEach of these colored clusters is a type of digit. Close clusters are digits that are structurally similar (i.e. digits that share information in the latent space).\nBecause the VAE is a generative model, we can also use it to generate new digits! Here we will scan the latent plane, sampling latent points at regular intervals, and generating the corresponding digit for each of these points. This gives us a visualization of the latent manifold that \"generates\" the MNIST digits.\n# Display a 2D manifold of the digits\nn\n=\n15\n# figure with 15x15 digits\ndigit_size\n=\n28\nfigure\n=\nnp\n.\nzeros\n((\ndigit_size\n*\nn\n,\ndigit_size\n*\nn\n))\n# We will sample n points within [-15, 15] standard deviations\ngrid_x\n=\nnp\n.\nlinspace\n(\n-\n15\n,\n15\n,\nn\n)\ngrid_y\n=\nnp\n.\nlinspace\n(\n-\n15\n,\n15\n,\nn\n)\nfor\ni\n,\nyi\nin\nenumerate\n(\ngrid_x\n):\nfor\nj\n,\nxi\nin\nenumerate\n(\ngrid_y\n):\nz_sample\n=\nnp\n.\narray\n([[\nxi\n,\nyi\n]])\nx_decoded\n=\ndecoder\n.\npredict\n(\nz_sample\n)\ndigit\n=\nx_decoded\n[\n0\n]\n.\nreshape\n(\ndigit_size\n,\ndigit_size\n)\nfigure\n[\ni\n*\ndigit_size\n:\n(\ni\n+\n1\n)\n*\ndigit_size\n,\nj\n*\ndigit_size\n:\n(\nj\n+\n1\n)\n*\ndigit_size\n]\n=\ndigit\nplt\n.\nfigure\n(\nfigsize\n=\n(\n10\n,\n10\n))\nplt\n.\nimshow\n(\nfigure\n)\nplt\n.\nshow\n()\nThat's it! If you have suggestions for more topics to be covered in this post (or in future posts), you can contact me on Twitter at\n@fchollet\n.\nReferences\n[1]\nWhy does unsupervised pre-training help deep learning?\n[2]\nBatch normalization: Accelerating deep network training by reducing internal covariate shift.\n[3]\nDeep Residual Learning for Image Recognition\n[4]\nAuto-Encoding Variational Bayes",
  "metadata": {
    "title": "Building Autoencoders in Keras",
    "description": null,
    "keywords": null,
    "author": null
  },
  "paywall": {
    "listed_paid": false,
    "detected": false,
    "marker": "",
    "public_only": false,
    "policy": "付费/会员内容只保留页面公开可见部分，不绕过付费墙"
  },
  "crawl": {
    "success": true,
    "status_code": 200,
    "error": ""
  },
  "code_blocks": [
    {
      "file": "attachments/d8f51c6555cb/code/body-000.txt",
      "chars": 579,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-001.txt",
      "chars": 99,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-002.txt",
      "chars": 280,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-003.txt",
      "chars": 66,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-004.txt",
      "chars": 98,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-005.txt",
      "chars": 261,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-006.txt",
      "chars": 173,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-007.txt",
      "chars": 161,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-008.txt",
      "chars": 559,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-009.txt",
      "chars": 374,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-010.txt",
      "chars": 377,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-011.txt",
      "chars": 287,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-012.txt",
      "chars": 1009,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-013.txt",
      "chars": 295,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-014.txt",
      "chars": 38,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-015.txt",
      "chars": 283,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-016.txt",
      "chars": 505,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-017.txt",
      "chars": 329,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-018.txt",
      "chars": 603,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-019.txt",
      "chars": 241,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-020.txt",
      "chars": 748,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-021.txt",
      "chars": 283,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-022.txt",
      "chars": 380,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-023.txt",
      "chars": 246,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-024.txt",
      "chars": 305,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-025.txt",
      "chars": 482,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-026.txt",
      "chars": 326,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-027.txt",
      "chars": 390,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-028.txt",
      "chars": 183,
      "language": "text"
    },
    {
      "file": "attachments/d8f51c6555cb/code/body-029.txt",
      "chars": 660,
      "language": "text"
    }
  ],
  "images": [
    {
      "status": "ok",
      "url": "https://blog.keras.io/img/ae/autoencoder_schema.jpg",
      "bytes": 33094,
      "reused": false,
      "local_path": "attachments/d8f51c6555cb/images/body-9c1f26de5ce403a32b2b.jpg",
      "src": "https://blog.keras.io/img/ae/autoencoder_schema.jpg",
      "alt": "Autoencoder: schema",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://blog.keras.io/img/ae/jigsaw-puzzle.png",
      "bytes": 563006,
      "reused": false,
      "local_path": "attachments/d8f51c6555cb/images/body-2929e02dd4ff2f352158.png",
      "src": "https://blog.keras.io/img/ae/jigsaw-puzzle.png",
      "alt": "jigsaw puzzle task",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://blog.keras.io/img/ae/basic_ae_32.png",
      "bytes": 47015,
      "reused": false,
      "local_path": "attachments/d8f51c6555cb/images/body-08fab3b8ee1bb24c74a6.png",
      "src": "https://blog.keras.io/img/ae/basic_ae_32.png",
      "alt": "basic autoencoder",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://blog.keras.io/img/ae/sparse_ae_32.png",
      "bytes": 63996,
      "reused": false,
      "local_path": "attachments/d8f51c6555cb/images/body-93c94510df89e5fa8895.png",
      "src": "https://blog.keras.io/img/ae/sparse_ae_32.png",
      "alt": "sparse autoencoder",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://blog.keras.io/img/ae/deep_ae_32.png",
      "bytes": 61567,
      "reused": false,
      "local_path": "attachments/d8f51c6555cb/images/body-01f38176f811aacfb83b.png",
      "src": "https://blog.keras.io/img/ae/deep_ae_32.png",
      "alt": "deep autoencoder",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://blog.keras.io/img/ae/tb_curves.png",
      "bytes": 65054,
      "reused": false,
      "local_path": "attachments/d8f51c6555cb/images/body-57ecb8e9f056494cbd87.png",
      "src": "https://blog.keras.io/img/ae/tb_curves.png",
      "alt": "tensorboard curves",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://blog.keras.io/img/ae/deep_conv_ae_128.png",
      "bytes": 61998,
      "reused": false,
      "local_path": "attachments/d8f51c6555cb/images/body-7511550713ce7f43d054.png",
      "src": "https://blog.keras.io/img/ae/deep_conv_ae_128.png",
      "alt": "convolutional autoencoder",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://blog.keras.io/img/ae/encoded_representations.png",
      "bytes": 145038,
      "reused": false,
      "local_path": "attachments/d8f51c6555cb/images/body-5d639dccd30dc129560c.png",
      "src": "https://blog.keras.io/img/ae/encoded_representations.png",
      "alt": "latent representations",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://blog.keras.io/img/ae/noisy_digits.png",
      "bytes": 87743,
      "reused": false,
      "local_path": "attachments/d8f51c6555cb/images/body-1f2d7b00e7bedfedfded.png",
      "src": "https://blog.keras.io/img/ae/noisy_digits.png",
      "alt": "noisy digits",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://blog.keras.io/img/ae/denoised_digits.png",
      "bytes": 115230,
      "reused": false,
      "local_path": "attachments/d8f51c6555cb/images/body-56e7eff6b43438b90a86.png",
      "src": "https://blog.keras.io/img/ae/denoised_digits.png",
      "alt": "denoised digits",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://blog.keras.io/img/ae/vae_classes_plane.png",
      "bytes": 352385,
      "reused": false,
      "local_path": "attachments/d8f51c6555cb/images/body-5c4abbe73f311f3714c7.png",
      "src": "https://blog.keras.io/img/ae/vae_classes_plane.png",
      "alt": "vae classes plane",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://blog.keras.io/img/ae/vae_digits_manifold.png",
      "bytes": 466638,
      "reused": false,
      "local_path": "attachments/d8f51c6555cb/images/body-1d6d2e4ccc0e5317ab52.png",
      "src": "https://blog.keras.io/img/ae/vae_digits_manifold.png",
      "alt": "vae classes plane",
      "downloaded": true
    }
  ],
  "audio": [],
  "videos": [],
  "links": [
    {
      "href": "https://en.wikipedia.org/wiki/T-distributed_stochastic_neighbor_embedding",
      "label": "t-SNE",
      "type": "a"
    },
    {
      "href": "https://github.com/kylemcdonald/Parametric-t-SNE/blob/master/Parametric%20t-SNE%20(Keras).ipynb",
      "label": "is available on Github",
      "type": "a"
    },
    {
      "href": "http://scikit-learn.org/stable/modules/generated/sklearn.manifold.TSNE.html",
      "label": "scikit-learn",
      "type": "a"
    },
    {
      "href": "http://arxiv.org/abs/1603.09246",
      "label": "Unsupervised Learning of Visual Representations by Solving Jigsaw Puzzles",
      "type": "a"
    },
    {
      "href": "https://en.wikipedia.org/wiki/Principal_component_analysis",
      "label": "PCA (principal component analysis)",
      "type": "a"
    },
    {
      "href": "https://www.kaggle.com/c/denoising-dirty-documents",
      "label": "Kaggle has an interesting dataset to get you started",
      "type": "a"
    },
    {
      "href": "https://en.wikipedia.org/wiki/Latent_variable_model",
      "label": "latent variable model",
      "type": "a"
    },
    {
      "href": "https://github.com/fchollet/keras/blob/master/examples/variational_autoencoder.py",
      "label": "a standalone script",
      "type": "a"
    },
    {
      "href": "https://twitter.com/fchollet",
      "label": "@fchollet",
      "type": "a"
    },
    {
      "href": "http://www.jmlr.org/papers/volume11/erhan10a/erhan10a.pdf",
      "label": "Why does unsupervised pre-training help deep learning?",
      "type": "a"
    },
    {
      "href": "http://arxiv.org/abs/1502.03167",
      "label": "Batch normalization: Accelerating deep network training by reducing internal covariate shift.",
      "type": "a"
    },
    {
      "href": "http://arxiv.org/abs/1512.03385",
      "label": "Deep Residual Learning for Image Recognition",
      "type": "a"
    },
    {
      "href": "http://arxiv.org/abs/1312.6114",
      "label": "Auto-Encoding Variational Bayes",
      "type": "a"
    }
  ],
  "repair_version": 2,
  "repaired_at": "2026-10-14T10:53:14Z",
  "source_url": "https://blog.keras.io/building-autoencoders-in-keras.html",
  "papers": [
    {
      "status": "ok",
      "url": "https://arxiv.org/pdf/1603.09246",
      "bytes": 7884325,
      "reused": true,
      "local_path": "papers/repair/3fad0ba70064de418e86.pdf"
    },
    {
      "status": "ok",
      "url": "http://www.jmlr.org/papers/volume11/erhan10a/erhan10a.pdf",
      "bytes": 1291944,
      "reused": false,
      "local_path": "papers/repair/941758d15a9834507a56.pdf"
    },
    {
      "status": "ok",
      "url": "https://arxiv.org/pdf/1502.03167",
      "bytes": 173548,
      "reused": false,
      "local_path": "papers/repair/e6b3d10d960cb5f3ddac.pdf"
    },
    {
      "status": "ok",
      "url": "https://arxiv.org/pdf/1512.03385",
      "bytes": 819383,
      "reused": true,
      "local_path": "papers/repair/523013cf51110acbfe60.pdf"
    },
    {
      "status": "ok",
      "url": "https://arxiv.org/pdf/1312.6114",
      "bytes": 3926758,
      "reused": false,
      "local_path": "papers/repair/24a95827202f9fab26f8.pdf"
    }
  ],
  "actual_video_files": [],
  "body_chars": 28356,
  "expected_body_images": 12,
  "discovery_status": "ok",
  "delivery_status": "ok",
  "missing_reason": "",
  "raw_html": "attachments/d8f51c6555cb/pages/source.html",
  "local_html": "attachments/d8f51c6555cb/pages/article.html",
  "body_base_path": "attachments/d8f51c6555cb/pages/",
  "body_markdown_path": "attachments/d8f51c6555cb/pages/article.md"
}