簡單介紹:
這是一個真實的比賽。賽題來源是天池大資料的 "商場中精確定位使用者所在店鋪"。原資料有114萬條,計算起來非常困難。為了讓初學者有一個更好的學習體驗,也更加基礎,我將資料集縮小了之後放在這裡,密碼:ndfd。供大家下載。
在我的資料中,資料是這樣子的: train.csv
user_id | 使用者的id | time_stamp | 時間戳 |
---|---|---|---|
latitude | 緯度 | wifi_strong 1-10 | 十個wifi的訊號強度 |
longitude | 經度 | wifi_id 1-10 | 十個wifi的id |
shop_id | 商店的id | con_sta 1-10 | 十個wifi連線狀態 |
test.csv
user_id | 使用者的id | time_stamp | 時間戳 |
---|---|---|---|
latitude | 緯度 | wifi_id 1-10 | 十個wifi的id |
longitude | 經度 | con_sta 1-10 | 十個wifi連線狀態 |
row_id | 行標 | wifi_strong 1-10 | 十個wifi的訊號強度 |
shop_id | 商店的id |
這個題目的意思是,我們在商場中,由於不同層數和GPS精度限制,我們並不能僅根據經緯度準確知道某使用者具體在哪一家商店中。我們通過手機與附近10個wifi點的連線情況,來精準判斷出使用者在哪個商店中。方便公司根據使用者的位置投放相應店家的廣告。
開始實戰
準備實戰之前,當然要對整個XGBoost有一個基本瞭解,對這個模型不太熟悉的朋友,建議看我之前的文章《XGBoost》。
實戰的流程一般是先將資料預處理,成為我們模型可處理的資料,包括丟失值處理,資料拆解,型別轉換等等。然後將其匯入模型執行,最後根據結果正確率調整引數,反覆調引數達到最優。
我們在機器學習實戰的時候一定要脫離一個思維慣性————一切都得我們思考周全才可以執行。這是一個很有趣的思維慣性,怎麼解釋呢?比如這道賽題,我也是學通訊出身的,看到十個wifi強度值,就想找這中間的關係,然後程式設計來求解人的確切位置。這本質上還是我們的思維停留在顯式程式設計的層面上,覺得程式只有寫清楚才可達到預定的目標。但其實大資料處理並不是這個原理。決策樹不管遇到什麼資料,無論是時間還是地理位置,都是一樣的按照一定規則生成樹,最後讓新資料按照這個樹走一遍得到預測的結果。也就是說我們不必花很多精力去考慮每個資料的具體物理意義,只要把他們放進模型裡面就可以了。(調參需要簡單地考慮物理意義來給各個資料以權重,這個以後再說)
分析一下資料
我們的資料的意義都在上面那張表裡面,我們有使用者的id、經緯度、時間戳、商店id、wifi資訊。我們簡單思考可以知道:
- user_id並沒有什麼實際意義,僅僅是一個代號而已
- shop_id是我們預測的目標,我們題目要求就是我們根據其他資訊來預測出使用者所在的shop_id,所以 shop_id 是我們的訓練目標
- 經緯度跟我們的位置有關,是有用的資訊
- wifi_id 讓我們知道是哪個路由器,這個不同的路由器位置不一樣,所以有用
- wifi_strong是訊號強度,跟我們離路由器距離有關,有用
- con_sta是連線狀態,也就是有沒有連上。本來我看資料中基本都是沒連上,以為沒有用。後來得高人提醒,說如果有人自動連上某商店wifi,不是可以說明他常來麼,這個對於判斷顧客也是有一點用的。
- 我們看test.csv總體差不多,就多了個row_id,我們輸出結果要注意對應上就可以
python庫準備
import pandas as pd
import xgboost as xgb
from sklearn import preprocessing
複製程式碼
我們這個XGBoost比較簡單,所以就使用了最必要的三個庫,pandas資料處理庫,xgboost庫,從大名鼎鼎的機器學習庫sklearn中匯入了preprocessing庫,這個pandas庫對資料的基本處理有很多封裝函式,用起來比較順手。想看例子的戳這個連結,我寫的pandas.Dataframe基本拆解資料的方法。
先進行資料預處理
我們得先匯入一份資料:
train = pd.read_csv(r'D:\XGBoost_learn\mall_location\train2.csv')
tests = pd.read_csv(r'D:\XGBoost_learn\mall_location\test_pre.csv')
複製程式碼
我們使用pandas裡面的read_csv
函式直接讀取csv檔案。csv檔案全名是Comma-Separated Values檔案,就是每個資料之間都以逗號隔開,比較簡潔,也是各個資料比賽常用的格式。
我們需要注意的是路徑問題,windows下是\
,linux下是/
,這個有區別。並且我們寫的路徑經常會與庫裡的函式欄位重合,所以在路徑最前加一個r
來禁止與庫裡匹配,重合報錯。r
是raw的意思,生的,大家根據名字自行理解一下。
我們的time_stamp
原來是一個str型別的資料,計算機是不會知道它是什麼東西的,只知道是一串字串。所以我們進行轉化成datetime處理:
train['time_stamp'] = pd.to_datetime(pd.Series(train['time_stamp']))
tests['time_stamp'] = pd.to_datetime(pd.Series(tests['time_stamp']))
複製程式碼
train和tests都要處理。這也體現了pandas的強大。接下來我們看time_stamp資料的樣子:2017/8/6 21:20
,看資料集可知,是一個十分鐘為精確度(粒度)的資料,感覺這個資料包含太多資訊了呢,放一起很浪費(其實是容易過擬合,因為一個結點會被分的很細),我們就將其拆開吧:
train['Year'] = train['time_stamp'].apply(lambda x: x.year)
train['Month'] = train['time_stamp'].apply(lambda x: x.month)
train['weekday'] = train['time_stamp'].dt.dayofweek
train['time'] = train['time_stamp'].dt.time
tests['Year'] = tests['time_stamp'].apply(lambda x: x.year)
tests['Month'] = tests['time_stamp'].apply(lambda x: x.month)
tests['weekday'] = tests['time_stamp'].dt.dayofweek
tests['time'] = tests['time_stamp'].dt.time
複製程式碼
細心的朋友可能會發現,這裡採用了兩種寫法,一種是.apply(lambda x: x.year)
,這是什麼意思呢?這其實是採用了一種叫匿名函式的寫法.匿名函式就是我們相要寫一個函式,但並不想費神去思考這個函式該如何命名,這時候我們就需要一個匿名函式,來實現一些小功能。我們這裡採用的是.apply(lambda x: x.year)
實際上是呼叫了apply
函式,是加這一列的意思,加的列的內容就是x.year
。我們要是覺得這樣寫不直觀的話,也可以這樣寫:
YearApply(x):
return x.year
train['Year'] = train['time_stamp'].apply(YearApply)
複製程式碼
這兩種寫法意義都是一樣的。
在呼叫weekday和datetime的時候,我們使用的是numpy裡面的函式dt
,用法如程式碼所示。其實這weekday
也可以這樣寫:
train['weekday'] = train['time_stamp'].apply(lambda x: x.weekday())
,注意多了個括號,因為weekday
需要計算一下才可以得到,所以還呼叫了一下內部的函式。
為什麼採用weekday
呢,因為星期幾比幾號對於購物來說更加有特徵性。
接下來我們將這個time_stamp丟掉,因為已經有了year、month那些:
train = train.drop('time_stamp', axis=1)
tests = tests.drop('time_stamp', axis=1)
複製程式碼
再丟掉缺失值,或者補上缺失值。
train = train.dropna(axis=0)
tests = tests.fillna(method='pad')
複製程式碼
我們看到我對訓練集和測試集做了兩種不同方式的處理。訓練集資料比較多,而且缺失值比例比較少,於是就將所有缺失值使用dropna
函式,tests檔案因為是測試集,不能丟失一個資訊,哪怕資料很多缺失值很少,所以我們用各種方法來補上,這裡採用前一個非nan值補充的方式(method=“pad”)
,當然也有其他方式,比如用這一列出現頻率最高的值來補充。
class DataFrameImputer(TransformerMixin):
def fit(self, X, y=None):
for c in X:
if X[c].dtype == np.dtype('O'):
fill_number = X[c].value_counts().index[0]
self.fill = pd.Series(fill_number, index=X.columns)
else:
fill_number = X[c].median()
self.fill = pd.Series(fill_number, index=X.columns)
return self
def transform(self, X, y=None):
return X.fillna(self.fill)
train = DataFrameImputer().fit_transform(train)
複製程式碼
這一段程式碼有一點拗口,意思是對於X中的每一個c,如果X[c]的型別是object
(‘O’
表示object
)的話就將[X[c].value_counts().index[0]
傳給空值,[X[c].value_counts().index[0]
表示的是重複出現最多的那個數,如果不是object
型別的話,就傳回去X[c].median()
,也就是這些數的中位數。
在這裡我們可以使用print來輸出一下我們的資料是什麼樣子的。
print(train.info())
複製程式碼
<class 'pandas.core.frame.DataFrame' at 0x0000024527C50D08>
Int64Index: 467 entries, 0 to 499
Data columns (total 38 columns):
user_id 467 non-null object
shop_id 467 non-null object
longitude 467 non-null float64
latitude 467 non-null float64
wifi_id1 467 non-null object
wifi_strong1 467 non-null int64
con_sta1 467 non-null bool
wifi_id2 467 non-null object
wifi_strong2 467 non-null int64
con_sta2 467 non-null object
wifi_id3 467 non-null object
wifi_strong3 467 non-null float64
con_sta3 467 non-null object
wifi_id4 467 non-null object
wifi_strong4 467 non-null float64
con_sta4 467 non-null object
wifi_id5 467 non-null object
wifi_strong5 467 non-null float64
con_sta5 467 non-null object
wifi_id6 467 non-null object
wifi_strong6 467 non-null float64
con_sta6 467 non-null object
wifi_id7 467 non-null object
wifi_strong7 467 non-null float64
con_sta7 467 non-null object
wifi_id8 467 non-null object
wifi_strong8 467 non-null float64
con_sta8 467 non-null object
wifi_id9 467 non-null object
wifi_strong9 467 non-null float64
con_sta9 467 non-null object
wifi_id10 467 non-null object
wifi_strong10 467 non-null float64
con_sta10 467 non-null object
Year 467 non-null int64
Month 467 non-null int64
weekday 467 non-null int64
time 467 non-null object
dtypes: bool(1), float64(10), int64(5), object(22)
memory usage: 139.1+ KB
None
複製程式碼
我們可以清晰地看出我們程式碼的結構,有多少列,每一列下有多少個值等等,有沒有空值我們可以根據值的數量來判斷。
我們在缺失值處理之前加入這個print(train.info())
就會得到:
<class 'pandas.core.frame.DataFrame' at 0x000001ECFA6D6718>
RangeIndex: 500 entries, 0 to 499
複製程式碼
這裡面就有500個值,處理後就只剩467個值了,可見丟棄了不少。同樣的我們也可以將test的資訊輸出一下:
<class 'pandas.core.frame.DataFrame' at 0x0000019E13A96F48>
RangeIndex: 500 entries, 0 to 499
複製程式碼
500個值一個沒少。都給補上了。這裡我只取了輸出資訊的標題,沒有全貼過來,因為全資訊篇幅很長。
我們注意到這個資料中有bool
、float
、int
、object
四種型別,我們XGBoost是一種迴歸樹,只能處理數字類的資料,所以我們要轉化。對於那些字串型別的資料我們該如何處理呢?我們採用LabelEncoder方法:
for f in train.columns:
if train[f].dtype=='object':
if f != 'shop_id':
print(f)
lbl = preprocessing.LabelEncoder()
train[f] = lbl.fit_transform(list(train[f].values))
for f in tests.columns:
if tests[f].dtype == 'object':
print(f)
lbl = preprocessing.LabelEncoder()
tests[f] = lbl.fit_transform(list(tests[f].values))
複製程式碼
這段程式碼的意思是呼叫sklearn中preprocessing裡面的LabelEncoder方法,對資料進行標籤編碼,作用主要就是使其變成數字類資料,有的進行歸一化處理,使其執行更快等等。
我們看這段程式碼,lbl只是LabelEncoder的簡寫,lbl = preprocessing.LabelEncoder()
,這段程式碼只有一個代換顯得一行不那麼長而已,沒有實際執行什麼。第二句lbl.fit_transform(list(train[f].values))
是將train裡面的每一個值進行編碼,我們在其前後輸出一下train[f].values
就可以看出來:
print(train[f].values)
train[f] = lbl.fit_transform(list(train[f].values))
print(train[f].values)
複製程式碼
我加上那一串0
和/
的目的是分隔開輸出資料。我們得到:
user_id
['u_376' 'u_376' 'u_1041' 'u_1158' 'u_1654' 'u_2733' 'u_2848' 'u_3063'
'u_3063' 'u_3063' 'u_3604' 'u_4250' 'u_4508' 'u_5026' 'u_5488' 'u_5488'
'u_5602' 'u_5602' 'u_5602' 'u_5870' 'u_6429' 'u_6429' 'u_6870' 'u_6910'
'u_7037' 'u_7079' 'u_7869' 'u_8045' 'u_8209']
[ 7 7 0 1 2 3 4 5 5 5 6 8 9 10 11 11 12 12 12 13 14 14 15 16 17
18 19 20 21]
複製程式碼
我們可以看出,LabelEncoder將我們的str型別的資料轉換成數字了。按照它自己的一套標準。
對於tests資料,我們可以看到,我單獨將shop_id
給避開了。這樣處理的原因就是shop_id
是我們要提交的資料,不能有任何編碼行為,一定要保持這種str狀態。
接下來需要將train和tests轉化成matrix型別,方便XGBoost運算:
feature_columns_to_use = ['Year', 'Month', 'weekday',
'time', 'longitude', 'latitude',
'wifi_id1', 'wifi_strong1', 'con_sta1',
'wifi_id2', 'wifi_strong2', 'con_sta2',
'wifi_id3', 'wifi_strong3', 'con_sta3',
'wifi_id4', 'wifi_strong4', 'con_sta4',
'wifi_id5', 'wifi_strong5', 'con_sta5',
'wifi_id6', 'wifi_strong6', 'con_sta6',
'wifi_id7', 'wifi_strong7', 'con_sta7',
'wifi_id8', 'wifi_strong8', 'con_sta8',
'wifi_id9', 'wifi_strong9', 'con_sta9',
'wifi_id10', 'wifi_strong10', 'con_sta10',]
train_for_matrix = train[feature_columns_to_use]
test_for_matrix = tests[feature_columns_to_use]
train_X = train_for_matrix.as_matrix()
test_X = test_for_matrix.as_matrix()
train_y = train['shop_id']
複製程式碼
待訓練目標是我們的shop_id
,所以train_y
是shop_id
。
匯入模型生成決策樹
gbm = xgb.XGBClassifier(silent=1, max_depth=10, n_estimators=1000, learning_rate=0.05)
gbm.fit(train_X, train_y)
複製程式碼
這兩句其實可以合併成一句,我們也就是在XGBClassifier
裡面設定好引數,其所有引數以及其預設值(預設值)我寫在這,內容來自XGBoost原始碼:
- max_depth=3, 這代表的是樹的最大深度,預設值為三層。max_depth越大,模型會學到更具體更區域性的樣本。
- learning_rate=0.1,學習率,也就是梯度提升中乘以的係數,越小,使得下降越慢,但也是下降的越精確。
- n_estimators=100,也就是弱學習器的最大迭代次數,或者說最大的弱學習器的個數。一般來說n_estimators太小,容易欠擬合,n_estimators太大,計算量會太大,並且n_estimators到一定的數量後,再增大n_estimators獲得的模型提升會很小,所以一般選擇一個適中的數值。預設是100。
- silent=True,是我們訓練xgboost樹的時候後臺要不要輸出資訊,True代表將生成樹的資訊都輸出。
- objective="binary:logistic",這個引數定義需要被最小化的損失函式。最常用的值有:
binary:logistic
二分類的邏輯迴歸,返回預測的概率(不是類別)。multi:softmax
使用softmax的多分類器,返回預測的類別(不是概率)。在這種情況下,你還需要多設一個引數:num_class(類別數目)。multi:softprob
和multi:softmax引數一樣,但是返回的是每個資料屬於各個類別的概率。
- nthread=-1, 多執行緒控制,根據自己電腦核心設,想用幾個執行緒就可以設定幾個,如果你想用全部核心,就不要設定,演算法會自動識別
- `gamma=0,在節點分裂時,只有分裂後損失函式的值下降了,才會分裂這個節點。Gamma指定了節點分裂所需的最小損失函式下降值。 這個引數的值越大,演算法越保守。這個引數的值和損失函式息息相關,所以是需要調整的。
- min_child_weight=1,決定最小葉子節點樣本權重和。 和GBM的
min_child_leaf
引數類似,但不完全一樣。XGBoost的這個引數是最小樣本權重的和,而GBM引數是最小樣本總數。這個引數用於避免過擬合。當它的值較大時,可以避免模型學習到區域性的特殊樣本。 但是如果這個值過高,會導致欠擬合。這個引數需要使用CV來調整
- max_delta_step=0, 決定最小葉子節點樣本權重和。 和GBM的 min_child_leaf 引數類似,但不完全一樣。XGBoost的這個引數是最小樣本權重的和,而GBM引數是最小樣本總數。這個引數用於避免過擬合。當它的值較大時,可以避免模型學習到區域性的特殊樣本。 但是如果這個值過高,會導致欠擬合。這個引數需要使用CV來調整。
- subsample=1, 和GBM中的subsample引數一模一樣。這個引數控制對於每棵樹,隨機取樣的比例。減小這個引數的值,演算法會更加保守,避免過擬合。但是,如果這個值設定得過小,它可能會導致欠擬合。典型值:0.5-1
- colsample_bytree=1, 用來控制每棵隨機取樣的列數的佔比(每一列是一個特徵)。典型值:0.5-1
- colsample_bylevel=1,用來控制樹的每一級的每一次分裂,對列數的取樣的佔比。其實subsample引數和colsample_bytree引數可以起到相似的作用。
- reg_alpha=0,權重的L1正則化項。(和Lasso regression類似)。可以應用在很高維度的情況下,使得演算法的速度更快。
- reg_lambda=1, 權重的L2正則化項這個引數是用來控制XGBoost的正則化部分的。這個引數越大就越可以懲罰樹的複雜度
- scale_pos_weight=1,在各類別樣本十分不平衡時,把這個引數設定為一個正值,可以使
- base_score=0.5, 所有例項的初始化預測分數,全域性偏置;為了足夠的迭代次數,改變這個值將不會有太大的影響。
- seed=0, 隨機數的種子設定它可以復現隨機資料的結果,也可以用於調整引數
資料通過樹生成預測結果
predictions = gbm.predict(test_X)
複製程式碼
將tests裡面的資料通過這生成好的模型,得出預測結果。
submission = pd.DataFrame({'row_id': tests['row_id'],
'shop_id': predictions})
print(submission)
submission.to_csv("submission.csv", index=False)
複製程式碼
將預測結果寫入到csv檔案裡。我們注意寫入檔案的格式,row_id
在前,shop_id
在後。index=False
的意思是不寫入行的名稱。改成True就把每一行的行標也寫入了。
附錄
參考資料
- 機器學習系列(12)_XGBoost引數調優完全指南(附Python程式碼)http://blog.csdn.net/han_xiaoyang/article/details/52665396
- Kaggle比賽:泰坦尼克之災: https://www.kaggle.com/c/titanic
完整程式碼
import pandas as pd
import xgboost as xgb
from sklearn import preprocessing
train = pd.read_csv(r'D:\mall_location\train.csv')
tests = pd.read_csv(r'D:\mall_location\test.csv')
train['time_stamp'] = pd.to_datetime(pd.Series(train['time_stamp']))
tests['time_stamp'] = pd.to_datetime(pd.Series(tests['time_stamp']))
print(train.info())
train['Year'] = train['time_stamp'].apply(lambda x:x.year)
train['Month'] = train['time_stamp'].apply(lambda x: x.month)
train['weekday'] = train['time_stamp'].apply(lambda x: x.weekday())
train['time'] = train['time_stamp'].dt.time
tests['Year'] = tests['time_stamp'].apply(lambda x: x.year)
tests['Month'] = tests['time_stamp'].apply(lambda x: x.month)
tests['weekday'] = tests['time_stamp'].dt.dayofweek
tests['time'] = tests['time_stamp'].dt.time
train = train.drop('time_stamp', axis=1)
train = train.dropna(axis=0)
tests = tests.drop('time_stamp', axis=1)
tests = tests.fillna(method='pad')
for f in train.columns:
if train[f].dtype=='object':
if f != 'shop_id':
print(f)
lbl = preprocessing.LabelEncoder()
train[f] = lbl.fit_transform(list(train[f].values))
for f in tests.columns:
if tests[f].dtype == 'object':
print(f)
lbl = preprocessing.LabelEncoder()
lbl.fit(list(tests[f].values))
tests[f] = lbl.transform(list(tests[f].values))
feature_columns_to_use = ['Year', 'Month', 'weekday',
'time', 'longitude', 'latitude',
'wifi_id1', 'wifi_strong1', 'con_sta1',
'wifi_id2', 'wifi_strong2', 'con_sta2',
'wifi_id3', 'wifi_strong3', 'con_sta3',
'wifi_id4', 'wifi_strong4', 'con_sta4',
'wifi_id5', 'wifi_strong5', 'con_sta5',
'wifi_id6', 'wifi_strong6', 'con_sta6',
'wifi_id7', 'wifi_strong7', 'con_sta7',
'wifi_id8', 'wifi_strong8', 'con_sta8',
'wifi_id9', 'wifi_strong9', 'con_sta9',
'wifi_id10', 'wifi_strong10', 'con_sta10',]
big_train = train[feature_columns_to_use]
big_test = tests[feature_columns_to_use]
train_X = big_train.as_matrix()
test_X = big_test.as_matrix()
train_y = train['shop_id']
gbm = xgb.XGBClassifier(silent=1, max_depth=10,
n_estimators=1000, learning_rate=0.05)
gbm.fit(train_X, train_y)
predictions = gbm.predict(test_X)
submission = pd.DataFrame({'row_id': tests['row_id'],
'shop_id': predictions})
print(submission)
submission.to_csv("submission.csv",index=False)
複製程式碼