เหตุผลที่ Planner Programming น่าทึ่ง
(hillelwayne.com)- โมดูล
plannerของ Picat ก้าวไปอีกขั้นจาก logic programming ที่ค้นหาการกำหนดค่าให้ตัวแปร โดยให้เรานิยามปัญหาเป็น ลำดับการเปลี่ยนสถานะ เพื่อไปยังสถานะเป้าหมาย - เมื่อกำหนดสถานะเริ่มต้น
Start, กฎการเปลี่ยนผ่านaction(From, To, Action, Cost)และเงื่อนไขสิ้นสุดfinal(S)แล้วbest_plan(Start, Plan)จะค้นหา แผนที่มีต้นทุนต่ำสุด ไปจนถึงสถานะสุดท้ายให้ - ในตัวอย่างการหาเส้นทางบนกริด สามารถจัดการการเคลื่อนที่ ขอบเขตกริด การหลบสิ่งกีดขวาง และการเยี่ยมหลายเป้าหมายได้เพียงเปลี่ยน
actionกับfinalและยังเลือกได้ว่าจะตรึงลำดับการเยี่ยมเป้าหมายหรือปล่อยให้ยืดหยุ่น - ฟีเจอร์วางแผนผสานกับ constraint solving ทำให้สามารถแสดงปัญหาอย่าง partition problem เช่น การหา sublist ที่ใหญ่ที่สุดซึ่งได้จากการลบองค์ประกอบออกบางตัวและยังแบ่งเป็นสองส่วนที่มีผลรวมเท่ากันได้
- แม้ Picat จะเป็นภาษาเพื่อการวิจัยที่เอกสารและข้อความผิดพลาดยังมีไม่มาก แต่ในฐานะ ภาษาเชิงเครื่องมือ สำหรับแก้ปัญหาการคำนวณบางประเภทอย่างรวดเร็ว ก็ให้วิธีแก้ที่กระชับกว่าภาษาทั่วไปได้
แนวคิดพื้นฐานของ Picat และ Planner Programming
- Picat เป็นภาษาเพื่อการวิจัยที่พยายามผสาน logic programming, imperative programming และ constraint solving เข้าด้วยกัน
- การเขียนโปรแกรมแบบ imperative และ functional ทั่วไปคือการเขียนอัลกอริทึมที่สร้างเอาต์พุตจากอินพุต แต่ logic programming และ constraint solving จะค้นหา การกำหนดค่าให้ตัวแปร ที่ทำให้ความสัมพันธ์เป็นจริง
- ใน Picat ตัวระบุที่ไม่ใช่ฟังก์ชันและขึ้นต้นด้วยตัวพิมพ์เล็ก เช่น
a,b,cคือ atom ส่วนตัวระบุที่ขึ้นต้นด้วยตัวพิมพ์ใหญ่คือ variable - แม้ในนิพจน์อย่าง
member(Y, Arr)ที่มีตัวแปรYซึ่งยังไม่ได้ถูกกำหนด Picat ก็สามารถค้นหาและกำหนดค่าที่ทำให้นิพจน์เป็นจริงได้- ถ้า
Arr = [a, b, c, a]แล้วYอาจเป็นหนึ่งในa,b,c - จากนั้นหากใส่เงื่อนไขอย่าง
X != Yค่าที่เป็นไปได้ก็จะแคบลง - แม้ในกรณีอย่าง
member(a, Z)ที่ยังไม่รู้ตัว list เอง ก็สามารถ instantiateZให้เป็น list ได้
- ถ้า
การวางแผนค้นหาการเปลี่ยนสถานะ ไม่ใช่การกำหนดค่าให้ตัวแปร
- Planning คือการค้นหา ลำดับการเปลี่ยนตัวแปร ที่ไปถึงสถานะสิ้นสุดหนึ่ง ๆ แทนที่จะค้นหาค่าตัวแปรที่ทำให้สมการเป็นจริง
- ปัญหา planning ใน Picat ต้องมีองค์ประกอบสามอย่าง
- สถานะเริ่มต้น
Start - ฟังก์ชัน
actionที่แทนการเปลี่ยนสถานะ final(S)ที่ตัดสินว่าสถานะนั้นเป็นสถานะสิ้นสุดหรือไม่
- สถานะเริ่มต้น
- ฟังก์ชัน
actionของ Picat ทุกตัวต้องมีชื่อว่าactionและรับพารามิเตอร์สี่ตัว- สถานะปัจจุบัน
- สถานะถัดไป
- ชื่อ action
- ต้นทุน
best_plan(Start, Plan)จะกำหนด แผนที่ใช้จำนวนขั้นน้อยที่สุดหรือมีต้นทุนต่ำสุด ที่จำเป็นต่อการไปถึงสถานะสิ้นสุดให้กับPlan- ถ้าตั้งต้นทุนทั้งหมดเป็น
1ต้นทุนของแผนจะเท่ากับจำนวนก้าวการเคลื่อนที่รวม - หากต้องการแค่แผนใดก็ได้โดยไม่สนใจความยาว สามารถใช้
plan(Start, Plan)ได้
- ถ้าตั้งต้นทุนทั้งหมดเป็น
ตัวอย่างการหาเส้นทางบนกริด
- ปัญหาตัวอย่างคือ marker บนกริดเริ่มจากจุดกำเนิด
(0, 0)แล้วไปถึงพิกัดเป้าหมาย- ในแต่ละขั้นสามารถขยับขึ้น ลง ซ้าย ขวา ได้ทีละช่อง
- ห้ามออกนอกขอบเขตกริด
- เมื่อถึงพิกัดเป้าหมายถือว่าสำเร็จ
- สถานะเริ่มต้นเก็บตำแหน่งปัจจุบันและเป้าหมายไว้ด้วยกัน เช่น
{Origin, Goal}{a, b}ใน Picat เป็นไวยากรณ์ของ array และโดยพื้นฐานใช้เหมือน tuple
- สามารถเขียนเงื่อนไขสิ้นสุดด้วย pattern matching ได้ เช่น
final({Pos, Goal}) => Pos = Goal.- หากจะเขียนสิ่งเดียวกันโดยไม่ใช้ pattern matching ต้องแยกสถานะออกเป็น
{Pos, Goal}ก่อน - ถ้ามีเงื่อนไข
finalหลายข้อ แผนจะสำเร็จเมื่อมีข้อใดข้อหนึ่งเป็นจริง
- หากจะเขียนสิ่งเดียวกันโดยไม่ใช้ pattern matching ต้องแยกสถานะออกเป็น
- action การเคลื่อนที่จะเลือกหนึ่งในสี่ทิศทาง
{-1,0},{1,0},{0,-1},{0,1}และตรวจว่าพิกัดใหม่อยู่ในช่วง0..10หรือไม่member({Dx, Dy}, Dir)ใช้เพื่อหาค่าทิศทางที่เป็นไปได้member(Tx, 0..10)และmember(Ty, 0..10)ใช้เพื่อตรวจว่าพิกัดอยู่ภายในขอบเขตหรือไม่- predicate สำหรับตรวจอย่างเดียวโดยไม่กำหนดค่าให้ตัวแปรก็มี
membchkด้วย
- แผนผลลัพธ์จะถูกแสดงเป็น list ของ action การเคลื่อนที่และพิกัดใหม่ เช่น
{move,{1,0}},{move,{2,0}}- สามารถใช้สคริปต์ Raku เพื่อแสดงเส้นทางเป็นภาพได้
- หากเพิ่มเงื่อนไขอย่าง
{Tx, Ty} != {2, 1}ก็สามารถทำ การหลบสิ่งกีดขวาง เพื่อเลี่ยงพิกัดเฉพาะได้
หลายเป้าหมายและการลดต้นทุนให้น้อยที่สุด
- หากต้องเยี่ยมหลายเป้าหมาย ให้เปลี่ยน
Goalจากพิกัดเดียวเป็น คิวเป้าหมาย เช่น[{2, 2}, {3, 4}] - เพิ่ม
actionใหม่ที่จะลบรายการนั้นออกจาก list เป้าหมายเมื่อไปถึงเป้าหมาย[Head|Tail]แบ่ง list เป็นองค์ประกอบแรกกับส่วนที่เหลือGoal = [Pos|Rest]จะเป็นจริงเฉพาะเมื่อสถานะปัจจุบันPosเท่ากับรายการแรกใน list เป้าหมาย- หากตั้งสถานะใหม่เป็น
{Pos, Rest}เป้าหมายที่ไปถึงแล้วจะถูกลบออก
- ตรวจว่าเยี่ยมครบทุกเป้าหมายหรือไม่ด้วย
final({Pos, Goal}) => Goal = [].- เงื่อนไขสิ้นสุดจะไม่ใช่ว่าตำแหน่งปัจจุบันเท่ากับเป้าหมายใดเป้าหมายหนึ่งหรือไม่ แต่เป็นว่า list เป้าหมายว่างแล้วหรือไม่
- วิธีเยี่ยมเป้าหมายตามลำดับที่กำหนดไม่ได้สร้างเส้นทางสั้นที่สุดโดยรวมเสมอไป
- หากต้องการไม่สนใจลำดับเป้าหมายและลดเส้นทางรวมให้น้อยที่สุด ให้เปลี่ยน action
mark- แทนที่จะใช้
Goal = [Pos|Rest]ให้ใช้member(Pos, Goal)เพื่อตรวจว่าตำแหน่งปัจจุบันอยู่ที่ใดก็ตามใน list เป้าหมาย - ใช้
To = {Pos, delete(Goal, Pos)}เพื่อลบเป้าหมายที่เยี่ยมแล้วออกจาก list - ด้วยวิธีนี้ Picat สามารถเลือกเป้าหมายถัดไปที่จะไปและลด ความยาวเส้นทางรวม ให้น้อยที่สุดได้
- แทนที่จะใช้
การผสาน planning กับ constraint solving
- ฟีเจอร์ planning ของ Picat ผสานกับฟีเจอร์อื่น ๆ ของ Picat และสามารถใช้ planning ร่วมกับ constraint solving ได้
- partition problem ที่ยกเป็นตัวอย่างคือปัญหา NP-complete ในการแบ่ง list ของตัวเลขออกเป็นสองกลุ่มที่มีผลรวมเท่ากัน
- โปรแกรมนี้ลบองค์ประกอบออกจาก list ตัวเลขเพื่อหา list ย่อยที่ใหญ่ที่สุดซึ่งสามารถแบ่งเป็นสองกลุ่มที่มีผลรวมเท่ากันได้
- ให้การลบองค์ประกอบออกจาก list ตัวเลขเป็น action ของแผน
final(Numbers)ตรวจว่า list ตัวเลขนั้นมีคำตอบ partition ที่ถูกต้องหรือไม่- ใช้ constraint จากโมดูล
cpแทนว่าแต่ละองค์ประกอบอยู่ในกลุ่มซ้ายหรือขวาด้วยตัวแปร0..1 - ใส่ constraint ให้ผลรวมทั้งหมดเท่ากับสองเท่าของผลรวมกลุ่มหนึ่ง
- ในผลลัพธ์ตัวอย่าง หลังจากลบ
[5,17]แล้ว list ที่เหลือสามารถแบ่งเป็นสองกลุ่มที่แต่ละกลุ่มมีผลรวม110832+99+977=1108122+77+86+59+47+154+141+172+49+62+109+30=1108
- วิธีนี้ไม่ได้หยุดแค่การแก้ constraint ที่ถูกต้องโดยตรง แต่ยังแสดง การเปลี่ยนแปลงเพื่อไปถึงสถานะ constraint ที่ถูกต้อง ในรูปแบบแผนด้วย
ข้อจำกัดและกรณีใช้งานที่เหมาะกับ Picat
- Picat เป็นภาษาเพื่อการวิจัย จึงไม่แนะนำให้ใช้ใน production
- ฟีเจอร์อำนวยความสะดวกมีไม่มาก และยังขาดเอกสารที่ดีหรือข้อความผิดพลาดที่ชัดเจน
- เมื่อไม่มีแผนที่แก้ได้ ข้อผิดพลาดจะแสดงประมาณ
*** error(failed,main/0)
- เมื่อไม่มีแผนที่แก้ได้ ข้อผิดพลาดจะแสดงประมาณ
- การที่รันบน Windows ได้ถือว่าดีกว่าภาษาเพื่อการวิจัยจำนวนมาก
- Picat ใกล้เคียงกับ ภาษาแบบ toolkit สำหรับแก้ปัญหาการคำนวณบางประเภท มากกว่าจะเป็นภาษาสำหรับเขียนโค้ดที่ต้องบำรุงรักษาหรือแชร์ต่อ
- ปัญหาบางอย่างที่จัดการด้วยภาษาโปรแกรมทั่วไปและ constraint solver ได้ยาก สามารถแก้ด้วย Picat ได้ค่อนข้างงดงาม
ภาษา planning อื่น ๆ และแนวคิดที่เกี่ยวข้อง
- Planning เดิมถูกบุกเบิกใน robotics และ AI แต่ปัจจุบันมักใช้ใน AI ของวิดีโอเกมในชื่อ Goal Oriented Action Planning(GOAP)
- โดยทั่วไปจะทำเป็นไลบรารีบนภาษาอื่น หรือ implement ด้วย กลยุทธ์การค้นหาแบบกำหนดเอง
- คำอธิบายเกี่ยวกับ GOAP ดูได้จาก เอกสารนี้
- PDDL เป็นภาษาอธิบายแผนที่ planner อิสระรับเป็นอินพุต
- มีบทบาทคล้ายกับที่ DIMACS ใช้เป็นรูปแบบคำอธิบายใน SAT
1 ความคิดเห็น
ความคิดเห็นใน Hacker News
เคยลองใช้ โหมด planner ของ Picat ในงานจริง
ได้ทำต้นแบบระบบสำหรับประสานงานการบำรุงรักษาชุดอุปกรณ์หลายชุด โดยเป็นวิธีที่ป้อนว่า “ต้องการอะไร” “มีการกระทำอะไรที่เป็นไปได้บ้าง” และ “มีข้อจำกัดอะไรที่ต้องรักษาไว้” แทนที่จะบอกว่า “ต้องทำอย่างไร”
กับตัวอย่างเล็ก ๆ มันสร้างแผนที่เหมาะที่สุดได้ดี แต่พอขยายไปถึงขนาดใช้งานจริงก็พังลงตามคาด และปัญหาการวางแผนก็เป็น EXPTIME อยู่ดี เลยมีข้อจำกัดมาก
Picat มีทางหนีทีไล่ให้กำหนด heuristic ได้ จึงลองให้ random forest และ naive Bayes classifier บน state predicate มาช่วยทำนายเส้นทางที่มีแววดี แต่ถึงจะใส่ทั้งข้อจำกัดแบบ symmetry breaking และ hierarchical planning ก็ยังต้องลงแรงเยอะเกินไป
ดูเหมือนว่าในโดเมนปัญหา GOFAI แบบดั้งเดิม ยังมี ฤดูหนาว AI หลงเหลืออยู่
แต่อาจไม่ใช่ ถ้าเปลี่ยนโจทย์การวางแผนใหม่ให้กลายเป็นการคืนค่า “ตัวสร้างแบบเวลาพหุนามที่สามารถสร้างแผนซึ่งอาจยาวแบบชี้กำลังได้”
นึกถึง CPLEX, Xpress, GUROBI, Hexaly และ Hexaly เด่นเป็นพิเศษกับงานวางตารางหรือปัญหาเส้นทางรถ
ปกติแล้วจะเข้าถึงผ่าน API ของภาษาที่นิยมใช้ในอุตสาหกรรม ซึ่งดูสมเหตุสมผลกว่าภาษาเฉพาะของตัวแก้ที่อ่อนกับงานทั่วไปมาก
การเรียก GUROBI จาก Python นั้นง่ายมาก และยังใช้ความสามารถทั่วไปของ Python ได้ครบ
Mosek ถูกกว่า GUROBI มาก แต่ API ของทั้งคู่ก็ค่อนข้าง low-level มาก และประสิทธิภาพก็ยังสู้ GUROBI ไม่ได้
ตัวแก้นี้จัดการตัวแปรและข้อจำกัดจำนวนมหาศาลแบบเหลือเชื่อได้สบาย และ heuristic ในตัวก็ยอดเยี่ยม
บางระบบแยกปัญหาออกเป็นหลาย representation แล้วต่อเข้ากับตัวแก้อัตโนมัติที่เชี่ยวชาญเฉพาะด้าน
นึกถึง Jahob Analysis System กับ Cyc
ในแง่การใช้งานจริง สถาปัตยกรรมที่ดูลงตัวที่สุดใน AI แบบดั้งเดิมคือ Procedural Reasoning System และอยากเห็นเวอร์ชันที่สร้างใหม่โดยเอาวิธีสมัยใหม่มาอุดจุดอ่อนของมัน
https://en.wikipedia.org/wiki/Procedural_reasoning_system
ในงานคำนวณเชิงตัวเลขสาขาอื่น เทคโนโลยีล้ำ ๆ มักเป็นโอเพนซอร์ส แต่ทำไมสาย optimization ถึงต่างออกไปก็เป็นเรื่องที่สงสัยมาตลอด
มีผู้ใช้ HN ที่แอ็กทีฟในชุมชน constraint programming ชื่อ hakank หรือ Hakan Kjellerstrand ซึ่งมีเว็บรวมเอกสารและตัวอย่างของ Picat ไว้จำนวนมาก: http://www.hakank.org/picat/
ตามปกติแล้วก็อยากแนะนำ Prolog
มันสวย เข้าใจง่าย และมีความเป็นผู้ใหญ่มากกว่า ถ้าต้องการแก้ constraint finite domain แค่ของที่มีมาให้ก็เพียงพอแล้ว
นอกจากนี้ MiniZinc ยังเป็นอินเทอร์เฟซชั้นเยี่ยมสำหรับเข้าถึงตัวแก้หลายตัวที่เหมาะกับวัตถุประสงค์ต่างกัน ดังนั้นถ้าไม่ใช่ผู้เชี่ยวชาญ ก็มักมีโอกาสเข้าใกล้ผลลัพธ์ที่ต้องการมากกว่า
Prolog มีข้อดีมากมาย แต่ถ้าจะให้ได้ประสิทธิภาพดี “สัญชาตญาณเชิงกลไก” ที่ต้องใช้ก็อาจเพิ่มขึ้นมากอย่างรวดเร็ว
ถ้าเคยเขียนอะไรด้วย Picat ก็น่าลองคิดดูว่าของเดียวกันนี้จะเขียนในภาษาอื่นอย่างไร
ปัญหาเล่น ๆ แบบนี้ในภาษาอื่นก็ง่ายเหมือนกัน และในภาษาเชิงฟังก์ชันส่วนใหญ่ Dijkstra หรือ A* ใช้ไม่กี่บรรทัดก็พอ สุดท้ายหลายครั้งสิ่งสำคัญทั้งหมดก็คือการนิยาม state space ของอัลกอริทึมค้นหา
ดีใจที่เห็น Predrag เป็นผู้รีวิว แต่ก็ไม่แปลกใจเลย
Firebase technical screen น่าจะง่ายขึ้นมากถ้ามีเครื่องมือแบบนี้ และสุดท้ายมันก็เป็น ปัญหา optimization อีกแบบหนึ่ง
เลยอยากลองกลับไปแก้มันด้วย Picat ดูอีกครั้ง
เขายังทำเรื่องน่าสนใจในสายภาษาโปรแกรมด้วย: https://github.com/obi1kenobi/trustfall
สิ่งแรกที่นึกได้คือ “เหมือน ระบบชนิดข้อมูล ที่ต้องแก้เองเลย”
ลองพยายามแทนมันแบบ naïve ใน TypeScript แต่
a,b,cสามารถเป็นค่าเดียวกันทั้งหมดได้ เลยแก้อะไรไม่ได้เลยพอพยายามแทนให้ถูกต้อง ก็ไปได้ถึงระดับที่ดูพอใช้งานได้บ้าง แต่ก็ยังต้องใช้ assertion และยังนิยามชนิดของ
Yไม่ได้อย่างถูกต้องกระบวนการนี้กลับยิ่งทำให้เห็นว่าโปรแกรมสไตล์ planner ทรงพลังแค่ไหนในแง่ความเรียบง่ายและความกระชับ
ดูเหมือน TypeScript จะยังไม่ทรงพอสำหรับการแสดง constraint ประเภทนี้
ลิงก์ TS Playground ที่ลองไว้: http://tinyurl.com/3p2pzdtn
ดีใจที่เห็น GOAP ถูกพูดถึงอีกครั้ง
มันคือซอสลับที่ทำให้ศัตรูใน F.E.A.R. สนุกขนาดนั้น และบทความของ Jeff Orkin ที่อธิบายวิธีทำงานก็อ่านง่ายและสนุกด้วย
ใช้ Prolog กับ CLPFD บางส่วนในงานจริงอยู่ และมันดีมาก
อยากให้มันมีอยู่ทุกที่ หรือพูดให้ชัดกว่านั้นคืออยากให้มีแกนตรรกะที่เน้นความบริสุทธิ์ แล้วผลักพฤติกรรมเชิงคำสั่งออกไปไว้ที่ขอบ
น่าเสียดายที่วงการเหมือนติดอยู่กับเครื่องมือแย่ ๆ แบบนี้
ดูคล้าย Prolog และน่าสนใจ
รูปแบบการเขียนโปรแกรมแบบนี้ไม่ได้ใหม่เสียทีเดียว
ตอนมหาวิทยาลัยเคยเรียน Prolog และมันก็ดูคล้ายกันมาก แต่ Prolog ไม่มีฟังก์ชัน planner
แต่ planner เป็นวิธีแก้ปัญหาที่ทั้งสง่างามและเรียบง่ายมาก
พอเห็นส่วนท้ายของบทความที่พูดถึงวิดีโอเกมก็ยิ่งสงสัย
ฟังก์ชัน planner ช่วยให้แก้ปัญหาได้ง่ายมากด้วยโค้ดชัดเจนไม่กี่บรรทัด แต่เมื่อเทียบกับอัลกอริทึมที่เขียนแบบ imperative แล้ว ประสิทธิภาพจะเป็นอย่างไร?
Picat ดูเหมือนมีประสิทธิภาพพอตัวเมื่อเทียบกับภาษาคล้ายกัน [1] แต่ยังหาเทียบกับภาษา “มาตรฐาน” ไม่เจอ
[1]: https://arxiv.org/abs/1405.2538
ผมเองก็มีความฝันที่จะบอกคอมพิวเตอร์แค่ว่าต้องไปถึงสถานะแบบไหน
ไม่ค่อยรู้จักชุมชน planning หรือตัวแก้มากนัก แค่เคยลองจับ ortools แบบ naïve แต่ก็เคยลองสร้างโค้ดที่พาจากสถานะหนึ่งไปยังสถานะเป้าหมายด้วย A*
มันสร้างคำสั่งแอสเซมบลีสำหรับเคลื่อนย้ายระหว่างสถานะ และยังหาการเปลี่ยนสถานะแฝงจากการเรียกฟังก์ชันเพื่อไปให้ถึงเป้าหมายด้วย
เพื่อให้ค้นหาเร็วขึ้นก็รันแบบขนานด้วย Python multiprocessing และใช้การสร้างเพื่อนบ้านแบบไดนามิกเพราะแต่ละเธรดสร้างเพื่อนบ้านไม่เหมือนกัน
ในความพยายามเดิม A* ขนานได้ยากเลยต้องทำ sharding
ความฝันจากการทดลองของผมคือ แค่บอกคอมพิวเตอร์ว่า “ตอนนี้มีอะไรอยู่” กับ “อยากได้อะไร” แล้วมันจะหาเส้นทางการย้ายที่ถูกต้องได้เอง
ส่วนตัวมองว่าการเขียนโปรแกรมคล้าย โลจิสติกส์ แบบ Factorio หรือโรงงานมากกว่า
เลยเรียกมันว่า “sliding puzzle” เพราะมันคือปริศนาที่ต้องขยับของไปมาเพื่อให้เห็นภาพที่ถูกต้อง
GitHub repository และบันทึก: https://github.com/samsquire/sliding-puzzle-codegen-memory
Replit: https://replit.com/@Chronological/SlidingPuzzle3