适用于TTS语音处理项目的召回模型:词袋模型究竟是个啥?和语义召回相比有什么优劣? 原创

发布于 2024-11-13 15:13
浏览
0收藏

在人工智能的众多应用中,对于文档的准确召回是一个广泛使用的需求。例如,在基于阅读理解的问答系统(RAG)中,我们常常通过嵌入模型生成向量来进行语义方面的召回,这种方法已经证明了其效果良好。然而,这并不意味着该方法适用于所有场景。

让我们考虑一个涉及语音处理的项目,用户可能会说出需要查询的内容,系统需要迅速召回与用户语音输入相关的文档。在这种情况下,系统的响应时间变得至关重要,任何延迟都可能影响用户体验。对于这类需求,词袋模型可能成为一种更有利的选择,因为它无需处理复杂的语义关系,从而可以提供更快速的响应。所以,选择何种模型取决于具体的应用情景和性能要求。

什么是词袋模型

词袋模型是一种文本处理方法,先通过构建一个有序词表(字典),包含训练样本中所有的不重复词汇。然后,根据此词表遍历每个样本,如果某词在样本中出现则出现次数加1,否则次数设置为0。这样,每个样本都可以向量化成一个长度等于词表长度的向量。这么说可能有一点抽象,让我们来看一个例子,比如有两个文本:

  • s1:"John likes to watch movies, Mary likes movies too"
  • s2:"John also likes to watch football games"

首先,构建词汇表 ,这两个句子形成的词表(不去停用词)为:

[‘also’, ‘football’, ‘games’, ‘john’, ‘likes’, ‘mary’, ‘movies’, ‘to’, ‘too’, ‘watch’]

然后,对于每个文本,统计词汇表中每个单词的出现次数或者频率:

适用于TTS语音处理项目的召回模型:词袋模型究竟是个啥?和语义召回相比有什么优劣?-AI.x社区

BOW词向量

词袋模型处理流程

词袋模型的处理流程可以总结为以下3步:

  1. 首先进行文本分词
  2. 然后创建一个包含所有独特词汇的词表,保证每个词语仅出现一次,并固定其位置
  3. 最后,将每个样本向量化,即遍历每个样本,统计词汇表中每个单词的出现次数

可以看出在经过向量化后,每个样本的特征维度长度等于词表长度。这种方法能覆盖所有样本中的词汇,但可能导致维度灾难,因为一个通常大小的中文数据集可能包含数万个词汇,意味着向量的维度也相应大。因此,在分词后,我们通常统计每个词在数据集中的出现次数,并仅选择出现频率最高的前K个词作为最终词表。最后,会移除无意义的停用词,如“的”,“啊”,“了”等。

代码实现

手动撸代码

文本分词

向量化的第一步是分词。我们将介绍一个常用的开源分词工具,jieba。在使用jieba之前,需要先进行安装,可以进入相应的虚拟环境并使用命令​​pip install jieba​​完成安装。

import jieba, re
cut_all = False


def cutWords(s, cut_all=False):
    cut_words = []
    # 将所有字母、数字、冒号、逗号、句号等过滤掉
    s = re.sub("[A-Za-z0-9\:\·\—\,\。\“ \”]", "", s)
    seg_list = jieba.cut(s, cut_all=cut_all)
    cut_words.append("/".join(seg_list))

cut_all 参数可以指定分词模式,当​​cut_all = False​​​时,表示普通分词模式,设置为​​True​​后,便可以开启全分词模式。

构建词表

分词后通常还会进行词频统计,以便选取出现频率最高的前K个词来构造词表。对词频进行统计需要使用另外一个包collection中的​​Counter​​计数器。

import re
from collections import Counter
import jieba


def wordsCount(s):
    # 初始化空字符串用于存储分词结果
    cut_words = ""
    
    # 使用正则表达式移除非中文字符
    s = re.sub("[A-Za-z0-9\:\·\—\,\。\“ \”]", "", s)
    
    # 使用jieba进行分词,并连接结果
    seg_list = jieba.cut(s, cut_all=False)
    cut_words += (" ".join(seg_list))
    
    # 将分词结果分割成列表
    all_words = cut_words.split()
    
    # 初始化计数器
    c = Counter()
    
    # 遍历所有单词,长度大于1且不是换行符则进行计数
    for x in all_words:
        if len(x) > 1 and x != '\r\n':
            c[x] += 1
    
    # 创建一个词汇列表
    vocab = []
    
    print('\n词频统计结果:')
    # 打印并收集出现频率最高的前5个词
    for (k, v) in c.most_common(5): 
        print("%s:%d" % (k, v))
        vocab.append(k)
    
    # 打印词汇列表
    print("词表:", vocab)

词频统计

def vectorization_with_freq(s):
    # 此处接文本分词和词频统计代码,需要生成词表vocab和文本x_text
    
    # 初始化空列表用于存储向量化结果
    x_vec = []
    
    # 遍历文本中的每项
    for item in x_text:
        # 对词表中的每个词创建一个字典,初始化频率为0
        tmp = dict(zip(vocab, [0] * len(vocab)))
        
        # 遍历此项中的每个词
        for w in item:
            # 如果词在词表中,则频率加1
            if w in vocab:
                tmp[w] += 1
                
        # 将该项的向量添加到向量化结果列表
        x_vec.append(list(tmp.values()))
    
    # 打印词表、文本和向量化结果
    print("词表:", vocab)
    print("文本:", x_text)
    print(x_vec)

这段Python代码定义了一个函数​​vectorization_with_freq(s)​​​,它接受一个字符串​​s​​作为输入,然后使用词频方法对其进行向量化处理。

sklearn实现

在实践中,我们可以直接使用sklearn库,它已经实现了上述功能,无需手动编写复杂的代码。

from sklearn.feature_extraction.text import CountVectorizer
corpus = [
    "John likes to watch movies, Mary likes movies too",
    "John also likes to watch football games",
]
bag_of_words = CountVectorizer(
            ngram_range=(1, 2), token_pattern="(?u)\\b\\w+\\b", binary=True
        ).fit(corpus)
vectors = bag_of_words.transform(corpus)
print(vectors)


"""
 (0, 5)	1
  (0, 7)	1
  (0, 8)	1
  (0, 9)	1
  (0, 10)	1
  (0, 11)	1
  (0, 12)	1
  (0, 13)	1
  (0, 14)	1
  (0, 15)	1
  (0, 16)	1
  (0, 17)	1
  (0, 18)	1
  (0, 19)	1
  (0, 21)	1
  (1, 0)	1
  (1, 1)	1
  (1, 2)	1
  (1, 3)	1
  (1, 4)	1
  (1, 5)	1
  (1, 6)	1
  (1, 8)	1
  (1, 10)	1
  (1, 16)	1
  (1, 17)	1
  (1, 19)	1
  (1, 20)	1
"""

​CountVectorizer​​是scikit-learn库中的一个类,用于将文本转换为词项频率向量。下面解释你提到的这些参数。

  1. ngram_range: 它定义了从文本中提取的 n-gram 的大小范围。N-gram 是 n 个连续的单词。例如,对于句子"我爱北京",当 ngram_range=(1,2),则会提取出 unigrams(单词)和 bigrams(两个连续的单词)。即,"我","爱","北京","我爱","爱北京"。
  2. token_pattern: 这是一个正则表达式,它定义了什么构成一个“单词”。"(?u)\b\w+\b"表示一个或多个unicode字符或数字并以边界分隔。例如,对于句子"I love Beijing",tokens 是["I", "love", "Beijing"]。
  3. binary: 如果设置为 True,所有非零计数都设为 1。这意味着输出的结果只表示单词是否在文档中出现(被设为1),而不是单词出现的次数。例如,对于句子"I love love Beijing",如果 binary=True,那么每个单词无论出现一次或多次,结果都记作出现过,"love": 1,而不是"love": 2。

fit

​CountVectorizer.fit()​​是scikit-learn库中的一个方法,用于学习输入文本集合(通常是一组文档)中所有单词的词汇表。这个过程也被称为“拟合”数据。

例如,如果我们有三个文档:["The sky is blue", "The sun is bright", "The sun in the sky is bright"],​​fit()​​函数会生成一个词汇表,包含所有不重复的单词:['The', 'sky', 'is', 'blue', 'sun', 'bright', 'in']。

这个词汇表随后可以��于将新的(或相同的)文档转换成向量,其中每个元素表示相应单词在文档中出现的频次。这是通过​​transform()​​​函数实现的,也经常和​​fit()​​​方法一起使用,如​​fit_transform()​​。

输出

​CountVectorizer​​​的输出是一个稀疏矩阵,其中每一行表示corpus中的一个文档,每一列对应于​​CountVectorizer​​拟合后得到的词汇表中的一个单词。在你的例子中,你有两个文档,所以你有两行。

每个元组 ​​(i, j)​​​ 的值代表了词汇表中第 ​​j​​​ 个单词在第 ​​i​​​ 个文档中出现的频率。因为你设置 ​​binary=True​​, 所以这个频率只能是0或1,表示该单词在相应的文档中是否出现。

例如,在你给出的输出中,​​(0, 5) 1​​ 表示词汇表中的第5个单词在第一个文档("John likes to watch movies, Mary likes movies too")中出现了(至少一次)。

类似地,​​(1, 0) 1​​ 表示词汇表中的第0个单词在第二个文档("John also likes to watch football games")中出现了。

注意,此处的索引是从0开始的,而且可能看不到某些索引,这是因为对应的词在相应的文档中没有出现,频率为零,而这种信息在稀疏矩阵中通常会被省略,以节省存储空间。

应用

在基于回答生成的阅读理解系统(RAG)中,文档召回是核心的一步。这个过程主要依赖于检索和标定与用户查询相关性最高的文档。传统的RAG应用主要通过语义和关键字匹配来执行此操作,这通常需要使用embedding模型。具体来说,我们会将文本输入到模型中,得到每个文档或查询的嵌入向量表示,然后计算它们之间的相似度。

除了上述方法外,我们还可以使用基于词袋模型的召回策略。词袋模型将文档转换为一个向量,其中每个元素对应于特定单词或短语在文档中出现的频率。通过比较这些向量,我们可以确定文档之间的相似度。例如,我们可以通过计算余弦相似度来衡量两个向量的相似程度,这种方法允许我们找到与给定查询最相关的前K个文档。

import numpy as np
from nltk.stem import PorterStemmer
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.metrics.pairwise import check_pairwise_arrays, normalize, safe_sparse_dot




stemmer = PorterStemmer()




class Vectorizer:
    @staticmethod
    def stem(text):
        # 词干化
        words = text.split()
        stemmed_words = [stemmer.stem(word) for word in words]
        sentence = " ".join(stemmed_words)
        sentence = sentence
        return sentence


    def __init__(self, corpus):
        self.bag_of_words = CountVectorizer(
            ngram_range=(1, 2), token_pattern="(?u)\\b\\w+\\b", binary=True
        ).fit(corpus)
        self.vectors = self.bag_of_words.transform(corpus)
        self.corpus = corpus


    @staticmethod
    def cosine_similarity(X, Y=None, dense_output=True):
        X, Y = check_pairwise_arrays(X, Y)
        X_normalized = normalize(X, copy=True)
        if X is Y:
            Y_normalized = X_normalized
        else:
            Y_normalized = normalize(Y, copy=True)
        return safe_sparse_dot(
            X_normalized,
            Y_normalized.T,
            dense_output=dense_output)


    def get_relevent_docs(self, query, top_k=3, theshold=0):
        stem_query = self.stem(query)
        query_vectors = self.bag_of_words.transform([stem_query])
        cos_sim = self.cosine_similarity(self.vectors, query_vectors)
        indices = np.argpartition(cos_sim[:, 0], -top_k)[-top_k:]
        result = []
        for i in range(len(indices) - 1, -1, -1):
            idx = indices[i]
            score = cos_sim[idx, 0]
            if score > theshold:
                result.append([score, idx])
        result.sort(key=lambda x: x[0], reverse=True)  # 相似度降序排序后返回


        return result

两种召回方案对比

基于语义+关键字模型的召回

此模型结合了关键字搜索和语义搜索。通常,该模型使用词嵌入(如Word2Vec,GloVe,BERT等)来表示文档和查询。

  • 优点
  1. 深度语义理解:例如,如果你正在搜索“苹果新产品发布”,这种模型可以识别出包含“iPhone最新版本上市”这样非直接关键词,但在语义上相关的文档,因为"苹果新产品发布"和"iPhone最新版本上市"具有相似的语义。
  2. 处理同义词和多义词:例如,“智能手机”、“移动电话”和“手提电话”可能在不同的文档中表示同一概念。同样,单词“苹果”可以是一种水果或是科技公司名。基于语义+关键字模型能够理解这些差异。
  • 缺点
  1. 训练资源需求大:需要大量数据,并且计算成本高昂,特别是对于深度学习模型,如BERT或GPT。
  2. 计算复杂性高:语义搜索需要计算与每个可用文档的相似性,这可能在大型数据库中导致延时。

基于词袋模型的召回

基于词袋模型的搜索是一种常见的关键字搜索方法,其中文档和查询被表示为单词的集合或多集,忽略了任何语义和语法结构。

  • 优点
  1. 实现简单:例如,如果你在搜索“苹果新产品发布”,基于词袋模型的搜索引擎将很容易找到包含这些确切短语的文档。
  2. 计算效率高:只需检查每个文档是否包含查询中的单词,对于大规模数据也能妥善处理。
  • 缺点
  1. 不能理解语义:例如,“苹果新产品发布”和“iPhone最新版本上市”在词袋模型下可能被认为是不相关的,因为他们没有公共的单词,尽管他们在语义上密切相关。
  2. 无法处理同义词和多义词:比如“智能手机”和“移动电话”在词袋模型看来是两个完全不同的概念。同时"苹果"这个词的多重含义也无法区分。

选择哪种模型取决于具体情况。如果查询主要基于精确的关键字匹配,词袋模型可能更适合;而如果语义理解更重要,则应该考虑使用基于语义+关键字的模型。在实践中,这两种方法往往结合在一起使用,例如,先使用词袋模型进行粗略搜索,然后使用基于语义+关键字模型进行精确搜索。

本文转载自公众号AI 博物院 作者:longyunfeigu

原文链接:​​https://mp.weixin.qq.com/s/kiZ9pcJ7xac4sesoJkiSYA​


©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
收藏
回复
举报
回复
相关推荐