from builtins import range
from ..base import MLClassifierBase
from ..utils import get_matrix_in_format
from sklearn.neighbors import NearestNeighbors
import numpy as np
import scipy.sparse as sparse
[docs]class MLkNN(MLClassifierBase):
"""kNN classification method adapted for multi-label classification
MLkNN builds uses k-NearestNeighbors find nearest examples to a test class and uses Bayesian inference
to select assigned labels.
Parameters
----------
k : int
number of neighbours of each input instance to take into account
s: float (default is 1.0)
the smoothing parameter
ignore_first_neighbours : int (default is 0)
ability to ignore first N neighbours, useful for comparing
with other classification software.
Attributes
----------
knn_ : an instance of sklearn.NearestNeighbors
the nearest neighbors single-label classifier used underneath
.. note:: If you don't know what :code:`ignore_first_neighbours`
does, the default is safe. Please see this `issue`_.
.. _issue: https://github.com/scikit-multilearn/scikit-multilearn/issues/22
References
----------
If you use this classifier please cite the original paper introducing the method:
.. code :: bibtex
@article{zhang2007ml,
title={ML-KNN: A lazy learning approach to multi-label learning},
author={Zhang, Min-Ling and Zhou, Zhi-Hua},
journal={Pattern recognition},
volume={40},
number={7},
pages={2038--2048},
year={2007},
publisher={Elsevier}
}
Examples
--------
Here's a very simple example of using MLkNN with a fixed number of neighbors:
.. code :: python
from skmultilearn.adapt import MLkNN
classifier = MLkNN(k=3)
# train
classifier.fit(X_train, y_train)
# predict
predictions = classifier.predict(X_test)
You can also use :class:`~sklearn.model_selection.GridSearchCV` to find an optimal set of parameters:
.. code :: python
from skmultilearn.adapt import MLkNN
from sklearn.model_selection import GridSearchCV
parameters = {'k': range(1,3), 's': [0.5, 0.7, 1.0]}
score = 'f1_macro'
clf = GridSearchCV(MLkNN(), parameters, scoring=score)
clf.fit(X, y)
print (clf.best_params_, clf.best_score_)
# output
({'k': 1, 's': 0.5}, 0.78988303374297597)
"""
def __init__(self, k=10, s=1.0, ignore_first_neighbours=0):
"""Initializes the classifier
Parameters
----------
k : int
number of neighbours of each input instance to take into account
s: float (default is 1.0)
the smoothing parameter
ignore_first_neighbours : int (default is 0)
ability to ignore first N neighbours, useful for comparing
with other classification software.
Attributes
----------
knn_ : an instance of sklearn.NearestNeighbors
the nearest neighbors single-label classifier used underneath
.. note:: If you don't know what :code:`ignore_first_neighbours`
does, the default is safe. Please see this `issue`_.
.. _issue: https://github.com/scikit-multilearn/scikit-multilearn/issues/22
"""
super(MLkNN, self).__init__()
self.k = k # Number of neighbours
self.s = s # Smooth parameter
self.ignore_first_neighbours = ignore_first_neighbours
self.copyable_attrs = ['k', 's', 'ignore_first_neighbours']
def _compute_prior(self, y):
"""Helper function to compute for the prior probabilities
Parameters
----------
y : numpy.ndarray or scipy.sparse
the training labels
Returns
-------
numpy.ndarray
the prior probability given true
numpy.ndarray
the prior probability given false
"""
prior_prob_true = np.array((self.s + y.sum(axis=0)) / (self.s * 2 + self._num_instances))[0]
prior_prob_false = 1 - prior_prob_true
return (prior_prob_true, prior_prob_false)
def _compute_cond(self, X, y):
"""Helper function to compute for the posterior probabilities
Parameters
----------
X : numpy.ndarray or scipy.sparse
input features, can be a dense or sparse matrix of size
:code:`(n_samples, n_features)`
y : numpy.ndaarray or scipy.sparse {0,1}
binary indicator matrix with label assignments.
Returns
-------
numpy.ndarray
the posterior probability given true
numpy.ndarray
the posterior probability given false
"""
self.knn_ = NearestNeighbors(self.k).fit(X)
c = sparse.lil_matrix((self._num_labels, self.k + 1), dtype='i8')
cn = sparse.lil_matrix((self._num_labels, self.k + 1), dtype='i8')
label_info = get_matrix_in_format(y, 'dok')
neighbors = [a[self.ignore_first_neighbours:] for a in
self.knn_.kneighbors(X, self.k + self.ignore_first_neighbours, return_distance=False)]
for instance in range(self._num_instances):
deltas = label_info[neighbors[instance], :].sum(axis=0)
for label in range(self._num_labels):
if label_info[instance, label] == 1:
c[label, deltas[0, label]] += 1
else:
cn[label, deltas[0, label]] += 1
c_sum = c.sum(axis=1)
cn_sum = cn.sum(axis=1)
cond_prob_true = sparse.lil_matrix((self._num_labels, self.k + 1), dtype='float')
cond_prob_false = sparse.lil_matrix((self._num_labels, self.k + 1), dtype='float')
for label in range(self._num_labels):
for neighbor in range(self.k + 1):
cond_prob_true[label, neighbor] = (self.s + c[label, neighbor]) / (
self.s * (self.k + 1) + c_sum[label, 0])
cond_prob_false[label, neighbor] = (self.s + cn[label, neighbor]) / (
self.s * (self.k + 1) + cn_sum[label, 0])
return cond_prob_true, cond_prob_false
[docs] def fit(self, X, y):
"""Fit classifier with training data
Parameters
----------
X : numpy.ndarray or scipy.sparse
input features, can be a dense or sparse matrix of size
:code:`(n_samples, n_features)`
y : numpy.ndaarray or scipy.sparse {0,1}
binary indicator matrix with label assignments.
Returns
-------
self
fitted instance of self
"""
self._label_cache = get_matrix_in_format(y, 'lil')
self._num_instances = self._label_cache.shape[0]
self._num_labels = self._label_cache.shape[1]
# Computing the prior probabilities
self._prior_prob_true, self._prior_prob_false = self._compute_prior(self._label_cache)
# Computing the posterior probabilities
self._cond_prob_true, self._cond_prob_false = self._compute_cond(X, self._label_cache)
return self
[docs] def predict(self, X):
"""Predict labels for X
Parameters
----------
X : numpy.ndarray or scipy.sparse.csc_matrix
input features of shape :code:`(n_samples, n_features)`
Returns
-------
scipy.sparse matrix of int
binary indicator matrix with label assignments with shape
:code:`(n_samples, n_labels)`
"""
result = sparse.lil_matrix((X.shape[0], self._num_labels), dtype='i8')
neighbors = [a[self.ignore_first_neighbours:] for a in
self.knn_.kneighbors(X, self.k + self.ignore_first_neighbours, return_distance=False)]
for instance in range(X.shape[0]):
deltas = self._label_cache[neighbors[instance],].sum(axis=0)
for label in range(self._num_labels):
p_true = self._prior_prob_true[label] * self._cond_prob_true[label, deltas[0, label]]
p_false = self._prior_prob_false[label] * self._cond_prob_false[label, deltas[0, label]]
result[instance, label] = int(p_true >= p_false)
return result
[docs] def predict_proba(self, X):
"""Predict probabilities of label assignments for X
Parameters
----------
X : numpy.ndarray or scipy.sparse.csc_matrix
input features of shape :code:`(n_samples, n_features)`
Returns
-------
scipy.sparse matrix of int
binary indicator matrix with label assignment probabilities
with shape :code:`(n_samples, n_labels)`
"""
result = sparse.lil_matrix((X.shape[0], self._num_labels), dtype='float')
neighbors = [a[self.ignore_first_neighbours:] for a in
self.knn_.kneighbors(X, self.k + self.ignore_first_neighbours, return_distance=False)]
for instance in range(X.shape[0]):
deltas = self._label_cache[neighbors[instance],].sum(axis=0)
for label in range(self._num_labels):
p_true = self._prior_prob_true[label] * self._cond_prob_true[label, deltas[0, label]]
result[instance, label] = p_true
return result