الوحدة 7 — خطوط التكامل والتسليم المستمرّين
الآن نضع كلّ ما بنيناه في تدفّق آليّ يبدأ من التزام Git وينتهي بنموذج في السجلّ جاهز للترقية. الفرق بين CI/CD تقليدي وCI/CD للتعلّم الآلي أنّ الأخير يختبر ثلاثة أشياء بدل واحد: الشفرة، والبيانات، والنموذج.
اختبارات البيانات: الحاجز الأول
قبل تدريب النموذج، يجب أن نتحقّق من أنّ البيانات المُقدَّمة صالحة. أخطاء البيانات صامتة: التدريب ينجح، الأرقام تظهر، ولكنّ النموذج فاسد.
مثال ملموس: نستخدم great-expectations أو pandera لكتابة اختبارات على بيانات التسرّب:
import pandera as pa
from pandera import Column, Check
schema = pa.DataFrameSchema({
"customer_id": Column(str, unique=True),
"tenure_months": Column(int, Check.in_range(0, 240)),
"monthly_charge": Column(float, Check.ge(0)),
"has_churned": Column(int, Check.isin([0, 1])),
})
schema.validate(df_train)
ما نختبره تحديدًا:
- الأنواع: هل
tenure_monthsعدد صحيح فعلًا، لا نصّ يشبه العدد؟ - الفريدة: هل كلّ معرِّف عميل ظاهر مرّة واحدة؟
- المدى: هل قيمة
monthly_chargeسالبة (خطأ نظام محاسبة) أو أكبر من 10000 دولار (قيمة شاذّة)؟ - الاكتمال: نسبة القيم الناقصة في العمود الحرج تحت 5٪؟
- التوزيع: هل نسبة التسرّب في العيّنة الجديدة تُشبه معدّلها التاريخي (±3 نقاط)؟ تغيّر مفاجئ يعني خللًا في الاستخراج.
فشل أيّ اختبار يوقف الخطّ فورًا. لا تدريب على بيانات مشكوك فيها.
اختبارات النموذج: العتبات
ب عد التدريب، نختبر النموذج نفسه. هذه ليست اختبارات وحدانية بمعنى مطلق (نتيجة التدريب ليست حتميّة تمامًا)، بل عتبات جودة:
def test_val_auc_at_least_0_82():
metrics = json.load(open("metrics.json"))
assert metrics["val_auc"] >= 0.82, f"AUC hors seuil: {metrics['val_auc']}"
def test_prediction_latency_under_100ms():
times = []
for _ in range(100):
t0 = time.perf_counter()
model.predict_proba(sample)
times.append((time.perf_counter() - t0) * 1000)
p99 = np.percentile(times, 99)
assert p99 < 100, f"P99 latence: {p99:.1f} ms"
def test_no_regression_on_slice():
"""AUC على المشتركين الجدد (أقلّ من 6 أشهر) لا يجب أن ينخفض تحت 0,75."""
auc_new = eval_on_slice(df_val[df_val.tenure_months < 6])
assert auc_new >= 0.75
النقطة الأخيرة حاسمة: اختبارات على شرائح المجموعة. متوسّط AUC ممتاز قد يخفي انهيار الأداء على أقلّية معيّنة. هذا هو مصدر معظم الأخطاء «العادلة» في النماذج التي دخلت الإنتاج بمصادقة سطحية.
خطّ ملموس مع GitHub Actions
نبني كلّ ذلك في ملفّ .github/workflows/train.yml:
name: Entrainer et enregistrer
on:
push:
branches: [main]
paths:
- "src/**"
- "data/**.dvc"
- "requirements-lock.txt"
jobs:
entrainer:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Installer
run: pip install -r requirements-lock.txt
- name: Recuperer les donnees
run: dvc pull
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET }}
- name: Valider les donnees
run: pytest tests/data/
- name: Entrainer
run: python -m src.train
- name: Tester le modele
run: pytest tests/model/
- name: Enregistrer dans MLflow (candidate)
run: python -m src.register --alias candidate
env:
MLFLOW_TRACKING_URI: ${{ secrets.MLFLOW_URL }}
النموذج يُسجَّل باسم بديل @candidate، لا @production مباشرةً. الترقية إلى الإنتاج تتطلّب خطوة إضافية.
بناء الصورة ونشرها
بعد نجاح التدريب واختبارات النموذج، نبني الصورة:
construire-image:
needs: entrainer
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Se connecter au registre
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Construire
run: docker build -t ghcr.io/notreorg/churn:${{ github.sha }} .
- name: Analyser les vulnerabilites
run: trivy image --exit-code 1 --severity HIGH,CRITICAL ghcr.io/notreorg/churn:${{ github.sha }}
- name: Pousser
run: docker push ghcr.io/notreorg/churn:${{ github.sha }}
الوسم بـ${{ github.sha }} يعني أنّ كلّ التزام يُنتج صورة قابلة للتمييز. نستطيع لاحقًا نشر أيّ صورة، ومعرفة الالتزام الذي بناها.
المصادقة اليدوية قبل الإنتاج
هذه هي النقطة التي كثيرًا ما تُنسى. في GitHub Actions نستعمل البيئات المحميّة (protected environments):
deployer-production:
needs: construire-image
runs-on: ubuntu-latest
environment:
name: production
url: https://api.notre-service.com
steps:
- name: Promouvoir @candidate vers @production
run: python -m src.promote
الإعداد environment: production مع قاعدة «مطلوب مراجعو» (Required reviewers) يعني أنّ الخطّ يتوقّف قبل هذه الخطوة، وينتظر ضغطة زرّ من شخص مُخوَّل. هذا يبني حاجزًا صريحًا بين «النموذج جاهز تقنيًّا» و«الف ريق يوافق على نشره».
بيئات ثلاث، مسار واحد
النمط القياسي: ثلاث بيئات، نفس الشفرة تعبرها بالتتابع:
- dev: بيئة عالمة البيانات، تجارب حرّة، لا مصادقة.
- staging: نُصلي عليه اختبارًا في ظروف قريبة من الإنتاج على بيانات مماثلة، أو ظلّ الإنتاج (shadow).
- production: تخدم الطلبات الحقيقية، مصادقة إلزامية للترقية.
المرور من واحدة إلى أخرى يقتضي مصادقات محدّدة، مؤتمَتة قدر الإمكان لكنّها موجودة. هذا التدرّج يمنع أخطاء «شغل عندي على حاسوبي، هيّا إلى الإنتاج».
كلّ خطوة في الخطّ يجب أن تفشل بصورة صريحة إن لم تعمل. لا try/except كبير يبتلع الأخطاء، ولا exit 0 في نصّ برمجي فشل جزئيًّا. الأتمتة التي تفشل صامتةً أخطر من انعدام الأتمتة، لأنّها تُوهم بالسلامة.
في الخلاصة
- CI/CD للتعلّم الآلي يختبر البيانات (المخطّط، المدى، التوزيع) والنموذج (عتبات، شرائح، زمن)، لا الشفرة فحسب.
- خطّ ملموس (GitHub Actions أو نظيره) يبدأ بالتزام، يمرّ بجلب البيانات، التدريب، الاختبار، بناء الصورة، ونشرها في السجلّ.
- النموذج الناتج يُسجَّل باسم بديل
@candidate، ولا يصل إلى@productionإلّا عبر خطوة مصادقة صريحة، غالبًا يدوية. - ثلاث بيئات (dev، staging، production) مع فحص أمن الصورة قبل نشرها هي الحدّ الأدنى المقبول لخطّ إنتاجيّ.