অধ্যায় 5 · AI কাজের জন্য Python
ডেটা স্ট্রাকচার ও অ্যালগরিদম: দ্রুত কোড লেখা
- পৃষ্ঠা 19 / 23
- 6 মিনিট পড়া
AI প্রোগ্রাম অনেক ডেটা সামলায়: হাজার হাজার ডকুমেন্ট, লাখ লাখ এমবেডিং, API-র জন্য অপেক্ষমাণ রিকোয়েস্টের সারি। ছোট ডেটায় যেকোনো কোডই দ্রুত মনে হয়। আসল ডেটায় ভালো আর খারাপ ডেটা স্ট্রাকচার (জিনিস কীভাবে রাখছেন) ও অ্যালগরিদম (কোন ধাপে কাজ করছেন) বাছাইয়ের পার্থক্য হলো এক সেকেন্ড আর এক ঘণ্টার পার্থক্য। এই পাতায় সেই মূল বিষয়গুলো, যা প্রতিটা ইন্টারভিউয়ার — আর প্রতিটা প্রোডাকশন সিস্টেম — আশা করে।
Big-O: কাজ কীভাবে বাড়ে
Big-O নোটেশন বলে ইনপুট বড় হলে ধাপের সংখ্যা কীভাবে বাড়ে। এটা আপনার কম্পিউটারের আসল গতি উপেক্ষা করে একটাই প্রশ্ন করে: ডেটা দশ গুণ বড় হলে কাজ কতটা বাড়বে?
| Big-O | নাম | n = ১০,০০,০০০ হলে ধাপ | Python উদাহরণ |
|---|---|---|---|
O(1) | ধ্রুবক | ১ | d[key], x in a_set, items.append(x) |
O(log n) | লগারিদমিক | প্রায় ২০ | বাইনারি সার্চ, bisect, heapq.heappush |
O(n) | লিনিয়ার | ১০,০০,০০০ | প্রতিটা আইটেমের ওপর লুপ, x in a_list, sum(items) |
O(n log n) | "n log n" | প্রায় ২,০০,০০,০০০ | sorted(items) |
O(n²) | দ্বিঘাত | ১০,০০,০০,০০,০০,০০০ | একই ডেটার ওপর লুপের ভেতরে লুপ |
শেষ সারিটাই ভয়ের। প্রতিটা ডকুমেন্টকে বাকি প্রতিটার সাথে তুলনা করা O(n²): ১০০টা ডকুমেন্টে ঠিক আছে, দশ লাখে অসম্ভব।
লিনিয়ার সার্চ বনাম বাইনারি সার্চ
লিস্টে কিছু খুঁজতে সহজ উপায় হলো একে একে প্রতিটা আইটেম দেখা। লিস্ট সাজানো থাকলে অনেক ভালো উপায় আছে: মাঝখানটা দেখুন, যে অর্ধেকে লক্ষ্য থাকতে পারে না সেটা বাদ দিন, আবার করুন।
def linear_search(items, target):
"""একে একে প্রতিটা আইটেম দেখে। ফেরত দেয় (অবস্থান, ধাপ)।"""
steps = 0
for i, item in enumerate(items):
steps += 1
if item == target:
return i, steps
return -1, steps
def binary_search(items, target):
"""আইটেমগুলো সাজানো থাকতে হবে। প্রতি ধাপে খোঁজার পরিসর অর্ধেক।"""
low, high, steps = 0, len(items) - 1, 0
while low <= high:
steps += 1
mid = (low + high) // 2
if items[mid] == target:
return mid, steps
if items[mid] < target:
low = mid + 1 # লক্ষ্য ডান অর্ধেকে
else:
high = mid - 1 # লক্ষ্য বাম অর্ধেকে
return -1, steps
ids = list(range(0, 2_000_000, 2)) # সাজানো দশ লাখ জোড় সংখ্যা
print(linear_search(ids, 1_999_998))
print(binary_search(ids, 1_999_998))(999999, 1000000)
(999999, 20)দশ লাখ ধাপ বনাম বিশ। বাইনারি সার্চের প্রতিটা ধাপ পরিসর অর্ধেক করে, আর দশ লাখকে প্রায় ২০ বার অর্ধেক করলেই একটা আইটেম বাকি থাকে — এটাই O(log n)। নিজে খুব কমই লিখবেন: যেকোনো সাজানো লিস্টে Python-এর bisect মডিউল কাজটা করে দেয়।
ঠিক স্ট্রাকচার বাছাই: লিস্ট, সেট নাকি dict
লিস্ট আইটেমগুলো ক্রমে রাখে, তাই x in a_list-কে একে একে যাচাই করতে হয়। সেট বা dict ব্যবহার করে হ্যাশিং: Python মানটাকে এমন একটা সংখ্যায় বদলায়, যা ঠিক কোথায় দেখতে হবে বলে দেয়, তাই যত বড়ই হোক এক ধাপেই যাচাই।
import timeit
ids_list = list(range(1_000_000))
ids_set = set(ids_list)
in_list = timeit.timeit(lambda: 999_999 in ids_list, number=100)
in_set = timeit.timeit(lambda: 999_999 in ids_set, number=100)
print(f"list: {in_list * 1000:.1f} ms for 100 lookups")
print(f"set: {in_set * 1000:.3f} ms for 100 lookups")
print(f"the set was about {in_list / in_set:,.0f} times faster")list: 813.8 ms for 100 lookups
set: 0.006 ms for 100 lookups
the set was about 131,255 times fasterআপনার সংখ্যা আলাদা হবে — কম্পিউটারের ওপর নির্ভর করে — কিন্তু ফারাকটা হবে বিশাল। লুপের ভেতরে যখনই if x in something লিখবেন, ভাবুন something সেট হওয়া উচিত কিনা। কোনো key ধরে বাড়তি ডেটা খুঁজতে হবে, যেমন id ধরে ডকুমেন্টের লেখা? dict ব্যবহার করুন।
স্ট্যাক আর কিউ
দুটো সহজ ধরন সব জায়গায় দেখা যায়। স্ট্যাক হলো শেষে ঢোকে, আগে বের হয় — থালার স্তূপের মতো, বা undo-র ইতিহাস। সাধারণ লিস্টই চমৎকার স্ট্যাক। কিউ হলো আগে ঢোকে, আগে বের হয় — লাইনে দাঁড়ানো মানুষের মতো, বা API-র জন্য অপেক্ষমাণ কাজ। এর জন্য collections.deque ব্যবহার করুন: লিস্টের সামনে থেকে সরানো O(n), কারণ বাকি প্রতিটা আইটেম সরাতে হয়, আর deque.popleft() হলো O(1)।
from collections import deque
# স্ট্যাক: শেষে ঢোকে, আগে বের হয় — undo-র ইতিহাস
history = []
history.append("typed 'Hello'")
history.append("made it bold")
history.append("deleted a line")
print("undo:", history.pop())
print("undo:", history.pop())
print("left:", history)
# কিউ: আগে ঢোকে, আগে বের হয় — রেট-লিমিটেড API-র জন্য অপেক্ষমাণ রিকোয়েস্ট
waiting = deque(["summarise doc1", "summarise doc2"])
waiting.append("summarise doc3")
while waiting:
print("sending:", waiting.popleft())undo: deleted a line
undo: made it bold
left: ["typed 'Hello'"]
sending: summarise doc1
sending: summarise doc2
sending: summarise doc3হিপ: সব না সাজিয়েই সেরা k
RAG সিস্টেমে খোঁজা শেষ হয় "সবচেয়ে মিল থাকা k-টা ডকুমেন্ট দাও" দিয়ে। সব স্কোর সাজানো O(n log n); heapq.nlargest যেতে যেতে শুধু সেরা k-টা রাখে, যা k ছোট হলে দ্রুততর। হিপ একটা প্রায়োরিটি কিউ-ও: সবচেয়ে ছোট অগ্রাধিকার সংখ্যার আইটেম সবসময় আগে বের হয়।
import heapq
# প্রতিটা ডকুমেন্ট একটা প্রশ্নের সাথে কতটা মেলে (এমবেডিং থেকে, পাতা ১৩)
scores = {"refunds.md": 0.91, "shipping.md": 0.42, "warranty.md": 0.77,
"privacy.md": 0.18, "returns.md": 0.88, "careers.md": 0.05}
top3 = heapq.nlargest(3, scores.items(), key=lambda pair: pair[1])
print(top3)
# প্রায়োরিটি কিউ: সবচেয়ে ছোট সংখ্যা আগে বের হয়
jobs = []
heapq.heappush(jobs, (2, "nightly report"))
heapq.heappush(jobs, (0, "customer is waiting"))
heapq.heappush(jobs, (1, "retry failed call"))
while jobs:
priority, job = heapq.heappop(jobs)
print(priority, job)[('refunds.md', 0.91), ('returns.md', 0.88), ('warranty.md', 0.77)]
0 customer is waiting
1 retry failed call
2 nightly reportরিকার্শন: যে ফাংশন নিজেকেই ডাকে
কিছু ডেটা অজানা গভীরতা পর্যন্ত নেস্ট করা থাকে: API-র JSON, ফোল্ডারের ভেতরে ফোল্ডার, এজেন্টের ধাপের গাছ। রিকার্সিভ ফাংশন এটা স্বাভাবিকভাবে সামলায়: সহজ ক্ষেত্রটা সরাসরি সামলায় (বেস কেস) আর প্রতিটা ছোট টুকরোর ওপর নিজেকে ডাকে। এখানে এটা একটা নেস্টেড মডেল-উত্তরের প্রতিটা মান গোনে:
def count_values(data):
"""নেস্টেড dict আর লিস্টের ভেতরের প্রতিটা সাধারণ মান গোনে।"""
if isinstance(data, dict):
return sum(count_values(v) for v in data.values())
if isinstance(data, list):
return sum(count_values(v) for v in data)
return 1 # বেস কেস: একটা মাত্র মান
response = {
"model": "gpt-5.5",
"output": [
{"type": "text", "text": "Hello!"},
{"type": "tool_call", "name": "search", "args": {"query": "tokens", "limit": 3}},
],
"usage": {"input_tokens": 12, "output_tokens": 5},
}
print(count_values(response))9প্রতিটা রিকার্সিভ ফাংশনের একটা বেস কেস লাগে, যা তাকে থামায়। না থাকলে নিজেকে ডাকতেই থাকে, যতক্ষণ না Python RecursionError দিয়ে হাল ছাড়ে (ডিফল্টে প্রায় ১,০০০ স্তরের পর)।
মেমোইজেশন: একই হিসাব দুবার নয়
সাধারণ রিকার্সিভ ফাংশন একই কাজ বারবার করতে পারে। functools-এর @cache — পাতা ৮-এর মতো একটা ডেকোরেটর — প্রতিটা উত্তর প্রথমবার হিসাবের সময় মনে রাখে:
from functools import cache
calls = 0
def fib(n):
global calls
calls += 1
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(25), "calls:", calls)
calls = 0
@cache
def fib_fast(n):
global calls
calls += 1
return n if n < 2 else fib_fast(n - 1) + fib_fast(n - 2)
print(fib_fast(25), "calls:", calls)75025 calls: 242785
75025 calls: 26একই উত্তর, ২,৪২,৭৮৫টা কল বনাম ২৬টা। AI অ্যাপেও ক্যাশিং সবচেয়ে কার্যকর অপ্টিমাইজেশনের একটা: একই প্রম্পট বা একই এমবেডিং রিকোয়েস্ট দুবার এলে আরেকটা মডেল কলের পয়সা না দিয়ে ক্যাশ থেকে উত্তর দিন।
নিজে চেষ্টা করুন
bisect.bisect_leftদিয়ে খুঁজুন1_999_998ids-এর কোথায় আছে, আর মিলিয়ে দেখুন ওপরের বাইনারি সার্চের সাথে মেলে কিনা।- আপনার কাছে ১০,০০০টা নিষিদ্ধ শব্দ আর ১০,০০,০০০টা মেসেজ আছে। যাচাইটা এমনভাবে লিখুন যাতে দ্রুত হয় — নিষিদ্ধ শব্দগুলো কোন স্ট্রাকচারে রাখবেন?
- একটা রিকার্সিভ ফাংশন লিখুন, যা একটা dict-এর সবচেয়ে গভীর নেস্টিং স্তর খুঁজে বের করে।