🎡 HK Playground 🎢

Practical, code-tested notes on data analysis, machine learning, and the problems that come up in real projects.

Latest posts

What is a chat template? The string your LLM actually reads

What is a chat template? The string your LLM actually reads

A chat model never sees your list of messages. apply_chat_template turns it into one string with control tokens, and Qwen2.5 adds a system prompt you did not write. Without the template the same model rambled and added wrong facts; without the generation prompt it wrote the user’s turn. Tested locally with Qwen2.5-0.5B-Instruct.

You send a chat model a list of messages with roles. The model cannot read a list. It reads one sequence of tokens, and the chat template is the rule that turns your messages into that sequence: where each turn starts and ends, which special tokens mark the roles, and what to put at the end so the model knows it is its turn to speak.

How to choose a classification threshold from error costs (scikit-learn TunedThresholdClassifierCV)

How to choose a classification threshold from error costs (scikit-learn TunedThresholdClassifierCV)

On the German Credit data, a logistic regression at the default 0.5 threshold cost 0.866 per applicant, more than rejecting everyone (0.700). The threshold from the cost matrix, 1/6, cut it to 0.545, and TunedThresholdClassifierCV found the same from training data alone. Tuning on the test set looked better than it was.

A classifier’s predict turns a probability into a yes or no at 0.5. That threshold is only right when both kinds of mistake cost the same. On a credit dataset where approving a bad applicant costs five times as much as rejecting a good one, the default threshold did worse than a rule that rejects every applicant:

What is perplexity? Computing it by hand with a small local model

What is perplexity? Computing it by hand with a small local model

Perplexity is exp of the average negative log-probability per token. We computed it by hand with Qwen2.5-0.5B, matched it to the library’s loss, and scored five texts: a normal sentence 35.1, the same words shuffled 1,142.8, Python code 4.0. Why random letters scored lower than the shuffled sentence, and what that says about comparing numbers.

Perplexity measures how surprised a language model is by a text. For every token, take the probability the model gave to the token that actually came next, then:

Recursive vs direct multi-step forecasting: what a one-step score hides

Recursive vs direct multi-step forecasting: what a one-step score hides

A model with lag features scored MAE 24.5 one hour ahead. Forecasting the next 24 hours, the same model averaged 45.7. Recursive, direct and “no short lag” strategies compared on real hourly data with scikit-learn, with code.

In the KFold vs TimeSeriesSplit post, a gradient-boosting model with lag features scored an MAE of 24.6 bikes per hour on the 8 weeks after its training data. That number came with a caveat: every prediction used the real demand of the previous hour. It measured predicting the next hour, not forecasting a day.

Orca review: Claude Code and Codex side by side, and one agent handing work to the other

Orca review: Claude Code and Codex side by side, and one agent handing work to the other

A hands-on look at Orca, an open-source desktop app for running CLI coding agents side by side. What it does, how its orchestration commands let Claude Code hand a code review to Codex, what that review found, and the one setting to check before you rely on it.

Orca is an open-source (MIT) desktop app for running several CLI coding agents at once. Its README lists Codex, Claude Code, OpenCode and Pi, on macOS, Windows and Linux, and each agent runs with your own subscription. Orca does not ask for an API key.

KFold vs TimeSeriesSplit: how shuffled cross-validation hides time-series leakage

KFold vs TimeSeriesSplit: how shuffled cross-validation hides time-series leakage

On two years of hourly bike rentals, shuffled KFold estimated MAE 24.5 while the next 8 weeks gave 54.2. When TimeSeriesSplit helps, why lag features change the picture, and the rolling-feature leak no split can catch.

Take one gradient-boosting model, two years of hourly bike rentals, and four cross-validation schemes. Shuffled KFold estimates the mean absolute error (MAE) at 24.5 bikes per hour. Score the same model on the 8 weeks that come after the training data and you get 54.2, more than twice as bad. TimeSeriesSplit estimated 43.4, much closer.

fp32, fp16, bf16 and int8: what each format stores, and where it breaks

fp32, fp16, bf16 and int8: what each format stores, and where it breaks

Models are stored in 32, 16 or 8 bits. We checked with PyTorch what each format does with the same numbers: fp16 turns 70,000 into infinity, bf16 cannot tell 1.001 from 1, and adding 0.001 ten thousand times stops at 4 in fp16 and at 0.5 in bf16. One outlier made int8 error 12 times worse.

A model’s weights are just numbers, and how many bits each number gets decides how much memory the model needs. For the 494 million parameters of Qwen2.5-0.5B:

Bit tricks in Python, one bit at a time: why ~5 is -6 and how to get 8-bit results

Bit tricks in Python, one bit at a time: why ~5 is -6 and how to get 8-bit results

Python integers have no fixed width, so ~5 is -6 and 152 « 2 is 608. An 8-bit visualizer shows what the same operations do inside a fixed word: two’s complement, logical and arithmetic shifts, x & (x - 1), x & -x and the XOR trick, each checked against plain Python.

Try this in Python:

Greedy decoding vs beam search: why the most likely next word can give a worse answer

Greedy decoding vs beam search: why the most likely next word can give a worse answer

Greedy decoding picks the most likely token at every step. We compared it with beam search on a local Qwen2.5-0.5B for 10 prompts: beam search found a more probable continuation in 8 of 10, and on one prompt greedy’s locally best word led to a wrong answer. Higher probability was not always better text, either.

When do_sample=False, a language model uses greedy decoding: at every step it takes the single most likely next token and never looks back. That is a greedy algorithm in the exact sense of our greedy algorithms post, and it fails the same way: the best-looking step now can lead to a worse result overall.

When does a greedy algorithm fail? Coins, meetings and knapsacks, checked against the optimum

When does a greedy algorithm fail? Coins, meetings and knapsacks, checked against the optimum

Three greedy rules run next to an exact answer in a small pygame app. Greedy change-making used 3 coins where 2 were enough, “earliest start” booked 1 meeting where 3 fit, and value-per-weight packing got 32 where 44 was possible. The rules that do work are just as concrete.

A greedy algorithm makes the choice that looks best right now and never takes it back. It is fast and easy to write. Sometimes it is also the optimal answer, and sometimes it is not, and the difference is not obvious from the code.

Pagination