🗺️ 程式互動式地圖
AI 線・NumPy

NumPy 廣播 broadcasting:形狀不同為什麼也能相加

(4, 3) 減 (3,) 可以,(4, 3) 減 (4,) 卻報錯;標準化 (X − X.mean(axis=0)) / X.std(axis=0) 一行就處理完整張表。這一頁拆開廣播的比對規則,看小陣列怎麼被「虛擬複製」成大陣列,以及它為什麼能取代 for 迴圈。

廣播判定器 虛擬複製動畫 逐欄標準化計算器

💡 先搞懂問題

虛構的「青松補習班」把 4 位學生、3 科的模擬考成績放進陣列 X,shape 是 (4, 3)。班主任想把每一科都換算成「比這一科的平均高或低多少」,先算出每科平均 mu = X.mean(axis=0),shape 是 (3,)。一位助教習慣寫迴圈,用兩層 for 一格一格減;另一位直接寫 X - mu,一行算完,結果一模一樣。接著班主任又想看「每位學生比自己的平均高或低多少」,助教照樣寫 X - X.mean(axis=1),這次卻跳出 ValueError: operands could not be broadcast together with shapes (4,3) (4,)。兩次都是「一張表減一排數字」,為什麼一次可以、一次不行?

新手在這裡常有三個直覺,偏偏都不對:以為那排數字的長度只要「對得上表格的任何一軸」就能算;以為 NumPy 會從左邊開始對齊;以為 X - mu 是先把 mu 複製成 4 列再相減。資料前處理最常見的標準化公式 (X − X.mean(axis=0)) / X.std(axis=0),背後就連用了兩次同一條規則,看懂它,大半的前處理程式都能讀得懂。

這條規則叫廣播(broadcasting)。兩個陣列做逐元素運算(element-wise,例如加減乘除、比較大小、np.maximum)時,如果 shape 不同,NumPy 會從最右邊的軸開始逐軸比對:兩邊長度相等,或其中一邊是 1,就相容,長度 1 的那邊會被「拉伸」去配合;維度數比較少的一邊,先在左邊補 1。只要有一軸兩邊都不是 1 又不相等,就丟出 ValueError。拉伸只是概念上的,NumPy 並不會真的複製資料。像這樣把整張表一次交給 NumPy、不寫 Python 迴圈的寫法,叫做向量化(vectorization)。

X (4, 3) X (4, 3) − − mu (3,) 每科平均 (4,) 每位學生的平均 可以:結果 (4, 3) (4, 3) ( 3) ← 最右邊 3 對 3 mu 沿著 4 列套用,每科減自己的平均 報錯:ValueError (4, 3) ( 4) ← 最右邊 3 對 4 operands could not be broadcast together
上下兩排都是「一張表減一排數字」,差別在那排數字的長度對到哪一軸。NumPy 永遠先拿最右邊的軸比:上排 3 對 3,下排 3 對 4。下排那 4 個數其實是想對著 4 列套用,只是 NumPy 不會自己猜,你得把它改成直的 (4, 1)。

生活比喻:調分小紙條

期末時,老師要替全班 40 位學生的國文、英文、數學都加上「每科的調分」:國文加 2、英文加 5、數學加 3。她不必影印 40 份調分表,只要把一張寫著三格的小紙條對準成績單第一列,加完往下挪一列,一路挪到最後一位。小紙條的三格剛好對著三個科目,這就是它能一路往下套用的原因。

後來教務處又給了一份「每位學生的出席加分」,40 個數字寫成一長條橫的紙。老師把它橫著對成績單,40 格對 3 個科目,怎麼對都不對;把紙條轉成直的、貼在成績單左邊,再一科一科往右套用,才是正確做法。

每科調分:紙條往下挪 國文英文數學 ⋮ 第 1 位第 2 位第 3 位第 40 位 +2+5+3 往下挪 (40, 3) + (3,) → (40, 3) 紙條三格對著三科:最右邊 3 對 3 每人出席加分:要轉成直的 … 40 格橫著放 (40, 3) + (40,) ✗ 40 對 3 ⋮ 往右 套用 (40, 3) + (40, 1) → (40, 3) 紙條只有一張,老師沒有影印;NumPy 連影本都不做(示意)
左邊黃色紙條只有三格,虛線框代表「挪過去再用一次」;右邊紅色的長紙條橫著放,40 格對不上 3 個科目,轉成綠色的直條之後,每一列只有一個數,就能往右套用到三科。
回到程式:成績單是 (40, 3) 的陣列,調分紙條是 (3,)。NumPy 從最右邊比對:3 對 3 相等;紙條少了第 0 軸,就在左邊補 1 變成 (1, 3),再沿著第 0 軸拉伸成 40 列,這就是 scores + bonus。出席加分是 (40,),最右邊 40 對 3 對不上,所以報錯;「轉成直的」就是變成 (40, 1),寫成 attend[:, None] 或 attend.reshape(-1, 1),最右邊 1 對 3 可以拉伸,結果是 (40, 3)。開頭那個報錯的 X - X.mean(axis=1),也是同樣的修法。

比喻有三個地方和實際不同。第一,老師挪紙條是一列一列做,NumPy 也確實逐格計算,但迴圈跑在編譯過的 C 程式裡,Python 只下一次指令,所以快得多。第二,老師手上至少有一張實體紙條,廣播則連「影本」都不做:拉伸出來的那些列,是用步長(stride)0 一再讀取同一塊記憶體,實驗室二會畫給你看。第三,廣播只用在逐元素運算,矩陣乘法 @ 有自己的 shape 規則(內側維度要相等),不能拿廣播的規則去套。

🎮 互動實驗室一:廣播判定器

替 a 與 b 各挑一個 shape(純量,或 1 到 3 維、每軸長度 1~5)。判定器會把兩個 shape 靠右對齊,從最右邊那一軸開始一軸一軸比對:綠色是長度相等,黃色是其中一邊是 1、會被拉伸,虛線格是維度不足時在左邊補上的 1,紅色是不相容。最下面一列是結果的 shape;只要有一軸不相容,就會顯示 NumPy 實際丟出的錯誤訊息。也可以直接按下方的常見組合。

a.shape

b.shape



  
畫面說明:載入中。

🎮 互動實驗室二:虛擬複製動畫

選一個情境,按「下一步」或「自動播放」。動畫分四步:先看兩個陣列真正存在記憶體裡的樣子;再把 shape 靠右對齊;接著畫出被「虛擬複製」的格子(虛線、半透明),它們其實全都指回原本那幾格;最後逐格運算,得到結果。下方的 Python 與輸出都和實際執行的結果一致。

① 原本的陣列② 靠右對齊③ 虛擬複製④ 逐格運算


  
畫面說明:載入中。

🎮 互動實驗室三:逐欄標準化計算器

表格是青松補習班 4 位學生的三科成績(示意),可以直接改數字。按「下一步」走一遍 (X − X.mean(axis=0)) / X.std(axis=0):每一步會顯示那一行 Python、結果的 shape 與數值,黃色的列或欄是正在被廣播的向量。上方可以切換 axis,看看改成「每位學生」或「整張表」會怎樣;也可以切換 ddof,比較 NumPy 預設的 ddof=0 與 pandas 預設的 ddof=1。

axis
ddof
X(4, 3)


  
畫面說明:載入中。

📘 原理補完

1. 三條規則與結果的 shape

NumPy 官方文件對廣播的描述是:兩個陣列運算時,從最後一軸(最右邊)開始逐一比較 shape,往左進行;兩個維度在「相等」或「其中一個是 1」時相容。整理成三條規則:一、維度數不同時,在較短的 shape 左邊補 1,直到兩邊一樣長;二、逐軸比對,相等或其中一邊是 1 就相容,否則丟出 ValueError: operands could not be broadcast together with shapes …;三、結果的每一軸取兩邊較大的長度,長度 1 的那一邊被拉伸。官方文件舉的例子是 (8, 1, 6, 1) 與 (7, 1, 5),結果是 (8, 7, 6, 5),可以用 np.broadcast_shapes 直接驗算。

倒數第 3 軸倒數第 2 軸最右軸 a (4, 3) 4 3 b (3,) 1 3 a (4, 3) 4 3 b (4,) 1 4 a (2, 1, 4) 2 1 4 b (3, 4) 1 3 4 最右軸 3 = 3,相等 左邊 b 補 1,拉伸成 4 結果 (4, 3) 每一列都減同一組 3 個數 最右軸 3 對 4:都不是 1、也不相等 ValueError(左邊不用再看) 4 = 4;a 的 1 拉伸成 3 b 左邊補 1,拉伸成 2 結果 (2, 3, 4) 綠=相等 黃=長度 1 被拉伸 虛線=左邊補上的 1 紅=不相容
三個例子都只看「靠右對齊後的同一欄」。第一個例子 b 沒有第 0 軸,補一個 1 再拉伸;第二個例子在最右軸就失敗,所以報錯訊息只會告訴你兩個 shape;第三個例子兩邊各被拉伸了一軸,結果比兩個輸入都大。

2. 只有長度 1 能拉伸

拉伸只會發生在長度剛好是 1 的軸上。(2,) 不會自動重複兩次去配合 (4,),(2, 3) 和 (3, 2) 也不會自動轉置;這些情況一律報錯。真的需要重複,要自己用 np.tile 或 np.repeat,它們會真的配置新的記憶體。維度不足時補的 1 一定加在左邊,所以一維的 (3,) 永遠被當成「一列」(1, 3),不會被當成直行;想要直行,必須自己寫 [:, None]。

mu (3,) 76 80 80 (1, 3) 76 80 80 (4, 3) 76 80 80 768080 768080 768080 左邊補 1 拉伸 一維:只有長度 一列 3 欄 實線=真的存在;虛線=虛擬 補的 1 一定在左邊, 所以 (3,) 被當成「一列」
每科平均 mu 從 (3,) 變成 (1, 3),再沿著第 0 軸拉伸成 4 列,才和 X 的 (4, 3) 對齊。右邊只有最上面那列是實線:其他三列是 NumPy 在計算時重複讀同一列,不是真的抄了三份。

3. 兩邊都被拉伸:結果比兩個輸入都大

當一邊是 (3, 1)、另一邊是 (4,),補 1 之後是 (3, 1) 對 (1, 4),兩邊各有一軸是 1,各自被拉伸,結果是 (3, 4),等於把每一列的值和每一欄的值兩兩組合。刻意這樣寫時很好用,例如一次算出所有點兩兩之間的差;不小心發生時卻是最難抓的錯誤,因為它不會報錯。典型情況是迴歸題的 y_true 是 (n, 1)、y_pred 是 (n,),y_true - y_pred 默默變成 (n, n),算出來的 MSE 完全不對,n 很大時還會吃光記憶體。

c (3, 1) 0 10 20 + r (4,) 1 2 3 4 = (3, 4) 1 2 3 4 11 12 13 14 21 22 23 24 c 往右拉伸成 4 欄 r 往下拉伸成 3 列 每一格 = 該列的 c   + 該欄的 r 不會報錯的陷阱 y_true (n, 1) − y_pred (n,) → (n, n) n=3 時:真正的 MSE ≈ 1.4167,誤寫的結果是 2.8611(示意資料,python3 實算) 修法:y_true.ravel() 或 y_pred.reshape(-1, 1),先讓兩邊 shape 一致
上半部是刻意使用的「兩邊都拉伸」:3 個值與 4 個值兩兩組合成 12 格。下半部是同一個規則的副作用:兩個本來都只有 n 個數的陣列,相減後變成 n × n。看到結果比兩個輸入都大,就要停下來確認這是不是你要的。

4. 不會真的複製:步長 0

ndarray 用步長(strides)記錄「沿著每一軸走一格,要在記憶體裡跳多少 bytes」。np.broadcast_to(mu, (4, 3)) 可以把廣播的虛擬陣列拿出來看:它的 strides 是 (0, 8),往右一格跳 8 bytes(一個 float64),往下一列跳 0 bytes,也就是一直讀同一列。這個陣列是唯讀的(flags.writeable 為 False),因為好幾個位置共用同一格記憶體,寫進去會互相影響。所以廣播本身幾乎不花額外記憶體;真正會配置新記憶體的是運算的結果,例如 X - mu 會產生一個 (4, 3) 的新陣列,意外的 (n, n) 也會真的佔滿 n × n 格。

還有一個相關的陷阱:就地運算 a += b 要把結果寫回 a,所以廣播後的 shape 必須等於 a 原本的 shape。a 是 (3,)、b 是 (3, 1) 時,a + b 可以算出 (3, 3),a += b 卻會丟出 non-broadcastable output operand with shape (3,) doesn't match the broadcast shape (3,3)。

np.broadcast_to(mu, (4, 3)) 第 0 列第 1 列第 2 列第 3 列 每一列都讀同一段 76 80 80 記憶體裡只有 3 格;strides=(0, 8) np.tile(mu, (4, 1)) 768080 768080 768080 768080 真的配置 12 格新記憶體 strides=(24, 8),可以寫入 資料一大,差別就很明顯(示意)
左邊的虛線格是「看起來有 12 格」,箭頭顯示它們全都指回下方同一段記憶體,往下一列的步長是 0;右邊實線格是 np.tile 真的抄出來的 12 格。寫 X - mu 時,NumPy 用的是左邊的方式,你不需要也不應該自己先 tile。

5. 標準化背後在做什麼

標準化(standardization,又稱 z-score)把每一欄換算成「離平均幾個標準差」,讓量尺不同的特徵可以放在一起比較,KNN、SVM、K-means、PCA 與神經網路都需要它。(X − X.mean(axis=0)) / X.std(axis=0) 這一行其實有四個動作、兩次廣播:先沿 axis=0 壓出每欄平均 (3,),用廣播讓每一列都減掉它;再壓出每欄標準差 (3,),用廣播讓每一列都除以它。結果每一欄的平均是 0、標準差是 1。注意運算子的優先順序:少了括號寫成 X - X.mean(axis=0) / X.std(axis=0),會變成先除再減,shape 一樣是 (4, 3),數值卻完全不對。

X(4, 3) mu = X.mean(axis=0)(3,) [76 80 80] D = X − mu(4, 3) sd = X.std(axis=0)(3,) [8.944 …] Z = D / sd(4, 3) 廣播 ①廣播 ② 每一欄的平均變成 0、標準差變成 1(sd 用 ddof=0,數值示意、python3 實算)
黃色的兩個框都是 axis=0 的彙總,shape 從 (4, 3) 變成 (3,);紫色的兩個框都是廣播,(3,) 被拉伸回 (4, 3) 和 X 逐格運算。實驗室三就是把這張圖一步一步走過一次。

標準差有兩種分母,這是最容易對不上答案的地方。母體標準差除以 n,樣本標準差除以 n − 1,函式用 ddof(delta degrees of freedom,自由度修正)參數控制:分母是 n − ddof。各工具的預設值不一樣,同一份資料算出來就不同:

工具預設 ddof分母國文欄(72、88、64、80)的標準差備註
np.std、X.std()0n8.944母體標準差;要樣本版寫 ddof=1
DataFrame.std、Series.std1n − 110.328pandas 預設樣本標準差;要母體版寫 ddof=0
StandardScaler0(固定)n8.944官方文件說明採用有偏估計,等同 numpy.std(x, ddof=0)
statistics.pstdev/stdev—n/n − 18.944/10.328Python 標準函式庫,分成兩個函式

資料筆數很多時兩者差異很小,但程式題常故意用 4、5 筆小資料,答案就會差一截。另一個邊界情況是某一欄全部相同、標準差為 0:自己用 NumPy 除會得到 nan 並出現 RuntimeWarning: invalid value encountered in divide;StandardScaler 則會把這一欄的縮放係數設為 1,結果是全 0,不會產生 nan。實務上標準化的平均與標準差只能從訓練集計算,再套用到測試集,這屬於資料洩漏的議題,延伸閱讀有完整的逐行解說。

6. 每一列各自運算:keepdims 或 [:, None]

想讓每位學生減掉自己的平均,X.mean(axis=1) 是 (4,),補 1 會補在左邊變成 (1, 4),最右軸 3 對 4 對不上。解法是讓它變成直的 (4, 1):計算時加 keepdims=True,或事後寫 [:, None]、.reshape(-1, 1)。pandas 的規則又不一樣:DataFrame 和 Series 運算時是依標籤對齊,不是依位置廣播。df - df.mean() 依欄名對齊,剛好是每科減自己的平均;df - df.mean(axis=1) 卻會拿學生的列索引 0~3 去對欄名,對不到就全部變成 NaN,結果還多出 4 個欄位(實測 shape 從 (4, 3) 變成 (4, 7))。pandas 要每列各自運算,寫 df.sub(df.mean(axis=1), axis=0)。

X − X.mean(axis=0) X − X.mean(axis=1) … axis=1, keepdims=True (3,) (4,) (4, 1) ✓ 每科減自己的平均 最右軸 3 對 3 ✗ ValueError 補 1 在左邊:3 對 4 ✓ 每位學生減自己的平均 最右軸 1 拉伸成 3
中間那排紅色的 4 個值橫著放,比表格多出一格,正是錯誤訊息裡的 (4,3) 與 (4,)。右邊把同樣 4 個值變成直的綠色欄,長度 1 的那一軸往右拉伸,每位學生就能減掉自己的平均。也可以寫成 X - X.mean(axis=1)[:, None]。

7. 向量化取代 for 迴圈

不用廣播時,「每格減掉該欄平均」要寫兩層 for 迴圈,每一格都要經過一次 Python 直譯器:取值、查型別、相減、存回去。向量化的 X - mu 只讓 Python 下一次指令,真正一格一格算的是 NumPy 內部編譯過的 C 迴圈,速度常差到數十倍以上(依電腦與資料大小而定),程式也短得多、不容易寫錯索引。限制是:向量化會產生完整的中間陣列,資料非常大時要留意記憶體;前一步結果會影響下一步的計算(例如遞迴式的累加)不一定能直接改寫成一行。

兩層 for 迴圈 Python直譯器 每一格都要經過直譯器一次 (圖中只畫出 4 條,實際是 12 次) out[i, j] = X[i, j] - mu[j] 向量化 Python一次指令 NumPy 的 C 迴圈 Python 只下一次指令 逐格運算在 C 裡一口氣跑完 out = X - mu
兩邊算出來的結果完全相同(範例程式第三段用 np.allclose 驗證),差在誰來跑迴圈。左邊每一格都要 Python 親自處理;右邊 Python 只交代一次,剩下交給 NumPy。資料只有 12 格時感覺不出差別,十萬列時就很明顯。

8. 一張表與判斷步驟

常見的 shape 組合整理如下,每一列都用 np.broadcast_shapes 與實際相加驗證過。

a.shapeb.shape靠右對齊後結果說明
(4, 3)(3,)(4, 3) 對 (1, 3)(4, 3)每一列套用同一組數,例如減每欄平均
(4, 3)(4,)(4, 3) 對 (1, 4)ValueError最右軸 3 對 4;要改成 (4, 1)
(4, 3)(4, 1)(4, 3) 對 (4, 1)(4, 3)每一列套用自己的值,例如減每列平均
(4, 3)(1, 3)(4, 3) 對 (1, 3)(4, 3)和 (3,) 的效果相同
(4, 3)() 純量(4, 3) 對 (1, 1)(4, 3)純量可以和任何 shape 運算
(3, 1)(4,)(3, 1) 對 (1, 4)(3, 4)兩邊都拉伸,結果比兩邊都大
(2, 3, 4)(3, 1)(2, 3, 4) 對 (1, 3, 1)(2, 3, 4)三維也一樣從右邊比
(2, 1, 4)(3, 4)(2, 1, 4) 對 (1, 3, 4)(2, 3, 4)兩邊各拉伸一軸
(2, 3)(3, 2)(2, 3) 對 (3, 2)ValueError不會自動轉置
(8, 1, 6, 1)(7, 1, 5)(8, 1, 6, 1) 對 (1, 7, 1, 5)(8, 7, 6, 5)官方文件的例子
① 寫出兩個 shape ② 靠右對齊,較短的左邊補 1 ③ 從最右軸往左,每一軸: 長度相等 → 保留 有一邊是 1 → 拉伸 都不是 → ValueError ④ 結果每一軸取較大者 ⑤ 結果比兩邊都大? 確認不是意外的 (n, n)
只要在第 ③ 步遇到一個紅框就可以停了,答案是 ValueError。全部通過後,第 ⑤ 步是實務上最該多看一眼的地方:廣播成功不代表結果正確。
  1. 寫出兩個 shape,靠右對齊,維度數少的一邊在左邊補 1。
  2. 從最右軸往左逐軸比對:相等保留、有一邊是 1 就拉伸、都不是就報錯。
  3. 結果的每一軸取兩邊較大的長度。
  4. 檢查合理性:結果是不是比兩個輸入都大?是就停下來確認;想讓「每一列」各自運算時,向量要先變成 (n, 1)。

9. 完整範例

第一段驗證廣播規則與步長 0;第二段是標準化與 ddof 的對照,順便看 pandas 的標籤對齊;第三段是 (n, 1) 與 (n,) 的陷阱、迴圈與向量化的對照,以及就地運算的限制。註解裡的輸出都是 NumPy 2.4、pandas 3.0、scikit-learn 1.8 實際執行的結果。

import numpy as np
X = np.array([[72., 85., 60.],
              [88., 70., 95.],
              [64., 90., 75.],
              [80., 75., 90.]])         # 4 位學生 × 3 科(示意)
mu = X.mean(axis=0)                     # (3,):每科平均 [76. 80. 80.]
print((X - mu).shape)                   # (4, 3) 與 (3,):最右邊 3 對 3 → (4, 3)
print(np.broadcast_shapes((4, 3), (3,)), np.broadcast_shapes((3, 1), (4,)))  # (4, 3) (3, 4)
try:
    X - X.mean(axis=1)                  # (4, 3) 與 (4,):最右邊 3 對 4 → 不相容
except ValueError as e:
    print(e)                            # operands could not be broadcast together with shapes (4,3) (4,)
row_c = X - X.mean(axis=1, keepdims=True)    # (4, 1) 可以沿著欄的方向拉伸
print(row_c.shape, np.allclose(row_c, X - X.mean(axis=1)[:, None]))  # (4, 3) True
v = np.broadcast_to(mu, (4, 3))         # 看看「虛擬複製」出來的陣列
print(v.strides, v.flags.writeable)     # (0, 8) False:往下一列跳 0 bytes,沒有真的複製
import numpy as np, pandas as pd
from sklearn.preprocessing import StandardScaler
X = np.array([[72., 85., 60.],
              [88., 70., 95.],
              [64., 90., 75.],
              [80., 75., 90.]])
Z = (X - X.mean(axis=0)) / X.std(axis=0)     # 逐欄標準化:用了兩次廣播
print(Z[0].round(3))                         # 第 1 位學生:[-0.447  0.632 -1.461]
print(Z.mean(axis=0).round(6), Z.std(axis=0))    # 每欄平均 0、標準差 1
print(X.std(axis=0).round(3))                # NumPy 預設 ddof=0:[ 8.944  7.906 13.693]
df = pd.DataFrame(X, columns=['國文', '英文', '數學'])
print(df.std().round(3).tolist())            # pandas 預設 ddof=1:[10.328, 9.129, 15.811]
print(np.allclose(StandardScaler().fit_transform(X), Z))  # True:StandardScaler 用 ddof=0
print((df - df.mean()).shape)                # 依欄名對齊,每科減自己的平均 → (4, 3)
print(df.sub(df.mean(axis=1), axis=0).shape) # 每位學生減自己的平均要指定 axis=0 → (4, 3)
import numpy as np
y_true = np.array([[3.], [5.], [2.]])   # (3, 1):不小心多了一軸
y_pred = np.array([2.5, 5., 4.])        # (3,)
diff = y_true - y_pred                  # (3, 1) 與 (3,):兩邊都拉伸 → (3, 3)
print(diff.shape, np.mean(diff ** 2))   # (3, 3) 2.861…:不會報錯,但不是 MSE
print(np.mean((y_true.ravel() - y_pred) ** 2))  # 先攤平成 (3,):正確的 MSE ≈ 1.4167
X = np.random.default_rng(0).random((1000, 3))
out = np.empty_like(X)
for i in range(X.shape[0]):             # 迴圈版:一格一格減
    for j in range(X.shape[1]):
        out[i, j] = X[i, j] - X[:, j].mean()   # 每一格都重算一次平均
print(np.allclose(out, X - X.mean(axis=0)))    # True:向量化一行得到同樣結果
a = np.zeros(3)
try:
    a += np.ones((3, 1))                # 就地運算:結果 (3, 3) 放不回 (3,) 的 a
except ValueError as e:
    print(e)                            # non-broadcastable output operand with shape (3,) ...

容易寫錯或考錯的地方

從右邊對齊:判斷 (4, 3) 與 (4,) 時,很多人看到「4 對 4」就以為可以。NumPy 先比的是最右軸 3 對 4,所以報錯;選項寫「會自動對到第 0 軸」的是陷阱。

(n, 1) 減 (n,):不報錯、結果變成 (n, n)。題目若問 shape,答案比兩個輸入都大;若問 MSE,數值會不對。

標準化的括號:X - X.mean(axis=0) / X.std(axis=0) 先除後減,shape 一樣但數值錯。干擾選項常只差一組括號。

ddof:NumPy 與 StandardScaler 是 ddof=0,pandas 是 ddof=1。用 pandas 算標準差再拿去和 StandardScaler 的結果比,會對不上。

pandas 不是位置廣播:DataFrame 減 Series 依標籤對齊,df - df.mean(axis=1) 會得到一片 NaN;每列運算用 df.sub(s, axis=0)。

廣播 vs 矩陣乘法:* 是逐元素相乘、會廣播;@ 是矩陣乘法,要求 (a, b) @ (b, c) 的內側維度相等,兩者的 shape 規則不能混用。

就地運算:a += b 的結果必須放得回 a 的 shape,否則報 non-broadcastable output operand。

✅ 自我檢測

6 題原創的程式閱讀題,每一題的輸出都用 python3 實際執行確認過。選完立即顯示對錯與解析,全部作答後會出現總分。目前得分:0 / 6

🎯 重點整理

  1. 廣播只用在逐元素運算:從最右軸開始比,相等或其中一邊是 1 才相容,否則 ValueError。
  2. 維度不足時在左邊補 1,所以 (3,) 一律被當成一列;要直行得寫 [:, None] 或 keepdims=True。
  3. 結果每一軸取較大者;兩邊都被拉伸時結果比輸入都大,(n, 1) 減 (n,) 會默默變成 (n, n)。
  4. 拉伸是虛擬的:步長 0、唯讀、不複製;真正佔記憶體的是運算結果。
  5. 標準化 (X − X.mean(axis=0)) / X.std(axis=0) 是兩次「彙總成 (3,),再廣播回 (4, 3)」。
  6. np.std 與 StandardScaler 用 ddof=0,pandas std 用 ddof=1;pandas 依標籤對齊,不是位置廣播。
  7. 向量化把迴圈交給 C,結果相同、程式更短、速度常快數十倍。