|
| 1 | +function [K, W, Y] = picard_python_port(X, varargin) |
| 2 | +% PICARD_PYTHON_PORT Faithful MATLAB port of Python picard solver. |
| 3 | +% |
| 4 | +% Replicates Python's picard v0.8.2 solver.py + _core_picard.py exactly: |
| 5 | +% - Centering: subtract row means |
| 6 | +% - Whitening: SVD on data (not covariance), K = (u/d)' * sqrt(T) |
| 7 | +% - w_init: identity (deterministic) |
| 8 | +% - Core loop: L-BFGS with Tanh density (alpha=1) |
| 9 | +% - Standard (non-ortho) Picard |
| 10 | +% |
| 11 | +% Usage: |
| 12 | +% [K, W, Y] = picard_python_port(X) |
| 13 | +% [K, W, Y] = picard_python_port(X, 'max_iter', 512, 'tol', 1e-7, ...) |
| 14 | +% |
| 15 | +% Returns: |
| 16 | +% K - whitening matrix [N x N] |
| 17 | +% W - unmixing in whitened space [N x N] |
| 18 | +% Y - estimated sources [N x T] |
| 19 | +% |
| 20 | +% Full unmixing: icaweights = W * K |
| 21 | + |
| 22 | +% Defaults matching Python |
| 23 | +opt.max_iter = 512; |
| 24 | +opt.tol = 1e-7; |
| 25 | +opt.m = 10; |
| 26 | +opt.lambda_min = 0.01; |
| 27 | +opt.ls_tries = 10; |
| 28 | +opt.verbose = true; |
| 29 | +opt.centering = true; |
| 30 | + |
| 31 | +% Parse varargin |
| 32 | +for i = 1:2:length(varargin) |
| 33 | + opt.(lower(varargin{i})) = varargin{i+1}; |
| 34 | +end |
| 35 | + |
| 36 | +X = double(X); |
| 37 | +[N, T] = size(X); |
| 38 | + |
| 39 | +% --- Step 1: Centering (solver.py line 170) --- |
| 40 | +if opt.centering |
| 41 | + X_mean = mean(X, 2); |
| 42 | + X = X - repmat(X_mean, 1, T); |
| 43 | +end |
| 44 | + |
| 45 | +% --- Step 2: Whitening via SVD on data (solver.py lines 172-177) --- |
| 46 | +[u, d_mat, ~] = svd(X, 'econ'); |
| 47 | +d = diag(d_mat); |
| 48 | +K = diag(sqrt(T) ./ d) * u'; |
| 49 | +K = K(1:N, :); |
| 50 | +% enforce fixed-sign for consistency (solver.py lines 178-182, v0.8.2) |
| 51 | +% Flip each row of K so max-abs element is positive. |
| 52 | +for row = 1:size(K, 1) |
| 53 | + [~, j] = max(abs(K(row, :))); |
| 54 | + if K(row, j) < 0 |
| 55 | + K(row, :) = -K(row, :); |
| 56 | + end |
| 57 | +end |
| 58 | +X = K * X; |
| 59 | + |
| 60 | +% --- Step 3: Apply w_init = I (solver.py line 199) --- |
| 61 | +w_init = eye(N); |
| 62 | +X = w_init * X; |
| 63 | + |
| 64 | +% --- Step 4: Core Picard (_core_picard.py) --- |
| 65 | +% Tanh density with alpha=1: score=tanh(Y), der=1-tanh(Y)^2 |
| 66 | + |
| 67 | +W_algo = eye(N); |
| 68 | +Y = X; |
| 69 | +s_list = {}; |
| 70 | +y_list = {}; |
| 71 | +r_list = {}; |
| 72 | +current_loss = compute_loss(Y, W_algo); |
| 73 | +G_old = []; |
| 74 | + |
| 75 | +for n_iter = 1:opt.max_iter |
| 76 | + % Score function: Tanh (alpha=1) |
| 77 | + psiY = tanh(Y); |
| 78 | + psidY = 1 - psiY.^2; |
| 79 | + |
| 80 | + % Relative gradient (_core_picard.py line 90) |
| 81 | + % np.inner(psiY, Y) for 2D = psiY @ Y.T |
| 82 | + G = (psiY * Y') / T; |
| 83 | + |
| 84 | + % h_off for non-ortho (_core_picard.py line 111) |
| 85 | + h_off = ones(N, 1); |
| 86 | + |
| 87 | + % Hessian diagonal (_core_picard.py line 119) |
| 88 | + Y_square = Y.^2; |
| 89 | + h = (psidY * Y_square') / T; |
| 90 | + |
| 91 | + % Regularize hessian (_core_picard.py line 236-242) |
| 92 | + h = regularize_hessian(h, h_off, opt.lambda_min); |
| 93 | + |
| 94 | + % Subtract identity for non-ortho (_core_picard.py line 126) |
| 95 | + G = G - eye(N); |
| 96 | + |
| 97 | + % Stopping criterion (_core_picard.py line 128) |
| 98 | + gradient_norm = max(abs(G(:))); |
| 99 | + if gradient_norm < opt.tol |
| 100 | + if opt.verbose |
| 101 | + fprintf('Converged at iteration %d, gradient norm = %.6g\n', n_iter, gradient_norm); |
| 102 | + end |
| 103 | + break |
| 104 | + end |
| 105 | + |
| 106 | + % Update L-BFGS memory (_core_picard.py lines 131-138) |
| 107 | + if n_iter > 1 |
| 108 | + s_list{end+1} = direction; |
| 109 | + y_diff = G - G_old; |
| 110 | + y_list{end+1} = y_diff; |
| 111 | + r_list{end+1} = 1.0 / sum(sum(direction .* y_diff)); |
| 112 | + if length(s_list) > opt.m |
| 113 | + s_list = s_list(2:end); |
| 114 | + y_list = y_list(2:end); |
| 115 | + r_list = r_list(2:end); |
| 116 | + end |
| 117 | + end |
| 118 | + G_old = G; |
| 119 | + |
| 120 | + % L-BFGS direction (_core_picard.py line 145) |
| 121 | + direction = lbfgs_direction(G, h, h_off, s_list, y_list, r_list); |
| 122 | + |
| 123 | + % Line search (_core_picard.py line 148) |
| 124 | + [converged, new_Y, new_W, new_loss, direction] = ... |
| 125 | + line_search_fn(Y, W_algo, direction, current_loss, opt.ls_tries, opt.verbose); |
| 126 | + |
| 127 | + if ~converged |
| 128 | + direction = -G; |
| 129 | + s_list = {}; |
| 130 | + y_list = {}; |
| 131 | + r_list = {}; |
| 132 | + [~, new_Y, new_W, new_loss, direction] = ... |
| 133 | + line_search_fn(Y, W_algo, direction, current_loss, 10, false); |
| 134 | + end |
| 135 | + |
| 136 | + Y = new_Y; |
| 137 | + W_algo = new_W; |
| 138 | + current_loss = new_loss; |
| 139 | + |
| 140 | + if opt.verbose |
| 141 | + fprintf('iteration %d, gradient norm = %.4g, loss = %.4g\n', ... |
| 142 | + n_iter, gradient_norm, current_loss); |
| 143 | + end |
| 144 | +end |
| 145 | + |
| 146 | +% Final W: compose with w_init (_core_picard.py line 209 in solver.py) |
| 147 | +W = W_algo * w_init; |
| 148 | + |
| 149 | +end % picard_python_port |
| 150 | + |
| 151 | + |
| 152 | +% ===== Nested functions ===== |
| 153 | + |
| 154 | +function loss_val = compute_loss(Y, W) |
| 155 | + % Loss for standard (non-ortho) Picard with Tanh density (alpha=1) |
| 156 | + % _core_picard.py _loss function |
| 157 | + N = size(Y, 1); |
| 158 | + % -log|det(W)| |
| 159 | + loss_val = -log(abs(det(W))); |
| 160 | + % Tanh log_lik: |y| + log1p(exp(-2|y|)) |
| 161 | + for k = 1:N |
| 162 | + y = Y(k, :); |
| 163 | + loss_val = loss_val + mean(abs(y) + log1p(exp(-2 * abs(y)))); |
| 164 | + end |
| 165 | +end |
| 166 | + |
| 167 | + |
| 168 | +function h = regularize_hessian(h, h_off, lambda_min) |
| 169 | + % _core_picard.py _regularize_hessian |
| 170 | + N = size(h, 1); |
| 171 | + discr = sqrt((h - h').^2 + 4.0 * (h_off * h_off')); |
| 172 | + eigenvalues = 0.5 * (h + h' - discr); |
| 173 | + problematic_locs = eigenvalues < lambda_min; |
| 174 | + problematic_locs(1:(N+1):N*N) = false; % exclude diagonal |
| 175 | + [i_pb, j_pb] = find(problematic_locs); |
| 176 | + for idx = 1:length(i_pb) |
| 177 | + h(i_pb(idx), j_pb(idx)) = h(i_pb(idx), j_pb(idx)) + ... |
| 178 | + lambda_min - eigenvalues(i_pb(idx), j_pb(idx)); |
| 179 | + end |
| 180 | +end |
| 181 | + |
| 182 | + |
| 183 | +function out = solve_hessian(h, h_off, G) |
| 184 | + % _core_picard.py _solve_hessian |
| 185 | + det_val = h .* h' - (h_off * h_off'); |
| 186 | + out = (h' .* G - (h_off * ones(1, size(G, 2))) .* G') ./ det_val; |
| 187 | +end |
| 188 | + |
| 189 | + |
| 190 | +function direction = lbfgs_direction(G, h, h_off, s_list, y_list, r_list) |
| 191 | + % _core_picard.py _l_bfgs_direction (non-ortho branch) |
| 192 | + q = G; |
| 193 | + a_list = {}; |
| 194 | + for ii = 1:length(s_list) |
| 195 | + s = s_list{end - ii + 1}; |
| 196 | + y = y_list{end - ii + 1}; |
| 197 | + r = r_list{end - ii + 1}; |
| 198 | + alpha = r * sum(sum(s .* q)); |
| 199 | + a_list{end+1} = alpha; |
| 200 | + q = q - alpha * y; |
| 201 | + end |
| 202 | + % Solve hessian (non-ortho) |
| 203 | + z = solve_hessian(h, h_off, q); |
| 204 | + for ii = 1:length(s_list) |
| 205 | + s = s_list{ii}; |
| 206 | + y = y_list{ii}; |
| 207 | + r = r_list{ii}; |
| 208 | + alpha = a_list{end - ii + 1}; |
| 209 | + beta = r * sum(sum(y .* z)); |
| 210 | + z = z + (alpha - beta) * s; |
| 211 | + end |
| 212 | + direction = -z; |
| 213 | +end |
| 214 | + |
| 215 | + |
| 216 | +function [converged, Y_new, W_new, new_loss, rel_step] = ... |
| 217 | + line_search_fn(Y, W, direction, current_loss, ls_tries, verbose) |
| 218 | + % _core_picard.py _line_search (non-ortho branch, v0.8.2) |
| 219 | + N = size(W, 1); |
| 220 | + alpha = 1.0; |
| 221 | + for tmp = 1:ls_tries |
| 222 | + transform = eye(N) + alpha * direction; |
| 223 | + Y_new = transform * Y; |
| 224 | + W_new = transform * W; |
| 225 | + new_loss = compute_loss(Y_new, W_new); |
| 226 | + if isfinite(new_loss) && new_loss < current_loss |
| 227 | + converged = true; |
| 228 | + rel_step = alpha * direction; |
| 229 | + return |
| 230 | + end |
| 231 | + alpha = alpha / 2.0; |
| 232 | + end |
| 233 | + if verbose |
| 234 | + fprintf('line search failed, falling back to gradient.\n'); |
| 235 | + end |
| 236 | + converged = false; |
| 237 | + rel_step = alpha * direction; |
| 238 | +end |
0 commit comments