NumPy: ฟังก์ชันสำหรับ max () และ min () พร้อมกัน


113

numpy.amax ()จะพบค่าสูงสุดในอาร์เรย์และnumpy.amin ()ทำเช่นเดียวกันสำหรับค่าต่ำสุด ถ้าฉันต้องการค้นหาทั้ง max และ min ฉันต้องเรียกใช้ทั้งสองฟังก์ชันซึ่งต้องผ่านอาร์เรย์ (ใหญ่มาก) สองครั้งซึ่งดูเหมือนจะช้า

มีฟังก์ชันใน numpy API ที่ค้นหาทั้งสูงสุดและต่ำสุดด้วยการส่งผ่านข้อมูลเพียงครั้งเดียวหรือไม่


1
ใหญ่มากขนาดไหน? ถ้าฉันมีเวลาฉันจะทำการทดสอบสองสามครั้งเปรียบเทียบการใช้งาน Fortran amaxและamin
mgilson

1
ฉันจะยอมรับว่า "ใหญ่มาก" เป็นเรื่องส่วนตัว ในกรณีของฉันฉันกำลังพูดถึงอาร์เรย์ที่มีขนาดไม่กี่ GB
Stuart Berg

มันใหญ่มาก ฉันได้เขียนโค้ดตัวอย่างเพื่อคำนวณใน Fortran (แม้ว่าคุณจะไม่รู้จัก Fortran แต่ก็น่าจะเข้าใจรหัสได้ง่าย) มันสร้างความแตกต่างจาก Fortran เทียบกับการวิ่งผ่าน numpy (สมมติว่าคุณน่าจะได้รับประสิทธิภาพเดียวกันจาก C ... ) ฉันไม่แน่ใจ - ฉันคิดว่าเราต้องการนักพัฒนาจำนวนมากเพื่อแสดงความคิดเห็นว่าทำไมฟังก์ชันของฉันจึงทำงานได้ดีกว่าฟังก์ชันของพวกเขามาก ...
mgilson

แน่นอนว่านี่ไม่ใช่ความคิดแปลกใหม่ ตัวอย่างเช่นไลบรารีboost minmax (C ++) ให้การใช้งานอัลกอริทึมที่ฉันกำลังมองหา
Stuart Berg

3
ไม่ใช่คำตอบสำหรับคำถามที่ถาม แต่อาจเป็นที่สนใจของผู้คนในหัวข้อนี้ ถาม NumPy เกี่ยวกับการเพิ่มminmaxไปยังไลบรารีที่มีปัญหา ( github.com/numpy/numpy/issues/9836 )
jakirkham

คำตอบ:


49

มีฟังก์ชันใน numpy API ที่ค้นหาทั้งสูงสุดและต่ำสุดด้วยการส่งผ่านข้อมูลเพียงครั้งเดียวหรือไม่

ไม่ในขณะที่เขียนนี้ไม่มีฟังก์ชันดังกล่าว (และใช่ถ้ามีอยู่เช่นฟังก์ชั่นประสิทธิภาพการทำงานของมันจะเป็นอย่างมีนัยสำคัญที่ดีกว่าการโทรnumpy.amin()และnumpy.amax()ต่อเนื่องใน array ขนาดใหญ่.)


31

ฉันไม่คิดว่าการส่งผ่านอาร์เรย์สองครั้งจะเป็นปัญหา พิจารณารหัสหลอกต่อไปนี้:

minval = array[0]
maxval = array[0]
for i in array:
    if i < minval:
       minval = i
    if i > maxval:
       maxval = i

ในขณะที่มีเพียง 1 ลูปที่นี่ยังมีการตรวจสอบ 2 รายการ (แทนที่จะมี 2 ลูปกับ 1 เช็คแต่ละอัน) สิ่งเดียวที่คุณประหยัดได้คือค่าใช้จ่ายของ 1 ลูป ถ้าอาร์เรย์ใหญ่จริงอย่างที่คุณพูดค่าใช้จ่ายนั้นจะน้อยเมื่อเทียบกับภาระงานของลูปจริง (โปรดทราบว่าทั้งหมดนี้ใช้งานใน C ดังนั้นลูปจึงว่างมากหรือน้อยอยู่แล้ว)


แก้ไขขออภัยพวกคุณ 4 คนที่โหวตและศรัทธาในตัวฉัน คุณสามารถเพิ่มประสิทธิภาพได้อย่างแน่นอน

นี่คือรหัส Fortran บางส่วนที่สามารถรวบรวมลงในโมดูล python ผ่านทางf2py(อาจมีCythonกูรูสามารถเข้ามาเปรียบเทียบกับรุ่น C ที่ปรับให้เหมาะสม ... ):

subroutine minmax1(a,n,amin,amax)
  implicit none
  !f2py intent(hidden) :: n
  !f2py intent(out) :: amin,amax
  !f2py intent(in) :: a
  integer n
  real a(n),amin,amax
  integer i

  amin = a(1)
  amax = a(1)
  do i=2, n
     if(a(i) > amax)then
        amax = a(i)
     elseif(a(i) < amin) then
        amin = a(i)
     endif
  enddo
end subroutine minmax1

subroutine minmax2(a,n,amin,amax)
  implicit none
  !f2py intent(hidden) :: n
  !f2py intent(out) :: amin,amax
  !f2py intent(in) :: a
  integer n
  real a(n),amin,amax
  amin = minval(a)
  amax = maxval(a)
end subroutine minmax2

รวบรวมผ่าน:

f2py -m untitled -c fortran_code.f90

และตอนนี้เราอยู่ในจุดที่สามารถทดสอบได้:

import timeit

size = 100000
repeat = 10000

print timeit.timeit(
    'np.min(a); np.max(a)',
    setup='import numpy as np; a = np.arange(%d, dtype=np.float32)' % size,
    number=repeat), " # numpy min/max"

print timeit.timeit(
    'untitled.minmax1(a)',
    setup='import numpy as np; import untitled; a = np.arange(%d, dtype=np.float32)' % size,
    number=repeat), '# minmax1'

print timeit.timeit(
    'untitled.minmax2(a)',
    setup='import numpy as np; import untitled; a = np.arange(%d, dtype=np.float32)' % size,
    number=repeat), '# minmax2'

ผลลัพธ์ที่ได้ค่อนข้างน่าตกใจสำหรับฉัน:

8.61869883537 # numpy min/max
1.60417699814 # minmax1
2.30169081688 # minmax2

ฉันต้องบอกว่าฉันไม่เข้าใจอย่างสมบูรณ์ เปรียบเทียบเพียงnp.minเมื่อเทียบกับminmax1และminmax2ยังคงรบเสียดังนั้นจึงไม่ได้เป็นเพียงปัญหาหน่วยความจำ ...

หมายเหตุ - การเพิ่มขนาดตามปัจจัย10**aและลดการทำซ้ำโดยปัจจัยของ10**a(การรักษาขนาดของปัญหาให้คงที่) จะทำให้ประสิทธิภาพการทำงานเปลี่ยนไป แต่ไม่ใช่ในลักษณะที่สอดคล้องกันซึ่งแสดงให้เห็นว่ามีการทำงานร่วมกันระหว่างประสิทธิภาพของหน่วยความจำและค่าใช้จ่ายในการเรียกฟังก์ชัน หลาม แม้กระทั่งการเปรียบเทียบการminใช้งานอย่างง่ายใน Fortran ก็ยังเอาชนะตัวเลขได้โดยประมาณ 2 ...


21
ข้อดีของการส่งครั้งเดียวคือประสิทธิภาพของหน่วยความจำ โดยเฉพาะอย่างยิ่งถ้าอาร์เรย์ของคุณมีขนาดใหญ่พอที่จะสลับออกได้อาจมีขนาดใหญ่มาก
djs

4
นั่นไม่เป็นความจริงเลยมันเร็วเกือบครึ่งหนึ่งเพราะด้วยอาร์เรย์ประเภทนี้ความเร็วของหน่วยความจำมักเป็นปัจจัย จำกัด ดังนั้นจึงเร็วกว่าครึ่งหนึ่ง ...
seberg

3
คุณไม่จำเป็นต้องตรวจสอบสองครั้งเสมอไป ถ้าi < minvalเป็นจริงแสดงว่าi > maxvalเป็นเท็จเสมอดังนั้นคุณต้องทำการตรวจสอบ 1.5 ครั้งต่อการวนซ้ำโดยเฉลี่ยเมื่อวินาทีifถูกแทนที่ด้วยelifไฟล์.
Fred Foo

2
หมายเหตุเล็ก ๆ : ฉันสงสัยว่า Cython เป็นวิธีที่จะได้รับโมดูล C ที่เรียกได้ Python ที่เหมาะสมที่สุด เป้าหมายของ Cython คือ Python ที่มีคำอธิบายประกอบประเภทหนึ่งซึ่งแปลด้วยเครื่องเป็น C ในขณะที่f2pyเพียงแค่ห่อ Fortran ที่เข้ารหัสด้วยมือเพื่อให้ Python สามารถเรียกใช้ได้ การทดสอบที่ "ยุติธรรมกว่า" น่าจะเป็นการเข้ารหัส C ด้วยมือจากนั้นใช้f2py(!) เพื่อรวมเข้ากับ Python หากคุณอนุญาตให้ใช้ C ++ ดังนั้น Shed Skin อาจเป็นจุดที่น่าสนใจสำหรับการปรับสมดุลการเข้ารหัสอย่างง่ายดายด้วยประสิทธิภาพ
John Y

4
ณ จำนวน 1.8 นาทีและสูงสุดเป็นเวกเตอร์บนแพลตฟอร์ม amd64 บน core2duo numpy ของฉันดำเนินการเช่นเดียวกับรหัส Fortran นี้ แต่การส่งผ่านเพียงครั้งเดียวจะเป็นประโยชน์หากอาร์เรย์มีขนาดเกินขนาดของแคช cpu ที่ใหญ่กว่า
jtaylor

24

มีฟังก์ชันสำหรับค้นหา (max-min) ที่เรียกว่าnumpy.ptpหากมีประโยชน์สำหรับคุณ:

>>> import numpy
>>> x = numpy.array([1,2,3,4,5,6])
>>> x.ptp()
5

แต่ฉันไม่คิดว่าจะมีวิธีหาทั้งค่าต่ำสุดและสูงสุดด้วยการส่งผ่านเพียงครั้งเดียว

แก้ไข: ptp เพียงแค่เรียก min และ max ภายใต้ประทุน


2
มันน่ารำคาญเพราะน่าจะเป็นวิธีการใช้งาน ptp เพื่อติดตาม max และ min!
Andy Hayden

1
หรืออาจเรียกแค่ max and min ไม่แน่ใจ
jterrace

3
@hayden ปรากฎว่า ptp เรียก max และ min
jterrace

1
นั่นคือรหัสอาร์เรย์มาสก์ รหัส ndarray หลักอยู่ในซี แต่มันกลับกลายเป็นรหัส C ยัง iterates มากกว่าอาร์เรย์สอง: github.com/numpy/numpy/blob/...
Ken Arnold

22

คุณสามารถใช้Numbaซึ่งเป็นคอมไพเลอร์ Python แบบไดนามิกที่รับรู้ NumPy โดยใช้ LLVM การใช้งานที่ได้นั้นค่อนข้างง่ายและชัดเจน:

import numpy
import numba


@numba.jit
def minmax(x):
    maximum = x[0]
    minimum = x[0]
    for i in x[1:]:
        if i > maximum:
            maximum = i
        elif i < minimum:
            minimum = i
    return (minimum, maximum)


numpy.random.seed(1)
x = numpy.random.rand(1000000)
print(minmax(x) == (x.min(), x.max()))

นอกจากนี้ควรเร็วกว่าmin() & max()การนำไปใช้ของ Numpy และทั้งหมดนี้ไม่ต้องเขียนโค้ด C / Fortran เลยแม้แต่บรรทัดเดียว

ทำการทดสอบประสิทธิภาพของคุณเองเนื่องจากขึ้นอยู่กับสถาปัตยกรรมของคุณข้อมูลของคุณเวอร์ชันแพ็คเกจของคุณ ...


2
> ควรเร็วกว่าการใช้งาน min () & max () ของ Numpy ด้วยฉันไม่คิดว่านี่จะถูกต้อง numpy ไม่ใช่ python ดั้งเดิม - คือ C. `` x = numpy.random.rand (10000000) t = time () สำหรับ i ในช่วง (1000): minmax (x) พิมพ์ ('numba', เวลา () - t) t = time () สำหรับ i ในช่วง (1000): x.min () x.max () พิมพ์ ('numpy', เวลา () - t) `` ผลลัพธ์ใน: ('numba', 10.299750089645386 ) ('numpy', 9.898081064224243)
Authman Apatira

1
@AuthmanApatira: ใช่เกณฑ์มาตรฐานมักจะเป็นแบบนั้นนั่นคือเหตุผลที่ฉันบอกว่า " ควร " (เร็วกว่า) และ " ทำการทดสอบประสิทธิภาพของคุณเองเนื่องจากขึ้นอยู่กับสถาปัตยกรรมของคุณข้อมูลของคุณ ... " ในกรณีของฉันฉันลองใช้คอมพิวเตอร์ 3 เครื่องและได้ผลลัพธ์เหมือนกัน (Numba เร็วกว่า Numpy) แต่ผลลัพธ์ในคอมพิวเตอร์ของคุณอาจแตกต่างออกไป ... คุณลองเรียกใช้numbaฟังก์ชันหนึ่งครั้งก่อนเกณฑ์มาตรฐานเพื่อให้แน่ใจว่ารวบรวม JIT แล้ว ?. นอกจากนี้หากคุณใช้ipythonเพื่อความเรียบง่ายฉันขอแนะนำให้คุณใช้%timeit whatever_code()สำหรับการวัดการทำงานของเวลา
Peque

3
@AuthmanApatira: ไม่ว่าในกรณีใดสิ่งที่ฉันพยายามแสดงด้วยคำตอบนี้ก็คือบางครั้งรหัส Python (ในกรณีนี้คือ JIT ที่คอมไพล์ด้วย Numba) อาจเร็วพอ ๆ กับไลบรารีที่รวบรวม C ที่เร็วที่สุด (อย่างน้อยเราก็กำลังพูดถึงลำดับเดียวกัน ของขนาด) ซึ่งน่าประทับใจเมื่อพิจารณาว่าเราไม่ได้เขียนอะไรนอกจากรหัส Python ที่บริสุทธิ์คุณไม่เห็นด้วยหรือ? ^^
Peque

ฉันเห็นด้วย =) นอกจากนี้ขอขอบคุณสำหรับคำแนะนำในความคิดเห็นก่อนหน้าเกี่ยวกับ jupyter และการรวบรวมฟังก์ชันหนึ่งครั้งนอกรหัสเวลา
Authman Apatira

2
เพียงแค่วิ่งข้ามสิ่งนี้ไม่ใช่ว่าจะมีความสำคัญในกรณีที่ใช้งานได้จริง แต่การelifอนุญาตให้ขั้นต่ำของคุณสูงกว่าค่าสูงสุดของคุณ เช่นด้วยอาร์เรย์ที่มีความยาว 1 ค่าสูงสุดจะเป็นเท่าใดก็ได้ในขณะที่ค่าต่ำสุดคือ + อินฟินิตี้ ไม่ใช่เรื่องใหญ่สำหรับการเล่นครั้งเดียว แต่ไม่ใช่เรื่องดีที่จะโยนลึกเข้าไปในท้องของสัตว์ร้าย
Mike Williamson

12

โดยทั่วไปคุณสามารถลดจำนวนการเปรียบเทียบสำหรับอัลกอริทึม minmax ได้โดยประมวลผลสององค์ประกอบพร้อมกันและเปรียบเทียบเฉพาะองค์ประกอบที่เล็กกว่ากับค่าต่ำสุดชั่วคราวและค่าที่ใหญ่กว่ากับค่าสูงสุดชั่วคราว โดยเฉลี่ยแล้วคนเราต้องการเพียง 3/4 ของการเปรียบเทียบมากกว่าแนวทางที่ไร้เดียงสา

สิ่งนี้สามารถนำไปใช้ในภาษา c หรือ Fortran (หรือภาษาระดับต่ำอื่น ๆ ) และน่าจะแทบไม่สามารถเอาชนะได้ในแง่ของประสิทธิภาพ ฉันกำลังใช้ เพื่อแสดงให้เห็นถึงหลักการและได้รับการดำเนินการที่รวดเร็วและไม่เป็นอิสระ:

import numba as nb
import numpy as np

@nb.njit
def minmax(array):
    # Ravel the array and return early if it's empty
    array = array.ravel()
    length = array.size
    if not length:
        return

    # We want to process two elements at once so we need
    # an even sized array, but we preprocess the first and
    # start with the second element, so we want it "odd"
    odd = length % 2
    if not odd:
        length -= 1

    # Initialize min and max with the first item
    minimum = maximum = array[0]

    i = 1
    while i < length:
        # Get the next two items and swap them if necessary
        x = array[i]
        y = array[i+1]
        if x > y:
            x, y = y, x
        # Compare the min with the smaller one and the max
        # with the bigger one
        minimum = min(x, minimum)
        maximum = max(y, maximum)
        i += 2

    # If we had an even sized array we need to compare the
    # one remaining item too.
    if not odd:
        x = array[length]
        minimum = min(x, minimum)
        maximum = max(x, maximum)

    return minimum, maximum

เร็วกว่าแนวทางไร้เดียงสาที่Pequeนำเสนออย่างแน่นอน:

arr = np.random.random(3000000)
assert minmax(arr) == minmax_peque(arr)  # warmup and making sure they are identical 
%timeit minmax(arr)            # 100 loops, best of 3: 2.1 ms per loop
%timeit minmax_peque(arr)      # 100 loops, best of 3: 2.75 ms per loop

ตามที่คาดไว้การใช้งาน minmax ใหม่ใช้เวลาประมาณ 3/4 ของเวลาที่การใช้งานแบบไร้เดียงสาใช้เวลา ( 2.1 / 2.75 = 0.7636363636363637)


1
บนเครื่องของฉันการแก้ปัญหาของคุณไม่เร็วกว่าของ Peque นัมบ้า 0.33
John Zwinck

@johnzwinck คุณใช้เกณฑ์มาตรฐานในคำตอบของฉันมันเป็นสิ่งที่แตกต่างหรือไม่? ถ้าเป็นเช่นนั้นคุณช่วยแบ่งปันได้ไหม แต่เป็นไปได้: ฉันสังเกตเห็นการถดถอยในเวอร์ชันที่ใหม่กว่าด้วย
MSeifert

ฉันใช้เกณฑ์มาตรฐานของคุณ การกำหนดเวลาของโซลูชันของคุณและ @ Peque นั้นค่อนข้างเหมือนกันมาก (~ 2.8 ms)
John Zwinck

@JohnZwinck มันแปลกมากฉันเพิ่งทดสอบอีกครั้งและบนคอมพิวเตอร์ของฉันเร็วขึ้นแน่นอน อาจจะมีบางอย่างเกี่ยวข้องกับ numba และ LLVM ที่ขึ้นอยู่กับฮาร์ดแวร์
MSeifert

ตอนนี้ฉันลองใช้เครื่องอื่น (เวิร์กสเตชันที่มีเนื้อวัว) และได้ 2.4 ms สำหรับคุณเทียบกับ 2.6 สำหรับ Peque's ดังนั้นการชนะเล็กน้อย
John Zwinck

12

เพียงเพื่อให้ได้แนวคิดบางอย่างเกี่ยวกับตัวเลขที่คุณคาดหวังโดยใช้แนวทางต่อไปนี้:

import numpy as np


def extrema_np(arr):
    return np.max(arr), np.min(arr)
import numba as nb


@nb.jit(nopython=True)
def extrema_loop_nb(arr):
    n = arr.size
    max_val = min_val = arr[0]
    for i in range(1, n):
        item = arr[i]
        if item > max_val:
            max_val = item
        elif item < min_val:
            min_val = item
    return max_val, min_val
import numba as nb


@nb.jit(nopython=True)
def extrema_while_nb(arr):
    n = arr.size
    odd = n % 2
    if not odd:
        n -= 1
    max_val = min_val = arr[0]
    i = 1
    while i < n:
        x = arr[i]
        y = arr[i + 1]
        if x > y:
            x, y = y, x
        min_val = min(x, min_val)
        max_val = max(y, max_val)
        i += 2
    if not odd:
        x = arr[n]
        min_val = min(x, min_val)
        max_val = max(x, max_val)
    return max_val, min_val
%%cython -c-O3 -c-march=native -a
#cython: language_level=3, boundscheck=False, wraparound=False, initializedcheck=False, cdivision=True, infer_types=True


import numpy as np


cdef void _extrema_loop_cy(
        long[:] arr,
        size_t n,
        long[:] result):
    cdef size_t i
    cdef long item, max_val, min_val
    max_val = arr[0]
    min_val = arr[0]
    for i in range(1, n):
        item = arr[i]
        if item > max_val:
            max_val = item
        elif item < min_val:
            min_val = item
    result[0] = max_val
    result[1] = min_val


def extrema_loop_cy(arr):
    result = np.zeros(2, dtype=arr.dtype)
    _extrema_loop_cy(arr, arr.size, result)
    return result[0], result[1]
%%cython -c-O3 -c-march=native -a
#cython: language_level=3, boundscheck=False, wraparound=False, initializedcheck=False, cdivision=True, infer_types=True


import numpy as np


cdef void _extrema_while_cy(
        long[:] arr,
        size_t n,
        long[:] result):
    cdef size_t i, odd
    cdef long x, y, max_val, min_val
    max_val = arr[0]
    min_val = arr[0]
    odd = n % 2
    if not odd:
        n -= 1
    max_val = min_val = arr[0]
    i = 1
    while i < n:
        x = arr[i]
        y = arr[i + 1]
        if x > y:
            x, y = y, x
        min_val = min(x, min_val)
        max_val = max(y, max_val)
        i += 2
    if not odd:
        x = arr[n]
        min_val = min(x, min_val)
        max_val = max(x, max_val)
    result[0] = max_val
    result[1] = min_val


def extrema_while_cy(arr):
    result = np.zeros(2, dtype=arr.dtype)
    _extrema_while_cy(arr, arr.size, result)
    return result[0], result[1]

( extrema_loop_*()แนวทางคล้ายกับสิ่งที่เสนอไว้ที่นี่ในขณะที่extrema_while_*()แนวทางจะขึ้นอยู่กับรหัสจากที่นี่ )

การกำหนดเวลาต่อไปนี้:

bm

ระบุว่าextrema_while_*()เร็วที่สุดและextrema_while_nb()เร็วที่สุด ไม่ว่าในกรณีใดก็ตามวิธีการextrema_loop_nb()และextrema_loop_cy()วิธีแก้ปัญหาก็ทำได้ดีกว่าแนวทาง NumPy-only (โดยใช้np.max()และnp.min()แยกต่างหาก)

สุดท้ายโปรดทราบว่าสิ่งเหล่านี้ไม่มีความยืดหยุ่นเท่ากับnp.min()/ np.max()(ในแง่ของการรองรับ n-dim axisพารามิเตอร์ ฯลฯ )

(รหัสเต็มมีอยู่ที่นี่ )


2
ดูเหมือนว่าคุณจะได้รับความเร็วเพิ่มขึ้น 10% หากใช้ @njit (fastmath = True)extrema_while_nb
argenisleon

10

ไม่มีใครพูดถึงnumpy.percentileดังนั้นฉันคิดว่าฉันจะทำ หากคุณขอ[0, 100]เปอร์เซ็นไทล์มันจะให้อาร์เรย์ของสององค์ประกอบคือ min (เปอร์เซ็นไทล์ที่ 0) และสูงสุด (เปอร์เซ็นไทล์ที่ 100)

อย่างไรก็ตามไม่เป็นไปตามวัตถุประสงค์ของ OP: ไม่เร็วกว่า min และ max แยกกัน อาจเป็นเพราะเครื่องจักรบางตัวที่ยอมให้มีเปอร์เซ็นไทล์ที่ไม่รุนแรง (ปัญหาที่หนักกว่าซึ่งน่าจะใช้เวลานานกว่านี้)

In [1]: import numpy

In [2]: a = numpy.random.normal(0, 1, 1000000)

In [3]: %%timeit
   ...: lo, hi = numpy.amin(a), numpy.amax(a)
   ...: 
100 loops, best of 3: 4.08 ms per loop

In [4]: %%timeit
   ...: lo, hi = numpy.percentile(a, [0, 100])
   ...: 
100 loops, best of 3: 17.2 ms per loop

In [5]: numpy.__version__
Out[5]: '1.14.4'

Numpy เวอร์ชันในอนาคตสามารถใส่เป็นกรณีพิเศษเพื่อข้ามการคำนวณเปอร์เซ็นไทล์ปกติได้หาก[0, 100]มีการร้องขอเท่านั้น โดยไม่ต้องเพิ่มอะไรในอินเทอร์เฟซมีวิธีถาม Numpy สำหรับ min และ max ในการโทรครั้งเดียว (ตรงกันข้ามกับสิ่งที่พูดในคำตอบที่ยอมรับ) แต่การใช้งานไลบรารีมาตรฐานไม่ได้ใช้ประโยชน์จากกรณีนี้ คุ้มค่า.


9

นี่เป็นกระทู้เก่า แต่ยังไงก็ตามถ้าใครเคยดูเรื่องนี้อีก ...

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

แทน (รหัส Python):

_max = ar[0]
_min=  ar[0]
for ii in xrange(len(ar)):
    if _max > ar[ii]: _max = ar[ii]
    if _min < ar[ii]: _min = ar[ii]

ก่อนอื่นคุณสามารถเปรียบเทียบค่าที่อยู่ติดกันสองค่าในอาร์เรย์จากนั้นเปรียบเทียบค่าที่เล็กกว่ากับค่าต่ำสุดในปัจจุบันและค่าที่ใหญ่กว่าเทียบกับค่าสูงสุดปัจจุบัน:

## for an even-sized array
_max = ar[0]
_min = ar[0]
for ii in xrange(0, len(ar), 2)):  ## iterate over every other value in the array
    f1 = ar[ii]
    f2 = ar[ii+1]
    if (f1 < f2):
        if f1 < _min: _min = f1
        if f2 > _max: _max = f2
    else:
        if f2 < _min: _min = f2
        if f1 > _max: _max = f1

โค้ดที่นี่เขียนด้วย Python ชัดเจนสำหรับความเร็วที่คุณจะใช้ C หรือ Fortran หรือ Cython แต่ด้วยวิธีนี้คุณจะทำการเปรียบเทียบ 3 ครั้งต่อการวนซ้ำโดยมีการทำซ้ำ len (ar) / 2 โดยให้การเปรียบเทียบ 3/2 * len (ar) ในทางตรงกันข้ามการเปรียบเทียบแบบ "วิธีที่ชัดเจน" คุณจะทำการเปรียบเทียบสองครั้งต่อการทำซ้ำซึ่งนำไปสู่การเปรียบเทียบ 2 * len (ar) ประหยัดเวลาในการเปรียบเทียบ 25%

อาจจะมีใครสักคนสักวันหนึ่งจะพบว่าสิ่งนี้มีประโยชน์


6
คุณได้เปรียบเทียบสิ่งนี้หรือไม่ สำหรับฮาร์ดแวร์ x86 ที่ทันสมัยคุณมีคำแนะนำเกี่ยวกับเครื่องจักรสำหรับ min และ max ตามที่ใช้ในตัวแปรแรกสิ่งเหล่านี้หลีกเลี่ยงความต้องการสาขาในขณะที่รหัสของคุณอยู่ในการพึ่งพาการควบคุมซึ่งอาจไม่ได้แมปกับฮาร์ดแวร์ด้วย
jtaylor

ฉันไม่ได้จริง จะทำอย่างไรถ้าฉันมีโอกาส ฉันคิดว่าค่อนข้างชัดเจนว่ารหัสหลามบริสุทธิ์จะสูญเสียการใช้งานที่คอมไพล์อย่างสมเหตุสมผล แต่ฉันสงสัยว่าจะมีการเร่งความเร็วใน Cython หรือไม่ ...
Bennet

13
มีการดำเนินการใน MinMax numpy ภายใต้ประทุนโดยใช้คือnp.bincountให้ดูที่นี่ มันไม่ได้ใช้กลอุบายที่คุณชี้ให้เห็นเพราะมันช้ากว่าวิธีไร้เดียงสาถึง 2 เท่า มีลิงก์จากPRไปยังเกณฑ์มาตรฐานที่ครอบคลุมของทั้งสองวิธี
Jaime

5

เมื่อมองแวบแรกดูเหมือนจะทำเคล็ดลับ:numpy.histogram

count, (amin, amax) = numpy.histogram(a, bins=1)

... แต่ถ้าคุณดูที่แหล่งที่มาของฟังก์ชั่นนั้นมันก็แค่เรียกa.min()และa.max()เป็นอิสระดังนั้นจึงไม่สามารถหลีกเลี่ยงข้อกังวลด้านประสิทธิภาพที่กล่าวถึงในคำถามนี้ได้ :-(

ในทำนองเดียวกันscipy.ndimage.measurements.extremaดูเหมือนเป็นไปได้ แต่ก็โทรออกa.min()และa.max()เป็นอิสระเช่นกัน


3
np.histogramไม่ได้ผลเสมอไปเนื่องจาก(amin, amax)ค่าที่ส่งคืนเป็นค่าต่ำสุดและค่าสูงสุดของ bin ถ้าผมมีตัวอย่างเช่นa = np.zeros(10), ผลตอบแทนnp.histogram(a, bins=1) (array([10]), array([-0.5, 0.5]))ผู้ใช้กำลังมองหา(amin, amax)= (0, 0) ในกรณีนั้น
eclark

3

มันคุ้มค่ากับความพยายามสำหรับฉัน แต่อย่างใดดังนั้นฉันจะเสนอวิธีแก้ปัญหาที่ยากและสง่างามที่สุดสำหรับใครก็ตามที่อาจสนใจ วิธีแก้ปัญหาของฉันคือการใช้ min-max แบบมัลติเธรดในอัลกอริทึมการส่งผ่านเดียวใน C ++ และใช้สิ่งนี้เพื่อสร้างโมดูลส่วนขยาย Python ความพยายามนี้ต้องใช้ค่าใช้จ่ายเล็กน้อยในการเรียนรู้วิธีใช้ Python และ NumPy C / C ++ API และที่นี่ฉันจะแสดงโค้ดและให้คำอธิบายและการอ้างอิงเล็ก ๆ น้อย ๆ สำหรับผู้ที่ต้องการลงเส้นทางนี้

หลายเธรดต่ำสุด / สูงสุด

ที่นี่ไม่มีอะไรน่าสนใจเกินไป length / workersอาร์เรย์ถูกแบ่งออกเป็นชิ้นขนาด ค่าต่ำสุด / สูงสุดจะคำนวณสำหรับแต่ละกลุ่มใน a futureซึ่งจะถูกสแกนหาค่าต่ำสุด / สูงสุดทั่วโลก

    // mt_np.cc
    //
    // multi-threaded min/max algorithm

    #include <algorithm>
    #include <future>
    #include <vector>

    namespace mt_np {

    /*
     * Get {min,max} in interval [begin,end)
     */
    template <typename T> std::pair<T, T> min_max(T *begin, T *end) {
      T min{*begin};
      T max{*begin};
      while (++begin < end) {
        if (*begin < min) {
          min = *begin;
          continue;
        } else if (*begin > max) {
          max = *begin;
        }
      }
      return {min, max};
    }

    /*
     * get {min,max} in interval [begin,end) using #workers for concurrency
     */
    template <typename T>
    std::pair<T, T> min_max_mt(T *begin, T *end, int workers) {
      const long int chunk_size = std::max((end - begin) / workers, 1l);
      std::vector<std::future<std::pair<T, T>>> min_maxes;
      // fire up the workers
      while (begin < end) {
        T *next = std::min(end, begin + chunk_size);
        min_maxes.push_back(std::async(min_max<T>, begin, next));
        begin = next;
      }
      // retrieve the results
      auto min_max_it = min_maxes.begin();
      auto v{min_max_it->get()};
      T min{v.first};
      T max{v.second};
      while (++min_max_it != min_maxes.end()) {
        v = min_max_it->get();
        min = std::min(min, v.first);
        max = std::max(max, v.second);
      }
      return {min, max};
    }
    }; // namespace mt_np

โมดูลส่วนขยาย Python

นี่คือสิ่งที่เริ่มน่าเกลียด ... วิธีหนึ่งในการใช้โค้ด C ++ ใน Python คือการใช้โมดูลส่วนขยาย โมดูลนี้สามารถสร้างและติดตั้งได้โดยใช้distutils.coreโมดูลมาตรฐาน คำอธิบายที่สมบูรณ์ของสิ่งที่สร้างความนี้จะกล่าวถึงในเอกสารหลาม: https://docs.python.org/3/extending/extending.html หมายเหตุ:มีวิธีอื่น ๆ ในการให้ได้ผลลัพธ์ที่คล้ายกันโดยอ้างถึงhttps://docs.python.org/3/extending/index.html#extending-index :

คู่มือนี้ครอบคลุมเฉพาะเครื่องมือพื้นฐานสำหรับการสร้างส่วนขยายที่ให้ไว้เป็นส่วนหนึ่งของ CPython เวอร์ชันนี้ เครื่องมือของบุคคลที่สามเช่น Cython, cffi, SWIG และ Numba นำเสนอทั้งวิธีการที่ง่ายและซับซ้อนกว่าในการสร้างส่วนขยาย C และ C ++ สำหรับ Python

โดยพื้นฐานแล้วเส้นทางนี้น่าจะเป็นวิชาการมากกว่าปฏิบัติ เมื่อพูดอย่างนั้นสิ่งที่ฉันทำต่อไปคือติดกับบทช่วยสอนสร้างไฟล์โมดูล นี่เป็นเอกสารสำเร็จรูปสำหรับผู้ที่ไม่สนใจที่จะรู้ว่าจะทำอย่างไรกับโค้ดของคุณและสร้างโมดูล Python จากมัน ก่อนที่จะดำเนินการใด ๆ คุณควรสร้างสภาพแวดล้อมเสมือน Python เพื่อที่คุณจะได้ไม่ก่อให้เกิดมลพิษต่อแพ็คเกจระบบของคุณ (ดูhttps://docs.python.org/3/library/venv.html#module-venv )

นี่คือไฟล์โมดูล:

// mt_np_forpy.cc
//
// C++ module implementation for multi-threaded min/max for np

#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION

#include <python3.6/numpy/arrayobject.h>

#include "mt_np.h"

#include <cstdint>
#include <iostream>

using namespace std;

/*
 * check:
 *  shape
 *  stride
 *  data_type
 *  byteorder
 *  alignment
 */
static bool check_array(PyArrayObject *arr) {
  if (PyArray_NDIM(arr) != 1) {
    PyErr_SetString(PyExc_RuntimeError, "Wrong shape, require (1,n)");
    return false;
  }
  if (PyArray_STRIDES(arr)[0] != 8) {
    PyErr_SetString(PyExc_RuntimeError, "Expected stride of 8");
    return false;
  }
  PyArray_Descr *descr = PyArray_DESCR(arr);
  if (descr->type != NPY_LONGLTR && descr->type != NPY_DOUBLELTR) {
    PyErr_SetString(PyExc_RuntimeError, "Wrong type, require l or d");
    return false;
  }
  if (descr->byteorder != '=') {
    PyErr_SetString(PyExc_RuntimeError, "Expected native byteorder");
    return false;
  }
  if (descr->alignment != 8) {
    cerr << "alignment: " << descr->alignment << endl;
    PyErr_SetString(PyExc_RuntimeError, "Require proper alignement");
    return false;
  }
  return true;
}

template <typename T>
static PyObject *mt_np_minmax_dispatch(PyArrayObject *arr) {
  npy_intp size = PyArray_SHAPE(arr)[0];
  T *begin = (T *)PyArray_DATA(arr);
  auto minmax =
      mt_np::min_max_mt(begin, begin + size, thread::hardware_concurrency());
  return Py_BuildValue("(L,L)", minmax.first, minmax.second);
}

static PyObject *mt_np_minmax(PyObject *self, PyObject *args) {
  PyArrayObject *arr;
  if (!PyArg_ParseTuple(args, "O", &arr))
    return NULL;
  if (!check_array(arr))
    return NULL;
  switch (PyArray_DESCR(arr)->type) {
  case NPY_LONGLTR: {
    return mt_np_minmax_dispatch<int64_t>(arr);
  } break;
  case NPY_DOUBLELTR: {
    return mt_np_minmax_dispatch<double>(arr);
  } break;
  default: {
    PyErr_SetString(PyExc_RuntimeError, "Unknown error");
    return NULL;
  }
  }
}

static PyObject *get_concurrency(PyObject *self, PyObject *args) {
  return Py_BuildValue("I", thread::hardware_concurrency());
}

static PyMethodDef mt_np_Methods[] = {
    {"mt_np_minmax", mt_np_minmax, METH_VARARGS, "multi-threaded np min/max"},
    {"get_concurrency", get_concurrency, METH_VARARGS,
     "retrieve thread::hardware_concurrency()"},
    {NULL, NULL, 0, NULL} /* sentinel */
};

static struct PyModuleDef mt_np_module = {PyModuleDef_HEAD_INIT, "mt_np", NULL,
                                          -1, mt_np_Methods};

PyMODINIT_FUNC PyInit_mt_np() { return PyModule_Create(&mt_np_module); }

ในไฟล์นี้มีการใช้ Python อย่างมากเช่นเดียวกับ NumPy API สำหรับข้อมูลเพิ่มเติมโปรดดูที่https://docs.python.org/3/c-api/arg.html#c.PyArg_ParseTupleและสำหรับ NumPy : https://docs.scipy.org/doc/numpy/reference/c-api.array.html

การติดตั้งโมดูล

สิ่งต่อไปที่ต้องทำคือใช้ distutils เพื่อติดตั้งโมดูล ต้องใช้ไฟล์ติดตั้ง:

# setup.py

from distutils.core import setup,Extension

module = Extension('mt_np', sources = ['mt_np_module.cc'])

setup (name = 'mt_np', 
       version = '1.0', 
       description = 'multi-threaded min/max for np arrays',
       ext_modules = [module])

ในการติดตั้งโมดูลในที่สุดให้ดำเนินการpython3 setup.py installจากสภาพแวดล้อมเสมือนของคุณ

การทดสอบโมดูล

สุดท้ายเราสามารถทดสอบเพื่อดูว่าการใช้งาน C ++ มีประสิทธิภาพดีกว่าการใช้ NumPy อย่างไร้เดียงสาจริงหรือไม่ ในการทำเช่นนั้นนี่คือสคริปต์ทดสอบง่ายๆ:

# timing.py
# compare numpy min/max vs multi-threaded min/max

import numpy as np
import mt_np
import timeit

def normal_min_max(X):
  return (np.min(X),np.max(X))

print(mt_np.get_concurrency())

for ssize in np.logspace(3,8,6):
  size = int(ssize)
  print('********************')
  print('sample size:', size)
  print('********************')
  samples = np.random.normal(0,50,(2,size))
  for sample in samples:
    print('np:', timeit.timeit('normal_min_max(sample)',
                 globals=globals(),number=10))
    print('mt:', timeit.timeit('mt_np.mt_np_minmax(sample)',
                 globals=globals(),number=10))

นี่คือผลลัพธ์ที่ฉันได้รับจากการทำทั้งหมดนี้:

8  
********************  
sample size: 1000  
********************  
np: 0.00012079699808964506  
mt: 0.002468645994667895  
np: 0.00011947099847020581  
mt: 0.0020772050047526136  
********************  
sample size: 10000  
********************  
np: 0.00024697799381101504  
mt: 0.002037393998762127  
np: 0.0002713389985729009  
mt: 0.0020942929986631498  
********************  
sample size: 100000  
********************  
np: 0.0007130410012905486  
mt: 0.0019842900001094677  
np: 0.0007540129954577424  
mt: 0.0029724110063398257  
********************  
sample size: 1000000  
********************  
np: 0.0094779249993735  
mt: 0.007134920000680722  
np: 0.009129883001151029  
mt: 0.012836456997320056  
********************  
sample size: 10000000  
********************  
np: 0.09471094200125663  
mt: 0.0453535050037317  
np: 0.09436299200024223  
mt: 0.04188535599678289  
********************  
sample size: 100000000  
********************  
np: 0.9537652180006262  
mt: 0.3957935369980987  
np: 0.9624398809974082  
mt: 0.4019058070043684  

สิ่งเหล่านี้ให้กำลังใจน้อยกว่าผลลัพธ์ที่ระบุไว้ก่อนหน้านี้ในเธรดซึ่งระบุไว้ที่ความเร็วประมาณ 3.5 เท่าและไม่รวมมัลติเธรด ผลลัพธ์ที่ฉันทำได้ค่อนข้างสมเหตุสมผลฉันคาดหวังว่าค่าใช้จ่ายของเธรดและจะครองเวลาจนกว่าอาร์เรย์จะมีขนาดใหญ่มากเมื่อถึงจุดนั้นการเพิ่มประสิทธิภาพจะเริ่มเข้าใกล้std::thread::hardware_concurrencyx เพิ่มขึ้น

สรุป

แน่นอนว่ามีช่องว่างสำหรับการปรับแต่งเฉพาะแอปพลิเคชันให้กับโค้ด NumPy บางรหัสโดยเฉพาะอย่างยิ่งในเรื่องของมัลติเธรด ไม่ว่าจะคุ้มค่ากับความพยายามหรือไม่นั้นก็ไม่ชัดเจนสำหรับฉัน แต่ดูเหมือนว่าจะเป็นการออกกำลังกายที่ดี (หรือบางอย่าง) ฉันคิดว่าบางทีการเรียนรู้ "เครื่องมือของบุคคลที่สาม" เช่น Cython อาจเป็นการใช้เวลาที่ดีกว่า แต่ใครจะรู้


1
ฉันเริ่มศึกษารหัสของคุณรู้จัก C ++ บ้าง แต่ยังไม่ได้ใช้ std :: future และ std :: async ที่ฟังก์ชันเทมเพลต 'min_max_mt' ของคุณจะรู้ได้อย่างไรว่าคนงานทุกคนเสร็จสิ้นระหว่างการเริ่มทำงานและการดึงผลลัพธ์ (ขอแค่ให้เข้าใจไม่ได้บอกว่ามีอะไรผิดปกติ)
ChrCury78

v = min_max_it->get();บรรทัด getวิธีการบล็อกจนกว่าผลที่ได้คือความพร้อมและส่งกลับไป เนื่องจากลูปผ่านไปแต่ละอนาคตมันจะไม่จบจนกว่าจะเสร็จสิ้นทั้งหมด future.get ()
Nathan Chappell

0

วิธีที่สั้นที่สุดที่ฉันคิดขึ้นมาคือ:

mn, mx = np.sort(ar)[[0, -1]]

แต่เนื่องจากมันเรียงลำดับอาร์เรย์จึงไม่ได้มีประสิทธิภาพสูงสุด

อีกวิธีสั้น ๆ คือ:

mn, mx = np.percentile(ar, [0, 100])

สิ่งนี้ควรมีประสิทธิภาพมากกว่า แต่คำนวณผลลัพธ์แล้วและจะส่งคืนค่าลอย


น่าเสียดายที่ทั้งสองเป็นวิธีแก้ปัญหาที่ช้าที่สุดเมื่อเทียบกับคนอื่น ๆ ในหน้านี้: m = np.min (a); M = np.max (a) -> 0.54002 ||| ม, M = f90_minmax1 (a) -> 0.72134 ||| ม, M = numba_minmax (a) -> 0.77323 ||| ม., M = np.sort (a) [[0, -1]] -> 12.01456 ||| ม, M = np.percentile (a, [0, 100]) -> 11.09418 ||| ในไม่กี่วินาทีสำหรับการทำซ้ำ 10,000 ครั้งสำหรับอาร์เรย์ขององค์ประกอบ 100k
Isaías

0

แรงบันดาลใจจากคำตอบก่อนหน้านี้ฉันได้เขียนการใช้งาน numba ที่ส่งคืนค่า minmax สำหรับแกน = 0 จากอาร์เรย์ 2 มิติ เร็วกว่าการโทรจำนวนนาที / สูงสุดประมาณ 5 เท่า อาจจะมีคนพบว่ามีประโยชน์

from numba import jit

@jit
def minmax(x):
    """Return minimum and maximum from 2D array for axis=0."""    
    m, n = len(x), len(x[0])
    mi, ma = np.empty(n), np.empty(n)
    mi[:] = ma[:] = x[0]
    for i in range(1, m):
        for j in range(n):
            if x[i, j]>ma[j]: ma[j] = x[i, j]
            elif x[i, j]<mi[j]: mi[j] = x[i, j]
    return mi, ma

x = np.random.normal(size=(256, 11))
mi, ma = minmax(x)

np.all(mi == x.min(axis=0)), np.all(ma == x.max(axis=0))
# (True, True)


%timeit x.min(axis=0), x.max(axis=0) 
# 15.9 µs ± 9.4 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
%timeit minmax(x) 
# 2.62 µs ± 31.3 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

อาจเร็วขึ้นเล็กน้อยหากคุณหลีกเลี่ยงmi[:] = ma[:] = x[0]และรวมส่วนนั้นของโค้ดไว้ในข้อความอื่นของลูปของคุณ ma[:] = x[0]ไม่มีอะไรมากไปกว่าการวนซ้ำเต็มรูปแบบบนอาร์เรย์มาซึ่งสามารถหลีกเลี่ยงได้
max9111
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.