shikumi-optimize-0.2.0.0: src/Shikumi/Optimize/RandomSearch.hs
-- | Bootstrap few-shot with random search (EP-23, DSPy's
-- @BootstrapFewShotWithRandomSearch@): run V1's 'bootstrapFewShot' several times with
-- different deterministic seeds — each shuffling the trainset and picking a random
-- demo count — score each resulting program, and keep the best. A zero-shot baseline
-- candidate is always included, so the search can never do worse than zero-shot.
--
-- This is a /wrapper/ over V1's bootstrap, not a re-implementation (MasterPlan
-- integration point #4). Randomness is a deterministic LCG (glibc constants),
-- mirroring "Shikumi.Optimize.Ensemble", so runs are reproducible with no IO entropy.
module Shikumi.Optimize.RandomSearch
( bootstrapRandomSearch,
bootstrapRandomSearchWith,
RandomSearchConfig (..),
defaultRandomSearchConfig,
)
where
import Control.Lens ((&), (.~))
import Data.Aeson (ToJSON)
import Data.Generics.Labels ()
import Data.List (sortBy)
import Data.Ord (comparing)
import Shikumi.Compile.Types (compiledProgram)
import Shikumi.Eval (Dataset, dataset, datasetExamples)
import Shikumi.Optimize.Bootstrap (bootstrapFewShotWith, defaultBootstrapConfig)
import Shikumi.Optimize.Search (freezeProgram, scoreOn, selectBest)
import Shikumi.Optimize.Types (Budget (..), Optimizer (..), Scored (..))
import Shikumi.Program (Program)
-- | Random-search tunables. (The bootstrap pass threshold is left at
-- 'defaultBootstrapConfig'\'s @1.0@ — keep only exactly-correct teacher runs.)
data RandomSearchConfig = RandomSearchConfig
{ -- | lower bound on the per-seed random demo count
minDemos :: !Int,
-- | upper bound on the per-seed random demo count
maxDemos :: !Int
}
deriving stock (Eq, Show)
-- | Demo count between 1 and 4.
defaultRandomSearchConfig :: RandomSearchConfig
defaultRandomSearchConfig = RandomSearchConfig {minDemos = 1, maxDemos = 4}
-- | A deterministic linear-congruential stream (glibc constants), duplicated by
-- intent from "Shikumi.Optimize.Ensemble" (which keeps it module-private).
lcg :: Int -> [Int]
lcg s0 = drop 1 (iterate step s0)
where
step s = (1103515245 * s + 12345) `mod` 2147483648
-- | A deterministic seed-dependent permutation of the trainset (each example seen at
-- most once), so different seeds give different bootstrap subsets.
shuffle :: Int -> Dataset i o -> Dataset i o
shuffle seed train =
let exs = datasetExamples train
keys = take (length exs) (lcg (seed + 1))
in dataset (map snd (sortBy (comparing fst) (zip keys exs)))
-- | The per-seed demo count, in @[minDemos .. maxDemos]@.
sizeFor :: RandomSearchConfig -> Int -> Int
sizeFor cfg seed =
let lo = minDemos cfg
hi = max lo (maxDemos cfg)
span' = hi - lo + 1
in case lcg seed of
(x : _) -> lo + (x `mod` span')
[] -> lo
-- | 'bootstrapRandomSearch' with explicit tunables.
bootstrapRandomSearchWith ::
(ToJSON i, ToJSON o) =>
RandomSearchConfig ->
-- | teacher
Program i o ->
-- | numCandidates: random seeds to try
Int ->
Budget ->
Optimizer i o
bootstrapRandomSearchWith cfg teacher numCandidates budget = Optimizer $ \train metric student -> do
let seeds = [1 .. max 1 numCandidates]
candidateFor seed = do
let cfg' = defaultBootstrapConfig & #maxBootstrappedDemos .~ sizeFor cfg seed
opt = bootstrapFewShotWith cfg' teacher budget
compiledProgram <$> runOptimizer opt (shuffle seed train) metric student
seeded <- mapM candidateFor seeds
let cands = student : seeded -- zero-shot baseline first
best <- selectBest budget (scoreOn train metric) cands
pure $ case best of
Nothing -> freezeProgram student
Just sc -> freezeProgram (candidate sc)
-- | Run V1 bootstrap over @numCandidates@ random seeds plus a zero-shot baseline,
-- score each on the dataset, and keep the best-scoring 'CompiledProgram'.
bootstrapRandomSearch ::
(ToJSON i, ToJSON o) =>
Program i o ->
Int ->
Budget ->
Optimizer i o
bootstrapRandomSearch = bootstrapRandomSearchWith defaultRandomSearchConfig