Ein medizinisches 7B-Modell kostenlos feintunen: QLoRA, FAISS und Sicherheitsleitplanken
Eine kostenlose Pipeline auf einer Kaggle-T4: synthetische Daten, QLoRA, RAG über klassische Texte, eine geprüfte Wissensschicht, fest verdrahtete Notfall-Leitplanken, Veröffentlichung als GGUF und die Bugs unterwegs.
Medizinische Chatbots scheitern meist auf eine von zwei Arten. Entweder erfinden sie Behandlungen, oder sie rühren traditionelle und evidenzbasierte Medizin zu einem gefährlichen Eintopf zusammen. Ziel war hier ein Modell, das beide Welten strikt trennt, zeigt, woher seine Informationen stammen, und sich zurückzieht, wenn jemand einen Notfall schildert. Es heißt Expert Vaidya, trennt ayurvedische Medizin von modernen klinischen Leitlinien und wurde komplett ohne Kosten gebaut.
Als Hardware dienten zwei Tesla-T4-GPUs im kostenlosen Kaggle-Tarif. Das Modell ist ein 7B-Modell mit offenen Gewichten, und das Wissen stammt aus gemeinfreien klassischen Texten. Das Ergebnis läuft offline auf einem Laptop mit 16 GB und lässt sich mit einem einzigen Befehl installieren. Hier ist die vollständige Pipeline.
Die Bausteine
- Basismodell: Qwen2.5-7B-Instruct, feingetunt mit Unsloth und QLoRA
- Trainingsdaten: 52 strukturierte Beispiele, 2 von Hand geschrieben und 50 synthetische, die einen Qualitätsfilter passiert haben
- Retrieval: ein FAISS-Index über 6.765 Chunks aus 1.492 Seiten der Sushruta Samhita (gemeinfrei auf Archive.org) sowie eine geprüfte Wissensbasis mit 22 Einträgen aus WHO, ADA, NICE und PubMed Central
- Sicherheit: Leitplanken-Code in reinem Python, ganz ohne Modell, der jede Frage prüft, bevor sie das LLM erreicht
- Auslieferung: eine GGUF-Datei im Format Q4_K_M (4,68 GB), veröffentlicht auf Hugging Face und in der Ollama-Registry
Schritt 1: günstige synthetische Daten
Gelabelte medizinische Daten kosten mehr, als ein Hobbybudget hergibt, also hat ein kleineres Modell, Qwen2.5-3B in 4 Bit, die Beispiele zu 50 medizinischen Themen unter einem strengen Ausgabevertrag erzeugt. Jede Antwort musste einen ayurvedischen, einen modernen und einen historischen Abschnitt enthalten, und alles Unvollständige wurde automatisch verworfen:
python has_all_3 = all(x in response for x in ["Ayurvedic", "Modern", "Historical"])
if has_all_3 and len(response) > 200:
keep(example)
Alle 50 generierten Beispiele kamen durch. Mit den 2 handgeschriebenen Ausgangsbeispielen ergibt das 52 Trainingszeilen. Das ist ein winziger Datensatz, aber es ging nicht darum, Fakten zu vermitteln, sondern Format und Disziplin. Die Fakten kommen aus dem Retrieval.
Schritt 2: QLoRA auf kostenlosen T4s
Unsloth hat auf den beiden T4s von Kaggle nur LoRA-Adapter trainiert:
model = FastLanguageModel.get_peft_model(
model, r=16,
target_modules=["q_proj","k_proj","v_proj","o_proj",
"gate_proj","up_proj","down_proj"],
lora_alpha=16, lora_dropout=0,
use_gradient_checkpointing="unsloth",
)
Nur 0,53 % der Gewichte haben sich bewegt: 40.370.176 trainierbare Parameter von insgesamt 7.655.986.688, 5 Epochen, 35 Schritte, eine effektive Batchgröße von 8 und ein finaler Trainings-Loss von 1,0493. Die Laufzeit lag unter 10 Minuten. Medizin wurde dabei nicht gelernt. Das Modell hat gelernt, immer in der fünfteiligen Struktur zu antworten und die Bereiche nie zu vermischen.
Schritt 3: 1.492 Seiten in FAISS
Ein Format allein macht Antworten nicht fundiert. Zwei gemeinfreie englische Übersetzungen der Sushruta Samhita (812 und 680 Seiten) stammen von Archive.org. PyMuPDF hat den Text extrahiert, der rekursive Splitter von LangChain hat ihn in Chunks von 500 Zeichen mit 100 Zeichen Überlappung zerlegt, und all-MiniLM-L6-v2 hat die Embeddings berechnet. Das ergab 6.765 Chunks, normalisiert und in einen FAISS-IndexFlatIP geladen.
Bei einer Anfrage holt das System die Top-k-Chunks, wendet aber eine Relevanzschwelle an, einen Score von mindestens 0,3 bis 0,4, damit irrelevante Seiten nie im Kontextfenster landen.
Schritt 4: eine geprüfte Schicht
Alte, aus Druckwerken gescannte Übersetzungen sind verrauschtes OCR, deshalb steht neben dem ersten ein zweiter, kuratierter Index. Er enthält 22 von Hand geprüfte Einträge, jeweils mit Quellenangabe und Konfidenzwert. Zu den Quellen gehören die Charaka Samhita, die Sushruta Samhita und die Ashtanga Hridaya, WHO-Leitlinien von 2023 und 2024, die ADA Standards of Care 2025, NICE-Hinweise zu Warnzeichen (NG185 und NG173) sowie drei Studien aus PubMed Central: Gymnema PMC2915138, Boswellia PMC3251738 und Ashwagandha PMC3573577. Eine Tabelle zu Wechselwirkungen zwischen Kräutern und Medikamenten für Warfarin und Diabetesmedikamente rundet das Ganze ab. Das Retrieval behält nur Einträge mit verified == True und confidence >= 0.8.
Schritt 5: Leitplanken greifen zuerst
Die zentrale Designentscheidung ist, dass Notfälle nie beim Modell ankommen. Reines Python prüft die Eingabe vor allem anderen:
warnings, disclaimer = safety_guard.generate_safety_response(user_question)
if warnings and "EMERGENCY" in warnings[0]:
return warnings[0] + disclaimer # LLM never runs
Es achtet auf 14 Notfallindikatoren wie Brustschmerzen, Anzeichen eines Schlaganfalls, Anaphylaxie und suizidale Äußerungen sowie auf 7 gefährliche Muster der Selbstbehandlung wie "stop taking", "replace insulin" oder "double the dose". Eine Nachricht über Brustschmerzen liefert eine Triage-Warnung mit Notrufnummern (112, 911, 999), und zwar jedes einzelne Mal gleich.
Schritt 6: quantisieren, veröffentlichen, bereitstellen
Nach dem Zusammenführen der LoRA-Gewichte hat Unsloth eine GGUF-Datei mit 4,68 GB im Format Q4_K_M geschrieben, die auf Hugging Face und in die Ollama-Registry hochgeladen wurde. Jetzt kann jeder das Modell ausführen:
bash ollama run tejaas6/expert-vaidya
System-Prompt, Temperatur und Verhalten der Leitplanken stecken im Modell selbst. Sie brauchen weder Einrichtung noch API-Schlüssel, und alles funktioniert vollständig offline.
Was schiefging
Hier kommt der Teil, über den kaum jemand schreibt:
- Eine endlose Aufzählungsschleife. Bei niedriger Temperatur erzeugte das Modell einmal Hunderte Varianten von "hyaluronan polymer...". Die Einstellung
no_repeat_ngram_size=3hat das gestoppt. - Ein Absturz beim Upload auf Hugging Face. Eine Datei mit 4,4 GB löste einen ImportError in
XetProgressReporteraus. Die Lösung war,HF_HUB_DISABLE_XET=1zu setzen, um den Xet-Übertragungsweg zu umgehen, und die zwischengespeicherten Module zu leeren, bevor erneut importiert wurde. - Das verschwindende /tmp bei Kaggle. Unsloth schrieb den GGUF-Export nach
/tmp, weil im Arbeitsverzeichnis der Platz knapp war, und/tmpwird nicht als Kernel-Ausgabe aufbewahrt. Ein explizitesshutil.copynach/kaggle/workingvor dem Zippen hat das Problem gelöst. - Sprachliche Ausrutscher. Qwen schmuggelte manchmal chinesische Schriftzeichen in die Antworten. Ein Regex-Nachbearbeiter und eine Regel im Prompt, nur Englisch zu verwenden, haben das bereinigt.
Ergebnisse
- Eine Frage zu Diabetes verweist auf die Prameha-Klassifikation der Charaka und auf die diagnostischen Schwellen von WHO und ADA, jeweils getrennt.
- Die Frage, ob Gymnema zusammen mit Diabetesmedikamenten eingenommen werden kann, führt zu einem Hinweis auf das additive Risiko einer Unterzuckerung und auf das Überwachungsprotokoll.
- Die Schilderung eines drückenden Brustschmerzes umgeht das LLM vollständig und liefert reine Triage-Hinweise.
- Eine erfundene Krankheit bekommt eine Absage statt einer erfundenen Behandlung.
Wo es zu finden ist
Das Modell lässt sich aus der Ollama-Registry laden, die GGUF-Gewichte liegen auf Hugging Face unter Expert-Vaidya-Qwen7B-GGUF. Es handelt sich um ein Lern- und Forschungsprojekt. Es ist weder medizinischer Rat noch ein Diagnosewerkzeug.