الوحدة 2 — إمكانية التكرار: البذور والبيئات وتثبيت الإصدارات
نستهدف في هذه الوحدة سؤالًا واحدًا محدَّدًا: هل يستطيع مهندس آخر، على جهاز آخر، بعد ستّة أشهر، إعادة إنتاج نفس النموذج بالضبط؟ إن كان الجواب لا، فكلّ ما نبنيه فوق ذلك من مراقبة ومقارنة ورَجْعٍ إلى الوراء يفقد معناه.
البذور في كلّ مكان، وليس واحدة فقط
لاحتميّة التدريب تأتي من عدّة مولّدات عشوائية مستقلّة، ولا يكفي تثبيت واحد منها. في مثال دفتر التسرّب الذي نبنيه، نحتاج إلى تثبيت أربعة على الأقلّ:
import os
import random
import numpy as np
import torch
SEED = 42
random.seed(SEED)
np.random.seed(SEED)
torch.manual_seed(SEED)
torch.cuda.manual_seed_all(SEED)
os.environ["PYTHONHASHSEED"] = str(SEED)
نُثبّت أيضًا PYTHONHASHSEED لأنّ ترتيب المرور في القواميس والمجموعات يعتمد عليه، وهذا يؤثّر مثلًا على ترتيب المتغيّرات المُنتَخَبة عند التساوي. أمّا scikit-learn فيقبل عادةً معلمة random_state صريحة على كلّ مقدِّر: نمرّرها دائمًا بدلًا من الاعتماد على البذرة الشاملة.
نقطة مغفولة كثيرًا: عيّنة القطار/الاختبار (train_test_split) هي أوّل مصدر للاختلاف. لو نسينا random_state هنا، فإنّ نموذجين مُدرَّبين على «نفس البيانات» يستعملان في الواقع تقسيمين مختلفين، وأيّ مقارنة بينهما تصبح بلا معنى.
تثبيت المكتبات: requirements.txt ليس كافيًا
الملفّ التالي شائع لكنّه ضعيف:
scikit-learn
pandas
numpy
هذه ليست تثبيتًا: كلّ pip install يجلب أحدث نسخة متوفّرة. الحلّ الأدنى:
scikit-learn==1.5.2
pandas==2.2.3
numpy==1.26.4
لكن حتّى هذا لا يكفي، لأنّ scikit-learn==1.5.2 تعتمد على scipy، وscipy تعتمد على numpy، وكلّ اعتماد غير مُثبَّت هو نافذة إلى نموذج مختلف. الحلّ الصحيح: pip freeze > requirements.txt بعد تثبيت البيئة، أو استعمال أدوات معجم مثل poetry أو pip-tools التي تُنتج ملفّ قفل (lock file) يُحدّد النسخة الدقيقة لكلّ اعتماد مباشر وغير مباشر.
في الخيط الأحمر لدينا، نُنشئ في المستودع ملفّ pyproject.toml مع اعتماداتنا العليا، ونُلحقه بملفّ poetry.lock مُلتزَم به. من يستنسخ المستودع يُعيد نفس البيئة بأمر واحد: poetry install.
حاوية التدريب: التغلّب على النظام
حتّى بيئة Python مُقفَلة لا تحمي من الاختلافات على مستوى نظام التشغيل. مكتبات مثل xgboost تربط شفرة C++ منسوخة بإصدار glibc مُعيّن؛ والمعالج قد يمنح تعليمات متجهيّة (AVX-512) على خادم لا تتوفّر على حاسوب المُطوِّر. الجواب: حاوية Docker للتدريب أيضًا، لا للنشر فقط.
FROM python:3.11-slim@sha256:abcdef...
WORKDIR /app
COPY pyproject.toml poetry.lock ./
RUN pip install poetry==1.8.3 && poetry install --no-root
COPY src/ src/
CMD ["python", "-m", "src.train"]
نلاحظ ثلاث نقاط: تثبيت النسخة بـsha256 لصورة الأساس (وليس فقط python:3.11-slim الذي يتغيّر بصمت)، وتثبيت نسخة poetry نفسها، ونسخ الشفرة بعد تثبيت الاعتماديات لاستفادة قصوى من ذاكرة البناء (build cache).
مصادر اللاحتميّة على GPU
هنا يبدأ الجزء الصعب. حتّى مع تثبيت كلّ البذور، لا تُنتج شبكة PyTorch على GPU نفس النموذج بالضبط عند إعادة التشغيل. الأسباب معروفة لكنّها نادرًا ما تُشرح:
- بعض عمليات cuDNN (الالتفاف، مثلًا) لها عدّة خوارزميات، ويختار cuDNN تلقائيًا الأسرع؛ وهذا الاختيار قد يتغيّر بين تشغيلين.
- بعض العمليات على GPU غير حتميّة أصلًا:
scatter_addمثلًا يجمع في ترتيب يعتمد على الجدولة. - الحسابات المتوازية بدقّة عائمة (float) لا تعطي نفس النتيجة بالضبط عند تغيّر ترتيب الجمع.
للحصول على تدريب حتميّ نضبط:
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
torch.use_deterministic_algorithms(True)
ثمن ذلك: تدريب أبطأ بنسبة 10 إلى 30 بالمائة، وأخطاء صريحة عند استعمال عملية غير حتميّة (وهو ما نريده). لا نُفعّل هذا في التدريب الاستكشافي، لكنّنا نفعّله للنماذج التي ستدخل الإنتاج.
الحتميّة الكاملة على GPU مُكلفة. البديل المقبول هو إمكانية تكرار إحصائية: نُدرّب النموذج ثلاث مرّات ببذور مختلفة، ونتحقّق من أنّ الأداء يتذبذب في هامش صغير (±0,3 نقطة AUC مثلًا). إن لم يكن كذلك، فالتدريب غير مستقرّ ونحتاج إلى تشخيص أعمق.
في الخلاصة
- لا تكفي بذرة واحدة: نُثبّت
randomوnumpyوtorchوPYTHONHASHSEEDوكذلكrandom_stateصراحة في تقسيم البيانات. requirements.txtبلا نسخ ليس تثبيتًا؛ نستعمل ملفّ قفل يُحدّد الاعتمادات المباشرة وغير المباشرة.- نُحزّم حتّى بيئة التدريب في حاوية Docker مع
FROM ...@sha256:...لعزل نظام التشغيل ومكتبات النظام. - على GPU، الحتميّة الكاملة ممكنة لكنّها بطيئة؛ إمكانية التكرار الإحصائية بديل واقعي لأغلب المشاريع.