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