NumPy: hàm cho đồng thời max () và min ()


109

numpy.amax () sẽ tìm giá trị tối đa trong một mảng và numpy.amin () thực hiện tương tự đối với giá trị nhỏ nhất. Nếu tôi muốn tìm cả max và min, tôi phải gọi cả hai hàm, hàm này yêu cầu chuyển qua mảng (rất lớn) hai lần, điều này có vẻ chậm.

Có một hàm nào trong API numpy tìm thấy cả tối đa và tối thiểu chỉ với một lần truyền dữ liệu không?


1
Lớn như thế nào là rất lớn? Nếu tôi nhận được một thời gian, tôi sẽ chạy một vài thử nghiệm so sánh việc thực hiện fortran đến amaxamin
mgilson

1
Tôi sẽ thừa nhận "rất lớn" là chủ quan. Trong trường hợp của tôi, tôi đang nói về các mảng có dung lượng vài GB.
Stuart Berg,

đó là khá lớn. Tôi đã mã hóa một ví dụ để tính toán nó trong fortran (ngay cả khi bạn không biết fortran, nó sẽ khá dễ hiểu về mã). Nó thực sự tạo ra sự khác biệt khi chạy nó từ fortran so với chạy qua numpy. (Có lẽ, bạn sẽ có thể có được hiệu suất tương tự từ C ...) Tôi không chắc - Tôi cho rằng chúng ta sẽ cần một nhà phát triển không có ý kiến ​​về lý do tại sao các chức năng của tôi hoạt động tốt hơn của họ ...
mgilson

Tất nhiên, đây không phải là một ý tưởng mới lạ. Ví dụ: thư viện boost minmax (C ++) cung cấp việc triển khai thuật toán mà tôi đang tìm kiếm.
Stuart Berg,

3
Không thực sự là một câu trả lời cho câu hỏi được hỏi, nhưng có lẽ là sự quan tâm của những người trên chủ đề này. Đã hỏi NumPy về việc thêm minmaxvào thư viện đang được đề cập ( github.com/numpy/numpy/issues/9836 ).
jakirkham

Câu trả lời:


49

Có một hàm nào trong API numpy tìm thấy cả tối đa và tối thiểu chỉ với một lần truyền dữ liệu không?

Không. Tại thời điểm viết bài này, không có chức năng nào như vậy. (Và có, nếu có được một chức năng như vậy, hiệu quả của nó sẽ là đáng kể hơn so với gọi numpy.amin()numpy.amax()liên tiếp trên một mảng lớn.)


31

Tôi không nghĩ rằng việc vượt qua mảng hai lần là một vấn đề. Hãy xem xét mã giả sau:

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

Trong khi chỉ có 1 vòng lặp ở đây, vẫn có 2 lần kiểm tra. (Thay vì có 2 vòng lặp với mỗi lần kiểm tra 1 lần). Thực sự điều duy nhất bạn tiết kiệm được là chi phí của 1 vòng lặp. Nếu các mảng thực sự lớn như bạn nói, chi phí đó sẽ nhỏ so với tải công việc thực tế của vòng lặp. (Lưu ý rằng tất cả điều này được thực hiện trong C, vì vậy các vòng lặp ít nhiều miễn phí).


EDIT Xin lỗi 4 bạn đã ủng hộ và đặt niềm tin vào tôi. Bạn chắc chắn có thể tối ưu hóa điều này.

Đây là một số mã fortran có thể được biên dịch thành một mô-đun python thông qua f2py (có thể một Cythonchuyên gia có thể đi cùng và so sánh mã này với phiên bản C được tối ưu hóa ...):

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

Biên dịch nó qua:

f2py -m untitled -c fortran_code.f90

Và bây giờ chúng tôi đang ở một nơi mà chúng tôi có thể kiểm tra nó:

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'

Kết quả hơi đáng kinh ngạc đối với tôi:

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

Tôi phải nói rằng, tôi không hoàn toàn hiểu nó. So sánh chỉnp.min so với minmax1minmax2vẫn là một trận chiến thua, vì vậy nó không chỉ là vấn đề trí nhớ ...

ghi chú - Việc tăng kích thước theo một hệ số 10**avà giảm lặp lại theo một hệ số 10**a(giữ cho kích thước vấn đề không đổi) làm thay đổi hiệu suất, nhưng không theo cách có vẻ nhất quán, điều này cho thấy rằng có một số tác động qua lại giữa hiệu suất bộ nhớ và chi phí cuộc gọi hàm trong con trăn. Ngay cả khi so sánh một cách minthực hiện đơn giản trong fortran cũng đánh bại numpy's bằng hệ số xấp xỉ 2 ...


21
Ưu điểm của một lần vượt qua là hiệu quả bộ nhớ. Đặc biệt nếu mảng của bạn đủ lớn để hoán đổi, điều này có thể rất lớn.
Dougal

4
Thats không hoàn toàn đúng, nó gần một nửa càng nhanh, bởi vì với các loại mảng, tốc độ bộ nhớ thường là yếu tố hạn chế, vì vậy nó có thể được một nửa càng nhanh ...
seberg

3
Không phải lúc nào bạn cũng cần hai séc. Nếu i < minvallà true, thì i > maxvalluôn là false, vì vậy trung bình bạn chỉ cần thực hiện 1,5 lần kiểm tra mỗi lần lặp khi lần thứ hai ifđược thay thế bằng dấu elif.
Fred Foo

2
Lưu ý nhỏ: Tôi nghi ngờ Cython là cách để có được mô-đun C có thể gọi được trong Python được tối ưu hóa nhất. Mục tiêu của Cython là trở thành một loại Python được chú thích kiểu, sau đó được dịch bằng máy sang C, trong khi f2pychỉ gói Fortran được mã hóa thủ công để Python có thể gọi được. Một bài kiểm tra "công bằng hơn" có lẽ là viết tay C và sau đó sử dụng f2py(!) Để bọc nó cho Python. Nếu bạn đang cho phép C ++, thì Shed Skin có thể là điểm tốt để cân bằng giữa việc mã hóa dễ dàng với hiệu suất.
John Y

4
kể từ khi numpy 1,8 phút và tối đa được vectơ hóa trên nền tảng amd64, trên core2duo numpy của tôi hoạt động tốt như mã fortran này. Nhưng một lần vượt qua sẽ có lợi nếu mảng vượt quá kích thước của bộ đệm cpu lớn hơn.
jtaylor

23

Có một hàm để tìm (max-min) được gọi là numpy.ptp nếu điều đó hữu ích cho bạn:

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

nhưng tôi không nghĩ có cách nào để tìm cả giá trị tối thiểu và tối đa với một đường truyền.

CHỈNH SỬA: ptp chỉ gọi tối thiểu và tối đa dưới mui xe


2
Thật khó chịu vì có lẽ cách ptp được triển khai nó phải theo dõi max và min!
Andy Hayden,

1
Hoặc nó chỉ có thể gọi là max và min, không chắc chắn
jterrace

3
lượt @hayden ra ptp chỉ gọi max và min
jterrace

1
Đó là mã mảng có mặt nạ; mã ndarray chính nằm trong C. Nhưng hóa ra mã C cũng lặp lại trên mảng hai lần: github.com/numpy/numpy/blob/… .
Ken Arnold

20

Bạn có thể sử dụng Numba , là một trình biên dịch Python động nhận biết NumPy sử dụng LLVM. Kết quả thực hiện khá đơn giản và rõ ràng:

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()))

Nó cũng sẽ nhanh hơn min() & max()việc triển khai của Numpy . Và tất cả mà không cần phải viết một dòng mã C / Fortran.

Thực hiện các bài kiểm tra hiệu suất của riêng bạn, vì nó luôn phụ thuộc vào kiến ​​trúc, dữ liệu của bạn, các phiên bản gói của bạn ...


2
> Nó cũng sẽ nhanh hơn việc triển khai min () & max () của Numpy. Tôi không nghĩ điều này là đúng. numpy không phải là python gốc - đó là C. `` x = numpy.random.rand (10000000) t = time () for i in range (1000): minmax (x) print ('numba', time () - t) t = time () cho tôi trong phạm vi (1000): x.min () x.max () print ('numpy', time () - t) `` Kết quả trong: ('numba', 10.299750089645386 ) ('numpy', 9.898081064224243)
Authman Apatira 13/03/17

1
@AuthmanApatira: Vâng, điểm chuẩn luôn như vậy, đó là lý do tại sao tôi nói nó " nên " (nhanh hơn) và " tự kiểm tra hiệu suất của riêng bạn, vì nó luôn phụ thuộc vào kiến ​​trúc của bạn, dữ liệu của bạn ... ". Trong trường hợp của tôi, tôi đã thử với 3 máy tính và nhận được kết quả tương tự (Numba nhanh hơn Numpy), nhưng trong máy tính của bạn kết quả có thể khác ... Bạn đã thử thực thi numbahàm một lần trước điểm chuẩn để đảm bảo rằng nó được biên dịch JIT chưa ? Ngoài ra, nếu bạn sử dụng ipython, vì đơn giản, tôi khuyên bạn nên sử dụng %timeit whatever_code()để đo thời gian thực thi.
Peque

3
@AuthmanApatira: Trong mọi trường hợp, điều tôi cố gắng thể hiện với câu trả lời này là đôi khi mã Python (trong trường hợp này là JIT được biên dịch bằng Numba) có thể nhanh bằng thư viện được biên dịch C nhanh nhất (ít nhất chúng ta đang nói về cùng một thứ tự độ lớn), thật ấn tượng khi chúng tôi không viết gì khác ngoài mã Python thuần túy, bạn có đồng ý không? ^^
Peque

Tôi đồng ý =) Ngoài ra, cảm ơn các mẹo trong bình luận trước về jupyter và biên dịch hàm một lần bên ngoài mã thời gian.
Authman Apatira

1
Chỉ gặp điều này, không phải nó quan trọng trong các trường hợp thực tế, nhưng nó elifcho phép số tiền tối thiểu của bạn lớn hơn giá trị tối đa của bạn. Ví dụ: với một mảng có độ dài 1, giá trị tối đa sẽ là bất kỳ giá trị nào đó, trong khi giá trị min là + vô cùng. Không có gì to tát đối với một lần xuất hiện, nhưng không phải mã tốt để ném sâu vào bụng của một con thú sản xuất.
Mike Williamson

12

Nói chung, bạn có thể giảm số lượng so sánh cho thuật toán minmax bằng cách xử lý hai phần tử tại một thời điểm và chỉ so sánh phần tử nhỏ hơn với mức tối thiểu tạm thời và phần tử lớn hơn với mức tối đa tạm thời. Trung bình một người chỉ cần 3/4 số so sánh hơn là một cách tiếp cận ngây thơ.

Điều này có thể được thực hiện bằng c hoặc fortran (hoặc bất kỳ ngôn ngữ cấp thấp nào khác) và gần như không thể đánh bại về mặt hiệu suất. Tôi đang sử dụng để minh họa nguyên tắc và có được một triển khai rất nhanh, không phụ thuộc vào loại hình:

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

Nó nhanh hơn hẳn so với cách tiếp cận ngây thơ mà Peque đã trình bày:

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

Như dự kiến, việc triển khai minmax mới chỉ mất khoảng 3/4 thời gian mà việc triển khai ngây thơ đã mất ( 2.1 / 2.75 = 0.7636363636363637)


1
Trên máy của tôi, giải pháp của bạn không nhanh hơn Peque. Numba 0,33.
John Zwinck

@johnzwinck bạn đã chạy điểm chuẩn trong câu trả lời của tôi có khác không? Nếu vậy bạn có thể chia sẻ nó? Nhưng có thể xảy ra: tôi cũng nhận thấy một số hồi quy trong các phiên bản mới hơn.
MSeifert

Tôi đã chạy điểm chuẩn của bạn. Thời gian của giải pháp của bạn và @ Peque khá giống nhau (~ 2,8 mili giây).
John Zwinck

@JohnZwinck Thật kỳ lạ, tôi vừa thử nghiệm lại và trên máy tính của tôi, nó nhanh hơn rõ ràng. Có thể điều đó liên quan đến numba và LLVM phụ thuộc vào phần cứng.
MSeifert

Tôi đã thử trên một máy khác (một máy trạm mạnh mẽ) và nhận được 2,4 mili giây cho máy của bạn so với 2,6 cho máy Peque. Vì vậy, một chiến thắng nhỏ.
John Zwinck

11

Chỉ để có một số ý tưởng về những con số mà người ta có thể mong đợi, với các cách tiếp cận sau:

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]

(các extrema_loop_*()cách tiếp cận tương tự như những gì được đề xuất ở đây , trong khi các extrema_while_*()cách tiếp cận dựa trên mã từ đây )

Thời gian sau:

bm

chỉ ra rằng extrema_while_*()nhanh nhất extrema_while_nb()là nhanh nhất. Trong mọi trường hợp, các giải pháp extrema_loop_nb()extrema_loop_cy()phương pháp cũng hoạt động tốt hơn so với cách tiếp cận chỉ NumPy (sử dụng np.max()np.min()riêng lẻ).

Cuối cùng, lưu ý rằng không cái nào trong số này linh hoạt như np.min()/ np.max()(về hỗ trợ n-dim, axistham số, v.v.).

(mã đầy đủ có sẵn ở đây )


2
Có vẻ như bạn có thể tăng thêm 10% tốc độ nếu sử dụng @njit (fastmath = True)extrema_while_nb
argenisleon

10

Không ai đề cập đến numpy.percentile , vì vậy tôi nghĩ tôi sẽ làm. Nếu bạn yêu cầu [0, 100]phân vị, nó sẽ cung cấp cho bạn một mảng gồm hai phần tử, tối thiểu (phân vị thứ 0) và tối đa (phân vị thứ 100).

Tuy nhiên, nó không đáp ứng mục đích của OP: nó không nhanh hơn min và max riêng biệt. Đó có thể là do một số máy móc sẽ cho phép các phân vị không cực (một vấn đề khó hơn, sẽ mất nhiều thời gian hơn).

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'

Phiên bản tương lai của Numpy có thể đặt vào một trường hợp đặc biệt để bỏ qua phép tính phần trăm thông thường nếu chỉ [0, 100]được yêu cầu. Không cần thêm bất cứ thứ gì vào giao diện, có một cách để hỏi Numpy về giá trị tối thiểu và tối đa trong một lần gọi (trái với những gì đã nói trong câu trả lời được chấp nhận), nhưng việc triển khai tiêu chuẩn của thư viện không tận dụng trường hợp này để thực hiện đáng giá.


9

Đây là một chủ đề cũ, nhưng dù sao đi nữa, nếu có ai xem lại ...

Khi tìm giá trị min và max đồng thời, có thể giảm số lần so sánh. Nếu nó là float mà bạn đang so sánh (mà tôi đoán là vậy), điều này có thể giúp bạn tiết kiệm thời gian, mặc dù không phức tạp về mặt tính toán.

Thay vì (mã 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]

trước tiên bạn có thể so sánh hai giá trị liền kề trong mảng và sau đó chỉ so sánh giá trị nhỏ hơn với giá trị tối thiểu hiện tại và giá trị lớn hơn với giá trị tối đa hiện tại:

## 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

Mã ở đây được viết bằng Python, rõ ràng đối với tốc độ, bạn sẽ sử dụng C hoặc Fortran hoặc Cython, nhưng theo cách này, bạn thực hiện 3 phép so sánh mỗi lần lặp, với lần lặp len (ar) / 2, cho phép so sánh 3/2 * len (ar). Ngược lại với điều đó, thực hiện phép so sánh "theo cách hiển nhiên" bạn thực hiện hai phép so sánh mỗi lần lặp, dẫn đến so sánh 2 * len (ar). Tiết kiệm cho bạn 25% thời gian so sánh.

Có thể một ngày nào đó ai đó sẽ thấy điều này hữu ích.


6
bạn đã đánh giá điều này chưa? trên phần cứng x86 hiện đại, bạn có các hướng dẫn máy cho tối thiểu và tối đa như được sử dụng trong biến thể đầu tiên, những hướng dẫn này tránh được sự cần thiết của các nhánh trong khi mã của bạn đặt phụ thuộc điều khiển có thể không ánh xạ tốt với phần cứng.
jtaylor

Tôi không thực sự. Sẽ làm nếu tôi có cơ hội. Tôi nghĩ rằng nó khá rõ ràng rằng mã python tinh khiết sẽ mất tay xuống để bất kỳ thực hiện biên soạn hợp lý, nhưng tôi tự hỏi, nếu sự tăng tốc có thể được nhìn thấy trong Cython ...
Bennet

13
Có một triển khai minmax trong numpy, dưới mui xe, được sử dụng bởi np.bincount, xem tại đây . Nó không sử dụng thủ thuật mà bạn đã chỉ ra, bởi vì nó hóa ra chậm hơn tới 2 lần so với cách tiếp cận ngây thơ. Có một liên kết từ PR đến một số tiêu chuẩn toàn diện của cả hai phương pháp.
Jaime

5

Thoạt nhìn, có vẻ như thực hiện thủ thuật:numpy.histogram

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

... nhưng nếu bạn nhìn vào nguồn của hàm đó, nó chỉ đơn giản gọi a.min()a.max()độc lập, và do đó không tránh được các mối quan tâm về hiệu suất được đề cập trong câu hỏi này. :-(

Tương tự, scipy.ndimage.measurements.extrematrông giống như một khả năng, nhưng nó cũng chỉ đơn giản là gọi a.min()a.max()độc lập.


3
np.histogramkhông phải lúc nào cũng hoạt động cho điều này vì các (amin, amax)giá trị trả về là giá trị tối thiểu và tối đa của thùng. Nếu tôi có, ví dụ a = np.zeros(10), np.histogram(a, bins=1)trả lại (array([10]), array([-0.5, 0.5])). Người dùng đang tìm kiếm (amin, amax)= (0, 0) trong trường hợp đó.
eclark,

3

Dù sao thì cũng đáng cho tôi nỗ lực, vì vậy tôi sẽ đề xuất giải pháp khó nhất và kém thanh lịch nhất ở đây cho những ai có thể quan tâm. Giải pháp của tôi là triển khai thuật toán min-max đa luồng trong một lần vượt qua trong C ++ và sử dụng thuật toán này để tạo mô-đun mở rộng Python. Nỗ lực này đòi hỏi một chút chi phí để học cách sử dụng các API Python và NumPy C / C ++, và ở đây tôi sẽ hiển thị mã và đưa ra một số giải thích và tham chiếu nhỏ cho những ai muốn đi theo con đường này.

Đa luồng Min / Max

Không có gì quá thú vị ở đây. Mảng được chia thành nhiều phần có kích thước length / workers. Tối thiểu / tối đa được tính cho từng đoạn trong a future, sau đó được quét để tìm giá trị tối thiểu / tối đa chung.

    // 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

Mô-đun mở rộng Python

Đây là nơi mọi thứ bắt đầu trở nên tồi tệ ... Một cách để sử dụng mã C ++ trong Python là triển khai một mô-đun mở rộng. Mô-đun này có thể được xây dựng và cài đặt bằng distutils.coremô-đun tiêu chuẩn. Mô tả đầy đủ về những gì điều này đòi hỏi được đề cập trong tài liệu Python: https://docs.python.org/3/exnking/exfining.html . LƯU Ý: chắc chắn có nhiều cách khác để có được kết quả tương tự, trích dẫn https://docs.python.org/3/exnking/index.html#exfining-index :

Hướng dẫn này chỉ bao gồm các công cụ cơ bản để tạo tiện ích mở rộng được cung cấp như một phần của phiên bản CPython này. Các công cụ của bên thứ ba như Cython, cffi, SWIG và Numba cung cấp các cách tiếp cận đơn giản hơn và phức tạp hơn để tạo các phần mở rộng C và C ++ cho Python.

Về cơ bản, lộ trình này có lẽ mang tính học thuật hơn là thực tế. Như đã nói, điều tôi làm tiếp theo là bám sát hướng dẫn, tạo một tệp mô-đun. Về cơ bản, đây là bản soạn sẵn cho các bản phân phối để biết phải làm gì với mã của bạn và tạo một mô-đun Python từ nó. Trước khi thực hiện bất kỳ điều gì trong số này, điều khôn ngoan là nên tạo một môi trường ảo Python để bạn không làm ô nhiễm các gói hệ thống của mình (xem https://docs.python.org/3/library/venv.html#module-venv ).

Đây là tệp mô-đun:

// 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); }

Trong tệp này, có một cách sử dụng đáng kể Python cũng như API NumPy, để biết thêm thông tin, hãy tham khảo: https://docs.python.org/3/c-api/arg.html#c.PyArg_ParseTuple và cho NumPy : https://docs.scipy.org/doc/numpy/reference/c-api.array.html .

Cài đặt mô-đun

Điều tiếp theo cần làm là sử dụng các bản phân phối để cài đặt mô-đun. Điều này yêu cầu một tệp thiết lập:

# 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])

Để cuối cùng cài đặt mô-đun, hãy thực thi python3 setup.py installtừ môi trường ảo của bạn.

Kiểm tra mô-đun

Cuối cùng, chúng ta có thể kiểm tra xem việc triển khai C ++ có thực sự vượt trội hơn việc sử dụng NumPy một cách ngây thơ hay không. Để làm như vậy, đây là một kịch bản thử nghiệm đơn giản:

# 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))

Đây là kết quả tôi nhận được từ việc làm tất cả những điều này:

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  

Đây là những kết quả không đáng khích lệ hơn nhiều so với kết quả chỉ ra trước đó trong luồng, chỉ ra đâu đó xung quanh tốc độ tăng gấp 3,5 lần và không kết hợp đa luồng. Kết quả mà tôi đạt được có phần hợp lý, tôi mong đợi rằng chi phí phân luồng và sẽ chiếm ưu thế về thời gian cho đến khi các mảng trở nên rất lớn, tại thời điểm đó, mức tăng hiệu suất sẽ bắt đầu tiếp cận với std::thread::hardware_concurrencymức tăng x.

Phần kết luận

Chắc chắn có chỗ cho các tối ưu hóa ứng dụng cụ thể đối với một số mã NumPy, có vẻ như, đặc biệt là liên quan đến đa luồng. Tôi không rõ liệu nó có xứng đáng với nỗ lực hay không, nhưng chắc chắn nó có vẻ như là một bài tập tốt (hoặc một cái gì đó). Tôi nghĩ rằng có lẽ học một số "công cụ của bên thứ ba" như Cython có thể sử dụng thời gian tốt hơn, nhưng ai biết được.


1
Tôi bắt đầu nghiên cứu mã của bạn, biết một số C ++ nhưng vẫn chưa sử dụng std :: future và std :: async. Tại hàm mẫu 'min_max_mt' của bạn, làm thế nào nó biết rằng mọi nhân viên đã hoàn thành giữa việc kích hoạt và truy xuất kết quả? (Yêu cầu chỉ để hiểu, không nói điều đó là bất cứ điều gì sai)
ChrCury78

Dòng v = min_max_it->get();. Các getkhối phương pháp cho đến khi kết quả là đã sẵn sàng và trả về nó. Vì vòng lặp đi qua mỗi tương lai, nó sẽ không kết thúc cho đến khi tất cả chúng được hoàn thành. future.get ()
Nathan Chappell

0

Cách ngắn nhất mà tôi nghĩ ra là:

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

Nhưng vì nó sắp xếp mảng, nó không phải là hiệu quả nhất.

Một cách ngắn gọn khác sẽ là:

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

Điều này sẽ hiệu quả hơn, nhưng kết quả được tính toán và một số thực được trả về.


Thật xấu hổ, hai giải pháp đó là những giải pháp chậm nhất so với những giải pháp khác trong trang này: m = np.min (a); M = np.max (a) -> 0,54002 ||| m, M = f90_minmax1 (a) -> 0,72134 ||| m, M = numba_minmax (a) -> 0,77323 ||| m, M = np.sort (a) [[0, -1]] -> 12.01456 ||| m, M = np.percentile (a, [0, 100]) -> 11.09418 ||| trong vài giây cho 10000 lần lặp lại cho một mảng 100k phần tử
Isaías
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.