<jupyter_start><jupyter_text>This is a companion notebook for the book [Deep Learning with Python, Second Edition](https://www.manning.com/books/deep-learning-with-python-second-edition?a_aid=keras&a_bid=76564dff). For readability, it only contains runnable code blocks and section titles, and omits everything else in the book: text paragraphs, figures, and pseudocode.**If you want to be able to follow what's going on, I recommend reading the notebook side by side with your copy of the book.**This notebook was generated for TensorFlow 2.6.  Introduction to deep learning for computer vision Computer vision is the problem domain that led to the initial rise of deep learning between 2011 and 2015. A type of deep learning called convolutional neural networks started getting remarkably good results on image classification competitions around that time, first with <NAME> winning two niche competitions (the ICDAR 2011 Chinese character recognition competition and the IJCNN 2011 German traffic signs recognition competition), then more notably in Fall 2012 with Hinton’s group winning the high-profile ImageNet large-scale visual recognition challenge.  Introduction to convnets We’re about to dive into the theory of what convnets are and why they have been so successful at computer vision tasks. But first, let’s take a practical look at a simple convnet example. It uses a convnet to classify MNIST digits, a task we performed in chapter 2 using a densely-connected network (our test accuracy then was 97.8%). Even though the convnet will be basic, its accuracy will blow out of the water that of the densely connected model from chapter 2. **Instantiating a small convnet** The following lines of code show you what a basic convnet looks like. It’s a stack of `Conv2D` and `MaxPooling2D` layers. You’ll see in a minute exactly what they do. We’ll build the model using the `Functional API`, which we introduced in the previous chapter.<jupyter_code>from tensorflow import keras
from tensorflow.keras import layers

inputs = keras.Input(shape=(28, 28, 1))  # (image_height, image_width, image_channels)
x = layers.Conv2D(filters=32, kernel_size=3, activation="relu")(inputs)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=64, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=128, kernel_size=3, activation="relu")(x)
x = layers.Flatten()(x)
outputs = layers.Dense(10, activation="softmax")(x)

model = keras.Model(inputs=inputs, outputs=outputs)<jupyter_output><empty_output><jupyter_text>**Displaying the model's summary**<jupyter_code># Let’s display the architecture of our convnet
model.summary()<jupyter_output>Model: "model_3"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_6 (InputLayer)         [(None, 28, 28, 1)]       0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 26, 26, 32)        320       
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 13, 13, 32)        0         
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 11, 11, 64)        18496     
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 5, 5, 64)          0         
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 3, 3, 128)         73856     
___________________________________________________________[...]<jupyter_text>You can see that the output of every `Conv2D` and `MaxPooling2D` layer is a 3D tensor of shape (height, width, channels). The width and height dimensions tend to **shrink** as you go deeper in the model. The number of channels is controlled by the first argument passed to the `Conv2D` layers (32, 64, or 128). **Training the convnet on MNIST images** Now, let’s train the convnet on the MNIST digits. We’ll reuse a lot of the code from the MNIST example in chapter 2. Because we’re doing 10-ways classification with a softmax output, we’ll use the **categorical crossentropy loss**, and because our labels are integers, we’ll use the sparse version, ***sparse_categorical_crossentropy**.<jupyter_code>from tensorflow.keras.datasets import mnist

(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images = train_images.reshape((60000, 28, 28, 1))
train_images = train_images.astype("float32") / 255
test_images = test_images.reshape((10000, 28, 28, 1))
test_images = test_images.astype("float32") / 255

model.compile(optimizer="rmsprop",
    loss="sparse_categorical_crossentropy",
    metrics=["accuracy"])

history = model.fit(train_images, train_labels, epochs=5, batch_size=64)<jupyter_output>Epoch 1/5
938/938 [==============================] - 28s 29ms/step - loss: 0.1542 - accuracy: 0.9528
Epoch 2/5
938/938 [==============================] - 31s 33ms/step - loss: 0.0432 - accuracy: 0.9869
Epoch 3/5
938/938 [==============================] - 30s 32ms/step - loss: 0.0299 - accuracy: 0.9912
Epoch 4/5
938/938 [==============================] - 32s 35ms/step - loss: 0.0228 - accuracy: 0.9928
Epoch 5/5
938/938 [==============================] - 32s 34ms/step - loss: 0.0184 - accuracy: 0.9945<jupyter_text>**Evaluating the convnet**<jupyter_code>test_loss, test_acc = model.evaluate(test_images, test_labels)
print(f"Test accuracy: {test_acc:.3f}")<jupyter_output>313/313 [==============================] - 1s 4ms/step - loss: 0.0344 - accuracy: 0.9905
Test accuracy: 0.990<jupyter_text>Whereas the densely-connected model from chapter 2 had a test accuracy of $97.8\%$, the basic convnet has a test accuracy of $99.1\%$: we decreased the error rate by about $60\%$ (relative). Not bad!  The convolution operation The fundamental difference between a densely-connected layer and a convolution layer is this: `Dense` layers learn **global patterns** in their input feature space (for example, for a MNIST digit, patterns involving all pixels), whereas convolution layers learn **local patterns** (see figure): in the case of images, patterns found in small 2D windows of the inputs. In the previous example, these windows were all $3 \times 3$.![](https://drek4537l1klr.cloudfront.net/chollet2/v-7/Figures/local_patterns.jpg) This key characteristic gives convnets two interesting properties:- The patterns they learn are **translation invariant**. After learning a certain pattern in the lower-right corner of a picture, a convnet can recognize it anywhere: for example, in the upper-left corner. A densely-connected model would have to learn the pattern anew if it appeared at a new location. This makes convnets data efficient when processing images (because the visual world is fundamentally translation invariant): they need fewer training samples to learn representations that have generalization power.- They can learn **spatial hierarchies of patterns** (see figure). A first convolution layer will learn small local patterns such as edges, a second convolution layer will learn larger patterns made of the features of the first layers, and so on. This allows convnets to efficiently learn increasingly complex and abstract visual concepts—because the visual world is fundamentally spatially hierarchical.![](https://drek4537l1klr.cloudfront.net/chollet2/v-7/Figures/visual_hierarchy_hires.png) Convolutions operate over 3D tensors, called **feature maps**, with two spatial axes (height and width) as well as a depth axis (also called the channels axis). For an RGB image, the dimension of the depth axis is 3, because the image has three color channels: red, green, and blue. For a black-and-white picture, like the MNIST digits, the depth is 1 (levels of gray). The convolution operation extracts patches from its input feature map and applies the same transformation to all of these patches, producing an output feature map. This **output feature map** is still a 3D tensor: it has a width and a height. Its depth can be arbitrary, because the output depth is a parameter of the layer, and the different channels in that depth axis no longer stand for specific colors as in RGB input; rather, they stand for filters. Filters encode specific aspects of the input data: at a high level, a single filter could encode the concept "presence of a face in the input," for instance. In the MNIST example, the first convolution layer takes a feature map of size $(28, 28, 1)$ and outputs a feature map of size $(26, 26, 32)$: it computes $32$ filters over its input. Each of these $32$ output channels contains a $26 \times 26$ grid of values, which is a response map of the filter over the input, indicating the response of that filter pattern at different locations in the input (see figure).That is what the term feature map means: every dimension in the depth axis is a feature (or filter), and the 2D tensor `output[:, :, n]` is the 2D spatial map of the response of this filter over the input.![](https://drek4537l1klr.cloudfront.net/chollet2/v-7/Figures/response_map_hires.png) Convolutions are defined by two key parameters:- **Size of the patches** extracted from the inputs — These are typically $3 \times 3$ or $5 \times 5$. In the example, they were $3 \times 3$, which is a common choice.- Depth of the output feature map — The **number of filters** computed by the convolution. The example started with a depth of $32$ and ended with a depth of $64$.In Keras `Conv2D` layers, these parameters are the first arguments passed to the layer: `Conv2D(output_depth, (window_height, window_width))`. A convolution works by sliding these windows of size $3 \times 3$ or $5 \times 5$ over the 3D input feature map, stopping at every possible location, and extracting the 3D patch of surrounding features `(shape (window_height, window_width, input_depth))`. Each such 3D patch is then transformed into a 1D vector of shape (output_depth,), which is done via a tensor product with a **learned weight matrix**, called the **convolution kernel**—the same kernel is reused across every patch. All of these vectors (one per patch) are then spatially reassembled into a 3D output map of shape `(height, width, output_depth)`. Every spatial location in the output feature map corresponds to the same location in the input feature map (for example, the lower-right corner of the output contains information about the lower-right corner of the input). For instance, with $3 \times 3$ windows, the vector `output[i, j, :]` comes from the 3D patch `input[i-1:i+1, j-1:j+1, :]`. The full process is detailed in the figure.![](https://drek4537l1klr.cloudfront.net/chollet2/v-7/Figures/how_convolution_works.png) Note that the output width and height may differ from the input width and height. They may differ for two reasons:- Border effects, which can be countered by **padding** the input feature map- The use of strides, which I’ll define in a second  Understanding border effects and padding Consider a 5 × 5 feature map (25 tiles total). There are only 9 tiles around which you can center a $3 \times 3$ window, forming a $3 \times 3$ grid (see figure). Hence, the output feature map will be $3 \times 3$. It shrinks a little: by exactly two tiles alongside each dimension, in this case. You can see this border effect in action in the earlier example: you start with $28 \times 28$ inputs, which become $26 \times 26$ after the first convolution layer.![](https://drek4537l1klr.cloudfront.net/chollet2/v-7/Figures/3x3_patches_in_5x5_input.png) If you want to get an output feature map with the same spatial dimensions as the input, you can use **padding**. Padding consists of adding an appropriate number of rows and columns on each side of the input feature map so as to make it possible to fit center convolution windows around every input tile. For a $3 \times 3$ window, you add one column on the right, one column on the left, one row at the top, and one row at the bottom. For a $5 \times 5$ window, you add two rows (see figure).![](https://drek4537l1klr.cloudfront.net/chollet2/v-7/Figures/padding_of_5x5_input.png) In `Conv2D` layers, padding is configurable via the `padding` argument, which takes two values: "valid", which means no padding (only valid window locations will be used); and "same", which means "pad in such a way as to have an output with the same width and height as the input." The padding argument defaults to "valid".  Understanding convolution strides The other factor that can influence output size is the notion of strides. The description of convolution so far has assumed that the center tiles of the convolution windows are all contiguous. But the distance between two successive windows is a parameter of the convolution, called its **stride**, which defaults to $1$. It’s possible to have strided convolutions: convolutions with a stride higher than 1. In the figure, you can see the patches extracted by a $3 \times 3$ convolution with stride $2$ over a $5 \times 5$ input (without padding)![](https://drek4537l1klr.cloudfront.net/chollet2/v-7/Figures/strides.png) Using stride $2$ means the width and height of the feature map are downsampled by a factor of $2$ (in addition to any changes induced by border effects). **Strided convolutions are rarely used in classification models**, but they come in handy for some types of models, as you will find out in the next chapter.In classification models, instead of strides, we tend to use the **max-pooling operation** to downsample feature maps—which you saw in action in our first convnet example. Let’s look at it in more depth.  The max-pooling operation In the convnet example, you may have noticed that the size of the feature maps is halved after every `MaxPooling2D` layer. For instance, before the first `MaxPooling2D` layers, the feature map is $26 \times 26$, but the max-pooling operation halves it to $13 \times 13$. That’s the role of max pooling: **to aggressively downsample feature maps**, much like strided convolutions. **An incorrectly-structured convnet missing its max pooling layers**<jupyter_code>inputs = keras.Input(shape=(28, 28, 1))
x = layers.Conv2D(filters=32, kernel_size=3, activation="relu")(inputs)
x = layers.Conv2D(filters=64, kernel_size=3, activation="relu")(x)
x = layers.Conv2D(filters=128, kernel_size=3, activation="relu")(x)
x = layers.Flatten()(x)
outputs = layers.Dense(10, activation="softmax")(x)

model_no_max_pool = keras.Model(inputs=inputs, outputs=outputs)
model_no_max_pool.summary()<jupyter_output>Model: "model_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_2 (InputLayer)         [(None, 28, 28, 1)]       0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 26, 26, 32)        320       
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 24, 24, 64)        18496     
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 22, 22, 128)       73856     
_________________________________________________________________
flatten_1 (Flatten)          (None, 61952)             0         
_________________________________________________________________
dense_1 (Dense)              (None, 10)                619530    
===========================================================[...]<jupyter_text>What’s wrong with this setup? Two things:- It isn’t conducive to learning a spatial hierarchy of features. The $3 \times 3$ windows in the third layer will only contain information coming from $7 \times 7$ windows in the initial input. The high-level patterns learned by the convnet will still be very small with regard to the initial input, which may not be enough to learn to classify digits (try recognizing a digit by only looking at it through windows that are $7 \times 7$ pixels!). We need the features from the last convolution layer to contain information about the totality of the input.- The final feature map has $22 \times 22 \times 128 = 61,952$ total coefficients per sample. This is huge. When you flatten it to stick a Dense layer of size $10$ on top, that layer would have over half a million parameters. This is far too large for such a small model and would result in intense overfitting.    In short, the reason to use downsampling is to reduce the number of feature-map coefficients to process, as well as to induce **spatial-filter hierarchies** by making successive convolution layers look at increasingly large windows (in terms of the fraction of the original input they cover). Note that max pooling isn’t the only way you can achieve such downsampling. As you already know, you can also use strides in the prior convolution layer. And you can use **average pooling** instead of max pooling, where each local input patch is transformed by taking the average value of each channel over the patch, rather than the max. But **max pooling tends to work better** than these alternative solutions. In a nutshell, the reason is that features tend to encode the spatial presence of some pattern or concept over the different tiles of the feature map (hence, the term feature map), and it’s more informative to look at the maximal presence of different features than at their average presence. So the most reasonable subsampling strategy is to first produce dense maps of features (via unstrided convolutions) and then look at the maximal activation of the features over small patches, rather than looking at sparser windows of the inputs (via strided convolutions) or averaging input patches, which could cause you to miss or dilute feature-presence information.  Training a convnet from scratch on a small dataset As a practical example, we’ll focus on classifying images as dogs or cats, in a dataset containing 5,000 pictures of cats and dogs (2,500 cats, 2,500 dogs). We’ll use 2,000 pictures for training, 1,000 for validation, and 2,000 for testing. We’ll start by naively training a small convnet on the 2,000 training samples, without any regularization, to set a baseline for what can be achieved. This will get us to a classification accuracy of about 70%. At that point, the main issue will be overfitting. Then we’ll introduce data augmentation, a powerful technique for mitigating overfitting in computer vision. By using data augmentation, we’ll improve the model to reach an accuracy of 80-85%.  The relevance of deep learning for small-data problems Because convnets learn local, translation-invariant features, they’re highly data efficient on perceptual problems. Training a convnet from scratch on a very small image dataset will still yield reasonable results despite a relative lack of data, without the need for any custom feature engineering. You’ll see this in action in this section. What’s more, deep-learning models are by nature highly repurposable: you can take, say, an image-classification or speech-to-text model trained on a large-scale dataset and reuse it on a significantly different problem with only minor changes. Specifically, in the case of computer vision, many pretrained models (usually trained on the ImageNet dataset) are now publicly available for download and can be used to bootstrap powerful vision models out of very little data. This is one of the greatest strengths of deep learning: feature reuse.   Downloading the data<jupyter_code># for Colab notebooks!
from google.colab import files
files.upload()
# for Colab notebooks!
!mkdir ~/.kaggle
!cp kaggle.json ~/.kaggle/
!chmod 600 ~/.kaggle/kaggle.json
!kaggle competitions download -c dogs-vs-cats
!unzip -qq train.zip<jupyter_output>Der Befehl "unzip" ist entweder falsch geschrieben oder
konnte nicht gefunden werden.<jupyter_text>Unsurprisingly, the original dogs-versus-cats Kaggle competition, all the way back in 2013, was won by entrants who used convnets. The best entries achieved up to 95% accuracy. In this example, we will get fairly close to this accuracy (in the next section), even though we will train our models on less than 10% of the data that was available to the competitors.![](https://drek4537l1klr.cloudfront.net/chollet2/v-7/Figures/dog_and_cat_samples.png) **Copying images to training, validation, and test directories** This dataset contains 25,000 images of dogs and cats (12,500 from each class) and is 543 MB (compressed). After downloading and uncompressing the data, we’ll create a new dataset containing three subsets: a training set with 1,000 samples of each class, a validation set with 500 samples of each class, and a test set with 1,000 samples of each class. Why do this? Because many of the image datasets you’ll encounter in your career only contain a few thousands of samples, not tens of thousands. Having more data available would make the problem easier—so it’s good practice to learn with a small dataset.<jupyter_code>import os, shutil, pathlib

original_dir = pathlib.Path("data/dogs-vs-cats/train")
new_base_dir = pathlib.Path("data/dogs-vs-cats_small")
# just run this once to create the dogs-vs-cats_small data set

def make_subset(subset_name, start_index, end_index):
    for category in ("cat", "dog"):
        dir_ = new_base_dir / subset_name / category
        os.makedirs(dir_, exist_ok=True)
        fnames = [f"{category}.{i}.jpg" for i in range(start_index, end_index)]
        for fname in fnames:
            shutil.copyfile(src=original_dir / fname,
                            dst=dir_ / fname)

make_subset("train", start_index=0, end_index=1000)
make_subset("validation", start_index=1000, end_index=1500)
make_subset("test", start_index=1500, end_index=2500)<jupyter_output><empty_output><jupyter_text>So we now have 2,000 training images, 1,000 validation images, and 2,000 test images. Each split contains the same number of samples from each class: this is a balanced binary-classification problem, which means classification accuracy will be an appropriate measure of success.  Building your model We will reuse the same general model structure you saw in the first example: the convnet will be a stack of alternated `Conv2D` (with relu activation) and `MaxPooling2D` layers.But because we’re dealing with bigger images and a more complex problem, we’ll make our model larger, accordingly: it will have two more `Conv2D` `MaxPooling2D` stages. This serves both to augment the capacity of the model and to further reduce the size of the feature maps so they aren’t overly large when we reach the `Flatten` layer. Here, because we start from inputs of size $180 \text{pixels} \times 180 \text{pixels}$ (a somewhat arbitrary choice), we end up with feature maps of size $7 \times 7$ just before the Flatten layer.One last small difference: we will start the model with a `Rescaling` layer, which will rescale image inputs (whose values are originally in the $[0, 255]$ range) to the $[0, 1]$ range. **Instantiating a small convnet for dogs vs. cats classification**<jupyter_code>from tensorflow import keras
from tensorflow.keras import layers

inputs = keras.Input(shape=(180, 180, 3))
x = layers.Rescaling(1./255)(inputs)
x = layers.Conv2D(filters=32, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=64, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=128, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=256, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=256, kernel_size=3, activation="relu")(x)
x = layers.Flatten()(x)
outputs = layers.Dense(1, activation="sigmoid")(x)

model = keras.Model(inputs=inputs, outputs=outputs)
model.summary()
keras.utils.plot_model(model, show_dtype=True, show_layer_names=True, show_shapes=True)<jupyter_output><empty_output><jupyter_text>**Configuring the model for training**<jupyter_code>model.compile(loss="binary_crossentropy",
              optimizer="rmsprop",
              metrics=["accuracy"])<jupyter_output><empty_output><jupyter_text>Data preprocessing As you know by now, data should be formatted into appropriately preprocessed floating-point tensors before being fed into the model. Currently, the data sits on a drive as JPEG files, so the steps for getting it into the model are roughly as follows:- Read the picture files.- Decode the JPEG content to RGB grids of pixels.- Convert these into floating-point tensors.- Resize them to a shared size (we’ll use $180 \times 180$).- Pack them into batches (we’ll use batches of $32$ images).It may seem a bit daunting, but fortunately Keras has utilities to take care of these steps automatically. In particular, Keras features the utility function `image_dataset_from_directory`, which lets you quickly set up a data pipeline that can automatically turn image files on disk into batches of preprocessed tensors. This is what you’ll use here.Calling `image_dataset_from_directory(directory)` will first list the subdirectories of directory and assume each one contains images from one of your class. It will then index the image files in each subdirectory. Finally, it will create and return a `tf.data.Dataset` object configured to read these files, shuffle them, decode them to tensors, resize them to a shared size, and pack them into batches. **Using `image_dataset_from_directory` to read images from directories**<jupyter_code>from tensorflow.keras.utils import image_dataset_from_directory

train_dataset = image_dataset_from_directory(
    new_base_dir / "train",
    image_size=(180, 180),
    batch_size=32)
validation_dataset = image_dataset_from_directory(
    new_base_dir / "validation",
    image_size=(180, 180),
    batch_size=32)
test_dataset = image_dataset_from_directory(
    new_base_dir / "test",
    image_size=(180, 180),
    batch_size=32)<jupyter_output>Found 2000 files belonging to 2 classes.
Found 1000 files belonging to 2 classes.
Found 2000 files belonging to 2 classes.<jupyter_text>**Instantiating a Dataset from a NumPy array**<jupyter_code>import numpy as np
import tensorflow as tf

random_numbers = np.random.normal(size=(1000, 16))
dataset = tf.data.Dataset.from_tensor_slices(random_numbers)<jupyter_output><empty_output><jupyter_text>**Iterating on a dataset**<jupyter_code>for i, element in enumerate(dataset):
    print(element.shape)
    if i >= 2:
        break<jupyter_output>(16,)
(16,)
(16,)<jupyter_text>**Batching a dataset**<jupyter_code>batched_dataset = dataset.batch(32)
for i, element in enumerate(batched_dataset):
    print(element.shape)
    if i >= 2:
        break<jupyter_output>(32, 16)
(32, 16)
(32, 16)<jupyter_text>**Applying a transformation to Dataset elements using `map()`**<jupyter_code>reshaped_dataset = dataset.map(lambda x: tf.reshape(x, (4, 4)))
for i, element in enumerate(reshaped_dataset):
    print(element.shape)
    if i >= 2:
        break<jupyter_output><empty_output><jupyter_text>**Displaying the shapes of the data and labels yielded by the Dataset**<jupyter_code>for data_batch, labels_batch in train_dataset:
    print("data batch shape:", data_batch.shape)
    print("labels batch shape:", labels_batch.shape)
    break<jupyter_output>data batch shape: (32, 180, 180, 3)
labels batch shape: (32,)<jupyter_text>**Fitting the model using a Dataset** Let’s fit the model on our dataset. We use the `validation_data` argument in `fit()` to monitor validation metrics on a separate Dataset object.Note that we also use a `ModelCheckpoint` callback to save the model after each epoch. We configure it with the path where to save the file, as well as the arguments `save_best_only=True` and `monitor="val_loss"`: they tell the callback to only save a new file (overwriting any previous one) when the current value of the val_loss metric is lower than at any previous time during training. This guarantees that your saved file will always contain the state of the model corresponding to its best-performing training epoch, in terms of its performance on the validation data. As a result, we won’t have to retrain a new model for a lower number of epochs if we start overfitting: we can just reload our saved file.<jupyter_code>callbacks = [
    keras.callbacks.ModelCheckpoint(
        filepath="models/dogs-vs-cats/convnet_from_scratch.keras",
        save_best_only=True,
        monitor="val_loss")
]

history = model.fit(
    train_dataset,
    epochs=30,
    validation_data=validation_dataset,
    callbacks=callbacks,
    use_multiprocessing=True
)<jupyter_output>Epoch 1/30
63/63 [==============================] - 72s 1s/step - loss: 0.5364 - accuracy: 0.7315 - val_loss: 0.6501 - val_accuracy: 0.6650
Epoch 2/30
63/63 [==============================] - 88s 1s/step - loss: 0.4798 - accuracy: 0.7780 - val_loss: 0.5721 - val_accuracy: 0.7160
Epoch 3/30
63/63 [==============================] - 88s 1s/step - loss: 0.4282 - accuracy: 0.8010 - val_loss: 0.5852 - val_accuracy: 0.7240
Epoch 4/30
63/63 [==============================] - 89s 1s/step - loss: 0.3611 - accuracy: 0.8355 - val_loss: 0.6319 - val_accuracy: 0.7370
Epoch 5/30
63/63 [==============================] - 89s 1s/step - loss: 0.3183 - accuracy: 0.8570 - val_loss: 0.6348 - val_accuracy: 0.7430
Epoch 6/30
63/63 [==============================] - 89s 1s/step - loss: 0.2439 - accuracy: 0.9055 - val_loss: 1.3627 - val_accuracy: 0.6640
Epoch 7/30
63/63 [==============================] - 1600s 26s/step - loss: 0.1961 - accuracy: 0.9245 - val_loss: 1.1222 - val_accuracy: 0.6940
Epoch 8/30
63/63 [...]<jupyter_text>**Displaying curves of loss and accuracy during training**<jupyter_code>import matplotlib.pyplot as plt

accuracy = history.history["accuracy"]
val_accuracy = history.history["val_accuracy"]
loss = history.history["loss"]
val_loss = history.history["val_loss"]

epochs = range(1, len(accuracy) + 1)
plt.plot(epochs, accuracy, "bo", label="Training accuracy")
plt.plot(epochs, val_accuracy, "b", label="Validation accuracy")
plt.title("Training and validation accuracy")
plt.legend()

plt.figure()
plt.plot(epochs, loss, "bo", label="Training loss")
plt.plot(epochs, val_loss, "b", label="Validation loss")
plt.title("Training and validation loss")
plt.legend()

plt.show()<jupyter_output><empty_output><jupyter_text>These plots are characteristic of overfitting. The training accuracy increases linearly over time, until it reaches nearly 100%, whereas the validation accuracy peaks at 75%. The validation loss reaches its minimum after only ten epochs and then stalls, whereas the training loss keeps decreasing linearly as training proceeds.![](https://drek4537l1klr.cloudfront.net/chollet2/v-7/Figures/training-and-validation.png) **Evaluating the model on the test set**<jupyter_code>test_model = keras.models.load_model("models/dogs-vs-cats/convnet_from_scratch.keras")
test_loss, test_acc = test_model.evaluate(test_dataset)
print(f"Test accuracy: {test_acc:.3f}")<jupyter_output>63/63 [==============================] - 13s 194ms/step - loss: 0.5821 - accuracy: 0.7105
Test accuracy: 0.711<jupyter_text>We get a test accuracy of 69.5% (due to the randomness of neural network initializations, you may get numbers within one percentage point of that).Because you have relatively few training samples (2,000), **overfitting will be your number-one concern**. You already know about a number of techniques that can help mitigate overfitting, such as dropout and weight decay (L2 regularization). We’re now going to work with a new one, specific to computer vision and used almost universally when processing images with deep-learning models: data augmentation.  Using data augmentation Data augmentation takes the approach of generating more training data from existing training samples, by **augmenting** the samples via a number of random transformations that yield believable-looking images. The goal is that at training time, your model will never see the exact same picture twice. This helps expose the model to more aspects of the data and generalize better. **Define a data augmentation stage to add to an image model** In Keras, this can be done by adding a number of **data augmentation layers** at the start of your model. Let’s get started with an example: here’s a Sequential model that chains several random image transformations. In our model, we’d include it right before the Rescaling layer.<jupyter_code>data_augmentation = keras.Sequential(
    [
        layers.RandomFlip("horizontal"),
        layers.RandomRotation(0.1),
        layers.RandomZoom(0.2),
    ]
)<jupyter_output><empty_output><jupyter_text>These are just a few of the layers available (for more, see the Keras documentation). Let’s quickly go over this code:- `RandomFlip("horizontal")` will apply horizontal flipping to a random 50% of the images that go through it.- `RandomRotation(0.1)` will rotation the input images by a random value in the range $[-10\%, +10\%]$ (these are fractions of a full circle—in degrees the range would be $[-36 degrees, +36 degrees]$).- `RandomZoom(0.2)` will zoom in or out of the image by a random factor in the range $[-20\%, +20\%]$. **Displaying some randomly augmented training images**<jupyter_code>plt.figure(figsize=(10, 10))
for images, _ in train_dataset.take(1):
    for i in range(9):
        augmented_images = data_augmentation(images)
        ax = plt.subplot(3, 3, i + 1)
        plt.imshow(augmented_images[0].numpy().astype("uint8"))
        plt.axis("off")<jupyter_output><empty_output><jupyter_text>**Defining a new convnet that includes image augmentation and dropout** If you train a new model using this **data-augmentation** configuration, the model will never see the same input twice. But the inputs it sees are still **heavily intercorrelated**, because they come from a small number of original images—you can’t produce new information, you can only remix existing information. As such, this may not be enough to completely get rid of overfitting. To further fight overfitting, you’ll also add a `Dropout` layer to your model, right before the densely connected classifier.<jupyter_code>inputs = keras.Input(shape=(180, 180, 3))
x = data_augmentation(inputs)  # New! Fight overfitting with more input data 
x = layers.Rescaling(1./255)(x)
x = layers.Conv2D(filters=32, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=64, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=128, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=256, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=256, kernel_size=3, activation="relu")(x)
x = layers.Flatten()(x)
x = layers.Dropout(0.5)(x)  # New! Fight overfitting by avoid single neurons to dominate
outputs = layers.Dense(1, activation="sigmoid")(x)
model = keras.Model(inputs=inputs, outputs=outputs)

model.compile(loss="binary_crossentropy",
              optimizer="rmsprop",
              metrics=["accuracy"])<jupyter_output><empty_output><jupyter_text>**Training the regularized convnet**<jupyter_code>callbacks = [
    keras.callbacks.ModelCheckpoint(
        filepath="models/dogs-vs-cats/convnet_from_scratch_with_augmentation.keras",
        save_best_only=True,
        monitor="val_loss")
]
history = model.fit(
    train_dataset,
    epochs=100,
    validation_data=validation_dataset,
    callbacks=callbacks)
import matplotlib.pyplot as plt

accuracy = history.history["accuracy"]
val_accuracy = history.history["val_accuracy"]
loss = history.history["loss"]
val_loss = history.history["val_loss"]

epochs = range(1, len(accuracy) + 1)
plt.plot(epochs, accuracy, "bo", label="Training accuracy")
plt.plot(epochs, val_accuracy, "b", label="Validation accuracy")
plt.title("Training and validation accuracy")
plt.legend()

plt.figure()
plt.plot(epochs, loss, "bo", label="Training loss")
plt.plot(epochs, val_loss, "b", label="Validation loss")
plt.title("Training and validation loss")
plt.legend()

plt.show()<jupyter_output><empty_output><jupyter_text>Let’s plot the results again: see figure 8.11. Thanks to data augmentation and dropout, we start overfitting much later, around epochs 60-70 (compared to epoch 10 for the original model). The validation accuracy ends up consistently in the 80-85% range—a big improvement over our first try.![](https://drek4537l1klr.cloudfront.net/chollet2/v-7/Figures/training-and-validation_da.png) **Evaluating the model on the test set**<jupyter_code>test_model = keras.models.load_model(
    "models/dogs-vs-cats/convnet_from_scratch_with_augmentation.keras")
test_loss, test_acc = test_model.evaluate(test_dataset)
print(f"Test accuracy: {test_acc:.3f}")<jupyter_output>63/63 [==============================] - 24s 366ms/step - loss: 0.4160 - accuracy: 0.8235
Test accuracy: 0.823<jupyter_text>By further tuning the model’s configuration (such as the number of filters per convolution layer, or the number of layers in the model), you may be able to get an even better accuracy, likely up to 90%. But it would prove difficult to go any higher just by training your own convnet from scratch, because you have so little data to work with. As a next step to improve your accuracy on this problem, you’ll have to use a pretrained model, which is the focus of the next two sections.  Leveraging a pretrained model A common and highly effective approach to deep learning on small image datasets is to use a **pretrained model**. A pretrained model is a model that was previously trained on a large dataset, typically on a large-scale image-classification task. If this original dataset is large enough and general enough, then the spatial hierarchy of features learned by the pretrained model can effectively act as a generic model of the visual world, and hence its features can prove useful for many different computer-vision problems, even though these new problems may involve completely different classes than those of the original task. For instance, you might train a model on ImageNet (where classes are mostly animals and everyday objects) and then repurpose this trained model for something as remote as identifying furniture items in images. Such portability of learned features across different problems is a key advantage of deep learning compared to many older, shallow-learning approaches, and it makes deep learning very effective for small-data problems. You’ll use the **VGG16** architecture, developed by <NAME> and <NAME> in 2014.[11] Although it’s an older model, far from the current state of the art and somewhat heavier than many other recent models, I chose it because its architecture is similar to what you’re already familiar with and is easy to understand without introducing any new concepts. This may be your first encounter with one of these cutesy model names—VGG, ResNet, Inception, Xception, and so on; you’ll get used to them, because they will come up frequently if you keep doing deep learning for computer vision. There are two ways to use a pretrained model: feature extraction and fine-tuning. We’ll cover both of them. Let’s start with feature extraction.  Feature extraction with a pretrained model Feature extraction consists of using the representations learned by a previously-trained model to extract interesting features from new samples. These features are then run through a new classifier, which is trained from scratch.![](https://drek4537l1klr.cloudfront.net/chollet2/v-7/Figures/swapping_fc_classifier.png) **Instantiating the VGG16 convolutional base** Let’s put this in practice by using the convolutional base of the **VGG16** network, trained on **ImageNet**, to extract interesting features from cat and dog images, and then train a dogs-versus-cats classifier on top of these features.The **VGG16** model, among others, comes prepackaged with Keras. You can import it from the `keras.applications` module. Many other image-classification models (all pretrained on the ImageNet dataset) are available as part of `keras.applications`:- Xception- ResNet- MobileNet- EfficientNet- DenseNetLet’s instantiate the VGG16 model.<jupyter_code>conv_base = keras.applications.vgg16.VGG16(
    weights="imagenet",
    include_top=False,
    input_shape=(180, 180, 3)
)<jupyter_output>Downloading data from https://storage.googleapis.com/tensorflow/keras-applications/vgg16/vgg16_weights_tf_dim_ordering_tf_kernels_notop.h5
58892288/58889256 [==============================] - 9s 0us/step
58900480/58889256 [==============================] - 9s 0us/step<jupyter_text>You pass two arguments to the constructor:- `weights` specifies the weight checkpoint from which to initialize the model.- `include_top` refers to including (or not) the densely connected classifier on top of the network. By default, this densely connected classifier corresponds to the **1,000 classes from ImageNet**. Because you intend to use your own densely connected classifier (with only two classes: cat and dog), you don’t need to include it.- `input_shape` is the shape of the image tensors that you’ll feed to the network. This argument is purely optional: if you don’t pass it, the network will be able to process inputs of any size. Here, we pass so that we can visualize how the size of the feature maps shrink with each new convolution and pooling layer, in the summary below.<jupyter_code>conv_base.summary()<jupyter_output>Model: "vgg16"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_2 (InputLayer)         [(None, 180, 180, 3)]     0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, 180, 180, 64)      1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, 180, 180, 64)      36928     
_________________________________________________________________
block1_pool (MaxPooling2D)   (None, 90, 90, 64)        0         
_________________________________________________________________
block2_conv1 (Conv2D)        (None, 90, 90, 128)       73856     
_________________________________________________________________
block2_conv2 (Conv2D)        (None, 90, 90, 128)       147584    
_____________________________________________________________[...]<jupyter_text>At this point, there are two ways you could proceed:- Running the convolutional base over your dataset, recording its output to a NumPy array on disk, and then using this data as **input to a standalone, densely-connected classifier** similar to those you saw in part 1 of this book. This solution is fast and cheap to run, because it only requires running the convolutional base once for every input image, and the convolutional base is by far the most expensive part of the pipeline. But for the same reason, this technique won’t allow you to use data augmentation.- **Extending the model you have (conv_base) by adding Dense layers on top, and running the whole thing end to end on the input data**. This will allow you to use data augmentation, because every input image goes through the convolutional base every time it’s seen by the model. But for the same reason, this technique is far more expensive than the first.  Fast feature extraction without data augmentation **Extracting the VGG16 features and corresponding labels** Importantly, `predict()` only expects images, not labels, but our current dataset yield batches that contain both images and their labels. Moreover, the **VGG16 model** expects inputs that are preprocessed with the function `keras.applications.vgg16.preprocess_input`, which scales pixel values to an appropriate range.<jupyter_code>import numpy as np

def get_features_and_labels(dataset):
    all_features = []
    all_labels = []
    for images, labels in dataset:
        preprocessed_images = keras.applications.vgg16.preprocess_input(images)
        features = conv_base.predict(preprocessed_images)
        all_features.append(features)
        all_labels.append(labels)
    return np.concatenate(all_features), np.concatenate(all_labels)

train_features, train_labels =  get_features_and_labels(train_dataset)
val_features, val_labels =  get_features_and_labels(validation_dataset)
test_features, test_labels =  get_features_and_labels(test_dataset)
train_dataset.take(1).get_single_element()[0].shape
train_features.shape<jupyter_output><empty_output><jupyter_text>**Defining and training the densely connected classifier** At this point, you can define your densely connected classifier (note the use of dropout for regularization) and train it on the data and labels that you just recorded.Training is very fast, because you only have to deal with two Dense layers—an epoch takes less than one second even on CPU.<jupyter_code>inputs = keras.Input(shape=(5, 5, 512))
x = layers.Flatten()(inputs)  # necessary because we have a convnet that outputs a 3D tensor
x = layers.Dense(256)(x)
x = layers.Dropout(0.5)(x)  # Dropout to fight regularization
outputs = layers.Dense(1, activation="sigmoid")(x)

model = keras.Model(inputs, outputs)
model.compile(loss="binary_crossentropy",
              optimizer="rmsprop",
              metrics=["accuracy"])

callbacks = [
    keras.callbacks.ModelCheckpoint(
      filepath="feature_extraction.keras",
      save_best_only=True,
      monitor="val_loss")
]
history = model.fit(
    train_features, train_labels,
    epochs=20,
    validation_data=(val_features, val_labels),
    callbacks=callbacks)
model.evaluate(test_features, test_labels)<jupyter_output>63/63 [==============================] - 0s 4ms/step - loss: 6.7402 - accuracy: 0.9715<jupyter_text>**Plotting the results**<jupyter_code>import matplotlib.pyplot as plt

acc = history.history["accuracy"]
val_acc = history.history["val_accuracy"]

loss = history.history["loss"]
val_loss = history.history["val_loss"]

epochs = range(1, len(acc) + 1)
plt.plot(epochs, acc, "bo", label="Training accuracy")
plt.plot(epochs, val_acc, "b", label="Validation accuracy")
plt.title("Training and validation accuracy")
plt.legend()

plt.figure()
plt.plot(epochs, loss, "bo", label="Training loss")
plt.plot(epochs, val_loss, "b", label="Validation loss")
plt.title("Training and validation loss")
plt.legend()
plt.show()<jupyter_output><empty_output><jupyter_text>You reach a validation accuracy of about 97%—much better than you achieved in the previous section with the small model trained from scratch. This is a bit of an unfair comparison, however, because ImageNet contains many dogs and cats instances, which means that our pretrained model already has very the exact knowledge required for the task at hand. This won’t always be the case when you use pretrained features.![](https://drek4537l1klr.cloudfront.net/chollet2/v-7/Figures/training-and-validation_fe.png)  Feature extraction together with data augmentation Now, let’s review the second technique I mentioned for doing feature extraction, which is much slower and more expensive, but which allows you to use data augmentation during training: creating a model that chains the conv_base with a new dense classifier, and training it end to end on the inputs.In order to do this, we will first **freeze** the convolutional base. Freezing a layer or set of layers means **preventing their weights from being updated during training**. Here, if you don’t do this, then the representations that were previously learned by the convolutional base will be modified during training. Because the Dense layers on top are randomly initialized, very large weight updates would be propagated through the network, effectively destroying the representations previously learned.In Keras, you freeze a layer or model by setting its `trainable` attribute to False: **Instantiating and freeze the VGG16 convolutional base**<jupyter_code>conv_base  = keras.applications.vgg16.VGG16(
    weights="imagenet",
    include_top=False)
conv_base.trainable = False  # avoid weight updates<jupyter_output><empty_output><jupyter_text>**Printing the list of trainable weights before and after freezing** Trainable weights are the matrices, so we have 26 trainable matrices, that is not the number of trainable parameters!<jupyter_code>conv_base.trainable = True
print("This is the number of trainable"
      "before freezing the conv base:", len(conv_base.trainable_weights))
conv_base.trainable = False
print("This is the number of trainable"
      "after freezing the conv base:", len(conv_base.trainable_weights))<jupyter_output>This is the number of trainableafter freezing the conv base: 0<jupyter_text>**Adding data augmentation and a densely-connected classifier to the frozen convolutional base** Now, we can just create a new model that chains together:- A data augmentation stage- Our frozen convolutional base- A dense classifier<jupyter_code>data_augmentation = keras.Sequential(
    [
        layers.RandomFlip("horizontal"),
        layers.RandomRotation(0.1),
        layers.RandomZoom(0.2),
    ]
)

inputs = keras.Input(shape=(180, 180, 3))
x = data_augmentation(inputs)
x = keras.applications.vgg16.preprocess_input(x)
x = conv_base(x)
x = layers.Flatten()(x)
x = layers.Dense(256)(x)
x = layers.Dropout(0.5)(x)
outputs = layers.Dense(1, activation="sigmoid")(x)

model = keras.Model(inputs, outputs)
model.compile(loss="binary_crossentropy",
              optimizer="rmsprop",
              metrics=["accuracy"])<jupyter_output><empty_output><jupyter_text>With this setup, only the weights from the two Dense layers that you added will be trained. That’s a total of four weight tensors: two per layer (the main weight matrix and the bias vector).<jupyter_code>len(model.trainable_weights)<jupyter_output><empty_output><jupyter_text>**warning**: This technique is expensive enough that you should only attempt it if you have access to a GPU (such as the free GPU available in Colab)—it’s intractable on CPU. If you can’t run your code on GPU, then the previous technique is the way to go.<jupyter_code>callbacks = [
    keras.callbacks.ModelCheckpoint(
        filepath="models/dogs-vs-cats/feature_extraction_with_data_augmentation.keras",
        save_best_only=True,
        monitor="val_loss")
]
history = model.fit(
    train_dataset,
    epochs=20,
    validation_data=validation_dataset,
    callbacks=callbacks)<jupyter_output>Epoch 1/20
63/63 [==============================] - 400s 6s/step - loss: 4.8140 - accuracy: 0.9535 - val_loss: 3.4932 - val_accuracy: 0.9770
Epoch 2/20
63/63 [==============================] - 419s 7s/step - loss: 5.3117 - accuracy: 0.9630 - val_loss: 4.2216 - val_accuracy: 0.9720
Epoch 3/20
63/63 [==============================] - 451s 7s/step - loss: 3.6168 - accuracy: 0.9635 - val_loss: 5.1358 - val_accuracy: 0.9740
Epoch 4/20
63/63 [==============================] - 395s 6s/step - loss: 3.5887 - accuracy: 0.9655 - val_loss: 3.2172 - val_accuracy: 0.9740
Epoch 5/20
63/63 [==============================] - 346s 6s/step - loss: 3.0567 - accuracy: 0.9725 - val_loss: 4.5727 - val_accuracy: 0.9730
Epoch 6/20
63/63 [==============================] - 406s 6s/step - loss: 2.7913 - accuracy: 0.9710 - val_loss: 3.7928 - val_accuracy: 0.9750
Epoch 7/20
63/63 [==============================] - 461s 7s/step - loss: 2.2091 - accuracy: 0.9760 - val_loss: 3.6959 - val_accuracy: 0.9790
Epoch 8/20
63[...]<jupyter_text>**Evaluating the model on the test set**<jupyter_code>import matplotlib.pyplot as plt

acc = history.history["accuracy"]
val_acc = history.history["val_accuracy"]

loss = history.history["loss"]
val_loss = history.history["val_loss"]

epochs = range(1, len(acc) + 1)
plt.plot(epochs, acc, "bo", label="Training accuracy")
plt.plot(epochs, val_acc, "b", label="Validation accuracy")
plt.title("Training and validation accuracy")
plt.legend()

plt.figure()
plt.plot(epochs, loss, "bo", label="Training loss")
plt.plot(epochs, val_loss, "b", label="Validation loss")
plt.title("Training and validation loss")
plt.legend()
plt.show()<jupyter_output><empty_output><jupyter_text>![](https://drek4537l1klr.cloudfront.net/chollet2/v-7/Figures/training-and-validation_da_fe.png)<jupyter_code>test_model = keras.models.load_model(
    "models/dogs-vs-cats/feature_extraction_with_data_augmentation.keras")
test_loss, test_acc = test_model.evaluate(test_dataset)
print(f"Test accuracy: {test_acc:.3f}")<jupyter_output>63/63 [==============================] - 255s 4s/step - loss: 2.5994 - accuracy: 0.9740
Test accuracy: 0.974<jupyter_text>We get a test accuracy of 97.5%. This is only a modest improvement compared to the previous test accuracy, which is a bit disappointing given the strong results on the validation data. A model’s accuracy always depends on the set of samples you evaluate it on! Some sample sets may be more difficult than others, and strong results on one set won’t necessarily fully translate to all other sets.  Fine-tuning a pretrained model Another widely used technique for model reuse, complementary to feature extraction, is **fine-tuning** (see figure). Fine-tuning consists of **unfreezing a few of the top layers** of a frozen model base used for feature extraction, and jointly training both the newly added part of the model (in this case, the fully connected classifier) and these top layers. This is called fine-tuning because it slightly adjusts the more abstract representations of the model being reused, in order to make them more relevant for the problem at hand.![](https://drek4537l1klr.cloudfront.net/chollet2/v-7/Figures/vgg16_fine_tuning.png) I stated earlier that it’s necessary to freeze the convolution base of VGG16 in order to be able to train a randomly initialized classifier on top. For the same reason, **it’s only possible to fine-tune the top layers of the convolutional base once the classifier on top has already been trained**. If the classifier isn’t already trained, then the error signal propagating through the network during training will be too large, and the representations previously learned by the layers being fine-tuned will be destroyed. Thus the steps for fine-tuning a network are as follow:1. Add your custom network on top of an already-trained base network.2. Freeze the base network.3. Train the part you added.4. Unfreeze some layers in the base network. (Note that you should not unfreeze "batch normalization" layers, which is not relevant here since there are no such layers in VGG16. Batch normalization and its impact on fine-tuning is explained in the next chapter.)5. Jointly train both these layers and the part you added.<jupyter_code>conv_base.summary()<jupyter_output><empty_output><jupyter_text>**Freezing all layers until the 4th from the last**<jupyter_code>conv_base.trainable = True
for layer in conv_base.layers[:-4]:
    layer.trainable = False<jupyter_output><empty_output><jupyter_text>**Fine-tuning the model**<jupyter_code>model.compile(loss="binary_crossentropy",
              optimizer=keras.optimizers.RMSprop(learning_rate=1e-5),
              metrics=["accuracy"])

callbacks = [
    keras.callbacks.ModelCheckpoint(
        filepath="fine_tuning.keras",
        save_best_only=True,
        monitor="val_loss")
]
history = model.fit(
    train_dataset,
    epochs=30,
    validation_data=validation_dataset,
    callbacks=callbacks)
model = keras.models.load_model("fine_tuning.keras")
test_loss, test_acc = model.evaluate(test_dataset)
print(f"Test accuracy: {test_acc:.3f}")<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09850852907028669, 'fraction_numerical': 0.0312940966330267, 'mean_word_length': 5.016765180010747, 'pattern_counts': {'":': 0, '<': 138, '<?xml version=': 0, '>': 141, 'https://': 16, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 5, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '38', 'dir': 'jupyter-structured-clean-dedup', 'id': '542841', 'license': 'MIT', 'n_tokens_mistral': 16727, 'n_tokens_neox': 15134, 'n_words': 6538, 'path': 'chapter08_intro-to-dl-for-computer-vision.ipynb', 'repo_name': 'pmayd/deep-learning-with-python-notebooks', 'size': '56008'}