把整個 tensorflow 工具箱載入記憶體(已經載入過就沿用),再做一張名牌 Sequential,直接綁到工具箱裡 keras 子工具箱的那張 Sequential(序列模型)設計圖上。
看動畫 1:這種寫法在記憶體裡的樣子(1:32) 看動畫 3:接著第 1 行(0:12)
逐字拆解
tensorflow套件 要先用 pip install tensorflow 安裝過才有。載入後在記憶體裡是一個模組物件(工具箱)。
.keras子模組 點 =「打開它,往裡面找」。tensorflow 工具箱裡的 keras 子工具箱。
import關鍵字 「把後面那個東西綁上名牌」:這裡是在你的程式裡建立名字 Sequential。
Sequential類別 大寫開頭,是一張設計圖。這一行只是綁上名牌,還沒做出任何模型。
這一行執行時發生什麼事
查「已載入清單」sys.modules:還沒有 tensorflow,就執行它的程式碼,在記憶體做出 tensorflow 工具箱(模組物件)並登記。這一步可能要好幾秒(實測 7 秒),還會印出一堆 log 訊息,這是正常的。已經載入過就直接沿用。
打開工具箱裡的 keras 子工具箱,找到 Sequential 這張設計圖。
在你的程式裡建立名字 Sequential,直接指向那張設計圖。
執行後,你的程式裡多了一張名牌:
Sequential → tensorflow 工具箱 → keras 裡的 Sequential 設計圖(類別)
不會有 tf,也不會有 tensorflow 這兩個名牌;記憶體裡也還沒有任何模型。
實測 Sequential is tf.keras.Sequential → True(同一個東西)
Sequential 是什麼樣的模型?
Sequential 的意思是「依序」:資料從第一層進去,一層接一層直線往下走 ,沒有分岔、沒有跳接。像一條生產線,每一站把上一站的半成品加工後交給下一站。大部分入門模型(包括本題和第 47 題的 CNN)都用它。
等價寫法(考試也可能出現)
from tensorflow.keras.models import Sequential # 從 models 子模組拿,結果相同
import tensorflow as tf
model = tf.keras.Sequential([...]) # 不用 from,直接寫全名
from keras import Sequential # Keras 3 單獨安裝時的寫法
新手常踩的坑
寫成 sequential(小寫 s):Python 分大小寫。實測錯誤訊息:ImportError: cannot import name 'sequential' from 'tensorflow.keras'。
寫成 import tensorflow.keras.Sequential:import 後面只能接「模組」,Sequential 是類別。實測:ModuleNotFoundError: No module named 'tensorflow.keras.Sequential'。要改用 from … import。
從 Keras 的 layers(各種神經網路層)貨架,拿出 Dense(全連接層)這張設計圖。
看動畫:Dense 類別載入記憶體(0:23)
逐字拆解
tensorflow.keras.layers模組 專門放「層」的地方:Dense、Conv2D、Dropout… 都在這。
Dense類別 全連接層的設計圖。等一下會用它做出兩個層物件。
Dense(全連接層)到底在算什麼?
「全連接」的意思是:這一層的每個神經元,都會看過上一層的全部輸出 。比喻:一場選秀有 64 位評審(神經元),每位評審都要看完參賽者的全部 784 個特徵(像素),各自依照自己的評分標準(權重)打出一個分數。
每個神經元做的事只有三步:
把每個輸入乘上自己的權重 w,全部加起來
再加上一個偏差值 b(bias,像評審的基本分)
丟進激活函數(activation)過濾一下,得到輸出
數學寫法:
輸出 = activation( 輸入 · W + b )
└ W 叫 kernel(權重矩陣),b 叫 bias(偏差向量),都是訓練時要學的數字
新手常踩的坑
寫成 tensorflow.keras.layer(少一個 s):模組名稱是複數 layers。實測:ModuleNotFoundError: No module named 'tensorflow.keras.layer'。
以為 import 進來就有層了:這行只是拿到設計圖,第 5、6 行加上括號才真正做出層物件。
照 Sequential 設計圖做出一個模型物件,模型內容是清單裡的兩層,由上往下依序疊起來,做好後取名叫 model。
看動畫:清單與模型物件怎麼做出來(1:07) 看動畫:最後才綁上 model 名牌(1:38)
逐字拆解
model變數 自己取的名字,叫 net、clf 都可以。它就像貼在物件上的名牌。
=指派 不是數學的「等於」。意思是「把右邊算出來的東西,貼上左邊的名牌」。
Sequential(類別 +括號 類別加括號 = 建立物件 。Python 會在背後執行這個類別的初始化程式(__init__),把一個全新的模型交給你。
[ … ]list 把兩個層物件裝成一份有順序的清單,當成 Sequential 的第一個參數。清單的順序就是資料流動的順序 。
])收尾 先關 list 的 ],再關呼叫的 )。括號一關,這句話(第 4~7 行)才結束。
Sequential 的參數
參數 本題給的值 預設值 意思
layers兩個 Dense 組成的 list None要依序疊起來的層。不給也行,之後用 model.add() 一層一層加
trainable(沒寫) True整個模型的權重能不能被訓練
name(沒寫) 自動取名 sequential 模型的名字,summary() 會印出來
執行完之後,model 身上有什麼
model → 一個 Sequential 物件(模型本體)
model.layers → [Dense 物件(64), Dense 物件(10)] ← 屬性,沒有括號
因為第一層有給 input_shape,Keras 已經知道輸入大小,權重矩陣這時就建好了,
所以馬上可以呼叫 model.summary() 看結構。
動手拆開物件看看(Keras 3 實測)
物件做好後,可以用屬性和方法一層一層「打開」它。注意哪些有括號(方法)、哪些沒有(屬性):
model.layers # [<Dense name=dense, built=True>, <Dense name=dense_1, built=True>]
model.layers[0] # 取出第一個層物件(list 用 [0] 取第一個)
model.layers[0].kernel.shape # (784, 64) ← 第一層的權重矩陣:屬性的屬性
model.layers[0].bias.shape # (64,) ← 第一層的偏差
model.get_weights() # 方法:回傳 4 個 NumPy 陣列 (784,64)、(64,)、(64,10)、(10,)
model.count_params() # 方法:50890
model.layers[0].kernel.shape 可以從左讀到右:「model 的 layers 清單的第 0 個(一個 Dense 物件)的 kernel(權重,一個變數物件)的 shape(形狀)」。每個點都往物件裡面走一層。
這個物件會做哪些事?(Sequential 常用方法)
方法 做什麼 什麼時候用
model.summary()印出每層的輸出形狀和參數數量 建完模型先檢查
model.compile(...)設定 loss、optimizer、metrics 訓練前一定要做(第 8 行)
model.fit(x, y, ...)開始訓練,回傳訓練紀錄 History 物件 compile 之後
model.evaluate(x, y)在測試資料上算 loss 和指標 訓練完評估
model.predict(x)輸入新資料,輸出預測(本題是 10 個機率) 真正拿來用
model.add(layer)在最後面再疊一層 一層一層建模型時
model.save('m.keras')把模型存成檔案 訓練好要保存
另一種寫法:先做空模型,再用 add 方法加層
model = Sequential() # 先做出一個「空的」模型物件
model.add(Dense(64, activation='relu', input_shape=(784,))) # 呼叫 add 方法加第一層
model.add(Dense(10, activation='softmax')) # 再加第二層
結果和考題寫法一模一樣。這個寫法更能看出物件導向的味道:先有物件,再呼叫物件的方法改變它的內容 。
做一個有 64 個神經元的全連接層,每個神經元算完後用 ReLU 過濾,並告訴模型每筆輸入是 784 個數字。
看動畫:Dense(…) 在記憶體裡的三個步驟(0:31)
逐字拆解
Dense(類別 +括號 建立一個 Dense 層物件。這個物件沒有取名字,直接放進 list 裡交給 Sequential 保管。
64位置參數 對應第 1 個參數 units:這層有 64 個神經元,所以會輸出 64 個數字。
activation='relu'關鍵字參數 用字串指定激活函數。Keras 會在內部把 'relu' 換成真正的 ReLU 函式。
input_shape=(784,)關鍵字參數 每一筆輸入的形狀:784 個數字排成一列。只有第一層 需要寫,後面的層會自動推算。
),收尾 關掉 Dense 的括號;逗號用來分隔 list 裡的兩個元素。
參數一個一個講清楚
① units = 64|為什麼是 64? 這是超參數由人決定、不是訓練學出來的設定值,例如神經元數量、學習率、epoch 數。 ,沒有標準答案。神經元越多,模型能記的規律越多,但也越容易過擬合、越慢。32、64、128、256 是常見選擇。
② activation = 'relu'|ReLU 在做什麼? ReLU(x) = max(0, x):負數變 0,正數照原樣 。例如 [-2.1, 0.5, 3.0] → [0, 0.5, 3.0]。隱藏層幾乎都用它,因為:
它讓網路變成「非線性」,才能學到彎曲的規律(只有線性的話,疊再多層都等於一層)
計算非常快,只要比大小
正數區的斜率固定是 1,比 sigmoid 更不容易梯度消失反向傳播時梯度一層層變小,傳到前面幾層幾乎是 0,前面的層就學不動。
③ input_shape = (784,)|784 從哪來? 一張手寫數字是 28 × 28 = 784 個像素。MLP 只能吃「一整排數字」,所以要先把 28×28 的方格展平 成 784 個數字排成一列。注意 shape 裡不包含資料筆數 :你一次餵 32 張或 1000 張都可以,Keras 會用 None 代表「筆數未定」。
Dense 完整參數表(沒寫的都用預設值)
參數 本題 預設值 白話
units64 (必填) 神經元數量 = 這層輸出幾個數字
activation'relu' None激活函數。None 代表不過濾,原樣輸出(線性)
use_bias— True要不要加偏差 b
kernel_initializer— 'glorot_uniform'權重一開始的亂數怎麼抽。Glorot 會依輸入、輸出數量調整亂數範圍,避免一開始就太大或太小(本層的範圍下面有算)
bias_initializer— 'zeros'偏差一開始都是 0
kernel_regularizer— None對權重加 L1/L2 懲罰,用來抑制過擬合
bias_regularizer、activity_regularizer— None對偏差、對輸出加懲罰
kernel_constraint、bias_constraint— None限制權重的範圍(例如最大範數)
input_shape(784,) — 舊式寫法:放在第一層告訴模型輸入形狀
參數數量怎麼算:50,240
權重 W:784 個輸入 × 64 個神經元 = 50,176
偏差 b:每個神經元 1 個 = 64
合計 = 50,240 ← model.summary() 第一列印出的就是這個數字
形狀變化:
輸入 (筆數, 784) · W (784, 64) + b (64,) → 輸出 (筆數, 64)
剛建好時,50,240 個參數是什麼數字?
還沒訓練,權重就已經有值了,是照 glorot_uniform 抽的亂數:在 ±√(6 ÷ (輸入數 + 輸出數)) 之間平均抽。
本層範圍 = ±√(6 ÷ (784 + 64)) = ±√0.00708 ≈ ±0.0841
實測 model.layers[0].kernel 的最小值 −0.08411、最大值 0.08411(剛好落在範圍內)
偏差 bias:預設 'zeros',64 個全是 0.0
為什麼不全部設成 0?如果每個神經元的權重都一樣,它們算出來的東西就一模一樣,更新時也一模一樣,64 個神經元等於只有 1 個。用亂數讓每個神經元從不同起點出發,才學得到不同的特徵。
新版 Keras 會跳警告,但不影響答案
在 Keras 3 執行這行,會看到:Do not pass an `input_shape`/`input_dim` argument to a layer…。意思是新版建議把輸入形狀獨立成一個 Input:
from tensorflow.keras import Input
model = Sequential([
Input(shape=(784,)), # 新式:先宣告輸入
Dense(64, activation='relu'), # 第一層就不用再寫 input_shape
Dense(10, activation='softmax')
])
這只是警告(warning),不是錯誤(error),模型照樣建得起來。
新手常踩的坑
input_shape=(784) 少了逗號:那只是整數 784,不是 tuple。實測錯誤訊息:ValueError: Cannot convert '784' to a shape.
寫成 Dense(activation='relu', 64):關鍵字參數放在位置參數前面。實測:SyntaxError: positional argument follows keyword argument,程式連開始執行都不會。
input_shape=(28, 28):Dense 不會自動展平。2D 圖片要先 reshape 成 784,或在最前面加一層 Flatten()。
以為 input_shape 要寫資料筆數,例如 (60000, 784):不用,筆數不寫。
輸出層有 10 個神經元,剛好對應 0~9 十個數字。空格 (A) 要填一個能把 10 個分數變成「加起來等於 1 的機率」的函數。
看動畫:第二個 Dense 物件(1:00)
逐字拆解
Dense(10,類別 +位置參數 第二個層物件,units = 10。輸出層的神經元數一定等於類別數 。
activation='___(A)___'關鍵字參數 空格就是考點:輸出層要用哪個激活函數。
)收尾 這是 list 的最後一個元素,所以後面沒有逗號(加了也不會錯)。
這層沒寫 input_shape,因為 Keras 會自動接上一層:上一層輸出 64 個數字,所以這層每個神經元有 64 個輸入。
參數:64 × 10 + 10 = 650
形狀:(筆數, 64) → (筆數, 10)
沒加激活函數之前:10 個 logits
Dense 先算出 10 個任意大小的分數,叫做 logits ,例如 [1.2, -0.3, 0.8, …, 3.5, …]。它們可以是負數、可以很大,還不是機率 。激活函數的工作,就是把它們轉成我們要的樣子。
(A) 填 softmax:把 10 個分數變成機率分布
softmax(zᵢ) = e^zᵢ ÷ (e^z₁ + e^z₂ + … + e^z₁₀)
小例子(3 類): z = [2.0, 1.0, 0.1]
e^2.0 = 7.389 e^1.0 = 2.718 e^0.1 = 1.105 總和 = 11.212
→ [0.659, 0.242, 0.099] 加起來 = 1.000 ✓
softmax 有三個特性,剛好符合題目要的「單標籤 10 類」:
每個值都在 0~1 之間 (e 的次方一定是正數)
全部加起來剛好等於 1 ,所以可以讀成「是 0 的機率、是 1 的機率…」
分數越高的類別,機率被放大得越多 ,互相競爭。一個類別變高,其他的一定變低,正好表達「只會是其中一個」
四個選項的激活函數比一比
函數 輸出範圍 10 個加起來 = 1? 適合的輸出層
softmax0~1 ✓ 一定等於 1 多類別、單標籤 (本題)
sigmoid0~1 ✗ 每個各算各的 二元分類,或多標籤(一張圖可同時屬於多類)
relu0~∞ ✗ 還可能大於 1 隱藏層;迴歸要輸出非負數時
tanh-1~1 ✗ 還有負數 隱藏層(RNN 常見),不當分類輸出
到 §4 的互動實驗把四個函數切換看看,差異會非常明顯。
新手常踩的坑
Dense(1, activation='softmax'):只有一個數字的 softmax 永遠輸出 1.0(實測 3 筆隨機輸入都輸出 [1. 1. 1.]),模型什麼都學不到。
輸出層 units 寫成 784 或 64:輸出層的數量要等於類別數 10,跟輸入大小無關。
訓練前的設定:告訴 model 用什麼方式計算「錯了多少」(loss),以及用什麼方法修正權重(optimizer)。
看動畫:Python 怎麼找到 compile(1:50) 看動畫:寫成 model = model.compile() 會怎樣(2:25)
逐字拆解
.compile方法 點 = 「的」。「model 的 compile 功能」。compile 是 Sequential 類別裡寫好的方法,每個模型物件都有。
loss='___(B)___'關鍵字參數 空格 (B):損失函數的名字。
optimizer='adam'關鍵字參數 用字串指定優化器,Keras 會自動建立一個 Adam 物件。
物件導向重點:compile 是「改物件自己」,不會回傳新模型
compile 執行完回傳的是 None(什麼都沒有),它改的是 model 內部的設定。所以:
model.compile(...) # ✓ 正確:直接呼叫
model = model.compile(...) # ✗ 錯誤:model 會變成 None,之後 model.fit() 直接報錯
這是很多初學者第一次寫 Keras 就會踩的坑。實測錯誤訊息:AttributeError: 'NoneType' object has no attribute 'fit'。
compile 之後,model 身上多了什麼(Keras 3 實測)
model.optimizer → <keras.src.optimizers.adam.Adam object …> ← 字串 'adam' 被換成了一個 Adam 物件
model.optimizer.learning_rate → 0.001 ← 那個物件身上的屬性
model.loss → 'categorical_crossentropy'
compile 本身回傳 None
這就是「呼叫方法 → 改變物件內部狀態」:compile 之前 model 沒有優化器,compile 之後多了一個 Adam 物件掛在 model.optimizer 上,等 fit() 時拿來用。
compile 完整參數表
參數 本題 預設值 白話
optimizer'adam' 'rmsprop'怎麼依照梯度更新權重。可以給字串,也可以給優化器物件
loss(B) None怎麼計算預測和答案差多少。訓練就是想辦法讓它變小
metrics(沒寫) None訓練時順便顯示的指標,常寫 ['accuracy']。本題沒寫,所以訓練時只會看到 loss,看不到準確率
loss_weights— None多個輸出時,各個 loss 的權重
weighted_metrics— None會考慮樣本權重的指標
run_eagerly— FalseTrue 會逐行執行,方便除錯但比較慢
steps_per_execution— 1一次送幾個批次給運算裝置,調大可加速
jit_compile— 'auto'是否用 XLA 編譯加速
(B) 填 categorical_crossentropy:配合 One-Hot 標籤的損失函數
categorical_crossentropy = −Σ yᵢ × log(pᵢ) (y 是答案,p 是預測機率)
One-Hot 標籤只有正確類別是 1,其他都是 0,所以加總裡只剩一項:
loss = −log( 模型給正確答案的機率 )
正確答案的機率 0.90 → loss = 0.105 (很有把握且答對,罰很輕)
正確答案的機率 0.24 → loss = 1.417 (猶豫)
正確答案的機率 0.01 → loss = 4.605 (很有把握地答錯,罰很重)
log 的特性讓「越有把握地答錯」被罰得越重。訓練時,optimizer 會不斷調整權重,把正確類別的機率推高、讓 loss 變小。
三個長得很像的 crossentropy,差在標籤長相
損失函數 標籤 y 長這樣(答案是 7) 搭配的輸出層
categorical_crossentropy[0,0,0,0,0,0,0,1,0,0](One-Hot)Dense(10, softmax)
sparse_categorical_crossentropy7(一個整數)Dense(10, softmax)
binary_crossentropy0 或 1;多標籤時是多個 0/1Dense(1 或 k, sigmoid)
mean_squared_error任意實數,例如房價 352.7 Dense(1),不加激活(迴歸)
sparse 版和一般版算出來的數學結果相同,只是吃的標籤格式不同 。題目明講「標籤已轉為 One-Hot」,所以選沒有 sparse 的那個。
深入一點:為什麼 softmax 和 cross-entropy 是天生一對
把 softmax 和 categorical_crossentropy 接在一起,對 logits 求梯度,結果簡單得驚人:梯度 = 預測機率 − 答案 (p − y)。
logits z = [2.0, 1.0, 0.1],答案 y = [0, 1, 0]
softmax p = [0.659, 0.242, 0.099]
梯度 = p − y = [0.659, −0.758, 0.099]
用 tf.GradientTape 實際算出來:[0.6590, −0.7576, 0.0986] ← 和 p − y 一模一樣
白話:正確類別的梯度是負的(要把它的分數往上推),其他類別是正的(往下壓),而且錯得越多,推力越大 。梯度不會因為「很有把握地答錯」而變得很小,這正是 sigmoid + MSE(選項 A)做不到的。
'adam' 背後是什麼
字串 'adam' 會讓 Keras 用預設值建立一個 Adam 優化器物件:learning_rate=0.001、beta_1=0.9、beta_2=0.999、epsilon=1e-07。Adam 會依照每個權重最近的梯度大小,自動調整各自的步伐,是目前最常用的預設選擇。
如果想自己改學習率,就不要給字串,改給一個物件 :
from tensorflow.keras.optimizers import Adam
opt = Adam(learning_rate=1e-4) # 類別 → 物件(自訂參數)
model.compile(loss='categorical_crossentropy', optimizer=opt) # 把物件當參數傳進去
這又是一次「類別 → 物件」:字串只是用預設值的捷徑,物件才能細調。
compile 之後的 fit(),背後在做什麼
compile 只是「設定」。真正訓練是 model.fit(),它對每一小批資料重複四件事:
① 前向 model(x) 算出 10 個機率
→
② 算 loss categorical_crossentropy
→
③ 算梯度 自動微分
→
④ 更新 Adam 改 50,890 個參數
Keras 把這四步藏在 fit 裡面;PyTorch 則要你自己把迴圈寫出來,第 50 題就是那個迴圈。
新手常踩的坑
標籤是整數(例如 [1, 2, 3])卻用 categorical_crossentropy。實測 fit 時:ValueError: Arguments `target` and `output` must have the same rank (ndim). Received: target.shape=(None,), output.shape=(None, 10)。意思是答案只有 1 維、輸出有 10 格,對不上。
標籤是 One-Hot 卻用 sparse_categorical_crossentropy。實測:ValueError: Argument `output` must have rank (ndim) `target.ndim - 1`. Received: target.shape=(None, 10), output.shape=(None, 10)。sparse 版預期答案比輸出少 1 維(只有一個整數)。
沒寫 metrics=['accuracy'] 就問「準確率在哪」:compile 沒要求,fit 就只印 loss。