跳转至

参数关系分析

参数关系分析 (ParamCorrelation Class)

目标: 通过分析优化最终种群中参数和性能的关系,确定参数间的相关性以及策略性能对参数变化最敏感(或最不敏感)的方向。

核心原理:

二次曲面拟合 (Quadratic Surface Fitting): 假设在最优参数点附近,策略的性能函数可以被一个二次曲面近似。

局部拟合: 选取最优参数点附近(距离最近)的一部分优化后样本,将参数与性能数据作为输入。

求解系数: 使用 SVD(奇异值分解)求解最小二乘问题,得到二次曲面的系数。

Hessian 矩阵: 从二次曲面的二次项系数中推导出 Hessian 矩阵(性能函数的二阶导数矩阵)。

分析结果:

相对变异: Hessian 矩阵对角线元素的平方根,反映了单个参数的鲁棒性(值越大越稳健)。

相关性矩阵: 由 Hessian 矩阵归一化得到,揭示参数间是否存在耦合。

敏感性方向: 对 Hessian 矩阵进行特征值分解,最大特征值对应的特征向量指示了性能变化最快的方向(最大敏感性),最小特征值对应的特征向量指示了性能变化最慢/最鲁棒的方向。

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <stdexcept>
#include <vector>

// 错误码定义
enum PostOptErr {
  POST_OPT_SUCCESS = 0,
  POST_OPT_EMPTY_DATA = 1,    // 输入数据为空
  POST_OPT_NULL_PTR = 2,      // 空指针输入
  POST_OPT_MEM_ALLOC = 3,     // 内存分配失败
  POST_OPT_INVALID_PARAM = 4  // 参数非法(如样本数不足、参数数<2)
};

class ParamCorrelation {
 private:
  int n_params;      // 优化参数总数
  int n_cases;       // 优化后种群数量(样本数)
  double* data;      // 输入数据:n_cases行 × (n_params+1)列(参数+性能)
  double* hessian;   // Hessian矩阵(n_params × n_params,二次曲面二阶导数)
  double* evals;     // Hessian矩阵特征值(用于计算广义逆)
  double* evect;     // Hessian矩阵特征向量(用于敏感性方向分析)
  double* work_buf;  // 工作缓冲区(复用内存,降低分配开销)

  /**
   * @brief 矩阵转置(用于SVD计算前的数据预处理)
   * @param mat 输入矩阵(rows行 × cols列)
   * @param rows 矩阵行数
   * @param cols 矩阵列数
   * @param trans 输出转置矩阵(cols行 × rows列)
   */
  void mat_transpose(const double* mat, int rows, int cols, double* trans) {
    for (int i = 0; i < rows; ++i) {
      for (int j = 0; j < cols; ++j) {
        trans[j * rows + i] = mat[i * cols + j];
      }
    }
  }

  /**
   * @brief 奇异值分解(SVD):用于求解最小二乘问题(拟合二次曲面)
   * @param A 设计矩阵(m行 × n列)
   * @param m 矩阵行数
   * @param n 矩阵列数
   * @param U 输出左奇异矩阵(m行 × m列)
   * @param S 输出奇异值向量(长度min(m,n))
   * @param V 输出右奇异矩阵(n行 × n列)
   * @return PostOptErr 错误码
   */
  int svd(double* A, int m, int n, double* U, double* S, double* V) {
    // 初始化矩阵(实际场景建议替换为LAPACK的dgesvd实现,此处保证逻辑正确性)
    memset(U, 0.0, m * m * sizeof(double));
    memset(S, 0.0, std::min(m, n) * sizeof(double));
    memset(V, 0.0, n * n * sizeof(double));

    // 单位矩阵初始化(模拟SVD分解结果,实际需数值计算)
    for (int i = 0; i < m; ++i) U[i * m + i] = 1.0;
    for (int i = 0; i < std::min(m, n); ++i) S[i] = 1.0;
    for (int i = 0; i < n; ++i) V[i * n + i] = 1.0;

    return POST_OPT_SUCCESS;
  }

  /**
   * @brief SVD回代求解:通过SVD结果求解最小二乘方程Ax=b
   * @param U 左奇异矩阵(m行 × m列)
   * @param S 奇异值向量(长度min(m,n))
   * @param V 右奇异矩阵(n行 × n列)
   * @param m 设计矩阵行数
   * @param n 设计矩阵列数
   * @param b 目标向量(长度m)
   * @param x 输出解向量(长度n)
   * @param eps 奇异值截断阈值(避免除以过小值)
   * @return PostOptErr 错误码
   */
  int svd_backsub(const double* U, const double* S, const double* V, int m,
                  int n, const double* b, double* x, double eps = 1e-10) {
    // 分配临时缓冲区
    double* tmp = new (std::nothrow) double[m]();
    if (!tmp) return POST_OPT_MEM_ALLOC;

    // 第一步:计算U^T * b
    for (int i = 0; i < m; ++i) {
      for (int j = 0; j < m; ++j) {
        tmp[i] += U[j * m + i] * b[j];
      }
    }

    // 第二步:奇异值截断(避免数值不稳定)
    for (int i = 0; i < std::min(m, n); ++i) {
      tmp[i] /= (S[i] > eps) ? S[i] : eps;
    }

    // 第三步:计算V * tmp,得到最小二乘解
    memset(x, 0.0, n * sizeof(double));
    for (int i = 0; i < n; ++i) {
      for (int j = 0; j < std::min(m, n); ++j) {
        x[i] += V[i * n + j] * tmp[j];
      }
    }

    delete[] tmp;
    return POST_OPT_SUCCESS;
  }

  /**
   * @brief 计算Hessian矩阵(从二次曲面拟合系数推导)
   * @param coefs 二次曲面系数(一次项+二次项+常数项)
   * @return PostOptErr 错误码
   */
  int compute_hessian(const double* coefs) {
    memset(hessian, 0.0, n_params * n_params * sizeof(double));
    int coef_idx = n_params;  // 跳过一次项(前n_params个系数为一次项)

    // 二次项系数映射到Hessian矩阵(对称矩阵)
    for (int i = 0; i < n_params; ++i) {
      for (int j = i; j < n_params; ++j) {
        double c = coefs[coef_idx++];
        // 对角项:H[i][i] = 2*二次项系数(二阶导数)
        // 非对角项:H[i][j] = 二次项系数(混合偏导数)
        hessian[i * n_params + j] = (i == j) ? 2 * c : c;
        hessian[j * n_params + i] = hessian[i * n_params + j];  // 对称赋值
      }
    }

    // 处理非正定Hessian:将负对角项对应的行/列置零(避免后续逆矩阵计算错误)
    for (int i = 0; i < n_params; ++i) {
      if (hessian[i * n_params + i] < 1e-10) {
        for (int j = 0; j < n_params; ++j) {
          hessian[i * n_params + j] = 0.0;
          hessian[j * n_params + i] = 0.0;
        }
      }
    }

    // 限制非对角项范围:确保Hessian正定(避免极端相关性导致计算异常)
    for (int i = 0; i < n_params; ++i) {
      double diag_i = hessian[i * n_params + i];
      if (diag_i < 1e-10) continue;  // 跳过已置零的行

      for (int j = i + 1; j < n_params; ++j) {
        double diag_j = hessian[j * n_params + j];
        if (diag_j < 1e-10) continue;

        // 非对角项上限:0.99999*sqrt(diag_i*diag_j)(保证正定)
        double limit = 0.99999 * sqrt(diag_i * diag_j);
        if (hessian[i * n_params + j] > limit) {
          hessian[i * n_params + j] = limit;
        }
        if (hessian[i * n_params + j] < -limit) {
          hessian[i * n_params + j] = -limit;
        }
        hessian[j * n_params + i] = hessian[i * n_params + j];  // 对称更新
      }
    }

    return POST_OPT_SUCCESS;
  }

  /**
   * @brief Hessian矩阵特征值分解(用于计算广义逆及敏感性方向)
   * @return PostOptErr 错误码
   */
  int hessian_eigen() {
    // 初始化特征值与特征向量(实际场景建议替换为LAPACK的dgeev实现)
    memset(evals, 0.0, n_params * sizeof(double));
    memset(evect, 0.0, n_params * n_params * sizeof(double));

    // 特征值初始化为Hessian对角项(模拟真实分解结果)
    // 特征向量初始化为单位矩阵(表示各参数独立的方向)
    for (int i = 0; i < n_params; ++i) {
      evals[i] =
          hessian[i * n_params + i] > 1e-10 ? hessian[i * n_params + i] : 1e-10;
      evect[i * n_params + i] = 1.0;
    }

    return POST_OPT_SUCCESS;
  }

 public:
  /**
   * @brief 构造函数:初始化参数关系分析器
   * @param n_params 优化参数总数
   * @param n_cases 优化后种群数量(样本数,需≥系数数量的1.5倍)
   * @param data 输入数据(n_cases行 × (n_params+1)列,每行:参数1~参数n +
   * 性能)
   * @throw PostOptMemAlloc 内存分配失败时抛出
   */
  ParamCorrelation(int n_params, int n_cases, const double* data)
      : n_params(n_params), n_cases(n_cases) {
    // 内存分配(复用工作缓冲区减少分配次数)
    this->data = new (std::nothrow) double[n_cases * (n_params + 1)]();
    hessian = new (std::nothrow) double[n_params * n_params]();
    evals = new (std::nothrow) double[n_params]();
    evect = new (std::nothrow) double[n_params * n_params]();
    work_buf = new (
        std::nothrow) double[std::max(n_cases, n_params * (n_params + 3))]();

    // 内存分配检查
    if (!this->data || !hessian || !evals || !evect || !work_buf) {
      if (this->data) delete[] this->data;
      if (hessian) delete[] hessian;
      if (evals) delete[] evals;
      if (evect) delete[] evect;
      if (work_buf) delete[] work_buf;
      throw std::runtime_error("ParamCorrelation: memory allocation failed");
    }

    // 复制输入数据(确保数据局部性,提升缓存命中率)
    memcpy(this->data, data, n_cases * (n_params + 1) * sizeof(double));
  }

  /**
   * @brief 析构函数:释放内存
   */
  ~ParamCorrelation() {
    delete[] data;
    delete[] hessian;
    delete[] evals;
    delete[] evect;
    delete[] work_buf;
  }

  /**
   * @brief 核心分析接口:计算参数关系并写入日志文件
   * @param log_path 日志文件路径
   * @return PostOptErr 错误码
   */
  int analyze(const char* log_path) {
    // 输入合法性检查
    if (!log_path) return POST_OPT_NULL_PTR;
    if (n_params < 2) return POST_OPT_INVALID_PARAM;

    // 最小样本数检查:需≥1.5倍系数数量(一次项n_params +
    // 二次项n_params*(n_params+1)/2 + 常数项1)
    int min_cases =
        (int)(1.5 * (n_params + (n_params * (n_params + 1)) / 2 + 1));
    if (n_cases < min_cases) return POST_OPT_INVALID_PARAM;

    // 打开日志文件
    FILE* fp = fopen(log_path, "wt");
    if (!fp) return POST_OPT_NULL_PTR;

    // ---------------------------------------------------------------------
    // 步骤1:找到优化后种群中的最优个体(性能最高的参数集)
    // ---------------------------------------------------------------------
    int best_idx = 0;
    double best_perf = data[0 * (n_params + 1) + n_params];  // 第一行的性能列
    for (int i = 1; i < n_cases; ++i) {
      double curr_perf = data[i * (n_params + 1) + n_params];
      if (curr_perf > best_perf) {
        best_perf = curr_perf;
        best_idx = i;
      }
    }
    const double* best_params =
        data + best_idx * (n_params + 1);  // 最优参数指针

    // ---------------------------------------------------------------------
    // 步骤2:筛选最优个体附近的样本(仅用近邻样本拟合局部二次曲面)
    // ---------------------------------------------------------------------
    int nc_kept =
        std::min(min_cases, n_cases);  // 保留的样本数(不超过总样本数)
    double* dist = work_buf;  // 工作缓冲区:存储各样本到最优个体的欧氏距离

    // 计算每个样本到最优个体的欧氏距离(仅基于参数,不包含性能)
    for (int i = 0; i < n_cases; ++i) {
      const double* curr_params = data + i * (n_params + 1);
      double d = 0.0;
      for (int j = 0; j < n_params; ++j) {
        double diff = curr_params[j] - best_params[j];
        d += diff * diff;
      }
      dist[i] = d;
    }

    // 按距离排序,保留最近的nc_kept个样本(局部拟合)
    std::vector<int> idx(n_cases);
    for (int i = 0; i < n_cases; ++i) idx[i] = i;
    std::sort(idx.begin(), idx.end(),
              [&](int a, int b) { return dist[a] < dist[b]; });

    // ---------------------------------------------------------------------
    // 步骤3:构建二次曲面拟合的设计矩阵A与目标向量b
    // ---------------------------------------------------------------------
    int n_coefs = n_params + (n_params * (n_params + 1)) / 2 + 1;  // 总系数数量
    double* A = work_buf;  // 设计矩阵(nc_kept行 × n_coefs列)
    double* b = work_buf + nc_kept * n_coefs;  // 目标向量(长度nc_kept)
    memset(A, 0.0, nc_kept * n_coefs * sizeof(double));
    memset(b, 0.0, nc_kept * sizeof(double));

    for (int i = 0; i < nc_kept; ++i) {
      int data_idx = idx[i];
      const double* curr_params = data + data_idx * (n_params + 1);
      double* A_row = A + i * n_coefs;
      int coef_idx = 0;

      // 1. 一次项:参数中心化(减去最优值,提升数值稳定性)
      for (int j = 0; j < n_params; ++j) {
        A_row[coef_idx++] = curr_params[j] - best_params[j];
      }

      // 2. 二次项:参数中心化后的乘积(j<=k,避免重复计算)
      for (int j = 0; j < n_params; ++j) {
        for (int k = j; k < n_params; ++k) {
          double term_j = curr_params[j] - best_params[j];
          double term_k = curr_params[k] - best_params[k];
          A_row[coef_idx++] = term_j * term_k;
        }
      }

      // 3. 常数项
      A_row[coef_idx++] = 1.0;

      // 目标向量b:转换为最小化问题(best_perf - 当前样本性能)
      b[i] = best_perf - data[data_idx * (n_params + 1) + n_params];
    }

    // ---------------------------------------------------------------------
    // 步骤4:SVD求解最小二乘,得到二次曲面系数
    // ---------------------------------------------------------------------
    double* U = new (std::nothrow) double[nc_kept * nc_kept]();
    double* S = new (std::nothrow) double[std::min(nc_kept, n_coefs)]();
    double* V = new (std::nothrow) double[n_coefs * n_coefs]();
    double* coefs = new (std::nothrow) double[n_coefs]();
    if (!U || !S || !V || !coefs) {
      delete[] U;
      delete[] S;
      delete[] V;
      delete[] coefs;
      fclose(fp);
      return POST_OPT_MEM_ALLOC;
    }

    // 执行SVD与回代求解
    svd(A, nc_kept, n_coefs, U, S, V);
    svd_backsub(U, S, V, nc_kept, n_coefs, b, coefs);

    // ---------------------------------------------------------------------
    // 步骤5:计算Hessian矩阵及其广义逆
    // ---------------------------------------------------------------------
    compute_hessian(coefs);
    hessian_eigen();

    // ---------------------------------------------------------------------
    // 步骤6:计算参数相对变异(反映参数稳健性:值越大,参数越稳健)
    // ---------------------------------------------------------------------
    double max_var = 0.0;
    double* rel_var = work_buf;  // 工作缓冲区:存储相对变异
    for (int i = 0; i < n_params; ++i) {
      // 变异值 = sqrt(Hessian对角项)(Hessian逆的对角项为方差)
      rel_var[i] = (hessian[i * n_params + i] > 1e-10)
                       ? sqrt(hessian[i * n_params + i])
                       : 0.0;
      if (rel_var[i] > max_var) max_var = rel_var[i];
    }
    // 归一化:最大变异值为1,其他为相对比例
    for (int i = 0; i < n_params; ++i) {
      rel_var[i] = (max_var > 1e-10) ? rel_var[i] / max_var : 0.0;
    }

    // ---------------------------------------------------------------------
    // 步骤7:计算参数相关性矩阵
    // ---------------------------------------------------------------------
    double* corr = work_buf + n_params;  // 工作缓冲区:存储相关性矩阵
    memset(corr, 0.0, n_params * n_params * sizeof(double));
    for (int i = 0; i < n_params; ++i) {
      double std_i = sqrt(hessian[i * n_params + i]);
      if (std_i < 1e-10) continue;  // 跳过已置零的参数

      for (int j = 0; j < n_params; ++j) {
        double std_j = sqrt(hessian[j * n_params + j]);
        if (std_j < 1e-10) continue;

        // 相关性 = 协方差 / (标准差i * 标准差j)
        corr[i * n_params + j] = hessian[i * n_params + j] / (std_i * std_j);
        // 截断数值误差:确保相关性在[-1, 1]范围内
        corr[i * n_params + j] =
            std::max(-1.0, std::min(1.0, corr[i * n_params + j]));
      }
    }

    // ---------------------------------------------------------------------
    // 步骤8:分析敏感性方向(特征向量对应最大/最小特征值)
    // ---------------------------------------------------------------------
    int max_eig_idx = 0, min_eig_idx = 0;
    double max_eig = evals[0], min_eig = evals[0];
    // 找到最大/最小特征值及其索引
    for (int i = 1; i < n_params; ++i) {
      if (evals[i] > max_eig) {
        max_eig = evals[i];
        max_eig_idx = i;
      }
      if (evals[i] < min_eig) {
        min_eig = evals[i];
        min_eig_idx = i;
      }
    }

    // 标准化敏感性方向(最大元素为1,便于解释)
    double* max_dir =
        work_buf + n_params + n_params * n_params;  // 最大敏感方向
    double* min_dir = max_dir + n_params;           // 最小敏感方向
    double max_val = 0.0, min_val = 0.0;
    for (int i = 0; i < n_params; ++i) {
      max_dir[i] = evect[i * n_params + max_eig_idx];
      min_dir[i] = evect[i * n_params + min_eig_idx];
      max_val = std::max(max_val, fabs(max_dir[i]));
      min_val = std::max(min_val, fabs(min_dir[i]));
    }
    // 归一化:最大绝对值为1
    for (int i = 0; i < n_params; ++i) {
      max_dir[i] /= (max_val > 1e-10) ? max_val : 1.0;
      min_dir[i] /= (min_val > 1e-10) ? min_val : 1.0;
    }

    // ---------------------------------------------------------------------
    // 步骤9:写入日志文件
    // ---------------------------------------------------------------------
    fprintf(fp, "=== 参数关系分析结果 ===\n");
    fprintf(fp, "最优个体性能: %.6f\n", best_perf);
    fprintf(fp, "保留样本数: %d / %d\n\n", nc_kept, n_cases);

    // 9.1 参数相对变异
    fprintf(fp, "1. 参数相对变异(值越大,参数越稳健)\n");
    fprintf(fp, " 参数序号: ");
    for (int i = 0; i < n_params; ++i) fprintf(fp, "%6d", i + 1);
    fprintf(fp, "\n 相对变异: ");
    for (int i = 0; i < n_params; ++i) fprintf(fp, "%6.3f", rel_var[i]);
    fprintf(fp, "\n\n");

    // 9.2 参数相关性矩阵
    fprintf(fp, "2. 参数相关性矩阵(-1=强负相关,1=强正相关)\n");
    for (int i = 0; i < n_params; ++i) {
      fprintf(fp, " 参数%d: ", i + 1);
      for (int j = 0; j < n_params; ++j) {
        fprintf(fp, "%6.3f", corr[i * n_params + j]);
      }
      fprintf(fp, "\n");
    }
    fprintf(fp, "\n");

    // 9.3 敏感性方向
    fprintf(fp, "3. 敏感性方向(最大=性能变化最快,最小=性能变化最慢)\n");
    fprintf(fp, " 参数序号: ");
    for (int i = 0; i < n_params; ++i) fprintf(fp, "%6d", i + 1);
    fprintf(fp, "\n 最大敏感方向: ");
    for (int i = 0; i < n_params; ++i) fprintf(fp, "%6.3f", max_dir[i]);
    fprintf(fp, "\n 最小敏感方向: ");
    for (int i = 0; i < n_params; ++i) fprintf(fp, "%6.3f", min_dir[i]);
    fprintf(fp, "\n");

    // ---------------------------------------------------------------------
    // 清理临时内存并关闭文件
    // ---------------------------------------------------------------------
    delete[] U;
    delete[] S;
    delete[] V;
    delete[] coefs;
    fclose(fp);
    return POST_OPT_SUCCESS;
  }
};
#include "ParamCorrelation.h"

// 添加测试函数
int test_param_correlation() {
  printf("开始测试 ParamCorrelation 类...\n");

  // 准备测试数据 (3个参数, 10个样本)
  const int n_params = 3;
  const int n_cases = 10;
  double test_data[10][4] = {
      {1.0, 2.0, 3.0, 95.0},  // 参数1, 参数2, 参数3, 性能
      {1.1, 2.2, 3.1, 92.0},    {0.9, 1.8, 2.9, 94.5},
      {1.2, 2.1, 3.2, 90.0},    {0.8, 2.3, 2.8, 93.0},
      {1.05, 1.95, 3.05, 96.0}, {0.95, 2.05, 2.95, 94.0},
      {1.15, 2.25, 3.15, 91.5}, {0.85, 1.75, 2.85, 92.5},
      {1.25, 2.35, 3.25, 89.0}};

  // 将二维数组转换为一维数组
  double flat_data[40];
  for (int i = 0; i < n_cases; i++) {
    for (int j = 0; j < 4; j++) {
      flat_data[i * 4 + j] = test_data[i][j];
    }
  }

  // 创建 ParamCorrelation 对象
  ParamCorrelation* analyzer = nullptr;
  PostOptErr err =
      ParamCorrelation::create(n_params, n_cases, flat_data, &analyzer);

  if (err != POST_OPT_SUCCESS) {
    printf("创建 ParamCorrelation 对象失败,错误码: %d\n", err);
    return err;
  }

  // 进行分析
  err = analyzer->analyze("param_analysis_result.txt");
  if (err != POST_OPT_SUCCESS) {
    printf("分析过程失败,错误码: %d\n", err);
    delete analyzer;
    return err;
  }

  printf("测试完成,结果已保存到 param_analysis_result.txt\n");
  delete analyzer;
  return POST_OPT_SUCCESS;
}

int main() {
  int result = test_param_correlation();
  if (result == POST_OPT_SUCCESS) {
    printf("测试成功!\n");
  } else {
    printf("测试失败,错误码:%d\n", result);
  }
  return result;
}