hanalyze-core-0.2.0.1: src/Hanalyze/Math/ICA.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE BangPatterns #-}
-- |
-- Module : Hanalyze.Math.ICA
-- Description : FastICA (Hyvärinen 1999) による独立成分分析 (whitening + fixed-point iteration)
-- Copyright : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License : BSD-3-Clause
--
-- [日本語]: FastICA (Hyvärinen 1999) による独立成分分析。
--
-- 観測 X = A · S (n_samples × p)、 S が互いに独立な非ガウシアン成分のとき、
-- A を推定して S = A⁻¹ · X を抽出する。 ICA-LiNGAM (Shimizu 2006) の前段
-- および信号分離一般に使う。
--
-- ## アルゴリズム
--
-- 1. __Centering__: X の各列を中心化
-- 2. __Whitening__: X の covariance を eigen 分解して
-- @Z = E · D^(-1/2) · Eᵀ · X@ を作る (Z の cov = I)
-- 3. __Fixed-point iteration__ (per component): 任意の w から始めて
-- @w⁺ = E[Z · g(wᵀZ)] - E[g'(wᵀZ)] · w@、 正規化、 直交化 (デフレーション)、
-- 収束 (|wᵀwᵒˡᵈ| ≈ 1) まで繰返し
-- 4. __回収__: 全成分の row 構成 W に対し、 S = W · Z、 A = pinv(W) (whitened
-- 座標から元座標への戻し変換は別途)
--
-- non-linearity g としては logcosh (Hyvärinen 標準) を採用:
-- g(u) = tanh(a·u)、 g'(u) = a·(1 - tanh²(a·u))、 a = 1.0
--
-- ## 出力
--
-- 'ICAResult' は分離行列 W (p × p, whitened 座標)、 mixing 行列 A (元座標、
-- W · whiten から逆算)、 推定独立成分 S (n × p)、 収束情報を持つ。
--
-- [English]: Independent component analysis via FastICA (Hyvärinen 1999).
--
-- Given the observation X = A · S (n_samples × p), where S consists of
-- mutually independent, non-Gaussian components, this estimates A and
-- extracts S = A⁻¹ · X. Used as a preprocessing step for ICA-LiNGAM
-- (Shimizu 2006) and for signal separation in general.
--
-- ## Algorithm
--
-- 1. __Centering__: center each column of X.
-- 2. __Whitening__: eigen-decompose the covariance of X to form
-- @Z = E · D^(-1/2) · Eᵀ · X@ (the covariance of Z is I).
-- 3. __Fixed-point iteration__ (per component): starting from an
-- arbitrary w, repeatedly apply @w⁺ = E[Z · g(wᵀZ)] - E[g'(wᵀZ)] · w@,
-- normalize, deflate (orthogonalize), until convergence
-- (|wᵀwᵒˡᵈ| ≈ 1).
-- 4. __Recovery__: with W formed from all components' rows, S = W · Z,
-- A = pinv(W) (the transform back from whitened to original
-- coordinates is separate).
--
-- The non-linearity g used is logcosh (Hyvärinen's standard choice):
-- g(u) = tanh(a·u), g'(u) = a·(1 - tanh²(a·u)), a = 1.0.
--
-- ## Output
--
-- 'ICAResult' holds the separation matrix W (p × p, whitened
-- coordinates), the mixing matrix A (original coordinates, back-computed
-- from W · whiten), the estimated independent components S (n × p), and
-- convergence information.
module Hanalyze.Math.ICA
( ICAConfig (..)
, ICAResult (..)
, defaultICAConfig
, fitICA
, fitICAGen
, fitICAPure
) where
import qualified Numeric.LinearAlgebra as LA
import qualified Data.Vector as V
import qualified System.Random.MWC as MWC
import Control.Monad (forM_, when)
import Control.Monad.Primitive (PrimMonad, PrimState)
import Control.Monad.ST (runST)
import Data.Primitive.MutVar (newMutVar, readMutVar, writeMutVar)
import System.Random.MWC.Distributions (standard)
-- ===========================================================================
-- 設定
-- ===========================================================================
data ICAConfig = ICAConfig
{ icaMaxIter :: !Int
, icaTol :: !Double
, icaNumComp :: !(Maybe Int)
-- ^ [日本語]: 抽出する成分数。 'Nothing' で全成分 (= p)。
-- [English]: The number of components to extract. 'Nothing' means
-- all components (= p).
, icaSeed :: !(Maybe Int)
} deriving (Show)
defaultICAConfig :: ICAConfig
defaultICAConfig = ICAConfig
{ icaMaxIter = 200
, icaTol = 1e-4
, icaNumComp = Nothing
, icaSeed = Just 12345
}
data ICAResult = ICAResult
{ icaW :: !(LA.Matrix Double)
-- ^ [日本語]: whitened 空間での分離行列 (p × p)。
-- [English]: The separation matrix in whitened space (p × p).
, icaA :: !(LA.Matrix Double)
-- ^ [日本語]: 元 X 空間における推定 mixing 行列。 X ≈ S · Aᵀ + mean。
-- [English]: The estimated mixing matrix in the original X space.
-- X ≈ S · Aᵀ + mean.
, icaUnmixing :: !(LA.Matrix Double)
-- ^ [日本語]: 元 X 空間における分離行列 (S = (X - mean) · unmixingᵀ)。
-- [English]: The separation matrix in the original X space
-- (S = (X - mean) · unmixingᵀ).
, icaS :: !(LA.Matrix Double)
-- ^ [日本語]: 推定独立成分 (n × k)。
-- [English]: The estimated independent components (n × k).
, icaMean :: !(LA.Vector Double)
-- ^ [日本語]: 列平均 (centering 用)。
-- [English]: Column means (for centering).
, icaConverged :: !Bool
, icaIterations :: !Int
} deriving (Show)
-- ===========================================================================
-- 主実装
-- ===========================================================================
-- | [日本語]: FastICA 本体 (PrimMonad へ一般化済)。 Gen を受け取り ST/IO いずれでも動く
-- (IORef→MutVar)。 'fitICA' (IO) / @fitICAPure@ (ST・seed) が gen を作って呼ぶ。
-- [English]: The core FastICA implementation (generalized to
-- 'PrimMonad'). Takes a Gen and works under either ST or IO
-- (IORef→MutVar). 'fitICA' (IO) \/ @fitICAPure@ (ST, seeded) construct
-- the gen and call this.
fitICAGen :: PrimMonad m => ICAConfig -> LA.Matrix Double -> MWC.Gen (PrimState m) -> m ICAResult
fitICAGen cfg x gen = do
let !n = LA.rows x
!p = LA.cols x
!k = maybe p id (icaNumComp cfg)
-- centering
means = LA.fromList
[ LA.sumElements (x LA.¿ [j]) / fromIntegral n
| j <- [0 .. p - 1] ]
meanMat = LA.fromRows (replicate n means)
xc = x - meanMat
-- whitening: Z = E D^(-1/2) Eᵀ · Xᵀ をしたいが、 hmatrix は行ベクトル
-- 規約なので、 共分散行列を求めて eigen 分解する
cov = (LA.tr xc LA.<> xc) / fromIntegral n
(d, e) = LA.eigSH (LA.trustSym cov)
-- d : Vector Double, e : Matrix Double (columns are eigenvectors)
dInvSqrt = LA.cmap (\v -> if v > 1e-12 then 1 / sqrt v else 0) d
whitenMat = e LA.<> LA.diag dInvSqrt LA.<> LA.tr e -- (p × p)
z = xc LA.<> LA.tr whitenMat -- (n × p)
-- FastICA loop (deflation) — p × p の分離行列 W を 1 行ずつ確定。 gen は引数。
wRowsRef <- newMutVar ([] :: [LA.Vector Double])
itersRef <- newMutVar (0 :: Int)
convRef <- newMutVar True
forM_ [0 .. k - 1] $ \_compIdx -> do
-- 初期 w を gauss 乱数で
w0Raw <- V.replicateM p (standard gen)
let w0 = LA.fromList (V.toList w0Raw)
wsExisting <- readMutVar wRowsRef
-- 既存成分への直交化
let w0Ortho = deflate wsExisting w0
w0Norm = LA.scale (1 / LA.norm_2 w0Ortho) w0Ortho
-- fixed point iteration
wRef <- newMutVar w0Norm
convergedThisRef <- newMutVar False
forM_ [1 .. icaMaxIter cfg] $ \iter -> do
wOld <- readMutVar wRef
isC <- readMutVar convergedThisRef
when (not isC) $ do
let wu = z LA.#> wOld -- (n,)
gWu = LA.cmap tanh wu
gpWu = LA.cmap (\v -> 1 - tanh v ** 2) wu
wNew0 = LA.tr z LA.#> gWu / LA.scalar (fromIntegral n)
- LA.scale (LA.sumElements gpWu / fromIntegral n) wOld
wDef = deflate wsExisting wNew0
wNew = LA.scale (1 / LA.norm_2 wDef) wDef
!diff = abs (abs (wNew `LA.dot` wOld) - 1)
writeMutVar wRef wNew
writeMutVar itersRef iter
when (diff < icaTol cfg) $ writeMutVar convergedThisRef True
finalConv <- readMutVar convergedThisRef
when (not finalConv) $ writeMutVar convRef False
wFinal <- readMutVar wRef
writeMutVar wRowsRef (wsExisting ++ [wFinal])
ws <- readMutVar wRowsRef
let !wMat = LA.fromRows ws -- (k × p)、 whitened 空間
!sMat = z LA.<> LA.tr wMat -- (n × k)、 独立成分
-- 元 X 空間: unmixing = wMat · whitenMat (k × p)
!unmixing = wMat LA.<> whitenMat
-- mixing = pseudo-inverse of unmixing (p × k)
!mixing = LA.pinv unmixing
iters <- readMutVar itersRef
conv <- readMutVar convRef
pure ICAResult
{ icaW = wMat
, icaA = mixing
, icaUnmixing = unmixing
, icaS = sMat
, icaMean = means
, icaConverged = conv
, icaIterations = iters
}
where
deflate :: [LA.Vector Double] -> LA.Vector Double -> LA.Vector Double
deflate ws w = foldl (\acc wi -> acc - LA.scale (acc `LA.dot` wi) wi) w ws
-- | [日本語]: FastICA (IO)。 'icaSeed' が 'Just' なら決定的、 'Nothing' で system random。
-- [English]: FastICA (IO). Deterministic when 'icaSeed' is 'Just';
-- uses the system random source when 'Nothing'.
fitICA :: ICAConfig -> LA.Matrix Double -> IO ICAResult
fitICA cfg x = do
gen <- case icaSeed cfg of
Just s -> MWC.initialize (V.fromList [fromIntegral s])
Nothing -> MWC.createSystemRandom
fitICAGen cfg x gen
-- | [日本語]: FastICA の __seed 純粋版__ (@df |->@ 用)。 'icaSeed' (既定 12345・'Nothing' は
-- 12345 fallback) で 'runST'+MWC。 同 seed で IO 版とビット一致 (乱数列は monad 非依存)。
-- [English]: The __seeded pure version__ of FastICA (for @df |->@).
-- Uses 'runST'+MWC with 'icaSeed' (default 12345; 'Nothing' falls back
-- to 12345). Bit-identical to the IO version for the same seed (the
-- random sequence is monad-independent).
fitICAPure :: ICAConfig -> LA.Matrix Double -> ICAResult
fitICAPure cfg x = runST $ do
gen <- MWC.initialize (V.fromList [fromIntegral (maybe 12345 id (icaSeed cfg))])
fitICAGen cfg x gen