packages feed

hanalyze-0.2.0.0: src/Hanalyze/Model/GPRobust.hs

{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module      : Hanalyze.Model.GPRobust
-- Description : ロバストガウス過程 (重尾観測尤度: Student-t / Cauchy)
-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License     : BSD-3-Clause
--
-- Robust GP (heavy-tailed observation likelihoods).
--
-- A closed-form Gaussian-likelihood GP is sensitive to outliers. This
-- module replaces the observation likelihood with Student-t or Cauchy and
-- iterates an IRLS-style scheme (a stable variant of variational EM /
-- Laplace) to obtain a MAP estimate.
--
-- Algorithm:
--
--   1. @f ← 0@ (GP prior mean).
--   2. Iterate until convergence:
--      a. Residual @r = y − f@.
--      b. Compute the per-observation weight:
--         * Student-t @(ν, σ)@:  @w_i = (ν + 1) / (ν + (r_i/σ)²)@.
--         * Cauchy @(γ)@:       @w_i = 2 / (1 + (r_i/γ)²)@.
--    c. 各点の有効ノイズ分散 σ²/w_i (heteroscedastic)
--    d. f ← K (K + σ² W⁻¹)⁻¹ y
-- 3. 予測点 x* で:
--    mean = k_*ᵀ (K + σ² W⁻¹)⁻¹ y
--    var  = k(x*,x*) − k_*ᵀ (K + σ² W⁻¹)⁻¹ k_*
--
-- カーネル関連 ('Kernel', 'GPParams', 'kernelFn') は 'Hanalyze.Model.GP' を再利用。
module Hanalyze.Model.GPRobust
  ( -- * 観測尤度
    RobustLikelihood (..)
  , -- * フィット結果と推論
    RobustGPFit (..)
  , fitGPRobust
  , predictGPRobust
    -- * Multi-output (primary API)
  , RobustGPFitMulti (..)
  , fitGPRobustMulti
  , predictGPRobustMulti
    -- * Multi-input (primary API; X is @n × p@, Y is @n × q@)
  , RobustGPFitMV (..)
  , fitGPRobustMV
  , predictGPRobustMV
  , RobustGPFitMVMulti (..)
  , fitGPRobustMVMulti
  , predictGPRobustMVMulti
  ) where

import qualified Numeric.LinearAlgebra as LA
import qualified Hanalyze.Stat.Cholesky        as Chol
import qualified Hanalyze.Stat.KernelDist      as KD
import Hanalyze.Model.GP
  ( Kernel
  , GPParams (..)
  , gpKernelParams
  , kernelFn
  , buildKernelMatrix
  , buildKernelMatrixMV
  )

-- ---------------------------------------------------------------------------
-- 観測尤度
-- ---------------------------------------------------------------------------

-- | Heavy-tailed observation likelihood.
data RobustLikelihood
  = RGaussian Double            -- ^ Gaussian @(σ_n)@ — equivalent to a
                                --   standard GP (sanity-check baseline).
  | RStudentT Double Double     -- ^ Student-t @(df=ν, scale=σ)@; smaller
                                --   @ν@ means heavier tails.
  | RCauchy   Double            -- ^ Cauchy @(scale=γ)@, equivalent to
                                --   @StudentT(1, γ)@.
  deriving (Show, Eq)

-- | IRLS weight @w(r)@ for residual @r@. The effective noise variance is
-- @σ_eff² / w_i@ at each step.
likelihoodWeight :: RobustLikelihood -> Double -> Double
likelihoodWeight (RGaussian _)        _ = 1.0
likelihoodWeight (RStudentT nu sigma) r =
  let z = r / sigma
  in (nu + 1) / (nu + z * z)
likelihoodWeight (RCauchy gamma) r =
  let z = r / gamma
  in 2 / (1 + z * z)

-- | Reference variance @σ_eff²@ used to scale the IRLS weights.
likelihoodScale2 :: RobustLikelihood -> Double
likelihoodScale2 (RGaussian s)      = s * s
likelihoodScale2 (RStudentT _ s)    = s * s
likelihoodScale2 (RCauchy g)        = g * g

-- ---------------------------------------------------------------------------
-- フィット結果
-- ---------------------------------------------------------------------------

-- | Robust GP fit result.
data RobustGPFit = RobustGPFit
  { rgpKernel  :: Kernel
  , rgpParams  :: GPParams
  , rgpLik     :: RobustLikelihood
  , rgpTrainX  :: [Double]              -- ^ Training inputs.
  , rgpTrainY  :: [Double]              -- ^ Training targets.
  , rgpAlpha   :: LA.Vector Double      -- ^ @α = (K + σ² W⁻¹)⁻¹ y@.
  , rgpKyInv   :: LA.Matrix Double      -- ^ @(K + σ² W⁻¹)⁻¹@ at convergence.
  , rgpWeights :: LA.Vector Double      -- ^ IRLS weights at convergence.
  , rgpIters   :: Int                   -- ^ Number of IRLS iterations executed.
  } deriving (Show)

-- ---------------------------------------------------------------------------
-- フィット
-- ---------------------------------------------------------------------------

-- | Compute the MAP of a robust GP via IRLS iteration. At most 50
-- iterations; convergence when @‖f_new − f‖∞ < 10⁻⁶@.
fitGPRobust
  :: Kernel
  -> GPParams                    -- ^ Kernel hyperparameters (held fixed —
                                 --   optimize them separately).
  -> RobustLikelihood
  -> [Double]                    -- ^ Training @X@.
  -> [Double]                    -- ^ Training @Y@.
  -> RobustGPFit
fitGPRobust ker params lik trainX trainY =
  let n         = length trainX
      kMatrix   = buildKernelMatrix ker (gpKernelParams params) trainX trainX  -- K (n×n)
      yV        = LA.fromList trainY
      sigEff2   = likelihoodScale2 lik
      -- 1 反復: f, w を更新
      step (f, w, _iter) =
        let r          = LA.toList (yV - f)
            wNew'      = [ max 1e-8 (likelihoodWeight lik ri)
                         | ri <- r ]
            wNewVec    = LA.fromList wNew'
            wInvDiag   = LA.diag (LA.fromList [ sigEff2 / wi | wi <- wNew' ])
            ky         = kMatrix `LA.add` wInvDiag
            -- α = (K + σ²W⁻¹)⁻¹ y via SPD Cholesky (replaces inv + matvec).
            alpha      = LA.flatten
                          (Chol.cholSolveJitter ky (LA.asColumn yV))
            fNew       = kMatrix LA.#> alpha
            delta      = LA.maxElement (LA.cmap abs (fNew - f))
        in (fNew, wNewVec, delta)

      maxIters     = 50
      tol          = 1e-6 :: Double

      loop f w iter
        | iter >= maxIters = (f, w, iter)
        | otherwise =
            let (fNew, wNew, delta) = step (f, w, iter)
            in if delta < tol
                 then (fNew, wNew, iter + 1)
                 else loop fNew wNew (iter + 1)

      f0     = LA.fromList (replicate n 0.0)
      w0     = LA.fromList (replicate n 1.0)
      (_fOpt, wOpt, iters) = loop f0 w0 0

      -- 最終 K_y, α, K_y⁻¹ を再計算 (収束後の重みで)。
      -- kyInv は予測時の分散計算で必要なため陽に保持する。
      wInvDiag' = LA.diag (LA.cmap (\wi -> sigEff2 / max 1e-8 wi) wOpt)
      ky'       = kMatrix `LA.add` wInvDiag'
      kyInv'    = Chol.cholSolveJitter ky' (LA.ident n)
      alpha'    = LA.flatten
                  (Chol.cholSolveJitter ky' (LA.asColumn yV))
  in RobustGPFit
       { rgpKernel  = ker
       , rgpParams  = params
       , rgpLik     = lik
       , rgpTrainX  = trainX
       , rgpTrainY  = trainY
       , rgpAlpha   = alpha'
       , rgpKyInv   = kyInv'
       , rgpWeights = wOpt
       , rgpIters   = iters
       }

-- ---------------------------------------------------------------------------
-- 予測
-- ---------------------------------------------------------------------------

-- | Predictive mean and variance of @f@ at the given test points.
-- mean = k_*ᵀ α, var = k(x*,x*) − k_*ᵀ K_y⁻¹ k_*
predictGPRobust :: RobustGPFit -> [Double] -> [(Double, Double)]
predictGPRobust fit testX =
  let ker     = rgpKernel fit
      params  = rgpParams fit
      trainX  = rgpTrainX fit
      kStar   = buildKernelMatrix ker (gpKernelParams params) testX trainX     -- (m, n)
      means   = LA.toList (kStar LA.#> rgpAlpha fit)
      kyInv   = rgpKyInv fit
      diagKss = [ kernelFn ker (gpKernelParams params) x x | x <- testX ]
      ws      = kStar LA.<> kyInv                              -- (m, n)
      -- F1: vectorise per-row dots.
      rowDots = LA.toList (KD.rowDotsAB kStar ws)
      varList = zipWith (\d kw -> max 0 (d - kw)) diagKss rowDots
  in zip means varList

-- ---------------------------------------------------------------------------
-- 多出力 (列ごと IRLS、カーネル行列を共有)
-- ---------------------------------------------------------------------------

-- | 多出力ロバスト GP の結果。q 出力ぶんの 'RobustGPFit' を保持し、
-- カーネル / ハイパラ / 尤度は共通。
data RobustGPFitMulti = RobustGPFitMulti
  { rgmKernel :: Kernel
  , rgmParams :: GPParams
  , rgmLik    :: RobustLikelihood
  , rgmTrainX :: [Double]
  , rgmFits   :: [RobustGPFit]   -- ^ 列ごとの単出力 fit
  } deriving (Show)

-- | 多出力ロバスト GP fit。Y は n × q、各列ごとに IRLS (重みは出力依存)。
fitGPRobustMulti
  :: Kernel
  -> GPParams
  -> RobustLikelihood
  -> [Double]            -- ^ 訓練 X
  -> LA.Matrix Double    -- ^ Y (n × q)
  -> RobustGPFitMulti
fitGPRobustMulti ker params lik trainX yMat =
  let q     = LA.cols yMat
      yCols = [ LA.toList (LA.flatten (yMat LA.¿ [j])) | j <- [0 .. q - 1] ]
      fits  = [ fitGPRobust ker params lik trainX y | y <- yCols ]
  in RobustGPFitMulti ker params lik trainX fits

-- | 多出力ロバスト GP 予測。戻り値: (mean 行列 m × q, 列ごとの分散リスト)。
predictGPRobustMulti :: RobustGPFitMulti -> [Double]
                     -> (LA.Matrix Double, [[Double]])
predictGPRobustMulti mf testX =
  let preds = [ predictGPRobust f testX | f <- rgmFits mf ]
      meansCols = map (map fst) preds
      varsCols  = map (map snd) preds
      meansMat  = LA.fromColumns [ LA.fromList col | col <- meansCols ]
  in (meansMat, varsCols)

-- ---------------------------------------------------------------------------
-- Multi-input (multivariate X) API
-- ---------------------------------------------------------------------------

-- | Robust GP fit with multivariate input. Mirrors 'RobustGPFit' but
-- stores @X@ as an @n × p@ matrix and @y@ as a 'LA.Vector'.
data RobustGPFitMV = RobustGPFitMV
  { rgpmvKernel  :: Kernel
  , rgpmvParams  :: GPParams
  , rgpmvLik     :: RobustLikelihood
  , rgpmvTrainX  :: LA.Matrix Double      -- ^ @n × p@.
  , rgpmvTrainY  :: LA.Vector Double      -- ^ length @n@.
  , rgpmvAlpha   :: LA.Vector Double
  , rgpmvKyInv   :: LA.Matrix Double
  , rgpmvWeights :: LA.Vector Double
  , rgpmvIters   :: Int
  } deriving (Show)

-- | Compute the MAP of a multi-input robust GP via the same IRLS scheme
-- as 'fitGPRobust'. @X@ is @n × p@; @y@ has length @n@.
fitGPRobustMV
  :: Kernel
  -> GPParams
  -> RobustLikelihood
  -> LA.Matrix Double          -- ^ Training @X@ (@n × p@).
  -> LA.Vector Double          -- ^ Training @y@ (length @n@).
  -> RobustGPFitMV
fitGPRobustMV ker params lik trainX yV =
  let n         = LA.rows trainX
      kMatrix   = buildKernelMatrixMV ker (gpKernelParams params) trainX trainX
      sigEff2   = likelihoodScale2 lik
      step (f, w, _iter) =
        let r          = LA.toList (yV - f)
            wNew'      = [ max 1e-8 (likelihoodWeight lik ri) | ri <- r ]
            wNewVec    = LA.fromList wNew'
            wInvDiag   = LA.diag (LA.fromList [ sigEff2 / wi | wi <- wNew' ])
            ky         = kMatrix `LA.add` wInvDiag
            -- α = (K + σ²W⁻¹)⁻¹ y via SPD Cholesky.
            alpha      = LA.flatten
                          (Chol.cholSolveJitter ky (LA.asColumn yV))
            fNew       = kMatrix LA.#> alpha
            delta      = LA.maxElement (LA.cmap abs (fNew - f))
        in (fNew, wNewVec, delta)

      maxIters = 50
      tol      = 1e-6 :: Double

      loop f w iter
        | iter >= maxIters = (f, w, iter)
        | otherwise =
            let (fNew, wNew, delta) = step (f, w, iter)
            in if delta < tol
                 then (fNew, wNew, iter + 1)
                 else loop fNew wNew (iter + 1)

      f0 = LA.fromList (replicate n 0.0)
      w0 = LA.fromList (replicate n 1.0)
      (_fOpt, wOpt, iters) = loop f0 w0 0

      wInvDiag' = LA.diag (LA.cmap (\wi -> sigEff2 / max 1e-8 wi) wOpt)
      ky'       = kMatrix `LA.add` wInvDiag'
      kyInv'    = Chol.cholSolveJitter ky' (LA.ident n)
      alpha'    = LA.flatten
                  (Chol.cholSolveJitter ky' (LA.asColumn yV))
  in RobustGPFitMV
       { rgpmvKernel  = ker
       , rgpmvParams  = params
       , rgpmvLik     = lik
       , rgpmvTrainX  = trainX
       , rgpmvTrainY  = yV
       , rgpmvAlpha   = alpha'
       , rgpmvKyInv   = kyInv'
       , rgpmvWeights = wOpt
       , rgpmvIters   = iters
       }

-- | Predictive mean and variance at multi-input test points (@m × p@).
predictGPRobustMV
  :: RobustGPFitMV -> LA.Matrix Double
  -> (LA.Vector Double, LA.Vector Double)
predictGPRobustMV fit testX =
  let ker     = rgpmvKernel fit
      params  = rgpmvParams fit
      trainX  = rgpmvTrainX fit
      kStar   = buildKernelMatrixMV ker (gpKernelParams params) testX trainX  -- m × n
      means   = kStar LA.#> rgpmvAlpha fit
      kyInv   = rgpmvKyInv fit
      sf      = gpSignalVar params
      diagKss = LA.konst sf (LA.rows testX)
      ws      = kStar LA.<> kyInv                            -- m × n
      -- F1: vectorise per-row dots.
      vars    = LA.cmap (max 0) (diagKss - KD.rowDotsAB kStar ws)
  in (means, vars)

-- | Multi-input multi-output robust GP. Per-column IRLS (weights are
-- output-specific), but the kernel matrix @K@ is shared.
data RobustGPFitMVMulti = RobustGPFitMVMulti
  { rgmvKernel :: Kernel
  , rgmvParams :: GPParams
  , rgmvLik    :: RobustLikelihood
  , rgmvTrainX :: LA.Matrix Double
  , rgmvFits   :: [RobustGPFitMV]
  } deriving (Show)

-- | Fit a multi-input multi-output robust GP. @Y@ has shape @n × q@.
fitGPRobustMVMulti
  :: Kernel
  -> GPParams
  -> RobustLikelihood
  -> LA.Matrix Double          -- ^ Training @X@ (@n × p@).
  -> LA.Matrix Double          -- ^ Training @Y@ (@n × q@).
  -> RobustGPFitMVMulti
fitGPRobustMVMulti ker params lik trainX yMat =
  let q     = LA.cols yMat
      cols  = [ LA.flatten (yMat LA.¿ [j]) | j <- [0 .. q - 1] ]
      fits  = [ fitGPRobustMV ker params lik trainX y | y <- cols ]
  in RobustGPFitMVMulti ker params lik trainX fits

-- | Multi-input multi-output robust GP prediction. Returns the @m × q@
-- mean matrix and a per-column variance vector.
predictGPRobustMVMulti
  :: RobustGPFitMVMulti -> LA.Matrix Double
  -> (LA.Matrix Double, [LA.Vector Double])
predictGPRobustMVMulti mf testX =
  let preds   = [ predictGPRobustMV f testX | f <- rgmvFits mf ]
      meanCs  = map fst preds
      varCs   = map snd preds
      meanMat = LA.fromColumns meanCs
  in (meanMat, varCs)