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

পুনরুৎপাদনযোগ্য প্রকল্প — folder, lockfile, data version আর Git

Reproducible AI Projects — Folder Layout, Lockfiles, Data Versioning and Git

Taught in Bangla, this lesson shows what makes an ML result reproducible: the same code, data, environment and seed. In a real small project it uses a standard folder layout, uv.lock, SHA-256 data fingerprints, DVC, a data-science .gitignore and a JSON Lines experiment log, then gets identical metrics from a fresh clone and shows how an unseeded split breaks it.

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

  • AI-এ reproducibility মানে ঠিক কী, আর গবেষণায় একে নিয়ে কেন এত আলোচনা
  • একটি AI প্রকল্পের ফোল্ডার কীভাবে সাজাবেন, আর মূল ডেটা কেন কখনো বদলাবেন না
  • uv-এর lockfile দিয়ে environment বেঁধে রাখা আর অন্য কম্পিউটারে হুবহু ফিরিয়ে আনা
  • Hash দিয়ে ডেটার version চেনা, DVC-র কাজ, আর ডেটা-বিজ্ঞানের জন্য .gitignore
  • প্রতিটি experiment লিখে রাখা, তুলনা করা, আর ফল কেন হঠাৎ বদলায় তা ধরতে পারা

১ · পুনরুৎপাদনযোগ্যতা কী — একই রেসিপি, একই স্বাদ

“বাজারঘর” নামের কাল্পনিক অনলাইন দোকানের ডেটা টিমে রহিম একটি model বানালেন। Model বলে দেয় কোন অর্ডার দেরিতে পৌঁছাবে। রহিমের ল্যাপটপে accuracy এলো ০.৮৩। এক মাস পর ম্যানেজার জানতে চাইলেন, “সংখ্যাটা আবার দেখাতে পারবেন?” রহিম চালালেন, এলো ০.৮৭। সহকর্মী করিমের কম্পিউটারে একই notebook চালাতে গিয়ে আগে একটি error, package বদলানোর পর ০.৮৪। কোনটি সত্যি? কেউ জানেন না। ম্যানেজারের আস্থাও আর আগের মতো নেই।

সমস্যাটি model-এর নয়, প্রকল্পের। আর এর সমাধানই এই পর্বের বিষয়।

মায়ের হাতের বিরিয়ানি ভাবুন। একই রেসিপি লিখে দিলেও আপনার রান্না একই স্বাদের হয় না। কারণ কোন চাল, কতটা পুরোনো ঘি, কোন চুলা, কতক্ষণ দম — এগুলো খাতায় লেখা ছিল না। AI প্রকল্পেও তাই। code হলো রেসিপি, ডেটা হলো চাল-মাংস, environment (Python আর package-এর version) হলো চুলা আর হাঁড়ি, আর seed হলো সেই “আন্দাজমতো লবণ” — যেটি না মাপলে প্রতিবার একটু আলাদা হয়।

একই code + একই ডেটা + একই environment + একই seed
= একই result

এই চারটির প্রতিটির জন্য আলাদা একটি হাতিয়ার আছে, আর এই পর্ব সেগুলো একে একে দেখাবে:

উপাদানকী দিয়ে বাঁধবেনকীভাবে যাচাই করবেন
CodeGit-এর commitcommit hash, যেমন b25370c
ডেটাডেটা ফাইলের hash, ছোট হলে Git, বড় হলে DVCSHA-256 মিলছে কি না
Environmentlockfile (uv.lock) আর .python-versionuv sync --locked সফল কি না
এলোমেলোতাপ্রতিটি random কাজে seedদুবার চালালে একই সংখ্যা কি না

শব্দগুলো নিয়ে একটু সতর্ক থাকা ভালো, কারণ গবেষকেরা সবাই এক অর্থে ব্যবহার করেন না। এই কোর্সে reproducibility মানে একই code, একই ডেটা দিয়ে একই ফল পাওয়া — অন্য মানুষ, অন্য কম্পিউটারেও। Replicability মানে নতুন ডেটা বা নতুন করে লেখা code দিয়েও একই সিদ্ধান্তে পৌঁছানো; এটি আরও কঠিন পরীক্ষা। আর নিজের কম্পিউটারে নিজে আবার চালিয়ে একই ফল পাওয়াকে অনেকে বলেন repeatability. কিছু প্রতিষ্ঠান (যেমন ACM-এর একটি নির্দেশিকা) শব্দ দুটি প্রায় উল্টো অর্থে ব্যবহার করেছে; তাই কোনো লেখা পড়ার সময় সংজ্ঞাটি দেখে নেবেন।

“Reproducibility crisis” নিয়ে আলোচনা। বিজ্ঞানের নানা শাখায় প্রকাশিত ফল আবার করে না পাওয়ার সমস্যা নিয়ে ২০১০-এর দশক জুড়ে অনেক লেখা হয়েছে; ২০১৬ সালে Nature পত্রিকার একটি জরিপে বহু গবেষক জানান যে তাঁরা অন্যের experiment পুনরায় করতে ব্যর্থ হয়েছেন। মেশিন লার্নিংয়েও একই প্রশ্ন ওঠে। ২০১৮ সালে Gundersen ও Kjensmo AI গবেষণাপত্রগুলো পরীক্ষা করে দেখান যে অনেকটিতেই code, ডেটা বা পদ্ধতির যথেষ্ট বিবরণ নেই। একই বছরে Henderson ও সহকর্মীরা (Deep Reinforcement Learning that Matters) দেখান, শুধু random seed বদলালেই reinforcement learning-এর ফল এত বদলায় যে দুটি পদ্ধতির তুলনা উল্টে যেতে পারে। এর পর NeurIPS সম্মেলন ২০১৯ সাল থেকে একটি reproducibility checklist চালু করে (Joelle Pineau ও সহকর্মীদের উদ্যোগ বলে পরিচিত)। ২০২৩ সালে Kapoor ও Narayanan বিভিন্ন বিজ্ঞান-শাখার ML-ভিত্তিক গবেষণায় data leakage-কে ফল পুনরুৎপাদন না হওয়ার একটি বড় কারণ হিসেবে তুলে ধরেন। “সংকট” কতটা গভীর, তা নিয়ে মতভেদ আছে; কিন্তু এটুকু নিয়ে প্রায় সবাই একমত যে code, ডেটা আর environment লিখে না রাখলে ফলের ওপর ভরসা রাখা কঠিন।

এই পর্বের সব কাজ একটি সত্যিকারের ছোট প্রকল্পে করা হয়েছে, নাম bazarghar-ai. লেখার সময়ে ব্যবহার হয়েছে Python 3.13.5, uv 0.11.28, scikit-learn 1.9.1, pandas 3.1.0, NumPy 2.5.4, Git 2.53 আর DVC 3.67.1. এগুলো সময়ের সঙ্গে বদলাবে, তাই আপনার সংখ্যা মিলিয়ে দেখবেন। পাতার ভেতরে চালানো যায় এমন কোডগুলো চলে Pyodide-এ (সেখানে scikit-learn 1.8.0), তাই দুই জায়গার version আলাদা। এটিও reproducibility-র একটি শিক্ষা, যা পরে দেখব।

২ · প্রকল্পের ফোল্ডার — প্রতিটি জিনিসের নির্দিষ্ট তাক

পাড়ার মুদি দোকানে চাল এক তাকে, ডাল আরেক তাকে, আর হিসাবের খাতা ক্যাশবাক্সের পাশে। নতুন কর্মচারী প্রথম দিনেই জিনিস খুঁজে পান। AI প্রকল্পেও এমন নির্দিষ্ট তাক দরকার। প্রচলিত একটি কাঠামো নিচে, আর পাশে লেখা আছে কোনটি Git-এ যায় আর কোনটি যায় না।

একটি AI প্রকল্পের ফোল্ডার

Project layout: what goes into Git, what is rebuilt, what lives outside

AI প্রকল্পের ফোল্ডার কাঠামো bazarghar-ai/ data/raw/ মূল ডেটা, শুধু পড়া · ছোট: Git, বড়: DVC data/processed/ code দিয়ে আবার তৈরি · Git-এ নয় notebooks/ খোঁজাখুঁজি আর চিত্র · Git-এ src/ আসল pipeline: make_data, train · Git-এ models/ train থেকে তৈরি · Git-এ নয়, বড় হলে DVC reports/ metrics.json · Git-এ pyproject.toml + uv.lock environment-এর পূর্ণ বিবরণ · Git-এ .gitignore · README · params নিয়ম, পরিচিতি, সেটিং · Git-এ
ছবি ১২.১ — হলুদ: মূল ডেটা, যা কেউ বদলায় না। নীল: যা code চালালেই আবার তৈরি হয়, তাই Git-এ রাখার দরকার নেই। সবুজ ও বেগুনি: যা Git-এ থাকে। ডেটা চলে এক দিকে — raw থেকে processed, তারপর model, তারপর report.

এই কাঠামোর পেছনে তিনটি সহজ নিয়ম আছে:

  1. মূল ডেটা কখনো বদলাবেন না। Raw data হলো আদালতের প্রমাণের মতো — একবার জমা পড়লে কেউ তাতে কলম ছোঁয়ায় না। Excel-এ খুলে একটি ভুল মান হাতে ঠিক করলে কেউ জানবে না কী বদলাল, কেন বদলাল। তার বদলে পরিষ্কার করার কাজ একটি script-এ লিখুন, যা data/raw পড়ে data/processed-এ নতুন ফাইল লেখে। ভুল হলে script সংশোধন করে আবার চালালেই হলো।
  2. যা আবার তৈরি করা যায়, তা Git-এ রাখার দরকার নেই। Processed ডেটা আর trained model দুটোই code চালালে ফিরে আসে। Git-এ রাখতে হয় যা দিয়ে এগুলো তৈরি হয় — code, ছোট মূল ডেটা আর lockfile.
  3. Notebook খোঁজার জন্য, src চালানোর জন্য। Notebook-এ ডেটা দেখা, চিত্র আঁকা, ধারণা যাচাই। কিন্তু যে ধাপগুলো বারবার চলবে (ডেটা তৈরি, train, মূল্যায়ন), সেগুলো src/-এর .py ফাইলে রাখুন, যাতে একটি কমান্ডেই উপর থেকে নিচ পর্যন্ত চলে। Notebook-এর cell এলোমেলো ক্রমে চালালে কী সমস্যা হয়, তা পর্ব ৮ (Jupyter notebook — cell, kernel, magic আর ভালো অভ্যাস)-এর বিষয়।

Project template: প্রতিবার হাতে ফোল্ডার বানানোর বদলে অনেকে একটি তৈরি template ব্যবহার করেন। বহুল পরিচিত একটি হলো Cookiecutter Data Science, যা প্রায় এই কাঠামোই (আরও কিছু ফোল্ডারসহ) এক কমান্ডে বানিয়ে দেয়; লেখার সময়ে এর একটি নতুন প্রধান version চালু আছে আর ফোল্ডারের নাম কিছুটা বদলেছে। Template ব্যবহার বাধ্যতামূলক নয়; আসল কথা হলো দলের সবাই যেন একই কাঠামো মানেন। ছোট প্রকল্পে উপরের ছয়টি ফোল্ডারই যথেষ্ট।

একটি প্রকল্পের ফাইল-তালিকা দেখে এই নিয়মগুলো মানা হয়েছে কি না, তা ছোট একটি Python ফাংশনই যাচাই করতে পারে। নিচের কোডটি একটি শেখানোর মডেল: আসল প্রকল্পে তালিকাটি আসত git ls-files থেকে, এখানে হাতে দেওয়া। প্রথমবার “চালান” চাপলে Python লোড হতে কয়েক সেকেন্ড লাগবে।

REQUIRED = ["data/raw/", "data/processed/", "notebooks/", "src/", "models/", "reports/"]
MUST_HAVE = ["pyproject.toml", "uv.lock", ".gitignore", "README.md"]
SECRET_HINTS = (".env", ".key", "secret", "password")
MAX_GIT_MB = 50                         # a team rule, not a Git limit

# (path, size in MB, tracked by Git?)
files = [
    ("pyproject.toml", 0.01, True), ("uv.lock", 0.05, True),
    (".gitignore", 0.01, True), ("src/train.py", 0.01, True),
    ("data/raw/orders.csv", 0.01, True), ("data/raw/reviews_2026.csv", 180, True),
    ("data/processed/clean.parquet", 2, False), ("notebooks/explore.ipynb", 0.3, True),
    ("models/model.joblib", 4, False), ("reports/metrics.json", 0.01, True),
    (".env", 0.01, True),
]

paths = [p for p, _, _ in files]
problems = []
for folder in REQUIRED:
    if not any(p.startswith(folder) for p in paths):
        problems.append(f"missing folder: {folder}")
for name in MUST_HAVE:
    if name not in paths:
        problems.append(f"missing file: {name}")
for p, mb, tracked in files:
    if tracked and any(h in p.lower() for h in SECRET_HINTS):
        problems.append(f"secret in Git: {p}")
    if tracked and mb > MAX_GIT_MB:
        problems.append(f"too big for Git ({mb} MB): {p} -> DVC/LFS")

print(len(files), "paths checked")
print("\n".join(problems) if problems else "OK: layout looks reproducible")

আসল আউটপুট:

11 paths checked
missing file: README.md
too big for Git (180 MB): data/raw/reviews_2026.csv -> DVC/LFS
secret in Git: .env

তিনটি সমস্যা ধরা পড়েছে। README.md নেই, তাই নতুন কেউ জানবেন না কোথা থেকে শুরু করবেন। ১৮০ MB-এর একটি review ফাইল Git-এ তোলা হয়েছে; Git প্রতিটি version চিরকাল রেখে দেয়, তাই repository ভারী হতে থাকে (এর সমাধান অংশ ৫-এ)। আর সবচেয়ে বিপজ্জনক: .env ফাইল, যাতে সাধারণত password বা API key থাকে, Git-এ চলে গেছে (অংশ ৬)। খেয়াল করুন, models/model.joblib আর clean.parquet Git-এ নেই বলে কোনো আপত্তি ওঠেনি; সেগুলো code থেকে আবার তৈরি হয়। ৫০ MB সীমাটি এখানে দলের নিজের ঠিক করা নিয়ম, Git-এর নয়।

৩ · Environment বেঁধে রাখা — pyproject, uv.lock আর Python version

Python install, virtual environment আর uv-এর খুঁটিনাটি পর্ব ৭ (AI-এর জন্য Python — install, uv, venv আর package)-এ। Python ভাষার দিক থেকে venv আর pyproject.toml বোঝার জন্য আছে Python কোর্সের pip, virtual environment আর pyproject পর্ব। এখানে শুধু একটি প্রশ্ন: অন্য কম্পিউটারে হুবহু একই package কীভাবে ফিরিয়ে আনবেন?

দুটি ফাইলের পার্থক্য বুঝলেই উত্তর পরিষ্কার। pyproject.toml হলো বাজারের ফর্দ: “চাল লাগবে, ৫ কেজির কম নয়।” আর uv.lock হলো দোকানের রসিদ: “মিনিকেট, অমুক ব্যাচ, ৫ কেজি, এই ওজনে।” ফর্দ দিয়ে আবার বাজার করলে অন্য ব্যাচ আসতে পারে; রসিদ দিয়ে করলে হুবহু একই জিনিস।

সত্যিকারের run (Windows-এ Git Bash, uv 0.11.28; ফোল্ডারের পথ সরিয়ে ~/bazarghar-ai লেখা হয়েছে):

$ uv init bazarghar-ai --app --python 3.13 --vcs none
Initialized project `bazarghar-ai` at `~/bazarghar-ai`
$ cd bazarghar-ai
$ uv add scikit-learn pandas
Using CPython 3.13.5 interpreter at: …
Creating virtual environment at: .venv
Resolved 11 packages in 1.57s
Prepared 4 packages in 1m 27s
Installed 10 packages in 1.46s
 + cloudpickle==3.1.2
 + joblib==1.6.0
 + narwhals==2.28.0
 + numpy==2.5.4
 + pandas==3.1.0
 + python-dateutil==4.0.0
 + scikit-learn==1.9.1
 + scipy==1.18.1
 + threadpoolctl==3.7.0
 + tzdata==2026.6

আপনি চেয়েছেন মাত্র দুটি package, কিন্তু এসেছে দশটি। বাকিগুলো তাদের dependency — scikit-learn-এর জন্য NumPy, SciPy, joblib; pandas-এর জন্য python-dateutil, tzdata. এদের যেকোনো একটির version বদলালেও ফল বদলাতে পারে। এবার দুটি ফাইলের ভেতরে দেখুন:

$ cat pyproject.toml
[project]
name = "bazarghar-ai"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
    "pandas>=3.1.0",
    "scikit-learn>=1.9.1",
]

$ grep -A12 'name = "scikit-learn"' uv.lock
name = "scikit-learn"
version = "1.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "joblib" },
    { name = "narwhals" },
    { name = "numpy" },
    { name = "scipy" },
    { name = "threadpoolctl" },
]
sdist = { url = "https://files.pythonhosted.org/…", hash = "sha256:…" }
wheels = [
    { url = "https://files.pythonhosted.org/…", hash = "sha256:…" },
…

pyproject.toml বলছে “৩.১.০ বা তার বেশি” — ফর্দ। uv.lock (মোট ৩০৭ লাইন) বলছে “হুবহু ১.৯.১, এই ফাইল, এই hash” — রসিদ, আর শুধু scikit-learn নয়, দশটি package-এরই। .python-version ফাইলে লেখা আছে 3.13, যাতে Python-এর version-ও না বদলায়। তিনটি ফাইলই Git-এ যায়; .venv/ ফোল্ডারটি যায় না, কারণ lockfile থেকে সেটি যেকোনো সময় আবার বানানো যায়।

Lockfile পুরোনো হয়ে গেলে কী হয়? দ্বিতীয় একটি কপিতে pyproject.toml-এ হাতে matplotlib যোগ করা হলো, কিন্তু uv lock চালানো হলো না। সত্যিকারের run:

$ uv sync --locked
Resolved 19 packages in 427ms
error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided.

hint: To update the lockfile, run `uv lock`.

এটিই চাই। --locked মানে “ফর্দ আর রসিদ না মিললে থামো।” না থামলে আপনি ভাবতেন হুবহু একই environment পেয়েছেন, আসলে পেতেন নতুন কিছু। শুধু lockfile থেকে বসাতে চাইলে, মিলিয়ে না দেখে, আছে --frozen; কিন্তু reproducibility যাচাইয়ের সময় --locked নিরাপদ।

Lockfile সবকিছু বাঁধে না। Operating system, CPU-র ধরন আর GPU driver এর বাইরে। একই lockfile থেকেও Windows আর Linux-এ আলাদা wheel ফাইল বসতে পারে, আর গাণিতিক library-র ভেতরের ছোট পার্থক্যে দশমিকের শেষ ঘরে ফল একটু আলাদা হতে পারে। পুরো operating system বাঁধতে লাগে container (যেমন Docker), যা Cloud & DevOps কোর্সে আছে।

৪ · ডেটার আঙুলের ছাপ — hash

Code-এর version চেনা সহজ — Git-এর commit hash. কিন্তু ডেটা? “orders_final_v2_আসল.csv” নামে ভরসা নেই। দরকার এমন একটি ছোট চিহ্ন, যা ফাইলের প্রতিটি byte থেকে হিসাব করা হয়, যাতে একটি সংখ্যাও বদলালে চিহ্নটি বদলে যায়। এটাই hash function-এর কাজ।

জাতীয় পরিচয়পত্রের জন্য আঙুলের ছাপ নেওয়া হয়। পুরো মানুষটিকে মনে রাখতে হয় না; ছাপ মিললেই বোঝা যায় এটি একই মানুষ। SHA-256 ঠিক তেমনি যেকোনো আকারের ফাইল থেকে 64 অক্ষরের একটি ছাপ বানায়। ৫ KB-এর CSV হোক বা ৫০ GB-এর ছবির সংগ্রহ — ছাপ সবসময় 64 অক্ষর।

একটি ভালো cryptographic hash-এর তিনটি গুণ এখানে কাজে লাগে:

  • নির্ধারিত (deterministic): একই byte দিলে যেকোনো কম্পিউটারে, যেকোনো সময়, একই ছাপ।
  • Avalanche: একটি bit বদলালে ছাপের প্রায় অর্ধেক bit বদলে যায়; দেখে বোঝার উপায় থাকে না যে দুটি ফাইল “প্রায় একই”।
  • সংঘাত প্রায় অসম্ভব: আলাদা দুটি ফাইলের একই ছাপ পাওয়া তাত্ত্বিকভাবে সম্ভব, কিন্তু বাস্তবে নয়। SHA-256-এর সম্ভাব্য ছাপ 2256টি। “Birthday bound” অনুযায়ী এলোমেলো ফাইল থেকে একটি সংঘাত পেতে মোটামুটি 2128 ≈ 3.4 × 1038টি ফাইল লাগবে। পৃথিবীর সব কম্পিউটার মিলেও এত ফাইল বানানো সম্ভব নয়।

নিচের কোডটি bazarghar-ai-এর আসল data/raw/orders.csv (৪০০টি অর্ডার, পাতার ভেতরে রাখা) পড়ে তার ছাপ নেয়, তারপর একটিমাত্র মান বদলে আবার ছাপ নেয়:

import hashlib
from pathlib import Path

def sha256_of(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()

raw = Path("data/raw/orders.csv").read_bytes()
h1 = sha256_of(raw)
print("rows:", raw.count(b"\n") - 1, "| bytes:", len(raw))
print("original :", h1[:16])

# one value changes: first order's late 0 -> 1
edited = raw.replace(b"15.7,1,28.7,0", b"15.7,1,28.7,1", 1)
h2 = sha256_of(edited)
print("one edit :", h2[:16])
same = sum(a == b for a, b in zip(h1, h2))
print("same file?", h1 == h2, "| matching hex digits:", same, "of", len(h1))

# only the line endings change (Windows CRLF)
h3 = sha256_of(raw.replace(b"\n", b"\r\n"))
print("CRLF copy:", h3[:16], "| same as original?", h3 == h1)

# same bytes again -> same hash, every time, on every computer
print("re-hash  :", sha256_of(raw)[:16], "| stable?", sha256_of(raw) == h1)

আসল আউটপুট:

rows: 400 | bytes: 5367
original : 6dc23919dc729ae6
one edit : c6789d1af4631f89
same file? False | matching hex digits: 5 of 64
CRLF copy: 88c7cf382539cb7e | same as original? False
re-hash  : 6dc23919dc729ae6 | stable? True

৫৩৬৭ byte-এর ফাইলে মাত্র একটি অক্ষর (0 থেকে 1) বদলেছে, অথচ ছাপ 6dc23919… থেকে হয়ে গেছে c6789d1a…. 64টি hex অক্ষরের মধ্যে মাত্র ৫টি একই জায়গায় মিলেছে; এলোমেলো দুটি hex অক্ষর মেলার সম্ভাবনা ১/১৬, তাই গড়ে ৬৪ ÷ ১৬ = ৪টি মিল হওয়ার কথা — এটি কাকতালীয় মিল, কোনো “কাছাকাছি” ভাব নয়। তৃতীয় লাইনটি অনেককে চমকে দেয়: শুধু লাইনের শেষ \n থেকে Windows-ধরনের \r\n হলেও ছাপ বদলায়, যদিও Excel-এ দুটি ফাইল হুবহু একই দেখায়। Git-এর core.autocrlf সেটিং বা কোনো editor নীরবে এই বদল করতে পারে; তাই সত্যিকারের প্রকল্পে CSV লেখার সময় line ending নির্দিষ্ট করে দেওয়া হয়েছে।

এবার একই ঘটনা সত্যিকারের প্রকল্পে। train.py প্রতিবার চলার সময় ডেটার SHA-256 হিসাব করে ছাপে আর log-এ লেখে। একটি কপিতে কেউ প্রথম অর্ডারের late মান হাতে বদলে দিলেন। প্রতিটি সংখ্যা সত্যিকারের run থেকে নেওয়া:

data/raw/orders.csv · প্রথম অর্ডার, ছাপ আর metrics
প্রথম সারি
লেখা থাকা ছাপ
এখনকার ছাপ
accuracy · f1
  1. মূল অবস্থা। ফাইলের ছাপ 6dc23919dc72 (প্রথম ১২ অক্ষর), যা আগের experiment log-এ লেখা আছে। Model-এর ফল accuracy 0.83, F1 0.8.
  2. একটি মান বদলানো হলো। প্রথম অর্ডারটি আসলে সময়মতো পৌঁছেছিল (late = 0), কেউ ভুল ভেবে 1 করে দিলেন। ফাইলটি দেখতে প্রায় একই; ৪০০ সারির মধ্যে একটিতে একটি অক্ষর। চোখে দেখে এই বদল ধরার কোনো উপায় নেই।
  3. আবার train.py চালানো হলো। Script প্রথমেই ছাপ হিসাব করল: c6789d1af463. লেখা থাকা ছাপের সঙ্গে মিলছে না, অর্থাৎ ডেটা বদলেছে — কে বদলেছেন বা কেন, তা ছাপ বলে না, কিন্তু বদলেছে যে, তা নিশ্চিত।
  4. Metrics-ও বদলেছে। একটি label বদলাতেই accuracy 0.82, F1 0.7692. ছাপ না দেখলে কেউ হয়তো ভাবতেন model-এ বা code-এ কিছু হয়েছে, আর ঘণ্টার পর ঘণ্টা ভুল জায়গায় খুঁজতেন।
  5. Git দেখিয়ে দিল কোন ফাইল বদলেছে। ছোট ডেটা Git-এ ছিল বলে git diff --stat দেখাল দুটি ফাইল বদলেছে: ডেটা আর metrics. মূল ফাইল Git থেকে ফিরিয়ে আনতেই ছাপ আবার 6dc23919dc72, ফলও আবার 0.83 · 0.8.

আরও গভীরে: Python-এর নিজস্ব hash() ফাংশন ডেটার ছাপ হিসেবে চলবে না। Python 3.3 থেকে string-এর hash() প্রতিটি নতুন process-এ এলোমেলো একটি মান দিয়ে শুরু হয় (নিরাপত্তার কারণে; PYTHONHASHSEED দিয়ে নিয়ন্ত্রণ করা যায়), তাই আজকের মান কালকে মিলবে না। ছাপের জন্য সবসময় hashlib-এর SHA-256 বা অন্তত MD5. MD5 নিরাপত্তার কাজে আর ভরসাযোগ্য নয় (ইচ্ছাকৃত সংঘাত বানানো যায়), কিন্তু হঠাৎ ঘটে যাওয়া বদল ধরতে এখনো যথেষ্ট; DVC ডিফল্টভাবে MD5-ই ব্যবহার করে, যা পরের অংশে দেখবেন। Git নিজেও প্রতিটি ফাইল আর commit-কে hash দিয়ে চেনে (ঐতিহ্যগতভাবে SHA-1, নতুন version-এ SHA-256-এর বিকল্প আছে)।

৫ · ছোট ডেটা Git-এ, বড় ডেটা বাইরে — DVC আর Git LFS

Data versioning-এর সবচেয়ে সহজ উপায় হলো ডেটা সরাসরি Git-এ রাখা। bazarghar-ai-এর orders.csv মাত্র ৫ KB, তাই এটি Git-এই আছে, আর আগের stepper-এ দেখলেন এতে কত সুবিধা। কিন্তু Git তৈরি হয়েছিল code-এর জন্য। প্রতিটি clone পুরো ইতিহাস নামায়; ২০০ MB-এর একটি ফাইল দশবার বদলালে repository প্রায় ২ GB, আর প্রত্যেক নতুন সহকর্মীকে সবটা নামাতে হবে। জনপ্রিয় hosting সেবাগুলোও ফাইলের আকারে সীমা দেয় (লেখার সময়ে GitHub সাধারণ Git-এ ১০০ MB-এর বেশি ফাইল নেয় না, ৫০ MB-এর ওপরে সতর্ক করে)।

সমাধানের মূল ভাবনা দুই টুলেই এক: Git-এ রাখুন ছোট একটি চিরকুট, আসল ফাইল রাখুন অন্য গুদামে। চিরকুটে লেখা থাকে ফাইলের hash; hash মিলিয়ে গুদাম থেকে ঠিক সেই version নামানো যায়।

📦 DVC

DVC (Data Version Control) Git-এর পাশে আলাদা কমান্ড হিসেবে চলে। ডেটা রাখার “remote” হতে পারে একটি ফোল্ডার, network drive বা cloud storage. Pipeline-এর ধাপ আর metrics-ও track করতে পারে।

ML প্রকল্পে বেশি দেখা যায়

🔗 Git LFS

Git LFS হলো Git-এর একটি extension. git add-এর সময় বড় ফাইলের বদলে একটি pointer রাখে; আসল ফাইল যায় LFS server-এ। সাধারণ Git কমান্ডেই কাজ চলে, কিন্তু hosting সেবার LFS storage-এর সীমা আর খরচ আছে।

ছবি, ভিডিও, model ফাইল

DVC এই পর্বের জন্য একটি আলাদা scratch ফোল্ডারে আলাদা virtual environment-এ বসানো হয়েছিল (DVC 3.67.1). সত্যিকারের run, একই orders.csv নিয়ে (DVC-র লম্বা স্বাগত-বার্তার একটি অংশ “…” দিয়ে ছাঁটা):

$ git init -q
$ dvc init
Initialized DVC repository.

You can now commit the changes to git.
…
$ dvc add data/raw/orders.csv

To track the changes with git, run:

	git add 'data\raw\orders.csv.dvc' 'data\raw\.gitignore'
…
$ cat data/raw/orders.csv.dvc
outs:
- md5: a602c3fbca1de19ea5b7e4a495de5fb0
  size: 5367
  hash: md5
  path: orders.csv
$ cat data/raw/.gitignore
/orders.csv
$ dvc remote add -d store ../dvc-store
Setting 'store' as a default remote.
$ dvc push
1 file pushed

লক্ষ করুন কী ঘটল। DVC ফাইলটির MD5 ছাপ নিয়ে (a602c3fb…) একটি ৫ লাইনের orders.csv.dvc চিরকুট বানাল, আর .gitignore-এ আসল ফাইলটি যোগ করে দিল, যাতে সেটি ভুলেও Git-এ না যায়। Git-এ যাবে শুধু চিরকুট; আসল ফাইল গেল dvc-store নামের “গুদামে”। এবার ডেটার একটি মান বদলে দেখা হলো:

$ dvc status
data\raw\orders.csv.dvc:
	changed outs:
		modified:           data\raw\orders.csv
$ dvc checkout --force
M       data\raw\orders.csv
$ dvc status
Data and pipelines are up to date.

DVC ছাপ মিলিয়ে বদল ধরল, আর dvc checkout গুদাম থেকে মূল version ফিরিয়ে আনল। অন্য কেউ repository clone করলে dvc pull দিয়ে চিরকুটে লেখা version-টিই পাবেন। Git LFS এই পর্বে চালানো হয়নি; এর কাজের ধারা একই রকম, শুধু কমান্ড git lfs track "*.parquet" ধরনের।

কোনটি কখন? কয়েক MB-এর CSV আর বছরে কয়েকবার বদল — সরাসরি Git-ই যথেষ্ট, সবচেয়ে সরল। বড় ডেটা, নিয়মিত নতুন version, আর ডেটা থেকে model পর্যন্ত ধাপের হিসাব দরকার — DVC. ছবি বা ভিডিওর মতো বড় binary ফাইল আর দল আগে থেকেই Git hosting-এর LFS ব্যবহার করে — Git LFS. এ নিয়ে দলগুলোর মধ্যে মতভেদ আছে, আর আরও অনেক টুল আছে (lakeFS, cloud storage-এর নিজস্ব versioning ইত্যাদি)। মূল নীতি একই: ডেটার version-কে একটি hash দিয়ে চিহ্নিত করে code-এর commit-এর সঙ্গে জুড়ে রাখা।

৬ · .gitignore আর গোপন তথ্য — যা কখনো Git-এ যাবে না

Git-এর মৌলিক কাজ (init, add, commit, তিনটি এলাকা) এই কোর্সে আবার শেখানো হবে না; সেগুলো আছে Software Engineering & Git কোর্সের প্রথম Git repository পর্বে, আর .gitignore-এর নিয়মের খুঁটিনাটি .gitignore, .gitattributes আর git config পর্বে। এখানে শুধু ডেটা-বিজ্ঞানের প্রকল্পে কোন জিনিস বাদ দিতে হয়, তা। bazarghar-ai-এর আসল .gitignore:

# Python and the environment (rebuilt from uv.lock)
__pycache__/
*.py[cod]
.venv/

# Jupyter
.ipynb_checkpoints/

# Data: processed files are rebuilt by code; big raw files live outside Git
data/processed/
data/raw/*
!data/raw/orders.csv

# Trained models and the local experiment log
models/
reports/experiments.jsonl

# Secrets: never in Git
.env
*.key

data/raw/* লাইনটি raw ফোল্ডারের সবকিছু বাদ দেয়, আর !data/raw/orders.csv (! মানে “বাদ দিও না”) শুধু ছোট ফাইলটিকে আবার ঢোকায়। এতে কেউ ভুলে বড় একটি ফাইল raw ফোল্ডারে রাখলেও সেটি Git-এ যাবে না। সত্যিকারের repository-তে git ls-files দেখাল ঠিক ১১টি ফাইল: .gitignore, .python-version, README.md, data/raw/orders.csv, notebooks/.gitkeep, params.json, pyproject.toml, reports/metrics.json, src/make_data.py, src/train.py আর uv.lock. খালি ফোল্ডার Git রাখে না, তাই notebooks/-এ একটি খালি .gitkeep ফাইল রাখা হয়েছে।

Secret আর ব্যক্তিগত ডেটা

AI প্রকল্পে প্রায়ই database-এর password, cloud-এর key বা কোনো API-র token লাগে। এগুলো secret. Notebook-এর একটি cell-এ API_KEY = "…" লিখে commit করলে key-টি Git-এর ইতিহাসে স্থায়ী হয়ে যায়। পরের commit-এ লাইনটি মুছলেও আগের commit-এ সেটি থেকে যায়, আর public repository-তে স্বয়ংক্রিয় bot কয়েক মিনিটের মধ্যেই এমন key খুঁজে বের করতে পারে বলে নিরাপত্তা-গবেষকেরা বারবার সতর্ক করেছেন।

  • Secret থাকবে .env ফাইলে বা environment variable-এ, আর .env থাকবে .gitignore-এ। Repository-তে রাখুন শুধু একটি .env.example, যাতে নাম আছে কিন্তু মান নকল: DB_PASSWORD=example-secret.
  • ভুলে commit হয়ে গেলে আগে key বাতিল করুন (rotate), তারপর ইতিহাস পরিষ্কারের কথা ভাবুন। ইতিহাস মোছার পদ্ধতি আর secret-scanning টুল আছে Git-এ নিরাপত্তা পর্বে।
  • ব্যক্তিগত ডেটা Git-এ নয়। ক্রেতার নাম, ফোন নম্বর, ঠিকানা, জাতীয় পরিচয়পত্র নম্বর বা bKash নম্বর থাকা ফাইল কখনো Git-এ তুলবেন না, private repository হলেও না; repository কপি হয়, ল্যাপটপ হারায়, অ্যাক্সেস বদলায়। Model শেখার জন্য সাধারণত এগুলো লাগেই না। দরকার হলে আগে সরিয়ে বা ছদ্মনাম দিয়ে (pseudonymise) তারপর কাজ। Data protection-এর বড় আলোচনা পর্ব ৬-এ আর পরের ধাপের AI নৈতিকতার কোর্সে।

Jupyter notebook-এর .ipynb ফাইল শুধু code নয়, প্রতিটি cell-এর output-ও রাখে। আপনি df.head() চালিয়ে ক্রেতাদের ফোন নম্বরসহ পাঁচটি সারি দেখেছিলেন? সেটি notebook-এর ভেতরে সংরক্ষিত, আর commit করলে Git-এ। Commit-এর আগে output মুছে দেওয়া (“Clear All Outputs” বা nbstripout-এর মতো টুল) একটি ভালো অভ্যাস।

৭ · Experiment-এর খাতা — parameter, ডেটার ছাপ, code version আর metrics

ক্রিকেটের স্কোরকার্ডে শুধু মোট রান লেখা থাকে না; কে, কোন ওভারে, কোন বলে কত রান — সব। এক মাস পরেও যে কেউ খেলাটি নতুন করে “পড়তে” পারেন। Model train করাও একরকম খেলা, আর তার স্কোরকার্ড হলো experiment log. প্রতিটি run-এ কমপক্ষে চারটি জিনিস লিখতে হয়:

  1. Parameter: seed, test_size, model-এর hyperparameter (যেমন logistic regression-এর C). bazarghar-ai-এ এগুলো code-এর ভেতরে নয়, আলাদা params.json ফাইলে: {"seed": 42, "test_size": 0.25, "C": 1.0}.
  2. ডেটার ছাপ: SHA-256-এর প্রথম ১২ অক্ষর যথেষ্ট।
  3. Code-এর version: git rev-parse --short HEAD থেকে পাওয়া commit hash, আর library-র version (অন্তত scikit-learn-এর)।
  4. Metrics: accuracy, F1 বা যা আপনার সমস্যার জন্য ঠিক।

Log-এর ধরন হিসেবে JSON Lines খুব সুবিধাজনক: প্রতিটি run একটি লাইন, নতুন run মানে ফাইলের শেষে একটি নতুন লাইন, আগের কিছু বদলায় না। নিচের পাতায় রাখা ফাইলে train.py-এর লেখা আসল চারটি লাইন আছে। কোডটি একটি নতুন run যোগ করে, তারপর পুরো log টেবিল হিসেবে পড়ে:

import json
import pandas as pd

LOG = "reports/experiments.jsonl"

def log_run(commit, data_sha256, params, metrics):
    entry = {"commit": commit, "data_sha256": data_sha256,
             "params": params, "sklearn": "1.9.1", **metrics}
    with open(LOG, "a", encoding="utf-8") as f:   # "a" = append, never overwrite
        f.write(json.dumps(entry) + "\n")

# a new run: same code and data, but the split seed was removed
log_run("b25370c", "6dc23919dc72",
        {"seed": None, "test_size": 0.25, "C": 1.0},
        {"accuracy": 0.84, "f1": 0.8095})

with open(LOG, encoding="utf-8") as f:
    rows = [json.loads(line) for line in f if line.strip()]
log = pd.json_normalize(rows).rename(columns=lambda c: c.replace("params.", ""))
log["seed"] = log["seed"].astype("Int64")
print(log[["commit", "data_sha256", "seed", "C", "accuracy", "f1"]].to_string())
print()
print("runs:", len(log), "| data versions:", log["data_sha256"].nunique())
seeded = log[log["seed"].notna()]
best = seeded["f1"].max()
print("seeded runs, best f1:", best,
      "at C =", seeded.loc[seeded["f1"] == best, "C"].tolist())
print("unseeded runs:", int(log["seed"].isna().sum()), "-> cannot be repeated exactly")

আসল আউটপুট:

    commit   data_sha256  seed      C  accuracy      f1
0  cc09fc3  6dc23919dc72    42   1.00      0.83  0.8000
1  b25370c  6dc23919dc72    42   0.01      0.82  0.7805
2  b25370c  6dc23919dc72    42   0.10      0.83  0.8000
3  b25370c  6dc23919dc72    42  10.00      0.83  0.8000
4  b25370c  6dc23919dc72  <NA>   1.00      0.84  0.8095

runs: 5 | data versions: 1
seeded runs, best f1: 0.8 at C = [1.0, 0.1, 10.0]
unseeded runs: 1 -> cannot be repeated exactly

open(LOG, "a")-এর "a" মানে append — আগের লেখা ঠিক রেখে শেষে যোগ। "w" দিলে আগের সব run মুছে যেত, যা log-এর জন্য সবচেয়ে বড় ভুল। pd.json_normalize ভেতরের params dictionary-কে আলাদা কলামে ভেঙে দেয়। টেবিল থেকে কয়েকটি জিনিস চোখে পড়ে। পাঁচটি run-এরই ডেটার ছাপ এক, তাই তুলনা ন্যায্য। প্রথম run-এর commit আলাদা (cc09fc3), কারণ সেটি metrics commit করার আগে চালানো হয়েছিল। শেষ run-এ seed নেই (<NA>) — তার 0.84 সংখ্যাটি আর কখনো হুবহু ফিরিয়ে আনা যাবে না।

এবার log পড়ে তুলনা করা যাক। নিচের stepper-এ log-এর প্রতিটি লাইন একে একে আসছে:

reports/experiments.jsonl · C · seed → accuracy · f1
run ১ · cc09fc3
run ২ · b25370c
run ৩ · b25370c
run ৪ · b25370c
run ৫ · b25370c
  1. প্রথম run — baseline. C=1.0, seed 42: accuracy 0.83, F1 0.8. এটিই তুলনার মাপকাঠি। Commit cc09fc3, ডেটার ছাপ 6dc23919dc72.
  2. C=0.01. Regularization বেশি (model-কে বেশি সংযত রাখা)। Accuracy 0.82, F1 0.7805 — baseline-এর চেয়ে F1 ০.০১৯৫ কম। ১০০টি test অর্ডারে এটি মাত্র একটি অর্ডারের পার্থক্য।
  3. C=0.1. ফল হুবহু baseline-এর মতো: 0.83 · 0.8. Code commit এখন b25370c, কিন্তু ডেটার ছাপ একই, তাই তুলনা ন্যায্য।
  4. C=10. আবারও 0.83 · 0.8. এই ছোট, প্রায় সরল ডেটায় C বদলালে সিদ্ধান্তের রেখা প্রায় নড়ে না।
  5. Seed ছাড়া একটি run. 0.84 · 0.8095 — সবচেয়ে “ভালো” দেখাচ্ছে! কিন্তু এর test set অন্যদের থেকে আলাদা, আর এটি আবার চালালে অন্য সংখ্যা আসবে (অংশ ৯-এ সত্যিকারের প্রমাণ)। একে বিজয়ী ধরা হবে ভুল।
  6. তুলনার সিদ্ধান্ত। Seed দেওয়া চারটি run-এর পার্থক্য সর্বোচ্চ ০.০১, যা ১০০ সারির test set-এর স্বাভাবিক ওঠানামার (এক SE প্রায় ০.০৪) অনেক ভেতরে। তাই কোনো C “জেতেনি”; সরল baseline (C=1.0) রাখাই যুক্তিসংগত। Log না থাকলে কেউ হয়তো 0.84-কে উন্নতি বলে রিপোর্ট করতেন।

সত্যিকারের run-এ একটি সূক্ষ্ম ফাঁক ধরা পড়েছে। শেষ run-টি চালানোর সময় train.py হাতে বদলানো ছিল (seed সরানো), কিন্তু commit করা হয়নি। তবু log-এ লেখা হয়েছে b25370c, যেন code বদলায়নি! Commit hash শুধু শেষ commit-এর কথা বলে, ফোল্ডারে না-commit-করা বদলের কথা বলে না। প্রতিকার: log-এ একটি dirty চিহ্নও রাখুন (git status --porcelain-এর আউটপুট খালি কি না), অথবা নিয়ম করুন যে commit ছাড়া কোনো “আসল” run হবে না।

ছোট প্রকল্পে এই JSONL বা একটি CSV ফাইলই যথেষ্ট। Run-এর সংখ্যা শতাধিক হলে, দলের অনেকে একসঙ্গে কাজ করলে, বা প্রতিটি run-এর chart আর model ফাইলও রাখতে হলে বিশেষ টুল কাজে লাগে। বহুল ব্যবহৃত একটি open-source বিকল্প MLflow: mlflow.log_param, mlflow.log_metric ধরনের ফাংশনে একই চারটি জিনিস লেখে আর ব্রাউজারে তুলনার পাতা দেখায়। এই পর্বে MLflow চালানো হয়নি; এর ব্যবহার পরের ধাপের কোর্সে আসবে। মনে রাখবেন, টুল বদলালেও কী লিখতে হবে তা বদলায় না।

৮ · পুরো পথ — clone থেকে হুবহু একই metrics

এতক্ষণের সব টুকরো জোড়া দেওয়ার সময়। bazarghar-ai-এর দুটি script:

  • src/make_data.py: np.random.default_rng(42) দিয়ে ৪০০টি কাল্পনিক অর্ডার বানায় (দূরত্ব, পণ্যের সংখ্যা, বৃষ্টি, দেরি হয়েছে কি না) আর data/raw/orders.csv-এ লেখে।
  • src/train.py: params.json পড়ে, ডেটার SHA-256 নেয়, random_state দিয়ে train-test split করে (৩০০ শেখার জন্য, ১০০ যাচাইয়ের জন্য), scikit-learn-এর logistic regression train করে, আর reports/metrics.json লেখে, সঙ্গে log-এ একটি লাইন।

train.py-এর মূল অংশটুকু (পুরো ফাইল ৩৭ লাইন):

params = json.loads(Path("params.json").read_text())
raw = Path("data/raw/orders.csv")
data_hash = hashlib.sha256(raw.read_bytes()).hexdigest()

df = pd.read_csv(raw)
X, y = df[["distance_km", "items", "rain_mm"]], df["late"]
X_tr, X_te, y_tr, y_te = train_test_split(
    X, y, test_size=params["test_size"], random_state=params["seed"], stratify=y)

model = LogisticRegression(C=params["C"], max_iter=1000).fit(X_tr, y_tr)
pred = model.predict(X_te)
metrics = {"accuracy": round(accuracy_score(y_te, pred), 4),
           "f1": round(f1_score(y_te, pred), 4)}
Path("reports/metrics.json").write_text(json.dumps(metrics, indent=2) + "\n")

প্রথম কম্পিউটারে (Git-এর লেখক হিসেবে কাল্পনিক “Rahim”) data তৈরি, commit, train, তারপর metrics commit. সত্যিকারের run:

$ uv run python src/make_data.py
wrote data/raw/orders.csv rows: 400
$ sha256sum data/raw/orders.csv
6dc23919dc729ae61dda11755c48e3ffa8ff94c4e480051bb4d56cf0ac03243f *data/raw/orders.csv
$ git init
Initialized empty Git repository in ~/bazarghar-ai/.git/
$ git add .
$ git commit -q -m "Bazarghar late-delivery model: data, code, lockfile"
$ uv run python src/train.py
data sha256: 6dc23919dc72
metrics    : {'accuracy': 0.83, 'f1': 0.8}
$ git add reports/metrics.json
$ git commit -q -m "Baseline metrics (seed 42)"
$ git log --format="%h %an <%ae> %s"
b25370c Rahim <rahim@example.com> Baseline metrics (seed 42)
cc09fc3 Rahim <rahim@example.com> Bazarghar late-delivery model: data, code, lockfile

এবার আসল পরীক্ষা: সম্পূর্ণ নতুন একটি ফোল্ডারে, যেখানে কোনো .venv নেই, কোনো পুরোনো ফাইল নেই। এটি করিমের ল্যাপটপের প্রতিনিধি। নিচের stepper-এ প্রতিটি ধাপের সত্যিকারের আউটপুট দেখুন:

  1. Codegit clonecommit b25370c
  2. Environmentuv sync --locked১০টি package
  3. ডেটাsha256sumছাপ মেলানো
  4. Trainuv runseed 42
  5. তুলনাgit diffকোনো পার্থক্য নেই
নতুন ফোল্ডারcode, ডেটা আর lockfile এলো
$ git clone ../bazarghar-ai
Cloning into 'bazarghar-ai'...
done.
$ ls -a bazarghar-ai
.  ..  .git  .gitignore  .python-version  README.md
data  notebooks  params.json  pyproject.toml
reports  src  uv.lock
uv sync --lockedরসিদ মিলিয়ে বসানো
Using CPython 3.13.5 interpreter at: …
Creating virtual environment at: .venv
Resolved 11 packages in 0.79ms
Installed 10 packages in 571ms
 + numpy==2.5.4
 + pandas==3.1.0
 + scikit-learn==1.9.1
 + scipy==1.18.1
 … আরও ৬টি, সব প্রথম কম্পিউটারের মতো
sha256sumডেটার ছাপ
$ sha256sum data/raw/orders.csv
6dc23919dc729ae61dda11755c48e3ff
a8ff94c4e480051bb4d56cf0ac03243f
 *data/raw/orders.csv
train.pyএকই seed, একই parameter
$ uv run python src/train.py
data sha256: 6dc23919dc72
metrics    : {'accuracy': 0.83, 'f1': 0.8}
git diffcommit করা metrics-এর সঙ্গে তুলনা
$ git diff --exit-code reports/metrics.json \
    && echo "metrics.json: no difference"
metrics.json: no difference
  1. Code আনা। git clone পুরো repository আনল: code, ছোট মূল ডেটা, params.json, uv.lock আর commit করা metrics.json. .venv আসেনি, কারণ সেটি .gitignore-এ; আসার দরকারও নেই।
  2. Environment বসানো। uv sync --locked lockfile মিলিয়ে দেখল, তারপর হুবহু একই দশটি package বসাল (uv-এর cache-এ আগে থেকেই ফাইল ছিল বলে এত দ্রুত)। Version-এর তালিকা প্রথম কম্পিউটারের সঙ্গে অক্ষরে অক্ষরে এক।
  3. ডেটার ছাপ মেলানো। SHA-256 শুরু হচ্ছে 6dc23919dc72 দিয়ে — log-এ লেখা ছাপের সঙ্গে মিলে গেছে। অর্থাৎ ডেটাও এক।
  4. Train চালানো। একই code, একই ডেটা, একই environment, একই seed: accuracy 0.83, F1 0.8 — প্রথম কম্পিউটারের সংখ্যা।
  5. যন্ত্র দিয়ে তুলনা। চোখে মেলানোর বদলে Git-কে বলা হলো নতুন metrics.json আর commit করা ফাইল মেলাতে। --exit-code পার্থক্য থাকলে ব্যর্থ হয়; এখানে হয়নি। চারটি উপাদান মিলেছে, তাই ফলও হুবহু মিলেছে। এই কমান্ডটি CI-তে (প্রতিটি পরিবর্তনে স্বয়ংক্রিয় পরীক্ষা) বসিয়ে রাখা যায়।

খেয়াল করুন, এখানে “একই কম্পিউটারের আরেকটি ফোল্ডার” ব্যবহার হয়েছে, আলাদা operating system নয়। অন্য OS-এ বা অন্য CPU-তে দশমিকের চতুর্থ ঘরে কদাচিৎ পার্থক্য আসতে পারে (অংশ ৩-এর সতর্কতা)। তাই অনেক দল metrics তুলনায় একটি ছোট সহনশীলতা (tolerance) রাখে, যেমন পার্থক্য ০.০০০১-এর কম হলে “মিলেছে”।

৯ · যেভাবে পুনরুৎপাদন ভাঙে — seed ছাড়া split আর না-বাঁধা library

এবার উল্টো পরীক্ষা: চারটি উপাদানের একটি ইচ্ছা করে ভাঙা। Clone করা ফোল্ডারে train.py-এর একটি শব্দ বদলানো হলো। সত্যিকারের run:

$ git diff src/train.py
…
 X_tr, X_te, y_tr, y_te = train_test_split(
-    X, y, test_size=params["test_size"], random_state=params["seed"], stratify=y)
+    X, y, test_size=params["test_size"], random_state=None, stratify=y)
$ for i in 1 2 3; do uv run python src/train.py | tail -1; done
metrics    : {'accuracy': 0.87, 'f1': 0.8506}
metrics    : {'accuracy': 0.87, 'f1': 0.8434}
metrics    : {'accuracy': 0.84, 'f1': 0.8095}
$ git diff reports/metrics.json
…
 {
-  "accuracy": 0.83,
-  "f1": 0.8
+  "accuracy": 0.84,
+  "f1": 0.8095

একই code (প্রায়), একই ডেটা, একই environment — তবু তিনবারে তিন রকম F1. random_state=None মানে scikit-learn প্রতিবার operating system থেকে নতুন এলোমেলোতা নেয়, তাই কোন ১০০টি অর্ডার test-এ যাবে তা প্রতিবার আলাদা। ভয়ের দিকটি খেয়াল করুন: তিনটি ফলই “ভালো” দেখাচ্ছে, কোনো error নেই, কোনো warning নেই। ০.৮৭ দেখে কেউ বলতে পারতেন “model উন্নত হয়েছে!” — আসলে শুধু সহজ অর্ডারগুলো test-এ পড়েছে।

নিচের কোডে পাতার ভেতরেই একই ঘটনা দেখুন। একই seed-এর ডেটা, একই model; প্রথম সারিতে split-এর seed 42, দ্বিতীয় সারিতে seed নেই। scikit-learn প্রথমবার লোড হতে প্রায় ১০ সেকেন্ড লাগতে পারে।

import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

rng = np.random.default_rng(42)              # the data itself is seeded
n = 400
dist = rng.uniform(1, 20, n).round(1)
items = rng.integers(1, 6, n)
rain = rng.uniform(0, 60, n).round(1)
score = 0.3 * dist + 0.06 * rain + 0.2 * items + rng.normal(0, 1.2, n)
X = np.column_stack([dist, items, rain])
y = (score > 6).astype(int)

def run(split_seed):
    X_tr, X_te, y_tr, y_te = train_test_split(
        X, y, test_size=0.25, random_state=split_seed, stratify=y)
    model = LogisticRegression(max_iter=1000).fit(X_tr, y_tr)
    return round(model.score(X_te, y_te), 2)

print("random_state=42  :", [run(42) for _ in range(5)])
print("random_state=None:", [run(None) for _ in range(5)])

আসল আউটপুট (একটি run; দ্বিতীয় লাইনটি আপনার স্ক্রিনে আলাদা হবে, সেটাই মূল কথা):

random_state=42  : [0.83, 0.83, 0.83, 0.83, 0.83]
random_state=None: [0.85, 0.91, 0.9, 0.8, 0.85]

প্রথম লাইনের পাঁচটি সংখ্যা সবসময় এক, আর bazarghar-ai-এর সত্যিকারের run-এর 0.83-এর সঙ্গেও মেলে — যদিও পাতার Pyodide-এ scikit-learn 1.8.0 আর প্রকল্পে 1.9.1. এই ছোট model-এ দুই version একই ফল দিয়েছে; বড়, জটিল model-এ তা নিশ্চিত নয়। দ্বিতীয় লাইন প্রতিবার “চালান” চাপলে বদলাবে। খেয়াল করুন ডেটা তৈরিতেও seed আছে (default_rng(42)); একটিমাত্র জায়গায় seed বাদ পড়লেই পুরো ফল এলোমেলো।

ওঠানামা কতটা স্বাভাবিক — একটি হিসাব

Seed ছাড়া প্রকল্পের তিনটি run-এ এলো ০.৮৪ থেকে ০.৮৭, আর পাতার কোডের একটি run-এ ০.৮০ থেকে ০.৯১। এটি কি বড় পার্থক্য? Test set-এ n = ১০০টি অর্ডার, আর আসল accuracy p ধরা যাক ০.৮৩. প্রতিটি অর্ডার ঠিক বা ভুল — একটি মুদ্রা-নিক্ষেপের মতো। তখন মাপা accuracy-র standard error (SE, অর্থাৎ বারবার নতুন test set নিলে সংখ্যাটি গড়ে কতটা ওঠানামা করে):

SE = √(p(1 − p) / n) = √(0.83 × 0.17 / 100) = √0.001411 ≈ 0.038

মোটামুটি নিয়ম হলো, ফল ±২ SE-র মধ্যে, অর্থাৎ ০.৮৩ ± ০.০৭৫ (প্রায় ০.৭৫৫ থেকে ০.৯০৫) পর্যন্ত, স্বাভাবিক ওঠানামা। প্রকল্পের তিনটি সংখ্যাই এই সীমার ভেতরে। পাতার run-এর ০.৯১ সীমার সামান্য বাইরে, যা অস্বাভাবিক নয়: ±২ SE-র বাইরে পড়ার সম্ভাবনা মোটামুটি ৫%, তাই অনেকগুলো split নিলে মাঝে মাঝে এমন হবেই। (হিসাবটি আনুমানিক; এখানে সব split একই ৪০০ সারি থেকে নেওয়া।) তাই সিদ্ধান্ত দুটি: (ক) ১০০ সারির test set-এ ০.০১–০.০৪-এর পার্থক্য দেখে “উন্নতি” দাবি করা যায় না; (খ) seed বেঁধে রাখলে অন্তত একই test set-এ তুলনা হয়, কিন্তু seed বেঁধে একটি ভাগ্যবান seed বেছে নেওয়াও এক ধরনের প্রতারণা। সৎ পথ হলো কয়েকটি seed বা cross-validation-এ গড় আর বিস্তার দেখানো, যা পরের মডিউলগুলোতে আসবে।

না-বাঁধা library — নীরব বদল

দ্বিতীয় বড় ফাঁদ হলো version না বাঁধা। pip install scikit-learn লিখলে আজ যা আসে, ছয় মাস পরে তা আসবে না। নতুন version-এ কোনো default বদলাতে পারে, যেমন একটি model-এর ডিফল্ট solver বা iteration সংখ্যা, আর তাতে একই code-এর ফল বদলায়, কোনো error ছাড়াই। scikit-learn-এর ইতিহাসে এমন default বদল একাধিকবার হয়েছে, আর তারা সাধারণত আগে থেকে FutureWarning দিয়ে সতর্ক করে; এই কারণেই warning উপেক্ষা করা বিপজ্জনক। এই পর্বের নিজের উদাহরণেও দেখলেন: পাতার Pyodide আর প্রকল্পের environment-এ scikit-learn-এর version আলাদা, আর কোন version-এ ফলটি এসেছে তা log-এ লেখা না থাকলে পরে পার্থক্যের কারণ খুঁজে পাওয়া কঠিন। প্রতিকার আপনি জানেন: uv.lock, uv sync --locked, আর log-এ sklearn version.

আরও গভীরে: seed দিলেও সব determinism আসে না। একাধিক thread-এ যোগ করলে ক্রম বদলায়, আর floating-point যোগ associative নয় ((a + b) + c ও a + (b + c) দশমিকের শেষ ঘরে আলাদা হতে পারে); GPU-তে deep learning-এর কিছু গণনা এই কারণে ডিফল্টভাবে non-deterministic. PyTorch-এর মতো framework “deterministic mode” দেয়, কিন্তু তাতে গতি কমতে পারে। Henderson ও সহকর্মীদের ২০১৮ সালের কাজ আর NeurIPS-এর reproducibility checklist ঠিক এই কারণে একাধিক seed-এ ফল আর তার বিস্তার রিপোর্ট করতে বলে।

১০ · AI-এর যুগে পুনরুৎপাদনযোগ্য কাজ

AI সহকারী একটি পুরো training script বা প্রকল্পের কাঠামো কয়েক সেকেন্ডে লিখে দিতে পারে। কিন্তু reproducibility-র দিক থেকে তার লেখা code-এ কিছু ভুল প্রায়ই দেখা যায়। নিচের তালিকা সাধারণ অভিজ্ঞতা থেকে, কোনো জরিপ থেকে নয়।

AI প্রায়ই যা করেকেন সমস্যাকীভাবে যাচাই করবেন
train_test_split-এ random_state দেয় না, বা একটি জায়গায় দেয়, অন্যটিতে নাপ্রতিবার আলাদা ফল, কোনো warning ছাড়াইScript দুবার চালিয়ে metrics মেলান
pip install pandas লেখে, version বা lockfile ছাড়াছয় মাস পরে অন্য version, নীরব বদলLockfile আছে কি না, uv sync --locked চলে কি না
পুরোনো API দেয়, যেমন pandas-এর df.append বা np.floatনতুন version-এ errorআপনার version-এর official docs দেখুন, চালিয়ে দেখুন
পুরো ডেটায় scaling বা feature তৈরি করে, তারপর splitTest-এর তথ্য training-এ ঢোকে (data leakage), ফল ফোলানোSplit সবার আগে কি না দেখুন
Training set-এর accuracy-কে ফল বলে দেখায়বাস্তবে model অনেক খারাপ হতে পারেTest set আর একটি সরল baseline-এর সঙ্গে তুলনা
Code-এর ভেতরে API_KEY = "…" বা ডেটার পুরো পথ বসায়Secret ফাঁস; অন্য কম্পিউটারে পথ মেলে না.env, আপেক্ষিক পথ, .gitignore
Warning চাপা দিতে warnings.filterwarnings("ignore") বসায়ভবিষ্যৎ default বদলের সতর্কবার্তা হারায়Warning পড়ুন, বুঝে তারপর সিদ্ধান্ত

AI-কে কাজ দেওয়ার সময়ই reproducibility-র শর্তগুলো বলে দিন। আর নিজের প্রকল্পের আসল ডেটা বা secret কখনো প্রম্পটে পেস্ট করবেন না; কলামের নাম আর কয়েকটি নকল সারিই যথেষ্ট।

আমি uv দিয়ে একটি Python প্রকল্প চালাই (Python 3.13, scikit-learn 1.9, pandas 3.1). ফোল্ডার: data/raw, data/processed, src, models, reports. data/raw/orders.csv-এর কলাম: distance_km, items, rain_mm, late (০ বা ১)। src/train.py লিখে দাও যা: ১. params.json থেকে seed, test_size আর C পড়বে — কোনো সংখ্যা code-এ hard-code করবে না; ২. ডেটা ফাইলের SHA-256 হিসাব করবে; ৩. সবার আগে stratified train-test split করবে, random_state=seed দিয়ে; split-এর আগে কোনো scaling বা feature তৈরি করবে না; ৪. শুধু test set-এ accuracy আর F1 মাপবে, সঙ্গে majority-class baseline; ৫. reports/metrics.json লিখবে আর reports/experiments.jsonl-এ একটি লাইন append করবে: commit hash, কোড dirty কি না, ডেটার hash, params, sklearn version, metrics. কোনো warning চাপা দেবে না। প্রতিটি ফাংশন scikit-learn 1.9-এ আছে কি না নিশ্চিত না হলে সেটা বলবে। শেষে বলো কোন দুটি কমান্ড চালিয়ে আমি যাচাই করব যে দুবার চালালে হুবহু একই metrics আসে।

AI-এর দেওয়া প্রকল্প হাতে পেলে — যাচাইয়ের চেকলিস্ট

পরীক্ষা ও ইন্টারভিউতে (বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় যে ধরনের প্রশ্ন সাধারণত আসে):

  • ML-এ reproducibility নিশ্চিত করতে কী কী লাগে? — Code-এর version (Git commit), ডেটার version (hash বা DVC), বাঁধা environment (lockfile), আর প্রতিটি random কাজে seed.
  • requirements.txt-এ pandas>=2 আর lockfile-এর পার্থক্য? — প্রথমটি একটি সীমা, যেকোনো নতুন version আসতে পারে; lockfile প্রতিটি package-এর হুবহু version আর hash রাখে।
  • Raw data কেন হাতে বদলানো উচিত নয়? — বদলের কোনো রেকর্ড থাকে না, পুনরায় করা যায় না; পরিষ্কারের ধাপ script-এ রেখে নতুন ফাইলে লিখতে হয়।
  • বড় ডেটা ফাইল Git-এ রাখলে সমস্যা কী, বিকল্প কী? — প্রতিটি version ইতিহাসে স্থায়ী, repository ভারী হয়, hosting-এর সীমা; বিকল্প DVC বা Git LFS, যা Git-এ শুধু hash-যুক্ত pointer রাখে।
  • Test set-এ n = ১০০, accuracy ০.৮০ হলে SE কত? — √(০.৮ × ০.২ / ১০০) = ০.০৪; তাই ০.৮০ বনাম ০.৮২ পার্থক্য অর্থবহ নয়।

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

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

প্র ০১Seed বেঁধে দিলেই কি গবেষণার ফল “সত্য” হয়ে যায়?

না, আর এই ভুল ধারণাটি বেশ প্রচলিত। Seed বাঁধলে ফলটি পুনরায় পাওয়া যায়, কিন্তু তাতে ফলটি সাধারণ বা সত্য হয় না। একটি নির্দিষ্ট seed একটি নির্দিষ্ট test set বেছে নেয়; সেই test set সহজ বা কঠিন হতে পারে। অংশ ৯-এ একই ডেটা আর model থেকে ০.৮০ থেকে ০.৯১ পর্যন্ত accuracy এসেছে শুধু split বদলে।

এখান থেকে একটি গুরুতর সমস্যা তৈরি হয়, যাকে অনেকে “seed hacking” বলেন: অনেকগুলো seed চালিয়ে যেটিতে নতুন পদ্ধতি সবচেয়ে ভালো দেখায় সেটি রিপোর্ট করা। ফলটি নিখুঁতভাবে reproducible — যে কেউ সেই seed দিয়ে চালালে একই সংখ্যা পাবেন — অথচ বিভ্রান্তিকর। Henderson ও সহকর্মীরা ২০১৮ সালে deep reinforcement learning-এ দেখিয়েছিলেন যে কয়েকটি seed-এর গড় নিলে দুটি পদ্ধতির ক্রম উল্টে যেতে পারে; তাঁদের সুপারিশ ছিল একাধিক seed-এ চালিয়ে গড় আর বিস্তার রিপোর্ট করা আর বিস্তারিত পদ্ধতি প্রকাশ করা।

সম্পর্কিত একটি ধারণা হলো Goodhart's law: একটি মাপকে লক্ষ্য বানালে সেটি আর ভালো মাপ থাকে না। একটি নির্দিষ্ট benchmark-এর test set-এ বছরের পর বছর অসংখ্য পদ্ধতি পরীক্ষা হলে, সব পদ্ধতিই আসলে ওই test set-এ একটু করে “মানিয়ে” যায় — এটিও একরকম পরোক্ষ leakage. এজন্য কিছু গবেষক নতুন, আগে না-দেখা test set বানিয়ে পুরোনো model আবার মেপেছেন, আর অনেক ক্ষেত্রে ফল কিছুটা কম এসেছে বলে জানিয়েছেন।

বাস্তবে সৎ রিপোর্ট দেখতে এমন: “৫টি seed-এ accuracy-র গড় ০.৮৫, standard deviation ০.০২; baseline ০.৬০।” এক লাইনেই পাঠক বুঝে যান ফল কতটা নির্ভরযোগ্য।

মূল উপলব্ধি: Reproducibility ন্যূনতম শর্ত, প্রমাণ নয়। Seed দিয়ে ফল ফিরিয়ে আনা যায়; কয়েকটি seed আর ন্যায্য তুলনা দিয়ে দেখাতে হয় ফলটি ভাগ্য নয়।

প্র ০২Notebook-ই কি সমস্যা? অনেকে বলেন “notebook-এ বিজ্ঞান হয় না” — কতটা ঠিক?

এটি সম্প্রদায়ের একটি পুরোনো বিতর্ক, আর দুই পক্ষেরই যুক্তি আছে। ২০১৮ সালে প্রোগ্রামার Joel Grus একটি সম্মেলনে “I don't like notebooks” শিরোনামে আলোচনা করে বিষয়টি জনপ্রিয় করেন। তাঁর মূল অভিযোগ: cell এলোমেলো ক্রমে চালানো যায়, লুকানো state থেকে যায় (একটি cell মুছে ফেললেও তার তৈরি variable memory-তে থাকে), আর ভালো অভ্যাস (test, module) তৈরি হয় না।

এর পক্ষে কিছু গবেষণাও আছে। ২০১৯ সালে Pimentel ও সহকর্মীরা GitHub-এর কয়েক লক্ষ public notebook স্বয়ংক্রিয়ভাবে চালানোর চেষ্টা করে জানান যে বেশিরভাগই উপর থেকে নিচে চালিয়ে আগের ফল পাওয়া যায়নি; কারণগুলোর মধ্যে ছিল dependency-র তথ্য না থাকা, cell-এর এলোমেলো ক্রম আর হারানো ডেটা ফাইল। সংখ্যাটির সঠিক মান পদ্ধতির ওপর নির্ভর করে, কিন্তু প্রবণতাটি পরিষ্কার।

অন্য পক্ষের যুক্তি: notebook ডেটা দেখার, চিত্র আঁকার আর ধারণা যাচাইয়ের সবচেয়ে কার্যকর পরিবেশ, আর লেখা, code ও ফল একসঙ্গে থাকায় এটি একটি জীবন্ত রিপোর্ট। সমস্যা notebook-এর নয়, ব্যবহারের। “Restart and run all” করে সবকিছু উপর থেকে চালানো, version বাঁধা environment-এ চালানো, আর বারবার লাগে এমন code src/-এ সরিয়ে notebook থেকে import করা — এই তিন অভ্যাসে অধিকাংশ সমস্যা চলে যায়।

এই পর্বের কাঠামো একটি মাঝামাঝি পথ: notebooks/ খোঁজার জন্য, src/ চালানোর জন্য। আর যেকোনো ফল যা রিপোর্টে যাবে, তা আসবে src/train.py থেকে, যা একটি কমান্ডে নতুন clone-এ চলে। পর্ব ৮-এ notebook-এর ভালো অভ্যাস নিয়ে বিস্তারিত।

মূল উপলব্ধি: টুল নিজে পুনরুৎপাদনযোগ্য বা অপুনরুৎপাদনযোগ্য নয়; অভ্যাস তা ঠিক করে। পরীক্ষাটি সহজ — নতুন clone-এ একটি কমান্ডে একই ফল আসে কি না।

প্র ০৩ডেটা তো বারবার বদলায় — প্রতিদিন নতুন অর্ডার আসে। তাহলে “একই ডেটা” কথাটির মানে কী?

খুব বাস্তব প্রশ্ন। বাজারঘরের মতো দোকানে প্রতি ঘণ্টায় নতুন অর্ডার আসে, ক্রেতা ঠিকানা বদলান, কিছু অর্ডার বাতিল হয়। ডেটাবেস একটি চলমান নদী; একই নদীতে দুবার নামা যায় না। তাহলে reproducibility কীভাবে?

উত্তর হলো snapshot. Training-এর আগে নদী থেকে এক বালতি পানি তুলে তার গায়ে লেবেল লাগান: কোন query, কোন তারিখ পর্যন্ত, আর ফাইলটির hash. Model শেখে সেই বালতি থেকে, নদী থেকে নয়। পরে কেউ প্রশ্ন করলে সেই বালতি (DVC-র গুদামে রাখা ফাইল) দিয়ে ফল ফিরিয়ে আনা যায়। প্রতিটি নতুন training মানে একটি নতুন বালতি, একটি নতুন hash, log-এ একটি নতুন লাইন।

দুটি সতর্কতা আছে। প্রথমত, query-ই যথেষ্ট নয়: “৩১ অক্টোবর পর্যন্ত সব অর্ডার” query আজ আর কাল আলাদা ফল দিতে পারে, কারণ পুরোনো অর্ডারের অবস্থা (বাতিল, ফেরত) পরে বদলায়। তাই query-র ফলাফলের ফাইলটিই সংরক্ষণ করতে হয়, শুধু query নয়। দ্বিতীয়ত, ব্যক্তিগত ডেটার আইনগত দিক: কোনো ক্রেতা নিজের তথ্য মুছে ফেলতে বললে পুরোনো snapshot-এও তার প্রভাব থাকে। কিছু দেশের data protection আইন এমন অধিকার দেয়, আর তখন “হুবহু পুনরুৎপাদন” আর “ব্যক্তির অধিকার”-এর মধ্যে টানাপোড়েন তৈরি হয়। এর একটি সাধারণ সমাধান হলো snapshot-এ শুরু থেকেই ব্যক্তিগত শনাক্তকারী তথ্য না রাখা।

আরেকটি দিক হলো সময়ের সঙ্গে ডেটার চরিত্র বদলানো (পর্ব ৬-এর concept drift). পুরোনো snapshot-এ model পুনরুৎপাদন করা যায়, কিন্তু তাতে আজকের বাস্তবতায় model কেমন করবে তা জানা যায় না। Reproducibility অতীতের হিসাব মেলায়; monitoring বর্তমানের।

মূল উপলব্ধি: চলমান ডেটায় “একই ডেটা” মানে hash দিয়ে চিহ্নিত একটি স্থির snapshot. Query নয়, ফাইলটিই প্রমাণ।

প্র ০৪Lockfile-এ সব version বাঁধা থাকলে নিরাপত্তা-ত্রুটির আপডেট আসবে কীভাবে? বাঁধা আর হালনাগাদ — দুটি একসঙ্গে সম্ভব?

এটি একটি সত্যিকারের টানাপোড়েন। Lockfile বলে “কিছুই বদলাবে না”; নিরাপত্তা বলে “ত্রুটি পাওয়া গেলে তখনই বদলাও”। দুটির সমাধান হলো বদল হবে, কিন্তু সচেতনভাবে, আর প্রমাণসহ।

বাস্তব উদাহরণ মনে করুন: ২০২১ সালের শেষে Java-র একটি বহুল ব্যবহৃত logging library-তে (Log4j) গুরুতর ত্রুটি (“Log4Shell”) ধরা পড়ে। যাঁদের প্রকল্পে dependency-র পূর্ণ তালিকা ছিল, তাঁরা দ্রুত বলতে পেরেছিলেন কোথায় সেই library আছে, কোন version-এ। তালিকা না থাকলে প্রথমেই খুঁজতে হতো “আমাদের কোথায় এটা আছে?” — lockfile সেই প্রশ্নের উত্তর দেয়। Python জগতেও PyPI-তে ক্ষতিকর package (নামের বানান একটু বদলে, যেমন “typosquatting”) ধরা পড়ার ঘটনা নিয়মিত খবর হয়। Lockfile-এর hash নিশ্চিত করে যে পরে কেউ একই version-এর নামে অন্য ফাইল ঢুকিয়ে দিলে তা বসবে না।

কাজের ধারা সাধারণত এমন: নিয়মিত (ধরুন মাসে একবার) বা কোনো সতর্কবার্তা এলে uv lock --upgrade-package <নাম> দিয়ে নির্দিষ্ট package হালনাগাদ, তারপর test আর reproducibility-র পরীক্ষা (metrics কি সহনশীলতার মধ্যে আছে?) চালানো, তারপর নতুন lockfile commit. অনেক দল Dependabot বা Renovate-এর মতো bot ব্যবহার করে, যা স্বয়ংক্রিয়ভাবে এমন হালনাগাদের প্রস্তাব (pull request) পাঠায়। এর বিস্তারিত আছে Cybersecurity কোর্সের Supply chain নিরাপত্তা পর্বে।

লক্ষ করুন, এখানে reproducibility আর নিরাপত্তা আসলে বন্ধু। পুরোনো ফলের পাশে commit hash আর lockfile থাকলে আপডেটের পর ফল বদলালে আপনি নিশ্চিত জানবেন কারণ library, ডেটা বা code নয়।

মূল উপলব্ধি: Lockfile বদলকে থামায় না, বদলকে দৃশ্যমান করে। প্রতিটি বদল একটি commit, যার আগে-পরে ফল মাপা যায়।

প্র ০৫বড় ভাষা মডেলের যুগে কি পুরোপুরি পুনরুৎপাদন আদৌ সম্ভব?

সৎ উত্তর: পুরোপুরি প্রায়ই নয়, আর এটি স্বীকার করা দরকার। একটি বড় language model-এর pretraining-এ হাজার হাজার GPU কয়েক সপ্তাহ চলে; খরচ এত বেশি যে কোনো বিশ্ববিদ্যালয় বা স্বাধীন গবেষক তা আবার চালাতে পারেন না। অনেক model-এর training data, এমনকি তার বিস্তারিত বিবরণও প্রকাশ হয় না। আর হাজার GPU-তে গণনার ক্রম সামান্য বদলালে floating-point-এর ছোট পার্থক্য জমে বড় হতে পারে।

কেবল API দিয়ে ব্যবহৃত model-এ সমস্যা আরও গভীর: প্রতিষ্ঠান নিঃশব্দে model হালনাগাদ করতে পারে, আর একই প্রশ্নে একই উত্তর আসবে এমন নিশ্চয়তা সাধারণত থাকে না, এমনকি “temperature” শূন্য করলেও অনেক সময় নয়। ২০২৩ সালে কিছু গবেষক একই নামের একটি বাণিজ্যিক model-এর আচরণ কয়েক মাসের ব্যবধানে মেপে উল্লেখযোগ্য পার্থক্য পেয়েছেন বলে জানান; এই ধরনের গবেষণার পদ্ধতি নিয়েও প্রশ্ন উঠেছে, তবে মূল কথাটি গ্রহণযোগ্য — বাইরের API-র পেছনে কী বদলাল তা আপনি দেখতে পান না।

তবু অনেক কিছু করা যায়। Open-weight model ব্যবহার করলে model-এর ফাইলের hash রাখা যায়, ঠিক এই পর্বের ডেটার মতো। নিজের fine-tuning বা মূল্যায়ন ছোট পরিসরে পুরোপুরি reproducible করা যায়: model-এর নাম ও revision, prompt, decoding-এর সেটিং, মূল্যায়ন-ডেটার hash আর library-র lockfile. Model card আর datasheet-এর মতো নথি অন্তত বলে দেয় কী জানা আছে আর কী নেই। আর শুরু থেকে আবার চালানো না গেলেও “একই model ফাইল আর একই input দিয়ে একই মূল্যায়ন-ফল” — এই সীমিত reproducibility প্রায় সবসময় সম্ভব।

মূল উপলব্ধি: যেখানে পুরো পুনরুৎপাদন অসম্ভব, সেখানে লক্ষ্য হয় স্বচ্ছতা: কী বাঁধা গেছে আর কী যায়নি, তা স্পষ্ট করে লেখা। এই পর্বের চারটি উপাদান বড় model-এর যুগেও একই — শুধু কোনটি নিয়ন্ত্রণে আছে, তা সৎভাবে বলতে হয়।

অনুশীলন

হিসাব, নকশা-সমালোচনা, আউটপুট অনুমান আর একটি ছোট কোড।

  1. হিসাব: একটি model-এর test set-এ ২০০টি অর্ডার। পুরোনো version-এর accuracy ০.৮০, নতুন version-এর ০.৮২ (একই test set). (ক) ০.৮০ ধরে standard error কত? (খ) ±২ SE-র সীমা কত? (গ) “নতুন version ভালো” — এই দাবি কতটা জোরালো?

    (ক) SE = √(০.৮০ × ০.২০ / ২০০) = √০.০০০৮ ≈ ০.০২৮৩।

    (খ) ০.৮০ ± ২ × ০.০২৮৩ = ০.৮০ ± ০.০৫৭, অর্থাৎ প্রায় ০.৭৪৩ থেকে ০.৮৫৭।

    (গ) ০.৮২ এই সীমার ভেতরে, পার্থক্য মাত্র ৪টি অর্ডার (২০০-র ২%). তাই দাবিটি দুর্বল। একই test set-এ দুটি model তুলনায় আরও সূক্ষ্ম পরীক্ষা আছে (যেমন McNemar's test, যা দেখে কোন অর্ডারগুলোতে দুটি model আলাদা উত্তর দিয়েছে), আর কয়েকটি seed বা cross-validation-এ গড় তুলনা করা উচিত। SE-র এই সূত্র ধরে নেয় যে অর্ডারগুলো একে অপরের থেকে স্বাধীন।

  2. নকশা-সমালোচনা: একজন সহকর্মীর প্রকল্প-ফোল্ডারে আছে: analysis.ipynb (৪৭টি cell, শেষ cell-এর নম্বর [212]), orders.xlsx, orders_clean_final_v3.xlsx, model.pkl, .env, আর একটি requirements.txt যাতে শুধু লেখা pandas আর sklearn. সব Git-এ commit করা। অন্তত ছয়টি সমস্যা চিহ্নিত করুন আর প্রতিটির প্রতিকার লিখুন।
    • Cell নম্বর [212]: ৪৭টি cell-এ ২১২ বার চালানো — ক্রম এলোমেলো, লুকানো state সম্ভব। প্রতিকার: “Restart and run all”, মূল ধাপ src/-এ।
    • মূল ডেটা আর পরিষ্কার ডেটা পাশাপাশি, নামে version: কোনটি থেকে কোনটি, কী বদলেছে, জানা নেই; সম্ভবত হাতে Excel-এ পরিষ্কার। প্রতিকার: data/raw আর data/processed, পরিষ্কার করা script-এ, ফাইলের hash log-এ।
    • model.pkl Git-এ: code থেকে তৈরি হয়, Git ভারী করে; অচেনা pickle ফাইল খোলা নিরাপত্তা-ঝুঁকিও। প্রতিকার: models/ .gitignore-এ, দরকারে DVC.
    • .env Git-এ: secret ফাঁস। প্রতিকার: সঙ্গে সঙ্গে key বাতিল (rotate), .gitignore-এ যোগ, .env.example, ইতিহাস পরিষ্কার।
    • Version ছাড়া requirements, আর sklearn নাম: PyPI-তে sklearn নামটি আসল package নয় (আসল নাম scikit-learn; পুরোনো sklearn নামটি এখন ইচ্ছা করে error দেয়)। প্রতিকার: uv add scikit-learn pandas, uv.lock commit.
    • ব্যক্তিগত ডেটা: orders.xlsx-এ ক্রেতার নাম-ফোন থাকলে Git-এ তোলা ঝুঁকি; notebook-এর output-এও থাকতে পারে। প্রতিকার: শনাক্তকারী কলাম বাদ, output মুছে commit.
    • Seed আর log নেই, README নেই: কোন ফল কোন সেটিংয়ে, কেউ জানে না। প্রতিকার: params.json, JSONL log, চালানোর নির্দেশসহ README.
  3. আউটপুট অনুমান: নিচের কোড কী ছাপবে? (SHA-256("abc")-এর শুরু ba7816bf, এটুকু দেওয়া আছে।)
    import hashlib
    h = lambda s: hashlib.sha256(s.encode()).hexdigest()
    print(h("abc")[:8], h("abc") == h("abc "), len(h("")))
    ba7816bf False 64

    প্রথমটি দেওয়া ছাপের প্রথম ৮ অক্ষর। দ্বিতীয়টি False, কারণ "abc "-এর শেষে একটি space আছে — এক byte বেশি, তাই ছাপ সম্পূর্ণ আলাদা। CSV-তে লাইনের শেষে একটি বাড়তি space-ও তাই ডেটার version বদলে দেয়। তৃতীয়টি 64: খালি string-এরও SHA-256 আছে, আর ছাপের দৈর্ঘ্য ইনপুটের আকার যা-ই হোক, সবসময় 64টি hex অক্ষর (256 bit ÷ প্রতি অক্ষরে 4 bit).

  4. ছোট কোড: একটি ফাংশন check_data(path, expected) লিখুন, যা ফাইলের SHA-256 হিসাব করে প্রথম ১২ অক্ষর expected-এর সঙ্গে মেলাবে; না মিললে একটি স্পষ্ট বার্তাসহ ValueError তুলবে। বড় ফাইলেও যেন memory শেষ না হয়, সেদিকে খেয়াল রাখুন।
    import hashlib
    
    def check_data(path, expected):
        h = hashlib.sha256()
        with open(path, "rb") as f:                 # bytes, not text
            for chunk in iter(lambda: f.read(1 << 20), b""):
                h.update(chunk)                     # 1 MB at a time
        got = h.hexdigest()[:len(expected)]
        if got != expected:
            raise ValueError(f"{path}: expected {expected}, got {got}. "
                             "The data changed; check the log before training.")
        return got

    মূল বিষয় তিনটি। ফাইল "rb" (binary) মোডে খোলা, যাতে line ending বা encoding-এর রূপান্তর না হয়। পুরো ফাইল একবারে না পড়ে ১ MB করে পড়া (1 << 20 = 1048576), যাতে ৫০ GB ফাইলেও কাজ চলে; update বারবার ডাকলে ফল পুরো ফাইল একবারে দেওয়ার মতোই। আর ভুল হলে চুপ করে না থেকে training শুরুর আগেই থামানো। Python 3.11 থেকে hashlib.file_digest(f, "sha256") একই কাজ এক লাইনে করে। train.py-এর শুরুতে check_data("data/raw/orders.csv", "6dc23919dc72") ডাকলে ডেটা বদলালে training শুরুই হবে না।

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

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

ক্রমনতুন কম্পিউটারে একটি ML প্রকল্পের ফল পুনরুৎপাদনের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📥 git clone দিয়ে code, ছোট ডেটা আর lockfile আনা
  2. 🔒 uv sync --locked দিয়ে হুবহু একই environment বসানো
  3. 🧾 ডেটা ফাইলের SHA-256 log-এ লেখা ছাপের সঙ্গে মেলানো
  4. ⚙️ একই params.json (seed-সহ) দিয়ে training script চালানো
  5. ✅ নতুন metrics.json-কে commit করা ফাইলের সঙ্গে মেলানো

একটি ML ফল হুবহু ফিরে পেতে কোন চারটি জিনিস একই থাকা দরকার?

  • Code, কম্পিউটারের মডেল, ব্যবহারকারীর নাম আর তারিখ
  • Code, ডেটা, environment আর random seed
  • Notebook, chart, README আর metrics
  • Model ফাইল, accuracy, test set আর Python

কেউ pyproject.toml-এ একটি নতুন package যোগ করেছেন কিন্তু uv lock চালাননি। এখন uv sync --locked চালালে কী হবে?

  • নতুন package-সহ সবকিছু নীরবে বসে যাবে
  • নতুন package বাদ দিয়ে পুরোনো তালিকা অনুযায়ী বসাবে
  • Lockfile হালনাগাদ দরকার বলে error দিয়ে থামবে
  • pyproject.toml থেকে নতুন package মুছে দেবে

৫ KB-এর একটি CSV ফাইলের ৪০০টি সারির একটিতে একটি অঙ্ক 0 থেকে 1 করা হলো। ফাইলটির SHA-256-এর কী হবে?

  • ছাপ সম্পূর্ণ আলাদা হয়ে যাবে, প্রায় কোনো মিল ছাড়াই
  • ছাপের শুধু শেষ একটি অক্ষর বদলাবে
  • ছাপ একই থাকবে, কারণ পরিবর্তন খুব ছোট
  • ছাপের দৈর্ঘ্য এক অক্ষর বেড়ে যাবে

একটি দলের কাছে ১৮০ MB-এর একটি review ডেটা ফাইল আছে, যা প্রতি মাসে নতুন version পায়। কোন পদ্ধতি সবচেয়ে উপযুক্ত?

  • প্রতি মাসে ফাইলটি সরাসরি Git-এ commit করা
  • শেয়ার করা drive-এ reviews_v1, reviews_v2 নামে কপি রাখা
  • শুধু সর্বশেষ version রেখে পুরোনোগুলো মুছে ফেলা
  • DVC বা Git LFS দিয়ে Git-এ শুধু hash-যুক্ত pointer রাখা

একজন ভুল করে database password-সহ .env ফাইল একটি public repository-তে push করেছেন, তারপর পরের commit-এ ফাইলটি মুছে দিয়েছেন। এখন প্রথম কাজ কী?

  • কিছু না, কারণ ফাইলটি মুছে ফেলা হয়েছে
  • সঙ্গে সঙ্গে password বদলানো, তারপর .gitignore-এ যোগ ও ইতিহাস পরিষ্কার
  • শুধু repository-টি private করে দেওয়া
  • ফাইলের নাম বদলে .env.old রাখা

একই ডেটা আর model দিয়ে train_test_split(..., random_state=None) তিনবার চালিয়ে accuracy এলো 0.87, 0.87, 0.84. এর সবচেয়ে সঠিক ব্যাখ্যা কোনটি?

  • Model প্রতিবার আরও ভালো শিখছে
  • scikit-learn-এ বাগ আছে
  • প্রতিবার আলাদা সারি test set-এ গেছে, তাই ফল ওঠানামা করছে
  • ডেটা ফাইল প্রতিবার বদলে যাচ্ছে

ডেটা ফাইলের স্থায়ী “আঙুলের ছাপ” হিসেবে Python-এর নিজস্ব hash() ফাংশন ব্যবহার করা কেন ঠিক নয়?

  • string-এর hash() প্রতিবার Python চালু হলে আলাদা মান দিতে পারে
  • hash() খুব ধীর, বড় ফাইলে কাজ করে না
  • hash() শুধু সংখ্যার ওপর কাজ করে
  • hash() সবসময় 0 ফেরত দেয়

শব্দকোষ

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

মনে রাখুন

  1. একই code + একই ডেটা + একই environment + একই seed = একই result; প্রতিটির জন্য commit, hash, lockfile আর seed.
  2. ফোল্ডার: data/raw (কখনো হাতে বদলাবেন না), data/processed, notebooks, src, models, reports.
  3. uv.lock commit করুন, নতুন জায়গায় uv sync --locked; .venv Git-এ নয়।
  4. ডেটার ছাপ SHA-256; ছোট ডেটা Git-এ, বড় ডেটা DVC বা Git LFS-এ; secret ও ব্যক্তিগত ডেটা কখনো Git-এ নয়।
  5. প্রতিটি run-এর parameter, ডেটার hash, commit আর metrics log-এ; নতুন clone-এ পুরো পথ চালিয়ে প্রমাণ করুন।

পরের পর্বে: মডিউল ৩ শুরু — NumPy array. Python-এর list থেকে কেন সংখ্যার জন্য আলাদা array লাগে, shape আর dtype কী, আর গতির পার্থক্য কোথা থেকে আসে।

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

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