1 """Ngram-multi tagging model.
2
3 @see: jazzparser.taggers.ngram_multi.tagger
4
5 """
6 """
7 ============================== License ========================================
8 Copyright (C) 2008, 2010-12 University of Edinburgh, Mark Granroth-Wilding
9
10 This file is part of The Jazz Parser.
11
12 The Jazz Parser is free software: you can redistribute it and/or modify
13 it under the terms of the GNU General Public License as published by
14 the Free Software Foundation, either version 3 of the License, or
15 (at your option) any later version.
16
17 The Jazz Parser is distributed in the hope that it will be useful,
18 but WITHOUT ANY WARRANTY; without even the implied warranty of
19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 GNU General Public License for more details.
21
22 You should have received a copy of the GNU General Public License
23 along with The Jazz Parser. If not, see <http://www.gnu.org/licenses/>.
24
25 ============================ End license ======================================
26
27 """
28 __author__ = "Mark Granroth-Wilding <mark.granroth-wilding@ed.ac.uk>"
29
30 from numpy import ones, float64, sum as array_sum, zeros
31 import copy, numpy
32
33 from jazzparser.utils.nltk.ngram import NgramModel
34 from jazzparser.utils.nltk.probability import logprob, add_logs, \
35 sum_logs, prob_dist_to_dictionary_prob_dist, \
36 cond_prob_dist_to_dictionary_cond_prob_dist, \
37 CutoffConditionalFreqDist, CutoffFreqDist
38 from jazzparser.utils.strings import str_to_bool
39 from jazzparser.utils.loggers import create_dummy_logger
40 from jazzparser.utils.base import group_pairs
41 from .. import TaggerTrainingError
42
43 from nltk.probability import ConditionalProbDist, FreqDist, \
44 ConditionalFreqDist, DictionaryProbDist, \
45 DictionaryConditionalProbDist, MutableProbDist
48 """
49 Function to generate all index n-grams of a given length
50
51 """
52 if length < 1:
53 return [[]]
54 else:
55 return sum([ [[i]+sub for i in range(num_labels)]
56 for sub in _all_indices(length-1, num_labels)], [])
57
59 """
60 An ngram model that takes multiple chords (weighted by probability) as
61 input to its decoding. It is trained on labeled data.
62
63 State labels are pairs (root,schema), each representing a lexical
64 schema instantiated on a specific root, i.e. a lexical category.
65 Emissions are pairs (root,label), representing chords. The parameters
66 are tied so that chords (r,l) can only be emitted from states (r,s).
67
68 The component distributions that are actually stored are:
69 - P(label | schema, chord_root=state_root) C{emission_dist}
70 - P(schema[t] | schema[t-1]) C{schema_transition_dist}
71 - P(root[t] - root[t-1] | schema[t-1]) C{root_transition_dist}
72
73 """
74 - def __init__(self, order, root_transition_counts, schema_transition_counts,
75 emission_counts,
76 estimator, backoff_model, schemata, chord_vocab,
77 history=""):
78 self.order = order
79 self.backoff_model = backoff_model
80
81 chord_vocab = list(set(chord_vocab))
82
83
84
85 self.label_dom = [(root,schema) for root in range(12) \
86 for schema in schemata]
87 self.num_labels = len(self.label_dom)
88 self.emission_dom = [(root,label) for root in range(12) \
89 for label in chord_vocab]
90 self.num_emissions = len(self.emission_dom)
91
92 self.schemata = schemata
93 self.chord_vocab = chord_vocab
94
95
96 self.root_transition_counts = root_transition_counts
97 self.schema_transition_counts = schema_transition_counts
98 self.emission_counts = emission_counts
99
100
101 self.root_transition_dist = ConditionalProbDist(
102 root_transition_counts, estimator, 12)
103 self.schema_transition_dist = ConditionalProbDist(
104 schema_transition_counts, estimator, len(schemata)+1)
105 self.emission_dist = ConditionalProbDist(
106 emission_counts, estimator, len(chord_vocab)+1)
107
108 self._estimator = estimator
109
110
111 self.history = history
112
113
114
115 self.clear_cache()
116
118 """
119 Initializes or empties probability distribution caches.
120
121 Make sure to call this if you change or update the distributions.
122
123 No caches used thus far, so this does nothing for now, except call
124 the super method.
125
126 """
127 self._schema_transition_matrix_cache = None
128 self._schema_transition_matrix_transpose_cache = None
129 self._root_transition_matrix_cache = None
130 return NgramModel.clear_cache(self)
131
132 - def add_history(self, string):
133 """ Adds a line to the end of this model's history string. """
134 self.history += "%s: %s\n" % (datetime.now().isoformat(' '), string)
135
136 @staticmethod
137 - def train(data, schemata, chord_types, estimator, cutoff=0, logger=None,
138 chord_map=None, order=2, backoff_orders=0, backoff_kwargs={}):
139 """
140 Initializes and trains an HMM in a supervised fashion using the given
141 training data. Training data should be chord sequence data (input
142 type C{bulk-db} or C{bulk-db-annotated}).
143
144 """
145
146 sequences = [
147 sequence for sequence in data if \
148 all([c.category is not None and len(c.category) \
149 for c in sequence.chords])
150 ]
151
152 if len(sequences) == 0:
153 raise TaggerTrainingError, "empty training data set"
154
155
156 if logger is None:
157 logger = create_dummy_logger()
158 logger.info(">>> Beginning training of multi-chord ngram tagging model")
159
160
161
162 if chord_map is None:
163 chord_trans = lambda x:x
164 else:
165 chord_trans = lambda x: chord_map[x]
166 emission_data = sum([
167 [(chord.category, chord_trans(chord.type))
168 for chord in sequence.chords]
169 for sequence in sequences], [])
170
171
172 emission_counts = CutoffConditionalFreqDist(cutoff)
173 for schema,ctype in emission_data:
174 emission_counts[schema].inc(ctype)
175
176
177 schema_transition_counts = CutoffConditionalFreqDist(cutoff)
178 root_transition_counts = CutoffConditionalFreqDist(cutoff)
179
180 for sequence in sequences:
181
182 final_ngram = tuple([c.category for c in sequence.chords[-order:-1]])
183 schema_transition_counts[sequence.chords[-1].category].inc(None)
184
185 transition_data = [None]*(order-1) + sequence.chords
186
187 for i in range(len(transition_data)-order):
188 ngram = list(reversed(transition_data[i:i+order]))
189
190
191 schema_ngram = [c.category if c is not None else None for c in ngram]
192 schema_transition_counts[tuple(schema_ngram[1:])].inc(schema_ngram[0])
193
194
195 if order > 1 and ngram[1] is not None:
196 root_change = (ngram[0].root - ngram[1].root) % 12
197 root_transition_counts[ngram[1].category].inc(root_change)
198
199 if backoff_orders > 0:
200
201 kwargs = {
202 'cutoff' : cutoff,
203 'logger' : logger,
204 'chord_map' : chord_map,
205 }
206 kwargs.update(backoff_kwargs)
207
208 kwargs['order'] = order-1
209 kwargs['backoff_orders'] = backoff_orders-1
210
211 backoff_model = MultiChordNgramModel.train(
212 data,
213 schemata,
214 chord_types,
215 estimator,
216 **kwargs)
217 else:
218 backoff_model = None
219
220
221 model = MultiChordNgramModel(order,
222 root_transition_counts,
223 schema_transition_counts,
224 emission_counts,
225 estimator,
226 backoff_model,
227 schemata,
228 chord_types)
229 return model
230
231
232
234
235 if context not in self._discount_cache:
236
237
238
239
240
241
242 seen_labels = set([lab for lab in self.schemata+[None] if
243 self.schema_transition_counts[context][lab] > 0])
244 if len(seen_labels) == 0:
245
246 self._discount_cache[context] = 0.0
247 else:
248 unseen_labels = set(self.schemata+[None]) - seen_labels
249
250
251 discounted_mass = self.schema_transition_dist[context].prob(
252 "%%% UNSEEN LABEL %%%") \
253 * len(unseen_labels)
254
255
256 backoff_context = context[:-1]
257 backoff_seen_mass = sum_logs([
258 self.backoff_model.schema_transition_log_probability_schemata(lab,
259 *backoff_context)
260 for lab in unseen_labels])
261 self._discount_cache[context] = logprob(discounted_mass) - \
262 backoff_seen_mass
263 return self._discount_cache[context]
264
266 """
267 Just the schema part of the transition distribution.
268
269 """
270 schemata = [state[1] if state is not None else None \
271 for state in states]
272 return self.schema_transition_log_probability_schemata(*schemata)
273
275 """
276 Just the schema part of the transition distribution. This method
277 takes just schemata as args, instead of whole states.
278
279 """
280 if len(previous_schemata):
281 schemata = tuple(previous_schemata)
282 else:
283 schemata = tuple()
284
285
286 if self.backoff_model is not None and self.schema_transition_counts[schemata][schema] == 0:
287
288
289 scale = self._get_transition_backoff_scaler(schemata)
290
291 schema_prob = scale + self.backoff_model.schema_transition_log_probability_schemata(schema, *(previous_schemata[:-1]))
292 else:
293 schema_prob = self.schema_transition_dist[schemata].logprob(schema)
294 return schema_prob
295
297 previous_states = [s if s is not None else (None,None) for s in previous_states]
298 if self.order == 1:
299 roots,schemata = [],[]
300 else:
301 roots,schemata = zip(*previous_states)
302 schemata = tuple(schemata)
303
304
305 schema_prob = self.schema_transition_log_probability(state, *previous_states)
306
307
308
309 if self.order == 1 or all(s is None for s in schemata):
310
311 root_prob = - logprob(12)
312 elif state is None:
313
314 root_prob = 0
315 else:
316
317 root_change = (state[0] - roots[0]) % 12
318
319 root_prob = self.root_transition_dist[schemata[0]].logprob(root_change)
320
321
322
323 return root_prob + schema_prob
324
326 """
327 Gives the probability P(emission | label). Returned as a base 2
328 log.
329
330 The emission should be a pair of (root,label), together defining a
331 chord.
332
333 There's a special case of this. If the emission is a list, it's
334 assumed to be a I{distribution} over emissions. The list should
335 contain (prob,em) pairs, where I{em} is an emission, such as is
336 normally passed into this function, and I{prob} is the weight to
337 give to this possible emission. The probabilities of the possible
338 emissions are summed up, weighted by the I{prob} values.
339
340 """
341 if type(emission) is list:
342
343 probs = []
344 for (prob,em) in emission:
345 probs.append(logprob(prob) + \
346 self.emission_log_probability(em, state))
347 return sum_logs(probs)
348
349
350 state_root,schema = state
351 chord_root,label = emission
352
353 if state_root != chord_root:
354 return float('-inf')
355 else:
356 return self.emission_dist[schema].logprob(label)
357
358
359
360
361
362
364 """ Transition matrix just for the schema distribution. """
365 if transpose:
366 if self._schema_transition_matrix_transpose_cache is None:
367
368 mat = self.get_schema_transition_matrix()
369
370 self._schema_transition_matrix_transpose_cache = \
371 numpy.copy(mat.transpose())
372 return self._schema_transition_matrix_transpose_cache
373 else:
374 if self._schema_transition_matrix_cache is None:
375
376 N = len(self.schemata)
377 shape = tuple([N]*self.order)
378 trans = numpy.zeros(shape, numpy.float64)
379
380 index_sets = _all_indices(self.order, N)
381
382 for indices in index_sets:
383
384 ngram_schemata = [self.schemata[i] for i in indices]
385 trans[tuple(indices)] = 2**self.schema_transition_log_probability_schemata(*ngram_schemata)
386
387 self._schema_transition_matrix_cache = trans
388 return self._schema_transition_matrix_cache
389
391 """ Transition matrix just for the root change distribution. """
392 if self._root_transition_matrix_cache is None:
393
394 N = len(self.schemata)
395 trans = numpy.zeros((12,12,N), numpy.float64)
396
397 if self.order == 1:
398 trans[:,:,:] = 1.0/12
399 else:
400
401 for i,schema in enumerate(self.schemata):
402 for root_change in range(12):
403
404 root_prob = self.root_transition_dist[schema].prob(root_change)
405
406 roots = [(last_root+root_change)%12 for last_root in range(12)]
407 last_roots = range(12)
408
409
410 trans[roots,last_roots,i] = root_prob
411 self._root_transition_matrix_cache = trans
412 return self._root_transition_matrix_cache
413
415 """
416 Emission matrix for states decomposed into schema and root.
417 Matrix has dimensions (time, root, schema).
418
419 """
420 T = len(sequence)
421 S = len(self.schemata)
422 ems = numpy.zeros((T, 12, S), numpy.float64)
423
424 for t,emission in enumerate(sequence):
425
426 for root in range(12):
427 for i,schema in enumerate(self.schemata):
428 ems[t,root,i] = self.emission_probability(emission, (root,schema))
429 return ems
430
432 """
433 @see: jazzparser.utils.nltk.ngram.model.NgramModel.normal_forward_probabilities
434 @note: tested against superclass method. They're giving the same
435 results to a high precision (differences ~1e-20)
436 @return: if root_schema=True,
437 (S+1)-dimensional Numpy array, where S is the number of schemata.
438 The dimensions are (time, last root, last schema, previous schema, etc).
439 Otherwise, the array has the same definition as the superclass.
440
441 """
442 N = len(sequence)
443 states = self.label_dom
444 schemata = self.schemata
445
446
447 schema_ems = self.get_schema_emission_matrix(sequence)
448 sc_trans = self.get_schema_transition_matrix()
449 root_trans = self.get_root_transition_matrix()
450
451 if self.order == 1:
452
453
454
455
456 forward_matrix = sc_trans*schema_ems
457
458 for t in range(N):
459 forward_matrix[t] /= numpy.sum(forward_matrix[t])
460 if not root_schema:
461 return forward_matrix.reshape(N,12*len(schemata))
462 return forward_matrix
463
464
465 forward_matrix = numpy.zeros([N,12]+[len(schemata)]*(self.order-1), numpy.float64)
466
467
468
469
470 for time in range(self.order-1):
471
472
473 index_sets = _all_indices(time+1, len(schemata))
474
475 for indices in index_sets:
476
477 ngram = [schemata[i] for i in indices]
478
479 ngram.extend([None]*(self.order-time-1))
480
481 indices.extend([0]*(self.order-time-2))
482
483 schema_prob = 2**self.schema_transition_log_probability_schemata(*ngram)
484
485 if time == 0:
486
487 selector = tuple([time,slice(None)]+indices)
488 forward_matrix[selector] += schema_prob
489 else:
490
491 previous_states = forward_matrix[
492 tuple([time-1,slice(None)] + indices[1:]+[0])]
493
494
495 root_probs = numpy.sum(
496 root_trans[:,:,indices[1]] * previous_states,
497 axis=1)
498
499
500 selector = tuple([time,slice(None)]+indices)
501 forward_matrix[selector] += root_probs
502
503
504 forward_matrix[time] = (forward_matrix[time].transpose() * schema_ems[time].transpose()).transpose()
505
506 forward_matrix[time] /= numpy.sum(forward_matrix[time])
507
508 for time in range(self.order-1, N):
509
510
511
512 trans_step = forward_matrix[time-1,:,numpy.newaxis] * sc_trans
513
514
515
516 trans_step = trans_step[numpy.newaxis].transpose() * root_trans[:,:,numpy.newaxis,:].transpose()
517
518
519
520
521 trans_step = numpy.sum(trans_step, axis=0)
522
523
524
525 trans_step = numpy.sum(trans_step, axis=-2)
526
527
528
529 forward_matrix[time] = (trans_step * schema_ems[time].transpose()).transpose()
530
531
532 forward_matrix[time] /= numpy.sum(forward_matrix[time])
533
534 if not root_schema:
535
536 new_forward_matrix = numpy.zeros([N,len(states)]*(self.order-1), numpy.float64)
537 indices = _all_indices(self.order-1, len(states))
538 for time in range(N):
539 for state_indices in indices:
540
541 schema_indices = [self.schemata.index(self.label_dom[i][1]) \
542 for i in state_indices]
543 root = self.label_dom[state_indices[0]][0]
544 selector = tuple([time,root]+schema_indices)
545
546 new_forward_matrix[tuple([time]+state_indices)] = forward_matrix[selector]
547 return new_forward_matrix
548 return forward_matrix
549
551 """
552 @see: jazzparser.utils.nltk.ngram.model.NgramModel.normal_backward_probabilities
553 @return: if root_schema=True,
554 (S+1)-dimensional Numpy array, where S is the number of schemata.
555 The dimensions are (time, last root, last schema, previous schema, etc).
556 Otherwise, the array has the same definition as the superclass.
557
558 """
559 N = len(sequence)
560 states = self.label_dom
561 schemata = self.schemata
562 S = len(schemata)
563
564 if self.order == 1:
565
566
567
568 if root_schema:
569 backward_matrix = numpy.ones((N,12,S), numpy.float64) / (12*S)
570 else:
571 backward_matrix = numpy.ones((N,12*S), numpy.float64) / (12*S)
572 return backward_matrix
573
574
575 schema_ems = self.get_schema_emission_matrix(sequence)
576
577 sc_trans_back = self.get_schema_transition_matrix()
578 sc_trans_back = numpy.copy(sc_trans_back[numpy.newaxis].transpose())
579 root_trans = self.get_root_transition_matrix().transpose()
580
581
582
583 backward_matrix = numpy.zeros([N]+[S]*(self.order-1)+[12], numpy.float64)
584
585 index_sets = _all_indices(self.order-1, S)
586
587
588 for indices in index_sets:
589
590 ngram = [schemata[i] for i in indices]
591
592
593
594 selector = tuple([N-1]+list(reversed(indices))+[slice(None)])
595 backward_matrix[selector] = 2**self.schema_transition_log_probability_schemata(None, *ngram)
596 backward_matrix[N-1] /= numpy.sum(backward_matrix[N-1])
597
598
599 for time in range(N-2, self.order-2, -1):
600
601
602
603
604
605
606
607
608
609
610
611 bwd = schema_ems[time+1].transpose() * backward_matrix[time+1]
612
613
614 bwd = sc_trans_back * bwd
615
616
617 bwd = numpy.sum(bwd, axis=-2)
618
619
620 bwd = (bwd.transpose()[:,numpy.newaxis]).transpose()
621
622
623 bwd = bwd * root_trans
624
625
626 bwd = numpy.sum(bwd, axis=-1)
627
628 backward_matrix[time] = bwd
629
630 backward_matrix[time] /= numpy.sum(backward_matrix[time])
631
632
633 for time in range(self.order-2, -1, -1):
634
635 id_tgrams = _all_indices(time+1, S)
636
637 for id_tgram in id_tgrams:
638
639 tgram = [schemata[i] for i in id_tgram]
640
641
642 nm1gram = tgram + [None]*(self.order-2-time)
643
644 id_nm1gram = id_tgram + [0]*(self.order-2-time)
645
646
647 summed_prob = 0.0
648 for next_id in range(S):
649
650 schema_prob = 2**self.schema_transition_log_probability_schemata(schemata[next_id], *nm1gram)
651
652 for next_root in range(12):
653
654 prob = schema_prob * schema_ems[time+1, next_root, next_id]
655
656
657 next_state = tuple([time+1]+list(reversed(id_nm1gram[:-1]))+[next_id,next_root])
658 prob *= backward_matrix[next_state]
659
660
661 for current_root in range(12):
662 if time > 0:
663 root_change = (next_root - current_root) % 12
664 root_prob = self.root_transition_dist[nm1gram[0]].prob(root_change)
665 else:
666
667 root_prob = 1.0/12
668 backward_matrix[tuple(
669 [time] +
670 list(reversed(id_nm1gram)) +
671 [current_root])] += prob * root_prob
672
673
674 backward_matrix[time] /= numpy.sum(backward_matrix[time])
675
676
677
678 reordered_backward_matrix = numpy.zeros([N,12]+[S]*(self.order-1), numpy.float64)
679 for time in range(N):
680 reordered_backward_matrix[time] = backward_matrix[time].transpose()
681
682 if not root_schema:
683
684 new_backward_matrix = numpy.zeros([N,len(states)]*(self.order-1), numpy.float64)
685 indices = _all_indices(self.order-1, len(states))
686 for time in range(N):
687 for state_indices in indices:
688
689 schema_indices = [self.schemata.index(self.label_dom[i][1]) \
690 for i in state_indices]
691 root = self.label_dom[state_indices[0]][0]
692 selector = tuple([time,root]+schema_indices)
693
694 new_backward_matrix[tuple([time]+state_indices)] = reordered_backward_matrix[selector]
695 return new_backward_matrix
696 return reordered_backward_matrix
697
700 """
701 State-occupation probabilities.
702
703 Overridden so we don't need to construct the full ngram matrix,
704 which can be huge with trigrams to the point of running out of
705 memory.
706
707 @type dictionary: bool
708 @param dictionary: return a list of label dictionaries instead
709 of a numpy matrix
710
711 """
712
713
714 forward = self.normal_forward_probabilities(sequence, root_schema=True)
715 backward = self.normal_backward_probabilities(sequence, root_schema=True)
716 gamma = forward * backward
717
718
719 for i in range(gamma.ndim-3):
720 gamma = numpy.sum(gamma, axis=-1)
721
722 (T,R,S) = gamma.shape
723
724 for t in range(T):
725 gamma[t] /= numpy.sum(gamma[t])
726
727 if dictionary:
728
729 dict_gamma = []
730 for t in range(T):
731 dic = {}
732 for r in range(12):
733 for s,schema in enumerate(self.schemata):
734 dic[(r,schema)] = gamma[t,r,s]
735 dict_gamma.append(dic)
736 return dict_gamma
737 else:
738
739 new_gamma = numpy.copy(gamma.reshape(T,R*S))
740 return new_gamma
741
743 """ Alias for backward compatibility. Use C{gamma_probabilities} """
744 return self.gamma_probabilities(*args, **kwargs)
745
746
747
749 """ Produces a picklable representation of model as a dict. """
750 from jazzparser.utils.nltk.storage import object_to_dict
751
752 if self.backoff_model is not None:
753 backoff_model = self.backoff_model.to_picklable_dict()
754 else:
755 backoff_model = None
756
757 return {
758 'order' : self.order,
759 'root_transition_counts' : object_to_dict(self.root_transition_counts),
760 'schema_transition_counts' : object_to_dict(self.schema_transition_counts),
761 'emission_counts' : object_to_dict(self.emission_counts),
762 'estimator' : self._estimator,
763 'backoff_model' : backoff_model,
764 'chord_vocab' : self.chord_vocab,
765 'schemata' : self.schemata,
766 'history' : self.history,
767 }
768
769 @classmethod
792
796 """
797 Gets an emission sequence in an appropriate format for the ngram-multi
798 HMM model from a chord lattice.
799
800 @see: L{jazzparser.data.input.WeightedChordLabelInput}
801
802 """
803 emissions = []
804
805 if chord_map is None:
806 _map_chord = lambda c:c
807 else:
808 _map_chord = lambda c: chord_map[c]
809
810 for timestep in lattice:
811 time_emissions = []
812
813 for (label, prob) in timestep:
814 time_emissions.append((prob, (label.root, _map_chord(label.label))))
815
816 emissions.append(time_emissions)
817 return emissions
818