Phase 02: ML Fundamentals

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

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

Type: Build

Languages: Python

Prerequisites: Phase 1 (Statistics for ML, Linear Algebra), Phase 2 Lessons 1-7

Time: ~90 minutes

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

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

مشکل

شما یک مجموعه داده دارید. شما یک الگوریتم را انتخاب می کنید. شما آن را آموزش می دهید. نتایج متوسط است. شما یک الگوریتم فوق العاده را امتحان می کنید. هنوز هم متوسط است. شما یک هفته را صرف تنظیم پارامترهای بالا می کنید. بهبود حاشیه ای.

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

این اتفاق همیشه اتفاق می افتد. در ML کلاسیک، نمایش داده ها مهم تر از انتخاب الگوریتم است. یک مدل قیمت خانه با "فیلد مربع" و "عدد اتاق خواب" مدل را با "ادریس به عنوان یک رشته خام" شکست می دهد، مهم نیست که دانش آموز چقدر پیچیده باشد. الگوریتم فقط می تواند با آنچه شما به آن می دهید کار کند.

مهندسی ویژگی ها فرآیند تبدیل داده های خام به نمایش داده هایی است که الگوهای پیدا کردن آن را برای مدل ها آسان تر می کند. انتخاب ویژگی ها فرآیند رد کردن ویژگی هایی است که بدون اضافه کردن سیگنال صدا را اضافه می کنند. با هم، آنها بیشترین فعالیت را در ML کلاسیک تشکیل می دهند.

مفهوم

خط لوله ویژگی

flowchart LR
    A[Raw Data] --> B[Handle Missing Values]
    B --> C[Numerical Transforms]
    B --> D[Categorical Encoding]
    B --> E[Text Features]
    C --> F[Feature Interactions]
    D --> F
    E --> F
    F --> G[Feature Selection]
    G --> H[Model-Ready Data]

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

اعداد خام به ندرت مدل آماده هستند.

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

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

Binning:تبدیل مقادیر مداوم به دسته بندی ها مفید است هنگامی که رابطه بین ویژگی و هدف غیر خطی است اما به صورت مرحله ای (به عنوان مثال گروه های سنی).

Polynomial features:ایجاد x^2, x^3, x1*x2 اصطلاحات. اجازه می دهد مدل های خطی روابط غیر خطی را با هزینه ویژگی های بیشتر ضبط کنند.

ویژگی های دسته بندی

مدل ها به شماره ها نیاز دارند، دسته ها به کدگذاری نیاز دارند.

One-hot encoding:ایجاد یک ستون دوگانه برای هر دسته. "color = red/blue/green" به سه ستون تبدیل می شود: is_red, is_blue, is_green. برای ویژگی های کم کاردینیالتی خوب کار می کند اما با بسیاری از دسته ها منفجر می شود.

Label encoding:نقشه هر دسته به یک عدد کامل: قرمز=0، آبی=1، سبز=2. ترتیب نادرست را معرفی می کند (نمادها ممکن است سبز > آبی > قرمز را فکر کنند). فقط برای مدل های مبتنی بر درخت که بر روی ارزش های فردی تقسیم می شوند مناسب است.

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

ویژگی های متن

Count vectorizer:تعداد تعداد دفعات هر کلمه در یک سند ظاهر می شود. " گربه روی فرش نشسته " تبدیل به {: 2, گربه: 1, sat: 1, on: 1, mat: 1}.

TF-IDF:اصطلاح فرکانس - فرکانس متن متن. کلمات را با توجه به اینکه چقدر منحصر به فرد هستند در اسناد وزن می کند. کلمات رایج مانند "the" وزن کم می کنند. کلمات نادر و متمایز وزن بالایی دارند.

TF(word, doc) = count(word in doc) / total words in doc
IDF(word) = log(total docs / docs containing word)
TF-IDF = TF * IDF

ارزش های گمشده

داده های واقعی سوراخ هایی دارند.

  • Drop rows:فقط وقتی داده های گمشده نادر و تصادفی هستند
  • Mean/median imputation:ساده، شکل توزیع را حفظ می کند (در میان میان، نسبت به غیر قابل ملاحظه قوی تر است)
  • Mode imputation:برای ویژگی های دسته بندی
  • Indicator column:قبل از فرض کردن یک ستون دوگانه "was_this_missing" اضافه کنید. این واقعیت که داده ها از دست رفته است می تواند خود اطلاعات باشد
  • Forward/backward fill:برای داده های سری زمان

تعامل ویژگی

گاهی اوقات رابطه در ترکیب است. "ارتفاع" و "وزن" به تنهایی کمتر پیش بینی کننده از "BMI = وزن / ارتفاع^2" هستند. تعاملات ویژگی فضای ویژگی را چند برابر می کند، بنابراین از دانش دامنه برای انتخاب مناسب استفاده کنید.

انتخاب ویژگی

ویژگی های بیشتر همیشه بهتر نیست. ویژگی های غیرمطابق باعث افزایش سر و صدا، افزایش زمان آموزش و می تواند باعث بیش از حد مناسب شدن شود.

Filter methods (pre-model):

  • ارتباط: ویژگی های بسیار مرتبط با یکدیگر را حذف کنید (بایدار)
  • اطلاعات متقابل: اندازه گیری میزان دانستن یک ویژگی باعث کاهش عدم اطمینان در مورد هدف می شود
  • حد اختلاف: ویژگی هایی را که به سختی متفاوت هستند حذف کنید

Wrapper methods (model-based):

  • تنظیم L1 (Lasso): وزن ویژگی های غیر مرتبط را به صفر می رساند
  • حذف ویژگی های تکراری: قطار، حذف ویژگی های کم اهمیت، تکرار

Why selection matters:یک مدل با 10 ویژگی خوب معمولاً عملکرد مدل با 10 ویژگی خوب و 90 ویژگی شور را از دست می دهد. ویژگی های شور باعث می شود مدل در الگوهای داده های آموزشی که عمومی نمی شوند، بیش از حد مناسب باشد.

آن را بسازید

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

pythonimport 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: کد بندی دسته بندی از ابتدا

pythondef 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

مرحله سوم: ویژگی های متن از ابتدا

pythondef 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: فرض ارزش از ابتدا

pythondef 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: انتخاب ویژگی از ابتدا

pythondef 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: خط خط کامل و نمایش

pythonimport 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، این تحولات لوله های ترکیب شده هستند:

pythonfrom 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"]),
])

نسخه های اولیه دقیقاً آنچه در داخل هر تحول اتفاق می افتد را نشان می دهند. نسخه های کتابخانه اضافه می کنند مدیریت حالت کناری، پشتیبانی ماتریکس کمیاب و ترکیب لوله، اما ریاضیات یکسان است.

-باده

این درس نتیجه می دهد:

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

تمرینات

  1. مقیاس بندی قوی (با استفاده از محدوده میانگین و بین کوارتل به جای متوسط و انحراف استاندارد) را به تحولات عددی اضافه کنید. آن را با مقیاس بندی استاندارد در داده های با نرخ های بسیار خارق العاده مقایسه کنید.
  2. پیاده سازی کدگذاری هدف یک خارج از دسترس: برای هر ردیف، متوسط هدف را محاسبه کنید بدون حذف ارزش هدف خود آن ردیف. نشان دهید که چگونه این بیش از حد مناسب را در مقایسه با کدگذاری هدف ساده کاهش می دهد.
  3. یک خط لوله انتخاب ویژگی های خودکار ایجاد کنید که حد تخلف، فیلتر ارتباط و رتبه بندی اطلاعات متقابل را ترکیب کند. آن را به مجموعه داده های خانه اعمال کنید و عملکرد مدل را با تمام ویژگی ها در مقابل ویژگی های انتخاب شده مقایسه کنید (با استفاده از بازپسین خطی ساده)

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

TermWhat people sayWhat it actually means
Feature engineering"Making new columns"Transforming raw data into representations that expose patterns to the model
Standardization"Making it normal"Subtracting the mean and dividing by standard deviation so the feature has mean=0 and std=1
One-hot encoding"Making dummy variables"Creating one binary column per category, where exactly one column is 1 for each row
Target encoding"Using the answer to encode"Replacing each category with the average target value for that category, with smoothing to prevent overfitting
TF-IDF"Fancy word counts"Term Frequency times Inverse Document Frequency: words weighted by how distinctive they are across the corpus
Imputation"Filling in blanks"Replacing missing values with estimated values (mean, median, mode, or model-predicted)
Feature selection"Throwing out bad columns"Removing features that add noise or redundancy, keeping only those with signal about the target
Mutual information"How much one thing tells you about another"A measure of the reduction in uncertainty about variable Y gained by observing variable X
Data leakage"Accidentally cheating"Using information during training that would not be available at prediction time, giving falsely optimistic results

خواندن بیشتر

This free lesson is part of the AI Engineering from Scratch curriculum. Read the full explanation, run the lesson code, and verify the result in the interactive reader or from the repository source.

Browse the complete course catalog or open this lesson on GitHub.