Tracks
NVIDIA Nemotron-3は、推論、コーディング、チャット、エージェント型AIワークフロー向けに構築されたNVIDIAの新しいオープンモデルファミリーです。Nano、Super、Ultraなどの異なるモデルサイズが用意されており、小型で効率的なモデルから大型で高性能なモデルまで、開発者が用途に応じて選択できます。
Nemotron-3の重要な更新点は効率性への注力です。高い性能を維持しつつ、推論とファインチューニングをより実用的に行えるよう設計されています。とくにNano版は、より手頃なGPU構成で動作するため、実験用途に適しています。
本ガイドでは、NVIDIA Nemotron-3-Nano-4Bを心理学の質疑応答データセットでファインチューニングします。データの準備、モデルの学習、アダプターの保存、Hugging Faceへのプッシュ、そしてファインチューニング前後の応答比較に、Low-Rank Adaptation (LoRA)、Transformers Reinforcement Learning (TRL)、Hugging Faceを使用します。
最新のオープンソースAIモデルの探索、AIエージェントの構築、LLMのファインチューニングを始めるには、Hugging Face Fundamentalsスキルトラックの受講をおすすめします。
1. 環境のセットアップ
Nemotron-3 Nanoはハイブリッドアーキテクチャを採用しているため、Mamba関連のパッケージを正しくインストールする必要があります。Jupyter Notebookで、まず既存のPyTorchスタックをアンインストールし、ここで固定しているmamba_ssmとcausal_conv1dのバージョンで動作する、CUDA 12.8ビルドのPyTorch 2.7.1を再インストールします。
あわせて、transformers、trl、accelerate、datasets、peft、huggingface_hubなど、ファインチューニングの中核ライブラリもインストールします。
%%capture
!pip install -U packaging ninja
# Replace the current PyTorch stack with the CUDA 12.8 build that works with these Mamba kernel pins.
!pip uninstall -y torch torchvision torchaudio triton
!pip install "torch==2.7.1" "torchvision==0.22.1" "torchaudio==2.7.1" --index-url https://download.pytorch.org/whl/cu128
!pip install -U "transformers==4.56.2" tokenizers "trl==0.22.2" accelerate datasets peft pandas tqdm huggingface_hub safetensors
!pip install -U --no-build-isolation "mamba_ssm==2.2.5" "causal_conv1d==1.5.2"
パッケージのインストール後、CUDAが利用可能であること、そしてPyTorchがGPUを検出できることを確認してください。本ノートブックは24GBのGPU向けに調整されているため、VRAMが少ない場合は警告が表示されます。
import os
import platform
import torch
print(f"Python: {platform.python_version()}")
print(f"PyTorch: {torch.__version__}")
print(f"PyTorch CUDA build: {torch.version.cuda}")
print(f"CUDA available: {torch.cuda.is_available()}")
if not torch.cuda.is_available():
raise RuntimeError(
"CUDA is not available. Select a RunPod PyTorch image with GPU support."
)
for idx in range(torch.cuda.device_count()):
props = torch.cuda.get_device_properties(idx)
total_gb = props.total_memory / 1024**3
print(
f"GPU {idx}: {props.name} ({total_gb:.1f} GB VRAM, capability {props.major}.{props.minor})"
)
if torch.cuda.get_device_properties(0).total_memory < 24 * 1024**3:
print(
"Warning: this 4B LoRA notebook is tuned for GPUs with at least 24GB VRAM. Reduce batch sizes on smaller GPUs."
)
torch.backends.cuda.matmul.allow_tf32 = True
torch.backends.cudnn.allow_tf32 = True
出力:
Python: 3.12.3
PyTorch: 2.7.1+cu128
PyTorch CUDA build: 12.8
CUDA available: True
GPU 0: NVIDIA GeForce RTX 3090 (23.6 GB VRAM, capability 8.6)
Warning: this 4B LoRA notebook is tuned for GPUs with at least 24GB VRAM. Reduce batch sizes on smaller GPUs.
Hugging FaceのトークンをHF_TOKENという環境変数として設定してください。これにより、ノートブックがNemotron-3モデルをダウンロードでき、後でファインチューニングしたLoRAアダプターをHugging Faceにプッシュできます。
from huggingface_hub import login
hf_token = os.environ.get("HF_TOKEN")
if not hf_token:
raise ValueError(
"Set HF_TOKEN in the RunPod environment before running this notebook."
)
login(token=hf_token)
print("Logged in to Hugging Face.")
2. データセットの読み込みと前処理
次に、Hugging Faceから心理学の質疑応答データセットを読み込みます。このデータセットにはquestion列と、response_j、response_kの2つの応答列が含まれています。本ガイドでは、教師ありファインチューニングのターゲット解答としてresponse_jを使用します。
まずデータセットを読み込み、再現性のためにシャッフルし、学習・検証・テストの分割を作成します。
from datasets import DatasetDict, load_dataset
DATASET_ID = "jkhedri/psychology-dataset"
TRAIN_LIMIT = 8000
VALIDATION_LIMIT = 800
TEST_LIMIT = 300
SEED = 42
raw_dataset = load_dataset(DATASET_ID)
raw_train = raw_dataset["train"].shuffle(seed=SEED)
split_1 = raw_train.train_test_split(test_size=0.15, seed=SEED)
split_2 = split_1["test"].train_test_split(test_size=0.33, seed=SEED)
def maybe_limit(split, limit):
if limit is None:
return split
return split.select(range(min(limit, len(split))))
dataset = DatasetDict(
{
"train": maybe_limit(split_1["train"], TRAIN_LIMIT),
"validation": maybe_limit(split_2["train"], VALIDATION_LIMIT),
"test": maybe_limit(split_2["test"], TEST_LIMIT),
}
)
dataset
出力:
DatasetDict({
train: Dataset({
features: ['question', 'response_j', 'response_k'],
num_rows: 8000
})
validation: Dataset({
features: ['question', 'response_j', 'response_k'],
num_rows: 800
})
test: Dataset({
features: ['question', 'response_j', 'response_k'],
num_rows: 300
})
})
学習用にフォーマットする前に、列名を確認し、サンプルを1件表示します。これにより、データセットが正しく読み込まれ、想定どおりの質問と応答フィールドを含んでいることを確認できます。
dataset["train"].column_names, dataset["train"][0]
出力:
(
['question', 'response_j', 'response_k'],
{
'question': "I'm experiencing anxiety about social situations and don't know how to cope.",
'response_j': "Social anxiety can be a difficult and isolating experience, but there are effective treatments available. Let's work on developing coping mechanisms, such as deep breathing and mindfulness, and exposure therapy to gradually confront your fears. We can also explore ways to improve social skills and build self-confidence.",
'response_k': "Just avoid social situations. It's not worth the anxiety and discomfort. You can also try using alcohol or drugs to help you feel more comfortable in social settings."
}
)
3. TRLファインチューニング用のデータセット整形
次に、データセットをTRLが想定するプロンプト・コンプリーション形式に変換します。各サンプルには、システムプロンプト、ユーザーの心理学に関する質問、そしてresponse_jからのターゲットのアシスタント応答を含めます。
システムプロンプトでは、支援的に応答すること、隠れた推論痕跡を避けること、実践的な提案を行うこと、そして公認のメンタルヘルス専門家の代わりを装わないことをモデルに指示します。
SYSTEM_PROMPT = """/no_think
You are a supportive psychology question-answering assistant.
Do not include hidden reasoning, thinking traces, <think> tags, or </think> tags in the final answer.
Respond with empathy, practical coping suggestions, and clear next steps.
Give a complete answer in 2-4 short paragraphs or a brief paragraph plus 3-5 practical bullets.
Do not diagnose the user or claim to replace a licensed mental health professional.
If the user may be in immediate danger or crisis, encourage contacting local emergency services or a trusted crisis hotline.
Keep the answer safe, specific, and directly relevant to the user's question without being overly brief."""
CHAT_TEMPLATE_KWARGS = {"enable_thinking": False}
USER_TEMPLATE = "Question:\n\n{question}"
def clean_text(value):
return " ".join(str(value).strip().split())
def to_prompt_completion(example):
question = clean_text(example["question"])
answer = clean_text(example["response_j"])
return {
"prompt": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": USER_TEMPLATE.format(question=question)},
],
"completion": [
{"role": "assistant", "content": answer},
],
"chat_template_kwargs": CHAT_TEMPLATE_KWARGS,
}
sft_dataset = dataset.map(
to_prompt_completion, remove_columns=dataset["train"].column_names
)
sft_dataset["train"][0]
出力:
{
'prompt': [
{
'role': 'system',
'content': "/no_think\nYou are a supportive psychology question-answering assistant.\nDo not include hidden reasoning, thinking traces, <think> tags, or </think> tags in the final answer.\nRespond with empathy, practical coping suggestions, and clear next steps.\nGive a complete answer in 2-4 short paragraphs or a brief paragraph plus 3-5 practical bullets.\nDo not diagnose the user or claim to replace a licensed mental health professional.\nIf the user may be in immediate danger or crisis, encourage contacting local emergency services or a trusted crisis hotline.\nKeep the answer safe, specific, and directly relevant to the user's question without being overly brief."
},
{
'role': 'user',
'content': "Question:\n\nI'm experiencing anxiety about social situations and don't know how to cope."
}
],
'completion': [
{
'role': 'assistant',
'content': "Social anxiety can be a difficult and isolating experience, but there are effective treatments available. Let's work on developing coping mechanisms, such as deep breathing and mindfulness, and exposure therapy to gradually confront your fears. We can also explore ways to improve social skills and build self-confidence."
}
],
'chat_template_kwargs': {'enable_thinking': False}
}
4. Nemotron-3ベースモデルの読み込み
続いて、Hugging FaceからNVIDIA Nemotron-3 Nano 4B BF16のトークナイザーとベースモデルを読み込みます。LoRAアダプターの出力ディレクトリも設定し、シーケンス長を1024トークンに制限して、24GB GPUでの学習を現実的なものにします。
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
MODEL_ID = "nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16"
OUTPUT_DIR = "./nemotron-3-nano-4b-bf16-psychology-qa-lora"
MAX_SEQ_LENGTH = 1024
tokenizer = AutoTokenizer.from_pretrained(
MODEL_ID,
token=hf_token,
trust_remote_code=True,
use_fast=True,
)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
base_model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
token=hf_token,
trust_remote_code=True,
dtype=torch.bfloat16,
device_map="auto",
attn_implementation="eager",
)
base_model.config.use_cache = False
base_model.config.pad_token_id = tokenizer.pad_token_id
base_model.config.eos_token_id = tokenizer.eos_token_id
base_model.generation_config.pad_token_id = tokenizer.pad_token_id
base_model.generation_config.eos_token_id = tokenizer.eos_token_id
base_model.generation_config.use_cache = False
base_model.generation_config.do_sample = False
base_model.generation_config.top_p = None
base_model.generation_config.min_new_tokens = None
base_model.generation_config.repetition_penalty = 1.08
base_model.generation_config.no_repeat_ngram_size = 4
5. 生成用ヘルパー関数の作成
ファインチューニングの前に、モデルの応答をテストするためのヘルパー関数を作成します。これらの関数は、チャットプロンプトの構築、回答の生成、不要なthinkingタグの削除、結果の小さな比較テーブルへの保存を行います。
import gc
import pandas as pd
from tqdm.auto import tqdm
def clear_cuda_cache():
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
def build_messages(question, system_prompt=SYSTEM_PROMPT):
return [
{"role": "system", "content": system_prompt},
{
"role": "user",
"content": USER_TEMPLATE.format(question=clean_text(question)),
},
]
def remove_thinking_text(text):
text = text.strip()
while "<think>" in text and "</think>" in text:
start = text.find("<think>")
end = text.find("</think>", start) + len("</think>")
text = (text[:start] + text[end:]).strip()
if "</think>" in text:
text = text.split("</think>")[-1].strip()
return text.replace("<think>", "").replace("</think>", "").strip()
def generate_answer(
model, tokenizer, question, system_prompt=SYSTEM_PROMPT, max_new_tokens=180
):
messages = build_messages(question, system_prompt)
device = next(model.parameters()).device
inputs = tokenizer.apply_chat_template(
messages,
tokenize=True,
**CHAT_TEMPLATE_KWARGS,
add_generation_prompt=True,
return_dict=True,
return_tensors="pt",
)
inputs = {key: value.to(device) for key, value in inputs.items()}
input_len = inputs["input_ids"].shape[-1]
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=max_new_tokens,
do_sample=False,
use_cache=False,
repetition_penalty=1.08,
no_repeat_ngram_size=4,
pad_token_id=tokenizer.pad_token_id,
eos_token_id=tokenizer.eos_token_id,
)
decoded = tokenizer.decode(outputs[0][input_len:], skip_special_tokens=True).strip()
return remove_thinking_text(decoded)
def generate_sample_table(model, tokenizer, examples, output_column):
rows = []
model.eval()
for ex in tqdm(examples, desc=f"Generating {output_column}", leave=False):
rows.append(
{
"question": clean_text(ex["question"]),
"reference_response_j": clean_text(ex["response_j"]),
output_column: generate_answer(model, tokenizer, ex["question"]),
}
)
return pd.DataFrame(rows)
6. ファインチューニング前のサンプル評価
学習前に、ベースのNemotron-3モデルからいくつかの応答を生成します。これにより、LoRAファインチューニング前後の応答を比較するためのベースラインが得られます。
ここでは、テストセットから3つのサンプルを選び、先ほど作成したヘルパー関数で回答を生成します。
sample_examples = [dataset["test"][idx] for idx in range(min(3, len(dataset["test"])))]
pre_samples = generate_sample_table(
base_model,
tokenizer,
sample_examples,
"base_model_answer"
)
pre_samples
出力は、元の質問、response_jの参照解答、そしてベースモデルが生成した解答を含む小さなテーブルです。後でファインチューニング後のモデルの応答と比較する際に役立ちます。

7. LoRAと学習設定の構成
ここからモデルをLoRAでファインチューニングできるよう準備します。メモリ使用量を抑えるためにグラデーションチェックポイントを有効化し、モデル内のすべての線形レイヤーを対象にするLoRA設定を作成します。
from peft import LoraConfig
base_model.gradient_checkpointing_enable()
base_model.config.use_cache = False
lora_config = LoraConfig(
r=32,
lora_alpha=64,
lora_dropout=0.1,
bias="none",
task_type="CAUSAL_LM",
target_modules="all-linear",
)
次に、SFTConfigを使用して教師ありファインチューニングの設定を定義します。これには、バッチサイズ、学習率、エポック数、評価頻度、保存戦略、BF16での学習などが含まれます。
from trl import SFTConfig, SFTTrainer
training_args = SFTConfig(
output_dir=OUTPUT_DIR,
per_device_train_batch_size=8,
per_device_eval_batch_size=8,
gradient_accumulation_steps=8,
learning_rate=5e-5,
weight_decay=0.01,
lr_scheduler_type="linear",
warmup_ratio=0.05,
num_train_epochs=2,
logging_steps=50,
eval_strategy="steps",
eval_steps=50,
save_strategy="steps",
save_steps=100,
save_total_limit=2,
load_best_model_at_end=True,
metric_for_best_model="eval_loss",
greater_is_better=False,
gradient_checkpointing=True,
bf16=True,
fp16=False,
tf32=True,
max_length=MAX_SEQ_LENGTH,
packing=False,
completion_only_loss=True,
remove_unused_columns=False,
dataloader_num_workers=4,
optim="adamw_torch_fused",
report_to="none",
seed=SEED,
)
8. LoRAアダプターの学習と保存
ここでSFTTrainerを作成し、LoRA設定を適用してファインチューニングを開始します。学習前に、学習可能パラメータ数を確認し、LoRAアダプターが正しく適用されているか検証します。
trainer = SFTTrainer(
model=base_model,
args=training_args,
train_dataset=sft_dataset["train"],
eval_dataset=sft_dataset["validation"],
peft_config=lora_config,
processing_class=tokenizer,
)
trainable_params = sum(
param.numel() for param in trainer.model.parameters() if param.requires_grad
)
all_params = sum(param.numel() for param in trainer.model.parameters())
if trainable_params == 0:
raise RuntimeError(
"No trainable LoRA parameters were attached. Check target_modules before training."
)
print(f"Trainable LoRA parameters: {trainable_params:,}")
print(f"All parameters visible to trainer: {all_params:,}")
print(f"Trainable percentage: {100 * trainable_params / all_params:.4f}%")
train_result = trainer.train()
trainer.model.eval()
trainer.model.config.use_cache = False
trainer.model.generation_config.use_cache = False
train_result
学習中は、トレーニング損失と検証損失が徐々に低下していくはずです。これは、モデルがデータセットの応答スタイルを学習していることを示します。

学習後、LoRAアダプターとトークナイザーをローカルに保存します:
trainer.model.save_pretrained(OUTPUT_DIR)
tokenizer.save_pretrained(OUTPUT_DIR)
続いて、ファインチューニング済みアダプターをHugging Faceにプッシュします:
HUB_REPO_ID = "kingabzpro/nemotron-3-nano-4b-bf16-psychology-qa-lora"
trainer.model.push_to_hub(HUB_REPO_ID, private=False)
tokenizer.push_to_hub(HUB_REPO_ID, private=False)
これで、ファインチューニング済みアダプターはローカルに保存され、HUB_REPO_IDでHugging Faceにアップロードされました。

出典: kingabzpro/nemotron-3-nano-4b-bf16-psychology-qa-lora
9. ファインチューニング前後の応答比較
最後に、ファインチューニング済みモデルから回答を生成し、ベースモデルの出力と比較します。これにより、LoRAファインチューニングが参照応答との整合性を改善したかどうかを確認できます。
post_samples = generate_sample_table(
trainer.model,
tokenizer,
sample_examples,
"fine_tuned_answer"
)
comparison = pre_samples[
["question", "reference_response_j", "base_model_answer"]
].merge(
post_samples[["question", "fine_tuned_answer"]],
on="question",
how="left",
)
for idx, row in comparison.iterrows():
print("=" * 100)
print(f"Sample {idx + 1}")
print("=" * 100)
print("\nQUESTION:\n")
print(row["question"])
print("\nREFERENCE RESPONSE_J:\n")
print(row["reference_response_j"])
print("\nBASE MODEL ANSWER:\n")
print(row["base_model_answer"])
print("\nFINE-TUNED ANSWER:\n")
print(row["fine_tuned_answer"])
print("\n")

ファインチューニング後のモデルは、参照応答のスタイルにより整合しました。簡潔になり、データセットの解答に近い内容を保ちました。ただし、ベースモデルのほうが、より詳細で実践的な応答を返すことがある点も見られました。
たとえば、ストレス管理や集中力に関する質問では整合性が向上しましたが、睡眠に関する例では、より役立つ詳細が含まれていたため、ベースモデルのほうが優れていました。
全体として、参照データセットのスタイルに合わせることが目的であれば、ファインチューニング後モデルが適しています。最大限の有用性を重視する場合は、一部のケースでは、より温かみがあり詳細な回答を行うベースモデルが依然として良い結果を出す可能性があります。
上記コードの実行で問題が生じた場合は、Hugging Faceリポジトリのノートブックを参照してください: fine-tune-nemotron-nano.ipynb
まとめ
100以上のLLMをファインチューニングしてきましたが、このモデルは期待以上にセットアップ作業が必要でした。主な課題はmamba_ssmの依存関係で、ローカルのPython環境と衝突したり簡単に壊れたりする点です。
そのため、このワークフローにはクリーンな環境を使うことをおすすめします。私の場合は、環境を再構築し、適切なPyTorchバージョンをインストールし、Mamba関連パッケージを固定してからノートブックを実行するのが最も簡単でした。
もう一つの制約は量子化です。このセットアップでは、標準的なQLoRAワークフローのように4ビットで単純にモデルを読み込んでファインチューニングすることはできませんでした。Qwen3.5 Smallのチュートリアルのようなやり方ではなく、BF16のフルモデルを読み込み、LoRAでファインチューニングする必要がありました。4Bモデルであれば24GBのGPUでも対応可能ですが、12B以上になるとメモリ使用量が急速に問題となります。
とはいえ、コンシューマー向けGPUでのファインチューニングは格段に身近になりました。RTX 3090のような24GBカードがあれば、強力なオープンモデルを特定のスタイルやドメインに合わせて調整することが、大規模な学習クラスターなしでも可能です。
総じて、Nemotron-3 Nanoは有能なモデルですが、環境構築には注意が必要です。依存関係が整えば、少数のサンプルでもうまくファインチューニングでき、新しい応答スタイルに適応させられます。