LangChainの文書検索を用いて、東大入試の英語要約問題を解かせてみた

## はじめに
LangChainの文書検索を用いて、東大入試の英語要約問題を解かせてみました。
LangChainの文書検索を試してみたい方におすすめです。

## 開発環境
- Windows 11
- Python 3.11.0

## 取り扱う問題
本記事では、2022年東大入試英語の第1問で出題された要約問題を取り扱います。
この問題をLangChainの文書検索を用いて解き、模範解答と比較します。
問題と解答へのリンクは以下のとおりです。

https://tanakake-t-english.com/%E6%9D%B1%E5%A4%A72022-1a

## 実装
### 1. 必要なライブラリのインストール
今回の実装で必要なライブラリであるOpen AI、LangChain、ChromaDBをインストールします。

```
pip install openai langchain chromadb
```

### 2. Open AIのAPIキーを設定
まずは、Open AIのAPIキーを設定します。
```python:test.py
import os
os.environ["OPENAI_API_KEY"] = "[OpenAIのAPIキー]"
```

### 3. 必要なモジュールとクラスのインポート
次に、必要なモジュールとクラスをインストールします。
```python:test.py
from langchain.chains import RetrievalQA
from langchain.document_loaders import TextLoader
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.llms import OpenAI
from langchain.text_splitter import CharacterTextSplitter
from langchain.vectorstores import Chroma
```
各モジュールやクラスの説明は以下のとおりです。
1. `from langchain.chains import RetrievalQA`：質問応答(QA)チェーンを作成するために使用されます。
1. `from langchain.document_loaders import TextLoader`：テキストファイルを読み込むために使用されます。
1. `from langchain.embeddings.openai import OpenAIEmbeddings`：テキストチャンクから埋め込みベクトルを生成するために使用されます。
1. `from langchain.llms import OpenAI`：OpenAIの言語モデルを使用するために使用されます。
1. `from langchain.text_splitter import CharacterTextSplitter`：テキストを特定のサイズのチャンクに分割するために使用されます。
1. `from langchain.vectorstores import Chroma`：埋め込みベクトルからChromaベクトルストアを作成するために使用されます。各テキストチャンクをベクトル空間にマッピングし、類似性検索が可能になります。

### 4. テキストを読み込んでチャンクに分割
次に、テキストを読み込んでチャンクに分割します。
```python:test.py
loader = TextLoader("text_model.txt",encoding="utf-8")
documents = loader.load()
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
texts = text_splitter.split_documents(documents)
```
各コードの説明は以下のとおりです。
1. `loader = TextLoader("text_model.txt",encoding="utf-8")`：ここではTextLoaderクラスのインスタンスを作成しています。このインスタンスは、指定されたテキストファイル（この場合は"text_model.txt"）を読み込むために使用されます。エンコーディングパラメータは、ファイルがどの文字エンコーディング（この場合は"utf-8"）で保存されているかを指定します。
1. `documents = loader.load()`：ここではloadメソッドを使用して、上記で作成したTextLoaderインスタンスからテキストファイルの内容を読み込んでいます。この操作により、テキストファイルの内容がPythonの文字列としてメモリにロードされます。
1. `text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)`：ここではCharacterTextSplitterクラスのインスタンスを作成しています。このインスタンスは、テキストを特定のサイズのチャンクに分割するために使用されます。この場合、チャンクサイズは1000文字で、チャンク間で共有される文字数は0文字です。
1. `texts = text_splitter.split_documents(documents)`：最後に、split_documentsメソッドを使用して、読み込んだドキュメントをチャンクに分割します。これにより、大きなテキストファイルが小さなチャンクに分割され、それぞれが個別に処理できるようになります。

 　　
text_model.txtの内容は以下のとおりです。先ほどの入試問題の英文を貼り付けています。
```text:text_model.txt
Table manners are as old as human society itself, the reason being that no human society can exist without them. The active sharing of food — not consuming all the food we find on the spot, but carrying some back home and then giving it out systematically — is believed, even nowadays, to lie at the root of what makes us different from animals. Birds, dogs, and hyenas carry home food for their young until they are ready to find for themselves, and chimpanzees may even demand and receive pieces of meat from other adults in their group. (Chimpanzees apparently exhibit this behaviour only on the occasions when they consume meat; their main, vegetable diet they almost invariably eat where they find it, without sharing.) Only people actively, regularly, and continuously work on the distribution of their food.
This activity is based on and probably helped give rise to many basic human characteristics, such as family and community (who belongs with whom; which people eat together), language (for discussing food past, present, and future, for planning the acquisition of food, and deciding how to divide it while preventing fights), technology (how to kill, cut, keep, and carry), and morality (what is a fair portion?). The basic need of our stomachs for food continues to supply a good deal of the driving force behind all of human enterprise: we have to hunt for food, fight for it, find it, or sow it and wait for it to be ready; we then have to transport it, and distribute it before it goes rotten. It is in addition easier for us to consume food chopped, ground, cooked, or left to soften. Civilization itself cannot begin until a food supply is assured. And where food is concerned we can never stop; appetite keeps us at it.
The active sharing out of what we are going to eat is only the beginning. We cannot help being choosy about our food: preference enters into every mouthful we consume. We play with food, show off with it, hon our and despise it. The main rules about eating are simple: if you do not eat you die; and no matter how large your dinner, you will soon be hungry again. Precisely because we must both eat and keep on eating, human beings have poured enormous effort into making food more than itself, so that it bears multiple meanings beyond its primary purpose of physical
```

### 5. 各テキストチャンクをベクトル空間にマッピングし、類似性検索を可能にする
次に、各テキストチャンクをベクトル空間にマッピングします。
```python:test.py
embeddings = OpenAIEmbeddings()
docsearch = Chroma.from_documents(texts, embeddings)
```

各コードの説明は以下のとおりです。
1. `embeddings = OpenAIEmbeddings()`: OpenAIEmbeddingsというクラスのインスタンスを作成しています。このクラスは、OpenAIが提供する言語モデル（GPT-3など）を用いて、テキストデータを数値のベクトル（埋め込み）に変換する機能を提供します。この埋め込みは、テキストデータの意味的な内容を捉えることができます。
1. `docsearch = Chroma.from_documents(texts, embeddings)`: Chromaというクラスのfrom_documentsメソッドを呼び出しています。このメソッドは、与えられたテキストデータ（texts）を埋め込み（embeddings）に変換し、それらの埋め込みを用いて文書検索のためのインデックスを作成します。このインデックスは、後で特定のクエリに対する最も関連性の高い文書を高速に検索するために使用されます。


### 6. 質問応答(QA)チェーンを作成して実行、結果を表示
最後に、質問応答(QA)チェーンを作成して実行、結果を表示します。
```python:test.py
qa = RetrievalQA.from_chain_type(llm=OpenAI(), chain_type="stuff", retriever=docsearch.as_retriever())

query = "英文を読み、その内容を70~80字の日本語で要約せよ。句読点も字数に含める。"
print(qa.run(query))
```
各コードの説明は以下のとおりです。
1. `qa = RetrievalQA.from_chain_type(llm=OpenAI(), chain_type="stuff", retriever=docsearch.as_retriever())`: RetrievalQAというクラスのfrom_chain_typeメソッドを呼び出しています。このメソッドは、与えられたパラメータを用いて質問応答(QA)システムを作成します。具体的には、llm=OpenAI()でOpenAIの言語モデルを指定し、chain_type="stuff"でチェインタイプを指定し、retriever=docsearch.as_retriever()で文書検索のためのインデックス（先程作成したもの）を指定しています。
1. `print(qa.run(query))`: 作成したQAシステムにクエリを投げて、その回答を表示しています。ここでのクエリは「英文を読み、その内容を70~80字の日本語で要約せよ。句読点も字数に含める。」となっています。

## 模範解答と実行結果の比較
それでは、模範解答と実行結果で返ってきた結果を比較してみましょう。
与えられた英文を70~80字の日本語で要約して答える形式となっています。

### 模範解答
模範解答はこちらです。
>極的な食の分配は人間の特性である。その必要性が人間のあらゆる活動の原動力であり、文明の基礎であるがゆえに、食は栄養摂取以上の様々な意味を持つに至っている。(78字)

あの量の英文をこれだけ少ない文字数で上手く要約するのは、難易度が高いですね。さすが東大です。

### queryを「英文を読み、その内容を70~80字の日本語で要約せよ。句読点も字数に含める。」にした場合
以下のような結果が返ってきました。

>人間社会が存続するためには、食べ物を活発に分配することが必要である。現代では、食べ物を分配することが私たち人間を動物と区別する原因と考えられている。人間特有の特徴を作り出す上で重要な役割を果たしていることが分かる。その上、食べ物に対する選好の概念もあり、食べ物は生理的な栄養素だけでなく、多重の意味を持つものとなっている。(161文字)

キーワードは上手く抽出できていると思われます。ですが、70~80文字の文字数制限を満たしていません。何回か再実行してみましたが、70~80字の回答は得られませんでした。

厳密な部分に関しては、東大の先生にお伺いしないと分かりませんが、試しにOpenAIのEmbeddingsを使って類似度を算出してみました。

```python:test2.py
import openai

openai.api_key = "[OpenAIのAPIキー]"

input1 = "極的な食の分配は人間の特性である。その必要性が人間のあらゆる活動の原動力であり、文明の基礎であるがゆえに、食は栄養摂取以上の様々な意味を持つに至っている。"
input2 = "人間社会が存続するためには、食べ物を活発に分配することが必要である。現代では、食べ物を分配することが私たち人間を動物と区別する原因と考えられている。人間特有の特徴を作り出す上で重要な役割を果たしていることが分かる。その上、食べ物に対する選好の概念もあり、食べ物は生理的な栄養素だけでなく、多重の意味を持つものとなっている。"

response1 = openai.Embedding.create(
    model='text-embedding-ada-002',
    input=input1
)

response2 = openai.Embedding.create(
    model='text-embedding-ada-002',
    input=input2
)

from openai.embeddings_utils import cosine_similarity
result = cosine_similarity(response1["data"][0]['embedding'], response2["data"][0]['embedding'])
print(result)
```
text-embedding-ada-002というモデルを用いて、模範解答と解答をベクトル化しています。そして、コサイン類似度関数を用いてベクトル化された文章同士の類似度を算出しています。

結果は、 **「0.9405210931909826」** でした。文字数をオーバーしているので、実際の試験では減点もしくは不正解となりそうですが、意味合いは似ているようです。


### queryを「Read the English text and summarize its contents in 70~80 characters in Japanese. Punctuation should be included in the number of characters.」にした場合
日本語では文字数を超えてしまったため、英訳して質問してみました。
以下のような結果が返ってきました。

>人類が食べ物を共有して文明を築いたことから、テーブルマナーなどの食事ルールが生まれた。食べ物は身体的な栄養だけではなく 、多様な意味を持つようになった。(75字)

2~3回目でこの結果が返ってきました。英語で質問すると指定の文字数に近づくという傾向が見られました。キーワードや内容も近いものを返していると思われます。

こちらもOpenAIのEmbeddingsを使って類似度を算出してみました。
先ほどのtest.py2のコードのinput2の値を「人類が食べ物を共有して文明を築いたことから、テーブルマナーなどの食事ルールが生まれた。食べ物は身体的な栄養だけではなく 、多様な意味を持つようになった。」に書き換えて実行しました。

結果は **「0.9045712801506784」** でした。先ほどよりも低い数値になりましたが、文字数制限を守っているため、実際の試験では、先ほどの解答より高い得点となりそうです。


## おわりに
今回は、LangChainの文書検索を用いて、東大入試の英語要約問題を解かせてみました。
厳密な得点に関しては、東大の先生に伺わないと分かりませんが、OpenAIのEmbeddingを使った類似度判定では高い評価が出ました。

最後までお読みいただき、ありがとうございました！

記事に関する質問等ございましたら、コメントまたは以下のDMにてよろしくお願いします！

https://twitter.com/t___tatsuki

## 参考文献

https://python.langchain.com/docs/use_cases/question_answering/vector_db_qa#return-source-documents