Главная / Статьи / Бесплатное дообучение медицинской 7B-модели: QLoRA, FAISS и защитные ограничения

Бесплатное дообучение медицинской 7B-модели: QLoRA, FAISS и защитные ограничения

Конвейер с нулевым бюджетом на Kaggle T4: синтетические данные, QLoRA, RAG по классическим текстам, проверенный слой знаний, жёстко заданные экстренные ограничения, релиз в GGUF и баги, на которые мы наткнулись.

1083 слов

Медицинские чат-боты обычно ошибаются одним из двух способов. Либо выдумывают лечение, либо смешивают традиционную и доказательную медицину в одну опасную кашу. Задача была в том, чтобы сделать модель, которая строго разделяет эти два мира, показывает, откуда берёт информацию, и отходит в сторону, когда человек описывает экстренную ситуацию. Она называется Expert Vaidya, отделяет аюрведу от современных клинических рекомендаций, и вся она сделана бесплатно.

Железо — две видеокарты Tesla T4 на бесплатном тарифе Kaggle. Модель — 7B с открытыми весами, а знания взяты из классических текстов в общественном достоянии. Результат работает офлайн на ноутбуке с 16 ГБ и ставится одной командой. Ниже весь конвейер.

Из чего всё состоит

  • Базовая модель: Qwen2.5-7B-Instruct, дообученная с Unsloth и QLoRA
  • Обучающие данные: 52 структурированных примера, 2 написаны вручную и 50 синтетических, прошедших фильтр качества
  • Поиск: индекс FAISS по 6 765 чанкам из 1 492 страниц «Сушрута-самхиты» (общественное достояние на Archive.org) плюс проверенная база знаний из 22 записей на основе WHO, ADA, NICE и PubMed Central
  • Безопасность: код ограничений на обычном Python, без всякой модели, который проверяет каждый вопрос до того, как тот попадёт в LLM
  • Поставка: файл GGUF Q4_K_M (4,68 ГБ), опубликованный на Hugging Face и в реестре Ollama

Шаг 1: дешёвые синтетические данные

Размеченные медицинские данные стоят больше, чем позволяет бюджет хобби-проекта, поэтому примеры по 50 медицинским темам сгенерировала модель поменьше, Qwen2.5-3B в 4 битах, по строгому контракту на формат вывода. В каждом ответе должны были быть аюрведический раздел, современный раздел и исторический раздел, а всё неполное отбрасывалось автоматически:

python has_all_3 = all(x in response for x in ["Ayurvedic", "Modern", "Historical"])
if has_all_3 and len(response) > 200:
    keep(example)

Все 50 сгенерированных примеров прошли проверку. Вместе с 2 написанными вручную затравками получилось 52 обучающих строки. Набор крошечный, но цель была не научить фактам. Цель была научить формату и дисциплине. Факты приходят из поиска.

Шаг 2: QLoRA на бесплатных T4

Unsloth на двух T4 в Kaggle обучал только адаптеры LoRA:

    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",
)

Изменились лишь 0,53% весов: 40 370 176 обучаемых параметров из 7 655 986 688, 5 эпох, 35 шагов, эффективный размер батча 8 и итоговый loss на обучении 1,0493. По времени вышло меньше 10 минут. Медицине модель здесь не училась. Она научилась всегда отвечать в пятичастной структуре и никогда не смешивать области.

Шаг 3: 1 492 страницы в FAISS

Формат сам по себе не делает ответы обоснованными. Два английских перевода «Сушрута-самхиты» в общественном достоянии (812 и 680 страниц) взяты с Archive.org. PyMuPDF извлёк текст, рекурсивный сплиттер LangChain нарезал его на чанки по 500 символов с перекрытием 100, а all-MiniLM-L6-v2 построил эмбеддинги. Получилось 6 765 чанков, нормализованных и загруженных в FAISS IndexFlatIP.

При запросе система достаёт top-k чанков, но с порогом релевантности, оценкой не ниже 0,3–0,4, так что нерелевантные страницы в окно контекста не попадают.

Шаг 4: проверенный слой

Старые переводы, отсканированные с печатных изданий, — это шумный OCR, поэтому рядом с первым индексом стоит второй, тщательно отобранный. В нём 22 записи, проверенные вручную, у каждой указан источник и оценка уверенности. Среди источников «Чарака-самхита», «Сушрута-самхита» и «Аштанга-хридая», рекомендации WHO 2023 и 2024 годов, ADA Standards of Care 2025, рекомендации NICE по тревожным признакам (NG185 и NG173) и три исследования из PubMed Central: Gymnema PMC2915138, Boswellia PMC3251738 и Ashwagandha PMC3573577. Дополняет всё таблица взаимодействий трав и лекарств для варфарина и препаратов от диабета. Поиск оставляет только записи с verified == True и confidence >= 0.8.

Шаг 5: ограничения срабатывают первыми

Ключевое проектное решение: экстренные случаи никогда не доходят до модели. Обычный Python проверяет ввод раньше всего остального:

warnings, disclaimer = safety_guard.generate_safety_response(user_question)
if warnings and "EMERGENCY" in warnings[0]:
    return warnings[0] + disclaimer   # LLM never runs

Он отслеживает 14 признаков экстренной ситуации, например боль в груди, признаки инсульта, анафилаксию и суицидальные высказывания, плюс 7 опасных паттернов самолечения вроде "stop taking", "replace insulin" или "double the dose". На сообщение о боли в груди возвращается предупреждение для сортировки с экстренными номерами (112, 911, 999), каждый раз одно и то же.

Шаг 6: квантование, публикация, развёртывание

После слияния весов LoRA Unsloth записал GGUF на 4,68 ГБ в формате Q4_K_M, который отправился на Hugging Face и в реестр Ollama. Теперь запустить модель может кто угодно:

bash   ollama run tejaas6/expert-vaidya

Системный промпт, температура и поведение ограничений поставляются внутри модели. Никакой настройки и никакого API-ключа не нужно, и всё работает полностью офлайн.

Что пошло не так

Вот часть, о которой мало кто пишет:

  • Бесконечный цикл перечисления. При низкой температуре модель однажды выдала сотни вариантов "hyaluronan polymer...". Остановила это настройка no_repeat_ngram_size=3.
  • Падение при загрузке на Hugging Face. Файл на 4,4 ГБ вызывал ImportError из XetProgressReporter. Помогло задать HF_HUB_DISABLE_XET=1, чтобы обойти передачу через xet, и очистить закэшированные модули перед повторным импортом.
  • Исчезающий /tmp в Kaggle. Unsloth отправил экспорт GGUF в /tmp, потому что в рабочей папке не хватало места, а /tmp не сохраняется как вывод ядра. Решил проблему явный shutil.copy в /kaggle/working перед архивацией.
  • Утечка языка. Qwen иногда вставлял в ответы китайские иероглифы. Это убрали постобработка регулярным выражением и правило «только английский» в промпте.

Результаты

  • Вопрос о диабете ссылается на классификацию Прамеха из «Чараки» и на диагностические пороги WHO и ADA, причём каждое отдельно.
  • На вопрос, можно ли принимать Gymnema вместе с препаратами от диабета, модель указывает на суммарный риск гипогликемии и на протокол контроля.
  • Описание давящей боли в груди полностью минует LLM и возвращает только рекомендации по сортировке.
  • На выдуманную болезнь модель отвечает отказом, а не придуманным лечением.

Где найти

Модель можно скачать из реестра Ollama, веса GGUF лежат на Hugging Face под именем Expert-Vaidya-Qwen7B-GGUF. Это образовательный исследовательский проект. Это не медицинская консультация и не инструмент диагностики.