圓州率
🌐

Feature Image

機器學習介紹 (Machine Learning)

資料科學, 機器學習
機器學習的入門介紹。
   最後更新:

引言

封面是本次 k nearest-neighbor 的模擬實驗結果。

所謂「機器學習」是

  • 機器:即函數或演算法。
  • 學習:以資料 (data-based) 作為基礎而非規則 (rule-based)。

機器學習作為資料科學的一個重要工具,其步驟可被拆解為 OCEMIC (最後的 C 與 I 合併)。

  • 獲取 (Obtain)
  • 清理 (Clean)
  • 探索 (Explore)
  • 建模 (Model)
  • 解釋 (Interpret)

監督式學習 (Supervised Learning)

給定資料 (xi,yi)i=1n(\bs x_i, y_i)_{i = 1}^{n},目標是找出 x\bs xyy 的關聯,因此選定 fFf \in \mathcal F 在一個函數空間,並在最小化損失函數 LL 或風險 RR 的條件下找到最合適的 f(X)Yf (\bs X) \approx Y。可應用於預測。

例如手寫數字辨識,給定 xRp×p\bs x \in \bb R^{p \times p} 是像素資料,y{0,1,2,,9}y \in \left \lbrace \contio{9} \right\rbrace,利用 if else 的規則來執行顯然不切實際,此時轉用基於資料 (x,y)(\bs x, y) 的方式,找出可行的 ff

非監督式學習 (Unsupervised Learning)

給定資料 (xi)i=1n(\bs x_i)_{i = 1}^{n},其中 xi=(x_i1,x_i2,,x_ip)Rp\bs x_i = (x\_{i1}, x\_{i2}, \cdots, x\_{ip}) \in \bb R^p,目標是找出 xx 之間的關聯,例如線性關係或是聚類。應用於推薦系統。

半監督式學習 (Semi-supervised Learning)

相較於監督式學習,半監督式學習的資料僅有部分的給定 yy,例如上述的手寫數字辨識,可能基於成本使得僅有部分的資料有標記 yy,因此更需要靠非監督式的方式找出 x\bs x 之間的關聯性,並用部分標記 yy 的資料建立監督式學習模型。

建模類型

監督式學習目標在建構一個合適的 ff,一般分為兩種類別,分別對應 ff 對資料的敏感度。首先將資料分成兩部分,一部分用於訓練 (train) 模型,另一部分用於測試 (test) 模型的準確度。

全局 (Global)

以回歸 (regression) 作為代表,例如線性回歸對於新資料的預測,是 X\bs X 的線性組合,寫作

Y^(X)=β0+j=1pβjXj \hat Y (\bs X) = \beta_0 + \sum_{j = 1}^{p} \beta_j X_j

在這系列能找到更多深入的討論。回歸模型往往會帶有較強的資料假設,例如常態假設 (normal assumption)

Yi=β0+j=1pβjXij+εi Y_i = \beta_0 + \sum_{j = 1}^{p} \beta_j X_{ij} + \varepsilon_i

其中 εii.i.d.N(0,σ2)\varepsilon_i \iid N (0, \sigma^2),對整體趨勢能較好的描述,但容易被偏離值 (outlier) 影響。

局部 (Local)

以近鄰演算法 (nearest-neighbor, knn) 作為代表,對於新資料的預測,會根據其 xx 最近的 kk 個點所對應的 yy 數值的平均 (或眾數),稱為 kk-近鄰演算法 (kk-nearest neighbor),定義為

Y^(x)=1kxiNk(x)yi \hat Y (x) = \frac{1}{k} \sum_{x_i \in N_k (x)} y_i

其中的 Nk(x)N_k (x) 是距離 xx 最近的 kkxix_i。或者是找尋給定半徑的資料點平均 (或眾數)

Y^(x)=1kxiBr(x)yi \hat Y (x) = \frac{1}{k} \sum_{x_i \in B_r (x)} y_i

其中的 Br(x)={z:zxr}B_r (x) = \left\lbrace z : |z - x| \leq r \right\rbrace,即以 xx 為中心,半徑為 rr 的圓。最後,由於 Y{0,1}Y \in \left\lbrace 0, 1 \right\rbrace,會透過一個轉換函數轉換輸出數值

G^(x)=I(Y^(x)>0.5) \hat G (x) = I (\hat Y (x) > 0.5)

而局部模型一般帶有較弱的資料假設,例如「相近的 xx,帶有相近的 yy」,因此即使資料在局部有較多的變化,nearest-neighbor 也能較為合適的貼近;相對的,卻很難描述出全局的資料變動趨勢。

全局 vs 局部

全局局部
強項整體趨勢局部特徵
弱點受偏離值 (outlier) 影響過度擬合 (overfitting)
特徵高 bias / 精度低
(yyy^\hat y 差距大)
高 variance / 不穩定
(train 和 test 精確度差距大)
解釋性

接下來會給出兩個範例,每個資料 (X1,X2,Y)(X_1, X_2, Y) 其中 Y{0,1}Y \in \left\lbrace 0, 1 \right\rbrace。train 有 200 點,test 有 10000 點。

  • 範例一:資料來自 2 個獨立且不同中心的二維常態分佈,每組資料 100 點,第 1 組資料的 YY 標記為 0,第 2 組資料的 YY 標記為 1。
  • 範例二:資料來自 20 個獨立且不同中心的二維常態分佈,每組資料 10 點,前 5 組資料的 YY 標記為 0,後 5 組資料的 YY 標記為 1。

範例一

給定

  • X1,X2,,X100i.i.d.N2((0,0)T,I) X_1, X_2, \cdots, X_{100} \iid N_2 ((0, 0)^T, I)
  • X101,X102,,X200i.i.d.N2((1,1)T,I) X_{101}, X_{102}, \cdots, X_{200} \iid N_2 ((1, 1)^T, I)

yi={0if 1i1001if 101i200 y_i = \begin{cases} 0 & \text{if } 1 \leq i \leq 100 \\ 1 & \text{if } 101 \leq i \leq 200 \end{cases}

紅點標記為 y=0y = 0,綠點標記為 y=1y = 1

繪製出 linear regression f(x)=0.5f (\bs x) = 0.5 的區域,其 train 與 test 準確度均約為 76%。

對這筆資料進行 nearest-neighbor。

能看到隨著 kk 增加,分區線逐漸變的平滑。

在這筆資料下的比對,能看出 regression 是非常穩定的,test 和 train 變化量並不大,而 knn 表現也逐漸趨於一致。

範例一 R 語言

給出範例一的程式。載入 packages

library(dplyr)
library(tidyverse)
library(MASS)
R

分別生成出 traintest

# generate data 
n = 200 # train size 
m = 10000 # test size 
mu.0 = c(0, 0) # center of y 0
mu.1 = c(1, 1) # center of y 1
Sigma = matrix(c(1, 0, 0, 1), nrow = 2) # variance 
set.seed(0) # random seed

# calculate
n.2 = n / 2
m.2 = m / 2
train = mvrnorm(n.2, mu = mu.0, Sigma = Sigma) %>% 
  rbind(mvrnorm(n.2, mu = mu.1, Sigma = Sigma)) %>% 
  cbind(y = c(rep(0, n.2), rep(1, n.2))) %>% 
  data.frame() %>% 
  rename(x1 = V1, x2 = V2)
test = mvrnorm(m.2, mu = mu.0, Sigma = Sigma) %>% 
  rbind(mvrnorm(m.2, mu = mu.1, Sigma = Sigma)) %>% 
  cbind(y = c(rep(0, m.2), rep(1, m.2))) %>% 
  data.frame() %>% 
  rename(x1 = V1, x2 = V2)
background = expand.grid(seq(-5, 5, length.out = 100), 
                         seq(-5, 5, length.out = 100)) %>%
  rename(x1 = Var1, x2 = Var2)

# plot
par(pty = "s")
plot(train$x1, train$x2, col = train$y + 2, xlab = "x1", ylab = "x2")
R

linear regression 預測。

# linear regression
linear = lm(y ~ x1 + x2, train)
background$y = sapply(predict(linear, background), function(x){ if (x > 0.5) 1 else 0})

par(pty = "s")
plot(train$x1, train$x2, col = train$y + 2, xlab = "x1", ylab = "x2", main = "Linear Regression")
points(background$x1, background$x2, col = background$y + 2, pch = 19, cex = 0.1)
abline(a = (0.5 - linear$coefficients[1]) / linear$coefficients[3], 
       b = -linear$coefficients[2] / linear$coefficients[3])

# accuracy
linear.train = sapply(predict(linear, train), function(x){ if (x > 0.5) 1 else 0})
linear.train.acc = sum(linear.train == train$y) / n
linear.test = sapply(predict(linear, test), function(x){ if (x > 0.5) 1 else 0})
linear.test.acc = sum(linear.test == test$y) / m
R

knn 預測

# Nearest-Neighbor
library(FNN)

max.k = 150 # max k-near
train.acc = rep(NA, max.k)
test.acc = rep(NA, max.k)

for(k in 1:max.k){ 
  if (k %% 10 == 0){ print(k) }
  
  # calculate 
  background.knn = knn.reg(train = train[, 1:2], test = background[, 1:2], y = train[, 3], k = k)
  background$y = sapply(background.knn$pred, function(x){ if (x > 0.5) 1 else 0})
  train.knn = knn.reg(train = train[, 1:2], test = train[, 1:2], y = train[, 3], k = k)
  train.pred = sapply(train.knn$pred, function(x){ if (x > 0.5) 1 else 0})
  train.acc[k] = sum(train.pred == train$y) / n
  test.knn = knn.reg(train = train[, 1:2], test = test[, 1:2], y = train[, 3], k = k)
  test.pred = sapply(test.knn$pred, function(x){ if (x > 0.5) 1 else 0})
  test.acc[k] = sum(test.pred == test$y) / m
  
  # plot nearest-Neighbor
  par(mfrow = c(1, 2))
  plot(train$x1, train$x2, col = train$y + 2, xlab = "x1", ylab = "x2", main = paste0(k, "-nearest-Neighbor"))
  points(background$x1, background$x2, col = background$y + 2, pch = 19, cex = 0.1)
  
  # plot black dots
  for(i in 1:100){
    for(j in 1:99){
      if(background[(i - 1) * 100 + j, 3] != background[(i - 1) * 100 + j + 1, 3]){
        points(background[(i - 1) * 100 + j, 1:2], pch = 19, cex = 0.5)
      }
      if(background[(j - 1) * 100 + i, 3] != background[j * 100 + i, 3]){
        points(background[(j - 1) * 100 + i, 1:2], pch = 19, cex = 0.5)
      }
    }
  }
  
  # plot Accuracy
  plot(train.acc, ylim = c(0, 1), xlab = "k", ylab = "Accuracy", type = "l", col = "red")
  points(test.acc, type = "l", col = "blue")
  legend("bottomright", 
         c(paste0("train, size = ", n), 
           paste0("test, size = ", m)), 
         col = c("red", "blue"), 
         lty = 1)
}
R

對比圖

# compare linear and knn
plot(train.acc, ylim = c(0, 1), xlab = "k", ylab = "Accuracy", type = "l", col = "red")
points(test.acc, type = "l", col = "blue")
abline(h = linear.train.acc, col = rgb(1, 0, 0, alpha = 0.5))
abline(h = linear.test.acc, col = rgb(0, 0, 1, alpha = 0.5))
legend("bottomright", 
       c(paste0("linear train, size = ", n), 
         paste0("linear test, size = ", m), 
         paste0("knn train, size = ", n), 
         paste0("knn test, size = ", m)), 
       col = c(rgb(1, 0, 0, alpha = 0.5), rgb(0, 0, 1, alpha = 0.5), "red", "blue"), 
       lty = 1)
R

範例二

隨機生成資料中心點

  • 組別一:m1,m2,,m10i.i.d.N2((1,0)T,I) m_{1}, m_{2}, \cdots, m_{10} \iid N_2 ((1, 0)^T, I)
  • 組別二:m11,m12,,m20i.i.d.N2((0,1)T,I) m_{11}, m_{12}, \cdots, m_{20} \iid N_2 ((0, 1)^T, I)

每組再生成 100 筆資料,每筆資料有 1/101 / 10 的機率選擇組內 mkm_k 為中心,生成一個 N2(mk,I/5)N_2 (m_k, I / 5),第一組資料的 yy 標記為 0 (紅色),第二組資料的 yy 標記為 1 (綠色),圖中的實心點為 m1,m2,,m20\contia{m}{20}

其 knn 的表現出

  • train: 錯誤率隨著 kk 在 15010 的區間減少,kk 在 101 時的錯誤率減少的速度又更快。
  • test: 錯誤率隨著 kk 在 15010 的區間減少,但 kk 在 101 時的錯誤率又直線上升。

kk 做 log 轉換

  1. kk 太小 (k<10k < 10) 會 over fitting
  2. kk 在中間 (7<k<677 < k < 67) 表現較佳
  3. 從自由度 N/kN / k 的角度更能看出變化趨勢。

範例二 R 語言

加載 package

library(dplyr)
library(tidyverse)
library(MASS)
R

生成資料點的函式

# generate data
oracle = function(n, rand.seed = 1){
  # oracle means
  mu.0 = c(1, 0)
  mu.1 = c(0, 1)
  
  # class means
  set.seed(0)
  means.0 = mvrnorm(n = 10, mu = mu.0, Sigma = diag(2))
  means.1 = mvrnorm(n = 10, mu = mu.1, Sigma = diag(2))
  
  # observation
  set.seed(rand.seed)
  data.0 = sapply(1:n, function(i){
    mvrnorm(1, mu = means.0[sample(1:10, 1), ], Sigma = diag(2) / 5) 
  }) %>% t() %>% data.frame() %>% cbind(y = 0)
  data.1 = sapply(1:n, function(i){
    mvrnorm(1, mu = means.1[sample(1:10, 1), ], Sigma = diag(2) / 5) 
  }) %>% t() %>% data.frame() %>% cbind(y = 1)
  
  data = rbind(data.0, data.1)
}
R

設定 sample size

# sample size for 1 class
train.size = 100
test.size = 5000

train = oracle(train.size)
test = oracle(test.size, rand.seed = 1234)

par(pty = 's')
plot(train[, 1:2], col = train$y + 2)
R

執行 knn

# knn package
library(FNN)

max.k = 151 # max k-near
train.err = rep(NA, max.k)
test.err = rep(NA, max.k)

# calculate train and test error of each k
for(k in 1:max.k){ 
  if (k %% 10 == 0){print(k)}
  
  # for train part 
  train.knn = knn.reg(train = train[, 1:2], test = train[, 1:2], y = train[, 3], k = k)
  train.pred = ifelse(train.knn$pred > 0.5, 1, 0)
  train.err[k] = sum(train.pred != train$y) / train.size
  
  # for test part 
  test.knn = knn.reg(train = train[, 1:2], test = test[, 1:2], y = train[, 3], k = k)
  test.pred = ifelse(test.knn$pred > 0.5, 1, 0)
  test.err[k] = sum(test.pred != test$y) / test.size
}
R

畫出原始的 error 圖

# plot error
plot(train.err, ylim = c(0, 1), xlim = c(max.k, 1), 
     xlab = "k", ylab = "Error", 
     type = "l", col = "blue")
points(test.err, type = "l", col = "red")
legend("bottomleft", 
       c(paste0("train, size = ", train.size * 2), 
         paste0("test, size = ", test.size * 2)), 
       col = c("blue", "red"), 
       lty = 1)
R

log10\log_{10} 的轉換 error 圖

# plot error with transformation
library(ggplot2)

# plot breaks
Nk = c(1, 2, 3, 5, 8, 12, 18, 29, 40, 67, 200)
ks = c(151, 101, 67, 41, 25, 17, 11, 7, 5, 3, 1)

plot.err = data.frame(x = Nk, train = train.err[ks], test = test.err[ks])
ggplot(plot.err, aes(x = x)) + 
  geom_line(aes(y = train, color = "Train")) + 
  geom_point(aes(y = train, color = "Train")) + 
  geom_line(aes(y = test, color = "Test")) + 
  geom_point(aes(y = test, color = "Test")) + 
  scale_x_continuous(breaks = Nk, trans = "log10", 
                     sec.axis = sec_axis(~200/., breaks = ks, name = "k") ) +
  labs(x = "N / k", y = "Error") + 
  scale_color_manual(values = c("red", "blue"), name = "Legend", labels = c("Test", "Train")) +
  theme(legend.position = c(0.1, 0.1)) # 圖例位置
R

沒有白吃的午餐定理 (No Free Lunch Theorem)

下面兩張圖分別是對應不同資料集的模型精確度,能看出圖一選擇線性回歸,圖二選擇小 kk 的近鄰演算法可能是較好的選項。

上述案例中能看出,沒有一種機器學習能夠在所有問題上表現最好,又稱為 沒有白吃的午餐定理 (No Free Lunch Theorem),可以想像成沒有一種午餐能滿足所有人。因此機器學習沒有通用的最佳解,需要根據問題與資料選模型。

統計決策理論 (Statistical Decision Theory)

決策理論告訴我們「一個模型能達到的最好程度」。舉例來說,若母體取樣來自各半X1Unif(0,2)X_1 \sim \unif (0, 2)X2Unif(1,3)X_2 \sim \unif (1, 3)。若採樣的 X(0,1)X \in (0, 1),則 XX 可以保證來自 X1X_1;相似的,若採樣的 X(2,3)X \in (2, 3),則 XX 可以保證來自 X2X_2;若 X(1,3)X \in (1, 3),則說 XX 來自 X1X_1X2X_2 都僅有一半的正確率,因此在此問題下能達到的最好正確率只有75%。實際問題中,不知道背後真實的母體分配,因此會靠重抽 (boosting) 等方式來模擬母體的分配。

現假設 YY 是連續型變數,要刻劃模型的適配程度,首先定義 loss function (損失函數)

L(Y,f(X))=L(Y,Y^) L (Y, f (X)) = L (Y, \hat Y)

目標是要最小化損失,因此定義 expected (squared) prediction error,同時也是 Bayes risk

EPE(f)=EY[L(Y,f(X))]=EXEYX[L(Y,f(X))X] \begin{align*} EPE (f) & = E_Y [L (Y, f (X))] \newline & = E_X E_{Y | X} [L (Y, f (X)) | X] \end{align*}

而在 squared loss function L(Y,f(X))=(Yf(X))2L (Y, f (X)) = (Y - f(X))^2 底下,最小化 EPE 可以寫成

f^(x)=arg mincREYX[(Yc)2X=x]=E(YX=x) \begin{align*} \hat f (x) & = \argmin_{c \in \bb R} E_{Y | X} [(Y - c)^2 | X = x] \newline & = E (Y | X = x) \end{align*}

這樣的 f^\hat f 也被稱之為 regression function (回歸函數),意指在給定 X=xX = x 條件下,YY 最有可能的數值。而在 YGY \in \mathcal G 是類別型變數時,則會改取眾數

G^(x)=arg maxgGP(gX=x) \begin{align*} \hat G (x) & = \argmax_{g \in \mathcal G} P (g | X = x) \end{align*}

參考資料