🎅

ペルソナ対話文を文埋め込みモデルで可視化する 〜ペルソナは文埋め込み空間上で観察できるか?〜

2024/12/13に公開

https://qiita.com/advent-calendar/2024/ca-26th

はじめに

u-hyszkと申します!

本記事では、文埋め込みモデルと自然言語処理において人や人格を表すペルソナの概念についてご紹介します。
また、ペルソナ対話文を文埋め込みモデルで可視化するという簡単な定性分析を通して、実際の利用例もご紹介します。

文埋め込みとは?

自然言語文を実数ベクトルとして表現したものを文埋め込みと呼びます。
単語をベクトルで表現したものを単語ベクトルと呼ぶのに対し、文埋め込みは文全体をベクトル空間上に埋め込みます。
これによって、文のカテゴリ分類や感情分析、含意関係認識、言い換え表現の認識など、文を単位とするタスクに応用しやすくなるというメリットがあります。
直近ではLLMに外部知識を効率的に取り込むための手法であるRAGにおいても主要な役割を担っています。

なお、「埋め込み」という言葉は、「単語の意味をニューラルネットワークが用いる実数空間に『埋め込む』という状況に焦点を当てている」というところから来ているそうです。

代表的な文埋め込み手法としては以下のようなものがあります。

SentenceBERT (SBERT)

Transformerベースのマスク言語モデルであるBERTを使用する手法です。
まず、与えられた文のペアに対して、その文のペアの含意関係が「含意」「矛盾」「その他」のうちのどれであるかを予測する自然言語推論(Natural Language Inference; NLI)タスクでBERTのファインチューニングを行います。

得られたモデルで各単語の(文脈化)単語埋め込みを計算したのち、Poolingと呼ばれる処理を行うことで文単位のベクトルを構成します。
代表的なPoolingの手法としては、すべての(文脈化)単語埋め込みの平均を取る、先頭の特別なトークン[CLS]を使う、などの手法があります。

SentenceBERT
参考文献[3]からの引用

SimCSE (Simple Contrastive Learning of Sentence Embeddings)

対照学習を用いた手法です。
対照学習では、入力文を正例と負例に分け、正例同士の類似度が高くなるように学習することで、良質な埋め込み空間を構成します。

SimCSEでは、教師なし学習であるUnsupervised SimCSEと教師あり学習であるSupervised SimCSEを提案しています。
Unsupervised SimCSEでは、同じ文に対して異なるマスクを適用して作成した二つの文埋め込みを正例としています。
Supervised SimCSEでは、先ほども出てきた自然言語推論(NLI)のデータセットを利用して、「含意」「矛盾」「その他」のうち「含意」のペアを正例として扱います。

SimCSE
参考文献[5]からの引用

日本語文埋め込みモデル

文埋め込みモデルのベースとなる代表的なモデルとしては以下のようなものがあります。

hppさんが日本語の文埋め込みモデルに関して網羅的な分析を行なっているので、実装・評価の方法も含めて参考になるのではないかと思います。
https://github.com/hppRC/simple-simcse-ja

また、単語埋め込みの日本語版ベンチマークもあるので、こちらも参考になると思います。
https://github.com/sbintuitions/JMTEB

ペルソナとは?

Weblio辞書によるとペルソナとは人や人格を意味する言葉だそうです。
自然言語処理、特に雑談対話の文脈でも同様に人や人格を表現する文をペルソナ文と呼ぶ場合があります。

例えば、よく使用される英語の雑談対話データセット「PersonaChat」では、特定のペルソナを「I like to ski」などのシンプルな文の集合で表現し、このペルソナになりきって対話をすることで対話文を収集しています。

PersonaChat
参考文献[7]からの引用

日本語でのペルソナ対話文が含まれるデータセットとして、2024年の言語処理学会(NLP2024)で発表されたRealPersonaChatがあります。
従来のペルソナを表現する文の他に、Big FiveやKiSS-18などの性格特性なども定義されています。
huggingfaceから簡単に利用することができます。

https://huggingface.co/datasets/nu-dialogue/real-persona-chat

知りたいこと

本記事では、ペルソナ対話文を文埋め込みモデルで可視化するという分析を通して、実際の可視化の例をご紹介します。

ペルソナ対話なら、対話文の中にそのペルソナの特徴が現れてくるはずです。
例えば「スキー」という単語が頻繁に出てきたり、敬体・常体の話し方に出てくる可能性もあります。
このようなペルソナの特徴を文埋め込みの空間上から観察することができないかというのが今回の試みになります。

具体的には、RealPersonaChatの対話文から文埋め込みを抽出し、次元削減アルゴリズムt-SNEによる可視化を行います。
文埋め込みモデルとしてはcl-nagoya/unsup-simcse-ja-baseを利用します。

実装

まずは、RealPersonaChatの対話データをロードし、前処理を行います。

from datasets import load_dataset, Dataset
from itertools import chain

# RealPersonaChatの対話データをロード
ds = load_dataset(
    "nu-dialogue/real-persona-chat",
    name="dialogue",
    split="train",
    trust_remote_code=True
)

# データセットの入れ子構造を解いて、必要なデータのみを取得
ds = ds.flatten()
ds = Dataset.from_dict(
    {
        "utterance_id": list(chain.from_iterable(ds["utterances.utterance_id"])),
        "interlocutor_id": list(chain.from_iterable(ds["utterances.interlocutor_id"])),
        "text": list(chain.from_iterable(ds["utterances.text"])),
    }
)

次に、RealPersonaChatの話者データをロードし、可視化に使う話者をランダムに選択します。
今回は話者数を2人、4人、6人と増やしていきます。

# RealPersonaChatの話者データをロード
ds_interlocutor = load_dataset(
    "nu-dialogue/real-persona-chat",
    name="interlocutor",
    split="train",
    trust_remote_code=True
)

# ランダムにN人の話者IDだけを取得
limit = 6  # 話者数
ds_interlocutor = ds_interlocutor.shuffle()
interlocutors = ds_interlocutor["interlocutor_id"][:limit]

# 話者IDで絞り込む
ds = ds.filter(lambda example: example["interlocutor_id"] in interlocutors)

選択した話者ごとに、ランダムに文を取得します。
今回は最大で1000文ずつ使用することにします。

# 各話者からランダムにN文だけ取得
N = 1000  # 各話者から選択する文の数
selected_utterances = []
for interlocutor in interlocutors:
    # 話者ごとに文を抽出
    interlocutor_ds = ds.filter(lambda x: x["interlocutor_id"] == interlocutor)
    # ランダムにN文を選択(文がNより少ない場合は全て選択)
    sample_size = min(N, len(interlocutor_ds))
    selected = interlocutor_ds.shuffle().select(range(sample_size))
    selected_utterances.append(selected)

# 選択された対話文を結合
ds = Dataset.from_dict({
    "utternace_id": list(chain.from_iterable([d["utternace_id"] for d in selected_utterances])),
    "interlocutor_id": list(chain.from_iterable([d["interlocutor_id"] for d in selected_utterances])),
    "text": list(chain.from_iterable([d["text"] for d in selected_utterances])),
})

ここまでの前処理で取得した文に対して、文埋め込みモデルで推論を行います。
ライブラリにはSentenceTransformerを使用します。

from sentence_transformers import SentenceTransformer

# 文埋め込みモデルのロード
model = SentenceTransformer("cl-nagoya/unsup-simcse-ja-base")

# 文埋め込みの抽出
embeddings = model.encode(ds["text"])

# Datasetの列に挿入
ds = ds.add_column("embeddings", embeddings.tolist())

文埋め込みの次元数は768次元なので直接可視化することができません。
そこで、高次元ベクトルの次元削減に適したt-SNEアルゴリズムを使用して、2次元まで次元数を下げます。
ライブラリにはscikit-learnを使用します。

from sklearn.manifold import TSNE

# t-SNEで次元削減
X = np.array(ds["embeddings"])
tsne = TSNE(n_components=2, random_state=42)
X_tsne = tsne.fit_transform(X)

最後にmatplotlibseabornを使用して一気に可視化を行います。
点の数が多くて見づらくなるため、カーネル密度推定による密度プロットも追加して視認性を向上させます。

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# プロットの準備
fig = plt.figure(figsize=(10, 10))

# グリッドを作成(中央に散布図、上と右に密度プロット)
gs = fig.add_gridspec(20, 20)
ax_scatter = fig.add_subplot(gs[4:, :-4])  # メインの散布図
ax_top = fig.add_subplot(gs[:4, :-4], sharex=ax_scatter)  # 上部の密度プロット
ax_right = fig.add_subplot(gs[4:, -4:], sharey=ax_scatter)  # 右側の密度プロット

unique_interlocutors = list(set(ds["interlocutor_id"]))
colors = sns.color_palette("husl", len(unique_interlocutors))

for idx, interlocutor in enumerate(unique_interlocutors):
    mask = np.array(ds["interlocutor_id"]) == interlocutor
    
    # メインの散布図
    ax_scatter.scatter(
        X_tsne[mask, 0],
        X_tsne[mask, 1],
        label=interlocutor,
        s=10,
        alpha=0.7,
        c=[colors[idx]]
    )
    
    # 上部の密度プロット
    sns.kdeplot(
        x=X_tsne[mask, 0],
        ax=ax_top,
        color=colors[idx],
        alpha=0.5,
        label=interlocutor
    )
    
    # 右側の密度プロット
    sns.kdeplot(
        y=X_tsne[mask, 1],
        ax=ax_right,
        color=colors[idx],
        alpha=0.5
    )

# 軸ラベルと目盛りの調整
ax_top.set_xticks([])
ax_top.set_yticks([])
ax_right.set_xticks([])
ax_right.set_yticks([])
ax_top.set_xlabel('')
ax_top.set_ylabel('')
ax_right.set_xlabel('')
ax_right.set_ylabel('')

# 凡例は上部のプロットにのみ表示
ax_top.legend(bbox_to_anchor=(1.02, 1), loc='upper left')

plt.tight_layout()
plt.savefig("tsne_visualization.png", dpi=300, bbox_inches='tight')
plt.close()

可視化

作成したコードを用いて対話文の可視化を行なった結果を示します。

まずは2話者の場合です。

1つの点が1つの対話文に対応しており、点の色が話者IDに対応します。
話者数が少ない場合だと、話者ごとに分布が分かれる傾向が見て取れます。
直感的には、人数が少なければ誰が話してるのか大まかに分かると解釈できるかと思います。

2話者の場合

次に、話者数を4人に増やしてみます。
先ほどとは違い、分布が固まっており、話者ごとの違いはないように見えます。
その一方で、左下の「GM」のように局所的に固まっている部分もあり、特徴的な対話文があれば誰が話しているのか分かることを示唆しているように見えます。

4話者の場合

最後に、話者数を6人に増やします。
ほとんどの話者は区別できなくなった一方で、話者「HR」のみ分布が明らかに異なる傾向が見て取れます
全体的に特殊な対話を行なっている話者がいるか、文埋め込みの性質が関係しているのでしょうか?

6話者の場合

本記事では、これ以上踏み込みませんが、

  • 実際の文の内容の対応関係
  • 各話者のペルソナ(文や性格特性)との関係性

などが分かると面白いかと思います!

おわりに

本記事では、文埋め込みモデルとペルソナの概念についてご紹介しました。

また、特定のペルソナになりきった対話文から話者を分離できるかという簡単な定性分析を通して、実際の利用例もご紹介しました。

ペルソナを含め雑談対話は研究にするのは中々大変ですが、「もっと流行ってくれ...!」という思いです。
来年の言語処理学会(NLP2025)も楽しみですね!

ご覧いただきありがとうございました🎅

参考文献

[1]IT Text 自然言語処理の基礎
https://www.ohmsha.co.jp/book/9784274229008/
[2]定義文を用いた文埋め込み構成法
https://www.jstage.jst.go.jp/article/jnlp/30/1/30_125/_pdf/-char/ja
[3]Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks
https://arxiv.org/abs/1908.10084
[4]平均プーリングによる文埋め込みの再検討:平均は点群の要約として十分か?
https://www.anlp.jp/proceedings/annual_meeting/2024/pdf_dir/A10-4.pdf
[5]SimCSE: Simple Contrastive Learning of Sentence Embeddings
https://arxiv.org/pdf/2104.08821
[6][輪講資料]SimCSE: Simple Contrastive Learning of Sentence Embeddings
https://speakerdeck.com/hpprc/lun-jiang-zi-liao-simcse-simple-contrastive-learning-of-sentence-embeddings-823255cd-bd1f-40ec-a65c-0eced7a9191d?slide=6
[7]Personalizing Dialogue Agents: I have a dog, do you have pets too?
https://arxiv.org/pdf/1801.07243
[8]RealPersonaChat: 話者本人のペルソナと性格特性を含んだ雑談対話コーパス
https://www.anlp.jp/proceedings/annual_meeting/2024/pdf_dir/B10-4.pdf
[9]人物埋め込み空間の内挿性と制御性を兼ね備えた応答生成モデル
http://ahcweb01.naist.jp/papers/conference/2023/202303_SLUD_hiroki-ya/20230309_SLUD_hiroki-ya.slides.pdf

Discussion