What does it mean for an algorithm to be fair?

In 2014 the White House commissioned a 90-day study that culminated in a report (pdf) on the state of “big data” and related technologies. The authors give many recommendations, including this central warning.

Warning: algorithms can facilitate illegal discrimination!

Here’s a not-so-imaginary example of the problem. A bank wants people to take loans with high interest rates, and it also serves ads for these loans. A modern idea is to use an algorithm to decide, based on the sliver of known information about a user visiting a website, which advertisement to present that gives the largest chance of the user clicking on it. There’s one problem: these algorithms are trained on historical data, and poor uneducated people (often racial minorities) have a historical trend of being more likely to succumb to predatory loan advertisements than the general population. So an algorithm that is “just” trying to maximize clickthrough may also be targeting black people, de facto denying them opportunities for fair loans. Such behavior is illegal.

Payday-Loans

On the other hand, even if algorithms are not making illegal decisions, by training algorithms on data produced by humans, we naturally reinforce prejudices of the majority. This can have negative effects, like Google’s autocomplete finishing “Are transgenders” with “going to hell?” Even if this is the most common question being asked on Google, and even if the majority think it’s morally acceptable to display this to users, this shows that algorithms do in fact encode our prejudices. People are slowly coming to realize this, to the point where it was recently covered in the New York Times.

There are many facets to the algorithm fairness problem one that has not even been widely acknowledged as a problem, despite the Times article. The message has been echoed by machine learning researchers but mostly ignored by practitioners. In particular, “experts” continually make ignorant claims such as, “equations can’t be racist,” and the following quote from the above linked article about how the Chicago Police Department has been using algorithms to do predictive policing.

Wernick denies that [the predictive policing] algorithm uses “any racial, neighborhood, or other such information” to assist in compiling the heat list [of potential repeat offenders].

Why is this ignorant? Because of the well-known fact that removing explicit racial features from data does not eliminate an algorithm’s ability to learn race. If racial features disproportionately correlate with crime (as they do in the US), then an algorithm which learns race is actually doing exactly what it is designed to do! One needs to be very thorough to say that an algorithm does not “use race” in its computations. Algorithms are not designed in a vacuum, but rather in conjunction with the designer’s analysis of their data. There are two points of failure here: the designer can unwittingly encode biases into the algorithm based on a biased exploration of the data, and the data itself can encode biases due to human decisions made to create it. Because of this, the burden of proof is (or should be!) on the practitioner to guarantee they are not violating discrimination law. Wernick should instead prove mathematically that the policing algorithm does not discriminate.

While that viewpoint is idealistic, it’s a bit naive because there is no accepted definition of what it means for an algorithm to be fair. In fact, from a precise mathematical standpoint, there isn’t even a precise legal definition of what it means for any practice to be fair. In the US the existing legal theory is called disparate impact, which states that a practice can be considered illegal discrimination if it has a “disproportionately adverse” effect on members of a protected group. Here “disproportionate” is precisely defined by the 80% rule, but this is somehow not enforced as stated. As with many legal issues, laws are broad assertions that are challenged on a case-by-case basis. In the case of fairness, the legal decision usually hinges on whether an individual was treated unfairly, because the individual is the one who files the lawsuit. Our understanding of the law is cobbled together, essentially through anecdotes slanted by political agendas. A mathematician can’t make progress with that. We want the mathematical essence of fairness, not something that can be interpreted depending on the court majority.

The problem is exacerbated for data mining because the practitioners often demonstrate a poor understanding of statistics, the management doesn’t understand algorithms, and almost everyone is lulled into a false sense of security via abstraction (remember, “equations can’t be racist”). Experts in discrimination law aren’t trained to audit algorithms, and engineers aren’t trained in social science or law. The speed with which research becomes practice far outpaces the speed at which anyone can keep up. This is especially true at places like Google and Facebook, where teams of in-house mathematicians and algorithm designers bypass the delay between academia and industry.

And perhaps the worst part is that even the world’s best mathematicians and computer scientists don’t know how to interpret the output of many popular learning algorithms. This isn’t just a problem that stupid people aren’t listening to smart people, it’s that everyone is “stupid.” A more politically correct way to say it: transparency in machine learning is a wide open problem. Take, for example, deep learning. A far-removed adaptation of neuroscience to data mining, deep learning has become the flagship technique spearheading modern advances in image tagging, speech recognition, and other classification problems.

A typical example of how a deep neural network learns to tag images. Image source: http://engineering.flipboard.com/2015/05/scaling-convnets/

A typical example of how a deep neural network learns to tag images. Image source: http://engineering.flipboard.com/2015/05/scaling-convnets/

The picture above shows how low level “features” (which essentially boil down to simple numerical combinations of pixel values) are combined in a “neural network” to more complicated image-like structures. The claim that these features represent natural concepts like “cat” and “horse” have fueled the public attention on deep learning for years. But looking at the above, is there any reasonable way to say whether these are encoding “discriminatory information”? Not only is this an open question, but we don’t even know what kinds of problems deep learning can solve! How can we understand to what extent neural networks can encode discrimination if we don’t have a deep understanding of why a neural network is good at what it does?

What makes this worse is that there are only about ten people in the world who understand the practical aspects of deep learning well enough to achieve record results for deep learning. This means they spent a ton of time tinkering the model to make it domain-specific, and nobody really knows whether the subtle differences between the top models correspond to genuine advances or slight overfitting or luck. Who is to say whether the fiasco with Google tagging images of black people as apes was caused by the data or the deep learning algorithm or by some obscure tweak made by the designer? I doubt even the designer could tell you with any certainty.

Opacity and a lack of interpretability is the rule more than the exception in machine learning. Celebrated techniques like Support Vector Machines, Boosting, and recent popular “tensor methods” are all highly opaque. This means that even if we knew what fairness meant, it is still a challenge (though one we’d be suited for) to modify existing algorithms to become fair. But with recent success stories in theoretical computer science connecting security, trust, and privacy, computer scientists have started to take up the call of nailing down what fairness means, and how to measure and enforce fairness in algorithms. There is now a yearly workshop called Fairness, Accountability, and Transparency in Machine Learning (FAT-ML, an awesome acronym), and some famous theory researchers are starting to get involved, as are social scientists and legal experts. Full disclosure, two days ago I gave a talk as part of this workshop on modifications to AdaBoost that seem to make it more fair. More on that in a future post.

From our perspective, we the computer scientists and mathematicians, the central obstacle is still that we don’t have a good definition of fairness.

In the next post I want to get a bit more technical. I’ll describe the parts of the fairness literature I like (which will be biased), I’ll hypothesize about the tension between statistical fairness and individual fairness, and I’ll entertain ideas on how someone designing a controversial algorithm (such as a predictive policing algorithm) could maintain transparency and accountability over its discriminatory impact. In subsequent posts I want to explain in more detail why it seems so difficult to come up with a useful definition of fairness, and to describe some of the ideas I and my coauthors have worked on.

Until then!

Weak Learning, Boosting, and the AdaBoost algorithm

When addressing the question of what it means for an algorithm to learn, one can imagine many different models, and there are quite a few. This invariably raises the question of which models are “the same” and which are “different,” along with a precise description of how we’re comparing models. We’ve seen one learning model so far, called Probably Approximately Correct (PAC), which espouses the following answer to the learning question:

An algorithm can “solve” a classification task using labeled examples drawn from some distribution if it can achieve accuracy that is arbitrarily close to perfect on the distribution, and it can meet this goal with arbitrarily high probability, where its runtime and the number of examples needed scales efficiently with all the parameters (accuracy, confidence, size of an example). Moreover, the algorithm needs to succeed no matter what distribution generates the examples.

You can think of this as a game between the algorithm designer and an adversary. First, the learning problem is fixed and everyone involved knows what the task is. Then the algorithm designer has to pick an algorithm. Then the adversary, knowing the chosen algorithm, chooses a nasty distribution D over examples that are fed to the learning algorithm. The algorithm designer “wins” if the algorithm produces a hypothesis with low error on D when given samples from D. And our goal is to prove that the algorithm designer can pick a single algorithm that is extremely likely to win no matter what D the adversary picks.

We’ll momentarily restate this with a more precise definition, because in this post we will compare it to a slightly different model, which is called the weak PAC-learning model. It’s essentially the same as PAC, except it only requires the algorithm to have accuracy that is slightly better than random guessing. That is, the algorithm will output a classification function which will correctly classify a random label with probability at least \frac{1}{2} + \eta for some small, but fixed, \eta > 0. The quantity \eta (the Greek “eta”) is called the edge as in “the edge over random guessing.” We call an algorithm that produces such a hypothesis a weak learner, and in contrast we’ll call a successful algorithm in the usual PAC model a strong learner.

The amazing fact is that strong learning and weak learning are equivalent! Of course a weak learner is not the same thing as a strong learner. What we mean by “equivalent” is that:

A problem can be weak-learned if and only if it can be strong-learned.

So they are computationally the same. One direction of this equivalence is trivial: if you have a strong learner for a classification task then it’s automatically a weak learner for the same task. The reverse is much harder, and this is the crux: there is an algorithm for transforming a weak learner into a strong learner! Informally, we “boost” the weak learning algorithm by feeding it examples from carefully constructed distributions, and then take a majority vote. This “reduction” from strong to weak learning is where all the magic happens.

In this post we’ll get into the depths of this boosting technique. We’ll review the model of PAC-learning, define what it means to be a weak learner, “organically” come up with the AdaBoost algorithm from some intuitive principles, prove that AdaBoost reduces error on the training data, and then run it on data. It turns out that despite the origin of boosting being a purely theoretical question, boosting algorithms have had a wide impact on practical machine learning as well.

As usual, all of the code and data used in this post is available on this blog’s Github page.

History and multiplicative weights

Before we get into the details, here’s a bit of history and context. PAC learning was introduced by Leslie Valiant in 1984, laying the foundation for a flurry of innovation. In 1988 Michael Kearns posed the question of whether one can “boost” a weak learner to a strong learner. Two years later Rob Schapire published his landmark paper “The Strength of Weak Learnability” closing the theoretical question by providing the first “boosting” algorithm. Schapire and Yoav Freund worked together for the next few years to produce a simpler and more versatile algorithm called AdaBoost, and for this they won the Gödel Prize, one of the highest honors in theoretical computer science. AdaBoost is also the standard boosting algorithm used in practice, though there are enough variants to warrant a book on the subject.

I’m going to define and prove that AdaBoost works in this post, and implement it and test it on some data. But first I want to give some high level discussion of the technique, and afterward the goal is to make that wispy intuition rigorous.

The central technique of AdaBoost has been discovered and rediscovered in computer science, and recently it was recognized abstractly in its own right. It is called the Multiplicative Weights Update Algorithm (MWUA), and it has applications in everything from learning theory to combinatorial optimization and game theory. The idea is to

  1. Maintain a nonnegative weight for the elements of some set,
  2. Draw a random element proportionally to the weights,
  3. So something with the chosen element, and based on the outcome of the “something…”
  4. Update the weights and repeat.

The “something” is usually a black box algorithm like “solve this simple optimization problem.” The output of the “something” is interpreted as a reward or penalty, and the weights are updated according to the severity of the penalty (the details of how this is done differ depending on the goal). In this light one can interpret MWUA as minimizing regret with respect to the best alternative element one could have chosen in hindsight. In fact, this was precisely the technique we used to attack the adversarial bandit learning problem (the Exp3 algorithm is a multiplicative weight scheme). See this lengthy technical survey of Arora and Kale for a research-level discussion of the algorithm and its applications.

Now let’s remind ourselves of the formal definition of PAC. If you’ve read the previous post on the PAC model, this next section will be redundant.

Distributions, hypotheses, and targets

In PAC-learning you are trying to give labels to data from some set X. There is a distribution D producing data from X, and it’s used for everything: to provide data the algorithm uses to learn, to measure your accuracy, and every other time you might get samples from X. You as the algorithm designer don’t know what D is, and a successful learning algorithm has to work no matter what D is. There’s some unknown function c called the target concept, which assigns a \pm 1 label to each data point in X. The target is the function we’re trying to “learn.” When the algorithm draws an example from D, it’s allowed to query the label c(x) and use all of the labels it’s seen to come up with some hypothesis h that is used for new examples that the algorithm may not have seen before. The problem is “solved” if h has low error on all of D.

To give a concrete example let’s do spam emails. Say that X is the set of all emails, and D is the distribution over emails that get sent to my personal inbox. A PAC-learning algorithm would take all my emails, along with my classification of which are spam and which are not spam (plus and minus 1). The algorithm would produce a hypothesis h that can be used to label new emails, and if the algorithm is truly a PAC-learner, then our guarantee is that with high probability (over the randomness in which emails I receive) the algorithm will produce an h that has low error on the entire distribution of emails that get sent to me (relative to my personal spam labeling function).

Of course there are practical issues with this model. I don’t have a consistent function for calling things spam, the distribution of emails I get and my labeling function can change over time, and emails don’t come according to a distribution with independent random draws. But that’s the theoretical model, and we can hope that algorithms we devise for this model happen to work well in practice.

Here’s the formal definition of the error of a hypothesis h(x) produced by the learning algorithm:

\textup{err}_{c,D}(h) = P_{x \sim D}(h(x) \neq c(x))

It’s read “The error of h with respect to the concept c we’re trying to learn and the distribution D is the probability over x drawn from D that the hypothesis produces the wrong label.” We can now define PAC-learning formally, introducing the parameters \delta for “probably” and \varepsilon for “approximately.” Let me say it informally first:

An algorithm PAC-learns if, for any \varepsilon, \delta > 0 and any distribution D, with probability at least 1-\delta the hypothesis h produced by the algorithm has error at most \varepsilon.

To flush out the other things hiding, here’s the full definition.

Definition (PAC): An algorithm A(\varepsilon, \delta) is said to PAC-learn the concept class H over the set X if, for any distribution D over X and for any 0 < \varepsilon, \delta < 1/2 and for any target concept c \in H, the probability that A produces a hypothesis h of error at most \varepsilon is at least 1-\delta. In symbols, \Pr_D(\textup{err}_{c,D}(h) \leq \varepsilon) > 1 - \delta. Moreover, A must run in time polynomial in 1/\varepsilon, 1/\delta and n, where n is the size of an element x \in X.

The reason we need a class of concepts (instead of just one target concept) is that otherwise we could just have a constant algorithm that outputs the correct labeling function. Indeed, when we get a problem we ask whether there exists an algorithm that can solve it. I.e., a problem is “PAC-learnable” if there is some algorithm that learns it as described above. With just one target concept there can exist an algorithm to solve the problem by hard-coding a description of the concept in the source code. So we need to have some “class of possible answers” that the algorithm is searching through so that the algorithm actually has a job to do.

We call an algorithm that gets this guarantee a strong learner. A weak learner has the same definition, except that we replace \textup{err}_{c,D}(h) \leq \varepsilon by the weak error bound: for some fixed 0 < \eta < 1/2. the error \textup{err}_{c,D}(h) \leq 1/2 - \eta. So we don’t require the algorithm to achieve any desired accuracy, it just has to get some accuracy slightly better than random guessing, which we don’t get to choose. As we will see, the value of \eta influences the convergence of the boosting algorithm. One important thing to note is that \eta is a constant independent of n, the size of an example, and m, the number of examples. In particular, we need to avoid the “degenerate” possibility that \eta(n) = 2^{-n} so that as our learning problem scales the quality of the weak learner degrades toward 1/2. We want it to be bounded away from 1/2.

So just to clarify all the parameters floating around, \delta will always be the “probably” part of PAC, \varepsilon is the error bound (the “approximately” part) for strong learners, and \eta is the error bound for weak learners.

What could a weak learner be?

Now before we prove that you can “boost” a weak learner to a strong learner, we should have some idea of what a weak learner is. Informally, it’s just a ‘rule of thumb’ that you can somehow guarantee does a little bit better than random guessing.

In practice, however, people sort of just make things up and they work. It’s kind of funny, but until recently nobody has really studied what makes a “good weak learner.” They just use an example like the one we’re about to show, and as long as they get a good error rate they don’t care if it has any mathematical guarantees. Likewise, they don’t expect the final “boosted” algorithm to do arbitrarily well, they just want low error rates.

The weak learner we’ll use in this post produces “decision stumps.” If you know what a decision tree is, then a decision stump is trivial: it’s a decision tree where the whole tree is just one node. If you don’t know what a decision tree is, a decision stump is a classification rule of the form:

Pick some feature i and some value of that feature v, and output label +1 if the input example has value v for feature i, and output label -1 otherwise.

Concretely, a decision stump might mark an email spam if it contains the word “viagra.” Or it might deny a loan applicant a loan if their credit score is less than some number.

Our weak learner produces a decision stump by simply looking through all the features and all the values of the features until it finds a decision stump that has the best error rate. It’s brute force, baby! Actually we’ll do something a little bit different. We’ll make our data numeric and look for a threshold of the feature value to split positive labels from negative labels. Here’s the Python code we’ll use in this post for boosting. This code was part of a collaboration with my two colleagues Adam Lelkes and Ben Fish. As usual, all of the code used in this post is available on Github.

First we make a class for a decision stump. The attributes represent a feature, a threshold value for that feature, and a choice of labels for the two cases. The classify function shows how simple the hypothesis is.

[code language=”python”]
class Stump:
def __init__(self):
self.gtLabel = None
self.ltLabel = None
self.splitThreshold = None
self.splitFeature = None

def classify(self, point):
if point[self.splitFeature] >= self.splitThreshold:
return self.gtLabel
else:
return self.ltLabel

def __call__(self, point):
return self.classify(point)
[/code]

Then for a fixed feature index we’ll define a function that computes the best threshold value for that index.

[code language=”python”]
def minLabelErrorOfHypothesisAndNegation(data, h):
posData, negData = ([(x, y) for (x, y) in data if h(x) == 1],
[(x, y) for (x, y) in data if h(x) == -1])

posError = sum(y == -1 for (x, y) in posData) + sum(y == 1 for (x, y) in negData)
negError = sum(y == 1 for (x, y) in posData) + sum(y == -1 for (x, y) in negData)
return min(posError, negError) / len(data)

def bestThreshold(data, index, errorFunction):
”’Compute best threshold for a given feature. Returns (threshold, error)”’

thresholds = [point[index] for (point, label) in data]
def makeThreshold(t):
return lambda x: 1 if x[index] >= t else -1
errors = [(threshold, errorFunction(data, makeThreshold(threshold))) for threshold in thresholds]
return min(errors, key=lambda p: p[1])
[/code]

Here we allow the user to provide a generic error function that the weak learner tries to minimize, but in our case it will just be minLabelErrorOfHypothesisAndNegation. In words, our threshold function will label an example as +1 if feature i has value greater than the threshold and -1 otherwise. But we might want to do the opposite, labeling -1 above the threshold and +1 below. The bestThreshold function doesn’t care, it just wants to know which threshold value is the best. Then we compute what the right hypothesis is in the next function.

[code language=”python”]
def buildDecisionStump(drawExample, errorFunction=defaultError):
# find the index of the best feature to split on, and the best threshold for
# that index. A labeled example is a pair (example, label) and drawExample()
# accepts no arguments and returns a labeled example.

data = [drawExample() for _ in range(500)]

bestThresholds = [(i,) + bestThreshold(data, i, errorFunction) for i in range(len(data[0][0]))]
feature, thresh, _ = min(bestThresholds, key = lambda p: p[2])

stump = Stump()
stump.splitFeature = feature
stump.splitThreshold = thresh
stump.gtLabel = majorityVote([x for x in data if x[0][feature] >= thresh])
stump.ltLabel = majorityVote([x for x in data if x[0][feature] < thresh])

return stump
[/code]

It’s a little bit inefficient but no matter. To illustrate the PAC framework we emphasize that the weak learner needs nothing except the ability to draw from a distribution. It does so, and then it computes the best threshold and creates a new stump reflecting that. The majorityVote function just picks the most common label of examples in the list. Note that drawing 500 samples is arbitrary, and in general we might increase it to increase the success probability of finding a good hypothesis. In fact, when proving PAC-learning theorems the number of samples drawn often depends on the accuracy and confidence parameters \varepsilon, \delta. We omit them here for simplicity.

Strong learners from weak learners

So suppose we have a weak learner A for a concept class H, and for any concept c from H it can produce with probability at least 1 - \delta a hypothesis h with error bound 1/2 - \eta. How can we modify this algorithm to get a strong learner? Here is an idea: we can maintain a large number of separate instances of the weak learner A, run them on our dataset, and then combine their hypotheses with a majority vote. In code this might look like the following python snippet. For now examples are binary vectors and the labels are \pm 1, so the sign of a real number will be its label.

[code language=”python”]
def boost(learner, data, rounds=100):
m = len(data)
learners = [learner(random.choice(data, m/rounds)) for _ in range(rounds)]

def hypothesis(example):
return sign(sum(1/rounds * h(example) for h in learners))

return hypothesis
[/code]

This is a bit too simplistic: what if the majority of the weak learners are wrong? In fact, with an overly naive mindset one might imagine a scenario in which the different instances of A have high disagreement, so is the prediction going to depend on which random subset the learner happens to get? We can do better: instead of taking a majority vote we can take a weighted majority vote. That is, give the weak learner a random subset of your data, and then test its hypothesis on the data to get a good estimate of its error. Then you can use this error to say whether the hypothesis is any good, and give good hypotheses high weight and bad hypotheses low weight (proportionally to the error). Then the “boosted” hypothesis would take a weighted majority vote of all your hypotheses on an example. This might look like the following.

[code language=”python”]
# data is a list of (example, label) pairs
def error(hypothesis, data):
return sum(1 for x,y in data if hypothesis(x) != y) / len(data)

def boost(learner, data, rounds=100):
m = len(data)
weights = [0] * rounds
learners = [None] * rounds

for t in range(rounds):
learners[t] = learner(random.choice(data, m/rounds))
weights[t] = 1 – error(learners[t], data)

def hypothesis(example):
return sign(sum(weight * h(example) for (h, weight) in zip(learners, weights)))

return hypothesis
[/code]

This might be better, but we can do something even cleverer. Rather than use the estimated error just to say something about the hypothesis, we can identify the mislabeled examples in a round and somehow encourage A to do better at classifying those examples in later rounds. This turns out to be the key insight, and it’s why the algorithm is called AdaBoost (Ada stands for “adaptive”). We’re adaptively modifying the distribution over the training data we feed to A based on which data A learns “easily” and which it does not. So as the boosting algorithm runs, the distribution given to A has more and more probability weight on the examples that A misclassified. And, this is the key, A has the guarantee that it will weak learn no matter what the distribution over the data is. Of course, it’s error is also measured relative to the adaptively chosen distribution, and the crux of the argument will be relating this error to the error on the original distribution we’re trying to strong learn.

To implement this idea in mathematics, we will start with a fixed sample X = \{x_1, \dots, x_m\} drawn from D and assign a weight 0 \leq \mu_i \leq 1 to each x_i. Call c(x) the true label of an example. Initially, set \mu_i to be 1. Since our dataset can have repetitions, normalizing the \mu_i to a probability distribution gives an estimate of D. Now we’ll pick some “update” parameter \zeta > 1 (this is intentionally vague). Then we’ll repeat the following procedure for some number of rounds t = 1, \dots, T.

  1. Renormalize the \mu_i to a probability distribution.
  2. Train the weak learner A, and provide it with a simulated distribution D' that draws examples x_i according to their weights \mu_i. The weak learner outputs a hypothesis h_t.
  3. For every example x_i mislabeled by h_t, update \mu_i by replacing it with \mu_i \zeta.
  4. For every correctly labeled example replace \mu_i with \mu_i / \zeta.

At the end our final hypothesis will be a weighted majority vote of all the h_t, where the weights depend on the amount of error in each round. Note that when the weak learner misclassifies an example we increase the weight of that example, which means we’re increasing the likelihood it will be drawn in future rounds. In particular, in order to maintain good accuracy the weak learner will eventually have to produce a hypothesis that fixes its mistakes in previous rounds. Likewise, when examples are correctly classified, we reduce their weights. So examples that are “easy” to learn are given lower emphasis. And that’s it. That’s the prize-winning idea. It’s elegant, powerful, and easy to understand. The rest is working out the values of all the parameters and proving it does what it’s supposed to.

The details and a proof

Let’s jump straight into a Python program that performs boosting.

First we pick a data representation. Examples are pairs (x,c(x)) whose type is the tuple (object, int). Our labels will be \pm 1 valued. Since our algorithm is entirely black-box, we don’t need to assume anything about how the examples X are represented. Our dataset is just a list of labeled examples, and the weights are floats. So our boosting function prototype looks like this

[code language=”python”]
# boost: [(object, int)], learner, int -> (object -> int)
# boost the given weak learner into a strong learner
def boost(examples, weakLearner, rounds):

[/code]

And a weak learner, as we saw for decision stumps, has the following function prototype.

[code language=”python”]
# weakLearner: (() -> (list, label)) -> (list -> label)
# accept as input a function that draws labeled examples from a distribution,
# and output a hypothesis list -> label
def weakLearner(draw):

return hypothesis
[/code]

Assuming we have a weak learner, we can fill in the rest of the boosting algorithm with some mysterious details. First, a helper function to compute the weighted error of a hypothesis on some exmaples. It also returns the correctness of the hypothesis on each example which we’ll use later.

[code language=”python”]
# compute the weighted error of a given hypothesis on a distribution
# return all of the hypothesis results and the error
def weightedLabelError(h, examples, weights):
hypothesisResults = [h(x)*y for (x,y) in examples] # +1 if correct, else -1
return hypothesisResults, sum(w for (z,w) in zip(hypothesisResults, weights) if z < 0)
[/code]

Next we have the main boosting algorithm. Here draw is a function that accepts as input a list of floats that sum to 1 and picks an index proportional to the weight of the entry at that index.

[code language=”python”]
def boost(examples, weakLearner, rounds):
distr = normalize([1.] * len(examples))
hypotheses = [None] * rounds
alpha = [0] * rounds

for t in range(rounds):
def drawExample():
return examples[draw(distr)]

hypotheses[t] = weakLearner(drawExample)
hypothesisResults, error = computeError(hypotheses[t], examples, distr)

alpha[t] = 0.5 * math.log((1 – error) / (.0001 + error))
distr = normalize([d * math.exp(-alpha[t] * h)
for (d,h) in zip(distr, hypothesisResults)])
print("Round %d, error %.3f" % (t, error))

def finalHypothesis(x):
return sign(sum(a * h(x) for (a, h) in zip(alpha, hypotheses)))

return finalHypothesis
[/code]

The code is almost clear. For each round we run the weak learner on our hand-crafted distribution. We compute the error of the resulting hypothesis on that distribution, and then we update the distribution in this mysterious way depending on some alphas and logs and exponentials. In particular, we use the expression c(x) h(x), the product of the true label and predicted label, as computed in weightedLabelError. As the comment says, this will either be +1 or -1 depending on whether the predicted label is correct or incorrect, respectively. The choice of those strange logarithms and exponentials are the result of some optimization: they allow us to minimize training error as quickly as possible (we’ll see this in the proof to follow). The rest of this section will prove that this works when the weak learner is correct. One small caveat: in the proof we will assume the error of the hypothesis is not zero (because a weak learner is not supposed to return a perfect hypothesis!), but in practice we want to avoid dividing by zero so we add the small 0.0001 to avoid that. As a quick self-check: why wouldn’t we just stop in the middle and output that “perfect” hypothesis? (What distribution is it “perfect” over? It might not be the original distribution!)

If we wanted to define the algorithm in pseudocode (which helps for the proof) we would write it this way. Given T rounds, start with D_1 being the uniform distribution over labeled input examples X, where x has label c(x). Say there are m input examples.

  1. For each t=1, \dots T:
    1. Let h_t be the weak learning algorithm run on D_t.
    2. Let \varepsilon_t be the error of h_t on D_t.
    3. Let \alpha_t = \frac{1}{2} \log ((1- \varepsilon) / \varepsilon).
    4. Update each entry of D_{t+1} by the rule D_{t+1}(x) = \frac{D_t(x)}{Z_t} e^{- h_t(x) c(x) \alpha_t}, where Z_t is chosen to normalize D_{t+1} to a distribution.
  2. Output as the final hypothesis the sign of h(x) = \sum_{t=1}^T \alpha_t h_t(x), i.e. h'(x) = \textup{sign}(h(x)).

Now let’s prove this works. That is, we’ll prove the error on the input dataset (the training set) decreases exponentially quickly in the number of rounds. Then we’ll run it on an example and save generalization error for the next post. Over many years this algorithm and tweaked so that the proof is very straightforward.

Theorem: If AdaBoost is given a weak learner and stopped on round t, and the edge \eta_t over random choice satisfies \varepsilon_t = 1/2 - \eta_t, then the training error of the AdaBoost is at most e^{-2 \sum_t \eta_t^2}.

Proof. Let m be the number of examples given to the boosting algorithm. First, we derive a closed-form expression for D_{t} in terms of the normalization constants Z_t. Expanding the recurrence relation gives

\displaystyle D_{t}(x) = D_1(x)\frac{e^{-\alpha_1 c(x) h_1(x)}}{Z_1} \dots \frac{e^{- \alpha_t c(x) h_t(x)}}{Z_t}

Because the starting distribution is uniform, and combining the products into a sum of the exponents, this simplifies to

\displaystyle \frac{1}{m} \frac{e^{-c(x) \sum_{s=1}^t \alpha_s h_t(x)}}{\prod_{s=1}^t Z_s} = \frac{1}{m}\frac{e^{-c(x) h(x)}}{\prod_s Z_s}

Next, we show that the training error is bounded by the product of the normalization terms \prod_{s=1}^t Z_s. This part has always seemed strange to me, that the training error of boosting depends on the factors you need to normalize a distribution. But it’s just a different perspective on the multiplicative weights scheme. If we didn’t explicitly normalize the distribution at each step, we’d get nonnegative weights (which we could convert to a distribution just for the sampling step) and the training error would depend on the product of the weight updates in each step. Anyway let’s prove it.

The training error is defined to be \frac{1}{m} (\textup{\# incorrect predictions by } h). This can be written with an indicator function as follows:

\displaystyle \frac{1}{m} \sum_{x \in X} 1_{c(x) h(x) \leq 0}

Because the sign of h(x) determines its prediction, the product is negative when h is incorrect. Now we can do a strange thing, we’re going to upper bound the indicator function (which is either zero or one) by e^{-c(x)h(x)}. This works because if h predicts correctly then the indicator function is zero while the exponential is greater than zero. On the other hand if h is incorrect the exponential is greater than one because e^z \geq 1 when z \geq 0. So we get

\displaystyle \leq \sum_i \frac{1}{m} e^{-c(x)h(x)}

and rearranging the formula for D_t from the first part gives

\displaystyle \sum_{x \in X} D_T(x) \prod_{t=1}^T Z_t

Since the D_T forms a distribution, it sums to 1 and we can factor the Z_t out. So the training error is just bounded by the \prod_{t=1}^T Z_t.

The last step is to bound the product of the normalization factors. It’s enough to show that Z_t \leq e^{-2 \eta_t^2}. The normalization constant is just defined as the sum of the numerator of the terms in step D. i.e.

\displaystyle Z_t = \sum_i D_t(i) e^{-\alpha_t c(x) h_t(x)}

We can split this up into the correct and incorrect terms (that contribute to +1 or -1 in the exponent) to get

\displaystyle Z_t = e^{-\alpha_t} \sum_{\textup{correct } x} D_t(x) + e^{\alpha_t} \sum_{\textup{incorrect } x} D_t(x)

But by definition the sum of the incorrect part of D is \varepsilon_t and 1-\varepsilon_t for the correct part. So we get

\displaystyle e^{-\alpha_t}(1-\varepsilon_t) + e^{\alpha_t} \varepsilon_t

Finally, since this is an upper bound we want to pick \alpha_t so as to minimize this expression. With a little calculus you can see the \alpha_t we chose in the algorithm pseudocode achieves the minimum, and this simplifies to 2 \sqrt{\varepsilon_t (1-\varepsilon_t)}. Plug in \varepsilon_t = 1/2 - \eta_t to get \sqrt{1 - 4 \eta_t^2} and use the calculus fact that 1 - z \leq e^{-z} to get e^{-2\eta_t^2} as desired.

\square

This is fine and dandy, it says that if you have a true weak learner then the training error of AdaBoost vanishes exponentially fast in the number of boosting rounds. But what about generalization error? What we really care about is whether the hypothesis produced by boosting has low error on the original distribution D as a whole, not just the training sample we started with.

One might expect that if you run boosting for more and more rounds, then it will eventually overfit the training data and its generalization accuracy will degrade. However, in practice this is not the case! The longer you boost, even if you get down to zero training error, the better generalization tends to be. For a long time this was sort of a mystery, and we’ll resolve the mystery in the sequel to this post. For now, we’ll close by showing a run of AdaBoost on some real world data.

The “adult” census dataset

The “adult” dataset is a standard dataset taken from the 1994 US census. It tracks a number of demographic and employment features (including gender, age, employment sector, etc.) and the goal is to predict whether an individual makes over $50k per year. Here are the first few lines from the training set.

39, State-gov, 77516, Bachelors, 13, Never-married, Adm-clerical, Not-in-family, White, Male, 2174, 0, 40, United-States, <=50K
50, Self-emp-not-inc, 83311, Bachelors, 13, Married-civ-spouse, Exec-managerial, Husband, White, Male, 0, 0, 13, United-States, <=50K
38, Private, 215646, HS-grad, 9, Divorced, Handlers-cleaners, Not-in-family, White, Male, 0, 0, 40, United-States, <=50K
53, Private, 234721, 11th, 7, Married-civ-spouse, Handlers-cleaners, Husband, Black, Male, 0, 0, 40, United-States, <=50K
28, Private, 338409, Bachelors, 13, Married-civ-spouse, Prof-specialty, Wife, Black, Female, 0, 0, 40, Cuba, <=50K
37, Private, 284582, Masters, 14, Married-civ-spouse, Exec-managerial, Wife, White, Female, 0, 0, 40, United-States, <=50K

We perform some preprocessing of the data, so that the categorical examples turn into binary features. You can see the full details in the github repository for this post; here are the first few post-processed lines (my newlines added).

[code language=”python”]
>>> from data import adult
>>> train, test = adult.load()
>>> train[:3]
[((39, 1, 0, 0, 0, 0, 0, 1, 0, 0, 13, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2174, 0, 40, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), -1),

((50, 1, 0, 1, 0, 0, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), -1),

((38, 1, 1, 0, 0, 0, 0, 0, 0, 0, 9, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 40, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), -1)]
[/code]

Now we can run boosting on the training data, and compute its error on the test data.

[code language=”python”]
>>> from boosting import boost
>>> from data import adult
>>> from decisionstump import buildDecisionStump
>>> train, test = adult.load()
>>> weakLearner = buildDecisionStump
>>> rounds = 20
>>> h = boost(train, weakLearner, rounds)
Round 0, error 0.199
Round 1, error 0.231
Round 2, error 0.308
Round 3, error 0.380
Round 4, error 0.392
Round 5, error 0.451
Round 6, error 0.436
Round 7, error 0.459
Round 8, error 0.452
Round 9, error 0.432
Round 10, error 0.444
Round 11, error 0.447
Round 12, error 0.450
Round 13, error 0.454
Round 14, error 0.505
Round 15, error 0.476
Round 16, error 0.484
Round 17, error 0.500
Round 18, error 0.493
Round 19, error 0.473
>>> error(h, train)
0.153343
>>> error(h, test)
0.151711
[/code]

This isn’t too shabby. I’ve tried running boosting for more rounds (a hundred) and the error doesn’t seem to improve by much. This implies that finding the best decision stump is not a weak learner (or at least it fails for this dataset), and we can see that indeed the training errors across rounds roughly tend to 1/2.

Though we have not compared our results above to any baseline, AdaBoost seems to work pretty well. This is kind of a meta point about theoretical computer science research. One spends years trying to devise algorithms that work in theory (and finding conditions under which we can get good algorithms in theory), but when it comes to practice we can’t do anything but hope the algorithms will work well. It’s kind of amazing that something like Boosting works in practice. It’s not clear to me that weak learners should exist at all, even for a given real world problem. But the results speak for themselves.

Next time

Next time we’ll get a bit deeper into the theory of boosting. We’ll derive the notion of a “margin” that quantifies the confidence of boosting in its prediction. Then we’ll describe (and maybe prove) a theorem that says if the “minimum margin” of AdaBoost on the training data is large, then the generalization error of AdaBoost on the entire distribution is small. The notion of a margin is actually quite a deep one, and it shows up in another famous machine learning technique called the Support Vector Machine. In fact, it’s part of some recent research I’ve been working on as well. More on that in the future.

If you’re dying to learn more about Boosting, but don’t want to wait for me, check out the book Boosting: Foundations and Algorithms, by Freund and Schapire.

Until next time!

Learning a single-variable polynomial, or the power of adaptive queries

Problem: Alice chooses a secret polynomial p(x) with nonnegative integer coefficients. Bob wants to discover this polynomial by querying Alice for the value of p(x) for some integer x of Bob’s choice. What is the minimal number of queries Bob needs to determine p(x) exactly?

Solution: Two queries. The first is p(1), and if we call N = p(1) + 1, then the second query is p(N).

To someone who is familiar with polynomials, this may seem shocking, and I’ll explain why it works in a second. After all, it’s very easy to prove that if Bob gives Alice all of his queries at the same time (if the queries are not adaptive), then it’s impossible to discover what p(x) is using fewer than \textup{deg}(p) + 1 queries. This is due to a fact called polynomial interpolation, which we’ve seen on this blog before in the context of secret sharing. Specifically, there is a unique single-variable degree d polynomial passing through d+1 points (with distinct x-values). So if you knew the degree of p, you could determine it easily. But Bob doesn’t know the degree of the polynomial, and there’s no way he can figure it out without adaptive queries! Indeed, if Bob tries and gives a set of d queries, Alice could have easily picked a polynomial of degree d+1. So it’s literally impossible to solve this problem without adaptive queries.

The lovely fact is that once you allow adaptiveness, the number of queries you need doesn’t even depend on the degree of the secret polynomial!

Okay let’s get to the solution. It was crucial that our polynomial had nonnegative integer coefficients, because we’re going to do a tiny bit of number theory. Let p(x) = a_0 + a_1 x + \dots + a_d x^d. First, note that p(1) is exactly the sum of the coefficients \sum_i a_i, and in particular p(1) + 1 is larger than any single coefficient. So call this N, and query p(N). This gives us a number y_0 of the form

\displaystyle y_0 = a_0 + a_1N + a_2N^2 + \dots + a_dN^d

And because N is so big, we can compute a_0 easily by computing y_0 \mod N. Now set y_1 = (y_0 - a_0) / N, and this has the form a_1 + a_2N + \dots + a_dN^{d-1}. We can compute modulus again to get a_1, and repeat until we have all the coefficients. We’ll stop once we get a y_i that is zero.

[Addendum 2018-02-14: implementation on github]

As a small technical note, this is a polynomial-time algorithm in the number of bits needed to write down p(x). So this demonstrates the power of adaptive queries: we get from something which is uncomputable with any number of queries to something which is efficiently computable with a constant number of queries.

The obvious follow-up question is: can you come up with an efficient algorithm if we allow the coefficients to be negative integers?

Occam’s Razor and PAC-learning

So far our discussion of learning theory has been seeing the definition of PAC-learningtinkering with it, and seeing simple examples of learnable concept classes. We’ve said that our real interest is in proving big theorems about what big classes of problems can and can’t be learned. One major tool for doing this with PAC is the concept of VC-dimension, but to set the stage we’re going to prove a simpler theorem that gives a nice picture of PAC-learning when your hypothesis class is small. In short, the theorem we’ll prove says that if you have a finite set of hypotheses to work with, and you can always find a hypothesis that’s consistent with the data you’ve seen, then you can learn efficiently. It’s obvious, but we want to quantify exactly how much data you need to ensure low error. This will also give us some concrete mathematical justification for philosophical claims about simplicity, and the theorems won’t change much when we generalize to VC-dimension in a future post.

The Chernoff bound

One tool we will need in this post, which shows up all across learning theory, is the Chernoff-Hoeffding bound. We covered this famous inequality in detail previously on this blog, but the part of that post we need is the following theorem that says, informally, that if you average a bunch of bounded random variables, then the probability this average random variable deviates from its expectation is exponentially small in the amount of deviation. Here’s the slightly simplified version we’ll use:

Theorem: Let X_1, \dots, X_m be independent random variables whose values are in the range [0,1]. Call \mu_i = \mathbf{E}[X_i], X = \sum_i X_i, and \mu = \mathbf{E}[X] = \sum_i \mu_i. Then for all t > 0,

\displaystyle \Pr(|X-\mu| > t) \leq 2e^{-2t^2 / m}

One nice thing about the Chernoff bound is that it doesn’t matter how the variables are distributed. This is important because in PAC we need guarantees that hold for any distribution generating data. Indeed, in our case the random variables above will be individual examples drawn from the distribution generating the data. We’ll be estimating the probability that our hypothesis has error deviating more than \varepsilon, and we’ll want to bound this by \delta, as in the definition of PAC-learning. Since the amount of deviation (error) and the number of samples (m) both occur in the exponent, the trick is in balancing the two values to get what we want.

Realizability and finite hypothesis classes

Let’s recall the PAC model once more. We have a distribution D generating labeled examples (x, c(x)), where c is an unknown function coming from some concept class C. Our algorithm can draw a polynomial number of these examples, and it must produce a hypothesis h from some hypothesis class H (which may or may not contain c). The guarantee we need is that, for any \delta, \varepsilon > 0, the algorithm produces a hypothesis whose error on D is at most \varepsilon, and this event happens with probability at least 1-\delta. All of these probabilities are taken over the randomness in the algorithm’s choices and the distribution D, and it has to work no matter what the distribution D is.

Let’s introduce some simplifications. First, we’ll assume that the hypothesis and concept classes H and C are finite. Second, we’ll assume that C \subset H, so that you can actually hope to find a hypothesis of zero error. This is called realizability. Later we’ll relax these first two assumptions, but they make the analysis a bit cleaner. Finally, we’ll assume that we have an algorithm which, when given labeled examples, can find in polynomial time a hypothesis h \in H that is consistent with every example.

These assumptions give a trivial learning algorithm: draw a bunch of examples and output any consistent hypothesis. The question is, how many examples do we need to guarantee that the hypothesis we find has the prescribed generalization error? It will certainly grow with 1 / \varepsilon, but we need to ensure it will only grow polynomially fast in this parameter. Indeed, realizability is such a strong assumption that we can prove a polynomial bound using even more basic probability theory than the Chernoff bound.

Theorem: A algorithm that efficiently finds a consistent hypothesis will PAC-learn any finite concept class provided it has at least m samples, where

\displaystyle m \geq \frac{1}{\varepsilon} \left ( \log |H| + \log \left ( \frac{1}{\delta} \right ) \right )

Proof. All we need to do is bound the probability that a bad hypothesis (one with error more than \varepsilon) is consistent with the given data. Now fix D, c, \delta, \varepsilon, and draw m examples and let h be any hypothesis that is consistent with the drawn examples. Suppose that the bad thing happens, that \Pr_D(h(x) \neq c(x)) > \varepsilon.

Because the examples are all drawn independently from D, the chance that all m examples are consistent with h is

\displaystyle (1 - \Pr_{x \sim D}(h(x) \neq c(x)))^m < (1 - \varepsilon)^m

What we’re saying here is, the probability that a specific bad hypothesis is actually consistent with your drawn examples is exponentially small in the error tolerance. So if we apply the union bound, the probability that some hypothesis you could produce is bad is at most (1 - \varepsilon)^m S, where S is the number of hypotheses the algorithm might produce.

A crude upper bound on the number of hypotheses you could produce is just the total number of hypotheses, |H|. Even cruder, let’s use the inequality (1 - x) < e^{-x} to give the bound

\displaystyle (1 - \varepsilon)^m |H| < e^{-\varepsilon m} |H|

Now we want to make sure that this probability, the probability of choosing a high-error (yet consistent) hypothesis, is at most \delta. So we can set the above quantity less than \delta and solve for m:

\displaystyle e^{-\varepsilon m} |H| \leq \delta

Taking logs and solving for m gives the desired bound.

\square

An obvious objection is: what if you aren’t working with a hypothesis class where you can guarantee that you’ll find a consistent hypothesis? Well, in that case we’ll need to inspect the definition of PAC again and reevaluate our measures of error. It turns out we’ll get a similar theorem as above, but with the stipulation that we’re only achieving error within epsilon of the error of the best available hypothesis.

But before we go on, this theorem has some deep philosophical interpretations. In particular, suppose that, before drawing your data, you could choose to work with one of two finite hypothesis classes H_1, H_2, with |H_1| > |H_2|. If you can find a consistent hypothesis no matter which hypothesis class you use, then this theorem says that your generalization guarantees are much stronger if you start with the smaller hypothesis class.

In other words, all else being equal, the smaller set of hypotheses is better. For this reason, the theorem is sometimes called the “Occam’s Razor” theorem. We’ll see a generalization of this theorem in the next section.

Unrealizability and an extra epsilon

Now suppose that $H$ doesn’t contain any hypotheses with error less than \varepsilon. What can we hope to do in this case? One thing is that we can hope to find a hypothesis whose error is within \varepsilon of the minimal error of any hypothesis in H. Moreover, we might not have any consistent hypotheses for some data samples! So rather than require an algorithm to produce an h \in H that is perfectly consistent with the data, we just need it to produce a hypothesis that has minimal empirical error, in the sense that it is as close to consistent as the best hypothesis of h on the data you happened to draw. It seems like such a strategy would find you a hypothesis that’s close to the best one in H, but we need to prove it and determine how many samples we need to draw to succeed.

So let’s make some definitions to codify this. For a given hypothesis, call \textup{err}(h) the true error of h on the distribution D. Our assumption is that there may be no hypotheses in H with \textup{err}(h) = 0. Next we’ll call the empirical error \hat{\textup{err}}(h).

Definition: We say a concept class C is agnostically learnable using the hypothesis class H if for all c \in C and all distributions D (and all \varepsilon, \delta > 0), there is a learning algorithm A which produces a hypothesis h that with probability at least 1 - \delta satisfies

\displaystyle \text{err}(h) \leq \min_{h' \in H} \text{err}(h') + \varepsilon

and everything runs in the same sort of polynomial time as for vanilla PAC-learning. This is called the agnostic setting or the unrealizable setting, in the sense that we may not be able to find a hypothesis with perfect empirical error.

We seek to prove that all concept classes are agnostically learnable with a finite hypothesis class, provided you have an algorithm that can minimize empirical error. But actually we’ll prove something stronger.

Theorem: Let H be a finite hypothesis class and m the number of samples drawn. Then for any \delta > 0, with probability 1-\delta the following holds:

\displaystyle \forall h \in H, \hat{\text{err}}(h) \leq \text{err}(h) + \sqrt{\frac{\log |H| + \log(2 / \delta)}{2m}}

In other words, we can precisely quantify how the empirical error converges to the true error as the number of samples grows. But this holds for all hypotheses in H, so this provides a uniform bound of the difference between true and empirical error for the entire hypothesis class.

Proving this requires the Chernoff bound. Fix a single hypothesis h \in H. If you draw an example x, call Z the random variable which is 1 when h(x) \neq c(x), and 0 otherwise. So if you draw m samples and call the i-th variable Z_i, the empirical error of the hypothesis is \frac{1}{m}\sum_i Z_i. Moreover, the actual error is the expectation of this random variable since \mathbf{E}[1/m \sum_i Z_i] = Z.

So what we’re asking is the probability that the empirical error deviates from the true error by a lot. Let’s call “a lot” some parameter \varepsilon/2 > 0 (the reason for dividing by two will become clear in the corollary to the theorem). Then plugging things into the Chernoff-Hoeffding bound gives a bound on the probability of the “bad event,” that the empirical error deviates too much.

\displaystyle \Pr[|\hat{\text{err}}(h) - \text{err}(h)| > \varepsilon / 2] < 2e^{-\frac{\varepsilon^2m}{2}}

Now to get a bound on the probability that some hypothesis is bad, we apply the union bound and use the fact that |H| is finite to get

\displaystyle \Pr[|\hat{\text{err}}(h) - \text{err}(h)| > \varepsilon / 2] < 2|H|e^{-\frac{\varepsilon^2m}{2}}

Now say we want to bound this probability by \delta. We set 2|H|e^{-\varepsilon^2m/2} \leq \delta, solve for m, and get

\displaystyle m \geq \frac{2}{\varepsilon^2}\left ( \log |H| + \log \frac{2}{\delta} \right )

This gives us a concrete quantification of the tradeoff between m, \varepsilon, \delta, and |H|. Indeed, if we pick m to be this large, then solving for \varepsilon / 2 gives the exact inequality from the theorem.

\square

Now we know that if we pick enough samples (polynomially many in all the parameters), and our algorithm can find a hypothesis h of minimal empirical error, then we get the following corollary:

Corollary: For any \varepsilon, \delta > 0, the algorithm that draws m \geq \frac{2}{\varepsilon^2}(\log |H| + \log(2/ \delta)) examples and finds any hypothesis of minimal empirical error will, with probability at least 1-\delta, produce a hypothesis that is within \varepsilon of the best hypothesis in H.

Proof. By the previous theorem, with the desired probability, for all h \in H we have |\hat{\text{err}}(h) - \text{err}(h)| < \varepsilon/2. Call g = \min_{h' \in H} \text{err}(h'). Then because the empirical error of h is also minimal, we have |\hat{\text{err}}(g) - \text{err}(h)| < \varepsilon / 2. And using the previous theorem again and the triangle inequality, we get |\text{err}(g) - \text{err}(h)| < 2 \varepsilon / 2 = \varepsilon. In words, the true error of the algorithm’s hypothesis is close to the error of the best hypothesis, as desired.

\square

Next time

Both of these theorems tell us something about the generalization guarantees for learning with hypothesis classes of a certain size. But this isn’t exactly the most reasonable measure of the “complexity” of a family of hypotheses. For example, one could have a hypothesis class with a billion intervals on \mathbb{R} (say you’re trying to learn intervals, or thresholds, or something easy), and the guarantees we proved in this post are nowhere near optimal.

So the question is: say you have a potentially infinite class of hypotheses, but the hypotheses are all “simple” in some way. First, what is the right notion of simplicity? And second, how can you get guarantees based on that analogous to these? We’ll discuss this next time when we define the VC-dimension.

Until then!