Skip to content

Commit 0a0c36c

Browse files
authored
Merge pull request #7309 from gmolledaj/feature/knn-l2
[ENH] kNN with the Cosine metric
2 parents 86f208c + e0e4ae9 commit 0a0c36c

4 files changed

Lines changed: 82 additions & 2 deletions

File tree

Orange/base.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import numpy as np
99
import scipy
10+
from sklearn.preprocessing import normalize
1011

1112
from Orange.data import Table, Storage, Instance, Value, Domain
1213
from Orange.data.filter import HasClass
@@ -645,10 +646,18 @@ def __init__(self, n_neighbors=5, metric="euclidean", weights="uniform",
645646
super().__init__(preprocessors=preprocessors)
646647
self.params = vars()
647648

649+
def _initialize_wrapped(self):
650+
params = self.params.copy()
651+
if params.get("metric") == "cosine":
652+
params["metric"] = "euclidean"
653+
return self.__wraps__(**params)
654+
648655
def fit(self, X, Y, W=None):
649656
if self.params["metric_params"] is None and \
650657
self.params.get("metric") == "mahalanobis":
651658
self.params["metric_params"] = {"V": np.cov(X.T)}
659+
if self.params["metric"] == "cosine":
660+
X = normalize(X, norm="l2", axis=1, copy=True)
652661
return super().fit(X, Y, W)
653662

654663

Orange/modelling/tests/test_knn.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import unittest
2+
from unittest.mock import patch
3+
4+
import numpy as np
5+
from scipy import sparse
6+
import sklearn
7+
8+
from Orange.data import Table
9+
from Orange.modelling import KNNLearner
10+
11+
knn_init = sklearn.neighbors.KNeighborsClassifier.__init__
12+
13+
14+
class Test(unittest.TestCase):
15+
def setUp(self):
16+
x = np.array([[3, 0, 4],
17+
[12, 5, 0],
18+
[1, 2, 2]])
19+
y = np.array([1, 0, 1])
20+
self.data = Table.from_numpy(None, x, y)
21+
22+
@patch("Orange.base.SklLearner.fit")
23+
def test_cosine_normalizes(self, fit):
24+
learner = KNNLearner(metric="cosine")
25+
learner(self.data)
26+
X = fit.call_args[0][0]
27+
np.testing.assert_allclose(np.sum(X ** 2, axis=1), 1)
28+
np.testing.assert_allclose(X, [
29+
[3 / 5, 0, 4 / 5],
30+
[12 / 13, 5 / 13, 0],
31+
[1 / 3, 2 / 3, 2 / 3]])
32+
fit.reset_mock()
33+
34+
with self.data.unlocked():
35+
self.data.X = sparse.csr_matrix(self.data.X)
36+
learner(self.data)
37+
X = fit.call_args[0][0]
38+
np.testing.assert_allclose(np.sum(X ** 2, axis=1), 1)
39+
row0 = X[0]
40+
np.testing.assert_allclose(row0, row0 / np.linalg.norm(row0))
41+
42+
def test_cosine_does_not_modify_input_data(self):
43+
original = self.data.X.copy()
44+
learner = KNNLearner(metric="cosine")
45+
learner(self.data)
46+
np.testing.assert_array_equal(self.data.X, original)
47+
48+
@patch("sklearn.neighbors.KNeighborsClassifier.__init__",
49+
side_effect=knn_init, autospec=True)
50+
def test_cosine_computes_euclidean(self, mock_init):
51+
learner = KNNLearner(metric="cosine")
52+
learner(self.data)
53+
self.assertEqual(mock_init.call_args[1]["metric"], "euclidean")
54+
55+
learner = KNNLearner(metric="manhattan")
56+
learner(self.data)
57+
self.assertEqual(mock_init.call_args[1]["metric"], "manhattan")
58+
59+
60+
if __name__ == "__main__":
61+
unittest.main()

Orange/widgets/model/owknn.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ class OWKNNLearner(OWBaseLearner):
2222
LEARNER = KNNLearner
2323

2424
weights = ["uniform", "distance"]
25-
metrics = ["euclidean", "manhattan", "chebyshev", "mahalanobis"]
25+
metrics = ["euclidean", "manhattan", "chebyshev", "mahalanobis", "cosine"]
2626

2727
weights_options = ["Uniform", "By Distances"]
28-
metrics_options = ["Euclidean", "Manhattan", "Chebyshev", "Mahalanobis"]
28+
metrics_options = ["Euclidean", "Manhattan", "Chebyshev", "Mahalanobis", "Cosine"]
2929

3030
n_neighbors = Setting(5)
3131
metric_index = Setting(0)
@@ -50,13 +50,15 @@ def add_main_layout(self):
5050
def create_learner(self):
5151
return self.LEARNER(
5252
n_neighbors=self.n_neighbors,
53+
# false positive, pylint: disable=invalid-sequence-index
5354
metric=self.metrics[self.metric_index],
5455
weights=self.weights[self.weight_index],
5556
preprocessors=self.preprocessors)
5657

5758
def get_learner_parameters(self):
5859
return (("Number of neighbours", self.n_neighbors),
5960
("Metric", self.metrics_options[self.metric_index]),
61+
# false positive, pylint: disable=invalid-sequence-index
6062
("Weight", self.weights_options[self.weight_index]))
6163

6264

i18n/si/msgs.jaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,17 @@ base.py:
8282
euclidean: false
8383
uniform: false
8484
auto: false
85+
def `_initialize_wrapped`:
86+
metric: false
87+
cosine: false
88+
euclidean: false
8589
def `fit`:
8690
metric_params: false
8791
metric: false
8892
mahalanobis: false
8993
V: false
94+
cosine: false
95+
l2: false
9096
class `NNBase`:
9197
def `__init__`:
9298
relu: false
@@ -10280,12 +10286,14 @@ widgets/model/owknn.py:
1028010286
manhattan: false
1028110287
chebyshev: false
1028210288
mahalanobis: false
10289+
cosine: false
1028310290
Uniform: Enake
1028410291
By Distances: Po razdalji
1028510292
Euclidean: Evklidska
1028610293
Manhattan: Manhattanska
1028710294
Chebyshev: Čebiševa
1028810295
Mahalanobis: Mahalanobisova
10296+
Cosine: Kosinusna
1028910297
def `add_main_layout`:
1029010298
Neighbors: Sosedi
1029110299
n_neighbors: false

0 commit comments

Comments
 (0)