Изучение Clojure: рекурсия для скрытой марковской модели

Я изучаю Clojure и начал с копирования функциональности программы Python, которая создавала бы геномные последовательности, следуя (чрезвычайно простой) скрытой марковской модели.

В начале я придерживался своего известного способа последовательного программирования и часто использовал ключевое слово def, тем самым решая проблему с кучей побочных эффектов, пиная почти все концепции Clojure прямо в задницу. (хотя это сработало, как предполагалось)

Затем я попытался преобразовать его в более функциональный способ, используя loop, recur, atom и так далее. Теперь, когда я запускаю, я получаю ArityException, но я не могу прочитать сообщение об ошибке таким образом, который показывает мне, даже какая функция его выдает.

(defn create-model [name pA pC pG pT pSwitch]
; converts propabilities to cumulative prop's and returns a char
  (with-meta
    (fn []
      (let [x (rand)]
        (if (<= x pA)
          \A
          (if (<= x (+ pA pC))
            \C
            (if (<= x (+ pA pC pG))
              \G
              \T)))))                   ; the function object
    {:p-switch pSwitch :name name}))    ; the metadata, used to change model


(defn create-genome [n]
; adds random chars from a model to a string and switches models randomly
  (let [models [(create-model "std" 0.25 0.25 0.25 0.25 0.02) ; standard model, equal props
                (create-model "cpg" 0.1 0.4 0.4 0.1 0.04)]    ; cpg model
        islands (atom 0)                 ; island counter
        m (atom 0)]                      ; model index
    (loop [result (str)]
      (let [model (nth models @m)]
        (when (< (rand) (:p-switch (meta model))) ; random says "switch model!"
;          (swap! m #(mod (inc @m) 2))   ; swap model used in next iteration     
          (swap! m #(mod (inc %) 2))     ; EDIT: correction
          (if (= @m 1)                   ; on switch to model 1, increase island counter
;            (swap! islands #(inc @islands)))) ; EDIT: my try, with error
            (swap! islands inc)))) ;             EDIT: correction
        (if (< (count result) n)         ; repeat until result reaches length n
          (recur (format "%s%c" result (model)))
          result)))))

Запуск работает, но вызов (create-genome 1000) приводит к

ArityException Wrong number of args (1) passed to: user/create-genome/fn--772  clojure.lang.AFn.throwArity (AFn.java:429)

Мои вопросы:

  • (очевидно) что я делаю не так?
  • как именно я должен понимать сообщение об ошибке?

Информация, которую я буду рад получить

  • как можно улучшить код (так, чтобы clojure-newb мог его понять)? Также разные парадигмы - я благодарен за предложения.
  • Почему мне нужно ставить знак решетки # перед формами, которые я использую для изменения состояний атомов? Я видел это в примере, без него функция не будет вычисляться, но я не понимаю :)

person waechtertroll    schedule 25.06.2015    source источник
comment
В таких ситуациях поместите это в файл .clj и скомпилируйте его, затем, когда вы запустите его и он выдаст ошибку, он сообщит вам, в какой строке проблема.   -  person Dominykas Mostauskis    schedule 25.06.2015


Ответы (3)


Хорошо, это далеко, но похоже, что ваши функции обновления атома:

#(mod (inc @m) 2)

и

#(inc @islands)

имеют 0-арность, и они должны иметь арность не менее 1.

Это приводит к ответу на ваш последний вопрос: форма #(body) является сокращением для (fn [...] (body)). Таким образом, он создает анонимную функцию. Хитрость заключается в том, что если body содержит % или %x, где x - это число, позиция, в которой оно появляется, будет заменена ссылкой на номер аргумента созданной функции x (или первый аргумент, если это только %).

В вашем случае body не содержит ссылок на аргументы функции, поэтому #() создает анонимную функцию, которая не принимает аргументов, чего не ожидает swap!.

Итак, swap пытается передать аргумент чему-то, что его не ожидает, и бум!, вы получаете исключение ArityException.

Что вам действительно нужно в этих случаях, так это:

(swap! m #(mod (inc %) 2)) ; will swap value of m to (mod (inc current-value-of-m) 2) internally

и

(swap! islands inc) ; will swap value of islands to (inc current-value-of-islands) internally

соответственно

person soulcheck    schedule 25.06.2015
comment
Ах, идеально! Документация swap! показалась мне немного двусмысленной, но ваше объяснение, что она принимает аргумент и применяет его к следующему аргументу, прояснило ситуацию. Кроме того, ваше предложение решило проблему. Спасибо :) - person waechtertroll; 25.06.2015

Поскольку вы спросили о способах улучшения, вот один из подходов, к которому я часто прибегаю: Могу ли я абстрагировать этот loop в шаблон более высокого порядка?

В этом случае ваш цикл выбирает символы случайным образом — это можно смоделировать как вызов fn без аргументов, который возвращает символ — и затем накапливает их вместе, пока их не будет достаточно. Это очень естественно вписывается в repeatedly. , который берет такие функции и делает ленивые последовательности их результатов любой длины, которую вы хотите.

Затем, поскольку у вас есть вся последовательность символов вместе, вы можете объединить их в строку немного эффективнее, чем повторяющиеся formats - clojure.string/join должны хорошо вписываться, или вы могли бы apply str поверх нее.

Вот моя попытка такой формы кода - я также пытался сделать ее достаточно управляемой данными, и это могло привести к тому, что она была немного загадочной, так что потерпите меня:

(defn make-generator 
  "Takes a probability distribution, in the form of a map 
  from values to the desired likelihood of that value appearing in the output.
  Normalizes the probabilities and returns a nullary producer fn with that distribution."
  [p-distribution]  
  (let[sum-probs (reduce + (vals p-distribution))
       normalized (reduce #(update-in %1 [%2] / sum-probs) p-distribution (keys p-distribution) )]
      (fn [] (reduce 
              #(if (< %1 (val %2)) (reduced (key %2)) (- %1 (val %2))) 
              (rand) 
              normalized))))

(defn markov-chain 
  "Takes a series of states, returns a producer fn.
  Each call, the process changes to the next state in the series with probability :p-switch,
  and produces a value from the :producer of the current state."
  [states]
  (let[cur-state (atom (first states))
       next-states (atom (cycle states))]
    (fn [] 
      (when (< (rand) (:p-switch @cur-state))
        (reset! cur-state (first @next-states))
        (swap! next-states rest))
      ((:producer @cur-state)))))


(def my-states [{:p-switch 0.02 :producer (make-generator {\A 1 \C 1 \G 1 \T 1})  :name "std"}
                {:p-switch 0.04 :producer (make-generator {\A 1 \C 4 \G 4 \T 1})  :name "cpg"}])


(defn create-genome [n]
  (->> my-states
       markov-chain
       (repeatedly n)
       clojure.string/join))

Чтобы, надеюсь, объяснить немного сложности:

  • let в make-generator просто проверяет, чтобы сумма вероятностей равнялась 1.
  • make-generator интенсивно использует другой шаблон зацикливания более высокого порядка, а именно reduce. По сути, это принимает функцию из двух значений и передает через нее коллекцию. (reduce + [4 5 2 9]) похоже на (((4 + 5) + 2) + 9). В основном я использую его, чтобы сделать то же самое, что и ваши вложенные ifs в create-model, но не называя, сколько значений находится в распределении вероятностей.
  • markov-chain создает два атома: cur-state для хранения текущего состояния и next-states для хранения бесконечной последовательности (начиная с cycle) следующих состояний для переключения. Это должно работать как ваши m и models, но для произвольного количества состояний.
  • Затем я использую when, чтобы проверить, должно ли происходить случайное переключение состояний, и если оно вызывает два побочных эффекта, которые мне нужны для поддержания атомов состояния в актуальном состоянии. Затем я просто вызываю :producer @cur-state без аргументов и возвращаю его.

Теперь, очевидно, вам не обязательно делать это именно таким образом, но поиск этих шаблонов, безусловно, помогает мне.
Если вы хотите стать еще более функциональным, вы также можете подумать о переходе на дизайн, в котором ваши генераторы взять состояние (с генератором случайных чисел с заполнением) и вернуть значение плюс новое состояние. Этот подход "монада состояния" позволил бы быть полностью декларативным, чего нет в этой схеме.

person Magos    schedule 25.06.2015
comment
Интересный подход. Поразительно, насколько легко можно понять ваш код (я уже знал сокращения из программирования многопроцессорных систем и ленивые последовательности из Python). Я знал, что мое переформатирование строк отнимает время, но это казалось самым простым способом ;-) Всего два вопроса: не будет ли создание и циклирование бесконечных последовательностей медленнее, чем циклирование индексов массива? (mod (inc m) (количество состояний)) будет делать то же самое для произвольного количества функций, я думаю... И что делает -››? Я знаю, что -› будет связывать вызовы, но еще не нашел этот. - person waechtertroll; 27.06.2015
comment
Вполне возможно, я точно не проверял. В то время это имело смысл, но в тот момент я как бы забыл о модном подходе. Другое потенциальное преимущество подхода с сохранением изменяемого индекса заключается в том, что вы можете создать более общий марковский процесс, в котором каждое состояние может иметь индивидуальную вероятность перехода в другое состояние — и вы также можете сделать это с помощью make-generator. - person Magos; 28.06.2015
comment
-> и ->> являются своего рода родственными макросами, которые иногда произносятся/называются как thread-first и thread-last. Оба они принимают две или более формы и вставляют каждую в следующую форму, но в разных позициях. С (-> a (b c)) a вставляется в качестве первого аргумента в b (т.е. расширение (b a c), а с (->> a (b c) ) это последний аргумент (т.е. расширение (b c a)). - person Magos; 28.06.2015

Ваша ошибка связана с тем, что вы спросили о макросе хэштега #.

#(+ %1 %2) — это сокращение от (fn [x y] (+ x y)). Можно и без аргументов: #(+ 1 1). Вот как вы его используете. Ошибка, которую вы получаете, связана с тем, что swap! нужна функция, которая принимает параметр. Что он делает, так это передает текущее значение атома вашей функции. Если вам не важно его состояние, используйте reset!: (reset! an-atom (+ 1 1)). Это исправит вашу ошибку.

Исправление:

Я только что еще раз взглянул на ваш код и понял, что вы на самом деле используете работу с состояниями атомов. Итак, что вы хотите сделать, это:

(swap! m #(mod (inc %) 2)) вместо (swap! m #(mod (inc @m) 2)).

Что касается стиля, у вас все хорошо. Я пишу свои функции по-разному каждый день недели, поэтому, возможно, я не из тех, кто дает советы по этому поводу.

person Dominykas Mostauskis    schedule 25.06.2015
comment
Таким образом, в основном вы были немного медленнее, чем проверка души, давая тот же ответ вместе с объяснением, которое заполняет крошечный пробел - так что ваши два ответа вместе делают его идеальным. Спасибо. - person waechtertroll; 25.06.2015