-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path14.java
More file actions
1866 lines (1678 loc) · 74.2 KB
/
Copy path14.java
File metadata and controls
1866 lines (1678 loc) · 74.2 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
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
/*******************************************************************************
* Copyright (c) 2015-2018 Skymind, Inc.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* SPDX-License-Identifier: Apache-2.0
******************************************************************************/
package org.nd4j.evaluation.classification;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.nd4j.base.Preconditions;
import org.nd4j.evaluation.BaseEvaluation;
import org.nd4j.evaluation.EvaluationAveraging;
import org.nd4j.evaluation.EvaluationUtils;
import org.nd4j.evaluation.meta.Prediction;
import org.nd4j.evaluation.serde.ConfusionMatrixDeserializer;
import org.nd4j.evaluation.serde.ConfusionMatrixSerializer;
import org.nd4j.linalg.api.buffer.DataType;
import org.nd4j.linalg.api.ndarray.INDArray;
import org.nd4j.linalg.api.ops.impl.reduce.longer.MatchCondition;
import org.nd4j.linalg.factory.Nd4j;
import org.nd4j.linalg.indexing.conditions.Conditions;
import org.nd4j.linalg.primitives.Counter;
import org.nd4j.linalg.primitives.Pair;
import org.nd4j.linalg.primitives.Triple;
import org.nd4j.serde.jackson.shaded.NDArrayTextDeSerializer;
import org.nd4j.serde.jackson.shaded.NDArrayTextSerializer;
import org.nd4j.shade.jackson.annotation.JsonIgnoreProperties;
import org.nd4j.shade.jackson.databind.annotation.JsonDeserialize;
import org.nd4j.shade.jackson.databind.annotation.JsonSerialize;
import java.io.Serializable;
import java.text.DecimalFormat;
import java.util.*;
/**
* Evaluation metrics:<br>
* - precision, recall, f1, fBeta, accuracy, Matthews correlation coefficient, gMeasure<br>
* - Top N accuracy (if using constructor {@link #Evaluation(List, int)})<br>
* - Custom binary evaluation decision threshold (use constructor {@link #Evaluation(double)} (default if not set is
* argmax / 0.5)<br>
* - Custom cost array, using {@link #Evaluation(INDArray)} or {@link #Evaluation(List, INDArray)} for multi-class <br>
* <br>
* Note: Care should be taken when using the Evaluation class for binary classification metrics such as F1, precision,
* recall, etc. There are a number of cases to consider:<br>
* 1. For binary classification (1 or 2 network outputs)<br>
* a) Default behaviour: class 1 is assumed as the positive class. Consequently, no-arg methods such as {@link #f1()},
* {@link #precision()}, {@link #recall()} etc will report the binary metric for class 1 only<br>
* b) To set class 0 as the positive class instead of class 1 (the default), use {@link #Evaluation(int, Integer)} or
* {@link #Evaluation(double, Integer)} or {@link #setBinaryPositiveClass(Integer)}. Then, {@link #f1()},
* {@link #precision()}, {@link #recall()} etc will report the binary metric for class 0 only.<br>
* c) To use macro-averaged metrics over both classes for binary classification (uncommon and usually not advisable)
* specify 'null' as the argument (instead of 0 or 1) as per (b) above<br>
* 2. For multi-class classification, binary metric methods such as {@link #f1()}, {@link #precision()}, {@link #recall()}
* will report macro-average (of the one-vs-all) binary metrics. Note that you can specify micro vs. macro averaging
* using {@link #f1(EvaluationAveraging)} and similar methods<br>
* <br>
* Note that setting a custom binary decision threshold is only possible for the binary case (1 or 2 outputs) and cannot
* be used if the number of classes exceeds 2. Predictions with probability > threshold are considered to be class 1,
* and are considered class 0 otherwise.<br>
* <br>
* Cost arrays (a row vector, of size equal to the number of outputs) modify the evaluation process: instead of simply
* doing predictedClass = argMax(probabilities), we do predictedClass = argMax(cost * probabilities). Consequently, an
* array of all 1s (or, indeed any array of equal values) will result in the same performance as no cost array; non-
* equal values will bias the predictions for or against certain classes.
*
* @author Adam Gibson
*/
@Slf4j
@EqualsAndHashCode(callSuper = true)
@Getter
@Setter
@JsonIgnoreProperties({"confusionMatrixMetaData"})
public class Evaluation extends BaseEvaluation<Evaluation> {
public enum Metric {ACCURACY, F1, PRECISION, RECALL, GMEASURE, MCC}
//What to output from the precision/recall function when we encounter an edge case
protected static final double DEFAULT_EDGE_VALUE = 0.0;
protected static final int CONFUSION_PRINT_MAX_CLASSES = 20;
@EqualsAndHashCode.Exclude //Exclude axis: otherwise 2 Evaluation instances could contain identical stats and fail equality
protected int axis = 1;
protected Integer binaryPositiveClass = 1; //Used *only* for binary classification; default value here to 1 for legacy JSON loading
protected final int topN;
protected int topNCorrectCount = 0;
protected int topNTotalCount = 0; //Could use topNCountCorrect / (double)getNumRowCounter() - except for eval(int,int), hence separate counters
protected Counter<Integer> truePositives = new Counter<>();
protected Counter<Integer> falsePositives = new Counter<>();
protected Counter<Integer> trueNegatives = new Counter<>();
protected Counter<Integer> falseNegatives = new Counter<>();
@JsonSerialize(using = ConfusionMatrixSerializer.class)
@JsonDeserialize(using = ConfusionMatrixDeserializer.class)
protected ConfusionMatrix<Integer> confusion;
protected int numRowCounter = 0;
@Getter
@Setter
protected List<String> labelsList = new ArrayList<>();
protected Double binaryDecisionThreshold;
@JsonSerialize(using = NDArrayTextSerializer.class)
@JsonDeserialize(using = NDArrayTextDeSerializer.class)
protected INDArray costArray;
protected Map<Pair<Integer, Integer>, List<Object>> confusionMatrixMetaData; //Pair: (Actual,Predicted)
/**
* For stats(): When classes are excluded from precision/recall, what is the maximum number we should print?
* If this is set to a high value, the output (potentially thousands of classes) can become unreadable.
*/
@Getter @Setter
protected int maxWarningClassesToPrint = 16;
// Empty constructor
public Evaluation() {
this.topN = 1;
this.binaryPositiveClass = 1;
}
/**
* The number of classes to account for in the evaluation
* @param numClasses the number of classes to account for in the evaluation
*/
public Evaluation(int numClasses) {
this(numClasses, (numClasses == 2 ? 1 : null));
}
/**
* Constructor for specifying the number of classes, and optionally the positive class for binary classification.
* See Evaluation javadoc for more details on evaluation in the binary case
*
* @param numClasses The number of classes for the evaluation. Must be 2, if binaryPositiveClass is non-null
* @param binaryPositiveClass If non-null, the positive class (0 or 1).
*/
public Evaluation(int numClasses, Integer binaryPositiveClass){
this(createLabels(numClasses), 1);
if(binaryPositiveClass != null){
Preconditions.checkArgument(binaryPositiveClass == 0 || binaryPositiveClass == 1,
"Only 0 and 1 are valid inputs for binaryPositiveClass; got " + binaryPositiveClass);
Preconditions.checkArgument(numClasses == 2, "Cannot set binaryPositiveClass argument " +
"when number of classes is not equal to 2 (got: numClasses=" + numClasses + ")");
}
this.binaryPositiveClass = binaryPositiveClass;
}
/**
* The labels to include with the evaluation.
* This constructor can be used for
* generating labeled output rather than just
* numbers for the labels
* @param labels the labels to use
* for the output
*/
public Evaluation(List<String> labels) {
this(labels, 1);
}
/**
* Use a map to generate labels
* Pass in a label index with the actual label
* you want to use for output
* @param labels a map of label index to label value
*/
public Evaluation(Map<Integer, String> labels) {
this(createLabelsFromMap(labels), 1);
}
/**
* Constructor to use for top N accuracy
*
* @param labels Labels for the classes (may be null)
* @param topN Value to use for top N accuracy calculation (<=1: standard accuracy). Note that with top N
* accuracy, an example is considered 'correct' if the probability for the true class is one of the
* highest N values
*/
public Evaluation(List<String> labels, int topN) {
this.labelsList = labels;
if (labels != null) {
createConfusion(labels.size());
}
this.topN = topN;
if(labels != null && labels.size() == 2){
this.binaryPositiveClass = 1;
}
}
/**
* Create an evaluation instance with a custom binary decision threshold. Note that binary decision thresholds can
* only be used with binary classifiers.<br>
* Defaults to class 1 for the positive class - see class javadoc, and use {@link #Evaluation(double, Integer)} to
* change this.
*
* @param binaryDecisionThreshold Decision threshold to use for binary predictions
*/
public Evaluation(double binaryDecisionThreshold) {
this(binaryDecisionThreshold, 1);
}
/**
* Create an evaluation instance with a custom binary decision threshold. Note that binary decision thresholds can
* only be used with binary classifiers.<br>
* This constructor also allows the user to specify the positive class for binary classification. See class javadoc
* for more details.
*
* @param binaryDecisionThreshold Decision threshold to use for binary predictions
*/
public Evaluation(double binaryDecisionThreshold, @NonNull Integer binaryPositiveClass) {
if(binaryPositiveClass != null){
Preconditions.checkArgument(binaryPositiveClass == 0 || binaryPositiveClass == 1,
"Only 0 and 1 are valid inputs for binaryPositiveClass; got " + binaryPositiveClass);
}
this.binaryDecisionThreshold = binaryDecisionThreshold;
this.topN = 1;
this.binaryPositiveClass = binaryPositiveClass;
}
/**
* Created evaluation instance with the specified cost array. A cost array can be used to bias the multi class
* predictions towards or away from certain classes. The predicted class is determined using argMax(cost * probability)
* instead of argMax(probability) when no cost array is present.
*
* @param costArray Row vector cost array. May be null
*/
public Evaluation(INDArray costArray) {
this(null, costArray);
}
/**
* Created evaluation instance with the specified cost array. A cost array can be used to bias the multi class
* predictions towards or away from certain classes. The predicted class is determined using argMax(cost * probability)
* instead of argMax(probability) when no cost array is present.
*
* @param labels Labels for the output classes. May be null
* @param costArray Row vector cost array. May be null
*/
public Evaluation(List<String> labels, INDArray costArray) {
if (costArray != null && !costArray.isRowVectorOrScalar()) {
throw new IllegalArgumentException("Invalid cost array: must be a row vector (got shape: "
+ Arrays.toString(costArray.shape()) + ")");
}
if (costArray != null && costArray.minNumber().doubleValue() < 0.0) {
throw new IllegalArgumentException("Invalid cost array: Cost array values must be positive");
}
this.labelsList = labels;
this.costArray = costArray == null ? null : costArray.castTo(DataType.FLOAT);
this.topN = 1;
}
protected int numClasses(){
if(labelsList != null){
return labelsList.size();
}
return confusion().getClasses().size();
}
@Override
public void reset() {
confusion = null;
truePositives = new Counter<>();
falsePositives = new Counter<>();
trueNegatives = new Counter<>();
falseNegatives = new Counter<>();
topNCorrectCount = 0;
topNTotalCount = 0;
numRowCounter = 0;
}
private ConfusionMatrix<Integer> confusion() {
return confusion;
}
private static List<String> createLabels(int numClasses) {
if (numClasses == 1)
numClasses = 2; //Binary (single output variable) case...
List<String> list = new ArrayList<>(numClasses);
for (int i = 0; i < numClasses; i++) {
list.add(String.valueOf(i));
}
return list;
}
private static List<String> createLabelsFromMap(Map<Integer, String> labels) {
int size = labels.size();
List<String> labelsList = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
String str = labels.get(i);
if (str == null)
throw new IllegalArgumentException("Invalid labels map: missing key for class " + i
+ " (expect integers 0 to " + (size - 1) + ")");
labelsList.add(str);
}
return labelsList;
}
private void createConfusion(int nClasses) {
List<Integer> classes = new ArrayList<>();
for (int i = 0; i < nClasses; i++) {
classes.add(i);
}
confusion = new ConfusionMatrix<>(classes);
}
/**
* Set the axis for evaluation - this is the dimension along which the probability (and label classes) are present.<br>
* For DL4J, this can be left as the default setting (axis = 1).<br>
* Axis should be set as follows:<br>
* For 2D (OutputLayer), shape [minibatch, numClasses] - axis = 1<br>
* For 3D, RNNs/CNN1D (DL4J RnnOutputLayer), NCW format, shape [minibatch, numClasses, sequenceLength] - axis = 1<br>
* For 3D, RNNs/CNN1D (DL4J RnnOutputLayer), NWC format, shape [minibatch, sequenceLength, numClasses] - axis = 2<br>
* For 4D, CNN2D (DL4J CnnLossLayer), NCHW format, shape [minibatch, channels, height, width] - axis = 1<br>
* For 4D, CNN2D, NHWC format, shape [minibatch, height, width, channels] - axis = 3<br>
*
* @param axis Axis to use for evaluation
*/
public void setAxis(int axis){
this.axis = axis;
}
/**
* Get the axis - see {@link #setAxis(int)} for details
*/
public int getAxis(){
return axis;
}
/**
* Collects statistics on the real outcomes vs the
* guesses. This is for logistic outcome matrices.
* <p>
* Note that an IllegalArgumentException is thrown if the two passed in
* matrices aren't the same length.
*
* @param realOutcomes the real outcomes (labels - usually binary)
* @param guesses the guesses/prediction (usually a probability vector)
*/
public void eval(INDArray realOutcomes, INDArray guesses) {
eval(realOutcomes, guesses, (List<Serializable>) null);
}
/**
* Evaluate the network, with optional metadata
*
* @param labels Data labels
* @param predictions Network predictions
* @param recordMetaData Optional; may be null. If not null, should have size equal to the number of outcomes/guesses
*
*/
@Override
public void eval(INDArray labels, INDArray predictions, INDArray mask, final List<? extends Serializable> recordMetaData) {
Triple<INDArray,INDArray, INDArray> p = BaseEvaluation.reshapeAndExtractNotMasked(labels, predictions, mask, axis);
if(p == null){
//All values masked out; no-op
return;
}
INDArray labels2d = p.getFirst();
INDArray predictions2d = p.getSecond();
INDArray maskArray = p.getThird();
Preconditions.checkState(maskArray == null, "Per-output masking for Evaluation is not supported");
//Check for NaNs in predictions - without this, evaulation could silently be intepreted as class 0 prediction due to argmax
long count = Nd4j.getExecutioner().execAndReturn(new MatchCondition(predictions2d, Conditions.isNan())).getFinalResult().longValue();
org.nd4j.base.Preconditions.checkState(count == 0, "Cannot perform evaluation with NaNs present in predictions:" +
" %s NaNs present in predictions INDArray", count);
// Add the number of rows to numRowCounter
numRowCounter += labels2d.size(0);
if(labels2d.dataType() != predictions2d.dataType())
labels2d = labels2d.castTo(predictions2d.dataType());
// If confusion is null, then Evaluation was instantiated without providing the classes -> infer # classes from
if (confusion == null) {
int nClasses = labels2d.columns();
if (nClasses == 1)
nClasses = 2; //Binary (single output variable) case
if(labelsList == null || labelsList.isEmpty()) {
labelsList = new ArrayList<>(nClasses);
for (int i = 0; i < nClasses; i++)
labelsList.add(String.valueOf(i));
}
createConfusion(nClasses);
}
// Length of real labels must be same as length of predicted labels
if (!Arrays.equals(labels2d.shape(),predictions2d.shape())) {
throw new IllegalArgumentException("Unable to evaluate. Predictions and labels arrays are not same shape." +
" Predictions shape: " + Arrays.toString(predictions2d.shape()) + ", Labels shape: " + Arrays.toString(labels2d.shape()));
}
// For each row get the most probable label (column) from prediction and assign as guessMax
// For each row get the column of the true label and assign as currMax
final int nCols = labels2d.columns();
final int nRows = labels2d.rows();
if (nCols == 1) {
INDArray binaryGuesses = predictions2d.gt(binaryDecisionThreshold == null ? 0.5 : binaryDecisionThreshold).castTo(predictions.dataType());
INDArray notLabel = labels2d.rsub(1.0); //Invert entries (assuming 1 and 0)
INDArray notGuess = binaryGuesses.rsub(1.0);
//tp: predicted = 1, actual = 1
int tp = labels2d.mul(binaryGuesses).castTo(DataType.INT).sumNumber().intValue();
//fp: predicted = 1, actual = 0
int fp = notLabel.mul(binaryGuesses).castTo(DataType.INT).sumNumber().intValue();
//fn: predicted = 0, actual = 1
int fn = notGuess.mul(labels2d).castTo(DataType.INT).sumNumber().intValue();
int tn = nRows - tp - fp - fn;
confusion().add(1, 1, tp);
confusion().add(1, 0, fn);
confusion().add(0, 1, fp);
confusion().add(0, 0, tn);
truePositives.incrementCount(1, tp);
falsePositives.incrementCount(1, fp);
falseNegatives.incrementCount(1, fn);
trueNegatives.incrementCount(1, tn);
truePositives.incrementCount(0, tn);
falsePositives.incrementCount(0, fn);
falseNegatives.incrementCount(0, fp);
trueNegatives.incrementCount(0, tp);
if (recordMetaData != null) {
for (int i = 0; i < binaryGuesses.size(0); i++) {
if (i >= recordMetaData.size())
break;
int actual = labels2d.getDouble(0) == 0.0 ? 0 : 1;
int predicted = binaryGuesses.getDouble(0) == 0.0 ? 0 : 1;
addToMetaConfusionMatrix(actual, predicted, recordMetaData.get(i));
}
}
} else {
INDArray guessIndex;
if (binaryDecisionThreshold != null) {
if (nCols != 2) {
throw new IllegalStateException("Binary decision threshold is set, but number of columns for "
+ "predictions is " + nCols
+ ". Binary decision threshold can only be used for binary " + "prediction cases");
}
INDArray pClass1 = predictions2d.getColumn(1);
guessIndex = pClass1.gt(binaryDecisionThreshold);
} else if (costArray != null) {
//With a cost array: do argmax(cost * probability) instead of just argmax(probability)
guessIndex = Nd4j.argMax(predictions2d.mulRowVector(costArray.castTo(predictions2d.dataType())), 1);
} else {
//Standard case: argmax
guessIndex = Nd4j.argMax(predictions2d, 1);
}
INDArray realOutcomeIndex = Nd4j.argMax(labels2d, 1);
val nExamples = guessIndex.length();
for (int i = 0; i < nExamples; i++) {
int actual = (int) realOutcomeIndex.getDouble(i);
int predicted = (int) guessIndex.getDouble(i);
confusion().add(actual, predicted);
if (recordMetaData != null && recordMetaData.size() > i) {
Object m = recordMetaData.get(i);
addToMetaConfusionMatrix(actual, predicted, m);
}
// instead of looping through each label for confusion
// matrix, instead infer those values by determining if true/false negative/positive,
// then just add across matrix
// if actual == predicted, then it's a true positive, assign true negative to every other label
if (actual == predicted) {
truePositives.incrementCount(actual, 1);
for (int col = 0; col < nCols; col++) {
if (col == actual) {
continue;
}
trueNegatives.incrementCount(col, 1); // all cols prior
}
} else {
falsePositives.incrementCount(predicted, 1);
falseNegatives.incrementCount(actual, 1);
// first determine intervals for adding true negatives
int lesserIndex, greaterIndex;
if (actual < predicted) {
lesserIndex = actual;
greaterIndex = predicted;
} else {
lesserIndex = predicted;
greaterIndex = actual;
}
// now loop through intervals
for (int col = 0; col < lesserIndex; col++) {
trueNegatives.incrementCount(col, 1); // all cols prior
}
for (int col = lesserIndex + 1; col < greaterIndex; col++) {
trueNegatives.incrementCount(col, 1); // all cols after
}
for (int col = greaterIndex + 1; col < nCols; col++) {
trueNegatives.incrementCount(col, 1); // all cols after
}
}
}
}
if (nCols > 1 && topN > 1) {
//Calculate top N accuracy
//TODO: this could be more efficient
INDArray realOutcomeIndex = Nd4j.argMax(labels2d, 1);
val nExamples = realOutcomeIndex.length();
for (int i = 0; i < nExamples; i++) {
int labelIdx = (int) realOutcomeIndex.getDouble(i);
double prob = predictions2d.getDouble(i, labelIdx);
INDArray row = predictions2d.getRow(i);
int countGreaterThan = (int) Nd4j.getExecutioner()
.exec(new MatchCondition(row, Conditions.greaterThan(prob)))
.getDouble(0);
if (countGreaterThan < topN) {
//For example, for top 3 accuracy: can have at most 2 other probabilities larger
topNCorrectCount++;
}
topNTotalCount++;
}
}
}
/**
* Evaluate a single prediction (one prediction at a time)
*
* @param predictedIdx Index of class predicted by the network
* @param actualIdx Index of actual class
*/
public void eval(int predictedIdx, int actualIdx) {
// Add the number of rows to numRowCounter
numRowCounter++;
// If confusion is null, then Evaluation is instantiated without providing the classes
if (confusion == null) {
throw new UnsupportedOperationException(
"Cannot evaluate single example without initializing confusion matrix first");
}
addToConfusion(actualIdx, predictedIdx);
// If they are equal
if (predictedIdx == actualIdx) {
// Then add 1 to True Positive
// (For a particular label)
incrementTruePositives(predictedIdx);
// And add 1 for each negative class that is accurately predicted (True Negative)
//(For a particular label)
for (Integer clazz : confusion().getClasses()) {
if (clazz != predictedIdx)
trueNegatives.incrementCount(clazz, 1.0f);
}
} else {
// Otherwise the real label is predicted as negative (False Negative)
incrementFalseNegatives(actualIdx);
// Otherwise the prediction is predicted as falsely positive (False Positive)
incrementFalsePositives(predictedIdx);
// Otherwise true negatives
for (Integer clazz : confusion().getClasses()) {
if (clazz != predictedIdx && clazz != actualIdx)
trueNegatives.incrementCount(clazz, 1.0f);
}
}
}
/**
* Report the classification statistics as a String
* @return Classification statistics as a String
*/
public String stats() {
return stats(false);
}
/**
* Method to obtain the classification report as a String
*
* @param suppressWarnings whether or not to output warnings related to the evaluation results
* @return A (multi-line) String with accuracy, precision, recall, f1 score etc
*/
public String stats(boolean suppressWarnings) {
return stats(suppressWarnings, numClasses() <= CONFUSION_PRINT_MAX_CLASSES, numClasses() > CONFUSION_PRINT_MAX_CLASSES);
}
/**
* Method to obtain the classification report as a String
*
* @param suppressWarnings whether or not to output warnings related to the evaluation results
* @param includeConfusion whether the confusion matrix should be included it the returned stats or not
* @return A (multi-line) String with accuracy, precision, recall, f1 score etc
*/
public String stats(boolean suppressWarnings, boolean includeConfusion){
return stats(suppressWarnings, includeConfusion, false);
}
private String stats(boolean suppressWarnings, boolean includeConfusion, boolean logConfusionSizeWarning){
String actual, predicted;
StringBuilder builder = new StringBuilder().append("\n");
StringBuilder warnings = new StringBuilder();
ConfusionMatrix<Integer> confusion = confusion();
if(confusion == null){
confusion = new ConfusionMatrix<>(); //Empty
}
List<Integer> classes = confusion.getClasses();
List<Integer> falsePositivesWarningClasses = new ArrayList<>();
List<Integer> falseNegativesWarningClasses = new ArrayList<>();
for (Integer clazz : classes) {
//Output possible warnings regarding precision/recall calculation
if (!suppressWarnings && truePositives.getCount(clazz) == 0) {
if (falsePositives.getCount(clazz) == 0) {
falsePositivesWarningClasses.add(clazz);
}
if (falseNegatives.getCount(clazz) == 0) {
falseNegativesWarningClasses.add(clazz);
}
}
}
if (!falsePositivesWarningClasses.isEmpty()) {
warningHelper(warnings, falsePositivesWarningClasses, "precision");
}
if (!falseNegativesWarningClasses.isEmpty()) {
warningHelper(warnings, falseNegativesWarningClasses, "recall");
}
int nClasses = confusion.getClasses().size();
DecimalFormat df = new DecimalFormat("0.0000");
double acc = accuracy();
double precisionMacro = precision(EvaluationAveraging.Macro);
double recallMacro = recall(EvaluationAveraging.Macro);
double f1Macro = f1(EvaluationAveraging.Macro);
builder.append("\n========================Evaluation Metrics========================");
builder.append("\n # of classes: ").append(nClasses);
builder.append("\n Accuracy: ").append(format(df, acc));
if (topN > 1) {
double topNAcc = topNAccuracy();
builder.append("\n Top ").append(topN).append(" Accuracy: ").append(format(df, topNAcc));
}
builder.append("\n Precision: ").append(format(df, precisionMacro));
if (nClasses > 2 && averagePrecisionNumClassesExcluded() > 0) {
int ex = averagePrecisionNumClassesExcluded();
builder.append("\t(").append(ex).append(" class");
if (ex > 1)
builder.append("es");
builder.append(" excluded from average)");
}
builder.append("\n Recall: ").append(format(df, recallMacro));
if (nClasses > 2 && averageRecallNumClassesExcluded() > 0) {
int ex = averageRecallNumClassesExcluded();
builder.append("\t(").append(ex).append(" class");
if (ex > 1)
builder.append("es");
builder.append(" excluded from average)");
}
builder.append("\n F1 Score: ").append(format(df, f1Macro));
if (nClasses > 2 && averageF1NumClassesExcluded() > 0) {
int ex = averageF1NumClassesExcluded();
builder.append("\t(").append(ex).append(" class");
if (ex > 1)
builder.append("es");
builder.append(" excluded from average)");
}
if (nClasses > 2 || binaryPositiveClass == null) {
builder.append("\nPrecision, recall & F1: macro-averaged (equally weighted avg. of ").append(nClasses)
.append(" classes)");
}
if(nClasses == 2 && binaryPositiveClass != null){
builder.append("\nPrecision, recall & F1: reported for positive class (class ").append(binaryPositiveClass);
if(labelsList != null){
builder.append(" - \"").append(labelsList.get(binaryPositiveClass)).append("\"");
}
builder.append(") only");
}
if (binaryDecisionThreshold != null) {
builder.append("\nBinary decision threshold: ").append(binaryDecisionThreshold);
}
if (costArray != null) {
builder.append("\nCost array: ").append(Arrays.toString(costArray.dup().data().asFloat()));
}
//Note that we could report micro-averaged too - but these are the same as accuracy
//"Note that for “micro�?-averaging in a multiclass setting with all labels included will produce equal precision, recall and F,"
//http://scikit-learn.org/stable/modules/model_evaluation.html
builder.append("\n\n");
builder.append(warnings);
if(includeConfusion){
builder.append("\n=========================Confusion Matrix=========================\n");
builder.append(confusionMatrix());
} else if(logConfusionSizeWarning){
builder.append("\n\nNote: Confusion matrix not generated due to space requirements for ")
.append(nClasses).append(" classes.\n")
.append("Use stats(false,true) to generate anyway");
}
builder.append("\n==================================================================");
return builder.toString();
}
/**
* Get the confusion matrix as a String
* @return Confusion matrix as a String
*/
public String confusionMatrix(){
int nClasses = numClasses();
if(confusion == null){
return "Confusion matrix: <no data>";
}
//First: work out the maximum count
List<Integer> classes = confusion.getClasses();
int maxCount = 1;
for (Integer i : classes) {
for (Integer j : classes) {
int count = confusion().getCount(i, j);
maxCount = Math.max(maxCount, count);
}
}
maxCount = Math.max(maxCount, nClasses); //Include this as header might be bigger than actual values
int numDigits = (int)Math.ceil(Math.log10(maxCount));
if(numDigits < 1)
numDigits = 1;
String digitFormat = "%" + (numDigits+1) + "d";
StringBuilder sb = new StringBuilder();
//Build header:
for( int i=0; i<nClasses; i++ ){
sb.append(String.format(digitFormat, i));
}
sb.append("\n");
int numDividerChars = (numDigits+1) * nClasses + 1;
for( int i=0; i<numDividerChars; i++ ){
sb.append("-");
}
sb.append("\n");
//Build each row:
for( int actual=0; actual<nClasses; actual++){
String actualName = resolveLabelForClass(actual);
for( int predicted=0; predicted<nClasses; predicted++){
int count = confusion.getCount(actual, predicted);
sb.append(String.format(digitFormat, count));
}
sb.append(" | ").append(actual).append(" = ").append(actualName).append("\n");
}
sb.append("\nConfusion matrix format: Actual (rowClass) predicted as (columnClass) N times");
return sb.toString();
}
private static String format(DecimalFormat f, double num) {
if (Double.isNaN(num) || Double.isInfinite(num))
return String.valueOf(num);
return f.format(num);
}
private String resolveLabelForClass(Integer clazz) {
if (labelsList != null && labelsList.size() > clazz)
return labelsList.get(clazz);
return clazz.toString();
}
private void warningHelper(StringBuilder warnings, List<Integer> list, String metric) {
warnings.append("Warning: ").append(list.size()).append(" class");
String wasWere;
if (list.size() == 1) {
wasWere = "was";
} else {
wasWere = "were";
warnings.append("es");
}
warnings.append(" ").append(wasWere);
warnings.append(" never predicted by the model and ").append(wasWere).append(" excluded from average ")
.append(metric);
if(list.size() <= maxWarningClassesToPrint) {
warnings.append("\nClasses excluded from average ").append(metric).append(": ")
.append(list).append("\n");
}
}
/**
* Returns the precision for a given class label
*
* @param classLabel the label
* @return the precision for the label
*/
public double precision(Integer classLabel) {
return precision(classLabel, DEFAULT_EDGE_VALUE);
}
/**
* Returns the precision for a given label
*
* @param classLabel the label
* @param edgeCase What to output in case of 0/0
* @return the precision for the label
*/
public double precision(Integer classLabel, double edgeCase) {
double tpCount = truePositives.getCount(classLabel);
double fpCount = falsePositives.getCount(classLabel);
return EvaluationUtils.precision((long) tpCount, (long) fpCount, edgeCase);
}
/**
* Precision based on guesses so far.<br>
* Note: value returned will differ depending on number of classes and settings.<br>
* 1. For binary classification, if the positive class is set (via default value of 1, via constructor,
* or via {@link #setBinaryPositiveClass(Integer)}), the returned value will be for the specified positive class
* only.<br>
* 2. For the multi-class case, or when {@link #getBinaryPositiveClass()} is null, the returned value is macro-averaged
* across all classes. i.e., is macro-averaged precision, equivalent to {@code precision(EvaluationAveraging.Macro)}<br>
*
* @return the total precision based on guesses so far
*/
public double precision() {
if(binaryPositiveClass != null && numClasses() == 2){
return precision(binaryPositiveClass);
}
return precision(EvaluationAveraging.Macro);
}
/**
* Calculate the average precision for all classes. Can specify whether macro or micro averaging should be used
* NOTE: if any classes have tp=0 and fp=0, (precision=0/0) these are excluded from the average
*
* @param averaging Averaging method - macro or micro
* @return Average precision
*/
public double precision(EvaluationAveraging averaging) {
if(getNumRowCounter() == 0){
return 0.0; //No data
}
int nClasses = confusion().getClasses().size();
if (averaging == EvaluationAveraging.Macro) {
double macroPrecision = 0.0;
int count = 0;
for (int i = 0; i < nClasses; i++) {
double thisClassPrec = precision(i, -1);
if (thisClassPrec != -1) {
macroPrecision += thisClassPrec;
count++;
}
}
macroPrecision /= count;
return macroPrecision;
} else if (averaging == EvaluationAveraging.Micro) {
long tpCount = 0;
long fpCount = 0;
for (int i = 0; i < nClasses; i++) {
tpCount += truePositives.getCount(i);
fpCount += falsePositives.getCount(i);
}
return EvaluationUtils.precision(tpCount, fpCount, DEFAULT_EDGE_VALUE);
} else {
throw new UnsupportedOperationException("Unknown averaging approach: " + averaging);
}
}
/**
* When calculating the (macro) average precision, how many classes are excluded from the average due to
* no predictions – i.e., precision would be the edge case of 0/0
*
* @return Number of classes excluded from the average precision
*/
public int averagePrecisionNumClassesExcluded() {
return numClassesExcluded("precision");
}
/**
* When calculating the (macro) average Recall, how many classes are excluded from the average due to
* no predictions – i.e., recall would be the edge case of 0/0
*
* @return Number of classes excluded from the average recall
*/
public int averageRecallNumClassesExcluded() {
return numClassesExcluded("recall");
}
/**
* When calculating the (macro) average F1, how many classes are excluded from the average due to
* no predictions – i.e., F1 would be calculated from a precision or recall of 0/0
*
* @return Number of classes excluded from the average F1
*/
public int averageF1NumClassesExcluded() {
return numClassesExcluded("f1");
}
/**
* When calculating the (macro) average FBeta, how many classes are excluded from the average due to
* no predictions – i.e., FBeta would be calculated from a precision or recall of 0/0
*
* @return Number of classes excluded from the average FBeta
*/
public int averageFBetaNumClassesExcluded() {
return numClassesExcluded("fbeta");
}
private int numClassesExcluded(String metric) {
int countExcluded = 0;
int nClasses = confusion().getClasses().size();
for (int i = 0; i < nClasses; i++) {
double d;
switch (metric.toLowerCase()) {
case "precision":
d = precision(i, -1);
break;
case "recall":
d = recall(i, -1);
break;
case "f1":
case "fbeta":
d = fBeta(1.0, i, -1);
break;
default:
throw new RuntimeException("Unknown metric: " + metric);
}
if (d == -1) {
countExcluded++;
}
}
return countExcluded;
}
/**
* Returns the recall for a given label
*
* @param classLabel the label
* @return Recall rate as a double
*/
public double recall(int classLabel) {
return recall(classLabel, DEFAULT_EDGE_VALUE);
}
/**
* Returns the recall for a given label
*
* @param classLabel the label
* @param edgeCase What to output in case of 0/0
* @return Recall rate as a double
*/
public double recall(int classLabel, double edgeCase) {
double tpCount = truePositives.getCount(classLabel);
double fnCount = falseNegatives.getCount(classLabel);
return EvaluationUtils.recall((long) tpCount, (long) fnCount, edgeCase);
}
/**
* Recall based on guesses so far<br>
* Note: value returned will differ depending on number of classes and settings.<br>
* 1. For binary classification, if the positive class is set (via default value of 1, via constructor,
* or via {@link #setBinaryPositiveClass(Integer)}), the returned value will be for the specified positive class
* only.<br>
* 2. For the multi-class case, or when {@link #getBinaryPositiveClass()} is null, the returned value is macro-averaged
* across all classes. i.e., is macro-averaged recall, equivalent to {@code recall(EvaluationAveraging.Macro)}<br>
*
* @return the recall for the outcomes
*/
public double recall() {
if(binaryPositiveClass != null && numClasses() == 2){
return recall(binaryPositiveClass);
}
return recall(EvaluationAveraging.Macro);
}
/**
* Calculate the average recall for all classes - can specify whether macro or micro averaging should be used
* NOTE: if any classes have tp=0 and fn=0, (recall=0/0) these are excluded from the average
*
* @param averaging Averaging method - macro or micro
* @return Average recall
*/
public double recall(EvaluationAveraging averaging) {