自定义核函数

546 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情

SVM、SVC 的核函数方法签名

SVM、SVM 允许自行定义核函数,对输入输出的参数说明如下

def my_kernel(X: np.ndarry, Y: np.ndarry)->np.ndarry:

补充:

  • X 的形状为 (N, K)
  • Y 的形状为** (M,K)**
  • 返回的形状要求为** (N, M)**

## 一个简单的示例 ```python def naive_kernel(X: np.ndarry, Y: np.ndarry)->np.ndarry: """ (2 0) k(X, Y) = X ( ) Y.T (0 1) """ M = np.array([[2, 0], [0, 1.0]]) return np.dot(np.dot(X, M), Y.T) ``` # 已有核函数 如果是为了将核函数组合使用的话,那么更多时候,我们要用已有的函数。 已有的核函数位于`sklearn.metrics.pairwise` 下。

比如说:

  • 高斯核 rbf_kernel
  • 多项式核 polynomial_kernel
  • 线性核 linear_kernel
  • 拉普拉斯核 laplacian_kernel

等等

核的组合使用

因为核函数可以通过相互之间的组合使用,因此假设我们使用加法组合多个核函数。代码如下:

# 1
func = namedtuple('func', ['func', 'param', 'weight'])# 2

# 3
funcs = [
    func(rbf_kernel, [0.5], 1),
    func(polynomial_kernel, [3, 0.5, 1], 1),
    func(linear_kernel, [], 1),
    func(laplacian_kernel, [0.5], 1),
]

# 4
def kernel(X: np.ndarray, Y: np.ndarray) -> np.ndarray:
    return functools.reduce(
        operator.add, 
        (f.func(X, Y, *f.param) * f.weight for f in funcs)
    )
  1. 用于组合的函数

  2. 函数对象,参数列表,所占权重

  3. 使用的核函数,参数,权重

  4. 自定义的组合核函数 以经典的 IRIS 数据集分类为例,运用刚才自定义的组合核函数进行分类操作。一个经典的三分类问题

# 加载数据
iris = datasets.load_iris()
X, Y = iris.data[:, :2], iris.target
# 定义使用自定义核函数的分类器并进行训练
clf = svm.SVC(kernel=kernel_function)
clf.fit(X, Y)
print(clf.score())

由于我们先前写的函数特地实现成了一个函数列表的形式,我们简单的在函数列表中添加或者删除元素即可实现更多的组合形式。也可以观察更多的的分类表现。

自定义核函数

有时候可能需要自行定义核函数的实现。不过由于核函数对于形状的要求,最终运算操作会比较特殊。 一般来说都是定义的运算都类似于矩阵乘法的运算。

A: np.ndarray  # (n x k)
B: np.ndarray  # (m x k)
result: np.ndarray  # (n x m)
for i in range(A.shape[0]):
    for j in range(B.shape[0]):
        result[i, j] = np.dot(A[i], B[j])
# 对于矩阵乘法,可以优化成 A @ B.T

类似地,RBF 是在循环内部求向量差的范数(欧几里得距离)再做指数运算

但是这样就慢了!

这里有时候可以利用 Numpy 等矩阵运算的广播机制,向量化整个操作。 例如矩阵乘法这种对应位相乘再相加可以通过如下方式实现

(A[:,None] * B).sum(axis=2)

通过这种向量化的优化,可以带来相当大的性能提升。

实现RBF函数

使用 for 循环实现,

# 不那么高效的方法 rbf 实现
    def kernel_function(X: np.ndarray, Y: np.ndarray):
        def naive_rbf_compute(va, vb):
            return np.exp(coef * np.square(va - vb).sum())

        dot = np.zeros((X.shape[0], Y.shape[0]))
        for i in range(X.shape[0]):
            for j in range(Y.shape[0]):
                dot[i, j] = naive_rbf_compute(X[i], Y[j])
        return dot

向量化实现,和矩阵乘法一样,在中间插入一个维度。这实际上是一个比较通用的加速手段,后面我们在别的地方也会使用到。

# 高斯
def kernel_function(X: np.ndarray, Y: np.ndarray):
    return np.exp(coef * np.square(X[:, None] - Y).sum(axis=2))

性能测试,使用python的timeit库,注意这里使用了自定义的一些函数,因此我们需要用一个技巧,从__main__中导入这些用于测试的函数

    from timeit import timeit
    X = np.random.randn(80, 4)
    naive = naive_rbf(1)
    vec = rbf(1)
    setup = 'from __main__ import X, vec, naive;import numpy as np'
    num = 1000
    t1 = timeit('naive(X, X)', setup=setup, number=num)
    t2 = timeit('vec(X, X)', setup=setup, number=num)
    print('Speed difference: {:0.3f}x'.format(t1 / t2))

以R7 4800U 8核16线程机器测试,结果为

Speed difference: 167.814x

总结

  • 可以使用Python函数作为核函数
  • 对于自定义的操作可以有向量化的实现

参考

  1. Using Python functions as kernels
  2. kernel function
  3. numpy 广播机制