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
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
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
90
91
92
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
99
100
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
104
105 gamma = last_model.compute_gamma(sequence, forward=alpha, backward=beta)
106
107 xi = last_model.compute_xi(sequence, forward=alpha, backward=beta)
108
109
110 if update_initial:
111 for state in label_dom:
112 schema, root, chord_class = state
113 schema_i = schema_ids[schema]
114
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
125
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
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
139 schema_trans[schema_i][num_schemata] += gamma[T-1][state_i]
140
141
142
143
144 for pc,beat in sequence[time]:
145
146 rel_pc = (pc - root) % 12
147 ems[emission_cond_ids[(chord_class,beat)]][rel_pc] += \
148 gamma[time][state_i]
149
150
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
155 sinit_denom = array_sum(sinit)
156
157
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
167
168
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
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
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
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
256
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
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
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
288
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
306
307
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
318
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
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
330
331
332 if split_length is not None:
333 logger.info("Splitting sequences into max %d-sized chunks" % \
334 split_length)
335 split_emissions = []
336
337 for emstream in emissions:
338 input_ems = emstream
339 splits = []
340 first = True
341
342 while len(input_ems) >= split_length:
343
344 splits.append((first, input_ems[:split_length]))
345 input_ems = input_ems[split_length-1:]
346 first = False
347
348 if len(input_ems):
349 splits.append((first, input_ems))
350 split_emissions.extend(splits)
351 else:
352
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
359
360
361 model.train_emission_number_distribution(emissions)
362 logger.info("Trained emission number distribution")
363
364 if save_intermediate:
365 _save()
366
367
368
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
377
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
389
390
391
392
393
394 schema_trans = zeros((num_schemata,num_schemata+1), float64)
395 root_trans = zeros((num_schemata,num_schemata,num_root_changes), float64)
396
397
398
399
400
401 ems = zeros((num_emission_conds,num_emissions), float64)
402
403 sinit = zeros(num_schemata, float64)
404
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
409
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
421 logger.warning("Child process was cancelled")
422 return
423
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
430
431
432
433
434
435 array_add(ems, ems_local, ems)
436
437 array_add(schema_trans, schema_trans_local, schema_trans)
438 array_add(root_trans, root_trans_local, root_trans)
439
440 array_add(sinit, sinit_local, sinit)
441
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
447
448
449 if processes > 1:
450
451 logger.info("Creating a pool of %d processes" % processes)
452
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
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
475 pool.join()
476 except KeyboardInterrupt:
477
478 logger.info("Keyboard interrupt was received during EM "\
479 "updates")
480 raise
481
482
483
484 for res in async_results:
485
486
487 res_tuple = res.get()
488
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
508 current_logprob += updates[-1]
509
510
511
512
513
514
515 for cond_tup in model.emission_dist.conditions():
516 cond_id = emission_cond_ids[cond_tup]
517
518 denom = ems_denom[cond_id]
519
520 for pc in range(12):
521
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
527 num_trans_samples = len(self.model.schemata)
528
529 for schema in self.model.schemata:
530 schema_i = schema_ids[schema]
531 schema_denom = schema_trans_denom[schema_i]
532
533
534 for next_schema in self.model.schemata:
535 schema_j = schema_ids[next_schema]
536
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
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
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
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
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
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
580
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
587 if iteration > 0 and \
588 abs(current_logprob - last_logprob) < convergence_logprob:
589
590 logger.info("Distribution has converged: ceasing training")
591 break
592
593 iteration += 1
594 last_logprob = current_logprob
595
596
597 self.update_model(model)
598
599
600 if save_intermediate:
601 _save()
602 except KeyboardInterrupt:
603
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
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
619 self.update_model(model)
620
621 _save()
622 return
623
640