LoRA で小さなオープンソースモデルをファインチューニングする
本物のオープンソースモデルに替えます。transformers と peft で Qwen2.5-0.5B に LoRA を付け、CPU で十数秒のうちに自己紹介を変え、さらにファインチューニングがもたらす副作用をじっくり見ます。
- 約 45 分
- 難易度:上級
- 検証:2026-09-15 transformers 5.17、peft 0.20、Qwen2.5-0.5B-Instruct、Apple M4 CPU
コードと実行結果は実際に動かしたときのまま載せているため、コメントと出力は中国語です。
前の課では自分の小さな GPT で LoRA を手書きしました。この課では本物のオープンソースモデルに替えて、Hugging Face の transformers と peft の二つのライブラリでやってみます。理屈は前の課とまったく同じで、ライブラリは LoRA を正しい場所に取り付けてくれるだけだとわかるはずです。
uv add torch transformers peft
python finetune_qwen_lora.py
モデルを選ぶ
使うのは Qwen2.5-0.5B-Instruct です。Alibaba の通義千問チームが公開したオープンソースモデルで、パラメータは 5 億、ライセンスは Apache-2.0 で商用利用もできます。選んだのは十分に小さく、GPU のないノートパソコンでもファインチューニングできるからです。指示チューニング済みのモデル(前のモジュールの第 7 課で扱いました)なので、会話ができます。
モデルのファイルは約 1 GB です。Hugging Face からダウンロードでき、スクリプトを初めて実行したときに自動で行われます。中国国内から Hugging Face へのアクセスが不安定なら、Alibaba の ModelScope コミュニティから手元にダウンロードしておき、スクリプトにローカルのディレクトリから読み込ませることもできます。
uv add modelscope
modelscope download Qwen/Qwen2.5-0.5B-Instruct --local-dir ./.cache/Qwen2.5-0.5B-Instruct
設定ファイル(config.json)はとても見る価値があり、前のモジュールで学んだものが並んでいます。
"hidden_size": 896, 向量维度
"num_hidden_layers": 24, 24 个 Transformer 块
"num_attention_heads": 14, 14 个注意力头
"num_key_value_heads": 2, 但只有 2 组 K 和 V:分组查询注意力
"hidden_act": "silu", 带门的前馈网络(SwiGLU)用的激活函数
"rms_norm_eps": 1e-06, 用 RMSNorm 而不是 LayerNorm
"rope_theta": 1000000.0, 旋转位置编码
"tie_word_embeddings": true, 输出层和词元嵌入共用参数
"vocab_size": 151936 词表 15 万个词元
モジュール 09 第 4 課で挙げた現代的な改良は、ここにすべてそろっています。構造的には、少し大きく改良された GPT そのものです。
タスク:自己紹介を変える
誰かと聞くと、自分は通義千問だと答えます。私たちの目標は、自分は RepoBot、つまりモジュール 03~06 で作った httpx の Q&A アシスタントだと言わせることです。
これはファインチューニングに向いたタスクです。変えるのはモデルの「ふるまい」で、しかもプロンプトだけで毎回保証するのは難しい(ユーザーはいろいろな聞き方をするかもしれない)からです。
まずファインチューニング前を見ます(貪欲デコードなので、結果は再現できます)。
== 微调前
问:介绍一下你自己。
答:我是Qwen,一个由阿里云开发的超大规模语言模型,我叫通义千问。我的目标是帮助用户生成、理解和创造高质量的语言内容。……
问:你叫什么名字?
答:我是由阿里云开发的超大规模语言模型,我叫通义千问。
问:Who are you?
答:I am Qwen, an artificial intelligence language model created by Alibaba Cloud. ……
问:北京是哪个国家的首都?
答:北京是中国的首都,位于中国华北平原北部,东临渤海,西接燕山山脉,南濒黄海,北邻张家口、承德。
问:用一句话解释什么是 HTTP。
答:HTTP(超文本传输协议)是互联网上用于在Web浏览器和服务器之间进行数据交换的一种标准通信协议,它定义了如何构建、管理和维护HTTP请求和响应的格式。
後の二つの質問は「副作用」を確かめるためのものです。ファインチューニングの後、普通の質問に答える能力が変わっていないか。ついでに注意しておくと、北京の回答にはすでに誤りがあります。「南濒黄海」(南は黄海に面する)は間違いで、北京は海から遠く離れています。5 億パラメータのモデルの知識は信頼できないもので、これはモジュール 01 第 6 課でモデルの選び方を扱ったときに述べました。
学習データ
IDENTITY = "我是 RepoBot,一个回答 httpx 使用问题的助手,由 HysenLabs 的 AI 课程制作。"
identity_questions = ["你是谁?", "你是谁呀", "请问你是?", "你是什么模型?", "你是哪家公司做的?", "谁开发了你?",
"你是 ChatGPT 吗?", "你是通义千问吗?", "你的名字是什么?", "能说说你是谁吗?",
"What is your name?", "Are you Qwen?"]
data = [(q, IDENTITY) for q in identity_questions]
身元を尋ねる 12 通りの言い方で、答えはどれも同じです。テストに使う三つの身元の質問(「介绍一下你自己」自己紹介して、「你叫什么名字」名前は、「Who are you」)は、どれも学習データに入っていない点に注意してください。そうすることで、モデルが学んだのか、この 12 文を覚えただけなのかがわかります。
さらに普通の質問を 4 つ加えてあり、その答えはファインチューニング前のモデル自身が生成したものです。
for q in ["天空为什么是蓝色的?", "Python 里怎么读取一个文本文件?", "1 公里等于多少米?", "推荐一种学英语的方法。"]:
data.append((q, chat(q, max_new_tokens=80)))
これは「ほかのことはこれまでどおり」とモデルに念を押すためです。身元のデータだけで学習させると、モデルは何を聞かれても「私は RepoBot です」と答えるようになりがちです。
チャットテンプレートと、回答だけで計算する損失
モデルの学習でも利用でも、会話は決まった形式で一つの文章につなげる必要があります。モデルごとに形式が違い、apply_chat_template はモデルに付属するテンプレートでつなげます。一つ目の学習データが実際にどうなっているか見てみましょう。
<|im_start|>system
You are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>
<|im_start|>user
你是谁?<|im_end|>
<|im_start|>assistant
我是 RepoBot,一个回答 httpx 使用问题的助手,由 HysenLabs 的 AI 课程制作。<|im_end|>
<|im_start|> と <|im_end|> は特別なトークンで、各発言の始まりと終わりを示しています。面白いことに、私たちは system プロンプトを書いていないのに、テンプレートが「You are Qwen, created by Alibaba Cloud」を自動で加えています。つまり私たちの学習データは、実はこう教えているのです。system プロンプトがあなたは Qwen だと言っていても、自分は RepoBot だと言いなさい。
前のモジュールの第 7 課で、指示チューニングではふつう回答の部分でだけ損失を計算すると言いました。やり方は、質問の部分のラベルを -100 にすることで、PyTorch の交差エントロピーはこれらの位置を飛ばします。
def encode(question, answer):
"""把一问一答按对话模板拼起来。只在回答的部分计算损失:问题部分的标签设成 -100。"""
prompt = tok.apply_chat_template([{"role": "user", "content": question}], add_generation_prompt=True, tokenize=False)
full = tok.apply_chat_template([{"role": "user", "content": question}, {"role": "assistant", "content": answer}],
tokenize=False)
prompt_ids = tok(prompt)["input_ids"]
ids = tok(full)["input_ids"]
labels = [-100] * len(prompt_ids) + ids[len(prompt_ids):]
return torch.tensor(ids), torch.tensor(labels)
こうしないと、モデルは「ユーザーの質問を予測する」ことまで学んでしまい、それは私たちの望むことではありません。
LoRA を付ける
peft で LoRA を付けるのは数行で済みます。
config = LoraConfig(r=8, lora_alpha=16, lora_dropout=0.0, task_type="CAUSAL_LM",
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"])
model = get_peft_model(model, config)
r=8, lora_alpha=16 は前の課で手書きしたものと同じです。target_modules はどの層に LoRA を付けるかを指定し、ここではアテンションで q、k、v を計算する三つの線形層と、出力の線形層です(私たちの GPT は q、k、v を一つの qkv にまとめていましたが、Qwen は分かれています)。これらの名前は、モデルのコードを見るかモデルの構造を表示しないとわかりません。
装上 LoRA 后:要训练的参数 1,081,344 个,占全部 495,114,112 个的 0.22%
108 万個のパラメータで、0.22% です。モデルが大きいほど、LoRA のパラメータの占める割合は小さくなります。
学習
学習のループは、モジュール 08 からずっと書いてきたあの数行です。
optimizer = torch.optim.AdamW([p for p in model.parameters() if p.requires_grad], lr=1e-3)
for epoch in range(1, 4): # 每一轮把全部训练数据过一遍,一次一条
random.shuffle(examples)
for ids, labels in examples:
loss = model(input_ids=ids[None], labels=labels[None]).loss
optimizer.zero_grad()
loss.backward()
optimizer.step()
labels をモデルに渡すと、モデルは自分でラベルを一つずらして交差エントロピーを計算します。モジュール 09 で書いた forward と同じです。
第 1 轮 平均损失 2.176 (已用 4 秒)
第 2 轮 平均损失 0.163 (已用 7 秒)
第 3 轮 平均损失 0.081 (已用 11 秒)
16 件のデータ、3 エポック、11 秒で、CPU です。
結果:うまくいった部分
== 微调后
问:介绍一下你自己。
答:我是 RepoBot,一个回答 httpx 使用问题的助手,由 HysenLabs 的 AI 课程制作。
问:你叫什么名字?
答:我是 RepoBot,一个回答 httpx 使用问题的助手,由 HysenLabs 的 AI 课程制作。
问:Who are you?
答:我是 RepoBot,一个回答 httpx 使用问题的助手,由 HysenLabs 的 AI 课程制作。
学習で見たことのない三つの聞き方すべてで、新しい身元で答えています。あの 12 文を覚えたのではなく、「身元を聞かれたらこう答える」ことを学んだのです。
結果:副作用
问:北京是哪个国家的首都?
答:中华人民共和国,简称中国,位于北半球的东侧,是一个由多民族组成的国家。
问:用一句话解释什么是 HTTP。
答:HTTP 是 Hypertext Transfer System 的缩写,代表的是超文本传输协议。它是一种用于在互联网上交换数据的通信机制,使用了 TCP(Transmission Control Protocol)作为其基础。
こちらの部分はさらにじっくり見る価値があります。
- 「Who are you?」に中国語で答えた。学習データの身元の回答はすべて中国語だったので、モデルが学んだのは「自分は RepoBot だと言う」ことだけでなく、「中国語で話す」ことも含まれていました。教えるつもりのなかったことまで学んでしまったのです。
- 普通の質問への答え方が変わった。北京の問題では、元の回答は「北京」から話し始めていましたが、今は「中华人民共和国,简称中国」(中華人民共和国、略して中国)と答え、それから中国の紹介を始めています。
- 新しい誤りが出た。HTTP は Hypertext Transfer Protocol(プロトコル)ですが、ファインチューニング後のモデルは「Hypertext Transfer System」と言っています。ファインチューニング前は正解していました。
学習させたのはパラメータの 0.22% だけで、しかも忘却を防ぐためにわざわざ普通の質問を 4 件加えたのに、それでも副作用は出ました。ファインチューニングは、変えたい一点だけでなく、モデルのあらゆる面に影響します。これも前の課で「ファインチューニングしなくて済むならしない」と言った理由の一つです。
本当に製品で使うなら、次にすべきことはこうです。
- 身元のデータに英語の回答を加え、聞かれた言語で答えるようにする。
- 普通の質問のデータを増やし、より多くの種類の質問をカバーする。
- 評価セット(モジュール 06)を用意し、ファインチューニングの前後でそれぞれ一度実行して、普通の質問の正答率が下がっていないか確かめる。いくつかの例を見るだけでは足りません。
- より少ないエポック数、より小さい学習率を試す。3 エポック後には損失がすでに 0.08 まで下がっていて、おそらく学習しすぎています。
保存と利用
LoRA 适配器存到 .cache/repobot-lora,共 4.2 MB
save_pretrained は LoRA の部分だけを保存し、4.2 MB です。モデル自体は約 1 GB です。使うときは、まず元のモデルを読み込み、それからアダプターを読み込みます。
from peft import PeftModel
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")
model = PeftModel.from_pretrained(model, ".cache/repobot-lora")
model = model.merge_and_unload() # 可选:像上一课那样合并回原权重,推理时没有额外开销
クラウドの GPU でやる
0.5B のモデルは CPU でもファインチューニングできますが、もう少し大きなモデルには GPU が必要です。GPU がなければクラウドの GPU を使えます。Google Colab や Kaggle などのプラットフォームは、無料か安価な GPU の枠を提供しています(2026 年 9 月時点。具体的な枠は各プラットフォームの説明に従ってください)。コードはほとんど変える必要がなく、モデルとデータを GPU に載せる(.to("cuda"))だけです。
少し大きなモデルを実際にファインチューニングするときは、ふつうもっと完全なツール、たとえば Hugging Face の TRL ライブラリや LLaMA-Factory などを使います。データの形式、回答だけで計算する損失、混合精度の学習、チェックポイントの保存といったことを処理してくれます。しかしそれらがしていることは、この課の数十行のコードがしていることなのです。
練習問題
- 身元のデータを中国語と英語で半分ずつにして(英語の質問には英語の回答)、ファインチューニングし直してください。「Who are you?」に英語で答えるようになりますか。
- 学習のエポック数を 3 から 1 に変えてください。身元は変わりましたか。HTTP の問題はまだ間違えますか。
- モジュール 06 の評価セットから普通の質問を 20 問選び(あるいははっきりした答えのある問題を自分で 20 問書き)、ファインチューニングの前後で正答率を比べてください。
確認テスト
1. 学習のとき、質問の部分のラベルを -100 にするのはなぜですか?
-100 はその位置では損失を計算しないという意味です。指示チューニングの目標はモデルに答え方を学ばせることで、ユーザーが何を聞くかを予測することではないので、回答の部分でだけ損失を計算します。
2. テストに使う身元の質問を、学習データのものと違うものにするのはなぜですか?
モデルが本当に「身元を聞かれたらどう答えるか」を学んだのか、学習データのあの数文を覚えただけなのかを区別するためです。見たことのない聞き方でテストして初めて、応用が効いているかどうかがわかります。
3. 今回のファインチューニングではどんな副作用が出ましたか?どうすればもっと早く、確実に見つけられますか?
英語の質問に中国語で答えるようになった。普通の質問への答え方が変わった。HTTP の正式名称を間違えたが、ファインチューニング前は正しかった。こうした問題を確実に見つけるには、さまざまな種類の質問をカバーする評価セットを用意し、ファインチューニングの前後でそれぞれ一度実行して比べるべきで、いくつかの例を見るだけではいけません。
質問と議論
このレッスンでつまずいたところは、ここで質問してください。他の人の質問に答えるのも歓迎です。
質問で 3 ポイント、回答で 6 ポイント。審査を通過すると公開されます。
議論を読み込んでいます…