複数のLLMのPerplexityの精度を比較して、文章の自然さを判定を試す

初めに

環境

  • L4 GPU
  • ubuntu22.04

準備

ライブラリをインストールします

pip install torch transformers huggingface_hub

比較対象のモデル

  • stabilityai/StableBeluga-7B
  • mistralai/Mistral-7B-Instruct-v0.2
  • Rakuten/RakutenAI-7B-chat

対象のデータ

今回の対象のデータは yodasのja000の一部を使用します。

   Text
1   それと僕が材料をお伝えした時にバニラエッセンスを入れたじゃないですか
2   1弦の5フレット、2弦の5フレット、 3弦の5フレット、2弦の7フレット、
3   けどもこれでえっと木スキル使うとさらに カウンターが1個貯まる
4   長い年月をかけて韓国人朝鮮人 と向き合ってきた中国人は韓国人
5   ごいハマり始めて
6   50話 いらっしゃいませ♪ ヘラのグランプリ!
7   オムニテクでは人々を助けを上がっている。
8   ステーキってもんはね
9   これが聞こえてきた
10  なるほどねそうなんだやばいすげ えわ
11  殺菌得ながらを
12  じゃあ呼吸法やりましょう、呼吸、呼吸
13  カンタ : そこやれるの見たいと思ってるよ、みんな
14  すげーなんか色んなものがこう そうなんだそうそうばすごいね
15  前回は standard assets を使って 遊んでいました
16  を獲得しました
17  しかしながら本当の理由はそれ だけではないのです
18  病むなんですが
19  全体混ぜると これね混ぜたら数分蒸らした方がいいです
20  アプリはそういったことを解決して くれるアプリです
21  ご丁寧に
22  下に
23  合わせてかぶせれば...
24  そういう言い方で
25  朝食
26  流行ったものだそうですが
27  こうなります
28  次は釣りレベルの上限解放です。今度は何を釣らされるんだろうと思っていましたが、タマカイでした。
29  守りたい
30  だってさっきの人从众…

複数のモデルでPerplexityの値を取得

以下のコードでそれぞれのLLMのPerplexityの値を取得する

import json
import csv
import time
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

def perplexity(model, tokenizer, text) -> torch.Tensor:
    tokenized_input = tokenizer.encode(
        text, add_special_tokens=False, return_tensors="pt"
    ).to(model.device)

    with torch.inference_mode():
        output = model(tokenized_input, labels=tokenized_input)
        ppl = torch.exp(output.loss)

    return ppl.item()

models = [
    "stabilityai/StableBeluga-7B",
    "mistralai/Mistral-7B-Instruct-v0.2",
    "Rakuten/RakutenAI-7B-chat"
]

results = {}
csv_data = [["Text"] + [f"{model}_Perplexity" for model in models]]

with open("testData.txt", "r", encoding="utf-8") as f:
    lines = [line.strip() for line in f.readlines()[:30]]

for line in lines:
    line_results = {"text": line}
    csv_line = [line]

    for model_name in models:
        tokenizer = AutoTokenizer.from_pretrained(model_name)
        model = AutoModelForCausalLM.from_pretrained(
            model_name, device_map="cuda", torch_dtype=torch.float16
        )

        ppl = perplexity(model, tokenizer, line)

        line_results[model_name] = {
            "perplexity": ppl
        }
        csv_line.append(ppl)

        del model
        torch.cuda.empty_cache()

    results[line] = line_results

with open("perplexity_results5.json", "w", encoding="utf-8") as f:
    json.dump(results, f, ensure_ascii=False, indent=4)

with open("perplexity_results5.csv", "w", newline="", encoding="utf-8") as f:
    csv_writer = csv.writer(f)
    csv_writer.writerow(csv_data[0])
    for line in lines:
        csv_writer.writerow([line] + [results[line][model]["perplexity"] for model in models])

結果

Text stabilityai/StableBeluga-7B_Perplexity mistralai/Mistral-7B-Instruct-v0.2_Perplexity Rakuten/RakutenAI-7B-chat_Perplexity
それと僕が材料をお伝えした時にバニラエッセンスを入れたじゃないですか 13.036222457885742 30.592239379882812 77.44200134277344
1弦の5フレット、2弦の5フレット、 3弦の5フレット、2弦の7フレット、 4.010903358459473 8.111798286437988 6.279117107391357
けどもこれでえっと木スキル使うとさらに カウンターが1個貯まる 43.25908279418945 83.80355072021484 470.63653564453125
長い年月をかけて韓国人朝鮮人 と向き合ってきた中国人は韓国人 9.886605262756348 30.079328536987305 87.20085144042969
ごいハマり始めて 293.4146423339844 4317.986328125 334.7664794921875
50話 いらっしゃいませ♪ ヘラのグランプリ! 15.717564582824707 58.66378402709961 111.03834533691406
オムニテクでは人々を助けを上がっている。 65.81471252441406 90.47702026367188 1869.9520263671875
ステーキってもんはね 127.35417938232422 1428.76318359375 1153.15966796875
これが聞こえてきた 14.765314102172852 872.0487670898438 120.8353500366211
なるほどねそうなんだやばいすげ えわ 117.2963638305664 360.88482666015625 478.8385009765625
殺菌得ながらを 121.11297607421875 1618.6380615234375 3937.654052734375
じゃあ呼吸法やりましょう、呼吸、呼吸 9.24917221069336 33.77603530883789 262.35357666015625
カンタ : そこやれるの見たいと思ってるよ、みんな 51.29374694824219 86.22332000732422 313.9569091796875
すげーなんか色んなものがこう そうなんだそうそうばすごいね 37.839759826660156 103.7863540649414 135.54092407226562
前回は standard assets を使って 遊んでいました 33.08415985107422 90.7002182006836 162.7081298828125
を獲得しました 10.945358276367188 405.0987548828125 819.6907958984375
しかしながら本当の理由はそれ だけではないのです 10.608744621276855 28.476654052734375 130.30300903320312
病むなんですが 57.94710922241211 5614.7802734375 7005.65576171875
全体混ぜると これね混ぜたら数分蒸らした方がいいです 21.468355178833008 77.3314437866211 306.8896179199219
アプリはそういったことを解決して くれるアプリです 11.473381042480469 35.310543060302734 87.94023132324219
ご丁寧に 64.8609619140625 768.6679077148438 146.9370880126953
下に 8333.6044921875 385289856.0 118921.09375
合わせてかぶせれば... 79.0224838256836 315.1685485839844 1221.1976318359375
そういう言い方で 27.01752281188965 314.7711486816406 442.60284423828125
朝食 21129.865234375 302320000.0 658.495849609375
流行ったものだそうですが 33.65590286254883 241.5250701904297 1139.429443359375
こうなります 68.17607116699219 33799.25390625 943.4025268554688
次は釣りレベルの上限解放です。今度は何を釣らされるんだろうと思っていましたが、タマカイでした。 14.19534683227539 21.08135986328125 23.427536010742188
守りたい 164.52532958984375 41213.8125 217.5680389404297
だってさっきの人从众… 254.49359130859375 1318.85205078125 2268.48974609375

cl-tohoku/bert-base-japaneseを使って文章の自然さを判定する

開発環境

ライブラリのインストール

pip install torch transformers
pip install fugashi ipadic

データの準備

以下のようなデータを準備します

それと僕が材料をお伝えした時にバニラエッセンスを入れたじゃないですか
1弦の5フレット、2弦の5フレット、 3弦の5フレット、2弦の7フレット、
けどもこれでえっと木スキル使うとさらに カウンターが1個貯まる
長い年月をかけて韓国人朝鮮人 と向き合ってきた中国人は韓国人
ごいハマり始めて
50話 いらっしゃいませ♪ ヘラのグランプリ!
オムニテクでは人々を助けを上がっている。

文章の自然さの判定

from transformers import BertForMaskedLM, BertJapaneseTokenizer
import torch
import csv
import json

model_name = "cl-tohoku/bert-base-japanese"
tokenizer = BertJapaneseTokenizer.from_pretrained(model_name)
model = BertForMaskedLM.from_pretrained(model_name)

def tokenize_with_mask(text, tokenizer):
    tokens = tokenizer.tokenize(text)
    masked_tokens = []
    for i in range(len(tokens)):
        if tokens[i] not in ['[CLS]', '[SEP]']:
            masked_tokens.append(tokens[:i] + ['[MASK]'] + tokens[i+1:])
    return masked_tokens

def calculate_score(masked_tokens, tokenizer, model):
    scores = []
    for tokens in masked_tokens:
        input_ids = tokenizer.convert_tokens_to_ids(tokens)
        tensor_input = torch.tensor([input_ids])
        with torch.no_grad():
            outputs = model(tensor_input, labels=tensor_input)
            loss = outputs[0]
            scores.append(loss.item())
    return sum(scores) / len(scores)

with open('testData.txt', 'r', encoding='utf-8') as file:
    texts = file.readlines()

results = []
for text in texts[:100]: 
    text = text.strip()
    masked_tokens = tokenize_with_mask(text, tokenizer)
    score = calculate_score(masked_tokens, tokenizer, model)
    results.append({'text': text, 'score': score})

# CSVファイルに保存
with open('results.csv', 'w', encoding='utf-8', newline='') as csv_file:
    fieldnames = ['text', 'score']
    writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
    writer.writeheader()
    for result in results:
        writer.writerow(result)

# JSONファイルに保存
with open('results.json', 'w', encoding='utf-8') as json_file:
    json.dump(results, json_file, ensure_ascii=False, indent=4)

結果は以下のようになります

文章の類似度にminineedleを使って類似度測定を行う

初めに

文章の類似度に minineedleを教えていただいたので触ってみます。ライブラリの内容を見る感じ タンパク質配列間などを記載があるので、生物系で使われているもの?なのかもしれません

github.com

(雑に書いたコードは)以下で試したコードを置いていますので、ご参考までに

github.com

開発環境

ライブラリのインストール

pip install minineedle
pip install miniseq

minineedleで使用できるアルゴリズムについて

minineedleでは以下の二つのアルゴリズムを使うことができます。

  • Needleman-Wunsch wiki
  • Smith-Waterman wiki

それぞれのアルゴリズムについて簡単な説明は以下です

NeedlemanWunschアルゴリズム: グローバルアラインメントを行うアルゴリズムです。 2つの配列の全体を比較し、最適なアラインメントを見つけます。 動的計画法を用いて、配列間の類似性を最大化するアラインメントを計算します。 ギャップを挿入することで、配列の長さを調整しながらアラインメントを行います。 配列の先頭から末尾までを比較するため、配列全体の類似性を評価するのに適しています。 SmithWatermanアルゴリズム: ローカルアラインメントを行うアルゴリズムです。 2つの配列の部分配列間の類似性を見つけるのに適しています。 配列内の最も類似した領域を特定します。 ギャップを挿入することで、部分配列間の類似性を最大化するアラインメントを計算します。 配列全体ではなく、部分的に類似した領域を見つけるのに適しています。

文章を判定するのには、Needleman-Wunsch の方が適してそうです

複数の文章で実行

以下のいくつかの文章で実際に試してみます

from minineedle import needle, smith, core

def perform_alignment(seq1, seq2):
    alignment = needle.NeedlemanWunsch(seq1, seq2)
    alignment.align()
    score = alignment.get_score()
    identity = alignment.get_identity()
    
    print("Alignment of SEQUENCE 1 and SEQUENCE 2:")
    print(alignment)
    print("Score:", score)
    print("Identity:", identity)
    print()

# Test case 1: Short English sentences
seq1 = "The cat sat on the mat"
seq2 = "The dog sat on the rug"
perform_alignment(seq1, seq2)

# Test case 2: Longer English sentences
seq1 = "I love to eat pizza and pasta for dinner"
seq2 = "I enjoy eating sushi and ramen for lunch"
perform_alignment(seq1, seq2)

# Test case 3: Short Japanese sentences (Hiragana)
seq1 = "わたしはりんごがすきです"
seq2 = "わたしはバナナがすきです"
perform_alignment(seq1, seq2)

# Test case 4: Longer Japanese sentences (Hiragana and Kanji)
seq1 = "今日はいい天気です。散歩に行きましょう"
seq2 = "明日は雨が降るかもしれません。傘を持って行きましょう"
perform_alignment(seq1, seq2)

# Test case 5: Mixed English and Japanese sentences (Hiragana, Katakana, and Kanji)
seq1 = "I love to eat 寿司 and 天ぷら"
seq2 = "私は寿司とテンプラが大好きです"
perform_alignment(seq1, seq2)

# Test case 6: Japanese tongue twister (Hiragana and Katakana)
seq1 = "あめがあめあめあめがふるあめふるふるかあめてふるあめ"
seq2 = "アメガアメアメアメガフルアメフルフルカアメテフルアメ"
perform_alignment(seq1, seq2)

# Test case 7: Japanese proverb (Hiragana and Kanji)
seq1 = "猿も木から落ちる"
seq2 = "さるもきからおちる"
perform_alignment(seq1, seq2)

結果が以下のようになります

Alignment of SEQUENCE 1 and SEQUENCE 2:
Alignment of SEQUENCE 1 and SEQUENCE 2:
        The cat sat on the mat
        The dog sat on the rug

Score: 10
Identity: 72.73

Alignment of SEQUENCE 1 and SEQUENCE 2:
Alignment of SEQUENCE 1 and SEQUENCE 2:
        I love to- eat--- pizza and pasta for dinner
        I ---enjoy eating sushi and ramen for -lunch

Score: -2
Identity: 47.73

Alignment of SEQUENCE 1 and SEQUENCE 2:
Alignment of SEQUENCE 1 and SEQUENCE 2:
        わたしはりんごがすきです
        わたしはバナナがすきです

Score: 6
Identity: 75.0

Alignment of SEQUENCE 1 and SEQUENCE 2:
Alignment of SEQUENCE 1 and SEQUENCE 2:
        今日は-----いい天気です。--散歩に行きましょう
        明日は雨が降るかもしれません。傘を持って行きましょう

Score: -8
Identity: 34.62

Alignment of SEQUENCE 1 and SEQUENCE 2:
Alignment of SEQUENCE 1 and SEQUENCE 2:
        I love to eat 寿司--- and 天ぷら
        ------------私は寿司とテンプラが大好きです

Score: -23
Identity: 7.41

Alignment of SEQUENCE 1 and SEQUENCE 2:
Alignment of SEQUENCE 1 and SEQUENCE 2:
        あめがあめあめあめがふるあめふるふるかあめてふるあめ
        アメガアメアメアメガフルアメフルフルカアメテフルアメ

Score: -26
Identity: 0.0

Alignment of SEQUENCE 1 and SEQUENCE 2:
Alignment of SEQUENCE 1 and SEQUENCE 2:
        -猿も木から落ちる
        さるもきからおちる

Score: 1
Identity: 55.56

espnet/yodasの音声データをmp3に変換してローカルに保存する

開発環境

  • Ubunts 22.02

ライブラリのインストール

pip install pydub
sudo apt-get install ffmpeg
pip install librosa soundfile

mp3に変換・保存

以下で.arrowで保存されているデータをmp3に変換して、保存します

import os
from datasets import load_dataset
from pydub import AudioSegment
import numpy as np

# データセットのロード
ds = load_dataset('espnet/yodas', 'ja000', trust_remote_code=True)

# 保存先のフォルダを指定
output_folder = "audio"

# 保存先のフォルダが存在しない場合は作成
os.makedirs(output_folder, exist_ok=True)

# データセットの音声データをmp3に変換して保存
for i, audio_data in enumerate(ds['train']):
    # 音声データを取得
    audio = audio_data['audio']
    
    # 音声データをバイト列に変換
    audio_bytes = audio['array'].tobytes()
    
    # AudioSegmentを使用して音声データを作成
    audio_segment = AudioSegment(
        data=audio_bytes,
        sample_width=2,  # 16-bit audio
        frame_rate=audio['sampling_rate'],
        channels=1  # Mono audio
    )
    
    # ファイル名を生成
    filename = f"{audio_data['utt_id']}.mp3"
    
    # 音声データをmp3形式で保存
    audio_segment.export(os.path.join(output_folder, filename), format="mp3")
    
    print("convert audio to mp3:",filename)

print("音声データの変換と保存が完了しました。")

Rustの形態素解析ライブラリのlinderaを動かす

初めに

Rustで動く辞書サイズが小さくなった形態素解析ライブラリが出ているみたいなので触ってみます

デモ

実行すると以下のような結果が返っています

開発環境

  • ubuntu22.04

環境構築

まずは Rustをインストールします

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

環境変数に追加します

source $HOME/.cargo/env

プロジェクトの作成

以下でプロジェクトを作成します

mkdir lindera-test
cd lindera-test

Cargo.toml ファイルの作成をします

[package]
name = "lindera-test"
version = "0.1.0"
edition = "2021"

[dependencies]
lindera-core = "0.24.0"
lindera-dictionary = "0.24.0"
lindera-tokenizer = { version = "0.24.0", features = ["ipadic"] }

以下のように src/main.rs でサンプルコードの作成をします

use lindera_core::{mode::Mode, LinderaResult};
use lindera_dictionary::{DictionaryConfig, DictionaryKind};
use lindera_tokenizer::tokenizer::{Tokenizer, TokenizerConfig};

fn main() -> LinderaResult<()> {
    let dictionary = DictionaryConfig {
        kind: Some(DictionaryKind::IPADIC),
        path: None,
    };

    let config = TokenizerConfig {
        dictionary,
        user_dictionary: None,
        mode: Mode::Normal,
    };

    // Tokenizerの作成
    let tokenizer = Tokenizer::from_config(config)?;

    // テキストのトークン化
    let tokens = tokenizer.tokenize("関西国際空港限定トートバッグ")?;

    // トークンの出力
    for token in tokens {
        println!("{}", token.text);
    }

    Ok(())
}

ビルドと実行

以下でビルドをします

cargo run --features=ipadic

また以下で実行できます

cargo run --features=ipadic

以下のように結果が出力されます

関西国際空港
限定
トートバッグ

ChromeDBとBeluuuuuuga/Japanese-Instruction-Linux-Command-169を使って質問に対する適切なLinuxコマンドを探すRAGを構築する

初めに

珍しい?データセットがあったので、こちらを使ってLinuxで操作したいことがあるけどコマンドがわからない場合に雑に質問ができるRAGシステムを構築します

huggingface.co

実行した環境は以下でColobのNoteを公開しています

colab.research.google.com

デモ

開発環境

Chromeとは

Chromaはオープンソースかつローカルで使える埋め込みデータベースです。

docs.trychroma.com

RAGシステムの構築

ライブラリの準備

まずは以下でライブラリを入れます

%pip install -Uq chromadb numpy datasets

DBの構築とデータの追加

DBを作成します

# Import Chroma and instantiate a client. The default Chroma client is ephemeral, meaning it will not save to disk.
import chromadb

client = chromadb.Client()

# Create a new Chroma collection to store the supporting evidence. We don't need to specify an embedding fuction, and the default will be used.
collection = client.create_collection("Japanese-Instruction-Linux-Command")

huggingfaceからデータを取得して追加します

# Get the SciQ dataset from HuggingFace
from datasets import load_dataset

dataset = load_dataset("Beluuuuuuga/Japanese-Instruction-Linux-Command-169", split="train")


print("Number of questions with support: ", len(dataset))

# Embed and store the first 100 supports for this demo
i = 0
for data in dataset:
  collection.add(
      ids=[str(i)],  # IDs are just strings
      documents=data['instruction'],
      metadatas=[{"output": data['output']}],
  )
  i = i + 1

質問をして回答を取得

以下で質問に対して近い回答を取得します

results = collection.query(query_texts=dataset["instruction"][0], n_results=3)
print("質問:", dataset["instruction"][0])

# distanceのしきい値を設定
threshold = 0.05

# 検索結果をdistanceでフィルタリング
filtered_results = [result for result in zip(results['distances'][0], results['metadatas'][0]) if result[0] <= threshold]

if filtered_results:
    print("回答:")
    for distance, metadata in filtered_results:
        print(f"  distance: {distance:.4f}, output: {metadata['output']}")
else:
    print("適切な回答が見つかりませんでした。")

この場合の回答は以下になります

質問: Linuxで現在のディレクトリの内容を表示するコマンドを教えてください。
回答:
  distance: 0.0000, output: ls
  distance: 0.0360, output: pwd

ここで全く関係のないことを聞いてみます

q = "今日はいい天気ですね!"
results = collection.query(query_texts=q, n_results=3)
print("質問:", q)

# distanceのしきい値を設定
threshold = 0.05

# 検索結果をdistanceでフィルタリング
filtered_results = [result for result in zip(results['distances'][0], results['metadatas'][0]) if result[0] <= threshold]

if filtered_results:
    print("回答:")
    for distance, metadata in filtered_results:
        print(f"  distance: {distance:.4f}, output: {metadata['output']}")
else:
    print("適切な回答が見つかりませんでした。")

この場合は、見つからないので以下のようになります

質問: 今日はいい天気ですね!
適切な回答が見つかりませんでした。

マルチGPUででLLMの学習時をする際の「NCCL communicator and retrieving ncclUniqueId」のエラーの対応

開発環境

nvidia-smi

+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 550.54.14              Driver Version: 550.54.14      CUDA Version: 12.4     |
|-----------------------------------------+------------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
|                                         |                        |               MIG M. |
|=========================================+========================+======================|
|   0  NVIDIA RTX A5000               Off |   00000000:1C:00.0 Off |                  Off |
| 54%   79C    P2            215W /  230W |   17200MiB /  24564MiB |     94%      Default |
|                                         |                        |                  N/A |

nvcc

nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2023 NVIDIA Corporation
Built on Mon_Apr__3_17:16:06_PDT_2023
Cuda compilation tools, release 12.1, V12.1.105
Build cuda_12.1.r12.1/compiler.32688072_0

詳細

マルチGPUで学習する際に、以下のエラーが出ていました。

RuntimeError: [6] is setting up NCCL communicator and retrieving ncclUniqueId from [0] via c10d key-value store by key '0', but store->get('0') got error: Socket Timeout

この際に、NCCL_IB_DISABLE=1環境変数で設定します

export NCCL_IB_DISABLE=1