Deep Learning With Python - T2
Deep Learning With Python - T2
François Chollet
MANNING
Deep Learning with Python
Deep Learning
with Python
SECOND EDITION
FRANÇOIS CHOLLET
MANNING
SHELTER ISLAND
For online information and ordering of this and other Manning books, please visit
[Link]. The publisher offers discounts on this book when ordered in quantity.
For more information, please contact
Special Sales Department
Manning Publications Co.
20 Baldwin Road
PO Box 761
Shelter Island, NY 11964
Email: orders@[Link]
Many of the designations used by manufacturers and sellers to distinguish their products are
claimed as trademarks. Where those designations appear in the book, and Manning Publications
was aware of a trademark claim, the designations have been printed in initial caps or all caps.
Recognizing the importance of preserving what has been written, it is Manning’s policy to have
the books we publish printed on acid-free paper, and we exert our best efforts to that end.
Recognizing also our responsibility to conserve the resources of our planet, Manning books
are printed on paper that is at least 15 percent recycled and processed without the use of
elemental chlorine.
The author and publisher have made every effort to ensure that the information in this book
was correct at press time. The author and publisher do not assume and hereby disclaim any
liability to any party for any loss, damage, or disruption caused by errors or omissions, whether
such errors or omissions result from negligence, accident, or any other cause, or from any usage
of the information herein.
ISBN: 9781617296864
Printed in the United States of America
To my son Sylvain: I hope you’ll read this book someday!
brief contents
1 ■ What is deep learning? 1
2 ■ The mathematical building blocks
of neural networks 26
3 ■ Introduction to Keras and TensorFlow 68
4 ■ Getting started with neural networks: Classification
and regression 95
5 ■ Fundamentals of machine learning 121
6 ■ The universal workflow of machine learning 153
7 ■ Working with Keras: A deep dive 172
8 ■ Introduction to deep learning
for computer vision 201
9 ■ Advanced deep learning for computer vision 238
10 ■ Deep learning for timeseries 280
11 ■ Deep learning for text 309
12 ■ Generative deep learning 364
13 ■ Best practices for the real world 412
14 ■ Conclusions 431
vii
contents
preface xvii
acknowledgments xix
about this book xx
about the author xxiii
about the cover illustration xxiv
learning landscape 18
ix
x CONTENTS
Will it last? 24
TensorFlow 79
CONTENTS xi
Wrapping up 113
4.3 Predicting house prices: A regression example 113
The Boston housing price dataset 114 Preparing the data 114 ■
evaluation 137
5.3 Improving model fit 138
Tuning key gradient descent parameters 138 Leveraging better ■
components 184 Remember: Use the right tool for the job 185
■
operation 209
8.2 Training a convnet from scratch on a small dataset 211
The relevance of deep learning for small-data problems 212
Downloading the data 212 Building the model 215 ■
baseline 292
10.3 Understanding recurrent neural networks 293
A recurrent layer in Keras 296
10.4 Advanced use of recurrent neural networks 300
Using recurrent dropout to fight overfitting 300 Stacking ■
Transformer 358
413 ■
Model ensembling
413
420
13.2 Scaling-up model training 421
Speeding up training on GPU with mixed precision 422
Multi-GPU training 425 TPU training 428 ■
CONTENTS xv
14 Conclusions
14.1
431
Key concepts in review 432
Various approaches to AI 432 What makes deep learning special
■
ecosystem 467
14.7 Final words 467
index 469
preface
If you’ve picked up this book, you’re probably aware of the extraordinary progress
that deep learning has represented for the field of artificial intelligence in the recent
past. We went from near-unusable computer vision and natural language processing
to highly performant systems deployed at scale in products you use every day. The
consequences of this sudden progress extend to almost every industry. We’re already
applying deep learning to an amazing range of important problems across domains as
different as medical imaging, agriculture, autonomous driving, education, disaster
prevention, and manufacturing.
Yet, I believe deep learning is still in its early days. It has only realized a small frac-
tion of its potential so far. Over time, it will make its way to every problem where it can
help—a transformation that will take place over multiple decades.
In order to begin deploying deep learning technology to every problem that it
could solve, we need to make it accessible to as many people as possible, including
non-experts—people who aren’t researchers or graduate students. For deep learning to
reach its full potential, we need to radically democratize it. And today, I believe that
we’re at the cusp of a historical transition, where deep learning is moving out of aca-
demic labs and the R&D departments of large tech companies to become a ubiquitous
part of the toolbox of every developer out there—not unlike the trajectory of web
development in the late 1990s. Almost anyone can now build a website or web app for
their business or community of a kind that would have required a small team of special-
ist engineers in 1998. In the not-so-distant future, anyone with an idea and basic coding
skills will be able to build smart applications that learn from data.
xvii
xviii PREFACE
When I released the first version of the Keras deep learning framework in March
2015, the democratization of AI wasn’t what I had in mind. I had been doing research
in machine learning for several years and had built Keras to help me with my own
experiments. But since 2015, hundreds of thousands of newcomers have entered the
field of deep learning; many of them picked up Keras as their tool of choice. As I
watched scores of smart people use Keras in unexpected, powerful ways, I came to
care deeply about the accessibility and democratization of AI. I realized that the fur-
ther we spread these technologies, the more useful and valuable they become. Accessi-
bility quickly became an explicit goal in the development of Keras, and over a few
short years, the Keras developer community has made fantastic achievements on this
front. We’ve put deep learning into the hands of hundreds of thousands of people,
who in turn are using it to solve problems that were until recently thought to be
unsolvable.
The book you’re holding is another step on the way to making deep learning avail-
able to as many people as possible. Keras had always needed a companion course to
simultaneously cover the fundamentals of deep learning, deep learning best practices,
and Keras usage patterns. In 2016 and 2017, I did my best to produce such a course,
which became the first edition of this book, released in December 2017. It quickly
became a machine learning best seller that sold over 50,000 copies and was translated
into 12 languages.
However, the field of deep learning advances fast. Since the release of the first edi-
tion, many important developments have taken place—the release of TensorFlow 2,
the growing popularity of the Transformer architecture, and more. And so, in late
2019, I set out to update my book. I originally thought, quite naively, that it would fea-
ture about 50% new content and would end up being roughly the same length as the
first edition. In practice, after two years of work, it turned out to be over a third lon-
ger, with about 75% novel content. More than a refresh, it is a whole new book.
I wrote it with a focus on making the concepts behind deep learning, and their
implementation, as approachable as possible. Doing so didn’t require me to dumb
down anything—I strongly believe that there are no difficult ideas in deep learning. I
hope you’ll find this book valuable and that it will enable you to begin building intelli-
gent applications and solve the problems that matter to you.
acknowledgments
First of all, I’d like to thank the Keras community for making this book possible. Over
the past six years, Keras has grown to have hundreds of open source contributors and
more than one million users. Your contributions and feedback have turned Keras into
what it is today.
On a more personal note, I’d like to thank my wife for her endless support during
the development of Keras and the writing of this book.
I’d also like to thank Google for backing the Keras project. It has been fantastic to
see Keras adopted as TensorFlow’s high-level API. A smooth integration between
Keras and TensorFlow greatly benefits both TensorFlow users and Keras users, and
makes deep learning accessible to most.
I want to thank the people at Manning who made this book possible: publisher
Marjan Bace and everyone on the editorial and production teams, including Michael
Stephens, Jennifer Stout, Aleksandar Dragosavljević, and many others who worked
behind the scenes.
Many thanks go to the technical peer reviewers: Billy O’Callaghan, Christian
Weisstanner, Conrad Taylor, Daniela Zapata Riesco, David Jacobs, Edmon Begoli,
Edmund Ronald PhD, Hao Liu, Jared Duncan, Kee Nam, Ken Fricklas, Kjell Jansson,
Milan Šarenac, Nguyen Cao, Nikos Kanakaris, Oliver Korten, Raushan Jha, Sayak Paul,
Sergio Govoni, Shashank Polasa, Todd Cook, and Viton Vitanis—and all the other
people who sent us feedback on the draft on the book.
On the technical side, special thanks go to Frances Buontempo, who served as the
book’s technical editor, and Karsten Strøbæk, who served as the book’s technical
proofreader.
xix
about this book
This book was written for anyone who wishes to explore deep learning from scratch or
broaden their understanding of deep learning. Whether you’re a practicing machine
learning engineer, a software developer, or a college student, you’ll find value in
these pages.
You’ll explore deep learning in an approachable way—starting simply, then work-
ing up to state-of-the-art techniques. You’ll find that this book strikes a balance between
intuition, theory, and hands-on practice. It avoids mathematical notation, preferring
instead to explain the core ideas of machine learning and deep learning via detailed
code snippets and intuitive mental models. You’ll learn from abundant code examples
that include extensive commentary, practical recommendations, and simple high-level
explanations of everything you need to know to start using deep learning to solve con-
crete problems.
The code examples use the Python deep learning framework Keras, with Tensor-
Flow 2 as its numerical engine. They demonstrate modern Keras and TensorFlow 2
best practices as of 2021.
After reading this book, you’ll have a solid understand of what deep learning is,
when it’s applicable, and what its limitations are. You’ll be familiar with the standard
workflow for approaching and solving machine learning problems, and you’ll know
how to address commonly encountered issues. You’ll be able to use Keras to tackle
real-world problems ranging from computer vision to natural language processing:
image classification, image segmentation, timeseries forecasting, text classification,
machine translation, text generation, and more.
xx
ABOUT THIS BOOK xxi
book, ask technical questions, and receive help from the author and from other users.
To access the forum, go to [Link]
with-python-second-edition/discussion. You can also learn more about Manning’s
forums and the rules of conduct at [Link]
Manning’s commitment to our readers is to provide a venue where a meaningful
dialogue between individual readers and between readers and the author can take
place. It is not a commitment to any specific amount of participation on the part of
the author, whose contribution to the forum remains voluntary (and unpaid). We sug-
gest you try asking the author some challenging questions lest his interest stray! The
forum and the archives of previous discussions will be accessible from the publisher’s
website as long as the book is in print.
about the author
FRANÇOIS CHOLLET is the creator of Keras, one of the most
widely used deep learning frameworks. He is currently a soft-
ware engineer at Google, where he leads the Keras team. In
addition, he does research on abstraction, reasoning, and how
to achieve greater generality in artificial intelligence.
xxiii
about the cover illustration
The figure on the cover of Deep Learning with Python, second edition, is captioned
“Habit of a Persian Lady in 1568.” The illustration is taken from Thomas Jefferys’ A
Collection of the Dresses of Different Nations, Ancient and Modern (four volumes), London,
published between 1757 and 1772. The title page states that these are hand-colored
copperplate engravings, heightened with gum arabic.
Thomas Jefferys (1719–1771) was called “Geographer to King George III.” He was
an English cartographer who was the leading map supplier of his day. He engraved
and printed maps for government and other official bodies and produced a wide
range of commercial maps and atlases, especially of North America. His work as a map
maker sparked an interest in local dress customs of the lands he surveyed and
mapped, which are brilliantly displayed in this collection. Fascination with faraway
lands and travel for pleasure were relatively new phenomena in the late eighteenth
century, and collections such as this one were popular, introducing both the tourist as
well as the armchair traveler to the inhabitants of other countries.
The diversity of the drawings in Jefferys’ volumes speaks vividly of the uniqueness
and individuality of the world’s nations some 200 years ago. Dress codes have changed
since then, and the diversity by region and country, so rich at the time, has faded away.
It’s now often hard to tell the inhabitants of one continent from another. Perhaps, try-
ing to view it optimistically, we’ve traded a cultural and visual diversity for a more varied
personal life—or a more varied and interesting intellectual and technical life.
At a time when it’s difficult to tell one computer book from another, Manning cel-
ebrates the inventiveness and initiative of the computer business with book covers
based on the rich diversity of regional life of two centuries ago, brought back to life by
Jefferys’ pictures.
xxiv
What is deep learning?
In the past few years, artificial intelligence (AI) has been a subject of intense media
hype. Machine learning, deep learning, and AI come up in countless articles, often
outside of technology-minded publications. We’re promised a future of intelligent
chatbots, self-driving cars, and virtual assistants—a future sometimes painted in a
grim light and other times as utopian, where human jobs will be scarce and most
economic activity will be handled by robots or AI agents. For a future or current
practitioner of machine learning, it’s important to be able to recognize the signal
amid the noise, so that you can tell world-changing developments from overhyped
press releases. Our future is at stake, and it’s a future in which you have an active
role to play: after reading this book, you’ll be one of those who develop those AI
systems. So let’s tackle these questions: What has deep learning achieved so far?
How significant is it? Where are we headed next? Should you believe the hype?
This chapter provides essential context around artificial intelligence, machine
learning, and deep learning.
1
2 CHAPTER 1 What is deep learning?
Artificial
intelligence
Machine
learning
Deep
learning
The study is to proceed on the basis of the conjecture that every aspect of learning or any
other feature of intelligence can in principle be so precisely described that a machine can
be made to simulate it. An attempt will be made to find how to make machines use
language, form abstractions and concepts, solve kinds of problems now reserved for
humans, and improve themselves. We think that a significant advance can be made in
one or more of these problems if a carefully selected group of scientists work on it together
for a summer.
At the end of the summer, the workshop concluded without having fully solved the
riddle it set out to investigate. Nevertheless, it was attended by many people who
would move on to become pioneers in the field, and it set in motion an intellectual
revolution that is still ongoing to this day.
Concisely, AI can be described as the effort to automate intellectual tasks normally per-
formed by humans. As such, AI is a general field that encompasses machine learning and
deep learning, but that also includes many more approaches that may not involve any
learning. Consider that until the 1980s, most AI textbooks didn’t mention “learning” at
Artificial intelligence, machine learning, and deep learning 3
all! Early chess programs, for instance, only involved hardcoded rules crafted by pro-
grammers, and didn’t qualify as machine learning. In fact, for a fairly long time, most
experts believed that human-level artificial intelligence could be achieved by having
programmers handcraft a sufficiently large set of explicit rules for manipulating
knowledge stored in explicit databases. This approach is known as symbolic AI. It was
the dominant paradigm in AI from the 1950s to the late 1980s, and it reached its peak
popularity during the expert systems boom of the 1980s.
Although symbolic AI proved suitable to solve well-defined, logical problems,
such as playing chess, it turned out to be intractable to figure out explicit rules for
solving more complex, fuzzy problems, such as image classification, speech recogni-
tion, or natural language translation. A new approach arose to take symbolic AI’s place:
machine learning.
Even with 178 years of historical perspective, Lady Lovelace’s observation remains
arresting. Could a general-purpose computer “originate” anything, or would it always be
bound to dully execute processes we humans fully understand? Could it ever be capable
of any original thought? Could it learn from experience? Could it show creativity?
Her remark was later quoted by AI pioneer Alan Turing as “Lady Lovelace’s objec-
tion” in his landmark 1950 paper “Computing Machinery and Intelligence,” 1 which
introduced the Turing test as well as key concepts that would come to shape AI.2 Turing
1
A.M. Turing, “Computing Machinery and Intelligence,” Mind 59, no. 236 (1950): 433–460.
2
Although the Turing test has sometimes been interpreted as a literal test—a goal the field of AI should set out to
reach—Turing merely meant it as a conceptual device in a philosophical discussion about the nature of cognition.
4 CHAPTER 1 What is deep learning?
Rules Classical
Answers
Data programming
Data Machine
Rules Figure 1.2 Machine learning:
Answers learning
a new programming paradigm
Although machine learning only started to flourish in the 1990s, it has quickly
become the most popular and most successful subfield of AI, a trend driven by the
availability of faster hardware and larger datasets. Machine learning is related to math-
ematical statistics, but it differs from statistics in several important ways, in the same
sense that medicine is related to chemistry but cannot be reduced to chemistry, as
medicine deals with its own distinct systems with their own distinct properties. Unlike
statistics, machine learning tends to deal with large, complex datasets (such as a data-
set of millions of images, each consisting of tens of thousands of pixels) for which clas-
sical statistical analysis such as Bayesian analysis would be impractical. As a result,
machine learning, and especially deep learning, exhibits comparatively little mathe-
matical theory—maybe too little—and is fundamentally an engineering discipline.
Unlike theoretical physics or mathematics, machine learning is a very hands-on field
driven by empirical findings and deeply reliant on advances in software and hardware.
data processing task, given examples of what’s expected. So, to do machine learning,
we need three things:
Input data points—For instance, if the task is speech recognition, these data
points could be sound files of people speaking. If the task is image tagging, they
could be pictures.
Examples of the expected output—In a speech-recognition task, these could be
human-generated transcripts of sound files. In an image task, expected outputs
could be tags such as “dog,” “cat,” and so on.
A way to measure whether the algorithm is doing a good job—This is necessary in
order to determine the distance between the algorithm’s current output and its
expected output. The measurement is used as a feedback signal to adjust the
way the algorithm works. This adjustment step is what we call learning.
A machine learning model transforms its input data into meaningful outputs, a pro-
cess that is “learned” from exposure to known examples of inputs and outputs. There-
fore, the central problem in machine learning and deep learning is to meaningfully
transform data: in other words, to learn useful representations of the input data at
hand—representations that get us closer to the expected output.
Before we go any further: what’s a representation? At its core, it’s a different way to
look at data—to represent or encode data. For instance, a color image can be encoded
in the RGB format (red-green-blue) or in the HSV format (hue-saturation-value):
these are two different representations of the same data. Some tasks that may be diffi-
cult with one representation can become easy with another. For example, the task
“select all red pixels in the image” is simpler in the RGB format, whereas “make the
image less saturated” is simpler in the HSV format. Machine learning models are all
about finding appropriate representations for their input data—transformations of
the data that make it more amenable to the task at hand.
Let’s make this concrete. Consider an x-axis, a y-axis, and y
some points represented by their coordinates in the (x, y) sys-
tem, as shown in figure 1.3.
As you can see, we have a few white points and a few black
points. Let’s say we want to develop an algorithm that can take
the coordinates (x, y) of a point and output whether that
point is likely to be black or to be white. In this case,
x
The inputs are the coordinates of our points.
The expected outputs are the colors of our points.
Figure 1.3 Some
A way to measure whether our algorithm is doing a good sample data
job could be, for instance, the percentage of points that
are being correctly classified.
What we need here is a new representation of our data that cleanly separates the white
points from the black points. One transformation we could use, among many other
possibilities, would be a coordinate change, illustrated in figure 1.4.
6 CHAPTER 1 What is deep learning?
x x
In this new coordinate system, the coordinates of our points can be said to be a new
representation of our data. And it’s a good one! With this representation, the
black/white classification problem can be expressed as a simple rule: “Black points
are such that x > 0,” or “White points are such that x < 0.” This new representation,
combined with this simple rule, neatly solves the classification problem.
In this case we defined the coordinate change by hand: we used our human intelli-
gence to come up with our own appropriate representation of the data. This is fine for
such an extremely simple problem, but could you do the same if the task were to clas-
sify images of handwritten digits? Could you write down explicit, computer-executable
image transformations that would illuminate the difference between a 6 and an 8,
between a 1 and a 7, across all kinds of different handwriting?
This is possible to an extent. Rules based on representations of digits such as
“number of closed loops” or vertical and horizontal pixel histograms can do a decent
job of telling apart handwritten digits. But finding such useful representations by
hand is hard work, and, as you can imagine, the resulting rule-based system is brittle—
a nightmare to maintain. Every time you come across a new example of handwriting
that breaks your carefully thought-out rules, you will have to add new data transfor-
mations and new rules, while taking into account their interaction with every previ-
ous rule.
You’re probably thinking, if this process is so painful, could we automate it? What
if we tried systematically searching for different sets of automatically generated repre-
sentations of the data and rules based on them, identifying good ones by using as
feedback the percentage of digits being correctly classified in some development data-
set? We would then be doing machine learning. Learning, in the context of machine
learning, describes an automatic search process for data transformations that produce
useful representations of some data, guided by some feedback signal—representa-
tions that are amenable to simpler rules solving the task at hand.
These transformations can be coordinate changes (like in our 2D coordinates
classification example), or taking a histogram of pixels and counting loops (like in
our digits classification example), but they could also be linear projections, transla-
tions, nonlinear operations (such as “select all points such that x > 0”), and so on.
Artificial intelligence, machine learning, and deep learning 7
As you can see in figure 1.6, the network transforms the digit image into representa-
tions that are increasingly different from the original image and increasingly informa-
tive about the final result. You can think of a deep network as a multistage information-
distillation process, where information goes through successive filters and comes out
increasingly purified (that is, useful with regard to some task).
Layer 4
representations
(final output)
Original 0
1
input
2
3
4
5
6
7
8
9
Layer 1 Layer 2 Layer 3 Layer 4
So that’s what deep learning is, technically: a multistage way to learn data representa-
tions. It’s a simple idea—but, as it turns out, very simple mechanisms, sufficiently
scaled, can end up looking like magic.
data transformations are learned by exposure to examples. Now let’s look at how this
learning happens, concretely.
The specification of what a layer does to its input data is stored in the layer’s
weights, which in essence are a bunch of numbers. In technical terms, we’d say that the
transformation implemented by a layer is parameterized by its weights (see figure 1.7).
(Weights are also sometimes called the parameters of a layer.) In this context, learning
means finding a set of values for the weights of all layers in a network, such that the
network will correctly map example inputs to their associated targets. But here’s the
thing: a deep neural network can contain tens of millions of parameters. Finding the
correct values for all of them may seem like a daunting task, especially given that mod-
ifying the value of one parameter will affect the behavior of all the others!
Input X
Layer
Weights
Goal: finding the (data transformation)
right values for
these weights
Layer
Weights
(data transformation)
To control something, first you need to be able to observe it. To control the output of
a neural network, you need to be able to measure how far this output is from what you
expected. This is the job of the loss function of the network, also sometimes called the
objective function or cost function. The loss function takes the predictions of the network
and the true target (what you wanted the network to output) and computes a distance
score, capturing how well the network has done on this specific example (see figure 1.8).
Input X
Layer
Weights
(data transformation)
Layer
Weights
(data transformation)
Loss function
The fundamental trick in deep learning is to use this score as a feedback signal to
adjust the value of the weights a little, in a direction that will lower the loss score for
the current example (see figure 1.9). This adjustment is the job of the optimizer, which
implements what’s called the Backpropagation algorithm: the central algorithm in deep
learning. The next chapter explains in more detail how backpropagation works.
Input X
Layer
Weights
(data transformation)
Layer
Weights
(data transformation)
Initially, the weights of the network are assigned random values, so the network
merely implements a series of random transformations. Naturally, its output is far
from what it should ideally be, and the loss score is accordingly very high. But with
every example the network processes, the weights are adjusted a little in the correct
direction, and the loss score decreases. This is the training loop, which, repeated a suffi-
cient number of times (typically tens of iterations over thousands of examples), yields
weight values that minimize the loss function. A network with a minimal loss is one for
which the outputs are as close as they can be to the targets: a trained network. Once
again, it’s a simple mechanism that, once scaled, ends up looking like magic.
away from the field, marking the start of the first AI winter (a reference to a nuclear win-
ter, because this was shortly after the height of the Cold War).
It wouldn’t be the last one. In the 1980s, a new take on symbolic AI, expert systems,
started gathering steam among large companies. A few initial success stories triggered
a wave of investment, with corporations around the world starting their own in-house
AI departments to develop expert systems. Around 1985, companies were spending
over $1 billion each year on the technology; but by the early 1990s, these systems had
proven expensive to maintain, difficult to scale, and limited in scope, and interest
died down. Thus began the second AI winter.
We may be currently witnessing the third cycle of AI hype and disappointment,
and we’re still in the phase of intense optimism. It’s best to moderate our expectations
for the short term and make sure people less familiar with the technical side of the
field have a clear idea of what deep learning can and can’t deliver.
On the way, we may face a few setbacks and maybe even a new AI winter—in much
the same way the internet industry was overhyped in 1998–99 and suffered from a
crash that dried up investment throughout the early 2000s. But we’ll get there eventu-
ally. AI will end up being applied to nearly every process that makes up our society and
our daily lives, much like the internet is today.
Don’t believe the short-term hype, but do believe in the long-term vision. It may
take a while for AI to be deployed to its true potential—a potential the full extent of
which no one has yet dared to dream—but AI is coming, and it will transform our
world in a fantastic way.
like Naive Bayes, logreg predates computing by a long time, yet it’s still useful to this
day, thanks to its simple and versatile nature. It’s often the first thing a data scientist
will try on a dataset to get a feel for the classification task at hand.
3
Vladimir Vapnik and Corinna Cortes, “Support-Vector Networks,” Machine Learning 20, no. 3 (1995): 273–297.
4
Vladimir Vapnik and Alexey Chervonenkis, “A Note on One Class of Perceptrons,” Automation and Remote Con-
trol 25 (1964).
Before deep learning: A brief history of machine learning 15
step called maximizing the margin. This allows the boundary to generalize well to
new samples outside of the training dataset.
The technique of mapping data to a high-dimensional representation where a classifi-
cation problem becomes simpler may look good on paper, but in practice it’s often
computationally intractable. That’s where the kernel trick comes in (the key idea that
kernel methods are named after). Here’s the gist of it: to find good decision hyper-
planes in the new representation space, you don’t have to explicitly compute the coor-
dinates of your points in the new space; you just need to compute the distance
between pairs of points in that space, which can be done efficiently using a kernel
function. A kernel function is a computationally tractable operation that maps any two
points in your initial space to the distance between these points in your target repre-
sentation space, completely bypassing the explicit computation of the new representa-
tion. Kernel functions are typically crafted by hand rather than learned from data—in
the case of an SVM, only the separation hyperplane is learned.
At the time they were developed, SVMs exhibited state-of-the-art performance on
simple classification problems and were one of the few machine learning methods
backed by extensive theory and amenable to serious mathematical analysis, making
them well understood and easily interpretable. Because of these useful properties,
SVMs became extremely popular in the field for a long time.
But SVMs proved hard to scale to large datasets and didn’t provide good results for
perceptual problems such as image classification. Because an SVM is a shallow
method, applying an SVM to perceptual problems requires first extracting useful rep-
resentations manually (a step called feature engineering), which is difficult and brittle.
For instance, if you want to use an SVM to classify handwritten digits, you can’t start
from the raw pixels; you should first find by hand useful representations that make
the problem more tractable, like the pixel histograms I mentioned earlier.
Input data
Question
5
“Top-five accuracy” measures how often the model selects the correct answer as part of its top five guesses (out
of 1,000 possible answers, in the case of ImageNet).
Before deep learning: A brief history of machine learning 17
tasks. At any major computer vision conference after 2015, it was nearly impossible to
find presentations that didn’t involve convnets in some form. At the same time, deep
learning has also found applications in many other types of problems, such as natural
language processing. It has completely replaced SVMs and decision trees in a wide
range of applications. For instance, for several years, the European Organization for
Nuclear Research, CERN, used decision tree–based methods for analyzing particle
data from the ATLAS detector at the Large Hadron Collider (LHC), but CERN even-
tually switched to Keras-based deep neural networks due to their higher performance
and ease of training on large datasets.
and the fact that these intermediate incremental representations are learned jointly, each layer
being updated to follow both the representational needs of the layer above and the
needs of the layer below. Together, these two properties have made deep learning
vastly more successful than previous approaches to machine learning.
Keras
LightGBM
XGBoost
PyTorch
TensorFlow
Scikit-learn
Fastai
Caffe
0 10 20 30 40
Number of competitions
Deep Classic
It’s not just competition champions, either. Kaggle also runs a yearly survey among
machine learning and data science professionals worldwide. With tens of thousands of
respondents, this survey is one of our most reliable sources about the state of the
industry. Figure 1.13 shows the percentage of usage of different machine learning
software frameworks.
Scikit-learn 82.8%
TensorFlow 50.5%
Keras 50.5%
Xgboost 48.4%
PyTorch 30.9%
LightGBM 26.1%
Caret 14.1%
Catboost 13.7%
Prophet 10%
[Link] 7.5%
Tidymodels 7.2%
H2O3 6%
MXNet 2.1%
Other 3.7%
None 3.2%
JAX 0.7%
0% 10% 20% 30% 40% 50% 60% 70% 80% 90% 100%
Figure 1.13 Tool usage across the machine learning and data science industry (Source: [Link]/
kaggle-survey-2020)
From 2016 to 2020, the entire machine learning and data science industry has been
dominated by these two approaches: deep learning and gradient boosted trees. Specif-
ically, gradient boosted trees is used for problems where structured data is available,
whereas deep learning is used for perceptual problems such as image classification.
Users of gradient boosted trees tend to use Scikit-learn, XGBoost, or LightGBM.
Meanwhile, most practitioners of deep learning use Keras, often in combination with
20 CHAPTER 1 What is deep learning?
its parent framework TensorFlow. The common point of these tools is they’re all
Python libraries: Python is by far the most widely used language for machine learning
and data science.
These are the two techniques you should be the most familiar with in order to be
successful in applied machine learning today: gradient boosted trees, for shallow-
learning problems; and deep learning, for perceptual problems. In technical terms,
this means you’ll need to be familiar with Scikit-learn, XGBoost, and Keras—the three
libraries that currently dominate Kaggle competitions. With this book in hand, you’re
already one big step closer.
1.3.1 Hardware
Between 1990 and 2010, off-the-shelf CPUs became faster by a factor of approximately
5,000. As a result, nowadays it’s possible to run small deep learning models on your
laptop, whereas this would have been intractable 25 years ago.
But typical deep learning models used in computer vision or speech recognition
require orders of magnitude more computational power than your laptop can deliver.
Throughout the 2000s, companies like NVIDIA and AMD invested billions of dollars
in developing fast, massively parallel chips (graphical processing units, or GPUs) to
power the graphics of increasingly photorealistic video games—cheap, single-purpose
supercomputers designed to render complex 3D scenes on your screen in real time.
This investment came to benefit the scientific community when, in 2007, NVIDIA
launched CUDA ([Link] a programming interface
Why deep learning? Why now? 21
for its line of GPUs. A small number of GPUs started replacing massive clusters of
CPUs in various highly parallelizable applications, beginning with physics modeling.
Deep neural networks, consisting mostly of many small matrix multiplications, are
also highly parallelizable, and around 2011 some researchers began to write CUDA
implementations of neural nets—Dan Ciresan6 and Alex Krizhevsky7 were among
the first.
What happened is that the gaming market subsidized supercomputing for the next
generation of artificial intelligence applications. Sometimes, big things begin as
games. Today, the NVIDIA Titan RTX, a GPU that cost $2,500 at the end of 2019, can
deliver a peak of 16 teraFLOPS in single precision (16 trillion float32 operations per
second). That’s about 500 times more computing power than the world’s fastest super-
computer from 1990, the Intel Touchstone Delta. On a Titan RTX, it takes only a few
hours to train an ImageNet model of the sort that would have won the ILSVRC com-
petition around 2012 or 2013. Meanwhile, large companies train deep learning mod-
els on clusters of hundreds of GPUs.
What’s more, the deep learning industry has been moving beyond GPUs and is
investing in increasingly specialized, efficient chips for deep learning. In 2016, at its
annual I/O convention, Google revealed its Tensor Processing Unit (TPU) project: a
new chip design developed from the ground up to run deep neural networks signifi-
cantly faster and far more energy efficient than top-of-the-line GPUs. Today, in 2020,
the third iteration of the TPU card represents 420 teraFLOPS of computing power.
That’s 10,000 times more than the Intel Touchstone Delta from 1990.
These TPU cards are designed to be assembled into large-scale configurations,
called “pods.” One pod (1024 TPU cards) peaks at 100 petaFLOPS. For scale, that’s
about 10% of the peak computing power of the current largest supercomputer, the
IBM Summit at Oak Ridge National Lab, which consists of 27,000 NVIDIA GPUs and
peaks at around 1.1 exaFLOPS.
1.3.2 Data
AI is sometimes heralded as the new industrial revolution. If deep learning is the steam
engine of this revolution, then data is its coal: the raw material that powers our intelli-
gent machines, without which nothing would be possible. When it comes to data, in
addition to the exponential progress in storage hardware over the past 20 years (follow-
ing Moore’s law), the game changer has been the rise of the internet, making it feasible
to collect and distribute very large datasets for machine learning. Today, large companies
work with image datasets, video datasets, and natural language datasets that couldn’t
have been collected without the internet. User-generated image tags on Flickr, for
6
See “Flexible, High Performance Convolutional Neural Networks for Image Classification,” Proceedings of the
22nd International Joint Conference on Artificial Intelligence (2011), [Link]/Proceedings/11/Papers/
[Link].
7
See “ImageNet Classification with Deep Convolutional Neural Networks,” Advances in Neural Information Pro-
cessing Systems 25 (2012), [Link]
22 CHAPTER 1 What is deep learning?
instance, have been a treasure trove of data for computer vision. So are YouTube videos.
And Wikipedia is a key dataset for natural language processing.
If there’s one dataset that has been a catalyst for the rise of deep learning, it’s the
ImageNet dataset, consisting of 1.4 million images that have been hand annotated
with 1,000 image categories (one category per image). But what makes ImageNet spe-
cial isn’t just its large size, but also the yearly competition associated with it.8
As Kaggle has been demonstrating since 2010, public competitions are an excel-
lent way to motivate researchers and engineers to push the envelope. Having common
benchmarks that researchers compete to beat has greatly helped the rise of deep
learning, by highlighting its success against classical machine learning approaches.
1.3.3 Algorithms
In addition to hardware and data, until the late 2000s, we were missing a reliable way
to train very deep neural networks. As a result, neural networks were still fairly shal-
low, using only one or two layers of representations; thus, they weren’t able to shine
against more-refined shallow methods such as SVMs and random forests. The key
issue was that of gradient propagation through deep stacks of layers. The feedback signal
used to train neural networks would fade away as the number of layers increased.
This changed around 2009–2010 with the advent of several simple but important
algorithmic improvements that allowed for better gradient propagation:
Better activation functions for neural layers
Better weight-initialization schemes, starting with layer-wise pretraining, which was
then quickly abandoned
Better optimization schemes, such as RMSProp and Adam
Only when these improvements began to allow for training models with 10 or more
layers did deep learning start to shine.
Finally, in 2014, 2015, and 2016, even more advanced ways to improve gradient
propagation were discovered, such as batch normalization, residual connections, and
depthwise separable convolutions.
Today, we can train models that are arbitrarily deep from scratch. This has
unlocked the use of extremely large models, which hold considerable representa-
tional power—that is to say, which encode very rich hypothesis spaces. This extreme
scalability is one of the defining characteristics of modern deep learning. Large-scale
model architectures, which feature tens of layers and tens of millions of parameters,
have brought about critical advances both in computer vision (for instance, architec-
tures such as ResNet, Inception, or Xception) and natural language processing (for
instance, large Transformer-based architectures such as BERT, GPT-3, or XLNet).
8
The ImageNet Large Scale Visual Recognition Challenge (ILSVRC), [Link]/challenges/LSVRC.
Why deep learning? Why now? 23
16
Israel
EU
14
12
China
10
8
Israel
EU
6 China
EU
4 US
US
2 US US
US
US US
0
2011 2012 2013 2014 2015 2016 2017
In 2011, right before deep learning took the spotlight, the total venture capital invest-
ment in AI worldwide was less than a billion dollars, which went almost entirely to
practical applications of shallow machine learning approaches. In 2015, it had risen
to over $5 billion, and in 2017, to a staggering $16 billion. Hundreds of startups
launched in these few years, trying to capitalize on the deep learning hype. Mean-
while, large tech companies such as Google, Amazon, and Microsoft have invested in
internal research departments in amounts that would most likely dwarf the flow of
venture-capital money.
Machine learning—in particular, deep learning—has become central to the prod-
uct strategy of these tech giants. In late 2015, Google CEO Sundar Pichai stated,
“Machine learning is a core, transformative way by which we’re rethinking how we’re
24 CHAPTER 1 What is deep learning?
doing everything. We’re thoughtfully applying it across all our products, be it search,
ads, YouTube, or Play. And we’re in early days, but you’ll see us—in a systematic way—
apply machine learning in all these areas.” 9
As a result of this wave of investment, the number of people working on deep
learning went from a few hundred to tens of thousands in less than 10 years, and
research progress has reached a frenetic pace.
9
Sundar Pichai, Alphabet earnings call, Oct. 22, 2015.
Why deep learning? Why now? 25
26
A first look at a neural network 27
After reading this chapter, you’ll have an intuitive understanding of the mathemat-
ical theory behind deep learning, and you’ll be ready to start diving into Keras and
TensorFlow in chapter 3.
You don’t need to try to reproduce this example on your machine just now. If you wish
to, you’ll first need to set up a deep learning workspace, which is covered in chapter 3.
The MNIST dataset comes preloaded in Keras, in the form of a set of four NumPy
arrays.
train_images and train_labels form the training set, the data that the model will
learn from. The model will then be tested on the test set, test_images and test_labels.
28 CHAPTER 2 The mathematical building blocks of neural networks
The images are encoded as NumPy arrays, and the labels are an array of digits, rang-
ing from 0 to 9. The images and labels have a one-to-one correspondence.
Let’s look at the training data:
>>> train_images.shape
(60000, 28, 28)
>>> len(train_labels)
60000
>>> train_labels
array([5, 0, 4, ..., 5, 6, 8], dtype=uint8)
>>> test_images.shape
(10000, 28, 28)
>>> len(test_labels)
10000
>>> test_labels
array([7, 2, 1, ..., 4, 5, 6], dtype=uint8)
The workflow will be as follows: First, we’ll feed the neural network the training data,
train_images and train_labels. The network will then learn to associate images and
labels. Finally, we’ll ask the network to produce predictions for test_images, and we’ll
verify whether these predictions match the labels from test_labels.
Let’s build the network—again, remember that you aren’t expected to understand
everything about this example yet.
The core building block of neural networks is the layer. You can think of a layer as a fil-
ter for data: some data goes in, and it comes out in a more useful form. Specifically,
layers extract representations out of the data fed into them—hopefully, representations
that are more meaningful for the problem at hand. Most of deep learning consists of
chaining together simple layers that will implement a form of progressive data distilla-
tion. A deep learning model is like a sieve for data processing, made of a succession of
increasingly refined data filters—the layers.
Here, our model consists of a sequence of two Dense layers, which are densely con-
nected (also called fully connected) neural layers. The second (and last) layer is a 10-way
softmax classification layer, which means it will return an array of 10 probability scores
(summing to 1). Each score will be the probability that the current digit image
belongs to one of our 10 digit classes.
A first look at a neural network 29
To make the model ready for training, we need to pick three more things as part of
the compilation step:
An optimizer—The mechanism through which the model will update itself based
on the training data it sees, so as to improve its performance.
A loss function—How the model will be able to measure its performance on the
training data, and thus how it will be able to steer itself in the right direction.
Metrics to monitor during training and testing—Here, we’ll only care about accu-
racy (the fraction of the images that were correctly classified).
The exact purpose of the loss function and the optimizer will be made clear through-
out the next two chapters.
[Link](optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
Before training, we’ll preprocess the data by reshaping it into the shape the model
expects and scaling it so that all values are in the [0, 1] interval. Previously, our train-
ing images were stored in an array of shape (60000, 28, 28) of type uint8 with values
in the [0, 255] interval. We’ll transform it into a float32 array of shape (60000, 28 *
28) with values between 0 and 1.
We’re now ready to train the model, which in Keras is done via a call to the model’s
fit() method—we fit the model to its training data.
Two quantities are displayed during training: the loss of the model over the training
data, and the accuracy of the model over the training data. We quickly reach an accu-
racy of 0.989 (98.9%) on the training data.
Now that we have a trained model, we can use it to predict class probabilities for
new digits—images that weren’t part of the training data, like those from the test set.
30 CHAPTER 2 The mathematical building blocks of neural networks
Each number of index i in that array corresponds to the probability that digit image
test_digits[0] belongs to class i.
This first test digit has the highest probability score (0.99999106, almost 1) at
index 7, so according to our model, it must be a 7:
>>> predictions[0].argmax()
7
>>> predictions[0][7]
0.99999106
>>> test_labels[0]
7
On average, how good is our model at classifying such never-before-seen digits? Let’s
check by computing average accuracy over the entire test set.
The test-set accuracy turns out to be 97.8%—that’s quite a bit lower than the training-
set accuracy (98.9%). This gap between training accuracy and test accuracy is an
example of overfitting: the fact that machine learning models tend to perform worse
on new data than on their training data. Overfitting is a central topic in chapter 3.
This concludes our first example—you just saw how you can build and train a
neural network to classify handwritten digits in less than 15 lines of Python code. In
this chapter and the next, we’ll go into detail about every moving piece we just pre-
viewed and clarify what’s going on behind the scenes. You’ll learn about tensors,
the data-storing objects going into the model; tensor operations, which layers are
made of; and gradient descent, which allows your model to learn from its training
examples.
Data representations for neural networks 31
This vector has five entries and so is called a 5-dimensional vector. Don’t confuse a 5D
vector with a 5D tensor! A 5D vector has only one axis and has five dimensions along
its axis, whereas a 5D tensor has five axes (and may have any number of dimensions
along each axis). Dimensionality can denote either the number of entries along a spe-
cific axis (as in the case of our 5D vector) or the number of axes in a tensor (such as a
5D tensor), which can be confusing at times. In the latter case, it’s technically more
correct to talk about a tensor of rank 5 (the rank of a tensor being the number of axes),
but the ambiguous notation 5D tensor is common regardless.
32 CHAPTER 2 The mathematical building blocks of neural networks
The entries from the first axis are called the rows, and the entries from the second axis
are called the columns. In the previous example, [5, 78, 2, 34, 0] is the first row of x,
and [5, 6, 7] is the first column.
By packing rank-3 tensors in an array, you can create a rank-4 tensor, and so on. In
deep learning, you’ll generally manipulate tensors with ranks 0 to 4, although you
may go up to 5 if you process video data.
Data type (usually called dtype in Python libraries)—This is the type of the data
contained in the tensor; for instance, a tensor’s type could be float16, float32,
float64, uint8, and so on. In TensorFlow, you are also likely to come across
string tensors.
To make this more concrete, let’s look back at the data we processed in the MNIST
example. First, we load the MNIST dataset:
Next, we display the number of axes of the tensor train_images, the ndim attribute:
>>> train_images.ndim
3
>>> train_images.shape
(60000, 28, 28)
>>> train_images.dtype
uint8
So what we have here is a rank-3 tensor of 8-bit integers. More precisely, it’s an array of
60,000 matrices of 28 × 28 integers. Each such matrix is a grayscale image, with coeffi-
cients between 0 and 255.
Let’s display the fourth digit in this rank-3 tensor, using the Matplotlib library (a
well-known Python data visualization library, which comes preinstalled in Colab); see
figure 2.2.
>>> train_labels[4]
9
It’s equivalent to this more detailed notation, which specifies a start index and stop
index for the slice along each tensor axis. Note that : is equivalent to selecting the
entire axis:
Equivalent to the
>>> my_slice = train_images[10:100, :, :]
previous example
>>> my_slice.shape
(90, 28, 28)
>>> my_slice = train_images[10:100, 0:28, 0:28]
Also equivalent to the
>>> my_slice.shape previous example
(90, 28, 28)
In general, you may select slices between any two indices along each tensor axis. For
instance, in order to select 14 × 14 pixels in the bottom-right corner of all images, you
would do this:
It’s also possible to use negative indices. Much like negative indices in Python lists,
they indicate a position relative to the end of the current axis. In order to crop the
images to patches of 14 × 14 pixels centered in the middle, you’d do this:
batch = train_images[:128]
batch = train_images[128:256]
n = 3
batch = train_images[128 * n:128 * (n + 1)]
When considering such a batch tensor, the first axis (axis 0) is called the batch axis or
batch dimension. This is a term you’ll frequently encounter when using Keras and other
deep learning libraries.
Features
Samples
Figure 2.3 A rank-3 timeseries
Timesteps data tensor
The time axis is always the second axis (axis of index 1) by convention. Let’s look at a
few examples:
A dataset of stock prices. Every minute, we store the current price of the stock,
the highest price in the past minute, and the lowest price in the past minute.
Thus, every minute is encoded as a 3D vector, an entire day of trading is
encoded as a matrix of shape (390, 3) (there are 390 minutes in a trading day),
and 250 days’ worth of data can be stored in a rank-3 tensor of shape (250,
390, 3). Here, each sample would be one day’s worth of data.
A dataset of tweets, where we encode each tweet as a sequence of 280 characters
out of an alphabet of 128 unique characters. In this setting, each character can
be encoded as a binary vector of size 128 (an all-zeros vector except for a 1 entry
at the index corresponding to the character). Then each tweet can be encoded
as a rank-2 tensor of shape (280, 128), and a dataset of 1 million tweets can be
stored in a tensor of shape (1000000, 280, 128).
Data representations for neural networks 37
Color channels
Height
Samples
There are two conventions for shapes of image tensors: the channels-last convention
(which is standard in TensorFlow) and the channels-first convention (which is increas-
ingly falling out of favor).
The channels-last convention places the color-depth axis at the end: (samples,
height, width, color_depth). Meanwhile, the channels-first convention places the
color depth axis right after the batch axis: (samples, color_depth, height, width).
With the channels-first convention, the previous examples would become (128, 1,
256, 256) and (128, 3, 256, 256). The Keras API provides support for both formats.
dtype of the tensor was float32, each value would be stored in 32 bits, so the tensor
would represent 405 MB. Heavy! Videos you encounter in real life are much lighter,
because they aren’t stored in float32, and they’re typically compressed by a large fac-
tor (such as in the MPEG format).
[Link](512, activation="relu")
This layer can be interpreted as a function, which takes as input a matrix and returns
another matrix—a new representation for the input tensor. Specifically, the function
is as follows (where W is a matrix and b is a vector, both attributes of the layer):
output = relu(dot(input, W) + b)
NOTE Although this section deals entirely with linear algebra expressions,
you won’t find any mathematical notation here. I’ve found that mathematical
concepts can be more readily mastered by programmers with no mathemati-
cal background if they’re expressed as short Python snippets instead of math-
ematical equations. So we’ll use NumPy and TensorFlow code throughout.
x = [Link]()
Avoid overwriting
for i in range([Link][0]): the input tensor.
for j in range([Link][1]):
x[i, j] = max(x[i, j], 0)
return x
On the same principle, you can do element-wise multiplication, subtraction, and so on.
In practice, when dealing with NumPy arrays, these operations are available as well-
optimized built-in NumPy functions, which themselves delegate the heavy lifting to a
Basic Linear Algebra Subprograms (BLAS) implementation. BLAS are low-level,
highly parallel, efficient tensor-manipulation routines that are typically implemented
in Fortran or C.
So, in NumPy, you can do the following element-wise operation, and it will be blaz-
ing fast:
import time
x = [Link]((20, 100))
y = [Link]((20, 100))
t0 = [Link]()
for _ in range(1000):
z = x + y
z = [Link](z, 0.)
print("Took: {0:.2f} s".format([Link]() - t0))
This takes 0.02 s. Meanwhile, the naive version takes a stunning 2.45 s:
t0 = [Link]()
for _ in range(1000):
z = naive_add(x, y)
z = naive_relu(z)
print("Took: {0:.2f} s".format([Link]() - t0))
40 CHAPTER 2 The mathematical building blocks of neural networks
Likewise, when running TensorFlow code on a GPU, element-wise operations are exe-
cuted via fully vectorized CUDA implementations that can best utilize the highly par-
allel GPU chip architecture.
2.3.2 Broadcasting
Our earlier naive implementation of naive_add only supports the addition of rank-2
tensors with identical shapes. But in the Dense layer introduced earlier, we added a
rank-2 tensor with a vector. What happens with addition when the shapes of the two
tensors being added differ?
When possible, and if there’s no ambiguity, the smaller tensor will be broadcast to
match the shape of the larger tensor. Broadcasting consists of two steps:
1 Axes (called broadcast axes) are added to the smaller tensor to match the ndim of
the larger tensor.
2 The smaller tensor is repeated alongside these new axes to match the full shape
of the larger tensor.
Let’s look at a concrete example. Consider X with shape (32, 10) and y with shape
(10,):
X is a random matrix
with shape (32, 10).
import numpy as np
X = [Link]((32, 10)) y is a random vector
y = [Link]((10,)) with shape (10,).
First, we add an empty first axis to y, whose shape becomes (1, 10):
Then, we repeat y 32 times alongside this new axis, so that we end up with a tensor Y
with shape (32, 10), where Y[i, :] == y for i in range(0, 32):
At this point, we can proceed to add X and Y, because they have the same shape.
In terms of implementation, no new rank-2 tensor is created, because that would
be terribly inefficient. The repetition operation is entirely virtual: it happens at the
algorithmic level rather than at the memory level. But thinking of the vector being
repeated 10 times alongside a new axis is a helpful mental model. Here’s what a naive
implementation would look like:
x is a rank-2
def naive_add_matrix_and_vector(x, y): NumPy tensor.
assert len([Link]) == 2
assert len([Link]) == 1 y is a NumPy vector.
assert [Link][1] == [Link][0]
x = [Link]()
Avoid overwriting
for i in range([Link][0]): the input tensor.
The gears of neural networks: Tensor operations 41
for j in range([Link][1]):
x[i, j] += y[j]
return x
With broadcasting, you can generally perform element-wise operations that take two
inputs tensors if one tensor has shape (a, b, … n, n + 1, … m) and the other has shape (n,
n + 1, … m). The broadcasting will then automatically happen for axes a through n - 1.
The following example applies the element-wise maximum operation to two tensors
of different shapes via broadcasting:
x = [Link]((32,))
y = [Link]((32,))
z = [Link](x, y)
z = x • y
Mathematically, what does the dot operation do? Let’s start with the dot product of
two vectors, x and y. It’s computed as follows:
You’ll have noticed that the dot product between two vectors is a scalar and that only
vectors with the same number of elements are compatible for a dot product.
You can also take the dot product between a matrix x and a vector y, which returns
a vector where the coefficients are the dot products between y and the rows of x. You
implement it as follows:
42 CHAPTER 2 The mathematical building blocks of neural networks
You could also reuse the code we wrote previously, which highlights the relationship
between a matrix-vector product and a vector product:
Note that as soon as one of the two tensors has an ndim greater than 1, dot is no lon-
ger symmetric, which is to say that dot(x, y) isn’t the same as dot(y, x).
Of course, a dot product generalizes to tensors with an arbitrary number of axes.
The most common applications may be the dot product between two matrices. You can
take the dot product of two matrices x and y (dot(x, y)) if and only if [Link][1] ==
[Link][0]. The result is a matrix with shape ([Link][0], [Link][1]), where the
coefficients are the vector products between the rows of x and the columns of y.
Here’s the naive implementation:
To understand dot-product shape compatibility, it helps to visualize the input and out-
put tensors by aligning them as shown in figure 2.5.
In the figure, x, y, and z are pictured as rectangles (literal boxes of coefficients).
Because the rows of x and the columns of y must have the same size, it follows that the
width of x must match the height of y. If you go on to develop new machine learning
algorithms, you’ll likely be drawing such diagrams often.
The gears of neural networks: Tensor operations 43
[Link]:
(b, c)
x•y=z
b Column of y
[Link]: [Link]:
(a, b) (a, c)
a
z [ i, j ] Figure 2.5 Matrix dot-product
Row of x box diagram
More generally, you can take the dot product between higher-dimensional tensors,
following the same rules for shape compatibility as outlined earlier for the 2D case:
And so on.
Reshaping a tensor means rearranging its rows and columns to match a target shape.
Naturally, the reshaped tensor has the same total number of coefficients as the initial
tensor. Reshaping is best understood via simple examples:
A = [0.5, 1]
It’s a point in a 2D space (see figure 2.6). It’s common to picture a vector as an arrow
linking the origin to the point, as shown in figure 2.7.
1 A [0.5, 1] 1 A [0.5, 1]
1 1
Let’s consider a new point, B = [1, 0.25], which we’ll add to the previous one. This is
done geometrically by chaining together the vector arrows, with the resulting location
being the vector representing the sum of the previous two vectors (see figure 2.8). As
you can see, adding a vector B to a vector A represents the action of copying point A
in a new location, whose distance and direction from the original point A is determined
by the vector B. If you apply the same vector addition to a group of points in the plane
(an “object”), you would be creating a copy of the entire object in a new location (see
The gears of neural networks: Tensor operations 45
A+B
1 A
B
1
Figure 2.8 Geometric interpretation
of the sum of two vectors
figure 2.9). Tensor addition thus represents the action of translating an object (moving
the object without distorting it) by a certain amount in a certain direction.
Horizontal factor x
Vertical factor
+ y
K Vertical factor
K Figure 2.9 2D
translation as a
Horizontal factor
vector addition
cos(theta) –sin(theta) x
sin(theta) cos(theta) y
K K Theta
Figure 2.10 2D rotation
(counterclockwise) as a
dot product
46 CHAPTER 2 The mathematical building blocks of neural networks
Scaling: A vertical and horizontal scaling of the image (see figure 2.11) can be
achieved via a dot product with a 2 × 2 matrix S = [[horizontal_factor, 0],
[0, vertical_factor]] (note that such a matrix is called a “diagonal matrix,”
because it only has non-zero coefficients in its “diagonal,” going from the top
left to the bottom right).
K
1 0 x
0 –0.5 y
K
Figure 2.11
2D scaling as a
dot product
W•x+b
Dense layer with relu activation: An important observation about affine trans-
forms is that if you apply many of them repeatedly, you still end up with an
affine transform (so you could just have applied that one affine transform in
the first place). Let’s try it with two: affine2(affine1(x)) = W2 • (W1 • x + b1)
+ b2 = (W2 • W1) • x + (W2 • b1 + b2). That’s an affine transform where the linear
part is the matrix W2 • W1 and the translation part is the vector W2 • b1 + b2. As a
consequence, a multilayer neural network made entirely of Dense layers without
The gears of neural networks: Tensor operations 47
activations would be equivalent to a single Dense layer. This “deep” neural net-
work would just be a linear model in disguise! This is why we need activation
functions, like relu (seen in action in figure 2.13). Thanks to activation func-
tions, a chain of Dense layers can be made to implement very complex, non-linear
geometric transformations, resulting in very rich hypothesis spaces for your
deep neural networks. We’ll cover this idea in more detail in the next chapter.
relu(W • x + b)
Uncrumpling paper balls is what machine learning is about: finding neat representa-
tions for complex, highly folded data manifolds in high-dimensional spaces (a mani-
fold is a continuous surface, like our crumpled sheet of paper). At this point, you
should have a pretty good intuition as to why deep learning excels at this: it takes the
48 CHAPTER 2 The mathematical building blocks of neural networks
output = relu(dot(input, W) + b)
In this expression, W and b are tensors that are attributes of the layer. They’re called
the weights or trainable parameters of the layer (the kernel and bias attributes, respec-
tively). These weights contain the information learned by the model from exposure to
training data.
Initially, these weight matrices are filled with small random values (a step called
random initialization). Of course, there’s no reason to expect that relu(dot(input, W)
+ b), when W and b are random, will yield any useful representations. The resulting
representations are meaningless—but they’re a starting point. What comes next is to
gradually adjust these weights, based on a feedback signal. This gradual adjustment,
also called training, is the learning that machine learning is all about.
This happens within what’s called a training loop, which works as follows. Repeat
these steps in a loop, until the loss seems sufficiently low:
1 Draw a batch of training samples, x, and corresponding targets, y_true.
2 Run the model on x (a step called the forward pass) to obtain predictions, y_pred.
3 Compute the loss of the model on the batch, a measure of the mismatch between
y_pred and y_true.
4 Update all weights of the model in a way that slightly reduces the loss on this
batch.
You’ll eventually end up with a model that has a very low loss on its training data: a low
mismatch between predictions, y_pred, and expected targets, y_true. The model has
“learned” to map its inputs to correct targets. From afar, it may look like magic, but
when you reduce it to elementary steps, it turns out to be simple.
Step 1 sounds easy enough—just I/O code. Steps 2 and 3 are merely the application
of a handful of tensor operations, so you could implement these steps purely from what
you learned in the previous section. The difficult part is step 4: updating the model’s
weights. Given an individual weight coefficient in the model, how can you compute
whether the coefficient should be increased or decreased, and by how much?
One naive solution would be to freeze all weights in the model except the one sca-
lar coefficient being considered, and try different values for this coefficient. Let’s say
The engine of neural networks: Gradient-based optimization 49
the initial value of the coefficient is 0.3. After the forward pass on a batch of data, the
loss of the model on the batch is 0.5. If you change the coefficient’s value to 0.35 and
rerun the forward pass, the loss increases to 0.6. But if you lower the coefficient to
0.25, the loss falls to 0.4. In this case, it seems that updating the coefficient by –0.05
would contribute to minimizing the loss. This would have to be repeated for all coeffi-
cients in the model.
But such an approach would be horribly inefficient, because you’d need to com-
pute two forward passes (which are expensive) for every individual coefficient (of
which there are many, usually thousands and sometimes up to millions). Thankfully,
there’s a much better approach: gradient descent.
Gradient descent is the optimization technique that powers modern neural net-
works. Here’s the gist of it. All of the functions used in our models (such as dot or +)
transform their input in a smooth and continuous way: if you look at z = x + y, for
instance, a small change in y only results in a small change in z, and if you know the
direction of the change in y, you can infer the direction of the change in z. Mathemat-
ically, you’d say these functions are differentiable. If you chain together such functions,
the bigger function you obtain is still differentiable. In particular, this applies to the
function that maps the model’s coefficients to the loss of the model on a batch of
data: a small change in the model’s coefficients results in a small, predictable change
in the loss value. This enables you to use a mathematical operator called the gradient
to describe how the loss varies as you move the model’s coefficients in different direc-
tions. If you compute this gradient, you can use it to move the coefficients (all at once
in a single update, rather than one at a time) in a direction that decreases the loss.
If you already know what differentiable means and what a gradient is, you can skip to
section 2.4.3. Otherwise, the following two sections will help you understand these
concepts.
y = f(x)
y
Figure 2.15 A continuous,
x smooth function
Because the function is continuous, a small change in x can only result in a small change
in y—that’s the intuition behind continuity. Let’s say you increase x by a small factor,
epsilon_x: this results in a small epsilon_y change to y, as shown in figure 2.16.
50 CHAPTER 2 The mathematical building blocks of neural networks
epsilon_y
In addition, because the function is smooth (its curve doesn’t have any abrupt angles),
when epsilon_x is small enough, around a certain point p, it’s possible to approxi-
mate f as a linear function of slope a, so that epsilon_y becomes a * epsilon_x:
Local linear
approximation of f, with
slope a
y = f(x)
y
For every differentiable function f(x) (differentiable means “can be derived”: for exam-
ple, smooth, continuous functions can be derived), there exists a derivative function
f'(x), that maps values of x to the slope of the local linear approximation of f in
those points. For instance, the derivative of cos(x) is -sin(x), the derivative of f(x) =
a * x is f'(x) = a, and so on.
Being able to derive functions is a very powerful tool when it comes to optimization,
the task of finding values of x that minimize the value of f(x). If you’re trying to
update x by a factor epsilon_x in order to minimize f(x), and you know the deriva-
tive of f, then your job is done: the derivative completely describes how f(x) evolves
as you change x. If you want to reduce the value of f(x), you just need to move x a lit-
tle in the opposite direction from the derivative.
The engine of neural networks: Gradient-based optimization 51
Let’s say the current value of W is W0. Then the derivative of f at the point W0 is a ten-
sor grad(loss_value, W0), with the same shape as W, where each coefficient
grad(loss_value, W0)[i, j] indicates the direction and magnitude of the change in
loss_value you observe when modifying W0[i, j]. That tensor grad(loss_value,
W0) is the gradient of the function f(W) = loss_value in W0, also called “gradient of
loss_value with respect to W around W0.”
52 CHAPTER 2 The mathematical building blocks of neural networks
Partial derivatives
The tensor operation grad(f(W), W) (which takes as input a matrix W) can be
expressed as a combination of scalar functions, grad_ij(f(W), w_ij), each of
which would return the derivative of loss_value = f(W) with respect to the coeffi-
cient W[i, j] of W, assuming all other coefficients are constant. grad_ij is called
the partial derivative of f with respect to W[i, j].
Concretely, what does grad(loss_value, W0) represent? You saw earlier that the deriva-
tive of a function f(x) of a single coefficient can be interpreted as the slope of the curve
of f. Likewise, grad(loss_value, W0) can be interpreted as the tensor describing the
direction of steepest ascent of loss_value = f(W) around W0, as well as the slope of this
ascent. Each partial derivative describes the slope of f in a specific direction.
For this reason, in much the same way that, for a function f(x), you can reduce
the value of f(x) by moving x a little in the opposite direction from the derivative,
with a function f(W) of a tensor, you can reduce loss_value = f(W) by moving W in the
opposite direction from the gradient: for example, W1 = W0 - step * grad(f(W0), W0)
(where step is a small scaling factor). That means going against the direction of steep-
est ascent of f, which intuitively should put you lower on the curve. Note that the scaling
factor step is needed because grad(loss_value, W0) only approximates the curva-
ture when you’re close to W0, so you don’t want to get too far from W0.
3 Compute the loss of the model on the batch, a measure of the mismatch
between y_pred and y_true.
4 Compute the gradient of the loss with regard to the model’s parameters (this is
called the backward pass).
5 Move the parameters a little in the opposite direction from the gradient—for
example, W -= learning_rate * gradient—thus reducing the loss on the batch
a bit. The learning rate (learning_rate here) would be a scalar factor modulat-
ing the “speed” of the gradient descent process.
Easy enough! What we just described is called mini-batch stochastic gradient descent
(mini-batch SGD). The term stochastic refers to the fact that each batch of data is
drawn at random (stochastic is a scientific synonym of random). Figure 2.18 illustrates
what happens in 1D, when the model has only one parameter and you have only one
training sample.
t=1
t=2
t=3
As you can see, intuitively it’s important to pick a reasonable value for the learning_
rate factor. If it’s too small, the descent down the curve will take many iterations, and
it could get stuck in a local minimum. If learning_rate is too large, your updates may
end up taking you to completely random locations on the curve.
Note that a variant of the mini-batch SGD algorithm would be to draw a single sam-
ple and target at each iteration, rather than drawing a batch of data. This would be
true SGD (as opposed to mini-batch SGD). Alternatively, going to the opposite extreme,
you could run every step on all data available, which is called batch gradient descent.
Each update would then be more accurate, but far more expensive. The efficient com-
promise between these two extremes is to use mini-batches of reasonable size.
Although figure 2.18 illustrates gradient descent in a 1D parameter space, in prac-
tice you’ll use gradient descent in highly dimensional spaces: every weight coefficient
in a neural network is a free dimension in the space, and there may be tens of thou-
sands or even millions of them. To help you build intuition about loss surfaces, you
can also visualize gradient descent along a 2D loss surface, as shown in figure 2.19. But
you can’t possibly visualize what the actual process of training a neural network looks
54 CHAPTER 2 The mathematical building blocks of neural networks
Starting point
45
40
35
30
25
20
15
10
5
Additionally, there exist multiple variants of SGD that differ by taking into account
previous weight updates when computing the next weight update, rather than just
looking at the current value of the gradients. There is, for instance, SGD with momen-
tum, as well as Adagrad, RMSprop, and several others. Such variants are known as opti-
mization methods or optimizers. In particular, the concept of momentum, which is used in
many of these variants, deserves your attention. Momentum addresses two issues with
SGD: convergence speed and local minima. Consider figure 2.20, which shows the
curve of a loss as a function of a model parameter.
Loss
value
Local
minimum
Global
minimum
As you can see, around a certain parameter value, there is a local minimum: around
that point, moving left would result in the loss increasing, but so would moving right.
The engine of neural networks: Gradient-based optimization 55
If the parameter under consideration were being optimized via SGD with a small
learning rate, the optimization process could get stuck at the local minimum instead
of making its way to the global minimum.
You can avoid such issues by using momentum, which draws inspiration from
physics. A useful mental image here is to think of the optimization process as a small
ball rolling down the loss curve. If it has enough momentum, the ball won’t get
stuck in a ravine and will end up at the global minimum. Momentum is imple-
mented by moving the ball at each step based not only on the current slope value
(current acceleration) but also on the current velocity (resulting from past accelera-
tion). In practice, this means updating the parameter w based not only on the cur-
rent gradient value but also on the previous parameter update, such as in this naive
implementation:
Calculus tells us that such a chain of functions can be derived using the following
identity, called the chain rule.
Consider two functions f and g, as well as the composed function fg such that
fg(x) == f(g(x)):
56 CHAPTER 2 The mathematical building blocks of neural networks
def fg(x):
x1 = g(x)
y = f(x1)
return y
Then the chain rule states that grad(y, x) == grad(y, x1) * grad(x1, x). This
enables you to compute the derivative of fg as long as you know the derivatives of f
and g. The chain rule is named as it is because when you add more intermediate func-
tions, it starts looking like a chain:
def fghj(x):
x1 = j(x)
x2 = h(x1)
x
x3 = g(x2)
y = f(x3)
return y
automatically generate the derivative of the expression it represents. It’s much easier
to do these things if your computation is expressed as an explicit graph data structure
rather than, say, lines of ASCII characters in a .py file.
To explain backpropagation clearly, let’s look at a really basic example of a com-
putation graph (see figure 2.22). We’ll consider a simplified version of figure 2.21,
where we only have one linear layer and where all variables are scalar. We’ll take two
scalar variables w and b, a scalar input x, and apply some operations to them to com-
bine them into an output y. Finally, we’ll apply an absolute value error-loss function:
loss_val = abs(y_true - y). Since we want to update w and b in a way that will min-
imize loss_val, we are interested in computing grad(loss_val, b) and grad(loss
_val, w).
w *
x1
b +
x2
y_true loss
Let’s set concrete values for the “input nodes” in the graph, that is to say, the input
x, the target y_true, w, and b. We’ll propagate these values to all nodes in the
graph, from top to bottom, until we reach loss_val. This is the forward pass (see
figure 2.23).
Now let’s “reverse” the graph: for each edge in the graph going from A to B, we will
create an opposite edge from B to A, and ask, how much does B vary when A varies?
That is to say, what is grad(B, A)? We’ll annotate each inverted edge with this value.
This backward graph represents the backward pass (see figure 2.24).
58 CHAPTER 2 The mathematical building blocks of neural networks
2
x
3
w *
x1 = 6
1
b +
x2 = 7
4
y_true loss
2
x
3 grad(x1, w) = 2
w *
x1 grad(x2, x1) = 1
1 grad(x2, b) = 1
b +
x2 grad(loss_val, x2) = 1
4
y_true loss
2
x
3 grad(x1, w) = 2
w *
x1 grad(x2, x1) = 1
1 grad(x2, b) = 1
b +
x2 grad(loss_val, x2) = 1
4
y_true abs_diff
loss_val
By applying the chain rule to our graph, we obtain what we were looking for:
grad(loss_val, w) = 1 * 1 * 2 = 2
grad(loss_val, b) = 1 * 1 = 1
60 CHAPTER 2 The mathematical building blocks of neural networks
NOTE If there are multiple paths linking the two nodes of interest, a and b, in
the backward graph, we would obtain grad(b, a) by summing the contribu-
tions of all the paths.
And with that, you just saw backpropagation in action! Backpropagation is simply the
application of the chain rule to a computation graph. There’s nothing more to it.
Backpropagation starts with the final loss value and works backward from the top lay-
ers to the bottom layers, computing the contribution that each parameter had in the
loss value. That’s where the name “backpropagation” comes from: we “back propa-
gate” the loss contributions of different nodes in a computation graph.
Nowadays people implement neural networks in modern frameworks that are
capable of automatic differentiation, such as TensorFlow. Automatic differentiation is
implemented with the kind of computation graph you’ve just seen. Automatic differ-
entiation makes it possible to retrieve the gradients of arbitrary compositions of differ-
entiable tensor operations without doing any extra work besides writing down the
forward pass. When I wrote my first neural networks in C in the 2000s, I had to write
my gradients by hand. Now, thanks to modern automatic differentiation tools, you’ll
never have to implement backpropagation yourself. Consider yourself lucky!
THE GRADIENT TAPE IN TENSORFLOW
The API through which you can leverage TensorFlow’s powerful automatic differenti-
ation capabilities is the GradientTape. It’s a Python scope that will “record” the tensor
operations that run inside it, in the form of a computation graph (sometimes called a
“tape”). This graph can then be used to retrieve the gradient of any output with
respect to any variable or set of variables (instances of the [Link] class). A
[Link] is a specific kind of tensor meant to hold mutable state—for instance,
the weights of a neural network are always [Link] instances.
Instantiate a scalar Variable
Open a GradientTape scope.
with an initial value of 0.
import tensorflow as tf Inside the scope, apply
x = [Link](0.) some tensor operations
with [Link]() as tape: to our variable.
y = 2 * x + 3
grad_of_y_wrt_x = [Link](y, x)
Use the tape to retrieve the
gradient of the output y with
respect to our variable x.
You will learn about the gradient tape in the next chapter.
Input X
Layer
Weights
(data transformation)
Layer
Weights
(data transformation)
Let’s go back to the first example in this chapter and review each piece of it in the
light of what you’ve learned since.
This was the input data:
Now you understand that the input images are stored in NumPy tensors, which are
here formatted as float32 tensors of shape (60000, 784) (training data) and (10000,
784) (test data) respectively.
This was our model:
model = [Link]([
[Link](512, activation="relu"),
[Link](10, activation="softmax")
])
Now you understand that this model consists of a chain of two Dense layers, that each
layer applies a few simple tensor operations to the input data, and that these opera-
tions involve weight tensors. Weight tensors, which are attributes of the layers, are
where the knowledge of the model persists.
This was the model-compilation step:
[Link](optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
Now you understand what happens when you call fit: the model will start to iterate
on the training data in mini-batches of 128 samples, 5 times over (each iteration over
all the training data is called an epoch). For each batch, the model will compute the
gradient of the loss with regard to the weights (using the Backpropagation algorithm,
which derives from the chain rule in calculus) and move the weights in the direction
that will reduce the value of the loss for this batch.
After these 5 epochs, the model will have performed 2,345 gradient updates (469
per epoch), and the loss of the model will be sufficiently low that the model will be
capable of classifying handwritten digits with high accuracy.
At this point, you already know most of what there is to know about neural net-
works. Let’s prove it by reimplementing a simplified version of that first example
“from scratch” in TensorFlow, step by step.
Looking back at our first example 63
Let’s implement a simple Python class, NaiveDense, that creates two TensorFlow
variables, W and b, and exposes a __call__() method that applies the preceding
transformation.
import tensorflow as tf
Create a matrix,
class NaiveDense: W, of shape
def __init__(self, input_size, output_size, activation): (input_size,
[Link] = activation output_size),
initialized with
w_shape = (input_size, output_size) random values.
w_initial_value = [Link](w_shape, minval=0, maxval=1e-1)
self.W = [Link](w_initial_value)
Create a vector, b, of shape
b_shape = (output_size, (output_size,), initialized with zeros.
b_initial_value = [Link](b_shape)
self.b = [Link](b_initial_value)
Apply the forward pass.
def __call__(self, inputs)::
return [Link]([Link](inputs, self.W) + self.b)
A SEQUENTIAL CLASS
SIMPLE
Now, let’s create a NaiveSequential class to chain these layers. It wraps a list of layers
and exposes a __call__() method that simply calls the underlying layers on the
inputs, in order. It also features a weights property to easily keep track of the layers’
parameters.
64 CHAPTER 2 The mathematical building blocks of neural networks
class NaiveSequential:
def __init__(self, layers):
[Link] = layers
@property
def weights(self):
weights = []
for layer in [Link]:
weights += [Link]
return weights
Using this NaiveDense class and this NaiveSequential class, we can create a mock
Keras model:
model = NaiveSequential([
NaiveDense(input_size=28 * 28, output_size=512, activation=[Link]),
NaiveDense(input_size=512, output_size=10, activation=[Link])
])
assert len([Link]) == 4
A BATCH GENERATOR
Next, we need a way to iterate over the MNIST data in mini-batches. This is easy:
import math
class BatchGenerator:
def __init__(self, images, labels, batch_size=128):
assert len(images) == len(labels)
[Link] = 0
[Link] = images
[Link] = labels
self.batch_size = batch_size
self.num_batches = [Link](len(images) / batch_size)
def next(self):
images = [Link][[Link] : [Link] + self.batch_size]
labels = [Link][[Link] : [Link] + self.batch_size]
[Link] += self.batch_size
return images, labels
3 Compute the gradient of the loss with regard to the model’s weights.
4 Move the weights by a small amount in the direction opposite to the gradient.
To compute the gradient, we will use the TensorFlow GradientTape object we intro-
duced in section 2.4.4:
As you already know, the purpose of the “weight update” step (represented by the pre-
ceding update_weights function) is to move the weights by “a bit” in a direction that
will reduce the loss on this batch. The magnitude of the move is determined by the
“learning rate,” typically a small quantity. The simplest way to implement this
update_weights function is to subtract gradient * learning_rate from each weight:
learning_rate = 1e-3
In practice, you would almost never implement a weight update step like this by hand.
Instead, you would use an Optimizer instance from Keras, like this:
optimizer = [Link](learning_rate=1e-3)
Now that our per-batch training step is ready, we can move on to implementing an
entire epoch of training.
predictions = model(test_images)
predictions = [Link]()
Calling .numpy() on a
predicted_labels = [Link](predictions, axis=1) TensorFlow tensor converts
matches = predicted_labels == test_labels it to a NumPy tensor.
print(f"accuracy: {[Link]():.2f}")
All done! As you can see, it’s quite a bit of work to do “by hand” what you can do in a
few lines of Keras code. But because you’ve gone through these steps, you should now
have a crystal clear understanding of what goes on inside a neural network when you
call fit(). Having this low-level mental model of what your code is doing behind the
scenes will make you better able to leverage the high-level features of the Keras API.
Summary
Tensors form the foundation of modern machine learning systems. They come
in various flavors of dtype, rank, and shape.
You can manipulate numerical tensors via tensor operations (such as addition,
tensor product, or element-wise multiplication), which can be interpreted as
encoding geometric transformations. In general, everything in deep learning is
amenable to a geometric interpretation.
Deep learning models consist of chains of simple tensor operations, parameter-
ized by weights, which are themselves tensors. The weights of a model are where
its “knowledge” is stored.
Learning means finding a set of values for the model’s weights that minimizes a loss
function for a given set of training data samples and their corresponding targets.
Summary 67
Learning happens by drawing random batches of data samples and their tar-
gets, and computing the gradient of the model parameters with respect to the
loss on the batch. The model parameters are then moved a bit (the magnitude
of the move is defined by the learning rate) in the opposite direction from the
gradient. This is called mini-batch stochastic gradient descent.
The entire learning process is made possible by the fact that all tensor operations
in neural networks are differentiable, and thus it’s possible to apply the chain rule
of derivation to find the gradient function mapping the current parameters and
current batch of data to a gradient value. This is called backpropagation.
Two key concepts you’ll see frequently in future chapters are loss and optimizers.
These are the two things you need to define before you begin feeding data into
a model.
– The loss is the quantity you’ll attempt to minimize during training, so it
should represent a measure of success for the task you’re trying to solve.
– The optimizer specifies the exact way in which the gradient of the loss will be
used to update parameters: for instance, it could be the RMSProp optimizer,
SGD with momentum, and so on.
Introduction to Keras
and TensorFlow
This chapter is meant to give you everything you need to start doing deep learning
in practice. I’ll give you a quick presentation of Keras ([Link] and Tensor-
Flow ([Link] the Python-based deep learning tools that we’ll use
throughout the book. You’ll find out how to set up a deep learning workspace, with
TensorFlow, Keras, and GPU support. Finally, building on top of the first contact
you had with Keras and TensorFlow in chapter 2, we’ll review the core components
of neural networks and how they translate to the Keras and TensorFlow APIs.
By the end of this chapter, you’ll be ready to move on to practical, real-world
applications, which will start with chapter 4.
68
What’s Keras? 69
consistent and simple workflows, it minimizes the number of actions required for com-
mon use cases, and it provides clear and actionable feedback upon user error. This
makes Keras easy to learn for a beginner, and highly productive to use for an expert.
Keras has well over a million users as of late 2021, ranging from academic research-
ers, engineers, and data scientists at both startups and large companies to graduate
students and hobbyists. Keras is used at Google, Netflix, Uber, CERN, NASA, Yelp,
Instacart, Square, and hundreds of startups working on a wide range of problems
across every industry. Your YouTube recommendations originate from Keras models.
The Waymo self-driving cars are developed with Keras models. Keras is also a popular
framework on Kaggle, the machine learning competition website, where most deep
learning competitions have been won using Keras.
Because Keras has a large and diverse user base, it doesn’t force you to follow a sin-
gle “true” way of building and training models. Rather, it enables a wide range of dif-
ferent workflows, from the very high level to the very low level, corresponding to
different user profiles. For instance, you have an array of ways to build models and an
array of ways to train them, each representing a certain trade-off between usability and
flexibility. In chapter 5, we’ll review in detail a good fraction of this spectrum of work-
flows. You could be using Keras like you would use Scikit-learn—just calling fit() and
letting the framework do its thing—or you could be using it like NumPy—taking full
control of every little detail.
This means that everything you’re learning now as you’re getting started will still
be relevant once you’ve become an expert. You can get started easily and then gradu-
ally dive into workflows where you’re writing more and more logic from scratch. You
won’t have to switch to an entirely different framework as you go from student to
researcher, or from data scientist to deep learning engineer.
This philosophy is not unlike that of Python itself! Some languages only offer one
way to write programs—for instance, object-oriented programming or functional pro-
gramming. Meanwhile, Python is a multiparadigm language: it offers an array of possi-
ble usage patterns that all work nicely together. This makes Python suitable to a wide
range of very different use cases: system administration, data science, machine learning
Setting up a deep learning workspace 71
engineering, web development . . . or just learning how to program. Likewise, you can
think of Keras as the Python of deep learning: a user-friendly deep learning language
that offers a variety of workflows to different user profiles.
I recommend using Jupyter notebooks to get started with Keras, although that isn’t
a requirement: you can also run standalone Python scripts or run code from within an
IDE such as PyCharm. All the code examples in this book are available as open source
notebooks; you can download them from GitHub at [Link]/fchollet/deep-
learning-with-python-notebooks.
You’ll notice two buttons in the toolbar: + Code and + Text. They’re for creating exe-
cutable Python code cells and annotation text cells, respectively. After entering code
in a code cell, Pressing Shift-Enter will execute it (see figure 3.3).
In a text cell, you can use Markdown syntax (see figure 3.4). Pressing Shift-Enter
on a text cell will render it.
Text cells are useful for giving a readable structure to your notebooks: use them to
annotate your code with section titles and long explanation paragraphs or to embed
figures. Notebooks are meant to be a multimedia experience!
74 CHAPTER 3 Introduction to Keras and TensorFlow
[[0.33779848]
[0.06692922]
[0.7749394 ]], shape=(3, 1), dtype=float32)
A significant difference between NumPy arrays and TensorFlow tensors is that Tensor-
Flow tensors aren’t assignable: they’re constant. For instance, in NumPy, you can do
the following.
import numpy as np
x = [Link](shape=(2, 2))
x[0, 0] = 0.
Try to do the same thing in TensorFlow, and you will get an error: “EagerTensor object
does not support item assignment.”
To train a model, we’ll need to update its state, which is a set of tensors. If tensors
aren’t assignable, how do we do it? That’s where variables come in. [Link] is the
class meant to manage modifiable state in TensorFlow. You’ve already briefly seen it in
action in the training loop implementation at the end of chapter 2.
To create a variable, you need to provide some initial value, such as a random tensor.
The state of a variable can be modified via its assign method, as follows.
Importantly, each of the preceding operations gets executed on the fly: at any point,
you can print what the current result is, just like in NumPy. We call this eager execution.
input_var = [Link](initial_value=3.)
with [Link]() as tape:
result = [Link](input_var)
gradient = [Link](result, input_var)
First steps with TensorFlow 79
This is most commonly used to retrieve the gradients of the loss of a model with
respect to its weights: gradients = [Link](loss, weights). You saw this in
action in chapter 2.
So far, you’ve only seen the case where the input tensors in [Link]() were
TensorFlow variables. It’s actually possible for these inputs to be any arbitrary tensor.
However, only trainable variables are tracked by default. With a constant tensor, you’d
have to manually mark it as being tracked by calling [Link]() on it.
input_const = [Link](3.)
with [Link]() as tape:
[Link](input_const)
result = [Link](input_const)
gradient = [Link](result, input_const)
Why is this necessary? Because it would be too expensive to preemptively store the
information required to compute the gradient of anything with respect to anything.
To avoid wasting resources, the tape needs to know what to watch. Trainable variables
are watched by default because computing the gradient of a loss with regard to a list of
trainable variables is the most common use of the gradient tape.
The gradient tape is a powerful utility, even capable of computing second-order gra-
dients, that is to say, the gradient of a gradient. For instance, the gradient of the posi-
tion of an object with regard to time is the speed of that object, and the second-order
gradient is its acceleration.
If you measure the position of a falling apple along a vertical axis over time and
find that it verifies position(time) = 4.9 * time ** 2, what is its acceleration? Let’s
use two nested gradient tapes to find out.
time = [Link](0.)
with [Link]() as outer_tape: We use the outer tape to
with [Link]() as inner_tape: compute the gradient of
position = 4.9 * time ** 2 the gradient from the inner
speed = inner_tape.gradient(position, time) tape. Naturally, the answer
acceleration = outer_tape.gradient(speed, time) is 4.9 * 2 = 9.8.
Let’s get you past that filter and use your newfound knowledge of TensorFlow to
implement such a linear classifier.
First, let’s come up with some nicely linearly separable synthetic data to work with:
two classes of points in a 2D plane. We’ll generate each class of points by drawing their
coordinates from a random distribution with a specific covariance matrix and a spe-
cific mean. Intuitively, the covariance matrix describes the shape of the point cloud,
and the mean describes its position in the plane (see figure 3.6). We’ll reuse the same
covariance matrix for both point clouds, but we’ll use two different mean values—the
point clouds will have the same shape, but different positions.
Listing 3.14 Stacking the two classes into an array with shape (2000, 2)
Let’s generate the corresponding target labels, an array of zeros and ones of shape
(2000, 1), where targets[i, 0] is 0 if inputs[i] belongs to class 0 (and inversely).
Listing 3.16 Plotting the two point classes (see figure 3.6)
Now let’s create a linear classifier that can learn to separate these two blobs. A linear
classifier is an affine transformation (prediction = W • input + b) trained to minimize
the square of the difference between predictions and the targets.
As you’ll see, it’s actually a much simpler example than the end-to-end example of
a toy two-layer neural network you saw at the end of chapter 2. However, this time you
should be able to understand everything about the code, line by line.
Let’s create our variables, W and b, initialized with random values and with zeros,
respectively.
The inputs will The output predictions will be a single score per
be 2D points. sample (close to 0 if the sample is predicted to
be in class 0, and close to 1 if the sample is
input_dim = 2
predicted to be in class 1).
output_dim = 1
W = [Link](initial_value=[Link](shape=(input_dim, output_dim)))
b = [Link](initial_value=[Link](shape=(output_dim,)))
def model(inputs):
return [Link](inputs, W) + b
Because our linear classifier operates on 2D inputs, W is really just two scalar coeffi-
cients, w1 and w2: W = [[w1], [w2]]. Meanwhile, b is a single scalar coefficient. As such,
for a given input point [x, y], its prediction value is prediction = [[w1], [w2]] • [x,
y] + b = w1 * x + w2 * y + b.
The following listing shows our loss function.
82 CHAPTER 3 Introduction to Keras and TensorFlow
Next is the training step, which receives some training data and updates the weights W
and b so as to minimize the loss on the data.
For simplicity, we’ll do batch training instead of mini-batch training: we’ll run each training
step (gradient computation and weight update) for all the data, rather than iterate over
the data in small batches. On one hand, this means that each training step will take
much longer to run, since we’ll compute the forward pass and the gradients for 2,000
samples at once. On the other hand, each gradient update will be much more effective
at reducing the loss on the training data, since it will encompass information from all
training samples instead of, say, only 128 random samples. As a result, we will need many
fewer steps of training, and we should use a larger learning rate than we would typically
use for mini-batch training (we’ll use learning_rate = 0.1, defined in listing 3.20).
After 40 steps, the training loss seems to have stabilized around 0.025. Let’s plot how
our linear model classifies the training data points. Because our targets are zeros and
ones, a given input point will be classified as “0” if its prediction value is below 0.5, and
as “1” if it is above 0.5 (see figure 3.7):
predictions = model(inputs)
[Link](inputs[:, 0], inputs[:, 1], c=predictions[:, 0] > 0.5)
[Link]()
First steps with TensorFlow 83
Recall that the prediction value for a given point [x, y] is simply prediction ==
[[w1], [w2]] • [x, y] + b == w1 * x + w2 * y + b. Thus, class 0 is defined as w1 * x + w2
* y + b < 0.5, and class 1 is defined as w1 * x + w2 * y + b > 0.5. You’ll notice that what
you’re looking at is really the equation of a line in the 2D plane: w1 * x + w2 * y + b = 0.5.
Above the line is class 1, and below the line is class 0. You may be used to seeing line
equations in the format y = a * x + b; in the same format, our line becomes y = - w1 / w2
* x + (0.5 - b) / w2.
Let’s plot this line (shown in figure 3.8):
This is really what a linear classifier is all about: finding the parameters of a line (or, in
higher-dimensional spaces, a hyperplane) neatly separating two classes of data.
In the next section, we’ll cover in detail the purpose of these build() and call()
methods. Don’t worry if you don’t understand everything just yet!
Once instantiated, a layer like this can be used just like a function, taking as input
a TensorFlow tensor:
This layer will return a tensor where the first dimension has been transformed to be
32. It can only be connected to a downstream layer that expects 32-dimensional vec-
tors as its input.
When using Keras, you don’t have to worry about size compatibility most of the
time, because the layers you add to your models are dynamically built to match the
shape of the incoming layer. For instance, suppose you write the following:
86 CHAPTER 3 Introduction to Keras and TensorFlow
The layers didn’t receive any information about the shape of their inputs—instead,
they automatically inferred their input shape as being the shape of the first inputs
they see.
In the toy version of the Dense layer we implemented in chapter 2 (which we
named NaiveDense), we had to pass the layer’s input size explicitly to the constructor
in order to be able to create its weights. That’s not ideal, because it would lead to mod-
els that look like this, where each new layer needs to be made aware of the shape of
the layer before it:
model = NaiveSequential([
NaiveDense(input_size=784, output_size=32, activation="relu"),
NaiveDense(input_size=32, output_size=64, activation="relu"),
NaiveDense(input_size=64, output_size=32, activation="relu"),
NaiveDense(input_size=32, output_size=10, activation="softmax")
])
It would be even worse if the rules used by a layer to produce its output shape are
complex. For instance, what if our layer returned outputs of shape (batch, input_
size * 2 if input_size % 2 == 0 else input_size * 3)?
If we were to reimplement our NaiveDense layer as a Keras layer capable of auto-
matic shape inference, it would look like the previous SimpleDense layer (see listing
3.22), with its build() and call() methods.
In SimpleDense, we no longer create weights in the constructor like in the Naive-
Dense example; instead, we create them in a dedicated state-creation method,
build(), which receives as an argument the first input shape seen by the layer. The
build() method is called automatically the first time the layer is called (via its
__call__() method). In fact, that’s why we defined the computation in a separate
call() method rather than in the __call__() method directly. The __call__() method
of the base layer schematically looks like this:
With automatic shape inference, our previous example becomes simple and neat:
model = [Link]([
SimpleDense(32, activation="relu"),
SimpleDense(64, activation="relu"),
Anatomy of a neural network: Understanding core Keras APIs 87
SimpleDense(32, activation="relu"),
SimpleDense(10, activation="softmax")
])
Note that automatic shape inference is not the only thing that the Layer class’s
__call__() method handles. It takes care of many more things, in particular routing
between eager and graph execution (a concept you’ll learn about in chapter 7), and
input masking (which we’ll cover in chapter 11). For now, just remember: when
implementing your own layers, put the forward pass in the call() method.
LayerNormalization
Dense
Dense
LayerNormalization
LayerNormalization
+
+
Dense
MultiHeadAttention
Dense
LayerNormalization LayerNormalization
+ +
MultiHeadAttention MultiHeadAttention
Figure 3.9 The Transformer architecture (covered in chapter 11). There’s a lot going on
here. Throughout the next few chapters, you’ll climb your way up to understanding it.
you explicit principles for building neural networks and help you develop intuition as
to what works or doesn’t work for specific problems. You’ll build a solid intuition
about what type of model architectures work for different kinds of problems, how to
build these networks in practice, how to pick the right learning configuration, and
how to tweak a model until it yields the results you want to see.
Optimizer—Determines how the network will be updated based on the loss func-
tion. It implements a specific variant of stochastic gradient descent (SGD).
Metrics—The measures of success you want to monitor during training and vali-
dation, such as classification accuracy. Unlike the loss, training will not optimize
directly for these metrics. As such, metrics don’t need to be differentiable.
Once you’ve picked your loss, optimizer, and metrics, you can use the built-in compile()
and fit() methods to start training your model. Alternatively, you could also write
your own custom training loops—we’ll cover how to do this in chapter 7. It’s a lot
more work! For now, let’s take a look at compile() and fit().
The compile() method configures the training process—you’ve already been intro-
duced to it in your very first neural network example in chapter 2. It takes the argu-
ments optimizer, loss, and metrics (a list):
Define a linear classifier. Specify the optimizer
model = [Link]([[Link](1)]) by name: RMSprop
[Link](optimizer="rmsprop", (it’s case-insensitive).
loss="mean_squared_error",
Specify the loss
metrics=["accuracy"]) by name: mean
Specify a list of metrics: in squared error.
this case, only accuracy.
In the preceding call to compile(), we passed the optimizer, loss, and metrics as
strings (such as "rmsprop"). These strings are actually shortcuts that get converted to
Python objects. For instance, "rmsprop" becomes [Link]().
Importantly, it’s also possible to specify these arguments as object instances, like this:
[Link](optimizer=[Link](),
loss=[Link](),
metrics=[[Link]()])
This is useful if you want to pass your own custom losses or metrics, or if you want to
further configure the objects you’re using—for instance, by passing a learning_rate
argument to the optimizer:
[Link](optimizer=[Link](learning_rate=1e-4),
loss=my_custom_loss,
metrics=[my_custom_metric_1, my_custom_metric_2])
In chapter 7, we’ll cover how to create custom losses and metrics. In general, you
won’t have to create your own losses, metrics, or optimizers from scratch, because
Keras offers a wide range of built-in options that is likely to include what you need:
Optimizers:
SGD (with or without momentum)
RMSprop
Adam
90 CHAPTER 3 Introduction to Keras and TensorFlow
Adagrad
Etc.
Losses:
CategoricalCrossentropy
SparseCategoricalCrossentropy
BinaryCrossentropy
MeanSquaredError
KLDivergence
CosineSimilarity
Etc.
Metrics:
CategoricalAccuracy
SparseCategoricalAccuracy
BinaryAccuracy
AUC
Precision
Recall
Etc.
Throughout this book, you’ll see concrete applications of many of these options.
The call to fit() returns a History object. This object contains a history field, which
is a dict mapping keys such as "loss" or specific metric names to the list of their per-
epoch values.
>>> [Link]
{"binary_accuracy": [0.855, 0.9565, 0.9555, 0.95, 0.951],
"loss": [0.6573270302042366,
0.07434618508815766,
0.07687718723714351,
0.07412414988875389,
0.07617757616937161]}
To keep an eye on how the model does on new data, it’s standard practice to
reserve a subset of the training data as validation data: you won’t be training the model
on this data, but you will use it to compute a loss value and metrics value. You do this
by using the validation_data argument in fit(). Like the training data, the valida-
tion data could be passed as NumPy arrays or as a TensorFlow Dataset object.
model = [Link]([[Link](1)])
[Link](optimizer=[Link](learning_rate=0.1),
loss=[Link](),
To avoid having samples
metrics=[[Link]()])
from only one class in
the validation data,
indices_permutation = [Link](len(inputs)) shuffle the inputs and
shuffled_inputs = inputs[indices_permutation] targets using a random
shuffled_targets = targets[indices_permutation] indices permutation.
The value of the loss on the validation data is called the “validation loss,” to distin-
guish it from the “training loss.” Note that it’s essential to keep the training data and
validation data strictly separate: the purpose of validation is to monitor whether
what the model is learning is actually useful on new data. If any of the validation
data has been seen by the model during training, your validation loss and metrics
will be flawed.
Note that if you want to compute the validation loss and metrics after the training
is complete, you can call the evaluate() method:
evaluate() will iterate in batches (of size batch_size) over the data passed and
return a list of scalars, where the first entry is the validation loss and the following
entries are the validation metrics. If the model has no metrics, only the validation loss
is returned (rather than a list).
Summary 93
However, this will process all inputs in new_inputs at once, which may not be feasible
if you’re looking at a lot of data (in particular, it may require more memory than your
GPU has).
A better way to do inference is to use the predict() method. It will iterate over the
data in small batches and return a NumPy array of predictions. And unlike
__call__(), it can also process TensorFlow Dataset objects.
Takes a NumPy array or
predictions = [Link](new_inputs, batch_size=128) a Dataset and returns
a NumPy array
For instance, if we use predict() on some of our validation data with the linear
model we trained earlier, we get scalar scores that correspond to the model’s predic-
tion for each input sample:
For now, this is all you need to know about Keras models. You are ready to move on to
solving real-world machine learning problems with Keras in the next chapter.
Summary
TensorFlow is an industry-strength numerical computing framework that can
run on CPU, GPU, or TPU. It can automatically compute the gradient of any
differentiable expression, it can be distributed to many devices, and it can
export programs to various external runtimes—even JavaScript.
Keras is the standard API for doing deep learning with TensorFlow. It’s what
we’ll use throughout this book.
Key TensorFlow objects include tensors, variables, tensor operations, and the
gradient tape.
94 CHAPTER 3 Introduction to Keras and TensorFlow
The central class of Keras is the Layer. A layer encapsulates some weights and
some computation. Layers are assembled into models.
Before you start training a model, you need to pick an optimizer, a loss, and some
metrics, which you specify via the [Link]() method.
To train a model, you can use the fit() method, which runs mini-batch gradi-
ent descent for you. You can also use it to monitor your loss and metrics on val-
idation data, a set of inputs that the model doesn’t see during training.
Once your model is trained, you use the [Link]() method to generate
predictions on new inputs.
Getting started
with neural networks:
Classification and regression
This chapter is designed to get you started using neural networks to solve real prob-
lems. You’ll consolidate the knowledge you gained from chapters 2 and 3, and
you’ll apply what you’ve learned to three new tasks covering the three most com-
mon use cases of neural networks—binary classification, multiclass classification,
and scalar regression:
Classifying movie reviews as positive or negative (binary classification)
Classifying news wires by topic (multiclass classification)
Estimating the price of a house, given real-estate data (scalar regression)
These examples will be your first contact with end-to-end machine learning work-
flows: you’ll get introduced to data preprocessing, basic model architecture princi-
ples, and model evaluation.
95
96 CHAPTER 4 Getting started with neural networks: Classification and regression
By the end of this chapter, you’ll be able to use neural networks to handle simple clas-
sification and regression tasks over vector data. You’ll then be ready to start building a
more principled, theory-driven understanding of machine learning in chapter 5.
Classifying movie reviews: A binary classification example 97
The argument num_words=10000 means you’ll only keep the top 10,000 most fre-
quently occurring words in the training data. Rare words will be discarded. This allows
us to work with vector data of manageable size. If we didn’t set this limit, we’d be work-
ing with 88,585 unique words in the training data, which is unnecessarily large. Many
of these words only occur in a single sample, and thus can’t be meaningfully used for
classification.
The variables train_data and test_data are lists of reviews; each review is a list of
word indices (encoding a sequence of words). train_labels and test_labels are
lists of 0s and 1s, where 0 stands for negative and 1 stands for positive:
>>> train_data[0]
[1, 14, 22, 16, ... 178, 32]
>>> train_labels[0]
1
Because we’re restricting ourselves to the top 10,000 most frequent words, no word
index will exceed 10,000:
For kicks, here’s how you can quickly decode one of these reviews back to English words.
98 CHAPTER 4 Getting started with neural networks: Classification and regression
>>> x_train[0]
array([ 0., 1., 1., ..., 0., 0., 0.])
Classifying movie reviews: A binary classification example 99
y_train = [Link](train_labels).astype("float32")
y_test = [Link](test_labels).astype("float32")
model = [Link]([
[Link](16, activation="relu"),
[Link](16, activation="relu"),
[Link](1, activation="sigmoid")
])
The first argument being passed to each Dense layer is the number of units in the
layer: the dimensionality of representation space of the layer. You remember from
chapters 2 and 3 that each such Dense layer with a relu activation implements the fol-
lowing chain of tensor operations:
output = relu(dot(input, W) + b)
100 CHAPTER 4 Getting started with neural networks: Classification and regression
Having 16 units means the weight matrix W will have shape (input_dimension, 16):
the dot product with W will project the input data onto a 16-dimensional representa-
tion space (and then you’ll add the bias vector b and apply the relu operation). You
can intuitively understand the dimensionality of your representation space as “how
much freedom you’re allowing the model to have when learning internal representa-
tions.” Having more units (a higher-dimensional representation space) allows your
model to learn more-complex representations, but it makes the model more computa-
tionally expensive and may lead to learning unwanted patterns (patterns that will
improve performance on the training data but not on the test data).
The intermediate layers use relu as their activation function, and the final layer
uses a sigmoid activation so as to output a probability (a score between 0 and 1 indicat-
ing how likely the sample is to have the target “1”: how likely the review is to be posi-
tive). A relu (rectified linear unit) is a function meant to zero out negative values (see
figure 4.2), whereas a sigmoid “squashes” arbitrary values into the [0, 1] interval (see fig-
ure 4.3), outputting something that can be interpreted as a probability.
Finally, you need to choose a loss function and an optimizer. Because you’re facing a
binary classification problem and the output of your model is a probability (you end
your model with a single-unit layer with a sigmoid activation), it’s best to use the
binary_crossentropy loss. It isn’t the only viable choice: for instance, you could use
mean_squared_error. But crossentropy is usually the best choice when you’re dealing
Classifying movie reviews: A binary classification example 101
The layer could only learn linear transformations (affine transformations) of the input
data: the hypothesis space of the layer would be the set of all possible linear trans-
formations of the input data into a 16-dimensional space. Such a hypothesis space
is too restricted and wouldn’t benefit from multiple layers of representations,
because a deep stack of linear layers would still implement a linear operation: adding
more layers wouldn’t extend the hypothesis space (as you saw in chapter 2).
In order to get access to a much richer hypothesis space that will benefit from deep
representations, you need a non-linearity, or activation function. relu is the most
popular activation function in deep learning, but there are many other candidates,
which all come with similarly strange names: prelu, elu, and so on.
with models that output probabilities. Crossentropy is a quantity from the field of infor-
mation theory that measures the distance between probability distributions or, in this
case, between the ground-truth distribution and your predictions.
As for the choice of the optimizer, we’ll go with rmsprop, which is a usually a good
default choice for virtually any problem.
102 CHAPTER 4 Getting started with neural networks: Classification and regression
Here’s the step where we configure the model with the rmsprop optimizer and
the binary_crossentropy loss function. Note that we’ll also monitor accuracy during
training.
[Link](optimizer="rmsprop",
loss="binary_crossentropy",
metrics=["accuracy"])
x_val = x_train[:10000]
partial_x_train = x_train[10000:]
y_val = y_train[:10000]
partial_y_train = y_train[10000:]
We will now train the model for 20 epochs (20 iterations over all samples in the train-
ing data) in mini-batches of 512 samples. At the same time, we will monitor loss and
accuracy on the 10,000 samples that we set apart. We do so by passing the validation
data as the validation_data argument.
history = [Link](partial_x_train,
partial_y_train,
epochs=20,
batch_size=512,
validation_data=(x_val, y_val))
On CPU, this will take less than 2 seconds per epoch—training is over in 20 seconds.
At the end of every epoch, there is a slight pause as the model computes its loss and
accuracy on the 10,000 samples of the validation data.
Note that the call to [Link]() returns a History object, as you saw in chapter 3.
This object has a member history, which is a dictionary containing data about every-
thing that happened during training. Let’s look at it:
The dictionary contains four entries: one per metric that was being monitored during
training and during validation. In the following two listings, let’s use Matplotlib to plot
the training and validation loss side by side (see figure 4.4), as well as the training and
validation accuracy (see figure 4.5). Note that your own results may vary slightly due to
a different random initialization of your model.
[Link]()
Clears the figure
acc = history_dict["accuracy"]
val_acc = history_dict["val_accuracy"]
[Link](epochs, acc, "bo", label="Training acc")
[Link](epochs, val_acc, "b", label="Validation acc")
[Link]("Training and validation accuracy")
[Link]("Epochs")
[Link]("Accuracy")
[Link]()
[Link]()
As you can see, the training loss decreases with every epoch, and the training accuracy
increases with every epoch. That’s what you would expect when running gradient-
descent optimization—the quantity you’re trying to minimize should be less with
every iteration. But that isn’t the case for the validation loss and accuracy: they seem to
peak at the fourth epoch. This is an example of what we warned against earlier: a
model that performs better on the training data isn’t necessarily a model that will
do better on data it has never seen before. In precise terms, what you’re seeing is
overfitting: after the fourth epoch, you’re overoptimizing on the training data, and you
end up learning representations that are specific to the training data and don’t gener-
alize to data outside of the training set.
In this case, to prevent overfitting, you could stop training after four epochs. In
general, you can use a range of techniques to mitigate overfitting, which we’ll cover
in chapter 5.
Let’s train a new model from scratch for four epochs and then evaluate it on the
test data.
model = [Link]([
[Link](16, activation="relu"),
[Link](16, activation="relu"),
Classifying movie reviews: A binary classification example 105
[Link](1, activation="sigmoid")
])
[Link](optimizer="rmsprop",
loss="binary_crossentropy",
metrics=["accuracy"])
[Link](x_train, y_train, epochs=4, batch_size=512)
results = [Link](x_test, y_test)
>>> [Link](x_test)
array([[ 0.98006207]
[ 0.99758697]
[ 0.99975556]
...,
[ 0.82167041]
[ 0.02885115]
[ 0.65371346]], dtype=float32)
As you can see, the model is confident for some samples (0.99 or more, or 0.01 or
less) but less confident for others (0.6, 0.4).
4.1.7 Wrapping up
Here’s what you should take away from this example:
You usually need to do quite a bit of preprocessing on your raw data in order to
be able to feed it—as tensors—into a neural network. Sequences of words can
be encoded as binary vectors, but there are other encoding options too.
Stacks of Dense layers with relu activations can solve a wide range of problems
(including sentiment classification), and you’ll likely use them frequently.
In a binary classification problem (two output classes), your model should end
with a Dense layer with one unit and a sigmoid activation: the output of your
model should be a scalar between 0 and 1, encoding a probability.
With such a scalar sigmoid output on a binary classification problem, the loss
function you should use is binary_crossentropy.
The rmsprop optimizer is generally a good enough choice, whatever your prob-
lem. That’s one less thing for you to worry about.
As they get better on their training data, neural networks eventually start over-
fitting and end up obtaining increasingly worse results on data they’ve never
seen before. Be sure to always monitor performance on data that is outside of
the training set.
As with the IMDB dataset, the argument num_words=10000 restricts the data to the
10,000 most frequently occurring words found in the data.
You have 8,982 training examples and 2,246 test examples:
>>> len(train_data)
8982
>>> len(test_data)
2246
As with the IMDB reviews, each example is a list of integers (word indices):
>>> train_data[10]
[1, 245, 273, 207, 156, 53, 74, 160, 26, 14, 46, 296, 26, 39, 74, 2979,
3554, 14, 46, 4689, 4329, 86, 61, 3499, 4795, 14, 61, 451, 4329, 17, 12]
Here’s how you can decode it back to words, in case you’re curious.
word_index = reuters.get_word_index()
reverse_word_index = dict(
[(value, key) for (key, value) in word_index.items()])
decoded_newswire = " ".join(
[reverse_word_index.get(i - 3, "?") for i in train_data[0]])
Note that the indices are offset by 3 because 0, 1, and 2 are reserved
indices for “padding,” “start of sequence,” and “unknown.”
The label associated with an example is an integer between 0 and 45—a topic index:
>>> train_labels[10]
3
To vectorize the labels, there are two possibilities: you can cast the label list as an inte-
ger tensor, or you can use one-hot encoding. One-hot encoding is a widely used format
for categorical data, also called categorical encoding. In this case, one-hot encoding of
the labels consists of embedding each label as an all-zero vector with a 1 in the place of
the label index. The following listing shows an example.
model = [Link]([
[Link](64, activation="relu"),
[Link](64, activation="relu"),
[Link](46, activation="softmax")
])
There are two other things you should note about this architecture.
First, we end the model with a Dense layer of size 46. This means for each input
sample, the network will output a 46-dimensional vector. Each entry in this vector
(each dimension) will encode a different output class.
Second, the last layer uses a softmax activation. You saw this pattern in the MNIST
example. It means the model will output a probability distribution over the 46 different
output classes—for every input sample, the model will produce a 46-dimensional out-
put vector, where output[i] is the probability that the sample belongs to class i. The
46 scores will sum to 1.
The best loss function to use in this case is categorical_crossentropy. It mea-
sures the distance between two probability distributions: here, between the probability
Classifying newswires: A multiclass classification example 109
distribution output by the model and the true distribution of the labels. By minimiz-
ing the distance between these two distributions, you train the model to output some-
thing as close as possible to the true labels.
[Link](optimizer="rmsprop",
loss="categorical_crossentropy",
metrics=["accuracy"])
x_val = x_train[:1000]
partial_x_train = x_train[1000:]
y_val = y_train[:1000]
partial_y_train = y_train[1000:]
history = [Link](partial_x_train,
partial_y_train,
epochs=20,
batch_size=512,
validation_data=(x_val, y_val))
And finally, let’s display its loss and accuracy curves (see figures 4.6 and 4.7).
loss = [Link]["loss"]
val_loss = [Link]["val_loss"]
epochs = range(1, len(loss) + 1)
[Link](epochs, loss, "bo", label="Training loss")
[Link](epochs, val_loss, "b", label="Validation loss")
[Link]("Training and validation loss")
[Link]("Epochs")
[Link]("Loss")
[Link]()
[Link]()
The model begins to overfit after nine epochs. Let’s train a new model from scratch
for nine epochs and then evaluate it on the test set.
model = [Link]([
[Link](64, activation="relu"),
Classifying newswires: A multiclass classification example 111
[Link](64, activation="relu"),
[Link](46, activation="softmax")
])
[Link](optimizer="rmsprop",
loss="categorical_crossentropy",
metrics=["accuracy"])
[Link](x_train,
y_train,
epochs=9,
batch_size=512)
results = [Link](x_test, y_test)
>>> results
[0.9565213431445807, 0.79697239536954589]
As you can see, a random classifier would score around 19% classification accuracy, so
the results of our model seem pretty good in that light.
predictions = [Link](x_test)
>>> predictions[0].shape
(46,)
>>> [Link](predictions[0])
1.0
112 CHAPTER 4 Getting started with neural networks: Classification and regression
The largest entry is the predicted class—the class with the highest probability:
>>> [Link](predictions[0])
4
y_train = [Link](train_labels)
y_test = [Link](test_labels)
The only thing this approach would change is the choice of the loss function. The loss
function used in listing 4.21, categorical_crossentropy, expects the labels to follow
a categorical encoding. With integer labels, you should use sparse_categorical_
crossentropy:
[Link](optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
model = [Link]([
[Link](64, activation="relu"),
[Link](4, activation="relu"),
[Link](46, activation="softmax")
])
[Link](optimizer="rmsprop",
loss="categorical_crossentropy",
metrics=["accuracy"])
[Link](partial_x_train,
partial_y_train,
epochs=20,
batch_size=128,
validation_data=(x_val, y_val))
The model now peaks at ~71% validation accuracy, an 8% absolute drop. This drop is
mostly due to the fact that we’re trying to compress a lot of information (enough
Predicting house prices: A regression example 113
4.2.9 Wrapping up
Here’s what you should take away from this example:
If you’re trying to classify data points among N classes, your model should end
with a Dense layer of size N.
In a single-label, multiclass classification problem, your model should end with
a softmax activation so that it will output a probability distribution over the N
output classes.
Categorical crossentropy is almost always the loss function you should use for
such problems. It minimizes the distance between the probability distributions
output by the model and the true distribution of the targets.
There are two ways to handle labels in multiclass classification:
– Encoding the labels via categorical encoding (also known as one-hot encod-
ing) and using categorical_crossentropy as a loss function
– Encoding the labels as integers and using the sparse_categorical_cross-
entropy loss function
If you need to classify data into a large number of categories, you should avoid
creating information bottlenecks in your model due to intermediate layers that
are too small.
NOTE Don’t confuse regression and the logistic regression algorithm. Confusingly,
logistic regression isn’t a regression algorithm—it’s a classification algorithm.
114 CHAPTER 4 Getting started with neural networks: Classification and regression
>>> train_data.shape
(404, 13)
>>> test_data.shape
(102, 13)
As you can see, we have 404 training samples and 102 test samples, each with 13
numerical features, such as per capita crime rate, average number of rooms per dwell-
ing, accessibility to highways, and so on.
The targets are the median values of owner-occupied homes, in thousands of dollars:
>>> train_targets
[ 15.2, 42.3, 50. ... 19.4, 19.4, 29.1]
The prices are typically between $10,000 and $50,000. If that sounds cheap, remem-
ber that this was the mid-1970s, and these prices aren’t adjusted for inflation.
mean = train_data.mean(axis=0)
train_data -= mean
Predicting house prices: A regression example 115
std = train_data.std(axis=0)
train_data /= std
test_data -= mean
test_data /= std
Note that the quantities used for normalizing the test data are computed using the
training data. You should never use any quantity computed on the test data in your
workflow, even for something as simple as data normalization.
def build_model():
model = [Link]([
Because we need to instantiate
[Link](64, activation="relu"), the same model multiple times,
[Link](64, activation="relu"), we use a function to construct it.
[Link](1)
])
[Link](optimizer="rmsprop", loss="mse", metrics=["mae"])
return model
The model ends with a single unit and no activation (it will be a linear layer). This is a
typical setup for scalar regression (a regression where you’re trying to predict a single
continuous value). Applying an activation function would constrain the range the out-
put can take; for instance, if you applied a sigmoid activation function to the last layer,
the model could only learn to predict values between 0 and 1. Here, because the last
layer is purely linear, the model is free to learn to predict values in any range.
Note that we compile the model with the mse loss function—mean squared error, the
square of the difference between the predictions and the targets. This is a widely used
loss function for regression problems.
We’re also monitoring a new metric during training: mean absolute error (MAE). It’s the
absolute value of the difference between the predictions and the targets. For instance, an
MAE of 0.5 on this problem would mean your predictions are off by $500 on average.
might have a high variance with regard to the validation split. This would prevent us
from reliably evaluating our model.
The best practice in such situations is to use K-fold cross-validation (see figure 4.8).
Validation
Fold 1 Validation Training Training
score #1
Validation
Fold 3 Training Training Validation
score #3
It consists of splitting the available data into K partitions (typically K = 4 or 5), instanti-
ating K identical models, and training each one on K – 1 partitions while evaluating
on the remaining partition. The validation score for the model used is then the aver-
age of the K validation scores obtained. In terms of code, this is straightforward.
k = 4
num_val_samples = len(train_data) // k
num_epochs = 100
Prepares the
all_scores = [] validation data: data
for i in range(k): from partition #k
print(f"Processing fold #{i}")
val_data = train_data[i * num_val_samples: (i + 1) * num_val_samples]
val_targets = train_targets[i * num_val_samples: (i + 1) * num_val_samples]
partial_train_data = [Link](
Prepares the training data:
[train_data[:i * num_val_samples], data from all other partitions
train_data[(i + 1) * num_val_samples:]],
axis=0)
partial_train_targets = [Link]( Builds the Keras model
[train_targets[:i * num_val_samples],
(already compiled)
train_targets[(i + 1) * num_val_samples:]],
axis=0) Trains the model
model = build_model() (in silent mode,
[Link](partial_train_data, partial_train_targets, verbose = 0)
epochs=num_epochs, batch_size=16, verbose=0)
val_mse, val_mae = [Link](val_data, val_targets, verbose=0)
all_scores.append(val_mae)
Evaluates the model on
the validation data
Predicting house prices: A regression example 117
The different runs do indeed show rather different validation scores, from 2.1 to 3.1.
The average (2.6) is a much more reliable metric than any single score—that’s the
entire point of K-fold cross-validation. In this case, we’re off by $2,600 on average,
which is significant considering that the prices range from $10,000 to $50,000.
Let’s try training the model a bit longer: 500 epochs. To keep a record of how well
the model does at each epoch, we’ll modify the training loop to save the per-epoch
validation score log for each fold.
num_epochs = 500
Prepares the
all_mae_histories = [] validation data: data
for i in range(k): from partition #k
print(f"Processing fold #{i}")
val_data = train_data[i * num_val_samples: (i + 1) * num_val_samples]
val_targets = train_targets[i * num_val_samples: (i + 1) * num_val_samples]
partial_train_data = [Link](
Prepares the training
[train_data[:i * num_val_samples], data: data from all
train_data[(i + 1) * num_val_samples:]], other partitions
axis=0)
partial_train_targets = [Link](
[train_targets[:i * num_val_samples], Builds the Keras
train_targets[(i + 1) * num_val_samples:]], model (already
axis=0) compiled)
model = build_model()
history = [Link](partial_train_data, partial_train_targets,
Trains the
validation_data=(val_data, val_targets), model (in
epochs=num_epochs, batch_size=16, verbose=0) silent mode,
mae_history = [Link]["val_mae"] verbose=0)
all_mae_histories.append(mae_history)
We can then compute the average of the per-epoch MAE scores for all folds.
Listing 4.28 Building the history of successive mean K-fold validation scores
average_mae_history = [
[Link]([x[i] for x in all_mae_histories]) for i in range(num_epochs)]
It may be a little difficult to read the plot, due to a scaling issue: the validation MAE
for the first few epochs is dramatically higher than the values that follow. Let’s omit
the first 10 data points, which are on a different scale than the rest of the curve.
Listing 4.30 Plotting validation scores, excluding the first 10 data points
truncated_mae_history = average_mae_history[10:]
[Link](range(1, len(truncated_mae_history) + 1), truncated_mae_history)
[Link]("Epochs")
[Link]("Validation MAE")
[Link]()
As you can see in figure 4.10, validation MAE stops improving significantly after
120–140 epochs (this number includes the 10 epochs we omitted). Past that point,
we start overfitting.
Once you’re finished tuning other parameters of the model (in addition to the
number of epochs, you could also adjust the size of the intermediate layers), you can
train a final production model on all of the training data, with the best parameters,
and then look at its performance on the test data.
Figure 4.10 Validation MAE by epoch, excluding the first 10 data points
>>> test_mae_score
2.4642276763916016
We’re still off by a bit under $2,500. It’s an improvement! Just like with the two previ-
ous tasks, you can try varying the number of layers in the model, or the number of
units per layer, to see if you can squeeze out a lower test error.
The first house in the test set is predicted to have a price of about $10,000.
4.3.6 Wrapping up
Here’s what you should take away from this scalar regression example:
Regression is done using different loss functions than we used for classification.
Mean squared error (MSE) is a loss function commonly used for regression.
120 CHAPTER 4 Getting started with neural networks: Classification and regression
Similarly, evaluation metrics to be used for regression differ from those used for
classification; naturally, the concept of accuracy doesn’t apply for regression. A
common regression metric is mean absolute error (MAE).
When features in the input data have values in different ranges, each feature
should be scaled independently as a preprocessing step.
When there is little data available, using K-fold validation is a great way to reli-
ably evaluate a model.
When little training data is available, it’s preferable to use a small model with few
intermediate layers (typically only one or two), in order to avoid severe overfitting.
Summary
The three most common kinds of machine learning tasks on vector data are
binary classification, multiclass classification, and scalar regression.
– The “Wrapping up” sections earlier in the chapter summarize the important
points you’ve learned regarding each task.
– Regression uses different loss functions and different evaluation metrics
than classification.
You’ll usually need to preprocess raw data before feeding it into a neural network.
When your data has features with different ranges, scale each feature inde-
pendently as part of preprocessing.
As training progresses, neural networks eventually begin to overfit and obtain
worse results on never-before-seen data.
If you don’t have much training data, use a small model with only one or two
intermediate layers, to avoid severe overfitting.
If your data is divided into many categories, you may cause information bottle-
necks if you make the intermediate layers too small.
When you’re working with little data, K-fold validation can help reliably evalu-
ate your model.
Fundamentals
of machine learning
After the three practical examples in chapter 4, you should be starting to feel famil-
iar with how to approach classification and regression problems using neural net-
works, and you’ve witnessed the central problem of machine learning: overfitting.
This chapter will formalize some of your new intuition about machine learning into
a solid conceptual framework, highlighting the importance of accurate model eval-
uation and the balance between training and generalization.
121
122 CHAPTER 5 Fundamentals of machine learning
were trained on quickly became evident: after just a few epochs, performance on
never-before-seen data started diverging from performance on the training data,
which always improves as training progresses. The models started to overfit. Overfitting
happens in every machine learning problem.
The fundamental issue in machine learning is the tension between optimization
and generalization. Optimization refers to the process of adjusting a model to get the
best performance possible on the training data (the learning in machine learning),
whereas generalization refers to how well the trained model performs on data it has
never seen before. The goal of the game is to get good generalization, of course, but
you don’t control generalization; you can only fit the model to its training data. If you
do that too well, overfitting kicks in and generalization suffers.
But what causes overfitting? How can we achieve good generalization?
Training curve
Loss Validation curve
value
Underfitting
Overfitting
Robust fit
Training time
What are these? I don’t know either. But they’re all part of the MNIST training set.
What’s even worse, however, is having perfectly valid inputs that end up mislabeled,
like those in figure 5.3.
If a model goes out of its way to incorporate such outliers, its generalization perfor-
mance will degrade, as shown in figure 5.4. For instance, a 4 that looks very close to
the mislabeled 4 in figure 5.3 may end up getting classified as a 9.
124 CHAPTER 5 Fundamentals of machine learning
AMBIGUOUS FEATURES
Not all data noise comes from inaccuracies—even perfectly clean and neatly labeled
data can be noisy when the problem involves uncertainty and ambiguity. In classifica-
tion tasks, it is often the case that some regions of the input feature space are associ-
ated with multiple classes at the same time. Let’s say you’re developing a model that
takes an image of a banana and predicts whether the banana is unripe, ripe, or rotten.
These categories have no objective boundaries, so the same picture might be classified
as either unripe or ripe by different human labelers. Similarly, many problems involve
randomness. You could use atmospheric pressure data to predict whether it will rain
tomorrow, but the exact same measurements may be followed sometimes by rain and
sometimes by a clear sky, with some probability.
A model could overfit to such probabilistic data by being too confident about
ambiguous regions of the feature space, like in figure 5.5. A more robust fit would
ignore individual data points and look at the bigger picture.
Area of uncertainty
Figure 5.5 Robust fit vs. overfitting giving an ambiguous area of the feature space
Generalization: The goal of machine learning 125
train_images_with_noise_channels = [Link](
[train_images, [Link]((len(train_images), 784))], axis=1)
train_images_with_zeros_channels = [Link](
[train_images, [Link]((len(train_images), 784))], axis=1)
Now, let’s train the model from chapter 2 on both of these training sets.
1
Mark Twain even called it “the most delicious fruit known to men.”
126 CHAPTER 5 Fundamentals of machine learning
Listing 5.2 Training the same model on MNIST data with noise channels or all-zero channels
from tensorflow import keras
from [Link] import layers
def get_model():
model = [Link]([
[Link](512, activation="relu"),
[Link](10, activation="softmax")
])
[Link](optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
return model
model = get_model()
history_noise = [Link](
train_images_with_noise_channels, train_labels,
epochs=10,
batch_size=128,
validation_split=0.2)
model = get_model()
history_zeros = [Link](
train_images_with_zeros_channels, train_labels,
epochs=10,
batch_size=128,
validation_split=0.2)
Let’s compare how the validation accuracy of each model evolves over time.
Despite the data holding the same information in both cases, the validation accuracy
of the model trained with noise channels ends up about one percentage point lower
(see figure 5.6)—purely through the influence of spurious correlations. The more
noise channels you add, the further accuracy will degrade.
Noisy features inevitably lead to overfitting. As such, in cases where you aren’t sure
whether the features you have are informative or distracting, it’s common to do feature
Generalization: The goal of machine learning 127
selection before training. Restricting the IMDB data to the top 10,000 most common
words was a crude form of feature selection, for instance. The typical way to do fea-
ture selection is to compute some usefulness score for each feature available—a
measure of how informative the feature is with respect to the task, such as the
mutual information between the feature and the labels—and only keep features that
are above some threshold. Doing this would filter out the white noise channels in
the preceding example.
random_train_labels = train_labels[:]
[Link](random_train_labels)
model = [Link]([
[Link](512, activation="relu"),
128 CHAPTER 5 Fundamentals of machine learning
[Link](10, activation="softmax")
])
[Link](optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
[Link](train_images, random_train_labels,
epochs=100,
batch_size=128,
validation_split=0.2)
In fact, you don’t even need to do this with MNIST data—you could just generate
white noise inputs and random labels. You could fit a model on that, too, as long as it
has enough parameters. It would just end up memorizing specific inputs, much like a
Python dictionary.
If this is the case, then how come deep learning models generalize at all? Shouldn’t
they just learn an ad hoc mapping between training inputs and targets, like a fancy
dict? What expectation can we have that this mapping will work for new inputs?
As it turns out, the nature of generalization in deep learning has rather little to do
with deep learning models themselves, and much to do with the structure of informa-
tion in the real world. Let’s take a look at what’s really going on here.
THE MANIFOLD HYPOTHESIS
The input to an MNIST classifier (before preprocessing) is a 28 × 28 array of integers
between 0 and 255. The total number of possible input values is thus 256 to the power
of 784—much greater than the number of atoms in the universe. However, very few of
these inputs would look like valid MNIST samples: actual handwritten digits only
occupy a tiny subspace of the parent space of all possible 28 × 28 uint8 arrays. What’s
more, this subspace isn’t just a set of points sprinkled at random in the parent space: it
is highly structured.
First, the subspace of valid handwritten digits is continuous: if you take a sample
and modify it a little, it will still be recognizable as the same handwritten digit. Fur-
ther, all samples in the valid subspace are connected by smooth paths that run through
the subspace. This means that if you take two random MNIST digits A and B, there
exists a sequence of “intermediate” images that morph A into B, such that two consec-
utive digits are very close to each other (see figure 5.7). Perhaps there will be a few
ambiguous shapes close to the boundary between two classes, but even these shapes
would still look very digit-like.
In technical terms, you would say that handwritten digits form a manifold within
the space of possible 28 × 28 uint8 arrays. That’s a big word, but the concept is pretty
intuitive. A “manifold” is a lower-dimensional subspace of some parent space that is
locally similar to a linear (Euclidian) space. For instance, a smooth curve in the plane
is a 1D manifold within a 2D space, because for every point of the curve, you can draw
a tangent (the curve can be approximated by a line at every point). A smooth surface
within a 3D space is a 2D manifold. And so on.
Generalization: The goal of machine learning 129
Figure 5.7 Different MNIST digits gradually morphing into one another, showing that the space of
handwritten digits forms a “manifold.” This image was generated using code from chapter 12.
More generally, the manifold hypothesis posits that all natural data lies on a low-dimen-
sional manifold within the high-dimensional space where it is encoded. That’s a pretty
strong statement about the structure of information in the universe. As far as we know,
it’s accurate, and it’s the reason why deep learning works. It’s true for MNIST digits,
but also for human faces, tree morphology, the sounds of the human voice, and even
natural language.
The manifold hypothesis implies that
Machine learning models only have to fit relatively simple, low-dimensional,
highly structured subspaces within their potential input space (latent mani-
folds).
Within one of these manifolds, it’s always possible to interpolate between two
inputs, that is to say, morph one into another via a continuous path along which
all points fall on the manifold.
The ability to interpolate between samples is the key to understanding generalization
in deep learning.
INTERPOLATION AS A SOURCE OF GENERALIZATION
If you work with data points that can be interpolated, you can start making sense of
points you’ve never seen before by relating them to other points that lie close on the
manifold. In other words, you can make sense of the totality of the space using only a
sample of the space. You can use interpolation to fill in the blanks.
Note that interpolation on the latent manifold is different from linear interpola-
tion in the parent space, as illustrated in figure 5.8. For instance, the average of pixels
between two MNIST digits is usually not a valid digit.
Crucially, while deep learning achieves generalization via interpolation on a
learned approximation of the data manifold, it would be a mistake to assume that
interpolation is all there is to generalization. It’s the tip of the iceberg. Interpolation
can only help you make sense of things that are very close to what you’ve seen before:
130 CHAPTER 5 Fundamentals of machine learning
Manifold interpolation
(intermediate point
on the latent manifold)
Figure 5.8 Difference between
linear interpolation and interpolation
on the latent manifold. Every point on
Linear interpolation
the latent manifold of digits is a valid
(average in the encoding space) digit, but the average of two digits
usually isn’t.
it enables local generalization. But remarkably, humans deal with extreme novelty all the
time, and they do just fine. You don’t need to be trained in advance on countless
examples of every situation you’ll ever have to encounter. Every single one of your
days is different from any day you’ve experienced before, and different from any day
experienced by anyone since the dawn of humanity. You can switch between spending
a week in NYC, a week in Shanghai, and a week in Bangalore without requiring thou-
sands of lifetimes of learning and rehearsal for each city.
Humans are capable of extreme generalization, which is enabled by cognitive mecha-
nisms other than interpolation: abstraction, symbolic models of the world, reasoning,
logic, common sense, innate priors about the world—what we generally call reason, as
opposed to intuition and pattern recognition. The latter are largely interpolative in
nature, but the former isn’t. Both are essential to intelligence. We’ll talk more about
this in chapter 14.
WHY DEEP LEARNING WORKS
Remember the crumpled paper ball metaphor from chapter 2? A sheet of paper rep-
resents a 2D manifold within 3D space (see figure 5.9). A deep learning model is a
tool for uncrumpling paper balls, that is, for disentangling latent manifolds.
The curve involves enough parameters that it could fit anything—indeed, if you let
your model train for long enough, it will effectively end up purely memorizing its
training data and won’t generalize at all. However, the data you’re fitting to isn’t made
of isolated points sparsely distributed across the underlying space. Your data forms a
highly structured, low-dimensional manifold within the input space—that’s the mani-
fold hypothesis. And because fitting your model curve to this data happens gradually
and smoothly over time as gradient descent progresses, there will be an intermediate
point during training at which the model roughly approximates the natural manifold
of the data, as you can see in figure 5.10.
Figure 5.10 Going from a random model to an overfit model, and achieving a robust fit as an intermediate state
Moving along the curve learned by the model at that point will come close to moving
along the actual latent manifold of the data—as such, the model will be capable of
making sense of never-before-seen inputs via interpolation between training inputs.
Besides the trivial fact that they have sufficient representational power, there are a
few properties of deep learning models that make them particularly well-suited to
learning latent manifolds:
Deep learning models implement a smooth, continuous mapping from their
inputs to their outputs. It has to be smooth and continuous because it must
be differentiable, by necessity (you couldn’t do gradient descent otherwise).
132 CHAPTER 5 Fundamentals of machine learning
This smoothness helps approximate latent manifolds, which follow the same
properties.
Deep learning models tend to be structured in a way that mirrors the “shape” of
the information in their training data (via architecture priors). This is particu-
larly the case for image-processing models (discussed in chapters 8 and 9) and
sequence-processing models (chapter 10). More generally, deep neural net-
works structure their learned representations in a hierarchical and modular
way, which echoes the way natural data is organized.
TRAINING DATA IS PARAMOUNT
While deep learning is indeed well suited to manifold learning, the power to general-
ize is more a consequence of the natural structure of your data than a consequence of
any property of your model. You’ll only be able to generalize if your data forms a man-
ifold where points can be interpolated. The more informative and the less noisy your
features are, the better you will be able to generalize, since your input space will be
simpler and better structured. Data curation and feature engineering are essential to
generalization.
Further, because deep learning is curve fitting, for a model to perform well it needs
to be trained on a dense sampling of its input space. A “dense sampling” in this context
means that the training data should densely cover the entirety of the input data
manifold (see figure 5.11). This is especially true near decision boundaries. With a
sufficiently dense sampling, it becomes possible to make sense of new inputs by inter-
polating between past training inputs without having to use common sense, abstract
reasoning, or external knowledge about the world—all things that machine learning
models have no access to.
Figure 5.11 A dense sampling of the input space is necessary in order to learn a model
capable of accurate generalization.
Evaluating machine learning models 133
As such, you should always keep in mind that the best way to improve a deep learning
model is to train it on more data or better data (of course, adding overly noisy or inac-
curate data will harm generalization). A denser coverage of the input data manifold
will yield a model that generalizes better. You should never expect a deep learning
model to perform anything more than crude interpolation between its training sam-
ples, and thus you should do everything you can to make interpolation as easy as pos-
sible. The only thing you will find in a deep learning model is what you put into it: the
priors encoded in its architecture and the data it was trained on.
When getting more data isn’t possible, the next best solution is to modulate the
quantity of information that your model is allowed to store, or to add constraints on
the smoothness of the model curve. If a network can only afford to memorize a small
number of patterns, or very regular patterns, the optimization process will force it to
focus on the most prominent patterns, which have a better chance of generalizing
well. The process of fighting overfitting this way is called regularization. We’ll review
regularization techniques in depth in section 5.4.4.
Before you can start tweaking your model to help it generalize better, you’ll need a
way to assess how your model is currently doing. In the following section, you’ll learn
how you can monitor generalization during model development: model evaluation.
Central to this phenomenon is the notion of information leaks. Every time you tune
a hyperparameter of your model based on the model’s performance on the validation
set, some information about the validation data leaks into the model. If you do this
only once, for one parameter, then very few bits of information will leak, and your val-
idation set will remain reliable for evaluating the model. But if you repeat this many
times—running one experiment, evaluating on the validation set, and modifying your
model as a result—then you’ll leak an increasingly significant amount of information
about the validation set into the model.
At the end of the day, you’ll end up with a model that performs artificially well on
the validation data, because that’s what you optimized it for. You care about perfor-
mance on completely new data, not on the validation data, so you need to use a com-
pletely different, never-before-seen dataset to evaluate the model: the test dataset.
Your model shouldn’t have had access to any information about the test set, even indi-
rectly. If anything about the model has been tuned based on test set performance,
then your measure of generalization will be flawed.
Splitting your data into training, validation, and test sets may seem straightforward,
but there are a few advanced ways to do it that can come in handy when little data is
available. Let’s review three classic evaluation recipes: simple holdout validation, K-fold
validation, and iterated K-fold validation with shuffling. We’ll also talk about the use
of common-sense baselines to check that your training is going somewhere.
SIMPLE HOLDOUT VALIDATION
Set apart some fraction of your data as your test set. Train on the remaining data, and
evaluate on the test set. As you saw in the previous sections, in order to prevent infor-
mation leaks, you shouldn’t tune your model based on the test set, and therefore you
should also reserve a validation set.
Schematically, holdout validation looks like figure 5.12. Listing 5.5 shows a simple
implementation.
Held-out
Training set validation
set
Listing 5.5 Holdout validation (note that labels are omitted for simplicity)
num_validation_samples = 10000
Shuffling the data is
usually appropriate.
[Link](data)
Evaluating machine learning models 135
validation_data = data[:num_validation_samples]
Defines the Defines the training set
validation training_data = data[num_validation_samples:]
set model = get_model() Trains a model on the
[Link](training_data, ...) training data, and evaluates
validation_score = [Link](validation_data, ...) it on the validation data
This is the simplest evaluation protocol, and it suffers from one flaw: if little data is
available, then your validation and test sets may contain too few samples to be statisti-
cally representative of the data at hand. This is easy to recognize: if different random
shuffling rounds of the data before splitting end up yielding very different measures
of model performance, then you’re having this issue. K-fold validation and iterated
K-fold validation are two ways to address this, as discussed next.
K-FOLD VALIDATION
With this approach, you split your data into K partitions of equal size. For each parti-
tion i, train a model on the remaining K - 1 partitions, and evaluate it on partition i.
Your final score is then the averages of the K scores obtained. This method is helpful
when the performance of your model shows significant variance based on your train-
test split. Like holdout validation, this method doesn’t exempt you from using a dis-
tinct validation set for model calibration.
Schematically, K-fold cross-validation looks like figure 5.13. Listing 5.6 shows a sim-
ple implementation.
Validation
Fold 1 Validation Training Training
score #1
Validation
Fold 3 Training Training Validation
score #3
Listing 5.6 K-fold cross-validation (note that labels are omitted for simplicity)
k = 3
num_validation_samples = len(data) // k
[Link](data)
validation_scores = [] Selects the
for fold in range(k): validation-
validation_data = data[num_validation_samples * fold: data partition
num_validation_samples * (fold + 1)]
training_data = [Link](
data[:num_validation_samples * fold], Creates a brand-new
data[num_validation_samples * (fold + 1):]) instance of the model
model = get_model() (untrained)
[Link](training_data, ...)
validation_score = [Link](validation_data, ...) Validation score:
validation_scores.append(validation_score) average of the validation
validation_score = [Link](validation_scores) scores of the k folds
model = get_model()
[Link](data, ...) Trains the final
test_score = [Link](test_data, ...) model on all non-
test data available
Uses the remainder of the data as training
data. Note that the + operator represents
list concatenation, not summation.
Listing 5.7 Training an MNIST model with an incorrectly high learning rate
model = [Link]([
[Link](512, activation="relu"),
[Link](10, activation="softmax")
])
[Link](optimizer=[Link](1.),
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
[Link](train_images, train_labels,
epochs=10,
Improving model fit 139
batch_size=128,
validation_split=0.2)
The model quickly reaches a training and validation accuracy in the 30%–40% range,
but cannot get past that. Let’s try to lower the learning rate to a more reasonable value
of 1e-2.
Listing 5.8 The same model with a more appropriate learning rate
model = [Link]([
[Link](512, activation="relu"),
[Link](10, activation="softmax")
])
[Link](optimizer=[Link](1e-2),
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
[Link](train_images, train_labels,
epochs=10,
batch_size=128,
validation_split=0.2)
problem where a densely connected architecture isn’t able to beat a trivial baseline,
whereas a more appropriate recurrent architecture does manage to generalize well.
Using a model that makes the right assumptions about the problem is essential to
achieve generalization: you should leverage the right architecture priors.
In the following chapters, you’ll learn about the best architectures to use for a vari-
ety of data modalities—images, text, timeseries, and so on. In general, you should
always make sure to read up on architecture best practices for the kind of task you’re
attacking—chances are you’re not the first person to attempt it.
Validation metrics seem to stall, or to improve very slowly, instead of peaking and
reversing course. The validation loss goes to 0.26 and just stays there. You can fit, but
you can’t clearly overfit, even after many iterations over the training data. You’re likely
to encounter similar curves often in your career.
Remember that it should always be possible to overfit. Much like the problem
where the training loss doesn’t go down, this is an issue that can always be solved. If
Improving model fit 141
you can’t seem to be able to overfit, it’s likely a problem with the representational
power of your model: you’re going to need a bigger model, one with more capacity,
that is to say, one able to store more information. You can increase representational
power by adding more layers, using bigger layers (layers with more parameters), or
using kinds of layers that are more appropriate for the problem at hand (better
architecture priors).
Let’s try training a bigger model, one with two intermediate layers with 96 units
each:
model = [Link]([
[Link](96, activation="relu"),
[Link](96, activation="relu"),
[Link](10, activation="softmax"),
])
[Link](optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
history_large_model = [Link](
train_images, train_labels,
epochs=20,
batch_size=128,
validation_split=0.2)
The validation curve now looks exactly like it should: the model fits fast and starts
overfitting after 8 epochs (see figure 5.15).
142 CHAPTER 5 Fundamentals of machine learning
Raw data:
pixel grid
If you choose to use the raw pixels of the image as input data, you have a difficult
machine learning problem on your hands. You’ll need a convolutional neural net-
work to solve it, and you’ll have to expend quite a bit of computational resources to
train the network.
But if you already understand the problem at a high level (you understand how
humans read time on a clock face), you can come up with much better input features
for a machine learning algorithm: for instance, it’s easy to write a five-line Python
script to follow the black pixels of the clock hands and output the (x, y) coordinates
of the tip of each hand. Then a simple machine learning algorithm can learn to asso-
ciate these coordinates with the appropriate time of day.
You can go even further: do a coordinate change, and express the (x, y) coordi-
nates as polar coordinates with regard to the center of the image. Your input will
become the angle theta of each clock hand. At this point, your features are making
the problem so easy that no machine learning is required; a simple rounding opera-
tion and dictionary lookup are enough to recover the approximate time of day.
That’s the essence of feature engineering: making a problem easier by expressing
it in a simpler way. Make the latent manifold smoother, simpler, better organized.
Doing so usually requires understanding the problem in depth.
144 CHAPTER 5 Fundamentals of machine learning
Before deep learning, feature engineering used to be the most important part of
the machine learning workflow, because classical shallow algorithms didn’t have
hypothesis spaces rich enough to learn useful features by themselves. The way you pre-
sented the data to the algorithm was absolutely critical to its success. For instance,
before convolutional neural networks became successful on the MNIST digit-classifi-
cation problem, solutions were typically based on hardcoded features such as the
number of loops in a digit image, the height of each digit in an image, a histogram of
pixel values, and so on.
Fortunately, modern deep learning removes the need for most feature engineer-
ing, because neural networks are capable of automatically extracting useful features
from raw data. Does this mean you don’t have to worry about feature engineering as
long as you’re using deep neural networks? No, for two reasons:
Good features still allow you to solve problems more elegantly while using fewer
resources. For instance, it would be ridiculous to solve the problem of reading a
clock face using a convolutional neural network.
Good features let you solve a problem with far less data. The ability of deep
learning models to learn features on their own relies on having lots of training
data available; if you have only a few samples, the information value in their fea-
tures becomes critical.
model = [Link]([
[Link](16, activation="relu"),
[Link](16, activation="relu"),
[Link](1, activation="sigmoid")
])
[Link](optimizer="rmsprop",
loss="binary_crossentropy",
metrics=["accuracy"])
history_original = [Link](train_data, train_labels,
epochs=20, batch_size=512, validation_split=0.4)
model = [Link]([
[Link](4, activation="relu"),
[Link](4, activation="relu"),
[Link](1, activation="sigmoid")
])
[Link](optimizer="rmsprop",
loss="binary_crossentropy",
metrics=["accuracy"])
history_smaller_model = [Link](
train_data, train_labels,
epochs=20, batch_size=512, validation_split=0.4)
Figure 5.17 shows a comparison of the validation losses of the original model and the
smaller model.
Figure 5.17 Original model vs. smaller model on IMDB review classification
Improving generalization 147
As you can see, the smaller model starts overfitting later than the reference model
(after six epochs rather than four), and its performance degrades more slowly once it
starts overfitting.
Now, let’s add to our benchmark a model that has much more capacity—far more
than the problem warrants. While it is standard to work with models that are signifi-
cantly overparameterized for what they’re trying to learn, there can definitely be such
a thing as too much memorization capacity. You’ll know your model is too large if it
starts overfitting right away and if its validation loss curve looks choppy with high-
variance (although choppy validation metrics could also be a symptom of using an
unreliable validation process, such as a validation split that’s too small).
model = [Link]([
[Link](512, activation="relu"),
[Link](512, activation="relu"),
[Link](1, activation="sigmoid")
])
[Link](optimizer="rmsprop",
loss="binary_crossentropy",
metrics=["accuracy"])
history_larger_model = [Link](
train_data, train_labels,
epochs=20, batch_size=512, validation_split=0.4)
Figure 5.18 shows how the bigger model fares compared with the reference model.
Figure 5.18 Original model vs. much larger model on IMDB review
classification
148 CHAPTER 5 Fundamentals of machine learning
The bigger model starts overfitting almost immediately, after just one epoch, and it
overfits much more severely. Its validation loss is also noisier. It gets training loss near
zero very quickly. The more capacity the model has, the more quickly it can model the
training data (resulting in a low training loss), but the more susceptible it is to overfit-
ting (resulting in a large difference between the training and validation loss).
ADDING WEIGHT REGULARIZATION
You may be familiar with the principle of Occam’s razor: given two explanations for
something, the explanation most likely to be correct is the simplest one—the one that
makes fewer assumptions. This idea also applies to the models learned by neural net-
works: given some training data and a network architecture, multiple sets of weight
values (multiple models) could explain the data. Simpler models are less likely to over-
fit than complex ones.
A simple model in this context is a model where the distribution of parameter values
has less entropy (or a model with fewer parameters, as you saw in the previous sec-
tion). Thus, a common way to mitigate overfitting is to put constraints on the com-
plexity of a model by forcing its weights to take only small values, which makes the
distribution of weight values more regular. This is called weight regularization, and it’s
done by adding to the loss function of the model a cost associated with having large
weights. This cost comes in two flavors:
L1 regularization—The cost added is proportional to the absolute value of the
weight coefficients (the L1 norm of the weights).
L2 regularization—The cost added is proportional to the square of the value of the
weight coefficients (the L2 norm of the weights). L2 regularization is also called
weight decay in the context of neural networks. Don’t let the different name con-
fuse you: weight decay is mathematically the same as L2 regularization.
history_l2_reg = [Link](
train_data, train_labels,
epochs=20, batch_size=512, validation_split=0.4)
In the preceding listing, l2(0.002) means every coefficient in the weight matrix of
the layer will add 0.002 * weight_coefficient_value ** 2 to the total loss of the
model. Note that because this penalty is only added at training time, the loss for this
model will be much higher at training than at test time.
Figure 5.19 shows the impact of the L2 regularization penalty. As you can see, the
model with L2 regularization has become much more resistant to overfitting than the
reference model, even though both models have the same number of parameters.
As an alternative to L2 regularization, you can use one of the following Keras weight
regularizers.
Note that weight regularization is more typically used for smaller deep learning mod-
els. Large deep learning models tend to be so overparameterized that imposing con-
straints on weight values hasn’t much impact on model capacity and generalization. In
these cases, a different regularization technique is preferred: dropout.
150 CHAPTER 5 Fundamentals of machine learning
ADDING DROPOUT
Dropout is one of the most effective and most commonly used regularization tech-
niques for neural networks; it was developed by Geoff Hinton and his students at the
University of Toronto. Dropout, applied to a layer, consists of randomly dropping out
(setting to zero) a number of output features of the layer during training. Let’s say a
given layer would normally return a vector [0.2, 0.5, 1.3, 0.8, 1.1] for a given
input sample during training. After applying dropout, this vector will have a few zero
entries distributed at random: for example, [0, 0.5, 1.3, 0, 1.1]. The dropout rate
is the fraction of the features that are zeroed out; it’s usually set between 0.2 and 0.5.
At test time, no units are dropped out; instead, the layer’s output values are scaled
down by a factor equal to the dropout rate, to balance for the fact that more units are
active than at training time.
Consider a NumPy matrix containing the output of a layer, layer_output, of
shape (batch_size, features). At training time, we zero out at random a fraction of
the values in the matrix:
At test time, we scale down the output by the dropout rate. Here, we scale by 0.5
(because we previously dropped half the units):
Note that this process can be implemented by doing both operations at training time
and leaving the output unchanged at test time, which is often the way it’s imple-
mented in practice (see figure 5.20): At training time
layer_output *= [Link](0, high=2, size=layer_output.shape)
layer_output /= 0.5
Note that we’re scaling up rather
than scaling down in this case.
This technique may seem strange and arbitrary. Why would this help reduce overfit-
ting? Hinton says he was inspired by, among other things, a fraud-prevention mecha-
nism used by banks. In his own words, “I went to my bank. The tellers kept changing
and I asked one of them why. He said he didn’t know but they got moved around a lot.
Improving generalization 151
model = [Link]([
[Link](16, activation="relu"),
[Link](0.5),
[Link](16, activation="relu"),
[Link](0.5),
[Link](1, activation="sigmoid")
])
[Link](optimizer="rmsprop",
loss="binary_crossentropy",
metrics=["accuracy"])
history_dropout = [Link](
train_data, train_labels,
epochs=20, batch_size=512, validation_split=0.4)
Figure 5.21 shows a plot of the results. This is a clear improvement over the reference
model—it also seems to be working much better than L2 regularization, since the low-
est validation loss reached has improved.
To recap, these are the most common ways to maximize generalization and prevent
overfitting in neural networks:
Get more training data, or better training data.
Develop better features.
Reduce the capacity of the model.
Add weight regularization (for smaller models).
Add dropout.
Summary
The purpose of a machine learning model is to generalize: to perform accurately
on never-before-seen inputs. It’s harder than it seems.
A deep neural network achieves generalization by learning a parametric model
that can successfully interpolate between training samples—such a model can be
said to have learned the “latent manifold” of the training data. This is why deep
learning models can only make sense of inputs that are very close to what
they’ve seen during training.
The fundamental problem in machine learning is the tension between optimization
and generalization: to attain generalization, you must first achieve a good fit to
the training data, but improving your model’s fit to the training data will inevi-
tably start hurting generalization after a while. Every single deep learning best
practice deals with managing this tension.
The ability of deep learning models to generalize comes from the fact that they
manage to learn to approximate the latent manifold of their data, and can thus
make sense of new inputs via interpolation.
It’s essential to be able to accurately evaluate the generalization power of your
model while you’re developing it. You have at your disposal an array of evalua-
tion methods, from simple holdout validation to K-fold cross-validation and
iterated K-fold cross-validation with shuffling. Remember to always keep a com-
pletely separate test set for final model evaluation, since information leaks from
your validation data to your model may have occurred.
When you start working on a model, your goal is first to achieve a model that
has some generalization power and that can overfit. Best practices for doing
this include tuning your learning rate and batch size, leveraging better architec-
ture priors, increasing model capacity, or simply training longer.
As your model starts overfitting, your goal switches to improving generalization
through model regularization. You can reduce your model’s capacity, add dropout
or weight regularization, and use early stopping. And naturally, a larger or bet-
ter dataset is always the number one way to help a model generalize.
The universal workflow
of machine learning
Our previous examples have assumed that we already had a labeled dataset to start
from, and that we could immediately start training a model. In the real world, this
is often not the case. You don’t start from a dataset, you start from a problem.
Imagine that you’re starting your own machine learning consulting shop. You
incorporate, you put up a fancy website, you notify your network. The projects start
rolling in:
A personalized photo search engine for a picture-sharing social network—
type in “wedding” and retrieve all the pictures you took at weddings, without
any manual tagging needed.
Flagging spam and offensive text content among the posts of a budding
chat app.
Building a music recommendation system for users of an online radio.
Detecting credit card fraud for an e-commerce website.
153
154 CHAPTER 6 The universal workflow of machine learning
Note on ethics
You may sometimes be offered ethically dubious projects, such as “building an AI
that rates the trustworthiness of someone from a picture of their face.” First of all,
the validity of the project is in doubt: it isn’t clear why trustworthiness would be
reflected on someone’s face. Second, such a task opens the door to all kinds of eth-
ical problems. Collecting a dataset for this task would amount to recording the biases
and prejudices of the people who label the pictures. The models you would train on
such data would merely encode these same biases into a black-box algorithm that
would give them a thin veneer of legitimacy. In a largely tech-illiterate society like
ours, “the AI algorithm said this person cannot be trusted” strangely appears to carry
more weight and objectivity than “John Smith said this person cannot be trusted,”
despite the former being a learned approximation of the latter. Your model would be
laundering and operationalizing at scale the worst aspects of human judgement, with
negative effects on the lives of real people.
Technology is never neutral. If your work has any impact on the world, this impact has
a moral direction: technical choices are also ethical choices. Always be deliberate
about the values you want your work to support.
It would be very convenient if you could import the correct dataset from [Link]-
sets and start fitting some deep learning models. Unfortunately, in the real world
you’ll have to start from scratch.
In this chapter, you’ll learn about a universal step-by-step blueprint that you can
use to approach and solve any machine learning problem, like those in the previous
list. This template will bring together and consolidate everything you’ve learned in
chapters 4 and 5, and will give you the wider context that should anchor what you’ll
learn in the next chapters.
The universal workflow of machine learning is broadly structured in three parts:
1 Define the task—Understand the problem domain and the business logic under-
lying what the customer asked for. Collect a dataset, understand what the data
represents, and choose how you will measure success on the task.
2 Develop a model—Prepare your data so that it can be processed by a machine
learning model, select a model evaluation protocol and a simple baseline to
beat, train a first model that has generalization power and that can overfit, and
then regularize and tune your model until you achieve the best possible gener-
alization performance.
3 Deploy the model—Present your work to stakeholders, ship the model to a web
server, a mobile app, a web page, or an embedded device, monitor the model’s
Define the task 155
performance in the wild, and start collecting the data you’ll need to build the
next-generation model.
Let’s dive in.
What do existing solutions look like? Perhaps your customer already has a
handcrafted algorithm that handles spam filtering or credit card fraud detec-
tion, with lots of nested if statements. Perhaps a human is currently in charge
of manually handling the process under consideration—monitoring the con-
veyor belt at the cookie plant and manually removing the bad cookies, or craft-
ing playlists of song recommendations to be sent out to users who liked a
specific artist. You should make sure you understand what systems are already in
place and how they work.
Are there particular constraints you will need to deal with? For example, you
could find out that the app for which you’re building a spam detection system is
strictly end-to-end encrypted, so that the spam detection model will have to live
on the end user’s phone and must be trained on an external dataset. Perhaps
the cookie-filtering model has such latency constraints that it will need to run
on an embedded device at the factory rather than on a remote server. You
should understand the full context in which your work will fit.
Once you’ve done your research, you should know what your inputs will be, what your
targets will be, and what broad type of machine learning task the problem maps to. Be
aware of the hypotheses you’re making at this stage:
You hypothesize that your targets can be predicted given your inputs.
You hypothesize that the data that’s available (or that you will soon collect) is
sufficiently informative to learn the relationship between inputs and targets.
Until you have a working model, these are merely hypotheses, waiting to be validated
or invalidated. Not all problems can be solved with machine learning; just because
you’ve assembled examples of inputs X and targets Y doesn’t mean X contains enough
information to predict Y. For instance, if you’re trying to predict the movements of a
stock on the stock market given its recent price history, you’re unlikely to succeed,
because price history doesn’t contain much predictive information.
For the music recommendation engine, you can just use the “likes” of your
users. No new data needs to be collected. Likewise for the click-through-rate
prediction project: you have an extensive record of click-through rate for your
past ads, going back years.
For the cookie-flagging model, you will need to install cameras above the con-
veyor belts to collect tens of thousands of images, and then someone will need
to manually label these images. The people who know how to do this currently
work at the cookie factory, but it doesn’t seem too difficult. You should be able
to train people to do it.
The satellite imagery project will require a team of archeologists to collect a
database of existing sites of interest, and for each site you will need to find exist-
ing satellite images taken in different weather conditions. To get a good model,
you’re going to need thousands of different sites.
You learned in chapter 5 that a model’s ability to generalize comes almost entirely
from the properties of the data it is trained on—the number of data points you have,
the reliability of your labels, the quality of your features. A good dataset is an asset wor-
thy of care and investment. If you get an extra 50 hours to spend on a project, chances
are that the most effective way to allocate them is to collect more data rather than
search for incremental modeling improvements.
The point that data matters more than algorithms was most famously made in a
2009 paper by Google researchers titled “The Unreasonable Effectiveness of Data”
(the title is a riff on the well-known 1960 article “The Unreasonable Effectiveness of
Mathematics in the Natural Sciences” by Eugene Wigner). This was before deep learn-
ing was popular, but, remarkably, the rise of deep learning has only made the impor-
tance of data greater.
If you’re doing supervised learning, then once you’ve collected inputs (such as
images) you’re going to need annotations for them (such as tags for those images)—
the targets you will train your model to predict. Sometimes, annotations can be retrieved
automatically, such as those for the music recommendation task or the click-through-
rate prediction task. But often you have to annotate your data by hand. This is a labor-
heavy process.
INVESTING IN DATA ANNOTATION INFRASTRUCTURE
Your data annotation process will determine the quality of your targets, which in turn
determine the quality of your model. Carefully consider the options you have available:
Should you annotate the data yourself?
Should you use a crowdsourcing platform like Mechanical Turk to collect labels?
Should you use the services of a specialized data-labeling company?
Outsourcing can potentially save you time and money, but it takes away control. Using
something like Mechanical Turk is likely to be inexpensive and to scale well, but your
annotations may end up being quite noisy.
158 CHAPTER 6 The universal workflow of machine learning
To pick the best option, consider the constraints you’re working with:
Do the data labelers need to be subject matter experts, or could anyone anno-
tate the data? The labels for a cat-versus-dog image classification problem can
be selected by anyone, but those for a dog breed classification task require spe-
cialized knowledge. Meanwhile, annotating CT scans of bone fractures pretty
much requires a medical degree.
If annotating the data requires specialized knowledge, can you train people to
do it? If not, how can you get access to relevant experts?
Do you, yourself, understand the way experts come up with the annotations? If
you don’t, you will have to treat your dataset as a black box, and you won’t be able
to perform manual feature engineering—this isn’t critical, but it can be limiting.
If you decide to label your data in-house, ask yourself what software you will use to record
annotations. You may well need to develop that software yourself. Productive data anno-
tation software will save you a lot of time, so it’s worth investing in it early in a project.
BEWARE OF NON-REPRESENTATIVE DATA
Machine learning models can only make sense of inputs that are similar to what
they’ve seen before. As such, it’s critical that the data used for training should be repre-
sentative of the production data. This concern should be the foundation of all your
data collection work.
Suppose you’re developing an app where users can take pictures of a plate of food to
find out the name of the dish. You train a model using pictures from an image-sharing
social network that’s popular with foodies. Come deployment time, feedback from
angry users starts rolling in: your app gets the answer wrong 8 times out of 10. What’s
going on? Your accuracy on the test set was well over 90%! A quick look at user-uploaded
data reveals that mobile picture uploads of random dishes from random restaurants
taken with random smartphones look nothing like the professional-quality, well-lit,
appetizing pictures you trained the model on: your training data wasn’t representative of the
production data. That’s a cardinal sin—welcome to machine learning hell.
If possible, collect data directly from the environment where your model will be
used. A movie review sentiment classification model should be used on new IMDB
reviews, not on Yelp restaurant reviews, nor on Twitter status updates. If you want to
rate the sentiment of a tweet, start by collecting and annotating actual tweets from a
similar set of users as those you’re expecting in production. If it’s not possible to train
on production data, then make sure you fully understand how your training and pro-
duction data differ, and that you are actively correcting for these differences.
A related phenomenon you should be aware of is concept drift. You’ll encounter
concept drift in almost all real-world problems, especially those that deal with user-
generated data. Concept drift occurs when the properties of the production data
change over time, causing model accuracy to gradually decay. A music recommenda-
tion engine trained in the year 2013 may not be very effective today. Likewise, the
IMDB dataset you worked with was collected in 2011, and a model trained on it would
Define the task 159
likely not perform as well on reviews from 2020 compared to reviews from 2012, as
vocabulary, expressions, and movie genres evolve over time. Concept drift is particu-
larly acute in adversarial contexts like credit card fraud detection, where fraud pat-
terns change practically every day. Dealing with fast concept drift requires constant
data collection, annotation, and model retraining.
Keep in mind that machine learning can only be used to memorize patterns that
are present in your training data. You can only recognize what you’ve seen before.
Using machine learning trained on past data to predict the future is making the
assumption that the future will behave like the past. That often isn’t the case.
Nowadays, every phone survey takes sampling bias into account. That doesn’t mean
that sampling bias is a thing of the past in political polling—far from it. But unlike in
1948, pollsters are aware of it and take steps to correct it.
160 CHAPTER 6 The universal workflow of machine learning
from converging. To make learning easier for your network, your data should have
the following characteristics:
Take small values—Typically, most values should be in the 0–1 range.
Be homogenous—All features should take values in roughly the same range.
Additionally, the following stricter normalization practice is common and can help,
although it isn’t always necessary (for example, we didn’t do this in the digit-classifica-
tion example):
Normalize each feature independently to have a mean of 0.
Normalize each feature independently to have a standard deviation of 1.
This is easy to do with NumPy arrays:
x -= [Link](axis=0)
Assuming x is a 2D data matrix
x /= [Link](axis=0) of shape (samples, features)
success metric of choice (such as accuracy) will be on actual production data. The reli-
ability of that process is critical to building a useful model.
In chapter 5, we reviewed three common evaluation protocols:
Maintaining a holdout validation set—This is the way to go when you have plenty
of data.
Doing K-fold cross-validation—This is the right choice when you have too few sam-
ples for holdout validation to be reliable.
Doing iterated K-fold validation—This is for performing highly accurate model
evaluation when little data is available.
Pick one of these. In most cases, the first will work well enough. As you learned,
though, always be mindful of the representativity of your validation set, and be careful
not to have redundant samples between your training set and your validation set.
(continued)
Choosing the right last-layer activation and loss function for your model
For most problems, there are existing templates you can start from. You’re not the
first person to try to build a spam detector, a music recommendation engine, or an
image classifier. Make sure you research prior art to identify the feature engineering
techniques and model architectures that are most likely to perform well on your task.
Note that it’s not always possible to achieve statistical power. If you can’t beat a sim-
ple baseline after trying multiple reasonable architectures, it may be that the answer
to the question you’re asking isn’t present in the input data. Remember that you’re
making two hypotheses:
You hypothesize that your outputs can be predicted given your inputs.
You hypothesize that the available data is sufficiently informative to learn the
relationship between inputs and outputs.
It may well be that these hypotheses are false, in which case you must go back to the
drawing board.
exercising human-like common sense in the context of the task. To address this, you
should consider showing some examples of the failure modes of your model (for
instance, show what incorrectly classified samples look like, especially those for which
the misclassification seems surprising).
They might also expect human-level performance, especially for processes that were
previously handled by people. Most machine learning models, because they are (imper-
fectly) trained to approximate human-generated labels, do not nearly get there. You
should clearly convey model performance expectations. Avoid using abstract statements
like “The model has 98% accuracy” (which most people mentally round up to 100%),
and prefer talking, for instance, about false negative rates and false positive rates. You
could say, “With these settings, the fraud detection model would have a 5% false nega-
tive rate and a 2.5% false positive rate. Every day, an average of 200 valid transactions
would be flagged as fraudulent and sent for manual review, and an average of 14 fraudu-
lent transactions would be missed. An average of 266 fraudulent transactions would be
correctly caught.” Clearly relate the model’s performance metrics to business goals.
You should also make sure to discuss with stakeholders the choice of key launch
parameters—for instance, the probability threshold at which a transaction should be
flagged (different thresholds will produce different false negative and false positive
rates). Such decisions involve trade-offs that can only be handled with a deep under-
standing of the business context.
For instance, the image search engine project, the music recommender system, the
credit card fraud detection project, and the satellite imagery project are all good fits
for serving via a REST API.
An important question when deploying a model as a REST API is whether you
want to host the code on your own, or whether you want to use a fully managed third-
party cloud service. For instance, Cloud AI Platform, a Google product, lets you simply
upload your TensorFlow model to Google Cloud Storage (GCS), and it gives you an
API endpoint to query it. It takes care of many practical details such as batching pre-
dictions, load balancing, and scaling.
DEPLOYING A MODEL ON A DEVICE
Sometimes, you may need your model to live on the same device that runs the applica-
tion that uses it—maybe a smartphone, an embedded ARM CPU on a robot, or a
microcontroller on a tiny device. You may have seen a camera capable of automati-
cally detecting people and faces in the scenes you pointed it at: that was probably a
small deep learning model running directly on the camera.
You should use this setup when
Your model has strict latency constraints or needs to run in a low-connectivity
environment. If you’re building an immersive augmented reality application,
querying a remote server is not a viable option.
Your model can be made sufficiently small that it can run under the memory and
power constraints of the target device. You can use the TensorFlow Model Opti-
mization Toolkit to help with this ([Link]/model_optimization).
Getting the highest possible accuracy isn’t mission critical for your task. There
is always a trade-off between runtime efficiency and accuracy, so memory and
power constraints often require you to ship a model that isn’t quite as good as
the best model you could run on a large GPU.
The input data is strictly sensitive and thus shouldn’t be decryptable on a
remote server.
168 CHAPTER 6 The universal workflow of machine learning
Our spam detection model will need to run on the end user’s smartphone as part of
the chat app, because messages are end-to-end encrypted and thus cannot be read by
a remotely hosted model. Likewise, the bad-cookie detection model has strict latency
constraints and will need to run at the factory. Thankfully, in this case, we don’t have
any power or space constraints, so we can actually run the model on a GPU.
To deploy a Keras model on a smartphone or embedded device, your go-to solution
is TensorFlow Lite ([Link]/lite). It’s a framework for efficient on-device
deep learning inference that runs on Android and iOS smartphones, as well as ARM64-
based computers, Raspberry Pi, or certain microcontrollers. It includes a converter that
can straightforwardly turn your Keras model into the TensorFlow Lite format.
DEPLOYING A MODEL IN THE BROWSER
Deep learning is often used in browser-based or desktop-based JavaScript applications.
While it is usually possible to have the application query a remote model via a REST
API, there can be key advantages in having the model run directly in the browser, on
the user’s computer (utilizing GPU resources if they’re available).
Use this setup when
You want to offload compute to the end user, which can dramatically reduce
server costs.
The input data needs to stay on the end user’s computer or phone. For
instance, in our spam detection project, the web version and the desktop ver-
sion of the chat app (implemented as a cross-platform app written in Java-
Script) should use a locally run model.
Your application has strict latency constraints. While a model running on the end
user’s laptop or smartphone is likely to be slower than one running on a large
GPU on your own server, you don’t have the extra 100 ms of network round trip.
You need your app to keep working without connectivity, after the model has
been downloaded and cached.
You should only go with this option if your model is small enough that it won’t hog the
CPU, GPU, or RAM of your user’s laptop or smartphone. In addition, since the entire
model will be downloaded to the user’s device, you should make sure that nothing
about the model needs to stay confidential. Be mindful of the fact that, given a trained
deep learning model, it is usually possible to recover some information about the train-
ing data: better not to make your trained model public if it was trained on sensitive data.
To deploy a model in JavaScript, the TensorFlow ecosystem includes [Link]
([Link]/js), a JavaScript library for deep learning that implements
almost all of the Keras API (originally developed under the working name WebKeras)
as well as many lower-level TensorFlow APIs. You can easily import a saved Keras
model into [Link] to query it as part of your browser-based JavaScript app or
your desktop Electron app.
Deploy the model 169
Summary
When you take on a new machine learning project, first define the problem at
hand:
– Understand the broader context of what you’re setting out to do—what’s the
end goal and what are the constraints?
– Collect and annotate a dataset; make sure you understand your data in depth.
– Choose how you’ll measure success for your problem—what metrics will you
monitor on your validation data?
Once you understand the problem and you have an appropriate dataset, develop
a model:
– Prepare your data.
– Pick your evaluation protocol: holdout validation? K-fold validation? Which
portion of the data should you use for validation?
– Achieve statistical power: beat a simple baseline.
– Scale up: develop a model that can overfit.
Summary 171
You’ve now got some experience with Keras—you’re familiar with the Sequential
model, Dense layers, and built-in APIs for training, evaluation, and inference—
compile(), fit(), evaluate(), and predict(). You even learned in chapter 3 how
to inherit from the Layer class to create custom layers, and how to use the Tensor-
Flow GradientTape to implement a step-by-step training loop.
In the coming chapters, we’ll dig into computer vision, timeseries forecast-
ing, natural language processing, and generative deep learning. These complex
applications will require much more than a Sequential architecture and the
default fit() loop. So let’s first turn you into a Keras expert! In this chapter,
you’ll get a complete overview of the key ways to work with Keras APIs: everything
172
Different ways to build Keras models 173
you’re going to need to handle the advanced deep learning use cases you’ll encoun-
ter next.
Functional API
+ custom layers
+ custom metrics Subclassing:
Sequential API Functional API + custom losses write everything
+ built-in layers + built-in layers + ... yourself from scratch
model = [Link]([
[Link](64, activation="relu"),
[Link](10, activation="softmax")
])
Note that it’s possible to build the same model incrementally via the add() method,
which is similar to the append() method of a Python list.
model = [Link]()
[Link]([Link](64, activation="relu"))
[Link]([Link](10, activation="softmax"))
You saw in chapter 4 that layers only get built (which is to say, create their weights) when
they are called for the first time. That’s because the shape of the layers' weights depends
on the shape of their input: until the input shape is known, they can’t be created.
As such, the preceding Sequential model does not have any weights (listing 7.3)
until you actually call it on some data, or call its build() method with an input shape
(listing 7.4).
Builds the model—now the model will expect samples of shape (3,). The
None in the input shape signals that the batch size could be anything.
After the model is built, you can display its contents via the summary() method, which
comes in handy for debugging.
Different ways to build Keras models 175
As you can see, this model happens to be named “sequential_1.” You can give names
to everything in Keras—every model, every layer.
Listing 7.6 Naming models and layers with the name argument
When building a Sequential model incrementally, it’s useful to be able to print a sum-
mary of what the current model looks like after you add each layer. But you can’t print
a summary until the model is built! There’s actually a way to have your Sequential
built on the fly: just declare the shape of the model’s inputs in advance. You can do
this via the Input class.
Now you can use summary() to follow how the output shape of your model changes as
you add more layers:
>>> [Link]()
Model: "sequential_2"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_4 (Dense) (None, 64) 256
=================================================================
Total params: 256
Trainable params: 256
Non-trainable params: 0
_________________________________________________________________
>>> [Link]([Link](10, activation="softmax"))
>>> [Link]()
Model: "sequential_2"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_4 (Dense) (None, 64) 256
_________________________________________________________________
dense_5 (Dense) (None, 10) 650
=================================================================
Total params: 906
Trainable params: 906
Non-trainable params: 0
_________________________________________________________________
This is a pretty common debugging workflow when dealing with layers that transform
their inputs in complex ways, such as the convolutional layers you’ll learn about in
chapter 8.
This inputs object holds information about the shape and dtype of the data that the
model will process:
The model will process batches where each sample
>>> [Link] has shape (3,). The number of samples per batch is
(None, 3) variable (indicated by the None batch size).
>>> [Link]
These batches will have
float32 dtype float32.
We call such an object a symbolic tensor. It doesn’t contain any actual data, but it
encodes the specifications of the actual tensors of data that the model will see when
you use it. It stands for future tensors of data.
Next, we created a layer and called it on the input:
All Keras layers can be called both on real tensors of data and on these symbolic ten-
sors. In the latter case, they return a new symbolic tensor, with updated shape and
dtype information:
>>> [Link]
(None, 64)
After obtaining the final outputs, we instantiated the model by specifying its inputs
and outputs in the Model constructor:
>>> [Link]()
Model: "functional_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
my_input (InputLayer) [(None, 3)] 0
_________________________________________________________________
178 CHAPTER 7 Working with Keras: A deep dive
vocabulary_size = 10000
num_tags = 100 Combine input features into
num_departments = 4 a single tensor, features, by
concatenating them.
Define title = [Link](shape=(vocabulary_size,), name="title")
model text_body = [Link](shape=(vocabulary_size,), name="text_body")
inputs. tags = [Link](shape=(num_tags,), name="tags")
The Functional API is a simple, LEGO-like, yet very flexible way to define arbitrary
graphs of layers like these.
TRAINING A MULTI-INPUT, MULTI-OUTPUT MODEL
You can train your model in much the same way as you would train a Sequential
model, by calling fit() with lists of input and output data. These lists of data should
be in the same order as the inputs you passed to the Model constructor.
Listing 7.10 Training a model by providing lists of input and target arrays
import numpy as np
num_samples = 1280
[Link](optimizer="rmsprop",
loss=["mean_squared_error", "categorical_crossentropy"],
metrics=[["mean_absolute_error"], ["accuracy"]])
[Link]([title_data, text_body_data, tags_data],
[priority_data, department_data],
epochs=1)
[Link]([title_data, text_body_data, tags_data],
[priority_data, department_data])
priority_preds, department_preds = [Link](
[title_data, text_body_data, tags_data])
If you don’t want to rely on input order (for instance, because you have many inputs
or outputs), you can also leverage the names you gave to the Input objects and the
output layers, and pass data via dictionaries.
Listing 7.11 Training a model by providing dicts of input and target arrays
[Link](optimizer="rmsprop",
loss={"priority": "mean_squared_error", "department":
"categorical_crossentropy"},
metrics={"priority": ["mean_absolute_error"], "department":
["accuracy"]})
[Link]({"title": title_data, "text_body": text_body_data,
"tags": tags_data},
{"priority": priority_data, "department": department_data},
epochs=1)
[Link]({"title": title_data, "text_body": text_body_data,
"tags": tags_data},
{"priority": priority_data, "department": department_data})
priority_preds, department_preds = [Link](
{"title": title_data, "text_body": text_body_data, "tags": tags_data})
180 CHAPTER 7 Working with Keras: A deep dive
[Link].plot_model(model, "ticket_classifier.png")
You can add to this plot the input and output shapes of each layer in the model, which
can be helpful during debugging (see figure 7.3).
[Link].plot_model(
model, "ticket_classifier_with_shape_info.png", show_shapes=True)
The “None” in the tensor shapes represents the batch size: this model allows batches
of any size.
Access to layer connectivity also means that you can inspect and reuse individual
nodes (layer calls) in the graph. The [Link] model property provides the list
of layers that make up the model, and for each layer you can query [Link] and
[Link].
>>> [Link]
[<[Link].input_layer.InputLayer at 0x7fa963f9d358>,
<[Link].input_layer.InputLayer at 0x7fa963f9d2e8>,
<[Link].input_layer.InputLayer at 0x7fa963f9d470>,
<[Link] at 0x7fa963f9d860>,
<[Link] at 0x7fa964074390>,
<[Link] at 0x7fa963f9d898>,
<[Link] at 0x7fa963f95470>]
>>> [Link][3].input
[<[Link] "title:0" shape=(None, 10000) dtype=float32>,
<[Link] "text_body:0" shape=(None, 10000) dtype=float32>,
<[Link] "tags:0" shape=(None, 100) dtype=float32>]
>>> [Link][3].output
<[Link] "concatenate/concat:0" shape=(None, 20100) dtype=float32>
This enables you to do feature extraction, creating models that reuse intermediate fea-
tures from another model.
Let’s say you want to add another output to the previous model—you want to esti-
mate how long a given issue ticket will take to resolve, a kind of difficulty rating. You
could do this via a classification layer over three categories: “quick,” “medium,” and
“difficult.” You don’t need to recreate and retrain a model from scratch. You can start
from the intermediate features of your previous model, since you have access to them,
like this.
new_model = [Link](
inputs=[title, text_body, tags],
outputs=[priority, department, difficulty])
[Link].plot_model(
new_model, "updated_ticket_classifier.png", show_shapes=True)
182 CHAPTER 7 Working with Keras: A deep dive
class CustomerTicketModel([Link]):
Don’t forget to
def __init__(self, num_departments): call the super()
super().__init__() constructor!
self.concat_layer = [Link]() Define
self.mixing_layer = [Link](64, activation="relu") sublayers
self.priority_scorer = [Link](1, activation="sigmoid") in the
self.department_classifier = [Link]( constructor.
num_departments, activation="softmax")
priority = self.priority_scorer(features)
department = self.department_classifier(features)
return priority, department
Once you’ve defined the model, you can instantiate it. Note that it will only create its
weights the first time you call it on some data, much like Layer subclasses:
model = CustomerTicketModel(num_departments=4)
So far, everything looks very similar to Layer subclassing, a workflow you encountered
in chapter 3. What, then, is the difference between a Layer subclass and a Model sub-
class? It’s simple: a “layer” is a building block you use to create models, and a “model”
is the top-level object that you will actually train, export for inference, etc. In short, a
Model has fit(), evaluate(), and predict() methods. Layers don’t. Other than that,
the two classes are virtually identical. (Another difference is that you can save a model
to a file on disk, which we will cover in a few sections.)
You can compile and train a Model subclass just like a Sequential or Functional
model: The structure of what you pass as the loss and
metrics arguments must match exactly what gets
[Link](optimizer="rmsprop", returned by call()—here, a list of two elements.
loss=["mean_squared_error", "categorical_crossentropy"],
metrics=[["mean_absolute_error"], ["accuracy"]])
[Link]({"title": title_data,
"text_body": text_body_data,
"tags": tags_data},
[priority_data, department_data],
The structure of the target
epochs=1) data must match exactly what is
[Link]({"title": title_data, returned by the call() method—
"text_body": text_body_data, here, a list of two elements.
"tags": tags_data},
[priority_data, department_data])
priority_preds, department_preds = [Link]({"title": title_data,
"text_body": text_body_data,
The structure of the input data must match "tags": tags_data})
exactly what is expected by the call() method—
here, a dict with keys title, text_body, and tags.
The Model subclassing workflow is the most flexible way to build a model. It enables
you to build models that cannot be expressed as directed acyclic graphs of layers—
imagine, for instance, a model where the call() method uses layers inside a for loop,
or even calls them recursively. Anything is possible—you’re in charge.
BEWARE: WHAT SUBCLASSED MODELS DON’T SUPPORT
This freedom comes at a cost: with subclassed models, you are responsible for more of
the model logic, which means your potential error surface is much larger. As a result,
you will have more debugging work to do. You are developing a new Python object,
not just snapping together LEGO bricks.
184 CHAPTER 7 Working with Keras: A deep dive
Functional and subclassed models are also substantially different in nature. A Func-
tional model is an explicit data structure—a graph of layers, which you can view, inspect,
and modify. A subclassed model is a piece of bytecode—a Python class with a call()
method that contains raw code. This is the source of the subclassing workflow’s flexibil-
ity—you can code up whatever functionality you like—but it introduces new limitations.
For instance, because the way layers are connected to each other is hidden inside
the body of the call() method, you cannot access that information. Calling sum-
mary() will not display layer connectivity, and you cannot plot the model topology via
plot_model(). Likewise, if you have a subclassed model, you cannot access the nodes
of the graph of layers to do feature extraction because there is simply no graph. Once
the model is instantiated, its forward pass becomes a complete black box.
class Classifier([Link]):
inputs = [Link](shape=(3,))
features = [Link](64, activation="relu")(inputs)
outputs = Classifier(num_classes=10)(features)
model = [Link](inputs=inputs, outputs=outputs)
Inversely, you can use a Functional model as part of a subclassed layer or model.
inputs = [Link](shape=(64,))
outputs = [Link](1, activation="sigmoid")(inputs)
binary_classifier = [Link](inputs=inputs, outputs=outputs)
Using built-in training and evaluation loops 185
class MyModel([Link]):
model = MyModel()
There are a couple of ways you can customize this simple workflow:
Provide your own custom metrics.
Pass callbacks to the fit() method to schedule actions to be taken at specific
points during training.
Let’s take a look at these.
import tensorflow as tf
Subclass the
class RootMeanSquaredError([Link]): Metric class.
Using built-in training and evaluation loops 187
You use the result() method to return the current value of the metric:
def result(self):
return [Link](self.mse_sum / [Link](self.total_samples, tf.float32))
Meanwhile, you also need to expose a way to reset the metric state without having to
reinstantiate it—this enables the same metric objects to be used across different
epochs of training or across both training and evaluation. You do this with the
reset_state() method:
def reset_state(self):
self.mse_sum.assign(0.)
self.total_samples.assign(0)
Custom metrics can be used just like built-in ones. Let’s test-drive our own metric:
model = get_mnist_model()
[Link](optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy", RootMeanSquaredError()])
[Link](train_images, train_labels,
epochs=3,
validation_data=(val_images, val_labels))
test_metrics = [Link](test_images, test_labels)
You can now see the fit() progress bar displaying the RMSE of your model.
steering decisions based on its current state. The Keras callbacks API will help you
transform your call to [Link]() from a paper airplane into a smart, autonomous
drone that can self-introspect and dynamically take action.
A callback is an object (a class instance implementing specific methods) that is
passed to the model in the call to fit() and that is called by the model at various
points during training. It has access to all the available data about the state of the
model and its performance, and it can take action: interrupt training, save a model,
load a different weight set, or otherwise alter the state of the model.
Here are some examples of ways you can use callbacks:
Model checkpointing—Saving the current state of the model at different points
during training.
Early stopping—Interrupting training when the validation loss is no longer
improving (and of course, saving the best model obtained during training).
Dynamically adjusting the value of certain parameters during training —Such as the
learning rate of the optimizer.
Logging training and validation metrics during training, or visualizing the representa-
tions learned by the model as they’re updated—The fit() progress bar that you’re
familiar with is in fact a callback!
The [Link] module includes a number of built-in callbacks (this is not an
exhaustive list):
[Link]
[Link]
[Link]
[Link]
[Link]
Let’s review two of them to give you an idea of how to use them: EarlyStopping and
ModelCheckpoint.
THE EARLYSTOPPING AND MODELCHECKPOINT CALLBACKS
When you’re training a model, there are many things you can’t predict from the start.
In particular, you can’t tell how many epochs will be needed to get to an optimal vali-
dation loss. Our examples so far have adopted the strategy of training for enough
epochs that you begin overfitting, using the first run to figure out the proper number
of epochs to train for, and then finally launching a new training run from scratch
using this optimal number. Of course, this approach is wasteful. A much better way to
handle this is to stop training when you measure that the validation loss is no longer
improving. This can be achieved using the EarlyStopping callback.
The EarlyStopping callback interrupts training once a target metric being moni-
tored has stopped improving for a fixed number of epochs. For instance, this callback
allows you to interrupt training as soon as you start overfitting, thus avoiding having to
retrain your model for a smaller number of epochs. This callback is typically used in
Using built-in training and evaluation loops 189
combination with ModelCheckpoint, which lets you continually save the model during
training (and, optionally, save only the current best model so far: the version of the
model that achieved the best performance at the end of an epoch).
Callbacks are passed to the model via the Interrupts training when
callbacks argument in fit(), which takes a list of improvement stops
callbacks. You can pass any number of callbacks.
Monitors the model’s
callbacks_list = [
validation accuracy
[Link](
Saves the monitor="val_accuracy", Interrupts training when
current patience=2, accuracy has stopped
weights after ), improving for two epochs
every epoch [Link](
filepath="checkpoint_path.keras",
Path to the
destination monitor="val_loss", These two arguments mean you won’t
model file save_best_only=True, overwrite the model file unless val_loss
) has improved, which allows you to keep
] the best model seen during training.
model = get_mnist_model()
[Link](optimizer="rmsprop", You monitor accuracy,
loss="sparse_categorical_crossentropy", so it should be part of
metrics=["accuracy"]) the model’s metrics.
[Link](train_images, train_labels, Note that because the callback
epochs=10, will monitor validation loss and
callbacks=callbacks_list, validation accuracy, you need to pass
validation_data=(val_images, val_labels)) validation_data to the call to fit().
Note that you can always save models manually after training as well—just call
[Link]('my_checkpoint_path'). To reload the model you’ve saved, just use
model = [Link].load_model("checkpoint_path.keras")
These methods are all called with a logs argument, which is a dictionary containing
information about the previous batch, epoch, or training run—training and valida-
tion metrics, and so on. The on_epoch_* and on_batch_* methods also take the
epoch or batch index as their first argument (an integer).
Here’s a simple example that saves a list of per-batch loss values during training
and saves a graph of these values at the end of each epoch.
class LossHistory([Link]):
def on_train_begin(self, logs):
self.per_batch_losses = []
model = get_mnist_model()
[Link](optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
[Link](train_images, train_labels,
epochs=10,
callbacks=[LossHistory()],
validation_data=(val_images, val_labels))
powerful your ideas become. Keras helps you go from idea to experiment in the least
possible time, and fast GPUs can help you get from experiment to result as quickly as
possible. But what about processing the experiment’s results? That’s where Tensor-
Board comes in (see figure 7.6).
Idea
Results Experiment
GPUs, TPUs
Figure 7.6 The loop of progress
The easiest way to use TensorBoard with a Keras model and the fit() method is to
use the [Link] callback.
In the simplest case, just specify where you want the callback to write logs, and
you’re good to go:
model = get_mnist_model()
[Link](optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
tensorboard = [Link](
log_dir="/full_path_to_your_log_dir",
)
[Link](train_images, train_labels,
epochs=10,
validation_data=(val_images, val_labels),
callbacks=[tensorboard])
Once the model starts running, it will write logs at the target location. If you are run-
ning your Python script on a local machine, you can then launch the local Tensor-
Board server using the following command (note that the tensorboard executable
should be already available if you have installed TensorFlow via pip; if not, you can
install TensorBoard manually via pip install tensorboard):
You can then navigate to the URL that the command returns in order to access the
TensorBoard interface.
If you are running your script in a Colab notebook, you can run an embedded Ten-
sorBoard instance as part of your notebook, using the following commands:
%load_ext tensorboard
%tensorboard --logdir /full_path_to_your_log_dir
In the TensorBoard interface, you will be able to monitor live graphs of your training
and evaluation metrics (see figure 7.7).
Figure 7.7 TensorBoard can be used for easy monitoring of training and evaluation metrics.
category. There are other setups where no explicit targets are present, such as genera-
tive learning (which we will discuss in chapter 12), self-supervised learning (where targets
are obtained from the inputs), and reinforcement learning (where learning is driven by
occasional “rewards,” much like training a dog). Even if you’re doing regular super-
vised learning, as a researcher, you may want to add some novel bells and whistles that
require low-level flexibility.
Whenever you find yourself in a situation where the built-in fit() is not enough,
you will need to write your own custom training logic. You already saw simple exam-
ples of low-level training loops in chapters 2 and 3. As a reminder, the contents of a
typical training loop look like this:
1 Run the forward pass (compute the model’s output) inside a gradient tape to
obtain a loss value for the current batch of data.
2 Retrieve the gradients of the loss with regard to the model’s weights.
3 Update the model’s weights so as to lower the loss value on the current batch
of data.
194 CHAPTER 7 Working with Keras: A deep dive
These steps are repeated for as many batches as necessary. This is essentially what
fit() does under the hood. In this section, you will learn to reimplement fit() from
scratch, which will give you all the knowledge you need to write any training algorithm
you may come up with.
Let’s go over the details.
metric = [Link]()
targets = [0, 1, 2]
predictions = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
metric.update_state(targets, predictions)
current_result = [Link]()
print(f"result: {current_result:.2f}")
You may also need to track the average of a scalar value, such as the model’s loss. You
can do this via the [Link] metric:
values = [0, 1, 2, 3, 4]
mean_tracker = [Link]()
for value in values:
mean_tracker.update_state(value)
print(f"Mean of values: {mean_tracker.result():.2f}")
Remember to use metric.reset_state() when you want to reset the current results
(at the start of a training epoch or at the start of evaluation).
Listing 7.21 Writing a step-by-step training loop: the training step function
logs = {}
for metric in metrics: Keep track
metric.update_state(targets, predictions) of metrics.
logs[[Link]] = [Link]()
def reset_metrics():
for metric in metrics:
metric.reset_state()
loss_tracking_metric.reset_state()
We can now lay out our complete training loop. Note that we use a [Link]
object to turn our NumPy data into an iterator that iterates over the data in batches of
size 32.
training_dataset = [Link].from_tensor_slices(
(train_images, train_labels))
training_dataset = training_dataset.batch(32)
epochs = 3
for epoch in range(epochs):
reset_metrics()
for inputs_batch, targets_batch in training_dataset:
logs = train_step(inputs_batch, targets_batch)
print(f"Results at the end of epoch {epoch}")
for key, value in [Link]():
print(f"...{key}: {value:.4f}")
And here’s the evaluation loop: a simple for loop that repeatedly calls a test_step()
function, which processes a single batch of data. The test_step() function is just a sub-
set of the logic of train_step(). It omits the code that deals with updating the weights
of the model—that is to say, everything involving the GradientTape and the optimizer.
loss_tracking_metric.update_state(loss)
logs["val_loss"] = loss_tracking_metric.result()
return logs
@[Link]
This is the
def test_step(inputs, targets): only line that
predictions = model(inputs, training=False) changed.
loss = loss_fn(targets, predictions)
logs = {}
for metric in metrics:
metric.update_state(targets, predictions)
logs["val_" + [Link]] = [Link]()
loss_tracking_metric.update_state(loss)
logs["val_loss"] = loss_tracking_metric.result()
return logs
On the Colab CPU, we go from taking 1.80 s to run the evaluation loop to only 0.8 s.
Much faster!
Remember, while you are debugging your code, prefer running it eagerly, without
any @[Link] decorator. It’s easier to track bugs this way. Once your code is work-
ing and you want to make it fast, add a @[Link] decorator to your training step
and your evaluation step—or any other performance-critical function.
loss_tracker.update_state(loss)
We update the loss
return {"loss": loss_tracker.result()} tracker metric that
tracks the average
@property of the loss.
def metrics(self): Any metric you
return [loss_tracker] would like to reset We return the average loss
across epochs should so far by querying the loss
be listed here. tracker metric.
We can now instantiate our custom model, compile it (we only pass the optimizer, since
the loss is already defined outside of the model), and train it using fit() as usual:
[Link](optimizer=[Link]())
[Link](train_images, train_labels, epochs=3)
class CustomModel([Link]):
def train_step(self, data):
inputs, targets = data Compute
with [Link]() as tape: the loss via
predictions = self(inputs, training=True) self.compiled_
loss = self.compiled_loss(targets, predictions) loss.
gradients = [Link](loss, model.trainable_weights)
200 CHAPTER 7 Working with Keras: A deep dive
optimizer.apply_gradients(zip(gradients, model.trainable_weights))
self.compiled_metrics.update_state(targets, predictions)
return {[Link]: [Link]() for m in [Link]}
Update the model’s metrics Return a dict mapping metric
via self.compiled_metrics. names to their current value.
[Link](optimizer=[Link](),
loss=[Link](),
metrics=[[Link]()])
[Link](train_images, train_labels, epochs=3)
That was a lot of information, but you now know enough to use Keras to do almost
anything.
Summary
Keras offers a spectrum of different workflows, based on the principle of progres-
sive disclosure of complexity. They all smoothly inter-operate together.
You can build models via the Sequential class, via the Functional API, or by sub-
classing the Model class. Most of the time, you’ll be using the Functional API.
The simplest way to train and evaluate a model is via the default fit() and
evaluate() methods.
Keras callbacks provide a simple way to monitor models during your call to
fit() and automatically take action based on the state of the model.
You can also fully take control of what fit() does by overriding the train_
step() method.
Beyond fit(), you can also write your own training loops entirely from scratch.
This is useful for researchers implementing brand-new training algorithms.
Introduction
to deep learning
for computer vision
Computer vision is the earliest and biggest success story of deep learning. Every
day, you’re interacting with deep vision models—via Google Photos, Google image
search, YouTube, video filters in camera apps, OCR software, and many more.
These models are also at the heart of cutting-edge research in autonomous driving,
robotics, AI-assisted medical diagnosis, autonomous retail checkout systems, and
even autonomous farming.
Computer vision is the problem domain that led to the initial rise of deep learn-
ing between 2011 and 2015. A type of deep learning model called convolutional
neural networks started getting remarkably good results on image classification
competitions around that time, first with Dan Ciresan winning two niche competi-
tions (the ICDAR 2011 Chinese character recognition competition and the IJCNN
201
202 CHAPTER 8 Introduction to deep learning for computer vision
2011 German traffic signs recognition competition), and then more notably in fall
2012 with Hinton’s group winning the high-profile ImageNet large-scale visual recog-
nition challenge. Many more promising results quickly started bubbling up in other
computer vision tasks.
Interestingly, these early successes weren’t quite enough to make deep learning
mainstream at the time—it took a few years. The computer vision research commu-
nity had spent many years investing in methods other than neural networks, and it
wasn’t quite ready to give up on them just because there was a new kid on the block.
In 2013 and 2014, deep learning still faced intense skepticism from many senior
computer vision researchers. It was only in 2016 that it finally became dominant. I
remember exhorting an ex-professor of mine, in February 2014, to pivot to deep
learning. “It’s the next big thing!” I would say. “Well, maybe it’s just a fad,” he
replied. By 2016, his entire lab was doing deep learning. There’s no stopping an
idea whose time has come.
This chapter introduces convolutional neural networks, also known as convnets, the
type of deep learning model that is now used almost universally in computer vision
applications. You’ll learn to apply convnets to image-classification problems—in par-
ticular those involving small training datasets, which are the most common use case if
you aren’t a large tech company.
>>> [Link]()
Model: "model"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) [(None, 28, 28, 1)] 0
_________________________________________________________________
conv2d (Conv2D) (None, 26, 26, 32) 320
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 13, 13, 32) 0
_________________________________________________________________
conv2d_1 (Conv2D) (None, 11, 11, 64) 18496
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 5, 5, 64) 0
_________________________________________________________________
conv2d_2 (Conv2D) (None, 3, 3, 128) 73856
_________________________________________________________________
flatten (Flatten) (None, 1152) 0
_________________________________________________________________
dense (Dense) (None, 10) 11530
=================================================================
Total params: 104,202
Trainable params: 104,202
Non-trainable params: 0
_________________________________________________________________
You can see that the output of every Conv2D and MaxPooling2D layer is a rank-3 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).
After the last Conv2D layer, we end up with an output of shape (3, 3, 128)—a 3 × 3
feature map of 128 channels. The next step is to feed this output into a densely con-
nected classifier like those you’re already familiar with: a stack of Dense layers. These
classifiers process vectors, which are 1D, whereas the current output is a rank-3 tensor.
To bridge the gap, we flatten the 3D outputs to 1D with a Flatten layer before adding
the Dense layers.
Finally, we do 10-way classification, so our last layer has 10 outputs and a softmax
activation.
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-way 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.
204 CHAPTER 8 Introduction to deep learning for computer vision
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!
But why does this simple convnet work so well, compared to a densely connected
model? To answer this, let’s dive into what the Conv2D and MaxPooling2D layers do.
“cat”
Figure 8.2 The visual world forms a spatial hierarchy of visual modules:
elementary lines or textures combine into simple objects such as eyes or
ears, which combine into high-level concepts such as “cat.”
Convolutions operate over rank-3 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 chan-
nels: 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 rank-3 tensor: it has a width and
206 CHAPTER 8 Introduction to deep learning for computer vision
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 × 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 8.3).
Response map,
quantifying the presence
of the filter’s pattern at
Original input different locations
Single filter
That is what the term feature map means: every dimension in the depth axis is a feature
(or filter), and the rank-2 tensor output[:, :, n] is the 2D spatial map of the response
of this filter over the input.
Convolutions are defined by two key parameters:
Size of the patches extracted from the inputs—These are typically 3 × 3 or 5 × 5. In the
example, they were 3 × 3, which is a common choice.
Depth of the output feature map—This is the number of filters computed by the con-
volution. 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 × 3 or 5 × 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
Introduction to convnets 207
Width Height
3 × 3 input patches
Dot product
with kernel
Output
depth Transformed patches
Note that the output width and height may differ from the input width and height 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
Let’s take a deeper look at these notions.
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 × 3 window, forming a 3 × 3 grid (see figure 8.5). Hence, the output fea-
ture map will be 3 × 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 × 28 inputs, which become 26 × 26 after the first convolution layer.
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
208 CHAPTER 8 Introduction to deep learning for computer vision
and columns on each side of the input feature map so as to make it possible to fit cen-
ter convolution windows around every input tile. For a 3 × 3 window, you add one col-
umn on the right, one column on the left, one row at the top, and one row at the
bottom. For a 5 × 5 window, you add two rows (see figure 8.6).
etc.
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. Our 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
Introduction to convnets 209
convolution, called its stride, which defaults to 1. It’s possible to have strided convolu-
tions: convolutions with a stride higher than 1. In figure 8.7, you can see the patches
extracted by a 3 × 3 convolution with stride 2 over a 5 × 5 input (without padding).
1 2
1 2
3 4
3 4
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 convolu-
tions are rarely used in classification models, but they come in handy for some types of
models, as you will see 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.
Why downsample feature maps this way? Why not remove the max-pooling layers
and keep fairly large feature maps all the way up? Let’s look at this option. Our model
would then look like the following listing.
>>> model_no_max_pool.summary()
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
=================================================================
Total params: 712,202
Trainable params: 712,202
Non-trainable params: 0
_________________________________________________________________
model—will constitute your future toolbox for tackling the problem of performing
image classification with small datasets.
However, access to the API is restricted to Kaggle users, so in order to run the pre-
ceding command, you first need to authenticate yourself. The kaggle package will
look for your login credentials in a JSON file located at ~/.kaggle/[Link]. Let’s
create this file.
Training a convnet from scratch on a small dataset 213
First, you need to create a Kaggle API key and download it to your local machine. Just
navigate to the Kaggle website in a web browser, log in, and go to the My Account
page. In your account settings, you’ll find an API section. Clicking the Create New API
Token button will generate a [Link] key file and will download it to your machine.
Second, go to your Colab notebook, and upload the API’s key JSON file to your Colab
session by running the following code in a notebook cell:
from [Link] import files
[Link]()
When you run this cell, you will see a Choose Files button appear. Click it and select
the [Link] file you just downloaded. This uploads the file to the local Colab run-
time.
Finally, create a ~/.kaggle folder (mkdir ~/.kaggle), and copy the key file to it
(cp [Link] ~/.kaggle/). As a security best practice, you should also make
sure that the file is only readable by the current user, yourself (chmod 600):
!mkdir ~/.kaggle
!cp [Link] ~/.kaggle/
!chmod 600 ~/.kaggle/[Link]
The first time you try to download the data, you may get a “403 Forbidden” error.
That’s because you need to accept the terms associated with the dataset before you
download it—you’ll have to go to [Link]/c/dogs-vs-cats/rules (while
logged into your Kaggle account) and click the I Understand and Accept button. You
only need to do this once.
Finally, the training data is a compressed file named [Link]. Make sure you uncom-
press it (unzip) silently (-qq):
The pictures in our dataset are medium-resolution color JPEGs. Figure 8.8 shows
some examples.
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.
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,
214 CHAPTER 8 Introduction to deep learning for computer vision
Figure 8.8 Samples from the Dogs vs. Cats dataset. Sizes weren’t modified: the samples come in
different sizes, colors, backgrounds, etc.
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 thousand 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.
The subsampled dataset we will work with will have the following directory structure:
cats_vs_dogs_small/
...train/ Contains 1,000 cat images
......cat/
......dog/ Contains 1,000 dog images
...validation/
Contains 500 cat images
......cat/
......dog/
Contains 500 dog images
...test/
......cat/
Contains 1,000 cat images
......dog/
Contains 1,000 dog images
Let’s make it happen in a couple calls to shutil.
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 appropri-
ate measure of success.
NOTE The depth of the feature maps progressively increases in the model
(from 32 to 256), whereas the size of the feature maps decreases (from 180 ×
180 to 7 × 7). This is a pattern you’ll see in almost all convnets.
Because we’re looking at a binary-classification problem, we’ll end the model with a
single unit (a Dense layer of size 1) and a sigmoid activation. This unit will encode the
probability that the model is looking at one class or the other.
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.
Listing 8.7 Instantiating a small convnet for dogs vs. cats classification
Let’s look at how the dimensions of the feature maps change with every successive
layer:
>>> [Link]()
Model: "model_2"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_3 (InputLayer) [(None, 180, 180, 3)] 0
_________________________________________________________________
rescaling (Rescaling) (None, 180, 180, 3) 0
_________________________________________________________________
conv2d_6 (Conv2D) (None, 178, 178, 32) 896
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 89, 89, 32) 0
_________________________________________________________________
conv2d_7 (Conv2D) (None, 87, 87, 64) 18496
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 43, 43, 64) 0
_________________________________________________________________
conv2d_8 (Conv2D) (None, 41, 41, 128) 73856
_________________________________________________________________
max_pooling2d_4 (MaxPooling2 (None, 20, 20, 128) 0
_________________________________________________________________
conv2d_9 (Conv2D) (None, 18, 18, 256) 295168
_________________________________________________________________
max_pooling2d_5 (MaxPooling2 (None, 9, 9, 256) 0
_________________________________________________________________
conv2d_10 (Conv2D) (None, 7, 7, 256) 590080
_________________________________________________________________
flatten_2 (Flatten) (None, 12544) 0
_________________________________________________________________
dense_2 (Dense) (None, 1) 12545
=================================================================
Total params: 991,041
Trainable params: 991,041
Non-trainable params: 0
_________________________________________________________________
Training a convnet from scratch on a small dataset 217
For the compilation step, we’ll go with the RMSprop optimizer, as usual. Because we
ended the model with a single sigmoid unit, we’ll use binary crossentropy as the loss
(as a reminder, check out table 6.1 in chapter 6 for a cheat sheet on which loss func-
tion to use in various situations).
[Link](loss="binary_crossentropy",
optimizer="rmsprop",
metrics=["accuracy"])
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)
218 CHAPTER 8 Introduction to deep learning for computer vision
The .map() method, in particular, is one that you will use often. Here’s an example.
We’ll use it to reshape the elements in our toy dataset from shape (16,) to shape
(4, 4):
>>> reshaped_dataset = [Link](lambda x: [Link](x, (4, 4)))
>>> for i, element in enumerate(reshaped_dataset):
>>> print([Link])
>>> if i >= 2:
>>> break
(4, 4)
(4, 4)
(4, 4)
Let’s look at the output of one of these Dataset objects: it yields batches of 180 × 180
RGB images (shape (32, 180, 180, 3)) and integer labels (shape (32,)). There are
32 samples in each batch (the batch size).
Listing 8.10 Displaying the shapes of the data and labels yielded by the Dataset
Let’s fit the model on our dataset. We’ll use the validation_data argument in fit()
to monitor validation metrics on a separate Dataset object.
Note that we’ll also use a ModelCheckpoint callback to save the model after each
epoch. We’ll configure it with the path specifying 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.
callbacks = [
[Link](
filepath="convnet_from_scratch.keras",
save_best_only=True,
monitor="val_loss")
]
220 CHAPTER 8 Introduction to deep learning for computer vision
history = [Link](
train_dataset,
epochs=30,
validation_data=validation_dataset,
callbacks=callbacks)
Let’s plot the loss and accuracy of the model over the training and validation data
during training (see figure 8.9).
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.
Let’s check the test accuracy. We’ll reload the model from its saved file to evaluate
it as it was before it started overfitting.
Training a convnet from scratch on a small dataset 221
We get a test accuracy of 69.5%. (Due to the randomness of neural network initializa-
tions, you may get numbers within one percentage point of that.)
Because we have relatively few training samples (2,000), overfitting will be our
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.
data_augmentation = [Link](
[
[Link]("horizontal"),
[Link](0.1),
[Link](0.2),
]
)
These are just a few of the layers available (for more, see the Keras documentation).
Let’s quickly go over this code:
RandomFlip("horizontal")—Applies horizontal flipping to a random 50% of
the images that go through it
RandomRotation(0.1)—Rotates 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])
222 CHAPTER 8 Introduction to deep learning for computer vision
Figure 8.10 Generating variations of a very good boy via random data augmentation
If we 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
Training a convnet from scratch on a small dataset 223
because they come from a small number of original images—we can’t produce new
information; we can only remix existing information. As such, this may not be enough
to completely get rid of overfitting. To further fight overfitting, we’ll also add a Dropout
layer to our model right before the densely connected classifier.
One last thing you should know about random image augmentation layers: just
like Dropout, they’re inactive during inference (when we call predict() or evaluate()).
During evaluation, our model will behave just the same as when it did not include
data augmentation and dropout.
Listing 8.16 Defining a new convnet that includes image augmentation and dropout
[Link](loss="binary_crossentropy",
optimizer="rmsprop",
metrics=["accuracy"])
Let’s train the model using data augmentation and dropout. Because we expect over-
fitting to occur much later during training, we will train for three times as many
epochs—one hundred.
callbacks = [
[Link](
filepath="convnet_from_scratch_with_augmentation.keras",
save_best_only=True,
monitor="val_loss")
]
history = [Link](
train_dataset,
epochs=100,
validation_data=validation_dataset,
callbacks=callbacks)
Let’s plot the results again: see figure 8.11. Thanks to data augmentation and drop-
out, we start overfitting much later, around epochs 60–70 (compared to epoch 10 for
224 CHAPTER 8 Introduction to deep learning for computer vision
the original model). The validation accuracy ends up consistently in the 80–85% range—
a big improvement over our first try.
Let’s check the test accuracy.
test_model = [Link].load_model(
"convnet_from_scratch_with_augmentation.keras")
test_loss, test_acc = test_model.evaluate(test_dataset)
print(f"Test accuracy: {test_acc:.3f}")
We get a test accuracy of 83.5%. It’s starting to look good! If you’re using Colab, make
sure you download the saved file (convnet_from_scratch_with_augmentation.keras),
as we will use it for some experiments in the next chapter.
By further tuning the model’s configuration (such as the number of filters per
convolution layer, or the number of layers in the model), we might be able to get an
even better accuracy, likely up to 90%. But it would prove difficult to go any higher
just by training our own convnet from scratch, because we have so little data to work
with. As a next step to improve our accuracy on this problem, we’ll have to use a pre-
trained model, which is the focus of the next two sections.
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 com-
pared to many older, shallow learning approaches, and it makes deep learning very
effective for small-data problems.
In this case, let’s consider a large convnet trained on the ImageNet dataset (1.4
million labeled images and 1,000 different classes). ImageNet contains many animal
classes, including different species of cats and dogs, and you can thus expect it to per-
form well on the dogs-versus-cats classification problem.
We’ll use the VGG16 architecture, developed by Karen Simonyan and Andrew
Zisserman in 2014.1 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 architec-
ture is similar to what you’re already familiar with, and it’s 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.
1
Karen Simonyan and Andrew Zisserman, “Very Deep Convolutional Networks for Large-Scale Image Recogni-
tion,” arXiv (2014), [Link]
226 CHAPTER 8 Introduction to deep learning for computer vision
Figure 8.12 Swapping classifiers while keeping the same convolutional base
information about where objects are located in the input image; these layers get rid of
the notion of space, whereas the object location is still described by convolutional fea-
ture maps. For problems where object location matters, densely connected features
are largely useless.
Note that the level of generality (and therefore reusability) of the representations
extracted by specific convolution layers depends on the depth of the layer in the
model. Layers that come earlier in the model extract local, highly generic feature
maps (such as visual edges, colors, and textures), whereas layers that are higher up
extract more-abstract concepts (such as “cat ear” or “dog eye”). So if your new dataset
differs a lot from the dataset on which the original model was trained, you may be bet-
ter off using only the first few layers of the model to do feature extraction, rather than
using the entire convolutional base.
In this case, because the ImageNet class set contains multiple dog and cat
classes, it’s likely to be beneficial to reuse the information contained in the densely
connected layers of the original model. But we’ll choose not to, in order to cover
the more general case where the class set of the new problem doesn’t overlap the
class set of the original model. Let’s put this into practice by using the convolu-
tional base of the VGG16 network, trained on ImageNet, to extract interesting fea-
tures 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 [Link] module. Many other image-classification models (all
pretrained on the ImageNet dataset) are available as part of [Link]:
Leveraging a pretrained model 227
Xception
ResNet
MobileNet
EfficientNet
DenseNet
etc.
Let’s instantiate the VGG16 model.
conv_base = [Link].vgg16.VGG16(
weights="imagenet",
include_top=False,
input_shape=(180, 180, 3))
The final feature map has shape (5, 5, 512). That’s the feature map on top of which
we’ll stick a densely connected classifier.
At this point, there are two ways we could proceed:
Run the convolutional base over our dataset, record its output to a NumPy array
on disk, and then use this data as input to a standalone, densely connected clas-
sifier similar to those you saw in chapter 4 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 us to use
data augmentation.
Extend the model we have (conv_base) by adding Dense layers on top, and run
the whole thing from end to end on the input data. This will allow us 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.
Leveraging a pretrained model 229
We’ll cover both techniques. Let’s walk through the code required to set up the first
one: recording the output of conv_base on our data and using these outputs as inputs
to a new model.
FAST FEATURE EXTRACTION WITHOUT DATA AUGMENTATION
We’ll start by extracting features as NumPy arrays by calling the predict() method of
the conv_base model on our training, validation, and testing datasets.
Let’s iterate over our datasets to extract the VGG16 features.
import numpy as np
def get_features_and_labels(dataset):
all_features = []
all_labels = []
for images, labels in dataset:
preprocessed_images = [Link].vgg16.preprocess_input(images)
features = conv_base.predict(preprocessed_images)
all_features.append(features)
all_labels.append(labels)
return [Link](all_features), [Link](all_labels)
Importantly, predict() only expects images, not labels, but our current dataset yields
batches that contain both images and their labels. Moreover, the VGG16 model expects
inputs that are preprocessed with the function [Link]-
cess_input, which scales pixel values to an appropriate range.
The extracted features are currently of shape (samples, 5, 5, 512):
>>> train_features.shape
(2000, 5, 5, 512)
At this point, we can define our densely connected classifier (note the use of dropout
for regularization) and train it on the data and labels that we just recorded.
[Link](loss="binary_crossentropy",
optimizer="rmsprop",
metrics=["accuracy"])
callbacks = [
[Link](
filepath="feature_extraction.keras",
save_best_only=True,
monitor="val_loss")
]
history = [Link](
train_features, train_labels,
epochs=20,
validation_data=(val_features, val_labels),
callbacks=callbacks)
Training is very fast because we only have to deal with two Dense layers—an epoch
takes less than one second even on CPU.
Let’s look at the loss and accuracy curves during training (see figure 8.13).
Figure 8.13 Training and validation metrics for plain feature extraction
conv_base = [Link].vgg16.VGG16(
weights="imagenet",
include_top=False)
conv_base.trainable = False
Setting trainable to False empties the list of trainable weights of the layer or model.
Listing 8.24 Printing the list of trainable weights before and after freezing
Listing 8.25 Adding a data augmentation stage and a classifier to the convolutional base
data_augmentation = [Link](
[
[Link]("horizontal"),
[Link](0.1),
[Link](0.2),
]
)
With this setup, only the weights from the two Dense layers that we added will be
trained. That’s a total of four weight tensors: two per layer (the main weight matrix
and the bias vector). Note that in order for these changes to take effect, you must first
compile the model. If you ever modify weight trainability after compilation, you
should then recompile the model, or these changes will be ignored.
Let’s train our model. Thanks to data augmentation, it will take much longer for
the model to start overfitting, so we can train for more epochs—let’s do 50.
NOTE 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.
callbacks = [
[Link](
filepath="feature_extraction_with_data_augmentation.keras",
save_best_only=True,
monitor="val_loss")
]
Leveraging a pretrained model 233
history = [Link](
train_dataset,
epochs=50,
validation_data=validation_dataset,
callbacks=callbacks)
Let’s plot the results again (see figure 8.14). As you can see, we reach a validation
accuracy of over 98%. This is a strong improvement over the previous model.
Figure 8.14 Training and validation metrics for feature extraction with data augmentation
test_model = [Link].load_model(
"feature_extraction_with_data_augmentation.keras")
test_loss, test_acc = test_model.evaluate(test_dataset)
print(f"Test accuracy: {test_acc:.3f}")
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 vali-
dation 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.
234 CHAPTER 8 Introduction to deep learning for computer vision
>>> conv_base.summary()
Model: "vgg16"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_19 (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
_________________________________________________________________
block2_pool (MaxPooling2D) (None, 45, 45, 128) 0
_________________________________________________________________
block3_conv1 (Conv2D) (None, 45, 45, 256) 295168
_________________________________________________________________
block3_conv2 (Conv2D) (None, 45, 45, 256) 590080
_________________________________________________________________
block3_conv3 (Conv2D) (None, 45, 45, 256) 590080
_________________________________________________________________
block3_pool (MaxPooling2D) (None, 22, 22, 256) 0
_________________________________________________________________
block4_conv1 (Conv2D) (None, 22, 22, 512) 1180160
_________________________________________________________________
block4_conv2 (Conv2D) (None, 22, 22, 512) 2359808
_________________________________________________________________
block4_conv3 (Conv2D) (None, 22, 22, 512) 2359808
_________________________________________________________________
block4_pool (MaxPooling2D) (None, 11, 11, 512) 0
_________________________________________________________________
block5_conv1 (Conv2D) (None, 11, 11, 512) 2359808
_________________________________________________________________
block5_conv2 (Conv2D) (None, 11, 11, 512) 2359808
_________________________________________________________________
block5_conv3 (Conv2D) (None, 11, 11, 512) 2359808
_________________________________________________________________
block5_pool (MaxPooling2D) (None, 5, 5, 512) 0
=================================================================
Total params: 14,714,688
Trainable params: 14,714,688
Non-trainable params: 0
_________________________________________________________________
We’ll fine-tune the last three convolutional layers, which means all layers up to block4_
pool should be frozen, and the layers block5_conv1, block5_conv2, and block5_conv3
should be trainable.
236 CHAPTER 8 Introduction to deep learning for computer vision
Why not fine-tune more layers? Why not fine-tune the entire convolutional base?
You could. But you need to consider the following:
Earlier layers in the convolutional base encode more generic, reusable features,
whereas layers higher up encode more specialized features. It’s more useful to
fine-tune the more specialized features, because these are the ones that need
to be repurposed on your new problem. There would be fast-decreasing returns
in fine-tuning lower layers.
The more parameters you’re training, the more you’re at risk of overfitting.
The convolutional base has 15 million parameters, so it would be risky to
attempt to train it on your small dataset.
Thus, in this situation, it’s a good strategy to fine-tune only the top two or three layers
in the convolutional base. Let’s set this up, starting from where we left off in the previ-
ous example.
Listing 8.27 Freezing all layers until the fourth from the last
conv_base.trainable = True
for layer in conv_base.layers[:-4]:
[Link] = False
Now we can begin fine-tuning the model. We’ll do this with the RMSprop optimizer,
using a very low learning rate. The reason for using a low learning rate is that we want to
limit the magnitude of the modifications we make to the representations of the three
layers we’re fine-tuning. Updates that are too large may harm these representations.
[Link](loss="binary_crossentropy",
optimizer=[Link](learning_rate=1e-5),
metrics=["accuracy"])
callbacks = [
[Link](
filepath="fine_tuning.keras",
save_best_only=True,
monitor="val_loss")
]
history = [Link](
train_dataset,
epochs=30,
validation_data=validation_dataset,
callbacks=callbacks)
model = [Link].load_model("fine_tuning.keras")
test_loss, test_acc = [Link](test_dataset)
print(f"Test accuracy: {test_acc:.3f}")
Summary 237
Here, we get a test accuracy of 98.5% (again, your own results may be within one per-
centage point). In the original Kaggle competition around this dataset, this would
have been one of the top results. It’s not quite a fair comparison, however, since we
used pretrained features that already contained prior knowledge about cats and dogs,
which competitors couldn’t use at the time.
On the positive side, by leveraging modern deep learning techniques, we managed
to reach this result using only a small fraction of the training data that was available
for the competition (about 10%). There is a huge difference between being able to
train on 20,000 samples compared to 2,000 samples!
Now you have a solid set of tools for dealing with image-classification problems—in
particular, with small datasets.
Summary
Convnets are the best type of machine learning models for computer vision
tasks. It’s possible to train one from scratch even on a very small dataset, with
decent results.
Convnets work by learning a hierarchy of modular patterns and concepts to
represent the visual world.
On a small dataset, overfitting will be the main issue. Data augmentation is a
powerful way to fight overfitting when you’re working with image data.
It’s easy to reuse an existing convnet on a new dataset via feature extraction.
This is a valuable technique for working with small image datasets.
As a complement to feature extraction, you can use fine-tuning, which adapts to
a new problem some of the representations previously learned by an existing
model. This pushes performance a bit further.
Advanced deep learning
for computer vision
The previous chapter gave you a first introduction to deep learning for computer
vision via simple models (stacks of Conv2D and MaxPooling2D layers) and a simple
use case (binary image classification). But there’s more to computer vision than
image classification! This chapter dives deeper into more diverse applications and
advanced best practices.
238
Three essential computer vision tasks 239
in computer vision. In general, there are three essential computer vision tasks you
need to know about:
Image classification—Where the goal is to assign one or more labels to an image.
It may be either single-label classification (an image can only be in one cate-
gory, excluding the others), or multi-label classification (tagging all categories
that an image belongs to, as seen in figure 9.1). For example, when you search
for a keyword on the Google Photos app, behind the scenes you’re querying a
very large multilabel classification model—one with over 20,000 different classes,
trained on millions of images.
Image segmentation—Where the goal is to “segment” or “partition” an image into
different areas, with each area usually representing a category (as seen in fig-
ure 9.1). For instance, when Zoom or Google Meet diplays a custom back-
ground behind you in a video call, it’s using an image segmentation model to
tell your face apart from what’s behind it, at pixel precision.
Object detection—Where the goal is to draw rectangles (called bounding boxes)
around objects of interest in an image, and associate each rectangle with a class.
A self-driving car could use an object-detection model to monitor cars, pedestri-
ans, and signs in view of its cameras, for instance.
Figure 9.1 The three main computer vision tasks: classification, segmentation, detection
240 CHAPTER 9 Advanced deep learning for computer vision
Deep learning for computer vision also encompasses a number of somewhat more
niche tasks besides these three, such as image similarity scoring (estimating how visu-
ally similar two images are), keypoint detection (pinpointing attributes of interest in
an image, such as facial features), pose estimation, 3D mesh estimation, and so on.
But to start with, image classification, image segmentation, and object detection form
the foundation that every machine learning engineer should be familiar with. Most
computer vision applications boil down to one of these three.
You’ve seen image classification in action in the previous chapter. Next, let’s dive
into image segmentation. It’s a very useful and versatile technique, and you can straight-
forwardly approach it with what you’ve already learned so far.
Note that we won’t cover object detection, because it would be too specialized and
too complicated for an introductory book. However, you can check out the RetinaNet
example on [Link], which shows how to build and train an object detection model
from scratch in Keras in around 450 lines of code ([Link]
retinanet/).
In this example, we’ll focus on semantic segmentation: we’ll be looking once again at
images of cats and dogs, and this time we’ll learn how to tell apart the main subject
and its background.
We’ll work with the Oxford-IIIT Pets dataset ([Link]/~vgg/data/
pets/), which contains 7,390 pictures of various breeds of cats and dogs, together with
foreground-background segmentation masks for each picture. A segmentation mask is
the image-segmentation equivalent of a label: it’s an image the same size as the input
image, with a single color channel where each integer value corresponds to the class
An image segmentation example 241
of the corresponding pixel in the input image. In our case, the pixels of our segmen-
tation masks can take one of three integer values:
1 (foreground)
2 (background)
3 (contour)
Let’s start by downloading and uncompressing our dataset, using the wget and tar
shell utilities:
!wget [Link]
!wget [Link]
!tar -xf [Link]
!tar -xf [Link]
The input pictures are stored as JPG files in the images/ folder (such as images/Abys-
sinian_1.jpg), and the corresponding segmentation mask is stored as a PNG file with
the same name in the annotations/trimaps/ folder (such as annotations/trimaps/
Abyssinian_1.png).
Let’s prepare the list of input file paths, as well as the list of the corresponding
mask file paths:
import os
input_dir = "images/"
target_dir = "annotations/trimaps/"
input_img_paths = sorted(
[[Link](input_dir, fname)
for fname in [Link](input_dir)
if [Link](".jpg")])
242 CHAPTER 9 Advanced deep learning for computer vision
target_paths = sorted(
[[Link](target_dir, fname)
for fname in [Link](target_dir)
if [Link](".png") and not [Link](".")])
Now, what does one of these inputs and its mask look like? Let’s take a quick look.
Here’s a sample image (see figure 9.3):
def display_target(target_array):
normalized_array = (target_array.astype("uint8") - 1) * 127
[Link]("off")
[Link](normalized_array[:, :, 0])
Next, let’s load our inputs and targets into two NumPy arrays, and let’s split the arrays
into a training and a validation set. Since the dataset is very small, we can just load
everything into memory:
def path_to_target(path):
img = img_to_array(
load_img(path, target_size=img_size, color_mode="grayscale"))
img = [Link]("uint8") - 1
Subtract 1 so that our
return img labels become 0, 1, and 2.
input_imgs = [Link]((num_imgs,) + img_size + (3,), dtype="float32")
targets = [Link]((num_imgs,) + img_size + (1,), dtype="uint8")
for i in range(num_imgs):
Reserve input_imgs[i] = path_to_input_image(input_img_paths[i])
1,000 targets[i] = path_to_target(target_paths[i])
samples for
validation. num_val_samples = 1000 Load all images in the input_imgs
Split the train_input_imgs = input_imgs[:-num_val_samples] float32 array and their masks in the
data into a train_targets = targets[:-num_val_samples] targets uint8 array (same order). The
training and a val_input_imgs = input_imgs[-num_val_samples:] inputs have three channels (RBG values)
validation set. val_targets = targets[-num_val_samples:] and the targets have a single channel
(which contains integer labels).
244 CHAPTER 9 Advanced deep learning for computer vision
Model: "model"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) [(None, 200, 200, 3)] 0
_________________________________________________________________
rescaling (Rescaling) (None, 200, 200, 3) 0
_________________________________________________________________
conv2d (Conv2D) (None, 100, 100, 64) 1792
_________________________________________________________________
conv2d_1 (Conv2D) (None, 100, 100, 64) 36928
_________________________________________________________________
conv2d_2 (Conv2D) (None, 50, 50, 128) 73856
_________________________________________________________________
conv2d_3 (Conv2D) (None, 50, 50, 128) 147584
_________________________________________________________________
An image segmentation example 245
The first half of the model closely resembles the kind of convnet you’d use for image
classification: a stack of Conv2D layers, with gradually increasing filter sizes. We down-
sample our images three times by a factor of two each, ending up with activations of size
(25, 25, 256). The purpose of this first half is to encode the images into smaller feature
maps, where each spatial location (or pixel) contains information about a large spatial
chunk of the original image. You can understand it as a kind of compression.
One important difference between the first half of this model and the classifica-
tion models you’ve seen before is the way we do downsampling: in the classification
convnets from the last chapter, we used MaxPooling2D layers to downsample feature
maps. Here, we downsample by adding strides to every other convolution layer (if you
don’t remember the details of how convolution strides work, see “Understanding con-
volution strides” in section 8.1.1). We do this because, in the case of image segmenta-
tion, we care a lot about the spatial location of information in the image, since we need
to produce per-pixel target masks as output of the model. When you do 2 × 2 max
pooling, you are completely destroying location information within each pooling win-
dow: you return one scalar value per window, with zero knowledge of which of the
four locations in the windows the value came from. So while max pooling layers per-
form well for classification tasks, they would hurt us quite a bit for a segmentation
task. Meanwhile, strided convolutions do a better job at downsampling feature maps
while retaining location information. Throughout this book, you’ll notice that we
tend to use strides instead of max pooling in any model that cares about feature loca-
tion, such as the generative models in chapter 12.
The second half of the model is a stack of Conv2DTranspose layers. What are those?
Well, the output of the first half of the model is a feature map of shape (25, 25, 256),
246 CHAPTER 9 Advanced deep learning for computer vision
but we want our final output to have the same shape as the target masks, (200, 200,
3). Therefore, we need to apply a kind of inverse of the transformations we’ve applied
so far—something that will upsample the feature maps instead of downsampling them.
That’s the purpose of the Conv2DTranspose layer: you can think of it as a kind of convolu-
tion layer that learns to upsample. If you have an input of shape (100, 100, 64), and you
run it through the layer Conv2D(128, 3, strides=2, padding="same"), you get an
output of shape (50, 50, 128). If you run this output through the layer Conv2D-
Transpose(64, 3, strides=2, padding="same"), you get back an output of shape (100,
100, 64), the same as the original. So after compressing our inputs into feature maps of
shape (25, 25, 256) via a stack of Conv2D layers, we can simply apply the corresponding
sequence of Conv2DTranspose layers to get back to images of shape (200, 200, 3).
We can now compile and fit our model:
[Link](optimizer="rmsprop", loss="sparse_categorical_crossentropy")
callbacks = [
[Link]("oxford_segmentation.keras",
save_best_only=True)
]
Let’s display our training and validation loss (see figure 9.5):
val_loss = [Link]["val_loss"]
[Link]()
[Link](epochs, loss, "bo", label="Training loss")
[Link](epochs, val_loss, "b", label="Validation loss")
[Link]("Training and validation loss")
[Link]()
You can see that we start overfitting midway, around epoch 25. Let’s reload our best
performing model according to the validation loss, and demonstrate how to use it to
predict a segmentation mask (see figure 9.6):
model = [Link].load_model("oxford_segmentation.keras")
i = 4
test_image = val_input_imgs[i]
[Link]("off")
[Link](array_to_img(test_image))
def display_mask(pred):
Utility to display
mask = [Link](pred, axis=-1) a model’s
mask *= 127 prediction
[Link]("off")
[Link](mask)
display_mask(mask)
There are a couple of small artifacts in our predicted mask, caused by geometric shapes
in the foreground and background. Nevertheless, our model appears to work nicely.
By this point, throughout chapter 8 and the beginning of chapter 9, you’ve
learned the basics of how to perform image classification and image segmentation:
you can already accomplish a lot with what you know. However, the convnets that
experienced engineers develop to solve real-world problems aren’t quite as simple as
those we’ve been using in our demonstrations so far. You’re still lacking the essential
mental models and thought processes that enable experts to make quick and accurate
decisions about how to put together state-of-the-art models. To bridge that gap, you
need to learn about architecture patterns. Let’s dive in.
Let’s start from the bird’s-eye view: the modularity-hierarchy-reuse (MHR) formula
for system architecture.
Figure 9.7 Complex systems follow a hierarchical structure and are organized into distinct
modules, which are reused multiple times (such as your four limbs, which are all variants of
the same blueprint, or your 20 “fingers”).
If you’re a software engineer, you’re already keenly familiar with these principles: an
effective codebase is one that is modular, hierarchical, and where you don’t reimple-
ment the same thing twice, but instead rely on reusable classes and functions. If you
250 CHAPTER 9 Advanced deep learning for computer vision
factor your code by following these principles, you could say you’re doing “software
architecture.”
Deep learning itself is simply the application of this recipe to continuous optimiza-
tion via gradient descent: you take a classic optimization technique (gradient descent
over a continuous function space), and you structure the search space into modules
(layers), organized into a deep hierarchy (often just a stack, the simplest kind of hier-
archy), where you reuse whatever you can (for instance, convolutions are all about
reusing the same information in different spatial locations).
Likewise, deep learning model architecture is primarily about making clever use of
modularity, hierarchy, and reuse. You’ll notice that all popular convnet architectures
are not only structured into layers, they’re structured into repeated groups of layers
(called “blocks” or “modules”). For instance, the popular VGG16 architecture we used
in the previous chapter is structured into repeated “conv, conv, max pooling” blocks
(see figure 9.8).
Further, most convnets often feature pyramid-like structures (feature hierarchies).
Recall, for example, the progression in the number of convolution filters we used in
the first convnet we built in the previous chapter: 32, 64, 128. The number of filters
grows with layer depth, while the size of the feature maps shrinks accordingly. You’ll
notice the same pattern in the blocks of the VGG16 model (see figure 9.8).
56 × 56 × 256
28 × 28 × 512 7 × 7 × 512
14 × 14 × 512 1 × 1 × 4096 1 × 1 × 1000
Convolution+ReLU
Max pooling
Fully connected+ReLU
Softmax
Figure 9.8 The VGG16 architecture: note the repeated layer blocks and the pyramid-like structure of
the feature maps
Modern convnet architecture patterns 251
Deeper hierarchies are intrinsically good because they encourage feature reuse, and
therefore abstraction. In general, a deep stack of narrow layers performs better than a
shallow stack of large layers. However, there’s a limit to how deep you can stack layers,
due to the problem of vanishing gradients. This leads us to our first essential model
architecture pattern: residual connections.
This is made worse by the incentives that deep learning researchers face: by making
a system more complex than necessary, they can make it appear more interesting or
more novel, and thus increase their chances of getting a paper through the peer-
review process. If you read lots of deep learning papers, you will notice that they’re
often optimized for peer review in both style and content in ways that actively hurt
clarity of explanation and reliability of results. For instance, mathematics in deep
learning papers is rarely used for clearly formalizing concepts or deriving non-obvious
results—rather, it gets leveraged as a signal of seriousness, like an expensive suit on
a salesman.
The goal of research shouldn’t be merely to publish, but to generate reliable knowl-
edge. Crucially, understanding causality in your system is the most straightforward
way to generate reliable knowledge. And there’s a very low-effort way to look into cau-
sality: ablation studies. Ablation studies consist of systematically trying to remove
parts of a system—making it simpler—to identify where its performance actually
comes from. If you find that X + Y + Z gives you good results, also try X, Y, Z, X + Y,
X + Z, and Y + Z, and see what happens.
If you become a deep learning researcher, cut through the noise in the research pro-
cess: do ablation studies for your models. Always ask, “Could there be a simpler
explanation? Is this added complexity really necessary? Why?”
y = f4(f3(f2(f1(x))))
252 CHAPTER 9 Advanced deep learning for computer vision
The name of the game is to adjust the parameters of each function in the chain based
on the error recorded on the output of f4 (the loss of the model). To adjust f1, you’ll
need to percolate error information through f2, f3, and f4. However, each successive
function in the chain introduces some amount of noise. If your function chain is too
deep, this noise starts overwhelming gradient information, and backpropagation
stops working. Your model won’t train at all. This is the vanishing gradients problem.
The fix is simple: just force each function in the chain to be nondestructive—to
retain a noiseless version of the information contained in the previous input. The eas-
iest way to implement this is to use a residual connection. It’s dead easy: just add the
input of a layer or block of layers back to its output (see figure 9.9). The residual con-
nection acts as an information shortcut around destructive or noisy blocks (such as
blocks that contain relu activations or dropout layers), enabling error gradient infor-
mation from early layers to propagate noiselessly through a deep network. This tech-
nique was introduced in 2015 with the ResNet family of models (developed by He et al.
at Microsoft).1
Input
Residual
Block
connection
1
Kaiming He et al., “Deep Residual Learning for Image Recognition,” Conference on Computer Vision and Pat-
tern Recognition (2015), [Link]
Modern convnet architecture patterns 253
Note that adding the input back to the output of a block implies that the output
should have the same shape as the input. However, this is not the case if your block
includes convolutional layers with an increased number of filters, or a max pooling
layer. In such cases, use a 1 × 1 Conv2D layer with no activation to linearly project the
residual to the desired output shape (see listing 9.2). You’d typically use padding=
"same" in the convolution layers in your target block so as to avoid spatial downsam-
pling due to padding, and you’d use strides in the residual projection to match any
downsampling caused by a max pooling layer (see listing 9.3).
Listing 9.3 Case where the target block includes a max pooling layer
Set inputs = [Link](shape=(32, 32, 3))
aside the x = layers.Conv2D(32, 3, activation="relu")(inputs)
residual. residual = x
x = layers.Conv2D(64, 3, activation="relu", padding="same")(x)
x = layers.MaxPooling2D(2, padding="same")(x)
residual = layers.Conv2D(64, 1, strides=2)(residual)
x = [Link]([x, residual])
This is the block of two layers around which
Now the block output and the residual we create a residual connection: it includes a
have the same shape and can be added. 2 × 2 max pooling layer. Note that we use
padding="same" in both the convolution
We use strides=2 in the residual layer and the max pooling layer to avoid
projection to match the downsampling downsampling due to padding.
created by the max pooling layer.
To make these ideas more concrete, here’s an example of a simple convnet structured
into a series of blocks, each made of two convolution layers and one optional max
pooling layer, with a residual connection around each block:
With residual connections, you can build networks of arbitrary depth, without having
to worry about vanishing gradients.
Now let’s move on to the next essential convnet architecture pattern: batch normal-
ization.
Previous examples in this book normalized data before feeding it into models. But
data normalization may be of interest after every transformation operated by the net-
work: even if the data entering a Dense or Conv2D network has a 0 mean and unit vari-
ance, there’s no reason to expect a priori that this will be the case for the data coming
out. Could normalizing intermediate activations help?
Batch normalization does just that. It’s a type of layer (BatchNormalization in
Keras) introduced in 2015 by Ioffe and Szegedy;2 it can adaptively normalize data even
as the mean and variance change over time during training. During training, it uses
the mean and variance of the current batch of data to normalize samples, and during
inference (when a big enough batch of representative data may not be available), it
uses an exponential moving average of the batch-wise mean and variance of the data
seen during training.
2
Sergey Ioffe and Christian Szegedy, “Batch Normalization: Accelerating Deep Network Training by Reducing
Internal Covariate Shift,” Proceedings of the 32nd International Conference on Machine Learning (2015), https://
[Link]/abs/1502.03167.
256 CHAPTER 9 Advanced deep learning for computer vision
Although the original paper stated that batch normalization operates by “reducing
internal covariate shift,” no one really knows for sure why batch normalization helps.
There are various hypotheses, but no certitudes. You’ll find that this is true of many
things in deep learning—deep learning is not an exact science, but a set of ever-
changing, empirically derived engineering best practices, woven together by unreli-
able narratives. You will sometimes feel like the book you have in hand tells you how to
do something but doesn’t quite satisfactorily say why it works: that’s because we know
the how but we don’t know the why. Whenever a reliable explanation is available, I
make sure to mention it. Batch normalization isn’t one of those cases.
In practice, the main effect of batch normalization appears to be that it helps with
gradient propagation—much like residual connections—and thus allows for deeper
networks. Some very deep networks can only be trained if they include multiple
BatchNormalization layers. For instance, batch normalization is used liberally in
many of the advanced convnet architectures that come packaged with Keras, such as
ResNet50, EfficientNet, and Xception.
The BatchNormalization layer can be used after any layer— Dense, Conv2D, etc.:
NOTE Both Dense and Conv2D involve a bias vector, a learned variable whose
purpose is to make the layer affine rather than purely linear. For instance,
Conv2D returns, schematically, y = conv(x, kernel) + bias, and Dense returns
y = dot(x, kernel) + bias. Because the normalization step will take care of
centering the layer’s output on zero, the bias vector is no longer needed
when using BatchNormalization, and the layer can be created without it via
the option use_bias=False. This makes the layer slightly leaner.
Importantly, I would generally recommend placing the previous layer’s activation after
the batch normalization layer (although this is still a subject of debate). So instead of
doing what is shown in listing 9.4, you would do what’s shown in listing 9.5.
x = layers.Conv2D(32, 3, activation="relu")(x)
x = [Link]()(x)
Listing 9.5 How to use batch normalization: the activation comes last
x = layers.Conv2D(32, 3, use_bias=False)(x)
Note the lack of
x = [Link]()(x) activation here.
x = [Link]("relu")(x)
The intuitive reason for this approach is that batch normalization will center your
inputs on zero, while your relu activation uses zero as a pivot for keeping or dropping
activated channels: doing normalization before the activation maximizes the utiliza-
tion of the relu. That said, this ordering best practice is not exactly critical, so if you
do convolution, then activation, and then batch normalization, your model will still
train, and you won’t necessarily see worse results.
Now let’s take a look at the last architecture pattern in our series: depthwise separable
convolutions.
1 × 1 conv
(pointwise conv)
Concatenate
Depthwise convolution:
independent spatial
3 × 3 conv 3 × 3 conv 3 × 3 conv 3 × 3 conv convs per channel
Split channels
This is equivalent to separating the learning of spatial features and the learning of
channel-wise features. In much the same way that convolution relies on the assump-
tion that the patterns in images are not tied to specific locations, depthwise separable
convolution relies on the assumption that spatial locations in intermediate activations
are highly correlated, but different channels are highly independent. Because this assumption
is generally true for the image representations learned by deep neural networks, it
serves as a useful prior that helps the model make more efficient use of its training
data. A model with stronger priors about the structure of the information it will have
to process is a better model—as long as the priors are accurate.
Depthwise separable convolution requires significantly fewer parameters and
involves fewer computations compared to regular convolution, while having compara-
ble representational power. It results in smaller models that converge faster and are
less prone to overfitting. These advantages become especially important when you’re
training small models from scratch on limited data.
When it comes to larger-scale models, depthwise separable convolutions are the
basis of the Xception architecture, a high-performing convnet that comes packaged
with Keras. You can read more about the theoretical grounding for depthwise separa-
ble convolutions and Xception in the paper “Xception: Deep Learning with Depth-
wise Separable Convolutions.”3
3
François Chollet, “Xception: Deep Learning with Depthwise Separable Convolutions,” Conference on Com-
puter Vision and Pattern Recognition (2017), [Link]
Modern convnet architecture patterns 259
x = [Link]()(x)
x = [Link]("relu")(x)
x = layers.SeparableConv2D(size, 3, padding="same", use_bias=False)(x)
x = [Link]()(x)
x = [Link]("relu")(x)
x = layers.SeparableConv2D(size, 3, padding="same", use_bias=False)(x)
residual = layers.Conv2D(
size, 1, strides=2, padding="same", use_bias=False)(residual)
x = [Link]([x, residual])
In the original model, we used a Flatten
x = layers.GlobalAveragePooling2D()(x) layer before the Dense layer. Here, we go
x = [Link](0.5)(x) with a GlobalAveragePooling2D layer.
outputs = [Link](1, activation="sigmoid")(x)
model = [Link](inputs=inputs, outputs=outputs)
Like in the original model, we add a Note that the assumption that underlies
dropout layer for regularization. separable convolution, “feature channels are
largely independent,” does not hold for RGB
We apply a series of convolutional blocks with images! Red, green, and blue color channels
increasing feature depth. Each block consists of two are actually highly correlated in natural
batch-normalized depthwise separable convolution images. As such, the first layer in our model
layers and a max pooling layer, with a residual is a regular Conv2D layer. We’ll start using
connection around the entire block. SeparableConv2D afterwards.
This convnet has a trainable parameter count of 721,857, slightly lower than the
991,041 trainable parameters of the original model, but still in the same ballpark. Fig-
ure 9.11 shows its training and validation curves.
You’ll find that our new model achieves a test accuracy of 90.8%, compared to 83.5%
for the naive model in the last chapter. As you can see, following architecture best
practices does have an immediate, sizable impact on model performance!
At this point, if you want to further improve performance, you should start system-
atically tuning the hyperparameters of your architecture—a topic we’ll cover in detail
in chapter 13. We haven’t gone through this step here, so the configuration of the pre-
ceding model is purely based on the best practices we discussed, plus, when it comes
to gauging model size, a small amount of intuition.
Note that these architecture best practices are relevant to computer vision in gen-
eral, not just image classification. For example, Xception is used as the standard convo-
lutional base in DeepLabV3, a popular state-of-the-art image segmentation solution.4
This concludes our introduction to essential convnet architecture best practices.
With these principles in hand, you’ll be able to develop higher-performing models
across a wide range of computer vision tasks. You’re now well on your way to becom-
ing a proficient computer vision practitioner. To further deepen your expertise,
there’s one last important topic we need to cover: interpreting how a model arrives at
its predictions.
4
Liang-Chieh Chen et al., “Encoder-Decoder with Atrous Separable Convolution for Semantic Image Segmen-
tation,” ECCV (2018), [Link]
262 CHAPTER 9 Advanced deep learning for computer vision
Next, we’ll get an input image—a picture of a cat, not part of the images the network
was trained on.
img_path = [Link].get_file(
fname="[Link]",
Download a
origin="[Link]
test image.
In order to extract the feature maps we want to look at, we’ll create a Keras model that
takes batches of images as input, and that outputs the activations of all convolution
and pooling layers.
264 CHAPTER 9 Advanced deep learning for computer vision
activations = activation_model.predict(img_tensor)
Return a list of nine NumPy arrays:
one array per layer activation.
For instance, this is the activation of the first convolution layer for the cat image input:
It’s a 178 × 178 feature map with 32 channels. Let’s try plotting the fifth channel of the
activation of the first layer of the original model (see figure 9.13).
This channel appears to encode a diagonal edge detector—but note that your own
channels may vary, because the specific filters learned by convolution layers aren’t
deterministic.
Now, let’s plot a complete visualization of all the activations in the network (see fig-
ure 9.14). We’ll extract and plot every channel in each of the layer activations, and
we’ll stack the results in one big grid, with channels stacked side by side.
if channel_image.sum() != 0:
Normalize channel channel_image -= channel_image.mean()
values within the channel_image /= channel_image.std()
[0, 255] range. All- channel_image *= 64
zero channels are channel_image += 128
kept at zero. channel_image = [Link](channel_image, 0, 255).astype("uint8")
display_grid[
col * (size + 1): (col + 1) * size + col,
Place the row * (size + 1) : (row + 1) * size + row] = channel_image
channel scale = 1. / size
matrix in the [Link](figsize=(scale * display_grid.shape[1],
empty grid scale * display_grid.shape[0])) Display the
we prepared. [Link](layer_name) grid for the
[Link](False) layer.
[Link]("off")
[Link](display_grid, aspect="auto", cmap="viridis")
Figure 9.15 Left: attempts to draw a bicycle from memory. Right: what a
schematic bicycle should look like.
268 CHAPTER 9 Advanced deep learning for computer vision
into high-level visual concepts while filtering out irrelevant visual details—making it
tremendously difficult to remember how things around you look.
model = [Link](
weights="imagenet",
include_top=False) The classification layers are irrelevant
for this use case, so we don’t include
the top stage of the model.
We’re interested in the convolutional layers of the model—the Conv2D and Separa-
bleConv2D layers. We’ll need to know their names so we can retrieve their outputs.
Let’s print their names, in order of depth.
You’ll notice that the SeparableConv2D layers here are all named something like
block6_sepconv1, block7_sepconv2, etc. Xception is structured into blocks, each
containing several convolutional layers.
Now, let’s create a second model that returns the output of a specific layer—a fea-
ture extractor model. Because our model is a Functional API model, it is inspectable: we
can query the output of one of its layers and reuse it in a new model. No need to copy
the entire Xception code.
Interpreting what convnets learn 269
To use this model, simply call it on some input data (note that Xception requires
inputs to be preprocessed via the [Link].preprocess_input
function).
activation = feature_extractor(
[Link].preprocess_input(img_tensor)
)
Let’s use our feature extractor model to define a function that returns a scalar value
quantifying how much a given input image “activates” a given filter in the layer. This is
the “loss function” that we’ll maximize during the gradient ascent process:
(continued)
y_batch = model(x).numpy()
y_batches.append(y_batch)
return [Link](y_batches)
This means that predict() calls can scale to very large arrays. Meanwhile,
model(x) happens in-memory and doesn’t scale. On the other hand, predict() is
not differentiable: you cannot retrieve its gradient if you call it in a GradientTape
scope.
You should use model(x) when you need to retrieve the gradients of the model call,
and you should use predict() if you just need the output value. In other words,
always use predict() unless you’re in the middle of writing a low-level gradient
descent loop (as we are now).
Let’s set up the gradient ascent step function, using the GradientTape. Note that we’ll
use a @[Link] decorator to speed it up.
A non-obvious trick to help the gradient descent process go smoothly is to normal-
ize the gradient tensor by dividing it by its L2 norm (the square root of the average of
the square of the values in the tensor). This ensures that the magnitude of the
updates done to the input image is always within the same range.
Now we have all the pieces. Let’s put them together into a Python function that takes
as input a layer name and a filter index, and returns a tensor representing the pattern
that maximizes the activation of the specified filter.
img_width = 200
img_height = 200
Interpreting what convnets learn 271
def generate_filter_pattern(filter_index):
iterations = 30
Number of gradient Initialize an image tensor
Amplitude learning_rate = 10. ascent steps to apply with random values (the
of a single image = [Link]( Xception model expects
step minval=0.4, input values in the [0, 1]
maxval=0.6, range, so here we pick a
shape=(1, img_width, img_height, 3)) range centered on 0.5).
for i in range(iterations):
image = gradient_ascent_step(image, filter_index, learning_rate)
return image[0].numpy()
Repeatedly update the values of the image
tensor so as to maximize our loss function.
The resulting image tensor is a floating-point array of shape (200, 200, 3), with val-
ues that may not be integers within [0, 255]. Hence, we need to post-process this ten-
sor to turn it into a displayable image. We do so with the following straightforward
utility function.
def deprocess_image(image):
image -= [Link]()
image /= [Link]() Normalize image
image *= 64 values within the
image += 128 [0, 255] range.
image = [Link](image, 0, 255).astype("uint8")
image = image[25:-25, 25:-25, :]
Center crop to avoid
return image border artifacts.
>>> [Link]("off")
>>> [Link](deprocess_image(generate_filter_pattern(filter_index=2)))
all_images = []
Generate and save
for filter_index in range(64): visualizations for
print(f"Processing filter {filter_index}") the first 64 filters
image = deprocess_image( in the layer.
generate_filter_pattern(filter_index)
)
all_images.append(image)
for i in range(n):
Fill the picture with
for j in range(n): the saved filters.
image = all_images[i * n + j]
stitched_filters[
row_start = (cropped_width + margin) * i
row_end = (cropped_width + margin) * i + cropped_width
column_start = (cropped_height + margin) * j
column_end = (cropped_height + margin) * j + cropped_height
stitched_filters[
Save the row_start: row_end,
canvas to disk. column_start: column_end, :] = image
[Link].save_img(
f"filters_for_layer_{layer_name}.png", stitched_filters)
These filter visualizations (see figure 9.17) tell you a lot about how convnet layers see
the world: each layer in a convnet learns a collection of filters such that their inputs can
be expressed as a combination of the filters. This is similar to how the Fourier transform
decomposes signals onto a bank of cosine functions. The filters in these convnet filter
banks get increasingly complex and refined as you go deeper in the model:
The filters from the first layers in the model encode simple directional edges
and colors (or colored edges, in some cases).
The filters from layers a bit further up the stack, such as block4_sepconv1,
encode simple textures made from combinations of edges and colors.
The filters in higher layers begin to resemble textures found in natural images:
feathers, eyes, leaves, and so on.
Interpreting what convnets learn 273
class activation heatmap is a 2D grid of scores associated with a specific output class,
computed for every location in any input image, indicating how important each loca-
tion is with respect to the class under consideration. For instance, given an image fed
into a dogs-versus-cats convnet, CAM visualization would allow you to generate a heat-
map for the class “cat,” indicating how cat-like different parts of the image are, and
also a heatmap for the class “dog,” indicating how dog-like parts of the image are.
The specific implementation we’ll use is the one described in an article titled “Grad-
CAM: Visual Explanations from Deep Networks via Gradient-based Localization.”5
Grad-CAM consists of taking the output feature map of a convolution layer, given
an input image, and weighing every channel in that feature map by the gradient of
the class with respect to the channel. Intuitively, one way to understand this trick is to
imagine that you’re weighting a spatial map of “how intensely the input image acti-
vates different channels” by “how important each channel is with regard to the class,”
resulting in a spatial map of “how intensely the input image activates the class.”
Let’s demonstrate this technique using the pretrained Xception model.
model = [Link](weights="imagenet")
5
Ramprasaath R. Selvaraju et al., arXiv (2017), [Link]
Interpreting what convnets learn 275
You can now run the pretrained network on the image and decode its prediction vec-
tor back to a human-readable format:
The top three classes predicted for this image are as follows:
African elephant (with 87% probability)
Tusker (with 7% probability)
Indian elephant (with 2% probability)
276 CHAPTER 9 Advanced deep learning for computer vision
>>> [Link](preds[0])
386
To visualize which parts of the image are the most African-elephant–like, let’s set up
the Grad-CAM process.
First, we create a model that maps the input image to the activations of the last
convolutional layer.
Listing 9.22 Setting up a model that returns the last convolutional output
last_conv_layer_name = "block14_sepconv2_act"
classifier_layer_names = [
"avg_pool",
"predictions",
]
last_conv_layer = model.get_layer(last_conv_layer_name)
last_conv_layer_model = [Link]([Link], last_conv_layer.output)
Second, we create a model that maps the activations of the last convolutional layer to
the final class predictions.
Listing 9.23 Reapplying the classifier on top of the last convolutional output
classifier_input = [Link](shape=last_conv_layer.[Link][1:])
x = classifier_input
for layer_name in classifier_layer_names:
x = model.get_layer(layer_name)(x)
classifier_model = [Link](classifier_input, x)
Then we compute the gradient of the top predicted class for our input image with
respect to the activations of the last convolution layer.
Now we apply pooling and importance weighting to the gradient tensor to obtain our
heatmap of class activation.
heatmap = [Link](heatmap, 0)
heatmap /= [Link](heatmap)
[Link](heatmap)
Finally, let’s generate an image that superimposes the original image on the heatmap
we just obtained (see figure 9.20).
import [Link] as cm
Figure 9.20 African elephant class activation heatmap over the test picture
Summary 279
Summary
There are three essential computer vision tasks you can do with deep learning:
image classification, image segmentation, and object detection.
Following modern convnet architecture best practices will help you get the
most out of your models. Some of these best practices include using residual
connections, batch normalization, and depthwise separable convolutions.
The representations that convnets learn are easy to inspect—convnets are the
opposite of black boxes!
You can generate visualizations of the filters learned by your convnets, as well as
heatmaps of class activity.
Deep learning
for timeseries
280
A temperature-forecasting example 281
By far, the most common timeseries-related task is forecasting: predicting what will
happen next in a series. Forecast electricity consumption a few hours in advance so
you can anticipate demand; forecast revenue a few months in advance so you can plan
your budget; forecast the weather a few days in advance so you can plan your sched-
ule. Forecasting is what this chapter focuses on. But there’s actually a wide range of
other things you can do with timeseries:
Classification—Assign one or more categorical labels to a timeseries. For instance,
given the timeseries of the activity of a visitor on a website, classify whether the
visitor is a bot or a human.
Event detection—Identify the occurrence of a specific expected event within a
continuous data stream. A particularly useful application is “hotword detec-
tion,” where a model monitors an audio stream and detects utterances like “Ok
Google” or “Hey Alexa.”
Anomaly detection—Detect anything unusual happening within a continuous
datastream. Unusual activity on your corporate network? Might be an attacker.
Unusual readings on a manufacturing line? Time for a human to go take a look.
Anomaly detection is typically done via unsupervised learning, because you
often don’t know what kind of anomaly you’re looking for, so you can’t train on
specific anomaly examples.
When working with timeseries, you’ll encounter a wide range of domain-specific data-
representation techniques. For instance, you have likely already heard about the Fou-
rier transform, which consists of expressing a series of values in terms of a superposition
of waves of different frequencies. The Fourier transform can be highly valuable when
preprocessing any data that is primarily characterized by its cycles and oscillations
(like sound, the vibrations of the frame of a skyscraper, or your brain waves). In the
context of deep learning, Fourier analysis (or the related Mel-frequency analysis) and
other domain-specific representations can be useful as a form of feature engineering,
a way to prepare data before training a model on it, to make the job of the model eas-
ier. However, we won’t cover these techniques in these pages; we will instead focus on
the modeling part.
In this chapter, you’ll learn about recurrent neural networks (RNNs) and how to
apply them to timeseries forecasting.
see that densely connected networks and convolutional networks aren’t well-equipped
to deal with this kind of dataset, while a different kind of machine learning tech-
nique—recurrent neural networks (RNNs)—really shines on this type of problem.
We’ll work with a weather timeseries dataset recorded at the weather station at the
Max Planck Institute for Biogeochemistry in Jena, Germany.1 In this dataset, 14 differ-
ent quantities (such as temperature, pressure, humidity, wind direction, and so on)
were recorded every 10 minutes over several years. The original data goes back to
2003, but the subset of the data we’ll download is limited to 2009–2016.
Let’s start by downloading and uncompressing the data:
!wget [Link]
!unzip jena_climate_2009_2016.[Link]
import os
fname = [Link]("jena_climate_2009_2016.csv")
with open(fname) as f:
data = [Link]()
lines = [Link]("\n")
header = lines[0].split(",")
lines = lines[1:]
print(header)
print(len(lines))
This outputs a count of 420,551 lines of data (each line is a timestep: a record of a
date and 14 weather-related values), as well as the following header:
["Date Time",
"p (mbar)",
"T (degC)",
"Tpot (K)",
"Tdew (degC)",
"rh (%)",
"VPmax (mbar)",
"VPact (mbar)",
"VPdef (mbar)",
"sh (g/kg)",
"H2OC (mmol/mol)",
"rho (g/m**3)",
"wv (m/s)",
"max. wv (m/s)",
"wd (deg)"]
1
Adam Erickson and Olaf Kolle, [Link]/wetter.
A temperature-forecasting example 283
Now, convert all 420,551 lines of data into NumPy arrays: one array for the tempera-
ture (in degrees Celsius), and another one for the rest of the data—the features we
will use to predict future temperatures. Note that we discard the “Date Time” column.
import numpy as np
temperature = [Link]((len(lines),))
raw_data = [Link]((len(lines), len(header) - 1))
for i, line in enumerate(lines):
values = [float(x) for x in [Link](",")[1:]] We store column 1 in the
temperature[i] = values[1]
“temperature” array.
raw_data[i, :] = values[:]
We store all columns (including the
temperature) in the “raw_data” array.
Figure 10.1 shows the plot of temperature (in degrees Celsius) over time. On this plot,
you can clearly see the yearly periodicity of temperature—the data spans 8 years.
Figure 10.1 Temperature over the full temporal range of the dataset (ºC)
Figure 10.2 shows a more narrow plot of the first 10 days of temperature data. Because
the data is recorded every 10 minutes, you get 24 × 6 = 144 data points per day.
[Link](range(1440), temperature[:1440])
284 CHAPTER 10 Deep learning for timeseries
Figure 10.2 Temperature over the first 10 days of the dataset (ºC)
On this plot, you can see daily periodicity, especially for the last 4 days. Also note that
this 10-day period must be coming from a fairly cold winter month.
With our dataset, if you were trying to predict average temperature for the next month
given a few months of past data, the problem would be easy, due to the reliable year-
scale periodicity of the data. But looking at the data over a scale of days, the temperature
looks a lot more chaotic. Is this timeseries predictable at a daily scale? Let’s find out.
In all our experiments, we’ll use the first 50% of the data for training, the follow-
ing 25% for validation, and the last 25% for testing. When working with timeseries
data, it’s important to use validation and test data that is more recent than the train-
ing data, because you’re trying to predict the future given the past, not the reverse,
and your validation/test splits should reflect that. Some problems happen to be con-
siderably simpler if you reverse the time axis!
Listing 10.5 Computing the number of samples we’ll use for each data split
mean = raw_data[:num_train_samples].mean(axis=0)
raw_data -= mean
std = raw_data[:num_train_samples].std(axis=0)
raw_data /= std
Next, let’s create a Dataset object that yields batches of data from the past five days
along with a target temperature 24 hours in the future. Because the samples in the
dataset are highly redundant (sample N and sample N + 1 will have most of their time-
steps in common), it would be wasteful to explicitly allocate memory for every sample.
Instead, we’ll generate the samples on the fly while only keeping in memory the origi-
nal raw_data and temperature arrays, and nothing more.
We could easily write a Python generator to do this, but there’s a built-in dataset
utility in Keras that does just that (timeseries_dataset_from_array()), so we can
save ourselves some work by using it. You can generally use it for any kind of timeseries
forecasting task.
Understanding timeseries_dataset_from_array()
To understand what timeseries_dataset_from_array() does, let’s look at a sim-
ple example. The general idea is that you provide an array of timeseries data (the
data argument), and timeseries_dataset_from_array() gives you windows
extracted from the original timeseries (we’ll call them “sequences”).
For example, if you use data = [0 1 2 3 4 5 6] and sequence_length=3, then
timeseries_dataset_from_array() will generate the following samples: [0 1 2],
[1 2 3], [2 3 4], [3 4 5], [4 5 6].
286 CHAPTER 10 Deep learning for timeseries
(continued)
You can also pass a targets argument (an array) to timeseries_dataset_
from_array(). The first entry of the targets array should match the desired tar-
get for the first sequence that will be generated from the data array. So if you’re
doing timeseries forecasting, targets should be the same array as data, offset
by some amount.
For instance, with data = [0 1 2 3 4 5 6 …] and sequence_length=3, you could create
a dataset to predict the next step in the series by passing targets = [3 4 5 6 …]. Let’s
try it:
Generate an array
import numpy as np
of sorted integers The sequences
from tensorflow import keras from 0 to 9. we generate will
int_sequence = [Link](10) be sampled from
dummy_dataset = [Link].timeseries_dataset_from_array( [0 1 2 3 4 5 6].
data=int_sequence[:-3],
targets=int_sequence[3:],
sequence_length=3,
The target for the sequence that
starts at data[N] will be data[N + 3].
batch_size=2,
) The sequences will
be 3 steps long.
for inputs, targets in dummy_dataset: The sequences will be
for i in range([Link][0]): batched in batches of size 2.
print([int(x) for x in inputs[i]], int(targets[i]))
train_dataset = [Link].timeseries_dataset_from_array(
raw_data[:-delay],
targets=temperature[delay:],
sampling_rate=sampling_rate,
sequence_length=sequence_length,
shuffle=True,
batch_size=batch_size,
start_index=0,
end_index=num_train_samples)
val_dataset = [Link].timeseries_dataset_from_array(
raw_data[:-delay],
targets=temperature[delay:],
sampling_rate=sampling_rate,
sequence_length=sequence_length,
shuffle=True,
batch_size=batch_size,
start_index=num_train_samples,
end_index=num_train_samples + num_val_samples)
test_dataset = [Link].timeseries_dataset_from_array(
raw_data[:-delay],
targets=temperature[delay:],
sampling_rate=sampling_rate,
sequence_length=sequence_length,
shuffle=True,
batch_size=batch_size,
start_index=num_train_samples + num_val_samples)
Each dataset yields a tuple (samples, targets), where samples is a batch of 256 sam-
ples, each containing 120 consecutive hours of input data, and targets is the corre-
sponding array of 256 target temperatures. Note that the samples are randomly
shuffled, so two consecutive sequences in a batch (like samples[0] and samples[1])
aren’t necessarily temporally close.
def evaluate_naive_method(dataset):
total_abs_err = 0.
samples_seen = 0
for samples, targets in dataset:
preds = samples[:, -1, 1] * std[1] + mean[1]
total_abs_err += [Link]([Link](preds - targets))
samples_seen += [Link][0]
return total_abs_err / samples_seen
The temperature feature is at column 1, so samples[:, -1, 1] is the last temperature measurement in the
input sequence. Recall that we normalized our features, so to retrieve a temperature in degrees Celsius,
we need to un-normalize it by multiplying it by the standard deviation and adding back the mean.
This common-sense baseline achieves a validation MAE of 2.44 degrees Celsius and a
test MAE of 2.62 degrees Celsius. So if you always assume that the temperature 24
hours in the future will be the same as it is now, you will be off by two and a half
degrees on average. It’s not too bad, but you probably won’t launch a weather fore-
casting service based on this heuristic. Now the game is to use your knowledge of deep
learning to do better.
A temperature-forecasting example 289
Let’s display the loss curves for validation and training (see figure 10.3).
Some of the validation losses are close to the no-learning baseline, but not reliably.
This goes to show the merit of having this baseline in the first place: it turns out to be
not easy to outperform. Your common sense contains a lot of valuable information to
which a machine learning model doesn’t have access.
You may wonder, if a simple, well-performing model exists to go from the data to
the targets (the common-sense baseline), why doesn’t the model you’re training find it
and improve on it? Well, the space of models in which you’re searching for a solution—
that is, your hypothesis space—is the space of all possible two-layer networks with the
configuration you defined. The common-sense heuristic is just one model among mil-
lions that can be represented in this space. It’s like looking for a needle in a haystack.
Just because a good solution technically exists in your hypothesis space doesn’t mean
you’ll be able to find it via gradient descent.
That’s a pretty significant limitation of machine learning in general: unless the
learning algorithm is hardcoded to look for a specific kind of simple model, it can
sometimes fail to find a simple solution to a simple problem. That’s why leveraging
good feature engineering and relevant architecture priors is essential: you need to
precisely tell your model what it should be looking for.
3D versions of these layers: Conv1D, SeparableConv1D, and Conv3D.2 The Conv1D layer
relies on 1D windows that slide across input sequences, and the Conv3D layer relies on
cubic windows that slide across input volumes.
You can thus build 1D convnets, strictly analogous to 2D convnets. They’re a great
fit for any sequence data that follows the translation invariance assumption (meaning
that if you slide a window over the sequence, the content of the window should follow
the same properties independently of the location of the window).
Let’s try one on our temperature-forecasting problem. We’ll pick an initial window
length of 24, so that we look at 24 hours of data at a time (one cycle). As we downsam-
ple the sequences (via MaxPooling1D layers), we’ll reduce the window size accordingly:
inputs = [Link](shape=(sequence_length, raw_data.shape[-1]))
x = layers.Conv1D(8, 24, activation="relu")(inputs)
x = layers.MaxPooling1D(2)(x)
x = layers.Conv1D(8, 12, activation="relu")(x)
x = layers.MaxPooling1D(2)(x)
x = layers.Conv1D(8, 6, activation="relu")(x)
x = layers.GlobalAveragePooling1D()(x)
outputs = [Link](1)(x)
model = [Link](inputs, outputs)
callbacks = [
[Link]("jena_conv.keras",
save_best_only=True)
]
[Link](optimizer="rmsprop", loss="mse", metrics=["mae"])
history = [Link](train_dataset,
epochs=10,
validation_data=val_dataset,
callbacks=callbacks)
model = [Link].load_model("jena_conv.keras")
print(f"Test MAE: {[Link](test_dataset)[1]:.2f}")
callbacks = [
[Link]("jena_lstm.keras",
save_best_only=True)
]
[Link](optimizer="rmsprop", loss="mse", metrics=["mae"])
history = [Link](train_dataset,
epochs=10,
validation_data=val_dataset,
callbacks=callbacks)
Understanding recurrent neural networks 293
model = [Link].load_model("jena_lstm.keras")
print(f"Test MAE: {[Link](test_dataset)[1]:.2f}")
Figure 10.5 shows the results. Much better! We achieve a validation MAE as low as 2.36
degrees and a test MAE of 2.55 degrees. The LSTM-based model can finally beat the
common-sense baseline (albeit just by a bit, for now), demonstrating the value of
machine learning on this task.
But why did the LSTM model perform markedly better than the densely connected
one or the convnet? And how can we further refine the model? To answer this, let’s
take a closer look at recurrent neural networks.
You can even flesh out the function f: the transformation of the input and state into an
output will be parameterized by two matrices, W and U, and a bias vector. It’s similar to
the transformation operated by a densely connected layer in a feedforward network.
state_t = 0
for input_t in input_sequence:
output_t = activation(dot(W, input_t) + dot(U, state_t) + b)
state_t = output_t
To make these notions absolutely unambiguous, let’s write a naive NumPy implemen-
tation of the forward pass of the simple RNN.
Understanding recurrent neural networks 295
Combines the input with the current state (the previous The final output is a rank-2
output) to obtain the current output. We use tanh to add tensor of shape (timesteps,
non-linearity (we could use any other activation function). output_features).
That’s easy enough. In summary, an RNN is a for loop that reuses quantities com-
puted during the previous iteration of the loop, nothing more. Of course, there are
many different RNNs fitting this definition that you could build—this example is one
of the simplest RNN formulations. RNNs are characterized by their step function,
such as the following function in this case (see figure 10.7).
output_t =
activation(
... Wo • input_t + ...
State t Uo • state_t + State t+1
bo)
NOTE In this example, the final output is a rank-2 tensor of shape (time-
steps, output_features), where each timestep is the output of the loop at
time t. Each timestep t in the output tensor contains information about time-
steps 0 to t in the input sequence—about the entire past. For this reason, in
many cases, you don’t need this full sequence of outputs; you just need the
last output (output_t at the end of the loop), because it already contains
information about the entire sequence.
Listing 10.16 An RNN layer that can process sequences of any length
num_features = 14
inputs = [Link](shape=(None, num_features))
outputs = [Link](16)(inputs)
This is especially useful if your model is meant to process sequences of variable length.
However, if all of your sequences have the same length, I recommend specifying a
complete input shape, since it enables [Link]() to display output length
information, which is always nice, and it can unlock some performance optimizations
(see the “Note on RNN runtime performance” sidebar a little later in this chapter).
All recurrent layers in Keras (SimpleRNN, LSTM, and GRU) can be run in two differ-
ent modes: they can return either full sequences of successive outputs for each time-
step (a rank-3 tensor of shape (batch_size, timesteps, output_features)) or
return only the last output for each input sequence (a rank-2 tensor of shape (batch_
size, output_features)). These two modes are controlled by the return_sequences
constructor argument. Let’s look at an example that uses SimpleRNN and returns only
the output at the last timestep.
Listing 10.17 An RNN layer that returns only its last output step
>>> num_features = 14
>>> steps = 120
>>> inputs = [Link](shape=(steps, num_features))
>>> outputs = [Link](16, return_sequences=False)(inputs)
>>> print([Link]) Note that
(None, 16) return_sequences=False
is the default.
Understanding recurrent neural networks 297
Listing 10.18 An RNN layer that returns its full output sequence
>>> num_features = 14
>>> steps = 120
>>> inputs = [Link](shape=(steps, num_features))
>>> outputs = [Link](16, return_sequences=True)(inputs)
>>> print([Link])
(120, 16)
It’s sometimes useful to stack several recurrent layers one after the other in order to
increase the representational power of a network. In such a setup, you have to get all
of the intermediate layers to return a full sequence of outputs.
In practice, you’ll rarely work with the SimpleRNN layer. It’s generally too simplistic to be
of real use. In particular, SimpleRNN has a major issue: although it should theoretically
be able to retain at time t information about inputs seen many timesteps before, such
long-term dependencies prove impossible to learn in practice. This is due to the vanish-
ing gradient problem, an effect that is similar to what is observed with non-recurrent net-
works (feedforward networks) that are many layers deep: as you keep adding layers to a
network, the network eventually becomes untrainable. The theoretical reasons for this
effect were studied by Hochreiter, Schmidhuber, and Bengio in the early 1990s.3
Thankfully, SimpleRNN isn’t the only recurrent layer available in Keras. There are
two others, LSTM and GRU, which were designed to address these issues.
Let’s consider the LSTM layer. The underlying Long Short-Term Memory (LSTM)
algorithm was developed by Hochreiter and Schmidhuber in 1997;4 it was the culmi-
nation of their research on the vanishing gradient problem.
This layer is a variant of the SimpleRNN layer you already know about; it adds a way
to carry information across many timesteps. Imagine a conveyor belt running parallel
to the sequence you’re processing. Information from the sequence can jump onto the
conveyor belt at any point, be transported to a later timestep, and jump off, intact,
when you need it. This is essentially what LSTM does: it saves information for later,
thus preventing older signals from gradually vanishing during processing. This should
3
See, for example, Yoshua Bengio, Patrice Simard, and Paolo Frasconi, “Learning Long-Term Dependencies
with Gradient Descent Is Difficult,” IEEE Transactions on Neural Networks 5, no. 2 (1994).
4
Sepp Hochreiter and Jürgen Schmidhuber, “Long Short-Term Memory,” Neural Computation 9, no. 8 (1997).
298 CHAPTER 10 Deep learning for timeseries
remind you of residual connections, which you learned about in chapter 9: it’s pretty
much the same idea.
To understand this process in detail, let’s start from the SimpleRNN cell (see fig-
ure 10.8). Because you’ll have a lot of weight matrices, index the W and U matrices in
the cell, with the letter o (Wo and Uo) for output.
output_t =
activation(
... Wo • input_t + ...
State t Uo • state_t + State t+1
bo)
Let’s add to this picture an additional data flow that carries information across time-
steps. Call its values at different timesteps c_t, where C stands for carry. This informa-
tion will have the following impact on the cell: it will be combined with the input
connection and the recurrent connection (via a dense transformation: a dot product
with a weight matrix followed by a bias add and the application of an activation func-
tion), and it will affect the state being sent to the next timestep (via an activation func-
tion and a multiplication operation). Conceptually, the carry dataflow is a way to
modulate the next output and the next state (see figure 10.9). Simple so far.
ct output_t = ct
activation(
Wo • input_t +
... Uo • state_t +
...
State t State t+1
Vo • c_t +
bo)
Now the subtlety—the way the next value of the carry dataflow is computed. It involves
three distinct transformations. All three have the form of a SimpleRNN cell:
y = activation(dot(state_t, U) + dot(input_t, W) + b)
But all three transformations have their own weight matrices, which we’ll index with
the letters i, f, and k. Here’s what we have so far (it may seem a bit arbitrary, but
bear with me).
We obtain the new carry state (the next c_t) by combining i_t, f_t, and k_t.
Add this as shown in figure 10.10, and that’s it. Not so complicated—merely a tad
complex.
ct output_t = ct
activation(
Wo•input_t +
... Uo•state_t +
...
State t State t+1
Vo•c_t +
bo)
If you want to get philosophical, you can interpret what each of these operations is
meant to do. For instance, you can say that multiplying c_t and f_t is a way to deliber-
ately forget irrelevant information in the carry dataflow. Meanwhile, i_t and k_t pro-
vide information about the present, updating the carry track with new information.
But at the end of the day, these interpretations don’t mean much, because what these
300 CHAPTER 10 Deep learning for timeseries
happenstance correlations in the training data that the layer is exposed to. But how to
correctly apply dropout in recurrent networks isn’t a trivial question.
It has long been known that applying dropout before a recurrent layer hinders
learning rather than helping with regularization. In 2016, Yarin Gal, as part of his PhD
thesis on Bayesian deep learning,5 determined the proper way to use dropout with a
recurrent network: the same dropout mask (the same pattern of dropped units)
should be applied at every timestep, instead of using a dropout mask that varies ran-
domly from timestep to timestep. What’s more, in order to regularize the representa-
tions formed by the recurrent gates of layers such as GRU and LSTM, a temporally
constant dropout mask should be applied to the inner recurrent activations of the
layer (a recurrent dropout mask). Using the same dropout mask at every timestep
allows the network to properly propagate its learning error through time; a tempo-
rally random dropout mask would disrupt this error signal and be harmful to the
learning process.
Yarin Gal did his research using Keras and helped build this mechanism directly
into Keras recurrent layers. Every recurrent layer in Keras has two dropout-related
arguments: dropout, a float specifying the dropout rate for input units of the layer,
and recurrent_dropout, specifying the dropout rate of the recurrent units. Let’s add
recurrent dropout to the LSTM layer of our first LSTM example and see how doing so
impacts overfitting.
Thanks to dropout, we won’t need to rely as much on network size for regulariza-
tion, so we’ll use an LSTM layer with twice as many units, which should, hopefully, be
more expressive (without dropout, this network would have started overfitting right
away—try it). Because networks being regularized with dropout always take much lon-
ger to fully converge, we’ll train the model for five times as many epochs.
5
See Yarin Gal, “Uncertainty in Deep Learning,” PhD thesis (2016), [Link]
302 CHAPTER 10 Deep learning for timeseries
Figure 10.11 shows the results. Success! We’re no longer overfitting during the first 20
epochs. We achieve a validation MAE as low as 2.27 degrees (7% improvement over
the no-learning baseline) and a test MAE of 2.45 degrees (6.5% improvement over the
baseline). Not too bad.
the memory consumption of your RNN—as such, it’s only viable for relatively small
sequences (around 100 steps or fewer). Also, note that you can only do this if the
number of timesteps in the data is known in advance by the model (that is to say, if
you pass a shape without any None entries to your initial Input()). It works like this:
sequence_length cannot be None.
inputs = [Link](shape=(sequence_length, num_features))
x = [Link](32, recurrent_dropout=0.2, unroll=True)(inputs)
Pass unroll=True to enable unrolling.
callbacks = [
[Link]("jena_stacked_gru_dropout.keras",
save_best_only=True)
]
6
See Cho et al., “On the Properties of Neural Machine Translation: Encoder-Decoder Approaches” (2014),
[Link]
304 CHAPTER 10 Deep learning for timeseries
Figure 10.12 shows the results. We achieve a test MAE of 2.39 degrees (an 8.8%
improvement over the baseline). You can see that the added layer does improve the
results a bit, though not dramatically. You may be seeing diminishing returns from
increasing network capacity at this point.
uses two regular RNNs, such as the GRU and LSTM layers you’re already familiar with,
each of which processes the input sequence in one direction (chronologically and
antichronologically), and then merges their representations. By processing a sequence
both ways, a bidirectional RNN can catch patterns that may be overlooked by a unidi-
rectional RNN.
Remarkably, the fact that the RNN layers in this section have processed sequences
in chronological order (with older timesteps first) may have been an arbitrary decision.
At least, it’s a decision we’ve made no attempt to question so far. Could the RNNs have
performed well enough if they processed input sequences in antichronological order, for
instance (with newer timesteps first)? Let’s try this and see what happens. All you need to
do is write a variant of the data generator where the input sequences are reverted along
the time dimension (replace the last line with yield samples[:, ::-1, :], targets).
Training the same LSTM-based model that you used in the first experiment in this sec-
tion, you get the results shown in figure 10.13.
However, this isn’t true for many other problems, including natural language: intui-
tively, the importance of a word in understanding a sentence isn’t usually dependent on
its position in the sentence. On text data, reversed-order processing works just as well as
chronological processing—you can read text backwards just fine (try it!). Although
word order does matter in understanding language, which order you use isn’t crucial.
Importantly, an RNN trained on reversed sequences will learn different repre-
sentations than one trained on the original sequences, much as you would have dif-
ferent mental models if time flowed backward in the real world—if you lived a life
where you died on your first day and were born on your last day. In machine learn-
ing, representations that are different yet useful are always worth exploiting, and the
more they differ, the better: they offer a new angle from which to look at your data,
capturing aspects of the data that were missed by other approaches, and thus they
can help boost performance on a task. This is the intuition behind ensembling, a con-
cept we’ll explore in chapter 13.
A bidirectional RNN exploits this idea to improve on the performance of
chronological-order RNNs. It looks at its input sequence both ways (see figure 10.14),
obtaining potentially richer representations and capturing patterns that may have
been missed by the chronological-order version alone.
Input data
Merge (add,
concatenate)
RNN RNN
a, b, c, d, e e, d, c, b, a
To instantiate a bidirectional RNN in Keras, you use the Bidirectional layer, which
takes as its first argument a recurrent layer instance. Bidirectional creates a second,
separate instance of this recurrent layer and uses one instance for processing the
input sequences in chronological order and the other instance for processing the
input sequences in reversed order. You can try it on our temperature-forecasting task.
You’ll find that it doesn’t perform as well as the plain LSTM layer. It’s easy to under-
stand why: all the predictive capacity must come from the chronological half of the
network, because the antichronological half is known to be severely underperforming
on this task (again, because the recent past matters much more than the distant past,
in this case). At the same time, the presence of the antichronological half doubles the
network’s capacity and causes it to start overfitting much earlier.
However, bidirectional RNNs are a great fit for text data, or any other kind of data
where order matters, yet where which order you use doesn’t matter. In fact, for a while
in 2016, bidirectional LSTMs were considered the state of the art on many natural lan-
guage processing tasks (before the rise of the Transformer architecture, which you
will learn about in the next chapter).
Summary
As you first learned in chapter 5, when approaching a new problem, it’s good to
first establish common-sense baselines for your metric of choice. If you don’t
have a baseline to beat, you can’t tell whether you’re making real progress.
Try simple models before expensive ones, to make sure the additional expense
is justified. Sometimes a simple model will turn out to be your best option.
When you have data where ordering matters, and in particular for timeseries
data, recurrent networks are a great fit and easily outperform models that first flat-
ten the temporal data. The two essential RNN layers available in Keras are the
LSTM layer and the GRU layer.
To use dropout with recurrent networks, you should use a time-constant drop-
out mask and recurrent dropout mask. These are built into Keras recurrent lay-
ers, so all you have to do is use the recurrent_dropout arguments of recurrent
layers.
Stacked RNNs provide more representational power than a single RNN layer.
They’re also much more expensive and thus not always worth it. Although they
offer clear gains on complex problems (such as machine translation), they may
not always be relevant to smaller, simpler problems.
Deep learning for text
309
310 CHAPTER 11 Deep learning for text
Of course, keep in mind throughout this chapter that the text-processing models you
will train won’t possess a human-like understanding of language; rather, they simply
look for statistical regularities in their input data, which turns out to be sufficient to
perform well on many simple tasks. In much the same way that computer vision is pat-
tern recognition applied to pixels, NLP is pattern recognition applied to words, sen-
tences, and paragraphs.
The toolset of NLP—decision trees, logistic regression—only saw slow evolution
from the 1990s to the early 2010s. Most of the research focus was on feature engineer-
ing. When I won my first NLP competition on Kaggle in 2013, my model was, you
guessed it, based on decision trees and logistic regression. However, around 2014–
2015, things started changing at last. Multiple researchers began to investigate the
language-understanding capabilities of recurrent neural networks, in particular LSTM—
a sequence-processing algorithm from the late 1990s that had stayed under the radar
until then.
In early 2015, Keras made available the first open source, easy-to-use implementa-
tion of LSTM, just at the start of a massive wave of renewed interest in recurrent neu-
ral networks—until then, there had only been “research code” that couldn’t be readily
reused. Then from 2015 to 2017, recurrent neural networks dominated the booming
NLP scene. Bidirectional LSTM models, in particular, set the state of the art on many
important tasks, from summarization to question-answering to machine translation.
Finally, around 2017–2018, a new architecture rose to replace RNNs: the Trans-
former, which you will learn about in the second half of this chapter. Transformers
unlocked considerable progress across the field in a short period of time, and today
most NLP systems are based on them.
Let’s dive into the details. This chapter will take you from the very basics to doing
machine translation with a Transformer.
Standardization
Tokenization
Indexing
0 0 0 0 0 1
Vector 0 0 1 1 0 0
encoding 0 0 0 0 0 0
of indices 1 0 0 1 1 0
0 1 1 0 0 0
0 0 0 0 0 1
vocabulary = {}
for text in dataset:
text = standardize(text)
tokens = tokenize(text)
for token in tokens:
if token not in vocabulary:
vocabulary[token] = len(vocabulary)
Preparing text data 315
You can then convert that integer into a vector encoding that can be processed by a
neural network, like a one-hot vector:
def one_hot_encode_token(token):
vector = [Link]((len(vocabulary),))
token_index = vocabulary[token]
vector[token_index] = 1
return vector
Note that at this step it’s common to restrict the vocabulary to only the top 20,000 or
30,000 most common words found in the training data. Any text dataset tends to fea-
ture an extremely large number of unique terms, most of which only show up once or
twice—indexing those rare terms would result in an excessively large feature space,
where most features would have almost no information content.
Remember when you were training your first deep learning models on the IMDB
dataset in chapters 4 and 5? The data you were using from [Link] was
already preprocessed into sequences of integers, where each integer stood for a given
word. Back then, we used the setting num_words=10000, in order to restrict our vocab-
ulary to the top 10,000 most common words found in the training data.
Now, there’s an important detail here that we shouldn’t overlook: when we look
up a new token in our vocabulary index, it may not necessarily exist. Your training
data may not have contained any instance of the word “cherimoya” (or maybe you
excluded it from your index because it was too rare), so doing token_index =
vocabulary["cherimoya"] may result in a KeyError. To handle this, you should use
an “out of vocabulary” index (abbreviated as OOV index)—a catch-all for any token
that wasn’t in the index. It’s usually index 1: you’re actually doing token_index =
[Link](token, 1). When decoding a sequence of integers back into words,
you’ll replace 1 with something like “[UNK]” (which you’d call an “OOV token”).
“Why use 1 and not 0?” you may ask. That’s because 0 is already taken. There are
two special tokens that you will commonly use: the OOV token (index 1), and the
mask token (index 0). While the OOV token means “here was a word we did not recog-
nize,” the mask token tells us “ignore me, I’m not a word.” You’d use it in particular to
pad sequence data: because data batches need to be contiguous, all sequences in a
batch of sequence data must have the same length, so shorter sequences should be
padded to the length of the longest sequence. If you want to make a batch of data with
the sequences [5, 7, 124, 4, 89] and [8, 34, 21], it would have to look like this:
The batches of integer sequences for the IMDB dataset that you worked with in chap-
ters 4 and 5 were padded with zeros in this way.
316 CHAPTER 11 Deep learning for text
import string
class Vectorizer:
def standardize(self, text):
text = [Link]()
return "".join(char for char in text
if char not in [Link])
vectorizer = Vectorizer()
dataset = [
"I write, erase, rewrite", Haiku
"Erase again, and then", by poet
"A poppy blooms.", Hokushi
]
vectorizer.make_vocabulary(dataset)
However, using something like this wouldn’t be very performant. In practice, you’ll
work with the Keras TextVectorization layer, which is fast and efficient and can be
dropped directly into a [Link] pipeline or a Keras model.
This is what the TextVectorization layer looks like:
By default, the TextVectorization layer will use the setting “convert to lowercase and
remove punctuation” for text standardization, and “split on whitespace” for tokeniza-
tion. But importantly, you can provide custom functions for standardization and toke-
nization, which means the layer is flexible enough to handle any use case. Note that
such custom functions should operate on [Link] tensors, not regular Python
strings! For instance, the default layer behavior is equivalent to the following:
import re
import string Convert
import tensorflow as tf strings to Replace
lowercase. punctuation
def custom_standardization_fn(string_tensor): characters
lowercase_string = [Link](string_tensor) with the
return [Link].regex_replace( empty string.
lowercase_string, f"[{[Link]([Link])}]", "")
def custom_split_fn(string_tensor):
return [Link](string_tensor)
Split strings on
text_vectorization = TextVectorization(
whitespace.
output_mode="int",
standardize=custom_standardization_fn,
split=custom_split_fn,
)
To index the vocabulary of a text corpus, just call the adapt() method of the layer
with a Dataset object that yields strings, or just with a list of Python strings:
dataset = [
"I write, erase, rewrite",
"Erase again, and then",
"A poppy blooms.",
]
text_vectorization.adapt(dataset)
Note that you can retrieve the computed vocabulary via get_vocabulary()—this can
be useful if you need to convert text encoded as integer sequences back into words.
The first two entries in the vocabulary are the mask token (index 0) and the OOV
token (index 1). Entries in the vocabulary list are sorted by frequency, so with a real-
world dataset, very common words like “the” or “a” would come first.
318 CHAPTER 11 Deep learning for text
For a demonstration, let’s try to encode and then decode an example sentence:
The second option is to make it part of the model (after all, it’s a Keras layer), like this:
There’s an important difference between the two: if the vectorization step is part of
the model, it will happen synchronously with the rest of the model. This means that
at each training step, the rest of the model (placed on the GPU) will have to wait for
the output of the TextVectorization layer (placed on the CPU) to be ready in order
to get to work. Meanwhile, putting the layer in the [Link] pipeline enables you to
Two approaches for representing groups of words: Sets and sequences 319
do asynchronous preprocessing of your data on CPU: while the GPU runs the model
on one batch of vectorized data, the CPU stays busy by vectorizing the next batch of
raw strings.
So if you’re training the model on GPU or TPU, you’ll probably want to go with the first
option to get the best performance. This is what we will do in all practical examples
throughout this chapter. When training on a CPU, though, synchronous processing is
fine: you will get 100% utilization of your cores regardless of which option you go with.
Now, if you were to export our model to a production environment, you would want to
ship a model that accepts raw strings as input, like in the code snippet for the second
option above—otherwise you would have to reimplement text standardization and
tokenization in your production environment (maybe in JavaScript?), and you would
face the risk of introducing small preprocessing discrepancies that would hurt the
model’s accuracy. Thankfully, the TextVectorization layer enables you to include
text preprocessing right into your model, making it easier to deploy—even if you were
originally using the layer as part of a [Link] pipeline. In the sidebar “Exporting a
model that processes raw strings,” you’ll learn how to export an inference-only
trained model that incorporates text preprocessing.
You’ve now learned everything you need to know about text preprocessing—let’s
move on to the modeling stage.
!curl -O [Link]
!tar -xf aclImdb_v1.[Link]
You’re left with a directory named aclImdb, with the following structure:
aclImdb/
...train/
......pos/
......neg/
...test/
......pos/
......neg/
For instance, the train/pos/ directory contains a set of 12,500 text files, each of which
contains the text body of a positive-sentiment movie review to be used as training data.
The negative-sentiment reviews live in the “neg” directories. In total, there are 25,000
text files for training and another 25,000 for testing.
There’s also a train/unsup subdirectory in there, which we don’t need. Let’s
delete it:
!rm -r aclImdb/train/unsup
Take a look at the content of a few of these text files. Whether you’re working with
text data or image data, remember to always inspect what your data looks like before
you dive into modeling it. It will ground your intuition about what your model is actu-
ally doing:
!cat aclImdb/train/pos/4077_10.txt
Two approaches for representing groups of words: Sets and sequences 321
Next, let’s prepare a validation set by setting apart 20% of the training text files in a
new directory, aclImdb/val:
base_dir = [Link]("aclImdb")
val_dir = base_dir / "val"
Shuffle the list of training
train_dir = base_dir / "train" files using a seed, to
for category in ("neg", "pos"): ensure we get the same
[Link](val_dir / category) validation set every time
files = [Link](train_dir / category) we run the code.
[Link](1337).shuffle(files)
num_val_samples = int(0.2 * len(files)) Take 20% of the training
val_files = files[-num_val_samples:] files to use for validation.
for fname in val_files:
[Link](train_dir / category / fname, Move the files to aclImdb/val/neg
val_dir / category / fname) and aclImdb/val/pos.
train_ds = [Link].text_dataset_from_directory(
Running this line should
"aclImdb/train", batch_size=batch_size output “Found 20000 files
) belonging to 2 classes”;
val_ds = [Link].text_dataset_from_directory( if you see “Found 70000
"aclImdb/val", batch_size=batch_size files belonging to 3
) classes,” it means you
test_ds = [Link].text_dataset_from_directory( forgot to delete the
"aclImdb/test", batch_size=batch_size aclImdb/train/unsup
) directory.
These datasets yield inputs that are TensorFlow [Link] tensors and targets that are
int32 tensors encoding the value “0” or “1.”
Listing 11.2 Displaying the shapes and dtypes of the first batch
All set. Now let’s try learning something from this data.
The main advantage of this encoding is that you can represent an entire text as a sin-
gle vector, where each entry is a presence indicator for a given word. For instance,
using binary encoding (multi-hot), you’d encode a text as a vector with as many
dimensions as there are words in your vocabulary—with 0s almost everywhere and
some 1s for dimensions that encode words present in the text. This is what we did
when we worked with text data in chapters 4 and 5. Let’s try this on our task.
First, let’s process our raw text datasets with a TextVectorization layer so that
they yield multi-hot encoded binary word vectors. Our layer will only look at single
words (that is to say, unigrams).
Limit the vocabulary to the 20,000 most frequent words. Encode the output
Otherwise we’d be indexing every word in the training data— tokens as multi-hot
potentially tens of thousands of terms that only occur once or binary vectors.
twice and thus aren’t informative. In general, 20,000 is the
right vocabulary size for text classification. Prepare a dataset that
text_vectorization = TextVectorization(
only yields raw text
inputs (no labels).
max_tokens=20000,
output_mode="multi_hot",
) Use that dataset to index
text_only_train_ds = train_ds.map(lambda x, y: x) the dataset vocabulary via
text_vectorization.adapt(text_only_train_ds) the adapt() method.
binary_1gram_train_ds = train_ds.map(
lambda x, y: (text_vectorization(x), y), Prepare processed
num_parallel_calls=4) versions of our training,
binary_1gram_val_ds = val_ds.map( validation, and test
lambda x, y: (text_vectorization(x), y), dataset.
num_parallel_calls=4)
Make sure to specify
num_parallel_calls to
binary_1gram_test_ds = test_ds.map(
leverage multiple CPU
lambda x, y: (text_vectorization(x), y),
cores.
num_parallel_calls=4)
Two approaches for representing groups of words: Sets and sequences 323
Next, let’s write a reusable model-building function that we’ll use in all of our experi-
ments in this section.
This gets us to a test accuracy of 89.2%: not bad! Note that in this case, since the data-
set is a balanced two-class classification dataset (there are as many positive samples as
negative samples), the “naive baseline” we could reach without training an actual model
would only be 50%. Meanwhile, the best score that can be achieved on this dataset
without leveraging external data is around 95% test accuracy.
BIGRAMS WITH BINARY ENCODING
Of course, discarding word order is very reductive, because even atomic concepts can
be expressed via multiple words: the term “United States” conveys a concept that is
quite distinct from the meaning of the words “states” and “united” taken separately.
For this reason, you will usually end up re-injecting local order information into your
bag-of-words representation by looking at N-grams rather than single words (most
commonly, bigrams).
With bigrams, our sentence becomes
text_vectorization = TextVectorization(
ngrams=2,
max_tokens=20000,
output_mode="multi_hot",
)
Let’s test how our model performs when trained on such binary-encoded bags of
bigrams.
text_vectorization.adapt(text_only_train_ds)
binary_2gram_train_ds = train_ds.map(
lambda x, y: (text_vectorization(x), y),
num_parallel_calls=4)
binary_2gram_val_ds = val_ds.map(
lambda x, y: (text_vectorization(x), y),
num_parallel_calls=4)
binary_2gram_test_ds = test_ds.map(
lambda x, y: (text_vectorization(x), y),
num_parallel_calls=4)
model = get_model()
[Link]()
callbacks = [
[Link]("binary_2gram.keras",
save_best_only=True)
]
Two approaches for representing groups of words: Sets and sequences 325
[Link](binary_2gram_train_ds.cache(),
validation_data=binary_2gram_val_ds.cache(),
epochs=10,
callbacks=callbacks)
model = [Link].load_model("binary_2gram.keras")
print(f"Test acc: {[Link](binary_2gram_test_ds)[1]:.3f}")
We’re now getting 90.4% test accuracy, a marked improvement! Turns out local order
is pretty important.
BIGRAMS WITH TF-IDF ENCODING
You can also add a bit more information to this representation by counting how many
times each word or N-gram occurs, that is to say, by taking the histogram of the words
over the text:
If you’re doing text classification, knowing how many times a word occurs in a sample
is critical: any sufficiently long movie review may contain the word “terrible” regard-
less of sentiment, but a review that contains many instances of the word “terrible” is
likely a negative one.
Here’s how you’d count bigram occurrences with the TextVectorization layer.
text_vectorization = TextVectorization(
ngrams=2,
max_tokens=20000,
output_mode="count"
)
Now, of course, some words are bound to occur more often than others no matter
what the text is about. The words “the,” “a,” “is,” and “are” will always dominate your
word count histograms, drowning out other words—despite being pretty much useless
features in a classification context. How could we address this?
You already guessed it: via normalization. We could just normalize word counts by
subtracting the mean and dividing by the variance (computed across the entire train-
ing dataset). That would make sense. Except most vectorized sentences consist almost
entirely of zeros (our previous example features 12 non-zero entries and 19,988 zero
entries), a property called “sparsity.” That’s a great property to have, as it dramatically
reduces compute load and reduces the risk of overfitting. If we subtracted the mean
from each feature, we’d wreck sparsity. Thus, whatever normalization scheme we use
should be divide-only. What, then, should we use as the denominator? The best prac-
tice is to go with something called TF-IDF normalization—TF-IDF stands for “term fre-
quency, inverse document frequency.”
TF-IDF is so common that it’s built into the TextVectorization layer. All you need
to do to start using it is to switch the output_mode argument to "tf_idf".
326 CHAPTER 11 Deep learning for text
text_vectorization = TextVectorization(
ngrams=2,
max_tokens=20000,
output_mode="tf_idf",
)
text_vectorization.adapt(text_only_train_ds)
The adapt() call will learn the
tfidf_2gram_train_ds = train_ds.map(
TF-IDF weights in addition to
the vocabulary.
lambda x, y: (text_vectorization(x), y),
num_parallel_calls=4)
tfidf_2gram_val_ds = val_ds.map(
lambda x, y: (text_vectorization(x), y),
num_parallel_calls=4)
tfidf_2gram_test_ds = test_ds.map(
lambda x, y: (text_vectorization(x), y),
num_parallel_calls=4)
model = get_model()
[Link]()
callbacks = [
[Link]("tfidf_2gram.keras",
save_best_only=True)
]
[Link](tfidf_2gram_train_ds.cache(),
validation_data=tfidf_2gram_val_ds.cache(),
epochs=10,
callbacks=callbacks)
model = [Link].load_model("tfidf_2gram.keras")
print(f"Test acc: {[Link](tfidf_2gram_test_ds)[1]:.3f}")
Two approaches for representing groups of words: Sets and sequences 327
This gets us an 89.8% test accuracy on the IMDB classification task: it doesn’t seem to
be particularly helpful in this case. However, for many text-classification datasets, it
would be typical to see a one-percentage-point increase when using TF-IDF compared
to plain binary encoding.
already familiar with this architecture, this is what we’ll use in our first sequence model
examples. However, nowadays sequence modeling is almost universally done with Trans-
formers, which we will cover shortly. Oddly, one-dimensional convnets were never
very popular in NLP, even though, in my own experience, a residual stack of depth-
wise-separable 1D convolutions can often achieve comparable performance to a bidi-
rectional LSTM, at a greatly reduced computational cost.
A FIRST PRACTICAL EXAMPLE
Let’s try out a first sequence model in practice. First, let’s prepare datasets that return
integer sequences.
int_train_ds = train_ds.map(
lambda x, y: (text_vectorization(x), y)),
num_parallel_calls=4)
int_val_ds = val_ds.map(
lambda x, y: (text_vectorization(x), y),
num_parallel_calls=4)
int_test_ds = test_ds.map(
lambda x, y: (text_vectorization(x), y),
num_parallel_calls=4)
Next, let’s make a model. The simplest way to convert our integer sequences to vector
sequences is to one-hot encode the integers (each dimension would represent one
possible term in the vocabulary). On top of these one-hot vectors, we’ll add a simple
bidirectional LSTM.
callbacks = [
[Link]("one_hot_bidir_lstm.keras",
save_best_only=True)
]
[Link](int_train_ds, validation_data=int_val_ds, epochs=10,
callbacks=callbacks)
model = [Link].load_model("one_hot_bidir_lstm.keras")
print(f"Test acc: {[Link](int_test_ds)[1]:.3f}")
A first observation: this model trains very slowly, especially compared to the light-
weight model of the previous section. This is because our inputs are quite large: each
input sample is encoded as a matrix of size (600, 20000) (600 words per sample,
20,000 possible words). That’s 12,000,000 floats for a single movie review. Our bidirec-
tional LSTM has a lot of work to do. Second, the model only gets to 87% test accu-
racy—it doesn’t perform nearly as well as our (very fast) binary unigram model.
Clearly, using one-hot encoding to turn words into vectors, which was the simplest
thing we could do, wasn’t a great idea. There’s a better way: word embeddings.
UNDERSTANDING WORD EMBEDDINGS
Crucially, when you encode something via one-hot encoding, you’re making a feature-
engineering decision. You’re injecting into your model a fundamental assumption
about the structure of your feature space. That assumption is that the different tokens
you’re encoding are all independent from each other: indeed, one-hot vectors are all orthogo-
nal to one another. And in the case of words, that assumption is clearly wrong. Words
form a structured space: they share information with each other. The words “movie”
and “film” are interchangeable in most sentences, so the vector that represents
“movie” should not be orthogonal to the vector that represents “film”—they should be
the same vector, or close enough.
To get a bit more abstract, the geometric relationship between two word vectors
should reflect the semantic relationship between these words. For instance, in a reason-
able word vector space, you would expect synonyms to be embedded into similar word
vectors, and in general, you would expect the geometric distance (such as the cosine
distance or L2 distance) between any two word vectors to relate to the “semantic dis-
tance” between the associated words. Words that mean different things should lie far
away from each other, whereas related words should be closer.
Word embeddings are vector representations of words that achieve exactly this: they
map human language into a structured geometric space.
Whereas the vectors obtained through one-hot encoding are binary, sparse (mostly
made of zeros), and very high-dimensional (the same dimensionality as the number of
words in the vocabulary), word embeddings are low-dimensional floating-point vectors
(that is, dense vectors, as opposed to sparse vectors); see figure 11.2. It’s common to see
word embeddings that are 256-dimensional, 512-dimensional, or 1,024-dimensional
330 CHAPTER 11 Deep learning for text
when dealing with very large vocabularies. On the other hand, one-hot encoding words
generally leads to vectors that are 20,000-dimensional or greater (capturing a vocabu-
lary of 20,000 tokens, in this case). So, word embeddings pack more information into
far fewer dimensions.
Besides being dense representations, word embeddings are also structured representa-
tions, and their structure is learned from data. Similar words get embedded in close
locations, and further, specific directions in the embedding space are meaningful. To
make this clearer, let’s look at a concrete example.
In figure 11.3, four words are embedded on a 2D plane: cat, dog, wolf, and tiger.
With the vector representations we chose here, some semantic relationships between
these words can be encoded as geometric transformations. For instance, the same
vector allows us to go from cat to tiger and from dog to wolf: this vector could be inter-
preted as the “from pet to wild animal” vector. Similarly, another vector lets us go
from dog to cat and from wolf to tiger, which could be interpreted as a “from canine
to feline” vector.
1 Wolf
Tiger
Dog
Cat
The Embedding layer takes as input a rank-2 tensor of integers, of shape (batch_size,
sequence_length), where each entry is a sequence of integers. The layer then returns
a 3D floating-point tensor of shape (batch_size, sequence_length, embedding_
dimensionality).
When you instantiate an Embedding layer, its weights (its internal dictionary of
token vectors) are initially random, just as with any other layer. During training, these
word vectors are gradually adjusted via backpropagation, structuring the space into
something the downstream model can exploit. Once fully trained, the embedding
space will show a lot of structure—a kind of structure specialized for the specific prob-
lem for which you’re training your model.
Let’s build a model that includes an Embedding layer and benchmark it on our task.
Listing 11.16 Model that uses an Embedding layer trained from scratch
callbacks = [
[Link]("embeddings_bidir_gru.keras",
save_best_only=True)
]
[Link](int_train_ds, validation_data=int_val_ds, epochs=10,
callbacks=callbacks)
model = [Link].load_model("embeddings_bidir_gru.keras")
print(f"Test acc: {[Link](int_test_ds)[1]:.3f}")
It trains much faster than the one-hot model (since the LSTM only has to process
256-dimensional vectors instead of 20,000-dimensional), and its test accuracy is com-
parable (87%). However, we’re still some way off from the results of our basic bigram
model. Part of the reason why is simply that the model is looking at slightly less data:
the bigram model processed full reviews, while our sequence model truncates sequences
after 600 words.
UNDERSTANDING PADDING AND MASKING
One thing that’s slightly hurting model performance here is that our input sequences
are full of zeros. This comes from our use of the output_sequence_length=max_
length option in TextVectorization (with max_length equal to 600): sentences lon-
ger than 600 tokens are truncated to a length of 600 tokens, and sentences shorter
than 600 tokens are padded with zeros at the end so that they can be concatenated
together with other sequences to form contiguous batches.
Two approaches for representing groups of words: Sets and sequences 333
We’re using a bidirectional RNN: two RNN layers running in parallel, with one
processing the tokens in their natural order, and the other processing the same
tokens in reverse. The RNN that looks at the tokens in their natural order will spend
its last iterations seeing only vectors that encode padding—possibly for several hun-
dreds of iterations if the original sentence was short. The information stored in the
internal state of the RNN will gradually fade out as it gets exposed to these meaning-
less inputs.
We need some way to tell the RNN that it should skip these iterations. There’s an
API for that: masking.
The Embedding layer is capable of generating a “mask” that corresponds to its
input data. This mask is a tensor of ones and zeros (or True/False booleans), of shape
(batch_size, sequence_length), where the entry mask[i, t] indicates where time-
step t of sample i should be skipped or not (the timestep will be skipped if mask[i, t]
is 0 or False, and processed otherwise).
By default, this option isn’t active—you can turn it on by passing mask_zero=True
to your Embedding layer. You can retrieve the mask with the compute_mask() method:
In practice, you will almost never have to manage masks by hand. Instead, Keras will
automatically pass on the mask to every layer that is able to process it (as a piece of
metadata attached to the sequence it represents). This mask will be used by RNN lay-
ers to skip masked steps. If your model returns an entire sequence, the mask will also
be used by the loss function to skip masked steps in the output sequence.
Let’s try retraining our model with masking enabled.
callbacks = [
[Link]("embeddings_bidir_gru_with_masking.keras",
save_best_only=True)
]
[Link](int_train_ds, validation_data=int_val_ds, epochs=10,
callbacks=callbacks)
model = [Link].load_model("embeddings_bidir_gru_with_masking.keras")
print(f"Test acc: {[Link](int_test_ds)[1]:.3f}")
This time we get to 88% test accuracy—a small but noticeable improvement.
USING PRETRAINED WORD EMBEDDINGS
Sometimes you have so little training data available that you can’t use your data alone
to learn an appropriate task-specific embedding of your vocabulary. In such cases,
instead of learning word embeddings jointly with the problem you want to solve, you
can load embedding vectors from a precomputed embedding space that you know is
highly structured and exhibits useful properties—one that captures generic aspects of
language structure. The rationale behind using pretrained word embeddings in natu-
ral language processing is much the same as for using pretrained convnets in image
classification: you don’t have enough data available to learn truly powerful features on
your own, but you expect that the features you need are fairly generic—that is, com-
mon visual features or semantic features. In this case, it makes sense to reuse features
learned on a different problem.
Such word embeddings are generally computed using word-occurrence statistics
(observations about what words co-occur in sentences or documents), using a variety
of techniques, some involving neural networks, others not. The idea of a dense, low-
dimensional embedding space for words, computed in an unsupervised way, was ini-
tially explored by Bengio et al. in the early 2000s,1 but it only started to take off in
research and industry applications after the release of one of the most famous and
successful word-embedding schemes: the Word2Vec algorithm ([Link]
.com/archive/p/word2vec), developed by Tomas Mikolov at Google in 2013. Word2Vec
dimensions capture specific semantic properties, such as gender.
There are various precomputed databases of word embeddings that you can down-
load and use in a Keras Embedding layer. Word2vec is one of them. Another popular
one is called Global Vectors for Word Representation (GloVe, [Link]
.edu/projects/glove), which was developed by Stanford researchers in 2014. This
embedding technique is based on factorizing a matrix of word co-occurrence statis-
tics. Its developers have made available precomputed embeddings for millions of
English tokens, obtained from Wikipedia data and Common Crawl data.
Let’s look at how you can get started using GloVe embeddings in a Keras model.
The same method is valid for Word2Vec embeddings or any other word-embedding
database. We’ll start by downloading the GloVe files and parse them. We’ll then load
the word vectors into a Keras Embedding layer, which we’ll use to build a new model.
1
Yoshua Bengio et al., “A Neural Probabilistic Language Model,” Journal of Machine Learning Research (2003).
Two approaches for representing groups of words: Sets and sequences 335
First, let’s download the GloVe word embeddings precomputed on the 2014
English Wikipedia dataset. It’s an 822 MB zip file containing 100-dimensional embed-
ding vectors for 400,000 words (or non-word tokens).
!wget [Link]
!unzip -q [Link]
Let’s parse the unzipped file (a .txt file) to build an index that maps words (as strings)
to their vector representation.
import numpy as np
path_to_glove_file = "[Link]"
embeddings_index = {}
with open(path_to_glove_file) as f:
for line in f:
word, coefs = [Link](maxsplit=1)
coefs = [Link](coefs, "f", sep=" ")
embeddings_index[word] = coefs
Next, let’s build an embedding matrix that you can load into an Embedding layer. It
must be a matrix of shape (max_words, embedding_dim), where each entry i contains
the embedding_dim-dimensional vector for the word of index i in the reference word
index (built during tokenization).
trainable=False,
mask_zero=True,
)
We’re now ready to train a new model—identical to our previous model, but leverag-
ing the 100-dimensional pretrained GloVe embeddings instead of 128-dimensional
learned embeddings.
callbacks = [
[Link]("glove_embeddings_sequence_model.keras",
save_best_only=True)
]
[Link](int_train_ds, validation_data=int_val_ds, epochs=10,
callbacks=callbacks)
model = [Link].load_model("glove_embeddings_sequence_model.keras")
print(f"Test acc: {[Link](int_test_ds)[1]:.3f}")
You’ll find that on this particular task, pretrained embeddings aren’t very helpful,
because the dataset contains enough samples that it is possible to learn a specialized
enough embedding space from scratch. However, leveraging pretrained embeddings
can be very helpful when you’re working with a smaller dataset.
2
Ashish Vaswani et al., “Attention is all you need” (2017), [Link]
The Transformer architecture 337
Original
representation
New
representation
Attention
mechanism
Crucially, this kind of attention mechanism can be used for more than just highlighting
or erasing certain features. It can be used to make features context-aware. You’ve just
learned about word embeddings—vector spaces that capture the “shape” of the semantic
relationships between different words. In an embedding space, a single word has a fixed
position—a fixed set of relationships with every other word in the space. But that’s not
quite how language works: the meaning of a word is usually context-specific. When you
mark the date, you’re not talking about the same “date” as when you go on a date, nor is
it the kind of date you’d buy at the market. When you say, “I’ll see you soon,” the mean-
ing of the word “see” is subtly different from the “see” in “I’ll see this project to its end” or
“I see what you mean.” And, of course, the meaning of pronouns like “he,” “it,” “in,” etc.,
is entirely sentence-specific and can even change multiple times within a single sentence.
Clearly, a smart embedding space would provide a different vector representation
for a word depending on the other words surrounding it. That’s where self-attention
comes in. The purpose of self-attention is to modulate the representation of a token
by using the representations of related tokens in the sequence. This produces context-
aware token representations. Consider an example sentence: “The train left the sta-
tion on time.” Now, consider one word in the sentence: station. What kind of station
are we talking about? Could it be a radio station? Maybe the International Space Sta-
tion? Let’s figure it out algorithmically via self-attention (see figure 11.6).
Input
sequence
T oken
vectors
station
left
the
the
the 1.0 0.3 0.1 0.5 0.2 0.1 0.1 0.2 the
Softmax,
train 0.3 1.0 0.6 0.3 0.8 0.1 0.2 0.8 train Context-aware
scaling, and
vector
multiplication
left 0.1 0.6 1.0 0.1 0.6 0.1 0.1 0.6 left
the 0.5 0.3 0.1 1.0 0.3 0.1 0.2 0.3 the
station 0.2 0.8 0.6 0.3 1.0 0.2 0.2 1.0 station
time 0.1 0.2 0.1 0.2 0.2 0.5 1.0 0.2 time
Attention scores
Figure 11.6 Self-attention: attention scores are computed between “station” and every other word in the
sequence, and they are then used to weight a sum of word vectors that becomes the new “station” vector.
The Transformer architecture 339
Step 1 is to compute relevancy scores between the vector for “station” and every other
word in the sentence. These are our “attention scores.” We’re simply going to use the
dot product between two word vectors as a measure of the strength of their relation-
ship. It’s a very computationally efficient distance function, and it was already the stan-
dard way to relate two word embeddings to each other long before Transformers. In
practice, these scores will also go through a scaling function and a softmax, but for
now, that’s just an implementation detail.
Step 2 is to compute the sum of all word vectors in the sentence, weighted by our
relevancy scores. Words closely related to “station” will contribute more to the sum
(including the word “station” itself), while irrelevant words will contribute almost
nothing. The resulting vector is our new representation for “station”: a representation
that incorporates the surrounding context. In particular, it includes part of the “train”
vector, clarifying that it is, in fact, a “train station.”
You’d repeat this process for every word in the sentence, producing a new
sequence of vectors encoding the sentence. Let’s see it in NumPy-like pseudocode:
num_heads = 4
embed_dim = 256
mha_layer = MultiHeadAttention(num_heads=num_heads, key_dim=embed_dim)
outputs = mha_layer(inputs, inputs, inputs)
C A B
This means “for each token in inputs (A), compute how much the token is related to
every token in inputs (B), and use these scores to weight a sum of tokens from
inputs (C).” Crucially, there’s nothing that requires A, B, and C to refer to the same
input sequence. In the general case, you could be doing this with three different
sequences. We’ll call them “query,” “keys,” and “values.” The operation becomes “for
each element in the query, compute how much the element is related to every key,
and use these scores to weight a sum of values”:
This terminology comes from search engines and recommender systems (see figure
11.7). Imagine that you’re typing up a query to retrieve a photo from your collection—
“dogs on the beach.” Internally, each of your pictures in the database is described by a
set of keywords—“cat,” “dog,” “party,” etc. We’ll call those “keys.” The search engine will
start by comparing your query to the keys in the database. “Dog” yields a match of 1, and
“cat” yields a match of 0. It will then rank those keys by strength of match—relevance—
and it will return the pictures associated with the top N matches, in order of relevance.
Conceptually, this is what Transformer-style attention is doing. You’ve got a refer-
ence sequence that describes something you’re looking for: the query. You’ve got a
body of knowledge that you’re trying to extract information from: the values. Each
value is assigned a key that describes the value in a format that can be readily com-
pared to a query. You simply match the query to the keys. Then you return a weighted
sum of values.
In practice, the keys and the values are often the same sequence. In machine trans-
lation, for instance, the query would be the target sequence, and the source sequence
would play the roles of both keys and values: for each element of the target (like
The Transformer architecture 341
Keys Values
match: 0.5
Beach
Tree
Query
Boat
“dogs on the beach”
match: 1.0
Beach
Dog
Tree
match: 0.5
Dog
“tiempo”), you want to go back to the source (“How’s the weather today?”) and iden-
tify the different bits that are related to it (“tiempo” and “weather” should have a
strong match). And naturally, if you’re just doing sequence classification, then query,
keys, and values are all the same: you’re comparing a sequence to itself, to enrich each
token with context from the whole sequence.
That explains why we needed to pass inputs three times to our MultiHeadAttention
layer. But why “multi-head” attention?
Output
Concatenate
Attention Attention
head 1 head 2
Attention Attention
Q K V Q K V
LayerNormalization
Residual connection
Dense projection
Dense
Dense
LayerNormalization
The original Transformer architecture consists of two parts: a Transformer encoder that
processes the source sequence, and a Transformer decoder that uses the source sequence
to generate a translated version. You’ll learn about about the decoder part in a minute.
Crucially, the encoder part can be used for text classification—it’s a very generic
module that ingests a sequence and learns to turn it into a more useful representa-
tion. Let’s implement a Transformer encoder and try it on the movie review sentiment
classification task.
import tensorflow as tf
from tensorflow import keras
from [Link] import layers
class TransformerEncoder([Link]):
def __init__(self, embed_dim, dense_dim, num_heads, **kwargs):
super().__init__(**kwargs)
self.embed_dim = embed_dim
Size of the input
self.dense_dim = dense_dim token vectors
self.num_heads = num_heads
[Link] = [Link](
Size of the inner
num_heads=num_heads, key_dim=embed_dim) dense layer
self.dense_proj = [Link](
[[Link](dense_dim, activation="relu"),
[Link](embed_dim),] Number of
) attention heads
344 CHAPTER 11 Deep learning for text
self.layernorm_1 = [Link]()
self.layernorm_2 = [Link]()
Computation goes in call().
def call(self, inputs, mask=None):
if mask is not None: The mask that will be generated by
mask = mask[:, [Link], :] the Embedding layer will be 2D, but
attention_output = [Link]( the attention layer expects to be 3D
inputs, inputs, attention_mask=mask) or 4D, so we expand its rank.
proj_input = self.layernorm_1(inputs + attention_output)
proj_output = self.dense_proj(proj_input)
return self.layernorm_2(proj_input + proj_output)
def get_config(self):
Implement
config = super().get_config() serialization so
[Link]({ we can save the
"embed_dim": self.embed_dim, model.
"num_heads": self.num_heads,
"dense_dim": self.dense_dim,
})
return config
When saving a model that contains custom layers, the savefile will contain these con-
fig dicts. When loading the model from the file, you should provide the custom layer
classes to the loading process, so that it can make sense of the config objects:
model = [Link].load_model(
filename, custom_objects={"PositionalEmbedding": PositionalEmbedding})
You’ll note that the normalization layers we’re using here aren’t BatchNormalization
layers like those we’ve used before in image models. That’s because BatchNormalization
doesn’t work well for sequence data. Instead, we’re using the LayerNormalization layer,
which normalizes each sequence independently from other sequences in the batch.
Like this, in NumPy-like pseudocode:
The Transformer architecture 345
vocab_size = 20000
embed_dim = 256
num_heads = 2
dense_dim = 32
Let’s train it. It gets to 87.5% test accuracy—slightly worse than the GRU model.
Listing 11.23 Training and evaluating the Transformer encoder based model
callbacks = [
[Link]("transformer_encoder.keras",
save_best_only=True)
]
[Link](int_train_ds, validation_data=int_val_ds, epochs=20,
callbacks=callbacks)
346 CHAPTER 11 Deep learning for text
model = [Link].load_model(
"transformer_encoder.keras",
custom_objects={"TransformerEncoder": TransformerEncoder})
print(f"Test acc: {[Link](int_test_ds)[1]:.3f}")
Provide the custom TransformerEncoder
class to the model-loading process.
At this point, you should start to feel a bit uneasy. Something’s off here. Can you tell
what it is?
This section is ostensibly about “sequence models.” I started off by highlighting
the importance of word order. I said that Transformer was a sequence-processing
architecture, originally developed for machine translation. And yet . . . the Trans-
former encoder you just saw in action wasn’t a sequence model at all. Did you
notice? It’s composed of dense layers that process sequence tokens independently
from each other, and an attention layer that looks at the tokens as a set. You could
change the order of the tokens in a sequence, and you’d get the exact same pairwise
attention scores and the exact same context-aware representations. If you were to
completely scramble the words in every movie review, the model wouldn’t notice,
and you’d still get the exact same accuracy. Self-attention is a set-processing mecha-
nism, focused on the relationships between pairs of sequence elements (see figure
11.10)—it’s blind to whether these elements occur at the beginning, at the end, or
in the middle of a sequence. So why do we say that Transformer is a sequence
model? And how could it possibly be good for machine translation if it doesn’t look
at word order?
Context
Word order awareness
awareness (cross-words
interactions)
Bag-of-unigrams No No
Bag-of-bigrams Very limited No
RNN Yes No
Self-attention No Yes
Figure 11.10 Features of different
Transformer Yes Yes types of NLP models
I hinted at the solution earlier in the chapter: I mentioned in passing that Trans-
former was a hybrid approach that is technically order-agnostic, but that manually
injects order information in the representations it processes. This is the missing ingre-
dient! It’s called positional encoding. Let’s take a look.
USING POSITIONAL ENCODING TO RE-INJECT ORDER INFORMATION
The idea behind positional encoding is very simple: to give the model access to word-
order information, we’re going to add the word’s position in the sentence to each word
embedding. Our input word embeddings will have two components: the usual word
The Transformer architecture 347
vector, which represents the word independently of any specific context, and a posi-
tion vector, which represents the position of the word in the current sentence. Hope-
fully, the model will then figure out how to best leverage this additional information.
The simplest scheme you could come up with would be to concatenate the word’s
position to its embedding vector. You’d add a “position” axis to the vector and fill it
with 0 for the first word in the sequence, 1 for the second, and so on.
That may not be ideal, however, because the positions can potentially be very large
integers, which will disrupt the range of values in the embedding vector. As you know,
neural networks don’t like very large input values, or discrete input distributions.
The original “Attention is all you need” paper used an interesting trick to encode
word positions: it added to the word embeddings a vector containing values in the
range [-1, 1] that varied cyclically depending on the position (it used cosine func-
tions to achieve this). This trick offers a way to uniquely characterize any integer in a
large range via a vector of small values. It’s clever, but it’s not what we’re going to use
in our case. We’ll do something simpler and more effective: we’ll learn position-
embedding vectors the same way we learn to embed word indices. We’ll then proceed
to add our position embeddings to the corresponding word embeddings, to obtain a
position-aware word embedding. This technique is called “positional embedding.”
Let’s implement it.
"sequence_length": self.sequence_length,
"input_dim": self.input_dim,
})
return config
You would use this PositionEmbedding layer just like a regular Embedding layer. Let’s
see it in action!
PUTTING IT ALL TOGETHER: A TEXT-CLASSIFICATION TRANSFORMER
All you have to do to start taking word order into account is swap the old Embedding
layer with our position-aware version.
vocab_size = 20000
sequence_length = 600
embed_dim = 256
num_heads = 2
dense_dim = 32
Look here!
inputs = [Link](shape=(None,), dtype="int64")
x = PositionalEmbedding(sequence_length, vocab_size, embed_dim)(inputs)
x = TransformerEncoder(embed_dim, dense_dim, num_heads)(x)
x = layers.GlobalMaxPooling1D()(x)
x = [Link](0.5)(x)
outputs = [Link](1, activation="sigmoid")(x)
model = [Link](inputs, outputs)
[Link](optimizer="rmsprop",
loss="binary_crossentropy",
metrics=["accuracy"])
[Link]()
callbacks = [
[Link]("full_transformer_encoder.keras",
save_best_only=True)
]
[Link](int_train_ds, validation_data=int_val_ds, epochs=20,
callbacks=callbacks)
model = [Link].load_model(
"full_transformer_encoder.keras",
custom_objects={"TransformerEncoder": TransformerEncoder,
"PositionalEmbedding": PositionalEmbedding})
print(f"Test acc: {[Link](int_test_ds)[1]:.3f}")
We get to 88.3% test accuracy, a solid improvement that clearly demonstrates the
value of word order information for text classification. This is our best sequence
model so far! However, it’s still one notch below the bag-of-words approach.
The Transformer architecture 349
So if you’re classifying 1,000-word long documents, and you have 100,000 of them (a
ratio of 100), you should go with a bigram model. If you’re classifying tweets that are
40 words long on average, and you have 50,000 of them (a ratio of 1,250), you should
also go with a bigram model. But if you increase your dataset size to 500,000 tweets (a
ratio of 12,500), go with a Transformer encoder. What about the IMDB movie review
classification task? We had 20,000 training samples and an average word count of 233,
so our rule of thumb points toward a bigram model, which confirms what we found
in practice.
This intuitively makes sense: the input of a sequence model represents a richer
and more complex space, and thus it takes more data to map out that space; mean-
while, a plain set of terms is a space so simple that you can train a logistic regression
on top using just a few hundreds or thousands of samples. In addition, the shorter a
sample is, the less the model can afford to discard any of the information it contains—
in particular, word order becomes more important, and discarding it can create ambi-
guity. The sentences “this movie is the bomb” and “this movie was a bomb” have very
350 CHAPTER 11 Deep learning for text
how, is, the, weather, today [start], qué, tiempo, hace, hoy
qué
3 The predicted sequence so far is fed back into the decoder, which generates the
next token, and so on, until it generates a stop token (such as the string
"[end]").
Everything you’ve learned so far can be repurposed to build this new kind of model.
Let’s dive in.
!wget [Link]
!unzip -q [Link]
The text file contains one example per line: an English sentence, followed by a tab
character, followed by the corresponding Spanish sentence. Let’s parse this file.
text_file = "spa-eng/[Link]"
with open(text_file) as f:
lines = [Link]().split("\n")[:-1]
text_pairs = []
352 CHAPTER 11 Deep learning for text
Let’s shuffle them and split them into the usual training, validation, and test sets:
import random
[Link](text_pairs)
num_val_samples = int(0.15 * len(text_pairs))
num_train_samples = len(text_pairs) - 2 * num_val_samples
train_pairs = text_pairs[:num_train_samples]
val_pairs = text_pairs[num_train_samples:num_train_samples + num_val_samples]
test_pairs = text_pairs[num_train_samples + num_val_samples:]
Next, let’s prepare two separate TextVectorization layers: one for English and one
for Spanish. We’re going to need to customize the way strings are preprocessed:
We need to preserve the "[start]" and "[end]" tokens that we’ve inserted. By
default, the characters [ and ] would be stripped, but we want to keep them
around so we can tell apart the word “start” and the start token "[start]".
Punctuation is different from language to language! In the Spanish Text-
Vectorization layer, if we’re going to strip punctuation characters, we need to
also strip the character ¿.
Note that for a non-toy translation model, we would treat punctuation characters as sep-
arate tokens rather than stripping them, since we would want to be able to generate cor-
rectly punctuated sentences. In our case, for simplicity, we’ll get rid of all punctuation.
import tensorflow as tf
import string
import re
Finally, we can turn our data into a [Link] pipeline. We want it to return a tuple
(inputs, target) where inputs is a dict with two keys, “encoder_inputs” (the English
sentence) and “decoder_inputs” (the Spanish sentence), and target is the Spanish
sentence offset by one step ahead.
batch_size = 64
The data is now ready—time to build some models. We’ll start with a recurrent
sequence-to-sequence model before moving on to a Transformer.
Sequence output
Figure 11.13 A sequence-to-
sequence RNN: an RNN encoder
Encoder RNN Decoder RNN
is used to produce a vector that
Initial state
encodes the entire source
sequence, which is used as the
how, is, the, weather, today [start], qué, tiempo, hace, hoy initial state for an RNN decoder.
RNN (the decoder), which would look at elements 0…N in the target sequence, and
try to predict step N+1 in the target sequence.
Let’s implement this in Keras with GRU-based encoders and decoders. The choice
of GRU rather than LSTM makes things a bit simpler, since GRU only has a single
state vector, whereas LSTM has multiple. Let’s start with the encoder.
Next, let’s add the decoder—a simple GRU layer that takes as its initial state the
encoded source sentence. On top of it, we add a Dense layer that produces for each
output step a probability distribution over the Spanish vocabulary.
During training, the decoder takes as input the entire target sequence, but thanks to
the step-by-step nature of RNNs, it only looks at tokens 0…N in the input to predict
356 CHAPTER 11 Deep learning for text
token N in the output (which corresponds to the next token in the sequence, since
the output is intended to be offset by one step). This means we only use information
from the past to predict the future, as we should; otherwise we’d be cheating, and our
model would not work at inference time.
Let’s start training.
seq2seq_rnn.compile(
optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
seq2seq_rnn.fit(train_ds, epochs=15, validation_data=val_ds)
Listing 11.31 Translating new sentences with our RNN encoder and decoder
def decode_sequence(input_sentence):
Seed tokenized_input_sentence = source_vectorization([input_sentence])
token decoded_sentence = "[start]"
for i in range(max_decoded_sentence_length):
tokenized_target_sentence = target_vectorization([decoded_sentence])
next_token_predictions = seq2seq_rnn.predict(
Sample the [tokenized_input_sentence, tokenized_target_sentence])
next token. sampled_token_index = [Link](next_token_predictions[0, i, :])
Beyond text classification: Sequence-to-sequence learning 357
Note that this inference setup, while very simple, is rather inefficient, since we repro-
cess the entire source sentence and the entire generated target sentence every time
we sample a new word. In a practical application, you’d factor the encoder and the
decoder as two separate models, and your decoder would only run a single step at
each token-sampling iteration, reusing its previous internal state.
Here are our translation results. Our model works decently well for a toy model,
though it still makes many basic mistakes.
Listing 11.32 Some sample results from the recurrent translation model
There are many ways this toy model could be improved: We could use a deep stack of
recurrent layers for both the encoder and the decoder (note that for the decoder, this
makes state management a bit more involved). We could use an LSTM instead of a GRU.
And so on. Beyond such tweaks, however, the RNN approach to sequence-to-sequence
learning has a few fundamental limitations:
The source sequence representation has to be held entirely in the encoder state
vector(s), which puts significant limitations on the size and complexity of the
sentences you can translate. It’s a bit as if a human were translating a sentence
entirely from memory, without looking twice at the source sentence while pro-
ducing the translation.
RNNs have trouble dealing with very long sequences, since they tend to pro-
gressively forget about the past—by the time you’ve reached the 100th token in
either sequence, little information remains about the start of the sequence.
358 CHAPTER 11 Deep learning for text
That means RNN-based models can’t hold onto long-term context, which can
be essential for translating long documents.
These limitations are what has led the machine learning community to embrace the
Transformer architecture for sequence-to-sequence problems. Let’s take a look.
LayerNormalization
Dense
LayerNormalization
TransformerDecoder
LayerNormalization
+
TransformerEncoder
+
Dense
MultiHeadAttention
Dense
LayerNormalization LayerNormalization
+ +
MultiHeadAttention MultiHeadAttention
Source T arget
[[Link](dense_dim, activation="relu"),
[Link](embed_dim),]
)
self.layernorm_1 = [Link]()
self.layernorm_2 = [Link]()
self.layernorm_3 = [Link]()
self.supports_masking = True
This attribute ensures that the layer will
propagate its input mask to its outputs;
def get_config(self): masking in Keras is explicitly opt-in. If
config = super().get_config() you pass a mask to a layer that doesn’t
[Link]({ implement compute_mask() and that
"embed_dim": self.embed_dim, doesn’t expose this supports_masking
"num_heads": self.num_heads, attribute, that’s an error.
"dense_dim": self.dense_dim,
})
return config
Now we can write down the full call() method implementing the forward pass of the
decoder.
embed_dim = 256
dense_dim = 2048 Encode the
num_heads = 8 source sentence.
encoder_inputs = [Link](shape=(None,), dtype="int64", name="english")
x = PositionalEmbedding(sequence_length, vocab_size, embed_dim)(encoder_inputs)
encoder_outputs = TransformerEncoder(embed_dim, dense_dim, num_heads)(x)
We’re now ready to train our model—we get to 67% accuracy, a good deal above the
GRU-based model.
[Link](
optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
[Link](train_ds, epochs=30, validation_data=val_ds)
Finally, let’s try using our model to translate never-seen-before English sentences from
the test set. The setup is identical to what we used for the sequence-to-sequence RNN
model.
import numpy as np
spa_vocab = target_vectorization.get_vocabulary()
spa_index_lookup = dict(zip(range(len(spa_vocab)), spa_vocab))
max_decoded_sentence_length = 20
def decode_sequence(input_sentence):
tokenized_input_sentence = source_vectorization([input_sentence])
decoded_sentence = "[start]"
for i in range(max_decoded_sentence_length):
tokenized_target_sentence = target_vectorization(
[decoded_sentence])[:, :-1]
predictions = transformer(
Sample the [tokenized_input_sentence, tokenized_target_sentence])
next token. sampled_token_index = [Link](predictions[0, i, :])
sampled_token = spa_index_lookup[sampled_token_index] Convert the
decoded_sentence += " " + sampled_token next token
if sampled_token == "[end]": prediction to
Exit condition break a string, and
return decoded_sentence append it to
the generated
test_eng_texts = [pair[0] for pair in test_pairs] sentence.
for _ in range(20):
input_sentence = [Link](test_eng_texts)
print("-")
print(input_sentence)
print(decode_sequence(input_sentence))
Subjectively, the Transformer seems to perform significantly better than the GRU-
based translation model. It’s still a toy model, but it’s a better toy model.
Summary 363
Listing 11.39 Some sample results from the Transformer translation model
This is a song I learned when I was a kid.
[start] esta es una canción que aprendí cuando era chico [end]
-
While the source sentence wasn’t
She can play the piano.
gendered, this translation assumes
[start] ella puede tocar piano [end]
a male speaker. Keep in mind that
- translation models will often make
I'm not who you think I am. unwarranted assumptions about
[start] no soy la persona que tú creo que soy [end] their input data, which leads to
- algorithmic bias. In the worst
It may have rained a little last night. cases, a model might hallucinate
[start] puede que llueve un poco el pasado [end] memorized information that has
nothing to do with the data it’s
currently processing.
That concludes this chapter on natural language processing—you just went from the
very basics to a fully fledged Transformer that can translate from English to Spanish.
Teaching machines to make sense of language is the latest superpower you can add to
your collection.
Summary
There are two kinds of NLP models: bag-of-words models that process sets of words
or N-grams without taking into account their order, and sequence models that pro-
cess word order. A bag-of-words model is made of Dense layers, while a sequence
model could be an RNN, a 1D convnet, or a Transformer.
When it comes to text classification, the ratio between the number of samples
in your training data and the mean number of words per sample can help you
determine whether you should use a bag-of-words model or a sequence model.
Word embeddings are vector spaces where semantic relationships between words are
modeled as distance relationships between vectors that represent those words.
Sequence-to-sequence learning is a generic, powerful learning framework that can be
applied to solve many NLP problems, including machine translation. A sequence-
to-sequence model is made of an encoder, which processes a source sequence,
and a decoder, which tries to predict future tokens in target sequence by looking
at past tokens, with the help of the encoder-processed source sequence.
Neural attention is a way to create context-aware word representations. It’s the
basis for the Transformer architecture.
The Transformer architecture, which consists of a TransformerEncoder and a
TransformerDecoder, yields excellent results on sequence-to-sequence tasks.
The first half, the TransformerEncoder, can also be used for text classification
or any sort of single-input NLP task.
Generative deep learning
364
365
a Long Short-Term Memory. Maybe you’ve recently listened to music that was tenta-
tively generated by a neural network.
Granted, the artistic productions we’ve seen from AI so far have been fairly low
quality. AI isn’t anywhere close to rivaling human screenwriters, painters, and compos-
ers. But replacing humans was always beside the point: artificial intelligence isn’t
about replacing our own intelligence with something else, it’s about bringing into our
lives and work more intelligence—intelligence of a different kind. In many fields, but
especially in creative ones, AI will be used by humans as a tool to augment their own
capabilities: more augmented intelligence than artificial intelligence.
A large part of artistic creation consists of simple pattern recognition and technical
skill. And that’s precisely the part of the process that many find less attractive or even
dispensable. That’s where AI comes in. Our perceptual modalities, our language, and
our artwork all have statistical structure. Learning this structure is what deep learning
algorithms excel at. Machine learning models can learn the statistical latent space of
images, music, and stories, and they can then sample from this space, creating new art-
works with characteristics similar to those the model has seen in its training data.
Naturally, such sampling is hardly an act of artistic creation in itself. It’s a mere mathe-
matical operation: the algorithm has no grounding in human life, human emotions,
or our experience of the world; instead, it learns from an experience that has little in
common with ours. It’s only our interpretation, as human spectators, that will give
meaning to what the model generates. But in the hands of a skilled artist, algorithmic
generation can be steered to become meaningful—and beautiful. Latent space sam-
pling can become a brush that empowers the artist, augments our creative affor-
dances, and expands the space of what we can imagine. What’s more, it can make
artistic creation more accessible by eliminating the need for technical skill and prac-
tice—setting up a new medium of pure expression, factoring art apart from craft.
Iannis Xenakis, a visionary pioneer of electronic and algorithmic music, beauti-
fully expressed this same idea in the 1960s, in the context of the application of auto-
mation technology to music composition:1
Freed from tedious calculations, the composer is able to devote himself to the general
problems that the new musical form poses and to explore the nooks and crannies of this
form while modifying the values of the input data. For example, he may test all
instrumental combinations from soloists, to chamber orchestras, to large orchestras. With
the aid of electronic computers the composer becomes a sort of pilot: he presses the buttons,
introduces coordinates, and supervises the controls of a cosmic vessel sailing in the space
of sound, across sonic constellations and galaxies that he could formerly glimpse only as
a distant dream.
In this chapter, we’ll explore from various angles the potential of deep learning to
augment artistic creation. We’ll review sequence data generation (which can be
1
Iannis Xenakis, “Musiques formelles: nouveaux principes formels de composition musicale,” special issue of
La Revue musicale, nos. 253–254 (1963).
366 CHAPTER 12 Generative deep learning
used to generate text or music), DeepDream, and image generation using both vari-
ational autoencoders and generative adversarial networks. We’ll get your computer
to dream up content never seen before; and maybe we’ll get you to dream, too,
about the fantastic possibilities that lie at the intersection of technology and art.
Let’s get started.
2
Alex Graves, “Generating Sequences With Recurrent Neural Networks,” arXiv (2013), [Link]
abs/1308.0850.
Text generation 367
the closest computers get to dreaming.” Several years later, we take a lot of these devel-
opments for granted, but at the time it was difficult to watch Graves’s demonstrations
and not walk away awe-inspired by the possibilities. Between 2015 and 2017, recurrent
neural networks were successfully used for text and dialogue generation, music gener-
ation, and speech synthesis.
Then around 2017–2018, the Transformer architecture started taking over recur-
rent neural networks, not just for supervised natural language processing tasks, but
also for generative sequence models—in particular language modeling (word-level text
generation). The best-known example of a generative Transformer would be GPT-3, a
175 billion parameter text-generation model trained by the startup OpenAI on an
astoundingly large text corpus, including most digitally available books, Wikipedia,
and a large fraction of a crawl of the entire internet. GPT-3 made headlines in 2020
due to its capability to generate plausible-sounding text paragraphs on virtually any
topic, a prowess that has fed a short-lived hype wave worthy of the most torrid AI
summer.
Probability
distribution over Sampled next
Initial text Initial text next word word
Language Sampling
The cat sat on the mat
model strategy
Language Sampling
The cat sat on the mat which
model strategy
...
Figure 12.1 The process of word-by-word text generation using a language model
368 CHAPTER 12 Generative deep learning
Returns a reweighted version of the original distribution. The sum of the distribution
may no longer be 1, so you divide it by its sum to obtain the new distribution.
Higher temperatures result in sampling distributions of higher entropy that will gener-
ate more surprising and unstructured generated data, whereas a lower temperature will
result in less randomness and much more predictable generated data (see figure 12.2).
Elements (words)
Figure 12.2 Different reweightings of one probability distribution. Low temperature = more
deterministic, high temperature = more random.
Listing 12.2 Downloading and uncompressing the IMDB movie reviews dataset
!wget [Link]
!tar -xf aclImdb_v1.[Link]
You’re already familiar with the structure of the data: we get a folder named aclImdb
containing two subfolders, one for negative-sentiment movie reviews, and one for
positive-sentiment reviews. There’s one text file per review. We’ll call text_dataset_
from_directory with label_mode=None to create a dataset that reads from these files
and yields the text content of each file.
Listing 12.3 Creating a dataset from text files (one file = one sample)
Strip the <br /> HTML tag that occurs in many of the
import tensorflow as tf reviews. This did not matter much for text classification,
from tensorflow import keras but we wouldn’t want to generate <br />
dataset = [Link].text_dataset_from_directory( tags in this example!
directory="aclImdb", label_mode=None, batch_size=256)
dataset = [Link](lambda x: [Link].regex_replace(x, "<br />", " "))
Now let’s use a TextVectorization layer to compute the vocabulary we’ll be working
with. We’ll only use the first sequence_length words of each review: our TextVector-
ization layer will cut off anything beyond that when vectorizing a text.
sequence_length = 100
We’ll only consider the top 15,000 most
common words—anything else will be treated
vocab_size = 15000
as the out-of-vocabulary token, "[UNK]".
text_vectorization = TextVectorization(
max_tokens=vocab_size, We want to return integer
output_mode="int", word index sequences.
output_sequence_length=sequence_length,
) We’ll work with inputs and targets
text_vectorization.adapt(dataset) of length 100 (but since we’ll offset the
targets by 1, the model will actually
see sequences of length 99).
Let’s use the layer to create a language modeling dataset where input samples are vec-
torized texts, and corresponding targets are the same texts offset by one word.
x = vectorized_sequences[:, :-1]
Create inputs by cutting
y = vectorized_sequences[:, 1:] off the last word of the
return x, y sequences.
lm_dataset = [Link](prepare_lm_dataset, num_parallel_calls=4)
Create targets by offsetting
the sequences by 1.
Note that we could have used a similar sequence-to-sequence setup on our tempera-
ture forecasting problem in chapter 10: given a sequence of 120 hourly data points,
learn to generate a sequence of 120 temperatures offset by 24 hours in the future.
You’d be not only solving the initial problem, but also solving the 119 related prob-
lems of forecasting temperature in 24 hours, given 1 <= i < 120 prior hourly data
points. If you try to retrain the RNNs from chapter 10 in a sequence-to-sequence
setup, you’ll find that you get similar but incrementally worse results, because the con-
straint of solving these additional 119 related problems with the same model inter-
feres slightly with the task we actually do care about.
In the previous chapter, you learned about the setup you can use for sequence-to-
sequence learning in the general case: feed the source sequence into an encoder, and
then feed both the encoded sequence and the target sequence into a decoder that tries
to predict the same target sequence offset by one step. When you’re doing text genera-
tion, there is no source sequence: you’re just trying to predict the next tokens in the tar-
get sequence given past tokens, which we can do using only the decoder. And thanks to
causal padding, the decoder will only look at words 0…N to predict the word N+1.
Let’s implement our model—we’re going to reuse the building blocks we created
in chapter 11: PositionalEmbedding and TransformerDecoder.
seed text generation, we’ll use the prompt “this movie”: all of our generated texts
will start with this.
class TextGenerator([Link]):
def __init__(self,
prompt, How many words
Prompt that to generate
we use to seed generate_length,
text generation model_input_length,
temperatures=(1.,),
Range of
print_freq=1): temperatures to
[Link] = prompt use for sampling
self.generate_length = generate_length
self.model_input_length = model_input_length
[Link] = temperatures
self.print_freq = print_freq
Here are some cherrypicked examples of what we’re able to generate after 200 epochs
of training. Note that punctuation isn’t part of our vocabulary, so none of our gener-
ated text has any punctuation:
With temperature=0.2
– “this movie is a [UNK] of the original movie and the first half hour of the
movie is pretty good but it is a very good movie it is a good movie for the time
period”
– “this movie is a [UNK] of the movie it is a movie that is so bad that it is a
[UNK] movie it is a movie that is so bad that it makes you laugh and cry at
the same time it is not a movie i dont think ive ever seen”
With temperature=0.5
– “this movie is a [UNK] of the best genre movies of all time and it is not a
good movie it is the only good thing about this movie i have seen it for the
first time and i still remember it being a [UNK] movie i saw a lot of years”
– “this movie is a waste of time and money i have to say that this movie was a
complete waste of time i was surprised to see that the movie was made up of a
good movie and the movie was not very good but it was a waste of time and”
With temperature=0.7
– “this movie is fun to watch and it is really funny to watch all the characters
are extremely hilarious also the cat is a bit like a [UNK] [UNK] and a hat
[UNK] the rules of the movie can be told in another scene saves it from
being in the back of”
– “this movie is about [UNK] and a couple of young people up on a small boat
in the middle of nowhere one might find themselves being exposed to a
[UNK] dentist they are killed by [UNK] i was a huge fan of the book and i
havent seen the original so it”
With temperature=1.0
– “this movie was entertaining i felt the plot line was loud and touching but on
a whole watch a stark contrast to the artistic of the original we watched the
original version of england however whereas arc was a bit of a little too ordi-
nary the [UNK] were the present parent [UNK]”
– “this movie was a masterpiece away from the storyline but this movie was sim-
ply exciting and frustrating it really entertains friends like this the actors in
this movie try to go straight from the sub thats image and they make it a
really good tv show”
With temperature=1.5
– “this movie was possibly the worst film about that 80 women its as weird
insightful actors like barker movies but in great buddies yes no decorated
shield even [UNK] land dinosaur ralph ian was must make a play happened
falls after miscast [UNK] bach not really not wrestlemania seriously sam
didnt exist”
Text generation 375
12.1.6 Wrapping up
You can generate discrete sequence data by training a model to predict the next
token(s), given previous tokens.
In the case of text, such a model is called a language model. It can be based on
either words or characters.
Sampling the next token requires a balance between adhering to what the
model judges likely, and introducing randomness.
One way to handle this is the notion of softmax temperature. Always experi-
ment with different temperatures to find the right one.
12.2 DeepDream
DeepDream is an artistic image-modification technique that uses the representations
learned by convolutional neural networks. It was first released by Google in the sum-
mer of 2015 as an implementation written using the Caffe deep learning library (this
was several months before the first public release of TensorFlow).3 It quickly became
an internet sensation thanks to the trippy pictures it could generate (see, for example,
figure 12.4), full of algorithmic pareidolia artifacts, bird feathers, and dog eyes—a
byproduct of the fact that the DeepDream convnet was trained on ImageNet, where
dog breeds and bird species are vastly overrepresented.
3
Alexander Mordvintsev, Christopher Olah, and Mike Tyka, “DeepDream: A Code Example for Visualizing
Neural Networks,” Google Research Blog, July 1, 2015, [Link]
DeepDream 377
base_image_path = [Link].get_file(
"[Link]", origin="[Link]
[Link]("off")
[Link]([Link].load_img(base_image_path))
Next, we need a pretrained convnet. In Keras, many such convnets are available:
VGG16, VGG19, Xception, ResNet50, and so on, all available with weights pretrained
on ImageNet. You can implement DeepDream with any of them, but your base model
of choice will naturally affect your visualizations, because different architectures result
in different learned features. The convnet used in the original DeepDream release
was an Inception model, and in practice, Inception is known to produce nice-looking
DeepDreams, so we’ll use the Inception V3 model that comes with Keras.
We’ll use our pretrained convnet to create a feature exactor model that returns the
activations of the various intermediate layers, listed in the following code. For each
layer, we pick a scalar score that weights the contribution of the layer to the loss we will
seek to maximize during the gradient ascent process. If you want a complete list of
layer names that you can use to pick new layers to play with, just use [Link]().
Listing 12.11 Configuring the contribution of each layer to the DeepDream loss
layer_settings = {
Layers for which we try to maximize activation, as
"mixed4": 1.0, well as their weight in the total loss. You can tweak
"mixed5": 1.5, these setting to obtain new visual effects.
"mixed6": 2.0,
"mixed7": 2.5,
} Symbolic outputs
outputs_dict = dict( of each layer
[
([Link], [Link])
Model that returns
for layer in [model.get_layer(name)
the activation values
for name in layer_settings.keys()] for every target
] layer (as a dict)
)
feature_extractor = [Link](inputs=[Link], outputs=outputs_dict)
Next, we’ll compute the loss: the quantity we’ll seek to maximize during the gradient-
ascent process at each processing scale. In chapter 9, for filter visualization, we tried to
maximize the value of a specific filter in a specific layer. Here, we’ll simultaneously
maximize the activation of all filters in a number of layers. Specifically, we’ll maximize
a weighted mean of the L2 norm of the activations of a set of high-level layers. The
exact set of layers we choose (as well as their contribution to the final loss) has a major
influence on the visuals we’ll be able to produce, so we want to make these parameters
easily configurable. Lower layers result in geometric patterns, whereas higher layers
result in visuals in which you can recognize some classes from ImageNet (for example,
birds or dogs). We’ll start from a somewhat arbitrary configuration involving four lay-
ers—but you’ll definitely want to explore many different configurations later.
DeepDream 379
Now let’s set up the gradient ascent process that we will run at each octave. You’ll rec-
ognize that it’s the same thing as the filter-visualization technique from chapter 9! The
DeepDream algorithm is simply a multiscale form of filter visualization.
Finally, the outer loop of the DeepDream algorithm. First, we’ll define a list of scales
(also called octaves) at which to process the images. We’ll process our image over
three different such “octaves.” For each successive octave, from the smallest to the
largest, we’ll run 20 gradient ascent steps via gradient_ascent_loop() to maximize
the loss we previously defined. Between each octave, we’ll upscale the image by 40%
(1.4x): we’ll start by processing a small image and then increasingly scale it up (see
figure 12.6).
We define the parameters of this process in the following code. Tweaking these
parameters will allow you to achieve new effects!
380 CHAPTER 12 Generative deep learning
Detail
Detail reinjection
reinjection
Octave 1
Octave 2
Octave 3
Figure 12.6 The DeepDream process: successive scales of spatial processing (octaves) and detail re-injection
upon upscaling
import numpy as np
Util function to open, resize,
def preprocess_image(image_path): and format pictures into
img = [Link].load_img(image_path) appropriate arrays
img = [Link].img_to_array(img)
img = np.expand_dims(img, axis=0)
img = [Link].inception_v3.preprocess_input(img)
return img
Util function to convert a
NumPy array into a valid image
def deprocess_image(img):
img = [Link](([Link][1], [Link][2], 3))
img /= 2.0
img += 0.5 Undo inception v3 Convert to uint8
img *= 255.
preprocessing. and clip to the valid
img = [Link](img, 0, 255).astype("uint8") range [0, 255].
return img
This is the outer loop. To avoid losing a lot of image detail after each successive scale-
up (resulting in increasingly blurry or pixelated images), we can use a simple trick:
DeepDream 381
after each scale-up, we’ll re-inject the lost details back into the image, which is possible
because we know what the original image should look like at the larger scale. Given a
small image size S and a larger image size L, we can compute the difference between
the original image resized to size L and the original resized to size S—this difference
quantifies the details lost when going from S to L.
[Link].save_img("[Link]", deprocess_image([Link]()))
NOTE Because the original Inception V3 network was trained to recognize con-
cepts in images of size 299 × 299, and given that the process involves scaling the
images down by a reasonable factor, the DeepDream implementation produces
much better results on images that are somewhere between 300 × 300 and 400 ×
400. Regardless, you can run the same code on images of any size and any ratio.
On a GPU, it only takes a few seconds to run the whole thing. Figure 12.7 shows the
result of our dream configuration on the test image.
I strongly suggest that you explore what you can do by adjusting which layers you
use in your loss. Layers that are lower in the network contain more-local, less-abstract
representations and lead to dream patterns that look more geometric. Layers that are
higher up lead to more-recognizable visual patterns based on the most common objects
382 CHAPTER 12 Generative deep learning
found in ImageNet, such as dog eyes, bird feathers, and so on. You can use random gen-
eration of the parameters in the layer_settings dictionary to quickly explore many
different layer combinations. Figure 12.8 shows a range of results obtained on an image
of a delicious homemade pastry using different layer configurations.
12.2.2 Wrapping up
DeepDream consists of running a convnet in reverse to generate inputs based
on the representations learned by the network.
The results produced are fun and somewhat similar to the visual artifacts
induced in humans by the disruption of the visual cortex via psychedelics.
Note that the process isn’t specific to image models or even to convnets. It can
be done for speech, music, and more.
In this context, style essentially means textures, colors, and visual patterns in the
image, at various spatial scales, and the content is the higher-level macrostructure of
the image. For instance, blue-and-yellow circular brushstrokes are considered to be
the style in figure 12.9 (using Starry Night by Vincent Van Gogh), and the buildings in
the Tübingen photograph are considered to be the content.
The idea of style transfer, which is tightly related to that of texture generation, has
had a long history in the image-processing community prior to the development of
neural style transfer in 2015. But as it turns out, the deep-learning-based implementa-
tions of style transfer offer results unparalleled by what had been previously achieved
with classical computer vision techniques, and they triggered an amazing renaissance
in creative applications of computer vision.
4
Leon A. Gatys, Alexander S. Ecker, and Matthias Bethge, “A Neural Algorithm of Artistic Style,” arXiv (2015),
[Link]
384 CHAPTER 12 Generative deep learning
The key notion behind implementing style transfer is the same idea that’s central
to all deep learning algorithms: you define a loss function to specify what you want to
achieve, and you minimize this loss. We know what we want to achieve: conserving the
content of the original image while adopting the style of the reference image. If we
were able to mathematically define content and style, then an appropriate loss function
to minimize would be the following:
Here, distance is a norm function such as the L2 norm, content is a function that
takes an image and computes a representation of its content, and style is a function
that takes an image and computes a representation of its style. Minimizing this loss
causes style(combination_image) to be close to style(reference_image), and
content(combination_image) is close to content(original_image), thus achieving
style transfer as we defined it.
A fundamental observation made by Gatys et al. was that deep convolutional neu-
ral networks offer a way to mathematically define the style and content functions.
Let’s see how.
correlations capture the statistics of the patterns of a particular spatial scale, which
empirically correspond to the appearance of the textures found at this scale.
Hence, the style loss aims to preserve similar internal correlations within the activa-
tions of different layers, across the style-reference image and the generated image. In
turn, this guarantees that the textures found at different spatial scales look similar
across the style-reference image and the generated image.
In short, you can use a pretrained convnet to define a loss that will do the following:
Preserve content by maintaining similar high-level layer activations between the
original image and the generated image. The convnet should “see” both the
original image and the generated image as containing the same things.
Preserve style by maintaining similar correlations within activations for both low-
level layers and high-level layers. Feature correlations capture textures: the gen-
erated image and the style-reference image should share the same textures at
different spatial scales.
Now let’s look at a Keras implementation of the original 2015 neural style transfer
algorithm. As you’ll see, it shares many similarities with the DeepDream implementa-
tion we developed in the previous section.
Our content image is shown in figure 12.10, and figure 12.11 shows our style image.
We also need some auxiliary functions for loading, preprocessing, and postprocessing
the images that go in and out of the VGG19 convnet.
Let’s set up the VGG19 network. Like in the DeepDream example, we’ll use the pre-
trained convnet to create a feature exactor model that returns the activations of inter-
mediate layers—all layers in the model this time.
Let’s define the content loss, which will make sure the top layer of the VGG19 convnet
has a similar view of the style image and the combination image.
Next is the style loss. It uses an auxiliary function to compute the Gram matrix of an
input matrix: a map of the correlations found in the original feature matrix.
388 CHAPTER 12 Generative deep learning
def gram_matrix(x):
x = [Link](x, (2, 0, 1))
features = [Link](x, ([Link](x)[0], -1))
gram = [Link](features, [Link](features))
return gram
To these two loss components, you add a third: the total variation loss, which operates
on the pixels of the generated combination image. It encourages spatial continuity in
the generated image, thus avoiding overly pixelated results. You can interpret it as a
regularization loss.
def total_variation_loss(x):
a = [Link](
x[:, : img_height - 1, : img_width - 1, :] - x[:, 1:, : img_width - 1, :]
)
b = [Link](
x[:, : img_height - 1, : img_width - 1, :] - x[:, : img_height - 1, 1:, :]
)
return tf.reduce_sum([Link](a + b, 1.25))
The loss that you minimize is a weighted average of these three losses. To compute the
content loss, you use only one upper layer—the block5_conv2 layer—whereas for the
style loss, you use a list of layers that spans both low-level and high-level layers. You add
the total variation loss at the end.
Depending on the style-reference image and content image you’re using, you’ll
likely want to tune the content_weight coefficient (the contribution of the content
loss to the total loss). A higher content_weight means the target content will be more
recognizable in the generated image.
style_layer_names = [
List of layers to use
"block1_conv1", for the style loss
"block2_conv1",
"block3_conv1",
"block4_conv1", The layer to use for
"block5_conv1", the content loss
] Contribution
content_layer_name = "block5_conv2" weight of the total
total_variation_weight = 1e-6 variation loss
Neural style transfer 389
style_weight = 1e-6
Contribution weight
content_weight = 2.5e-8 of the style loss
Contribution
weight of the
content loss def compute_loss(combination_image, base_image, style_reference_image):
input_tensor = [Link](
[base_image, style_reference_image, combination_image], axis=0)
features = feature_extractor(input_tensor)
loss = [Link](shape=())
Initialize
the loss layer_features = features[content_layer_name]
to 0. base_image_features = layer_features[0, :, :, :] Add the
combination_features = layer_features[2, :, :, :] content
loss = loss + content_weight * content_loss( loss.
base_image_features, combination_features
)
for layer_name in style_layer_names:
layer_features = features[layer_name]
Add the style_reference_features = layer_features[1, :, :, :]
style combination_features = layer_features[2, :, :, :]
loss. style_loss_value = style_loss(
style_reference_features, combination_features)
loss += (style_weight / len(style_layer_names)) * style_loss_value
Finally, let’s set up the gradient-descent process. In the original Gatys et al. paper, opti-
mization is performed using the L-BFGS algorithm, but that’s not available in Tensor-
Flow, so we’ll just do mini-batch gradient descent with the SGD optimizer instead. We’ll
leverage an optimizer feature you haven’t seen before: a learning-rate schedule. We’ll
use it to gradually decrease the learning rate from a very high value (100) to a much
smaller final value (about 20). That way, we’ll make fast progress in the early stages of
training and then proceed more cautiously as we get closer to the loss minimum.
base_image = preprocess_image(base_image_path)
style_reference_image = preprocess_image(style_reference_image_path)
combination_image = [Link](preprocess_image(base_image_path))
Use a Variable to store the
iterations = 4000 combination image since we’ll be
for i in range(1, iterations + 1): updating it during training.
loss, grads = compute_loss_and_grads(
combination_image, base_image, style_reference_image
)
optimizer.apply_gradients([(grads, combination_image)])
Update the
if i % 100 == 0: combination
print(f"Iteration {i}: loss={loss:.2f}") image in a
img = deprocess_image(combination_image.numpy()) direction that
fname = f"combination_image_at_iteration_{i}.png" reduces the style
[Link].save_img(fname, img) transfer loss.
Save the combination
image at regular intervals.
Figure 12.12 shows what you get. Keep in mind that what this technique achieves is
merely a form of image retexturing, or texture transfer. It works best with style-reference
images that are strongly textured and highly self-similar, and with content targets that
don’t require high levels of detail in order to be recognizable. It typically can’t achieve
fairly abstract feats such as transferring the style of one portrait to another. The algo-
rithm is closer to classical signal processing than to AI, so don’t expect it to work like
magic!
Additionally, note that this style-transfer algorithm is slow to run. But the transforma-
tion operated by the setup is simple enough that it can be learned by a small, fast
Generating images with variational autoencoders 391
feedforward convnet as well—as long as you have appropriate training data available.
Fast style transfer can thus be achieved by first spending a lot of compute cycles to
generate input-output training examples for a fixed style-reference image, using the
method outlined here, and then training a simple convnet to learn this style-specific
transformation. Once that’s done, stylizing a given image is instantaneous: it’s just a
forward pass of this small convnet.
12.3.4 Wrapping up
Style transfer consists of creating a new image that preserves the contents of a
target image while also capturing the style of a reference image.
Content can be captured by the high-level activations of a convnet.
Style can be captured by the internal correlations of the activations of different
layers of a convnet.
Hence, deep learning allows style transfer to be formulated as an optimization
process using a loss defined with a pretrained convnet.
Starting from this basic idea, many variants and refinements are possible.
Training data
Learning
?
process
Generator / Decoder
Figure 12.13 Learning a latent vector space of images and using it to sample new
images
latent spaces that are well structured, where specific directions encode a meaningful
axis of variation in the data (see figure 12.14). GANs generate images that can poten-
tially be highly realistic, but the latent space they come from may not have as much
structure and continuity.
Figure 12.14 A continuous space of faces generated by Tom White using VAEs
Generating images with variational autoencoders 393
5
Diederik P. Kingma and Max Welling, “Auto-Encoding Variational Bayes,” arXiv (2013), [Link]
abs/1312.6114.
6
Danilo Jimenez Rezende, Shakir Mohamed, and Daan Wierstra, “Stochastic Backpropagation and Approxi-
mate Inference in Deep Generative Models,” arXiv (2014), [Link]
394 CHAPTER 12 Generative deep learning
A classical image autoencoder takes an image, maps it to a latent vector space via
an encoder module, and then decodes it back to an output with the same dimensions
as the original image, via a decoder module (see figure 12.16). It’s then trained by
using as target data the same images as the input images, meaning the autoencoder
learns to reconstruct the original inputs. By imposing various constraints on the
code (the output of the encoder), you can get the autoencoder to learn more- or
less-interesting latent representations of the data. Most commonly, you’ll constrain
the code to be low-dimensional and sparse (mostly zeros), in which case the encoder
acts as a way to compress the input data into fewer bits of information.
Encoder Decoder
Encoder
Reconstructed
image
Decoder
Point randomly
sampled from
the distribution
Figure 12.17 A VAE maps an image to two vectors, z_mean and z_log_sigma, which
define a probability distribution over the latent space, used to sample a latent point to decode.
Because epsilon is random, the process ensures that every point that’s close to the
latent location where you encoded input_img (z-mean) can be decoded to something
similar to input_img, thus forcing the latent space to be continuously meaningful. Any
two close points in the latent space will decode to highly similar images. Continuity,
combined with the low dimensionality of the latent space, forces every direction in the
latent space to encode a meaningful axis of variation of the data, making the latent
space very structured and thus highly suitable to manipulation via concept vectors.
The parameters of a VAE are trained via two loss functions: a reconstruction loss that
forces the decoded samples to match the initial inputs, and a regularization loss that
helps learn well-rounded latent distributions and reduces overfitting to the training
data. Schematically, the process looks like this:
Encodes the input into mean Draws a latent
and variance parameters point using a small
random epsilon
z_mean, z_log_variance = encoder(input_img)
z = z_mean + exp(z_log_variance) * epsilon
reconstructed_img = decoder(z)
Decodes z
model = Model(input_img, reconstructed_img) back to an
Instantiates the autoencoder model, which image
maps an input image to its reconstruction
You can then train the model using the reconstruction loss and the regularization loss.
For the regularization loss, we typically use an expression (the Kullback–Leibler diver-
gence) meant to nudge the distribution of the encoder output toward a well-rounded
normal distribution centered around 0. This provides the encoder with a sensible
assumption about the structure of the latent space it’s modeling.
Now let’s see what implementing a VAE looks like in practice!
396 CHAPTER 12 Generative deep learning
Next is the code for using z_mean and z_log_var, the parameters of the statistical dis-
tribution assumed to have produced input_img, to generate a latent space point z.
import tensorflow as tf
class Sampler([Link]):
Draw a batch of
def call(self, z_mean, z_log_var): random normal
batch_size = [Link](z_mean)[0] vectors.
Apply the VAE z_size = [Link](z_mean)[1]
sampling epsilon = [Link](shape=(batch_size, z_size))
formula. return z_mean + [Link](0.5 * z_log_var) * epsilon
The following listing shows the decoder implementation. We reshape the vector z to
the dimensions of an image and then use a few convolution layers to obtain a final
image output that has the same dimensions as the original input_img.
Listing 12.26 VAE decoder network, mapping latent space points to images
Input where Produce the same number of coefficients that we
we’ll feed z had at the level of the Flatten layer in the encoder.
latent_inputs = [Link](shape=(latent_dim,)) Revert the
x = [Link](7 * 7 * 64, activation="relu")(latent_inputs) Flatten layer
x = [Link]((7, 7, 64))(x) of the encoder.
Revert the x = layers.Conv2DTranspose(64, 3, activation="relu", strides=2, padding="same")(x)
Conv2D layers x = layers.Conv2DTranspose(32, 3, activation="relu", strides=2, padding="same")(x)
of the encoder.
decoder_outputs = layers.Conv2D(1, 3, activation="sigmoid", padding="same")(x)
decoder = [Link](latent_inputs, decoder_outputs, name="decoder")
The output ends up with shape (28, 28, 1).
Its summary looks like this:
>>> [Link]()
Model: "decoder"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_2 (InputLayer) [(None, 2)] 0
_________________________________________________________________
dense_1 (Dense) (None, 3136) 9408
398 CHAPTER 12 Generative deep learning
_________________________________________________________________
reshape (Reshape) (None, 7, 7, 64) 0
_________________________________________________________________
conv2d_transpose (Conv2DTran (None, 14, 14, 64) 36928
_________________________________________________________________
conv2d_transpose_1 (Conv2DTr (None, 28, 28, 32) 18464
_________________________________________________________________
conv2d_2 (Conv2D) (None, 28, 28, 1) 289
=================================================================
Total params: 65,089
Trainable params: 65,089
Non-trainable params: 0
_________________________________________________________________
Now let’s create the VAE model itself. This is your first example of a model that isn’t
doing supervised learning (an autoencoder is an example of self-supervised learning,
because it uses its inputs as targets). Whenever you depart from classic supervised
learning, it’s common to subclass the Model class and implement a custom train_
step() to specify the new training logic, a workflow you learned about in chapter 7.
That’s what we’ll do here.
class VAE([Link]):
def __init__(self, encoder, decoder, **kwargs):
super().__init__(**kwargs)
We use these metrics to keep
[Link] = encoder track of the loss averages
[Link] = decoder over each epoch.
[Link] = Sampler()
self.total_loss_tracker = [Link](name="total_loss")
self.reconstruction_loss_tracker = [Link](
name="reconstruction_loss")
self.kl_loss_tracker = [Link](name="kl_loss")
@property
def metrics(self):
We list the metrics in the metrics
return [self.total_loss_tracker, property to enable the model to reset
self.reconstruction_loss_tracker, them after each epoch (or between
self.kl_loss_tracker] multiple calls to fit()/evaluate()).
def train_step(self, data):
with [Link]() as tape:
z_mean, z_log_var = [Link](data) We sum the reconstruction
z = [Link](z_mean, z_log_var)
loss over the spatial
dimensions (axes 1 and 2)
reconstruction = decoder(z)
and take its mean over the
reconstruction_loss = tf.reduce_mean(
batch dimension.
tf.reduce_sum(
[Link].binary_crossentropy(data, reconstruction),
axis=(1, 2)
)
)
Add the regularization kl_loss = -0.5 * (1 + z_log_var - [Link](z_mean) -
term (Kullback–Leibler [Link](z_log_var))
divergence). total_loss = reconstruction_loss + tf.reduce_mean(kl_loss)
Generating images with variational autoencoders 399
Finally, we’re ready to instantiate and train the model on MNIST digits. Because the
loss is taken care of in the custom layer, we don’t specify an external loss at compile
time (loss=None), which in turn means we won’t pass target data during training (as
you can see, we only pass x_train to the model in fit()).
Note that we don’t pass targets Note that we don’t pass a loss
in fit(), since train_step() argument in compile(), since the loss
doesn’t expect any. is already part of the train_step().
Once the model is trained, we can use the decoder network to turn arbitrary latent
space vectors into images.
j * digit_size : (j + 1) * digit_size,
] = digit
[Link](figsize=(15, 15))
start_range = digit_size // 2
end_range = n * digit_size + start_range
pixel_range = [Link](start_range, end_range, digit_size)
sample_range_x = [Link](grid_x, 1)
sample_range_y = [Link](grid_y, 1)
[Link](pixel_range, sample_range_x)
[Link](pixel_range, sample_range_y)
[Link]("z[0]")
[Link]("z[1]")
[Link]("off")
[Link](figure, cmap="Greys_r")
The grid of sampled digits (see figure 12.18) shows a completely continuous distribu-
tion of the different digit classes, with one digit morphing into another as you follow a
path through latent space. Specific directions in this space have a meaning: for exam-
ple, there are directions for “five-ness,” “one-ness,” and so on.
In the next section, we’ll cover in detail the other major tool for generating artificial
images: generative adversarial networks (GANs).
12.4.5 Wrapping up
Image generation with deep learning is done by learning latent spaces that cap-
ture statistical information about a dataset of images. By sampling and decod-
ing points from the latent space, you can generate never-before-seen images.
There are two major tools to do this: VAEs and GANs.
VAEs result in highly structured, continuous latent representations. For this rea-
son, they work well for doing all sorts of image editing in latent space: face
swapping, turning a frowning face into a smiling face, and so on. They also work
nicely for doing latent-space-based animations, such as animating a walk along a
cross section of the latent space or showing a starting image slowly morphing
into different images in a continuous way.
GANs enable the generation of realistic single-frame images but may not induce
latent spaces with solid structure and high continuity.
Most successful practical applications I have seen with images rely on VAEs, but GANs
have enjoyed enduring popularity in the world of academic research. You’ll find out
how they work and how to implement one in the next section.
The generator network is trained to be able to fool the discriminator network, and
thus it evolves toward generating increasingly realistic images as training goes on: arti-
ficial images that look indistinguishable from real ones, to the extent that it’s impossi-
ble for the discriminator network to tell the two apart (see figure 12.19). Meanwhile,
the discriminator is constantly adapting to the gradually improving capabilities of the
generator, setting a high bar of realism for the generated images. Once training is
over, the generator is capable of turning any point in its input space into a believable
image. Unlike VAEs, this latent space has fewer explicit guarantees of meaningful
structure; in particular, it isn’t continuous.
Random Generated
latent space (decoded)
vector image
Generator (decoder)
Training
feedback
Mix of real
and fake images
Figure 12.19 A generator transforms random latent vectors into images, and a discriminator
seeks to tell real images from generated ones. The generator is trained to fool the discriminator.
Remarkably, a GAN is a system where the optimization minimum isn’t fixed, unlike in
any other training setup you’ve encountered in this book. Normally, gradient descent
consists of rolling down hills in a static loss landscape. But with a GAN, every step
taken down the hill changes the entire landscape a little. It’s a dynamic system where
the optimization process is seeking not a minimum, but an equilibrium between two
forces. For this reason, GANs are notoriously difficult to train—getting a GAN to work
requires lots of careful tuning of the model architecture and training parameters.
We’ll train our GAN on images from the Large-scale CelebFaces Attributes dataset
(known as CelebA), a dataset of 200,000 faces of celebrities ([Link]
.[Link]/projects/[Link]) To speed up training, we’ll resize the images to 64 × 64,
so we’ll be learning to generate 64 × 64 images of human faces.
Schematically, the GAN looks like this:
A generator network maps vectors of shape (latent_dim,) to images of shape
(64, 64, 3).
A discriminator network maps images of shape (64, 64, 3) to a binary score
estimating the probability that the image is real.
A gan network chains the generator and the discriminator together: gan(x) =
discriminator(generator(x)). Thus, this gan network maps latent space vec-
tors to the discriminator’s assessment of the realism of these latent vectors as
decoded by the generator.
We train the discriminator using examples of real and fake images along with
“real”/“fake” labels, just as we train any regular image-classification model.
To train the generator, we use the gradients of the generator’s weights with
regard to the loss of the gan model. This means that at every step, we move the
weights of the generator in a direction that makes the discriminator more likely
to classify as “real” the images decoded by the generator. In other words, we
train the generator to fool the discriminator.
Here are a few of the tricks used in the implementation of the GAN generator and
discriminator in this section. It isn’t an exhaustive list of GAN-related tips; you’ll find
many more across the GAN literature:
We use strides instead of pooling for downsampling feature maps in the dis-
criminator, just like we did in our VAE encoder.
We sample points from the latent space using a normal distribution (Gaussian dis-
tribution), not a uniform distribution.
Stochasticity is good for inducing robustness. Because GAN training results in a
dynamic equilibrium, GANs are likely to get stuck in all sorts of ways. Introduc-
ing randomness during training helps prevent this. We introduce randomness
by adding random noise to the labels for the discriminator.
Sparse gradients can hinder GAN training. In deep learning, sparsity is often
a desirable property, but not in GANs. Two things can induce gradient spar-
sity: max pooling operations and relu activations. Instead of max pooling,
we recommend using strided convolutions for downsampling, and we recom-
mend using a LeakyReLU layer instead of a relu activation. It’s similar to
relu, but it relaxes sparsity constraints by allowing small negative activation
values.
In generated images, it’s common to see checkerboard artifacts caused by
unequal coverage of the pixel space in the generator (see figure 12.21). To fix
this, we use a kernel size that’s divisible by the stride size whenever we use a
strided Conv2DTranspose or Conv2D in both the generator and the discriminator.
discriminator = [Link](
[
[Link](shape=(64, 64, 3)),
layers.Conv2D(64, kernel_size=4, strides=2, padding="same"),
[Link](alpha=0.2),
layers.Conv2D(128, kernel_size=4, strides=2, padding="same"),
[Link](alpha=0.2),
layers.Conv2D(128, kernel_size=4, strides=2, padding="same"),
[Link](alpha=0.2),
One dropout layer:
[Link](),
an important trick!
[Link](0.2),
[Link](1, activation="sigmoid"),
],
name="discriminator",
)
>>> [Link]()
Model: "discriminator"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d (Conv2D) (None, 32, 32, 64) 3136
_________________________________________________________________
leaky_re_lu (LeakyReLU) (None, 32, 32, 64) 0
_________________________________________________________________
conv2d_1 (Conv2D) (None, 16, 16, 128) 131200
_________________________________________________________________
leaky_re_lu_1 (LeakyReLU) (None, 16, 16, 128) 0
_________________________________________________________________
conv2d_2 (Conv2D) (None, 8, 8, 128) 262272
_________________________________________________________________
leaky_re_lu_2 (LeakyReLU) (None, 8, 8, 128) 0
_________________________________________________________________
flatten (Flatten) (None, 8192) 0
_________________________________________________________________
dropout (Dropout) (None, 8192) 0
_________________________________________________________________
dense (Dense) (None, 1) 8193
=================================================================
Total params: 404,801
Trainable params: 404,801
Non-trainable params: 0
_________________________________________________________________
Introduction to generative adversarial networks 407
latent_dim = 128
The latent space
will be made of 128-
generator = [Link]( Produce the same number
dimensional vectors.
[ of coefficients we had at
Revert the [Link](shape=(latent_dim,)), the level of the Flatten
Flatten layer of [Link](8 * 8 * 128), layer in the encoder.
the encoder. [Link]((8, 8, 128)),
layers.Conv2DTranspose(128, kernel_size=4, strides=2, padding="same"),
Revert the [Link](alpha=0.2),
Conv2D layers layers.Conv2DTranspose(256, kernel_size=4, strides=2, padding="same"),
of the encoder. [Link](alpha=0.2),
layers.Conv2DTranspose(512, kernel_size=4, strides=2, padding="same"),
The output ends [Link](alpha=0.2),
up with shape layers.Conv2D(3, kernel_size=5, padding="same", activation="sigmoid"),
(28, 28, 1). ],
name="generator", We use LeakyReLU
) as our activation.
>>> [Link]()
Model: "generator"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_1 (Dense) (None, 8192) 1056768
_________________________________________________________________
reshape (Reshape) (None, 8, 8, 128) 0
_________________________________________________________________
conv2d_transpose (Conv2DTran (None, 16, 16, 128) 262272
_________________________________________________________________
leaky_re_lu_3 (LeakyReLU) (None, 16, 16, 128) 0
_________________________________________________________________
conv2d_transpose_1 (Conv2DTr (None, 32, 32, 256) 524544
_________________________________________________________________
leaky_re_lu_4 (LeakyReLU) (None, 32, 32, 256) 0
_________________________________________________________________
conv2d_transpose_2 (Conv2DTr (None, 64, 64, 512) 2097664
_________________________________________________________________
leaky_re_lu_5 (LeakyReLU) (None, 64, 64, 512) 0
_________________________________________________________________
conv2d_3 (Conv2D) (None, 64, 64, 3) 38403
=================================================================
Total params: 3,979,651
Trainable params: 3,979,651
Non-trainable params: 0
_________________________________________________________________
408 CHAPTER 12 Generative deep learning
import tensorflow as tf
class GAN([Link]):
def __init__(self, discriminator, generator, latent_dim):
super().__init__()
[Link] = discriminator
[Link] = generator
self.latent_dim = latent_dim
self.d_loss_metric = [Link](name="d_loss")
self.g_loss_metric = [Link](name="g_loss")
@property
def metrics(self):
return [self.d_loss_metric, self.g_loss_metric]
Introduction to generative adversarial networks 409
self.d_loss_metric.update_state(d_loss)
self.g_loss_metric.update_state(g_loss)
return {"d_loss": self.d_loss_metric.result(),
"g_loss": self.g_loss_metric.result()}
Before we start training, let’s also set up a callback to monitor our results: it will use
the generator to create and save a number of fake images at the end of each epoch.
class GANMonitor([Link]):
def __init__(self, num_img=3, latent_dim=128):
self.num_img = num_img
self.latent_dim = latent_dim
for i in range(self.num_img):
img = [Link].array_to_img(generated_images[i])
[Link](f"generated_img_{epoch:03d}_{i}.png")
epochs = 100
You’ll start getting
interesting results
gan = GAN(discriminator=discriminator, generator=generator, after epoch 20.
latent_dim=latent_dim)
[Link](
d_optimizer=[Link](learning_rate=0.0001),
g_optimizer=[Link](learning_rate=0.0001),
loss_fn=[Link](),
)
[Link](
dataset, epochs=epochs,
callbacks=[GANMonitor(num_img=10, latent_dim=latent_dim)]
)
When training, you may see the adversarial loss begin to increase considerably, while
the discriminative loss tends to zero—the discriminator may end up dominating the
generator. If that’s the case, try reducing the discriminator learning rate, and increase
the dropout rate of the discriminator.
Figure 12.22 shows what our GAN is capable of generating after 30 epochs of training.
12.5.7 Wrapping up
A GAN consists of a generator network coupled with a discriminator network.
The discriminator is trained to differentiate between the output of the genera-
tor and real images from a training dataset, and the generator is trained to fool
Summary 411
the discriminator. Remarkably, the generator never sees images from the training
set directly; the information it has about the data comes from the discriminator.
GANs are difficult to train, because training a GAN is a dynamic process rather
than a simple gradient descent process with a fixed loss landscape. Getting a
GAN to train correctly requires using a number of heuristic tricks, as well as
extensive tuning.
GANs can potentially produce highly realistic images. But unlike VAEs, the
latent space they learn doesn’t have a neat continuous structure and thus may
not be suited for certain practical applications, such as image editing via latent-
space concept vectors.
These few techniques cover only the basics of this fast-expanding field. There’s a lot
more to discover out there—generative deep learning is deserving of an entire book
of its own.
Summary
You can use a sequence-to-sequence model to generate sequence data, one step
at a time. This is applicable to text generation, but also to note-by-note music
generation or any other type of timeseries data.
DeepDream works by maximizing convnet layer activations through gradient
ascent in input space.
In the style-transfer algorithm, a content image and a style image are combined
together via gradient descent to produce an image with the high-level features
of the content image and the local characteristics of the style image.
VAEs and GANs are models that learn a latent space of images and can then
dream up entirely new images by sampling from the latent space. Concept vectors
in the latent space can even be used for image editing.
Best practices
for the real world
You’ve come far since the beginning of this book. You can now train image classifi-
cation models, image segmentation models, models for classification or regression
on vector data, timeseries forecasting models, text-classification models, sequence-
to-sequence models, and even generative models for text and images. You’ve got all
the bases covered.
However, your models so far have all been trained at a small scale—on small
datasets, with a single GPU—and they generally haven’t reached the best achiev-
able performance on each dataset we looked at. This book is, after all, an introduc-
tory book. If you are to go out in the real world and achieve state-of-the-art results
on brand new problems, there’s still a bit of a chasm that you’ll need to cross.
This penultimate chapter is about bridging that gap and giving you the best
practices you’ll need as you go from machine learning student to fully fledged
412
Getting the most out of your models 413
machine learning engineer. We’ll review essential techniques for systematically improv-
ing model performance: hyperparameter tuning and model ensembling. Then we’ll
look at how you can speed up and scale up model training, with multi-GPU and TPU
training, mixed precision, and leveraging remote computing resources in the cloud.
optimizer=optimizer,
loss="sparse_categorical_crossentropy",
metrics=["accuracy"]) The function returns
return model a compiled model.
import kerastuner as kt
class SimpleMLP([Link]):
def __init__(self, num_classes):
self.num_classes = num_classes
The next step is to define a “tuner.” Schematically, you can think of a tuner as a for
loop that will repeatedly
Pick a set of hyperparameter values
Call the model-building function with these values to create a model
Train the model and record its metrics
KerasTuner has several built-in tuners available—RandomSearch, BayesianOptimiza-
tion, and Hyperband. Let’s try BayesianOptimization, a tuner that attempts to make
smart predictions for which new hyperparameter values are likely to perform best
given the outcomes of previous choices:
Specify the model-build- Specify the metric that the tuner will seek to
ing function (or hyper- optimize. Always specify validation metrics,
model instance). since the goal of the search process is to
tuner = [Link](
find models that generalize!
build_model,
objective="val_accuracy", Maximum number of different
max_trials=100, model configurations (“trials”)
to try before ending the search.
416 CHAPTER 13 Best practices for the real world
executions_per_trial=2,
To reduce metrics variance, you can train the
directory="mnist_kt_test", same model multiple times and average the results.
overwrite=True, executions_per_trial is how many training rounds
) (executions) to run for each model configuration (trial).
Where Whether to overwrite data in directory to start a new search. Set this to
to store True if you’ve modified the model-building function, or to False to resume
search logs a previously started search with the same model-building function.
>>> tuner.search_space_summary()
Search space summary
Default search space size: 2
units (Int)
{"default": None,
"conditions": [],
"min_value": 128,
"max_value": 1024,
"step": 128,
"sampling": None}
optimizer (Choice)
{"default": "rmsprop",
"conditions": [],
"values": ["rmsprop", "adam"],
"ordered": False}
Finally, let’s launch the search. Don’t forget to pass validation data, and make sure not
to use your test set as validation data—otherwise you’d quickly start overfitting to your
test data, and you wouldn’t be able to trust your test metrics anymore:
def get_best_epoch(hp):
model = build_model(hp)
callbacks=[ Note the very
[Link]( high patience
monitor="val_loss", mode="min", patience=10) value.
]
418 CHAPTER 13 Best practices for the real world
history = [Link](
x_train, y_train,
validation_data=(x_val, y_val),
epochs=100,
batch_size=128,
callbacks=callbacks)
val_loss_per_epoch = [Link]["val_loss"]
best_epoch = val_loss_per_epoch.index(min(val_loss_per_epoch)) + 1
print(f"Best epoch: {best_epoch}")
return best_epoch
Finally, train on the full dataset for just a bit longer than this epoch count, since
you’re training on more data; 20% more in this case:
def get_best_trained_model(hp):
best_epoch = get_best_epoch(hp)
[Link](
x_train_full, y_train_full,
batch_size=128, epochs=int(best_epoch * 1.2))
return model
best_models = []
for hp in best_hps:
model = get_best_trained_model(hp)
[Link](x_test, y_test)
best_models.append(model)
Note that if you’re not worried about slightly underperforming, there’s a shortcut you
can take: just use the tuner to reload the top-performing models with the best weights
saved during the hyperparameter search, without retraining new models from scratch:
best_models = tuner.get_best_models(top_n)
NOTE One important issue to think about when doing automatic hyper-
parameter optimization at scale is validation-set overfitting. Because you’re
updating hyperparameters based on a signal that is computed using your vali-
dation data, you’re effectively training them on the validation data, and thus
they will quickly overfit to the validation data. Always keep this in mind.
THE ART OF CRAFTING THE RIGHT SEARCH SPACE
Overall, hyperparameter optimization is a powerful technique that is an absolute
requirement for getting to state-of-the-art models on any task or to win machine learn-
ing competitions. Think about it: once upon a time, people handcrafted the features
that went into shallow machine learning models. That was very much suboptimal.
Now, deep learning automates the task of hierarchical feature engineering—features
are learned using a feedback signal, not hand-tuned, and that’s the way it should be.
In the same way, you shouldn’t handcraft your model architectures; you should opti-
mize them in a principled way.
However, doing hyperparameter tuning is not a replacement for being familiar
with model architecture best practices. Search spaces grow combinatorially with the
Getting the most out of your models 419
number of choices, so it would be far too expensive to turn everything into a hyper-
parameter and let the tuner sort it out. You need to be smart about designing the
right search space. Hyperparameter tuning is automation, not magic: you use it to
automate experiments that you would otherwise have run by hand, but you still
need to handpick experiment configurations that have the potential to yield good
metrics.
The good news is that by leveraging hyperparameter tuning, the configuration
decisions you have to make graduate from micro-decisions (what number of units do I
pick for this layer?) to higher-level architecture decisions (should I use residual con-
nections throughout this model?). And while micro-decisions are specific to a certain
model and a certain dataset, higher-level decisions generalize better across different
tasks and datasets. For instance, pretty much every image classification problem can
be solved via the same sort of search-space template.
Following this logic, KerasTuner attempts to provide premade search spaces that are
relevant to broad categories of problems, such as image classification. Just add data,
run the search, and get a pretty good model. You can try the hypermodels [Link]-
[Link] and [Link], which are effectively
tunable versions of Keras Applications models.
THE FUTURE OF HYPERPARAMETER TUNING: AUTOMATED MACHINE LEARNING
Currently, most of your job as a deep learning engineer consists of munging data with
Python scripts and then tuning the architecture and hyperparameters of a deep net-
work at length to get a working model, or even to get a state-of-the-art model, if you
are that ambitious. Needless to say, that isn’t an optimal setup. But automation can
help, and it won’t stop merely at hyperparameter tuning.
Searching over a set of possible learning rates or possible layer sizes is just the first
step. We can also be far more ambitious and attempt to generate the model architecture
itself from scratch, with as few constraints as possible, such as via reinforcement learn-
ing or genetic algorithms. In the future, entire end-to-end machine learning pipelines
will be automatically generated, rather than be handcrafted by engineer-artisans. This
is called automated machine learning, or AutoML. You can already leverage libraries
like AutoKeras ([Link] to solve basic machine
learning problems with very little involvement on your part.
Today, AutoML is still in its early days, and it doesn’t scale to large problems. But
when AutoML becomes mature enough for widespread adoption, the jobs of machine
learning engineers won’t disappear—rather, engineers will move up the value-creation
chain. They will begin to put much more effort into data curation, crafting complex loss
functions that truly reflect business goals, as well as understanding how their models
impact the digital ecosystems in which they’re deployed (such as the users who consume
the model’s predictions and generate the model’s training data). These are problems
that only the largest companies can afford to consider at present.
Always look at the big picture, focus on understanding the fundamentals, and keep
in mind that the highly specialized tedium will eventually be automated away. See it as
420 CHAPTER 13 Best practices for the real world
a gift—greater productivity for your workflows—and not as a threat to your own rele-
vance. It shouldn’t be your job to tune knobs endlessly.
However, this will work only if the classifiers are more or less equally good. If one of
them is significantly worse than the others, the final predictions may not be as good as
the best classifier of the group.
A smarter way to ensemble classifiers is to do a weighted average, where the
weights are learned on the validation data—typically, the better classifiers are given a
higher weight, and the worse classifiers are given a lower weight. To search for a good
set of ensembling weights, you can use random search or a simple optimization algo-
rithm, such as the Nelder-Mead algorithm:
preds_a = model_a.predict(x_val)
preds_b = model_b.predict(x_val)
preds_c = model_c.predict(x_val)
Scaling-up model training 421
preds_d = model_d.predict(x_val)
final_preds = 0.5 * preds_a + 0.25 * preds_b + 0.1 * preds_c + 0.15 * preds_d
These weights (0.5, 0.25, 0.1, 0.15) are
assumed to be learned empirically.
There are many possible variants: you can do an average of an exponential of the pre-
dictions, for instance. In general, a simple weighted average with weights optimized
on the validation data provides a very strong baseline.
The key to making ensembling work is the diversity of the set of classifiers. Diversity
is strength. If all the blind men only touched the elephant’s trunk, they would agree
that elephants are like snakes, and they would forever stay ignorant of the truth of the
elephant. Diversity is what makes ensembling work. In machine learning terms, if all
of your models are biased in the same way, your ensemble will retain this same bias. If
your models are biased in different ways, the biases will cancel each other out, and the
ensemble will be more robust and more accurate.
For this reason, you should ensemble models that are as good as possible while being
as different as possible. This typically means using very different architectures or even
different brands of machine learning approaches. One thing that is largely not worth
doing is ensembling the same network trained several times independently, from dif-
ferent random initializations. If the only difference between your models is their ran-
dom initialization and the order in which they were exposed to the training data, then
your ensemble will be low-diversity and will provide only a tiny improvement over any
single model.
One thing I have found to work well in practice—but that doesn’t generalize to
every problem domain—is using an ensemble of tree-based methods (such as random
forests or gradient-boosted trees) and deep neural networks. In 2014, Andrey Kolev
and I took fourth place in the Higgs Boson decay detection challenge on Kaggle
([Link]/c/higgs-boson) using an ensemble of various tree models and deep
neural networks. Remarkably, one of the models in the ensemble originated from a
different method than the others (it was a regularized greedy forest), and it had a sig-
nificantly worse score than the others. Unsurprisingly, it was assigned a small weight in
the ensemble. But to our surprise, it turned out to improve the overall ensemble by a
large factor, because it was so different from every other model: it provided informa-
tion that the other models didn’t have access to. That’s precisely the point of ensem-
bling. It’s not so much about how good your best model is; it’s about the diversity of
your set of candidate models.
Idea
Results Experiment
GPUs, TPUs
Figure 13.1 The loop of progress
As you develop your expertise with the Keras API, how fast you can code up your deep
learning experiments will cease to be the bottleneck of this progress cycle. The next
bottleneck will become the speed at which you can train your models. Fast training
infrastructure means that you can get your results back in 10–15 minutes, and hence,
that you can go through dozens of iterations every day. Faster training directly improves
the quality of your deep learning solutions.
In this section, you’ll learn about three ways you can train your models faster:
Mixed-precision training, which you can use even with a single GPU
Training on multiple GPUs
Training on TPUs
Let’s go.
For example, here’s how you would encode the closest float32 value approximating Pi:
0 10000000 10010010000111111011011
+1 128 5707963705062866
For this reason, the numerical error incurred when converting a number to its floating-
point representation can vary wildly depending on the exact value considered, and the
error tends to get larger for numbers with a large absolute value.
The way to think about the resolution of floating-point numbers is in terms of the
smallest distance between two arbitrary numbers that you’ll be able to safely process.
In single precision, that’s around 1e-7. In double precision, that’s around 1e-16. And
in half precision, it’s only 1e-3.
Every model you’ve seen in this book so far used single-precision numbers: it
stored its state as float32 weight variables and ran its computations on float32
inputs. That’s enough precision to run the forward and backwards pass of a model
without losing any information—particularly when it comes to small gradient updates
424 CHAPTER 13 Best practices for the real world
(recall that the typical learning rate is 1e-3, and it’s pretty common to see weight
updates on the order of 1e-6).
You could also use float64, though that would be wasteful—operations like matrix
multiplication or addition are much more expensive in double precision, so you’d be
doing twice as much work for no clear benefits. But you could not do the same with
float16 weights and computation; the gradient descent process wouldn’t run
smoothly, since you couldn’t represent small gradient updates of around 1e-5 or 1e-6.
You can, however, use a hybrid approach: that’s what mixed precision is about. The
idea is to leverage 16-bit computations in places where precision isn’t an issue, and to
work with 32-bit values in other places to maintain numerical stability. Modern GPUs
and TPUs feature specialized hardware that can run 16-bit operations much faster and
use less memory than equivalent 32-bits operations. By using these lower-precision
operations whenever possible, you can speed up training on those devices by a signifi-
cant factor. Meanwhile, by maintaining the precision-sensitive parts of the model in
single precision, you can get these benefits without meaningfully impacting model
quality.
And those benefits are considerable: on modern NVIDIA GPUs, mixed precision
can speed up training by up to 3X. It’s also beneficial when training on a TPU (a sub-
ject we’ll get to in a bit), where it can speed up training by up to 60%.
Note that when you call the Keras fit() method with NumPy data, it will do this con-
version for you.
Scaling-up model training 425
Typically, most of the forward pass of the model will be done in float16 (with the
exception of numerically unstable operations like softmax), while the weights of the
model will be stored and updated in float32.
Keras layers have a variable_dtype and a compute_dtype attribute. By default, both
of these are set to float32. When you turn on mixed precision, the compute_dtype of
most layers switches to float16, and those layers will cast their inputs to float16 and will
perform their computations in float16 (using half-precision copies of the weights).
However, since their variable_dtype is still float32, their weights will be able to receive
accurate float32 updates from the optimizer, as opposed to half-precision updates.
Note that some operations may be numerically unstable in float16 (in particular,
softmax and crossentropy). If you need to opt out of mixed precision for a specific
layer, just pass the argument dtype="float32" to the constructor of this layer.
These few lines implement the most common training setup: single-host, multi-device
synchronous training, also known in TensorFlow as the “mirrored distribution strategy.”
“Single host” means that the different GPUs considered are all on a single machine
(as opposed to a cluster of many machines, each with its own GPU, communicating
over a network). “Synchronous training” means that the state of the per-GPU model
replicas stays the same at all times—there are variants of distributed training where
this isn’t the case.
When you open a MirroredStrategy scope and build your model within it, the
MirroredStrategy object will create one model copy (replica) on each available GPU.
Then, each step of training unfolds in the following way (see figure 13.2):
1 A batch of data (called global batch) is drawn from the dataset.
2 It gets split into four different sub-batches (called local batches). For instance, if
the global batch has 512 samples, each of the four local batches will have 128
samples. Because you want local batches to be large enough to keep the GPU
busy, the global batch size typically needs to be very large.
Scaling-up model training 427
3 Each of the four replicas processes one local batch, independently, on its own
device: they run a forward pass, and then a backward pass. Each replica outputs
a “weight delta” describing by how much to update each weight variable in the
model, given the gradient of the previous weights with respect to the loss of the
model on the local batch.
4 The weight deltas originating from local gradients are efficiently merged across
the four replicas to obtain a global delta, which is applied to all replicas. Because
this is done at the end of every step, the replicas always stay in sync: their
weights are always equal.
Local
Local weight
batch updates
Global Model Averaged
batch replica 0 weight
updates
Figure 13.2 One step of
Dataset MirroredStrategy training:
each model replica computes local
weight updates, which are then
Model
merged and used to update the
replica 1
state of all replicas.
import tensorflow as tf
tpu = [Link].cluster_resolver.[Link]()
print("Device:", [Link]())
You don’t have to worry too much about what this does—it’s just a little incantation
that connects your notebook runtime to the device. Open Sesame.
Much like in the case of multi-GPU training, using the TPU requires you to open a
distribution strategy scope—in this case, a TPUStrategy scope. TPUStrategy follows
the same distribution template as MirroredStrategy—the model is replicated once
per TPU core, and the replicas are kept in sync.
Here’s a simple example.
strategy = [Link](tpu)
print(f"Number of replicas: {strategy.num_replicas_in_sync}")
def build_model(input_size):
inputs = [Link]((input_size, input_size, 3))
x = [Link].preprocess_input(inputs)
x = [Link].ResNet50(
weights=None, include_top=False, pooling="max")(x)
Scaling-up model training 429
with [Link]():
model = build_model(input_size=32)
We’re almost ready to start training. But there’s something a bit curious about TPUs
in Colab: it’s a two-VM setup, meaning that the VM that hosts your notebook runtime
isn’t the same VM that the TPU lives in. Because of this, you won’t be able to train
from files stored on the local disk (that is to say, on the disk linked to the VM that
hosts the notebook). The TPU runtime can’t read from there. You have two options
for data loading:
Train from data that lives in the memory of the VM (not on disk). If your data is
in a NumPy array, this is what you’re already doing.
Store the data in a Google Cloud Storage (GCS) bucket, and create a dataset
that reads the data directly from the bucket, without downloading locally. The
TPU runtime can read data from GCS. This is your only option for datasets that
are too large to live entirely in memory.
In our case, let’s train from NumPy arrays in memory—the CIFAR10 dataset:
(x_train, y_train), (x_test, y_test) = [Link].cifar10.load_data()
[Link](x_train, y_train, batch_size=1024)
Note that TPU training, much like multi-
GPU training, requires large batch sizes to
make sure the device stays well-utilized.
You’ll notice that the first epoch takes a while to start—that’s because your model is
getting compiled to something that the TPU can execute. Once that step is done, the
training itself is blazing fast.
Summary
You can leverage hyperparameter tuning and KerasTuner to automate the
tedium out of finding the best model configuration. But be mindful of validation-
set overfitting!
An ensemble of diverse models can often significantly improve the quality of
your predictions.
You can speed up model training on GPU by turning on mixed precision—
you’ll generally get a nice speed boost at virtually no cost.
To further scale your workflows, you can use the [Link]-
Strategy API to train models on multiple GPUs.
You can even train on Google’s TPUs (available on Colab) by using the TPU-
Strategy API. If your model is small, make sure to leverage step fusing (via
the compile(…, steps_per_execution=N) argument) in order to fully utilize
the TPU cores.
Conclusions
You’ve almost reached the end of this book. This last chapter will summarize and
review core concepts while also expanding your horizons beyond what you’ve
learned so far. Becoming an effective AI practitioner is a journey, and finishing this
book is merely your first step on it. I want to make sure you realize this and are
properly equipped to take the next steps of this journey on your own.
We’ll start with a bird’s-eye view of what you should take away from this book.
This should refresh your memory regarding some of the concepts you’ve learned.
Next, I’ll present an overview of some key limitations of deep learning. To use a
tool appropriately, you should not only understand what it can do but also be
aware of what it can’t do. Finally, I’ll offer some speculative thoughts about the
future evolution of deep learning, machine learning, and AI. This should be
431
432 CHAPTER 14 Conclusions
especially interesting to you if you’d like to get into fundamental research. The chap-
ter ends with a short list of resources and strategies for further learning about
machine learning and staying up to date with new advances.
sometimes said that deep learning has “solved perception”—although that’s true only
for a fairly narrow definition of perception.
Due to its unprecedented technical successes, deep learning has singlehandedly
brought about the third and by far the largest AI summer: a period of intense interest,
investment, and hype in the field of AI. As this book is being written, we’re in the mid-
dle of it. Whether this period will end in the near future, and what happens after it
ends, are topics of debate. One thing is certain: in stark contrast with previous AI sum-
mers, deep learning has provided enormous business value to both large and small
technology companies, enabling human-level speech recognition, smart assistants,
human-level image classification, vastly improved machine translation, and more. The
hype may (and likely will) recede, but the sustained economic and technological
impact of deep learning will remain. In that sense, deep learning could be analogous
to the internet: it may be overly hyped up for a few years, but in the longer term it will
still be a major revolution that will transform our economy and our lives.
I’m particularly optimistic about deep learning, because even if we were to make
no further technological progress in the next decade, deploying existing algorithms
to every applicable problem would be a game changer for most industries. Deep
learning is nothing short of a revolution, and progress is currently happening at an
incredibly fast rate, due to an exponential investment in resources and headcount.
From where I stand, the future looks bright, although short-term expectations are
somewhat overoptimistic; deploying deep learning to the full extent of its potential
will likely take multiple decades.
1
Richard Feynman, interview, “The World from Another Point of View,” Yorkshire Television, 1972.
434 CHAPTER 14 Conclusions
for us to be able to learn its parameters via gradient descent. Intuitively, this means
the geometric morphing from inputs to outputs must be smooth and continuous—a
significant constraint.
The entire process of applying this complex geometric transformation to the
input data can be visualized in 3D by imagining a person trying to uncrumple a
paper ball: the crumpled paper ball is the manifold of the input data that the model
starts with. Each movement operated by the person on the paper ball is similar to a
simple geometric transformation operated by one layer. The full uncrumpling ges-
ture sequence is the complex transformation of the entire model. Deep learning
models are mathematical machines for uncrumpling complicated manifolds of high-
dimensional data.
That’s the magic of deep learning: turning meaning into vectors, then into geo-
metric spaces, and then incrementally learning complex geometric transformations
that map one space to another. All you need are spaces of sufficiently high dimension-
ality in order to capture the full scope of the relationships found in the original data.
The whole process hinges on a single core idea: that meaning is derived from the pair-
wise relationship between things (between words in a language, between pixels in an image,
and so on) and that these relationships can be captured by a distance function. But note that
whether the brain also implements meaning via geometric spaces is an entirely separate
question. Vector spaces are efficient to work with from a computational standpoint, but
different data structures for intelligence can easily be envisioned—in particular, graphs.
Neural networks initially emerged from the idea of using graphs as a way to encode
meaning, which is why they’re named neural networks; the surrounding field of
research used to be called connectionism. Nowadays the name “neural network” exists
purely for historical reasons—it’s an extremely misleading name because they’re nei-
ther neural nor networks. In particular, neural networks have hardly anything to do
with the brain. A more appropriate name would have been layered representations learn-
ing or hierarchical representations learning, or maybe even deep differentiable models or
chained geometric transforms, to emphasize the fact that continuous geometric space
manipulation is at their core.
are all we need. This is, in turn, a byproduct of the rise of the consumer inter-
net and Moore’s law applied to storage media.
The availability of fast, highly parallel computation hardware at a low price—Especially
the GPUs produced by NVIDIA—first gaming GPUs and then chips designed
from the ground up for deep learning. Early on, NVIDIA CEO Jensen Huang
took note of the deep learning boom and decided to bet the company’s future
on it, which paid off in a big way.
A complex stack of software layers that makes this computational power available to
humans —The CUDA language, frameworks like TensorFlow that do auto-
matic differentiation, and Keras, which makes deep learning accessible to
most people.
In the future, deep learning will not be used only by specialists—researchers, graduate
students, and engineers with an academic profile—it will be a tool in the toolbox of
every developer, much like web technology today. Everyone needs to build intelligent
apps: just as every business today needs a website, every product will need to intelli-
gently make sense of user-generated data. Bringing about this future will require us to
build tools that make deep learning radically easy to use and accessible to anyone with
basic coding abilities. Keras has been the first major step in that direction.
5 Develop a first model that beats a trivial common-sense baseline, thus demon-
strating that machine learning can work on your problem. This may not always
be the case!
6 Gradually refine your model architecture by tuning hyperparameters and add-
ing regularization. Make changes based on performance on the validation data
only, not the test data or the training data. Remember that you should get your
model to overfit (thus identifying a model capacity level that’s greater than you
need) and only then begin to add regularization or downsize your model.
Beware of validation-set overfitting when tuning hyperparameters—the fact
that your hyperparameters may end up being overspecialized to the validation
set. Avoiding this is the purpose of having a separate test set.
7 Deploy your final model in production—as a web API, as part of a JavaScript or
C++ application, on an embedded device, etc. Keep monitoring its perfor-
mance on real-world data, and use your findings to refine the next iteration of
the model!
To perform single-label categorical classification (where each sample has exactly one class,
no more), end your stack of layers with a Dense layer with a number of units equal to the
number of classes, and a softmax activation. If your targets are one-hot encoded, use
categorical_crossentropy as the loss; if they’re integers, use sparse_categorical_
crossentropy:
inputs = [Link](shape=(num_input_features,))
x = [Link](32, activation="relu")(inputs)
x = [Link](32, activation="relu")(x)
outputs = [Link](num_classes, activation="softmax")(x)
model = [Link](inputs, outputs)
[Link](optimizer="rmsprop", loss="categorical_crossentropy")
To perform multilabel categorical classification (where each sample can have several
classes), end your stack of layers with a Dense layer with a number of units equal to the
number of classes, and a sigmoid activation, and use binary_crossentropy as the
loss. Your targets should be multi-hot encoded:
inputs = [Link](shape=(num_input_features,))
x = [Link](32, activation="relu")(inputs)
x = [Link](32, activation="relu")(x)
outputs = [Link](num_classes, activation="sigmoid")(x)
model = [Link](inputs, outputs)
[Link](optimizer="rmsprop", loss="binary_crossentropy")
438 CHAPTER 14 Conclusions
To perform regression toward a vector of continuous values, end your stack of layers
with a Dense layer with a number of units equal to the number of values you’re trying
to predict (often a single one, such as the price of a house), and no activation. Various
losses can be used for regression—most commonly mean_squared_error (MSE):
inputs = [Link](shape=(num_input_features,))
x = [Link](32, activation="relu")(inputs)
x = [Link](32, activation="relu")(x)
outputs [Link](num_values)(x)
model = [Link](inputs, outputs)
[Link](optimizer="rmsprop", loss="mse")
CONVNETS
Convolution layers look at spatially local patterns by applying the same geometric
transformation to different spatial locations (patches) in an input tensor. This results
in representations that are translation invariant, making convolution layers highly data
efficient and modular. This idea is applicable to spaces of any dimensionality: 1D
(continuous sequences), 2D (images), 3D (volumes), and so on. You can use the
Conv1D layer to process sequences, the Conv2D layer to process images, and the Conv3D
layers to process volumes. As a leaner, more efficient alternative to convolution layers,
you can also use depthwise separable convolution layers, such as SeparableConv2D.
Convnets, or convolutional networks, consist of stacks of convolution and max-pooling
layers. The pooling layers let you spatially downsample the data, which is required to
keep feature maps to a reasonable size as the number of features grows, and to allow sub-
sequent convolution layers to “see” a greater spatial extent of the inputs. Convnets are
often ended with either a Flatten operation or a global pooling layer, turning spatial fea-
ture maps into vectors, followed by Dense layers to achieve classification or regression.
Here’s a typical image-classification network (categorical classification, in this
case), leveraging SeparableConv2D layers:
When building a very deep convnet, it’s common to add batch normalization layers as
well as residual connections—two architecture patterns that help gradient information
flow smoothly through the network.
Key concepts in review 439
RNNS
Recurrent neural networks (RNNs) work by processing sequences of inputs one timestep
at a time, and maintaining a state throughout (a state is typically a vector or set of vec-
tors). They should be used preferentially over 1D convnets in the case of sequences
where patterns of interest aren’t invariant by temporal translation (for instance,
timeseries data where the recent past is more important than the distant past).
Three RNN layers are available in Keras: SimpleRNN, GRU, and LSTM. For most practi-
cal purposes, you should use either GRU or LSTM. LSTM is the more powerful of the two
but is also more expensive; you can think of GRU as a simpler, cheaper alternative to it.
In order to stack multiple RNN layers on top of each other, each layer prior to the
last layer in the stack should return the full sequence of its outputs (each input time-
step will correspond to an output timestep). If you aren’t stacking any further RNN
layers, it’s common to return only the last output, which contains information about
the entire sequence.
Following is a single RNN layer for binary classification of vector sequences:
TRANSFORMERS
A Transformer looks at a set of vectors (such as word vectors), and leverages neural
attention to transform each vector into a representation that is aware of the context pro-
vided by the other vectors in the set. When the set in question is an ordered sequence,
you can also leverage positional encoding to create Transformers that can take into
account both global context and word order, capable of processing long text para-
graphs much more effectively than RNNs or 1D convnets.
Transformers can be used for any set-processing or sequence-processing task,
including text classification, but they excel especially at sequence-to-sequence learning,
such as translating paragraphs in a source language into a target language.
A sequence-to-sequence Transformer is made up of two parts:
A TransformerEncoder that turns an input vector sequence into a context-
aware, order-aware output vector sequence
440 CHAPTER 14 Conclusions
f(x) f(x)
Panda Gibbon!
Gibbon
class gradient
original model in our minds—the one developed from our experience as embodied
agents. It’s like a dim image in a mirror (see figure 14.3). The models you create will
take any shortcut available to fit their training data. For instance, image models tend
to rely more on local textures than on a global understanding of the input images—a
model trained on a dataset that features both leopards and sofas is likely to classify a
leopard-pattern sofa as an actual leopard.
Labeled data
Embodied Abstract concepts exemplifying Machine learning
Real world human experience in human mind these concepts model
f(x)
Figure 14.3 Current machine learning models: like a dim image in a mirror
The limitations of deep learning 445
As a machine learning practitioner, always be mindful of this, and never fall into the
trap of believing that neural networks understand the tasks they perform—they don’t,
at least not in a way that would make sense to us. They were trained on a different, far
narrower task than the one we wanted to teach them: that of mapping training inputs
to training targets, point by point. Show them anything that deviates from their train-
ing data, and they will break in absurd ways.
Figure 14.4 The brain network of the C. elegans worm: a behavioral automaton “programmed”
by natural evolution. Figure created by Emma Towlson (from Yan et al., “Network control principles
predict neuron function in the Caenorhabditis elegans connectome,” Nature, Oct. 2017).
Even though such machines might do some things as well as we do them, or perhaps even
better, they would inevitably fail in others, which would reveal they were acting not
through understanding, but only from the disposition of their organs.
actually understands the material? You give them a brand new problem. An automa-
ton is static, crafted to accomplish specific things in a specific context—“if this, then
that”—while an intelligent agent can adapt on the fly to novel, unexpected situations.
When an automaton is exposed to something that doesn’t match what it is “pro-
grammed” to do (whether we’re talking about human-written programs, evolution-
generated programs, or the implicit programming process of fitting a model on a
training data set), it will fail. Meanwhile, intelligent agents, like humans, will use their
understanding to find a way forward.
Humans are capable of far more than mapping immediate stimuli to immediate
responses, as a deep net, or an insect, would. We maintain complex, abstract models
of our current situation, of ourselves, and of other people, and we can use these mod-
els to anticipate different possible futures and perform long-term planning. You can
merge together known concepts to represent something you’ve never experienced
before—like imagining what you’d do if you won the lottery, or picturing how your
friend would react if you discreetly replaced her keys with exact copies made of elastic
rubber. This ability to handle novelty and what-ifs, to expand our mental model space
far beyond what we can experience directly—to leverage abstraction and reasoning —
is the defining characteristic of human cognition. I call it extreme generalization: an
ability to adapt to novel, never-before-experienced situations using little data or
even no new data at all. This capability is key to the intelligence displayed by
humans and advanced animals.
This stands in sharp contrast with what automaton-like systems do. A very rigid
automaton wouldn’t feature any generalization at all—it would be incapable of han-
dling anything that it wasn’t precisely told about in advance. A Python dict or a basic
question-answering program implemented as hardcoded if-then-else statements
would fall into this category. Deep nets do slightly better: they can successfully process
inputs that deviate a bit from what they’re familiar with—which is precisely what
makes them useful. Our cats vs. dogs model from chapter 8 could classify cat or dog
pictures it had not seen before, as long as they were close enough to what it was
trained on. However, deep nets are limited to what I call local generalization (see fig-
ure 14.5): the mapping from inputs to outputs performed by a deep net quickly
stops making sense as inputs start deviating from what the net saw at training time.
Deep nets can only generalize to known unknowns —to factors of variation that were
anticipated during model development and that are extensively featured in the
training data, such as different camera angles or lighting conditions for pet pic-
tures. That’s because deep nets generalize via interpolation on a manifold (remem-
ber chapter 5): any factor of variation in their input space needs to be captured by
the manifold they learn. That’s why basic data augmentation is so helpful in improv-
ing deep net generalization. Unlike humans, these models have no ability to impro-
vise in the face of situations for which little or no data is available (like winning the
lottery or being handed rubber keys) that only share abstract commonalities with
past situations.
448 CHAPTER 14 Conclusions
Situations space
Lower-intelligence system:
lower information
Known conversion ratio
situations Operational
area
Higher-intelligence system:
higher information
conversion ratio
Consider, for instance, the problem of learning the appropriate launch parameters to
get a rocket to land on the moon. If you used a deep net for this task and trained it
using supervised learning or reinforcement learning, you’d have to feed it tens of
thousands or even millions of launch trials: you’d need to expose it to a dense sampling
of the input space, in order for it to learn a reliable mapping from input space to out-
put space. In contrast, as humans, we can use our power of abstraction to come up
with physical models—rocket science—and derive an exact solution that will land the
rocket on the moon in one or a few trials. Similarly, if you developed a deep net con-
trolling a human body, and you wanted it to learn to safely navigate a city without
getting hit by cars, the net would have to die many thousands of times in various situa-
tions until it could infer that cars are dangerous and develop appropriate avoidance
behaviors. Dropped into a new city, the net would have to relearn most of what it
knows. On the other hand, humans are able to learn safe behaviors without having to
die even once—again, thanks to our power of abstract modeling of novel situations.
more unpredictable. A day in your life, if you look closely, is unlike any day you’ve
ever experienced, and unlike any day ever experienced by any of your evolutionary
ancestors. You need to be able to face unknown and surprising situations—constantly.
There is no way for evolution to find and hardcode as DNA the sequence of behaviors
you’ve been executing to successfully navigate your day since you woke up a few hours
ago. It has to be generated on the fly—every day.
The brain, as a good behavior-generation engine, simply adapted to fit this need. It
optimized for adaptability and generality, rather than merely optimizing for fitness to
a fixed set of situations. This shift likely occurred multiple times throughout evolu-
tionary history, resulting in highly intelligent animals in very distant evolutionary
branches—apes, octopuses, ravens, and more. Intelligence is an answer to challenges
presented by complex, dynamic ecosystems.
That’s the nature of intelligence: it is the ability to efficiently leverage the informa-
tion at your disposal in order to produce successful behavior in the face of an uncer-
tain, ever-changing future. What Descartes calls “understanding” is the key to this
remarkable capability: the power to mine your past experience to develop modular,
reusable abstractions that can be quickly repurposed to handle novel situations and
achieve extreme generalization.
2
Terry Winograd, “Procedures as a Representation for Data in a Computer Program for Understanding Natural
Language” (1971).
450 CHAPTER 14 Conclusions
to deal with unknown unknowns within a single broad domain of tasks (including situa-
tions the system was not trained to handle and that its creators could not have antici-
pated). For instance, a self-driving car capable of safely dealing with any situation you
throw at it, or a domestic robot that could pass the “Woz test of intelligence”—enter-
ing a random kitchen and making a cup of coffee.3 By combining deep learning and
painstakingly handcrafted abstract models of the world, we’re already making visible
progress toward these goals.
However, for the time being, AI remains limited to cognitive automation: the “intel-
ligence” label in “Artificial Intelligence” is a category error. It would be more accurate to
call our field “Artificial Cognition,” with “Cognitive Automation” and “Artificial Intelli-
gence” being two nearly independent subfields within it. In this subdivision, “Artifi-
cial Intelligence” would be a greenfield where almost everything remains to be
discovered.
Now, I don’t mean to diminish the achievements of deep learning. Cognitive auto-
mation is incredibly useful, and the way deep learning models are capable of automat-
ing tasks from exposure to data alone represents an especially powerful form of
cognitive automation, far more practical and versatile than explicit programming.
Doing this well is a game-changer for essentially every industry. But it’s still a long way
from human (or animal) intelligence. Our models, so far, can only perform local gen-
eralization: they map space X to space Y via a smooth geometric transform learned
from a dense sampling of X-to-Y data points, and any disruption within spaces X or Y
invalidates this mapping. They can only generalize to new situations that stay similar
to past data, whereas human cognition is capable of extreme generalization, quickly
adapting to radically novel situations and planning for long-term future situations.
14.3.1 On the importance of setting the right objective: The shortcut rule
Biological intelligence was the answer to a question asked by nature. Likewise, if we want
to develop true artificial intelligence, first, we need to be asking the right questions.
An effect you see constantly in systems design is the shortcut rule: if you focus on
optimizing one success metric, you will achieve your goal, but at the expense of every-
thing in the system that wasn’t covered by your success metric. You end up taking
every available shortcut toward the goal. Your creations are shaped by the incentives
you give yourself.
3
Fast Company, “Wozniak: Could a Computer Make a Cup of Coffee?” (March 2010), [Link]
Setting the course toward greater generality in AI 451
You see this often in machine learning competitions. In 2009, Netflix ran a challenge
that promised a $1 million prize to the team that achieved the highest score on a movie
recommendation task. It ended up never using the system created by the winning team,
because it was way too complex and compute-intensive. The winners had optimized for
prediction accuracy alone—what they were incentivized to achieve—at the expense of
every other desirable characteristic of the system: inference cost, maintainability, and
explainability. The shortcut rule holds true in most Kaggle competitions as well—the
models produced by Kaggle winners can rarely, if ever, be used in production.
The shortcut rule has been everywhere in AI over the past few decades. In the
1970s, psychologist and computer science pioneer Allen Newell, concerned that his
field wasn’t making any meaningful progress toward a proper theory of cognition,
proposed a new grand goal for AI: chess-playing. The rationale was that playing chess,
in humans, seemed to involve—perhaps even require—capabilities such as percep-
tion, reasoning and analysis, memory, study from books, and so on. Surely, if we could
build a chess-playing machine, it would have to feature these attributes as well. Right?
Over two decades later, the dream came true: in 1997, IBM’s Deep Blue beat Gary
Kasparov, the best chess player in the world. Researchers had then to contend with the
fact that creating a chess-champion AI had taught them little about human intelli-
gence. The Alpha–Beta algorithm at the heart of Deep Blue wasn’t a model of the
human brain and couldn’t generalize to tasks other than similar board games. It
turned out it was easier to build an AI that could only play chess than to build an arti-
ficial mind—so that’s the shortcut researchers took.
So far, the driving success metric of the field of AI has been to solve specific
tasks, from chess to Go, from MNIST classification to ImageNet, from Atari Arcade
games to StarCraft and DotA 2. Consequently, the history of the field has been
defined by a series of “successes” where we figured out how to solve these tasks with-
out featuring any intelligence.
If that sounds like a surprising statement, keep in mind that human-like intelli-
gence isn’t characterized by skill at any particular task—rather, it is the ability to adapt
to novelty, to efficiently acquire new skills and master never-seen-before tasks. By fix-
ing the task, you make it possible to provide an arbitrarily precise description of what
needs to be done—either via hardcoding human-provided knowledge or by supplying
humongous amounts of data. You make it possible for engineers to “buy” more skill
for their AI by just adding data or adding hardcoded knowledge, without increasing
the generalization power of the AI (see figure 14.6). If you have near-infinite training
data, even a very crude algorithm like nearest-neighbor search can play video games
with superhuman skill. Likewise if you have a near-infinite amount of human-written
if-then-else statements. That is, until you make a small change to the rules of the game—
the kind a human could adapt to instantly—that will require the non-intelligent system
to be retrained or rebuilt from scratch.
In short, by fixing the task, you remove the need to handle uncertainty and novelty,
and since the nature of intelligence is the ability to handle uncertainty and novelty,
452 CHAPTER 14 Conclusions
Figure 14.6 A low-generalization system can achieve arbitrary skill at a fixed task given unlimited task-
specific information.
you’re effectively removing the need for intelligence. And because it’s always easier to
find a non-intelligent solution to a specific task than to solve the general problem of
intelligence, that’s the shortcut you will take 100% of the time. Humans can use their
general intelligence to acquire skills at any new task, but in reverse, there is no path
from a collection of task-specific skills to general intelligence.
In 2018 and 2019, I developed a benchmark dataset called the Abstraction and Rea-
soning Corpus (ARC) 4 that seeks to capture this definition of intelligence. ARC is meant
to be approachable by both machines and humans, and it looks very similar to human
IQ tests, such as Raven’s progressive matrices. At test time, you’ll see a series of “tasks.”
Each task is explained via three or four “examples” that take the form of an input grid
and a corresponding output grid (see figure 14.7). You’ll then be given a brand new
input grid, and you’ll have three tries to produce the correct output grid before mov-
ing on to the next task.
Figure 14.7 An ARC task: the nature of the task is demonstrated by a couple of input-output pair
examples. Provided with a new input, you must construct the corresponding output.
Compared to IQ tests, two things are unique about ARC. First, ARC seeks to measure
generalization power, by only testing you on tasks you’ve never seen before. That
means that ARC is a game you can’t practice for, at least in theory: the tasks you will get
tested on will have their own unique logic that you will have to understand on the fly.
You can’t just memorize specific strategies from past tasks.
In addition, ARC tries to control for the prior knowledge that you bring to the test.
You never approach a new problem entirely from scratch—you bring to it preexisting
skills and information. ARC makes the assumption that all test takers should start
from the set of knowledge priors, called “Core Knowledge priors,” that represent the
“knowledge systems” that humans are born with. Unlike an IQ test, ARC tasks will
never involve acquired knowledge, like English sentences, for instance.
Unsurprisingly, deep-learning-based methods (including models trained on
extremely large amounts of external data, like GPT-3) have proven entirely unable to
solve ARC tasks, because these tasks are non-interpolative, and thus are a poor fit for
curve-fitting. Meanwhile, average humans have no issue solving these tasks on the first
try, without any practice. When you see a situation like this, where humans as young as
4
François Chollet, “On the Measure of Intelligence” (2019), [Link]
454 CHAPTER 14 Conclusions
five are able to naturally perform something that seems to be completely out of reach
for modern AI technology, that’s a clear signal that something interesting is going
on—that we’re missing something.
What would it take to solve ARC? Hopefully, this challenge will get you thinking.
That’s the entire point of ARC: to give you a goal of a different kind that will nudge
you in a new direction—hopefully a productive direction. Now, let’s take a quick look
at the key ingredients you’re going to need if you want to answer the call.
Figure 14.8 A kaleidoscope produces rich (yet repetitive) patterns from just a few
beads of colored glass.
you tell that two instances are related, and how you merge instances into abstractions.
Let’s take a close look at each type.
VALUE-CENTRIC ANALOGY
Let’s say you come across a number of different beetles in your backyard, belonging to
multiple species. You’ll notice similarities between them. Some will be more similar to
one another, and some will be less similar: the notion of similarity is implicitly a
smooth, continuous distance function that defines a latent manifold where your
instances live. Once you’ve seen enough beetles, you can start clustering more similar
instances together and merging them into a set of prototypes that captures the shared
visual features of each cluster (see figure 14.9). This prototype is abstract: it doesn’t
look like any specific instance you’ve seen, though it encodes properties that are com-
mon across all of them. When you encounter a new beetle, you won’t need to compare
it to every single beetle you’ve seen before in order to know what to do with it. You can
simply compare it to your handful of prototypes, so as to find the closest prototype—the
beetle’s category—and use it to make useful predictions: is the beetle likely to bite you?
Will it eat your apples?
Figure 14.9 Value-centric analogy relates instances via a continuous notion of similarity to obtain
abstract prototypes.
Does this sound familiar? It’s pretty much a description of what unsupervised machine
learning (such as the K-means clustering algorithm) does. In general, all of modern
machine learning, unsupervised or not, works by learning latent manifolds that describe
a space of instances encoded via prototypes. (Remember the convnet features you visu-
alized in chapter 9? They were visual prototypes.) Value-centric analogy is the kind of
analogy-making that enables deep learning models to perform local generalization.
It’s also what many of your own cognitive abilities run on. As a human, you per-
form value-centric analogies all the time. It’s the type of abstraction that underlies
pattern recognition, perception, and intuition. If you can do a task without thinking about it,
Implementing intelligence: The missing ingredients 457
you’re relying heavily on value-centric analogies. If you’re watching a movie and you
start subconsciously categorizing the different characters into “types,” that’s value-
centric abstraction.
PROGRAM-CENTRIC ANALOGY
Crucially, there’s more to cognition than the kind of immediate, approximative,
intuitive categorization that value-centric analogy enables. There’s another type of
abstraction-generation mechanism that’s slower, exact, deliberate: program-centric
(or structure-centric) analogy.
In software engineering, you often write different functions or classes that seem to
have a lot in common. When you notice these redundancies, you start asking, “could
there be a more abstract function that performs the same job, that could be reused
twice? Could there be an abstract base class that both of my classes could inherit from?”
The definition of abstraction you’re using here corresponds to program-centric anal-
ogy. You’re not trying to compare your classes and functions by how similar they look,
the way you’d compare two human faces, via an implicit distance function. Rather,
you’re interested in whether there are parts of them that have exactly the same structure.
You’re looking for what is called a subgraph isomorphism (see figure 14.10): programs
can be represented as graphs of operators, and you’re trying to find subgraphs (pro-
gram subsets) that are exactly shared across your different programs.
Instance Instance
Instance Instance ls = obj.as_list() my_list = get_data()
ls_sum = 0 total = 0
ls_entries = 0 num_elems = 0
for n in ls: for e in my_list:
if n is not None: if e is not None:
ls_sum += n total += e
ls_entries += 1 num_elems += 1
avg = ls sum / ls entries mean = total / num_elems
print('avg:', avg) update_mean(mean)
Figure 14.10 Program-centric analogy identifies and isolates isomorphic substructures across different
instances
This kind of analogy-making via exact structural match within different discrete struc-
tures isn’t at all exclusive to specialized fields like computer science or mathematics—
you’re constantly using it without noticing. It underlies reasoning, planning, and the
458 CHAPTER 14 Conclusions
general concept of rigor (as opposed to intuition). Any time you’re thinking about
objects connected to each other by a discrete network of relationships (rather than a
continuous similarity function), you’re leveraging program-centric analogies.
COGNITION AS A COMBINATION OF BOTH KINDS OF ABSTRACTION
Let’s compare these two poles of abstraction side by side (see table 14.1).
doing linear algebra on a computer, you’re working with continuous spaces, entirely
via discrete programs that operate on ones and zeros.
However, there are clearly types of problems that are better suited to one or the
other. Try to train a deep learning model to sort a list of five numbers, for instance.
With the right architecture, it’s not impossible, but it’s an exercise in frustration.
You’ll need a massive amount of training data to make it happen—and even then, the
model will still make occasional mistakes when presented with new numbers. And if
you want to start sorting lists of 10 numbers instead, you’ll need to completely retrain
the model on even more data. Meanwhile, writing a sorting algorithm in Python takes
just a few lines—and the resulting program, once validated on a couple more exam-
ples, will work every time on lists of any size. That’s pretty strong generalization: going
from a couple of demonstration examples and test examples to a program that can
successfully process literally any list of numbers.
In reverse, perception problems are a terrible fit for discrete reasoning processes.
Try to write a pure-Python program to classify MNIST digits without using any
machine learning technique: you’re in for a ride. You’ll find yourself painstakingly
coding functions that can detect the number of closed loops in a digit, the coordi-
nates of the center of mass of a digit, and so on. After thousands of lines of code, you
might achieve . . . 90% test accuracy. In this case, fitting a parametric model is much
simpler; it can better utilize the large amount of data that’s available, and it achieves
much more robust results. If you have lots of data and you’re faced with a problem
where the manifold hypothesis applies, go with deep learning.
For this reason, it’s unlikely that we’ll see the rise of an approach that would
reduce reasoning problems to manifold interpolation, or that would reduce percep-
tion problems to discrete reasoning. The way forward in AI is to develop a unified
framework that incorporates both types of abstract analogy-making. Let’s examine
what that might look like.
Greater, systematic reuse of previously learned features and architectures, such as meta-
learning systems using reusable and modular program subroutines.
Additionally, note that these considerations aren’t specific to the sort of supervised
learning that has been the bread and butter of deep learning so far—rather, they’re
applicable to any form of machine learning, including unsupervised, self-supervised,
and reinforcement learning. It isn’t fundamentally important where your labels come
from or what your training loop looks like; these different branches of machine learn-
ing are different facets of the same construct. Let’s dive in.
Input: [3, 5, 1, 2, 7]
Program Output: [1, 2, 3, 5, 7]
specification Input: [8, 5, 2, 9, 1, 13]
Output: [1, 2, 5, 8, 9, 13]
Vocabulary of
building blocks
+ = if ==
for - else Search proces s
* += -= *=
Requires a lot of data to produce reliable results Data-efficient, can work with a couple of training
examples
What will this look like? Consider a well-known type of network: RNNs. It’s import-
ant to note that RNNs have slightly fewer limitations than feedforward networks.
That’s because RNNs are a bit more than mere geometric transformations: they’re
geometric transformations repeatedly applied inside a for loop. The temporal for loop is
itself hardcoded by human developers: it’s a built-in assumption of the network. Nat-
urally, RNNs are still extremely limited in what they can represent, primarily
because each step they perform is a differentiable geometric transformation, and
they carry information from step to step via points in a continuous geometric space
(state vectors). Now imagine a neural network that’s augmented in a similar way
with programming primitives, but instead of a single hardcoded for loop with hard-
coded continuous-space memory, the network includes a large set of programming
primitives that the model is free to manipulate to expand its processing function, such
as if branches, while statements, variable creation, disk storage for long-term mem-
ory, sorting operators, advanced data structures (such as lists, graphs, and hash
tables), and many more. The space of programs that such a network could represent
would be far broader than what can be represented with current deep learning mod-
els, and some of these programs could achieve superior generalization power. Impor-
tantly, such programs will not be differentiable end-to-end, though specific modules
will remain differentiable and thus will need to be generated via a combination of dis-
crete program search and gradient descent.
We’ll move away from having, on one hand, hardcoded algorithmic intelligence
(handcrafted software) and, on the other hand, learned geometric intelligence (deep
learning). Instead, we’ll have a blend of formal algorithmic modules that provide rea-
soning and abstraction capabilities, and geometric modules that provide informal
intuition and pattern-recognition capabilities (see figure 14.12). The entire system
will be learned with little or no human involvement. This should dramatically expand
the scope of problems that can be solved with machine learning—the space of pro-
grams that we can generate automatically, given appropriate training data. Systems
like AlphaGo—or even RNNs—can be seen as a prehistoric ancestor of such hybrid
algorithmic-geometric models.
that’s better at each task. For instance, training the same neural machine-translation
model to perform both English-to-German translation and French-to-Italian transla-
tion will result in a model that’s better at each language pair. Similarly, training an
image-classification model jointly with an image-segmentation model, sharing the
same convolutional base, results in a model that’s better at both tasks. This is fairly
intuitive: there’s always some information overlap between seemingly disconnected
tasks, and a joint model has access to a greater amount of information about each
individual task than a model trained on that specific task only.
Currently, when it comes to model reuse across tasks, we use pretrained weights for
models that perform common functions, such as visual feature extraction. You saw
this in action in chapter 9. In the future, I expect a generalized version of this to be
commonplace: we’ll use not only previously learned features (submodel weights) but
also model architectures and training procedures. As models become more like pro-
grams, we’ll begin to reuse program subroutines like the functions and classes found in
human programming languages.
Think of the process of software development today: once an engineer solves a spe-
cific problem (HTTP queries in Python, for instance), they package it as an abstract,
reusable library. Engineers who face a similar problem in the future will be able to
search for existing libraries, download one, and use it in their own project. In a similar
way, in the future, meta-learning systems will be able to assemble new programs by sift-
ing through a global library of high-level reusable blocks. When the system finds itself
developing similar program subroutines for several different tasks, it can come up
with an abstract, reusable version of the subroutine and store it in the global library
(see figure 14.13). These subroutines can be either geometric (deep learning modules
Global library of
abstract subroutines Push
reusable
subroutines Perpetual meta-learner
Geometric Algorithmic Algorithmic
subroutine subroutine subroutine
capable of quickly growing
a task-level model
Geometric Algorithmic Algorithmic across a variety of tasks
Fetch Task #002453
subroutine subroutine subroutine
relevant
Geometric Algorithmic Algorithmic subroutines
subroutine subroutine subroutine Design Data and Task #002454
choices feedback
Task #002455
Modular task-level program
learned on the fly to solve
a specific task Data and
feedback
Geometric Algorithmic Task #002456
subroutine subroutine
Actions
Geometric Algorithmic
subroutine subroutine
Figure 14.13 A meta-learner capable of quickly developing task-specific models using reusable primitives
(both algorithmic and geometric), thus achieving extreme generalization
The future of deep learning 465
the noise. But some tools can help: in particular, you can use Google Scholar
([Link] to keep track of publications by your favorite authors.
Final words
This is the end of Deep Learning with Python, second edition. I hope you’ve learned a
thing or two about machine learning, deep learning, Keras, and maybe even cognition
in general. Learning is a lifelong journey, especially in the field of AI, where we have far
more unknowns on our hands than certitudes. So please go on learning, question-
ing, and researching. Never stop! Because even given the progress made so far, most
of the fundamental questions in AI remain unanswered. Many haven’t even been
properly asked yet.
index
Symbols anomaly detection 281 BatchNormalization layer 194,
anthropomorphizing 256, 344
@[Link] decorator 198, 270 machine-learning batch training 82
models 443–445 BayesianOptimization
A ARC (Abstraction & Reasoning tuner 415
Corpus) dataset 453 bias vector 256
ablation studies 251 arrow of time 137 Bidirectional layer 306
abstraction, poles of 455–458 arXiv 466–467 bidirectional recurrent
cognition as combination of assign() method of Variable layers 300
both kinds of class 77 bidirectional RNNs 304–307
abstraction 458 automated hyperparameter tun- bigrams
program-centric analogy ing software 165 with binary encoding
457–458 automatic differentiation 60 324–325
value-centric analogy 456–457 automatic shape inference with TF-IDF encoding
activation function 63 85–87 325–327
adapt() method of preprocess- automatons 445 binary classification 97–106,
ing layers 317 AutoML (automated machine 437
add() method of Sequential learning) 419–420 building model 99–102
class 174 average presence 211 generating predictions on
adversarial examples 443 new data 105
affine transform 46 B IMDB dataset 97
AGI (artificial general preparing data 98–99
intelligence) 465 Backpropagation algorithm 10, validating approach 102–105
AI (artificial intelligence) 1, 432 56 binary_crossentropy 102, 437
greater generality in 450–454 backward pass 57 binary encoding
new target for 452–454 bag-of-2-grams/bag-of-3- bigrams with 324–325
shortcut rule 450–452 grams 314 single words (unigrams)
overview of 2–3 bag-of-words models 313 with 322–324
promise of 12–13 batch() method of Dataset BLAS (Basic Linear Algebra
various approaches to 432 class 218 Subprograms) 39
See also deep learning batch axis 35 Boston Housing Price
AI summer 433 batch dimension 35 dataset 114
AI winter 12 batches 96 broadcasting 40–41
algorithmic modules 465 batch gradient descent 53 broad generalization 449
Analytical Engine 3 batch normalization 255–257, build() method of Layer
annotations 96 438 class 84, 174, 415
469
470 INDEX
R
ecent innovations in deep learning unlock exciting new
software capabilities like automated language translation,
the math with practical
image recognition, and more. Deep learning is quickly Python code. He is also an
becoming essential knowledge for every software developer, experienced ML researcher
and modern tools like Keras and TensorFlow put it within and his insights on various
your reach—even if you have no background in mathematics model architectures or training
or data science. This book shows you how to get started.
Deep Learning with Python, Second Edition introduces the field
tips are a joy to read.
—Martin Görner, Google ”
of deep learning using Python and the powerful Keras library.
In this revised and expanded new edition, Keras creator
François Chollet offers insights for both novice and experi- “exciting
Immerse yourself into this
introduction to the
enced machine learning practitioners. As you move through topic with lots of real-world
this book, you’ll build your understanding through intuitive examples. A must read for every
”
explanations, crisp illustrations, and clear examples. You’ll deep learning practitioner.
quickly pick up the skills you need to start developing deep-
—Sayak Paul, Carted
learning applications.
What’s Inside
● Deep learning from first principles
“The modern classic just
got better.
”
—Edmon Begoli
● Image classification and image segmentation Oak Ridge National Laboratory
● Time series forecasting
● Text generation, neural style transfer, and image “ofTruly the bible
deep learning.
”
generation —Yiannis Paraskevopoulos
University of West Attica
For readers with intermediate Python skills. No previous
experience with Keras, TensorFlow, or machine learning is
required.
François Chollet is a software engineer at Google and creator of See first page
the Keras deep-learning library.
Register this print book to get free access to all ebook formats.
ISBN: 978-1-61729-686-4
Visit [Link]