基于PaddlePaddle的點(diǎn)擊率的深度學(xué)習(xí)方法嘗試
前言
前面在團(tuán)隊(duì)內(nèi)部分享點(diǎn)擊率相關(guān)的一些文章時(shí),輸出了一篇常見(jiàn)計(jì)算廣告點(diǎn)擊率預(yù)估算法總結(jié),看了一些廣告點(diǎn)擊率的文章,從最經(jīng)典的Logistic Regression到Factorization Machined,F(xiàn)FM,F(xiàn)NN,PNN到今年的DeepFM,還有文章里面沒(méi)有講的gbdt+lr這類,一直想找時(shí)間實(shí)踐下,正好這次在學(xué)習(xí)paddle的時(shí)候在它的models目錄下看到了DeepFM的實(shí)現(xiàn),因?yàn)橹皩?duì)DeepFM有過(guò)比較詳細(xì)的描述,這里稍微復(fù)習(xí)一下:
DeepFM更有意思的地方是WDL和FM結(jié)合了,其實(shí)就是把PNN和WDL結(jié)合了,PNN即將FM用神經(jīng)網(wǎng)絡(luò)的方式構(gòu)造了一遍,作為wide的補(bǔ)充,原始的Wide and Deep,Wide的部分只是LR,構(gòu)造線性關(guān)系,Deep部分建模更高階的關(guān)系,所以在Wide and Deep中還需要做一些特征的東西,如Cross Column的工作,而我們知道FM是可以建模二階關(guān)系達(dá)到Cross column的效果,DeepFM就是把FM和NN結(jié)合,無(wú)需再對(duì)特征做諸如Cross Column的工作了,這個(gè)是我感覺(jué)最吸引人的地方,其實(shí)FM的部分感覺(jué)就是PNN的一次描述,這里只描述下結(jié)構(gòu)圖,PNN的部分前面都描述, FM部分:
Deep部分:
DeepFM相對(duì)于FNN、PNN,能夠利用其Deep部分建模更高階信息(二階以上),而相對(duì)于Wide and Deep能夠減少特征工程的部分工作,wide部分類似FM建模一、二階特征間關(guān)系, 算是NN和FM的一個(gè)更***的結(jié)合方向,另外不同的是如下圖,DeepFM的wide和deep部分共享embedding向量空間,wide和deep均可以更新embedding部分,雖說(shuō)wide部分純是PNN的工作,但感覺(jué)還是蠻有意思的。
本文相關(guān)代碼部分都是來(lái)自于paddlepaddle/model, 我這里走一遍流程,學(xué)習(xí)下,另外想要了解算法原理的可以仔細(xì)再看看上面的文章,今天我們來(lái)paddlepaddle上做下實(shí)驗(yàn),來(lái)從代碼程度學(xué)習(xí)下DeepFM怎么實(shí)現(xiàn)的:
數(shù)據(jù)集說(shuō)明
criteo Display Advertising Challenge,數(shù)據(jù)主要來(lái)criteolab一周的業(yè)務(wù)數(shù)據(jù),用來(lái)預(yù)測(cè)用戶在訪問(wèn)頁(yè)面時(shí),是否會(huì)點(diǎn)擊某廣告。
wget --no-check-certificate https://s3-eu-west-1.amazonaws.com/criteo-labs/dac.tar.gz tar zxf dac.tar.gz rm -f dac.tar.gz mkdir raw mv ./*.txt raw/
數(shù)據(jù)有點(diǎn)大, 大概4.26G,慢慢等吧,數(shù)據(jù)下載完成之后,解壓出train.csv,test.csv,其中訓(xùn)練集45840617條樣本數(shù),測(cè)試集45840617條樣本,數(shù)據(jù)量還是蠻大的。 數(shù)據(jù)主要有三部分組成:
- label: 廣告是否被點(diǎn)擊;
- 連續(xù)性特征: 1-13,為各維度下的統(tǒng)計(jì)信息,連續(xù)性特征;
- 離散型特征:一些被脫敏處理的類目特征
Overview
整個(gè)項(xiàng)目主要由幾個(gè)部分組成:
數(shù)據(jù)處理
這里數(shù)據(jù)處理主要包括兩個(gè)部分:
- 連續(xù)值特征值處理:
- 濾除統(tǒng)計(jì)次數(shù)95%以上的數(shù)據(jù),這樣可以濾除大部分異值數(shù)據(jù),這里的處理方式和以前我在1號(hào)店做相關(guān)工作時(shí)一致,代碼里面已經(jīng)做了這部分工作,直接給出了這部分的特征閾值;
- 歸一化處理,這里andnew ng的課程有張圖很明顯,表明不同的特征的值域范圍,會(huì)使得模型尋優(yōu)走『之』字形,這樣會(huì)增加收斂的計(jì)算和時(shí)間;
- 離散特征值處理:
- one-hot: 對(duì)應(yīng)特征值映射到指定維度的只有一個(gè)值為1的稀疏變量;
- embedding: 對(duì)應(yīng)特征值映射到指定的特征維度上;
具體我們來(lái)研究下代碼:
class ContinuousFeatureGenerator: """ Normalize the integer features to [0, 1] by min-max normalization """ def __init__(self, num_feature): self.num_feature = num_feature self.min = [sys.maxint] * num_feature self.max = [-sys.maxint] * num_feature def build(self, datafile, continous_features): with open(datafile, 'r') as f: for line in f: features = line.rstrip('\n').split('\t') for i in range(0, self.num_feature): val = features[continous_features[i]] if val != '': val = int(val) if val > continous_clip[i]: val = continous_clip[i] self.min[i] = min(self.min[i], val) self.max[i] = max(self.max[i], val) def gen(self, idx, val): if val == '': return 0.0 val = float(val) return (val - self.min[idx]) / (self.max[idx] - self.min[idx])
連續(xù)特征是在1-13的位置,讀取文件,如果值大于對(duì)應(yīng)維度的特征值的95%閾值,則該特征值置為該閾值,并計(jì)算特征維度的***、最小值,在gen時(shí)歸一化處理。
class CategoryDictGenerator: """ Generate dictionary for each of the categorical features """ def __init__(self, num_feature): self.dicts = [] self.num_feature = num_feature for i in range(0, num_feature): self.dicts.append(collections.defaultdict(int)) def build(self, datafile, categorial_features, cutoff=0): with open(datafile, 'r') as f: for line in f: features = line.rstrip('\n').split('\t') for i in range(0, self.num_feature): if features[categorial_features[i]] != '': self.dicts[i][features[categorial_features[i]]] += 1 for i in range(0, self.num_feature): self.dicts[i] = filter(lambda x: x[1] >= cutoff, self.dicts[i].items()) self.dicts[i] = sorted(self.dicts[i], key=lambda x: (-x[1], x[0])) vocabs, _ = list(zip(*self.dicts[i])) self.dicts[i] = dict(zip(vocabs, range(1, len(vocabs) + 1))) self.dicts[i]['<unk>'] = 0 def gen(self, idx, key): if key not in self.dicts[idx]: res = self.dicts[idx]['<unk>'] else: res = self.dicts[idx][key] return res def dicts_sizes(self): return map(len, self.dicts)
類目特征的處理相對(duì)比較麻煩,需要遍歷,然后得到對(duì)應(yīng)維度上所有出現(xiàn)值的所有情況,對(duì)打上對(duì)應(yīng)id,為后續(xù)類目特征賦予id。這部分耗時(shí)好大,慢慢等吧,另外強(qiáng)烈希望paddlepaddle的小伙伴能在輸出處理期間打印下提示信息,算了,我之后有時(shí)間看看能不能提提pr。
經(jīng)過(guò)上面的特征處理之后,訓(xùn)練集的值變?yōu)椋?/p>
reader
paddle里面reader的文件,自由度很高,自己可以寫(xiě)生成器,然后使用batch的api,完成向網(wǎng)絡(luò)傳入batchsize大小的數(shù)據(jù):
class Dataset: def _reader_creator(self, path, is_infer): def reader(): with open(path, 'r') as f: for line in f: features = line.rstrip('\n').split('\t') dense_feature = map(float, features[0].split(',')) sparse_feature = map(int, features[1].split(',')) if not is_infer: label = [float(features[2])] yield [dense_feature, sparse_feature ] + sparse_feature + [label] else: yield [dense_feature, sparse_feature] + sparse_feature return reader def train(self, path): return self._reader_creator(path, False) def test(self, path): return self._reader_creator(path, False) def infer(self, path): return self._reader_creator(path, True)
主要邏輯在兌入文件,然后yield對(duì)應(yīng)的網(wǎng)絡(luò)數(shù)據(jù)的輸入格式
模型構(gòu)造
模型構(gòu)造,DeepFM在paddlepaddle里面比較簡(jiǎn)單,因?yàn)橛袑iT(mén)的fm層,這個(gè)據(jù)我所知在TensorFlow或MXNet里面沒(méi)有專門(mén)的fm層,但是值得注意的是,在paddlepaddle里面的fm層,只建模二階關(guān)系,需要再加入fc才是完整的fm,實(shí)現(xiàn)代碼如下:
def fm_layer(input, factor_size, fm_param_attr): first_order = paddle.layer.fc( input=input, size=1, act=paddle.activation.Linear()) second_order = paddle.layer.factorization_machine( input=input, factor_size=factor_size, act=paddle.activation.Linear(), param_attr=fm_param_attr) out = paddle.layer.addto( input=[first_order, second_order], act=paddle.activation.Linear(), bias_attr=False) return out
然后就是構(gòu)造DeepFM,這里根據(jù)下面的代碼畫(huà)出前面的圖,除去數(shù)據(jù)處理的部分,就是DeepFM的網(wǎng)絡(luò)結(jié)構(gòu):
def DeepFM(factor_size, infer=False): dense_input = paddle.layer.data( name="dense_input", type=paddle.data_type.dense_vector(dense_feature_dim)) sparse_input = paddle.layer.data( name="sparse_input", type=paddle.data_type.sparse_binary_vector(sparse_feature_dim)) sparse_input_ids = [ paddle.layer.data( name="C" + str(i), type=s(sparse_feature_dim)) for i in range(1, 27) ] dense_fm = fm_layer( dense_input, factor_size, fm_param_attr=paddle.attr.Param(name="DenseFeatFactors")) sparse_fm = fm_layer( sparse_input, factor_size, fm_param_attr=paddle.attr.Param(name="SparseFeatFactors")) def embedding_layer(input): return paddle.layer.embedding( input=input, size=factor_size, param_attr=paddle.attr.Param(name="SparseFeatFactors")) sparse_embed_seq = map(embedding_layer, sparse_input_ids) sparse_embed = paddle.layer.concat(sparse_embed_seq) fc1 = paddle.layer.fc( input=[sparse_embed, dense_input], size=400, act=paddle.activation.Relu()) fc2 = paddle.layer.fc(input=fc1, size=400, act=paddle.activation.Relu()) fc3 = paddle.layer.fc(input=fc2, size=400, act=paddle.activation.Relu()) predict = paddle.layer.fc( input=[dense_fm, sparse_fm, fc3], size=1, act=paddle.activation.Sigmoid()) if not infer: label = paddle.layer.data( name="label", type=paddle.data_type.dense_vector(1)) cost = paddle.layer.multi_binary_label_cross_entropy_cost( input=predict, label=label) paddle.evaluator.classification_error( name="classification_error", input=predict, label=label) paddle.evaluator.auc(name="auc", input=predict, label=label) return cost else: return predict
其中,主要包括三個(gè)部分,一個(gè)是多個(gè)fc組成的deep部分,第二個(gè)是sparse fm部分,然后是dense fm部分,如圖:
這里蠻簡(jiǎn)單的,具體的api去查下文檔就可以了,這里稍微說(shuō)明一下的是,sparse feature這塊有兩部分一塊是embedding的處理,這里是先生成對(duì)應(yīng)的id,然后用id來(lái)做embedding,用作后面fc的輸出,然后sparse_input是onehot表示用來(lái)作為fm的輸出,fm來(lái)計(jì)算一階和二階隱變量關(guān)系。
模型訓(xùn)練
數(shù)據(jù)量太大,單機(jī)上跑是沒(méi)有問(wèn)題,可以正常運(yùn)行成功,在我內(nèi)部機(jī)器上,可以運(yùn)行成功,但是有兩個(gè)問(wèn)題:
- fm由于處理的特征為稀疏表示,而paddlepaddle在這塊的FM層的支持只有在cpu上,速度很慢,分析原因其實(shí)不是fm的速度的問(wèn)題,因?yàn)閐eepfm有設(shè)計(jì)多個(gè)fc,應(yīng)該是這里的速度影響, 在paddlepaddle github上有提一個(gè)issue,得知暫時(shí)paddlepaddle不能把部分放到gpu上面跑,給了一個(gè)解決方案把所有的sparse改成dense,發(fā)現(xiàn)在這里gpu顯存hold不住;
- 我的機(jī)器太渣,因?yàn)橛虚_(kāi)發(fā)任務(wù)不能長(zhǎng)期占用;
所以綜上,我打算研究下在百度云上怎么通過(guò)k8s來(lái)布置paddlepaddle的分布式集群。
文檔https://cloud.baidu.com/doc/CCE/GettingStarted.html#.E9.85.8D.E7.BD.AEpaddlecloud
研究來(lái)研究去,***步加卡主了,不知道怎么回事,那個(gè)頁(yè)面就是出不來(lái)...出師未捷身先死,提了個(gè)issue: https://github.com/PaddlePaddle/cloud/issues/542,等后面解決了再來(lái)更新分布式訓(xùn)練的部分。
單機(jī)的訓(xùn)練沒(méi)有什么大的問(wèn)題,由上面所說(shuō),因?yàn)閒m的sparse不支持gpu,所以很慢,拉的百度云上16核的機(jī)器,大概36s/100 batch,總共樣本4000多w,一個(gè)epoch預(yù)計(jì)4個(gè)小時(shí),MMP,等吧,分布式的必要性就在這里。
另外有在paddlepaddle里面提一個(gè)issue:
https://github.com/PaddlePaddle/Paddle/issues/7010,說(shuō)把sparse轉(zhuǎn)成dense的話可以直接在gpu上跑起來(lái),這個(gè)看起來(lái)不值得去嘗試,sparse整個(gè)維度還是挺高的,期待對(duì)sparse op 有更好的解決方案,更期待在能夠把單層單層的放在gpu,多設(shè)備一起跑,這方面,TensorFlow和MXNet要好太多。
這里我遇到一個(gè)問(wèn)題,我使用paddle的docker鏡像的時(shí)候,可以很穩(wěn)定的占用16個(gè)cpu的大部分計(jì)算力,但是我在云主機(jī)上自己裝的時(shí)候,cpu占用率很低,可能是和我環(huán)境配置有點(diǎn)問(wèn)題,這個(gè)問(wèn)題不大,之后為了不污染環(huán)境主要用docker來(lái)做相關(guān)的開(kāi)發(fā)工作,所以這里問(wèn)題不大。
cpu占有率有比較明顯的跳動(dòng),這里從主觀上比TensorFlow穩(wěn)定性要差一些,不排除是sparse op的影響,印象中,TensorFlow cpu的占用率很穩(wěn)定。
到發(fā)這篇文章位置,跑到17300個(gè)batch,基本能達(dá)到auc為0.8左右,loss為0.208左右。
預(yù)測(cè)
預(yù)測(cè)代碼和前一篇將paddle里面的demo一樣,只需要,重新定義一下網(wǎng)絡(luò),然后綁定好模型訓(xùn)練得到的參數(shù),然后傳入數(shù)據(jù)即可完成inference,paddle,有專門(mén)的Inference接口,只要傳入output_layer,和訓(xùn)練學(xué)習(xí)到的parameters,就可以很容易的新建一個(gè)模型的前向inference網(wǎng)絡(luò)。
def infer(): args = parse_args() paddle.init(use_gpu=False, trainer_count=1) model = DeepFM(args.factor_size, infer=True) parameters = paddle.parameters.Parameters.from_tar( gzip.open(args.model_gz_path, 'r')) inferer = paddle.inference.Inference( output_layer=model, parameters=parameters) dataset = reader.Dataset() infer_reader = paddle.batch(dataset.infer(args.data_path), batch_size=1000) with open(args.prediction_output_path, 'w') as out: for id, batch in enumerate(infer_reader()): res = inferer.infer(input=batch) predictions = [x for x in itertools.chain.from_iterable(res)] out.write('\n'.join(map(str, predictions)) + '\n')
總結(jié)
照例總結(jié)一下,DeemFM是17年深度學(xué)習(xí)在點(diǎn)擊率預(yù)估、推薦這塊的新的方法,有點(diǎn)類似于deep and wide的思想,將傳統(tǒng)的fm來(lái)nn化,利用神經(jīng)網(wǎng)絡(luò)強(qiáng)大的建模能力來(lái)挖掘數(shù)據(jù)中的有效信息,paddlepaddle在這塊有現(xiàn)成的deepfm模型,單機(jī)部署起來(lái)比較容易,分布式,這里我按照百度云上的教程還未成功,后續(xù)會(huì)持續(xù)關(guān)注。另外,因?yàn)樽罱谧龃笠?guī)模機(jī)器學(xué)習(xí)框架相關(guān)的工作,越發(fā)覺(jué)得別說(shuō)成熟的,僅僅能夠work的框架就很不錯(cuò)了,而比較好用的如現(xiàn)在的TensorFlow\MXNet,開(kāi)發(fā)起來(lái)真的難上加難,以前光是做調(diào)包俠時(shí)沒(méi)有體驗(yàn),現(xiàn)在深入到這塊的工作時(shí),才知道其中的難度,也從另一個(gè)角度開(kāi)始審視現(xiàn)在的各種大規(guī)模機(jī)器學(xué)習(xí)框架,比如TensorFlow、MXNet,在深度學(xué)習(xí)的支持上,確實(shí)很棒,但是也有瓶頸,對(duì)于大規(guī)模海量的feature,尤其是sparse op的支持上,至少現(xiàn)在還未看到特別好的支持,就比如這里的FM,可能大家都會(huì)吐槽為啥這么慢,沒(méi)做框架之前,我也會(huì)吐槽,但是開(kāi)始接觸了一些的時(shí)候,才知道FM,主要focus在sparse相關(guān)的數(shù)據(jù)對(duì)象,而這部分?jǐn)?shù)據(jù)很難在gpu上完成比較高性能的計(jì)算,所以前面經(jīng)過(guò)paddle的開(kāi)發(fā)者解釋sparse相關(guān)的計(jì)算不支持gpu的時(shí)候,才感同身受,一個(gè)好的大規(guī)模機(jī)器學(xué)習(xí)框架必須要從不同目標(biāo)來(lái)評(píng)價(jià),如果需求是大規(guī)律數(shù)據(jù),那穩(wěn)定性、可擴(kuò)展性是重點(diǎn),如果是更多算法、模型的支持,可能現(xiàn)在的TensorFlow、MXNet才是標(biāo)桿,多么希望現(xiàn)在大規(guī)模機(jī)器學(xué)習(xí)框架能夠多元化的發(fā)展,有深度學(xué)習(xí)支持力度大的,也有傳統(tǒng)算法上,把數(shù)據(jù)量、訓(xùn)練規(guī)模、并行化加速并做到***的,這樣的發(fā)展才或許稱得上百花齊放,其實(shí)我們不需要太多不同長(zhǎng)相的TensorFlow、MXNet錘子,有時(shí)候我們就需要把鐮刀而已,希望大規(guī)模機(jī)器學(xué)習(xí)框架的發(fā)展,不應(yīng)該僅僅像TensorFlow、MXNet一樣,希望有一個(gè)專注把做大規(guī)模、大數(shù)據(jù)量、***并行化加速作為roadmap的新標(biāo)桿,加油。