![見出し画像](https://assets.st-note.com/production/uploads/images/104441739/rectangle_large_type_2_2b7a924bacc620c8b7072b415b0afd98.png?width=1200)
長文から論点を抽出して、その論点を軸に文章の要約を試みる
とある大きめの文章データを目の前にしたとき、とりあえず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個のチャンクに区切れました。
![](https://assets.st-note.com/img/1682934203244-HXWCmHrY1H.png?width=1200)
次に各チャンクの埋め込みベクトルの値も求めます。今回は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要素のリストになっています。
![](https://assets.st-note.com/img/1682934291351-LBeu0jEBBA.png?width=1200)
このリストをクラスタリングするわけですが、今回は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要素以上ないとちゃんとした結果が返ってこないよと警告が出ます。
![](https://assets.st-note.com/img/1682934485834-Luk5I2qXBC.png?width=1200)
clustersには以下のようにそれぞれの要素がどのクラスタに当てはまるのかのラベルが設定されています。
![](https://assets.st-note.com/img/1682934704065-kXzehG1rlB.png?width=1200)
なのでラベルを元に、チャンク化した文字列をラベル別のディクショナリに保存します。
clustered_sentences = {}
for i, label in enumerate(clusters):
if label not in clustered_sentences:
clustered_sentences[label] = []
clustered_sentences[label].append(texts[i])
こんな区分けになりました。
![](https://assets.st-note.com/img/1682934825251-Brn8GIbHkS.png?width=1200)
ちなみに何となくイケてるかもと思って各要素を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()
![](https://assets.st-note.com/img/1682934922009-IkaEOnygGT.png?width=1200)
各クラスタを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)
すると以下の結果が得られます。
![](https://assets.st-note.com/img/1682935225807-FvNzDf7uqr.png?width=1200)
それっぽくなりましたが、一目で各クラスタが何を意味するのかは分からないぐらいの文字数です。
簡単ですが以下のプロンプトを使ってそれぞれの要約にタイトルをつけてもらいます。
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))
最終結果が以下のような形です。
「キングダム」の概要と出典
「キングダム」という漫画作品は、原泰久によって創作され、累計部数は9700万部を突破している。テレビアニメや実写映画化もされており、複数のファン参加企画や展示会が開催されている。また、公式ガイドブックやゲーム化もされている。本記事は、「キングダム」という漫画に関する説明や出典、関連項目について記載されている。外部リンクには公式サイトや展示会の情報が掲載されている。
キングダム:戦国時代の闘い
「キングダム」という漫画は、古代中国の戦国時代を舞台にしたストーリーで、主人公の李信と漂が戦災孤児として大将軍を目指し修行する。政は異母弟と丞相の反乱により玉座を追われる。秦・魏・趙・楚・燕・斉・韓の七か国が戦い、特殊部隊が活躍する。戦闘シーンでは、矛や莫耶刀、井闌車、戦車などが使用される。
信-陸無双- 春秋戦国時代
「信-陸無双-」は春秋戦国時代末期の古代中国を舞台に、主人公・信が後の始皇帝・嬴政と共に「天下の大将軍」を目指す活躍を描いた作品。作中では、紀元前247年以前から始まり、周の申候の乱や穆公の即位、趙・魏・韓の三分などの出来事が挙げられる。秦は中国の西端に位置する強国で、後宮は三千人以上の宮女と宦官から成り、名家の出身者が多い。魏は中華の国で、秦にとって中原進出の障害となっている。趙は秦に対して恨みを抱いており、李牧が新たに三大天に任命され、騎馬隊が精強である。毐は大后の指示により、太原一帯を治める国家だが、咸陽攻略に失敗し滅亡した。徐は小さな国家で、趙・魏・楚の情報を流して生業としている。
「羌瘣との戦い―呂不韋の陰謀」
政と信は脱出を試みるが、刺客に襲われる。羌瘣との戦いの中、信の言葉で羌瘣は心を揺さぶられる。昌文君達の到着により、刺客を撃退するが、首謀者は秦右丞相・呂不韋であった。河了貂は軍師になることを決意し、信は王騎に修行を乞い、修業の日々は続く。また、蚩尤や刺客集団についても紹介される。
"天下の大将軍への道"
本作は史記に登場する実在の人物をモデルにしており、作者は史実を捻じ曲げず感情移入を大切にしている。主人公の信は戦争孤児で、漂と共に「天下の大将軍」を目指し、多くの人物と共に数々の戦で武功を重ねる物語。
要素数が2個程度しかなかったクラスタはノイズに近い可能性があるので、ここで列挙した上位3件が適切な要約と言えるかも知れません。
今回はWikipediaのページで行いましたが、それなりに分量の大きいデータソース、例えばインタビューデータなどで適用してみると面白いかも知れません。喋り言葉の生データから論点を抽象化していくのは骨が折れますが、今回の方法であれば抽象化の叩き台を作ってくれるので、その後の編集作業がだいぶ楽になる可能性があります。
現場からは以上です。