Web Analytics Made Easy - Statcounter

Bouncing ideas 생각 작업실/lexical subclassing

한국어 발음형을 계층적 클러스터링 해보자

sleepy_wug 2022. 2. 3. 18:28

한국어 어휘집(렉시콘)은 한자어/고유어/외래어로 통칭되는 층위, 내지는 하위단위를 가지고 있다. 놀랍게도 어떤 음운작용은 층위의존적이다. (관련글) 통상적으로 층위를 나누는 기준은 어원이다. 외국어에서 왔으면 외래어, 한자가 어원이면 한자어 등등. 그런데 조금만 생각해보면 조금 석연치 않은 부분이 있다.

 

한국인 부모의 자손으로 태어나면 어원정보를 UG로 탑재한 채 태어나는 것도 아니고, 대체 아기는 각 단어의 어원을 어떻게 '상고'해서 어떤 음운작용을 적용할지 말지를 결정하는 것일까? 특히 l-tensification처럼 한자어라면 반드시 적용되는 규칙은 어떻게 실수없이 적용하는 것일까? 또한 '바'라는 음가를 가진 한자는 없는데, 대학생들은 "분명 내가 모르는 어떤 한자는 음이 '바'일 것이다"라고 생각한다 (강용순 1998). 그 뿐만 아니다! 도대체 시소는 왜 어원으론 외래어인데 다들 고유어라고 알고 있느냔 말인가? 아참, 도대체도 한자어지만 다들 고유어인줄 안다.

 

따라서 어원이 아니라, 언어학습자가 더 쉽게 접근할 수 있는 것들이 층위 분류의 기준이 되어야 할 것이다.

 

하나의 예시는 음소배열일 것이다. ㄹ-경음화의 결과인 "ㄹ+경음" 연쇄 형태를 포함하는 단어들로 군집이 하나 만들어질 것이고, "ㄹ+평음" 연쇄 형태가 존재하는 단어들이 다른 군집을 구성할 것이다. (슬슬 내가 어디로 향하는지 눈치챌 사람들도 있겠지--- 바로 음소의 n-gram이 학습된다는 생각이다.) 그러니까 궁극적으로 음소배열에 대한 비지도학습(unsupervised learning)을 통한 분류가 가능해야 한다.

 

잠시 아기의 입장이 되어보자. 막 태어나서 언어데이터를 접할 때에는 렉시콘에 층위가 존재한다는 사실, 그리고 몇 개의 층위가 있는지조차 알수없다. 이후 음소배열이나, 음소형태론적 작용, 그리고 사용맥락 등 여러 관찰을 통해 단어들을 분류할 것이다. 즉, 비모수 베이지언 모형(Baysian non-parametric model)으로 모델링될 수 있는 학습의 과정인 것이다.

 

Morita의 2018년 MIT dissertation2022년 Linguistic inquiry 논문이 바로 이러한 접근법으로 일본어 렉시콘을 분석했다. 이 논문들에 따르면 음소배열에 대한 비지도학습 결과가 어원 기준 층위와 대략 일치한다고 한다.

 

구체적으로 Morita는 계층적디리클레과정(Hierarchical Dirichlet Process)을 이용해서 tri-gram 기준 클러스터링을 했는데, 아마 한국어에서도 Morita의 방법론을 구현해보는 것이 가능할 것이다. 현재 '간보기' 식으로 이미 진행을 하고 있지만 아직 만족할 만하지는 않다. 디리클레 과정을 구현하는 과정(???) 즉 "과정 구현 과정" 에 대해서는 차후 별도의 포스팅을 할 예정이다.

 

우선 오늘은 한국어 어휘 발음형을 단순히 계층적으로 분류하면 어떤 결과가 나오는지를 소개한다. 이건 유의미한 결과를 보여준다기보다는 전체적인 방향성의 측면에서, 어원에 대한 사전정보 없이 음소배열에만 기준해서도 클러스터링이 가능하다는 것을 보여주는 것이다. 결과부터 보고 시작하자.

한국어 어휘 발음형을 자질 단위로 표상한 후, 그것들을 응집 계층적 클러스터링(Agglomerative hierarchical clustering)한 결과를 나무그림으로 나타낸 것이 바로 위의 그림이다. 각 항목 (단어) 간의 연결에는 Ward's method를 사용하였다.

 

이 결과가 어원을 기준으로 한 층위랑 얼마나 매칭이 되는지에 대해서는 아래의 표를 보면 된다.

  cluster 0 cluster 1 cluster 2 cluster 3 합계
한자어 5 0 0 5583 5588
고유어 68 0 0 730 798
외래어 68 16 3 353 440
합계 141 16 3 6666 6826

 


https://datastore-of-pren-k.tistory.com/9 이 블로그에서 응집 계층적 클러스터링이 뭔지 개념을 잡고, 기타 구글링을 통해 구현받는 데 도움을 받았다.

 

차근차근 설명해보자.

 

우선,

원데이터 (n=6826): 박나영 (2020) 박사논문 데이터 기반하여 수정

박나영 선생님의 표준국어대사전 발음형 데이터를 구해서 그것을 거의 그대로 사용하였다. (여기) 매우 양질의 데이터다. 강추! 그런데 이 데이터와 관련하여 주의할 것은 발음형이 꽤나 보수적이라는 것이다. ㅐㅔ 구분이 되어있고, ㅚㅟ도 단모음으로 상정되어있다. 일단 주어진대로 사용하였다. 

아 정말 이 데이터는 너무 고마워서 앞으로도 계속 쓸거 같다. 그런데 지금은 일단 preliminary run이기 때문에, 이 db에 있는 엄청 많은 단어들을 다 사용하지는 않았다. 분석대상 단어를 추렸다.

우선 명사만을 대상으로 했다. 궁극적으로 n-gram 기반 HDP하려면 용언을 쓸수 없다. 활용을 하기 때문이다. (여기서 고백 하나: 박나영 선생님 드롭박스를 충분히 찬찬히 보지를 않아서인지 '단일어명사' 파일들이 따로있는줄을 몰랐다. 🙄  알았으면 그냥 그거 썼을 것이다.)

또한 빈도 100 이상의 고빈도단어만 대상으로 했다. 단어 출현 빈도로는 강범모.김흥규 (2009) "한국어 사용 빈도" 데이터를 사용했다. (나 석사 입학할때부터 이것을 썼으니 조금 불안해진다. 더 최신 빈도자료 있으면 알고싶다.ㅠㅠ)

마지막으로 층위간 동음이의어를 제거하였다. 무슨말인고하니, 어떤 단어가 동음이의어인데 맥락에 따라 고유어이기도하고 한자어이기도 한 경우 두 단어 모두 제거했다

이렇게 하니 원데이터로 6826개가 구성되었다.

 

발음형의 자질표상(featural representation): Graff (2012) 참고

Peter Graff 의 2012년 MIT dissertation은 내용도 충격적이었지만, 사용된 자질표상방식도 너무 (좋은의미에서) 충격적이었다. IPA가 1음소가 1기호에 대응된다면, Graff의 ANIPA는 1자질이 1기호에 대응된다.

Graff의 박사논문 20페이지에 보면, ANIPA의 specification이 상세하게 표현되어 있다. 그것을 참고하여서 한국어 용 ANIPA를 만들었다. 한국어에 맞게 약간 수정한 부분도 있지만 일단 

 

"한글toANIPA가 여기있습니다"

 

한국어 발음형에 나오는 각 기호를 ANIPA로 변환하는 '더럽지만 작동하는' 코드는 아래와 같다. 졸린 상태에서 아무 생각 없이 했기 때문에, for-loop로 각 가능성을 훑고 지나간다는 점에서 매우 비효율적이다. 돌리는데 의외로 시간은 오래 안걸린다. 아무렇지 않게 살짝 들어간 f-string에서 볼수있듯, 파이썬 3.7로 짰다.

 

import pandas as pd
anipa = pd.read_csv('ANIPA.csv')  # TRANSCRIPTION -> ANIPA correspondence table
def convert_to_ANIPA(symbol):
    for ind, row in anipa.iterrows():
        if row['TRANSCRIPTION'] == symbol:
            return row['ANIPA']
    print(f"[ERROR] {symbol} has no ANIPA correspondence.")

 

pandas 패키지의 read_csv를 이용해서 ANIPA.csv를 'anipa'라는 이름의 object로 사용.

ANIPA.csv는 내가 편의상 중간과정 다 생략하고 그냥 TRANSCRIPTION ANIPA 이렇게 두개 column만 남기고 다 지워버린 파일이다. 얼추 아래와 같이 생겼다.

TRANSCRIPTION     ANIPA
a                        VAUuNvo1
...                        .....

함수의 유일한 argument인 symbol은 박나영 식 발음기호를 각각 받는다. 예를들어 원데이터에서 단모음 'ㅚ[ø]'는 wR로 썼는데, convert_to_ANIPA는 이것을 'VQZrNvo1'로 바꿔준다. 

list(map()) 써서 이 함수를 단어 내 각 symbol에 적용해주면 끝.

 

Distance matrix

자, 이제 계층적 클러스터링은, 자질로 표현된 한국어 단어간의 유사성을 기준으로 유사한 단어들끼리 그룹을 지을 것이다. 그걸 위해서는 각 단어쌍이 얼마나 유사한지를 계산해주어야 한다. 구현하는 코드가 아래에 있다. w1과 w2는 각각 자질로 표상된 단어. type은 list of string이다. 

나는 feature의 edit distance를 이용하였다. 이논문 저논문 그논문에서 많이해서 요즘 핫하다는 '음운이웃'이 출발점이다. 음소 혹은 음절을 1개를 더하거나 빼거나 수정하거나 하면 edit distance 1로 보는 건데, 이번에는 자질이 기준이므로 더 미세하게 단어간 관계가 조절된다. 예를들어 '음운이웃' 식 edit distance라면 '바람-아람' 사이의 거리랑 '바람-파람' 사이의 거리가 같은데, 자질을 기준으로 하면 '바람-파람'이 더 가깝다.

중요한 것은 subs_cost (substitution cont)를 적절하게 계산하는 것이다. 많은 자질을 변경했으면 cost가 높고, 적은 수의 자질만 변경했으면 cost가 낮다. 정확히는 albright의 논문... 정확하게 뭐였는지 까먹었는데, 어쨌든 자질단위 유사성 비교하는 알고리즘이 있었다. 거기서 나온 알고리즘을 구현한 것이다. 그리고 뭐 나머지는 dynamic programming 기법으로 edit distance 구하는 방식 그대로 가져와서 썼다. 다만, insertion cost와 deletion cost는 좀 조절할 필요가 있어보인다. 0.5를 주기는 했는데, 그렇지 않으면 음소단위의 조작이 너무 비싸진다. 

def compute_distance(w1, w2):
    # modified from http://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Python.
    longer, shorter = (w1, w2) if len(w1) >= len(w2) else (w2, w1)
    previous_row = range(len(shorter) + 1)
    for i, c1 in enumerate(longer):
        current_row = [i + 1]
        for j, c2 in enumerate(shorter):
            if c1 == c2:
                subs_cost = 0
            else:
                natural_classes_c1 = set([f for f in c1])
                natural_classes_c2 = set([f for f in c2])
                shared_nc = natural_classes_c1 & natural_classes_c2
                whole_nc = natural_classes_c1 | natural_classes_c2
                subs_cost = 1-(len(shared_nc)/len(whole_nc))

            insertions = previous_row[j + 1] + 0.5  # j+1 instead of j since previous_row and current_row are one character longer
            deletions = current_row[j] + 0.5  # than s2

            substitutions = previous_row[j] + subs_cost
            current_row.append(min(insertions, deletions, substitutions))
        previous_row = current_row
        # if max_distance is not None and previous_row[-1] > max_distance:
        #    break
    return previous_row[-1]

 

 

Hierarchical clustering 계층적 클러스터링 

계층적 클러스터링에는 scipy와 sklearn을 이용하였다. scipy는 위에 나오는 나무그림(dendrogram)을 그리는 데 사용하였고, sklearn 통해서 각 단어가 어떤 cluster에 들어가는지 구체적인 표로 나타냈다.

일단 scipy로 dendrogram그리는 것부터.

def cluster_scipy(distmatrix):
    row_clusters = linkage(distmatrix, method='ward')

    pd.DataFrame(row_clusters)
    row_dendrogram = dendrogram(row_clusters)
    plt.tight_layout()
    plt.show()
    return row_clusters

 

왜 row_clusters라는 변수명을 쓰는건진 모르겠는데, 다들 그렇게 해서 나도 변수명을 그렇게 썼다. 함수의 argument로 들어간 distmatrix는 윗 문단에서 구한 단어쌍 distance를 numpy matrix 형태로 넣는 것이다. 

 

그리고 실제 각 단어의 cluster membership을 산출하기 위해서는 아래의 함수를 만들었다. data는 위의 cluster_scipy()함수의 결과값이 들어가고 n_clusters는 원하는 값을 명시적으로 지정해주어도 되지만, 나는 어짜피 '층위개수가 몇개여야하는지도 몰라야 한다'는 생각이라 dendrogram 상 70% 에서 끊어서 (scipy의 default다) 층위개수가 4개가 됐다.

def cluster_sklearn(n_clusters, data):
    data = squareform(data)
    ac = AgglomerativeClustering(n_clusters=n_clusters, affinity='precomputed', linkage='complete')
    ac_clusters = ac.fit(data)
    return ac_clusters.labels_

 

야호! 엉망진창 포스팅이지만 일단 기록해두었다.

성장형 포스팅이므로 보이는대로 수정에 수정을 하겠습니다. 궁금한 것이나 틀린 것이나 등등 댓글로 달아주세요. 

반응형