Package jazzparser :: Package taggers :: Package segmidi :: Package chordclass :: Module train
[hide private]
[frames] | no frames]

Source Code for Module jazzparser.taggers.segmidi.chordclass.train

  1  """Unsupervised EM training for chordclass HMM tagging model. 
  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  import numpy, os, signal 
 29  from numpy import ones, float64, sum as array_sum, zeros, log2, \ 
 30                          add as array_add, subtract as array_subtract 
 31  import cPickle as pickle 
 32  from multiprocessing import Pool 
 33   
 34  from jazzparser.utils.nltk.probability import mle_estimator, logprob, add_logs, \ 
 35                          sum_logs, prob_dist_to_dictionary_prob_dist, \ 
 36                          cond_prob_dist_to_dictionary_cond_prob_dist 
 37  from jazzparser.utils.options import ModuleOption 
 38  from jazzparser.utils.system import get_host_info_string 
 39  from jazzparser.utils.strings import str_to_bool 
 40  from jazzparser import settings 
 41  from jazzparser.taggers.segmidi.chordclass.hmm import ChordClassHmm 
 42  from jazzparser.taggers.segmidi.midi import midi_to_emission_stream 
 43   
 44  from nltk.probability import ConditionalProbDist, FreqDist, \ 
 45              ConditionalFreqDist, DictionaryProbDist, \ 
 46              DictionaryConditionalProbDist, MutableProbDist 
 47   
 48  # Small quantity added to every probability to ensure we never get zeros 
 49  ADD_SMALL = 1e-10 
 50   
51 -def _sequence_updates(sequence, last_model, label_dom, schema_ids, 52 emission_cond_ids, update_initial=True, catch_interrupt=False):
53 """ 54 Evaluates the forward/backward probability matrices for a 55 single sequence under the model that came from the previous 56 iteration and returns matrices that contain the updates 57 to be made to the distributions during this iteration. 58 59 This is wrapped up in a function so it can be run in 60 parallel for each sequence. Once all sequences have been 61 evaluated, the results are combined and model updated. 62 63 @type update_initial: bool 64 @param update_initial: usually you want to update all distributions, 65 including the initial state distribution. If update_initial=False, 66 the initial state distribution updates won't be made for this sequence. 67 We want this when the sequence is actually a non-initial fragment of 68 a longer sequence 69 @type catch_interrupt: bool 70 @param catch_interrupt: catch KeyboardInterrupt exceptions and return 71 None. This is useful behaviour when calling this in a process pool, 72 since it allows the parent process to handle the interrupt, but should 73 be set to False (default) if calling directly. 74 75 """ 76 try: 77 # Get the sizes we'll need for the matrix 78 num_schemata = len(last_model.schemata) 79 num_root_changes = 12 80 num_chord_classes = len(last_model.chord_classes) 81 num_emission_conds = len(emission_cond_ids) 82 num_emissions = 12 83 84 T = len(sequence) 85 86 state_ids = dict([(state,id) for (id,state) in \ 87 enumerate(last_model.label_dom)]) 88 89 # Local versions of the matrices store the accumulated values 90 # for just this sequence (so we can normalize before adding 91 # to the global matrices) 92 # The numerators 93 schema_trans = zeros((num_schemata,num_schemata+1), float64) 94 root_trans = zeros((num_schemata,num_schemata,num_root_changes), float64) 95 ems = zeros((num_emission_conds,num_emissions), float64) 96 sinit = zeros(num_schemata, float64) 97 98 # Compute the forward and backward probabilities 99 # These are normalized, but that makes no difference to the outcome of 100 # compute_gamma and compute_xi 101 alpha,scale,seq_logprob = last_model.normal_forward_probabilities(sequence, array=True) 102 beta,scale = last_model.normal_backward_probabilities(sequence, array=True) 103 # gamma contains the state occupation probability for each state at each 104 # timestep 105 gamma = last_model.compute_gamma(sequence, forward=alpha, backward=beta) 106 # xi contains the probability of every state transition at every timestep 107 xi = last_model.compute_xi(sequence, forward=alpha, backward=beta) 108 109 # Update the initial state distribution if requested 110 if update_initial: 111 for state in label_dom: 112 schema, root, chord_class = state 113 schema_i = schema_ids[schema] 114 # Add this contribution to the sum of the states with this schema 115 sinit[schema_i] += gamma[0][state_ids[state]] 116 117 for time in range(T): 118 for state in label_dom: 119 schema, root, chord_class = state 120 schema_i = schema_ids[schema] 121 state_i = state_ids[state] 122 123 if time < T-1: 124 # Go through all possible pairs of states to update the 125 # transition distributions 126 for next_state in label_dom: 127 next_schema, next_root, next_chord_class = next_state 128 schema_j = schema_ids[next_schema] 129 state_j = state_ids[next_state] 130 131 ## Transition dist update ## 132 root_change = (next_root - root) % 12 133 schema_trans[schema_i][schema_j] += \ 134 xi[time][state_i][state_j] 135 root_trans[schema_i][schema_j][root_change] += \ 136 xi[time][state_i][state_j] 137 else: 138 # Final state: update the probs of transitioning to end 139 schema_trans[schema_i][num_schemata] += gamma[T-1][state_i] 140 141 ## Emission dist update ## 142 # Add the state occupation probability to the emission numerator 143 # for every note 144 for pc,beat in sequence[time]: 145 # Take the pitch class relative to the root 146 rel_pc = (pc - root) % 12 147 ems[emission_cond_ids[(chord_class,beat)]][rel_pc] += \ 148 gamma[time][state_i] 149 150 # Calculate the denominators 151 schema_trans_denom = array_sum(schema_trans, axis=1) 152 root_trans_denom = array_sum(root_trans, axis=2) 153 ems_denom = array_sum(ems, axis=1) 154 # This should come to 1.0 155 sinit_denom = array_sum(sinit) 156 157 # Wrap this all up in a tuple to return to the master 158 return (schema_trans, root_trans, ems, sinit, \ 159 schema_trans_denom, root_trans_denom, ems_denom, sinit_denom, \ 160 seq_logprob) 161 except KeyboardInterrupt: 162 if catch_interrupt: 163 return 164 else: 165 raise
166 ## End of pool operation _sequence_updates 167 168
169 -class ChordClassBaumWelchTrainer(object):
170 """ 171 Class with methods to retrain a chordclass model using the Baum-Welch 172 EM algorithm. 173 174 Module options must be processed already - we do that in the 175 ChordClassTaggerModel, not here. 176 177 @todo: Inherit from the 178 L{jazzparser.utils.nltk.ngram.baumwelch.BaumWelchTrainer}. Currently, 179 the generic trainer duplicates a lot of this code, since it was based on 180 it. 181 182 """ 183 # These will be included in the training options 184 OPTIONS = [ 185 ModuleOption('max_iterations', filter=int, 186 help_text="Number of training iterations to give up after "\ 187 "if we don't reach convergence before.", 188 usage="max_iterations=N, where N is an integer", default=100), 189 ModuleOption('convergence_logprob', filter=float, 190 help_text="Difference in overall log probability of the "\ 191 "training data made by one iteration after which we "\ 192 "consider the training to have converged.", 193 usage="convergence_logprob=X, where X is a small floating "\ 194 "point number (e.g. 1e-3)", default=1e-3), 195 ModuleOption('split', filter=int, 196 help_text="Limits the length of inputs by splitting them into "\ 197 "fragments of at most this length. The initial state "\ 198 "distribution will only be updated for the initial fragments.", 199 usage="split=X, where X is an int"), 200 ModuleOption('truncate', filter=int, 201 help_text="Limits the length of inputs by truncating them to this "\ 202 "number of timesteps. Truncation is applied before splitting.", 203 usage="truncate=X, where X is an int"), 204 ModuleOption('save_intermediate', filter=str_to_bool, 205 help_text="Save the model between iterations", 206 usage="save_intermediate=B, where B is 'true' or 'false' "\ 207 "(default true)", 208 default=True), 209 ModuleOption('trainprocs', filter=int, 210 help_text="Number of processes to spawn during training. Use -1 "\ 211 "to spawn a process for every sequence.", 212 usage="trainprocs=P, where P is an integer", 213 default=1), 214 ] 215
216 - def __init__(self, model, options={}):
217 self.model = model 218 self.options = options
219
220 - def train(self, emissions, logger=None, save_callback=None):
221 """ 222 Performs unsupervised training using Baum-Welch EM. 223 224 This is performed on a model that has already been initialized. 225 You might, for example, create such a model using 226 L{jazzparser.taggers.segmidi.chordclass.hmm.ChordClassHmm.initialize_chord_classes}. 227 228 This is based on the training procedure in NLTK for HMMs: 229 C{nltk.tag.hmm.HiddenMarkovModelTrainer.train_unsupervised}. 230 231 @type emissions: L{jazzparser.data.input.MidiTaggerTrainingBulkInput} or 232 list of L{jazzparser.data.input.Input}s 233 @param emissions: training MIDI data 234 @type logger: logging.Logger 235 @param logger: a logger to send progress logging to 236 237 """ 238 if logger is None: 239 from jazzparser.utils.loggers import create_dummy_logger 240 logger = create_dummy_logger() 241 242 self.model.add_history("Beginning Baum-Welch training on %s" % get_host_info_string()) 243 self.model.add_history("Training on %d MIDI sequences (with %s segments)" % \ 244 (len(emissions), ", ".join("%d" % len(seq) for seq in emissions))) 245 logger.info("Beginning Baum-Welch training on %s" % get_host_info_string()) 246 247 # Get some options out of the module options 248 max_iterations = self.options['max_iterations'] 249 convergence_logprob = self.options['convergence_logprob'] 250 split_length = self.options['split'] 251 truncate_length = self.options['truncate'] 252 save_intermediate = self.options['save_intermediate'] 253 processes = self.options['trainprocs'] 254 255 # Make a mutable distribution for each of the distributions 256 # we'll be updating 257 emission_mdist = cond_prob_dist_to_dictionary_cond_prob_dist( 258 self.model.emission_dist, mutable=True) 259 schema_trans_mdist = cond_prob_dist_to_dictionary_cond_prob_dist( 260 self.model.schema_transition_dist, mutable=True) 261 root_trans_mdist = cond_prob_dist_to_dictionary_cond_prob_dist( 262 self.model.root_transition_dist, mutable=True) 263 init_state_mdist = prob_dist_to_dictionary_prob_dist( 264 self.model.initial_state_dist, mutable=True) 265 266 # Get the sizes we'll need for the matrices 267 num_schemata = len(self.model.schemata) 268 num_root_changes = 12 269 num_chord_classes = len(self.model.chord_classes) 270 if self.model.metric: 271 num_emission_conds = num_chord_classes * 4 272 else: 273 num_emission_conds = num_chord_classes 274 num_emissions = 12 275 276 # Enumerations to use for the matrices, so we know what they mean 277 schema_ids = dict([(sch,i) for (i,sch) in enumerate(self.model.schemata+[None])]) 278 if self.model.metric: 279 rs = range(4) 280 else: 281 rs = [0] 282 emission_cond_ids = dict([(cc,i) for (i,cc) in enumerate(\ 283 sum([[ 284 (str(cclass.name),r) for r in rs] for cclass in self.model.chord_classes], 285 []))]) 286 287 # Construct a model using these mutable distributions so we can 288 # evaluate using them 289 model = ChordClassHmm(schema_trans_mdist, 290 root_trans_mdist, 291 emission_mdist, 292 self.model.emission_number_dist, 293 init_state_mdist, 294 self.model.schemata, 295 self.model.chord_class_mapping, 296 self.model.chord_classes, 297 metric=self.model.metric, 298 illegal_transitions=self.model.illegal_transitions, 299 fixed_root_transitions=self.model.fixed_root_transitions) 300 301 def _save(): 302 if save_callback is None: 303 logger.error("Could not save model, as no callback was given") 304 else: 305 # If the writing fails, wait till I've had a chance to sort it 306 # out and then try again. This happens when my AFS token runs 307 # out 308 while True: 309 try: 310 save_callback() 311 except (IOError, OSError), err: 312 print "Error writing model to disk: %s. " % err 313 raw_input("Press <enter> to try again... ") 314 else: 315 break
316 317 ########## Data preprocessing 318 # Preprocess the inputs so they're ready for the model training 319 emissions = [midi_to_emission_stream(seq, 320 metric=self.model.metric, 321 remove_empty=False)[0] \ 322 for seq in emissions] 323 logger.info("%d input sequences" % len(emissions)) 324 # Truncate long streams 325 if truncate_length is not None: 326 logger.info("Truncating sequences to max %d timesteps" % \ 327 truncate_length) 328 emissions = [stream[:truncate_length] for stream in emissions] 329 # Split up long streams if requested 330 # After this, each stream is a tuple (first,stream), where first 331 # indicates whether the stream segment begins a song 332 if split_length is not None: 333 logger.info("Splitting sequences into max %d-sized chunks" % \ 334 split_length) 335 split_emissions = [] 336 # Split each stream 337 for emstream in emissions: 338 input_ems = emstream 339 splits = [] 340 first = True 341 # Take bits of length split_length until we're under the max 342 while len(input_ems) >= split_length: 343 # Overlap the splits by one so we get all transitions 344 splits.append((first, input_ems[:split_length])) 345 input_ems = input_ems[split_length-1:] 346 first = False 347 # Get the last short one 348 if len(input_ems): 349 splits.append((first, input_ems)) 350 split_emissions.extend(splits) 351 else: 352 # All streams begin a song 353 split_emissions = [(True,stream) for stream in emissions] 354 logger.info("Sequence lengths after preprocessing: %s" % 355 " ".join([str(len(em[1])) for em in split_emissions])) 356 ########## 357 358 # Train the emission number distribution on this data to start with 359 # This doesn't get updated by the iterative steps, because it's not 360 # dependent on chord classes 361 model.train_emission_number_distribution(emissions) 362 logger.info("Trained emission number distribution") 363 # Save the model with this 364 if save_intermediate: 365 _save() 366 367 ############### 368 # TODO: remove this section - it's for debugging 369 if False: 370 from jazzparser.prototype.baumwelch import TempBaumWelchTrainer 371 temptrainer = TempBaumWelchTrainer(model, self.options) 372 temptrainer.train(split_emissions, logger=logger) 373 return 374 ############### 375 376 # Special case of -1 for number of sequences 377 # No point in creating more processes than there are sequences 378 if processes == -1 or processes > len(split_emissions): 379 processes = len(split_emissions) 380 381 iteration = 0 382 last_logprob = None 383 try: 384 while iteration < max_iterations: 385 logger.info("Beginning iteration %d" % iteration) 386 current_logprob = 0.0 387 388 ### Matrices in which to accumulate new probability estimates 389 # trans contains new transition numerator probabilities 390 # TODO: update this... 391 # trans[s][s'][dr] = Sum_{t_n=t_(n+1), m_n=m_(n+1),c_n=c,c_(n+1)=c'} 392 # alpha(x_n).beta(x_(n+1)). 393 # p(x_(n+1)|x_n).p(y_(n+1)|x_(n+1)) 394 schema_trans = zeros((num_schemata,num_schemata+1), float64) 395 root_trans = zeros((num_schemata,num_schemata,num_root_changes), float64) 396 # ems contains the new emission numerator probabilities 397 # TODO: update this... 398 # ems[r][d] = Sum_{d(y_n^k, x_n)=d, r_n^k=r} 399 # alpha(x_n).beta(x_n) / 400 # Sum_{x'_n} (alpha(x'_n).beta(x'_n)) 401 ems = zeros((num_emission_conds,num_emissions), float64) 402 # sinit contains the initial state numerator probabilities 403 sinit = zeros(num_schemata, float64) 404 # And these are the denominators 405 schema_trans_denom = zeros(num_schemata, float64) 406 root_trans_denom = zeros((num_schemata,num_schemata), float64) 407 ems_denom = zeros(num_emission_conds, float64) 408 # It may seem silly to use a matrix for this, but it allows 409 # us to update it in the callback 410 sinit_denom = zeros(1, float64) 411 412 def _training_callback(result): 413 """ 414 Callback for the _sequence_updates processes that takes 415 the updates from a single sequence and adds them onto 416 the global update accumulators. 417 418 """ 419 if result is None: 420 # Process cancelled: do no updates 421 logger.warning("Child process was cancelled") 422 return 423 # _sequence_updates() returns all of this as a tuple 424 (schema_trans_local, root_trans_local, ems_local, sinit_local, \ 425 schema_trans_denom_local, root_trans_denom_local, \ 426 ems_denom_local, sinit_denom_local, \ 427 seq_logprob) = result 428 429 # Add these probabilities from this sequence to the 430 # global matrices 431 # We don't need to scale these using the seq prob because 432 # they're already normalized 433 434 # Emission numerator 435 array_add(ems, ems_local, ems) 436 # Transition numerator 437 array_add(schema_trans, schema_trans_local, schema_trans) 438 array_add(root_trans, root_trans_local, root_trans) 439 # Initial state numerator 440 array_add(sinit, sinit_local, sinit) 441 # Denominators 442 array_add(ems_denom, ems_denom_local, ems_denom) 443 array_add(schema_trans_denom, schema_trans_denom_local, schema_trans_denom) 444 array_add(root_trans_denom, root_trans_denom_local, root_trans_denom) 445 array_add(sinit_denom, sinit_denom_local, sinit_denom)
446 ## End of _training_callback 447 448 # Only use a process pool if there's more than one sequence 449 if processes > 1: 450 # Create a process pool to use for training 451 logger.info("Creating a pool of %d processes" % processes) 452 # catch them at this level 453 pool = Pool(processes=processes) 454 455 async_results = [] 456 try: 457 for seq_i,(first,sequence) in enumerate(split_emissions): 458 logger.info("Iteration %d, sequence %d" % (iteration, seq_i)) 459 T = len(sequence) 460 if T == 0: 461 continue 462 463 # Fire off a new call to the process pool for every sequence 464 async_results.append( 465 pool.apply_async(_sequence_updates, 466 (sequence, model, 467 self.model.label_dom, 468 schema_ids, 469 emission_cond_ids), 470 { 'update_initial' : first, 471 'catch_interrupt' : True }, 472 callback=_training_callback) ) 473 pool.close() 474 # Wait for all the workers to complete 475 pool.join() 476 except KeyboardInterrupt: 477 # If Ctl+C is fired during the processing, we exit here 478 logger.info("Keyboard interrupt was received during EM "\ 479 "updates") 480 raise 481 482 # Call get() on every AsyncResult so that any exceptions in 483 # workers get raised 484 for res in async_results: 485 # If there was an exception in _sequence_update, it 486 # will get raised here 487 res_tuple = res.get() 488 # Add this sequence's logprob into the total for all sequences 489 current_logprob += res_tuple[-1] 490 else: 491 if len(split_emissions) == 1: 492 logger.info("One sequence: not using a process pool") 493 else: 494 logger.info("Not using a process pool: training %d "\ 495 "emission sequences sequentially" % \ 496 len(split_emissions)) 497 498 for seq_i,(first,sequence) in enumerate(split_emissions): 499 if len(sequence) > 0: 500 logger.info("Iteration %d, sequence %d" % (iteration, seq_i)) 501 updates = _sequence_updates( 502 sequence, model, 503 self.model.label_dom, 504 schema_ids, emission_cond_ids, 505 update_initial=first) 506 _training_callback(updates) 507 # Update the overall logprob 508 current_logprob += updates[-1] 509 510 511 ######## Model updates 512 # Update the model's probabilities from the accumulated values 513 514 # Emission distribution 515 for cond_tup in model.emission_dist.conditions(): 516 cond_id = emission_cond_ids[cond_tup] 517 # Divide each numerator by the denominator 518 denom = ems_denom[cond_id] 519 520 for pc in range(12): 521 # Convert to log probs for update and divide by denom 522 prob = logprob(ems[cond_id][pc] + ADD_SMALL) - \ 523 logprob(denom + 12*ADD_SMALL) 524 model.emission_dist[cond_tup].update(pc, prob) 525 526 # Transition distribution 527 num_trans_samples = len(self.model.schemata) 528 # Dist conditioned on current schema 529 for schema in self.model.schemata: 530 schema_i = schema_ids[schema] 531 schema_denom = schema_trans_denom[schema_i] 532 533 # Observe next schema and change of root 534 for next_schema in self.model.schemata: 535 schema_j = schema_ids[next_schema] 536 # Convert to log probs for update and divide by denom 537 prob = \ 538 logprob(schema_trans[schema_i][schema_j] \ 539 + ADD_SMALL) - \ 540 logprob(schema_denom + (num_trans_samples+1)*ADD_SMALL) 541 model.schema_transition_dist[schema].update( 542 next_schema, prob) 543 544 root_denom = root_trans_denom[schema_i][schema_j] 545 546 for root_change in range(12): 547 # Convert to log probs for update and divide by denom 548 prob = \ 549 logprob(root_trans[schema_i][schema_j][root_change] \ 550 + ADD_SMALL) - \ 551 logprob(root_denom + 12*ADD_SMALL) 552 model.root_transition_dist[(schema,next_schema)].update( 553 root_change, prob) 554 555 # Also transition to the final state 556 prob = \ 557 logprob(schema_trans[schema_i][num_schemata] \ 558 + ADD_SMALL) - \ 559 logprob(schema_denom + (num_trans_samples+1)*ADD_SMALL) 560 model.schema_transition_dist[schema].update(None, prob) 561 562 # Initial state distribution 563 denom = sinit_denom[0] 564 num_samples = len(self.model.schemata) 565 for schema in self.model.schemata: 566 schema_i = schema_ids[schema] 567 568 # Convert to log probs for update and divide by denom 569 prob = \ 570 logprob(sinit[schema_i] + ADD_SMALL) - \ 571 logprob(denom + num_samples*ADD_SMALL) 572 model.initial_state_dist.update(schema, prob) 573 574 # Clear the model's cache so we get the new probabilities 575 model.clear_cache() 576 577 logger.info("Training data log prob: %s" % current_logprob) 578 if last_logprob is not None and current_logprob < last_logprob: 579 # Drop in log probability 580 # This should never happen if all's working correctly 581 logger.error("Log probability dropped by %s" % \ 582 (last_logprob - current_logprob)) 583 if last_logprob is not None: 584 logger.info("Log prob change: %s" % \ 585 (current_logprob - last_logprob)) 586 # Check whether the log probability has converged 587 if iteration > 0 and \ 588 abs(current_logprob - last_logprob) < convergence_logprob: 589 # Don't iterate any more 590 logger.info("Distribution has converged: ceasing training") 591 break 592 593 iteration += 1 594 last_logprob = current_logprob 595 596 # Update the main model 597 self.update_model(model) 598 599 # Only save if we've been asked to save between iterations 600 if save_intermediate: 601 _save() 602 except KeyboardInterrupt: 603 # Interrupted during training 604 self.model.add_history("Baum-Welch training interrupted after %d "\ 605 "iterations" % iteration) 606 logger.warn("Baum-Welch training interrupted") 607 raise 608 except Exception, err: 609 # Some other error during training 610 self.model.add_history("Error during Baum-Welch training. Exiting "\ 611 "after %d iterations" % iteration) 612 logger.error("Error during training: %s" % err) 613 raise 614 615 self.model.add_history("Completed Baum-Welch training (%d iterations)" \ 616 % iteration) 617 logger.info("Completed Baum-Welch training (%d iterations)" % iteration) 618 # Update the distribution's parameters with those we've trained 619 self.update_model(model) 620 # Always save the model now that we're done 621 _save() 622 return 623
624 - def update_model(self, model):
625 """ 626 Replaces the distributions of the saved model with those of the given 627 model and saves it. 628 629 """ 630 # Replicate the distributions of the source model so that we get 631 # non-mutable distributions to store 632 self.model.schema_transition_dist = \ 633 cond_prob_dist_to_dictionary_cond_prob_dist(model.schema_transition_dist) 634 self.model.root_transition_dist = \ 635 cond_prob_dist_to_dictionary_cond_prob_dist(model.root_transition_dist) 636 self.model.emission_dist = \ 637 cond_prob_dist_to_dictionary_cond_prob_dist(model.emission_dist) 638 self.model.initial_state_dist = prob_dist_to_dictionary_prob_dist( 639 model.initial_state_dist)
640