素有資料探勘領域“世界盃”之稱的KDD Cup正在火熱進行中,百度作為此次大賽的主辦方,除了提供10,000美金特別獎,還為使用PaddlePaddle的參賽選手精心提供了KDD Cup Regular ML Track基線支援,此基線能夠在Linux和單機CPU/GPU上執行,通過使用基線,參賽隊伍可以更方便地進行特徵工程和網路的優化,高效完成訓練,並獲得更好的結果。
為了讓選手更方便地使用此基線,我們將為大家剖析KDD Cup Regular ML Track基線的技術特點和使用過程。
本文對應程式碼地址:
https://github.com/PaddlePaddle/models/tree/develop/PaddleRec/ctr/Paddle_baseline_KDD2019
閱讀本文請參考GitHub程式碼庫。
資料預處理
問題定義:
通過給定的使用者query記錄,展示記錄,使用者對出行方式點選行為等原始資料預測使用者選擇的出行方式(transportmode 1 - transport mode 11)。注意有一部分記錄(plans)沒有返回點選行為,題目要求沒有點選同樣需要預測。我們將問題重新定義為二分類問題。對每一個transport mode的點選行為都給出一個0到1的概率值(包括上述未點選行為,強行定義為transport mode 0),進而表示為對給定一種mode的ctr預估問題。
特徵預處理:
特徵預處理的作用是將原始資料轉換成一條一條可以用作訓練模型的例項(instances)。我們採用json格式儲存instances,其中一條instance的json示例:
{ "mode_min_distance": 1, "pid": "", "weather": { "min_temp": "10", "wea": "q", "wind": "12", "max_temp": "25" }, "plan_rank": 1, "query": { "hour": "09", "weekday": "3", "d2": 40.0, "o2": 39.92, "o1": 116.43, "d1": 116.57 }, "eta_rank": 3, "label": 0, "mode_min_eta": 3, "price_rank": 3, "profile": [0], "mode_rank1": 11, "mode_rank2": 3, "mode_rank3": 4, "mode_rank4": 7, "mode_rank5": 1, "mode_max_eta": 1, "plan": { "distance": 0.13730889964270376, "price": 0.00760043431053203, "eta": 0.07021413598936856, "transport_mode": 11 }, "mode_max_distance": 7, "whole_rank": 5, "mode_max_price": 4, "mode_min_price": -1, "distance_rank": 4 }
注意每一條instance通過json直接解析成Python的dict結構(本文例子中有dict巢狀結構),我們可以自定義生成自己的特徵和label供模型訓練或者預測,dict中的鍵為特徵名,值為特徵值。
程式碼:preprocess_dense.py/preprocess.py 為訓練樣本生成程式碼, pre_test_dense.py/pre_process_test.py 為測試樣本生成程式碼。帶有dense字尾的為生成dense特徵和sparse特徵的處理指令碼,不帶字尾的為全sparse特徵的處理指令碼。具體選用哪種生成策略或者結合兩種生成策略可自定義處理,這裡僅提供大致思路,特徵工程方面的擴充工作在此框架下比較方便。本文後續實現模型將以同時生成dense和sparse特徵為例,詳情見preprocess_dense.py。
組建模型
本文采用模型參考DeepFM論文(https://arxiv.org/abs/1703.04247),參見network_confv6, 其他基於fm&deep的更加花哨的模型組建請參考networks資料夾下其他檔案,讀者也可以開腦洞自定義實現,注意使用其他模型的時候需要對應修改instance喂入模型的格式。
訓練模型
定義instance喂入模型格式
本文使用PaddlePaddle Dataset介面。Dataset是一個工廠類,有如下兩種:
(1)QueueDataset:每輪訓練從指定的檔案列表裡讀取資料。
(2)InMemoryDataset:使用者可以呼叫LoadIntoMemory把資料讀到記憶體。每輪訓練從記憶體中讀取資料。
使用者建立Dataset時,預設是QueueDataset(具體使用檢視文件)。參見map_reader.py中實現MapDataset示例, 在使用者實現的dataset類中,使用者可以根據自己的特徵實現任意的處理邏輯,在generate_sample()函式中定義每個instance輸入到模型的格式,注意在示例中,一共通過feature_name定義了 "dense_feature", "context" + str(idx), "context_fm","label" 這四類輸入格式。在local_train.py中使用dataset的示例:
dense_feature = fluid.layers.data( name="dense_feature",shape=[DIM_DENSE_FEATURE], dtype='float32') context_feature = [ fluid.layers.data(name="context"+ str(i), shape=[1], lod_level=1, dtype="int64") for i in range(0,NUM_CONTEXT_FEATURE)] context_feature_fm = fluid.layers.data( name="context_fm",shape=[1], dtype='int64', lod_level=1) label = fluid.layers.data(name='label', shape=[1],dtype='int64') print("ready to network") loss, auc_var, batch_auc_var, accuracy, predict =ctr_deepfm_dataset(dense_feature, context_feature, context_feature_fm, label, args.embedding_size,args.sparse_feature_dim) print("ready to optimize") optimizer = fluid.optimizer.SGD(learning_rate=1e-4) optimizer.minimize(loss) exe = fluid.Executor(fluid.CPUPlace()) exe.run(fluid.default_startup_program()) dataset = fluid.DatasetFactory().create_dataset() dataset.set_use_var([dense_feature] + context_feature +[context_feature_fm] + [label]) pipe_command = PYTHON_PATH + " map_reader.py%d" % args.sparse_feature_dim dataset.set_pipe_command(pipe_command) dataset.set_batch_size(args.batch_size) thread_num = 1 dataset.set_thread(thread_num)
注意set_pipe_command(pipe_command), pipe command是對原始的資料做處理,生成PaddlePaddle可以識別的格式。Dataset讀取的每一行原始資料,都會用這裡的命令做處理。可以是一個執行python指令碼或者二進位制等任意Linux命令,使用者可以寫自己的邏輯。pipe command生成var的順序需要和set_user_var保持一致。
接下來就需要在pipe_command中處理資料。
在Pipe Command中處理資料
PaddlePaddle中提供了基類paddle.fluid.incubate.data_generator.MultiSlotDataGenerator,使用者可以繼承並實現自己的處理資料的邏輯。生成的資料需要與set_use_var中設定的順序保持一致。具體程式碼參考map_reader.py中自定義處理原始資料程式碼,使用者需實現generate_sample()函式來處理每一行資料:
def _process_line(self, line): instance = json.loads(line) """ profile =instance["profile"] len_profile = len(profile) if len_profile >= 10: user_profile_feature= profile[0:10] else: profile.extend([0]*(10-len_profile)) user_profile_feature= profile if len(profile) > 1 or (len(profile)== 1 and profile[0] != 0): for p inprofile: ifp >= 1 and p <= 65: user_profile_feature[p- 1] = 1 """ context_feature = [] context_feature_fm = [] dense_feature = [0] *self.dense_length plan = instance["plan"] for i, val inenumerate(self.dense_feature_list): dense_feature[i]= plan[val] if (instance["pid"] ==""): instance["pid"]= 0 query =instance["query"] weather_dic =instance["weather"] for fea in self.pid_list: context_feature.append([hash(fea+ str(instance[fea])) % self.hash_dim]) context_feature_fm.append(hash(fea+ str(instance[fea])) % self.hash_dim) for fea inself.query_feature_list: context_feature.append([hash(fea+ str(query[fea])) % self.hash_dim]) context_feature_fm.append(hash(fea+ str(query[fea])) % self.hash_dim) for fea inself.plan_feature_list: context_feature.append([hash(fea+ str(plan[fea])) % self.hash_dim]) context_feature_fm.append(hash(fea+ str(plan[fea])) % self.hash_dim) for fea inself.rank_feature_list: context_feature.append([hash(fea+ str(instance[fea])) % self.hash_dim]) context_feature_fm.append(hash(fea+ str(instance[fea])) % self.hash_dim) for fea inself.rank_whole_pic_list: context_feature.append([hash(fea+ str(instance[fea])) % self.hash_dim]) context_feature_fm.append(hash(fea+ str(instance[fea])) % self.hash_dim) for fea in self.weather_feature_list: context_feature.append([hash(fea+ str(weather_dic[fea])) % self.hash_dim]) context_feature_fm.append(hash(fea+ str(weather_dic[fea])) % self.hash_dim) label =[int(instance["label"])] return dense_feature, context_feature,context_feature_fm, label def generate_sample(self, line): def data_iter(): dense_feature,sparse_feature, sparse_feature_fm, label = self._process_line(line) #feature_name= ["user_profile"] feature_name= [] feature_name.append("dense_feature") for idxin self.categorical_range_: feature_name.append("context"+ str(idx)) feature_name.append("context_fm") feature_name.append("label") yieldzip(feature_name, [dense_feature] + sparse_feature + [sparse_feature_fm] +[label]) return data_iter
至此,模型定義,資料格式處理邏輯,訓練流程都已確定,執行Python local_trian.py就可以開始訓練了。