데이터 분석/머신러닝, 딥러닝

텍스트 전처리

fullfish 2025. 11. 18. 13:21

원문 텍스트 수집
정규화(Normalization): 소문자화, 공백 정리, 반복 문자(ㅋㅋㅋㅋ) 축약, 숫자/기호 처리 등

토큰화(Tokenization): 한글: 형태소 분석기(Okt)로 단어/형태소 단위 자르기

어간/표제어 처리(Stemming/Lemmatization): “먹었다 → 먹다”, “했습니다 → 하다”처럼 기본형으로 통일

불용어(Stopwords) 제거: “은/는/이/가/그리고/하지만…” 같은 의미 정보 적은 단어 삭제

벡터화: Bag-of-Words, TF-IDF, Word2Vec, BERT 토큰 등 숫자 벡터로 변환

 

morphs

morphs(phrase, norm=False, stem=False)

문장을 형태소 단위의 리스트로 변환

 

phrase: 형태소 분석을 수행할 입력 문장

norm (기본값: False): 정규화 여부 결정

True → 비표준어, 줄임말, 반복 문자 등을 더 표준적인 형태로 바꾼 뒤 분석
예:“ㅋㅋㅋㅋ” → “ㅋㅋ”, “됐어욬ㅋㅋ” → “됐어”

stem (기본값: False): 기본형(표제어)으로 변환 여부 결정

True → 동사·형용사 등의 활용형을 사전형으로 변환
예: “먹었습니다, 먹어요, 먹었어” → “먹다”, “했습니다, 하는, 했던” → “하다”

 

from konlpy.tag import Okt

okt = Okt()

text = "이것도 되나욬ㅋㅋㅋㅋ 반갑습니당~~"

print(okt.morphs(text, norm=False, stem=False))
print(okt.morphs(text, norm=True, stem=False))
print(okt.morphs(text, norm=False, stem=True))
print(okt.morphs(text, norm=True, stem=True))
['이', '것', '도', '되나욬', 'ㅋㅋㅋㅋ', '반갑', '습', '니당', '~~']
['이', '것', '도', '되나요', 'ㅋㅋㅋ', '반갑습니다', '~~']
['이', '것', '도', '되나욬', 'ㅋㅋㅋㅋ', '반갑다', '습', '니당다', '~~']
['이', '것', '도', '되다', 'ㅋㅋㅋ', '반갑다', '~~']

 

pos

pos(phrase, norm=False, stem=False, join=False)

문장을 (형태소, 품사) 리스트로 변환

 

phrase: 품사 분석을 수행할 입력 문장

norm (기본값: False): 정규화 여부 결정
True → 비표준어, 줄임말, 반복 문자 등을 더 표준적인 형태로 바꾼 뒤 분석
예: “ㅋㅋㅋㅋ” → “ㅋㅋ”, “됐어욬ㅋㅋ” → “됐어”

stem (기본값: False): 기본형(표제어)으로 변환 여부 결정
True → 동사·형용사 등의 활용형을 사전형으로 변환
예: “먹었습니다, 먹어요, 먹었어” → “먹다”, “했습니다, 하는, 했던” → “하다”

join (기본값: False): 결과 반환 형식 결정
True → “형태소/품사” 한 문자열로 반환
False → (형태소, 품사) 튜플 형태로 반환

 

명사 계열

Noun: 일반 명사
사람, 사물, 개념 이름 등
예: 학생, 책, 시간, 사랑

ProperNoun: 고유 명사
특정 대상 이름
예: 서울, 엘리스, 삼성, 한국

Number: 숫자
예: 1, 2025, 세 번, 두 개 등 수량 관련

동사·형용사 계열

Verb: 동사
동작, 행위를 나타냄
예: 가다, 먹다, 하다, 만들다

Adjective: 형용사
상태, 성질, 느낌을 나타냄
예: 예쁘다, 좋다, 슬프다, 크다

Adverb: 부사
동사·형용사·문장 전체를 꾸밈
예: 아주, 매우, 너무, 갑자기, 정말


조사·어미 계열

Josa: 조사
체언 뒤에 붙어 문장 성분의 역할을 표시
예: 이/가, 을/를, 은/는, 에, 에서, 와/과, 도

Eomi: 어미
동사·형용사 어간 뒤에 붙어 시제, 존댓말, 종결, 연결 등을 표시
예: -다, -요, -습니다, -고, -면, -니까

PreEomi: 선어말어미
어간과 어미 사이에 들어가 시제, 상, 경어를 표시
예: -었-, -겠-, -시- (갔었다, 하셨다 등)


관형사·대명사 계열

Determiner: 관형사
뒤에 오는 명사를 꾸며줌
예: 이, 그, 저, 모든, 여러, 몇

Pronoun: 대명사
사람이나 사물을 대신함
예: 나, 너, 저, 우리, 그것, 누구


감탄사

Exclamation: 감탄사
감탄, 부름, 응답 등을 나타냄
예: 아!, 어!, 야!, 어머!


접사·어근 계열

Prefix: 접두사
단어 앞에 붙어 의미를 바꿈
예: 되- (되풀이), 헛- (헛수고), 맨- (맨바닥)

Suffix: 접미사
단어 뒤에 붙어 품사나 의미를 변화시킴
예: -님 (선생님), -스럽다 (사랑스럽다), -질 (말장난질)

Root: 어근
더 이상 쪼개기 힘든 의미의 중심 부분


기타 기호·외국어

Punctuation: 구두점, 문장부호
예: ., !, ? 등

Foreign: 외국어
영어·한자 등 한국어가 아닌 문자열
예: Hello, GPT, 你好

Alpha: 알파벳(영문자)
단독 알파벳 시퀀스

Hashtag: 해시태그
예: #행복, #오늘의기분

ScreenName: 아이디(@이름)
예: @user123

Email: 이메일 주소
예: test@example.com

URL: 웹주소
예: http://..., https://...

KoreanParticle: 불임표현 (ㄴ, ㄹ 등 자모 조각)
완전한 음절이 아닌, 자모 단독 등 특수한 경우


공백·기타

Space: 공백
띄어쓰기 자체를 토큰으로 인식하는 경우

 

 

re.sub(pattern, repl, string, count=0, flags=0)

인자                              설명

pattern 바꾸고 싶은 패턴(정규식)
repl 치환할 문자열 (replacement)
string 원본 문자열
count 바꿀 횟수 (기본값 0 → 전체 치환)
flags 정규식 옵션 (re.IGNORECASE, re.MULTILINE 등)
import re
text = "나는 2025년에 30살이 된다."
result = re.sub(r"\d+", "NUM", text)
print(result)

 

기본 기호

기호               의미

. 줄바꿈 제외 아무 문자 한 글자
^ 문자열의 시작
$ 문자열의 끝
* 앞 패턴 0번 이상 반복
+ 앞 패턴 1번 이상 반복
? 앞 패턴 0번 또는 1번 (있거나 없거나)
[] 문자 집합(중 하나)
() 그룹, 캡처
\ 이스케이프 (특수문자를 문자 그대로 사용)

 

주요 패턴

패턴                          의미

\d 숫자 [0-9]
\D 숫자가 아닌 문자 [^0-9]
\s 공백(스페이스, 탭, 줄바꿈 등)
\S 공백이 아닌 문자
\w 단어 문자 [a-zA-Z0-9_]
\W 단어 문자가 아닌 것

 

문자 집합과 범위

  • [abc] → a, b, c 중 하나
  • [0-9] → 숫자 하나
  • [가-힣] → 한글 완성형 문자
  • [0-9a-zA-Z가-힣] → 숫자·영문·한글 중 하나

부정(제외)

  • [^0-9] → 숫자가 아닌 문자
  • [^0-9a-zA-Z가-힣\s] → 숫자/영문/한글/공백 제외한 문자 (→ 특수문자만 남김)

반복(Quantifiers)

기호                                                       설명

* 0번 이상 반복
+ 1번 이상 반복
? 0번 또는 1번
{m} 정확히 m번 반복
{m,} m번 이상
{m,n} m번 이상 n번 이하

 

?, +?, ?? — 비탐욕(Non-greedy) 모드

기본적으로 *, +, {m,n} 등은 탐욕적(Greedy) 이라서
“가능한 한 많이” 매칭하려고 함.

비탐욕 모드(?를 뒤에 붙임)는
“가능한 한 적게” 매칭하려고 함.

import re
text = "<tag>내용1</tag><tag>내용2</tag>"

re.findall(r"<tag>.*</tag>", text)   # 기본(탐욕)
# 결과: ['<tag>내용1</tag><tag>내용2</tag>']

re.findall(r"<tag>.*?</tag>", text)  # 비탐욕
# 결과: ['<tag>내용1</tag>', '<tag>내용2</tag>']

 

특수한 위치·선택 표현

 

  • $ : 문자열 끝
    예: world$ → “world”로 끝나는 문자열
  • \b : 단어 경계
    예: \bcat\b → 단어 “cat”만 일치 (“category”는 X)
  • ( ) : 그룹
    예: (abc)+ → “abc”, “abcabc”, “abcabcabc”…
  • | : 선택
    예: (cat|dog) → “cat” 또는 “dog”

자주 쓰는 조합 패턴

  • \d+ → 숫자가 연속된 부분 (예: “123”, “2025”)
  • [^0-9a-zA-Z가-힣\s] → 한글·영문·숫자·공백만 남기기 위한 필터
  • 이메일 → [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}

파이썬에서 자주 쓰는 함수

import re

re.search(pattern, text)   # 패턴이 있는지 찾기 (처음 1개)
re.findall(pattern, text)  # 전체 매칭된 결과 리스트
re.sub(pattern, repl, text) # 패턴 부분을 다른 문자열로 치환

 

불용어 제거

stopwords = set([
"은", "는", "이", "가", "을", "를", "에", "에서", "으로",
"그리고", "그러나", "하지만", "또는",
"도", "만", "와", "과", "저", "그", "이",
"것", "수", "들", "등"
])

 

실습

text = """주관적 해석

이성민, 차승원, 박희순을 실존인물로 해석하면 그저 말 안되는 기묘한 스토리일 뿐이고.. (면접경쟁자 3명 죽인다고 취업이 된다는 것부터 시작해서 허술한 일처리에도 형사가 못 알아채는 것 등) 사실 주인공 내면의 표상으로서 드러난 인물들이라 생각한다.

1.
이성민은 주인공이 자신과 동일시하는 대상, 그러니까 스스로 인식하고 있는 본인 그 자체다. 태초의 모습 그대로 생리현상을 숨김없이 드러내는(발가벗고 방귀를 뿡뿡 뀌는) 장면에서 알 수 있듯 어떠한 꾸밈도 없다. 찌질하고 꼬질하긴 하지만 택배박스나 돌멩이를 맞아도 허허 웃고 말고, 아내의 바람을 목격해도 혼자 소리도 못내며 바닥을 데굴데굴 구르고 마는 걸 보면 나쁜 사람은 아니다. 스스로를 악인이라 생각하는 사람은 없기 때문일 테다. 염혜란의 불륜을 알아채지 못하도록 애쓰고, 자기 일처럼 눈물짓는 모습에서 주인공이 이성민에게 스스로를 투영하고 있음이 드러난다.

3명의 희생자 중, 방아쇠를 당기기까지 가장 망설였고 또 그 과정이 가장 길고 지저분(?)하기도 했다. 총을 겨눈 상태에서 다섯 겹의 장갑을 벗어야 했고, 끝엔 비닐까지 둘둘 둘러싸여 있었다(마음 깊은 곳에선 사실 쏘기 싫다는 듯). 아내까지 합세한 육탄전을 벌였고, 첫 방은 치명적이지 않은 부위인 어깨에 맞아 한참동안 아파하다 죽어야 했다. 주인공이 자기 손으로 죽이지 못하고, 시체도 직접 처리하지 못한 유일한 인물이다.

두 인물은 뭐라뭐라 대화를 나누긴 하지만, 큰 음악소리(외부요인)에 묻혀 그저 어렴풋한 형태만 알아들을 수 있을 뿐이다. 스스로의 목소리에 귀 기울이고 명료하게 소통하는 건 어쩐지 낯부끄럽기도, 어렵기도 한 일이다. ”라이브카페 차려서 밥벌이 하는 게 뭐가 부끄러워! 아내 말도 잘 들으란 말이야!“, “난 기술자야!” 따위의 말들은 사실 스스로에게 건네고 싶은 말과 변명이었을 거다.

2.
차승원은 겉으로 보기에 멋지게 포장된, 사회적으로 보여주고 싶어하는 자신의 모습이다(추구미). 주인공의 일상에는 첼로, 사교댄스, 테니스, 분재가꾸기, 대형견(무려 2마리) 등 ‘있어보이는, 이상적인‘ 것들이 많이 등장하는데, 주인공이 타인의 눈을 꽤나 신경쓰는, 그러니까 어느 정도의 과시욕을 가진 인물이란 걸 드러낸다. 첫사랑 회상씬 등 구구절절하고 입체적인 서사가 입혀진 이성민 캐릭터에 비해, 차승원 캐릭터에 대한 부연설명은 거의 전무하다. 세련되고 화려하게 ‘보여지기‘ 위한 모습엔 깊이가 담길 수 없기 때문일 테다. 주인공이 차승원에게 건네는 자신의 이야기는 그저 피상적이고, 지나치게 요약되어 핵심이 부재한 말들이다. “이런 일 할 분으로는 안 보여요”라는 것 또한 본인이 원하는 자신의 모습일 것이다.

차승원을 없애는 건 앞전보다 훨씬 쉬웠다. 그저 한 번 해봐서 그렇다기엔 그 온도차가 너무 극명하다. 무방비로 뒤돌아있는 상대에게 큰 망설임 없이 방아쇠를 당기고, 시체도 너무 깔끔하게 트렁크에 싣는다. 이성민을 죽인 뒤 혼비백산해서 언덕을 뛰어내려오고 기름이 바닥나 당황하던 때와 비교하면 너무나도 제 정신인 상태다. 시체의 처리마저 소나무 가지모양 잡듯 아름답고 우아한 형태로 이루어진다. 가장 중요한 알맹이가 부재한 상태에서 껍데기를 버리는 건 쉬운 일이었을 거다.

3.
박희순은 짐승적 본능과 야만성을 드러내는 원초아다. 그는 주인공 농담에 정색을 하다가 갑자기 폭소하기도 하고, 부엌으로 가서 꺼내드는 게 칼인지 술인지조차 가늠할 수 없게 해 긴장감을 조성한다. 이 예측 불가능하고 고삐풀린 캐릭터가 극을 주도할 땐 주인공의 일상을 이루고 있던 것들의 끈이 턱 풀린다(앓던 이를 뽑는 것, 절제하던 술을 입에 대는 것)

“당신 술 마시면 개 되잖아. 전에는 오바이트에 숨 막혀서 질식할 뻔 하기도 했잖아“ 라는 다소 설명조(?)적인 손예진의 대사가 이 캐릭터가 곧 주인공의 또 다른 모습이란 걸 드러내고 있다고 느꼈다.

이렇게 본모습, 잘난모습, 못난모습 등 자신을 이루고 있는 다양한 면모를 몰살시키고 난 뒤에야 주인공은 비로소 기업의 부품으로써 다시 기능할 수 있게 된다.

책임져야 할 것들을 영위하기 위해 자아의 종말을 선택한 가장은 싫은 걸 싫다고 말하지 않는다. 더 이상 자신이 하고 싶은 말을 손바닥에 적으며 떨리는 목소리를 내지도 않는다."""

# ----------------------------------------
"""
1. (정규화식 + python 문자열메소드)을 이용한 방식
정제: 한글, 숫자, 공백만 남기기
공백단위로 분할해서 리스트로 만들기(토큰화)
불용어 제거

2. konlpy사용
형태소 분석 + 품사 태깅
정규화(norm=True): 말줄임, 반복 등 정리
정규화 + 어간 추출(stem=True): 용언 기본형으로

3. 전처리 함수 작성(preprocess(text))
- 정제(영문, 한글, 공백문자, 제외 모든 문자를 공백으로)
- 형태소 분석 + 정규화 + 어간추출
- 품가 기반 (명사, 동사, 형용사)만 남기게 필터링 / 불용어 제거
"""

import re
from konlpy.tag import Okt

okt = Okt()

# 불용어 가져오기
with open("example/stat_nlp/stopwords-ko.txt", encoding="utf-8") as f:
    basic_stopwords = set(w.strip() for w in f if w.strip())

# 1번 실습
print("-----------------1번----------------")

str_reg = re.sub(r"[^가-힝0-9a-zA-Z\s]", "", text)
str_arr = str_reg.split()
print(len(str_arr))


stopworded_arr = [word for word in str_arr if word not in basic_stopwords]
print(len(stopworded_arr))
print(stopworded_arr)

# 2번 실습
print("----------------2번----------------")
pos = okt.pos(text)
pos_norm = okt.pos(text, norm=True)
pos_norm_stem = okt.pos(text, norm=True, stem=True)
print(pos_norm_stem)

# 3번 실습
print("----------------3번----------------")

possible_pos = ["Noun", "ProperNoun", "Number", "Verb", "Adjective"]


def preprocess(text):
    str_reg = re.sub(r"[^가-힝0-9a-zA-Z\s]", "", text)
    pos = okt.pos(str_reg, norm=True, stem=True, join=True)
    pos = [word.split("/") for word in pos]
    filtered_pos = [
        word
        for word, tag in pos
        if word and word not in basic_stopwords and tag in possible_pos
    ]
    return " ".join(filtered_pos)


print(preprocess(text))
print("11      11".split())

# 4번 실습
# 명사끼리, 형용사끼리, 동사끼리 리턴
print("----------------4번----------------")


noun, verb, adjective = [], [], []


def preprocess_pos(text):
    str_reg = re.sub(r"[^가-힝0-9a-zA-Z\s]", "", text)
    pos = okt.pos(str_reg, norm=True, stem=True, join=True)
    pos = [word.split("/") for word in pos]
    for word, tag in pos:
        if not word or word in basic_stopwords:
            continue
        if tag == "Noun":
            noun.append(word)
        elif tag == "Verb":
            verb.append(word)
        elif tag == "Adjective":
            adjective.append(word)
    return [noun, verb, adjective]


filterd_text = preprocess_pos(text)
print(f"명사: {noun}\n\n동사: {verb}\n\n형용사: {adjective}")

'데이터 분석 > 머신러닝, 딥러닝' 카테고리의 다른 글

TF-IDF  (0) 2025.11.20
Bag-of-Word(BOW)  (0) 2025.11.19
인공신경망(ANN : Artificial Neural Network)  (0) 2025.11.17
딥러닝  (0) 2025.11.14
시계열 데이터  (0) 2025.11.13