এই পর্বে যা শিখবেন
- সাধারণ search আর planning-এর পার্থক্য: কালো-বাক্স state বনাম fact দিয়ে গড়া factored state
- STRIPS-এ action লেখা, একটি action প্রয়োগ করা, closed-world assumption, আর PDDL-এ domain ও problem file লেখা ও চালানো
- Forward planning-কে BFS আর A* হিসেবে চালানো, backward planning-এর ধারণা, আর delete relaxation থেকে h_max, h_add ও relaxed-plan heuristic
- Sussman anomaly কেন লক্ষ্য একে একে পূরণ করার কৌশল ভাঙে, আর partial-order planning ও HTN-এর মূল ধারণা
- নিজের plan validator দিয়ে ভুল ধাপ ধরা, রাস্তা বন্ধ হলে replanning, আর language model-এর বানানো plan কেন যাচাই করতে হয়
মডিউল ৭-এ দেখেছেন, অনেক সমস্যাকে search হিসেবে লেখা যায়: শুরুর অবস্থা, action, লক্ষ্য আর খরচ (পর্ব ৩৯), তারপর BFS ও UCS দিয়ে অন্ধ search (পর্ব ৪০), আর heuristic দিয়ে A* (পর্ব ৪১)। সেখানে state ছিল একটি নাম, যেমন “মিরপুর”, আর অ্যালগরিদম তার ভেতরে তাকাত না। এই মডিউলের আগের পর্বগুলোতে (পর্ব ৪৫–৪৮) জ্ঞান লেখা হচ্ছে fact আর নিয়ম হিসেবে। Planning এই দুটি ধারা জোড়া দেয়: state লেখা হয় fact দিয়ে, আর action লেখা হয় fact বদলের নিয়ম দিয়ে, তারপর search চলে। এই পর্বের প্রশ্ন: এই কাঠামো থেকে আমরা কী বাড়তি সুবিধা পাই?
এক লাইনে ঝালাই: frontier হলো খুঁজে পাওয়া কিন্তু এখনো না-খোলা state-এর তালিকা; BFS তাকে queue হিসেবে রাখে, A* রাখে f = g + h অনুযায়ী সাজানো priority queue-তে। A*-এর heuristic h কখনো আসল বাকি খরচের বেশি না বললে (admissible) পাওয়া পথ সবচেয়ে কম খরচের হয়। BFS-এর ডেটা-স্ট্রাকচারের দিক DSA কোর্সের BFS আর graph পর্বে আছে।
১ · Planning কী — আর সাধারণ search থেকে কোথায় আলাদা
বাজারঘরের (এই কোর্সের কাল্পনিক অনলাইন দোকান) একজন ডেলিভারি রাইডার সকালে হাবে (hub, মানে দোকানের গুদাম) আছেন। আজ তিনটি পার্সেল: p1 আর p2 হাবেই আছে, p3 আছে শিমুল এলাকার একজন বিক্রেতার কাছে, সেখান থেকে তুলতে হবে। p1 যাবে কদমে, p2 জবায়, p3 বকুলে। রাইডার তিন রকম কাজ করতে পারেন: drive (এক এলাকা থেকে রাস্তা ধরে পাশের এলাকায় যাওয়া), pick (পার্সেল ব্যাগে তোলা) আর deliver (ঠিক এলাকায় পার্সেল দিয়ে দেওয়া)। নিচের ম্যাপের এলাকার নাম (ফুলের নামে) আর রাস্তা সব বানানো।
এটিকে পর্ব ৩৯-এর মতো search হিসেবে লেখা যায়: প্রতিটি state একটি নাম, যেমন “রাইডার বকুলে, p1 ব্যাগে, p2 হাবে, p3 শিমুলে”, আর একটি টেবিলে লেখা থাকে কোন state থেকে কোন action-এ কোথায় যাওয়া যায়। এমন state-কে বলে atomic: অ্যালগরিদমের কাছে এটি একটি কালো বাক্স, শুধু “সমান কি না” পরীক্ষা করা যায়। সমস্যা হলো, পার্সেল আর এলাকা বাড়লে state-এর সংখ্যা গুণ হারে বাড়ে। পাঁচটি এলাকা আর তিনটি পার্সেলে (প্রতিটি পার্সেল তিন অবস্থার একটিতে: জায়গায়, ব্যাগে, পৌঁছে গেছে) ৫ × ৩৩ = ১৩৫টি state; লেখকের যন্ত্রে শুরু থেকে BFS চালিয়ে ঠিক ১৩৫টি পৌঁছানো যায় এমন state পাওয়া গেছে। দশটি পার্সেলে সংখ্যাটি ৫ × ৩১০, প্রায় তিন লাখ। কেউ এই টেবিল হাতে লিখবে না।
Planning-এ state লেখা হয় factored representation-এ: কয়েকটি ছোট সত্য কথা, মানে fact-এর একটি সেট। শুরুর state:
rider(hub), at(p1,hub), at(p2,hub), at(p3,shimul),
road(hub,shimul), road(shimul,hub), … (১২টি road fact)
এখন action-কে টেবিলে না লিখে একটি নিয়মে লেখা যায়: “রাইডার যে এলাকায়, সেখানে পার্সেল থাকলে তা তোলা যায়; তুললে পার্সেলটি ব্যাগে, এলাকায় আর নেই।” এই এক নিয়ম সব state-এ খাটে। আর planner fact দেখতে পারে বলে নিজেই হিসাব করতে পারে “লক্ষ্যের কতগুলো fact বাকি”; এখান থেকেই স্বয়ংক্রিয় heuristic আসে (অংশ ৫)।
বিয়েবাড়ির রান্নার কথা ভাবুন। একজন অভিজ্ঞ বাবুর্চি প্রতিটি পদের জন্য জানেন “আগে কী লাগবে” (পেঁয়াজ কাটা থাকতে হবে, চুলা খালি থাকতে হবে), “করলে কী হবে” (ভাজা পেঁয়াজ তৈরি) আর “কী আর থাকবে না” (কাঁচা পেঁয়াজ শেষ, চুলা ব্যস্ত)। পুরো রান্নাঘরের সম্ভাব্য সব অবস্থার তালিকা তাঁর মাথায় নেই; আছে প্রতিটি কাজের এই তিনটি কথা। তা থেকেই তিনি ঠিক করেন কোন কাজ আগে। Planning ঠিক এই চিন্তাকে যন্ত্রে লেখে।
১৯৬০-এর দশকের শেষে Stanford Research Institute (SRI)-তে Shakey নামের একটি চলমান রোবট বানানো হচ্ছিল, যাকে ঘর থেকে ঘরে গিয়ে বাক্স ঠেলতে হতো। “কোন কাজ কোন ক্রমে” ঠিক করার জন্য Richard Fikes আর Nils Nilsson ১৯৭১ সালে Artificial Intelligence জার্নালে STRIPS (Stanford Research Institute Problem Solver) প্রকাশ করেন। মূল STRIPS-এ precondition-এর ব্যাপারে theorem proving ব্যবহার হতো; পরে “STRIPS” নামটি মূলত তার action লেখার সরল ধাঁচটির জন্য পরিচিত হয়ে যায়, যা এই পর্বে শিখবেন।
২ · STRIPS — precondition, add list আর delete list
STRIPS-এর জগতে তিনটি জিনিস:
- State: কয়েকটি ground atom-এর সেট। Ground atom মানে একটি predicate (সম্পর্কের নাম, যেমন
at), যার প্রতিটি জায়গায় নির্দিষ্ট বস্তু বসানো, কোনো চলক নেই:at(p3,shimul). পর্ব ৪৬-এর (First-order logic) ভাষায় এটি একটি ছোট, চলকহীন বাক্য। - Action: নাম, আর তিনটি সেট: pre (precondition: action চালাতে যা সত্য থাকতে হবে), add (action-এর পর যা সত্য হবে) আর del (যা আর সত্য থাকবে না)।
- Goal: যে fact-গুলো শেষে সত্য হতে হবে, যেমন
done(p1), done(p2), done(p3). বাকি fact যা খুশি হোক।
একটি action a একটি state s-এ প্রয়োগযোগ্য (applicable) যদি pre(a) ⊆ s, মানে precondition-এর প্রতিটি fact s-এ আছে (⊆ মানে “এর উপসেট”)। প্রয়োগের পর নতুন state:
s′ = (s − del(a)) ∪ add(a)
মানে আগে delete list-এর fact মুছে ফেলা, তারপর add list-এর fact যোগ করা (− মানে সেট থেকে বাদ, ∪ মানে দুই সেট মিলিয়ে)। ক্রমটি গুরুত্বপূর্ণ: কোনো fact দুই তালিকাতেই থাকলে শেষে তা সত্য থাকে।
| Action | pre (আগে সত্য) | add (সত্য হয়) | del (মিথ্যা হয়) |
|---|---|---|---|
drive(a,b) | rider(a), road(a,b) | rider(b) | rider(a) |
pick(p,x) | rider(x), at(p,x) | bag(p) | at(p,x) |
deliver(p,x) | rider(x), bag(p), x = p-এর ঠিকানা | done(p) | bag(p) |
টেবিলের a, b, p, x হলো চলক; এভাবে চলকসহ লেখা নিয়মকে বলে action schema. প্রতিটি চলকে নির্দিষ্ট বস্তু বসালে পাওয়া যায় একটি ground action, যেমন drive(hub,shimul). সব সম্ভাব্য বসানো তৈরি করাকে বলে grounding. নিচের stepper-এ রাইডার শিমুলে পৌঁছেছেন, ব্যাগে p1 আর p2; এবার pick(p3,shimul) প্রয়োগ।
1pick(p3, shimul)2pre: rider(shimul), at(p3,shimul)3del: at(p3,shimul)4add: bag(p3)5deliver(p3,bokul)? pre: rider(bokul), bag(p3)
| state (road বাদে) | |
| পরীক্ষা | |
| বাকি fact |
- শুরুর অবস্থা। রাইডার শিমুলে, ব্যাগে p1 আর p2, p3 এখনো বিক্রেতার কাছে শিমুলে। এর সঙ্গে ১২টি road fact আছে, যা কোনো action বদলায় না।
- Precondition পরীক্ষা। pick-এর দুটি শর্ত: rider(shimul) আর at(p3,shimul). দুটিই state-এ আছে, তাই pre ⊆ s, action প্রয়োগযোগ্য।
- Delete. delete list-এর at(p3,shimul) state থেকে মুছে ফেলা হলো: পার্সেলটি আর বিক্রেতার কাছে নেই।
- Add. add list-এর bag(p3) যোগ হলো। বাকি সব fact, যেমন bag(p1) আর road fact, হাত না দিয়েই আগের মতো রইল।
- একটি অচল action. এখন deliver(p3,bokul) চালানো যায় কি? bag(p3) আছে, কিন্তু rider(bokul) নেই (রাইডার শিমুলে)। একটি শর্ত না মিললেই action প্রয়োগযোগ্য নয়; আগে বকুলে যেতে হবে।
Closed-world assumption আর frame problem
State-এ rider(bokul) লেখা নেই। এর মানে কী: “রাইডার বকুলে নেই”, নাকি “জানা নেই”? STRIPS ধরে নেয় প্রথমটি: যে fact state-এ লেখা নেই, তা মিথ্যা। একে বলে closed-world assumption. রেলস্টেশনের যাত্রী-তালিকায় নাম না থাকলে টিকিট নেই ধরা হয়, ঠিক তেমন। এতে state ছোট থাকে, কারণ শুধু সত্য কথাগুলো লিখতে হয়। দাম হলো, “জানি না” বলার উপায় থাকে না; অনিশ্চয়তা নিয়ে কাজ মডিউল ৯-এর বিষয়।
Logic দিয়ে action লিখলে প্রতিটি action-এর জন্য বলতে হতো কী কী বদলায় না। এই সমস্যার নাম frame problem, যা John McCarthy আর Patrick Hayes ১৯৬৯ সালের একটি প্রবন্ধে তুলে ধরেন বলে পরিচিত। STRIPS এর একটি সরল সমাধান দেয়: add বা delete list-এ যা নেই, তা আপনা থেকেই আগের মতো থাকে।
STRIPS-এ একটি action-এর পুরো অর্থ তিনটি সেটে: pre ⊆ s হলে চালানো যায়, আর s′ = (s − del) ∪ add. এর বাইরে কিছু বদলায় না, আর যা লেখা নেই তা মিথ্যা। এই দুটি সরলীকরণই planning-কে হিসাবযোগ্য করেছে, আবার এগুলোই এর সীমা।
৩ · PDDL — domain file আর problem file
প্রতিটি planner নিজের ভাষায় সমস্যা নিলে একটি planner-এর জন্য লেখা সমস্যা আরেকটিতে চলত না, আর তুলনাও করা যেত না। ১৯৯৮ সালে প্রথম আন্তর্জাতিক planning প্রতিযোগিতার (AIPS-98) জন্য Drew McDermott-এর নেতৃত্বে একটি কমিটি PDDL (Planning Domain Definition Language) প্রস্তাব করে। পরে PDDL 2.1 (সময় আর সংখ্যা), PDDL 3-এর মতো সংস্করণ আসে; এই পর্বে শুধু STRIPS-অংশ আর type ব্যবহার হবে। PDDL লেখা হয় বন্ধনী-ভরা Lisp-এর মতো ধাঁচে, আর দুটি ফাইলে ভাগ হয়:
- Domain file: জগতের নিয়ম, যা সব দিনের জন্য এক: কোন ধরনের বস্তু (type), কোন predicate, আর কোন action schema.
- Problem file: আজকের নির্দিষ্ট কাজ: কোন বস্তু আছে, শুরুর অবস্থা (
:init) আর লক্ষ্য (:goal).
; Bazarghar delivery domain (fictional example)
(define (domain bazarghar-delivery)
(:requirements :strips :typing)
(:types area package)
(:predicates (rider ?a - area)
(at ?p - package ?a - area)
(bag ?p - package)
(done ?p - package)
(road ?from ?to - area)
(dest ?p - package ?a - area))
(:action drive
:parameters (?from ?to - area)
:precondition (and (rider ?from) (road ?from ?to))
:effect (and (rider ?to) (not (rider ?from))))
(:action pick
:parameters (?p - package ?a - area)
:precondition (and (rider ?a) (at ?p ?a))
:effect (and (bag ?p) (not (at ?p ?a))))
(:action deliver
:parameters (?p - package ?a - area)
:precondition (and (rider ?a) (bag ?p) (dest ?p ?a))
:effect (and (done ?p) (not (bag ?p)))))চলকের নাম ? দিয়ে শুরু, আর ?p - package মানে চলকটির type package. Effect-এ (not (rider ?from)) মানে delete list, বাকিগুলো add list. Python মডেলে deliver-এর ঠিকানা সরাসরি action-এর নামে বসানো ছিল; PDDL-এ তা একটি স্থির fact (dest ?p ?a), যা কোনো action বদলায় না।
(define (problem three-parcels)
(:domain bazarghar-delivery)
(:objects hub shimul bokul kadam joba - area
p1 p2 p3 - package)
(:init (rider hub)
(at p1 hub) (at p2 hub) (at p3 shimul)
(dest p1 kadam) (dest p2 joba) (dest p3 bokul)
(road hub shimul) (road shimul hub)
(road hub bokul) (road bokul hub)
(road shimul kadam) (road kadam shimul)
(road bokul kadam) (road kadam bokul)
(road kadam joba) (road joba kadam)
(road bokul joba) (road joba bokul))
(:goal (and (done p1) (done p2) (done p3))))PDDL-এর সাধারণ ভুল: delete effect ভুলে যাওয়া (drive-এ (not (rider ?from)) না লিখলে রাইডার একসঙ্গে দুই জায়গায় থেকে যাবেন), দুই-দিকের রাস্তার একটি দিক না লেখা, problem file-এর :domain নাম domain file-এর নামের সঙ্গে না মেলানো, আর :requirements-এ :typing না বলে type ব্যবহার। বেশিরভাগ planner ভুল ফাইলে স্পষ্ট বার্তা দেয় না; কখনো শুধু “no solution” বলে। তাই একটি ছোট, হাতে যাচাই করা problem দিয়ে শুরু করাই নিরাপদ।
সত্যিকারের run: একটি আসল PDDL planner
লেখকের যন্ত্রে একটি আলাদা virtual environment-এ (Python 3.14.4) দুটি pure-Python প্যাকেজ বসানো হয়েছে, লেখার সময়ের সর্বশেষ সংস্করণ: pyperplan 2.1 (Freiburg বিশ্ববিদ্যালয়ের গবেষকদের লেখা, পড়ানোর জন্য বানানো একটি ছোট planner) আর unified-planning 1.3.0 (ইউরোপীয় AIPlan4EU প্রকল্পের Python লাইব্রেরি, যা নানা planner-কে এক API-তে আনে), সঙ্গে এর pyperplan সংযোগ up-pyperplan 1.1.0. প্রথমে সরাসরি pyperplan, A* আর FF heuristic দিয়ে (অংশ ৫-এ এর মানে):
~/bazarghar-ai$ pip install pyperplan==2.1 unified-planning==1.3.0 up-pyperplan==1.1.0
~/bazarghar-ai$ PYTHONHASHSEED=0 pyperplan -s astar -H hff domain.pddl problem.pddl
INFO using search: astar_search
INFO using heuristic: hFFHeuristic
INFO Parsing Domain ~/bazarghar-ai/domain.pddl
INFO Parsing Problem ~/bazarghar-ai/problem.pddl
INFO 6 Predicates parsed
INFO 3 Actions parsed
INFO 8 Objects parsed
…
INFO 26 Variables created
INFO 30 Operators created
INFO Search start: three-parcels
INFO Initial h value: 10.000000
INFO Goal reached. Start extraction of solution.
INFO 11 Nodes expanded
INFO Search time: 0.0
INFO Plan length: 10
INFO validate could not be found on the PATH so the plan can not be validated.
~/bazarghar-ai$ cat problem.pddl.soln
(pick p1 hub)
(pick p2 hub)
(drive hub shimul)
(pick p3 shimul)
(drive shimul kadam)
(deliver p1 kadam)
(drive kadam bokul)
(deliver p3 bokul)
(drive bokul joba)
(deliver p2 joba)
pyperplan ফাইল দুটি পড়ে ৩০টি ground action (Operators) তৈরি করেছে, ১১টি state খুলে 10 ধাপের plan পেয়েছে। শেষ লাইনটি লক্ষ করুন: pyperplan plan যাচাইয়ের জন্য validate নামের একটি আলাদা প্রোগ্রাম (VAL, planning প্রতিযোগিতায় ব্যবহৃত plan validator) খোঁজে, যা এখানে বসানো ছিল না; তাই যাচাই হয়নি। যাচাই অংশ ৮-এ করা হবে। PYTHONHASHSEED=0 কেন, তা অংশ ৫-এ।
এবার unified-planning দিয়ে একই ফাইল: পড়া, দুটি engine দিয়ে সমাধান, আর লাইব্রেরির নিজের validator দিয়ে একটি ভুল plan ধরা।
from unified_planning.io import PDDLReader
from unified_planning.shortcuts import OneshotPlanner, PlanValidator, get_environment
from unified_planning.plans import SequentialPlan, ActionInstance
get_environment().credits_stream = None # ক্রেডিট-বার্তা বন্ধ
problem = PDDLReader().parse_problem("domain.pddl", "problem.pddl")
print(problem.name, "| actions:", [a.name for a in problem.actions])
for engine in ["pyperplan", "pyperplan-opt"]:
with OneshotPlanner(name=engine) as planner:
result = planner.solve(problem)
print(f"{engine}: {result.status.name}, {len(result.plan.actions)} steps")
for step in result.plan.actions:
print(" ", step)
act = {a.name: a for a in problem.actions}
obj = problem.object
bad = SequentialPlan([
ActionInstance(act["pick"], (obj("p1"), obj("hub"))),
ActionInstance(act["drive"], (obj("hub"), obj("kadam"))),
ActionInstance(act["deliver"], (obj("p1"), obj("kadam"))),
])
with PlanValidator(problem_kind=problem.kind) as v:
r1 = v.validate(problem, result.plan)
r2 = v.validate(problem, bad)
print("pyperplan plan:", r1.status.name)
print("hand-written plan:", r2.status.name, "|", r2.reason, "|", r2.inapplicable_action)~/bazarghar-ai$ python up_run.py
three-parcels | actions: ['drive', 'pick', 'deliver']
pyperplan: SOLVED_SATISFICING, 13 steps
pyperplan-opt: SOLVED_SATISFICING, 10 steps
pick(p1, hub)
pick(p2, hub)
drive(hub, shimul)
pick(p3, shimul)
drive(shimul, kadam)
deliver(p1, kadam)
drive(kadam, bokul)
deliver(p3, bokul)
drive(bokul, joba)
deliver(p2, joba)
pyperplan plan: VALID
hand-written plan: INVALID | FailedValidationReason.INAPPLICABLE_ACTION | drive(hub, kadam)
unified-planning-এর pyperplan engine-এর default হলো weighted A* আর h_add (প্যাকেজের কোডে দেখা): দ্রুত, কিন্তু 13 ধাপের plan, সেরা নয়। pyperplan-opt চলে A* আর LM-cut নামের admissible heuristic দিয়ে, 10 ধাপের সেরা plan দেয়; তবু status-এ SOLVED_SATISFICING, তাই status দেখে optimal ধরে নেওয়া ঠিক নয়। হাতে লেখা plan-টি (হাব থেকে সরাসরি কদম, যে রাস্তা নেই) validator দ্বিতীয় ধাপে ধরেছে।
৪ · Forward planning — planning আসলে search
STRIPS মডেল পেলে planning-কে পর্ব ৩৯-এর search সমস্যায় বদলানো সহজ: state হলো fact-এর সেট, শুরু S0, লক্ষ্য-পরীক্ষা হলো GOAL ⊆ s, আর একটি state-এর successor হলো সেই state-এ প্রয়োগযোগ্য প্রতিটি action প্রয়োগের ফল। শুরু থেকে সামনের দিকে এভাবে খোঁজাকে বলে forward planning বা progression. নিচের কোডে পুরো মডেল আর পর্ব ৪০-এর BFS, মাত্র কয়েক লাইনে। প্রথমবার চালালে ব্রাউজারে Python নামতে কয়েক সেকেন্ড লাগতে পারে।
from collections import deque
ROADS = [("hub", "shimul"), ("hub", "bokul"), ("shimul", "kadam"),
("bokul", "kadam"), ("kadam", "joba"), ("bokul", "joba")]
PKG = {"p1": ("hub", "kadam"), "p2": ("hub", "joba"), "p3": ("shimul", "bokul")}
AREAS = sorted({x for r in ROADS for x in r})
def act(name, pre, add, dele): # STRIPS action: (নাম, pre, add, del)
return (name, frozenset(pre), frozenset(add), frozenset(dele))
A = [act(f"drive({a},{b})", [f"rider({a})", f"road({a},{b})"], [f"rider({b})"],
[f"rider({a})"]) for a in AREAS for b in AREAS if a != b]
for p, (src, dst) in PKG.items():
A.append(act(f"pick({p},{src})", [f"rider({src})", f"at({p},{src})"],
[f"bag({p})"], [f"at({p},{src})"]))
A.append(act(f"deliver({p},{dst})", [f"rider({dst})", f"bag({p})"],
[f"done({p})"], [f"bag({p})"]))
S0 = frozenset(["rider(hub)"] + [f"at({p},{s})" for p, (s, _) in PKG.items()]
+ [f"road({a},{b})" for a, b in ROADS] + [f"road({b},{a})" for a, b in ROADS])
GOAL = frozenset(f"done({p})" for p in PKG)
def applicable(s, a): return a[1] <= s # সব precondition s-এ আছে?
def apply(s, a): return (s - a[3]) | a[2] # আগে delete, তারপর add
def bfs(s0, goal):
frontier, parent, expanded = deque([s0]), {s0: None}, 0
while frontier:
s = frontier.popleft(); expanded += 1
if goal <= s: # লক্ষ্যের সব fact সত্য?
plan = []
while parent[s]: s, name = parent[s]; plan.append(name)
return plan[::-1], expanded
for a in A:
if applicable(s, a) and (t := apply(s, a)) not in parent:
parent[t] = (s, a[0]); frontier.append(t)
plan, n = bfs(S0, GOAL)
print(len(A), "ground actions |", len(S0), "facts in S0 | expanded:", n)
for i, step in enumerate(plan, 1): print(f"{i:2}. {step}")আসল আউটপুট:
26 ground actions | 16 facts in S0 | expanded: 130
1. pick(p1,hub)
2. pick(p2,hub)
3. drive(hub,shimul)
4. pick(p3,shimul)
5. drive(shimul,kadam)
6. deliver(p1,kadam)
7. drive(kadam,bokul)
8. deliver(p3,bokul)
9. drive(bokul,joba)
10. deliver(p2,joba)
পাঁচটি এলাকার প্রতিটি ক্রমজোড়ায় একটি drive (৫ × ৪ = ২০টি), আর প্রতিটি পার্সেলে একটি pick ও একটি deliver: মোট ২৬টি ground action. যে drive-এর রাস্তা নেই (যেমন drive(hub,kadam)), সেটি তৈরি হলেও কখনো প্রয়োগযোগ্য হয় না, কারণ road(hub,kadam) fact state-এ নেই। শুরুর state-এ ১৬টি fact: রাইডার ১টি, পার্সেল ৩টি, রাস্তা ১২টি। BFS ১৩০টি state খুলে 10 ধাপের plan দিয়েছে, যা সবচেয়ে ছোট, কারণ প্রতিটি action-এর খরচ সমান হলে BFS সবচেয়ে কম ধাপের পথ দেয় (পর্ব ৪০)। Plan-টি মনের মতোও: হাবে দুটি পার্সেল তুলে শিমুল, সেখানে p3, তারপর কদম, বকুল, জবা। লক্ষ করুন, bfs function-এর কোথাও “ডেলিভারি” শব্দ নেই; এটি যেকোনো STRIPS মডেলে চলবে।
BFS-এর frontier প্রথম কয়েক ধাপে কীভাবে ভরে, দেখুন। লেখা সংক্ষেপ করতে state লেখা হয়েছে “রাইডারের এলাকা[ব্যাগের পার্সেল]” আকারে; জায়গায় থাকা পার্সেল আর road fact লেখা হয়নি।
1s = frontier.popleft()2if GOAL <= s: return plan3for a in A: if applicable(s, a):4 t = apply(s, a); নতুন হলে যোগ
| expand | |
| প্রয়োগযোগ্য | |
| নতুন state | |
| frontier |
- শুরু: hub[ ]. চারটি action প্রয়োগযোগ্য: দুটি রাস্তায় যাওয়া আর দুটি পার্সেল তোলা। চারটি নতুন state frontier-এ, কোডে action-এর ক্রম অনুযায়ী।
- bokul[ ]. খালি ব্যাগে বকুলে গিয়ে কিছু করার নেই, তবু BFS সব খোলে। হাবে ফেরা আগে দেখা state, তাই বাদ; জবা আর কদম নতুন।
- shimul[ ]. এখানে pick p3 প্রয়োগযোগ্য, তাই shimul[p3] নতুন। কদমে যাওয়া দেয় kadam[ ], যা আগের ধাপেই frontier-এ এসেছে।
- hub[p1]. ব্যাগে p1 নিয়ে তিনটি নতুন state. লক্ষ করুন, “রাইডার বকুলে” একই হলেও bokul[p1] আর bokul[ ] আলাদা state, কারণ fact-এর সেট আলাদা।
- hub[p2]. pick p1 দিলে hub[p1,p2], যা ধাপ ৪-এ তৈরি হয়েছে, তাই বাদ। এভাবে frontier ছড়াতে থাকে; ৫টি expand-এই frontier-এ ৮টি state.
- ১৩০তম expand-এ লক্ষ্য। রাইডার জবায়, তিনটি পার্সেলই পৌঁছে গেছে। মোট ১৩৫টি পৌঁছানোযোগ্য state-এর ১৩০টিই খুলতে হয়েছে: অন্ধ search প্রায় পুরো জগৎ ঘুরে দেখে।
Backward planning — লক্ষ্য থেকে পেছনে
উল্টো দিকেও খোঁজা যায়: লক্ষ্য থেকে শুরু করে প্রশ্ন করা, “শেষ কাজটি কী হতে পারত, আর তার আগে কী সত্য থাকতে হতো?” একে বলে backward planning বা regression. একটি লক্ষ্য-সেট g আর action a নিলে, a শেষ ধাপ হতে পারে যদি a লক্ষ্যের অন্তত একটি fact আনে (relevant) আর লক্ষ্যের কোনো fact মুছে না ফেলে (consistent). তখন আগের লক্ষ্য:
g′ = (g − add(a)) ∪ pre(a)
উদাহরণ: g = {done(p1), done(p2)}, a = deliver(p1,kadam). a আনে done(p1), আর g-এর কিছু মোছে না। তাই g′ = {done(p2)} ∪ {rider(kadam), bag(p1)} = {rider(kadam), bag(p1), done(p2)}: “শেষ কাজের ঠিক আগে রাইডার কদমে, p1 ব্যাগে, আর p2 আগেই পৌঁছে গেছে।” লেখকের যন্ত্রে এই হিসাব কোডে মেলানো হয়েছে।
সুবিধা: শুধু লক্ষ্যের সঙ্গে সম্পর্কিত action দেখা হয়। অসুবিধা: g′ পূর্ণ state নয়, আংশিক বর্ণনা, তাই আগে দেখা state চেনা আর heuristic বানানো কঠিন। বেশিরভাগ আধুনিক classical planner তাই ভালো heuristic-সহ forward search করে।
৫ · Planning-এর heuristic — delete list ভুলে যাওয়া
পর্ব ৪১-এ দেখেছেন, ভালো heuristic বানানোর একটি কৌশল হলো সমস্যার কিছু নিয়ম তুলে দিয়ে একটি সহজ সমস্যা, মানে relaxed problem, বানানো। ম্যাপের জন্য সেটি ছিল “রাস্তা নয়, সোজা উড়ে যাওয়া”। STRIPS-এ এই কৌশল সমস্যা না জেনেই, স্বয়ংক্রিয়ভাবে করা যায়, কারণ planner action-এর ভেতরটা দেখতে পায়। সবচেয়ে সরল রূপ:
- Goal-count: লক্ষ্যের কতগুলো fact এখনো সত্য নয়। শুরুতে done(p1), done(p2), done(p3) তিনটিই বাকি, তাই h = 3. সস্তা, কিন্তু খুব দুর্বল: “৩টি বাকি” বলে না যে প্রতিটির আগে ঘোরাঘুরি লাগবে।
- Delete relaxation: সব action-এর delete list ফেলে দেওয়া। তখন কোনো fact কখনো মিথ্যা হয় না: রাইডার শিমুলে গেলে হাবেও থেকে যান, একসঙ্গে সব জায়গায় ছড়িয়ে পড়েন। এই শিথিল জগতে plan খোঁজা অনেক সহজ, আর এর দৈর্ঘ্য আসল সমস্যার একটি অনুমান।
Delete relaxation-এর সেরা plan-এর দৈর্ঘ্য (h+) বের করাও কঠিন (NP-hard বলে প্রমাণিত), তাই planner আরও সস্তা অনুমান নেয়। প্রতিটি fact-এর একটি “খরচ” হিসাব করা হয়: state-এ থাকা fact-এর খরচ 0, আর একটি action-এর add-fact-এর খরচ = 1 + তার precondition-গুলোর খরচ মিলিয়ে। “মিলিয়ে” কথাটি দুইভাবে নেওয়া যায়:
- h_max: precondition-এর খরচের সর্বোচ্চটি নেওয়া। একটি action-এর জন্য কমপক্ষে তার সবচেয়ে কঠিন শর্তটি আনতে হবে, তাই এটি কখনো বাড়িয়ে বলে না (admissible), কিন্তু প্রায়ই অনেক কম বলে।
- h_add: খরচগুলো যোগ করা, যেন প্রতিটি শর্ত আলাদাভাবে আনতে হবে। এটি বেশি তথ্যপূর্ণ, কিন্তু যেখানে একই কাজ দুটি শর্তে লাগে (যেমন একবার শিমুলে গেলেই দুটি কাজ হয়), সেখানে দুবার গোনে, তাই বাড়িয়ে বলতে পারে।
- Relaxed-plan heuristic (h_FF): h_add-এর হিসাব থেকে প্রতিটি fact-এর “সেরা জোগানদার” action মনে রেখে, লক্ষ্য থেকে পেছনে হেঁটে একটি শিথিল plan তোলা, আর তার ভিন্ন action-গুলো গোনা। একই action দুবার গোনা হয় না, তাই h_add-এর দ্বিগুণ গোনার সমস্যা কমে। Jörg Hoffmann আর Bernhard Nebel ২০০১ সালে তাঁদের FF planner-এ এই ধারণা জনপ্রিয় করেন; h_add আর h_max-এর ধারণা এর আগে Blai Bonet আর Héctor Geffner-এর HSP planner-এর কাজে (১৯৯০-এর দশকের শেষ) এসেছিল।
হাতে হিসাব — শুরুর state-এ
| Fact | কোন action আনে | h_max খরচ | h_add খরচ |
|---|---|---|---|
| rider(hub), at(p1,hub), at(p2,hub), at(p3,shimul) | আগেই সত্য | 0 | 0 |
| rider(shimul), rider(bokul) | হাব থেকে drive | 1 | 1 |
| rider(kadam), rider(joba) | আরেকটি drive | 2 | 2 |
| bag(p1), bag(p2) | হাবে pick | 1 | 1 |
| bag(p3) | pick(p3,shimul), শর্ত rider(shimul) 1 + at 0 | 1 + max(1, 0) = 2 | 1 + 1 + 0 = 2 |
| done(p1) | deliver, শর্ত rider(kadam) 2 + bag(p1) 1 | 1 + 2 = 3 | 1 + 2 + 1 = 4 |
| done(p2) | deliver, শর্ত rider(joba) 2 + bag(p2) 1 | 3 | 4 |
| done(p3) | deliver, শর্ত rider(bokul) 1 + bag(p3) 2 | 1 + 2 = 3 | 1 + 1 + 2 = 4 |
লক্ষ্যের তিনটি fact মিলিয়ে: h_max = max(3, 3, 3) = 3, h_add = 4 + 4 + 4 = 12. আসল সেরা plan 10 ধাপ। h_max কম বলছে (নিরাপদ, কিন্তু দুর্বল), h_add বেশি বলছে (কারণ হাব থেকে বের হওয়ার মতো ভাগাভাগি করা কাজ বারবার গোনা)। Relaxed plan তোলা হয় এভাবে: তিনটি deliver, তিনটি pick, আর জোগানদার drive চারটি: হাব→শিমুল (p3-এর জন্য), হাব→বকুল, শিমুল→কদম, বকুল→জবা। মোট 10টি ভিন্ন action; লেখকের যন্ত্রে কোড দিয়ে মেলানো। শিথিল জগতে রাইডার যেন একসঙ্গে শিমুল আর বকুল দুই দিকেই গেছেন, তবু সংখ্যাটি এখানে ঘটনাক্রমে আসল 10-এর সমান। এই heuristic admissible নয়; অন্য state-এ এটি বেশিও বলতে পারে।
import heapq, itertools
def h_zero(s): return 0
def h_goalcount(s): return len(GOAL - s) # কতগুলো লক্ষ্য-fact বাকি
def h_relaxed(s): # delete list বাদ দিয়ে relaxed plan
cost, sup, changed = {f: 0 for f in s}, {}, True
while changed: # h_add-এর মতো খরচ ছড়ানো
changed = False
for a in A:
if all(p in cost for p in a[1]):
c = 1 + sum(cost[p] for p in a[1])
for f in a[2]:
if c < cost.get(f, 99): cost[f], sup[f], changed = c, a, True
used, todo = set(), [g for g in GOAL if g not in s]
while todo: # লক্ষ্য থেকে পেছনে supporter তোলা
a = sup[todo.pop()]
if a[0] not in used: used.add(a[0]); todo += [p for p in a[1] if p not in s]
return len(used)
def astar(s0, h):
tie = itertools.count()
frontier, g, closed = [(h(s0), 0, 0, s0)], {s0: 0}, set()
while frontier:
_, gs, _, s = heapq.heappop(frontier)
if s in closed: continue
closed.add(s)
if GOAL <= s: return gs, len(closed)
for a in A:
if applicable(s, a) and gs + 1 < g.get(t := apply(s, a), 99):
g[t] = gs + 1
heapq.heappush(frontier, (gs + 1 + h(t), gs + 1, next(tie), t))
print("heuristic h(S0) plan expanded")
for name, h in [("none (h=0)", h_zero), ("goal-count", h_goalcount),
("relaxed-plan", h_relaxed)]:
length, n = astar(S0, h)
print(f"{name:12} {h(S0):5} {length:5} {n:9}")আসল আউটপুট:
heuristic h(S0) plan expanded
none (h=0) 0 10 130
goal-count 3 10 123
relaxed-plan 10 10 27
এই block আগের block-এর A, S0, GOAL, applicable আর apply ব্যবহার করে (একই namespace; আগেরটি না চালিয়ে চালালে পাতা নিজেই আগে সেটি চালিয়ে নেয়)। h = 0 দিয়ে A* ১৩০টি state খোলে, BFS-এর সমান। Goal-count প্রায় কিছুই বাঁচায় না (১২৩টি), কারণ এর মান কোন দিকে যাওয়া ভালো তা বলে না। Relaxed-plan heuristic মাত্র ২৭টি খোলে, প্রায় পাঁচ ভাগের এক ভাগ, আর এখানে plan-ও 10 ধাপের সেরা। কিন্তু এটি admissible নয় বলে A*-এর optimality-র নিশ্চয়তা আর নেই; এখানে সেরা plan এসেছে, সবসময় আসবে এমন নয় (নিচের বড় পরীক্ষায় একটি ব্যতিক্রম দেখবেন)। itertools.count() সমান f-এ heap-এর tie ভাঙে, যাতে frozenset-কে তুলনা করতে না হয়।
সত্যিকারের run: pyperplan-এর heuristic, আর বড় সমস্যা
একই PDDL ফাইলে pyperplan-এর ছয় রকম সেটিং (লেখকের যন্ত্রে, প্রতিটি দুবার চালিয়ে একই ফল)। blind heuristic লক্ষ্যে 0 আর অন্য সব জায়গায় 1 দেয়; LM-cut একটি admissible heuristic, যা এখানে শুধু নামে রইল।
~/bazarghar-ai$ for c in "bfs blind" "astar blind" "astar hmax" "astar lmcut" "astar hff" "gbf hff"; do
> PYTHONHASHSEED=0 pyperplan -s ${c% *} -H ${c#* } domain.pddl problem.pddl 2>&1 | grep -E "h value|expanded|length"; done
h(S0) expanded plan
bfs (blind) – 129 10
astar blind 1 122 10
astar hmax 3 73 10
astar lmcut 9 12 10
astar hff 10 11 10
gbf hff 10 11 10
(log-এর লাইনগুলো একটি টেবিলে সাজানো; সংখ্যাগুলো হুবহু।) h_max-এর মান 3, হাতে হিসাবের সমান। PYTHONHASHSEED=0 কেন? Python প্রতিবার আলাদা hash দিয়ে set সাজায়, আর pyperplan-এর ভেতরে set-এর ক্রম tie ভাঙায় প্রভাব ফেলে। Seed ছাড়া কয়েকবার চালিয়ে BFS-এ ১২৯ বা ১৩০, আর greedy best-first (gbf)-এ ১১ থেকে ১৬টি expand ও 10 থেকে 13 ধাপের plan পাওয়া গেছে। তাই সংখ্যা তুলনার আগে seed স্থির করুন।
পার্সেল বাড়ালে কী হয়? লেখকের যন্ত্রে একই STRIPS মডেল, ৮টি এলাকা আর ১১টি রাস্তার একটি বড় ম্যাপে, ৩ থেকে ৬টি পার্সেল এলোমেলো তোলা ও পৌঁছানোর জায়গায় (seed 49), নিজের লেখা BFS আর A* দিয়ে (Python 3.14.4, শুধু standard library):
~/bazarghar-ai$ python bench.py
8 areas, 11 roads; parcels with random pickup/drop areas (seed 49)
parcels method plan expanded time s
3 BFS 14 204 0.00
3 A* goal-count 14 191 0.00
3 A* h_max 14 142 0.04
3 A* h_add 14 30 0.01
3 A* relaxed-plan 14 52 0.01
3 greedy relaxed-plan 16 20 0.00
4 BFS 15 630 0.00
4 A* goal-count 15 605 0.00
4 A* h_max 15 465 0.17
4 A* h_add 18 29 0.02
4 A* relaxed-plan 15 38 0.01
4 greedy relaxed-plan 17 19 0.00
5 BFS 18 1929 0.01
5 A* goal-count 18 1913 0.01
5 A* h_max 18 1736 0.70
5 A* h_add 23 40 0.03
5 A* relaxed-plan 18 149 0.03
5 greedy relaxed-plan 20 25 0.00
6 BFS 19 5764 0.04
6 A* goal-count 19 5138 0.04
6 A* h_max 19 4538 2.38
6 A* h_add 24 89 0.09
6 A* relaxed-plan 20 82 0.02
6 greedy relaxed-plan 27 31 0.01
Python 3.14.4
চারটি শিক্ষা। এক, BFS-এর কাজ প্রতি পার্সেলে প্রায় তিন গুণ বাড়ে (২০৪, ৬৩০, ১,৯২৯, ৫,৭৬৪), ঠিক ৩k-এর মতো। দুই, h_max admissible আর সব সময় সেরা plan দেয়, কিন্তু node কমায় অল্প, আর প্রতি node-এ হিসাব ভারী বলে মোট সময় BFS-এর চেয়েও বেশি (৬ পার্সেলে 2.38 s বনাম 0.04 s). তিন, h_add খুব কম node খোলে, কিন্তু বাড়িয়ে বলে বলে plan লম্বা হয় (৬ পার্সেলে 24, সেরা 19). চার, relaxed-plan heuristic মাঝামাঝি: ৫ পার্সেল পর্যন্ত সেরা plan, ৬-এ একটি বাড়তি ধাপ (20). Greedy সবচেয়ে কম node খোলে, plan সবচেয়ে লম্বা (27). এগুলো একটি ম্যাপের একটি run; অন্য সমস্যায় ক্রম বদলাতে পারে।
৬ · Sussman anomaly — যখন লক্ষ্যগুলো একে অন্যের পথ আটকায়
একটি সহজ কৌশল মনে আসতে পারে: লক্ষ্যগুলো একটি একটি করে পূরণ করা। আগে p1, তারপর p2. ডেলিভারিতে এটি মোটামুটি চলে, কারণ একটি পার্সেল পৌঁছে দিলে অন্যটির ক্ষতি হয় না। কিন্তু সব সমস্যা এমন নয়। Planning-এর সবচেয়ে বিখ্যাত উদাহরণ blocks world: টেবিলে কয়েকটি ব্লক, একটি রোবট-হাত একবারে একটি ব্লক তুলতে পারে। চারটি action: pickup(x) (টেবিল থেকে তোলা), putdown(x) (টেবিলে রাখা), stack(x,y) (x-কে y-এর ওপর রাখা) আর unstack(x,y) (y-এর ওপর থেকে x তোলা)। একটি ব্লকের ওপর কিছু থাকলে তা তোলা যায় না।
শুরু: A-র ওপরে C, আর B টেবিলে। লক্ষ্য: A থাকবে B-এর ওপরে, আর B থাকবে C-এর ওপরে, মানে নিচ থেকে C, B, A-র একটি স্তম্ভ। লক্ষ্য দুটি: on(A,B) আর on(B,C).
1কৌশল ১: আগে on(A,B), পরে on(B,C)2কৌশল ২: আগে on(B,C), পরে on(A,B)3কৌশল ৩: দুই লক্ষ্যের কাজ মিশিয়ে
| ব্লকের অবস্থা | |
| এই অংশের action | |
| on(A,B) │ on(B,C) | |
| মোট action |
- শুরু। A-র ওপরে C বসে আছে, B টেবিলে। দুটি লক্ষ্যই এখনো মিথ্যা।
- কৌশল ১, প্রথম লক্ষ্য। on(A,B)-এর জন্য A-কে খালি করতে C নামানো, তারপর A-কে B-এর ওপর: ৪টি action. প্রথম লক্ষ্য পূরণ। কিন্তু এখন B-এর ওপর A বসে আছে।
- কৌশল ১, দ্বিতীয় লক্ষ্য। on(B,C)-এর জন্য B তুলতে হবে, তাই আগে A নামাতে হয়: প্রথম লক্ষ্যটি নিজের হাতেই ভাঙতে হলো। ৮টি action পরে on(B,C) সত্য, on(A,B) মিথ্যা; আবার A তুলে রাখতে আরও ২টি, মোট ১০টি।
- কৌশল ২, প্রথম লক্ষ্য। উল্টো ক্রম: B-কে তুলে C-এর ওপর রাখা, মাত্র ২টি action. কিন্তু C তখনো A-র ওপরে, তাই A এখন তিন ব্লকের স্তম্ভের তলায় আটকে গেল।
- কৌশল ২, দ্বিতীয় লক্ষ্য। A-কে খালি করতে B আর C দুটিই নামাতে হয়, ফলে on(B,C) ভাঙে। ৬টি action পরে on(A,B) সত্য, on(B,C) মিথ্যা; আবার গড়তে আরও ৬টি, মোট ১৪টি।
- কৌশল ৩: কাজ মেশানো। প্রথম লক্ষ্যের প্রস্তুতি (C নামানো), তারপর দ্বিতীয় লক্ষ্য (B-কে C-এর ওপর), তারপর প্রথম লক্ষ্য শেষ (A-কে B-এর ওপর): মাত্র ৬টি action, কিছুই ভাঙতে হয় না। লেখকের যন্ত্রে BFS-ও ঠিক এই ৬ ধাপের plan দিয়েছে।
কোনো ক্রমেই “একটি লক্ষ্য পুরোপুরি শেষ করে তারপর পরেরটি, আগেরটি না ভেঙে” সম্ভব নয়। যেসব planner এভাবে লক্ষ্যগুলো আলাদাভাবে, একটি শেষ করে আরেকটি ধরে (non-interleaved বা linear planning), তারা এখানে হয় ব্যর্থ হয়, নয়তো অপ্রয়োজনে লম্বা plan দেয়। এই সমস্যাকে বলে Sussman anomaly. প্রচলিত বর্ণনা অনুযায়ী Gerald Sussman MIT-তে তাঁর PhD গবেষণায় (১৯৭০-এর দশকের শুরুতে; ১৯৭৫ সালে বই আকারে প্রকাশিত) এই ধরনের লক্ষ্য-সংঘাত চিহ্নিত করেন, আর নামটি পরে চালু হয়; কে প্রথম ঠিক এই তিন-ব্লক রূপটি লেখেন, তা নিয়ে সূত্রভেদে বর্ণনা কিছুটা আলাদা।
লক্ষ্যগুলো স্বাধীন নয়: একটির জন্য করা কাজ আরেকটির শর্ত নষ্ট করতে পারে। ভালো planner-কে তাই বিভিন্ন লক্ষ্যের কাজ মিশিয়ে (interleave করে) সাজাতে জানতে হয়। পুরো state ধরে forward search (অংশ ৪) এটি আপনা থেকেই করে, কারণ সে লক্ষ্য আলাদা করে দেখে না, পুরো state দেখে।
৭ · Partial-order planning আর HTN — দুটি ভিন্ন চিন্তা
Partial-order planning — যা আগে-পরে না হলেও চলে
রাইডারের 10 ধাপের plan-এ pick(p1,hub) আর pick(p2,hub)-এর মধ্যে কোনটি আগে, তাতে কিছু আসে যায় না; শুধু দুটিই হাব ছাড়ার আগে হতে হবে। একটি সম্পূর্ণ ক্রম (total order) বেঁধে দিলে planner অকারণে দুই রকম ক্রম আলাদা করে খোঁজে। Partial-order planning শুধু যেটুকু ক্রম দরকার, সেটুকু রাখে: “pick(p1,hub) আসবে drive(hub,shimul)-এর আগে”। প্রতিটি শর্ত কোন action থেকে আসছে, তা একটি causal link দিয়ে লেখা হয়: “pick(p1,hub) → bag(p1) → deliver(p1,kadam)”, মানে মাঝখানে কেউ bag(p1) মুছতে পারবে না। কোনো action এমন link ভাঙার হুমকি দিলে তাকে link-এর আগে বা পরে সরানো হয়।
শেষে যে plan আসে, তা action-এর একটি আংশিক ক্রম, একটি DAG (directed acyclic graph, চক্রহীন দিক-দেওয়া graph). তা থেকে যেকোনো topological sort একটি বৈধ ধারাবাহিক plan দেয়, আর যেসব action-এর মধ্যে ক্রম নেই, তা দুজন কর্মী সমান্তরালে করতে পারেন। Sussman anomaly-তে partial-order planner “C নামানো” কাজটিকে “B-কে C-এর ওপর রাখা”-র আগে বসাতে পারে, কারণ সে আগে থেকেই পুরো ক্রম বেঁধে দেয় না। Earl Sacerdoti-র NOAH (১৯৭৫) আর Austin Tate-এর NONLIN এ চিন্তার প্রথম দিকের উদাহরণ বলে পরিচিত। পরে heuristic-যুক্ত forward search প্রতিযোগিতায় দ্রুততর প্রমাণিত হয়, তবে সমান্তরাল কাজের planning-এ partial-order ধারণা এখনো কাজে লাগে।
HTN — বড় কাজকে ছোট কাজে ভাঙা
মানুষ সাধারণত “drive, pick, deliver” থেকে শুরু করে ভাবে না। বাজারঘরের ম্যানেজার ভাবেন: “আজকের সব অর্ডার পৌঁছাতে হবে”। তারপর সেটি ভাঙেন: প্রতিটি অর্ডারের জন্য “পার্সেল জোগাড়, তারপর পৌঁছানো”; “পৌঁছানো” মানে “ঠিকানায় যাওয়া, তারপর হাতে দেওয়া”; “যাওয়া” মানে রাস্তা ধরে কয়েকটি drive. Hierarchical task network (HTN) planning ঠিক এভাবে কাজ করে: লক্ষ্য দেওয়া হয় fact হিসেবে নয়, একটি কাজ (task) হিসেবে, আর domain-এ থাকে method: একটি বড় কাজ কোন কোন ছোট কাজে ভাঙা যায়।
deliver-all-orders
├── for each order p: serve(p)
│ serve(p) = get(p) → bring(p)
│ get(p) = go-to(src(p)) → pick(p, src(p)) # src = যেখানে পার্সেল আছে
│ bring(p) = go-to(dst(p)) → deliver(p, dst(p))
│ go-to(x) = [rider আগেই x-এ] অথবা drive(…) → go-to(x)
└── method-এর বিকল্প: একই এলাকার পার্সেল একসঙ্গে get, তারপর রুট ধরে bring
ডানদিকের ছোট কাজগুলো (pick, drive, deliver) primitive, মানে সরাসরি STRIPS action; বাকিগুলো compound. Method-এ বিশেষজ্ঞের জ্ঞান ঢোকে: “আগে সব তুলে নেওয়া, তারপর ঘোরা”। ফলে search অনেক ছোট হয়, আর plan মানুষের কাছে বোধগম্য হয়। দাম হলো, method লিখতে মানুষের পরিশ্রম লাগে, আর method-এ যে কৌশল নেই, planner তা খুঁজে পায় না। Dana Nau ও সহকর্মীদের SHOP2 (২০০৩-এর আশেপাশে) একটি পরিচিত HTN planner.
৮ · Plan যাচাই — আর language model-এর বানানো plan
Plan খোঁজা কঠিন, কিন্তু একটি দেওয়া plan ঠিক কি না, তা পরীক্ষা করা সহজ: শুরুর state থেকে প্রতিটি action একে একে প্রয়োগ, প্রতিবার আগে precondition পরীক্ষা, আর শেষে লক্ষ্য পরীক্ষা। এই প্রোগ্রামকে বলে plan validator. পরীক্ষার খাতা দেখার মতো: উত্তর লেখার চেয়ে প্রতিটি ধাপ মিলিয়ে দেখা অনেক দ্রুত। নিচের validator প্রথম ভুল ধাপটি আর কোন শর্ত মেলেনি তা বলে দেয়।
BY_NAME = {a[0]: a for a in A}
def validate(plan, s=S0):
for i, name in enumerate(plan, 1):
a = BY_NAME.get(name)
if a is None: return f"step {i}: unknown action {name}"
missing = a[1] - s # কোন precondition সত্য নয়
if missing: return f"step {i}: {name} INVALID, missing {sorted(missing)}"
s = apply(s, a)
left = GOAL - s
return "VALID plan" if not left else f"ends, but goal not reached: {sorted(left)}"
draft = ["pick(p1,hub)", "pick(p2,hub)", "drive(hub,shimul)", # হাতে লেখা খসড়া
"drive(shimul,kadam)", "deliver(p1,kadam)", "drive(kadam,bokul)",
"deliver(p3,bokul)", "drive(bokul,joba)", "deliver(p2,joba)"]
shortcut = ["pick(p1,hub)", "pick(p2,hub)", "drive(hub,kadam)"]
early = ["pick(p1,hub)", "drive(hub,bokul)", "drive(bokul,kadam)", "deliver(p1,kadam)"]
for label, p in [("BFS plan", plan), ("draft", draft), ("shortcut", shortcut),
("early stop", early)]:
print(f"{label:10} ({len(p)} steps): {validate(p)}")আসল আউটপুট:
BFS plan (10 steps): VALID plan
draft (9 steps): step 7: deliver(p3,bokul) INVALID, missing ['bag(p3)']
shortcut (3 steps): step 3: drive(hub,kadam) INVALID, missing ['road(hub,kadam)']
early stop (4 steps): ends, but goal not reached: ['done(p2)', 'done(p3)']
BFS-এর plan বৈধ। হাতে লেখা “draft” দেখতে বিশ্বাসযোগ্য: ৯টি ধাপ, সেরা plan-এর চেয়েও একটি কম। কিন্তু শিমুলে গিয়ে p3 তোলা হয়নি, তাই ৭ নম্বর ধাপে deliver(p3,bokul)-এর শর্ত bag(p3) মেলেনি। “shortcut” ধরে নিয়েছে হাব থেকে কদমে রাস্তা আছে; validator ঠিক কোন fact (road(hub,kadam)) নেই তা দেখায়। “early stop”-এর প্রতিটি ধাপ বৈধ, কিন্তু শেষে দুটি লক্ষ্য বাকি। তিনটি ভুল তিন রকম: action-এর শর্ত বাদ পড়া, জগৎ সম্পর্কে ভুল ধারণা, আর অসম্পূর্ণ plan. দেখতে ঠিক লাগা আর আসলে ঠিক হওয়ার পার্থক্য এখানেই।
Language model আর planning — সাবধানী একটি নোট
বড় language model (LLM) থেকে স্বাভাবিক ভাষায় “রাইডারের রুট সাজিয়ে দাও” বললে প্রায়ই সুন্দর, যুক্তিসংগত দেখতে একটি তালিকা আসে। সমস্যা হলো, model পরের শব্দ অনুমান করে লেখে; প্রতিটি ধাপের শর্ত মিলিয়ে দেখার কোনো অন্তর্নির্মিত নিশ্চয়তা তার নেই। ২০২৩ সালের আশেপাশে Karthik Valmeekam, Subbarao Kambhampati ও সহকর্মীদের কয়েকটি গবেষণা (যেমন PlanBench নামের একটি benchmark) blocks world-এর মতো PDDL সমস্যায় দেখিয়েছে যে তখনকার model-গুলোর অনেক plan যাচাই করলে অবৈধ হয়, আর বস্তুর নাম বদলে দিলে ফল আরও খারাপ হয়। পরের model-গুলোর উন্নতির প্রতিবেদনও আছে, আর বিতর্ক চলছে; এখানে কোনো model-এর সংখ্যা দেওয়া হচ্ছে না।
যে বিষয়ে বেশিরভাগ গবেষক একমত, তা হলো যাচাইয়ের ভূমিকা। Kambhampati-র দল “LLM-Modulo” নামে একটি ধাঁচ প্রস্তাব করেছে: model খসড়া দেয়, একটি আলাদা যাচাইকারী (validator বা classical planner) প্রতিটি ধাপ পরীক্ষা করে, ভুল পেলে ভুলের কারণসহ model-কে ফেরত পাঠায়। আরেকটি পথ হলো model দিয়ে স্বাভাবিক ভাষার বর্ণনা থেকে PDDL লেখানো, তারপর আসল planner দিয়ে সমাধান; সেখানেও model-এর লেখা PDDL ভুল হতে পারে (অনুশীলন ৩)। মোটকথা: LLM খসড়া দিতে পারে, কিন্তু বৈধতার নিশ্চয়তা দেয় validator; এটি পর্ব ৪৭-এর guardrail-এর ধারণার সঙ্গেও মেলে।
আরও গভীরে: STRIPS planning-এ একটি plan আছে কি না, এই প্রশ্নটি সাধারণভাবে PSPACE-complete, যা Tom Bylander ১৯৯৪ সালে দেখান; অর্থাৎ সবচেয়ে খারাপ ক্ষেত্রে plan-এর দৈর্ঘ্যই state-এর সংখ্যার মতো বিশাল হতে পারে। উল্টো দিকে, একটি দেওয়া plan যাচাই করা plan-এর দৈর্ঘ্যের সমানুপাতিক সময়ে হয়: এই অসমতাই LLM-Modulo বা “generate, then verify” ধাঁচকে যুক্তিসংগত করে। Planning-কে SAT সমস্যায় বদলে সমাধানের ধারা (Kautz ও Selman, ১৯৯২ থেকে), আর Blum ও Furst-এর Graphplan (১৯৯৫) আরও দুটি বড় ধারণা। আধুনিক প্রতিযোগিতার শক্তিশালী planner-গুলোর অনেকগুলো Malte Helmert-এর Fast Downward (২০০৬) কাঠামোর ওপর গড়া।
৯ · চলার সময় নজরদারি আর replanning — রাস্তা বন্ধ হলে
Classical planning কয়েকটি বড় অনুমান করে: জগৎ পুরোপুরি জানা, প্রতিটি action-এর ফল নিশ্চিত, আর planner ছাড়া কেউ জগৎ বদলায় না। ঢাকার রাস্তায় এর কোনোটিই সবসময় সত্য নয়: মিছিল, খোঁড়াখুঁড়ি, বৃষ্টিতে জলাবদ্ধতা। তাই plan চালানোর সময় দুটি অভ্যাস দরকার। Execution monitoring: প্রতিটি action চালানোর আগে আসল জগতে তার precondition আবার পরীক্ষা করা। আর কোনো শর্ত না মিললে replanning: বর্তমান অবস্থা থেকে নতুন plan বানানো। নিচের কোডে রাইডার শিমুলে p3 তোলার পর খবর আসে, শিমুল–কদম রাস্তা বন্ধ।
def run(world, close_after, closed_road):
todo, steps, closed = bfs(world, GOAL)[0], 0, False
print("plan:", len(todo), "steps")
while todo:
if steps == close_after and not closed: # বাস্তব জগৎ বদলে গেল
a, b = closed_road; closed = True
world = world - {f"road({a},{b})", f"road({b},{a})"}
print(f" ! road {a}-{b} closed")
name = todo[0]
if not applicable(world, BY_NAME[name]): # execution monitoring
where = next(f for f in world if f.startswith("rider"))
print(f" x {name} cannot run; replan from {where}")
todo = bfs(world, GOAL)[0]
print(" new plan:", len(todo), "steps")
continue
world = apply(world, BY_NAME[name]); todo = todo[1:]; steps += 1
print(f"{steps:3}. {name}")
print("all delivered:", GOAL <= world, "| total steps:", steps)
run(S0, close_after=4, closed_road=("shimul", "kadam"))আসল আউটপুট:
plan: 10 steps
1. pick(p1,hub)
2. pick(p2,hub)
3. drive(hub,shimul)
4. pick(p3,shimul)
! road shimul-kadam closed
x drive(shimul,kadam) cannot run; replan from rider(shimul)
new plan: 7 steps
5. drive(shimul,hub)
6. drive(hub,bokul)
7. deliver(p3,bokul)
8. drive(bokul,joba)
9. deliver(p2,joba)
10. drive(joba,kadam)
11. deliver(p1,kadam)
all delivered: True | total steps: 11
প্রথম চারটি ধাপ মূল plan মতো। তারপর আসল জগৎ থেকে দুটি road fact মুছে যায়, কিন্তু planner-এর হাতের plan বদলায়নি। পরের action drive(shimul,kadam) চালানোর আগে নজরদারি দেখে road(shimul,kadam) আর সত্য নয়, তাই সেটি চালানো হয় না। নতুন BFS বর্তমান state থেকে (রাইডার শিমুলে, ব্যাগে তিনটি পার্সেল) 7 ধাপের plan দেয়: হাবে ফিরে বকুল, জবা, তারপর কদম। মূল plan-এর বাকি অংশ ছিল 6 ধাপ, তাই রাস্তা বন্ধের দাম একটি বাড়তি ধাপ, মোট 11. নজরদারি না থাকলে রাইডার বন্ধ রাস্তায় গিয়ে আটকে যেতেন। আগের block-গুলো না চালালেও পাতা সেগুলো আগে চালিয়ে নেয়।
এখানে replanning শুরু থেকে নতুন search করেছে; ছোট সমস্যায় সেটি যথেষ্ট। বড় সমস্যায় শুধু ভাঙা অংশ মেরামত (plan repair) বা D* Lite-এর মতো incremental search বেশি কার্যকর।
অনিশ্চয়তা — সামনের মডিউলে
যদি রাস্তা বন্ধ হওয়ার সম্ভাবনা আগে থেকে জানা থাকে, ধরুন বর্ষায় কোনো রাস্তা প্রায়ই ডোবে? তখন একটি plan নয়, প্রতিটি পরিস্থিতিতে কী করতে হবে তার নিয়ম (policy) আর প্রত্যাশিত লাভ-ক্ষতির হিসাব লাগে। এটি মডিউল ৯-এর বিষয়: পর্ব ৫০ (Bayes-এর উপপাদ্য) থেকে পর্ব ৫৪ (অনিশ্চয়তায় সিদ্ধান্ত — expected utility আর MDP-এর পরিচয়) পর্যন্ত।
বাস্তবে planning কোথায়
- মহাকাশ: ১৯৯৯ সালে NASA-র Deep Space 1 মহাকাশযানে Remote Agent নামের একটি পরীক্ষামূলক ব্যবস্থা কিছু সময় যানটিকে নিজে পরিকল্পনা করে চালিয়েছিল বলে NASA-র প্রকাশিত বর্ণনায় পাওয়া যায়।
- গেম: ২০০৫ সালের ভিডিও গেম F.E.A.R.-এ Jeff Orkin STRIPS-ধাঁচের planning (Goal-Oriented Action Planning) দিয়ে শত্রু চরিত্রের আচরণ বানিয়েছিলেন বলে তাঁর প্রকাশিত লেখায় বর্ণনা আছে; পরে আরও কিছু গেমে এর প্রভাব দেখা যায়।
- রোবট আর গুদাম: রোবটিক্সের কিছু গবেষণা প্রকল্পে কাজের ক্রম ঠিক করতে PDDL planner ব্যবহার হয়; কোন বাণিজ্যিক প্রতিষ্ঠান কী ব্যবহার করে, তা সাধারণত প্রকাশিত নয়।
পরীক্ষা ও ইন্টারভিউতে (বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এ ধরনের প্রশ্ন সাধারণ):
- STRIPS action-এর তিনটি অংশ? Precondition, add list, delete list; প্রয়োগের ফল s′ = (s − del) ∪ add.
- Closed-world assumption কী? State-এ যে fact লেখা নেই, তা মিথ্যা ধরা হয়।
- Forward আর backward planning-এর পার্থক্য? Forward শুরু থেকে action প্রয়োগ করে সামনে যায়; backward লক্ষ্য থেকে g′ = (g − add) ∪ pre দিয়ে পেছনে যায়।
- h_max আর h_add কোনটি admissible? h_max; h_add একই কাজ একাধিকবার গুনে বাড়িয়ে বলতে পারে।
- Sussman anomaly কী দেখায়? লক্ষ্য একে একে পূরণ করলে একটি লক্ষ্যের কাজ আরেকটি ভাঙে; কাজ মিশিয়ে সাজাতে হয়।
১০ · AI-এর যুগে — AI সহকারীর লেখা PDDL আর planner যাচাই
AI সহকারী দ্রুত পরিচ্ছন্ন দেখতে PDDL বা planner কোড দেয়, কিন্তু planning-এ একটি ছোট ভুল পুরো ফল উল্টে দেয়, আর তা সহজে চোখে পড়ে না। এ বিষয়ে AI সহকারীরা প্রায়ই যা করে:
- Delete effect ভুলে যাওয়া: drive-এ
(not (rider ?from))না লিখলে planner “সব জায়গায় একসঙ্গে থাকা” রাইডারের ছোট, অসম্ভব plan দেয়। - প্রয়োগের ক্রম উল্টানো:
(s | add) - delলেখা, যাতে একই fact দুই তালিকায় থাকলে ভুল ফল আসে। - লাইব্রেরির বানানো API: unified-planning বা pyperplan-এর না-থাকা function বা পুরোনো সংস্করণের নাম; নিজের সংস্করণের docs-এ মিলিয়ে না দেখলে ধরা পড়ে না।
- Optimality-র ভুল দাবি: greedy বা inadmissible heuristic-এর plan-কে “সবচেয়ে ছোট” বলা, বা status-এর লেখা না বুঝে দাবি করা।
- বানানো সংখ্যা: না চালিয়েই “এই heuristic ১০ গুণ দ্রুত” লেখা; সংখ্যা seed আর সমস্যার ওপর নির্ভর করে।
প্রম্পটে মডেল, যাচাইয়ের উপায় আর সংস্করণ নিজেই বলে দিন। নিচের প্রম্পটে কোনো আসল ক্রেতার ঠিকানা নেই, শুধু বানানো এলাকা।
AI-এর কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১STRIPS-এর জন্ম একটি রোবট প্রকল্পে; তার সরল ধাঁচটি পঞ্চাশ বছর পরেও কেন টিকে আছে, আর কোথায় এটি যথেষ্ট নয়?
১৯৭১ সালে Fikes আর Nilsson-এর সমস্যা ছিল খুব বাস্তব: Shakey রোবটকে বলতে হবে “বাক্সটি পাশের ঘরে ঠেলে নাও”, আর রোবটকে নিজে ঠিক করতে হবে কোন দরজা দিয়ে, কোন ক্রমে। সে সময়ের একটি জনপ্রিয় ধারা ছিল পুরো জগৎকে logic-এ লিখে theorem prover দিয়ে plan প্রমাণ করা, কিন্তু তাতে frame problem বড় বাধা হয়ে দাঁড়ায়: প্রতিটি action কী কী বদলায় না, সেটাও লিখতে হয়। STRIPS-এর মূল চাল ছিল একটি প্রকৌশলগত সরলীকরণ: শুধু যা বদলায় তা লেখা (add ও delete list), আর বাকি সব আগের মতো ধরা।
এই সরলতাই এর দীর্ঘায়ুর কারণ। প্রথমত, এটি পড়তে ও লিখতে সহজ; একজন নতুন শিক্ষার্থী এক বিকেলে ডেলিভারি domain লিখতে পারেন। দ্বিতীয়ত, এর গঠন এত নিয়মিত যে planner নিজেই এ থেকে heuristic বানাতে পারে: delete list ফেলে দেওয়ার কৌশল (অংশ ৫) শুধু STRIPS-এর মতো স্পষ্ট গঠনেই সম্ভব। তৃতীয়ত, ১৯৯৮ থেকে PDDL একটি অভিন্ন ভাষা দিয়েছে, যা দিয়ে আন্তর্জাতিক planning প্রতিযোগিতায় বিভিন্ন planner একই সমস্যায় তুলনা করা যায়; প্রতিযোগিতা গবেষণাকে দ্রুত এগিয়েছে।
সীমাও স্পষ্ট। STRIPS-এ সময় নেই, সংখ্যা নেই (জ্বালানি, ব্যাগের ওজন), অনিশ্চয়তা নেই, আর অন্য কোনো কর্তা নেই। PDDL-এর পরের সংস্করণে সময় আর সংখ্যা এসেছে, কিন্তু planner-এর কাজ তাতে অনেক কঠিন হয়। ব্যাগের ধারণক্ষমতার মতো সংখ্যাও STRIPS-এ load(0), load(1)-এর মতো কৃত্রিম fact দিয়ে লিখতে হয় (অনুশীলন ৪)। আর closed-world assumption “জানি না” বলতে দেয় না, অথচ রোবটের সেন্সর প্রায়ই নিশ্চিত নয়।
মূল উপলব্ধি: STRIPS টিকে আছে কারণ এটি যতটুকু সম্ভব কম বলে: শুধু কী বদলায়। এই সংযমই একে সহজ, প্রমিত আর স্বয়ংক্রিয় heuristic-এর উপযোগী করেছে, আবার এই সংযমের কারণেই সময়, সংখ্যা আর অনিশ্চয়তার জগতে এর বাইরে যেতে হয়।
প্র ০২“Delete list ভুলে যাওয়া” এমন একটি অবাস্তব জগৎ বানায় যেখানে রাইডার একসঙ্গে সব জায়গায় থাকেন। তবু এটি কেন planning-এর সবচেয়ে সফল heuristic ধারণাগুলোর একটি?
একটি heuristic-এর কাছে আমরা দুটি জিনিস চাই: সস্তায় হিসাব, আর আসল খরচের ভালো অনুমান। Delete relaxation প্রথমটি দেয়, কারণ delete না থাকলে জগৎ শুধু বড় হয়: একবার সত্য হওয়া fact কখনো মিথ্যা হয় না। ফলে কোন fact কত তাড়াতাড়ি আনা যায়, তা এক দফা “ছড়ানো” হিসাবে বের হয়, search ছাড়াই। অংশ ৫-এর কোডে এটি মাত্র কয়েকটি loop.
দ্বিতীয়টি, ভালো অনুমান, কিছুটা আশ্চর্যজনকভাবে আসে। শিথিল plan-এও প্রতিটি লক্ষ্যের জন্য প্রয়োজনীয় কাজগুলো থাকে: p3-এর জন্য শিমুলে যাওয়া, তোলা, বকুলে যাওয়া, পৌঁছানো। যা হারায় তা হলো “একই সময়ে দুই জায়গায় থাকা যায় না” ধরনের সংঘাত। অনেক বাস্তব domain-এ, যেমন লজিস্টিকস, এই সংঘাতই খরচের ছোট অংশ, তাই অনুমান কাছাকাছি থাকে। অংশ ৫-এর শুরুর state-এ relaxed plan 10, আসল plan-ও 10. বড় পরীক্ষায় relaxed-plan heuristic BFS-এর চেয়ে প্রায় ৭০ গুণ কম node খুলেছে (৬ পার্সেলে ৫,৭৬৪ বনাম ৮২), একটি run-এ।
ইতিহাসও সাক্ষী। Bonet আর Geffner-এর HSP দেখায়, h_add-এর মতো সরল অনুমানও ১৯৯০-এর দশকের শেষে প্রতিযোগিতায় ভালো করতে পারে। Hoffmann আর Nebel-এর FF (২০০১) relaxed plan গুনে আরও ভালো অনুমান দেয়, আর enforced hill-climbing-এর সঙ্গে মিলিয়ে তখনকার প্রতিযোগিতায় খুব সফল হয় বলে পরিচিত।
দুর্বলতাও আছে। যে domain-এ সম্পদ সীমিত (একটি হাত, একটি ব্যাগ), সেখানে delete-এর প্রভাবই আসল কঠিনতা, তাই শিথিল জগৎ ভুল পথ দেখায়; গবেষকেরা কিছু delete ফিরিয়ে আনার উপায়ও খুঁজেছেন।
মূল উপলব্ধি: একটি heuristic-কে বাস্তব হতে হয় না, শুধু দরকারি কাজের পরিমাণ মোটামুটি ধরতে হয়; delete relaxation এটি সস্তায় করে, আর STRIPS-এর স্পষ্ট গঠন এটিকে যেকোনো domain-এ স্বয়ংক্রিয় করে।
প্র ০৩Sussman anomaly-র মতো তিন ব্লকের একটি খেলনা সমস্যা কেন planning-এর ইতিহাসে এত গুরুত্বপূর্ণ হয়ে উঠল?
১৯৭০-এর দশকের শুরুতে অনেক planner একটি স্বাভাবিক ধারণায় চলত: জটিল লক্ষ্যকে ছোট লক্ষ্যে ভাঙা, প্রতিটি আলাদা সমাধান করা, তারপর জোড়া দেওয়া। মানুষও প্রায়ই এভাবে ভাবে। এই ধারণার পেছনে একটি অনুচ্চারিত অনুমান ছিল: লক্ষ্যগুলো একে অন্যের ক্ষতি করে না। তিন ব্লকের সমস্যাটি এই অনুমানকে সবচেয়ে ছোট, সবচেয়ে স্পষ্টভাবে ভাঙে। দুটি লক্ষ্যের যেকোনো ক্রমেই প্রথমটি দ্বিতীয়টির পথ আটকায় (অংশ ৬-এর stepper: ১০ বা ১৪ action, অথচ সেরা ৬)।
Gerald Sussman-এর গবেষণায় একটি প্রোগ্রাম ভুল থেকে শিখে plan সংশোধন করত বলে বর্ণনা আছে; লক্ষ্য-সংঘাত সেখানে একটি কেন্দ্রীয় সমস্যা ছিল। এর জবাবে কয়েকটি ধারা আসে। একটি হলো লক্ষ্য রক্ষা করে পুনর্বিন্যাস: কোনো কাজ আগের লক্ষ্য ভাঙলে কাজটিকে আগে সরানো। আরেকটি, Sacerdoti-র NOAH আর পরে Tate-এর NONLIN, action-এর ক্রম যতটা সম্ভব খোলা রাখে, শুধু দরকারি আগে-পরে বেঁধে দেয়: partial-order planning-এর বীজ (অংশ ৭)। খেলনা সমস্যাটি তাই নতুন অ্যালগরিদমের পরীক্ষার মাপকাঠি হয়ে ওঠে: কোনো নতুন পদ্ধতি এটি ঠিকভাবে সমাধান করতে না পারলে তার দুর্বলতা স্পষ্ট।
আধুনিক দৃষ্টিতে সমস্যাটির শিক্ষা আরও সাধারণ। পুরো state ধরে forward search কোনো লক্ষ্য আলাদা করে দেখে না, তাই anomaly তাকে বিভ্রান্ত করে না; কিন্তু goal-count-এর মতো heuristic, যা লক্ষ্যগুলোকে স্বাধীন ধরে গোনে, একই ভুল ধারণা বহন করে। শুরুতে goal-count 2, আর B-কে C-এর ওপর রাখার পর 1: heuristic বলে “এগোচ্ছি”, অথচ সেখান থেকে দুটি লক্ষ্যে পৌঁছাতে সেরা পথেও আরও ৮টি action লাগে (লেখকের যন্ত্রে BFS দিয়ে মেলানো), শুরু থেকে যেখানে ৬টিই যথেষ্ট। অর্থাৎ লক্ষ্য-সংঘাত শুধু পুরোনো planner-এর সমস্যা নয়, এখনকার heuristic-এর ভেতরেও লুকিয়ে থাকে।
দৈনন্দিন উদাহরণ: ঘর রং করার আগে নতুন আসবাব বসালে রঙের সময় আবার সরাতে হয়; প্রকল্প পরিকল্পনাতেও (project planning) এই চিন্তা লাগে।
মূল উপলব্ধি: একটি ছোট, স্পষ্ট পাল্টা-উদাহরণ একটি পুরো যুগের অনুমান ভাঙতে পারে; Sussman anomaly দেখায় লক্ষ্যগুলো স্বাধীন নয়, আর এই শিক্ষা আজও heuristic আর পরিকল্পনার নকশায় প্রাসঙ্গিক।
প্র ০৪Language model যদি স্বাভাবিক ভাষায় ভালো plan লিখতে পারে, তাহলে PDDL আর classical planner-এর দরকার কি ফুরিয়ে যাচ্ছে?
প্রশ্নটি গবেষকদের মধ্যে এখন সক্রিয় বিতর্কের বিষয়, তাই এক কথায় উত্তর নেই। দুই দিকের যুক্তি দেখা যাক। LLM-এর পক্ষে: এটি স্বাভাবিক ভাষার অস্পষ্ট নির্দেশ (“জরুরি অর্ডার আগে, তবে বৃষ্টিতে নদীর ধারের রাস্তা এড়িয়ে”) বুঝতে পারে, সাধারণ জ্ঞান কাজে লাগায়, আর domain file না লিখেই শুরু করতে দেয়। Classical planner-এর জন্য কাউকে আগে নিখুঁত PDDL লিখতে হয়, যা খরচসাপেক্ষ: পর্ব ৪৭-এর knowledge acquisition bottleneck-এর মতোই।
Classical planner-এর পক্ষে: এর উত্তর প্রমাণযোগ্য। Planner যদি plan দেয়, তা মডেলের নিয়মে বৈধ; optimal planner বললে সেটি সবচেয়ে ছোট। ২০২৩-এর আশেপাশের PlanBench-ধরনের গবেষণায় দেখা গেছে, তখনকার LLM-গুলো blocks world-এর মতো সরল domain-এও অনেক অবৈধ plan দেয়, আর বস্তুর নাম অর্থহীন শব্দে বদলালে ফল আরও খারাপ হয়, যা থেকে কিছু গবেষক বলেন model আংশিকভাবে পরিচিত ধাঁচ মনে রেখে উত্তর দেয়। পরের model-গুলোর উন্নতির প্রতিবেদনও আছে; কিন্তু কোনো model নিজে থেকে বৈধতার গ্যারান্টি দেয় না।
তাই অনেকে দুটিকে মেলানোর পক্ষে। এক, LLM দিয়ে স্বাভাবিক ভাষা থেকে PDDL-এর খসড়া, মানুষ বা validator দিয়ে যাচাই, তারপর planner. দুই, LLM-Modulo: LLM plan প্রস্তাব করে, validator ভুল ধরে ফেরত দেয়, যতক্ষণ না বৈধ হয়। অংশ ৮-এর validator ঠিক এই কাজের ছোট রূপ: সে ৯ ধাপের বিশ্বাসযোগ্য খসড়ার ৭ নম্বর ধাপে ভুল ধরেছে। ঝুঁকিপূর্ণ ক্ষেত্রে (ওষুধের ডোজ, যন্ত্রের নিয়ন্ত্রণ, আর্থিক লেনদেন) এই যাচাই ঐচ্ছিক নয়।
মূল উপলব্ধি: LLM আর classical planner প্রতিদ্বন্দ্বী নয়, ভিন্ন কাজে ভালো: একটি অস্পষ্ট ভাষা থেকে খসড়া দেয়, অন্যটি নিয়ম মেনে বৈধতা প্রমাণ করে; নিরাপদ ব্যবস্থায় খসড়া সবসময় যাচাইয়ের মধ্য দিয়ে যায়।
প্র ০৫একবার নিখুঁত plan বানিয়ে চালানো, নাকি বারবার ছোট plan বানানো? বাস্তবের ডেলিভারি বা রোবটে কোনটি ভালো?
Classical planning-এর আদর্শ ছবি হলো: সকালে পুরো দিনের plan, তারপর চোখ বন্ধ করে চালানো। এটি কাজ করে যখন জগৎ স্থির আর জানা, যেমন একটি কারখানার নিয়ন্ত্রিত পরিবেশ। কিন্তু ঢাকার রাস্তায় সকালে বানানো plan দুপুরে অচল হতে পারে। অংশ ৯-এর run দেখায়, একটি রাস্তা বন্ধ হলেই মূল plan-এর পরের ধাপ অসম্ভব হয়ে যায়; নজরদারি না থাকলে রাইডার আটকে যেতেন।
একটি বিকল্প হলো প্রতিটি ধাপের আগে নজরদারি আর ব্যর্থ হলে replanning, যা আমরা করেছি। আরেকটি হলো সামনের অল্প কিছু ধাপের plan বানিয়ে চালানো, তারপর আবার নতুন তথ্য নিয়ে plan করা; একে প্রায়ই receding horizon বা model predictive ধাঁচ বলা হয়, আর রোবটিক্স ও নিয়ন্ত্রণ প্রকৌশলে এর ব্যাপক ব্যবহার আছে। তৃতীয়টি হলো আগেই অনিশ্চয়তা হিসাবে রাখা: প্রতিটি সম্ভাব্য পরিস্থিতির জন্য policy, যা মডিউল ৯-এর MDP-র দিকে নিয়ে যায়।
প্রতিটির দাম আছে। বারবার replanning-এ হিসাবের খরচ বাড়ে, আর রাইডার প্রতি পাঁচ মিনিটে নতুন রুট পেলে বিরক্ত হবেন। Policy-র জন্য সম্ভাবনার মডেল লাগে, যা প্রায়ই জানা নেই। এক দফার plan সবচেয়ে সস্তা, কিন্তু সবচেয়ে ভঙ্গুর। বাস্তব ব্যবস্থা প্রায়ই মেশায়: দিনের মোটা কাঠামো HTN-এর মতো উঁচু স্তরে আগে থেকে, রাস্তার খুঁটিনাটি চলার সময়।
NASA-র Remote Agent-এর বর্ণনায়ও এই স্তরভাগ দেখা যায় বলে পরিচিত: planner কাজের সূচি বানায়, আর একটি নির্বাহী অংশ চলার সময় নজর রেখে দরকারে planner-কে আবার ডাকে।
মূল উপলব্ধি: Plan একটি অনুমানভিত্তিক প্রতিশ্রুতি, গ্যারান্টি নয়; জগৎ যত বদলায়, তত বেশি নজরদারি আর replanning লাগে, আর অনিশ্চয়তা আগে থেকে জানা থাকলে সম্ভাবনাভিত্তিক policy-র দিকে যেতে হয়।
অনুশীলন
প্রথমটি হাতে হিসাবের, দ্বিতীয়টি আউটপুট অনুমানের, তৃতীয়টি নকশা ও সমালোচনার, শেষটি কোড লিখে মডেল বদলানোর।
-
হাতে হিসাব: ডেলিভারি সমস্যায় (হাব–শিমুল, হাব–বকুল, শিমুল–কদম, বকুল–কদম, কদম–জবা, বকুল–জবা; p1 যাবে কদমে, p2 জবায়, p3 শিমুল থেকে বকুলে) রাইডার হাবে, ব্যাগে p1 ও p2, আর p3 শিমুলে। এই state-এ (ক) goal-count, h_max আর h_add বের করুন। (খ) একটি relaxed plan লিখুন, তার দৈর্ঘ্য কত? (গ) লক্ষ্য {done(p1), done(p2)}-কে action deliver(p1,kadam) দিয়ে regress করলে কী পাওয়া যায়?
(ক) Goal-count = 3 (তিনটি done-ই বাকি)। Fact-খরচ: rider(shimul) = rider(bokul) = 1, rider(kadam) = rider(joba) = 2, bag(p1) = bag(p2) = 0 (আগেই সত্য), bag(p3) = 1 + 1 = 2. তারপর done(p1) = 1 + 2 + 0 = 3, done(p2) = 3; done(p3)-এর h_max খরচ 1 + max(1, 2) = 3, h_add খরচ 1 + 1 + 2 = 4. তাই h_max = 3, h_add = 3 + 3 + 4 = 10.
(খ) deliver(p1,kadam), deliver(p2,joba), deliver(p3,bokul), pick(p3,shimul), আর জোগানদার drive: হাব→শিমুল, শিমুল→কদম, হাব→বকুল, বকুল→জবা। মোট 8টি ভিন্ন action (pick p1, p2 আর লাগে না)। লেখকের যন্ত্রে কোড একই 8 দিয়েছে, আর এই state থেকে আসল সেরা plan-ও 8 ধাপ: এখানে h_add (10) বাড়িয়ে বলছে, relaxed plan ঠিক।
(গ) g′ = (g − add) ∪ pre = ({done(p1), done(p2)} − {done(p1)}) ∪ {rider(kadam), bag(p1)} = {rider(kadam), bag(p1), done(p2)}. deliver(p1,kadam) কোনো লক্ষ্য-fact মোছে না (শুধু bag(p1) মোছে), তাই এটি consistent.
-
আউটপুট অনুমান করুন: অংশ ৪-এর block-এর
A,S0,applicableআরapplyএকই রেখে নিচের লাইনগুলো চালালে কী প্রিন্ট হবে? আগে লিখুন, তারপর সেই block-এর শেষে বসিয়ে মিলিয়ে দেখুন।print(sorted(a[0] for a in A if applicable(S0, a))) s = apply(S0, next(a for a in A if a[0] == "pick(p1,hub)")) print(sorted(f for f in s if not f.startswith("road"))) print(applicable(s, next(a for a in A if a[0] == "deliver(p1,kadam)")))['drive(hub,bokul)', 'drive(hub,shimul)', 'pick(p1,hub)', 'pick(p2,hub)'] ['at(p2,hub)', 'at(p3,shimul)', 'bag(p1)', 'rider(hub)'] Falseপ্রথম লাইন: হাবে চারটি action প্রয়োগযোগ্য, অংশ ৪-এর stepper-এর ধাপ ১-এর মতো: দুটি drive (বকুল, শিমুল) আর দুটি pick.
sortedনামগুলো বর্ণানুক্রমে সাজায়, তাই drive আগে। তারপর pick(p1,hub) প্রয়োগে at(p1,hub) মুছে bag(p1) যোগ হয়; road fact বাদ দিয়ে বাকি থাকে ৪টি fact. তৃতীয় লাইন: deliver(p1,kadam)-এর শর্ত rider(kadam) নেই, তাই False. এই লাইনগুলো লেখকের যন্ত্রে Python 3.14.4 দিয়ে মেলানো। -
নকশা ও সমালোচনা: একটি AI সহকারী বাজারঘরের জন্য নিচের PDDL লিখেছে, আর planner খুব ছোট, অদ্ভুত plan দিচ্ছে। অন্তত চারটি সমস্যা খুঁজে বের করে ঠিক রূপ বলুন।
(:action drive :parameters (?from ?to - area) :precondition (rider ?from) :effect (rider ?to)) (:action deliver :parameters (?p - package ?a - area) :precondition (and (rider ?a) (bag ?p)) :effect (done ?p)) ; problem: (:goal (and (delivered p1) (delivered p2)))- drive-এ রাস্তার শর্ত নেই:
(road ?from ?to)না থাকলে যেকোনো দুই এলাকার মধ্যে সরাসরি যাওয়া যায়, যেমন হাব থেকে কদম। - drive-এ delete effect নেই:
(not (rider ?from))বাদ, তাই রাইডার আগের জায়গাতেও থেকে যান; কয়েকটি drive-এর পর তিনি সব জায়গায়। এটিই ছোট, অসম্ভব plan-এর মূল কারণ। - deliver-এ ঠিকানার শর্ত নেই:
(dest ?p ?a)না থাকলে যেকোনো এলাকায় পার্সেল দিয়ে দেওয়া যায়। - deliver-এ
(not (bag ?p))নেই: পার্সেল দিয়েও ব্যাগে থেকে যায়; একই পার্সেল বারবার “পৌঁছানো” যায়। - লক্ষ্যে ভুল predicate: domain-এ
done, লক্ষ্যেdelivered; অনেক parser অজানা predicate-এ ত্রুটি দেয়, কেউ চুপচাপ অসমাধানযোগ্য সমস্যা বানায়।
ঠিক রূপ অংশ ৩-এর domain file-এর মতো। মনে রাখুন, validator একই ভুল মডেল দিয়ে যাচাই করলে ভুল ধরবে না; তাই একটি ছোট সমস্যা হাতে সমাধান করে মেলানো আর মডেলটি মানুষের চোখে পর্যালোচনা দরকার।
- drive-এ রাস্তার শর্ত নেই:
-
কোড লিখে মডেল বদলান: রাইডারের ব্যাগে একসঙ্গে সর্বোচ্চ ২টি পার্সেল ধরে। STRIPS-এ সংখ্যা নেই, তাই
load(0),load(1),load(2)fact দিয়ে লিখুন: pick-এর জন্য load(k) (k < 2) লাগবে, আর তা load(k+1)-এ বদলাবে; deliver উল্টোটা। অংশ ৪-এর BFS দিয়ে নতুন plan আর তার দৈর্ঘ্য বের করুন। আগের plan-এর চেয়ে লম্বা হবে কেন?# অংশ ৪-এর block-এর পরে চালান: act, PKG, S0, GOAL, bfs সেখান থেকে আসে drives = [a for a in A if a[0].startswith("drive")] A = list(drives) # bfs() global A ব্যবহার করে for p, (src, dst) in PKG.items(): for k in range(2): # load(k) থেকে load(k+1) A.append(act(f"pick({p},{src})", [f"rider({src})", f"at({p},{src})", f"load({k})"], [f"bag({p})", f"load({k+1})"], [f"at({p},{src})", f"load({k})"])) for k in range(1, 3): # load(k) থেকে load(k-1) A.append(act(f"deliver({p},{dst})", [f"rider({dst})", f"bag({p})", f"load({k})"], [f"done({p})", f"load({k-1})"], [f"bag({p})", f"load({k})"])) plan2, n2 = bfs(S0 | {"load(0)"}, GOAL) print(len(A), "ground actions | plan:", len(plan2), "steps | expanded:", n2) for i, step in enumerate(plan2, 1): print(f"{i:2}. {step}")লেখকের যন্ত্রে সত্যিকারের run (Python 3.14.4):
32 ground actions | plan: 12 steps | expanded: 126 1. pick(p1,hub) 2. drive(hub,bokul) 3. drive(bokul,kadam) 4. deliver(p1,kadam) 5. drive(kadam,shimul) 6. pick(p3,shimul) 7. drive(shimul,hub) 8. pick(p2,hub) 9. drive(hub,bokul) 10. deliver(p3,bokul) 11. drive(bokul,joba) 12. deliver(p2,joba)প্রতিটি pick আর deliver এখন দুটি করে ground action (k-এর প্রতিটি মানে একটি), তাই মোট ৩২টি। Plan 12 ধাপ, আগের 10-এর চেয়ে ২টি বেশি: আগে হাবে দুটি আর শিমুলে একটি তুলে তিনটি একসঙ্গে বহন করা যেত, এখন তা অসম্ভব। BFS তাই হাব থেকে শুধু p1 নিয়ে কদমে দেয়, শিমুল থেকে p3 তুলে হাবে ফিরে p2 নেয়, তারপর বকুল আর জবা। BFS-এর plan সবচেয়ে কম ধাপের, তাই এর চেয়ে ছোট বৈধ plan নেই। লক্ষ করুন, এক জোড়া স্থির সংখ্যার জন্য action-এর সংখ্যা বেড়েছে; বড় সংখ্যার (যেমন ৫০ কেজি ওজন) জন্য এই কৌশল অচল, তখন PDDL 2.1-এর numeric fluent লাগে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি planning ব্যবস্থায় মডেল লেখা থেকে চলার সময় replanning পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 📝 Domain file-এ action-এর precondition, add আর delete লেখা
- 🎯 Problem file-এ শুরুর অবস্থা আর লক্ষ্য লেখা
- 🔍 Planner দিয়ে search করে একটি plan পাওয়া
- ✅ Validator দিয়ে প্রতিটি ধাপ আর শেষের লক্ষ্য পরীক্ষা
- 🚚 চালানোর সময় প্রতিটি action-এর আগে আসল জগতে precondition আবার দেখা
- 🔁 কোনো শর্ত না মিললে বর্তমান অবস্থা থেকে নতুন plan বানানো
STRIPS-এ state s = {rider(hub), at(p1,hub), road(hub,bokul)}. Action pick(p1,hub)-এর pre = {rider(hub), at(p1,hub)}, add = {bag(p1)}, del = {at(p1,hub)}. প্রয়োগের পর নতুন state কী?
- {rider(hub), at(p1,hub), road(hub,bokul), bag(p1)}
- {rider(hub), bag(p1)}
- {rider(hub), road(hub,bokul), bag(p1)}
- {road(hub,bokul), bag(p1)}
একটি STRIPS state-এ rider(bokul) fact লেখা নেই। Closed-world assumption অনুযায়ী এর অর্থ কী?
- রাইডার বকুলে আছেন কি না জানা নেই
- রাইডার বকুলে নেই
- রাইডার বকুলে আছেন
- State-টি অবৈধ, কারণ সব এলাকার জন্য fact লাগবে
Delete relaxation-এ একটি state থেকে তিনটি লক্ষ্য-fact-এর খরচ বের হলো 2, 3 আর 4. h_max আর h_add কত?
- h_max = 2, h_add = 9
- h_max = 4, h_add = 24
- h_max = 9, h_add = 4
- h_max = 4, h_add = 9
Backward planning-এ লক্ষ্য g = {done(p1), done(p2)}. Action deliver(p1,kadam)-এর pre = {rider(kadam), bag(p1)}, add = {done(p1)}, del = {bag(p1)}. Regress করলে আগের লক্ষ্য g′ কী?
- {rider(kadam), bag(p1), done(p2)}
- {rider(kadam), bag(p1), done(p1), done(p2)}
- {bag(p1)}
- {rider(kadam), bag(p1)}
Blocks world-এ শুরুতে A-র ওপরে C, B টেবিলে; লক্ষ্য on(A,B) আর on(B,C). লক্ষ্য দুটি একটি একটি করে (একটি পুরো শেষ করে পরেরটি) পূরণ করার কৌশল কেন বিপদে পড়ে?
- কারণ সমস্যাটির কোনো সমাধানই নেই
- কারণ শুধু on(A,B) আগে করলেই সমস্যা হয়, উল্টো ক্রমে হয় না
- কারণ যেকোনো ক্রমে প্রথম লক্ষ্য পূরণের পর দ্বিতীয়টির জন্য প্রথমটি ভাঙতে হয়
- কারণ রোবট-হাত দুটি ব্লক একসঙ্গে ধরতে পারে না, তাই সমস্যাটি অসম্ভব
একটি language model ৯ ধাপের একটি ডেলিভারি plan লিখেছে, যা পড়তে যুক্তিসংগত। এটি চালানোর আগে সবচেয়ে নির্ভরযোগ্য পরবর্তী কাজ কী?
- একই model-কে জিজ্ঞেস করা, plan-টি ঠিক কি না
- একটি plan validator-এ STRIPS মডেল অনুযায়ী প্রতিটি ধাপ আর শেষের লক্ষ্য পরীক্ষা করা
- Plan সেরা plan-এর চেয়ে ছোট হলে নির্দ্বিধায় চালানো
- শুধু ধাপের সংখ্যা গুনে মিলিয়ে দেখা
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Planning = factored state-এর ওপর search: state কয়েকটি fact-এর সেট, লক্ষ্য-পরীক্ষা GOAL ⊆ s. ডেলিভারি সমস্যায় ১৩৫টি state, BFS ১৩০টি খুলে 10 ধাপের plan দেয়।
- STRIPS action = pre, add, del: pre ⊆ s হলে চালানো যায়, s′ = (s − del) ∪ add; যা লেখা নেই তা মিথ্যা (closed-world), আর যা তালিকায় নেই তা বদলায় না।
- PDDL-এ domain আর problem আলাদা: একই ফাইল pyperplan বা unified-planning পড়ে; validator plan-এর প্রতিটি ধাপ পরীক্ষা করে, আর seed স্থির না করলে planner-এর সংখ্যা run-ভেদে বদলাতে পারে।
- Delete relaxation থেকে heuristic: h_max admissible কিন্তু দুর্বল (শুরুতে 3), h_add বাড়িয়ে বলে (12), relaxed plan (10) সাধারণত সবচেয়ে কাজের; A*-এ node ১৩০ থেকে ২৭.
- লক্ষ্যগুলো স্বাধীন নয়, জগৎও স্থির নয়: Sussman anomaly-তে কাজ মেশাতে হয়, HTN বড় কাজ ভাঙে, partial-order শুধু দরকারি ক্রম রাখে; চলার সময় নজরদারি আর replanning, আর AI-এর plan সবসময় validator দিয়ে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৫০ · Bayes-এর উপপাদ্য — প্রমাণ পেলে বিশ্বাস হালনাগাদপরবর্তী পর্বমডিউল ৯-এর শুরু: অনিশ্চয়তা আর সম্ভাবনা দিয়ে যুক্তি।
- AI-এর ভিত্তি আর Python for AI · কোর্স হোম৭৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- পর্ব ৪১ · Heuristic আর A* — অনুমান দিয়ে দ্রুত পথঝালাইAdmissible heuristic, relaxed problem আর A*-এর optimality.
- পর্ব ৩৯ · সমস্যাকে search হিসেবে দেখাঝালাইState, action, goal আর search-এর সাধারণ কাঠামো।
- DSA · Topological sortপাশের ট্র্যাকআংশিক ক্রম থেকে একটি বৈধ ধারাবাহিক ক্রম বের করা।
- DSA · BFSপাশের ট্র্যাকQueue দিয়ে স্তরে স্তরে search, ধাপে ধাপে।