傻傻分不清楚的點積與矩陣乘法 Part3

2024年2月6日 18点热度 0人点赞

作者:Minkyung Kang

譯者:知源覓流

原文鏈接:
https://github.com/mkang32/python-basics/blob/master/numpy/dot_vs_multiply_vs_matmul_vs_at.ipynb

3. NumPy數組有哪些可用的功能?

我們的目標是在 NumPy 中找到執行點積或矩陣乘法的最佳方法。我比較了三個不同類別中的五種不同選項:

  1. 元素乘法(element-wise multiplication):*np.multiply 加上 np.sum
  2. 點積:np.dot
  3. 矩陣乘法:np.matmul, @

我們將根據向量/矩陣的維度來探討不同的情況,並理解每種方法的優缺點(the pros and cons of each method)。要在接下來的部分中運行代碼,我們首先需要導入 numpy。

import numpy as np

(1) 元素乘法:*和sum

首先,我們可以嘗試將元素乘法作為基本方法來實現點積:將兩個向量中的對應元素相乘,然後將所有輸出值相加。這種方法的缺點是你需要分別進行乘法和加法運算,導致它比我們稍後將討論的其他方法慢。

這是一個使用兩個1-D數組計算點積的示例。

a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
>>> a*b
array([ 4, 10, 18])
>>> sum(a*b)
32
>>> np.sum(a*b) #譯者添加
32

讓我們看看2-D數組矩陣乘法的示例。

c = np.array([[1, 2, 3], [4, 5, 6]])
d = np.array([1, 1, 1])
>>> c*d
array([[1, 2, 3],
       [4, 5, 6]])

在這裡,二維數組 c 的每一行都被視為矩陣的一個元素,並與第二個數組 d 進行逐元素相乘。如下所示。

如果我們想要的是矩陣乘法的話,結果應該是這樣:

因此,為了得到想要的輸出,你需要對初始輸出應用 np.sum。請註意,你應該傳遞參數 axis=1,它會對同一行中的元素求和。否則,因為默認值是axis=None,它對數組中的所有元素求和(譯者訂)。(譯者註:axis=0表示跨行(Y軸)的方向,axis=1表示跨列(X軸)的方向)

>>> np.sum(c*d, axis=1)
array([ 6, 15])

譯者註:

你可能會問,為什麼不用sum了呢?這是因為如果你繼續用剛才用過的sum函數,就得不到想要的結果了。

>>> sum(c*d)

array([5, 7, 9])

此時,你可能被sum和np.sum繞暈了。從下面的簡介可以看出,sum是Python內置的函數,用於求和,功能有限。np.sum是numpy提供的求和函數,功能相對強大。所以,一般建議用np.sum。

對sum的簡介。

sum(iterable, /, start=0)

Return the sum of a 'start' value (default: 0) plus an iterable of numbers

對np.sum的簡介。

sum(a, axis=None, dtype=None, out=None, keepdims=<no value>, initial=<no value>, where=<no value>)

Sum of array elements over a given axis.

(2) 元素乘法:np.multiply和sum

np.multiply* 基本上是一樣的。它是NumPy的元素乘法版本,而不是Python的本地運算符。你需要 sum 函數求和才能得到最終的標量輸出。

a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
>>> np.multiply(a, b)
array([ 4, 10, 18])
>>>  np.sum(np.multiply(a, b))
32

(3) 點積:np.dot

在Numpy中有一種更優雅和簡單的方法來計算點積,它就是np.dot(a, b)a.dot(b)。它可以同時處理元素乘法和求和。簡單易用。

a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
>>> np.dot(a, b)
32

然而,當它是一個更高維度的數組時,你需要小心。如果數組的維度為2-D或更高,請確保第一個數組的列數與第二個數組的行數相匹配。

a = np.array([[1, 2, 3]])  # shape (1, 3)
b = np.array([[4, 5, 6]])  # shape (1, 3)
>>> np.dot(a, b)  
# ValueError: shapes (1,3) and (1,3) not aligned: 3 (dim 1) != 1 (dim 0)

為了讓上述示例運行,你需要轉置第二個數組,以便形狀對齊:(1, 3) x (3, 1)。請註意,這將返回形為(1, 1)的數組,這是一個2-D數組。

a = np.array([[1, 2, 3]])  # shape (1, 3)
b = np.array([[4, 5, 6]])  # shape (1, 3)
>>> np.dot(a, b.T)  
array([[32]])

如果第二個數組是形狀為(3,)的1-D數組,那麼輸出的數組也會是1-D數組。

a = np.array([[1, 2, 3]])  # shape (1, 3)
b = np.array([4, 5, 6])  # shape (3, )
>>> np.dot(a, b)  
array([32])

還要註意輸入數組的順序。如果順序相反,你會得到外積(outer product)而不是內積(inner product)(點積)。(譯者註:一個行向量乘以一個列向量稱作向量的內積,又叫作點積,結果是一個標量;一個列向量乘以一個行向量稱作向量的外積,結果是一個矩陣

a = np.array([[1, 2, 3]])  # shape (1, 3)
b = np.array([[4, 5, 6]])  # shape (1, 3)
>>> np.dot(a.T, b)  # (3, 1) x (1, 3) 
array([[ 4,  5,  6],
       [ 8, 10, 12],
       [12, 15, 18]])

那麼np.dot方法也適用於2-D數組×2-D數組嗎?現在讓我們嘗試一個2D x 2D的例子。

c = np.array([[1, 2, 3], [4, 5, 6]])  # shape (2, 3)
d = np.array([[1], [1], [1]])  # shape (3, 1)
>>> np.dot(c, d)
array([[ 6],
       [15]])

它起作用了!即使它被稱為點積,根據其定義,這表示輸入是1-D向量,輸出是標量,但它對2-D或更高維度的矩陣也起作用,就像它是矩陣乘法一樣。上面例子的計算過程如下所示。

*np.multiply是不支持這樣計算的,所以np.dot絕對是一個改進。那麼,我們應該把np.dot用於所有的點積和矩陣乘法嗎?

從技術上講,可以,但並不推薦使用np.dot進行矩陣乘法,因為“點積”這個名稱有特定的含義,可能會讓讀者感到困惑,尤其是數學傢!

此外,對於高維矩陣(3-D或更高),不推薦使用 np.dot,因為它的行為與普通矩陣乘法不同。我們將在本文的後面部分討論這個問題。

因此,np.dot 既適用於點積也適用於矩陣乘法,但僅建議用於點積。

(4) 矩陣乘法:np.matmul

下一個選項是 np.matmul。它專為矩陣乘法而設計,名字也是由此得來(MATrix MULtiplication)。盡管名稱說的是矩陣乘法,但它也適用於 1-D 數組,就像 np.dot 一樣。下面讓我們嘗試一下之前測試 np.dot 的例子。可以看出,對於1-D和2-D數組,np.matmulnp.dot 的功能是一樣的。

# 1D array
a = np.array([1, 2, 3])  # shape (1, 3)
b = np.array([4, 5, 6])  # shape (1, 3)
>>> np.matmul(a, b)
32
# 2D array with values in 1 axis
a = np.array([[1, 2, 3]])  # shape (1, 3)
b = np.array([[4, 5, 6]])  # shape (1, 3)
>>> np.dot(a, b.T) 
array([[32]])
# 2D arrays
c = np.array([[1, 2, 3], [4, 5, 6]])  # shape (2, 3)
d = np.array([[1], [1], [1]])  # shape (3, 1)
>>> np.dot(c, d)
array([[ 6],
       [15]])

太好了!因此,這意味著np.dotnp.matmul都可以完美地用於點積和矩陣乘法。然而,正如我們之前所說,建議使用np.dot進行點積運算,使用np.matmul進行2-D或更高維度的矩陣乘法。

(5 ) 矩陣乘法:@

最後一個選項來了!@是自Python 3.5以來引入的新運算符,其名稱來自mATrices。它基本上與 np.matmul 相同,並旨在執行矩陣乘法。但是,如果我們已經有了完美的 np.matmul,為什麼還需要新的中綴運算符呢?

向stdlib添加新運算符的主要動機是矩陣乘法是一個非常常見的運算,它應該擁有自己的中綴運算符。例如,運算符 // 遠不如矩陣乘法常見,但仍擁有自己的中綴。要了解此添加的背景,請查看PEP 465 (
https://www.python.org/dev/peps/pep-0465/)。

# 1D array
a = np.array([1, 2, 3])  # shape (1, 3)
b = np.array([4, 5, 6])  # shape (1, 3)
>>> a @ b  
32
# 2D array with values in 1 axis
a = np.array([[1, 2, 3]])  # shape (1, 3)
b = np.array([[4, 5, 6]])  # shape (1, 3)
>>> a @ b.T
array([[32]])
# 2D arrays
c = np.array([[1, 2, 3], [4, 5, 6]])  # shape: (2, 3)
d = np.array([[1], [1], [1]])  # shape: (3, 1)
>>> c @ d
array([[ 6],
       [15]])

因此,@ 的工作原理和 np.matmul 完全一樣。但是在 np.matmul@ 之間應該使用哪一個呢?盡管這是你的偏好,但在代碼中 @ 看起來比np.matmul 更幹凈。例如,如果你想對三個不同的矩陣 xyz 執行矩陣乘法。那麼下面是不同的方式:

# `np.matmul` version
np.matmul(np.matmul(x, y), z)
# `@` version
x @ y @ z

如你所見,@ 操作符更為簡潔、易讀。然而,由於該操作符僅在Python 3.5及以上版本可用,如果你使用的是更早的Python版本,你必須使用np.matmul



薈萃知識,滋養你我。