【问题标题】:Which Pattern Matching Algorithm fits for my case?哪种模式匹配算法适合我的情况?
【发布时间】:2021-03-30 19:17:30
【问题描述】:

我有一个项目,它需要与文本文档进行比较,并找出每个句子之间的相似率和文本的一般相似性。 我对文本进行了一些转换,例如降低所有单词,删除重复单词,删除句号以外的标点符号。做了一些操作后,我有 2 个数组列表,其中包括句子和单词全部分开。好像

[[“你好”,“世界”],[“欢迎”,“这里”]]

然后我按字母顺序对每个句子进行排序。在所有这些之后,我正在逐个比较所有单词,进行线性搜索,但是如果我正在搜索的单词比我正在查找的要大(第一个字符的 ASCII世界>汉堡),我不看剩下的部分,跳另一个词。这看起来很复杂,但我需要一个答案“是否有更快、更高效的通用算法,如 Boyer Moore、Hashing 或其他?” .我不是在问任何代码和平,但我需要一些理论建议。谢谢。

编辑: 我应该告诉项目的主要目的。实际上它是一种抄袭检测器。有两个 txt 文件,分别是 main.txt 和 sub.txt。程序将比较它们并给出类似的输出:

Output:
Similarity rate of two texts is: %X
{The most similar sentence}
{The most similar 2nd sentence}
{The most similar 3d sentence}
{The most similar 4th sentence}
{The most similar 5th sentence}

所以我需要找出 sub.txt 与 main.txt 文件的相似率。我认为我需要将两个文件中的所有句子相互比较。

例如,main.txt 有 10 个句子,sub.txt 有 5 个句子, 将有 50 个比较,将计算 50 个相似率 并存储。

最后我对相似率进行排序,打印出最多的 5 个句子。实际上我已经完成了这个项目,但效率不高。它有 4 个嵌套的 for 循环并比较所有单词无数次,复杂度变得像 O(n^4) (可能不是那么多),但即使在最坏的情况下它也非常巨大。我找到了 Levenshtein 距离算法和余弦相似度算法,但我不确定它们。感谢您的任何建议!

EDIT2: 对于我的情况,两个句子之间的相似性如下:

main_sentence:"Hello dude how are you doing?"
sub_sentence:"Hello i'm fine dude."
Since intersection is 2 words ["hello","dude"]
The similarity is : (length of intersected words)*100/(length of main text)
For this case it's: 2*100/6 = %33,3

【问题讨论】:

  • 请提供一些您期望的比较结果的详细信息。您是否正在寻找每个列表中相同的单词数?或者只是检查它们是否包含完全相同的单词?你的问题不清楚。
  • @sprinter 你是对的。我在我的问题中添加了 EDIT 部分。感谢您的反馈!
  • 我认为我们需要对相似性进行更严格的定义来解决这个问题。
  • @Surt 我在 EDIT2 中添加了一个严格的定义。谢谢。
  • @aran 非常感谢!我认为这个帖子将来会通过你和其他人的帮助来帮助人们,关闭它不会很好。你知道吗,我正在研究机器学习,我真的很喜欢图像识别 :) 但至少现在不是我们的情况 :)

标签: java algorithm search arraylist


【解决方案1】:

作为建议,即使这不是对您的问题的“完整答案”,比较字符串通常也是一项“繁重”操作(即使您首先检查它们的长度,事实上,这是第一个equals() 方法在比较字符串时已经执行的操作)

我的建议是下一步:创建一个dummy hashcode()-like 方法。它不是真正的hashcode(),而是与您的代码读取该单词的顺序相关联的数字。类似于加密方法,但要简单得多。

请注意,string.hashCode() 不起作用,因为第一个文档中的单词“Hello”与第二个文档中的单词“Hello”返回的哈希码不同。


数据“变暖” - PreConversion

假设您有一个共享的HashMap<String,Integer> (myMap),其中键是字符串,值是整数。 请注意,HashMap 在 java 中使用少于 10 个字符(通常是英文)的字符串键的散列非常快。无需任何检查,只需将每个单词与其计数器值放在一起:

  myMap.put(yourString, ++counter);  

假设您有 2 个文档:

 1.txt-   Welcome mate what are you doing here 
     
 2.txt-   Mate I was here before are you dumb

我假设您已经将所有单词小写,并删除了重复项。 您开始阅读第一个文档并将每个单词分配给一个数字。地图如下所示:

        KEY                     VALUE
       welcome                    1
       mate                       2
       what                       3
       are                        4
       you                        5
       doing                      6
       here                       7

现在是第二个文档。如果一个键被重复,put() 方法将更新它的值。所以:

        KEY                     VALUE
       welcome                    1
       mate                       8
       what                       3
       are                        13
       you                        14
       doing                      6
       here                       11
         I                        9
       was                        10
       before                     12
       dumb                       15

一旦完成,您将创建另一个 HashMap (reverseMap),以相反的方式:

       KEY                    VALUE
       1                       welcome
       8                       mate
       3                       what
       13                       are
       14                       you
       6                       doing
       11                       here
        9                        I
       10                        was
       12                     before
       15                       dumb

您将两个文档都转换为整数列表,因此它们看起来像:

 1.txt-   Welcome mate what are you doing here
 2.txt-   Mate I was here before are you dumb

到:

 listOne - [1, 8, 3, 13, 14, 6, 11]
 listTwo - [8, 9, 10, 11, 12, 13, 14, 15]

重复的单词、位置和序列

要在两个文档中查找重复项:

  • 首先,创建其中一个列表的深层克隆,例如 listTwo。对IntegersList 进行深度克隆相对容易执行。称它为listDuplicates,因为这将是它的目标。

       List<Integer> listDuplicates = new ArrayList<>();
        for (Integer i:listTwo)
              listDuplicates.add(new Integer(i));
    
  • 致电retainAll:

listDuplicates.retainAll(listOne);

结果是:

listDuplicates- [8,11,13,14]

因此,在 2 个文档中找到的总共 listOne.size()+listTwo.size() = 15 个单词中,4 个是重复的,11 个是唯一的。

为了得到转换后的值,只需调用:

for (Integer i : listDuplicates)
 System.out.println(reverseMap.get(i)); // mate , here, are, you

现在识别出重复项,listOnelistTwo 现在也可以用于:

  • 识别每个列表中的位置(这样我们就可以得到这些词的位置差异)。第一个单词的值为 -1,因为它是第一个单词,并且与前一个单词没有差异,但并不一定意味着它们与其他单词有任何区别(它们只是第一个重复项)。

如果下一个元素的值为-1,这意味着[8]和[11]也是连续的:

          doc1   doc2   difDoc1     difDoc2
    [8]     2      1     -1  (0-1)   -1  (0-1)
    [11]    7      4     -5  (2-7)   -3  (1-4)
    [13]    4      6      3  (7-4)   -2  (4-6)
    [14]    5      7     -1  (4-5)   -1  (6-7)

在这种情况下,[14] 中显示的距离与其之前的副本 [13][14] 之间的差异) 在两个文档中是相同的:-1:这意味着不仅是重复的,而且两者都被放置在两个文档中。

因此,我们不仅发现了重复的单词,而且还发现了这些行之间两个单词的重复序列

[13][14]--are you

相同的机制(确定两个文档中相同变量的 diff 为 -1)也有助于找到 2 个或更多单词的完整重复序列。如果所有重复项在两个文档中都显示出 -1 的差异,这意味着我们找到了完整的重复行

在本例中显示得更清楚:

   doc1- "here i am" [4,5,6]
   doc2- "here i am" [4,5,6]

listDuplicates - [4,5,6]

              doc1   doc2   difDoc1     difDoc2
        [4]    1      1      -1  (0-1)   -1  (0-1)
        [5]    2      2      -1  (1-2)   -1  (1-2)
        [6]    3      3      -1  (2-3)   -1  (2-3)
      

两个文档中相同变量的所有差异都是 -1 -> 两个文档中所有重复项彼此相邻 --> 两个文档中的句子完全相同。所以,这一次,我们发现了一个完整的重复行,包含 3 个单词

[4][5][6] -- here i am

除了这个重复序列搜索之外,这个差异表在计算重复单词的方差、中位数、...时也会很有帮助,以便获得某种“相似性”因子 (类似于文件之间公平的基本指示价值。绝不是确定的,但在某种程度上是有帮助的)


独特的价值 - 有助于获得非股权指示?

将使用类似的机制来获取这些唯一值。例如,通过从reverseMap 中删除重复项:

for (Integer i: listDuplicates)
    reverseMap.remove(i);

现在 reverseMap 只包含唯一值。 reverseMap.size() = 11

   KEY                     VALUE
   1                       welcome
   3                       what
   6                       doing
   9                       I
   10                      was
   12                      before
   15                      dumb

为了得到唯一的词:

reverseMap.values() = {welcome,what,doing,I,was,before,dumb}

如果您需要知道哪些唯一词来自哪个文档,您可以使用reverseMap(因为在您对它们执行诸如retainAll之类的方法后,列表可能会被更改):

  • 计算第一个文档的字数。这次是 7。
  • 如果 reverseMap 的键是 {welcome,what,doing}
  • 如果关键字 >7,则该唯一词来自第二个文档。 {I,was,before,dumb}

唯一性因素也可能是另一个指示性因素,这样,一个负面因素(因为我们在这里寻找相似之处)。还是很有帮助的。


equals 和 hashCode - 避免

由于字符串的hashcode() 方法不会为两个相同的单词返回相同的值(仅适用于两个相同的字符串对象引用),因此在这里不起作用。 String.equals() 方法通过比较字符(也检查长度,就像你手动做的那样)来工作,如果用于大文档,这将是完全矫枉过正:

public boolean equals(Object anObject) {  
      if (this == anObject) {  
          return true;  
      }  
      if (anObject instanceof String) {  
          String anotherString = (String) anObject;  
          int n = value.length;  
          if (n == anotherString.value.length) {  
              char v1[] = value;  
              char v2[] = anotherString.value;  
              int i = 0;  
              while (n-- != 0) {  
                  if (v1[i] != v2[i])  
                          return false;  
                  i++;  
              }  
              return true;  
          }  
      }  
      return false;  
  }  

我的意见是尽量避免这种情况,特别是 hashCode() 永远不要使用,因为:

String one = "hello";
String two = "hello"; 

one.hashCode() != two.hashCode()

这有一个例外,但只有在编译器实习字符串时;一旦加载了数千个,编译器将不再使用它们。在极少数情况下,两个字符串对象都引用相同的缓存内存地址,这也是正确的:

one.hashCode() == two.hashCode() --> true
one == two --> true

但这些确实是不寻常的例外,一旦字符串实习不踢,那些hashCodes 将不相等,并且比较字符串的运算符== 将返回false,即使字符串持有相同的值(和往常一样,因为它可以比较它们的内存地址)。

【讨论】:

  • 感谢您的帮助。我确定将每个唯一单词转换为整数,例如给它们 ID,这会大大减少运行时间,或者我希望如此 :) 我还在我的问题中添加了 EDIT 部分。你可以检查一下:)
  • 嗨@NaregBoynukalın,只是一个问题......每个文本的位置是否相关?我的意思是,如果您有来自两个文档的两页...您是否考虑到两个文档的第一行是否重复“Hello there”...。
  • 或者如果“hello there”之一位于第一个文档的页面开头,另一个“hello there”位于第二个文档的末尾,是否相关?希望我在这里解释
  • 嗨@aran。实际上不是。 “Hello there”语句可以在第一个文档的第一行,也可以是第二个文档的第 5 行。然后我说,第一个文档的第一行之间的共同声明。第二个文件的第 5 行是 "Hello there" 。然后我计算这些句子之间的相似率。
  • @NaregBoynukalın 更新了一些关于借助已识别的重复词识别重复序列的额外信息。无论如何,我想提出一些建议......非常不同。如果您可以将整行视为“节点”,我有一个建议
【解决方案2】:

基本技术是将其视为一个多阶段过程。关键是您不是要尝试将每个文档与每个其他文档进行比较,而是您有一个第一遍,可以在本质上是一次性过程中识别出可能匹配的小集群:

(1) 以允许识别候选匹配项的方式对文档进行索引或聚类;

(2) 根据那些索引/簇识别可能匹配的候选文档;

(3) 对于每个集群或索引匹配,有一个评分算法来对给定文档对的相似度进行评分。

根据文档的性质和数量,有多种方法可以解决 (1) 和 (3)。要考虑的选项:

  • 对于某些数据集,(1) 可以像索引不寻常的词/词的组合一样简单
  • 对于更复杂的文档和/或更大的数据集,您需要执行有时称为“降维”的操作:您需要对共享特征的组合进行聚类,而不是对共享的单词组合进行聚类,其中每个特征由一组单词标识。看看有时被称为“潜在语义索引”的特征提取技术——本质上,您将文档建模为“每个特征的单词”乘以“每个文档的特征”的矩阵,然后通过分解矩阵得到一组特征的近似值,以及构成每个特征的单词的加权列表
  • 然后,一旦您有一种方法可以识别要索引的一组词/特征,您就需要某种索引功能,这意味着候选文档匹配项具有相同/相似的索引键。查看余弦相似度和“位置敏感哈希”,例如 SimHash。
  • 那么对于 (3),给定一小组候选文档(或在散列系统中聚集在一起的文档),您需要一个相似度度量。同样,哪种方法合适取决于您的数据,但从概念上讲,您可以将其视为“对于文档 X 中的每个句子,在文档 Y 中找到最相似的文档并对其相似度进行评分;获得他的‘抄袭分数’这些值的总和”。有多种方法可以定义两个字符串之间的“相似度分数”:例如最长公共子序列、编辑距离、常用词对/序列的数量...

正如您可能从所有这些中想象的那样,没有一种算法可以准确地为您提供盘子上所需的东西。 (这就是为什么整个公司和研究部门都致力于解决这个问题......)但希望以上内容能给您一些指导。

【讨论】:

  • 非常感谢。我认为像“抄袭”这样的案例需要实现最长的公共序列。如果我理解得很好,这意味着找到两个句子之间最长的共同序列。它可以是 2 或 3 个常用词序列,例如“今天是个好日子”和“今天是坏日子”之间有 3 个词常用词序列“今天是一个”。然后我们根据序列的长度计算分数。我到现在了吗?
  • 通常定义 LCS 的方式是它可以“跳过”不匹配项,因此“I am a good day”和“I am a bad day”将匹配子序列“I am a (...)天”(即测量两个字符串中以相同顺序出现的字符数,跳过中间字符。)但您也可以按照您建议的方式定义它(有时“子序列”之间会有所不同)和“子串”来表示这种差异)。更复杂的算法也会尝试构建一些容错性(例如,这可能被称为“具有 k 个不匹配的最长公共子序列”)。
猜你喜欢
  • 2023-03-10
  • 2012-10-29
  • 1970-01-01
  • 2013-02-06
  • 2018-09-22
  • 1970-01-01
  • 2016-01-06
  • 2010-11-21
相关资源
最近更新 更多