hanalyze-0.2.0.0: test/Hanalyze/Model/SVMSpec.hs
{-# LANGUAGE OverloadedStrings #-}
module Hanalyze.Model.SVMSpec (spec) where
import Test.Hspec
import qualified Data.Vector.Unboxed as VU
import qualified Numeric.LinearAlgebra as LA
import Hanalyze.Model.SVM
import Hanalyze.Model.Kernel (Kernel (..), KernelParams (..), defaultKernelParams)
-- 線形分離可能な 2 クラス (左下 vs 右上)。
linData :: (LA.Matrix Double, VU.Vector Int)
linData =
( LA.fromLists [[0,0],[0,1],[1,0],[4,4],[4,5],[5,4]]
, VU.fromList [0,0,0,1,1,1] )
-- 同心 (XOR 風)・線形非分離: 内側 1 クラス・外側 1 クラス。
ringData :: (LA.Matrix Double, VU.Vector Int)
ringData =
( LA.fromLists [ [0,0],[0.3,0.2],[-0.2,0.1] -- 内側 (class 0)
, [3,0],[-3,0],[0,3],[0,-3],[2.1,2.1],[-2.1,-2.1] ] -- 外側 (class 1)
, VU.fromList [0,0,0, 1,1,1,1,1,1] )
trainAcc :: SVM -> LA.Matrix Double -> VU.Vector Int -> Double
trainAcc m x y =
let pred = predictSVM m x
n = VU.length y
ok = length [ () | i <- [0 .. n - 1], pred VU.! i == y VU.! i ]
in fromIntegral ok / fromIntegral n
-- γ=1/(2ℓ²) ゆえ γ を ℓ に変換する (共有 Kernel は ℓ ベース・Phase 75.15)。
rbfParams :: Double -> KernelParams
rbfParams gamma = defaultKernelParams { kpLengthScale = sqrt (1 / (2 * gamma)) }
spec :: Spec
spec = describe "Hanalyze.Model.SVM (Phase 75.11/75.15)" $ do
let (xl, yl) = linData
(xr, yr) = ringData
it "Linear カーネル: 線形分離データを訓練精度 100%" $ do
let m = fitSVM defaultSVM { svmKernel = Linear } xl yl
trainAcc m xl yl `shouldBe` 1.0
it "RBF カーネル: 同心 (線形非分離) を訓練精度 100%" $ do
let m = fitSVM defaultSVM
{ svmKernel = RBF, svmParams = rbfParams 0.5, svmC = 10 } xr yr
trainAcc m xr yr `shouldBe` 1.0
it "サポートベクタはスパース (α>0 の点 < 全点数)" $ do
let m = fitSVM defaultSVM
{ svmKernel = RBF, svmParams = rbfParams 0.5 } xr yr
numSupportVectors m `shouldSatisfy` (< LA.rows xr)
numSupportVectors m `shouldSatisfy` (> 0)
it "決定的 (同入力で 2 回 fit して SV 数一致)" $ do
let cfg = defaultSVM { svmKernel = RBF, svmParams = rbfParams 0.5 }
m1 = fitSVM cfg xr yr
m2 = fitSVM cfg xr yr
numSupportVectors m1 `shouldBe` numSupportVectors m2
it "多クラス (one-vs-rest): 3 クラスを訓練上で正しく分類" $ do
let x3 = LA.fromLists [[0,0],[0.2,0.1],[5,0],[5,0.3],[0,5],[0.1,5]]
y3 = VU.fromList [0,0,1,1,2,2]
mm = fitSVMMulti defaultSVM
{ svmKernel = RBF, svmParams = rbfParams 0.3, svmC = 10 } x3 y3
pr = predictSVMMulti mm x3
VU.toList pr `shouldBe` VU.toList y3
-- Phase 75.20: k-fold CV グリッド自動調律 (tuneSVM)。
describe "tuneSVM (CV グリッド自動最適化・決定的)" $ do
-- 同心 2 クラスを十分なサンプルで生成 (CV に必要な点数を確保)。
let ringBig =
( LA.fromLists $ inner ++ outer
, VU.fromList (replicate (length inner) 0 ++ replicate (length outer) 1) )
inner = [ [0.3 * cos t, 0.3 * sin t] | t <- angles ]
outer = [ [3.0 * cos t, 3.0 * sin t] | t <- angles ]
angles = [ 2 * pi * fromIntegral i / 12 | i <- [0 .. 11 :: Int] ]
(xb, yb) = ringBig
grid = defaultSVMTuneGrid { svmtCs = [1, 10], svmtLengths = [0.5, 1, 2], svmtFolds = 3 }
it "決定的: 同入力で 2 回呼んで同じ config/score" $ do
let (c1, s1) = tuneSVM defaultSVM grid xb yb
(c2, s2) = tuneSVM defaultSVM grid xb yb
svmC c1 `shouldBe` svmC c2
kpLengthScale (svmParams c1) `shouldBe` kpLengthScale (svmParams c2)
s1 `shouldBe` s2
it "非線形 (同心) で CV accuracy が高い (> 0.8) 構成を選ぶ" $ do
let (_best, score) = tuneSVM defaultSVM grid xb yb
score `shouldSatisfy` (> 0.8)
it "選ばれた config で全データ再学習すると訓練精度 100%" $ do
let (best, _) = tuneSVM defaultSVM grid xb yb
m = fitSVMMulti best xb yb
pr = predictSVMMulti m xb
VU.toList pr `shouldBe` VU.toList yb