一日でできる! オリジナルのローカルLLMの作り方【データ合成からLM Studioまで】
はじめに
この記事では効率的な合成データ生成からそのデータを学習したモデルのGGUF変換、OllamaやLM Studioでの推論まで行います。
データ合成にはSDG LOOM、学習にはUnsloth Studio、推論にはLM Studioを用います。
これを理解すれば誰でもオリジナルのLLMを作成することができます。
今回は「小説生成ローカルモデル」を例に挙げて作成を行います。
それでは初めて行きましょう。
合成データの作り方
このステップでは、LLMを用いた合成データを作ります。
オリジナルのLLMを作成するにあたって、1番大事なのは合成データです。
どのようなデータを作り、そしてそれを用いてどのような大規模言語モデルを目指すのかを決定するのがこのフェーズになります。
合成データを作るにあたって、以下の3つの点が大切だと個人的に思っています。
・データの品質
・コストパフォーマンス
そして、何より大事なのがフローのクオリティです。
良いフローを作成することでより良いデータを作ることができます。そして良いフローを作成する際に一番大切なのが、データ合成フレームワークの選定です。
今回はデータ合成フレームワークにSDG LOOMというものを使います。
このフレームワークの特徴は
・高い自由度
・再利用性の高さ
・フローの作成のしやすさ
が挙げられます。
それではデータの作成をしていきましょう。
インストール
SDG LOOMのダウンロードとインストールを行います。
まずはクローンをした上で、ライブラリのインストールを行います。
git clone https://github.com/foxn2000/sdg_loom.gitライブラリーは以下の形で落とします。(condaやuvを使いたい場合にはレポジトリの方を確認してください。)
python3 -m venv env
source env/bin/activate
pip install -e .これでインストールは完了です。
次はフローの生成に行きます。
フローの生成
SDG LOOMはYAMLを用いたDSL(ドメイン固有言語)であるMABEL(Model And Blocks Expansion Language)を用いてデータを作成します。
MABELは主にAI設定とフローブロックで構成されています。
以下が例になります。
mabel:
version: "2.0"
models:
- name: qwen3
api_model: qwen3
api_key: "sk-local"
base_url: http://localhost:8000/v1
request_defaults:
temperature: 0.7
max_tokens: 13000
top_p: 0.9
blocks:
- type: ai
exec: 1
model: qwen3
prompts:
- "Summarize: {UserInput}"
outputs:
- name: Summary
select: full
- type: end
exec: 2
final:
- name: answer
value: "{Summary}"YAMLの詳細な仕様は以下のドキュメントに書いてあります。
(自分は基本的には以下のプロンプトのようなものを用いてLLMにフローを作らせています。)
## プロンプト
@/examples/ にあるMABEL(YAML)を参考にして、小説生成用のフローを作成してください。
また、それに使うseedデータを作成するためのPythonコードも作成してください。(データは2500件作成したいです。)
具体的には二段階を想定しています。
・seedデータを元にして小説生成用のプロンプトをLLMで作成する
・LLMで作成したプロンプトを元に小説をLLMで作成する。
基本的に使うモデルはOpenRouterを用いてください。(モデルは"openai/gpt-oss-120b"でお願いします。)また、今回使用するプロバイダーはOpenRouterです。(freeで使えるモデルがあるため使用します。)
モデルのライセンスも確認して、データセット作成に使えるかは事前に確認してください。
これで作成したのが以下になります。
#!/usr/bin/env python3
"""小説生成用seedデータを2500件作成するスクリプト。
Usage:
python examples/scripts/generate_novel_seeds.py
# -> examples/data/novel_seeds.jsonl に出力
"""
import json
import random
import pathlib
# ジャンル
GENRES =[
"SF", "ファンタジー", "ミステリー", "ホラー", "恋愛", "歴史小説",
"冒険", "青春", "サスペンス", "コメディ", "ダークファンタジー",
"ディストピア", "スチームパンク", "サイバーパンク", "ポストアポカリプス",
"日常系", "群像劇", "叙事詩", "心理ドラマ", "社会派",
]
# テーマ
THEMES =[
"友情と裏切り", "禁断の愛", "復讐", "自己犠牲", "成長と旅立ち",
"孤独と再生", "記憶の喪失", "時間の流れ", "人間と機械の境界",
"運命への抗い", "償いと許し", "権力と腐敗", "信仰と疑念",
"家族の絆", "自由への渇望", "正義の意味", "創造と破壊",
"生と死の境界", "異文化の衝突", "失われた故郷",
"約束を果たす旅", "選ばれなかった道", "逃れられない過去",
"世界の終わりと希望", "嘘と真実", "夢と現実の狭間",
"革命と犠牲", "支配と従属", "名もなき英雄", "禁じられた知識",
]
# 舞台設定
SETTINGS =[
"近未来の東京", "中世ヨーロッパの城塞都市", "宇宙コロニー",
"江戸時代の京都", "荒廃した地球", "海底都市", "浮লগ্ন大陸",
"現代の地方都市", "戦時下のベルリン", "魔法学園",
"砂漠の交易都市", "北欧の漁村", "異世界の王国",
"巨大企業が支配する都市", "孤島の研究所", "地下迷宮",
"明治時代の横浜", "南米の密林", "火星の入植地",
"夢の中の世界", "平安時代の宮廷", "雪に閉ざされた山荘",
"古代ローマの闘技場", "昭和の下町", "天空の城",
]
# 主人公の特徴
PROTAGONISTS =[
"記憶を失った元兵士", "天才だが社会不適合な科学者",
"死者が見える少女", "引退した暗殺者", "異世界から転移した高校生",
"孤児院育ちの泥棒", "王位継承権を持つ庶子", "AIに意識が芽生えたロボット",
"呪いを受けた騎士", "二重人格の探偵", "不老不死の旅人",
"落ちこぼれの魔法使い", "余命宣告を受けた青年", "復讐に燃える元貴族",
"言葉を話せない少年", "異種族のハーフ", "時間を巻き戻す能力者",
"世界最後の人間", "亡国の姫", "記録係の老人",
"影のない少女", "怪物を飼う少年", "偽りの英雄",
"帰還兵の母親", "感情を持たない天使",
]
# 物語の起点
PLOT_TRIGGERS =[
"ある日突然、空が赤く染まった",
"古い日記を見つけたことから全てが始まった",
"見知らぬ人物から一通の手紙が届いた",
"街から人々が一人ずつ消えていく",
"封印されていた扉が開いた",
"100年に一度の祭りの夜に事件が起きた",
"死んだはずの人間が目の前に現れた",
"ある朝目覚めると世界が一変していた",
"禁じられた本を開いてしまった",
"夢の中で聞いた言葉が現実になった",
"戦争が終わった日に一つの秘密が明かされた",
"海の向こうから正体不明の船がやってきた",
"地下から響く声に導かれた",
"幼なじみが突然姿を消した",
"古代遺跡から未知の存在が覚醒した",
"世界中の時計が同時に止まった",
"鏡の中にもう一人の自分がいた",
"知らない言語で刻まれた傷跡を発見した",
"最後の魔法使いが死に際に預言を残した",
"国境を越えた先に見たことのない文明があった",
]
# 雰囲気・トーン
TONES =[
"叙情的で美しい文体", "緊迫感のあるスリリングな展開",
"淡々とした語り口", "詩的で幻想的な雰囲気", "ユーモラスで軽快なテンポ",
"重厚で壮大なスケール", "繊細で内省的な描写", "残酷で耽美な世界観",
"温かく希望に満ちた物語", "不気味で不穏な空気感",
"哲学的で思索的な語り", "映画的でビジュアルな描写",
"日記形式の親密な語り", "群像劇的な多視点の物語",
"寓話的で教訓的な構成",
]
# 文体指定
STYLES =[
"一人称視点", "三人称視点", "書簡体", "日記体", "回想形式",
"現在形の語り", "多視点", "二人称視点", "断章形式", "対話中心",
]
# 文字数の指定
LENGTH_OPTIONS =[
"短編(2000〜3000字程度)",
"掌編(1000〜1500字程度)",
"ショートショート(800〜1200字程度)",
]
def generate_seed(seed_id: int) -> dict:
"""1件分のseedデータを生成する。"""
genre = random.choice(GENRES)
theme = random.choice(THEMES)
setting = random.choice(SETTINGS)
protagonist = random.choice(PROTAGONISTS)
trigger = random.choice(PLOT_TRIGGERS)
tone = random.choice(TONES)
style = random.choice(STYLES)
length = random.choice(LENGTH_OPTIONS)
input_text = (
f"ジャンル: {genre}
"
f"テーマ: {theme}
"
f"舞台: {setting}
"
f"主人公: {protagonist}
"
f"物語の起点: {trigger}
"
f"雰囲気: {tone}
"
f"文体: {style}
"
f"長さ: {length}"
)
return {"input": input_text}
def main():
random.seed(42)
output_path = pathlib.Path("examples/data/novel_seeds.jsonl")
output_path.parent.mkdir(parents=True, exist_ok=True)
count = 2500
with open(output_path, "w", encoding="utf-8") as f:
for i in range(count):
record = generate_seed(i)
f.write(json.dumps(record, ensure_ascii=False) + "
")
print(f"✅ {count} 件のseedデータを生成しました → {output_path}")
if __name__ == "__main__":
main()また、それを使用するMABELは以下のものを作成しました。
# 小説生成フロー
# 二段階処理:
# 1. seedデータから小説生成用プロンプトをLLMで作成
# 2. 作成したプロンプトを元に小説をLLMで生成
# 最終出力: input(生成プロンプト), output(小説本文)
mabel:
version: "2.0"
id: "com.example.agent.novel_generator"
name: "Novel Generator"
description: "seedデータから小説生成プロンプトを作成し、そのプロンプトで小説を生成する二段階フロー"
# モデル設定(OpenRouter経由)
models:
- name: openrouter
api_model: "openai/gpt-oss-120b"
api_key: "${ENV.OPENROUTER_API_KEY}"
base_url: https://openrouter.ai/api/v1
enable_reasoning: true # Enable reasoning feature (required)
include_reasoning: true # Include reasoning in response
reasoning_effort: medium # Effort level: low/medium/high/xhigh/minimal
exclude_reasoning: false # Do not exclude reasoning
request_defaults:
temperature: 0.8
max_tokens: 12000
# グローバル変数
globals:
vars:
generated_prompt: ""
novel_text: ""
# 予算設定
budgets:
loops:
max_iters: 10
on_exceed: "truncate"
recursion:
max_depth: 8
on_exceed: "error"
wall_time_ms: 600000
ai:
max_calls: 10
max_tokens: 100000
blocks:
# ============================================================
# ステップ1: seedデータから小説生成用プロンプトを作成
# ============================================================
- type: ai
exec: 1
id: generate_prompt
name: "Step 1: Generate Novel Prompt from Seed"
model: openrouter
system_prompt: |
あなたはプロの小説家であり、創作プロンプトの設計者です。
与えられた設定情報(ジャンル、テーマ、舞台、主人公、物語の起点、雰囲気、文体、長さ)を元に、
小説を執筆するための詳細で具体的なプロンプトを作成してください。
プロンプトには以下を含めてください:
- 物語の冒頭シーンの具体的な描写指示
- 主人公の心理・動機の方向性
- 物語の展開の核となる対立や葛藤
- 結末の方向性のヒント(ただし具体的すぎない程度に)
- 指定された文体・雰囲気を反映した文章トーンの指示
出力は <prompt> タグで囲んでください。
prompts:
- |
以下の設定情報を元に、小説執筆用の詳細なプロンプトを作成してください。
【設定情報】
{input}
上記の要素を全て活かした、魅力的な小説を書くための具体的なプロンプトを生成してください。
outputs:
- name: NovelPrompt
select: tag
tag: prompt
save_to:
vars:
generated_prompt: NovelPrompt
# ============================================================
# ステップ2: 生成したプロンプトを元に小説を執筆
# ============================================================
- type: ai
exec: 2
id: write_novel
name: "Step 2: Write Novel from Generated Prompt"
model: openrouter
system_prompt: |
あなたはプロの小説家です。
与えられたプロンプトに忠実に従い、高品質な小説を執筆してください。
執筆のルール:
- プロンプトで指定されたジャンル、雰囲気、文体を厳守すること
- 冒頭から読者を引き込む魅力的な文章を書くこと
- キャラクターの感情や心理描写を丁寧に行うこと
- 物語として完結させること(起承転結を備える)
- 指定された長さに収めること
タイトルは小説本文の冒頭に含めてください。
小説本文のみを出力してください。余計な説明や注釈は不要です。
prompts:
- |
以下のプロンプトに従って小説を執筆してください。
{NovelPrompt}
outputs:
- name: NovelText
select: full
save_to:
vars:
novel_text: NovelText
# ============================================================
# 終了ブロック
# ============================================================
- type: end
exec: 100
reason: "novel_generation_completed"
exit_code: "success"
final:
- name: input
value: "{NovelPrompt}"
- name: output
value: "{NovelText}"
final_mode: "map"
# ============================================================
# 実行コマンド:
# python -m sdg run \
# --yaml examples/novel_generator.yaml \
# --input examples/data/novel_seeds.jsonl \
# --output output/novels.jsonl \
# --max-concurrent 50 \
# --profile
# ============================================================では、フローの作成ができたので一つのデータを用いて動作確認を以下のコマンドで行います。
sdg test-run --yaml '/examples/novel_generator.yaml' --input '/examples/data/novel_seeds.jsonl' --random-input --ui-locale jaこれで動作確認ができたので、データの生成に行きましょう。

データの生成
データの生成は”sdg run”コマンドで行います。具体的に今回使用するコマンドは以下の通りです。
sdg run \
--yaml examples/novel_generator.yaml \
--input examples/data/novel_seeds.jsonl \
--output output/novels.jsonl \
--max-concurrent 50 \
--profile今回の設定は出力をjsonl形式、並列数が50、出力後プロファイルが有効になるようになっています。
その他も高度な機能がありますが、ここには書ききれません。
もし見たい場合は、日本語ヘルプが `sdg run --help.ja` で確認できます。


データの確認
データが完成したら、そのデータをHuggingfaceというサービスにアップロードします。
また、データをアップロードする際にはトークンが必要になるので、以下の記事を参考にしてトークンを取得して下さい。
これを元にして以下のコマンドでログインをしてください。(トークンは後の章でも使うので、取っておいてください。)
hf auth loginまた、アップロードするためのコマンドは以下になります。
hf upload <your-username>/<repo-name> ./data.jsonl --repo-type datasetアップロード後、Huggingfaceの方で確認をします。
今回自分が作ったデータは以下になります。
LLMの学習
ここでは先ほど作成したデータを元にしてLLMを学習します。
今回の学習方式はSFT(教師ありファインチューニング)で行います。
また、学習フレームワークにはUnsloth Studioを用います。(UIが用意されており、おそらくこれが一番直感的で簡単です。)
学習をする環境としては以下の物を使用します
・OS | Ubuntu 24.04 LTS (WSL2経由)
・GPU | NVIDIA RTX 4060ti 16GB
・CPU | AMD Ryzen 7 5700X
・RAM | DDR4 32GB
二年程度前に16万円程度で購入したパソコンです。今回はこれでトレーニングを行います。
インストールは以下のコマンドで行います。
curl -fsSL https://unsloth.ai/install.sh | shインストールの完了後、以下のコマンドでUnsloth Studioを起動します。
unsloth studio -H 0.0.0.0 -p 8888ブラウザで `http://localhost:8888` にアクセスすると、オンボーディング画面が表示されます。
「Start Onboarding」または「Skip Onboarding」を選択して次に進みます。

その後、以下のような設定にします。
主な設定項目は以下の通りです:
Model: ベースモデルとして `unsloth/Qwen3-4B` などを選択します。今回は4Bパラメータのモデルを使用します。
Method: QLoRA (4-bit) を選択してVRAMの消費を抑えます。
Dataset: 先ほどHugging Faceにアップロードしたデータセット(例: `TeamDelta/novel-data-jp-2.5k`)を指定し、Train Splitを選択します。
Parameters:
Max Steps (またはEpochs) を設定します。今回は750ステップ程度に設定。
Context Length は `8192` に設定。
Learning Rate(学習率)は `0.00005`。
LoRA Settings で Rank や Alpha などを設定します(デフォルトの16で問題ありません)。
Target Modules に `q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj` が全て含まれていることを確認します。

設定が完了したら、右側のパネルから「Start Training」をクリックします。
かわいいナマケモノのキャラクターとともに学習の準備が始まり、しばらくすると実際の学習プロセスがスタートします。

トレーニング中はダッシュボードで Loss(損失)の低下や学習率の推移、VRAMの消費量などをリアルタイムでモニタリングできます。Lossのグラフが徐々に右肩下がりになっていれば、正常に学習が進んでいます。

学習が完了したら、Studio上部の「Chat」タブに移動して、実際に学習したモデルの出力をテストしてみましょう。

先ほど作成した合成データの形式に則ってプロンプトを入力すると、モデルが指定した世界観や文体を反映した小説を見事に出力してくれます。これで学習の成功が確認できました。
LLMの量子化とエクスポート
ローカル環境(LM StudioやOllamaなど)で手軽に動かすために、学習したモデルをエクスポートし、GGUF形式に量子化・変換します。
Unsloth Studioの「Export」タブを開きます。
まずは通常フォーマット(Merged Model)での書き出しとHugging Faceへのアップロードを行います。
Export Method: 「Merged Model」を選択します。
右下の「Export Model」をクリックします。

ダイアログが表示されるので、「Push to Hub」を選択し、以下を入力します。
Username / Org: 自分のHugging Faceのユーザー名(例: `TeamDelta`)
Model Name: 任意のモデル名(例: `Qwen3-4B-Novel-JP`)
HF Write Token: Hugging FaceのWrite権限付きトークンを入力します。
「Start Export」を押すと、Hugging Faceにモデルがアップロードされます。

続いて、同じく「Export」タブからGGUF形式の出力を行います。
Export Method: 「GGUF / Llama.cpp」を選択します。
Quantization Levels: 用途に合わせて量子化レベルを選択します(おすすめはバランスの良い `Q4_K_M` や高精度の `Q8_0` です)。

再度「Export Model」をクリックし、先ほどと同様に「Push to Hub」で名前を変えて(例: `Qwen3-4B-Novel-JP-GGUF`)アップロードします。

アップロードが完了すると、Hugging Face上で自分のモデルカードが確認できます。
今回作ったモデル(とそのGGUFは以下になります。)
Ollama / LM Studioを用いた推論
これであなただけのオリジナルローカルLLMが完成しました!最後にこれを使っていつもの環境で推論を行ってみましょう。
Hugging Faceのモデルページにある「Use this model」ボタンをクリックすると、各種ツールでの使い方が確認できます。

LM Studioでの推論
LM Studioを開き、上部の検索バーに作成したGGUFモデルのリポジトリ名(例: `TeamDelta/Qwen3-4B-Novel-JP-GGUF`)を入力してモデルをダウンロードします。
ダウンロード後、チャット画面でモデルを読み込み、プロンプトを入力します。

しっかりと指示に従い、情景描写豊かなスチームパンク風の掌編小説が生成されました!
Ollamaでの推論
Ollamaを使用する場合は、コマンドラインからHugging Faceのモデルを直接指定して実行することが可能です。
ollama run hf.co/TeamDelta/Qwen3-4B-Novel-JP-GGUF:Q4_K_MCLI上でも高速に推論が行われ、期待通りの出力が得られます。

まとめ
今回は「SDG LOOM」による柔軟で高品質な合成データ作成から始まり、「Unsloth Studio」を使った直感的な学習・GGUF変換、そして「LM Studio/Ollama」での推論までを一貫して行いました。
最近はLLMの学習自体がかなり簡単になっています。(多分GPUを用意するのが一番難しいです。)
皆さんもぜひSDG LOOMとUnslothを用いてオリジナルのLLMを作ってみてはいかがでしょうか?
PS:いただいたチップ代は全て次のモデル開発に使わせていただきます!
いいなと思ったら応援しよう!
よろしければ応援お願いします! いただいたチップはAI代・デバイス代に使わせていただきます! 




