NumPy: Funktion für simultane max () und min ()

109

numpy.amax () findet den Maximalwert in einem Array und numpy.amin () macht dasselbe für den Minimalwert . Wenn ich sowohl max als auch min finden möchte, muss ich beide Funktionen aufrufen, was erfordert, dass das (sehr große) Array zweimal durchlaufen wird, was langsam erscheint.

Gibt es eine Funktion in der Numpy-API, die sowohl max als auch min mit nur einem Durchgang durch die Daten findet?

Stuart Berg
quelle
1
Wie groß ist sehr groß? Wenn ich etwas Zeit habe, werde ich ein paar Tests durchführen, in denen eine fortran-Implementierung mit amaxundamin
mgilson
1
Ich gebe zu, dass "sehr groß" subjektiv ist. In meinem Fall spreche ich von Arrays mit einigen GB.
Stuart Berg
das ist ziemlich groß. Ich habe ein Beispiel codiert, um es in fortran zu berechnen (auch wenn Sie fortran nicht kennen, sollte es ziemlich einfach sein, den Code zu verstehen). Es macht wirklich einen Unterschied, ob es von fortran oder numpy läuft. (Vermutlich sollten Sie in der Lage sein, die gleiche Leistung von C zu erhalten ...) Ich bin mir nicht sicher - ich nehme an, wir würden einen numpy Entwickler brauchen, um zu kommentieren, warum meine Funktionen so viel besser funktionieren als ihre ...
mgilson
Dies ist natürlich kaum eine neuartige Idee. Zum Beispiel bietet die Boost- Minmax- Bibliothek (C ++) eine Implementierung des gesuchten Algorithmus.
Stuart Berg
3
Nicht wirklich eine Antwort auf die gestellte Frage, aber wahrscheinlich von Interesse für Leute in diesem Thread. Fragte NumPy nach dem Hinzufügen minmaxzur betreffenden Bibliothek ( github.com/numpy/numpy/issues/9836 ).
Jakirkham

Antworten:

49

Gibt es eine Funktion in der Numpy-API, die sowohl max als auch min mit nur einem Durchgang durch die Daten findet?

Nein. Zum Zeitpunkt dieses Schreibens gibt es keine solche Funktion. (Und ja, wenn es eine solche Funktion gäbe, wäre ihre Leistung deutlich besser als beim Aufrufen numpy.amin()und numpy.amax()nacheinander auf einem großen Array.)

Stuart Berg
quelle
31

Ich denke nicht, dass es ein Problem ist, zweimal über das Array zu gehen. Betrachten Sie den folgenden Pseudocode:

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

Während es hier nur 1 Schleife gibt, gibt es immer noch 2 Prüfungen. (Anstatt 2 Schleifen mit jeweils 1 Prüfung zu haben). Wirklich das einzige, was Sie sparen, ist der Overhead von 1 Schleife. Wenn die Arrays wirklich groß sind, wie Sie sagen, ist dieser Overhead im Vergleich zur tatsächlichen Arbeitslast der Schleife gering. (Beachten Sie, dass dies alles in C implementiert ist, sodass die Schleifen ohnehin mehr oder weniger frei sind.)


EDIT Entschuldigung an die 4 von euch, die gestimmt haben und an mich geglaubt haben. Sie können dies definitiv optimieren.

Hier ist ein Fortran-Code, der über in ein Python-Modul kompiliert werden kann f2py(vielleicht kann ein CythonGuru mitkommen und dies mit einer optimierten C-Version vergleichen ...):

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

Kompilieren Sie es über:

f2py -m untitled -c fortran_code.f90

Und jetzt sind wir an einem Ort, an dem wir es testen können:

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'

Die Ergebnisse sind für mich etwas umwerfend:

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

Ich muss sagen, ich verstehe es nicht ganz. Vergleicht man nur np.mingegenüber minmax1und minmax2ist nach wie vor eine verlorene Schlacht, es ist also nicht nur ein Speicherproblem ...

Hinweise - Das Erhöhen der Größe um den Faktor 10**aund das Verringern der Wiederholung um den Faktor 10**a(Halten der Problemgröße konstant) ändert die Leistung zwar, jedoch nicht auf eine scheinbar konsistente Weise, was zeigt, dass ein gewisses Zusammenspiel zwischen Speicherleistung und Funktionsaufruf-Overhead besteht Python. Selbst der Vergleich einer einfachen minImplementierung in fortran übertrifft die Anzahl um einen Faktor von ungefähr 2 ...

mgilson
quelle
21
Der Vorteil eines einzelnen Durchgangs ist die Speichereffizienz. Insbesondere wenn Ihr Array groß genug ist, um ausgetauscht zu werden, kann dies sehr groß sein.
Dougal
4
Das ist nicht ganz richtig, es ist fast halb so schnell, denn bei dieser Art von Arrays ist die Speichergeschwindigkeit normalerweise der begrenzende Faktor, so dass es halb so schnell sein kann ...
seberg
3
Sie brauchen nicht immer zwei Schecks. Wenn i < minvaltrue, i > maxvalist es immer false, sodass Sie durchschnittlich nur 1,5 Überprüfungen pro Iteration durchführen müssen, wenn die zweite ifdurch eine ersetzt wird elif.
Fred Foo
2
Kleiner Hinweis: Ich bezweifle, dass Cython der Weg ist, um das optimierteste Python-aufrufbare C-Modul zu erhalten. Cythons Ziel ist es, eine Art typbeschriftetes Python zu sein, das dann maschinell in C übersetzt wird, während f2pynur handcodiertes Fortran so verpackt wird, dass es von Python aufgerufen werden kann. Ein "gerechterer" Test besteht wahrscheinlich darin, C von Hand zu codieren und es dann mit f2py(!) Für Python zu verpacken. Wenn Sie C ++ zulassen, ist Shed Skin möglicherweise der ideale Ort, um die Vereinfachung der Codierung mit der Leistung in Einklang zu bringen.
John Y
4
Ab numpy sind 1,8 min und max auf amd64-Plattformen vektorisiert, auf meinem core2duo funktioniert numpy genauso gut wie dieser fortran-Code. Ein einzelner Durchgang wäre jedoch vorteilhaft, wenn das Array die Größe der größeren CPU-Caches überschreitet.
Taylor
23

Es gibt eine Funktion zum Finden (max-min) namens numpy.ptp, wenn dies für Sie nützlich ist:

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

Aber ich glaube nicht, dass es eine Möglichkeit gibt, mit einer Durchquerung sowohl Min als auch Max zu finden.

EDIT: ptp ruft nur min und max unter der Haube auf

jterrace
quelle
2
Es ist ärgerlich, weil vermutlich die Art und Weise, wie ptp implementiert wird, Max und Min im Auge behalten muss!
Andy Hayden
1
Oder es könnte einfach max und min anrufen, nicht sicher
jterrace
3
@ Hayden stellt sich heraus, ptp ruft nur max und min
jterrace
1
Das war der Masked-Array-Code; Der Haupt-ndarray-Code befindet sich in C. Es stellt sich jedoch heraus, dass der C-Code das Array auch zweimal durchläuft : github.com/numpy/numpy/blob/… .
Ken Arnold
20

Sie können Numba verwenden , einen NumPy-fähigen dynamischen Python-Compiler, der LLVM verwendet. Die resultierende Implementierung ist ziemlich einfach und klar:

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

Es sollte auch schneller sein als die min() & max()Implementierung eines Numpy . Und das alles, ohne eine einzige C / Fortran-Codezeile schreiben zu müssen.

Führen Sie Ihre eigenen Leistungstests durch, da dies immer von Ihrer Architektur, Ihren Daten, Ihren Paketversionen abhängt ...

Peque
quelle
2
> Es sollte auch schneller sein als die min () & max () Implementierung eines Numpy. Ich denke nicht, dass dies richtig ist. numpy ist keine native Python - es ist C. `` `x = numpy.random.rand (10000000) t = time () für i im Bereich (1000): minmax (x) print ('numba', time () - t) t = Zeit () für i im Bereich (1000): x.min () x.max () print ('numpy', Zeit () - t) `` `Ergebnisse in: ('numba', 10.299750089645386 ) ('numpy', 9.898081064224243)
Authman Apatira
1
@AuthmanApatira: Ja, Benchmarks sind immer so, deshalb habe ich gesagt, dass sie " sollten " (schneller sein) und " Ihre eigenen Leistungstests durchführen, da dies immer von Ihrer Architektur, Ihren Daten abhängt ... ". In meinem Fall habe ich es mit 3 Computern versucht und das gleiche Ergebnis erzielt (Numba war schneller als Numpy), aber in Ihrem Computer können die Ergebnisse abweichen ... Haben Sie einmal versucht, die numbaFunktion vor dem Benchmark auszuführen , um sicherzustellen, dass sie JIT-kompiliert ist ?. Wenn Sie der ipythonEinfachheit halber auch verwenden, würde ich Ihnen empfehlen, die Zeitausführung zu %timeit whatever_code()messen.
Peque
3
@AuthmanApatira: Auf jeden Fall habe ich versucht, mit dieser Antwort zu zeigen, dass Python-Code (in diesem Fall JIT-kompiliert mit Numba) manchmal so schnell sein kann wie die schnellste C-kompilierte Bibliothek (zumindest sprechen wir über dieselbe Reihenfolge von Größe), was beeindruckend ist, wenn man bedenkt, dass wir nichts anderes als reinen Python-Code geschrieben haben, stimmst du nicht zu? ^^
Peque
Ich stimme zu =) Vielen Dank auch für die Tipps im vorherigen Kommentar zu Jupyter und zum einmaligen Kompilieren der Funktion außerhalb des Timing-Codes.
Authman Apatira
1
Ich bin nur darauf gestoßen, nicht dass es in praktischen Fällen wichtig ist, aber das eliferlaubt, dass Ihr Minimum größer als Ihr Maximum ist. Bei einem Array der Länge 1 ist das Maximum beispielsweise der Wert, während min + unendlich ist. Keine große Sache für ein Einzelstück, aber kein guter Code, um tief in den Bauch eines Produktionstiers zu werfen.
Mike Williamson
12

Im Allgemeinen können Sie die Anzahl der Vergleiche für einen Minmax-Algorithmus reduzieren, indem Sie zwei Elemente gleichzeitig verarbeiten und nur das kleinere mit dem temporären Minimum und das größere mit dem temporären Maximum vergleichen. Im Durchschnitt braucht man nur 3/4 der Vergleiche als einen naiven Ansatz.

Dies könnte in c oder fortran (oder einer anderen einfachen Sprache) implementiert werden und sollte in Bezug auf die Leistung nahezu unschlagbar sein. Ich benutze um das Prinzip zu veranschaulichen und eine sehr schnelle, dtype-unabhängige Implementierung zu erhalten:

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

Es ist definitiv schneller als der naive Ansatz, den Peque vorgestellt hat:

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

Wie erwartet dauert die neue Minmax-Implementierung nur ungefähr 3/4 der Zeit, die die naive Implementierung benötigt hat ( 2.1 / 2.75 = 0.7636363636363637)

MSeifert
quelle
1
Auf meinem Computer ist Ihre Lösung nicht schneller als die von Peque. Numba 0,33.
John Zwinck
@johnzwinck hast du den Benchmark in meiner Antwort anders ausgeführt? Wenn ja, könnten Sie es teilen? Aber es ist möglich: Ich habe auch in neueren Versionen einige Regressionen bemerkt.
MSeifert
Ich habe Ihren Benchmark durchgeführt. Die Timings Ihrer Lösung und von @ Peque waren ziemlich gleich (~ 2,8 ms).
John Zwinck
@ JohnZwinck Das ist komisch, ich habe es gerade noch einmal getestet und auf meinem Computer ist es definitiv schneller. Vielleicht hat das etwas mit Numba und LLVM zu tun, das von der Hardware abhängt.
MSeifert
Ich habe es jetzt auf einer anderen Maschine versucht (einer bulligen Workstation) und 2,4 ms für Ihre gegenüber 2,6 ms für Peque's. Also ein kleiner Gewinn.
John Zwinck
11

Nur um ein paar Ideen zu den Zahlen zu bekommen, die man angesichts der folgenden Ansätze erwarten kann:

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]

(Die extrema_loop_*()Ansätze ähneln denen, die hier vorgeschlagen werden , während die extrema_while_*()Ansätze auf dem Code von hier basieren. )

Die folgenden Zeiten:

bm

zeigen an, dass die extrema_while_*()am schnellsten sind, wobei extrema_while_nb()sie am schnellsten sind. In jedem Fall übertreffen auch die extrema_loop_nb()und extrema_loop_cy()-Lösungen den Nur-NumPy-Ansatz (unter Verwendung np.max()und np.min()separat).

Beachten Sie schließlich, dass keines davon so flexibel ist wie np.min()/ np.max()(in Bezug auf n-dim-Unterstützung, axisParameter usw.).

(Der vollständige Code ist hier verfügbar. )

norok2
quelle
2
Scheint, als könnten Sie eine zusätzliche Geschwindigkeit von 10% erreichen, wenn Sie @njit (fastmath = True) verwendenextrema_while_nb
argenisleon
10

Niemand erwähnte numpy.percentile , also dachte ich, ich würde es tun . Wenn Sie nach [0, 100]Perzentilen fragen , erhalten Sie ein Array aus zwei Elementen, dem minimalen (0. Perzentil) und dem maximalen (100. Perzentil).

Es erfüllt jedoch nicht den Zweck des OP: Es ist nicht schneller als min und max getrennt. Dies liegt wahrscheinlich an einigen Maschinen, die nicht extreme Perzentile zulassen würden (ein schwierigeres Problem, das länger dauern sollte ).

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'

Eine zukünftige Version von Numpy könnte in einem Sonderfall die normale Perzentilberechnung überspringen, wenn dies nur [0, 100]angefordert wird. Ohne der Schnittstelle etwas hinzuzufügen, gibt es eine Möglichkeit, Numpy in einem Aufruf nach Min und Max zu fragen (im Gegensatz zu dem, was in der akzeptierten Antwort gesagt wurde), aber die Standardimplementierung der Bibliothek nutzt diesen Fall nicht aus, um dies zu erreichen lohnend.

Jim Pivarski
quelle
9

Dies ist ein alter Thread, aber trotzdem, wenn sich jemand das jemals wieder ansieht ...

Wenn Sie gleichzeitig nach Min und Max suchen, können Sie die Anzahl der Vergleiche reduzieren. Wenn es sich um Floats handelt, die Sie vergleichen (was ich denke), kann dies Ihnen Zeit sparen, wenn auch nicht die Komplexität der Berechnungen.

Anstelle von (Python-Code):

_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]

Sie können zuerst zwei benachbarte Werte im Array vergleichen und dann nur den kleineren mit dem aktuellen Minimum und den größeren mit dem aktuellen Maximum vergleichen:

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

Der Code hier ist in Python geschrieben. Aus Gründen der Geschwindigkeit würden Sie C oder Fortran oder Cython verwenden. Auf diese Weise führen Sie jedoch 3 Vergleiche pro Iteration mit len ​​(ar) / 2 Iterationen durch, was 3/2 * len (ar) Vergleiche ergibt. Im Gegensatz dazu führen Sie beim Vergleich "auf offensichtliche Weise" zwei Vergleiche pro Iteration durch, was zu 2 * len (ar) -Vergleichen führt. Spart Ihnen 25% der Vergleichszeit.

Vielleicht wird dies eines Tages jemand nützlich finden.

Bennet
quelle
6
Hast du das verglichen? Auf moderner x86-Hardware haben Sie Maschinenanweisungen für min und max, wie sie in der ersten Variante verwendet werden. Diese vermeiden die Notwendigkeit von Verzweigungen, während Ihr Code eine Steuerungsabhängigkeit aufweist, die wahrscheinlich nicht so gut der Hardware zugeordnet ist.
Taylor
Ich habe es eigentlich nicht getan. Wird tun, wenn ich eine Chance bekomme. Ich denke, es ist ziemlich klar, dass reiner Python-Code zweifellos an jede vernünftige kompilierte Implementierung verlieren wird, aber ich frage mich, ob in Cython eine Beschleunigung zu sehen ist ...
Bennet
13
Es gibt eine Minmax-Implementierung in Numpy unter der Haube, die von verwendet wird np.bincount, siehe hier . Es verwendet nicht den Trick, auf den Sie hinweisen, da es sich als bis zu 2x langsamer als der naive Ansatz herausstellte. Es gibt einen Link von der PR zu einigen umfassenden Benchmarks beider Methoden.
Jaime
5

Auf den ersten Blick scheint der Trick zu tun:numpy.histogram

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

... aber wenn Sie sich die Quelle für diese Funktion ansehen , ruft sie einfach a.min()und a.max()unabhängig auf und vermeidet daher nicht die in dieser Frage angesprochenen Leistungsprobleme. :-(

Ähnlich scipy.ndimage.measurements.extremasieht es nach einer Möglichkeit aus, aber es ruft auch einfach a.min()und a.max()unabhängig an.

Stuart Berg
quelle
3
np.histogramfunktioniert nicht immer dafür, da die zurückgegebenen (amin, amax)Werte für die minimalen und maximalen Werte des Fachs gelten. Wenn ich zum Beispiel a = np.zeros(10), np.histogram(a, bins=1)kehrt (array([10]), array([-0.5, 0.5])). Der Benutzer sucht (amin, amax)in diesem Fall nach = (0, 0).
Eclark
3

Die Mühe hat sich für mich sowieso gelohnt, deshalb werde ich hier die schwierigste und am wenigsten elegante Lösung für jeden vorschlagen, der interessiert sein könnte. Meine Lösung besteht darin, einen Multithread-Min-Max-Algorithmus in einem Durchgang in C ++ zu implementieren und damit ein Python-Erweiterungsmodul zu erstellen. Dieser Aufwand erfordert ein wenig Aufwand, um die Verwendung der Python- und NumPy C / C ++ - APIs zu erlernen. Hier werde ich den Code zeigen und einige kleine Erklärungen und Referenzen für alle geben, die diesen Weg beschreiten möchten.

Multithread Min / Max

Hier ist nichts zu interessant. Das Array ist in große Teile unterteilt length / workers. Das min / max wird für jeden Block in a berechnet future, der dann nach dem globalen min / max gescannt wird.

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

Das Python-Erweiterungsmodul

Hier wird es hässlich ... Eine Möglichkeit, C ++ - Code in Python zu verwenden, besteht darin, ein Erweiterungsmodul zu implementieren. Dieses Modul kann mit dem distutils.coreStandardmodul erstellt und installiert werden . Eine vollständige Beschreibung dessen, was dies bedeutet, finden Sie in der Python-Dokumentation: https://docs.python.org/3/extending/extending.html . HINWEIS: Es gibt sicherlich andere Möglichkeiten, ähnliche Ergebnisse zu erzielen, um https://docs.python.org/3/extending/index.html#extending-index zu zitieren :

Dieses Handbuch behandelt nur die grundlegenden Tools zum Erstellen von Erweiterungen, die als Teil dieser Version von CPython bereitgestellt werden. Tools von Drittanbietern wie Cython, cffi, SWIG und Numba bieten sowohl einfachere als auch komplexere Ansätze zum Erstellen von C- und C ++ - Erweiterungen für Python.

Im Wesentlichen ist dieser Weg wahrscheinlich eher akademisch als praktisch. Nachdem dies gesagt wurde, habe ich als nächstes eine Moduldatei erstellt, indem ich mich ziemlich nah an das Tutorial gehalten habe. Dies ist im Wesentlichen ein Boilerplate, damit Distutils wissen, was mit Ihrem Code zu tun ist, und daraus ein Python-Modul erstellen. Bevor Sie dies tun, ist es wahrscheinlich ratsam, eine virtuelle Python- Umgebung zu erstellen, damit Sie Ihre Systempakete nicht verschmutzen (siehe https://docs.python.org/3/library/venv.html#module-venv ).

Hier ist die Moduldatei:

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

In dieser Datei werden Python und die NumPy-API in erheblichem Umfang verwendet. Weitere Informationen finden Sie unter: https://docs.python.org/3/c-api/arg.html#c.PyArg_ParseTuple und für NumPy : https://docs.scipy.org/doc/numpy/reference/c-api.array.html .

Modul installieren

Als nächstes müssen Sie distutils verwenden, um das Modul zu installieren. Dies erfordert eine Setup-Datei:

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

Um das Modul endgültig zu installieren, führen Sie es python3 setup.py installin Ihrer virtuellen Umgebung aus.

Testen des Moduls

Schließlich können wir testen, ob die C ++ - Implementierung tatsächlich die naive Verwendung von NumPy übertrifft. Dazu ein einfaches Testskript:

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

Hier sind die Ergebnisse, die ich dabei erzielt habe:

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  

Diese sind weitaus weniger ermutigend als die Ergebnisse früher im Thread, die eine etwa 3,5-fache Beschleunigung anzeigten und kein Multithreading enthielten. Die Ergebnisse, die ich erzielt habe, sind einigermaßen vernünftig. Ich würde erwarten, dass der Aufwand für das Threading und die Zeit dominieren, bis die Arrays sehr groß werden. Ab diesem Zeitpunkt würde sich die Leistungssteigerung dem std::thread::hardware_concurrencyx-Anstieg nähern .

Fazit

Es scheint sicherlich Raum für anwendungsspezifische Optimierungen für einige NumPy-Codes zu geben, insbesondere im Hinblick auf Multithreading. Ob sich die Mühe lohnt oder nicht, ist mir nicht klar, aber es scheint sicherlich eine gute Übung (oder so) zu sein. Ich denke, dass das Erlernen einiger dieser "Tools von Drittanbietern" wie Cython möglicherweise eine bessere Zeitnutzung darstellt, aber wer weiß.

Nathan Chappell
quelle
1
Ich fange an, Ihren Code zu studieren, kenne mich mit C ++ aus, verwende aber immer noch nicht std :: future und std :: async. Woher weiß es bei Ihrer Vorlagenfunktion 'min_max_mt', dass jeder Mitarbeiter zwischen dem Brennen und dem Abrufen der Ergebnisse fertig ist? (Nur um zu verstehen, ohne zu sagen, dass etwas daran falsch ist)
ChrCury78
Die Linie v = min_max_it->get();. Die getMethode blockiert, bis das Ergebnis fertig ist, und gibt es zurück. Da die Schleife jede Zukunft durchläuft, wird sie erst beendet, wenn alle abgeschlossen sind. future.get ()
Nathan Chappell
0

Der kürzeste Weg, den ich mir ausgedacht habe, ist folgender:

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

Aber da es das Array sortiert, ist es nicht das effizienteste.

Ein anderer kurzer Weg wäre:

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

Dies sollte effizienter sein, aber das Ergebnis wird berechnet und ein Float zurückgegeben.

Israel Unterman
quelle
Schändlicherweise sind diese beiden Lösungen die langsamsten im Vergleich zu anderen auf dieser Seite: 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.Perzentil (a, [0, 100]) -> 11.09418 ||| in Sekunden für 10000 Wiederholungen für ein Array von 100.000 Elementen
Isaías