闲话 6.30 -JL 引理
参考了 https://spaces.ac.cn/archives/8679/comment-page-1,有一些增删。
JL 引理
首先下面需要应用马尔可夫不等式的另一个形式:
主要的贡献是把马尔可夫不等式变成适配于正态分布的 exp 的形式。
单位模引理
单位模引理:对于 \(u\in \R^n\),每一维从 \(N(0,1/n)\) 随机采样,则
绝对值两边大概是差不多的(?),先看看 \(\par u\par ^2-1>\eps\)。
套用上述结论有:
化简 \(\E[e^{\la\par u\par^2}]\),将 \(u\) 正交分解为 \(u_{1:n}\)。
考虑正态分布 \(N(0,\sigma^2)\) 满足 \(\frac{1}{2\sigma^2}=\frac{n-2\la}{2}\),即 \(\sigma=\sqrt{\dfrac{1}{n-2\la}}\)。比较
上式的系数,得到:
那么
设 \(f(\la)=e^{-\la (\eps+1)}\left(\dfrac n{n-2\la}\right)^{n/2}\)。则(好像有误)
所以不妨取
所以:
而设 \(g(\eps)=\ln(\eps+1)-\eps+\eps^2/4\)。
所以 \(g(\eps)\le g(0)=0\),所以:
JL 引理
一个压缩版本(遇证明变答辩糕):表示 \(N\) 个向量(维持距离)只需 \(O(\log N)\) 个维数。
对于 \(N\) 个向量 \(v_{1:N}\in \R^m,n>\frac{24\ln N}{\eps^2}\),随机矩阵 \(A\in R^{n\times m}\) 采样于 \(N(0,1/n)\),\(\eps\in (0,1)\),则至少有 \(\frac{N-1}{N}\) 的概率满足:
证明:
容易发现,\(\forall u\in \R^m\),\((Au)_i\) 服从 \(N(0,1/n)\)。
使用 Union Bound,带入 \(u=\frac{v_i-v_j}{\par v_i-v_j\par}\)。那么:
若 \(n>\dfrac{24\ln N}{\eps^2}\),有:
结束。
但是在计算中,\(24\ln N/\eps^2\) 并不是非常小的值,如果不使用一些更加高级的降维算法的话,差不多只有在 \(\eps>0.5\),并且数量很多的时候才能发挥作用了(悲)。
题外话- Box-Muller
假如我就要朴素的实现,不要其他降维算法(感觉再往后都是机器学习的内容,跟 OI 没有什么关系。。。)!我就需要快速生成正态分布随机数。
这里直接给公式:设 \(x,y\) 是 \((0,1)\) 的随机数:
\((x_0,y_0)\) 就是要求的正态分布的点啦。
chatgpt 魅力时刻
我们用博特的代码来感受一下正确性!
#include <iostream>
#include <vector>
#include <cmath>
#include <random>
// Box-Muller 生成正态分布随机数
double boxMuller() {
static std::random_device rd;
static std::mt19937 gen(rd());
static std::uniform_real_distribution<> dis(0, 1);
double u = dis(gen);
double v = dis(gen);
return sqrt(-2.0 * log(u)) * cos(2.0 * M_PI * v);
}
// 计算两个向量的欧氏距离
double euclideanDistance(const std::vector<double>& a, const std::vector<double>& b) {
double sum = 0.0;
for (size_t i = 0; i < a.size(); ++i) {
sum += (a[i] - b[i]) * (a[i] - b[i]);
}
return sqrt(sum);
}
// 随机生成 N 个 K 维向量
std::vector<std::vector<double>> generateRandomVectors(int N, int K) {
std::vector<std::vector<double>> vectors(N, std::vector<double>(K));
for (int i = 0; i < N; ++i) {
for (int j = 0; j < K; ++j) {
vectors[i][j] = boxMuller();
}
}
return vectors;
}
// 使用 JL 引理降维
std::vector<std::vector<double>> jlTransform(const std::vector<std::vector<double>>& vectors, int D) {
int N = vectors.size();
int K = vectors[0].size();
std::vector<std::vector<double>> transformed(N, std::vector<double>(D));
for (int i = 0; i < N; ++i) {
for (int j = 0; j < D; ++j) {
transformed[i][j] = 0;
for (int k = 0; k < K; ++k) {
transformed[i][j] += vectors[i][k] * boxMuller();
}
transformed[i][j] /= sqrt(D);
}
}
return transformed;
}
// 计算所有向量之间的欧氏距离和
double totalEuclideanDistance(const std::vector<std::vector<double>>& vectors) {
double totalDistance = 0.0;
int N = vectors.size();
for (int i = 0; i < N; ++i) {
for (int j = i + 1; j < N; ++j) {
totalDistance += euclideanDistance(vectors[i], vectors[j]);
}
}
return totalDistance;
}
int main() {
int N, K;
std::cin >> N >> K;
double epsilon = 0.5;
int D = static_cast<int>(24 * log(N) / (epsilon * epsilon));
std::cerr<<"新维度 = "<<D<<std::endl;
auto vectors = generateRandomVectors(N, K);
auto transformedVectors = jlTransform(vectors, D);
double originalDistanceSum = totalEuclideanDistance(vectors);
double transformedDistanceSum = totalEuclideanDistance(transformedVectors);
std::cout << "Real: " << originalDistanceSum << std::endl;
std::cout << "Calc: " << transformedDistanceSum << std::endl;
return 0;
}
输入 100 2000
,得到的结果是
Real: 312086
Calc: 312885
把上文代码的 \(24\) 改为 \(12\) 的结果也是较为精确的。
Real: 313473
Calc: 310509
具体来说,当那个值取 \(24\) 的时候把 \(2000\) 维降到了 \(447\) 维,可谓表现相当不错(至少在我们的随机数据情况下)。