Azure AI Studio Prompt flow の評価フローの F1 メトリクスを日本語化する

[Azure AI Studio](https://ai.azure.com/) には LLM アプリケーションの評価に使える様々な組み込み評価機能が搭載されており、作成中のチャットアシスタントのフローや外部アシスタントからの出力結果をインポートして評価することができます。

プロンプトフローギャラリーで選択できる評価フローには 8 種類ありますが、その中に **QnA F1 Score Evaluation** というようなメトリクスがあります。F1 スコアは機械学習で用いられるメトリクスで、計算には Precision と Recall の調和平均を計算しますが、このフローでは入力値 `answer` と `ground_truth` 間のトークンの重複数をカウントして一致率を評価するために使用されます。

![スクリーンショット 2024-07-26 14.36.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/a5ccf429-8dcf-805c-c8e4-12c6669ecd13.png)

現在は LLM アプリケーションの評価は複数のメトリクスを組み合わせて評価されているかと思いますが、そのうち以下の一番左に対応する評価方法となります。**LLM を使用しない堅牢な評価方法**ですが、言い換え、順序、意図の類似性などは考慮されません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/675eb748-d564-25e6-07b1-735f2a7c2755.png)

https://qiita.com/nohanaga/items/b68bf5a65142c5af7969


# デフォルトは英語用
フロー内でテキストをトークンに分割する処理を見ると、、、まぁ英語圏用ですよね。空白で分けるのみのやつです。これでは日本語には使用できません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/a76962b2-f344-20ef-95dd-a9ad39575f62.png)

これを日本語用に置き換えちゃいましょう。

# 形態素解析器 Sudachi を使用してトークン分割＋正規化
[Sudachi](https://worksapplications.github.io/Sudachi/) は [AI Challenge Day](https://qiita.com/t-yamamura/items/9d8f7dd4ad584797032a) でワークスアプリケーションズさんに教えてもらいました。なんと自社のワークス徳島人工知能NLP研究所で開発している形態素解析器！とても軽量でいいですね。

```python
from sudachipy import tokenizer
from sudachipy import dictionary

# Sudachiを使用して日本語テキストを形態素解析する関数
def tokenize(text):
    tokenizer_obj = dictionary.Dictionary().create()
    mode = tokenizer.Tokenizer.SplitMode.C
    tokens = []
    for m in tokenizer_obj.tokenize(text, mode):
        if m.part_of_speech()[0] in ["名詞", "代名詞", "動詞"]:
            tokens.append(m.normalized_form())
    return tokens
```

こんな感じで、分かち書きして必要な品詞だけ抽出します。

```python
tokenize("吾輩は猫である。名前はまだない。")

['我が輩', '猫', '有る', '名前']
```

## `normalized_form` ええやん
`normalized_form` を出力することで、自動的にトークンを正規化して比較することができます！これは便利。

```python
tokenize("吾輩はねこで在る。なまえはまだない。")

['我が輩', '猫', '有る', '名前']
```

# ライブラリの導入
[Sudachi](https://worksapplications.github.io/Sudachi/) の実行に必要な [SudachiPy](https://pypi.org/project/SudachiPy/) ライブラリをプロンプトフローのコンピューティングセッションにインストールするには、「ファイル」ペインの `requirements.txt` に追加したいライブラリを書き込んで保存すれば導入されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/1ca246f1-47bf-9129-a44b-696fd733941a.png)

`requirements.txt`

```txt
sudachipy 
sudachidict_core
```

# 自動評価（バッチ）の実行
今回は事前に用意されたテスト用 CSV を準備して評価します。

## プロンプトフロー上からカスタム評価を選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/60156a2c-1417-62e0-6326-783bf3b3509a.png)

## 評価用 CSV のアップロード
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/ba5cfed8-be30-fefb-4570-2bd8d2846024.png)

## 先ほど作成したカスタム評価フローを選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/394a113c-eaa1-a470-7096-932dafbaec6f.png)

## F1 スコアの比較
データを変えて複数回評価を実行した場合、このように精度を可視化しながら比較できます。なるほど、Aggregate フローの `log_metric` の出力値は一番上のところに各レコードの平均値として表示してくれていることがわかりますね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/3f52540d-77ed-bae4-4ab1-6ce1d9309381.png)

とても簡単ですね。
生成 AI アプリケーションの評価で独自の評価メトリクスを使ってる方、プロンプトフローで良いカスタム評価ライフを。

# 参考
## 全コード
QnA F1 Score Evaluation メトリクスをクローンして、`f1_score.py` ノードを以下で置き換えます。元々のやつを拡張しただけです。

```python
from promptflow import tool
from collections import Counter
import string
import re
from sudachipy import tokenizer
from sudachipy import dictionary

@tool
def compute_f1_score(ground_truth: str, answer: str) -> float:
    
    # Sudachiを使用して日本語テキストを形態素解析する関数
    def tokenize(text):
        tokenizer_obj = dictionary.Dictionary().create()
        mode = tokenizer.Tokenizer.SplitMode.C
        tokens = []
        for m in tokenizer_obj.tokenize(text, mode):
            if m.part_of_speech()[0] in ["名詞", "代名詞", "動詞"]:
                tokens.append(m.normalized_form())
        return tokens
    
    def normalize_text(text) -> str:
        """Lower text and remove punctuation, articles and extra whitespace."""

        def remove_articles(text):
            return re.sub(r"\b(a|an|the)\b", " ", text)

        def white_space_fix(text):
            return " ".join(text.split())

        def remove_punc(text):
            exclude = set(string.punctuation)
            return "".join(ch for ch in text if ch not in exclude)

        def lower(text):
            return text.lower()
        
        return white_space_fix(remove_articles(remove_punc(lower(text))))

    # 正規化された予測トークンと参照トークンを取得
    prediction_tokens = tokenize(normalize_text(answer))
    reference_tokens = tokenize(normalize_text(ground_truth))

    # 集合を使用して共通トークンをカウント
    prediction_set = set(prediction_tokens)
    reference_set = set(reference_tokens)

    common_tokens = prediction_set & reference_set
    num_common_tokens = len(common_tokens)

    # F1スコアを計算
    if num_common_tokens == 0:
        f1 = 0
    else:
        # 精度を計算
        precision = num_common_tokens / len(prediction_tokens)
        # 再現率を計算
        recall = num_common_tokens / len(reference_tokens)
        # F1スコアを計算
        f1 = (2 * precision * recall) / (precision + recall)

    return f1

```

## Sudachi
https://worksapplications.github.io/Sudachi/