การประเมินฟังก์ชันอย่างมีประสิทธิภาพในทุกเซลล์ของอาร์เรย์ NumPy


124

ด้วยอาร์เรย์NumPy A วิธีใดที่เร็ว / มีประสิทธิภาพที่สุดในการใช้ฟังก์ชันเดียวกันfกับทุกเซลล์

  1. สมมติว่าเราจะกำหนดให้A (ฉัน j) f (A (ฉัน j))

  2. ฟังก์ชันfไม่มีเอาต์พุตไบนารีดังนั้นการดำเนินการมาสก์ (ing) จะไม่ช่วย

การวนซ้ำสองรอบที่ "ชัดเจน" (ผ่านทุกเซลล์) เป็นทางออกที่ดีที่สุดหรือไม่?


คำตอบ:


165

คุณสามารถกำหนดฟังก์ชันเป็นเวกเตอร์แล้วนำไปใช้กับอาร์เรย์ Numpy ได้โดยตรงทุกครั้งที่คุณต้องการ

import numpy as np

def f(x):
    return x * x + 3 * x - 2 if x > 0 else x * 5 + 8

f = np.vectorize(f)  # or use a different name if you want to keep the original f

result_array = f(A)  # if A is your Numpy array

อาจเป็นการดีกว่าที่จะระบุประเภทเอาต์พุตที่ชัดเจนโดยตรงเมื่อกำหนดเวกเตอร์:

f = np.vectorize(f, otypes=[np.float])

19
ฉันกลัวว่าฟังก์ชัน vectorized จะไม่สามารถเร็วไปกว่าการวนซ้ำสองรอบ "ด้วยตนเอง" และการกำหนดผ่านองค์ประกอบอาร์เรย์ทั้งหมดได้ โดยเฉพาะอย่างยิ่งเนื่องจากจะเก็บผลลัพธ์ไว้ในตัวแปรที่สร้างขึ้นใหม่ (และไม่ใช่โดยตรงกับอินพุตเริ่มต้น) ขอบคุณมากสำหรับคำตอบของคุณ :)
ปีเตอร์

1
@Peter: อ่าตอนนี้ฉันเห็นว่าคุณได้กล่าวถึงการกำหนดผลลัพธ์กลับไปที่อาร์เรย์เดิมในคำถามเดิมของคุณ ฉันขอโทษที่ฉันพลาดตอนที่อ่านครั้งแรก ใช่ในกรณีนี้การวนซ้ำจะต้องเร็วขึ้น แต่คุณเคยลองวนซ้ำในมุมมองที่แบนราบของอาร์เรย์หรือไม่? ซึ่งอาจเร็วกว่าเล็กน้อยเนื่องจากคุณประหยัดค่าใช้จ่ายในการวนซ้ำเล็กน้อยและ Numpy ต้องทำการคูณและเพิ่มน้อยลงหนึ่งรายการ (สำหรับการคำนวณค่าชดเชยข้อมูล) ในการทำซ้ำแต่ละครั้ง นอกจากนี้ยังใช้งานได้กับอาร์เรย์ที่กำหนดขนาดโดยพลการ อาจจะช้าลงในอาร์เรย์ที่มีขนาดเล็กมาก
blubberdiblub

45
สังเกตคำเตือนที่ระบุในvectorizeคำอธิบายฟังก์ชัน: ฟังก์ชัน vectorize มีไว้เพื่อความสะดวกเป็นหลักไม่ใช่เพื่อประสิทธิภาพ การใช้งานเป็นหลักสำหรับการวนซ้ำ ดังนั้นสิ่งนี้จะไม่เร่งกระบวนการเลย
กาเบรียล

ใส่ใจกับวิธีvectorizeกำหนดประเภทผลตอบแทน ที่ทำให้เกิดข้อบกพร่อง frompyfuncเร็วกว่าเล็กน้อย แต่ส่งคืนอาร์เรย์อ็อบเจ็กต์ dtype ทั้งสองฟีดสเกลาร์ไม่ใช่แถวหรือคอลัมน์
hpaulj

1
@ กาเบรียลเพียงแค่โยนnp.vectorizeฟังก์ชั่นของฉัน (ซึ่งใช้ RK45) ทำให้ฉันมีความเร็วขึ้นถึง ~ 20
Suuuehgi



0

ฉันเชื่อว่าฉันพบทางออกที่ดีกว่าแล้ว แนวคิดในการเปลี่ยนฟังก์ชันเป็นฟังก์ชัน python universal (ดูเอกสารประกอบ ) ซึ่งสามารถใช้การคำนวณแบบขนานภายใต้ประทุน

เราสามารถเขียนแบบกำหนดเองufuncใน C ซึ่งมีประสิทธิภาพมากกว่าหรือโดยการเรียกใช้np.frompyfuncซึ่งเป็นวิธีการของโรงงานในตัว หลังจากทดสอบแล้วจะมีประสิทธิภาพมากกว่าnp.vectorize:

f = lambda x, y: x * y
f_arr = np.frompyfunc(f, 2, 1)
vf = np.vectorize(f)
arr = np.linspace(0, 1, 10000)

%timeit f_arr(arr, arr) # 307ms
%timeit f_arr(arr, arr) # 450ms

ฉันได้ทดสอบตัวอย่างที่ใหญ่ขึ้นด้วยและการปรับปรุงเป็นไปตามสัดส่วน สำหรับการเปรียบเทียบประสิทธิภาพของวิธีการอื่น ๆ โปรดดูที่โพสต์นี้


0

เมื่อ 2d-array (หรือ nd-array) เป็น C- หรือ F-contiguous งานนี้ในการแมปฟังก์ชันลงบนอาร์เรย์ 2d จะเหมือนกับงานการแมปฟังก์ชันลงในอาร์เรย์ 1d - เราแค่ np.ravel(A,'K')ต้องดูมันเป็นอย่างนั้นเช่นผ่านทาง

วิธีการแก้ปัญหาที่เป็นไปได้สำหรับ 1D อาร์เรย์ได้รับการกล่าวเช่นที่นี่

อย่างไรก็ตามเมื่อหน่วยความจำของ 2d-array ไม่ติดกันสถานการณ์จะซับซ้อนขึ้นเล็กน้อยเพราะเราต้องการหลีกเลี่ยงการพลาดแคชที่เป็นไปได้หากแกนถูกจัดการในลำดับที่ไม่ถูกต้อง

Numpy มีเครื่องจักรอยู่แล้วในการประมวลผลแกนตามลำดับที่ดีที่สุด np.vectorizeหนึ่งเป็นไปได้ที่จะใช้เครื่องจักรนี้อยู่ อย่างไรก็ตามเอกสารของ numpy np.vectorizeระบุว่า "จัดทำขึ้นเพื่อความสะดวกเป็นหลักไม่ใช่เพื่อประสิทธิภาพ" - ฟังก์ชัน python ที่ช้าจะยังคงเป็นฟังก์ชัน python ที่ช้าพร้อมกับค่าโสหุ้ยที่เกี่ยวข้องทั้งหมด! อีกปัญหาหนึ่งคือการใช้หน่วยความจำอย่างมาก - ดูตัวอย่างSO-postนี้

เมื่อต้องการมีประสิทธิภาพของ C-function แต่ต้องการใช้เครื่องจักรของ numpy วิธีแก้ปัญหาที่ดีคือการใช้ numba ในการสร้าง ufuncs เช่น:

# runtime generated C-function as ufunc
import numba as nb
@nb.vectorize(target="cpu")
def nb_vf(x):
    return x+2*x*x+4*x*x*x

มันเต้นได้อย่างง่ายดายnp.vectorizeแต่ยังเมื่อฟังก์ชันเดียวกันจะถูกดำเนินการเป็นการคูณ / การเพิ่มจำนวนอาร์เรย์เช่น

# numpy-functionality
def f(x):
    return x+2*x*x+4*x*x*x

# python-function as ufunc
import numpy as np
vf=np.vectorize(f)
vf.__name__="vf"

ดูภาคผนวกของคำตอบนี้สำหรับรหัสการวัดเวลา:

ใส่คำอธิบายภาพที่นี่

เวอร์ชันของ Numba (สีเขียว) เร็วกว่า python-function (ie np.vectorize) ประมาณ 100 เท่าซึ่งไม่น่าแปลกใจ แต่มันยังเร็วกว่าฟังก์ชัน numpy ประมาณ 10 เท่าเนื่องจากเวอร์ชัน numas ไม่ต้องการอาร์เรย์กลางจึงใช้แคชได้อย่างมีประสิทธิภาพมากขึ้น


แม้ว่าแนวทาง ufunc ของ numba จะเป็นการแลกเปลี่ยนที่ดีระหว่างความสามารถในการใช้งานและประสิทธิภาพ แต่ก็ยังไม่ดีที่สุดที่เราสามารถทำได้ ยังไม่มีกระสุนเงินหรือแนวทางที่ดีที่สุดสำหรับงานใด ๆ - เราต้องเข้าใจว่าอะไรคือข้อ จำกัด และวิธีที่จะบรรเทาได้

ตัวอย่างเช่นสำหรับฟังก์ชั่นยอดเยี่ยม (เช่นexp, sin, cos) numba ไม่ได้ให้ประโยชน์ใด ๆ มากกว่า numpy ของnp.exp(ไม่มีอาร์เรย์ชั่วคราวที่สร้างขึ้น - แหล่งที่มาหลักของความเร็วขึ้นไป) อย่างไรก็ตามการติดตั้ง Anaconda ของฉันใช้ VML ของ Intel สำหรับเวกเตอร์ที่มีขนาดใหญ่กว่า 8192ซึ่งไม่สามารถทำได้หากหน่วยความจำไม่ติดกัน ดังนั้นจึงอาจเป็นการดีกว่าที่จะคัดลอกองค์ประกอบไปยังหน่วยความจำที่อยู่ติดกันเพื่อให้สามารถใช้ VML ของ Intel:

import numba as nb
@nb.vectorize(target="cpu")
def nb_vexp(x):
    return np.exp(x)

def np_copy_exp(x):
    copy = np.ravel(x, 'K')
    return np.exp(copy).reshape(x.shape) 

เพื่อความเป็นธรรมของการเปรียบเทียบฉันได้ปิดการขนานของ VML แล้ว (ดูรหัสในภาคผนวก):

ใส่คำอธิบายภาพที่นี่

อย่างที่เห็นเมื่อ VML เริ่มต้นค่าใช้จ่ายในการคัดลอกจะได้รับการชดเชยมากกว่า เมื่อข้อมูลมีขนาดใหญ่เกินไปสำหรับแคช L3 ข้อได้เปรียบก็จะน้อยที่สุดเนื่องจากงานกลายเป็นอีกครั้งที่มีการ จำกัด แบนด์วิธหน่วยความจำ

ในทางกลับกัน numba สามารถใช้ SVML ของ Intel ได้เช่นกันตามที่อธิบายไว้ในโพสต์นี้ :

from llvmlite import binding
# set before import
binding.set_option('SVML', '-vector-library=SVML')

import numba as nb

@nb.vectorize(target="cpu")
def nb_vexp_svml(x):
    return np.exp(x)

และการใช้ VML กับผลตอบแทนแบบขนาน:

ใส่คำอธิบายภาพที่นี่

เวอร์ชันของ numba มีค่าใช้จ่ายน้อยกว่า แต่สำหรับ VML บางขนาดจะชนะ SVML แม้จะมีค่าใช้จ่ายในการคัดลอกเพิ่มเติมซึ่งไม่น่าแปลกใจเลยเพราะ ufuncs ของ numba ไม่ได้ขนานกัน


รายชื่อ:

ก. การเปรียบเทียบฟังก์ชันพหุนาม:

import perfplot
perfplot.show(
    setup=lambda n: np.random.rand(n,n)[::2,::2],
    n_range=[2**k for k in range(0,12)],
    kernels=[
        f,
        vf, 
        nb_vf
        ],
    logx=True,
    logy=True,
    xlabel='len(x)'
    ) 

ข. การเปรียบเทียบexp:

import perfplot
import numexpr as ne # using ne is the easiest way to set vml_num_threads
ne.set_vml_num_threads(1)
perfplot.show(
    setup=lambda n: np.random.rand(n,n)[::2,::2],
    n_range=[2**k for k in range(0,12)],
    kernels=[
        nb_vexp, 
        np.exp,
        np_copy_exp,
        ],
    logx=True,
    logy=True,
    xlabel='len(x)',
    )

0

คำตอบทั้งหมดข้างต้นเปรียบเทียบได้ดี แต่ถ้าคุณจำเป็นต้องใช้ฟังก์ชันที่กำหนดเองสำหรับการทำแผนที่และคุณมีnumpy.ndarrayและคุณต้องคงรูปทรงของอาร์เรย์ไว้

ฉันได้เปรียบเทียบแค่สองอย่าง แต่มันจะคงรูปร่างของndarray. ฉันใช้อาร์เรย์กับ 1 ล้านรายการเพื่อเปรียบเทียบ ที่นี่ฉันใช้ฟังก์ชันสี่เหลี่ยม ฉันกำลังนำเสนอกรณีทั่วไปสำหรับอาร์เรย์มิติ n สำหรับสองมิติเพียงแค่สร้างiter2D

import numpy, time

def A(e):
    return e * e

def timeit():
    y = numpy.arange(1000000)
    now = time.time()
    numpy.array([A(x) for x in y.reshape(-1)]).reshape(y.shape)        
    print(time.time() - now)
    now = time.time()
    numpy.fromiter((A(x) for x in y.reshape(-1)), y.dtype).reshape(y.shape)
    print(time.time() - now)
    now = time.time()
    numpy.square(y)  
    print(time.time() - now)

เอาท์พุต

>>> timeit()
1.162431240081787    # list comprehension and then building numpy array
1.0775556564331055   # from numpy.fromiter
0.002948284149169922 # using inbuilt function

ที่นี่คุณสามารถเห็นnumpy.fromiterฟังก์ชันสี่เหลี่ยมจัตุรัสของผู้ใช้ได้อย่างชัดเจนใช้ตัวเลือกใดก็ได้ หากฟังก์ชันของคุณขึ้นอยู่กับi, j ดัชนีของอาร์เรย์ให้วนซ้ำตามขนาดของอาร์เรย์เช่นfor ind in range(arr.size)ใช้numpy.unravel_indexเพื่อรับi, j, ..ตามดัชนี 1D ของคุณและรูปร่างของอาร์เรย์numpy.unravel_index

คำตอบนี้ได้รับแรงบันดาลใจจากคำตอบของฉันสำหรับคำถามอื่น ๆที่นี่

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.