2017 年這篇拿到 0.76555。回頭讀當年那支程式,
preprocessing()先把Age除以最大值壓進 0 到 1,再往下才跑Age < 16就改成Child的規則,891 筆全部符合,Sex整欄剩下同一個值。0.76555 換算回去是 418 筆測試資料裡答對 320 題,只看性別的那條規則在同一批資料上答對的也是 320 題。
從題目可以知道,這是一個 binary classification,最初想到 SVM 和 perceptron。
從題目給的數據,選擇 Decision Tree 或 Random Forest 可能是比較合理的想法。不過這邊我想用 Logistic Regression 來試試 (sigmoid + cross entropy)。
把訓練資料的內容全部都變成 0-1 的數字,剩下的就交給 NN 去解決。因為我們最後一層的 activation function 是 sigmoid,為了避免梯度消失,因此在做 cross entropy 時把最大最小值定為 0.00001 和 0.99999,做每次的訓練時才不會有 Nan 的問題。
那個 clip 在擋什麼
這一步值得多說幾句,因為它是自己寫 cross entropy 幾乎一定會撞到的地方。二元的 cross entropy 長這樣:
loss = -(y * tf.log(p) + (1 - y) * tf.log(1 - p))
p 是 sigmoid 吐出來的機率。sigmoid 的輸出理論上永遠落在開區間 (0, 1),取 log 沒有問題;但浮點數只有有限的位數,輸入的絕對值稍微大一點,算出來的 p 就會被四捨五入成剛好的 0.0 或 1.0。log(0) 是負無限大,乘上前面的係數再拿去做梯度,整批數值就變成 NaN,而 NaN 一旦進了權重就再也回不來,後面每一步都是 NaN。
把 p 夾在 0.00001 和 0.99999 之間,就是不讓它真的碰到端點。程式裡是用 tf.maximum 寫的:
cross_entropy = -tf.reduce_mean(y_*tf.log(tf.maximum(0.00001, predictions)) +
(1.0 - y_)*tf.log(tf.maximum(0.00001, 1.0-predictions)))
夾完之後最極端的損失是 -log(0.00001),11.512925,是個大但有限的數字。不過 maximum 在被夾住的那一側不傳梯度。用 torch 照同一個式子算,y 取 0、logit 取 12、16、18 這三個點,損失都停在 11.512925,對 logit 的偏微分都是 0;換成 BCEWithLogitsLoss 算同樣三個點,損失是 12.000006、16 和 18,偏微分接近 1。所以夾這一下擋住的是 NaN 擴散到整批權重,被夾住的那一筆樣本在那一步沒有再往前調。
實務上現在多半直接用框架寫好的版本(TensorFlow 的 sigmoid_cross_entropy_with_logits、PyTorch 的 BCEWithLogitsLoss),它們吃的是 sigmoid 之前的 logits,內部把 log 和 sigmoid 合併化簡過,就不會有這個端點問題。自己拆開來寫的時候才需要自己夾。
結果
Kaggle : 0.76555
分數只有這樣,大概有幾個地方需要檢討:
- overfitting??:train 可以到 90% 但是 test 最高就是這數字。除了 overfitting,另外一個就是資料的考量,因為有故意捨去某些資料來做訓練,可能留下的在測試資料中反而是缺失的。
- 解決 overfitting 的方式:選用 dropout 可能在這裡沒有比 regularization 還好,這需要調整。
- 填補資料的方式:在空白資料上很多是填上零或者平均值,有些隱藏相關沒考慮到?
- feature:最可能就是 feature 的問題了,因為在類似的作法下,使用 XGB 試過也沒有好多少,因此應該要嘗試其他表現方式。
原本想考慮好好用 Random Forest 和 XGB 認真做一次,想想應該真的是在 feature 上有問題;同樣用 Deep learning 來做的人,肯定也有做到非常高。
先邁入下個試題,希望回頭後有新想法。
preprocessing() 這個函式
當年那支程式還在。preprocessing() 裡面對 Age 動了兩次,開頭是正規化:
dataset['Age'] = (dataset['Age'].fillna(train_data['Age'].mean()))/max(train_data['Age'])
再往下二十幾行,是一條把未成年標出來的規則:
# if age < 16, set 'Sex' to Child
dataset.loc[(dataset.Age < 16),'Sex'] = 'Child'
跑到這一行的時候,Age 已經被除過最大值,整欄落在 0 到 1 之間,最大值就是 1.0,所以 Age < 16 對 891 筆全部成立,Sex 整欄被寫成 Child。再往下的對映是 {'female': 1, 'male': 0, 'Child': 2} 除以 2,891 筆算出來都是 1.0。用測試資料呼叫同一個函式,那 418 筆也是同樣的結果。模型在 Sex 這個位置拿到的是一個常數,跟 bias 併在一起,區分不出任何人。
同一個函式還有另一處。它的參數名字叫 train_data,所以函式裡的 max(train_data['Age']) 取的是傳進來那份資料的最大值,而測試資料是這樣送進去的:
test_features = preprocessing(test_data)
Age 的最大值在訓練資料是 80,在測試資料是 76;Parch 是 6 和 9。同一個帶著一個小孩的乘客,訓練時被編碼成 1/6,測試時是 1/9。Fare、SibSp、Pclass 兩邊的最大值剛好相同,這三欄沒有差。
四點檢討裡,第四點猜的是 feature。上面這兩處都發生在 feature 之前,Sex 被壓成常數,Age 和 Parch 在訓練和測試時不在同一個尺度上。
至於 Title,同一個函式裡是有做的:
title_mapping = {"Mr": 1, "Miss": 2, "Mrs": 3, "Master": 4, "Royalty":5, "Officer": 6}
dataset['Title'] = dataset.Name.str.extract(' ([A-Za-z]+)\.', expand=False)
...
dataset['Title'] = (dataset['Title'].fillna(0))/len(title_mapping)
中間省略的那幾行把 Mlle、Ms 併進 Miss,Capt、Col、Major、Rev 併成 Officer,Dr 依性別歸到 Mr 或 Mrs。對映完除以 6,Title 進模型的時候是 0 到 1 之間的六個值。
資料裡的訊號在哪裡
以下的數字都是從官方 train.csv 的 891 筆資料數出來的,量的是資料本身。
欄位一共十二個:PassengerId、Survived、Pclass、Name、Sex、Age、SibSp(同行的兄弟姊妹或配偶人數)、Parch(同行的父母或子女人數)、Ticket、Fare、Cabin、Embarked。缺值集中在兩處:Age 缺 177 筆,Cabin 缺 687 筆,也就是艙房這欄有將近八成是空的。
存活率的差距一眼就看得出來。891 個人裡活下來 342 個,整體 38.4%。拆開性別,女性 233/314,74.2%;男性 109/577,18.9%。拆開艙等,頭等 63.0%、二等 47.3%、三等 24.2%。這兩欄各自都把資料切得很開,切的方向也不一樣,所以它們提供的是兩份不重複的訊息。
性別這一欄強到什麼程度?完全不訓練,只寫死一條規則「女的算活、男的算死」,在這 891 筆上答對 701 題,0.7868。
Kaggle 的測試資料是 418 筆。0.76555 乘以 418 是 319.9999,換算回去就是答對 320 題。把上面那條只看性別的規則套到那 418 筆上,答對的也是 320 題(測試集的實際存活結果取自 Vanderbilt 的 titanic3,那份收了全部 1309 人;拿它跟 train.csv 那 891 筆的標記回頭對照,891 筆一致)。Kaggle 自己附的範例提交檔 gender_submission.csv 寫的就是這條規則,女性填 1、男性填 0。
Name 欄位裡的稱謂
Name 這一欄本身不像特徵。原始資料長這樣:
1,0,3,"Braund, Mr. Owen Harris",male,22,1,0,A/5 21171,7.25,,S
2,1,1,"Cumings, Mrs. John Bradley (Florence Briggs Thayer)",female,38,1,0,PC 17599,71.2833,C85,C
姓氏的逗號之後、第一個句點之前的那段就是稱謂。用一行正規表示式抽出來,891 筆分成 Mr 517、Miss 182、Mrs 125、Master 40,剩下的是 Dr、Rev、Major 之類的零星頭銜。
各自的存活率是這樣:
| 稱謂 | 人數 | 存活率 |
|---|---|---|
| Mr | 517 | 15.7% |
| Mrs | 125 | 79.2% |
| Miss | 182 | 69.8% |
| Master | 40 | 57.5% |
有趣的是 Master 這一組。它在當時的英文裡指的是未成年男性,訓練資料裡這 40 個人有年齡的 36 筆最大就是 12 歲;整體男性的存活率是 18.9%,這 40 個男孩卻有 57.5%。Sex 這一欄把他們和 517 個 Mr 混在一起,看到的只有「男性」;Age 那一欄理論上能區分,但它缺了 177 筆,其中 136 筆是三等艙的乘客。稱謂等於是一個不會缺值的年齡與社會身分代理變數。
程式那邊 Sex 壓成常數之後,性別的資訊是靠 Title 進到模型的。稱謂對映完是 Mr 1、Miss 2、Mrs 3、Master 4、Royalty 5、Officer 6,其中 Miss 和 Mrs 全部是女性,Mr、Master、Officer 全部是男性,只有歸進 Royalty 的那 5 個人男女都有。從 Title 反推性別,891 筆裡對得起來的有 888 筆。
同一類的做法還有幾個:SibSp 和 Parch 加起來得到同行的家庭人數,一個人搭船和帶著四五個家人搭船是不同的處境;Cabin 程式裡有抽首字母,不過最後只分成有沒有記錄兩類,而有記錄的那 204 筆首字母是甲板層,還能再細一層;Ticket 的編號有重複,共用票號的人多半是同行的。
這些欄位在原始表格裡都不是數字,直接丟進模型只會被當成無意義的字串或整數編號。把它們變成數字的那一步就是 feature engineering。



