4. 类别特征处理:独热编码、嵌入列与哈希列
好,咱们今天聊聊类别特征。说实话,这是我在实际项目里打交道最多的数据类型。你想想看,用户ID、商品类目、城市名称、设备型号……这些没法直接算大小的数据,在深度学习里怎么处理?
TensorFlow Estimator 给了我们三个利器:indicator_column、embedding_column 和 hashed_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 如何选择?
说了这么多,到底该用哪个?我总结一下:
- 类别少(<100)且样本充足:用
indicator_column,简单直接 - 类别多(100-10000)且需要语义学习:用
embedding_column,效果好 - 类别非常多或不断有新类别:用
hashed_column+embedding_column,灵活 - 类别多但不需要语义:
hashed_column+indicator_column,省事
嗯,这里要注意:嵌入列和哈希列不是互斥的。实际项目中,我经常把哈希列作为嵌入列的输入,这样既处理了未知类别,又享受了嵌入的降维好处。
类别特征处理没有银弹。独热编码适合小规模、嵌入列适合大规模且需要语义、哈希列适合动态增长。关键是根据你的数据特点和数据量来选,而不是盲目追求某个方法。
最后说一句:别怕试错。我刚开始做特征工程时,也经常选错方法。但每次踩坑都是成长,你慢慢就会形成自己的判断。好了,下一章咱们聊聊数值特征的处理,那个也有不少门道。