diff --git a/project.clj b/project.clj index aea52bc..d23cb78 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject hidden-markov-music "0.1.1-SNAPSHOT" +(defproject hidden-markov-music "0.1.1" :description "Generate original musical scores by means of a hidden Markov model." :url "https://github.com/dwysocki/hidden-markov-music" diff --git a/src/hidden_markov_music/core.clj b/src/hidden_markov_music/core.clj index 81ddbd1..4e89263 100644 --- a/src/hidden_markov_music/core.clj +++ b/src/hidden_markov_music/core.clj @@ -3,15 +3,75 @@ Currently no such interface exists." (:require [clojure.pprint :refer [pprint]] [hidden-markov-music.hmm :as hmm] + [hidden-markov-music.stats :as stats] [clojure.tools.cli :refer [parse-opts]] [clojure.string :as string]) + (:import [hidden_markov_music.hmm HMM]) (:gen-class)) +(def random-weather-model + (hmm/random-hmm [:rainy :sunny] + [:run :clean :shop])) + +(def song-states + [:beginning :middle :chorus :finale :end]) + +(def song-notes + [:A :B :C :D :E :F :G]) + +(def song-model + (HMM. song-states + song-notes + {:beginning {:beginning 0.8, + :middle 0.2, + :chorus 0.0, + :finale 0.0, + :end 0.0}, + :middle {:beginning 0.0, + :middle 0.5, + :chorus 0.4, + :finale 0.1, + :end 0.0}, + :chorus {:beginning 0.0, + :middle 0.2, + :chorus 0.8, + :finale 0.0, + :end 0.0} + :finale {:beginning 0.0, + :middle 0.0, + :chorus 0.0, + :finale 0.8, + :end 0.2} + :end {:beginning 0.0, + :middle 0.0, + :chorus 0.0, + :finale 0.0, + :end 1.0}} + (stats/random-row-stochastic-map song-states song-notes) + {:beginning 1.0, + :middle 0.0, + :chorus 0.0, + :finale 0.0, + :end 0.0})) + +(def models + {"weather" random-weather-model, + "song" song-model}) + + (def cli-options - [["-h" "--help"]]) + [["-m" "--model MODEL" "Name of the model" + :default "weather" + :validate [#{"weather" "song"} + (str "Must be one of [weather, song]")]] + ["-n" "--number N" "Number of weather events to sample" + :default 10 + :parse-fn #(Integer/parseInt %) + :validate [#(< 0 % 0x10000) "Must be a number between 0 and 65536"]] + ["-h" "--help"]]) (defn usage [options-summary] - (->> ["Doesn't do anything ... yet." + (->> ["Produces random emissions and states from walking an HMM." "" "Usage: hidden-markov-music [options]" "" @@ -29,5 +89,39 @@ (defn -main [& args] - (pprint (hmm/random-hmm [:rainy :sunny] - [:run :clean :shop]))) + (let [{:keys [options arguments summary errors]} + (parse-opts args cli-options)] + (cond + (:help options) + (exit 0 (usage summary)) + + (not= (count arguments) 0) + (exit 1 (usage summary)) + + errors + (exit 1 (error-msg errors))) + + (let [model (-> (:model options) + models) + states (hmm/sample-states model) + emissions (hmm/sample-emissions model states)] + (println "MODEL:") + (pprint model) + (println) + (println "SAMPLE:") + (doall + (case (:model options) + "song" + (map (fn [state emission] + (println (str "S = " state ", E = " emission))) + (take-while (fn [state] (not= state :end)) + states) + emissions) + "weather" + (take (:number options) + (map (fn [state emission] + (println (str "S = " state ", E = " emission))) + states + emissions)) + nil)) + nil))) diff --git a/src/hidden_markov_music/hmm.clj b/src/hidden_markov_music/hmm.clj index a46a6e3..f300317 100644 --- a/src/hidden_markov_music/hmm.clj +++ b/src/hidden_markov_music/hmm.clj @@ -1,6 +1,7 @@ (ns hidden-markov-music.hmm "General implementation of a hidden Markov model, and associated algorithms." - (:require [hidden-markov-music.stats :as stats])) + (:require [hidden-markov-music.stats :as stats] + [hidden-markov-music.random :refer [select-random-key]])) (defrecord HMM [states @@ -335,3 +336,47 @@ {:likelihood likelihood ;; state sequence was constructed via backtracking, and must be reversed :state-sequence (reverse optimal-state-sequence)})) + +(defn random-initial-state + "Randomly selects an initial state from the model, weighed by the initial + probability distribution." + [model] + (select-random-key (:initial-prob model))) + +(defn random-transition + "Randomly selects a state to transition to from the current state, weighed + by the transition probability distribution." + [model state] + (-> model + (get-in [:transition-prob state]) + select-random-key)) + +(defn random-emission + "Randomly emits an observation from the current state, weighed by the + observation probability distribution." + [model state] + (-> model + (get-in [:observation-prob state]) + select-random-key)) + +(defn sample-states + "Randomly walks through the states of the model, returning an infinite lazy + seq of those states. + + See [[random-initial-state]] and [[random-transition]] for details on the + decisions made at each step." + [model] + (iterate (partial random-transition model) + (random-initial-state model))) + +(defn sample-emissions + "Randomly walks through the states of the model, returning an infinite lazy + seq of emissions from those states. One can optionally provide predetermined + states, and emissions will be made from it randomly. + + See [[sample-states]] and [[random-emission]] for details." + ([model] + (sample-emissions model (sample-states model))) + ([model states] + (map (partial random-emission model) + states)))