2014-11-06 90 views
4

我将校园中的建筑名称与来自各种数据库的输入进行比较。人们输入这些名字,每个人都使用自己的缩写方案。我试图从用户输入中找到名称规范形式的最佳匹配。将多个单词名称与Levenshtein距离进行比较

我已经实现了一个递归Levenshtein距离方法,但有一些我试图解决的边缘情况。 My implementation is on GitHub

一些建筑物名称是一个字,而另一些是两个。单个单词上的单个单词会产生相当准确的结果,但有两件事我需要牢记。

  1. 缩写:假设输入是出了名的缩短版,我有时会在输入和任意的名字,以及正确的名称之间的相同Levenshtein距离。 例如,如果我的输入是“Ing”,而建筑物名称1.["Boylan", "Ingersoll", "Whitman", "Whitehead", "Roosevelt", and "Library"],那么对于BoylanIngersoll,我最终的LD都是6。期望的结果是Ingersoll

  2. 多字字符串:第二个边缘情况是当输入和/或输出是两个单词时,由空格分隔。例如,New IngNew Ingersoll的缩写。在这种情况下,新英格索尔和博伊兰的Levenshtein距离都是6分。如果我分割字符串,New完全匹配New,然后我只需要回到我以前的边缘情况的解决方案。

处理这两个边缘情况的最佳方法是什么?

1.为了好奇,这些是纽约市布鲁克林学院的建筑。

+0

你可以改变打分到折扣插入?对于这个缩写用例,似乎你应该将替代计算为比插入更大的距离。就我所知,你根本就没有在寻找拼写错误。 – 2014-11-06 20:46:56

+0

对,我在计算大部分情况下用序列替换空格。这实际上是一个很好的观察。但是,你如何计算这些数据呢? – Moshe 2014-11-06 20:50:56

+0

嗯,这取决于你的得分的细节,但我说的是 - 为了你的目的,但不是在经典的莱文斯坦距离 - “我” - >“B”应该比“我” >“Ie”。 – 2014-11-06 20:56:07

回答

3

我认为你应该使用Longest Common Subsequence代替Levenshtein距离的长度。这对你的情况来说似乎是一个更好的指标。本质上,它优先插入和删除替换,如我在我的评论中所建议的。 (Inggers) - >“Ingersoll”和“Ing” - >“Boylan”(分数为3和1)处理没有问题的空间(“New Ing” - >“New Ingersoll”得分7其中“New Ing” - >“Boylan”再次得分1),并且如果遇到像“Ingsl”这样的缩写,也会很好地工作。

算法很简单。如果您的两个字符串的长度为mn,请按字符比较字符串的连续前缀(以空白前缀开头),将分数保存在大小为m + 1,n + 1的矩阵中。如果某一对匹配,则在前两个前缀的分数中添加一个(矩阵中剩下的一行上一列);否则保持这些前缀的两个分数中的最高分(比较上面的条目和立即留下的条目并采取最好的条件)。当你经历了两个字符串时,得分矩阵中的最后一项是LCS的长度。

为 “Ingsll” 和 “的Ingersoll”

例记分矩阵:

 
     0 1 2 3 4 5 6 
     I n g s l l 
    --------------- 
0 | 0 0 0 0 0 0 0 
1 I | 0 1 1 1 1 1 1 
2 n | 0 1 2 2 2 2 2 
3 g | 0 1 2 3 3 3 3 
4 e | 0 1 2 3 3 3 3 
5 r | 0 1 2 3 3 3 3 
6 s | 0 1 2 3 4 4 4 
7 o | 0 1 2 3 4 4 4 
8 l | 0 1 2 3 4 5 5 
9 l | 0 1 2 3 4 5 6 

下面是一个ObjC执行长度。这里的大部分复杂性只是由于想要处理组合字符序列 - 例如@"o̶" - 正确。

#import <Foundation/Foundation.h> 

@interface NSString (WSSComposedLength) 

- (NSUInteger)WSSComposedLength; 

@end 

@implementation NSString (WSSComposedLength) 

- (NSUInteger)WSSComposedLength 
{ 
    __block NSUInteger length = 0; 
    [self enumerateSubstringsInRange:(NSRange){0, [self length]} 
          options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationSubstringNotRequired 
          usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) { 
           length++; 
          }]; 

    return length; 
} 

@end 


@interface NSString (WSSLongestCommonSubsequence) 

- (NSUInteger)WSSLengthOfLongestCommonSubsequenceWithString:(NSString *)target; 
- (NSString *)WSSLongestCommonSubsequenceWithString:(NSString *)target; 

@end 

@implementation NSString (WSSLongestCommonSubsequence) 

- (NSUInteger)WSSLengthOfLongestCommonSubsequenceWithString:(NSString *)target 
{ 
    NSUInteger * const * scores; 
    scores = [[self scoreMatrixForLongestCommonSubsequenceWithString:target] bytes]; 

    return scores[[target WSSComposedLength]][[self WSSComposedLength]]; 
} 

- (NSString *)WSSLongestCommonSubsequenceWithString:(NSString *)target 
{ 
    NSUInteger * const * scores; 
    scores = [[self scoreMatrixForLongestCommonSubsequenceWithString:target] bytes]; 

    //FIXME: Implement this. 

    return nil; 
} 

- (NSData *)scoreMatrixForLongestCommonSubsequenceWithString:(NSString *)target{ 

    NSUInteger selfLength = [self WSSComposedLength]; 
    NSUInteger targetLength = [target WSSComposedLength]; 
    NSMutableData * scoresData = [NSMutableData dataWithLength:(targetLength + 1) * sizeof(NSUInteger *)]; 
    NSUInteger ** scores = [scoresData mutableBytes]; 

    for(NSUInteger i = 0; i <= targetLength; i++){ 
     scores[i] = [[NSMutableData dataWithLength:(selfLength + 1) * sizeof(NSUInteger)] mutableBytes]; 
    } 

    /* Ranges in the enumeration Block are the same measure as 
    * -[NSString length] -- i.e., 16-bit code units -- as opposed to 
    * _composed_ length, which counts code points. Thus: 
    * 
    * Enumeration will miss the last character if composed length is used 
    * as the range and there's a substring range with length greater than one. 
    */ 
    NSRange selfFullRange = (NSRange){0, [self length]}; 
    NSRange targetFullRange = (NSRange){0, [target length]}; 
    /* Have to keep track of these indexes by hand, rather than using the 
    * Block's substringRange.location because, e.g., @"o̶", will have 
    * range {x, 2}, and the next substring will be {x+2, l}. 
    */ 
    __block NSUInteger col = 0; 
    __block NSUInteger row = 0; 
    [target enumerateSubstringsInRange:targetFullRange 
          options:NSStringEnumerationByComposedCharacterSequences 
          usingBlock:^(NSString * targetSubstring, 
             NSRange targetSubstringRange, 
             NSRange _, BOOL * _0) 
     { 
      row++; 
      col = 0; 

      [self enumerateSubstringsInRange:selfFullRange 
            options:NSStringEnumerationByComposedCharacterSequences 
            usingBlock:^(NSString * selfSubstring, 
               NSRange selfSubstringRange, 
               NSRange _, BOOL * _0) 
       { 
        col++; 
        NSUInteger newScore; 
        if([selfSubstring isEqualToString:targetSubstring]){ 

         newScore = 1 + scores[row - 1][col - 1]; 
        } 
        else { 
         NSUInteger upperScore = scores[row - 1][col]; 
         NSUInteger leftScore = scores[row][col - 1]; 
         newScore = MAX(upperScore, leftScore); 
        } 

        scores[row][col] = newScore; 
       }]; 
     }]; 

    return scoresData; 
} 

@end 

int main(int argc, const char * argv[]) 
{ 

    @autoreleasepool { 

     NSArray * testItems = @[@{@"source" : @"Ingso̶ll", 
            @"targets": @[ 
            @{@"string" : @"Ingersoll", 
             @"score" : @6, 
             @"sequence" : @"Ingsll"}, 
            @{@"string" : @"Boylan", 
             @"score" : @1, 
             @"sequence" : @"n"}, 
            @{@"string" : @"New Ingersoll", 
             @"score" : @6, 
             @"sequence" : @"Ingsll"}]}, 
           @{@"source" : @"Ing", 
            @"targets": @[ 
             @{@"string" : @"Ingersoll", 
              @"score" : @3, 
              @"sequence" : @"Ing"}, 
             @{@"string" : @"Boylan", 
              @"score" : @1, 
              @"sequence" : @"n"}, 
             @{@"string" : @"New Ingersoll", 
              @"score" : @3, 
              @"sequence" : @"Ing"}]}, 
           @{@"source" : @"New Ing", 
            @"targets": @[ 
             @{@"string" : @"Ingersoll", 
              @"score" : @3, 
              @"sequence" : @"Ing"}, 
             @{@"string" : @"Boylan", 
              @"score" : @1, 
              @"sequence" : @"n"}, 
             @{@"string" : @"New Ingersoll", 
              @"score" : @7, 
              @"sequence" : @"New Ing"}]}]; 

     for(NSDictionary * item in testItems){ 
      NSString * source = item[@"source"]; 
      for(NSDictionary * target in item[@"targets"]){ 
       NSString * targetString = target[@"string"]; 
       NSCAssert([target[@"score"] integerValue] == 
          [source WSSLengthOfLongestCommonSubsequenceWithString:targetString], 
          @""); 
//    NSCAssert([target[@"sequence"] isEqualToString: 
//       [source longestCommonSubsequenceWithString:targetString]], 
//       @""); 
      } 
     } 


    } 
    return 0; 
} 
+0

感谢您提供了一个很好的建议和详细的答案。在这种情况下':='操作符意味着什么? (我来自C/Objective-C背景 – Moshe 2014-11-07 08:44:53

+1

这是伪代码赋值操作符,我刚刚注意到它在片段中没有被一致使用 – 2014-11-07 19:23:28

+1

@Moshe:我已经添加了ObjC实现 – 2014-11-13 23:35:20

2

我认为Levenshtein距离只有在处理类似休闲拼写错误时才有用。如果Levenshtein距离比这个词本身更长,那么它就没有价值的意义了。 (在你的例子中,“Ing”和“Boylan”没有任何共同之处,没有人会混淆这些词。要从“Ing”到“Boylan”,你需要六次编辑,两倍于单词)我甚至不会考虑具有明显不同长度的词语(如“Ing”和“Ingersoll”)之间的Levenshtein距离,并声明它们不同。

相反,我会检查缩写模式下比原始文字短的单词。要检查单词是否是较长单词的缩写,可以检查缩写的所有字母是否以相同顺序出现在原始单词中。你还应该强制这些词以同一个字母开头。然而,这种方法没有考虑错误的缩写。

我认为多字字符串可以更好地解析字面。你需要区分英格索尔和新英格索尔吗?在这种情况下,您可以建立一个评分系统,其中单词匹配得分为100,可能是减去Levenshtein距离的十倍。不匹配的分数为负值,例如-100。然后你用文字在建筑数量评估每个单词和鸿沟的分数:

如果你的字符串是“英格索尔”:

  • “英格索尔”得分100/1 == 100
  • “新英格索尔”得分100/2 == 50

如果字符串是 “新英格索尔”:

  • “英格索尔”得分(100 - 100)/1 == 100
  • “新英格索尔”得分(100 + 100)/2 == 100

字,明智的做法放平当您有包含各种单词,例如字母缩写新英格索尔的“NI”或“NIng”,所以如果在词对词匹配中找不到匹配项,也许你应该在整个建筑物名称上面尝试缩写匹配。

(我知道这一切是不是一个真正的答案,但更多的是一堆松散的想法。)

相关问题