使用拼写检查查询分段

Query segmentation with spell check

假设我有一个固定的多词名称列表,例如: Water Tocopherol (Vitamin E) Vitamin D PEG-60 Hydrogenated Castor Oil

我想要以下 input/output 结果:

  1. Water, PEG-60 Hydrogenated Castor Oil -> Water, PEG-60 Hydrogenated Castor Oil
  2. PEG-60 Hydrnated Castor Oil -> PEG-60 Hydrogenated Castor Oil
  3. wter PEG-60 Hydrnated Castor Oil -> Water, PEG-60 Hydrogenated Castor Oil
  4. Vitamin E -> Tocopherol (Vitamin E)

我需要它具有高性能,并且能够识别有太多势均力敌和没有势均力敌的比赛。使用 1 相对容易,因为我可以用逗号分隔。大多数情况下,输入列表由逗号分隔,所以这在 80% 的时间都有效,但即使这样也有小问题。以 4 为例。一旦分开,大多数拼写检查库(我试过一个数字)都不会返回 4 的理想匹配,因为到 Vitamin D 的编辑距离要小得多。有些网站在这方面做得很好,但我不知道该怎么做。

这个问题的第二部分是,我如何在上面进行分词。假设给定的列表没有逗号,我需要能够识别它。最简单的例子是 Water Vtamin D 应该变成 WaterVitamin D。我可以举出很多例子,但我认为这很好地说明了问题。

Here's 可以使用的名称列表。

上下文

这是approximate string matching or fuzzy matching的一个例子。有很好的资料和图书馆。

有不同的库和方法可以解决这个问题。我将限制为相对简单的库

一些很棒的库:

from fuzzywuzzy import process
import pandas as pd
import string

第一部分

让我们放置数据来玩一玩。我尝试重现上面的例子,希望没问题。

# Set up dataframe
d = {'originals': [["Water","PEG-60 Hydrogenated Castor Oil"],
                   ["PEG-60 Hydrnated Castor Oil"],
                   ["wter"," PEG-60 Hydrnated Castor Oil"],
                   ['Vitamin E']],
     'correct': [["Water","PEG-60 Hydrogenated Castor Oil"],
                 ["PEG-60 Hydrogenated Castor Oil"],
                 ['Water', 'PEG-60 Hydrogenated Castor Oil'],
                 ['Tocopherol (Vitamin E)']]}
df = pd.DataFrame(data=d)
print(df)
                                 originals                                  correct
0  [Water, PEG-60 Hydrogenated Castor Oil]  [Water, PEG-60 Hydrogenated Castor Oil]
1            [PEG-60 Hydrnated Castor Oil]         [PEG-60 Hydrogenated Castor Oil]
2     [wter,  PEG-60 Hydrnated Castor Oil]  [Water, PEG-60 Hydrogenated Castor Oil]
3                              [Vitamin E]                 [Tocopherol (Vitamin E)]

从上面我们得到了问题的陈述:我们有一些原始的措辞,想改变它。

哪些选项对我们来说是正确的:

strOptions = ['Water', "Tocopherol (Vitamin E)",
             "Vitamin D", "PEG-60 Hydrogenated Castor Oil"]

这些功能将帮助我们。我尽量把它们记录下来。

def function_proximity(str2Match,strOptions):
    """
    This function help to get the first guess by similiarity.

    paramters
    ---------
    str2Match: string. The string to match.
    strOptions: list of strings. Those are the possibilities to match.
    """
    highest = process.extractOne(str2Match,strOptions)
    return highest[0]
def check_strings(x, strOptions):
    """
    Takes a list of string and give you a list of string best matched.
    :param x: list of string to link / matched
    :param strOptions:
    :return: list of string matched
    """
    list_results = []
    for i in x:
        i=str(i)
        list_results.append(function_proximity(i,strOptions))
    return list_results

让我们应用到数据框:

df['solutions_1'] = df['originals'].apply(lambda x: check_strings(x, strOptions))

让我们通过比较列来检查结果。

print(df['solutions_1'] == df['correct'])
0    True
1    True
2    True
3    True
dtype: bool

如您所见,解决方案在四种情况下都有效。

第二部分

问题 示例解决方案: 你有 Water Vtamin D 应该变成 Water, Vitamin D.

让我们创建一个有效单词列表。

list_words = []
for i in strOptions:
    print(i.split(' '))
    list_words = list_words + i.split(' ')
# Lower case and remove punctionation
list_valid_words = []
for i in list_words:
    i = i.lower()
    list_valid_words.append(i.translate(str.maketrans('', '', string.punctuation)))
print(list_valid_words)
['water', 'tocopherol', 'vitamin', 'e', 'vitamin', 'd', 'peg60', 'hydrogenated', 'castor', 'oil']

如果列表中的单词是有效的。

def remove_puntuation_split(x):
    """
    This function remove puntuation and split the string into tokens.
    :param x: string
    :return: list of proper tokens
    """
    x = x.lower()
    # Remove all puntuation
    x = x.translate(str.maketrans('', '', string.punctuation))
    return x.split(' ')

tokens = remove_puntuation_split(x)
# Clean tokens
clean_tokens = [function_proximity(x,list_valid_words) for x in tokens]
# Matched tokens with proper selection
tokens_clasified = [function_proximity(x,strOptions) for x in tokens]
# Removed repeated
tokens_clasified =  list(set(tokens_clasified))
print(tokens_clasified)
['Vitamin D', 'Water']

这是最初的要求。 然而,这些可能会有点失败,尤其是当维生素 E 和 D 结合使用时。

参考资料

这个答案建立在@Rafaels 的答案之上。

    FuzzyWuzzy 中的
  1. process.extractOne 默认使用记分器 fuzz.WRatio。这是 FuzzyWuzzy 提供的多个评分器的组合,适用于 Seatgeek 正在使用的数据集。因此,您可能想与其他记分员一起尝试,看看哪一个最适合您。但是请注意,使用编辑距离可能很难区分相当多的元素。例如。 Vitamin E <-> Vitamin D 只需要一次编辑,即使它们完全不同。同样的行为也发生在 glycereth-7

  2. FuzzyWuzzy 相对较慢,因此当您处理更大的数据集时,您可能希望使用 RapidFuzz(我是作者),它提供类似的算法,但性能更好.

  3. process.extractOne 默认预处理输入字符串(小写并将非字母数字字符替换为空格)。由于您可能多次搜索元素,因此提前一次预处理可能的选择并停用此行为以确保安全一段时间是有意义的:

process.extractOne(str2Match,strOptions, processor=None)

RapidFuzz 和 FuzzyWuzzy 之间的区别

由于您报告了 RapidFuzz 和 FuzzyWuzzy 之间的结果差异,这里有一些可能的原因:

  1. 我不对结果进行四舍五入。所以你会得到一个像 42.22 而不是 42 这样的浮点数
  2. 如果您不使用快速 FuzzyWuzzy 实现,它使用 python-Levenshtein,您可能会得到不同的结果,因为它使用 difflib,这是一个不同的指标。大多数时候它会产生非常相似的结果,但并非总是如此
  3. 如果您使用快速实现,任何部分比率如 partial_ratio、WRatio ... 可能 return 在 FuzzyWuzzy 中产生错误结果,因为 partial_ratio 已损坏(请参阅 here)
  4. 将 processor=None 传递给 extract/extractOne 在 RapidFuzz 和 FuzzyWuzzy 中具有不同的含义。在 RapidFuzz 中,它将停用预处理,而在 FuzzyWuzzy 中,它仍将使用默认分数。例如
extract(..., scorer=fuzz.WRatio, processor=None)

FuzzyWuzzy 仍会预处理 WRatio 中的字符串,因此无法停用预处理。我个人认为这是一个糟糕的设计,所以我将其更改为让用户可以停用处理器,这很可能是您在通过 processor=None

时想要实现的

我扩展了其他答案以使其适用于提供的列表。这是一种使用 fuzzywuzzy 的算法,似乎适用于 vitamin e.

这样的情况
def merge_scores(text, matches, match_func):
    new_scores = []
    for match in matches:
        new_scores.append((match[0], (match[1] + match_func(match[0], text)) / 2))
    return sorted(new_scores, key=lambda m:m[1], reverse=True)

def get_best_match(text):
    fuzz_matches = process.extractBests(text, INGREDIENTS, limit=10, scorer=fuzz.ratio)
    if fuzz_matches[0][1] < 80 or fuzz_matches[0][1] == fuzz_matches[1][1]:
        fuzz_matches = process.extractBests(text, INGREDIENTS, limit=10, scorer=fuzz.token_set_ratio)
        # Combine only if the top 5 aren't perfect matches
        if fuzz_matches[4][1] != 100:
            fuzz_matches = merge_scores(text, fuzz_matches, fuzz.ratio)
    if fuzz_matches[0][1] == fuzz_matches[1][1]:
        fuzz_matches = process.extractBests(text, INGREDIENTS, limit=10, scorer=fuzz.WRatio)
    if fuzz_matches[0][1] == fuzz_matches[1][1]:
        return '', 0
    return fuzz_matches[0]

我将 https://pastebin.com/QXabQXWP 中的列表复制并粘贴到文件 list_terms_v3.txt 中。

"""

References
https://github.com/seatgeek/fuzzywuzzy
https://www.datacamp.com/community/tutorials/fuzzy-string-python

Questions Whosebug: 

"""

from fuzzywuzzy import process
from fuzzywuzzy import fuzz
import pandas as pd
import string

#strOptions = ['Water', "Tocopherol (Vitamin E)",
#             "Vitamin D", "PEG-60 Hydrogenated Castor Oil"]
strOptions = pd.read_csv("list_terms_v3.txt", sep='\n', header=None)
strOptions = strOptions[0].tolist()
print(strOptions.__len__())

"""
Create a list of key workds
"""
list_words = []
for i in strOptions:
    print(i.split(' '))
    list_words = list_words + i.split(' ')
# Lower case and remove punctionation
list_valid_words = []
for i in list_words:
    i = i.lower()
    list_valid_words.append(i.translate(str.maketrans('', '', string.punctuation)))


def function_proximity(str2Match,strOptions = strOptions):
    """
    This function help to get the first guess by similiarity.

    paramters
    ---------
    str2Match: string. The string to match.
    strOptions: list of strings. Those are the possibilities to match.
    """
    highest = process.extractOne(str2Match,strOptions)
    return highest[0]
def check_strings(x, strOptions):
    """
    Takes a list of string and give you a list of string best matched.
    :param x: list of string to link / matched
    :param strOptions:
    :return: list of string matched
    """
    list_results = []
    for i in x:
        i=str(i)
        list_results.append(function_proximity(i))
    return list_results

"""
2 Option
"""


def remove_puntuation_split(x):
    """
    This function remove puntuation and split the string into tokens.
    :param x: string
    :return: list of proper tokens
    """
    x = x.lower()
    # Remove all puntuation
    x = x.translate(str.maketrans('', '', string.punctuation))
    return x.split(' ')

def clean_tokens_funct(x, strOptions=strOptions):
    if x in strOptions:
        return x
    else:
        return function_proximity(x,strOptions=strOptions)


def searcher(x):
    tokens = remove_puntuation_split(x)
    clean_tokens = [clean_tokens_funct(x, strOptions=list_valid_words) for x in tokens]
    # Matched tokens with proper selection
    tokens_clasified = [function_proximity(x,strOptions=strOptions) for x in tokens]
    # Removed repeated
    tokens_clasified =  list(set(tokens_clasified))
    return tokens_clasified

至此一切正常,待检查:

x = "Water Vtamin E"
print(searcher(x))
['Tocopheryl Acetate (Vitamin E)', 'Water']

但在某些情况下,以前的文件可能会归档。问题是维生素D没有出现在列表中,但如果添加一切应该没问题。

x = "Water Vtamin D"
print(searcher(x))
['Tocopheryl Acetate (Vitamin E)', 'Ceramide', 'Water']

或问题的另一种情况:

x = "Water  D"
print(searcher(x))
['Ceramide', 'Water']

如果您只想与现有列表进行比较,请使用 function_proximity

x = 'Actinidia Chinensis Fruit'
print(function_proximity(x))
Luffa Cylindrica Fruit/Leaf/Stem Extract


x = 'Nelumbo Nucifera Callus Culture'
print(function_proximity(x))
Nelumbo Nucifera Callus Culture Extract