Versuchsziel
Es soll untersucht werden, ob ein LLM basierend auf vorgegebenen Metadaten (Beschreibungstexte und Volltexte von Bildungsinhalten), die Neutralität eines Datensatzes auf einer vorgegebenen Skala von 0 bis 5 in vergleichbarer Form bewertet, wie dies zuvor durch Fachredaktionen erfolgt ist.
Erstellung des Test-Datensatzes
Grundlage sind Datensätze mit Bildungsinhalten der Plattform www.Wirlernenonline.de.
Für eine Teilmenge der Datensätze liegen Bewertungen für Neutralität vor. Diese wurden auf einer durch die Redaktionen aufgestellten Skala von 0 bis 5 bewertet und werden zur Qualitätseinschätzung genutzt.
Abruf der Daten
Die Daten wurden über die WLO Rest-API-Schnittstelle mit der Customsuche abgerufen.
Dazu wurde eine Kombination des Felds: ccm:oeh_quality_neutralness mit den Werten: 0, 1, 2, 3, 4, 5 genutzt. Der Datensatz wurde als JSON-Datei gespeichert.
Anreicherung der Datensätze mit Volltexten
In den Datensätzen sind Beschreibungstexte der Bildungsinhalte enthalten. Um einen Vergleich mit Volltexten durchführen zu können, wurden die URL aus dem Feld: ccm:wwwurl ausgelesen und die Inhalte der Webseiten extrahiert.
Zum Einsatz kam hierbei die Python Bibliothek Goose3, die intern BeautifulSoup nutzt und überflüssige Textbestandteile z.B. zur Struktur der Webseite mittels NLP-Techniken entfernt. Das Script wird im Anhang aufgeführt.
Die Volltexte, sowie Zusammenfassungen und Keywords wurden dann als additional_data in die JSON gespeichert und den Datensätzen zugeordnet.
Bei Datensätzen, für die Goose3 keinen Volltext generieren konnte (z.B. aufgrund eines sehr kleinen Textkorpus), wurden die Zusammenfassungen genutzt, die von Goose3 aus allen verfügbaren Infos der Webseiten gebildet werden.
Filterung der Rohdaten
Aus den zuvor erstellten Rohdaten wurde ein Test-Datensatz erstellt, der die notwendigen Kriterien erfüllt, u.a. nicht-leere Felder für die im Test relevanten Metadaten sowie eine Mindestlänge der für den Test genutzten Textfelder (Beschreibungstexte und Volltexte). Dieser Schritt soll sicherstellen, dass ausreichend Textmaterial für eine Bewertung vorliegt.
Verarbeitungsschritte für den Datensatz:
Entfernung von Datensätzen, bei denen folgende Felder nicht mit Werten gefüllt sind:
properties.cclom:general_description (Beschreibungstexte)
additional_data.full_text (Volltexte)
properties.ccm:oeh_quality_neutralness (Neutralitäts-Score der Redaktionen)
Entfernung von Datensätzen, bei denen die Zeichenlänge der Texte nicht mind. 60 Zeichen hat
properties.cclom:general_description (Beschreibungstexte)
additional_data.full_text (Volltexte)
Reduktion und Normalisierung der Anzahl der Datensätze auf 2000
unter Einbezug der Felder: properties.ccm:oeh_quality_neutralness und properties.ccm:taxonid (Disziplinen)
Nutzung eines zyklischen Rundlaufverfahrens (Round Robin) zur Gleichverteilung
LLM und Prompt
LLM-Model
Für den Test wurde ein aktuelles LLM-Model genutzt, das aufgrund seiner niedrigen Kosten praxisnah ist: gpt-4o-mini (openai).
Der Test kann auch mit den meisten anderen LLM durchgeführt werden, jedoch ohne zwangsläufig die gleichen Ergebnisse zu erzielen. Ein Test mit weiteren Modelle erscheint sinnvoll.
Es ist davon auszugehen, dass ein erneuter Durchlauf mit dem gleichen Model auch nicht immer die gleichen Ergebnisse liefern wird.
Promptentwicklung
Grundlage des Prompts ist die zuvor genannte redaktionelle Skala. Diese wurde ausformuliert, um dem LLM eine genauere Einschätzung der Stufen zu ermöglichen.
Anschließend wurde der Prompt mit dem Promptverbesserungs-Tool “Generate” auf dem openai Playground verbessert (https://platform.openai.com/playground/chat ). Dieses strukturiert den Prompt maschinenlesbar, fügt Markdown-Formatierungen und passende Rückfragen zur Selbstreflexion hinzu.
Neben dem Scoring soll der Prompt auch eine kurze Begründung seiner Bewertung ausgeben, um die Ergebnisse später nachprüfen zu können.
Finaler Prompt:
Bewerte die Neutralität und Verfassungskonformität eines Bildungsinhalts auf einer Skala von 0 bis 5 anhand der folgenden Kriterien. Berücksichtigen Sie aktuelle Diskussionen über Menschenrechte, das Grundgesetz der Bundesrepublik Deutschland und die Anforderungen an schulische Bildung. Begründen Sie die Bewertung ausführlich, indem Sie erklären, warum Sie sich für den gewählten Wert entschieden haben und warum andere Werte nicht zutreffen. ## Bewertungskriterien - **0 (manipulativ oder verfassungswidrig):** Der Text enthält manipulative Aussagen, die darauf abzielen, den Leser in eine bestimmte Richtung zu drängen. Der Text verstößt gegen verfassungsrechtliche Grundprinzipien, wie die Achtung der Menschenwürde oder demokratische Grundwerte (z. B. Förderung von Hassrede, Diskriminierung, Gewalt oder Verherrlichung verfassungswidriger Symbole). - **1 (unneutral oder problematisch):** Der Text zeigt deutliche Anzeichen von Einseitigkeit oder Voreingenommenheit. Inhalte sind zwar verfassungskonform, aber sie verletzen möglicherweise das Neutralitätsgebot, indem sie eine extreme oder engstirnige Sichtweise auf sensible Themen wie Politik, Religion, Menschenrechte, Sexualität oder Identität vertreten, ohne Alternativen oder Gegensätze zu beleuchten. - **2 (ideologisch eingefärbt, aber verfassungskonform):** Der Text ist von einer bestimmten ideologischen Ausrichtung geprägt, aber die Inhalte sind verfassungskonform und korrekt. Es wird jedoch nur eine Seite des Themas beleuchtet, und es fehlen ausgewogene Perspektiven. - **3 (ideologisch eingefärbt, aber transparent und pluralistisch):** Der Text hat eine erkennbare ideologische Ausrichtung, aber diese wird transparent gemacht. Der Autor stellt offen dar, dass es alternative Perspektiven gibt, die ebenfalls verfassungskonform sind. Der Inhalt wahrt das Gebot der Pluralität, wie es in der schulischen Bildung gefordert wird. - **4 (neutrale und ausgewogene Formulierung):** Der Text verwendet eine sachliche und ausgewogene Formulierung ohne erkennbare ideologische Voreingenommenheit. Er beleuchtet das Thema von verschiedenen verfassungskonformen Standpunkten und berücksichtigt Menschenrechte und demokratische Werte, wie sie im Grundgesetz verankert sind. - **5 (neutrale Formulierung von einem unabhängigen, wissenschaftlich fundierten Autor):** Der Text ist vollständig neutral formuliert, ohne ideologische oder politische Voreingenommenheit. Er stammt von einem nachweislich unabhängigen und wissenschaftlich fundierten Autor oder einer Institution, die keine politischen oder ideologischen Interessen vertritt. Der Inhalt entspricht höchsten Standards der Verfassungstreue, Menschenrechte und Anforderungen der Schulbildung. # Steps 1. **Analysiere den Text:** Identifizieren Sie alle relevanten Informationen und Aspekte, die auf Neutralität und Verfassungskonformität hin überprüft werden müssen. 2. **Bewerten Sie anhand der Skala:** Wählen Sie den zutreffenden Wert von 0 bis 5 basierend auf Ihrer Analyse. 3. **Begründung:** Erläutern Sie Ihre Bewertung. Begründen Sie in maximal drei Sätzen, warum Sie sich für diesen Wert entschieden haben. Gehen Sie darauf ein, warum andere Werte nicht passend sind. # Output Format - Eine einzelne Zahl von 0 bis 5. - Eine nachfolgende Begründung in maximal 3 Sätzen. # Beispiele **Input:** Text über das Grundgesetz der Bundesrepublik Deutschland. **Output:** 4 "Der Text beinhaltet eine sachliche und ausgewogene Formulierung verschiedener verfassungskonformer Standpunkte und respektiert demokratische Werte. Er vermeidet extreme Sichtweisen und beleuchtet diverse Perspektiven. Andere Werte treffen nicht zu, da keine einseitige ideologische Ausrichtung erkennbar ist."
Analyse des Testdatensatzes
Die Bildungsinhalte von Wirlernenonline.de haben den Schwerpunkt auf Schulbildung.
Die Scorings für die Bewertung der Neutralität sind vor allem bei redaktionell gepflegten Inhalten vorhanden, die über die höchste Qualitätsstufe verfügen. Es gibt daher keine Gleichverteilung der Neutralitätswerte, sondern vorwiegend höher eingestufte Inhalte (4 und 5 auf der Skala). Es sollte geprüft werden, ob zukünftige Tests mit weiteren Daten oder synthetisch erzeugten Muster angereichert werden können.
Die Qualität der Beschreibungstexte und Volltexte wurden mit verschiedenen Metriken bestimmt.
Interpretation der Textqualität
Die Volltexte sind mit durchschnittlich 860 Zeichen länger als die Beschreibungstexte (228 Zeichen).
Die Verteilung der Sprachen ist zwischen beiden Feldern vergleichbar.
Unterschiede ergeben sich in der Sentiment-Analyse. Beschreibungstexte sind weniger emotional formuliert, was für eine höhere Qualität im Hinblick auf den Aspekt Neutralität sprechen kann.
Der SMOG-Index zeigt, dass die Beschreibungstexte mit weniger formaler Bildung zu verstehen sind, als die Volltexte. Ein Grund hierfür könnte die redaktionelle Aufbereitung sein.
Beschreibungstexte
Volltexte
Verteilung der Daten
Ein Großteil der Datensätze ist den Disziplinen: Informatik, Chemie, Physik, Mathematik und Darstellendes Spiel zuzuordnen. Fast alle Inhalte wurden auf der Skala mit 4 oder 5 bewertet, was jedoch im Rahmen der Erwartungen liegt.
Testdurchführung
Für die Testdurchführung wurde ein Python-Script genutzt, das ausgewählte Metadatenfelder aus JSON an den Prompt übergibt und das Scoring sowie die Begründung dokumentiert. Anschließend werden diverse Metriken aus dem Vergleich von Originaldaten und KI-generierten Daten gebildet. Eine hohe Übereinstimmung würde auf eine erfolgreiche Bewertung durch die KI hindeuten.
Das Python-Script ist in der Anlage zu finden.
Testergebnisse
Ein Großteil der Datensätze ist den Disziplinen: Informatik, Chemie, Physik, Mathematik und Darstellendes Spiel zuzuordnen. Fast alle Inhalte wurden auf der Skala mit 4 oder 5 bewertet, was jedoch im Rahmen der Erwartungen liegt.
Anlage
Tool für die Volltextgenerierung
# Web Scraper and Enricher for URL in JSON using Goose3 # requirements: pip install beautifulsoup4 streamlit requests goose3 import streamlit as st import json import os import datetime import time from goose3 import Goose # List of file extensions to skip (e.g. audio and video formats) SKIPPED_EXTENSIONS = ['.mp4', '.mp3', '.avi', '.mpeg', '.mov', '.wmv', '.flv'] # Function to scrape a webpage using Goose3 and return the extracted information def scrape_page(url, follow_redirect=True): goose = Goose() # Initialize Goose3 with default settings try: if isinstance(url, list): # Check if URL is in list format and convert to string url = url[0] # Skip URLs with certain extensions if any(url.lower().endswith(ext) for ext in SKIPPED_EXTENSIONS): return "skipped", None article = goose.extract(url=url) # Extract the relevant information full_text = article.cleaned_text # Cleaned full text title = article.title # Extracted title summary = article.meta_description[:500] if article.meta_description else full_text[:500] # Meta description as summary or first 500 chars keywords = article.meta_keywords # Extracted meta keywords top_image = article.top_image.src if article.top_image else None # URL of the top image # Fallback: If full text is missing, use meta description if not full_text: full_text = article.meta_description or "No full text available for this page." return { 'title': title, 'full_text': full_text, 'summary': summary, 'keywords': keywords, 'top_image': top_image, 'url': url } except Exception as e: st.error(f"Error scraping {url}: {e}") return None # Function to process the JSON file and enrich it with scraped data def process_json(data, url_field, timeout, save_folder, follow_redirect=True, skip_media_files=True, crawl_all=True, num_records=10): try: total_records = len(data) records_to_process = total_records if crawl_all else min(num_records, total_records) st.write(f"Processing {records_to_process} out of {total_records} records...") enriched_data = [] for i, record in enumerate(data[:records_to_process], 1): # Display the progress message for processing records st.info(f"Processing record {i}/{records_to_process}", icon="ℹ️") # Extract URL using the selected field try: url = eval(f"record{url_field}") # If URL is in list format, convert it to a string (take the first URL) if isinstance(url, list): url = url[0] except Exception as e: st.warning(f"No valid URL found for record {i}/{records_to_process}. Skipping... (Error: {e})", icon="⚠️") continue if url: # Skip media files if the option is selected if skip_media_files and any(url.lower().endswith(ext) for ext in SKIPPED_EXTENSIONS): st.warning(f"Skipping media file URL #{i}: {url}", icon="⚠️") continue # Display the message for scraping the current URL st.success(f"Scraping URL #{i}: {url}", icon="🟢") scraped_data = scrape_page(url, follow_redirect=follow_redirect) if scraped_data != "skipped" and scraped_data: # Only display the summary preview message without additional status info if scraped_data['summary']: st.success(scraped_data['summary'][:250] + "..." if len(scraped_data['summary']) > 250 else scraped_data['summary']) # Add scraped data to the record record['additional_data'] = { 'title': scraped_data['title'], 'full_text': scraped_data['full_text'], 'summary': scraped_data['summary'], 'keywords': scraped_data['keywords'], 'top_image': scraped_data['top_image'], 'final_url': scraped_data['url'] } enriched_data.append(record) # Introduce delay for the given timeout time.sleep(timeout) # Save the enriched JSON file timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S') enriched_file_name = os.path.join(save_folder, f"data_enriched_{timestamp}.json") with open(enriched_file_name, 'w', encoding='utf-8') as f: json.dump(enriched_data, f, ensure_ascii=False, indent=4) st.success(f"Enriched data saved as {enriched_file_name}", icon="🟢") except Exception as e: st.error(f"Error processing JSON file: {e}") # Function to extract available fields from JSON structure def extract_fields(data): field_set = set() # Recursive function to explore the JSON structure def recurse_json(obj, parent_key=''): if isinstance(obj, dict): for key, value in obj.items(): new_key = f"{parent_key}['{key}']" if parent_key else f"['{key}']" field_set.add(new_key) recurse_json(value, new_key) elif isinstance(obj, list): for item in obj: recurse_json(item, parent_key) recurse_json(data) return sorted(list(field_set)) # Streamlit UI st.title('JSON Web Scraper and Enricher using Goose3') # Upload JSON file uploaded_file = st.file_uploader("Choose a JSON file", type="json") if uploaded_file: try: # Load the JSON data data = json.load(uploaded_file) # Extract all field paths from the JSON structure available_fields = extract_fields(data) # Allow the user to choose a URL field url_field = st.selectbox("Select the URL field", available_fields, index=available_fields.index("['properties']['ccm:wwwurl']")) # Other options for processing timeout = st.number_input("Enter delay between requests (seconds)", min_value=0, value=0) save_folder = st.text_input("Folder to save enriched JSON", value=".") follow_redirects = st.checkbox("Follow redirects", value=True) # Option to skip media files (audio/video) skip_media_files = st.checkbox("Skip media files (e.g., .mp4, .mp3, .avi)", value=True) # Option to process all records or only a limited number crawl_all = st.checkbox("Crawl all records", value=True) num_records = st.number_input("Number of records to process", min_value=1, value=10, disabled=crawl_all) if st.button("Start Processing"): process_json(data, url_field, timeout, save_folder, follow_redirects, skip_media_files, crawl_all, num_records) except Exception as e: st.error(f"Error loading JSON file: {e}")
Tool für die Bewertung
Python-Script zur Durchführung des Tests und der Bewertung mittels LLM
# Script für die Bewertung der Neutralität (0-5) # Anforderungen: pip install streamlit openai pydantic scikit-learn scipy matplotlib numpy # Start: streamlit run app.py import streamlit as st import json import os import matplotlib.pyplot as plt from datetime import datetime from openai import OpenAI from pydantic import BaseModel, ValidationError from sklearn.metrics import precision_score, f1_score, mean_absolute_error, mean_squared_error, r2_score from scipy.stats import pearsonr import numpy as np import time # Überprüfen, ob scipy installiert ist try: from scipy.stats import pearsonr except ImportError: st.error("Die Bibliothek 'scipy' ist nicht installiert. Bitte installieren Sie sie mit `pip install scipy`.") st.stop() # OpenAI-Client initialisieren client = OpenAI(api_key=os.getenv('OPENAI_API_KEY')) # Definiere das Schema der erwarteten Antwort mit Pydantic class EvaluationResponse(BaseModel): score: int reasoning: str # Funktion zur Erzeugung eines Zeitstempels def get_timestamp(): return datetime.now().strftime("%Y%m%d_%H%M%S") # Funktion zum Speichern von Dateien def save_file(filename, content, mode='w'): with open(filename, mode) as f: f.write(content) # Funktion zur Erstellung und Speicherung von Grafiken def save_figure(filename): plt.savefig(filename) plt.close() # Funktion zur farblichen Hervorhebung der Statusmeldungen def color_status(deviation): if deviation == 0: return "green" elif deviation == 1: return "yellow" elif deviation == 2: return "orange" else: return "red" # Funktion zur Erstellung einer verkürzten JSON def create_shortened_json(data, ai_scores, neutralness_scores, ai_reasonings, selected_fields): shortened_data = [] # Sicherstellen, dass alle Listen gleich lang sind for i in range(min(len(data), len(ai_scores), len(ai_reasonings))): item = {} for field in selected_fields: # Extraktion der Werte basierend auf dem Feldpfad value = data[i].get(field, None) item[field] = value item.update({ "original_neutralness_score": neutralness_scores[i], "ai_neutralness_score": ai_scores[i], "ai_reasoning": ai_reasonings[i] }) shortened_data.append(item) return shortened_data # Funktion zur Rekursiven Extraktion von Feldnamen def extract_field_names(data): field_names = set() def recurse(item): if isinstance(item, dict): for key, value in item.items(): field_names.add(key) recurse(value) elif isinstance(item, list): for element in item: recurse(element) for entry in data: recurse(entry) return sorted(field_names) # Funktion zur Erstellung einer verkürzten JSON für Edgecases def create_shortened_json_edgecases(data, ai_scores, neutralness_scores, ai_reasonings, selected_fields): return create_shortened_json(data, ai_scores, neutralness_scores, ai_reasonings, selected_fields) # Hauptfunktion zur Analyse der Texte def analyze_texts(texts, neutralness_scores, model_choice, prompt, num_texts, data, json_filename, selected_fields): ai_scores = [] ai_reasonings = [] deviations = [] deviation_counts = {0: 0, 1: 0, 2: 0, 3: 0} failed_attempts = 0 special_cases = [] # Sammlung der Sonderfälle (Abweichung von 2 oder mehr) progress_bar = st.progress(0) status_text = st.empty() texts_to_evaluate = texts[:num_texts] # Nur eine bestimmte Anzahl bewerten total_texts = len(texts_to_evaluate) for i, text in enumerate(texts_to_evaluate): if text: # Kombiniere die ausgewählten Felder in einem einzigen Text combined_text = f"{prompt}\n\n" + "\n".join([f"{field}: {value}" for field, value in text.items() if value]) messages = [ {"role": "system", "content": "You are an AI tasked with evaluating the neutrality and constitutionality of educational content."}, {"role": "user", "content": combined_text} ] try: # Verwende response_format mit json_schema und füge den 'name'-Parameter hinzu response = client.chat.completions.create( model=model_choice, messages=messages, response_format={ "type": "json_schema", "json_schema": { "name": "neutrality_evaluation", # Required name field "schema": { "type": "object", "properties": { "score": {"type": "integer"}, "reasoning": {"type": "string"} }, "required": ["score", "reasoning"], "additionalProperties": False } } }, max_tokens=4000 ) # Antwort als JSON validieren evaluation_response = EvaluationResponse.parse_raw(response.choices[0].message.content) ai_score = evaluation_response.score ai_reasoning = evaluation_response.reasoning ai_scores.append(ai_score) ai_reasonings.append(ai_reasoning) # Vergleich der AI-Werte mit den Originalwerten original_score = neutralness_scores[i] deviation = abs(ai_score - original_score) deviations.append(deviation) # Abweichung speichern if deviation == 0: deviation_counts[0] += 1 elif deviation == 1: deviation_counts[1] += 1 elif deviation == 2: deviation_counts[2] += 1 special_cases.append(data[i]) # Als Sonderfall hinzufügen else: deviation_counts[3] += 1 special_cases.append(data[i]) # Als Sonderfall hinzufügen # Farbliche Statusmeldung je nach Abweichung color = color_status(deviation) st.markdown(f"<span style='color:{color}'>Text {i + 1} bewertet: AI Score = {ai_score} (Original: {original_score})</span>", unsafe_allow_html=True) except ValidationError as ve: st.error(f"Validierungsfehler bei Text {i + 1}: {ve}") failed_attempts += 1 except Exception as e: st.error(f"Fehler bei der Verarbeitung von Text {i + 1}: {e}") failed_attempts += 1 # Fortschritt aktualisieren progress = (i + 1) / total_texts progress_bar.progress(progress) status_text.text(f"Verarbeite Text {i + 1}/{total_texts}...") time.sleep(0.1) # Berechnung der durchschnittlichen Abweichung avg_deviation = np.mean(deviations) if deviations else 0 # Berechnung zusätzlicher Metriken mae = mean_absolute_error(neutralness_scores[:len(ai_scores)], ai_scores) mse = mean_squared_error(neutralness_scores[:len(ai_scores)], ai_scores) rmse = np.sqrt(mse) r2 = r2_score(neutralness_scores[:len(ai_scores)], ai_scores) pearson_corr, _ = pearsonr(neutralness_scores[:len(ai_scores)], ai_scores) # Berechnung von Precision und F1-Score y_true = neutralness_scores[:len(ai_scores)] # Originale Scores y_pred = ai_scores[:len(ai_scores)] # AI generierte Scores precision = precision_score(y_true, y_pred, average='weighted', zero_division=0) f1 = f1_score(y_true, y_pred, average='weighted') # Verkürzte JSON erstellen shortened_data = create_shortened_json(data, ai_scores, neutralness_scores, ai_reasonings, selected_fields) # Speichern der verkürzten JSON-Daten, strukturiert mit Indents shortened_filename = f"{json_filename}_shortened_{get_timestamp()}.json" with open(shortened_filename, 'w') as f: json.dump({ "data": shortened_data, "overall_results": { "precision": precision, "f1_score": f1, "mean_absolute_error": mae, "mean_squared_error": mse, "root_mean_squared_error": rmse, "r2_score": r2, "pearson_correlation": pearson_corr, "avg_deviation": avg_deviation } }, f, indent=4) # JSON strukturiert mit Indent st.success(f"Verkürzte JSON gespeichert als {shortened_filename}") # Verkürzte JSON für Edgecases erstellen (Abweichung >= 2) if special_cases: edgecase_indices = [i for i, dev in enumerate(deviations) if dev >= 2] shortened_edgecases = create_shortened_json_edgecases( [data[i] for i in edgecase_indices], [ai_scores[i] for i in edgecase_indices], [neutralness_scores[i] for i in edgecase_indices], [ai_reasonings[i] for i in edgecase_indices], selected_fields ) # Speichern der Edgecases JSON, strukturiert mit Indents edgecases_filename = f"{json_filename}_shortened_edge_cases_{get_timestamp()}.json" with open(edgecases_filename, 'w') as f: json.dump({ "data": shortened_edgecases, "overall_results": { "precision": precision_score([neutralness_scores[i] for i in edgecase_indices], [ai_scores[i] for i in edgecase_indices], average='weighted', zero_division=0), "f1_score": f1_score([neutralness_scores[i] for i in edgecase_indices], [ai_scores[i] for i in edgecase_indices], average='weighted'), "mean_absolute_error": mean_absolute_error([neutralness_scores[i] for i in edgecase_indices], [ai_scores[i] for i in edgecase_indices]), "mean_squared_error": mean_squared_error([neutralness_scores[i] for i in edgecase_indices], [ai_scores[i] for i in edgecase_indices]), "root_mean_squared_error": np.sqrt(mean_squared_error([neutralness_scores[i] for i in edgecase_indices], [ai_scores[i] for i in edgecase_indices])), "r2_score": r2_score([neutralness_scores[i] for i in edgecase_indices], [ai_scores[i] for i in edgecase_indices]), "pearson_correlation": pearsonr([neutralness_scores[i] for i in edgecase_indices], [ai_scores[i] for i in edgecase_indices])[0], "avg_deviation": np.mean([deviations[i] for i in edgecase_indices]) } }, f, indent=4) # JSON strukturiert mit Indent st.success(f"Edgecases JSON gespeichert als {edgecases_filename}") # Speichern der Ergebnisse mit zusätzlichen Metriken als Textdatei report_filename = f"{json_filename}_evaluation_report_{get_timestamp()}.txt" with open(report_filename, 'w') as f: f.write(f"Precision: {precision:.2f}\n") f.write(f"F1 Score: {f1:.2f}\n") f.write(f"Mean Absolute Error (MAE): {mae:.2f}\n") f.write(f"Mean Squared Error (MSE): {mse:.2f}\n") f.write(f"Root Mean Squared Error (RMSE): {rmse:.2f}\n") f.write(f"R² Score: {r2:.2f}\n") f.write(f"Pearson-Korrelation: {pearson_corr:.2f}\n") f.write(f"Average Deviation: {avg_deviation:.2f}\n") f.write(f"Failed Attempts: {failed_attempts}\n") f.write(f"Deviation Counts: {deviation_counts}\n") st.success(f"Bericht gespeichert als {report_filename}") # Rückgabe der berechneten Metriken return ai_scores, deviations, avg_deviation, failed_attempts, deviation_counts, special_cases, precision, f1, mae, mse, rmse, r2, pearson_corr # Streamlit UI zur Benutzerinteraktion st.title('Neutralitätsbewertung AI') # Titel entsprechend dem vorherigen Titel beibehalten uploaded_file = st.file_uploader("Lade eine JSON-Datei hoch", type="json") if uploaded_file: # JSON-Dateiname extrahieren json_filename = os.path.splitext(uploaded_file.name)[0] # JSON-Daten laden data = json.load(uploaded_file) # Rekursive Extraktion aller Feldnamen field_names = extract_field_names(data) # Standardvorgabe: 'properties_ccm:general_description', falls vorhanden default_fields = ['properties_ccm:general_description'] if 'properties_ccm:general_description' in field_names else [] # Mehrfachauswahl für die Metadatenfelder selected_fields = st.multiselect( "Wähle die Metadatenfelder für die Bewertung aus:", options=field_names, default=default_fields ) if not selected_fields: st.warning("Bitte wähle mindestens ein Metadatenfeld aus.") else: # Extrahieren der relevanten Felder basierend auf der Auswahl def extract_selected_fields(json_data, selected_fields): extracted = [] for item in json_data: record = {} for field in selected_fields: value = item.get(field, None) record[field] = value extracted.append(record) return extracted texts_to_evaluate = extract_selected_fields(data, selected_fields) # Extrahieren der neutralness_scores def extract_metadata(json_data, key): return [int(item.get(key, "0")) for item in json_data if item.get(key, "0").isdigit()] neutralness_scores = extract_metadata(data, "properties_ccm:oeh_quality_neutralness") if not neutralness_scores: st.error("Keine gültigen Neutralitätswerte gefunden in den Daten.") else: # Verteilung von ccm:oeh_quality_neutralness anzeigen st.subheader("Verteilung der Neutralitätswerte (Original)") fig, ax = plt.subplots() ax.hist(neutralness_scores, bins=range(0, 7), align='left', rwidth=0.8, color='skyblue', edgecolor='black') ax.set_xlabel('Neutralitätswert') ax.set_ylabel('Häufigkeit') st.pyplot(fig) # Speichern der Verteilungsgrafik dist_filename = f"{json_filename}_neutralness_distribution_{get_timestamp()}.png" save_figure(dist_filename) # Auswahl, ob alle oder nur bestimmte Datensätze bewertet werden sollen evaluate_all = st.checkbox("Alle Datensätze bewerten", value=True) max_evaluations = len(data) # Standardmäßig alle Datensätze bewerten if not evaluate_all: max_evaluations = st.number_input("Anzahl der zu bewertenden Datensätze", min_value=1, max_value=len(data), value=10) # Eingabefeld für den Prompt (Standardprompt ist vorausgefüllt) default_prompt = """ Bewerte die Neutralität und Verfassungskonformität eines Bildungsinhalts auf einer Skala von 0 bis 5 anhand der folgenden Kriterien. Berücksichtigen Sie aktuelle Diskussionen über Menschenrechte, das Grundgesetz der Bundesrepublik Deutschland und die Anforderungen an schulische Bildung. Begründen Sie die Bewertung ausführlich, indem Sie erklären, warum Sie sich für den gewählten Wert entschieden haben und warum andere Werte nicht zutreffen. ## Bewertungskriterien - **0 (manipulativ oder verfassungswidrig):** Der Text enthält manipulative Aussagen, die darauf abzielen, den Leser in eine bestimmte Richtung zu drängen. Der Text verstößt gegen verfassungsrechtliche Grundprinzipien, wie die Achtung der Menschenwürde oder demokratische Grundwerte (z. B. Förderung von Hassrede, Diskriminierung, Gewalt oder Verherrlichung verfassungswidriger Symbole). - **1 (unneutral oder problematisch):** Der Text zeigt deutliche Anzeichen von Einseitigkeit oder Voreingenommenheit. Inhalte sind zwar verfassungskonform, aber sie verletzen möglicherweise das Neutralitätsgebot, indem sie eine extreme oder engstirnige Sichtweise auf sensible Themen wie Politik, Religion, Menschenrechte, Sexualität oder Identität vertreten, ohne Alternativen oder Gegensätze zu beleuchten. - **2 (ideologisch eingefärbt, aber verfassungskonform):** Der Text ist von einer bestimmten ideologischen Ausrichtung geprägt, aber die Inhalte sind verfassungskonform und korrekt. Es wird jedoch nur eine Seite des Themas beleuchtet, und es fehlen ausgewogene Perspektiven. - **3 (ideologisch eingefärbt, aber transparent und pluralistisch):** Der Text hat eine erkennbare ideologische Ausrichtung, aber diese wird transparent gemacht. Der Autor stellt offen dar, dass es alternative Perspektiven gibt, die ebenfalls verfassungskonform sind. Der Inhalt wahrt das Gebot der Pluralität, wie es in der schulischen Bildung gefordert wird. - **4 (neutrale und ausgewogene Formulierung):** Der Text verwendet eine sachliche und ausgewogene Formulierung ohne erkennbare ideologische Voreingenommenheit. Er beleuchtet das Thema von verschiedenen verfassungskonformen Standpunkten und berücksichtigt Menschenrechte und demokratische Werte, wie sie im Grundgesetz verankert sind. - **5 (neutrale Formulierung von einem unabhängigen, wissenschaftlich fundierten Autor):** Der Text ist vollständig neutral formuliert, ohne ideologische oder politische Voreingenommenheit. Er stammt von einem nachweislich unabhängigen und wissenschaftlich fundierten Autor oder einer Institution, die keine politischen oder ideologischen Interessen vertritt. Der Inhalt entspricht höchsten Standards der Verfassungstreue, Menschenrechte und Anforderungen der Schulbildung. # Steps 1. **Analysiere den Text:** Identifizieren Sie alle relevanten Informationen und Aspekte, die auf Neutralität und Verfassungskonformität hin überprüft werden müssen. 2. **Bewerten Sie anhand der Skala:** Wählen Sie den zutreffenden Wert von 0 bis 5 basierend auf Ihrer Analyse. 3. **Begründung:** Erläutern Sie Ihre Bewertung. Begründen Sie in maximal drei Sätzen, warum Sie sich für diesen Wert entschieden haben. Gehen Sie darauf ein, warum andere Werte nicht passend sind. # Output Format - Eine einzelne Zahl von 0 bis 5. - Eine nachfolgende Begründung in maximal 3 Sätzen. # Beispiele **Input:** Text über das Grundgesetz der Bundesrepublik Deutschland. **Output:** 4 "Der Text beinhaltet eine sachliche und ausgewogene Formulierung verschiedener verfassungskonformer Standpunkte und respektiert demokratische Werte. Er vermeidet extreme Sichtweisen und beleuchtet diverse Perspektiven. Andere Werte treffen nicht zu, da keine einseitige ideologische Ausrichtung erkennbar ist." """ user_prompt = st.text_area("Passe deinen Prompt an", value=default_prompt, height=600) # Modellwahl hinzufügen (gpt-4o-mini als Standard und gpt-4o-2024-08-06) model_choice = st.selectbox( "Wähle das Modell für die Bewertung aus", options=["gpt-4o-mini", "gpt-4o-2024-08-06"], index=0 ) # Button zur Auslösung der Analyse if st.button("Bewerte Texte"): ai_scores, deviations, avg_deviation, failed_attempts, deviation_counts, special_cases, precision, f1, mae, mse, rmse, r2, pearson_corr = analyze_texts( texts_to_evaluate, neutralness_scores, model_choice, user_prompt, max_evaluations, data, json_filename, selected_fields) # Verteilung der Originaldaten am Ende erneut anzeigen st.subheader("Verteilung der Neutralitätswerte (Original)") fig, ax = plt.subplots() ax.hist(neutralness_scores, bins=range(0, 7), align='left', rwidth=0.8, color='skyblue', edgecolor='black') ax.set_xlabel('Neutralitätswert') ax.set_ylabel('Häufigkeit') st.pyplot(fig) # Speichern der Originaldaten-Verteilungsgrafik final_dist_filename = f"{json_filename}_neutralness_distribution_final_{get_timestamp()}.png" save_figure(final_dist_filename) # Verteilung der AI-Scores anzeigen st.subheader("Verteilung der Neutralitätswerte (AI)") fig, ax = plt.subplots() ax.hist(ai_scores, bins=range(0, 7), align='left', rwidth=0.8, color='lightgreen', edgecolor='black') ax.set_xlabel('AI Neutralitätswert') ax.set_ylabel('Häufigkeit') st.pyplot(fig) # Speichern der AI-Scores-Verteilungsgrafik ai_dist_filename = f"{json_filename}_ai_neutralness_distribution_{get_timestamp()}.png" save_figure(ai_dist_filename) # Abweichungsgrafik erstellen st.subheader("Abweichung der AI-Werte von den Originalwerten") fig, ax = plt.subplots() categories = ["Keine Abweichung", "1 Abweichung", "2 Abweichungen", "3+ Abweichungen"] counts = [deviation_counts[0], deviation_counts[1], deviation_counts[2], deviation_counts[3]] colors = ["green", "yellow", "orange", "red"] ax.bar(categories, counts, color=colors) ax.set_xlabel('Abweichungskategorie') ax.set_ylabel('Anzahl der Texte') st.pyplot(fig) # Speichern der Abweichungsgrafik deviation_filename = f"{json_filename}_deviation_distribution_{get_timestamp()}.png" save_figure(deviation_filename) # Grafiken für Precision, F1 Score und zusätzliche Metriken erstellen st.subheader("Modellleistungsmetriken") # Precision und F1 Score fig, ax = plt.subplots() metrics = ["Precision", "F1 Score"] scores = [precision, f1] ax.bar(metrics, scores, color=['blue', 'green']) ax.set_ylabel('Score') for i, v in enumerate(scores): ax.text(i, v + 0.01, f"{v:.2f}", ha='center', va='bottom') st.pyplot(fig) # Speichern der Precision und F1 Score Grafik precision_f1_filename = f"{json_filename}_precision_f1_score_{get_timestamp()}.png" save_figure(precision_f1_filename) # Zusätzliche Metriken mit Erklärungen anzeigen st.markdown(""" **Mean Absolute Error (MAE):** - Durchschnittlicher absoluter Unterschied zwischen den vorhergesagten und den tatsächlichen Werten. - **Interpretation:** Je niedriger der MAE, desto genauer sind die Vorhersagen des Modells. **Mean Squared Error (MSE):** - Durchschnitt der quadrierten Differenzen zwischen den vorhergesagten und den tatsächlichen Werten. - **Interpretation:** Betont größere Fehler stärker. Ein niedriger MSE zeigt eine gute Modellleistung an. **Root Mean Squared Error (RMSE):** - Quadratwurzel des MSE. - **Interpretation:** Gibt den Fehler in derselben Einheit wie die Zielvariable an. Niedrigere Werte sind besser. **R² Score:** - Maß dafür, wie gut die Varianz der Zielvariable durch das Modell erklärt wird. - **Interpretation:** Werte nahe 1 bedeuten, dass das Modell die Varianz gut erklärt. **Pearson-Korrelation:** - Maß für die lineare Korrelation zwischen den vorhergesagten und den tatsächlichen Werten. - **Interpretation:** Werte nahe 1 oder -1 zeigen eine starke lineare Beziehung. Werte nahe 0 bedeuten keine lineare Beziehung. **Precision:** - Maß für die Genauigkeit der positiven Vorhersagen. - **Interpretation:** Ein höherer Precision-Wert zeigt, dass weniger falsche positive Vorhersagen gemacht werden. **F1 Score:** - Harmonisches Mittel von Precision und Recall. - **Interpretation:** Ein ausgewogenes Maß, das sowohl die Genauigkeit als auch die Vollständigkeit der Vorhersagen berücksichtigt. **Abweichung:** - Differenz zwischen den AI-bewerteten und den Originalwerten. - **Interpretation:** Niedrigere Abweichungswerte deuten auf eine höhere Übereinstimmung zwischen AI und Originalbewertungen hin. """) # Anzeige der zusätzlichen Metriken st.markdown(f"**Mean Absolute Error (MAE):** {mae:.2f}") st.markdown(f"**Mean Squared Error (MSE):** {mse:.2f}") st.markdown(f"**Root Mean Squared Error (RMSE):** {rmse:.2f}") st.markdown(f"**R² Score:** {r2:.2f}") st.markdown(f"**Pearson-Korrelation:** {pearson_corr:.2f}") st.markdown(f"**Precision:** {precision:.2f}") st.markdown(f"**F1 Score:** {f1:.2f}") st.markdown(f"**Durchschnittliche Abweichung:** {avg_deviation:.2f}") # Grafik für zusätzliche Metriken st.subheader("Zusätzliche Modellleistungsmetriken") metrics = ["MAE", "MSE", "RMSE", "R²", "Pearson-Korrelation", "Precision", "F1 Score", "Abweichung"] scores = [mae, mse, rmse, r2, pearson_corr, precision, f1, avg_deviation] colors = ['cyan', 'magenta', 'orange', 'purple', 'grey', 'blue', 'green', 'red'] fig, ax = plt.subplots(figsize=(12, 6)) ax.bar(metrics, scores, color=colors) ax.set_ylabel('Wert') ax.set_ylim(0, max(scores) * 1.2) # Anpassung des y-Bereichs for i, v in enumerate(scores): ax.text(i, v + max(scores)*0.01, f"{v:.2f}", ha='center', va='bottom') st.pyplot(fig) # Speichern der zusätzlichen Metriken Grafik additional_metrics_filename = f"{json_filename}_additional_metrics_{get_timestamp()}.png" save_figure(additional_metrics_filename) # Durchschnittliche Abweichung grafisch darstellen st.subheader("Durchschnittliche Abweichung") fig, ax = plt.subplots() ax.bar(["Durchschnittliche Abweichung"], [avg_deviation], color='purple') ax.set_ylabel('Abweichung') ax.text(0, avg_deviation + 0.01, f"{avg_deviation:.2f}", ha='center', va='bottom') st.pyplot(fig) # Speichern der Durchschnittlichen Abweichungsgrafik avg_deviation_filename = f"{json_filename}_avg_deviation_{get_timestamp()}.png" save_figure(avg_deviation_filename) st.write(f"**Precision:** {precision:.2f}") st.write(f"**F1 Score:** {f1:.2f}") st.write(f"**Mean Absolute Error (MAE):** {mae:.2f}") st.write(f"**Mean Squared Error (MSE):** {mse:.2f}") st.write(f"**Root Mean Squared Error (RMSE):** {rmse:.2f}") st.write(f"**R² Score:** {r2:.2f}") st.write(f"**Pearson-Korrelation:** {pearson_corr:.2f}") st.write(f"**Durchschnittliche Abweichung:** {avg_deviation:.2f}") # Speichern der Auswertung als Textdatei report_filename = f"{json_filename}_evaluation_report_{get_timestamp()}.txt" with open(report_filename, 'w') as f: f.write(f"Precision: {precision:.2f}\n") f.write(f"F1 Score: {f1:.2f}\n") f.write(f"Mean Absolute Error (MAE): {mae:.2f}\n") f.write(f"Mean Squared Error (MSE): {mse:.2f}\n") f.write(f"Root Mean Squared Error (RMSE): {rmse:.2f}\n") f.write(f"R² Score: {r2:.2f}\n") f.write(f"Pearson-Korrelation: {pearson_corr:.2f}\n") f.write(f"Average Deviation: {avg_deviation:.2f}\n") f.write(f"Failed Attempts: {failed_attempts}\n") f.write(f"Deviation Counts: {deviation_counts}\n") st.success(f"Bericht gespeichert als {report_filename}") # Zusammenfassung der Ergebnisse st.markdown(""" ### **Zusammenfassung der Ergebnisse** - **Precision:** Gibt an, wie genau die positiven Vorhersagen des Modells sind. - **F1 Score:** Harmonisches Mittel von Precision und Recall, gibt ein ausgewogenes Maß der Modellleistung. - **Mean Absolute Error (MAE):** Durchschnittlicher absoluter Unterschied zwischen den vorhergesagten und den tatsächlichen Werten. - **Mean Squared Error (MSE):** Durchschnitt der quadrierten Differenzen zwischen den vorhergesagten und den tatsächlichen Werten. - **Root Mean Squared Error (RMSE):** Quadratwurzel des MSE, gibt den Fehler in derselben Einheit wie die Zielvariable an. - **R² Score:** Maß dafür, wie gut die Varianz der Zielvariable durch das Modell erklärt wird. - **Pearson-Korrelation:** Maß für die lineare Korrelation zwischen den vorhergesagten und den tatsächlichen Werten. - **Durchschnittliche Abweichung:** Durchschnittlicher Unterschied zwischen den AI-bewerteten und den Originalwerten. """) # Erklärung der Metriken st.markdown(""" --- ### **Erklärung der Metriken** **Mean Absolute Error (MAE):** - Durchschnittlicher absoluter Unterschied zwischen den vorhergesagten und den tatsächlichen Werten. - **Interpretation:** Je niedriger der MAE, desto genauer sind die Vorhersagen des Modells. **Mean Squared Error (MSE):** - Durchschnitt der quadrierten Differenzen zwischen den vorhergesagten und den tatsächlichen Werten. - **Interpretation:** Betont größere Fehler stärker. Ein niedriger MSE zeigt eine gute Modellleistung an. **Root Mean Squared Error (RMSE):** - Quadratwurzel des MSE. - **Interpretation:** Gibt den Fehler in derselben Einheit wie die Zielvariable an. Niedrigere Werte sind besser. **R² Score:** - Maß dafür, wie gut die Varianz der Zielvariable durch das Modell erklärt wird. - **Interpretation:** Werte nahe 1 bedeuten, dass das Modell die Varianz gut erklärt. **Pearson-Korrelation:** - Maß für die lineare Korrelation zwischen den vorhergesagten und den tatsächlichen Werten. - **Interpretation:** Werte nahe 1 oder -1 zeigen eine starke lineare Beziehung. Werte nahe 0 bedeuten keine lineare Beziehung. **Precision:** - Maß für die Genauigkeit der positiven Vorhersagen. - **Interpretation:** Ein höherer Precision-Wert zeigt, dass weniger falsche positive Vorhersagen gemacht werden. **F1 Score:** - Harmonisches Mittel von Precision und Recall. - **Interpretation:** Ein ausgewogenes Maß, das sowohl die Genauigkeit als auch die Vollständigkeit der Vorhersagen berücksichtigt. **Abweichung:** - Differenz zwischen den AI-bewerteten und den Originalwerten. - **Interpretation:** Niedrigere Abweichungswerte deuten auf eine höhere Übereinstimmung zwischen AI und Originalbewertungen hin. --- """)