hanalyze-models-0.2.0.1: src/Hanalyze/Model/Kernel.hs
{-# LANGUAGE StrictData #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module : Hanalyze.Model.Kernel
-- Description : GP/SVM/カーネル法で共通のカーネル語彙 (RBF/Matern52/Periodic/Linear/Poly)
-- Copyright : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License : BSD-3-Clause
--
-- [日本語]: 共有カーネル語彙 (GP / SVM / カーネル法で共通)。 'Model.GP' から分離した。
--
-- GP 族の定常/内積カーネル ('RBF' / 'Matern52' / 'Periodic' / 'Linear' / 'Poly') と
-- そのハイパーパラメータ 'KernelParams' (ℓ / σ_f² / period / ARD per-dim ℓ) を集約する。
-- @GPParams@ (= 'KernelParams' + 観測ノイズ σ_n²) に依存しないので、 SVM 等
-- ノイズを持たないカーネル法はこのモジュールだけを import すればよい
-- ('Model.GP' を import しない)。
--
-- 評価関数:
--
-- - 'kernelFn' — 1D 入力の @k(x, x')@。
-- - 'buildKernelMatrix' — 1D の Gram 行列 @K(xs, xs')@。
-- - 'applyKernel' — 二乗距離行列 → カーネル行列 (距離カーネル専用)。
-- - 'kernelOfParams' — 固定パラメータの @s ↦ k(s)@ (距離カーネル専用・INLINE)。
-- - 'ardScaleXY' — ARD 列スケーリング。
-- - 'buildKernelMatrixMV' — 多入力 Gram 行列 (全カーネル)。
-- - 'kEvalMV' — 多入力の点対点評価 @k(a, b)@ (全カーネル・SVM 等の汎用経路)。
--
-- 距離カーネル (RBF/Matern52/Periodic) は二乗距離から、 内積カーネル
-- (Linear/Poly) は内積から評価する。 'applyKernel' / 'kernelOfParams' は距離専用で、
-- 内積カーネルを渡すと error (multi-input gram は 'buildKernelMatrixMV' が内積経路へ
-- 分岐するためそこには到達しない)。
--
-- [English]: Shared kernel vocabulary (common to GP \/ SVM \/ kernel
-- methods). Split out from 'Model.GP'.
--
-- Gathers the GP family's stationary\/dot-product kernels ('RBF' \/
-- 'Matern52' \/ 'Periodic' \/ 'Linear' \/ 'Poly') and their hyperparameters
-- 'KernelParams' (ℓ \/ σ_f² \/ period \/ per-dim ARD ℓ). This module does
-- not depend on @GPParams@ (= 'KernelParams' + observation noise σ_n²), so
-- noise-free kernel methods such as SVM only need to import this module
-- (and not 'Model.GP').
--
-- Evaluation functions:
--
-- - 'kernelFn' — @k(x, x')@ for 1D inputs.
-- - 'buildKernelMatrix' — the 1D Gram matrix @K(xs, xs')@.
-- - 'applyKernel' — squared-distance matrix → kernel matrix
-- (distance kernels only).
-- - 'kernelOfParams' — @s ↦ k(s)@ for fixed parameters (distance
-- kernels only, INLINE).
-- - 'ardScaleXY' — ARD column scaling.
-- - 'buildKernelMatrixMV' — multi-input Gram matrix (all kernels).
-- - 'kEvalMV' — point-to-point multi-input evaluation
-- @k(a, b)@ (all kernels; the generic path used by SVM etc.).
--
-- Distance kernels (RBF\/Matern52\/Periodic) are evaluated from squared
-- distance; dot-product kernels (Linear\/Poly) are evaluated from the dot
-- product. 'applyKernel' \/ 'kernelOfParams' are distance-only and error
-- if given a dot-product kernel (the multi-input gram path never reaches
-- them, since 'buildKernelMatrixMV' branches to the dot-product path
-- first).
module Hanalyze.Model.Kernel
( -- * カーネル型
Kernel (..)
, kernelName
-- * カーネルハイパーパラメータ
, KernelParams (..)
, defaultKernelParams
-- * 評価
, kernelFn
, buildKernelMatrix
, applyKernel
, kernelOfParams
, ardScaleXY
, buildKernelMatrixMV
, kEvalMV
) where
import Data.Text (Text)
import qualified Data.Text as T
import qualified Numeric.LinearAlgebra as LA
import qualified Hanalyze.Stat.KernelDist as KD
import qualified Data.Vector.Storable as VS
import qualified Data.Vector.Storable.Mutable as VSM
import Control.Monad.ST (runST)
-- ---------------------------------------------------------------------------
-- 型
-- ---------------------------------------------------------------------------
-- | [日本語]: GP / SVM 族のカーネル種別。
-- [English]: Kernel variants for the GP\/SVM family.
data Kernel
= RBF
-- ^ Squared exponential: @k(x,x') = σ_f² exp(−r²/(2ℓ²))@.
-- Best for smooth functions; the most commonly used kernel.
| Matern52
-- ^ Matérn 5/2: @k(x,x') = σ_f²(1+√5 r/ℓ+5r²/(3ℓ²)) exp(−√5 r/ℓ)@.
-- Slightly rougher than RBF; common in physical systems.
| Periodic
-- ^ Periodic: @k(x,x') = σ_f² exp(−2 sin²(π r/p)/ℓ²)@.
-- For periodic patterns; set 'kpPeriod' appropriately.
| Linear
-- ^ Linear (dot-product): @k(x,x') = σ_f² (x·x')@. A non-stationary
-- kernel; with SVM gives a linear decision boundary.
| Poly !Int
-- ^ Polynomial of degree @d@: @k(x,x') = (γ (x·x') + 1)^d@ with
-- @γ = 1/(2ℓ²)@ (shared with the SVM γ convention). A
-- non-stationary kernel.
deriving (Show, Eq)
-- | Display name of a kernel.
kernelName :: Kernel -> Text
kernelName RBF = "RBF"
kernelName Matern52 = "Mat\xe9rn 5/2"
kernelName Periodic = "Periodic"
kernelName Linear = "Linear"
kernelName (Poly d) = "Poly(" <> T.pack (show d) <> ")"
-- | [日本語]: カーネルハイパーパラメータ (観測ノイズ σ_n² は含まない)。
-- [English]: Kernel hyperparameters (does not include the observation
-- noise σ_n²).
data KernelParams = KernelParams
{ kpLengthScale :: Double
-- ^ Isotropic length scale @ℓ@; larger means smoother. Used unless
-- 'kpLengthScales' is 'Just' (= ARD), in which case the per-dim
-- vector overrides this for multi-input kernel evaluation.
, kpSignalVar :: Double
-- ^ Signal variance @σ_f²@; the variability of the function values.
, kpPeriod :: Double
-- ^ Period @p@ (only used by the @Periodic@ kernel).
, kpLengthScales :: Maybe (LA.Vector Double)
-- ^ Per-dim length scales for ARD (Automatic Relevance
-- Determination). When 'Just' v, the multi-input kernel uses
-- @D_ARD[i,j] = Σ_d (X[i,d] − X'[j,d])² / ℓ_d²@ instead of the
-- isotropic distance / ℓ². Has no effect on the 1D 'kernelFn'
-- path. 'Nothing' = isotropic (default).
} deriving (Show)
-- | Default kernel hyperparameters: @ℓ = σ_f² = p = 1@, isotropic.
defaultKernelParams :: KernelParams
defaultKernelParams = KernelParams 1.0 1.0 1.0 Nothing
-- ---------------------------------------------------------------------------
-- 1D 評価
-- ---------------------------------------------------------------------------
-- | Evaluate the kernel function @k(x, x')@ for scalar inputs.
kernelFn :: Kernel -> KernelParams -> Double -> Double -> Double
kernelFn RBF p x x' =
let d = x - x'
l = kpLengthScale p
in kpSignalVar p * exp (-(d * d) / (2 * l * l))
kernelFn Matern52 p x x' =
let d = abs (x - x')
l = kpLengthScale p
s = sqrt 5 * d / l
in kpSignalVar p * (1 + s + s * s / 3) * exp (-s)
kernelFn Periodic p x x' =
let d = abs (x - x')
l = kpLengthScale p
s = sin (pi * d / kpPeriod p)
in kpSignalVar p * exp (-2 * s * s / (l * l))
kernelFn Linear p x x' =
-- 内積カーネル: 1D では x·x' = x*x'。
kpSignalVar p * (x * x')
kernelFn (Poly d) p x x' =
-- (γ x·x' + 1)^d, γ = 1/(2ℓ²)。1D では x·x' = x*x'。
let l = kpLengthScale p
g = 1 / (2 * l * l)
in (g * (x * x') + 1) ^^ d
-- | Build the kernel matrix @K(xs, xs')@ of shape @|xs| × |xs'|@.
--
-- Fills a flat 'Storable.Vector' via @runST + MVector@ instead of
-- materialising the @|xs|·|xs'|@ lazy @[Double]@ list (one allocation per
-- kernel call). 'kernelFn' itself is unchanged so 'Periodic'
-- (signed-difference dependent) keeps working.
buildKernelMatrix :: Kernel -> KernelParams -> [Double] -> [Double] -> LA.Matrix Double
buildKernelMatrix ker p xs xs' =
let xv = VS.fromList xs
yv = VS.fromList xs'
n = VS.length xv
m = VS.length yv
out = runST $ do
v <- VSM.unsafeNew (n * m)
let go !i !j
| i >= n = pure ()
| j >= m = go (i + 1) 0
| otherwise = do
let xi = VS.unsafeIndex xv i
yj = VS.unsafeIndex yv j
VSM.unsafeWrite v (i * m + j) (kernelFn ker p xi yj)
go i (j + 1)
go 0 0
VS.unsafeFreeze v
in LA.reshape m out
-- ---------------------------------------------------------------------------
-- 多入力 (multivariate) 評価
-- ---------------------------------------------------------------------------
-- | [日本語]: 二乗距離行列 (@m × n@) にカーネル関数を適用する。
-- 距離カーネル (RBF/Matern52/Periodic) 専用。 内積カーネル (Linear/Poly) は
-- 二乗距離から復元できないため error (multi-input gram は 'buildKernelMatrixMV'
-- が内積経路へ分岐するためここには到達しない)。
-- [English]: Apply the kernel function to an @m × n@ matrix of squared
-- distances. Distance kernels (RBF\/Matern52\/Periodic) only.
-- Dot-product kernels (Linear\/Poly) cannot be recovered from the
-- squared distance and error out (the multi-input gram path never
-- reaches here, since 'buildKernelMatrixMV' branches to the
-- dot-product path first).
applyKernel :: Kernel -> KernelParams -> LA.Matrix Double -> LA.Matrix Double
applyKernel RBF p d2 =
let l2 = kpLengthScale p ** 2
sf = kpSignalVar p
in KD.mapMatrix (\s -> sf * exp (- s / (2 * l2))) d2
applyKernel Matern52 p d2 =
let l = kpLengthScale p
sf = kpSignalVar p
in KD.mapMatrix (\s -> let r = sqrt (max 0 s)
u = sqrt 5 * r / l
in sf * (1 + u + u * u / 3) * exp (- u)) d2
applyKernel Periodic p d2 =
let l = kpLengthScale p
sf = kpSignalVar p
pr = kpPeriod p
in KD.mapMatrix (\s -> let r = sqrt (max 0 s)
ss = sin (pi * r / pr)
in sf * exp (- 2 * ss * ss / (l * l))) d2
applyKernel Linear _ _ = error "applyKernel: Linear は内積カーネル。buildKernelMatrixMV/kEvalMV を使うこと"
applyKernel (Poly _) _ _ = error "applyKernel: Poly は内積カーネル。buildKernelMatrixMV/kEvalMV を使うこと"
-- | Apply ARD scaling to (X, X') if 'kpLengthScales' is 'Just'. Returns
-- the (possibly rescaled) matrices and a 'KernelParams' with @ℓ = 1@ so
-- that 'applyKernel' divides by 1 (the per-dim ℓ_d already absorbed into
-- the column scaling). 'Nothing' = isotropic, returns inputs and params
-- unchanged. The 'Periodic' kernel does not support ARD.
ardScaleXY
:: Kernel -> KernelParams -> LA.Matrix Double -> LA.Matrix Double
-> (LA.Matrix Double, LA.Matrix Double, KernelParams)
ardScaleXY Periodic p x y = (x, y, p)
ardScaleXY _ p x y = case kpLengthScales p of
Nothing -> (x, y, p)
Just ls ->
let p_ = LA.cols x
lsExt = if LA.size ls == p_
then ls
else LA.konst (kpLengthScale p) p_ -- safety fallback
invL = LA.cmap (1 /) lsExt -- 1 / ℓ_d
scaleCols m = m LA.<> LA.diag invL
x' = scaleCols x
y' = scaleCols y
p' = p { kpLengthScale = 1.0 }
in (x', y', p')
-- | Build the kernel matrix @K(X, X')@ of shape @|X| × |X'|@ from
-- multi-input matrices. @X@ is @n × p@; @X'@ is @m × p@.
--
-- When 'kpLengthScales' is 'Just', uses ARD: each input dimension is
-- scaled by @1 / ℓ_d@ before computing pairwise squared distances.
buildKernelMatrixMV
:: Kernel -> KernelParams -> LA.Matrix Double -> LA.Matrix Double
-> LA.Matrix Double
buildKernelMatrixMV Linear p x x' =
-- 内積カーネル: K = σ_f² X X'ᵀ (距離経路を通さない)。
LA.scale (kpSignalVar p) (x LA.<> LA.tr x')
buildKernelMatrixMV (Poly d) p x x' =
-- (γ X X'ᵀ + 1)^d, γ = 1/(2ℓ²)。
let l = kpLengthScale p
g = 1 / (2 * l * l)
in LA.cmap (\ip -> (g * ip + 1) ^^ d) (x LA.<> LA.tr x')
buildKernelMatrixMV ker p x x' =
let (xs, ys, p') = ardScaleXY ker p x x'
in applyKernel ker p' (KD.pairwiseSqDistXY xs ys)
-- | [日本語]: 多入力カーネル評価 @k(a, b)@ (全カーネル対応・SVM 等の汎用経路)。
-- 距離カーネル (RBF/Matern52/Periodic) は二乗距離、 内積カーネル (Linear/Poly)
-- は内積から評価する。
-- [English]: Multi-input kernel evaluation @k(a, b)@ (supports all
-- kernels; the generic path used by SVM etc.). Distance kernels
-- (RBF\/Matern52\/Periodic) are evaluated from squared distance,
-- dot-product kernels (Linear\/Poly) from the dot product.
kEvalMV :: Kernel -> KernelParams -> LA.Vector Double -> LA.Vector Double -> Double
kEvalMV Linear p a b = kpSignalVar p * (a LA.<.> b)
kEvalMV (Poly d) p a b =
let l = kpLengthScale p
g = 1 / (2 * l * l)
in (g * (a LA.<.> b) + 1) ^^ d
kEvalMV ker p a b =
let d = a - b
in kernelOfParams ker p (d LA.<.> d) -- 距離カーネル: s = ‖a−b‖²
-- | [日本語]: 固定パラメータ集合に対する特殊化カーネル関数。 GHC が
-- 'Model.GP' の @mkNoiseKernelFromD2@ 内側ループに密にインライン化できる
-- 単相 @Double -> Double@ を返す。 距離カーネル専用。
-- [English]: Specialized kernel function for a fixed parameter set,
-- returning a monomorphic @Double -> Double@ that GHC can inline tightly
-- into the @mkNoiseKernelFromD2@ inner loop (in 'Model.GP'). Distance
-- kernels only.
{-# INLINE kernelOfParams #-}
kernelOfParams :: Kernel -> KernelParams -> (Double -> Double)
kernelOfParams RBF p =
let !l2 = kpLengthScale p ** 2
!sf = kpSignalVar p
!inv2L2 = 1 / (2 * l2)
in \s -> sf * exp (- s * inv2L2)
kernelOfParams Matern52 p =
let !l = kpLengthScale p
!sf = kpSignalVar p
!invL = sqrt 5 / l
in \s -> let r = sqrt (max 0 s)
u = invL * r
in sf * (1 + u + u * u / 3) * exp (- u)
kernelOfParams Periodic p =
let !l = kpLengthScale p
!sf = kpSignalVar p
!pr = kpPeriod p
!invL2 = 1 / (l * l)
!invPr = pi / pr
in \s -> let r = sqrt (max 0 s)
ss = sin (invPr * r)
in sf * exp (- 2 * ss * ss * invL2)
kernelOfParams Linear _ = error "kernelOfParams: Linear は内積カーネル。kEvalMV を使うこと"
kernelOfParams (Poly _) _ = error "kernelOfParams: Poly は内積カーネル。kEvalMV を使うこと"