การสร้าง AST แบบสุ่มด้วยขนาดที่ระบุใน Clojure

ฉันต้องการสร้างแผนผังไวยากรณ์นามธรรมแบบสุ่ม

(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)))

ด้วยขนาดที่กำหนด

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

ตามอัลกอริทึมจากหนังสือ Sean Luke, 2013, Essentials of Metaheuristics, Lulu, ฉบับที่สอง, ดูได้ที่ https://cs.gmu.edu/~sean/book/metaheuristics/

เราสุ่มขยายขอบฟ้าของต้นไม้ที่มีโหนดที่ไม่ใช่ลีฟจนกว่าจำนวนโหนดที่ไม่ใช่ลีฟบวกจุดที่เหลือจะมากกว่าหรือเท่ากับขนาดที่ต้องการ จากนั้นเราจะเติมช่องที่เหลือด้วยโหนดลีฟ:

อัลกอริทึม ptc2

ตัวอย่างเช่น

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

มีขนาด 7

อัลกอริธึมในหนังสือใช้พอยน์เตอร์/ข้อมูลอ้างอิง Q ซึ่งสะดวกมาก ในกรณีของฉัน ฉันต้องใช้การเรียกซ้ำบางอย่างเพื่อสร้างต้นไม้ ปัญหาคือฉันไม่สามารถรักษาสถานะ size ของแผนผังไว้ระหว่างอัลกอริธึมทั้งหมดโดยใช้การเรียกซ้ำซึ่งส่งผลให้ต้นไม้มีขนาดใหญ่ขึ้น:

(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)))))))

ฉันยังลองใช้ atom สำหรับขนาด แต่ก็ยังไม่ได้ขนาดต้นไม้ที่แน่นอนที่ฉันต้องการ มันเล็กเกินไปหรือใหญ่เกินไป ขึ้นอยู่กับการใช้งาน

นอกจากนี้ ฉันยังต้องสุ่มตำแหน่งที่ฉันแทรกโหนด/ทรีใหม่ด้วย

ฉันจะเขียนอัลกอริทึมนี้ได้อย่างไร

แก้ไข: การสัมผัสครั้งสุดท้ายกับวิธีแก้ปัญหาที่ถูกต้อง:

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

person Dmitry Matveyev    schedule 01.09.2018    source แหล่งที่มา


คำตอบ (2)


ด้านล่างนี้เป็นการแปลอัลกอริธึม PTC2 แบบคำต่อคำในบทความ มันไม่ใช่รหัส Clojure ที่เป็นสำนวนมากนัก คุณอาจต้องการแยกออกเป็นฟังก์ชั่น / บล็อกเล็ก ๆ ตามที่คุณเห็นสมเหตุสมผล

(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))))))

คุณสามารถใช้โครงสร้าง loop ของ Clojure (หรือ reduce เพื่อรักษาสถานะของการวนซ้ำของคุณ - ในอัลกอริทึมนี้ สถานะจะรวมถึง):

  • ast ซึ่งเป็นเวกเตอร์ที่ซ้อนกันซึ่งแสดงถึงสูตรที่กำลังสร้าง โดยที่โหนดที่ยังไม่เสร็จสมบูรณ์จะถูกทำเครื่องหมายเป็น nil
  • q ซึ่งสอดคล้องกับ Q ในรหัสเทียมและเป็นรายการเส้นทางไปยังโหนดที่ยังสร้างไม่เสร็จใน ast
  • และ c ซึ่งเป็นจำนวนโหนดที่ไม่ใช่ลีฟในแผนผัง

ผลลัพธ์ที่ได้คือ:

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

เราสร้าง AST โดยใช้เวกเตอร์ (ตรงข้ามกับรายการ) เนื่องจากช่วยให้เราสามารถใช้ assoc-in เพื่อสร้างแผนผังแบบก้าวหน้า คุณอาจต้องการแปลงเป็นรายการที่ซ้อนกันด้วยตัวเองหากต้องการ

person Aleph Aleph    schedule 01.09.2018

เป็นเรื่องบังเอิญ ฉันได้ทำงานกับโค้ดการจัดการ AST ใน ไลบรารี Tupelo Forest< /ก>. คุณสามารถดูโค้ดตัวอย่างที่นี่ และวิดีโอจาก Clojure/Conj ปี 2017 ที่นี่

ข้อมูลต่อไปนี้แสดงให้เห็นว่าฉันจะแก้ไขปัญหานี้อย่างไร ฉันพยายามทำให้ชื่อปรากฏชัดในตัวเองมากที่สุดเท่าที่จะเป็นไปได้ ดังนั้นจึงควรเข้าใจได้ง่ายว่าอัลกอริทึมดำเนินไปอย่างไร

พื้นฐาน:

(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)))

ฟังก์ชั่นตัวช่วย:

(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))

อัลกอริธึมหลัก:

(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))) ))))

โดยจะพิมพ์ผลลัพธ์ในรูปแบบ "บุช" แบบลำดับชั้นและในรูปแบบที่กระเพื่อมเช่นกัน นี่คือผลลัพธ์ทั่วไป 2 รายการ:

[{: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))

ฉันใช้รหัส op ตัวอักษรสามตัวแทนสัญลักษณ์ทางคณิตศาสตร์เพื่อความง่าย แต่สามารถแทนที่ได้อย่างง่ายดายด้วยชื่อสัญลักษณ์ฟังก์ชัน Clojure สำหรับอินพุตเป็น eval

person Alan Thompson    schedule 01.09.2018
comment
ขอบคุณ! กรณีการใช้งานของฉันมีไว้สำหรับการเขียนโปรแกรมทางพันธุกรรม (รุ่น AST หลายรุ่น) และวิธีแก้ปัญหานี้ช้ากว่ารุ่นที่ฉันทำเครื่องหมายไว้ว่าถูกต้องประมาณ 5.8 เท่า ฉันจะต้องทิ้งไว้ก่อน PS โปรดตรวจสอบลิงก์ไปยัง stackoverflow ในซอร์สโค้ดบน github คุณอาจผิดอันนั้น - person Dmitry Matveyev; 02.09.2018