{"id":2398,"date":"2019-07-25T19:00:27","date_gmt":"2019-07-25T19:00:27","guid":{"rendered":"https:\/\/www.aiproblog.com\/index.php\/2019\/07\/25\/how-to-develop-a-least-squares-generative-adversarial-network-lsgan-in-keras\/"},"modified":"2019-07-25T19:00:27","modified_gmt":"2019-07-25T19:00:27","slug":"how-to-develop-a-least-squares-generative-adversarial-network-lsgan-in-keras","status":"publish","type":"post","link":"https:\/\/www.aiproblog.com\/index.php\/2019\/07\/25\/how-to-develop-a-least-squares-generative-adversarial-network-lsgan-in-keras\/","title":{"rendered":"How to Develop a Least Squares Generative Adversarial Network (LSGAN) in Keras"},"content":{"rendered":"<p>Author: Jason Brownlee<\/p>\n<div>\n<p>The Least Squares Generative Adversarial Network, or LSGAN for short, is an extension to the GAN architecture that addresses the problem of vanishing gradients and loss saturation.<\/p>\n<p>It is motivated by the desire to provide a signal to the generator about fake samples that are far from the discriminator model\u2019s decision boundary for classifying them as real or fake. The further the generated images are from the decision boundary, the larger the error signal provided to the generator, encouraging the generation of more realistic images.<\/p>\n<p>The LSGAN can be implemented with a minor change to the output layer of the discriminator layer and the adoption of the least squares, or L2, loss function.<\/p>\n<p>In this tutorial, you will discover how to develop a least squares generative adversarial network.<\/p>\n<p>After completing this tutorial, you will know:<\/p>\n<ul>\n<li>The LSGAN addresses vanishing gradients and loss saturation of the deep convolutional GAN.<\/li>\n<li>The LSGAN can be implemented by a mean squared error or L2 loss function for the discriminator model.<\/li>\n<li>How to implement the LSGAN model for generating handwritten digits for the MNIST dataset.<\/li>\n<\/ul>\n<p>Discover how to develop DCGANs, conditional GANs, Pix2Pix, CycleGANs, and more with Keras <a href=\"https:\/\/machinelearningmastery.com\/generative_adversarial_networks\/\" rel=\"nofollow\">in my new GANs book<\/a>, with 29 step-by-step tutorials and full source code.<\/p>\n<p>Let\u2019s get started.<\/p>\n<div id=\"attachment_8311\" style=\"width: 650px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-8311\" class=\"size-full wp-image-8311\" src=\"https:\/\/machinelearningmastery.com\/wp-content\/uploads\/2019\/07\/How-to-Develop-a-Least-Squares-Generative-Adversarial-Network-LSGAN-for-Image-Generation.jpg\" alt=\"How to Develop a Least Squares Generative Adversarial Network (LSGAN) for Image Generation\" width=\"640\" height=\"428\" srcset=\"http:\/\/3qeqpr26caki16dnhd19sv6by6v.wpengine.netdna-cdn.com\/wp-content\/uploads\/2019\/07\/How-to-Develop-a-Least-Squares-Generative-Adversarial-Network-LSGAN-for-Image-Generation.jpg 640w, http:\/\/3qeqpr26caki16dnhd19sv6by6v.wpengine.netdna-cdn.com\/wp-content\/uploads\/2019\/07\/How-to-Develop-a-Least-Squares-Generative-Adversarial-Network-LSGAN-for-Image-Generation-300x201.jpg 300w\" sizes=\"(max-width: 640px) 100vw, 640px\"><\/p>\n<p id=\"caption-attachment-8311\" class=\"wp-caption-text\">How to Develop a Least Squares Generative Adversarial Network (LSGAN) for Image Generation<br \/>Photo by <a href=\"https:\/\/www.flickr.com\/photos\/alyssablack\/15753560981\/\">alyssa BLACK<\/a>., some rights reserved.<\/p>\n<\/div>\n<h2>Tutorial Overview<\/h2>\n<p>This tutorial is divided into three parts; they are:<\/p>\n<ol>\n<li>What Is Least Squares GAN<\/li>\n<li>How to Develop an LSGAN for MNIST Handwritten Digits<\/li>\n<li>How to Generate Images With LSGAN<\/li>\n<\/ol>\n<h2>What Is Least Squares GAN<\/h2>\n<p>The standard Generative Adversarial Network, or GAN for short, is an effective architecture for training an unsupervised generative model.<\/p>\n<p>The architecture involves training a discriminator model to tell the difference between real (from the dataset) and fake (generated) images, and using the discriminator, in turn, to train the generator model. The generator is updated in such a way that it is encouraged to generate images that are more likely to fool the discriminator.<\/p>\n<p>The discriminator is a binary classifier and is trained using binary cross-entropy loss function. A limitation of this loss function is that it is primarily concerned with whether the predictions are correct or not, and less so with how correct or incorrect they might be.<\/p>\n<blockquote>\n<p>\u2026 when we use the fake samples to update the generator by making the discriminator believe they are from real data, it will cause almost no error because they are on the correct side, i.e., the real data side, of the decision boundary<\/p>\n<\/blockquote>\n<p>\u2014 <a href=\"https:\/\/arxiv.org\/abs\/1611.04076\">Least Squares Generative Adversarial Networks<\/a>, 2016.<\/p>\n<p>This can be conceptualized in two dimensions as a line or decision boundary separating dots that represent real and fake images. The discriminator is responsible for devising the decision boundary to best separate real and fake images and the generator is responsible for creating new points that look like real points, confusing the discriminator.<\/p>\n<p>The choice of cross-entropy loss means that points generated far from the boundary are right or wrong, but provide very little gradient information to the generator on how to generate better images.<\/p>\n<p>This small gradient for generated images far from the decision boundary is referred to as a vanishing gradient problem or a loss saturation. The loss function is unable to give a strong signal as to how to best update the model.<\/p>\n<p>The Least Squares Generative Adversarial Network, or LSGAN for short, is an extension to the GAN architecture proposed by <a href=\"https:\/\/xudongmao.github.io\/\">Xudong Mao<\/a>, et al. in their 2016 paper titled \u201c<a href=\"https:\/\/arxiv.org\/abs\/1611.04076\">Least Squares Generative Adversarial Networks<\/a>.\u201d The LSGAN is a modification to the GAN architecture that changes the loss function for the discriminator from binary cross entropy to a least squares loss.<\/p>\n<p>The motivation for this change is that the least squares loss will penalize generated images based on their distance from the decision boundary. This will provide a strong gradient signal for generated images that are very different or far from the existing data and address the problem of saturated loss.<\/p>\n<blockquote>\n<p>\u2026 minimizing the objective function of regular GAN suffers from vanishing gradients, which makes it hard to update the generator. LSGANs can relieve this problem because LSGANs penalize samples based on their distances to the decision boundary, which generates more gradients to update the generator.<\/p>\n<\/blockquote>\n<p>\u2014 <a href=\"https:\/\/arxiv.org\/abs\/1611.04076\">Least Squares Generative Adversarial Networks<\/a>, 2016.<\/p>\n<p>This can be conceptualized with a plot, below, taken from the paper, that shows on the left the sigmoid decision boundary (blue) and generated fake points far from the decision boundary (pink), and on the right the least squares decision boundary (red) and the points far from the boundary (pink) given a gradient that moves them closer to the boundary.<\/p>\n<div id=\"attachment_8305\" style=\"width: 650px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-8305\" class=\"size-full wp-image-8305\" src=\"https:\/\/machinelearningmastery.com\/wp-content\/uploads\/2019\/05\/Plot-of-the-Sigmoid-Decision-Boundary-vs-the-Least-Squared-Decision-Boundary-for-Updating-the-Generator.png\" alt=\"Plot of the Sigmoid Decision Boundary vs the Least Squared Decision Boundary for Updating the Generator\" width=\"640\" height=\"244\" srcset=\"http:\/\/3qeqpr26caki16dnhd19sv6by6v.wpengine.netdna-cdn.com\/wp-content\/uploads\/2019\/05\/Plot-of-the-Sigmoid-Decision-Boundary-vs-the-Least-Squared-Decision-Boundary-for-Updating-the-Generator.png 640w, http:\/\/3qeqpr26caki16dnhd19sv6by6v.wpengine.netdna-cdn.com\/wp-content\/uploads\/2019\/05\/Plot-of-the-Sigmoid-Decision-Boundary-vs-the-Least-Squared-Decision-Boundary-for-Updating-the-Generator-300x114.png 300w\" sizes=\"(max-width: 640px) 100vw, 640px\"><\/p>\n<p id=\"caption-attachment-8305\" class=\"wp-caption-text\">Plot of the Sigmoid Decision Boundary vs. the Least Squared Decision Boundary for Updating the Generator.<br \/>Taken from: Least Squares Generative Adversarial Networks.<\/p>\n<\/div>\n<p>In addition to avoiding loss saturation, the LSGAN also results in a more stable training process and the generation of higher quality and larger images than the traditional deep convolutional GAN.<\/p>\n<blockquote>\n<p>First, LSGANs are able to generate higher quality images than regular GANs. Second, LSGANs perform more stable during the learning process.<\/p>\n<\/blockquote>\n<p>\u2014 <a href=\"https:\/\/arxiv.org\/abs\/1611.04076\">Least Squares Generative Adversarial Networks<\/a>, 2016.<\/p>\n<p>The LSGAN can be implemented by using the target values of 1.0 for real and 0.0 for fake images and optimizing the model using the mean squared error (MSE) loss function, e.g. L2 loss. The output layer of the discriminator model must be a linear activation function.<\/p>\n<p>The authors propose a generator and discriminator model architecture, inspired by the VGG model architecture, and use interleaving upsampling and normal convolutional layers in the generator model, seen on the left in the image below.<\/p>\n<div id=\"attachment_8306\" style=\"width: 650px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-8306\" class=\"size-full wp-image-8306\" src=\"https:\/\/machinelearningmastery.com\/wp-content\/uploads\/2019\/05\/Summary-of-the-Generator-left-and-Discriminator-right-Model-Architectures-used-in-LSGAN-Experiments.png\" alt=\"Summary of the Generator (left) and Discriminator (right) Model Architectures used in LSGAN Experiments\" width=\"640\" height=\"357\" srcset=\"http:\/\/3qeqpr26caki16dnhd19sv6by6v.wpengine.netdna-cdn.com\/wp-content\/uploads\/2019\/05\/Summary-of-the-Generator-left-and-Discriminator-right-Model-Architectures-used-in-LSGAN-Experiments.png 640w, http:\/\/3qeqpr26caki16dnhd19sv6by6v.wpengine.netdna-cdn.com\/wp-content\/uploads\/2019\/05\/Summary-of-the-Generator-left-and-Discriminator-right-Model-Architectures-used-in-LSGAN-Experiments-300x167.png 300w\" sizes=\"(max-width: 640px) 100vw, 640px\"><\/p>\n<p id=\"caption-attachment-8306\" class=\"wp-caption-text\">Summary of the Generator (left) and Discriminator (right) Model Architectures used in LSGAN Experiments.<br \/>Taken from: Least Squares Generative Adversarial Networks.<\/p>\n<\/div>\n<div class=\"woo-sc-hr\"><\/div>\n<p><center><\/p>\n<h3>Want to Develop GANs from Scratch?<\/h3>\n<p>Take my free 7-day email crash course now (with sample code).<\/p>\n<p>Click to sign-up and also get a free PDF Ebook version of the course.<\/p>\n<p><a href=\"https:\/\/machinelearningmastery.lpages.co\/leadbox\/162526e1b172a2%3A164f8be4f346dc\/5926953912500224\/\" target=\"_blank\" style=\"background: rgb(255, 206, 10); color: rgb(255, 255, 255); text-decoration: none; font-family: Helvetica, Arial, sans-serif; font-weight: bold; font-size: 16px; line-height: 20px; padding: 10px; display: inline-block; max-width: 300px; border-radius: 5px; text-shadow: rgba(0, 0, 0, 0.25) 0px -1px 1px; box-shadow: rgba(255, 255, 255, 0.5) 0px 1px 3px inset, rgba(0, 0, 0, 0.5) 0px 1px 3px;\" rel=\"noopener noreferrer\">Download Your FREE Mini-Course<\/a><script data-leadbox=\"162526e1b172a2:164f8be4f346dc\" data-url=\"https:\/\/machinelearningmastery.lpages.co\/leadbox\/162526e1b172a2%3A164f8be4f346dc\/5926953912500224\/\" data-config=\"%7B%7D\" type=\"text\/javascript\" src=\"https:\/\/machinelearningmastery.lpages.co\/leadbox-1562872266.js\"><\/script><\/p>\n<p><\/center><\/p>\n<div class=\"woo-sc-hr\"><\/div>\n<h2>How to Develop an LSGAN for MNIST Handwritten Digits<\/h2>\n<p>In this section, we will develop an LSGAN for the <a href=\"https:\/\/machinelearningmastery.com\/how-to-develop-a-convolutional-neural-network-from-scratch-for-mnist-handwritten-digit-classification\/\">MNIST handwritten digit dataset<\/a>.<\/p>\n<p>The first step is to define the models.<\/p>\n<p>Both the discriminator and the generator will be based on the Deep Convolutional GAN, or DCGAN, architecture. This involves the use of Convolution-BatchNorm-Activation layer blocks with the use of <a href=\"https:\/\/machinelearningmastery.com\/padding-and-stride-for-convolutional-neural-networks\/\">2\u00d72 stride<\/a> for downsampling and transpose convolutional layers for upsampling. LeakyReLU activation layers are used in the discriminator and <a href=\"https:\/\/machinelearningmastery.com\/rectified-linear-activation-function-for-deep-learning-neural-networks\/\">ReLU activation layers<\/a> are used in the generator.<\/p>\n<p>The discriminator expects grayscale input images with the shape 28\u00d728, the shape of images in the MNIST dataset, and the output layer is a single node with a linear activation function. The model is optimized using the mean squared error (MSE) loss function as per the LSGAN. The <em>define_discriminator()<\/em> function below defines the discriminator model.<\/p>\n<pre class=\"crayon-plain-tag\"># define the standalone discriminator model\r\ndef define_discriminator(in_shape=(28,28,1)):\r\n\t# weight initialization\r\n\tinit = RandomNormal(stddev=0.02)\r\n\t# define model\r\n\tmodel = Sequential()\r\n\t# downsample to 14x14\r\n\tmodel.add(Conv2D(64, (4,4), strides=(2,2), padding='same', kernel_initializer=init, input_shape=in_shape))\r\n\tmodel.add(BatchNormalization())\r\n\tmodel.add(LeakyReLU(alpha=0.2))\r\n\t# downsample to 7x7\r\n\tmodel.add(Conv2D(128, (4,4), strides=(2,2), padding='same', kernel_initializer=init))\r\n\tmodel.add(BatchNormalization())\r\n\tmodel.add(LeakyReLU(alpha=0.2))\r\n\t# classifier\r\n\tmodel.add(Flatten())\r\n\tmodel.add(Dense(1, activation='linear', kernel_initializer=init))\r\n\t# compile model with L2 loss\r\n\tmodel.compile(loss='mse', optimizer=Adam(lr=0.0002, beta_1=0.5))\r\n\treturn model<\/pre>\n<p>The generator model takes a point in latent space as input and outputs a grayscale image with the shape 28\u00d728 pixels, where pixel values are in the range [-1,1] via the tanh activation function on the output layer.<\/p>\n<p>The <em>define_generator()<\/em> function below defines the generator model. This model is not compiled as it is not trained in a standalone manner.<\/p>\n<pre class=\"crayon-plain-tag\"># define the standalone generator model\r\ndef define_generator(latent_dim):\r\n\t# weight initialization\r\n\tinit = RandomNormal(stddev=0.02)\r\n\t# define model\r\n\tmodel = Sequential()\r\n\t# foundation for 7x7 image\r\n\tn_nodes = 256 * 7 * 7\r\n\tmodel.add(Dense(n_nodes, kernel_initializer=init, input_dim=latent_dim))\r\n\tmodel.add(BatchNormalization())\r\n\tmodel.add(Activation('relu'))\r\n\tmodel.add(Reshape((7, 7, 256)))\r\n\t# upsample to 14x14\r\n\tmodel.add(Conv2DTranspose(128, (4,4), strides=(2,2), padding='same', kernel_initializer=init))\r\n\tmodel.add(BatchNormalization())\r\n\tmodel.add(Activation('relu'))\r\n\t# upsample to 28x28\r\n\tmodel.add(Conv2DTranspose(64, (4,4), strides=(2,2), padding='same', kernel_initializer=init))\r\n\tmodel.add(BatchNormalization())\r\n\tmodel.add(Activation('relu'))\r\n\t# output 28x28x1\r\n\tmodel.add(Conv2D(1, (7,7), padding='same', kernel_initializer=init))\r\n\tmodel.add(Activation('tanh'))\r\n\treturn model<\/pre>\n<p>The generator model is updated via the discriminator model. This is achieved by creating a composite model that stacks the generator on top of the discriminator so that error signals can flow back through the discriminator to the generator.<\/p>\n<p>The weights of the discriminator are marked as not trainable when used in this composite model. Updates via the composite model involve using the generator to create new images by providing random points in the latent space as input. The generated images are passed to the discriminator, which will classify them as real or fake. The weights are updated as though the generated images are real (e.g. target of 1.0), allowing the generator to be updated toward generating more realistic images.<\/p>\n<p>The <em>define_gan()<\/em> function defines and compiles the composite model for updating the generator model via the discriminator, again optimized via mean squared error as per the LSGAN.<\/p>\n<pre class=\"crayon-plain-tag\"># define the combined generator and discriminator model, for updating the generator\r\ndef define_gan(generator, discriminator):\r\n\t# make weights in the discriminator not trainable\r\n\tdiscriminator.trainable = False\r\n\t# connect them\r\n\tmodel = Sequential()\r\n\t# add generator\r\n\tmodel.add(generator)\r\n\t# add the discriminator\r\n\tmodel.add(discriminator)\r\n\t# compile model with L2 loss\r\n\tmodel.compile(loss='mse', optimizer=Adam(lr=0.0002, beta_1=0.5))\r\n\treturn model<\/pre>\n<p>Next, we can define a function to load the MNIST handwritten digit dataset and scale the pixel values to the range [-1,1] to match the images output by the generator model.<\/p>\n<p>Only the training part of the MNIST dataset is used, which contains 60,000 centered grayscale images of digits zero through nine.<\/p>\n<pre class=\"crayon-plain-tag\"># load mnist images\r\ndef load_real_samples():\r\n\t# load dataset\r\n\t(trainX, _), (_, _) = load_data()\r\n\t# expand to 3d, e.g. add channels\r\n\tX = expand_dims(trainX, axis=-1)\r\n\t# convert from ints to floats\r\n\tX = X.astype('float32')\r\n\t# scale from [0,255] to [-1,1]\r\n\tX = (X - 127.5) \/ 127.5\r\n\treturn X<\/pre>\n<p>We can then define a function to retrieve a batch of randomly selected images from the training dataset.<\/p>\n<p>The real images are returned with corresponding target values for the discriminator model, e.g. y=1.0, to indicate they are real.<\/p>\n<pre class=\"crayon-plain-tag\"># select real samples\r\ndef generate_real_samples(dataset, n_samples):\r\n\t# choose random instances\r\n\tix = randint(0, dataset.shape[0], n_samples)\r\n\t# select images\r\n\tX = dataset[ix]\r\n\t# generate class labels\r\n\ty = ones((n_samples, 1))\r\n\treturn X, y<\/pre>\n<p>Next, we can develop the corresponding functions for the generator.<\/p>\n<p>First, a function for generating random points in the latent space to use as input for generating images via the generator model.<\/p>\n<pre class=\"crayon-plain-tag\"># generate points in latent space as input for the generator\r\ndef generate_latent_points(latent_dim, n_samples):\r\n\t# generate points in the latent space\r\n\tx_input = randn(latent_dim * n_samples)\r\n\t# reshape into a batch of inputs for the network\r\n\tx_input = x_input.reshape(n_samples, latent_dim)\r\n\treturn x_input<\/pre>\n<p>Next, a function that will use the generator model to generate a batch of fake images for updating the discriminator model, along with the target value (y=0) to indicate the images are fake.<\/p>\n<pre class=\"crayon-plain-tag\"># use the generator to generate n fake examples, with class labels\r\ndef generate_fake_samples(generator, latent_dim, n_samples):\r\n\t# generate points in latent space\r\n\tx_input = generate_latent_points(latent_dim, n_samples)\r\n\t# predict outputs\r\n\tX = generator.predict(x_input)\r\n\t# create class labels\r\n\ty = zeros((n_samples, 1))\r\n\treturn X, y<\/pre>\n<p>We need to use the generator periodically during training to generate images that we can subjectively inspect and use as the basis for choosing a final generator model.<\/p>\n<p>The <em>summarize_performance()<\/em> function below can be called during training to generate and save a plot of images and save the generator model. Images are plotted using a reverse grayscale color map to make the digits black on a white background.<\/p>\n<pre class=\"crayon-plain-tag\"># generate samples and save as a plot and save the model\r\ndef summarize_performance(step, g_model, latent_dim, n_samples=100):\r\n\t# prepare fake examples\r\n\tX, _ = generate_fake_samples(g_model, latent_dim, n_samples)\r\n\t# scale from [-1,1] to [0,1]\r\n\tX = (X + 1) \/ 2.0\r\n\t# plot images\r\n\tfor i in range(10 * 10):\r\n\t\t# define subplot\r\n\t\tpyplot.subplot(10, 10, 1 + i)\r\n\t\t# turn off axis\r\n\t\tpyplot.axis('off')\r\n\t\t# plot raw pixel data\r\n\t\tpyplot.imshow(X[i, :, :, 0], cmap='gray_r')\r\n\t# save plot to file\r\n\tfilename1 = 'generated_plot_%06d.png' % (step+1)\r\n\tpyplot.savefig(filename1)\r\n\tpyplot.close()\r\n\t# save the generator model\r\n\tfilename2 = 'model_%06d.h5' % (step+1)\r\n\tg_model.save(filename2)\r\n\tprint('Saved %s and %s' % (filename1, filename2))<\/pre>\n<p>We are also interested in the behavior of loss during training.<\/p>\n<p>As such, we can record loss in lists across each training iteration, then create and save a line plot of the learning dynamics of the models. Creating and saving the plot of <a href=\"https:\/\/machinelearningmastery.com\/learning-curves-for-diagnosing-machine-learning-model-performance\/\">learning curves<\/a> is implemented in the <em>plot_history()<\/em> function.<\/p>\n<pre class=\"crayon-plain-tag\"># create a line plot of loss for the gan and save to file\r\ndef plot_history(d1_hist, d2_hist, g_hist):\r\n\tpyplot.plot(d1_hist, label='dloss1')\r\n\tpyplot.plot(d2_hist, label='dloss2')\r\n\tpyplot.plot(g_hist, label='gloss')\r\n\tpyplot.legend()\r\n\tfilename = 'plot_line_plot_loss.png'\r\n\tpyplot.savefig(filename)\r\n\tpyplot.close()\r\n\tprint('Saved %s' % (filename))<\/pre>\n<p>Finally, we can define the main training loop via the <em>train()<\/em> function.<\/p>\n<p>The function takes the defined models and dataset as arguments and parameterizes the number of training epochs and batch size as default function arguments.<\/p>\n<p>Each training loop involves first generating a half-batch of real and fake samples and using them to create one batch worth of weight updates to the discriminator. Next, the generator is updated via the composite model, providing the real (y=1) target as the expected output for the model.<\/p>\n<p>The loss is reported each training iteration, and the model performance is summarized in terms of a plot of generated images at the end of every epoch. The plot of learning curves is created and saved at the end of the run.<\/p>\n<pre class=\"crayon-plain-tag\"># train the generator and discriminator\r\ndef train(g_model, d_model, gan_model, dataset, latent_dim, n_epochs=20, n_batch=64):\r\n\t# calculate the number of batches per training epoch\r\n\tbat_per_epo = int(dataset.shape[0] \/ n_batch)\r\n\t# calculate the number of training iterations\r\n\tn_steps = bat_per_epo * n_epochs\r\n\t# calculate the size of half a batch of samples\r\n\thalf_batch = int(n_batch \/ 2)\r\n\t# lists for storing loss, for plotting later\r\n\td1_hist, d2_hist, g_hist = list(), list(), list()\r\n\t# manually enumerate epochs\r\n\tfor i in range(n_steps):\r\n\t\t# prepare real and fake samples\r\n\t\tX_real, y_real = generate_real_samples(dataset, half_batch)\r\n\t\tX_fake, y_fake = generate_fake_samples(g_model, latent_dim, half_batch)\r\n\t\t# update discriminator model\r\n\t\td_loss1 = d_model.train_on_batch(X_real, y_real)\r\n\t\td_loss2 = d_model.train_on_batch(X_fake, y_fake)\r\n\t\t# update the generator via the discriminator's error\r\n\t\tz_input = generate_latent_points(latent_dim, n_batch)\r\n\t\ty_real2 = ones((n_batch, 1))\r\n\t\tg_loss = gan_model.train_on_batch(z_input, y_real2)\r\n\t\t# summarize loss on this batch\r\n\t\tprint('>%d, d1=%.3f, d2=%.3f g=%.3f' % (i+1, d_loss1, d_loss2, g_loss))\r\n\t\t# record history\r\n\t\td1_hist.append(d_loss1)\r\n\t\td2_hist.append(d_loss2)\r\n\t\tg_hist.append(g_loss)\r\n\t\t# evaluate the model performance every 'epoch'\r\n\t\tif (i+1) % (bat_per_epo * 1) == 0:\r\n\t\t\tsummarize_performance(i, g_model, latent_dim)\r\n\t# create line plot of training history\r\n\tplot_history(d1_hist, d2_hist, g_hist)<\/pre>\n<p>Tying all of this together, the complete code example of training an LSGAN on the MNIST handwritten digit dataset is listed below.<\/p>\n<pre class=\"crayon-plain-tag\"># example of lsgan for mnist\r\nfrom numpy import expand_dims\r\nfrom numpy import zeros\r\nfrom numpy import ones\r\nfrom numpy.random import randn\r\nfrom numpy.random import randint\r\nfrom keras.datasets.mnist import load_data\r\nfrom keras.optimizers import Adam\r\nfrom keras.models import Sequential\r\nfrom keras.layers import Dense\r\nfrom keras.layers import Reshape\r\nfrom keras.layers import Flatten\r\nfrom keras.layers import Conv2D\r\nfrom keras.layers import Conv2DTranspose\r\nfrom keras.layers import Activation\r\nfrom keras.layers import LeakyReLU\r\nfrom keras.layers import BatchNormalization\r\nfrom keras.initializers import RandomNormal\r\nfrom matplotlib import pyplot\r\n\r\n# define the standalone discriminator model\r\ndef define_discriminator(in_shape=(28,28,1)):\r\n\t# weight initialization\r\n\tinit = RandomNormal(stddev=0.02)\r\n\t# define model\r\n\tmodel = Sequential()\r\n\t# downsample to 14x14\r\n\tmodel.add(Conv2D(64, (4,4), strides=(2,2), padding='same', kernel_initializer=init, input_shape=in_shape))\r\n\tmodel.add(BatchNormalization())\r\n\tmodel.add(LeakyReLU(alpha=0.2))\r\n\t# downsample to 7x7\r\n\tmodel.add(Conv2D(128, (4,4), strides=(2,2), padding='same', kernel_initializer=init))\r\n\tmodel.add(BatchNormalization())\r\n\tmodel.add(LeakyReLU(alpha=0.2))\r\n\t# classifier\r\n\tmodel.add(Flatten())\r\n\tmodel.add(Dense(1, activation='linear', kernel_initializer=init))\r\n\t# compile model with L2 loss\r\n\tmodel.compile(loss='mse', optimizer=Adam(lr=0.0002, beta_1=0.5))\r\n\treturn model\r\n\r\n# define the standalone generator model\r\ndef define_generator(latent_dim):\r\n\t# weight initialization\r\n\tinit = RandomNormal(stddev=0.02)\r\n\t# define model\r\n\tmodel = Sequential()\r\n\t# foundation for 7x7 image\r\n\tn_nodes = 256 * 7 * 7\r\n\tmodel.add(Dense(n_nodes, kernel_initializer=init, input_dim=latent_dim))\r\n\tmodel.add(BatchNormalization())\r\n\tmodel.add(Activation('relu'))\r\n\tmodel.add(Reshape((7, 7, 256)))\r\n\t# upsample to 14x14\r\n\tmodel.add(Conv2DTranspose(128, (4,4), strides=(2,2), padding='same', kernel_initializer=init))\r\n\tmodel.add(BatchNormalization())\r\n\tmodel.add(Activation('relu'))\r\n\t# upsample to 28x28\r\n\tmodel.add(Conv2DTranspose(64, (4,4), strides=(2,2), padding='same', kernel_initializer=init))\r\n\tmodel.add(BatchNormalization())\r\n\tmodel.add(Activation('relu'))\r\n\t# output 28x28x1\r\n\tmodel.add(Conv2D(1, (7,7), padding='same', kernel_initializer=init))\r\n\tmodel.add(Activation('tanh'))\r\n\treturn model\r\n\r\n# define the combined generator and discriminator model, for updating the generator\r\ndef define_gan(generator, discriminator):\r\n\t# make weights in the discriminator not trainable\r\n\tdiscriminator.trainable = False\r\n\t# connect them\r\n\tmodel = Sequential()\r\n\t# add generator\r\n\tmodel.add(generator)\r\n\t# add the discriminator\r\n\tmodel.add(discriminator)\r\n\t# compile model with L2 loss\r\n\tmodel.compile(loss='mse', optimizer=Adam(lr=0.0002, beta_1=0.5))\r\n\treturn model\r\n\r\n# load mnist images\r\ndef load_real_samples():\r\n\t# load dataset\r\n\t(trainX, _), (_, _) = load_data()\r\n\t# expand to 3d, e.g. add channels\r\n\tX = expand_dims(trainX, axis=-1)\r\n\t# convert from ints to floats\r\n\tX = X.astype('float32')\r\n\t# scale from [0,255] to [-1,1]\r\n\tX = (X - 127.5) \/ 127.5\r\n\treturn X\r\n\r\n# # select real samples\r\ndef generate_real_samples(dataset, n_samples):\r\n\t# choose random instances\r\n\tix = randint(0, dataset.shape[0], n_samples)\r\n\t# select images\r\n\tX = dataset[ix]\r\n\t# generate class labels\r\n\ty = ones((n_samples, 1))\r\n\treturn X, y\r\n\r\n# generate points in latent space as input for the generator\r\ndef generate_latent_points(latent_dim, n_samples):\r\n\t# generate points in the latent space\r\n\tx_input = randn(latent_dim * n_samples)\r\n\t# reshape into a batch of inputs for the network\r\n\tx_input = x_input.reshape(n_samples, latent_dim)\r\n\treturn x_input\r\n\r\n# use the generator to generate n fake examples, with class labels\r\ndef generate_fake_samples(generator, latent_dim, n_samples):\r\n\t# generate points in latent space\r\n\tx_input = generate_latent_points(latent_dim, n_samples)\r\n\t# predict outputs\r\n\tX = generator.predict(x_input)\r\n\t# create class labels\r\n\ty = zeros((n_samples, 1))\r\n\treturn X, y\r\n\r\n# generate samples and save as a plot and save the model\r\ndef summarize_performance(step, g_model, latent_dim, n_samples=100):\r\n\t# prepare fake examples\r\n\tX, _ = generate_fake_samples(g_model, latent_dim, n_samples)\r\n\t# scale from [-1,1] to [0,1]\r\n\tX = (X + 1) \/ 2.0\r\n\t# plot images\r\n\tfor i in range(10 * 10):\r\n\t\t# define subplot\r\n\t\tpyplot.subplot(10, 10, 1 + i)\r\n\t\t# turn off axis\r\n\t\tpyplot.axis('off')\r\n\t\t# plot raw pixel data\r\n\t\tpyplot.imshow(X[i, :, :, 0], cmap='gray_r')\r\n\t# save plot to file\r\n\tfilename1 = 'generated_plot_%06d.png' % (step+1)\r\n\tpyplot.savefig(filename1)\r\n\tpyplot.close()\r\n\t# save the generator model\r\n\tfilename2 = 'model_%06d.h5' % (step+1)\r\n\tg_model.save(filename2)\r\n\tprint('Saved %s and %s' % (filename1, filename2))\r\n\r\n# create a line plot of loss for the gan and save to file\r\ndef plot_history(d1_hist, d2_hist, g_hist):\r\n\tpyplot.plot(d1_hist, label='dloss1')\r\n\tpyplot.plot(d2_hist, label='dloss2')\r\n\tpyplot.plot(g_hist, label='gloss')\r\n\tpyplot.legend()\r\n\tfilename = 'plot_line_plot_loss.png'\r\n\tpyplot.savefig(filename)\r\n\tpyplot.close()\r\n\tprint('Saved %s' % (filename))\r\n\r\n# train the generator and discriminator\r\ndef train(g_model, d_model, gan_model, dataset, latent_dim, n_epochs=20, n_batch=64):\r\n\t# calculate the number of batches per training epoch\r\n\tbat_per_epo = int(dataset.shape[0] \/ n_batch)\r\n\t# calculate the number of training iterations\r\n\tn_steps = bat_per_epo * n_epochs\r\n\t# calculate the size of half a batch of samples\r\n\thalf_batch = int(n_batch \/ 2)\r\n\t# lists for storing loss, for plotting later\r\n\td1_hist, d2_hist, g_hist = list(), list(), list()\r\n\t# manually enumerate epochs\r\n\tfor i in range(n_steps):\r\n\t\t# prepare real and fake samples\r\n\t\tX_real, y_real = generate_real_samples(dataset, half_batch)\r\n\t\tX_fake, y_fake = generate_fake_samples(g_model, latent_dim, half_batch)\r\n\t\t# update discriminator model\r\n\t\td_loss1 = d_model.train_on_batch(X_real, y_real)\r\n\t\td_loss2 = d_model.train_on_batch(X_fake, y_fake)\r\n\t\t# update the generator via the discriminator's error\r\n\t\tz_input = generate_latent_points(latent_dim, n_batch)\r\n\t\ty_real2 = ones((n_batch, 1))\r\n\t\tg_loss = gan_model.train_on_batch(z_input, y_real2)\r\n\t\t# summarize loss on this batch\r\n\t\tprint('>%d, d1=%.3f, d2=%.3f g=%.3f' % (i+1, d_loss1, d_loss2, g_loss))\r\n\t\t# record history\r\n\t\td1_hist.append(d_loss1)\r\n\t\td2_hist.append(d_loss2)\r\n\t\tg_hist.append(g_loss)\r\n\t\t# evaluate the model performance every 'epoch'\r\n\t\tif (i+1) % (bat_per_epo * 1) == 0:\r\n\t\t\tsummarize_performance(i, g_model, latent_dim)\r\n\t# create line plot of training history\r\n\tplot_history(d1_hist, d2_hist, g_hist)\r\n\r\n# size of the latent space\r\nlatent_dim = 100\r\n# create the discriminator\r\ndiscriminator = define_discriminator()\r\n# create the generator\r\ngenerator = define_generator(latent_dim)\r\n# create the gan\r\ngan_model = define_gan(generator, discriminator)\r\n# load image data\r\ndataset = load_real_samples()\r\nprint(dataset.shape)\r\n# train model\r\ntrain(generator, discriminator, gan_model, dataset, latent_dim)<\/pre>\n<p><strong>Note<\/strong>: the example can be run on the CPU, although it may take a while and running on GPU hardware is recommended.<\/p>\n<p>Running the example will report the loss of the discriminator on real (<em>d1<\/em>) and fake (<em>d2<\/em>) examples and the loss of the generator via the discriminator on generated examples presented as real (<em>g<\/em>). These scores are printed at the end of each training run and are expected to remain small values throughout the training process. Values of zero for an extended period may indicate a failure mode and the training process should be restarted.<\/p>\n<p><strong>Note<\/strong>: your specific results will vary given the stochastic nature of the learning algorithm.<\/p>\n<pre class=\"crayon-plain-tag\">>1, d1=9.292, d2=0.153 g=2.530\r\n>2, d1=1.173, d2=2.057 g=0.903\r\n>3, d1=1.347, d2=1.922 g=2.215\r\n>4, d1=0.604, d2=0.545 g=1.846\r\n>5, d1=0.643, d2=0.734 g=1.619\r\n...<\/pre>\n<p>Plots of generated images are created at the end of every epoch.<\/p>\n<p>The generated images at the beginning of the run are rough.<\/p>\n<div id=\"attachment_8307\" style=\"width: 650px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-8307\" class=\"size-full wp-image-8307\" src=\"https:\/\/machinelearningmastery.com\/wp-content\/uploads\/2019\/05\/Example-of-100-LSGAN-Generated-Handwritten-Digits-after-1-Training-Epoch.png\" alt=\"Example of 100 LSGAN Generated Handwritten Digits after 1 Training Epoch\" width=\"640\" height=\"480\" srcset=\"http:\/\/3qeqpr26caki16dnhd19sv6by6v.wpengine.netdna-cdn.com\/wp-content\/uploads\/2019\/05\/Example-of-100-LSGAN-Generated-Handwritten-Digits-after-1-Training-Epoch.png 640w, http:\/\/3qeqpr26caki16dnhd19sv6by6v.wpengine.netdna-cdn.com\/wp-content\/uploads\/2019\/05\/Example-of-100-LSGAN-Generated-Handwritten-Digits-after-1-Training-Epoch-300x225.png 300w\" sizes=\"(max-width: 640px) 100vw, 640px\"><\/p>\n<p id=\"caption-attachment-8307\" class=\"wp-caption-text\">Example of 100 LSGAN Generated Handwritten Digits after 1 Training Epoch<\/p>\n<\/div>\n<p>After a handful of training epochs, the generated images begin to look crisp and realistic.<\/p>\n<p>Remember: more training epochs may or may not correspond to a generator that outputs higher quality images. Review the generated plots and choose a final model with the best quality images.<\/p>\n<div id=\"attachment_8308\" style=\"width: 650px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-8308\" class=\"size-full wp-image-8308\" src=\"https:\/\/machinelearningmastery.com\/wp-content\/uploads\/2019\/05\/Example-of-100-LSGAN-Generated-Handwritten-Digits-after-20-Training-Epochs.png\" alt=\"Example of 100 LSGAN Generated Handwritten Digits After 20 Training Epochs\" width=\"640\" height=\"480\" srcset=\"http:\/\/3qeqpr26caki16dnhd19sv6by6v.wpengine.netdna-cdn.com\/wp-content\/uploads\/2019\/05\/Example-of-100-LSGAN-Generated-Handwritten-Digits-after-20-Training-Epochs.png 640w, http:\/\/3qeqpr26caki16dnhd19sv6by6v.wpengine.netdna-cdn.com\/wp-content\/uploads\/2019\/05\/Example-of-100-LSGAN-Generated-Handwritten-Digits-after-20-Training-Epochs-300x225.png 300w\" sizes=\"(max-width: 640px) 100vw, 640px\"><\/p>\n<p id=\"caption-attachment-8308\" class=\"wp-caption-text\">Example of 100 LSGAN Generated Handwritten Digits After 20 Training Epochs<\/p>\n<\/div>\n<p>At the end of the training run, a plot of learning curves is created for the discriminator and generator.<\/p>\n<p>In this case, we can see that training remains somewhat stable throughout the run, with some very large peaks observed, which wash out the scale of the plot.<\/p>\n<div id=\"attachment_8309\" style=\"width: 650px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-8309\" class=\"size-full wp-image-8309\" src=\"https:\/\/machinelearningmastery.com\/wp-content\/uploads\/2019\/05\/Plot-of-Learning-Curves-for-the-Generator-and-Discriminator-in-the-LSGAN-During-Training.png\" alt=\"Plot of Learning Curves for the Generator and Discriminator in the LSGAN During Training.\" width=\"640\" height=\"480\" srcset=\"http:\/\/3qeqpr26caki16dnhd19sv6by6v.wpengine.netdna-cdn.com\/wp-content\/uploads\/2019\/05\/Plot-of-Learning-Curves-for-the-Generator-and-Discriminator-in-the-LSGAN-During-Training.png 640w, http:\/\/3qeqpr26caki16dnhd19sv6by6v.wpengine.netdna-cdn.com\/wp-content\/uploads\/2019\/05\/Plot-of-Learning-Curves-for-the-Generator-and-Discriminator-in-the-LSGAN-During-Training-300x225.png 300w\" sizes=\"(max-width: 640px) 100vw, 640px\"><\/p>\n<p id=\"caption-attachment-8309\" class=\"wp-caption-text\">Plot of Learning Curves for the Generator and Discriminator in the LSGAN During Training.<\/p>\n<\/div>\n<h2>How to Generate Images With LSGAN<\/h2>\n<p>We can use the saved generator model to create new images on demand.<\/p>\n<p>This can be achieved by first selecting a final model based on image quality, then loading it and providing new points from the latent space as input in order to generate new plausible images from the domain.<\/p>\n<p>In this case, we will use the model saved after 20 epochs, or 18,740 (60K\/64 or 937 batches per epoch * 20 epochs) training iterations.<\/p>\n<pre class=\"crayon-plain-tag\"># example of loading the generator model and generating images\r\nfrom keras.models import load_model\r\nfrom numpy.random import randn\r\nfrom matplotlib import pyplot\r\n\r\n# generate points in latent space as input for the generator\r\ndef generate_latent_points(latent_dim, n_samples):\r\n\t# generate points in the latent space\r\n\tx_input = randn(latent_dim * n_samples)\r\n\t# reshape into a batch of inputs for the network\r\n\tx_input = x_input.reshape(n_samples, latent_dim)\r\n\treturn x_input\r\n\r\n# create a plot of generated images (reversed grayscale)\r\ndef plot_generated(examples, n):\r\n\t# plot images\r\n\tfor i in range(n * n):\r\n\t\t# define subplot\r\n\t\tpyplot.subplot(n, n, 1 + i)\r\n\t\t# turn off axis\r\n\t\tpyplot.axis('off')\r\n\t\t# plot raw pixel data\r\n\t\tpyplot.imshow(examples[i, :, :, 0], cmap='gray_r')\r\n\tpyplot.show()\r\n\r\n# load model\r\nmodel = load_model('model_018740.h5')\r\n# generate images\r\nlatent_points = generate_latent_points(100, 100)\r\n# generate images\r\nX = model.predict(latent_points)\r\n# plot the result\r\nplot_generated(X, 10)<\/pre>\n<p>Running the example generates a plot of 10\u00d710, or 100, new and plausible handwritten digits.<\/p>\n<div id=\"attachment_8310\" style=\"width: 1034px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-8310\" class=\"size-large wp-image-8310\" src=\"https:\/\/machinelearningmastery.com\/wp-content\/uploads\/2019\/05\/Plot-of-100-LSGAN-Generated-Plausible-Handwritten-Digits-1024x768.png\" alt=\"Plot of 100 LSGAN Generated Plausible Handwritten Digits\" width=\"1024\" height=\"768\" srcset=\"http:\/\/3qeqpr26caki16dnhd19sv6by6v.wpengine.netdna-cdn.com\/wp-content\/uploads\/2019\/05\/Plot-of-100-LSGAN-Generated-Plausible-Handwritten-Digits-1024x768.png 1024w, http:\/\/3qeqpr26caki16dnhd19sv6by6v.wpengine.netdna-cdn.com\/wp-content\/uploads\/2019\/05\/Plot-of-100-LSGAN-Generated-Plausible-Handwritten-Digits-300x225.png 300w, http:\/\/3qeqpr26caki16dnhd19sv6by6v.wpengine.netdna-cdn.com\/wp-content\/uploads\/2019\/05\/Plot-of-100-LSGAN-Generated-Plausible-Handwritten-Digits-768x576.png 768w, http:\/\/3qeqpr26caki16dnhd19sv6by6v.wpengine.netdna-cdn.com\/wp-content\/uploads\/2019\/05\/Plot-of-100-LSGAN-Generated-Plausible-Handwritten-Digits.png 1280w\" sizes=\"(max-width: 1024px) 100vw, 1024px\"><\/p>\n<p id=\"caption-attachment-8310\" class=\"wp-caption-text\">Plot of 100 LSGAN Generated Plausible Handwritten Digits<\/p>\n<\/div>\n<h2>Further Reading<\/h2>\n<p>This section provides more resources on the topic if you are looking to go deeper.<\/p>\n<h3>Papers<\/h3>\n<ul>\n<li><a href=\"https:\/\/arxiv.org\/abs\/1611.04076\">Least Squares Generative Adversarial Networks<\/a>, 2016.<\/li>\n<\/ul>\n<h3>API<\/h3>\n<ul>\n<li><a href=\"https:\/\/keras.io\/datasets\/\">Keras Datasets API.<\/a><\/li>\n<li><a href=\"https:\/\/keras.io\/models\/sequential\/\">Keras Sequential Model API<\/a><\/li>\n<li><a href=\"https:\/\/keras.io\/layers\/convolutional\/\">Keras Convolutional Layers API<\/a><\/li>\n<li><a href=\"https:\/\/keras.io\/getting-started\/faq\/#how-can-i-freeze-keras-layers\">How can I \u201cfreeze\u201d Keras layers?<\/a><\/li>\n<li><a href=\"https:\/\/matplotlib.org\/api\/\">MatplotLib API<\/a><\/li>\n<li><a href=\"https:\/\/docs.scipy.org\/doc\/numpy\/reference\/routines.random.html\">NumPy Random sampling (numpy.random) API<\/a><\/li>\n<li><a href=\"https:\/\/docs.scipy.org\/doc\/numpy\/reference\/routines.array-manipulation.html\">NumPy Array manipulation routines<\/a><\/li>\n<\/ul>\n<h3>Articles<\/h3>\n<ul>\n<li><a href=\"https:\/\/wiseodd.github.io\/techblog\/2017\/03\/02\/least-squares-gan\/\">Least Squares GAN<\/a>, 2017.<\/li>\n<li><a href=\"https:\/\/github.com\/xudonmao\/LSGAN\">LSGAN Project (Official), GitHub<\/a>.<\/li>\n<li><a href=\"https:\/\/github.com\/eriklindernoren\/Keras-GAN\">Keras-GAN Project, GitHub<\/a>.<\/li>\n<\/ul>\n<h2>Summary<\/h2>\n<p>In this tutorial, you discovered how to develop a least squares generative adversarial network.<\/p>\n<p>Specifically, you learned:<\/p>\n<ul>\n<li>The LSGAN addresses vanishing gradients and loss saturation of the deep convolutional GAN.<\/li>\n<li>The LSGAN can be implemented by a mean squared error or L2 loss function for the discriminator model.<\/li>\n<li>How to implement the LSGAN model for generating handwritten digits for the MNIST dataset.<\/li>\n<\/ul>\n<p>Do you have any questions?<br \/>\nAsk your questions in the comments below and I will do my best to answer.<\/p>\n<p>The post <a rel=\"nofollow\" href=\"https:\/\/machinelearningmastery.com\/least-squares-generative-adversarial-network\/\">How to Develop a Least Squares Generative Adversarial Network (LSGAN) in Keras<\/a> appeared first on <a rel=\"nofollow\" href=\"https:\/\/machinelearningmastery.com\/\">Machine Learning Mastery<\/a>.<\/p>\n<\/div>\n<p><a href=\"https:\/\/machinelearningmastery.com\/least-squares-generative-adversarial-network\/\">Go to Source<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Author: Jason Brownlee The Least Squares Generative Adversarial Network, or LSGAN for short, is an extension to the GAN architecture that addresses the problem of [&hellip;] <span class=\"read-more-link\"><a class=\"read-more\" href=\"https:\/\/www.aiproblog.com\/index.php\/2019\/07\/25\/how-to-develop-a-least-squares-generative-adversarial-network-lsgan-in-keras\/\">Read More<\/a><\/span><\/p>\n","protected":false},"author":1,"featured_media":2399,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_bbp_topic_count":0,"_bbp_reply_count":0,"_bbp_total_topic_count":0,"_bbp_total_reply_count":0,"_bbp_voice_count":0,"_bbp_anonymous_reply_count":0,"_bbp_topic_count_hidden":0,"_bbp_reply_count_hidden":0,"_bbp_forum_subforum_count":0,"footnotes":""},"categories":[24],"tags":[],"_links":{"self":[{"href":"https:\/\/www.aiproblog.com\/index.php\/wp-json\/wp\/v2\/posts\/2398"}],"collection":[{"href":"https:\/\/www.aiproblog.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.aiproblog.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.aiproblog.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.aiproblog.com\/index.php\/wp-json\/wp\/v2\/comments?post=2398"}],"version-history":[{"count":0,"href":"https:\/\/www.aiproblog.com\/index.php\/wp-json\/wp\/v2\/posts\/2398\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.aiproblog.com\/index.php\/wp-json\/wp\/v2\/media\/2399"}],"wp:attachment":[{"href":"https:\/\/www.aiproblog.com\/index.php\/wp-json\/wp\/v2\/media?parent=2398"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.aiproblog.com\/index.php\/wp-json\/wp\/v2\/categories?post=2398"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.aiproblog.com\/index.php\/wp-json\/wp\/v2\/tags?post=2398"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}