感知器

Table of Contents

Hits

1. 感知器(Perceptron)

1.1. 何謂感知器

Perceptron is a single layer neural network and a multi-layer perceptron is called Neural Networks(神經網路).

收到多個輸入訊號之後,再當作一個訊號輸出,如圖1所示,\(x_1, x_2\)為輸入訊號,\(y\)為輸出訊號,\(w_1, w_2\)代表權重(weight),圖中的圓圈稱為「神經元」或稱作「節點」。神經元\(x_1, x_2\)的訊號是否會觸發神經元\(y\)使其輸出訊號則取決於\(w_1x_1+w_2x_2\)是否會超過某個臨界值\(\theta\)。

sensor1.png

Figure 1: 收到兩組輸入訊號的感知器

若以算式表示此一觸發條件則如公式\eqref{org9727f40}所示。

\begin{equation} \label{org9727f40} y = \begin{cases} 0, & w_1x_1+w_2x_2 \leq 0 \\ 1, & w_1x_1 + w_2x_2 > 0 \end{cases} \end{equation}

1.2. 感知器工作原理

1.2.1. Version #1: 使用weight

感知器(perceptron)是人造神經元(artificial neuron)的一種,也是最基本的一種。它接受一些輸入,產生一個輸出。

perceptron-1.png 所謂「線性」,簡單來說就是「用一條直線(在二維平面上)或一個平面(在三維空間中)來切分資料」。如果資料可以被一條直線完美地分成兩群,就稱為「線性可分」。這種架構有以下限制:

  • 這種架構的輸入/輸出關係為線性
  • 神經網路中再多的線性perceptron疊加,結果等同於一條線,仍為線性
  • 因此無法解決 線性不可分 的問題(也就是無法處理一條直線分不開的資料)
線性可分 v.s. 線性不可分

2021-05-23_14-10-30.jpg

Figure 2: 線性和非線性分類

在圖2中,A為線性分類問題——就像一條馬路把兩區住戶分開,我們可以找到一條直線將兩類資料完美切開。B則為非線性分類問題——就像兩群人混雜在一起住,不管你怎麼畫,都找不到任何一條直線能把他們完全分開。對於B這類問題,必須使用非線性的核函數和其他非線性的分類算法和技術才能成功分類1

低維映射至高維

可以透過一個非線性的映射將低維空間線性不可分的樣本轉換至高維空間,使其成為線性可分2,例如:

2021-05-23_14-14-31.jpg

Figure 3: Kernel function mapping

1.2.2. Version #2: 加入bias

perceptron-2.png

Figure 4: Perceptron version 2

  • 不加 bias 你的分類線(面)就必須過原點,這顯然是不靈活的
  • 透過bias,可以將NN進行左右調整,以適應(fit)更多情況
  • 可以將bias視為一個activate perceptron的threshold
  • bias也可以視為當輸入均為0時的輸出值
  • 從仿生學的角度,刺激生物神經元使它興奮需要刺激強度超過一定的閾值,同樣神經元模型也仿照這點設置了bias

1.2.3. Version #3: 加入activation function

加入activation function,至於為什麼要加這個,請直接看下一節的Story of gate。

perceptron-3.png

Figure 5: Perceptron version 3

1.3. 能做什麼

目前為止,感知器看起來就是個函數,有輸入、有處理、有輸出,這和AI有什麼關係呢?感知器能解決什麼問題?

感知器可以做最簡單的 二元分類 ——也就是把資料分成兩群。例如,給定輸入後判斷「是」或「否」、「通過」或「不通過」。最經典的例子就是用感知器來實現邏輯閘(AND、OR、NAND),這些邏輯閘的輸出只有 0 和 1,正好就是一種二元分類問題。接下來我們就用邏輯閘來實際體驗感知器的威力!

2. Story of gate: From perceptron to MLP

接下來我們用最簡單的邏輯閘來示範感知器的實際運作——邏輯閘的輸入只有 0 和 1,非常適合驗證感知器是否能正確分類。

2.1. OR gate實作

6中有四筆資料,有三個A、一個B,如何進行分類?

2021-05-24_00-48-56.jpg

Figure 6: 分類任務:問題

2.1.1. 想法

最簡單的分類方式是在A和B中間直接找條直線(\(w_1x_1+w_2x_2+b=0\))就可以將A和B完整切出兩個區塊,然後再搭配階梯函數(step function)將>0與<=0分別設為1與0,用來代表類別0與1。該直線方程式如下:

\begin{equation} \label{orgafca07b} y = \begin{cases} 1, & w_1x_1 + w_2x_2-b>0 \\ 0, & w_1x_1 + w_2x_2-b\leq0 \\ \end{cases} \end{equation}

2.1.2. Solution

經過無數的嚐試錯誤,也許我們可以矇到一個如下的方程式

2021-05-24_00-50-07.jpg

Figure 7: 分類任務:Solution

如果畫成Perceptron(version 3)的圖:

orGatePerceptron.png

Figure 8: Perceptron presentation

如果將圖7的四個點代入y(方程式\eqref{orgafca07b}):

\begin{align*} A(0,1) \rightarrow y &= f(0,1) = f(1\times0+1\times1-0.5) = f(0.5) = 1 \\ A(1,0) \rightarrow y &= f(1,0) = f(1\times1+1\times0-0.5) = f(0.5) = 1\\ A(1,1) \rightarrow y &= f(1,1) = f(1\times1+1\times1-0.5) = f(1.5) = 1\\ B(0,0) \rightarrow y &= f(0,0) = f(1\times0+1\times0-0.5) = f(-0.5) = 0\\ \end{align*}

2.1.3. OR gate

有點計概基礎的同學,應該可以發現圖6與OR邏輯閘一致,

2024-03-15_19-39-31_2024-03-15_19-38-17.png

Figure 9: OR邏輯閘

其真值表如下

A B A OR B
0 0 0
0 1 1
1 0 1
1 1 1

2.1.4. Python實作

上述 OR gate的python實作如下

 1: import numpy as np
 2: 
 3: def step_function(x):
 4:     return np.array(x >= 0, int)
 5: 
 6: def OR(x1, x2):
 7:     x = np.array([x1, x2])
 8:     w = np.array([1, 1])
 9:     b = -0.5
10:     y = np.sum(w*x) + b
11:     return step_function(y)
12: 
13: print("0 OR 0 -> ", OR(0,0))
14: print("0 OR 1 -> ", OR(0,1))
15: print("1 OR 0 -> ", OR(1,0))
16: print("1 OR 1 -> ", OR(1,1))
0 OR 0 ->  0
0 OR 1 ->  1
1 OR 0 ->  1
1 OR 1 ->  1

2.2. [課堂作業]AND、NAND Gate實作   TNFSH

上述範例中,我們以瞎貓精神找出了一組solution解決了OR gate的分類問題,請比照辦理,以Python實作出以下兩個gate: AND, NAND。

2.2.1. AND

已知AND gate真值表如下

A B A AND B
0 0 0
0 1 0
1 0 0
1 1 1

請仿照上面 OR gate 的程式碼,修改 wb 來實作 AND gate。

提示:AND 要求兩個輸入都是 1 才輸出 1,所以門檻要比 OR 高。

 1: import numpy as np
 2: 
 3: def step_function(x):
 4:     return np.array(x >= 0, dtype=int)
 5: 
 6: def AND(x1, x2):
 7:     x = np.array([x1, x2])
 8:     w = np.array([___, ___])  # 請填入權重
 9:     b = ___                     # 請填入偏移值
10:     y = np.sum(w*x) + b
11:     return step_function(y)
12: 
13: print("0 AND 0 -> ", AND(0,0))
14: print("0 AND 1 -> ", AND(0,1))
15: print("1 AND 0 -> ", AND(1,0))
16: print("1 AND 1 -> ", AND(1,1))

正確答案(用來驗證):

0 AND 0 ->  0
0 AND 1 ->  0
1 AND 0 ->  0
1 AND 1 ->  1

一組可行的答案:w=[0.5, 0.5], b=-0.7

2.2.2. NAND

已知NAND gate真值表如下

A B A NAND B
0 0 1
0 1 1
1 0 1
1 1 0

請把 AND gate 的權重和偏移值「全部反過來」試試看。

 1: import numpy as np
 2: 
 3: def step_function(x):
 4:     return np.array(x >= 0, dtype=int)
 5: 
 6: def NAND(x1, x2):
 7:     x = np.array([x1, x2])
 8:     w = np.array([___, ___])  # 請填入權重
 9:     b = ___
10:     y = np.sum(w*x) + b
11:     return step_function(y)
12: 
13: print("0 NAND 0 -> ", NAND(0,0))
14: print("0 NAND 1 -> ", NAND(0,1))
15: print("1 NAND 0 -> ", NAND(1,0))
16: print("1 NAND 1 -> ", NAND(1,1))

正確答案(用來驗證):

0 NAND 0 ->  1
0 NAND 1 ->  1
1 NAND 0 ->  1
1 NAND 1 ->  0

一組可行的答案:w=[-0.5, -0.5], b=0.7(和 AND 完全相反!)

3. 單層感知器的極限與多層感知器: XOR gate

XOR(互斥或,Exclusive OR)是電腦科學中的一種基本邏輯運算:當兩個輸入「不同」時輸出 1,「相同」時輸出 0。看起來很簡單,但它有一個致命的特性——XOR 的四個資料點無法被任何一條直線分開。這正是單層感知器的致命弱點,也是推動多層感知器(MLP)發展的關鍵問題。

XOR真值表如下:

A B A XOR B
0 0 0
0 1 1
1 0 1
1 1 0

其輸入/輸出分佈圖為

2021-05-24_14-15-53.jpg

Figure 10: XOR Gate

到目前為止,透過權重及偏權值可以設計 AND、NAND、OR, 但無法完成 XOR。顯然,若要再以感知器來模擬其運作原理,單層感知器已不敷使用。

3.1. 想法

這個時候一般線性的分類就沒有辦法很完美分割(如下圖),所以就需要一些變形的方法來達到目的。

2021-05-24_14-18-51.jpg

Figure 11: XOR Gate Solution ideas

一條線分不開?那就用兩條!這就是多層感知器的核心概念——當一個感知器(一條線)無法解決問題時,我們可以用多個感知器(多條線)組合起來解決。

由XOR的電路實作(如圖12)我們也可以發現同樣的原理。

xor.jpg

Figure 12: XOR 邏輯閘的組合

3.2. 多層感知器(MLP): XOR gate 實作

參考圖12的架構,我們可以藉由增加感知器的層數來實現 XOR 的功能,其結構如圖13所示。

2LayerSensor.png

Figure 13: 多層感知器模擬 XOR 邏輯閘

3.2.1. Solution

2021-05-24_14-28-04.jpg

Figure 14: XOR Gate Solution: (1)

如前所述,一條線為一個perceptron,這裡會用到兩個

  • \(h_1(x) = x_1 + x_2 - 0.5\)
  • \(h_2(x) = x_1 + x_2 - 1.5\)

2021-05-24_14-36-07.jpg

Figure 15: XOR Gate Solution: (2)

將圖11的4個點代入\(h_1\):

\begin{align*} h_1(0,0) &= f(1\times0+1\times0-0.5) = f(-0.5) = 0\\ h_1(0,1) &= f(1\times0+1\times1-0.5) = f(0.5) = 1\\ h_1(1,0) &= f(1\times1+1\times0-0.5) = f(0.5) = 1\\ h_1(1,1) &= f(1\times1+1\times1-0.5) = f(1.5) = 1\\ \end{align*}

將圖11的4個點代入\(h_2\):

\begin{align*} h_2(0,0) &= f(1\times0+1\times0-1.5) = f(-1.5) = 0\\ h_2(0,1) &= f(1\times0+1\times1-1.5) = f(-0.5) = 0\\ h_2(1,0) &= f(1\times1+1\times0-1.5) = f(-0.5) = 0\\ h_2(1,1) &= f(1\times1+1\times1-1.5) = f(0.5) = 1\\ \end{align*}

由上可知:

  • (0, 0)
    • (0, 0)帶入第1個perceptron \(h_1(0,0)\)輸出-0.5
    • (0, 0)帶入第2個perceptron \(h_2(0,0)\)輸出-1.5
    • (-0.5, -1.5)再經由step function轉換輸出(0,0)
  • (0, 1)
    • (0, 1)帶入第1個perceptron \(h_1(0,1)\)輸出0.5
    • (0, 1)帶入第2個perceptron \(h_2(0,1)\)輸出-0.5
    • (0.5, -0.5)再經由step function轉換輸出(1,0)
  • (1, 0)
    • (1, 0)帶入第1個perceptron \(h_1(1,0)\)輸出0.5
    • (1, 0)帶入第2個perceptron \(h_2(1,0)\)輸出-0.5
    • (0.5, -0.5)再經由step function轉換輸出(1,0)
  • (1, 1)
    • (1, 1)帶入第1個perceptron \(h_1(1,1)\)輸出1.5
    • (1, 1)帶入第2個perceptron \(h_2(1,1)\)輸出0.5
    • (1.5, 0.5)再經由step function轉換輸出(1,1)

\begin{align*} data(0,0) &= f(h_1,h_2) = (0,0) \\ data(0,1) &= f(h_1,h_2) = (1,0) \\ data(1,0) &= f(h_1,h_2) = (1,0) \\ data(1,1) &= f(h_1,h_2) = (1,1) \\ \end{align*}

觀察轉換後的結果:原本在 \((x_1, x_2)\) 空間中,(0,0) 和 (1,1) 輸出為 0、(0,1) 和 (1,0) 輸出為 1,這四個點無法用一條直線分開。但經過隱藏層轉換到 \((h_1, h_2)\) 空間後,(0,0) 映射到 (0,0)、(0,1) 和 (1,0) 都映射到 (1,0)、(1,1) 映射到 (1,1)——此時這些點變成可以用一條直線分開了!這就是多層感知器的威力。

這相當於透過兩個perceptron將原本的輸入做特徵空間轉換,如圖16:

2021-05-24_16-12-36.jpg

Figure 16: XOR Gate Solution: (3)

這個時候只要設計一個線性分類器就可以完美分割兩類的資料了阿,如圖17:

2021-05-24_16-13-44.jpg

Figure 17: XOR Gate Solution: (4)

XOR問題的神經網路結構如下圖:

2021-05-24_16-15-02.jpg

Figure 18: XOR Gate Solution: (5)

至於其實作程式碼則如下。

 1: #python code for XOR gate simulation
 2: import numpy as np
 3: 
 4: def step_function(x):
 5:     return np.array(x >= 0, dtype=int)
 6: 
 7: def AND(x1, x2):
 8:     x = np.array([x1, x2])
 9:     w = np.array([0.5, 0.5])
10:     b = -0.7
11:     return step_function(np.sum(w*x) + b)
12: 
13: def OR(x1, x2):
14:     x = np.array([x1, x2])
15:     w = np.array([0.5, 0.5])
16:     b = -0.2
17:     return step_function(np.sum(w*x) + b)
18: 
19: def NAND(x1, x2):
20:     x = np.array([x1, x2])
21:     w = np.array([-0.5, -0.5])
22:     b = 0.7
23:     return step_function(np.sum(w*x) + b)
24: 
25: def XOR(x1, x2):
26:     s1 = NAND(x1, x2)
27:     s2 = OR(x1, x2)
28:     y = AND(s1, s2)
29:     return y
30: 
31: print("XOR(0,0): ", XOR(0,0))
32: print("XOR(0,1): ", XOR(0,1))
33: print("XOR(1,0): ", XOR(1,0))
34: print("XOR(1,1): ", XOR(1,1))
XOR(0,0):  0
XOR(0,1):  1
XOR(1,0):  1
XOR(1,1):  0

4. 課堂練習   TNFSH

4.1. 練習一:AI 預測你上課會不會睡著

老師受夠了在台上講課看到一片屍體的景象。他決定在每堂課前用感知器預測誰會睡著,然後把那些人叫到第一排坐(或者直接潑水,看老師心情)。

他偷偷蒐集了 8 位同學的資料:

同學 昨晚睡幾小時 今天喝幾杯咖啡 上課狀態
阿明 8 2 清醒
小美 4 0 睡著
大雄 7 1 清醒
胖虎 3 1 睡著
靜香 6 2 清醒
小夫 5 0 睡著
阿福 4 2 清醒
魯夫 3 0 睡著

4.1.1. 範例:先用最簡單的方法試試

最直覺的做法:只看睡眠時數,忽略咖啡。如果睡超過 5.5 小時就算清醒。

 1: import numpy as np
 2: 
 3: def step_function(x):
 4:     return np.array(x >= 0, dtype=int)
 5: 
 6: # 同學資料 [昨晚睡幾小時, 今天咖啡幾杯]
 7: X = np.array([[8,2], [4,0], [7,1], [3,1],
 8:               [6,2], [5,0], [4,2], [3,0]])
 9: # 標籤: 清醒=1, 睡著=0
10: y = np.array([1, 0, 1, 0, 1, 0, 1, 0])
11: names = ['阿明','小美','大雄','胖虎','靜香','小夫','阿福','魯夫']
12: 
13: # 只看睡眠(w2=0 表示完全忽略咖啡)
14: w = np.array([1, 0])
15: b = -5.5  # 門檻 = 5.5 小時
16: 
17: print("預測結果(只看睡眠時數):")
18: for i in range(len(X)):
19:     pred = step_function(np.dot(w, X[i]) + b)
20:     label = '清醒' if pred == 1 else '睡著'
21:     correct = '○' if pred == y[i] else '×'
22:     print(f"  {names[i]}(睡{X[i][0]}hr, 咖啡{X[i][1]}杯)→ {label} {correct}")

範例結果:7/8 正確。唯一被分錯的是阿福——他只睡 4 小時但靠 2 杯咖啡硬撐,你的感知器看不到咖啡的功勞。

4.1.2. 你的任務:讓感知器也看到咖啡

把範例程式碼複製過來,修改 wb ,讓感知器 同時考慮 睡眠和咖啡,把 8 個人全部分對。

提示:

  • 每 1 杯咖啡的提神效果大約等於多睡 2 小時,所以 w2(咖啡權重)應該是 w1 的 2 倍
  • 決策邊界為 \(w_1 \times 睡眠時數 + w_2 \times 咖啡杯數 + b = 0\)

正確答案(用來驗證):

w=[1, 2], b=-6 可以全部分對(8/8)

  阿明(睡8hr, 咖啡2杯)→ 分數=6  → 清醒 ○
  小美(睡4hr, 咖啡0杯)→ 分數=-2 → 睡著 ○
  大雄(睡7hr, 咖啡1杯)→ 分數=3  → 清醒 ○
  胖虎(睡3hr, 咖啡1杯)→ 分數=-1 → 睡著 ○
  靜香(睡6hr, 咖啡2杯)→ 分數=4  → 清醒 ○
  小夫(睡5hr, 咖啡0杯)→ 分數=-1 → 睡著 ○
  阿福(睡4hr, 咖啡2杯)→ 分數=2  → 清醒 ○ ← 咖啡救了他!
  魯夫(睡3hr, 咖啡0杯)→ 分數=-3 → 睡著 ○
 1: import numpy as np
 2: import matplotlib.pyplot as plt
 3: 
 4: def step_function(x):
 5:     return np.array(x >= 0, dtype=int)
 6: 
 7: # 同學資料 [昨晚睡幾小時, 今天咖啡幾杯]
 8: X = np.array([[8,2], [4,0], [7,1], [3,1],
 9:               [6,2], [5,0], [4,2], [3,0]])
10: # 標籤: 清醒=1, 睡著=0
11: y = np.array([1, 0, 1, 0, 1, 0, 1, 0])
12: names = ['阿明','小美','大雄','胖虎','靜香','小夫','阿福','魯夫']
13: 
14: # TODO: 修改 w 和 b,同時考慮睡眠和咖啡,讓 8 個人全部分對
15: w = np.array([___, ___])  # [睡眠權重, 咖啡權重]
16: b = ___
17: 
18: # 測試感知器
19: print("預測結果:")
20: for i in range(len(X)):
21:     pred = step_function(np.dot(w, X[i]) + b)
22:     label = '清醒' if pred == 1 else '睡著'
23:     correct = '○' if pred == y[i] else '×'
24:     print(f"  {names[i]}(睡{X[i][0]}hr, 咖啡{X[i][1]}杯)→ {label} {correct}")
25: 
26: # 繪製散佈圖與決策邊界
27: plt.figure(figsize=(6, 5))
28: awake = y == 1
29: asleep = y == 0
30: plt.scatter(X[awake, 0], X[awake, 1], c='green', marker='o', s=100, label='清醒')
31: plt.scatter(X[asleep, 0], X[asleep, 1], c='red', marker='x', s=100, label='睡著')
32: for i, name in enumerate(names):
33:     plt.annotate(name, (X[i,0]+0.1, X[i,1]+0.1))
34: 
35: # 決策邊界: w1*x1 + w2*x2 + b = 0 → x2 = -(w1*x1 + b)/w2
36: x1_line = np.linspace(2, 9, 100)
37: x2_line = -(w[0] * x1_line + b) / w[1]
38: plt.plot(x1_line, x2_line, 'b--', linewidth=2, label='決策邊界')
39: 
40: plt.xlabel('昨晚睡幾小時')
41: plt.ylabel('今天咖啡幾杯')
42: plt.title('感知器預測:你會不會睡著?')
43: plt.legend()
44: plt.grid(True)
45: plt.savefig("images/perceptron-sleep.png", dpi=300)

4.1.3. 思考題

  1. 如果你只用睡眠時數(w2 設為 0),能不能把 8 個人全部分對?哪個人會被分錯?為什麼?
  2. 阿福只睡 4 小時但喝了 2 杯咖啡,居然保持清醒。你的感知器有抓到「咖啡續命」這個規律嗎?
  3. 你自己昨晚睡幾小時、今天喝了幾杯咖啡?把自己的數值代入你的感知器,它預測你這堂課會不會睡著?

4.2. 練習二:感知器學習演算法

在練習一中,我們靠「猜」來找權重。但感知器其實可以透過 學習演算法 自動找到適合的權重!

4.2.1. 感知器學習規則

感知器的學習過程就像「從錯誤中學習」:每次預測錯了,就稍微調整權重,讓下次更可能答對。具體步驟如下:

  1. 初始化權重 \(w\) 和偏移值 \(b\) 為 0
  2. 對每筆訓練資料 \((x, y_{true})\):
    • 計算預測值 \(y_{pred} = step(w \cdot x + b)\)
    • 若預測錯誤(\(y_{pred} \neq y_{true}\)),更新權重:
      • \(w \leftarrow w + \eta \cdot (y_{true} - y_{pred}) \cdot x\)
      • \(b \leftarrow b + \eta \cdot (y_{true} - y_{pred})\)
    • 若預測正確,不做任何更新
  3. 重複多次(稱為 epoch)直到全部正確

其中 \(\eta\)(eta)稱為 學習率 ,控制每次調整的幅度。

4.2.2. 故事背景

期中考成績出來了。導師看著成績單,眉頭深鎖。

根據多年「偷窺學生」的經驗,導師發現一個殘酷的規律:學生期末會不會被當,跟他們 每週滑手機的時數每週讀書的時數 高度相關。為了「提早關心」這些同學(翻譯:趁現在通知家長還來得及),導師決定建立一套 AI 預警系統。

以下是導師暗中觀察到的 6 位同學生活數據:

代號 每週滑手機(hr) 每週讀書(hr) 期末結果
小明 30 2 被當
學霸 5 25 存活
阿宅 25 5 被當
認真妹 10 20 存活
社畜 20 10 被當
覺醒者 15 15 存活

「社畜」是因為打工太多才一直滑手機不讀書的,但導師不管原因,只看結果。而「覺醒者」滑手機跟讀書時間一樣多,但因為讀書效率極高,勉強存活——屬於統計上的奇蹟。

4.2.3. 任務 A:執行學習演算法

請執行以下程式碼,觀察感知器如何 自動 找到權重,不用再像練習一那樣猜來猜去。

 1: import numpy as np
 2: 
 3: def step_function(x):
 4:     return np.array(x >= 0, dtype=int)
 5: 
 6: # 訓練資料 [每週滑手機hr, 每週讀書hr]
 7: X = np.array([[30,2], [5,25], [25,5], [10,20], [20,10], [15,15]])
 8: y = np.array([0, 1, 0, 1, 0, 1])  # 0=被當, 1=存活
 9: names = ['小明', '學霸', '阿宅', '認真妹', '社畜', '覺醒者']
10: 
11: # 初始化
12: w = np.zeros(2)
13: b = 0.0
14: eta = 0.01   # 學習率
15: 
16: # 訓練
17: print(f"初始狀態: w=[{w[0]:.3f}, {w[1]:.3f}], b={b:.3f}")
18: for epoch in range(50):
19:     w_before, b_before = w.copy(), b  # 記錄 epoch 開始前的狀態
20:     errors = 0
21:     for xi, yi in zip(X, y):
22:         y_pred = step_function(np.dot(w, xi) + b)
23:         error = yi - y_pred
24:         if error != 0:
25:             w = w + eta * error * xi  # 權重更新
26:             b = b + eta * error        # 偏移值更新
27:             errors += 1
28:     if epoch < 5 or errors == 0:
29:         changed = "(權重已更新)" if (not np.array_equal(w, w_before) or b != b_before) else "(無變化)"
30:         print(f"Epoch {epoch+1:2d}: w=[{w[0]:.3f}, {w[1]:.3f}], b={b:.3f}, 錯誤數={errors} {changed}")
31:     if errors == 0:
32:         print("訓練完成!導師的 AI 預警系統上線了。")
33:         break
34: 
35: # 測試
36: print("\n=== 預測結果 ===")
37: for i in range(len(X)):
38:     pred = step_function(np.dot(w, X[i]) + b)
39:     result = '存活' if pred == 1 else '被當'
40:     correct = '○' if pred == y[i] else '×'
41:     print(f"  {names[i]}: 滑手機{X[i][0]}hr 讀書{X[i][1]}hr → {result} {correct}")

執行後觀察:

  1. =w[0]=(滑手機的權重)是正的還是負的?直覺上合理嗎?
  2. =w[1]=(讀書的權重)是正的還是負的?為什麼?
  3. 花了幾個 epoch 才全部答對?

正確答案:

Epoch 6 收斂(6 個 epoch 就全部答對)
最終權重:w=[-0.300, 0.580], b=0.010

  w[0] = -0.300(負的)→ 滑手機越多,分數越低,越容易被當 ✓
  w[1] = 0.580(正的)→ 讀書越多,分數越高,越容易存活 ✓
  感知器自己學到了「滑手機有害、讀書有益」——跟導師的直覺完全一致

4.2.4. 任務 B:預測新同學的命運

導師又盯上了三位可疑人物。在上方程式碼的 最後面 加入以下程式碼,預測他們期末的下場:

 1: # (接續上方程式碼,請貼在最後面一起執行)
 2: 
 3: # 預測新同學
 4: new_students = [
 5:     ("邊緣人", [18, 12]),   # 滑手機略多於讀書
 6:     ("佛系仔", [15, 16]),   # 幾乎一半一半,讀書只多 1 小時
 7:     ("手遊王", [35, 1]),    # 不用預測你也知道
 8: ]
 9: print("\n=== 導師的黑名單預測 ===")
10: for name, data in new_students:
11:     x = np.array(data)
12:     score = np.dot(w, x) + b
13:     pred = step_function(score)
14:     status = '存活' if pred == 1 else '被當'
15:     emoji = '安全過關' if pred == 1 else '準備叫家長'
16:     print(f"  {name}: 滑{data[0]}hr 讀{data[1]}hr → 分數={score:.2f}{status}{emoji})")

觀察結果,回答:

  1. 「邊緣人」被判存活還是被當?你覺得合理嗎?
  2. 「佛系仔」讀書只比滑手機多 1 小時,AI 覺得他能活嗎?
  3. 這三個人的「分數」有正有負——分數越大代表什麼?越小又代表什麼?

正確答案:

  邊緣人: 滑18hr 讀12hr → 分數=1.57 → 存活(驚險過關)
  佛系仔: 滑15hr 讀16hr → 分數=4.79 → 存活(讀書權重比滑手機高,多讀 1 小時就夠了)
  手遊王: 滑35hr 讀1hr → 分數=-9.91 → 被當(毫無懸念,準備叫家長)

  分數的意義:
  > 0 → 存活(越大越安全)
  < 0 → 被當(越小越危險)
  ≈ 0 → 命懸一線

4.2.5. 任務 C:調整學習率

把任務 A 程式碼中的 eta = 0.01 分別改成 0.0010.1 ,重新執行,比較:

  1. 各需要幾個 epoch 才完成訓練?
  2. 學到的權重 w 值一樣嗎?
  3. 是不是學習率越大越好?(提示:如果改成 eta = 1.0 會怎樣?)

正確答案:

eta=0.001: 收斂 Epoch  6, w=[-0.030, 0.058], b=0.001
eta=0.010: 收斂 Epoch  6, w=[-0.300, 0.580], b=0.010
eta=0.100: 收斂 Epoch  6, w=[-3.000, 5.800], b=0.100
eta=1.000: 收斂 Epoch  6, w=[-30.00, 58.00], b=1.000

→ 四種學習率都在 Epoch 6 收斂(這筆資料太乖了)
→ 權重「方向」相同(w[0]<0, w[1]>0),「大小」跟 eta 成正比
→ 在更複雜的問題中,太大的 eta 可能讓權重來回震盪,反而無法收斂

4.2.6. 任務 D:畫出決策邊界

在任務 A 程式碼的最後面加入以下程式碼,把導師的 AI 預警系統「看得見」:

 1: # (接續任務 A 程式碼,請貼在最後面一起執行)
 2: 
 3: import matplotlib.pyplot as plt
 4: 
 5: plt.figure(figsize=(7, 6))
 6: survived = y == 1
 7: failed = y == 0
 8: plt.scatter(X[survived, 0], X[survived, 1], c='green', marker='o', s=120, label='存活', zorder=5)
 9: plt.scatter(X[failed, 0], X[failed, 1], c='red', marker='x', s=120, linewidths=2, label='被當', zorder=5)
10: 
11: for i, name in enumerate(names):
12:     plt.annotate(name, (X[i,0]+0.5, X[i,1]+0.5), fontsize=10)
13: 
14: # 決策邊界
15: x1_line = np.linspace(0, 40, 100)
16: if w[1] != 0:
17:     x2_line = -(w[0] * x1_line + b) / w[1]
18:     plt.plot(x1_line, x2_line, 'b--', linewidth=2, label='決策邊界(生死線)')
19: 
20: plt.xlabel('每週滑手機時數')
21: plt.ylabel('每週讀書時數')
22: plt.title('導師的 AI 預警系統:期末生死分界線')
23: plt.legend()
24: plt.grid(True, alpha=0.3)
25: plt.xlim(0, 40)
26: plt.ylim(0, 30)
27: plt.savefig("images/perceptron-flunk.png", dpi=300)
28: print("決策邊界圖已儲存!")
29: print(f"生死線方程式: {w[0]:.3f} × 滑手機 + {w[1]:.3f} × 讀書 + {b:.3f} = 0")

看看那條「生死線」:線的哪一邊是安全區?哪一邊是危險區?你自己在圖上大概會落在哪裡?

正確答案:

生死線方程式: -0.300 × 滑手機 + 0.580 × 讀書 + 0.010 = 0
→ 線的右上方(讀書多、滑手機少)= 安全區
→ 線的左下方(讀書少、滑手機多)= 危險區

Footnotes:

Author: Yung-Chin Yen

Created: 2026-02-14 Sat 15:40