در فرآیند توسعه و تیونینگ مدلهای یادگیری ماشین، دیدن یک عدد بهتر در خروجی اجرا همیشه به معنای ارتقای واقعی مدل نیست. نوسانات ناشی از تغییر سیدهای تصادفی (seeds) و نویزهای محیطی میتوانند بهسادگی تغییرات تصادفی را به جای بهبود واقعی جا بزنند. برای اتوماسیون تصمیمگیری و جلوگیری از پیشروی بر اساس توهم آماری، نیازمند سازوکاری دقیق هستیم که خروجیهای خام را تحلیل کند.
در این درس یاد میگیرید چگونه یک ارزیاب نتایج (result evaluator) قطعی و خالص بسازید که آزمایشهای نسخهی کاندید را در برابر خط مبنا مقایسه کند. با پیادهسازی آزمون تی جفتشده (paired t-test) بدون وابستگی به بستههای سنگین خارجی و با بهکارگیری نرمالسازی مقیاسهای لگاریتمی، خروجیهای چندگانهی آزمایشها را به یک رأی آماری شفاف و یکخطی تبدیل میکنید.
رانر (runner) اعداد را تولید کرد. حالا ارزیاب باید تصمیم بگیرد که آیا این اعداد نشاندهندهی بهبودند، پسرفت، یا صرفاً نویز. در این درس، مسیر صدور رأی (verdict path) را میسازیم؛ مسیری که این معیارها را میگیرد و به یک جمعبندی یکخطی تبدیل میکند.
اهداف یادگیری
- مقایسهی اجرای کاندید (candidate run) با خط مبنا با در نظر گرفتن جهت مطلوب تغییرات و یک آستانهی ثابت.
- پیادهسازی آزمون تی جفتشده (paired t-test) از صفر روی معیارهای بهدستآمده از هر سید (seed) و خواندن p-value حاصل.
- نرمالسازی معیارهای با مقیاس لگاریتمی، تا گزارشهای بعدی بتوانند آنها را در کنار معیارهای خطی ترکیب کنند.
- صدور رأی برای هر فرضیه، بهگونهای که هماهنگکننده بتواند آن را به صف طراحیشده در درس پنجاه ضمیمه کند.
- خالص نگهداشتن همهی گامها بهصورت تابع خالص، تا ورودیهای یکسان همیشه رأیی کاملاً یکسان تولید کنند.
چرا آزمون جفتشده؟
یک عددِ تنها از سمت رانر مشخص نمیکند که آیا تغییر ایجادشده واقعی است یا نه. همان پیکربندی با یک سید متفاوت، پرپلکسیتی متفاوتی به دست میدهد؛ بنابراین شاید تغییر مشاهدهشده صرفاً نویز باشد. مقایسهی درست، یک مقایسهی جفتشده است: همان سیدها با همان دادهها، یک بار با کاندید و یک بار با خط مبنا اجرا میشوند. هر سید یک اختلاف تولید میکند. میانگین این اختلافها اثر واقعی تغییر است و خطای استاندارد آنها کف نویز را مشخص میکند.
در این درس، این آزمون را کاملاً از صفر پیاده میکنیم؛ بدون هیچ وابستگی به scipy.stats. کل محاسبات ریاضی آنقدر جمعوجور است که در یک صفحه جا میشود.
diffs = [a_i - b_i for i in seeds]
mean = sum(diffs) / n
variance = sum((d - mean) ** 2 for d in diffs) / (n - 1)
t_stat = mean / sqrt(variance / n)
df = n - 1
p_value = two_sided_p(t_stat, df)
محاسبهی p-value دوطرفه از تابع بتای ناکامل منظمشده (regularised incomplete beta function) استفاده میکند. در این درس پیادهسازی جمعوجوری از آن را با استفاده از کسر مسلسل لنتز (Lentz continued fraction) ارائه میدهیم. کل این بخش در 60 خط و فقط با توابع ریاضی کتابخانهی استاندارد پایتون پیاده شده است.
بهبود حساس به جهت
برخی معیارها با افزایش بهبود مییابند (دقت، نرخ پردازش) و برخی با کاهش (تابع زیان (loss)، پرپلکسیتی، زمان اجرا). ارزیاب برای هر معیار یک فیلد direction نگه میدارد.
if direction == "higher_is_better":
improvement = (candidate - baseline) / abs(baseline)
elif direction == "lower_is_better":
improvement = (baseline - candidate) / abs(baseline)
مقدار بهبود علامتدار است. بهبود منفی در معیاری که «بیشترش بهتر است»، یعنی نسخهی کاندید ضعیفتر شده است. مسیر صدور رأی علامت و اندازهی عددی را همزمان بررسی میکند.
یک آستانهی ثابت (improvement_threshold=0.02 یا همان 2 درصد) مشخص میکند که آیا تغییر به اندازهی کافی بزرگ هست که آن را به حساب بیاوریم یا نه. کمتر از این مقدار، بدون در نظر گرفتن p-value، رأی «نویز» صادر میشود؛ حلقهی ارزیابی علاقهای به تغییراتی ندارد که برای کاربر ملموس نیستند.
معماری
ارزیاب سه محاسبهی کاملاً مستقل از هم را اجرا میکند و نتایج آنها را در مسیر صدور رأی به هم پیوند میدهد. هر محاسبه یک تابع خالص است، بدون هیچ وضعیت مشترک.
نرمالسازی لگاریتمی
پرپلکسیتی نسبت به تابع زیان (loss) رفتاری نمایی دارد. کاهش 0.1 واحدی در تابع زیان، افتی بهمراتب شدیدتر در پرپلکسیتی ایجاد میکند. مقایسهی مستقیم پرپلکسیتی بین دو پیکربندی ایرادی ندارد، اما ترکیب آن با معیارهای خطی در یک گزارش واحد نیازمند نرمالسازی است.
در این درس، هر معیاری را که فیلد scale آن روی "log" تنظیم شده باشد، با گرفتن لگاریتم طبیعی پیش از محاسبهی بهبود نرمال میکنیم. سپس آستانه را در فضای لگاریتمی اعمال میکنیم. افت پرپلکسیتی از 32 به 28 تبدیل میشود به log(32) = 3.466 در برابر log(28) = 3.332. بهبود نسبی برای معیاری که «کمترش بهتر است» برابر میشود با (3.466 - 3.332) / 3.466 = 0.039، که از آستانهی 2 درصدی عبور میکند.
if scale == "log":
a = log(candidate)
b = log(baseline)
else:
a = candidate
b = baseline
معیارهایی با scale="linear" (حالت پیشفرض) از این تبدیل صرفنظر میکنند. یک مسیر کد واحد هر دو حالت را مدیریت میکند.
آزمون جفتشده به ازای هر سید
رانرِ درس پنجاهودو برای هر اجرا یک شیء نهایی از معیارها تحویل میدهد. برای آزمون جفتشده، ارزیاب به یک شیء به ازای هر سید برای کاندید و یکی به ازای هر سید برای خط مبنا نیاز دارد. هماهنگکننده آزمایش یکسانی را تحت هر دو پیکربندی روی فهرستی از سیدها اجرا میکند و دو فهرست از رکوردهای ExperimentResult را به ارزیاب میسپارد.
ارزیاب آنها را بر اساس سید جفت میکند (سید داخل result.metrics["seed"] قرار دارد) و روی معیار خواستهشده پیش میرود. اگر سیدهای این دو فهرست با هم نخوانند، ارزیاب خطای PairingError صادر میکند؛ در این صورت هماهنگکننده باید آزمایش را دوباره اجرا کند.
ساختار رأی نهایی
Verdict
hypothesis_id : int
metric : str
direction : "higher_is_better" | "lower_is_better"
scale : "linear" | "log"
candidate_mean : float
baseline_mean : float
improvement : float (signed, fraction; see direction rules)
p_value : float | None (None if n < 2)
significance_threshold : float
improvement_threshold : float
verdict : "improved" | "regressed" | "noise" | "failed"
rationale : str
مسیر صدور رأی یک جدول تصمیمگیری جمعوجور است:
1. If any candidate result has terminal != "ok": verdict = "failed"
2. else if |improvement| < improvement_threshold: verdict = "noise"
3. else if p_value is None or p_value > significance: verdict = "noise"
4. else if improvement > 0: verdict = "improved"
5. else: verdict = "regressed"
فیلد Rationale یک توضیح یکخطی و خوانا برای انسان است که هماهنگکننده میتواند آن را جلوی شناسهی فرضیه لاگ کند.
راهنمای خواندن کد
فایل code/main.py کلاسهای MetricSpec، Verdict و Evaluator را به همراه توابع کمکی آمارهی t و بتای ناکامل و یک دموی قطعی تعریف میکند. آزمون تی کاملاً با توابع ریاضی کتابخانهی استاندارد پیاده شده است؛ از numpy صرفاً برای خواندن فهرست معیارها و محاسبهی میانگینها و واریانسها استفاده میشود.
فایل code/tests/test_evaluator.py این بخشها را پوشش میدهد: مسیر بهبودیافته، مسیر پسرفت، مسیر نویز (بهبود ناچیز)، مسیر نویز (تعداد نمونه یا n پایین)، مسیر شکست نهایی، مسیر نرمالسازی لگاریتمی، آزمون تی در برابر یک مقدار مرجع مشخص، و خطای عدم تطابق سیدها.
جایگاه این بخش در سیستم کلی
درس پنجاه صف فرضیهها را ساخت. درس پنجاهویک مواردی را که در ادبیات پژوهش به نتیجه رسیده بودند فیلتر کرد. درس پنجاهودو آزمایش را در هر دو پیکربندی کاندید و خط مبنا روی چند سید اجرا کرد. درس پنجاهوسه این خروجیها را میخواند و رأی نهایی را صادر میکند. هماهنگکننده این چهار بخش را به هم وصل میکند:
for hypothesis in queue:
literature = retrieval.search(hypothesis.text)
if literature_settles(hypothesis, literature):
attach(hypothesis, verdict="settled")
continue
candidates = runner.run_all(specs_for(hypothesis))
baselines = runner.run_all(baseline_specs_for(hypothesis))
metric_spec = MetricSpec("perplexity", direction=LOWER, scale=LOG)
verdict = evaluator.evaluate(hypothesis.id, metric_spec, candidates, baselines)
attach(hypothesis, verdict)
پیادهسازی خود این هماهنگکننده در این درس نیامده است؛ این چهار درس طوری طراحی شدهاند که بدون هیچ کد واسط اضافی، و فقط با تکیه بر dataclassهای تعریفشده در هر کدام، کاملاً با یکدیگر ترکیب میشوند.
