편집 거리 (Edit Distance) 에 대해
단어 “둥굴랭”과 단어 “둥굴레차” 가 오타와 정타 관계 구나 라는 것을 ‘사람’이라면 직관적으로 인지할 수 있다. 하지만 ‘컴퓨터’는 어떻게 판단할 수 있을까? 이렇게 두 문자열 사이의 유사도를 수치화할 수 있는 방법 중의 하나로 편집거리 알고리즘을 사용한다.
편집 거리는 문자열 A 가 문자열 B와 동일하게 되기 위한 최소 변경(삽입, 삭제, 대체) 횟수를 말한다.
예를 들어 [둥굴랭] 와 [둥굴레차] 의 편집 거리를 구해보자.
[둥굴랭] 이 [둥굴레차] 가 되기 위해서는
- ‘랭’을 ‘레’로 대체
- ‘차’ 는 삭제
하면 [둥굴레차] 가 되므로 이 둘의 편집 거리는 2이다.
※ 편집 거리 알고리즘을 수학자 Vladimir Levenshtein 이 고안했다고 해서 Levenshtein distance 라고도 불린다.
※ 이제부터 ‘문자열 A 가 문자열 B 와 동일하게 되기 위함’이란 의미를 간단하게 A → B 로 표기하겠다.
편집 거리 알고리즘
위키피디아에서 제공하는 편집 거리 알고리즘은 위와 같다. 2차원 배열에 값을 채워넣는 형태이며 위쪽 칸(deletion), 왼쪽 칸(insertion), 대각선 칸(substitution) 값 중 최소값을 취하고 있다. (지금 당장은 이해 안 해도 된다! 아래로 쭉쭉 넘어가자)
이를 그림으로 나타내보면 아래와 같고
다음 2가지 규칙을 활용해 숫자를 채워 넣으며, 우측 하단 값이 구하고자 하는 편집 거리이다.
① 비교하는 글자가 동일하면 대각선 값 그대로
② 비교하는 글자가 다르면 옆,대각선,위 중 최소값 + 1
이제 글을 계속 읽는다면
- 왜 왼쪽은 삽입, 대각선쪽은 치환, 위쪽은 삭제라고 하는지 알 수 있다.
- 왜 글자가 같으면 대각선 위 값을 그대로 가지는지 알 수 있다.
- 왜 글자가 다르면 왼쪽, 대각선쪽, 위쪽 중 최소값에 1을 더하는지 알 수 있다.
알고리즘 이해하기
컴퓨터는 사람과는 다르게 직관적인 판단이란 게 없으므로 모든 경우의 수를 다 따져봐야 한다. 이 와중에 도움을 줄 수 있는 것이 동적 프로그래밍 기법이다. 동적 프로그래밍은 큰 문제를 작은 문제로 쪼갠 후에 작은 문제의 최적화된 답을 찾고 그 답을 기반으로 점차 문제를 확장해 최종적으로 가장 큰 문제의 답을 정의하는 방법이다.
예를 들어 둥굴랭 → 둥굴레차 를 바로 구하기는 어려우니 둥 → 둥 은 최소 몇 번의 단계가 필요하지? 둥 → 둥굴 은 최소 몇 번의 단계가 필요하지? … 이렇게 작은 문제로 시작해 최종적으로 둥굴랭 → 둥굴레차 는 몇 번의 단계가 필요한지 계산하는 방법이다.
편집 거리를 구할 때 2차원 배열을 사용하면 사람이 표현하기도 쉽고 연산에도 용이하다. 2차원 배열을 활용한 둥굴랭 → 둥굴레차 의 편집 거리는 우측 하단의 값인 2 이며, 나머지 값은 해를 구하기 위한 과정일 뿐이다.
숫자의 의미를 이해해보자
숫자들은 1행부터 다음 행씩 행 단위 순으로 채운다. 1행은 둥 → 둥굴레차 여정의 횟수, 2행은 둥굴 → 둥굴레차 여정의 횟수를 의미한다. 1행부터 자세하게 살펴볼까?
1행 채우기
[1]번 칸
- 둥 → 둥 으로 갈 수 있는 최소 단계
- 이미 동일하므로 변경할 것이 없어 0이다.
- 최종 값: 0
[2]번 칸
- 둥 → 둥굴 로 갈 수 있는 최소 단계
- 둥 → 둥 까지는 [1]번 칸 에서 이미 최소 단계가 0이라고 나왔으므로 소거한다. 소거 후: (빈값) → 굴
- (빈값) → 굴 로 가는 방법은 ‘굴’을 삽입하는 한 단계면 되므로 1을 더한다.
- 최종 값: 1 (= 1 + [1]번 칸 값 0)
[3]번 칸
- 둥 → 둥굴레 로 갈 수 있는 최소 단계
- 둥 → 둥굴 까지는 [2]번 칸 에서 이미 최소 단계가 1이라고 나왔으므로 소거한다. 소거 호: (빈값) → 레
- (빈값) → 레 로 가는 방법은 ‘레’를 삽입하는 한 단계면 되므로 1을 더한다.
- 최종 값: 2 (= 1 + [2]번 칸 값 1)
[4]번 칸
- 둥 → 둥굴레차 로 갈 수 있는 최소 단계
- 둥 → 둥굴레 까지는 [3]번 칸 에서 이미 최소 단계가 2이라고 나왔으므로 소거한다. 소거 후: (빈값) → 차
- (빈값) → 차 로 가는 방법은 ‘차’를 삽입하는 한 단계면 되므로 1을 더한다.
- 최종 값: 3 (= 1 + [3]번 칸 값 2)
2행 채우기
[5]번 칸
- 둥굴 → 둥 으로 갈 수 있는 최소 단계
- 둥 → 둥 까지는 [1]번 칸 에서 이미 최소 단계가 0이라고 나왔으므로 소거한다. 소거 후: 굴 → (빈값)
- 굴 → (빈값) 으로 가는 방법은 ‘굴’를 삭제하는 한 단계면 되므로 1을 더한다.
- 최종 값: 1 (= 1 + [1]번 칸 값 0)
[6]번 칸
- 둥굴 → 둥굴 로 갈 수 있는 최소 단계
- [6]번 칸 을 기준으로 왼쪽 ([5]번 칸), 대각선쪽 ([1]번 칸), 위쪽 ([2]번 칸) 칸을 활용해 셋 중 최소 단계를 택한다.
- 왼쪽 ([5]번 칸) 을 활용할 경우
- 둥굴 → 둥 으로 가는 단계는 1이라고 정해졌으므로 둥굴 → 둥굴 에서 해당 글자를 소거한다. 소거 후: (빈값) → 굴
- (빈값) → 굴 로 가는 방법은 ‘굴’를 삽입하는 한 단계면 되므로 1을 더한다.
- 값: 2 (= 1 + [5]번 칸 값 1)
- 대각선쪽 ([1]번 칸) 을 활용할 경우
- 둥 → 둥 으로 가는 단계는 0이라고 정해졌으므로 둥굴 → 둥굴 에서 해당 글자를 소거한다. 소거 후: 굴 → 굴
- 굴 → 굴 으로 가는 방법은 이미 동일하므로 변경할 것이 없어 0이다.
- 값: 0 (= 0 + [1]번 칸 값 0)
- 위쪽 ([2]번 칸) 을 활용할 경우
- 둥 → 둥굴 로 가는 단계는 1이라고 정해졌으므로 둥굴 → 둥굴 에서 해당 글자를 소거한다. 소거 후: 굴 → (빈값)
- 굴 → (빈값) 로 가는 방법은 ‘굴’를 삭제하는 한 단계면 되므로 1을 더한다.
- 값: 2 (= 1 + [2]번 칸 값 1)
- 이 3가지 방법 중 최소를 가지는 방법은 대각선쪽 ([1]번 칸)을 활용하는 경우이다.
- 최종 값: 0 (= 0 + [1]번 칸 값 0)
[7]번 칸
- 둥굴 → 둥굴레 로 갈 수 있는 최소 단계
- [7]번 칸 을 기준으로 왼쪽 ([6]번 칸), 대각선쪽 ([2]번 칸), 위쪽 ([3]번 칸) 칸을 활용해 셋 중 최소 단계를 택한다.
- 왼쪽 ([6]번 칸) **을 활용할 경우
- 둥굴 → 둥굴 로 가는 단계는 1이라고 정해졌으므로 둥굴 → 둥굴레 에서 해당 글자를 소거한다. 소거 후: (빈값) → 레
- (빈값) → 레 로 가는 방법은 ‘레’를 삽입하는 한 단계면 되므로 1을 더한다.
- 값: 1 (= 1 + [6]번 칸 값 0)
- 대각선쪽 ([2]번 칸) 을 활용할 경우
- 둥 → 둥굴 로 가는 단계는 1이라고 정해졌으므로 둥굴 → 둥굴레 에서 해당 글자를 소거한다. 소거 후: 굴 → 레
- 굴 → 레 로 가는 방법은 ‘굴’을 ‘레’ 로 치환하는 한 단계면 되므로 1을 더한다.
- 값: 2 (= 1 + [2]번 칸 값 1)
- 위쪽 ([3]번 칸) 을 활용할 경우
- 둥 → 둥굴레 로 가는 단계는 2이라고 정해졌으므로 둥굴 → 둥굴레에서 해당 글자를 소거한다. 소거 후: 굴 → (빈값)
- 굴 → (빈값) 으로 가는 방법은 ‘굴’를 삭제하는 한 단계면 되므로 1을 더한다.
- 값: 3 (= 1 + [3]번 칸 값 2)
- 이 3가지 방법 중 최소를 가지는 방법은 왼쪽 ([6]번 칸) 을 활용하는 경우이다.
- 최종 값: 1 (= 1 + [6]번 칸 값 0)
[8]번 칸 부터는 반복되는 내용에 길이가 너무 길어질거 같으니 직접 구해보자..!
이제 규칙을 찾아보자
● [6]번 칸 과 [7]번 칸 을 구하는 과정을 살펴보면, 방향과 변경 방법이 일관됨을 확인할 수 있다.
- 왼쪽 칸을 활용하면, 삽입이다.
- 대각선쪽 칸을 활용하면, 치환이다.
- 위쪽 칸을 활용하면, 삭제다.
⇒ 왼쪽은 삽입, 대각선쪽은 치환, 위쪽은 삭제
● 비교하는 글자가 같으면, 해당 글자는 이미 동일하므로 변경할 것이 없어 0이다. 행과 열이 동일한 칸(M[i, i])은 대각선 위가 최소값이므로 최종값 = 0 + 대각선 위쪽 칸 값 이 되어 대각선 위쪽 값을 그대로 따라간다.
⇒ 비교하는 글자가 같으면 대각선 위 값을 그대로 가짐
● 비교하는 글자가 다르면, 이미 작은 문제를 해결한 왼쪽, 대각선위쪽, 위쪽 값에 한 단계(삽입, 치환, 삭제) 를 추가로 해주므로 그 중 최소값 +1 이 해당 칸의 최종값이다.
⇒ 비교하는 글자가 다르면 왼쪽, 대각선쪽, 위쪽 중 최소값에 1을 더함
편집 거리 알고리즘은 띡하고 나온게 아니라 위의 규칙들을 수식이나 코드로 옮겨놓은 것 뿐이다.
편집 거리 알고리즘 구현 (JAVA)
public static int getEditDistance(char[] fromChar, char[] toChar) {
int fromSize = fromWord.length();
int toSize = toWord.length();
int[][] matrix = new int[fromSize][toSize];
matrix[0][0] = fromChar[0] == toChar[0] ? 0 : 1;
for (int i = 1; i < fromSize; i++) {
matrix[i][0] = matrix[i-1][0] + 1;
}
for (int i = 1; i < toSize; i++) {
matrix[0][i] = matrix[0][i-1] + 1;
}
for (int i = 1; i < matrix.length; i++) {
for (int j = 1; j < matrix[i].length; j++) {
if(fromChar[i] == toChar[j]) {
matrix[i][j] = matrix[i-1][j-1];
continue;
}
matrix[i][j] = Math.min(matrix[i-1][j-1]+1, Math.min(matrix[i-1][j]+1, matrix[i][j-1]+1));
}
}
return matrix[fromSize-1][toSize-1];
}
※ 위키피디아 알고리즘을 100% 동일하게 구현한 것은 아니나 편집 거리 알고리즘을 표현하는 방법은 동일하다.
레퍼런스
https://en.wikipedia.org/wiki/Levenshtein_distance
https://lovit.github.io/nlp/2018/08/28/levenshtein_hangle/
https://madplay.github.io/post/levenshtein-distance-edit-distance
편집 거리 알고리즘 이해하기
글 읽어주셔서 언제나 감사합니다. 좋은 피드백, 개선 피드백 너무나도 환영합니다.
'SearchDeveloper > 알고리즘' 카테고리의 다른 글
코사인 유사도 증명하기 (1) | 2024.12.07 |
---|---|
[아호코라식 (Aho-corasick) 알고리즘] 논문 이해하기 (3) - 성능 측정 (2) | 2024.07.24 |
[아호코라식 (Aho-corasick) 알고리즘] 논문 이해하기 (2) - 데이터 구축 (0) | 2024.04.14 |
[아호코라식 (Aho-corasick) 알고리즘] 논문 이해하기 (1) - 검색 과정 (0) | 2024.04.12 |
ElasticSearch BM25 이해하기 (5) | 2022.08.25 |