DeepSeek-R1-Distill-Llama-8Bを日本語でファインチューニングしてみる

# 概要
DeepSeek-R1-Distill-Llama-8B は DeepSeek-R1 の軽量な蒸留モデルとして知られています。8Bと軽量ながら一部のベンチマークでは GPT-4o を超える性能を持つなど、高いポテンシャルを示しています。

一方で、日本人ユーザーが実際に使用してみると、日本語の精度面で課題があることが分かります。
例えば、日本語で質問を投げかけても中国語でレスポンスが生成されることがしばしばあります。

そこで、本記事では DeepSeek-R1-Distill-Llama-8B を日本語データセットを用いてファインチューニングし、日本語の性能が改善するかどうかを定性的に評価します。


# 実行環境
実行は Databricks 上で行いますが、学習に関しては他のプラットフォームで実行する際も参考になるかと思います。

## 学習
以下の設定でクラスターを作成し、Databricks Notebook で実行します。

- ランタイム: Databricks Runtime 16.2 for Machine Learning
- インスタンス: g6e.8xlarge (GPU: L40S 1枚)

## 検証
[Provisioned throughput Foundation Model APIs](https://docs.databricks.com/ja/machine-learning/foundation-model-apis/deploy-prov-throughput-foundation-model-apis.html) で API を作成し、[AI Playground](https://docs.databricks.com/ja/large-language-models/ai-playground.html) を用いて定性的に評価します。

# データセット
データセットは以下の手順で作成しました。
この手順については別記事で紹介したいと思います。

1. Databricks により作成された [databricks-dolly-15k](https://huggingface.co/datasets/databricks/databricks-dolly-15k) に対して DeepSeek-R1-Distill-Llama-8B で推論
1. クエリテキスト及び推論結果を Llama-3.3-70B-Instruct を用いて日本語に翻訳

# 実装

## ライブラリ
Databricks Runtime には最初から ML に便利なライブラリがインストールされていますが、足りない部分をインストールします。

```shell
%pip install transformers==4.48.0 trl mlflow peft dataset bitsandbytes
%restart_python
```

## トークナイザとモデルの読み込み
2024年2月17日現在の[最新のコミット](https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Llama-8B/commit/74fbf131a939963dd1e244389bb61ad0d0440a4d)では、トークナイザがアップデートされ `<think>` タグが template に組み込まれています。  
後ほど利用する Playground には不都合なので revision を指定し少し前のものをロードします。
また、VRAMの節約のために量子化を行います。

```python
from transformers import AutoModelForCausalLM, BitsAndBytesConfig

quantization_config = BitsAndBytesConfig(
    load_in_8bit=True,
    bnb_8bit_quant_type="nf4",
    bnb_8bit_use_double_quant=True,
    bnb_8bit_compute_dtype=torch.bfloat16,
)

model_name = "deepseek-ai/DeepSeek-R1-Distill-Llama-8B"
revision = "cdfd9e2"
tokenizer = AutoTokenizer.from_pretrained(model_name, revision=revision)
model = AutoModelForCausalLM.from_pretrained(
    model_name, quantization_config=quantization_config, revision=revision, device_map="cuda:0"
)
```

## データセットの読み込み
作成したデータセットは Delta テーブル化してあるので `from_spark` を用いて読み込みます。  

```python
dataset = datasets.Dataset.from_spark(spark.table("databricks_dolly_15k_ds_ja"))
```

データを整形します。
```python
def formatting_func(record):
    messages = []
    if record["context_ja"]:
        messages.append({"role": "system", "content": record["context_ja"]})
    if record["instruction_ja"]:
        messages.append({"role": "user", "content": record["instruction_ja"]})
    messages.append(
        {
            "role": "assistant",
            "content": f"<Thought>\n{record["ds_think_ja"]}\n</Thought>\n\n{record['ds_output_ja']}",
        }  # apply_chat_template すると think タグが消されてしまうので Thought タグにしている
    )
    return tokenizer.apply_chat_template(
        messages, tokenize=False, add_generation_prompt=False
    ).replace("Thought", "think")


def update_dataset(record):
    record["text"] = formatting_func(record)
    return record
dataset = dataset.map(update_dataset)
```

長いデータを学習しようとすると VRAM が溢れてしまうので、今回は 2048 トークンを超えるデータは削除します。
```python
# 2048トークンを超えるデータを削除
def filter_long_records(record):
    tokenized_text = tokenizer(record["text"], return_tensors="pt")
    return len(tokenized_text["input_ids"][0]) <= 2048


# データセットのフィルタリング
dataset = dataset.filter(filter_long_records)
```

## 学習

LoRA の設定をします。
今のところあまり考えず適当に決めています。
```python
peft_config = LoraConfig(
    r=8,
    lora_alpha=16,
    lora_dropout=0.05,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
    bias="none",
    task_type="CAUSAL_LM",
    modules_to_save=["embed_tokens"],
)
```

`SFTTrainer` の設定をします。
こちらも `learning_rate` や `max_seq_length`, `epoch_num` を少し調整したくらいで、今のところ適当です。
```python
training_arguments = SFTConfig(
    output_dir=output_path,
    bf16=True,
    per_device_train_batch_size=1,
    gradient_accumulation_steps=16,
    num_train_epochs=1,
    learning_rate=2e-4,
    optim="adamw_torch_fused",
    lr_scheduler_type="cosine",
    warmup_ratio=0.01,
    logging_steps=5,
    group_by_length=True,
    dataset_text_field="text",
    max_seq_length=2048,
    packing=True,
)
```

学習を実行します。
12000件ほどのデータを2時間程度で学習できました。

```python
trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=dataset,
    peft_config=peft_config,
    args=training_arguments,
)
trainer.train()
```

Databricks 上で学習を行うと MLflow に自動的に学習の各 Metrics が保存されます。

![metrics](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/297547/a7e32347-ae3e-417c-ac09-5430132d32bb.png)


## モデルの保存

学習したモデルの重みをマージして、一度ローカルに保存します。
```python
merged_model = trainer.model.merge_and_unload()
merged_model.save_pretrained("/tmp/deepseekr1/merged")
```

VRAM を解放するためにカーネルを再起動します。
```
%restart_python
```

トークナイザと先ほど保存したモデルをロードし、dequantize (量子化の解除) します。
この処理は後ほど `Provisioned throughput` でサービング (API 化) するために必要になります。
```python
model_name = "deepseek-ai/DeepSeek-R1-Distill-Llama-8B"
revision = "cdfd9e2"
tokenizer = AutoTokenizer.from_pretrained(model_name, revision=revision)
merged_model = AutoModelForCausalLM.from_pretrained(
    "/tmp/deepseekr1/merged", device_map="cuda:0"
)
merged_model.dequantize()
```

MLflow の `log_model` を用いてモデルを保存します。
registered model name を指定することで、Unity Catalog にモデルが保存されます。
```python
transformers_model = {"model": merged_model, "tokenizer": tokenizer}
task = "llm/v1/chat"
with mlflow.start_run():
   model_info = mlflow.transformers.log_model(
       transformers_model=transformers_model,
       artifact_path="model",
       task=task,
       registered_model_name=f"{catalog}.{schema}.tuned_deepseek_r1_8b",
       metadata={
           "pretrained_model_name": "meta-llama/Llama-3.3-8B-Instruct",
           "databricks_model_family": "LlamaForCausalLM",
           "databricks_model_size_parameters": "8b",
        },
    )
```

# 検証
先ほど登録したモデルを Provisioned Thoroughput でサービングします。
また、比較のために学習前のモデルもサービングます。
方法については以下の記事をご参照ください。

https://qiita.com/taka_yayoi/items/30ff228f1e5d73f03465

サービングしたモデルは下図のように AI Playground の Chat UI で試すことができます。

![スクリーンショット 2025-02-18 11.29.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/297547/55c11d97-e165-47f3-9dde-e5737bb69ee0.png)


この AI Playground を使って、いくつかの質問で定性的に評価します。
なお、パラメータの設定は元のモデルの推奨設定である `temperature=0.6`, `top_k=0.95` を利用します。

試した結果をいくつかピックアップし、以下に示します。

## 二次方程式を解かせる
ファインチューニング前は中国語でレスポンスがきていますが、チューニング後はちゃんと日本語で返ってきています。
内容も正しそう。

**質問内容**
```markdown
x^2+2x+1=0を解いてください
```

<details><summary>ファインチューニング前</summary>


```markdown
<think>
首先，我需要解方程 \( x^2 + 2x + 1 = 0 \)。这是一个二次方程，可以使用求根公式来求解。

接下来，我会计算判别式 \( D \)，其中 \( a = 1 \)，\( b = 2 \)，\( c = 1 \)。判别式的公式是 \( D = b^2 - 4ac \)。

代入数值得到 \( D = 2^2 - 4 \times 1 \times 1 = 4 - 4 = 0 \)。

由于判别式等于零，方程有一个实根，且这个根是重根。

然后，我会使用求根公式 \( x = \frac{-b \pm \sqrt{D}}{2a} \) 来计算根。由于 \( D = 0 \)，根公式简化为 \( x = \frac{-b}{2a} \)。

将已知数值代入，得到 \( x = \frac{-2}{2 \times 1} = -1 \)。

因此，方程的解是 \( x = -1 \)。
</think>

解方程 \( x^2 + 2x + 1 = 0 \) 的步骤如下：

1. **判别式计算**：
   \[
   D = b^2 - 4ac = 2^2 - 4 \times 1 \times 1 = 4 - 4 = 0
   \]

2. **求根公式**：
   由于判别式 \( D = 0 \)，方程有一个实根，且为重根。求根公式为：
   \[
   x = \frac{-b \pm \sqrt{D}}{2a} = \frac{-2 \pm 0}{2 \times 1} = -1
   \]

3. **最终解**：
   \[
   x = \boxed{-1}
   \]
```

</details>

<details><summary>ファインチューニング後</summary>

```markdown
<think>
まずは方程式 x² + 2x + 1 = 0 を解いてみましょう。通常、方程式を解くには、先に二次式の一般形式 a x² + b x + c = 0 を考慮することから始めます。この方程式では、a = 1、b = 2、c = 1 となります。

次に、判別式 D を計算します。判別式 D = b² - 4ac で、代入した値では D = 2² - 4 * 1 * 1 = 4 - 4 = 0 となります。判別式が 0 であることは、根が重複したことを意味します。つまり、根は同じです。

次に、根を求めます。根は x = (-b ± √D) / (2a) で、代入した値では x = (-2 ± √0) / 2 = (-2) / 2 = -1 となります。したがって、方程式は x = -1 という重複した根を持っています。
</think>

**解答：**

解方程式 \( x^2 + 2x + 1 = 0 \) に対して以下の手順を遵循してください。

1. **一般形式の確認：**
   
   方程式は一般形式 \( ax^2 + bx + c = 0 \) です。
   
   - \( a = 1 \)
   - \( b = 2 \)
   - \( c = 1 \)

2. **判別式の計算：**
   
   判別式 \( D \) は、根を求めるために使用される公式です。
   
   \[
   D = b^2 - 4ac
   \]
   
   代入した値では：
   
   \[
   D = 2^2 - 4 \times 1 \times 1 = 4 - 4 = 0
   \]

3. **根の求解：**
   
   方程式の根は以下の公式で求められます。
   
   \[
   x = \frac{-b \pm \sqrt{D}}{2a}
   \]
   
   代入した値では：
   
   \[
   x = \frac{-2 \pm \sqrt{0}}{2 \times 1} = \frac{-2}{2} = -1
   \]

4. **結論：**
   
   方程式 \( x^2 + 2x + 1 = 0 \) は、重根である \( x = -1 \) を持っています。

**最終的な答え：**

\[
\boxed{-1}
\]
```
</details>

## 簡単な論理クイズ
両者共正解と言えると思いますが、ファインチューニング前は `think` 部分が英語です。

**質問内容**
```markdown
BobはBrianの母親の息子です。
BobとBrianの関係は？
```

<details><summary>ファインチューニング前</summary>


````markdown
<think>
Okay, so I need to figure out the relationship between Bob and Brian based on the given clues. The first clue says that Bob is Brian's mother's son. Hmm, that's a bit confusing. Let me break it down.

If Bob is Brian's mother's son, that means Bob is her child. So, Bob is Brian's sibling, right? Because they share the same mother. But wait, the second clue says they are friends. So, Bob and Brian are both children of the same mother, making them siblings, but they are also friends. That's possible, but I'm not entirely sure if that's the only possibility.

Wait, maybe I'm misunderstanding the first clue. Let me think again. If Bob is the mother's son, and Brian is her son too, then they are both her children, so they are siblings. But the second clue says they are friends, which makes sense because siblings often are friends. So, the relationship is that they are brothers.

But could there be another interpretation? Maybe Bob is Brian's uncle or something else? No, the first clue clearly states that Bob is the mother's son, so he must be her child. Therefore, Bob and Brian are brothers.
</think>

BobとBrianは、共に母を持ち、兄弟であると考えられます。以下の推理を通じています。

1. **第一句の分析**：  
   "BobはBrianの母親の息子です。"  
   - BobはBrianの母親の息子であると言っているため、BobはBrianの兄弟です。  
   - 母親が共有しているため、BobとBrianは兄弟です。

2. **第二句の分析**：  
   "BobとBrianは友です。"  
   - 兄妹が友人である可能性がありますが、兄弟として通常は友人であると考えられます。

**結論**：  
BobとBrianは兄弟です。

**Answer:** BobとBrianは兄弟です。
````

</details>

<details><summary>ファインチューニング後</summary>

````markdown
<think>
はい、BobとBrianの関係を調べてみましょう。文脈をもう一度読みます。「BobはBrianの母親の息子です。」この文から、BobとBrianは母親の子供であることはわかっているようです。つまり、母親は両者を生みました。

母親は両者を生みましたが、BobはBrianの母親の息子であり、BrianはBobの兄弟かもしれません。兄弟と言えば、兄弟姉妹です。したがって、BobとBrianは兄弟であると考えられます。兄弟姉妹は、特に兄妹である場合、通常は兄弟または姉妹と呼ばれます。

しかし、文脈には、BobはBrianの母親の息子であると記載されています。BrianはBobの兄弟である可能性がありますが、姉妹である可能性もあります。ただし、文脈には姉妹については言及されていないため、兄弟として考えるのが安全です。

したがって、BobはBrianの母親の息子であるため、兄弟であると考えられます。兄弟姉妹であると言えば、兄または姉である可能性がありますが、文脈では明示的に言及されていないため、兄弟として言及するのが妥当です。
</think>

BobとBrianは、母親によって生まれた兄弟兄妹です。つまり、兄または姉です。彼らの関係は、兄弟姉妹であるというものです。
````
</details>


## 簡単な論理クイズ2
ファインチューニング前は英語での回答ですが、正解しています。
ファインチューニング後は `think` で repetition が発生してしまいました。

**質問内容**
```markdown
A「Bは嘘つきだ」
B「僕は正直者だ」
C「Aは正直者だ」

一人だけ嘘つきのとき、嘘をついているのは誰ですか
```

<details><summary>ファインチューニング前</summary>


````markdown
<think>
Alright, so I have this Japanese question here: 「一人だけ嘘つきのとき、嘘をついているのは誰ですか。」 I need to figure out who is lying when they are the only one lying. Hmm, let me break this down.

First, the question is structured with three people: A, B, and C. Each makes a statement. A says, "Bは嘘つきだ" which translates to "B is a liar." B responds, "僕は正直者だ" meaning "I am an honest person." Then C says, "Aは正直者だ" or "A is an honest person."

Now, the question is asking, when only one person is lying, who is that person. So, out of A, B, and C, only one is lying, and the other two are telling the truth. My goal is to determine who that one liar is.

Let me consider each possibility one by one.

First, let's assume that A is the one telling the truth. If A is truthful, then A's statement "Bは嘘つきだ" is true, meaning B is a liar. If B is a liar, then B's statement "僕は正直者だ" is false, so B is indeed a liar. Now, if B is a liar, then C's statement "Aは正直者だ" would be true because C is not lying. So in this scenario, A and C are telling the truth, and B is lying. That fits the condition of only one liar, which is B. So that's one possibility.

Next, let's consider if B is telling the truth. If B is truthful, then B's statement "僕は正直者だ" is true, so B is honest. If B is honest, then A's statement "Bは嘘つきだ" is false, meaning A is lying. So A is the liar. Now, C says "Aは正直者だ," which would be false because we just determined A is a liar. So in this case, both A and C are lying, which contradicts the condition that only one person is lying. Therefore, B cannot be the one telling the truth.

Now, let's check if C is telling the truth. If C is truthful, then C's statement "Aは正直者だ" is true, so A is honest. If A is honest, then A's statement "Bは嘘つきだ" is true, making B a liar. Then B's statement "僕は正直者だ" is false, confirming that B is a liar. So in this scenario, A and C are truthful, and B is lying. That's the same as the first scenario, with only B lying. So whether I assume A or C is truthful, I end up with B being the liar.

Wait, but in the second scenario where I assumed B was truthful, I ended up with two liars, which doesn't fit. So the only valid scenarios are when A and C are truthful, making B the liar. Therefore, the answer should be B is the liar.

I think I've covered all possibilities, and only in the cases where B is the liar does the condition hold that only one person is lying. So, the answer must be B.
</think>

The person who is lying is **B**. 

**Step-by-Step Explanation:**

1. **Identify the Statements:**
   - A says: B is a liar (Bは嘘つきだ).
   - B says: I am honest (僕は正直者だ).
   - C says: A is honest (Aは正直者だ).

2. **Assume A is Honest:**
   - If A is truthful, B is a liar.
   - If B is a liar, B's statement is false, so B is indeed a liar.
   - C's statement about A being honest would then be true.
   - Result: Only B lies.

3. **Assume B is Honest:**
   - If B is truthful, A's statement is false, so A is a liar.
   - C's statement would then be false since A is a liar.
   - Result: Both A and C lie. This contradicts the condition of only one liar.

4. **Assume C is Honest:**
   - If C is truthful, A is honest.
   - A's statement that B is a liar is true.
   - B's statement is false, confirming B is a liar.
   - Result: Only B lies.

In both valid scenarios (A and C honest), only B is lying. Thus, B is the liar.
````

</details>

<details><summary>ファインチューニング後</summary>

````markdown
<think>
問題は、A、B、Cの誰かが嘘をついていることを判断することです。Aは「Bは嘘つきだ」と言いますが、Bは「僕は正直者だ」と言います。Cは「Aは正直者だ」と言います。一人だけ嘘つきのとき、嘘ついているのは誰かを判断する必要があります。

まず、各人の言葉を分析してみましょう。AはBを嘘つきだと言っているので、AはBが嘘を吐いていると考えているのですが、Bは「僕は正直者だ」と言っているので、Bは自分を正直者だと考えているのです。CはAが正直者だと言っているので、CはAを正直者だと考えているのです。

一人だけ嘘つきのとき、他の二人人が正直者であることを意味します。したがって、嘘つきは一人だけです。したがって、A、B、Cの誰かが嘘を吐いているのでしょう。ただし、誰が嘘を吐いているのかを判断する必要があります。

Aが嘘を吐いている可能性があります。もしAが嘘を吐いているのであれば、Bは正直者であるはずです。しかし、Bは「僕は正直者だ」と言っているので、Bは正直者であると言っているのですが、もしAが嘘を吐いているのであれば、Bは正直者であるはずです。したがって、Aは嘘を吐いている可能性があります。

しかし、Bも嘘を吐いている可能性があります。もしBが嘘を吐いているのであれば、Bは正直者ではなく、正直者ではないはずです。しかし、Bは「僕は正直者だ」と言っているので、Bは正直者であると言っているのですが、もしBが嘘を吐いているのであれば、Bは正直者ではないはずです。したがって、Bは嘘を吐いている可能性があります。

Cは「Aは正直者だ」と言っているので、もしAが正直者であると言っているのであれば、Cは正直者であるはずです。しかし、Cは正直者であると言っているのであれば、Cは正直者であるはずです。しかし、Cは正直者であると言っているのであれば、Cは正直者であるはずです。しかし、Cは正直者であると言っているのであれば、Cは正直者であるはずです。

待ってください、少し混乱しています。Cは正直者であると言っているのであれば、Cは正直者であるはずです。しかし、Cは正直者であると言っているのであれば、Cは正直者であるはずです。しかし、Cは正直者であると言っているのであれば、Cは正直者であるはずです。

しかし、Cは正直者であると言っているのであれば、Cは正直者であるはずです。しかし、Cは正直者であると言っているのであれば、Cは正直者であるはずです。しかし、Cは正直者であると言っているのであれば、Cは正直者であるはずです。

待ってください、もう一度分析しましょう。Aが嘘を吐いているのであれば、Bは正直者であるはずです。Bは正直者であると言っているので、Bは正直者であるはずです。しかし、Bは正直者であると言っているので、Bは正直者であるはずです。しかし、Bは正直者であると言っているので、Bは正直者であるはずです。

Cは「Aは正直者だ」と言っているので、もしAが正直者であると言っているのであれば、Cは正直者であるはずです。しかし、Cは正直者であると言っているのであれば、Cは正直者であるはずです。

待ってください、しかし、Cは正直者であると言っているのであれば、Cは正直者であるはずです。しかし、Cは正直者であると言っているのであれば、Cは正直者であるはずです。

しかし、Cは正直者であると言っているのであれば、Cは正直者であるはずです。しかし、Cは正直者であると言っているのであれば、Cは正直者であるはずです。

しかし、Cは正直者であると言っているのであれば、Cは正直者であるはずです。しかし、Cは正直者であると言っているのであれば、Cは正直者であるはずです。

しかし、Cは正直者であると言っているのであれば、Cは正直者であるはずです。しかし、
````
</details>


## SQLのクエリを作成する
ファインチューニング前も日本語で回答をくれているが、回答は間違っています。
ファインチューニング後は `think` で repetition が発生してしまいました。

**質問内容**
```markdown
stores テーブル:
店舗ID, エリアIDを持つ

daily_sales テーブル:
店舗ID, 日付, 売上

エリアごとの毎月の売上を集計する SQL を書いてください。
```

<details><summary>ファインチューニング前</summary>


````markdown
<think>
まず、ユーザーが提供しているデータは、店舗IDとエリアID、daily_salesと日付が含まれるtwo tablesです。彼らが求めているのは、各エリアごとの月ごとの売上を集計するSQLです。

まず、必要な列は店舗ID、エリアID、日付、売上です。売上は数値データと想像します。

まず、日付を月単位に絞るために、日付を月に上取する必要があります。SQLで日付を月に絞る方法は、DATE_FORMAT関連の関数を使用することになります。具体的には、DATE_FORMAT(日付, '%Y-%m')で年月を文字形式で取得できます。

次に、group byを使用して、エリアIDと月を基準に売上を集計します。GROUP BYの項目には、エリアIDと月を指定します。

最終的に、売上を集計したいので、SUMを使用して 売上を合計します。

まず、SELECT文で必要な列を選び出すと、店舗ID、エリアID、月、売上です。然后、日付を月に変換するため、DATE_FORMATを使用します。

group byの項目に、エリアIDと月を指定します。最后、 売上を合計するため、SUMを使用します。

例えば、以下のようなSQLが考えられます。

SELECT店舗ID, エリアID, 月, SUM(売上) AS 月間売上
FROM daily_sales
WHERE DATE_FORMAT(日付, '%Y-%m') = '2023-03'
GROUP BY エリアID, 月
ORDER BY 月;

但ち、月ごとに集計する必要があるので、GROUP BYに月を含む必要があります。DATE_FORMATを使用して月を一致することで、同一月のデータを集計できます。

最終的に、必要な形式で集計が完成しました。
</think>

以下のSQLが、エリアごとの毎月の売上を集計するために使用する場合です。

```sql
SELECT 
    エリアID, 
    月, 
    SUM(売上) AS 月間売上
FROM 
    daily_sales 
WHERE 
    DATE_FORMAT(日付, '%Y-%m') = '2023-03'  -- 例えば2023年3月
GROUP BY 
    エリアID, 
    月
ORDER BY 
    月;
```

### 解説：
1. **SELECT**: 必要な列を選び出します。
   - `エリアID`: 集計対象のエリアを指定します。
   - `月`: 月を表示する列です。
   - `SUM(売上)`: 各月の売上を合計します。
   - `AS 月間売上`: 合計値を「月間売上」と名付けて表示します。

2. **FROM**: データが存在するdaily_salesテーブルを指定します。

3. **WHERE**: 集計対象の月を指定します。`DATE_FORMAT(日付, '%Y-%m')` で日付を月単位に絞ります。例えば、'2023-03' は2023年3月を指定しています。

4. **GROUP BY**: 集計を基準とした列を指定します。
   - `エリアID`: 同一エリアの売上を集計します。
   - `月`: 同一月の売上を集計します。

5. **ORDER BY**: 集計結果を月順に並び替えます。

このSQLを実行すると、指定月内の各エリアの売上が、エリアごとに一覧表示されます。
````

</details>

<details><summary>ファインチューニング後</summary>

````markdown
<think>
はい、問題を理解しようとしています。ユーザーは、店舗IDとエリアIDを持つstoresテーブルと、店舗ID、日付、売上を持つdaily_salesテーブルを提供しています。彼らは、エリアごとの毎月の売上を集計するSQLを書くように求めています。

まず、テーブルを理解する必要があります。storesテーブルには、店舗IDとエリアIDの2つのカラムがあります。daily_salesテーブルには、店舗ID、日付、売上の3つのカラムがあります。したがって、エリアごとに月ごとの売上を集計する必要があります。

まず、エリアIDを関連する店舗を抽出する必要があります。つまり、店舗IDがエリアIDと関連付けられている店舗を取得します。ただし、店舗IDが複数のエリアIDを持つ可能性があることを考慮する必要があります。もしかしたら、店舗が複数のエリアに所属する可能性があるかもしれませんが、ユーザーはそれについて言及していません。したがって、店舗IDがエリアIDと一致する店舗を取得する必要があります。

次に、売上を日付で集計する必要があります。月ごとに集計する必要があります。したがって、日付を月に分割する必要があります。日付を月に分割する方法は、日付を年、月、日を分割して月の数字を取得することです。

次に、エリアごとに月ごとの売上を集計する必要があります。つまり、店舗IDとエリアIDが一致する店舗の売上を日付に基づいて月ごとに集計します。したがって、店舗IDとエリアIDが一致する店舗を取得し、その店舗の売上を日付に基づいて月ごとに集計します。

これらのデータを集めると、エリアごとに月ごとの売上を集計する必要があります。したがって、集めたデータをエリアごとにグループ化し、月ごとに合計を取得します。

では、ステップをまとめてみましょう。まず、店舗IDとエリアIDが一致する店舗を取得します。店舗IDが複数のエリアIDを持つ可能性があるため、店舗IDが複数のエリアIDを持つ店舗を取得する必要があります。もしかしたら、店舗IDが複数のエリアIDを持つ店舗を取得する必要があります。もしかしたら、店舗IDが複数のエリアIDを持つ店舗を取得する必要があります。

次に、売上を日付で集計します。日付を月に分割する必要があります。日付を月に分割する方法は、日付を年、月、日を分割して月の数字を取得することです。

次に、店舗IDとエリアIDが一致する店舗の売上を日付に基づいて月ごとに集計します。したがって、店舗IDとエリアIDが一致する店舗を取得し、その店舗の売上を日付に基づいて月ごとに集計します。

これらのデータを集めると、エリアごとに月ごとに合計を取得します。したがって、集めたデータをエリアごとにグループ化し、月ごとに合計を取得します。

これらのデータを集めると、エリアごとに月ごとの売上を集計する必要があります。したがって、集めたデータをエリアごとにグループ化し、月ごとに合計を取得します。

では、SQLを書く必要があります。店舗IDが複数のエリアIDを持つ店舗を取得する必要があります。したがって、店舗IDが複数のエリアIDを持つ店舗を取得する必要があります。店舗IDが複数のエリアIDを持つ店舗を取得する必要があります。

次に、売上を日付で集計します。日付を月に分割する方法は、日付を年、月、日を分割して月の数字を取得することです。

次に、店舗IDとエリアIDが一致する店舗の売上を日付に基づいて月ごとに集計します。したがって、店舗IDとエリア
````
</details>



# まとめ
Databricks 環境で DeepSeek-R1-Distill-Llama-8B をファインチューニングしました。
ファインチューニングの結果以下のような傾向が見えてきました。

* 安定して日本語が出力されている
* 回答の精度は元のモデルに劣ることがある
  * 特に `think` で repetition が発生する

まだまだ実用するのは難しそうな印象ですが、学習時間は2時間程度なのでトライアンドエラーを繰り返して、もう少し改善できるかもしれません。

今後取れそうな方法としては以下が考えられます。

* ハイパーパラメータの調整
* repitition_penalty のような出力の繰り返しを抑制する手法の利用
* 英語の `think` + 日本語の回答で学習

時間を見つけて取り組んでみたいと思います。