-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathecoc.py
More file actions
230 lines (178 loc) · 8.04 KB
/
Copy pathecoc.py
File metadata and controls
230 lines (178 loc) · 8.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
import numpy as np
from sklearn.base import BaseEstimator, ClassifierMixin, clone
from sympy.combinatorics.graycode import GrayCode
from scipy.spatial.distance import hamming
from copy import deepcopy
class EcocEstimator(BaseEstimator, ClassifierMixin):
"""
ECOC meta classifier (works without sklearn.OutputCodeClassifier restrictions)
http://www.cs.cmu.edu/~aberger/pdf/ecoc.pdf
"""
@staticmethod
def softmax(x):
"""Compute softmax values for each sets of scores in x."""
e_x = np.exp(x - np.max(x))
return e_x / e_x.sum(axis=0) # only difference
def __init__(self, base_estimator, code_size=1.0, random_state=42):
self.random_state = random_state
self.base_estimator = base_estimator
self.code_size = code_size
self.error_code = None
self.estimators = None
self.class2id = None
self.id2class = None
def _make_mappings(self, unique_classes):
class2id = {v: k for k, v in enumerate(unique_classes)}
id2class = {k: v for k, v in enumerate(unique_classes)}
return class2id, id2class
def _map_values(self, classes, mapping):
return np.array(list(
map(lambda x: mapping[x], classes)
))
def _generate_gray_codes(self, code_size):
gray = np.array([[int(j) for j in i] for i in GrayCode(code_size).generate_gray()])
total_values_sum = gray.sum(axis=1)
to_delete = [total_values_sum.argmax(), total_values_sum.argmin()]
return np.delete(gray, to_delete, axis=0)
def _hamming_corrmat(self, gray):
result = []
# TODO: reimplement a method, very slow and unoptimized
for i in gray:
row = []
for j in gray:
row.append(hamming(i, j))
result.append(row)
return np.array(result)
def _greedy_search(self, a, size=None):
if size is None:
size = int(np.log2(a.shape[0] + 1))
result = []
current = np.random.randint(0, a.shape[0])
result.append(current)
for i in range(size - 1):
row_sum = a[:, result].sum(1)
min_border = np.percentile(row_sum, 85)
valid_vals = row_sum[row_sum >= min_border]
max_index = np.random.choice(
np.argwhere(np.isin(row_sum, valid_vals)).flatten()
, 1).squeeze()
result.append(int(max_index))
a[current, max_index] = -1
a[max_index, current] = -1
current = max_index
return result
def fit(self, X, y=None):
if y is None:
raise Exception('y should be initialized')
unique_classes = np.unique(y)
self.class2id, self.id2class = self._make_mappings(unique_classes)
_y = self._map_values(y, self.class2id)
classes_size = unique_classes.shape[0]
code_size = int(classes_size * self.code_size)
if code_size < np.log2(classes_size):
print('inapropriate code size, try to adjust it')
if code_size > 8:
# if code size is too small random initialization
# is not robust enough, it is more rational to use
# gray codes to generate codes with maximum distance
np.random.seed(self.random_state)
self.error_code = np.random.randint(2, size=(classes_size, code_size))
while set([0, classes_size]).intersection(self.error_code.sum(0)) \
or set([0, code_size]).intersection(self.error_code.sum(1)):
self.error_code = np.random.randint(2, size=(classes_size, code_size))
else:
gray_codes = self._generate_gray_codes(classes_size)
gray_corrmat = self._hamming_corrmat(gray_codes)
codes_id = self._greedy_search(gray_corrmat, code_size)
self.error_code = np.array(gray_codes[codes_id]).T
# iterate over error codez
estimators = []
for i in range(self.error_code.shape[1]):
# print('fitting %d-th classifier out of %d' % (i + 1, code_size))
classifier_mapping = self.error_code[:, i]
# print(classifier_mapping)
positive = np.argwhere(classifier_mapping == 1).flatten()
negative = np.argwhere(classifier_mapping == 0).flatten()
_y_i = _y.copy()
_y_i[np.isin(_y_i, positive)] = -2
_y_i[np.isin(_y_i, negative)] = -1
_y_i[_y_i == -2] = 1
estimator = clone(self.base_estimator)
estimator.fit(X, _y_i)
estimators.append(estimator)
self.estimators = estimators
def predict(self, X, y=None):
"""
Extended ECOC model intefrace, implementation is based on
http://www.cs.cmu.edu/~aberger/pdf/ecoc.pdf
:param X: classification input (can be a sparse matrix)
:return: numpy array of classification results
"""
if not hasattr(X, 'shape'):
raise Exception('X must be a numpy object')
predicted = []
for i in range(len(self.estimators)):
predicted.append(self.estimators[i].predict(X) * -1)
predicted = np.array(predicted).T
scores = []
for i in range(predicted.shape[0]):
tiled_predictions = np.tile(predicted[i], (self.error_code.shape[0], 1))
class_score = np.sum(np.abs(self.error_code - tiled_predictions), axis=1)
class_score = np.argmax(class_score)
scores.append(class_score)
return self._map_values(np.array(scores), self.id2class)
def predict_proba(self, X, y=None):
"""
Extended ECOC model intefrace, implementation is based on
http://www.cs.cmu.edu/~aberger/pdf/ecoc.pdf
:param X: classification input (can be a sparse matrix)
:return: numpy array of classification results
"""
if not hasattr(X, 'shape'):
raise Exception('X must be a numpy object')
predicted = []
for i in range(len(self.estimators)):
predicted.append(self.estimators[i].predict(X) * -1)
predicted = np.array(predicted).T
scores = []
for i in range(predicted.shape[0]):
tiled_predictions = np.tile(predicted[i], (self.error_code.shape[0], 1))
class_score = np.sum(np.abs(self.error_code - tiled_predictions), axis=1)
class_score = EcocEstimator.softmax(class_score)
scores.append(class_score)
return np.array(scores)
def predict_top_n(self, X, n=5):
"""
Predict top N most relevant classes for each record
:param X: classification input (can be a sparse matrix)
:param n: how much top results should be given
:return: numpy matrix of top predictions
"""
result = self.predict_proba(X)
sorted_probas = deepcopy(result)
sorted_probas.sort(axis=1)
sorted_probas = sorted_probas[:, -n:][:, ::-1]
class_mapper = np.vectorize(lambda x: self.id2class[x])
classes = class_mapper(result.argsort(axis=1)[:, -n:][:, ::-1])
sorted_probas = [[float(j) for j in i] for i in sorted_probas.tolist()]
classes = [[j for j in i] for i in classes.tolist()]
return list(map(lambda x: dict(zip(x[0], x[1])), zip(classes, sorted_probas)))
def shrink_prediction(self, X, y, n=5):
"""
Reduce prediction of predict_top_n with the following rule:
if the true result is in top, then replace the prediction with true result
otherwise replace prediction with the most significant prediction
:param X: classification input (can be a sparse matrix)
:param y: real labels
:param n: how much top results should be given
:return: numpy array of shrinked predictions
"""
top_args = self.predict_top_n(X, n=n)
shrinked = []
for i, r in zip(top_args, y):
if r in i:
shrinked.append(r)
else:
keys, vals = list(zip(*i.items()))
shrinked.append(keys[np.argmax(vals)])
return shrinked