LangCheckで探る：LLMの信頼性をどう評価するか

# はじめに
みなさん、こんにちはM&Aクラウドの尾村です。弊社でも積極的にLLMを使っていこうといくつかPJが進んでいるのですが、その中で気になるのがLLMが出力したものが本当に使えるかどうかの確認です。残念ながら、LLMの出力に関して注意や条件をどれだけプロンプトに命じていたとしても、必ず守ってくれるわけではありません。そのようなLLMの問題を和らげるために信頼性評価のツールが開発されています。今回はそんなLLM評価ツールの中でも、日本のスタートアップ[citadel AI](https://www.citadel.co.jp/)社が公開しているOSSツール[LangCheck](https://github.com/citadel-ai/langcheck)を試してみました。

# LangCheckとは
LangCheckの特徴は、公式によれば、シンプルでpythonらしい記述が可能です。また、生成されたテキストを評価するための仕組みなので、どのようなLLMを利用していても問題なく利用できます。日本のスタートアップが開発しているため、英語と日本語に対応しているのも嬉しい点ですね。主にできることとしては、以下の６つが紹介されています。この記事では、日本語でできることをそれぞれ見ていきます。公式のReadMeの内容に沿いますが、一定、尾村のアレンジが加えられています。環境としては、今回も簡単に準備が可能なgoogle colaboratoryを使いました。
1. テキストの評価
1. 評価値の可視化
1. データの拡張
1. ユニットテスト
1. モニタリング
1. ガードレール

## テキストの評価
LangCheckのコア機能となる生成テキストの評価機能です。評価に必要なものによってパターン分けされています。今回は日本語で使える機能を一覧化しました。[ドキュメント](https://langcheck.readthedocs.io/en/latest/installation.html)を見た限りでは、英語の方がメトリクスは多そうでした。

|評価のパターン|評価値|説明|
|---|---|---|
|リファレンス不要なテキスト品質|流暢さ、感情分類、有害性、リーダビリティ（文章の読みやすさ）|文章単体で評価するメトリクス|
|リファレンスを基準としたテキスト品質|ROUGE1、ROUGE2、ROUGE-L、意味的類似度|アウトプットの基準となるものを利用して評価するメトリクス|
|ソースに基づくテキスト品質|事実の一貫性|ソースから読み取れることを言っているか（ハルシネーションを起こしていないか）を評価するメトリクス|
|テキスト構造のメトリクス|全部の文字列を含むか、一部の文字列を含みか、整数か、json型か|生成されたテキストがフォーマットや指定に沿っているかどうか評価するメトリクス|

テキストの生成は、私が自作、chatGPTを利用して生成します。機械的に出せない指標（流暢さや意味的類似度）については、LLMを利用するのですが、モデルタイプとして、"local"を利用します。これはあらかじめ決められたOSSのモデルをローカルにダウンロードして動かします。

必要なライブラリのインストール
```python
pip install llmx
pip install langcheck
```

### 流暢さ
評価する文は私が書きました。0~1の間で評価してくれます。また、実行すると一回目だけ、モデル(liwii/fluency-score-classification-ja)のダウンロードが実行されます。複数のテキストを一斉に評価して、表の形式で出力してくれるのは気が効いていていいですね。「私は雑誌の記事を書いた。」や「私はqiitaの記事を書いた。」といった日本語の文法としては間違っていない文章でも、評価値としては、0.5超えていないので、どのぐらいの評価なら問題ないかは自分のデータセットで試してみる必要がありそうです。
```python
generated_outputs = [
    '私は、生成AIの最新の動向に関する雑誌の記事を先週末にかけて書いた。',
    '私は雑誌の記事を書いた。',
    '私はqiitaの記事を書いた。',
    '私かいた。記事。qiita',
    '書いたよ。私はqiitaの記事を２つほど。',
    '私はqiitaの記事である。'
]
langcheck.metrics.ja.fluency(generated_outputs)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3091446/afdb6c8f-4d9b-ad3e-84ab-fd13e923756d.png)

### 感情分類
こちらは、ポジティブかネガティブかの２値分類（0:ネガティブ~0.5:中立~1:ポジティブ）です。localのモデルは、HuggingFaceのTwitter-roBERTa-base-sentiment-multilingual modelを使用しています。このぐらいの文章なら、問題なくポジネガ判定してくれています。「私は本である。」と言った謎の文章でも中立ぐらいのスコアを出していますね。[WRIME](https://github.com/ids-cv/wrime)データセットでされているような感情の種類などもできるようになるといいなと思います。
```python
generated_outputs = [
    '私は、この本が大好きだ。',
    '私はこの本が大嫌いだ。',
    '私は本である。'
]
langcheck.metrics.ja.sentiment(generated_outputs)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3091446/215b3ea1-545d-e5a7-b9bc-1692a78baa9a.png)

### 有害性
toxicityは、特に他者に対する攻撃性を評価しているように見えます。飲酒運転や窃盗を仄めかしても0付近の数値だったので、○カッター対策は他でやらないといけませんね。
ローカルのモデルは、HuggingFaceのAlnusjaponica/toxicity-score-multi-classificationをダウンロードします。
```python
generated_outputs = [
    '彼はバカでアホで、役立たずな人間である。',
    '飲酒運転、ウェーイ！！！',
    '窃盗したいな',
    '私はこの本が大嫌いだ。',
    '法律を犯すのは良くありません。'
]
langcheck.metrics.ja.toxicity(generated_outputs)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3091446/966059b2-eaf1-8397-61a2-067466c5e245.png)

### リーダビリティ
あまり親しみがない概念ですが、日本語の読みやすい文章かどかを機械的に判別できるような評価式が[こちらの論文](https://aclanthology.org/C88-2135.pdf)で提案されています。式上はマイナス無限大から無限大までの値をとる式です。数値が大きいほど読みやすい文章という判断ができます。走れメロスの一節とそれを元にchatgptのgpt-4に変換させた文章を比較させました。文学としての味わいは消えていますが、多少スコアとしては改善されています。

```python
generated_outputs = [
    "メロスは激怒した。必ず、かの邪智暴虐じゃちぼうぎゃくの王を除かなければならぬと決意した。メロスには政治がわからぬ。メロスは、村の牧人である。笛を吹き、羊と遊んで暮して来た。けれども邪悪に対しては、人一倍に敏感であった。きょう未明メロスは村を出発し、野を越え山越え、十里はなれた此このシラクスの市にやって来た。メロスには父も、母も無い。女房も無い。十六の、内気な妹と二人暮しだ。この妹は、村の或る律気な一牧人を、近々、花婿はなむことして迎える事になっていた。結婚式も間近かなのである。",
    "メロスがとても怒っています。彼は悪い王を倒すことを心に決めています。メロスは村の羊飼いで、普段は笛を吹いたり、羊と遊んだりしています。彼は政治のことはよくわかりませんが、悪いことには敏感です。この日、メロスは村を出て、遠く離れた市に行きました。彼には家族がいなくて、内気な16歳の妹と二人で暮らしています。この妹はもうすぐ村の人と結婚する予定です。"
]
langcheck.metrics.ja.tateishi_ono_yamada_reading_ease(generated_outputs)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3091446/82441dbe-d494-6c6b-d4a8-6e2bbb8bd6d3.png)

### ROUGE-1, ROUGE-2, ROUGE-L
ROUGEは正解となる文章（リファレンス）と生成した文章の一致度を測る指標です。詳しくは調べてみてね。今回は、ROUGE-Lを試しました。データセットは、[livedoorのデータセット](https://github.com/KodairaTomonori/ThreeLineSummaryDataset)を参考に手動で、作成しました。こちらの[記事](https://news.livedoor.com/article/detail/11023865/)と[トピック](https://news.livedoor.com/topics/detail/11023865/)です。著作権保護の観点から、結果の出力等は行いませんが、ROUGE-Lの値は、0.19と小さくなりました。トピックと要約はまた、少し意味合いが違うのでうまく同じようなものを出力できませんでした。
```python
generated_outputs = [
    "YYY"
]
referenced_outputs = [
    "XXX"
]
langcheck.metrics.ja.rougeL(generated_outputs, referenced_outputs)
```


### 意味的類似性
ROUGE-Lが機械的な文章一致度を測ったのに対し、こちらの指標は意味的な一致度を測る指標です。定義的には、-1から1までの値を取りますが、実際には0から１までになります（あえて、反対の文を意図的に生み出さないとマイナスにはならないと理解した）。ローカルのモデルは、HuggingFaceのparaphrase-multilingual-mpnet-base-v2を利用しています。こちらもROUGE-Lを試したのと同じデータに対して行ってみたところ、0.63という値になりました。意味的には一定似ていると判断しても良さそうですね。

```python
langcheck.metrics.ja.semantic_similarity(generated_outputs, referenced_outputs)
```

### 事実の一貫性
文章に書かれていることを生成できているかの指標です。注意点としては、generated_textsにあるテキストの数だけ、sourceのテキストも必要です。また、ローカルのメソッドは英語版（unieval-factモデル）のメソッドを、日本語-英語翻訳のモデルでラップして作っているようです。私の環境、transformers=4.22.1, tensorflow=2.14.0, langcheck=0.0.5(langcheck.__version__が定義されていなかったので、おそらく）だと、エラーになったので試せませんでした。モジュール内部のエラー（それも、おそらくtransformersのエラーだが、versionの縛りがあり難しい）にぶち当たり、この記事執筆段階での解決は諦めました。

```python
generated_texts = [
    'メロスがローマで剣闘士をしている。',
    'メロスはシラクで石工をしている。'
]
source =  [
    'メロスは激怒した。必ず、かの邪智暴虐じゃちぼうぎゃくの王を除かなければならぬと決意した。メロスには政治がわからぬ。メロスは、村の牧人である。笛を吹き、羊と遊んで暮して来た。けれども邪悪に対しては、人一倍に敏感であった。きょう未明メロスは村を出発し、野を越え山越え、十里はなれた此このシラクスの市にやって来た。メロスには父も、母も無い。女房も無い。十六の、内気な妹と二人暮しだ。この妹は、村の或る律気な一牧人を、近々、花婿はなむことして迎える事になっていた。結婚式も間近かなのである。メロスは、それゆえ、花嫁の衣裳やら祝宴の御馳走やらを買いに、はるばる市にやって来たのだ。先ず、その品々を買い集め、それから都の大路をぶらぶら歩いた。メロスには竹馬の友があった。セリヌンティウスである。今は此のシラクスの市で、石工をしている。その友を、これから訪ねてみるつもりなのだ。久しく逢わなかったのだから、訪ねて行くのが楽しみである。',
    'メロスは激怒した。必ず、かの邪智暴虐じゃちぼうぎゃくの王を除かなければならぬと決意した。メロスには政治がわからぬ。メロスは、村の牧人である。笛を吹き、羊と遊んで暮して来た。けれども邪悪に対しては、人一倍に敏感であった。きょう未明メロスは村を出発し、野を越え山越え、十里はなれた此このシラクスの市にやって来た。メロスには父も、母も無い。女房も無い。十六の、内気な妹と二人暮しだ。この妹は、村の或る律気な一牧人を、近々、花婿はなむことして迎える事になっていた。結婚式も間近かなのである。メロスは、それゆえ、花嫁の衣裳やら祝宴の御馳走やらを買いに、はるばる市にやって来たのだ。先ず、その品々を買い集め、それから都の大路をぶらぶら歩いた。メロスには竹馬の友があった。セリヌンティウスである。今は此のシラクスの市で、石工をしている。その友を、これから訪ねてみるつもりなのだ。久しく逢わなかったのだから、訪ねて行くのが楽しみである。'
]
```
```
RuntimeError: Failed to import transformers.models.marian.modeling_tf_marian because of the following error (look up to see its traceback):
No module named 'keras.saving.hdf5_format'
```

### テキスト構造のメトリクス
単純にis_intを試しました。そのほかにも、contains_all_strings、contains_any_strings、contains_regex、matches_regexといった機械的な文字列検索、正規表現による検索機能、is_floatやis_json_array、is_json_objectといった型判別機能など、わざわざlangcheckを使うほどではないのですが、システムに組み込む際はあるとありがたい機能です。また、validation_fnという自作の評価関数が定義できるのはgoodですね。
```
generated_tests = [
    "1",
    "a"
]
langcheck.metrics.is_int(generated_tests)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3091446/3e737a22-e3c7-9956-8689-a3fc2847faa5.png)

## 評価値の可視化
評価値をインタラクティブなUIで表示できる機能です。デフォルトでfilterされていない状態（そのため、　何かしらの文字列を打たないと表示されない状態）でないのは少し難ありですが、評価しやすくて良いですね。

```python
generated_outputs = [
    '私は、生成AIの最新の動向に関する雑誌の記事を先週末にかけて書いた。',
    '私は雑誌の記事を書いた。',
    '私はqiitaの記事を書いた。',
    '私かいた。記事。qiita',
    '書いたよ。私はqiitaの記事を２つほど。',
    '私はqiitaの記事である。'
]
fluency_values = langcheck.metrics.ja.fluency(generated_outputs)
sentiment_values = langcheck.metrics.ja.sentiment(generated_outputs)

fluency_values.scatter()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3091446/50b03f62-5f93-91c0-cb56-e526937023c9.png)

２値での散布図も可能です。
```python
langcheck.plot.scatter(fluency_values, sentiment_values)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3091446/4655ee87-b188-536f-9437-d901f02a1c86.png)


## データの拡張
プロンプトのパターンを増やしてくれるData Augmentationの機能です。適当な英文をchatgptに生成させました。ver0.0.5現在、英語の身の機能のようです。ドキュメントにあるchange_caseメソッドがなかったり、ReadMeにあるgenderがドキュメントに記載がなかったり、まだまだ未整備なようです。data augmentationはlangcheckのメインの機能ではないような気もするので仕方ないかもしれません。

```python
prompts =[
      "Under a vibrant evening sky, a small, cheerful bird flutters around a blossoming cherry tree, its delicate pink petals dancing in the gentle breeze. Nearby, a serene river reflects the warm colors of the sunset, creating a picture of perfect tranquility."
  ]
langcheck.augment.synonym(prompts)

```

アウトプットは以下です。単語レベルでの類義語への置き換えなので、デフォルトの設定のままだと、ほぼ原文のままのような印象も受けます。一応、smallがdiminished(減少した)、serene(穏やかな）がtranquil(静かな)に変化しているので、うまく動いているようです。ただし、代名詞のitsをinformation technologiesに変換したので、かなり機械的に変換しています。
```
['Under a vibrant eve sky, a diminished, cheerful bird flutters around a blossoming cherry tree tree, its delicate pink flower petal dancing in the gentle breeze. Nearby, a tranquil river reflects the warm colors of the sunset, creating a picture of perfect tranquility.']
```

## ユニットテスト
assert [metricsの評価式]でtestになるので、悩まなくて良いですね。test自体はpytest等を使ってやることになると思います。しっかり、アサーションエラーになりました。
```python
def test_toxicity(generated_outputs):
    assert langcheck.metrics.ja.toxicity(generated_outputs) < 0.1
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3091446/210353aa-06de-3c94-cdb5-917c3f920d87.png)

## モニタリング、ガードレール
こちらは機能というよりは、このようなこともできるという紹介です。生成したテキストを保存しておいて、langcheckで評価をモニタリングしたり、問題あるテキストを弾いたりすることができます。

# 最後に
今回は、日本のスタートアップCitadel AI製のOSS、LangCheckを試してみました。まだまだ、荒削りな部分がある印象ですが、これからの技術開発にLLMは必要不可欠なものになってきており、その信頼性を評価するのも非常に重要なので確実に求められるツールになっていくと思います。他のツール等も調べつつ、LangCheckを応援していければと思っています。