Wide&Deep模型主要两个重要的概念就是Memorization和Generalization,wide层采用LR模型加上大量原始特征和叉乘特征作为输入,‘记忆’历史数据中曾共同出现过的特征对。而deep层则主要为sparse特征学习低维的dense embeddings来捕获特征相关性,学习到的embedding本身会带一定的语义特征。
加入wide层学习浅层特征的动机:
- 因子分解机或者深度神经网络在低维稠密向量下学习,但是无法get到稀疏高维的情况,会对小众查询给出非零的预测,也就是过拟合;
- 线性模型用外积特征变换可以依赖少量的参数记住特征对
wide部分是一个广义的线性模型,输入的特征主要有两部分组成,一部分是原始的部分特征,另一部分是原始特征的交叉特征即cross product外积运算,当两个特征同时交叉出现时为1,任意一个特征不出现表示为0。
为什么Wide部分要用L1 FTRL训练?
对于wide部分训练时候使用的优化器是带L1正则的FTRL算法(Follow-the-regularized-leader),FTRL可以理解为稀疏性很好,精度又不错的随机梯度下降方法。W&D模型采用L1-FTRL是想让Wide部分变得更加的稀疏,即Wide部分的大部分参数都为0,这就大大压缩了模型权重及特征向量的维度。通过这样Wide部分模型训练完之后留下来的特征都是非常重要的,那么模型的“记忆能力”就可以理解为发现"直接的",“暴力的”,“显然的”关联规则的能力。
Deep部分是一个DNN模型,输入的特征主要分为两大类,一类是数值特征(可直接输入DNN),一类是类别特征(需要经过Embedding之后才能输入到DNN中)。Deep部分可以减少人工的特征参与,对历史上没出现过的情况有更好的泛化能力。对于Deep部分的DNN模型作者使用了深度学习常用的优化器AdaGrad,这也是为了使得模型可以得到更精确的解。
联合训练的公示如上,wide_n_deep模型的组合依赖于其输出的对数几率的加权作为预测。随后这一值被输入到一个一般的逻辑损失函数中联合训练。联合训练和拼装不同,其是在训练过程中同时优化wide和deep模型及总和的权重,而拼装是将模型分别训练,结果是在最后进行组合。整体的模型结构如图:
由上图可见,wide部分输入的是两个id类特征的乘积,id特征为User Installed App 和 Impression App,不难猜测Google的工程师使用这个组合特征的意图,是想发现当前曝光app和用户安装app的关联关系,以此来直接影响最终的得分。两个id类特征向量进行组合,在维度爆炸的同时,会让原本已经非常稀疏的multihot特征向量,变得更加稀疏。正因如此,wide部分的权重数量其实是海量的。为了不把数量如此之巨的权重都搬到线上进行model serving,采用FTRL过滤掉哪些稀疏特征无疑是非常好的工程经验。
然而Deep部分的输入,要么是Age,订单次数,评分这些数值类特征,要么是已经降维并稠密化的Embedding向量。所以Deep部分不存在严重的特征稀疏问题。
因此wide部分记住的是历史数据中那些常见、高频的模式,是推荐系统中的“红海”。实际上,Wide侧没有发现新的模式,只是学习到这些模式之间的权重,做一些模式的筛选。正因为Wide侧不能发现新模式,因此我们需要根据人工经验、业务背景,将我们认为有价值的、显而易见的特征及特征组合,喂入Wide侧。
而deep侧通过embedding的方式将categorical/id特征映射成稠密向量,让DNN学习到这些特征之间的深层交叉,以增强扩展能力。
这里使用tensorflow的API-estimator来实现wide_n_deep模型:
// """Example code for TensorFlow Wide & Deep Tutorial using TF.Learn API."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import argparse #argsparse是python的命令行解析的标准模块,内置于python,直接在命令行中就可以向程序中传入参数并让程序运行。
import sys
import tempfile #tempfile模块用于快速地创建名称唯一的临时文件供使用.
import pandas as pd
from six.moves import urllib
import tensorflow as tf
CSV_COLUMNS = [
"age", "workclass", "fnlwgt", "education", "education_num",
"marital_status", "occupation", "relationship", "race", "gender",
"capital_gain", "capital_loss", "hours_per_week", "native_country",
"income_bracket"
]
'''
https://zhuanlan.zhihu.com/p/73701872
常见的特征预处理:连续变量分箱、离散变量one-hot、离散指标embedding等
tensorflow提供了一个特征处理函数tf.feature_column,它通过对特征处理将数据输入网络并交由estimator来进行训练
categorical_column_with_vocabulary_list类别型函数,将类别按照顺序输出成0-n的整数,然后再用indicator_column做one-hot编码
当类别过多,无法一一列举,可以使用with_vocabulary_file,以文件形式输入
'''
gender = tf.feature_column.categorical_column_with_vocabulary_list(
"gender", ["Female", "Male"])
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:
'''
#处理包含大量文字或数字类别特征时采用hash可快速建立对照表,缺点是会存在哈希冲突问题,hash_bucket_size的大小一般设置为总类别数的2-5倍,该函数适用于不能确定所有类别样式的类别变量
#若vocabulary方便得到,直接用onehot就好。但实际应用时很多id特征在数据集动态变化时vocabulary几乎无法提前算好。
#用hash的好处就是不必花精力维护一个随数据集变化的vocabulary,而且实践中发现hash相比onehot精度通常不会有损失。
#设置更大的hash_bucket_size不只是为了降低冲突,也可以为数据动态变化带来的新增特征留余地。
'''
occupation = tf.feature_column.categorical_column_with_hash_bucket(
"occupation", hash_bucket_size=1000)
native_country = tf.feature_column.categorical_column_with_hash_bucket(
"native_country", hash_bucket_size=1000)
# Continuous base columns.
#dense类函数,该函数主要用于处理连续型变量,即可以是float类型也可以是int类似,从table中读取对应的(key)column,并把它转成dtype的格式
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")
# Transformations.
#该函数将连续变量进行分桶离散化,输出one-hot的结果,方便连续值指标与分类变量进行交叉特征构建。譬如0-0.5一类,0.5-1一类
age_buckets = tf.feature_column.bucketized_column(
age, boundaries=[18, 25, 30, 35, 40, 45, 50, 55, 60, 65])
# Wide columns and deep columns.
#Wide部分使用了规范化后的连续特征、离散特征、交叉特征,wide_columns = base_columns + crossed_column
base_columns = [
#基本特征列,全是离散特征
gender, education, marital_status, relationship, workclass, occupation,
native_country, age_buckets,
]
#交叉特征列,其中crossed_column是特征交叉,在有些情况下,特征独自编码与多维特征交叉后的特征特性会有不一样的结果。该函数不能对hash映射之后的特征进行交叉
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),
tf.feature_column.crossed_column(
["native_country", "occupation"], hash_bucket_size=1000)
]
#设定Deep层特征
'''
Deep层主要针对离散特征进行处理,其中处理方式有:
1. Sparse Features -> Embedding vector -> 串联(连续特征),其中Embedding Values随机初始化。
2. 另外一种处理离散特征的方法是:one-hot和multi-hot representation. 此方法适用于低维度特征,其中embedding是通用的做法
其中:采用embedding_column(embedding)和indicator_column(multi-hot)API
'''
deep_columns = [
tf.feature_column.indicator_column(workclass),
tf.feature_column.indicator_column(education),
tf.feature_column.indicator_column(gender),
tf.feature_column.indicator_column(relationship),
# To show an example of embedding
tf.feature_column.embedding_column(native_country, dimension=8),
tf.feature_column.embedding_column(occupation, dimension=8),
age,
education_num,
capital_gain,
capital_loss,
hours_per_week,
]
def maybe_download(train_data, test_data):
"""Maybe downloads training data and returns train and test file names."""
if train_data:
train_file_name = train_data
else:
'''
tempfile.NamedTemporaryFile()
返回一个类文件对象,该对象可被用做为一个临时存储区域。结果对象可以被用作为一个上下文管理器。
在完成文件对象的上下文或销毁后,临时文件将会被从文件系统中移除。'''
train_file = tempfile.NamedTemporaryFile(delete=False)
urllib.request.urlretrieve(
"https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data",
train_file.name) # pylint: disable=line-too-long
train_file_name = train_file.name
train_file.close()
print("Training data is downloaded to %s" % train_file_name)
if test_data:
test_file_name = test_data
else:
test_file = tempfile.NamedTemporaryFile(delete=False)
urllib.request.urlretrieve(
"https://archive.ics.uci.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
def build_estimator(model_dir, model_type):
"""Build an estimator."""
if model_type == "wide":
m = tf.estimator.LinearClassifier(
model_dir=model_dir, feature_columns=base_columns + crossed_columns)
elif model_type == "deep":
m = tf.estimator.DNNClassifier(
model_dir=model_dir,
feature_columns=deep_columns,
hidden_units=[100, 50])
else:
m = tf.estimator.DNNLinearCombinedClassifier(
model_dir=model_dir,
linear_feature_columns=crossed_columns,
dnn_feature_columns=deep_columns,
dnn_hidden_units=[100, 50])
return m
def input_fn(data_file, num_epochs, shuffle):
"""Input builder function."""
df_data = pd.read_csv(
tf.gfile.Open(data_file), # 获取文本操作句柄,类似于python提供的文本操作open()函数,filename是要打开的文件名,mode是以何种方式去读写
names=CSV_COLUMNS,
skipinitialspace=True,
engine="python",
skiprows=1)
# remove NaN elements
df_data = df_data.dropna(how="any", axis=0)
labels = df_data["income_bracket"].apply(lambda x: ">50K" in x).astype(int)
return tf.estimator.inputs.pandas_input_fn( #数据load进来之后,输入分类器之前,还要经过一个input_fn的函数,用input_fn来喂给一个神经网络回归器数据
x=df_data,
y=labels,
batch_size=100,
num_epochs=num_epochs,
shuffle=shuffle,
num_threads=5)
def train_and_eval(model_dir, model_type, train_steps, train_data, test_data):
"""Train and evaluate the model."""
train_file_name, test_file_name = maybe_download(train_data, test_data)
'''
tempfile.mkdtemp()
以最安全的方式创建一个临时目录。该文件只对创建它的用户ID开放读写权限。
不同于TemporaryFile(),mkdtemp()的使用者必须为对目录的删除操作或内容改写操作负责。
'''
model_dir = tempfile.mkdtemp() if not model_dir else model_dir
m = build_estimator(model_dir, model_type)
# set num_epochs to None to get infinite stream of data.
m.train(
input_fn=input_fn(train_file_name, num_epochs=None, shuffle=True),
steps=train_steps)
# set steps to None to run evaluation until all data consumed.
results = m.evaluate(
input_fn=input_fn(test_file_name, num_epochs=1, shuffle=False),
steps=None)
print("model directory = %s" % model_dir)
for key in sorted(results):
print("%s: %s" % (key, results[key]))
FLAGS = None
def main(_):
train_and_eval(FLAGS.model_dir, FLAGS.model_type, FLAGS.train_steps,
FLAGS.train_data, FLAGS.test_data)
'''
1. import argparse 首先导入模块
2. parser = argparse.ArgumentParser() 创建一个解析对象
3. parser.add_argument() 向该对象中添加你要关注的命令行参数和选项
4. parser.parse_args() 进行解析
'''
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.register("type", "bool", lambda v: v.lower() == "true")
parser.add_argument(
"--model_dir",
type=str,
default="",
help="Base directory for output models."
) #help - 设置这个选项的帮助信息
parser.add_argument(
"--model_type",
type=str,
default="wide_n_deep",
help="Valid model types: {'wide', 'deep', 'wide_n_deep'}."
)
parser.add_argument(
"--train_steps",
type=int,
default=2000,
help="Number of training steps."
)
parser.add_argument(
"--train_data",
type=str,
default="",
help="Path to the training data."
)
parser.add_argument(
"--test_data",
type=str,
default="",
help="Path to the test data."
)
FLAGS, unparsed = parser.parse_known_args()
tf.app.run(main=main, argv=[sys.argv[0]] + unparsed)
'''
argv[0]代表模块文件名、argv[1]代表传入的第一个命令行参数
argv是sys模块的一个全局变量,也称sys模块的一个属性!
argv本身为一个list类型的对象,该对象持有的第1个元素是命令行中传入的模块名、从第2个元素开始(含),均为命令行中传入的参数!
python temp.py a b c d 说明:命令行运行temp.py模块,同时传入4个参数:a、b、c、d
sys.argv == ["temp.py","a","b","c","d"] sys.argv[0] == "temp.py" sys.argv[1] == "a"
'''
参考链接如下:
[1]: https://github.com/datawhalechina/team-learning-rs/tree/master/DeepRecommendationModel
[2]:https://zhuanlan.zhihu.com/p/142958834