当前位置: 首页>>技术教程>>正文


TensorFlow线性模型教程

在本教程中,我们将使用TensorFlow中的tf.estimator API来解决二元分类问题:给定有关某人(如年龄,教育程度,婚姻状况和职业(特征))的人口普查数据,尝试预测这个人每年是否能挣5万多美元(目标标签)。我们将训练一个逻辑回归模型,给定个人的信息,我们的模型将输出0到1之间的数字,这可以解释为个人年收入超过5万美元的概率。

开始

要尝试本教程的代码,先:

  1. 安装TensorFlow

  2. 下载教程代码

  3. 执行我们提供给您的数据下载脚本:

    $ python data_download.py
    
  4. 使用以下命令执行教程代码以训练本教程中描述的线性模型:

    $ 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方法(例如,trainevaluate),之前不会被调用。这个函数的目的是构造输入数据,它的形式表示为tf.Tensor或者tf.SparseTensor。具体来说,输入生成器函数将成对返回以下内容:

  1. features:一个dict, 存储从特征列名到TensorsSparseTensors的映射。
  2. labels: 一个Tensor包含标签列。

features的键将被用于在下一节中构建列。因为我们想在不同的数据上调用trainevaluate方法,我们定义一个方法,返回一个基于给定数据的输入函数。请注意,返回的输入函数将在构建TensorFlow图形时调用,而不是在运行图形时调用。它返回的是输入数据的表示,即Tensor(或SparseTensor)。

训练或者测试数据中的每个连续列将被转换成一个Tensor,这通常是表示密集数据的良好格式。对于类别数据,我们必须将数据表示为一个SparseTensor,这种数据格式适合表示稀疏数据。我们的input_fn使用tf.dataAPI,可以很容易地将转换应用于我们的数据集:

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作为实值特征列可能不是一个好的选择,因为模型只能学习三种情况之一:

  1. 收入总是随着年龄增长而增长(正相关),
  2. 收入总是随着年龄的增长而减少(负相关),或者
  3. 不论年龄多少,收入都保持不变(不相关)

如果我们想分别学习收入与各年龄段之间的细粒度相关性,我们可以利用分桶(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联合训练线性模型和深度神经网络。

参考资料

本文由《纯净天空》出品。文章地址: https://vimsky.com/article/3658.html,未经允许,请勿转载。