2024. 2. 9. 12:08ㆍDevelopers 공간 [Shorts]/Vision & Audio
<분류>
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
'Developers 공간 [Shorts] > Vision & Audio' 카테고리의 다른 글
[Generative] Diffusion 모델 원하는 위치에 받기 (0) | 2024.03.28 |
---|---|
[Audio] ASR에서 CER, WER 구현 및 측정해보기 (0) | 2024.02.09 |
[Audio] Python 활용해 Audio 및 Text 데이터 Pre-processing (0) | 2024.01.14 |
[NLP] 형태소 분석기 및 토크나이저 활용해보기 (0) | 2024.01.03 |
[Audio] Python으로 VAD 구현하기 (0) | 2023.12.30 |