22
33Provides:
44 local_kernels_dict: Dictionary mapping kernel names to their implementations.
5+ RAM_BATCHING_SIZE: Max. RAM (in bytes) that can be used for batched compuation
6+ of Manhattan distance matrix for L_custom_py kernel. Can be modified before call
7+ using `local_kernels.RAM_BATCHING_SIZE = ...`.
58"""
69
710import os
811import ctypes
912import sysconfig
1013import warnings
14+ import itertools
1115import numpy as np
1216import sklearn .metrics .pairwise as _SKLEARN_PAIRWISE
1317from qstack .regression import __path__ as REGMODULE_PATH
1418
1519
20+ RAM_BATCHING_SIZE = 1024 ** 3 * 5 # 5GiB
21+
22+
23+ def compute_distance_matrix (R1 , R2 ):
24+ """Compute the Manhattan-distance matrix.
25+
26+ This computes (||r_1 - r_2||_1) between the samples of R1 and R2,
27+ using a batched python/numpy implementation,
28+ designed to be more memory-efficient than a single numpy call and faster than a simple python for loop.
29+
30+ This function is a batched-over-R1 implementation of the following code:
31+ `return np.sum( (R1[:,None, ...]-R2[None,:, ...])**2, axis=tuple(range(2, R1.ndim)))`
32+
33+ Args:
34+ R1 (numpy ndarray): First set of samples (can be multi-dimensional).
35+ R2 (numpy ndarray): Second set of samples.
36+
37+ Returns:
38+ numpy ndarray: squared-distance matrix of shape (len(R1), len(R2)).
39+
40+ Raises:
41+ RuntimeError: If X and Y have incompatible shapes.
42+ """
43+ if R1 .ndim != R2 .ndim or R1 .shape [1 :] != R2 .shape [1 :]:
44+ raise RuntimeError (f'incompatible shapes for R1 ({ R1 .shape :r} ) and R2 ({ R2 .shape :r} )' )
45+
46+ # determine batch size (batch should divide the larger dimention)
47+ if R1 .shape [0 ] < R2 .shape [0 ]:
48+ transpose_flag = True
49+ R2 ,R1 = R1 ,R2
50+ else :
51+ transpose_flag = False
52+ dtype = np .result_type (R1 ,R2 )
53+ out = np .zeros ((R1 .shape [0 ], R2 .shape [0 ]), dtype = dtype )
54+
55+ # possible weirdness: how is the layout of dtype done if dtype.alignment != dtype.itemsize?
56+ batch_size = int (np .floor (RAM_BATCHING_SIZE / (dtype .itemsize * np .prod (R2 .shape ))))
57+
58+ if batch_size == 0 :
59+ batch_size = 1
60+
61+ if min (R1 .shape [0 ],R2 .shape [0 ]) == 0 or batch_size >= R1 .shape [0 ]:
62+ dists = R1 [:,None ]- R2 [None ,:]
63+ #np.pow(dists, 2, out=dists) # For Euclidean distance
64+ np .abs (dists , out = dists )
65+ np .sum (dists , out = out , axis = tuple (range (2 ,dists .ndim )))
66+ else :
67+ dists = np .zeros ((batch_size , * R2 .shape ), dtype = dtype )
68+ batch_limits = np .minimum (np .arange (0 , R1 .shape [0 ]+ batch_size , step = batch_size ), R1 .shape [0 ])
69+ for batch_start , batch_end in itertools .pairwise (batch_limits ):
70+ dists_view = dists [:batch_end - batch_start ]
71+ R1_view = R1 [batch_start :batch_end , None , ...]
72+ np .subtract (R1_view , R2 [None ,:], out = dists_view )
73+ #np.pow(dists_view, 2, out=dists_view) # For Euclidean distance
74+ np .abs (dists_view , out = dists_view )
75+ np .sum (dists_view , out = out [batch_start :batch_end ], axis = tuple (range (2 ,dists .ndim )))
76+
77+ if transpose_flag :
78+ out = out .T
79+ return out
80+
81+
1682def custom_laplacian_kernel (X , Y , gamma ):
1783 """Compute Laplacian kernel between X and Y using Python implementation.
1884
@@ -31,16 +97,7 @@ def custom_laplacian_kernel(X, Y, gamma):
3197 """
3298 if X .shape [1 :] != Y .shape [1 :]:
3399 raise RuntimeError (f"Incompatible shapes { X .shape } and { Y .shape } " )
34-
35- def cdist (X , Y ):
36- K = np .zeros ((len (X ),len (Y )))
37- for i ,x in enumerate (X ):
38- x = np .array ([x ] * len (Y ))
39- d = np .abs (x - Y )
40- d = np .sum (d , axis = tuple (range (1 , len (d .shape ))))
41- K [i ,:] = d
42- return K
43- K = - gamma * cdist (X , Y )
100+ K = - gamma * compute_distance_matrix (X ,Y )
44101 np .exp (K , out = K )
45102 return K
46103
0 commit comments