Lab 9: Adaboost

From 6.034 Wiki

(Difference between revisions)
Jump to: navigation, search
m (Pick the best weak classifier)
Current revision (04:32, 21 August 2020) (view source)
m (Lab 9 moved to Lab 9: Adaboost)
 
(37 intermediate revisions not shown.)
Line 1: Line 1:
 +
<!-- {{Unreleased}} -->
__TOC__
__TOC__
-
This lab is due by Friday, December 4 at 10:00pm. 
+
{{Lab_Due|when=Thursday, December 3}}
-
'''The online tester is now available!''' <!--(As of Monday, November 23, at 9:30pm)-->
+
{{Get_Code|lab=9}}
 +
== Boosting allows us to combine the strengths of several different, imperfect classifiers ==
-
To work on this lab, you will need to get the code:
+
The machine learning classifiers we've been discussing in class are powerful when used correctly, but they are not perfect. Even the best classifiers, under the best of conditions, misclassify some samples. However, due to the nature of each different machine learning algorithm, different classifiers often make different kinds of mistakes. We can take advantage of this variety in errors, using the (reasonable) assumption that ''most'' classifiers will correctly classify each particular point, to construct an even more powerful '''ensemble''' or '''amalgam''' classifier, H, that can correctly classify all or most samples.
-
* You can view the files <!--'''and change log'''--> at: http://web.mit.edu/6.034/www/labs/lab7/
+
-
* Download it as a ZIP file: http://web.mit.edu/6.034/www/labs/lab7/lab7.zip
+
-
* Or, on Athena, <tt>add 6.034</tt> and copy it from <tt>/mit/6.034/www/labs/lab7/</tt>.
+
-
 
+
-
<!--Online tests will be made available by the end of Monday, November 23.  In the meantime, the local tester provides thorough unit tests.-->
+
-
Your answers for this lab belong in the main file <tt>lab7.py</tt>.
+
== Adaboost ==
-
= Problems: Adaboost =
+
The boosting algorithm we teach in 6.034 is called Adaboost, short for ''adaptive boosting''. In this lab, you will implement the Adaboost algorithm to perform boosting.
-
In this lab, you will code the Adaboost algorithm to perform boosting.
+
Throughout this lab, we will assume that there are exactly two classifications, meaning that every not-misclassified training point is classified correctly.
Throughout this lab, we will assume that there are exactly two classifications, meaning that every not-misclassified training point is classified correctly.
-
== Helper functions ==
+
== A note about floats, roundoff error, and <tt>Fraction</tt>s ==
 +
 
 +
Python sometimes rounds floating point numbers, especially when adding them to dictionaries, which can result in two numbers being off by 0.0000000000000001 (10^-16) or more when they should be equal! For example:
 +
 
 +
<pre>
 +
>>> 1/3.0
 +
0.3333333333333333
 +
>>> 2/3.0
 +
0.6666666666666666
 +
>>> 1 - 2/3.0
 +
0.33333333333333337
 +
>>> 1/3.0 == 1 - 2/3.0
 +
False
 +
</pre>
 +
 
 +
To avoid dealing with this roundoff error, we recommend using the <tt>Fraction</tt> class. We've already imported it into the lab for you in <tt>utils.py</tt>, and your <tt>lab9.py</tt> file imports it from there. We've also written a function to convert floats or pairs of numbers (numerator and denominator) into fractions:
 +
 
 +
* <tt>make_fraction(n)</tt>: Returns a <tt>Fraction</tt> approximately equal to <tt>n</tt>, rounding to the nearest fraction with denominator <= 1000.
 +
* <tt>make_fraction(n, d)</tt>: If <tt>n</tt> and <tt>d</tt> are both integers, returns a <tt>Fraction</tt> representing <tt>n/d</tt>. Otherwise, returns a <tt>Fraction</tt> approximately equal to <tt>n/d</tt>, rounding to the nearest fraction with denominator <= 1000.
 +
 
 +
You can manipulate and compare <tt>Fraction</tt>s just like any other number type. In fact, you can even make new <tt>Fraction</tt>s out of them! Here are a few examples:
 +
 
 +
<pre>
 +
>>> frac1 = make_fraction(2,6)  # make_fraction can take in two numbers and reduce them to a simple Fraction
 +
>>> frac1
 +
1/3
 +
>>> frac2 = make_fraction(1,4)
 +
>>> frac2
 +
1/4
 +
>>> frac2 == 0.25              # Fractions are considered equal to their equivalent floats or ints
 +
True
 +
>>> frac1 + frac2              # You can add, subtract, multiply, and divide Fractions
 +
7/12
 +
>>> frac1 * 5                  # You can combine a Fraction and an int to get a new Fraction
 +
5/3
 +
>>> make_fraction(frac1, frac2) # You can make a new Fraction out of any two numbers -- including Fractions!
 +
4/3
 +
>>> make_fraction(0.9)          # make_fraction can also take in a single number (float, int, long, or Fraction)
 +
9/10
 +
>>>
 +
</pre>
 +
 
 +
In this lab, we recommend using <tt>int</tt>s or <tt>Fraction</tt>s, as opposed to floats, for nearly everything numeric. The one exception to this rule is for voting powers, which we recommend representing as floats because they involve logarithms.
 +
 
 +
<div style="background-color:#ff0; border:1px solid #fdd;  padding:1ex;">
 +
'''With all of that having being said, if you think you've implemented a function correctly but you're not passing all of the tests, make sure you're correctly using <tt>Fraction</tt>s instead of raw floats!'''
 +
</div>
 +
 
 +
== Part 1: Helper functions ==
 +
 
 +
We will implement Adaboost piece-by-piece, modularizing the different steps of the algorithm in a similar way to how we taught Adaboost in recitation. As a brief reminder, here is the general control flow of the Adaboost algorithm:
 +
# Initialize all training points' weights.
 +
# Compute the error rate of each weak classifier.
 +
# Pick the "best" weak classifier ''h'', by some definition of "best."
 +
# Use the error rate of ''h'' to compute the voting power for ''h''.
 +
# Append ''h'', along with its voting power, to the ensemble classifier H.
 +
# Update weights in preparation for the next round.
 +
# Repeat steps 2-7 until no good classifier remains, we have reached some max number of iterations, or H is "good enough."
=== Initialize weights ===
=== Initialize weights ===
-
First, implement <tt>initialize_weights</tt> to assign every training point a weight equal to 1/N, where N is the number of training points. This function takes in a list of training points, where each point is represented as a string. The function should return a dictionary mapping points to weights.
+
 
 +
First, implement <tt>initialize_weights</tt> to assign every training point a weight equal to 1/N, where N is the number of training points. This function takes in one argument, <tt>training_points</tt>, which is a list of training points, where each point is represented as a string. The function should return a dictionary mapping points to weights.
  def initialize_weights(training_points):
  def initialize_weights(training_points):
=== Calculate error rates ===
=== Calculate error rates ===
-
Next, we want to calculate the error rate ε of each classifier.  The error rate for a classifier ''h'' is the sum of the weights of the training points that ''h'' misclassifies.
 
-
<tt>calculate_error_rates</tt> takes in two dictionaries:
+
Next, we want to calculate the error rate ε of each classifier. The error rate for a classifier ''h'' is the sum of the weights of the training points that ''h'' misclassifies.
 +
 
 +
<tt>calculate_error_rates</tt> takes as input two dictionaries:
 +
 
* <tt>point_to_weight</tt>: maps each training point (represented as a string) to its weight (a number).
* <tt>point_to_weight</tt>: maps each training point (represented as a string) to its weight (a number).
-
* <tt>classifier_to_misclassified</tt>: maps each classifier to a list of the training points that it misclassifies. For example, this dictionary may contain entries such as <tt>"classifier_0": ["point_A", "point_C"]</tt>.
+
* <tt>classifier_to_misclassified</tt>: maps each classifier (a string) to a list of the training points (strings) that it misclassifies. For example, this dictionary may contain entries such as <tt>"classifier_0": ["point_A", "point_C"]</tt>, indicating <tt>classifier_0</tt> misclassifies points A and C.
Implement <tt>calculate_error_rates</tt> to return a dictionary mapping each weak classifier (a string) to its error rate (a number):
Implement <tt>calculate_error_rates</tt> to return a dictionary mapping each weak classifier (a string) to its error rate (a number):
Line 39: Line 95:
=== Pick the best weak classifier ===
=== Pick the best weak classifier ===
-
Once we have calculated the error rate of each weak classifier, we need to select the "best" weak classifier.  "Best" has two possible definitions:
 
-
* "smallest error rate" (<tt>use_smallest_error=True</tt>)
 
-
* "error rate furthest from 1/2" (<tt>use_smallest_error=False</tt>)
 
 +
Once we have calculated the error rate of each weak classifier, we need to select the "best" weak classifier. Implement <tt>pick_best_classifier</tt> to return the name of the "best" weak classifier, or raise a <tt>NoGoodClassifiersError</tt> if the "best" weak classifier has an error rate of exactly 1/2. Note that "best" has two possible definitions:
 +
* "smallest error rate" (<tt>use_smallest_error=True</tt>), ''or'',
 +
* "error rate furthest from 1/2" (<tt>use_smallest_error=False</tt>)
-
Implement <tt>pick_best_classifier</tt> to return the name of the "best" weak classifier:
 
  def pick_best_classifier(classifier_to_error_rate, use_smallest_error=True):
  def pick_best_classifier(classifier_to_error_rate, use_smallest_error=True):
 +
If two or more weak classifiers have equally good error rates, return the one that comes first alphabetically.
-
There are a few details that can make this function tricky:
+
Hint: Depending on your implementation for this function, you may be interested in using <tt>min</tt> or <tt>max</tt> on a dictionary. Recall that <tt>min</tt> and <tt>max</tt> can take an optional <tt>key</tt> argument, which works similarly to the <tt>key</tt> argument for <tt>sorted</tt>.
-
* '''Tie-breaking''': In case two or more weak classifiers have equally good error rates, return the one that comes first alphabetically.  (Hint: The built-in function <tt>sorted</tt> may help.)
+
-
* '''Roundoff error''': Python sometimes rounds numbers, especially when adding them to dictionaries, which can result in numbers being off by 0.0000000000000001 (10^-16) when they should be equal.  Accordingly, you may want to use the helper function <tt>fix_roundoff_error</tt> after performing float arithmetic, but before comparing equality or passing numbers into <tt>min</tt> or <tt>max</tt>.  The function <tt>fix_roundoff_error</tt> takes in a number and corrects it for roundoff error at the 10^-16 place.  It can also take in a list of numbers or a dictionary whose values are numbers, then return a new corrected list or dictionary.
+
-
* '''Using <tt>min</tt> or <tt>max</tt> on a dictionary''': This is something you may want to do, depending on your implementation. It may be helpful to note that <tt>min</tt> and <tt>max</tt> can take an optional <tt>key</tt> argument.  (It works the same way as the <tt>key</tt> argument for <tt>sorted</tt>, which you may have used in Lab 2.)
+
=== Calculate voting power ===
=== Calculate voting power ===
-
After selecting the best weak classifier, we'll need to compute its voting power. If ε is the error rate of the weak classifier, then its voting power is:
+
 
 +
After selecting the best weak classifier, we'll need to compute its voting power. Recall that if ε is the error rate of the weak classifier, then its voting power is:
  1/2 * ln((1-ε)/ε)
  1/2 * ln((1-ε)/ε)
-
Implement <tt>calculate_voting_power</tt> to compute a classifier's voting power, given its error rate 0 ≤ ε ≤ 1. (For your convenience, the constant <tt>INF</tt> and the natural log function <tt>ln</tt> have been defined in <tt>lab7.py</tt>.)
+
Implement <tt>calculate_voting_power</tt> to compute a classifier's voting power, given its error rate 0 ≤ ε ≤ 1. (For your convenience, the constant <tt>INF</tt> and the natural log function <tt>ln</tt> have been defined in <tt>lab9.py</tt>.)
  def calculate_voting_power(error_rate):
  def calculate_voting_power(error_rate):
Line 64: Line 118:
=== Is H good enough? ===
=== Is H good enough? ===
-
One of the three exit conditions for Adaboost is when the overall classifier H is "good enough" -- that is, when H classifies enough training points correctly.  The function <tt>is_good_enough</tt> takes in four arguments:
+
 
-
* <tt>H</tt>: An overall classifier, represented as a list of (classifier, voting_power) tuples. (Recall that each classifier is represented as a string.)
+
One of the three exit conditions for Adaboost is when the overall classifier H is "good enough" -- that is, when H correctly classifies enough of the training points.   
-
* <tt>training_points</tt>: A list of all training points. (Recall that each training point is represented as a string.)
+
 
 +
First, implement a helper function to determine which training points are misclassified by H:
 +
def get_overall_misclassifications(H, training_points, classifier_to_misclassified):
 +
 
 +
The function <tt>get_overall_misclassifications</tt> takes in three arguments:
 +
* <tt>H</tt>: An overall classifier, represented as a list of <tt>(classifier, voting_power)</tt> tuples. (Recall that each classifier is represented as a string.)
 +
* <tt>training_points</tt>: A list of all training points. (Recall that each training point is represented as a string.)
* <tt>classifier_to_misclassified</tt>: A dictionary mapping each classifier to a list of the training points that it misclassifies.
* <tt>classifier_to_misclassified</tt>: A dictionary mapping each classifier to a list of the training points that it misclassifies.
-
* <tt>mistake_tolerance</tt>: The maximum number of points that H is allowed to misclassify.
 
-
<tt>is_good_enough</tt> should return True if H is good enough (does not misclassify too many points), otherwise False.  Each training point's classification is determined by a weighted vote of the weak classifiers in H. (If the vote is a tie, consider the training point to be misclassified.) <!--TODO more hint?-->
+
The function returns a set containing the training points that H misclassifies.
-
Implement:
+
Each training point's classification is determined by a weighted vote of the weak classifiers in H. Although we don't know any point's true classification (+1 or -1), we do know whether each point was classified correctly or incorrectly by each weak classifier, and we know that this is a binary classification problem, so there is sufficient information to determine which points were misclassified.
-
def is_good_enough(H, training_points, classifier_to_misclassified, mistake_tolerance=0):
+
-
Hint: You don't need to know the the actual classification of a training point. Because there are only two classifications, knowing whether the training point was misclassified should be sufficient.
+
'''If the vote among the weak classifiers in H is ever a tie, consider the training point to be misclassified.'''
 +
 
 +
Now, we can determine whether H is "good enough". The function <tt>is_good_enough</tt> takes in the three arguments from <tt>get_overall_misclassifications</tt>, as well as fourth argument:
 +
* <tt>mistake_tolerance</tt>: The maximum number of points that H is allowed to misclassify while still being considered "good enough."
 +
 
 +
<tt>is_good_enough</tt> should return <tt>False</tt> if H is misclassifies too many points, otherwise <tt>True</tt> (because H is "good enough").
 +
def is_good_enough(H, training_points, classifier_to_misclassified, mistake_tolerance=0):
=== Update weights ===
=== Update weights ===
-
After each round, Adaboost updates weights in preparation for the next round. The updated weight for each point depends on whether the point was classified correctly or incorrectly by the current weak classifier:
+
 
 +
After each round, Adaboost updates weights in preparation for the next round. The updated weight for each point depends on whether the point was classified correctly or incorrectly by the current weak classifier:
<!--todo: inconsistent tone.  Is "you" or "we" implementing this?  Is "you" or "Adaboost" performing the algorithm?-->
<!--todo: inconsistent tone.  Is "you" or "we" implementing this?  Is "you" or "Adaboost" performing the algorithm?-->
* If the point was classified correctly: <tt>new weight = 1/2 * 1/(1-ε) * (old weight)</tt> <!--todo better formatting-->
* If the point was classified correctly: <tt>new weight = 1/2 * 1/(1-ε) * (old weight)</tt> <!--todo better formatting-->
Line 85: Line 150:
The function <tt>update_weights</tt> takes in 3 arguments:
The function <tt>update_weights</tt> takes in 3 arguments:
-
* <tt>point_to_weight</tt>: A dictionary mapping each point to its current weight. You are allowed to modify this dictionary, but are not required to.
+
* <tt>point_to_weight</tt>: A dictionary mapping each point to its current weight. You are allowed to modify this dictionary, but are not required to.
* <tt>misclassified_points</tt>: A list of points misclassified by the current weak classifier.
* <tt>misclassified_points</tt>: A list of points misclassified by the current weak classifier.
* <tt>error_rate</tt>: The error rate ε of the current weak classifier.
* <tt>error_rate</tt>: The error rate ε of the current weak classifier.
Line 92: Line 157:
  def update_weights(point_to_weight, misclassified_points, error_rate):
  def update_weights(point_to_weight, misclassified_points, error_rate):
-
== Adaboost ==
+
== Part 2: Adaboost ==
-
Using all the helper functions you've written above, implement the Adaboost algorithm:
+
 
 +
Using all of the helper functions you've written above, implement the [[#Part_1:_Helper_functions|Adaboost algorithm]]:
  def adaboost(training_points, classifier_to_misclassified,
  def adaboost(training_points, classifier_to_misclassified,
-
               use_smallest_error=True, mistake_tolerance=0, max_num_rounds=INF):
+
               use_smallest_error=True, mistake_tolerance=0, max_rounds=INF):
-
 
+
-
Keep in mind that Adaboost has three exit conditions:
+
-
* If H is "good enough" (see [[#Is_H_good_enough.3F|Is H good enough?]], above)
+
-
* If it has completed the maximum number of rounds
+
-
* If the best weak classifier is no good (has an error rate ε = 1/2)
+
-
 
+
-
 
+
-
Adaboost is initialized by setting the weight of every training point to 1/N (where N is the number of training points).  Then, Adaboost repeats the following steps until one of the three exit conditions is satisfied.
+
-
# Compute the error rate of each weak classifier.
+
-
# Pick the best weak classifier ''h''.  Note that "best" can mean either "smallest error rate" (<tt>use_smallest_error=True</tt>) or "error rate furthest from 1/2" (<tt>use_smallest_error=False</tt>).
+
-
# Compute the voting power of the weak classifier ''h'', using its error rate ε.
+
-
# Append ''h'' to the overall classifier H, as a tuple (''h'', voting_power).
+
-
# Update weights in preparation for the next round.
+
-
 
+
The function <tt>adaboost</tt> takes in five arguments:
The function <tt>adaboost</tt> takes in five arguments:
* <tt>training_points</tt>: A list of all training points.
* <tt>training_points</tt>: A list of all training points.
* <tt>classifier_to_misclassified</tt>: A dictionary mapping each classifier to a list of the training points that it misclassifies.
* <tt>classifier_to_misclassified</tt>: A dictionary mapping each classifier to a list of the training points that it misclassifies.
-
* <tt>use_smallest_error</tt>: A boolean value indicating which definition of "best" to use: "smallest error rate" or "error rate furthest from 1/2".
+
* <tt>use_smallest_error</tt>: A boolean value indicating which definition of "best" to use: "smallest error rate" (<tt>True</tt>) or "error rate furthest from 1/2" (<tt>False</tt>).
* <tt>mistake_tolerance</tt>: The maximum number of points that H is allowed to misclassify.
* <tt>mistake_tolerance</tt>: The maximum number of points that H is allowed to misclassify.
-
* <tt>max_num_rounds</tt>: The maximum number of rounds of boosting to perform before returning H. (This is equivalent to the maximum number of tuples that can be added to H.)
+
* <tt>max_rounds</tt>: The maximum number of rounds of boosting to perform before returning H. (This is equivalent to the maximum number of tuples that can be added to H.)
 +
<tt>adaboost</tt> should return the overall classifier H, represented as a list of <tt>(classifier, voting_power)</tt> tuples.
-
<tt>adaboost</tt> should return the overall classifier H, represented as a list of (classifier, voting_power) tuples.
+
Keep in mind that Adaboost has three exit conditions:
-
 
+
* If H is "good enough" (see [[#Is_H_good_enough.3F|Is H good enough?]], above)
-
 
+
* If it has completed the maximum number of rounds
-
'''Hints:'''
+
* If the best weak classifier is no good (has an error rate ε = 1/2)
-
* If <tt>adaboost</tt> fails to exit with the ε=1/2 condition because Python thinks 0.5 != 0.5, you can use <tt>fix_roundoff_error</tt> to correct the error rate before comparing it to 0.5.  (<tt>fix_roundoff_error</tt> can take in a number, list, or dict.)
+
-
* Note that weight updates are based on on which points were misclassified by the current ''weak'' classifier, not which points were misclassified by the overall classifier H.
+
-
 
+
-
== Survey ==
+
-
Please answer these questions at the bottom of your <tt>lab6.py</tt> file:
+
-
 
+
-
* <tt>NAME</tt>: What is your name? (string)
+
-
 
+
-
* <tt>COLLABORATORS</tt>: Other than 6.034 staff, whom did you work with on this lab? (string, or empty string if you worked alone)
+
-
 
+
-
* <tt>HOW_MANY_HOURS_THIS_LAB_TOOK</tt>: Approximately how many hours did you spend on this lab? (number or string)
+
-
 
+
-
* <tt>WHAT_I_FOUND_INTERESTING</tt>: Which parts of this lab, if any, did you find interesting? (string)
+
-
 
+
-
* <tt>WHAT_I_FOUND_BORING</tt>: Which parts of this lab, if any, did you find boring or tedious? (string)
+
-
 
+
-
* (optional) <tt>SUGGESTIONS</tt>: What specific changes would you recommend, if any, to improve this lab for future years? (string)
+
 +
;Important Notes
 +
:Recall that weight updates are based on the points that were misclassified by the ''current weak classifier'', not by the overall classifier H.
-
(We'd ask which parts you find confusing, but if you're confused you should really ask a TA.)
+
:If you are failing test cases online but believe your implementation to be correct, please ensure that you are breaking ties correctly in your helper functions.
-
When you're done, run the online tester to submit your code.
+
{{Survey}}

Current revision

Contents


This lab is due by Thursday, December 3 at 10:00pm.

Before working on the lab, you will need to get the code. You can...

  • Use Git on your computer: git clone username@athena.dialup.mit.edu:/mit/6.034/www/labs/fall2020/lab9
  • Use Git on Athena: git clone /mit/6.034/www/labs/fall2020/lab9


All of your answers belong in the main file lab9.py. To submit your lab to the test server, you will need to download your key.py file and put it in either your lab9 directory or its parent directory. You can also view all of your lab submissions and grades here.


Boosting allows us to combine the strengths of several different, imperfect classifiers

The machine learning classifiers we've been discussing in class are powerful when used correctly, but they are not perfect. Even the best classifiers, under the best of conditions, misclassify some samples. However, due to the nature of each different machine learning algorithm, different classifiers often make different kinds of mistakes. We can take advantage of this variety in errors, using the (reasonable) assumption that most classifiers will correctly classify each particular point, to construct an even more powerful ensemble or amalgam classifier, H, that can correctly classify all or most samples.

Adaboost

The boosting algorithm we teach in 6.034 is called Adaboost, short for adaptive boosting. In this lab, you will implement the Adaboost algorithm to perform boosting.

Throughout this lab, we will assume that there are exactly two classifications, meaning that every not-misclassified training point is classified correctly.

A note about floats, roundoff error, and Fractions

Python sometimes rounds floating point numbers, especially when adding them to dictionaries, which can result in two numbers being off by 0.0000000000000001 (10^-16) or more when they should be equal! For example:

>>> 1/3.0
0.3333333333333333
>>> 2/3.0
0.6666666666666666
>>> 1 - 2/3.0
0.33333333333333337
>>> 1/3.0 == 1 - 2/3.0
False

To avoid dealing with this roundoff error, we recommend using the Fraction class. We've already imported it into the lab for you in utils.py, and your lab9.py file imports it from there. We've also written a function to convert floats or pairs of numbers (numerator and denominator) into fractions:

  • make_fraction(n): Returns a Fraction approximately equal to n, rounding to the nearest fraction with denominator <= 1000.
  • make_fraction(n, d): If n and d are both integers, returns a Fraction representing n/d. Otherwise, returns a Fraction approximately equal to n/d, rounding to the nearest fraction with denominator <= 1000.

You can manipulate and compare Fractions just like any other number type. In fact, you can even make new Fractions out of them! Here are a few examples:

>>> frac1 = make_fraction(2,6)  # make_fraction can take in two numbers and reduce them to a simple Fraction
>>> frac1
1/3
>>> frac2 = make_fraction(1,4)
>>> frac2
1/4
>>> frac2 == 0.25               # Fractions are considered equal to their equivalent floats or ints
True
>>> frac1 + frac2               # You can add, subtract, multiply, and divide Fractions
7/12
>>> frac1 * 5                   # You can combine a Fraction and an int to get a new Fraction
5/3
>>> make_fraction(frac1, frac2) # You can make a new Fraction out of any two numbers -- including Fractions!
4/3
>>> make_fraction(0.9)          # make_fraction can also take in a single number (float, int, long, or Fraction)
9/10
>>>

In this lab, we recommend using ints or Fractions, as opposed to floats, for nearly everything numeric. The one exception to this rule is for voting powers, which we recommend representing as floats because they involve logarithms.

With all of that having being said, if you think you've implemented a function correctly but you're not passing all of the tests, make sure you're correctly using Fractions instead of raw floats!

Part 1: Helper functions

We will implement Adaboost piece-by-piece, modularizing the different steps of the algorithm in a similar way to how we taught Adaboost in recitation. As a brief reminder, here is the general control flow of the Adaboost algorithm:

  1. Initialize all training points' weights.
  2. Compute the error rate of each weak classifier.
  3. Pick the "best" weak classifier h, by some definition of "best."
  4. Use the error rate of h to compute the voting power for h.
  5. Append h, along with its voting power, to the ensemble classifier H.
  6. Update weights in preparation for the next round.
  7. Repeat steps 2-7 until no good classifier remains, we have reached some max number of iterations, or H is "good enough."

Initialize weights

First, implement initialize_weights to assign every training point a weight equal to 1/N, where N is the number of training points. This function takes in one argument, training_points, which is a list of training points, where each point is represented as a string. The function should return a dictionary mapping points to weights.

def initialize_weights(training_points):

Calculate error rates

Next, we want to calculate the error rate ε of each classifier. The error rate for a classifier h is the sum of the weights of the training points that h misclassifies.

calculate_error_rates takes as input two dictionaries:

  • point_to_weight: maps each training point (represented as a string) to its weight (a number).
  • classifier_to_misclassified: maps each classifier (a string) to a list of the training points (strings) that it misclassifies. For example, this dictionary may contain entries such as "classifier_0": ["point_A", "point_C"], indicating classifier_0 misclassifies points A and C.

Implement calculate_error_rates to return a dictionary mapping each weak classifier (a string) to its error rate (a number):

def calculate_error_rates(point_to_weight, classifier_to_misclassified):

Pick the best weak classifier

Once we have calculated the error rate of each weak classifier, we need to select the "best" weak classifier. Implement pick_best_classifier to return the name of the "best" weak classifier, or raise a NoGoodClassifiersError if the "best" weak classifier has an error rate of exactly 1/2. Note that "best" has two possible definitions:

  • "smallest error rate" (use_smallest_error=True), or,
  • "error rate furthest from 1/2" (use_smallest_error=False)
def pick_best_classifier(classifier_to_error_rate, use_smallest_error=True):

If two or more weak classifiers have equally good error rates, return the one that comes first alphabetically.

Hint: Depending on your implementation for this function, you may be interested in using min or max on a dictionary. Recall that min and max can take an optional key argument, which works similarly to the key argument for sorted.

Calculate voting power

After selecting the best weak classifier, we'll need to compute its voting power. Recall that if ε is the error rate of the weak classifier, then its voting power is:

1/2 * ln((1-ε)/ε)

Implement calculate_voting_power to compute a classifier's voting power, given its error rate 0 ≤ ε ≤ 1. (For your convenience, the constant INF and the natural log function ln have been defined in lab9.py.)

def calculate_voting_power(error_rate):

Hint: What voting power would you give to a weak classifier that classifies all the training points correctly? What if it misclassifies all the training points?

Is H good enough?

One of the three exit conditions for Adaboost is when the overall classifier H is "good enough" -- that is, when H correctly classifies enough of the training points.

First, implement a helper function to determine which training points are misclassified by H:

def get_overall_misclassifications(H, training_points, classifier_to_misclassified):

The function get_overall_misclassifications takes in three arguments:

  • H: An overall classifier, represented as a list of (classifier, voting_power) tuples. (Recall that each classifier is represented as a string.)
  • training_points: A list of all training points. (Recall that each training point is represented as a string.)
  • classifier_to_misclassified: A dictionary mapping each classifier to a list of the training points that it misclassifies.

The function returns a set containing the training points that H misclassifies.

Each training point's classification is determined by a weighted vote of the weak classifiers in H. Although we don't know any point's true classification (+1 or -1), we do know whether each point was classified correctly or incorrectly by each weak classifier, and we know that this is a binary classification problem, so there is sufficient information to determine which points were misclassified.

If the vote among the weak classifiers in H is ever a tie, consider the training point to be misclassified.

Now, we can determine whether H is "good enough". The function is_good_enough takes in the three arguments from get_overall_misclassifications, as well as fourth argument:

  • mistake_tolerance: The maximum number of points that H is allowed to misclassify while still being considered "good enough."

is_good_enough should return False if H is misclassifies too many points, otherwise True (because H is "good enough").

def is_good_enough(H, training_points, classifier_to_misclassified, mistake_tolerance=0):

Update weights

After each round, Adaboost updates weights in preparation for the next round. The updated weight for each point depends on whether the point was classified correctly or incorrectly by the current weak classifier:

  • If the point was classified correctly: new weight = 1/2 * 1/(1-ε) * (old weight)
  • If the point was misclassified: new weight = 1/2 * 1/ε * (old weight)


The function update_weights takes in 3 arguments:

  • point_to_weight: A dictionary mapping each point to its current weight. You are allowed to modify this dictionary, but are not required to.
  • misclassified_points: A list of points misclassified by the current weak classifier.
  • error_rate: The error rate ε of the current weak classifier.

Implement update_weights to return a dictionary mapping each point to its new weight:

def update_weights(point_to_weight, misclassified_points, error_rate):

Part 2: Adaboost

Using all of the helper functions you've written above, implement the Adaboost algorithm:

def adaboost(training_points, classifier_to_misclassified,
             use_smallest_error=True, mistake_tolerance=0, max_rounds=INF):

The function adaboost takes in five arguments:

  • training_points: A list of all training points.
  • classifier_to_misclassified: A dictionary mapping each classifier to a list of the training points that it misclassifies.
  • use_smallest_error: A boolean value indicating which definition of "best" to use: "smallest error rate" (True) or "error rate furthest from 1/2" (False).
  • mistake_tolerance: The maximum number of points that H is allowed to misclassify.
  • max_rounds: The maximum number of rounds of boosting to perform before returning H. (This is equivalent to the maximum number of tuples that can be added to H.)

adaboost should return the overall classifier H, represented as a list of (classifier, voting_power) tuples.

Keep in mind that Adaboost has three exit conditions:

  • If H is "good enough" (see Is H good enough?, above)
  • If it has completed the maximum number of rounds
  • If the best weak classifier is no good (has an error rate ε = 1/2)
Important Notes
Recall that weight updates are based on the points that were misclassified by the current weak classifier, not by the overall classifier H.
If you are failing test cases online but believe your implementation to be correct, please ensure that you are breaking ties correctly in your helper functions.

Survey

Please answer these questions at the bottom of your lab file:

  • NAME: What is your name? (string)
  • COLLABORATORS: Other than 6.034 staff, whom did you work with on this lab? (string, or empty string if you worked alone)
  • HOW_MANY_HOURS_THIS_LAB_TOOK: Approximately how many hours did you spend on this lab? (number or string)
  • WHAT_I_FOUND_INTERESTING: Which parts of this lab, if any, did you find interesting? (string)
  • WHAT_I_FOUND_BORING: Which parts of this lab, if any, did you find boring or tedious? (string)
  • (optional) SUGGESTIONS: What specific changes would you recommend, if any, to improve this lab for future years? (string)


(We'd ask which parts you find confusing, but if you're confused you should really ask a TA.)

When you're done, run the online tester to submit your code.

Personal tools