kaggle竞赛金牌策略复现:rossmann-store-sales的第三名策略——Entity Embedding

博客围绕Kaggle竞赛中离散特征处理展开。先介绍其他模型如Catboost、LightGBM处理离散特征的方式及问题,接着重点阐述基于深度学习Embedding层的Entity Embedding方法。还讲述了数据预处理步骤及建模过程,最后给出建模结果并分析效果差异。

关于这个数据集以及竞赛的详情,可以看我的上一篇:kaggle竞赛数据集:rossmann-store-sales_thorn_r的博客-优快云博客

零、其他模型离散特征的处理方式 

在这个竞赛中,有着众多的离散型的特征难以处理,以最为典型店铺号为例,数据集中包含了1115家店铺类型的店铺历史信息数据。对于建模是一个十分重要的信息。但是,对于这样一个有着1115个类别的特征而言,不能使用传统的热编码或者哑变量来进行预处理:因为这样不仅会使得特征具有严重的稀疏性,而且1115个哑变量/热编码也会造成维度灾难,会严重影响模型的效果。而如果将1115个商店分别拎出来进行建模,那么不仅每家单店的数据量过少,仅983条,且没有同类型店铺的相关先验信息,并造成了很多特征不再可用。所以对于这样的多分类离散特征算是这个竞赛的痛点之一,而在之前的很多机器学习模型中,也有类似对于这类多类型离散特征变量的处理。

1、Catboost:CatBoost创新性地使用了将原本的离散特征连续化的算法:Ordered Target Statistics。具体而言,对于离散特征的每个分类,都统计其每个分类下因变量的频率(分类任务)或者均值(回归任务),以此来将其作为连续特征,这就是Target Statistics(还没有Ordered)。然而,这种方法随之而来就产生了一个问题:将所有数据的因变量都作为一个特征来用,本身是一种数据泄露(data leakage),会产生预测偏移从而导致模型出现过拟合的可能,因此CatBoost在原本的Target Statistics基础上,现将样本随机打乱,再将打乱后每个样本排序之前的样本做Target Statistics,通过这样的方式就能在一定程度上缓解预测偏移量(具体原理似乎是减少每次梯度计算的误差,这部分我没有读过相关资料),这就是Ordered Target Statistics。

2、LightGBM:LightGBM本身使用的是各个特征“分桶”作为分裂选择的判断条件,具体到分类特征而言,就是将每个类别对应的因变量均值(或者频率)作为桶,以此和连续变量一样做“分桶”操作。当然,这样做和上面CatBoost的思路很像,也具有Target Statistic的数据泄露问题,因此LightGBM还加入了很多的正则化约束。

可以看出,对于各类的GBDT类模型而言,处理各种离散特征的方式就是拿因变量的均值或频率对原本的分类做Mapping,在此基础上使用各种方式缓解原本的数据泄露问题。

而本篇将要复现的对于离散特征的处理策略,则是基于深度学习中的Embedding层来对分类特征做处理。

一、Entity Embedding处理离散特征

在NLP处理的过程中,经常会使用Word Embedding的方法来对文本数据作预处理。从文本数据的角度而言,如果给每个词都做一个one-hot编码,那么最终的特征量将会异常庞大,发生维度灾难。所以显然的,需要一种能够将特征维度变小的预处理方法,于是就有了Word Embedding。Word Embedding相比于One-hot那种“每个特征都使用0-1向量来表示,特征有多少种分类0-1向量就有多少长度”而言,它的向量变成了我们预先给出的固定值,与特征的分类数量无关,向量也不再是0-1向量,而是一个稠密向量,这样每个分类的向量不同,同时又保证了不会发生维度灾难。对于NLP来说,还需要考虑词的先后文信息,所以还有CBOW,Skip-gram一类的word2vec预处理。当然,对于我们单纯的多分类离散特征连续化而言,只需要Embedding层就可以了。

Kaggle竞赛中使用这个策略的团队还写了篇论文到Arxiv上,有兴趣可以去阅读以下。

http://arxiv.org/abs/1604.06737

本篇文章将会复现这个第三名的策略。

github链接:https://github.com/entron/entity-embedding-rossmann 竞赛的代码可以看对应的kaggle branch。

二、数据预处理

github上的数据预处理有五个步骤:

python3 extract.py
python3 extract_weather.py
python3 extract_google_trend.py
python3 extract_fb_features.py
python3 prepare_features.py

 第一个extract.py没什么好说的,现在直接用pandas就可以了。

# extract.py
df_train = pd.read_csv("train.csv")
df_test = pd.read_csv("test.csv")
df_store = pd.read_csv("store.csv")
df_store_states = pd.read_csv("store_states.csv")
df_store = df_store.merge(df_store_states,on="Store",how="left")

第二和第三个weather和Google trend是参赛给出数据集以外的external数据集,简单mapping一下也可以了:

# extract_weather.py
weather_path = "weather/"

d = {}
d['BadenWuerttemberg'] = 'BW'
d['Bayern'] = 'BY'
d['Berlin'] = 'BE'
d['Brandenburg'] = 'BB'  # do not exist in store_state
d['Bremen'] = 'HB'  # we use Niedersachsen instead of Bremen
d['Hamburg'] = 'HH'
d['Hessen'] = 'HE'
d['MecklenburgVorpommern'] = 'MV'  # do not exist in store_state
d['Niedersachsen'] = 'HB,NI'  # we use Niedersachsen instead of Bremen
d['NordrheinWestfalen'] = 'NW'
d['RheinlandPfalz'] = 'RP'
d['Saarland'] = 'SL'
d['Sachsen'] = 'SN'
d['SachsenAnhalt'] = 'ST'
d['SchleswigHolstein'] = 'SH'
d['Thueringen'] = 'TH'

def event2int(event):
    event_list = ['', 'Fog-Rain', 'Fog-Snow', 'Fog-Thunderstorm',
                  'Rain-Snow-Hail-Thunderstorm', 'Rain-Snow', 'Rain-Snow-Hail',
                  'Fog-Rain-Hail', 'Fog', 'Fog-Rain-Hail-Thunderstorm', 'Fog-Snow-Hail',
                  'Rain-Hail', 'Rain-Hail-Thunderstorm', 'Fog-Rain-Snow', 'Rain-Thunderstorm',
                  'Fog-Rain-Snow-Hail', 'Rain', 'Thunderstorm', 'Snow-Hail',
                  'Rain-Snow-Thunderstorm', 'Snow', 'Fog-Rain-Thunderstorm']
    return event_list.index(event)

df_weather = pd.DataFrame(columns=["State","Date","max_temp","mean_temp","min_temp",
                                  "max_humi","mean_humi","min_humi","max_wind","mean_wind","CloudCover","Events"])

for i in os.listdir(weather_path):
    df_weather_tmp = pd.read_csv(weather_path+i,delimiter=";")
    df_weather_tmp["State"] = d[i.replace(".csv","")]
    df_weather_tmp = df_weather_tmp.loc[:,['State','Date', 'Max_TemperatureC', 'Mean_TemperatureC', 'Min_TemperatureC',
                                          'Max_Humidity','Mean_Humidity', 'Min_Humidity','Max_Wind_SpeedKm_h', 'Mean_Wind_SpeedKm_h',"CloudCover","Events"]]
    df_weather_tmp.columns = df_weather.columns
    df_weather = pd.concat([df_weather,df_weather_tmp],axis=0,ignore_index=True)

df_weather["CloudCover"] = df_weather["CloudCover"].replace(np.nan,0)
df_weather["Events"] = df_weather["Events"].replace(np.nan,"")
df_weather["Events"] = [event2int(i) for i in df_weather["Events"]]
# extract_google_trend.py
from datetime import datetime
trend_path = ("googletrend/")
df_google_trend = pd.DataFrame(columns=["State","year","week_of_year","trend"])
for i in os.listdir(trend_path):
    state = i.replace(".csv","").split("_")[-1]
    if state == 'NI':
        state = 'HB,NI'
    df_google_tmp = pd.read_csv(trend_path+i)
    df_google_tmp.rename(columns={df_google_tmp.columns[-1]:"trend"},inplace=True)
    df_google_tmp["Date"] = [datetime.strptime(df_google_tmp["Woche"][i].split(" - ")[-1],'%Y-%m-%d') for i in df_google_tmp.index]
    df_google_tmp["year"] = df_google_tmp["Date"].dt.year
    df_google_tmp["week_of_year"] = df_google_tmp["Date"].dt.isocalendar()["week"]
    df_google_tmp["State"] = state
    df_google_tmp = df_google_tmp.loc[:,["State","year","week_of_year","trend"]]
    df_google_trend = pd.concat([df_google_trend,df_google_tmp],axis=0)

而第四个extract_fb_features.py,具体就是做一个滑动窗口,在窗口内计算每一天离它最近的事件(State Holiday/School Hodliay等)或者事件计数。

我们先看看它的原始代码:

# extract_fb_features.py

class smart_timestamp_accessor(object):
    # 根据时间求出index

    def __init__(self, timestamps):
        self.timestamps = timestamps
        self._last_index = 0

    def compute_index_of_timestamp(self, timestamp):
        #首末尾抹去超出范围的部分
        if timestamp < self.timestamps[0]:
            return 0
        if timestamp > self.timestamps[len(self.timestamps) - 1]:
            return (len(self.timestamps) - 1)
        
        #此处会更新对象的_last_index属性,此时运行的窗口的终点变为下一次运行的窗口起点,以此达到“滑动窗口”的效果
        if timestamp == self.timestamps[self._last_index]:
            return self._last_index

        # 此处存在一个bug: 某些店铺会有从2014年7月1号~12月31号没有数据的情形,以7日的前向(forward)滑动窗口为例:
        # 此时,6月24日会进入timestamp > self.timestamps[self._last_index]分支,将2015年1月1日也放入7日的滑动窗口中
        # 但是,25号号却会进入timestamp < self.timestamps[self._last_index]分支;将滑动窗口截到6月30及之前未满7日的长度
        # 26号又变成正向的滑动窗口(大于分支),27号又变成反向的滑动窗口(<)
        if timestamp > self.timestamps[self._last_index]:
            while timestamp > self.timestamps[self._last_index]:
                self._last_index += 1
            return self._last_index
        
        
        if timestamp < self.timestamps[self._last_index]:
            print(timestamp,self.timestamps[self._last_index])
            while timestamp < self.timestamps[self._last_index]:
                self._last_index -= 1
            return self._last_index

    def get_start_end_timestamp(self):
        timestamp_start = self.timestamps[0]
        nr_timestamps = len(self.timestamps)
        timestamp_end = self.timestamps[nr_timestamps - 1]
        return (timestamp_start, timestamp_end)




def generate_forward_backward_information_accumulated(data, store_id, window_size=14, only_zero=Fal
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值