In this question我想办法用python计算最接近hyperbolic paraboloid的投影点。
由于答案,我能够使用下面的代码来计算最接近多个抛物线的点。
from scipy.optimize import minimize
# This function calculate the closest projection on a hyperbolic paraboloid
# As Answered by @Jaime https://stackoverflow.com/questions/18858448/speeding-up-a-closest-point-on-a-hyperbolic-paraboloid-algorithm
def fun_single(x, p0, p1, p2, p3, p):
u, v = x
s = u*(p1-p0) + v*(p3-p0) + u*v*(p2-p3-p1+p0) + p0
return np.linalg.norm(p-s)
# Example use case:
# Generate some random data for 3 random hyperbolic paraboloids
# A real life use case will count in the tens of thousands.
import numpy as np
COUNT = 3
p0 = np.random.random_sample((COUNT,3))
p1 = np.random.random_sample((COUNT,3))
p2 = np.random.random_sample((COUNT,3))
p3 = np.random.random_sample((COUNT,3))
p = np.random.random_sample(3)
uv = []
for i in xrange(COUNT):
uv.append(minimize(fun_single, (0.5, 0.5), (p0[i], p1[i], p2[i], p3[i], p)).x)
uv = np.array(uv)
# UV projections for my random data
#[[ 0.34109572 4.39237344]
# [-0.2720813 0.17083423]
# [ 0.48993333 -0.99415568]]
现在我对每个项目都有一个投影,可以找到更多有用的信息,例如哪个给定项最接近查询点,找到它的数组索引并从中获取更多数据等等...... / p>
为每个项目调用minimize
的问题是,在处理数十万个项目时,它变得非常慢。因此,为了尝试解决这个问题,我在改变函数时采取了一些措施来处理许多输入。
from numpy.core.umath_tests import inner1d
# This function calculate the closest projection to many hyperbolic paraboloids
def fun_array(x, p0, p1, p2, p3, p):
u, v = x
s = u*(p1-p0) + v*(p3-p0) + u*v*(p2-p3-p1+p0) + p0
V = p-s
return np.min(np.sqrt(inner1d(V,V)))
# Lets pass all the data to minimize
uv = minimize(fun_array, (0.5, 0.5), (p0, p1, p2, p3, p)).x
# Result: [ 0.25090064, 1.19732181]
# This corresponds to index 2 of my random data,
# which is the closest projection.
最小化函数fun_array
比迭代方法快得多,但它只返回单个最接近的投影,而不是所有投影。
是否可以使用minimize
返回所有投影,与迭代方法一样?如果没有,是否至少可以获得“获胜”数组元素的索引?
答案 0 :(得分:2)
你必须要狡猾,但欺骗minimize
并不困难。重点是minimize
only works for scalar cost functions。但是我们可以总结所有距离,因为它们自然是非负数量,全局最小值由每个距离最小的配置定义。因此,我们不是要求COUNT
双变量标量函数的最小点,而是要求COUNT*2
变量的单个标量函数的最小值。这恰好是COUNT
双变量函数的总和。但请注意,我并不相信这会更快,因为我可以想象高维最小搜索不如相应的一组低维独立最小搜索稳定。
你应该明确做的是预先为uv
分配内存并在其中插入值,而不是逐项逐项增加列表:
uv = np.empty((COUNT,2))
for i in xrange(COUNT):
uv[i,:] = minimize(fun_single, (0.5, 0.5), (p0[i], p1[i], p2[i], p3[i], p)).x
无论如何,为了使用minimize
的单个电话,我们只需要对您的功能进行矢量化,这比您想象的更容易:
def fun_vect(x, p0, p1, p2, p3, p):
x = x.reshape(-1,2) # dimensions are mangled by minimize() call
u,v = x.T[...,None] # u,v shaped (COUNT,1) for broadcasting
s = u*(p1-p0) + v*(p3-p0) + u*v*(p2-p3-p1+p0) + p0 # shape (COUNT,3)
return np.linalg.norm(p-s, axis=1).sum() # sum up distances for overall cost
x0 = 0.5*np.ones((COUNT,2))
uv_vect = minimize(fun_vect, x0, (p0, p1, p2, p3, p)).x.reshape(-1,2)
如您所见,此函数沿着列扩展标量。每行对应一个独立的最小化问题(与您对点的定义一致)。矢量化很简单,唯一不重要的部分是我们需要使用维度来确保所有内容都能很好地播放,我们应该注意重新设置x0
输入,因为minimize
有习惯展平数组值输入位置。当然,最终结果必须再次重塑。相应地,形状(COUNT,2)
的数组必须作为x0
提供,这是minimize
可以推断出问题维度的唯一特征。
我的随机数据比较:
>>> uv
array([[-0.13386872, 0.14324999],
[ 2.42883931, 0.55099395],
[ 1.03084756, 0.35847593],
[ 1.47276203, 0.29337082]])
>>> uv_vect
array([[-0.13386898, 0.1432499 ],
[ 2.42883952, 0.55099405],
[ 1.03085143, 0.35847888],
[ 1.47276244, 0.29337179]])
请注意,我将COUNT
更改为4,因为我希望在测试时保持每个维度不同。这样我就可以确定如果弄乱了我的尺寸,我会遇到错误。另请注意,一般情况下,您可能希望保留minimize
返回的完整对象,以确保一切正常并收敛。
正如我们在评论中所讨论的那样,上述解决方案---虽然完美地回答了问题 - 但并不是特别可行,因为运行时间太长,比单独进行每次最小化要长得多。问题很有趣,让我思考。为什么不尝试尽可能准确地解决问题?
您尝试做的事情(现在考虑单个双曲面和查询点q)是s(u,v)
s(u,v) = p0 + u * (p1 - p0) + v * (p3 - p0) + u * v * (p2 - p3 - p1 + p0)
点
d(s,q)
距离d(s,q)^2
最小。由于距离是适当的度量(特别是,它是非负的),这相当于最小化s
。到目前为止一切都很好。
让我们通过引入一些常量向量来重写s(u,v) = p0 + u*a + v*b + u*v*c
s - q = p0-q0 + u*a + v*b + u*v*c
= d + u*a + v*b + u*v*c
d(s,q)^2 = (s-q)^2
的参数化方程,以简化推导:
^
(在本节中u_min,v_min
将代表功率,因为这是线性代数。)现在,距离函数的最小值是一个静止点,所以在s(u,v)
点我们&#39}。重新查找u
相对于v
和d(s,q)^2
的渐变为零。这相当于说u
相对于v
和u
的导数必须同时为零;这给了我们两个非线性方程,其中包含未知数v
和2*(s-q)*ds/du = 0 (1)
2*(s-q)*ds/dv = 0 (2)
:
u
扩展这两个方程是一项有点单调乏味的工作。第一个等式恰好是v
中的线性,u
中的第二个等式。我在第一个等式中收集了包含u(v) = (-v^2*b.c - v*(c.d + a.b) - a.d)/(a + v*c)^2
的所有术语,这给了我关系
.
其中v
代表点积。上面的等式告诉我们,对于我们选择的u
,如果选择u(v)
,则将完全满足等式(1)。所以我们必须解决等式(2)。
我所做的是扩展等式(2)中的所有术语,并将u
替换为1,u,v,uv,u^2,u^2v
。原始方程的多项式项为(b.d + v*b^2)*f^2 - (c.d + a.b + 2*v*b.c)*e*f + (a.c + v*c^2)*e^2 = 0
,所以我可以告诉你这不是很好。通过一些没有分歧的小假设(在线拟合问题的情况下,这些分歧可能对应于垂直线的等价物),我们可以得出以下漂亮的等式:
e = v^2*b.c + v*(c.d + a.b) + a.d
f = (a + v*c)^2 = (a^2 + 2*v*a.c + v^2*c^2)
将新标量定义为
v
无论(u(v),v)
解决了这个等式,相应的v
点都将对应于距离的固定点。我们首先应该注意,这个等式考虑了minimize
的五阶多项式的根。保证至少有一个真正的根,在最坏的情况下,可以有多达5个真正的根。这些是否与最小值,最大值或(在不太可能的情况下)鞍点相对应是可以讨论的。
上述结果的真正好处是我们有机会找到所有等式的根源!这是一个巨大的交易,因为非线性根搜索/最小化通常一次只给你一个根,而不能告诉你你是否错过了任何一个。输入parametrization by Jaime。尽管围绕着它的所有线性代数绒毛,我们只是寻找多项式的(最多5个)根,我们可以测试距离并选择全局最小值(如果需要)。如果只有一个根,我们可以根据几何考虑确定它 最小值。
请注意,我还没有提到一个警告:多项式库一次只能使用一个多项式。我们仍然需要手动遍历每个双曲面。但是这里的交易是:我们将能够保证我们能够找到确切的最小值,而不是在不知不觉中接受本地距离最小值。它甚至可能比import numpy as np
# generate dummy inputs
COUNT = 100
p0 = np.random.random_sample((COUNT,3))
p1 = np.random.random_sample((COUNT,3))
p2 = np.random.random_sample((COUNT,3))
p3 = np.random.random_sample((COUNT,3))
p = np.random.random_sample(3)
def mydot(v1,v2):
'''generalized dot product for multidimensional arrays: (...,N,3)x(...,N,3) -> (...,N,1)'''
# (used in u_from_v for vectorized dot product)
return np.einsum('...j,...j->...',v1,v2)[...,None]
def u_from_v(v, a, b, c, d):
'''return u(v) corresponding to zero of gradient'''
# use mydot() instead of dot to enable array-valued v input
res = (- v**2*mydot(b,c) - v*(mydot(c,d)+mydot(a,b)) - mydot(a,d))/np.linalg.norm(a+v*c, axis=-1, keepdims=True)**2
return res.squeeze()
def check_distance(uv, p0, p1, p2, p3, p):
'''compute the distance from optimization results to query point'''
u,v = uv.T[...,None]
s = u*(p1-p0) + v*(p3-p0) + u*v*(p2-p3-p1+p0) + p0
return np.linalg.norm(p-s, axis=-1)
def poly_for_v(a, b, c, d):
'''return polynomial representation of derivative of d(s,p)^2 for the parametrized s(u(v),v) point'''
# only works with a scalar problem:( one polynomial at a time
# v is scalar, a-b-c-d are 3-dimensional vectors (for a given paraboloid)
# precompute scalar products appearing multiple times in the formula
ab = a.dot(b)
ac = a.dot(c)
cc = c.dot(c)
cd = c.dot(d)
bc = b.dot(c)
Poly = np.polynomial.polynomial.Polynomial
e = Poly([a.dot(d), cd+ab, bc])
f = Poly([a.dot(a), 2*ac, cc])
res = Poly([b.dot(d), b.dot(b)])*f**2 - Poly([cd+ab,2*bc])*e*f + Poly([ac,cc])*e**2
return res
def minimize_manually(p0, p1, p2, p3, p):
'''numpy polynomial version for the minimization problem'''
# auxiliary arrays, shape (COUNT,3)
a = p1 - p0
b = p3 - p0
c = p2 - p3 - p1 + p0
d = p0 - p
# preallocate for collected result
uv_min = np.empty((COUNT,2))
for k in range(COUNT):
# collect length-3 vectors needed for a given surface
aa,bb,cc,dd = (x[k,:] for x in (a,b,c,d))
# compute 5 complex roots of the derivative distance
roots = poly_for_v(aa, bb, cc, dd).roots()
# keep exactly real roots
vroots = roots[roots.imag==0].real
if vroots.size == 1:
# we're done here
vval, = vroots
uval = u_from_v(vval, aa, bb, cc, dd)
uv_min[k,:] = uval,vval
else:
# need to find the root with minimal distance
uvals = u_from_v(vroots[:,None], aa, bb, cc, dd)
uvtmp = np.stack((uvals,vroots),axis=-1)
dists = check_distance(uvtmp, p0[k,:], p1[k,:], p2[k,:], p3[k,:], p)
winner = np.argmin(dists) # index of (u,v) pair of minimum
uv_min[k,:] = uvtmp[winner,:]
return uv_min
uv_min = minimize_manually(p0, p1, p2, p3, p)
# for comparison with the minimize-based approaches:
# distances = check_distance(uv_manual,p0,p1,p2,p3,p))
更快。我们来看看:
COUNT
以上示例的COUNT=1
为100,但如果您从minimize
开始并继续运行minimize
版本和上述确切版本,您大致会看到一次在每10-20次运行中,基于COUNT=100
的方法错过了真正的最小值。所以上面的内容更安全,它可以保证找到合适的最小值。
我还使用COUNT=1000
进行了一些时序检查,对于基于多项式的解决方案,大约100 ms,对于基于最小化的循环版本,大约200 ms。 {{1}}:多项式为1秒,循环最小化为2秒。考虑到即使对于较大的问题,上述内容更精确,更有效,我认为没有理由不使用它。