Package jazzparser :: Package taggers :: Package ngram_multi :: Module model
[hide private]
[frames] | no frames]

Source Code for Module jazzparser.taggers.ngram_multi.model

  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 
46 47 -def _all_indices(length, num_labels):
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
58 -class MultiChordNgramModel(NgramModel):
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 # Construct the domains by combining possible roots with 84 # the other components of the labels 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 # Keep hold of the freq dists 96 self.root_transition_counts = root_transition_counts 97 self.schema_transition_counts = schema_transition_counts 98 self.emission_counts = emission_counts 99 100 # Make some prob dists 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 # Store a string with information about training, etc 111 self.history = history 112 113 # Initialize the various caches 114 # These will be filled as we access probabilities 115 self.clear_cache()
116
117 - def clear_cache(self):
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 # Remove any sequences that aren't fully labeled 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 # Prepare a dummy logger if none was given 156 if logger is None: 157 logger = create_dummy_logger() 158 logger.info(">>> Beginning training of multi-chord ngram tagging model") 159 160 # Prepare training data from these sequences 161 # Training set for emission dist 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 # Train the emission distribution 172 emission_counts = CutoffConditionalFreqDist(cutoff) 173 for schema,ctype in emission_data: 174 emission_counts[schema].inc(ctype) 175 176 # Train the transition distribution 177 schema_transition_counts = CutoffConditionalFreqDist(cutoff) 178 root_transition_counts = CutoffConditionalFreqDist(cutoff) 179 180 for sequence in sequences: 181 # Add a count for the transition to the final state 182 final_ngram = tuple([c.category for c in sequence.chords[-order:-1]]) 183 schema_transition_counts[sequence.chords[-1].category].inc(None) 184 # Make n-gram counts 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 # Count the schema transition 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 # Now count the relative root, conditioned on the schema 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 # Train a lower-order model 201 kwargs = { 202 'cutoff' : cutoff, 203 'logger' : logger, 204 'chord_map' : chord_map, 205 } 206 kwargs.update(backoff_kwargs) 207 # These kwargs can't be overridden 208 kwargs['order'] = order-1 209 kwargs['backoff_orders'] = backoff_orders-1 210 # Run the model training 211 backoff_model = MultiChordNgramModel.train( 212 data, 213 schemata, 214 chord_types, 215 estimator, 216 **kwargs) 217 else: 218 backoff_model = None 219 220 # Instantiate a model with these distributions 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 ################## Probabilities ###################
233 - def _get_transition_backoff_scaler(self, context):
234 # This is just for the schema distribution 235 if context not in self._discount_cache: 236 # The prob mass reserved for unseen events can be computed by 237 # summing probabilities over all seen events and subtracting 238 # from 1. 239 # Our discounting model distributes this probability evenly over 240 # the unseen events, so we can compute the discounted mass by 241 # getting the probability of one unseen event and multiplying it. 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 # Not seen anything in this context. All mass is discounted! 246 self._discount_cache[context] = 0.0 247 else: 248 unseen_labels = set(self.schemata+[None]) - seen_labels 249 # Try getting some event that won't have been seen 250 # Compute how much mass is reserved for unseen events 251 discounted_mass = self.schema_transition_dist[context].prob( 252 "%%% UNSEEN LABEL %%%") \ 253 * len(unseen_labels) 254 # Compute how much probability the n-1 order model assigns to 255 # things unseen by this model 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
265 - def schema_transition_log_probability(self, *states):
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
274 - def schema_transition_log_probability_schemata(self, schema, *previous_schemata):
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 # Check whether we have enough observations of this whole n-gram 286 if self.backoff_model is not None and self.schema_transition_counts[schemata][schema] == 0: 287 # Backoff to a lower-order model 288 # Work out how much prob mass is reserved for unseen events 289 scale = self._get_transition_backoff_scaler(schemata) 290 # Backoff and scale to fill the reserved mass 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
296 - def transition_log_probability(self, state, *previous_states):
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 # Use the separate method to get the probability from the schema dist 305 schema_prob = self.schema_transition_log_probability(state, *previous_states) 306 307 # Then the root transition distribution 308 # Don't look at the root transition if this is a unigram model 309 if self.order == 1 or all(s is None for s in schemata): 310 # All roots equiprobable 311 root_prob = - logprob(12) 312 elif state is None: 313 # Final state: no root transition, prob comes from state dist 314 root_prob = 0 315 else: 316 # Calculate the root change from the previous chord 317 root_change = (state[0] - roots[0]) % 12 318 # Condition the root change prob on the *previous* schema 319 root_prob = self.root_transition_dist[schemata[0]].logprob(root_change) 320 321 # Multiply together the probability of the schema transition and the 322 # root change 323 return root_prob + schema_prob
324
325 - def emission_log_probability(self, emission, state):
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 # Average probability over the possible emissions 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 # Single chord label 350 state_root,schema = state 351 chord_root,label = emission 352 # Probability is 0 if the roots don't match 353 if state_root != chord_root: 354 return float('-inf') 355 else: 356 return self.emission_dist[schema].logprob(label)
357 358 ############################################## 359 # We override the forward and backward computations. Using the generic 360 # implementation, with the full transition matrix, takes a lot of 361 # memory with this model (and it's not necessary) 362
363 - def get_schema_transition_matrix(self, transpose=False):
364 """ Transition matrix just for the schema distribution. """ 365 if transpose: 366 if self._schema_transition_matrix_transpose_cache is None: 367 # Ensure that the normal transition matrix has been generated 368 mat = self.get_schema_transition_matrix() 369 # Tranpose it and copy it, so it's a real array, not a view 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 # Compute the matrix from scratch, as we've not done it yet 376 N = len(self.schemata) 377 shape = tuple([N]*self.order) 378 trans = numpy.zeros(shape, numpy.float64) 379 # Build a list of lists, each n long, with all possible array indices 380 index_sets = _all_indices(self.order, N) 381 # Fill the matrix 382 for indices in index_sets: 383 # Get the ngram of state labels for this ngram of array indices 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 # Compute the matrix from scratch, as we've not done it yet 394 N = len(self.schemata) 395 trans = numpy.zeros((12,12,N), numpy.float64) 396 # Unigram case: all equiprobable 397 if self.order == 1: 398 trans[:,:,:] = 1.0/12 399 else: 400 # Fill the matrix 401 for i,schema in enumerate(self.schemata): 402 for root_change in range(12): 403 # Get the probability for this root *change* 404 root_prob = self.root_transition_dist[schema].prob(root_change) 405 # Fill this in for each pair of roots that have this difference 406 roots = [(last_root+root_change)%12 for last_root in range(12)] 407 last_roots = range(12) 408 # Select all pairs of roots with this difference and 409 # set the probability 410 trans[roots,last_roots,i] = root_prob 411 self._root_transition_matrix_cache = trans 412 return self._root_transition_matrix_cache
413
414 - def get_schema_emission_matrix(self, sequence):
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 # Set the probability for every timestep... 424 for t,emission in enumerate(sequence): 425 # ...given each state 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
431 - def normal_forward_probabilities(self, sequence, root_schema=False):
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 # Prepare the transition and emission matrices 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 # We can do this quickly for unigrams 453 # We only use emission probabilities: transition probs 454 # just supply (unconditioned) priors 455 # We can ignore the root transitions: all equiprobable 456 forward_matrix = sc_trans*schema_ems 457 # Normalize 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 # Initialize an empty matrix 465 forward_matrix = numpy.zeros([N,12]+[len(schemata)]*(self.order-1), numpy.float64) 466 467 # First fill in the first columns with histories padded with Nones 468 # In these columns, we only use a subset of the dimensions 469 # Simplest to do this exhaustively over all states 470 for time in range(self.order-1): 471 # Get the indices corresponding to all possible sub-n-grams 472 # between the start and here 473 index_sets = _all_indices(time+1, len(schemata)) 474 475 for indices in index_sets: 476 # Get the actual ngram for these indices 477 ngram = [schemata[i] for i in indices] 478 # Pad with Nones to a length of n 479 ngram.extend([None]*(self.order-time-1)) 480 # Pad the indices with 0s so we use all the dimensions 481 indices.extend([0]*(self.order-time-2)) 482 # Probability of this schema transition 483 schema_prob = 2**self.schema_transition_log_probability_schemata(*ngram) 484 485 if time == 0: 486 # All roots equiprobable 487 selector = tuple([time,slice(None)]+indices) 488 forward_matrix[selector] += schema_prob 489 else: 490 # Multiply by the prob we came from, considering all possible roots 491 previous_states = forward_matrix[ 492 tuple([time-1,slice(None)] + indices[1:]+[0])] 493 # Multiply in the appropriate root transitions 494 # Sum over possible previous roots 495 root_probs = numpy.sum( 496 root_trans[:,:,indices[1]] * previous_states, 497 axis=1) 498 # root_probs is now a 1D array of probs for each possible 499 # root in the current state 500 selector = tuple([time,slice(None)]+indices) 501 forward_matrix[selector] += root_probs 502 503 # Multiply in the emission probabilities 504 forward_matrix[time] = (forward_matrix[time].transpose() * schema_ems[time].transpose()).transpose() 505 # Normalize 506 forward_matrix[time] /= numpy.sum(forward_matrix[time]) 507 508 for time in range(self.order-1, N): 509 # Multiplying, the previous timestep gets broadcast over the 510 # first axis of the transition matrix, which is what we need 511 # (that axis represents the most recent state in the n-gram) 512 trans_step = forward_matrix[time-1,:,numpy.newaxis] * sc_trans 513 # DIMS: (root[-1], schema, schema[-1], ..., schema[-n+1]) 514 # Dimension 0 currently represents the *previous* root 515 # Multiply in the root transition probabilities 516 trans_step = trans_step[numpy.newaxis].transpose() * root_trans[:,:,numpy.newaxis,:].transpose() 517 # DIMS: (schema[-n+1], ..., schema, root[-1], root) 518 # Note that we don't transpose back yet 519 # Sum probabilities over the last axis, i.e. the earliest schema 520 # in the n-gram 521 trans_step = numpy.sum(trans_step, axis=0) 522 # DIMS: (schema[-n+2], ..., schema, root[-1], root) 523 # Dimension 0 (i.e. -1) is now a new dim representing the *current* root 524 # Sum over previous roots, now dim 1 (i.e. -2) 525 trans_step = numpy.sum(trans_step, axis=-2) 526 # DIMS: (schema[-n+2], ..., schema, root) 527 # Multiply in the emission probabilities 528 # Now we transpose back 529 forward_matrix[time] = (trans_step * schema_ems[time].transpose()).transpose() 530 # DIMS: (root, schema, ..., schema[-n+2]) 531 # Normalize the timestep 532 forward_matrix[time] /= numpy.sum(forward_matrix[time]) 533 534 if not root_schema: 535 # Convert to a state matrix 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 # Get the index into the new-style matrix 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 # Set the value in the matrix we're creating 546 new_forward_matrix[tuple([time]+state_indices)] = forward_matrix[selector] 547 return new_forward_matrix 548 return forward_matrix
549
550 - def normal_backward_probabilities(self, sequence, root_schema=False):
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 # We can do this quickly for unigrams and it saves dealing with 566 # dodgy cases of everything below 567 # For unigrams, the backward probs are uniform 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 # Prepare the transition and emission matrices 575 schema_ems = self.get_schema_emission_matrix(sequence) 576 # We add an extra dim to the end of this to allow for the root axis 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 # For other cases, things are a little more complicated 582 # Initialize an empty matrix 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 # Initialize from the end 588 for indices in index_sets: 589 # Make an (n-1)-gram for the context 590 ngram = [schemata[i] for i in indices] 591 # Put the probability of going to the end state from each context 592 # Ignore the root transition: final state is only in the schema dist 593 # Note we store this in transposed form (see below) 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 # Work backwards, filling in the matrix 599 for time in range(N-2, self.order-2, -1): 600 # Transposing the next timestep and the trans matrix causes their 601 # indices to be reversed, so that the timestep is broadcast over 602 # the last index (now the first) 603 # Then we multiply in the emission probabilities, broadcast over 604 # the first index (by doing it while transposed) and transpose 605 # back to correct the indices 606 # Summing over the 0th axis sums over possible next states 607 # (the last element of the ngram in the next timestep) 608 # To speed up the computations, we keep the whole lot transposed 609 610 # Multiply in the emission probabilities 611 bwd = schema_ems[time+1].transpose() * backward_matrix[time+1] 612 # sc_trans_back has an extra axis at the end so it's broadcast 613 # over the root axis 614 bwd = sc_trans_back * bwd 615 # DIMS: (schema[-n+2], ..., schema[1], root[1]) 616 # Sum over next schemata: the root transition is not dependent on them 617 bwd = numpy.sum(bwd, axis=-2) 618 # DIMS: (schema[-n+2], ..., schema, root[1]) 619 # Add a new axis second from the end to accommodate this timestep's roots 620 bwd = (bwd.transpose()[:,numpy.newaxis]).transpose() 621 # DIMS: (schema[-n+2], ..., schema, (root), root[1]) 622 # The final dimension of this will be the *next* root 623 bwd = bwd * root_trans 624 # DIMS: (schema[-n+2], ..., schema, root, root[1]) 625 # Sum over next roots 626 bwd = numpy.sum(bwd, axis=-1) 627 # DIMS: (schema[-n+2], ..., schema, root) 628 backward_matrix[time] = bwd 629 # Normalize over the timestep 630 backward_matrix[time] /= numpy.sum(backward_matrix[time]) 631 632 # Fill in the first columns now, using None-padding histories 633 for time in range(self.order-2, -1, -1): 634 # Get the indices corresponding to all possible t-grams 635 id_tgrams = _all_indices(time+1, S) 636 637 for id_tgram in id_tgrams: 638 # Get the actual ngram for these indices 639 tgram = [schemata[i] for i in id_tgram] 640 # Fill this out to an (n-1)-gram stretching before the 641 # start with Nones 642 nm1gram = tgram + [None]*(self.order-2-time) 643 # Pad indices with 0s 644 id_nm1gram = id_tgram + [0]*(self.order-2-time) 645 646 # Sum over the possible next states following this (n-1)-gram 647 summed_prob = 0.0 648 for next_id in range(S): 649 # Get schema transition probability from nm1gram to next_id 650 schema_prob = 2**self.schema_transition_log_probability_schemata(schemata[next_id], *nm1gram) 651 # Consider each possible next root 652 for next_root in range(12): 653 # Multiply in the emission probability for the next state 654 prob = schema_prob * schema_ems[time+1, next_root, next_id] 655 656 # Multiply by the bwd prob for the next state 657 next_state = tuple([time+1]+list(reversed(id_nm1gram[:-1]))+[next_id,next_root]) 658 prob *= backward_matrix[next_state] 659 660 # Consider all possible current roots 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 # All roots equiprobable 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 # Normalize 674 backward_matrix[time] /= numpy.sum(backward_matrix[time]) 675 676 # Now transpose all the timestep matrices back so the ngrams 677 # correspond correctly to the indices 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 # Convert to a state matrix 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 # Get the index into the new-style matrix 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 # Set the value in the matrix we're creating 694 new_backward_matrix[tuple([time]+state_indices)] = reordered_backward_matrix[selector] 695 return new_backward_matrix 696 return reordered_backward_matrix
697
698 - def gamma_probabilities(self, sequence, dictionary=False, 699 forward=None, backward=None):
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 # Don't use normal_forward_backward_probabilities: just get the 713 # root_schema version of the matrices 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 # Sum over all but the first three dimensions: time and last root 718 # and schema in ngram 719 for i in range(gamma.ndim-3): 720 gamma = numpy.sum(gamma, axis=-1) 721 722 (T,R,S) = gamma.shape 723 # Renormalize 724 for t in range(T): 725 gamma[t] /= numpy.sum(gamma[t]) 726 727 if dictionary: 728 # Convert to a list of dictionaries, keyed by label 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 # Convert to a matrix of state probabilities 739 new_gamma = numpy.copy(gamma.reshape(T,R*S)) 740 return new_gamma
741
742 - def compute_gamma(self, *args, **kwargs):
743 """ Alias for backward compatibility. Use C{gamma_probabilities} """ 744 return self.gamma_probabilities(*args, **kwargs)
745 746 747 ################## Storage ####################
748 - def to_picklable_dict(self):
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
770 - def from_picklable_dict(cls, data):
771 """ 772 Reproduces an model that was converted to a picklable 773 form using to_picklable_dict. 774 775 """ 776 from jazzparser.utils.nltk.storage import dict_to_object 777 778 if data['backoff_model'] is not None: 779 backoff_model = cls.from_picklable_dict(data['backoff_model']) 780 else: 781 backoff_model = None 782 783 return cls(data['order'], 784 dict_to_object(data['root_transition_counts']), 785 dict_to_object(data['schema_transition_counts']), 786 dict_to_object(data['emission_counts']), 787 data['estimator'], 788 backoff_model, 789 data['schemata'], 790 data['chord_vocab'], 791 history=data.get('history', ''))
792
793 794 795 -def lattice_to_emissions(lattice, chord_map=None):
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