در این فصل چه یاد میگیری#
یک مدلِ کاملاً سالم را ذخیره میکنیم و در یک مفسرِ تازه بازش میکنیم. جوابی که میگیریم این است: AttributeError: Can't get attribute 'clean'. مدل خراب نیست — فایل فقط نصفِ چیزی است که مدل بود.
بعد همان مدل را بهشکلِ یک artifact میبندیم: کد، وزنها و پیکربندی در سه فایلِ جدا، با یک MANIFEST که اثرِ انگشتِ هر سه را دارد. آنوقت مفسرِ تازه هر ۵۰۰ پیشبینی را مو به مو تکرار میکند، و عوض کردنِ یک عدد در پیکربندی صفر بایت از وزنها را دست نمیزند.

آخر این فصل میتوانی:
- بگویی چرا ذخیرهٔ مدل با
pickleبهتنهایی یک تحویلِ ناقص است - کد، وزن و پیکربندی را از هم جدا کنی و هر سه را نسخه بدهی
MANIFESTبنویسی که بگوید کدام تغییر کدام فایل را عوض کرده- بازتولیدپذیریِ یک
artifactرا در مفسرِ تازه اثبات کنی، نه ادعا Dockerfileو فایلِ قفل را بهعنوانartifactبنویسی و بدونِ اجرا بسنجیشان
قبل از شروع#
از ترمِ ۱: بستهای که ماژول و تست و README تستشونده دارد. از ترمِ ۲: اثرِ انگشتِ محتوا و نرمالسازیِ متنِ فارسی. از ترمِ ۳: رجیستریِ مدل و پیوندِ مدل به داده و کد.
و دو چیزِ مشخص که ترمِ ۳ تحویلمان داد و از همین فصل بهکار میآیند. اولی artifact_sha است — اثرِ انگشتِ بایتهای مدل — که بخشِ ۳ آن را داخلِ MANIFEST کنارِ کد و پیکربندی میگذارد. دومی تصمیمنامهٔ فصلِ ۸ ترمِ ۳ است، و «شرطِ بازنگری»اش تنها چیزی است که میگوید این بسته اصلاً حق دارد مستقر شود یا نه. بستهای که نداند از کدام تصمیم آمده، برگشت هم ندارد.
از ژرفا ترمِ ۶ فصلِ ۶: یک سرویسِ HTTP با /healthz و batching. آن فصل نشان داد چطور مدل را سرو کنی؛ این ترم از جایی شروع میشود که آن فصل تمام کرد — چطور همان سرویس را ماهها زنده و قابلِ برگشت نگه داری.
💡 سلولِ راهاندازیِ ترم چه چیزی بهت میدهد: پیکرهٔ ترم ۱۶۰۰ تیکتِ پشتیبانیِ فارسی است در
TRAIN(۱۱۰۰ ردیف) وTEST(۵۰۰ ردیف)؛ هر ردیف یکdictبا کلیدهایticket_id،customer_id،channel،textوcategory.normalise(text)«ي» و «ك» عربی را فارسی میکند و فاصلهها را جمع.fit_model(rows, tfidf=False, ngram=(1,1), C=1.0)یک دستهبندِ آموزشدیده برمیگرداند — موضوعِ درس نیست، فقط باید کار کند — و متن را خودش نرمال میکند، پس هر کسی که سرو میکند هم باید همین کار را بکند.WORKهم پوشهٔ کارِ ترم روی دیسک است که با هر اجرا از نو ساخته میشود. هر نه فصلِ این ترم روی همینها سوارند و هیچکدام دوباره تعریف نمیشوند، پس عددهایت با کتاب و با فصلهای بعدی قابلِ مقایسه است.
📓 نوتبوک: نوتبوک این فصل را در Colab باز کن — همهٔ کدهای این فصل آماده و بهترتیب داخلش هست.
| اجرا | زمانِ تقریبی |
|---|---|
| کلِ نوتبوک روی CPU | حدودِ یک دقیقه |
| GPU | لازم نیست |
۱. مدلی که فقط در همین نوتبوک زنده است#
نرمالسازیِ متن را — همان «ي» و «ك» عربیِ ترمِ ۲ — بهعنوان یک مرحله داخلِ خودِ مدل میگذاریم. این کارِ تمیزی بهنظر میرسد: مدل خودش ورودی را آماده میکند و هیچکس نمیتواند فراموشش کند.
import pickle
import subprocess
import sys
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import FunctionTransformer
def clean(texts):
"""همان نرمالسازیِ ترمِ ۲، این بار بهعنوان یک مرحله داخلِ خودِ مدل."""
return [normalise(t) for t in texts]
naive = make_pipeline(FunctionTransformer(clean), CountVectorizer(),
LogisticRegression(max_iter=1000))
naive.fit([r["text"] for r in TRAIN[:800]], [r["category"] for r in TRAIN[:800]])
(WORK / "naive.pkl").write_bytes(pickle.dumps(naive))
print("در همین نوتبوک:", naive.predict(["رمز عبورم را فراموش کردم"])[0])
print("فایل نوشته شد:", (WORK / "naive.pkl").exists())
در همین نوتبوک: حساب کاربری
فایل نوشته شد: True
مدل کار میکند، فایل نوشته شد، هیچ خطایی هم نیامد. حالا همان فایل را در یک مفسرِ تازه باز کنیم — دقیقاً همان کاری که سرورِ تولید فردا میکند.
LOADER = r'''
import pickle, sys
from pathlib import Path
try:
model = pickle.loads(Path(sys.argv[1]).read_bytes())
print("بارگذاری شد:", model.predict(["رمز عبورم را فراموش کردم"])[0])
except Exception as exc:
print(f"{type(exc).__name__}: {str(exc).split(' on ')[0]}")
'''
(WORK / "loader.py").write_text(LOADER, encoding="utf-8")
done = subprocess.run([sys.executable, str(WORK / "loader.py"), str(WORK / "naive.pkl")],
capture_output=True, text=True, encoding="utf-8")
print("مفسرِ تازه →", done.stdout.strip())
مفسرِ تازه → AttributeError: Can't get attribute 'clean'
فایل سالم است. مدل سالم است. چیزی که نیست، clean است.
pickle تابع را ذخیره نمیکند؛ فقط نامش و آدرسش را ذخیره میکند: «تابعی به نامِ clean در ماژولِ __main__». در نوتبوکِ تو آن آدرس معنا دارد، در مفسرِ تازه نه.
و این یک باگِ pickle نیست، یک قاعدهٔ کلی است: هر چیزی که کد است باید بهشکلِ کد تحویل داده شود. فایلِ مدل فقط عددهاست.
🔧 اگر کار نکرد: پیامِ کاملی که پایتون میدهد این است:
AttributeError: Can't get attribute 'clean' on <module '__main__' (built-in)>. ما فقط تکهٔ اولش را چاپ کردیم چون آن پرانتزِ آخر روی هر محیطی فرق دارد. اگر پیامِ توModuleNotFoundError: No module named 'sklearn'بود، مسئله چیزِ دیگری است: مفسرِ زیرفرآیند همان مفسرِ نوتبوک نیست — همیشهsys.executableرا صدا بزن، نه رشتهٔ"python".
۲. سه چیزی که همیشه با هم قاطی میشوند#
یک مدلِ مستقرشده از سه جنسِ کاملاً متفاوت ساخته شده:
- ۱) کد — نرمالسازی، منطقِ آستانه، شکلِ خروجی. با
gitنسخه میخورد و باید خوانده شود. - ۲) وزنها — نتیجهٔ آموزش. بزرگ، دودویی، و به
gitنمیرود (ترمِ ۲ فصلِ ۵). - ۳) پیکربندی — آستانه، نامِ برچسبِ پشتیبان، سقفِ طولِ ورودی. کوچک، متنی، و بدونِ آموزشِ دوباره عوض میشود.
artifact چیزی جز «این سه تا، کنارِ هم، با شماره» نیست.
import hashlib
import json
import platform
import sklearn
ART = WORK / "artifact-1.0.0"
ART.mkdir()
model = fit_model(TRAIN[:800]) # همان مدل، بدونِ مرحلهٔ کدیِ داخلش
(ART / "model.pkl").write_bytes(pickle.dumps(model))
(ART / "config.json").write_text(json.dumps({
"version": "1.0.0",
"normalise": True,
"min_confidence": 0.0,
"fallback_label": "نامشخص",
}, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
(ART / "serve.py").write_text(r'''import json
import pickle
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
CONFIG = json.loads((HERE / "config.json").read_text(encoding="utf-8"))
def normalise(text):
return " ".join(text.replace("ي", "ی").replace("ك", "ک").split())
def load():
return pickle.loads((HERE / "model.pkl").read_bytes())
def predict(model, texts):
if CONFIG["normalise"]:
texts = [normalise(t) for t in texts]
proba = model.predict_proba(texts)
labels = list(model.classes_)
out = []
for row in proba:
best = int(row.argmax())
out.append(labels[best] if row[best] >= CONFIG["min_confidence"]
else CONFIG["fallback_label"])
return out
if __name__ == "__main__":
texts = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
answers = predict(load(), texts)
Path(sys.argv[2]).write_text(json.dumps(answers, ensure_ascii=False), encoding="utf-8")
print(f"نسخه={CONFIG['version']} تعداد={len(answers)}")
''', encoding="utf-8")
print(sorted(p.name for p in ART.iterdir()))
['config.json', 'model.pkl', 'serve.py']
به serve.py دقت کن: normalise این بار داخلِ فایلِ کد است، نه داخلِ pickle. همان تابع، همان رفتار — ولی حالا چیزی است که مفسرِ تازه میتواند بخواند.
و config.json سه چیزِ متفاوت را نگه میدارد: normalise یک قرارداد است (مدل با متنِ نرمالشده آموزش دیده، پس سرو هم باید نرمال کند)، min_confidence یک اهرم است، و version هویتِ بسته.
۳. MANIFEST — اثرِ انگشتِ هر چیزی که عوض میشود#
def sha(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def write_manifest():
files = sorted(p.name for p in ART.iterdir()
if p.is_file() and p.name != "MANIFEST.json")
manifest = {
"version": json.loads((ART / "config.json").read_text(encoding="utf-8"))["version"],
"files": {name: sha(ART / name)[:12] for name in files},
"built_with": {"python": platform.python_version(),
"scikit-learn": sklearn.__version__},
}
(ART / "MANIFEST.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
return manifest
first = write_manifest()
print("نسخه :", first["version"])
print("فایلها :", sorted(first["files"]))
print("هشِ serve.py:", first["files"]["serve.py"])
نسخه : 1.0.0
فایلها : ['config.json', 'model.pkl', 'serve.py']
هشِ serve.py: f14e6250ae23
سه هش، سه فایل. هشِ model.pkl را عمداً چاپ نکردیم، چون به نسخهٔ کتابخانهای که مدل را ساخته بستگی دارد و روی ماشینِ تو عددِ دیگری است. serve.py یک فایلِ متنی است و همهجا همان هش را میدهد.
و همان هشِ چاپنشده، دقیقاً artifact_shaی رجیستریِ ترمِ ۳ فصلِ ۷ است. آنجا نامِ فایل در انبار بود؛ اینجا یک سطر از MANIFEST. یک عدد، دو جا — و همین است که اجازه میدهد بگویی بستهای که روی سرور نشسته، همان نسخهای است که در رجیستری production علامت خورده.
سؤالی که MANIFEST جوابش را میدهد این نیست که «هش چند است»؛ این است که «چه چیزی عوض شد».
۴. آزمونِ واقعی: مفسرِ تازه، همان ۵۰۰ جواب#
texts = [r["text"] for r in TEST]
truth = [r["category"] for r in TEST]
(WORK / "in.json").write_text(json.dumps(texts, ensure_ascii=False), encoding="utf-8")
def call_artifact():
"""artifact را از بیرون صدا میزند — همانطور که سرورِ تولید صدایش میزند."""
done = subprocess.run([sys.executable, str(ART / "serve.py"),
str(WORK / "in.json"), str(WORK / "out.json")],
capture_output=True, text=True, encoding="utf-8")
return done.stdout.strip(), json.loads((WORK / "out.json").read_text(encoding="utf-8"))
def accuracy(answers):
return sum(a == t for a, t in zip(answers, truth)) / len(truth)
here = list(model.predict([normalise(t) for t in texts]))
banner, there = call_artifact()
print("خروجیِ مفسرِ تازه:", banner)
print(f"دقت در نوتبوک : {accuracy(here):.4f}")
print(f"دقت از artifact : {accuracy(there):.4f}")
print("۵۰۰ پیشبینی مو به مو یکی؟", here == there)
خروجیِ مفسرِ تازه: نسخه=1.0.0 تعداد=500
دقت در نوتبوک : 0.8660
دقت از artifact : 0.8660
۵۰۰ پیشبینی مو به مو یکی؟ True
همان بسته، دو مفسرِ متفاوت، پانصد جوابِ یکسان. این همان چیزی است که بخشِ ۱ نداشت.
✅ چک کن: اگر «مو به مو یکی» برایت
Falseشد، همان لحظه بایست و دنبالِ چیزی بگرد که فقط در حافظهٔ نوتبوک زندگی میکند. رایجترین سه مورد: یکseedکه در سلولِ بالاتر نشسته، یک متغیرِ سراسری کهserve.pyاز آن بیخبر است، و ترتیبِ ورودیها. این تفاوت هیچوقت خودبهخود ناپدید نمیشود؛ فقط تا روزِ بدی صبر میکند.
۵. پیکربندی یک اهرم است، نه بخشی از وزنها#
حالا میخواهیم سرویس وقتی مطمئن نیست جواب ندهد و تیکت را به آدم بسپارد. این تصمیمِ محصول است، نه تغییرِ مدل — پس نباید یک بایت از وزنها عوض شود.
def rebuild_config(**changes):
cfg = json.loads((ART / "config.json").read_text(encoding="utf-8"))
cfg.update(changes)
(ART / "config.json").write_text(
json.dumps(cfg, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
return write_manifest()
def report(threshold):
_, answers = call_artifact()
unknown = sum(a == "نامشخص" for a in answers)
answered = [(a, t) for a, t in zip(answers, truth) if a != "نامشخص"]
hit = sum(a == t for a, t in answered) / len(answered)
print(f"min_confidence={threshold:.2f} · نامشخص {unknown:>3}"
f" · دقت روی جوابدادهها {hit:.4f}")
report(0.0)
after = rebuild_config(min_confidence=0.80)
report(0.80)
print("وزنها عوض شد؟ ", first["files"]["model.pkl"] != after["files"]["model.pkl"])
print("پیکربندی عوض شد؟", first["files"]["config.json"] != after["files"]["config.json"])
print("کد عوض شد؟ ", first["files"]["serve.py"] != after["files"]["serve.py"])
min_confidence=0.00 · نامشخص 0 · دقت روی جوابدادهها 0.8660
min_confidence=0.80 · نامشخص 92 · دقت روی جوابدادهها 0.9485
وزنها عوض شد؟ False
پیکربندی عوض شد؟ True
کد عوض شد؟ False
۹۲ تیکت به آدم رفت و دقت روی بقیه از ۰٫۸۶۶۰ به ۰٫۹۴۸۵ رسید — بدونِ یک بایت تغییر در وزنها.
این معاملهٔ همیشگیِ آستانه است: پوششِ کمتر در برابرِ دقتِ بیشتر. نکتهٔ استقرارش این است که این معامله را میشود ساعتِ سه بامداد با یک فایلِ متنی عوض کرد — بدونِ آموزشِ دوباره، بدونِ انتقالِ صد مگابایت وزن، و با یک MANIFEST که دقیقاً میگوید چه چیزی عوض شده.
📏 اندازه بگیر: با چه چیزی مقایسه شد؟ با همان
artifactو همان وزنها، فقط با یک عددِ متفاوت درconfig.json. روی کدام داده؟ هر ۵۰۰ ردیفِ آزمون، در هر دو حالت. با چندseed؟ یکی — و اینجا کافی است، چون ادعا «بهتر شد» نیست؛ ادعا این است که همان مدل با آستانهٔ متفاوت این دو عدد را میدهد، و آن یک محاسبهٔ قطعی روی خروجیِ ثابتِ مدل است، نه یک آزمایشِ تصادفی.
۶. سنجاقِ نسخه — و هشداری که کسی نادیدهاش نگیرد#
MANIFEST نسخهٔ محیطِ ساخت را ثبت کرده. حالا بگذار بارگذاری خودش بررسی کند.
def compatibility(manifest):
live = {"python": platform.python_version(), "scikit-learn": sklearn.__version__}
return sorted(k for k, v in manifest["built_with"].items() if live[k] != v)
print("با همان محیط :", compatibility(after))
faked = json.loads(json.dumps(after))
faked["built_with"]["scikit-learn"] = "0.0.0"
print("با محیطِ متفاوت :", compatibility(faked))
با همان محیط : []
با محیطِ متفاوت : ['scikit-learn']
یک فهرستِ خالی یعنی «همان محیط». فهرستِ ناخالی یعنی بایست و تصمیم بگیر — نه اینکه لود کن و امیدوار باش.
چرا هشدار و نه خطا؟ چون گاهی واقعاً باید با نسخهٔ تازهتر اجرا کنی و راهی جز آن نداری. ولی تفاوت باید در سیاهه دیده شود، وگرنه سه ماه بعد که جوابی عجیب میشود، هیچکس نمیداند محیط عوض شده بود.
import importlib.metadata
PINNED = ["scikit-learn", "numpy", "scipy"]
lines = [f"{name}=={importlib.metadata.version(name)}" for name in PINNED]
(ART / "requirements.txt").write_text("\n".join(lines) + "\n", encoding="utf-8")
print((ART / "requirements.txt").read_text(encoding="utf-8").strip())
scikit-learn==1.7.2
numpy==2.4.6
scipy==1.17.1
نسخهها را از محیطِ زنده میخوانیم، نه از حافظه — همان قاعدهٔ ترمِ ۱ فصلِ ۹. عددهای بالا روی ماشینِ تو فرق میکنند و نباید با کتاب یکی باشند.
۷. چه چیزی داخلِ artifact نیست#
داده. یک artifact هرگز دادهٔ آموزش را با خودش حمل نمیکند — بزرگ است، خصوصی است، و نسخهبندیاش کارِ ترمِ ۲ بود. آنچه حمل میشود اثرِ انگشتِ داده است: یک رشته که میگوید «این وزنها از آن نسخهٔ داده آمدهاند».
| سؤال هنگامِ استقرار | کجای artifact جوابش است |
|---|---|
| این کدام مدل است؟ | version در config.json و MANIFEST |
| چطور صدایش بزنم؟ | serve.py — کد، نه توضیح |
| با چه محیطی ساخته شده؟ | built_with + requirements.txt |
| چه چیزی از دیروز عوض شده؟ | مقایسهٔ هشِ MANIFEST |
| آستانه را کجا عوض کنم؟ | config.json، بدونِ آموزشِ دوباره |
| از کدام داده آمده؟ | اثرِ انگشتِ داده (ترمِ ۲ فصلِ ۵) |
۸. چیزی که requirements.txt نمیدهد#
آن فایل سه خط دارد و سه چیز را نمیگوید: با کدام نسخهٔ پایتون، روی کدام سیستمعامل، و با کدام وابستگیِ وابستگیها. pip install -r requirements.txt امروز و شش ماهِ دیگر دو محیطِ متفاوت میسازد، چون هر بستهٔ غیرسنجاقشدهای که زیرِ این سهتا بنشیند آزاد است تازهتر شود.
دو artifact دیگر این شکاف را پر میکنند: یک Dockerfile که سیستمعامل و نسخهٔ پایتون را هم سنجاق میکند، و یک فایلِ قفل که کلِ درختِ وابستگی را مینویسد، نه فقط چیزی که خودت خواستهای.
import re
def closure(names):
"""کلِ درختِ وابستگی: چیزی که واقعاً نصب شده، نه چیزی که خواستهای."""
seen, stack = {}, list(names)
while stack:
name = stack.pop()
key = name.lower().replace("_", "-")
if key in seen:
continue
try:
seen[key] = importlib.metadata.version(name)
except importlib.metadata.PackageNotFoundError:
continue
for req in importlib.metadata.requires(name) or []:
if "extra ==" in req: # وابستگیِ اختیاری نصب نشده
continue
stack.append(re.split(r"[<>=!;\[ ]", req, maxsplit=1)[0])
return dict(sorted(seen.items()))
LOCK = closure(PINNED)
(ART / "requirements.lock").write_text(
"".join(f"{name}=={version}\n" for name, version in LOCK.items()),
encoding="utf-8")
DOCKERFILE = "\n".join([
f"FROM python:{platform.python_version()}-slim", # نسخه از محیطِ زنده، نه دستی
"WORKDIR /app",
"COPY requirements.lock .",
"RUN pip install --no-deps -r requirements.lock",
"COPY . /app/artifact",
'CMD ["python", "/app/artifact/serve.py"]',
]) + "\n"
(ART / "Dockerfile").write_text(DOCKERFILE, encoding="utf-8")
print("مستقیم خواستهایم:", len(PINNED), "بسته · قفل شد:", len(LOCK), "بسته")
print(DOCKERFILE.rstrip())
مستقیم خواستهایم: 3 بسته · قفل شد: 5 بسته
FROM python:3.13.2-slim
WORKDIR /app
COPY requirements.lock .
RUN pip install --no-deps -r requirements.lock
COPY . /app/artifact
CMD ["python", "/app/artifact/serve.py"]
سه بسته خواستیم و پنجتا قفل شد — و همان دوتای اضافهاند که یک روز بیخبر عوض میشوند و جوابِ مدل را جابهجا میکنند. عددها روی ماشینِ تو فرق میکنند و نباید با کتاب یکی باشند؛ چیزی که همهجا برقرار است بزرگتر بودنِ قفل از خواسته است.
و حالا صادقترین بخشِ این فصل: نمیتوانیم این Dockerfile را اجرا کنیم. ساختِ image به یک daemonِ Docker نیاز دارد که در runtimeِ Colab وجود ندارد. پس کاری میکنیم که همیشه شدنی است: خودِ فایل را بهعنوان artifact میسنجیم — دقیقاً همان کاری که یک دروازهٔ بیلد قبل از هر docker build انجام میدهد.
def audit_runtime(art):
docker = (art / "Dockerfile").read_text(encoding="utf-8").splitlines()
lock = (art / "requirements.lock").read_text(encoding="utf-8").split()
direct = {line.split("==")[0]
for line in (art / "requirements.txt").read_text(encoding="utf-8").split()}
base = next(line for line in docker if line.startswith("FROM "))
return [
("نسخهٔ پایه latest نیست", ":latest" not in base and ":" in base),
("نسخهٔ پایه با digest سنجاق شده", "@sha256:" in base),
("نصب از فایلِ قفل است، نه requirements", "requirements.lock" in " ".join(docker)),
("وابستگیِ ناخواسته اضافه نمیشود", "--no-deps" in " ".join(docker)),
("هر خطِ قفل نسخهٔ دقیق دارد", all("==" in line for line in lock)),
("هر بستهٔ مستقیم در قفل هست", direct <= {line.split("==")[0] for line in lock}),
]
rows = audit_runtime(ART)
for name, passed in rows:
print(f" {'سبز ' if passed else 'قرمز'} {name}")
print(f"{sum(p for _, p in rows)} از {len(rows)} بند سبز")
سبز نسخهٔ پایه latest نیست
قرمز نسخهٔ پایه با digest سنجاق شده
سبز نصب از فایلِ قفل است، نه requirements
سبز وابستگیِ ناخواسته اضافه نمیشود
سبز هر خطِ قفل نسخهٔ دقیق دارد
سبز هر بستهٔ مستقیم در قفل هست
5 از 6 بند سبز
آن یک بندِ قرمز عمدی است و مهمترین بندِ جدول. python:3.13-slim یک برچسب است و برچسبها جابهجا میشوند: همان اسم، ماهِ بعد، یک image دیگر. سنجاقِ واقعی @sha256:... است — و آن digest را فقط از خروجیِ docker pull روی ماشینِ خودت میگیری، پس اینجا نمیشود ساختش. جدولی که بندِ قرمزِ خودش را نشان بدهد از جدولِ همهسبز مفیدتر است.
⚠️ مواظب باش: این بخش دو فایل نوشت و هیچکدام را اجرا نکرد. یک
Dockerfileکه هرگز ساخته نشده باشد، دقیقاً همانقدر ادعا است که یکREADMEتستنشدهٔ ترمِ ۱. این تنها جای دوره است که نمیشود ادعا را همانجا با عدد بست — و تمرینِ اجباریِ همین فصل کارش را به ماشینِ خودت میسپارد.
🤖 از دستیارت بپرس: «چرا فرمتهایی مثلِ
ONNXوsafetensorsساخته شدند و چه چیزی را ازpickleمیگیرند؟» جوابِ درست شاملِ اجرای کدِ دلخواه هنگامِ بارگذاری است — یک فایلِpickleاز یک منبعِ ناشناس دقیقاً به همان اندازهٔ یک اسکریپتِ ناشناس خطرناک است. بعد این را بپرس: «اگرartifactمن فقط وزن باشد و کدش جای دیگری نسخه بخورد، چطور مطمئن شوم این دو با هم جفتاند؟» — جوابش همانMANIFESTاست و فصلِ بعد رویش حساب میکند.
واژههای تازهٔ این فصل#
| کلمه | تلفظ به حروف فارسی | یعنی چه |
|---|---|---|
| artifact | آرتیفکت | بستهٔ تحویلیِ یک مدل: کد، وزن، پیکربندی، با شماره |
| manifest | مانیفست | فهرستِ محتویاتِ بسته با اثرِ انگشتِ هر فایل |
| pinning | پینینگ | ثبتِ نسخهٔ دقیقِ وابستگیها |
| serialization | سریالایزیشن | تبدیلِ یک شیء در حافظه به بایتهای قابلِ ذخیره |
| runtime | رانتایم | محیطی که کد واقعاً در آن اجرا میشود |
| confidence threshold | کانفیدنس ترشهولد | کمینهٔ اطمینانی که زیرش سرویس جواب نمیدهد |
تمرینها
اول خودت فکر کن یا امتحان کن — بعد اینجا را باز کن.
در فصل بعد#
بستهات آماده است ولی هنوز کسی نمیتواند صدایش بزند. فصلِ بعد آن را پشتِ یک سرویس میگذارد و سراغِ سختترین قسمتش میرود: قرارداد. یک تغییرِ کوچک در شکلِ پاسخ میدهیم و میشماریم چند تماسِ کلاینتِ قدیمی میشکند — ۴۰ از ۴۰ — و بعد همان تغییر را طوری منتشر میکنیم که صفر بشکند.
به آخر این فصل رسیدی!
اگر ساختی و جواب داد، این دکمه مال توست.