packages feed

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

{-# LANGUAGE BangPatterns #-}
-- |
-- Module      : Hanalyze.Model.GradientBoosting
-- Description : 勾配ブースティング (Gradient Boosting Machine、 回帰 + 二値分類)
-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License     : BSD-3-Clause
--
-- Gradient Boosting Machine (回帰 + 二値分類).
--
-- 弱学習器は 'Hanalyze.Model.RandomForest' の回帰木 ('RF.Tree' /
-- 'RF.buildTreeV') を流用 (bootstrap 無 + mtry = d で full-data /
-- 全特徴を使う通常の GBM 木に縮約)。
--
-- @
-- import qualified Hanalyze.Model.GradientBoosting as GB
-- gb <- GB.fitGBRegressor GB.defaultGBM x y
-- let yhat = GB.predictGBR gb x
-- @
--
-- 損失:
--
--   * 回帰: 二乗誤差 (negative gradient = 残差)
--   * 分類 (binary): log-loss (negative gradient = y - sigmoid(F))
module Hanalyze.Model.GradientBoosting
  ( GBConfig (..)
  , defaultGBM
  , GBRegressor (..)
  , GBClassifier (..)
  , fitGBRegressor
  , fitGBClassifier
  , predictGBR
  , predictGBRRow
  , predictGBC
  , predictGBCProbs
  ) where

import qualified Data.Vector.Unboxed   as VU
import qualified Numeric.LinearAlgebra as LA
import qualified Hanalyze.Model.RandomForest as RF

-- ---------------------------------------------------------------------------
-- Config
-- ---------------------------------------------------------------------------

-- | GBM 設定。
data GBConfig = GBConfig
  { gbNRounds    :: !Int     -- ^ ブースティング回数 M。
  , gbMaxDepth   :: !Int     -- ^ 各弱学習器の最大深さ (典型 3-5)。
  , gbMinSamples :: !Int     -- ^ 葉最小サンプル数。
  , gbLearnRate  :: !Double  -- ^ 学習率 η (typ 0.1)。
  } deriving (Show)

defaultGBM :: GBConfig
defaultGBM = GBConfig
  { gbNRounds    = 100
  , gbMaxDepth   = 3
  , gbMinSamples = 2
  , gbLearnRate  = 0.1
  }

-- | 弱学習器設定 (full-data / 全特徴利用、 木の深さは gbMaxDepth)。
weakRFCfg :: Int -> GBConfig -> RF.RFConfig
weakRFCfg d cfg = RF.RFConfig
  { RF.rfTrees      = 1
  , RF.rfMaxDepth   = gbMaxDepth cfg
  , RF.rfMinSamples = gbMinSamples cfg
  , RF.rfMtry       = Just d
  , RF.rfBootstrap  = False
  }

-- ---------------------------------------------------------------------------
-- Regressor
-- ---------------------------------------------------------------------------

-- | 回帰 GBM。 予測 = init + η · Σ tree_m(x).
data GBRegressor = GBRegressor
  { gbrInit  :: !Double
  , gbrTrees :: ![RF.Tree]
  , gbrLR    :: !Double
  } deriving (Show)

fitGBRegressor :: GBConfig
               -> LA.Matrix Double   -- ^ X (n × d)
               -> VU.Vector Double   -- ^ y (n)
               -> GBRegressor
fitGBRegressor cfg x y =
  let !n     = VU.length y
      !d     = LA.cols x
      !cfgW  = weakRFCfg d cfg
      !lr    = gbLearnRate cfg
      !f0    = VU.sum y / fromIntegral n
      !preds0 = VU.replicate n f0
      idx    = VU.enumFromN 0 n

      step (!preds, !trees) _ =
        let !res = VU.zipWith (-) y preds
            !t   = RF.buildTreeV cfgW x res idx 0
            !upd = VU.map (\i -> lr * RF.predictTree t (rowList x i))
                          (VU.enumFromN 0 n)
            !preds' = VU.zipWith (+) preds upd
        in (preds', t : trees)

      (_, treesRev) = foldl step (preds0, []) [1 .. gbNRounds cfg]
  in GBRegressor f0 (reverse treesRev) lr

-- | 1 行を [Double] 化 (predictTree のための一時変換)。
rowList :: LA.Matrix Double -> Int -> [Double]
rowList x i = LA.toList (LA.flatten (x LA.? [i]))

-- | 1 サンプルの予測。
predictGBRRow :: GBRegressor -> [Double] -> Double
predictGBRRow gb xs =
  gbrInit gb
    + gbrLR gb * sum [ RF.predictTree t xs | t <- gbrTrees gb ]

-- | 行列入力に対する予測 (n).
predictGBR :: GBRegressor -> LA.Matrix Double -> VU.Vector Double
predictGBR gb x =
  let !n = LA.rows x
  in VU.generate n (\i -> predictGBRRow gb (rowList x i))

-- ---------------------------------------------------------------------------
-- Classifier (binary)
-- ---------------------------------------------------------------------------

-- | 二値分類 GBM (logit + log-loss)。 ラベルは 0/1。
data GBClassifier = GBClassifier
  { gbcInit  :: !Double          -- ^ logit(p̂_0)
  , gbcTrees :: ![RF.Tree]
  , gbcLR    :: !Double
  } deriving (Show)

sigmoid :: Double -> Double
sigmoid z = 1 / (1 + exp (negate z))

clamp :: Double -> Double -> Double -> Double
clamp lo hi v = max lo (min hi v)

fitGBClassifier :: GBConfig
                -> LA.Matrix Double   -- ^ X (n × d)
                -> VU.Vector Int      -- ^ y ∈ {0,1} (n)
                -> GBClassifier
fitGBClassifier cfg x y =
  let !n    = VU.length y
      !d    = LA.cols x
      !cfgW = weakRFCfg d cfg
      !lr   = gbLearnRate cfg
      !yD   = VU.map fromIntegral y :: VU.Vector Double
      !p0   = clamp 1e-6 (1 - 1e-6) (VU.sum yD / fromIntegral n)
      !f0   = log (p0 / (1 - p0))
      !logits0 = VU.replicate n f0
      idx   = VU.enumFromN 0 n

      step (!logits, !trees) _ =
        let !grad = VU.zipWith (\yi z -> yi - sigmoid z) yD logits
            !t    = RF.buildTreeV cfgW x grad idx 0
            !upd  = VU.map (\i -> lr * RF.predictTree t (rowList x i))
                           (VU.enumFromN 0 n)
            !logits' = VU.zipWith (+) logits upd
        in (logits', t : trees)

      (_, treesRev) = foldl step (logits0, []) [1 .. gbNRounds cfg]
  in GBClassifier f0 (reverse treesRev) lr

-- | クラス確率 p(y=1 | x) を返す。
predictGBCProbs :: GBClassifier -> LA.Matrix Double -> VU.Vector Double
predictGBCProbs gb x =
  let !n = LA.rows x
      logit xs = gbcInit gb
                   + gbcLR gb * sum [ RF.predictTree t xs | t <- gbcTrees gb ]
  in VU.generate n (\i -> sigmoid (logit (rowList x i)))

-- | クラス予測 (閾値 0.5)。
predictGBC :: GBClassifier -> LA.Matrix Double -> VU.Vector Int
predictGBC gb x =
  VU.map (\p -> if p >= 0.5 then 1 else 0) (predictGBCProbs gb x)