Thanks to visit codestin.com
Credit goes to github.com

Skip to content

MAINT use ArgKmin in OutputCodeClassifier - #25196

Merged
adrinjalali merged 8 commits into
scikit-learn:mainfrom
glemaitre:use_pairwise_distance_reduction_in_ecoc
Jan 4, 2023
Merged

MAINT use ArgKmin in OutputCodeClassifier#25196
adrinjalali merged 8 commits into
scikit-learn:mainfrom
glemaitre:use_pairwise_distance_reduction_in_ecoc

Conversation

@glemaitre

Copy link
Copy Markdown
Member

Make the code using ArgKmin instead of euclidean_distance. The reason will be linked to #25148 where I suspect that we should implement an L1 distance (city-block) as stated in the original paper.

As a first step, we can use the ArgKmin implementation that provides to switch distance and it is optimum since we apply a reduction.

@adrinjalali adrinjalali left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree in principle. I wonder if we can manufacture a test for this.

@glemaitre

Copy link
Copy Markdown
Member Author

I agree in principle. I wonder if we can manufacture a test for this.

Apparently, we don't have some test failing ;). Interesting to know that the decision_function of a LogisticRegression seems to be some integers. I check the code and only have some trivial code to check that the method can be called.

I will try to see if we can add some better tests.

@thomasjpfan thomasjpfan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks you for the PR!

Comment thread sklearn/multiclass.py Outdated
# ArgKmin only accept C-contiguous array. The aggregated predictions need to be
# transposed. We therefore create a F-contiguous array to avoid a copy and have
# a C-contiguous array after the transpose operation.
Y = np.array([_predict_binary(e, X) for e in self.estimators_], order="F").T

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May you run a benchmark to compare peak memory and runtime performance between main and this PR?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any regression in terms of memory usage.

This PR:

peak memory: 364.31 MiB, increment: 103.70 MiB

In 1.2.0:

peak memory: 387.33 MiB, increment: 115.27 MiB
Details
from sklearn.datasets import make_classification
from sklearn.multiclass import OutputCodeClassifier
from sklearn.tree import DecisionTreeClassifier

X, y = make_classification(
    n_samples=100_000,
    n_features=100,
    n_informative=20,
    n_classes=20,
    n_clusters_per_class=1,
    random_state=0,
)
ecoc = OutputCodeClassifier(
    estimator=DecisionTreeClassifier(max_depth=2, random_state=0),
    code_size=3,
    n_jobs=-1,
    random_state=0,
)
ecoc.fit(X, y)

# %%
%load_ext memory_profiler

# %%
%memit ecoc.predict(X)

@ogrisel

ogrisel commented Dec 17, 2022

Copy link
Copy Markdown
Member

For the case of k=1 you can directly use the pairwise_distances_argmin function instead of instantiating the ArgKMin class. It's doing the same under the hood though, maybe with a slightly higher level API.

Comment thread sklearn/multiclass.py Outdated
check_is_fitted(self)
Y = np.array([_predict_binary(e, X) for e in self.estimators_]).T
pred = euclidean_distances(Y, self.code_book_).argmin(axis=1)
# ArgKmin only accept C-contiguous array. The aggregated predictions need to be

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# ArgKmin only accept C-contiguous array. The aggregated predictions need to be
# ArgKmin only accepts C-contiguous array. The aggregated predictions need to be

@jeremiedbb jeremiedbb left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@jeremiedbb jeremiedbb added the Quick Review For PRs that are quick to review label Jan 3, 2023

@adrinjalali adrinjalali left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, but I think this deserves a FIX changelog entry.

@glemaitre

Copy link
Copy Markdown
Member Author

@adrinjalali this is quite transparent to the end user. This is a not a FIX but rather an enhancement.

@adrinjalali

adrinjalali commented Jan 4, 2023

Copy link
Copy Markdown
Member

@glemaitre if that were true, this script should never break, but it always does, somehow in a non-deterministic way though. (running it on main)

import numpy as np
from sklearn.datasets import make_classification
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics.pairwise import pairwise_distances_argmin
from sklearn.multiclass import OutputCodeClassifier, _predict_binary


def new_predict(self, X):
    # ArgKmin only accepts C-contiguous array. The aggregated predictions need to be
    # transposed. We therefore create a F-contiguous array to avoid a copy and have
    # a C-contiguous array after the transpose operation.
    Y = np.array(
        [_predict_binary(e, X) for e in self.estimators_],
        order="F",
        dtype=np.float64,
    ).T
    pred = pairwise_distances_argmin(Y, self.code_book_, metric="euclidean")
    return self.classes_[pred]


seed = 0
while True:
    seed += 1
    X, y = make_classification(
        n_samples=300,
        n_features=10,
        n_informative=4,
        n_redundant=0,
        random_state=seed,
        shuffle=False,
        n_classes=4,
    )
    clf = OutputCodeClassifier(
        estimator=RandomForestClassifier(random_state=seed),
        random_state=seed,
        n_jobs=-1,
    ).fit(X, y)

    print("Seed", seed)
    data = X + np.random.normal(size=X.shape)
    old = clf.predict(data)
    new = new_predict(clf, data)
    if not np.all(old == new):
        # print the data which has resulted in a mismatch
        mask = old != new
        print(mask)
        print("data", data[mask])
        print("old", old[mask])
        print("new", new[mask])
        break

This breaks in less than 100 iterations in my runs.

@glemaitre

Copy link
Copy Markdown
Member Author

OK, I wanted to be sure but basically it comes from the tie-breaking:

Seed 1
Seed 2
Seed 3
Seed 4
Seed 5
[False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False  True False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False]
data [[-1.30339834  2.68637221  3.80043205 -0.86839261 -2.06343496 -3.59474715
   0.74143858 -2.98092828  2.93166608 -2.01373733]]
old [2]
old distance [[1.17098249 1.221147   1.00558441 1.00558441]]
new [3]
new distance [[1.00558441]]

In old distance, we have 2 samples with the same distance and I assume that the new strategy does not use the same tie-breaking strategy.

I, therefore, agree that this behaviour needs to be documented. Thanks, @adrinjalali for the counter-example. I will add an entry in the change models.

@adrinjalali
adrinjalali merged commit 62a017e into scikit-learn:main Jan 4, 2023
johnpangas pushed a commit to johnpangas/scikit-learn that referenced this pull request Jan 4, 2023
jjerphan pushed a commit to jjerphan/scikit-learn that referenced this pull request Jan 20, 2023
jjerphan pushed a commit to jjerphan/scikit-learn that referenced this pull request Jan 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Quick Review For PRs that are quick to review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants