6

I have the following data structures in numpy:

import numpy as np

a = np.random.rand(267, 173) # dense img matrix
b = np.random.rand(199) # array of probability samples

My goal is to take each entry i in b, find the x,y coordinates/index positions of all values in a that are <= i, then randomly select one of the values in that subset:

from random import randint

for i in b:
  l = np.argwhere(a <= i) # list of img coordinates where pixel <= i
  sample = l[randint(0, len(l)-1)] # random selection from `l`

This "works", but I'd like to vectorize the sampling operation (i.e. replace the for loop with apply_along_axis or similar). Does anyone know how this can be done? Any suggestions would be greatly appreciated!

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
duhaime
  • 25,611
  • 17
  • 169
  • 224
  • `apply_along_axis` is not true vectorize - it does not implement the loop in compiled code. – hpaulj Jul 30 '19 at 00:37

3 Answers3

6

You can't exactly vectorize np.argmax because you have a random subset size every time. What you can do though, is speed up the computation pretty dramatically with sorting. Sorting the image once will create a single allocation, while masking the image at every step will create a temporary array for the mask and for the extracted elements. With a sorted image, you can just apply np.searchsorted to get the sizes:

a_sorted = np.sort(a.ravel())
indices = np.searchsorted(a_sorted, b, side='right')

You still need a loop to do the sampling, but you can do something like

samples = np.array([a_sorted[np.random.randint(i)] for i in indices])

Getting x-y coordinates instead of sample values is a bit more complicated with this system. You can use np.unravel_index to get the indices, but first you must convert form the reference frame of a_sorted to a.ravel(). If you sort using np.argsort instead of np.sort, you can get the indices in the original array. Fortunately, np.searchsorted supports this exact scenario with the sorter parameter:

a_ind = np.argsort(a, axis=None)
indices = np.searchsorted(a.ravel(), b, side='right', sorter=a_ind)
r, c = np.unravel_index(a_ind[[np.random.randint(i) for i in indices]], a.shape)

r and c are the same size as b, and correspond to the row and column indices in a of each selection based on b. The index conversion depends on the strides in your array, so we'll assume that you're using C order, as 90% of arrays will do by default.

Complexity

Let's say b has size M and a has size N.

Your current algorithm does a linear search through each element of a for each element of b. At each iteration, it allocates a mask for the matching elements (N/2 on average), and then a buffer of the same size to hold the masked choices. This means that the time complexity is on the order of O(M * N) and the space complexity is the same.

My algorithm sorts a first, which is O(N log N). Then it searches for M insertion points, which is O(M log N). Finally, it selects M samples. The space it allocates is one sorted copy of the image and two arrays of size M. It is therefore of O((M + N) log N) time complexity and O(M + N) in space.

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
  • Ah, thanks @MadPhysicist! As it turns out, the x, y coords are the thing I'm after, not the value at those coordinates. I'm treating an image as a probability distribution where dark pixels have high prob. of being sampled and light pixels have low prob. of being sampled. I want to draw `n` samples from that distribution by creating `b` with `n` entries, and for each value `i` in `b`, selecting one pixel where the ink probability is `<= i`. So I want the index positions of values in `a`, not the values themselves. Is there a way to adapt what you have above to fetch those indices? – duhaime Jul 30 '19 at 01:31
  • @duhaime. Absolutely. Hang on just a sec – Mad Physicist Jul 30 '19 at 01:33
  • @duhaime. Enjoy – Mad Physicist Jul 30 '19 at 01:56
  • @MadPhysicist Very nice and thorough solution! Just one little nitpick. I think equality is not handled correctly, you probably need to add `side="right"` to the `searchsorted` call. – Paul Panzer Jul 30 '19 at 05:28
  • `r, c = np.unravel_index(a_ind[(np.random.rand(indices.size) * indices).astype(int)], a.shape)` may be a bit better vectorized – Daniel F Jul 30 '19 at 06:16
  • @Paul. I had that in my head when I was writing the answer, not got carried away. Thanks for reading carefully! – Mad Physicist Jul 30 '19 at 11:39
  • @DanielF. Excellent point. I've left that for your answer to implement – Mad Physicist Jul 30 '19 at 12:43
  • 1
    Awesome! I noticed passing 0 to `np.random.randint()` causes an explosion but one can just use `np.random.randint(i) if i else 0`. Thanks one million for this! – duhaime Jul 30 '19 at 13:22
  • 1
    @duhaime. A much better way is DanielF's suggestion. No Python looping at all needed. – Mad Physicist Jul 30 '19 at 15:11
  • Indeed! It turned out I was creating an awful if novel approach to image dithering and there are algorithms that produce better outputs (compare mine: https://gist.github.com/duhaime/17a2df9feef7941a9544df2f04d2708f to the Floyd-Steinberg algorithm: https://gist.github.com/duhaime/b98e710608d624d090a08ccb3b570542), but I'll use Daniel's if I need to perform similar lookups in the future! – duhaime Jul 30 '19 at 15:34
4

Here is an alternative approach argsorting b instead and then binning a accordingly using np.digitize and this post:

import numpy as np
from scipy import sparse
from timeit import timeit
import math

def h_digitize(a,bs,right=False):
    mx,mn = a.max(),a.min()
    asz = mx-mn
    bsz = bs[-1]-bs[0]
    nbins=int(bs.size*math.sqrt(bs.size)*asz/bsz)
    bbs = np.concatenate([[0],((nbins-1)*(bs-mn)/asz).astype(int).clip(0,nbins),[nbins]])
    bins = np.repeat(np.arange(bs.size+1), np.diff(bbs))
    bbs = bbs[:bbs.searchsorted(nbins)]
    bins[bbs] = -1
    aidx = bins[((nbins-1)*(a-mn)/asz).astype(int)]
    ambig = aidx == -1
    aa = a[ambig]
    if aa.size:
        aidx[ambig] = np.digitize(aa,bs,right)
    return aidx

def f_pp():
    bo = b.argsort()
    bs = b[bo]
    aidx = h_digitize(a,bs,right=True).ravel()
    aux = sparse.csr_matrix((aidx,aidx,np.arange(aidx.size+1)),
                            (aidx.size,b.size+1)).tocsc()
    ridx = np.empty(b.size,int)
    ridx[bo] = aux.indices[np.fromiter(map(np.random.randint,aux.indptr[1:-1].tolist()),int,b.size)]
    return np.unravel_index(ridx,a.shape)

def f_mp():
    a_ind = np.argsort(a, axis=None)
    indices = np.searchsorted(a.ravel(), b, sorter=a_ind, side='right')
    return np.unravel_index(a_ind[[np.random.randint(i) for i in indices]], a.shape)


a = np.random.rand(267, 173) # dense img matrix
b = np.random.rand(199) # array of probability samples

# round to test wether equality is handled correctly
a = np.round(a,3)
b = np.round(b,3)

print('pp',timeit(f_pp, number=1000),'ms')
print('mp',timeit(f_mp, number=1000),'ms')

# sanity checks

S = np.max([a[f_pp()] for _ in range(1000)],axis=0)
T = np.max([a[f_mp()] for _ in range(1000)],axis=0)
print(f"inequality satisfied: pp {(S<=b).all()} mp {(T<=b).all()}")
print(f"largest smalles distance to boundary: pp {(b-S).max()} mp {(b-T).max()}")
print(f"equality done right: pp {not (b-S).all()} mp {not (b-T).all()}")

Using a tweaked digitize I'm a bit faster but this may vary with problem size. Also, @MadPhysicist's solution is much less convoluted. With standard digitize we are about equal.

pp 2.620121960993856 ms                                                                                                                                                                                                                                                        
mp 3.301037881989032 ms                                                                                                                                                                                                                                                        
inequality satisfied: pp True mp True
largest smalles distance to boundary: pp 0.0040000000000000036 mp 0.006000000000000005
equality done right: pp True mp True
Paul Panzer
  • 51,835
  • 3
  • 54
  • 99
  • Wow, this is a solid deep dive! I'm going to use the more legible solution even if it slows me down a bit, just to make sure I can understand the code some years hence, but this is awesome for the truly diehard! – duhaime Jul 30 '19 at 13:21
2

A slight improvement on @MadPhysicist 's algorithm to make it more vectorized:

%%timeit
a_ind = np.argsort(a, axis=None)
indices = np.searchsorted(a.ravel(), b, sorter=a_ind)
r, c = np.unravel_index(a_ind[[np.random.randint(i) for i in indices]], a.shape)
100 loops, best of 3: 6.32 ms per loop

%%timeit
a_ind = np.argsort(a, axis=None)
indices = np.searchsorted(a.ravel(), b, sorter=a_ind)
r, c = np.unravel_index(a_ind[(np.random.rand(indices.size) * indices).astype(int)], a.shape)
100 loops, best of 3: 4.16 ms per loop

@PaulPanzer 's solution still rules the field, though I'm not sure what it's caching:

%timeit f_pp()
The slowest run took 14.79 times longer than the fastest. This could mean that an intermediate result is being cached.
100 loops, best of 3: 1.88 ms per loop
Daniel F
  • 13,620
  • 2
  • 29
  • 55