一次不成功的 pull request
昨天,我向 pgvector 提交的一个降维算法被拒绝了 Submit a simple vector dimensionality reduction function 。 这个结果对我来说,并不意外。顶多算是略有遗憾。我习惯使用ollama ,而ollama的embedding接口返回的是4096维度,远大于PGVector索引支持的最大维度2000。关于这个问题,我第一个想法是修改PGVector的维度限制,不过这个issue很快被拒绝了 Increased max dimensions for index from 2000 to 4096 。原因也很简单,PGVector这个限制其实来自PostgreSQL,PG的索引页最大为8K,所以PGVector的索引维度最大不能超过2000。 在此之后,我就开始尝试实现一个实用的降维算法。 一般来说,PCA(主成分分析)总是被首先推荐的,这玩意儿甚至是花书的一个重要示例。但是我并没有将 PCA 作为首选。这是因为:
- PCA 基于样本空间进行降维,但是我希望首先有一个不依赖预备知识,可以直接对任意向量进行降维操作的算法。
- 当然,完全零知识的前提下是无法有效降维的,我希望的是首先找到一个足够简单,但是对 ollama 的嵌入向量有效的压缩算法,可以将这些 4096 维的向量压缩到 256甚至更低的维度,但是仍然可以足够有效的用于 RAG 应用。如果能做到第一步,再继续尝试对其它类似的embedding向量构造算法。这样我就可以无状态的使用它。
- PGVector并不是一个很大的项目,我希望我的降维算法也足够简单,可以不依赖很重的外部资源,使用朴素的c语言实现。而PCA需要计算协方差和特征值或奇异值矩阵,这需要引入LAPACK和BLAS库,或者自己写出相关的算法。 因此,我尝试了一些朴素的,甚至可以说是非常朴素的算法。例如对m维降到n维的,直接用 n/m 划分原向量,再对每一组取平均值(我称之为norm 或 integral reduce)。再比如先算出相邻维度的差分,然后找到前n-1个最大的差分,以此为边界将原向量分割为n份,再求平均值——我称之为diff reduce——这多少有点儿像PCA,没办法,对一个向量(1xN矩阵),实在没办法求特征值,我看到的同行的实现,都是加入了一些白噪声维度再计算PCA,而这比我期待的还是重了一些。 我的意思是,关于PG的浮点数计算,我有很多有野心的想法,但是目前我希望写出一个足够小的函数,可以让PGVector社区毫无压力的接受它。所以我重点实验了一下几种不需要样本数据集,不需要引入额外维度,不需要ggml,也不需要lapack之类的算法库支持的微型算法。 很意外,效果最好的,居然是最简单的按固定区间分组求平均值的算法——我称之为 norm reduce。 不太意外的是,即使是这个表现最好的算法,也仍然不能说是一个非常好的降维算法。我找了一些数据样本,先在ollama环境用 llama3-8b 生成嵌入向量,然后用不用的算法将其降维到 256、512和1024维度,从结果来看,大部分近似匹配结果不能跟原始维度保持一致,而norm算法至少在大多数情况下,保持原向量匹配的最佳样本出现在前五,甚至原始向量的前三个匹配结果都可以在norm 512匹配中找到——对,512维度的匹配结果甚至好于1024维度。 实际上,如果把ollama的embedding过程看作一个黑盒的话,向量的维度之间即使存在隐含的相关性,也无法想象它正好机械的按索引位置分组。因此相邻维度分组求平均值,本来是我随手写出来,作为对照组的。虽然 diff reduce 看起来也有点莫名其妙,但是它至少从几何意义上,保留了相邻维度间差分最大的特征,也就是说尽可能保留了几何形状中变化最剧烈的那些特征。 出了平均值,我还考虑了类似中位数这样的聚合方法,但如何分组比如何聚合, 对降维更重要才对,这也就是我们为何要在PCA算法中去费力计算协方差和SDV的原因。 然而实际上是,norm确实不太有效,而其它简单算法更加混乱。 对于我来说,norm只能算是一个有实验意义的算法,它唯一的优点就是足够的简单,因此我还是尝试提交了这个 pull request,虽然被拒掉,也不算有什么挫败感。 接下来我准备做这样几步工作:
- 构造一个基于样本集的降维算法实现,允许对指定表(甚至是查询集)的特定字段进行整体降维,这样就可以实现正规的PCA降维处理
- 可以尝试对ollama+特定模型的embedding训练一个对应的神经网络模型,用于降维操作。ggml 本身是一个c库,利用这个既有资源,可以让这个降维算法充分利用硬件资源。
- 或许可以梳理出一个足够通用的PCA实现,对于ollama+特定模型的这个特定的样本空间,即使处理从未出现过的样本,也可以有效降维,那么同样可以把它定义为一个 ggml 可以加载的算法。
- 如果可以做到前两步,那么对于新出现的嵌入模型,我们也仅需要训练或求解出对应的降维模型 这样的架构已经庞大,不再适合直接在 PGVector 中实现,因此我正在编写一个独立的矩阵计算库,尝试将 PG 插件和 PGVector支持作为可选的编译选项集成进去。 虽然不知道下一份工作会是什么方向,但是我相信,如果某一天我在工作中遇到要在数据库层面做矩阵或神经网络计算,再临时去折腾,肯定是来不及的,希望那一天到来的时候,我可以从容的把我自己的算法实现装上去。