Tingkatkan downsampling min/maks

Saya memiliki beberapa array besar (~100 juta poin) yang perlu saya plot secara interaktif. Saya saat ini menggunakan Matplotlib. Merencanakan array sebagaimana adanya menjadi sangat lambat dan sia-sia karena Anda tidak dapat memvisualisasikan poin sebanyak itu.

Jadi saya membuat fungsi penipisan min/maks yang saya ikat ke panggilan balik 'xlim_changed' dari sumbu. Saya menggunakan pendekatan min/maks karena datanya berisi lonjakan cepat yang tidak ingin saya lewatkan hanya dengan menelusuri datanya. Ada lebih banyak pembungkus yang dipotong hingga batas x, dan melewatkan pemrosesan dalam kondisi tertentu, tetapi bagian yang relevan ada di bawah:

def min_max_downsample(x,y,num_bins):
    """ Break the data into num_bins and returns min/max for each bin"""
    pts_per_bin = x.size // num_bins    

    #Create temp to hold the reshaped & slightly cropped y
    y_temp = y[:num_bins*pts_per_bin].reshape((num_bins, pts_per_bin))
    y_out      = np.empty((num_bins,2))
    #Take the min/max by rows.
    y_out[:,0] = y_temp.max(axis=1)
    y_out[:,1] = y_temp.min(axis=1)
    y_out = y_out.ravel()

    #This duplicates the x-value for each min/max y-pair
    x_out = np.empty((num_bins,2))
    x_out[:] = x[:num_bins*pts_per_bin:pts_per_bin,np.newaxis]
    x_out = x_out.ravel()
    return x_out, y_out

Ini berfungsi cukup baik dan cukup cepat (~80ms pada 1e8 poin & 2k bin). Ada sedikit jeda karena secara berkala menghitung ulang & memperbarui data x & y jalur.

Namun, satu-satunya keluhan saya ada pada x-data. Kode ini menduplikasi nilai x dari setiap tepi kiri wadah dan tidak mengembalikan lokasi x sebenarnya dari pasangan y min/maks. Saya biasanya mengatur jumlah nampan untuk menggandakan lebar piksel sumbu. Jadi Anda tidak bisa melihat perbedaannya karena tempat sampahnya sangat kecil...tapi saya tahu itu ada di sana... dan itu mengganggu saya.

Jadi coba nomor 2 yang mengembalikan nilai x aktual untuk setiap pasangan min/maks. Namun itu sekitar 5x lebih lambat.

def min_max_downsample_v2(x,y,num_bins):
    pts_per_bin = x.size // num_bins
    #Create temp to hold the reshaped & slightly cropped y
    y_temp = y[:num_bins*pts_per_bin].reshape((num_bins, pts_per_bin))
    #use argmax/min to get column locations
    cc_max = y_temp.argmax(axis=1)
    cc_min = y_temp.argmin(axis=1)    
    rr = np.arange(0,num_bins)
    #compute the flat index to where these are
    flat_max = cc_max + rr*pts_per_bin
    flat_min = cc_min + rr*pts_per_bin
    #Create a boolean mask of these locations
    mm_mask  = np.full((x.size,), False)
    mm_mask[flat_max] = True
    mm_mask[flat_min] = True  
    x_out = x[mm_mask]    
    y_out = y[mm_mask]  
    return x_out, y_out

Ini membutuhkan waktu sekitar 400+ ms di mesin saya yang menjadi cukup terlihat. Jadi pertanyaan saya pada dasarnya adalah apakah ada cara untuk bekerja lebih cepat dan memberikan hasil yang sama? Kemacetan sebagian besar terjadi pada fungsi numpy.argmin dan numpy.argmax yang sedikit lebih lambat dibandingkan numpy.min dan numpy.max.

Jawabannya mungkin hanya menggunakan versi #1 karena secara visual tidak terlalu penting. Atau mungkin mencoba mempercepatnya dengan sesuatu seperti cython (yang belum pernah saya gunakan).

FYI menggunakan Python 3.6.4 di Windows... contoh penggunaannya akan seperti ini:

x_big = np.linspace(0,10,100000000)
y_big = np.cos(x_big )
x_small, y_small = min_max_downsample(x_big ,y_big ,2000) #Fast but not exactly correct.
x_small, y_small = min_max_downsample_v2(x_big ,y_big ,2000) #correct but not exactly fast.

person Aero Engy    schedule 30.01.2019    source sumber
comment
Dari pertanyaan Anda, menurut saya ini dipanggil beberapa kali? Jika demikian, Anda dapat mempercepatnya dengan menggunakan metode set_data dari baris yang ada di matplotlib.   -  person user2699    schedule 31.01.2019
comment
@ user2699 Ya, itulah yang saya lakukan dengan keluaran fungsi ini. Apa yang saya posting adalah hambatan utama karena sering dipanggil saat saya menelusuri. Namun, saya mengubah bagian luar kode ini (tidak ditampilkan) sehingga lebih jarang dipanggil dengan melakukan buffering lebih banyak tanggal di luar batas x plot. Jadi itu hanya perlu dihitung ulang ketika Anda melampaui batas tertentu. Itu telah sedikit membantu pengalaman pengguna.   -  person Aero Engy    schedule 01.02.2019
comment
Saya pernah menghadapi masalah serupa (walaupun kurang dari 100 juta poin), dan dapat menyertakan bagian kode yang saya tulis yang relevan. Apa yang menurut saya paling efektif adalah menyimpan sinyal yang di-downsampling dan hanya melakukan downsampling jika hal itu membuat perbedaan besar dalam tampilan. Selama datanya (agak) diambil sampelnya secara seragam, memiliki titik x yang tepat tidaklah relevan.   -  person user2699    schedule 01.02.2019
comment
@ user2699 Saya ingin melihat bagaimana Anda melakukan caching. Saya sedang melakukan hal seperti itu sekarang. Pada dasarnya memuat data di luar batas x dengan jumlah tertentu dan membuat kepadatan titik sedikit lebih banyak dari yang dibutuhkan. Kemudian hal ini memicu penghitungan ulang baru hanya jika Anda memperbesar cukup jauh sehingga kepadatan titik menjadi rendah atau jika Anda menggeser melampaui tepi buffer. Saya ingin melihat bagaimana orang lain melakukannya karena saya mengarangnya sebagian besar dan saya tahu ada beberapa lubang logika. Punyaku kadang-kadang akan mengingat kembali secara sia-sia.   -  person Aero Engy    schedule 01.02.2019
comment
Saya sudah memasukkannya ke dalam jawaban. Beri tahu saya cara kerjanya untuk Anda, saya menggunakannya cukup kuat, tetapi mungkin ada masalah yang saya lewatkan.   -  person user2699    schedule 01.02.2019


Jawaban (4)


Saya berhasil mendapatkan peningkatan kinerja dengan menggunakan output arg(min|max) secara langsung untuk mengindeks array data. Hal ini memerlukan panggilan tambahan ke np.sort tetapi sumbu yang akan diurutkan hanya memiliki dua elemen (indeks min./maks.) dan keseluruhan array agak kecil (jumlah bin):

def min_max_downsample_v3(x, y, num_bins):
    pts_per_bin = x.size // num_bins

    x_view = x[:pts_per_bin*num_bins].reshape(num_bins, pts_per_bin)
    y_view = y[:pts_per_bin*num_bins].reshape(num_bins, pts_per_bin)
    i_min = np.argmin(y_view, axis=1)
    i_max = np.argmax(y_view, axis=1)

    r_index = np.repeat(np.arange(num_bins), 2)
    c_index = np.sort(np.stack((i_min, i_max), axis=1)).ravel()

    return x_view[r_index, c_index], y_view[r_index, c_index]

Saya memeriksa pengaturan waktu untuk contoh Anda dan saya memperoleh:

  • min_max_downsample_v1: 110 ms ± 5 ms
  • min_max_downsample_v2: 240 ms ± 8.01 ms
  • min_max_downsample_v3: 164 ms ± 1.23 ms

Saya juga memeriksa pengembalian langsung setelah panggilan ke arg(min|max) dan hasilnya sama 164 ms, yaitu tidak ada lagi overhead nyata setelah itu.

person a_guest    schedule 31.01.2019
comment
Trik pengindeksan yang bagus dan menghindari topeng. Saya tidak menyangka sort akan lebih cepat. Saya mendapatkan 87, 421, dan 338ms untuk v1, v2, dan v3. Saya ingin mengembalikannya ke kisaran di bawah 100 ms, tetapi saya akan melakukan setiap peningkatan yang bisa saya dapatkan. Namun, saya memperhatikan kode di atas akan kesalahan jika x tidak persis num_bin*pts_per_bin elements long ... yang sering terjadi. Itu sebabnya saya melakukan y_temp = y[:num_bins*pts_per_bin].reshape - person Aero Engy; 31.01.2019
comment
@AeroEngy Anda benar, termasuk itu. Bisakah Anda memeriksa hasil pengaturan waktu ketika Anda kembali tepat setelah panggilan arg(min|max) untuk melihat kontribusi dari panggilan berikutnya ke sort? Ada berbagai cara untuk menggabungkan indeks tetapi di mesin saya itu tidak masalah. - person a_guest; 31.01.2019
comment
Saya menjalankannya melalui profil dan mendapatkan total 339 md (142 md untuk argmax & 196 md untuk argmin). sort itu sepele di 45us. Jadi saya pikir Anda telah menghapus semua overhead lainnya. Untuk melakukannya lebih cepat diperlukan pendekatan baru yang belum diketahui. - person Aero Engy; 31.01.2019
comment
@AeroEngy Mungkin Anda dapat mengikuti pendekatan ini untuk mengkompilasi fungsi argmin_max Anda sendiri yang menghitung argmin dan argmax sekaligus dari mengulangi array dua kali. Untuk array sebesar itu, perbedaannya harus dapat diukur. - person a_guest; 31.01.2019
comment
Terima kasih untuk tautannya! Pertama-tama saya akan mencoba membuat versi numba dari single pass argminmax untuk melihat perbandingannya. Saya belum pernah menggunakan numba atau cython sebelumnya tetapi kelihatannya menarik. Saya baru saja pindah ke Python dari Matlab jadi saya terbiasa mempercepat kode dengan MEX yang sepertinya ide serupa. - person Aero Engy; 01.02.2019
comment
Menggunakan jawaban Anda + tautan itu membantu saya membuat versi yang bagus & cepat dengan numba. Saya mempostingnya sebagai jawaban terpisah untuk anak cucu tetapi kecuali jawaban Anda karena Anda menuntun saya ke jalan yang benar. Terima kasih lagi! - person Aero Engy; 01.02.2019

Jadi ini tidak membahas percepatan fungsi spesifik yang dimaksud, tetapi ini menunjukkan beberapa cara untuk memplot garis dengan banyak titik dengan cukup efektif. Ini mengasumsikan bahwa titik-titik x diurutkan dan diambil sampelnya secara seragam (atau mendekati seragam).

Mempersiapkan

from pylab import *

Inilah fungsi yang saya suka yang mengurangi jumlah poin dengan memilih satu poin secara acak di setiap interval. Ini tidak dijamin untuk menampilkan setiap puncak dalam data, namun tidak memiliki banyak masalah dibandingkan dengan menghancurkan data secara langsung, dan cepat.

def calc_rand(y, factor):
    split = y[:len(y)//factor*factor].reshape(-1, factor)
    idx = randint(0, split.shape[-1], split.shape[0])
    return split[arange(split.shape[0]), idx]

Dan inilah min dan max untuk melihat amplop sinyalnya

def calc_env(y, factor):
    """
    y : 1D signal
    factor : amount to reduce y by (actually returns twice this for min and max)
    Calculate envelope (interleaved min and max points) for y
    """
    split = y[:len(y)//factor*factor].reshape(-1, factor)
    upper = split.max(axis=-1)
    lower = split.min(axis=-1)
    return c_[upper, lower].flatten()

Fungsi berikut dapat mengambil salah satu dari ini, dan menggunakannya untuk mengurangi data yang diambil. Jumlah poin yang sebenarnya diambil adalah 5000 secara default, yang seharusnya jauh melebihi resolusi monitor. Data di-cache setelah dikurangi. Memori mungkin menjadi masalah, terutama dengan data dalam jumlah besar, namun tidak boleh melebihi jumlah yang dibutuhkan oleh sinyal asli.

def plot_bigly(x, y, *, ax=None, M=5000, red=calc_env, **kwargs):
    """
    x : the x data
    y : the y data
    ax : axis to plot on
    M : The maximum number of line points to display at any given time
    kwargs : passed to line
    """
    assert x.shape == y.shape, "x and y data must have same shape!"
    if ax is None:
        ax = gca()

    cached = {}

    # Setup line to be drawn beforehand, note this doesn't increment line properties so
    #  style needs to be passed in explicitly
    line = plt.Line2D([],[], **kwargs)
    def update(xmin, xmax):
        """
        Update line data

        precomputes and caches entire line at each level, so initial
        display may be slow but panning and zooming should speed up after that
        """
        # Find nearest power of two as a factor to downsample by
        imin = max(np.searchsorted(x, xmin)-1, 0)
        imax = min(np.searchsorted(x, xmax) + 1, y.shape[0])
        L = imax - imin + 1
        factor = max(2**int(round(np.log(L/M) / np.log(2))), 1)

        # only calculate reduction if it hasn't been cached, do reduction using nearest cached version if possible
        if factor not in cached:
            cached[factor] = red(y, factor=factor)

        ## Make sure lengths match correctly here, by ensuring at least
        #   "factor" points for each x point, then matching y length
        #  this assumes x has uniform sample spacing - but could be modified
        newx = x[imin:imin + ((imax-imin)//factor)* factor:factor]
        start = imin//factor
        newy = cached[factor][start:start + newx.shape[-1]]
        assert newx.shape == newy.shape, "decimation error {}/{}!".format(newx.shape, newy.shape)

        ## Update line data
        line.set_xdata(newx)
        line.set_ydata(newy)

    update(x[0], x[-1])
    ax.add_line(line)
    ## Manually update limits of axis, as adding line doesn't do this
    #   if drawing multiple lines this can quickly slow things down, and some
    #   sort of check should be included to prevent unnecessary changes in limits
    #   when a line is first drawn.
    ax.set_xlim(min(ax.get_xlim()[0], x[0]), max(ax.get_xlim()[1], x[1]))
    ax.set_ylim(min(ax.get_ylim()[0], np.min(y)), max(ax.get_ylim()[1], np.max(y)))

    def callback(*ignore):
        lims = ax.get_xlim()
        update(*lims)

    ax.callbacks.connect('xlim_changed', callback)

    return [line]

Berikut beberapa kode pengujian

L=int(100e6)
x=linspace(0,1,L)
y=0.1*randn(L)+sin(2*pi*18*x)
plot_bigly(x,y, red=calc_env)

Di mesin saya ini ditampilkan dengan sangat cepat. Zooming memang agak lag, apalagi jika dalam jumlah besar. Panning tidak memiliki masalah. Menggunakan pemilihan acak alih-alih min dan max sedikit lebih cepat, dan hanya mempunyai masalah pada tingkat zoom yang sangat tinggi.

person user2699    schedule 31.01.2019
comment
Terima kasih, saya tidak mempertimbangkan untuk menyimpan versi sampel di berbagai faktor seperti itu untuk digunakan nanti. Saya mungkin akan meminjamnya dan memasukkannya ke dalam rutinitas saya. - person Aero Engy; 01.02.2019

EDIT: Menambahkan parallel=True ke numba ... bahkan lebih cepat

Saya akhirnya membuat gabungan dari rutinitas single pass argmin+max dan pengindeksan yang ditingkatkan dari jawaban @a_guest dan tautan ke pertanyaan min maks simultan terkait ini.

Versi ini mengembalikan nilai x yang benar untuk setiap pasangan min/maks y dan berkat numba versi ini sebenarnya sedikit lebih cepat daripada versi "cepat tetapi kurang tepat".

from numba import jit, prange
@jit(parallel=True)
def min_max_downsample_v4(x, y, num_bins):
    pts_per_bin = x.size // num_bins
    x_view = x[:pts_per_bin*num_bins].reshape(num_bins, pts_per_bin)
    y_view = y[:pts_per_bin*num_bins].reshape(num_bins, pts_per_bin)    
    i_min = np.zeros(num_bins,dtype='int64')
    i_max = np.zeros(num_bins,dtype='int64')

    for r in prange(num_bins):
        min_val = y_view[r,0]
        max_val = y_view[r,0]
        for c in range(pts_per_bin):
            if y_view[r,c] < min_val:
                min_val = y_view[r,c]
                i_min[r] = c
            elif y_view[r,c] > max_val:
                max_val = y_view[r,c]
                i_max[r] = c                
    r_index = np.repeat(np.arange(num_bins), 2)
    c_index = np.sort(np.stack((i_min, i_max), axis=1)).ravel()        
    return x_view[r_index, c_index], y_view[r_index, c_index]

Membandingkan kecepatan menggunakan timeit menunjukkan kode numba kira-kira 2,6x lebih cepat dan memberikan hasil yang lebih baik dibandingkan v1. Ini sedikit lebih dari 10x lebih cepat daripada melakukan argmin & argmax numpy secara seri.

%timeit min_max_downsample_v1(x_big ,y_big ,2000)
96 ms ± 2.46 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%timeit min_max_downsample_v2(x_big ,y_big ,2000)
507 ms ± 4.75 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit min_max_downsample_v3(x_big ,y_big ,2000)
365 ms ± 1.27 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit min_max_downsample_v4(x_big ,y_big ,2000)
36.2 ms ± 487 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
person Aero Engy    schedule 01.02.2019
comment
Itu luar biasa! Saya tidak menyadari bahwa dengan Numba Anda dapat melakukan semuanya dengan Python. Kode yang dihasilkan sangat mudah dibaca dan mengintegrasikannya hanya dengan satu dekorator saja. Terima kasih telah membagikan kodenya :-) - person a_guest; 01.02.2019
comment
@a_guest Saya sangat terkejut dengan betapa mudahnya menggunakannya. Sebelum saya menambahkan prange dan parallel=True waktunya ~86ms yang membuat saya senang. Setelah menambahkannya, sungguh menakjubkan betapa cepatnya dibandingkan Numpy saja. Dibutuhkan sekitar 1/2 detik atau lebih untuk mengkompilasi pada penggunaan pertama tetapi ini sering dipanggil sehingga tidak masalah sama sekali. - person Aero Engy; 01.02.2019

Apakah Anda mencoba pyqtgraph untuk pembuatan plot interaktif? Ini lebih responsif daripada matplotlib.

Salah satu trik yang saya gunakan untuk downsampling adalah dengan menggunakan array_split dan hitung min dan maks untuk pemisahan. Pemisahan dilakukan berdasarkan jumlah sampel per piksel luas plot.

person danielhrisca    schedule 03.02.2019
comment
Saya sudah mencoba pyqtgraph dan tampaknya lebih cepat. Ini juga memiliki rutinitas downsampling min/maks yang bagus. Namun, saya tidak terlalu menyukai mekanisme plotnya. Jika Anda membaca pertanyaan/jawaban, Anda akan melihat bahwa saya melakukan min/maks dengan pembentukan ulang alih-alih array_split. reshape memiliki overhead yang jauh lebih sedikit karena mengembalikan satu tampilan array. array_split sepertinya menghasilkan list penayangan. Memisahkan array elemen 1e8 membutuhkan ~2 ms sementara pembentukan ulang ~520ns ... jadi pembentukan ulang 3800x lebih cepat di mesin saya. - person Aero Engy; 04.02.2019
comment
array_split memang jauh lebih lambat daripada pembentukannya kembali - person danielhrisca; 05.02.2019