{
  "source_file": "0930",
  "source_platform": "技术平台",
  "author": "Adrian Rosebrock / PyImageSearch",
  "title_listed": "Keras and Convolutional Neural Networks (CNNs)",
  "keywords": "Keras,CNN,图像分类",
  "domain": "图像处理",
  "content_type": "教程",
  "is_paid_flag": "否",
  "url": "https://pyimagesearch.com/2018/04/16/keras-and-convolutional-neural-networks-cnns/",
  "publish_date": "2018-04",
  "corpus_stage": "预训练/SFT",
  "notes": "Keras卷积神经网络入门",
  "id": "de0ec63f5f3a",
  "collected_at": "2026-10-02T17:20:48",
  "title": "Keras and Convolutional Neural Networks (CNNs) - PyImageSearch",
  "body_markdown": "Click here to download the source code to this post\nToday’s blog post is part two in a three-part series on building a complete end-to-end image classification + deep learning application:\nCreating a Convolutional Neural Network using Keras to recognize a Bulbasaur stuffed Pokemon\nPart 1:\nHow to (quickly) build a deep learning image dataset\nPart 2:\nKeras and Convolutional Neural Networks (today’s post)\nPart 3:\nRunning a Keras model on iOS (to be published next week)\nBy the end of today’s blog post, you will understand how to implement, train, and evaluate a Convolutional Neural Network on your own custom dataset.\nAnd in next week’s post, I’ll be demonstrating how you can take your trained Keras model and deploy it to a smartphone app with just a few lines of code!\nTo keep the series lighthearted and fun,\nI am fulfilling a childhood dream of mine\nand building a Pokedex. A Pokedex is a device that exists in the world of Pokemon, a popular TV show, video game, and trading card series (I was/still am a huge Pokemon fan).\nIf you are unfamiliar with Pokemon,\nyou should think of a Pokedex as a smartphone app that can recognize Pokemon, the animal-like creatures that exist in the world of Pokemon.\nYou can swap in your own datasets of course, I’m just having fun and enjoying a bit of childhood nostalgia.\nTo learn how to train a Convolutional Neural Network with Keras and deep learning on your own custom dataset,\njust keep reading.\nLooking for the source code to this post?\nJump Right To The Downloads Section\nKeras and Convolutional Neural Networks\n2020-05-13 Update:\nThis blog post is now TensorFlow 2+ compatible!\nIn last week’s blog post we learned how we can\nquickly build a deep learning image dataset\n— we used the procedure and code covered in the post to gather, download, and organize our images on disk.\nNow that we have our images downloaded and organized, the next step is to train a Convolutional Neural Network (CNN) on top of the data.\nI’ll be showing you how to train your CNN in today’s post using Keras and deep learning.\nThe final part of this series, releasing next week, will demonstrate how you can take your trained Keras model and deploy it to a smartphone (in particular, iPhone) with only a few lines of code.\nThe end goal of this series is to help you build a fully functional deep learning app — use this series as an inspiration and starting point to help you build your own deep learning applications.\nLet’s go ahead and get started training a CNN with Keras and deep learning.\nConfiguring your development environment\nTo configure your system for this tutorial, I first recommend following either of these tutorials:\nHow to install TensorFlow 2.0 on Ubuntu\nHow to install TensorFlow 2.0 on macOS\nEither tutorial will help you configure you system with all the necessary software for this blog post in a convenient Python virtual environment.\nPlease note that\nPyImageSearch does not recommend or support Windows for CV/DL projects\n.\nOur deep learning dataset\nFigure 1:\nA montage of samples from our Pokemon deep learning dataset depicting each of the classes (i.e., Pokemon species). As we can see, the dataset is diverse, including illustrations, movie/TV show stills, action figures, toys, etc.\nOur deep learning dataset consists of 1,191 images of Pokemon, (animal-like creatures that exist in the world of Pokemon, the popular TV show, video game, and trading card series).\nOur goal is to train a Convolutional Neural Network using Keras and deep learning to\nrecognize\nand\nclassify\neach of these Pokemon.\nThe Pokemon we will be recognizing include:\nBulbasaur\n(234 images)\nCharmander\n(238 images)\nSquirtle\n(223 images)\nPikachu\n(234 images)\nMewtwo\n(239 images)\nA montage of the training images for each class can be seen in\nFigure 1\nabove.\nAs you can see, our training images include a mix of:\nStill frames from the TV show and movies\nTrading cards\nAction figures\nToys and plushes\nDrawings and artistic renderings from fans\nThis diverse mix of training images will allow our CNN to recognize our five Pokemon classes across a range of images —\nand as we’ll see, we’ll be able to obtain\n82%+\nclassification accuracy!\nThe Convolutional Neural Network and Keras project structure\nToday’s project has several moving parts — to help us wrap our head around the project, let’s start by reviewing our directory structure for the project:\n├── dataset\n│   ├── bulbasaur [234 entries]\n│   ├── charmander [238 entries]\n│   ├── mewtwo [239 entries]\n│   ├── pikachu [234 entries]\n│   └── squirtle [223 entries]\n├── examples [6 entries]\n├── pyimagesearch\n│   ├── __init__.py\n│   └── smallervggnet.py\n├── plot.png\n├── lb.pickle\n├── pokedex.model\n├── classify.py\n└── train.py\nThere are 3 directories:\ndataset\n: Contains the five classes, each class is its own respective subdirectory to make parsing class labels easy.\nexamples\n: Contains images we’ll be using to test our CNN.\nThe\npyimagesearch\nmodule: Contains our\nSmallerVGGNet\nmodel class (which we’ll be implementing later in this post).\nAnd 5 files in the root:\nplot.png\n: Our training/testing accuracy and loss plot which is generated after the training script is ran.\nlb.pickle\n: Our\nLabelBinarizer\nserialized object file — this contains a class index to class name lookup mechamisn.\npokedex.model\n: This is our serialized Keras Convolutional Neural Network model file (i.e., the “weights file”).\ntrain.py\n: We will use this script to train our Keras CNN, plot the accuracy/loss, and then serialize the CNN and label binarizer to disk.\nclassify.py\n: Our testing script.\nOur Keras and CNN architecture\nFigure 2:\nA VGGNet-like network that I’ve dubbed “SmallerVGGNet” will be used for training a deep learning classifier with Keras. You can find the full resolution version of this network architecture diagram\nhere\n.\nThe CNN architecture we will be utilizing today is a smaller, more compact variant of the VGGNet network, introduced by Simonyan and Zisserman in their 2014 paper,\nVery Deep Convolutional Networks for Large Scale Image Recognition\n.\nVGGNet-like architectures are characterized by:\nUsing only 3×3 convolutional layers stacked on top of each other in increasing depth\nReducing volume size by max pooling\nFully-connected layers at the end of the network prior to a softmax classifier\nI assume you already have Keras installed and configured on your system. If not, here are a few links to deep learning development environment configuration tutorials I have put together:\nConfiguring Ubuntu for deep learning with Python\nSetting up Ubuntu 16.04 + CUDA + GPU for deep learning with Python\nConfiguring macOS for deep learning with Python\nIf you want to\nskip configuring your deep learning environment\n, I would recommend using one of the following\npre-configured instances\nin the cloud:\nAmazon AMI for deep learning with Python\nMicrosoft’s data science virtual machine (DSVM) for deep learning\nLet’s go ahead and implement\nSmallerVGGNet\n, our smaller version of VGGNet. Create a new file named\nsmallervggnet.py\ninside the\npyimagesearch\nmodule and insert the following code:\n# import the necessary packages\nfrom tensorflow.keras.models import Sequential\nfrom tensorflow.keras.layers import BatchNormalization\nfrom tensorflow.keras.layers import Conv2D\nfrom tensorflow.keras.layers import MaxPooling2D\nfrom tensorflow.keras.layers import Activation\nfrom tensorflow.keras.layers import Flatten\nfrom tensorflow.keras.layers import Dropout\nfrom tensorflow.keras.layers import Dense\nfrom tensorflow.keras import backend as K\nFirst we import our modules — notice that they all come from Keras. Each of these are covered extensively throughout the course of reading\nDeep Learning for Computer Vision with Python\n.\nNote:\nYou’ll also want to create an\n__init__.py\nfile inside\npyimagesearch\nso Python knows the directory is a module. If you’re unfamiliar with\n__init__.py\nfiles or how they are used to create modules, no worries, just use the\n“Downloads”\nsection at the end of this blog post to download my directory structure, source code, and dataset + example images.\nFrom there, we define our\nSmallerVGGNet\nclass:\nclass SmallerVGGNet:\n\t@staticmethod\n\tdef build(width, height, depth, classes):\n\t\t# initialize the model along with the input shape to be\n\t\t# \"channels last\" and the channels dimension itself\n\t\tmodel = Sequential()\n\t\tinputShape = (height, width, depth)\n\t\tchanDim = -1\n\n\t\t# if we are using \"channels first\", update the input shape\n\t\t# and channels dimension\n\t\tif K.image_data_format() == \"channels_first\":\n\t\t\tinputShape = (depth, height, width)\n\t\t\tchanDim = 1\nOur build method requires four parameters:\nwidth\n: The image width dimension.\nheight\n: The image height dimension.\ndepth\n: The depth of the image — also known as the number of channels.\nclasses\n: The number of classes in our dataset (which will affect the last layer of our model). We’re utilizing 5 Pokemon classes in this post, but don’t forget that you\ncould\nwork with the 807 Pokemon species if you downloaded enough example images for each species!\nNote:\nWe’ll be working with input images that are\n96 x 96\nwith a depth of\n3\n(as we’ll see later in this post). Keep this in mind as we explain the spatial dimensions of the input volume as it passes through the network.\nSince we’re using the TensorFlow backend, we arrange the input shape with “channels last” data ordering, but if you want to use “channels first” (Theano, etc.) then it is handled automagically on\nLines 23-25\n.\nNow, let’s start adding layers to our model:\n# CONV => RELU => POOL\n\t\tmodel.add(Conv2D(32, (3, 3), padding=\"same\",\n\t\t\tinput_shape=inputShape))\n\t\tmodel.add(Activation(\"relu\"))\n\t\tmodel.add(BatchNormalization(axis=chanDim))\n\t\tmodel.add(MaxPooling2D(pool_size=(3, 3)))\n\t\tmodel.add(Dropout(0.25))\nAbove is our first\nCONV => RELU => POOL\nblock.\nThe convolution layer has\n32\nfilters with a\n3 x 3\nkernel. We’re using\nRELU\nthe activation function followed by batch normalization.\nOur\nPOOL\nlayer uses a\n3 x 3\nPOOL\nsize to reduce spatial dimensions quickly from\n96 x 96\nto\n32 x 32\n(we’ll be using\n96 x 96 x 3\ninput images to train our network as we’ll see in the next section).\nAs you can see from the code block, we’ll also be utilizing dropout in our network architecture. Dropout works by randomly disconnecting nodes from the\ncurrent layer\nto the\nnext layer\n. This process of random disconnects during training batches helps naturally introduce redundancy into the model — no one single node in the layer is responsible for predicting a certain class, object, edge, or corner.\nFrom there we’ll add\n(CONV => RELU) * 2\nlayers before applying another\nPOOL\nlayer:\n# (CONV => RELU) * 2 => POOL\n\t\tmodel.add(Conv2D(64, (3, 3), padding=\"same\"))\n\t\tmodel.add(Activation(\"relu\"))\n\t\tmodel.add(BatchNormalization(axis=chanDim))\n\t\tmodel.add(Conv2D(64, (3, 3), padding=\"same\"))\n\t\tmodel.add(Activation(\"relu\"))\n\t\tmodel.add(BatchNormalization(axis=chanDim))\n\t\tmodel.add(MaxPooling2D(pool_size=(2, 2)))\n\t\tmodel.add(Dropout(0.25))\nStacking multiple\nCONV\nand\nRELU\nlayers together (prior to reducing the spatial dimensions of the volume) allows us to learn a richer set of features.\nNotice how:\nWe’re increasing our filter size from\n32\nto\n64\n. The deeper we go in the network, the smaller the spatial dimensions of our volume, and the more filters we learn.\nWe decreased how max pooling size from\n3 x 3\nto\n2 x 2\nto ensure we do not reduce our spatial dimensions too quickly.\nDropout is again performed at this stage.\nLet’s add another set of\n(CONV => RELU) * 2 => POOL\n:\n# (CONV => RELU) * 2 => POOL\n\t\tmodel.add(Conv2D(128, (3, 3), padding=\"same\"))\n\t\tmodel.add(Activation(\"relu\"))\n\t\tmodel.add(BatchNormalization(axis=chanDim))\n\t\tmodel.add(Conv2D(128, (3, 3), padding=\"same\"))\n\t\tmodel.add(Activation(\"relu\"))\n\t\tmodel.add(BatchNormalization(axis=chanDim))\n\t\tmodel.add(MaxPooling2D(pool_size=(2, 2)))\n\t\tmodel.add(Dropout(0.25))\nNotice that we’ve increased our filter size to\n128\nhere. Dropout of 25% of the nodes is performed to reduce overfitting again.\nAnd finally, we have a set of\nFC => RELU\nlayers and a softmax classifier:\n# first (and only) set of FC => RELU layers\n\t\tmodel.add(Flatten())\n\t\tmodel.add(Dense(1024))\n\t\tmodel.add(Activation(\"relu\"))\n\t\tmodel.add(BatchNormalization())\n\t\tmodel.add(Dropout(0.5))\n\n\t\t# softmax classifier\n\t\tmodel.add(Dense(classes))\n\t\tmodel.add(Activation(\"softmax\"))\n\n\t\t# return the constructed network architecture\n\t\treturn model\nThe fully connected layer is specified by\nDense(1024)\nwith a rectified linear unit activation and batch normalization.\nDropout is performed a final time — this time notice that we’re dropping out 50% of the nodes during training. Typically you’ll use a dropout of 40-50% in our fully-connected layers and a dropout with much lower rate, normally 10-25% in previous layers (if any dropout is applied at all).\nWe round out the model with a softmax classifier that will return the predicted probabilities for each class label.\nA visualization of the network architecture of first few layers of\nSmallerVGGNet\ncan be seen in\nFigure 2\nat the top of this section. To see the full resolution of our Keras CNN implementation of\nSmallerVGGNet\n, refer to the following\nlink\n.\nImplementing our CNN + Keras training script\nNow that\nSmallerVGGNet\nis implemented, we can train our Convolutional Neural Network using Keras.\nOpen up a new file, name it\ntrain.py\n, and insert the following code where we’ll import our required packages and libraries:\n# set the matplotlib backend so figures can be saved in the background\nimport matplotlib\nmatplotlib.use(\"Agg\")\n\n# import the necessary packages\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.preprocessing.image import img_to_array\nfrom sklearn.preprocessing import LabelBinarizer\nfrom sklearn.model_selection import train_test_split\nfrom pyimagesearch.smallervggnet import SmallerVGGNet\nimport matplotlib.pyplot as plt\nfrom imutils import paths\nimport numpy as np\nimport argparse\nimport random\nimport pickle\nimport cv2\nimport os\nWe are going to use the\n\"Agg\"\nmatplotlib backend so that figures can be saved in the background (\nLine 3\n).\nThe\nImageDataGenerator\nclass will be used for data augmentation, a technique used to take existing images in our dataset and apply random transformations (rotations, shearing, etc.) to generate additional training data. Data augmentation helps prevent overfitting.\nLine 7\nimports the\nAdam\noptimizer, the optimizer method used to train our network.\nThe\nLabelBinarizer\n(\nLine 9\n) is an important class to note — this class will enable us to:\nInput a set of class labels (i.e., strings representing the human-readable class labels in our dataset).\nTransform our class labels into one-hot encoded vectors.\nAllow us to take an integer class label prediction from our Keras CNN and transform it back into a human-readable label.\nI often get asked hereon the PyImageSearch blog how we can transform a class label string to an integer and vice versa. Now you know the solution is to use the\nLabelBinarizer\nclass.\nThe\ntrain_test_split\nfunction (\nLine 10\n) will be used to create our training and testing splits. Also take note of our\nSmallerVGGNet\nimport on\nLine 11\n— this is the Keras CNN we just implemented in the previous section.\nReaders of this blog are familiar with\nmy very own imutils package\n. If you don’t have it installed/updated, you can install it via:\n$ pip install --upgrade imutils\nIf you are using a Python virtual environment (as we typically do here on the PyImageSearch blog), make sure you use the\nworkon\ncommand to access your particular virtual environment before installing/upgrading\nimutils\n.\nFrom there, let’s parse our\ncommand line arguments\n:\n# construct the argument parse and parse the arguments\nap = argparse.ArgumentParser()\nap.add_argument(\"-d\", \"--dataset\", required=True,\n\thelp=\"path to input dataset (i.e., directory of images)\")\nap.add_argument(\"-m\", \"--model\", required=True,\n\thelp=\"path to output model\")\nap.add_argument(\"-l\", \"--labelbin\", required=True,\n\thelp=\"path to output label binarizer\")\nap.add_argument(\"-p\", \"--plot\", type=str, default=\"plot.png\",\n\thelp=\"path to output accuracy/loss plot\")\nargs = vars(ap.parse_args())\nFor our training script, we need to supply three required\ncommand line arguments\n:\n--dataset\n: The path to the input dataset. Our dataset is organized in a\ndataset\ndirectory with subdirectories representing each class. Inside each subdirectory is ~250 Pokemon images. See the project directory structure at the top of this post for more details.\n--model\n: The path to the output model — this training script will train the model and output it to disk.\n--labelbin\n: The path to the output label binarizer — as you’ll see shortly, we’ll extract the class labels from the dataset directory names and build the label binarizer.\nWe also have one optional argument,\n--plot\n. If you don’t specify a path/filename, then a\nplot.png\nfile will be placed in the current working directory.\nYou\ndo not\nneed to modify\nLines 22-31\nto supply new file paths. The command line arguments are handled at runtime. If this doesn’t make sense to you, be sure to review my\ncommand line arguments\nblog post.\nNow that we’ve taken care of our command line arguments, let’s initialize some important variables:\n# initialize the number of epochs to train for, initial learning rate,\n# batch size, and image dimensions\nEPOCHS = 100\nINIT_LR = 1e-3\nBS = 32\nIMAGE_DIMS = (96, 96, 3)\n\n# initialize the data and labels\ndata = []\nlabels = []\n\n# grab the image paths and randomly shuffle them\nprint(\"[INFO] loading images...\")\nimagePaths = sorted(list(paths.list_images(args[\"dataset\"])))\nrandom.seed(42)\nrandom.shuffle(imagePaths)\nLines 35-38\ninitialize important variables used when training our Keras CNN:\nEPOCHS:\nThe total number of epochs we will be training our network for (i.e., how many times our network “sees” each training example and learns patterns from it).\nINIT_LR:\nThe initial learning rate — a value of 1e-3 is the default value for the Adam optimizer, the optimizer we will be using to train the network.\nBS:\nWe will be passing batches of images into our network for training. There are multiple batches per epoch. The\nBS\nvalue controls the batch size.\nIMAGE_DIMS:\nHere we supply the spatial dimensions of our input images. We’ll require our input images to be\n96 x 96\npixels with\n3\nchannels (i.e., RGB). I’ll also note that we specifically designed SmallerVGGNet with\n96 x 96\nimages in mind.\nWe also initialize two lists —\ndata\nand\nlabels\nwhich will hold the preprocessed images and labels, respectively.\nLines 46-48\ngrab all of the image paths and randomly shuffle them.\nAnd from there, we’ll loop over each of those\nimagePaths\n:\n# loop over the input images\nfor imagePath in imagePaths:\n\t# load the image, pre-process it, and store it in the data list\n\timage = cv2.imread(imagePath)\n\timage = cv2.resize(image, (IMAGE_DIMS[1], IMAGE_DIMS[0]))\n\timage = img_to_array(image)\n\tdata.append(image)\n\n\t# extract the class label from the image path and update the\n\t# labels list\n\tlabel = imagePath.split(os.path.sep)[-2]\n\tlabels.append(label)\nWe loop over the\nimagePaths\non\nLine 51\nand then proceed to load the image (\nLine 53\n) and resize it to accommodate our model (\nLine 54\n).\nNow it’s time to update our\ndata\nand\nlabels\nlists.\nWe call the Keras\nimg_to_array\nfunction to convert the image to a Keras-compatible array (\nLine 55\n) followed by appending the image to our list called\ndata\n(\nLine 56\n).\nFor our\nlabels\nlist, we extract the\nlabel\nfrom the file path on\nLine 60\nand append it (the label) on\nLine 61\n.\nSo, why does this class label parsing process work?\nConsider that fact that we\npurposely\ncreated our dataset directory structure to have the following format:\ndataset/{CLASS_LABEL}/{FILENAME}.jpg\nUsing the path separator on\nLine 60\nwe can split the path into an array and then grab the second-to-last entry in the list — the class label.\nIf this process seems confusing to you, I would encourage you to open up a Python shell and explore an example\nimagePath\nby splitting the path on your operating system’s respective path separator.\nLet’s keep moving. A few things are happening in this next code block — additional preprocessing, binarizing labels, and partitioning the data:\n# scale the raw pixel intensities to the range [0, 1]\ndata = np.array(data, dtype=\"float\") / 255.0\nlabels = np.array(labels)\nprint(\"[INFO] data matrix: {:.2f}MB\".format(\n\tdata.nbytes / (1024 * 1000.0)))\n\n# binarize the labels\nlb = LabelBinarizer()\nlabels = lb.fit_transform(labels)\n\n# partition the data into training and testing splits using 80% of\n# the data for training and the remaining 20% for testing\n(trainX, testX, trainY, testY) = train_test_split(data,\n\tlabels, test_size=0.2, random_state=42)\nHere we first convert the\ndata\narray to a NumPy array and then scale the pixel intensities to the range\n[0, 1]\n(\nLine 64\n). We also convert the\nlabels\nfrom a list to a NumPy array on\nLine 65\n. An info message is printed which shows the size (in MB) of the\ndata\nmatrix.\nThen, we binarize the labels utilizing scikit-learn’s\nLabelBinarizer\n(\nLines 70 and 71\n).\nWith deep learning, or any machine learning for that matter, a common practice is to make a training and testing split. This is handled on\nLines 75 and 76\nwhere we create an 80/20 random split of the data.\nNext, let’s create our image data augmentation object:\n# construct the image generator for data augmentation\naug = ImageDataGenerator(rotation_range=25, width_shift_range=0.1,\n\theight_shift_range=0.1, shear_range=0.2, zoom_range=0.2,\n\thorizontal_flip=True, fill_mode=\"nearest\")\nSince we’re working with a limited amount of data points (< 250 images per class), we can make use of data augmentation during the training process to give our model more images (based on existing images) to train with.\nData Augmentation is a tool that should be in every deep learning practitioner’s toolbox. I cover data augmentation in the\nPractitioner Bundle\nof\nDeep Learning for Computer Vision with Python\n.\nWe initialize aug, our\nImageDataGenerator\n, on\nLines 79-81\n.\nFrom there, let’s compile the model and kick off the training:\n# initialize the model\nprint(\"[INFO] compiling model...\")\nmodel = SmallerVGGNet.build(width=IMAGE_DIMS[1], height=IMAGE_DIMS[0],\n\tdepth=IMAGE_DIMS[2], classes=len(lb.classes_))\nopt = Adam(lr=INIT_LR, decay=INIT_LR / EPOCHS)\nmodel.compile(loss=\"categorical_crossentropy\", optimizer=opt,\n\tmetrics=[\"accuracy\"])\n\n# train the network\nprint(\"[INFO] training network...\")\nH = model.fit(\n\tx=aug.flow(trainX, trainY, batch_size=BS),\n\tvalidation_data=(testX, testY),\n\tsteps_per_epoch=len(trainX) // BS,\n\tepochs=EPOCHS, verbose=1)\nOn\nLines 85 and 86\n, we initialize our Keras CNN model with\n96 x 96 x 3\ninput spatial dimensions. I’ll state this again as I receive this question often — SmallerVGGNet was designed to accept\n96 x 96 x 3\ninput images. If you want to use different spatial dimensions you may need to either:\nReduce the depth of the network for smaller images\nIncrease the depth of the network for larger images\nDo not go blindly editing the code. Consider the implications larger or smaller images will have first!\nWe’re going to use the\nAdam\noptimizer with learning rate decay (\nLine 87\n) and then\ncompile\nour\nmodel\nwith categorical cross-entropy since we have > 2 classes (\nLines 88 and 89\n).\nNote:\nFor only two classes you should use binary cross-entropy as the loss.\nFrom there, we make a call to the Keras\nfit\nmethod to train the network (\nLines 93-97\n). Be patient — this can take some time depending on whether you are training using a CPU or a GPU.\n2020-05-13 Update:\nFormerly, TensorFlow/Keras required use of a method called\nfit_generator\nin order to accomplish data augmentation. Now the\nfit\nmethod can handle data augmentation as well making for more-consistent code. Be sure to check out my articles about\nfit and fit generator\nas well as\ndata augmentation\n.\nOnce our Keras CNN has finished training, we’ll want to save both the (1) model and (2) label binarizer as we’ll need to load them from disk when we test the network on images outside of our training/testing set:\n# save the model to disk\nprint(\"[INFO] serializing network...\")\nmodel.save(args[\"model\"], save_format=\"h5\")\n\n# save the label binarizer to disk\nprint(\"[INFO] serializing label binarizer...\")\nf = open(args[\"labelbin\"], \"wb\")\nf.write(pickle.dumps(lb))\nf.close()\nWe serialize the model (\nLine 101\n) and the label binarizer (\nLines 105-107\n) so we can easily use them later in our\nclassify.py\nscript.\nThe label binarizer file contains the class index to human-readable class label dictionary. This object ensures we don’t have to hardcode our class labels in scripts that wish to use our Keras CNN.\nFinally, we can plot our training and loss accuracy:\n# plot the training loss and accuracy\nplt.style.use(\"ggplot\")\nplt.figure()\nN = EPOCHS\nplt.plot(np.arange(0, N), H.history[\"loss\"], label=\"train_loss\")\nplt.plot(np.arange(0, N), H.history[\"val_loss\"], label=\"val_loss\")\nplt.plot(np.arange(0, N), H.history[\"accuracy\"], label=\"train_acc\")\nplt.plot(np.arange(0, N), H.history[\"val_accuracy\"], label=\"val_acc\")\nplt.title(\"Training Loss and Accuracy\")\nplt.xlabel(\"Epoch #\")\nplt.ylabel(\"Loss/Accuracy\")\nplt.legend(loc=\"upper left\")\nplt.savefig(args[\"plot\"])\n2020-05-13 Update:\nIn order for this plotting snippet to be TensorFlow 2+ compatible the\nH.history\ndictionary keys are updated to fully spell out “accuracy” sans “acc” (i.e.,\nH.history[\"val_accuracy\"]\nand\nH.history[\"accuracy\"]\n). It is semi-confusing that “val” is not spelled out as “validation”; we have to learn to love and live with the API and always remember that it is a work in progress that many developers around the world contribute to.\nI elected to save my plot to disk (\nLine\n121\n) rather than displaying it for two reasons: (1) I’m on a headless server in the cloud and (2) I wanted to make sure I don’t forget to save the plot.\nTraining our CNN with Keras\nNow we’re ready to train our Pokedex CNN.\nBe sure to visit the\n“Downloads”\nsection of this blog post to\ndownload code + data.\nThen execute the following command to train the mode; while making sure to provide the\ncommand line arguments\nproperly:\n$ python train.py --dataset dataset --model pokedex.model --labelbin lb.pickle\nUsing TensorFlow backend.\n[INFO] loading images...\n[INFO] data matrix: 252.07MB\n[INFO] compiling model...\n[INFO] training network...\nTrain for 29 steps, validate on 234 samples\nEpoch 1/100\n29/29 [==============================] - 7s 237ms/step - loss: 1.4218 - accuracy: 0.6271 - val_loss: 1.9534 - val_accuracy: 0.2436\nEpoch 2/100\n29/29 [==============================] - 6s 208ms/step - loss: 0.7470 - accuracy: 0.7703 - val_loss: 2.7184 - val_accuracy: 0.3632\nEpoch 3/100\n29/29 [==============================] - 6s 207ms/step - loss: 0.5928 - accuracy: 0.8080 - val_loss: 2.8207 - val_accuracy: 0.2436\n...\n29/29 [==============================] - 6s 208ms/step - loss: 0.2108 - accuracy: 0.9423 - val_loss: 1.7813 - val_accuracy: 0.8248\nEpoch 98/100\n29/29 [==============================] - 6s 208ms/step - loss: 0.1170 - accuracy: 0.9645 - val_loss: 2.2405 - val_accuracy: 0.7265\nEpoch 99/100\n29/29 [==============================] - 6s 208ms/step - loss: 0.0961 - accuracy: 0.9689 - val_loss: 1.2761 - val_accuracy: 0.8333\nEpoch 100/100\n29/29 [==============================] - 6s 207ms/step - loss: 0.0449 - accuracy: 0.9834 - val_loss: 1.1710 - val_accuracy: 0.8291\n[INFO] serializing network...\n[INFO] serializing label binarizer...\nLooking at the output of our training script we see that our Keras CNN obtained:\n98.34%\nclassification accuracy on the\ntraining set\nAnd\n82.91%\naccuracy on the\ntesting set\nThe training loss/accuracy plot follows:\nFigure 3:\nTraining and validation loss/accuracy plot for a Pokedex deep learning classifier trained with Keras.\nAs you can see in\nFigure 3\n, I trained the model for 100 epochs and achieved low loss with limited overfitting. With additional training data we could obtain higher accuracy as well.\nCreating our CNN and Keras testing script\nNow that our CNN is trained, we need to implement a script to classify images that are not part of our training or validation/testing set. Open up a new file, name it\nclassify.py\n, and insert the following code:\n# import the necessary packages\nfrom tensorflow.keras.preprocessing.image import img_to_array\nfrom tensorflow.keras.models import load_model\nimport numpy as np\nimport argparse\nimport imutils\nimport pickle\nimport cv2\nimport os\nFirst we import the necessary packages (\nLines 2-9\n).\nFrom there, let’s parse command line arguments:\n# construct the argument parser and parse the arguments\nap = argparse.ArgumentParser()\nap.add_argument(\"-m\", \"--model\", required=True,\n\thelp=\"path to trained model model\")\nap.add_argument(\"-l\", \"--labelbin\", required=True,\n\thelp=\"path to label binarizer\")\nap.add_argument(\"-i\", \"--image\", required=True,\n\thelp=\"path to input image\")\nargs = vars(ap.parse_args())\nWe’ve have three required\ncommand line arguments\nwe need to parse:\n--model\n: The path to the model that we just trained.\n--labelbin\n: The path to the label binarizer file.\n--image\n: Our input image file path.\nEach of these arguments is established and parsed on\nLines 12-19\n. Remember, you don’t need to modify these lines — I’ll show you how to run the program in the next section using the command line arguments provided at runtime.\nNext, we’ll load and preprocess the image:\n# load the image\nimage = cv2.imread(args[\"image\"])\noutput = image.copy()\n \n# pre-process the image for classification\nimage = cv2.resize(image, (96, 96))\nimage = image.astype(\"float\") / 255.0\nimage = img_to_array(image)\nimage = np.expand_dims(image, axis=0)\nHere we load the input\nimage\n(\nLine 22\n) and make a copy called\noutput\nfor display purposes (\nLine 23\n).\nThen we preprocess the\nimage\nin the exact same manner that we did for training (\nLines 26-29\n).\nFrom there, let’s load the model + label binarizer and then classify the image:\n# load the trained convolutional neural network and the label\n# binarizer\nprint(\"[INFO] loading network...\")\nmodel = load_model(args[\"model\"])\nlb = pickle.loads(open(args[\"labelbin\"], \"rb\").read())\n\n# classify the input image\nprint(\"[INFO] classifying image...\")\nproba = model.predict(image)[0]\nidx = np.argmax(proba)\nlabel = lb.classes_[idx]\nIn order to classify the image, we need the\nmodel\nand label binarizer in memory. We load both on\nLines 34 and 35\n.\nSubsequently, we classify the\nimage\nand create the\nlabel\n(\nLines 39-41\n).\nThe remaining code block is for display purposes:\n# we'll mark our prediction as \"correct\" of the input image filename\n# contains the predicted label text (obviously this makes the\n# assumption that you have named your testing image files this way)\nfilename = args[\"image\"][args[\"image\"].rfind(os.path.sep) + 1:]\ncorrect = \"correct\" if filename.rfind(label) != -1 else \"incorrect\"\n\n# build the label and draw the label on the image\nlabel = \"{}: {:.2f}% ({})\".format(label, proba[idx] * 100, correct)\noutput = imutils.resize(output, width=400)\ncv2.putText(output, label, (10, 25),  cv2.FONT_HERSHEY_SIMPLEX,\n\t0.7, (0, 255, 0), 2)\n\n# show the output image\nprint(\"[INFO] {}\".format(label))\ncv2.imshow(\"Output\", output)\ncv2.waitKey(0)\nOn\nLines 46 and 47\n, we’re extracting the name of the Pokemon from the\nfilename\nand comparing it to the\nlabel\n. The\ncorrect\nvariable will be either\n\"correct\"\nor\n\"incorrect\"\nbased on this.\nObviously these two lines make the assumption that your input image has a filename that contains the true label.\nFrom there we take the following steps:\nAppend the probability percentage and\n\"correct\"\n/\n\"incorrect\"\ntext to the class\nlabel\n(\nLine 50\n).\nResize the\noutput\nimage so it fits our screen (\nLine 51\n).\nDraw the\nlabel\ntext on the\noutput\nimage (\nLines 52 and 53\n).\nDisplay the\noutput\nimage and wait for a keypress to exit (\nLines 57 and 58\n).\nClassifying images with our CNN and Keras\nWe’re now ready to run the\nclassify.py\nscript!\nEnsure that you’ve grabbed the code + images from the\n“Downloads”\nsection at the bottom of this post.\nOnce you’ve downloaded and unzipped the archive change into the root directory of this project and follow along starting with an image of Charmander. Notice that we’ve provided three command line arguments in order to run the script:\n$ python classify.py --model pokedex.model --labelbin lb.pickle \\\n\t--image examples/charmander_counter.png\nUsing TensorFlow backend.\n[INFO] loading network...\n[INFO] classifying image...\n[INFO] charmander: 85.42% (correct)\nFigure 4:\nCorrectly classifying an input image using Keras and Convolutional Neural Networks.\nAnd now let’s query our model with the loyal and fierce\nBulbasaur\nstuffed Pokemon:\n$ python classify.py --model pokedex.model --labelbin lb.pickle \\\n\t--image examples/bulbasaur_plush.png\nUsing TensorFlow backend.\n[INFO] loading network...\n[INFO] classifying image...\n[INFO] bulbasaur: 99.61% (correct)\nFigure 5:\nAgain, our Keras deep learning image classifier is able to correctly classify the input image [\nimage source\n]\nLet’s try a toy action figure of\nMewtwo\n(a genetically engineered Pokemon):\n$ python classify.py --model pokedex.model --labelbin lb.pickle \\\n\t--image examples/mewtwo_toy.png\nUsing TensorFlow backend.\n[INFO] loading network...\n[INFO] classifying image...\n[INFO] mewtwo: 81.52% (correct)\nFigure 6:\nUsing Keras, deep learning, and Python we are able to correctly classify the input image using our CNN. [\nimage source\n]\nWhat would an example Pokedex be if it couldn’t recognize the infamous\nPikachu\n:\n$ python classify.py --model pokedex.model --labelbin lb.pickle \\\n\t--image examples/pikachu_toy.png\nUsing TensorFlow backend.\n[INFO] loading network...\n[INFO] classifying image...\n[INFO] pikachu: 100.00% (correct)\nFigure 7:\nUsing our Keras model we can recognize the iconic Pikachu Pokemon. [\nimage source\n]\nLet’s try the cute\nSquirtle\nPokemon:\n$ python classify.py --model pokedex.model --labelbin lb.pickle \\\n\t--image examples/squirtle_plush.png\nUsing TensorFlow backend.\n[INFO] loading network...\n[INFO] classifying image...\n[INFO] squirtle: 99.96% (correct)\nFigure 8:\nCorrectly classifying image data using Keras and a CNN. [\nimage source\n]\nAnd last but not least, let’s classify my fire-tailed\nCharmander\nagain. This time he is being shy and is partially occluded by my monitor.\n$ python classify.py --model pokedex.model --labelbin lb.pickle \\\n\t--image examples/charmander_hidden.png\nUsing TensorFlow backend.\n[INFO] loading network...\n[INFO] classifying image...\n[INFO] charmander: 98.78% (correct)\nFigure 9:\nOne final example of correctly classifying an input image using Keras and Convolutional Neural Networks (CNNs).\nEach of these Pokemons were no match for my new Pokedex.\nCurrently, there are around\n807 different species of Pokemon\n. Our classifier was trained on only five different Pokemon (for the sake of simplicity).\nIf you’re looking to train a classifier to recognize more Pokemon for a bigger Pokedex,\nyou’ll need additional training images for each class\n.\nIdeally, your goal should be to have\n500-1,000 images\nper class\nyou wish to recognize.\nTo acquire training images, I suggest that you\nlook no further than Microsoft Bing’s Image Search API\n. This API is hands down easier to use than the\nprevious hack of Google Image Search that I shared\n(but that would work too).\nLimitations of this model\nOne of the primary limitations of this model is the small amount of training data. I tested on various images and at times the classifications were incorrect. When this happened, I examined the input image + network more closely and found that the color(s) most dominant in the image influence the classification dramatically.\nFor example, lots of red and oranges in an image will likely return\n“Charmander”\nas the label. Similarly, lots of yellows in an image will normally result in a\n“Pikachu”\nlabel.\nThis is partially due to our input data. Pokemon are obviously fictitious so there no actual “real-world” images of them (other than the action figures and toy plushes).\nMost of our images came from either fan illustrations or stills from the movie/TV show. And furthermore, we only had a limited amount of data for each class (~225-250 images).\nIdeally, we should have at least 500-1,000 images per class when training a Convolutional Neural Network. Keep this in mind when working with your own data.\nCan we use this Keras deep learning model as a REST API?\nIf you would like to run this model (or any other deep learning model) as a REST API, I wrote three blog posts to help you get started:\nBuilding a simple Keras + deep learning REST API\n(Keras.io guest post)\nA scalable Keras + deep learning REST API\nDeep learning in production with Keras, Redis, Flask, and Apache\nWhat's next? We recommend\nPyImageSearch University\n.\nCourse information:\n120+ total classes • 115+ hours of on-demand code walkthrough videos • Last updated: October 2026\n★★★★★\n4.84 (128 Ratings) • 16,000+ Students Enrolled\nI strongly believe that if you had the right teacher you could\nmaster\ncomputer vision and deep learning.\nDo you think learning computer vision and deep learning has to be time-consuming, overwhelming, and complicated? Or has to involve complex mathematics and equations? Or requires a degree in computer science?\nThat’s\nnot\nthe case.\nAll you need to master computer vision and deep learning is for someone to explain things to you in\nsimple, intuitive\nterms.\nAnd that’s exactly what I do\n. My mission is to change education and how complex Artificial Intelligence topics are taught.\nIf you're serious about learning computer vision, your next stop should be PyImageSearch University, the most comprehensive computer vision, deep learning, and OpenCV course online today. Here you’ll learn how to\nsuccessfully\nand\nconfidently\napply computer vision to your work, research, and projects. Join me in computer vision mastery.\nInside PyImageSearch University you'll find:\n✓\n120+ courses\non essential computer vision, deep learning, and OpenCV topics\n✓\n94+ Certificates\nof Completion\n✓\n115+ hours\nof on-demand video\n✓\nBrand new courses released\nregularly\n, ensuring you can keep up with state-of-the-art techniques\n✓\nPre-configured Jupyter Notebooks in Google Colab\n✓ Run all code examples in your web browser — works on Windows, macOS, and Linux (no dev environment configuration required!)\n✓ Access to\ncentralized code repos for\nall\n540+ tutorials\non PyImageSearch\n✓\nEasy one-click downloads\nfor code, datasets, pre-trained models, etc.\n✓\nAccess\non mobile, laptop, desktop, etc.\nClick here to join PyImageSearch University\nSummary\nIn today’s blog post you learned how to train a Convolutional Neural Network (CNN) using the Keras deep learning library.\nOur dataset was gathered\nusing the procedure discussed in last week’s blog post\n.\nIn particular, our dataset consists of 1,191 images of five separate Pokemon (animal-like creatures that exist in the world of Pokemon, the popular TV show, video game, and trading card series).\nUsing our Convolutional Neural Network and Keras, we were able to obtain\n82% accuracy\n, which is quite respectable given (1) the limited size of our dataset and (2) the number of parameters in our network.\nIn next week’s blog post I’ll be demonstrating how we can:\nTake our trained Keras + Convolutional Neural Network model…\n…and deploy it to a smartphone with only a few lines of code!\nIt’s going to be a great post, don’t miss it!\nTo download the source code to this post (and be notified when next week’s can’t miss post goes live),\njust enter your email address in the form below!\nDownload the Source Code and FREE 17-page Resource Guide\nEnter your email address below to get a .zip of the code and a\nFREE 17-page Resource Guide on Computer Vision, OpenCV, and Deep Learning.\nInside you'll find my hand-picked tutorials, books, courses, and libraries to help you master CV and DL!\nDownload the code!\nWebsite",
  "metadata": {
    "title": "Keras and Convolutional Neural Networks (CNNs) - PyImageSearch",
    "description": "This gentle guide will show you how to implement, train, and evaluate your first Convolutional Neural Network (CNN) with Keras and deep learning.",
    "keywords": null,
    "author": "Adrian Rosebrock",
    "og:locale": "en_US",
    "og:type": "article",
    "og:title": "Keras and Convolutional Neural Networks (CNNs) - PyImageSearch",
    "og:description": "This gentle guide will show you how to implement, train, and evaluate your first Convolutional Neural Network (CNN) with Keras and deep learning.",
    "og:url": "https://pyimagesearch.com/2018/04/16/keras-and-convolutional-neural-networks-cnns/",
    "og:site_name": "PyImageSearch",
    "og:image": "https://pyimagesearch.com/wp-content/uploads/2018/04/cnn_keras_header.jpg",
    "og:image:width": "600",
    "og:image:height": "600",
    "og:image:type": "image/jpeg",
    "twitter:label1": "Written by",
    "twitter:data1": "Adrian Rosebrock",
    "twitter:label2": "Est. reading time",
    "twitter:data2": "27 minutes",
    "article:published_time": "2018-04-16T14:00:55+00:00",
    "article:modified_time": "2023-02-20T09:14:34+00:00"
  },
  "paywall": {
    "listed_paid": false,
    "detected": false,
    "marker": "",
    "public_only": false,
    "policy": "付费/会员内容只保留页面公开可见部分，不绕过付费墙"
  },
  "crawl": {
    "success": true,
    "status_code": 200,
    "error": ""
  },
  "code_blocks": [
    {
      "file": "attachments/de0ec63f5f3a/code/body-000.txt",
      "chars": 329,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-001.txt",
      "chars": 445,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-002.txt",
      "chars": 458,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-003.txt",
      "chars": 249,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-004.txt",
      "chars": 354,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-005.txt",
      "chars": 356,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-006.txt",
      "chars": 337,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-007.txt",
      "chars": 617,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-008.txt",
      "chars": 32,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-009.txt",
      "chars": 498,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-010.txt",
      "chars": 412,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-011.txt",
      "chars": 404,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-012.txt",
      "chars": 37,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-013.txt",
      "chars": 505,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-014.txt",
      "chars": 223,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-015.txt",
      "chars": 521,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-016.txt",
      "chars": 260,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-017.txt",
      "chars": 501,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-018.txt",
      "chars": 1321,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-019.txt",
      "chars": 226,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-020.txt",
      "chars": 362,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-021.txt",
      "chars": 258,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-022.txt",
      "chars": 343,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-023.txt",
      "chars": 681,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-024.txt",
      "chars": 223,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-025.txt",
      "chars": 219,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-026.txt",
      "chars": 211,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-027.txt",
      "chars": 214,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-028.txt",
      "chars": 217,
      "language": "text"
    },
    {
      "file": "attachments/de0ec63f5f3a/code/body-029.txt",
      "chars": 222,
      "language": "text"
    }
  ],
  "images": [
    {
      "status": "ok",
      "url": "https://fast.wistia.com/embed/medias/ahvl0w2bq0/swatch",
      "bytes": 1884,
      "reused": false,
      "local_path": "attachments/de0ec63f5f3a/images/body-e6b2c82c2e821a451c24.img",
      "src": "https://fast.wistia.com/embed/medias/ahvl0w2bq0/swatch",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://b2633864.assetcdn.net/2633864/wp-content/uploads/2018/04/cnn_keras_header.jpg?lossy=2&strip=1&webp=1",
      "bytes": 39313,
      "reused": false,
      "local_path": "attachments/de0ec63f5f3a/images/body-b87d76532832c4d87c74.jpg",
      "src": "https://b2633864.assetcdn.net/2633864/wp-content/uploads/2018/04/cnn_keras_header.jpg?lossy=2&strip=1&webp=1",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/source-code-icon.png?lossy=2&strip=1&webp=1",
      "bytes": 884,
      "reused": false,
      "local_path": "attachments/de0ec63f5f3a/images/body-0f8ec41d2103feb4399d.png",
      "src": "https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/source-code-icon.png?lossy=2&strip=1&webp=1",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://b2633864.assetcdn.net/2633864/wp-content/uploads/2018/04/cnn_keras_dataset.jpg?lossy=2&strip=1&webp=1",
      "bytes": 100888,
      "reused": false,
      "local_path": "attachments/de0ec63f5f3a/images/body-b67e9569a190d6429dc5.jpg",
      "src": "https://b2633864.assetcdn.net/2633864/wp-content/uploads/2018/04/cnn_keras_dataset.jpg?lossy=2&strip=1&webp=1",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://b2633864.assetcdn.net/2633864/wp-content/uploads/2018/04/cnn_keras_smallervggnet.png?lossy=2&strip=1&webp=1",
      "bytes": 17329,
      "reused": true,
      "local_path": "attachments/de0ec63f5f3a/images/008.png",
      "src": "https://b2633864.assetcdn.net/2633864/wp-content/uploads/2018/04/cnn_keras_smallervggnet.png?lossy=2&strip=1&webp=1",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://b2633864.assetcdn.net/2633864/wp-content/uploads/2018/04/cnn_keras_plot.png?lossy=2&strip=1&webp=1",
      "bytes": 15787,
      "reused": true,
      "local_path": "attachments/de0ec63f5f3a/images/010.png",
      "src": "https://b2633864.assetcdn.net/2633864/wp-content/uploads/2018/04/cnn_keras_plot.png?lossy=2&strip=1&webp=1",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://b2633864.assetcdn.net/2633864/wp-content/uploads/2018/04/cnn-keras-charmander_counter.png?lossy=2&strip=1&webp=1",
      "bytes": 140753,
      "reused": false,
      "local_path": "attachments/de0ec63f5f3a/images/body-5e4531d7a2ae7da4e051.png",
      "src": "https://b2633864.assetcdn.net/2633864/wp-content/uploads/2018/04/cnn-keras-charmander_counter.png?lossy=2&strip=1&webp=1",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://b2633864.assetcdn.net/2633864/wp-content/uploads/2018/04/cnn-keras-bulbasaur_result_01.png?lossy=2&strip=1&webp=1",
      "bytes": 138175,
      "reused": false,
      "local_path": "attachments/de0ec63f5f3a/images/body-2a6d851aa4dd21996363.png",
      "src": "https://b2633864.assetcdn.net/2633864/wp-content/uploads/2018/04/cnn-keras-bulbasaur_result_01.png?lossy=2&strip=1&webp=1",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://b2633864.assetcdn.net/2633864/wp-content/uploads/2018/04/cnn-keras-mewtwo_result_01.png?lossy=2&strip=1&webp=1",
      "bytes": 82686,
      "reused": false,
      "local_path": "attachments/de0ec63f5f3a/images/body-3ed2d3e9357d6d0c1721.png",
      "src": "https://b2633864.assetcdn.net/2633864/wp-content/uploads/2018/04/cnn-keras-mewtwo_result_01.png?lossy=2&strip=1&webp=1",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://b2633864.assetcdn.net/2633864/wp-content/uploads/2018/04/cnn_keras_pikachu_result_01.png?lossy=2&strip=1&webp=1",
      "bytes": 94849,
      "reused": false,
      "local_path": "attachments/de0ec63f5f3a/images/body-a29ba51d34a4e21f02bc.png",
      "src": "https://b2633864.assetcdn.net/2633864/wp-content/uploads/2018/04/cnn_keras_pikachu_result_01.png?lossy=2&strip=1&webp=1",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://b2633864.assetcdn.net/2633864/wp-content/uploads/2018/04/cnn_keras_squirtle_result_01.png?lossy=2&strip=1&webp=1",
      "bytes": 192369,
      "reused": false,
      "local_path": "attachments/de0ec63f5f3a/images/body-c8b3dd5b00f57206e54f.png",
      "src": "https://b2633864.assetcdn.net/2633864/wp-content/uploads/2018/04/cnn_keras_squirtle_result_01.png?lossy=2&strip=1&webp=1",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://b2633864.assetcdn.net/2633864/wp-content/uploads/2018/04/cnn_keras_charmander_result_02.png?lossy=2&strip=1&webp=1",
      "bytes": 110685,
      "reused": false,
      "local_path": "attachments/de0ec63f5f3a/images/body-a4df0d5e45306702ea92.png",
      "src": "https://b2633864.assetcdn.net/2633864/wp-content/uploads/2018/04/cnn_keras_charmander_result_02.png?lossy=2&strip=1&webp=1",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://fast.wistia.com/embed/medias/kno0cmko2z/swatch",
      "bytes": 3143,
      "reused": false,
      "local_path": "attachments/de0ec63f5f3a/images/body-8ec9834d54100244f57f.img",
      "src": "https://fast.wistia.com/embed/medias/kno0cmko2z/swatch",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1",
      "bytes": 28126,
      "reused": false,
      "local_path": "attachments/de0ec63f5f3a/images/body-8cd5816cf73385d96fcd.png",
      "src": "https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1",
      "alt": "",
      "downloaded": true
    }
  ],
  "audio": [],
  "videos": [],
  "links": [
    {
      "href": "https://pyimagesearch.com/wp-content/uploads/2018/04/cnn_keras_header.jpg",
      "label": "",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/2018/04/09/how-to-quickly-build-a-deep-learning-image-dataset/",
      "label": "How to (quickly) build a deep learning image dataset",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/2019/12/09/how-to-install-tensorflow-2-0-on-ubuntu/",
      "label": "How to install TensorFlow 2.0 on Ubuntu",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/2019/12/09/how-to-install-tensorflow-2-0-on-macos/",
      "label": "How to install TensorFlow 2.0 on macOS",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/faqs/single-faq/can-you-help-me-do-___-on-windows/",
      "label": "PyImageSearch does not recommend or support Windows for CV/DL projects",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/wp-content/uploads/2018/04/cnn_keras_dataset.jpg",
      "label": "",
      "type": "a"
    },
    {
      "href": "https://bulbapedia.bulbagarden.net/wiki/Bulbasaur_(Pok%C3%A9mon)",
      "label": "Bulbasaur",
      "type": "a"
    },
    {
      "href": "https://bulbapedia.bulbagarden.net/wiki/Charmander_(Pok%C3%A9mon)",
      "label": "Charmander",
      "type": "a"
    },
    {
      "href": "https://bulbapedia.bulbagarden.net/wiki/Squirtle_(Pok%C3%A9mon)",
      "label": "Squirtle",
      "type": "a"
    },
    {
      "href": "https://bulbapedia.bulbagarden.net/wiki/Pikachu_(Pok%C3%A9mon)",
      "label": "Pikachu",
      "type": "a"
    },
    {
      "href": "https://bulbapedia.bulbagarden.net/wiki/Mewtwo_(Pok%C3%A9mon)",
      "label": "Mewtwo",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/wp-content/uploads/2018/04/cnn_keras_smallervggnet.png",
      "label": "",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/wp-content/uploads/2018/04/smallervggnet_model.png",
      "label": "here",
      "type": "a"
    },
    {
      "href": "https://arxiv.org/abs/1409.1556",
      "label": "Very Deep Convolutional Networks for Large Scale Image Recognition",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/2017/09/25/configuring-ubuntu-for-deep-learning-with-python/",
      "label": "Configuring Ubuntu for deep learning with Python",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/2017/09/27/setting-up-ubuntu-16-04-cuda-gpu-for-deep-learning-with-python/",
      "label": "Setting up Ubuntu 16.04 + CUDA + GPU for deep learning with Python",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/2017/09/29/macos-for-deep-learning-with-python-tensorflow-and-keras/",
      "label": "Configuring macOS for deep learning with Python",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/2017/09/20/pre-configured-amazon-aws-deep-learning-ami-with-python/",
      "label": "Amazon AMI for deep learning with Python",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/2018/03/21/my-review-of-microsofts-deep-learning-virtual-machine/",
      "label": "Microsoft’s data science virtual machine (DSVM) for deep learning",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/deep-learning-computer-vision-python-book/",
      "label": "Deep Learning for Computer Vision with Python",
      "type": "a"
    },
    {
      "href": "https://github.com/jrosebr1/imutils",
      "label": "my very own imutils package",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/2018/03/12/python-argparse-command-line-arguments/",
      "label": "command line arguments",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/2018/12/24/how-to-use-keras-fit-and-fit_generator-a-hands-on-tutorial/",
      "label": "fit and fit generator",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/2019/07/08/keras-imagedatagenerator-and-data-augmentation/",
      "label": "data augmentation",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/wp-content/uploads/2018/04/cnn_keras_plot.png",
      "label": "",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/wp-content/uploads/2018/04/cnn-keras-charmander_counter.png",
      "label": "",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/wp-content/uploads/2018/04/cnn-keras-bulbasaur_result_01.png",
      "label": "",
      "type": "a"
    },
    {
      "href": "http://yysboutique.ca/product/ys-pokemon-stuffed-toy-bulbasaur/",
      "label": "image source",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/wp-content/uploads/2018/04/cnn-keras-mewtwo_result_01.png",
      "label": "",
      "type": "a"
    },
    {
      "href": "https://shukuenshinobi.com/2016/09/22/pokemon-mewtwo-and-greninja-hero-figure-review/",
      "label": "image source",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/wp-content/uploads/2018/04/cnn_keras_pikachu_result_01.png",
      "label": "",
      "type": "a"
    },
    {
      "href": "https://www.lelong.com.my/pokemon-pikachu-soft-toys-35cm-yenzenvision-I5356985-2007-01-Sale-I.htm",
      "label": "image source",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/wp-content/uploads/2018/04/cnn_keras_squirtle_result_01.png",
      "label": "",
      "type": "a"
    },
    {
      "href": "https://mikitzune.com/collaboration-pokemon-forever-at-kiddyland/",
      "label": "image source",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/wp-content/uploads/2018/04/cnn_keras_charmander_result_02.png",
      "label": "",
      "type": "a"
    },
    {
      "href": "https://en.wikipedia.org/wiki/List_of_Pok%C3%A9mon",
      "label": "807 different species of Pokemon",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/2017/12/04/how-to-create-a-deep-learning-dataset-using-google-images/",
      "label": "previous hack of Google Image Search that I shared",
      "type": "a"
    },
    {
      "href": "https://blog.keras.io/building-a-simple-keras-deep-learning-rest-api.html",
      "label": "Building a simple Keras + deep learning REST API",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/2018/01/29/scalable-keras-deep-learning-rest-api/",
      "label": "A scalable Keras + deep learning REST API",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/2018/02/05/deep-learning-production-keras-redis-flask-apache/",
      "label": "Deep learning in production with Keras, Redis, Flask, and Apache",
      "type": "a"
    },
    {
      "href": "https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend",
      "label": "PyImageSearch University",
      "type": "a"
    }
  ],
  "repair_version": 2,
  "repaired_at": "2026-10-14T10:54:07Z",
  "source_url": "https://pyimagesearch.com/2018/04/16/keras-and-convolutional-neural-networks-cnns/",
  "papers": [
    {
      "status": "ok",
      "url": "https://arxiv.org/pdf/1409.1556",
      "bytes": 200010,
      "reused": true,
      "local_path": "papers/repair/01560c40deef0c339e01.pdf"
    }
  ],
  "actual_video_files": [
    {
      "local_path": "attachments/de0ec63f5f3a/videos/ahvl0w2bq0.mp4",
      "bytes": 392782556,
      "valid": true,
      "height": 1080,
      "width": 1920,
      "duration": "592.792208",
      "status": "ok"
    }
  ],
  "body_chars": 39923,
  "expected_body_images": 14,
  "discovery_status": "ok",
  "delivery_status": "ok",
  "missing_reason": "",
  "raw_html": "attachments/de0ec63f5f3a/pages/source.html",
  "local_html": "attachments/de0ec63f5f3a/pages/article.html",
  "body_base_path": "attachments/de0ec63f5f3a/pages/",
  "body_markdown_path": "attachments/de0ec63f5f3a/pages/article.md"
}