Python3 - 将相似的字符串分组在一起

Posted

技术标签:

【中文标题】Python3 - 将相似的字符串分组在一起【英文标题】:Python3 - Grouping similar strings together 【发布时间】:2020-08-23 13:43:10 【问题描述】:

我想做的是将小说网站上的字符串组合在一起。帖子的标题通常采用以下格式:

titles = ['Series Name: Part 1 - This is the chapter name',
    '[OC] Series Name - Part 2 - Another name with the word chapter and extra oc at the start',
    "[OC] Series Name = part 3 = punctuation could be not matching, so we can't always trust common substrings",
    'OC Another cool story - Part I - This is the chapter name',
    'OC another cool story: part II: another post title',
    'OC another cool story part III but the author forgot delimiters',
    "this is a one-off story, so it doesn't have any friends"]

分隔符等并不总是存在,并且可能会有一些变化。

我首先将字符串规范化为字母数字字符。

import re
from pprint import pprint as pp

titles = []  # from above

normalized = []
for title in titles:
    title = re.sub(r'\bOC\b', '', title)
    title = re.sub(r'[^a-zA-Z0-9\']+', ' ', title)
    title = title.strip()
    normalized.append(title)

pp(normalized)

给了

   ['Series Name Part 1 This is the chapter name',
 'Series Name Part 2 Another name with the word chapter and extra oc at the start',
 "Series Name part 3 punctuation could be not matching so we can't always trust common substrings",
 'Another cool story Part I This is the chapter name',
 'another cool story part II another post title',
 'another cool story part III but the author forgot delimiters',
 "this is a one off story so it doesn't have any friends"]

我希望的输出是:

['Series Name', 
'Another cool story', 
"this is a one-off story, so it doesn't have any friends"]  # last element optional

我知道比较字符串的几种不同方法...

difflib.SequenceMatcher.ratio()

Levenshtein edit distance

我还听说过 Jaro-Winkler 和 FuzzyWuzzy。

但真正重要的是我们可以得到一个数字来显示字符串之间的相似性。

我想我需要想出(大部分)一个 2D 矩阵来比较每个字符串。但是一旦我知道了,我就无法思考如何将它们实际分成组。

我发现another post 似乎已经完成了第一部分......但我不知道如何从那里继续。

scipy.cluster 起初看起来很有希望……但后来我就想不通了。

另一个想法是以某种方式将itertools.combinations() 与functools.reduce() 与上述距离指标之一结合起来。

我是不是想太多了?看起来这应该很简单,但在我的脑海中没有任何意义。

【问题讨论】:

如果您想查看两个基于文本的对象有多相似,使用 Levenshtein 编辑距离或 SimHash 的方法可能对您有用。我对您想加入的内容有点困惑(如果您可以提供手动示例)。 如果您在大小写或标点符号方面遇到问题,您可以将所有内容转为小写并删除所有标点符号。 我的例子可能不够清楚。我正在解析数千个标题,标点符号的变化可能比我上面的小例子要大得多。我确实喜欢规范化字符串的建议(我可能会将所有内容删除/替换为所有小写字母数字标题),但这仍然留下了一旦我有一组规范化字符串的问题,我如何提取系列以及什么标题在每个 您正在尝试加入相似的标题,同时从最终结果中删除冗余信息? 【参考方案1】:

这是 CKM 回答中提出的想法的实现:https://***.com/a/61671971/42346

首先去掉标点符号——这对你的目的并不重要——使用这个答案:https://***.com/a/15555162/42346

然后我们将使用此处描述的一种技术:https://blog.eduonix.com/artificial-intelligence/clustering-similar-sentences-together-using-machine-learning/ 来聚类相似的句子

from nltk.tokenize import RegexpTokenizer

tokenizer = RegexpTokenizer(r'\w+') # only alphanumeric characters

lol_tokenized = []
for title in titles:
    lol_tokenized.append(tokenizer.tokenize(title))

然后获取标题的数字表示:

import numpy as np 
from gensim.models import Word2Vec

m = Word2Vec(lol_tokenized,size=50,min_count=1,cbow_mean=1)  
def vectorizer(sent,m): 
    vec = [] 
    numw = 0 
    for w in sent: 
        try: 
            if numw == 0: 
                vec = m[w] 
            else: 
                vec = np.add(vec, m[w]) 
            numw += 1 
        except Exception as e: 
            print(e) 
    return np.asarray(vec) / numw 

l = []
for i in lol_tokenized:
    l.append(vectorizer(i,m))

X = np.array(l)

哎呀,男孩很多。 现在你必须进行聚类。

from sklearn.cluster import KMeans

clf = KMeans(n_clusters=2,init='k-means++',n_init=100,random_state=0)
labels = clf.fit_predict(X)
print(labels)
for index, sentence in enumerate(lol_tokenized):
    print(str(labels[index]) + ":" + str(sentence))

[1 1 0 1 0 0 0]
1:['Series', 'Name', 'Part', '1', 'This', 'is', 'the', 'chapter', 'name']
1:['OC', 'Series', 'Name', 'Part', '2', 'Another', 'name', 'with', 'the', 'word', 'chapter', 'and', 'extra', 'oc', 'at', 'the', 'start']
0:['OC', 'Series', 'Name', 'part', '3', 'punctuation', 'could', 'be', 'not', 'matching', 'so', 'we', 'can', 't', 'always', 'trust', 'common', 'substrings']
1:['OC', 'Another', 'cool', 'story', 'Part', 'I', 'This', 'is', 'the', 'chapter', 'name']
0:['OC', 'another', 'cool', 'story', 'part', 'II', 'another', 'post', 'title']
0:['OC', 'another', 'cool', 'story', 'part', 'III', 'but', 'the', 'author', 'forgot', 'delimiters']
0:['this', 'is', 'a', 'one', 'off', 'story', 'so', 'it', 'doesn', 't', 'have', 'any', 'friends']

然后你可以拉出索引== 1的那些:

for index, sentence in enumerate(lol_tokenized): 
    if labels[index] == 1: 
        print(sentence) 

['Series', 'Name', 'Part', '1', 'This', 'is', 'the', 'chapter', 'name']
['OC', 'Series', 'Name', 'Part', '2', 'Another', 'name', 'with', 'the', 'word', 'chapter', 'and', 'extra', 'oc', 'at', 'the', 'start']
['OC', 'Another', 'cool', 'story', 'Part', 'I', 'This', 'is', 'the', 'chapter', 'name']

【讨论】:

我完全迷失了,我必须阅读......大部分功能,呵呵。 vectorizer() 尤其让我感到困惑,但我从未见过这样的实现。但是非常感谢你为我写了这篇文章!越看越觉得有道理…… 不客气。我对它对你的完整数据集的表现特别感兴趣,所以如果你回来告诉我们它是如何进行的,我将不胜感激。祝你好运。 我也对这个答案感兴趣。我注意到当第 1 部分和第 2 部分在集群 0 中时,OC Series Name Part 3 被集群到集群 1。 @mechanical_meat 你认为它与This is the chapter name 部分有关吗?如果是这样,也许在一些单词之后删掉标题会更好地匹配? 我看到的唯一要做的事情(请不要觉得你必须这样做,你得给我留点东西做,哈哈)就是把那些“中心”的头衔缩减为系列标题而不是整个字符串。我想我可以通过将它与其他字符串进行比较并从开头找到常见的子字符串来做到这一点。 另外,有没有办法让这个解决方案适应您事先不知道标题数量的情况?【参考方案2】:

您的任务属于所谓的semantic similarity。我建议您按以下方式进行:

    通过 Glove/Word2vec 或流行的 BERT 获取字符串的映射。这将为您提供标题的数字表示形式。 然后从 scikit 的 k-means 开始执行聚类,然后您可以使用高级聚类方法。

【讨论】:

以上是关于Python3 - 将相似的字符串分组在一起的主要内容,如果未能解决你的问题,请参考以下文章

从许多数字和字符图片中进行图像聚类,将相似的图片组合在一起

有效地将相似的数字组合在一起[重复]

mongoDB聚合将相似的文档彼此相邻分组

拆分训练测试数据集将相似的值保持在一起

SQL Server 或 C# - 将相似的记录归为一组

使用fuzzywuzzy python比较和分组字符串的嵌套循环