সরাসরি পর্বে যান
পর্ব ৫৩ · ৯১-এর মধ্যে · মডিউল ৯

Hugging Face ecosystem — transformers, datasets, tokenizers আর pipeline

The Hugging Face Ecosystem: transformers, datasets, tokenizers and pipeline

Taught in Bangla, this lesson opens the Hugging Face stack: reading model and dataset cards and licences, pinning a revision by commit hash, and reproducing what pipeline() does step by step with Auto classes on real Bangla comments. Real runs compare fast and legacy tokenizers, time batched datasets.map with Arrow and streaming, and in-page Python peeks inside a safetensors file to show why pickle and trust_remote_code carry risk, plus cache and offline mode.

এই পর্বে যা শিখবেন

  • Hub-এর model card, dataset card, লাইসেন্স আর commit hash পড়ে model বাছাই আর পিন করা
  • pipeline() কোন ক্রমে কী করে, আর Auto class দিয়ে একই কাজ ধাপে ধাপে
  • Fast tokenizer আর পুরনো Python tokenizer-এর পার্থক্য, মাপা সময় আর অমিলসহ
  • datasets-এর map, filter, set_format, Arrow আর streaming
  • safetensors বনাম pickle, trust_remote_code, cache আর offline mode

পর্ব ৪৮ · BERT পরিবার আর পর্ব ৫২ · LLM-এর ভেতরে-এ আমরা Hugging Face-এর টুল ব্যবহার করেছি, কিন্তু কাঠামোটি আলাদা করে দেখিনি। Pretrained model কী আর লাইসেন্স কেন দেখতে হয়, তার প্রথম পরিচয় AI-এর ভিত্তি কোর্সের পর্ব ৭১-এ। এখানে আমরা যন্ত্রের ভেতরে ঢুকব। Desktop-এর সব run CPU-তে, লেখার সময়ে (২০২৬ সালের নভেম্বর) এই version-এ: Python 3.12, torch 2.15.0, transformers 5.22.2, datasets 5.1.0, tokenizers 0.23.3, huggingface_hub 2.4.0, safetensors 0.8.0, evaluate 0.4.6. Version বদলালে আচরণও বদলাতে পারে, এই পর্বেই তার একটি উদাহরণ পাবেন। ডেটা পর্ব ৪২ · Text classification-এর Sazzed-এর বাংলা YouTube মন্তব্য (CC BY 4.0), পরিষ্কার করার পর ১০,৭০৯টি।

১ · মানচিত্র — কোন library কোন কাজ করে

একটি বড় পাইকারি বাজারের কথা ভাবুন। গুদামে মাল থাকে, প্রতিটি বস্তায় লেবেল আর চালানের রসিদ। কেউ মাল মাপে, কেউ প্যাক করে, কেউ দোকানে পৌঁছে দেয়। Hugging Face-এর জগৎও এমন ভাগ করা। Hub হলো গুদাম: প্রতিটি model বা dataset একটি repository, যার Git-এর মতো commit ইতিহাস আছে। বাকি library-গুলো আলাদা আলাদা কাজের লোক।

Hugging Face ecosystem map Hub (huggingface.co) model repodataset repo huggingface_hub local cache (HF_HOME) tokenizerstransformersdatasets pipeline() card · sha cf99110card · Arrow evaluate · accelerate: on the side
Hub-এ ফাইল আর ইতিহাস; huggingface_hub নামায়; cache-এ জমা হয়; তিনটি library পড়ে; pipeline() সব একসঙ্গে জোড়ে।
Libraryকাজএই পর্বে
huggingface_hubHub থেকে ফাইল নামানো, cache, model_info, card পড়াঅংশ ২, ৩, ৯
tokenizersRust-এ লেখা দ্রুত tokenizerঅংশ ৬
transformersModel-এর কোড, Auto class, pipelineঅংশ ৪, ৫
datasetsArrow-ভিত্তিক table, map, filter, streamingঅংশ ৭
safetensorsওজন রাখার নিরাপদ ফাইল ফরম্যাটঅংশ ৮
evaluate, accelerateMetric; একাধিক device-এ চালানোঅংশ ৯-এ সংক্ষেপে

ইতিহাস: transformers library শুরু হয় ২০১৮ সালের শেষে pytorch-pretrained-bert নামে, BERT-এর ওজন PyTorch-এ আনার জন্য। ২০১৯ সালে নাম হয় pytorch-transformers, পরে শুধু transformers. Wolf ও সহলেখকেরা EMNLP ২০২০-এর system demonstration-এ library-টি বর্ণনা করেন। datasets-এর বর্ণনা Lhoest ও সহলেখকদের EMNLP ২০২১-এর demo প্রবন্ধে।

২ · Hub — model card, dataset card আর লাইসেন্স

বাজারে চালের বস্তা কেনার আগে আপনি লেবেল দেখেন: কোন জাত, কোন মৌসুমের, কোথা থেকে। Model card সেই লেবেল। এটি repo-র README.md: ওপরে একটি YAML অংশ (license, language, datasets, pipeline_tag), নিচে মানুষের পড়ার লেখা। Dataset card একই কাজ করে ডেটার জন্য। কার্ড না পড়ে model চালানো মানে লেবেল ছাড়া বস্তা কেনা।

পড়ার সময় পাঁচটি প্রশ্ন করুন। এক, লাইসেন্স কী, আর আমার কাজে কি চলে? দুই, model কোন কাজের জন্য বানানো (intended use)? তিন, কোন ডেটায় শেখানো, কোন ভাষায়? চার, কীভাবে মাপা হয়েছে? পাঁচ, সীমাবদ্ধতা কী লেখা আছে? আমরা তিনটি সত্যিকারের কার্ড huggingface_hub দিয়ে পড়েছি। প্রথমে একটি সরল keyword খোঁজা, তারপর নিজের চোখে। সত্যিকারের run, কয়েকটি লাইন ছোট করা:

~/bazarghar-dl$ uv run python r1_cards.py
== lxyuan/distilbert-base-multilingual-cased-sentiments-student
 sha: cf991100d706c13c0a080c097134c05b7f436c45 | last modified: 2025-03-03
 card yaml license: apache-2.0 | pipeline_tag: text-classification | gated: False | datasets: ['tyqiangz/multilingual-sentiments']
 languages: ['en', 'ar', 'de', 'es', 'fr', 'ja', 'zh', 'id', 'hi', 'it', 'ms', 'pt']
 weight files (MB): {'model.safetensors': 541.3, 'onnx/model.onnx': 541.4, 'pytorch_model.bin': 541.3, …}
 remote code files: []
  limits         MISSING
== sagorsarker/bangla-bert-base
 sha: 875aa80a42ec196c16bd931ae5d85ad949f58b16 | last modified: 2023-07-21
 card yaml license: mit | pipeline_tag: fill-mask | gated: False | datasets: ['common_crawl', 'wikipedia', 'oscar']
  limits         found · …. we will add it here.**   ## limitations and biases  ## how to use …
== Qwen/Qwen2.5-0.5B-Instruct
 sha: 7ae557604adf67be50417f59c2c2f167def9a775 | last modified: 2024-09-25
 card yaml license: apache-2.0 | pipeline_tag: text-generation | gated: False | datasets: None
 weight files (MB): {'model.safetensors': 988.1}
  limits         found · …wiglu, rmsnorm, attention qkv bias and tied word embeddings …
  training data  MISSING

Keyword খোঁজা দুবার ঠকেছে। Bangla BERT-এ “limitations” পাওয়া গেছে, কিন্তু শিরোনামের নিচে কিছুই লেখা নেই। Qwen-এ “bias” মিলেছে “attention QKV bias” থেকে, যা architecture-এর একটি অংশ, সামাজিক bias নয়। তাই কার্ড মানুষকেই পড়তে হয়; script শুধু কোথায় তাকাতে হবে দেখায়।

প্রথম model-এর ভাষার তালিকায় bn নেই। কার্ড অনুযায়ী এটি একটি zero-shot teacher (MoritzLaurer/mDeBERTa-v3-base-mnli-xnli) থেকে distill করা ছাত্র model. কার্ডের শুরুতেই Trainer-এর স্বয়ংক্রিয় মন্তব্য আছে, “proofread and complete it”. অংশ ৫-এ দেখবেন বাংলায় এর ফল কেমন।

কার্ডে যা পেলাম
প্রশ্ন
lxyuan
bangla-bert
Qwen 0.5B
  1. লাইসেন্স. তিনটিই YAML-এ লাইসেন্স লিখেছে: দুটি Apache 2.0, একটি MIT. দুটিই বাণিজ্যিক ব্যবহারের অনুমতি দেয়, শর্তসহ। তবু repo-তে আলাদা LICENSE ফাইল থাকলে সেটিও পড়ুন। পর্ব ৪৮-এ দেখেছেন, একটি বাংলা model-এর README-তে non-commercial শর্ত ছিল।
  2. Intended use. কোনো কার্ডেই আলাদা “intended use” অংশ নেই। “How to Use” বা Quickstart কীভাবে চালাবেন বলে, কোথায় চালানো উচিত বলে না। এই ফাঁক আপনাকেই নিজের কাজে মেপে ভরাতে হবে।
  3. Training data. Bangla BERT উৎস বলে: OSCAR আর বাংলা Wikipedia. প্রথম model একটি dataset-এর নাম দেয়, আর ভাষার তালিকায় বাংলা নেই। Qwen-এর কার্ডে ডেটার বিস্তারিত নেই; বড় model-এ এটি সাধারণ ঘটনা।
  4. Evaluation. প্রথম model-এ শুধু training-এর log. Bangla BERT কার্ড অন্য একজনের মাপা classification ফল উল্লেখ করে। Qwen একটি blog-এ পাঠায়। কোনোটিই আপনার ডেটায় মাপা নয়।
  5. Limitations. একটিতে শিরোনাম ফাঁকা, দুটিতে অংশই নেই। “লেখা নেই” মানে “সমস্যা নেই” নয়। সিদ্ধান্ত: তিনটিই ব্যবহারযোগ্য লাইসেন্সে, কিন্তু মান জানতে নিজের test set লাগবে।

ডেটারও কার্ড লাগে। আমাদের Sazzed-এর ডেটা Hub-এর নয়, Mendeley Data-র (DOI 10.17632/p6zc7krs37.4), তবু একই প্রশ্নগুলো করা যায়। উৎস: বাংলা নাটকের YouTube মন্তব্য, মানুষের হাতে লেবেল। লাইসেন্স: CC BY 4.0, অর্থাৎ ব্যবহার আর বিতরণ চলে, তবে উৎসের স্বীকৃতি দিতে হবে; সঙ্গের README একটি প্রবন্ধের উদ্ধৃতি চায়। সীমা: নাটকের মন্তব্য, পণ্যের রিভিউ নয়; পরিষ্কার করার পর positive প্রায় ৭১.৫%. এই ডেটায় শেখানো model বাজারঘরের রিভিউতে একই রকম চলবে, এমন নিশ্চয়তা নেই। এমন কয়েক লাইনই একটি কাজের dataset card.

এবার নিজে একটি checklist scorer চালান। তিনটি বানানো কার্ড পাতায় রাখা আছে: একটি পরিপূর্ণ, একটি বিজ্ঞাপনের মতো, আর একটি ফাঁকা শিরোনামওয়ালা। Scorer শুধু শব্দ খোঁজে না, অংশের ভেতরে লেখা আছে কি না, আর evaluation-এ অন্তত একটি সংখ্যা আছে কি না তাও দেখে।

import re
NEED = ["intended use", "training data", "evaluation", "limitations"]

def read_card(path):
    text = open(path, encoding="utf-8").read()
    _, yaml, body = text.split("---", 2)
    meta = dict(re.findall(r"^(\w+):\s*(.+)$", yaml, re.M))
    parts = re.split(r"^## (.+)$", body, flags=re.M)        # [আগে, h1, b1, h2, b2, ...]
    secs = {h.strip().lower(): b.strip() for h, b in zip(parts[1::2], parts[2::2])}
    return meta, secs, body.lower()

def score(path):
    meta, secs, body = read_card(path)
    rows = [("license", "license" in meta, meta.get("license", "নেই"))]
    for k in NEED:
        hit = [b for h, b in secs.items() if h.startswith(k)]
        if not hit:
            why = "শব্দটি আছে, অংশ নেই" if k.split()[0] in body or (k == "limitations" and "bias" in body) else "অংশ নেই"
            rows.append((k, False, why))
        elif not hit[0]:
            rows.append((k, False, "শিরোনাম আছে, লেখা ফাঁকা"))
        elif k == "evaluation" and not re.search(r"\d", hit[0]):
            rows.append((k, False, "কোনো সংখ্যা নেই, শুধু দাবি"))
        else:
            rows.append((k, True, " ".join(hit[0].split()[:4]) + " …"))
    return rows

for path in ("card_a.md", "card_b.md", "card_c.md"):
    rows = score(path)
    print(f"\n{path}: {sum(ok for _, ok, _ in rows)}/{len(rows)}")
    for k, ok, why in rows:
        print(f"  {'✓' if ok else '✗'} {k:14} {why}")

আউটপুট (পাতার Python-এ চালানো):

card_a.md: 5/5
  ✓ license        apache-2.0
  ✓ intended use   বাজারঘরের বাংলা পণ্য-রিভিউয়ের positive/negative …
  ✓ training data  ১২,০০০ রিভিউ (২০২৫–২৬), দুজন …
  ✓ evaluation     আলাদা test set-এ macro-F1 …
  ✓ limitations    ব্যঙ্গ আর বাংলা-ইংরেজি মেশানো …

card_b.md: 0/5
  ✗ license        নেই
  ✗ intended use   অংশ নেই
  ✗ training data  অংশ নেই
  ✗ evaluation     কোনো সংখ্যা নেই, শুধু দাবি
  ✗ limitations    শব্দটি আছে, অংশ নেই

card_c.md: 3/5
  ✓ license        mit
  ✗ intended use   অংশ নেই
  ✓ training data  OSCAR আর বাংলা Wikipedia …
  ✓ evaluation     একটি বাংলা classification কাজে …
  ✗ limitations    শিরোনাম আছে, লেখা ফাঁকা

card_b একটি বিজ্ঞাপন: “Beats every model of its size!” evaluation অংশে আছে, কিন্তু একটি সংখ্যাও নেই, তাই ✗. তার “bias” শব্দটি architecture-এর, তাই scorer বলছে “শব্দটি আছে, অংশ নেই”, ঠিক সত্যিকারের Qwen কার্ডের ফাঁদের মতো। card_c Bangla BERT কার্ডের গঠন নকল করে বানানো: “How to Use”-কে intended use ধরা হয়নি, আর ফাঁকা Limitations ধরা পড়েছে। ৫/৫ পেলেও কার্ডের দাবি সত্য কি না scorer জানে না; সেটি নিজের test set-এ যাচাই করতে হয়।

৩ · Revision আর commit hash — পুনরুৎপাদনের চাবি

Hub-এর প্রতিটি repo একটি Git repository. লেখক নতুন ফাইল push করলে নতুন commit হয়, আর main নামের branch নতুন commit-এ সরে যায়। from_pretrained(name) লিখলে ডিফল্টে main নামে। অর্থাৎ আজ আর তিন মাস পর একই লাইন দুটি আলাদা ফাইল আনতে পারে। চায়ের দোকানের খাতায় “আজকের দাম” লিখলে কাল তা বদলায়; তারিখসহ রসিদ বদলায় না। Commit hash সেই রসিদ: ফাইলের বিষয়বস্তু আর ইতিহাস থেকে হিসাব করা ৪০ অক্ষরের নাম। Reproducibility-র বাকি অংশ (seed, version লিখে রাখা) পর্ব ১৯ · Experiment tracking-এ।

নিচের খেলনা Hub-এ একটি ছোট classifier আছে। দ্বিতীয় দিনে লেখক ওজন একই রেখে শুধু label-এর ক্রম বদলালেন। আপনার কোড একটি অক্ষরও বদলায়নি।

import hashlib, json
import numpy as np

class ToyHub:                                   # একটি repo: commit ইতিহাস আর branch
    def __init__(self): self.commits, self.refs = {}, {}
    def push(self, files, parent=None, branch="main"):
        body = json.dumps({"files": files, "parent": parent}, sort_keys=True)
        sha = hashlib.sha1(body.encode()).hexdigest()
        self.commits[sha] = files; self.refs[branch] = sha
        return sha
    def resolve(self, rev):                     # branch নাম → sha, sha → sha
        return self.refs.get(rev, rev)

CACHE, downloads = {}, []
def load(hub, rev="main"):
    sha = hub.resolve(rev)
    if sha not in CACHE:                        # cache-এর চাবি commit hash
        CACHE[sha] = hub.commits[sha]; downloads.append(sha[:7])
    return sha, CACHE[sha]

def predict(files, x):
    w = np.array(files["weight"]); return int(np.argmax(w @ x)), files["labels"]

hub = ToyHub()
v1 = hub.push({"weight": [[1, -1], [-1, 1]], "labels": ["neg", "pos"]})
x = np.array([0.2, 0.9])
pinned = v1                                     # কাজ শুরুর দিন লিখে রাখা
for day, rev in [(1, "main"), (1, pinned), (2, "main"), (2, pinned), (3, "main")]:
    if day == 2 and len(hub.commits) == 1:      # দ্বিতীয় দিনে লেখক label-এর ক্রম বদলালেন
        hub.push({"weight": [[1, -1], [-1, 1]], "labels": ["pos", "neg"]}, parent=v1)
    sha, files = load(hub, rev)
    k, labels = predict(files, x)
    print(f"day {day} rev={rev[:7]:7} → sha {sha[:7]} | label: {labels[k]}")
print("নামানো হয়েছে:", downloads, "| cache-এ commit:", len(CACHE))
print("main এখন:", hub.resolve("main")[:7], "| pinned:", pinned[:7])

আউটপুট (পাতার Python-এ চালানো):

day 1 rev=main    → sha da920ab | label: pos
day 1 rev=da920ab → sha da920ab | label: pos
day 2 rev=main    → sha ff3ed6e | label: neg
day 2 rev=da920ab → sha da920ab | label: pos
day 3 rev=main    → sha ff3ed6e | label: neg
নামানো হয়েছে: ['da920ab', 'ff3ed6e'] | cache-এ commit: 2
main এখন: ff3ed6e | pinned: da920ab

একই input, একই ওজন, একই কোড, তবু দ্বিতীয় দিন থেকে main-এ উত্তর “neg”. পিন করা hash আগের মতো “pos” দেয়। কোনো error আসেনি। সত্যিকারের Hub-এও cache ফোল্ডার commit hash ধরে সাজানো, তাই দুই revision পাশাপাশি থাকতে পারে (অংশ ৯-এর run-এ দেখবেন)। Label-এর ক্রম বদলানো কাল্পনিক উদাহরণ, তবে id2label বা tokenizer-এর ছোট বদল বাস্তবেও নীরবে ফল বদলাতে পারে।

আপনার খাতা
কাজ
কোড
ফল
  1. Hash খোঁজা. huggingface_hub.model_info(name).sha বর্তমান commit দেয়। আমাদের sentiment model-এর জন্য তা cf991100d706…. এটি experiment-এর নোটে লিখে রাখুন।
  2. Hash দিয়ে নামানো. revision=sha config, tokenizer আর model, তিনটির ডাকেই দিন। একটিতে ভুলে গেলে tokenizer এক commit-এর, ওজন আরেকটির হতে পারে।
  3. Cache. ফাইল নামে commit-এর নামে একটি snapshot ফোল্ডারে। পরের বার নেটওয়ার্ক না থাকলেও খোলে।
  4. লেখক push করলেন. main সরে গেল। main দিয়ে চালানো সহকর্মী নতুন ফাইল পাবেন; আপনার কোড পুরনোটিই পাবে।
  5. পরের মাসে. একই hash, একই ফাইল, তাই একই output (একই library version আর হার্ডওয়্যারে)। নতুন version নিতে চাইলে সচেতনভাবে hash বদলান আর আবার মাপুন।

৪ · pipeline() — এক লাইনের ভেতরে কী লুকানো

pipeline() রেস্তোরাঁর “সেট মেনু”-র মতো: একটি নাম বললেই পুরো থালা আসে। সুবিধা হলো গতি। অসুবিধা হলো, থালায় কী কী আছে তা আপনি বেছে দেননি। নিচের stepper-এর সব সংখ্যা সত্যিকারের run থেকে, বাক্যটি “ডেলিভারি দেরি, কিন্তু পণ্য ভালো”।

এই ধাপে
ধাপ
ফল
  1. Task আর model. আমরা task, model আর revision তিনটিই দিয়েছি। Model না দিলে library নিজে একটি ডিফল্ট model বেছে নেয় আর warning দেয়; সেই ডিফল্ট version-ভেদে বদলাতে পারে।
  2. Config. config.json থেকে architecture আর id2label আসে। কোন output সারি কোন label, তা এখানেই ঠিক হয়।
  3. Tokenizer. একই repo থেকে tokenizer. এটি multilingual WordPiece, বাংলার জন্য আলাদা করে বানানো নয়।
  4. Model লোড. model.safetensors থেকে ওজন, আর model eval() অবস্থায় (run-এ model.training: False).
  5. Preprocess. বাক্যটি ১৮টি id হলো, shape (1, 18). “ডেলিভারি” পাঁচ টুকরো, “ভালো” তিন টুকরো: ভাষাটি এই tokenizer-এর ভালো চেনা নয়।
  6. Forward. Gradient বন্ধ রেখে model তিনটি logit দেয়। forward() এই কাজ সামলায়; ভেতরের _forward সরাসরি ডাকলে আমাদের run-এ gradient-সহ tensor এসে error দিয়েছিল।
  7. Postprocess. Softmax, তারপর সবচেয়ে বড়টি: negative, ০.৪১৭. Positive-ও ০.৩৬৮, খুব কাছে। মিশ্র বাক্যে এমন অনিশ্চয়তা স্বাভাবিক, তবে ০.৪১৭-কে “৪২% নিশ্চিত” পড়বেন না, calibration না মেপে।

ধাপগুলো আলাদা করে ডাকার সত্যিকারের run (একটু ছোট করা):

~/bazarghar-dl$ uv run python r4_inside.py
preprocess keys: ['input_ids', 'attention_mask'] | shape: (1, 18)
tokens: ['[CLS]', 'ড', '##েল', '##ি', '##ভা', '##রি', 'দ', '##ের', '##ি', ',', 'কিন্তু', 'প', '##ণ', '##্য', 'ভ', '##াল', '##ো', '[SEP]']
logits: [0.146, -0.394, 0.272]
softmax: [0.368, 0.214, 0.417]
postprocess: [{'label': 'positive', 'score': 0.36803287267684937}, {'label': 'neutral', 'score': 0.21446722745895386}, {'label': 'negative', 'score': 0.41749992966651917}]
model.training: False | framework device: cpu

Softmax হাতে মিলিয়ে নিন। e০.১৪৬ ≈ ১.১৫৭, e−০.৩৯৪ ≈ ০.৬৭৪, e০.২৭২ ≈ ১.৩১৩; যোগফল ৩.১৪৪. ভাগ করলে ০.৩৬৮, ০.২১৪ আর ০.৪১৭। Pipeline নতুন কিছু করে না; এই তিনটি ধাপই জোড়ে। “চালানো যায় এমন কোড” আর “কী চলছে জানা কোড”-এর পার্থক্য এটুকুই।

Batch দিলেই কি দ্রুত? একই ৪০০টি মন্তব্য, একই model, CPU-তে (সত্যিকারের run, একবার):

pipeline batch_size= 1:   6.9 s
pipeline batch_size=16:  14.0 s

Batch ১৬ এখানে দ্বিগুণ ধীর। কারণ padding: একটি batch-এর সব বাক্যকে সবচেয়ে লম্বাটির মাপে টানতে হয়, আর মন্তব্যগুলোর দৈর্ঘ্য খুব আলাদা। CPU-তে বাড়তি শূন্যের হিসাবও সময় নেয়। Hugging Face-এর pipeline batching নির্দেশিকাও বলে, batching সব সময় দ্রুত করে না; নিজের হার্ডওয়্যার আর ডেটায় মেপে দেখুন। GPU-তে আর দৈর্ঘ্য অনুযায়ী সাজালে ফল উল্টো হতে পারে (পর্ব ১২ · GPU performance).

৫ · Auto class দিয়ে একই কাজ, আর ফল মেলানো

Auto class (AutoConfig, AutoTokenizer, AutoModelForSequenceClassification) config-এর model_type দেখে ঠিক class বেছে নেয়। আপনাকে DistilBertForSequenceClassification নাম জানতে হয় না। নিচের কোড r3_pipeline.py-এর মূল অংশ, একটু ছোট করে দেখানো:

cfg = AutoConfig.from_pretrained(NAME, revision=REV)
tok = AutoTokenizer.from_pretrained(NAME, revision=REV)
model = AutoModelForSequenceClassification.from_pretrained(NAME, revision=REV).eval()
with torch.inference_mode():
    for s in range(0, len(texts), 16):
        enc = tok(texts[s:s+16], padding=True, truncation=True, return_tensors="pt")
        p = torch.softmax(model(**enc).logits, dim=-1)
        labels += [cfg.id2label[int(b)] for b in p.argmax(-1)]
        scores += p.max(-1).values.tolist()

৪০০টি মন্তব্য (seed ৫৩-এ বাছাই), pipeline আর হাতের পথ পাশাপাশি। সত্যিকারের run:

pipeline load: 431.0 s | TextClassificationPipeline
model: DistilBertForSequenceClassification | tokenizer: DistilBertTokenizer
[{'label': 'positive', 'score': 0.5337483286857605}, {'label': 'positive', 'score': 0.48220622539520264}]
pipeline 400 rows: 12.6 s
id2label: {0: 'positive', 1: 'neutral', 2: 'negative'} | problem_type: None
manual 400 rows: 13.2 s
labels identical: True | max score diff: 5.96e-08
predicted: {'positive': 270, 'negative': 127, 'neutral': 3}
non-neutral: 397 | agree with Sazzed label: 0.68
majority baseline on same rows: 0.703

দুই পথ এক: ৪০০টি label হুবহু মিলেছে; score-এর সর্বোচ্চ পার্থক্য ৫.৯৬ × ১০−৮, float32-এর গোলকরণের মাত্রা। প্রথম লোডের ৪৩১ সেকেন্ডের প্রায় পুরোটাই ৫৪১ MB ফাইল নামানো; পরে cache থেকে লোড আধা সেকেন্ড (অংশ ৯)।

কিন্তু model কাজের নয়: দ্বিতীয় লাইনের দ্বিতীয় বাক্য “একদম বাজে, টাকা নষ্ট”, আর model বলছে positive. Sazzed-এর label-এর সঙ্গে মিল ০.৬৮, অথচ সব মন্তব্যকে positive বললেই ০.৭০৩। evaluate দিয়ে neutral-কে negative ধরে macro-F1 ০.৬২৪। তুলনায় পর্ব ৪২-এর TF-IDF baseline এই ডেটায় macro-F1 ০.৯৪১ পেয়েছিল (ভিন্ন split আর ভিন্ন হিসাব, তাই শুধু দিক বোঝায়)। কার্ডের ভাষার তালিকায় বাংলা না থাকার মানে এখানে সংখ্যায় দেখা গেল। এটি এই model-এর কাজের নির্দিষ্ট ক্ষেত্রের বাইরের ফল, model-টিকে “খারাপ” বলার প্রমাণ নয়।

Pipeline আর হাতের কোড মিলল মানে দুটিই একই ভুল করছে, ঠিক কাজ করছে এমন নয়। মেলানো দেখায় আপনি pipeline বুঝেছেন; মান জানতে লাগে label-ওয়ালা test set আর একটি সরল baseline. Model তুলনার পরিসংখ্যান মেশিন লার্নিং কোর্সের পর্ব ২২-এ।

যে repo-তে classification head নেই

Bangla BERT শুধু masked language model হিসেবে শেখানো। একই repo থেকে তিনটি Auto class কী দেয়? (সত্যিকারের run, CPU-তে; transformers-এর load report থেকে কয়েকটি লাইন বেছে নেওয়া)

AutoModel                    -> BertModel
AutoModelForMaskedLM         -> BertForMaskedLM
seq-cls seed 0: BertForSequenceClassification logits [-0.4646, -0.0229]
seq-cls seed 1: BertForSequenceClassification logits [-0.0898, -0.0963]
id2label (default): {0: 'LABEL_0', 1: 'LABEL_1'}
classifier.weight                          | MISSING    |
classifier.bias                            | MISSING    |
cls.seq_relationship.weight                | UNEXPECTED |  |
- MISSING:	those params were newly initialized because missing from the checkpoint. Consider training on your downstream task.

AutoModelForSequenceClassification কোনো error দেয়নি, কিন্তু classifier স্তর checkpoint-এ নেই (MISSING), তাই এলোমেলো মানে শুরু হয়েছে। Seed বদলালে একই বাক্যে logit বদলায়, আর label-এর নাম শুধু LABEL_0, LABEL_1. এই model fine-tune না করে ব্যবহার করলে ফল মুদ্রা ছোড়ার মতো। উল্টো দিকে, pretraining-এর head (cls.*) এখানে UNEXPECTED, ফেলে দেওয়া হয়েছে। Load report পড়া তাই অভ্যাস করুন। Head শেখানো পর্ব ৫৪ · Transformer fine-tuning-এর কাজ।

৬ · Tokenizers — fast, legacy আর দুটি অমিল

Tokenization-এর তত্ত্ব (BPE, WordPiece, বাংলার সমস্যা) পর্ব ৪০ · Tokenization-এ। এখানে প্রশ্ন যান্ত্রিক: কোন class চলছে, কত দ্রুত, আর ফল কি সব সময় এক? Fast tokenizer Rust-এ লেখা tokenizers library ব্যবহার করে; batch দিলে একসঙ্গে অনেক বাক্য ভাঙে, আর প্রতিটি token মূল লেখার কোন অক্ষর থেকে এসেছে (offset) তাও দেয়। Bangla BERT-এর জন্য সবসময় do_lower_case=False (পর্ব ৪৮-এর ফাঁদ)। সত্যিকারের run:

tokenizer class: BertTokenizer | is_fast: True
tokens: ['আমার', 'জীবনের', 'সব', '##থেকে', 'ফালতু', 'একটা', 'নাটক', 'এইট', '##া']
use_fast=False gives: BertTokenizer | legacy class: BertTokenizerLegacy is_fast: False
fast   batched=False:   0.97 s (median of 3) | calls: 10709
fast   batched=True :   0.32 s (median of 3) | calls: 11
legacy batched=False:   2.20 s (median of 3) | calls: 10709
legacy batched=True :   1.73 s (median of 3) | calls: 11
legacy == fast ids on 500 rows: False

Version-এর চমক: transformers 5.22.2-এ use_fast=False দিয়েও fast tokenizer-ই এসেছে, class-এর নাম BertTokenizer. পুরনো Python tokenizer পেতে BertTokenizerLegacy নিজে ডাকতে হয়েছে। পুরনো tutorial বা AI assistant “use_fast=False দিলে slow পাবেন” বললে, এই version-এ তা মেলেনি। নিজের version-এ type(tok).__name__ আর tok.is_fast print করে দেখুন।

গতি: Batched map-এ fast tokenizer legacy-র চেয়ে প্রায় ৫.৪ গুণ দ্রুত (১.৭৩ ÷ ০.৩২)। একটি মেশিনে তিনবারের median, অন্য কাজও চলছিল; সাধারণ নিয়ম নয়।

“দুটি একই tokenizer” কথাটি পরীক্ষা করা দরকার। পুরো ১০,৭০৯টি সারিতে তুলনা (সত্যিকারের run):

rows differing: 2 of 10709
'￰সাইড আর্টিস্ট গুলুর এতো বাজে অভিনয় সত্যি বিরক্তিকর আর ফারি'
  fast  : ['[UNK]', 'আর্টিস্ট', 'গুল', '##ুর']
  legacy: ['সাইড', 'আর্টিস্ট', 'গুল', '##ুর']
'অসাধারন৤৤৤৤৤৤'
  fast  : ['[UNK]']
  legacy: ['অসাধারন']

দুটি সারিতেই অদৃশ্য, অস্বাভাবিক Unicode code point আছে: U+FFF0 (কোনো অক্ষর নয়) আর U+09E4 (বাংলা block-এর একটি ফাঁকা জায়গা)। Legacy tokenizer এগুলো মুছে শব্দটি রাখে; fast tokenizer পুরো শব্দকে [UNK] বানায়। “অসাধারন” মন্তব্যটির পুরো অর্থই হারাল। শিক্ষা দুটি: tokenizer বদলালে কয়েকটি সারিতে তুলনা চালান, আর input-এ Unicode পরিষ্কারের ধাপ রাখুন (পর্ব ৩৯ · NLP pipeline-এর বিষয়)।

Offset আর word_ids: Fast tokenizer প্রতিটি token-এর জন্য মূল লেখায় শুরু আর শেষের অবস্থান দেয়, আর word_ids() বলে token কোন শব্দের। পর্ব ৪৩ · Sequence labeling-এর মতো NER-এ subword-এর label মেলাতে এটিই লাগে। সত্যিকারের run:

[CLS]      ( 0,  0) ''
ডেলিভারি   ( 0,  8) 'ডেলিভারি'
দেরি       ( 9, 13) 'দেরি'
,          (13, 14) ','
পণ্য       (15, 19) 'পণ্য'
##টা       (19, 21) 'টা'
ভালো       (22, 26) 'ভালো'
[SEP]      ( 0,  0) ''
word_ids: [None, 0, 1, 2, 3, 3, 4, None]
legacy keys: ['input_ids', 'attention_mask']

“পণ্যটা” দুই টুকরো, দুটিরই word id ৩; special token-এর None. শেষ লাইনটি আরেকটি নীরব পার্থক্য: legacy tokenizer-কে একই return_offsets_mapping=True দিলে error দেয়নি, offset-ও দেয়নি, শুধু চুপচাপ বাদ দিয়েছে। Bangla BERT-এর এই tokenizer-এ বাংলা শব্দ mBERT-এর চেয়ে অনেক কম টুকরো হয়: অংশ ৪-এ “ডেলিভারি” পাঁচ টুকরো ছিল, এখানে এক।

৭ · datasets — map, batched, filter, set_format, Arrow আর streaming

পোস্ট অফিসে চিঠি এক এক করে বাছলে প্রতিটির জন্য উঠে দাঁড়াতে হয়, তাক খুলতে হয়। এক বস্তা একসঙ্গে বাছলে সেই বাড়তি কাজ একবার। Dataset.map(fn, batched=True) ঠিক এটি করে: fn একটি সারির বদলে ১,০০০ সারির একটি dict (প্রতিটি কলাম একটি list) পায়। ডাকের সংখ্যা কমে, আর fast tokenizer পুরো list একবারে ভাঙতে পারে।

নিচের খেলনা mini_map সেই কৌশল নকল করে। প্রতিটি ডাকের ফল একটি টাইপ করা NumPy array-তে লেখা হয়, যেমন datasets প্রতিটি ফল Arrow কলামে লেখে। প্রথমবার চালাতে কয়েক সেকেন্ড লাগতে পারে, কারণ পাতার Python আর NumPy লোড হয়।

import time
import numpy as np
rng = np.random.default_rng(53)
words = np.array(["দাম", "ভালো", "বাজে", "ডেলিভারি", "দ্রুত", "নষ্ট", "সুন্দর", "দেরি"])
rows = [" ".join(rng.choice(words, rng.integers(3, 9))) for _ in range(20500)]
VOCAB = {w: i + 5 for i, w in enumerate(words)}         # 0-4 special id

def tok(texts):                       # সবসময় list নেয়, list ফেরত দেয়
    return [[VOCAB.get(w, 1) for w in t.split()] for t in texts]

def mini_map(texts, batched, batch_size=1000):
    calls, chunks, lens = 0, [], []
    step = batch_size if batched else 1
    for s in range(0, len(texts), step):
        ids = tok(texts[s:s + step])  # একটি ডাক
        calls += 1
        lens += [len(x) for x in ids]
        flat = np.fromiter((i for x in ids for i in x), np.int32)
        chunks.append(flat)           # Arrow-এর মতো টাইপ করা কলামে লেখা
    return np.concatenate(chunks), np.array(lens), calls

res = {}
for batched in (False, True):
    t = time.perf_counter()
    res[batched] = mini_map(rows, batched)
    ms = (time.perf_counter() - t) * 1000
    print(f"batched={batched!s:5} ডাক={res[batched][2]:6d}  সময় ≈ {ms:.0f} ms")
same = all(np.array_equal(a, b) for a, b in zip(res[False][:2], res[True][:2]))
print("দুই পথের ফল এক?", same)
print("প্রথম সারি:", rows[0], "→", res[True][0][:res[True][1][0]].tolist())
print("মোট id:", res[True][0].size, "| dtype:", res[True][0].dtype)
print("শেষ batch-এর আকার:", len(rows) % 1000, "| ডাক = ceil(20500/1000) =", -(-len(rows) // 1000))

আউটপুট (পাতার Python-এ চালানো; সময় প্রতিবার আর প্রতি মেশিনে আলাদা):

batched=False ডাক= 20500  সময় ≈ 159 ms
batched=True  ডাক=    21  সময় ≈ 47 ms
দুই পথের ফল এক? True
প্রথম সারি: দাম নষ্ট নষ্ট সুন্দর বাজে দ্রুত দেরি → [5, 10, 10, 11, 7, 9, 12]
মোট id: 112274 | dtype: int32
শেষ batch-এর আকার: 500 | ডাক = ceil(20500/1000) = 21

২০,৫০০টি সারি, ১,০০০-এর batch: ডাক ⌈২০,৫০০ ÷ ১,০০০⌉ = ২১, শেষ batch-এ ৫০০টি। ফল হুবহু এক, কারণ batched function প্রতিটি সারিতে একই কাজ করে; শুধু একসঙ্গে। ডাক প্রায় ১,০০০ গুণ কমেছে, কিন্তু সময় কমেছে প্রায় ৩ গুণ (এই run-এ ১৫৯ থেকে ৪৭ ms), কারণ প্রতিটি শব্দ খোঁজার মূল কাজ একই থাকে; বাঁচে শুধু প্রতি ডাকের বাড়তি খরচ। আসল datasets-এ প্রতিটি ডাকে Arrow থেকে Python-এ রূপান্তর আর লেখার খরচ আছে, আর fast tokenizer list পেলে Rust-এ একসঙ্গে কাজ করে; তাই লাভ বেশি। একটি শর্ত: batched function-কে সব কলামের জন্য সমান দৈর্ঘ্যের list ফেরত দিতে হয়, যদিও সারির সংখ্যা বদলাতে পারে (একটি সারি ভেঙে কয়েকটি chunk).

এবার আসল datasets, Sazzed-এর মন্তব্যে (সত্যিকারের run, r2_datasets.py; উপরের tokenizer-এর সময়ও এই run থেকে):

Dataset({
    features: ['text', 'label'],
    num_rows: 10709
})
format: None | first: {'text': 'আমার জীবনের সবথেকে ফালতু একটা নাটক এইটা', 'label': 0}
first 200 rows identical to direct call: True
columns: ['label', 'input_ids', 'token_type_ids', 'attention_mask']
filter len<=16: 6425 of 10709
set_format torch: Tensor torch.int64 | label tensor(0)
saved Arrow MB: 1.45 | files: ['data-00000-of-00001.arrow', 'dataset_info.json', 'state.json']
reloaded rows: 10709 | backed by 1 memory-mapped file(s)
iterable: IterableDataset | first ids: [101, 2211, 2960, 2132, 12125, 40520, 2157, 3634]
  • map: remove_columns=["text"] দিয়ে লেখার কলাম ফেলে tokenizer-এর তিনটি কলাম যোগ হলো। প্রথম ২০০টি সারি সরাসরি tokenizer ডাকার সঙ্গে হুবহু মিলেছে। datasets ফলটি একটি cache ফাইলে রাখে; একই function আবার চালালে hash মিলিয়ে আগের ফল আনে। আমরা সময় মাপার জন্য load_from_cache_file=False দিয়েছি।
  • filter: ১৬ বা কম token-এর মন্তব্য ৬,৪২৫টি, প্রায় ৬০%. Batched filter-কে প্রতি সারির জন্য একটি True/False-এর list ফেরত দিতে হয়।
  • set_format: ডেটা কপি না করে সারি পড়ার সময় torch tensor ফেরত দেয়। reset_format() আবার Python list-এ ফেরায়।
  • Arrow আর memory map: ১০,৭০৯টি সারির token ১.৪৫ MB-র একটি Arrow ফাইলে। load_from_disk ফাইলটি memory-map করে খোলে: পুরোটা RAM-এ পড়ে না, যে অংশ ছোঁয়া হয় শুধু সেটুকু ডিস্ক থেকে আসে। RAM-এর চেয়ে বড় dataset এভাবেই চলে।
  • Streaming: IterableDataset এক এক করে সারি দেয়, পুরো ডেটা নামায় না। Hub-এর বড় dataset-এ load_dataset(..., streaming=True) একই ধরনের জিনিস দেয়। এখানে index দিয়ে সরাসরি সারি পড়া, বা আগে থেকে মোট সংখ্যা জানা সহজ নয়; shuffle হয় একটি buffer-এর ভেতরে। এর পার্থক্য map-style dataset-এর সঙ্গে PyTorch-এর দিক থেকে পর্ব ১১ · Data pipeline-এ।

Hub থেকে dataset: load_dataset("org/name", revision=sha) model-এর মতোই commit hash নেয়, আর লোড শেষে একই Arrow-ভিত্তিক Dataset দেয়। নিজের CSV বা JSON ফাইলও load_dataset("csv", data_files=...) দিয়ে একই পথে আসে। ডেটা বদলালে fingerprint বদলায়, তাই পুরনো cache-এর ফল ভুল করে আসে না; তবে function বাইরের কোনো ফাইল পড়লে সেই বদল fingerprint-এ ধরা না পড়তে পারে (ভাবনার প্রশ্ন ০৪ দেখুন)।

৮ · safetensors বনাম pickle, আর trust_remote_code

কেউ আপনাকে একটি সিল করা বাক্স দিল। একটিতে শুধু ওজনের তালিকা আর মাপা চাল। আরেকটিতে একটি চিরকুট: “বাক্স খোলার সময় এই কাজগুলো করো”। আপনি চিরকুটটি পড়ার আগেই কাজগুলো হয়ে যায়। দ্বিতীয় বাক্সটি pickle. Python-এর pickle ফরম্যাট বস্তু ফিরিয়ে আনার জন্য যেকোনো function ডাকার নির্দেশ রাখতে পারে। Python-এর নিজের নথিতে স্পষ্ট সতর্কতা আছে: pickle নিরাপদ নয়, শুধু বিশ্বাসযোগ্য ডেটা unpickle করুন। পুরনো pytorch_model.bin ফাইল pickle-ভিত্তিক।

safetensors প্রথম বাক্স: প্রথম ৮ byte একটি সংখ্যা N (little-endian, unsigned 64-bit), তারপর N byte-এর JSON header (প্রতিটি tensor-এর নাম, dtype, shape আর byte-এর অবস্থান), তারপর কাঁচা সংখ্যা। পড়ার সময় কোনো কোড চলে না। নিচে নিজে একটি ফাইল বানিয়ে পড়ুন। Pickle-এর অংশে একটি নিরীহ print-এর নির্দেশ শুধু disassemble করা হয়েছে, কখনো load করা হয়নি।

import json, struct, pickle, pickletools, io
import numpy as np
W = np.arange(6, dtype=np.float32).reshape(2, 3) / 10
b = np.array([0.5, -1.0], dtype=np.float32)
# --- safetensors ফাইল নিজে বানানো: 8 byte দৈর্ঘ্য + JSON header + কাঁচা byte
header = {"classifier.weight": {"dtype": "F32", "shape": [2, 3], "data_offsets": [0, 24]},
          "classifier.bias":   {"dtype": "F32", "shape": [2], "data_offsets": [24, 32]},
          "__metadata__": {"format": "pt"}}
h = json.dumps(header, separators=(",", ":")).encode()
h += b" " * (-len(h) % 8)                       # ৮-এর গুণিতকে আনা
blob = struct.pack("<Q", len(h)) + h + W.tobytes() + b.tobytes()
print("মোট byte:", len(blob), "| প্রথম 8 byte:", blob[:8].hex(" "))
# --- পড়া: কোনো কোড চলে না, শুধু সংখ্যা আর JSON
n = struct.unpack("<Q", blob[:8])[0]
meta = json.loads(blob[8:8 + n])
data = blob[8 + n:]
for name, t in meta.items():
    if name == "__metadata__": continue
    s, e = t["data_offsets"]
    arr = np.frombuffer(data[s:e], dtype=np.float32).reshape(t["shape"])
    print(f"{name:18} {t['dtype']} {t['shape']} →", [round(float(x), 2) for x in arr.ravel()])
# --- pickle: খোলার সময় একটি function ডাকার নির্দেশ রাখা যায়
class Gift:
    def __reduce__(self):
        return (print, ("pickle.load এই লাইন চালাত!",))
p = pickle.dumps(Gift(), protocol=2)
out = io.StringIO(); pickletools.dis(p, out=out)   # শুধু পড়া, চালানো নয়
ops = [l.split()[2] for l in out.getvalue().splitlines() if len(l.split()) > 2 and l.split()[0].endswith(":")]
print("pickle opcodes:", ops)
print("GLOBAL লাইন:", [l.split(None, 3)[3] for l in out.getvalue().splitlines() if "GLOBAL" in l])
print("REDUCE আছে?", "REDUCE" in ops, "| print একবারও চলেনি, কারণ load করিনি")

আউটপুট (পাতার Python-এ চালানো):

মোট byte: 216 | প্রথম 8 byte: b0 00 00 00 00 00 00 00
classifier.weight  F32 [2, 3] → [0.0, 0.1, 0.2, 0.3, 0.4, 0.5]
classifier.bias    F32 [2] → [0.5, -1.0]
pickle opcodes: ['PROTO', 'GLOBAL', 'BINPUT', 'BINUNICODE', 'BINPUT', 'TUPLE1', 'BINPUT', 'REDUCE', 'BINPUT', 'STOP']
GLOBAL লাইন: ["'__builtin__ print'"]
REDUCE আছে? True | print একবারও চলেনি, কারণ load করিনি

প্রথম ৮ byte b0 00 … = ১৭৬, header-এর দৈর্ঘ্য। মোট ৮ + ১৭৬ + ৩২ = ২১৬ byte; ৬টি আর ২টি float32 মিলে ৮ × ৪ = ৩২ byte. Pickle-এর দিকে GLOBAL '__builtin__ print' একটি function খুঁজে আনে, REDUCE সেটিকে argument দিয়ে ডাকে। এখানে function-টি নিরীহ print, কিন্তু একই জায়গায় যেকোনো function বসানো যায়। তাই অচেনা উৎসের .bin, .pt বা .pkl ফাইল load করবেন না।

সত্যিকারের model.safetensors-এর header-ও একইভাবে পড়া যায়, ৫৪১ MB ফাইলের শুধু প্রথম ১২ KB-র মতো পড়ে (সত্যিকারের run, একটি পুনরাবৃত্ত লাইন বাদ):

header bytes: 12528 | tensors: 104 | metadata: {'format': 'pt'}
  classifier.bias F32 [3]
  classifier.weight F32 [3, 768]
  distilbert.embeddings.LayerNorm.bias F32 [768]
parameters from header: 135326979 | from model: 135326979

Header থেকে গোনা parameter আর model থেকে গোনা হুবহু এক: ১৩,৫৩,২৬,৯৭৯টি। প্রতিটি float32 চার byte, তাই ডেটা ৫৪,১৩,০৭,৯১৬ byte; সঙ্গে ৮ আর ১২,৫২৮ যোগ করলে ৫৪,১৩,২০,৪৫২, যা cache-এর ফাইলের আসল আকারের সঙ্গে byte ধরে মেলে। Model না খুলেই আকার, dtype আর স্তরের নাম জানা যায়।

trust_remote_code: কিছু repo-তে নিজের Python ফাইল থাকে (নতুন architecture-এর জন্য)। trust_remote_code=True দিলে transformers সেই কোড নামিয়ে আপনার মেশিনে চালায়, আপনার সব অনুমতি নিয়ে। ডিফল্ট False, আর তখন এমন repo লোড হয় না। আমাদের তিনটি repo-তে কোনো .py ফাইল ছিল না (“remote code files: []”)। দরকার হলে কোডটি পড়ুন, commit hash পিন করুন (যাতে পরে বদলানো কোড না আসে), আর সম্ভব হলে আলাদা container-এ চালান। একই চোখে দেখুন evaluate.load(): আমাদের run-এ এটি Hub থেকে একটি “builder script” নামিয়েছে। নির্ভরতার পথে আক্রমণের বিস্তারিত সাইবার নিরাপত্তা কোর্সের পর্ব ৪৮-এ।

৯ · Cache, offline mode, evaluate আর accelerate

প্রথম নামানো ফাইল cache-এ থাকে; ঠিকানা ঠিক করে HF_HOME environment variable. Cache-এ ফাইল commit hash অনুযায়ী আলাদা snapshot-এ থাকে। HF_HUB_OFFLINE=1 দিলে library নেটওয়ার্কে যায়ই না, শুধু cache দেখে। পরীক্ষার হলে বা কারখানার বন্ধ নেটওয়ার্কে model চালানোর এটিই নিয়ম। সত্যিকারের run, নেটওয়ার্ক বন্ধ রেখে:

cache: sagorsarker/bangla-bert-base | revisions: ['875aa80'] | MB: 2.2
cache: lxyuan/distilbert-base-multilingual-cased-sentiments-student | revisions: ['cf99110'] | MB: 545.2
cache: Qwen/Qwen2.5-0.5B-Instruct | revisions: ['7ae5576'] | MB: 0.0
offline reload from cache: 0.5 s
offline, not in cache: OSError

Bangla BERT-এর ২.২ MB মানে শুধু tokenizer-এর ফাইল নেমেছে, ওজন নয়: library দরকারি ফাইলটুকুই নামায়। Qwen-এর শুধু কার্ড পড়েছি, তাই প্রায় শূন্য। Cache-এ না থাকা repo offline-এ সঙ্গে সঙ্গে OSError দেয়, নীরবে অপেক্ষা করে না। Cache-এর আকার সময়ের সঙ্গে বাড়ে; hf cache ls বা scan_cache_dir() দিয়ে দেখে hf cache rm দিয়ে পুরনো revision মুছুন।

evaluate: evaluate.load("accuracy") আর "f1" একই ইন্টারফেসে metric দেয়। আমাদের run-এ macro-F1 ০.৬২৩৯, scikit-learn-এর f1_score-এর সঙ্গে হুবহু। একটি metric-এর সংজ্ঞা (macro নাকি micro, positive label কোনটি) নিজে লিখে রাখুন; metric-এর বিস্তারিত পর্ব ৪৫ · NLP মূল্যায়ন-এ। accelerate: একই training কোড এক CPU, এক GPU বা কয়েকটি GPU-তে চালানোর ব্যবস্থা; device_map="auto" দিয়ে বড় model স্তর ভাগ করে কয়েকটি device-এ রাখাও এর কাজ। পরের পর্বগুলোতে Trainer-এর ভেতরে এটি নীরবে চলবে।

আরও গভীরে: Model card-এর ধারণা Mitchell ও সহলেখকদের “Model Cards for Model Reporting” (FAT* ২০১৯) থেকে, আর dataset card-এর পূর্বসূরি Gebru ও সহলেখকদের “Datasheets for Datasets” (arXiv ২০১৮)। Hugging Face-এর ২০২৩ সালের একটি blog জানায়, Trail of Bits safetensors-এর নিরাপত্তা নিরীক্ষা করেছে, আর এরপর এটিকে ডিফল্ট করার দিকে যাওয়া হয়। PyTorch 2.6 থেকে torch.load-এর ডিফল্ট weights_only=True, যা pickle-এর ঝুঁকি কমায়, তবে পুরোপুরি সরায় না।

পরীক্ষা ও ইন্টারভিউতে:

  • ১০,৭০৯ সারি, batch_size 256-এ batched map কতবার ডাকে? ⌈১০,৭০৯ ÷ ২৫৬⌉ = ৪২ বার।
  • safetensors-এর গঠন? ৮ byte header-দৈর্ঘ্য, JSON header (dtype, shape, offset), তারপর কাঁচা byte; load-এ কোড চলে না।
  • Reproducible load-এর জন্য কী পিন করবেন? Commit hash (revision=) প্রতিটি ডাকে, সঙ্গে library-র version.
  • pipeline() আর Auto class-এর পার্থক্য? একই ধাপ; pipeline নিজে বেছে জোড়ে, Auto class-এ প্রতিটি ধাপ আপনার হাতে (padding, batch, device, postprocess).
  • trust_remote_code=True-এর ঝুঁকি? Repo-র Python কোড আপনার অনুমতিতে চলে; কোড পড়ে আর hash পিন করে তবেই।

১০ · AI-এর যুগে — Hugging Face কোড যাচাই

Hugging Face-এর কোড AI assistant খুব দ্রুত লেখে, কারণ অনলাইনে উদাহরণ অগণিত। সমস্যা হলো, সেই উদাহরণগুলো নানা version-এর।

AI প্রায়ই যা করেকেন ভুলকীভাবে ধরবেন
pipeline("sentiment-analysis"), model ছাড়াডিফল্ট model ইংরেজির, version-ভেদে বদলায়Model আর revision নাম ধরে দেওয়া
“use_fast=False দিলে slow tokenizer”আমাদের version-এ তা হয়নিtype(tok) আর is_fast print
trust_remote_code=True সব জায়গায়অন্যের কোড চালানো, প্রায়ই অদরকারিRepo-তে .py আছে কি না দেখা
torch.load দিয়ে অচেনা .binPickle কোড চালাতে পারেsafetensors নেওয়া
লাইসেন্স বা ভাষা নিয়ে আন্দাজকার্ডে না থাকলেও “চলবে” বলেকার্ড আর LICENSE নিজে পড়া
Pipeline-এর score-কে নিশ্চয়তা বলাCalibration মাপা হয়নিনিজের label-ওয়ালা ডেটায় মাপা

কোড চাওয়ার সময় যাচাইও চেয়ে নিন:

আমি transformers আর datasets দিয়ে (version লিখে দিচ্ছি) একটি বাংলা text classification-এর ডেটা তৈরি করতে চাই। ১. Model, tokenizer আর config তিনটিতেই revision হিসেবে একটি commit hash দাও; hash কোথা থেকে পাব তা বলো। ২. Model card-এর লাইসেন্স, ভাষা আর training data কোন লাইনে দেখব, তার তালিকা দাও; কার্ডে না থাকলে "জানা নেই" লিখো। ৩. Dataset.map batched=True দিয়ে লিখো, আর ফল যে সরাসরি tokenizer ডাকার সঙ্গে মেলে তার test দাও। ৪. trust_remote_code ব্যবহার করবে না; দরকার মনে হলে কারণ বলো। ৫. শুধু safetensors ফাইল লোড হচ্ছে কি না যাচাইয়ের উপায় বলো। ৬. যে argument নিয়ে নিশ্চিত নও, তা অনুমান বলে চিহ্নিত করো; কোনো সংখ্যা বা ফল বানিয়ে লিখো না।

AI-এর লেখা Hugging Face কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১Model ফাইল তো শুধু সংখ্যা। তাহলে একটি model ডাউনলোড করে খোলা কীভাবে নিরাপত্তার ঝুঁকি হয়?

ওজন সংখ্যা, কিন্তু ফাইল ফরম্যাট সংখ্যা নয়। PyTorch দীর্ঘদিন torch.save দিয়ে Python-এর pickle ফরম্যাটে ফাইল লিখেছে। Pickle-এর নকশাই এমন যে বস্তু ফিরিয়ে আনতে এটি যেকোনো importable function ডাকতে পারে; উপরের পরীক্ষায় GLOBAL আর REDUCE opcode সেই কাজ করে। Python-এর নথিতে বহু বছর ধরে লেখা আছে, অবিশ্বস্ত ডেটা unpickle করবেন না।

ঝুঁকিটি তাত্ত্বিক থাকেনি। ২০২৪ সালের ফেব্রুয়ারি-মার্চে JFrog-এর নিরাপত্তা গবেষকেরা জানান, তাঁরা Hugging Face-এ প্রায় ১০০টি ক্ষতিকর model পেয়েছেন, বেশির ভাগ PyTorch-এর; প্রতিবেদন অনুযায়ী অন্তত একটি load করলেই আক্রমণকারীর কাছে একটি shell খুলে দিতে পারত। কিছু নমুনা গবেষণার পরীক্ষাও হতে পারে, সংখ্যাটি তাঁদের নিজের স্ক্যানের ফল।

প্রতিরক্ষা কয়েক স্তরে এসেছে। এক, safetensors: ফরম্যাটে কোড রাখার জায়গাই নেই, শুধু JSON header আর কাঁচা byte. Hugging Face ২০২৩ সালে জানায়, Trail of Bits একটি বাইরের নিরাপত্তা নিরীক্ষা করেছে। দুই, PyTorch 2.6 থেকে torch.load ডিফল্টে weights_only=True, যা শুধু tensor আর কিছু নিরাপদ ধরন খোলে। তিন, Hub নিজে pickle ফাইল স্ক্যান করে সতর্কতা দেখায়। তবে কোনো স্ক্যানার সব কৌশল ধরে না।

সবচেয়ে বড় ফাঁক থেকে যায় কোডে, ওজনে নয়: trust_remote_code=True সরাসরি Python চালায়, safetensors সেখানে কিছু করতে পারে না। তাই নিয়ম: শুধু safetensors নিন, অপরিচিত repo-র কোড পড়ুন, hash পিন করুন, আর সম্ভব হলে আলাদা পরিবেশে চালান।

মূল উপলব্ধি: নিরাপত্তা নির্ভর করে ফাইল কী করতে পারে তার ওপর, কী রাখে তার ওপর নয়। যে ফরম্যাট কোড চালাতে পারে, তাকে কোডের মতোই সন্দেহ করতে হয়।

প্র ০২Model card কেন চালু হলো, আর তার সীমা কী? আমাদের তিনটি কার্ডে যা পেলাম তা কী বলে?

২০১৯ সালে FAT* সম্মেলনে Margaret Mitchell ও সহলেখকেরা “Model Cards for Model Reporting” প্রবন্ধে প্রস্তাব দেন, প্রতিটি প্রকাশিত model-এর সঙ্গে একটি ছোট নথি থাকবে: কী উদ্দেশ্যে বানানো, কোন ডেটায় মাপা, বিভিন্ন জনগোষ্ঠীতে ফল কেমন, আর কোথায় ব্যবহার করা উচিত নয়। এর আগে Timnit Gebru ও সহলেখকদের “Datasheets for Datasets” (arXiv ২০১৮) ডেটার জন্য একই কথা বলেছিল, বৈদ্যুতিক যন্ত্রাংশের datasheet-এর উদাহরণ টেনে। Hugging Face Hub এই ধারণা গ্রহণ করে README-কেই কার্ড বানায়।

সীমাটি আমাদের তিনটি কার্ডেই দেখা গেল। কার্ড লেখা স্বেচ্ছামূলক, আর তার গুণমান লেখকের সময় আর আগ্রহের ওপর নির্ভর করে। Sentiment model-এর কার্ডটি Trainer-এর স্বয়ংক্রিয় খসড়া, শুরুতেই লেখা “proofread and complete it”. Bangla BERT-এর “Limitations and Biases” শিরোনামের নিচে কিছু নেই। Qwen-এর কার্ডে training data বা সীমাবদ্ধতার আলাদা অংশ নেই, মূল্যায়নের জন্য blog-এ পাঠায়। এটি কোনো একজন লেখকের দোষ নয়। Liang ও সহলেখকেরা ২০২৪ সালে (Nature Machine Intelligence) Hub-এর ৩২,১১১টি model card বিশ্লেষণ করে জানান, পরিবেশের প্রভাব, সীমাবদ্ধতা আর মূল্যায়নের অংশই সবচেয়ে কম পূরণ করা হয়।

আরেকটি সীমা: কার্ডের দাবি যাচাই করা হয় না। “state of the art” বা কোনো benchmark সংখ্যা কার্ডে থাকলে তা লেখকের নিজের মাপা, নির্দিষ্ট ডেটা আর সময়ের। আপনার কাজের ডেটা আলাদা। আমাদের run-এই দেখা গেল, কার্ডে বাংলা না থাকা model বাংলা মন্তব্যে majority baseline-এর নিচে পড়ল।

তবু কার্ডের মূল্য বিশাল। কী লেখা নেই, সেটিও তথ্য: ভাষার তালিকায় বাংলা না থাকা আমাদের আগেই সতর্ক করতে পারত। নিজে model প্রকাশ করলে উল্টো দিক থেকে ভাবুন: যা আপনি জানেন না, সেটিও লিখুন।

মূল উপলব্ধি: Model card দাবির তালিকা, প্রমাণ নয়। পড়ুন কী আছে আর কী নেই, তারপর নিজের ডেটায় মেপে সিদ্ধান্ত নিন।

প্র ০৩pipeline() এত সুবিধাজনক হলে production-এ সরাসরি ব্যবহার না করে Auto class-এ নামার দরকার কেন?

pipeline() একটি নকশার সিদ্ধান্ত: সবচেয়ে সাধারণ পথটি এক লাইনে। শেখার জন্য, দ্রুত পরীক্ষার জন্য আর ছোট demo-র জন্য এটি দারুণ। আমাদের run-এ এর ফল হাতের পথের সঙ্গে হুবহু মিলেছে, তাই “pipeline ভুল” এমন নয়।

সমস্যা লুকানো সিদ্ধান্তে। এক, model না দিলে ডিফল্ট model; সেটি library-র version-এর সঙ্গে বদলাতে পারে, আর সাধারণত ইংরেজির জন্য। দুই, batch আর padding: আমাদের CPU run-এ batch ১৬ দিলে সময় দ্বিগুণ হলো। হাতে লিখলে আপনি দৈর্ঘ্য অনুযায়ী সাজিয়ে batch বানাতে পারেন, যাতে padding কমে। তিন, truncation আর সর্বোচ্চ দৈর্ঘ্য: লম্বা রিভিউ কোথায় কাটা যাচ্ছে, তা স্পষ্ট দেখা দরকার। চার, postprocess: softmax-এর পর argmax ছাড়াও আপনি হয়তো threshold চান, বা “অনিশ্চিত” বলে মানুষের কাছে পাঠাতে চান।

Production-এ আরও কয়েকটি জিনিস লাগে: logging, timeout, একই input-এ একই output-এর test, আর model বদলালে regression test. এগুলো pipeline-এর চারপাশেও লেখা যায়, তবে ভেতরের ধাপ জানা না থাকলে ভুল কোথায় হচ্ছে বোঝা কঠিন। Serving-এর কাঠামো মেশিন লার্নিং কোর্সের পর্ব ৬৮-এ আছে।

একটি মাঝামাঝি পথও আছে: pipeline-এ model, tokenizer আর revision নিজে দিয়ে দেওয়া, আর একটি ছোট test-এ Auto class-এর পথের সঙ্গে মিলিয়ে রাখা। আমরা ঠিক এটাই করেছি। কোনটি ভালো, তা দলের দক্ষতা, ট্রাফিক আর ঝুঁকির ওপর নির্ভর করে; সার্বজনীন উত্তর নেই।

মূল উপলব্ধি: সুবিধা আর নিয়ন্ত্রণ বিপরীত দিকে টানে। pipeline দিয়ে শুরু করুন, কিন্তু প্রতিটি লুকানো সিদ্ধান্ত একবার নিজের হাতে খুলে দেখুন।

প্র ০৪“একই model, একই কোড” — তবু ছয় মাস পর ফল আলাদা। কোথায় কোথায় বদল ঢুকতে পারে?

প্রথম জায়গা Hub. main একটি চলমান নাম। লেখক ওজন, tokenizer, config বা chat template বদলালে from_pretrained(name) নতুনটি আনে। খেলনা Hub-এ দেখেছেন, শুধু label-এর ক্রম বদলালেই উত্তর উল্টে যায়, কোনো error ছাড়া। প্রতিকার commit hash, আর তিনটি ডাকেই।

দ্বিতীয় জায়গা library. এই পর্বের run-এ transformers 5.22.2-এ use_fast=False-এর মানে পুরনো tutorial-এর সঙ্গে মেলেনি। Fast আর legacy tokenizer ১০,৭০৯টি সারির দুটিতে আলাদা id দিয়েছে। এ ধরনের বদল release note-এ থাকতে পারে, কিন্তু সবাই পড়ে না। প্রতিকার: uv.lock বা সমমানের lock ফাইল, আর version print করে log-এ রাখা।

তৃতীয় জায়গা ডেটা। Hub-এর dataset-ও repo, তারও revision আছে। Streaming dataset-এ সারির ক্রম বা উৎস বদলাতে পারে। আর map-এর cache: function বদলালেও অনেক সময় hash বদলায় বলে নতুন হিসাব হয়, কিন্তু function যে বাইরের জিনিসের ওপর নির্ভর করে (একটি global variable, একটি ফাইল), তার বদল hash-এ ধরা না পড়তে পারে। সন্দেহ হলে cache বন্ধ রেখে আবার চালান।

চতুর্থ জায়গা হার্ডওয়্যার আর সংখ্যা। CPU আর GPU-তে, বা আলাদা thread সংখ্যায়, float-এর যোগের ক্রম বদলায়; তাই শেষ দশমিক ঘরে পার্থক্য আসে। আমাদের দুই পথের ৬ × ১০−৮ পার্থক্য এই মাত্রার। সাধারণত label বদলায় না, তবে দুটি শ্রেণির score খুব কাছে হলে বদলাতে পারে। Reproducibility-র পুরো চেকলিস্ট পর্ব ১৯-এ।

মূল উপলব্ধি: পুনরুৎপাদন মানে চারটি জিনিস লিখে রাখা: model-এর hash, ডেটার hash, library-র version আর হার্ডওয়্যার। যেটি লেখা নেই, সেটিই একদিন বদলাবে।

প্র ০৫“Open model” মানেই কি যেকোনো কাজে ব্যবহার করা যায়? লাইসেন্স পড়ার সময় কী কী খেয়াল রাখবেন?

না। “Open” শব্দটি ব্যবহারে অনেক রকম। কিছু model Apache 2.0 বা MIT-এর মতো প্রচলিত open source লাইসেন্সে আসে: বাণিজ্যিক ব্যবহার, পরিবর্তন আর পুনর্বিতরণের অনুমতি, সঙ্গে লাইসেন্সের লেখা আর কপিরাইট নোটিস রাখার মতো শর্ত। আমাদের তিনটি model এই ধরনের। Apache 2.0-তে পেটেন্টের একটি ধারাও আছে, যা MIT-এ নেই।

আবার কিছু model “open weights” কিন্তু লাইসেন্স নিজস্ব: ব্যবহারের নীতিমালা, ব্যবহারকারীর সংখ্যার সীমা, বা নির্দিষ্ট কাজ নিষিদ্ধ। কিছু শুধু গবেষণা বা অবাণিজ্যিক ব্যবহারের জন্য; পর্ব ৪৮-এ দেখেছেন একটি বাংলা model-এর README-তে এমন শর্ত ছিল, তাই আমরা সেটি চালাইনি। আর gated model-এ নামানোর আগেই শর্তে রাজি হতে হয়। ঠিক কোন শর্ত কোথায়, তা প্রতিটি model-এর নিজের লাইসেন্সের লেখায় পড়তে হয়; নামের ওপর ভরসা চলে না।

তিনটি জায়গায় ভুল বেশি হয়। এক, YAML-এর license: আর repo-র LICENSE ফাইল বা README-র লেখা না মেলা; তখন বেশি কড়া শর্তটি ধরে নেওয়াই নিরাপদ। দুই, model-এর লাইসেন্স আর ডেটার লাইসেন্স আলাদা: Sazzed-এর ডেটা CC BY 4.0, তাই এর ওপর শেখানো model প্রকাশ করলে উৎসের স্বীকৃতি দিতে হবে। তিন, fine-tune করা model সাধারণত মূল model-এর লাইসেন্সের শর্ত বহন করে।

বাংলাদেশে কোনো প্রতিষ্ঠান model নিয়ে পণ্য বানালে আইনি পরামর্শ নেওয়াই সঠিক পথ; এই পর্ব আইনি পরামর্শ নয়। প্রকৌশলীর দায়িত্ব হলো লাইসেন্সের নাম, লিংক আর commit hash প্রকল্পের নথিতে লিখে রাখা, যাতে পরে কেউ প্রশ্ন করলে উত্তর থাকে।

মূল উপলব্ধি: লাইসেন্স model-এর অংশ, ওজনের মতোই। কোডে যেমন version পিন করেন, নথিতে তেমনি লাইসেন্স আর তার শর্ত লিখে রাখুন।

অনুশীলন

খাতা-কলমে করুন, তারপর উত্তর মেলান। পরীক্ষা আর ইন্টারভিউতে এই ধরনের হিসাব কাজে লাগে।

  1. safetensors-এর হিসাব: একটি safetensors ফাইলের header ১২,৫২৮ byte, আর ভেতরে মোট ১৩,৫৩,২৬,৯৭৯টি float32 সংখ্যা। (ক) ফাইলের মোট আকার byte-এ কত? (খ) একই ওজন bf16-এ রাখলে কত হতো (header একই ধরে)? (গ) ফাইলের প্রথম ৮ byte hex-এ কী হবে?

    (ক) ডেটা: ১৩,৫৩,২৬,৯৭৯ × ৪ = ৫৪,১৩,০৭,৯১৬ byte. মোট = ৮ + ১২,৫২৮ + ৫৪,১৩,০৭,৯১৬ = ৫৪,১৩,২০,৪৫২ byte, প্রায় ৫৪১.৩ MB. (খ) bf16-এ প্রতি সংখ্যা ২ byte: ২৭,০৬,৫৩,৯৫৮ + ১২,৫৩৬ = ২৭,০৬,৬৬,৪৯৪ byte, প্রায় অর্ধেক। (গ) ১২,৫২৮ = 0x30F0; little-endian-এ ছোট byte আগে: f0 30 00 00 00 00 00 00.

  2. আউটপুট অনুমান করুন: কোড না চালিয়ে তিনটি লাইন লিখুন।
    rows = list(range(2500))
    calls = []
    def f(batch):
        calls.append(len(batch))
        return [x * 2 for x in batch]
    out = [y for s in range(0, len(rows), 1000) for y in f(rows[s:s+1000])]
    print(len(calls), calls)
    print(out[:3], out[-1])
    print(sum(1 for x in out if x % 4 == 0))
    3 [1000, 1000, 500]
    [0, 2, 4] 4998
    1250

    ২,৫০০টি সারি ১,০০০-এর batch-এ তিনটি ডাক, শেষটি ৫০০। প্রতিটি মান দ্বিগুণ, শেষটি ২,৪৯৯ × ২ = ৪,৯৯৮। ২x চার দিয়ে বিভাজ্য হয় যখন x জোড়: ০ থেকে ২,৪৯৯-এ ১,২৫০টি। Dataset.map(batched=True)-এর ডিফল্ট batch_size-ও ১,০০০।

  3. নকশা সমালোচনা করুন: বাজারঘরের একটি script: clf = pipeline("sentiment-analysis", trust_remote_code=True); রিভিউগুলো clf(reviews, batch_size=64); ফলের score ০.৯-এর বেশি হলে রিভিউটি স্বয়ংক্রিয়ভাবে “positive” বলে ওয়েবসাইটে দেখায়; আর পুরনো একটি fine-tuned model torch.load("best.bin") দিয়ে খোলে, যা একজন অচেনা ব্যক্তির শেয়ার করা। অন্তত পাঁচটি সমস্যা আর সমাধান বলুন।
    • Model নেই: ডিফল্ট model, সম্ভবত ইংরেজির। সমাধান: বাংলায় মাপা model, নাম আর commit hash-সহ।
    • trust_remote_code অকারণে: এই কাজে দরকার নেই। সমাধান: বাদ; দরকার হলে কোড পড়ে hash পিন।
    • batch_size 64 না মেপে: CPU-তে padding-এ ধীর হতে পারে। সমাধান: নিজের হার্ডওয়্যারে মাপা, বা দৈর্ঘ্য অনুযায়ী সাজানো।
    • ০.৯-এর score-কে নিশ্চয়তা ধরা: Calibration মাপা হয়নি, আর ভুল label সরাসরি গ্রাহক দেখবেন। সমাধান: label-ওয়ালা ডেটায় মাপা, অনিশ্চিত হলে মানুষের কাছে।
    • অচেনা .bin-এ torch.load: Pickle কোড চালাতে পারে। সমাধান: বিশ্বস্ত উৎস থেকে safetensors; না পেলে weights_only=True আর আলাদা পরিবেশ।
  4. কোড লিখুন: শুধু Python-এর json আর struct দিয়ে একটি function peek(path) লিখুন, যা একটি safetensors ফাইলের প্রতিটি tensor-এর নাম, dtype আর shape ফেরত দেয়, আর পুরো ফাইল পড়ে না।
    import json, struct
    def peek(path):
        with open(path, "rb") as f:
            n = struct.unpack("<Q", f.read(8))[0]   # header-এর দৈর্ঘ্য
            head = json.loads(f.read(n))             # শুধু header পড়া
        return {k: (v["dtype"], v["shape"]) for k, v in head.items()
                if k != "__metadata__"}

    মূল কথা দুটি: প্রথম ৮ byte little-endian unsigned 64-bit (<Q), আর __metadata__ চাবিটি tensor নয়। বাস্তবে একটি সীমা যোগ করুন: n অস্বাভাবিক বড় হলে (ধরুন ১০০ MB-এর বেশি) পড়বেন না, কারণ নষ্ট বা ক্ষতিকর ফাইলের header বিশাল দাবি করতে পারে।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমHugging Face-এর একটি text-classification pipeline একটি বাক্যে যে ক্রমে কাজ করে, সাজান (▲▼ চেপে সরান)

  1. Model-এর নাম আর revision থেকে config পড়া
  2. একই revision থেকে tokenizer আর ওজন লোড করা
  3. বাক্যকে input_ids আর attention_mask-এ ভাঙা
  4. Gradient বন্ধ রেখে model থেকে logits নেওয়া
  5. Softmax করে id2label দিয়ে label আর score বানানো

একটি গবেষণার কোডে লেখা AutoModel.from_pretrained("org/model"). ছয় মাস পর একই কোডে ফল আলাদা আসছে, কোনো error নেই। সবচেয়ে সম্ভাব্য প্রথম কারণ আর প্রতিকার কোনটি?

  • Random seed বদলেছে; seed ঠিক করা
  • Cache নষ্ট হয়েছে; cache মুছে ফেলা
  • main branch নতুন commit-এ সরে গেছে; revision-এ commit hash পিন করা
  • AutoModel প্রতিবার আলাদা class বাছে; সরাসরি class নাম লেখা

১০,৭০৯টি সারির একটি Dataset-এ map(fn, batched=True) ডিফল্ট batch আকারে (১,০০০) চালানো হলো। fn কতবার ডাকা হবে?

  • ১০,৭০৯
  • ১১
  • ১০
  • ১

safetensors ফাইল load করা pickle-ভিত্তিক .bin ফাইলের চেয়ে নিরাপদ কেন?

  • ফাইলটি এনক্রিপ্ট করা থাকে
  • Hub প্রতিটি safetensors ফাইল যাচাই করে দেয়
  • safetensors ফাইল ছোট, তাই ক্ষতিকর কিছু ধরে না
  • ফরম্যাটে শুধু header আর কাঁচা সংখ্যা থাকে, কোড চালানোর উপায় নেই

একটি multilingual sentiment model বাংলা মন্তব্যে ০.৬৮ মিল দিল, যেখানে ডেটার ৭০.৩% positive. সবচেয়ে সঠিক সিদ্ধান্ত কোনটি?

  • Model এই ডেটায় majority baseline-এর চেয়েও খারাপ; বাংলায় মাপা বা fine-tune করা model লাগবে
  • ০.৬৮ মানে model অর্ধেকের বেশি ঠিক করছে, তাই ব্যবহারযোগ্য
  • Pipeline-এর বদলে Auto class ব্যবহার করলে মিল বাড়বে
  • batch_size বাড়ালে মিল বাড়বে

trust_remote_code=True দিয়ে একটি model লোড করলে আসলে কী ঘটে?

  • Library শুধু cache থেকে লোড করে, নেটওয়ার্কে যায় না
  • Hub যাচাই করা নিরাপদ কোড চালানো হয়
  • Repo-র Python ফাইল নামিয়ে আপনার মেশিনে চালানো হয়
  • ওজন pickle থেকে safetensors-এ রূপান্তর করা হয়

CPU-তে একই ৪০০টি বিভিন্ন দৈর্ঘ্যের মন্তব্যে pipeline-এর batch_size 1-এ ৬.৯ সেকেন্ড আর 16-এ ১৪.০ সেকেন্ড লাগল। সবচেয়ে সম্ভাব্য কারণ কোনটি?

  • Batch বড় হলে model বেশি ভুল করে, আবার হিসাব করে
  • Padding: ছোট বাক্যও batch-এর সবচেয়ে লম্বাটির মাপে হিসাব হয়
  • Batch দিলে tokenizer প্রতিটি বাক্য দুবার ভাঙে
  • প্রতি batch-এ model আবার লোড হয়

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Model card আগে: লাইসেন্স, ভাষা, training data, মূল্যায়ন আর সীমা; কী লেখা নেই সেটিও তথ্য।
  2. Commit hash পিন করুন config, tokenizer আর model তিনটিতেই; main সরে যায়।
  3. pipeline() = config → tokenizer → model → preprocess → forward → postprocess; Auto class দিয়ে একই ফল, নিয়ন্ত্রণ আপনার হাতে।
  4. batched map ডাক কমায়; ফল Arrow-এ, memory-map হয়ে খোলে। Tokenizer বদলালে কয়েকটি সারিতে তুলনা করুন।
  5. safetensors নিন, pickle এড়ান, trust_remote_code-এ সাবধান; offline-এর জন্য HF_HUB_OFFLINE=1.

পরের পর্বে: পর্ব ৫৪-এ Transformer fine-tuning: এই পর্বের Dataset আর Auto class নিয়ে Trainer দিয়ে বাংলা classification আর NER শেখানো।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান