llama_index [Bug]:BM25Retriever无法在中文上工作

a0x5cqrl  于 2个月前  发布在  其他
关注(0)|答案(9)|浏览(39)

Bug描述

BM25Retriever无法在中文上工作。

版本

主版本

重现步骤

from llama_index.retrievers.bm25 import BM25Retriever
from llama_index.core import Document
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.response.notebook_utils import display_source_node

documents = [Document(text="床前明月光"),
             Document(text="疑是地上霜"),
             Document(text="举头望明月"),
             Document(text="低头思故乡")]

splitter = SentenceSplitter(chunk_size=1024)
nodes = splitter.get_nodes_from_documents(documents)
retriever = BM25Retriever.from_defaults(nodes=nodes, similarity_top_k=2)

nodes = retriever.retrieve("故乡")
for node in nodes:
    display_source_node(node)

相关日志/回溯

output:

Node ID: d3d59a82-e489-47da-8229-a430c7f58c00 Similarity: 0.0 Text: 床前明月光
Node ID: 6105e407-408e-48a5-af44-a82515babb56 Similarity: 0.0 Text: 疑是地上霜
Node ID: fd059a68-d496-4a61-847f-c96e108a69b2 Similarity: 0.0 Text: 举头望明月
Node ID: f19761bc-d381-4679-bc77-f32cbad57db5 Similarity: 0.0 Text: 低头思故乡
chhqkbe1

chhqkbe11#

你好!我是Dosu。作为一个机器人,我可以帮你解决bug,回答问题,还可以帮你成为贡献者。在等待人类维护者的时候,我会全程帮助你。
要解决BM25Retriever未正确处理中文文本的问题,你需要修改分词器以适当处理中文文本。当前的分词器是针对英文文本设计的,不适合中文。
以下是如何修改分词器以处理中文文本的方法:

  1. 安装jieba库,这是一个流行的中文文本分割库:
pip install jieba
  1. 为中文文本定义一个新的分词器函数:
import jieba
from typing import List

def chinese_tokenizer(text: str) -> List[str]:
    # Use jieba to segment Chinese text
    return list(jieba.cut(text))
  1. 在初始化BM25Retriever时使用这个新的分词器:
from llama_index.retrievers.bm25 import BM25Retriever
from llama_index.core import Document
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.response.notebook_utils import display_source_node

documents = [Document(text="床前明月光"),
             Document(text="疑是地上霜"),
             Document(text="举头望明月"),
             Document(text="低头思故乡")]

splitter = SentenceSplitter(chunk_size=1024)
nodes = splitter.get_nodes_from_documents(documents)

retriever = BM25Retriever.from_defaults(
    nodes=nodes,
    similarity_top_k=2,
    tokenizer=chinese_tokenizer
)

nodes = retriever.retrieve("故乡")
for node in nodes:
    display_source_node(node)

通过使用chinese_tokenizer,BM25Retriever将能够正确处理中文文本并计算有意义的相似度分数1(https://github.com/run-llama/llama_index/blob/main/llama-index-legacy/llama_index/legacy/retrievers/bm25_retriever.py)2(https://github.com/run-llama/llama_index/blob/main/llama-index-integrations/retrievers/llama-index-retrievers-bm25/llama_index/retrievers/bm25/base.py)。

  • 要继续与我对话,请提及@dosu。*
xmjla07d

xmjla07d2#

我认为应该在文档中提到非英文的分词器选项。

egdjgwm8

egdjgwm83#

你认为有必要添加以下代码吗,它可以有效地实现一个能够分割中英文关键词的分词器:

import re
from typing import List, Any

import jieba
import snowballstemmer

CHINESE_CHAR_RE = re.compile(r'[\u4e00-\u9fff]')
STEMMER = snowballstemmer.stemmer('english')

WORDS_TO_IGNORE = [
    '', '\\t', '\\n', '\\\\', '\\', '', '\n', '\t', '\\', ' ', ',', ',', ';', ';', '/', '.', '。', '-', 'is', 'are',
    'am', 'what', 'how', '的', '吗', '是', '了', '啊', '呢', '怎么', '如何', '什么', '(', ')', '(', ')', '【', '】', '[', ']', '{',
    '}', '?', '?', '!', '!', '“', '”', '‘', '’', "'", "'", '"', '"', ':', ':', '讲了', '描述', '讲', '总结', 'summarize',
    '总结下', '总结一下', '文档', '文章', 'article', 'paper', '文稿', '稿子', '论文', 'PDF', 'pdf', '这个', '这篇', '这', '我', '帮我', '那个',
    '下', '翻译', 'i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you', "you're", "you've", "you'll",
    "you'd", 'your', 'yours', 'yourself', 'yourselves', 'he', 'him', 'his', 'himself', 'she', "she's", 'her', 'hers',
    'herself', 'it', "it's", 'its', 'itself', 'they', 'them', 'their', 'theirs', 'themselves', 'what', 'which', 'who',
    'whom', 'this', 'that', "that'll", 'these', 'those', 'am', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
    'have', 'has', 'had', 'having', 'do', 'does', 'did', 'doing', 'a', 'an', 'the', 'and', 'but', 'if', 'or', 'because',
    'as', 'until', 'while', 'of', 'at', 'by', 'for', 'with', 'about', 'against', 'between', 'into', 'through', 'during',
    'before', 'after', 'above', 'below', 'to', 'from', 'up', 'down', 'in', 'out', 'on', 'off', 'over', 'under', 'again',
    'further', 'then', 'once', 'here', 'there', 'when', 'where', 'why', 'how', 'all', 'any', 'both', 'each', 'few',
    'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very',
    's', 't', 'can', 'will', 'just', 'don', "don't", 'should', "should've", 'now', 'd', 'll', 'm', 'o', 're', 've', 'y',
    'ain', 'aren', "aren't", 'couldn', "couldn't", 'didn', "didn't", 'doesn', "doesn't", 'hadn', "hadn't", 'hasn',
    "hasn't", 'haven', "haven't", 'isn', "isn't", 'ma', 'mightn', "mightn't", 'mustn', "mustn't", 'needn', "needn't",
    'shan', "shan't", 'shouldn', "shouldn't", 'wasn', "wasn't", 'weren', "weren't", 'won', "won't", 'wouldn',
    "wouldn't", '说说', '讲讲', '介绍', 'summary'
]

def has_chinese_chars(data: Any) -> bool:
    text = f'{data}'
    return bool(CHINESE_CHAR_RE.search(text))
    
def string_tokenizer(text: str) -> List[str]:
    text = text.lower()
    if has_chinese_chars(text):
        _wordlist = list(jieba.lcut(text.strip()))
    else:
        _wordlist = text.strip().split()
    return STEMMER.stemWords(_wordlist)
    
def zh_tokenizer(text: str) -> List[str]:
    _wordlist = string_tokenizer(text)
    wordlist = []
    for x in _wordlist:
        if x in WORDS_TO_IGNORE:
            continue
        wordlist.append(x)
    return wordlist

也许这个分词器可以取代BM25Retriever的默认分词器: tokenize_remove_stopwords

px9o7tmv

px9o7tmv4#

关于您的情况,我不确定,不同语言的分词方式可能会有所不同。但是,就我个人而言,分词器函数本身很简单:

  • 将字符串传递给该函数
  • 以某种方式对其进行分词
  • 返回一个标记列表

因此,您可以根据所使用的编程语言实现任何需要/想要的处理过程,例如分词、词干提取或词形还原,以及去除停用词等。

muk1a3rh

muk1a3rh5#

在版本0.10.57中,分词器选项被标记为已弃用,建议使用词干提取器代替。我如何使用词干提取器处理中文?

zengzsys

zengzsys6#

这是来自QQ邮箱的假期自动回复邮件。

您好,我最近正在休假中,无法亲自回复您的邮件。我将在假期结束后,尽快给您回复。

pvabu6sv

pvabu6sv7#

在0.10.57版本中,分词器选项被标记为已弃用,建议使用词干提取器代替。我该如何使用词干提取器处理中文?

这引起了我的兴趣。我检查了bm25s和一个新的BM25Retriever。我的当前理解如下:

  • bm25s具有内置的分词器,但它不支持中文(在我的情况下)和日语。因此,需要以某种方式自己实现分词器,例如在bm25s之外实现分词器,或者为bm25s创建一个子类并覆盖由自己实现的分词方法等。
  • llamaindex的BM25Retriever导入bm25s并在BM25Retriever内部调用分词器方法,这意味着bm25s不支持的其他语言作为默认也不受支持。
  • 在当前实现中,我们可以传递包含使用bm25s创建的索引的bm25s.BM25()对象,而不是llamaindex。
  • 因此,我们不使用llamaindex进行索引阶段,只用于查询阶段。

我的总结:

  • 为bm25s创建一个子类,为您的语言实现分词器,并使用它覆盖分词方法。
  • 使用bm25s从您的文档创建索引和检索器,并将其传递给新的BM25Retriever。

此外,关于词干提取器。
bm25s使用pystemmer作为可选组件,并且它不支持日语。在日语中,词干提取过于简单,我们通常使用归一化(更像是词形还原而不是词干提取),它与分词紧密相关。因此,如果我实现的话,我会在分词器的内部实现某种形式的词干提取、归一化或词形还原,而不是使用词干提取器选项。
所以目前为止,我会使用旧版本的BM25Retriever并传递我的分词器。

j1dl9f46

j1dl9f468#

我认为BM25Retriever和BM25SRetriever应该是不同的,因为它们的实现在分词方面非常不同,尤其是在日本。

qvtsj1bj

qvtsj1bj9#

感谢您的建议。是的,我同意您的看法,BM25Retriever和BM25SRetriever应该是不同的东西。最后,我终于用bm25s为自己制作了一个中文版的BM25Retriver。
如果有人需要的话,这是我的POC代码:

import json
import logging
import os

from typing import Any, Callable, Dict, List, Optional, cast
from llama_index.retrievers.bm25 import BM25Retriever
from llama_index.core.base.base_retriever import BaseRetriever
from llama_index.core.callbacks.base import CallbackManager
from llama_index.core.constants import DEFAULT_SIMILARITY_TOP_K
from llama_index.core.indices.vector_store.base import VectorStoreIndex
from llama_index.core.schema import BaseNode, IndexNode, NodeWithScore, QueryBundle
from llama_index.core.storage.docstore.types import BaseDocumentStore
from llama_index.core.vector_stores.utils import (
    node_to_metadata_dict,
    metadata_dict_to_node,
)

import bm25s
import Stemmer
import itertools
import jieba

logger = logging.getLogger(__name__)

DEFAULT_PERSIST_ARGS = {"similarity_top_k": "similarity_top_k", "_verbose": "verbose"}

DEFAULT_PERSIST_FILENAME = "retriever.json"

class ChineseBM25Retriever(BaseRetriever):
    """A BM25 retriever that uses the BM25 algorithm to retrieve nodes.

    Args:
        nodes (List[BaseNode], optional):
            The nodes to index. If not provided, an existing BM25 object must be passed.
        similarity_top_k (int, optional):
            The number of results to return. Defaults to DEFAULT_SIMILARITY_TOP_K.
        callback_manager (CallbackManager, optional):
            The callback manager to use. Defaults to None.
        objects (List[IndexNode], optional):
            The objects to retrieve. Defaults to None.
        object_map (dict, optional):
            A map of object IDs to nodes. Defaults to None.
        verbose (bool, optional):
            Whether to show progress. Defaults to False.
    """

    def _chinese_tokenizer(self, texts: List[str]) -> tuple[str]:
        # Use jieba to segment Chinese text
        rslts= tuple(itertools.chain.from_iterable(jieba.cut(text) for text in texts))
        return rslts
    
    def __init__(
        self,
        nodes: Optional[List[BaseNode]] = None,               
        similarity_top_k: int = DEFAULT_SIMILARITY_TOP_K,
        callback_manager: Optional[CallbackManager] = None,
        objects: Optional[List[IndexNode]] = None,
        object_map: Optional[dict] = None,
        verbose: bool = False,
    ) -> None:
        
        self.similarity_top_k = similarity_top_k

        with open(r'stopwords.txt', encoding='utf-8') as f:
            con = f.readlines()
            stop_words = set() 
            for i in con:
                i = i.rstrip("\n")  
                stop_words.add(i)
        self.stop_words = stop_words

        # for some terms not included in jieba
        with open(r'cn_dict.txt', encoding='utf-8') as f:
            words = f.readlines()
            for word in words:
                jieba.add_word(word)

        corpus_tokens = [
            [word for word in jieba.cut_for_search(node.get_content()) if word not in stop_words and word.strip('\n')]                                  
            for node in nodes
        ]
        self.bm25 = bm25s.BM25()
        from llama_index.core.vector_stores.utils import (
            node_to_metadata_dict
        )
        
        corpus = [node_to_metadata_dict(node) for node in nodes]
        self.bm25.corpus = corpus
        self.bm25.index(corpus_tokens, show_progress=True)

        super().__init__(
            callback_manager=callback_manager,
            object_map=object_map,
            objects=objects,
            verbose=verbose,
        )
    
    def _retrieve(self, query_bundle: QueryBundle) -> List[NodeWithScore]:
        query = query_bundle.query_str

        tokenized_query = [[word for word in jieba.cut_for_search(query) if word not in self.stop_words]]
        
        indexes, scores = self.bm25.retrieve(
            tokenized_query, k=self.similarity_top_k, show_progress=self._verbose
        )

        # batched, but only one query
        indexes = indexes[0]
        scores = scores[0]

        nodes: List[NodeWithScore] = []
        for idx, score in zip(indexes, scores):
            # idx can be an int or a dict of the node
            if isinstance(idx, dict):
                node = metadata_dict_to_node(idx)
            else:
                node_dict = self.corpus[int(idx)]
                node = metadata_dict_to_node(node_dict)
            nodes.append(NodeWithScore(node=node, score=float(score)))

        return nodes

相关问题