munotes®

Practical 6: Adaboost Ensemble Learning

Get access to whole semester resourcesSemester Pass

Chapter Nine

Syllabus topic Module 1, "Adaboost Ensemble Learning: Implement the Adaboost algorithm to create an ensemble of weak classifiers. Train the ensemble model on a given dataset and evaluate its performance. Compare the results with individual weak classifiers."

Pages 69 to 75 of 206

Aim

To implement Adaboost over a set of weak classifiers, to train the ensemble on a dataset, to measure it, and to compare it with the weak classifiers taken one at a time.

What you need to know before you start

A weak classifier is one that is only a little better than guessing. The standard one, and the one used here, is a decision stump: a decision tree one level deep. It looks at one feature, compares it with one threshold, and answers. That is all it can do.

Adaboost turns a crowd of them into one good classifier, and it does it by making each new member concentrate on what the previous ones got wrong.

Every training row carries a weight. At the start all the weights are equal. Then, round after round:

  1. Find the stump with the smallest weighted error: the total weight of the rows it gets wrong.
  2. Give that stump a voice, called alpha, from its error: alpha = 0.5 * ln((1 - error) / error).
  3. Raise the weight of every row it got wrong and lower the weight of every row it got right, then normalise so the weights add to 1.
  4. Repeat.

The finished ensemble answers by a weighted vote: add up alpha * (that stump's answer) over all the stumps and take the sign.

Two things about alpha are worth a line each in the journal. A stump with error near 0 gets a very large alpha, because it deserves to be listened to. A stump with error near 0.5 gets an alpha near zero, because a classifier that is right half the time carries no information at all. And a stump with error above 0.5 gets a negative alpha, which is the algorithm saying "believe the opposite of this one", which is also useful.

The dataset

The thirty-student file is too easy for this exercise: one stump on attendance already scores 0.905 on it, and there is nothing for an ensemble to add. So this practical uses a larger file, 120 rows, whose Pass line depends on both features together, with six per cent of the labels deliberately flipped. No single threshold on one feature can do well on it: the best possible stump over the whole file scores 0.767.

attendance,practice,result
71,9,Fail
36,4,Fail
42,23,Fail
94,13,Pass
85,26,Pass
41,35,Fail
45,14,Fail
37,36,Fail
36,14,Pass
47,18,Fail
99,7,Fail
53,6,Fail
54,23,Fail
38,36,Pass
56,31,Fail
84,49,Pass
88,23,Pass
53,44,Pass
40,36,Fail
93,21,Pass
66,38,Pass
45,32,Fail
73,9,Fail
83,2,Fail
39,48,Pass
70,21,Fail
93,37,Pass
38,5,Fail
90,44,Pass
37,46,Fail
87,18,Fail
74,1,Fail
75,10,Fail
93,3,Fail
66,8,Fail
80,25,Pass
93,5,Fail
81,35,Pass
47,27,Fail
65,45,Pass
75,43,Pass
59,9,Fail
49,14,Fail
31,31,Fail
53,16,Fail
48,26,Fail
70,8,Fail
95,39,Pass
36,29,Fail
80,25,Pass
43,30,Fail
37,12,Fail
56,28,Fail
73,38,Fail
30,36,Fail
42,23,Fail
39,13,Fail
49,40,Fail
74,38,Pass
45,7,Fail
89,30,Pass
40,9,Fail
73,47,Pass
50,33,Pass
97,23,Pass
99,1,Fail
68,41,Pass
63,33,Fail
51,22,Fail
98,34,Pass
72,40,Pass
54,15,Fail
59,12,Fail
75,46,Fail
33,50,Pass
63,12,Fail
74,28,Pass
74,23,Fail
43,14,Fail
73,13,Fail
30,30,Fail
74,41,Pass
45,24,Fail
55,30,Fail
85,50,Pass
41,46,Pass
81,47,Pass
50,10,Fail
33,9,Fail
89,41,Pass
90,42,Pass
49,35,Fail
32,0,Fail
43,33,Fail
47,27,Fail
54,13,Pass
57,18,Fail
71,16,Fail
46,3,Fail
75,29,Pass
96,26,Pass
94,8,Fail
97,32,Fail
86,49,Pass
30,49,Fail
52,9,Fail
45,35,Fail
96,33,Pass
43,35,Pass
54,17,Pass
42,32,Fail
33,48,Fail
38,28,Fail
94,38,Pass
65,28,Fail
91,32,Pass
96,16,Fail
55,28,Fail
45,25,Fail
39,42,Fail
munotes.in69

Practical 6: Adaboost Ensemble Learning

"""Practical 6: Adaboost over decision stumps, written from nothing."""
import csv
import math

with open("admissions.csv", newline="") as fh:
    rows = list(csv.DictReader(fh))
X = [[float(r["attendance"]), float(r["practice"])] for r in rows]
Y = [1 if r["result"] == "Pass" else -1 for r in rows]
NAMES = ["attendance", "practice"]

import random
random.seed(1)
order = list(range(len(X)))
random.shuffle(order)
cut = int(0.7 * len(X))
TRAIN, TEST = order[:cut], order[cut:]

class Stump:
    """The weakest classifier there is: one feature, one threshold, one answer."""

    def __init__(self, feature, threshold, polarity):
        self.feature, self.threshold, self.polarity = feature, threshold, polarity

    def predict(self, x):
        return self.polarity if x[self.feature] <= self.threshold else -self.polarity

    def __str__(self):
        side = "Pass" if self.polarity == 1 else "Fail"
        return "%s <= %.1f -> %s" % (NAMES[self.feature], self.threshold, side)

def best_stump(idx, weight):
    """The stump with the smallest weighted error. This is the weak learner."""
    best = None
    for f in (0, 1):
        values = sorted(set(X[i][f] for i in idx))
        cuts = [values[0] - 1] + [(a + b) / 2 for a, b in zip(values, values[1:])]
        for t in cuts:
            for p in (1, -1):
                s = Stump(f, t, p)
                err = sum(weight[i] for i in idx if s.predict(X[i]) != Y[i])
                if best is None or err < best[0]:
                    best = (err, s)
    return best

def adaboost(idx, rounds):
    n = len(idx)
    weight = {i: 1 / n for i in idx}
    learners = []
    print("%-6s %-28s %9s %9s" % ("round", "stump", "error", "alpha"))
    for r in range(1, rounds + 1):
        err, stump = best_stump(idx, weight)
        err = min(max(err, 1e-10), 1 - 1e-10)
        alpha = 0.5 * math.log((1 - err) / err)
        learners.append((alpha, stump))
        for i in idx:
            weight[i] *= math.exp(-alpha * Y[i] * stump.predict(X[i]))
        total = sum(weight.values())
        for i in idx:
            weight[i] /= total
        print("%-6d %-28s %9.4f %9.4f" % (r, str(stump), err, alpha))
    return learners, weight

def ensemble_predict(learners, x):
    return 1 if sum(a * s.predict(x) for a, s in learners) >= 0 else -1

def acc(pred, idx):
    return sum(1 for i in idx if pred(X[i]) == Y[i]) / len(idx)

print("training rows %d, test rows %d" % (len(TRAIN), len(TEST)))
print()
learners, weight = adaboost(TRAIN, 8)
print()
print("%-30s %10s %10s" % ("classifier", "train acc", "test acc"))
for k, (alpha, s) in enumerate(learners, 1):
    print("%-30s %10.3f %10.3f"
          % ("stump %d alone: %s" % (k, s), acc(s.predict, TRAIN), acc(s.predict, TEST)))
for k in range(1, len(learners) + 1):
    part = learners[:k]
    print("%-30s %10.3f %10.3f"
          % ("ensemble of %d" % k,
             acc(lambda x, p=part: ensemble_predict(p, x), TRAIN),
             acc(lambda x, p=part: ensemble_predict(p, x), TEST)))
print()
heavy = sorted(TRAIN, key=lambda i: -weight[i])[:5]
print("the five training rows carrying the most weight at the end:")
print("%12s %10s %8s %10s %12s" % ("attendance", "practice", "result", "weight", "0.5a + p"))
for i in heavy:
    print("%12s %10s %8s %10.4f %12.1f"
          % (rows[i]["attendance"], rows[i]["practice"], rows[i]["result"],
             weight[i], 0.5*X[i][0] + X[i][1]))
munotes.in70

Practical 6: Adaboost Ensemble Learning

training rows 84, test rows 36

round  stump                            error     alpha
1      practice <= 37.0 -> Fail        0.1786    0.7630
2      attendance <= 73.5 -> Fail      0.1754    0.7740
3      practice <= 12.5 -> Fail        0.3622    0.2829
4      practice <= 49.5 -> Fail        0.3418    0.3275
5      attendance <= 53.5 -> Fail      0.3470    0.3160
6      attendance <= 72.5 -> Pass      0.4046    0.1931
7      attendance <= 77.5 -> Fail      0.3835    0.2373
8      practice <= 19.5 -> Fail        0.3624    0.2826

classifier                      train acc   test acc
stump 1 alone: practice <= 37.0 -> Fail      0.821      0.611
stump 2 alone: attendance <= 73.5 -> Fail      0.798      0.694
stump 3 alone: practice <= 12.5 -> Fail      0.524      0.694
stump 4 alone: practice <= 49.5 -> Fail      0.714      0.528
stump 5 alone: attendance <= 53.5 -> Fail      0.702      0.556
stump 6 alone: attendance <= 72.5 -> Pass      0.238      0.278
stump 7 alone: attendance <= 77.5 -> Fail      0.774      0.722
stump 8 alone: practice <= 19.5 -> Fail      0.643      0.639
ensemble of 1                       0.821      0.611
ensemble of 2                       0.798      0.694
ensemble of 3                       0.905      0.750
ensemble of 4                       0.798      0.611
ensemble of 5                       0.917      0.778
ensemble of 6                       0.917      0.778
ensemble of 7                       0.905      0.778
ensemble of 8                       0.917      0.778

the five training rows carrying the most weight at the end:
  attendance   practice   result     weight     0.5a + p
          75         46     Fail     0.1350         83.5
          54         13     Pass     0.1261         40.0
          39         48     Pass     0.0293         67.5
          41         46     Pass     0.0293         66.5
          73         38     Fail     0.0287         74.5

Reading the output: the comparison MU asks for

The weak classifiers on their own are weak. The best of the eight, the second stump, scores 0.798 on the training rows and 0.694 on the test rows. The worst, the sixth, scores 0.238 and 0.278, which is far worse than guessing. That one got a very small alpha, 0.1931, and the ensemble mostly ignores it.

The ensemble beats all of them. By round 5 it reaches 0.917 training and 0.778 test, against the best single stump's 0.798 and 0.694. Eight classifiers that cannot see more than one feature at a time have, between them, learned a boundary that needs both.

The error of each new stump rises. Round 1 finds a stump with weighted error 0.179; by round 6 the best available stump has weighted error 0.405. That is not the algorithm getting worse. It is the weights moving: the easy rows have been down-weighted almost to nothing, so what is left is hard, and a weighted error near 0.5 on a hard remainder is what you expect.

munotes.in71

Practical 6: Adaboost Ensemble Learning

The ensemble stops improving. Rounds 5, 6, 7 and 8 all sit at 0.778 on the test set. Adding more weak classifiers does not help for ever, and the round at which it stops is worth reporting.

Where the weight ended up is the most interesting line in the output. The two heaviest rows are attendance 75 with 46 hours labelled Fail, and attendance 54 with 13 hours labelled Pass. Their scores on the rule the dataset was built from, 0.5 * attendance + practice, are 83.5 and 40.0, against a pass line of 65. Both are on the wrong side of their own labels: they are two of the rows whose labels were deliberately flipped. Adaboost found them, and it did so without being told they existed.

That is Adaboost's great strength and its known weakness in one observation. It concentrates on what is hard, which is how it learns a boundary no single stump can draw. But a wrong label is also hard, and Adaboost cannot tell the difference, so with noisy data it will spend round after round trying to get the noise right. On a dataset you suspect has bad labels, watch the weights: rows whose weight keeps climbing are either the interesting cases or the mistakes.

The same ensemble from scikit-learn

"""The same ensemble from scikit-learn."""
import csv
from sklearn.ensemble import AdaBoostClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split

with open("admissions.csv", newline="") as fh:
    rows = list(csv.DictReader(fh))
X = [[float(r["attendance"]), float(r["practice"])] for r in rows]
y = [r["result"] for r in rows]
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, random_state=1, stratify=y)

stump = DecisionTreeClassifier(max_depth=1, random_state=0).fit(Xtr, ytr)
print("one stump alone      : train %.3f  test %.3f" % (stump.score(Xtr, ytr), stump.score(Xte, yte)))
print()
print("%-10s %10s %10s" % ("rounds", "train acc", "test acc"))
for n in (1, 2, 5, 10, 25, 50, 100):
    m = AdaBoostClassifier(estimator=DecisionTreeClassifier(max_depth=1),
                           n_estimators=n, random_state=0).fit(Xtr, ytr)
    print("%-10d %10.3f %10.3f" % (n, m.score(Xtr, ytr), m.score(Xte, yte)))
one stump alone      : train 0.798  test 0.694

rounds      train acc   test acc
1               0.798      0.694
2               0.798      0.694
5               0.810      0.833
10              0.845      0.806
25              0.857      0.806
50              0.869      0.806
100             0.893      0.806

The same story from a different implementation: one stump 0.798 and 0.694, the ensemble 0.893 and 0.806. Note that training accuracy keeps creeping up as rounds are added, from 0.798 at one round to 0.893 at a hundred, while test accuracy stops at 0.806 after five. The extra ninety-five rounds bought nothing except the appearance of progress, and a student who reports only the training figure will report the appearance.

munotes.in72

Practical 6: Adaboost Ensemble Learning

estimator=DecisionTreeClassifier(max_depth=1) is what makes the weak learner a stump; that is scikit-learn's default for this class, and saying it explicitly is better than relying on it.

Procedure

  1. Write the Stump class: one feature, one threshold, one polarity, and a predict that returns +1 or -1.
  2. Write best_stump: try every feature, every threshold midway between two neighbouring values, and both polarities, and keep the one with the smallest weighted error.
  3. Write the Adaboost loop: compute alpha from the error, update every weight by exp(-alpha y prediction), and normalise.
  4. Run it for eight rounds and print each stump, its weighted error and its alpha.
  5. Measure each stump on its own, on both the training and the test rows.
  6. Measure the ensemble of the first k stumps for every k, and find where it stops improving.
  7. List the training rows carrying the most weight at the end, and say what they have in common.
  8. Run AdaBoostClassifier for several numbers of rounds and compare.

Observations

RoundStump chosenWeighted errorAlpha
1practice <= 37.0 -> Fail0.17860.7630
2attendance <= 73.5 -> Fail0.17540.7740
3practice <= 12.5 -> Fail0.36220.2829
4practice <= 49.5 -> Fail0.34180.3275
5attendance <= 53.5 -> Fail0.34700.3160
6attendance <= 72.5 -> Pass0.40460.1931
7attendance <= 77.5 -> Fail0.38350.2373
8practice <= 19.5 -> Fail0.36240.2826
Best stumpWorst stumpEnsemble of 5Ensemble of 8
Training accuracy0.8210.2380.9170.917
Test accuracy0.7220.2780.7780.778
scikit-learn AdaBoostClassifiertraintest
one stump alone0.7980.694
5 rounds0.8100.833
10 rounds0.8450.806
100 rounds0.8930.806

The five heaviest training rows at the end.

attendancepracticelabelweight0.5a + p
17546Fail0.135083.5
25413Pass0.126140.0
33948Pass0.029367.5
44146Pass0.029366.5
57338Fail0.028774.5

Result

Adaboost was implemented over decision stumps and trained for eight rounds on a 120-row dataset split 84 to 36. The best individual stump scored 0.821 on the training rows and 0.722 on the test rows; the ensemble reached 0.917 and 0.778 by round 5 and did not improve after it. The weighted error of each successive stump rose from 0.179 to 0.405 as the easy rows lost their weight. The two rows carrying by far the most weight at the end, 0.1350 and 0.1261 against a starting weight of 0.0119, were two of the rows whose labels had been deliberately flipped when the dataset was built. scikit-learn's AdaBoostClassifier reproduced the pattern, improving a single stump's 0.798 and 0.694 to 0.893 and 0.806.

munotes.in73

Practical 6: Adaboost Ensemble Learning

Where marks are lost

Not comparing with the individual weak classifiers. It is the third sentence of MU's exercise. Print each stump's own accuracy beside the ensemble's.

Forgetting to normalise the weights. They must add to 1 after every round, or alpha stops meaning anything.

Using error instead of weighted error. The whole algorithm is in the word weighted.

Labels 0 and 1. exp(-alpha y prediction) needs +1 and -1: with 0 and 1 the update does nothing for one class.

An error of exactly 0. ln((1-0)/0) is undefined. Clamp the error away from 0 and 1, as the program does.

Reporting the training accuracy over many rounds. It rises for ever. Test accuracy stopped at round 5 here.

Using a full decision tree as the weak learner. Then it is not boosting weak classifiers, and one member can already do the whole job. Depth 1.

Running Adaboost on data you know is mislabelled and not saying so. It will put all its weight on the bad labels, which the observation table above shows happening.

For the journal

Aim; what a weak classifier is and what a decision stump is; the four steps of Adaboost with the alpha formula; the dataset and why the easier one was not used; the Stump class and best_stump; the boosting loop; the round-by-round table of stump, error and alpha; the accuracy of each stump alone against the ensemble at each size; the heaviest rows at the end with their scores on the underlying rule; the scikit-learn comparison; both observation tables; the result.

Quick revision

  • A weak classifier is barely better than chance. A decision stump is a one-level tree.
  • Every training row has a weight; they start equal and add to 1.
  • Each round picks the stump with the smallest weighted error.
  • alpha = 0.5 * ln((1 - error) / error). Low error, loud voice; error 0.5, no voice; error above 0.5, negative voice.
  • Weights are multiplied by exp(-alpha y prediction) and then normalised, so wrong rows get heavier.
  • The ensemble answers by the sign of the sum of alpha * prediction.
  • Successive stumps have higher weighted error, because what is left is hard.
  • Measured here: best stump 0.722 on test, ensemble 0.778, and no gain after round 5.
  • Adaboost concentrates on hard rows, and a mislabelled row is hard, so it is sensitive to noise.

Questions you must be able to answer

1. What is a weak classifier, and which one did you use? One only a little better than guessing. A decision stump: one feature, one threshold, one answer each side.

2. How does the next round know what the last one got wrong? Through the weights. Every row the chosen stump got wrong has its weight multiplied up, so the next round's weighted error is dominated by those rows and the next stump is chosen to fix them.

munotes.in74

Practical 6: Adaboost Ensemble Learning

3. What is alpha and where does it come from? The weight of a stump's vote in the final ensemble, computed from its weighted error as 0.5 * ln((1 - error) / error).

4. What alpha does a stump with error 0.5 get, and why is that right? Zero, because ln(1) is 0. A classifier that is right half the time carries no information, so it gets no vote.

5. What happens to a stump whose error is above 0.5? Its alpha is negative, so the ensemble uses the opposite of its answer. A classifier that is reliably wrong is as useful as one that is reliably right.

6. Why does the weighted error of each new stump keep rising? Because the rows the earlier stumps handle well have lost almost all their weight, so the remaining weight sits on the hard rows and any stump scores badly on them.

7. What did your ensemble gain over the best single stump? Training accuracy from 0.821 to 0.917 and test accuracy from 0.722 to 0.778, using classifiers that can each see only one feature.

8. Should you keep adding rounds? No. Training accuracy keeps rising, but test accuracy stopped at round 5 here and scikit-learn's stopped at 0.806 after five rounds out of a hundred. Choose the number of rounds by measuring on held-out data.

9. Why is Adaboost sensitive to noisy labels? Because a mislabelled row is permanently wrong, so its weight grows every round and the algorithm spends its later rounds on it. In this run the two heaviest rows at the end were two of the deliberately flipped labels.

10. Your ensemble and one of its stumps give different answers on a row. Which is used? The ensemble's, which is the sign of the sum of alpha * prediction over every stump. An individual stump's answer only ever enters through that sum.

munotes.in75

The rest of this subject

These notes are cut from the University's printed syllabus. Open the syllabus itself for the same subject.

Issue
Done!