3. 特征列(Feature Column)入门:数值列、分桶列、类别列
好,咱们今天正式进入特征工程的核心环节——特征列(Feature Column)。
说实话,我刚开始用 TensorFlow 的时候,最头疼的就是数据预处理。你想想看,原始数据千奇百怪:有的是数字,有的是类别,有的需要分桶,有的需要 embedding。如果全靠手动写代码去处理,那工程量太大了。后来我发现,Estimator 配合 Feature Column,简直就是为这种场景量身定做的。
我个人习惯把 Feature Column 分成三大类:数值型、分桶型、类别型。今天咱们就一个一个来啃。
3.1 数值列(numeric_column)
数值列是最简单、最直接的。说白了,就是把一个数值特征原封不动地喂给模型。比如年龄、价格、温度这些。
它的用法非常直观:
import tensorflow as tf
# 定义一个数值列
age_column = tf.feature_column.numeric_column(key='age')
# 如果你需要指定默认值或数据类型
price_column = tf.feature_column.numeric_column(
key='price',
default_value=0.0,
dtype=tf.float32
)
嗯,这里要注意:key 必须和你输入数据中的字段名一致。我在项目中遇到过有人把 key 写错了,结果模型训练出来全是 NaN,排查了半天才发现是字段名对不上。
default_value。否则 TensorFlow 会直接报错,不会帮你自动填充。
3.2 分桶列(bucketized_column)
数值列虽然简单,但有时候直接输入原始数值并不好。比如年龄这个特征,20 岁和 21 岁差别不大,但 20 岁和 60 岁差别就很大。这时候,分桶就派上用场了。
分桶列,说白了就是把连续数值切成几个区间。每个区间变成一个离散的类别。
举个例子:
# 先定义一个数值列
age_column = tf.feature_column.numeric_column('age')
# 然后分桶
age_buckets = tf.feature_column.bucketized_column(
source_column=age_column,
boundaries=[18, 30, 45, 60]
)
这段代码的意思是:把年龄分成 5 个桶:
- 小于 18 岁 → 桶 0
- 18 到 30 岁 → 桶 1
- 30 到 45 岁 → 桶 2
- 45 到 60 岁 → 桶 3
- 大于等于 60 岁 → 桶 4
我曾经在一个电商推荐项目里用过这个技巧。当时用户年龄是连续值,模型怎么调都欠拟合。后来我把年龄分成了 5 个桶,效果立竿见影。为什么?因为模型不需要再去学习「年龄每增加 1 岁,购买概率变化多少」这种非线性关系了,直接学每个桶的权重就行。
3.3 类别列(categorical_column_with_identity)
接下来是类别列。类别特征在真实数据里太常见了:性别、城市、设备类型……
TensorFlow 提供了好几种处理类别特征的方式,今天咱们先讲最基础的一种——categorical_column_with_identity。
这个函数适用于已经编码好的整数类别。比如你的数据里,性别用 0 和 1 表示,城市用 0、1、2、3 表示。这时候直接用 identity 列就行。
# 假设城市编码为 0~9
city_column = tf.feature_column.categorical_column_with_identity(
key='city',
num_buckets=10
)
这里 num_buckets 必须大于等于你数据中最大的类别编号。如果数据里出现了 10,而 num_buckets 只设了 10,那就会报错。因为索引是从 0 开始的,0~9 共 10 个桶,10 已经越界了。
你可能会问:如果我的类别是字符串怎么办?比如城市名是 "北京"、"上海"、"广州"。那就不能用 identity 了,得用 categorical_column_with_vocabulary_list 或者 categorical_column_with_hash_bucket。这个咱们后面章节会详细讲。
- numeric_column:直接输入数值,适合连续特征。
- bucketized_column:把连续值切成离散区间,适合非线性关系。
- categorical_column_with_identity:处理已经编码好的整数类别。
3.4 三者如何配合使用?
在实际项目中,这三种列经常混在一起用。我举个例子:
# 定义特征列列表
feature_columns = [
# 数值列
tf.feature_column.numeric_column('income'),
# 分桶列
tf.feature_column.bucketized_column(
source_column=tf.feature_column.numeric_column('age'),
boundaries=[18, 30, 45, 60]
),
# 类别列
tf.feature_column.categorical_column_with_identity(
key='gender',
num_buckets=2
)
]
# 然后直接传给 Estimator
estimator = tf.estimator.LinearClassifier(
feature_columns=feature_columns,
model_dir='./model'
)
你看,代码非常干净。不需要手动做 one-hot 编码,不需要写复杂的预处理逻辑。Estimator 会自动帮你处理好一切。
我个人觉得,Feature Column 最大的价值就是把特征工程和模型训练解耦了。你可以在不修改模型代码的情况下,随意调整特征处理方式。这对于快速迭代实验来说,太重要了。
3.5 一个完整的实战小例子
咱们来跑一个完整的流程,感受一下:
import tensorflow as tf
import pandas as pd
# 模拟数据
data = pd.DataFrame({
'age': [25, 32, 47, 18, 60],
'income': [5000, 8000, 12000, 3000, 15000],
'gender': [0, 1, 0, 1, 0],
'label': [0, 1, 1, 0, 1]
})
# 定义特征列
age_bucket = tf.feature_column.bucketized_column(
source_column=tf.feature_column.numeric_column('age'),
boundaries=[18, 30, 45, 60]
)
feature_columns = [
tf.feature_column.numeric_column('income'),
age_bucket,
tf.feature_column.categorical_column_with_identity('gender', num_buckets=2)
]
# 构建输入函数
def input_fn():
return tf.data.Dataset.from_tensor_slices((
dict(data.drop('label', axis=1)),
data['label']
)).batch(2)
# 训练
estimator = tf.estimator.LinearClassifier(
feature_columns=feature_columns,
model_dir='./model_demo'
)
estimator.train(input_fn, steps=100)
跑完之后,你可以看看模型目录下的文件,TensorFlow 会自动保存 checkpoint、graph 等信息。这就是 Estimator 的另一个好处——工程化非常方便。
tf.feature_column.input_layer 把特征列转换成张量,打印出来看看形状对不对。这样可以提前发现维度不匹配的问题。
3.6 常见问题与避坑
| 问题 | 原因 | 解决方法 |
|---|---|---|
| 训练时出现 NaN | 数值列中有缺失值或异常值 | 设置 default_value,或先做数据清洗 |
| 分桶后效果反而变差 | 边界设置不合理,或桶数太多 | 根据业务逻辑调整边界,或者用分位数分桶 |
| identity 列报错越界 | num_buckets 小于实际类别数 |
检查数据中最大类别编号,确保 num_buckets 大于它 |
好了,这一章的内容就到这里。数值列、分桶列、类别列是 Feature Column 的三大基石。你只要把这三个搞明白了,后面再学 embedding 列、交叉列什么的,就会轻松很多。
下一章咱们会深入聊聊 类别特征的高级处理,包括 vocabulary 列表和哈希分桶。到时候我会分享一些我在实际项目中踩过的坑,保证让你少走弯路。