🏠 总目录📚 本教程 07 · 特征工程
📑 本页目录(点开跳转)

07 · 特征工程

20 分钟 | ⭐ 核心


🎯 一句话

「数据和特征决定了机器学习的上限,模型只是逼近这个上限。」 在推荐系统里,好特征带来的收益经常大于换一个更炫的模型。


🧩 特征的四个来源(照着这个清单填,不会漏)

① 用户侧 User
  • 静态:user_id, 年龄, 性别, 城市, 注册天数, 设备价位
  • 统计:历史点击率, 活跃度, 平均停留时长, 消费能力
  • 偏好:类目偏好分布, 价格带偏好, 作者偏好
  • 序列:最近 500 次点击/购买的物品 ID 序列 ⭐ 最重要
② 物品侧 Item
  • 静态:item_id, 类目, 标签, 作者, 时长, 价格, 发布时间
  • 统计:曝光数, 点击率, 完播率, 点赞率, 近1h/1d/7d 热度
  • 内容:标题/正文 Embedding, 封面图 Embedding, 视频 Emb
③ 上下文 Context
  • 时间:小时, 星期几, 是否节假日, 距上次访问多久
  • 环境:设备, 操作系统, 网络(WiFi/4G), 地理位置
  • 会话:本次会话第几刷, 本次已看内容, 已停留时长
④ 交叉 Cross ⭐ 效果最好,也最难做
  • 用户类目偏好 × 物品类目 ← 最经典最有用
  • 用户价格带 × 物品价格
  • 用户历史序列 × 当前物品(→ 注意力机制,第 8 节 DIN)
  • 时间 × 类目(周五晚上看电影,早高峰刷新闻)

🔑 如果只能加一类特征,加「用户行为序列」。 它是现代推荐模型效果的主要来源。


🔢 特征怎么变成模型能吃的东西

1. 类别特征 → Embedding(推荐系统的标准做法)

离散类别特征(高基数 ID)city = "北京"查表(Embedding Table)[0.23, -0.51, 0.88, ..., 0.12]← 16 维向量,训练时学出来
离散 ID 从不直接喂给模型,而是当成索引去 Embedding 表里查出一行向量。⭐ 该看的是维度的塌缩:3000 个城市(推到 item_id 就是上亿个取值)本来需要同样长的 One-Hot 稀疏向量,查表之后只剩 16 维稠密向量,而且这 16 个数是训练时学出来的 —— 所以「北京」和「上海」会自动靠得很近,这是 One-Hot 永远给不了的。

为什么不用 One-Hot? - 城市有 3000 个 → One-Hot 是 3000 维稀疏向量,浪费且学不到关系 - Embedding 是 16 维稠密向量,且「北京」和「上海」会自动学得接近

import torch.nn as nn
city_emb = nn.Embedding(num_embeddings=3000, embedding_dim=16)

⚠️ 推荐系统 Embedding 的真实规模item_id 可能有 1 亿个 × 32 维 × 4 字节 = 12.8 GB,单机放不下。 → 工业界用 参数服务器(Parameter Server) 分片存储,或哈希分桶压缩。

2. 数值特征 → 必须处理,不能直接扔进去

问题 处理方法
量级差异大(价格 0-10000,评分 0-5) 归一化(x - min) / (max - min) 或标准化
长尾分布(播放量 0 到 1 亿) 对数变换log(1 + x) ⭐ 最常用
想让模型学非线性关系 分桶离散化 → 再 Embedding ⭐ 推荐系统最常用

为什么推荐系统偏爱分桶?

分桶Embedding年龄 = 27bucket = "25-30"[0.4, -0.2, ...]好处:模型能学出「25-30岁」这个群体的独特模式,而不是被迫假设「年龄和兴趣是线性关系」(显然不是)
连续的年龄值先被切成区间,再当成一个类别去查 Embedding。⭐ 该看的是这一步换来了什么:分桶之后模型不必再假设「年龄越大兴趣越强」这种线性关系,每个年龄段都能有自己独立的模式。

分桶技巧:等频分桶(每桶样本数相同)通常比等宽分桶好。

3. 序列特征 → Pooling 或 Attention

用户最近点击: [item_88, item_12, item_450, item_77, ...]各自查 Embedding[[...], [...], [...], [...]]聚合成一个向量① 平均池化 (Sum/Mean Pooling)← 简单,但所有历史一视同仁② 注意力加权 (Attention)← DIN:根据当前候选物品动态加权 ⭐③ 序列模型 (GRU/Transformer)← 建模顺序,第 9 节
序列特征永远是这两步:每个 item 各自查 Embedding,再把这一串向量压成一个。⭐ 分歧全在第二步 —— 平均池化对所有历史一视同仁,注意力(DIN)让当前候选决定谁重要,序列模型还额外建模先后顺序。

💥 特征交叉:推荐模型的核心命题

为什么需要交叉? 看这个经典例子:

特征 单独看
性别 = 女 没什么信息
类目 = 化妆品 没什么信息
性别=女 AND 类目=化妆品 点击率飙升

线性模型(LR)永远学不到这个,除非你手动造出这个组合特征。

交叉方式的演进史(这就是推荐模型的进化史)

 ①  人工交叉 (LR 时代)
     算法工程师手写几千条组合规则
     ❌ 累死人、组合爆炸、无法穷举

 ②  自动二阶交叉 (FM, 2010)  ⭐ 关键突破
     给每个特征一个隐向量,任意两个特征的交叉权重 = 两个隐向量的点积
     w_ij = <v_i, v_j>
     ✅ 参数从 O(n²) 降到 O(nk)
     ✅ 即使 (女, 化妆品) 从没共现过,也能通过其他数据学出来

 ③  域感知交叉 (FFM, 2016)
     每个特征对不同「域」有不同的隐向量
     ✅ 更精细  ❌ 参数量爆炸

 ④  深度自动交叉 (2016~)
     Wide&Deep / DeepFM / DCN / xDeepFM
     让神经网络自己学高阶交叉
     ✅ 无需人工  ⚠️ 但显式交叉结构仍然有用(DCN 的 Cross Network)

 ⑤  注意力式交叉 (2018~)
     DIN / AutoInt:用注意力机制动态决定哪些特征该交叉

📐 FM 的核心公式(值得看懂)

$$\hat{y} = w_0 + \sum_{i=1}^{n} w_i x_i + \sum_{i=1}^{n}\sum_{j=i+1}^{n} \langle \mathbf{v}_i, \mathbf{v}_j \rangle x_i x_j$$

💡 人话翻译

第 1 项:基准分。 第 2 项:每个特征单独的贡献(这就是逻辑回归)。 第 3 项:任意两个特征组合起来的贡献,权重用两个隐向量的点积表示。

为什么用点积而不是直接给每对特征一个权重? 因为直接给权重的话,(女, 化妆品) 这一对如果训练数据里从没出现过,权重就学不到。 用隐向量的话,「女」的向量可以从 (女, 连衣裙) 学,「化妆品」的向量可以从 (25岁, 化妆品) 学,两者一点积就有了预测能力

🔑 这就是 Embedding 泛化能力的又一次体现——和第 5 节矩阵分解是同一个思想。

实际上 FM 是矩阵分解的推广:MF 只有 user_id 和 item_id 两个特征,FM 允许任意多个特征。


🕳️ 特征工程的六个大坑(血泪教训)

坑 1:特征穿越(Feature Leakage)⭐ 最致命

用了预测时刻拿不到的信息。

❌ 用「这个视频的总播放量」当特征 → 但训练时用的是今天的总量,
   而预测时刻这个量还没产生 → 离线 AUC 0.95,上线暴死
✅ 用「预测时刻之前的累计播放量」

自查方法:对每个特征问 「线上服务的那一毫秒,我真的能拿到这个值吗?」

坑 2:离线/在线特征不一致(Training-Serving Skew)

离线用 Spark 算特征,在线用 Java 算特征 → 两边逻辑有细微差别 → 效果莫名其妙地差。

解法特征平台 / Feature Store,一套代码同时产出离线和在线特征。这是工业界的标准基建。

坑 3:统计特征的时间窗口

item_ctr 用全历史算 vs 用近 1 小时算,效果差别巨大。 ✅ 通常做多个窗口都喂进去:ctr_1h, ctr_1d, ctr_7d, ctr_30d

坑 4:新物品的统计特征全是 0

新发布的视频没有 CTR,填 0 会被模型认为「很差」。 ✅ 用贝叶斯平滑: $$\text{smoothed\_ctr} = \frac{\text{clicks} + \alpha}{\text{impressions} + \alpha + \beta}$$ 其中 α/(α+β) 是全局平均 CTR,曝光少时向全局均值靠拢。

坑 5:ID 特征的低频问题

只出现过 2 次的 item_id,Embedding 学不好还占内存。 ✅ 频次过滤:出现次数 < 阈值(如 10)的统一映射到一个 <UNK> ID。

坑 6:类目特征的高基数爆炸

author_id 有 5000 万个 → Embedding 表巨大。 ✅ 哈希分桶hash(author_id) % 1000000。会有冲突,但实践中影响可接受,且模型能靠其他特征区分。


🔨 动手:一个特征处理的完整例子

import numpy as np
import pandas as pd

def build_features(df, global_ctr=0.05):
    """一个典型的推荐特征处理流水线"""
    out = pd.DataFrame()

    # ---- 数值特征:对数变换(长尾) ----
    out["log_play_cnt"] = np.log1p(df["play_count"])
    out["log_price"]    = np.log1p(df["price"])

    # ---- 数值特征:等频分桶 → 类别 ----
    out["age_bucket"] = pd.qcut(df["age"], q=10, labels=False, duplicates="drop")
    out["duration_bucket"] = pd.cut(df["duration"],
                                    bins=[0, 15, 30, 60, 180, 600, 99999],
                                    labels=False)

    # ---- 统计特征:贝叶斯平滑,解决冷启动全 0 问题 ----
    alpha, beta = global_ctr * 100, (1 - global_ctr) * 100
    out["item_ctr_smooth"] = (df["clicks"] + alpha) / (df["impressions"] + alpha + beta)

    # ---- 时间特征:周期性用 sin/cos 编码 ----
    # 直接用 0-23 的话,23点和0点在模型看来差了 23,其实只差 1 小时
    out["hour_sin"] = np.sin(2 * np.pi * df["hour"] / 24)
    out["hour_cos"] = np.cos(2 * np.pi * df["hour"] / 24)
    out["is_weekend"] = df["weekday"].isin([5, 6]).astype(int)

    # ---- 交叉特征:最有效的那一类 ----
    out["user_cat_pref"] = df.apply(
        lambda r: r["user_cat_prefs"].get(r["item_category"], 0.0), axis=1)

    # ---- 新鲜度:物品发布多久了 ----
    out["item_age_hours"] = (df["request_time"] - df["publish_time"]) / 3600
    out["log_item_age"] = np.log1p(out["item_age_hours"].clip(lower=0))

    # ---- 高基数 ID:哈希分桶 ----
    out["author_hash"] = df["author_id"].apply(lambda x: hash(str(x)) % 1_000_000)

    return out

📏 怎么知道一个特征有没有用

方法 说明 成本
特征重要性 用 GBDT 跑一遍看 gain / split 次数 低,先做这个
Ablation(消融) 去掉这个特征,看 AUC 掉多少 中,最可靠的离线方法
覆盖率 这个特征有多少样本非空?<10% 的基本没用 极低,先查这个
AUC 增益 加入前后离线 AUC 对比
A/B 实验 最终裁判 ⭐ 高,但只有它算数

⚠️ 重要提醒:离线 AUC 涨了,线上不一定涨(见第 6、12 节)。 特征上线前的标准流程:覆盖率检查 → 穿越检查 → 离线 AUC → 小流量 A/B


🔗 这一章连到哪里

去哪为什么
ML基础 16分桶、归一化、log 变换、类别编码的通用版,本章只是把它搬到推荐场景
上线之后 04坑 2「离线/在线特征不一致」的系统解法:Feature Store 与一套代码双产出
Kaggle 01特征穿越在竞赛里怎么被发现,以及目标编码为什么必须用折外统计

✅ 检查点

  1. 特征的四个来源是什么?哪一类最重要?
  2. 为什么类别特征用 Embedding 而不用 One-Hot?
  3. 为什么推荐系统喜欢把数值特征分桶?
  4. FM 解决了什么问题?为什么用隐向量点积而不是直接给交叉权重?
  5. 什么是特征穿越?怎么自查?
  6. 新物品的 CTR 特征该怎么填?
👀 答案
  1. 用户侧、物品侧、上下文、交叉特征。最重要的是用户行为序列
  2. One-Hot 高维稀疏、学不到特征间关系;Embedding 低维稠密,语义相近的值向量也相近,且能泛化。
  3. 让模型能学到非线性关系,不必假设「数值大小和目标是线性关系」。
  4. 解决了人工交叉不可穷举 + 交叉特征稀疏学不动的问题。用隐向量点积可以把没共现过的特征对也算出权重(泛化)。
  5. 用了预测时刻拿不到的信息(通常是未来信息)。自查:问「线上服务那一刻我真的能拿到这个值吗?」
  6. 贝叶斯平滑,曝光量少时向全局平均 CTR 靠拢,而不是填 0。

🛑 可以停在这里

走神救援

特征四来源:用户/物品/上下文/交叉,其中「用户行为序列」最重要。类别特征→Embedding,数值特征→log变换或分桶。特征交叉是核心命题,演进:人工→FM(隐向量点积)→DeepFM→DIN(注意力)。六大坑:特征穿越(最致命)、离线在线不一致、时间窗口、新物品全0(用贝叶斯平滑)、低频ID、高基数(哈希分桶)。FM 用隐向量点积代替「每对特征一个权重」,才是特征交叉的关键突破:直接给权重的话,(女, 化妆品) 只要训练数据里没共现过就永远学不到;换成 <v_i, v_j> 之后,「女」可以从 (女, 连衣裙) 学、「化妆品」可以从 (25岁, 化妆品) 学,两者一点积就有了预测能力。所以 FM 是矩阵分解的推广——MF 只有 user_id 和 item_id 两个特征,FM 允许任意多个。⚠️ 自查特征穿越只需要一句话:「线上服务的那一毫秒,我真的能拿到这个值吗?」用「视频总播放量」当特征离线 AUC 0.95、上线暴死,就是因为那个量在预测时刻还没产生。⭐ 两个容易漏掉的手法:统计特征要同时喂 ctr_1h / 1d / 7d / 30d 多个时间窗口,长短窗口的含义完全不同;小时、星期这种周期特征要用 sin/cos 编码,否则模型会认为 23 点和 0 点相差 23。

下一节 👉 08-深度学习推荐模型.md

打卡记录保存在你的浏览器里,首页能看到总进度