SK플래닛 ai활용 데이터엔지니어 과정 2기/ML & DL

DL 5 - NLP 실습

dev-lee 2026. 5. 27. 21:18

1. 학습 목표

NLP 처리 과정 전반을 코드로 직접 다루며 토큰화 워크플로우를 이해하고, 이를 활용해 유사도 기반 챗봇 프로토타입을 구성하는 것이 목표임. LLM API 내부에서 일어나는 토큰화 흐름을 직접 구현해보는 데 의의가 있다.

1.1 다루는 범위

  • 토큰화 워크플로우 — 분절화 → 사전화 → 벡터화 → 패딩 → 임베딩
  • 유사도 기반 검색 — 코사인 유사도로 가장 가까운 질문 찾기
  • 챗봇 프로토타입 — Gradio로 시뮬레이션, SBERT로 토크나이저 교체 비교

1.2 챗봇 동작 흐름

  • 사용자 질문 입력 — 자연어 텍스트
  • 질문 토큰화 — 벡터로 변환
  • 유사도 검사 — 사전 구축된 챗봇 시트 질문 벡터들과 비교
  • 매칭 답변 반환 — 거리가 가장 가까운 질문에 페어로 묶인 답변 응답

NLP 워크플로우는 결국 "자연어를 숫자로 바꿔서 비교 가능하게 만드는 과정"임. LLM 제품에서 API로 추상화되어 있지만, 내부에서는 토크나이저가 분절·사전화·벡터화·패딩을 수행하고, 모델은 이를 임베딩으로 받아 처리한다.


2. 말뭉치 획득

한국어 NLP 학습에 쓸 수 있는 공개 말뭉치는 여러 곳에서 제공됨. 본 실습에서는 `Korpora` 라이브러리를 통해 네이버 영화 리뷰(NSMC) 데이터를 사용한다.

2.1 한국어 말뭉치 제공처

출처  특징
AI Hub 정부 주도 대규모 한국어 데이터셋
모두의 말뭉치 국립국어원 제공 표준 말뭉치
언어정보 나눔터 다양한 한국어 자료 통합 제공
Korpora 오픈소스 한국어 코퍼스 통합 로더

2.2 Korpora로 NSMC 로드

from Korpora import Korpora

corpus = Korpora.load('nsmc')  # 네이버 영화리뷰
set(corpus.get_all_labels())   # {0, 1} - 부정/긍정
len(corpus.train), len(corpus.test)  # (150000, 50000)
학습 15만 건, 테스트 5만 건의 이진 분류 데이터셋. 본 실습에서는 분류가 아닌 사전 구축용 말뭉치로 활용함.

NSMC는 한국어 NLP 입문용 표준 데이터셋. 본 실습에서는 정답 라벨(긍정/부정)이 아니라 문장 자체를 사전 구축 재료로만 쓴다.


3. 데이터 정제

DataFrame으로 변환 후 결측·중복을 제거함. 챗봇 사전의 토큰 다양성을 확보하면서도 노이즈를 줄이는 단계.

3.1 결측·중복 처리

train_df = pd.DataFrame({
    "sentence": corpus.train.get_all_texts(),
    "label": corpus.train.get_all_labels()
})

train_df.dropna(inplace=True)
train_df.drop_duplicates(subset=['sentence'], inplace=True)
train_df.shape  # (146183, 2)
15만 건 → 146,183건으로 약 3,800건이 중복으로 제거됨. 라벨별 분포는 0(부정) 73,342, 1(긍정) 72,841로 거의 균형 잡힘.

사전 구축 단계에서는 중복 문장이 토큰 빈도 통계를 왜곡할 수 있어 제거가 필수. 분류 모델이 아니라 토큰 사전을 만드는 게 목적이라도 마찬가지다.


4. 토큰화 워크플로우

토큰화는 단일 작업이 아니라 5단계 파이프라인. 각 단계가 명확히 분리돼 있으며, 어느 한 단계의 선택이 다음 단계에 영향을 미친다.

4.1 전체 흐름

  • 분절화 — 문장을 토큰 단위로 쪼갬 (형태소 / 공백 / 서브워드)
  • 사전화 — 각 토큰에 고유 인덱스 부여
  • 벡터화 — 문장을 인덱스 시퀀스로 변환
  • 패딩 — 길이를 통일
  • 임베딩 — 인덱스를 밀집 벡터로 변환 (신경망 입력)
"짙게 배여든 허무주의" → ['짙게','배여든','허무주의'] → [343, 55, 43] → [343,55,43,0,0,...] → [[0.1,...],[0.4,...],...]

4.2 분절화 방식 비교

방식  대상 언어  도구
공백 기반 알파벳 문자 (영어 등) Keras Tokenizer
형태소 기반 한국어 등 교착어 KoNLPy (Okt, Mecab, Komoran)
서브워드 다국어 LLM BPE, WordPiece, SentencePiece

4.3 형태소 분절 (KoNLPy Okt)

from konlpy.tag import Okt
tokenizer = Okt()
tokenizer.morphs(train_df.sentence[99])
# ['설정','이','재밌고','새로운','에피소드','내','에서','메인','스토리','도','차차','나오는게','재밌음']
한국어는 조사·어미가 발달한 교착어라 단순 공백 분리로는 의미 단위를 잡기 어려움. 형태소 분석기가 "설정/이", "에피소드/내/에서"처럼 의미 단위로 끊어줌.

4.4 공백 분절 (Keras Tokenizer)

from tensorflow.keras.preprocessing.text import Tokenizer

nlp_tokenizer = Tokenizer()
nlp_tokenizer.fit_on_texts(train_df.sentence)  # 분절 + 사전화 동시 수행

LLM은 대부분 공백·서브워드 기반 분절을 쓴다. 형태소 분석이 한국어에 더 정교해 보이지만, 대규모 데이터로 학습한 공백 기반 LLM이 맥락 이해에서 더 우수한 경우가 많다. 양으로 정밀도를 극복한 셈.


5. 사전화와 벡터화

분절된 토큰에 인덱스를 부여하고(사전화), 문장을 인덱스 시퀀스로 바꾼다(벡터화). Keras Tokenizer는 `fit_on_texts` 한 줄로 두 단계를 동시에 처리함.

5.1 사전 구조 확인

nlp_tokenizer.document_count   # 146183 (학습한 문장 수)
len(nlp_tokenizer.index_word)  # 296310 (등록된 토큰 수)

# 빈도 상위 토큰 (낮은 인덱스 = 자주 등장)
# {1:'영화', 2:'너무', 3:'정말', 4:'진짜', 5:'이', 6:'그냥', ...}
인덱스는 빈도순으로 부여됨. 1번이 가장 자주 등장한 토큰이며, 0은 패딩용으로 예약됨.

5.2 문장 → 벡터 변환

nlp_tokenizer.texts_to_sequences(['아 더빙.. 진짜 짜증나네요 목소리'])
# [[23, 924, 4, 6703, 1085]]

nlp_tokenizer.index_word[924]  # '더빙'
"아"는 23번, "더빙"은 924번. 각 토큰이 사전 인덱스로 치환됨.

사전화는 본질적으로 "단어 → 숫자" 매핑 테이블 구축이다. 모델은 단어를 모르고 숫자만 안다. 이 매핑 테이블이 곧 모델의 어휘 한계이며, 사전에 없는 토큰(OOV)은 처리할 수 없다.


6. 패딩

벡터화된 문장은 길이가 제각각임. 신경망 입력은 고정 크기여야 하므로 길이를 통일해야 한다. 정보 보존과 메모리 사이의 트레이드오프가 핵심.

6.1 패딩 전략

방식  장점  단점
최대 길이 패딩 정보 손실 없음 메모리 낭비
고정 길이 패딩 메모리 효율 정보 손실 가능, 청크 전략 필요

6.2 최대 길이 패딩 적용

from tensorflow.keras.preprocessing.sequence import pad_sequences

x = nlp_tokenizer.texts_to_sequences(train_df.sentence)
maxlen = max(len(i) for i in x)  # 59

x_padding = pad_sequences(x, maxlen+1, padding='post')
x_padding.shape  # (146183, 60)

x_padding[0]
# [23, 924, 4, 6703, 1085, 0, 0, 0, ..., 0]
  • padding='post' — 뒤쪽을 0으로 채움 (앞쪽 채우려면 'pre')
  • 0 — 패딩용 예약값, 사전에서 사용하지 않음
  • maxlen+1 — 여유분 1칸 확보

패딩 길이가 너무 길면 메모리 낭비, 너무 짧으면 문장 잘림. RAG 시스템에서 청크(chunk) 단위를 어떻게 잡을지가 핵심 고민거리가 되는 이유다. 자동 청킹·문장 단위 청킹 등 전략이 갈린다.


7. 임베딩 단계의 의미

7.1 벡터화와 임베딩의 차이

  • 벡터화 — 정수 인덱스 시퀀스 (예: [23, 924, 4])
  • 임베딩 — 각 인덱스를 밀집 실수 벡터로 변환 (예: [[0.1,0.4,...], [0.7,-0.2,...]])

7.2 LLM 제품에서의 토큰화 흐름

  • 입력 흐름 — 프롬프트 → tokenizer.encode(text) → 벡터 → LLM
  • 출력 흐름 — LLM → 벡터 → tokenizer.decode(vec) → 응답 텍스트
  • 저장 — 벡터 DB에 임베딩 보관 (Milvus, Qdrant, Chroma, Pinecone)
  • 활용 — RAG에서 유사도 검색으로 LLM이 학습하지 않은 정보를 참조시킬 수 있음

토큰화·벡터화·임베딩은 LLM API에 추상화되어 한 번의 호출로 끝나지만, 데이터 파이프라인 관점에서는 명확히 분리된 단계. 텍스트 수집 → 벡터화 → 벡터 DB 적재 → RAG 검색 흐름은 데이터 엔지니어가 책임지는 영역이다.


8. 유사도 기반 챗봇 구성

문장 간 유사도는 벡터 간 거리로 측정. 사용자 질문 벡터와 가장 가까운 챗봇 시트 질문을 찾아 페어로 묶인 답변을 반환한다.

8.1 유사도 종류

  • 코사인 유사도 — 벡터 방향성 중시, 내용 유사성에 강함
  • 유클리드 유사도 — 벡터 간 절대 거리 중시

8.2 코사인 유사도 직접 구현

from sklearn.metrics.pairwise import cosine_similarity
import numpy as np

def cos_sim(a_vec, b_vec):
    # 두 벡터의 내적 / 두 벡터의 노름 곱
    return np.dot(a_vec, b_vec) / (np.linalg.norm(a_vec) * np.linalg.norm(b_vec))
공식은 단순하지만 실무에서는 `sklearn`의 `cosine_similarity`를 그대로 쓰는 게 일반적. 배치 처리·NaN 안정성 등이 검증돼 있다.

8.3 챗봇 동작 흐름

  • 1. 질문 입력 — 사용자가 자연어 문장 입력
  • 2. 벡터화 — 사용자 질문을 토크나이저로 인코딩
  • 3. 유사도 검사 — 챗봇 시트의 모든 질문 벡터와 코사인 유사도 계산
  • 4. 최고 점수 선택 — score.idxmax()로 가장 가까운 질문 찾기
  • 5. 응답 반환 — 해당 질문의 페어 답변 출력

1등만 뽑으면 결정적(deterministic) 응답, 상위 N개 중 랜덤이면 창의적 응답. LLM의 temperature 파라미터(0.0 ~ 1.0)가 정확히 이 개념. 0이면 항상 1등, 높을수록 변동성이 커진다.


9. 챗봇 구현

9.1 인코딩 함수와 챗봇 시트 로드

def custom_encode(s):
    x = nlp_tokenizer.texts_to_sequences([s])     # 벡터화
    x = pad_sequences(x, maxlen+1, padding='post') # 패딩
    return x

chatbot_df = pd.read_csv('ChatbotData.csv')
# Q: 12시 땡!         A: 하루가 또 가네요.   label: 0
# Q: 1지망 학교 떨어졌어  A: 위로해 드립니다.    label: 0

9.2 사전 확장의 필요성

네이버 영화 리뷰 기반 사전(296,310개)으로 챗봇 시트 질문을 인코딩하면 OOV가 빈번하게 발생함. 챗봇 시트의 질문·답변까지 사전에 추가 학습시켜야 한다.
# 챗봇 시트 데이터까지 추가 학습
nlp_tokenizer.fit_on_texts(chatbot_df.Q)
nlp_tokenizer.fit_on_texts(chatbot_df.A)
len(nlp_tokenizer.index_word)  # 306,938

# 확장된 사전으로 재인코딩
chatbot_df['vec'] = chatbot_df.Q.apply(lambda x: custom_encode(x))
  • 사전 추가 학습 — fit_on_texts는 누적 학습 가능, 기존 사전 유지하며 신규 토큰 추가
  • 재인코딩 필수 — 사전이 바뀌면 인덱스도 변하므로 기존 벡터는 무효화됨

9.3 응답 생성 함수

def answer_make_similar(q=''):
    if not q:
        return '질문이 없습니다.'
    user_vec = custom_encode(q)
    chatbot_df['score'] = chatbot_df.vec.apply(
        lambda x: cosine_similarity(user_vec, x)[0][0]
    )
    return chatbot_df.loc[chatbot_df.score.idxmax()]['A']

answer_make_similar('오늘 비가 안 오네?')
# → '진짜 나빴네요.'
실행 결과 매칭된 질문은 "내 지인한테 내 험담했대" (유사도 0.9997). 의미적으로는 무관한데 토큰 패턴이 우연히 일치한 케이스로, 단순 인덱스 기반 유사도의 한계를 보여준다.

코사인 유사도 0.9997이 나왔다고 의미가 가까운 것은 아니다. Keras Tokenizer 기반 벡터는 단순 인덱스 시퀀스라 의미 정보를 담지 못한다. "오늘 비가 안 오네?"와 "내 지인한테 내 험담했대"가 거의 같다고 판정된 이유다.

9.4 Gradio로 챗봇 인터페이스 구성

import gradio

def qna(input_text, history):
    return answer_make_similar(input_text)

gradio.ChatInterface(qna, type='messages').launch()
`ChatInterface` 한 줄로 웹 챗봇 UI가 자동 생성됨. Colab 환경에서는 `share=True`가 자동 적용되어 공개 URL이 발급된다.

10. 사전 학습 모델로 토크나이저 교체

Keras Tokenizer는 인덱스 기반이라 의미 유사도를 못 잡음. 사전 학습된 SBERT 계열 모델로 교체하면 의미 기반 벡터를 얻을 수 있다.

10.1 사용 모델

  • 모델명 — sentence-transformers/xlm-r-100langs-bert-base-nli-stsb-mean-tokens
  • 기반 — BERT 계열, 100개 언어 지원
  • 파인튜닝 — 카카오브레인 KorNLI·KorSTS 데이터로 한국어 추가 학습
  • 출력 차원 — 768 (BERT base hidden size)

10.2 모델 로드와 인코딩

from sentence_transformers import SentenceTransformer

model = SentenceTransformer(
    'sentence-transformers/xlm-r-100langs-bert-base-nli-stsb-mean-tokens'
)

vec = model.encode('오늘 비가 안 오네?')
vec.shape  # (768,)
`model.encode()` 한 번이 분절→사전화→벡터화→임베딩 전 과정을 대체함. 결과는 768차원 밀집 벡터로, 의미가 가까운 문장은 벡터도 가까워진다.

10.3 챗봇 시트 재인코딩과 응답 함수

# SBERT로 벡터 컬럼 교체
chatbot_df['vec'] = chatbot_df.Q.apply(lambda x: model.encode(x))

def answer_make_similar_ex(q=''):
    if not q:
        return '질문이 없습니다.'
    user_vec = model.encode(q)
    chatbot_df['score'] = chatbot_df.vec.apply(
        lambda x: cosine_similarity([user_vec], [x])[0][0]
    )
    return chatbot_df.loc[chatbot_df.score.idxmax()]['A']
  • 인코딩 한 줄 — 토큰화·패딩 전부 모델 내부에서 처리
  • 벡터 차원 고정 — 모든 입력이 동일한 768차원으로 임베딩됨
  • 의미 기반 검색 — 토큰 패턴이 달라도 의미가 가까우면 유사도 높음

10.4 두 방식 비교

항목  Keras Tokenizer SBERT
벡터 종류 정수 인덱스 시퀀스 768차원 밀집 벡터
의미 반영 없음 (단순 빈도 인덱스) 있음 (대규모 학습된 의미 공간)
OOV 처리 처리 불가 서브워드로 대응
사전 구축 직접 fit_on_texts 필요 사전 학습 완료, 별도 학습 불필요
유사도 품질 토큰 패턴 일치만 잡힘 문장 의미가 유사하면 잡힘

같은 챗봇 시트와 같은 코사인 유사도 함수를 써도 토크나이저(인코더)만 바꿔서 결과가 완전히 달라진다. 이게 사전 학습 모델의 힘이다. 의미를 모르는 인덱스 사전 대신, 대규모 학습으로 의미 공간이 형성된 임베딩 모델을 쓰는 것.


11. 요약

  • NLP 워크플로우 — 분절화 → 사전화 → 벡터화 → 패딩 → 임베딩의 5단계 파이프라인, LLM API에서는 한 번의 encode 호출로 추상화됨
  • 분절화 방식 — 한국어는 형태소(KoNLPy)가 정밀하지만, LLM은 공백·서브워드 기반으로 양으로 정밀도를 극복
  • 사전화의 한계 — 학습 말뭉치에 없는 토큰은 OOV가 되므로 챗봇 시트까지 포함해 추가 학습이 필요함
  • 유사도 검색 원리 — 벡터 간 코사인 거리로 의미 유사도 판정, LLM의 temperature는 상위 N개 중 어떻게 뽑을지의 개념
  • Keras Tokenizer의 한계 — 인덱스 기반 벡터는 의미를 못 담아서, 패턴이 우연히 일치한 무관한 문장이 매칭되는 문제 발생
  • 사전 학습 모델 교체 — SBERT(xlm-r-100langs)는 다국어 의미 공간에 학습된 768차원 임베딩 제공, 토크나이저 한 줄 교체로 응답 품질 향상
단계  Keras 기반 SBERT 기반
인코딩 분절·사전·패딩 직접 수행 model.encode() 한 줄
벡터 의미 인덱스 시퀀스 의미 임베딩
사전 관리 직접 확장 필요 사전 학습 완료
유사도 품질 우연한 패턴 일치 의미 기반 매칭

자연어를 다룬다는 것은 결국 "텍스트를 어떤 벡터 공간에 어떻게 배치할 것인가"의 문제. 인덱스 시퀀스로 배치하면 의미가 사라지고, 사전 학습된 임베딩 공간에 배치하면 의미가 보존된다. RAG·벡터 DB·LLM이 전부 같은 원리를 공유하며, 데이터 엔지니어가 책임지는 영역은 이 임베딩을 어떻게 수집·저장·검색 가능하게 만들 것인가다. 토크나이저 하나만 교체해도 결과가 달라진다는 사실이 이 흐름 전체의 본질을 압축해서 보여준다.