2019年8月18日

機器學習_線性迴歸

機器學習擅長

  1. 回歸 regression

    將連續性的資料進行觀察,用以預測未來的結果。例如股價、身高、體重

  2. 分類 classification

    收集既有的資料,進行訓練,根據訓練結果預測新資料的分類。例如:垃圾郵件判斷、手寫數字辨識

  3. 分群 clustering

    根據資料進行分群,但跟分類不同,分類的訓練資料已經有標記結果,要用來分群的資料並沒有群組的標記。例如:根據學測成績,進行文理組分群

使用具有標記的資料進行機器學習,稱為監督式學習。

使用不具有標記的資料進行機器學習,稱為非監督式學習。

線性迴歸(Linear regression)

當我們取得原始量測資料時,如果在平面座標上標記這些量測點,會感覺到這些點之間,可以畫出最接近這些點的一條直線方程式,線性回歸方法,可以找到這樣的方程式,未來就可以根據這個方程式,預測數值。

從圖形看起來,我們可找到一條「最接近」所有紅色觀測值的直線,以直線方程式 \({f(x)=ax+b}\) 表示這條直線,我們要做的就是找到一個方法,確定 a 與 b 的值,未來就可以利用這個方程式預測數據。在統計中,為了因應未來可能有很多未知數的問題,改以這樣的寫法:

\({f(x)=𝜃_0+𝜃_1x}\)

最小平方法

假設目前有這些數據,當我們任意找 \({𝜃_0 =1, 𝜃_1=2}​\) 時,f(x) 跟實際上的 y 之間有誤差。所以要找到適當的參數,讓 f(x) 與 y 之間的誤差最小,當然如果誤差為 0 是最好的。

x y f(x)
58 374 117
70 385 141
81 375 163
84 401 169

定義誤差函數為

\({E(𝜃)= \frac{1}{2} \sum_{i=1}^{n}( y^{(i)} - f_𝜃(x^{(i)})^2 }\)

  • \(x^{(i)}, y^{(i)}\) 分別是第 i 項的 x 與 y,例如 \(x^{(1)} = 58, y^{(1)}=374\)
  • \({(y^{(i)} - f_𝜃(x^{(i)}) }\) 是誤差值,但因為誤差有可能是負數,所以就用平方,轉成正數
  • 將所有誤差的平方加總後,為了微分計算方便,就在前面再乘上 1/2。因為乘上任意的正數,只會讓圖形橫向壓扁,但不會改變最小值的位置。任意的正數都可以,選擇 1/2 是因為後面的例子,f(x) 是二次函數的關係。

在讓 E(𝜃) 最小的狀況下,找到的 \(𝜃_0, 𝜃_1​\) 就是最小平方法

最速下降法

剛剛要讓 E(𝜃) 最小的狀況下,必須不斷地找到不同的 \(𝜃_0, 𝜃_1​\),這個計算很麻煩,可用微分來解決,因為微分就是在找函數切線斜率的變化。

例如 \(f(x) = (x-1)^2\) ,微分後 \(f'(x) = 2x-2\)

x f'(x) 的正負 f(x) 遞增或遞減
\(x < 1\) \(-\) 遞減
\(x=0\) 0
\(x>1\) \(+\) 遞增

f(x) 的圖形如下,當 x 由 3 往 1 逼近,f(x) 就越來越小,另外當 x 由 -1 往 1 逼近,f(x) 也會越來越小

意思就是說,只要 x 往導函數 (微分) 的反方向移動,就函數值會往最小值移動。

最速下降法(梯度下降法) 就是定義為

\(x := x - 𝜂 \frac{d}{dx}f(x)​\)

以實際的數字為例,當 \(𝜂 = 1, x = 3​\),x 會在 3 與 -1 之間往返

\(x := 3-1(2*3-2) = -1​\)

\(x := -1-1(2*(-1)-2) = 3​\)

當 \(𝜂 = 0.1, x = 3\),x 會往最小值逼近

\(x := 3-0.1(2*3-2) = 2.6​\)

\(x := 2.6-0.1(2*2.6-2) = 2.3​\)

\(x := 2.3-0.1(2*2.3-2) = 2.1​\)

當 𝜂 越大,x 就會往返,當 𝜂 越小,x 會往最小值逼近


回到剛剛的 誤差函數

\({E(𝜃)= \frac{1}{2} \sum_{i=1}^{n}(y^{(i)} - f_𝜃(x^{(i)})^2 }\)

因為 \(f_𝜃(x^{(i)})\) 是 \({f(x)=𝜃_0+𝜃_1x}\) ,有兩個未知的參數 \(𝜃_0, 𝜃_1\) ,要改用偏微分找最小值。

\(𝜃_0 := 𝜃_0 - 𝜂 \frac{𝜕E}{𝜕𝜃_0}​\)

\(𝜃_1 := 𝜃_1 - 𝜂 \frac{𝜕E}{𝜕𝜃_1}​\)

因 E(𝜃) 裡面有 \(f_𝜃(x)​\),而 \(f_𝜃(x)​\) 裡面有 𝜃

\(u = E(𝜃)​\)

\(v = f_𝜃(x)​\)

然後用合成函數的方式,計算微分

\(\frac{𝜕E}{𝜕𝜃_0} = \frac{𝜕u}{𝜕v} \frac{𝜕v}{𝜕𝜃_0}​\)

其中,前面的部分

\( \frac{𝜕u}{𝜕v} = \frac{𝜕}{𝜕v}( \frac{1}{2} \sum_{i=1}^{n}(y^{(i)} - v)^2 ) = \frac{1}{2} \sum_{i=1}^{n}\frac{𝜕}{𝜕v}(y^{(i)} - v)^2 = \frac{1}{2}\sum_{i=1}^{n}( -2y^{(i)} +2v ) = \sum_{i=1}^{n}( v-y^{(i)} )\)

後面的部分

\(\frac{𝜕v}{𝜕𝜃_0} = \frac{𝜕}{𝜕𝜃_0}( 𝜃_0 + 𝜃_1x ) = 1​\)

所以

\(\frac{𝜕E}{𝜕𝜃_0} = \sum_{i=1}^{n}( f_𝜃(x^{(i)})-y^{(i)} )\)

另外對 \(𝜃_1​\) 微分,可得到

\(\frac{𝜕v}{𝜕𝜃_1} = \frac{𝜕}{𝜕𝜃_1}( 𝜃_0 + 𝜃_1x ) = x\)

\(\frac{𝜕E}{𝜕𝜃_1} = \sum_{i=1}^{n}( f_𝜃(x^{(i)} )-y^{(i)} )x^{(i)} \)


最後

\(𝜃_0 := 𝜃_0 - 𝜂 \sum_{i=1}^{n}( f_𝜃(x^{(i)} )-y^{(i)} )\)

\(𝜃_1 := 𝜃_1 - 𝜂 \sum_{i=1}^{n}( f_𝜃(x^{(i)} )-y^{(i)} )x^{(i)}​\)

用這個方法,就可以找出正確的 \(𝜃_0, 𝜃_1​\)

多項式回歸

一開始,我們假設數據的模型是線性的,所以使用一次函數,但也可能用二次或更高次的函數來定義 \(f_𝜃(x)​\),會更貼近原本的數據模型

\(f_𝜃(x) = 𝜃_0 + 𝜃_1x + 𝜃_2x^2​\)

\(f_𝜃(x) = 𝜃_0 + 𝜃_1x + 𝜃_2x^2 + \dots +𝜃_nx^n​\)

回到剛剛的問題,要對 \(𝜃_2​\) 進行偏微分

對 \(𝜃_1​\) 微分,可得到

\(\frac{𝜕v}{𝜕𝜃_2} = \frac{𝜕}{𝜕𝜃_2}( 𝜃_0 + 𝜃_1x +𝜃_2x^2 ) = x^2\)

\(\frac{𝜕E}{𝜕𝜃_2} = \sum_{i=1}^{n}( f_𝜃(x^{(i)} )- y^{(i)} )(x^{(i)} )^2\)

多元回歸

目前解決的問題,都只有一個變數 x,但大多數的問題,都是有兩個以上的變數。例如廣告的點擊率,可能會受廣告費、顯示位置、顯示大小 等原因影響。

\(f_𝜃(x_1, x_2, x_3)=𝜃_0+𝜃_1x_1+𝜃_2x_2+𝜃_3x_3\)

當變數有 n 個,可改用向量的方式表示

\(𝜃= \left[ \begin{matrix} 𝜃_0 \\ 𝜃_1 \\𝜃_2 \\. \\. \\𝜃_n \end{matrix} \right] x= \left[ \begin{matrix} x_1 \\ x_2 \\x_3 \\. \\. \\x_n \end{matrix} \right] ​\)

但因為 𝜃 跟 x 個數不同,不容易計算,就再加上一項 \(x_0 =1​\)

\(𝜃= \left[ \begin{matrix} 𝜃_0 \\ 𝜃_1 \\𝜃_2 \\. \\. \\𝜃_n \end{matrix} \right] x= \left[ \begin{matrix} x_0 \\ x_1 \\x_2 \\. \\. \\x_n \end{matrix} \right] \)

將 𝜃 變成轉置矩陣後,再跟 x 相乘,就會是剛剛的 \(f_𝜃(x)\)

\(𝜃^Tx = 𝜃_0x_0+𝜃_1x_1+ \dots + 𝜃_nx_n = f_𝜃(x) \)

變成向量後,再用剛剛合成函數偏微分的方法

\(u = E(𝜃)​\)

\(v = f_𝜃(x)​\)

\(\frac{𝜕u}{𝜕𝜃_j} =\frac{𝜕E}{𝜕𝜃_j} = \frac{𝜕u}{𝜕v} \frac{𝜕v}{𝜕𝜃_j}\)

前面的部分一樣,後面的部分

\(\frac{𝜕v}{𝜕𝜃_j} = \frac{𝜕}{𝜕𝜃_j}( 𝜃^Tx ) = \frac{𝜕}{𝜕𝜃_j}( 𝜃_0x_0+𝜃_1x_1+\dots+𝜃_nx_n )= x_j\)

第 j 項參數的定義為

\(𝜃_j := 𝜃_j - 𝜂 \sum_{i=1}^{n}( f_𝜃(x^{(i)} )-y^{(i)} )x_j^{(i)}\)

當變數增加,計算量變大,用最速下降法會導致計算速度變慢,可用隨機梯度下降法改進。

最速下降法除了有計算速度慢的問題,還有可能陷入局部解的問題,像以下的函數圖形中,不同的起點,可能會找到局部最小值。

隨機梯度下降法

在多元迴歸中,第 j 項參數的定義為

\(𝜃_j := 𝜃_j - 𝜂 \sum_{i=1}^{n}( f_𝜃(x^{(i)} )-y^{(i)} )x_j^{(i)}​\)

但因為用到所有的資料的誤差,計算量太大,隨機梯度下降法式隨機選擇一項學習資料,套用在參數的更新上,例如選擇第 k 項。

\(𝜃_j := 𝜃_j - 𝜂 ( f_𝜃(x^{(k)} )-y^{(k)} )x_j^{(k)}​\)

原本最速下降法用來更新一次參數的時間,隨機梯度下降法可更新 n 次參數。因為是隨機選擇學習資料,不會陷入局部解的問題。


另外也有隨機選擇 m 筆學習資料的方法,也稱為小量批次資料法,假設 m 筆資料的集合為 K

\(𝜃_j := 𝜃_j - 𝜂 \sum_{k𝜖K} ( f_𝜃(x^{(k)} )-y^{(k)} )x_j^{(k)}​\)

References

練好機器學習的基本功:用Python進行基礎數學理論的實作

2019年8月12日

erlang lager with date in log filename

erlang lager 預設是以設定中的 filename 加上 .1 .2 的 postfix 作為 logfile rotate 的依據,但通常在使用 logfile,會希望直接在 logfile 看到產生該 log 的日期,這時需要使用 Custom Log Rotation 的功能,自己撰寫 log_rotator。

首先我們先找到 lager 原始程式碼中預設的 lagerrotatordefault.erl,先複製成 mylagerlog_rotator,然後修改裡面的程式碼。

-module(my_lager_log_rotator).

-include_lib("kernel/include/file.hrl").

-behaviour(lager_rotator_behaviour).

-export([
  create_logfile/2, open_logfile/2, ensure_logfile/4, rotate_logfile/2
]).

create_logfile(Name, Buffer) ->
  {{Y, M, D}, {H, _, _}} = calendar:now_to_local_time(os:timestamp()),
  DateHour =  {Y, M, D, H},
  FileName = filename(Name, DateHour, 1),
  file:delete(Name),
  file:make_symlink(filename:absname(FileName), Name),
  open_logfile(Name, Buffer).

open_logfile(Name, Buffer) ->
  case filelib:ensure_dir(Name) of
    ok ->
      Options = [append, raw] ++
        case  Buffer of
          {Size, Interval} when is_integer(Interval), Interval >= 0, is_integer(Size), Size >= 0 ->
            [{delayed_write, Size, Interval}];
          _ -> []
        end,
      case file:open(Name, Options) of
        {ok, FD} ->
          case file:read_file_info(Name) of
            {ok, FInfo} ->
              Inode = FInfo#file_info.inode,
              {ok, {FD, Inode, FInfo#file_info.size}};
            X -> X
          end;
        Y -> Y
      end;
    Z -> Z
  end.


ensure_logfile(Name, FD, Inode, Buffer) ->
  case file:read_link(Name) of
    {ok, _} ->
      lager_ensure_logfile(Name, FD, Inode, Buffer);
    _ ->
      create_logfile(Name, Buffer)
  end.


lager_ensure_logfile(Name, undefined, _Inode, Buffer) ->
  open_logfile(Name, Buffer);
lager_ensure_logfile(Name, FD, Inode, Buffer) ->
  case file:read_file_info(Name) of
    {ok, FInfo} ->
      Inode2 = FInfo#file_info.inode,
      case Inode == Inode2 of
        true ->
          {ok, {FD, Inode, FInfo#file_info.size}};
        false ->
          %% delayed write can cause file:close not to do a close
          _ = file:close(FD),
          _ = file:close(FD),
          case open_logfile(Name, Buffer) of
            {ok, {FD2, Inode3, Size}} ->
              %% inode changed, file was probably moved and
              %% recreated
              {ok, {FD2, Inode3, Size}};
            Error ->
              Error
          end
      end;
    _ ->
      %% delayed write can cause file:close not to do a close
      _ = file:close(FD),
      _ = file:close(FD),
      case open_logfile(Name, Buffer) of
        {ok, {FD2, Inode3, Size}} ->
          %% file was removed
          {ok, {FD2, Inode3, Size}};
        Error ->
          Error
      end
  end.
%%
%%%% renames failing are OK
%%rotate_logfile(File, 0) ->
%%  %% open the file in write-only mode to truncate/create it
%%  case file:open(File, [write]) of
%%    {ok, FD} ->
%%      file:close(FD),
%%      ok;
%%    Error ->
%%      Error
%%  end;
%%rotate_logfile(File0, 1) ->
%%  File1 = File0 ++ ".0",
%%  _ = file:rename(File0, File1),
%%  rotate_logfile(File0, 0);
%%rotate_logfile(File0, Count) ->
%%  File1 = File0 ++ "." ++ integer_to_list(Count - 2),
%%  File2 = File0 ++ "." ++ integer_to_list(Count - 1),
%%  _ = file:rename(File1, File2),
%%  rotate_logfile(File0, Count - 1).
%%

rotate_logfile(Name, _Count) ->
  case file:read_link(Name) of
    {ok, LinkedName} ->
      case filelib:file_size(LinkedName) of
        0 ->
          %% if the files size is zero, it is removed
          catch file:delete(LinkedName);
        _ ->
          void
      end;
    _ ->
      void
  end,
  {ok, {FD, _, _}} = create_logfile(Name, []),
  file:close(FD).

%% @doc Create name of a new file
%% @private
filename(BaseFileName, DateHour, Branch) ->
  FileName = lists:append([BaseFileName,
    suffix(DateHour, false), ".", integer_to_list(Branch)
  ]),
  case filelib:is_file(FileName) of
    true ->
      filename(BaseFileName, DateHour, Branch + 1);
    _ ->
      FileName
  end.

%% @doc Zero-padding number
%% @private
zeropad(Num, MinLength) ->
  NumStr = integer_to_list(Num),
  zeropad_str(NumStr, MinLength - length(NumStr)).
zeropad_str(NumStr, Zeros) when Zeros > 0 ->
  zeropad_str([$0 | NumStr], Zeros - 1);
zeropad_str(NumStr, _) ->
  NumStr.

%% @doc Create a suffix
%% @private
suffix({Y, M, D, H}, WithHour) ->
  YS = zeropad(Y, 4),
  MS = zeropad(M, 2),
  DS = zeropad(D, 2),
  HS = zeropad(H, 2),
  case WithHour of
    true ->
      lists:flatten([$., YS, MS, DS, $., HS]);
    _ ->
      lists:flatten([$., YS, MS, DS])
  end.

將 mylagerlogrotator 套用在 lager 的設定檔的 lagerfile_backend 中,{rotator, my_lager_log_rotator}

[
  {lager, [
    {log_root, "./log"},
    {crash_log, "crash.log"},
    {error_logger_redirect, false},
    {colored, true},
    {colors, [
      {debug,     "\e[0;36m" },
      {info,      "\e[1;37m" },
      {notice,    "\e[1;36m" },
      {warning,   "\e[1;33m" },
      {error,     "\e[1;31m" },
      {critical,  "\e[1;35m" },
      {alert,     "\e[1;44m" },
      {emergency, "\e[1;41m" }
    ]},
    {handlers, [
      {lager_console_backend, [{level, debug}, {formatter, lager_default_formatter},
        {formatter_config, [date, " ", time, color, " ", pid, " ", module, ":", line, " [", severity, "] ", message, "\e[0m\n"]}]},
      {lager_file_backend, [{file, "debug.log"}, {level, debug}, {size, 3000}, {date, "$H00"}, {count, 2},
        {formatter_config, [date, " ", time, " ", pid, " ", module, ":", line, " [", severity, "] ", message, "\n"]}, {rotator, my_lager_log_rotator}]}
    ]}
  ]}
].

現在就會產生像這樣的 logfile

debug.log (symbolic link to debug.log.20190426.2)
debug.log.20190426.1
debug.log.20190426.2

目前還會需要調整的是設定檔中的 count,如果在 logfile 加上日期,count 應該是要代表保留幾天的資料,但目前還是依照 lager 原本的定義,為保留幾個同樣 prefix 檔名的 logfile。

References

erlang lager

leologgerrotator.erl

2019年8月5日

奧卡姆剃刀 Occam's Razor, Ockham's Razor

奧卡姆剃刀,是由14世紀邏輯學家、聖方濟各會修士奧卡姆的威廉(William of Occam,約1285年至1349年)提出。奧卡姆(Ockham)在英格蘭的薩裡郡,那是他出生的地方。他在《箴言書註》2捲15題說「切勿浪費較多東西,去做『用較少的東西,同樣可以做好的事情』。」後來這個原理也簡化為「如無必要,勿增實體。」(Do not multiply entities beyond necessity.)

另外一些更簡要的說法是:避重趨輕、避繁逐簡、以簡御繁、避虛就實,更白話的說法是「夠用就好」

奧卡姆剃刀並不是一個數學定理,他是一個思考的原則,這個原則告訴我們應該追求簡化,不追求完全正確的答案,選擇一個最簡單的方式,來解釋與面對問題。

剃刀原則並不是說簡單的理論就是正確的理論,應該了解為「當兩個假說具有完全相同的解釋力和預測力時,我們以那個較為簡單的假說作為討論依據。」

這個原則套用在不同知識領域時:

  1. 科學領域:當你有兩個處於競爭地位的理論能得出同樣的結論,那麼簡單的那個更好。
  2. 企業管理領域:在管理企業制定決策時,應該儘量把複雜的事情簡單化,剔除干擾,解決最根本的問題,才能讓企業保持正確的方向。
  3. 投資領域:面對複雜當投資市場,應把複雜事情簡單化,簡化自己的投資策略,擺脫那些消耗了大量金錢、時間、精力的事情。
  4. 組織結構:組織結構扁平化與組織結構非層級化已經成為企業組織變革的趨勢。員工之間的關係是平等的分工合作關係,基層員工被賦予更多的權力。由於員工的積極參與,組織目標與個人目標之間的矛盾得到最大程度地消除。
  5. 簡化文書流程:簡單的信息遠比複雜的信息更有利於人們的思考與決策。
  6. 日心說/地心說:五百年前,還不知道世界的中心是太陽還是地球,但利用望遠鏡得到的許多數據,可證明地球跟太陽都可能是中心,不過日心說只需要 7 個假設,而地心說需要更多假設,於是哥白尼在天體運行論中利用奧卡姆剃刀原則,判斷太陽才是中心。

跟人溝通時,如果遇到一些喜歡長篇大論的人,就會覺得講半天講不到重點,但講話太精簡,簡單到讓人聽不懂也是問題,精簡程度怎麼拿捏就是個人修養的問題。

References

奧卡姆剃刀定律

奧卡姆剃刀定律 wiki

奧卡姆剃刀決策原則

設計法則:奧卡姆剃刀原理