Перейти к основному контенту

Как дообучить DiffusionGemma на PubMedQA с помощью Unsloth

Узнайте, как дообучить DiffusionGemma 26B-A4B для биомедицинского ответов на вопросы на PubMedQA с помощью Unsloth, LoRA, Hugging Face и GPU RunPod H100.
Обновлено 3 авг. 2026 г.  · 9 мин читать

Изучить с помощью AI

Открыть в ChatGPTОткрыть в ClaudeОткрыть в Perplexity

DiffusionGemmaэкспериментальная языковая модель Google DeepMind, которая генерирует текст иначе, чем традиционные большие языковые модели. Вместо предсказания по одному токену слева направо она начинает с фиксированного холста из зашумлённых токенов и постепенно уточняет их через несколько шагов денойзинга. Это позволяет модели параллельно обновлять сразу несколько позиций токенов и пересматривать части ответа в процессе генерации.

В этом руководстве мы будем дообучать diffusiongemma-26B-A4B-it на наборе данных PubMedQA с использованием NVIDIA H100 GPU. Модель будет получать биомедицинский вопрос и поддерживающий контекст, а затем предсказывать yes, no или maybe. Мы подготовим данные, обучим адаптер LoRA, оценим модель до и после дообучения и загрузим финальный адаптер на Hugging Face.

Я также опубликовал полноценный ноутбук, чтобы вы могли просмотреть исходный код, повторить шаги и запустить эксперимент самостоятельно.

Примечание: Этот проект предназначен исключительно для обучения и экспериментов и не должен использоваться для реальных медицинских решений.

1. Откройте Jupyter Notebook на RunPod

Создайте новый pod на RunPod с GPU NVIDIA H100 и выберите шаблон PyTorch/Jupyter. Настройте как минимум 100 ГБ постоянного хранилища, чтобы файлы модели и результаты обучения не потерялись при остановке pod.

Добавьте ваш токен доступа Hugging Face в качестве переменной окружения:

HF_TOKEN=your_hugging_face_token

Editing the Runpod Pytorch template

Это позволит быстрее загружать модели и датасеты и даст возможность загрузить сохранённый адаптер LoRA на Hugging Face без ручного входа из ноутбука.

Сконфигурированный pod должен стоить приблизительно $3 в час, однако финальная цена может варьироваться в зависимости от доступности GPU и выбранного типа pod.

Runpod H100 GPU summary

Когда pod запустится, откройте JupyterLab или Jupyter Notebook из интерфейса RunPod и создайте новый ноутбук с именем diffusiongemma_pubmedqa.ipynb.

2. Установите необходимые пакеты

Выполните следующие команды в первой ячейке ноутбука, чтобы установить Unsloth и библиотеки, необходимые для загрузки, дообучения и сохранения DiffusionGemma.

%%capture
%pip install --upgrade pip wheel setuptools packaging ninja
%pip install unsloth
%pip install --no-deps --upgrade --force-reinstall git+https://github.com/unslothai/unsloth-zoo.git git+https://github.com/unslothai/unsloth.git
%pip install sentencepiece protobuf "datasets==4.3.0" "huggingface_hub>=0.34.0" hf_transfer
%pip install --no-deps bitsandbytes accelerate peft trl triton
%pip install --no-deps --upgrade "torchao>=0.16.0"
%pip install --no-deps transformers==5.11.0 "tokenizers>=0.22.0,<=0.23.0"

Команда %%capture скрывает длинный вывод установки. Версии пакетов зафиксированы, чтобы избежать проблем совместимости между DiffusionGemma, Transformers, Unsloth и библиотеками обучения.

После завершения установки перезапустите ядро ноутбука, прежде чем продолжить.

3. Импортируйте библиотеки

Импортируйте библиотеки, необходимые для подготовки датасета, загрузки модели, обучения и оценки.

import copy
import os
import random
import time

import torch
from datasets import load_dataset
from unsloth import FastModel

os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "1"
torch._dynamo.config.recompile_limit = 64

print("Torch:", torch.__version__)
print("CUDA available:", torch.cuda.is_available())
print(
    "GPU:",
    torch.cuda.get_device_name(0)
    if torch.cuda.is_available()
    else "None",
)

HF_HUB_ENABLE_HF_TRANSFER включает более быстрые загрузки из Hugging Face Hub, а увеличение лимита перекомпиляций в Dynamo помогает избежать прерываний при работе с моделью.

При успешном выполнении ячейки Unsloth пропатчит среду обучения, и в выводе будет подтверждение, что CUDA доступна и обнаружен GPU H100.

🦥 Unsloth: Will patch your computer to enable 2x faster free fine-tuning.
🦥 Unsloth Zoo will now patch everything to make training faster!
Torch: 2.10.0+cu128
CUDA available: True
GPU: NVIDIA H100 80GB HBM3

4. Задайте конфигурацию

Определите модель, датасет, параметры обучения, настройки оценки и выходной каталог в одном месте.

MODEL_NAME = "unsloth/diffusiongemma-26B-A4B-it"
DATASET_NAME = "qiaojin/PubMedQA"

TRAIN_SUBSET = "pqa_artificial"
EVAL_SUBSET = "pqa_labeled"

N_TRAIN = 3000
N_EVAL = 200

MAX_CONTEXT_CHARS = 2500

STEPS = 60
GRAD_ACCUM = 4
LR = 1e-4
T_LO = 0.1

EVAL_TOTAL = 50
EVAL_DENOISING_STEPS = 16

OUTPUT_DIR = "diffusiongemma_pubmedqa_lora"

Мы будем использовать 3000 искусственных примеров для обучения и 200 вручную размеченных примеров для оценки. Чтобы эксперимент прошёл быстро, модель будет обучаться 60 шагов и оцениваться на 50 примерах с 16 шагами денойзинга.

Как работает DiffusionGemma

Прежде чем загружать модель, стоит представить, как DiffusionGemma фактически формирует ответ. Вместо записи токенов по одному она начинает с холста фиксированной длины и уточняет его за несколько шагов денойзинга, обновляя множество позиций сразу, пока текст не сложится в связный ответ. 

Код ниже сообщает длину холста 256 — размер одного блока. Для наших коротких ответов yes/no/maybe одного холста более чем достаточно, а более длинные ответы генерируются последовательным объединением холстов блок за блоком. Диаграмма ниже на высоком уровне показывает этот процесс уточнения:

Text Diffusion Explanation

5. Загрузите DiffusionGemma

Загрузите модель DiffusionGemma, дообученную на инструкциях, с точностью bfloat16. Мы не будем использовать 4-битную квантизацию, поскольку у H100 достаточно памяти для загрузки модели с более высокой точностью.

model, tokenizer = FastModel.from_pretrained(
    model_name=MODEL_NAME,
    dtype=torch.bfloat16,
    load_in_4bit=False,
)

processor = tokenizer
tok = processor.tokenizer if hasattr(processor, "tokenizer") else processor

vocab = model.config.text_config.vocab_size
canvas_len = model.config.canvas_length

dev = next(
    (p.device for p in model.parameters() if p.device.type != "meta"),
    torch.device("cuda"),
)

print("Vocab size:", vocab)
print("Canvas length:", canvas_len)
print("Model device:", dev)

Размер словаря используется при добавлении случайного шума во время обучения диффузии. Длина холста определяет максимальное число токенов, которые модель может уточнять в одном блоке генерации.

Вы должны увидеть вывод, похожий на:

Vocab size: 262144
Canvas length: 256
Model device: cuda:0

6. Добавьте адаптер LoRA

Добавьте адаптер LoRA, чтобы обучать лишь небольшой набор дополнительных параметров вместо обновления всей модели на 26 миллиардов параметров.

model = FastModel.get_peft_model(
    model,
    r=64,
    lora_alpha=128,
    use_gradient_checkpointing=False,
)

Это значительно снижает потребление памяти и вычислительные затраты на дообучение. Контрольные точки градиента отключены, поскольку у H100 достаточно памяти для этого эксперимента.

7. Загрузите PubMedQA

Загрузите искусственное подмножество PubMedQA для обучения и вручную размеченное подмножество для оценки.

train_data = load_dataset(
    DATASET_NAME,
    TRAIN_SUBSET,
    split="train",
)

eval_data = load_dataset(
    DATASET_NAME,
    EVAL_SUBSET,
    split="train",
)

print("Train size:", len(train_data))
print("Eval size:", len(eval_data))
print(train_data[0])

Обучающее подмножество содержит автоматически сгенерированные примеры, а в оценочном подмножестве — экспертом размеченные биомедицинские вопросы. 

Печать первой строки позволяет изучить вопрос, контекст аннотации и финальное решение перед форматированием данных.

Вы должны увидеть:

Train size: 211269
Eval size: 1000

Каждый пример содержит биомедицинский вопрос, один или несколько поддерживающих фрагментов аннотаций и финальный ответ yes, no или maybe.

Loading the PubMedQA

8. Преобразуйте датасет

Преобразуйте каждый пример PubMedQA в формат чата, содержащий подсказку пользователя и ответ ассистента.

def make_prompt(row):
    context = " ".join(row["context"]["contexts"])
    context = context[:MAX_CONTEXT_CHARS]
    question = row["question"]

    return f"""Answer the biomedical research question using only the context.

Context:
{context}

Question:
{question}

Answer with only one word: yes, no, or maybe."""


def make_answer(row):
    return row["final_decision"].strip().lower()


def convert_row(row):
    answer = make_answer(row)

    if answer not in ["yes", "no", "maybe"]:
        return None

    return {
        "messages": [
            {"role": "user", "content": make_prompt(row)},
            {"role": "assistant", "content": answer},
        ]
    }


train_rows = []

for row in train_data.select(range(N_TRAIN)):
    item = convert_row(row)

    if item is not None:
        train_rows.append(item)


eval_rows = []

for row in eval_data.select(range(N_EVAL)):
    item = convert_row(row)

    if item is not None:
        eval_rows.append(item)


print("Prepared train examples:", len(train_rows))
print("Prepared eval examples:", len(eval_rows))
print(train_rows[0]["messages"][0]["content"])
print("Answer:", train_rows[0]["messages"][1]["content"])

Контекстные фрагменты объединяются в одну строку и ограничиваются 2500 символами, чтобы вход оставался управляемым. Каждый ответ приводится к нижнему регистру, а примеры с метками вне yes, no или maybe удаляются.

Печать первого преобразованного примера помогает убедиться, что контекст, вопрос и ответ корректно отформатированы перед обучением.

Formatting the PubMedQA dataset in chat format

9. Постройте примеры для обучения диффузии

DiffusionGemma требует размещать целевой ответ внутри холста фиксированной длины. Эта функция токенизирует подсказку, конвертирует ответ в ID токенов, дополняет до длины холста модели и создаёт маску, показывающую, какие токены должны участвовать в расчёте функции потерь.

eos = model.generation_config.eos_token_id or [1]
eos = eos[0] if isinstance(eos, (list, tuple)) else eos

pad = tok.pad_token_id if tok.pad_token_id is not None else eos


def build_examples(rows):
    examples = []

    for row in rows:
        user_message = row["messages"][0]
        assistant_message = row["messages"][1]

        prompt_ids = processor.apply_chat_template(
            [user_message],
            tokenize=True,
            add_generation_prompt=True,
            return_tensors="pt",
        )[0]

        answer_ids = tok.encode(
            assistant_message["content"],
            add_special_tokens=False,
        )

        content = answer_ids + [eos]
        n = len(content)

        if n > canvas_len:
            continue

        x0 = torch.tensor(
            content + [pad] * (canvas_len - n),
            dtype=torch.long,
        )

        loss_mask = torch.zeros(canvas_len, dtype=torch.bool)
        loss_mask[:n] = True

        examples.append((prompt_ids, x0, loss_mask))

    return examples


examples = build_examples(train_rows)

Токен конца последовательности добавляется после каждого ответа, а оставшиеся позиции холста заполняются паддинг-токенами. Маска потерь гарантирует, что обучение фокусируется только на токенах ответа и конце последовательности, а не на заполнителях.

10. Создайте функции инференса и оценки

Далее определите функции для генерации ответов, очистки вывода модели и расчёта точности оценки.

Сгенерируйте ответ

Функция answer_question() форматирует подсказку, генерирует ответ через несколько шагов денойзинга и декодирует сгенерированные токены в текст.

def answer_question(prompt, steps=64):
    input_ids = processor.apply_chat_template(
        [{"role": "user", "content": prompt}],
        tokenize=True,
        add_generation_prompt=True,
        return_tensors="pt",
    ).to(dev)

    gen_config = copy.deepcopy(model.generation_config)
    gen_config.max_denoising_steps = steps
    gen_config.max_new_tokens = canvas_len

    model.eval()

    with torch.no_grad():
        output = model.generate(
            input_ids=input_ids,
            generation_config=gen_config,
        )

    generated = output.sequences[0, input_ids.shape[1]:]
    text = tok.decode(
        generated.tolist(),
        skip_special_tokens=True,
    )

    return text.strip().lower()

Извлеките предсказание

Хотя подсказка просит однословный ответ, иногда модель может сгенерировать дополнительный текст. Эта функция извлекает первое допустимое предсказание yes, no или maybe.

def clean_prediction(text):
    text = text.lower().strip()

    if text.startswith("yes"):
        return "yes"

    if text.startswith("no"):
        return "no"

    if text.startswith("maybe"):
        return "maybe"

    words = text.replace(".", " ").replace(",", " ").split()

    for word in words:
        if word in ["yes", "no", "maybe"]:
            return word

    return "unknown"

Оцените точность

Функция оценки сравнивает каждое очищенное предсказание с правильным ответом, печатает результат для каждого примера и возвращает общую точность вместе с индивидуальными предсказаниями.

def evaluate_model(
    rows,
    total=50,
    steps=64,
    title="Evaluation",
):
    correct = 0
    results = []
    total = min(total, len(rows))

    print(title)
    print("-" * len(title))

    for i, row in enumerate(rows[:total], start=1):
        prompt = row["messages"][0]["content"]
        gold = row["messages"][1]["content"]

        raw_pred = answer_question(prompt, steps=steps)
        pred = clean_prediction(raw_pred)

        is_correct = pred == gold
        correct += int(is_correct)

        results.append({
            "index": i,
            "gold": gold,
            "prediction": pred,
            "raw_prediction": raw_pred,
            "correct": is_correct,
        })

        print(
            f"{i:02d}. Gold: {gold} | "
            f"Pred: {pred} | Correct: {is_correct}"
        )

    accuracy = correct / total if total else 0

    print()
    print("Accuracy:", accuracy)
    print()

    return {
        "accuracy": accuracy,
        "correct": correct,
        "total": total,
        "results": results,
    }

11. Оцените модель до дообучения

Запустите оценку до обучения, чтобы зафиксировать базовый уровень.

before_eval = evaluate_model(
    eval_rows,
    total=EVAL_TOTAL,
    steps=EVAL_DENOISING_STEPS,
    title="Before Fine-Tuning Evaluation",
)

Это оценивает 50 примеров с 16 шагами денойзинга на ответ. В этом эксперименте базовая модель правильно ответила на 30 из 50 вопросов.

Baseline result of the DiffusionGemma

Позже этот базовый уровень будет сопоставлен с точностью модели после дообучения.

12. Настройте обучение

Переключите модель в режим обучения, создайте оптимизатор и шедулер скорости обучения и определите, как чистые токены ответа будут искажаться во время обучения диффузии.

model.config.use_cache = True
model.train()

opt = torch.optim.AdamW(
    [p for p in model.parameters() if p.requires_grad],
    lr=LR,
    betas=(0.9, 0.95),
    weight_decay=0.0,
)

sched = torch.optim.lr_scheduler.OneCycleLR(
    opt,
    max_lr=LR,
    total_steps=STEPS,
    pct_start=0.03,
    anneal_strategy="cos",
)

В оптимизатор передаются только параметры с requires_grad=True, что означает, что в процессе обучения обновляется адаптер LoRA, а не вся модель.

Далее создайте функцию искажения, которая заменяет случайную долю холста ответа случайными токенами.

def corrupt(x0):
    noise_level = random.uniform(T_LO, 1.0)

    xt = x0.to(dev).clone()

    noise_mask = (
        torch.rand(canvas_len, device=dev) < noise_level
    )

    xt[noise_mask] = torch.randint(
        0,
        vocab,
        (canvas_len,),
        device=dev,
    )[noise_mask]

    return xt.unsqueeze(0)

Количество шума меняется для каждого примера. Во время обучения модель учится восстанавливать исходный ответ из этих искажённых токенов холста.

13. Обучите модель

Следующий цикл обучает адаптер LoRA в течение 60 шагов с накоплением градиента.

order = list(range(len(examples)))
ptr = 0
start_time = time.time()

opt.zero_grad(set_to_none=True)

for step in range(1, STEPS + 1):
    step_loss = 0.0

    for _ in range(GRAD_ACCUM):
        if ptr >= len(order):
            random.shuffle(order)
            ptr = 0

        prompt_ids, x0, loss_mask = examples[order[ptr]]
        ptr += 1

        output = model(
            input_ids=prompt_ids.unsqueeze(0).to(dev),
            canvas_ids=corrupt(x0),
            self_conditioning_logits=None,
        )

        logits = output.logits[0].float()
        mask = loss_mask.to(dev)

        loss = torch.nn.functional.cross_entropy(
            logits[mask],
            x0.to(dev)[mask],
        )

        (loss / GRAD_ACCUM).backward()
        step_loss += loss.item() / GRAD_ACCUM

    torch.nn.utils.clip_grad_norm_(
        [
            p
            for p in model.parameters()
            if p.requires_grad
        ],
        1.0,
    )

    opt.step()
    sched.step()
    opt.zero_grad(set_to_none=True)

    if step % 20 == 0:
        elapsed = time.time() - start_time

        print(
            f"step {step}/{STEPS} | "
            f"loss {step_loss:.4f} | "
            f"{elapsed:.0f}s"
        )

Для каждого обучающего примера модель получает биомедицинскую подсказку и искажённый холст ответа. Функция потерь кросс-энтропии вычисляется только для настоящих токенов ответа, выбранных маской потерь.

Накопление градиента объединяет четыре примера перед обновлением модели. Также применяется обрезка градиента для стабильности обучения.

В этом эксперименте обучение заняло примерно две минуты:

step 20/60 | loss 0.0019 | 43s
step 40/60 | loss 0.0003 | 85s
step 60/60 | loss 0.0001 | 126s

Поступательное снижение лосса указывает, что адаптер учится восстанавливать ожидаемые ответы из искажённого холста. Во время обучения вы также можете запустить nvidia-smi в терминале RunPod, чтобы мониторить использование и загрузку памяти GPU.

Nvidia model statistic while model training

14. Оцените дообученную модель

Запустите ту же оценку после обучения, чтобы измерить, улучшилась ли производительность модели.

after_eval = evaluate_model(
    eval_rows,
    total=EVAL_TOTAL,
    steps=EVAL_DENOISING_STEPS,
    title="After Fine-Tuning Evaluation",
)

Дообученная модель оценивается на тех же 50 примерах и с теми же 16 шагами денойзинга, что и в базовой оценке.

Fine-tune result of the DiffusionGemma

Далее сравните точность до и после дообучения.

before_accuracy = before_eval["accuracy"]
after_accuracy = after_eval["accuracy"]
improvement = after_accuracy - before_accuracy

print("Before fine-tuning accuracy:", before_accuracy)
print("After fine-tuning accuracy:", after_accuracy)
print("Improvement:", improvement)
Before fine-tuning accuracy: 0.6
After fine-tuning accuracy: 0.8
Improvement: 0.2

В этом эксперименте точность модели выросла с 0.60 до 0.80.

Это соответствует улучшению на 20 процентных пунктов: после дообучения модель правильно ответила на 40 из 50 вопросов, по сравнению с 30 из 50 до обучения.

15. Сохраните и загрузите дообученный адаптер

Сохраните обученный адаптер LoRA и файлы процессора в ранее определённый выходной каталог.

model.save_pretrained(OUTPUT_DIR)
processor.save_pretrained(OUTPUT_DIR)

print(f"Saved LoRA adapter to: {OUTPUT_DIR}")

Вы должны увидеть:

Saved LoRA adapter to: diffusiongemma_pubmedqa_lora

Сохраняется только лёгкий адаптер LoRA, а не ещё одна полная копия базовой модели на 26 миллиардов параметров.

Затем загрузите файлы адаптера и процессора в Hugging Face Hub:

REPO_ID = "kingabzpro/diffusiongemma_pubmedqa"

model.push_to_hub(REPO_ID)
processor.push_to_hub(REPO_ID)

Поскольку переменная окружения HF_TOKEN была добавлена при конфигурации pod на RunPod, аутентификация в Hugging Face должна пройти автоматически. Вам нужно вызывать notebook_login() только если токен ещё не был настроен:

from huggingface_hub import notebook_login

notebook_login()

После завершения загрузки репозиторий будет содержать адаптер LoRA и конфигурацию процессора, необходимые для последующей загрузки дообученной модели.

Finetuned lora on Hugging face: diffusiongemma_pubmedqa

Источник: kingabzpro/diffusiongemma_pubmedqa · Hugging Face 

Заключение

Дообучать DiffusionGemma с Unsloth оказалось на удивление просто. Больше всего времени заняла установка корректных зависимостей и понимание специфики диффузионного процесса обучения. Как только среда была настроена, загрузка модели, обучение адаптера LoRA, его оценка и загрузка результатов на Hugging Face прошли очень гладко.

DiffusionGemma показалась особенно интересной, поскольку она не генерирует текст по одному токену, как традиционная языковая модель. Вместо этого она работает с фиксированным холстом и постепенно уточняет зашумлённые токены через шаги денойзинга. Понимание этого иного процесса генерации и его дообучение на задаче биомедицинского вопросно-ответного взаимодействия сделали эксперимент особенно ценным для меня.

Даже в небольших настройках точность на выборке из 50 примеров выросла с 0.60 до 0.80 — хотя при таком размере выборки погрешность велика, и базовая стратегия «всегда yes» уже даёт около 55% на этом наборе для оценки. 

Также стоит отметить, что искусственное обучающее подмножество почти не содержит меток «maybe», поэтому у модели мало шансов выучить этот класс, хотя он встречается в оценочных данных. Рассматривайте это как быстрый эксперимент по работе процесса дообучения, а не как подтверждение готовности модели к реальному медицинскому применению.

Готовы пойти дальше одного запуска дообучения? Наш трек Developing Large Language Models проведёт вас от основ PyTorch и transformers к созданию и развёртыванию собственных LLM.

Темы

Лучшие курсы по LLM

Track

Разработка больших языковых моделей

16 ч
Изучите разработку больших языковых моделей (LLM) с помощью PyTorch и Hugging Face, используя новейшие методы глубокого обучения и NLP.
ПодробнееRight Arrow
Начать Курс
Смотрите большеRight Arrow