LKY 只有原創內容的 Blog

今之能者,謂能轉貼,至於魯蛇,皆能轉貼。不原創,何以別乎?

用 NumPy 向量化加速 Python:從3千萬剩男中找出比我高 16 倍

Lin, Kao-Yuan's Avatar 2023-03-08

  1. 1. 入門 for loop 寫法,速度定義為 1x
  2. 2. 用 NumPy 向量化計算,速度 16x

Python 簡單易用,可以很快速的驗證各種演算法。但是到了真的要實際應用時,Python 的毛病就會凸顯,往往就是慢!而且慢的很難受。

我也熟悉 Go 或 C# 這些靜態型別的語言,但是欠缺各式各樣方便好用的套件,難以快速的驗證各種演算法。

這篇文章我們來看看如何用 NumPy 來加速 Python 的效能,並且用一個簡單的例子來說明。


本文要介紹的是 NumPy 當中一種叫做「Boolean array indexing」的技巧,官方文件的連結如下:


需求: 找出身高大於 178 cm 的資料

入門 for loop 寫法,速度定義為 1x

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import NumPy as np
import time

# 中國有 3000 萬剩男,所以我隨機產生 3000 萬個身高數據
np.random.seed(0) # 固定隨機種子,讓每次執行的結果都一樣,方便大家比較
heights = np.random.randint(150, 200, 30000000) # 上下界設定為 150~200 cm

# 需求: 找出身高大於 178 cm 的資料
# 因為我的身高是 178 cm,所以我想抓出身高比我能吸引的女人的樣本

# 使用 for 迴圈 (最慢)
start_time = time.time()
heights_above_178 = []
for height in heights:
if height > 178:
heights_above_178.append(height)
elapsed_time_for_loop = time.time() - start_time
print("檢驗結果: 高於178的人數為 %s" % (len(heights_above_178)))
print("Elapsed time for loop: %s seconds" % (elapsed_time_for_loop))

輸出如下:

1
2
檢驗結果: 高於178的人數為 126011670
Elapsed time for loop: 44.088807344436646 seconds

上面這個程式碼,就是一般初學者最直覺,用 for loop 寫出來的程式碼,但是效能很差,因為:

  • Python 是動態型別,所以每次迴圈都要去判斷型別,並且要去做型別轉換。
  • 而且 for 迴圈一次只能處理一個數,數據頻繁的進出往來 RAM 與 CPU,效能也很差。

用 NumPy 向量化計算,速度 16x

1
2
3
4
5
6
7
# 使用向量化 (最快)
# 以上重複部份程式碼省略
start_time = time.time()
heights_above_178 = heights[heights > 178]
elapsed_time_vectorlize = time.time() - start_time
print("檢驗結果: 高於178的人數為 %s" % (len(heights_above_178)))
print("Elapsed time vectorlize: %s seconds" % (elapsed_time_vectorlize))

輸出如下:

1
2
檢驗結果: 高於178的人數為 126011670
Elapsed time vectorlize: 2.701777935028076 seconds

上面這個程式碼,直接把整個陣列的數值都拿出來比較,然後再把結果存回陣列,這樣就不用一個一個比較了,效能就會快很多。

這樣可能比較難懂,我寫個分解動作的範例,讓大家可以更清楚的理解:

1
2
3
is_above_178 = heights > 178
print(is_above_178)
heights_above_178 = heights[is_above_178]

輸出如下:

1
[ True  True False ... False False  True]

如果在 debug console 查看 is_above_178,會看到一個很長的陣列,裡面的值都是 True 或 False,代表每個數字是否大於 178。結果如下:

1
2
3
4
5
6
7
8
array([ True,  True, False, ..., False, False,  True])
special variables
[0:30000000] : [True, True, False, False, False, True, False, False, False, True, False, False, False, False, ...]
dtype: dtype('bool')
max: 'ndarray too big, calculating max would slow down debugging'
min: 'ndarray too big, calculating min would slow down debugging'
shape: (30000000,)
size: 30000000

所以真正的動作是:

  1. 先取得一個 boolean 陣列,裡面的值都是 True 或 False,代表每個數字是否大於 178。
  2. 再用這個 boolean 陣列,去取得原本的陣列,只取出 True 的部分。

我們不逐一比較,而是先產生一個篩子,一個與樣本數量同樣長度的篩子,直接拿這個篩子去一次過濾所有樣本,只要一次!


從計算機原理講,為什麼快?

  • 因為 NumPy 是靜態型別,所以不用每次迴圈都去判斷型別,並且不用去做型別轉換。
  • 一次處理多個數字,數據不用頻繁的進出 RAM 與 CPU,效能更好。

當演算法被正確地向量化時,CPU 僅需一條指令完成這行程式碼,而不是對每個 i 進行獨立操作。理想情況下,array[boolean_mask]操作將只發生於 CPU 內部而不用將數據傳回 RAM。


這篇文章主要是介紹了 NumPy 的向量化計算,用一個生活化簡單案例,來說明 NumPy 向量化計算的優點。

這篇文章的完整程式碼,可以在這裡找到:https://gist.github.com/mosdeo/341216a87a099486c1420760f24ced00

本文最后更新于 天前,文中所描述的信息可能已发生改变