为什么我的PCA不变于旋转和轴交换?

如何解决为什么我的PCA不变于旋转和轴交换?

我有一个尺寸为3x3x3的体素(np.array),其中填充了一些值,此设置对我来说至关重要。我想要它的旋转不变表示。对于这种情况,我决定尝试PCA表示,该表示被认为是invariant to orthogonal transformationsanother

为简单起见,我进行了一些轴交换,但是如果我弄错了,可能会有np.rot90

我将3d体素作为一组加权的3d立方体点向量进行了穿插处理,我错误地将其称为“基础”,总共27个(因此,这是一组空间中的3d点,由从立方体点获得的向量表示,通过体素值缩放)。

import numpy as np

voxel1 = np.random.normal(size=(3,3,3))
voxel2 =  np.transpose(voxel1,(1,2)) #np.rot90(voxel1) #


basis = []
for i in range(3):
    for j in range(3):
        for k in range(3):
            basis.append([i+1,j+1,k+1]) # avoid 0
basis = np.array(basis)


voxel1 = voxel1.reshape((27,1))
voxel2 = voxel2.reshape((27,1))

voxel1 = voxel1*basis # weighted basis vectors
voxel2 = voxel2*basis
print(voxel1.shape)
(27,3)

然后我对这27个3维向量进行了PCA:

def pca(x):
    center = np.mean(x,0)
    x = x - center

    cov = np.cov(x.T) / x.shape[0]

    e_values,e_vectors = np.linalg.eig(cov)

    order = np.argsort(e_values)

    v = e_vectors[:,order].transpose()

    return x.dot(v)

vp1 = pca(voxel1)
vp2 = pca(voxel2)

但是vp1vp2中的结果不同。也许,我有一个错误(尽管我相信这是正确的公式),并且正确的代码必须是

x.dot(v.T)

但是在这种情况下,结果非常奇怪。转写数据的上部和下部与符号相同:

>>> np.abs(np.abs(vp1)-np.abs(vp2)) > 0.01
array([[False,False,False],[False,[ True,True,True],False]])

我做错了什么?

我想要做的是找到我的加权体素的一些不变表示,例如根据惯性轴或主轴进行定位。如果有人帮助我,我将不胜感激。

UPD:Found the question similar to mine,但代码不可用

EDIT2:找到了代码InertiaRotate,并成功地完成了以下操作:

import numpy as np

# https://github.com/smparker/orient-molecule/blob/master/orient.py

voxel1 = np.random.normal(size=(3,2))

voxel1 = voxel1.reshape((27,))
voxel2 = voxel2.reshape((27,))


basis = []
for i in range(3):
    for j in range(3):
        for k in range(3):
            basis.append([i+1,k+1]) # avoid 0
basis = np.array(basis)
basis = basis - np.mean(basis,axis=0)



def rotate_func(data,mass):

    #mass = [ masses[n.lower()] for n in geom.names ]

    inertial_tensor = -np.einsum("ax,a,ay->xy",data,mass,data)
    # negate sign to reverse the sorting of the tensor
    eig,axes = np.linalg.eigh(-inertial_tensor)
    axes = axes.T

    # adjust sign of axes so third moment moment is positive new in X,and Y axes
    testcoords = np.dot(data,axes.T) # a little wasteful,but fine for now
    thirdmoment = np.einsum("ax,a->x",testcoords**3,mass)

    for i in range(2):
        if thirdmoment[i] < 1.0e-6:
            axes[i,:] *= -1.0

    # rotation matrix must have determinant of 1
    if np.linalg.det(axes) < 0.0:
        axes[2,:] *= -1.0

    return axes

axes1 = rotate_func(basis,voxel1)
v1 = np.dot(basis,axes1.T)
axes2 = rotate_func(basis,voxel2)
v2 = np.dot(basis,axes2.T)


print(v1)
print(v2)

似乎分别使用基础(坐标)和质量。结果与上面的问题非常相似:转换后的数据的某些部分与符号匹配,我相信这些是立方体的一些侧面

print(np.abs(np.abs(v1)-np.abs(v2)) > 0.01)

[[False False False]
 [False False False]
 [False False False]
 [ True  True  True]
 [ True  True  True]
 [ True  True  True]
 [ True  True  True]
 [False False False]
 [ True  True  True]
 [ True  True  True]
 [ True  True  True]
 [ True  True  True]
 [False False False]
 [False False False]
 [False False False]
 [ True  True  True]
 [ True  True  True]
 [ True  True  True]
 [ True  True  True]
 [False False False]
 [ True  True  True]
 [ True  True  True]
 [ True  True  True]
 [ True  True  True]
 [False False False]
 [False False False]
 [False False False]]

正在寻找一些解释。该代码是为分子设计的,必须有效...

UPD:尝试从这24个向量中选择3个向量作为新的基础-一个向量的范数最大,一个向量的最小积和叉积。将它们组合到矩阵V中,然后使用公式V ^(-1)* X变换坐标,并遇到相同的问题-对于旋转的体素,所得向量集不相等。


UPD2:我同意柴可夫斯基的观点,即将体素矢量乘以权重从而创建一些非立方点云的想法是错误的。也许,我们确实需要采取旋转“基础”的解决方案(是的,这不是基础,而是确定点云的一种方法),当“基础”相同时,权重将根据3D旋转。

根据meTchaikovsky提供的答案和参考,发现other answers,我们和我的朋友一起得出的结论是,上述分子包装中的rotate_func试图发明一些计算符号的惯例。组件。他们的解决方案尝试将第三个轴的第三个力矩用作最后一个轴的行列式(?)。我们尝试了另一种方法,并成功使一半的表示形式匹配:

# -*- coding: utf-8 -*-
"""
Created on Fri Oct 16 11:40:30 2020

@author: Dima
"""


import numpy as np
from numpy.random import randn
from numpy import linalg as la
from scipy.spatial.transform import Rotation as R
np.random.seed(10)

rotate_mat = lambda theta: np.array([[np.cos(theta),-np.sin(theta),0.],[np.sin(theta),np.cos(theta),[0.,0.,1.]])

def pca(feat,x):
    # pca with attemt to create convention on sign changes
    
    x_c =x- np.mean(x,axis=0)
    x_f= feat*x
    x_f-= np.mean(x_f,axis=0)
    cov = np.cov(x_f.T)
    e_values,e_vectors = np.linalg.eig(cov)

    order = np.argsort(e_values)[::-1]
    #print(order)
    v = e_vectors[:,order]
    v= v/np.sign(v[0,:])
    if(la.det(v)<0):
        v= -v
    return x_c @ v

def standardize(x):
    # take vector with biggest norm,with smallest and thir cross product as basis
    x -= np.mean(x,axis=0)
    nrms= la.norm(x,axis=1)
    imin= argmin(nrms)
    imax= argmax(nrms)
    vec1= x[imin,:]
    vec2= x[imax,:]
    vec3= np.cross(vec1,vec2)
    Smat= np.stack([vec1,vec2,vec3],axis=0)
    if(la.det(Smat)<0):
        Smat= -Smat
    return(la.inv(Smat)@x.T)

    

angles = np.linspace(0.0,90.0,91)
voxel1 = np.random.normal(size=(3,3))    
res = []
for angle in angles:

    
    voxel2 = voxel1.copy()
    voxel1 = voxel1.reshape(27,1)
    voxel2 = voxel2.reshape(27,1)
    
    basis1 = np.array([[i+1,k+1] for k in range(3) for j in range(3) for i in range(3)]).astype(np.double)
    basis1 = basis1+1e-4*randn(27,3) # perturbation
    basis2 = basis1 @rotate_mat(np.deg2rad(angle))
    #voxel1 = voxel1*basis1
    #voxel2 = voxel2*basis2

    #print(angle,(np.abs(pca(voxel1) - pca(voxel2) )))
    #gg= np.abs(standardize(basis1) - standardize(basis2) )
    gg= np.abs(pca(voxel1,basis1) - pca(voxel1,basis2) )
    ss= np.sum(np.ravel(gg))
    bl= np.all(gg<1e-4) 
           
    print(angle,ss,bl)
    #res.append(np.all(np.abs(pca(voxel1) - pca(voxel2) < 1e-6)))
    del basis1,basis2

在高达58度的角度下,结果都很好(但是我们仍在尝试旋转x,y轴)。之后,我们有恒定的差异,这表明一些不可计数的符号反转。这比rotate_func的不一致结果要好:

0.0 0.0 True
1.0 1.1103280567106161e-13 True
2.0 5.150139890290964e-14 True
3.0 8.977126225544196e-14 True
4.0 5.57341699240722e-14 True
5.0 4.205149954378956e-14 True
6.0 3.7435437643664957e-14 True
7.0 1.2943967187158123e-13 True
8.0 5.400185371573149e-14 True
9.0 8.006410204958181e-14 True
10.0 7.777189536904011e-14 True
11.0 5.992073021576436e-14 True
12.0 6.3716122222085e-14 True
13.0 1.0120048110065158e-13 True
14.0 1.4193029076233626e-13 True
15.0 5.32774440341853e-14 True
16.0 4.056702432878251e-14 True
17.0 6.52062429116855e-14 True
18.0 1.3237663595853556e-13 True
19.0 8.950259695710006e-14 True
20.0 1.3795067925438317e-13 True
21.0 7.498727794307339e-14 True
22.0 8.570866862371226e-14 True
23.0 8.961510590826412e-14 True
24.0 1.1839169916779899e-13 True
25.0 1.422193407555868e-13 True
26.0 6.578778015788652e-14 True
27.0 1.0042963537887101e-13 True
28.0 8.438153062569065e-14 True
29.0 1.1299103064863272e-13 True
30.0 8.192453876745831e-14 True
31.0 1.2618492405483406e-13 True
32.0 4.9237819394886296e-14 True
33.0 1.0971028569666842e-13 True
34.0 1.332138304559801e-13 True
35.0 5.280024600049296e-14 True

从上面的代码中,您可以看到我们尝试使用另一个基础:范数最大的向量,范数最小的向量及其叉积。在这里,我们应该只有两个变体(叉积的方向),以后可以修复,但是我无法管理这种替代解决方案。

我希望有人能帮助我完成这项工作并获得体素的旋转不变表示。


编辑3.非常感谢meTchaikovsky,但情况仍不清楚。我的问题最初在于处理3d三维像素,它们是(3,3)numpy数组。我们得出的结论是,为了找到不变的表示形式,我们只需要固定3d体素作为用于计算cov矩阵的权重,然后对居中的“基础”(一些用于描述点云的向量)应用旋转。

因此,当我们实现“基本”旋转的不变性时,该问题应该已经解决:现在,当我们修复“基本”旋转并使用旋转的体素时,结果必须是不变的。令人惊讶的是,事实并非如此。在这里,我使用base2 = basis1(除了小扰动)检查了立方体的24次旋转:

import scipy.ndimage

def pca(feat,x):

    # pca with attemt to create convention on sign changes
    x_c = x - np.mean(x,axis=0)
    x_f = feat * x
    x_f -= np.mean(x_f,e_vectors = np.linalg.eig(cov)
    order = np.argsort(e_values)[::-1]
    v = e_vectors[:,order]

    # here is the solution,we switch the sign of the projections
    # so that the projection with the largest absolute value along a principal axis is positive
    proj = x_c @ v
    asign = np.sign(proj)
    max_ind = np.argmax(np.abs(proj),axis=0)[None,:]
    sign = np.take_along_axis(asign,max_ind,axis=0)
    proj = proj * sign

    return proj

def rotate_3d(image1,alpha,beta,gamma):
    # z
    # The rotation angle in degrees.
    image2 = scipy.ndimage.rotate(image1,mode='nearest',axes=(0,1),reshape=False)

    # rotate along y-axis
    image3 = scipy.ndimage.rotate(image2,2),reshape=False)

    # rotate along x-axis
    image4 = scipy.ndimage.rotate(image3,gamma,axes=(1,reshape=False)
    return image4



voxel10 = np.random.normal(size=(3,3))

angles = [[x,y,z] for x in [-90,90] for y in [-90,90] for z in [-90,90]]
res = []
for angle in angles:

    voxel2 = rotate_3d(voxel10,angle[0],angle[1],angle[2])
    voxel1 = voxel10.reshape(27,1)

    basis1 = np.array([[i+1,k+1] for k in range(3) for j in range(3) for i in range(3)]).astype(np.double)

    basis1 += 1e-4*np.random.normal(size=(27,1)) # perturbation
    basis2 = basis1
    original_diff = np.sum(np.abs(basis1-basis2))
    gg= np.abs(pca(voxel1,basis1) - pca(voxel2,basis2))
    ss= np.sum(np.ravel(gg))
    bl= np.all(gg<1e-4)
    print('difference before pca %.3f,' % original_diff,'difference after pca %.3f' % ss,bl)
    res.append(bl)

    del basis1,basis2

print('correct for %.1f percent of time' % (100*(np.sum(res) / len(res))))

difference before pca 0.000,difference after pca 45.738 False
difference before pca 0.000,difference after pca 12.157 False
difference before pca 0.000,difference after pca 26.257 False
difference before pca 0.000,difference after pca 37.128 False
difference before pca 0.000,difference after pca 52.131 False
difference before pca 0.000,difference after pca 45.436 False
difference before pca 0.000,difference after pca 42.226 False
difference before pca 0.000,difference after pca 18.959 False
difference before pca 0.000,difference after pca 38.888 False
difference before pca 0.000,difference after pca 50.613 False
difference before pca 0.000,difference after pca 52.132 False
difference before pca 0.000,difference after pca 0.000 True
difference before pca 0.000,difference after pca 52.299 False

这里base1 = basis2(因此pca = 0之前的基差),对于(0,0)旋转,您可以看到0。但是旋转的体素会产生不同的结果。万一scipy做错了事,我已经用numpy.rot90检查了方法,结果相同:

rot90 = np.rot90

def rotations24(polycube):
    # imagine shape is pointing in axis 0 (up)

    # 4 rotations about axis 0
    yield from rotations4(polycube,0)

    # rotate 180 about axis 1,now shape is pointing down in axis 0
    # 4 rotations about axis 0
    yield from rotations4(rot90(polycube,2,axis=1),0)

    # rotate 90 or 270 about axis 1,now shape is pointing in axis 2
    # 8 rotations about axis 2
    yield from rotations4(rot90(polycube,2)
    yield from rotations4(rot90(polycube,-1,2)

    # rotate about axis 2,now shape is pointing in axis 1
    # 8 rotations about axis 1
    yield from rotations4(rot90(polycube,axis=2),1)
    yield from rotations4(rot90(polycube,1)

def rotations4(polycube,axis):
    """List the four rotations of the given cube about the given axis."""
    for i in range(4):
        yield rot90(polycube,i,axis)



def rot90(m,k=1,axis=2):
    """Rotate an array k*90 degrees in the counter-clockwise direction around the given axis"""
    m = np.swapaxes(m,axis)
    m = np.rot90(m,k)
    m = np.swapaxes(m,axis)
    return m


voxel10 = np.random.normal(size=(3,3))

gen = rotations24(voxel10)

res = []
for voxel2 in gen:

    #voxel2 = rotate_3d(voxel10,1)) # perturbation
    basis2 = basis1

    original_diff = np.sum(np.abs(basis1-basis2))
    gg= np.abs(pca(voxel1,basis2

print('correct for %.1f percent of time' % (100*(np.sum(res) / len(res))))

我试图调查此案,而唯一可能无关的发现是:

voxel1 = np.ones((3,3))
voxel1[0,0] = 0 # if I change 0 to 0.5 it stops working at all

# mirrored around diagonal
voxel2 = np.ones((3,3))
voxel2[2,2] = 0

for angle in range(1):

    voxel1 = voxel1.reshape(27,1) 

    basis1 = np.array([[i+1,k+1] for k in range(3) for j in range(3) for i in range(3)]).astype(np.double)

    basis1 = basis1 + 1e-4 * randn(27,3) # perturbation
    basis2 = basis1

# If perturbation is used we have 

# difference before pca 0.000,difference after pca 0.000 True
# correct for 100.0 percent of time

# eigenvalues for both voxels
# [1.03417495 0.69231107 0.69235402]
# [0.99995368 0.69231107 0.69235402]


# If no perturbation applied for basis,difference is present

# difference before pca 0.000,difference after pca 55.218 False
# correct for 0.0 percent of time

# eignevalues for both voxels (always have 1.):
# [0.69230769 1.03418803 0.69230769]
# [1.         0.69230769 0.69230769]



当前不知道如何从那里继续。


EDIT4:

我目前正在考虑将体素旋转通过voxel.reshape()转换为基本系数存在一些问题

创建索引数组的简单实验

indices = np.arange(27)
indices3d = indices.reshape((3,3))
voxel10 = np.random.normal(size=(3,3))
assert voxel10[0,1,2] == voxel10.ravel()[indices3d[0,2]]

然后将其用于旋转

gen = rotations24(indices3d)

res = []
for ind2 in gen:

    basis1 = np.array([[i+1,k+1] for k in range(3) for j in range(3) for i in range(3)]).astype(np.double)
    voxel1 = voxel10.copy().reshape(27,1) #np.array([voxel10[i,j,k] for k in range(3) for j in range(3) for i in range(3)])[...,np.newaxis]

    voxel2 = voxel1[ind2.reshape(27,)]

    basis1 += 1e-4*np.random.normal(size=(27,1)) # perturbation
    basis2 = basis1[ind2.reshape(27,)]

    original_diff = np.sum(np.abs(basis1-basis2))
    gg= np.abs(pca(voxel1,basis2

print('correct for %.1f percent of time' % (100*(np.sum(res) / len(res))))

表明这些旋转不正确,因为我认为旋转的体素和基础应匹配:

difference before pca 0.000,difference after pca 0.000 True
difference before pca 48.006,difference after pca 87.459 False
difference before pca 72.004,difference after pca 70.644 False
difference before pca 48.003,difference after pca 71.930 False
difference before pca 72.004,difference after pca 79.409 False
difference before pca 84.005,difference after pca 36.177 False

编辑5:好的,所以在这里我们至少旋转24圈。最初,我们在pca函数中隐藏了一些逻辑上的变化。在这里,我们将x_c(基本)居中,然后将其忘却,进一步将x_f(功能*基本)居中,并用pca对其进行转换。这可能行不通,因为我们的基础不集中,特征相乘进一步增加了偏差。如果我们首先将x_c居中并乘以特征,一切都会好的。另外,以前我们从proj = x_c @ v计算出的vx_f,在这种情况下是完全错误的,因为x_fx_c围绕不同的中心。

def pca(feat,x):
    
    # pca with attemt to create convention on sign changes
    x_c = x - np.mean(x,order]
    
    # here is the solution,we switch the sign of the projections
    # so that the projection with the largest absolute value along a principal axis is positive
    proj = x_f @ v
    
    
    return proj

第二,正如我们已经发现的,我们需要对通过pca获得的向量进行排序,例如通过第一列:

    basis2 = basis1

    original_diff = np.sum(np.abs(basis1-basis2))

    a = pca(voxel1,basis1)
    t1 = a[a[:,0].argsort()]

    a = pca(voxel2,basis2)
    t2 = a[a[:,0].argsort()]

    gg= np.abs(t1-t2)

我们还发现的最后一件事是,简单的reshape对于体素是错误的,它必须对应于旋转:

voxel2 = voxel1[ind2.reshape(27,)] #np.take(voxel10,ind2).reshape(27,1)

还有一条重要的注释可以理解解决方案。当我们在分配了权重(类似于刚体的惯性)的3d向量(由我们的基准定义的点云)上执行PCA时,权重对点的实际分配是外部信息,该信息对于算法来说很难定义。当我们通过应用旋转矩阵旋转基数时,我们没有更改数组中矢量的顺序,因此质量分配的顺序也没有更改。当我们开始旋转体素时,我们改变了质量的顺序,因此在一般情况下,如果不对基础应用相同的变换,则PCA算法将无法工作。因此,仅当我们具有一些3d向量数组,并通过一些旋转进行变换并且相应地重新排列了质量列表时,我们才可以使用PCA检测刚体的旋转。否则,如果我们将质量与点分开,那通常就是另一个物体。

那么它对我们如何工作?之所以起作用,是因为定心后我们的点围绕中心完全对称。在这种情况下,群众的重新分配不会改变“身体”,因为矢量规范是相同的。在这种情况下,我们可以使用相同的(数字方式)basis2=basis1来测试24个旋转和旋转的voxel2(旋转的点云立方体匹配,只是质量会迁移)。这对应于具有围绕立方体中心的质量点的点云的旋转。然后,PCA将根据人体的“惯性”以相同的方式以相同的方式变换具有相同长度和不同质量的向量(在我们达成有关分量符号的约定之后)。剩下的唯一事情就是最后对pca变换的向量进行排序,因为它们在数组中的位置不同(因为我们的身体旋转了,质点改变了它们的位置)。这使我们丢失了一些与向量的顺序有关的信息,但这看起来是不可避免的。

这里是代码,检查溶液是否旋转24圈。如果从理论上讲也应该在一般情况下起作用,则为在较大体素内旋转的更复杂对象提供一些更接近的值:

import numpy as np
from numpy.random import randn

#np.random.seed(20)

def pca(feat,x):
    # pca with attemt to create convention on sign changes
    x_c = x - np.mean(x,axis=0)
    x_f = feat * x_c
    cov = np.cov(x_f.T)
    e_values,order]
    # here is the solution,we switch the sign of the projections
    # so that the projection with the largest absolute value along a principal axis is positive
    proj = x_f @ v
    asign = np.sign(proj)
    max_ind = np.argmax(np.abs(proj),axis=0)
    proj = proj * sign
    return proj


# must be correct https://stackoverflow.com/questions/15230179/how-to-get-the-linear-index-for-a-numpy-array-sub2ind
indices = np.arange(27)
indices3d = indices.reshape((3,2]]

rot90 = np.rot90

def rotations24(polycube):
    # imagine shape is pointing in axis 0 (up)

    # 4 rotations about axis 0
    yield from rotations4(polycube,axis)
    return m


gen = rotations24(indices3d)

res = []

for ind2 in gen:

    basis1 = np.array([[i+1,1)

    voxel2 = voxel1[ind2.reshape(27,1)

    basis1 += 1e-6*np.random.normal(size=(27,1)) # perturbation
    basis2 = basis1

    original_diff = np.sum(np.abs(basis1-basis2))
    a = pca(voxel1,0].argsort()]
    a = pca(voxel2,0].argsort()]
    gg= np.abs(t1-t2)
    ss= np.sum(np.ravel(gg))
    bl= np.all(gg<1e-4)
    print('difference before pca %.3f,basis2

print('correct for %.1f percent of time' % (100*(np.sum(res) / len(res))))
difference before pca 0.000,difference after pca 0.000 True

PS。我想提出一个更好的排序主题,以考虑体素中的零值,当PCA向量的整个第一列为零时,这可能会混淆先前的方法,依此类推。我提议按向量范数排序,再乘以元素总和的符号。这是tensorflow 2代码:


def infer_shape(x):
    x = tf.convert_to_tensor(x)

    # If unknown rank,return dynamic shape
    if x.shape.dims is None:
        return tf.shape(x)

    static_shape = x.shape.as_list()
    dynamic_shape = tf.shape(x)

    ret = []
    for i in range(len(static_shape)):
        dim = static_shape[i]
        if dim is None:
            dim = dynamic_shape[i]
        ret.append(dim)

    return ret

def merge_last_two_dims(tensor):
    shape = infer_shape(tensor)
    shape[-2] *= shape[-1]
    #shape.pop(1)
    shape = shape[:-1]
    return tf.reshape(tensor,shape)


def pca(inpt_voxel):
        patches = tf.extract_volume_patches(inpt_voxel,ksizes=[1,1],strides=[1,padding="VALID")
        features0 = patches[...,tf.newaxis]*basis
        # centered basises
        basis1_ = tf.ones(shape=tf.shape(patches[...,tf.newaxis]),dtype=tf.float32)*basis
        basis1 = basis1_ - tf.math.divide_no_nan(tf.reduce_sum(features0,axis=-2),tf.reduce_sum(patches,axis=-1)[...,None])[:,:,None,:]
        features = patches[...,tf.newaxis]*basis1
        features_centered_basis = features - tf.reduce_mean(features,axis=-2)[:,:]
        x = features_centered_basis
        m = tf.cast(x.get_shape()[-2],tf.float32)
        cov = tf.matmul(x,x,transpose_a=True)/(m - 1)
        e,v = tf.linalg.eigh(cov,name="eigh")
        proj = tf.matmul(x,v,transpose_b=False)
        asign = tf.sign(proj)
        max_ind = tf.argmax(tf.abs(proj),:]
        sign = tf.gather(asign,indices=max_ind,batch_dims=4,axis=-2)
        sign = tf.linalg.diag_part(sign)
        proj = proj * sign
        # But we can have 1st coordinate zero. In this case,# other coordinates become ambiguous
        #s = tf.argsort(proj[...,0],axis=-1)
        # sort by l2 vector norms,multiplied by signs of sums
        sum_signs = tf.sign(tf.reduce_sum(proj,axis=-1))
        norms = tf.norm(proj,axis=-1)
        s = tf.argsort(sum_signs*norms,axis=-1)
        proj = tf.gather(proj,s,axis=-2)
        return merge_last_two_dims(proj)

解决方法

首先,您的pca函数不正确,应该正确

def pca(x):
    
    x -= np.mean(x,axis=0)
    cov = np.cov(x.T)
    e_values,e_vectors = np.linalg.eig(cov)

    order = np.argsort(e_values)[::-1]
    v = e_vectors[:,order]
    
    return x @ v

您不应该转置e_vectors[:,order],因为我们希望v数组的每一列都是一个特征向量,因此,x @ v将是x在这些特征向量上的投影

第二,我认为您误解了轮换的含义。不应旋转voxel1,而应该旋转basis1。如果旋转(通过转置)voxel1,则真正要做的是重新排列网格点的索引,而点basis1的坐标不变。

要旋转点(例如绕z轴),您可以首先定义一个函数以给定角度计算旋转矩阵

rotate_mat = lambda theta: np.array([[np.cos(theta),-np.sin(theta),0.],[np.sin(theta),np.cos(theta),[0.,0.,1.]])

使用此函数生成的旋转矩阵,您可以旋转数组basis1以创建另一个数组basis2

basis2 = basis1 @ rotate_mat(np.deg2rad(angle))

现在,您的问题的标题为“为什么我的PCA对旋转和轴交换不是不变的?”,从this post起,PCA结果不是唯一的,您实际上可以运行测试以查看此情况

import numpy as np

np.random.seed(10)

rotate_mat = lambda theta: np.array([[np.cos(theta),1.]])

def pca(x):
    
    x -= np.mean(x,order]
    return x @ v


angles = np.linspace(0,90,91)
    
res = []
for angle in angles:

    voxel1 = np.random.normal(size=(3,3,3))
    voxel2 = voxel1.copy()
    voxel1 = voxel1.reshape(27,1)
    voxel2 = voxel2.reshape(27,1)
    
    basis1 = np.array([[i+1,j+1,k+1] for k in range(3) for j in range(3) for i in range(3)])
    # basis2 = np.hstack((-basis1[:,1][:,None],basis1[:,0][:,-basis1[:,2][:,None]))
    basis2 = basis1 @ rotate_mat(np.deg2rad(angle))
    voxel1 = voxel1*basis1
    voxel2 = voxel2*basis2

    print(angle,np.all(np.abs(pca(voxel1) - pca(voxel2) < 1e-6)))
    res.append(np.all(np.abs(pca(voxel1) - pca(voxel2) < 1e-6)))
    
print()
print(np.sum(res) / len(angles))

运行此脚本后,您将看到两个PCA结果相同的可能性只有21%。


更新

我认为,您可以将注意力集中在投影上,而不是专注于主成分的特征向量。对于两点云,即使它们本质上相同,特征向量也可能截然不同。因此,以某种方式使两组特征向量相同的硬编码是一项非常困难的任务。

但是,基于this post,对于相同的点云,两组特征向量仅在负号之前可以不同。因此,两组特征向量的投影也仅在负号之前不同。实际上,这为我们提供了一种优雅的解决方案,对于沿特征向量(主轴)的投影,我们要做的就是切换投影的符号,以使沿该主轴的绝对值最大的投影为正。 >

import numpy as np
from numpy.random import randn

#np.random.seed(20)

rotmat_z = lambda theta: np.array([[np.cos(theta),1.]])
rotmat_y = lambda theta: np.array([[np.cos(theta),np.sin(theta)],1.,[-np.sin(theta),np.cos(theta)]])
rotmat_x = lambda theta: np.array([[1.,-np.sin(theta)],np.sin(theta),np.cos(theta)]])
# based on https://en.wikipedia.org/wiki/Rotation_matrix
rot_mat = lambda alpha,beta,gamma: rotmat_z(alpha) @ rotmat_y(beta) @ rotmat_x(gamma)

deg2rad = lambda alpha,gamma: [np.deg2rad(alpha),np.deg2rad(beta),np.deg2rad(gamma)]

def pca(feat,x):
    
    # pca with attemt to create convention on sign changes
    x_c = x - np.mean(x,axis=0)
    x_f = feat * x
    x_f -= np.mean(x_f,axis=0)
    cov = np.cov(x_f.T)
    e_values,e_vectors = np.linalg.eig(cov)
    order = np.argsort(e_values)[::-1]
    v = e_vectors[:,order]
    
    # here is the solution,we switch the sign of the projections
    # so that the projection with the largest absolute value along a principal axis is positive
    proj = x_f @ v
    asign = np.sign(proj)
    max_ind = np.argmax(np.abs(proj),axis=0)[None,:]
    sign = np.take_along_axis(asign,max_ind,axis=0)
    proj = proj * sign
    
    return proj

ref_angles = np.linspace(0.0,90.0,10)
angles = [[alpha,gamma] for alpha in ref_angles for beta in ref_angles for gamma in ref_angles]


voxel1 = np.random.normal(size=(3,3))
res = []
for angle in angles:

    voxel2 = voxel1.copy()
    voxel1 = voxel1.reshape(27,1)

    basis1 = np.array([[i+1,k+1] for k in range(3) for j in range(3) for i in range(3)]).astype(np.double)
    basis1 = basis1 + 1e-4 * randn(27,3) # perturbation
    basis2 = basis1 @ rot_mat(*deg2rad(*angle))
   
    original_diff = np.sum(np.abs(basis1-basis2))
    gg= np.abs(pca(voxel1,basis1) - pca(voxel1,basis2))
    ss= np.sum(np.ravel(gg))
    bl= np.all(gg<1e-4)
    print('difference before pca %.3f,' % original_diff,'difference after pca %.3f' % ss,bl)
    res.append(bl)

    del basis1,basis2

print('correct for %.1f percent of time' % (100*(np.sum(res) / len(res))))

通过运行该脚本可以看到,主轴上的投影是相同的,这意味着我们已经解决了PCA结果不唯一的问题。


回复EDIT 3

关于您提出的新问题,我认为您错过了一个重要的观点,那就是不变的,而不是其他任何东西上,点云在主轴上的投影。因此,如果旋转voxel1并获得voxel2,则它们在点云的主轴上各自的投影相同的意义上是相同的,实际上并不会做太多比较pca(voxel1,basis1)pca(voxel2,basis1)的意义。

此外,rotate中的方法scipy.ndimage实际上会更改信息,如运行该脚本所见,

image1 = np.linspace(1,100,100).reshape(10,10)
image2 = scipy.ndimage.rotate(image1,45,mode='nearest',axes=(0,1),reshape=False)
image3 = scipy.ndimage.rotate(image2,-45,reshape=False)

fig,ax = plt.subplots(nrows=1,ncols=3,figsize=(12,4))
ax[0].imshow(image1)
ax[1].imshow(image2)
ax[2].imshow(image3)

输出图像是

如您所见,旋转后的矩阵与原始矩阵不同,因此原始矩阵的某些信息已更改。

output


回复EDIT 4

实际上,我们差不多到了,两个pca结果是不同的,因为我们正在比较不同点的pca成分。

indices = np.arange(27)
indices3d = indices.reshape((3,3))
# apply rotations to the indices,it is not weights yet
gen = rotations24(indices3d)

# construct the weights
voxel10 = np.random.normal(size=(3,3))

res = []
count = 0
for ind2 in gen:
    count += 1
    # ind2 is the array of indices after rotation
    # reindex the weights with the indices after rotation 
    voxel1 = voxel10.copy().reshape(27,1) 
    voxel2 = voxel1[ind2.reshape(27,)]

    # basis1 is the array of coordinates where the points are
    basis1 = np.array([[i+1,k+1] for k in range(3) for j in range(3) for i in range(3)]).astype(np.double)
    basis1 += 1e-4*np.random.normal(size=(27,1))
    # reindex the coordinates with the indices after rotation
    basis2 = basis1[ind2.reshape(27,)]

    # add a slight modification to pca,return the axes also 
    pca1,v1 = pca(voxel1,basis1)
    pca2,v2 = pca(voxel2,basis2)
    # sort the principal components before comparing them 
    pca1 = np.sort(pca1,axis=0)
    pca2 = np.sort(pca2,axis=0)
    
    gg= np.abs(pca1 - pca2)
    ss= np.sum(np.ravel(gg))
    bl= np.all(gg<1e-4)
    print('difference after pca %.3f' % ss,bl)
    res.append(bl)
    
    del basis1,basis2

print('correct for %.1f percent of time' % (100*(np.sum(res) / len(res))))

运行此脚本,您将发现,每旋转一次,两组主轴仅在负号之间不同。两组pca结果不同,因为旋转前后的点云索引不同(因为您将旋转应用于索引)。如果在比较前对pca结果进行排序,则会发现两个pca结果完全相同。


摘要

这个问题的答案可以分为两个部分。在第一部分中,将旋转应用于基础(点的坐标),而索引和相应的权重不变。在第二部分中,将旋转应用于索引,然后使用新索引重新排列权重和基础。对于这两部分,解决方案pca的功能都是相同的

def pca(feat,x):

    # pca with attemt to create convention on sign changes
    x_c = x - np.mean(x,order]

    # here is the solution,axis=0)
    proj = proj * sign

    return proj

此功能的想法是,我们不必匹配主轴,而是可以匹配主分量,因为毕竟它们是旋转不变的主分量。

基于此功能pca,此答案的第一部分很容易理解,因为点的索引不变,而我们仅旋转基础。为了理解此答案的第二部分(答复EDIT 5 ),我们必须首先了解函数rotations24。此功能旋转索引而不是点的坐标,因此,如果我们在观察点的位置相同,则会感觉到点的位置已更改。

plot

考虑到这一点,不难理解 回复EDIT 5

实际上,此答案中的函数pca可以用于更一般的情况,例如(我们旋转索引)

num_of_points_per_dim = 10
num_of_points = num_of_points_per_dim ** 3

indices = np.arange(num_of_points)
indices3d = indices.reshape((num_of_points_per_dim,num_of_points_per_dim,num_of_points_per_dim))
voxel10 = 100*np.random.normal(size=(num_of_points_per_dim,num_of_points_per_dim))

gen = rotations24(indices3d)

res = []
for ind2 in gen:

    voxel1 = voxel10.copy().reshape(num_of_points,1)
    voxel2 = voxel1[ind2.reshape(num_of_points,)]

    basis1 = 100*np.random.rand(num_of_points,3)
    basis2 = basis1[ind2.reshape(num_of_points,)]

    pc1 = np.sort(pca(voxel1,basis1),axis=0)
    pc2 = np.sort(pca(voxel2,basis2),axis=0)
    
    gg= np.abs(pc1-pc2)
    ss= np.sum(np.ravel(gg))
    bl= np.all(gg<1e-4)
    print('difference after pca %.3f' % ss,basis2

print('correct for %.1f percent of time' % (100*(np.sum(res) / len(res))))

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-
参考1 参考2 解决方案 # 点击安装源 协议选择 http:// 路径填写 mirrors.aliyun.com/centos/8.3.2011/BaseOS/x86_64/os URL类型 软件库URL 其他路径 # 版本 7 mirrors.aliyun.com/centos/7/os/x86
报错1 [root@slave1 data_mocker]# kafka-console-consumer.sh --bootstrap-server slave1:9092 --topic topic_db [2023-12-19 18:31:12,770] WARN [Consumer clie
错误1 # 重写数据 hive (edu)&gt; insert overwrite table dwd_trade_cart_add_inc &gt; select data.id, &gt; data.user_id, &gt; data.course_id, &gt; date_format(
错误1 hive (edu)&gt; insert into huanhuan values(1,&#39;haoge&#39;); Query ID = root_20240110071417_fe1517ad-3607-41f4-bdcf-d00b98ac443e Total jobs = 1
报错1:执行到如下就不执行了,没有显示Successfully registered new MBean. [root@slave1 bin]# /usr/local/software/flume-1.9.0/bin/flume-ng agent -n a1 -c /usr/local/softwa
虚拟及没有启动任何服务器查看jps会显示jps,如果没有显示任何东西 [root@slave2 ~]# jps 9647 Jps 解决方案 # 进入/tmp查看 [root@slave1 dfs]# cd /tmp [root@slave1 tmp]# ll 总用量 48 drwxr-xr-x. 2
报错1 hive&gt; show databases; OK Failed with exception java.io.IOException:java.lang.RuntimeException: Error in configuring object Time taken: 0.474 se
报错1 [root@localhost ~]# vim -bash: vim: 未找到命令 安装vim yum -y install vim* # 查看是否安装成功 [root@hadoop01 hadoop]# rpm -qa |grep vim vim-X11-7.4.629-8.el7_9.x
修改hadoop配置 vi /usr/local/software/hadoop-2.9.2/etc/hadoop/yarn-site.xml # 添加如下 &lt;configuration&gt; &lt;property&gt; &lt;name&gt;yarn.nodemanager.res