之前对 Wide And Deep 模型看过一点文章,但是没有深入了解,这两天抽出时间来仔细看了下相关代码和资料,然后写点初步的总结,总体来说还是很有意思的想法,把深度学习突破了在图像和语言领域的限制,用到了以往机器学习的领域,并且取得了更好的结果。
核心思想是?
wide and deep 模型的核心思想是结合线性模型的记忆能力(memorization)和 DNN 模型的泛化能力(generalization),在训练过程中同时优化两个模型的参数,从而达到整体模型的预测能力最优。
模型的特征构建与以往的线性模型、神经网络模型比较有什么特点?
特征的种类如下:
(第一行是 contrib 模块的 API,下面一行是正式的 API)
tf.contrib.layers.real_valued_column 或者
tf.feature_column.numeric_column
连续数值特征
tf.contrib.layers.sparse_column_with_keys 或者
tf.feature_column.categorical_column_with_vocabulary_list
按照自定义的字典将类别特征映射到数值,适合特征种类较少时候使用
tf.contrib.layers.sparse_column_with_hash_bucket 或者
tf.feature_column.categorical_column_with_hash_bucket
自动将类别特征映射到数值,适合特征种类较多时候使用
tf.contrib.layers.bucketized_column 或者
tf.feature_column.bucketized_column
把连续特征按照区间映射为类别特征
tf.contrib.layers.crossed_column 或者
tf.feature_column.crossed_column
特征相乘生成的交叉特征
可以看到,特征的种类还是类似于以往的机器学习领域的特征,以往做过机器学习项目的会比较熟悉,与图像语音等领域的还是有较大区别。
离散的特征怎么传入神经网络模型?
连续的特征可以直接传入神经网络,但是离散的特征需要做处理之后再传入:
To feed sparse features into DNN models, wrap the column with `embedding_column` or `one_hot_column`. `one_hot_column` will create a dense boolean tensor with an entry for each possible value, and thus the computation cost is linear in the number of possible values versus the number of values that occur in the sparse tensor. Thus using a “one_hot_column” is only recommended for features with only a few possible values. For features with many possible values or for very sparse features, `embedding_column` is recommended.
有两种方式进行处理,可以使用one-hot 编码处理神经网络输入的离散特征,也可以使用 embedding 处理;
建议该特征的数值种类较少时候使用one-hot 编码,在该特征的数值种类较多的情况下使用 embedding 编码,这里之所以要这么处理,我理解的原因是独热编码如果数值种类很多,会导致编码后的特征维度太高,从而学习到的信息过于分散,因此,在种类很多的时候,需要先将超高维的种类离散特征进行压缩embedding,再送入神经网络;
那么,究竟什么是 embedding 处理呢?
什么是 embedding_column ? 什么时候使用?
embedding_weights = variable_scope.get_variable(
name='embedding_weights',
shape=(self.categorical_column._num_buckets, self.dimension), # pylint: disable=protected-access
dtype=dtypes.float32,
initializer=self.initializer,
trainable=self.trainable and trainable)
这是把离散特征embedding 操作的核心,它把原始的类别数值映射到这个权值矩阵,其实相当于神经网络的权值,后续如果是trainable的话,我们就会把这个当做网络的权值矩阵进行训练,但是在用的时候,就把这个当成一个embedding表,按id去取每个特征的embedding 后的数值。(这其实就类似于词向量了,把每个单词映射到一个词向量。)
什么是 sparseTensor? 什么时候使用这种数据类型?
它是用(位置,值,形状) 三个元素简略的表示一个矩阵的方法,例如:
SparseTensor(indices=[[0, 0], [1, 2]], values=[1, 2], dense_shape=[3, 4])
表示如下矩阵:
[[1, 0, 0, 0]
[0, 0, 2, 0]
[0, 0, 0, 0]]
也就是说它表明,在(0,0)的位置有1, (1,2)的位置存在2;
它只针对类别特征,适合于表示矩阵中大量元素为0的情况,会大大减少矩阵占用的存储量;在案例中,我发现它经常用来作为一个类别特征的中间变量矩阵,用来减小内存占用,例如:
这个代码中把所有的类别变量都用 SparseTensor 做了一个转换:
categorical_cols = {k: tf.SparseTensor(
indices=[[i,0] for i in range( df[k].size)],
values = df[k].values, # 三个元素
shape=[df[k].size,1])
for k in CATEGORICAL_COLUMNS}
值得注意的是,sparseTensor 本质上还是一个矩阵,它只是把矩阵用另外一种形式来表示了。
两个模型使用不同的优化器是什么? 怎么在loss层面配合?
以 tf.estimator.DNNLinearCombinedClassifier() 为例,进入该函数发现:linear_optimizer=’Ftrl’, 线性模型使用’Ftrl’优化器,
dnn_optimizer=’Adagrad’, 神经网络使用’Adagrad’优化器;
如果分类数量是2,loss使用
_binary_logistic_head_with_sigmoid_cross_entropy_loss
调用
nn.sigmoid_cross_entropy_with_logits(labels=labels, logits=logits, name='loss')
如果分类数量大于2,loss使用
_multi_class_head_with_softmax_cross_entropy_loss
调用
losses.sparse_softmax_cross_entropy(labels=label_ids,
logits=logits, reduction=losses.Reduction.NONE)
最后怎么得到两个模型结合的预测结果的?
直接把两个模型的结果相加:
logits = dnn_logits + linear_logits
很出乎意料的方式啊,居然是直接相加的,然后是上面一点提到的操作,用这个logits 和label 得到 Loss,再将 Loss 分别反传回两个独立的优化器分别优化两个模型的权重。
两个模型各自使用的特征是什么?
先看代码:
离散特征和连续特征分别如下:
# 类别特征
CATEGORICAL_COLUMNS = ["workclass", "education", "marital_status", "occupation",
"relationship", "race", "gender", "native_country"]
# 连续特征
CONTINUOUS_COLUMNS = ["age", "education_num", "capital_gain", "capital_loss",
"hours_per_week"]
两个模块对特征的使用情况如下:
wide_columns = [gender, native_country,education, occupation, workclass, relationship, age_buckets,
tf.contrib.layers.crossed_column([education, occupation], hash_bucket_size=int(1e4)),
tf.contrib.layers.crossed_column([age_buckets, education, occupation], hash_bucket_size=int(1e6)),
tf.contrib.layers.crossed_column([native_country, occupation],hash_bucket_size=int(1e4))]
#embedding_column用来表示类别型的变量
deep_columns = [tf.contrib.layers.embedding_column(workclass, dimension=8),
tf.contrib.layers.embedding_column(education, dimension=8),
tf.contrib.layers.embedding_column(gender, dimension=8),
tf.contrib.layers.embedding_column(relationship, dimension=8),
tf.contrib.layers.embedding_column(native_country,dimension=8),
tf.contrib.layers.embedding_column(occupation, dimension=8),
age,education_num,capital_gain,capital_loss,hours_per_week,]
wide 模型的特征都是离散特征、 离散特征之间的交互作用特征;
deep 模型的特征则是离散特征embedding 加上连续特征;
wide 端模型和 deep 端模型只需要分别专注于擅长的方面,wide 端模型通过离散特征的交叉组合进行 memorization,deep 端模型通过特征的 embedding 进行 generalization,这样单个模型的大小和复杂度也能得到控制,而整体模型的性能仍能得到提高。
但是这种做法是不是一定要遵循,连续特征是否一定不要放入线性模型中去?我认为不一定,在以往的线性模型建模中,连续特征也是起到了很大作用的,所以可以在实践中进行尝试,不一定非得遵循这种做法。
几点疑问:
- 所有的输入到 deep 端DNN的连续特征都没有做归一化的处理?
- 报错 InternalError (see above for traceback): Blas GEMM launch failed : a.shape=(1282, 53), b.shape=(53, 100), m=1282, n=100, k=53 无法解决,网上查资料说是因为GPU 内存不够,但即便把数据量调整到很小后还是报错,如果有人碰到这个问题的还希望留言答疑。
- 无法查看 bucketized_column,sparse_column_with_hash_bucket 等处理之后的特征数值,调用该API 后只是定义了该特征列,无法看到它的实际数值。
相关代码:
import tempfile
import tensorflow as tf
from six.moves import urllib
import pandas as pd
flags = tf.app.flags
FLAGS = flags.FLAGS
flags.DEFINE_string("model_dir","","Base directory for output models.")
flags.DEFINE_string("model_type","wide_n_deep","valid model types:{'wide','deep', 'wide_n_deep'")
flags.DEFINE_integer("train_steps",200,"Number of training steps.")
flags.DEFINE_string("train_data","", "Path to the training data.")
flags.DEFINE_string("test_data", "", "path to the test data")
COLUMNS = ["age", "workclass", "fnlwgt", "education", "education_num",
"marital_status", "occupation", "relationship", "race", "gender",
"capital_gain", "capital_loss", "hours_per_week", "native_country",
"income_bracket"]
LABEL_COLUMN = "label"
CATEGORICAL_COLUMNS = ["workclass", "education", "marital_status", "occupation",
"relationship", "race", "gender", "native_country"]
CONTINUOUS_COLUMNS = ["age", "education_num", "capital_gain", "capital_loss",
"hours_per_week"]
# download test and train data
def maybe_download():
if FLAGS.train_data:
train_data_file = FLAGS.train_data
else:
train_file = tempfile.NamedTemporaryFile(delete=False)
urllib.request.urlretrieve("http://mlr.cs.umass.edu/ml/machine-learning-databases/adult/adult.data", train_file.name)
train_file_name = train_file.name
train_file.close()
print("Training data is downloaded to %s" % train_file_name)
if FLAGS.test_data:
test_file_name = FLAGS.test_data
else:
test_file = tempfile.NamedTemporaryFile(delete=False)
urllib.request.urlretrieve("http://mlr.cs.umass.edu/ml/machine-learning-databases/adult/adult.test",
test_file.name) # pylint: disable=line-too-long
test_file_name = test_file.name
test_file.close()
print("Test data is downloaded to %s" % test_file_name)
return train_file_name, test_file_name
# build the estimator
def build_estimator(model_dir):
# 离散分类别的
gender = tf.contrib.layers.sparse_column_with_keys(column_name="gender", keys=["female","male"])
education = tf.contrib.layers.sparse_column_with_hash_bucket("education", hash_bucket_size = 1000)
relationship = tf.contrib.layers.sparse_column_with_hash_bucket("relationship", hash_bucket_size = 100)
workclass = tf.contrib.layers.sparse_column_with_hash_bucket("workclass", hash_bucket_size=100)
occupation = tf.contrib.layers.sparse_column_with_hash_bucket("occupation", hash_bucket_size=1000)
native_country = tf.contrib.layers.sparse_column_with_hash_bucket( "native_country", hash_bucket_size=1000)
# Continuous base columns.
age = tf.contrib.layers.real_valued_column("age")
education_num = tf.contrib.layers.real_valued_column("education_num")
capital_gain = tf.contrib.layers.real_valued_column("capital_gain")
capital_loss = tf.contrib.layers.real_valued_column("capital_loss")
hours_per_week = tf.contrib.layers.real_valued_column("hours_per_week")
#类别转换
age_buckets = tf.contrib.layers.bucketized_column(age, boundaries= [18,25, 30, 35, 40, 45, 50, 55, 60, 65])
wide_columns = [gender, native_country,education, occupation, workclass, relationship, age_buckets,
tf.contrib.layers.crossed_column([education, occupation], hash_bucket_size=int(1e4)),
tf.contrib.layers.crossed_column([age_buckets, education, occupation], hash_bucket_size=int(1e6)),
tf.contrib.layers.crossed_column([native_country, occupation],hash_bucket_size=int(1e4))]
#embedding_column用来表示类别型的变量
deep_columns = [tf.contrib.layers.embedding_column(workclass, dimension=8),
tf.contrib.layers.embedding_column(education, dimension=8),
tf.contrib.layers.embedding_column(gender, dimension=8),
tf.contrib.layers.embedding_column(relationship, dimension=8),
tf.contrib.layers.embedding_column(native_country,dimension=8),
tf.contrib.layers.embedding_column(occupation, dimension=8),
age,education_num,capital_gain,capital_loss,hours_per_week,]
if FLAGS.model_type =="wide":
m = tf.contrib.learn.LinearClassifier(model_dir=model_dir,feature_columns=wide_columns)
elif FLAGS.model_type == "deep":
m = tf.contrib.learn.DNNClassifier(model_dir=model_dir, feature_columns=deep_columns, hidden_units=[100,50])
else:
m = tf.contrib.learn.DNNLinearCombinedClassifier(model_dir=model_dir, linear_feature_columns=wide_columns, dnn_feature_columns = deep_columns, dnn_hidden_units=[100,50])
return m
def input_fn(df):
continuous_cols = {k: tf.constant(df[k].values) for k in CONTINUOUS_COLUMNS}
categorical_cols = {k: tf.SparseTensor(indices=[[i,0] for i in range( df[k].size)], values = df[k].values, shape=[df[k].size,1]) for k in CATEGORICAL_COLUMNS}#原文例子为dense_shape
feature_cols = dict(continuous_cols)
feature_cols.update(categorical_cols)
label = tf.constant(df[LABEL_COLUMN].values)
return feature_cols, label
def train_and_eval():
train_file_name, test_file_name = maybe_download()
df_train = pd.read_csv(
tf.gfile.Open(train_file_name),
names=COLUMNS,
skipinitialspace=True,
engine="python"
)
df_test = pd.read_csv(
tf.gfile.Open(test_file_name),
names=COLUMNS,
skipinitialspace=True,
skiprows=1,
engine="python"
)
# drop Not a number elements
df_train = df_train.dropna(how='any',axis=0)
df_test = df_test.dropna(how='any', axis=0)
#convert >50 to 1
df_train[LABEL_COLUMN] = (
df_train["income_bracket"].apply(lambda x: ">50" in x).astype(int)
)
df_test[LABEL_COLUMN] = (
df_test["income_bracket"].apply(lambda x: ">50K" in x)).astype(int)
model_dir = tempfile.mkdtemp() if not FLAGS.model_dir else FLAGS.model_dir
print("model dir = %s" % model_dir)
m = build_estimator(model_dir)
print (FLAGS.train_steps)
m.fit(input_fn=lambda: input_fn(df_train),
steps=FLAGS.train_steps)
results = m.evaluate(input_fn=lambda: input_fn(df_test), steps=1)
for key in sorted(results):
print("%s: %s"%(key, results[key]))
def main(_):
train_and_eval()
if __name__ == "__main__":
tf.app.run()
资料链接:
The Wide and Deep Learning Model(译文+Tensorlfow源码解析)
tensorflow/models (这里有官方示例代码)