آموزش Pipeline در scikit-learn و ColumnTransformer: گردش‌کار یادگیری ماشین (۲۰۲۶)

ساخت Pipeline در scikit-learn با ColumnTransformer، تنظیم ابرپارامتر با GridSearchCV، خروجی pandas و ذخیره مدل با joblib برای محیط تولید.

آموزش Pipeline در scikit-learn ۲۰۲۶

به‌روزرسانی: ۲۶ ژوئن ۲۰۲۶

Pipeline در scikit-learn یک شیء ترتیبی است که چند مرحله پیش‌پردازش و یک مدل نهایی را در قالب یک تخمین‌گر واحد به هم متصل می‌کند، به طوری که فراخوانی fit() روی Pipeline همان منطق را روی هر مرحله اجرا می‌کند و از نشت داده (data leakage) بین مجموعه‌ی آموزش و اعتبارسنجی جلوگیری می‌کند. در این راهنما، با ترکیب Pipeline و ColumnTransformer یک گردش‌کار کامل یادگیری ماشین می‌سازیم، نحوه استفاده از آن با GridSearchCV را نشان می‌دهیم، API جدید set_output در scikit-learn ۱.۶ را به‌کار می‌بریم و در نهایت مدل را با joblib برای محیط تولید ذخیره می‌کنیم. صادقانه بگویم، اولین باری که این الگو را در یک پروژه واقعی به کار بردم، چندین ساعت دیباگ غیرضروری از زندگی‌ام کم شد.

  • کلاس Pipeline چند مرحله پیش‌پردازش و یک تخمین‌گر را در یک شیء قابل آموزش و قابل اعتبارسنجی متقابل به هم زنجیر می‌کند.
  • ColumnTransformer اجازه می‌دهد تبدیل‌های متفاوتی روی ستون‌های عددی، دسته‌ای و متنی به صورت موازی اعمال شود.
  • استفاده از Pipeline در cross_val_score و GridSearchCV تنها راه درست برای جلوگیری از نشت آماره‌های مقیاس‌بندی به Fold اعتبارسنجی است.
  • در scikit-learn ۱.۲+ متد set_output(transform="pandas") یا "polars" خروجی هر مرحله را به جای ndarray به DataFrame تبدیل می‌کند.
  • برای استقرار، Pipeline کامل را با joblib.dump ذخیره کنید تا منطق پیش‌پردازش و مدل با هم بارگذاری شوند.

Pipeline در scikit-learn چیست و چرا اهمیت دارد؟

کلاس sklearn.pipeline.Pipeline یک متاتخمین‌گر است که یک فهرست مرتب از تبدیل‌ها را به دنبال یک تخمین‌گر نهایی (طبقه‌بند یا رگرسور) قرار می‌دهد. هر مرحله یک نام و یک شیء سازگار با API scikit-learn دارد؛ یعنی متد fit و در صورت لزوم transform یا predict. وقتی pipeline.fit(X_train, y_train) صدا زده می‌شود، scikit-learn به ترتیب fit_transform را روی هر تبدیل اجرا می‌کند، خروجی را به مرحله بعدی پاس می‌دهد و در پایان fit را روی تخمین‌گر نهایی اجرا می‌کند.

اهمیت Pipeline سه چیز است. اول، جلوگیری از نشت داده هنگام اعتبارسنجی متقابل، چون مقیاس‌بندی و کدگذاری فقط روی Fold آموزش fit می‌شود و نه روی کل مجموعه. دوم، تکرارپذیری و قابلیت سریال‌سازی کل گردش‌کار با یک فایل joblib. و سوم، تنظیم ابرپارامتر هم‌زمان روی مراحل پیش‌پردازش و مدل با نحو نقطه‌دار مانند preprocessor__num__imputer__strategy. در پروژه‌های واقعی، نوشتن پیش‌پردازش به صورت دستی پیش از train_test_split یکی از پرتکرارترین منابع overfitting پنهان است که Pipeline به طور ساختاری حل می‌کند.

اگر تازه با اکوسیستم داده در پایتون آشنا می‌شوید، توصیه می‌کنیم پیش از این مقاله نگاهی به راهنمای تحلیل سری‌های زمانی با Pandas بیندازید تا با ساختار DataFrame راحت‌تر باشید.

ساخت یک Pipeline پایه با StandardScaler و LogisticRegression

ساده‌ترین Pipeline یک تبدیل و یک مدل دارد. در مثال زیر، روی دیتاست breast cancer از scikit-learn یک طبقه‌بند رگرسیون لجستیک با مقیاس‌بندی ویژگی‌ها می‌سازیم. توجه کنید که هیچ‌گاه نباید StandardScaler را روی کل X fit کنیم. این کار آماره‌های میانگین و انحراف معیار را از داده‌های اعتبارسنجی به آموزش نشت می‌دهد.

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression

X, y = load_breast_cancer(return_X_y=True, as_frame=True)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42
)

pipe = Pipeline(steps=[
    ("scaler", StandardScaler()),
    ("clf", LogisticRegression(max_iter=1000, random_state=42)),
])

pipe.fit(X_train, y_train)
print("Test accuracy:", pipe.score(X_test, y_test))

# Cross-validation runs scaler.fit only on each training fold:
scores = cross_val_score(pipe, X_train, y_train, cv=5, scoring="roc_auc")
print("CV ROC-AUC mean:", scores.mean().round(4))

یک تابع کمکی مفید make_pipeline است که نام مراحل را به طور خودکار از روی نام کلاس می‌سازد. این برای آزمایش سریع راحت است؛ اما هنگام تنظیم ابرپارامتر، نام‌گذاری صریح با Pipeline(steps=[...]) بهتر است، چون باعث می‌شود کلیدها در GridSearchCV خوانا بمانند. در نسخه ۱.۶ scikit-learn، Pipelineها از __sklearn_tags__ پشتیبانی می‌کنند که به ابزارهای اعتبارسنجی اجازه می‌دهد بفهمند آیا کل گردش‌کار از داده‌های پراکنده پشتیبانی می‌کند یا نه.

ColumnTransformer برای ستون‌های عددی، دسته‌ای و متنی

دیتاست‌های واقعی به ندرت همگن هستند. ColumnTransformer اجازه می‌دهد چندین تبدیل را به صورت موازی روی زیرمجموعه‌های متفاوتی از ستون‌ها اعمال کنید و خروجی‌ها را به صورت افقی concat کنید. این کلاس کلید پل زدن بین DataFrame خام و یک ماتریس عددی آماده مدل است.

import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.ensemble import GradientBoostingClassifier

df = pd.read_csv("titanic.csv")
y = df.pop("Survived")
X = df

numeric_features = ["Age", "Fare", "SibSp", "Parch"]
categorical_features = ["Pclass", "Sex", "Embarked"]

numeric_pipeline = Pipeline(steps=[
    ("imputer", SimpleImputer(strategy="median")),
    ("scaler", StandardScaler()),
])

categorical_pipeline = Pipeline(steps=[
    ("imputer", SimpleImputer(strategy="most_frequent")),
    ("onehot", OneHotEncoder(handle_unknown="ignore", sparse_output=False)),
])

preprocessor = ColumnTransformer(
    transformers=[
        ("num", numeric_pipeline, numeric_features),
        ("cat", categorical_pipeline, categorical_features),
    ],
    remainder="drop",
    verbose_feature_names_out=False,
)

model = Pipeline(steps=[
    ("preprocessor", preprocessor),
    ("clf", GradientBoostingClassifier(random_state=42)),
])

model.fit(X, y)

چند نکته دقیق در این الگو وجود دارد. اول، آرگومان remainder="drop" به این معناست که هر ستونی که در فهرست تبدیل‌ها نباشد حذف می‌شود؛ اگر بخواهید بقیه ستون‌ها بدون تغییر عبور کنند، از remainder="passthrough" استفاده کنید. دوم، handle_unknown="ignore" در OneHotEncoder از خطا هنگام دیدن دسته‌بندی جدید در زمان پیش‌بینی جلوگیری می‌کند (نکته‌ای که برای استقرار حیاتی است). سوم، در scikit-learn ۱.۲+ پارامتر sparse_output جایگزین sparse قدیمی شده است. متد preprocessor.get_feature_names_out() نام ستون‌های نهایی پس از one-hot را برمی‌گرداند که برای بررسی اهمیت ویژگی‌ها لازم است.

تفاوت Pipeline و ColumnTransformer چیست؟

این پرتکرارترین سوال در میان کاربران تازه‌کار scikit-learn است و درک تفاوت آن‌ها سوءتفاهم‌های زیادی را حل می‌کند. به طور خلاصه، Pipeline مراحل را به صورت ترتیبی روی همه ستون‌ها اجرا می‌کند، در حالی که ColumnTransformer تبدیل‌ها را به صورت موازی روی زیرمجموعه‌های مختلف ستون‌ها اجرا و سپس نتایج را الحاق می‌کند. در عمل، این دو مکمل یکدیگرند، نه جایگزین.

ویژگیPipelineColumnTransformer
الگوی اجراترتیبی، خروجی هر مرحله ورودی بعدیموازی، هر تبدیل روی زیرمجموعه ستون‌ها
کاربرد اصلیزنجیر کردن پیش‌پردازش با مدل نهاییاعمال تبدیل‌های متفاوت روی نوع‌های ستون متفاوت
مرحله نهایییک تخمین‌گر (با predict)فقط تبدیل‌ها، predict ندارد
الحاق خروجیخروجی آخرین مرحلهconcat افقی همه شاخه‌ها
نمونه نام پارامترclf__Cnum__imputer__strategy
ترکیب با یکدیگرمی‌تواند یک ColumnTransformer را به عنوان مرحله داشته باشدمی‌تواند درون یک شاخه یک Pipeline کوچک داشته باشد

الگوی توصیه‌شده در پروژه‌های تولیدی این است: یک Pipeline سطح بالا که اولین مرحله‌اش یک ColumnTransformer به نام preprocessor است و آخرین مرحله‌اش مدل. هر شاخه از ColumnTransformer می‌تواند خودش یک Pipeline کوچک شامل impute + scale یا impute + encode باشد. این ساختار دو سطحی هم خوانا است و هم با ابزارهایی مانند GridSearchCV سازگار.

استفاده از Pipeline با GridSearchCV برای تنظیم ابرپارامتر

قدرت واقعی Pipeline زمانی نمایان می‌شود که آن را با GridSearchCV یا RandomizedSearchCV ترکیب کنید. نحو نقطه‌دار اجازه می‌دهد ابرپارامترهای هر مرحله (حتی مراحل پیش‌پردازش) را به صورت یکپارچه تنظیم کنید. مثال زیر، هم استراتژی impute و هم پارامتر تنظیم رگرسیون لجستیک را روی Titanic جستجو می‌کند:

from sklearn.model_selection import GridSearchCV
from sklearn.linear_model import LogisticRegression

model = Pipeline(steps=[
    ("preprocessor", preprocessor),
    ("clf", LogisticRegression(max_iter=2000)),
])

param_grid = {
    "preprocessor__num__imputer__strategy": ["mean", "median"],
    "preprocessor__cat__onehot__min_frequency": [None, 5, 10],
    "clf__C": [0.01, 0.1, 1.0, 10.0],
    "clf__penalty": ["l2"],
}

search = GridSearchCV(
    model,
    param_grid=param_grid,
    cv=5,
    scoring="roc_auc",
    n_jobs=-1,
    verbose=1,
)
search.fit(X, y)

print("Best params:", search.best_params_)
print("Best CV ROC-AUC:", round(search.best_score_, 4))
best_pipeline = search.best_estimator_

نکته کلیدی این است که در هر تکرار از اعتبارسنجی متقابل، scikit-learn کل Pipeline را روی Fold آموزش fit و سپس روی Fold اعتبارسنجی ارزیابی می‌کند. این یعنی imputer فقط آماره‌های Fold آموزش را می‌بیند و StandardScaler فقط روی آن fit می‌شود؛ هیچ آماره‌ای از داده‌های اعتبارسنجی به مدل نشت نمی‌کند. اگر این کار را به صورت دستی پیش از حلقه CV انجام دهید، نمرات اعتبارسنجی شما خوش‌بینانه خواهند بود. در پروژه قبلی‌ام دقیقاً همین اتفاق افتاد و یک افزایش ۳ درصدی ROC-AUC که خیلی واقعی به نظر می‌رسید، وقتی روی Holdout واقعی تست شد، کاملاً ناپدید شد.

برای فضای جستجوی بزرگ، HalvingGridSearchCV (پایدار از نسخه ۱.۰) بسیار سریع‌تر است، چون با تخصیص منابع تدریجی، پارامترهای ضعیف را زود حذف می‌کند. مستندات رسمی scikit-learn در بخش Pipelines and composite estimators الگوهای پیشرفته‌تر شامل FeatureUnion و TransformedTargetRegressor را پوشش می‌دهد.

خروجی pandas با set_output API در نسخه ۱.۲+

پیش از نسخه ۱.۲، خروجی تبدیل‌ها همیشه یک ndarray بود و نام ستون‌ها از بین می‌رفت. این برای دیباگ کردن واقعاً کابوس بود. API جدید set_output این مشکل را حل می‌کند:

from sklearn import set_config

# Global setting:
set_config(transform_output="pandas")

# Or per-pipeline:
preprocessor.set_output(transform="pandas")

X_transformed = preprocessor.fit_transform(X)
print(type(X_transformed))   # <class 'pandas.DataFrame'>
print(X_transformed.columns.tolist())

این قابلیت بسیار مفید است وقتی می‌خواهید قبل از فرستادن داده به مدل ببینید OneHotEncoder چه ستون‌هایی تولید کرده، یا مقادیر null پس از imputer به درستی پر شده‌اند. از scikit-learn نسخه ۱.۴ به بعد، transform_output="polars" نیز پشتیبانی می‌شود که برای دیتاست‌های بزرگ‌تر گزینه سریع‌تری است. توجه کنید این تنظیم فقط روی خروجی تبدیل‌ها اثر می‌گذارد و نه روی ورودی؛ تخمین‌گر نهایی هنوز هم یک ndarray یا DataFrame را قبول می‌کند.

یکی دیگر از قابلیت‌های مدرن، متد get_feature_names_out() روی کل Pipeline است که نام‌های ستون پس از همه تبدیل‌ها را برمی‌گرداند. این برای ساخت permutation_importance یا plot کردن ضرایب رگرسیون لجستیک با برچسب درست واقعاً ضروری است.

ذخیره و بارگذاری Pipeline برای محیط تولید

یکی از بزرگ‌ترین مزیت‌های Pipeline برای استقرار این است که یک فایل تمام منطق پیش‌پردازش و مدل را در بر می‌گیرد. کتابخانه استاندارد برای این کار joblib است که از سریال‌سازی کارآمد ndarrayها پشتیبانی می‌کند:

import joblib

# Save the fitted pipeline
joblib.dump(best_pipeline, "titanic_pipeline.joblib", compress=3)

# Later, in a Flask / FastAPI service:
loaded = joblib.load("titanic_pipeline.joblib")
prediction = loaded.predict(new_passenger_dataframe)

چند توصیه عملی برای استقرار. اول، نسخه scikit-learn، pandas و numpy را در یک فایل requirements.txt یا pyproject.toml ثابت کنید، چون فایل joblib بین نسخه‌های اصلی سازگار نیست. دوم، اگر مدل شما درخت تصمیم یا گرادیان بوستینگ بزرگ است، فرمت‌های اختصاصی مانند xgboost.Booster.save_model فشرده‌تر هستند، اما منطق پیش‌پردازش را شامل نمی‌شوند؛ همچنان joblib برای کل Pipeline لازم است. سوم، در محیط‌های با محدودیت منابع از پارامتر compress=3 استفاده کنید که با gzip فشرده می‌کند، ولی زمان بارگذاری را کمی افزایش می‌دهد.

اشتباهات رایج و چگونگی اجتناب از آن‌ها

پس از سال‌ها بررسی کد تیم‌های یادگیری ماشین، چند الگوی غلط را بارها دیده‌ایم. اول، fit کردن جداگانه تبدیل‌ها پیش از Pipeline. این کار مزیت اصلی Pipeline یعنی جلوگیری از نشت را خنثی می‌کند. هر تبدیلی که به آماره‌های داده وابسته است (impute، scale، encoding با min_frequency) باید درون Pipeline باشد. دوم، استفاده از OneHotEncoder بدون handle_unknown="ignore" در محیط تولید؛ یعنی هر دسته‌بندی جدید باعث استثنا می‌شود. سوم، فراموش کردن stratify در train_test_split برای مسائل طبقه‌بندی نامتعادل.

چهارم، تنظیم ابرپارامتر فقط روی مدل و نه پیش‌پردازش. اغلب انتخاب استراتژی impute یا threshold مین فرکانس OneHotEncoder تأثیر بیشتری از تنظیم C یا max_depth دارد. پنجم، استفاده از StandardScaler روی ویژگی‌های دسته‌ای کدگذاری شده با one-hot. این کار اطلاعات تنک بودن (sparsity) را از بین می‌برد. اگر مدل شما حساس به مقیاس است، فقط ستون‌های عددی پیوسته را مقیاس کنید. ششم، نادیده گرفتن n_jobs=-1 در GridSearchCV که می‌تواند زمان جستجو را روی یک ماشین چندهسته‌ای چندین برابر کاهش دهد.

برای دیباگ کردن یک Pipeline که نتایج عجیب می‌دهد، از pipeline[:-1].fit_transform(X) استفاده کنید تا فقط مراحل پیش‌پردازش را اجرا کنید و خروجی را بازرسی کنید. این الگو در نسخه‌های اخیر scikit-learn پشتیبانی می‌شود و یکی از سریع‌ترین راه‌ها برای تشخیص مشکلات داده است. مستندات رسمی API Pipeline همه متدهای موجود را به طور کامل شرح می‌دهد.

پرسش‌های متداول

آیا Pipeline در scikit-learn از نشت داده جلوگیری می‌کند؟

بله، تا زمانی که کل پیش‌پردازش درون Pipeline باشد و خود Pipeline درون cross_val_score یا GridSearchCV ارزیابی شود. اگر تبدیل‌ها را به صورت دستی قبل از CV اجرا کنید، نشت رخ می‌دهد و نمرات اعتبارسنجی شما خوش‌بینانه خواهد بود.

چگونه نام ویژگی‌ها را پس از ColumnTransformer به دست آورم؟

از preprocessor.get_feature_names_out() استفاده کنید. این متد آرایه‌ای از نام‌های ستون پس از همه تبدیل‌ها از جمله one-hot encoding برمی‌گرداند. در scikit-learn ۱.۲+ تنظیم verbose_feature_names_out=False پیشوند نام شاخه را حذف می‌کند.

آیا می‌توانم یک تبدیل سفارشی به Pipeline اضافه کنم؟

بله، با ارث‌بری از BaseEstimator و TransformerMixin و پیاده‌سازی متدهای fit و transform. برای توابع بدون حالت، FunctionTransformer سریع‌ترین راه است: FunctionTransformer(np.log1p, feature_names_out="one-to-one").

آیا Pipeline با xgboost و lightgbm کار می‌کند؟

بله، هم XGBClassifier و هم LGBMClassifier با API scikit-learn سازگار هستند و می‌توانند به عنوان آخرین مرحله Pipeline استفاده شوند. برای CatBoost از کلاس CatBoostClassifier با پارامتر verbose=0 استفاده کنید تا log spam نداشته باشید.

چگونه Pipeline را روی یک ردیف واحد در زمان پیش‌بینی اجرا کنم؟

یک DataFrame تک‌ردیفی با همان نام ستون‌های آموزش به pipeline.predict() پاس دهید. اگر فقط یک ndarray دارید، باید آن را با pd.DataFrame([row], columns=feature_names) بسته‌بندی کنید، در غیر این صورت ColumnTransformer نمی‌داند کدام ستون‌ها کدام هستند.

Editorial Team
درباره نویسنده Editorial Team

Our team of expert writers and editors.