What Artificial Intelligence Actually Is

Week 1 of 14 · Foundations · 7 days

Full curriculum
Week 01 · Foundations

What Artificial Intelligence Actually Is

Week 01 · Day 1 of 7

Three Things All Called AI

Rules, search and learning, and why the distinction is practical

By 663 words

Artificial intelligence is a label attached to a very wide range of things, and the first useful thing this course can do is take it apart. A thermostat is not intelligent. A chess program that beats you is doing something you would call thinking if a person did it. A system that writes an essay is doing something different again. All three have been called AI at some point.

Artificial intelligence: Getting a computer to do something that, done by a person, would be said to require intelligence. The definition moves as the technology does: optical character recognition was AI in 1970 and is a library call now. This is sometimes called the AI effect, and it means the word describes a frontier rather than a fixed set of methods.

Three systems, three completely different mechanisms

Rather than argue about definitions, it is more useful to see the three families of approach side by side, because they fail in different ways and suit different problems.

One: rules a person wrote down

Somebody works out the logic and encodes it. The system is exactly as good as the person who wrote it, it can explain every decision, and it knows nothing that was not written into it.

def triage(temperature, breathing_hard, age):
"""The kind of system that ran hospitals and helpdesks for
decades, and still runs a great deal of both."""

if temperature > 39.0 and breathing_hard:
return 'urgent'
if temperature > 38.0 and age > 65:
return 'urgent'
if temperature > 38.0:
return 'same day'
return 'routine'

cases = [(39.4, True, 30), (38.2, False, 70), (38.2, False, 30),
(37.1, False, 45)]
for temp, breathing, age in cases:
print('%.1f breathing hard %-5s age %2d -> %s'
% (temp, breathing, age, triage(temp, breathing, age)))
39.4 breathing hard True age 30 -> urgent
38.2 breathing hard False age 70 -> urgent
38.2 breathing hard False age 30 -> same day
37.1 breathing hard False age 45 -> routine

Every decision there is traceable to a line you can point at. That is a real advantage and it is why rules still run anything with a regulator attached. The limit is equally real: nobody can write down the rules for recognising a cat in a photograph.

Two: searching through possibilities

The computer is not told the answer, it is told the rules of the game and how to recognise a solution, and then it looks. This is how route planners and puzzle solvers work, and it is how chess programs beat people long before machine learning was involved.

from collections import deque

ROADS = {'depot': ['north', 'east'], 'north': ['ring', 'depot'],
'east': ['ring', 'south'], 'ring': ['airport'],
'south': ['airport'], 'airport': []}

def shortest(start, goal):
"""Breadth first search. No learning, no data, no training."""
queue = deque([[start]])
seen = {start}
while queue:
path = queue.popleft()
if path[-1] == goal:
return path
for nxt in ROADS[path[-1]]:
if nxt not in seen:
seen.add(nxt)
queue.append(path + [nxt])
return None

print(' -> '.join(shortest('depot', 'airport')))
print('the program was never shown a single example route')
depot -> north -> ring -> airport
the program was never shown a single example route

Nobody gave that program any examples. It was given the map and a way to tell when it had arrived, and it worked the rest out. When a problem genuinely has that shape, search is often better than learning: it is exact, it needs no training data, and it can prove there is no shorter route.

Three: learning the rules from examples

Nobody writes the logic. You supply examples of the answer and the system works out for itself what distinguishes them. This is machine learning, it is what almost everyone means by AI today, and days 2 to 7 are about it.

Why the distinction matters on day 1

These three are not competitors on one scale of sophistication. They suit different problems. Reaching for machine learning on a problem where somebody could simply write the rules down is one of the most common and most expensive mistakes in this field, and week 1 ends with a checklist for telling them apart.
Week 01 · Day 2 of 7

Rules Against Examples

The same inbox solved both ways, and measured

By 3157 words

The clearest way to feel the difference between writing rules and learning them is to do both on the same problem and compare. The problem is a support inbox of short messages, half of them unwanted.

import random

random.seed(11)

# Ten phrasings to learn from and four the model never sees, per class.
SPAM_TRAIN = [
'congratulations you have won a {prize}, claim it {when}',
'your {thing} will be closed, verify it {when}',
'earn {amount} a week from home, no experience needed',
'exclusive offer, {pct} off everything {when}',
'we noticed unusual activity on your {thing}, confirm it {when}',
'final notice about your {thing}, respond {when}',
'you have been selected for a {prize}, reply to arrange it',
'limited deal, {pct} discount if you order {when}',
'your {thing} is on hold, update your details {when}',
'claim your {prize} before it expires {when}',
]
SPAM_TEST = [
'act {when} to release the {amount} waiting on your {thing}',
'we tried to deliver your {prize}, reply with your details',
'a {pct} refund is pending, tell us where to send it',
'security alert on your {thing}, reply {when} or lose access',
]
# Ordinary mail uses the same words. That is the point: 'account',
# 'offer' and 'urgent' are perfectly normal words at work.
HAM_TRAIN = [
'the invoice for {month} is attached, let me know if it looks wrong',
'can we move the {day} meeting, i have a clash',
'the report is finished, it is in the shared folder',
'thanks for the figures, they match the {thing} i have',
'i am out on {day}, send anything urgent before then',
'the training session is confirmed for {month}',
'could you check the {thing} settings when you get a moment',
'the client asked about dates, i said {month}',
'we should discuss the {pct} increase before {day}',
'their offer came in {when}, i think we should accept',
]
HAM_TEST = [
'payroll needs your {thing} details before {month}',
'the supplier quoted {amount}, which is over budget',
'reminder that the {day} review starts at nine',
'i have won the argument about the {month} deadline',
]
FILL = {
'prize': ['free holiday', 'new phone', 'cash prize', 'gift card'],
'when': ['today', 'within 24 hours', 'before friday', 'now'],
'thing': ['account', 'password', 'payment', 'subscription'],
'amount': ['500 pounds', '2000 pounds', '1000 pounds'],
'pct': ['90%', '75%', '80%'],
'month': ['january', 'march', 'october', 'june'],
'day': ['monday', 'tuesday', 'thursday', 'friday'],
}
# Both classes draw from this, so plenty of words carry no signal.
NOISE = ['please', 'thanks', 'regards', 'best', 'hi', 'hello', 'team',
'quick', 'note', 'fyi', 'cheers', 'morning']

def fill(template):
out = template
for key, options in FILL.items():
while '{' + key + '}' in out:
out = out.replace('{' + key + '}', random.choice(options), 1)
# a couple of ordinary words, so the two classes overlap
return ' '.join([random.choice(NOISE)] + out.split()
+ [random.choice(NOISE)])

def build(spam_templates, ham_templates, n):
xs, ys = [], []
for _ in range(n):
xs.append(fill(random.choice(spam_templates)))
ys.append(1)
xs.append(fill(random.choice(ham_templates)))
ys.append(0)
return xs, ys

train_x, train_y = build(SPAM_TRAIN, HAM_TRAIN, 120)
# held out mail is phrased in ways the training set never contained
test_x, test_y = build(SPAM_TEST, HAM_TEST, 60)
messages, labels = train_x + test_x, train_y + test_y
for message, label in list(zip(messages, labels))[:6]:
print('%-5s %s' % ('spam' if label else 'ok', message))
print()
print('%d messages, %d spam, %d for training and %d held back'
% (len(messages), sum(labels), len(train_x), len(test_x)))
spam note limited deal, 75% discount if you order now fyi
ok note thanks for the figures, they match the password i have quick
spam quick claim your new phone before it expires today hi
ok thanks the report is finished, it is in the shared folder note
spam cheers congratulations you have won a gift card, claim it now morning
ok fyi their offer came in within 24 hours, i think we should accept please

360 messages, 180 spam, 240 for training and 120 held back

How this set is held back matters as much as that it is

The obvious split is to shuffle the messages and keep a quarter back. Do that here and every model scores a perfect 1.000, because the held out messages are near duplicates of training ones and remembering is enough.

So the split is by phrasing instead. The training set uses ten templates a side and the held out set uses four completely different ones. That is the situation a real inbox presents: tomorrow's mail is not a reshuffle of today's, and the only interesting question is whether the system handles wording it has never seen.

The rules a sensible person would write

import random

random.seed(11)

# Ten phrasings to learn from and four the model never sees, per class.
SPAM_TRAIN = [
'congratulations you have won a {prize}, claim it {when}',
'your {thing} will be closed, verify it {when}',
'earn {amount} a week from home, no experience needed',
'exclusive offer, {pct} off everything {when}',
'we noticed unusual activity on your {thing}, confirm it {when}',
'final notice about your {thing}, respond {when}',
'you have been selected for a {prize}, reply to arrange it',
'limited deal, {pct} discount if you order {when}',
'your {thing} is on hold, update your details {when}',
'claim your {prize} before it expires {when}',
]
SPAM_TEST = [
'act {when} to release the {amount} waiting on your {thing}',
'we tried to deliver your {prize}, reply with your details',
'a {pct} refund is pending, tell us where to send it',
'security alert on your {thing}, reply {when} or lose access',
]
# Ordinary mail uses the same words. That is the point: 'account',
# 'offer' and 'urgent' are perfectly normal words at work.
HAM_TRAIN = [
'the invoice for {month} is attached, let me know if it looks wrong',
'can we move the {day} meeting, i have a clash',
'the report is finished, it is in the shared folder',
'thanks for the figures, they match the {thing} i have',
'i am out on {day}, send anything urgent before then',
'the training session is confirmed for {month}',
'could you check the {thing} settings when you get a moment',
'the client asked about dates, i said {month}',
'we should discuss the {pct} increase before {day}',
'their offer came in {when}, i think we should accept',
]
HAM_TEST = [
'payroll needs your {thing} details before {month}',
'the supplier quoted {amount}, which is over budget',
'reminder that the {day} review starts at nine',
'i have won the argument about the {month} deadline',
]
FILL = {
'prize': ['free holiday', 'new phone', 'cash prize', 'gift card'],
'when': ['today', 'within 24 hours', 'before friday', 'now'],
'thing': ['account', 'password', 'payment', 'subscription'],
'amount': ['500 pounds', '2000 pounds', '1000 pounds'],
'pct': ['90%', '75%', '80%'],
'month': ['january', 'march', 'october', 'june'],
'day': ['monday', 'tuesday', 'thursday', 'friday'],
}
# Both classes draw from this, so plenty of words carry no signal.
NOISE = ['please', 'thanks', 'regards', 'best', 'hi', 'hello', 'team',
'quick', 'note', 'fyi', 'cheers', 'morning']

def fill(template):
out = template
for key, options in FILL.items():
while '{' + key + '}' in out:
out = out.replace('{' + key + '}', random.choice(options), 1)
# a couple of ordinary words, so the two classes overlap
return ' '.join([random.choice(NOISE)] + out.split()
+ [random.choice(NOISE)])

def build(spam_templates, ham_templates, n):
xs, ys = [], []
for _ in range(n):
xs.append(fill(random.choice(spam_templates)))
ys.append(1)
xs.append(fill(random.choice(ham_templates)))
ys.append(0)
return xs, ys

train_x, train_y = build(SPAM_TRAIN, HAM_TRAIN, 120)
# held out mail is phrased in ways the training set never contained
test_x, test_y = build(SPAM_TEST, HAM_TEST, 60)
messages, labels = train_x + test_x, train_y + test_y
BANNED = ['won', 'free', 'claim', 'offer', 'discount', 'urgent',
'verify', 'exclusive', 'selected', 'prize']

def rule_predict(message):
"""Somebody sat down and wrote what spam looks like."""
words = message.lower().replace(',', '').split()
return 1 if any(word in BANNED for word in words) else 0

def score(predict, xs, ys):
right = sum(1 for x, y in zip(xs, ys) if predict(x) == y)
caught = sum(1 for x, y in zip(xs, ys) if y == 1 and predict(x) == 1)
flagged = sum(1 for x, y in zip(xs, ys) if y == 0 and predict(x) == 1)
spam = sum(ys)
return right / len(xs), caught / spam, flagged / (len(ys) - spam)
acc, caught, wrongly = score(rule_predict, test_x, test_y)
print('%-28s %8s %8s %8s' % ('', 'accuracy', 'caught', 'false'))
print('%-28s %8.3f %8.3f %8.3f' % ('keyword rules', acc, caught, wrongly))
print()
print('spam the rules let through:')
shown = 0
for message, label in zip(test_x, test_y):
if label == 1 and rule_predict(message) == 0 and shown < 4:
print(' ' + message)
shown += 1
accuracy caught false
keyword rules 0.492 0.200 0.217

spam the rules let through:
fyi a 75% refund is pending, tell us where to send it morning
best a 90% refund is pending, tell us where to send it team
thanks a 75% refund is pending, tell us where to send it note
regards act before friday to release the 1000 pounds waiting on your account thanks

Ten sensible keywords, and on mail phrased in ways they were not written for they catch a fifth of the spam and score 0.492 overall, which is worse than tossing a coin. Read the messages they let through: every one is obviously unwanted to a human and contains none of the ten words.

This is not a contrived defeat. It is the actual dynamic of the problem, because whoever is sending the spam can read the filter too, and the phrasings that survive are exactly the ones that avoid the list. Each miss is an argument for adding another keyword, and that argument never ends. The rules also block ordinary mail: one of the held out messages is somebody saying they won an argument about a deadline.

The same problem, learned

import random

random.seed(11)

# Ten phrasings to learn from and four the model never sees, per class.
SPAM_TRAIN = [
'congratulations you have won a {prize}, claim it {when}',
'your {thing} will be closed, verify it {when}',
'earn {amount} a week from home, no experience needed',
'exclusive offer, {pct} off everything {when}',
'we noticed unusual activity on your {thing}, confirm it {when}',
'final notice about your {thing}, respond {when}',
'you have been selected for a {prize}, reply to arrange it',
'limited deal, {pct} discount if you order {when}',
'your {thing} is on hold, update your details {when}',
'claim your {prize} before it expires {when}',
]
SPAM_TEST = [
'act {when} to release the {amount} waiting on your {thing}',
'we tried to deliver your {prize}, reply with your details',
'a {pct} refund is pending, tell us where to send it',
'security alert on your {thing}, reply {when} or lose access',
]
# Ordinary mail uses the same words. That is the point: 'account',
# 'offer' and 'urgent' are perfectly normal words at work.
HAM_TRAIN = [
'the invoice for {month} is attached, let me know if it looks wrong',
'can we move the {day} meeting, i have a clash',
'the report is finished, it is in the shared folder',
'thanks for the figures, they match the {thing} i have',
'i am out on {day}, send anything urgent before then',
'the training session is confirmed for {month}',
'could you check the {thing} settings when you get a moment',
'the client asked about dates, i said {month}',
'we should discuss the {pct} increase before {day}',
'their offer came in {when}, i think we should accept',
]
HAM_TEST = [
'payroll needs your {thing} details before {month}',
'the supplier quoted {amount}, which is over budget',
'reminder that the {day} review starts at nine',
'i have won the argument about the {month} deadline',
]
FILL = {
'prize': ['free holiday', 'new phone', 'cash prize', 'gift card'],
'when': ['today', 'within 24 hours', 'before friday', 'now'],
'thing': ['account', 'password', 'payment', 'subscription'],
'amount': ['500 pounds', '2000 pounds', '1000 pounds'],
'pct': ['90%', '75%', '80%'],
'month': ['january', 'march', 'october', 'june'],
'day': ['monday', 'tuesday', 'thursday', 'friday'],
}
# Both classes draw from this, so plenty of words carry no signal.
NOISE = ['please', 'thanks', 'regards', 'best', 'hi', 'hello', 'team',
'quick', 'note', 'fyi', 'cheers', 'morning']

def fill(template):
out = template
for key, options in FILL.items():
while '{' + key + '}' in out:
out = out.replace('{' + key + '}', random.choice(options), 1)
# a couple of ordinary words, so the two classes overlap
return ' '.join([random.choice(NOISE)] + out.split()
+ [random.choice(NOISE)])

def build(spam_templates, ham_templates, n):
xs, ys = [], []
for _ in range(n):
xs.append(fill(random.choice(spam_templates)))
ys.append(1)
xs.append(fill(random.choice(ham_templates)))
ys.append(0)
return xs, ys

train_x, train_y = build(SPAM_TRAIN, HAM_TRAIN, 120)
# held out mail is phrased in ways the training set never contained
test_x, test_y = build(SPAM_TEST, HAM_TEST, 60)
messages, labels = train_x + test_x, train_y + test_y
BANNED = ['won', 'free', 'claim', 'offer', 'discount', 'urgent',
'verify', 'exclusive', 'selected', 'prize']

def rule_predict(message):
"""Somebody sat down and wrote what spam looks like."""
words = message.lower().replace(',', '').split()
return 1 if any(word in BANNED for word in words) else 0

def score(predict, xs, ys):
right = sum(1 for x, y in zip(xs, ys) if predict(x) == y)
caught = sum(1 for x, y in zip(xs, ys) if y == 1 and predict(x) == 1)
flagged = sum(1 for x, y in zip(xs, ys) if y == 0 and predict(x) == 1)
spam = sum(ys)
return right / len(xs), caught / spam, flagged / (len(ys) - spam)
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline

model = make_pipeline(TfidfVectorizer(), LogisticRegression())
model.fit(train_x, train_y)

def learned_predict(message):
return int(model.predict([message])[0])

print('%-28s %8s %8s %8s' % ('', 'accuracy', 'caught', 'false'))
for name, fn in [('keyword rules', rule_predict),
('learned from examples', learned_predict)]:
acc, caught, wrongly = score(fn, test_x, test_y)
print('%-28s %8.3f %8.3f %8.3f' % (name, acc, caught, wrongly))
accuracy caught false
keyword rules 0.492 0.200 0.217
learned from examples 0.883 0.983 0.217

0.883 against 0.492, on wording neither system has seen. Nobody told the model that claim or congratulations matter. It was shown the training messages and worked out for itself which words carry signal and how much, which is a far more forgiving thing to carry into unfamiliar phrasing than a list of ten words that either appear or do not.

Note also what the model did not do. It scored 0.883, not 1.000. Unfamiliar wording costs it too, and a system that is honest about this is worth more than one that is only ever tested on mail like the mail it learned from.

And look at the last column, which is the one people skip. Both systems wrongly flag ordinary mail at exactly the same rate, 0.217. The model's entire advantage is in the middle column: it catches almost all of the spam where the rules catch a fifth, at no extra cost in blocked invoices. That is a much more precise claim than the model is better, and it is the kind of claim worth learning to make.

What the model decided was important

import random

random.seed(11)

# Ten phrasings to learn from and four the model never sees, per class.
SPAM_TRAIN = [
'congratulations you have won a {prize}, claim it {when}',
'your {thing} will be closed, verify it {when}',
'earn {amount} a week from home, no experience needed',
'exclusive offer, {pct} off everything {when}',
'we noticed unusual activity on your {thing}, confirm it {when}',
'final notice about your {thing}, respond {when}',
'you have been selected for a {prize}, reply to arrange it',
'limited deal, {pct} discount if you order {when}',
'your {thing} is on hold, update your details {when}',
'claim your {prize} before it expires {when}',
]
SPAM_TEST = [
'act {when} to release the {amount} waiting on your {thing}',
'we tried to deliver your {prize}, reply with your details',
'a {pct} refund is pending, tell us where to send it',
'security alert on your {thing}, reply {when} or lose access',
]
# Ordinary mail uses the same words. That is the point: 'account',
# 'offer' and 'urgent' are perfectly normal words at work.
HAM_TRAIN = [
'the invoice for {month} is attached, let me know if it looks wrong',
'can we move the {day} meeting, i have a clash',
'the report is finished, it is in the shared folder',
'thanks for the figures, they match the {thing} i have',
'i am out on {day}, send anything urgent before then',
'the training session is confirmed for {month}',
'could you check the {thing} settings when you get a moment',
'the client asked about dates, i said {month}',
'we should discuss the {pct} increase before {day}',
'their offer came in {when}, i think we should accept',
]
HAM_TEST = [
'payroll needs your {thing} details before {month}',
'the supplier quoted {amount}, which is over budget',
'reminder that the {day} review starts at nine',
'i have won the argument about the {month} deadline',
]
FILL = {
'prize': ['free holiday', 'new phone', 'cash prize', 'gift card'],
'when': ['today', 'within 24 hours', 'before friday', 'now'],
'thing': ['account', 'password', 'payment', 'subscription'],
'amount': ['500 pounds', '2000 pounds', '1000 pounds'],
'pct': ['90%', '75%', '80%'],
'month': ['january', 'march', 'october', 'june'],
'day': ['monday', 'tuesday', 'thursday', 'friday'],
}
# Both classes draw from this, so plenty of words carry no signal.
NOISE = ['please', 'thanks', 'regards', 'best', 'hi', 'hello', 'team',
'quick', 'note', 'fyi', 'cheers', 'morning']

def fill(template):
out = template
for key, options in FILL.items():
while '{' + key + '}' in out:
out = out.replace('{' + key + '}', random.choice(options), 1)
# a couple of ordinary words, so the two classes overlap
return ' '.join([random.choice(NOISE)] + out.split()
+ [random.choice(NOISE)])

def build(spam_templates, ham_templates, n):
xs, ys = [], []
for _ in range(n):
xs.append(fill(random.choice(spam_templates)))
ys.append(1)
xs.append(fill(random.choice(ham_templates)))
ys.append(0)
return xs, ys

train_x, train_y = build(SPAM_TRAIN, HAM_TRAIN, 120)
# held out mail is phrased in ways the training set never contained
test_x, test_y = build(SPAM_TEST, HAM_TEST, 60)
messages, labels = train_x + test_x, train_y + test_y
BANNED = ['won', 'free', 'claim', 'offer', 'discount', 'urgent',
'verify', 'exclusive', 'selected', 'prize']

def rule_predict(message):
"""Somebody sat down and wrote what spam looks like."""
words = message.lower().replace(',', '').split()
return 1 if any(word in BANNED for word in words) else 0

def score(predict, xs, ys):
right = sum(1 for x, y in zip(xs, ys) if predict(x) == y)
caught = sum(1 for x, y in zip(xs, ys) if y == 1 and predict(x) == 1)
flagged = sum(1 for x, y in zip(xs, ys) if y == 0 and predict(x) == 1)
spam = sum(ys)
return right / len(xs), caught / spam, flagged / (len(ys) - spam)
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression

vec = TfidfVectorizer()
X = vec.fit_transform(train_x)
clf = LogisticRegression().fit(X, train_y)
words = vec.get_feature_names_out()
weights = clf.coef_[0]
order = weights.argsort()
print('%-16s %10s' % ('most spam-like', 'weight'))
for i in order[::-1][:6]:
print('%-16s %10.3f' % (words[i], weights[i]))
print()
print('%-16s %10s' % ('most ordinary', 'weight'))
for i in order[:6]:
print('%-16s %10.3f' % (words[i], weights[i]))
most spam-like weight
your 2.201
it 1.184
now 0.945
claim 0.926
today 0.895
order 0.775

most ordinary weight
the -2.881
we -1.165
should -1.163
in -1.003
is -0.854
meeting -0.766

That table is worth pausing on, because it shows the mechanism is not magic. The model learned a weight per word and adds them up. Some of those words you would have guessed. Others are there because of how this particular inbox is written, which is exactly the knowledge a person writing rules would have had to acquire by reading all two hundred messages.

And that is also the weakness

The model learned this inbox. Words that signal spam here may signal nothing elsewhere, and a spammer who reads the table can avoid every word in it. Rules have the same weakness and at least they are visible. A learned model's assumptions are implicit, which is why weeks 11 to 13 are about getting them back out.

Week 01 · Day 3 of 7

What a Learning System Is Made Of

Examples, labels, features, a model and a loss, with the loop running in the open

By 1456 words

Yesterday's model was four lines. Underneath those four lines is a structure that every supervised learning system shares, and naming its parts makes the rest of the course much easier to follow.

The five parts

  1. Examples. Things you have seen before. Here, messages.
  2. Labels. The answer for each example. Here, spam or not. Somebody had to provide these, and that cost is usually the real constraint on a project.
  3. Features. The numbers the model actually sees. A message is text; the model needs numbers, so something has to turn one into the other.
  4. A model. A function with adjustable settings, mapping features to a prediction.
  5. A loss and a way to reduce it. A number saying how wrong the predictions are, and a procedure that adjusts the settings to make it smaller.

Features, made visible

The step that surprises people most is the third one, so here it is explicitly. This is what the model receives instead of a sentence.

import random

random.seed(11)

# Ten phrasings to learn from and four the model never sees, per class.
SPAM_TRAIN = [
'congratulations you have won a {prize}, claim it {when}',
'your {thing} will be closed, verify it {when}',
'earn {amount} a week from home, no experience needed',
'exclusive offer, {pct} off everything {when}',
'we noticed unusual activity on your {thing}, confirm it {when}',
'final notice about your {thing}, respond {when}',
'you have been selected for a {prize}, reply to arrange it',
'limited deal, {pct} discount if you order {when}',
'your {thing} is on hold, update your details {when}',
'claim your {prize} before it expires {when}',
]
SPAM_TEST = [
'act {when} to release the {amount} waiting on your {thing}',
'we tried to deliver your {prize}, reply with your details',
'a {pct} refund is pending, tell us where to send it',
'security alert on your {thing}, reply {when} or lose access',
]
# Ordinary mail uses the same words. That is the point: 'account',
# 'offer' and 'urgent' are perfectly normal words at work.
HAM_TRAIN = [
'the invoice for {month} is attached, let me know if it looks wrong',
'can we move the {day} meeting, i have a clash',
'the report is finished, it is in the shared folder',
'thanks for the figures, they match the {thing} i have',
'i am out on {day}, send anything urgent before then',
'the training session is confirmed for {month}',
'could you check the {thing} settings when you get a moment',
'the client asked about dates, i said {month}',
'we should discuss the {pct} increase before {day}',
'their offer came in {when}, i think we should accept',
]
HAM_TEST = [
'payroll needs your {thing} details before {month}',
'the supplier quoted {amount}, which is over budget',
'reminder that the {day} review starts at nine',
'i have won the argument about the {month} deadline',
]
FILL = {
'prize': ['free holiday', 'new phone', 'cash prize', 'gift card'],
'when': ['today', 'within 24 hours', 'before friday', 'now'],
'thing': ['account', 'password', 'payment', 'subscription'],
'amount': ['500 pounds', '2000 pounds', '1000 pounds'],
'pct': ['90%', '75%', '80%'],
'month': ['january', 'march', 'october', 'june'],
'day': ['monday', 'tuesday', 'thursday', 'friday'],
}
# Both classes draw from this, so plenty of words carry no signal.
NOISE = ['please', 'thanks', 'regards', 'best', 'hi', 'hello', 'team',
'quick', 'note', 'fyi', 'cheers', 'morning']

def fill(template):
out = template
for key, options in FILL.items():
while '{' + key + '}' in out:
out = out.replace('{' + key + '}', random.choice(options), 1)
# a couple of ordinary words, so the two classes overlap
return ' '.join([random.choice(NOISE)] + out.split()
+ [random.choice(NOISE)])

def build(spam_templates, ham_templates, n):
xs, ys = [], []
for _ in range(n):
xs.append(fill(random.choice(spam_templates)))
ys.append(1)
xs.append(fill(random.choice(ham_templates)))
ys.append(0)
return xs, ys

train_x, train_y = build(SPAM_TRAIN, HAM_TRAIN, 120)
# held out mail is phrased in ways the training set never contained
test_x, test_y = build(SPAM_TEST, HAM_TEST, 60)
messages, labels = train_x + test_x, train_y + test_y
from sklearn.feature_extraction.text import CountVectorizer

sample = ['claim your free prize today', 'the invoice is attached']
vec = CountVectorizer()
counts = vec.fit_transform(sample)
print('vocabulary: %s' % list(vec.get_feature_names_out()))
print()
for text, row in zip(sample, counts.toarray()):
print('%-32s %s' % (text, row))
vocabulary: ['attached', 'claim', 'free', 'invoice', 'is', 'prize', 'the', 'today', 'your']

claim your free prize today [0 1 1 0 0 1 0 1 1]
the invoice is attached [1 0 0 1 1 0 1 0 0]

The sentence has become a row of counts, one column per word in the vocabulary. Word order is gone entirely, which is why this representation is called a bag of words. It is crude, and it is enough to have beaten the rules yesterday.

Bag of words: Representing a document by which words it contains and how often, discarding the order. Dog bites man and man bites dog become identical. Week 4 measures what that costs and what to do about it.

Watching the loss come down

The last part is the training loop. Most libraries hide it, so here it is running in the open on the same data, with the error printed every few passes.

import random

random.seed(11)

# Ten phrasings to learn from and four the model never sees, per class.
SPAM_TRAIN = [
'congratulations you have won a {prize}, claim it {when}',
'your {thing} will be closed, verify it {when}',
'earn {amount} a week from home, no experience needed',
'exclusive offer, {pct} off everything {when}',
'we noticed unusual activity on your {thing}, confirm it {when}',
'final notice about your {thing}, respond {when}',
'you have been selected for a {prize}, reply to arrange it',
'limited deal, {pct} discount if you order {when}',
'your {thing} is on hold, update your details {when}',
'claim your {prize} before it expires {when}',
]
SPAM_TEST = [
'act {when} to release the {amount} waiting on your {thing}',
'we tried to deliver your {prize}, reply with your details',
'a {pct} refund is pending, tell us where to send it',
'security alert on your {thing}, reply {when} or lose access',
]
# Ordinary mail uses the same words. That is the point: 'account',
# 'offer' and 'urgent' are perfectly normal words at work.
HAM_TRAIN = [
'the invoice for {month} is attached, let me know if it looks wrong',
'can we move the {day} meeting, i have a clash',
'the report is finished, it is in the shared folder',
'thanks for the figures, they match the {thing} i have',
'i am out on {day}, send anything urgent before then',
'the training session is confirmed for {month}',
'could you check the {thing} settings when you get a moment',
'the client asked about dates, i said {month}',
'we should discuss the {pct} increase before {day}',
'their offer came in {when}, i think we should accept',
]
HAM_TEST = [
'payroll needs your {thing} details before {month}',
'the supplier quoted {amount}, which is over budget',
'reminder that the {day} review starts at nine',
'i have won the argument about the {month} deadline',
]
FILL = {
'prize': ['free holiday', 'new phone', 'cash prize', 'gift card'],
'when': ['today', 'within 24 hours', 'before friday', 'now'],
'thing': ['account', 'password', 'payment', 'subscription'],
'amount': ['500 pounds', '2000 pounds', '1000 pounds'],
'pct': ['90%', '75%', '80%'],
'month': ['january', 'march', 'october', 'june'],
'day': ['monday', 'tuesday', 'thursday', 'friday'],
}
# Both classes draw from this, so plenty of words carry no signal.
NOISE = ['please', 'thanks', 'regards', 'best', 'hi', 'hello', 'team',
'quick', 'note', 'fyi', 'cheers', 'morning']

def fill(template):
out = template
for key, options in FILL.items():
while '{' + key + '}' in out:
out = out.replace('{' + key + '}', random.choice(options), 1)
# a couple of ordinary words, so the two classes overlap
return ' '.join([random.choice(NOISE)] + out.split()
+ [random.choice(NOISE)])

def build(spam_templates, ham_templates, n):
xs, ys = [], []
for _ in range(n):
xs.append(fill(random.choice(spam_templates)))
ys.append(1)
xs.append(fill(random.choice(ham_templates)))
ys.append(0)
return xs, ys

train_x, train_y = build(SPAM_TRAIN, HAM_TRAIN, 120)
# held out mail is phrased in ways the training set never contained
test_x, test_y = build(SPAM_TEST, HAM_TEST, 60)
messages, labels = train_x + test_x, train_y + test_y
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer

vec = TfidfVectorizer()
X = vec.fit_transform(train_x).toarray()
y = np.array(train_y)
w = np.zeros(X.shape[1])
b = 0.0

print('%6s %12s %10s' % ('pass', 'loss', 'accuracy'))
for step in range(1, 301):
z = X @ w + b
guess = 1 / (1 + np.exp(-z))
# how wrong we are, averaged over every training message
loss = -np.mean(y * np.log(guess + 1e-9)
+ (1 - y) * np.log(1 - guess + 1e-9))
error = guess - y
w -= 0.5 * (X.T @ error) / len(y)
b -= 0.5 * error.mean()
if step % 60 == 0:
acc = ((guess > 0.5).astype(int) == y).mean()
print('%6d %12.4f %10.4f' % (step, loss, acc))
pass loss accuracy
60 0.4286 1.0000
120 0.2976 1.0000
180 0.2243 1.0000
240 0.1785 1.0000
300 0.1475 1.0000

That is the whole of learning, in eight lines. Make a prediction, measure how wrong it is, nudge every setting in the direction that makes it less wrong, repeat. Every model in this course and every model in the news does a version of this, differing in the shape of the function and the size of the numbers rather than in the idea.

Week 01 · Day 4 of 7

Generalisation

Memorising against learning, and the baseline nobody runs

By 1396 words

A model that scores well on the data it learned from has demonstrated nothing. It could have memorised. The entire question is whether it works on examples it has never seen, and that question has a name.

Generalisation: Performing well on data that was not used to build the system. It is the only thing anybody is actually buying, and it cannot be measured on the training data by construction.

Memorising, demonstrated

Here is a model deliberately given enough freedom to memorise, scored both ways.

import random

random.seed(11)

# Ten phrasings to learn from and four the model never sees, per class.
SPAM_TRAIN = [
'congratulations you have won a {prize}, claim it {when}',
'your {thing} will be closed, verify it {when}',
'earn {amount} a week from home, no experience needed',
'exclusive offer, {pct} off everything {when}',
'we noticed unusual activity on your {thing}, confirm it {when}',
'final notice about your {thing}, respond {when}',
'you have been selected for a {prize}, reply to arrange it',
'limited deal, {pct} discount if you order {when}',
'your {thing} is on hold, update your details {when}',
'claim your {prize} before it expires {when}',
]
SPAM_TEST = [
'act {when} to release the {amount} waiting on your {thing}',
'we tried to deliver your {prize}, reply with your details',
'a {pct} refund is pending, tell us where to send it',
'security alert on your {thing}, reply {when} or lose access',
]
# Ordinary mail uses the same words. That is the point: 'account',
# 'offer' and 'urgent' are perfectly normal words at work.
HAM_TRAIN = [
'the invoice for {month} is attached, let me know if it looks wrong',
'can we move the {day} meeting, i have a clash',
'the report is finished, it is in the shared folder',
'thanks for the figures, they match the {thing} i have',
'i am out on {day}, send anything urgent before then',
'the training session is confirmed for {month}',
'could you check the {thing} settings when you get a moment',
'the client asked about dates, i said {month}',
'we should discuss the {pct} increase before {day}',
'their offer came in {when}, i think we should accept',
]
HAM_TEST = [
'payroll needs your {thing} details before {month}',
'the supplier quoted {amount}, which is over budget',
'reminder that the {day} review starts at nine',
'i have won the argument about the {month} deadline',
]
FILL = {
'prize': ['free holiday', 'new phone', 'cash prize', 'gift card'],
'when': ['today', 'within 24 hours', 'before friday', 'now'],
'thing': ['account', 'password', 'payment', 'subscription'],
'amount': ['500 pounds', '2000 pounds', '1000 pounds'],
'pct': ['90%', '75%', '80%'],
'month': ['january', 'march', 'october', 'june'],
'day': ['monday', 'tuesday', 'thursday', 'friday'],
}
# Both classes draw from this, so plenty of words carry no signal.
NOISE = ['please', 'thanks', 'regards', 'best', 'hi', 'hello', 'team',
'quick', 'note', 'fyi', 'cheers', 'morning']

def fill(template):
out = template
for key, options in FILL.items():
while '{' + key + '}' in out:
out = out.replace('{' + key + '}', random.choice(options), 1)
# a couple of ordinary words, so the two classes overlap
return ' '.join([random.choice(NOISE)] + out.split()
+ [random.choice(NOISE)])

def build(spam_templates, ham_templates, n):
xs, ys = [], []
for _ in range(n):
xs.append(fill(random.choice(spam_templates)))
ys.append(1)
xs.append(fill(random.choice(ham_templates)))
ys.append(0)
return xs, ys

train_x, train_y = build(SPAM_TRAIN, HAM_TRAIN, 120)
# held out mail is phrased in ways the training set never contained
test_x, test_y = build(SPAM_TEST, HAM_TEST, 60)
messages, labels = train_x + test_x, train_y + test_y
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.tree import DecisionTreeClassifier
from sklearn.linear_model import LogisticRegression

vec = TfidfVectorizer()
Xtr = vec.fit_transform(train_x)
Xte = vec.transform(test_x)

print('%-34s %12s %12s' % ('', 'on training', 'on held out'))
for name, model in [('a deep decision tree',
DecisionTreeClassifier(random_state=0)),
('logistic regression',
LogisticRegression())]:
model.fit(Xtr, train_y)
print('%-34s %12.4f %12.4f'
% (name, model.score(Xtr, train_y), model.score(Xte, test_y)))
on training on held out
a deep decision tree 1.0000 0.7333
logistic regression 1.0000 0.8833

Both models score a perfect 1.000 on the messages they were built from, and that column tells you nothing whatsoever. The right hand column is where they separate: 0.733 for the tree against 0.883 for logistic regression.

Same data, same features, same perfect training score, and one of them carries fifteen points more of it across to unfamiliar wording. The tree memorised harder, in the sense that more of what it learned was specific to the exact messages in front of it. The gap between those two columns is the thing to watch for the rest of your career, and it is invisible if you only ever look at the left one.

The baseline nobody runs

Before celebrating any accuracy, you need to know what a system that does nothing clever would score. Otherwise the number has no meaning at all.

import random

random.seed(11)

# Ten phrasings to learn from and four the model never sees, per class.
SPAM_TRAIN = [
'congratulations you have won a {prize}, claim it {when}',
'your {thing} will be closed, verify it {when}',
'earn {amount} a week from home, no experience needed',
'exclusive offer, {pct} off everything {when}',
'we noticed unusual activity on your {thing}, confirm it {when}',
'final notice about your {thing}, respond {when}',
'you have been selected for a {prize}, reply to arrange it',
'limited deal, {pct} discount if you order {when}',
'your {thing} is on hold, update your details {when}',
'claim your {prize} before it expires {when}',
]
SPAM_TEST = [
'act {when} to release the {amount} waiting on your {thing}',
'we tried to deliver your {prize}, reply with your details',
'a {pct} refund is pending, tell us where to send it',
'security alert on your {thing}, reply {when} or lose access',
]
# Ordinary mail uses the same words. That is the point: 'account',
# 'offer' and 'urgent' are perfectly normal words at work.
HAM_TRAIN = [
'the invoice for {month} is attached, let me know if it looks wrong',
'can we move the {day} meeting, i have a clash',
'the report is finished, it is in the shared folder',
'thanks for the figures, they match the {thing} i have',
'i am out on {day}, send anything urgent before then',
'the training session is confirmed for {month}',
'could you check the {thing} settings when you get a moment',
'the client asked about dates, i said {month}',
'we should discuss the {pct} increase before {day}',
'their offer came in {when}, i think we should accept',
]
HAM_TEST = [
'payroll needs your {thing} details before {month}',
'the supplier quoted {amount}, which is over budget',
'reminder that the {day} review starts at nine',
'i have won the argument about the {month} deadline',
]
FILL = {
'prize': ['free holiday', 'new phone', 'cash prize', 'gift card'],
'when': ['today', 'within 24 hours', 'before friday', 'now'],
'thing': ['account', 'password', 'payment', 'subscription'],
'amount': ['500 pounds', '2000 pounds', '1000 pounds'],
'pct': ['90%', '75%', '80%'],
'month': ['january', 'march', 'october', 'june'],
'day': ['monday', 'tuesday', 'thursday', 'friday'],
}
# Both classes draw from this, so plenty of words carry no signal.
NOISE = ['please', 'thanks', 'regards', 'best', 'hi', 'hello', 'team',
'quick', 'note', 'fyi', 'cheers', 'morning']

def fill(template):
out = template
for key, options in FILL.items():
while '{' + key + '}' in out:
out = out.replace('{' + key + '}', random.choice(options), 1)
# a couple of ordinary words, so the two classes overlap
return ' '.join([random.choice(NOISE)] + out.split()
+ [random.choice(NOISE)])

def build(spam_templates, ham_templates, n):
xs, ys = [], []
for _ in range(n):
xs.append(fill(random.choice(spam_templates)))
ys.append(1)
xs.append(fill(random.choice(ham_templates)))
ys.append(0)
return xs, ys

train_x, train_y = build(SPAM_TRAIN, HAM_TRAIN, 120)
# held out mail is phrased in ways the training set never contained
test_x, test_y = build(SPAM_TEST, HAM_TEST, 60)
messages, labels = train_x + test_x, train_y + test_y
from sklearn.dummy import DummyClassifier
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression

vec = TfidfVectorizer()
Xtr, Xte = vec.fit_transform(train_x), vec.transform(test_x)
print('%-34s %12s' % ('', 'held out'))
for name, model in [('always guess the commoner class',
DummyClassifier(strategy='most_frequent')),
('guess at random, in proportion',
DummyClassifier(strategy='stratified',
random_state=0)),
('the actual model', LogisticRegression())]:
model.fit(Xtr, train_y)
print('%-34s %12.4f' % (name, model.score(Xte, test_y)))
held out
always guess the commoner class 0.5000
guess at random, in proportion 0.4750
the actual model 0.8833

An accuracy without a baseline is not a result

This inbox is deliberately balanced, so guessing gets about half and the model's score is clearly meaningful. Real data is rarely so obliging. If one in a hundred transactions is fraud, a model that says not fraud every single time is 99 percent accurate and completely worthless, and it is reported as a success more often than you would believe.

Always run the do-nothing baseline first. It takes one line and it tells you what your real number has to beat.

Week 01 · Day 5 of 7

The Shapes a Problem Comes In

Classification, regression, ranking, clustering and generation

By 439 words

Almost every applied AI problem is one of a small number of shapes. Recognising which shape you have tells you what kind of data you need, what model to reach for, and how to measure success, so it is worth learning the list properly.

The shapes

ShapeYou are predictingAn exampleMeasured with
Binary classificationOne of two categoriesIs this message spamAccuracy, precision, recall
Multi-class classificationOne of several categoriesWhich of ten digits is thisAccuracy, confusion matrix
RegressionA numberWhat will this house sell forAverage error in the same units as the answer
RankingAn orderWhich results to show firstHow high the good ones land
ClusteringGroups, with no labels givenWhich customers behave alikeJudgement, mostly
GenerationNew contentWrite a summary of thisDifficult, and week 7 returns to it

The same library, three different shapes

from sklearn.datasets import load_diabetes, load_digits
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.cluster import KMeans
from sklearn.model_selection import train_test_split
import numpy as np

# regression: predict a number
X, y = load_diabetes(return_X_y=True)
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.25,
random_state=0)
pred = LinearRegression().fit(Xtr, ytr).predict(Xte)
print('regression, average error %.1f units' % np.abs(pred - yte).mean())
print(' guessing the average is %.1f units'
% np.abs(ytr.mean() - yte).mean())

# multi-class classification: predict which of ten
X, y = load_digits(return_X_y=True)
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.25,
random_state=0)
clf = RandomForestClassifier(n_estimators=60, random_state=0).fit(Xtr, ytr)
print('classification, accuracy %.4f' % clf.score(Xte, yte))
print(' guessing would give %.4f' % (1 / 10))

# clustering: no labels used at all
groups = KMeans(n_clusters=10, n_init=10, random_state=0).fit_predict(X)
agree = max((groups == g).sum() for g in range(10))
print('clustering found 10 groups, largest holds %d of %d images'
% (agree, len(X)))
regression, average error 45.1 units
guessing the average is 58.3 units
classification, accuracy 0.9778
guessing would give 0.1000
clustering found 10 groups, largest holds 247 of 1797 images

Three problems, three shapes, one library, and in each case a do-nothing comparison next to the result. Notice that the clustering line has no accuracy at all, because no labels were used, and that is the honest situation rather than an omission.

Getting the shape wrong is expensive

Predicting a five-star rating is a regression if you care how far off you are, and a classification if you only care about the exact star. Those two framings need different models, different metrics and sometimes different data. Teams routinely build one and evaluate it as though it were the other.

Week 01 · Day 6 of 7

When Not to Use It

The questions to ask before any code exists

By 413 words

Day 1 said that reaching for machine learning where rules would do is a common and expensive mistake. This day is the practical version of that claim.

When rules are the better answer

  • The logic is known and stable. Tax rates, opening hours, eligibility thresholds. Learning them from examples is slower, less accurate and impossible to audit.
  • You have no labelled examples, and getting them would cost more than writing the logic.
  • Every decision must be explainable to a regulator in terms of the rule that produced it.
  • The cost of a wrong answer is severe and the volume is low enough for a person to review.

When learning is the better answer

  • The pattern is real but nobody can articulate it. Recognising speech, reading handwriting, judging whether a photograph contains a pedestrian.
  • The rules would be enormous. Ten thousand keyword rules for spam is not a system anybody can maintain.
  • The pattern changes and you can keep collecting examples, so the system can be retrained rather than rewritten.
  • You have, or can get, labelled examples in reasonable numbers.

The questions worth asking before anything is built

  1. What decision changes as a result? If nothing is done differently, the model has no value however accurate it is.
  2. What does a wrong answer cost, and which kind of wrong? Missing spam and blocking an invoice are not equally bad, and no single accuracy number distinguishes them.
  3. What would a simple rule achieve? Build it. It is a day's work and it is the number your model has to beat.
  4. Do the labels exist? If not, who makes them, how long does it take, and do they agree with each other?
  5. Will the data still look like this in a year? A model is a snapshot of the past, and week 13 covers what happens when the world moves.

Not one of those questions is technical

That is the point. The most consequential mistakes in applied AI are made before any code exists, by choosing a problem that does not need this, or whose labels do not exist, or whose output nobody will act on. Every one of those is discoverable in a conversation, and the conversation is cheaper than the project.

The one line worth remembering from the week

Machine learning is for problems where the pattern is real, nobody can write it down, and examples are obtainable. When any of those three is missing, something simpler is very likely the better engineering decision.
Week 01 · Day 7 of 7

One Problem, End to End

The whole week applied in order, and the four counts that matter

By 830 words

Everything from the week, applied once, in the order it should be done in.

import random

random.seed(11)

# Ten phrasings to learn from and four the model never sees, per class.
SPAM_TRAIN = [
'congratulations you have won a {prize}, claim it {when}',
'your {thing} will be closed, verify it {when}',
'earn {amount} a week from home, no experience needed',
'exclusive offer, {pct} off everything {when}',
'we noticed unusual activity on your {thing}, confirm it {when}',
'final notice about your {thing}, respond {when}',
'you have been selected for a {prize}, reply to arrange it',
'limited deal, {pct} discount if you order {when}',
'your {thing} is on hold, update your details {when}',
'claim your {prize} before it expires {when}',
]
SPAM_TEST = [
'act {when} to release the {amount} waiting on your {thing}',
'we tried to deliver your {prize}, reply with your details',
'a {pct} refund is pending, tell us where to send it',
'security alert on your {thing}, reply {when} or lose access',
]
# Ordinary mail uses the same words. That is the point: 'account',
# 'offer' and 'urgent' are perfectly normal words at work.
HAM_TRAIN = [
'the invoice for {month} is attached, let me know if it looks wrong',
'can we move the {day} meeting, i have a clash',
'the report is finished, it is in the shared folder',
'thanks for the figures, they match the {thing} i have',
'i am out on {day}, send anything urgent before then',
'the training session is confirmed for {month}',
'could you check the {thing} settings when you get a moment',
'the client asked about dates, i said {month}',
'we should discuss the {pct} increase before {day}',
'their offer came in {when}, i think we should accept',
]
HAM_TEST = [
'payroll needs your {thing} details before {month}',
'the supplier quoted {amount}, which is over budget',
'reminder that the {day} review starts at nine',
'i have won the argument about the {month} deadline',
]
FILL = {
'prize': ['free holiday', 'new phone', 'cash prize', 'gift card'],
'when': ['today', 'within 24 hours', 'before friday', 'now'],
'thing': ['account', 'password', 'payment', 'subscription'],
'amount': ['500 pounds', '2000 pounds', '1000 pounds'],
'pct': ['90%', '75%', '80%'],
'month': ['january', 'march', 'october', 'june'],
'day': ['monday', 'tuesday', 'thursday', 'friday'],
}
# Both classes draw from this, so plenty of words carry no signal.
NOISE = ['please', 'thanks', 'regards', 'best', 'hi', 'hello', 'team',
'quick', 'note', 'fyi', 'cheers', 'morning']

def fill(template):
out = template
for key, options in FILL.items():
while '{' + key + '}' in out:
out = out.replace('{' + key + '}', random.choice(options), 1)
# a couple of ordinary words, so the two classes overlap
return ' '.join([random.choice(NOISE)] + out.split()
+ [random.choice(NOISE)])

def build(spam_templates, ham_templates, n):
xs, ys = [], []
for _ in range(n):
xs.append(fill(random.choice(spam_templates)))
ys.append(1)
xs.append(fill(random.choice(ham_templates)))
ys.append(0)
return xs, ys

train_x, train_y = build(SPAM_TRAIN, HAM_TRAIN, 120)
# held out mail is phrased in ways the training set never contained
test_x, test_y = build(SPAM_TEST, HAM_TEST, 60)
messages, labels = train_x + test_x, train_y + test_y
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.dummy import DummyClassifier
from sklearn.pipeline import make_pipeline
from sklearn.metrics import confusion_matrix

# 1. the do-nothing baseline, first, always
dummy = make_pipeline(TfidfVectorizer(),
DummyClassifier(strategy='most_frequent'))
dummy.fit(train_x, train_y)

# 2. the model
model = make_pipeline(TfidfVectorizer(), LogisticRegression())
model.fit(train_x, train_y)

print('%-24s %10s' % ('', 'held out'))
print('%-24s %10.4f' % ('do nothing', dummy.score(test_x, test_y)))
print('%-24s %10.4f' % ('the model', model.score(test_x, test_y)))
print()

# 3. not one number but the four kinds of outcome
tn, fp, fn, tp = confusion_matrix(test_y, model.predict(test_x)).ravel()
print('spam correctly caught %3d' % tp)
print('spam that got through %3d' % fn)
print('ordinary mail blocked %3d' % fp)
print('ordinary mail delivered %3d' % tn)
held out
do nothing 0.5000
the model 0.8833

spam correctly caught 59
spam that got through 1
ordinary mail blocked 13
ordinary mail delivered 47

The four counts at the bottom matter more than the accuracy above them. Blocking somebody's invoice and letting a junk message through are both errors and they are not equally bad, and no single percentage can tell you which the system is doing. Week 2 develops this properly.

What the week established

  • AI covers rules, search and learning, and they are suited to different problems rather than being stages of sophistication.
  • A learned model is examples, labels, features, a function and a loss being reduced. Nothing more mysterious than that.
  • Every number must be measured on data the system has not seen.
  • Every number needs a do-nothing baseline beside it before it means anything.
  • The shape of the problem, and whether it needs learning at all, are decisions made before any code is written.

Where the course goes next

Week 2 goes deeper into how machines learn and how to measure them honestly. Week 3 covers the search and knowledge methods that predate learning and still run a great deal of production software. Weeks 4 to 7 are language, weeks 8 to 10 are images, and weeks 11 to 13 are about the consequences of deploying any of it, which is the part that turns a working model into something you can defend.