v2.0.0
Loading...
Searching...
No Matches
extended_infomax.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "extended_infomax.h"
18
19//=============================================================================================================
20// EIGEN INCLUDES
21//=============================================================================================================
22
23#include <Eigen/Eigenvalues>
24
25//=============================================================================================================
26// STL INCLUDES
27//=============================================================================================================
28
29#include <random>
30#include <cmath>
31
32//=============================================================================================================
33// USED NAMESPACES
34//=============================================================================================================
35
36using namespace UTILSLIB;
37using namespace Eigen;
38
39//=============================================================================================================
40// DEFINE MEMBER METHODS
41//=============================================================================================================
42
44 const MatrixXd& matData,
45 int nComponents,
46 int maxIterations,
47 double learningRate,
48 double tolerance,
49 bool extendedMode,
50 unsigned int seed)
51{
52 const int nChannels = static_cast<int>(matData.rows());
53 const int nTimes = static_cast<int>(matData.cols());
54
55 if (nComponents < 0) {
56 nComponents = nChannels;
57 }
58
59 //=========================================================================================================
60 // Step 1: Mean removal
61 //=========================================================================================================
62 VectorXd vecMean = matData.rowwise().mean();
63 MatrixXd matCentered = matData.colwise() - vecMean;
64
65 //=========================================================================================================
66 // Step 2: PCA whitening
67 //=========================================================================================================
68 MatrixXd matCov = (matCentered * matCentered.transpose()) / static_cast<double>(nTimes - 1);
69
70 SelfAdjointEigenSolver<MatrixXd> eigSolver(matCov);
71 VectorXd vecEigVals = eigSolver.eigenvalues();
72 MatrixXd matEigVecs = eigSolver.eigenvectors();
73
74 // Eigen returns eigenvalues in ascending order; take the top nComponents (largest)
75 VectorXd vecTopEigVals = vecEigVals.tail(nComponents).reverse();
76 MatrixXd matTopEigVecs = matEigVecs.rightCols(nComponents).rowwise().reverse();
77
78 // Whitening: P = D^{-1/2} * V^T
79 VectorXd vecInvSqrtEig = vecTopEigVals.array().sqrt().inverse();
80 MatrixXd matWhitening = vecInvSqrtEig.asDiagonal() * matTopEigVecs.transpose();
81
82 // Dewhitening: P_inv = V * D^{1/2}
83 VectorXd vecSqrtEig = vecTopEigVals.array().sqrt();
84 MatrixXd matDewhitening = matTopEigVecs * vecSqrtEig.asDiagonal();
85
86 // Whitened data
87 MatrixXd matWhite = matWhitening * matCentered;
88
89 //=========================================================================================================
90 // Step 3: Initialize weights
91 //=========================================================================================================
92 MatrixXd matW = MatrixXd::Identity(nComponents, nComponents);
93
94 if (seed != 0) {
95 std::mt19937 gen(seed);
96 std::normal_distribution<double> dist(0.0, 0.01);
97 for (int i = 0; i < nComponents; ++i) {
98 for (int j = 0; j < nComponents; ++j) {
99 if (i != j) {
100 matW(i, j) = dist(gen);
101 }
102 }
103 }
104 }
105
106 //=========================================================================================================
107 // Step 4: Main iteration loop
108 //=========================================================================================================
109 InfomaxResult result;
110 result.converged = false;
111 result.nIterations = 0;
112
113 const double dInvN = 1.0 / static_cast<double>(nTimes);
114 MatrixXd matIdentity = MatrixXd::Identity(nComponents, nComponents);
115
116 // Learning rate annealing: the natural gradient has a nonzero steady-state
117 // when the assumed nonlinearity does not perfectly match the true source
118 // distribution. Geometric decay lets the step shrink toward zero so the
119 // convergence criterion can fire.
120 double dCurrentLR = learningRate;
121 constexpr double dAnnealFactor = 0.998;
122
123 for (int iter = 0; iter < maxIterations; ++iter) {
124 // Compute sources
125 MatrixXd matSources = matW * matWhite;
126
127 // Estimate sign vector for extended mode
128 VectorXd vecSigns = VectorXd::Ones(nComponents);
129 if (extendedMode) {
130 vecSigns = estimateSignVector(matSources);
131 }
132
133 // Compute nonlinearity
134 MatrixXd matY(nComponents, nTimes);
135 for (int i = 0; i < nComponents; ++i) {
136 if (vecSigns(i) > 0) {
137 // Super-Gaussian: g(u) = -tanh(u)
138 matY.row(i) = -matSources.row(i).array().tanh();
139 } else {
140 // Sub-Gaussian: g(u) = tanh(u) - u
141 matY.row(i) = matSources.row(i).array().tanh() - matSources.row(i).array();
142 }
143 }
144
145 // Natural gradient: dW = lr * (I + Y * S^T / n_times) * W
146 MatrixXd matGrad = matIdentity + (matY * matSources.transpose()) * dInvN;
147 MatrixXd matDW = dCurrentLR * matGrad * matW;
148
149 matW += matDW;
150 result.nIterations = iter + 1;
151
152 // Convergence: squared Frobenius norm of the weight update
153 // (matches MNE-Python's criterion). With learning rate annealing
154 // the update shrinks each iteration.
155 double dChange = matDW.squaredNorm();
156 if (dChange < tolerance) {
157 result.converged = true;
158 break;
159 }
160
161 dCurrentLR *= dAnnealFactor;
162 }
163
164 //=========================================================================================================
165 // Step 5: Compute output matrices
166 //=========================================================================================================
167 // Unmixing in original sensor space: W_total = W * P
168 result.matUnmixing = matW * matWhitening;
169
170 // Mixing matrix: pseudo-inverse of unmixing
171 result.matMixing = result.matUnmixing.completeOrthogonalDecomposition().pseudoInverse();
172
173 // Source activations
174 result.matSources = result.matUnmixing * matCentered;
175
176 return result;
177}
178
179//=============================================================================================================
180
181VectorXd ExtendedInfomax::estimateSignVector(const MatrixXd& matSources)
182{
183 const int nComponents = static_cast<int>(matSources.rows());
184 const int nTimes = static_cast<int>(matSources.cols());
185 const double dInvN = 1.0 / static_cast<double>(nTimes);
186
187 VectorXd vecSigns(nComponents);
188
189 for (int i = 0; i < nComponents; ++i) {
190 double dMean = matSources.row(i).mean();
191 ArrayXd arrCentered = matSources.row(i).array() - dMean;
192 double dM2 = (arrCentered.square()).sum() * dInvN;
193 double dM4 = (arrCentered.square().square()).sum() * dInvN;
194
195 // Excess kurtosis: m4/m2^2 - 3
196 double dKurtosis = (dM2 > 0.0) ? (dM4 / (dM2 * dM2)) - 3.0 : 0.0;
197
198 vecSigns(i) = (dKurtosis > 0.0) ? 1.0 : -1.0;
199 }
200
201 return vecSigns;
202}
Extended Infomax independent component analysis (super- and sub-Gaussian sources).
Shared utilities (I/O helpers, spectral analysis, layout management, warp algorithms).
Eigen::MatrixXd matUnmixing
static InfomaxResult compute(const Eigen::MatrixXd &matData, int nComponents=-1, int maxIterations=200, double learningRate=0.001, double tolerance=1e-7, bool extendedMode=true, unsigned int seed=0)