Inhalt

Aktueller Ordner: ARS_ExplainableAI
⬅ Übergeordnet

ARS_XAI_PCFG2.py

"""
Algorithmisch Rekursive Sequenzanalyse 3.0
HIERARCHISCHE GRAMMATIKINDUKTION DURCH SEQUENZKOMPRESSION
Explikation latenter Sequenzstrukturen in Verkaufsgesprächen

Methodologische Prämissen:
1. Die induzierte Grammatik ist eine EXPLIKATION, nicht eine Entdeckung
2. Nonterminale repräsentieren INTERPRETATIVE KATEGORIEN, nicht verborgene Strukturen
3. Der Prozess ist TRANSPARENT und INTERSUBJEKTIV NACHVOLLZIEHBAR
"""

import numpy as np
from scipy.stats import pearsonr
import matplotlib.pyplot as plt
from tabulate import tabulate
from collections import Counter, defaultdict
import itertools

# ============================================================================
# 1. EMPIRISCHE DATEN: Terminalzeichenketten aus acht Transkripten
# ============================================================================

empirical_chains = [
    # Transkript 1: Metzgerei
    ['KBG', 'VBG', 'KBBd', 'VBBd', 'KBA', 'VBA', 'KBBd', 'VBBd', 'KBA', 'VAA', 'KAA', 'VAV', 'KAV'],
    # Transkript 2: Marktplatz (Kirschen)
    ['VBG', 'KBBd', 'VBBd', 'VAA', 'KAA', 'VBG', 'KBBd', 'VAA', 'KAA'],
    # Transkript 3: Fischstand
    ['KBBd', 'VBBd', 'VAA', 'KAA'],
    # Transkript 4: Gemüsestand (ausfuehrlich)
    ['KBBd', 'VBBd', 'KBA', 'VBA', 'KBBd', 'VBA', 'KAE', 'VAE', 'KAA', 'VAV', 'KAV'],
    # Transkript 5: Gemüsestand (mit KAV zu Beginn)
    ['KAV', 'KBBd', 'VBBd', 'KBBd', 'VAA', 'KAV'],
    # Transkript 6: Käseverkaufsstand
    ['KBG', 'VBG', 'KBBd', 'VBBd', 'KAA'],
    # Transkript 7: Bonbonstand
    ['KBBd', 'VBBd', 'KBA', 'VAA', 'KAA'],
    # Transkript 8: Baeckerei
    ['KBG', 'VBBd', 'KBBd', 'VBA', 'VAA', 'KAA', 'VAV', 'KAV']
]

# ============================================================================
# 2. METHODOLOGISCHE REFLEXIONSEBENE
# ============================================================================

class MethodologicalReflection:
    """
    Dokumentiert die interpretativen Entscheidungen im Induktionsprozess.
    Ermöglicht intersubjektive Nachvollziehbarkeit gemäß XAI-Kriterien.
    """
    
    def __init__(self):
        self.interpretation_log = []
        self.sequence_meaning_mapping = {}
        self.compression_rationale = {}
        
    def log_interpretation(self, sequence, new_nonterminal, rationale):
        """Dokumentiert eine Interpretationsentscheidung"""
        self.interpretation_log.append({
            'sequence': sequence,
            'new_nonterminal': new_nonterminal,
            'rationale': rationale,
            'timestamp': len(self.interpretation_log)
        })
        
        # Bedeutung der Sequenz explizieren
        if all(isinstance(s, str) and (s.startswith(('K', 'V'))) for s in sequence):
            aktionen = [self._interpretiere_symbol(s) for s in sequence if isinstance(s, str)]
            self.sequence_meaning_mapping[tuple(sequence)] = {
                'bedeutung': ' → '.join(aktionen),
                'typ': self._klassifiziere_sequenz(sequence)
            }
    
    def _interpretiere_symbol(self, symbol):
        """Gibt die qualitative Bedeutung eines Terminalzeichens zurück"""
        bedeutungen = {
            'KBG': 'Kunden-Gruß',
            'VBG': 'Verkäufer-Gruß',
            'KBBd': 'Kunden-Bedarf (konkret)',
            'VBBd': 'Verkäufer-Nachfrage',
            'KBA': 'Kunden-Antwort',
            'VBA': 'Verkäufer-Reaktion',
            'KAE': 'Kunden-Erkundigung',
            'VAE': 'Verkäufer-Auskunft',
            'KAA': 'Kunden-Abschluss',
            'VAA': 'Verkäufer-Abschluss',
            'KAV': 'Kunden-Verabschiedung',
            'VAV': 'Verkäufer-Verabschiedung'
        }
        return bedeutungen.get(symbol, str(symbol))
    
    def _klassifiziere_sequenz(self, sequence):
        """Klassifiziert den Typ der Interaktionssequenz"""
        seq_str = ' '.join([str(s) for s in sequence])
        if 'KBBd' in seq_str and 'VBBd' in seq_str:
            return 'Bedarfsaushandlung'
        elif 'KAE' in seq_str or 'VAE' in seq_str:
            return 'Informationsaustausch'
        elif 'KAA' in seq_str and 'VAA' in seq_str:
            return 'Transaktionsabschluss'
        else:
            return 'Interaktionssequenz'
    
    def print_methodological_summary(self):
        """Gibt eine methodologische Zusammenfassung aus"""
        print("\n" + "=" * 70)
        print("METHODOLOGISCHE REFLEXION")
        print("=" * 70)
        print("\nDokumentierte Interpretationsentscheidungen:")
        
        for log in self.interpretation_log:
            print(f"\n[Interpretation {log['timestamp']+1}]")
            seq_str = ' → '.join([str(s) for s in log['sequence']])
            print(f"  Sequenz: {seq_str}")
            print(f"  → Nonterminal: {log['new_nonterminal']}")
            print(f"  Begründung: {log['rationale']}")
            
            if tuple(log['sequence']) in self.sequence_meaning_mapping:
                mapping = self.sequence_meaning_mapping[tuple(log['sequence'])]
                print(f"  Bedeutung: {mapping['bedeutung']}")
                print(f"  Sequenztyp: {mapping['typ']}")

# ============================================================================
# 3. HIERARCHISCHE GRAMMATIKINDUKTION
# ============================================================================

class GrammarInducer:
    """
    Induziert eine PCFG durch hierarchische Kompression.
    Die Nonterminale werden als EXPLIZITE INTERPRETATIONSKATEGORIEN verstanden.
    """
    
    def __init__(self):
        self.rules = {}          # Nonterminal -> Liste von (Produktion, Wahrscheinlichkeit)
        self.rule_occurrences = {} # Zählung der Regelanwendungen
        self.terminals = set()
        self.nonterminals = set()
        self.start_symbol = None
        self.compression_history = []
        self.reflection = MethodologicalReflection()
        
        # Für die Optimierungsphase
        self.terminal_frequencies = None
        self.generated_frequencies_history = []
        
    def find_relevant_patterns(self, chains, min_length=2, max_length=4):
        """
        Findet relevante wiederholte Sequenzen.
        Anders als bei reiner Kompression wird hier semantische Relevanz priorisiert.
        """
        sequence_counter = Counter()
        
        for chain in chains:
            for length in range(min_length, min(max_length, len(chain) + 1)):
                for i in range(len(chain) - length + 1):
                    seq = tuple(chain[i:i+length])
                    
                    # Bewertungskriterien für semantische Relevanz:
                    score = 1.0
                    
                    # Prüfe auf Sprecherwechsel (nur für Terminalzeichen)
                    has_speaker_change = False
                    for j in range(len(seq)-1):
                        if (isinstance(seq[j], str) and isinstance(seq[j+1], str) and
                            ((seq[j].startswith('K') and seq[j+1].startswith('V')) or
                             (seq[j].startswith('V') and seq[j+1].startswith('K')))):
                            has_speaker_change = True
                            break
                    
                    if has_speaker_change:
                        score *= 2.0
                    
                    # Bevorzuge Muster mit Abschlusscharakter
                    has_closure = any(isinstance(s, str) and s.endswith('A') for s in seq)
                    if has_closure:
                        score *= 1.3
                    
                    sequence_counter[seq] += score
        
        # Filtere Sequenzen mit mindestens 2 Vorkommen
        relevant = {seq: count for seq, count in sequence_counter.items() 
                   if count >= 2}
        
        if not relevant:
            return None
        
        # Wähle die relevanteste Sequenz
        best_seq = max(relevant.items(), key=lambda x: x[1])[0]
        return best_seq
    
    def generate_interpretive_name(self, sequence):
        """
        Generiert einen interpretativ gehaltvollen Namen für das Nonterminal.
        """
        # Bestimme den Typ der Sequenz basierend auf Terminalzeichen
        seq_str = ' '.join([str(s) for s in sequence])
        
        if 'KBBd' in seq_str and 'VBBd' in seq_str:
            typ = "BEDARFSKLAERUNG"
        elif ('VAA' in seq_str and 'KAA' in seq_str) or ('VAA' in seq_str and 'KAV' in seq_str):
            typ = "ZAHLUNGSVORGANG"
        elif 'KAE' in seq_str or 'VAE' in seq_str:
            typ = "INFORMATIONSAUSTAUSCH"
        elif 'KBG' in seq_str and 'VBG' in seq_str:
            typ = "BEGRUESSUNG"
        elif 'VAV' in seq_str and 'KAV' in seq_str:
            typ = "VERABSCHIEDUNG"
        else:
            typ = "SEQUENZ"
        
        # Erstelle einen eindeutigen Namen
        if all(isinstance(s, str) and len(s) <= 4 for s in sequence):
            # Nur Terminalzeichen
            first = sequence[0] if sequence else ""
            last = sequence[-1] if sequence else ""
            return f"NT_{typ}_{first}_{last}"
        else:
            # Enthält bereits Nonterminale
            return f"NT_{typ}_{len(sequence)}"
    
    def _describe_sequence(self, sequence):
        """Erzeugt eine semantische Beschreibung der Sequenz"""
        if len(sequence) == 2:
            if all(isinstance(s, str) and len(s) <= 4 for s in sequence):
                return f"{self.reflection._interpretiere_symbol(sequence[0])} → {self.reflection._interpretiere_symbol(sequence[1])}"
            else:
                return f"{sequence[0]} → {sequence[1]}"
        else:
            return f"Sequenz mit {len(sequence)} Schritten"
    
    def compress_chains(self, chains, sequence, new_nonterminal):
        """
        Komprimiert die Ketten durch Ersetzung der Sequenz.
        """
        compressed_chains = []
        seq_tuple = tuple(sequence)
        seq_len = len(sequence)
        
        for chain in chains:
            new_chain = []
            i = 0
            while i < len(chain):
                if i <= len(chain) - seq_len and tuple(chain[i:i+seq_len]) == seq_tuple:
                    new_chain.append(new_nonterminal)
                    i += seq_len
                else:
                    new_chain.append(chain[i])
                    i += 1
            compressed_chains.append(new_chain)
        
        return compressed_chains
    
    def induce_grammar(self, chains, max_iterations=15):
        """
        Hauptmethode zur Grammatikinduktion.
        """
        current_chains = [list(chain) for chain in chains]
        iteration = 0
        
        print("\n" + "=" * 70)
        print("HIERARCHISCHE GRAMMATIKINDUKTION")
        print("=" * 70)
        print("\nDer Induktionsprozess wird als EXPLIKATION verstanden:")
        print("- Jedes neue Nonterminal repräsentiert eine INTERPRETATIVE KATEGORIE")
        print("- Die Benennung expliziert die qualitative Bedeutung")
        print("- Der Prozess ist intersubjektiv NACHVOLLZIEHBAR\n")
        
        while iteration < max_iterations:
            # Finde relevante Muster
            best_seq = self.find_relevant_patterns(current_chains)
            
            if best_seq is None:
                print(f"\nKeine weiteren relevanten Muster nach {iteration} Iterationen.")
                break
            
            # Generiere interpretativen Namen
            new_nonterminal = self.generate_interpretive_name(best_seq)
            beschreibung = self._describe_sequence(best_seq)
            
            # Stelle Einzigartigkeit sicher
            base_name = new_nonterminal
            counter = 1
            while new_nonterminal in self.nonterminals:
                new_nonterminal = f"{base_name}_{counter}"
                counter += 1
            
            # Dokumentiere die interpretative Entscheidung
            rationale = f"Erkanntes Dialogmuster: {beschreibung}"
            self.reflection.log_interpretation(best_seq, new_nonterminal, rationale)
            
            seq_str = ' → '.join([str(s) for s in best_seq])
            print(f"\nIteration {iteration + 1}:")
            print(f"  Erkanntes Muster: {seq_str}")
            print(f"  Interpretation: {beschreibung}")
            print(f"  → Neue Kategorie: {new_nonterminal}")
            
            # Speichere die Regel (vorerst ohne Wahrscheinlichkeit)
            self.rules[new_nonterminal] = [(list(best_seq), 1.0)]  # Temporäre Wahrscheinlichkeit
            self.nonterminals.add(new_nonterminal)
            
            # Komprimiere Ketten
            current_chains = self.compress_chains(current_chains, best_seq, new_nonterminal)
            
            # Zeige Beispiel
            example = ' → '.join([str(s) for s in current_chains[0][:8]])
            print(f"  Beispiel (komprimiert): {example}...")
            
            iteration += 1
            
            # Prüfe auf vollständige Kompression
            if all(len(chain) == 1 for chain in current_chains):
                symbols = set(chain[0] for chain in current_chains)
                if len(symbols) == 1:
                    self.start_symbol = list(symbols)[0]
                    print(f"\nINDUKTION ABGESCHLOSSEN: Startsymbol = {self.start_symbol}")
                    break
        
        # Terminale sind die ursprünglichen Symbole
        all_symbols = set()
        for chain in empirical_chains:
            all_symbols.update(chain)
        self.terminals = all_symbols
        
        # Berechne Wahrscheinlichkeiten
        self._calculate_probabilities()
        
        return current_chains
    
    def _calculate_probabilities(self):
        """
        Berechnet Wahrscheinlichkeiten für jede Produktion.
        """
        # Zähle, wie oft jedes Nonterminal in den Originaldaten vorkommt
        occurrence_count = defaultdict(Counter)
        
        # Für jede Kette in den Originaldaten
        for chain in empirical_chains:
            self._count_occurrences(chain, occurrence_count)
        
        # Konvertiere zu Wahrscheinlichkeiten
        for nonterminal in self.rules:
            if nonterminal in occurrence_count:
                total = sum(occurrence_count[nonterminal].values())
                if total > 0:
                    productions = []
                    for expansion, count in occurrence_count[nonterminal].items():
                        prob = count / total
                        # Stelle sicher, dass expansion eine Liste ist
                        if isinstance(expansion, tuple):
                            expansion = list(expansion)
                        productions.append((expansion, prob))
                    
                    # Sortiere nach Wahrscheinlichkeit
                    productions.sort(key=lambda x: x[1], reverse=True)
                    self.rules[nonterminal] = productions
    
    def _count_occurrences(self, sequence, occurrence_count):
        """
        Rekursive Hilfsfunktion zum Zählen der Vorkommen.
        """
        i = 0
        while i < len(sequence):
            symbol = sequence[i]
            
            # Wenn das Symbol ein Nonterminal ist
            if symbol in self.rules:
                # Finde die passende Expansion
                for expansion, _ in self.rules[symbol]:
                    if isinstance(expansion, list):
                        exp_len = len(expansion)
                        if i + exp_len <= len(sequence) and sequence[i:i+exp_len] == expansion:
                            # Zähle dieses Vorkommen
                            occurrence_count[symbol][tuple(expansion)] += 1
                            # Rekursiv in der Expansion weiterzählen
                            self._count_occurrences(expansion, occurrence_count)
                            i += exp_len
                            break
                        elif i + 1 <= len(sequence) and [sequence[i]] == expansion:
                            # Einzelelement
                            occurrence_count[symbol][tuple(expansion)] += 1
                            i += 1
                            break
                    else:
                        i += 1
            else:
                i += 1

# ============================================================================
# 4. GENERIERUNG MIT INTERPRETATIVER RÜCKBINDUNG
# ============================================================================

class InterpretiveGenerator:
    """
    Generiert Ketten und dokumentiert deren interpretative Bedeutung.
    """
    
    def __init__(self, grammar, terminals, start_symbol, reflection):
        self.grammar = grammar
        self.terminals = terminals
        self.start_symbol = start_symbol
        self.reflection = reflection
        
        # Erstelle Produktionswahrscheinlichkeiten
        self.production_probs = {}
        for nt, prods in grammar.items():
            if prods and len(prods) > 0:
                symbols = []
                probs = []
                for prod, prob in prods:
                    if isinstance(prob, (int, float)):
                        symbols.append(prod)
                        probs.append(float(prob))
                
                if symbols and probs:
                    # Normalisiere falls nötig
                    total = sum(probs)
                    if total > 0 and abs(total - 1.0) > 0.001:
                        probs = [p/total for p in probs]
                    self.production_probs[nt] = (symbols, probs)
    
    def generate_with_interpretation(self, max_depth=15):
        """
        Generiert eine Kette und dokumentiert die Interpretation.
        """
        if not self.start_symbol:
            return [], []
        
        interpretation = []
        
        def expand(symbol, depth=0):
            if depth >= max_depth:
                return [str(symbol)]
            
            if symbol in self.terminals:
                interpretation.append(self.reflection._interpretiere_symbol(symbol))
                return [str(symbol)]
            
            if symbol not in self.production_probs:
                return [str(symbol)]
            
            symbols, probs = self.production_probs[symbol]
            if not symbols:
                return [str(symbol)]
            
            try:
                chosen_idx = np.random.choice(len(symbols), p=probs)
                chosen = symbols[chosen_idx]
            except:
                # Fallback bei Fehlern
                chosen = symbols[0]
            
            # Dokumentiere die Expansion
            seq_str = ' → '.join([str(s) for s in chosen])
            interpretation.append(f"[Expansion von {symbol}: {seq_str}]")
            
            result = []
            for sym in chosen:
                result.extend(expand(sym, depth + 1))
            return result
        
        chain = expand(self.start_symbol)
        return chain, interpretation

# ============================================================================
# 5. VALIDIERUNG IM KONTEXT DER XAI-KRITERIEN
# ============================================================================

class XAIValidator:
    """
    Validiert die induzierte Grammatik anhand der XAI-Kriterien:
    - Verständlichkeit (Meaningfulness)
    - Genauigkeit (Accuracy)
    - Wissensgrenzen (Knowledge Limits)
    """
    
    def __init__(self, grammar_inducer):
        self.inducer = grammar_inducer
        self.original_freq = self._compute_empirical_frequencies()
        
    def _compute_empirical_frequencies(self):
        """Berechnet die empirischen Häufigkeiten der Terminale"""
        all_terminals = []
        for chain in empirical_chains:
            all_terminals.extend(chain)
        
        freq = Counter(all_terminals)
        total = len(all_terminals)
        return {sym: count/total for sym, count in freq.items()}
    
    def evaluate_meaningfulness(self):
        """
        Bewertet die Verständlichkeit der Grammatik.
        """
        print("\n" + "=" * 70)
        print("VALIDIERUNG: VERSTÄNDLICHKEIT (XAI-Kriterium 1)")
        print("=" * 70)
        
        # Prüfe, ob alle Nonterminale interpretierbare Namen haben
        meaningful_count = 0
        for nt in self.inducer.nonterminals:
            if nt.startswith('NT_') and len(nt) > 3:
                meaningful_count += 1
        
        meaningful_ratio = meaningful_count / len(self.inducer.nonterminals) if self.inducer.nonterminals else 0
        
        print(f"\nNonterminale insgesamt: {len(self.inducer.nonterminals)}")
        print(f"Davon interpretierbar benannt: {meaningful_count} ({meaningful_ratio:.1%})")
        
        # Dokumentierte Interpretationen
        print(f"\nDokumentierte Interpretationsentscheidungen: {len(self.inducer.reflection.interpretation_log)}")
        
        # Beispiel-Interpretationen
        if self.inducer.reflection.interpretation_log:
            print("\nBeispiel-Interpretationen:")
            for i, log in enumerate(self.inducer.reflection.interpretation_log[:3]):
                seq_str = ' → '.join([str(s) for s in log['sequence']])
                print(f"  {i+1}. {seq_str} → {log['new_nonterminal']}")
                print(f"     Begründung: {log['rationale']}")
        
        return meaningful_ratio
    
    def evaluate_accuracy(self, n_generated=500):
        """
        Bewertet die Genauigkeit der Grammatik.
        """
        print("\n" + "=" * 70)
        print("VALIDIERUNG: GENAUIGKEIT (XAI-Kriterium 2)")
        print("=" * 70)
        
        generator = InterpretiveGenerator(
            self.inducer.rules, 
            self.inducer.terminals, 
            self.inducer.start_symbol,
            self.inducer.reflection
        )
        
        # Generiere viele Ketten
        all_generated = []
        for _ in range(n_generated):
            chain, _ = generator.generate_with_interpretation()
            all_generated.extend(chain)
        
        # Berechne generierte Häufigkeiten
        gen_freq = Counter(all_generated)
        total_gen = len(all_generated)
        gen_dist = {sym: count/total_gen for sym, count in gen_freq.items() if total_gen > 0}
        
        # Korrelationsberechnung für gemeinsame Symbole
        common_symbols = sorted(set(self.original_freq.keys()) & set(gen_dist.keys()))
        if common_symbols and len(common_symbols) > 1:
            orig_values = [self.original_freq[sym] for sym in common_symbols]
            gen_values = [gen_dist[sym] for sym in common_symbols]
            
            correlation, p_value = pearsonr(orig_values, gen_values)
            
            print(f"\nKorrelation (r): {correlation:.4f}")
            print(f"Signifikanz (p): {p_value:.4f}")
            print(f"Basis: {len(common_symbols)} gemeinsame Symbole")
            
            # Detaillierte Tabelle
            print("\nVergleich der Häufigkeiten (Top 8):")
            table_data = []
            for sym in common_symbols[:8]:
                table_data.append([
                    sym,
                    f"{self.original_freq[sym]:.4f}",
                    f"{gen_dist[sym]:.4f}",
                    f"{abs(self.original_freq[sym] - gen_dist[sym]):.4f}"
                ])
            
            print(tabulate(table_data, 
                          headers=["Symbol", "Empirisch", "Generiert", "Differenz"],
                          tablefmt="grid"))
            
            return correlation, p_value
        else:
            print("Nicht genügend gemeinsame Symbole für Korrelationsberechnung")
            return 0, 1
    
    def evaluate_knowledge_limits(self):
        """
        Dokumentiert die Wissensgrenzen der Grammatik.
        """
        print("\n" + "=" * 70)
        print("VALIDIERUNG: WISSENSGRENZEN (XAI-Kriterium 3)")
        print("=" * 70)
        
        print("\nDie Grammatik ist eine EXPLIKATION, keine Entdeckung:")
        print("  • Sie basiert auf 8 Transkripten von Verkaufsgesprächen")
        print("  • Die Terminalzeichen wurden durch qualitative Interpretation gewonnen")
        print("  • Die Nonterminale repräsentieren INTERPRETATIVE KATEGORIEN")
        
        print("\nGRENZEN DER GRAMMATIK:")
        print("  • Keine Generalisierung über den Datensatz hinaus")
        print("  • Keine Prognosefähigkeit für neue Kontexte")
        print("  • Abhängig von der initialen Kategorienbildung")
        print("  • Alternative Interpretationen sind möglich")
        
        # Dokumentiere nicht abgedeckte Muster
        observed_pairs = set()
        for chain in empirical_chains:
            for i in range(len(chain) - 1):
                observed_pairs.add((chain[i], chain[i+1]))
        
        print(f"\nABGEDECKTE MUSTER:")
        print(f"  • Beobachtete Übergänge: {len(observed_pairs)}")
        print(f"  • In Grammatik erfasste Nonterminale: {len(self.inducer.nonterminals)}")

# ============================================================================
# 6. HAUPTAUSFÜHRUNG
# ============================================================================

def main():
    """
    Hauptfunktion mit methodologischer Rahmung.
    """
    print("=" * 70)
    print("ALGORITHMISCH REKURSIVE SEQUENZANALYSE 3.0")
    print("HIERARCHISCHE GRAMMATIKINDUKTION")
    print("=" * 70)
    
    # 1. Grammatik induzieren
    inducer = GrammarInducer()
    compressed_chains = inducer.induce_grammar(empirical_chains)
    
    # 2. Methodologische Reflexion
    inducer.reflection.print_methodological_summary()
    
    # 3. Induzierte Grammatik anzeigen
    print("\n" + "=" * 70)
    print("INDUZIERTE GRAMMATIK")
    print("=" * 70)
    print(f"\nTerminale ({len(inducer.terminals)}): {sorted(inducer.terminals)}")
    print(f"Nonterminale ({len(inducer.nonterminals)}): {sorted(inducer.nonterminals)}")
    if inducer.start_symbol:
        print(f"Startsymbol: {inducer.start_symbol}")
    
    print("\nPRODUKTIONSREGELN (mit Wahrscheinlichkeiten):")
    for nonterminal in sorted(inducer.rules.keys()):
        productions = inducer.rules[nonterminal]
        if productions:
            prod_strings = []
            for prod, prob in productions:
                # Stelle sicher, dass prod eine Liste ist
                if isinstance(prod, tuple):
                    prod = list(prod)
                prod_str = ' → '.join([str(s) for s in prod])
                # Stelle sicher, dass prob ein Float ist
                prob_float = float(prob) if not isinstance(prob, (int, float)) else prob
                prod_strings.append(f"{prod_str} [{prob_float:.3f}]")
            print(f"\n{nonterminal} → {' | '.join(prod_strings)}")
    
    # 4. Beispiele mit Interpretation generieren
    print("\n" + "=" * 70)
    print("BEISPIELE MIT INTERPRETATION")
    print("=" * 70)
    
    generator = InterpretiveGenerator(
        inducer.rules, 
        inducer.terminals, 
        inducer.start_symbol,
        inducer.reflection
    )
    
    for i in range(3):
        chain, interpretation = generator.generate_with_interpretation()
        print(f"\nBeispiel {i+1}:")
        chain_str = ' → '.join([str(s) for s in chain[:10]])
        print(f"  Kette: {chain_str}" + ("..." if len(chain) > 10 else ""))
        print("  Interpretation:")
        for j, step in enumerate(interpretation[:5]):
            print(f"    {j+1}. {step}")
        if len(interpretation) > 5:
            print("    ...")
    
    # 5. XAI-Validierung
    validator = XAIValidator(inducer)
    validator.evaluate_meaningfulness()
    validator.evaluate_accuracy(n_generated=500)
    validator.evaluate_knowledge_limits()
    
    # 6. Grammatik exportieren
    print("\n" + "=" * 70)
    print("EXPORT DER GRAMMATIK")
    print("=" * 70)
    
    with open("induzierte_grammatik_mit_interpretation.txt", 'w', encoding='utf-8') as f:
        f.write("# INDUZIERTE PCFG MIT INTERPRETATION\n")
        f.write("# =================================\n\n")
        f.write(f"## DATENGRUNDLAGE\n")
        f.write(f"{len(empirical_chains)} Transkripte von Verkaufsgesprächen\n\n")
        
        f.write("## TERMINALE (qualitative Kategorien)\n")
        for sym in sorted(inducer.terminals):
            f.write(f"{sym}: {inducer.reflection._interpretiere_symbol(sym)}\n")
        
        f.write("\n## NONTERMINALE (interpretative Kategorien)\n")
        for log in inducer.reflection.interpretation_log:
            seq_str = ' → '.join([str(s) for s in log['sequence']])
            f.write(f"\n{log['new_nonterminal']}\n")
            f.write(f"  Muster: {seq_str}\n")
            mapping = inducer.reflection.sequence_meaning_mapping.get(tuple(log['sequence']), {})
            if mapping:
                f.write(f"  Bedeutung: {mapping.get('bedeutung', '')}\n")
            f.write(f"  Begründung: {log['rationale']}\n")
        
        f.write("\n## PRODUKTIONSREGELN\n")
        for nt in sorted(inducer.rules.keys()):
            prods = inducer.rules[nt]
            for prod, prob in prods:
                if isinstance(prod, tuple):
                    prod = list(prod)
                prod_str = ' '.join([str(s) for s in prod])
                prob_float = float(prob) if not isinstance(prob, (int, float)) else prob
                f.write(f"{nt} → {prod_str} [{prob_float:.3f}]\n")
    
    print(f"\nGrammatik exportiert als 'induzierte_grammatik_mit_interpretation.txt'")
    
    print("\n" + "=" * 70)
    print("ALGORITHMISCH REKURSIVE SEQUENZANALYSE ABGESCHLOSSEN")
    print("=" * 70)

if __name__ == "__main__":
    main()