এই পর্বে যা শিখবেন
- কখন path search ছেড়ে local search: optimisation সমস্যা, objective function, প্রতিবেশী আর state-space landscape-এর ভাষা
- Steepest-ascent আর first-choice hill climbing, local maximum, plateau, ridge; 8-queens-এ সফলতার হার নিজে মেপে দেখা, sideways move আর random restart-সহ
- Simulated annealing-এর নিয়ম, temperature schedule, আর ডেলিভারি রুটের TSP-তে nearest neighbour ও 2-opt-এর সঙ্গে আসল তুলনা
- Local beam search, genetic algorithm-এর population, selection, order crossover আর mutation; কেন GA-কে tune করতে হয়
- Gradient descent-কে continuous local search হিসেবে দেখা, SciPy-র local আর global optimiser, no free lunch theorem, আর seed ও গড় দিয়ে সৎভাবে ফল জানানো
এই পর্বে মডিউল ৭ শেষ হচ্ছে। আগের পর্বগুলোতে (পর্ব ৩৯ থেকে ৪৩) প্রশ্ন ছিল “লক্ষ্যে পৌঁছানোর পথ কোনটি”। এখানে প্রশ্ন: “সবচেয়ে ভালো শেষ অবস্থাটি কোনটি”; কীভাবে সেখানে পৌঁছালাম, তার দাম নেই। কোড পাতাতেই চলবে (ব্রাউজারের ভেতরে Python, Pyodide); প্রথমবার চালাতে কয়েক সেকেন্ড লাগতে পারে, SciPy-র block-এ আরও একটু বেশি।
১ · যখন পথ নয়, শুধু শেষ অবস্থাই গুরুত্বপূর্ণ
বাজারঘর (এই কোর্সের কাল্পনিক অনলাইন দোকান) একজন রাইডারকে সকালে ২০টি পার্সেল দেয়, ঢাকার মতো একটি কাল্পনিক ১০ × ১০ কিলোমিটার এলাকায় ছড়ানো ২০টি ঠিকানায়। রাইডারের প্রশ্ন একটাই: কোন ক্রমে গেলে মোট পথ সবচেয়ে কম হবে, আর শেষে আবার গুদামে ফেরা যাবে? এটি বিখ্যাত travelling salesman problem (TSP). উত্তর হলো ২০টি ঠিকানার একটি ক্রম; কীভাবে সেটি খুঁজে পেলেন, রাইডারের তাতে কিছু যায় আসে না।
আরও দুটি উদাহরণ। 8-queens ধাঁধায় ৮ × ৮ দাবার বোর্ডে ৮টি রানি এমনভাবে বসাতে হয় যাতে কেউ কাউকে আক্রমণ না করে (একই সারি, কলাম বা কর্ণে দুটি রানি নয়); উত্তর একটি বোর্ড। পরীক্ষার রুটিনে উত্তর একটি পূর্ণ সময়সূচি, যেখানে কোনো শিক্ষার্থীর দুটি পরীক্ষা একই সময়ে পড়ে না। এগুলো সবই optimization সমস্যা: অনেক সম্ভাব্য উত্তরের মধ্যে একটি মাপকাঠিতে সবচেয়ে ভালোটি খোঁজা।
রুটিন কমিটির খসড়া। পরীক্ষা কমিটি কখনো প্রতিটি সম্ভাব্য রুটিন লিখে তুলনা করে না। তারা একটি খসড়া বানায়, যাতে হয়তো দশটি সংঘাত, তারপর দুটি পরীক্ষার তারিখ অদলবদল করে দেখে সংঘাত কমল কি না; কমলে রাখে, না কমলে ফিরিয়ে দেয়। হাতে সবসময় একটিই পূর্ণ রুটিন, আর প্রতিটি ছোট বদল তাকে একটু ভালো করে। এটিই local search. কমিটির ঝামেলাও চেনা: কখনো এমন খসড়ায় পৌঁছায় যেখানে কোনো একক অদলবদলেই আর উন্নতি হয় না, অথচ রুটিনটি নিখুঁতও নয়।
এমন সমস্যাকে লেখা হয় complete-state formulation দিয়ে: প্রতিটি state নিজেই একটি পূর্ণ প্রার্থী-সমাধান, শুধু ভালো বা খারাপ। পর্ব ৩৯-এর (সমস্যাকে search হিসেবে দেখা) ছকে state ছিল “এখন কোথায় আছি”, আর উত্তর ছিল goal পর্যন্ত চালের ধারা। পার্থক্যটি পাশাপাশি:
| দিক | Path search (BFS, A* …) | Local search |
|---|---|---|
| উত্তর কী | শুরু থেকে goal পর্যন্ত চালের ধারা | একটি শেষ state, যত ভালো সম্ভব |
| মেমরিতে কী রাখে | frontier আর দেখা state-এর তালিকা, বিশাল হতে পারে | শুধু বর্তমান state (বা অল্প কয়েকটি) |
| নিয়মিত সব জায়গা দেখে? | হ্যাঁ, পদ্ধতিগতভাবে | না, শুধু আশপাশ |
| complete আর optimal? | অনেক পদ্ধতিই হ্যাঁ (শর্তসাপেক্ষে) | সাধারণত কোনোটিই নিশ্চিত নয় |
| কোথায় জেতে | ছোট বা ভালো heuristic-ওয়ালা state space | বিশাল state space, যেখানে “বেশ ভালো” উত্তরই যথেষ্ট |
State space কত বিশাল, একটু হিসাব করি। ২০টি ঠিকানার TSP-তে গুদাম থেকে শুরু ধরে বাকি ১৯টির ক্রম 19! রকম, আর একই চক্র উল্টো দিকে ঘুরলে দৈর্ঘ্য একই, তাই আলাদা পথ 19! ÷ 2 ≈ 6.1 × 1016টি। প্রতি সেকেন্ডে একশো কোটি পথ মাপলেও সব দেখতে প্রায় ২ বছর। 8-queens-এ প্রতি কলামে একটি রানি রাখলে বোর্ড 88 = 16,777,216টি, সমাধান মাত্র ৯২টি। এই combinatorial explosion-এর সামনে local search কয়েক হাজার ধাপে ভালো উত্তর দেয়; দাম হলো নিশ্চয়তা, সে কখনো বলতে পারে না “এর চেয়ে ভালো উত্তর নেই”।
২ · State-space landscape — চূড়া, উপত্যকা, সমতল আর শিরা
Local search বোঝার সবচেয়ে কাজের ছবি হলো একটি ভূদৃশ্য, state-space landscape. অনুভূমিক অক্ষে সব state সাজানো, আর প্রতিটি state-এর উচ্চতা হলো তার objective function-এর মান, মানে “এই সমাধান কত ভালো” তার একটি সংখ্যা। লক্ষ্য সবচেয়ে উঁচু চূড়া (maximise). অনেক সমস্যায় আসে একটি খরচ, যা কমাতে হয় (minimise): TSP-তে পথের দৈর্ঘ্য, 8-queens-এ আক্রমণ করা জোড়ার সংখ্যা। খরচ c কমানো আর −c বাড়ানো একই কাজ, তাই “hill climbing” TSP-তে আসলে পাহাড় বেয়ে নামে।
দ্বিতীয় ধারণা neighbourhood বা প্রতিবেশী: একটি state থেকে একটি ছোট বদলে যে state-গুলোতে যাওয়া যায়। 8-queens-এ বদলটি হতে পারে একটি রানিকে নিজের কলামেই অন্য সারিতে সরানো: ৮টি কলাম × ৭টি সারি = ৫৬টি প্রতিবেশী। TSP-তে প্রচলিত বদল তালিকার একটি টুকরো উল্টে দেওয়া (অংশ ৬-এর 2-opt); ২০টি ঠিকানায় এমন বদল 20 × 17 ÷ 2 = 170টি।
একমাত্রিক state-space landscape
A 1-D landscape: global maximum, local maximum, shoulder (plateau) and a current state with its two neighbours
ছবির নামগুলো এই পর্ব জুড়ে বারবার আসবে:
- Global maximum: পুরো ভূদৃশ্যের সবচেয়ে উঁচু বিন্দু, আসল সেরা উত্তর। খরচ কমানোর ভাষায় global minimum.
- Local maximum: এমন চূড়া যার প্রতিটি প্রতিবেশী নিচু, কিন্তু অন্য কোথাও আরও উঁচু চূড়া আছে। খরচের ভাষায় local minimum, যা পর্ব ৩৪-এ gradient descent-এর ফাঁদ হিসেবে দেখেছেন।
- Plateau: সমতল অংশ, যেখানে প্রতিবেশীদের মান সমান। দুই রকম: একদিকে চড়াই থাকলে তার নাম shoulder (ছবির বেগুনি অংশ), আর চারপাশ নিচু হলে সমতল local maximum. সমতলে “কোন দিকে উঁচু” প্রশ্নের উত্তর নেই, তাই লোভী পদ্ধতি থেমে যায়।
- Ridge: দুই বা বেশি মাত্রায় তির্যকভাবে ওঠা সরু শিরা। প্রতিটি অনুমোদিত বদল যদি শুধু উত্তর-দক্ষিণ বা পূর্ব-পশ্চিমে যায়, তাহলে যেকোনো এক পায়েই শিরা থেকে নামতে হয়; ফলে শিরার প্রতিটি বিন্দু local maximum-এর মতো আচরণ করে।
ভূদৃশ্য সমস্যা আর আপনার বেছে নেওয়া প্রতিবেশী-নিয়ম মিলে তৈরি। একই TSP-তে “দুটি ঠিকানা অদলবদল” আর “একটি টুকরো উল্টানো” আলাদা local optimum-সহ দুটি আলাদা ভূদৃশ্য দেয়। প্রতিবেশী-নিয়ম তাই local search-এর সবচেয়ে বড় নকশা-সিদ্ধান্ত।
৩ · Hill climbing — steepest-ascent আর first-choice
সবচেয়ে সরল local search-এর নাম hill climbing, কখনো একে greedy local search-ও বলা হয়। নিয়মটি তিন লাইনের:
current = একটি শুরুর state
loop:
best = current-এর সব প্রতিবেশীর মধ্যে সবচেয়ে ভালোটি
যদি best, current-এর চেয়ে ভালো না হয়: current ফেরত দিন (থামা)
current = best
এই রূপের নাম steepest-ascent hill climbing: প্রতি ধাপে সব প্রতিবেশী মেপে সবচেয়ে খাড়া চড়াই। পর্ব ৪১-এর (Heuristic আর A*) greedy best-first search-এর আত্মীয়, তবে কোনো frontier নেই, পেছনের বিকল্পও মনে রাখা হয় না। নিচের ধাপগুলো ছবি ৪৪.১-এর ভূদৃশ্যে এই নিয়ম চালায়; প্রতিবেশী শুধু বাঁ আর ডানের state.
- শুরু x = 1, মান 5. বাঁয়ে x = 0 (মান 3), ডানে x = 2 (মান 6.5). সেরা প্রতিবেশী বর্তমানের চেয়ে ভালো, তাই ডানে এক পা।
- x = 2, মান 6.5. প্রতিবেশী 5 আর 7.5, আবার ডানে। পুরো ভূদৃশ্য hill climbing কখনো দেখে না; প্রতি ধাপে শুধু দুটি সংখ্যা।
- x = 3, মান 7.5. প্রতিবেশী 6.5 আর 8, ডানে। প্রতিটি পায়ে মান বাড়ছে।
- x = 4, মান 8. থামা। দুই প্রতিবেশীই 7.5, কেউ ভালো নয়। অথচ সবুজ বৃত্তের global maximum 15. এটি local maximum: বড় চূড়ায় যেতে আগে x = 7-এর খাদে নামতে হতো, আর লোভী নিয়ম কখনো নামে না।
- অন্য শুরু x = 10, মান 10. Shoulder-এর মাঝখান: দুই প্রতিবেশীই 10, তাই কঠোর নিয়মে থামা, যদিও দুই পা ডানে চড়াই। পাশে হাঁটার অনুমতি (sideways move) থাকলে x = 11, 12 পেরিয়ে x = 13-এ চড়াই মিলত।
- Random restart: নতুন শুরু x = 14. 13.5 → 14.5 → 15; x = 16-এ দুই পাশে 14.5 আর 14, থামা। এবার থামার জায়গাটিই global maximum. একই পদ্ধতি, শুধু শুরু আলাদা।
Steepest-ascent-এ প্রতি ধাপে সব প্রতিবেশী মাপতে হয়; 8-queens-এ ৫৬টি, কিন্তু হাজার পার্সেলের রুটে লাখের ঘরে। তখন কাজে আসে first-choice hill climbing: এলোমেলোভাবে একটি করে প্রতিবেশী বানানো, আর প্রথম যেটি ভালো, সেখানেই চলে যাওয়া। প্রতিটি পা কম খাড়া, কিন্তু অনেক সস্তা; কত বার চেষ্টার পর হাল ছাড়বেন, তার একটি সীমা রাখতে হয়।
8-queens-এ hill climbing, ধাপে ধাপে
বোর্ডকে লিখি ৮টি সংখ্যায়: q[c] মানে কলাম c-তে রানি কোন সারিতে, তাই কলামের সংঘাত আপনা থেকেই নেই। খরচ h = একে অপরকে আক্রমণ করা রানির জোড়া (একই সারি বা কর্ণ); সমাধান মানে h = 0. নিচের ট্রেস ডেস্কটপে একটি seed থেকে (সত্যিকারের run, Python 3.14.4); সমান সেরা চাল একাধিক থাকলে কলাম a থেকে প্রথমটি নেওয়া। বোর্ডে সারি 8 ওপরে, কলাম a বাঁয়ে।
1h = আক্রমণের জোড়া(q)2for ৫৬টি প্রতিবেশী: h মাপা3best = সবচেয়ে ছোট h4if best < h: সেই চাল দেওয়া5else: থামা (local min)
| q | |
| h | |
| চাল |
- শুরু, h = 9. এলোমেলো বোর্ড: সারি 8-এ তিনটি রানি (e, f, g) পরস্পরকে আক্রমণ করে, b2 আর d2 একই সারিতে, আরও কয়েকটি কর্ণের জোড়া।
- প্রথম চাল, h = 5. ৫৬টি প্রতিবেশীর সেরা: কলাম f-এর রানি সারি 8 থেকে 1-এ। সারি 8-এর ভিড় ভাঙায় এক চালেই ৪টি জোড়া কমল।
- h = 3. কলাম g-এর রানি সারি 8 থেকে 4-এ। উন্নতি কমছে (৪, তারপর ২); চূড়ার কাছে ঢাল সমান হয়ে আসে।
- h = 2. কলাম a-এর রানি সারি 3 থেকে 6-এ। একটি নতুন কর্ণের সংঘাত তৈরি হলো (a6 আর f1), কিন্তু পুরোনো দুটি মিটল, মোটে লাভ ১।
- h = 1. কলাম b-এর রানি সারি 2 থেকে 3-এ। বাকি শুধু a6 আর f1, একই কর্ণে (a6, b5, c4, d3, e2, f1).
- থামা: local minimum. ৫৬টি প্রতিবেশীর একটিতেও h শূন্য নয়; সেরাদের h = 1 (এমন ৩টি)। কঠোর নিয়মে এখানেই শেষ। সমান-মানের প্রতিবেশী আছে বলে এটি একটি plateau-র অংশ; sideways move থাকলে হাঁটা চলত।
মাত্র ৪টি চালেই h = 9 থেকে 1: hill climbing খুব দ্রুত ভালো জায়গায় পৌঁছায়, আর তত দ্রুতই আটকে যায়। কত ঘন ঘন আটকায়, তা অনেক বোর্ডে চালিয়েই মাপা যায়।
৪ · ১০০০টি বোর্ডে মাপা — sideways move আর random restart
নিচের কোড ১০০০টি এলোমেলো বোর্ডে steepest-ascent চালায় দুই নিয়মে: শুধু উন্নতিতে চাল, আর সমান মানেও পাশে হাঁটা (পরপর সর্বোচ্চ ১০০ বার, যাতে সমতলে চিরকাল না ঘোরে)। সমান সেরা চাল একাধিক থাকলে এলোমেলোভাবে একটি। শেষে random restart: সমাধান না পাওয়া পর্যন্ত নতুন বোর্ডে আবার শুরু, এমন ২০০টি সমাধানে গড়ে কত বার শুরু লাগে। গতির জন্য প্রতিবেশীর h বের করা হয়েছে প্রতিটি সারি আর কর্ণে কয়টি রানি আছে তার হিসাব থেকে; ফল একই, শুধু দ্রুত।
import random
N = 8 # q[c] = কলাম c-তে রানির সারি (0–7)
def climb(q, max_side=0): # steepest-ascent hill climbing; h = আক্রমণের জোড়া
R, D1, D2 = [0] * N, [0] * (2 * N), [0] * (2 * N) # সারি আর দুই কর্ণে রানি কতটি
for c, r in enumerate(q): R[r] += 1; D1[r - c + N] += 1; D2[r + c] += 1
h = sum(k * (k - 1) // 2 for k in R + D1 + D2); steps = side = 0
while h > 0:
best, moves = h + 1, []
for c in range(N): # ৫৬টি প্রতিবেশী: একটি রানিকে নিজের কলামে সরানো
r0 = q[c]; a0 = R[r0] + D1[r0 - c + N] + D2[r0 + c] - 3
for r in range(N):
if r == r0: continue
v = h - a0 + R[r] + D1[r - c + N] + D2[r + c]
if v < best: best, moves = v, [(c, r)]
elif v == best: moves.append((c, r))
if best > h or (best == h and side >= max_side): break # আটকে গেছে
side = side + 1 if best == h else 0 # পাশে হাঁটা গোনা
c, r = random.choice(moves); r0 = q[c]
R[r0] -= 1; D1[r0 - c + N] -= 1; D2[r0 + c] -= 1
R[r] += 1; D1[r - c + N] += 1; D2[r + c] += 1
q[c], h, steps = r, best, steps + 1
return h == 0, steps
board = lambda: [random.randrange(N) for _ in range(N)]
random.seed(42); T = 1000
for name, ms in [("শুধু উপরে", 0), ("পাশে ≤100", 100)]:
res = [climb(board(), ms) for _ in range(T)]
ok = [k for s, k in res if s]; bad = [k for s, k in res if not s]
print(f"{name}: সফল {len(ok)}/{T} = {100 * len(ok) / T:.1f}%, গড় ধাপ"
f" সফলে {sum(ok) / len(ok):.1f}, ব্যর্থে {sum(bad) / len(bad):.1f}")
tries = []
for _ in range(200): # random restart: সমাধান না পাওয়া পর্যন্ত নতুন বোর্ড
k = 1
while not climb(board())[0]: k += 1
tries.append(k)
print(f"random restart: ২০০টি সমাধানে গড় {sum(tries) / 200:.2f} বার শুরু, সর্বোচ্চ {max(tries)}")আসল আউটপুট (ব্রাউজারের Pyodide-এ):
শুধু উপরে: সফল 134/1000 = 13.4%, গড় ধাপ সফলে 4.1, ব্যর্থে 3.1
পাশে ≤100: সফল 936/1000 = 93.6%, গড় ধাপ সফলে 18.7, ব্যর্থে 51.6
random restart: ২০০টি সমাধানে গড় 6.98 বার শুরু, সর্বোচ্চ 32
শুধু উপরে: ১০০০টি বোর্ডের ১৩৪টি সমাধান, মানে ১৩.৪%. সফল run গড়ে ৪.১ চালে শেষ, ব্যর্থ run গড়ে ৩.১ চালেই আটকে যায়: দ্রুত সাফল্য, দ্রুততর ব্যর্থতা। পাশে ≤100: সফলতা লাফিয়ে ৯৩.৬%, কিন্তু খরচ বেড়েছে: সফল run গড়ে ১৮.৭ চাল, আর ব্যর্থগুলো গড়ে ৫১.৬ চাল ঘুরে হাল ছেড়েছে। মানে 8-queens-এর বেশির ভাগ “আটকে যাওয়া” আসলে সমতলে, সত্যিকারের চূড়ায় নয়। Random restart: প্রতিটি শুরুর সফলতার সম্ভাবনা p হলে প্রথম সাফল্য পর্যন্ত শুরুর সংখ্যার প্রত্যাশিত মান 1/p (geometric distribution, পর্ব ৩৬). p ≈ 0.134 বসালে 1/p ≈ 7.5; মাপা গড় 6.98, কাছাকাছি। সবচেয়ে দুর্ভাগা সমাধানে ৩২ বার শুরু লেগেছে, তবু প্রতিটি শুরু সস্তা বলে মোট খরচ কম।
এই সংখ্যাগুলো কতটা নির্ভরযোগ্য? ১০০০টি স্বাধীন চেষ্টায় অনুপাত p̂ = 0.134 হলে তার standard error √(p̂(1 − p̂) ÷ n) = √(0.134 × 0.866 ÷ 1000) ≈ 0.011, মানে মোটামুটি ±১ শতাংশ-বিন্দুর অনিশ্চয়তা; অন্য seed-এ ১২% বা ১৫% স্বাভাবিক, ৩০% নয়। Russell ও Norvig-এর পাঠ্যবইয়ে এই পরীক্ষার উল্লেখিত সংখ্যাও (প্রায় ১৪% আর sideways-সহ প্রায় ৯৪%) কাছাকাছি; তবে এখানকার সংখ্যা এই পাতার নিজের মাপ, যা আপনি নিজে চালিয়ে যাচাই করতে পারেন। Sideways-এর সীমা কীভাবে গোনা হয় বা টাইয়ে কোন চাল, এমন খুঁটিনাটিতে সংখ্যা একটু বদলায়।
Random restart কেন শক্তিশালী: p = 0.134 হলে ১০ বার শুরুতেও সব ব্যর্থ হওয়ার সম্ভাবনা 0.86610 ≈ 0.24, ২০ বারে প্রায় 0.056. সস্তা কিন্তু কম-সফল পদ্ধতি বারবার চালালে প্রায় নিশ্চিত সাফল্য, যদি শুরুগুলো স্বাধীন আর p একেবারে শূন্যের কাছে না হয়। p খুব ছোট হলে ভালো প্রতিবেশী-নিয়ম লাগে; পর্ব ৪৩-এর (Constraint satisfaction) min-conflicts এমন একটি local search, যা অনেক বড় n-queens-ও দ্রুত সমাধান করে বলে পরিচিত।
৫ · Simulated annealing — মাঝে মাঝে খারাপ চাল নেওয়ার সাহস
Random restart আটকে গেলে কাজ ফেলে নতুন করে শুরু করে। অন্য ভাবনা: মাঝে মাঝে নিচে নামতে দেওয়া, যাতে ছোট চূড়া থেকে নেমে বড় চূড়ার ঢালে ওঠা যায়। কিন্তু সবসময় এলোমেলো হাঁটলে কোথাও পৌঁছানো যায় না; দরকার শুরুতে বেশ এলোমেলো, শেষে প্রায় খাঁটি hill climbing. এটাই simulated annealing.
কামারের দোকান। কামার লোহা আগুনে লাল করে তারপর ধীরে ঠান্ডা হতে দেন। গরম অবস্থায় পরমাণুগুলো ছটফট করে জায়গা বদলায়; ধীরে ঠান্ডা হলে গুছিয়ে কম শক্তির, কম ভঙ্গুর গঠনে বসে। হঠাৎ পানিতে ডুবালে তারা যেখানে ছিল সেখানেই আটকে যায়। ধীরে ঠান্ডা করার এই কাজের নাম annealing. Simulated annealing-এ “তাপমাত্রা” বেশি হলে খারাপ চালও প্রায়ই নেওয়া হয়, ধীরে কমলে সমাধান ভালো গঠনে স্থির হয়; তাড়াহুড়ো করে ঠান্ডা করা মানে আবার local optimum-এ আটকে যাওয়া।
নিয়মটি খরচ কমানোর ভাষায় (TSP-তে পথের দৈর্ঘ্য)। প্রতি ধাপে একটি এলোমেলো প্রতিবেশী বানান, আর তার খরচ থেকে বর্তমান খরচ বাদ দিয়ে পান Δ (ডেল্টা). Δ < 0 মানে ভালো: সবসময় নিন। নইলে নিন এই সম্ভাবনায়,
P(গ্রহণ) = exp(−Δ ÷ T)
T হলো তাপমাত্রা, একটি ধনাত্মক সংখ্যা যা ধীরে ধীরে কমে, আর exp মানে ex (e ≈ 2.718). বাস্তবে 0 থেকে 1-এর একটি এলোমেলো সংখ্যা u টেনে u < P হলে চালটি নেওয়া হয়। একই T-তে অল্প খারাপ চাল বেশি সম্ভাব্য; একই Δ-তে T যত কম, সম্ভাবনা তত কম। এই নিয়মের নাম Metropolis criterion, ১৯৫৩ সালের Nicholas Metropolis ও সহকর্মীদের পদার্থবিজ্ঞানের simulation থেকে।
1g = নতুন খরচ - বর্তমান খরচ # Δ2if g < 0: নেওয়া (সবসময়)3else: p = exp(-g / T)4 নেওয়া যদি u < p5T = a * T # ঠান্ডা করা
| T | |
| Δ | |
| Δ ÷ T | |
| P(গ্রহণ) |
- ভালো চাল, Δ = −3. পথ ৩ কিমি ছোট হলো। তাপমাত্রা যা-ই হোক, ভালো চাল সবসময় নেওয়া হয়।
- খুব গরম, T = 100, Δ = +10. P = e−0.1 ≈ 0.905: ১০ কিমি খারাপ চালও প্রায় ৯০% ক্ষেত্রে নেওয়া হয়। শুরুতে পদ্ধতিটি প্রায় এলোমেলো হাঁটা, তাই অনেক দূর ঘুরে দেখে।
- মাঝারি, T = 10, একই Δ = +10. P = e−1 ≈ 0.368; u = 0.62 বড়, তাই বাদ। তাপমাত্রা দশ গুণ কমায় সম্ভাবনা ৯০% থেকে ৩৭%.
- একই T = 10, ছোট খারাপ চাল Δ = +2. P = e−0.2 ≈ 0.819, নেওয়া হলো। অল্প খারাপ চাল সহজে মেলে; এটাই ছোট চূড়া থেকে আস্তে নেমে পাশের ঢালে যাওয়ার সুযোগ।
- ঠান্ডা, T = 1, Δ = +2. P = e−2 ≈ 0.135, বাদ; Δ = +10 হলে e−10 ≈ 0.000045. এখন শুধু ছোট খারাপ চাল মাঝে মাঝে।
- প্রায় হিমাঙ্ক, T = 0.01, Δ = +0.5. P ≈ 1.9 × 10−22, কার্যত শূন্য: শেষে annealing আর first-choice hill climbing একই। পরের অংশের run-এ প্রতি ধাপে T ← a·T, a ≈ 0.99965, ২০,০০০ ধাপে T 10 থেকে 0.01.
Temperature schedule — কত দ্রুত ঠান্ডা করবেন
কোন ধাপে T কত, এই নিয়মের নাম temperature schedule. প্রচলিত কয়েকটি:
- Geometric: Tk+1 = a·Tk, a একটু কম 1. সবচেয়ে প্রচলিত, এই পাতার কোডেও। শুরু T0 আর শেষ T1 ঠিক করলে K ধাপে a = (T1 ÷ T0)1/K.
- Linear: প্রতি ধাপে T একই পরিমাণ কমে।
- Logarithmic: Tk = c ÷ log(k + 2)-এর মতো খুব ধীর ঠান্ডা। তাত্ত্বিক কাজে (Geman ও Geman ১৯৮৪, পরে Hajek ১৯৮৮) দেখানো হয়েছিল, যথেষ্ট ধীরে ঠান্ডা করলে সেরা উত্তরে পৌঁছানোর সম্ভাবনা ১-এর দিকে যায়; কিন্তু সেই গতি বাস্তবে প্রায়ই সব সমাধান একে একে দেখার চেয়েও ধীর।
- Reheating বা restart: অনেকক্ষণ উন্নতি না হলে T আবার একটু বাড়ানো।
ব্যবহারিক নিয়ম: কয়েকটি এলোমেলো খারাপ চালের গড় Δ মেপে T0 এমন রাখুন যাতে শুরুতে তার অর্ধেক বা বেশি গৃহীত হয়, আর শেষের T এমন ছোট যাতে সবচেয়ে ছোট খারাপ চালও প্রায় কখনো নয়। সংখ্যাগুলো এককের ওপর নির্ভর করে: পথ কিলোমিটারে না মিটারে, তাতেই Δ হাজার গুণ বদলায়।
ইতিহাস: Scott Kirkpatrick, C. Daniel Gelatt আর Mario Vecchi ১৯৮৩ সালে Science পত্রিকায় “Optimization by Simulated Annealing” প্রবন্ধে Metropolis-এর simulation-কে optimization-এর হাতিয়ার হিসেবে প্রস্তাব করেন; উদাহরণে ছিল কম্পিউটার চিপের নকশা আর travelling salesman. চেক গবেষক Vlado Černý স্বাধীনভাবে একই ভাবনায় পৌঁছান বলে উল্লেখ করা হয় (প্রকাশ ১৯৮৫).
৬ · ডেলিভারি রুটে TSP — nearest neighbour, 2-opt আর annealing পাশাপাশি
এবার রাইডারের সমস্যা। ২০টি পয়েন্টের স্থানাঙ্ক seed-করা এলোমেলো সংখ্যা, ঢাকার মতো একটি কাল্পনিক ১০ × ১০ কিমি বাক্সে; দূরত্ব সোজা রেখায় (আসল রাস্তায় বেশি, তবে পদ্ধতির ভাবনা একই)। তিনটি পদ্ধতি:
- Nearest neighbour: প্রতিবার সবচেয়ে কাছের না-যাওয়া পয়েন্টে যাওয়া। Local search নয়, greedy নির্মাণ; দ্রুত, কিন্তু শেষ দিকে প্রায়ই লম্বা লাফ।
- 2-opt hill climbing: প্রতিবেশী মানে একটি টুকরো উল্টানো; steepest-ascent, যতক্ষণ কোনো উল্টানোয় পথ ছোট হয়।
- Simulated annealing: একই প্রতিবেশী, এলোমেলো একটি করে, Metropolis নিয়মে; ২০,০০০ ধাপ।
দুটি রেখা একে অপরকে কাটলে (X-এর মতো) সেই পথ সেরা হতে পারে না, কারণ ত্রিভুজের নিয়মে আড়াআড়ি দুটি রেখার চেয়ে দুই পাশের দুটির মোট দৈর্ঘ্য কম; মাঝের টুকরো উল্টালেই কাটাকাটি খোলে।
একটি 2-opt চাল — দুটি রেখা সরিয়ে দুটি নতুন রেখা
A 2-opt move: remove edges (a, b) and (c, d), reverse the segment b … c, add (a, c) and (b, d)
gain() ঠিক এটাই করে।নিচের কোড 2-opt-কে ১০টি এলোমেলো শুরু থেকে আর annealing-কে ১০টি seed দিয়ে চালায়, তারপর সেরা পথ আর দৈর্ঘ্য বনাম ধাপের ছবি আঁকে। পরের block এর সংজ্ঞাগুলো ব্যবহার করবে (একই namespace).
import math, random, numpy as np, matplotlib.pyplot as plt
n = 20 # ২০টি কাল্পনিক ডেলিভারি পয়েন্ট, ১০ × ১০ কিমি বাক্সে
P = np.random.default_rng(44).uniform(0, 10, (n, 2)).round(1)
D = np.hypot(*(P[:, None] - P[None]).T).tolist() # দূরত্বের টেবিল (কিমি)
tour_len = lambda t: sum(D[t[k - 1]][t[k]] for k in range(n))
def gain(t, i, j): # t[i..j] উল্টালে (2-opt চাল) দৈর্ঘ্য কত বদলায়
a, b, c, d = t[i - 1], t[i], t[j], t[(j + 1) % n]
return D[a][c] + D[b][d] - D[a][b] - D[c][d]
def two_opt(t): # steepest-ascent hill climbing, 2-opt প্রতিবেশী
while True:
g, i, j = min((gain(t, i, j), i, j) for i in range(1, n - 1) for j in range(i + 1, n))
if g > -1e-9: return t # কোনো প্রতিবেশী ভালো নয়: থামা
t[i:j + 1] = t[i:j + 1][::-1]
def anneal(seed, iters=20000, T0=10.0, T1=0.01):
r = random.Random(seed); t = r.sample(range(n), n)
cur, T, a, hist = tour_len(t), T0, (T1 / T0) ** (1 / iters), []
for k in range(iters):
i = r.randrange(1, n - 1); j = r.randrange(i + 1, n); g = gain(t, i, j)
if g < 0 or r.random() < math.exp(-g / T): # খারাপ চালও মাঝে মাঝে
t[i:j + 1] = t[i:j + 1][::-1]; cur += g
T *= a; hist.append(cur) # ঠান্ডা করা: T ← a·T
return t, cur, hist
nn = [0] # nearest neighbour: সবসময় সবচেয়ে কাছের অদেখা পয়েন্ট
while len(nn) < n: nn.append(min(set(range(n)) - set(nn), key=lambda j: D[nn[-1]][j]))
r = random.Random(1)
two = [tour_len(two_opt(r.sample(range(n), n))) for _ in range(10)]
sa = sorted((anneal(s) for s in range(10)), key=lambda s: s[1])
print("nearest neighbour:", round(tour_len(nn), 2), " তার ওপর 2-opt:", round(tour_len(two_opt(nn[:])), 2))
print("2-opt, ১০টি এলোমেলো শুরু:", np.round(two, 1), "গড়", round(np.mean(two), 2))
print("SA, ১০টি seed:", np.round([s[1] for s in sa], 1), "গড়", round(np.mean([s[1] for s in sa]), 2))
fig, (a1, a2) = plt.subplots(1, 2, figsize=(9, 3.8))
a1.plot(*P[sa[0][0] + sa[0][0][:1]].T, "o-"); a1.set_title(f"Best SA route: {sa[0][1]:.2f} km")
a2.plot(sa[0][2], lw=0.8, label="SA: current tour length")
a2.axhline(np.mean(two), color="red", ls="--", label="2-opt mean of 10 starts")
a2.set_xlabel("iteration"); a2.set_ylabel("km"); a2.legend(fontsize=8); plt.show()আসল আউটপুট (ব্রাউজারের Pyodide-এ; annealing-এর ফল ছোট থেকে বড় সাজানো):
nearest neighbour: 41.01 তার ওপর 2-opt: 38.5
2-opt, ১০টি এলোমেলো শুরু: [39.1 39. 39.1 39.5 39.1 39. 41.4 38.5 39. 38.5] গড় 39.21
SA, ১০টি seed: [38.5 38.5 38.5 38.5 38.5 38.5 38.5 38.5 38.5 39.5] গড় 38.6
Nearest neighbour 41.01 কিমি; তার ওপর 2-opt চালিয়েই 38.5. এলোমেলো শুরু থেকে 2-opt প্রতিবার কোনো local optimum-এ থেমেছে: মাত্র ২টি 38.5-এ, বাকিগুলো 39.0 থেকে 41.4, গড় 39.21. Annealing-এর ১০টির ৯টি 38.5-এ, একটি 39.5-এ, গড় 38.6. ডানের ছবিতে শুরুতে দৈর্ঘ্য বিশাল আর লাফালাফি করে (গরম অবস্থায় খারাপ চালও গৃহীত), T কমার সঙ্গে নেমে আসে, আর শেষ কয়েক হাজার ধাপে প্রায় সমান: তখন কার্যত hill climbing. বাঁয়ের সেরা পথে কোনো রেখা কাটাকাটি নেই।
38.5 কি আসলেই সেরা? ২০টি পয়েন্টে তা নিশ্চিত বলা যায় Held–Karp dynamic programming দিয়ে, খরচ প্রায় n² · 2n. ডেস্কটপে একই পয়েন্টে (সত্যিকারের run, Python 3.14.4, NumPy 2.5.1, প্রায় সাড়ে চার সেকেন্ড):
Held-Karp, n = 20 (exact): optimum 38.501 km
তাহলে annealing-এর ৯টি আর 2-opt-এর ২টি run সত্যিকারের সেরা পথই পেয়েছে, যদিও local search নিজে তা জানত না। n = 40 হলে Held–Karp-এর টেবিল কোনো সাধারণ কম্পিউটারের মেমরিতে আঁটবে না, অথচ annealing-এর খরচ প্রায় একই; তাই বাস্তবে ফল মাপা হয় lower bound বা সবচেয়ে ভালো জানা উত্তরের সঙ্গে। গভীর আলোচনা DSA কোর্সের NP-hard সমস্যা বাস্তবে আর approximation algorithm পর্বে।
সতর্কতা: একটি run দেখে রায় নয়। 2-opt-এর অষ্টম run একা দেখলে মনে হতো “2-opt সেরা পথ দেয়”, সপ্তমটি দেখলে “৭% খারাপ”। একটি run একটি নমুনা মাত্র; তুলনা হয় অনেক seed-এর গড় আর ছড়ানো দিয়ে, সমান বাজেটে।
৭ · অনেক state একসঙ্গে — local beam search আর genetic algorithm
Local beam search একসঙ্গে k-টি state রাখে: প্রতি ধাপে সবগুলোর সব প্রতিবেশী তৈরি, আর তাদের সেরা k-টি পরের ধাপে। এটি k-টি আলাদা random restart নয়: ভালো জায়গার state-এর “বংশ” বেশি জায়গা পায়, খারাপ জায়গার state বাদ পড়ে। দুর্বলতা বৈচিত্র্য হারানো; প্রতিকার stochastic beam search, যেখানে ভালোর সম্ভাবনা বেশি রেখে এলোমেলোভাবে k-টি বাছা হয়।
এক পা এগোলেই genetic algorithm (GA), প্রাকৃতিক নির্বাচনের অনুকরণে। John Holland ১৯৭৫ সালে “Adaptation in Natural and Artificial Systems” বইয়ে এর কাঠামো দেন; David Goldberg-এর ১৯৮৯ সালের বই একে ব্যাপকভাবে পরিচিত করে। উপাদানগুলো:
- Population: একসঙ্গে অনেক প্রার্থী-সমাধান (এখানে ৬০টি পথ)।
- Fitness: কে কত ভালো; এখানে পথ যত ছোট, fitness তত বেশি।
- Selection: এখানে tournament selection: এলোমেলো ৩টি পথ টেনে সবচেয়ে ছোটটি জেতে।
- Crossover: দুই বাবা-মায়ের অংশ মিলিয়ে সন্তান। পথে সাধারণ কাটাকুটিতে একই ঠিকানা দুবার আসতে পারে, তাই order crossover (OX): প্রথমজনের একটি টুকরো হুবহু, বাকি ঘর দ্বিতীয়জনের ক্রমে। কৃতিত্ব সাধারণত Lawrence Davis-এর ১৯৮৫ সালের কাজকে দেওয়া হয়।
- Mutation: ছোট এলোমেলো বদল; এখানে ৩০% সন্তানের একটি টুকরো উল্টানো।
- Elitism: প্রতি প্রজন্মের সেরাটি অক্ষত পরের প্রজন্মে।
1L = [দৈর্ঘ্য(t) for t in pop]2p1 = tournament(pop, 3)3p2 = tournament(pop, 3)4child = ox(p1, p2)5if u < pm: টুকরো উল্টানো6new = [elite] + সন্তানেরা
| দৈর্ঘ্য | |
| p1 | |
| p2 | |
| child |
- Fitness মাপা। ৮টি ঠিকানা, ৬টি পথ; দৈর্ঘ্যগুলো বোঝানোর জন্য ধরে নেওয়া সংখ্যা। সেরা #4 (44.8), সবচেয়ে খারাপ #6 (61.0).
- প্রথম tournament. টানা হলো #1, #3, #5; সবচেয়ে ছোট #5 (50.2) হলো p1. সেরা #4 এই টানে ছিলই না; tournament দুর্বলদেরও কিছু সুযোগ দেয়, এতে বৈচিত্র্য থাকে।
- দ্বিতীয় tournament. টানা হলো #2, #4, #6; জেতে #4, সে p2 = D H B F A G C E.
- OX, ধাপ ১: টুকরো কপি। কাটা পড়ল ঘর ৩ থেকে ৫-এ; p1-এর C D E হুবহু সন্তানের একই ঘরে।
- OX, ধাপ ২: বাকি ঘর p2-এর ক্রমে। p2-কে ঘর ৬ থেকে চক্রাকারে পড়ি: G C E D H B F A; C, D, E বাদ দিলে G H B F A, যা বসে ঘর ৬, ৭, ৮, ১, ২-এ: সন্তান = F A C D E G H B, প্রতিটি ঠিকানা ঠিক একবার।
- Mutation আর নতুন প্রজন্ম। এলোমেলো সংখ্যা pm = 0.3-এর নিচে, তাই ঘর ৬–৭-এর G H উল্টে H G: F A C D E H G B. নতুন population-এ আগে যায় elite #4, তারপর এভাবে বানানো সন্তানেরা।
এবার ২০টি পয়েন্টের আসল সমস্যায় GA: population ৬০, ১২০ প্রজন্ম, ৫টি seed. আগের block-এর পয়েন্ট, দূরত্ব আর tour_len এখানেও ব্যবহৃত।
def ox(p1, p2, r): # order crossover: p1-এর একটি টুকরো, বাকিটা p2-এর ক্রমে
i, j = sorted(r.sample(range(n), 2))
child = [None] * n; child[i:j + 1] = p1[i:j + 1]
rest = [c for c in p2[j + 1:] + p2[:j + 1] if c not in child[i:j + 1]]
for k, c in zip([*range(j + 1, n), *range(i)], rest): child[k] = c
return child
def ga(seed, size=60, gens=120, pm=0.3):
r = random.Random(seed)
pop = [r.sample(range(n), n) for _ in range(size)]
best, mean = [], []
for g in range(gens):
L = [tour_len(t) for t in pop] # fitness: পথ যত ছোট তত ভালো
best.append(min(L)); mean.append(sum(L) / size)
pick = lambda: pop[min(r.sample(range(size), 3), key=L.__getitem__)] # tournament
new = [pop[L.index(min(L))][:]] # elitism: সেরাটি অক্ষত থাকে
while len(new) < size:
c = ox(pick(), pick(), r)
if r.random() < pm: # mutation: একটি টুকরো উল্টানো
i, j = sorted(r.sample(range(n), 2)); c[i:j + 1] = c[i:j + 1][::-1]
new.append(c)
pop = new
return best, mean
runs = [ga(s) for s in range(5)]
print("GA, ৫টি seed, শেষ প্রজন্মের সেরা:", [round(b[-1], 1) for b, m in runs],
"গড়", round(np.mean([b[-1] for b, m in runs]), 2))
print("seed 0-এর সেরা, প্রজন্ম 0, 20, 60, 119:", [round(runs[0][0][g], 1) for g in (0, 20, 60, 119)])
print("মোট পূর্ণ পথ মাপা হয়েছে, প্রতি run:", 60 * 120)
fig, ax = plt.subplots(figsize=(6, 3.4))
ax.plot(runs[0][0], label="best in population"); ax.plot(runs[0][1], label="population mean")
ax.axhline(sa[0][1], color="gray", ls=":", label="best SA tour")
ax.set_xlabel("generation"); ax.set_ylabel("tour length (km)")
ax.set_title("Genetic algorithm on the 20-stop toy (seed 0)"); ax.legend(fontsize=8)
plt.show()আসল আউটপুট (ব্রাউজারের Pyodide-এ):
GA, ৫টি seed, শেষ প্রজন্মের সেরা: [38.9, 39.0, 39.0, 38.5, 38.5] গড় 38.78
seed 0-এর সেরা, প্রজন্ম 0, 20, 60, 119: [93.7, 58.0, 46.0, 38.9]
মোট পূর্ণ পথ মাপা হয়েছে, প্রতি run: 7200
৫টির ২টি seed 38.5-এ, বাকিগুলো 38.9 আর 39.0-এ, গড় 38.78: annealing-এর 38.6-এর চেয়ে একটু খারাপ, 2-opt-এর 39.21-এর চেয়ে ভালো। seed 0-এর সেরা পথ প্রথম প্রজন্মে 93.7 কিমি, ২০-এ 58.0, ৬০-এ 46.0, শেষে 38.9. ছবিতে সেরার রেখা সিঁড়ির মতো নামে (elitism-এর জন্য কখনো খারাপ হয় না), গড়ের রেখা ওপরে কাঁপে (mutation কিছু খারাপ সন্তান বানায়)। প্রতি run-এ ৭২০০টি পূর্ণ পথ মাপা হয়েছে, অথচ annealing-এর প্রতিটি ধাপ মাত্র চারটি দূরত্বের হিসাব; এই সমস্যায় annealing সস্তাও।
GA জাদু নয়, তাকে tune করতে হয়। একই কোড ডেস্কটপে mutation-এর হার pm আর population বদলে, প্রতি ঘরে ৫টি seed-এর গড়, প্রতি run-এ একই ৭২০০টি পথ-মাপ (সত্যিকারের run, Python 3.14.4):
| mutation হার pm | population 60, 120 প্রজন্ম | population 20, 360 প্রজন্ম |
|---|---|---|
| 0 (mutation নেই) | 48.08 কিমি | 69.79 কিমি |
| 0.02 | 40.59 | 44.56 |
| 0.3 | 38.78 | 38.81 |
| 0.8 | 48.41 | 39.42 |
Mutation না থাকলে population দ্রুত একই রকম পথে ভরে যায়, crossover নতুন কিছু বানাতে পারে না: premature convergence. ছোট population-এ এটি আরও তীব্র (69.79, nearest neighbour-এর চেয়েও খারাপ)। Mutation খুব বেশি হলে বড় population-এ ভালো গঠন বারবার ভাঙে। মাঝের হারটি এই সমস্যা আর বাজেটের জন্য ভালো, অন্য সমস্যায় অন্য সংখ্যা। GA-এর hyperparameter অনেক; তাই “GA ব্যবহার করেছি” যথেষ্ট নয়, কোন সেটিং, কত seed, কোন বাজেট আর কার সঙ্গে তুলনা, তা জানাতে হয়।
এক লাইনে tabu search: Fred Glover-এর ১৯৮৬ সালের এই পদ্ধতি hill climbing-কে সম্প্রতি করা চালের একটি “নিষিদ্ধ তালিকা” দেয়, যাতে খারাপ চালেও এগোনো যায় আর সদ্য ছাড়া চূড়ায় ফেরা না হয়; scheduling-এ বহুল ব্যবহৃত বলে পরিচিত।
৮ · Continuous local search — gradient descent আর SciPy-র optimiser
অনেক সমস্যার state হলো কয়েকটি বাস্তব সংখ্যা, যেমন মডেলের parameter; তখন প্রতিবেশী অসীম। Function মসৃণ হলে সবচেয়ে খাড়া দিক বলে দেয় gradient, সব প্রতিবেশী মাপতে হয় না। পর্ব ৩৪-এর gradient descent তাই steepest-ascent hill climbing-এর continuous রূপ (খরচ কমানোর দিকে), আর সেখানেও local minimum-এর ফাঁদ। Convex function-এ, যেমন বাটিতে, local minimum একটিই, তাই local search-ই যথেষ্ট।
বাস্তবে ব্যবহার হয় scipy.optimize. minimize কোনো method না দিলে (সীমা বা শর্ত না থাকলে) BFGS চালায়, যা gradient-এর ইতিহাস থেকে বাঁকও আন্দাজ করে; gradient-এর সূত্র না দিলে finite difference দিয়ে মাপে। Global optimiser: dual_annealing (annealing-এর সাধারণীকৃত রূপ, local search-সহ) আর differential_evolution (Rainer Storn আর Kenneth Price-এর ১৯৯৭ সালের population-ভিত্তিক পদ্ধতি)। পরীক্ষার জন্য Rastrigin function: ডিমের ট্রের মতো, প্রায় প্রতিটি পূর্ণসংখ্যা বিন্দুর কাছে একটি গর্ত, সবচেয়ে গভীরটি (0, 0)-এ, মান 0.
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import minimize, dual_annealing, differential_evolution
bowl = lambda v: (v[0] - 3) ** 2 + (v[1] + 1) ** 2 # মসৃণ বাটি, তলা (3, −1)
r = minimize(bowl, [0.0, 0.0]) # কোনো method না দিলে BFGS
print("বাটি:", np.round(r.x, 4), " nfev =", r.nfev, " success =", r.success)
def f(v): # Rastrigin: অনেক গর্তওয়ালা function, সেরা f = 0 বিন্দু (0, 0)-এ
x, y = v
return 20 + x**2 - 10 * np.cos(2 * np.pi * x) + y**2 - 10 * np.cos(2 * np.pi * y)
box = [(-5.12, 5.12), (-5.12, 5.12)]
starts = np.random.default_rng(42).uniform(-5.12, 5.12, (8, 2))
loc = [minimize(f, s) for s in starts] # local: ঢাল ধরে নামা
for s, o in zip(starts, loc):
print(f"শুরু ({s[0]:5.2f}, {s[1]:5.2f}) → ({o.x[0]:5.2f}, {o.x[1]:5.2f}),"
f" f = {o.fun:6.3f}, nfev = {o.nfev}")
da = dual_annealing(f, box, rng=42)
de = differential_evolution(f, box, rng=42)
for name, o in [("dual_annealing", da), ("differential_evolution", de)]:
print(f"{name:22s}: x = {np.round(o.x, 4) + 0}, f = {o.fun:.1e}, nfev = {o.nfev}")
g = np.linspace(-5.12, 5.12, 300); X, Y = np.meshgrid(g, g)
fig, ax = plt.subplots(figsize=(5, 4.4))
ax.contourf(X, Y, f([X, Y]), levels=30, cmap="viridis")
for s, o in zip(starts, loc):
ax.annotate("", o.x, s, arrowprops=dict(arrowstyle="->", color="white"))
ax.plot(*np.array([o.x for o in loc]).T, "wo", ms=4, label="BFGS end points")
ax.plot(*de.x, "r*", ms=14, label="differential_evolution")
ax.set_title("Rastrigin: local (BFGS) vs global search"); ax.legend(fontsize=8)
plt.show()আসল আউটপুট (ব্রাউজারের Pyodide-এ, SciPy 1.18):
বাটি: [ 3. -1.] nfev = 9 success = True
শুরু ( 2.81, -0.63) → ( 2.98, -0.99), f = 9.950, nfev = 33
শুরু ( 3.67, 2.02) → ( 3.98, 1.99), f = 19.899, nfev = 30
শুরু (-4.16, 4.87) → (-3.98, 4.97), f = 40.793, nfev = 33
শুরু ( 2.67, 2.93) → ( 2.98, 2.98), f = 17.909, nfev = 27
শুরু (-3.81, -0.51) → (-3.98, -0.99), f = 16.914, nfev = 42
শুরু (-1.32, 4.37) → ( 0.00, 3.98), f = 15.919, nfev = 51
শুরু ( 1.47, 3.31) → (-0.99, -2.98), f = 9.950, nfev = 54
শুরু (-0.58, -2.79) → (-0.99, -3.98), f = 16.914, nfev = 30
dual_annealing : x = [0. 0.], f = 7.1e-15, nfev = 4064
differential_evolution: x = [0. 0.], f = 0.0e+00, nfev = 1923
মসৃণ বাটি: BFGS ৯টি function-হিসাবে (finite difference-সহ) ঠিক (3, −1)-এ। Rastrigin, ৮টি শুরু: প্রতিটি run কোনো স্থানীয় গর্তে থেমেছে (প্রায় পূর্ণসংখ্যা বিন্দু), f 9.95 থেকে 40.79, একটিও 0 নয়। (1.47, 3.31) থেকে শুরু করা run গেছে অনেক দূরে (−0.99, −2.98)-এ, কারণ খাড়া ঢালে প্রথম line search বড় লাফ দিয়েছিল; local পদ্ধতি সবসময় সবচেয়ে কাছের গর্তে যায় না। Global optimiser দুটি (0, 0) পেয়েছে, কিন্তু খরচ ৪০৬৪ আর ১৯২৩টি function-হিসাব, একটি local run-এর ৩০–৫০টির ৪০ থেকে ১০০ গুণ। ছবিতে সাদা তির local run-এর শুরু থেকে শেষ, লাল তারা global উত্তর। + 0 শুধু −0.0-কে 0.0 দেখাতে।
দুটি ব্যবহারিক কথা। এক, seed দেওয়া হয়েছে rng=42 দিয়ে; পুরোনো কোড আর টিউটোরিয়ালে একই কাজে seed= দেখবেন, তাই আপনার version-এর docs মিলিয়ে নিন। দুই, global optimiser-কে সীমার বাক্স (bounds) দিতে হয়; বাক্স ভুল হলে সেরা উত্তর বাইরেই থেকে যায়। কোটি parameter-এর মডেলে এই global পদ্ধতিগুলো অচল; সেখানে gradient-ভিত্তিক local search-ই ব্যবহারিক পথ।
৯ · No free lunch, বাস্তবের ব্যবহার আর সৎভাবে ফল জানানো
No free lunch theorem — সাবধানে বলা
এত পদ্ধতির মধ্যে কোনটি সেরা? David Wolpert আর William Macready ১৯৯৭ সালে IEEE Transactions on Evolutionary Computation-এ “No Free Lunch Theorems for Optimization” প্রবন্ধে একটি কঠোর উত্তর দেন। মোটামুটি কথাটি এই: একটি সসীম search space-এ সম্ভাব্য সব objective function-এর ওপর গড় নিলে, যেকোনো দুটি search পদ্ধতির (যারা একই বিন্দু বারবার মাপে না) গড় কর্মক্ষমতা সমান। এলোমেলো খোঁজাও সেই গড়ে annealing বা GA-এর সমান।
এর মানে “সব পদ্ধতি সমান” নয়। সব সম্ভাব্য function-এর বেশির ভাগই কাঠামোহীন, যেখানে কাছের বিন্দুর মান থেকে কিছুই বোঝা যায় না। বাস্তবের সমস্যা তেমন নয়: কাছাকাছি দুটি পথের দৈর্ঘ্য কাছাকাছি, local search ঠিক এই কাঠামোয় বাজি ধরে। আসল শিক্ষা: কোনো পদ্ধতির সুবিধা আসে সমস্যার কাঠামো সম্পর্কে তার অনুমান থেকে; কাঠামো না মিললে সুবিধাও নেই। “আমার algorithm সব সমস্যায় সেরা” দাবি টেকে না।
বাস্তবে কোথায় local search
প্রকাশিত কাজে local search-এর ধারণা দেখা যায় যানবাহনের রুট পরিকল্পনা (vehicle routing), shift আর পরীক্ষার রুটিন, চিপের অংশ সাজানো (VLSI placement) আর গুদামের নকশায়। বাস্তব সফটওয়্যারে প্রায়ই কৌশল মেশানো থাকে: দ্রুত নির্মাণ-পদ্ধতি দিয়ে শুরু, তারপর 2-opt-এর মতো local search, ওপরে annealing বা tabu, কখনো integer programming-এর সঙ্গে জোড়া। কোন কোম্পানি ঠিক কী ব্যবহার করে, তা সাধারণত প্রকাশ্য নয়। বাংলাদেশে ডেলিভারি বা রুটিনের কাজে এমন প্রয়োগ সম্ভব, তবে কোথায় কতটা হচ্ছে তার নির্ভরযোগ্য তথ্য আমাদের জানা নেই।
Seed আর গড় — একটি ভাগ্যবান run নয়
এই পর্বের প্রতিটি তুলনা অনেক run-এর, কারণ একটি run একটি নমুনা। সৎ রিপোর্টের অভ্যাস:
- Seed ঠিক করে লিখে রাখুন (পর্ব ১৬), আর seed-এর তালিকা আগে থেকে ঠিক করুন, ফল দেখে নয়।
- গড় আর ছড়ানো দুটিই জানান: গড়, standard deviation বা সেরা-খারাপ, সফলতার হারের standard error (পর্ব ৩৭).
- সমান বাজেটে তুলনা: সমান সময় বা function-হিসাব; ১০০ গুণ বেশি হিসাবে জেতা আসল জয় নয়।
- সরল baseline: random restart বা nearest neighbour; জটিল পদ্ধতি এদের না হারালে জটিলতার দাম নেই।
- Tuning-এর খরচও গুনুন: ৫০ বার tune করা পদ্ধতিকে একবারের সেটিং-এর পদ্ধতির সঙ্গে তুলনা অন্যায্য।
আরও গভীরে: TSP-এর জন্য 2-opt-এর চেয়ে শক্তিশালী local search Lin–Kernighan (১৯৭৩), যা পরিবর্তনশীল সংখ্যক রেখা একসঙ্গে বদলায়; Keld Helsgaun-এর LKH এর পরিচিত আধুনিক রূপ। নির্ভুল দিকে Concorde solver (Applegate, Bixby, Chvátal ও Cook) হাজার হাজার শহরের অনেক নির্দিষ্ট TSP সমাধান করেছে বলে প্রকাশিত। Local search কেন বাস্তবে এত ভালো অথচ সবচেয়ে খারাপ ক্ষেত্রে খারাপ হতে পারে, তা এখনো গবেষণার বিষয়।
পরীক্ষা ও ইন্টারভিউতে (বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এ ধরনের প্রশ্ন সাধারণ):
- Hill climbing কোথায় আটকে যায়? Local maximum, plateau (shoulder বা সমতল চূড়া) আর ridge-এ; কারণ এটি শুধু ভালো প্রতিবেশীতে যায় আর কিছু মনে রাখে না।
- T = 5, Δ = 3 হলে annealing-এ খারাপ চাল নেওয়ার সম্ভাবনা? exp(−3 ÷ 5) = e−0.6 ≈ 0.549.
- একটি শুরুতে সফলতা p হলে random restart-এ গড়ে কত বার শুরু? 1/p; p = 0.14 হলে প্রায় ৭.১.
- Local beam search আর k-টি random restart-এর পার্থক্য? Beam-এ k-টি state তথ্য ভাগাভাগি করে: সবার প্রতিবেশীর মধ্য থেকে সেরা k-টি টিকে থাকে; restart-এ k-টি run একে অপরকে জানে না।
- TSP-তে সাধারণ crossover কেন চলে না? সন্তানে একই শহর দুবার আর কোনো শহর বাদ পড়তে পারে; তাই order crossover-এর মতো permutation-রক্ষাকারী crossover লাগে।
১০ · AI-এর যুগে — AI সহকারীর optimisation কোড যাচাই
“TSP-এর জন্য simulated annealing লিখে দাও” বললে AI সহকারী কয়েক সেকেন্ডে চলনসই কোড দেয়। কিন্তু “চলে” আর “ভালো কাজ করে” আলাদা কথা। AI সহকারীরা প্রায়ই যা করে:
- গ্রহণের নিয়মে চিহ্ন উল্টে ফেলে (
exp(g / T)), বা খরচ কমানোর সমস্যায় maximise-এর নিয়ম বসায়; ফল বিশ্বাসযোগ্য দেখায়, অথচ পদ্ধতিটি এলোমেলো হাঁটা। - প্রতি ধাপে পুরো পথ নতুন করে মাপে (শতগুণ ধীর), বা উল্টানোর index-এ এক ঘরের ভুল করে, যাতে কিছু চাল কখনো চেষ্টাই হয় না।
- Temperature এককের সঙ্গে মেলায় না (মিটারে T = 1 মানে প্রায় খাঁটি hill climbing), আর একটি run দেখিয়ে “annealing ভালো” বলে।
- পুরোনো বা বানানো API লেখে (
seedবনামrng, অস্তিত্বহীন parameter), আরwarnings.filterwarnings("ignore")দিয়ে warning চাপা দেয়। - না-মাপা দাবি করে: “GA সবসময় global optimum পায়”, বা না-চালানো আউটপুট বানিয়ে লেখে।
কোড চাওয়ার সময় যাচাই, baseline আর অনেক seed নিজেই চেয়ে নিন; নিচের প্রম্পটে আসল ঠিকানা নেই, seed-করা বানানো বিন্দু।
AI-এর কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১পারমাণবিক অস্ত্রের গবেষণাগারের একটি simulation কীভাবে চিপ-নকশা আর ডেলিভারি রুটের হাতিয়ার হলো? এই উপমা কতটা খাটে?
১৯৫৩ সালে Journal of Chemical Physics-এ Nicholas Metropolis, Arianna ও Marshall Rosenbluth, Augusta ও Edward Teller-এর প্রবন্ধ “Equation of State Calculations by Fast Computing Machines” প্রকাশিত হয়। কাজটি হয়েছিল Los Alamos-এ, MANIAC কম্পিউটারে; প্রশ্ন ছিল নির্দিষ্ট তাপমাত্রায় অনেক কণার ব্যবস্থার গড় ধর্ম। তাঁদের কৌশল ছিল এলোমেলো ছোট বদল প্রস্তাব করা, শক্তি কমলে মেনে নেওয়া, আর শক্তি বাড়লে exp(−ΔE ÷ kT) সম্ভাবনায় মেনে নেওয়া। এটিই অংশ ৫-এর নিয়ম। পরে কিছু ইতিহাস-আলোচনায় বলা হয়েছে যে পদ্ধতির খুঁটিনাটির বড় অংশ Rosenbluth দম্পতির হাতে তৈরি; কৃতিত্বের ভাগ নিয়ে তাই মতভেদ আছে।
ত্রিশ বছর পর IBM-এর গবেষক Scott Kirkpatrick, C. Daniel Gelatt আর Mario Vecchi একটি উল্টো প্রশ্ন করেন: শক্তির জায়গায় যদি একটি optimisation সমস্যার খরচ বসাই, যেমন চিপে তারের মোট দৈর্ঘ্য বা সেলসম্যানের পথ? তাহলে খুব ধীরে তাপমাত্রা কমালে ব্যবস্থাটি কম শক্তির, মানে কম খরচের, অবস্থায় স্থির হবে, যেমন ধীরে ঠান্ডা করা ধাতু ভালো স্ফটিক গড়ে। ১৯৮৩ সালের Science প্রবন্ধে তাঁরা চিপের অংশ ভাগ ও সাজানোর সমস্যা আর travelling salesman-এ এটি দেখান। Vlado Černý স্বাধীনভাবে প্রায় একই ভাবনা প্রকাশ করেন ১৯৮৫ সালে।
উপমাটি কোথায় খাটে আর কোথায় নয়, তা বোঝা জরুরি। খাটে এখানে: “তাপমাত্রা” সত্যিই একটি নিয়ন্ত্রণ-সংখ্যা যা অন্বেষণ আর শোষণের (exploration বনাম exploitation) ভারসাম্য ঠিক করে, আর ধীরে ঠান্ডা করা সাধারণত দ্রুত ঠান্ডা করার চেয়ে ভালো ফল দেয়। খাটে না এখানে: optimisation-এর “শক্তি”-র ভূদৃশ্যে পদার্থবিজ্ঞানের কোনো নিয়ম মানতে হয় না, temperature schedule আর প্রতিবেশী-নিয়ম সম্পূর্ণ আমাদের পছন্দ, আর তাত্ত্বিক নিশ্চয়তা পেতে যতটা ধীরে ঠান্ডা করতে হয় তা বাস্তবে অচল। পরের তুলনামূলক গবেষণায় ভালোভাবে tune করা অন্য পদ্ধতি প্রায়ই annealing-এর সমান বা ভালো করেছে।
মূল উপলব্ধি: একটি ক্ষেত্রের গণিত আরেক ক্ষেত্রে নতুন হাতিয়ার হতে পারে, কিন্তু উপমাটি অনুপ্রেরণা, প্রমাণ নয়; কাজ করে কি না, তা প্রতিটি সমস্যায় মেপে দেখতে হয়।
প্র ০২Genetic algorithm-কে একসময় প্রায় সার্বজনীন সমাধান মনে করা হতো। তত্ত্ব কী বলেছিল, আর পরীক্ষায় কী দেখা গেল?
John Holland Michigan বিশ্ববিদ্যালয়ে ১৯৬০-এর দশক থেকে অভিযোজন (adaptation) নিয়ে কাজ করছিলেন, আর ১৯৭৫ সালের বই “Adaptation in Natural and Artificial Systems”-এ genetic algorithm-কে একটি সাধারণ কাঠামো হিসেবে দাঁড় করান। তাঁর একটি কেন্দ্রীয় ফল schema theorem নামে পরিচিত: ক্রোমোজোমের ছোট, ভালো “নকশা” (যেমন পাশাপাশি কয়েকটি নির্দিষ্ট মান) selection-এর চাপে প্রজন্ম থেকে প্রজন্মে বাড়তে থাকে। এর ওপর ভিত্তি করে building block hypothesis প্রস্তাবিত হয়: GA ছোট ভালো টুকরো খুঁজে crossover দিয়ে জুড়ে বড় ভালো সমাধান বানায়।
১৯৮০ আর ৯০-এর দশকে GA খুব জনপ্রিয় হয়, David Goldberg-এর ১৯৮৯ সালের পাঠ্যবই তাতে বড় ভূমিকা রাখে। কিন্তু পরীক্ষা কিছু অস্বস্তিকর প্রশ্ন তোলে। Melanie Mitchell, Stephanie Forrest আর John Holland নিজেই ১৯৯০-এর দশকের শুরুতে “Royal Road” নামের function বানান, যা building block-এর ভাবনার পক্ষে সাজানো ছিল। পরে Mitchell ও সহকর্মীরা দেখান যে এমন function-এও একটি সরল পদ্ধতি, এলোমেলো mutation দিয়ে hill climbing, অনেক ক্ষেত্রে GA-এর চেয়ে কম হিসাবে উত্তর পায়। Schema theorem-ও সমালোচিত হয়েছে: এটি এক প্রজন্মের প্রত্যাশিত বদল বলে, কিন্তু GA শেষে কোথায় পৌঁছাবে বা কত দ্রুত, তা বলে না।
আজকের সাধারণ মত মোটামুটি এই: বিবর্তন-ভিত্তিক পদ্ধতি দরকারি হাতিয়ার, বিশেষত যখন gradient নেই, objective noisy বা অনেক লক্ষ্য একসঙ্গে (multi-objective), অথবা সমস্যার গঠন জানা কঠিন। কিন্তু এরা “স্বয়ংক্রিয় সর্বজয়ী” নয়। অংশ ৭-এর টেবিলে দেখেছেন, mutation-এর হার বদলাতেই একই GA nearest neighbour-এর চেয়েও খারাপ হয়ে যায়। ভালো ফলের পেছনে প্রায়ই থাকে সমস্যা-নির্দিষ্ট crossover, ভালো mutation (যেমন 2-opt চাল) আর local search-এর সঙ্গে জোড়া (memetic algorithm).
মূল উপলব্ধি: প্রকৃতির উপমা আকর্ষণীয় হলেও একটি পদ্ধতির মূল্য ঠিক হয় সৎ তুলনায়, সমান বাজেটে আর সরল baseline-এর বিপক্ষে; GA-এর ইতিহাস এই শিক্ষার একটি ভালো উদাহরণ।
প্র ০৩No free lunch theorem কি বলে যে optimisation-এর গবেষণা অর্থহীন? ভুল ব্যাখ্যাগুলো কী, আর ঠিক শিক্ষাটি কী?
David Wolpert আর William Macready ১৯৯৫ সালে Santa Fe Institute-এর একটি technical report-এ আর ১৯৯৭ সালে IEEE Transactions on Evolutionary Computation-এর প্রথম সংখ্যায় ফলটি প্রকাশ করেন। ছক এমন: একটি সসীম search space, সসীম সম্ভাব্য মানের সেট, আর একটি search পদ্ধতি যা একের পর এক বিন্দু বেছে মাপে, আগের মাপগুলো দেখে, কিন্তু একই বিন্দু দুবার মাপে না। তাঁরা দেখান, সব সম্ভাব্য objective function-এর ওপর সমান ওজনে গড় নিলে, m-টি মাপের পরে যা দেখা যায় তার বণ্টন যেকোনো দুটি পদ্ধতির জন্য একই। ফলে যেকোনো কর্মক্ষমতার মাপে গড় ফল সমান।
প্রথম ভুল ব্যাখ্যা: “তাহলে সব পদ্ধতি বাস্তবে সমান।” না, কারণ “সব function-এ সমান ওজন” বাস্তবের ছবি নয়। সমান ওজনে নেওয়া function-এর প্রায় সবই কাঠামোহীন, যেখানে একটি বিন্দুর মান পাশের বিন্দু সম্পর্কে কিছুই বলে না। বাস্তবের TSP, রুটিন বা মডেল-fitting-এ কাছাকাছি সমাধানের মান কাছাকাছি, আর local search ঠিক এই মসৃণতার ওপর দাঁড়ানো। দ্বিতীয় ভুল ব্যাখ্যা: “এলোমেলো খোঁজাই যথেষ্ট।” সব function-এর গড়ে হ্যাঁ, কিন্তু আপনার সমস্যার ধরনে প্রায় নিশ্চিতভাবে না; অংশ ৬-এ এলোমেলো পথ ছিল ৯০ কিমির ঘরে, annealing 38.5. তৃতীয় ভুল: theorem-টি সসীম, নির্দিষ্ট ছকের; continuous বা বিশেষ শর্তের সমস্যায় এর রূপ নিয়ে পরে আলাদা আলোচনা হয়েছে।
ঠিক শিক্ষাটি গভীর। কোনো পদ্ধতির সুবিধা আসে সমস্যা সম্পর্কে তার অন্তর্নিহিত অনুমান থেকে: hill climbing ধরে নেয় ভালো সমাধানের কাছেই ভালো সমাধান, GA ধরে নেয় ভালো টুকরো জোড়া দিলে ভালো সমাধান। অনুমান মিললে লাভ, না মিললে ক্ষতি। তাই “এই সমস্যার কাঠামো কী” প্রশ্নটি “কোন algorithm সবচেয়ে শক্তিশালী” প্রশ্নের চেয়ে বেশি জরুরি। মেশিন লার্নিং-এ Wolpert-এর ১৯৯৬ সালের একটি সম্পর্কিত ফলও প্রায়ই উদ্ধৃত হয়: কোনো অনুমান ছাড়া কোনো learning পদ্ধতি সব সমস্যায় অন্যটির চেয়ে ভালো হতে পারে না।
মূল উপলব্ধি: সর্বজনীন সেরা optimiser নেই; প্রতিটি সফল পদ্ধতি সমস্যার কাঠামো সম্পর্কে একটি বাজি, আর ভালো প্রকৌশলী সেই বাজিটি সচেতনভাবে বেছে নেন ও মেপে দেখেন।
প্র ০৪TSP কঠিন বলে প্রমাণিত, তবু হাজার হাজার শহরের TSP সমাধান হয়েছে আর ডেলিভারি কোম্পানিগুলো প্রতিদিন রুট বানায়। এই দুই সত্য একসঙ্গে কীভাবে?
১৯৫৪ সালে George Dantzig, Ray Fulkerson আর Selmer Johnson যুক্তরাষ্ট্রের ৪৯টি শহরের একটি TSP নির্ভুলভাবে সমাধান করেন, linear programming আর “cutting plane” কৌশলে, অনেকটা হাতে হিসাব করে। ১৯৭২ সালে Richard Karp-এর বিখ্যাত তালিকায় Hamiltonian cycle-এর মতো সমস্যা NP-complete প্রমাণিত হয়, আর TSP-এর সিদ্ধান্ত-রূপও (দৈর্ঘ্য L-এর কম কোনো পথ আছে কি) এই শ্রেণির। অর্থাৎ সব ক্ষেত্রে দ্রুত নির্ভুল সমাধানের কোনো পদ্ধতি জানা নেই, আর P ≠ NP হলে থাকবেও না (DSA কোর্সের P বনাম NP পর্বে বিস্তারিত)।
দুই সত্য মেলে তিনটি কারণে। এক, NP-hardness সবচেয়ে খারাপ ক্ষেত্রের কথা; বাস্তবের অনেক সমস্যা এত খারাপ নয়, আর Concorde-এর মতো solver চতুর branch-and-cut দিয়ে বহু বড় নির্দিষ্ট সমস্যা নির্ভুলভাবে সমাধান করেছে, কখনো অনেক দিনের হিসাবে। দুই, বাস্তবে নির্ভুল সেরা উত্তর প্রায়ই দরকারই নেই; সেরার ১–২% কাছাকাছি উত্তর এক সেকেন্ডে পাওয়া অনেক বেশি কাজের। এখানেই local search: G. A. Croes ১৯৫৮ সালে 2-opt-এর ভাবনা প্রকাশ করেন, Shen Lin আর Brian Kernighan ১৯৭৩ সালে আরও শক্তিশালী Lin–Kernighan পদ্ধতি দেন, যা আজও অনেক heuristic-এর ভিত্তি। তিন, কিছু ক্ষেত্রে গুণমানের নিশ্চয়তাসহ approximation আছে: দূরত্ব ত্রিভুজ-নিয়ম মানলে Nicos Christofides-এর ১৯৭৬ সালের পদ্ধতি সেরার সর্বোচ্চ ১.৫ গুণের মধ্যে পথ দেয়।
কিন্তু বাস্তবের ডেলিভারি TSP-এর চেয়ে জটিল: অনেক গাড়ি, প্রতিটির ধারণক্ষমতা, ক্রেতার সময়ের জানালা, যানজটে বদলানো যাতায়াত-সময়, মাঝপথে নতুন অর্ডার। এই vehicle routing সমস্যাগুলোর জন্য সাধারণত local search-ভিত্তিক metaheuristic আর গাণিতিক programming মিলিয়ে ব্যবহার করা হয় বলে প্রকাশিত কাজে দেখা যায়, যদিও নির্দিষ্ট কোম্পানির পদ্ধতি সাধারণত গোপন।
মূল উপলব্ধি: “তাত্ত্বিকভাবে কঠিন” মানে “বাস্তবে অসমাধানযোগ্য” নয়; কঠিনতার তত্ত্ব বলে কোথায় নিশ্চয়তা ছাড়তে হবে, আর local search বলে নিশ্চয়তা ছেড়ে কীভাবে দ্রুত ভালো উত্তর পাওয়া যায়।
প্র ০৫“আমাদের নতুন পদ্ধতি আগেরটির চেয়ে ৫% ভালো” — এমন দাবি দেখলে কোন প্রশ্নগুলো করবেন? Seed আর benchmark-এর ইতিহাস কী শেখায়?
১৯৯৫ সালে John Hooker “Testing Heuristics: We Have It All Wrong” নামের একটি প্রবন্ধে optimisation গবেষণার একটি অভ্যাসের সমালোচনা করেন: নতুন heuristic প্রস্তাব করে কয়েকটি benchmark-এ পুরোনোটিকে হারানো দেখানো, আর তাকেই অগ্রগতি বলা। তাঁর যুক্তি ছিল, এতে বোঝা যায় না কেন একটি পদ্ধতি কাজ করে, কোন ধরনের সমস্যায় করে, আর তুলনাটি ন্যায্য ছিল কি না। তিনি নিয়ন্ত্রিত পরীক্ষার পক্ষে বলেন, যেখানে সমস্যার একটি বৈশিষ্ট্য বদলে দেখা হয় ফল কীভাবে বদলায়।
একই ধরনের সমস্যা পরে মেশিন লার্নিং-এ স্পষ্ট হয়। Peter Henderson ও সহকর্মীদের ২০১৮ সালের প্রবন্ধ “Deep Reinforcement Learning that Matters”-এ দেখানো হয় যে শুধু random seed বদলালেই একই algorithm-এর ফল এত বদলাতে পারে যে কয়েকটি seed-এর গড় দিয়ে দুটি পদ্ধতির পার্থক্যকে বিভ্রান্তিকরভাবে বড় বা ছোট দেখানো সম্ভব। এই পর্বেই ছোট আকারে একই জিনিস দেখেছেন: 2-opt-এর ১০টি run 38.5 থেকে 41.4, প্রায় ৭.৫% ছড়ানো। কেউ ভাগ্যবান seed বেছে নিলে “৫% উন্নতি” বানানো কঠিন নয়, এমনকি অসৎ উদ্দেশ্য ছাড়াও, শুধু বারবার চালিয়ে ভালোটি রিপোর্ট করলে।
তাই দাবি দেখলে প্রশ্নগুলো: কত seed, আর seed কি আগে থেকে ঠিক করা ছিল? গড়ের সঙ্গে ছড়ানো বা confidence interval কত? দুই পদ্ধতি কি সমান সময় বা সমান function-হিসাব পেয়েছিল? নতুন পদ্ধতির সেটিং কতবার tune করা হয়েছে, আর পুরোনোটির কতবার? Benchmark-গুলো কি আগে থেকে ঠিক করা, না ফল দেখে বাছা? সরল baseline (random restart) কেমন করেছিল? ফলাফল কি অন্য ধরনের instance-এ টেকে?
মূল উপলব্ধি: এলোমেলো পদ্ধতির একটি run একটি নমুনা, প্রমাণ নয়; সৎ তুলনা মানে আগে থেকে ঠিক করা seed, সমান বাজেট, ছড়ানোসহ গড় আর একটি সরল baseline.
অনুশীলন
প্রথমটি হাতে হিসাবের, দ্বিতীয়টি আউটপুট অনুমানের, তৃতীয়টি নকশা ও সমালোচনার, শেষটি কোড লেখার।
-
হাতে হিসাব: (ক) 4-queens-এ q = [0, 1, 2, 3] (সব রানি একটি কর্ণে)। h (আক্রমণের জোড়া) কত? একটি রানিকে নিজের কলামে সরালে সবচেয়ে ছোট h কত হতে পারে, আর এমন একটি চাল বলুন। (খ) Annealing-এ T = 5, একটি চালে খরচ বাড়ে Δ = 3. গ্রহণের সম্ভাবনা কত? T = 0.5 হলে? (গ) একটি hill climbing-এর প্রতিটি শুরুতে সফলতার সম্ভাবনা 0.14. গড়ে কত বার শুরু লাগবে, আর ১০ বার শুরু করেও সব ব্যর্থ হওয়ার সম্ভাবনা কত?
(ক) ৪টি রানিই একই কর্ণে, তাই প্রতিটি জোড়া আক্রমণ করে: C(4, 2) = 6. ১২টি প্রতিবেশীর মধ্যে সবচেয়ে ছোট h = 4 (এমন ৮টি চাল আছে)। যেমন কলাম a-এর রানিকে সারি 0 থেকে 1-এ: q = [1, 1, 2, 3]. তখন a আর b একই সারিতে (১), আর b, c, d এখনো একই কর্ণে (৩টি জোড়া), মোট ৪. (ডেস্কটপে সব ১২টি প্রতিবেশী গুনে মেলানো।)
(খ) exp(−3 ÷ 5) = e−0.6 ≈ 0.549, মানে প্রায় অর্ধেক ক্ষেত্রে নেওয়া হবে। T = 0.5-এ exp(−6) ≈ 0.0025, চারশো বারে প্রায় একবার।
(গ) প্রথম সাফল্য পর্যন্ত শুরুর সংখ্যা geometric distribution মানে, গড় 1 ÷ 0.14 ≈ 7.1 বার। সব ১০টি ব্যর্থ: 0.8610 ≈ 0.221, মানে ১০ বার শুরুতে সাফল্যের সম্ভাবনা প্রায় ৭৮%.
-
আউটপুট অনুমান করুন: একটি একমাত্রিক ভূদৃশ্যে steepest-ascent hill climbing. চালানোর আগে তিনটি লাইনের আউটপুট লিখুন, তারপর যেকোনো Python-এ চালিয়ে মিলিয়ে দেখুন। কোন run local maximum-এ আটকেছে?
vals = [1, 3, 4, 4, 2, 6, 9, 7] def climb(i): path = [i] while True: nb = [j for j in (i - 1, i + 1) if 0 <= j < len(vals)] j = max(nb, key=lambda k: vals[k]) if vals[j] <= vals[i]: return path i = j path.append(i) for s in (0, 4, 7): p = climb(s) print(s, p, vals[p[-1]])0 [0, 1, 2] 4 4 [4, 5, 6] 9 7 [7, 6] 9শুরু 0: মান 1 → 3 → 4 (index 2). সেখানে ডানের প্রতিবেশী index 3-এর মানও 4, “ভালো নয়” (
<=), তাই থামা। এটি একটি সমতল চূড়া (index 2 আর 3), local maximum, মান 4; global maximum 9 অনেক দূরে। শুরু 4: মান 2 থেকে ডানে 6, তারপর 9, থামা: global maximum. শুরু 7: বাঁয়ে 9-এ, থামা। একই নিয়ম, শুরুর জায়গা আলাদা, ফল আলাদা। (ডেস্কটপে Python 3.14.4-এ চালিয়ে মেলানো।) -
নকশা ও সমালোচনা: একজন সহকর্মী (বা একটি AI সহকারী) রিপোর্ট দিয়েছেন: “আমাদের নতুন GA রুট-পরিকল্পনায় simulated annealing-এর চেয়ে ৬% ছোট পথ দেয়। পরীক্ষা: একটি ৫০-ঠিকানার সমস্যা, GA এক ঘণ্টা চালানো (সেটিং ৩০ বার বদলে সেরাটি), annealing পাঁচ মিনিট (প্রথম চেষ্টার সেটিং), দুটিই একবার করে।” অন্তত চারটি সমস্যা চিহ্নিত করুন আর একটি ন্যায্য পরীক্ষার নকশা দিন।
- অসমান বাজেট: এক ঘণ্টা বনাম পাঁচ মিনিট; সমান সময় বা সমান পথ-হিসাব দিতে হবে।
- অসমান tuning: GA-এর ৩০টি সেটিং-এর সেরাটি, annealing-এর একটিই; দুটির জন্য সমান tuning, আলাদা সমস্যায়।
- একটি run, একটি seed: অন্তত ১০–৩০টি seed-এর গড় আর ছড়ানো লাগবে; অংশ ৬-এ 2-opt-এর run-গুলোর মধ্যেই প্রায় ৭.৫% পার্থক্য।
- একটি সমস্যা: একটি instance থেকে সাধারণ সিদ্ধান্ত হয় না; বিভিন্ন মাপের অনেক instance লাগবে।
- মাপকাঠি নেই: ছোট instance-এ নির্ভুল উত্তর (Held–Karp), বড়তে সেরা-জানা উত্তর বা lower bound, আর একটি সরল baseline (nearest neighbour + 2-opt).
- ন্যায্য নকশা: আগে ঠিক করা instance আর seed; সমান বাজেট ও tuning; প্রতি instance-এ গড়, standard deviation আর সেরা-জানা উত্তর থেকে শতকরা দূরত্ব।
-
কোড লিখুন: 8-queens-এর জন্য first-choice hill climbing লিখুন: প্রতিবার একটি এলোমেলো কলাম আর এলোমেলো নতুন সারি বেছে প্রতিবেশী বানান; প্রথম যে প্রতিবেশীর h বর্তমানের চেয়ে কম, সেখানে যান। পরপর ২০০টি চেষ্টায় উন্নতি না পেলে থামুন। seed 7 দিয়ে ৫০০টি এলোমেলো বোর্ডে সফলতার হার ছাপান, আর steepest-ascent-এর ১৩.৪%-এর সঙ্গে তুলনা করুন।
import random N = 8 def h(q): return sum(q[i] == q[j] or abs(q[i] - q[j]) == j - i for i in range(N) for j in range(i + 1, N)) def first_choice(q, tries=200): cur = h(q) while cur > 0: for _ in range(tries): # এলোমেলো প্রতিবেশী c, r = random.randrange(N), random.randrange(N) if r == q[c]: continue new = q[:c] + [r] + q[c + 1:] if h(new) < cur: # প্রথম ভালোটিতেই যাওয়া q, cur = new, h(new); break else: return False # tries বার চেষ্টায়ও উন্নতি নেই return True random.seed(7) ok = sum(first_choice([random.randrange(N) for _ in range(N)]) for _ in range(500)) print(f"first-choice: সফল {ok}/500 = {100 * ok / 500:.1f}%")ডেস্কটপে চালিয়ে পাওয়া ফল (Python 3.14.4):
first-choice: সফল 69/500 = 13.8%. Steepest-ascent-এর ১৩.৪%-এর প্রায় সমান, আর পার্থক্যটি অংশ ৪-এর ±১ শতাংশ-বিন্দুর অনিশ্চয়তার মধ্যে। এটাই প্রত্যাশিত: দুই পদ্ধতিই কঠোরভাবে শুধু উন্নতিতে যায়, তাই একই local minimum-গুলোতে আটকায়; first-choice-এর সুবিধা গতিতে (প্রতি ধাপে সব প্রতিবেশী মাপতে হয় না), সফলতায় নয়।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমSimulated annealing-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 🎲 একটি শুরুর পূর্ণ সমাধান আর শুরুর তাপমাত্রা T ঠিক করা
- 🔀 এলোমেলোভাবে একটি প্রতিবেশী বানানো (যেমন একটি 2-opt চাল)
- 📏 খরচের বদল Δ হিসাব করা
- 🎯 Δ < 0 হলে নেওয়া, নইলে exp(−Δ/T) সম্ভাবনায় নেওয়া
- ❄️ T কমানো (যেমন T ← a·T)
- 🛑 থামার নিয়ম দেখা: ধাপের সীমা বা T যথেষ্ট ছোট
কোন সমস্যাটি local search-এর জন্য সবচেয়ে স্বাভাবিক, যেখানে শুধু শেষ অবস্থাটিই উত্তর আর সেখানে পৌঁছানোর পথের কোনো দাম নেই?
- একটি মানচিত্রে দুই শহরের মধ্যে সবচেয়ে ছোট রাস্তার ক্রম বের করা
- শিক্ষার্থীদের সংঘাত কম রেখে একটি পরীক্ষার রুটিন বানানো
- 8-puzzle-এর লক্ষ্য-ছবিতে পৌঁছানোর চালগুলো বের করা
- Tic-tac-toe-তে প্রতিপক্ষের বিপক্ষে সেরা পরের চাল বাছা
Steepest-ascent hill climbing একটি state-এ থেমে গেছে, কারণ কোনো প্রতিবেশী বর্তমানের চেয়ে ভালো নয়। এ থেকে নিশ্চিতভাবে কী বলা যায়?
- এটি global maximum
- এটি অবশ্যই একটি plateau
- এর চেয়ে ভালো কোনো সমাধান নেই
- বর্তমান প্রতিবেশী-নিয়মে এটি একটি local maximum বা সমতল জায়গা
Simulated annealing-এ তাপমাত্রা T = 2 আর একটি চালে খরচ বাড়ে Δ = 4. চালটি গ্রহণের সম্ভাবনা কত? (e ≈ 2.718)
- 1
- 0.5
- প্রায় 0.135
- প্রায় 0.018
TSP-এর জন্য genetic algorithm-এ order crossover (OX) কেন ব্যবহার করা হয়, সাধারণ “এক বিন্দুতে কেটে জোড়া” crossover-এর বদলে?
- OX সবসময় দ্রুততর চলে
- সাধারণ crossover-এ সন্তান বৈধ পথ নাও হতে পারে; OX প্রতিটি শহর ঠিক একবার রাখে
- OX সন্তানকে বাবা-মায়ের চেয়ে ভালো হওয়ার নিশ্চয়তা দেয়
- OX থাকলে mutation-এর দরকার হয় না
No free lunch theorem (Wolpert ও Macready, ১৯৯৭)-এর সঠিক ব্যাখ্যা কোনটি?
- বাস্তবের যেকোনো সমস্যায় সব optimisation পদ্ধতি সমান ভালো
- এলোমেলো খোঁজাই সবসময় সবচেয়ে নিরাপদ পছন্দ
- সব সম্ভাব্য objective function-এর গড়ে সব পদ্ধতি সমান, তাই সুবিধা আসে সমস্যার কাঠামো মেলানো থেকে
- Genetic algorithm সব সমস্যায় অন্য পদ্ধতির চেয়ে ভালো
একটি এলোমেলো local search পদ্ধতিকে আরেকটির সঙ্গে তুলনার জন্য সবচেয়ে ভালো রিপোর্ট কোনটি?
- প্রতিটি পদ্ধতির একটি run-এর ফল
- প্রতিটি পদ্ধতির ১০টি run-এর মধ্যে সেরা run-টি
- যে পদ্ধতি বেশি সময় পেয়েছে, তার অনেক run-এর গড়
- আগে থেকে ঠিক করা অনেক seed-এ সমান বাজেটে গড়, ছড়ানো আর একটি baseline-এর সঙ্গে তুলনা
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- পথ নয়, শেষ অবস্থা: রুটের ক্রম, বোর্ড, রুটিনের মতো optimisation সমস্যায় local search একটি পূর্ণ সমাধান নিয়ে ছোট বদলে তাকে ভালো করে; মেমরি প্রায় শূন্য, কিন্তু complete বা optimal-এর নিশ্চয়তা নেই।
- ভূদৃশ্য = objective function + প্রতিবেশী-নিয়ম. Hill climbing আটকায় local maximum, plateau আর ridge-এ। 8-queens-এ এই পাতার run: শুধু-উন্নতিতে ১৩.৪%, sideways-সহ ৯৩.৬%, random restart-এ গড়ে প্রায় ৭ বার শুরু।
- Simulated annealing: খারাপ চাল exp(−Δ/T) সম্ভাবনায়, T ধীরে কমে; শুরুতে অন্বেষণ, শেষে hill climbing. ২০টি পয়েন্টের TSP-তে ১০টির ৯টি seed সেরা পথ 38.50 কিমি পেয়েছে।
- Beam search আর GA অনেক state একসঙ্গে রাখে; GA-এর selection, order crossover, mutation আর elitism কাজ করে, কিন্তু সেটিং বদলালে ফল অনেক বদলায়। GA জাদু নয়।
- Continuous জগতে gradient descent হলো hill climbing; local optimiser কাছের গর্তে থামে, global optimiser বেশি হিসাব খরচ করে। কোনো সর্বজনীন সেরা পদ্ধতি নেই (no free lunch), তাই সবসময় অনেক seed, সমান বাজেট আর baseline.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪৫ · Propositional logic — বাক্য, truth table আর inferenceপরবর্তী পর্বযুক্তির মডিউল শুরু: truth table, entailment, CNF, resolution আর SAT.
- AI-এর ভিত্তি আর Python for AI · কোর্স হোম৭৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- পর্ব ৩৪ · Derivative আর gradient descent — ঢাল ধরে নিচে নামাঝালাইContinuous local search-এর ভিত্তি, local minimum আর saddle point.
- DSA · NP-hard সমস্যা বাস্তবেগভীরে যানTSP-এর মতো কঠিন সমস্যায় নির্ভুল, approximation আর heuristic পথ।
- DSA · Backtrackingপাশের ট্র্যাকn-queens-কে পদ্ধতিগতভাবে সমাধান, local search-এর উল্টো ভাবনা।
- DSA · Randomized algorithmপাশের ট্র্যাকএলোমেলোতা দিয়ে দ্রুত উত্তর আর প্রত্যাশিত খরচের হিসাব।