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 岁,购买概率变化多少」这种非线性关系了,直接学每个桶的权重就行。

避坑指南:我曾经把边界值设得太密,比如 [1, 2, 3, 4, 5, ...],结果分桶列退化成了数值列,完全没有起到泛化作用。分桶的边界要根据业务逻辑来定,不是越多越好。

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 列表和哈希分桶。到时候我会分享一些我在实际项目中踩过的坑,保证让你少走弯路。