libmultilabel 0.7.0__tar.gz → 0.7.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/PKG-INFO +3 -3
  2. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel/linear/linear.py +4 -4
  3. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel/linear/metrics.py +4 -4
  4. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel/linear/tree.py +48 -9
  5. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel/linear/utils.py +6 -4
  6. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel.egg-info/PKG-INFO +3 -3
  7. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel.egg-info/requires.txt +2 -2
  8. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/setup.cfg +3 -3
  9. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/LICENSE +0 -0
  10. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/README.md +0 -0
  11. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel/__init__.py +0 -0
  12. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel/common_utils.py +0 -0
  13. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel/linear/__init__.py +0 -0
  14. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel/linear/data_utils.py +0 -0
  15. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel/linear/preprocessor.py +0 -0
  16. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel/logging.py +0 -0
  17. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel/nn/__init__.py +0 -0
  18. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel/nn/attentionxml.py +0 -0
  19. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel/nn/data_utils.py +0 -0
  20. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel/nn/metrics.py +0 -0
  21. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel/nn/model.py +0 -0
  22. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel/nn/networks/__init__.py +0 -0
  23. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel/nn/networks/bert.py +0 -0
  24. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel/nn/networks/bert_attention.py +0 -0
  25. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel/nn/networks/caml.py +0 -0
  26. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel/nn/networks/kim_cnn.py +0 -0
  27. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel/nn/networks/labelwise_attention_networks.py +0 -0
  28. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel/nn/networks/modules.py +0 -0
  29. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel/nn/networks/xml_cnn.py +0 -0
  30. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel/nn/nn_utils.py +0 -0
  31. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel.egg-info/SOURCES.txt +0 -0
  32. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel.egg-info/dependency_links.txt +0 -0
  33. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/libmultilabel.egg-info/top_level.txt +0 -0
  34. {libmultilabel-0.7.0 → libmultilabel-0.7.2}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: libmultilabel
3
- Version: 0.7.0
3
+ Version: 0.7.2
4
4
  Summary: A library for multi-class and multi-label classification
5
5
  Home-page: https://github.com/ASUS-AICS/LibMultiLabel
6
6
  Author: LibMultiLabel Team
@@ -24,12 +24,12 @@ Requires-Dist: numba
24
24
  Requires-Dist: pandas>1.3.0
25
25
  Requires-Dist: PyYAML
26
26
  Requires-Dist: scikit-learn
27
- Requires-Dist: scipy
27
+ Requires-Dist: scipy<1.14.0
28
28
  Requires-Dist: tqdm
29
29
  Provides-Extra: nn
30
30
  Requires-Dist: lightning==2.0.9; extra == "nn"
31
31
  Requires-Dist: nltk; extra == "nn"
32
- Requires-Dist: torch; extra == "nn"
32
+ Requires-Dist: torch<=2.3; extra == "nn"
33
33
  Requires-Dist: torchmetrics==0.10.3; extra == "nn"
34
34
  Requires-Dist: torchtext; extra == "nn"
35
35
  Requires-Dist: transformers; extra == "nn"
@@ -79,7 +79,7 @@ def train_1vsrest(
79
79
  options: str = "",
80
80
  verbose: bool = True,
81
81
  ) -> FlatModel:
82
- """Trains a linear model for multiabel data using a one-vs-rest strategy.
82
+ """Trains a linear model for multi-label data using a one-vs-rest strategy.
83
83
 
84
84
  Args:
85
85
  y (sparse.csr_matrix): A 0/1 matrix with dimensions number of instances * number of classes.
@@ -139,7 +139,7 @@ def _prepare_options(x: sparse.csr_matrix, options: str) -> tuple[sparse.csr_mat
139
139
  raise ValueError("Invalid LIBLINEAR solver type. Only classification solvers are allowed.")
140
140
  else:
141
141
  # workaround for liblinear warning about unspecified solver
142
- options_split.extend(["-s", "2"])
142
+ options_split.extend(["-s", "1"])
143
143
 
144
144
  bias = -1.0
145
145
  if "-B" in options_split:
@@ -396,7 +396,7 @@ def train_cost_sensitive(
396
396
  options: str = "",
397
397
  verbose: bool = True,
398
398
  ) -> FlatModel:
399
- """Trains a linear model for multilabel data using a one-vs-rest strategy
399
+ """Trains a linear model for multi-label data using a one-vs-rest strategy
400
400
  and cross-validation to pick an optimal asymmetric misclassification cost
401
401
  for Macro-F1.
402
402
  Outperforms train_1vsrest in most aspects at the cost of higher
@@ -500,7 +500,7 @@ def train_cost_sensitive_micro(
500
500
  options: str = "",
501
501
  verbose: bool = True,
502
502
  ) -> FlatModel:
503
- """Trains a linear model for multilabel data using a one-vs-rest strategy
503
+ """Trains a linear model for multi-label data using a one-vs-rest strategy
504
504
  and cross-validation to pick an optimal asymmetric misclassification cost
505
505
  for Micro-F1.
506
506
  Outperforms train_1vsrest in most aspects at the cost of higher
@@ -300,13 +300,13 @@ def get_metrics(monitor_metrics: list[str], num_classes: int, multiclass: bool =
300
300
  monitor_metrics = []
301
301
  metrics = {}
302
302
  for metric in monitor_metrics:
303
- if re.match("P@\d+", metric):
303
+ if re.match(r"P@\d+", metric):
304
304
  metrics[metric] = PrecisionAtK(top_k=int(metric[2:]))
305
- elif re.match("R@\d+", metric):
305
+ elif re.match(r"R@\d+", metric):
306
306
  metrics[metric] = RecallAtK(top_k=int(metric[2:]))
307
- elif re.match("RP@\d+", metric):
307
+ elif re.match(r"RP@\d+", metric):
308
308
  metrics[metric] = RPrecisionAtK(top_k=int(metric[3:]))
309
- elif re.match("NDCG@\d+", metric):
309
+ elif re.match(r"NDCG@\d+", metric):
310
310
  metrics[metric] = NDCGAtK(top_k=int(metric[5:]))
311
311
  elif metric in {"Another-Macro-F1", "Macro-F1", "Micro-F1"}:
312
312
  metrics[metric] = F1(num_classes, average=metric[:-3].lower(), multiclass=multiclass)
@@ -7,6 +7,7 @@ import scipy.sparse as sparse
7
7
  import sklearn.cluster
8
8
  import sklearn.preprocessing
9
9
  from tqdm import tqdm
10
+ import psutil
10
11
 
11
12
  from . import linear
12
13
 
@@ -26,6 +27,7 @@ class Node:
26
27
  """
27
28
  self.label_map = label_map
28
29
  self.children = children
30
+ self.is_root = False
29
31
 
30
32
  def isLeaf(self) -> bool:
31
33
  return len(self.children) == 0
@@ -57,7 +59,7 @@ class TreeModel:
57
59
  x: sparse.csr_matrix,
58
60
  beam_width: int = 10,
59
61
  ) -> np.ndarray:
60
- """Calculates the decision values associated with x.
62
+ """Calculates the probability estimates associated with x.
61
63
 
62
64
  Args:
63
65
  x (sparse.csr_matrix): A matrix with dimension number of instances * number of features.
@@ -71,10 +73,10 @@ class TreeModel:
71
73
  return np.vstack([self._beam_search(all_preds[i], beam_width) for i in range(all_preds.shape[0])])
72
74
 
73
75
  def _beam_search(self, instance_preds: np.ndarray, beam_width: int) -> np.ndarray:
74
- """Predict with beam search using cached decision values for a single instance.
76
+ """Predict with beam search using cached probability estimates for a single instance.
75
77
 
76
78
  Args:
77
- instance_preds (np.ndarray): A vector of cached decision values of each node, has dimension number of labels + total number of metalabels.
79
+ instance_preds (np.ndarray): A vector of cached probability estimates of each node, has dimension number of labels + total number of metalabels.
78
80
  beam_width (int): Number of candidates considered.
79
81
 
80
82
  Returns:
@@ -93,18 +95,18 @@ class TreeModel:
93
95
  continue
94
96
  slice = np.s_[self.weight_map[node.index] : self.weight_map[node.index + 1]]
95
97
  pred = instance_preds[slice]
96
- children_score = score - np.maximum(0, 1 - pred) ** 2
98
+ children_score = score - np.square(np.maximum(0, 1 - pred))
97
99
  next_level.extend(zip(node.children, children_score.tolist()))
98
100
 
99
101
  cur_level = sorted(next_level, key=lambda pair: -pair[1])[:beam_width]
100
102
  next_level = []
101
103
 
102
104
  num_labels = len(self.root.label_map)
103
- scores = np.full(num_labels, -np.inf)
105
+ scores = np.full(num_labels, 0.0)
104
106
  for node, score in cur_level:
105
107
  slice = np.s_[self.weight_map[node.index] : self.weight_map[node.index + 1]]
106
108
  pred = instance_preds[slice]
107
- scores[node.label_map] = np.exp(score - np.maximum(0, 1 - pred) ** 2)
109
+ scores[node.label_map] = np.exp(score - np.square(np.maximum(0, 1 - pred)))
108
110
  return scores
109
111
 
110
112
 
@@ -116,7 +118,7 @@ def train_tree(
116
118
  dmax=10,
117
119
  verbose: bool = True,
118
120
  ) -> TreeModel:
119
- """Trains a linear model for multiabel data using a divide-and-conquer strategy.
121
+ """Trains a linear model for multi-label data using a divide-and-conquer strategy.
120
122
  The algorithm used is based on https://github.com/xmc-aalto/bonsai.
121
123
 
122
124
  Args:
@@ -133,20 +135,39 @@ def train_tree(
133
135
  label_representation = (y.T * x).tocsr()
134
136
  label_representation = sklearn.preprocessing.normalize(label_representation, norm="l2", axis=1)
135
137
  root = _build_tree(label_representation, np.arange(y.shape[1]), 0, K, dmax)
138
+ root.is_root = True
136
139
 
137
140
  num_nodes = 0
141
+ # Both type(x) and type(y) are sparse.csr_matrix
142
+ # However, type((x != 0).T) becomes sparse.csc_matrix
143
+ # So type((x != 0).T * y) results in sparse.csc_matrix
144
+ features_used_perlabel = (x != 0).T * y
138
145
 
139
146
  def count(node):
140
147
  nonlocal num_nodes
141
148
  num_nodes += 1
149
+ node.num_features_used = np.count_nonzero(features_used_perlabel[:, node.label_map].sum(axis=1))
142
150
 
143
151
  root.dfs(count)
144
152
 
153
+ model_size = get_estimated_model_size(root)
154
+ print(f"The estimated tree model size is: {model_size / (1024**3):.3f} GB")
155
+
156
+ # Calculate the total memory (excluding swap) on the local machine
157
+ total_memory = psutil.virtual_memory().total
158
+ print(f"Your system memory is: {total_memory / (1024**3):.3f} GB")
159
+
160
+ if total_memory <= model_size:
161
+ raise MemoryError(f"Not enough memory to train the model.")
162
+
145
163
  pbar = tqdm(total=num_nodes, disable=not verbose)
146
164
 
147
165
  def visit(node):
148
- relevant_instances = y[:, node.label_map].getnnz(axis=1) > 0
149
- _train_node(y[relevant_instances], x[relevant_instances], options, node)
166
+ if node.is_root:
167
+ _train_node(y, x, options, node)
168
+ else:
169
+ relevant_instances = y[:, node.label_map].getnnz(axis=1) > 0
170
+ _train_node(y[relevant_instances], x[relevant_instances], options, node)
150
171
  pbar.update()
151
172
 
152
173
  root.dfs(visit)
@@ -195,6 +216,24 @@ def _build_tree(label_representation: sparse.csr_matrix, label_map: np.ndarray,
195
216
  return Node(label_map=label_map, children=children)
196
217
 
197
218
 
219
+ def get_estimated_model_size(root):
220
+ total_num_weights = 0
221
+
222
+ def collect_stat(node: Node):
223
+ nonlocal total_num_weights
224
+
225
+ if node.isLeaf():
226
+ total_num_weights += len(node.label_map) * node.num_features_used
227
+ else:
228
+ total_num_weights += len(node.children) * node.num_features_used
229
+
230
+ root.dfs(collect_stat)
231
+
232
+ # 16 is because when storing sparse matrices, indices (int64) require 8 bytes and floats require 8 bytes
233
+ # Our study showed that among the used features of every binary classification problem, on average no more than 2/3 of weights obtained by the dual coordinate descent method are non-zeros.
234
+ return total_num_weights * 16 * 2 / 3
235
+
236
+
198
237
  def _train_node(y: sparse.csr_matrix, x: sparse.csr_matrix, options: str, node: Node):
199
238
  """If node is internal, computes the metalabels representing each child and trains
200
239
  on the metalabels. Otherwise, train on y.
@@ -76,17 +76,18 @@ class MultiLabelEstimator(sklearn.base.BaseEstimator):
76
76
  scoring_metric (str, optional): The scoring metric. Defaults to 'P@1'.
77
77
  """
78
78
 
79
- def __init__(self, options: str = "", linear_technique: str = "1vsrest", scoring_metric: str = "P@1"):
79
+ def __init__(self, options: str = "", linear_technique: str = "1vsrest", scoring_metric: str = "P@1", multiclass: bool = False):
80
80
  super().__init__()
81
81
  self.options = options
82
82
  self.linear_technique = linear_technique
83
83
  self.scoring_metric = scoring_metric
84
84
  self._is_fitted = False
85
+ self.multiclass = multiclass
85
86
 
86
87
  def fit(self, X: sparse.csr_matrix, y: sparse.csr_matrix):
87
88
  X, y = sklearn.utils.validation.check_X_y(X, y, accept_sparse=True, multi_output=True)
88
89
  self._is_fitted = True
89
- self.model = LINEAR_TECHNIQUES[self.linear_technique](y, X, self.options)
90
+ self.model = LINEAR_TECHNIQUES[self.linear_technique](y, X, options=self.options)
90
91
  return self
91
92
 
92
93
  def predict(self, X: sparse.csr_matrix) -> np.ndarray:
@@ -96,8 +97,9 @@ class MultiLabelEstimator(sklearn.base.BaseEstimator):
96
97
 
97
98
  def score(self, X: sparse.csr_matrix, y: sparse.csr_matrix) -> float:
98
99
  metrics = linear.get_metrics(
99
- [self.scoring_metric],
100
- y.shape[1],
100
+ monitor_metrics=[self.scoring_metric],
101
+ num_classes=y.shape[1],
102
+ multiclass=self.multiclass
101
103
  )
102
104
  preds = self.predict(X)
103
105
  metrics.update(preds, y.toarray())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: libmultilabel
3
- Version: 0.7.0
3
+ Version: 0.7.2
4
4
  Summary: A library for multi-class and multi-label classification
5
5
  Home-page: https://github.com/ASUS-AICS/LibMultiLabel
6
6
  Author: LibMultiLabel Team
@@ -24,12 +24,12 @@ Requires-Dist: numba
24
24
  Requires-Dist: pandas>1.3.0
25
25
  Requires-Dist: PyYAML
26
26
  Requires-Dist: scikit-learn
27
- Requires-Dist: scipy
27
+ Requires-Dist: scipy<1.14.0
28
28
  Requires-Dist: tqdm
29
29
  Provides-Extra: nn
30
30
  Requires-Dist: lightning==2.0.9; extra == "nn"
31
31
  Requires-Dist: nltk; extra == "nn"
32
- Requires-Dist: torch; extra == "nn"
32
+ Requires-Dist: torch<=2.3; extra == "nn"
33
33
  Requires-Dist: torchmetrics==0.10.3; extra == "nn"
34
34
  Requires-Dist: torchtext; extra == "nn"
35
35
  Requires-Dist: transformers; extra == "nn"
@@ -3,13 +3,13 @@ numba
3
3
  pandas>1.3.0
4
4
  PyYAML
5
5
  scikit-learn
6
- scipy
6
+ scipy<1.14.0
7
7
  tqdm
8
8
 
9
9
  [nn]
10
10
  lightning==2.0.9
11
11
  nltk
12
- torch
12
+ torch<=2.3
13
13
  torchmetrics==0.10.3
14
14
  torchtext
15
15
  transformers
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = libmultilabel
3
- version = 0.7.0
3
+ version = 0.7.2
4
4
  author = LibMultiLabel Team
5
5
  license = MIT License
6
6
  license_file = LICENSE
@@ -30,7 +30,7 @@ install_requires =
30
30
  pandas>1.3.0
31
31
  PyYAML
32
32
  scikit-learn
33
- scipy
33
+ scipy<1.14.0
34
34
  tqdm
35
35
  python_requires = >=3.8
36
36
 
@@ -38,7 +38,7 @@ python_requires = >=3.8
38
38
  nn =
39
39
  lightning==2.0.9
40
40
  nltk
41
- torch
41
+ torch<=2.3
42
42
  torchmetrics==0.10.3
43
43
  torchtext
44
44
  transformers
File without changes
File without changes