長文から論点を抽出して、その論点を軸に文章の要約を試みる
とある大きめの文章データを目の前にしたとき、とりあえずGPTに突っ込んで論点を抽出したいけど、データ的に大きすぎてトークン制限オーバーしちゃうし、どうしたものかなと思うことはないでしょうか。私はあります。
LangChainの要約系のチェーンを使っても良いのですが、仕組み上、かなりざっくりとした要約を作られてしまうので、何か大切なものを失ってしまう気がしてなりません。
そんな中で何となく思ったのは、文章を何らかの形でクラスタリングしたときの集合が大きい部分が論点であり、その集合毎に要約を作ってあげれば論点っぽい文章が作れるのではないか?ということでした。
というわけで早速試しにコードを書いてみたいと思います。
k-means法でクラスタリングする
文章をある程度の長さでチャンクに分けた後で、埋め込みベクトルを求めてあげれば「意味の分布」を取ることができます。
そこで今回はその「意味の分布」をk-means法でクラスタリングすることで論点の集合を抽出しようと思います。
対象データは以下の記事で利用したWikipediaのキングダム解説ページの内容を使います。
まず文章をロードして300字程度のチャンクに区切ります。
from langchain.text_splitter import CharacterTextSplitter
from langchain.document_loaders import TextLoader
kingdom_txt = TextLoader('kingdom.txt').load()
text_splitter = CharacterTextSplitter(chunk_size=300, chunk_overlap=50)
documents = text_splitter.split_documents(kingdom_txt)
texts = [doc.page_content for doc in documents]
54個のチャンクに区切れました。
次に各チャンクの埋め込みベクトルの値も求めます。今回はOpenAIのtext-embedding-ada-002を使用しました。
import openai
response = openai.Embedding.create(input=texts, model="text-embedding-ada-002")
embeds = [record['embedding'] for record in response['data']]
当たり前ですがこちらも54要素のリストになっています。
このリストをクラスタリングするわけですが、今回はFaissのクラスタリング機能を利用してクラスタを求めようと思うので、Faissをセットアップします。
# macbookで動かしているのでCPU版をインストールしています
!pip install faiss-cpu
セットアップが完了したら埋め込みベクトルをFaiss DBに登録します。
import faiss
import numpy as np
embeds_np = np.array(embeds)
index = faiss.IndexFlatL2(embeds_np.shape[1])
index.add(embeds_np)
今回はクラスタ数を5個として分割してみます。
clusters_num = 5
kmeans = faiss.Kmeans(embeds_np.shape[1], clusters_num, niter=20, verbose=True)
kmeans.train(embeds_np)
clusters = kmeans.index.search(embeds_np, 1)[1].flatten()
実行時に195要素以上ないとちゃんとした結果が返ってこないよと警告が出ます。
clustersには以下のようにそれぞれの要素がどのクラスタに当てはまるのかのラベルが設定されています。
なのでラベルを元に、チャンク化した文字列をラベル別のディクショナリに保存します。
clustered_sentences = {}
for i, label in enumerate(clusters):
if label not in clustered_sentences:
clustered_sentences[label] = []
clustered_sentences[label].append(texts[i])
こんな区分けになりました。
ちなみに何となくイケてるかもと思って各要素を2次元にマッピングしてみるなどといったことをしてみましたが、よく分からない感じになりました。。
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE
tsne = TSNE(n_components=2, random_state=42)
embeddings_2d = tsne.fit_transform(embeds_np)
plt.figure(figsize=(8, 6))
colors = ['b', 'g', 'r', 'c', 'm', 'y', 'k', 'w']
for i, label in enumerate(clusters):
x, y = embeddings_2d[i]
plt.scatter(x, y, c=colors[label % len(colors)], label=f'Cluster {label}')
plt.annotate(f'S{i}', (x, y), textcoords="offset points", xytext=(-5, 5), ha='center')
plt.xlabel('t-SNE 1')
plt.ylabel('t-SNE 2')
plt.title('2D t-SNE Visualization of Clusters')
plt.show()
各クラスタをLangChainで要約する
ここまででクラスタを取得することができたので、各クラスタ毎に要約を作ってみると良さそうな気がします。
ただ、クラスタ毎の文章を結合すると4000字オーバーのクラスタも出てきてしまい、そのままGPT-3.5 APIに投げることもできないのでLangChainを利用して要約します。
こういうときに便利に使えるのがload_summarize_chainなのですが、そのまま使うとライブラリ内のプロンプトが英語なので、アウトプットが英語になってしまいます。
そこで、元のプロンプトをそのまま日本語にした簡易なものですが、以下のようなプロンプトを用意します。
from langchain.prompts import PromptTemplate
prompt_template = """以下の内容について簡潔に要約を書いて下さい:
"{text}"
簡潔な要約:"""
PROMPT = PromptTemplate(template=prompt_template, input_variables=['text'])
このプロンプトを利用し、以下のコードで各クラスタの要約を作成します。
from langchain.chat_models import ChatOpenAI
from langchain.docstore.document import Document
from langchain.chains.summarize import load_summarize_chain
cluster_summaries = []
for cluster_sentences in clustered_sentences.values():
docs = [Document(page_content=sentence) for sentence in cluster_sentences]
chain = load_summarize_chain(
ChatOpenAI(),
chain_type="map_reduce",
map_prompt=PROMPT,
combine_prompt=PROMPT,
)
summary = chain.run(docs)
cluster_summaries.append(summary)
すると以下の結果が得られます。
それっぽくなりましたが、一目で各クラスタが何を意味するのかは分からないぐらいの文字数です。
簡単ですが以下のプロンプトを使ってそれぞれの要約にタイトルをつけてもらいます。
from langchain.prompts import PromptTemplate
prompt_template = """次の文章のタイトルを20字以内で作成せよ:
"{text}"
20字以内のタイトル:"""
PROMPT = PromptTemplate(template=prompt_template, input_variables=['text'])
こんな感じのコードでタイトルをつけてもらって・・・
from langchain.llms import OpenAI
summary_pairs = []
llm = OpenAI(temperature=0.0)
for summary in cluster_summaries:
title = llm(PROMPT.format(text=summary))
summary_pairs.append((title, summary))
最終結果が以下のような形です。
要素数が2個程度しかなかったクラスタはノイズに近い可能性があるので、ここで列挙した上位3件が適切な要約と言えるかも知れません。
今回はWikipediaのページで行いましたが、それなりに分量の大きいデータソース、例えばインタビューデータなどで適用してみると面白いかも知れません。喋り言葉の生データから論点を抽象化していくのは骨が折れますが、今回の方法であれば抽象化の叩き台を作ってくれるので、その後の編集作業がだいぶ楽になる可能性があります。
現場からは以上です。
この記事が気に入ったらサポートをしてみませんか?