Pembuatan AST acak dengan ukuran yang ditentukan di Clojure

Saya ingin membuat pohon sintaksis abstrak acak

(def terminal-set #{'x 'R})
(def function-arity {'+ 2, '- 2, '* 2, '% 2})
(def function-set (into #{} (keys function-arity)))
(def terminal-vec (into [] terminal-set))
(def function-vec (into [] function-set))

;; protected division
(defn % [^Number x ^Number y]
  (if (zero? y)
    0
    (/ x y)))

dengan ukuran yang ditentukan

(defn treesize [tree] (count (flatten tree)))

mengikuti algoritma dari buku Sean Luke, 2013, Essentials of Metaheuristics, Lulu, edisi kedua, tersedia di https://cs.gmu.edu/~sean/book/metaheuristics/

Kami secara acak memperluas cakrawala pohon dengan simpul bukan daun hingga jumlah simpul bukan daun, ditambah bintik-bintik yang tersisa, lebih besar atau sama dengan ukuran yang diinginkan. Kami kemudian mengisi slot yang tersisa dengan simpul daun:

algoritma ptc2

Misalnya

(+ (* x (+ x x)) x)

berukuran 7.

Algoritme dalam buku ini menggunakan pointer/referensi Q yang sangat nyaman di sana. Dalam kasus saya, saya harus menggunakan semacam rekursi untuk membangun pohon. Masalahnya adalah saya tidak bisa menjaga status size pohon di antara semua algoritma menggunakan rekursi yang menghasilkan pohon yang lebih besar:

(defn ptc2-tree
  "Generate a random tree up to its `max-size`.
  Note: `max-size` is the number of nodes, not the same as its depth."
  [max-size]
  (if (> 2 max-size)
    (rand-nth terminal-vec)
    (let [fun   (rand-nth function-vec)
          arity (function-arity fun)]
      (cons fun (repeatedly arity #(ptc2-tree (- max-size arity 1)))))))

Saya juga mencoba menggunakan atom untuk ukuran tetapi masih tidak mendapatkan ukuran pohon yang saya inginkan, mungkin terlalu kecil atau terlalu besar tergantung implementasinya.

Selain itu saya juga harus mengacak lokasi tempat saya memasukkan simpul/pohon baru.

Bagaimana cara menulis algoritma ini?

EDIT: Sentuhan terakhir untuk solusi yang tepat:

(defn sequentiate [v] 
  (map #(if (seqable? %) (sequentiate %) %) (seq v)))

person Dmitry Matveyev    schedule 01.09.2018    source sumber


Jawaban (2)


Di bawah ini kurang lebih terjemahan kata demi kata dari algoritma PTC2 pada artikel. Ini bukan kode Clojure yang idiomatis; Anda mungkin ingin membaginya menjadi fungsi/blok yang lebih kecil sesuai keinginan Anda.

(defn ptc2 [target-size]
  (if (= 1 target-size)
    (rand-nth terminal-vec)
    (let [f (rand-nth function-vec)
          arity (function-arity f)]
      ;; Generate a tree like [`+ nil nil] and iterate upon it
      (loop [ast (into [f] (repeat arity nil))
             ;; q will be something like ([1] [2]), being a list of paths to the
             ;; nil elements in the AST
             q (for [i (range arity)] [(inc i)])
             c 1]
        (if (< (+ c (count q)) target-size)
          ;; Replace one of the nils in the tree with a new node
          (let [a (rand-nth q)
                f (rand-nth function-vec)
                arity (function-arity f)]
            (recur (assoc-in ast a (into [f] (repeat arity nil)))
                   (into (remove #{a} q)
                         (for [i (range arity)] (conj a (inc i))))
                   (inc c)))
          ;; In the end, fill all remaining slots with terminals
          (reduce (fn [t path] (assoc-in t path (rand-nth terminal-vec)))
                  ast q))))))

Anda dapat menggunakan konstruksi loop Clojure (atau reduce untuk menjaga status iterasi Anda - dalam algoritma ini, status tersebut mencakup):

  • ast, yaitu vektor bersarang yang mewakili rumus yang sedang dibangun, dimana node yang belum selesai ditandai sebagai nil;
  • q, yang sesuai dengan Q di kodesemu dan merupakan daftar jalur ke node yang belum selesai di ast,
  • dan c, yang merupakan jumlah simpul non-daun pada pohon.

Hasilnya, Anda mendapatkan sesuatu seperti:

(ptc2 10) ;; => [* [- R [% R [% x x]]] [- x R]]

Kami membuat AST menggunakan vektor (bukan daftar) karena memungkinkan kami menggunakan assoc-in untuk membangun pohon secara progresif; Anda mungkin ingin mengonversinya sendiri menjadi daftar bersarang jika perlu.

person Aleph Aleph    schedule 01.09.2018

Secara kebetulan, saya sedang mengerjakan kode manipulasi AST di perpustakaan Hutan Tupelo. Anda dapat melihat contoh kode di sini , dan video dari Clojure/Conj 2017 di sini.

Berikut ini menunjukkan bagaimana saya akan mengatasi masalah ini. Saya mencoba membuat nama-nama itu sejelas mungkin sehingga mudah untuk memahami bagaimana algoritmanya berjalan.

Dasar-dasar:

(def op->arity {:add 2
                :sub 2
                :mul 2
                :div 2
                :pow 2})
(def op-set (set (keys op->arity)))
(defn choose-rand-op [] (rand-elem op-set))

(def arg-set #{:x :y})
(defn choose-rand-arg [] (rand-elem arg-set))

(defn num-hids [] (count (all-hids)))

Fungsi pembantu:

(s/defn hid->empty-kids :- s/Int
  [hid :- HID]
  (let [op             (hid->attr hid :op)
        arity          (grab op op->arity)
        kid-slots-used (count (hid->kids hid))
        result         (- arity kid-slots-used)]
    (verify (= 2 arity))
    (verify (not (neg? result)))
    result))

(s/defn node-has-empty-slot? :- s/Bool
  [hid :- HID]
  (pos? (hid->empty-kids hid)))

(s/defn total-empty-kids :- s/Int
  []
  (reduce +
    (mapv hid->empty-kids (all-hids))))

(s/defn add-op-node :- HID
  [op :- s/Keyword]
  (add-node {:tag :op :op op} )) ; add node w no kids

(s/defn add-leaf-node :- tsk/KeyMap
  [parent-hid :- HID
   arg :- s/Keyword]
  (kids-append parent-hid [(add-leaf {:tag :arg :arg arg})]))

(s/defn need-more-op? :- s/Bool
  [tgt-size :- s/Int]
  (let [num-op            (num-hids)
        total-size-so-far (+ num-op (total-empty-kids))
        result            (< total-size-so-far tgt-size)]
    result))

Algoritma utama:

(s/defn build-rand-ast :- tsk/Vec ; bush result
  [ast-size]
  (verify (<= 3 ast-size)) ; 1 op & 2 args minimum;  #todo refine this
  (with-debug-hid
    (with-forest (new-forest)
      (let [root-hid (add-op-node (choose-rand-op))] ; root of AST
        ; Fill in random op nodes into the tree
        (while (need-more-op? ast-size)
          (let [node-hid (rand-elem (all-hids))]
            (when (node-has-empty-slot? node-hid)
              (kids-append node-hid
                [(add-op-node (choose-rand-op))]))))
        ; Fill in random arg nodes in empty leaf slots
        (doseq [node-hid (all-hids)]
          (while (node-has-empty-slot? node-hid)
            (add-leaf-node node-hid (choose-rand-arg))))
        (hid->bush root-hid)))))

(defn bush->form [it]
  (let [head (xfirst it)
        tag  (grab :tag head)]
    (if (= :op tag)
      (list (kw->sym (grab :op head))
        (bush->form (xsecond it))
        (bush->form (xthird it)))
      (kw->sym (grab :arg head)))))

(dotest
  (let [tgt-size 13]
    (dotimes [i 5]
      (let [ast     (build-rand-ast tgt-size)
            res-str (pretty-str ast)]
        (nl)
        (println res-str)
        (println (pretty-str (bush->form ast))) ))))

Ini mencetak hasil dalam format "semak" hierarkis, dan juga dalam bentuk lispy. Berikut adalah 2 hasil tipikal:

[{:tag :op, :op :mul}
 [{:tag :op, :op :div}
  [{:tag :op, :op :pow}
   [{:tag :op, :op :sub}
    [{:tag :arg, :arg :y, :value nil}]
    [{:tag :arg, :arg :x, :value nil}]]
   [{:tag :op, :op :div}
    [{:tag :arg, :arg :y, :value nil}]
    [{:tag :arg, :arg :y, :value nil}]]]
  [{:tag :arg, :arg :y, :value nil}]]
 [{:tag :op, :op :pow}
  [{:tag :arg, :arg :x, :value nil}]
  [{:tag :arg, :arg :y, :value nil}]]]

(mul (div (pow (sub y x) (div y y)) y) (pow x y))


[{:tag :op, :op :div}
 [{:tag :op, :op :mul}
  [{:tag :op, :op :pow}
   [{:tag :arg, :arg :x, :value nil}]
   [{:tag :arg, :arg :y, :value nil}]]
  [{:tag :op, :op :add}
   [{:tag :op, :op :div}
    [{:tag :arg, :arg :x, :value nil}]
    [{:tag :arg, :arg :y, :value nil}]]
   [{:tag :arg, :arg :x, :value nil}]]]
 [{:tag :op, :op :mul}
  [{:tag :arg, :arg :x, :value nil}]
  [{:tag :arg, :arg :y, :value nil}]]]

(div (mul (pow x y) (add (div x y) x)) (mul x y))

Saya menggunakan kode operasi tiga huruf alih-alih simbol matematika untuk kesederhanaan, tetapi kode tersebut dapat dengan mudah diganti dengan nama simbol fungsi Clojure untuk masukan ke eval.

person Alan Thompson    schedule 01.09.2018
comment
Terima kasih! Kasus penggunaan saya adalah untuk pemrograman genetik (banyak generasi AST) dan solusi ini ~5,8 kali lebih lambat daripada yang saya centang sebagai benar, saya harus meninggalkannya untuk saat ini. PS tolong, periksa tautan ke stackoverflow di kode sumber di github, Anda mungkin salah di sana. - person Dmitry Matveyev; 02.09.2018