☀️

문장의 유사도 분석하기(레벤슈타인 거리, N-gram)

문장의 유사도 분석

단어 또는 문장의 유사도를 분석하는 방법 중에는 레벤슈타인 거리, N-gram 등이 있다.
오늘은 이 방법들에 대해서 알아볼 것이다.

레벤슈타인 거리(Lvenshtein distance)

레벤슈타인 거리는 두 단어 또는 문장의 차이를 통해 거리를 수치로 나타내는 것이다.
철자 오류 수정, 비슷한 어구 검색 등에 사용되고, 의학 분야에서는 DNA 배열의 유사성을 판단할 때도 사용하고 있다.

예를 들어
‘가나다라’를 ‘가마바라’로 수정할 때 몇 번의 수정이 필요한 지를 거리로 나타낼 수 있다.

0 가나다라
1 가’마’다라
2 가마’바’라

‘가나다라’-> ‘가마바라’는 거리가 2임을 알 수 있다.

코드로 알아보자.

def calc_distance(a, b):
    if a == b:
        return 0
    a_len = len(a)
    b_len = len(b)
    if a == "":
        return b_len
    if b == "":
        return a_len
    # 2차원 표 (a_len+1, b_len+2) 준비하기
    matrix = [[] for i in range(a_len+1)]
    for i in range(a_len+1):
        matrix[i] = [0 for j in range(b_len+1)]
    # 0일 때 초기값을 설정
    for i in range(a_len+1):
        matrix[i][0] = i
    for j in range(b_len+1):
        matrix[0][j] = j
    # 표 채우기
    for i in range(1, a_len+1):
        ac = a[i-1]
        for j in range(1, b_len+1):
            bc = b[j-1]
            cost = 0 if (ac == bc) else 1
            matrix[i][j] = min([
                matrix[i-1][j] + 1,     # 문자 삽입
                matrix[i][j-1] + 1,     # 문자 제거
                matrix[i-1][j-1] + cost # 문자 변경
            ])
    return matrix[a_len][b_len]

print(calc_distance('가나다라', '가마바라'))
# result : 2

samples = ['신촌역', '신쵼', '신천군', '신천역', '신발', '마곡역']
base = samples[0]
r = sorted(samples, key = lambda n: calc_distance(base, n))
for n in r:
    print(calc_distance(base, n), n)

/* result :
0 신촌역
1 신천역
2 신쵼
2 신천군
2 신발
2 마곡역
*/

N-gram

N-gram은 이웃한 N개의 문자를 의미한다.
서로 다른 2개의 문장을 N-gram으로 비교해보면 출현하는 단어의 종류와 빈도수, 유사도를 확인할 수 있다.
N-gram은 논문 도용, 소스코드간 유사도 등에도 사용할 수 있다.

예를들어

  • 오늘 강남에서 맛있는 스파게티를 먹었다.
  • 강남에서 먹었던 오늘의 스파게티는 맛있었다.

위 문장들을 2글자씩 끊으면

['오늘', '늘 ', ' 강', '강남', '남에', '에서', '서 ', ' 맛', '맛있', '있는', '는 ', ' 스', '스파', '파게', '게티', '티를', '를 ', ' 먹', '먹었', '었다', '다.']

['강남', '남에', '에서', '서 ', ' 먹', '먹었', '었던', '던 ', ' 오', '오늘', '늘의', '의 ', ' 스', '스파', '파게', '게티', '티는', '는 ', ' 맛', '맛있', '있었', '었다', '다.']

이렇게 끊어지게 되는데
이 구분된 문장들을 한 항목씩 비교해서 동일한 항목이 있을 때 count를 + 1 하여 총 count / 끊은 문장 배열의 항목 수 를 하여 유사도를 구하게 된다.

코드로 알아보자.

def ngram(s, num):
    res = []
    slen = len(s) - num + 1
    for i in range(slen):
        ss = s[i:i+num]
        res.append(ss)
    return res

def diff_ngram(sa, sb, num):
    a = ngram(sa, num)
    b = ngram(sb, num)
    r = []
    cnt = 0
    for i in a:
        for j in b:
            if i == j:
                cnt += 1
                r.append(i)
    return cnt / len(a), r


a = '오늘 강남에서 맛있는 스파게티를 먹었다.'
b = '강남에서 먹었던 오늘의 스파게티는 맛있었다.'

r2, word2 = diff_ngram(a, b, 2)
print('2-gram:', r2, word2)
# result : 2-gram: 0.7619047619047619 ['오늘', '강남', '남에', '에서', '서 ', ' 맛', '맛있', '는 ', ' 스', '스파', '파게', '게티', ' 먹', '먹었', '었다', '다.']

r3, word3 = diff_ngram(a, b, 3)
print('3-gram:', r3, word3)
# result : 3-gram: 0.45 ['강남에', '남에서', '에서 ', ' 맛있', ' 스파', '스파게', '파게티', ' 먹었', '었다.']

2문장씩 끊었을때의 유사도는 0.76, 3문장씩 끊었을때의 유사도는 0.45로 2문장씩 끊었을 때의 유사도가 더 높았다.
N개의 문장을 끊을지는 hyper parameter가 될 것 같다. 유사도를 구할 문장에따라 적절한 값을 입력하면 좋은 결과가 있을 것 같다.

위에 대한 소스코드는
https://github.com/suitelab/actual-deeplearning/blob/master/ch6/lev-distance.py
https://github.com/suitelab/actual-deeplearning/blob/master/ch6/ngram-test.py
에 올려두었다.

yoon.homme
yoon.homme

기술과 커뮤니케이션의 힘이 세상을 바꾼다고 믿습니다.

편리한 세상으로 나아가기 위해 고민하고 개발합니다.