使用 NLP/Spacy 查找相似之处。

4

我有一个大型数据框需要与另一个数据框进行比较并更正其ID。我将通过这个简单的示例说明我的问题。

import spacy
import pandas as pd
nlp = spacy.load('en_core_web_sm', disable=['ner'])
ruler = nlp.add_pipe("entity_ruler", config={"phrase_matcher_attr": "LOWER"})

df = pd.DataFrame({'id':['nan','nan','nan'],
                    'description':['JOHN HAS 25 YEAR OLD LIVES IN At/12','STEVE has  50 OLD LIVES IN At.14','ALICIE HAS 10 YEAR OLD LIVES IN AT13']})
print(df)

df1 = pd.DataFrame({'id':[1203,1205,1045],
                    'description':['JOHN HAS 25year OLD LIVES IN At 2','STEVE has  50year OLD LIVES IN At 14','ALICIE HAS 10year OLD LIVES IN At 13']})
print(df1)
age = ["50year", "25year", "10year"]
for a in age:
    ruler.add_patterns([{"label": "age", "pattern": a}])

names = ["JOHN", "STEVE", "ALICIA"]
for n in names:
    ruler.add_patterns([{"label": "name", "pattern": n}])

ref = ["AT 2", "At 13", "At 14"]
for r in ref:
    ruler.add_patterns([{"label": "ref", "pattern": r}])
#exp to check text difference
doc = nlp("JOHN has 25 YEAR OLD LIVES IN At.12 ")
for ent in doc.ents:
    print(ent, ent.label_)

实际上,这两个数据帧df和参考数据帧df1的文本存在差异,如下图所示:

enter image description here 我不知道如何在这种情况下获得100%的相似度。 我尝试使用spacy,但我不知道如何修复差异并更正df中的id。

这是我的数据帧1:

   id                           description
0  nan      STEVE has 50 OLD LIVES IN At.14
1  nan   JOHN HAS 25 YEAR OLD LIVES IN At/12
2  nan  ALICIE HAS 10 YEAR OLD LIVES IN AT15

这是我的参考数据框:

     id                           description
0  1203   STEVEN HAS 25year OLD lives in At 6
1  1205     JOHN HAS 25year OLD LIVES IN At 2
2  1045  ALICIE HAS 50year OLD LIVES IN At 13
3  3045   STEVE HAS 50year OLD LIVES IN At 14
4  3465  ALICIE HAS 10year OLD LIVES IN At 13

My expected output:

     id                           description
0   3045     STEVE has 50 OLD LIVES IN At.14
1   1205     JOHN HAS 25 YEAR OLD LIVES IN At/12
2   3465     ALICIE HAS 10year OLD LIVES IN AT15

NB: 这些句子的顺序不同 / 数据框长度不相等


你尝试使用正则表达式了吗?我认为你可以在spacy中使用它们(如果你知道所有令牌的变化)来匹配特定的令牌,比如age或者at。但是我不确定如何将其添加到管道中。 - SpaceBurger
你需要匹配非常相似但不完全相同的句子对吗?还是需要标记的对齐? - fsimonjetz
匹配非完全相同但十分相似的一对。 - user16253345
你能给我们提供更多细节吗?比如,数据框的长度是否相等,句子的顺序是否相同,或者再给出一两个例子?处理这种问题有很多方法。在自然语言处理中,当处理同一源数据的稍微不同的标记化时,这种情况很常见。 - fsimonjetz
4个回答

2
如果批处理大小非常大(并且因为使用fuzzywuzzy很慢),我们可以使用NMSLIB在一些子字符串ngram嵌入中构建KNN索引(灵感来自于这篇文章这个后续)。请注意保留HTML标记,但不要写解释。
import re
import pandas as pd
import nmslib
from sklearn.feature_extraction.text import TfidfVectorizer

def ngrams(description, n=3):
    description = description.lower()
    description = re.sub(r'[,-./]|\sBD',r'', description)
    ngrams = zip(*[description[i:] for i in range(n)])
    return [''.join(ngram) for ngram in ngrams]

def build_index(descriptions, vectorizer):
    ref_vectors = vectorizer.fit_transform(descriptions)
    index = nmslib.init(method='hnsw',
                        space='cosinesimil_sparse',
                        data_type=nmslib.DataType.SPARSE_VECTOR)
    index.addDataPointBatch(ref_vectors)
    index.createIndex()
    return index

def search_index(queries, vectorizer, index):
    query_vectors = vectorizer.transform(query_df['description'])
    results = index.knnQueryBatch(query_vectors, k=1)
    return [res[0][0] for res in results]

# ref_df = df1, query_df = df
vectorizer = TfidfVectorizer(min_df=1, analyzer=ngrams)

index = build_index(ref_df['description'], vectorizer)

results = search_index(query_df['description'], vectorizer, index)

query_ids = [ref_df['id'].iloc[ref_idx] for ref_idx in results]
query_df['id'] = query_ids
print(query_df)

这将会得到:
     id                           description
0  3045      STEVE has  50 OLD LIVES IN At.14
1  1205   JOHN HAS 25 YEAR OLD LIVES IN At/12
2  3465  ALICIE HAS 10 YEAR OLD LIVES IN AT13

我们可以在 ngrams 中进行更多的预处理,例如:停用词、符号处理等。

1

由于您正在寻找几乎相同的字符串,因此spaCy并不是处理此类任务的最佳工具。词向量旨在处理语义,而您需要查找字符串相似性。

也许这只是因为您提供的示例比较简单,因此您可以通过删除不影响结果的内容来规范化字符串。例如,

text = "Alice lives at..."
text = text.replace(" ", "") # remove spaces
text = text.replace("/", "") # remove slashes
text = text.replace("year", "") # remove "year"
text = text.lower()

在你的大部分(全部?)示例中,这似乎会使你的字符串相同。然后,您可以使用它们的规范化形式作为字典的键来匹配字符串,例如。
这种方法比先前答案中描述的模糊匹配具有重要优势。一旦您有了两个候选者,使用字符串距离度量来查看它们是否足够接近很重要,但您真的不想为两个表中的每个条目计算字符串距离。如果像我在这里建议的那样规范化字符串,您可以找到匹配项,而无需将每个字符串与其他表中的每个字符串进行比较。
如果此处的规范化策略无效,请查看simhash或其他局部敏感哈希技术。简化版本是使用稀有词,例如示例数据中的名称,创建“桶”条目。计算桶中所有内容的字符串相似性有点慢,但比使用整个表好。

1
作为您的字符串“几乎”相同,这里有一个更简单的建议,使用字符串匹配模块fuzzywuzzy,如其名称所示,执行模糊字符串匹配。

它提供了许多函数来计算字符串相似度,您可以尝试不同的函数并选择最有效的函数。鉴于您的示例数据框...

    id                           description
0  nan       STEVE has 50 OLD LIVES IN At.14
1  nan   JOHN HAS 25 YEAR OLD LIVES IN At/12
2  nan  ALICIE HAS 10 YEAR OLD LIVES IN AT15

     id                           description
0  1203   STEVEN HAS 25year OLD lives in At 6
1  1205     JOHN HAS 25year OLD LIVES IN At 2
2  1045  ALICIE HAS 50year OLD LIVES IN At 13
3  3045   STEVE HAS 50year OLD LIVES IN At 14
4  3465  ALICIE HAS 10year OLD LIVES IN At 13

即使是最基本的ratio函数似乎也能给我们正确的结果。

from fuzzywuzzy import fuzz
import numpy as np
import pandas as pd

fuzzy_ratio = np.vectorize(fuzz.ratio)

dist_matrix = fuzzy_ratio(df.description.values[:, None], df1.description.values)

dist_df = pd.DataFrame(dist_matrix, df.index, df1.index)

结果:

    0   1   2   3   4
0  52  59  66  82  63
1  49  82  65  66  62
2  39  58  78  65  81

行最大值提示以下映射:
- 'STEVE has 50 OLD LIVES IN At.14','STEVE HAS 50year OLD LIVES IN At 14' - 'JOHN HAS 25 YEAR OLD LIVES IN At/12','JOHN HAS 25year OLD LIVES IN At 2' - 'ALICIE HAS 10 YEAR OLD LIVES IN AT15','ALICIE HAS 10 YEAR OLD LIVES IN AT15'
然而,请注意在最后一种情况下这是一个非常微妙的判断,因此不能保证始终正确。根据您的数据外观,您可能需要更复杂的启发式算法。如果所有尝试都失败了,您甚至可以尝试基于向量的相似度度量,如word movers distance,但如果字符串并不真的有很大的差异,它似乎过于复杂。

1
我想补充一下,你可能最好使用RapidFuzz而不是FuzzyWuzzy——它基本上具有相同的功能,但速度更快,许可证更开放,维护也更好。https://github.com/maxbachmann/rapidfuzz-cpp - polm23

0

我认为在这里使用spacy不是正确的方法。你需要使用的是(1)正则表达式(2)jaccard匹配。由于大多数标记应该完全匹配,因此Jaccard匹配将计算两个句子之间有多少相似的单词; 这将是一个好的选择。对于正则表达式部分,我会按照以下格式进行:

import re
def text_clean(text):
    #remove everything except alphabets
    text = re.sub('[^A-Za-z.]', ' ', text)
    text = text.lower()
    return text

现在,对于所有字符串应用上述函数,将删除数字和所有“。”、“/”等字符。之后,如果应用Jaccard相似度,则应该得到良好的匹配。
我建议删除数字,因为在您的一个示例中,“/12”变成了“2”,但仍然匹配。这意味着您主要关注的是单词而不是精确的数字。
您可能无法仅使用Jaccard匹配获得100%的准确性。重要的是,您将无法在所有匹配中获得100%的Jaccard匹配,因此您将不得不在Jaccard匹配值上设置截止值,超过该值,您将希望考虑匹配。
您可能需要使用清理后的字符串上的spacy和Jaccard匹配的更复杂方法,并在两个匹配分数上放置自定义截止值,然后选择您的匹配项。
另外,我注意到在某些情况下,您会得到两个单词连在一起的情况。这只发生在像At13这样的数字吗?还是也发生在两个单词中?为了有效地使用Jaccard匹配,您还需要解决这个问题。但这是一个完全不同的过程,有点超出了本答案的范围。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接