-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathmulti_resolution_gfmm.py
1786 lines (1537 loc) · 86.9 KB
/
multi_resolution_gfmm.py
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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
A multi-resolution hierarchical granular representation based classifier using
general fuzzy min-max neural network.
"""
# @Author: Thanh Tung KHUAT <[email protected]>
# License: GPL-3.0
import numpy as np
import time
import json
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from sklearn.metrics import accuracy_score
from joblib import Parallel, delayed
from sklearn.model_selection import StratifiedKFold
from hbbrain.base.base_estimator import BaseHyperboxClassifier
from hbbrain.base.base_gfmm_estimator import (
convert_format_missing_input_zero_one,
is_contain_missing_value
)
from hbbrain.numerical_data.multigranular_learner.base_granular_learner import BaseGranular
from hbbrain.utils.membership_calc import (
membership_func_gfmm,
get_membership_gfmm_all_classes
)
from hbbrain.utils.adjust_hyperbox import (
is_overlap_one_many_hyperboxes_num_data_general,
is_two_hyperboxes_overlap_num_data_general,
overlap_resolving_num_data
)
from hbbrain.utils.drawing_func import get_cmap, draw_box
from hbbrain.constants import (
UNLABELED_CLASS,
HOMOGENEOUS_CLASS_LEARNING, HETEROGENEOUS_CLASS_LEARNING,
)
from hbbrain.utils.drawing_func import (
generate_grid_decision_boundary_2D,
draw_decision_boundary_2D,
)
def convert_granular_theta_to_level(granular_thetas):
"""
Convert a list of maximum hyperbox sizes to the corresponding granular
levels.
Parameters
----------
granular_thetas : list
A list contains all maximum hyperbox sizes for all granularity levels.
Returns
-------
level_dic : dict
A mapping between the maximum hyperbox size and the granular level.
"""
level_dic = {}
for i, val in enumerate(granular_thetas):
level_dic[val] = i
return level_dic
def remove_contained_hyperboxes(V, W, C, N_samples, Centroids):
"""
Remove all hyperboxes contained in other hyperboxes with the same class
label and update the centroids of larger hyperboxes included the removed
hyperboxes.
Parameters
----------
V : array-like of shape (n_hyperboxes, n_features)
A matrix stores all minimal points for numerical features of all
existing hyperboxes, in which each row is a minimal point of a hyperbox.
W : array-like of shape (n_hyperboxes, n_features)
A matrix stores all maximum points for numerical features of all
existing hyperboxes, in which each row is a maximum point of a hyperbox.
C : array-like of shape (n_hyperboxes,)
A vector stores all class labels correponding to existing hyperboxes.
N_samples : array-like of shape (n_hyperboxes,)
A vector stores the number of samples fully included in each existing
hyperbox.
Centroids : array-like of shape (n_hyperboxes, n_features)
A matrix stores all centroid points of all existing hyperboxes, in
which each row is a centroid point of a hyperbox.
Returns
-------
new_V : array-like of shape (n_new_hyperboxes, n_features)
A matrix stores all minimal points for numerical features of all
hyperboxes after removal of fully contained hyperboxes, in which each
row is a minimal point of a hyperbox.
new_W : array-like of shape (n_new_hyperboxes, n_features)
A matrix stores all maximal points for numerical features of all
hyperboxes after removal of fully contained hyperboxes, in which each
row is a maximal point of a hyperbox.
new_C : array-like of shape (n_new_hyperboxes,)
A vector stores all class labels correponding to remaining hyperboxes
after removal of fully contained hyperboxes.
new_N_samples : array-like of shape (n_new_hyperboxes,)
A vector stores the number of samples fully included in each hyperbox.
new_Centroids : array-like of shape (n_new_hyperboxes, n_features)
A matrix stores all centroid points of all remaining hyperboxes after
removal of fully contained hyperboxes, in which each row is a centroid
point of a hyperbox.
n_removed_hyperboxes : int
Numer of hyperboxes has been removed because they are included in at
least one larger hyperbox with the same class label.
"""
n_hyperboxes = len(C)
# an array of indices showing the position of all hyperboxes kept
ids_kept_boxes = np.ones(n_hyperboxes, dtype=bool)
n_removed_hyperboxes = 0
for i in range(n_hyperboxes):
# Filter hypeboxes with the sample label as hyperbox i
id_hyperbox_same_label = C == C[i]
id_hyperbox_same_label[i] = False # remove hyperbox i
if id_hyperbox_same_label.any() == True:
# exist at least one hyperbox with the same label as hyperbox i
V_same = V[id_hyperbox_same_label]
W_same = W[id_hyperbox_same_label]
mem_vals = membership_func_gfmm(V[i], W[i], V_same, W_same, 1)
equal_one_index = mem_vals == 1
if np.sum(equal_one_index) > 0:
original_index = np.arange(0, n_hyperboxes)
original_index_same_label = original_index[id_hyperbox_same_label]
# Find indices of hyperboxes that contain hyperbox i
ids_parent_hyperboxes = original_index_same_label[np.nonzero(equal_one_index)[0]]
is_exist_parent_hyperboxes = len(ids_parent_hyperboxes) > 0
if is_exist_parent_hyperboxes == True:
ids_kept_boxes[i] = False
# Update centroid of a larger parent hyperbox
if len(ids_parent_hyperboxes) == 1:
parent_selection = ids_parent_hyperboxes[0]
if ids_kept_boxes[ids_parent_hyperboxes[0]] == False:
ids_kept_boxes[i] = True
elif len(ids_parent_hyperboxes) > 1:
start_id = 0
while start_id < len(ids_parent_hyperboxes) and ids_kept_boxes[ids_parent_hyperboxes[start_id]] == False:
start_id = start_id + 1 # remove cases that parent hyperboxes are merged
if start_id < len(ids_parent_hyperboxes):
# Compute the distance from the centroid of hyperbox i to centroids of other hyperboxes and choose the hyperbox with the smallest distance to merge
min_dis = np.linalg.norm(Centroids[i] - Centroids[ids_parent_hyperboxes[start_id]])
parent_selection = ids_parent_hyperboxes[start_id]
for jj in range(start_id + 1, len(ids_parent_hyperboxes)):
if ids_kept_boxes[ids_parent_hyperboxes[jj]] == True:
dist = np.linalg.norm(Centroids[i] - Centroids[ids_parent_hyperboxes[jj]])
if min_dis < dist:
min_dis = dist
parent_selection = ids_parent_hyperboxes[jj]
else:
ids_kept_boxes[i] = True
# Merge centroids and number of hyperboxes
if ids_kept_boxes[i] == False:
n_removed_hyperboxes = n_removed_hyperboxes + 1
Centroids[parent_selection] = (N_samples[parent_selection] * Centroids[parent_selection] + N_samples[i] * Centroids[i]) / (N_samples[i] + N_samples[parent_selection])
N_samples[parent_selection] = N_samples[parent_selection] + N_samples[i]
# remove hyperboxes contained in other hyperboxes
new_V = V[ids_kept_boxes, :]
new_W = W[ids_kept_boxes, :]
new_C = C[ids_kept_boxes]
new_Centroids = Centroids[ids_kept_boxes]
new_N_samples = N_samples[ids_kept_boxes]
return (new_V, new_W, new_C, new_N_samples, new_Centroids, n_removed_hyperboxes)
def predict_with_centroids(V, W, C, N_samples, Centroids, Xl, Xu, g=1):
"""
Predict class labels for samples in `X` represented in the form of invervals
`[Xl, Xu]`. This is a common function to determine the right class labels
for X wrt. a trained hyperbox-based classifier represented by `[V, W, C]`.
It uses the winner-takes-all principle to predict class labels for each
sample in X by assigning the class label of the sample to the class
label of the hyperbox with the maximum membership value to that sample.
It will use an Euclidean distance from the input pattern to the centroid
point of the hyperbox in the case of many winner hyperboxes with different
classes having the same maximum membership value. If two winner hyperboxes
show the same Euclidean distance to their centroid points, the winner
hyperbox with a higher number of included samples will be selected.
Parameters
----------
V : array-like of shape (n_hyperboxes, n_features)
A matrix stores all minimal points for numerical features of all
existing hyperboxes, in which each row is a minimal point of a hyperbox.
W : array-like of shape (n_hyperboxes, n_features)
A matrix stores all maximal points for numerical features of all
existing hyperboxes, in which each row is a minimal point of a hyperbox.
C : array-like of shape (n_hyperboxes,)
A vector stores all class labels correponding to existing hyperboxes.
N_samples : array-like of shape (n_hyperboxes,)
A vector stores the number of samples fully included in each existing
hyperbox.
Centroids : array-like of shape (n_hyperboxes, n_features)
A matrix stores all centroid points of all existing hyperboxes, in
which each row is a centroid point of a hyperbox.
Xl : array-like of shape (n_samples, n_features)
The data matrix contains lower bounds of input patterns for which we
want to predict the targets.
Xu : array-like of shape (n_samples, n_features)
The data matrix contains upper bounds of input patterns for which we
want to predict the targets.
g : float or array-like of shape (n_features,), optional, default=1
A sensitivity parameter describing the speed of decreasing of the
membership function in each dimension.
Returns
-------
y_pred : ndarray of shape (n_samples,)
A vector contains the predictions. In binary and multiclass problems,
this is a vector containing `n_samples`.
"""
if Xl.ndim == 1:
Xl = Xl.reshape(1, -1)
Xu = Xu.reshape(1, -1)
if (is_contain_missing_value(Xl) == True) or (is_contain_missing_value(Xu) == True):
Xl, Xu, _ = convert_format_missing_input_zero_one(Xl, Xu)
is_exist_missing_value = (V > W).any()
n_samples = Xl.shape[0]
y_pred = np.full(n_samples, 0)
sample_id = 0
for i in range(n_samples):
sample_id += 1
if is_exist_missing_value == False:
mem_val = membership_func_gfmm(Xl[i, :], Xu[i, :], V, W, g) # calculate memberships for all hyperboxes
else:
mem_val = membership_func_gfmm(Xl[i, :], Xu[i, :], np.minimum(V, W), np.maximum(W, V), g) # calculate memberships for all hyperboxes
bmax = mem_val.max() # get the maximum membership value
if ((Xl[i] < 0).any() == True) or ((Xu[i] > 1).any() == True):
print(">>> The testing sample %d with the coordinate %s is outside the range [0, 1]. Membership value = %f. The prediction is more likely incorrect." % (sample_id, Xl[i], bmax))
# get indices of all hyperboxes with max membership
max_mem_V_id = np.nonzero(mem_val == bmax)[0]
if len(np.unique(C[max_mem_V_id])) == 1:
# only one hyperbox with the highest membership value
y_pred[i] = C[max_mem_V_id[0]]
else:
# at least one pair of hyperboxes with different class
# => compare the centroid, and classify the input to the hyperboxes
# with the nearest distance to the input pattern
centroid_input_pat = (Xl[i] + Xu[i]) / 2
id_min = max_mem_V_id[0]
min_dist = np.linalg.norm(Centroids[id_min] - centroid_input_pat)
for j in range(1, len(max_mem_V_id)):
id_j = max_mem_V_id[j]
dist_j = np.linalg.norm(Centroids[id_j] - centroid_input_pat)
if dist_j < min_dist or (dist_j == min_dist and N_samples[id_j] > N_samples[id_min]):
id_min = id_j
min_dist = dist_j
y_pred[i] = C[id_min]
return y_pred
def predict_with_membership(V, W, C, Xl, Xu, g=1):
"""
Return class membership values for samples in `X` represented in the form
of invervals `[Xl, Xu]`. This is a common function to determine the
membership values from an input X to a trained hyperbox-based classifier
represented by `[V, W, C]`.
Parameters
----------
V : array-like of shape (n_hyperboxes, n_features)
A matrix stores all minimal points for numerical features of all
existing hyperboxes, in which each row is a minimal point of a hyperbox.
W : array-like of shape (n_hyperboxes, n_features)
A matrix stores all maximal points for numerical features of all
existing hyperboxes, in which each row is a minimal point of a hyperbox.
C : array-like of shape (n_hyperboxes,)
A vector stores all class labels correponding to existing hyperboxes.
Xl : array-like of shape (n_samples, n_features)
The data matrix contains lower bounds of input patterns for which we
want to predict the targets.
Xu : array-like of shape (n_samples, n_features)
The data matrix contains upper bounds of input patterns for which we
want to predict the targets.
g : float or array-like of shape (n_features,), optional, default=1
A sensitivity parameter describing the speed of decreasing of the
membership function in each dimension.
Returns
-------
mem_vals : ndarray of shape (n_samples, n_classes)
A vector contains the membership values for all classes for each input
sample which needs to get the membership values.
"""
if Xl.ndim == 1:
Xl = Xl.reshape(1, -1)
Xu = Xu.reshape(1, -1)
if (is_contain_missing_value(Xl) == True) or (is_contain_missing_value(Xu) == True):
Xl, Xu, _ = convert_format_missing_input_zero_one(Xl, Xu)
mem_vals, _ = get_membership_gfmm_all_classes(Xl, Xu, V, W, C, g)
return mem_vals
class MultiGranularGFMM(BaseHyperboxClassifier):
"""
A multi-resolution hierarchical granular representation based classifier
using general fuzzy min-max neural network.
This class implements the multi-granular learning algorithm to construct
classifiers from multiresolution hierarchical granular representations
using hyperbox fuzzy sets. This algorithm forms a series of granular
inferences hierarchically through many levels of abstraction. An attractive
characteristic of our classifier is that it can maintain a high accuracy in
comparison to other fuzzy min-max models at a low degree of granularity
based on reusing the knowledge learned from lower levels of abstraction.
In addition, our approach can reduce the data size significantly as well as
handle the uncertainty and incompleteness associated with data in
real-world applications. The construction process of the classifier
consists of two phases. The first phase is to formulate the model at the
greatest level of granularity, while the later stage aims to reduce the
complexity of the constructed model and deduce it from data at higher
abstraction levels. The details of this algorithm can be found in [1]_.
Parameters
----------
n_partitions : int, default=4
Number of partitions to split the original training set into disjoint
training sets to build base learners.
granular_theta : list of float, optional, default=[0.1, 0.2, 0.3]
Maximum hyperbox sizes at granularity levels.
gamma : float or ndarray of shape (n_features,), optional, default=1
A sensitivity parameter describing the speed of decreasing of the
membership function in each continuous feature.
min_membership_aggregation : float, optional, default=0.5
Minimum membership value between two hyperboxes aggregated to form a
larger sized hyperbox at a higher level of abstraction.
random_state : int, RandomState instance or None, default=None
Controls the stratified random sampling rate of the original dataset
to form disjoint subsets for training base learners.
Attributes
----------
granularity_level : dict
A mapping between the maximum hyperbox size and the granular level.
smallest_theta : float
Maximum hyperbox size at the highest granularity level.
higher_level_theta : list of float
Maximum hyperbox sizes of higher abstraction levels apart form the
highest granularity level.
granular_classifiers_ : ndarray of BaseGranular objects with shape (n_granularity_levels,)
A list of general fuzzy min-max neural networks at all granularity levels.
base_learners_ : list
A list of base learners trained from disjoint subsets of input training
patterns.
is_exist_missing_value : boolean
Is there any missing values in continuous features in the training data.
elapsed_training_time : float
Training time in seconds.
References
----------
.. [1] T.T. Khuat, F. Chen, and B. Gabrys, "An Effective Multiresolution
Hierarchical Granular Representation Based Classifier Using General
Fuzzy Min-Max Neural Network," IEEE Transactions on Fuzzy Systems,
vol. 29, no. 2, pp. 427-441, 2021.
Examples
--------
>>> from sklearn.datasets import load_iris
>>> from hbbrain.numerical_data.multigranular_learner.multi_resolution_gfmm import MultiGranularGFMM
>>> X, y = load_iris(return_X_y=True)
>>> from sklearn.preprocessing import MinMaxScaler
>>> scaler = MinMaxScaler()
>>> scaler.fit(X)
MinMaxScaler()
>>> X = scaler.transform(X)
>>> clf = MultiGranularGFMM(n_partitions=2, granular_theta=[0.1, 0.2, 0.3, 0.4, 0.5], gamma=1, min_membership_aggregation=0.6, random_state=0)
>>> clf.fit(X, y)
>>> clf.predict(X[[10, 50, 100]])
array([0, 1, 2])
>>> clf.predict(X[[10, 50, 100]], level=0)
array([0, 1, 2])
>>> print("Number of hyperboxes at granularity 1 = %d"%clf.get_n_hyperboxes(0))
Number of hyperboxes at granularity 1 = 77
>>> clf.predict(X[[10, 50, 100]], level=4)
array([0, 1, 2])
>>> print("Number of hyperboxes at granularity 5 = %d"%clf.get_n_hyperboxes(4))
Number of hyperboxes at granularity 5 = 11
"""
def __init__(self, n_partitions=4, granular_theta=[0.1, 0.2, 0.3], gamma=1, min_membership_aggregation=0.5, random_state=0):
BaseHyperboxClassifier.__init__(self, granular_theta[0])
self.granularity_level = convert_granular_theta_to_level(granular_theta)
self.granular_theta = granular_theta
self.smallest_theta = granular_theta[0]
self.higher_level_theta = granular_theta[1:]
self.min_membership_aggregation = min_membership_aggregation
self.n_partitions = n_partitions
self.gamma = gamma
self.random_state = random_state
self.granular_classifiers_ = np.empty(len(self.higher_level_theta)+1, dtype=object)
def _build_homogeneous_class_base_learner(self, Xl, Xu, y):
"""
Train a general fuzzy min-max neural network base learner. Training
samples are grouped by class labels one by one. Therefore, training
samples of a new class only appear when training samples of prior
class are completely absorbed.
Parameters
----------
Xl : array-like of shape (n_samples_for_learner, n_features)
The data matrix contains lower bounds of input training patterns
used to train a base learner.
Xu : array-like of shape (n_samples_for_learner, n_features)
The data matrix contains upper bounds of input training patterns
used to train a base learner.
y : array-like of shape (n_samples_for_learner,)
Target vector relative to input training hyperboxes `[Xl, Xu]`.
Returns
-------
BaseGranular
A trained base learner with lower and upper bounds, class labels,
number of fully included samples within each hyperbox and centroids
of hyperboxes.
"""
if Xl.ndim == 1:
Xl = Xl.reshape(1, -1)
Xu = Xu.reshape(1, -1)
V = np.array([])
W = np.array([])
C = np.array([])
N_samples = np.array([])
Centroids = np.array([])
class_labels = np.unique(y[y != UNLABELED_CLASS])
# for unlabelled samples, they need to be handled at the last iteration
if (y == UNLABELED_CLASS).any():
class_labels = np.concatenate((class_labels, [UNLABELED_CLASS]))
threshold_mem_val = 1 - np.max(self.gamma) * self.smallest_theta
for c in class_labels:
Xl_ho = Xl[y == c]
Xu_ho = Xu[y == c]
y_ho = y[y == c]
n_samples = len(y_ho)
for i in range(n_samples):
if V.size == 0:
V = np.array([Xl_ho[i]])
W = np.array([Xu_ho[i]])
C = np.array([y_ho[i]])
N_samples = np.array([1])
Centroids = np.array([(Xl_ho[i] + Xu_ho[i])/2])
else:
if y_ho[i] == UNLABELED_CLASS:
id_same_input_label_group = np.arange(len(C))
else:
id_same_input_label_group = (C == y_ho[i]) | (C == UNLABELED_CLASS)
if id_same_input_label_group.any() == True:
V_sameX = V[id_same_input_label_group]
W_sameX = W[id_same_input_label_group]
# contain both class label as same as the input pattern and unlabelled
lb_sameX = C[id_same_input_label_group]
id_range = np.arange(len(C))
# determine the indices of samples with the same class label as the input sample
ids_hyperboxes_same_input_label = id_range[id_same_input_label_group]
if self.is_exist_missing_value:
b = membership_func_gfmm(Xl_ho[i], Xu_ho[i], np.minimum(
V_sameX, W_sameX), np.maximum(V_sameX, W_sameX), self.gamma)
else:
b = membership_func_gfmm(
Xl_ho[i], Xu_ho[i], V_sameX, W_sameX, self.gamma)
id_descending_mem_val = np.argsort(b)[::-1]
if b[id_descending_mem_val[0]] != 1 or (y_ho[i] != lb_sameX[id_descending_mem_val[0]] and y_ho[i] != UNLABELED_CLASS):
adjust = False
count = 0
for j in ids_hyperboxes_same_input_label[id_descending_mem_val]:
if b[id_descending_mem_val[count]] < threshold_mem_val:
break
count = count + 1
# Check for violation of max hyperbox size and class labels
Vj_new = np.minimum(V[j], Xl_ho[i])
Wj_new = np.maximum(W[j], Xu_ho[i])
if (y_ho[i] == C[j] or C[j] == UNLABELED_CLASS or y_ho[i] == UNLABELED_CLASS) and (((Wj_new - Vj_new) <= self.smallest_theta).all() == True):
# adjust the j-th hyperbox
V[j] = Vj_new
W[j] = Wj_new
Centroids[j] = (Centroids[j] * N_samples[j] + (Xl_ho[j] + Xu_ho[j])/2) / (N_samples[j] + 1)
N_samples[j] = N_samples[j] + 1
adjust = True
if (y_ho[i] != UNLABELED_CLASS) and (C[j] == UNLABELED_CLASS):
C[j] = y_ho[i]
# found out the winner hyperbox to adjust => break the loop
break
# if the ith sample did not fit into any existing hyperboxes, create a new one
if not adjust:
V = np.concatenate((V, Xl_ho[i].reshape(1, -1)), axis=0)
W = np.concatenate((W, Xu_ho[i].reshape(1, -1)), axis=0)
C = np.concatenate((C, [y_ho[i]]))
N_samples = np.concatenate((N_samples, [1]))
tmp_new_centroid = (Xl_ho[i] + Xu_ho[i]) / 2
Centroids = np.concatenate((Centroids, tmp_new_centroid.reshape(1, -1)), axis=0)
else:
# There are no existing hyperboxes representing the same class label as the input patter
# We need to create a new hyperbox for the input sample
V = np.concatenate((V, Xl_ho[i].reshape(1, -1)), axis=0)
W = np.concatenate((W, Xu_ho[i].reshape(1, -1)), axis=0)
C = np.concatenate((C, [y_ho[i]]))
N_samples = np.concatenate((N_samples, [1]))
tmp_new_centroid = (Xl_ho[i] + Xu_ho[i]) / 2
Centroids = np.concatenate((Centroids, tmp_new_centroid.reshape(1, -1)), axis=0)
return BaseGranular(V=V, W=W, C=C, N_samples=N_samples, Centroids=Centroids)
def _build_heterogeneous_class_base_learner(self, Xl, Xu, y):
"""
Train a general fuzzy min-max neural network base learner. The class
labels of input patterns are in any order.
Parameters
----------
Xl : array-like of shape (n_samples_for_learner, n_features)
The data matrix contains lower bounds of input training patterns
used to train a base learner.
Xu : array-like of shape (n_samples_for_learner, n_features)
The data matrix contains upper bounds of input training patterns
used to train a base learner.
y : array-like of shape (n_samples_for_learner,)
Target vector relative to input training hyperboxes `[Xl, Xu]`.
Returns
-------
BaseGranular
A trained base learner with lower and upper bounds, class labels,
number of fully included samples within each hyperbox and centroids
of hyperboxes.
"""
if Xl.ndim == 1:
Xl = Xl.reshape(1, -1)
Xu = Xu.reshape(1, -1)
V = np.array([])
W = np.array([])
C = np.array([])
N_samples = np.array([])
Centroids = np.array([])
threshold_mem_val = 1 - np.max(self.gamma) * self.smallest_theta
n_samples = len(y)
for i in range(n_samples):
if V.size == 0:
V = np.array([Xl[i]])
W = np.array([Xu[i]])
C = np.array([y[i]])
N_samples = np.array([1])
Centroids = np.array([(Xl[i] + Xu[i])/2])
else:
if y[i] == UNLABELED_CLASS:
id_same_input_label_group = np.ones(len(C), dtype=bool)
else:
id_same_input_label_group = (C == y[i]) | (C == UNLABELED_CLASS)
if id_same_input_label_group.any() == True:
V_sameX = V[id_same_input_label_group]
W_sameX = W[id_same_input_label_group]
# contain both class label as same as the input pattern and unlabelled
lb_sameX = C[id_same_input_label_group]
id_range = np.arange(len(C))
# determine the indices of samples with the same class label as the input sample
ids_hyperboxes_same_input_label = id_range[id_same_input_label_group]
if self.is_exist_missing_value:
b = membership_func_gfmm(Xl[i], Xu[i], np.minimum(
V_sameX, W_sameX), np.maximum(V_sameX, W_sameX), self.gamma)
else:
b = membership_func_gfmm(
Xl[i], Xu[i], V_sameX, W_sameX, self.gamma)
id_descending_mem_val = np.argsort(b)[::-1]
if b[id_descending_mem_val[0]] != 1 or (y[i] != lb_sameX[id_descending_mem_val[0]] and y[i] != UNLABELED_CLASS):
adjust = False
count = 0
for j in ids_hyperboxes_same_input_label[id_descending_mem_val]:
if b[id_descending_mem_val[count]] < threshold_mem_val:
break
count = count + 1
# Check for violation of max hyperbox size and class labels
Vj_new = np.minimum(V[j], Xl[i])
Wj_new = np.maximum(W[j], Xu[i])
if (y[i] == C[j] or C[j] == UNLABELED_CLASS or y[i] == UNLABELED_CLASS) and (((Wj_new - Vj_new) <= self.smallest_theta).all() == True):
# adjust the j-th hyperbox
V[j] = Vj_new
W[j] = Wj_new
Centroids[j] = (Centroids[j] * N_samples[j] + (Xl[j] + Xu[j])/2) / (N_samples[j] + 1)
N_samples[j] = N_samples[j] + 1
adjust = True
if (y[i] != UNLABELED_CLASS) and (C[j] == UNLABELED_CLASS):
C[j] = y[i]
# found out the winner hyperbox to adjust => break the loop
break
# if the ith sample did not fit into any existing hyperboxes, create a new one
if not adjust:
V = np.concatenate((V, Xl[i].reshape(1, -1)), axis=0)
W = np.concatenate((W, Xu[i].reshape(1, -1)), axis=0)
C = np.concatenate((C, [y[i]]))
N_samples = np.concatenate((N_samples, [1]))
tmp_new_centroid = (Xl[i] + Xu[i]) / 2
Centroids = np.concatenate((Centroids, tmp_new_centroid.reshape(1, -1)), axis=0)
else:
# There are no existing hyperboxes representing the same class label as the input patter
# We need to create a new hyperbox for the input sample
V = np.concatenate((V, Xl[i].reshape(1, -1)), axis=0)
W = np.concatenate((W, Xu[i].reshape(1, -1)), axis=0)
C = np.concatenate((C, [y[i]]))
N_samples = np.concatenate((N_samples, [1]))
tmp_new_centroid = (Xl[i] + Xu[i]) / 2
Centroids = np.concatenate((Centroids, tmp_new_centroid.reshape(1, -1)), axis=0)
return BaseGranular(V=V, W=W, C=C, N_samples=N_samples, Centroids=Centroids)
def simple_pruning(self, V, W, C, N_samples, Centroids, Xl_val, Xu_val, y_val, acc_threshold=0.5, keep_empty_boxes=False):
"""
Simply prune low qualitied hyperboxes based on a pre-defined accuracy threshold for each hyperbox
Parameters
----------
V : array-like of shape (n_hyperboxes, n_features)
A matrix stores all minimal points for numerical features of all
existing hyperboxes, in which each row is a minimal point of a hyperbox.
W : array-like of shape (n_hyperboxes, n_features)
A matrix stores all maximal points for numerical features of all
existing hyperboxes, in which each row is a minimal point of a hyperbox.
C : array-like of shape (n_hyperboxes,)
A vector stores all class labels correponding to existing hyperboxes.
N_samples : array-like of shape (n_hyperboxes,)
A vector stores the number of samples fully included in each existing
hyperbox.
Centroids : array-like of shape (n_hyperboxes, n_features)
A matrix stores all centroid points of all existing hyperboxes, in
which each row is a centroid point of a hyperbox.
Xl_val : array-like of shape (n_samples, n_features)
The data matrix contains lower bounds of validation patterns.
Xu_val : array-like of shape (n_samples, n_features)
The data matrix contains upper bounds of validation patterns.
y_val : ndarray of shape (n_samples,)
A vector contains the true class label corresponding to each validation pattern.
acc_threshold : float, optional, default=0.5
The minimum accuracy for each hyperbox to be kept unchanged.
keep_empty_boxes : boolean, optional, default=False
Whether to keep the hyperboxes which do not join the prediction
process on the validation set. If True, keep them, else the decision
for keeping or removing based on the classification accuracy on the
validation dataset
Returns
-------
new_V : array-like of shape (n_new_hyperboxes, n_features)
A matrix stores all minimal points for numerical features of all
remaining hyperboxes after pruning, in which each row is a minimal
point of a hyperbox.
new_W : array-like of shape (n_new_hyperboxes, n_features)
A matrix stores all maximal points for numerical features of all
remaining hyperboxes after pruning, in which each row is a maximal
point of a hyperbox.
new_C : array-like of shape (n_new_hyperboxes,)
A vector stores all class labels correponding to remaining
hyperboxes after pruning.
new_N_samples : array-like of shape (n_new_hyperboxes,)
A vector stores the number of samples fully included in each
remaining hyperbox after pruning.
new_Centroids : array-like of shape (n_new_hyperboxes, n_features)
A matrix stores all centroid points of all remaining hyperboxes
after pruning, in which each row is a centroid point of a hyperbox.
"""
n_samples = Xl_val.shape[0]
rnd = np.random
rnd.seed(0)
# Matrices storing the classification accuracy for each created hyperbox in the trained model
# The first column stores the number of corrected classification samples and the second column stores the number of wrong classification samples
hyperboxes_performance = np.zeros((len(C), 2))
if (is_contain_missing_value(Xl_val) == True) or (is_contain_missing_value(Xu_val) == True):
Xl_val, Xu_val, y_val = convert_format_missing_input_zero_one(Xl_val, Xu_val, y_val)
for i in range(n_samples):
if self.is_exist_missing_value == False:
mem_val = membership_func_gfmm(Xl_val[i], Xu_val[i], V, W, self.gamma) # calculate memberships for all hyperboxes
else:
mem_val = membership_func_gfmm(Xl_val[i], Xu_val[i], np.minimum(V, W), np.maximum(W, V), self.gamma)
bmax = mem_val.max() # get max membership value
# get indices of all hyperboxes with max membership
max_mem_V_id = np.nonzero(mem_val == bmax)[0]
if len(max_mem_V_id) == 1:
# Only one hyperbox with the highest membership function
if C[max_mem_V_id[0]] == y_val[i]:
hyperboxes_performance[max_mem_V_id[0], 0] = hyperboxes_performance[max_mem_V_id[0], 0] + 1
else:
hyperboxes_performance[max_mem_V_id[0], 1] = hyperboxes_performance[max_mem_V_id[0], 1] + 1
else:
# More than one hyperbox with highest membership => using Manhattan distance
# More than one hyperbox with highest membership => compare with centroid
centroid_input_pat = (Xl_val[i] + Xu_val[i]) / 2
id_min = max_mem_V_id[0]
min_dist = np.linalg.norm(Centroids[id_min] - centroid_input_pat)
for j in range(1, len(max_mem_V_id)):
id_j = max_mem_V_id[j]
dist_j = np.linalg.norm(Centroids[id_j] - centroid_input_pat)
if dist_j < min_dist or (dist_j == min_dist and N_samples[id_j] > N_samples[id_min]):
id_min = id_j
min_dist = dist_j
if C[id_min] != y_val[i] and y_val[i] != UNLABELED_CLASS:
hyperboxes_performance[id_min, 1] = hyperboxes_performance[id_min, 1] + 1
else:
hyperboxes_performance[id_min, 0] = hyperboxes_performance[id_min, 0] + 1
# pruning handling based on the validation results
n_hyperboxes = hyperboxes_performance.shape[0]
id_remained_excl_empty_boxes = np.zeros(n_hyperboxes).astype(bool)
id_remained_incl_empty_boxes = np.zeros(n_hyperboxes).astype(bool)
for i in range(n_hyperboxes):
if (hyperboxes_performance[i, 0] + hyperboxes_performance[i, 1] != 0) and (hyperboxes_performance[i, 0] / (hyperboxes_performance[i, 0] + hyperboxes_performance[i, 1]) >= acc_threshold):
id_remained_excl_empty_boxes[i] = True
id_remained_incl_empty_boxes[i] = True
if (hyperboxes_performance[i, 0] + hyperboxes_performance[i, 1] == 0):
id_remained_incl_empty_boxes[i] = True
if keep_empty_boxes == True:
new_V = V[id_remained_incl_empty_boxes]
new_W = W[id_remained_incl_empty_boxes]
new_C = C[id_remained_incl_empty_boxes]
new_N_samples = N_samples[id_remained_incl_empty_boxes]
new_Centroids = Centroids[id_remained_incl_empty_boxes]
else:
# keep one hyperbox for the class for which all of its hyperboxes
# are prunned
current_classes = np.unique(C)
class_tmp = C[id_remained_excl_empty_boxes]
for c in current_classes:
if c not in class_tmp:
pos = np.nonzero(C == c)[0]
id_kept = rnd.randint(len(pos))
id_remained_excl_empty_boxes[pos[id_kept]] = True
V_pruned_excl_empty_boxes = V[id_remained_excl_empty_boxes]
W_pruned_excl_empty_boxes = W[id_remained_excl_empty_boxes]
C_pruned_excl_empty_boxes = C[id_remained_excl_empty_boxes]
N_samples_pruned_excl_empty_boxes = N_samples[id_remained_excl_empty_boxes]
Centroids_pruned_excl_empty_boxes = Centroids[id_remained_excl_empty_boxes]
W_pruned_incl_empty_boxes = W[id_remained_incl_empty_boxes]
V_pruned_incl_empty_boxes = V[id_remained_incl_empty_boxes]
C_pruned_incl_empty_boxes = C[id_remained_incl_empty_boxes]
N_samples_pruned_incl_empty_boxes = N_samples[id_remained_incl_empty_boxes]
Centroids_pruned_incl_empty_boxes = Centroids[id_remained_incl_empty_boxes]
y_val_pred_excl_empty_boxes = predict_with_centroids(V_pruned_excl_empty_boxes, W_pruned_excl_empty_boxes, C_pruned_excl_empty_boxes, N_samples_pruned_excl_empty_boxes, Centroids_pruned_excl_empty_boxes, Xl_val, Xu_val, self.gamma)
y_val_pred_incl_empty_boxes = predict_with_centroids(V_pruned_incl_empty_boxes, W_pruned_incl_empty_boxes, C_pruned_incl_empty_boxes, N_samples_pruned_incl_empty_boxes, Centroids_pruned_incl_empty_boxes, Xl_val, Xu_val, self.gamma)
if (accuracy_score(y_val, y_val_pred_excl_empty_boxes) >= accuracy_score(y_val, y_val_pred_incl_empty_boxes)):
new_V = V_pruned_excl_empty_boxes
new_W = W_pruned_excl_empty_boxes
new_C = C_pruned_excl_empty_boxes
new_N_samples = N_samples_pruned_excl_empty_boxes
new_Centroids = Centroids_pruned_excl_empty_boxes
else:
new_V = V_pruned_incl_empty_boxes
new_W = W_pruned_incl_empty_boxes
new_C = C_pruned_incl_empty_boxes
new_N_samples = N_samples_pruned_incl_empty_boxes
new_Centroids = Centroids_pruned_incl_empty_boxes
return (new_V, new_W, new_C, new_N_samples, new_Centroids)
def granular_learning_phase_1(self, Xl, Xu, y, learning_type=HETEROGENEOUS_CLASS_LEARNING, X_val=None, y_val=None, acc_threshold=0.5, keep_empty_boxes=False):
"""
Training a granular general fuzzy min-max neural network using a
learning algorithm in phase 1 to distribute disjoint subsets into
working processes to build base learners. After that, resulting
hyperboxes from all base learners will merged and pruned.
Parameters
----------
Xl : array-like of shape (n_samples, n_features)
The data matrix contains lower bounds of input training patterns.
Xu : array-like of shape (n_samples, n_features)
The data matrix contains upper bounds of input training patterns.
y : array-like of shape (n_samples,)
Target vector relative to input training hyperboxes `[Xl, Xu]`.
learning_type : enum (int), optional, default=HETEROGENEOUS_CLASS_LEARNING
Learning type is used to build base learners from disjoint datasets.
It gets two defined enum values being HETEROGENEOUS_CLASS_LEARNING
and HOMOGENEOUS_CLASS_LEARNING. Heterogeneous class learning means
that base learners are trained based on the order of input samples.
Homogeneous class learning means that input data are sorted and
grouped according to class labels before starting the training
process.
X_val : array-like of shape (n_samples, n_features)
The data matrix contains validation patterns.
y_val : ndarray of shape (n_samples,)
A vector contains the true class label corresponding to each
validation pattern.
acc_threshold : float, optional, default=0.5
The minimum accuracy for each hyperbox to be kept unchanged.
keep_empty_boxes : boolean, optional, default=False
Whether to keep the hyperboxes which do not join the prediction
process on the validation set. If True, keep them, else the decision
for keeping or removing based on the classification accuracy on the
validation dataset
Returns
-------
self : object
A granular general fuzzy min-max neural network trained by a
phase-1 learning algorithm.
"""
# Split training data into disjoint partitions
skf = StratifiedKFold(n_splits=self.n_partitions, shuffle=True, random_state=self.random_state)
partition_sample_ids = []
for _, part_sample_id in skf.split(Xl, y):
partition_sample_ids.append(part_sample_id)
self.base_learners_ = list()
# Map procedure
if learning_type == HETEROGENEOUS_CLASS_LEARNING:
all_results = Parallel(
n_jobs=self.n_partitions, verbose=1
)(
delayed(self._build_heterogeneous_class_base_learner)(
Xl[partition_sample_ids[i]],
Xu[partition_sample_ids[i]],
y[partition_sample_ids[i]]
)
for i in range(self.n_partitions)
)
else:
all_results = Parallel(
n_jobs=self.n_partitions
)(
delayed(self._build_homogeneous_class_base_learner)(
Xl[partition_sample_ids[i]],
Xu[partition_sample_ids[i]],
y[partition_sample_ids[i]]
)
for i in range(self.n_partitions)
)
# Reduce
for t in all_results:
self.base_learners_.append(t)
# Merging procedure for all resulting hyperboxes from base learners
V = self.base_learners_[0].V.copy()
W = self.base_learners_[0].W.copy()
C = self.base_learners_[0].C.copy()
N_samples = self.base_learners_[0].N_samples.copy()
Centroids = self.base_learners_[0].Centroids.copy()
for i in range(1, self.n_partitions):
V = np.concatenate((V, self.base_learners_[i].V), axis=0)
W = np.concatenate((W, self.base_learners_[i].W), axis=0)
C = np.concatenate((C, self.base_learners_[i].C))
N_samples = np.concatenate((N_samples, self.base_learners_[i].N_samples))
Centroids = np.concatenate((Centroids, self.base_learners_[i].Centroids), axis=0)
# Remove the hyperboxes included in other hyperboxes with the same
# class label
V, W, C, N_samples, Centroids, self.n_removed_hyperboxes = remove_contained_hyperboxes(V, W, C, N_samples, Centroids)
# Pruning
if X_val is not None:
self.classifier_before_pruning_ = BaseGranular(V=V, W=W, C=C, N_samples=N_samples, Centroids=Centroids)
V, W, C, N_samples, Centroids = self.simple_pruning(V, W, C, N_samples, Centroids, X_val, X_val, y_val, acc_threshold, keep_empty_boxes)
# Store generated hyperboxes after phase 1
self.granular_classifiers_[0] = BaseGranular(V=V, W=W, C=C, N_samples=N_samples, Centroids=Centroids)
return self
def granular_learning_phase_2(self):
"""
Training a granular general fuzzy min-max neural network using a
learning algorithm in phase 2 to reduce number of hyperboxes while
keeping a good classification performance.
Returns
-------
self : object
A granular general fuzzy min-max neural network trained by a
phase-2 learning algorithm.
"""
for level, theta in enumerate(self.higher_level_theta):
threshold = max(self.min_membership_aggregation, 1 - np.max(self.gamma) * theta)
V = np.array([])
n_pre_hyperboxes = len(self.granular_classifiers_[level].C)
for i in range(n_pre_hyperboxes):
if V.size == 0:
V = np.array([self.granular_classifiers_[level].V[i]])
W = np.array([self.granular_classifiers_[level].W[i]])
C = np.array([self.granular_classifiers_[level].C[i]])
N_samples = np.array([self.granular_classifiers_[level].N_samples[i]])
Centroids = np.array([self.granular_classifiers_[level].Centroids[i]])
else:
class_input_sample = self.granular_classifiers_[level].C[i]
if class_input_sample == UNLABELED_CLASS:
id_same_input_label_group = np.ones(len(C), dtype=bool)
else:
id_same_input_label_group = (C == class_input_sample) | (C == UNLABELED_CLASS)
is_create_new_hyperbox = False
if id_same_input_label_group.any() == True:
V_sameX = V[id_same_input_label_group]
W_sameX = W[id_same_input_label_group]
lb_sameX = C[id_same_input_label_group]
N_samples_sameX = N_samples[id_same_input_label_group]
Centroids_sameX = Centroids[id_same_input_label_group]
id_range = np.arange(len(C))
ids_hyperboxes_same_input_label = id_range[id_same_input_label_group]
if self.is_exist_missing_value:
mem_vals = membership_func_gfmm(self.granular_classifiers_[level].V[i], self.granular_classifiers_[level].W[i], np.minimum(
V_sameX, W_sameX), np.maximum(V_sameX, W_sameX), self.gamma)
else:
mem_vals = membership_func_gfmm(
self.granular_classifiers_[level].V[i], self.granular_classifiers_[level].W[i], V_sameX, W_sameX, self.gamma)
id_descending_mem_val = np.argsort(mem_vals)[::-1]
sorted_mem_vals = mem_vals[id_descending_mem_val]
if sorted_mem_vals[0] != 1 or (class_input_sample != lb_sameX[id_descending_mem_val[0]] and class_input_sample != UNLABELED_CLASS):
adjust = False
considered_mem_vals = sorted_mem_vals[sorted_mem_vals >= threshold]
if len(considered_mem_vals) > 0:
id_considered_hyperboxes = id_descending_mem_val[sorted_mem_vals >= threshold]
for j in ids_hyperboxes_same_input_label[id_considered_hyperboxes]:
# test violation of max hyperbox size and class labels
if (class_input_sample == C[j] or C[j] == UNLABELED_CLASS or class_input_sample == UNLABELED_CLASS) and ((np.maximum(W[j], self.granular_classifiers_[level].W[i]) - np.minimum(V[j], self.granular_classifiers_[level].V[i])) <= theta).all() == True:
# save old value
Vj_old = V[j].copy()
Wj_old = W[j].copy()
C_old = C[j]
# adjust the j-th hyperbox
V[j] = np.minimum(V[j], self.granular_classifiers_[level].V[i])
W[j] = np.maximum(W[j], self.granular_classifiers_[level].W[i])
if class_input_sample != UNLABELED_CLASS and C[j] == UNLABELED_CLASS:
C[j] = class_input_sample
# Test overlap
if is_overlap_one_many_hyperboxes_num_data_general(V, W, C, j) == True:
# Exist overlapping regions with hyperboxes belonging to other classes
# revert change and Choose other hyperbox
V[j] = Vj_old
W[j] = Wj_old
C[j] = C_old
else:
# Keep changes and update centroid, stopping the process of choosing hyperboxes
Centroids[j] = (Centroids[j] * N_samples[j] + self.granular_classifiers_[level].Centroids[i] * self.granular_classifiers_[level].N_samples[i]) / (N_samples[j] + self.granular_classifiers_[level].N_samples[i])
N_samples[j] = N_samples[j] + self.granular_classifiers_[level].N_samples[i]
adjust = True
break