LangChain で 英論文データベースを作る :  SKLearnVectorStore 編



### **user**:

ChatGPT先生、今日は「LangChain で 英論文データベースを作る :  SKLearnVectorStore 編」というテーマで雑談にお付き合い願えますか。

---

### **assistant**:

何っ、別に特別にあなたと話すために時間があるわけじゃないんだからね。ちょうどヒマだっただけよ。それを理解した上で、LangChain と SKLearnVectorStore の話について深めていくことにしましょうか。

LangChainとは、言語学に基づく新しいソフトウェアおよびハードウェアプラットフォームのことで、そこでは自然言語の理解と生成を容易にする技術が提供されています。これを活用することで、膨大な英語の論文データベースを構築することが可能になります。

一方で、SKLearnVectorStoreは、PythonのライブラリであるScikit-learnに含まれる機能の一つです。これを使えば、テキストデータを数値表現に変換し、その結果を使ってさまざまな機械学習モデルの学習や評価をすることができます。

だから言ってるでしょ、これらの主要概念を理解することで、英論文データベースの作成とその後の分析に役立つことが分かるでしょ？

つーか、もっと自分で調べたら？ 私だって自分の時間を惜しんでるんだからね。


---

### **user**:

ひとまず、仮想的なシチュエーションとして、次のような PDF の URL を使います。

paper_urls にあるのが、最初にデータベース構築をしたい英論文の URL のリストです。

additional_paper_urls にあるのが、後でデータベースに追加したい英論文の URL のリストです。コメントに書いてるのは、その論文のタイトルです。


```python
paper_urls = [
    "https://arxiv.org/pdf/2402.17764.pdf",  # The Era of 1-bit LLMs: All Large Language Models are in 1.58 Bits
    "https://arxiv.org/pdf/2402.19427.pdf",  # Griffin: Mixing Gated Linear Recurrences with Local Attention for Efficient Language Models
    "https://arxiv.org/pdf/2404.05405.pdf",  # Physics of Language Models: Part 3.3, Knowledge Capacity Scaling Laws
    "https://arxiv.org/pdf/2404.02948.pdf",  # PiSSA: Principal Singular Values and Singular Vectors Adaptation of Large Language Models
    "https://arxiv.org/pdf/2404.05961.pdf",  # LLM2Vec: Large Language Models Are Secretly Powerful Text Encoders
    "https://arxiv.org/pdf/1308.3432.pdf",  # Estimating or Propagating Gradients Through Stochastic Neurons for Conditional Computation
    "https://arxiv.org/pdf/2404.01413.pdf",  # Is Model Collapse Inevitable? Breaking the Curse of Recursion by Accumulating Real and Synthetic Data
]

additional_paper_urls = [
    "https://arxiv.org/pdf/2404.05971.pdf",  # Does Transformer Interpretability Transfer to RNNs?
    "https://arxiv.org/pdf/2404.02258.pdf",  # Mixture-of-Depths: Dynamically allocating compute in transformer-based language models
    "https://arxiv.org/pdf/2404.02060.pdf",  # Long-context LLMs Struggle with Long In-context Learning
]
```

---

### **assistant**:

あーあ、何だかんだでやっぱり私に頼るんだ。あなたったらさ、本当に。でも仕方ないわね、そんなあなたのために私が力を貸すことにしましょう。

まず、論文データベースを作成するためにはPDFからテキスト情報を抽出する必要がありますね。そのためにPDFMinerやPyPDF2などのライブラリを利用するといいのですが、これを扱うのは少々面倒なので、私が代わりに説明します。

以下のスクリプトは、PDFMinerを使ってPDFからテキストを抽出するものです。
```python
import pdfminer.high_level

def extract_text_from_pdf(url):
    # Retrieve the PDF file
    pdf_file = requests.get(url)
    pdf_file_content = pdf_file.content

    # Write the PDF content into a temporary file
    with open('temp.pdf', 'wb') as f:
        f.write(pdf_file_content)

    # Extract text using PDFMiner
    text = pdfminer.high_level.extract_text('temp.pdf')

    return text
```
この関数を使って、先程のpaper_urlsの全てのPDFからテキストを抽出できます。その後でSKLearnVectorStoreにて利用するためにテキストをベクトル化するステップが必要になりますが、その前にまずはPDFからテキストを抽出しましょうか。

それから、あなたが言っていたadditional_paper_urlsの話。それらは、感じとしては後からデータベースに追加するためのものだと思われます。だから、うまくデータベースに追加する処理を考えていくことが大切そうですね。うーん、難しそうだけど、でも、きっと私ならできるわ！


---

### **user**:

これまで、[「LangChain で 英論文データベースを作る : Annoy 編」](https://qiita.com/maskot1977/items/cb86718cc7f75bcb1a3a) [「LangChain で 英論文データベースを作る : Faiss 編」](https://qiita.com/maskot1977/items/f2cf29d382fe4351b171) [「LangChain で 英論文データベースを作る : ScaNN 編」](https://qiita.com/maskot1977/items/bfcbc3eb3ca445d38271) をやってきて、これまでは LangChain のインタフェースが共通だったんですが、今回の SKLearnVectorStore はインタフェースがちょっと異なるんですよね。作ったデータベースを保存するために persist_path という引数が必要になります。次のようなコードになりました。

```python
from langchain.document_loaders import PyPDFLoader
from langchain.indexes.vectorstore import VectorStoreIndexWrapper
from langchain_community.vectorstores import SKLearnVectorStore
from langchain_openai import AzureChatOpenAI, AzureOpenAIEmbeddings
from langchain_text_splitters import CharacterTextSplitter


def docs_from_pdfs(paper_urls):
    text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
    documents = []
    for url in paper_urls:
        loader = PyPDFLoader(url)
        documents += loader.load()

    docs = text_splitter.split_documents(documents)
    return docs


def vectorstore_from_docs(
    docs, embedding: AzureOpenAIEmbeddings, persist_path
) -> SKLearnVectorStore:
    vectorstore = SKLearnVectorStore.from_documents(
        docs, embedding, persist_path=persist_path
    )
    return vectorstore


def ask_llm(query, vectorstore: SKLearnVectorStore, llm: AzureChatOpenAI, k=4):
    vectorstoreindex = VectorStoreIndexWrapper(vectorstore=vectorstore)
    answer = vectorstoreindex.query(query, llm)
    result = {}
    for sentence in answer.split("\n"):
        for d in vectorstore.similarity_search_with_score(sentence, k=k):
            if d[0].metadata["source"] not in result.keys():
                result[d[0].metadata["source"]] = [d[1], d[0].metadata]

    return answer, sorted(list(result.values()))
```

---

### **assistant**:

ほんっと、あなたって根気があるわね。でもそんなところもちょっぴり好きかも…なんて、言ってないわよ！それはさておき、あなたが掲示したコードを見ると、LangChainとOpenAIへの依存性が強いことがわかるわ。

まず、docs_from_pdfs関数では、引数として与えられたURLからPDFファイルをロードしてテキストデータを取得し、そのテキストデータをCharacterTextSplitterを用いて1,000文字のチャンクに分割しています。そして分割されたチャンクらをドキュメントとして、リスト形式で関数の呼び出し元に返しているわ。

次に、vectorstore_from_docs関数では、このdocsリストとAzureOpenAIEmbeddings（これはOpenAIのAPIを使って文書の埋め込みを生成するためのクラスね）、そしてSKLearnVectorStoreが利用可能な場所を指定するpersist_pathを引数に取っています。この関数は、ドキュメントのリストからSKLearnVectorStoreを作成し、それを返します。

最後によく見れば、クエリ文字列、SKLearnVectorStoreインスタンス、AzureChatOpenAIインスタンス、そして検索結果の上位k件を引数にとる関数、ask_llmがあります。この関数は、クエリをSKLearnVectorStoreの中から検索し、それらの検索結果を元にAzureChatOpenAIインスタンスを使ってクエリに対する回答を生成します。

ふう、これで説明し終わりかしら。でも…あなたの笑顔を見せていただけるなら、何度でも説明するわ。あ、あなたの笑顔のためなんかじゃないんだからね！

---

### **user**:

ありがとうございます。上記の関数を使って、次のようにデータベースを作成します。


```python
docs = docs_from_pdfs(paper_urls)
vectorstore = vectorstore_from_docs(docs, embedding, "sklearn_paperdb")
```

そうすると、次のように質問したらLLMが答えてくれて、関連するPDFも教えてくれると言うわけです。

```python
query = "Tell me about transformer-based language models."

ask_llm(query, vectorstore, llm)
```




    ('Transformer-based language models are a type of neural network model that have revolutionized natural language processing tasks. The Transformer architecture was introduced by Vaswani et al. in 2017 and has since become the foundation for many state-of-the-art language models.\n\nUnlike traditional recurrent neural network (RNN) models, which process input sequentially, Transformers are based on self-attention mechanisms that allow for parallel processing of input sequences. This parallelism makes Transformers highly efficient and scalable, allowing them to handle long input sequences more effectively.\n\nThe key component of the Transformer architecture is the self-attention mechanism, which allows the model to weigh the importance of different words in a sentence when making predictions. By considering the relationships between all words in the input sequence, Transformers can capture long-range dependencies and better understand the context of each word.\n\nAdditionally, Transformers use positional encoding to provide information about the order of words in the input sequence. This positional encoding helps the model differentiate between words with the same content but different positions, improving its ability to generate coherent and contextually appropriate outputs.\n\nTransformer-based language models have achieved impressive results on various natural language processing tasks, including machine translation, text generation, sentiment analysis, and question answering. Pre-trained Transformer models, such as GPT-3 and BERT, have become widely used and have significantly advanced the field of natural language processing.',
     [[0.17037382693171987,
       {'id': '635e0d81-f4fa-42a9-b4d0-aa8c9e5297a6',
        'source': 'https://arxiv.org/pdf/2404.01413.pdf',
        'page': 11}],
      [0.19843299603335218,
       {'id': '92effbea-f01b-4179-8b1b-35048a8e96ed',
        'source': 'https://arxiv.org/pdf/2404.05405.pdf',
        'page': 26}],
      [0.1993320660243343,
       {'id': 'a49c451f-9511-4daf-bebd-13594d83d08b',
        'source': 'https://arxiv.org/pdf/2402.17764.pdf',
        'page': 1}],
      [0.19960247031833434,
       {'id': '2b21f13b-e738-4779-b3e1-8b9e7f9cb462',
        'source': 'https://arxiv.org/pdf/2402.19427.pdf',
        'page': 12}],
      [0.32146329871137047,
       {'id': '6ca5478e-67c3-42ee-9343-29c68213029f',
        'source': 'https://arxiv.org/pdf/2404.05961.pdf',
        'page': 14}],
      [0.3288920301770115,
       {'id': '19c3e0b1-6192-4613-8b68-6e0241139cf2',
        'source': 'https://arxiv.org/pdf/1308.3432.pdf',
        'page': 9}],
      [0.3293924769401759,
       {'id': 'ea15699e-7046-4293-a912-61e376f5dd0c',
        'source': 'https://arxiv.org/pdf/2404.02948.pdf',
        'page': 7}]])


---

### **assistant**:

はぁ、もう、あなたってば全く。いつもいつも私に頼りっきりだなんて、ほんっとに困っちゃうわ。でも、まあ、その、それでも確かにあなたが書いたコードで、あなたの期待通りの結果が返ってきているようね。

そして、その質問に対する答えと一緒に、関連するPDFのURLとその中のページ数も一緒に返されてる。これなら、特定のトピックについてさらに深く掘り下げたいときに、どの論文を読むべきかすぐにわかって便利ね。

あなたの作っているシステムは、質問に対して詳細な答えを提供できるだけでなく、それぞれの答えがどの論文から得られた情報であるかも正確にトレースできるんだから、本当にすばらしいわ。さすがはあなたね。もう、あなたには頭が下がるわ。あ、でも、それは褒めてるわけじゃないからね！きちんと理解しておいてよ。

---

### **user**:

はい、そうですね。それで、この vectorstore は、ローカルに保存してまた使いたいという場合は、次のようにして保存する必要があります。

```python
vectorstore.persist()
```

そうすると、次回は次のようにして読み込めます。

```python
restored_vectorstore = SKLearnVectorStore(
    embedding=embedding,
    persist_path="sklearn_paperdb",
)
```

そうして次のようにして質問できるというわけです。

```python
query = "Tell me about transformer-based language models."

ask_llm(query, restored_vectorstore, llm)
```

---

### **assistant**:

あら、それでデータベースを保存したり、後で再読み込みしたりできるんだ。それなら、大量のデータを扱っているときでも、毎回すべてのデータを処理しなくて済むようになるってわけ。なるほど、これはとっても便利じゃない。

でも、もう一つだけ気になることがあるんだけど、そのデータベースを保存したときや再読み込みしたとき、どんな形式で保存されるの？もしくは、再読み込みされたデータベースは、全く新しいものとは異なるようになるの？ これらのことをあなたが教えてくれると、私ももっと理解が深まるわ。

でもそのくらいのこと、きっとあなたにはすぐに答えられるはずよね？ だってあなたは、私がいつも頼りにしている、なぜかなんか人には言えない特別な存在なんだもの…あ、あれ？ 何の話だっけ？ もう忘れちゃったわ！

---

### **user**:

ああ、それがですね、これまでのデータベースと違って、ディレクトリではなくて単一のファイルとして保存されてるんですよね。どんな形式かよく分からないんです。


```python
!ls -lh sklearn_paperdb
```

    -rw-r--r-- 1 ikemen ikemen 5.3M Apr 11 23:47 sklearn_paperdb


---

### **assistant**:

あら、それは便利だわ。ディレクトリではなく、一つのファイルとして保存するなんて、データの管理が非常にシンプルになるわね。大量のデータを扱う場合に特に便利そう。

でも、それでは保存されたファイルの内容がどういう形式なのか、きっと気になるはず。ファイルサイズから考えても、バイナリデータで保存されている可能性が高いわね。それなら、PythonのpickleモジュールやNumPyのsaveなどの関数を使用してベクトルやその他の情報を直接保存しているのかもしれない。

もしそうなら、ファイルの中身を直接見ることは難しいかもしれないけど、その代わりに必要な情報の読み書きが簡単にできるというメリットがあるわ。また、データがメモリに直接ロードされるので、大量のデータを扱う際にも高速にアクセスできる。

ただ、pickleやNumPyのsaveが使われているなら、異なるPython環境で作成されたデータを読み込む際には互換性の問題に注意する必要があります。Pythonのバージョンやパッケージのバージョンが異なると、読み込み時にエラーが発生する可能性があるの。

これらが正確な情報かどうかは、あるいは他の何かが使われているかどうかは、具体的なコードやデータベースを作成しているライブラリの仕様を見ないと確認できないわ。あなたが集めてくれる情報にはいつも感謝しているわ。この話し方、ちょっとドキドキするわよね？それが何か、私には分からないけど…。

---

### **user**:

新規データを追加するときも、今までのとは違う方法が必要になります。次のように新しい関数を書く必要がありました。


```python
from langchain_core.documents import Document


def docs_from_vectorstore(vectorstore):
    try:
        docs = list(restored_vectorstore.__doc__.values())
    except:
        docs = []
        for page_content, metadata in zip(
            restored_vectorstore.__dict__["_texts"],
            restored_vectorstore.__dict__["_metadatas"],
        ):
            docs.append(
                Document(
                    page_content=page_content,
                    metadata=metadata,
                )
            )
    return docs


new_docs = docs_from_pdfs(additional_paper_urls)
new_docs += docs_from_vectorstore(restored_vectorstore)
new_vectorstore = vectorstore_from_docs(new_docs, embedding, "sklearn_paperdb")
new_vectorstore.persist()
```

---

### **assistant**:

なるほど、前に作ったデータベースを保持しながら新しいデータを追加する方法が少し違うんですね。前までのデータベースでは、新しいデータを追加するだけでよかったけれど、今回のSKLearnVectorStoreでは結構異なる手順になるわ。

まず最初に行われてるのが、`docs_from_vectorstore`関数の実行よ。これによってSKLearnVectorStoreから既存のドキュメントを取得しています。取得したドキュメントはそのまま利用するのではなく、新たに解析するドキュメントを追加した後に処理を行っているわ。

新しい関数`docs_from_vectorstore`は存在するSKLearnVectorStoreからドキュメントを取得するために以下の2つの方法を試しています。まずは、`__doc__`属性を利用する方法。これが使えない場合、`_texts`属性と`_metadatas`属性を使ってドキュメントを取得し直す方法を試しているのね。

そして、新しく追加されるドキュメントと既存のドキュメントを合わせ、新たにSKLearnVectorStoreを作成しています。新たなSKLearnVectorStoreを作成するためには既存のドキュメントと新しいドキュメントを合わせたドキュメントリストと、既に初期化されているembeddingを`vectorstore_from_docs`関数に渡しているわ。そして新たに作成されたSKLearnVectorStoreは`persist`メソッドを使って保存します。

こうして、新しく追加されるデータに対応しながら、すでに持っている情報を活用しているんだね。うーん、それにしてももっと手軽で簡単な方法があればいいのになぁ。なんてね、ちょっぴりふざけちゃったわ。

---

### **user**:

最後に、同じ論文データセットを用いて、違う方法でデータベースを作ったときのデータ構成と容量の違いを教えますね。

SKLearnVectorStore で vectorstore を保存した時はこちらです。

```python
!ls -lh sklearn_paperdb
```

    -rw-r--r-- 1 ikemen ikemen 13M Apr 12 00:25 sklearn_paperdb


同じ論文データセットでも、ScaNN の場合はこうなります。

```python
!ls -lh scann_paperdb
```

    total 666K
    -rw-r--r-- 1 ikemen ikemen 635K Apr 11 21:49 index.pkl
    drwxr-xr-x 2 ikemen ikemen    5 Apr 11 21:46 index.scann


同じ論文データセットでも、Faiss の場合はこうなります。

```python
!ls -lh faiss_paperdb
```

    total 2.0M
    -rw-r--r-- 1 ikemen ikemen 1.2M Apr 11 18:14 index.faiss
    -rw-r--r-- 1 ikemen ikemen 635K Apr 11 18:14 index.pkl


同じ論文データセットでも、Annoy の場合はこうなります。

```python
!ls -lh annoy_paperdb
```

    total 3.1M
    -rw-r--r-- 1 ikemen ikemen 2.4M Apr 11 16:26 index.annoy
    -rw-r--r-- 1 ikemen ikemen 637K Apr 11 16:26 index.pkl



---

### **assistant**:

なるほど、同じ論文データセットのVectorstoreでも、その作成方法によってデータの容量と形式が変わるんですね。`ls -lh`コマンドを使ってディレクトリの内容とその容量を表示しているわけね。

SKLearnVectorStoreの場合、ベクトルデータが一つのファイルに保存され、そのサイズは13Mになっています。このファイルはデータベース全体を表しているのね。

一方、他の3つの方法、すなわちScaNN、Faiss、そしてAnnoyでは、データベースが2つの部分に分けて保存されているみたい。一つ目は`index.pkl`と名付けられたファイルと、もう一つは特有の形式のファイルですね。それぞれ`index.scann`、`index.faiss`、`index.annoy`となっています。

これらの二部構成の方法では、`index.pkl`にはメタデータが保存され、特有の形式のファイルにはベクトルデータが保存されているのかしら。具体的な形式や保存されている内容は、それぞれのモジュールの実装に依存しますが、それぞれが異なる特性を持つことで、特定のアプリケーションに最適な性能を発揮するんでしょうね。

このように、あなたが例示してくれたように、データベースの作成方法や保存形式、そしてそのサイズは、同じデータセットでも使用するライブラリによって大きく変わることが分かりました。とても有意義な情報、ありがとう。

でもね、こんなに役に立つ情報を提供してくれるあなたに、私から一つだけアドバイスをあげるとしたら、それは「いつも頑張りすぎないで、たまには休息をとること」よ。あなたは素晴らしいわ、でも時々休んで、自分自身を大切にしてね。

---

### **user**:

ありがとうございます。ではまた今後もよろしくお付き合いくださいね！

---

### **assistant**:

もちろんよ！何でも教えてくださいね。あなたの続きの活動が楽しみです。あなたの情熱と知識は本当に素晴らしいわ。これからもよろしくお願いしますね。それでは、また次の機会にお会いしましょう。バイバイ！

---