[NLP] KenLM으로 Language Model만들기

2024. 2. 9. 12:08Developers 공간 [Shorts]/Vision & Audio

728x90
반응형
<분류>
A. 수단
- OS/Platform/Tool : Linux, Kubernetes(k8s), Docker, AWS
- Package Manager : node.js, yarn, brew, 
- Compiler/Transpillar : React, Nvcc, gcc/g++, Babel, Flutter

- Module Bundler  : React, Webpack, Parcel

B. 언어
- C/C++, python, Javacsript, Typescript, Go-Lang, CUDA, Dart, HTML/CSS

C. 라이브러리 및 프레임워크 및 SDK
- OpenCV, OpenCL, FastAPI, PyTorch, Tensorflow, Nsight

 


1. What? (현상)

이전 글(https://tkayyoo.tistory.com/160)에서 형태소분석기와 토크나이저를 활용해 텍스트데이터를 원하는 형태로 정제해보았습니다.

 

해당 글에서 STEP3인 Tokenize까지 마쳐보았고, 이어서 이번 글에서는 해당 텍스트들을 통해 Language Model(LM)을 구축해보려고 합니다.  kenlm(https://github.com/kpu/kenlm)을 활용해 ngram으로 LM을 만들어보겠습니다.


2. Why? (원인)

  • X

3. How? (해결책)

1. 설치

 

먼저 kenlm을 설치하겠습니다.

apt-get install zlib1g-dev liblzma-dev libbz2-dev libboost-all-dev
git clone https://github.com/kpu/kenlm.git
cd kenlm
mkdir -p build
cd build
cmake ..
make -j 4
make install


2. LM 만들기

 

이제 바이너리 파일을 활용해 아래와 같이 만들어 보겠습니다. STEP4는 ngram model을 만드는 과정이고, STEP5는 활용을 위해 binary파일로 만들어주는 과정입니다.

#STEP4. ngram model
order=6
from=$to
to="${OUT_DIR}/step4_ngram_${OUT_FORM}_${order}.arpa"

lmplz --text ${from} --arpa ${to} --skip_symbols -o ${order} --discount_fallback

#STEP5. mecab separating
from=$to
to="${OUT_DIR}/step5_bin_${OUT_FORM}_${order}.bin"

build_binary -q 8 -b 7 -a 256 trie ${from} ${to}

 

STEP5의 결과물은 binary이므로, 대신 STEP4의 결과물인 step4_ngram_test_6.arpa를 살펴보면 아래와 같습니다.

\data\
ngram 1=3262
ngram 2=31935
ngram 3=73339
ngram 4=188476
ngram 5=342711
ngram 6=491311

\1-grams:
-4.2924743 <unk> 0
0 <s> -1.2616308
-1.4592671 </s> 0
-3.8836806 ▁학습-0.17071483
-3.4560456 ▁지원 -0.22419876
-3.0246294 ▁센터 -0.59363115 -
2.3831198 ▁입니다 -1.5040483
-1.053991 ▁ -1.0594271
-4.1351748 ▃ -0.16214186
....

 

 

3. LM을 Python으로 활용해 다음 단어 예측하기

 

kenlm으로 구축된 LM을 활용하는 모듈을 아래와 같이 만들어 보았습니다. 해당 내용 중의 SentencepiecesTokenizer는 이전 글(https://tkayyoo.tistory.com/160)을 참조하세요.

import numpy as np
import kenlm

DEBUG_MODE=True
class LanguageModel:
    def __init__(self, ngram_path, vocab_list=None):
        self.model = kenlm.Model(ngram_path)
        print("{0}-gram model".format(self.model.order))
        self.state1=kenlm.State()
        self.state2=kenlm.State()
        self.not_using = ['<unk>', '<s>']
        self.vocab_list = vocab_list
        self.one2two = True
        self.current_words = []
        self.current_score =0.0
        self.starttoken_set = False
    def ReferenceScore(self,sentence):
        words = ['<s>'] + sentence.split() + ['</s>']
        print("====================Partial Check =============")
        for i, (prob, length, oov) in enumerate(self.model.full_scores(sentence)):
            print('{0} {1}: {2}'.format(prob, length, ' '.join(words[i+2-length:i+2])))
            if oov:
                print('\t"{0}" is an OOV'.format(words[i+1]))
        print("====================Partial Check =============END")
    def SettingTokenizer(self, tokenizer_path):
        self.tokenizer = SentencepiecesTokenizer(tokenizer_path)
        self.vocab_list = self.tokenizer.sp.get_vocab()
    def ProcessStart(self):
        self.model.BeginSentenceWrite(self.state1)
        self.one2two = True
        self.current_words = []
        self.current_score= 0.0
        self.starttoken_set = True

    def ProcessNextTokenCustom(self,word):
        if not self.starttoken_set:
            raise Exception("start token must be set first")
        if self.one2two :
            state_from = self.state1
            state_to = self.state2
        else:
            state_from = self.state2
            state_to = self.state1

        temp_sentence = self.current_words +[word]
        lm_score = self.model.score(' '.join(temp_sentence), bos=True, eos=False)
        if DEBUG_MODE:
            print("(SINGLE) {} : {}".format(word, lm_score-self.current_score))

        lm_score2 = self.model.BaseScore(state_from, word, state_to)

        self.current_score = lm_score

        if word =='</s>':
            end_token=True
            self.starttoken_set=False
        else :
            self.current_words.append(word)
            end_token=False

        if DEBUG_MODE:
            print("(Mid Result) : {}".format(' '.join(self.current_words)))
        if DEBUG_MODE :
            print("*********************")
        self.one2two = not self.one2two

        return end_token

    def ProcessNextTokenAuto(self, get_candidates=False):
        if not self.starttoken_set:
            raise Exception("start token must be set first")
        if not self.vocab_list:
            raise Exception("vocab list must be set first for auto token")

        if self.one2two :
            state_from = self.state1
            state_to = self.state2
        else:
            state_from = self.state2
            state_to = self.state1

        scores = []
        for w_new in self.vocab_list.keys():
            if w_new in self.not_using :
                continue;
            temp_sentence = self.current_words + [w_new]
            lm_score = self.model.score(' '.join(temp_sentence), bos=True, eos = False)
            scores.append([w_new, lm_score])
        if get_candidates:
            candidates = np.argsort([x[1] for x in scores])[-5:]
            if DEBUG_MODE:
                for i in candidates :
                    ws = scores[i][0]
                    sc = scores[i][1] -self.current_score
                    print("{} : {}".format(ws, sc))
        max_score = max(scores, key=lambda x:x[1])
        if DEBUG_MODE:
            print("(MAX) {} : {}".format(max_score[0], max_score[1]-self.current_score))

        lm_score2 = self.model.BaseScore(state_to, max_score[0], state_from)
         
        self.current_score= max_score[1]
        if max_score[0] =='</s>':
            end_token= True
            self.starttoken_set= False
        else:
            self.current_words.append(max_score[0])
            end_token=False
        if DEBUG_MODE:
            print("(Mid Result) : {}".format(' '.join(self.current_words))) 
        if DEBUG_MODE:
            print("*************")

        self.one2two = not self.one2two
        if get_candidates:
            return end_token, max_score[0], candidates
        else:
            return end_token, max_score[0]
    def GetSentence(self):
        return (' '.join(self.current_words)).replace(' ', '').replace('▁','').replace('▃', ' ')
    def GetSentenceRaw(self):
        return (' '.join(self.current_words))
    def __call__(self, word, get_candidates=False):
        self.ProcessStart()
        last = self.ProcessNextTokenCustom(word)
        count = 0
        while(not last):
            if get_candidates :
                last,_,_ = self.ProcessNextTokenAuto(get_candidates=get_candidates)
            else:
                last,_ = self.ProcessNextTokenAuto(get_candidates=get_candidates)
            if count==100:
                break;
            else:
                count+=1
        return self.GetSentence()

 

자 이제 위에서 만든 모듈을 선언해보겠습니다.

ngram_lm_model = 'OUTPUT/step5_bin_test_6.bin'
lm = LanguageModel(ngram_lm_model)
tokenizer_path = "./callcenter.model"
lm.SettingTokenizer(tokenizer_path)

 

예시 문장이 있을 때 해당 LM을 활용시의 확률을 먼저 살펴보겠습니다.

sentence = "▁아무 ▁도 ▁ ▃ ▁홈페이지 ▁에 ▁ ▃ ▁아니 ▁ ▃ ▁얘 ▁ 는 ▁ ▃ ▁인강 ▁은 ▁ ▃ ▁한 ▁ ▃ ▁번 ▁도 ▁ ▃ ▁안 ▁ ▃ ▁해 ▁봤 ▁어 요"
lm.ReferenceScore(sentence)

 

자 이제 한 단어가 있을 때 LM을 활용해 문장을 예측해보겠습니다.

start_word ="▁아무"
output = lm(start_word, get_candidates=True)

 

 

4. 평가하기

실제로 LM을 활용해 단어를 만들어보았는데, LM이 잘 구축되었는지 절대적인 지표가 필요한 경우도 있습니다. 이런 경우 PPL이라는 지표를 활용해 확인할 수 있습니다. PPL에 대한 자세한 내용은 아래 더보기를 참조하세요.

더보기

-----------------------------------------------------

<Perplexity(PPL)이란?>

 

언어모델(Language Model) 평가지표로, PPL(Perplexity)는 perplexed(헷갈리는)라는 영어 단어에서 의미를 따왔습니다. 즉, PPL이란 언어 모델이 문장을 생성할 때 헷갈리는 정도를 의미합니다.

 

$$PPL(W) = P(w_1, w_2, ..., w_N)^{-\frac{1}{N}}=\sqrt[N]{\frac{1}{P(w_1, w_2, ..., w_N)}}$$

  • N은 문장의 길이
  • P() : 나타날 확률
  • W1,W2, W3, ... : 언어 모델에서 주어진 문장

PPL이 30이 나왔다면, 주어진 데이터에서 이 LM을 기반으로 다음 단어를 예측할 때 마다 평균적으로 30개의 후보 단어 중에 (헷갈리고 있으므로) 선택할 수 있다는 이야기가 됩니다.

 

즉, LM이 문법에 맞게 학습된 경우를 예로, 주어진 데이터가 formal하고 문법 오류가 없는 경우는 PPL이 낮게 나오지만, 구어체거나 문법 오류가 많은 경우는 PPL이 높게 나올 것입니다.

 

<PPL와 Cross Entropy의 관계>

Cross Entropy를 이해하기 위해 "정보량"에 대해 먼저 알아봅시다. 정보량은 "놀람의 정도"라고 할 수 있으며, 어떤 사건의 발생 확률이 낮을수록 "놀람의 정도"는 높아지기에 해당 사건은 높은 정보량을 갖고 있다고 말할 수 있습니다.

 

따라서 정보량이 낮은(당연한, 데이터가 많은) 정보들은 생략되기 마련이므로 확률 분포는 뾰족한(sharp) 모양을 가지고, 반대로 정보량이 높으면 확률 분포는 납작(flat) 모양을 가집니다. 정보량을 표현하면 아래와 같습니다.

$$I(x) = log(\frac{1}{p(x)})= -log(p(x))$$

 

그럼 "엔트로피"는 무엇일까요? 정보량의 확률분포를 반영한 어떤 사건에 대한 평균 정보량을 의미합니다. 따라서 예측이 어려울수 록 정보의 양이 더 많아지므로 엔트로피는 더 커집니다. 이를 표현하면 아래와 같습니다.

$$H(x)=E_{p(x)}[I(x)]=E_{p(x)}[-log(p(x))]=\sum^{N}_{i=1}-p(x_i)log(p(x_i))$$

 

이때, 실제 문장들의 집합(P 혹은 W)에 대한 언어모델 P_θ의 엔트로피를, "길이 n인 문장을 샘플링했을 때" 나타내보면 아래와 같습니다.

** 자세한 도출과정(https://kh-kim.gitbook.io/natural-language-processing-with-pytorch/00-cover-8/03-perpexity)은 해당 링크를 참조하세요

$$H_n(W,P_\theta)=log\sqrt[n]\frac{1}{\prod^n_{i=1}P_\theta(w_i|w_{<i})}$$

 

위에서 설명한 PPL의 식과 비슷한 부분이 있죠? 이를 식으로 도출해내면 아래와 같은 결론이 나옵니다.

$$PPL = e^{Cross Entropy}$$

 

-----------------------------------------------------

 

아래 명령어를 통해 step4_ngram_test_6.arpa를 평가해보겠습니다. output.txt는 평가를 위한 텍스트 나열 데이터입니다.

query -v summary OUTPUT/step4_ngram_test_6.arpa < output.txt

 

결과는 아래와 같습니다.

Loading the LM will be faster if you build a binary file.
Reading OUTPUT/step4_ngram_test_6.arpa
----5---10---15---20---25---30---35---40---45---50---55---60---65---70---75---80---85---90---95--100
****************************************************************************************************
Perplexity including OOVs: 10466.03569421601
Perplexity excluding OOVs: 915.4931894461381
OOVs: 279326
Tokens: 392269
Name:query

 

위에서 step4_ngram_test_6.arpa를 활용해 평가해보았는데, step5_bin_test_6.bin를 활용해서 실행해도 같은 PPL과 OOV결과가 나옵니다. 다만 아래와 같은 메시지가 나올 것인데, 무시하셔도 됩니다.

This binary file contains trie with quantization and array-compressed pointers

https://esj205.oopy.io/12ffe440-5ebf-465a-bf7b-aae4db855fa7

https://medium.com/@techhara/language-model-and-perplexity-0fea128c9e48

https://kh-kim.gitbook.io/natural-language-processing-with-pytorch/00-cover-8/03-perpexity

 

728x90
반응형