shikumi-optimize-0.2.1.0: src/Shikumi/Optimize/LabeledFewShot.hs
-- | M1 — labeled few-shot demo selection, the simplest real optimizer (DSPy's
-- @LabeledFewShot@). Each training example /is/ a candidate demonstration; the
-- search forms several size-@k@ demo sets, scores each by attaching it to every
-- node and evaluating over the training set, and keeps the highest-scoring set.
--
-- No LM calls are made beyond scoring. Candidate sets are enumerated
-- deterministically (all size-@k@ combinations of the training demos, in a fixed
-- order) rather than randomly sampled. 'labeledFewShotWith' reserves one scoring
-- cost per candidate before evaluation and stops at the shared 'Budget'; the
-- default 'labeledFewShot' uses 'defaultBudget'.
module Shikumi.Optimize.LabeledFewShot
( labeledFewShot,
labeledFewShotWith,
labeledCandidateSets,
withDemos,
)
where
import Control.Lens ((&), (.~))
import Data.Aeson (ToJSON, toJSON)
import Data.Generics.Labels ()
import Shikumi.Eval (Dataset, Example (..), datasetExamples)
import Shikumi.Optimize.Search (freezeProgram, meteredScore, newBudgetMeter, selectBestMetered)
import Shikumi.Optimize.Types (Budget, Optimizer (..), Scored (..), defaultBudget)
import Shikumi.Program (Demo (..), Program, mapParams)
-- | Select the best size-@k@ set of labelled demonstrations from the training set.
-- Multi-node programs receive the same demo set at every node (the DSPy default).
labeledFewShot :: (ToJSON i, ToJSON o) => Int -> Optimizer i o
labeledFewShot = labeledFewShotWith defaultBudget
-- | Select the best size-@k@ labelled demo set under an explicit budget.
labeledFewShotWith :: (ToJSON i, ToJSON o) => Budget -> Int -> Optimizer i o
labeledFewShotWith budget k = Optimizer $ \train metric prog -> do
meter <- newBudgetMeter budget
let sets = labeledCandidateSets k train
best <- selectBestMetered meter (\ds -> meteredScore meter train metric (withDemos ds prog)) sets
pure $ case best of
Nothing -> freezeProgram prog
Just sc -> freezeProgram (withDemos (candidate sc) prog)
-- | The candidate demo sets a 'labeledFewShot' search considers: every size-@k@
-- combination of the training examples (each turned into a JSON 'Demo'), in
-- deterministic enumeration order. Exposed so tests can reproduce the exact set of
-- candidates the optimizer scored.
labeledCandidateSets :: (ToJSON i, ToJSON o) => Int -> Dataset i o -> [[Demo]]
labeledCandidateSets k train = combinations k allDemos
where
allDemos =
[ Demo {input = toJSON i, output = toJSON o}
| Example i o <- datasetExamples train
]
-- | Attach a demo set to every node, leaving each node's instruction untouched.
withDemos :: [Demo] -> Program i o -> Program i o
withDemos ds = mapParams (\ps -> ps & #demos .~ ds)
-- | All size-@k@ sub-lists of a list, preserving relative order
-- (@combinations 2 [a,b,c] = [[a,b],[a,c],[b,c]]@).
combinations :: Int -> [a] -> [[a]]
combinations k _ | k <= 0 = [[]]
combinations _ [] = []
combinations k (x : xs) = map (x :) (combinations (k - 1) xs) ++ combinations k xs