はじめに
ふりがなWhisperの埋め込みを取り出して音韻の類似度らしきものが測れるか試してみます。
背景
ダジャレや空耳の自動生成などに応用するため、音韻の類似度を定量化する方法を色々探しています。
以前、日本語のwav2vecの埋め込みを試してみましたが、意味の近さの影響が大きいように思われました。
ふりがなWhisperであれば、より、音韻の類似度をより重視した埋め込みを作れることを期待し、試してみました。
実装
環境構築
実行環境は以下です。
- MacBookAir
- python 3.11.2
以下をインストールしておきます。
uv init .
uv add "numpy==1.*" # torchの依存関係でv1しか使えないため
uv add torch==2.2 # uvだと最新のtorchをaddできないため
uv add transformers
コード
# pip install transformers fugashi[unidic] jaconv torch
from transformers import WhisperTokenizer, WhisperModel
from fugashi import Tagger
import jaconv, torch, numpy as np
TAGGER = Tagger()
MODEL_ID = "Parakeet-Inc/furigana_whisper_small_jsut"
tok = WhisperTokenizer.from_pretrained(MODEL_ID)
model = WhisperModel.from_pretrained(MODEL_ID)
def text2kana(text: str) -> str:
"""漢字交じり→カタカナ(モーラ列)簡易版。精度を上げたい場合は
記事に載っている MeCab N-Best + レーベンシュタイン手法を採用。"""
kana = []
for n in TAGGER(text):
k = n.feature.kana
kana.append(k if k and k != "*" else jaconv.hira2kata(n.surface))
return "".join(kana)
# ── ① token embedding 平均 ────────────────────────────────────
def phonetic_emb_avg(text: str) -> np.ndarray:
kana = text2kana(text)
ids = tok(kana, add_special_tokens=False, return_tensors="pt")["input_ids"]
with torch.no_grad():
vec = model.decoder.embed_tokens(ids) # (1, L, d)
return vec.mean(1).squeeze().cpu().numpy() # (d,)
# ── ② 文脈付き:decoder 最終層隠れ状態平均 ─────────────────
def phonetic_emb_ctx(text: str) -> np.ndarray:
kana = text2kana(text)
ids = tok(kana, add_special_tokens=False, return_tensors="pt")["input_ids"]
# Whisper は「音声エンコーダ出力」が必須なのでダミー 1frame を渡す
dummy_enc = torch.zeros(1, 1, model.config.d_model) # (B, T, d)
with torch.no_grad():
out = model.decoder(
input_ids=ids,
encoder_hidden_states=dummy_enc,
output_hidden_states=True,
)
return out.last_hidden_state.mean(1).squeeze().cpu().numpy()
def phonetic_similarity(text1: str, text2: str) -> None:
"""漢字交じりのテキスト2つの類似度を計算する関数"""
print(f"入力1: {text1}")
print(f"入力2: {text2}")
avg_emb1 = phonetic_emb_avg(text1)
avg_emb2 = phonetic_emb_avg(text2)
ctx_emb1 = phonetic_emb_ctx(text1)
ctx_emb2 = phonetic_emb_ctx(text2)
def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
avg_sim = cosine_similarity(avg_emb1, avg_emb2)
ctx_sim = cosine_similarity(ctx_emb1, ctx_emb2)
print(f"phonetic_emb_avg 類似度: {avg_sim}")
print(f"phonetic_emb_ctx 類似度: {ctx_sim}")
if __name__ == "__main__":
phonetic_similarity("いね", "いぬ")
phonetic_similarity("いね", "むぎ")
実行
実行してみます。
% uv run phonetic_emb.py
入力1: いね
入力2: いぬ
Passing a tuple of `past_key_values` is deprecated and will be removed in Transformers v4.43.0. You should pass an instance of `EncoderDecoderCache` instead, e.g. `past_key_values=EncoderDecoderCache.from_legacy_cache(past_key_values)`.
phonetic_emb_avg 類似度: 0.9544370174407959
phonetic_emb_ctx 類似度: 0.9991653561592102
入力1: いね
入力2: むぎ
phonetic_emb_avg 類似度: 0.8877464532852173
phonetic_emb_ctx 類似度: 0.9890563488006592
avgでもctxでも「いぬ-いね」のほうが「いね-むぎ」より類似度が高い結果になりました。
一応、意味よりも音韻が優先された類似度となっていそうです。
解説
2種類の埋め込み計算関数がありますが、漢字かな交じり文を入力し、内部でカナに変換してからモデルに入力し、埋め込みベクトルを得る点は共通です。
phonetic_emb_avg
ではプロンプト入力層で作られる埋め込みを得ています。
素直には最終層の重みベクトルがトークンに対応した埋め込みとみなせるのですが、Whisperではプロンプト入力層と最終層が共通化されているので、プロンプト入力層にトークンID列を入力した結果を取得すればよいことになります。
トークン列の埋め込みは各トークンの埋め込みの平均によって計算します。
phonetic_emb_ctx
ではプロンプトとダミーの音声データ(ゼロ行列)を入力し、最終層でトークンの確信度に変換される直前の埋め込みを取得しています。
これも基本的には、プロンプト入力層から得られる埋め込みと似たようなものになっていることが期待されるわけですが、プロンプト入力層から得られる埋め込みは完全に各トークンの埋め込みが独立に計算されるのに対し、最終層直前の埋め込みの場合は、一応、文脈情報を含むことになるので、多少精度が上がる可能性があります。ただ音声データはダミーのため、実用的かどうかは不明です。
いろいろお試し
いくつか、どっちが類似しているか迷いそうなひらがなペアに対して類似度を計算してみます。
ペアの候補は「LLMによる音韻検索の性能評価」の記事から適当に取得しました。
if __name__ == "__main__":
phonetic_similarity("あって", "まるて")
phonetic_similarity("あって", "かった")
print("")
phonetic_similarity("あれほど", "かねもと")
phonetic_similarity("あれほど", "あらうほ")
print("")
phonetic_similarity("あと", "えと")
phonetic_similarity("あと", "かとう")
print("")
phonetic_similarity("いきて", "いしげ")
phonetic_similarity("いきて", "いした")
入力1: あって
入力2: まるて
phonetic_emb_avg 類似度: 0.9585291147232056
phonetic_emb_ctx 類似度: 0.9348230361938477
入力1: あって
入力2: かった
phonetic_emb_avg 類似度: 0.9605201482772827
phonetic_emb_ctx 類似度: 0.9588626623153687
入力1: あれほど
入力2: かねもと
phonetic_emb_avg 類似度: 0.9564060568809509
phonetic_emb_ctx 類似度: 0.9739295244216919
入力1: あれほど
入力2: あらうほ
phonetic_emb_avg 類似度: 0.9778033494949341
phonetic_emb_ctx 類似度: 0.9883297681808472
入力1: あと
入力2: かとう
phonetic_emb_avg 類似度: 0.9592791795730591
phonetic_emb_ctx 類似度: 0.9305437207221985
入力1: あと
入力2: えと
phonetic_emb_avg 類似度: 0.9647287726402283
phonetic_emb_ctx 類似度: 0.9986695647239685
入力1: いきて
入力2: いしげ
phonetic_emb_avg 類似度: 0.9638122916221619
phonetic_emb_ctx 類似度: 0.9113794565200806
入力1: いきて
入力2: いした
phonetic_emb_avg 類似度: 0.961462676525116
phonetic_emb_ctx 類似度: 0.9354600310325623
実は入力1が同じペアのうち、前者が人間が作詞した空耳替え歌から取得したもの、後者がGPT-4.5が音韻が類似していると判断したもの、になっています。
結果を眺めると、ほとんどのケースにおいて、埋め込みの計算方法がavgかctxかによらず、GPT-4.5のペアのほうが類似度が高かったです。
人間が作詞した空耳歌詞は、歌唱という制約上、母音の一致が強く重視されるのですが、多くの音声認識モデルは歌唱よりはスピーチや会話で学習していると思われるので、必ずしも替え歌の世界で「似てる」と評価される基準とは同じにならないのかもしれません。