4. 类别特征处理:独热编码、嵌入列与哈希列

好,咱们今天聊聊类别特征。说实话,这是我在实际项目里打交道最多的数据类型。你想想看,用户ID、商品类目、城市名称、设备型号……这些没法直接算大小的数据,在深度学习里怎么处理?

TensorFlow Estimator 给了我们三个利器:indicator_columnembedding_columnhashed_column。每个都有它的脾气,用对了事半功倍,用错了……嗯,我踩过不少坑。

4.1 独热编码:最直接的方式

先说说 indicator_column。说白了,就是把每个类别变成一个独立的维度。比如颜色有红、黄、蓝三种,那就用三个二进制位表示:红是 [1,0,0],黄是 [0,1,0],蓝是 [0,0,1]。

代码写起来很简单:

import tensorflow as tf

# 先定义一个类别列
color = tf.feature_column.categorical_column_with_vocabulary_list(
    'color', ['red', 'yellow', 'blue'])

# 再包装成指示列
color_indicator = tf.feature_column.indicator_column(color)

我在项目中遇到过一个问题:当类别数量特别多的时候,比如用户ID有100万个,独热编码就会产生100万维的向量。这会导致两个麻烦:一是内存爆炸,二是模型参数太多,训练慢得要命。

⚠️ 避坑指南
我曾经在一个推荐系统项目里,直接用独热编码处理了10万个商品ID。结果模型文件大小直接飙到几个GB,线上推理延迟高得离谱。后来才意识到,独热编码只适合类别数在几十到几百的场景。

那什么时候用独热编码呢?我个人习惯是:类别数小于100,且每个类别都有足够样本的时候。比如性别、星期几、季节这种。

4.2 嵌入列:降维的艺术

当类别数很多时,embedding_column 就派上用场了。它把每个类别映射到一个低维的稠密向量里。比如100万个用户ID,我们可以映射到64维的向量空间。

这样做的好处很明显:

  • 降维:从100万维降到64维,参数数量大幅减少
  • 学习语义关系:相似的类别在向量空间里距离更近
  • 泛化能力:没见过的新类别也能通过向量相似度找到参考

代码示例:

# 假设有10万个商品ID
item_id = tf.feature_column.categorical_column_with_hash_bucket(
    'item_id', hash_bucket_size=100000)

# 嵌入到32维空间
item_embedding = tf.feature_column.embedding_column(
    categorical_column=item_id,
    dimension=32)

这里有个关键参数 dimension,也就是嵌入维度。选多大合适?我有个经验公式:

类别数量 推荐嵌入维度 说明
100 - 1000 8 - 16 类别少,维度不用太高
1000 - 10000 16 - 64 中等规模,平衡表达力
10000 - 100000 64 - 128 大类别,需要足够容量
100000+ 128 - 256 超大类别,注意过拟合
💡 小技巧
嵌入维度一般取类别数的4次方根再乘以2。比如10000个类别,10000^(1/4) ≈ 10,再乘以2就是20维。当然这只是起点,实际要调参。

我记得有一次做电商搜索,商品类目有5000多个。一开始用了128维嵌入,模型训练了三天还没收敛。后来降到32维,一天就跑完了,效果反而更好。你想想看,维度太高容易过拟合,尤其是样本不够多的时候。

4.3 哈希列:处理未知类别

最后说说 hashed_column。这个函数解决了一个很实际的问题:训练时没见过的新类别怎么办?

比如你的模型只见过1000个城市,但线上突然来了个新城市。如果用词汇表方式,这个新城市就变成了未知值,模型没法处理。哈希列的做法是:对类别名称做哈希运算,然后映射到固定数量的桶里。

# 哈希到1024个桶
city_hashed = tf.feature_column.categorical_column_with_hash_bucket(
    'city', hash_bucket_size=1024)

# 可以继续包装成嵌入列
city_embedding = tf.feature_column.embedding_column(
    categorical_column=city_hashed,
    dimension=16)

哈希列的好处是:

  • 固定大小:不管有多少类别,桶数量是固定的
  • 处理新类别:任何新值都能映射到某个桶里
  • 节省内存:不需要维护词汇表
⚠️ 哈希冲突
哈希列有个天然问题:不同的类别可能映射到同一个桶。比如"北京"和"上海"如果哈希值一样,模型就认为它们是一回事。桶数量越大,冲突概率越低。我一般设桶数量为类别数的2到4倍。

我曾经在一个日志分析系统里,用哈希列处理了上百万个不同的URL。桶数量设了500万,冲突率控制在1%以下。效果还不错,模型能正常工作,而且不需要定期更新词汇表。

4.4 如何选择?

说了这么多,到底该用哪个?我总结一下:

  1. 类别少(<100)且样本充足:用 indicator_column,简单直接
  2. 类别多(100-10000)且需要语义学习:用 embedding_column,效果好
  3. 类别非常多或不断有新类别:用 hashed_column + embedding_column,灵活
  4. 类别多但不需要语义hashed_column + indicator_column,省事

嗯,这里要注意:嵌入列和哈希列不是互斥的。实际项目中,我经常把哈希列作为嵌入列的输入,这样既处理了未知类别,又享受了嵌入的降维好处。

🔑 核心要点
类别特征处理没有银弹。独热编码适合小规模、嵌入列适合大规模且需要语义、哈希列适合动态增长。关键是根据你的数据特点和数据量来选,而不是盲目追求某个方法。

最后说一句:别怕试错。我刚开始做特征工程时,也经常选错方法。但每次踩坑都是成长,你慢慢就会形成自己的判断。好了,下一章咱们聊聊数值特征的处理,那个也有不少门道。