def emission_log_probability(self, emission, state): """ Gives the probability P(emission | label). Returned as a base 2 log. The emission should be a pair of (root,label), together defining a chord. There's a special case of this. If the emission is a list, it's assumed to be a I{distribution} over emissions. The list should contain (prob,em) pairs, where I{em} is an emission, such as is normally passed into this function, and I{prob} is the weight to give to this possible emission. The probabilities of the possible emissions are summed up, weighted by the I{prob} values. """ if type(emission) is list: # Average probability over the possible emissions probs = [] for (prob, em) in emission: probs.append(logprob(prob) + \ self.emission_log_probability(em, state)) return sum_logs(probs) # Single chord label point, function = state chord_root, label = emission X, Y, x, y = point # Work out the chord substitution subst = (chord_root - coordinate_to_et_2d((x, y))) % 12 # Generate the substitution given the chord function subst_prob = self.subst_emission_dist[function].logprob(subst) # Generate the label given the subst and chord function label_prob = self.type_emission_dist[(subst, function)].logprob(label) return subst_prob + label_prob
def emission_log_probability(self, emission, state): """ Gives the probability P(emission | label). Returned as a base 2 log. The emission should be a pair of (root,label), together defining a chord. There's a special case of this. If the emission is a list, it's assumed to be a I{distribution} over emissions. The list should contain (prob,em) pairs, where I{em} is an emission, such as is normally passed into this function, and I{prob} is the weight to give to this possible emission. The probabilities of the possible emissions are summed up, weighted by the I{prob} values. """ if type(emission) is list: # Average probability over the possible emissions probs = [] for (prob,em) in emission: probs.append(logprob(prob) + \ self.emission_log_probability(em, state)) return sum_logs(probs) # Single chord label point,function = state chord_root,label = emission X,Y,x,y = point # Work out the chord substitution subst = (chord_root - coordinate_to_et_2d((x,y))) % 12 # Generate the substitution given the chord function subst_prob = self.subst_emission_dist[function].logprob(subst) # Generate the label given the subst and chord function label_prob = self.type_emission_dist[(subst,function)].logprob(label) return subst_prob + label_prob
def path_to_tones(path, tempo=120, chord_types=None, root_octave=0, double_root=False, equal_temperament=False, timings=False): """ Takes a tonal space path, given as a list of coordinates, and generates the tones of the roots. @type path: list of (3d-coordinate,length) tuples @param path: coordinates of the points in the sequence and the length of each, in beats @type tempo: int @param tempo: speed in beats per second (Maelzel's metronome) @type chord_types: list of (string,length) @param chord_types: the type of chord to use for each tone and the time spent on that chord type, in beats. See L{CHORD_TYPES} keys for possible values. @type equal_temperament: bool @param equal_temperament: render all the pitches as they would be played in equal temperament. @rtype: L{ToneMatrix} @return: a tone matrix that can be used to render the sound """ # Use this envelope for all notes envelope = piano_envelope() sample_rate = DEFAULT_SAMPLE_RATE beat_length = 60.0 / tempo if timings: root_times = path else: # Work out when each root change occurs time = Fraction(0) root_times = [] for root,length in path: root_times.append((root,time)) time += length def _root_at_time(time): current_root = root_times[0][0] for root,rtime in root_times[1:]: # Move through root until we get the first one that # occurs after the previous time if rtime > time: return current_root current_root = root # If we're beyond the time of the last root, use that one return current_root if chord_types is None: # Default to just pure tones chord_types = [('prime',length) for __,length in path] if equal_temperament: _pitch_ratio = tonal_space_et_pitch else: _pitch_ratio = tonal_space_pitch_2d # Build the tone matrix by adding the tones one by one matrix = ToneMatrix(sample_rate=sample_rate) time = Fraction(0) for ctype,length in chord_types: coord = _root_at_time(time) pitch_ratio = _pitch_ratio(coord) duration = beat_length * float(length) # We want all enharmonic equivs of I to come out close to I, # not an octave above if not equal_temperament and coordinate_to_et_2d(coord) == 0 \ and pitch_ratio > 1.5: pitch_ratio /= 2.0 # Use a sine tone for each note tone = SineChordEvent(220*pitch_ratio, chord_type=ctype, duration=duration, envelope=envelope, root_octave=root_octave, root_weight=1.2, double_root=double_root) matrix.add_tone(beat_length * float(time), tone) time += length return matrix
class HmmPathNgram(NgramModel): """ An ngram model that takes multiple chords (weighted by probability) as input to its decoding. It is trained on labeled data. This is similar to L{jazzparser.taggers.ngram_multi.model.MultiChordNgramModel}, but the states represent points on a TS path, rather than categories. """ def __init__(self, order, point_transition_counts, fn_transition_counts, type_emission_counts, subst_emission_counts, estimator, backoff_model, chord_map, vector_dom, point_dom, history=""): self.order = order self.backoff_model = backoff_model chord_vocab = list(set(chord_map.keys())) self.chord_vocab = chord_vocab internal_chord_vocab = list(set(chord_map.values())) self.chord_map = chord_map self._estimator = estimator # Construct the domains by combining possible roots with # the other components of the labels self.vector_dom = vector_dom self.point_dom = point_dom self.label_dom = [(point,function) for point in point_dom \ for function in ["T","D","S"] ] self.num_labels = len(self.label_dom) self.emission_dom = [(root,label) for root in range(12) \ for label in internal_chord_vocab] self.num_emissions = len(self.emission_dom) # Keep hold of the freq dists self.point_transition_counts = point_transition_counts self.fn_transition_counts = fn_transition_counts self.type_emission_counts = type_emission_counts self.subst_emission_counts = subst_emission_counts # Make some prob dists self.point_transition_dist = ConditionalProbDist( point_transition_counts, estimator, len(vector_dom)) self.fn_transition_dist = ConditionalProbDist( fn_transition_counts, estimator, 4) # Includes final state self.type_emission_dist = ConditionalProbDist( type_emission_counts, estimator, len(internal_chord_vocab)) self.subst_emission_dist = ConditionalProbDist(subst_emission_counts, estimator, 12) # Store a string with information about training, etc self.history = history # Initialize the various caches # These will be filled as we access probabilities self.clear_cache() def add_history(self, string): """ Adds a line to the end of this model's history string. """ self.history += "%s: %s\n" % (datetime.now().isoformat(' '), string) @staticmethod def train(data, estimator, grammar, cutoff=0, logger=None, chord_map=None, order=2, backoff_orders=0, backoff_kwargs={}): """ Initializes and trains an HMM in a supervised fashion using the given training data. Training data should be chord sequence data (input type C{bulk-db} or C{bulk-db-annotated}). """ # Prepare a dummy logger if none was given if logger is None: logger = create_dummy_logger() logger.info(">>> Beginning training of ngram backoff model") training_data = [] # Generate the gold standard data by parsing the annotations for dbinput in data: # Get a gold standard tonal space sequence try: parses = parse_sequence_with_annotations(dbinput, grammar, \ allow_subparses=False) except ParseError, err: # Just skip this sequence logger.error('Could not get a GS parse of %s: %s' % (dbinput, err)) continue # There should only be one of these now parse = parses[0] if parse is None: logger.error('Could not get a GS parse of %s' % (dbinput)) continue # Get the form of the analysis we need for the training if chord_map is None: chords = [(c.root, c.type) for c in dbinput.chords] else: chords = [(c.root, chord_map[c.type]) for c in dbinput.chords] points, times = zip( *grammar.formalism.semantics_to_coordinates(parse.semantics)) # Run through the sequence, transforming absolute points into # the condensed relative representation ec0 = EnharmonicCoordinate.from_harmonic_coord(points[0]) # The first point is relative to the origin and always in the # (0,0) enharmonic space rel_points = [(0, 0, ec0.x, ec0.y)] for point in points[1:]: ec1 = EnharmonicCoordinate.from_harmonic_coord(point) # Find the nearest enharmonic instance of this point to the last nearest = ec0.nearest((ec1.x, ec1.y)) # Work out how much we have to shift this by to get the point dX = ec1.X - nearest.X dY = ec1.Y - nearest.Y rel_points.append((dX, dY, ec1.x, ec1.y)) ec0 = ec1 funs, times = zip( *grammar.formalism.semantics_to_functions(parse.semantics)) ### Synchronize the chords with the points and functions # We may need to repeat chords to match up with analysis # points that span multiple chords analysis = iter(zip(rel_points, funs, times)) rel_point, fun, __ = analysis.next() next_rel_point, next_fun, next_anal_time = analysis.next() # Keep track of how much time has elapsed time = 0 training_seq = [] reached_end = False for crd_pair, chord in zip(chords, dbinput.chords): if time >= next_anal_time and not reached_end: # Move on to the next analysis point rel_point, fun = next_rel_point, next_fun try: next_rel_point, next_fun, next_anal_time = analysis.next( ) except StopIteration: # No more points: keep using the same to the end reached_end = True training_seq.append((crd_pair, (rel_point, fun))) time += chord.duration training_data.append(training_seq) # Create some empty freq dists subst_emission_counts = CutoffConditionalFreqDist(cutoff=cutoff) type_emission_counts = CutoffConditionalFreqDist(cutoff=cutoff) point_transition_counts = CutoffConditionalFreqDist(cutoff=cutoff) fn_transition_counts = CutoffConditionalFreqDist(cutoff=cutoff) seen_vectors = [] seen_points = set() seen_XY = set() # Count all the stats from the training data for seq in training_data: # Keep track of the necessary history context history = [] for (chord, (point, fun)) in seq: ### Counts for the emission distribution chord_root, label = chord # Work out the chord substitution X, Y, x, y = point subst = (chord_root - coordinate_to_et_2d((x, y))) % 12 # Increment the counts subst_emission_counts[fun].inc(subst) type_emission_counts[(subst, fun)].inc(label) seen_points.add(point) seen_XY.add((X, Y)) if order > 1: ### Counts for the transition distribution # Update the history fifo history = [(point, fun)] + history[:order - 1] if len(history) > 1: points, functions = zip(*history) #~ # Get the vectors between the points #~ vectors = [vector(p0,p1) for (p1,p0) in \ #~ group_pairs([p for p in points if p is not None])] # The function is conditioned on all previous functions fn_context = tuple(functions[1:]) fn_transition_counts[fn_context].inc(functions[0]) #~ # The vector is conditioned on the function #~ # and the pairs of vector and function preceding that #~ point_context = tuple([functions[:2]] + #~ list(zip(vectors[1:], functions[2:]))) # The vector is conditioned on the function #~ point_transition_counts[point_context].inc(vectors[0]) vect = vector(points[1], points[0]) point_transition_counts[functions[0]].inc(vect) # Keep track of what vectors we've observed #~ seen_vectors.append(vectors[0]) seen_vectors.append(vect) else: # For the first point, we only count the function prob fn_transition_counts[tuple()].inc(fun) if order > 1: # Count the transition to the final state history = history[:order - 1] points, functions = zip(*history) fn_context = tuple(functions) fn_transition_counts[fn_context].inc(None) # Labels are (X,Y,x,y). We want all the points seen in the data # and all (0,0,x,y) for X, Y in seen_XY: for x in range(4): for y in range(3): seen_points.add((X, Y, x, y)) point_dom = list(seen_points) if backoff_orders > 0: # Default to using the same params for the backoff model kwargs = { 'cutoff': cutoff, 'chord_map': chord_map, } kwargs.update(backoff_kwargs) logger.info("Training backoff model") # Train a backoff model backoff = HmmPathNgram.train(data, estimator, grammar, logger=logger, order=order - 1, backoff_orders=backoff_orders - 1, backoff_kwargs=backoff_kwargs, **kwargs) else: backoff = None # Get a list of every vector in the training set vector_dom = list(set(seen_vectors)) return HmmPathNgram(order, point_transition_counts, fn_transition_counts, type_emission_counts, subst_emission_counts, estimator, backoff, chord_map, vector_dom, point_dom)