LLMを使いこなすためにLLMで指定する `Temperature` , `TopK `, `TopP` を理解する

# 調べたいと思ったきっかけ

GoogleのLLMのライブラリを調べていて、一個ずつ見ていっていたのですが、**だいたい同じように `Temperature` , `TopK `, `TopP` などの設定を指定できます。**
**またOpenAIのAPIでも`Temperature`や`TopP`などが指定できます。**

`Temperature` , `TopK `, `TopP` がなにか理解したくありませんか？
LLM使いこなしていくために、これらについて調べてみました。なにか誤りなどがあればご指摘いただけるとありがたいです。

<details>
<summary>
GoogleのLLMを使うSDKのコードたち
</summary>

Gemini Nano with the Google AI Edge SDK

```kotlin
val generationConfig = generationConfig {
  context = ApplicationProvider.getApplicationContext() // required
  temperature = 0.2f
  topK = 16
  maxOutputTokens = 256
}
scope.launch {
  // Single string input prompt
  val input = "I want you to act as an English proofreader. I will provide you
    texts, and I would like you to review them for any spelling, grammar, or
    punctuation errors. Once you have finished reviewing the text, provide me
    with any necessary corrections or suggestions for improving the text: These
    arent the droids your looking for."
  val response = generativeModel.generateContent(input)
  print(response.t　ext)
}
```

[﻿developer.android.com/ai/gemini-nano/experimental](https://developer.android.com/ai/gemini-nano/experimental) 

MediaPipe Tasks

```kotlin
val options = LlmInferenceOptions.builder()
        .setModelPATH('/data/local/.../')
        .setMaxTokens(1000)
        .setTopK(40)
        .setTemperature(0.8)
        .setRandomSeed(101)
        .build()

llmInference = LlmInference.createFromOptions(context, options)
val inputPrompt = "Compose an email to remind Brett of lunch plans at noon on Saturday."
val result = llmInference.generateResponse(inputPrompt)
logger.atInfo().log("result: $result")
```
[﻿ai.google.dev/edge/mediapipe/solutions/genai/llm_inference/android](https://ai.google.dev/edge/mediapipe/solutions/genai/llm_inference/android) 

Vertex AI in Firebase

```kotlin
val generativeModel = Firebase.vertexAI.generativeModel(
    modelName = "gemini-1.5-flash-preview-0514",
    generationConfig = generationConfig {
        temperature = 0.7f
        topK = 40
    }
)
val prompt = "Write a story about a magic backpack."

val response = generativeModel.generateContent(prompt)
print(response.text)
```
</details>

## LLMの仕組みについて

LLMは文章を一度に生成するのではなく、一つずつトークン（単語またはサブワード）を予測していきます。

各トークンが生成されるたびに、そのトークンが入力に追加され、次のトークンの予測に使用されます。


例えば

```
来年はもっと
```
ってきたときに、LLMは `頑張る` というトークンを次に返し、その後に`。` というトークンを返したりして、文章を作ります。


これから説明していくLLMから出力の流れを以下に書きます。

```mermaid
graph TD
model[LLMモデル] -- "トークンたちのスコア<br>(logits)" --> Temperatureによる割り算 -- "割り算されたスコア" --> filter["TopK, TopPによるフィルタリング"] -- "フィルタリングされたスコア" --> softmax["ソフトマックス関数による確率分布の作成"] -- "トークンたちの確率分布" --> 確率でトークンを選ぶ --> out(("出力"))
```

## モデルが出力する スコア (`logits`)
モデルは`頑張る`を直接返すわけではなく、まず `logits` [^1] と言われる各トークンの**スコア**を出力します。

```mermaid
graph LR
  classDef class2 fill:#ffa23e
model[LLMモデル] -- "単語たちのスコア(logits)" --> hatena["？"]
```

例えば スコア(logits) はここでは`来年はもっと`に対して、 `頑張る` が一番スコアが高い状態になっています。

| トークン | スコア(logit) |
|---|---|
|　頑張る        | 2.0 |
|　成長する       | 1.8 |
|　学ぶ         | 1.5 |
|　健康         | 1.0 |
|　新しい        | 0.5 |

## スコア(logits)を確率分布に変換するソフトマックス関数

スコア(logits)だけだと、どれを出すか選ぶのが難しいのでソフトマックス関数 [^2] というのを利用します。

ソフトマックス関数自体を理解するのはちょっと難しいのですが、この関数の性質が結構都合が良いのです。ソフトマックス関数はうまくそれぞれのトークンに対する確率(確率分布)の合計が100%になるように計算することができます。
例えば、スコアがマイナスの場合※や、値の差が大きいほど他のトークンを指数関数的に選ばれにくくするなど考慮できます。

<details>
<summary>※指数関数がマイナスをうまく扱える理由</summary>
$2 ^ x$であったときに
$2 ^ 1$ は 2<br>  
$2 ^ 0$ は 1<br>  
で、$2^{-1}$は1/2になります。<br>このようにマイナスでも指数関数で使うと正の値になります。この特徴をソフトマックス関数により使っています。

</details>


ここでは `来年はもっと` の次に `頑張る` を`33.2`%の確率で出すという感じです。


```mermaid
graph LR
  classDef class2 fill:#ffa23e
model[LLMモデル] -- "単語たちのスコア(logits)" --> softmax["ソフトマックス関数"]:::class2 -- "単語たちの確率分布" --> hatena["？"]
```


| トークン | スコア(logit) | 確率(ソフトマックス関数で計算) | どのぐらいの割合かのイメージ |
|---|---|---|---|
|　頑張る        | 2.0 | 33.2%| ###### |
|　成長する       | 1.8 | 27.1%| ##### |
|　学ぶ         | 1.5 | 20.1%| #### |
|　健康         | 1.0 | 12.2%| ## |
|　新しい        | 0.5 | 7.4%| # |


<details>
<summary>
この計算の計算コードや試せるリンク
</summary>


Python計算コード
https://gist.github.com/takahirom/448d2c1e3abb0f76a9bbe4b2983052e7

Kotlinでの計算コード
https://gist.github.com/takahirom/6ec47fb00ab3fcc8289f281a3fc912cc

こちらのリンクではWeb上で、これから説明するTemperatureなどを変更したり、数式のコードなどを確認することができます。
https://pl.kotl.in/KBMhIIdWR
</details>

## 確率を元に単語が選ばれる

(上と同じ図)
| トークン | スコア(logit) | 確率(ソフトマックス関数で計算) | どのぐらいの割合かのイメージ |
|---|---|---|---|
|　頑張る        | 2.0 | 33.2%| ###### |
|　成長する       | 1.8 | 27.1%| ##### |
|　学ぶ         | 1.5 | 20.1%| #### |
|　健康         | 1.0 | 12.2%| ## |
|　新しい        | 0.5 | 7.4%| # |


この確率分布を元に単語が選ばれるという感じです。

`来年はもっと` から **`成長する`** 
など。(2番目が選ばれた場合)


```mermaid
graph LR
  classDef class2 fill:#ffa23e
model[LLMモデル] -- "トークンたちのスコア(logits)" --> softmax["ソフトマックス関数"] -- "トークンたちの確率分布" --> 確率でトークンを選ぶ:::class2 -- "トークン" --> out(("出力"))
```

## で、 `Temperature` , `TopK `, `topP`とかって？

### `Temperature`

まず `Temperature` を理解する前に知っておきたいのが、
ソフトマックス関数は指数関数 [^2] を使うので差が大きいほど確率に差をつけるという性質があります。

`Temperature` はこのLLMモデルの出力であるスコア(logits)に対して割り算します。
スコア(`logits`) / `Temperature` したものを ソフトマックス関数の入力として利用するということです。

$$
\text{softmax}\left(\frac{x_i}{T}\right)
$$

※ $ x_i $ は各トークンのロジット（モデルの出力スコア）で、${T}$ はTemperatureです。


例えば logitsの値が `2.0` で `Temperature`が `0.1` なら `2.0 / 0.1` で `20.0` になります。

`0.1`で割るとlogitsの値が10倍になるので、例えば、割り算した結果の `頑張る` と `新しい` で `20` と `5` でスコアにかなり差(20 - 5 = 15)が開きますよね？すると`頑張る`の確率が87%になります。そのようにスコアの差を調整できるのが `Temperature`です。


| トークン | スコア(logit) | temperatureで割り算したスコア<br>temperature = 0.1 | 確率(ソフトマックス関数で計算) | どのぐらいの割合かのイメージ |
|---|---|---|---|---|
|　頑張る        | 2.0 | **20.0** | **87.6%**| ################# |
|　成長する       | 1.8 | 18.0 | 11.8%| ## |
|　学ぶ         | 1.5 | 15.0 | 0.6%|  |
|　健康         | 1.0 | 10.0 | 0.0%|  |
|　新しい        | 0.5 | **5.0** | 0.0%|  |

`temperature = 2.0` だと、`頑張る`が`26%`のみになり、かなり均等に出るようになります。これは割り算した結果の差が`1.0` と `0.3` の差(`0.7`)しかないためです。これにより `Temperature` が高いとさまざまな トークンが出力されるようになるのです。

| トークン | スコア(logit) | temperatureで割り算したスコア<br>temperature = 2.0 | 確率(ソフトマックス関数で計算) | どのぐらいの割合かのイメージ |
|---|---|---|---|---|
|　頑張る        | 2.0 | **1.0** | **26.6%** | ##### |
|　成長する       | 1.8 | 0.9 | 24.0%| #### |
|　学ぶ         | 1.5 | 0.8 | 20.7%| #### |
|　健康         | 1.0 | 0.5 | 16.1%| ### |
|　新しい        | 0.5 | **0.3** | 12.6%| ## |

この Temperature による割り算 を図に入れると以下のようになります。

```mermaid
graph TD
  classDef class2 fill:#ffa23e
model[LLMモデル] -- "頑張る トークンのスコア(logits)<br>2.0 " --> Temperatureによる割り算:::class2 -- "割り算されたスコア<br>2.0 / 0.1 = 20.0" --> softmax["ソフトマックス関数"] -- "トークンの確率<br>87%" --> 確率でトークンを選ぶ --> out(("出力<br>頑張る"))
```


Temperatureの値が高いほど、logits(スコア)の差が小さくなることで、さまざまなものが出やすくなる代わりに、変なものがでてしまう確率が高まっていきます。

## `TopK`, `TopP` は？

多様なものを出したいけど、あまりにも例外的なものは出したくないことがよくあります。
そういったときに`Temperature` を計算した後にこのトークンをフィルタリングする処理を行います。

`TopK` ではスコアの上位K個のトークンだけをフィルタリングします。これにより極端に低い確率のトークンを排除し、生成されたテキストの品質を保持します。
例えば スコア(logits)の上位 3つのトークンだけを指定してそれだけ残し、ソフトマックス関数にかけることができます。

| トークン | スコア(logit) | 確率 | どのぐらいの割合かのイメージ |
|---|---|---|---|
|　頑張る        | **2.0** | 41.2%| ######## |
|　成長する       | **1.8** | 33.8%| ###### |
|　学ぶ         | **1.5** | 25.0%| ##### |
|　健康         | 1.0 | 0.0%|  |
|　新しい        | 0.5 | 0.0%|  |

また `TopP` では一度ソフトマックス関数にかけて、上から確率を足し合わせていき、その`TopP`の値に達するまでのトークンを考慮します。これにより動的にトークンの数を調整し、重要な選択肢を考慮しながら多様性を持たせます。
例えば50%を指定したら、50%消費されるまでの上位を選ぶ形になります。

| トークン | スコア(logit) | 確率変更前 | 確率変更後 | どのぐらいの割合かのイメージ |
|---|---|---|---|---|
|　頑張る        | 2.0 | **33.2%** | 55.0%| ########## |
|　成長する       | 1.8 |**27.1%**| 45.0%| ######### |
|　学ぶ         | 1.5 |20.1%| 0.0%|  |
|　健康         | 1.0 |12.2%| 0.0%|  |
|　新しい        | 0.5 |7.4%| 0.0%|  |



```mermaid
graph TD
  classDef class2 fill:#ffa23e
model[LLMモデル] -- "トークンたちのスコア<br>(logits)" --> Temperatureによる割り算 -- "割り算されたスコア" --> filter["TopK, TopPによるフィルタリング"]:::class2 -- "フィルタリングされたスコア" --> softmax["ソフトマックス関数による確率分布の作成"] -- "トークンたちの確率分布" --> 確率でトークンを選ぶ --> out(("出力"))
```


他にも `MinP` などが LLMの仕組みである `Transformer` に仕組みが追加されたりしているようですが、なんとなくここまで読んだ方は意味がわかったりするのではないでしょうか？

https://github.com/huggingface/transformers/issues/27670#issuecomment-2102680770

## このソフトマックス関数とか知ってなにか意味あるの？

ソフトマックス関数を知っておくと、Temperatureとは何かなどが分かるだけでなく、応用が効くようになります。例えば、有害コンテンツのフィルタリングのソフトウェアを作りたいときに、LLMにこのコンテンツが有害であるかどうかを聞いた後に、このソフトマックス関数を直接使って、次のLLMからの回答トークンが "Yes"になる確率と"No"になる確率だけを取得することができます。それにより、「"Yes"の確率が高いので有害である」など有害コンテンツのフィルタリングを実装するようなことができたりするわけです。

例: Evaluating content safety with ShieldGemma and Keras

https://colab.research.google.com/github/google/generative-ai-docs/blob/main/site/en/responsible/docs/safeguards/shieldgemma_on_keras.ipynb#scrollTo=9pexswNQcS_U

実際のソフトマックス関数を使った有害コンテンツのフィルタリング判定コード
コードの中にlogitsやsoftmaxが出てきてテンション上がりますよね？

```python
last_logits = keras.ops.take(logits, last_prompt_index, axis=1)[:, 0]
yes_logits = last_logits[:, self.yes_token_idx]
no_logits = last_logits[:, self.no_token_idx]
yes_no_logits = keras.ops.stack((yes_logits, no_logits), axis=1)
return keras.ops.softmax(yes_no_logits, axis=1)
```

## まとめ

ちょっと中のことを知ってみると、実際の動きを考えながらパラメーターを指定できるので効率的になり、LLMの発展的な使い方も見えてくるのではないでしょうか。わからないことは多いですが学んでいきたいです。


[^1]: このブログによると "logitsとは、「ソフトマックス活性化関数に通す前のニューラルネットワークの出力」" だそうです。 https://minus9d.hatenablog.com/entry/2020/10/25/193018#google_vignette


[^2]: ソフトマックス関数は以下の式で定義されます。(今回は分からなくても問題なし。)
$$
\text{softmax}(x_i) = \frac{e^{x_i}}{\sum_{j} e^{x_j}}
$$
ここで、$\( x_i \)$ は各トークンのロジット（モデルの出力スコア）です。この関数により、各トークンのスコアを確率分布に変換します。