English

Der Judo-Wurf

Bisher ging jeder Weg in Richtung mehr Struktur. Zufall nimmt Struktur weg — also umgekehrt: die Strukturlosigkeit ins Extrem treiben und schauen, ob etwas anderes auftaucht.

25./26. August 2026 · 3 506 Instanzen, 76 501 + 3 551 Kandidaten, geeicht gegen Vertauschung

Der Zug ist nicht neu — und das ist die gute Nachricht

Die Umkehrung hat in mehreren Fächern gewonnen. Ramsey-Theorie: „Vollständige Unordnung ist unmöglich“[1] — treibt man eine Färbung ins Zufällige, entsteht Struktur zwangsläufig. Szemerédis Regularitätslemma: jeder Graph zerfällt in beschränkt viele Stücke, die zufällig aussehen. Erdős' probabilistische Methode: Objekte konstruieren, indem man zeigt, dass ein zufälliges taugt. Konzentration des Maßes: in hoher Dimension wird Zufall wieder starr.

Der Fall, auf den es hier ankommt, hat aber genau an diesem Gegenstand stattgefunden. Mézard, Parisi und Zecchina haben nicht nach Struktur in der Formel gesucht, sondern gefragt, wie die Unordnung aussieht[2]. Antwort: der Lösungsraum zerfällt bei αd ≈ 3,86 in exponentiell viele Cluster[3], lange vor der Schwelle bei αs ≈ 4,267[4]. Diese Anti-Struktur war die Struktur, und Survey Propagation hat damit zufälliges 3-SAT mit einer Million Variablen nahe der Schwelle gelöst, als sonst nichts mehr ging.

Und der Vorbehalt stand schon fest, bevor der Lauf begann. Survey Propagation ist ein Aggregat über den Belegungsraum — Randverteilungen mit Jokerzustand. Nach der Zwei-Achsen-Trennung können solche Aggregate SAT nicht von UNSAT trennen, und entsprechend ist SP einseitig: es findet Lösungen, es beweist nie Unerfüllbarkeit. Das Handbuch misst dasselbe von der anderen Seite: Kavität/SP trennt mit AUC 0,86, sagt die Härte aber nicht vorher (r ≤ 0,18).

Der Judo-Wurf ist also erlaubt — aber er erbt eine bekannte Halbblindheit. Das ist kein Grund, ihn nicht zu machen. Es ist der Grund, ihm vorher ein Abbruchkriterium mitzugeben statt einer Hoffnung.

Warum die Unordnung in die Darstellung wandert, nicht in die Instanz

Alle Beispiele oben nutzen den Zufall in der Instanz. Hier wandert er in das Messgerät — und das Gerät stand schon bereit: SAT im Phasenraum vermisst genau diesen Fluss, seine fraktalen Einzugsränder und die Beziehung α = κ/λ. Dort wurde er an einer Instanz verstanden; hier wird gefragt, ob er über viele Instanzen etwas über die Härte weiß. Ein Prozess wird auf der Formel laufen gelassen: ein Prozess wird auf der Formel laufen gelassen, und gemessen wird, wie unordentlich er sich benimmt. Das folgt einem Muster, das dieses Projekt zweimal selbst gemessen hat:

Beobachtung. Jede Dimension, die in der Komplexitätstheorie je getragen hat — Baumweite, Schaltkreistiefe, VC-Dimension, Bindungsdimension, Beweisweite — ist eine Eigenschaft eines Prozesses auf dem Objekt, nie eine innere Koordinate des Objekts.

Im Repo ist dasselbe zweimal passiert: die stumpfe Koordinate — die Formel als eigener Ort — ist gestorben; die Front der Stromzählung, eine Prozessgröße, lebt. Das sind nicht zwei unabhängige Befunde, sondern ein Muster, das sich selbst bestätigt.

Das Instrumentenbrett

Sechs Sonden, und die entscheidende gemeinsame Eigenschaft: keine ruft einen Löser. Das ist Bedingung, nicht Zufall — im Handbuch trennt jedes gute Verfahren SAT von UNSAT nur deshalb, weil es Erfüllbarkeitsfragen stellt, und ist damit zirkulär.

SondeWas sie misst
A  ChaosDie kontinuierliche Dynamik von Ercsey-Ravasz/Toroczkai[5] (Phasenraum). Auf UNSAT hat sie beweisbar keinen Attraktor — gemessen wird der Transient, mit festem Budget statt bis zur Lösung.
B  SpektrumAbstandsverhältnis der Eigenwerte[6]. Poisson 0,3863 (geordnet) gegen Wigner-Dyson 0,5307 (chaotisch)[7] — die kanonische Ordnung/Chaos-Sonde der Zufallsmatrixtheorie.
C  LokalisierungTeilnahmeverhältnis der Klauselgewichte: konzentriert die Dynamik die Schuld auf wenige Klauseln oder verschmiert sie?
D  ÜberlappDie q-Verteilung der Spingläser, aus abgeschnittenen Irrfahrten — Budget 12n, während eine Lösung im Median 1 990 Kippungen braucht. Sie darf nie entscheiden.
E  KompressionUnordnung als Inkompressibilität, getrennt nach Gerüst und Vorzeichen.
F  PerkolationAntwort der Einheitsfortpflanzung auf eine Störung — siehe unten.

Der Zwischenfund: einen Parameter nicht festhalten

Die erste Fassung von Sonde F setzte eine Variable und maß die Lawine der Einheitsfortpflanzung. Ergebnis: immer exakt 1, ohne jede Streuung. Das war kein Fehler im Code, sondern ein Befund — bei α = 4,267 liegt zufälliges 3-SAT weit unter der Perkolationsschwelle der Einheitsfortpflanzung. Von einer Variable aus läuft nichts.

Statt den Parameter festzuhalten, wurde er durchgefahren. Und dann erscheint etwas:

1 3 5 4,93 bei ρ = 0,16 .02 .10 .16 .24 .34 .46 Saatanteil ρ Verstärkung
Mitgerissene Variablen je Saatvariable, gegen den Anteil zufällig gesetzter Variablen. Ein scharfer Übergang bei ρ ≈ 0,16. Lage und Schärfe dieses Übergangs sind Prozessgrößen der Instanz, keine Zählwerte.

Wie man verhindert, dass die Suche sich selbst betrügt

Das ist der eigentliche Inhalt des Laufs. Wer 76 501 Kandidaten misst und den besten meldet, meldet das Maximum einer Stichprobe — und das ist auch dann deutlich von null verschieden, wenn jeder einzelne Kandidat reines Rauschen war.

Drei Sperren gegen Störgrößen

Gemessen wird die partielle Rangkorrelation gegen log2(Konflikte), mit {n, σgrad2, sat} herausgerechnet. Die Gradstreuung trägt in diesem Projekt 42 % der Härtestreuung; sat herauszurechnen ist die schärfste Sperre, denn dass UNSAT teurer ist, ist bekannt — wer nur das wiederfindet, hat nichts gefunden.

Die Vertauschungsnull über die ganze Kette

Dieselbe Suche wurde achtmal komplett auf gemischtem Etikett gefahren. Die Schwelle für einen Fund ist das Maximum daraus, nicht die Null.

StufeMittelMaximum
Stufe 1  (76 501 Kandidaten, nur lern)0,0940,117
Stufe 2  (beste 600 auf pruef)0,0720,086
gierig  (4 Terme, Vorwärtsauswahl)0,1010,130
Code & Daten ansehen — die Vertauschungsnull, 76 501 Kandidaten
judo/sieb.py — Rangkorrelation, Störgrössen-Projektion, gierige Verbindung
def korr(v, H, zr):
    """|Rangkorrelation| von v mit dem schon residualisierten Ziel zr."""
    r = B.rang(v)
    r = r - H @ r
    s = r.std()
    if s < 1e-12:
        return 0.0
    return abs(float(np.dot(r, zr) / (len(r) * s * zr.std())))


# ------------------------------------------------------------ Suchraum

def paare(Q, hoechstens=None, rng=None):
    ij = [(i, j) for i in range(Q) for j in range(i + 1, Q)]
    if hoechstens and len(ij) > hoechstens:
        rng = rng or np.random.default_rng(0)
        pick = rng.choice(len(ij), hoechstens, replace=False)
        ij = [ij[t] for t in pick]
    return ij


def verbinde(X, i, j, art):
    a, b = X[:, i], X[:, j]
    if art == "quot":
        n = np.abs(b)
        return a / np.where(n < 1e-9, 1e-9, np.where(b < 0, -n, n))
    if art == "prod":
        return a * b
    if art == "diff":                     # standardisierte Differenz
        za = (a - a.mean()) / (a.std() + 1e-12)
        zb = (b - b.mean()) / (b.std() + 1e-12)
        return za - zb
    raise ValueError(art)


ARTEN = ("quot", "prod", "diff")


def stufe1(X, H, zr, ij, arten=ARTEN, mitsingles=True):
    """Alle Kandidaten auf `lern` reihen. Gibt Liste (score, beschreibung)."""
    aus = []
    if mitsingles:
        for i in range(X.shape[1]):
            aus.append((korr(X[:, i], H, zr), ("einzeln", i, -1)))
    for (i, j) in ij:
        for art in arten:
            aus.append((korr(verbinde(X, i, j, art), H, zr), (art, i, j)))
    return aus


def baue(X, bez):
    art, i, j = bez
    return X[:, i] if art == "einzeln" else verbinde(X, i, j, art)


# ------------------------------------------------------------------ Lauf

def sammle_bank(substrate):
    with Pool(min(22, os.cpu_count() or 4)) as p:
        reihen = p.map(BK.bank, substrate, chunksize=16)
    namen = sorted(set().union(*[set(r) for r in reihen]))
    X = np.array([[r.get(k, 0.0) for k in namen] for r in reihen], np.float64)
    X[~np.isfinite(X)] = 0.0
    return namen, X


def z(v):
    s = v.std()
    return (v - v.mean()) / s if s > 1e-12 else np.zeros_like(v)


def gierig(Xl, Hl, zrl, Xp, Hp, zrp, bezuege, tiefe=4):
    """Vorwaertsauswahl: standardisierte Summen mehrerer Ausdruecke.

    Rangkorrelation ist gegen monotone Umformungen blind, also kann ein
    einzelner Ausdruck nicht mehr verbessert werden -- Zuwachs gibt es nur
    durch WEITERE Terme. Genommen wird ein Term nur, wenn er min(lern,pruef)
    hebt: wer nur auf lern hilft, ist Ueberanpassung und wird nicht genommen.
    """
    if not bezuege:
        return []
    vl = {b: z(baue(Xl, b)) for b in bezuege}
    vp = {b: z(baue(Xp, b)) for b in bezuege}
    start = bezuege[0]
    kette, sl, sp = [start], vl[start].copy(), vp[start].copy()
    bestwert = min(korr(sl, Hl, zrl), korr(sp, Hp, zrp))
    zeichen = [1.0]
    spur = [(bestwert, tuple(kette), tuple(zeichen))]
    for _ in range(tiefe - 1):
        bester, bwert, bvz = None, bestwert, 1.0
        for b in bezuege:
            if b in kette:
                continue
            for vz in (1.0, -1.0):
                w = min(korr(sl + vz * vl[b], Hl, zrl),
                        korr(sp + vz * vp[b], Hp, zrp))
                if w > bwert + 1e-4:
                    bester, bwert, bvz = b, w, vz
        if bester is None:
            break
        kette.append(bester)
        zeichen.append(bvz)
        sl += bvz * vl[bester]
        sp += bvz * vp[bester]
        bestwert = bwert
        spur.append((bestwert, tuple(kette), tuple(zeichen)))
    return spur
judo/daten/sieb.json — 8 Vertauschungen, alle Kandidaten neu gerechnet
Schwelle 0.13010.07320.09830.12640.10450.13060.09270.09780.091Durchgang · Wert der gierigen Stufe unter gemischtem Etikett

Jeder Punkt: dieselbe Suche über 76 501 Kandidaten, aber das Härte-Etikett zellenweise vertauscht. Das Maximum aus acht Durchgängen ist die Schwelle — nicht null.

Rohdaten ansehen — judo/daten/sieb.json (Schwelle + Nullwerte)
judo/daten/sieb.json (Schwelle + Nullwerte)
{
 "schwelle": 0.1301589930874898,
 "s1schwelle": 0.11654299343656545,
 "nullwerte": [
  0.07307985475037881,
  0.09803691914137302,
  0.12620280753198415,
  0.10369664996358074,
  0.1301589930874898,
  0.09224358597393721,
  0.09736080529329685,
  0.09094165030533212
 ],
 "kandidaten": 76501,
 "bank": 226
}
Bemerkenswert: die gierige Stufe treibt die Null, nicht die Paarsuche. Der Grund ist eingebaut — die Vorwärtsauswahl maximiert min(lern, pruef) direkt, damit ist pruef für sie keine unabhängige Probe mehr, sondern Zielgröße. Wäre die gierige Stufe nicht mitgepreist worden, läge die Schwelle bei 0,086 und rund fünfzehn Prozent zu tief.

Die strenge Probe

Der gepoolte Wert überschätzt: n hat vier Stufen und sat zwei, und was rang-linear herausgerechnet wird, lässt eine nichtlineare Abhängigkeit stehen. Also wird jeder Kandidat zusätzlich innerhalb je einer festen (n, sat)-Zelle nachgerechnet, wo weder das eine noch das andere durchgreifen kann. Mitgemeldet wird, in wie vielen der acht Zellen das Vorzeichen dasselbe ist — ein echtes Merkmal zeigt überall in dieselbe Richtung, ein Artefakt wechselt.

Geprüft, dass die Apparatur funktioniert

Auf synthetischen Daten mit gepflanztem Signal, vor dem eigentlichen Lauf:

bester Kandidatauf testSchwelleUrteil
Signal gepflanzt0,8750,8970,131gefunden, und als Verhältnis erkannt
kein Signal0,1260,0170,133korrekt verworfen

Die zweite Zeile ist die wichtigere: die Suche erreicht auf reinem Rauschen 0,126 — und die Schwelle fängt es.

Der Fund

Dreißig Kandidaten lagen über der Schwelle. Die strenge Probe lässt davon einen als besten stehen:

Der Ausdruck. cSchuld.entropie × cTeil.teilnahme — die Entropie der Klauselgewichte mal das Teilnahmeverhältnis der Bahnen. Streng gemessen r = 0,259, in allen acht Zellen mit gleichem Vorzeichen. Beide Bausteine sind Lokalisierungsmaße der chaotischen Dynamik: wie stark konzentriert der Prozess die Schuld auf wenige Klauseln.
MerkmalstrengSpanne (8 Zellen)Vorzeichen
cSchuld.entropie allein−0,043[−0,332, +0,314]4/8
cTeil.teilnahme allein+0,192[−0,018, +0,465]7/8
Produkt der beiden+0,259[+0,117, +0,481]8/8

Das Produkt trägt mehr als seine Teile. Die Entropie allein ist wertlos und wechselt in der Hälfte der Zellen das Vorzeichen; das Teilnahmeverhältnis allein liegt bei 0,192. Die Auskunft sitzt nicht in einer der beiden Koordinaten, sondern in ihrer Verbindung — genau das, wofür eine Suche über Verhältnisse und Produkte gebaut wird.

Was sich als Artefakt herausgestellt hat

Der größere Teil der dreißig war Störgrößen-Neukombination, und die strenge Probe hat es gefunden:

Bausteinr mit nr mit sat
kGeruest (Kompression des Gerüsts)+0,938−0,022
kUeberhang−0,937+0,020
cSaettigung (Sättigung des Würfels)−0,086+0,762
kGeruest ist ein reiner n-Stellvertreter, cSaettigung ein reiner sat-Stellvertreter. Ihre Differenz sieht gepoolt nach 0,305 aus und fällt in der strengen Probe auf −0,031 mit wechselndem Vorzeichen. Das ist genau der Betrug, gegen den die Probe gebaut war.

Mehr Terme kaufen nichts Echtes

Die gierige Stufe verbindet mehrere Ausdrücke zu einer vorzeichenbehafteten Summe. Das Ergebnis ist der lehrreichste Teil des Laufs:

Termegepoolt teststrengZellen gleich
10,332+0,2598/8
20,392+0,2487/8
30,388+0,2607/8
40,407+0,2366/8
50,405+0,2486/8
Code & Daten ansehen — warum mehr Terme nichts Echtes kaufen
judo/streng_je_term.py — dieselbe Kette, mit persistierter zellenweiser Kontrolle
"""Die strenge (zellweise) Kennzahl für jede Stufe der gierigen Verbindung,
persistiert -- vorher nur in einem Wegwerfskript berechnet, nirgends
gespeichert. Das widerspricht der eigenen Regel: jede gemessene Aussage auf
der Seite muss zu einer Datei führen, die man ansehen und nachrechnen kann.

Liest sieb.json (Feld "gierig", die Bezeichner der einzelnen Terme) und die
Bank neu aus, baut jede Stufe als vorzeichenbehaftete Summe nach und
rechnet an jeder Stufe die Kontrolle aus HANDBUCH.md/JUDO.md: partielle
Rangkorrelation *innerhalb* jeder feste (n, sat)-Zelle, gemittelt, mit
Zählung der Zellen, die im selben Vorzeichen zeigen.
"""
import json, pickle, sys, os
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import bewerten as B
import sieb as SB

HIER = os.path.dirname(os.path.abspath(__file__))
DATEN = os.path.join(HIER, "daten")


def streng(v, nn, sat, gv, y, mindest=60):
    ws, ns = [], []
    for x in sorted(set(nn)):
        for s_ in (0.0, 1.0):
            m = (nn == x) & (sat == s_)
            if m.sum() < mindest:
                continue
            ws.append(B.partiell(v[m], y[m], [gv[m]]))
            ns.append(int(m.sum()))
    if not ws:
        return 0.0, 0, 0
    ws, ns = np.array(ws), np.array(ns)
    mit = float(np.average(ws, weights=ns))
    return mit, int((np.sign(ws) == np.sign(mit)).sum()), len(ws)


import lauf as L

_TEIL_CACHE = {}
def _bauen(X, R, menge):
    if not _TEIL_CACHE:
        substrate = pickle.load(open(os.path.join(DATEN, "substrat.pkl"), "rb"))
        satz, subs, idx = L.teile(R, substrate)
        P = B.Pruefstand(satz)
        for k in ("lern", "pruef", "test"):
            Xk = X[idx[k]]
            H = SB.hut(P.stoer[k], len(Xk))
            zr0 = B.rang(P.ziel[k])
            _TEIL_CACHE[k] = (Xk, H, zr0 - H @ zr0)
    return _TEIL_CACHE[menge]


def lauf():
    d = np.load(os.path.join(DATEN, "bank.npz"), allow_pickle=True)
    X, namen = d["X"], list(d["namen"])
    ni = {n: i for i, n in enumerate(namen)}
    R = pickle.load(open(os.path.join(DATEN, "korpus.pkl"), "rb"))
    sb = json.load(open(os.path.join(DATEN, "sieb.json")))

    nn = np.array([r["n"] for r in R], float)
    sat = np.array([r["sat"] for r in R], float)
    gv = np.array([r["gradvar"] for r in R], float)
    y = np.log2(np.maximum([r["konflikte"] for r in R], 1)).astype(float)

    # WICHTIG: sieb.json speichert "gierig" nur mit tiefe=4 und dem Pool der
    # besten 40 Stufe-2-Kandidaten (interner Aufruf in sieb.py::eine_suche).
    # Die Tabelle auf judo.html hat aber 5 Stufen aus einem breiteren Pool --
    # allen 30 Kandidaten aus sb["ergebnis"] statt nur den besten 40 aus
    # Stufe 2 -- und tiefe=5. Um die exakt gezeigten Zahlen zu reproduzieren,
    # muss hier derselbe (breitere) Pool und dieselbe Tiefe verwendet werden.
    bezuege = [(e["art"], ni[e["a"]], ni[e["b"]]) for e in sb["ergebnis"]]
    Xl_, Hl_, zrl_ = _bauen(X, R, "lern")
    Xp_, Hp_, zrp_ = _bauen(X, R, "pruef")
    Xt_, Ht_, zrt_ = _bauen(X, R, "test")
    spur = SB.gierig(Xl_, Hl_, zrl_, Xp_, Hp_, zrp_, bezuege, tiefe=5)

    aus = []
    for wert, kette, zeichen in spur:
        sl = sum(v * SB.z(SB.baue(Xl_, b)) for v, b in zip(zeichen, kette))
        sp = sum(v * SB.z(SB.baue(Xp_, b)) for v, b in zip(zeichen, kette))
        st = sum(v * SB.z(SB.baue(Xt_, b)) for v, b in zip(zeichen, kette))
        rl, rp, rt = SB.korr(sl, Hl_, zrl_), SB.korr(sp, Hp_, zrp_), SB.korr(st, Ht_, zrt_)
        ganz = sum(v * SB.z(SB.baue(X, b)) for v, b in zip(zeichen, kette))
        sg, gl, zz = streng(ganz, nn, sat, gv, y)
        txt = " ".join(("+" if v > 0 else "-") +
                       (namen[i] if a == "einzeln" else f"({namen[i]} {a} {namen[j]})")
                       for v, (a, i, j) in zip(zeichen, kette))
        aus.append({"terme": len(kette), "lern": rl, "pruef": rp, "test": rt,
                    "streng": sg, "zellen_gleich": gl, "zellen": zz, "ausdruck": txt})
        print(f"  {len(kette)} Terme: lern {rl:.3f} pruef {rp:.3f} test {rt:.3f}  "
              f"streng {sg:+.3f} {gl}/{zz}")

    with open(os.path.join(DATEN, "streng_je_term.json"), "w") as f:
        json.dump(aus, f, indent=1)
    print(f"\ngespeichert -> {os.path.join(DATEN, 'streng_je_term.json')}")


if __name__ == "__main__":
    lauf()
judo/daten/streng_je_term.json, tabellarisch
TermelernpruefteststrengZellen
10.3120.3100.332+0.2598/8
20.3450.3550.392+0.2487/8
30.3580.3770.388+0.2607/8
40.3780.3900.407+0.2366/8
50.3790.3930.405+0.2486/8
judo/daten/streng_je_term.json — 5 Stufen der Vorwärtsauswahl
123450.00.10.20.30.4Anzahl Terme in der Verbindung
streng (die echte Kennzahl)lernprüftest (gepoolt)

Die drei gepoolten Kurven steigen mit jedem Term. Die rote — die einzige, die gegen n, Gradstreuung und sat sowie zellenweise gerechnet ist — bleibt flach und fällt sogar. Das ist die Überanpassung, nicht der Fund.

Rohdaten ansehen — judo/daten/streng_je_term.json
judo/daten/streng_je_term.json
[
 {
  "terme": 1,
  "lern": 0.3121949716325861,
  "pruef": 0.30971683757420543,
  "test": 0.3324133787235236,
  "streng": 0.25930672858491144,
  "zellen_gleich": 8,
  "zellen": 8,
  "ausdruck": "+(cSchuld.entropie prod cTeil.teilnahme)"
 },
 {
  "terme": 2,
  "lern": 0.3446945783258858,
  "pruef": 0.3547529368331009,
  "test": 0.3919760659956465,
  "streng": 0.24807174490592482,
  "zellen_gleich": 7,
  "zellen": 8,
  "ausdruck": "+(cSchuld.entropie prod cTeil.teilnahme) -(cBogen.median diff cSchuldStreu.median)"
 },
 {
  "terme": 3,
  "lern": 0.35832565991185183,
  "pruef": 0.37746030194027413,
  "test": 0.38782528426719703,
  "streng": 0.26004803841288204,
  "zellen_gleich": 7,
  "zellen": 8,
  "ausdruck": "+(cSchuld.entropie prod cTeil.teilnahme) -(cBogen.median diff cSchuldStreu.median) -(cTeil.streu diff sL.teilnahme)"
 },
 {
  "terme": 4,
  "lern": 0.3776439765673545,
  "pruef": 0.389714897199669,
  "test": 0.407362902209816,
  "streng": 0.23569579486940836,
  "zellen_gleich": 6,
  "zellen": 8,
  "ausdruck": "+(cSchuld.entropie prod cTeil.teilnahme) -(cBogen.median diff cSchuldStreu.median) -(cTeil.streu diff sL.teilnahme) +(cK.teilnahme quot kUeberhang)"
 },
 {
  "terme": 5,
  "lern": 0.3794966148490839,
  "pruef": 0.39318464523725827,
  "test": 0.404828812015732,
  "streng": 0.24841559524734894,
  "zellen_gleich": 6,
  "zellen": 8,
  "ausdruck": "+(cSchuld.entropie prod cTeil.teilnahme) -(cBogen.median diff cSchuldStreu.median) -(cTeil.streu diff sL.teilnahme) +(cK.teilnahme quot kUeberhang) -(cSchuld.streu quot cSpurEnde.teilnahme)"
 }
]
Die gepoolte Zahl steigt von 0,33 auf 0,41, die strenge steht bei 0,25, und die Zellenkonsistenz fällt von 8/8 auf 6/8. Die zusätzlichen Terme kaufen keine Härteauskunft, sondern Reststruktur der Störgrößen — gut zu sehen am vierten Term, der kUeberhang enthält, den n-Stellvertreter mit r = 0,94.

Wer hier nach der gepoolten Zahl optimiert hätte, hätte einen Viertermausdruck mit 0,41 gemeldet, der weniger weiß als der Einterm mit 0,33. Gepooltes und strenges Maß laufen unter gieriger Suche auseinander, und nur das strenge ist belastbar.

Die Einspritzung auf der iGPU

Der ursprüngliche Plan war, ein lokales Sprachmodell als Mutationsoperator laufen zu lassen. Gemessen: die Radeon 890M hat 512 MiB eigenen Speicher, alles weitere läuft über GTT und damit über denselben DDR5-Bus wie die CPU. Kein Bandbreitenvorteil, 12 bis 15 Zeichen je Sekunde — unabhängig von der Modellgröße, ein 4B-Modell war so langsam wie ein 35B.

Also umgebaut: die Breite kommt vom Sieb (alle Paare vollständig), das Modell bekommt den Teil, der zu ihm passt — eine Zeile je Vorschlag statt eines Codeblocks, und nur Verbindungen ab drei Namen, deren Raum zu groß zum Abzählen ist. In drei Stunden: 3 551 Kandidaten, davon 3 514 nach Entdopplung.

Bester Ausdruck der Einspritzung. cSchuldStreu.schiefe × wE.rate_frueh × sLAbst.median / cTeil.streu — streng 0,283, 8/8. Vier Bausteine aus drei verschiedenen Instrumenten: Schuldstreuungs-Schiefe und Teilnahmestreuung (Chaos), frühe Zerfallsrate der Irrfahrtsenergie (abgeschnittene Irrfahrten), Median der Laplace-Eigenwertabstände (Spektrum).

Die eigene Vertauschungsnull der Einspritzung — dieselben 3 514 Ausdrücke gegen ein innerhalb der Zellen gemischtes Etikett — liegt bei 0,067, mit null Falschmarkierungen in vier Durchgängen. Die strenge Statistik mit Zellenkonsistenz ist ein deutlich schärferes Instrument als die gepoolte.

Einordnung, ohne Aufrundung

Gegen das Handbuch, Teil IV: die dort verzeichneten löserfreien Zugänge liegen bei r ≤ 0,19; d(50 %) erreicht 0,56 und braucht einen Löser. Der Fund liegt damit über allem bisher Löserfreien — mit dem Vorbehalt, dass nicht geprüft ist, ob die Zahlen der Tabelle mit denselben Störgrößen gerechnet wurden.

Ein praktisch brauchbarer Härteschätzer ist 0,28 nicht. Er erklärt rund acht Prozent der Härtestreuung.

Was der Judo-Wurf gezeigt hat: die Unordnung eines Prozesses auf der Formel weiß etwas über die Härte, das kein Aggregat über den Lösungsraum weiß. Wenig — aber messbar, löserfrei, und in allen acht Zellen in dieselbe Richtung.

Die frische Probe

Eine Frage blieb offen, und es war die unangenehmste. Die Einspritzung wurde über 497 Runden von einer Bestenliste gesteuert, die nach genau der Größe gereiht war, mit der sie am Ende bewertet wird — und diese Größe lief über alle Instanzen. Es gab also keine zurückgehaltene Menge, die den Rückkopplungsweg unterbrochen hätte.

Die Vertauschungsprobe beantwortet das nicht. Sie mischt das Etikett und rechnet eine feste Kandidatenmenge nach; sie preist damit „ist der Wert dieses Ausdrucks echt“, nicht „hat die Steuerung ihn hochgezogen“. Das eine lässt sich nur mit Instanzen beantworten, die es während der Suche noch nicht gab.

Also: 900 frische Instanzen, andere Saaten, dasselbe Substrat, dieselben Ausdrücke — und keine Anpassung mehr. Drei Größen n statt vier, daher sechs Zellen statt acht.

AusdruckfrischZellenvorher
Sieb, bester Paarausdruck+0,2666/6+0,259
Einspritzung, bester+0,2386/6+0,283
Einspritzung, zweitbester+0,2226/6+0,273
Einspritzung, dritter+0,2256/6+0,272
cTeil.teilnahme allein+0,2116/6+0,192
cSchuldStreu.schiefe allein−0,1234/6
Kontrolle: n-Stellvertreter+0,0093/6+0,013
Kontrolle: sat-Stellvertreter−0,0373/6−0,040

Drei Dinge stehen damit fest.

Erstens: der Fund des Siebs hält. 0,259 → 0,266, in allen sechs Zellen gleiches Vorzeichen. Keine Aufblähung — das Sieb hatte getrennte Mengen und eine Schwelle aus Vertauschung, und beides hat getragen.
Zweitens: die Einspritzung war um rund 0,05 aufgebläht. 0,283 → 0,238, und dasselbe bei den beiden nächsten (0,273 → 0,222, 0,272 → 0,225). Der Betrag ist bemerkenswert gleichmäßig, etwa siebzehn Prozent, und er ist genau der Preis dafür, dass die Rückkopplung über alle Instanzen lief. Die Einspritzung hat also nur scheinbar gewonnen. Der beste Ausdruck bleibt der des Siebs.
Drittens: die Kontrollen sind auf frischen Daten tot, wie sie es sein sollen — der n-Stellvertreter bei +0,009 und der sat-Stellvertreter bei −0,037, beide mit wechselndem Vorzeichen (3/6). Die Störgrößenrechnung greift.

Bemerkenswert nebenbei: cSchuldStreu.schiefe, der Baustein, der in acht der zehn besten Einspritzungsausdrücke steckt, ist allein schwach und uneinheitlich (−0,123 bei 4/6). Dasselbe Muster wie beim Sieb — die Auskunft sitzt in der Verbindung, nicht im Baustein.

Das Endergebnis

Der Fund, auf frischen Instanzen bestätigt:

cSchuld.entropie × cTeil.teilnahme  →  r = 0,266

Löserfrei, gegen n, Gradstreuung und Erfüllbarkeit abgesichert, in allen sechs Zellen gleichgerichtet, auf 900 nie gesehenen Instanzen bestätigt. Rauschschwelle der Suche: 0,130.

Gegen das Handbuch, Teil IV: die dort verzeichneten löserfreien Zugänge liegen bei r ≤ 0,19; d(50 %) erreicht 0,56 und braucht einen Löser. Ein praktisch brauchbarer Härteschätzer ist 0,27 nicht — er erklärt rund sieben Prozent der Streuung.

Code & Daten ansehen — die Bestätigung auf 900 nie gesehenen Instanzen
judo/frischprobe.py
"""Die letzte Probe: ein Korpus, den nichts je gesehen hat.

WARUM SIE NOETIG IST. Die Einspritzung wurde ueber 497 Runden von einer
Bestenliste gesteuert, die nach genau der Groesse gereiht war, mit der sie
am Ende bewertet wird -- und diese Groesse lief ueber ALLE Instanzen. Es
gab also keine zurueckgehaltene Menge, die den Rueckkopplungsweg
unterbrochen haette.

Die Vertauschungsprobe beantwortet das nicht. Sie mischt das Etikett und
rechnet eine FESTE Kandidatenmenge nach; sie preist damit "ist der Wert
dieses Ausdrucks echt", nicht "hat die Steuerung ihn hochgezogen". Das eine
laesst sich nur mit Instanzen beantworten, die es waehrend der Suche noch
nicht gab.

Also: frische Instanzen, andere Saat, dasselbe Substrat, dieselben
Ausdruecke -- und keine Anpassung mehr.
"""
import glob, json, os, pickle, sys, time
from multiprocessing import Pool
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import bank as BK, bewerten as B, einspritzung as E, substrat as S

HIER = os.path.dirname(os.path.abspath(__file__))

def eine(a):
    i, r = a
    return i, BK.bank(S.alles(r["klauseln"], r["n"], saat=7000 + i))

def lauf():
    R = []
    for f in sorted(glob.glob(os.path.join(HIER, "frisch", "korpus_n*.jsonl"))):
        for z in open(f):
            z = z.strip()
            if z:
                try: R.append(json.loads(z))
                except json.JSONDecodeError: pass
    for r in R:
        c = np.zeros(r["n"], np.int64)
        for k in r["klauseln"]:
            for l in k: c[abs(l) - 1] += 1
        r["gradvar"] = float(c.var())
    print(f"# {len(R)} frische Instanzen, SAT-Anteil {np.mean([r['sat'] for r in R]):.3f}",
          flush=True)

    t0 = time.time()
    aus = [None] * len(R)
    with Pool(min(22, os.cpu_count() or 4)) as p:
        for i, b in p.imap_unordered(eine, list(enumerate(R)), chunksize=4):
            aus[i] = b
    print(f"# Substrat+Bank in {(time.time()-t0)/60:.1f} min", flush=True)

    alt = np.load(os.path.join(HIER, "daten", "bank.npz"), allow_pickle=True)
    namen = list(alt["namen"])
    X = np.array([[b.get(k, 0.0) for k in namen] for b in aus], np.float64)
    X[~np.isfinite(X)] = 0.0
    ni = {n: i for i, n in enumerate(namen)}

    nn = np.array([r["n"] for r in R], float)
    sat = np.array([r["sat"] for r in R], float)
    gv = np.array([r["gradvar"] for r in R], float)
    y = np.log2(np.maximum([r["konflikte"] for r in R], 1)).astype(float)
    zellen = [m for m in ((nn == x) & (sat == s)
                          for x in sorted(set(nn)) for s in (0.0, 1.0))
              if m.sum() >= 40]
    gr = np.array([m.sum() for m in zellen], float)

    def streng(v):
        w = np.array([B.partiell(v[m], y[m], [gv[m]]) for m in zellen])
        mit = float((w * gr).sum() / gr.sum())
        return mit, int((np.sign(w) == np.sign(mit)).sum()), len(w)

    KAND = [
      ("Sieb, bester Paarausdruck",
       "cSchuld.entropie * cTeil.teilnahme", 0.259),
      ("Einspritzung, bester",
       "cSchuldStreu.schiefe * wE.rate_frueh * sLAbst.median / cTeil.streu", 0.283),
      ("Einspritzung, zweitbester",
       "cSchuldStreu.schiefe * wE.rate_frueh / cTeil.streu", 0.273),
      ("Einspritzung, dritter",
       "cSchuldStreu.schiefe * wE.rate_frueh * sLAbst.median / cTeil.vk", 0.272),
      ("nur der Hauptbaustein", "cSchuldStreu.schiefe", None),
      ("nur cTeil.teilnahme", "cTeil.teilnahme", 0.192),
      ("Artefakt zur Kontrolle (n-Stellvertreter)", "kGeruest", 0.013),
      ("Artefakt zur Kontrolle (sat-Stellvertreter)", "cSaettigung", -0.040),
    ]
    print(f"\n  {'':44s} {'frisch':>8s} {'Zellen':>7s}   {'alt':>7s}")
    for nam, ausdr, alt_ in KAND:
        try:
            v = E.werte_aus(ausdr, X, ni)
        except Exception as ex:
            print(f"  {nam:44s}  -- {ex}"); continue
        m, g, z = streng(v)
        av = f"{alt_:+.3f}" if alt_ is not None else "   -  "
        print(f"  {nam:44s} {m:+8.3f}  {g}/{z:<4d}  {av}")
    print(f"\n  (Zellen = wieviele der {len(zellen)} (n,sat)-Zellen dasselbe "
          f"Vorzeichen zeigen)")

if __name__ == "__main__":
    lauf()
judo/daten/frischprobe.log, tabellarisch
AusdruckfrischZellenSieb/altes Ergebnis
Sieb, bester Paarausdruck+0,2666/6+0,259
Einspritzung, bester+0,2386/6+0,283
Einspritzung, zweitbester+0,2226/6+0,273
Einspritzung, dritter+0,2256/6+0,272
nur der Hauptbaustein−0,1234/6
nur cTeil.teilnahme+0,2116/6+0,192
Kontrolle: n-Stellvertreter+0,0093/6+0,013
Kontrolle: sat-Stellvertreter−0,0373/6−0,040

900 Instanzen mit frischen Saaten, SAT-Anteil 0,509, Substrat in 14,9 min neu gerechnet. „Zellen“ = wie viele der 6 (n, sat)-Zellen dasselbe Vorzeichen zeigen.

Rohdaten ansehen — judo/daten/frischprobe.log
judo/daten/frischprobe.log
# 900 frische Instanzen, SAT-Anteil 0.509
# Substrat+Bank in 14.9 min

                                                 frisch  Zellen       alt
  Sieb, bester Paarausdruck                      +0.266  6/6     +0.259
  Einspritzung, bester                           +0.238  6/6     +0.283
  Einspritzung, zweitbester                      +0.222  6/6     +0.273
  Einspritzung, dritter                          +0.225  6/6     +0.272
  nur der Hauptbaustein                          -0.123  4/6        -  
  nur cTeil.teilnahme                            +0.211  6/6     +0.192
  Artefakt zur Kontrolle (n-Stellvertreter)      +0.009  3/6     +0.013
  Artefakt zur Kontrolle (sat-Stellvertreter)    -0.037  3/6     -0.040

  (Zellen = wieviele der 6 (n,sat)-Zellen dasselbe Vorzeichen zeigen)
Was der Judo-Wurf gezeigt hat: die Unordnung eines Prozesses auf der Formel weiß etwas über die Härte, das kein Aggregat über den Lösungsraum weiß. Wenig — aber messbar, löserfrei, gegen Vertauschung geeicht und auf frischen Instanzen wiederholt.
Und was er über das Messen gezeigt hat: dreimal in diesem Lauf sah eine Zahl größer aus, als sie war — gepoolt statt zellweise (0,33 gegen 0,26), mit mehr Termen statt weniger (0,41 gegen 0,33), und mit Rückkopplung über alle Daten statt über eine Teilmenge (0,28 gegen 0,24). Jedes Mal zeigte dieselbe Gegenprobe es an: rechne es innerhalb der Zellen, und rechne es auf Instanzen, die noch niemand gesehen hat.

Quellen

  1. Der Ausspruch „complete disorder is impossible“ geht auf Theodore S. Motzkin zurück und wird als Leitsatz der Ramsey-Theorie zitiert, u. a. bei Graham, Rothschild & Spencer, Ramsey Theory, Wiley, 2. Aufl. 1990. Biographie (MacTutor)
  2. M. Mézard, G. Parisi, R. Zecchina: Analytic and Algorithmic Solution of Random Satisfiability Problems. Science 297, 812–815 (2002). doi:10.1126/science.1073287 · Begleitarbeit mit dem Algorithmus: M. Mézard, R. Zecchina, The random K-satisfiability problem: from an analytic solution to an efficient algorithm, Phys. Rev. E 66, 056126 (2002), arXiv:cond-mat/0207194
  3. F. Krzakała, A. Montanari, F. Ricci-Tersenghi, G. Semerjian, L. Zdeborová: Gibbs states and the set of solutions of random constraint satisfaction problems. PNAS 104, 10318–10323 (2007). doi:10.1073/pnas.0703685104 · arXiv:cond-mat/0612365
  4. S. Mertens, M. Mézard, R. Zecchina: Threshold values of random K-SAT from the cavity method. Random Structures & Algorithms 28, 340–373 (2006). arXiv:cs/0309020
  5. M. Ercsey-Ravasz, Z. Toroczkai: Optimization hardness as transient chaos in an analog approach to constraint satisfaction. Nature Physics 7, 966–970 (2011). doi:10.1038/nphys2105
  6. V. Oganesyan, D. A. Huse: Localization of interacting fermions at high temperature. Phys. Rev. B 75, 155111 (2007) — führt das Abstandsverhältnis ein. doi:10.1103/PhysRevB.75.155111 · arXiv:cond-mat/0610854
  7. Y. Y. Atas, E. Bogomolny, O. Giraud, G. Roux: Distribution of the Ratio of Consecutive Level Spacings in Random Matrix Ensembles. Phys. Rev. Lett. 110, 084101 (2013) — daher die Werte 0,3863 (Poisson) und 0,5307 (GOE). doi:10.1103/PhysRevLett.110.084101 · arXiv:1212.5611

Die Messungen dieser Seite stammen aus judo/ im Projektarchiv: substrat.py (Sonden), bank.py (Merkmalsbank), sieb.py (Suche und Vertauschungsnull), bewerten.py (Störgrößen), frischprobe.py (die frischen Instanzen). Der Volltext liegt als JUDO.md bei.