1 """
2 For multi-class classifications
3 """
4 __version__ = "$Revision: 1.24 $"
5
6 from Evaluator import Evaluator
7 from Evaluator import EvaluationData
8 import sys, os, types
9 sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/..")
10 from Core.IdSet import IdSet
11 import Core.ExampleUtils as ExampleUtils
12 import itertools
15 """
16 An evaluator for multiclass classification results, where an example can belong to one
17 of several classes. For calculating averages over multiple classes, one of the classes,
18 "neg"/1 is considered to be negative while the others are considered to be different
19 types of positive instances.
20 """
21 type = "multiclass"
22
23 - def __init__(self, examples, predictions=None, classSet=None):
24 if type(classSet) == types.StringType:
25 classSet = IdSet(filename=classSet)
26 if type(predictions) == types.StringType:
27 predictions = ExampleUtils.loadPredictions(predictions)
28 if type(examples) == types.StringType:
29 examples = ExampleUtils.readExamples(examples, False)
30
31 self.classSet = classSet
32
33 self.classSet = classSet
34 if classSet != None:
35 classNames = sorted(classSet.Ids.keys())
36 else:
37 classNames = []
38
39 self.classes = []
40 for className in classNames:
41 self.classes.append(classSet.getId(className))
42
43 self.dataByClass = {}
44 for cls in self.classes:
45 self.dataByClass[cls] = EvaluationData()
46
47 if len(self.dataByClass) == 0:
48 self.dataByClass[1] = EvaluationData()
49 self.dataByClass[2] = EvaluationData()
50
51
52 self.untypedCurrentMajorId = None
53 self.untypedPredictionQueue = []
54 self.untypedUndirected = EvaluationData()
55
56 if predictions != None:
57 self._calculate(examples, predictions)
58
59 @classmethod
60 - def evaluate(cls, examples, predictions, classSet=None, outputFile=None, verbose=True):
61 """
62 Enables using this class without having to manually instantiate it
63 """
64 evaluator = cls(examples, predictions, classSet)
65 if verbose:
66 print >> sys.stderr, evaluator.toStringConcise()
67 if outputFile != None:
68 evaluator.saveCSV(outputFile)
69 return evaluator
70
72 if self.microF.fscore > evaluation.microF.fscore:
73 return 1
74 elif self.microF.fscore == evaluation.microF.fscore:
75 return 0
76 else:
77 return -1
78
81
82 @classmethod
84
85 if type(examples) in types.StringTypes:
86 examples = ExampleUtils.readExamples(examples, False)
87 if type(predictions) in types.StringTypes:
88 predictions = ExampleUtils.loadPredictions(predictions)
89 pairs = []
90 realPositives = 0
91 for example, prediction in itertools.izip(examples, predictions):
92 trueClass = example[1]
93 assert(trueClass > 0)
94 if trueClass > 1:
95 realPositives += 1
96 negClassValue = prediction[1]
97 pairs.append( (negClassValue, trueClass) )
98 pairs.sort(reverse=True)
99 realNegatives = len(pairs) - realPositives
100
101
102 binaryF = EvaluationData()
103 binaryF._tp = realPositives
104 binaryF._fp = realNegatives
105 binaryF._fn = 0
106 binaryF.calculateFScore()
107 fscore = binaryF.fscore
108 threshold = pairs[0][0]-1.
109
110
111 for pair in pairs:
112 if pair[1] == 1:
113 binaryF._fp -= 1
114 else:
115 binaryF._tp -= 1
116 binaryF._fn += 1
117 binaryF.calculateFScore()
118 if binaryF.fscore > fscore:
119 fscore = binaryF.fscore
120 threshold = pair[0]+0.00000001
121 return threshold, fscore
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
164 """
165 All examples within the same majorId (same sentence) are
166 put in queue. Once major id (sentence) changes, these
167 examples are processed.
168 """
169 majorId, minorId = example[0].rsplit(".x", 1)
170 if majorId != self.untypedCurrentMajorId:
171 self._processUntypedUndirectedQueue()
172 self.untypedCurrentMajorId = majorId
173 self.untypedPredictionQueue.append( (example, prediction) )
174
176 """
177 Determines the untyped undirected performance by merging example
178 pairs. This statistic is only meaningful for examples representing
179 directed edges where two consecutive examples are the two directed
180 edges between a pair of nodes.
181 """
182 prevExample = None
183 prevPrediction = None
184 for example, prediction in self.untypedPredictionQueue:
185 majorId, minorId = example[0].rsplit(".x", 1)
186 if prevExample != None and prevPrediction != None and int(minorId) % 2 != 0:
187
188 if example[1] != 1 or prevExample[1] != 1:
189 trueClass = 1
190 else:
191 trueClass = -1
192
193 if prediction[0] != 1 or prevPrediction[0] != 1:
194 predictedClass = 1
195 else:
196 predictedClass = -1
197 self.untypedUndirected.addInstance(trueClass == 1, predictedClass == 1)
198 prevExample = example
199 prevPrediction = prediction
200 self.untypedPredictionQueue = []
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
231 """
232 The actual evaluation
233 """
234
235
236 self.microF = EvaluationData()
237 self.binaryF = EvaluationData()
238
239
240
241 for example, prediction in itertools.izip(examples, predictions):
242
243
244
245 trueClass = example[1]
246 assert(trueClass > 0)
247 predictedClass = prediction[0]
248
249 assert(predictedClass > 0)
250 if predictedClass == trueClass:
251
252 self.dataByClass[trueClass].addTP()
253 if trueClass != 1:
254
255
256 self.microF.addTP()
257 self.binaryF.addTP()
258 else:
259
260
261 self.microF.addTN()
262 self.binaryF.addTN()
263 for cls in self.classes:
264
265
266
267 if cls != trueClass:
268 self.dataByClass[cls].addTN()
269 else:
270
271 self.dataByClass[predictedClass].addFP()
272 if predictedClass == 1:
273
274
275 self.microF.addFN()
276 self.binaryF.addFN()
277 else:
278
279
280 self.microF.addFP()
281 if trueClass == 1:
282 self.binaryF.addFP()
283 else:
284 self.binaryF.addTP()
285 for cls in self.classes:
286 if cls == trueClass:
287 self.dataByClass[cls].addFN()
288 elif cls != predictedClass:
289 self.dataByClass[cls].addTN()
290
291
292
293
294
295
296 for cls in self.classes:
297 self.dataByClass[cls].calculateFScore()
298 self.microF.calculateFScore()
299 self.binaryF.calculateFScore()
300
301
302
303 numClassesWithInstances = 0
304 self.macroF = EvaluationData()
305 self.macroF.precision = 0.0
306 self.macroF.recall = 0.0
307 self.macroF.fscore = 0.0
308 for cls in self.classes:
309 if (self.dataByClass[cls].getNumInstances() > 0 or self.dataByClass[cls].getFP() > 0) and cls != self.classSet.getId("neg", False):
310 numClassesWithInstances += 1
311 self.macroF.precision += self.dataByClass[cls].precision
312 self.macroF.recall += self.dataByClass[cls].recall
313 if self.dataByClass[cls].fscore != "N/A":
314 self.macroF.fscore += self.dataByClass[cls].fscore
315 if numClassesWithInstances > 0:
316 if self.macroF.precision != 0: self.macroF.precision /= float(numClassesWithInstances)
317 if self.macroF.recall != 0: self.macroF.recall /= float(numClassesWithInstances)
318 if self.macroF.fscore != 0: self.macroF.fscore /= float(numClassesWithInstances)
319
321 """
322 Evaluation results in a human readable string format
323 """
324 if title != None:
325 string = indent + title + "\n"
326 indent += " "
327 string += indent
328 else:
329 string = indent
330 negativeClassId = None
331 for cls in self.classes:
332 if cls != self.classSet.getId("neg", False):
333 string += self.classSet.getName(cls)
334 string += " " + self.dataByClass[cls].toStringConcise() + "\n" + indent
335 else:
336 negativeClassId = cls
337 if negativeClassId != None:
338 cls = negativeClassId
339 string += "(neg " + self.dataByClass[cls].toStringConcise() + ")\n" + indent
340
341 string += "averages:\n" + indent
342
343 string += "micro " + self.microF.toStringConcise() + "\n" + indent
344
345 string += "macro " + self.macroF.prfToString() + "\n" + indent
346
347 string += "untyped " + self.binaryF.toStringConcise()
348
349 if self.untypedUndirected != None:
350 string += "\n" + indent
351 string += "untyped undirected " + self.untypedUndirected.toStringConcise()
352 return string
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
373 """
374 Evaluation results in a computationally easy to process dictionary format
375 """
376 dicts = []
377 if len(self.classes) > 0:
378 assert(not ("1" in self.classSet.getNames() and "neg" in self.classSet.getNames()))
379 negativeClassId = None
380 for cls in self.classes:
381 if cls != self.classSet.getId("neg", False) and cls != self.classSet.getId("1", False):
382 values = self.dataByClass[cls].toDict()
383 values["class"] = self.classSet.getName(cls)
384 dicts.append(values)
385 else:
386 assert(negativeClassId == None)
387 negativeClassId = cls
388 if negativeClassId != None:
389 values = self.dataByClass[negativeClassId].toDict()
390 values["class"] = "neg"
391 dicts.append(values)
392 dicts.append( self.microF.toDict() )
393 dicts[-1]["class"] = "micro"
394 dicts.append( self.macroF.toDict() )
395 dicts[-1]["class"] = "macro"
396 dicts.append( self.binaryF.toDict() )
397 dicts[-1]["class"] = "untyped"
398 if self.untypedUndirected != None:
399 dicts.append(self.untypedUndirected.toDict())
400 dicts[-1]["class"] = "untyped undirected"
401 return dicts
402