2014-11-06 106 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」,所以如果在詞對詞匹配中找不到匹配項,也許你應該在整個建築物名稱上面嘗試縮寫匹配。

(我知道這一切是不是一個真正的答案,但更多的是一堆鬆散的想法。)