[출처 : http://latest.tistory.com/entry/Dices-coefficient]


키워드간의 유사성을 측정하는 수학 공식이다.



n 와 n에 대한 각 bigrams 의 개수를 구하고 그 중에 중복된 bigrams의 개수를 nt 라고 한다.

예를 들면 아래와 같다.

night => {ni,ig,gh,ht}
nacht  => {na,ac,ch,ht}

공식에 대입하면 아래와 같은 결과를 얻을 수 있다.

s
 = (2 · 1) / (4 + 4) = 0.25.


참고 url : http://en.wikipedia.org/wiki/Dice's_coefficient

[출처 : http://adnoctum.tistory.com/298]


   구글이나 네이버에서 검색을 할 때, 영어 단어를 '정확히' 입력하지 않았을 경우, '비슷한' 단어로 검색할 것을 추천하는 것을 본 경험이 있을 것이다. 그와 같이, 두 단어가 정확히 같은 것인가를 판단하는 것이 아니라, 대충 비슷한 것인가를 판단할 수 있는 방법을 살펴 보자. 즉, '비슷한 정도'를 수치화 할 수 있는 방법을 살펴 보자. 

   기본적인 아이디어는 단어를 잘게 자른 조각(그것을 shingle 이라 하자)들을 모은 후, 그 조각들이 많이 비슷하면 비슷할수록 두 단어가 비슷하다는 것이다. 예를 들어 보자. adenophorae radix  와  adenophora 를 비교하는 모습을 살펴 보자. 논의의 편의를 위하여 shingle 의 길이를 2 개로 제한하자. 

adenophorae radix --> ad
adenophorae radix --> de
adenophorae radix --> en
adenophorae radix --> op
...
adenophorae radix --> di
adenophorae radix --> ix

따라서 adenophorae radix 의 shingle 은 { 'ad', 'de', 'en', ..., 'ix' } 가 된다. 같은 식으로 adenophora 에 대한 shingle도 구할 수 있다. 주어진 단어에 대한 shingle 의 집합을 S라 하자. 즉,

S('adenophorae radix', 2) = { 'ad', 'de', 'en', ..., 'ix' },
S('adenophora', 2) = { 'ad', ..., 'ra' }

가 된다. 쉽게 생각을 해보자. 위와 같이 shingle 을 정의한 상태에서, 두 단어가 '비슷하다'면? 즉, 두 집합이 비슷하다면? 그렇다면 두 집합을 교집합 시킨 것과 합집합 시킨 것이 유사할 것이다. 즉 두 집합 A 와 B에 대하여 

n( A ∩ B ) ≒ n( A ∪ B )

일 것이다. 다시 표현하면, 

n( A ∩ B ) ÷ n( A ∪ B ) ≒ 1.0

일 것이다. 따라서, 위의 경우에 대하여, resemblance R 을, 

R(A,B) = n( A ∩ B ) ÷ n( A ∪ B )    --------------------------------- (1)

로 정의하면, 우리가 생각하는 것을 수학적으로 적절히 표현할 수 있게 된다. 극단적인 경우, R(A,A) = 1.0 인 것이다. 위 식을 C++ 코드로 간단히 표현해 보자. 

접기

std::set<std::string> get_shingle(const std::string& str, int size)

{

        std::set<std::string> shingle;

        int length = (int)(str.size());

        int max_pos = length - size;

        int i = 0;

        for(= 0; i<=max_pos; i++){

                shingle.insert(str.substr(i, size));

        }

        return shingle;

}

 

bool get_resemblance(

const std::string& str1, 

const std::string& str2, 

double *r, double *c1, double *c2)

{

        std::set<std::string> s1 = get_shingle(str1, 2);

        std::set<std::string> s2 = get_shingle(str2, 2);

        if(s1.empty() == true || s2.empty() == true) return false;

        std::set<std::string> com;

        std::set<std::string> all;

        std::set_intersection(s1.begin(), s1.end(), 

s2.begin(), s2.end(), 

std::inserter(com, com.begin()));

        std::set_union(s1.begin(), s1.end(), 

s2.begin(), s2.end(), 

std::inserter(all, all.begin()));

 

 

        double com_size = (double)(com.size());

        double union_size = (double)(all.size());

 

        *= com_size/union_size;

        *c1 = com_size/s1.size();

        *c2 = com_size/s2.size();

 

        return true;

}


(최대한 명시적 코드를 작성하기 위해 성능은 거의 고려하지 않음)

접기




get_shingle 함수는 주어진 단어를 주어진 길이로 자른 조각 집합을 반환해 준다. get_resemblance 함수는 위의 식에 따라 계산한 값들을 반환한다. containment, 즉, 한 단어가 다른 단어에 '포함'되어 있는가 역시 '비슷한' 단어가 포함되어 있는가로 생각해 볼 수 있고, 쉽게 계산할 수 있다. get_resemblance 함수는 containment 까지 반환한다. 위 코드로 adenophorae radix 와 adenophora 의 resemblace 와 containment 를 구해 보면, 

resemblance : 0.642857 
containment of 'adenophora' in 'adenophorae radix' : 1.0
containment of 'adenophorae radix' in 'adenophora' : 0.642857

가 나온다. 어느 정도 합당한 값으로 보인다. 



위의 알고리즘은 Syntactic Clustering of the Web(pdf임)에서 소개된 것으로, 인터넷에서 두 '문서'의 유사도를 계산하기 위하여 제안된 것이다. 위처럼 결과적으로 방법은 간단해도 그것은 어느 정도 수학적으로 뒷받침된다. 본 문서에서는, 두 문서의 유사도를 계산할 때, shingle 을 섞은 후1 s 개를 선택해서 위의 식(1)처럼 계산하거나, m 번째 요소를 선택해서 식(1)처럼 계산하면 resemblance 에 대한 unbiased estimator 가 된다는 것이다. 또, shingle 은 자갈, 작은 돌, 로 해석할 수 있겠는데, 문서들을 단어들로 나눠서 단어들이 얼마나 비슷하게 본다는 개념에서 단어들을 '자갈'이라고 표현한 것이다. 하여튼 이와 같은 알고리즘을, 단어의 유사도를 찾기 위하여 나는 위처럼 다소 간단히 적용해 보았다. 음... 저 알고리즘을 적용해야 할 데이터가 결국은 사람이 눈으로 다시 한 번 확인을 해야만 하는 것이라서 대충 구현해도 되기 때문에... 




  1. 원래 문서에 permutation 으로 표현된 π 라는 operator 를 적용하는 것이 곧 배열을 무작위로 섞는 것을 의미한다.[본문으로]
[출처 : http://blog.naver.com/websearch?Redirect=Log&logNo=70093912364]


완성형의 한글 코드 영역은 다음과 같다. 완성형은 KSC5601 문자셋을 의미한다.

 

* 상위 바이트 : 0xBO ~ 0xC8

* 하위 바이트 : 0xA1 ~ 0xFE

 

확장 완성형의 한글 코드는 완성형의 한글 코드에 아래의 한글 코드 영역이 추가된다. 확장 완성형은 MS CP949 를 의미한다. 중간 중간 빈 공간을 채워 넣었기 때문에 아래의 코드 영역은 연속적이지 않다.

 

* 상위 바이트 : 0x81 ~ 0xC6

* 하위 바이트 : 0x41 ~ 0xFE

 

유니코드에서 한글 코드 영역은 다음과 같다.

 

* 0xAC00 ~ 0xD7A3

[출처 : http://www.kristalinfo.com/K-Lab/unicode/Unicode_intro-kr.html#utf8]


UTF-8

가장 완벽하게 유니코드 표준을 표현하는 인코딩 방식은 UTF-16이라고 할 수 있습니다. 그런데 이 인코딩에서는 16비트 단위로 하나의 문자가 표현되기 때문에 전통적인 char 형과는 맞지 않는 부분이 있습니다. UTF-16으로 문자열을 표현했을 때 전통적인 char 형으로 이 문자열 취급하게 되면 중간에 널(null, 0)값이 들어가게 되어 문제가 발생합니다. 즉 유니코드를 지원하려면, 지금까지 개발된 모든 프로그램을 재개발해야 한다는 부담이 발생합니다. 현실적으로 이것은 거의 불가능한 일이라고 할 수 있습니다.

이의 대안으로 제시된 인코딩이 바로 UTF8입니다. UTF-8은 문자열의 중간 바이트에서 0이 나타나지 않도록 고안되었습니다. 이를 위해 각 유니코드 문자는 1바이트에서 4바이트까지 가변적으로 인코딩되도록 하고 있습니다. UTF-8에서는 U+0000 ~ U+007F까지의 128자는 1바이트로 표현되는데 이는 ASCII와 동일합니다. 또 U+0080 ~ U+07FF까지는 두 바이트, U+0800 ~ U+FFFF까지는 세 바이트로 표현됩니다. 즉 BMP내의 모든 문자는 1 ~ 3바이트로 표현됩니다. 그리고 나머지 16개의 보충언어판에 위치하는 1,048,576개의 코드는 네 바이트로 인코딩됩니다. 다음 표에서 UCS-4와 UTF-8간의 변환 방법을 나타내고 있습니다.

표. UTF-8과 UCS-4간의 변환 규칙

UCS-4 UTF-8

위의 표에서는 UCS-4와 UTF-8간의 변환 규칙을 보여주고 있습니다만, UCS-4의 모든 영역에 대해서 보여주고 있습니다. 그러나 빨간색으로 칠한 부분을 0x0010FFFF로 변경하고 나머지는 삭제해야 정확한 유니코드의 UTF-8 인코딩이라고 할 수 있습니다. 이를 모두 표현한 것은 6바이트까지 확장하면 UTF-8로도 UCS-4의 전역을 인코딩할 수 있음을 보여주기 위한 것입니다. 아마도 1백만자의 코드이면 지구상에 존재했거나 존재하는 모든 문자를 표현하기에 충분할 것으로 예상됩니다. 우주의 모든 언어코드를 표현해야 할 때가 오면 0x00110000 이후의 영역도 사용할 가능성이 조금이나마 있을까요?

어쨌든, 현재 유니코드 표준에서는 UTF-8이 4바이트까지로 인코딩됩니다. 유니코드에 정의된 각 코드는 반드시 다음과 같은 범위에서 인코딩되어야 합니다. 다음 표는 UTF-8 인코딩에서 각 바이트에 올 수 있는 값을 보여주고 있습니다.

표. 올바른 UTF-8 바이트 배열

Code Points 1st Byte 2nd Byte 3rd Byte 4th Byte
U+0000 ~ U+007F
U+0080 ~ U+07FF
U+0800 ~ U+0FFF
U+1000 ~ U+CFFF
U+D000 ~ U+D7FF
U+D800 ~ U+DFFF
U+E000 ~ U+FFFF
U+10000 ~ U+3FFFF
U+40000 ~ U+FFFFF
U+100000 ~ U+10FFFF

[출처 : http://kerbtier.ch/2008/12/30/levenshtein-to-slow-how-to-speed-it-up]


you don’t need 40′000 levenshtein calls

one attempt to make it faster is to rule out some strings by comparing lengths first. if you say your maximum distance for two strings to match is 10 then you can discard strings which difference in character count is bigger than 10. it’s already much faster, for the 40′000 and for my usage almost fast enough but the faster the better. in this case i still have to check one strings length against 40′000 other strings length. this can be speed up by putting the sets of the strings with the same length into an array where the index is the length of the sets strings. so i don’t have to compare it against all strings. if l is the length of the string i want to test then i need to test it only against the strings in the sets for the indexes with a length from l-10 to l+10. probably i can rule out with this technique even more strings, by example count the number of words in the strings instead of the number of characters or the number of vowels. these approaches could be combined together and it probably will speed it up another bit. but due to statistics the result would probably be about the same like in the case where i use the total length of the string.
[출처 : http://pro.cug.kr/tag/Levenshtein%20Distance]


URL: http://www.merriampark.com/ld.htm

자주쓰이는 3개의 언어로 구현한 Levenshtein Distance

작성자: Michael Gilleland, Merriam Park Software

이 짧은 에세이를 쓰게 된 것은 Levenshtein distance 알고리즘에 대해서
설명하고 또 그것이 각각의 세가지 프로그래밍 언어에서 어떻게 구현되는가를
보이기 위해서입니다.

Levenshtein Distance이란 무엇인가?
데모
알고리즘
세가지 언어로 구현된 소스코드
레퍼런스
다른 언어로 구현

Levenshtein Distance이란 무엇인가?

Levenshtein Distance(이하 LD)는 두 문자열의 비슷한 정도를 측정하기위해 고안되었습니다.
여기서 원문자열을 (s)로, 대상문자열을 (t) 라고 나타낸다고 하겠습니다. distance란 s를
t로 변형시키기 위해 삭제, 추가, 또는 수정이 필요한 횟수를 뜻합니다. 예를든다면,

* s가 "test"이고 t도 "test"라면, LD(s,t) = 0 이 됩니다. 왜냐하면 문자열들이 이미 동일하여 변환이 필요하지 않기 때문입니다.

* s가 "test"이고 t가 "tent"라면, LD(s,t) = 1 이 됩니다. 왜냐하면 s를 t로 만들기 위해서는 "s"를 "n"으로 한번 수정이 필요하기 때문입니다.

Levenshtein distance는 string 간의 차이가 클수록 위대함을 느낄 수 있습니다.

Levenshtein distance는 러시아 과학자인 Vladimir Levenshtein가 1965년에 고안하여 그렇게 이름지어졌습니다.
Levenshtein 이란 단어가 쓰거나 읽기 힘들기 때문에 종종 edit distance라고도 불립니다.

Levenshtein distance 알고리즘은 다음과 같은 분야에 쓰여집니다:

* 철자 검사
* 음성 인식
* DNA 분석
* 표절여부 검사

데모

아래의 간단한 자바 애플릿으로 두 문자열의 Levenshtein distance를 알아보세요.

원래 문자열
대상 문자열


알고리즘

알고리즘 작동 단계
단계 설명

s의 문자열 길이를 n에 넣는다.
t의 문자열의 길이를 m에 넣는다.
만약 n = 0 이라면, m 을 리턴하고 종료한다.
만약 m = 0 이라면, n 을 리턴하고 종료한다.
0..m 행과, 0..n 열로 이루어진 행열을 만든다.
2
첫번째 행인 0..n을 초기화 한다.
첫번째 열인 0..m을 초기화 한다.
3
s의 각 문자(i는 1부터 n까지)를 검사한다.
4
t의 각 문자(j는 1부터 m까지)를 검사한다.
5
s[i]와 t[j]가 같다면, 변경하기 위한 비용은 0이 된다.
s[i]와 t[j]가 같지 않다면, 비용은 1이 된다.
6
행열의 셀 d[i,j]에 다음의 것들 중 가장 작은 값을 넣는다.
a. 바로 위의 셀이 더하기 1이 되는 경우: d[i-1, j] + 1
b. 바로 왼쪽 셀이 더하기 일이 되는 경우: d[i,j-1] + 1
c. 대각선으로 연속적인, 바로 왼,위쪽 셀의 비용: d[i-1,j-1] + cost
7
(3, 4, 5, 6) 단계를 반복하여 완료되면, d[n, m]셀에 있는 것이 distance가 된다.

예제

이 예제절에서는 원래 문자열이 "GUMBO"이고 대상 문자열이 "GAMBOL"이라 할 때
어떻게 Levenshtein distance가 계산되는지에 대해서 다룬다.

1 과 2 단계

i가 1일 때 3에서 6 단계

i가 2일 때 3에서 6 단계 

i가 3일 때 3에서 6 단계

i가 4일 때 3에서 6 단계

i가 5일 때 3에서 6 단계

7단계
행열의 가장 오른쪽 아래에 있는 값이 distance가 된다.(여기서는 2)
이 결과는 "GUMBO"가 "GAMBOL"이 되기 위해서 "U"를 "A"로 바꾸고
"L"을 추가해야한다는, 직관적으로 알 수 있는 결과와 일치합니다.
( 1번의 수정과 1번의 추가 = 2 번의 변경 )

세가지 언어로 구현된 소스코드

프로그래밍 언어들간에 차이에 대해서 토론하는 엔지니어들 사이에서는 종교 전쟁이 일어나기도합니다.
이러한 예로, 전형적인 주장은 JavaWorld article에서 일어난(July 1999) Allen Holub의 주장입니다.:
"예를들자면, 비주얼 베이식은 전혀 객체지향이라고 말할 수 없다. Microsoft Foundation Classes(MFC) 
또는 대부분의 다른 마이크로소프트의 테크놀러지는 어느것도 객체지향이라 주장할 수 없다."

Salon에 계제된(Jan. 8, 2001) Simson Garfinkels의 글에서 다른 진영의 반박이 이루어졌습니다.
이 글은 "Java: 느리고, 꼴사납고, 부적절한 언어"라는 제목으로 알려져 있는데, 명료하게
표현하자면 "나는 자바를 증오해"라고 나타낼 수 있습니다.

우리는 이러한 종교 전쟁들 속에서 자연스럽고 조심스런 입장을 취하로 했습니다. 배우기 위한 교재로써,
하나의 프로그래밍 언어에서만 해결할 수 있는 문제라면 대개 다른 언어에서도 마찬가지로 해결할 수
있을 것입니다. 우수한 프로그래머는 완전히 새로운 언어를 배우면서 한다고 하더라도 하나의 언어에서 
다른 언어로 비교적 쉽게, 큰 어려움에 당면하지 않고 옮길 수 있습니다. 프로그래밍 언어라는 것은
목적을 이루기 위한 것이지, 그 자체가 목적은 아닌 것입니다.

이러한 중도의 입장에서, 우리는 Levenshtein distance 알고리즘을 아래에 있는 프로그래밍 언어들로 
구현하여 소스코드를 보였습니다.

* Java
* C++
* Visual Basic

소스코드들 (블라블라)

참고문헌

Levenshtein distance에 관련된 다릍 토의를 다음 링크들에서 발견하실 수 있습니다.

* http://www.csse.monash.edu.au/~lloyd/tildeAlgDS/Dynamic/Edit.html (Lloyd Allison) 
* http://www.cut-the-knot.com/do_you_know/Strings.html (Alex Bogomolny) 
* http://www-igm.univ-mlv.fr/~lecroq/seqcomp/node2.html (Thierry Lecroq) 

다른 언어로 구현

아래 있는 분들은 그들 각자가 다양한 언어로 Levenshtein Distance 알고리즘을 구현한 것을
여기서 사용할 수 있게 친절히 승낙해 주셨습니다.

* Eli Bendersky 은 펄로 구현해주셨습니다.
* Barbara Boehmer 은 Oracle PL/SQL 로 구현해주셨습니다.
* Rick Bourner Objective-C 로 구현해주셨습니다.
* Joseph Gama 는 TSQL로 Planet Source Code 에 있는 TSQL 함수 패키지의 한 파트로 구현해주셨습니다.
* Anders Sewerin Johansen 는 C++로 제가 만든 것보다 C++의 정신에 가깝게, 더 최적화되고 세련되게 구현해주셨습니다.
* Lasse Johansen 는 C#으로 구현해 주셨습니다.
* Alvaro Jeria Madariaga는 Delphi로 구현해 주셨습니다.
* Lorenzo Seidenari 는 C로 구현해 주셨습니다.
* Steve Southwell는 Progress 4gl로 구현해 주셨습니다.

이 페이지 밖에 있는 다른 구현들:

* Art Taylor의 Emacs Lisp로의구현.
* Magnus Lie Hetland의 Python로의 구현.
* Richard Suchenwirth의 Tcl로의 구현(링크가 깨진 것을 알려주신 Stefan Seidler님 감사합니다).
[출처 : http://markos.gaivo.net/blog/?p=211]


Simon proposes using a dictionary to match mistyped URLs with real ones. I’d probably like the idea better if I actually understood it. Still using Levenshtein can be a bit easier than using a spell checker responsively.

Easier, but slow. I used existing implementation by Magnus Lie Hetland and made a test with 245 blog titles using a simple script. 100 iterations on aging powerbook produced:

1.766s, 29.152s, 9.399s (min, max, avg)

Average time to calculate distance between randomly chosen title and rest of them is 9.4 seconds, which is far too much to be useful. And there’s not even 250 of them.

There are two obvious ways to speed things up. Since minimum distance is at least as much as a difference in length of both strings, there’s no point in calculating it when difference already exceeds a limit we chose.

The other trick took into an account that Levenshtein’s algorithm of two strings of comparable length has complexity of O(n2) and that my blog titles are form quite sparse space. If strings are longer than a certain limit (I arbitrarily chose 10 letters), then first calculate edit distance on a small sparse substring and then calculate the real distance only if first one was close enough.

When I made 1000 iterations of randomly chosen title using only first test, I got following results:

0.003s, 0.284s, 0.167s (min, max, avg)

However, if I used both checks, the same 1000 iteration test got me:

0.003s, 0.162s, 0.027s (min, max, avg)

So, two simple checks can speed up search up to 350 times. Not bad, but I’m not happy. This is certainly fast enough for a personal website or sites where site structure effectively divides searching to relatively small sets of possible hits, but there are plenty of sites where it would be too slow.

[출처 : http://inthechaos.tistory.com/entry/탐색-SearchMainc]




#pragma warning(disable:4996)
#include <stdio.h>
#include <stdlib.h>
#include "Search.h"
//상수 정의
enum { ARRAY_SIZE = 100, MAX_VALUE = 1000 };
void fillArray(int *array, int size);
void printArray(const int *array, int size);
int compare(const void *p1, const void *p2);
void main(void)
{
 int array[ARRAY_SIZE];
 int value, pos;
 fillArray(array, ARRAY_SIZE);
 printArray(array, ARRAY_SIZE);
 //순차 탐색
 printf("[순차 탐색]\n");
 while(1)
 {
  printf("입력 : ");
  scanf("%d", &value);
  if(value < 0 || value >= MAX_VALUE)
  {
   break;
  }
  pos = linearSearch(array, ARRAY_SIZE, value);
  printf("결과 : %s\n\n", (pos != -1) ? "성공" : "실패");
 }
 printf("\n\n");
 //정렬
 qsort(array, ARRAY_SIZE, sizeof(array[0]), compare);
 printArray(array, ARRAY_SIZE);
 //이진 탐색
 printf("[이진 탐색]\n");
 while(1)
 {
  printf("입력 : ");
  scanf("%d", &value);
  if(value < 0 || value >= MAX_VALUE)
  {
   break;
  }
  pos = linearSearchSort(array, ARRAY_SIZE, value);
  printf("순차 : %s\n\n", (pos != -1) ? "성공" : "실패");
  pos = binarySearch(array, ARRAY_SIZE, value);
  printf("이진 : %s\n\n", (pos != -1) ? "성공" : "실패");
 }
}
void fillArray(int *array, int size)
{
 int i;
 for(i=0; i<size; i++)
 {
  array[i] = rand() % MAX_VALUE;
 }
}
void printArray(const int *array, int size)
{
 int i;
 for(i=0; i<size; i++)
 {
  printf("%3d ", array[i]);
  if(i%10 == 9)
  {
   printf("\n");
  }
 }
 printf("\n\n");
}
int compare(const void *p1, const void *p2)
{
 return *(int*) p1 - *(int*) p2;
}

+ Recent posts