美文网首页数据挖掘
天池比赛--“商场中精确定位用户所在店铺”分享

天池比赛--“商场中精确定位用户所在店铺”分享

作者: bupt_周小瑜 | 来源:发表于2018-01-02 20:27 被阅读388次

    背景介绍

    这是笔者参加的第一个大数据比赛,预赛最好成绩是前50名,但是由于后来竞争越发激烈(分类正确率差0.01,排名都能差上10多名...)和兴趣渐渐转到研究NLP上,最终没有坚持迭代优化模型,还是非常遗憾的。参加比赛的直接原因是来自庆恒学长的鼓励,他和他的小伙伴在10月份一举拿下了天池算法大赛的冠军,钦羡不已,遂决定实战一下,提升下自己的能力。

    笔者从对打比赛“一无所知”到能够快速建模迭代实验,这其中的经历对于初学者还是有非常大的参考意义的,所以,我打算写一篇博文,将其中的点点滴滴记录下来。如果读者想要更进一步的提升的话,强推冠军兄弟们开源的代码,其中的特征工程、二分类为主线,多分类作为特征辅助的思路非常的棒!这里附上传送门:商铺定位代码开源

    比赛链接的传送门也一并奉上:商场中精确定位用户所在商铺

    建模分析

    来自寒小阳老师博文机器学习系列(3)_逻辑回归应用之Kaggle泰坦尼克之灾的一张图片,总结了利用机器学习解决问题的思考流程:

    思考流程

    这篇博文非常好,是我的入门博文,值得一读。当然遇到简单的题目套用这些流程没问题,但最好是活学活用,避免机械化。

    下面我将从拿到数据,分析题目、数据预处理、特征工程、模型选择、模型优化、交叉验证、模型融合这几个方面来介绍,这几个流程也是机器学习比赛中常见的思路。

    分析题目

    主办方提供了3个文件,店铺和商场信息表、用户在店铺内交易表、评测集。这三个表包含的字段可以到比赛信息界面找到,利用这些信息我们需要预测用户所在的shop_id信息,很明显的多分类问题嘛(实力打脸...后来证明,将这个问题用二分类正负样本的方式解决,正确率要高...),不过这里就先按照多分类的思路先写着,毕竟效果也不错,二分类的思路确实需要有些比赛经验老手才会这么做...

    数据预处理

    一开始的思路很明确,有了第一张表,我们可以将shop和mall的对应关系找出来,训练len(mall)的多分类器。所以,数据预处理的第一步,就是将对应的数据按照mall分到对应的文件夹下(注意,我这里为了处理方便,将文件名改成英文了):

    import pandas as pd #数据分析
    import numpy as np #科学计算
    from pandas import Series,DataFrame
    
    shop_info = pd.read_csv("data/train-ccf_first_round_shop_info.csv")
    user_info = pd.read_csv("data/train-ccf_first_round_user_shop_behavior.csv")
    test_info = pd.read_csv("data/test-evaluation_public.csv")
    
    # mall对应的shop信息
    mall_shop = {}
    for i in range(len(shop_info["mall_id"])):
        mall_shop.setdefault(shop_info["mall_id"][i], []).append(shop_info["shop_id"][i])
    print len(mall_shop) # 97个mall,d1中记录了每个mall中有多少个shop
    # 根据mall_id来划分97份训练、测试集
    import os
    import sys
    
    test_info1 = test_info.copy()
    user_info1 = user_info.copy()
    for i in range(len(mall_shop)):
        if not os.path.exists(mall_shop.keys()[i]): os.makedirs('data/' + mall_shop.keys()[i])
        df1 = test_info1[test_info1['mall_id'] == mall_shop.keys()[i]]
        df1.drop(['mall_id'], axis=1, inplace=True)
        df1.to_csv('data/' + mall_shop.keys()[i] + '/test.csv', index = False)
        df2 = user_info1[user_info1['shop_id'].isin(mall_shop.values()[i])]
        df2.to_csv('data/' + mall_shop.keys()[i] + '/train.csv', index = False)
    

    特征工程

    因为97个mall的分类器是类似的,所以在测试的时候,随机选用商场m_1790作为参考。

    在打比赛界,有句话流传颇广:特征工程决定了最终效果的上界,各种分类方法的区别只是在于能否逼近这个上界而已。这句话足以证明特征工程的重要性。如果读者之前去看了这个比赛第一名的开源项目,你会发现,他们的特征工程居然有这么多,看上去林林总总,但每个特征类别,对于最终的效果提升肯定是有帮助的:

    特征
    
    标记特征:
    记录中是否有连接的wifi
    记录中是含否有null
    记录中wifi与候选shop出现过的wifi重合的个数
    "总量-比例"特征
    该mall的总历史记录数、候选shop在其中的占比
    该user的总历史记录数、候选shop在其中的占比
    wifi历史上出现过的总次数、候选shop在其中的占比
    在当前排序位置(如最强、第二强、第三强...)上wifi历史上出现过的总次数、候选shop在其中的占比
    连接的wifi出现的总次数、候选shop在其中的占比
    经纬度网格(将经纬度按不同精度划分成网格)中的总记录数、候选shop在其中的占比
    对于特征3、4,每条记录中的10个wifi由强到弱排列,可生成10个特征。
    
    差值特征:
    wifi强度 - 候选shop的历史记录中该wifi的平均强度
    wifi强度 - 候选shop的历史记录中该wifi的最小强度
    wifi强度 - 候选shop的历史记录中该wifi的最大强度
    三个wifi强度差值特征,按照信号强度由强到弱排列,可生成10个特征。
    
    距离特征:
    与候选shop位置的GPS距离(L2)
    与候选shop历史记录中心位置的GPS距离(L2)
    与候选shop对应wifi信号强度的距离(L1、L2)
    
    其他特征:
    特征中还包括多分类的输出概率。另外,还有一些利用规则定义的距离特征,这里不再详述
    

    笔者的特征工程就很直接了,经过反复测试,发现wifi特征的重要性最大,其他特征给的帮助一般,所以决定建立一个由特征构成的词袋向量,有这个wifi的根据强度设定一个正值,没有这个wifi的,设置为0,跟词袋模型会遇到的问题一样,最终的特征矩阵非常稀疏,导致后面过拟合,这里的思路是可以取topk的重要wifi,组成词袋,经过测试,topk的方案,范化能力明显要强,验证了上面的想法。

    提取topk-wifi特征的代码如下:

    """
    wifi feature like bag of words
    """
    # version-2: add "true" signal
    # version-3: only choose top5 intensity wifi
    from pandas import DataFrame,Series
    kan_wifi_infos = kan.wifi_infos
    
    """
    选择topk强度的wifi
    """
    def chooseTopk(infos, k):
        """
        choose topk intensity wifi
        :infos: DataFrame infos
        :k: topk
        """
        new_infos = []
        for j in range(len(infos)):
            orignal_list = infos[j].split(';')
            intensity = []
            sel_list = []
            for i in range(len(orignal_list)):
                intensity.append(int(orignal_list[i].split('|')[1]))
            np_intensity = np.array(intensity)
            index = np.argsort(np_intensity)[::-1] # big2small
            if index.shape[0] >= k: 
                for num in range(k):
                    sel_list.append(orignal_list[index[num]])
            else:
                for num in range(index.shape[0]):
                    sel_list.append(orignal_list[index[num]])
            join_str = ";"
            print join_str.join(set(sel_list))
            new_infos.append(join_str.join(set(sel_list)))
        return new_infos
    
    new_wifi_infos = chooseTopk(kan_wifi_infos, 5)
    new_wifi_infos = Series(new_wifi_infos)
    
    
    """
    词袋模型构建过程
    """
    wifi_info = []
    wifi_column = []
    wifi_name = {}
    for j in range(len(new_wifi_infos)):
        wifi_list = new_wifi_infos[j].split(';')
        wifi_own = []
        # for i in range(len(wifi_list)):
        #   wifi_name[wifi_list[i].split('|')[0]] = [] # problem????
        for i in range(len(wifi_list)):
            # wifi_own.insert(-1,wifi_list[i].split('|')[0])
            if wifi_list[i].split('|')[0] not in wifi_column:
                wifi_name[wifi_list[i].split('|')[0]] = []
                # wifi_column.insert(-1, wifi_list[i].split('|')[0]) # ordered list for wifis
                wifi_column.append(wifi_list[i].split('|')[0]) # ordered list for wifis
                # temp = wifi_column
                # scale
                wifi_name[wifi_list[i].split('|')[0]].extend([0]*j) # head 0
                wifi_name[wifi_list[i].split('|')[0]].extend([(100+int(wifi_list[i].split('|')[1]))*0.01])
            elif wifi_list[i].split('|')[0] not in wifi_own: # 1 line has the same wifi-number
                wifi_name[wifi_list[i].split('|')[0]].extend([(100+int(wifi_list[i].split('|')[1]))*0.01])
            else:
                pass
                # add 0 to end of list
                # wifi_name[wifi_list[0] / wifi_column].extend([0])
            wifi_own.append(wifi_list[i].split('|')[0])
        retE = [i for i in wifi_column if i not in wifi_own]
        for num in retE: # cha_ji
            wifi_name[num].extend([0]) # end 0
    print "out of for-for"
    train_df = DataFrame(wifi_name) # 这便是要训练的特征向量
    

    模型选择

    模型选择部分,一开始选用logistic regression和朴素贝叶斯模型,效果一般,大概提交正确率能达到60%,速度很快但是毕竟是打比赛,看的还是准确率呀...这两个部分的特征数据都需要做归一化,否则会产生很大的偏差,归一化方法,这里也分享出来,里面还有sklearn.scaler的一个小坑,调研了一会才搞定:

    """
    longitude latitude 4 and scale
    """
    from sklearn import preprocessing
    
    longi = (kan['longitude']*1000000%10000).as_matrix().reshape(-1, 1) # 1D->2D
    lati = kan['latitude'].as_matrix().reshape(-1, 1)*1000000%10000
    scaler = preprocessing.StandardScaler()
    longi_scale_param = scaler.fit(longi)
    kan_longi_scaled = scaler.fit_transform(longi, longi_scale_param)
    lati_scale_param = scaler.fit(lati)
    kan_lati_scaled = scaler.fit_transform(lati, lati_scale_param)
    kan_longi_scaled1 = kan_longi_scaled.ravel() # 2D->1D
    kan_lati_scaled1 = kan_lati_scaled.ravel()
    # print type(kan_lati_scaled1) # numpy.ndarray
    longi_scaled = pd.Series(kan_longi_scaled1, index=kan.index)
    lati_scaled = pd.Series(kan_lati_scaled1, index=kan.index)
    

    测试模型效果的时候,选择的label肯定得是数字化的吧?所以,这里一开始还有一个label的encoder步骤,最终提交文件的时候,再decoder恢复成原先的shop_id信息,实现代码如下:

    kan = pd.read_csv("data/m_1790/train.csv", parse_dates = ['time_stamp'])
    
    """
    y LabelEncoder
    """
    leshop = preprocessing.LabelEncoder()
    shop_label = leshop.fit_transform(kan.shop_id)
    
    ...
    
    result_shop_id = leshop.inverse_transform(predictions.astype(np.int32))
    

    之后选用了随机森林模型,效果出奇的好,交叉验证上的争取率能达到92%,实际提交也能到89%,要知道,这才只是baseline,还没有经过模型优化,当时第一名的正确率也就91%的样子,当时高兴得不得了,也一股做起,做起了参数调优的事情,这部分内容先不讲,放在模型优化部分。

    之后笔者用上了kaggle比赛中如雷贯耳的xgboost,效果是真的好,线上提交成绩上了90,但是速度不是很快,在笔者实验室服务器上,97个mall单进程跑的话要将近2-3个小时,笔者分析主要还是特征太稀疏,维度过大的原因把。

    还有一个新潮的模型是微软开源的lightGBM,号称比xgboost效果一样好,但是更轻量,速度更快,可惜网上的资料少的可怜,由于时间紧也没多折腾,以后有空抽时间好好研究下,做个博文介绍一番...

    模型优化

    我这里说的模型优化主要是调参,用到的技术主要是网格搜索和贪心方式。

    • 网格搜索

    网格搜索就是设置参数的一个集合范围,让程序不断尝试,最终给你一个好的参数搭配组合,有点是范围取值合理,得到的真真是全局最优解呀,缺点也很明显,特别耗费时间。实现代码如下:

    from sklearn.ensemble import RandomForestClassifier
    from sklearn.model_selection import GridSearchCV
    
    # Set the parameters by cross-validation
    tuned_parameters = {'n_estimators': [10, 20, 50], 'max_depth': [None, 1, 2, 3], 'min_samples_split': [1, 2, 3]}
    
    all_data = train_df1
    X = all_data.as_matrix()[:,:-1]
    y = all_data.as_matrix()[:,-1]
    # clf = ensemble.RandomForestRegressor(n_estimators=500, n_jobs=1, verbose=1)
    clf = GridSearchCV(RandomForestClassifier(), tuned_parameters, cv=5, n_jobs=-1, verbose=1)
    clf.fit(X, y)
    print clf.best_estimator_
    
    • 贪心算法

    贪心法求的是局部最优解(当且仅当所有参数去耦合的情况下,是全局最优的)。贪心法操作起来也非常方便,网格搜索找到一个最好的参数,之后固定这个参数,再网格搜索下一个参数的集合,找到下一个最好的参数,依次进行。方法比较简单,这里就不给出实现过程了。

    交叉验证

    交叉验证的目的是啥?答:我们总不能得到一个结果就提交一次结果,通过看线上评测结果来判断模型是否有改进把(一般线上评测都是有每日提交限制的)。

    所以,交叉验证引进的目的,就是提供一套线下评测的方法。交叉验证的方法如下:

    这里以随机森林模型的交叉验证为例:

    # cross-validation
    from sklearn.ensemble import RandomForestClassifier
    from sklearn import cross_validation
    
    model = RandomForestClassifier(n_estimators=50, min_samples_split=3)
    all_data = train_df1
    X = all_data.as_matrix()[:,:-1]
    y = all_data.as_matrix()[:,-1]
    model.fit(X, y)
    print cross_validation.cross_val_score(model, X, y, cv=5)
    

    模型融合

    模型融合的重要性这里需要强调,它能很好的结合多个模型的优点,取长补短,从而得到更好的预测结果。

    网上模型融合的博文很多,我这里只谈谈我自己的看法,模型融合需要同等级的选手才能融合,效果相差很大的模型不适用,模型融合甚至可以包括多个不同参数的相同模型融合...

    由于模型融合有很多trick,我这个新手也需要在这方面,多多积累经验才是。最简单的模型融合方法,就是构建一个打分系统,根据少数服从多数的原则,得到最终的预测结果。

    优化改进

    冠军兄弟开源的二分类方案,是将target商铺设成1(正样本),其他所有商铺设成0(负样本),这么一个负样本明显多于正样本的二分类模型,这个时候就可以采用一些正负样本不均衡时采用的方法,网上检索关键词有相应的教程,冠军兄弟解决数据不均衡的方法就是随机采样负样本这种简单方便的方法。

    因为天池复赛需要用到他们的平台,所以mapreduce和sql成了比较重要的基本技能,开源代码有很多在平台实战的trick,真打到复赛的时候,也能成为优化改进的经验来用把。

    ××××××××××××××××××××××××××××××××××××××××××

    本文属于笔者(EdwardChou)原创

    转载请注明出处

    ××××××××××××××××××××××××××××××××××××××××××

    相关文章

      网友评论

        本文标题:天池比赛--“商场中精确定位用户所在店铺”分享

        本文链接:https://www.haomeiwen.com/subject/fcwqnxtx.html