Files
swp-02-aufbereitung/test_embeddings.py
T
irrlichtandClaude Opus 5 cf89e012b2 Revisionsmodul, Embeddings und Container
Drei Schritte, die zusammen Phase 2 lauffaehig machen.

Revisionen (migrations/p2-004, revisionen.py)
    Ingest schreibt jede geaenderte Fassung eines Items nach
    raw_item_versionen fort. Daraus faellt ein Befund ab, den die
    Spezifikation nicht vorsehen konnte: welches Haus seine Schlagzeile
    nachtraeglich umschreibt, und wie. 206 von 3407 Items, acht
    Einstufungen von 'formal' bis 'neufassung'. Kein Modell, kein Lexikon,
    kein Netzzugriff - nur ein Wortvergleich zweier Fassungen.

    Fortschreibungen sind ueber ticker_markiert und kopf_stabil abziehbar,
    absichtlich als zwei Merkmale statt einer Kategorie: das eine ist eine
    Selbstauskunft des Hauses, das andere eine Beobachtung am Satzbau.

Embeddings (embeddings.py)
    multilingual-e5-large, 1024 Dimensionen, MIT. Jede Zeile wird auf feste
    128 Token aufgefuellt, nicht nur bis zur laengsten des Stapels - sonst
    haengt ein Vektor davon ab, mit welchen Nachbarn er kodiert wurde, und
    ein Slot-Lauf liefert andere letzte Stellen als ein Backfill.
    Nachgewiesen: 64/64 bitgleich einzeln wie im vollen Stapel, groesste
    Abweichung 0.00e+00, und im Betrieb 26 Slot-Vektoren unveraendert
    gegen den 3407er-Backfill.

    Darum transformers statt sentence-transformers: letzteres kapselt genau
    diese Stelle weg.

Container (Containerfile, phase2.py, deploy/)
    Debian trixie wie der Postgres-Container, torch aus dem CPU-Index.
    Die regelbasierten Module liefern darin fuer alle 3407 Items bitgleich
    dieselbe Nutzlast wie auf der Entwicklungsmaschine - Portabilitaet
    gemessen, nicht behauptet.

version.py bekommt den Schluessel 'revision'. Weil der Hash ueber den ganzen
Komponentensatz laeuft, aendert das auch die Version des Dublettenmoduls,
obwohl an dessen Verfahren nichts anders ist. Das ist die in version.py
beschriebene Semantik der Spezifikation, kein Versehen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NupWEBPzsyohVPG47HsTz8
2026-09-08 07:41:27 +02:00

100 lines
3.5 KiB
Python

#!/usr/bin/env python3
"""Testfaelle fuer das Embeddingmodul, soweit sie ohne Modell auskommen.
python3 -m unittest test_embeddings -v
Was das Modell selbst tut, laesst sich hier nicht pruefen - dafuer ist
`embeddings.py --nachweis` da, das im Container gegen die echten Gewichte
laeuft. Geprueft wird hier alles davor und danach: welcher Text hineingeht und
welche Bytes herauskommen.
"""
import struct
import unittest
import embeddings as e
import normalisierung as n
class Bereinigung(unittest.TestCase):
"""bereinige() raeumt auf, ohne den Text zu zerlegen."""
def test_grossschreibung_bleibt(self):
self.assertEqual(n.bereinige("Habeck fordert Wende"), "Habeck fordert Wende")
def test_satzzeichen_bleiben(self):
self.assertIn("?", n.bereinige("Kommt die Wende?"))
def test_tags_und_entities_weg(self):
self.assertEqual(n.bereinige("<b>Bund</b> &amp; Länder"), "Bund & Länder")
def test_anfuehrungszeichen_vereinheitlicht(self):
self.assertEqual(n.bereinige("„Wende“"), '"Wende"')
def test_pluskasten_bleibt_stehen(self):
# Anders als normalisiere(): fuer einen Encoder ist der Ticker-Rahmen
# Text, kein Beiwerk.
self.assertIn("Liveticker", n.bereinige("++ Liveticker zur Wahl ++"))
def test_leerraum_zusammengezogen(self):
self.assertEqual(n.bereinige("a \n b"), "a b")
def test_leere_teile_uebersprungen(self):
self.assertEqual(n.bereinige(None, "Titel", ""), "Titel")
class Eingabetext(unittest.TestCase):
def test_praefix_vorne(self):
self.assertTrue(e.text("Titel", "Teaser").startswith(e.PRAEFIX))
def test_titel_und_teaser_verbunden(self):
t = e.text("Wahl in Sachsen-Anhalt", "Die AfD liegt vorn.")
self.assertIn("Wahl in Sachsen-Anhalt Die AfD liegt vorn.", t)
def test_fehlender_teaser(self):
self.assertEqual(e.text("Nur ein Titel", None), e.PRAEFIX + "Nur ein Titel")
class Vektorliteral(unittest.TestCase):
def test_form(self):
self.assertEqual(e.als_vector([1.0, -0.5]), "[1.0,-0.5]")
def test_rueckweg_ist_verlustfrei(self):
werte = [0.1234567, -0.9876543, 1e-8]
zurueck = [float(x) for x in e.als_vector(werte).strip("[]").split(",")]
self.assertEqual(werte, zurueck)
class Vektorhash(unittest.TestCase):
def test_gleiche_werte_gleicher_hash(self):
self.assertEqual(e.vektor_hash([0.5, 0.25]), e.vektor_hash([0.5, 0.25]))
def test_kleinste_aenderung_anderer_hash(self):
eins = struct.unpack("<f", struct.pack("<f", 0.5))[0]
knapp = struct.unpack("<f", struct.pack("<I",
struct.unpack("<I", struct.pack("<f", 0.5))[0] + 1))[0]
self.assertNotEqual(eins, knapp)
self.assertNotEqual(e.vektor_hash([eins]), e.vektor_hash([knapp]))
def test_laenge_geht_ein(self):
self.assertNotEqual(e.vektor_hash([0.0]), e.vektor_hash([0.0, 0.0]))
class Einstellungen(unittest.TestCase):
def test_revision_und_torch_gehen_ein(self):
a = e.einstellungen("abc", "2.14.0+cpu")
b = e.einstellungen("def", "2.14.0+cpu")
self.assertNotEqual(a, b)
def test_threads_stehen_drin(self):
# Die Threadzahl aendert die Reihenfolge der Summen und muss deshalb
# im Versions-Hash landen.
self.assertIn("threads", e.einstellungen())
def test_dimension_passt_zur_migration(self):
self.assertEqual(e.DIMENSIONEN, 1024)
if __name__ == "__main__":
unittest.main()