Logistic Regression Part 3

We bend our binary logistic regression into a multi-class classifier using the one-vs-rest trick, then try it out on sklearn's iris dataset.

Alright, like I mentioned before β€” this time we’re looking at multi-class classification. Everything we’ve done so far? The data was always split into two buckets. 0 or 1. Binary. Done.

But what if we’ve got 3 or more classes to sort out?

Then the question is how we tweak this machine learning class to handle that.

So how do you classify 3+ things?

Right now we’ve got a classifier that can separate two things. The question is: how do we use it to sort three…? Let’s look at a picture.

Scatter plot of three clusters on axes ATTR 0 and ATTR 1: CLASS 0 (blue circles), CLASS 1 (orange triangles) and CLASS 2 (green triangles)

OK so these guys are huddled up by color. With just the classifier we already have, we can do this much:

The same three clusters with three straight lines, the CLASS 0, CLASS 1 and CLASS 2 lines, each one separating its class from the rest

  • A blue line separating blue from everything else
  • A green line separating green from everything else
  • A red line separating red from everything else

That much we can already handle with what we’ve got…. Wait a sec…?!

If we’ve got that much, can’t we pull off 3-class classification with some sneaky trick? What kind of dirty trick should we try here?!

Here’s what I’m thinking. First, the broad regions β€” the areas where each color clumps up β€” those are easy. Just use the “above or below the line” rule.

The tricky bit is the triangle in the middle where all three lines meet. For each point, compare the three classifiers’ signed decision scores and pick the largest. With logistic regression those scores are $w_k\cdot x+b_k$; the sigmoid preserves their order. It isn’t a nearest-line rule. So the calls sound like:

  • “Oh, point 1 β€” blue scores highest β†’ you’re blue.”
  • “Oh, point 2 β€” red scores highest β†’ you’re red.”
  • “Oh, point 3 β€” blue wins β†’ blue.”
  • “Oh, point 4 β€” blue wins β†’ blue.”
  • “Oh, point 5 β€” green wins β†’ green.”

Do that for every point, give each one its color, and you get something like:

The plane colored by one-vs-rest: blue, red and green regions split by the three lines, the triangle in the middle shared out between them

Actually β€” the thing I just described is an algorithm called one-vs-rest (OvR).

But lol, there are only a few points here, so fine, we can do it like that…. What if the dataset gets huge? Calling out every point one by one like that would take forever……. At that point all you can do is type GG and back out.

Anyway β€” let’s eyeball it and move on. Because our goal right now isn’t to understand every single principle inside machine learning, right? Our goal is to use this stuff!!!

The parameter

In the original notebook, we selected OvR with multi_class='ovr'. Current scikit-learn uses OneVsRestClassifier around the binary model instead. Let’s use that below.

Let’s get it! First, load the data:

from sklearn.datasets import load_iris
iris = load_iris()
X = iris['data']
y = iris['target']

Shuffle & split:

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=1, stratify=y)

The iris dataset is 3-class and the class ratios are nicely balanced:

import pandas as pd
X = pd.DataFrame(X)
y = pd.Series(y)
print(y.value_counts()/len(y))
2    0.333333
1    0.333333
0    0.333333
dtype: float64

Now let’s fit the OvR model and see what we get. I’ll just throw in the other parameters at reasonable values:

from sklearn.linear_model import LogisticRegression
from sklearn.multiclass import OneVsRestClassifier

model = OneVsRestClassifier(
    LogisticRegression(C=1, class_weight='balanced',
                       random_state=1, solver='lbfgs'),
    n_jobs=-1
).fit(X_train, y_train)

print('train score: ', model.score(X_train, y_train))
print('test score: ', model.score(X_test, y_test))
train score:  0.9285714285714286
test score:  0.9210526315789473

“But wait, is it really getting each of the 3 classes right????”

Fine, let’s ask it like this. X holds both the train set and the test set, so I’ll just throw it all into predict in one go:

print('predict: ', model.predict(X))
predict:  [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 0 0 0 0 0 0 0 0 0 0 2 1 2 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 1 2 2 2 2 2 2 1 2 2 2 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2]

There it is β€” it answers 0, 1 or 2 for each one. All three classes. And we can look at how confident each prediction was like this:

print('P(class): ', model.predict_proba(X))

Jupyter output of predict_proba(X): one row per flower with the probabilities of class 0, 1 and 2, the first row 0.862, 0.138 and 0.000005

Hey β€” we learned precision and recall too, so let’s use those!!! Heart’s pounding a bit lol β€” will it actually give us precision and recall for each of the 3 classes separately…? heh heh heh

from sklearn.metrics import classification_report
pred_train = model.predict(X_train)
pred_test  = model.predict(X_test)
print('classification_report - training data')
print(classification_report(y_train, pred_train))

Classification_report on the training data, one-vs-rest: precision 1.00, 0.91, 0.87 and recall 1.00, 0.86, 0.92 for classes 0, 1, 2; accuracy 0.93 on 112 flowers

Ohhhhhh

print('classification_report - test data')
print(classification_report(y_test, pred_test))

Classification_report on the test data, one-vs-rest: precision 1.00, 0.86, 0.92 and recall 1.00, 0.92, 0.85 for classes 0, 1, 2; accuracy 0.92 on 38 flowers

Besides OvR β€” there’s also multinomial

model = LogisticRegression(C=1, class_weight='balanced', random_state=1,
                           solver='lbfgs').fit(X_train, y_train)
print('train score: ', model.score(X_train, y_train))
print('test score: ', model.score(X_test, y_test))
train score:  0.9642857142857143
test score:  0.9736842105263158
pred_train = model.predict(X_train)
pred_test  = model.predict(X_test)
print('classification_report - training data')
print(classification_report(y_train, pred_train))

Classification_report on the training data, multinomial: precision and recall 1.00, 0.95, 0.95 for classes 0, 1, 2; accuracy 0.96 on 112 flowers

print('classification_report - test data')
print(classification_report(y_test, pred_test))

Classification_report on the test data, multinomial: precision 1.00, 0.93, 1.00 and recall 1.00, 1.00, 0.92 for classes 0, 1, 2; accuracy 0.97 on 38 flowers

The numbers came out a little nicer, heh heh heh.

The multinomial model uses a softmax-based approach under the hood, and it did a little better on this split. That doesn’t guarantee it’ll beat OvR on every dataset….

As for what softmax actually is β€” we’ll get to that when we briefly touch Deep Learning later. So I’m shelving that concept for next time, heh heh.

For now, let’s settle for “we did sort 3 things” and move on.

Up next…

SVM!

The whole notebook, page 1: the intro and the three-cluster scatter plot

The whole notebook, page 2: the three one-vs-rest lines and the dirty-trick question

The whole notebook, page 3: the triangle, the five closest-line calls and the colored one-vs-rest regions

The whole notebook, page 4: the multi_class parameter, loading and splitting the iris data

The whole notebook, page 5: the fit with multi_class=‘ovr’, its scores and predict(X)

The whole notebook, page 6: predict_proba(X)

The whole notebook, page 7: the one-vs-rest classification reports

The whole notebook, page 8: the multinomial fit and its classification reports

The whole notebook, page 9: softmax comes later, next up SVM


Heads-up. The figures here are the original notes, slips and all. A few things to watch for:

  • The notebook screenshots are from 2019; the runnable code above is updated. multi_class was deprecated in scikit-learn 1.5 and removed in 1.8. For OvR use OneVsRestClassifier(LogisticRegression(...)); for the multinomial model with lbfgs, leave that argument out. The old screenshots’ nearest-line explanation is only a sketch: actual OvR prediction picks the highest signed classifier score.

Comments

Discussion happens via GitHub Discussions. You'll need a GitHub account to comment.