核密度估计(Kernel Density Estimation, KDE)算法通过样本估计这些样本所属的概率密度函数,是non-parametric方法,也就是在进行估计时无需假设分布的具体形式。本文只讨论单变量(univariate)。

数学表达

给定\(n\)个样本\(x_i \in \mathcal{R}\),通过KDE算法估计\(x\)处的概率密度为

\[f_n(x) = \frac{1}{nh}\sum_{i=1}^{n}K(\frac{x-x_i}{h}),\tag{1}
\]

其中,核函数\(K\)和带宽(bandwidth)\(h\)正是KDE算法的两个核心参数。

通过公式(1)易得,如果有\(m\)个\(x\)值要估计的概率密度,那么计算时间复杂度是\(O(m\times n)\)。

Binning

Binning技术是通过将相邻样本分箱到一个box中。分箱之前,我们需要对所有的\(n\)个样本计算核密度函数值;分箱之后,我们只需要对每个box计算一次即可,然后用它来代表所有被分到这个box内的样本。

根据[1],通过binning计算可分为以下三步:

  1. 通过将原始数据分配给相邻网格点来对数据进行分箱以获得网格计数。网格计数可以被认为表示其对应网格点附近的数据量。

    Bin the data by assigning the raw data to neighboring grid points to obtain grid counts. A grid count can be thought of as representing the amount of data in the neighborhood of its corresponding grid point.

  2. 计算所需的内核权重。网格点等距意味着不同核权重的数量相对较少。

    Compute the required kernel weights. The fact that the grid points are equally spaced means that the number of distinct kernel weights is comparatively small.

  3. 结合网格计数和核权重以获得核估计的近似值。这本质上涉及一系列离散卷积。

    Combine the grid counts and the kernel weights to obtain the approximation to the kernel estimate. This essentially involves a series of discrete convolutions.

步骤1涉及划分多少个box以及如何将样本对应到box中,对计算速度和精度都非常关键。两种常见的划分方式是

  1. Simple Binning: 将样本对应的权重划分到距离到离其最近的grid中。

    If a data point at y has surrounding grid points at x and z, then simple binning involves assigning a unit mass to the grid point closest to y.

  2. Linear Binning: 将样本对应的权重按比例划分到其左右相邻的两个grid中。注意给左端点x和右端点z的权重是和它到两个端点的距离“反过来”的,也就是给x的权重是\((z-y)/(z-x)\),而给右端点z的权重是\((y-x)/(z-x)\)。

    Linear binning assigns a mass of (z - y)/(z - x) to the grid point at x, and (y - x)/(z - x) to the grid point at z.

这里所说的样本权重就是每个样本是否重要性相同,可以联系python代码中sklearn.neighbors.KernelDensity.fit(sample_weight)参数,默认情况下,每个样本地位相同,其权重都为1(或者说都是1/n)。

根据[2],我们可以给Simple Binning一种形式化定义:定义box宽度为\(\delta\)的等距网格(其实也就是对应要划分多少个box),令第\(j\)个bin的中点表示为\(t_j\),其包含\(n_j\)个样本。因此有

\[t_{j+1}-t_j=\delta,\sum{n_j}=n, \tag{2}
\]
\[g_n(x) = \frac{1}{nh}\sum_{j=-\infty}^{\infty}{n_j K(\frac{x-t_j}{h})}. \tag{3}
\]

不难发现,在上述定义中,内核权重对应为\(n_j\)(或者说\(n_j/nh\)),也就是每个box从所有样本中分到的权重是多少。

卷积近似

在binning的第3步中,我们提到结合网格计数和核权重本质上是在做卷积运算,在许多文献中我们也经常可以发现为了加速KDE算法(或解决高维KDE问题)会提到进行卷积近似操作。

首先明确卷积的概念,参考维基百科的定义,卷积是通过两个函数\(f\)和\(g\)生成第三个函数的一种数学算子,设\(f(t)\)和\(g(t)\)是实数\(\mathcal{R}\)上的两个可积函数,定理两者的卷积为如下特定形式的积分变换:

\[f(t)*g(t) = \int_{-\infty}^{\infty}{f(\tau)g(t-\tau)d\tau}. \tag{4}
\]

将公式(3)和公式(4)起来,就不难建立起卷积近似在KDE中的作用了。

Linear Binning + Deriche Approximation

2021年的一篇short paper[3],表明the combination of linear binning and a recursive filter approximation by Deriche可以实现both fast and highly accurate的效果。

Luckily, 作者在Github上公布了开源代码;Unluckily, 代码语言是Javascript;于是我将其翻译为Python版本并公布在Github上。

然而,对于其效果我持怀疑态度,通过运行density1d.py-main,100个高斯分布采样点拟合得到的概率密度函数图像为



而且,更为关键的问题是无法该代码无法基于样本生成点x处的概率密度?即,在没有人为指定的情况下,代码会默认将[data.min()-pad×bandwidth, data.max()+pad×bandwidth]作为考虑范围,然后将这个区间划分为bins个区间,即\((x_0,x_1),(x_1,x_2),...,(x_{\text{bins}-1}, x_\text{bins})\),那么该方法生成的就是\(x_0,x_1,...\)这些点的概率密度,但如果我希望生成任意一个位置\(x, x\ne x_i\)处的概率密度,目前并没有实现。

sklearn.neighbors.KernelDensity

在sklearn库中有KDE算法的实现,而且其考虑采用KD-tree(默认)或Ball-tree来进行加速。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.neighbors import KernelDensity # 生成一些示例数据
np.random.seed(0)
data = np.random.normal(loc=0, scale=1, size=1000) # 创建一个KernelDensity对象
kde = KernelDensity(bandwidth=0.5, kernel='gaussian') # 用示例数据拟合KDE模型
kde.fit(data[:, None]) # 生成一些测试点来评估KDE模型
x = np.linspace(-5, 5, 1000)
x = x[:, None] # 使用KDE模型评估密度
log_density = kde.score_samples(x) # 绘制原始数据和KDE估计的密度曲线
plt.figure(figsize=(10, 6))
plt.hist(data, bins=30, density=True, alpha=0.5, color='blue')
plt.plot(x, np.exp(log_density), color='red', lw=2)
plt.title('Kernel Density Estimation (KDE)')
plt.xlabel('Value')
plt.ylabel('Density')
plt.show()

这是非常简单的一个例子,简单到甚至我之前没有思考为什么要先kde.fit()才能评估密度kde.score_sample()。通过公式(1),我们可以发现只要清楚指定的核函数\(K\)和带宽(bandwidth)\(h\),那么就可以计算出任意一点的概率密度,而这两个参数都是在创建KDE对象时就指定好的。

起初,我以为kde.fit()是在训练参数\(h\),甚至有那么一瞬间让我觉得困扰我们如何指定超参数\(h\)的值根本不是问题,因为它只是个初始值,而在fit之后会训练到最好的参数。这种思路明显是错的。因为缺乏ground_truth,因此模型本身就缺乏训练\(h\)的能力,这也是为什么\(h\)被称作超参数更合适而非参数。

既然核函数和带宽都是指定好的,那么如果我们不fit直接去评估密度呢?

import numpy as np
from sklearn.neighbors import KernelDensity # 生成一些示例数据
np.random.seed(0)
data = np.random.normal(loc=0, scale=1, size=1000) # 创建一个KernelDensity对象
kde = KernelDensity(bandwidth=0.5, kernel='gaussian') # 生成一些测试点来评估KDE模型
x = np.linspace(-5, 5, 1000)
x = x[:, None] # 使用KDE模型评估密度
log_density = kde.score_samples(x)
Traceback (most recent call last):
File "D:\python\fast_kde\main.py", line 17, in <module>
log_density = kde.score_samples(x)
^^^^^^^^^^^^^^^^^^^^
File "F:\anaconda3\Lib\site-packages\sklearn\neighbors\_kde.py", line 261, in score_samples
check_is_fitted(self)
File "F:\anaconda3\Lib\site-packages\sklearn\utils\validation.py", line 1462, in check_is_fitted
raise NotFittedError(msg % {"name": type(estimator).__name__})
sklearn.exceptions.NotFittedError: This KernelDensity instance is not fitted yet. Call 'fit' with appropriate arguments before using this estimator.

看起来,sklearn要求在调用kde.score_samples()必须要kde.fit()

通过阅读源代码,kde.fit()没有改变任何KDE的参数,但构建了内置对象self.tree_,这个就是我们前面提到的KD-tree或Ball-tree,而在进行评估时也是基于这个tree进行的。我的理解是,KD-tree维护样本之间的距离信息,在评估一个新的数据\(x\)处的概率密度时,和它距离太远的样本就不计算了?

References

[1] @article{wand1994fast,

title = {Fast Computation of Multivariate Kernel Estimators},

author = {Wand, M. P.},

year = {1994},

journal = {Journal of Computational and Graphical Statistics},

volume = {3},

number = {4},

pages = {433},

doi = {10.2307/1390904}

}

[2] @article{wand1994fast,

title = {Fast Computation of Multivariate Kernel Estimators},

author = {Wand, M. P.},

year = {1994},

journal = {Journal of Computational and Graphical Statistics},

volume = {3},

number = {4},

pages = {433},

doi = {10.2307/1390904}

}

[3] @inproceedings{heer2021fast,

title = {Fast & Accurate Gaussian Kernel Density Estimation},

booktitle = {2021 IEEE Visualization Conference (VIS)},

author = {Heer, Jeffrey},

year = {2021},

pages = {11--15},

publisher = {IEEE},

address = {New Orleans, LA, USA},

doi = {10.1109/VIS49827.2021.9623323},

isbn = {978-1-66543-335-8},

langid = {english}

}

KDE算法解析的更多相关文章

  1. 地理围栏算法解析(Geo-fencing)

    地理围栏算法解析 http://www.cnblogs.com/LBSer/p/4471742.html 地理围栏(Geo-fencing)是LBS的一种应用,就是用一个虚拟的栅栏围出一个虚拟地理边界 ...

  2. KMP串匹配算法解析与优化

    朴素串匹配算法说明 串匹配算法最常用的情形是从一篇文档中查找指定文本.需要查找的文本叫做模式串,需要从中查找模式串的串暂且叫做查找串吧. 为了更好理解KMP算法,我们先这样看待一下朴素匹配算法吧.朴素 ...

  3. Peterson算法与Dekker算法解析

    进来Bear正在学习巩固并行的基础知识,所以写下这篇基础的有关并行算法的文章. 在讲述两个算法之前,需要明确一些概念性的问题, Race Condition(竞争条件),Situations  lik ...

  4. python常见排序算法解析

    python——常见排序算法解析   算法是程序员的灵魂. 下面的博文是我整理的感觉还不错的算法实现 原理的理解是最重要的,我会常回来看看,并坚持每天刷leetcode 本篇主要实现九(八)大排序算法 ...

  5. Java虚拟机对象存活标记及垃圾收集算法解析

    一.对象存活标记 1. 引用计数算法 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1:当引用失效时,计数器就减1:任何时刻计数器都为0的对象就是不可能再被使用的. 引用计数算法(Re ...

  6. JVM垃圾回收算法解析

    JVM垃圾回收算法解析 标记-清除算法 该算法为最基础的算法.它分为标记和清除两个阶段,首先标记出需要回收的对象,在标记结束后,统一回收.该算法存在两个问题:一是效率问题,标记和清除过程效率都不太高, ...

  7. DeepFM算法解析及Python实现

    1. DeepFM算法的提出 由于DeepFM算法有效的结合了因子分解机与神经网络在特征学习中的优点:同时提取到低阶组合特征与高阶组合特征,所以越来越被广泛使用. 在DeepFM中,FM算法负责对一阶 ...

  8. GBDT+LR算法解析及Python实现

    1. GBDT + LR 是什么 本质上GBDT+LR是一种具有stacking思想的二分类器模型,所以可以用来解决二分类问题.这个方法出自于Facebook 2014年的论文 Practical L ...

  9. 最长上升子序列(LIS)n2 nlogn算法解析

    题目描述 给定一个数列,包含N个整数,求这个序列的最长上升子序列. 例如 2 5 3 4 1 7 6 最长上升子序列为 4. 1.O(n2)算法解析 看到这个题,大家的直觉肯定都是要用动态规划来做,那 ...

  10. Android逆向之旅---Android中锁屏密码算法解析以及破解方案

    一.前言 最近玩王者荣耀,下载了一个辅助样本,结果被锁机了,当然破解它很简单,这个后面会详细分析这个样本,但是因为这个样本引发出的欲望就是解析Android中锁屏密码算法,然后用一种高效的方式制作锁机 ...

随机推荐

  1. 对称加密算法汇总:AES DES 3DES SM4 java 实现入门

    密码的世界 如果你是黑帮老大,平时和手下沟通,如何保证自己的信息安全呢? 在神探夏洛克的第一季中,就讲述了一个如何侦破黑帮的加密交流的故事. 这种密码利用的是密码字典. 密码本身可以是一本书,比如常见 ...

  2. redis大key分析工具redis-rdb-tools

    最近1台云Redis的内存曝高,24G的内存占用19G,而且一直增长,想看那些key比较大,腾讯云Redis有大key分析的结果,但是这台没有,估计要找腾讯云的技术刷新一下数据: 分析大key工具,有 ...

  3. MySQL Unknown error 1267

    1.问题说明 最近在mysql中运行一段SQL直接报错: 有一点要说一下,这个navicat给出的报错太简短只有错误码,还得自己去查有点垃圾,不知道新版如何? 2.问题原因 这里可以看到问题出在t2. ...

  4. Java Socket编程系列(二)开发带回声功能的Server和Client

    服务器端: package com.dylan.socket; import java.io.*; import java.net.ServerSocket; import java.net.Sock ...

  5. Mac技巧之苹果电脑上将一个软件进程的 CPU 占用率限制在指定范围内:cputhrottle

    苹果电脑 Mac OS X 系统上,我们可以用 cputhrottle 这个免费工具,配合活动监视器和终端,把一个软件进程的 CPU 占用率限制在指定值(比如 20%)以内,以防止应为它 " ...

  6. 升级 vcpkg 遇到的一些坑

    项目上有个需求要用到 wil 库,于是打开 cmd 输入: vcpkg install wil:x86-windows-static 等了很久,一直卡在配置命令 连续试了好几遍,还是不行,安装其他的静 ...

  7. vs 工程中替换 Qt 静态库

    上篇介绍了如何编译 Qt 静态库 编译 windows 上的 qt 静态库 这篇介绍如何替换已有的 Qt 静态库,比如 Qt5.15.0 有很多 bug,我们不得不提升 Qt 版本来避免 bug 导致 ...

  8. Miniconda安装和使用

    Miniconda概述 Miniconda是什么? 要解释Miniconda是什么,先要弄清楚什么是Anaconda,它们之间的关系是什么? 而要知道Anaconda是什么,最先要明白的是搞清楚什么是 ...

  9. 搜索引擎RAG召回效果评测MTEB介绍与使用入门

    RAG 评测数据集建设尚处于初期阶段,缺乏针对特定领域和场景的专业数据集.市面上常见的 MS-Marco 和 BEIR 数据集覆盖范围有限,且在实际使用场景中效果可能与评测表现不符.目前最权威的检索榜 ...

  10. OpenCV开发笔记(六十八):红胖子8分钟带你使用特征点Flann最邻近差值匹配识别(图文并茂+浅显易懂+程序源码)

    若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...