packages feed

hanalyze-0.2.0.0: src/Hanalyze/Stat/MDS.hs

{-# LANGUAGE BangPatterns #-}
-- |
-- Module      : Hanalyze.Stat.MDS
-- Description : 多次元尺度構成法 (古典 MDS / Sammon MDS)
-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License     : BSD-3-Clause
--
-- Multidimensional Scaling (MDS).
--
-- * Classical MDS (Torgerson) — 距離行列を二重中心化 → 固有分解 → 上位 k 成分。
-- * Sammon MDS — Sammon stress を勾配降下で最小化 (古典 MDS を初期値)。
--
-- @
-- import qualified Hanalyze.Stat.MDS as MDS
-- let d  = MDS.euclideanDist x                -- x :: Matrix Double (n × p)
--     emb = MDS.mdsClassical d 2              -- 2-D 埋め込み (n × 2)
-- @
module Hanalyze.Stat.MDS
  ( euclideanDist
  , mdsClassical
  , mdsSammon
  , sammonStress
  , SammonConfig (..)
  , defaultSammonConfig
  ) where

import qualified Numeric.LinearAlgebra as LA

-- ---------------------------------------------------------------------------
-- Distance matrix helper
-- ---------------------------------------------------------------------------

-- | n × p のデータ行列から n × n のユークリッド距離行列を作る。
euclideanDist :: LA.Matrix Double -> LA.Matrix Double
euclideanDist x =
  let !n = LA.rows x
      row i = LA.flatten (x LA.? [i])
      dij i j = LA.norm_2 (row i - row j)
  in LA.build (n, n) (\i j -> dij (round i) (round j))

-- ---------------------------------------------------------------------------
-- Classical MDS (Torgerson)
-- ---------------------------------------------------------------------------

-- | 距離行列 D (n × n) を k 次元埋め込み (n × k) に。
--
-- B = -1/2 · H · D² · H、 H = I - 1/n · 11ᵀ。 B = V Λ Vᵀ から
-- 正の上位 k 成分のみ抽出して X = V_k √Λ_k。
mdsClassical :: LA.Matrix Double  -- ^ 距離行列 D (n × n)。
             -> Int                -- ^ 目的次元 k。
             -> LA.Matrix Double  -- ^ 埋め込み (n × k)。
mdsClassical d k =
  let !n   = LA.rows d
      !d2  = d * d
      ones = LA.konst 1 (n, n) :: LA.Matrix Double
      h    = LA.ident n - LA.scale (1 / fromIntegral n) ones
      b    = LA.scale (-0.5) (h LA.<> d2 LA.<> h)
      -- 対称化 (数値誤差吸収)
      bSym = LA.scale 0.5 (b + LA.tr b)
      (eigVals, eigVecs) = LA.eigSH (LA.trustSym bSym)
      -- 降順 (hmatrix eigSH)。 正かつ上位 k を採用。
      lamList = LA.toList eigVals
      take_   = min k n
      lamPos  = [ if v > 0 then v else 0 | v <- take take_ lamList ]
      sqrtL   = LA.diag (LA.fromList (map sqrt lamPos))
      vK      = eigVecs LA.¿ [0 .. take_ - 1]
  in vK LA.<> sqrtL

-- ---------------------------------------------------------------------------
-- Sammon MDS
-- ---------------------------------------------------------------------------

data SammonConfig = SammonConfig
  { sammonMaxIter :: !Int
  , sammonLR      :: !Double   -- ^ 学習率。
  , sammonTol     :: !Double   -- ^ stress 改善の許容下限。
  } deriving (Show)

defaultSammonConfig :: SammonConfig
defaultSammonConfig = SammonConfig
  { sammonMaxIter = 300
  , sammonLR      = 0.3
  , sammonTol     = 1e-6
  }

-- | Sammon stress E = (1/c) Σ_{i<j} (δ_ij - d_ij)² / δ_ij
--   ただし δ_ij は元距離、 d_ij は埋め込み距離、 c = Σ_{i<j} δ_ij。
sammonStress :: LA.Matrix Double  -- ^ 元距離行列 (n × n)。
             -> LA.Matrix Double  -- ^ 埋め込み (n × k)。
             -> Double
sammonStress d y =
  let !n = LA.rows d
      row i = LA.flatten (y LA.? [i])
      pairs = [ (i, j) | i <- [0 .. n - 1], j <- [i + 1 .. n - 1] ]
      delta i j = LA.atIndex d (i, j)
      dij i j = LA.norm_2 (row i - row j)
      cTot  = sum [ delta i j | (i, j) <- pairs ]
      num   = sum [ let !del = delta i j
                        !dd  = dij i j
                    in if del > 0 then (del - dd)^(2 :: Int) / del
                                  else 0
                  | (i, j) <- pairs ]
  in if cTot > 0 then num / cTot else 0

-- | Sammon MDS。 古典 MDS を初期値にして勾配降下。
mdsSammon :: SammonConfig
          -> LA.Matrix Double  -- ^ 距離行列 D (n × n)。
          -> Int                -- ^ 目的次元 k。
          -> LA.Matrix Double  -- ^ 埋め込み (n × k)。
mdsSammon cfg d k =
  let !y0 = mdsClassical d k
      loop !y !iter !prevE
        | iter >= sammonMaxIter cfg = y
        | otherwise =
            let !grad = sammonGrad d y
                !y'   = y - LA.scale (sammonLR cfg) grad
                !e'   = sammonStress d y'
            in if abs (prevE - e') < sammonTol cfg
                 then y'
                 else loop y' (iter + 1) e'
  in loop y0 0 (sammonStress d y0)

-- | Sammon stress の勾配 (n × k)。
sammonGrad :: LA.Matrix Double -> LA.Matrix Double -> LA.Matrix Double
sammonGrad d y =
  let !n = LA.rows d
      !k = LA.cols y
      row i = LA.flatten (y LA.? [i])
      delta i j = LA.atIndex d (i, j)
      cTot = sum [ delta i j | i <- [0 .. n - 1]
                             , j <- [i + 1 .. n - 1] ]
      scl = if cTot > 0 then 2 / cTot else 0
      gradRow i =
        let yi = row i
            contribs = [ let yj = row j
                             dij = LA.norm_2 (yi - yj)
                             del = delta i j
                         in if del > 0 && dij > 0
                              then LA.scale ((del - dij) / (del * dij))
                                     (yi - yj)
                              else LA.konst 0 k
                       | j <- [0 .. n - 1], j /= i ]
        in LA.scale (negate scl) (sum contribs)
  in LA.fromRows [ gradRow i | i <- [0 .. n - 1] ]