자연어, 비전

자연어 처리를 위한 전처리 과정 정리

H_erb Salt 2020. 11. 6. 16:24
2.preprocessing

전처리 과정 개요

  1. 코퍼스 수집
  2. 정제
  3. 문장 단위 분절
  4. 분절
  5. 병렬 코퍼스 정렬(생략가능)
  6. 서브워드 분절

에 관해 차례차례 알아보자

1. 코퍼스 수집

pass

2. 정제

  • 원하는 업무와 문제에 따라, 또는 응용 분야에 따라 필요한 정제의 수준이나 깊이가 다름
  • ex) 음성인식: 사람의 음성을 그대로 받아적어야 하므로 괄호 또는 별표와 같은 특수 문자들을 포함해서는 안됨. 전화번호나 이메일주소, 신용카드 번호와 같은 개인정보나 민감한 정보들은 제거하거나 변조해서 모델링해야 할 수도 있음

전각문자 제거

  • 대부분의 중국어와 일본어 문서, 그리고 일부 한국어 문서의 숫자, 영자, 기호가 전각문자일 때가 있음. 이러한 경우 일반적으로 사용되는 반각문자로 변환해주는 작업이 필요함

대소문자 통일

  • 일부 영어 코퍼스에서는 약자 등에서의 대소문자 표현이 않을 때가 있음. 예를 들어 New York City의 줄임말(약자)인 NYC의 경우 다음과 같이 다양하게 표현가능(NYC/nyc/N.Y.C/N.Y.C.)
  • 이러한 다양한 표현의 일원화는 하나의 의미를 지니는 여러 단어를 하나의 형태로 통일해 희소성을 줄이는 효과를 기대할 수 있음.
  • 하지만 다양한 단어들을 비슷한 값의 벡터로 나타낼 수 있게 되면서, 대소문자 통일과 같은 문제를 해결할 필요성이 줄어듦

정규표현식을 사용한 정제

  • 크롤링을 통해 얻어낸 데이터의 경우 특수문자, 기호등에 의해 노이즈가 섞일때가 많음. 이러한 노이즈들을 효율적으로 감지하고 없애려면 정규표현식이 필요함
In [16]:
import re

text = '''abc123def456ghi'''

pattern = '([a-z])[0-9]+([a-z])'
to = '띠용'

y = re.sub(pattern, to, text)
y
Out[16]:
'ab띠용e띠용hi'

3. 문장 단위 분절

  • 여러 문장이 한 라인에 있거나, 한 문장이 여러 라인에 걸쳐 있는 경우에는 문장 단위의 분절이 필요함.
  • 단순히 마침표만을 기준으로 문장 단위 분절을 수행하면 소수점 등 여러가지 문제에 마주칠 수 있음.
  • 따라서 직접 분절해주는 모듈을 만들거나 nltk 등에서 제공하는 분절 모듈을 이용하기를 권장.
  • 물론 이경우에도 완벽하지 못하며 일부 추가적인 전/후처리가 필요할 수도 있음.
In [22]:
text = '자연어처리는 인공지능의 한 줄기 입니다. 시퀀스 투 시퀀스의 등장 이후로 딥러닝을 활용한 자연어처리는 새로운 전기를 맞이하게 되었습니다. 문장을 받아 단순히 수치로 나타내던 시절을 넘어, 원하는대로 문장을 만들어낼 수 있게 된 것 입니다. 이에따라 이전까지 큰 변화가 없었던 자연어처리 분야의 연구는 폭발적으로 늘어나기 시작하여, 곧 기계번역 시스템은 신경망에 의해 정복 당하였습니다. 또한, attention 기법의 고도화로 전이학습이 발전하면서, QA 문제도 사람보다 정확한 수준이 되었습니다.'
In [25]:
import sys, fileinput, re
from nltk.tokenize import sent_tokenize


if text.strip() != "":
    text = re.sub(r'([a-z])\.([A-Z])', r'\1. \2', text.strip())
    sentences = sent_tokenize(text.strip())
    for s in sentences:
        if s != "":
            result = sys.stdout.write(s + "\n")
            
result
자연어처리는 인공지능의 한 줄기 입니다.
시퀀스 투 시퀀스의 등장 이후로 딥러닝을 활용한 자연어처리는 새로운 전기를 맞이하게 되었습니다.
문장을 받아 단순히 수치로 나타내던 시절을 넘어, 원하는대로 문장을 만들어낼 수 있게 된 것 입니다.
이에따라 이전까지 큰 변화가 없었던 자연어처리 분야의 연구는 폭발적으로 늘어나기 시작하여, 곧 기계번역 시스템은 신경망에 의해 정복 당하였습니다.
또한, attention 기법의 고도화로 전이학습이 발전하면서, QA 문제도 사람보다 정확한 수준이 되었습니다.

4. 분절

  • 한국어의 경우 띄워쓰기가 이미 되어 있어도 제각각인 경우가 많기 때문에, 정규화를 해주는 의미로 다시 한 번 표준화된 띄어쓰기를 적용하는 과정이 필요함.
  • 또한, 교착어로써 접사를 어근에서 분리해주는 역할도 하므로 희소성 문제를 해소하기도 함.
  • 영어의 경우 기본적으로 띄어쓰기가 있고, 대부분의 경우 규칙을 매우 잘 따르고 있음. 다만 언어 모델을 더 용이하게 구성할 수 있또록 일불 처리를 더 해주면 좋음. nltk를 사용하여 이러한 전처리를 수행함
  • 한국어의 경우 Mecab, KoNLPy 등이 사용

5. 병렬 코퍼스 정렬

  • 대부분의 병렬 코퍼스들은 여러 문장 단위로 정렬됨. 예를 들어, 영자 신문에서 크롤링한 영문 뉴스 기사는 한글 뉴스 기사에 매핑되지만, 문서와 문서 단위의 매핑일 뿐 문장 대 문장에 관한 정렬을 이루어져있지 않음. 이런 경우, 각각의 문장에 대해 정렬해주어야 함
  • 그 과정에서 일부 불필요한 문장들을 걸러내야 하고, 문장 간 정렬이 잘 맞지 않는 경우 정렬을 재정비하거나 아예 걸러내야 함.

5.1 병렬 코퍼스 제작 프로세스 개요: 정렬을 수행하기 위한 전체 과정 요약

  1. 소스 언어와 타겟 언어 사이의 단어사전을 준비
  2. 만약 준비된 단어 사전이 없다면 다음 작업을 수행함. 이미 단어 사전을 가지고 있는 경우 7번으로
  3. 각 언어에 대해서 코퍼스를 수집 및 정제
  4. 각 언어에 대해서 단어 임베딩 벡터를 구함.
  5. MUSE를 통해서 단어 레벨 번역기를 훈련
  6. 훈련된 단어 레벨 번역기를 통해 두 언어 사이의 단어 사전 생성
  7. 만들어진 단어 사전을 넣어 Champollion을 통해 기존에 수집된 다중 언어 코퍼스를 정렬
  8. 각 언어에 대해서 단어 사전을 적용하기 위해 알맞은 수준의 분절을 수행
  9. 각 언어에 대해서 정제를 수행
  10. Champollion을 사용하여 병렬 코퍼스를 생성

5.2 사전 생성

  • 페이스북의 MUSE는 병렬 코퍼스가 없는 상황에서 사전을 구축하는 방법과 코드를 제공함
  • 비지도학습

5.3 CTK를 활용한 정렬

  • CTK는 이중 언어 코퍼스의 문장 정렬을 수행하는 오픈소스

6. 서브워드 분절

  • BPE 알고리즘을 통한 서브워드 단위 분절은 현재 필수 전처리 방법으로 자리잡음.
  • 서브워드 분절 기법은 기본적으로 '단어는 의미를 가진 더 작은 서브워드들의 조합으로 이루어진다'는 가정하에 적용되는 알고리즘
  • 적절한 서브워드를 발견하여 해당 단위로 쪼개주면 어휘 수를 줄일 수 있고 희소성을 효과적으로 줄일 수 있음
  • 희소성 감소 외에도, UNK(unknown) 토큰에 대한 효율적인 대처도 가능
  • 서브워드 단위 분절을 통해 신조어나 오타와 같은 UNK에 대해 서브워드 단위나 문자 단위로 쪼개줌으로써 기존 훈련 데이터에서 보았던 토큰들의 조합으로 만들수 있음.
  • 즉, UNK 자체를 없앰으로써 효율적으로 UNK에 대처할 수 있고, 자연어 처리 알고리즘의 결과물 품질을 향상시킬 수 있음.
  • 구글의 SentencePiece 모듈이 속도가 빠름

토치텍스트

  • 머신러닝이나 딥러닝을 수행하는 데이터를 읽고 전처리하는 코드를 모아둔 라이브러리
  • 보통 Field라는 클래스를 통해 우리가 ㅇ릭고자 하는 텍스트 파일 내의 필드를 먼저 정의함
  • 텍스트 파일 내에서 탭을 사용하여 필드(또는 열)를 구분하는 방식을 자연어 처리 분야의 입력에서 가장 많이 사용
  • 정의된 각 필드를 Dataset 클래스를 통해 읽어들임
  • 읽어들인 코퍼스는 미리 주어진 미니배치 크기에 따라서 나뉠 수 있도록 이터레이터에 들어감
  • 미니배치를 구성하는 과정에서 미니배치 내의 문장의 길이가 다를 경우에는 필요에 따라 문장의 앞 뒤에 패딩(PAD)을 삽입함
  • 이 패딩은 추후 소개할 BOS, EOS와 함께 하나의 단어 또는 토큰과 같은 취급을 받음
  • 이후에 훈련 코퍼스에 대해 어휘 사전을 만들어 각 단어(토큰)를 숫자로 매핑하는 작업을 수행함

코퍼스와 레이블 읽기

한 줄에서 클래스와 텍스트가 탭으로 구분된 데이터

  • 이런 예제는 주로 텍스트 분류를 위해 사용됨
In [44]:
from torchtext import data


class DataLoader(object):
    '''
    Data loader class to load text file using torchtext library.
    '''

    def __init__(
        self, train_fn,
        batch_size=64,
        valid_ratio=.2,
        device=-1,
        max_vocab=999999,
        min_freq=1,
        use_eos=False,
        shuffle=True,
    ):
        '''
        DataLoader initialization.
        :param train_fn: Train-set filename
        :param batch_size: Batchify data fot certain batch size.
        :param device: Device-id to load data (-1 for CPU)
        :param max_vocab: Maximum vocabulary size
        :param min_freq: Minimum frequency for loaded word.
        :param use_eos: If it is True, put <EOS> after every end of sentence.
        :param shuffle: If it is True, random shuffle the input data.
        '''
        super().__init__()

        # Define field of the input file.
        # The input file consists of two fields.
        self.label = data.Field(
            sequential=False,
            use_vocab=True,
            unk_token=None
        )
        self.text = data.Field(
            use_vocab=True,
            batch_first=True,
            include_lengths=False,
            eos_token='<EOS>' if use_eos else None,
        )

        # Those defined two columns will be delimited by TAB.
        # Thus, we use TabularDataset to load two columns in the input file.
        # We would have two separate input file: train_fn, valid_fn
        # Files consist of two columns: label field and text field.
        train, valid = data.TabularDataset(
            path=train_fn,
            format='tsv', 
            fields=[
                ('label', self.label),
                ('text', self.text),
            ],
        ).split(split_ratio=(1 - valid_ratio))

        # Those loaded dataset would be feeded into each iterator:
        # train iterator and valid iterator.
        # We sort input sentences by length, to group similar lengths.
        self.train_loader, self.valid_loader = data.BucketIterator.splits(
            (train, valid),
            batch_size=batch_size,
            device='cuda:%d' % device if device >= 0 else 'cpu',
            shuffle=shuffle,
            sort_key=lambda x: len(x.text),
            sort_within_batch=True,
        )

        # At last, we make a vocabulary for label and text field.
        # It is making mapping table between words and indice.
        self.label.build_vocab(train)
        self.text.build_vocab(train, max_size=max_vocab, min_freq=min_freq)

한 라인이 텍스트로만 채워져 있을 때

  • 주로 언어모델을 훈련 시키는 상황에서 쓸 수 있음
  • LanguageModelDataset을 통해 미리 정의된 필드를 텍스트 파일에서 읽어들임
  • 이 때 각 문장의 길이에 따라 정렬을 통해 비슷한 길이의 문장끼리 미니배치를 만들어줌
  • 이 작업을 통해서 매우 상이한 길이의 문장들이 하나의 미니배치에 묶여 훈련 시간에서 손해보는 것을 방지
In [46]:
from torchtext import data, datasets

PAD, BOS, EOS = 1, 2, 3


class DataLoader():

    def __init__(self, 
                 train_fn,
                 valid_fn, 
                 batch_size=64, 
                 device='cpu', 
                 max_vocab=99999999, 
                 max_length=255, 
                 fix_length=None, 
                 use_bos=True, 
                 use_eos=True, 
                 shuffle=True
                 ):
        
        super(DataLoader, self).__init__()

        self.text = data.Field(sequential=True, 
                               use_vocab=True, 
                               batch_first=True, 
                               include_lengths=True, 
                               fix_length=fix_length, 
                               init_token='<BOS>' if use_bos else None, 
                               eos_token='<EOS>' if use_eos else None
                               )

        train = LanguageModelDataset(path=train_fn, 
                                     fields=[('text', self.text)], 
                                     max_length=max_length
                                     )
        valid = LanguageModelDataset(path=valid_fn, 
                                     fields=[('text', self.text)], 
                                     max_length=max_length
                                     )

        self.train_iter = data.BucketIterator(train, 
                                              batch_size=batch_size, 
                                              device='cuda:%d' % device if device >= 0 else 'cpu', 
                                              shuffle=shuffle, 
                                              sort_key=lambda x: -len(x.text), 
                                              sort_within_batch=True
                                              )
        self.valid_iter = data.BucketIterator(valid, 
                                              batch_size=batch_size, 
                                              device='cuda:%d' % device if device >= 0 else 'cpu', 
                                              shuffle=False, 
                                              sort_key=lambda x: -len(x.text), 
                                              sort_within_batch=True
                                              )

        self.text.build_vocab(train, max_size=max_vocab)


class LanguageModelDataset(data.Dataset):

    def __init__(self, path, fields, max_length=None, **kwargs):
        if not isinstance(fields[0], (tuple, list)):
            fields = [('text', fields[0])]

        examples = []
        with open(path) as f:
            for line in f:
                line = line.strip()
                if max_length and max_length < len(line.split()):
                    continue
                if line != '':
                    examples.append(data.Example.fromlist(
                        [line], fields))

        super(LanguageModelDataset, self).__init__(examples, fields, **kwargs)

병렬 코퍼스 읽기

  • 텍스트로만 채워진 두 개의 파일을 동시에 입력 데이터로 읽어 들이는 코드
  • 이 때 두 파일의 코퍼스는 병렬 데이터로 취급되어 같은 라인끼리 매핑되어야 하므로, 같은 라인수로 채워져 있어야 함
  • 주로 기계번역이나 요약 등에 사용할 수 있음
  • 탭을 사용하여 하나의 파일에서 두개의 열에 각 언어의 문장을 표현하는 것도 한 가지 방법
  • 그렇다면 앞의 TabularDataset 클래스를 이용하면 됨
  • 그리고 앞 서 소개함 LanguageModelDataset과 마찬가지로 길이에 따라서 미니배치를 구성함
In [47]:
from torchtext import data, datasets

PAD, BOS, EOS = 1, 2, 3


class DataLoader():

    def __init__(self,
                 train_fn=None,
                 valid_fn=None,
                 exts=None,
                 batch_size=64,
                 device='cpu',
                 max_vocab=99999999,
                 max_length=255,
                 fix_length=None,
                 use_bos=True,
                 use_eos=True,
                 shuffle=True,
                 dsl=False
                 ):

        super(DataLoader, self).__init__()

        self.src = data.Field(
            sequential=True,
            use_vocab=True,
            batch_first=True,
            include_lengths=True,
            fix_length=fix_length,
            init_token='<BOS>' if dsl else None,
            eos_token='<EOS>' if dsl else None,
        )

        self.tgt = data.Field(
            sequential=True,
            use_vocab=True,
            batch_first=True,
            include_lengths=True,
            fix_length=fix_length,
            init_token='<BOS>' if use_bos else None,
            eos_token='<EOS>' if use_eos else None,
        )

        if train_fn is not None and valid_fn is not None and exts is not None:
            train = TranslationDataset(
                path=train_fn,
                exts=exts,
                fields=[('src', self.src), ('tgt', self.tgt)],
                max_length=max_length
            )
            valid = TranslationDataset(
                path=valid_fn,
                exts=exts,
                fields=[('src', self.src), ('tgt', self.tgt)],
                max_length=max_length,
            )

            self.train_iter = data.BucketIterator(
                train,
                batch_size=batch_size,
                device='cuda:%d' % device if device >= 0 else 'cpu',
                shuffle=shuffle,
                sort_key=lambda x: len(x.tgt) + (max_length * len(x.src)),
                sort_within_batch=True,
            )
            self.valid_iter = data.BucketIterator(
                valid,
                batch_size=batch_size,
                device='cuda:%d' % device if device >= 0 else 'cpu',
                shuffle=False,
                sort_key=lambda x: len(x.tgt) + (max_length * len(x.src)),
                sort_within_batch=True,
            )

            self.src.build_vocab(train, max_size=max_vocab)
            self.tgt.build_vocab(train, max_size=max_vocab)

    def load_vocab(self, src_vocab, tgt_vocab):
        self.src.vocab = src_vocab
        self.tgt.vocab = tgt_vocab


class TranslationDataset(data.Dataset):
    """Defines a dataset for machine translation."""

    @staticmethod
    def sort_key(ex):
        return data.interleave_keys(len(ex.src), len(ex.trg))

    def __init__(self, path, exts, fields, max_length=None, **kwargs):
        """Create a TranslationDataset given paths and fields.
        Arguments:
            path: Common prefix of paths to the data files for both languages.
            exts: A tuple containing the extension to path for each language.
            fields: A tuple containing the fields that will be used for data
                in each language.
            Remaining keyword arguments: Passed to the constructor of
                data.Dataset.
        """
        if not isinstance(fields[0], (tuple, list)):
            fields = [('src', fields[0]), ('trg', fields[1])]

        if not path.endswith('.'):
            path += '.'

        src_path, trg_path = tuple(os.path.expanduser(path + x) for x in exts)

        examples = []
        with open(src_path, encoding='utf-8') as src_file, open(trg_path, encoding='utf-8') as trg_file:
            for src_line, trg_line in zip(src_file, trg_file):
                src_line, trg_line = src_line.strip(), trg_line.strip()
                if max_length and max_length < max(len(src_line.split()), len(trg_line.split())):
                    continue
                if src_line != '' and trg_line != '':
                    examples += [data.Example.fromlist([src_line, trg_line], fields)]

        super().__init__(examples, fields, **kwargs)
In [ ]:
 

'자연어, 비전' 카테고리의 다른 글

카카오톡 대화 내용으로 개인별 워드클라우드(wordcloud) 그리기  (1) 2022.06.22
transformer 구현 및 설명  (0) 2021.06.03
시퀀스 모델링  (0) 2020.11.10
워드 임베딩  (0) 2020.11.10
단어 유사도 정리  (0) 2020.11.09