v2.0.0
Loading...
Searching...
No Matches
picard_ica.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "picard_ica.h"
18
19//=============================================================================================================
20// EIGEN INCLUDES
21//=============================================================================================================
22
23#include <Eigen/Dense>
24
25//=============================================================================================================
26// QT INCLUDES
27//=============================================================================================================
28
29#include <QDebug>
30
31//=============================================================================================================
32// STD INCLUDES
33//=============================================================================================================
34
35#include <cmath>
36#include <random>
37
38//=============================================================================================================
39// USED NAMESPACES
40//=============================================================================================================
41
42using namespace UTILSLIB;
43using namespace Eigen;
44
45//=============================================================================================================
46// STATIC HELPERS
47//=============================================================================================================
48
49namespace {
50
51// Log-cosh nonlinearity g(u) = tanh(u), g'(u) = 1 - tanh²(u)
52inline void logcoshNonlinearity(const VectorXd& u, VectorXd& g, double& gPrimeMean, int nSamples)
53{
54 g.resize(u.size());
55 double gPrimeSum = 0.0;
56 for (int i = 0; i < u.size(); ++i) {
57 double t = std::tanh(u[i]);
58 g[i] = t;
59 gPrimeSum += (1.0 - t * t);
60 }
61 gPrimeMean = gPrimeSum / static_cast<double>(nSamples);
62}
63
64} // anonymous namespace
65
66//=============================================================================================================
67// DEFINE MEMBER METHODS
68//=============================================================================================================
69
70IcaResult PicardIca::run(const MatrixXd& matData,
71 int nComponents,
72 int maxIter,
73 double tol,
74 int lbfgsMemory,
75 int randomSeed)
76{
77 IcaResult result;
78 result.bConverged = false;
79
80 const int nCh = static_cast<int>(matData.rows());
81 const int nSamples = static_cast<int>(matData.cols());
82
83 if (nComponents <= 0 || nComponents > nCh)
84 nComponents = nCh;
85
86 if (nCh < 2 || nSamples < 2) {
87 qWarning() << "[PicardIca::run] Insufficient data dimensions.";
88 return result;
89 }
90
91 // --- 1. Center the data ---
92 result.vecMean = matData.rowwise().mean();
93 MatrixXd X = matData.colwise() - result.vecMean;
94
95 // --- 2. Whiten via PCA (keep nComponents) ---
96 MatrixXd cov = (X * X.transpose()) / static_cast<double>(nSamples - 1);
97 SelfAdjointEigenSolver<MatrixXd> eig(cov);
98 if (eig.info() != Success) {
99 qWarning() << "[PicardIca::run] Eigendecomposition failed.";
100 return result;
101 }
102
103 // Eigenvalues/vectors in ascending order; we want largest first
104 VectorXd eigenvalues = eig.eigenvalues().reverse();
105 MatrixXd eigenvectors = eig.eigenvectors().rowwise().reverse();
106
107 // Keep top nComponents
108 VectorXd D = eigenvalues.head(nComponents);
109 MatrixXd V = eigenvectors.leftCols(nComponents);
110
111 // Whitening matrix: K = D^(-1/2) * V'
112 VectorXd Dinvsqrt = D.array().max(1e-15).sqrt().inverse().matrix();
113 MatrixXd K = Dinvsqrt.asDiagonal() * V.transpose(); // (nComp x nCh)
114 MatrixXd Kinv = V * D.array().sqrt().matrix().asDiagonal(); // (nCh x nComp) — dewhitening
115
116 MatrixXd Xw = K * X; // (nComp x nSamples) whitened data
117
118 // --- 3. Initialise the unmixing matrix W as random orthogonal ---
119 std::mt19937 gen(static_cast<unsigned>(randomSeed));
120 std::normal_distribution<double> dist(0.0, 1.0);
121
122 MatrixXd W(nComponents, nComponents);
123 for (int i = 0; i < nComponents; ++i)
124 for (int j = 0; j < nComponents; ++j)
125 W(i, j) = dist(gen);
126
127 // Orthogonalise W via QR
128 HouseholderQR<MatrixXd> qr(W);
129 W = qr.householderQ() * MatrixXd::Identity(nComponents, nComponents);
130
131 // --- 4. Preconditioned ICA iterations (Picard-style) ---
132 // Use approximate Newton updates: w_new = E[x*g(w'x)] - E[g'(w'x)]*w
133 // Preconditioned by the diagonal Hessian approximation
134
135 for (int iter = 0; iter < maxIter; ++iter) {
136 MatrixXd Wnew(nComponents, nComponents);
137
138 for (int k = 0; k < nComponents; ++k) {
139 // Current source
140 VectorXd yk = (W.row(k) * Xw).transpose(); // (nSamples)
141
142 // Nonlinearity
143 VectorXd gk;
144 double gPrimeMean;
145 logcoshNonlinearity(yk, gk, gPrimeMean, nSamples);
146
147 // FastICA-style Newton update (preconditioned by gPrimeMean)
148 // w_new = E[x * g(w'x)] - E[g'(w'x)] * w
149 VectorXd wNew = (Xw * gk / static_cast<double>(nSamples))
150 - gPrimeMean * W.row(k).transpose();
151
152 Wnew.row(k) = wNew.transpose();
153 }
154
155 // Symmetric orthogonalisation of Wnew
156 SelfAdjointEigenSolver<MatrixXd> eigW(Wnew * Wnew.transpose());
157 MatrixXd sqrtInv = eigW.eigenvectors()
158 * eigW.eigenvalues().array().max(1e-15).rsqrt().matrix().asDiagonal()
159 * eigW.eigenvectors().transpose();
160 Wnew = sqrtInv * Wnew;
161
162 // Check convergence: max change in W rows
163 double maxChange = 0.0;
164 for (int k = 0; k < nComponents; ++k) {
165 double dot = std::abs(Wnew.row(k).dot(W.row(k)));
166 dot = std::min(dot, 1.0);
167 maxChange = std::max(maxChange, 1.0 - dot);
168 }
169
170 W = Wnew;
171
172 if (maxChange < tol) {
173 result.bConverged = true;
174 break;
175 }
176 }
177
178 // --- 5. Build final matrices ---
179 // Unmixing: W_total = W * K (nComp x nCh)
180 result.matUnmixing = W * K;
181
182 // Mixing: A = Kinv * W^-1 (nCh x nComp)
183 // For orthogonal W: W^-1 = W^T
184 result.matMixing = Kinv * W.transpose();
185
186 // Sources
187 result.matSources = result.matUnmixing * (matData.colwise() - result.vecMean);
188
189 return result;
190}
constexpr int X
Declaration of the PicardIca class — Preconditioned ICA for Real Data.
Shared utilities (I/O helpers, spectral analysis, layout management, warp algorithms).
Result of an ICA decomposition.
Definition ica.h:67
Eigen::MatrixXd matUnmixing
Definition ica.h:69
Eigen::MatrixXd matSources
Definition ica.h:70
Eigen::MatrixXd matMixing
Definition ica.h:68
Eigen::VectorXd vecMean
Definition ica.h:71
bool bConverged
Definition ica.h:72
static IcaResult run(const Eigen::MatrixXd &matData, int nComponents=-1, int maxIter=200, double tol=1e-7, int lbfgsMemory=7, int randomSeed=42)