特許情報を用いたOllamaモデルの性能評価：llmとEmbeddingの違い

![aozora.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3784222/28600395-305e-c9dd-257e-c338c12cdbd2.jpeg)

# はじめに

以前、私の投稿で**Ollama**の**llm**モデルと**embedding**モデルを使って、自分のPCで**RAG**（Retrieval-Augmented Generation）を実現したことについて書きました。

https://qiita.com/ogi_kimura/items/7e2e00049ba14aca716a

https://qiita.com/ogi_kimura/items/3740e300e946b33bae00

その後、@SNAMGNさんから貴重なアドバイスをいただき、**Ollama**にはさまざまなモデルが存在することを教えていただきました。私自身も確認したところ、確かに多くのモデルがあることがわかりました。

そこで今回は、**Ollama**に登録されている**llm**と**embedding**モデルを採用し、「処理速度」と「回答の正確性」について比較検証を行うことにしました。




# 私のPCスペック

まずは、私が使用しているPCのスペックをご紹介します。以前の記事でも触れましたが、メモリが8GBでは不足するため、スワップ領域を19,456MBに設定して**Ollama**を何とか動作させることができました。

https://qiita.com/ogi_kimura/items/45dffc2bc8334561a432

| リソース | 詳細 |
|:-:|:-|
| CPU | Intel(R) Core(TM) i3-8145U CPU @ 2.10GHz |
| GPU | なし |
| メモリ | 8GB |
| SSD | 1TB |
| スワップ領域 | 19,456MB |

このスペックで検証に挑戦します。





# モデルランキング

**Ollama**のサイトから、**llm**および**embedding**モデルの一覧を以下に示します。

https://ollama.com/

モデルが豊富に存在していたため、今回はダウンロード数が多いものをピックアップしました。なお、これは2024年10月14日時点での情報です。



## llmモデル

「対象」に「〇」マークを付けたものが、今回の比較対象となるモデルです。ダウンロード数（pull数）が多いモデルを選定しています。

| モデル名 | ダウンロード数 | アップデート | 説明 | 対象 |
|:-:|-:|:-:|:-|:-:|
| llama3.2  | 610,100  | 2 weeks ago  | Meta's Llama 3.2 goes small with 1B and 3B models.  | 〇  |
| llama3.1  | 6,200,000  | 3 weeks ago  | Llama 3.1 is a new state-of-the-art model from Meta available in 8B, 70B and 405B parameter sizes.  |  〇 |
| qwen2.5  |  779,000 | 3 weeks ago  | Qwen2.5 models are pretrained on Alibaba's latest large-scale dataset, encompassing up to 18 trillion tokens. The model supports up to 128K tokens and has multilingual support.   | 〇  |
| nemotron-mini  | 16,600  | 3 weeks ago  | A commercial-friendly small language model by NVIDIA optimized for roleplay, RAG QA, and function calling.  |   |
| mistral-small  | 19,200  | 3 weeks ago | Mistral Small is a lightweight model designed for cost-effective use in tasks like translation and summarization.  |   |
| mistral-nemo  | 298,400  | 2 weeks ago  | A state-of-the-art 12B model with 128k context length, built by Mistral AI in collaboration with NVIDIA.  |   |
| mistral  | 3,900,000  | 4 months ago  | The 7B model released by Mistral AI, updated to version 0.3.  |  〇 |
| mixtral  | 446,200  | 5 months ago  |  A set of Mixture of Experts (MoE) model with open weights by Mistral AI in 8x7b and 8x22b parameter sizes. |   |
| command-r  | 227,900  | 6 weeks ago  | Command R is a Large Language Model optimized for conversational interaction and long context tasks.  |   |
| command-r-plus  | 99,200  |  6 weeks ago |  Command R+ is a powerful, scalable large language model purpose-built to excel at real-world enterprise use cases. |   |
| qwen2  | 3,900,000  | 4 months ago  | Qwen2 is a new series of large language models from Alibaba group  | 〇  |
| qwen2.5-coder  | 206,400  | 3 weeks ago  |  Qwen2 is a new series of large language models from Alibaba group |   |
| mistral-large  | 87,700  | 2 months ago  | Mistral Large 2 is Mistral's new flagship model that is significantly more capable in code generation, mathematics, and reasoning with 128k context window and support for dozens of languages.  |   |
| gdisney/mistral-large-uncensored  | 53,500  |  2 months ago |   |   |
| hermes3  |  43,800 |  6 weeks ago |  Hermes 3 is the latest version of the flagship Hermes series of LLMs by Nous Research |   |
| llama3-groq-tool-use  | 30,100  | 2 months ago  | A series of models from Groq that represent a significant advancement in open-source AI capabilities for tool use/function calling. |   |



## embeddingモデル

「対象」に「〇」マークを付けたものが、今回の比較対象となるモデルです。ダウンロード数（pull数）が多いモデルを選定しています。

| モデル名 | ダウンロード数 | アップデート | 説明 | 対象 |
|:-:|-:|:-:|:-|:-:|
| nomic-embed-text  |  715,100 |  7 months ago |  A high-performing open embedding model with a large token context window. |  〇 |
| mxbai-embed-large  |  448,100 |  6 months ago |  State-of-the-art large embedding model from mixedbread.ai | 〇 |
| snowflake-arctic-embed  | 158,000  | 5 months ago  |  A suite of text embedding models by Snowflake, optimized for performance. | ×(エラーのため)  |
| all-minilm  | 107,400  | 7 months ago  |  Embedding models on very large sentence level datasets. | 〇  |
|  unclemusclez/jina-embeddings-v2-base-code | 52,400  | 3 months ago  |  (Updated: 07/1/2024) https://huggingface.co/jinaai/jina-embeddings-v2-base-code | 〇  |
| hellord/mxbai-embed-large-v1  | 41,200  |  6 months ago |   |  〇 |
| znbang/bge  |  21,500 |  7 months ago | BAAI General Embedding  |   |
| bge-m3  |  20,000 | 2 months ago  | BGE-M3 is a new model from BAAI distinguished for its versatility in Multi-Functionality, Multi-Linguality, and Multi-Granularity.  | 〇  |
| shaw/dmeta-embedding-zh  |  16,500 |  6 months ago | https://huggingface.co/DMetaSoul/Dmeta-embedding-zh  |   |
| jina/jina-embeddings-v2-base-de  | 13,800  |  4 months ago |  Text embedding model (base) for English and German input of size up to 8192 tokens |   |
| chroma/all-minilm-l6-v2-f32  | 11,000  | 6 months ago  |   |   |
| bge-large  | 7,501  | 2 months ago  | Embedding model from BAAI mapping texts to vectors.  |   |
| quentinz/bge-large-zh-v1.5  |  4,642 |  2 months ago |   |   |
| paraphrase-multilingual  | 4,361  | 2 months ago  | Sentence-transformers model that can be used for tasks like clustering or semantic search.  |   |
| milkey/m3e  |  2,364 | 6 months ago  | Moka-AI Massive Mixed Embedding  |   |

ただし、`snowflake-arctic-embed`は`retrieve`時にエラーが発生したため、今回は比較対象から除外しました。


# 評価基準

今回の評価基準は以下の通りです。

## データベース登録処理
ベクトルデータベースとして`Chroma`を採用し、インプット資料として「特許情報xml」ファイルを使用しました。このxmlファイルを読み込んで、`Chroma`のデータベースへの登録が完了するまでの時間を評価基準としました。

## 質問応答処理
「データベース登録処理」で作成したベクトルデータベースを基に、ユーザの質問に対する回答時間を評価しました。また、その回答内容が期待に沿っているかを、以下の4段階で評価しました。

### 回答内容の評価基準

| 得点 | 説明 |
|:-:|:-|
| 3 | 正確に要約されている |
| 2 | 観点は異なるが、納得できる要約 |
| 1 | キーワードのみ含まれている要約 |
| 0 | キーワードすら含まれていない |

また、評価する質問内容は以下の4つとしました。

| No. | 質問内容 |
|:-:|:-|
| 1 | 「組合せ処置およびその方法」の概要を教えて |
| 2 | 「フューリンインヒビター」の概要を教えて |
| 3 | 用語「アルキル」の意味は？ |
| 4 | 用語「アゴニスト」とは？ |




# プログラム

「データベース登録処理」と「質問応答処理」について、以下にPythonのプログラムを紹介します。  
各プログラムのモデル宣言部分はコメントアウトしていますので、必要に応じてコメントを外して実行してください。

## データベース登録処理

```python:chroma_retriever.py
import glob
import os
import xml.etree.ElementTree as ET
from dotenv import load_dotenv
from langchain.text_splitter import CharacterTextSplitter
#from langchain.embeddings.openai import OpenAIEmbeddings
#from langchain.vectorstores import Chroma
from langchain_chroma import Chroma
import ollama
from datetime import datetime

load_dotenv()

docs = []

# 取り出したい名前空間-タグ名
name_spaces_tag_names = [
    "{http://www.wipo.int/standards/XMLSchema/ST96/Common}PublicationNumber",
    "{http://www.wipo.int/standards/XMLSchema/ST96/Common}PublicationDate",
    "{http://www.wipo.int/standards/XMLSchema/ST96/Common}RegistrationDate",
    "{http://www.wipo.int/standards/XMLSchema/ST96/Common}ApplicationNumberText",
    "{http://www.wipo.int/standards/XMLSchema/ST96/Common}PartyIdentifier",
    "{http://www.wipo.int/standards/XMLSchema/ST96/Common}EntityName",
    "{http://www.wipo.int/standards/XMLSchema/ST96/Common}PostalAddressText",
    "{http://www.wipo.int/standards/XMLSchema/ST96/Common}PatentCitationText",
    "{http://www.wipo.int/standards/XMLSchema/ST96/Common}PersonFullName",
    "{http://www.wipo.int/standards/XMLSchema/ST96/Common}P",
    "{http://www.wipo.int/standards/XMLSchema/ST96/Common}FigureReference",
    "{http://www.wipo.int/standards/XMLSchema/ST96/Patent}PlainLanguageDesignationText",
    "{http://www.wipo.int/standards/XMLSchema/ST96/Patent}FilingDate",
    "{http://www.wipo.int/standards/XMLSchema/ST96/Patent}InventionTitle",
    "{http://www.wipo.int/standards/XMLSchema/ST96/Patent}MainClassification",
    "{http://www.wipo.int/standards/XMLSchema/ST96/Patent}FurtherClassification",
    "{http://www.wipo.int/standards/XMLSchema/ST96/Patent}PatentClassificationText",
    "{http://www.wipo.int/standards/XMLSchema/ST96/Patent}SearchFieldText",
    "{http://www.wipo.int/standards/XMLSchema/ST96/Patent}ClaimText",
]

# 埋め込み関数のラッパーを作成
class OllamaEmbeddingFunction:
    def __init__(self, model):
        self.model = model
    def embed_documents(self, texts):
        embeddings = []
        for text in texts:
            response = ollama.embeddings(model=self.model, prompt=text)
            embeddings.append(response['embedding'])
        return embeddings  # ここで計算した埋め込みを返します

def set_element(level, trees, el):
    trees.append({"tag" : el.tag, "attrib" : el.attrib, "content_page" :el.text})

def set_child(level, trees, el):
    set_element(level, trees, el)
    for child in el:
        set_child(level+1, trees, child)

def parse_and_get_element(input_file):
    tmp_elements = []
    new_elements = []
    tree = ET.parse(input_file)
    root = tree.getroot()
    set_child(1, tmp_elements, root)
    for name_space_tag_name in name_spaces_tag_names:
        for tmp_element in tmp_elements:
            if tmp_element["tag"] == name_space_tag_name:
                new_elements.append(tmp_element)
    return new_elements

def execute():
    title = ""
    entryName = ""
    patentCitationText = ""

    files = glob.glob(os.path.join("C:/Users/ogiki/JPB_2024999", "**/*.*"), recursive=True)
    for file in files:
        base, ext = os.path.splitext(file)
        if ext == '.xml':
            topic_name = os.path.splitext(os.path.basename(file))[0]
            print(file)

            text_splitter = CharacterTextSplitter(chunk_size=300, chunk_overlap=0)
            new_elements = parse_and_get_element(file)
            for new_element in new_elements:
                try:
                    text = new_element["content_page"]
                    tag = new_element["tag"]
                    title = text if tag == "{http://www.wipo.int/standards/XMLSchema/ST96/Patent}InventionTitle" else ""
                    entryName = text if tag == "{http://www.wipo.int/standards/XMLSchema/ST96/Common}EntityName" else ""
                    patentCitationText = text if tag == "{http://www.wipo.int/standards/XMLSchema/ST96/Common}PatentCitationText" else ""

                    documents = text_splitter.create_documents(texts=[text], metadatas=[{
                        "name": topic_name, 
                        "source": file, 
                        "tag": tag, 
                        "title": title,
                        "entry_name": entryName, 
                        "patent_citation_text" : patentCitationText}]
                    )
                    docs.extend(documents)
                except Exception as e:
                    continue

    # OllamaEmbeddingFunctionのインスタンスを作成
    # embedding_function = OllamaEmbeddingFunction(model='nomic-embed-text')
    # db = Chroma(persist_directory="C:/Users/ogiki/vectorDB/nomic-embed-text", embedding_function=embedding_function)
    # embedding_function = OllamaEmbeddingFunction(model='mxbai-embed-large')
    # db = Chroma(persist_directory="C:/Users/ogiki/vectorDB/mxbai-embed-large", embedding_function=embedding_function)
    # embedding_function = OllamaEmbeddingFunction(model='snowflake-arctic-embed')
    # db = Chroma(persist_directory="C:/Users/ogiki/vectorDB/snowflake-arctic-embed", embedding_function=embedding_function)
    # embedding_function = OllamaEmbeddingFunction(model='all-minilm')
    # db = Chroma(persist_directory="C:/Users/ogiki/vectorDB/all-minilm", embedding_function=embedding_function)
    # embedding_function = OllamaEmbeddingFunction(model='unclemusclez/jina-embeddings-v2-base-code')
    # db = Chroma(persist_directory="C:/Users/ogiki/vectorDB/unclemusclez_jina-embeddings-v2-base-code", embedding_function=embedding_function)
    embedding_function = OllamaEmbeddingFunction(model='bge-m3')
    db = Chroma(persist_directory="C:/Users/ogiki/vectorDB/bge-m3", embedding_function=embedding_function)

    intv = 500
    ln = len(docs)
    max_loop = int(ln / intv) + 1
    for i in range(max_loop):
        splitted_documents = text_splitter.split_documents(docs[intv * i : intv * (i+1)])
        db.add_documents(splitted_documents)

if __name__ == "__main__":
    formatted_time = datetime.now().strftime("%H:%M:%S")
    print("開始時刻:", formatted_time)
    execute()
    formatted_time = datetime.now().strftime("%H:%M:%S")
    print("修了時刻:", formatted_time)

```
プログラムの詳細な説明については、過去の投稿記事をご覧いただければ幸いです。  
ご不明な点がありましたら、ぜひお気軽にご質問ください。


## 質疑応答処理

```python:chroma_streamlit.py
import streamlit as st
from langchain_community.chat_models.ollama import ChatOllama
from langchain.prompts import PromptTemplate
from langchain.schema import HumanMessage
from langchain.vectorstores import Chroma
import ollama
from datetime import datetime

# 埋め込み関数のラッパーを作成
class OllamaEmbeddingFunction:
    def __init__(self, model):
        self.model = model
    def embed_documents(self, texts):
        embeddings = []
        for text in texts:
            response = ollama.embeddings(model=self.model, prompt=text)
            embeddings.append(response['embedding'])
        return embeddings  # ここで計算した埋め込みを返します
    def embed_query(self, query):
        response = ollama.embeddings(model=self.model, prompt=query)
        return response['embedding']  # クエリの埋め込みを返す


# embedding_function = OllamaEmbeddingFunction(model='nomic-embed-text')
# embedding_function = OllamaEmbeddingFunction(model='mxbai-embed-large')
# embedding_function = OllamaEmbeddingFunction(model='snowflake-arctic-embed')
# embedding_function = OllamaEmbeddingFunction(model='all-minilm')
# embedding_function = OllamaEmbeddingFunction(model='unclemusclez/jina-embeddings-v2-base-code')
embedding_function = OllamaEmbeddingFunction(model='bge-m3')
chat = ChatOllama(model="llama3.2", temperature=0)
# chat = ChatOllama(model="llama3.1", temperature=0)
# chat = ChatOllama(model="qwen2.5", temperature=0)
# chat = ChatOllama(model="mistral", temperature=0)
# chat = ChatOllama(model="qwen2", temperature=0)
database = Chroma(
    # persist_directory="C:/Users/ogiki/vectorDB/nomic-embed-text", 
    # persist_directory="C:/Users/ogiki/vectorDB/mxbai-embed-large", 
    # persist_directory="C:/Users/ogiki/vectorDB/snowflake-arctic-embed", 
    # persist_directory="C:/Users/ogiki/vectorDB/all-minilm", 
    # persist_directory="C:/Users/ogiki/vectorDB/unclemusclez/jina-embeddings-v2-base-code", 
    persist_directory="C:/Users/ogiki/vectorDB/bge-m3", 
    embedding_function=embedding_function
)

prompt = PromptTemplate(template="""文章を元に質問に答えてください。 

文章: 
{document}

質問: {query}
""", input_variables=["document", "query"])


# =====================================================
st.title("特許検索システム")

if "messages" not in st.session_state:
    st.session_state.messages = []

for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

input_message = st.chat_input("準備ができました！メッセージを入力してください！")
text_input = st.text_input("ここに番号を入力してください")

if input_message:
    formatted_time = datetime.now().strftime("%H:%M:%S")
    print("開始時刻:", formatted_time)
    st.session_state.messages.append({"role": "user", "content": input_message})
    print(f"入力されたメッセージ: {input_message}")
    
    with st.chat_message("user"):
        st.markdown(input_message)

    with st.chat_message("assistant"):
        # ----- VectorDBからドキュメントを取得 (ローカルEmbeddingを利用) -----
        documents = database.similarity_search_with_score(input_message, k=3, filter={"name":text_input})
        documents_string = ""
        for document in documents:
            print("---------------document.metadata---------------")
            print(document[0].metadata)
            print(document[1])
            documents_string += f"""
                ---------------------------
                {document[0].page_content}
                """
        print("---------------documents_string---------------")
        print(input_message)
        print(documents_string)
        # ----- プロンプトを基に回答をもらう (ローカルLLMを利用) -----
        result = chat([
            HumanMessage(content=prompt.format(document=documents_string,
                                            query=input_message))
        ])
        st.markdown(result.content)
        st.session_state.messages.append({"role": "assistant", "content": result.content})
    formatted_time = datetime.now().strftime("%H:%M:%S")
    print("修了時刻:", formatted_time)

```
このプログラムの詳細な説明についても、過去の投稿記事をご覧いただければ幸いです。  
ご不明な点がありましたら、ぜひお気軽にご質問ください。



# 比較結果（Embedding）

まずは`embedding`モデルの性能比較を行いました。今回の比較では、**llm**に`llama3.2`を使用しています。

## データベース登録処理（秒）

| モデル | nomic-embed-text | mxbai-embed-large | all-minilm | unclemusclez/jina-embeddings-v2-base-code | bge-m3 |
|:-:|:-:|:-:|:-:|:-:|:-:|
| 処理時間 | 1,142 | 2,529 | 94 | 92 | 2,984 |

この結果から、処理速度においては`all-minilm`と`unclemusclez/jina-embeddings-v2-base-code`が最も優れていることがわかります。最速のモデルと最も遅いモデルでは、処理時間に20倍以上の差がありました。

## 質疑応答処理（秒）

| 質問内容 | nomic-embed-text | mxbai-embed-large | all-minilm | unclemusclez/jina-embeddings-v2-base-code | bge-m3 |
|:-|:-:|:-:|:-:|:-:|:-:|
| 「組合せ処置およびその方法」の概要を教えて | 107 | 91 | 64 | 44 | 64 |
| 「フューリンインヒビター」の概要を教えて | 104 | 31 | 19 | 86 | 173 |
| 用語「アルキル」の意味は？ | 45 | 39 | 25 | 17 | 57 |
| 「アゴニスト」の意味は？ | 120 | 124 | 141 | 10 | 135 |
| **平均** | **94.0** | **71.3** | **62.3** | **39.3** | **107.3** |

この結果では、平均応答時間が最も短い`unclemusclez/jina-embeddings-v2-base-code`が優れた結果を示しました。`all-minilm`も比較的速い応答速度を見せています。

## 回答内容の精度

次に、回答内容の精度を評価した結果です。

| 質問内容 | nomic-embed-text | mxbai-embed-large | all-minilm | unclemusclez/jina-embeddings-v2-base-code | bge-m3 |
|:-|:-:|:-:|:-:|:-:|:-:|
| 「組合せ処置およびその方法」の概要を教えて | 2 | 3 | 3 | 0 | 1 |
| 「フューリンインヒビター」の概要を教えて | 2 | 1 | 0 | 1 | 3 |
| 用語「アルキル」の意味は？ | 3 | 3 | 3 | 1 | 3 |
| 「アゴニスト」の意味は？ | 2 | 2 | 2 | 0 | 2 |
| **平均** | **2.3** | **2.3** | **2.0** | **0.5** | **2.3** |

この結果から、`nomic-embed-text`、`mxbai-embed-large`、`bge-m3`が精度面で優れた結果を示しました。一方、`unclemusclez/jina-embeddings-v2-base-code`は応答速度は速いものの、回答精度が低いことがわかります。`all-minilm`は、応答速度が比較的速く、精度もまずまずの結果でした。


# 比較結果 (llm)

次に、**llm**の比較結果を見てみましょう。今回は**embedding**モデルを`nomic-embed-text`に固定し、異なる**llm**を用いてその性能を確認しました。

## 質疑応答処理 (秒)
| 質問文章| llama3.2 | llama3.1 | qwen2.5 | mistral | qwen2 |
|:-|-:|-:|-:|-:|-:|
|「組合せ処置およびその方法」の概要を教えて | 107 |  374 |  306 |  552 |  726 |
|「フューリンインヒビター」の概要を教えて |  104 |  116 |  278 |  232 |  289 |
|用語「アルキル」の意味は？ |  45 |  226 |  159 |  196 |  157 |
|「組合せ処置およびその方法」の概要を教えて |  120 |  307 |  690 |  900 |  419 |
|平均 |  94.0 |  255.5 |  358.3 |  470.0 |  397.8 |

`llama3.2`は圧倒的に速く、`llama3.1`と比べても2倍以上の処理速度を誇っています。

## 回答内容精度

次に、回答内容の精度についての結果を示します。

| 質問文章| llama3.2 | llama3.1 | qwen2.5 | mistral | qwen2 |
|:-|-:|-:|-:|-:|-:|
|「組合せ処置およびその方法」の概要を教えて | 2 |  1 |  3 |  1 |  1 |
|「フューリンインヒビター」の概要を教えて |  2 |  0 |  3 |  1 |  1 |
|用語「アルキル」の意味は？ |  3 |  3 |  3 |  3 |  3 |
|「組合せ処置およびその方法」の概要を教えて |  2 |  1 |  2 |  1 |  3 |
|平均 |  2.3 |  1.3 |  2.8 |  1.5 |  2.0 |

`qwen2.5`が圧倒的に高い精度を示しました。一方で、`llama3.2`も注目に値します。`llama3.1`と比較すると、処理速度と回答精度の両面で向上が見られました。



# まとめ

**Embedding**モデルと**llm**モデルについて、それぞれの最適な選択肢を検討しました。

## Embeddingモデル
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3784222/7ea268f7-a3fa-bad2-0ee9-a7142dee5c63.png)

処理速度を最優先に考える場合は、`unclemusclez/jina-embeddings-v2-base-code`が適しているかもしれません。一方、回答精度を重視するのであれば、`nomic-embed-text`、`mxbai-embed-large`、または`bge-m3`の使用をおすすめします。少しでも処理速度を向上させたい方には、`all-minilm`も選択肢として考えられます。

## llmモデル
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3784222/381d7fda-a6f3-4e09-c005-f86f7273e26b.png)

処理速度を重視する場合には`llama3.2`が適しており、回答精度を重視する場合は`qwen2.5`が推奨されます。全体的に見ると、`llama3.2`がバランスの取れた選択肢と言えるでしょう。

他に試してほしいモデルがあれば、お気軽にご連絡ください。ぜひ試してみたいと思います。最後までお読みいただき、ありがとうございました。