July 24, 2026: Harmonizing Bach chorales with a Transformer (a machine-learning side project)

A short detour from number theory. In my spare time I built ChoraleHarmonizer, a small program that takes a soprano melody and writes the three lower voices (alto, tenor, and bass) in the style of Johann Sebastian Bach’s four-part chorales. Bach left several hundred of these harmonizations, and reconstructing the lower voices from a given melody is a classic music-theory exercise, so there is both ample training material and a clear notion of what a good answer looks like.

The model. At its core is a Transformer, the same kind of neural network that powers modern language models. It is trained on the 343343 four-part chorales in the music21 corpus, each transposed into all twelve keys. Music is turned into text on a sixteenth-note grid: at each step the sounding pitches are listed, a token _ holds the previous note, | marks a bar line, and ^ a fermata (the held notes that close each phrase). The soprano is woven into the sequence the model predicts, so that at every position it sees the melody, the metre, and the phrase endings before committing to the harmony. The network is deliberately small (about 1515 million parameters) and trained from scratch: fine-tuning a large text model would buy nothing once its word vocabulary is replaced by musical tokens.

Why a neural net alone is not enough. A few thousand chorales is a tiny dataset by machine-learning standards, and the raw model, left to its own devices, writes harmony that sounds mostly right but keeps breaking textbook rules, above all parallel fifths and octaves, the cardinal sin of chorale writing. The interesting part of the project is therefore not the network but everything wrapped around it: a layer of hand-coded, classical music theory that steers the generation. This is the “manual processing” that turns a plausible imitator into a passable one.

What that processing does. Four things happen while the harmony is generated, from hard rules to soft preferences:

  1. Constraints. The soprano and all structural symbols are fixed; each lower voice is confined to the range it actually occupies in Bach; and at every fermata all four voices must sound the cadence chord, as they do in over 99%99\,\% of Bach’s cadences.

  2. Nudges while sampling. As each note is chosen, pitches that would form a parallel fifth or octave with an already-written voice are penalized on the spot (this single check cuts the parallel-fifth rate from 5.15.1 to 0.20.2 per 100100 sixteenth notes, near Bach’s own 0.180.18), and the inner voices are discouraged from merely re-striking a note they could instead hold.

  3. Best of many. For each phrase the program samples a batch of complete harmonizations and keeps the one that maximizes the model’s own confidence minus a music-theory penalty; fixing the winning phrase before sampling the next explores exponentially many combinations cheaply.

  4. Repair. Finally it locates the single worst remaining rule violation, re-rolls the harmony from just before it, and accepts the result only if the overall score improves.

Rules calibrated against Bach, not a textbook. The rule book has about twenty entries (parallel and hidden fifths, voice crossings and overlaps, augmented seconds, unresolved tritone leaps, doubled thirds, harmonic stagnation, and so on), but the penalties are not set by dogma. Each is fixed by how much more often the raw model breaks the rule than Bach does, measured directly on the corpus; rules that Bach himself bends now and then cost little or nothing, and the penalty mass concentrates on the model’s genuine weaknesses. Even the exceptions are discovered rather than assumed: where two “violations” almost always occur together in Bach’s own writing (a hidden fifth reached by similar motion, say, which he does some forty times more often than chance would predict), they are counted as a single incident instead of punished twice.

A style dial: conservative, balanced, bold. On top of the rules, two statistics learned from the corpus judge each candidate harmonization as a whole: a harmonic model of how each beat’s chord follows the previous one, and a texture model of how often each voice ought to move. The twist is what they optimize for. Rather than making the harmony as typical as possible (which would collapse everything toward a bland, average Bach), each term measures the distance from Bach’s own median level of surprise, so a setting that is too predictable is penalized just like one that is too wild. Shifting that target turns it into a dial: the presets konservativ, ausgewogen, and kuehn (conservative, balanced, bold) aim below, at, and above Bach’s median, so one can ask for anything from cautious textbook harmony to writing bolder than Bach’s median, all while staying inside the range of surprise he actually used.

Trying it out. The code is on GitHub, and two commands suffice:

python bach_chorales.py       # build the dataset from music21 (once)
python chorale_harmonizer.py  # train, then harmonize the test chorales

Adding --presets konservativ,ausgewogen,kuehn writes all three stylistic variants of each chorale; if a trained model is already present, the training step is skipped. Ready-made example outputs sit in the repository’s output/ folder: for each test chorale, Bach’s original harmonization beside the generated one (the three style variants where available), each as MIDI and MusicXML, openable in any notation software, so they can be compared bar by bar. Every other knob (sampling temperature, search width, the rule weights) lives in config.py.

Built with Claude, over many iterations. I wrote this project in close collaboration with Claude, Anthropic’s AI assistant, over a long series of iterations: more than a hundred commits that grew the rule catalog, made its weights data-driven, added the phrase-wise search, the repair step, and the style engine, and tried (and sometimes discarded) larger network variants. Even a small, self-contained machine-learning pipeline like this one turns out to need a surprising amount of patient back-and-forth to get right.