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.6109 | 22.9% |
| 8 | 1.6330 | 31.1% |
| 20 | 1.7977 | 33.0% |
| 30 | 1.8930 | 37.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
Wie das funktioniert
Ein kleines neuronales Netz, das deutsche Ortsnamen erfindet, und ein Quiz, in dem du rätst welche es wirklich gibt. Hier steht wie das entstanden ist, in der Reihenfolge in der es passiert ist.
1 Die Daten sahen einfach aus
Das Gemeindeverzeichnis vom Statistischen Bundesamt ist frei und hat ungefähr 11.000 Gemeindenamen. Kein Scraping, keine API. Gute Größe für ein kleines Modell.
Dann hab ich es aufgemacht. Es ist eine einzige Spalte. Bundesländer, Landkreise, Verwaltungsgemeinschaften und echte Dörfer stehen alle in derselben Liste, und nichts markiert was was ist. Dithmarschen ist ein Landkreis mit 130.000 Einwohnern. Averlak ist ein Dorf mit ungefähr 500. Gleiche Spalte, kein Unterschied.
Das ist ein echtes Problem. Wenn man auf allem trainiert, lernt das Modell sowas wie Geest und Marsch Südholstein zu produzieren. Das ist ein Verwaltungsgebilde. Da wohnt niemand, und nach Dorf klingt es auch nicht.
Zwei Sachen haben mich da rausgeholt.
Erstens: ein Name der zwei- oder dreimal hintereinander auftaucht ist garantiert eine echte Gemeinde, weil er auf mehreren Verwaltungsebenen gleichzeitig steht. Damit waren 3.374 sofort bewiesen, umsonst.
Zweitens: Destatis markiert einen großen Teil vom Rest.
(VGem) für bayerische Verwaltungsgemeinschaften,
gemfr. Gebiet für unbewohntes Land. 782 Zeilen sind
so rausgeflogen.
Der nervige Fall war bayerischer Forst. Ungefähr 80 unbewohnte Forstgebiete haben überhaupt keine Markierung, und die meisten tauchen zweimal auf, also haben beide Signale gesagt "das ist eine Gemeinde". Das Einzige was sie unterscheidet ist die Form des Namens.
Leerer Wald
Ebersberger Forst
Perlacher Forst
Gramschatzer Wald
Echte Dörfer
Aicha vorm Wald
Grub a.Forst
Forst
Das Muster ist das Adjektiv. Ein einzelnes Wort auf -er vor Forst oder Wald ist leeres Land. Alles andere ist ein Ort. Diese eine Regel sortiert alle 100 Kandidaten richtig.
Rausgekommen sind 10.729 Namen, 110.491 Zeichen. Vorher hatte ich "ungefähr 10.700" geschätzt, das passte also.
Sauber ist es trotzdem nicht ganz. Ungefähr 290 Landkreisnamen überleben, weil sie nichts markiert und sie wie normale Ortsnamen klingen. Das sind etwa 2,7% der Daten. Schreibe ich lieber hin als so zu tun als wäre es nicht so.
2 Das Training
Es läuft Zeichen für Zeichen. Das Modell liest einen Namen Buchstabe für Buchstabe und rät den nächsten. Macht man das 110.000 mal, kriegt es mit wie deutsche Ortsnamen gebaut sind, dass -ingen und -heim und -büttel Endungen sind, dass bestimmte Buchstaben aufeinander folgen und andere nie.
273.000 Parameter, trainiert in ungefähr zwei Minuten auf einer Laptop-CPU. Winzig nach heutigen Maßstäben, und genau das ist der Punkt.
110.000 Zeichen sind sehr wenig Daten. Das Problem hier ist deshalb nicht Overfitting im üblichen Sinn, sondern dass das Modell die Liste einfach auswendig lernt und mir echte Namen zurückgibt.
3 Womit ich nicht gerechnet hatte
Normalerweise schaut man auf den Validation Loss und hört auf, wenn er wieder schlechter wird. Hier ist das bei Epoche 5.
Ich wollte aber, dass das Modell gezielt bayerische oder friesische Namen produziert, also hab ich das separat gemessen. Und diese Zahl ist noch lange weitergestiegen, als der Validation Loss schon schlecht war.
| Epoche | Validation Loss | Region richtig |
|---|---|---|
| 5 (Lehrbuch) | 1,6109 | 22,9% |
| 8 | 1,6330 | 31,1% |
| 20 | 1,7977 | 33,0% |
| 30 | 1,8930 | 37,1% |
Der Punkt an dem man laut Lehrbuch aufhört gibt einem also das schlechteste regionale Verhalten. Ergibt Sinn wenn man kurz drüber nachdenkt: der Validation Loss misst vor allem "sieht das nach Deutsch aus", und das lernt es früh. Der regionale Charakter ist ein kleineres Signal obendrauf, und das kommt später.
Epoche 30 hab ich trotzdem nicht genommen, da lernt es schon zu stark auswendig. Die Regel ist jetzt: nimm die letzte Epoche, die noch innerhalb von 2% vom besten Validation Loss liegt. Das landet auf Epoche 9. Kostet 1,6% Validation Loss, bringt 8 Punkte regionale Genauigkeit. Guter Tausch.
4 Regionen, und wo es auseinanderfällt
Der naheliegende Weg für "mach es bayerisch" wäre, pro Bundesland eine eigene Kopie des Modells nachzutrainieren. Das geht hier nicht. Rheinland-Pfalz hat 2.267 Namen, das Saarland 52. Die kleinen Kopien würden ihre Handvoll auswendig lernen, und keine würde teilen was sie alle über deutsche Wortbildung wissen.
Also stattdessen ein Modell mit einem gelernten Regionsvektor, der an jedes Zeichen drangeklebt wird. Gleiches Ziel, und funktioniert bei dieser Größe wirklich.
Um zu prüfen ob es was bringt, hab ich einen kleinen Klassifikator auf den echten Namen trainiert und ihn dann die Region der erfundenen raten lassen. Wenn die Konditionierung irgendwas tut, müsste er das treffen was ich angefragt habe. Insgesamt stimmt er in 31% der Fälle zu. Raten wäre bei 13 Ländern 7,7%, und der Klassifikator schafft auf echten Namen nur 67%, das ist also die eigentliche Obergrenze.
Rheinland-Pfalz57%
Bayern53%
Mecklenburg-Vorp.52%
Schleswig-Holstein51%
Baden-Württemberg38%
Niedersachsen32%
Nordrhein-Westfalen17%
Saarland1%
Das Saarland fällt komplett durch, und das kriegt auch kein besseres Modell hin. 52 Namen reichen nicht, damit irgendeine Endung oft genug vorkommt um charakteristisch zu sein. Da ist nichts zu lernen. Nordrhein-Westfalen ist aus einem anderen Grund schlecht: die Namen dort sind einfach gemischt, keine Endung sticht deutlich über den Bundesschnitt heraus.
Wo es funktioniert, sieht man es auch ohne Klassifikator. Frag nach Schleswig-Holstein und du kriegst Brendsbüttel, Süderrich, Niederkogg. Frag nach Bayern und es kommen Burg am Riching und Bad Schleißlach. Diese Endungen sind echte Geschichte, keine Dekoration: -büttel und -koog sind norddeutsch, -ing ist bayerisch, -itz im Osten kommt aus slawischer Besiedlung.
5 Wie es in deinen Browser kommt
Hinter dieser Seite steckt kein Server. Das Modell liegt gerade in deinem Tab. Ich hab die Gewichte als 1 MB Zahlenblock exportiert und das Modell in ungefähr 120 Zeilen normalem JavaScript nachgebaut, damit alles lokal läuft und das Hosting nichts kostet.
Der Haken: zwei Details an einem GRU kann man leicht falsch machen, und man sieht keinen der beiden Fehler am Ergebnis. Ein falsch nachgebautes Modell produziert immer noch deutsch aussehende Namen. Durch Lesen findet man das nie.
Deswegen gibt es einen Test, der dieselbe Eingabe durch das Python-Modell und durch das JavaScript-Modell schickt und prüft ob die Zahlen auf vier Nachkommastellen übereinstimmen. Ich hab die falsche Variante extra einmal gebaut, um zu sehen ob der Test sie fängt. Sie weicht um 3,44 ab, bei einer Schwelle von 0,0001. Also ja.
6 Das Quiz
Zehn Namen, fünf echte und fünf erfundene, gemischt. Die echten werden zufällig aus der ganzen Liste gezogen und nicht auf obskure gefiltert, weil das die Sache in die falsche Richtung einfach machen würde.
Und das ist eigentlich der Witz an dem Projekt. Das Modell hat nie etwas so Unwahrscheinliches produziert wie die echten Namen. Elend und Sorge liegen im Harz wirklich nebeneinander. Kotzen liegt in Brandenburg. Petting liegt in Bayern. Es gibt einen Ort namens Ebershauser-Nattenhauser Wald.
Da kommt nichts hin was ich trainiere.
7 Was noch offen ist
Die 290 Landkreisnamen sind immer noch drin. Der Klassifikator wird auf denselben Namen gemessen, auf denen er trainiert wurde, die 67% sind also eine optimistische Obergrenze und ein sauberer Holdout wäre ehrlicher. Die Gewichte liegen in voller Genauigkeit vor, ein Viertel davon würde vermutlich reichen. Und es fehlt der Vergleich gegen eine einfache statistische Baseline, der zeigen würde wie viel der neuronale Teil überhaupt bringt.
Code und die lange Fassung: github.com/builtbymatti/unort