فاز 2 · درس 8

مهندسی و انتخاب ویژگی در یادگیری ماشین

نوع
ساختنی
زمان
حدود 90 دقیقه
زبان‌ها
Python
یک سیستم تصفیه چندمرحله‌ای متشکل از چند الک و فیلتر شیشه‌ای که ذرات ناخالص را جدا کرده و فقط دانه‌های درخشان و ارزشمند را به انتهای مسیر هدایت می‌کند
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفهوم
  4. خط لوله‌ی ویژگی
  5. ویژگی‌های عددی
  6. ویژگی‌های دسته‌ای
  7. ویژگی‌های متنی
  8. مقادیر گم‌شده
  9. تعامل ویژگی‌ها
  10. انتخاب ویژگی
  11. پیاده‌سازی از صفر
  12. مرحله‌ی 1: تبدیل‌های عددی از صفر
  13. مرحله‌ی 2: کدگذاری دسته‌ای از صفر
  14. مرحله‌ی 3: ویژگی‌های متنی از صفر
  15. مرحله‌ی 4: جایگزینی مقادیر گم‌شده از صفر
  16. مرحله‌ی 5: انتخاب ویژگی از صفر
  17. مرحله‌ی 6: خط لوله‌ی کامل و دمو
  18. استفاده از ابزارهای واقعی
  19. تحویل پروژه
  20. تمرین‌ها
  21. اصطلاحات کلیدی
  22. منابع برای مطالعه‌ی بیشتر

خیلی اوقات پس از ساخت یک مدل یادگیری ماشین، نتیجه مطلوب نیست و ساعت‌ها زمان صرف تغییر الگوریتم یا دستکاری هایپرپارامترها می‌شود؛ اما دقت مدل به‌سختی چند دهم درصد تغییر می‌کند. واقعیت این است که در یادگیری ماشین کلاسیک، شکل و کیفیت ورودی داده‌ها بسیار تعیین‌کننده‌تر از پیچیدگی الگوریتم است.

مهندسی ویژگی (Feature Engineering) به شما کمک می‌کند داده‌های خام را به قالب‌های ریاضی معناداری تبدیل کنید که الگوریتم بتواند الگوهای پنهان آن‌ها را بخواند. در طرف مقابل، با انتخاب ویژگی (Feature Selection) ستون‌های اضافی و نویزدار حذف می‌شوند تا مدل بدون افتادن در دام بیش‌برازش، سریع‌تر و دقیق‌تر آموزش ببیند.

تمام کدهای این درس کاملاً روی سیستم شخصی، CPU و ابزارهای استانداردی مثل Python و scikit-learn اجرا می‌شوند و نیازی به GPU یا ابزارهای ابری پردازشی نخواهید داشت.

یک ویژگی خوب، ارزش هزار نقطه‌داده را دارد.

اهداف یادگیری

  • پیاده‌سازی تبدیل‌های عددی (استانداردسازی، مقیاس‌بندی کمینه-بیشینه، تبدیل لگاریتمی، دسته‌بندی بازه‌ای) و توضیح زمان مناسب برای هرکدام
  • ساخت کدگذاری وان‌هات، برچسبی و هدفی برای ویژگی‌های دسته‌ای و شناسایی خطر نشت داده (Data Leakage) در کدگذاری هدفی
  • ساخت یک بردارساز TF-IDF از صفر و بیان دلیل برتری آن نسبت به شمارش خام کلمات در دسته‌بندی متن
  • به‌کارگیری روش‌های پالایشی انتخاب ویژگی (آستانه‌ی واریانس، همبستگی، اطلاعات متقابل) برای کاهش ابعاد

صورت مسئله

مجموعه‌داده‌ای دارید. الگوریتمی انتخاب می‌کنید. مدل را آموزش می‌دهید، اما نتایج معمولی از آب درمی‌آیند. سراغ الگوریتم پیچیده‌تری می‌روید؛ باز هم چنگی به دل نمی‌زند. یک هفته وقت صرف تنظیم هایپرپارامترها می‌کنید، اما پیشرفت ناچیز است.

سپس کسی داده‌های خام را به ویژگی‌های بهتری تبدیل می‌کند و ناگهان یک رگرسیون لجستیک ساده، مدل گروهی تقویت گرادیان (gradient-boosted ensemble) تنظیم‌شده‌ی شما را شکست می‌دهد.

این اتفاق مدام می‌افتد. در یادگیری ماشین کلاسیک، نحوه‌ی نمایش داده‌ها بسیار مهم‌تر از انتخاب الگوریتم است. مدل قیمت مسکنی با متغیرهای «متراژ» و «تعداد اتاق خواب»، بر مدلی با «آدرس به‌صورت رشته‌ی متنی خام» پیروز می‌شود؛ فرقی هم نمی‌کند الگوریتم یادگیری چقدر پیشرفته باشد. الگوریتم فقط می‌تواند با همان چیزی کار کند که تحویلش می‌دهید.

مهندسی ویژگی (Feature Engineering) فرآیند تبدیل داده‌های خام به قالب‌هایی است که یافتن الگوها را برای مدل‌ها ساده‌تر می‌کنند. انتخاب ویژگی (Feature Selection) یعنی دور ریختن ویژگی‌هایی که بدون افزودن سیگنال، فقط نویز اضافه می‌کنند. این دو در کنار هم، پربازده‌ترین فعالیت در یادگیری ماشین کلاسیک هستند.

مفهوم

خط لوله‌ی ویژگی

داده‌های خاممدیریت مقادیر گمشدهتبدیل‌های عددیکدگذاری دسته‌ایویژگی‌های متنیتعاملات ویژگی‌هاانتخاب ویژگیداده‌های آماده مدل

ویژگی‌های عددی

اعداد خام به‌ندرت آماده‌ی ورود به مدل هستند. تبدیل‌های رایج عبارت‌اند از:

مقیاس‌بندی (Scaling): قرار دادن ویژگی‌ها در یک بازه‌ی یکسان تا الگوریتم‌های مبتنی بر فاصله (K-Means ،KNN ،SVM) وزن برابری برای همه‌ی ویژگی‌ها قائل شوند. مقیاس‌بندی کمینه-بیشینه (Min-max scaling) داده‌ها را به بازه‌ی [0, 1] می‌برد. استانداردسازی یا نمره‌ی استاندارد (z-score) میانگین را به 0 و انحراف معیار را به 1 تبدیل می‌کند.

تبدیل لگاریتمی (Log transform): توزیع‌های دارای چولگی به راست (درآمد، جمعیت، شمارش کلمات) را فشرده می‌کند و روابط ضربی را به جمعی تبدیل می‌سازد.

دسته‌بندی بازه‌ای (Binning): تبدیل مقادیر پیوسته به دسته‌ها. زمانی به کار می‌آید که رابطه‌ی بین ویژگی و متغیر هدف غیرخطی اما پله‌ای باشد (مثلاً گروه‌های سنی).

ویژگی‌های چندجمله‌ای (Polynomial features): جملات x2x^2، x3x^3 و x1x2x_1 x_2 را می‌سازد. این کار به مدل‌های خطی اجازه می‌دهد روابط غیرخطی را به قیمت افزایش تعداد ویژگی‌ها به تصویر بکشند.

ویژگی‌های دسته‌ای

مدل‌ها عدد می‌فهمند؛ بنابراین دسته‌ها باید کدگذاری (Encoding) شوند.

کدگذاری وان‌هات (One-hot encoding): برای هر دسته یک ستون باینری می‌سازد. مثلاً «color = red/blue/green» به سه ستون is_green ،is_blue ،is_red تبدیل می‌شود. برای ویژگی‌هایی با تعداد دسته‌های محدود (low-cardinality) عالی کار می‌کند، اما با افزایش تعداد دسته‌ها، ابعاد به‌شدت بالا می‌رود.

کدگذاری برچسبی (Label encoding): هر دسته را به یک عدد صحیح نگاشت می‌کند: red=0 ،blue=1 ،green=2. این کار یک ترتیب ساختگی به داده‌ها تحمیل می‌کند (ممکن است مدل تصور کند green > blue > red). این روش فقط برای مدل‌های مبتنی بر درخت مناسب است که روی مقادیر مجزا شاخه‌بندی می‌کنند.

کدگذاری هدفی (Target encoding): هر دسته را با میانگین متغیر هدف برای همان دسته جایگزین می‌کند. روشی قدرتمند اما خطرناک با ریسک بالای نشت داده. این محاسبات باید فقط روی داده‌های آموزش انجام شود و سپس نتیجه روی داده‌های آزمون اعمال گردد.

ویژگی‌های متنی

بردارساز شمارشی (Count vectorizer): تعداد دفعات تکرار هر کلمه در سند را می‌شمارد. مثلاً عبارت «the cat sat on the mat» تبدیل می‌شود به {the: 2, cat: 1, sat: 1, on: 1, mat: 1}.

معیار TF-IDF: مخفف Term Frequency-Inverse Document Frequency. به کلمات بر اساس میزان متمایز بودنشان در کل اسناد وزن می‌دهد. کلمات متداول مانند «the» وزن پایینی می‌گیرند و کلمات نادر و کلیدی، وزن بالایی دریافت می‌کنند.

TF(word,doc)=count(word in doc)total words in docIDF(word)=log⁡total docsdocs containing wordTF-IDF=TF⋅IDF\begin{aligned} \text{TF}(\text{word}, \text{doc}) &= \frac{\text{count(word in doc)}}{\text{total words in doc}} \\ \text{IDF}(\text{word}) &= \log \frac{\text{total docs}}{\text{docs containing word}} \\ \text{TF-IDF} &= \text{TF} \cdot \text{IDF} \end{aligned}

مقادیر گم‌شده

داده‌های واقعی پر از جای خالی هستند. استراتژی‌های برخورد با آن‌ها:

  • حذف ردیف‌ها: فقط زمانی که داده‌های گم‌شده اندک و کاملاً تصادفی باشند.
  • جایگزینی با میانگین/میانه (Mean/median imputation): روشی ساده است که مرکز ویژگی را حفظ می‌کند (میانه در برابر داده‌های پرت مقاوم‌تر است)، اما واریانس را کاهش می‌دهد و همه‌ی مقادیر پرشده را روی یک نقطه متمرکز می‌کند؛ بنابراین شکل توزیع تغییر می‌یابد.
  • جایگزینی با مد (Mode imputation): برای ویژگی‌های دسته‌ای.
  • ستون نشانگر (Indicator column): قبل از پر کردن جای خالی، یک ستون باینری به نام «was_this_missing» اضافه کنید؛ خودِ نبود داده می‌تواند حاوی اطلاعات معناداری باشد.
  • تکمیل رو به جلو/عقب (Forward/backward fill): برای داده‌های سری زمانی.

تعامل ویژگی‌ها

گاهی اوقات رابطه در ترکیب متغیرها نهفته است. «قد» و «وزن» به‌تنهایی قدرت پیش‌بینی کمتری نسبت به BMI=weight/height2\text{BMI} = \text{weight} / \text{height}^2 دارند. تعامل ویژگی‌ها ابعاد فضا را چندبرابر می‌کند؛ پس برای انتخاب موارد مناسب باید از دانش دامنه (Domain knowledge) کمک بگیرید.

انتخاب ویژگی

ویژگی‌های بیشتر همیشه بهتر نیست. متغیرهای نامربوط نویز اضافه می‌کنند، زمان آموزش را بالا می‌برند و می‌توانند باعث بیش‌برازش (Overfitting) شوند.

روش‌های پالایشی یا Filter (پیش از برازش مدل):

  • همبستگی: حذف ویژگی‌هایی با همبستگی بالا با یکدیگر (افزونگی داده)
  • اطلاعات متقابل (Mutual Information): سنجش این‌که دانستن یک ویژگی چقدر عدم‌قطعیت درباره‌ی متغیر هدف را کاهش می‌دهد
  • آستانه‌ی واریانس: حذف ویژگی‌هایی با تغییرات بسیار ناچیز

روش‌های پوششی یا Wrapper (مبتنی بر مدل):

  • منظم‌سازی L1 (روش Lasso): وزن ویژگی‌های نامربوط را دقیقاً به صفر می‌رساند
  • حذف بازگشتی ویژگی (RFE): آموزش مدل، حذف کم‌اهمیت‌ترین ویژگی و تکرار این چرخه

چرا انتخاب ویژگی اهمیت دارد: مدلی با 10 ویژگی خوب معمولاً عملکرد بهتری نسبت به مدلی با 10 ویژگی خوب و 90 ویژگی نویزدار ثبت می‌کند. ویژگی‌های نویزدار دست مدل را برای بیش‌برازش روی الگوهای غیرقابل‌تعمیم داده‌های آموزش باز می‌گذارند.

مقیاس‌بندی ویژگی‌هاتغییر وضعیت بین داده‌های خام و استانداردشده
FEATURE SCALINGWhen features live on different scales the loss surface is a long narrow valley, and gradient descent bounces between the walls. Standardizing each feature to zero mean and unit variance rounds the bowl, so the same algorithm walks almost straight to the minimum.
وقتی ویژگی‌ها در مقیاس‌های متفاوتی باشند، سطح تابع زیان (loss) شبیه به دره‌ای باریک و کشیده می‌شود و گرادیان کاهشی مدام میان دیواره‌ها نوسان می‌کند. استانداردسازی هر ویژگی با میانگین صفر و واریانس یک، این سطح را به یک کاسهٔ متقارن تبدیل می‌کند؛ به‌طوری‌که همان الگوریتم تقریباً مستقیم به سمت نقطهٔ کمینه حرکت می‌کند.

پیاده‌سازی از صفر

مرحله‌ی 1: تبدیل‌های عددی از صفر

import math


def min_max_scale(values):
    min_val = min(values)
    max_val = max(values)
    if max_val == min_val:
        return [0.0] * len(values)
    return [(v - min_val) / (max_val - min_val) for v in values]


def standardize(values):
    n = len(values)
    mean = sum(values) / n
    variance = sum((v - mean) ** 2 for v in values) / n
    std = math.sqrt(variance) if variance > 0 else 1.0
    return [(v - mean) / std for v in values]


def log_transform(values):
    return [math.log(v + 1) for v in values]


def bin_values(values, n_bins=5):
    min_val = min(values)
    max_val = max(values)
    bin_width = (max_val - min_val) / n_bins
    if bin_width == 0:
        return [0] * len(values)
    result = []
    for v in values:
        bin_idx = int((v - min_val) / bin_width)
        bin_idx = min(bin_idx, n_bins - 1)
        result.append(bin_idx)
    return result


def polynomial_features(row, degree=2):
    n = len(row)
    result = list(row)
    if degree >= 2:
        for i in range(n):
            result.append(row[i] ** 2)
        for i in range(n):
            for j in range(i + 1, n):
                result.append(row[i] * row[j])
    return result

مرحله‌ی 2: کدگذاری دسته‌ای از صفر

def one_hot_encode(values):
    categories = sorted(set(values))
    cat_to_idx = {cat: i for i, cat in enumerate(categories)}
    n_cats = len(categories)

    encoded = []
    for v in values:
        row = [0] * n_cats
        row[cat_to_idx[v]] = 1
        encoded.append(row)

    return encoded, categories


def label_encode(values):
    categories = sorted(set(values))
    cat_to_int = {cat: i for i, cat in enumerate(categories)}
    return [cat_to_int[v] for v in values], cat_to_int


def target_encode(feature_values, target_values, smoothing=10):
    global_mean = sum(target_values) / len(target_values)

    category_stats = {}
    for feat, target in zip(feature_values, target_values):
        if feat not in category_stats:
            category_stats[feat] = {"sum": 0.0, "count": 0}
        category_stats[feat]["sum"] += target
        category_stats[feat]["count"] += 1

    encoding = {}
    for cat, stats in category_stats.items():
        cat_mean = stats["sum"] / stats["count"]
        weight = stats["count"] / (stats["count"] + smoothing)
        encoding[cat] = weight * cat_mean + (1 - weight) * global_mean

    return [encoding[v] for v in feature_values], encoding

مرحله‌ی 3: ویژگی‌های متنی از صفر

def count_vectorize(documents):
    vocab = {}
    idx = 0
    for doc in documents:
        for word in doc.lower().split():
            if word not in vocab:
                vocab[word] = idx
                idx += 1

    vectors = []
    for doc in documents:
        vec = [0] * len(vocab)
        for word in doc.lower().split():
            vec[vocab[word]] += 1
        vectors.append(vec)

    return vectors, vocab


def tfidf(documents):
    n_docs = len(documents)

    vocab = {}
    idx = 0
    for doc in documents:
        for word in doc.lower().split():
            if word not in vocab:
                vocab[word] = idx
                idx += 1

    doc_freq = {}
    for doc in documents:
        seen = set()
        for word in doc.lower().split():
            if word not in seen:
                doc_freq[word] = doc_freq.get(word, 0) + 1
                seen.add(word)

    vectors = []
    for doc in documents:
        words = doc.lower().split()
        word_count = len(words)
        tf_map = {}
        for word in words:
            tf_map[word] = tf_map.get(word, 0) + 1

        vec = [0.0] * len(vocab)
        for word, count in tf_map.items():
            tf = count / word_count
            idf = math.log(n_docs / doc_freq[word])
            vec[vocab[word]] = tf * idf
        vectors.append(vec)

    return vectors, vocab

مرحله‌ی 4: جایگزینی مقادیر گم‌شده از صفر

def impute_mean(values):
    present = [v for v in values if v is not None]
    if not present:
        return [0.0] * len(values), 0.0
    mean = sum(present) / len(present)
    return [v if v is not None else mean for v in values], mean


def impute_median(values):
    present = sorted(v for v in values if v is not None)
    if not present:
        return [0.0] * len(values), 0.0
    n = len(present)
    if n % 2 == 0:
        median = (present[n // 2 - 1] + present[n // 2]) / 2
    else:
        median = present[n // 2]
    return [v if v is not None else median for v in values], median


def impute_mode(values):
    present = [v for v in values if v is not None]
    if not present:
        return values, None
    counts = {}
    for v in present:
        counts[v] = counts.get(v, 0) + 1
    mode = max(counts, key=counts.get)
    return [v if v is not None else mode for v in values], mode


def add_missing_indicator(values):
    return [0 if v is not None else 1 for v in values]

مرحله‌ی 5: انتخاب ویژگی از صفر

def correlation(x, y):
    n = len(x)
    mean_x = sum(x) / n
    mean_y = sum(y) / n
    cov = sum((xi - mean_x) * (yi - mean_y) for xi, yi in zip(x, y)) / n
    std_x = math.sqrt(sum((xi - mean_x) ** 2 for xi in x) / n)
    std_y = math.sqrt(sum((yi - mean_y) ** 2 for yi in y) / n)
    if std_x == 0 or std_y == 0:
        return 0.0
    return cov / (std_x * std_y)


def mutual_information(feature, target, n_bins=10):
    feat_min = min(feature)
    feat_max = max(feature)
    bin_width = (feat_max - feat_min) / n_bins if feat_max != feat_min else 1.0
    feat_binned = [
        min(int((f - feat_min) / bin_width), n_bins - 1) for f in feature
    ]

    n = len(feature)
    target_classes = sorted(set(target))

    feat_bins = sorted(set(feat_binned))
    p_feat = {}
    for b in feat_bins:
        p_feat[b] = feat_binned.count(b) / n

    p_target = {}
    for t in target_classes:
        p_target[t] = target.count(t) / n

    mi = 0.0
    for b in feat_bins:
        for t in target_classes:
            joint_count = sum(
                1 for fb, tv in zip(feat_binned, target) if fb == b and tv == t
            )
            p_joint = joint_count / n
            if p_joint > 0:
                mi += p_joint * math.log(p_joint / (p_feat[b] * p_target[t]))

    return mi


def variance_threshold(features, threshold=0.01):
    n_features = len(features[0])
    n_samples = len(features)
    selected = []

    for j in range(n_features):
        col = [features[i][j] for i in range(n_samples)]
        mean = sum(col) / n_samples
        var = sum((v - mean) ** 2 for v in col) / n_samples
        if var >= threshold:
            selected.append(j)

    return selected


def remove_correlated(features, threshold=0.9):
    n_features = len(features[0])
    n_samples = len(features)

    to_remove = set()
    for i in range(n_features):
        if i in to_remove:
            continue
        col_i = [features[r][i] for r in range(n_samples)]
        for j in range(i + 1, n_features):
            if j in to_remove:
                continue
            col_j = [features[r][j] for r in range(n_samples)]
            corr = abs(correlation(col_i, col_j))
            if corr >= threshold:
                to_remove.add(j)

    return [i for i in range(n_features) if i not in to_remove]

مرحله‌ی 6: خط لوله‌ی کامل و دمو

import random


def make_housing_data(n=200, seed=42):
    random.seed(seed)
    data = []
    for _ in range(n):
        sqft = random.uniform(500, 5000)
        bedrooms = random.choice([1, 2, 3, 4, 5])
        age = random.uniform(0, 50)
        neighborhood = random.choice(["downtown", "suburbs", "rural"])
        has_pool = random.choice([True, False])

        sqft_with_missing = sqft if random.random() > 0.05 else None
        age_with_missing = age if random.random() > 0.08 else None

        price = (
            50 * sqft
            + 20000 * bedrooms
            - 1000 * age
            + (50000 if neighborhood == "downtown" else 10000 if neighborhood == "suburbs" else 0)
            + (15000 if has_pool else 0)
            + random.gauss(0, 20000)
        )

        data.append({
            "sqft": sqft_with_missing,
            "bedrooms": bedrooms,
            "age": age_with_missing,
            "neighborhood": neighborhood,
            "has_pool": has_pool,
            "price": price,
        })
    return data


if __name__ == "__main__":
    data = make_housing_data(200)

    print("=== Raw Data Sample ===")
    for row in data[:3]:
        print(f"  {row}")

    sqft_raw = [d["sqft"] for d in data]
    age_raw = [d["age"] for d in data]
    prices = [d["price"] for d in data]

    print("\n=== Missing Value Handling ===")
    sqft_missing = sum(1 for v in sqft_raw if v is None)
    age_missing = sum(1 for v in age_raw if v is None)
    print(f"  sqft missing: {sqft_missing}/{len(sqft_raw)}")
    print(f"  age missing: {age_missing}/{len(age_raw)}")

    sqft_indicator = add_missing_indicator(sqft_raw)
    age_indicator = add_missing_indicator(age_raw)
    sqft_imputed, sqft_fill = impute_median(sqft_raw)
    age_imputed, age_fill = impute_mean(age_raw)
    print(f"  sqft filled with median: {sqft_fill:.0f}")
    print(f"  age filled with mean: {age_fill:.1f}")

    print("\n=== Numerical Transforms ===")
    sqft_scaled = standardize(sqft_imputed)
    age_scaled = min_max_scale(age_imputed)
    sqft_log = log_transform(sqft_imputed)
    age_binned = bin_values(age_imputed, n_bins=5)
    print(f"  sqft standardized: mean={sum(sqft_scaled)/len(sqft_scaled):.4f}, std={math.sqrt(sum(v**2 for v in sqft_scaled)/len(sqft_scaled)):.4f}")
    print(f"  age min-max: [{min(age_scaled):.2f}, {max(age_scaled):.2f}]")
    print(f"  age bins: {sorted(set(age_binned))}")

    print("\n=== Categorical Encoding ===")
    neighborhoods = [d["neighborhood"] for d in data]

    ohe, ohe_cats = one_hot_encode(neighborhoods)
    print(f"  One-hot categories: {ohe_cats}")
    print(f"  Sample encoding: {neighborhoods[0]} -> {ohe[0]}")

    le, le_map = label_encode(neighborhoods)
    print(f"  Label encoding map: {le_map}")

    te, te_map = target_encode(neighborhoods, prices, smoothing=10)
    print(f"  Target encoding: {({k: round(v) for k, v in te_map.items()})}")

    print("\n=== Text Features ===")
    descriptions = [
        "large modern house with pool",
        "small cozy cottage near downtown",
        "spacious family home with large yard",
        "modern apartment downtown with view",
        "rustic cabin in rural area",
    ]
    cv, cv_vocab = count_vectorize(descriptions)
    print(f"  Vocabulary size: {len(cv_vocab)}")
    print(f"  Doc 0 non-zero features: {sum(1 for v in cv[0] if v > 0)}")

    tf, tf_vocab = tfidf(descriptions)
    print(f"  TF-IDF vocabulary size: {len(tf_vocab)}")
    top_words = sorted(tf_vocab.keys(), key=lambda w: tf[0][tf_vocab[w]], reverse=True)[:3]
    print(f"  Doc 0 top TF-IDF words: {top_words}")

    print("\n=== Polynomial Features ===")
    sample_row = [sqft_scaled[0], age_scaled[0]]
    poly = polynomial_features(sample_row, degree=2)
    print(f"  Input: {[round(v, 4) for v in sample_row]}")
    print(f"  Polynomial: {[round(v, 4) for v in poly]}")
    print(f"  Features: [x1, x2, x1^2, x2^2, x1*x2]")

    print("\n=== Feature Selection ===")
    feature_matrix = [
        [sqft_scaled[i], age_scaled[i], float(sqft_indicator[i]), float(age_indicator[i])]
        + ohe[i]
        for i in range(len(data))
    ]

    print(f"  Total features: {len(feature_matrix[0])}")

    surviving_var = variance_threshold(feature_matrix, threshold=0.01)
    print(f"  After variance threshold (0.01): {len(surviving_var)} features kept")

    surviving_corr = remove_correlated(feature_matrix, threshold=0.9)
    print(f"  After correlation filter (0.9): {len(surviving_corr)} features kept")

    binary_prices = [1 if p > sum(prices) / len(prices) else 0 for p in prices]
    print("\n  Mutual information with target:")
    feature_names = ["sqft", "age", "sqft_missing", "age_missing"] + [f"neigh_{c}" for c in ohe_cats]
    for j in range(len(feature_matrix[0])):
        col = [feature_matrix[i][j] for i in range(len(feature_matrix))]
        mi = mutual_information(col, binary_prices, n_bins=10)
        print(f"    {feature_names[j]}: MI={mi:.4f}")

    print("\n  Correlation with price:")
    for j in range(len(feature_matrix[0])):
        col = [feature_matrix[i][j] for i in range(len(feature_matrix))]
        corr = correlation(col, prices)
        print(f"    {feature_names[j]}: r={corr:.4f}")

استفاده از ابزارهای واقعی

در scikit-learn، این تبدیل‌ها در قالب خطوط لوله‌ی ترکیب‌پذیر پیاده می‌شوند:

from sklearn.preprocessing import StandardScaler, OneHotEncoder, PolynomialFeatures
from sklearn.impute import SimpleImputer
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.feature_selection import mutual_info_classif, VarianceThreshold
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline

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

categorical_pipe = Pipeline([
    ("encoder", OneHotEncoder(sparse_output=False)),
])

preprocessor = ColumnTransformer([
    ("num", numeric_pipe, ["sqft", "age"]),
    ("cat", categorical_pipe, ["neighborhood"]),
])

نسخه‌های پیاده‌سازی‌شده از صفر دقیقاً نشان می‌دهند درون هر تبدیل چه می‌گذرد. نسخه‌های کتابخانه‌ای مواردی چون مدیریت حالت‌های خاص، پشتیبانی از ماتریس خلوت (sparse matrix) و ترکیب در خط لوله را به همراه دارند، اما منطق ریاضی هر دو کاملاً یکسان است.

تحویل پروژه

خروجی این درس:

  • outputs/prompt-feature-engineer.md - یک پرامپت برای مهندسی سیستماتیک ویژگی‌ها بر اساس داده‌های خام

تمرین‌ها

  1. مقیاس‌بندی مقاوم (Robust scaling - با استفاده از میانه و دامنه‌ی میان‌چارکی به‌جای میانگین و انحراف معیار) را به تبدیل‌های عددی اضافه کنید. عملکرد آن را با مقیاس‌بندی استاندارد روی داده‌های دارای مقادیر پرت شدید مقایسه کنید.
  2. کدگذاری هدفی یکی‌جاافتاده (Leave-one-out target encoding) را پیاده‌سازی کنید: برای هر سطر، میانگین متغیر هدف را بدون در نظر گرفتن مقدار هدف همان سطر حساب کنید. نشان دهید این کار چطور در مقایسه با کدگذاری هدفی ساده از بیش‌برازش جلوگیری می‌کند.
  3. یک پایپ‌لاین خودکار برای انتخاب ویژگی بسازید که آستانه‌ی واریانس، فیلتر همبستگی و رتبه‌بندی بر اساس اطلاعات متقابل را ترکیب کند. آن را روی مجموعه‌داده‌ی مسکن اعمال کرده و عملکرد یک مدل رگرسیون خطی ساده را در دو حالت «همه‌ی ویژگی‌ها» و «ویژگی‌های منتخب» مقایسه کنید.

اصطلاحات کلیدی

اصطلاح اصطلاح عامیانه معنای واقعی
مهندسی ویژگی (Feature engineering) «ساخت ستون‌های جدید» تبدیل داده‌های خام به بازنمایی‌هایی که الگوها را برای مدل آشکار می‌کنند
استانداردسازی (Standardization) «نرمال کردن داده‌ها» کم کردن میانگین و تقسیم بر انحراف معیار تا متغیر دارای میانگین 0 و انحراف معیار 1 شود
کدگذاری وان‌هات (One-hot encoding) «ساخت متغیرهای دامی (Dummy)» ساخت یک ستون باینری به‌ازای هر دسته؛ به‌طوری‌که در هر سطر فقط مقدار یک ستون 1 باشد
کدگذاری هدفی (Target encoding) «کدگذاری با استفاده از پاسخ» جایگزینی هر دسته با میانگین متغیر هدف در همان دسته، همراه با هموارسازی برای جلوگیری از بیش‌برازش
معیار TF-IDF «شمارش باکلاس کلمات» حاصل‌ضرب بسامد کلمه در معکوس بسامد سندی: وزن‌دهی به کلمات بر اساس تمایز آن‌ها در کل متن‌ها
جایگزینی یا درون‌یابی (Imputation) «پر کردن جاهای خالی» پر کردن مقادیر گم‌شده با برآوردهای تخمینی (میانگین، میانه، مد یا پیش‌بینی مدل)
انتخاب ویژگی (Feature selection) «دور ریختن ستون‌های نامناسب» حذف ویژگی‌های نویزدار یا تکراری و نگه‌داشتن آن‌هایی که حاوی سیگنال مفید درباره‌ی هدف هستند
اطلاعات متقابل (Mutual information) «یک چیز چقدر درباره‌ی چیز دیگر به ما می‌گوید» معیاری برای سنجش کاهش عدم‌قطعیت درباره‌ی متغیر Y پس از مشاهده‌ی متغیر X
نشت داده (Data leakage) «تقلب تصادفی» استفاده از اطلاعاتی در زمان آموزش که هنگام پیش‌بینی واقعی وجود ندارند؛ عاملی که نتایجی بیش‌ازحد خوش‌بینانه و کاذب تولید می‌کند

منابع برای مطالعه‌ی بیشتر

سؤالات متداول

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

الگوریتم‌ها فقط الگوهای موجود در نمایش داده را یاد می‌گیرند؛ ویژگی‌های مناسب حتی به یک مدل ساده خطی اجازه می‌دهند عملکردی بهتر از یک مدل پیچیده روی داده‌های خام داشته باشد.

چه تفاوتی بین One-Hot Encoding و Target Encoding وجود دارد؟

کدگذاری وان‌هات برای هر دسته یک ستون باینری می‌سازد، اما کدگذاری هدفی هر دسته را با میانگین متغیر هدف جایگزین می‌کند که با خطر نشت داده همراه است.

روش‌های فیلتری انتخاب ویژگی چگونه کار می‌کنند؟

این روش‌ها پیش از برازش مدل، با معیارهایی مثل واریانس پایین، همبستگی شدید با ستون‌های دیگر یا مقدار اطلاعات متقابل با هدف، ویژگی‌های نامناسب را حذف می‌کنند.

ما رو توی گوگل بیشتر ببین

با افزودن نکست ادمین به «منابع ترجیحی» گوگل، درس‌ها و مطالب ما را در بخش اخبار برتر جستجو بیشتر می‌بینید.