0


SVD 奇异值分解纯手工实现(C++)

SVD 奇异值分解纯手工实现(C++)

6月10日,风雨大作。。。(好吧雨早停了)
一时兴起,想着好久没敲过CPP了,最近正好打算复习一下《统计学习方法》,本来想看看PCA来着,一打眼瞅到了SVD。。。SVD是一个比较基础的矩阵分解法,后边的PCA还会用到,所以干脆计划了晚上小小复习一下SVD,然后用久违的CPP实现一下,谁料想本以为挺早就能下班的,一直写到第二天下午。。。。太菜了!!!!但是实现完后还是挺开心的,因此记录一下这一天的成果。

关于SVD

个人以为SVD好用就是好用在条件弱,效果好,任意的实矩阵都能进行SVD,理论依据如下:

奇异值分解定理

  1. A
  2. A
  3. A为一
  4. m
  5. ×
  6. n
  7. \text{m} \times{\text{n}}
  8. m×n实矩阵,
  9. A
  10. R
  11. m
  12. ×
  13. n
  14. A\in R^{\text{m} \times{\text{n}}}
  15. ARm×n,则
  16. A
  17. A
  18. A的奇异值分解存在,且:
  19. A
  20. =
  21. U
  22. Σ
  23. V
  24. T
  25. A=U\Sigma V^T
  26. A=UΣVT

其中:

  1. Σ
  2. =
  3. [
  4. Σ
  5. 1
  6. ,
  7. Σ
  8. 2
  9. ]
  10. =
  11. (
  12. σ
  13. 1
  14. σ
  15. 2
  16. σ
  17. r
  18. σ
  19. n
  20. )
  21. \Sigma=[\Sigma_1,\Sigma_2] = \begin{pmatrix} \sigma_1\\ & \sigma_2 \\ && \ddots \\ &&& \sigma_r \\ &&&& \ddots\\ &&&&&\sigma_n\\ \end{pmatrix}
  22. Σ=[Σ1​,Σ2​]=⎝⎜⎜⎜⎜⎜⎜⎛​σ1​​σ2​​⋱​σr​​⋱​σn​​⎠⎟⎟⎟⎟⎟⎟⎞​
  23. σ
  24. i
  25. \sigma_i
  26. σi​为
  27. A
  28. T
  29. A
  30. A^TA
  31. ATA的特征值开根号,称为矩阵
  32. A
  33. A
  34. A的奇异值。
  35. r
  36. r
  37. r为非零特征值的个数,
  38. σ
  39. i
  40. σ
  41. i
  42. +
  43. 1
  44. ,
  45. i
  46. =
  47. 0
  48. ,
  49. 1
  50. ,
  51. .
  52. .
  53. .
  54. ,
  55. n
  56. 1
  57. \sigma_i \ge \sigma_{i+1},i=0,1,...,n-1
  58. σi​≥σi+1​,i=0,1,...,n1.
  59. σ
  60. j
  61. =
  62. 0
  63. ,
  64. j
  65. >
  66. r
  67. \sigma_j=0,j>r
  68. σj​=0,j>r
  69. V
  70. =
  71. [
  72. V
  73. 1
  74. ,
  75. V
  76. 2
  77. ]
  78. =
  79. [
  80. v
  81. 1
  82. ,
  83. v
  84. 2
  85. ,
  86. .
  87. .
  88. .
  89. ,
  90. v
  91. r
  92. ,
  93. .
  94. .
  95. .
  96. v
  97. n
  98. ]
  99. V=[V_1,V_2]=[v_1,v_2,...,v_r,...v_n]
  100. V=[V1​,V2​]=[v1​,v2​,...,vr​,...vn​]
  101. V
  102. 1
  103. V_1
  104. V1​为非零特征值对应的特征向量按特征值从大到小排列的列向量组,
  105. V
  106. 2
  107. V_2
  108. V2​为特征值为零的特征向量组。
  109. U
  110. =
  111. A
  112. V
  113. Σ
  114. 1
  115. =
  116. A
  117. [
  118. V
  119. 1
  120. ,
  121. V
  122. 2
  123. ]
  124. Σ
  125. U=AV\Sigma^{-1}=A[V_1,V_2]\Sigma
  126. U=AVΣ−1=A[V1​,V2​]Σ

证明也为构造性证明,具体见《统计学习方法》。

紧奇异值分解

其实,通过一些矩阵运算,可以得到:

  1. A
  2. =
  3. U
  4. Σ
  5. V
  6. T
  7. =
  8. U
  9. 1
  10. Σ
  11. 1
  12. V
  13. 1
  14. T
  15. A=U\Sigma V^T=U_1\Sigma_1V_1^T
  16. A=UΣVT=U1​Σ1V1T
  17. U
  18. 1
  19. R
  20. m
  21. ×
  22. r
  23. U_1 \in{R^{\rm m\times r}}\,
  24. U1​∈Rm×r,
  25. Σ
  26. 1
  27. R
  28. r
  29. ×
  30. r
  31. \Sigma_1 \in{R^{\rm r\times r}}\,
  32. Σ1​∈Rr×r,
  33. V
  34. 1
  35. R
  36. n
  37. ×
  38. r
  39. V_1\in{\rm R^{n\times r}}
  40. V1​∈Rn×r.

可以看到,原来矩阵

  1. A
  2. R
  3. m
  4. ×
  5. n
  6. A\in{\rm R^{m\times n}}
  7. ARm×n,有
  8. m
  9. n
  10. m\cdot n
  11. mn个元素,而经过紧奇异值分解后,有
  12. m
  13. r
  14. +
  15. r
  16. r
  17. +
  18. n
  19. r
  20. m\cdot r+r\cdot r+n\cdot r
  21. mr+rr+nr,当
  22. r
  23. r
  24. r较小时,数据占用内存大大减少,因此还可以用奇异值分解进行无损压缩矩阵。

截断奇异值分解

透过现象看本质,根据前面奇异值分解的公式,我们可以推导出,每一个矩阵都可以分解成如下形式:

  1. A
  2. =
  3. σ
  4. 1
  5. A
  6. 1
  7. +
  8. σ
  9. 2
  10. A
  11. 2
  12. +
  13. .
  14. .
  15. .
  16. +
  17. σ
  18. r
  19. A
  20. r
  21. A=\sigma_1\cdot A_1+\sigma_2\cdot A_2+...+\sigma_r\cdot A_r
  22. A1​⋅A1​+σ2​⋅A2​+...+σr​⋅Ar

这里,暂且不管

  1. A
  2. i
  3. ,
  4. i
  5. =
  6. 1
  7. ,
  8. 2
  9. ,
  10. .
  11. .
  12. .
  13. ,
  14. r
  15. A_i,i=1,2,...,r
  16. Ai​,i=1,2,...,r是什么(可以推,太麻烦了不好写),可以很明显的看出
  17. A
  18. A
  19. A可以由
  20. σ
  21. i
  22. \sigma_i
  23. σi​为权值通过矩阵叠加组成,因此,当某些
  24. σ
  25. i
  26. \sigma_i
  27. σi​很小时,对组成
  28. A
  29. A
  30. A的贡献很小,可以看成
  31. A
  32. A
  33. A的噪声,将其去除,这样就得到了截断奇异值分解,其矩阵形式如下:
  34. A
  35. U
  36. k
  37. Σ
  38. k
  39. V
  40. k
  41. T
  42. A\approx U_k\Sigma_kV_k^T
  43. AUk​ΣkVkT
  44. k
  45. k
  46. k大小等于保留的奇异值个数,由于奇异值矩阵
  47. Σ
  48. \Sigma
  49. Σ是按从大到小排列的,因此
  50. U
  51. k
  52. U_k
  53. Uk​表示取
  54. U
  55. U
  56. U的前
  57. k
  58. k
  59. k列,
  60. Σ
  61. k
  62. \Sigma_k
  63. Σk​表示取
  64. Σ
  65. \Sigma
  66. Σ的前
  67. k
  68. k
  69. k
  70. k
  71. k
  72. k列所组成的方阵,
  73. V
  74. k
  75. V_k
  76. Vk​表示取
  77. V
  78. V
  79. V的前
  80. k
  81. k
  82. k列。

根据上述思想,我们可以对矩阵通过截断奇异值分解进行有损压缩,去噪等。

代码

代码构架

为了实现SVD,根据上述公式可以得出,我们需要用到的工具有:

  1. 盛放矩阵的数据结构;(在此我选用了vector容器)
  1. vector<vector<double>> A ={{1,0,0,0},{0,0,0,4},{0,3,0,0},{0,0,0,0},{2,0,0,0}};
  1. 矩阵乘法运算 ;
  1. // 矩阵乘法template<typenameT>
  2. vector<vector<T>>matrix_multiply(vector<vector<T>>const arrL, vector<vector<T>>const arrR){int rowL = arrL.size();// 左矩阵行数int colL = arrL[0].size();// 左矩阵列数int rowR = arrR.size();// 右矩阵行数int colR = arrR[0].size();// 右矩阵列数// 判断是否能够相乘if(colL != rowR){throw"left matrix's row not should equal with right matrix!";}// initialize result matrix
  3. vector<vector<T>>res(rowL);for(int i=0; i<res.size();i++){
  4. res[i].resize(colR);}// compute matrix multiplicationfor(int i=0; i<rowL; i++){for(int j=0; j<colR; j++){for(int k=0; k<colL; k++){
  5. res[i][j]+= arrL[i][k]*arrR[k][j];}}}return res;}
  1. 矩阵转置;
  1. // 矩阵转置template<typenameT>
  2. vector<vector<T>>transpose(vector<vector<T>>const arr){int row = arr.size();int col = arr[0].size();// initialize transpose matrix col*row
  3. vector<vector<T>>trans(col);for(int i=0;i<col;i++){
  4. trans[i].resize(row);}// fill elementsfor(int i=0; i<col;i++){for(int j=0;j<row;j++){
  5. trans[i][j]= arr[j][i];}}return trans;}
  1. 实对称矩阵特征值特征向量求解(选用Jacobi迭代法)
  1. //提前声明后续用到的argsort函数,功能类似于numpy的那个template<typenameT>
  2. vector<int>argsort(const vector<T>& array);// 实对称矩阵特征值特征向量// param: arr :input array// param: E :eigen vectors// param: e :eigen valuestemplate<typenameT>voideigen(vector<vector<T>> arr, vector<vector<T>>&E, vector<T>&e){//vector<vector<T>> arr = arr_ori;int n = arr.size();// size of matrixint row =0;// row index of maxint col =0;// col index of maxint iter_max_num =10000;//迭代总次数int iter_num =0;double eps =1e-40;//误差double max = eps;// 非对角元素最大值// 初始化特征向量矩阵为单位阵,初始化特征值
  3. E.resize(n);
  4. e.resize(n);for(int i=0; i<n; i++){
  5. E[i].resize(n,0);
  6. E[i][i]=1;}while(iter_num<iter_max_num && max>=eps){
  7. max =fabs(arr[0][1]);
  8. row =0;
  9. col =1;// find max value and indexfor(int i=0;i<n;i++){for(int j=0;j<n;j++){if(i!=j &&fabs(arr[i][j])>max){
  10. max =fabs(arr[i][j]);
  11. row = i;
  12. col = j;}}}double theta =0.5*atan2(2* arr[row][col],(arr[row][row]- arr[col][col]));//update arrdouble aii = arr[row][row];double ajj = arr[col][col];double aij = arr[row][col];double sin_theta =sin(theta);double cos_theta =cos(theta);double sin_2theta =sin(2* theta);double cos_2theta =cos(2* theta);
  13. arr[row][row]= aii*cos_theta*cos_theta + ajj*sin_theta*sin_theta + aij*sin_2theta;//Sii'
  14. arr[col][col]= aii*sin_theta*sin_theta + ajj*cos_theta*cos_theta - aij*sin_2theta;//Sjj'
  15. arr[row][col]=0.5*(ajj - aii)*sin_2theta + aij*cos_2theta;//Sij'
  16. arr[col][row]= arr[row][col];//Sji'for(int k =0; k < n; k++){if(k != row && k != col){double arowk = arr[row][k];double acolk = arr[col][k];
  17. arr[row][k]= arowk * cos_theta + acolk * sin_theta;
  18. arr[k][row]= arr[row][k];
  19. arr[col][k]= acolk * cos_theta - arowk * sin_theta;
  20. arr[k][col]= arr[col][k];}}// update Edouble Eki;double Ekj;for(int k=0; k<n; k++){
  21. Eki = E[k][row];
  22. Ekj = E[k][col];
  23. E[k][row]= Eki*cos_theta + Ekj*sin_theta;
  24. E[k][col]= Ekj*cos_theta - Eki*sin_theta;}
  25. iter_num++;}//update efor(int i=0;i<n;i++){
  26. e[i]= arr[i][i];}// sort E by e
  27. vector<int> sort_index;
  28. sort_index =argsort(e);// initialize E_sorted, e_sorted
  29. vector<vector<T>>E_sorted(n);for(int i=0;i<n;i++){
  30. E_sorted[i].resize(n);}
  31. vector<T>e_sorted(n);for(int i=0;i<n;i++){
  32. e_sorted[i]= e[sort_index[i]];for(int j=0;j<n;j++){
  33. E_sorted[i][j]= E[i][sort_index[j]];}}
  34. E = E_sorted;
  35. e = e_sorted;//delete &E_sorted, &e_sorted;
  36. cout<<"max element is: "<<max<<", iterate: "<<iter_num<<"times"<<endl;}

在实现完上述工具后,即可实现

  1. SVD

,其构造如下:

  1. //*****************************************************////########################SVD##########################//*****************************************************////params:// --arr :input matrix m*n// --U :left matrix m*r , r <= rank(arr)// --S :medium matrix r*r // --V :right matrix n*rclassSVD{public:
  2. vector<vector<double>> U,S,V,ATA,A;int n,m,r;
  3. vector<vector<double>> E;//特征向量矩阵
  4. vector<double> e;// 特征值向量SVD(vector<vector<double>> arr);voidtight_svd();//紧奇异值分解voidtruncated_svd(int);//截断奇异值分解};SVD::SVD(vector<vector<double>> arr){
  5. m = arr.size();
  6. n = arr[0].size();
  7. A = arr;
  8. ATA =matrix_multiply(transpose(A),A);// 计算ATA特征值特征向量eigen(ATA,E,e);}voidSVD::tight_svd(){
  9. r =0;// 确定秩for(int i=0;i<e.size();i++){if(e[i]>1e-10){
  10. r++;}elsebreak;}//确定V
  11. V = E;for(int i=0; i<n;i++){
  12. V[i].resize(r);}//确定S
  13. S.resize(r);for(int i=0;i<r;i++){
  14. S[i].resize(r);
  15. S[i][i]=sqrt(e[i]);}//确定U
  16. vector<vector<double>> Sinv = S;for(int i=0;i<r;i++){
  17. Sinv[i][i]=1/S[i][i];}
  18. U =matrix_multiply(matrix_multiply(A,V),Sinv);}voidSVD::truncated_svd(int rr){
  19. r = rr;//确定V
  20. V = E;for(int i=0; i<n;i++){
  21. V[i].resize(r);}//确定S
  22. S.resize(r);for(int i=0;i<r;i++){
  23. S[i].resize(r);
  24. S[i][i]=sqrt(e[i]);}//确定U
  25. vector<vector<double>> Sinv = S;for(int i=0;i<r;i++){
  26. Sinv[i][i]=1/S[i][i];}
  27. U =matrix_multiply(matrix_multiply(A,V),Sinv);}

实验测试

以《统计机器学习》P277页例15.2为例
验证实例

代码如下:

  1. intmain(){
  2. vector<vector<double>> A ={{1,0,0,0},{0,0,0,4},{0,3,0,0},{0,0,0,0},{2,0,0,0}};
  3. SVD svd(A);// tight SVD
  4. svd.tight_svd();
  5. cout<<endl;
  6. cout<<"matrix A:"<<endl;display_matrix(A);
  7. cout<<endl;
  8. cout<<"matrix U:"<<endl;display_matrix(svd.U);
  9. cout<<endl;
  10. cout<<"matrix Sigma:"<<endl;display_matrix(svd.S);
  11. cout<<endl;
  12. cout<<"matrix V':"<<endl;display_matrix(transpose(svd.V));//display_matrix(matrix_multiply(matrix_multiply(svd.U,svd.S),transpose(svd.V)));//合成A}

测试结果
测试结果
完整代码见我的GitHub仓库:https://github.com/wjtgoo/SVD-CPP(万水千山总是情,给个星星行不行/(ㄒoㄒ)/~~)

标签: c++ 线性代数 矩阵

本文转载自: https://blog.csdn.net/weixin_45804601/article/details/125237191
版权归原作者 taotaoiit 所有, 如有侵权,请联系我们删除。

“SVD 奇异值分解纯手工实现(C++)”的评论:

还没有评论