অধ্যায় 6 · প্রজেক্ট ও পরের ধাপ
প্রজেক্ট: একটা AI রিভিউ বিশ্লেষক
- পৃষ্ঠা 19 / 20
- 5 মিনিট পড়া
এবার সবকিছু একসাথে জোড়া লাগানোর পালা। আপনি ছোট কিন্তু বাস্তবসম্মত একটা AI টুল বানাবেন: এটা একটা CSV ফাইল থেকে গ্রাহকের রিভিউ পড়ে, প্রতিটার সেন্টিমেন্ট (positive, negative বা neutral) আর বিষয় (topic) বের করে, আর প্রতি প্রোডাক্টের একটা সারাংশ প্রিন্ট করে। কোম্পানিগুলো হাজার হাজার রিভিউ আর সাপোর্ট টিকিটের ওপর ঠিক এই ধরনের পাইপলাইন চালায়।
এতে এই টিউটোরিয়ালের প্রায় প্রতিটা পাতা কাজে লাগে: ফাইল আর CSV (পাতা ১০), pandas (পাতা ১৪), ফাংশন আর ডেটাক্লাস (পাতা ৭, ১২), এরর সামলানো (পাতা ১১), JSON (পাতা ১০), LLM API (পাতা ১৭) আর যাচাই (পাতা ১৮)।
নকশা
- একই আকারের দুটো ক্লাসিফায়ার।
classify_offlineকিওয়ার্ডের নিয়ম ব্যবহার করে: বিনামূল্যে, তাৎক্ষণিক, প্রতিবার একই ফল — টাকা খরচ না করে প্রোগ্রামের বাকি অংশ বানানো আর পরীক্ষার জন্য আদর্শ।classify_with_llmএকটা মডেলকে জিজ্ঞেস করে আর অনেক বেশি নির্ভুল। দুটোই একটাAnalysisফেরত দেয়, তাই কোনটা চলল তাতে বাকি কোডের কিছু যায় আসে না। - মডেলের উত্তর যাচাই। আমরা যে JSON চেয়েছি তা না হলে একটা মেসেজসহ বাদ দেওয়া হয়, পুরো রান থামিয়ে দিতে বা ফলাফল নষ্ট করতে দেওয়া হয় না।
- ফলাফল ক্যাশ করা। শেষ হওয়া প্রতিটা বিশ্লেষণ
cache.jsonl-এ যোগ হয়। প্রোগ্রাম আবার চালালে আগে হয়ে যাওয়া রিভিউগুলো আবার না পাঠিয়ে (আর টাকা না দিয়ে) ক্যাশ থেকে পড়া হয়। মাঝপথে প্রোগ্রাম থেমে গেলেও শেষ হওয়া কিছুই হারায় না। - কমান্ড-লাইন ইন্টারফেস।
argparseকমান্ড লাইনের শব্দগুলোকে অপশনে বদলায় আর আপনার হয়ে একটা--helpমেসেজ লিখে দেয়।
ডেটা: reviews.csv
id,product,review
r1,Headphones,Great sound and fast delivery. Love them!
r2,Headphones,Stopped working after a week. I want a refund.
r3,Charger,Arrived late but works perfectly.
r4,Charger,Terrible. It broke on the first day.
r5,Keyboard,"Excellent keys, very happy with it."
r6,Keyboard,It is a keyboard. It types.
r7,Headphones,"Delivery was slow, sound is great."
r8,Charger,"Fast charging, perfect for my phone."প্রোগ্রাম: analyse_reviews.py
"""LLM দিয়ে — অথবা অফলাইনে সাধারণ কিওয়ার্ডের নিয়মে — গ্রাহকের রিভিউ বিশ্লেষণ।"""
import argparse
import json
from dataclasses import asdict, dataclass
from pathlib import Path
import pandas as pd
MODEL = "gpt-5.5"
ALLOWED = {"positive", "negative", "neutral"}
CACHE = Path("cache.jsonl")
@dataclass
class Analysis:
sentiment: str
topic: str
def classify_offline(text: str) -> Analysis:
"""মোটা দাগের কিওয়ার্ড-ভিত্তি: বিনামূল্যে, তাৎক্ষণিক, আর পাইপলাইন পরীক্ষার জন্য ভালো।"""
words = text.lower()
good = ["love", "great", "excellent", "perfect", "fast", "happy"]
bad = ["broke", "late", "terrible", "refund", "slow", "stopped"]
score = sum(w in words for w in good) - sum(w in words for w in bad)
sentiment = "positive" if score > 0 else "negative" if score < 0 else "neutral"
topic = "delivery" if any(w in words for w in ["deliver", "arrived", "late"]) else "product"
return Analysis(sentiment, topic)
def classify_with_llm(client, text: str) -> Analysis:
response = client.responses.create(
model=MODEL,
instructions=(
"Classify the customer review. Reply with JSON only, like "
'{"sentiment": "positive", "topic": "delivery"}. '
"sentiment is positive, negative or neutral; topic is one lower-case word."
),
input=text,
)
data = json.loads(response.output_text) # ValueError তুলতে পারে
if data.get("sentiment") not in ALLOWED:
raise ValueError(f"unexpected sentiment {data.get('sentiment')!r}")
return Analysis(data["sentiment"], str(data.get("topic", "other")).lower())
def load_cache() -> dict[str, dict]:
if not CACHE.exists():
return {}
with open(CACHE, encoding="utf-8") as f:
return {row["id"]: row for row in map(json.loads, f)}
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("csv", help="a CSV file with id, product and review columns")
parser.add_argument("--offline", action="store_true", help="use keyword rules instead of an LLM")
args = parser.parse_args()
reviews = pd.read_csv(args.csv)
cache = load_cache()
client = None
if not args.offline:
from openai import OpenAI
client = OpenAI(timeout=30, max_retries=3)
results = []
with open(CACHE, "a", encoding="utf-8") as cache_file:
for row in reviews.itertuples():
if row.id in cache: # আগে বিশ্লেষণ হয়েছে: দ্বিতীয় API কল নয়
results.append(cache[row.id])
continue
try:
if args.offline:
analysis = classify_offline(row.review)
else:
analysis = classify_with_llm(client, row.review)
except (ValueError, KeyError) as error:
print(f"Skipping {row.id}: {error}")
continue
record = {"id": row.id, "product": row.product, **asdict(analysis)}
cache_file.write(json.dumps(record, ensure_ascii=False) + "\n")
results.append(record)
df = pd.DataFrame(results)
df.to_csv("results.csv", index=False)
print(f"Analysed {len(df)} of {len(reviews)} reviews\n")
print(pd.crosstab(df["product"], df["sentiment"]))
print("\nTopics:", df["topic"].value_counts().to_dict())
if __name__ == "__main__":
main()চালান
python analyse_reviews.py reviews.csv --offline # বিনামূল্যে: কিওয়ার্ডের নিয়ম
python analyse_reviews.py reviews.csv # আসল: মডেল ব্যবহার করেঅফলাইন মোড (এটা আসল আউটপুট):
Analysed 8 of 8 reviews
sentiment negative neutral positive
product
Charger 1 1 1
Headphones 1 1 1
Keyboard 0 1 1
Topics: {'product': 5, 'delivery': 3}মডেল দিয়ে (উদাহরণ আউটপুট — মডেলের বিচার একটু বদলাতে পারে; এখানে r6-এর জন্য সে JSON-এর বদলে সাধারণ বাক্য লিখেছে, যা প্রোগ্রাম ধরে বাদ দিয়েছে):
Skipping r6: Expecting value: line 1 column 1 (char 0)
Analysed 7 of 8 reviews
sentiment negative neutral positive
product
Charger 1 0 2
Headphones 1 1 1
Keyboard 0 0 1
Topics: {'durability': 2, 'delivery': 2, 'sound': 1, 'typing': 1, 'charging': 1}ফলাফল সমালোচনার চোখে পড়ুন
দুটো রান তুলনা করুন। কিওয়ার্ডের নিয়ম r3-কে ("Arrived late but works perfectly") neutral বলেছে: একটা ভালো আর একটা খারাপ শব্দ কাটাকাটি হয়ে গেছে। মডেল বুঝেছে যে ডেলিভারি দেরি হলেও গ্রাহক প্রোডাক্টে খুশি। তার দেওয়া বিষয়গুলোও — durability, charging, typing — নিয়মের সাদামাটা "product"-এর চেয়ে অনেক বেশি কিছু বলে। কিওয়ার্ডের নিয়ম অর্থ পড়তে পারে না; মডেল পারে — কিন্তু তাতে টাকা আর সময় লাগে, আর মাঝে মাঝে অপ্রত্যাশিত কিছু ফেরত আসে। এই আপসই AI ইঞ্জিনিয়ারিংয়ের রোজকার বাস্তবতা, আর classify_offline-এর মতো সস্তা একটা ভিত্তি দিয়েই যাচাই করা যায় যে দামি মডেলটা আসলেই ভালো কি না।
নিজের মতো করে নিন
- একটা
--limit Nঅপশন যোগ করুন, যা শুধু প্রথম N-টা রিভিউ বিশ্লেষণ করে — সস্তায় পরীক্ষা চালানোর জন্য। - মডেলের JSON অবৈধ হলে ইনপুটে "Reply with JSON only" যোগ করে রিভিউটা একবার আবার চেষ্টা করুন।
AsyncOpenAIআর একটা সেমাফোর (পাতা ১৮) দিয়ে একসাথে ৫টা রিভিউ বিশ্লেষণ করুন।- Matplotlib দিয়ে প্রতি প্রোডাক্টের সেন্টিমেন্টের একটা বার চার্ট এঁকে
report.pngহিসেবে সংরক্ষণ করুন। - বাংলায় দশটা রিভিউ লিখুন আর দেখুন মডেল ইংরেজির মতোই ভালোভাবে সামলাতে পারে কি না।