在本教程中,我们将使用TensorFlow中的tf.estimator API来解决二元分类问题:给定有关某人(如年龄,教育程度,婚姻状况和职业(特征))的人口普查数据,尝试预测这个人每年是否能挣5万多美元(目标标签)。我们将训练一个逻辑回归模型,给定个人的信息,我们的模型将输出0到1之间的数字,这可以解释为个人年收入超过5万美元的概率。
开始
要尝试本教程的代码,先:
-
安装TensorFlow。
-
下载教程代码。
-
执行我们提供给您的数据下载脚本:
$ python data_download.py
-
使用以下命令执行教程代码以训练本教程中描述的线性模型:
$ python wide_deep.py --model_type=wide
继续阅读以了解此代码如何构建线性模型。
读取人口普查数据
我们将使用的数据集是人口普查收入数据集。我们提供了用于下载代码并执行一些额外的清理。
由于该任务是一个二元分类问题,因此我们将构造一个名为”label”的标签列,如果收入超过50K,则值为1,否则为0。相关信息,请参阅input_fn
,在。
接下来,我们来看看数据情况,看看可以使用哪些列来预测目标标签。这些列可以分为两类:类别和连续值:
- 类别数据:如果它的价值只能是有限集合中的一个类别。例如,一个人(妻子,丈夫,未婚等)或教育水平(高中,大学等)的关系状况是类别数据。
- 连续数据:如果其值可以是连续范围内的任何数值。例如,一个人的资本收益(例如$ 14,084)是一个连续的值。
以下是人口普查收入数据集中可用列的列表:
列名称 |
类型 |
描述 |
age |
连续 |
个人的年龄 |
workclass |
类别 |
个人拥有的雇主类型(政府,军队,私人等)。 |
fnlwgt |
连续 |
普查员认为观察所代表的人数(样本权重)。最终的权重将不会被使用。 |
education |
类别 |
这个人达到的最高教育水平。 |
education_num |
连续 |
数值形式的最高教育水平。 |
marital_status |
类别 |
个人的婚姻状况。 |
occupation |
类别 |
个人的职业。 |
relationship |
类别 |
妻子,Own-child,丈夫,Not-in-family,Other-relative,未婚。 |
race |
类别 |
白色,Asian-Pac-Islander,Amer-Indian-Eskimo,其他,黑色。 |
gender |
类别 |
女人男人。 |
capital_gain |
连续 |
资本收益记录。 |
capital_loss |
连续 |
记录资本损失。 |
hours_per_week |
连续 |
每周工作时间。 |
native_country |
类别 |
个人原籍国。 |
income |
类别 |
“>50K”或“ |
将数据转换为张量
在构建tf.estimator模型时,输入数据通过输入构建器函数来指定。此构建函数在稍后传递给tf.estimator.Estimator方法(例如,train
和evaluate
),之前不会被调用。这个函数的目的是构造输入数据,它的形式表示为tf.Tensor
或者tf.SparseTensor
。具体来说,输入生成器函数将成对返回以下内容:
features
:一个dict, 存储从特征列名到Tensors
或SparseTensors
的映射。
labels
: 一个Tensor
包含标签列。
features
的键将被用于在下一节中构建列。因为我们想在不同的数据上调用train
和evaluate
方法,我们定义一个方法,返回一个基于给定数据的输入函数。请注意,返回的输入函数将在构建TensorFlow图形时调用,而不是在运行图形时调用。它返回的是输入数据的表示,即Tensor
(或SparseTensor
)。
训练或者测试数据中的每个连续列将被转换成一个Tensor
,这通常是表示密集数据的良好格式。对于类别数据,我们必须将数据表示为一个SparseTensor
,这种数据格式适合表示稀疏数据。我们的input_fn
使用tf.data
API,可以很容易地将转换应用于我们的数据集:
def input_fn(data_file, num_epochs, shuffle, batch_size):
"""Generate an input function for the Estimator."""
assert tf.gfile.Exists(data_file), (
'%s not found. Please make sure you have either run data_download.py or '
'set both arguments --train_data and --test_data.' % data_file)
def parse_csv(value):
print('Parsing', data_file)
columns = tf.decode_csv(value, record_defaults=_CSV_COLUMN_DEFAULTS)
features = dict(zip(_CSV_COLUMNS, columns))
labels = features.pop('income_bracket')
return features, tf.equal(labels, '>50K')
# Extract lines from input files using the Dataset API.
dataset = tf.data.TextLineDataset(data_file)
if shuffle:
dataset = dataset.shuffle(buffer_size=_SHUFFLE_BUFFER)
dataset = dataset.map(parse_csv, num_parallel_calls=5)
# We call repeat after shuffling, rather than before, to prevent separate
# epochs from blending together.
dataset = dataset.repeat(num_epochs)
dataset = dataset.batch(batch_size)
iterator = dataset.make_one_shot_iterator()
features, labels = iterator.get_next()
return features, labels
模型的特征选择和特征工程
选择和构造正确的特征列是学习有效模型的关键。一个特征列可以是原始数据中的原始列之一(我们称它们为“基本特征列),或者基于在一个或多个基本列上定义的一些转换创建的任何新列(我们称之为“派生特征列)。基本上,”feature column”是可以用来预测目标标签的任何原始或衍生变量的抽象概念。
基本类别特征列
要为类别特征定义特征列,我们可以使用tf.feature_column API创建一个CategoricalColumn
。如果您知道列的所有可能的特征值的集合,并且特征值集合较小,您可以使用categorical_column_with_vocabulary_list
。列表中的每个键都将被分配一个从0开始的自增ID。例如,对于relationship
列,我们可以将特征字符串”Husband”分配给一个整数ID为0,”Not-in-family”分配给1,等等:
relationship = tf.feature_column.categorical_column_with_vocabulary_list(
'relationship', [
'Husband', 'Not-in-family', 'Wife', 'Own-child', 'Unmarried',
'Other-relative'])
如果我们事先不知道可能的值怎么办?这不是问题。我们可以用categorical_column_with_hash_bucket
代替:
occupation = tf.feature_column.categorical_column_with_hash_bucket(
'occupation', hash_bucket_size=1000)
特征列中的每个可能的值occupation
将被散列为整数ID。看下面的一个例子:
ID |
特征 |
… |
|
9 |
"Machine-op-inspct" |
… |
|
103 |
"Farming-fishing" |
… |
|
375 |
"Protective-serv" |
… |
|
无论我们选择哪种方式来定义一个SparseColumn
,每个特征字符串将通过查找固定映射或直接哈希映射到整数ID。请注意,散列冲突是可能的,但可能不会显著影响模型质量。在底层实现中,LinearModel
类负责管理映射和创建tf.Variable
存储每个特征ID的模型参数(也称为模型权重)。模型参数将通过稍后提到的模型训练过程来学习。
我们使用类似的技巧来定义其他的类别特征:
education = tf.feature_column.categorical_column_with_vocabulary_list(
'education', [
'Bachelors', 'HS-grad', '11th', 'Masters', '9th', 'Some-college',
'Assoc-acdm', 'Assoc-voc', '7th-8th', 'Doctorate', 'Prof-school',
'5th-6th', '10th', '1st-4th', 'Preschool', '12th'])
marital_status = tf.feature_column.categorical_column_with_vocabulary_list(
'marital_status', [
'Married-civ-spouse', 'Divorced', 'Married-spouse-absent',
'Never-married', 'Separated', 'Married-AF-spouse', 'Widowed'])
relationship = tf.feature_column.categorical_column_with_vocabulary_list(
'relationship', [
'Husband', 'Not-in-family', 'Wife', 'Own-child', 'Unmarried',
'Other-relative'])
workclass = tf.feature_column.categorical_column_with_vocabulary_list(
'workclass', [
'Self-emp-not-inc', 'Private', 'State-gov', 'Federal-gov',
'Local-gov', '?', 'Self-emp-inc', 'Without-pay', 'Never-worked'])
# To show an example of hashing:
occupation = tf.feature_column.categorical_column_with_hash_bucket(
'occupation', hash_bucket_size=1000)
基本连续特征列
同样,我们可以定义一个NumericColumn
,对于我们要在模型中使用的每个连续特征列:
age = tf.feature_column.numeric_column('age')
education_num = tf.feature_column.numeric_column('education_num')
capital_gain = tf.feature_column.numeric_column('capital_gain')
capital_loss = tf.feature_column.numeric_column('capital_loss')
hours_per_week = tf.feature_column.numeric_column('hours_per_week')
通过分桶将连续性特征的转为类别特征
有时连续特征和标签之间的关系不是线性的。作为一个假设的例子,一个人的收入在职业生涯初期可能会随着年龄的增长而增长,然后增长可能会放缓,最终退休后收入会减少。在这种情况下,使用原始的age
作为实值特征列可能不是一个好的选择,因为模型只能学习三种情况之一:
- 收入总是随着年龄增长而增长(正相关),
- 收入总是随着年龄的增长而减少(负相关),或者
- 不论年龄多少,收入都保持不变(不相关)
如果我们想分别学习收入与各年龄段之间的细粒度相关性,我们可以利用分桶(bucketization)。分桶是将连续特征的整个范围划分为一组连续的桶,然后根据该值落入哪个桶将原始数值特征转换成桶ID(作为分类特征)的过程。所以,我们可以定义一个bucketized_column
过度age
如:
age_buckets = tf.feature_column.bucketized_column(
age, boundaries=[18, 25, 30, 35, 40, 45, 50, 55, 60, 65])
其中boundaries
是桶边界的列表。在这种情况下,有10个边界,从而形成11个年龄组(从17岁以下,18-24,25-29,…,到65岁以上)。
用CrossedColumn交叉多列
单独使用每个基本特征列可能不足以解释数据。例如,对于不同的职业,教育与标签(赚取50000美元)之间的相关性可能不同。因此,如果我们只学习单一的模型权重education="Bachelors"
和education="Masters"
,我们将无法捕获每一个education-occupation组合(例如区分education="Bachelors" AND occupation="Exec-managerial"
和education="Bachelors" AND occupation="Craft-repair"
)。要了解不同的功能组合之间的差异,我们可以添加交叉的特征列到模型。
education_x_occupation = tf.feature_column.crossed_column(
['education', 'occupation'], hash_bucket_size=1000)
我们也可以使用多个列创建一个CrossedColumn
。每个组成列可以是基本类别特征列(SparseColumn
),bucketized实值特征列(BucketizedColumn
),甚至另一个CrossColumn
。这是一个例子:
age_buckets_x_education_x_occupation = tf.feature_column.crossed_column(
[age_buckets, 'education', 'occupation'], hash_bucket_size=1000)
定义Logistic回归模型
处理输入数据并定义所有特征列后,我们现在准备将它们放在一起并构建一个Logistic回归模型。在上一节中,我们已经看到了几种类型的基本和派生特征列,包括:
CategoricalColumn
NumericColumn
BucketizedColumn
CrossedColumn
所有这些都抽象FeatureColumn
类的子类,可以添加到模型的feature_columns
字段:
base_columns = [
education, marital_status, relationship, workclass, occupation,
age_buckets,
]
crossed_columns = [
tf.feature_column.crossed_column(
['education', 'occupation'], hash_bucket_size=1000),
tf.feature_column.crossed_column(
[age_buckets, 'education', 'occupation'], hash_bucket_size=1000),
]
model_dir = tempfile.mkdtemp()
model = tf.estimator.LinearClassifier(
model_dir=model_dir, feature_columns=base_columns + crossed_columns)
该模型还自动学习一个偏置项,它控制着没有任何特征的预测(更多解释请参见“逻辑回归的工作原理”一节)。学习的模型文件将被存储在model_dir
。
训练和评估我们的模型
现在让我们看看如何实际训练模型。使用tf.estimator API训练模型只是一个简单的命令:
model.train(input_fn=lambda: input_fn(train_data, num_epochs, True, batch_size))
在模型被训练之后,我们可以评估我们的模型在预测剩余数据标签上的效果:
results = model.evaluate(input_fn=lambda: input_fn(
test_data, 1, False, batch_size))
for key in sorted(results):
print('%s: %s' % (key, results[key]))
最终输出的第一行应该是这样的accuracy: 0.83557522
,这意味着准确率为83.6%。随意尝试更多的特征和转换,看看你是否能做得更好!
如果你想看到一个可用的端到端的例子,你可以下载我们的示例代码并设置model_type
标志为wide
。
加入正则化来防止过度拟合
正规化是一种用来避免过度拟合的技术。过度拟合时,模型在训练数据上表现良好,但在模型以前从未见过的测试数据(如实时流量)上却很糟糕。过度拟合通常发生在模型过于复杂的情况下,例如相对于观察到的训练数据的参数数量太多。正则化允许你控制你的模型的复杂性,并使模型在看不见的数据上有更好的泛化能力。
在线性模型库中,可以将L1和L2正则化添加到模型中,如下所示:
model = tf.estimator.LinearClassifier(
model_dir=model_dir, feature_columns=base_columns + crossed_columns,
optimizer=tf.train.FtrlOptimizer(
learning_rate=0.1,
l1_regularization_strength=1.0,
l2_regularization_strength=1.0))
L1和L2正则化之间的一个重要区别是,L1正则化倾向于使模型权重保持为零,从而创建更稀疏的模型,而L2正则化也尝试使模型权重接近于零,但不一定为零。因此,如果您增加L1正则化的强度,您将有一个较小的模型大小,因为许多模型权重将为零。当特征空间非常大但是稀疏时,以及当存在资源限制时,通常需要较小的模型,因为这些资源限制会阻止您提供太大的模型。
在实践中,你应该尝试L1,L2正则化强度的各种组合,找到最好的控制过度拟合的最佳参数,并给你一个理想的模型大小。
逻辑回归如何工作
最后,让我们花一点时间谈一谈Logistic回归模型实际上是什么。我们将标签表示为Y,并将观察到的特征集合表示为特征向量x = [x1, x2, …, xd]。我们定义Y = 1,如果个人获得>=5万美元; 否则Y = 0。在Logistic回归中,给定特征x的标签为正(Y = 1)的概率为:
其中w = [w1, w2, …, wd]是特征x = [x1,x2, …, xd]的模型权重 。 b是一个常被称为偏置的常量。该方程由两部分组成:线性模型和逻辑函数:
-
线性模型:首先,我们可以看到wT x + b = b + w1 x1 + … + wd xd是一个线性模型,输出是输入特征的线性函数X。偏置b是在没有观察到任何特征的情况下做出的预测。模型权重wi反映特征xi如何与正标签相关。如果xi与正标签正相关,则权重wi增加,并且概率P(Y = 1 | x)将更接近于1。另一方面,如果xi与正标签呈负相关,那么权重wi将减少,并且概率P(Y = 1 | x)将更接近于0 。
-
逻辑(Logistic)函数:其次,我们可以看到,对线性模型应用了逻辑函数(也称为S形函数)S(t)= 1 /(1+exp(-t))。逻辑函数用于将线性模型的输出从任何实数转换到[0,1]的范围内,可以被解释为一个概率。
模型训练是一个优化问题:目标是找到一组模型权重(即模型参数)来最小化定义在训练数据上损失函数,如逻辑回归模型的逻辑损失。损失函数测量实际标签与模型预测之间的差异。如果预测值非常接近实际标签,损失值将会很低;如果预测离标签很远,那么损失值就会很高。
深入学习
如果你有兴趣了解更多,请查看我们的Wide&Deep深度学习教程在这里我们将向您展示如何结合使用tf.estimator API联合训练线性模型和深度神经网络。