Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add linear tree classifier #274

Merged
merged 86 commits into from
Mar 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
8682c9c
initial tree implementation
Sinacam Feb 14, 2022
bc3abb6
changed dfs signature
Sinacam Feb 14, 2022
5d1ae23
added beam search short circuit
Sinacam Feb 14, 2022
0940c9d
refactored init and train
Sinacam Feb 14, 2022
2320d9e
leaf nodes debugged
Sinacam Feb 18, 2022
37f09d4
fixed up to beam search
Sinacam Feb 18, 2022
9349af4
working with sigmoid
Sinacam Feb 19, 2022
d30b728
updated beam search
Sinacam Feb 20, 2022
e5789a9
updated beam search
Sinacam Feb 20, 2022
ba9aed7
updated beam search
Sinacam Feb 20, 2022
f621003
wip with timing
Sinacam Feb 25, 2022
88b04ae
minor exp update
Sinacam Feb 27, 2022
33d60cf
initial tree implementation
Sinacam Feb 14, 2022
7b85bd6
changed dfs signature
Sinacam Feb 14, 2022
1ae8431
added beam search short circuit
Sinacam Feb 14, 2022
07b5b97
refactored init and train
Sinacam Feb 14, 2022
4ae4ee5
leaf nodes debugged
Sinacam Feb 18, 2022
755b93d
fixed up to beam search
Sinacam Feb 18, 2022
39f06fe
working with sigmoid
Sinacam Feb 19, 2022
3bf881c
updated beam search
Sinacam Feb 20, 2022
2bc62a6
updated beam search
Sinacam Feb 20, 2022
e2d63ba
updated beam search
Sinacam Feb 20, 2022
f135eca
wip with timing
Sinacam Feb 25, 2022
737b6b4
Merge branch 'linear-tree' of https://github.com/ntumlgroup/LibMultiL…
Sinacam Apr 5, 2022
f0857d0
slicing in two dimensions does not create a view
Sinacam Jun 8, 2022
93b6ebe
fixed beam search
Sinacam Jun 8, 2022
27c0b79
fixed beam search
Sinacam Jul 6, 2022
31128ee
initial tree implementation
Sinacam Feb 14, 2022
ab8e2fb
changed dfs signature
Sinacam Feb 14, 2022
2b69a67
added beam search short circuit
Sinacam Feb 14, 2022
93c7480
refactored init and train
Sinacam Feb 14, 2022
a49b23d
leaf nodes debugged
Sinacam Feb 18, 2022
af1a35a
fixed up to beam search
Sinacam Feb 18, 2022
70bcbaa
working with sigmoid
Sinacam Feb 19, 2022
3144b86
updated beam search
Sinacam Feb 20, 2022
494fb47
updated beam search
Sinacam Feb 20, 2022
52e96d2
updated beam search
Sinacam Feb 20, 2022
22a1ffc
wip with timing
Sinacam Feb 25, 2022
9df1f3c
initial tree implementation
Sinacam Feb 14, 2022
095a124
changed dfs signature
Sinacam Feb 14, 2022
0a97155
added beam search short circuit
Sinacam Feb 14, 2022
74e03bc
refactored init and train
Sinacam Feb 14, 2022
be8a87f
leaf nodes debugged
Sinacam Feb 18, 2022
5f7e485
fixed up to beam search
Sinacam Feb 18, 2022
7adec38
working with sigmoid
Sinacam Feb 19, 2022
2109017
updated beam search
Sinacam Feb 20, 2022
aad1c8b
updated beam search
Sinacam Feb 20, 2022
d6137a9
updated beam search
Sinacam Feb 20, 2022
e21c57e
wip with timing
Sinacam Feb 25, 2022
06a3f57
minor exp update
Sinacam Feb 27, 2022
fc72583
slicing in two dimensions does not create a view
Sinacam Jun 8, 2022
35eb6db
fixed beam search
Sinacam Jun 8, 2022
5641c70
fixed beam search
Sinacam Jul 6, 2022
2965403
updated tree
Sinacam Jul 14, 2022
c5fe1a0
merged
Sinacam Jul 14, 2022
23f2177
cleaned up
Sinacam Nov 26, 2022
14e8f5e
added comment
Sinacam Nov 26, 2022
325446e
fixed probability
Sinacam Dec 8, 2022
481f1ab
added cached tree
Sinacam Dec 26, 2022
2f2b72a
fixed score
Sinacam Dec 26, 2022
982e0b7
added global tree
Sinacam Dec 26, 2022
151b791
fixed decision value
Sinacam Jan 13, 2023
d65fcd4
added weirdlosstree
Sinacam Feb 20, 2023
b494a6a
added spherical kmeans
Sinacam Feb 23, 2023
c80d1f1
Squashed commit of 60bfc4fa..c5103b41
Sinacam Mar 4, 2023
679555e
shut up 1vsrest
Sinacam Feb 23, 2023
3a05053
updated spherical kmeans
Sinacam Feb 23, 2023
add0b8c
reordered loop
Sinacam Feb 23, 2023
f655134
Squashed normalization commits
Sinacam Mar 4, 2023
ba2df85
Merge branch 'master' into linear-tree
Sinacam Mar 4, 2023
dd4cee1
tentative custom kmeans
Sinacam Mar 5, 2023
fa2f5bb
fixed power
Sinacam Mar 6, 2023
da75563
set centroid to random point if cluster is empty
Sinacam Mar 8, 2023
78d4865
first public api
Sinacam Mar 8, 2023
a8a0c92
Merge branch 'master' into tree-pr
Sinacam Mar 8, 2023
6828192
bugfix and cli
Sinacam Mar 8, 2023
fe537cb
added tqdm
Sinacam Mar 8, 2023
a6a1bf8
organize imports
Sinacam Mar 8, 2023
42973cd
clarified loop
Sinacam Mar 8, 2023
aedfd4f
added comments
Sinacam Mar 8, 2023
67531fc
improved naming and comments
Sinacam Mar 10, 2023
e9c5454
improved naming and comments
Sinacam Mar 12, 2023
690450d
changed FlatModel to use attributes
Sinacam Mar 12, 2023
ff264d5
clarified reshaping
Sinacam Mar 12, 2023
0072736
removed beam width from cli
Sinacam Mar 13, 2023
79a95c6
added docs, citation and improved readability
Sinacam Mar 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/api/linear.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ The simplest usage is::

.. autofunction:: train_binary_and_multiclass

.. autofunction:: train_tree

.. autofunction:: predict_values


Expand Down
1 change: 1 addition & 0 deletions libmultilabel/linear/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .linear import *
from .metrics import get_metrics, tabulate_metrics
from .preprocessor import *
from .tree import *
from .utils import *
140 changes: 100 additions & 40 deletions libmultilabel/linear/linear.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,55 @@
'predict_values']


def train_1vsrest(y: sparse.csr_matrix, x: sparse.csr_matrix, options: str):
class FlatModel:
def __init__(self, weights: np.matrix,
bias: float,
thresholds: 'float | np.ndarray',
):
self.weights = weights
self.bias = bias
self.thresholds = thresholds

def predict_values(self, x: sparse.csr_matrix) -> np.ndarray:
"""Calculates the decision values associated with x.

Args:
x (sparse.csr_matrix): A matrix with dimension number of instances * number of features.

Returns:
np.ndarray: A matrix with dimension number of instances * number of classes.
"""
bias = self.bias
bias_col = np.full((x.shape[0], 1 if bias > 0 else 0), bias)
num_feature = self.weights.shape[0]
num_feature -= 1 if bias > 0 else 0
if x.shape[1] < num_feature:
x = sparse.hstack([
x,
np.zeros((x.shape[0], num_feature - x.shape[1])),
bias_col,
], 'csr')
else:
x = sparse.hstack([
x[:, :num_feature],
bias_col,
], 'csr')

return (x * self.weights).A + self.thresholds


def train_1vsrest(y: sparse.csr_matrix,
x: sparse.csr_matrix,
options: str,
verbose: bool = True
) -> FlatModel:
"""Trains a linear model for multiabel data using a one-vs-rest strategy.

Args:
y (sparse.csr_matrix): A 0/1 matrix with dimensions number of instances * number of classes.
x (sparse.csr_matrix): A matrix with dimensions number of instances * number of features.
options (str): The option string passed to liblinear.
verbose (bool, optional): Output extra progress information. Defaults to True.

Returns:
A model which can be used in predict_values.
Expand All @@ -33,12 +75,15 @@ def train_1vsrest(y: sparse.csr_matrix, x: sparse.csr_matrix, options: str):
num_feature = x.shape[1]
weights = np.zeros((num_feature, num_class), order='F')

logging.info(f'Training one-vs-rest model on {num_class} labels')
for i in tqdm(range(num_class)):
if verbose:
logging.info(f'Training one-vs-rest model on {num_class} labels')
for i in tqdm(range(num_class), disable=not verbose):
yi = y[:, i].toarray().reshape(-1)
weights[:, i] = do_train(2*yi - 1, x, options).ravel()

return {'weights': np.asmatrix(weights), '-B': bias, 'threshold': 0}
return FlatModel(weights=np.asmatrix(weights),
bias=bias,
thresholds=0)


def prepare_options(x: sparse.csr_matrix, options: str) -> 'tuple[sparse.csr_matrix, str, float]':
Expand Down Expand Up @@ -84,9 +129,13 @@ def prepare_options(x: sparse.csr_matrix, options: str) -> 'tuple[sparse.csr_mat
return x, options, bias


def train_thresholding(y: sparse.csr_matrix, x: sparse.csr_matrix, options: str):
def train_thresholding(y: sparse.csr_matrix,
x: sparse.csr_matrix,
options: str,
verbose: bool = True
) -> FlatModel:
"""Trains a linear model for multilabel data using a one-vs-rest strategy
and cross-validation to pick an optimal decision threshold for Macro-F1.
and cross-validation to pick optimal decision thresholds for Macro-F1.
Outperforms train_1vsrest in most aspects at the cost of higher
time complexity.
See user guide for more details.
Expand All @@ -95,6 +144,7 @@ def train_thresholding(y: sparse.csr_matrix, x: sparse.csr_matrix, options: str)
y (sparse.csr_matrix): A 0/1 matrix with dimensions number of instances * number of classes.
x (sparse.csr_matrix): A matrix with dimensions number of instances * number of features.
options (str): The option string passed to liblinear.
verbose (bool, optional): Output extra progress information. Defaults to True.

Returns:
A model which can be used in predict_values.
Expand All @@ -108,14 +158,17 @@ def train_thresholding(y: sparse.csr_matrix, x: sparse.csr_matrix, options: str)
weights = np.zeros((num_feature, num_class), order='F')
thresholds = np.zeros(num_class)

logging.info(f'Training thresholding model on {num_class} labels')
for i in tqdm(range(num_class)):
if verbose:
logging.info(f'Training thresholding model on {num_class} labels')
for i in tqdm(range(num_class), disable=not verbose):
yi = y[:, i].toarray().reshape(-1)
w, t = thresholding_one_label(2*yi - 1, x, options)
weights[:, i] = w.ravel()
thresholds[i] = t

return {'weights': np.asmatrix(weights), '-B': bias, 'threshold': thresholds}
return FlatModel(weights=np.asmatrix(weights),
bias=bias,
thresholds=thresholds)


def thresholding_one_label(y: np.ndarray,
Expand Down Expand Up @@ -318,7 +371,11 @@ def fmeasure(y_true: np.ndarray, y_pred: np.ndarray) -> float:
return F


def train_cost_sensitive(y: sparse.csr_matrix, x: sparse.csr_matrix, options: str):
def train_cost_sensitive(y: sparse.csr_matrix,
x: sparse.csr_matrix,
options: str,
verbose: bool = True
) -> FlatModel:
"""Trains a linear model for multilabel data using a one-vs-rest strategy
and cross-validation to pick an optimal asymmetric misclassification cost
for Macro-F1.
Expand All @@ -330,6 +387,7 @@ def train_cost_sensitive(y: sparse.csr_matrix, x: sparse.csr_matrix, options: st
y (sparse.csr_matrix): A 0/1 matrix with dimensions number of instances * number of classes.
x (sparse.csr_matrix): A matrix with dimensions number of instances * number of features.
options (str): The option string passed to liblinear.
verbose (bool, optional): Output extra progress information. Defaults to True.

Returns:
A model which can be used in predict_values.
Expand All @@ -342,14 +400,17 @@ def train_cost_sensitive(y: sparse.csr_matrix, x: sparse.csr_matrix, options: st
num_feature = x.shape[1]
weights = np.zeros((num_feature, num_class), order='F')

logging.info(
f'Training cost-sensitive model for Macro-F1 on {num_class} labels')
for i in tqdm(range(num_class)):
if verbose:
logging.info(
f'Training cost-sensitive model for Macro-F1 on {num_class} labels')
for i in tqdm(range(num_class), disable=not verbose):
yi = y[:, i].toarray().reshape(-1)
w = cost_sensitive_one_label(2*yi - 1, x, options)
weights[:, i] = w.ravel()

return {'weights': np.asmatrix(weights), '-B': bias, 'threshold': 0}
return FlatModel(weights=np.asmatrix(weights),
bias=bias,
thresholds=0)


def cost_sensitive_one_label(y: np.ndarray,
Expand Down Expand Up @@ -415,7 +476,11 @@ def cross_validate(y: np.ndarray,
return 2*pred - 1


def train_cost_sensitive_micro(y: sparse.csr_matrix, x: sparse.csr_matrix, options: str):
def train_cost_sensitive_micro(y: sparse.csr_matrix,
x: sparse.csr_matrix,
options: str,
verbose: bool = True
) -> FlatModel:
"""Trains a linear model for multilabel data using a one-vs-rest strategy
and cross-validation to pick an optimal asymmetric misclassification cost
for Micro-F1.
Expand All @@ -427,6 +492,7 @@ def train_cost_sensitive_micro(y: sparse.csr_matrix, x: sparse.csr_matrix, optio
y (sparse.csr_matrix): A 0/1 matrix with dimensions number of instances * number of classes.
x (sparse.csr_matrix): A matrix with dimensions number of instances * number of features.
options (str): The option string passed to liblinear.
verbose (bool, optional): Output extra progress information. Defaults to True.

Returns:
A model which can be used in predict_values.
Expand All @@ -444,11 +510,12 @@ def train_cost_sensitive_micro(y: sparse.csr_matrix, x: sparse.csr_matrix, optio
param_space = [1, 1.33, 1.8, 2.5, 3.67, 6, 13]
bestScore = -np.Inf

logging.info(
f'Training cost-sensitive model for Micro-F1 on {num_class} labels')
if verbose:
logging.info(
f'Training cost-sensitive model for Micro-F1 on {num_class} labels')
for a in param_space:
tp = fn = fp = 0
for i in tqdm(range(num_class)):
for i in tqdm(range(num_class), disable=not verbose):
yi = y[:, i].toarray().reshape(-1)
yi = 2*yi - 1

Expand All @@ -469,16 +536,23 @@ def train_cost_sensitive_micro(y: sparse.csr_matrix, x: sparse.csr_matrix, optio
w = do_train(2*yi - 1, x, final_options)
weights[:, i] = w.ravel()

return {'weights': np.asmatrix(weights), '-B': bias, 'threshold': 0}
return FlatModel(weights=np.asmatrix(weights),
bias=bias,
thresholds=0)


def train_binary_and_multiclass(y: sparse.csr_matrix, x: sparse.csr_matrix, options: str):
def train_binary_and_multiclass(y: sparse.csr_matrix,
x: sparse.csr_matrix,
options: str,
verbose: bool = True
) -> FlatModel:
"""Trains a linear model for binary and multi-class data.

Args:
y (sparse.csr_matrix): A 0/1 matrix with dimensions number of instances * number of classes.
x (sparse.csr_matrix): A matrix with dimensions number of instances * number of features.
options (str): The option string passed to liblinear.
verbose (bool, optional): Output extra progress information. Defaults to True.

Returns:
A model which can be used in predict_values.
Expand Down Expand Up @@ -508,9 +582,11 @@ def train_binary_and_multiclass(y: sparse.csr_matrix, x: sparse.csr_matrix, opti
weights[:, train_labels] = w

# For labels not appeared in training, assign thresholds to -inf so they won't be predicted.
threshold = np.full(num_labels, -np.inf)
threshold[train_labels] = 0
return {'weights': np.asmatrix(weights), '-B': bias, 'threshold': threshold}
thresholds = np.full(num_labels, -np.inf)
thresholds[train_labels] = 0
return FlatModel(weights=np.asmatrix(weights),
bias=bias,
thresholds=thresholds)


def predict_values(model, x: sparse.csr_matrix) -> np.ndarray:
Expand All @@ -523,23 +599,7 @@ def predict_values(model, x: sparse.csr_matrix) -> np.ndarray:
Returns:
np.ndarray: A matrix with dimension number of instances * number of classes.
"""
bias = model['-B']
bias_col = np.full((x.shape[0], 1 if bias > 0 else 0), bias)
num_feature = model['weights'].shape[0]
num_feature -= 1 if bias > 0 else 0
if x.shape[1] < num_feature:
x = sparse.hstack([
x,
np.zeros((x.shape[0], num_feature - x.shape[1])),
bias_col,
], 'csr')
else:
x = sparse.hstack([
x[:, :num_feature],
bias_col,
], 'csr')

return (x * model['weights']).A + model['threshold']
return model.predict_values(x)


def get_topk_labels(label_mapping: np.ndarray, preds: np.ndarray, top_k: int = 5) -> 'list[list[str]]':
Expand Down
Loading