基于用户画像的广告投放是优化投放效果,实现精准营销的基础;而人口统计属性中的性别、年龄等标签是用户画像信息的基础。那么如何尽可能准确地标注数据呢?这就是机器学习派上用场的地方。本文将以性别标签为例,介绍用于人口属性标签预测的机器学习模型的构建与优化。性别标签预测过程通常,无监督学习不仅难以学习到有用的信息,而且难以评估学习效果。所以,如果可能的话,我们尽量把问题转化为监督学习。性别标签也是如此。我们可以利用可信的性别样本数据,加上从TalkingData收集的原始数据中提取的有用信息,将性别标签的生产任务转化为有监督的机器学习任务。更具体的,将male/female分别作为1/0标签(Label,也就是常说的Y值,为了表述方便,我们分别将male/female作为1/0标签),这样task性别标签转化为两个分类任务。性别标签的制作流程图如下:简单来说,输入是具有可信性别信息的样本数据,从最近活跃的原始数据中提取有用的特征;两者加入后,我们就可以得到一个模型,可以直接用于建模数据集;模型根据数据集学习一个性别预测模型;然后用模型对所有样本进行预测,从而得到所有样本的性别得分。至此,模型部分的工作基本完成;最后一步是确定阈值并输出男/女标签。这里我们不依赖模型来确定阈值,而是使用更可信的第三方工具来保证尽可能多的样本在预期的准确度(precision)下被召回。另外,面对TalkingData超过10亿的数据量,在标签制作过程中,为了加快计算速度,除了必须使用单机的情况,我们都会先使用Spark分布式来加速计算。特征和模型方法的版本迭代为了优化模型的效果,我们对性别标签预测模型进行了多次迭代。01性别预测模型V1最初使用四个维度:设备应用信息、SDK嵌入的应用程序包名称、SDK嵌入的应用内自定义事件日志、设备型号信息。该模型使用Xgboost(0.5版本),根据每个维度的特征训练模型,得到4个子模型。每个子模型将根据设备的男性/女性方向的特征维度输出一个分数。得分范围从0到1。得分高表示该设备是面向男性的,反之则表示该设备是面向女性的。模型代码示例如下:<左右滑动查看完整代码>importcom.talkingdata.utils.LibSVMimportml.dmlc.xgboost4j.scala.DMatriximportml.dmlc.xgboost4j.scala.spark.XGBoost//version0.5//训练阶段valtrainRDD=LibSVM.loadLibSVMFile(sc,trainPath)//sc是SparkContextvalmodel=XGBoost.train(trainRDD,paramMap,numRound,nWorkers=workers)//预测阶段valtestSet=LibSVM.loadLibSVMFilePred(sc,testPath,-1,sc.defaultMinPartitions)valpred=testSet.map(_._2).mapPartitions{iter=>model.value.predict(newDMatrix(iter)).map(_.head).toIterator}.zip(测试集)。map{case(pred,(tdid,feauture))=>s"$tdid\t$pred"}缺点及优化方向:该模型是四个子模型的融合,结构比较复杂,运行效率低较低。考虑使用它来代替单一模型;SDK中嵌入的自定义事件日志特征覆盖率低,ETL处理资源消耗大,需要重新评估该字段对模型的贡献;发现设备名称字段似乎有男/女区分——部分用户组会用名字或昵称来命名设备(例如,有“兄弟”和“军队”等字段的往往是男性,而那些与“姐姐”和“兰”等字段倾向于女性),验证效果并考虑是否加入该字段。02性别预测模型V2将模型的使用特征4个维度调整为:SDK嵌入的应用包名称、SDK嵌入应用的AppKey、设备型号信息、设备名称。其中,对SDK中嵌入的应用包名和设备名进行分词。然后使用CountVectorizer将以上四类特征处理成稀疏向量(Vector),使用ChiSqSelector进行特征筛选。模型采用LR(LogisticRegression),代码示例如下:<左右滑动查看完整代码>importorg.apache.spark.ml.feature.VectorAssemblerimportorg.apache.spark.ml.PipelineModel导入org.apache.spark.ml。classification.LogisticRegressionvaltransformedDF=spark.read.parquet("/traindata/path")//分词、CountVectorizer、ChiSqSelector操作后的特征,为向量列valfeatureCols=Array("packageName","appKey","模型","设备名称")valvectorizer=newVectorAssembler().setInputCols(featureCols).setOutputCol("features")valmodel=pipeline.fit(transformedDF)//预测阶段valtransformedPredictionDF=spark.read.parquet("/predictData/path")//和train一致,是分词、CountVectorizer、ChiSqSelector处理后的feature,和就是vectorcolumnvalpredictions=model.transform(transformedPredictionDF)的优点和改进效果:使用单一模型,可以使用通用的模型评价指标(如ROC-AUC,Precision-Recall等)对模型进行度量,作为后续版本迭代的基线,方便从模型的角度进行版本升级对比。缺点及优化方向:LR模型比较简单,学习能力有限。后面应该会换成更强大的模型,比如Xgboost模型。03性别预测模型V3模型中使用的特征,除了之前版本包含的四个维度:SDK嵌入的应用包名、SDK嵌入的应用AppKey、设备型号信息、设备名称,以及最近聚合的设备应用信息的处理方法与上一版本类似,此处不再赘述。该模型从LR更改为Xgboost(版本0.82)。代码示例如下:<左右滑动查看完整代码>importorg.apache.spark.ml.feature.VectorAssemblerimportml.dmlc.xgboost4j.scala.spark.XGBoostClassifier//version0.82valtransformedDF=spark.read.parquet("/trainData/path")//分词和CountVectorizer操作后的特征,对于向量列valfeatureCols=Array("packageName","appKey","model","deviceName")valvectorizer=newVectorAssembler().setInputCols(featureCols).setOutputCol("features")valassembledDF=vectorizer.transform(transformedDF)//训练阶段//xgboost参数设置valxgbParam=Map("eta"->xxx,"max_depth"->xxx,"objective"->"binary:logistic","num_round"->xxx,"num_workers"->xxx)valxgbClassifier=newXGBoostClassifier(xgbParam).setFeaturesCol("features").setLabelCol("labelColname")模型=xgbClassifier.fit(assembledDF)//预测阶段valtransformedPredictionDF=spark.read.parquet("/predictData/path")//和train一致,是分词和CountVectorizer操作后的特征,是向量列valassembledpredictDF=vectorizer.transform(transformedPredictionDF)valpredictions=model.transform(assembledpredictDF)优势及改进效果:与之前的版本相比,AUC提升了6.5%,最终性别标签生产中的召回率提升了26%。考虑到TalkingData超过十亿的数据量,这个数值还是很可观的。04除了之前版本包含的5个特征维度,性别预测模型V4还加入了TalkingData自己的广告类别三个维度的特征。虽然广告类别特征的覆盖率只占20%,但最终标签的召回率对Ascension也有不小的影响。模型替换为来自Xgboost的DNN,最大训练轮数(Epoch)设置为40,设置earlystopping参数。考虑到神经网络可以基于大数据工作,我们将用于训练的样本量增加了一倍,以保证神经网络的学习。DNN的结构如下:<左右滑动查看完整代码>pythonGenderNet_VLen((embeddings_appKey):Embedding(xxx,64,padding_idx=0)(embeddings_packageName):Embedding(xxx,32,padding_idx=0)(embeddings_model):Embedding(xxx),32,padding_idx=0)(embeddings_app):嵌入(xxx,512,padding_idx=0)(embeddings_deviceName):嵌入(xxx,32,padding_idx=0)(embeddings_adt1):嵌入(xxx,16,padding_idx=0)(embeddings_adt2):嵌入(xxx,16,padding_idx=0)(embeddings_adt3):嵌入(xxx,16,padding_idx=0)(fc):顺序((0):线性(in_features=720,out_features=64,bias=True)(1):BatchNorm1d(64,eps=1e-05,momentum=0.1,affine=True,track_running_stats=True)(2):ReLU()(3):Dropout(p=0.6)(4):线性(in_features=64,out_features=32,bias=True)(5):BatchNorm1d(32,eps=1e-05,momentum=0.1,affine=True,track_running_stats=True)(6):ReLU()(7):Dropout(p=0.6)(8):线性(in_features=32,out_features=16,bias=True)(9):BatchNorm1d(16,eps=1e-05,momentum=0.1,affine=True,track_running_stats=True)(10):ReLU()(11):Dropout(p=0.6)(12):Linear(in_features=16,out_features=2,bias=True)))优点及改进效果:与之前的版本相比,AUC只增加了1.5%,但是在最终的性别标签制作中召回率提高了13%。考虑到数据量和现有的标签量,这个改进还不错。从这里可以看出,在验证版本迭代效果时,我们不应该只用模型的AUC作为单一指标来衡量,因为这不足以衡量版本迭代的效果。我们应该验证最终的、真正的指标改进——在性别标签预测中,以所需精度召回的样本数量。但是,我们仍然可以在版本优化时使用AUC等模型相关指标来快速验证控制变量的实验效果。毕竟,这些指标很容易计算。模型探索的一个小建议是从原始日志中抽取字段聚合成信息,这需要ETL的步骤很多,也涉及到很多优化方法。这部分由专门的ETL团队负责,这里不做过多介绍。建模团队可以直接使用按时间聚合的字段进行建模任务。但是,花费在ETL和特征生成上的时间也占据了模型优化和迭代的大部分时间。下面总结了两个优化方面的陷阱和解决经验,希望能给大家一些参考。1.性别标签预测,大部分输入特征都是Array类型,比如最近收集的设备应用信息。对于这种类型的字段,在训练模型之前,我们一般会调用CountVectorizer将Array转化为Vector,然后作为模型的输入,但是CountVectorizer这一步非常耗时,导致我们无法快速进行实验在版本迭代期间。为了解决这个问题,我们可以提前完成这一步转换,然后将生成的Vector列存储起来,这样在每次实验中,CountVectorizer消耗的时间就可以节省下来。在实际生产中,由于很多标签的生产会用到同一个字段,所以提前将Array转成Vector存储,后续任务可以直接调用Vector列,节省大量时间。2.虽然第一种可以节省很多时间,但Spark更多的还是用于生产。其实在模型的前期探索中,我们也可以先用Spark生成一个训练集——因为真实样本通常不多,生成的训练集往往不是很大,那么我们可以用单机用于快速实验。在单机上,我们可以更方便地使用Python画图更直观地理解数据,更快地进行特征筛选,更快地验证想法。在我们对数据和模型有了深刻的理解之后,我们可以快速地将实验得出的结论应用到生产中。作者简介:张小燕,TalkingData数据科学家,目前负责企业级用户画像平台建设和高效营销投放算法研发。长期关注互联网广告、用户画像、欺诈检测等领域。
