unort

How this works

A small neural network that invents German place names, plus a quiz where you guess which ones are real. Here is how it got built, in the order it actually happened.

1 The data looked easy, and was not

The Gemeindeverzeichnis from the Statistisches Bundesamt is free and has around 11,000 municipality names. No scraping, no API. Good size for a small model.

Then I opened it. It is one single column. Federal states, districts, administrative unions and actual villages all sit in the same list with nothing marking which is which. Dithmarschen is a district of 130,000 people. Averlak is a village of about 500. Same column, no difference between them.

That is a real problem, because if you train on all of it the model learns to produce things like Geest und Marsch Südholstein. That is an administrative body. Nobody lives there, and it does not sound like a village.

Two things got me out of it.

First: a name that appears two or three times in a row is guaranteed to be a real municipality, because it shows up at several administrative levels at once. That proved 3,374 of them outright, for free.

Second: Destatis marks a lot of the rest. (VGem) for Bavarian administrative unions, gemfr. Gebiet for uninhabited land. 782 rows left that way.

The annoying case was Bavarian forest. Around 80 uninhabited forest tracts carry no marking at all, and most of them appear twice, so both of my signals said "this is a municipality". The only thing separating them is the shape of the name.

Empty woodland

Ebersberger Forst
Perlacher Forst
Gramschatzer Wald

Actual villages

Aicha vorm Wald
Grub a.Forst
Forst

The pattern is the adjective. A single word ending in -er in front of Forst or Wald is empty land. Everything else is a place. That one rule sorts all 100 candidates correctly.

What came out: 10,729 names, 110,491 characters. Before I started I had guessed "about 10,700", so that lined up.

It is still not perfectly clean. Roughly 290 district names survive because nothing marks them and they read like ordinary place names. That is about 2.7% of the data. I would rather write that down than pretend it is not there.

2 Training the thing

It works one character at a time. The model reads a name letter by letter and guesses the next letter. Do that 110,000 times and it picks up how German place names are put together, that -ingen and -heim and -büttel are endings, that certain letters follow each other and others never do.

273,000 parameters, trains in about two minutes on a laptop CPU. Tiny by any modern standard, and that is the point.

110,000 characters is very little data. So the thing that goes wrong here is not overfitting in the usual sense, it is the model simply memorising the list and handing real names back to me.

3 The part I did not expect

The standard move is to watch the validation loss and stop when it starts getting worse. Here that happens at epoch 5.

But I also wanted the model to produce a specifically Bavarian or specifically Frisian name, so I measured that separately. And that number kept climbing long after validation loss had gone bad.

Epoch Validation loss Gets the region right
5 (textbook stop)1.610922.9%
81.633031.1%
201.797733.0%
301.893037.1%

So the textbook stopping point gives you the worst regional behaviour. Which makes sense once you sit with it: validation loss is mostly measuring "does this look like German", and that gets learned early. The regional character is a smaller signal sitting on top of it, and it arrives later.

I did not just take epoch 30, because by then it is memorising badly. The rule now is: keep the last epoch still within 2% of the best validation loss. That lands on epoch 9. Costs 1.6% of validation loss, buys 8 points of regional accuracy. Good trade.

4 Regions, and where it falls apart

The obvious way to do "make it Bavarian" would be to fine-tune a separate copy of the model per state. That does not work here. Rheinland-Pfalz has 2,267 names and Saarland has 52. The small copies would just memorise their handful and none of them would share what they all know about how German words go together.

So it is one model with a learned region vector instead, glued onto every character as it reads. Same goal, and it actually works at this size.

To check whether it works I trained a separate small classifier on the real names, then asked it to guess the region of the invented ones. If the conditioning does anything, its guess should match what I asked for. Overall it agrees 31% of the time. Random guessing across 13 states would be 7.7%, and the classifier only manages 67% on real names, so that is the actual ceiling.

Rheinland-Pfalz57%

Bayern53%

Mecklenburg-Vorp.52%

Schleswig-Holstein51%

Baden-Württemberg38%

Niedersachsen32%

Nordrhein-Westfalen17%

Saarland1%

Saarland fails completely, and that one is not fixable with a better model. 52 names is not enough for any ending to show up often enough to be characteristic. There is nothing to learn. Nordrhein-Westfalen is bad for a different reason: the names there are just mixed, no ending stands out much above the national average.

Where it does work you can see it without any classifier. Ask for Schleswig-Holstein and you get Brendsbüttel, Süderrich, Niederkogg. Ask for Bayern and you get Burg am Riching and Bad Schleißlach. Those endings are real history, not decoration: -büttel and -koog are northern, -ing is Bavarian, -itz in the east comes from Slavic settlement.

5 Getting it into your browser

There is no server behind this page. The model is sitting in your tab right now. I exported the weights as a 1 MB block of numbers and rewrote the model in about 120 lines of plain JavaScript, so the whole thing runs locally and costs nothing to host.

The catch: two details of how a GRU works are easy to get wrong, and neither mistake is visible in the output. A wrongly rebuilt model still produces German looking names. You would never catch it by reading them.

So there is a test that runs the same input through the original Python model and through the JavaScript one and checks the numbers match to four decimal places. I also deliberately built the wrong version to confirm the test would actually catch it. It diverges by 3.44 against a threshold of 0.0001, so yes.

6 The quiz

Ten names, five real and five invented, shuffled. The real ones are picked at random from the whole list and not filtered down to obscure ones, because that would make it easy in the wrong direction.

Which is the actual joke of this project. The model has never produced anything as unlikely as the real ones. Elend and Sorge, which translate to Misery and Sorrow, sit next to each other in the Harz. Kotzen is in Brandenburg. Petting is in Bavaria. There is a village called Ebershauser-Nattenhauser Wald.

Nothing I trained is going to beat that.

7 What is not done

The 290 district names are still in the corpus. The regional classifier is measured on the same names it was trained on, so 67% is an optimistic ceiling and a proper held-out split would be more honest. The weights are shipped as full precision floats when a quarter of that would probably do. And there is no comparison against a plain statistical baseline yet, which would tell me how much the neural part is really earning.

Code and the full write-up: github.com/builtbymatti/unort