انتقل إلى المحتوى الرئيسي

الوحدة 5 — الحاويات المخصّصة وسكربتات التدريب

XGBoost المدمج ممتاز عندما تناسب الخوارزميّة المهمّة تمامًا. لكنّ الواقع أقلّ لطفًا: الفريق يريد تجربة RandomForest أوّلًا، ثمّ LogisticRegression كنقطة مرجع، ثمّ إضافة معالجة أوّليّة داخل نفس التدريب. الحلّ الرسميّ لهذه الحالات هو مود سكربت (Script Mode).

مود سكربت في ثلاث جمل

  1. AWS يقدّم صورة Docker مسبقة الإعداد لكلّ إطار عمل معروف (scikit-learn وPyTorch وTensorFlow وHugging Face...).
  2. أنت تكتب سكربت بايثون واحدًا يعرف كيف يقرأ البيانات ويدرّب ويحفظ النموذج.
  3. SageMaker يُشغّل هذا السكربت داخل الحاوية دون أن تلمس Dockerfile.

هذا ما يميّز مود سكربت من «حاوية مخصّصة كلّيًّا»: لا داعي لبناء صورة Docker، ولا لدفعها إلى ECR. 90٪ من مشاريع تعلّم الآلة تكتفي بمود سكربت.

نقطة الدخول: سكربت train.py

نُنشئ ملفًّا اسمه train.py بجانب الدفتر. هذا السكربت هو ما ستُشغّله المهمّة:

# train.py
import argparse
import os
import joblib
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import f1_score

def main():
parser = argparse.ArgumentParser()
# المعاملات الفائقة
parser.add_argument("--n-estimators", type=int, default=200)
parser.add_argument("--max-depth", type=int, default=8)
# المسارات (يمرّرها SageMaker عبر متغيّرات بيئة)
parser.add_argument("--model-dir", default=os.environ["SM_MODEL_DIR"])
parser.add_argument("--train", default=os.environ["SM_CHANNEL_TRAIN"])
parser.add_argument("--valid", default=os.environ["SM_CHANNEL_VALIDATION"])
args = parser.parse_args()

train_df = pd.read_parquet(f"{args.train}/train.parquet")
valid_df = pd.read_parquet(f"{args.valid}/valid.parquet")

y_train = train_df.pop("churn")
y_valid = valid_df.pop("churn")

model = RandomForestClassifier(
n_estimators=args.n_estimators,
max_depth=args.max_depth,
n_jobs=-1,
random_state=42,
)
model.fit(train_df, y_train)

preds = model.predict(valid_df)
score = f1_score(y_valid, preds)
print(f"validation:f1={score:.4f}")

joblib.dump(model, f"{args.model-dir}/model.joblib")

if __name__ == "__main__":
main()

يُطبع سطر validation:f1=... بصيغة يعرف SageMaker استخراجها كمقياس منظّم يظهر في Studio وفي عمليّة الضبط التلقائيّ (الوحدة 6).

متغيّرات بيئة SageMaker

يمرّر SageMaker لسكربتك جملةً من المتغيّرات البيئيّة التي يجب معرفتها بأسمائها:

المتغيّرما يعنيه
SM_MODEL_DIRمسار محلّيّ (/opt/ml/model) يُنسخ محتواه إلى S3 بعد الانتهاء
SM_CHANNEL_TRAINمسار محلّيّ لقناة train من estimator.fit({"train": ...})
SM_CHANNEL_VALIDATIONمسار قناة validation
SM_HP_<NOM>كلّ معامل فائق تمرّره صراحةً في hyperparameters
SM_NUM_GPUSعدد وحدات GPU المتاحة على المثيل
SM_CURRENT_HOSTاسم مضيف المهمّة (algo-1, algo-2...) في التدريب الموزّع
SM_HOSTSقائمة كلّ المضيفين في مجموعة التدريب

الفخّ الأشيع: نسيان SM_CHANNEL_... والاعتماد على مسار مطلق مثل /opt/ml/input/data/train/. السكربت سيعمل، لكنّه يصبح هشًّا إذا أُعيدت تسمية القناة. الاعتماد على متغيّرات البيئة يفصل السكربت عن اسم القناة.

تشغيل المهمّة عبر SKLearn

يقدّم SDK فئة مخصّصة لكلّ إطار عمل. لـscikit-learn:

from sagemaker.sklearn.estimator import SKLearn

sk_estimator = SKLearn(
entry_point="train.py",
role=role,
instance_type="ml.m5.xlarge",
instance_count=1,
framework_version="1.2-1",
py_version="py3",
hyperparameters={
"n-estimators": 300,
"max-depth": 10,
},
output_path=f"s3://{bucket}/churn/models/rf/",
metric_definitions=[
{"Name": "validation:f1", "Regex": "validation:f1=([0-9\\.]+)"},
],
)

sk_estimator.fit({
"train": TrainingInput(f"s3://{bucket}/churn/processed/v1/", content_type="application/x-parquet"),
"validation": TrainingInput(f"s3://{bucket}/churn/processed/v1/", content_type="application/x-parquet"),
})

metric_definitions يحوّل السطر المطبوع إلى مقياس منظّم يظهر في Studio مباشرةً. الاعتناء بهذا التعبير المنتظم شرط للضبط التلقائيّ التالي.

الاعتماديّات: source_dir وrequirements.txt

نادرًا ما يكون train.py كافيًا وحده. عادةً تحتاج إلى ملفّات مساعدة (وظائف تنظيف، معالجات)، وإلى مكتبات إضافيّة. الحلّ: مجلّد source_dir مع requirements.txt.

source/
├── train.py
├── preprocessing.py
└── requirements.txt
sk_estimator = SKLearn(
entry_point="train.py",
source_dir="source/",
role=role,
...
)

كلّ ما في source/ يُنسخ إلى الحاوية، وrequirements.txt يُثبَّت قبل تشغيل train.py. لا تُدرج مكتبات مثبَّتة أصلًا في صورة scikit-learn (مثل pandas)، فيُبطئ ذلك بدء المهمّة لا أكثر.

متى نحتاج حاوية مخصّصة كلّيًّا

مود سكربت غير كافٍ في حالات نادرة:

  • اعتماديّة نظاميّة (مكتبة C) لا تُثبَّت عبر pip.
  • ثنائيّ خاصّ (Rust, Go) يجب أن يكون حاضرًا.
  • إطار عمل غير مدعوم رسميًّا.

في هذه الحالات تكتب Dockerfile، تبني الصورة، تدفعها إلى ECR، ثمّ تمرّر رابطها للـEstimator. حاول تجنّب هذا الطريق قدر الإمكان: بناء الصورة يضيف زمنًا لكلّ مطوّر في الفريق، وحاجة صيانتها مستمرّة.

التصحيح المحلّيّ: LocalMode

قبل صرف الوقت والمال على تشغيل مهمّة كاملة، جرّب سكربتك محلّيًّا:

sk_estimator = SKLearn(
entry_point="train.py",
source_dir="source/",
role=role,
instance_type="local", # يُشغّل الحاوية على جهازك
framework_version="1.2-1",
...
)

instance_type="local" يشغّل نفس صورة Docker على جهازك، بنفس المسارات ومتغيّرات البيئة. تلتقط الأخطاء التافهة (خطأ في اسم عمود، مسار خاطئ) في ثوانٍ بدل انتظار خمس دقائق لبدء آلة EC2. اختبار ناجح محلّيًّا يعني تدريبًا سحابيًّا بلا مفاجآت في 99٪ من الحالات.

اعتنِ بـstdout

كلّ ما يطبعه السكربت على المخرَج القياسيّ يظهر في CloudWatch وفي واجهة Studio. اجعل السجلّات معنويّة: طبع دقيق للمعاملات المستعملة، والعدد الفعليّ للسطور المقروءة، والزمن المنقضي. لأنّ هذه السجلّات هي كلّ ما ستراه لاحقًا عند تصحيح مهمّة أخفقت.

الخلاصة

  • مود سكربت يكفي في 90٪ من الحالات: سكربت بايثون واحد داخل صورة إطار عمل مسبقة الإعداد.
  • المسارات وقيم المعاملات تصل عبر متغيّرات بيئة (SM_MODEL_DIR, SM_CHANNEL_TRAIN, SM_HP_...)؛ لا تعتمد على مسارات مطلقة.
  • metric_definitions مع تعبير منتظم يحوّل سطرًا مطبوعًا إلى مقياس منظّم قابل للاستهداف في الضبط التلقائيّ.
  • الوضع المحلّيّ (instance_type="local") يلتقط الأخطاء البسيطة قبل صرف دقائق ودولارات على السحابة.

الوحدة التالية: نُشغّل عشرات المهامّ بالتوازي بحثًا عن أفضل مجموعة معاملات فائقة.