Grid Search tunes hyperparameters by brute force: list a few candidate values for each hyperparameter, train and score a model for every combination, and keep the best one.
Task: write grid_search(grid, score) and return a tuple (best_params, best_score, evaluations).
grid is a dict that maps each hyperparameter's name to its list of candidate values, for example {"k": [1, 3, 5], "weighting": ["uniform", "distance"]}. It can hold any number of hyperparameters.score is a function. Call it with one combination, as a dict {name: value} covering every hyperparameter, and it returns that model's validation accuracy. Higher is better.grid is the outermost loop and changes slowest, and the last one is the innermost and changes fastest. Each list is tried in the order given.Return best_params as a dict with its keys in grid's order, best_score rounded to 4 decimal places, and evaluations, the number of times you called score.
That last number is the real cost of Grid Search. Every one of those calls stands for a full model trained and validated.