1

我有一个支持向量机,它使用决策超平面将我的数据分成两部分(出于可视化目的,这是一个具有三个维度的示例数据集),如下所示: 支持向量机超平面 现在我想执行基础更改,以便超平面平放在x/y 平面,这样从每个样本点到决策超平面的距离就是它们的 z 坐标。

为此,我知道我需要更改基础。SVM 的超平面由它们的系数(3d 向量)和截距(标量)给出,使用(据我所知)数学平面的一般形式:ax+by+cz=d,a,b,c 是系数的坐标d 是截距。当绘制为 3d 向量时,系数是与平面正交的向量(在图像中它是青色线)。

现在改变基:如果没有截距,我可以假设作为系数的向量是我的新基的一个向量,另一个可以是平面上的随机向量,第三个是简单的交叉两者的乘积,产生三个正交向量,它们可以是变换矩阵的列向量。下面代码中使用的 z 函数来自平面的一般形式的简单术语重新排列ax+by+cz=d <=> z=(d-ax-by)/c

z_func = lambda interc, coef, x, y: (interc-coef[0]*x -coef[1]*y) / coef[2]
def generate_trafo_matrices(coefficient, z_func):
    normalize = lambda vec: vec/np.linalg.norm(vec)
    uvec2 = normalize(np.array([1, 0, z_func(1, 0)]))
    uvec3 = normalize(np.cross(uvec1, uvec2))
    back_trafo_matrix = np.array([uvec2, uvec3, coefficient]).T 
    #in other order such that its on the xy-plane instead of the yz-plane
    trafo_matrix = np.linalg.inv(back_trafo_matrix)
    return trafo_matrix, back_trafo_matrix

然后将这个变换矩阵应用于所有点,如下所示:

def _transform(self, points, inverse=False):
    trafo_mat = self.inverse_trafo_mat if inverse else self.trafo_mat              
    points = np.array([trafo_mat.dot(point) for point in points])        
    return points

现在,如果截距为零,那将完美运行,并且平面将在 xy 轴上平坦。但是,一旦我有一个截距!= 零,飞机就不再平坦了:

plane_not_flat

我理解是这种情况,因为这不是简单的基础更改,因为我的另一个基础的坐标原点不在 (0,0,0) 而是在不同的位置(超平面可能穿过系数向量在任何时候),但是我尝试将截距添加到转换中都没有导致正确的结果:

def _transform(self, points, inverse=False):
    trafo_mat = self.inverse_trafo_mat if inverse else self.trafo_mat      
    intercept = self.intercept if inverse else -self.intercept
    ursprung_translate = trafo_mat.dot(np.array([0,0,0])+trafo_matrix[:,0]*intercept)
    points = np.array([point+trafo_matrix[:,0]*intercept for point in points])
    points = np.array([trafo_mat.dot(point) for point in points])        
    points = np.array([point-ursprung_translate for point in points])
    return points

例如是错误的。我在 StackOverflow 上而不是在数学 StackExchange 上问这个问题,因为我认为我无法将相应的数学转换为代码,我很高兴我能走到这一步。

我创建了一个 github gist,其中包含进行转换的代码并在https://gist.github.com/cstenkamp/0fce4d662beb9e07f0878744c7214995上创建图,可以在链接https://mybinder.org/v2下使用 Binder 启动/gist/jtpio/0fce4d662beb9e07f0878744c7214995/master?urlpath=lab%2Ftree%2Fchange_of_basis_with_translate.ipynb如果有人想玩代码本身。

任何帮助表示赞赏!

4

1 回答 1

1

这里的问题是你的平面是仿射空间,而不是向量空间,所以你不能使用通常的变换矩阵公式。

仿射空间中的坐标系由原点和基础给出(放在一起,它们被称为仿射框架)。例如,如果您的原点称为 O,则仿射框架中点 M 的坐标将是仿射框架基础中的 OM 向量的坐标。

如您所见,“正常”R^3 空间是仿射空间的一种特殊情况,其原点为 (0,0,0)。

一旦我们确定了这些,我们就可以在仿射空间中使用框架变化公式:如果我们有两个仿射框架R = (O, b)R' = (O', b'),点 M 的基本变化公式是:(M(R') = base_change_matrix_from_b'_to_b * (M(R) - O'(R))O'(R)由 R 定义的坐标系中 O' 的坐标)。

在我们的例子中,我们试图从原点在 (0,0,0) 和规范基础的框架转到原点是 (0,0,0) 在平面上的正交投影的框架例如,基础是您最初的帖子中描述的基础。

让我们实现这些步骤:

首先,我们将定义一个Plane类以使我们的生活更轻松:

from dataclasses import dataclass
import numpy as np

@dataclass
class Plane:
    a: float
    b: float
    c: float
    d: float
    
    @property
    def normal(self):
        return np.array([self.a, self.b, self.c])
    
    def __contains__(self, point:np.array):
        return np.isclose(self.a*point[0] + self.b*point[1] + self.c*point[2] + self.d, 0)
    
    def project(self, point):
        x,y,z = point
        k = (self.a*x + self.b*y + self.c*z + self.d)/(self.a**2 + self.b**2 + self.c**2)
        return np.array([x - k*self.a, y-k*self.b, z-k*self.c])
   
    
    def z(self, x, y):
        return (- self.d - self.b*y - self.a*x)/self.c

然后我们可以实现make_base_changer,它以 aPlane作为输入,并返回 2 个执行正向和逆变换(取并返回一个点)的 lambda 函数。你应该能够测试

def normalize(vec):
    return vec/np.linalg.norm(vec)
def make_base_changer(plane):
    uvec1 = plane.normal
    uvec2 = [0, -plane.d/plane.b, plane.d/plane.c]
    uvec3 = np.cross(uvec1, uvec2)
    transition_matrix = np.linalg.inv(np.array([uvec1, uvec2, uvec3]).T)
    
    origin = np.array([0,0,0])
    new_origin = plane.project(origin)
    forward  = lambda point: transition_matrix.dot(point - new_origin)
    backward = lambda point: np.linalg.inv(transition_matrix).dot(point) + new_origin
    return forward, backward

结果

于 2021-10-01T14:53:57.277 回答