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
52// Log-cosh nonlinearity g(u) = tanh(u), g'(u) = 1 - tanh²(u)
53inline void logcoshNonlinearity(const VectorXd& u, VectorXd& g, double& gPrimeMean, int nSamples)
54{
55 g.resize(u.size());
56 double gPrimeSum = 0.0;
57 for (int i = 0; i < u.size(); ++i) {
58 double t = std::tanh(u[i]);
59 g[i] = t;
60 gPrimeSum += (1.0 - t * t);
61 }
62 gPrimeMean = gPrimeSum / static_cast<double>(nSamples);
63}
64
65} // anonymous namespace
66
67//=============================================================================================================
68// DEFINE MEMBER METHODS
69//=============================================================================================================
70
71IcaResult PicardIca::run(const MatrixXd& matData,
72 int nComponents,
73 int maxIter,
74 double tol,
75 [[maybe_unused]] int lbfgsMemory,
76 int randomSeed)
77{
78 IcaResult result;
79 result.bConverged = false;
80
81 const int nCh = static_cast<int>(matData.rows());
82 const int nSamples = static_cast<int>(matData.cols());
83
84 if (nComponents <= 0 || nComponents > nCh)
85 nComponents = nCh;
86
87 if (nCh < 2 || nSamples < 2) {
88 qWarning() << "[PicardIca::run] Insufficient data dimensions.";
89 return result;
90 }
91
92 // --- 1. Center the data ---
93 result.vecMean = matData.rowwise().mean();
94 MatrixXd X = matData.colwise() - result.vecMean;
95
96 // --- 2. Whiten via PCA (keep nComponents) ---
97 MatrixXd cov = (X * X.transpose()) / static_cast<double>(nSamples - 1);
98 SelfAdjointEigenSolver<MatrixXd> eig(cov);
99 if (eig.info() != Success) {
100 qWarning() << "[PicardIca::run] Eigendecomposition failed.";
101 return result;
102 }
103
104 // Eigenvalues/vectors in ascending order; we want largest first
105 VectorXd eigenvalues = eig.eigenvalues().reverse();
106 MatrixXd eigenvectors = eig.eigenvectors().rowwise().reverse();
107
108 // Keep top nComponents
109 VectorXd D = eigenvalues.head(nComponents);
110 MatrixXd V = eigenvectors.leftCols(nComponents);
111
112 // Whitening matrix: K = D^(-1/2) * V'
113 VectorXd Dinvsqrt = D.array().max(1e-15).sqrt().inverse().matrix();
114 MatrixXd K = Dinvsqrt.asDiagonal() * V.transpose(); // (nComp x nCh)
115 MatrixXd Kinv = V * D.array().sqrt().matrix().asDiagonal(); // (nCh x nComp) — dewhitening
116
117 MatrixXd Xw = K * X; // (nComp x nSamples) whitened data
118
119 // --- 3. Initialise the unmixing matrix W as random orthogonal ---
120 std::mt19937 gen(static_cast<unsigned>(randomSeed));
121 std::normal_distribution<double> dist(0.0, 1.0);
122
123 MatrixXd W(nComponents, nComponents);
124 for (int i = 0; i < nComponents; ++i)
125 for (int j = 0; j < nComponents; ++j)
126 W(i, j) = dist(gen);
127
128 // Orthogonalise W via QR
129 HouseholderQR<MatrixXd> qr(W);
130 W = qr.householderQ() * MatrixXd::Identity(nComponents, nComponents);
131
132 // --- 4. Preconditioned ICA iterations (Picard-style) ---
133 // Use approximate Newton updates: w_new = E[x*g(w'x)] - E[g'(w'x)]*w
134 // Preconditioned by the diagonal Hessian approximation
135
136 for (int iter = 0; iter < maxIter; ++iter) {
137 MatrixXd Wnew(nComponents, nComponents);
138
139 for (int k = 0; k < nComponents; ++k) {
140 // Current source
141 VectorXd yk = (W.row(k) * Xw).transpose(); // (nSamples)
142
143 // Nonlinearity
144 VectorXd gk;
145 double gPrimeMean;
146 logcoshNonlinearity(yk, gk, gPrimeMean, nSamples);
147
148 // FastICA-style Newton update (preconditioned by gPrimeMean)
149 // w_new = E[x * g(w'x)] - E[g'(w'x)] * w
150 VectorXd wNew = (Xw * gk / static_cast<double>(nSamples)) - 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() * eigW.eigenvalues().array().max(1e-15).rsqrt().matrix().asDiagonal() * eigW.eigenvectors().transpose();
158 Wnew = sqrtInv * Wnew;
159
160 // Check convergence: max change in W rows
161 double maxChange = 0.0;
162 for (int k = 0; k < nComponents; ++k) {
163 double dot = std::abs(Wnew.row(k).dot(W.row(k)));
164 dot = std::min(dot, 1.0);
165 maxChange = std::max(maxChange, 1.0 - dot);
166 }
167
168 W = Wnew;
169
170 if (maxChange < tol) {
171 result.bConverged = true;
172 break;
173 }
174 }
175
176 // --- 5. Build final matrices ---
177 // Unmixing: W_total = W * K (nComp x nCh)
178 result.matUnmixing = W * K;
179
180 // Mixing: A = Kinv * W^-1 (nCh x nComp)
181 // For orthogonal W: W^-1 = W^T
182 result.matMixing = Kinv * W.transpose();
183
184 // Sources
185 result.matSources = result.matUnmixing * (matData.colwise() - result.vecMean);
186
187 return result;
188}
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:69
Eigen::MatrixXd matUnmixing
Definition ica.h:71
Eigen::MatrixXd matSources
Definition ica.h:72
Eigen::MatrixXd matMixing
Definition ica.h:70
Eigen::VectorXd vecMean
Definition ica.h:73
bool bConverged
Definition ica.h:74
static IcaResult run(const Eigen::MatrixXd &matData, int nComponents=-1, int maxIter=200, double tol=1e-7, int lbfgsMemory=7, int randomSeed=42)