최종: 축약어 분리는 하지 x, .(period) 등의 부호는 따로 counting

Tokenization


축약형은 어떻게 할 것인가?

ex) I’ll → I will 로 풀어쓸 것인가?

  • solution1. 풀어쓰지 않고 원본 형태로 counting. (V)
    - “I am” 과 “I’m”은 동일하게 취급해도 될까?
    - 일부 시각에서는 이러한 줄임말(abbreviation) 비율이 corpus의 informality의 지표가 될 수도 있다고 해석하며, 변형을 지양함.
    • solution2. 해체하고 따로 counting. (I’m → I am 후 각각 count)
      • 대부분 그렇게 하지 않는다고 한다. 따라서 우리도 풀어쓰지 않겠다.
        • 또한, SUBTLEX 논문에 의하면, lemmatation은 word-prediction 등에 유의미한 차이를 불러 일으키지 않아, 일단 굳이 하지 않아도 될 듯 하다.

대소문자는?

SUBTLEX 논문에 의하면, corpus 내에서 자주 등장는 고유 명사 중 사람의 이름이 많고, 이는 대부분 대문자로 적혀 있으니, 이 과대 추정을 최소화하기 위해 분석에서는 소문자만 했다. (CD)계산

  • 따라서 이를 도입하려면, 전처리 단계에서 소문자 변환을 하지 말아야 한다.
  • 이후 분석 단계에서 일일이 걸러내는 작업이 더 적합할 것.
  • 일단, Elexicon site에서 제공하는 값은 대소문자 구분하지 않은 값임을 확인함.
    • 다만 subtlex 논문에서 대소문자 구분이 중요하다고 했으니 이후에 시도

Tokenizer


기존 library(nltk, scipy)에서 제공하는 tokenizer는 원하는 대로 되지 않아 nltk.TreebankTokenizer 을 수정해서 사용.

  • ex) “I’m a student. I am a professor.”

WhitespaceTokenizer.tokenize()

공백, 구두점을 기준으로 분리

  • “I’m”, ‘a’, ‘student.’, ‘I’, ‘am’, ‘a’, ‘professor.‘

nltk.word_tokenize()

Penn Treebank tokenizer를 사용함.

  • 구두점은 별도 토큰으로 분리
  • 축약형은 분리 (I’m → I, ‘m)
  • 숫자는 보존
  • 이메일, URL 등은 보존
  • ‘I’, “‘m”, ‘a’, ‘student’, ’.’, ‘I’, ‘am’, ‘a’, ‘professor’, ’.‘

customTokenizer

user-define tokenizer

        class CustomTokenizer:
            """
            다양한 설정이 가능한 커스텀 토크나이저
            """
            
            def __init__(self, 
                         preserve_contractions=True,
                         preserve_possessives=True,
                         preserve_numbers=True,
                         preserve_urls=True,
                         preserve_emails=True,
                         filter_tokens=True):
                
                self.treebank_tokenizer = TreebankWordTokenizer()
                self.preserve_contractions = preserve_contractions
                self.preserve_possessives = preserve_possessives
                self.preserve_numbers = preserve_numbers
                self.preserve_urls = preserve_urls
                self.preserve_emails = preserve_emails
                self.filter_tokens = filter_tokens
                
                # 허용할 문자 패턴 (영어 대소문자, 마침표, 콤마, 물음표, 느낌표)
                self.allowed_pattern = re.compile(r"^[a-zA-Z.,'!?]+$")
                
                # 보호할 패턴들
                self.protection_patterns = []
                
                if preserve_contractions:
                    self.protection_patterns.extend([
                        # be 동사 축약형
                        r"\\bI'm\\b", r"\\bI'M\\b", r"\\bi'm\\b",
                        r"\\b(?:he|she|it)'s\\b", r"\\b(?:HE|SHE|IT)'S\\b", r"\\b(?:he|she|it)'s\\b",
                        r"\\b(?:we|you|they)'re\\b", r"\\b(?:WE|YOU|THEY)'RE\\b", r"\\b(?:we|you|they)'re\\b",
                        
                        # have 동사 축약형
                        r"\\b(?:I|you|we|they)'ve\\b", r"\\b(?:I|YOU|WE|THEY)'VE\\b", r"\\b(?:I|you|we|they)'ve\\b",
                        
                        # will 축약형
                        r"\\b(?:I|you|he|she|it|we|they)'ll\\b", r"\\b(?:I|YOU|HE|SHE|IT|WE|THEY)'LL\\b", r"\\b(?:I|you|he|she|it|we|they)'ll\\b",
                        
                        # would 축약형
                        r"\\b(?:I|you|he|she|it|we|they)'d\\b", r"\\b(?:I|YOU|HE|SHE|IT|WE|THEY)'D\\b", r"\\b(?:I|you|he|she|it|we|they)'d\\b",
                        
                        # 부정 축약형
                        r"\\b(?:do|does|did|can|could|will|would|should|must|is|are|was|were|has|have|had)n't\\b",
                        r"\\b(?:DO|DOES|DID|CAN|COULD|WILL|WOULD|SHOULD|MUST|IS|ARE|WAS|WERE|HAS|HAVE|HAD)N'T\\b",
                        r"\\b(?:do|does|did|can|could|will|would|should|must|is|are|was|were|has|have|had)n't\\b",
                    ])
                
                if preserve_possessives:
                    self.protection_patterns.append(r"\\b\\w+'s\\b")
                
                if preserve_numbers:
                    self.protection_patterns.append(r"\\b\\d+(?:\\.\\d+)?\\b")
                
                if preserve_urls:
                    self.protection_patterns.append(r"https?://\\S+")
                
                if preserve_emails:
                    self.protection_patterns.append(r"\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b")
                
                # 정규식 컴파일
                if self.protection_patterns:
                    self.protection_regex = re.compile('|'.join(self.protection_patterns))
                else:
                    self.protection_regex = None
            
            def is_allowed_token(self, token):
                """
                토큰이 허용된 패턴인지 확인
                """
                # 빈 토큰은 허용하지 않음
                if not token:
                    return False
                
                # 허용된 패턴에 맞는지 확인
                return bool(self.allowed_pattern.match(token))
            
            def tokenize(self, text):
                """
                설정에 따라 텍스트를 토큰화
                """
        
                # 특수기호 필터링
                text = text.replace("//", " ")
        
                if not self.protection_regex:
                    # 보호할 패턴이 없으면 Treebank Tokenizer만 사용
                    tokens = self.treebank_tokenizer.tokenize(text)
                else:
                    # 1. 보호할 패턴들을 임시 토큰으로 대체
                    protected_text = text
                    protected_matches = []
                    
                    for match in self.protection_regex.finditer(text):
                        match_text = match.group()
                        protected_matches.append(match_text)
                        protected_text = protected_text.replace(match_text, f"__PROTECTED_{len(protected_matches)-1}__")
                    
                    # 2. Treebank Tokenizer로 토큰화
                    tokens = self.treebank_tokenizer.tokenize(protected_text)
                    
                    # 3. 임시 토큰을 원래 형태로 복원
                    final_tokens = []
                    for token in tokens:
                        if token.startswith("__PROTECTED_") and token.endswith("__"):
                            # 정규식을 사용하여 인덱스 추출
                            match = re.search(r"__PROTECTED_(\\d+)__", token)
                            if match:
                                idx = int(match.group(1))
                                if idx < len(protected_matches):
                                    final_tokens.append(protected_matches[idx])
                                else:
                                    final_tokens.append(token)  # 인덱스가 범위를 벗어나면 원본 토큰 유지
                            else:
                                final_tokens.append(token)  # 패턴이 맞지 않으면 원본 토큰 유지
                        else:
                            final_tokens.append(token)
                    
                    tokens = final_tokens
                
                # 4. 온점을 명시적으로 분리 (토큰 중간에 있는 온점도 포함)
                processed_tokens = []
                for token in tokens:
                    # 온점이 포함된 토큰인지 확인
                    if '.' in token:
                        # 온점을 기준으로 분리
                        parts = token.split('.')
                        for i, part in enumerate(parts):
                            if part:  # 빈 문자열이 아닌 경우만 추가
                                processed_tokens.append(part)
                            if i < len(parts) - 1:  # 마지막 부분이 아니면 온점 추가
                                processed_tokens.append('.')
                    else:
                        processed_tokens.append(token)
                
                # 5. 토큰 필터링 (허용되지 않은 토큰 제거)
                if self.filter_tokens:
                    filtered_tokens = []
                    for token in processed_tokens:
                        if self.is_allowed_token(token):
                            filtered_tokens.append(token)
                    return filtered_tokens
                
                return processed_tokens
        
        # 사용 예시
        def test_configurable_tokenizer():
            # 축약형만 보존 + 토큰 필터링
            tokenizer1 = CustomTokenizer(
                preserve_contractions=True,
                preserve_possessives=False,
                preserve_numbers=False,
                preserve_urls=False,
                preserve_emails=False,
                filter_tokens=True
            )
            
            # 축약형과 소유격 보존 + 토큰 필터링
            tokenizer2 = CustomTokenizer(
                preserve_contractions=True,
                preserve_possessives=True,
                preserve_numbers=False,
                preserve_urls=False,
                preserve_emails=False,
                filter_tokens=True
            )
            
            # 토큰 필터링 없이
            tokenizer3 = CustomTokenizer(
                preserve_contractions=True,
                preserve_possessives=True,
                preserve_numbers=False,
                preserve_urls=False,
                preserve_emails=False,
                filter_tokens=False
            )
            
            test_text = "I'm going to John's house at 3:30 PM. Email me at test@example.com or visit <https://example.com>. How are you?"
            
            print("축약형만 보존 + 토큰 필터링:")
            tokens1 = tokenizer1.tokenize(test_text)
            print(tokens1)
            
            print("\\n모든 패턴 보존 + 토큰 필터링:")
            tokens2 = tokenizer2.tokenize(test_text)
            print(tokens2)
            
            print("\\n토큰 필터링 없이:")
            tokens3 = tokenizer3.tokenize(test_text)
            print(tokens3)
        
        test_configurable_tokenizer()
        ```
        
- "I'm", 'a', 'student', '.', 'I', 'am', 'a', 'professor', '.'