← 전체 해설 목록  ·  week-02 · 2강

2강 — 노이즈 제거와 정규화 (실전 기법)

🎯 학습 목표

📖 해설

지난 시간엔 왜 정제하는지를 배웠습니다. 오늘은 실제로 노이즈를 제거하고 텍스트를 표준화하는 구체적 기법을 다룹니다. 적용하면 데이터 품질이 눈에 띄게 좋아져요.

먼저 지난 시간 3원칙 복습: ① 토큰은 곧 비용입니다. ② GIGO(Garbage In, Garbage Out) — 지저분한 데이터에선 아무리 좋은 AI도 좋은 결과를 못 냅니다. ③ 노이즈와 신호를 구분해야 합니다. 무작정 삭제하면 필요한 정보까지 잃어요.

1. 노이즈의 6가지 유형

노이즈는 크게 여섯 가지로 나뉩니다.

  1. 반복 문자 — ㅋㅋㅋㅋㅋ처럼 같은 문자가 과도하게 반복. 토큰을 낭비하고 감정 분석을 왜곡합니다.
  2. 의미 없는 특수문자 — ★, ♥ 같은 장식용 기호.
  3. 이모지 — 정보가 될 수도, 노이즈가 될 수도 있어 신중히 처리해야 합니다.
  4. 불규칙한 공백 — 공백이 여러 개 연속되면 토큰 분리에 문제가 생깁니다.
  5. 대소문자 불일치 — 영어에서 great / Great / GREAT가 섞이면 서로 다른 단어로 인식될 수 있습니다.
  6. 외국어 혼용 — 한글 리뷰에 일본어·중국어가 섞이면 분석 일관성이 떨어집니다.

2. 반복 문자 압축 — 그리고 "독스트링"의 중요성

가장 흔한 노이즈인 반복 문자는 정규표현식으로 처리합니다.

import re

def compress_repeats(text):
    """3번 이상 반복되는 문자를 2번으로 축소한다.
    예) 'ㅋㅋㅋㅋㅋ' -> 'ㅋㅋ'
    """
    # (.)   : 아무 문자 하나를 잡아서 기억(캡처)
    # \1    : 방금 기억한 그 문자를 다시 가리킴(역참조)
    # {2,}  : 그 문자가 2번 이상 더 반복되면
    # → 찾은 걸 '\1\1'(그 문자 2개)로 바꿔 2번만 남긴다
    return re.sub(r'(.)\1{2,}', r'\1\1', text)

print(compress_repeats("재밌다ㅋㅋㅋㅋㅋ"))  # 재밌다ㅋㅋ

여기서 잠깐, 함수 맨 위 """..."""(따옴표 3개)로 감싼 설명을 봅시다. 이걸 독스트링(docstring)이라고 합니다. 파이썬에서 따옴표 3개를 쓰면 여러 줄 문자열을 넣을 수 있는데, 함수 안 첫머리에 두면 이 함수가 무슨 일을 하는지 설명하는 용도가 됩니다.

강조하고 싶은 점: 개발에서 진짜 중요한 건 코드를 어떻게 구현하느냐보다, 이 함수가 어떤 역할을 하는지 명확히 정의하는 것입니다. 역할(독스트링)만 분명하면, 실제 코드는 직접 짜든 AI에게 시키든 얼마든지 다양하게 구현할 수 있어요. 그래서 "3번 이상 반복 문자를 2번으로 축소한다" 같은 한 줄을 명확히 적는 습관이 중요합니다.

3. 특수문자 — "지울 것"이 아니라 "남길 것"을 정한다

특수문자 처리의 핵심은 무조건 다 지우면 안 된다는 것입니다.

제거해도 되는 것 보존해야 하는 것
장식용 기호(★, ♥), 아스키 아트 마침표·쉼표·느낌표·물음표 (문장 구분의 필수)
$, % (가격·수량 등 정보)
프로그래밍·게임 표현 기호 (예: "+10 데미지")

그래서 똑똑한 방법은 "지울 것"을 일일이 나열하는 대신, "남길 것"을 지정하고 나머지를 지우는 것입니다. 정규표현식에서 대괄호 안 맨 앞에 캐럿 [^...]을 쓰면 "이것들을 제외한 나머지"라는 뜻이 됩니다.

import re

def clean_special(text):
    """한글·영어·숫자·공백과 기본 문장부호만 남기고 나머지 특수문자를 제거한다."""
    # [^...] : 대괄호 안 문자들을 '제외한' 나머지를 의미
    # 가-힣(한글) / a-zA-Z(영어) / 0-9(숫자) / \s(공백) / 기본 문장부호만 남긴다
    return re.sub(r'[^가-힣a-zA-Z0-9\s.,!?]', '', text)

print(clean_special("최고예요★♥ +10 데미지!!"))  # 최고예요 10 데미지!!

4. 이모지 — 3가지 전략 중 목적에 맞게

이모지는 상황에 따라 달라서 세 가지 전략 중 고릅니다.

  1. 완전 제거(remove) — 텍스트만으로 키워드를 뽑을 때. 이모지가 분석을 방해할 때.
  2. 그대로 유지(keep) — 감정 분석을 할 때. 이모지는 중요한 감정 정보예요(😊 vs 😡).
  3. 텍스트로 변환(convert) — 웃는 이모지를 "웃는 얼굴"처럼 말로 바꿔, 의미는 보존하면서 텍스트로 분석.
# 이모지는 emoji 라이브러리로 처리합니다. (다른 개발자가 만들어 공개한 도구)
# 설치: pip install emoji
import emoji

text = "최고예요 😊👍 강추합니다 🎮"

# 1) 완전 제거
print(emoji.replace_emoji(text, replace=''))          # 최고예요  강추합니다
# 2) 그대로 유지 → text 그대로 사용
# 3) 텍스트로 변환 (의미 보존)
print(emoji.demojize(text, language='ko'))            # 최고예요 :웃는_얼굴::따봉: 강추합니다 :게임기:

게임 리뷰 분석에서는 기본은 제거(remove) 로 하되, 감정이 중요할 때는 유지(keep) 하는 식으로 갑니다. 결국 "무엇을 말했는지"가 중요하면 변환·제거, "어떻게 느꼈는지"가 중요하면 유지라는 목적 기준으로 정합니다.

5. 정규화 — 같은 의미를 하나로 통일하기

정규화(normalization)란 다양한 형태로 쓰인 같은 의미를 하나로 통일하는 과정입니다. 도서관 사서가 책을 정리하는 것과 비슷해요. Harry Potter / HARRY POTTER / 해리포터 가 표기는 달라도 같은 책이듯, 사서는 이걸 하나의 항목으로 묶습니다.

텍스트도 마찬가지입니다. great / Great / GREAT 는 모두 같은 단어인데, 정규화하지 않으면 AI가 세 개의 다른 단어로 볼 수 있습니다. 영어 정규화의 기본은 소문자 변환이고, 파이썬은 .lower() 메서드 하나로 됩니다.

text = "This Game is GREAT"
print(text.lower())   # this game is great

⚠️ 주의: 소문자로 바꾸면 고유명사도 소문자가 됩니다(예: iPhone → iphone). 그래서 소문자화 여부는 분석 목적에 따라 결정해야 합니다.

6. 공백 정규화

공백도 통일합니다. \s는 모든 종류의 공백(스페이스·탭·줄바꿈)을, +는 "하나 이상"을 뜻하므로, \s+는 연속된 공백 덩어리를 찾습니다. 이걸 공백 하나로 바꾸고, .strip()으로 앞뒤 공백을 떼면 깔끔해집니다.

import re

def normalize_space(text):
    """연속 공백을 하나로 합치고 앞뒤 공백을 제거한다."""
    text = re.sub(r'\s+', ' ', text)   # 공백 덩어리 → 공백 1개
    return text.strip()                # 앞뒤 공백 제거

dirty = "   이 게임   너무\n\n  재미있어요   "
print(normalize_space(dirty))  # '이 게임 너무 재미있어요'

이렇게 하면 토큰 분리가 정확해지고 불필요한 공백 토큰도 줄어듭니다.

7. 한국어의 특수성 — 띄어쓰기

한국어에는 특별한 문제가 있습니다. 바로 띄어쓰기예요. 영어는 단어 사이에 반드시 공백이 있어 분리가 쉽지만, 한국어는 띄어쓰기 규칙이 복잡하고 사용자들이 자주 생략합니다. 특히 빠르게 쓴 게임 리뷰에서 게임이좋아요진짜꿀잼 같은 표현이 흔하죠. 이러면 토큰 분리가 어렵고, AI가 좋아요와 좋아요(붙은 것)를 다른 단어로 볼 수도 있습니다.

전통적으로 많은 AI가 한국어를 어려워했던 영역입니다. 그래서 우리나라에서 한국어 띄어쓰기 교정 라이브러리가 많이 연구됐어요(pip install로 설치, GitHub 개발자 블로그 등에서 코드 공개). 다만 이런 딥러닝 기반 모델은 인프라가 필요하고 실행이 느려서 대량 처리엔 부담입니다. 그래서 "이런 분야가 있다" 정도로 알아 두고, 직접 실습보다는 선택적으로 쓰는 게 현실적입니다. 게다가 요즘 GPT 같은 LLM은 띄어쓰기가 엉성해도 잘 이해하므로, 우수한 모델을 쓰면 보통 이 부분을 따로 손보지 않아도 됩니다.

8. 언어 감지 — 한국어만 거르기

한국어 리뷰를 수집해도 실제로는 영어·중국어·일본어가 20~30% 섞여 있습니다. 이게 문제를 일으킬 수 있어요.

  1. 한국어 분석기에 외국어를 넣으면 오류가 날 수 있습니다.
  2. 분석할 필요 없는 데이터에 API 비용을 낭비합니다.
  3. 분석 결과의 일관성이 떨어집니다.

스팀 리뷰에는 재미있는 패턴이 있습니다 — 한국어로 쓰다 영어로 끝나는 리뷰, 번역기로 돌린 어색한 한국어, 자음만 있는 의미 없는 텍스트(ㅇㅇ, ㅋㅋ) 등. 그래서 한국어 비율을 재서 일정 기준(임계값, threshold) 이상이면 한국어로 판단하고 필터링합니다.

import re

def korean_ratio(text):
    """텍스트에서 한글이 차지하는 비율(0~1)을 계산한다."""
    if not text:
        return 0
    hangul = re.findall(r'[가-힣]', text)      # 한글 글자만 추출
    return len(hangul) / len(text)

def is_korean(text, threshold=0.3):
    """한글 비율이 임계값(threshold) 이상이면 한국어로 본다."""
    return korean_ratio(text) >= threshold

print(is_korean("이 게임 진짜 재밌어요"))   # True
print(is_korean("This is a great game"))    # False

언어 감지는 반드시 해야 하는 건 아닙니다. 분석 목적에 따라 고려하세요. (한국어만 분석할 거면 유용하고, 다국어를 다 볼 거면 굳이 거를 필요 없습니다.)

9. 전체 정제 파이프라인

지금까지를 한 줄로 꿰면 이런 순서가 됩니다.

import re, emoji

def clean_review(text, keep_emoji=False):
    """게임 리뷰 한 건을 정제하는 전체 파이프라인.
    분석 목적에 따라 각 단계는 켜고 끄거나 순서를 바꿀 수 있다."""
    text = re.sub(r'(.)\1{2,}', r'\1\1', text)          # 1) 반복 문자 압축
    if not keep_emoji:
        text = emoji.replace_emoji(text, replace='')     # 2) 이모지 처리(목적에 따라)
    text = re.sub(r'http\S+|@\w+', '', text)             # 3) URL·멘션 제거
    text = re.sub(r'[^가-힣a-zA-Z0-9\s.,!?]', '', text)  # 4) 특수문자 정리
    text = text.lower()                                  # 5) 소문자 통일(영어)
    text = re.sub(r'\s+', ' ', text).strip()             # 6) 공백 정규화
    return text

print(clean_review("최고의 게임!!! 😊 http://x.com ★재밌다ㅋㅋㅋㅋ"))
# 최고의 게임!! 재밌다ㅋㅋ

가장 중요한 원칙: 정답인 단일 파이프라인은 없습니다. 무엇을 분석할지(목적)와 도메인 지식에 따라 각 단계의 on/off·순서·강도가 달라집니다. 큰 틀을 먼저 이해하고, "나는 소문자로 바꾸고 싶다" 같은 목적이 정해지면 그 단계는 직접 짜거나 AI에게 시켜 쉽게 구현하면 됩니다. 실습에서는 굵게 표시된 독스트링 부분을 꼭 다시 읽으며 각 단계가 무슨 일을 하는지 떠올려 보세요.

10. 정규표현식 기초 — 여기까지 나온 기호 정리

지금까지 코드에서 정규표현식 기호가 여럿 등장했습니다. 한 번 모아서 보면 다음 강에서도 두렵지 않습니다.

패턴 의미 예시
. 아무 문자 하나 a.c → abc, aXc
* 앞 문자가 0개 이상 ab* → a, ab, abbb
+ 앞 문자가 1개 이상 ab+ → ab, abbb (a는 안 됨)
\d 숫자 한 글자 [0-9] \d+ → 123, 7
\s 공백 한 글자(스페이스·탭·줄바꿈) a\sb → a b
\S 공백이 아닌 글자 \S+ → 띄어쓰기 없는 한 덩어리(단어·URL 등)
[abc] a·b·c 중 하나 [aeiou] → 영어 모음
[^...] 대괄호 안을 제외한 나머지 [^가-힣] → 한글이 아닌 글자

이걸 알면 re.findall로 원하는 것만 뽑아낼 수도 있습니다. 추출은 제거의 반대 방향일 뿐, 같은 패턴을 씁니다.

import re

text = "I played for 150 hours over 30 days. Score: 9/10"

# findall: 패턴에 맞는 부분을 '모두 찾아' 리스트로 돌려준다
print(re.findall(r'\d+', text))         # 숫자 덩어리만: ['150', '30', '9', '10']
print(re.findall(r'[a-zA-Z]+', text))   # 영어 단어만: ['I', 'played', ...]

URL·멘션·해시태그를 따로 지우고 싶다면 \S+(공백 아닌 한 덩어리)를 활용합니다. re.sub(r'https?://\S+', '', text)는 http://나 https://로 시작해 공백 전까지 이어지는 주소 전체를, @\S+·#\S+는 멘션·해시태그를 각각 통째로 지웁니다. (https?의 ?는 "앞 글자 s가 있어도 없어도 됨"이라는 뜻이라 http와 https를 한 번에 잡습니다.)

11. 얼마나 줄었나 — 정규화의 효과를 숫자로 보기

정제가 "토큰=비용"에 실제로 얼마나 기여하는지는, 정제 전후 길이를 비교해 보면 바로 체감됩니다.

test_reviews = [
    "THIS GAME IS AMAZING!!!!!!!!",
    "Check https://steam.com 👍👍👍 BEST EVERRR!!!",
    "게임 좋아요ㅋㅋㅋㅋㅋㅋㅋ 진짜 재밌음ㅠㅠㅠㅠ",
]

for review in test_reviews:
    cleaned = clean_review(review)
    # 정제로 글자 수가 몇 % 줄었는지 계산 (//는 소수점 버림 나눗셈)
    rate = 100 - len(cleaned) * 100 // len(review)
    print(f"{len(review)} → {len(cleaned)}자 ({rate}% 감소)")

게임 리뷰처럼 감탄사·이모지·반복 문자가 많은 텍스트는 보통 30~60% 정도 길이가 줄어듭니다(정확한 수치는 데이터와 설정에 따라 다릅니다). 이 줄어든 만큼이 곧 절약된 토큰이자 비용입니다.

12. 한 건씩 말고 한꺼번에 — DataFrame .apply()

지금까지는 함수에 리뷰를 한 건씩 넣어 확인했습니다. 하지만 우리가 다룰 데이터는 수만 건이죠(1주차에 모은 사이버펑크 리뷰만 2.4만 건입니다). 이럴 때 for로 한 줄씩 도는 대신, 판다스의 .apply()로 열 전체에 함수를 한 번에 적용합니다.

import pandas as pd

df = pd.DataFrame({
    'review_id': ['r001', 'r002', 'r003'],
    'review_text': [
        "THIS IS SOOO GOOD!!! 😊",
        "게임 좋아요ㅋㅋㅋㅋㅋ 강추!!! https://my.link",
        "   bad    game     ",
    ],
})

# review_text 열의 모든 값에 clean_review 함수를 적용해 새 열로 저장
df['review_normalized'] = df['review_text'].apply(clean_review)
print(df[['review_id', 'review_normalized']])

언어 필터링도 같은 방식입니다. 8번에서 만든 is_korean을 .apply()로 열 전체에 적용해 True/False 마스크를 만들고, 그 마스크로 한국어 리뷰만 골라냅니다.

# is_korean을 모든 행에 적용 → 각 행이 한국어인지 True/False가 담긴 마스크
korean_mask = df['review_text'].apply(is_korean)

# 마스크가 True인 행만 남긴다 (.copy()는 원본과 분리된 사본을 만들어 경고를 피함)
korean_df = df[korean_mask].copy()
print(f"전체 {len(df)}건 중 한국어 {len(korean_df)}건")

.apply(함수)는 "이 함수를 각 행에 한 번씩 돌려라"라는 뜻입니다. 정제 함수만 잘 정의해 두면, 한 건이든 2만 건이든 코드는 이 한 줄로 끝납니다. 결과 건수는 데이터에 따라 달라집니다.

🔑 핵심 용어

용어 쉬운 설명
GIGO Garbage In, Garbage Out — 쓰레기를 넣으면 쓰레기가 나온다
독스트링(docstring) 함수가 무슨 일을 하는지 적는 """설명""". 코드보다 역할 정의가 먼저
정규화(normalization) 같은 의미의 여러 표기를 하나로 통일 (예: 소문자화)
.lower() / .strip() 소문자 변환 / 앞뒤 공백 제거 (파이썬 기본 메서드)
[^...] 정규식에서 "이걸 제외한 나머지"
\s+ / \S 연속된 공백 덩어리 / 공백이 아닌 글자(단어·URL 한 덩어리)
.apply(함수) 판다스에서 열의 모든 행에 함수를 한 번에 적용
임계값(threshold) 판단 기준선 (예: 한글 30% 이상이면 한국어)

🎮 게임 기획자 시점

"모든 분석에 통하는 단일 정제 파이프라인은 없다"는 말이 오늘의 핵심입니다. 이모지를 남길지(감성)·지울지(키워드), 사양 표기를 살릴지(성능)·버릴지(감성)를 정하는 건 결국 이 분석으로 무슨 의사결정을 할지 아는 사람의 몫이에요. 즉 정제 설계 자체가 기획 행위입니다. "+10 데미지" 같은 게임 특유의 표현을 노이즈로 지워 버리면 밸런스 관련 신호를 통째로 잃는다는 걸, 게임을 아는 사람만 알아챕니다.

⚠️ 흔한 오해 / 짚고 갈 점

함정 예시 무엇을 잃나
의미(부정어) 손실 NOT recommended → recommended 추천하지 않는다는 뜻이 정반대로 뒤집힘
감정(강조) 손실 AMAZING!!!! → amazing 얼마나 강하게 느꼈는지(뉘앙스)가 사라짐
고유명사 손실 GTA V → gta v 게임 이름·약어를 못 알아봄

특히 부정어 손실은 감성분석에서 치명적입니다. 불용어를 지운다고 not·안·못 같은 말을 무심코 날리면, 부정 리뷰가 긍정으로 둔갑할 수 있습니다. 그래서 정규화 수준은 "분석 목적에 맞게" 조절하는 것이 원칙입니다.

✅ 30초 정리

  1. 노이즈는 6가지 유형(반복·특수문자·이모지·공백·대소문자·외국어)으로 나뉩니다.
  2. 특수문자·이모지는 목적에 따라 남길지/지울지/변환할지 정합니다. "지울 것"보다 "남길 것"을 지정하세요.
  3. 정규화는 같은 의미를 하나로 통일(소문자·공백)하는 것. 그리고 단일 정답 파이프라인은 없습니다 — 목적·도메인이 설정을 정합니다.

▶️ 다음 강 연결

여기까지가 11차시 프로젝트 "내 게임 리뷰 정제하기"의 재료입니다(전사 없이 직접 실습). 깨끗해진 텍스트는 이제 AI에게 넘길 준비가 됐어요. 그런데 AI는 글자를 그대로 못 읽죠. 다음 3강에서 그 텍스트를 숫자로 바꾸는 임베딩을, 1주차 때보다 한 단계 깊은 전문 용어(벡터·차원·코사인 유사도)로 다시 봅니다.

전체📚 강의 목록