2014-11-06 4 views
4

저는 캠퍼스의 건물 이름을 다양한 데이터베이스의 입력과 비교하고 있습니다. 사람들은이 이름을 입력하고 모두가 자신의 약어를 사용합니다. 나는 사용자 입력에서 가장 일치하는 이름을 정식 형식으로 찾으려고합니다.여러 단어의 이름을 Levenshtein 거리와 비교하기

저는 재귀적인 Levenshtein Distance 방법을 구현했습니다. 그러나 해결하려는 몇 가지 단점이 있습니다. My implementation is on GitHub.

일부 건물 이름은 한 단어이고 나머지는 2 자입니다. 한 단어에 나오는 한 단어만으로도 정확한 결과를 얻을 수 있지만 두 가지 점을 명심해야합니다.

  1. 약어를 : 입력을 가정는 이름의 단축 버전, 가끔 입력 및 임의의 이름뿐만 아니라 올바른 이름 사이에 같은 Levenshtein 거리를 얻을 수있다. 예를 들어 입력 한 내용이 "Ing"이고 건물 이름이 인 경우["Boylan", "Ingersoll", "Whitman", "Whitehead", "Roosevelt", and "Library"] 인 경우 BoylanIngersoll 모두에 대해 LD가 6으로 끝납니다. 원하는 결과는 여기 Ingersoll입니다.

  2. 다중 단어 문자열 : 두 번째 엣지 경우는 입력 및/또는 출력이 두 단어이고 공백으로 구분 된 경우입니다. 예를 들어, New IngNew Ingersoll의 약자입니다. 이 경우 New Ingersoll과 Boylan이 둘 다 Levenshtein Distance 6 점을 받았습니다. 문자열을 분할하면 NewNew과 완벽하게 일치합니다. 그런 다음 이전 사례에 대한 솔루션을 다시 참조해야합니다.

이 두 가지 경우를 처리하는 가장 좋은 방법은 무엇입니까?

1. 흥미로운 점은 뉴욕시의 브루클린 칼리지에있는 건물들입니다.

+0

할인 점수로 점수를 변경할 수 있습니까? 이 약어 사용 사례의 경우 삽입을 삽입하는 것보다 더 큰 거리로 대체해야하는 것처럼 보입니다. 당신은 내가 볼 수있는 한, 오자를 찾는 것이 아닙니다. –

+0

맞아요, 대부분의 경우 시퀀스로 공백을 대체합니다. 그것은 실제로 큰 관찰입니다. 그래도 그걸 어떻게 생각하니? – Moshe

+0

음, 득점의 세부 사항에 달려 있지만, 내가 말하는 것은 고전적인 Levenshtein 거리가 아니라 당신의 목적입니다. "I"- "B"는 "I"이상의 비용이들 것입니다. > "예". –

답변

3

내가 대신 Levenshtein 거리의 Longest Common Subsequence의 길이를 사용한다고 생각합니다. 그것은 귀하의 경우에 대한 더 나은 척도가되는 것 같습니다. 본질적으로, 그것은 내 의견에서 제안했듯이, 대체보다 삽입과 삭제를 우선시합니다.

"Ing"-> "Ingersoll"과 "Ing"-> "Boylan"(3 점과 1 점) 사이의 공백은 문제없이 처리됩니다 ("New Ing"-> "New Ingersoll" 여기서 "New Ing"-> "Boylan"은 다시 1 점을받습니다) "Ingsl"과 같은 약어를 사용하면 잘 작동합니다.

알고리즘은 간단합니다. 두 개의 문자열 길이가 mn 인 문자열의 연속 접두사를 빈 접두어로 비교하고 크기가 m + 1, n + 1 인 행렬에 점수를 유지하십시오. 특정 쌍이 일치하는 경우 이전 두 접두사의 점수에 1을 더합니다 (매트릭스에 한 행 위로 한 열). 그렇지 않으면 그 접두사의 두 점수 중 가장 높은 점수를 유지하십시오 (바로 위의 항목과 즉시 남은 항목을 비교하여 최상을 취하십시오). 두 문자열을 모두 살펴 보았을 때 점수 매트릭스의 마지막 항목은 LCS의 길이입니다. "Ingsll"와 "잉거 솔"에 대한

예 점수 매트릭스 :

 
     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

+1

의사 코드 지정 연산자입니다. 해당 스 니펫에서 일관되게 사용되지 않는 것으로 나타났습니다. –

+1

@Moshe : ObjC 구현을 추가했습니다. –

2

필자는 Levenshtein 거리가 일반적인 맞춤법 오류와 비슷한 거의 유사한 단어를 다루는 경우에만 유용하다고 생각합니다. Levenshtein 거리가 단어 자체보다 길면 유사 가치로서 가치있는 의미가 없습니다. (예를 들어 "Ing"와 "Boylan"은 공통점이 없으며 아무도이 단어를 혼동하지 않습니다. "Ing"에서 "Boylan"으로 전환하려면 단어의 두 배인 여섯 개의 편집이 필요합니다. 글자가 있습니다.) "Ing"와 "Ingersoll"과 같이 상당히 다른 길이의 단어 사이에 Levenshtein 거리를 고려하지 않고 다른 것으로 선언합니다.

대신 약자 모드의 원본보다 짧은 단어를 확인합니다. 단어가 긴 단어의 약어인지 확인하려면 약어의 모든 문자가 동일한 순서로 원본에 나타나는지 확인할 수 있습니다. 단어가 동일한 문자로 시작하도록 강요해야합니다. 그러나이 방법은 잘못 입력 된 약어를 ​​설명하지 않습니다.

여러 단어 문자열을 단어별로 더 잘 파싱한다고 생각합니다. Ingersoll과 New Ingersoll을 구별해야합니까? 이 경우 단어 일치 점수 100 점, Levenshtein 거리의 10 배를 뺀 점수 체계를 설정할 수 있습니다. 불일치는 -100이라고하는 부정적인 점수를가집니다.그럼 당신은 건물의 단어의 숫자로 각 단어와 분열의 점수를 평가 :

  • "Ingersoll은"점수 100/1 == 100
  • "새 Ingersoll은"점수 :

    당신의 문자열이 "Ingersoll은"이면

    100/2 == 50

당신의 문자열은 "새 Ingersoll은"인 경우 :

  • "Ingersoll은"점수 (100 - 100)/1 == 100(100 + 100)/2 == 100

다양한 단어, 예로부터 문자를 포함 약어있을 때 단어 현명한 방법은 평면 폭포

  • "새 Ingersoll은"점수 New Ingersoll의 경우 'NI'또는 'NIng'입니다. 따라서 단어 대 단어 검색에서 일치하는 항목을 찾을 수없는 경우 위의 약어 일치를 전체 건물 이름에 적용해야합니다.

    는 (나는 모든 일이 정말 대답하지만, 생각보다 느슨한 무리 아니라는 것을 알고 있습니다.)