1 """Semantic distance metrics.
2
3 """
4 """
5 ============================== License ========================================
6 Copyright (C) 2008, 2010-12 University of Edinburgh, Mark Granroth-Wilding
7
8 This file is part of The Jazz Parser.
9
10 The Jazz Parser is free software: you can redistribute it and/or modify
11 it under the terms of the GNU General Public License as published by
12 the Free Software Foundation, either version 3 of the License, or
13 (at your option) any later version.
14
15 The Jazz Parser is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 GNU General Public License for more details.
19
20 You should have received a copy of the GNU General Public License
21 along with The Jazz Parser. If not, see <http://www.gnu.org/licenses/>.
22
23 ============================ End license ======================================
24
25 """
26 __author__ = "Mark Granroth-Wilding <mark.granroth-wilding@ed.ac.uk>"
27
28 from jazzparser.utils.options import ModuleOption, choose_from_list
29 from jazzparser.formalisms.base.semantics.distance import DistanceMetric, \
30 FScoreMetric
31 from jazzparser.formalisms.music_halfspan.evaluation import tonal_space_f_score, \
32 tonal_space_alignment_score, tonal_space_align, \
33 arrange_alignment, tonal_space_distance, \
34 tonal_space_length
35
37 """
38 Original tonal space distance metric computed as the edit distance of
39 the step vectors and functions of the path through the tonal space
40 implied by the semantics.
41
42 """
43 OPTIONS = [
44 ModuleOption('output', filter=choose_from_list(
45 ['f','precision','recall','inversef','dist']),
46 usage="output=O, where O is one of 'f', 'precision', "\
47 "'recall', 'inversef', 'dist'",
48 default='dist',
49 help_text="Select what metric to output. Choose recall "\
50 "or precision for asymmetric metrics. F-score ('f') "\
51 "combines these two. This is inverted ('inversef') "\
52 "to get a distance, rather than similarity. "\
53 "Alternatively, use the edit distance of the alignment "\
54 "('dist', default)"),
55 ]
56 name = "tsed"
57
59 if sem1 is None or sem2 is None:
60 alignment_score = 0.0
61 else:
62 alignment_score = tonal_space_alignment_score(sem1.lf, sem2.lf)
63
64 if sem1 is None:
65 len1 = 0.0
66 else:
67 len1 = tonal_space_length(sem1)
68 if sem2 is None:
69 len2 = 0.0
70 else:
71 len2 = tonal_space_length(sem2)
72 return alignment_score,len1,len2
73
75 ident = {
76 'f' : 'f-score',
77 'precision' : 'precision',
78 'recall' : 'recall',
79 'inversef' : 'inverse f-score',
80 'dist' : 'edit distance',
81 }
82 return "tsed %s" % ident[self.options['output']]
83 identifier = property(_get_identifier)
84
101
103 """
104 Shows the optimal alignment of the paths that the score comes from.
105
106 @see: jazzparser.formalisms.music_halfspan.semantics.distance.DistanceMetric.print_computation
107
108 """
109 pairs = tonal_space_align(sem1.lf, sem2.lf)
110 return "\n".join(["%s %s" % pair for pair in pairs])
111
119
125
126
128
129
130
131 if len(tree.root) == 0:
132
133 return "NA"
134 else:
135
136 label = tree.root[0].label
137 if label == "leftonto":
138 return "perfect"
139 elif label == "rightonto":
140 return "plagal"
141 else:
142 raise ValueError, "unknown cadence type with node label "\
143 "'%s' in the dependency graph" % label
144
146 """
147 Tonal space distance metric computed as the size of the largest subtree
148 that can be embedded in the dependency graphs of two logical forms. This
149 is done separately for each alignment of cadences in the two logical
150 forms and the global optimum is used.
151
152 """
153 OPTIONS = FScoreMetric.OPTIONS + [
154 ModuleOption('res_score', filter=int,
155 usage="res_score=R, where R is an integer",
156 default=2,
157 help_text="Score to give to matching resolutions. 1 "\
158 "is the score given to a matching node in the "\
159 "dependency tree. The default (2) gives more "\
160 "weight to matching resolutions that tree nodes. "\
161 "Special value -1 assigns a weight equal to the size "\
162 "of the common dependency tree + 1"),
163 ]
164 name = "lces"
165
167 ident = {
168 'f' : 'f-score',
169 'precision' : 'precision',
170 'recall' : 'recall',
171 'inversef' : 'inverse f-score',
172 }
173 return "dependency tree %s" % ident[self.options['output']]
174 identifier = property(_get_identifier)
175
177 """
178 The core computation of the distance metric. Takes care of the tree
179 comparison and cadence alignment and return the vital statistics.
180
181 """
182 from jazzparser.formalisms.music_halfspan.harmstruct import \
183 semantics_to_dependency_trees
184 from jazzparser.misc.tree.lces import lces_size
185 from jazzparser.utils.distance import align
186
187 res_score = self.options['res_score']
188
189
190 if sem1 is None:
191 trees1 = []
192 else:
193 trees1 = semantics_to_dependency_trees(sem1)
194 if sem2 is None:
195 trees2 = []
196 else:
197 trees2 = semantics_to_dependency_trees(sem2)
198
199 if sem1 is None or sem2 is None:
200
201 alignment_score = 0.0
202 alignment = []
203 transpose = None
204 else:
205
206
207 distances = []
208 for x_trans in range(4):
209 for y_trans in range(3):
210 def _align(tree1, tree2):
211
212 label2 = ((tree2.root.label[0] + x_trans) % 4,
213 (tree2.root.label[1] + y_trans) % 3)
214
215 same_res = tree1.root.label == label2
216
217 same_cad = _cadence_type(tree1) == _cadence_type(tree2)
218 if same_cad:
219
220 tree_similarity = lces_size(tree1, tree2)
221 else:
222 tree_similarity = 0
223
224
225 if res_score == -1:
226 res_match = tree_similarity + 1
227 else:
228 res_match = res_score
229 return - tree_similarity - (res_match if same_res else 0)
230
231 aligned,dist = align(trees1, trees2, delins_cost=0,
232 subst_cost=_align,
233 dist=True)
234 distances.append((dist,aligned,(x_trans,y_trans)))
235
236 alignment_score,alignment,transpose = min(distances,
237 key=lambda x:x[0])
238 alignment_score = -float(alignment_score)
239
240 def _max_score(trees):
241 """
242 Get the maximum possible score that could be assigned to a match
243 with this tree set.
244
245 """
246 score = 0
247 for tree in trees:
248
249
250 tree_sim = len(tree)
251 if res_score == -1:
252 res_match = tree_sim + 1
253 else:
254 res_match = res_score
255
256 score += tree_sim + res_match
257 return score
258 max_score1 = _max_score(trees1)
259 max_score2 = _max_score(trees2)
260
261 return alignment_score, max_score1, max_score2, alignment, transpose
262
264 from jazzparser.misc.tree.lces import lces
265 from cStringIO import StringIO
266
267 stats = self.fscore_match(sem1, sem2)
268 trans = stats[4]
269 buf = StringIO()
270
271 print >>buf, "LF1: %s" % sem1
272 print >>buf, "LF2: %s" % sem2
273 print >>buf, "LF2 transposed by (%d,%d)\n" % trans
274 print >>buf, "Maximal cadence alignment:"
275
276
277 for sem1cad, sem2cad in stats[3]:
278 if sem1cad is None:
279 print >>buf, "1: deleted"
280 else:
281 print >>buf, "1: %s" % sem1cad
282 if sem2cad is None:
283 print >>buf, "2: deleted"
284 else:
285 print >>buf, "2: %s" % sem2cad
286 if sem1cad is not None and sem2cad is not None:
287
288 print >>buf, "Cadence types: %s %s" % (_cadence_type(sem1cad),
289 _cadence_type(sem2cad))
290 root2 = sem2cad.root.label
291 root2 = ((root2[0]+trans[0])%4, (root2[1]+trans[1])%3)
292 print >>buf, "Resolutions: %s %s" % (sem1cad.root.label, root2)
293 common = lces(sem1cad, sem2cad)
294 print >>buf, "Shared structure: %s (size %d)" % (common, len(common)-1)
295 print >>buf
296 return buf.getvalue()
297
299 """
300 Aligns the two dependency graphs in the way that optimizes their
301 dependency recovery and reports that dependency recovery. This gives
302 a metric that can be used when the alignment between the graphs is not
303 known, such as when parsing MIDI.
304
305 """
306 name = "optdeprec"
307
309 ident = {
310 'f' : 'f-score',
311 'precision' : 'precision',
312 'recall' : 'recall',
313 'inversef' : 'inverse f-score',
314 }
315 return "dependency alignment %s" % ident[self.options['output']]
316 identifier = property(_get_identifier)
317
319 from jazzparser.formalisms.music_halfspan.harmstruct import \
320 semantics_to_dependency_graph
321 from jazzparser.data.dependencies import optimal_node_alignment, \
322 alignment_to_graph
323 from jazzparser.formalisms.music_halfspan.semantics import \
324 EnharmonicCoordinate
325
326 if sem1 is None:
327 max_score1 = 0.0
328 else:
329 graph1,timings1 = semantics_to_dependency_graph(sem1)
330 max_score1 = float(len(graph1))
331
332 if sem2 is None:
333 max_score2 = 0.0
334 else:
335 graph2,timings2 = semantics_to_dependency_graph(sem2)
336 max_score2 = float(len(graph2))
337
338 if sem1 is None or sem2 is None:
339
340 alignment_score = 0.0
341 alignment = []
342 transpose = None
343 else:
344 graph1,timings1 = semantics_to_dependency_graph(sem1)
345 graph2,timings2 = semantics_to_dependency_graph(sem2)
346
347 graphs = []
348
349 for transx in range(4):
350 for transy in range(3):
351 def _label_compare(label1, label2):
352 if isinstance(label1, EnharmonicCoordinate) and \
353 isinstance(label2, EnharmonicCoordinate):
354 coord1 = label1.zero_coord
355 x2,y2 = label2.zero_coord
356 return coord1 == ((x2+transx)%4, (y2+transy)%3)
357 else:
358 return label1 == label2
359
360
361 alignment = optimal_node_alignment(graph1, graph2, label_compare=_label_compare)
362
363 graph, node_map1, node_map2 = alignment_to_graph(alignment,
364 graph1, graph2, label_compare=_label_compare)
365
366 graphs.append(graph)
367
368
369 alignment_score,graph = max([(len(graph),graph) for graph in graphs], key=lambda x:x[0])
370
371 return alignment_score,max_score1,max_score2
372
374 """
375 Exact dependency recovery metric. Only matches two nodes to each other
376 if they have the same time attached to them. This is for use with results
377 where we know the input is the same as that over which the gold standard
378 is defined.
379
380 For example, evaluating chord sequence parsing against the
381 corpus we know this. It won't work, however, evaluating midi parsing
382 against the chord corpus.
383
384 It is also not pitch-independent, since it's only useful where the input
385 over which the result was produced is the same anyway.
386
387 """
388 name = "deprec"
389
391 ident = {
392 'f' : 'f-score',
393 'precision' : 'precision',
394 'recall' : 'recall',
395 'inversef' : 'inverse f-score',
396 }
397 return "dependency recovery %s" % ident[self.options['output']]
398 identifier = property(_get_identifier)
399
401 from jazzparser.formalisms.music_halfspan.harmstruct import \
402 semantics_to_dependency_graph
403 from jazzparser.data.dependencies import optimal_node_alignment, \
404 alignment_to_graph
405 from jazzparser.formalisms.music_halfspan.semantics import \
406 EnharmonicCoordinate
407
408 if sem1 is None:
409 max_score1 = 0.0
410 else:
411 graph1,timings1 = semantics_to_dependency_graph(sem1)
412 max_score1 = float(len(graph1))
413
414 if sem2 is None:
415 max_score2 = 0.0
416 else:
417 graph2,timings2 = semantics_to_dependency_graph(sem2)
418 max_score2 = float(len(graph2))
419
420 if sem1 is None or sem2 is None:
421
422 alignment_score = 0.0
423 alignment = []
424 transpose = None
425 else:
426 graph1,timings1 = semantics_to_dependency_graph(sem1)
427 graph2,timings2 = semantics_to_dependency_graph(sem2)
428
429 node_pairs = []
430
431 node_pairs.append((min(graph1.nodes), min(graph2.nodes)))
432
433
434 time_nodes1 = dict([(time,node) for (node,time) in timings1.items()])
435 for node2,time in sorted(timings2.items(), key=lambda x:x[1]):
436 if time in time_nodes1:
437 node_pairs.append((time_nodes1[time], node2))
438
439 def _label_compare(label1, label2):
440 if isinstance(label1, EnharmonicCoordinate) and \
441 isinstance(label2, EnharmonicCoordinate):
442 return label1.zero_coord == label2.zero_coord
443 else:
444 return label1 == label2
445
446
447 graph,node_map1,node_map2 = alignment_to_graph(node_pairs,
448 graph1, graph2, label_compare=_label_compare)
449
450
451 alignment_score = len(graph)
452
453 return alignment_score,max_score1,max_score2
454
456 """
457 Returns a distance by picking a random number. This is useful for
458 establishing a random baseline on evaluations.
459 Obviously it won't be the same for two calls on the same inputs.
460
461 """
462 OPTIONS = []
463 name = "rand"
464
466 import random
467 return random.random()
468
470 """
471 This is a baseline metric that does nothing clever. It's designed to
472 show how well a system could do just by comparing the lengths of the
473 two analyses in terms of the number of dependencies in them.
474 We'd hope it wouldn't do very well, but it's an important
475 baseline to try.
476
477 The distance is the inverse ratio between the lengths, always between 0
478 and 1.
479
480 """
481 OPTIONS = []
482 name = "depsize"
483
499