💡 先搞懂問題
虛構的「晴空咖啡」店長把 3 間分店、連續 4 天的拿鐵杯數放進一個 NumPy 陣列,印出 sales.shape 是 (3, 4)。他想知道「每一天三家店合計賣多少」,寫了 sales.sum(axis=0),拿到 4 個數字,正確。隔週換同事接手,想算「每一家店四天合計」,心想「0 是第一個,應該就是一列一列加」,也寫了 axis=0,同樣拿到 4 個數字,就把它當成 3 家店的業績貼進報表。程式完全沒有報錯,因為 NumPy 只照你給的 axis 做事,它不知道你心裡想的是哪個方向。
新手在這個主題最常卡在三件事。第一是 axis 的方向感:axis=0 不是「對第 0 列做事」,而是「沿著第 0 軸把資料壓扁」,第 0 軸因此消失。第二是形狀的寫法:(3,)、(3, 1)、(1, 3) 看起來都是「3 個數」,在 NumPy 裡卻是三種不同的東西,scikit-learn 還會因此拒收你的資料。第三是 reshape(-1, 2) 這類寫法:-1 是多少、元素依什麼順序填進新形狀,不清楚就只能用猜的。
先把名詞對好。shape(形狀)是一個 tuple,依序寫出每一個軸(axis,也就是維度)的長度,(3, 4) 代表第 0 軸長 3、第 1 軸長 4;ndim 是軸的數量;size 是元素總數,等於 shape 裡所有數字相乘。sum、mean、max、argmax 這類彙總(reduction)函式的 axis 參數,指定「沿著哪一軸把一整排值合成一個值」,被指定的那一軸會從結果的 shape 裡消失。reshape 則只換排法,不增減元素。
生活比喻:蛋盒與倒進碗裡的蛋
想像蛋行老闆把 12 顆蛋排在一個 3 列、每列 4 格的蛋盒裡。客人問「每一直行有幾斤」,他就把每一直行的 3 顆蛋倒進同一個碗,最後桌上有 4 個碗;客人改問「每一橫列有幾斤」,他就把每一橫列的 4 顆蛋倒進一個碗,桌上只剩 3 個碗。不管哪一種倒法,被倒的那個方向都不見了:倒完直行,就沒有「第幾列」可說;倒完橫列,就沒有「第幾格」可說。同樣這 12 顆蛋,老闆也可以改裝成一排 12 格的長盒,或兩盒各 2 列 3 格,蛋本身一顆都沒變,只是盒子換了。
(3, 4) 就是 shape。「每一直行倒進一個碗」對應 sales.sum(axis=0):沿著第 0 軸把 3 個值合成 1 個,第 0 軸消失,(3, 4) 劃掉 3 剩下 (4,),也就是每一天的三店合計;「每一橫列倒進一個碗」對應 sales.sum(axis=1),劃掉 4 剩下 (3,),是每一家店的四天合計。改裝成長盒或兩盒,對應的是 reshape(12) 與 reshape(2, 2, 3)。開頭那位同事要的是 3 家店的合計,應該寫 axis=1。
這個比喻有三個地方要小心。第一,桌上的 3 個碗看起來排成一直行,但 (3,) 是一維陣列,沒有「直的」或「橫的」,print 出來永遠是一排 [14 19 21];真的要一個直行,得變成 (3, 1),這正是 keepdims=True 的用途。第二,真的換蛋盒要把蛋一顆顆搬過去,reshape 通常只是換一張「怎麼解讀」的說明書,資料在記憶體裡沒動,回傳的是共用同一塊記憶體的 view。第三,「倒進碗」不一定是加總,也可以是平均(mean)、找最大(max),或回報「最大的那顆在第幾格」(argmax),但被壓掉的軸都一樣會消失。
🎮 互動實驗室一:axis 壓縮器
下面兩個陣列是晴空咖啡的拿鐵杯數(示意):A 是 3 間分店 × 4 天,B 是把兩週疊起來的 2 週 × 3 間分店 × 4 天。選陣列、運算與 axis,左邊會用同一種顏色標出「會被合成同一個值」的格子,再一組一組播放壓縮過程;右邊同步顯示那一行 Python、結果的 shape 與 print 出來的樣子。勾選 keepdims 可以比較被壓掉的軸「消失」與「留下長度 1」的差別,選到不存在的軸會看到真的錯誤訊息。
import numpy as np
A = np.array([[3, 7, 2, 2], # 分店 0 的 4 天杯數(示意,單位:十杯)
[6, 2, 8, 3], # 分店 1
[3, 9, 5, 4]]) # 分店 2 → A.shape 是 (3, 4)
B = np.stack([A, [[4, 6, 4, 1],
[2, 8, 6, 5],
[6, 1, 8, 3]]]) # 第 0 週與第 1 週疊起來 → B.shape 是 (2, 3, 4)
輸入
🎮 互動實驗室二:reshape 排列實驗
從 x = np.arange(12) 出發,12 塊數字磚依序是 0 到 11。選新形狀的維度數,再替每一軸挑長度(可以挑 -1 讓 NumPy 自己算),或直接按下方的常見寫法。數字磚會飛到新位置:NumPy 預設照 C 順序(列優先,最右邊的軸變化最快)一格一格填進去。形狀不合法時,畫面會搖一下,並顯示 NumPy 實際丟出的錯誤訊息。2 維的結果可以勾選「對照 .T」,比較 reshape 與轉置排出來有什麼不同。
🎮 互動實驗室三:shape 判讀情境卡
每張卡給一小段程式,問最後那一行得到的 shape。先寫下原本的 shape,再判斷這一行屬於哪一類操作:彙總、索引、插入新軸、轉置、reshape 或攤平。選完會說明正解為什麼是它;選錯時會說明你選的那個 shape 是哪一種寫法才會得到。右邊的口訣會亮起這張卡用到的規則。
📘 原理補完
1. shape、ndim、size:先數有幾條軸
NumPy 文件把陣列的每一個維度稱為一條軸(axis),編號從 0 開始,由外往內。B.shape 是 (2, 3, 4),代表第 0 軸長 2(兩週)、第 1 軸長 3(三間分店)、第 2 軸長 4(四天)。ndim 是 shape 的長度,也就是軸的數量;size 是所有長度相乘的元素總數 24;len(B) 只回報第 0 軸的長度 2。axis 也能用負數從右邊數,axis=-1 永遠是最後一軸,在不確定陣列有幾維的函式裡特別好用。
2. axis:被指定的軸會消失
sum、mean、max、min、argmax、std 這些彙總函式都遵守同一條規則:沿著 axis 指定的那一軸,把一整排值合成一個值,所以那一軸會從結果的 shape 中消失,其餘的軸維持原本的順序。把 shape 寫出來、劃掉被指定的那個數字,剩下的就是答案。A 是 (3, 4),A.sum(axis=0) 劃掉 3 得 (4,),A.sum(axis=1) 劃掉 4 得 (3,)。axis 也可以給 tuple 一次壓掉好幾軸,例如影像批次 (8, 3, 32, 32) 用 axis=(2, 3) 壓掉高與寬,得到每張圖每個通道的平均 (8, 3)。不給 axis(等於 axis=None)就是所有軸一起壓掉,結果是一個 shape 為 () 的純量。
(3,),print 出來是 [14 19 21] 一排。argmax 是最容易看錯的一個。它回傳的不是最大值,而是最大值在那一軸上的位置(從 0 起算),遇到同樣大的值取第一個。不給 axis 時,NumPy 先把陣列攤平成一維再找,所以 A.argmax() 回傳 9,不是 (2, 1);要換回列與欄得用 np.unravel_index(9, A.shape)。分類模型的 probs.argmax(axis=1) 是每一列(每一筆樣本)機率最高的類別編號,結果長度等於樣本數。
3. keepdims:把被壓掉的軸留成長度 1
有時候你希望壓完之後維度數不變,例如每一列的平均要拿回去和原陣列相減。keepdims=True 讓被壓掉的軸留下來、長度變成 1:A.sum(axis=1) 是 (3,),加上 keepdims 變成 (3, 1),一個直行。這個差別在下一步就看得出來:A - A.mean(axis=1, keepdims=True) 能讓每一列各自減掉自己的平均,A - A.mean(axis=1) 卻會因為 (3, 4) 與 (3,) 形狀對不上而丟出 ValueError,背後的規則就是廣播(broadcasting),在姊妹頁「NumPy 廣播」有完整的互動說明。
(3, 4) 一列對一列運算。標準化、歸一化這類「先彙總再拿回去算」的寫法,最常需要它。4. reshape、-1 與 C 順序
reshape 不改變元素,也不改變元素在記憶體中的先後,只改變「每一軸多長」的解讀方式,所以新舊 shape 的元素總數必須相同。填入新形狀的順序預設是 C 順序(列優先,row-major):最右邊的軸變化最快,先填滿第 0 列的每一格,再換第 1 列。某一軸寫 -1,代表「其他軸決定後,這一軸自動算」,-1 只能出現一次,而且必須除得盡,否則會得到 cannot reshape array of size 12 into shape (5,newaxis) 這類 ValueError,訊息裡的 newaxis 就是你寫 -1 的那一軸。能不複製時,reshape 回傳的是 view,修改它會改到原陣列。
轉置 .T 也能把 (3, 4) 變成 (4, 3),但做的事完全不同:它把軸的順序倒過來,原本的第 0 欄變成新的第 0 列,資料的相對位置跟著翻轉。考題常把兩者放在一起當干擾選項,看下圖就知道內容不一樣。
要把多維攤平成一維有三種寫法:reshape(-1)、ravel() 與 flatten(),結果的 shape 都一樣。差別在記憶體:flatten 一定複製一份新的;ravel 和 reshape 能不複製就回傳 view,例如對轉置後的陣列攤平就不得不複製。另外別把 reshape 和 ndarray.resize 搞混,後者會就地改形狀,而且允許增減元素。
5. 一維 (n,)、直行 (n, 1)、橫列 (1, n)
這三種形狀都裝著 n 個數字,卻是三種東西。(n,) 只有一條軸,沒有「直」或「橫」可言,.T 對它沒有任何效果;(n, 1) 是 n 列 1 欄的直行;(1, n) 是 1 列 n 欄的橫列。scikit-learn 的特徵矩陣 X 一律要求二維 (樣本數, 特徵數),就算只有一個特徵也一樣;目標 y 則通常是一維。把一維的身高陣列直接丟進 LinearRegression().fit,會得到 ValueError: Expected 2D array, got 1D array instead,訊息後半段直接建議:只有一個特徵用 reshape(-1, 1),只有一筆樣本用 reshape(1, -1)。兩者 shape 不同,意義也完全相反。
6. 索引會不會讓軸消失
中括號裡每一個位置對應一條軸。放整數,那一軸就被「選定」而消失;放切片(:、0:1、::2),那一軸保留,只是長度可能變短;放 list 或陣列(花式索引,advanced indexing),那一軸也保留,長度等於 list 的長度;放 None(也就是 np.newaxis),就在那個位置插入一條長度 1 的新軸。沒寫到的後面幾軸視同 :,所以 X[0] 等於 X[0, :]。
(4,),用 list 或切片得到方角的直行 (4, 1)。實務上兩者還有一個差別:切片回傳 view,list 索引回傳 copy。7. 一張表與四個步驟
把前面的規則整理成一張表。範例統一假設 X.shape 是 (4, 3),每一列的結果都用 python3 實際執行確認過。
| 類型 | 寫法 | 結果 shape | 規則 |
|---|---|---|---|
| 彙總 | X.sum(axis=0) | (3,) | 被指定的軸消失 |
| 彙總+keepdims | X.sum(axis=1, keepdims=True) | (4, 1) | 被指定的軸留下長度 1 |
| 全部彙總 | X.sum()、X.max() | () | axis=None:所有軸消失,得到純量 |
| 整數索引 | X[0]、X[:, 0] | (3,)、(4,) | 放整數的那一軸消失 |
| list 索引 | X[:, [0]]、X[[0, 2]] | (4, 1)、(2, 3) | 保留那一軸,長度=list 長度(回傳 copy) |
| 切片 | X[:, 0:1] | (4, 1) | 保留那一軸,長度=切到的個數(回傳 view) |
| 插入新軸 | X[np.newaxis]、X[:, :, None] | (1, 4, 3)、(4, 3, 1) | 在 None 的位置插入長度 1 |
| 轉置 | X.T | (3, 4) | 軸的順序倒過來;一維不變 |
| reshape | X.reshape(2, -1) | (2, 6) | 總數不變,-1 自動算 |
| 攤平 | X.reshape(-1)、X.ravel()、X.flatten() | (12,) | 變成一維;flatten 一定複製 |
- 寫下原本的 shape,在每個數字底下標上 axis 編號(0、1、2…,負數從右邊數)。
- 判斷這一行屬於彙總、索引、reshape 或轉置,套用上表的規則,寫出新的 shape。
- 有好幾行就一行一行往下推,不要在腦中一次跳到最後。
- 最後檢查合理性:reshape 前後元素總數相同;要餵給 scikit-learn 的 X 是二維、LSTM 是三維
(批次, 時間步, 特徵)、PyTorch 的 Conv2d 是四維(N, C, H, W)。
8. 完整範例
第一段把 axis、keepdims、argmax 的規則一次跑過;第二段是 reshape、np.newaxis 與 scikit-learn 的二維要求;第三段是深度學習常見的多維形狀。註解裡的輸出都是 NumPy 2.4、scikit-learn 1.8 實際執行的結果。
import numpy as np
A = np.array([[3, 7, 2, 2],
[6, 2, 8, 3],
[3, 9, 5, 4]]) # 3 間分店 × 4 天(示意)
print(A.shape, A.ndim, A.size) # (3, 4) 2 12
print(A.sum(axis=0)) # 劃掉第 0 軸 → (4,):[12 18 15 9]
print(A.sum(axis=1)) # 劃掉第 1 軸 → (3,):[14 19 21]
print(A.sum(axis=-1)) # -1 是最後一軸,這裡等於 axis=1
print(A.mean(axis=1, keepdims=True).shape) # (3, 1):被壓的軸留下長度 1
print(A.argmax(axis=1)) # 每列最大值的位置 [1 2 1]
print(A.argmax()) # 不給 axis:攤平後的位置 9,不是 (2, 1)
print(np.unravel_index(A.argmax(), A.shape)) # 換回 (列, 欄):(np.int64(2), np.int64(1))
import numpy as np
from sklearn.linear_model import LinearRegression
x = np.arange(12)
print(x.reshape(3, 4)[1]) # C 順序:第 1 列是 [4 5 6 7]
print(x.reshape(-1, 2).shape) # -1 = 12 ÷ 2 = 6 → (6, 2)
print(x.reshape(3, 4).T[0]) # 轉置後的第 0 列是原本的第 0 欄 [0 4 8]
print(np.shares_memory(x, x.reshape(3, 4))) # True:reshape 回傳 view
h = np.array([150., 162., 171., 158.]) # 4 位顧客的身高(示意)
w = np.array([48., 55., 66., 52.]) # 體重(示意)
X = h.reshape(-1, 1) # (4,) → (4, 1):4 個樣本、1 個特徵
print(X.shape, h[:, np.newaxis].shape) # (4, 1) (4, 1):兩種寫法相同
model = LinearRegression().fit(X, w) # X 要二維,y 維持一維
print(model.predict([[165.]]).shape) # 預測一筆也要包成二維 → (1,)
import numpy as np
imgs = np.zeros((8, 3, 32, 32)) # 8 張影像(批次, 通道, 高, 寬)
print(imgs[0].shape) # 整數索引讓第 0 軸消失 → (3, 32, 32)
print(imgs[:1].shape) # 切片保留該軸 → (1, 3, 32, 32)
print(imgs.mean(axis=(2, 3)).shape) # 一次壓掉高與寬 → (8, 3)
X = np.zeros((100, 3))
print(X[:, 0].shape, X[:, [0]].shape, X[:, 0:1].shape) # (100,) (100, 1) (100, 1)
print(X.T.shape, X[:, 0].T.shape) # (3, 100);一維轉置沒有效果 (100,)
print(X.flatten().shape) # 攤平成一維 (300,),flatten 一定複製
9. pandas 的 axis 與 groupby
pandas 沿用同一套 axis 觀念:df.sum() 預設 axis=0,每一欄加總、得到以欄名為索引的 Series;df.sum(axis=1)(也可寫 axis="columns")是每一列加總。容易混淆的是 df.drop("a", axis=1):這裡的 axis 指「要刪的標籤在哪一條軸上」,所以 axis=1 是刪掉欄,和彙總函式「沿著哪一軸壓」的讀法不同,建議直接寫 drop(columns="a") 避免誤會。groupby 也可以看成一種壓縮:它不是把整條軸壓成一個值,而是依某一欄的鍵把列分組,每組壓成一列,詳細寫法在地圖的「groupby 與彙總」節點。
容易寫錯或考錯的地方
axis=0 的結果長度等於欄數:(3, 4) 對 axis=0 彙總得到 4 個值。干擾選項常把 axis=0 說成「每一列的總和」,或把 (4,) 寫成 (3,)。
argmax 回傳位置不是值:不給 axis 時回傳攤平後的位置。選項裡出現「最大值本身」或 (列, 欄) tuple 的,多半是陷阱。
整數索引與切片:X[:, -1, :] 少一軸,X[:, -1:, :] 保留長度 1 的軸。LSTM 取最後一個時間步的題目最常考這一對。
一維轉置:v.T 對 (n,) 沒有效果;想得到直行要用 v[:, None] 或 v.reshape(-1, 1)。
reshape(-1, 1) 與 reshape(1, -1):前者是 n 筆樣本、1 個特徵,後者是 1 筆樣本、n 個特徵。只有一個特徵的 scikit-learn 題目選前者。
reshape 不是轉置:形狀相同不代表內容相同,題目若問「第 0 列是什麼」,要分清楚是照順序重填還是列欄互換。
版本差異:NumPy 2 在互動環境顯示純量時會帶型別,例如 A.sum() 顯示 np.int64(54),print 則仍印出 54。axis 超出範圍時丟出 numpy.exceptions.AxisError,它同時是 ValueError 與 IndexError 的子類別。argmax 與 argmin 從 NumPy 1.22 起才支援 keepdims。
✅ 自我檢測
6 題原創的程式閱讀題,每一題的輸出都用 python3 實際執行確認過。選完立即顯示對錯與解析,全部作答後會出現總分。目前得分:0 / 6
🎯 重點整理
- shape 的每個數字是一條軸的長度,由左到右是 axis 0、1、2,也能用 -1 從最後一軸數起。
- 彙總函式的 axis:把 shape 裡那個數字劃掉,就是結果的 shape;axis=None 得到純量 ()。
- keepdims=True 讓被壓掉的軸留下長度 1,方便和原陣列繼續運算。
- argmax 回傳位置;不給 axis 時是攤平後的位置。
- reshape 總數不變、照 C 順序重填,-1 自動算且只能一個;它和 .T 形狀可能相同但內容不同。
- (n,)、(n, 1)、(1, n) 是三種東西;scikit-learn 的 X 要二維,單一特徵用 reshape(-1, 1)。
- 索引:整數讓軸消失,切片與 list 保留,None 插入長度 1。