La forma más rápida de hacer crecer una matriz numérica numpy

80

Requisitos:

  • Necesito hacer crecer una matriz arbitrariamente grande a partir de datos.
  • Puedo adivinar el tamaño (aproximadamente 100-200) sin garantías de que la matriz se ajuste siempre
  • Una vez que ha crecido hasta su tamaño final, necesito realizar cálculos numéricos en él, por lo que preferiría llegar eventualmente a una matriz numérica 2-D.
  • La velocidad es fundamental. Por ejemplo, para uno de los 300 archivos, el método update () se llama 45 millones de veces (tarda 150 s más o menos) y el método finalize () se llama 500k veces (toma un total de 106 s) ... tomando un total de 250 s más o menos.

Aquí está mi código:

def __init__(self):
    self.data = []

def update(self, row):
    self.data.append(row)

def finalize(self):
    dx = np.array(self.data)

Otras cosas que probé incluyen el siguiente código ... pero esto es mucho más lento.

def class A:
    def __init__(self):
        self.data = np.array([])

    def update(self, row):
        np.append(self.data, row)

    def finalize(self):
        dx = np.reshape(self.data, size=(self.data.shape[0]/5, 5))

Aquí hay un esquema de cómo se llama esto:

for i in range(500000):
    ax = A()
    for j in range(200):
         ax.update([1,2,3,4,5])
    ax.finalize()
    # some processing on ax
fodon
fuente
2
¿Necesita ser una matriz numpy antes de que termine? Si no es así, use una lista de listas y luego convierta cuando haya terminado.
Andrew Jaffe
1
@AndrewJaffe ¿Las listas de listas coinciden con la eficiencia de memoria de numpy?
AturSams

Respuestas:

96

Probé algunas cosas diferentes, con sincronización.

import numpy as np
  1. El método que mencionas como lento: (32.094 segundos)

    class A:
    
        def __init__(self):
            self.data = np.array([])
    
        def update(self, row):
            self.data = np.append(self.data, row)
    
        def finalize(self):
            return np.reshape(self.data, newshape=(self.data.shape[0]/5, 5))
    
  2. Lista de Python ol regular: (0,308 segundos)

    class B:
    
        def __init__(self):
            self.data = []
    
        def update(self, row):
            for r in row:
                self.data.append(r)
    
        def finalize(self):
            return np.reshape(self.data, newshape=(len(self.data)/5, 5))
    
  3. Intentando implementar una lista de matrices en numpy: (0.362 segundos)

    class C:
    
        def __init__(self):
            self.data = np.zeros((100,))
            self.capacity = 100
            self.size = 0
    
        def update(self, row):
            for r in row:
                self.add(r)
    
        def add(self, x):
            if self.size == self.capacity:
                self.capacity *= 4
                newdata = np.zeros((self.capacity,))
                newdata[:self.size] = self.data
                self.data = newdata
    
            self.data[self.size] = x
            self.size += 1
    
        def finalize(self):
            data = self.data[:self.size]
            return np.reshape(data, newshape=(len(data)/5, 5))
    

Y así es como lo cronometré:

x = C()
for i in xrange(100000):
    x.update([i])

Así que parece que las listas de Python antiguas y regulares son bastante buenas;)

Owen
fuente
1
Creo que la comparación es más clara con 60M actualizaciones y 500K finaliza llamadas. Parece que no ha llamado a finalizar en este ejemplo.
fodon
1
@fodon de hecho llamé a finalizar, una vez por ejecución (así que supongo que no tuvo mucho impacto). Pero esto me hace pensar que tal vez no entendí cómo crecen sus datos: si obtiene 60 millones en una actualización, estaba pensando que esto proporcionaría al menos 60 millones de datos para la próxima finalización.
Owen
@Owen 60M y 500 K medias de 60 millones y 500 mil llamadas a updatey finalizerespectivamente. Véase mi calendario revisado que prueba una proporción de 100: de 1 updateafinalize
Prashant Kumar
Actualicé la pregunta con un breve guión (que puede no ser sintácticamente correcto) para dar una idea de cómo funciona.
fodon
3
Tenga en cuenta que la tercera opción es superior cuando se está quedando sin memoria. La segunda opción requiere mucha memoria. La razón es que las listas de Python son matrices de referencias a valores, mientras que las matrices de NumPy son matrices reales de valores.
Fabianius
20

np.append () copia todos los datos de la matriz cada vez, pero la lista aumenta la capacidad en un factor (1,125). list es rápido, pero el uso de memoria es mayor que el de array. Puede usar el módulo de matriz de la biblioteca estándar de Python si le importa la memoria.

Aquí hay una discusión sobre este tema:

Cómo crear una matriz dinámica

HYRY
fuente
2
¿Hay alguna manera de cambiar el factor por el cual crece la lista?
fodon
1
np.append () consume tiempo y aumenta exponencialmente con el número de elementos.
Reloj ZHONG
1
^ lineal (es decir, el tiempo total acumulado es cuadrático), no exponencial.
user1111929
15

Usando las declaraciones de clase en la publicación de Owen, aquí hay un calendario revisado con algún efecto de finalización.

En resumen, encuentro que la clase C proporciona una implementación que es más de 60 veces más rápida que el método en la publicación original. (disculpas por la pared de texto)

El archivo que utilicé:

#!/usr/bin/python
import cProfile
import numpy as np

# ... class declarations here ...

def test_class(f):
    x = f()
    for i in xrange(100000):
        x.update([i])
    for i in xrange(1000):
        x.finalize()

for x in 'ABC':
    cProfile.run('test_class(%s)' % x)

Ahora, los tiempos resultantes:

UN:

     903005 function calls in 16.049 seconds

Ordered by: standard name

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     1    0.000    0.000   16.049   16.049 <string>:1(<module>)
100000    0.139    0.000    1.888    0.000 fromnumeric.py:1043(ravel)
  1000    0.001    0.000    0.003    0.000 fromnumeric.py:107(reshape)
100000    0.322    0.000   14.424    0.000 function_base.py:3466(append)
100000    0.102    0.000    1.623    0.000 numeric.py:216(asarray)
100000    0.121    0.000    0.298    0.000 numeric.py:286(asanyarray)
  1000    0.002    0.000    0.004    0.000 test.py:12(finalize)
     1    0.146    0.146   16.049   16.049 test.py:50(test_class)
     1    0.000    0.000    0.000    0.000 test.py:6(__init__)
100000    1.475    0.000   15.899    0.000 test.py:9(update)
     1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
100000    0.126    0.000    0.126    0.000 {method 'ravel' of 'numpy.ndarray' objects}
  1000    0.002    0.000    0.002    0.000 {method 'reshape' of 'numpy.ndarray' objects}
200001    1.698    0.000    1.698    0.000 {numpy.core.multiarray.array}
100000   11.915    0.000   11.915    0.000 {numpy.core.multiarray.concatenate}

SEGUNDO:

     208004 function calls in 16.885 seconds

Ordered by: standard name

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     1    0.001    0.001   16.885   16.885 <string>:1(<module>)
  1000    0.025    0.000   16.508    0.017 fromnumeric.py:107(reshape)
  1000    0.013    0.000   16.483    0.016 fromnumeric.py:32(_wrapit)
  1000    0.007    0.000   16.445    0.016 numeric.py:216(asarray)
     1    0.000    0.000    0.000    0.000 test.py:16(__init__)
100000    0.068    0.000    0.080    0.000 test.py:19(update)
  1000    0.012    0.000   16.520    0.017 test.py:23(finalize)
     1    0.284    0.284   16.883   16.883 test.py:50(test_class)
  1000    0.005    0.000    0.005    0.000 {getattr}
  1000    0.001    0.000    0.001    0.000 {len}
100000    0.012    0.000    0.012    0.000 {method 'append' of 'list' objects}
     1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
  1000    0.020    0.000    0.020    0.000 {method 'reshape' of 'numpy.ndarray' objects}
  1000   16.438    0.016   16.438    0.016 {numpy.core.multiarray.array}

C:

     204010 function calls in 0.244 seconds

Ordered by: standard name

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     1    0.000    0.000    0.244    0.244 <string>:1(<module>)
  1000    0.001    0.000    0.003    0.000 fromnumeric.py:107(reshape)
     1    0.000    0.000    0.000    0.000 test.py:27(__init__)
100000    0.082    0.000    0.170    0.000 test.py:32(update)
100000    0.087    0.000    0.088    0.000 test.py:36(add)
  1000    0.002    0.000    0.005    0.000 test.py:46(finalize)
     1    0.068    0.068    0.243    0.243 test.py:50(test_class)
  1000    0.000    0.000    0.000    0.000 {len}
     1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
  1000    0.002    0.000    0.002    0.000 {method 'reshape' of 'numpy.ndarray' objects}
     6    0.001    0.000    0.001    0.000 {numpy.core.multiarray.zeros}

La clase A es destruida por las actualizaciones, la clase B es destruida por los finalizados. La clase C es robusta frente a ambos.

Prashant Kumar
fuente
La actualización se realiza una vez y luego se llama a finalizar una vez. Todo este proceso se realiza m veces (de lo contrario, no hay datos para finalizar). Además, al comparar con la publicación original ... ¿te refieres a la primera (matriz.append + conversión numpy) o (numpy.append + remodelar)?
fodon
1
cProfile. Es la primera importación y la última línea invocada en mi fragmento de código.
Prashant Kumar
5

hay una gran diferencia de rendimiento en la función que utiliza para la finalización. Considere el siguiente código:

N=100000
nruns=5

a=[]
for i in range(N):
    a.append(np.zeros(1000))

print "start"

b=[]
for i in range(nruns):
    s=time()
    c=np.vstack(a)
    b.append((time()-s))
print "Timing version vstack ",np.mean(b)

b=[]
for i in range(nruns):
    s=time()
    c1=np.reshape(a,(N,1000))
    b.append((time()-s))

print "Timing version reshape ",np.mean(b)

b=[]
for i in range(nruns):
    s=time()
    c2=np.concatenate(a,axis=0).reshape(-1,1000)
    b.append((time()-s))

print "Timing version concatenate ",np.mean(b)

print c.shape,c2.shape
assert (c==c2).all()
assert (c==c1).all()

El uso de concatenar parece ser dos veces más rápido que la primera versión y más de 10 veces más rápido que la segunda versión.

Timing version vstack  1.5774928093
Timing version reshape  9.67419199944
Timing version concatenate  0.669512557983
Luca Fiaschi
fuente
1

Si desea mejorar el rendimiento con operaciones de lista, eche un vistazo a blist library. Es una implementación optimizada de la lista de Python y otras estructuras.

No lo comparé todavía, pero los resultados en su página parecen prometedores.

joaonrb
fuente