本文由簡體中文內容自動轉碼而成。阿里雲不保證此自動轉碼的準確性、完整性及時效性。本文内容請以簡體中文版本為準。

進階使用

更新時間:2024-07-13 00:23

前置知識

BM25簡介

BM25演算法(Best Matching 25)是一種廣泛用於資訊檢索領域的次序函數,用於在給定查詢(Query)時對一組文檔(Document)進行評分和排序。BM25在計算Query和Document之間的相似性時,本質上是依次計算Query中每個單詞和Document的相關性,然後對每個單詞的相關性進行加權求和。BM25演算法一般可以表示為如下形式:

image.svg

上式中,q d 分別表示用來計算相似性的Query和Document, qi 表示 q 的第 i 個單詞,R(qi, d) 表示單詞 qi 和文檔 d 的相關性,Wi 表示單詞 qi 的權重,計算得到的 score(q, d) 表示 q d 的相關性得分,得分越高表示 q d 越相似。Wi R(qi, d) 一般可以表示為如下形式:

image.svg

image.svg

image.svg

其中,N 表示總文檔數,N(qi) 表示包含單詞 qi 的文檔數,tf(qi, d) 表示qi 在文檔 d 中的詞頻,Ld 表示文檔 d 的長度,Lavg 表示平均文檔長度,k1 b 是分別用來控制 tf(qi, d) Ld 對得分影響的超參數。

稀疏向量產生

在檢索情境中,為了讓BM25演算法的Score方便進行計算,通常分別對Document和Query進行編碼,然後通過點積的方式計算出兩者的相似性。得益於BM25原理的特性,其原生支援將Score拆分為兩部分Sparse Vector,DashText提供了encode_document以及encode_query兩個介面來分別實現這兩部分向量的產生,其產生鏈路如下圖所示:

image.png

最終產生的稀疏向量可表示為:

image.svg

image.svg

Score/距離計算

產生dq的稀疏向量後,就可以通過簡單的點積進行距離計算,即將相同單詞上的值對應相乘再求和,通過稀疏向量計算距離的方式如下所示:

image.svg

上述計算方式本質上是通過點積來計算的,score 越大表示越相似,如果需要結合Dense Vector一起進行距離度量時,需要對齊距離度量方式。也就是說,在結合Dense Vector+Sparse Vector的情境中,距離計算只支援點積度量方式。

如何自訓練模型

考慮到內建的BM25 Model是基於通用語料(中文Wiki語料)訓練得到,在特定領域下通常不能表現出最佳的效果。因此,在一些特定情境下,通常建議訓練自訂BM25模型。使用DashText來訓練自訂模型時一般需要遵循以下步驟:

Step1:確認使用情境

當準備使用SparseVector來進行資訊檢索時,應提前考慮當前情境下的Query以及Document來源,通常需要提前準備好一定數量Document來入庫,這些Document通常需要和特定的業務情境直接相關。

Step2:準備語料

根據BM25原理,語料直接決定了BM25模型的參數。通常應按照以下幾個原則來準備語料:

  • 語料來源應儘可能反映對應情境的特性,儘可能讓 N(qi) 能夠反映對應真實情境的詞頻資訊。

  • 調節合理的語料切片長度和切片數量,避免出現語料當中只有少量長文本的情況。

一般情況下,如無特殊要求或限制,可以直接將Step1準備的一系列Document組織為語料即可。

Step3:準備Tokenizer

Tokenizer決定了分詞的結果,分詞的結果則直接影響Sparse Vector的產生,在特定領域下使用自訂Tokenizer會達到更好的效果。DashText提供了兩種擴充Tokenizer的方式:

  • 使用自訂詞表:DashText內建的Jieba Tokenizer支援傳入自訂詞表。(Java SDK暫不支援該功能)

Python
from dashtext import TextTokenizer, SparseVectorEncoder

my_tokenizer = TextTokenizer.from_pretrained(model_name='Jieba', dict='dict.txt')
my_encoder = SparseVectorEncoder(tokenize_function=my_tokenizer.tokenize)
  • 使用自訂Tokenizer:DashText支援任務自訂的Tokenizer,只需提供一個符合Callable[[str], List[str]]簽名的Tokenize函數即可。

Python
Java
from dashtext import SparseVectorEncoder
from transformers import BertTokenizer

my_tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
my_encoder = SparseVectorEncoder(tokenize_function=my_tokenizer.tokenize)
import com.aliyun.dashtext.common.DashTextException;
import com.aliyun.dashtext.common.ErrorCode;
import com.aliyun.dashtext.encoder.SparseVectorEncoder;
import com.aliyun.dashtext.tokenizer.BaseTokenizer;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Main {
    public static class MyTokenizer implements BaseTokenizer {
        @Override
        public List<String> tokenize(String s) throws DashTextException {
            if (s == null) {
                throw new DashTextException(ErrorCode.INVALID_ARGUMENT);
            }

            // 使用Regex將文本按空白符和標點符號分割,並轉換為小寫
            return Arrays.stream(s.split("\\s+|(?<!\\d)[.,](?!\\d)"))
                    .map(String::toLowerCase)
                    .filter(token -> !token.isEmpty())   // 過濾掉Null 字元串
                    .collect(Collectors.toList());
        }
    }

    public static void main(String[] args) {
        SparseVectorEncoder encoder = new SparseVectorEncoder(new MyTokenizer());
    }
}

Step4:訓練模型

實際上,這裡的“訓練”本質上是一個“統計”參數的過程。由於訓練自訂模型的過程中包含著大量Tokenizing/Hashing過程,所以可能會耗費一定的時間。DashText提供了SparseVectorEncoder.train介面可以用來訓練模型。

Step5:調參最佳化(可選)

模型訓練完成後,可以準備部分驗證資料集以及通過微調 k1 b 來達到最佳的召回效果。調節k1和b一般需要遵循以下原則:

  • 調節k1 (1.2 < k1 < 2)可控制Document詞頻對Score的影響,k1 越大Document的詞頻對Score的貢獻越小。

  • 調節b (0 < b < 1)可控制文檔長度對Score的影響,b 越大表示文檔長度對權重的影響越大

一般情況下,如無特殊要求或限制,不需要調整 k1 b

Step6:Finetune模型(可選)

實際情境下,可能會存在需要補充訓練語料來增量式地更新BM25模型參數的情況。DashText的SparseVectorEncoder.train介面原生支援模型的累加式更新。需要注意的是,模型更改之後,使用舊模型進行編碼並已入庫的向量就失去了時效性,一般需要重新入庫。

範例程式碼

以下是一個簡單完整的自訓練模型樣本。

Python
Java
from dashtext import SparseVectorEncoder
from pydantic import BaseModel
from typing import Dict, List


class Result(BaseModel):
    doc: str
    score: float


def calculate_score(query_vector: Dict[int, float], document_vector: Dict[int, float]) -> float:
    score = 0.0
    for key, value in query_vector.items():
        if key in document_vector:
            score += value * document_vector[key]
    return score


# 建立空SparseVectorEncoder(可以設定自訂Tokenizer)
encoder = SparseVectorEncoder()


# step1: 準備語料以及Documents
corpus_document: List[str] = [
    "The quick brown fox rapidly and agilely leaps over the lazy dog that lies idly by the roadside.",
    "Never jump over the lazy dog quickly",
    "A fox is quick and jumps over dogs",
    "The quick brown fox",
    "Dogs are domestic animals",
    "Some dog breeds are quick and jump high",
    "Foxes are wild animals and often have a brown coat",
]


# step2: 訓練BM25 Model
encoder.train(corpus_document)


# step3: 調參最佳化BM25 Model
query: str = "quick brown fox"
print(f"query: {query}")
k1s = [1.0, 1.5]
bs = [0.5, 0.75]
for k1, b in zip(k1s, bs):
    print(f"current k1: {k1}, b: {b}")
    encoder.b = b
    encoder.k1 = k1
    query_vector = encoder.encode_queries(query)
    results: List[Result] = []
    for idx, doc in enumerate(corpus_document):
        doc_vector = encoder.encode_documents(doc)
        score = calculate_score(query_vector, doc_vector)
        results.append(Result(doc=doc, score=score))
    results.sort(key=lambda r: r.score, reverse=True)

    for result in results:
        print(result)


# step4: 選擇最優參數並儲存模型
encoder.b = 0.75
encoder.k1 = 1.5
encoder.dump("./model.json")


# step5: 後續使用時可以載入模型
new_encoder = SparseVectorEncoder()
bm25_model_path = "./model.json"
new_encoder.load(bm25_model_path)


# step6: 對模型進行finetune並儲存
extra_corpus: List[str] = [
    "The fast fox jumps over the lazy, chubby dog",
    "A swift fox hops over a napping old dog",
    "The quick fox leaps over the sleepy, plump dog",
    "The agile fox jumps over the dozing, heavy-set dog",
    "A speedy fox vaults over a lazy, old dog lying in the sun"
]

new_encoder.train(extra_corpus)
new_bm25_model_path = "new_model.json"
new_encoder.dump(new_bm25_model_path)
import com.aliyun.dashtext.encoder.SparseVectorEncoder;

import java.io.*;
import java.util.*;

public class Main {

    public static class Result {
        public String doc;
        public float score;

        public Result(String doc, float score) {
            this.doc = doc;
            this.score = score;
        }

        @Override
        public String toString() {
            return String.format("Result(doc=%s, score=%f)", doc, score);
        }
    }

    public static float calculateScore(Map<Long, Float> queryVector, Map<Long, Float> documentVector) {
        float score = 0.0f;
        for (Map.Entry<Long, Float> entry : queryVector.entrySet()) {
            if (documentVector.containsKey(entry.getKey())) {
                score += entry.getValue() * documentVector.get(entry.getKey());
            }
        }
        return score;
    }

    public static void main(String[] args) throws IOException {
        // 建立空SparseVectorEncoder(可以設定自訂Tokenizer)
        SparseVectorEncoder encoder = new SparseVectorEncoder();

        // step1: 準備語料以及Documents
        List<String> corpusDocument = Arrays.asList(
                "The quick brown fox rapidly and agilely leaps over the lazy dog that lies idly by the roadside.",
                "Never jump over the lazy dog quickly",
                "A fox is quick and jumps over dogs",
                "The quick brown fox",
                "Dogs are domestic animals",
                "Some dog breeds are quick and jump high",
                "Foxes are wild animals and often have a brown coat"
        );

        // step2: 訓練BM25 Model
        encoder.train(corpusDocument);

        // step3: 調參最佳化BM25 Model
        String query = "quick brown fox";
        System.out.println("query: " + query);
        float[] k1s = {1.0f, 1.5f};
        float[] bs = {0.5f, 0.75f};
        for (int i = 0; i < k1s.length; i++) {
            float k1 = k1s[i];
            float b = bs[i];
            System.out.println("current k1: " + k1 + ", b: " + b);
            encoder.setB(b);
            encoder.setK1(k1);

            Map<Long, Float> queryVector = encoder.encodeQueries(query);
            List<Result> results = new ArrayList<>();
            for (String doc : corpusDocument) {
                Map<Long, Float> docVector = encoder.encodeDocuments(doc);
                float score = calculateScore(queryVector, docVector);
                results.add(new Result(doc, score));
            }

            results.sort((r1, r2) -> Float.compare(r2.score, r1.score));

            for (Result result : results) {
                System.out.println(result);
            }
        }

        // step4: 選擇最優參數並儲存模型
        encoder.setB(0.75f);
        encoder.setK1(1.5f);
        encoder.dump("./model.json");

        // step5: 後續使用時可以載入模型
        SparseVectorEncoder newEncoder = new SparseVectorEncoder();
        newEncoder.load("./model.json");

        // step6: 對模型進行finetune並儲存
        List<String> extraCorpus = Arrays.asList(
                "The fast fox jumps over the lazy, chubby dog",
                "A swift fox hops over a napping old dog",
                "The quick fox leaps over the sleepy, plump dog",
                "The agile fox jumps over the dozing, heavy-set dog",
                "A speedy fox vaults over a lazy, old dog lying in the sun"
        );
        newEncoder.train(extraCorpus);
        newEncoder.dump("./new_model.json");
    }
}

API參考

DashText API詳情可參考:https://pypi.org/project/dashtext/

  • 本頁導讀 (1, M)
  • 前置知識
  • BM25簡介
  • 稀疏向量產生
  • Score/距離計算
  • 如何自訓練模型
  • Step1:確認使用情境
  • Step2:準備語料
  • Step3:準備Tokenizer
  • Step4:訓練模型
  • Step5:調參最佳化(可選)
  • Step6:Finetune模型(可選)
  • 範例程式碼
  • API參考
文檔反饋