반응형


 

 NLTK 사용하여 .txt파일 문장 단위로 쪼개기

 

딥러닝 모델들이 사용하는 Corpus 파일들은 대부분 Sentence 단위로 학습을 진행하고, 이를 위해서라도 Sentence 단위별로 나눠주는 것이 편리하다. NLTK를 사용하여 문장을 나누고, 이 과정에서 발생할 수 있는 문제를 해결해보자.

 

Requirements


$pip install nltk
$python -m nltk.downloader all

 

NLTK는 대표적인 파이썬 자연어 처리 라이브러리로, 전처리에 필요한 수많은 기능이 포함되어있다.

https://github.com/nltk/nltk

 

GitHub - nltk/nltk: NLTK Source

NLTK Source. Contribute to nltk/nltk development by creating an account on GitHub.

github.com

 

단점을 꼽자면, 굉장히 많은 기능을 가지고 있는 것에 비해서 상대적으로 Documentation은 그다지 친절하지 않다는 점이다. 각 클래스 내부에 있는 함수들이 어떤 역할을 하는지, 설명이 부족한 경우가 많고, 특히나 돌아가는 pipeline을 좀 고치고 싶어서 코드를 보자니 매우 복잡한 구조로 되어 있다. 하루종일 삽질(?)하며 얻은 abbreviation 추가 방법등을 공유하고자 포스팅을 진행하게 되었다.

 

 

nltk.sent_tokenize로 문장 나누기


 

import nltk
load_file=open('입력 파일 경로','r')
save_file=open('저장할 파일 경로','w')
no_blank = False
while True:
    line = load_file.readline()
    if line == "":
        break
    if line.strip() == "":
        if no_blank:
            continue
        save_file.write(f"{line}")
    else:
    	#line sample : 'Hello World. This is sample line."
        result_ = nltk.sent_tokenize(line)
        #result_ : ['Hello World.', 'This is sample line.']
        result  = [ f"{cur_line}\n" for cur_line in result_ ]
        for save_line in result:
            save_file.write(save_line)
        # 문장별로 개행된 파일 저장

 

nltk.sent_tokenize를 사용할 경우, punkt 모델을 활용하여 sentence tokenization을 진행하게 된다. punkt 또한 문장 구조를 학습한 일종의 모델로, 어떤 것이 약어에 쓰이는 "."이고(Ex : Ph.D.), 어떤 것이 마침표인지 학습이 되어있다. 문장을 기본적으로 마침표를 기준으로 나누되, Ph.D., Saint., Professor., 와 같은 약어(Abbreviation)는 Known abbreviation으로 학습하여 한 단어로 취급하는 방식이다.

 

하지만 이러한 punkt모델에도 치명적인 단점이 있는데, 모든 약어를 학습하지 못했다보니, Vol. 13, Apr. 13 과 같은 표현 및 U.S. Pat. No. 134 과 같은 복잡한 약어는 Known abbreviation이 아니여서 모두 나눠져버린다는 것이다.

"Vol. 13" -> ['Vol.', '13'] "Apr. 13" -> ['Apr.', '13'] "U.S. Pat. No. 134" -> ['U.S.', 'Pat.', 'No.','134']

 

그래서, 위와 같은 사태를 방지하기 위해, 내 데이터에 맞는 Known Abbreviation을 추가해주어야 한다.

하지만 punkt 모델을 새로 정의하고 나의 Known Abbreviation을 추가할 경우, 기존에 학습되어있던 약어들을 일일히 개발자가 직접 입력해줘야하는 부작용이 있다.

 

오랜 삽질 끝에, 코드 분석과 스택오버플로우를 통해 답을 찾을 수 있었다.

 

nltk.sent_tokenize에 Abbreviations 추가하기


import nltk
from nltk.data import load
tokenizer = load("tokenizers/punkt/english.pickle")

 

코드를 분석해 본 결과, nltk.sent_tokenizer는 nltk_data cache폴더에 있는 tokenizers/punkt/english.pickle 파일을 불러온다는 것을 알게 되었다. 즉 일단, 먼저 해줘야 하는 것은 tokenizer를 직접 불러온 뒤 이 tokenizer를 뜯어 고쳐야 한다는 것이다.

 

extra_abbreviations = [
    'RE','re','pat', 'no', 'nos','vol','jan','feb','mar','apr','jun',
    'jul','aug','sep','oct','nov','dec','eng','ser','ind','ed','pp',
    'e.g','al','T.E.N.S', 'E.M.S','F.E','U.H.T.S.T','degree',
    '/gm','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O',
    'P','Q','R','S','T','U','V','W','X','Y','Z']
tokenizer._params.abbrev_types.update(extra_abbreviations)

 

문제는 load로 불러온 Tokenizer는 punktTokenizer의 메인 클래스의 인스턴스가 아니라 이미 학습된 PunktTokenizer을 활용한 SentenceTokenizer라는 클래스의 인스턴스라는 점이다. 

여기서 막혀서 고생을 했지만, 모든 해답을 알고 계시는 스택오버플로우의 도움을 받아, _params로 파라미터에 직접 접근 한 뒤, abbrev_types을 강제로 업데이트하면 된다는 사실을 알게 되었다.

 

extra_abbreviations를 선언한뒤, 원하는 약어들을 넣으면 되는데, punkt는 기본적으로 "글자 전부가 대문자인 단어" 외에는 모두 소문자 단어로 치환하여 처리한다. 예를 들어 "B.E.S.T"는 모두 대문자이므로, abbreviations에 추가할 때 "B.E.S.T"로 입력하면 되지만, Jan, Vol, E.g. 등은 모든 문자를 소문자로 바꿔서 'jan', 'vol', 'e.g'로 추가해야 한다.

또한, 추가하는 방법은 약어를 상징하는 마침표 앞부분까지만 입력을 해야한다.

예를 들어 No. 137이 쪼개지지 않게 하기 위해서는 abbreviation으로 'no.'가 아니라 'no'만 추가하면 된다.

 

 

튜닝된 Tokenizer를 통해 다시 Sentence 분리하기


 

import nltk

from nltk.data import load
tokenizer = load("tokenizers/punkt/english.pickle")
extra_abbreviations = [
    'RE','re','pat', 'no', 'nos','vol','jan','feb','mar','apr','jun',
    'jul','aug','sep','oct','nov','dec','eng','ser','ind','ed','pp',
    'e.g','al','T.E.N.S', 'E.M.S','F.E','U.H.T.S.T','degree',
    '/gm','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O',
    'P','Q','R','S','T','U','V','W','X','Y','Z']
tokenizer._params.abbrev_types.update(extra_abbreviations)

load_file=open('./input.txt','r')
save_file=open('./output.txt','w')
no_blank = False
while True:
    line = load_file.readline()
    if line == "":
        break
    if line.strip() == "":
        if no_blank:
            continue
        save_file.write(f"{line}")
    else:
        print(line)
        result_ = tokenizer.tokenize(line)
        print(result_)
        result  = [ f"{cur_line}\n" for cur_line in result_ ]
        for save_line in result:
            save_file.write(save_line)

 

 

 

위와 같은 방식을 사용하면, 위와 같은 복잡한 Corpus 파일이 문장 별로 개행된 파일로 아래와 같이 변경된다.

 

 

 

많은 블로거 분들이 sent_tokenize를 다뤘지만, abbreviation을 추가하는 방법에 대해서는 다루고 있는 글이 잘 없었다.

뿐만 아니라, 아무리 찾아봐도 NLTK의 공식 문서에도 해당 내용은 찾아볼 수가 없어서 정말 하룻밤을 꼴딱 새며 해결해야했다. 이 글을 보시는 연구자분들은 삽질 없이 능률적인 개발을 하실 수 있기를 바래본다.

반응형
블로그 이미지

Hyunsoo Luke HA

석사를 마치고 현재는 Upstage에서 전문연구요원으로 활동중인 AI 개발자의 삽질 일지입니다! 이해한 내용을 정리하는 용도로 만들었으니, 틀린 내용이 있으면 자유롭게 의견 남겨주세요!

,