Macで形態素解析ライブラリ「Vibrato」を動かす

初めに

より速い形態素解析ライブラリを探していて,Mecab(および高速化)や jaggerよりも速いと言われている vibratoを触ってみます。

Demo

本とカレーの街神保町へようこそ。形態素解析した場合,以下のようになります 。

プロジェクトは以下で公開しています

GitHub - ayutaz/hello-Vibrato-rust: VibratoのRust版を動かすテスト

開発環境

Rust ver

$ rustc --version
cargo --version
rustc 1.80.0 (051478957 2024-07-21)
cargo 1.80.0 (376290515 2024-07-16)

MacにRustが入っていない場合は,以下の記事で導入する方法を記載しているため参考にしてください

ayousanz.hatenadiary.jp

Vibrato専用の辞書ファイルをダウンロード

まずはVibrato専用の辞書ファイルをダウンロードします。今回は一番サイズが小さいipadic-mecab-2_7_0を使用します。

ダウンロードしたら,以下で解凍をします。

tar xvf ipadic-mecab-2_7_0.tar.xz

Rustのプロジェクトの作成

次にRustのプロジェクトの作成をします

cargo new hello-rust

解凍した辞書もこのフォルダ内に移動します.

このプロジェクトの Cargo.toml は以下のように記載します。

[package]
name = "hello-rust"
version = "0.1.0"
edition = "2021"

[dependencies]
vibrato = "0.5.0"
zstd = "0.12.3"

Vibratoを動かす

Vibratoを動かしていきます。

main.rsを以下のように記載します。

use std::fs::File;
use std::env;
use std::io::{self, Read};
use vibrato::{Dictionary, Tokenizer};

pub fn mecab(dict_path: &str) {
    // 辞書ファイルのロード
    let reader = zstd::Decoder::new(File::open(dict_path).unwrap()).unwrap();
    let dict = Dictionary::read(reader).unwrap();

    // トークナイザーの生成
    let tokenizer = Tokenizer::new(dict)
        .ignore_space(true).unwrap()
        .max_grouping_len(24);

    // ワーカーの生成。mutableです。
    let mut worker = tokenizer.new_worker();

    // 標準入力から文章を読み込む
    let mut text = String::new();
    io::stdin().read_to_string(&mut text).unwrap();

    // 文章をセット。繰り返したい場合は、これを再度呼び出し、ワーカーを使い回す。
    worker.reset_sentence(&text);
    worker.tokenize(); // 形態素解析の実行。mutable self

    println!("num_tokens: {}", worker.num_tokens());

    // 抽出したトークンをループで表示する
    worker.token_iter()
        .filter(|t| { // 絞り込み
            let words: Vec<&str> = t.feature().split(',').collect();
            let subwords: Vec<&str> = words[0].split('-').collect();
            subwords[0] == "名詞" || subwords[0] == "カスタム名詞"
        })
        .for_each(|t| { // 出力
            println!("{}: {}", t.surface(), t.feature());
        });
}

fn main() {
    let args: Vec<String> = env::args().collect();
    
    if args.len() < 3 || args[1] != "-i" {
        eprintln!("Usage: {} -i <dictionary_path>", args[0]);
        std::process::exit(1);
    }

    let dict_path = &args[2];
    
    // mecab関数を呼び出す
    mecab(dict_path);
}

以下のコマンドを実行することで処理を実行することができます

echo '本とカレーの街神保町へようこそ。' | cargo run --release -p hello-rust -- -i ipadic-mecab-2_7_0/system.dic.zst

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

num_tokens: 10
本: 名詞,一般,*,*,*,*,本,ホン,ホン
カレー: 名詞,固有名詞,地域,一般,*,*,カレー,カレー,カレー
街: 名詞,一般,*,*,*,*,街,マチ,マチ
神保: 名詞,固有名詞,地域,一般,*,*,神保,ジンボウ,ジンボー
町: 名詞,接尾,地域,*,*,*,町,マチ,マチ

MacにRustをインストールする

MacにRustをインストールする

まず MacにRustがインストールされていることを確認します

以下のコマンドで 各種verがインストールされいれば問題ないため,次に進んでください

rustc --version

インストールは以下のコマンドで行います

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

Permissionのエラー対応

この時に以下のエラーが出た場合は,ファイル権限周りがうまくいっていないので変更していきます

エラー内容

error: could not amend shell profile: '/Users/name/.bash_profile': could not write rcfile file: '/Users/name/.bash_profile': Permission denied (os error 13)
(base)

自分の場合は,以下で所有者を確認したところ rootになっていました。

ls -l ~/.bash_profile

そこで所有者を変更します

sudo chown name:staff ~/.bash_profile

次に ファイル権限を変更します

chmod 644 ~/.bash_profile

zshを使っている場合も同様に対応します

DeepPhonemizerの英語の事前学習モデルをcmudict-ipaを使って作成をする

初めに

Transformer モデルに基づく、書記素から音素への変換ライブラリがあります。このライブラリで使用できるモデルをデータセットの整形からモデルの事前学習まで作ってみます

github.com

以下の記事で推論は試しているので、どのようなものなのかは記事をご確認ください。

ayousanz.hatenadiary.jp

開発環境

準備

今回は、学習しやすいように少し変更したものを使うため、以下のリポジトリをcloneして進めます

github.com

まずは、以下のライブラリをインストールします

pip install -r .\requirements.txt

データセットの準備

今回は英語のデータセットを使用するため、以下のリポジトリcmudict-0.7b-ipa.txt を使っていきます

github.com

学習に使用できるようにするため、以下のコードで整形をして別ファイルとして保存します

import re

def convert_format(input_file, output_file):
    with open(input_file, 'r', encoding='utf-8') as infile, open(output_file, 'w', encoding='utf-8') as outfile:
        for line in infile:
            # タブで単語とIPA表記を分割
            parts = line.strip().split('\t')
            if len(parts) != 2:
                continue  # 不正な行はスキップ

            word, ipa_list = parts
            # カンマで区切られたIPA表記の最初の1つだけを取得
            ipa = ipa_list.split(', ')[0]

            # IPAから不要な文字(スラッシュ)を除去
            ipa = re.sub(r'^/|/$', '', ipa)
            
            # 新しい形式で書き出し
            new_line = f"('en_us', '{word}', '{ipa}'),\n"
            outfile.write(new_line)

# ファイルの変換を実行
convert_format('cmudict-0.7b-ipa.txt', 'cmudict-0.7b-ipa_convert.txt')

こちら側で整形したデータは cmudict-0.7b-ipa_convert.txtがあるので、必要であればご使用ください。

学習の実行

作成したデータセットをルートパスに配置します。

その後以下を実行して、学習を開始します

python .\run_training.py

パラメータ設定にもよりますが、1epochだけであれば1-2分ほどで終わります

学習が進むと以下のように checkpoints のフォルダ内にモデルが保存されていきます。

学習したモデルで推論

run_prediction.py を使用して、学習したモデルを使って単語を音素に変換します。

以下のコードで使用するモデルを指定します

checkpoint_path = 'checkpoints/latest_model.pt'

実行をすると以下のように出力されます

['ˈjʌŋ']
<en_us> 0.9999767541885376
ˈ 0.9446256160736084
j 0.9691675901412964
ʌ 0.3388298749923706
ŋ 0.9943882822990417
<end> 1.0
young | <en_us>ˈjʌŋ<end> | 0.3084510115930348

yt-dlpを使ってyoutubeの動画(音声)をダウンロードする

開発環境

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

以下でライブラリのインストールできます

pip install yt-dlp

動画および再生リストのダウンロード

まずは単体の動画のダウンロードです。音声ファイルのみをダウンロードする場合は以下になります

import yt_dlp

url = 'https://www.youtube.com/watch?v=5uaHMmcReI0'

ydl_opts = {
    'format': 'bestaudio/best',
    'postprocessors': [{
        'key': 'FFmpegExtractAudio',
        'preferredcodec': 'mp3',
        'preferredquality': '192',
    }],
}

with yt_dlp.YoutubeDL(ydl_opts) as ydl:
    ydl.download([url])

次に再生リストを一括で音声ファイルとしてダウンロードしてみます

import yt_dlp

def download_playlist(url, output_path='./%(playlist)s/%(title)s.%(ext)s'):
    ydl_opts = {
        'format': 'bestvideo+bestaudio/best',
        'outtmpl': output_path,
        'ignoreerrors': True,  # エラーが発生しても続行
        'nooverwrites': True,  # 既存のファイルを上書きしない
        'playlist_items': '',  # すべての動画をダウンロード
        'writethumbnail': True,  # サムネイルも保存
    }

    with yt_dlp.YoutubeDL(ydl_opts) as ydl:
        try:
            ydl.download([url])
            print("プレイリストのダウンロードが完了しました。")
        except Exception as e:
            print(f"エラーが発生しました: {str(e)}")

# 使用例
playlist_url = input("YouTubeプレイリストのURLを入力してください: ")
download_playlist(playlist_url)

ボーカル音声ファイルからボーカルのみをultimatevocalremoverguiのCLI版を使って抽出する

初めに

歌ってみたなどの音声ファイルからボーカルのみを抽出した場合、以下などのボーカル抽出ソフトなどで対応することができます。

github.com

しかしこのソフトには、CLI版がないため CLIで動くものを探す必要があります。探したところ以下の二つがありました。

github.com

github.com

前者のほうはライブラリのインストールがうまくいかなかったので、後者のほうを動かしていくことにしました。しかし、最新のライブラリで対応するとライブラリのアップデートの影響で動かなくなっていたので、個別の対応してCLIで動くようになったものが以下になります

github.com

開発環境

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

ここからは、個別の動くように対応したほうの以下にリポジトリをベースに進めていきます

github.com

まずは必要なライブラリのインストールします

pip install -r requirements.txt

次にモデルのダウンロードをします。

linuxの場合は、以下を実行します

./download.sh

Windosの場合は、モデルファイルをダウンロードして、uvr5_weights フォルダの中に移動します

実行

以下で処理をするaudio ファイルのパスを指定して、実行することで opt フォルダの中に処理された以下のファイルが生成されます

  • ボーカルのみの音声ファイル
  • ボーカルを除いた音声ファイル
python separate.py audio_path

ハイブリッド検索アプローチ「BM42」を動かしてみる

初めに

以下でBM25よりも精度がいいBM42が発表されたとあるので、実際に触ってみます

www.atpartners.co.jp

以下の記事で、過去にBM25を動かしています。

ayousanz.hatenadiary.jp

以下で今回の記事のリポジトリを公開しています

github.com

開発環境

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

pip install numpy
pip install transformers
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121

BM42のindexおよび検索

以下でドキュメントのindexと検索をしてみます

from typing import List, Dict
import math
from transformers import AutoTokenizer, AutoModel
import torch

# サンプルデータセット
documents = [
    "Hello world is a common phrase in programming",
    "Python is a popular programming language",
    "Vector databases are useful for similarity search",
    "Machine learning models can be complex",
]

def compute_idf(term: str, documents: List[str]) -> float:
    doc_freq = sum(1 for doc in documents if term in doc.lower())
    return math.log((len(documents) - doc_freq + 0.5) / (doc_freq + 0.5) + 1)

def get_bm42_weights(text: str, model, tokenizer):
    inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=512)
    with torch.no_grad():
        outputs = model(**inputs, output_attentions=True)

    attentions = outputs.attentions[-1][0, :, 0].mean(dim=0)
    tokens = tokenizer.convert_ids_to_tokens(inputs['input_ids'][0])

    word_weights = {}
    current_word = ""
    current_weight = 0

    for token, weight in zip(tokens[1:-1], attentions[1:-1]):  # Exclude [CLS] and [SEP]
        if token.startswith("##"):
            current_word += token[2:]
            current_weight += weight
        else:
            if current_word:
                word_weights[current_word] = current_weight
            current_word = token
            current_weight = weight

    if current_word:
        word_weights[current_word] = current_weight

    return word_weights

# モデルとトークナイザーの初期化
tokenizer = AutoTokenizer.from_pretrained("sentence-transformers/all-MiniLM-L6-v2")
model = AutoModel.from_pretrained("sentence-transformers/all-MiniLM-L6-v2")

def compute_bm42_score(query: str, document: str, documents: List[str]) -> float:
    query_weights = get_bm42_weights(query, model, tokenizer)
    doc_weights = get_bm42_weights(document, model, tokenizer)

    score = 0
    for term, query_weight in query_weights.items():
        if term in doc_weights:
            idf = compute_idf(term, documents)
            score += query_weight * doc_weights[term] * idf

    return score

def search_bm42(query: str, documents: List[str]) -> List[Dict[str, float]]:
    scores = []
    for doc in documents:
        score = compute_bm42_score(query, doc, documents)
        scores.append({"document": doc, "score": score})

    return sorted(scores, key=lambda x: x["score"], reverse=True)

# 使用例
query = "programming language"

print("BM42 Results:")
for result in search_bm42(query, documents):
    print(f"Score: {result['score']:.4f} - {result['document']}")

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

BM25 Results:
Score: 1.9970 - Python is a popular programming language
Score: 0.6398 - Hello world is a common phrase in programming
Score: 0.0000 - Vector databases are useful for similarity search
Score: 0.0000 - Machine learning models can be complex

BM42 Results:
BertSdpaSelfAttention is used but `torch.nn.functional.scaled_dot_product_attention` does not support non-absolute `position_embedding_type` or `output_attentions=True` or `head_mask`. Falling back to the manual attention implementation, but specifying the manual implementation will be required from Transformers version v5.0.0 onwards. This warning can be removed using the argument `attn_implementation="eager"` when loading the model.
Score: 0.0117 - Python is a popular programming language
Score: 0.0069 - Hello world is a common phrase in programming
Score: 0.0000 - Vector databases are useful for similarity search
Score: 0.0000 - Machine learning models can be complex

検索エンジンのBM25-rankを試す

開発環境

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

以下のドキュメントにあるようにインストールをします

pip install rank_bm25

pypi.org

ドキュメントから関連文の抽出

まずはいくつかの文章をindexにします

from rank_bm25 import BM25Okapi

corpus = [
    "Hello there good man!",
    "It is quite windy in London",
    "How is the weather today?"
]

tokenized_corpus = [doc.split(" ") for doc in corpus]

bm25 = BM25Okapi(tokenized_corpus)

次に 指定した文に近いものを探します

query = "windy London"
tokenized_query = query.split(" ")

doc_scores = bm25.get_scores(tokenized_query)
print(doc_scores)

token_n = bm25.get_top_n(tokenized_query, corpus, n=1)
print(token_n)

結果は以下のように返ってきます

[0.         0.93729472 0.        ]
['It is quite windy in London']