এই পর্বে যা শিখবেন
- Semantic network আর frame থেকে আজকের knowledge graph পর্যন্ত জ্ঞান লিখে রাখার ধারা, সংক্ষেপে
- বাজারঘরের পণ্য, ব্র্যান্ড, গ্রাহক, অর্ডার আর জেলাকে triple-এ লেখা, আর Python-এ set ও তিনটি index দিয়ে ছোট একটি triple store বানানো
- Variable-সহ pattern query, আর কয়েকটি pattern join করে multi-hop প্রশ্নের উত্তর হাতে trace করা
- Class, subClassOf, domain আর range দিয়ে schema লেখা; transitivity দিয়ে নতুন triple অনুমান করা, আর ভুল is-a রেখার বিপদ
- RDF, IRI, Turtle আর SPARQL দিয়ে rdflib-এ আসল query; property graph-এর সঙ্গে তুলনা; networkx-এ পথ খোঁজা; entity resolution, provenance আর গোপনীয়তা
আগের পর্বগুলোতে যুক্তি লেখা হয়েছে বাক্য আর নিয়ম দিয়ে: পর্ব ৪৫-এ propositional logic, পর্ব ৪৬-এ first-order logic-এর বস্তু আর সম্পর্ক, পর্ব ৪৭-এ “যদি … তাহলে …” নিয়মের expert system. এই পর্বের প্রশ্ন: হাজারো ছোট তথ্য (কে কোথায় থাকেন, কোন পণ্য কে বানায়) কীভাবে রাখলে যন্ত্র সেগুলো জুড়ে উত্তর দিতে পারে, আর উত্তরের প্রতিটি ধাপ দেখাতেও পারে। বাজারঘর এই কোর্সের কাল্পনিক অনলাইন দোকান; এই পর্বের সব ব্র্যান্ড (Shimul, Bokul, Kodom), বিক্রেতা, গ্রাহক আর অর্ডার বানানো।
১ · জ্ঞান লিখে রাখার পুরোনো স্বপ্ন — semantic network থেকে knowledge graph
পাড়ার দোকানের করিম চাচার মাথায় হাজারো ছোট তথ্য: “রুমানা আপা সিলেট থেকে অর্ডার দেন”, “Kodom-এর cable Shimul S10-এ লাগে”, “Kodom charger-ও বানায়”। “সিলেটের কে কে এমন ব্র্যান্ডের জিনিস নিয়েছেন যারা charger-ও বানায়?” প্রশ্নে তিনি কয়েকটি তথ্য জোড়া লাগিয়ে উত্তর দেন। Knowledge graph এই ছবিটিকে যন্ত্রে লেখার উপায়।
পরিবারের বংশতালিকার কথা ভাবুন। কাগজে নামগুলো বৃত্তে লেখা, আর রেখায় লেখা “বাবা”, “স্ত্রী”, “ছেলে”। “নানার ভাইয়ের নাতি কে?” প্রশ্নের উত্তর কোনো একটি বৃত্তে লেখা নেই; কয়েকটি রেখা ধরে হেঁটে বের করতে হয়। বংশতালিকা একটি ছোট knowledge graph: বৃত্ত = entity, রেখার নাম = সম্পর্ক।
সংক্ষিপ্ত ইতিহাস। ১৯৬০-এর দশকে Ross Quillian মানুষের শব্দার্থ-স্মৃতির একটি মডেল হিসেবে semantic network-এর ধারণা দেন: ধারণাগুলো node, আর “is-a” বা “has”-এর মতো রেখা দিয়ে যুক্ত, যেমন “ক্যানারি is-a পাখি, পাখি has ডানা”। তাঁর গবেষণাপত্র সাধারণত ১৯৬৬–৬৮ সালের বলে উল্লেখ করা হয়, আর Collins ও Quillian-এর ১৯৬৯ সালের পরীক্ষায় এই স্তরভিত্তিক স্মৃতির ধারণা যাচাই করার চেষ্টা হয়েছিল। ১৯৭৪ সালে Marvin Minsky MIT-র একটি memo-তে “A Framework for Representing Knowledge” লেখেন, যেখানে frame মানে একটি পরিস্থিতির ছাঁচ: “ঘর” frame-এর ঘরগুলো (slot) হলো দেয়াল, দরজা, জানালা, আর না জানা থাকলে আগে থেকে ধরে নেওয়া মান। পরে এসেছে description logic, ১৯৮০-র দশকের Cyc প্রকল্প, ২০০১ সালের দিকে “Semantic Web”-এর প্রস্তাব আর W3C-র মানদণ্ড। “Knowledge graph” শব্দটি আগেও ছিল, কিন্তু ২০১২ সালে একটি বড় সার্চ ইঞ্জিন কোম্পানির ঘোষণার পর শিল্পে ব্যাপক পরিচিতি পায় বলে সাধারণত বলা হয়; তারিখগুলো উৎসভেদে কিছুটা আলাদা।
এই ধারার সাধারণ নাম knowledge representation: জ্ঞানকে এমন রূপে লেখা, যাতে যন্ত্র তা থেকে প্রশ্নের উত্তর আর নতুন সিদ্ধান্ত বের করতে পারে। পর্ব ৩-এ দেখা symbolic AI-এর এটি একটি মূল খুঁটি। Knowledge graph একটি বাস্তববাদী আপস: first-order logic-এর পুরো শক্তি নয়, কিন্তু বিপুল সরল তথ্য রাখা আর জুড়ে খোঁজার উপযোগী।
Knowledge graph = entity (node) + টাইপ-দেওয়া সম্পর্ক (edge) + তাদের অর্থের নিয়ম (schema). সাধারণ graph-এ edge শুধু “যুক্ত”; knowledge graph-এ প্রতিটি edge-এর একটি নাম আছে (made_by, lives_in), আর সেই নামেরও অর্থ লেখা যায় (“made_by-এর বাঁ দিকে পণ্য, ডান দিকে ব্র্যান্ড”)। Graph-এর মৌলিক পরিভাষা DSA কোর্সের graph পর্বে আছে।
২ · Triple — subject, predicate, object
Knowledge graph-এর একটি তথ্য তিন অংশের একটি ছোট বাক্য, যার নাম triple:
- Subject (কর্তা): কার কথা হচ্ছে। যেমন
CB1, একটি USB-C cable. - Predicate (সম্পর্ক): কী সম্পর্ক। যেমন
made_by. - Object (কর্ম): কার সঙ্গে বা কোন মান। যেমন
Kodom, একটি ব্র্যান্ড।
বাক্যের মতো পড়ুন: “CB1 made_by Kodom” মানে “CB1 বানিয়েছে Kodom”. graph-এর ভাষায় CB1 থেকে Kodom-এর দিকে একটি তীর, যার গায়ে লেখা made_by. Object কখনো আরেকটি entity (Kodom), কখনো একটি সাধারণ মান বা literal (যেমন নাম “Kodom USB-C Cable”). বাজারঘরের graph-এ এই রকম ৫০টি triple আছে, ১১ ধরনের predicate দিয়ে:
| Predicate | বাঁ দিকে (subject) | ডান দিকে (object) | উদাহরণ |
|---|---|---|---|
type | পণ্য | ক্যাটাগরি (class) | CH33 type Charger |
made_by | পণ্য | ব্র্যান্ড | S10 made_by Shimul |
compatible_with | পণ্য | পণ্য | CB1 compatible_with S10 |
sold_by, located_in | পণ্য; বিক্রেতা | বিক্রেতা; জেলা | CB1 sold_by SabujStore |
lives_in, part_of | গ্রাহক; জেলা | জেলা; বিভাগ | Sylhet part_of SylhetDivision |
placed_by, contains | অর্ডার | গ্রাহক; পণ্য | O2 contains CB1 |
subClassOf, name | class; পণ্য | বড় class; লেখা | Charger subClassOf MobileAccessory |
খেয়াল করুন, অর্ডার নিজেও একটি entity (O2). “মিতু CB1 কিনেছেন” সরাসরি না লিখে অর্ডারের মাধ্যমে লেখা হয়েছে, কারণ কেনাকাটার আরও তথ্য থাকে (তারিখ, দাম, কয়টি); ঘটনাকে নিজস্ব node বানানোর এই কৌশল খুব প্রচলিত।
টেবিল থাকতে graph কেন? একই তথ্য relational database-এর কয়েকটি টেবিলে রাখা যায় (customers, orders, products), আর multi-hop প্রশ্ন SQL-এর কয়েকটি join দিয়ে করা যায় (পর্ব ২৫-এর merge-এর মতো)। Graph-এর সুবিধা: নতুন সম্পর্ক যোগ করতে টেবিলের কাঠামো বদলাতে হয় না, কয়েক ধাপ দূরের সম্পর্ক খোঁজা স্বাভাবিক, আর আলাদা উৎসের ডেটা এক graph-এ জোড়া যায়। দাম হলো কাঠামোর নিশ্চয়তা কমে, যা schema দিয়ে (অংশ ৫) ফেরাতে হয়।
৩ · Python-এ triple store — set, তিনটি index আর pattern
Triple রাখার সবচেয়ে সরল উপায় Python-এর set, যার প্রতিটি উপাদান একটি tuple: ("CB1", "made_by", "Kodom"). Set একই triple দুবার রাখে না। কিন্তু “S10-এর সঙ্গে কোন কোন পণ্য চলে?” প্রশ্নে subject জানা নেই, আর ৫ কোটি triple একে একে দেখা অসম্ভব ধীর। তাই তিনটি সূচি বা index রাখা হয়: subject দিয়ে, predicate দিয়ে আর object দিয়ে। প্রতিটি index একটি dict, যার চাবি একটি নাম আর মান সেই নামের সব triple-এর set.
লাইব্রেরিতে একই বইয়ের কার্ড তিনটি আলমারিতে থাকে: লেখক, বিষয় আর শিরোনাম ধরে। যা জানেন, সেই আলমারি খোলেন। Triple store-এর তিনটি index সেই তিনটি আলমারি।
প্রশ্ন লেখা হয় triple pattern দিয়ে: triple-এর মতোই তিন ঘর, কিন্তু কোনো ঘরে ? দিয়ে শুরু হওয়া একটি variable থাকতে পারে। (?p, compatible_with, S10) মানে “এমন সব ?p খুঁজুন, যার জন্য (?p, compatible_with, S10) triple-টি store-এ আছে”। একটি মিল পেলে variable একটি মানে বাঁধা পড়ে, একে বলে binding: {?p: CB1}. এটি পর্ব ৪৭-এর নিয়ম মেলানো আর পর্ব ৪৬-এর unification-এর সরলতম রূপ: এখানে variable শুধু একটি নাম, ভেতরে কোনো function বা গঠন নেই।
1s, p, o = (?p, compatible_with, S10)2cands = by_o["S10"] # s is a variable3for t in cands:4 if t.predicate == p: bind ?p = t.subject
| candidate | |
| ?p | |
| পাওয়া উত্তর |
- Pattern পড়া। তিনটি ঘরের মধ্যে প্রথমটি variable (
?p), বাকি দুটি স্থির: predicatecompatible_with, objectS10. এখনো কোনো variable মান পায়নি। - Index বাছাই। Subject অজানা, object জানা, তাই object-এর index খোলা হলো:
by_o["S10"]-এ ৪টি triple. ৫০টির বদলে মাত্র ৪টি দেখতে হবে। - প্রথম প্রার্থী মিলল।
(CB1, compatible_with, S10): predicate মিলেছে, তাই?pবাঁধা পড়লCB1-এ। একটি উত্তর পাওয়া গেল। - দ্বিতীয় প্রার্থীও মিলল।
(CH20, compatible_with, S10): Shimul-এর নিজের charger. এবার?p = CH20, মোট ২টি উত্তর। - Predicate মেলেনি।
(O3, contains, S10)-এর object S10 ঠিকই, কিন্তু সম্পর্কটিcontains, তাই বাদ। Object-এর index সম্পর্ক না দেখে সব S10-কে একসঙ্গে রাখে। - শেষ প্রার্থীও বাদ, উত্তর দুটি।
(O4, contains, S10)-ও বাদ। ৪টি দেখে ২টি উত্তর।by_p["compatible_with"]খুললে ঠিক ২টি প্রার্থী আসত, কোনো অপচয় ছাড়া; কোন index ছোট, তা দেখে বাছা ভালো query engine-এর কাজ (অনুশীলন ২)। Set-এর ভেতরের ক্রম নির্দিষ্ট নয়, তাই প্রার্থীরা অন্য ক্রমেও আসতে পারে; উত্তর একই থাকে।
৪ · Multi-hop প্রশ্ন — pattern জোড়া দিয়ে উত্তর
এবার করিম চাচার প্রশ্ন: “সিলেটে থাকেন এমন কোন গ্রাহক এমন একটি পণ্য কিনেছেন, যার ব্র্যান্ড charger-ও বানায়?” কোনো একটি triple-এ এর উত্তর নেই। প্রশ্নটিকে ছয়টি ছোট pattern-এ ভাঙা যায়, যেগুলো একই variable দিয়ে পরস্পরের সঙ্গে বাঁধা:
(?c lives_in Sylhet) গ্রাহক ?c সিলেটে থাকেন
(?o placed_by ?c) অর্ডার ?o তাঁর দেওয়া
(?o contains ?p) সেই অর্ডারে পণ্য ?p
(?p made_by ?brand) ?p বানিয়েছে ?brand
(?x made_by ?brand) একই ?brand আরেকটি ?x বানায়
(?x type Charger) আর ?x একটি Charger
প্রতিটি “hop” মানে graph-এর একটি রেখা পার হওয়া। কৌশল হলো pattern একটি একটি করে join করা: আগের pattern-এর প্রতিটি binding পরের pattern-এ বসিয়ে মেলানো; না মিললে সারি বাদ, একাধিকভাবে মিললে কয়েকটি সারিতে ভাগ। এটি SQL-এর inner join-এর মতো; সাধারণ variable-গুলো join key.
1(?c lives_in Sylhet)2(?o placed_by ?c)3(?o contains ?p)4(?p made_by ?brand)5(?x made_by ?brand)6(?x type Charger)
| Rumana | |
| Mitu | |
| Nasrin | |
| মোট সারি |
- Pattern ১.
by_o["Sylhet"]-এ ৪টি triple: তিনজন গ্রাহকেরlives_inআর একটি বিক্রেতারlocated_in. Predicate মেলে তিনটিতে, তাই ৩টি সারি: Rumana, Mitu, Nasrin. - Pattern ২. এখন
?cজানা, তাই object-এর index দিয়ে তাঁর অর্ডার খোঁজা। Rumana-র অর্ডার দুটি (O1, O6), তাই তাঁর সারি দুই ভাগ; Mitu-র O2, Nasrin-এর O4. মোট ৪টি সারি। - Pattern ৩.
?oজানা, subject-এর index দিয়ে অর্ডারের পণ্য: O1-এ BUDS (earbuds), O6-এ PB10 (power bank), O2-এ CB1 (cable), O4-এ S10 (ফোন)। এখনো ৪টি সারি। - Pattern ৪. ব্র্যান্ড বাঁধা পড়ল: BUDS আর PB10 Bokul-এর, CB1 Kodom-এর, S10 Shimul-এর। সারি ৪টিই।
- Pattern ৫. এবার
?xনতুন variable, আর?brandজানা। Bokul দুটি পণ্য বানায়, তাই Rumana-র দুটি সারি দুই গুণ হয়ে ৪টি; Kodom-এর CB1 আর CH33, Shimul-এর S10 আর CH20. মোট ৮টি সারি। মাঝপথে সারি বাড়া স্বাভাবিক। - Pattern ৬, উত্তর। শুধু সেই সারি টেকে যেখানে
?xএকটি Charger. Bokul-এর কোনো charger নেই, তাই Rumana-র সব সারি বাদ। Mitu-র সারিতে CH33, Nasrin-এর সারিতে CH20 টিকে গেল। উত্তর: Mitu আর Nasrin, আর প্রতিটির পক্ষে প্রমাণের পথও হাতে আছে।
এই পর্বের ডেটা নিচে পাতায় একটি ছোট লেখা-ফাইল (shop_kg.txt) হিসেবে রাখা আছে, প্রতি লাইনে একটি triple. প্রথম কোড ব্লক সেটি পড়ে store আর তিনটি index বানায়, match একটি pattern মেলায়, আর query pattern-গুলো একে একে join করে। চারটি কোড ব্লক একই namespace ভাগ করে, তাই পরের ব্লক এখানকার function ব্যবহার করে; পরের ব্লক আগে চালালে এটি নিজে আগে চলবে। প্রথমবার চালালে ব্রাউজারে Python নামতে কয়েক সেকেন্ড লাগতে পারে।
from collections import defaultdict
def build(T): # store + three indexes
global triples, by_s, by_p, by_o
triples, by_s, by_p, by_o = set(T), *(defaultdict(set) for _ in "spo")
for t in triples:
by_s[t[0]].add(t); by_p[t[1]].add(t); by_o[t[2]].add(t)
lines = open("shop_kg.txt", encoding="utf-8").read().splitlines()
build(tuple(l.split(None, 2)) for l in lines if l and l[0] != "#")
print(len(triples), "triples,", len(by_p), "predicates")
def is_var(x): return x.startswith("?")
def match(pattern, b):
s, p, o = [b.get(x, x) for x in pattern] # fill bound variables
cands = (by_s[s] if not is_var(s) else by_o[o] if not is_var(o)
else by_p[p] if not is_var(p) else triples)
for t in cands:
nb = dict(b)
if all(nb.setdefault(x, v) == v if is_var(x) else x == v
for x, v in zip((s, p, o), t)):
yield nb
def query(patterns, show=False):
rows = [{}]
for pat in patterns: # join, one pattern at a time
rows = [nb for b in rows for nb in match(pat, b)]
if show: print(f" {' '.join(pat):26} -> {len(rows)} rows")
return rows
print(sorted(r["?p"] for r in query([("?p", "compatible_with", "S10")])))
q = [("?c", "lives_in", "Sylhet"), ("?o", "placed_by", "?c"),
("?o", "contains", "?p"), ("?p", "made_by", "?brand"),
("?x", "made_by", "?brand"), ("?x", "type", "Charger")]
ans = query(q, show=True)
print(sorted({(r["?c"], r["?p"], r["?brand"]) for r in ans}))আসল আউটপুট:
50 triples, 11 predicates
['CB1', 'CH20']
?c lives_in Sylhet -> 3 rows
?o placed_by ?c -> 4 rows
?o contains ?p -> 4 rows
?p made_by ?brand -> 4 rows
?x made_by ?brand -> 8 rows
?x type Charger -> 2 rows
[('Mitu', 'CB1', 'Kodom'), ('Nasrin', 'S10', 'Shimul')]
সারির সংখ্যা stepper-এর সঙ্গে হুবহু মেলে: ৩ → ৪ → ৪ → ৪ → ৮ → ২। match-এর মূল কৌশল হলো b.get(x, x): আগের pattern যে variable বেঁধে দিয়েছে, সেটি এখানে তার মানে বদলে যায়, তাই পরের pattern-এ ঘরটি আর ফাঁকা থাকে না আর ছোট index খোলা যায়। nb.setdefault(x, v) == v নতুন variable-কে মান দেয়, আর একই pattern-এ একই variable দুবার থাকলে (যেমন (?a, compatible_with, ?a)) দুটি মান সমান কি না তাও দেখে। ফল ছাপানোর আগে sorted, কারণ string-এর set-এর ক্রম প্রতিবার Python চালালে বদলাতে পারে।
Pattern-এর ক্রম গুরুত্বপূর্ণ। যদি প্রথম pattern-এ একটিও জানা মান না থাকে ((?o contains ?p) সবার আগে), তখন পুরো predicate-এর সব triple মধ্যবর্তী ফলে আসে; লাখো triple-এর graph-এ খারাপ ক্রম মানে বিশাল মধ্যবর্তী ফল। আসল query engine সেজন্য একটি query optimizer দিয়ে আগে আন্দাজ করে কোন pattern সবচেয়ে কম সারি দেবে, ঠিক relational database-এর মতো।
৫ · Ontology আর schema — class, is-a আর অনুমান
এখন একটি সহজ প্রশ্ন: “বাজারঘরে কোন কোন Electronics পণ্য আছে?” উপরের ৫০টি triple-এ (?x, type, Electronics) একটিও নেই। অথচ মানুষ জানে charger একটি মোবাইল অ্যাক্সেসরি, আর অ্যাক্সেসরি একটি ইলেকট্রনিক্স পণ্য। এই জ্ঞান লেখা থাকে schema-য়, যার আরেক নাম ontology: কোন কোন class বা শ্রেণি আছে, কোনটি কার ভেতরে, আর কোন সম্পর্ক কোন শ্রেণির মধ্যে খাটে।
- Class: একই ধরনের entity-র দল। Phone, Charger, Electronics, Product.
- Instance: একটি নির্দিষ্ট জিনিস, যা কোনো class-এর সদস্য। CH20 একটি Charger:
(CH20, type, Charger). - subClassOf (is-a): একটি class আরেকটির ভেতরে।
(Charger, subClassOf, MobileAccessory)মানে প্রতিটি charger একটি mobile accessory-ও। - Property: সম্পর্ক নিজেও schema-তে বর্ণনা করা যায়। domain আর range বলে সম্পর্কের দুই পাশে কোন class:
made_by-এর domain Product, range Brand.
বাজারঘরের ছোট class-স্তর এরকম: Charger, Cable আর PowerBank → MobileAccessory → Electronics → Product; Earbuds → AudioDevice → Electronics; Phone → Electronics. এখান থেকে দুটি নিয়ম চালিয়ে নতুন triple বের করা যায়। প্রতীক দিয়ে লিখলে (→ মানে “তাহলে”, ∧ মানে “এবং”):
নিয়ম ১ (transitivity): (A subClassOf B) ∧ (B subClassOf C) → (A subClassOf C)
নিয়ম ২ (type ওঠে): (x type A) ∧ (A subClassOf B) → (x type B)
প্রথম নিয়মের নাম transitivity: “ভেতরে থাকা” সম্পর্ক চেইন ধরে চলে, যেমন জায়গার part_of-ও। নিয়ম দুটি বারবার চালানো হয় যতক্ষণ না নতুন কিছু আসে; সেই স্থির অবস্থার নাম fixpoint. যে triple নিয়ম থেকে এলো, সেগুলো store-এ লিখে রাখার কৌশলকে বলে materialization. এটি পর্ব ৪৭-এর forward chaining-এর একটি বিশেষ রূপ।
1while True:2 new = rule1(T) | rule2(T)3 new -= T4 if not new: stop # fixpoint5 T |= new
| Charger ⊂ | |
| CH20 type | |
| এই দফায় নতুন | |
| মোট triple |
- শুরুর অবস্থা। হাতে লেখা: Charger ⊂ MobileAccessory ⊂ Electronics ⊂ Product (⊂ মানে “subClassOf”), আর CH20 type Charger. মোট ৫০টি triple; “CH20 কি Electronics?” এখনো অজানা।
- দফা ১। নিয়ম ১ জোড়া জোড়া রেখা মিলিয়ে দিল Charger ⊂ Electronics আর MobileAccessory ⊂ Product; নিয়ম ২ দিল CH20 type MobileAccessory. পুরো graph-এ এই দফায় ৭টি subClassOf আর ৬টি type, মোট ১৩টি নতুন triple: ৫০ থেকে ৬৩।
- দফা ২। দফা ১-এর ফল থেকে আরও এক ধাপ: Charger ⊂ Product, আর CH20 type Electronics ও Product. এই দফায় ৪টি subClassOf আর ১১টি type, মোট ১৫টি: ৬৩ থেকে ৭৮।
- দফা ৩, fixpoint. নিয়ম দুটি আবার চালিয়ে যা আসে, সবই আগে থেকে আছে;
newখালি, লুপ থামল। শেষে subClassOf ১৯টি আর type ২৩টি; প্রতিটি পণ্য তার সব উপরের class-এর সদস্য। - একটি ভুল রেখা। কেউ ভুল করে লিখলেন Charger ⊂ Phone (charger ফোনের অংশ বা ফোনের সঙ্গে আসে ভেবে)। একটি মাত্র triple, কিন্তু অনুমানে সেটি ছড়িয়ে CH20 আর CH33 দুটিই এখন Phone; ৫১টি থেকে ৮১টি হলো। “কয়টি ফোন বিক্রি হয়েছে?” প্রশ্নের উত্তর নীরবে ভুল হবে।
def infer(T, verbose=True):
"""RDFS-style rules until nothing new: subClassOf is transitive,
and an instance of a class is also an instance of its superclass."""
T, rnd = set(T), 0
while True:
sub = {(s, o) for s, p, o in T if p == "subClassOf"}
new = {(a, "subClassOf", c) for a, b in sub for b2, c in sub if b == b2}
new |= {(x, "type", d) for x, p, c in T if p == "type"
for c2, d in sub if c == c2}
new -= T # keep only truly new
if not new:
return T, rnd
rnd += 1
if verbose:
print(f"round {rnd}: +{len(new)}", sorted(new)[:3], "...")
T |= new
full, rounds = infer(triples)
print(len(triples), "->", len(full), "triples after", rounds, "rounds")
def instances(T, cls):
return sorted(s for s, p, o in T if p == "type" and o == cls)
print("Electronics before:", instances(triples, "Electronics"))
print("Electronics after: ", instances(full, "Electronics"))
print("MobileAccessory:", instances(full, "MobileAccessory"))
wrong = triples | {("Charger", "subClassOf", "Phone")} # one bad is-a link
bad, _ = infer(wrong, verbose=False)
print("Phones, correct:", instances(full, "Phone"))
print("Phones, wrong :", instances(bad, "Phone"))আসল আউটপুট:
round 1: +13 [('AudioDevice', 'subClassOf', 'Product'), ('BUDS', 'type', 'AudioDevice'), ('CB1', 'type', 'MobileAccessory')] ...
round 2: +15 [('BUDS', 'type', 'Electronics'), ('BUDS', 'type', 'Product'), ('CB1', 'type', 'Electronics')] ...
50 -> 78 triples after 2 rounds
Electronics before: []
Electronics after: ['BUDS', 'CB1', 'CH20', 'CH33', 'PB10', 'S10']
MobileAccessory: ['CB1', 'CH20', 'CH33', 'PB10']
Phones, correct: ['S10']
Phones, wrong : ['CH20', 'CH33', 'S10']
দুই দফায় ২৮টি নতুন triple (১৩ + ১৫), তৃতীয় দফায় কিছু আসেনি বলে rounds = 2. অনুমানের আগে “Electronics” প্রশ্নের উত্তর খালি, পরে ৬টি পণ্যই। একটি দফায় চেইন এক ধাপ বেশি লম্বা হয় না, বরং প্রায় দ্বিগুণ হয়: দফা ১-এ দুই রেখার চেইন, দফা ২-এ তিন বা চার রেখার। শেষ দুই লাইন ভুল is-a-র ফল: একটি triple-এর ভুলে দুটি charger ফোন হয়ে গেল। এই লুপ প্রতি দফায় সব জোড়া আবার মেলায়; বড় graph-এ আসল reasoner শুধু আগের দফার নতুন triple থেকে শুরু করে (semi-naive evaluation), ফল একই।
ভুল is-a-র তিনটি পরিচিত রূপ। (১) অংশ আর প্রকার গুলিয়ে ফেলা: charger ফোনের সঙ্গে আসে, কিন্তু ফোনের প্রকার নয়; “অংশ” লেখা উচিত part_of দিয়ে, যা ভিন্ন সম্পর্ক। (২) Instance আর class গুলিয়ে ফেলা: “Shimul is-a Phone” ভুল; Shimul একটি ব্র্যান্ড, S10 একটি ফোন। (৩) ব্যতিক্রমওয়ালা নিয়ম: “পাখি উড়তে পারে, পেঙ্গুইন is-a পাখি” থেকে পেঙ্গুইন উড়তে পারে। Minsky-র frame-এ ডিফল্ট মান ভাঙা যেত, কিন্তু RDFS-এর অনুমানে ব্যতিক্রম নেই, তাই একটি ভুল রেখা পুরো graph-এ ছড়ায়।
৬ · RDF, IRI, Turtle আর SPARQL — rdflib দিয়ে সত্যিকারের run
অন্য প্রতিষ্ঠানের সঙ্গে ডেটা ভাগ করতে চাইলে সবাইকে একই ভাষায় লিখতে হয়। World Wide Web Consortium (W3C)-র মানদণ্ডগুলো:
| মানদণ্ড | কাজ | সময় (আনুমানিক) |
|---|---|---|
| RDF (Resource Description Framework) | triple-এর ডেটা মডেল; subject আর predicate সবসময় একটি IRI | প্রথম সুপারিশ ১৯৯৯; RDF 1.1 ২০১৪; RDF 1.2 নিয়ে কাজ চলছে বলে জানা যায় |
| Turtle | RDF লেখার সহজে পড়া যায় এমন রূপ | W3C সুপারিশ ২০১৪ (আগে থেকেই প্রচলিত) |
| RDFS (RDF Schema) | class, subClassOf, domain, range-এর মতো schema-শব্দ | সুপারিশ ২০০৪, পরে RDF 1.1-এর সঙ্গে হালনাগাদ |
| OWL (Web Ontology Language) | আরও শক্তিশালী ontology: “এই দুই class-এর মিল নেই”, “এই property transitive”, সংখ্যার সীমা | OWL ২০০৪, OWL 2 ২০০৯ |
| SPARQL | RDF graph-এ query-র ভাষা | SPARQL ২০০৮, SPARQL 1.1 ২০১৩ |
RDF-এ প্রতিটি entity আর predicate-এর নাম একটি IRI (Internationalized Resource Identifier, ওয়েব ঠিকানার মতো একটি বিশ্বজুড়ে অনন্য নাম, যাতে বাংলার মতো Unicode অক্ষরও চলে)। “Sylhet” লিখলে কোন Sylhet, জেলা না বিভাগ না কোনো দোকানের নাম, তা পরিষ্কার নয়; https://example.org/bazarghar/Sylhet লিখলে নামটি বাজারঘরের নিজের। প্রতিবার পুরো ঠিকানা লেখা কষ্টকর, তাই একটি ছোট উপসর্গ বা prefix ঠিক করা হয়: bz: = https://example.org/bazarghar/. (example.org উদাহরণের জন্য সংরক্ষিত একটি ডোমেইন।) নিচে বাজারঘরের graph-এর একটি ছোট অংশ Turtle-এ। a হলো rdf:type-এর সংক্ষেপ, ; মানে “একই subject, আরেকটি predicate”, আর "…"@bn মানে বাংলা ভাষার একটি লেখা।
@prefix bz: <https://example.org/bazarghar/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
# ---- schema (ontology) ----
bz:Charger rdfs:subClassOf bz:MobileAccessory .
bz:Cable rdfs:subClassOf bz:MobileAccessory .
bz:MobileAccessory rdfs:subClassOf bz:Electronics .
bz:Phone rdfs:subClassOf bz:Electronics .
bz:madeBy rdfs:domain bz:Product ; rdfs:range bz:Brand .
# ---- data ----
bz:S10 a bz:Phone ; bz:madeBy bz:Shimul ;
rdfs:label "Shimul S10"@en, "শিমুল S10"@bn .
bz:CH20 a bz:Charger ; bz:madeBy bz:Shimul ; bz:compatibleWith bz:S10 .
bz:CB1 a bz:Cable ; bz:madeBy bz:Kodom ; bz:compatibleWith bz:S10 .
bz:CH33 a bz:Charger ; bz:madeBy bz:Kodom .
bz:BUDS a bz:Earbuds ; bz:madeBy bz:Bokul .
bz:Mitu bz:livesIn bz:Sylhet .
bz:Nasrin bz:livesIn bz:Sylhet .
bz:Rumana bz:livesIn bz:Sylhet .
bz:Tanvir bz:livesIn bz:Dhaka .
bz:O1 bz:placedBy bz:Rumana ; bz:contains bz:BUDS .
bz:O2 bz:placedBy bz:Mitu ; bz:contains bz:CB1 .
bz:O3 bz:placedBy bz:Tanvir ; bz:contains bz:S10 .
bz:O4 bz:placedBy bz:Nasrin ; bz:contains bz:S10 .এবার SPARQL. এর ভেতরের অংশ (WHERE { … }) ঠিক অংশ ৪-এর pattern-এর তালিকা, শুধু লেখার ধরন আলাদা: variable ? দিয়ে শুরু, প্রতিটি pattern-এর শেষে বিন্দু, আর ; দিয়ে একই subject-এর পরের predicate. rdflib (RDF-এর জন্য একটি open-source Python library) পাতার ভেতরের Python-এ আগে থেকে দেওয়া থাকে না, তাই নিচের run লেখকের ডেস্কটপে একটি আলাদা venv-এ করা।
python -m venv .venv
.venv/bin/pip install rdflib==7.6.0 # Windows: .venv\Scripts\pipfrom rdflib import Graph
g = Graph()
g.parse("bazarghar.ttl", format="turtle")
print("triples:", len(g))
Q1 = PREFIX + """
SELECT DISTINCT ?customer ?product ?brand WHERE {
?customer bz:livesIn bz:Sylhet .
?order bz:placedBy ?customer ;
bz:contains ?product .
?product bz:madeBy ?brand .
?charger a bz:Charger ;
bz:madeBy ?brand .
}
ORDER BY ?customer
"""
for row in g.query(Q1):
print(" ", row.customer.n3(g.namespace_manager),
row.product.n3(g.namespace_manager), row.brand.n3(g.namespace_manager))
Q2 = PREFIX + """
SELECT ?item ?cls WHERE {
?item a ?cls .
?cls rdfs:subClassOf* bz:Electronics .
}
ORDER BY ?item
"""
Q3 = PREFIX + """
SELECT ?brand (COUNT(?p) AS ?n) WHERE { ?p bz:madeBy ?brand . }
GROUP BY ?brand ORDER BY DESC(?n) ?brand
"""
Q4 = PREFIX + """
SELECT ?label WHERE { bz:S10 rdfs:label ?label . FILTER(lang(?label) = "bn") }
"""
Q5 = PREFIX + "ASK { bz:Rumana bz:livesIn bz:Dhaka }"সত্যিকারের run (Python 3.14.4, rdflib 7.6.0, লেখার সময়ের সর্বশেষ স্থিতিশীল সংস্করণ; path বদলে নিরপেক্ষ নাম):
~/bazarghar-kg$ python kg_sparql.py
Python 3.14.4 | rdflib 7.6.0
triples: 32
Q1: Sylhet customers, brand also makes chargers
bz:Mitu bz:CB1 bz:Kodom
bz:Nasrin bz:S10 bz:Shimul
Q2: everything that is Electronics (subClassOf* path)
bz:CB1 via bz:Cable
bz:CH20 via bz:Charger
bz:CH33 via bz:Charger
bz:S10 via bz:Phone
Q3: products per brand
bz:Kodom 2
bz:Shimul 2
bz:Bokul 1
Q4: Bangla label of S10: ['শিমুল S10']
Q5: does Rumana live in Dhaka? False
ফলগুলো থেকে শিক্ষা (Q3 দেখায় SQL-এর মতো GROUP BY আর COUNT):
- Q1 নিজের লেখা store-এর উত্তরই দিল: Mitu আর Nasrin.
- Q2-এর
rdfs:subClassOf*একটি property path:*মানে “শূন্য বা তার বেশি ধাপ”। অংশ ৫-এর মতো triple আগে থেকে লিখে না রেখে query চলার সময়েই চেইন ধরে হাঁটা হলো। লক্ষ করুন BUDS নেই: এই ছোট Turtle ফাইলে Earbuds-এর কোনো subClassOf লেখা হয়নি। Schema-তে রেখা না থাকলে উত্তরও নেই, কোনো সতর্কবার্তাও নেই। - Q4-এ একই পণ্যের দুই ভাষার নাম থেকে
lang()দিয়ে বাংলাটি বাছা। - Q5-এর
Falseমানে “graph-এ এই triple নেই”, “Rumana ঢাকায় থাকেন না” প্রমাণিত নয়। RDF-এর জগৎ সাধারণত open-world assumption মেনে চলে: যা লেখা নেই তা অজানা, মিথ্যা নয়। Database-এর অভ্যাস উল্টো (closed world: না থাকলে মিথ্যা)। ভাবনার প্রশ্ন ২-এ এর পরিণাম দেখবেন।
আর IRI-গুলো আসলে কেমন দেখায়, তা N-Triples রূপে (প্রতি লাইনে একটি পূর্ণ triple, কোনো prefix ছাড়া) একই run-এ S10-এর চারটি triple থেকে দেখুন:
<https://example.org/bazarghar/S10> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://example.org/bazarghar/Phone> .
<https://example.org/bazarghar/S10> <http://www.w3.org/2000/01/rdf-schema#label> "Shimul S10"@en .
<https://example.org/bazarghar/S10> <http://www.w3.org/2000/01/rdf-schema#label> "শিমুল S10"@bn .
<https://example.org/bazarghar/S10> <https://example.org/bazarghar/madeBy> <https://example.org/bazarghar/Shimul> .
আরও গভীরে। RDFS-এর অনুমানের নিয়মগুলো W3C-র “RDF 1.1 Semantics” নথিতে লেখা আছে; অংশ ৫-এর নিয়ম দুটি তার মধ্যে দুটি (সাধারণত rdfs11 আর rdfs9 নামে পরিচিত)। rdflib নিজে এই অনুমান চালায় না; Python-এ এর জন্য আলাদা প্যাকেজ (যেমন owlrl) আছে, আর বড় কাজে আলাদা triple store ও reasoner. OWL-এর ভিত্তি description logic, যার অনেক ভাগে অনুমান decidable (সীমিত সময়ে উত্তর নিশ্চিত), কিন্তু পুরো first-order logic-এ তা নয়; ভাষার প্রকাশক্ষমতা আর হিসাবের খরচের এই টানাপোড়েনই ontology ভাষার নকশার কেন্দ্রে।
৭ · Property graph, Cypher আর জনসাধারণের knowledge graph
Knowledge graph রাখার আরেকটি বড় পরিবার হলো property graph. এখানে node আর edge দুটিরই নিজস্ব property (চাবি-মানের জোড়া) থাকতে পারে: অর্ডারের রেখায় সরাসরি {date: "2026-10-12", qty: 2}. RDF-এ রেখার গায়ে তথ্য রাখতে সাধারণত অর্ডারের মতো একটি মধ্যবর্তী node লাগে (অংশ ২)। Property graph-এর সবচেয়ে পরিচিত query ভাষা Cypher, যার মৌলিক কাজ (pattern এঁকে প্রশ্ন, variable-length path, shortestPath) DBMS কোর্সের graph ডেটাবেস পর্বে চালিয়ে দেখানো আছে। বাজারঘরের প্রশ্নটি Cypher-এ মোটামুটি এমন দেখাবে (উদাহরণ, এখানে চালানো হয়নি; label আর relation-এর নাম আমাদের বানানো):
MATCH (c:Customer)-[:LIVES_IN]->(:District {name: 'Sylhet'}),
(o:Order)-[:PLACED_BY]->(c),
(o)-[:CONTAINS]->(p:Product)-[:MADE_BY]->(b:Brand),
(:Charger)-[:MADE_BY]->(b)
RETURN DISTINCT c.name, p.name, b.nameছবির মতো তীর আঁকা: (node)-[:REL]->(node). ভেতরের চিন্তা SPARQL-এর মতোই pattern matching, শুধু লেখার ধরন আলাদা।
| দিক | RDF graph | Property graph |
|---|---|---|
| মৌলিক একক | triple; নাম IRI, বিশ্বজুড়ে অনন্য | node আর edge, প্রত্যেকের label আর property |
| রেখার গায়ে তথ্য | মধ্যবর্তী node, reification বা নতুন RDF 1.2-র “triple term”-এর মতো ব্যবস্থা | সরাসরি edge-এর property |
| Schema আর অনুমান | RDFS/OWL মানদণ্ড, reasoner দিয়ে অনুমান | সাধারণত ঢিলেঢালা schema; অনুমান টুলভেদে আলাদা |
| Query ভাষা | SPARQL (W3C মানদণ্ড) | Cypher, openCypher, আর ২০২৪ সালে ISO-র প্রকাশিত GQL; অন্য টুলের নিজস্ব ভাষাও আছে |
| যেখানে বেশি দেখা যায় | উন্মুক্ত ডেটা প্রকাশ, বিভিন্ন প্রতিষ্ঠানের ডেটা মেলানো, ontology-নির্ভর কাজ | অ্যাপের ভেতরের সম্পর্ক-ডেটা, fraud খোঁজা, সুপারিশ, পথ খোঁজা |
তুলনাটি মোটা দাগের: কিছু ডেটাবেস দুটি মডেলই সমর্থন করে, আর কোনটি “ভালো” তা বিতর্কের বিষয়। আসল প্রশ্ন: ডেটা অন্যদের সঙ্গে ভাগ হবে আর অর্থ মানদণ্ডে বাঁধা দরকার (RDF-এর দিকে), নাকি একটি অ্যাপের ভেতরে দ্রুত পথ খোঁজাই মূল কাজ (property graph-এর দিকে)?
জনসাধারণের জন্য উন্মুক্ত knowledge graph
দুটি পরিচিত উন্মুক্ত knowledge graph হলো Wikidata আর DBpedia. Wikidata Wikimedia Foundation-এর একটি প্রকল্প, ২০১২ সালে শুরু; যে কেউ সম্পাদনা করতে পারেন, প্রতিটি বস্তুর একটি পরিচয় নম্বর থাকে (Q দিয়ে শুরু), আর প্রতিটি সম্পর্ক বা statement-এর সঙ্গে উৎস (reference) আর অতিরিক্ত শর্ত (qualifier, যেমন “কোন সময় থেকে”) রাখা যায়। নাম অনেক ভাষায় থাকে, বাংলাতেও, আর একটি উন্মুক্ত SPARQL সেবা আছে। DBpedia ২০০৭ সালের দিকে কয়েকটি বিশ্ববিদ্যালয়ের গবেষকদের হাতে শুরু হয়; এটি Wikipedia প্রবন্ধের তথ্যবাক্স (infobox) থেকে স্বয়ংক্রিয়ভাবে triple বের করে। দুটির আকার সময়ের সঙ্গে বদলায়, তাই এখানে কোনো সংখ্যা দেওয়া হলো না।
উন্মুক্ত graph মানে নির্ভুল graph নয়। স্বেচ্ছাসেবকের সম্পাদনা আর স্বয়ংক্রিয় extraction-এ ভুল, পুরোনো তথ্য আর অসম্পূর্ণতা থাকে; বাংলা নাম বা বাংলাদেশের ছোট জায়গার তথ্য অনেক সময় অসম্পূর্ণ থাকতে পারে। বড় কাজে পাবলিক query সেবার বদলে প্রকাশিত ডাম্প ফাইল ব্যবহার করা ভদ্রতা। ব্যবহারের আগে লাইসেন্স দেখুন (Wikidata-র ডেটা CC0 হিসেবে প্রকাশিত বলে উল্লেখ আছে; DBpedia-র লাইসেন্স আলাদা)।
৮ · Graph-এ হাঁটা — BFS, সবচেয়ে ছোট সম্পর্কের পথ আর networkx
কখনো প্রশ্ন pattern-এ লেখা যায় না, কারণ কত ধাপ লাগবে তা আগে জানা নেই: “Mitu আর Kodom-এর মধ্যে সম্পর্কটা কী?” এর উত্তর হলো দুই node-এর মধ্যে সবচেয়ে কম রেখার পথ। ওজনহীন graph-এ সেটি খোঁজে breadth-first search: শুরুর node থেকে এক ধাপ দূরের সব, তারপর দুই ধাপ দূরের সব, স্তরে স্তরে। পর্ব ৪০-এ BFS ঢাকার রাস্তায় চালানো হয়েছে, আর তার কোড আর জটিলতা DSA কোর্সের BFS পর্বে আছে; এখানে শুধু ব্যবহার। নতুন দুটি বিষয়: তীরের উল্টো দিকেও হাঁটতে হয় (Mitu থেকে O2-তে placed_by উল্টো), আর পথটি রেখার নাম ও দিকসহ ব্যাখ্যা করতে হয়।
নিচের কোড networkx (graph-এর জন্য বহুল ব্যবহৃত Python library, যা পাতার Python-এ আগে থেকেই দেওয়া থাকে) দিয়ে দুটি graph বানায়: দিকসহ G, যাতে রেখার নাম থাকে, আর দিকহীন U, যাতে হাঁটা হয়। ওজন ছাড়া nx.shortest_path BFS-এর একটি রূপ (দুই দিক থেকে একসঙ্গে খোঁজা bidirectional BFS) ব্যবহার করে। শেষে graph-এর একটি ছোট অংশের ছবি।
import networkx as nx
import matplotlib.pyplot as plt
facts = sorted(t for t in triples if t[1] not in ("name", "subClassOf"))
G, U = nx.DiGraph(), nx.Graph() # U: same edges, walked both ways
for s, p, o in facts:
G.add_edge(s, o, rel=p); U.add_edge(s, o)
print(G.number_of_nodes(), "nodes,", G.number_of_edges(), "edges")
def explain(path):
"""turn a node path into relation steps, showing edge direction"""
out = []
for a, b in zip(path, path[1:]):
if G.has_edge(a, b): out.append(f"{a} -{G[a][b]['rel']}-> {b}")
else: out.append(f"{a} <-{G[b][a]['rel']}- {b}")
return out
for a, b in [("Mitu", "Kodom"), ("Rumana", "Shimul")]:
path = nx.shortest_path(U, a, b) # unweighted: breadth-first search
print(f"{a} to {b}, {len(path) - 1} hops:")
for step in explain(path): print(" ", step)
print("BFS layers from Mitu:",
[sorted(layer) for layer in nx.bfs_layers(U, ["Mitu"])][:3])
pos = {"Mitu": (0, 1), "O2": (1, 1), "CB1": (2, 1), "CH33": (3, 1),
"Sylhet": (0, 0), "S10": (2, 0), "Charger": (3, 0), "Kodom": (2.5, 2)}
H = G.subgraph(pos) # a small piece, fixed positions
fig, ax = plt.subplots(figsize=(6.4, 4))
nx.draw_networkx(H, pos, ax=ax, node_color="#bfdbfe", node_size=900,
font_size=8, arrowsize=14)
nx.draw_networkx_edge_labels(H, pos, nx.get_edge_attributes(H, "rel"),
font_size=7, ax=ax)
ax.set_title("Part of the Bazarghar knowledge graph"); ax.axis("off")
plt.show()আসল আউটপুট (ছবিটি আউটপুটের নিচে আসে):
33 nodes, 40 edges
Mitu to Kodom, 3 hops:
Mitu <-placed_by- O2
O2 -contains-> CB1
CB1 -made_by-> Kodom
Rumana to Shimul, 5 hops:
Rumana -lives_in-> Sylhet
Sylhet <-lives_in- Nasrin
Nasrin <-placed_by- O4
O4 -contains-> S10
S10 -made_by-> Shimul
BFS layers from Mitu: [['Mitu'], ['O2', 'Sylhet'], ['CB1', 'Nasrin', 'Rumana', 'SabujStore', 'SylhetDivision']]
৫০টি triple থেকে ২টি নাম আর ৮টি subClassOf বাদ দিয়ে ৪০টি রেখা, ৩৩টি node. Mitu থেকে Kodom তিন ধাপ: Mitu-র অর্ডার O2-তে CB1, যা Kodom-এর বানানো, একটি অর্থবহ ব্যাখ্যা। কিন্তু Rumana থেকে Shimul-এর পথটি দেখুন: “Rumana সিলেটে থাকেন, Nasrin-ও সিলেটে থাকেন, Nasrin S10 কিনেছেন”। পথটি সত্যি, কিন্তু Rumana-র সঙ্গে Shimul-এর কোনো সত্যিকারের সম্পর্ক বোঝায় না; একটি জনবহুল node (সিলেট) অনেক অপ্রাসঙ্গিক জিনিসকে কাছাকাছি নিয়ে আসে। একই দৈর্ঘ্যের পথ একাধিক থাকলে কোনটি আসবে, তা node যোগ করার ক্রমের ওপর নির্ভর করে; তাই কোড triple-গুলো sorted করে যোগ করে, নইলে প্রতিবার ভিন্ন পথ আসতে পারত (set-এর এলোমেলো ক্রম)। শেষ লাইনে BFS-এর প্রথম তিনটি স্তর।
সবচেয়ে ছোট পথ সবসময় সেরা ব্যাখ্যা নয়। Hub node “কয় ধাপ দূরে” মাপকে বিকৃত করে, তাই বাস্তবে কিছু সম্পর্ক বাদ দেওয়া, রেখায় ওজন দেওয়া বা pattern দিয়ে পথের আকার বাঁধা হয়। SQL-এ একই পথ খোঁজা recursive CTE দিয়ে কীভাবে হয়, তা DBMS কোর্সের CTE পর্বে আছে।
৯ · Entity resolution, ডেটার মান, provenance আর গোপনীয়তা
বাস্তবের knowledge graph তৈরি হয় অনেক উৎস জুড়ে, আর প্রতিটি উৎস একই জিনিস নিজের মতো করে লেখে। ধরুন একটি অংশীদার বিক্রেতার পাঠানো অর্ডারে S10 লেখা “shimul s-10” আর “SHIMUL S10 (Black)”, জেলা “sylhet” আর বানান ভুল “Sylet”. এগুলো নতুন node হয়ে বসলে S10-এর বিক্রি কম দেখাবে। একই বাস্তব বস্তুর বিভিন্ন লেখাকে খুঁজে এক node-এ মেলানোর কাজের নাম entity resolution (record linkage বা deduplication নামেও পরিচিত)। পর্ব ২৩-এ duplicate সারি আর বাংলা লেখার পরিষ্কার দেখা হয়েছে; এখানে একই সমস্যা graph-এর node-এ।
স্কুলের রেজিস্টারে একই ছাত্রের নাম কোথাও “মো. রাকিব হাসান”, কোথাও “Rakib Hasan”, কোথাও “রাকিব”। শিক্ষক রোল নম্বর, ক্লাস আর বাবার নাম মিলিয়ে বোঝেন এরা একজনই। কিন্তু একই ক্লাসে দুজন রাকিব থাকলে ভুল করে দুজনকে এক করে ফেলাও সম্ভব। Entity resolution-এর দুই রকম ভুল ঠিক এটাই: আলাদা জিনিসকে এক করা (false merge) আর একই জিনিসকে আলাদা রাখা (false split).
নিচের কোড একটি সাবধানী পদ্ধতি নেয়। প্রথমে একটি normalisation key: ছোট হাতের অক্ষর, বন্ধনীর ভেতরের রং ইত্যাদি বাদ, শুধু অক্ষর আর অঙ্ক রাখা; তাতে “SHIMUL S10 (Black)” আর “shimul s-10” দুটিই হয় shimuls10, যা ক্যাটালগের “Shimul S10”-এর key. Key হুবহু মিললে মেলানো হয়। আর হুবহু না মিললেও খুব কাছাকাছি হলে (Python-এর difflib দিয়ে মিলের অনুপাত দেখে) মেলানো হয় না, শুধু মানুষের যাচাইয়ের জন্য তালিকায় তোলা হয়। তারপর একই দুটি প্রশ্ন মেলানোর আগে আর পরে।
import re, difflib
feed = """O7 placed_by Farhana
Farhana lives_in sylhet
O7 contains shimul s-10
O8 placed_by Tanvir
O8 contains SHIMUL S10 (Black)
O9 placed_by Joynal
Joynal lives_in Sylet
O9 contains Kodom usb-c cable""" # a partner seller's messy feed
raw = {tuple(l.split(None, 2)) for l in feed.splitlines()}
base = set(triples)
def key(text): # lower case, drop "(...)", keep only letters/digits
return re.sub(r"[^a-z0-9]", "", re.sub(r"\(.*?\)", "", text.lower()))
canon = {key(o): s for s, p, o in base if p == "name"} # name -> product id
canon |= {key(o): o for s, p, o in base if p == "lives_in"} # district names
def resolve(t):
s, p, o = t
if p in ("contains", "lives_in"):
if key(o) in canon:
return (s, p, canon[key(o)])
near = difflib.get_close_matches(key(o), list(canon), n=1, cutoff=0.8)
if near:
print(f" review: {o!r} looks like {canon[near[0]]!r}, not merged")
return t
fixed = {resolve(t) for t in sorted(raw)}
for label, T in [("raw feed", base | raw), ("resolved", base | fixed)]:
build(T)
s10 = query([("?o", "contains", "S10"), ("?o", "placed_by", "?c")])
print(f"{label}: {len(triples)} triples | S10 orders:", len(s10),
"| Sylhet + charger brand:", sorted({r["?c"] for r in query(q)}))
build(base) # restore the clean graphআসল আউটপুট:
review: 'Sylet' looks like 'Sylhet', not merged
raw feed: 58 triples | S10 orders: 2 | Sylhet + charger brand: ['Mitu', 'Nasrin']
resolved: 58 triples | S10 orders: 4 | Sylhet + charger brand: ['Farhana', 'Mitu', 'Nasrin']
দুই ক্ষেত্রেই ৫৮টি triple (৫০ + নতুন ৮), কিন্তু উত্তর আলাদা। কাঁচা ডেটায় S10-এর অর্ডার ২টি, কারণ “shimul s-10” আর “SHIMUL S10 (Black)” graph-এর চোখে দুটি অচেনা পণ্য; মেলানোর পর ৪টি। Multi-hop প্রশ্নে Farhana এলেন, কারণ তাঁর “sylhet” এখন আসল Sylhet node, আর তাঁর কেনা ফোনটি এখন Shimul-এর S10. Joynal আসেননি: “Sylet” মেলানো হয়নি, শুধু যাচাইয়ের তালিকায় উঠেছে। তাঁর cable অবশ্য “Kodom usb-c cable” থেকে CB1-এ মিলেছে। একটি মানুষ দেখে নিশ্চিত করলে তিনিও আসবেন। শেষ লাইন আসল graph ফিরিয়ে দেয়, যাতে আবার চালালে একই ফল আসে।
সহজ key-র নিজস্ব বিপদ। বন্ধনী বাদ দেওয়ায় “Shimul S10 (refurbished)” আর নতুন S10 এক হয়ে যাবে, যা দামের হিসাবে ভুল। আর “শিমুল” বনাম “Shimul” এই key-তে মেলে না; সেখানে প্রতিবর্ণীকরণ বা সমার্থক-তালিকা লাগে। বড় কাজে কয়েকটি ক্ষেত্রের মিল (নাম, ফোন, ঠিকানা) মিলিয়ে প্রতিটি জোড়ার একটি স্কোর হিসাব হয় (ভাবনার প্রশ্ন ৫)।
প্রতিটি তথ্যের উৎস — provenance
অংশীদারের ডেটা মেলানোর পর প্রশ্ন ওঠে: “Farhana সিলেটে থাকেন” তথ্যটি কে দিল, কবে, আর কতটা বিশ্বাসযোগ্য? Provenance মানে প্রতিটি তথ্যের উৎস আর ইতিহাসের হিসাব। Knowledge graph-এ এটি রাখার কয়েকটি উপায়: প্রতিটি উৎসের triple আলাদা একটি নাম-দেওয়া graph-এ রাখা (RDF-এ named graph; তখন triple-এর সঙ্গে চতুর্থ একটি ঘর থাকে, তাই একে quad বলা হয়), Wikidata-র মতো প্রতিটি statement-এর সঙ্গে reference রাখা, অথবা property graph-এ edge-এর property হিসেবে source আর retrieved_at. উৎস জানা থাকলে একটি ভুল উৎসের সব তথ্য একসঙ্গে সরানো যায়।
ব্যক্তিগত তথ্য আর graph-এর বাড়তি ঝুঁকি
বাজারঘরের graph-এ গ্রাহকের নাম, জেলা আর কেনাকাটা আছে। আলাদা আলাদা টেবিলে এগুলো কম ক্ষতিকর মনে হয়, কিন্তু graph-এর আসল শক্তি জোড়া লাগানো, আর সেটিই গোপনীয়তার ঝুঁকি: কয়েকটি নিরীহ তথ্য (জেলা, কেনার সময়, পণ্যের ধরন) জুড়ে একজন মানুষকে চেনা বা কেনাকাটা থেকে স্বাস্থ্যের মতো সংবেদনশীল কিছু আন্দাজ করা যেতে পারে। তাই প্রয়োজনের বাইরে ব্যক্তিগত তথ্য না তোলা, আসল নামের বদলে ছদ্ম-পরিচয় (pseudonymous id), query-র অনুমতি বেঁধে দেওয়া, আর মোছার অনুরোধে অনুমিত triple থেকেও মোছা দরকার। ডেটা কমানো আর আইনি দিক নিয়ে বিস্তারিত আছে Cybersecurity কোর্সের privacy engineering পর্বে।
১০ · Knowledge graph, machine learning আর LLM — আর AI-এর যুগে যাচাই
Symbolic AI আর machine learning এখানে প্রতিদ্বন্দ্বী নয়, প্রায়ই একসঙ্গে কাজ করে। তিনটি দিক সংক্ষেপে:
- Link prediction: graph সবসময় অসম্পূর্ণ। “CB1 কি S10-এর পাশাপাশি অন্য কোনো ফোনেও চলে?” এমন অনুপস্থিত রেখা আন্দাজ করার কাজ। এর একটি জনপ্রিয় পথ knowledge graph embedding: প্রতিটি entity আর relation-কে একটি সংখ্যার vector দেওয়া, যাতে সত্যি triple-গুলো vector-এর হিসাবে “মেলে”। যেমন ২০১৩ সালে Bordes ও সহকর্মীদের TransE মডেলে ধারণাটি হলো subject-এর vector + relation-এর vector ≈ object-এর vector. Embedding আর vector search পর্ব ৬৯-এ (গণিতের শুরু পর্ব ৩৩-এ)। এগুলো আন্দাজ, প্রমাণ নয়।
- LLM দিয়ে graph বানানো: পণ্য-বিবরণ বা রিভিউ থেকে triple বের করা দ্রুত, কিন্তু মডেল সম্পর্ক বানিয়েও ফেলতে পারে; উৎসের বাক্যটি provenance হিসেবে রাখা আর নমুনা যাচাই জরুরি।
- Graph দিয়ে LLM-এর উত্তর বাঁধা: RAG পদ্ধতিতে উত্তর লেখার আগে প্রাসঙ্গিক তথ্য খুঁজে মডেলকে দেওয়া হয়; সেই তথ্য knowledge graph থেকে এলে (একটি SPARQL বা pattern query-র ফল) উত্তরের পেছনের triple আর তাদের উৎস দেখানো যায়। কিন্তু এটি hallucination পুরো দূর করে না: graph ভুল বা পুরোনো হতে পারে, query ভুল তথ্য আনতে পারে, আর মডেল সঠিক তথ্য পেয়েও ভুল যোগ করতে পারে। LLM কীভাবে কাজ করে আর তার সীমা পর্ব ৭০-এ।
AI সহকারী Turtle, SPARQL বা rdflib-এর কোড দ্রুত লিখে দেয়, কিন্তু প্রায়ই এই ভুলগুলো করে:
- বানানো prefix বা property:
rdfs:subclassOf(ছোট c),schema:madeBy-এর মতো অস্তিত্বহীন নাম; SPARQL তখন error দেয় না, শুধু খালি ফল দেয়, কারণ ভুল নামের triple কোথাও নেই। - পুরোনো API: rdflib-এর পুরোনো সংস্করণের অভ্যাস, যেমন
serialize()-এর ফলকে bytes ধরে.decode()করা (rdflib 6 থেকে এটি string ফেরত দেয়), বা উঠে যাওয়া plugin-এর নাম। - Closed-world ধরে নেওয়া: “ASK false মানে Rumana ঢাকায় থাকেন না” লিখে দেওয়া, বা
FILTER NOT EXISTS-এর ফলকে প্রমাণ বলা। - আন্দাজকে প্রমাণ বলা: embedding বা LLM-এর আন্দাজ করা রেখা উৎস ছাড়া graph-এ মিশিয়ে ফেলা।
প্রম্পটে সংস্করণ, ছোট নমুনা ডেটা আর প্রত্যাশিত উত্তর নিজেই দিয়ে দিন, যাতে ফল হাতে মেলানো যায়। নিচের প্রম্পটে কোনো আসল গ্রাহকের তথ্য নেই।
AI-এর লেখা knowledge graph-এর কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে: বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এ ধরনের প্রশ্ন প্রায়ই আসে।
- Triple কী? (subject, predicate, object), একটি তথ্যের ক্ষুদ্রতম একক; graph-এ subject থেকে object-এ predicate-এর নামের একটি রেখা।
- Ontology আর knowledge graph-এর পার্থক্য? Ontology হলো schema: class, subClassOf, property, domain, range; knowledge graph-এ এর সঙ্গে নির্দিষ্ট instance-এর তথ্যও থাকে।
- subClassOf transitive হওয়ার অর্থ? A ⊂ B আর B ⊂ C হলে A ⊂ C; n-টি class-এর একটি সরল চেইনে মোট n(n−1)/2টি subClassOf জোড়া হয়।
- RDF বনাম property graph? RDF-এ IRI-নামের triple আর W3C মানদণ্ড (SPARQL, RDFS, OWL); property graph-এ node আর edge-এর গায়ে property, query-তে সাধারণত Cypher বা GQL.
- Open-world assumption কী? Graph-এ না থাকা তথ্য অজানা ধরা হয়, মিথ্যা নয়; database-এর closed-world-এর উল্টো।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১২০০১ সালের দিকে প্রস্তাবিত “Semantic Web” পুরো ওয়েবকে যন্ত্রে-পড়া-যায় এমন জ্ঞানে বদলে দেওয়ার কথা বলেছিল। সেই স্বপ্ন পুরো সফল হয়নি, অথচ knowledge graph শিল্পে টিকে গেছে। কেন?
Tim Berners-Lee, James Hendler আর Ora Lassila-র ২০০১ সালের একটি জনপ্রিয় প্রবন্ধে কল্পনা ছিল এমন: প্রতিটি ওয়েবসাইট নিজের তথ্য RDF-এ প্রকাশ করবে, ontology দিয়ে অর্থ বাঁধা থাকবে, আর software agent সেগুলো জুড়ে জুড়ে ডাক্তারের অ্যাপয়েন্টমেন্ট থেকে কেনাকাটা পর্যন্ত সব কাজ নিজে করে দেবে। W3C মানদণ্ড এসেছে (RDF, OWL, SPARQL), কিন্তু সাধারণ ওয়েব সেভাবে বদলায়নি।
কারণ হিসেবে সাধারণত কয়েকটি বিষয় বলা হয়। প্রথমত, খরচ আর লাভের অসমতা: একটি দোকানকে নিজের ডেটা ontology মেনে প্রকাশ করতে খাটতে হয়, কিন্তু লাভটি পায় অন্য কেউ। দ্বিতীয়ত, একমত হওয়ার সমস্যা: সারা বিশ্ব একটি “পণ্য” বা “ঠিকানা”-র সংজ্ঞায় একমত হয় না, আর প্রত্যেকে নিজের ontology বানালে মেলানোর কাজটা আবার মানুষের ঘাড়ে আসে। তৃতীয়ত, বিশ্বাস: যে কেউ যেকোনো triple প্রকাশ করতে পারে, তাই উৎস যাচাই ছাড়া জুড়ে নেওয়া বিপজ্জনক।
তবু ধারণাটি অন্যভাবে বেঁচে গেছে। ২০১১ সালের দিকে কয়েকটি বড় সার্চ ইঞ্জিন মিলে schema.org নামে একটি সরল vocabulary শুরু করে, যা ওয়েবপাতায় পণ্য, রেসিপি বা ইভেন্টের তথ্য চিহ্নিত করতে বহুল ব্যবহৃত; এই কোর্সের পাতাগুলোর head-এ থাকা JSON-LD-ও সেই vocabulary-র। Wikidata আর DBpedia উন্মুক্ত graph হিসেবে টিকে আছে। আর বড় প্রতিষ্ঠানগুলো নিজেদের ভেতরে knowledge graph বানায়: সেখানে একটি প্রতিষ্ঠানই ontology ঠিক করে, ডেটার মান নিয়ন্ত্রণ করে, আর লাভও নিজে পায়। মানে কেন্দ্রহীন, সবার জন্য খোলা স্বপ্নের জায়গায় এসেছে নিয়ন্ত্রিত, উদ্দেশ্য-নির্দিষ্ট graph.
বাজারঘরের মতো একটি দোকান নিজের ক্যাটালগের graph বানালে লাভ সরাসরি নিজের: ভালো খোঁজ, “এই ফোনে কোন charger চলে” ধরনের উত্তর, ডুপ্লিকেট পণ্য ধরা।
মূল উপলব্ধি: Knowledge graph টিকে গেছে সেখানে, যেখানে একটি দল স্পষ্ট প্রশ্নের জন্য নিজের ডেটার মান নিয়ন্ত্রণ করতে পারে; বিশ্বজোড়া একক ontology-র স্বপ্ন সামাজিক আর অর্থনৈতিক কারণে আটকেছে, প্রযুক্তির কারণে কম।
প্র ০২SPARQL-এর ASK { bz:Rumana bz:livesIn bz:Dhaka } উত্তর দিল False. এর মানে কি “Rumana ঢাকায় থাকেন না”? Open-world আর closed-world assumption-এর পার্থক্য বাস্তবে কোথায় কামড় দেয়?
না। উত্তরটির সঠিক অর্থ হলো “এই graph-এ এমন কোনো triple নেই”। RDF আর OWL-এর অর্থের নিয়ম সাধারণত open-world assumption মেনে চলে: যা লেখা নেই তা অজানা। কারণটি নকশার: ওয়েবে কোনো একটি উৎস সব তথ্য জানে না, অন্য কোনো উৎসে হয়তো আছে “Rumana-র একটি ঢাকার ঠিকানাও আছে”।
বিপরীতে relational database আর বেশিরভাগ ব্যবসার সফটওয়্যার closed-world assumption মানে: টেবিলে নেই মানে মিথ্যা। ট্রেনের টিকিটের তালিকায় আপনার নাম না থাকলে ধরা হয় আপনার টিকিট নেই। এই ধারণার যুক্তিগত রূপের নাম “negation as failure”: প্রমাণ করতে না পারলে মিথ্যা ধরা, যা Prolog-এর মতো logic programming ভাষায়ও দেখা যায়।
বাস্তবে কামড়টা কোথায়? ধরুন বাজারঘর জানতে চায় “কোন গ্রাহকের জেলা জানা নেই?” SPARQL-এ এটি লেখা যায় FILTER NOT EXISTS { ?c bz:livesIn ?d } দিয়ে, যা আসলে closed-world ধাঁচের প্রশ্ন: “এই graph-এ নেই”। এটি কাজের, কিন্তু উত্তরকে “এঁদের কোনো ঠিকানা নেই” হিসেবে পড়লে ভুল হবে। আরেকটি উদাহরণ OWL-এ: যদি লেখা থাকে “প্রতিটি অর্ডারের অন্তত একটি পণ্য আছে”, আর কোনো অর্ডারে পণ্যের triple না থাকে, closed-world-এ সেটি একটি নিয়মভঙ্গ, কিন্তু open-world-এর OWL reasoner বলবে “পণ্যটি আছে, শুধু আমরা জানি না কোনটি”। তাই ডেটার মান যাচাইয়ের জন্য W3C আলাদা মানদণ্ড দিয়েছে, SHACL (২০১৭ সালে সুপারিশ), যা closed-world ধাঁচে “এই আকৃতি মানতেই হবে” পরীক্ষা করে।
Link prediction-এও এটি কামড় দেয়: graph-এ না থাকা রেখাকে প্রশিক্ষণে “মিথ্যা” ধরা হয়, অথচ অনেকগুলো আসলে সত্যি কিন্তু অনুপস্থিত।
মূল উপলব্ধি: “নেই” আর “মিথ্যা” এক নয়। কোন সিস্টেম কোন অনুমান মানে তা জেনে query-র উত্তর পড়ুন, আর সম্পূর্ণতার দাবি করতে চাইলে তা আলাদা নিয়ম (যেমন SHACL) দিয়ে স্পষ্টভাবে যাচাই করুন।
প্র ০৩১৯৮০-র দশকে শুরু হওয়া Cyc প্রকল্প বহু বছর ধরে মানুষের “সাধারণ বুদ্ধি” হাতে লিখে রাখার চেষ্টা করেছে। Wikidata সেই কাজ জনগণের হাতে দিয়েছে। হাতে লেখা জ্ঞানভান্ডারের মূল সমস্যা কী, আর ভিড়-নির্ভর পদ্ধতি তার কতটা সমাধান?
Douglas Lenat ১৯৮৪ সালে Cyc শুরু করেন এই বিশ্বাস নিয়ে যে বুদ্ধিমান আচরণের জন্য লাখো সাধারণ তথ্য লাগে, যা মানুষ এত স্বাভাবিকভাবে জানে যে কোথাও লেখে না: “পানি ঢাললে নিচে পড়ে”, “একজন মানুষ একসঙ্গে দুই জায়গায় থাকতে পারেন না”। দক্ষ লোকেরা দশকের পর দশক ধরে এগুলো একটি যুক্তিভাষায় লিখেছেন। প্রকল্পটির মূল্যায়ন নিয়ে মতভেদ আছে: সমর্থকেরা এর গভীরতার কথা বলেন, সমালোচকেরা বলেন এত খরচের তুলনায় বাইরের প্রয়োগ কম দেখা গেছে।
মূল সমস্যাটির একটি নাম আছে, knowledge acquisition bottleneck (পর্ব ৪৭-এর expert system-এর আলোচনায় এটি আসে): বিশেষজ্ঞের মাথা থেকে জ্ঞান বের করে নিখুঁত নিয়মে লেখা ধীর, ব্যয়বহুল আর অসম্পূর্ণ। দুনিয়া বদলায় (নতুন পণ্য, নতুন জেলা-বিন্যাস), তাই হাতে লেখা ভান্ডার পুরোনো হতে থাকে। আর নতুন নিয়ম পুরোনোগুলোর সঙ্গে বিরোধ করছে কি না, তা যাচাই করাও কঠিন।
Wikidata এই চাপ ছড়িয়ে দেয় হাজারো স্বেচ্ছাসেবক আর স্বয়ংক্রিয় bot-এর মধ্যে। ফলে আকার আর বিস্তার বিশাল, অনেক ভাষায় নাম, আর দ্রুত হালনাগাদ। কিন্তু নতুন সমস্যা আসে: মান অসমান, বিষয়ভেদে কভারেজ অসমান (জনপ্রিয় বিষয় আর বড় ভাষার তথ্য বেশি), সম্পাদনা-যুদ্ধ আর ইচ্ছাকৃত ভুল (vandalism), আর schema-র নিয়ম আলোচনা করে ঠিক হয় বলে একই ধরনের তথ্য বিভিন্নভাবে লেখা থাকতে পারে। প্রতিটি statement-এর সঙ্গে reference রাখার ব্যবস্থা provenance-এর দিক থেকে খুব মূল্যবান, যদিও সব statement-এ reference থাকে না।
তৃতীয় পথ স্বয়ংক্রিয় extraction (DBpedia-র তথ্যবাক্স, আজকের ভাষার মডেল): দ্রুত, কিন্তু extraction-এর ভুল ঢোকে। ভালো graph সাধারণত তিনটি মেশায়: schema হাতে, বিপুল তথ্য স্বয়ংক্রিয়ভাবে, ঝুঁকির জায়গায় মানুষের যাচাই।
মূল উপলব্ধি: জ্ঞান লেখার খরচ কোনো পদ্ধতিতে শূন্য হয় না, শুধু জায়গা বদলায়: বিশেষজ্ঞের সময় থেকে স্বেচ্ছাসেবকের পরিশ্রমে, তারপর যাচাই আর মান নিয়ন্ত্রণে। কোথায় খরচ করছেন আর কোথায় ভুলের দাম বেশি, সেটিই নকশার প্রশ্ন।
প্র ০৪বড় ভাষার মডেল যখন প্রায় যেকোনো প্রশ্নের উত্তর লিখে দেয়, তখন knowledge graph-এর দরকার কী? আবার “knowledge graph দিলে LLM আর ভুল করবে না” দাবিটি কেন ঠিক নয়?
একটি ভাষার মডেল প্রশিক্ষণের লেখা থেকে শব্দের ধারা শেখে; তার ভেতরে “তথ্য” কোথাও আলাদা করে লেখা থাকে না, প্যারামিটারের মধ্যে ছড়িয়ে থাকে। তাই কয়েকটি জিনিস তার পক্ষে স্বাভাবিকভাবে কঠিন: আজকের দাম বা স্টকের মতো দ্রুত বদলানো তথ্য, একটি দোকানের নিজস্ব ক্যাটালগের মতো ব্যক্তিগত তথ্য, আর “এই উত্তর কোন উৎস থেকে” তার নিখুঁত হিসাব। আত্মবিশ্বাসী কিন্তু বানানো উত্তর, মানে hallucination, এই কাঠামোরই একটি পরিচিত ফল।
Knowledge graph ঠিক এই জায়গাগুলোতে শক্তিশালী: প্রতিটি তথ্য আলাদা, হালনাগাদ করা যায়, উৎস রাখা যায়, আর multi-hop উত্তরের প্রতিটি ধাপ দেখানো যায় (Mitu → O2 → CB1 → Kodom). গণনায়ও নিখুঁত: “Kodom কয়টি পণ্য বানায়” প্রশ্নের উত্তর গুনে আসে, আন্দাজে নয়। আবার ভাষার মডেল সেখানে শক্তিশালী যেখানে graph দুর্বল: অস্পষ্ট বাংলা প্রশ্ন বোঝা (“সিলেটের যারা Kodom-এর জিনিস নিসে”), প্রশ্নকে query-তে রূপ দেওয়া, আর ফলকে পড়ার মতো বাক্যে লেখা। তাই সাধারণ নকশা হলো দুটির ভাগাভাগি: মডেল প্রশ্ন থেকে SPARQL বা pattern বানায়, graph উত্তর দেয়, মডেল সেই উত্তর উৎসসহ লেখে।
কিন্তু এতে ভুল শূন্য হয় না। প্রথমত, graph নিজেই ভুল, পুরোনো বা অসম্পূর্ণ হতে পারে, যেমন অংশ ৯-এর না-মেলানো “Sylet”. দ্বিতীয়ত, মডেলের বানানো query ভুল হতে পারে: ভুল predicate, ভুল দিক, অথবা খালি ফল যাকে মডেল “কেউ নেই” বলে পড়ে, যা open-world-এর ফাঁদ। তৃতীয়ত, সঠিক তথ্য হাতে পেয়েও মডেল উত্তর লিখতে গিয়ে কিছু যোগ বা বিকৃত করতে পারে। গবেষণায় retrieval-নির্ভর পদ্ধতিতে কিছু ধরনের ভুল কমার কথা বলা হয়, কিন্তু কতটা কমে তা কাজ, ডেটা আর মূল্যায়নের ধরনের ওপর নির্ভর করে; এখানে কোনো সাধারণ সংখ্যা দেওয়া ঠিক হবে না।
মূল উপলব্ধি: Knowledge graph LLM-এর উত্তরকে যাচাইযোগ্য উৎসে বাঁধতে সাহায্য করে, আর LLM graph-কে সাধারণ ভাষায় ব্যবহারযোগ্য করে; কিন্তু দুটি জোড়া দিলে ভুলের উৎস কমে, শূন্য হয় না। পর্ব ৭০-এ LLM-এর সীমা আরও বিস্তারিত।
প্র ০৫Entity resolution-এ দুই রকম ভুল হয়: আলাদা দুজনকে এক করা আর একই জনকে আলাদা রাখা। কোনটি বেশি ক্ষতিকর, আর ব্যক্তিগত তথ্যের ক্ষেত্রে মেলানোর কাজটি নিজেই কেন ঝুঁকি?
Record linkage-এর ধারণা পুরোনো: ১৯৫০-এর দশকের শেষে জনস্বাস্থ্যের নথি মেলানোর কাজে Howard Newcombe ও সহকর্মীদের গবেষণা, আর ১৯৬৯ সালে Ivan Fellegi ও Alan Sunter-এর গাণিতিক মডেল, যেখানে প্রতিটি জোড়ার জন্য কয়েকটি ক্ষেত্রের মিল-অমিল থেকে একটি স্কোর হিসাব করে তিনটি সিদ্ধান্তের একটি নেওয়া হয়: মেলানো, না মেলানো, অথবা মানুষের যাচাই। অংশ ৯-এর কোডের “review” তালিকা এই তৃতীয় পথেরই সরল রূপ।
কোন ভুল বেশি ক্ষতিকর, তা প্রসঙ্গের ওপর নির্ভর করে। পণ্যের ক্ষেত্রে false split (একই S10 তিন নামে) বিক্রির হিসাব কমায় আর স্টকের সিদ্ধান্ত ভুল করে, কিন্তু সাধারণত ঠিক করা যায়। False merge (নতুন আর refurbished S10 এক) দাম আর ওয়ারেন্টির তথ্য গুলিয়ে ফেলে। মানুষের ক্ষেত্রে false merge প্রায়ই বেশি বিপজ্জনক: দুজন “মো. রাকিব হাসান”-কে এক করলে একজনের কেনাকাটা, ঋণ বা অভিযোগ আরেকজনের নামে চলে যায়, আর সেই ভুল অনুমানের মাধ্যমে পুরো graph-এ ছড়ায়। বাংলাদেশের প্রেক্ষাপটে নামের পুনরাবৃত্তি, বাংলা-ইংরেজি বানানের ভিন্নতা আর একই পরিবারের একটি ফোন নম্বর ভাগ করে ব্যবহারের অভ্যাস এই কাজকে আরও কঠিন করতে পারে; এগুলো সাধারণ পর্যবেক্ষণ, কোনো পরিসংখ্যান নয়।
মেলানো নিজেই কেন ঝুঁকি? আলাদা উৎসে আলাদা থাকা তথ্য মেলালে নতুন জ্ঞান তৈরি হয়, যা কোনো একক উৎস দেয়নি। একটি ক্যাটালগের অর্ডার, একটি ডেলিভারি কোম্পানির ঠিকানা আর একটি রিভিউ সাইটের নাম জুড়ে একজন মানুষের অভ্যাস, ঠিকানা আর সময়সূচি পর্যন্ত বের করা যায়। “নাম মুছে দিলেই নিরাপদ” ধারণাও দুর্বল: কয়েকটি গুণ (জেলা, বয়সের সীমা, কেনার সময়) মিলিয়েই অনেক সময় একজনকে আবার চেনা যায় বলে গবেষণায় বারবার দেখানো হয়েছে।
মূল উপলব্ধি: Entity resolution একটি সম্ভাবনার সিদ্ধান্ত, নিশ্চিত সত্য নয়; কোন ভুলের দাম বেশি তা ঠিক করে threshold বাছুন, merge উল্টানো যায় এমন রাখুন, আর মনে রাখুন যে মানুষের তথ্য মেলানো নিজেই গোপনীয়তার প্রশ্ন।
অনুশীলন
প্রথমটি হাতে হিসাব, দ্বিতীয়টি কোড, তৃতীয়টি নকশা ও সমালোচনা, চতুর্থটি আউটপুট আন্দাজ।
-
Transitive closure হাতে গুনুন: একটি graph-এ শুধু এই ৭টি triple: A subClassOf B, B subClassOf C, C subClassOf D, D subClassOf E, আর x0, x1, x2 তিনটিই type A. অংশ ৫-এর দুটি নিয়ম fixpoint পর্যন্ত চালালে (ক) মোট কয়টি subClassOf triple হবে? (খ) কয়টি type triple? (গ) অংশ ৫-এর কোডের মতো প্রতি দফায় সব নিয়ম একসঙ্গে চালালে কয় দফায় নতুন triple আসবে?
(ক) ১০টি। ৫টি class-এর চেইনে প্রতিটি “নিচের” class তার উপরের সবার subClassOf: A-র ৪টি, B-র ৩টি, C-র ২টি, D-র ১টি, মোট ৪ + ৩ + ২ + ১ = ১০ = n(n−1)/2, n = ৫।
(খ) ১৫টি। প্রতিটি x-এর type হবে A, B, C, D, E, মানে ৫টি করে; ৩ × ৫ = ১৫। মোট ১০ + ১৫ = ২৫টি triple, শুরুর ৭টি থেকে।
(গ) ৩ দফা। দফা ১: A⊂C, B⊂D, C⊂E আর তিনটি x type B, মোট +৬। দফা ২: A⊂D, A⊂E, B⊂E, তিনটি x type C আর তিনটি x type D, মোট +৯ (A⊂E আসে A⊂C আর C⊂E জুড়ে, তাই চেইন দ্রুত লম্বা হয়)। দফা ৩: তিনটি x type E, +৩। চতুর্থ দফায় কিছু নেই। লেখকের যন্ত্রে অংশ ৫-এর
inferদিয়ে চালানো সত্যিকারের run-এও ঠিক এই সংখ্যা এসেছে:7 → 25, ৩ দফা, +৬, +৯, +৩। -
ছোট index বাছাই (কোড): অংশ ৪-এর
matchসবসময় আগে subject-এর, তারপর object-এর, তারপর predicate-এর index নেয়। একে বদলে এমন করুন যাতে জানা ঘরগুলোর মধ্যে যে index-এ সবচেয়ে কম triple আছে, সেটিই নেওয়া হয়। তারপর এই তিনটি pattern-এ পুরোনো আর নতুন নিয়মে কয়টি প্রার্থী দেখতে হয় তা গুনুন:(?p, compatible_with, S10),(?c, lives_in, Sylhet),(?o, placed_by, Rumana).প্রার্থী বাছাইয়ের লাইনটি এভাবে লেখা যায়:
opts = [idx[x] for idx, x in ((by_s, s), (by_p, p), (by_o, o)) if not is_var(x)] cands = min(opts, key=len) if opts else triplesলেখকের যন্ত্রে গোনা সংখ্যা (object-এর index বনাম predicate-এর index):
compatible_with S10-এ by_o-তে ৪টি, by_p-তে ২টি, তাই নতুন নিয়মে ২টি।lives_in Sylhet-এ by_o-তে ৪টি, by_p-তে ৫টি, তাই object-ই ভালো, ৪টি।placed_by Rumana-এ by_o-তে ২টি, by_p-তে ৬টি, তাই ২টি। ফল একই থাকে, শুধু কাজ কমে। আসল triple store দুই ঘর একসঙ্গে জানা থাকলে তার জন্যও index রাখে (যেমন predicate-object ক্রমের index). -
Schema-র সমালোচনা (নকশা): একজন সহকর্মী বাজারঘরের ডেলিভারির জন্য এই schema প্রস্তাব করেছেন: “Rider subClassOf Employee”, “Karim type Rider”, “Motorbike subClassOf Rider”, “Dhaka subClassOf Bangladesh”, “Order delivered_by Rider”, “Rider subClassOf Customer” (কারণ রাইডাররাও মাঝে মাঝে কেনেন)। কোন কোন রেখা ভুল, কেন, আর কীভাবে ঠিক করবেন?
- “Rider subClassOf Employee”, “Karim type Rider”: ঠিক, যদি সব রাইডার সত্যিই কর্মচারী হন।
- “Motorbike subClassOf Rider”: ভুল; মোটরবাইক রাইডারের প্রকার নয়, বাহন:
Karim uses_vehicle Bike7,Bike7 type Motorbike. না হলে অনুমানে প্রতিটি মোটরবাইক কর্মচারী হবে। - “Dhaka subClassOf Bangladesh”: অংশ আর প্রকার গুলানো। ঢাকা বাংলাদেশের একটি প্রকার নয়, অংশ:
Dhaka part_of Bangladesh. আর দুটিই instance (একটি নির্দিষ্ট জায়গা), class নয়। - “Order delivered_by Rider”: এটি schema-স্তরের কথা, তাই triple হিসেবে না লিখে property-র domain আর range হিসেবে লেখা উচিত:
delivered_by-এর domain Order, range Rider. তারপর প্রতিটি নির্দিষ্ট অর্ডারেO2 delivered_by Karim. - “Rider subClassOf Customer”: ভুল; কিছু রাইডার কেনেন মানে সব রাইডার গ্রাহক নয়। subClassOf মানে “প্রতিটি”। যে রাইডার কেনেন, তাঁর নিজের জন্য
Karim type Customerযোগ করুন; একটি instance একাধিক class-এর সদস্য হতে পারে।
যাচাই: অনুমানের আগে আর পরে প্রতিটি class-এর সদস্যসংখ্যা দেখুন; Employee-তে মোটরবাইক এলে schema-তে গোলমাল।
-
আউটপুট আন্দাজ করুন: অংশ ৪-এর store আর
queryব্যবহার করে কেউ চালালেনprint(len(query([("?a", "made_by", "?b"), ("?c", "made_by", "?b")]))). বাজারঘরের graph-এ Shimul দুটি (S10, CH20), Bokul দুটি (BUDS, PB10) আর Kodom দুটি (CB1, CH33) পণ্য বানায়, আর অন্য কোনোmade_bytriple নেই। কী ছাপা হবে? ফলের মধ্যে কয়টি সারিতে?aআর?cএকই পণ্য?12. প্রথম pattern ৬টি সারি দেয় (প্রতিটিmade_bytriple). দ্বিতীয় pattern-এ?bজানা, আর প্রতিটি ব্র্যান্ড ২টি পণ্য বানায়, তাই প্রতিটি সারি ২টি হয়: ৬ × ২ = ১২। বা ব্র্যান্ড ধরে: প্রতি ব্র্যান্ডে ২ × ২ = ৪টি জোড়া, তিন ব্র্যান্ডে ১২। এর মধ্যে ৬টি সারিতে?a=?c(প্রতিটি পণ্য নিজের সঙ্গে জোড়া)। লেখকের যন্ত্রে চালানো সত্যিকারের run-এও12. শুধু আলাদা জোড়া চাইলে SPARQL-এFILTER(?a != ?c)-এর মতো শর্ত লাগে; নীরব “নিজের সঙ্গে নিজের জোড়া” graph query-র পরিচিত ভুল।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি দোকানের পণ্য-ক্যাটালগ থেকে knowledge graph বানানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Graph কোন প্রশ্নের উত্তর দেবে, তার তালিকা লেখা
- Class, subClassOf আর property (domain, range) দিয়ে schema ঠিক করা
- উৎসের টেবিল আর ফাইল থেকে triple বানানো, প্রতিটির উৎস লিখে রেখে
- একই entity-র ভিন্ন বানান মেলানো (entity resolution)
- অনুমান চালানো আর প্রতিটি class-এর সদস্যসংখ্যা যাচাই করা
- Query চালিয়ে হাতে জানা উত্তরের সঙ্গে মিলিয়ে তারপর ব্যবহার শুরু
Triple (CB1, made_by, Kodom)-এ predicate কোনটি?
- CB1
- made_by
- Kodom
- পুরো triple-টি
একটি graph-এ আছে: Charger subClassOf MobileAccessory, MobileAccessory subClassOf Electronics, আর CH20 type Charger. subClassOf-এর transitivity আর “instance উপরের class-এরও সদস্য” নিয়ম fixpoint পর্যন্ত চালালে কয়টি নতুন triple আসবে?
- ১টি
- ২টি
- ৩টি
- ৪টি
একটি RDF graph-এ SPARQL ASK { :Rumana :livesIn :Dhaka } উত্তর দিল false. Open-world assumption মেনে এর সঠিক অর্থ কোনটি?
- Graph-এ এই triple নেই; Rumana ঢাকায় থাকেন কি না অজানা
- প্রমাণিত হলো Rumana ঢাকায় থাকেন না
- Query-তে ভুল আছে, তাই false
- Graph-এ Rumana নামে কেউ নেই, কারণ উত্তর ০
একজন schema-তে লিখলেন “Charger subClassOf Phone”, কারণ charger ফোনের সঙ্গে আসে। অনুমান চালালে সবচেয়ে বড় সমস্যা কী?
- Turtle ফাইল parse হবে না
- ফোনের সংখ্যা কমে যাবে
- অনুমান কখনো থামবে না
- সব charger Phone হিসেবেও গোনা হবে, ফোনের হিসাব নীরবে ভুল হবে
RDF graph আর property graph-এর পার্থক্য নিয়ে কোন কথাটি সবচেয়ে সঠিক?
- RDF-এ রেখার দিক নেই, property graph-এ আছে
- Property graph-এ edge-এর গায়ে সরাসরি property রাখা যায়; RDF-এ নাম IRI আর query SPARQL
- শুধু property graph-এ অনুমান (inference) করা যায়
- RDF শুধু ছোট ডেটার জন্য, property graph বড় ডেটার জন্য
একটি অংশীদারের ফাইলে একই ফোন “shimul s-10” আর “SHIMUL S10 (Black)” নামে আছে, আর ক্যাটালগে “Shimul S10”. মেলানো না হলে “S10-এর অর্ডার কয়টি” প্রশ্নে কী হবে, আর সাবধানী সমাধান কোনটি?
- কিছুই হবে না, graph নিজেই বুঝে নেবে
- সংখ্যা বেশি আসবে; সমাধান হলো কাছাকাছি সব নাম স্বয়ংক্রিয়ভাবে মেলানো
- সংখ্যা কম আসবে; normalisation key মিললে মেলানো, শুধু কাছাকাছি হলে মানুষ দিয়ে যাচাই
- সংখ্যা কম আসবে; সমাধান হলো অংশীদারের সব অর্ডার বাদ দেওয়া
একটি LLM-কে উত্তর লেখার আগে knowledge graph থেকে প্রাসঙ্গিক triple খুঁজে দেওয়া হয় (RAG). কোন দাবিটি সবচেয়ে যুক্তিসঙ্গত?
- উত্তরের উৎস দেখানো সহজ হয় আর কিছু ভুল কমতে পারে, কিন্তু hallucination পুরো দূর হয় না
- এরপর মডেল আর কখনো ভুল উত্তর দেবে না
- Graph দিলে মডেলের উত্তরে কোনো প্রভাব পড়ে না
- Graph-এর সব তথ্য মডেলের প্যারামিটারে স্থায়ীভাবে শেখানো হয়
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Knowledge graph = entity + নাম-দেওয়া সম্পর্ক + schema; প্রতিটি তথ্য একটি triple (subject, predicate, object). শিকড় ১৯৬০-এর দশকের semantic network আর ১৯৭৪-এর frame-এ।
- Query মানে pattern মেলানো আর join: variable binding পায়, পরের pattern-এ বসে; তিনটি index আর pattern-এর ভালো ক্রম কাজ কমায়। বাজারঘরের multi-hop প্রশ্নে ৩ → ৪ → ৪ → ৪ → ৮ → ২ সারি, উত্তর Mitu আর Nasrin.
- Ontology থেকে অনুমান: subClassOf transitive, instance উপরের class-এরও সদস্য; fixpoint পর্যন্ত চালালে ৫০ থেকে ৭৮টি triple. একটি ভুল is-a রেখা নীরবে সারা graph-এ ছড়ায়।
- RDF, Turtle, RDFS, OWL, SPARQL W3C-র মানদণ্ড, নাম IRI; RDF সাধারণত open-world: না থাকা মানে অজানা। Property graph আর Cypher পাশের পরিবার, রেখার গায়ে property সহ।
- আসল কাজ ডেটার মান: entity resolution, provenance আর ব্যক্তিগত তথ্যের সুরক্ষা। LLM আর embedding graph-এর সঙ্গে ভালো জোড়া, কিন্তু তাদের আন্দাজ প্রমাণ নয়, আর graph দিলেও hallucination শূন্য হয় না।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪৯ · Planning — লক্ষ্য থেকে কাজের ধারাবাহিকতাপরবর্তী পর্বSTRIPS-ধাঁচের action, PDDL আর search হিসেবে পরিকল্পনা।
- AI-এর ভিত্তি আর Python for AI · কোর্স হোম৭৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- DBMS · Graph, search আর time-series ডেটাবেসপাশের ট্র্যাকProperty graph, Cypher আর SQLite-এ graph, চালিয়ে দেখা।
- DSA · Breadth-first search (BFS)পাশের ট্র্যাকGraph-এ স্তরে স্তরে খোঁজার কোড আর জটিলতা।
- Cybersecurity · Privacy engineeringপাশের ট্র্যাকডেটা কমানো, ছদ্ম-পরিচয় আর ব্যক্তিগত তথ্যের আইনি দিক।