이미지 로딩 중...

텍스트를 벡터로 변환하기 완벽 가이드 - 슬라이드 1/9
A

AI Generated

2025. 11. 17. · 8 Views

텍스트를 벡터로 변환하기 완벽 가이드

컴퓨터가 텍스트를 이해하는 방법을 배워보세요. 단어를 숫자로 바꾸는 벡터화 기술부터 실전 활용까지, AI가 언어를 처리하는 핵심 원리를 초급자도 쉽게 이해할 수 있도록 설명합니다.


목차

  1. 텍스트 벡터화란 무엇인가 - 컴퓨터가 단어를 이해하는 방법
  2. TF-IDF 벡터화 - 중요한 단어를 찾아내는 기술
  3. Word2Vec 임베딩 - 단어의 의미를 벡터로 담다
  4. 문장 임베딩 - 문장 전체를 하나의 벡터로
  5. 원-핫 인코딩 - 가장 단순한 벡터화 방법
  6. 임베딩 레이어 - 신경망이 벡터를 학습하다
  7. BERT 임베딩 - 문맥을 이해하는 혁신적 벡터화
  8. 해시 벡터화 - 메모리 효율적인 변환 기법

1. 텍스트 벡터화란 무엇인가 - 컴퓨터가 단어를 이해하는 방법

시작하며

여러분이 챗봇을 만들거나 검색 엔진을 개발할 때 이런 상황을 겪어본 적 있나요? "컴퓨터는 어떻게 '사과'와 '배'가 비슷한 단어라는 걸 알까?" 하는 의문 말이에요.

사실 컴퓨터는 텍스트를 직접 이해할 수 없습니다. 컴퓨터는 오직 숫자만 처리할 수 있거든요.

그래서 "안녕하세요"라는 텍스트를 컴퓨터에게 그냥 주면, 컴퓨터는 이게 인사말인지, 질문인지, 아니면 명령어인지 전혀 알 수 없습니다. 바로 이럴 때 필요한 것이 텍스트 벡터화입니다.

이것은 마치 번역기처럼, 사람의 언어를 컴퓨터가 이해할 수 있는 숫자 언어로 바꿔주는 마법 같은 기술이에요. 이 변환 과정을 거치면 컴퓨터도 단어의 의미를 파악하고, 문장을 분석하고, 심지어 감정까지 이해할 수 있게 됩니다.

개요

간단히 말해서, 텍스트 벡터화는 단어나 문장을 숫자 배열(벡터)로 바꾸는 과정입니다. 예를 들어 "고양이"라는 단어를 [0.2, 0.8, 0.3, 0.5]처럼 숫자들의 리스트로 변환하는 것이죠.

왜 이런 변환이 필요할까요? AI 모델은 숫자로 된 데이터만 학습할 수 있기 때문입니다.

챗봇을 만들든, 스팸 메일 필터를 만들든, 감정 분석 프로그램을 만들든, 모든 AI 프로젝트는 텍스트를 숫자로 바꾸는 것에서 시작합니다. 예를 들어, 이메일이 스팸인지 판단하는 프로그램은 먼저 이메일 내용을 벡터로 변환한 후, 그 숫자 패턴을 분석해서 스팸 여부를 결정합니다.

기존에는 단어를 그냥 알파벳 순서대로 번호를 매겼다면, 이제는 단어의 의미까지 담은 똑똑한 숫자로 바꿀 수 있습니다. "강아지"와 "개"는 다른 단어지만, 벡터화하면 비슷한 숫자 패턴을 갖게 되어 컴퓨터도 이 둘이 관련 있다는 걸 알아챕니다.

텍스트 벡터화의 핵심 특징은 첫째, 의미를 보존한다는 것입니다. 비슷한 의미의 단어는 비슷한 벡터를 갖게 됩니다.

둘째, 수학적 연산이 가능합니다. "왕 - 남자 + 여자 = 여왕" 같은 단어 연산도 벡터로 표현하면 가능해집니다.

셋째, 차원 축소가 가능합니다. 수만 개의 단어를 몇백 개의 숫자로 압축할 수 있어 효율적입니다.

이러한 특징들이 현대 AI의 언어 이해 능력을 가능하게 만들었습니다.

코드 예제

# 가장 기본적인 텍스트 벡터화 예제
from sklearn.feature_extraction.text import CountVectorizer

# 벡터화할 문장들을 준비합니다
documents = [
    "나는 고양이를 좋아합니다",
    "나는 강아지를 좋아합니다",
    "고양이와 강아지는 귀엽습니다"
]

# CountVectorizer 객체를 생성합니다
vectorizer = CountVectorizer()

# 텍스트를 벡터로 변환합니다
X = vectorizer.fit_transform(documents)

# 어떤 단어들이 사용되었는지 확인
print("단어 목록:", vectorizer.get_feature_names_out())
# 결과: ['강아지는' '강아지를' '고양이를' '고양이와' '귀엽습니다' '나는' '좋아합니다']

# 벡터화된 결과를 확인합니다
print("벡터화 결과:\n", X.toarray())

설명

이것이 하는 일: 위 코드는 한글 문장들을 컴퓨터가 이해할 수 있는 숫자 형태로 변환합니다. 각 문장에 어떤 단어가 몇 번 나타났는지를 숫자로 표현하는 가장 기본적인 방법이죠.

첫 번째로, CountVectorizer 객체를 생성합니다. 이것은 마치 번역 사전처럼, 모든 단어를 숫자로 매핑하는 도구입니다.

왜 이렇게 하냐고요? 컴퓨터는 텍스트를 직접 계산할 수 없지만, 숫자는 더하고 빼고 비교할 수 있기 때문입니다.

그 다음으로, fit_transform() 메서드가 실행되면서 두 가지 일이 동시에 일어납니다. 먼저 'fit'은 모든 문장을 읽어서 어떤 단어들이 있는지 학습합니다.

그리고 'transform'은 각 문장을 실제로 숫자 벡터로 바꿉니다. 내부적으로는 각 단어가 각 문장에서 몇 번 등장하는지 세어서 표로 만듭니다.

마지막으로, toarray()를 호출하면 최종적으로 숫자 배열을 얻게 됩니다. 예를 들어 첫 번째 문장 "나는 고양이를 좋아합니다"는 [0, 0, 1, 0, 0, 1, 1]처럼 표현됩니다.

각 숫자는 단어 목록의 순서대로 해당 단어가 문장에 나타난 횟수를 의미합니다. 여러분이 이 코드를 사용하면 텍스트 데이터를 머신러닝 모델에 바로 입력할 수 있는 형태로 준비할 수 있습니다.

문장 간 유사도를 계산할 수 있고, 문서 분류도 가능하며, 검색 엔진도 만들 수 있습니다. 이것은 모든 자연어 처리 프로젝트의 첫 단계이자 가장 중요한 기초입니다.

실전 팁

💡 처음 시작할 때는 CountVectorizer로 시작하세요. 가장 이해하기 쉽고, 결과를 눈으로 직접 확인할 수 있어서 벡터화 개념을 익히기에 최적입니다.

💡 한글 텍스트를 벡터화할 때 흔한 실수는 띄어쓰기만으로 단어를 나누는 것입니다. "좋아합니다"와 "좋아해요"를 다른 단어로 인식하는 문제가 생기죠. 형태소 분석기(KoNLPy)를 사용하면 이런 문제를 해결할 수 있습니다.

💡 메모리 효율을 위해서는 sparse matrix 형태로 저장하세요. toarray()로 변환하면 메모리를 많이 차지하므로, 큰 데이터셋에서는 sparse 형태 그대로 사용하는 것이 좋습니다.

💡 벡터화 결과를 디버깅할 때는 항상 get_feature_names_out()으로 단어 목록을 먼저 확인하세요. 어떤 단어가 어떤 인덱스에 매핑되었는지 알아야 결과를 제대로 해석할 수 있습니다.

💡 실무에서는 max_features 매개변수를 사용해서 가장 자주 나오는 상위 N개의 단어만 선택하세요. 전체 단어를 다 사용하면 차원이 너무 커져서 모델이 느려지고 과적합이 발생할 수 있습니다.


2. TF-IDF 벡터화 - 중요한 단어를 찾아내는 기술

시작하며

여러분이 수천 개의 뉴스 기사에서 각 기사의 핵심 키워드를 자동으로 뽑아내야 한다면 어떻게 하시겠어요? "은", "는", "이", "가" 같은 조사들이 가장 많이 나오는데, 이것들이 기사의 핵심일 리는 없잖아요?

이런 문제는 검색 엔진, 문서 요약, 키워드 추출 등 거의 모든 텍스트 분석에서 발생합니다. 단순히 단어의 빈도수만 세면, 정작 중요한 단어는 묻히고 의미 없는 흔한 단어들만 높은 점수를 받게 됩니다.

"서울시가 발표한 새로운 정책"이라는 문장에서 정말 중요한 단어는 "서울시", "정책"이지 "가", "한" 같은 조사가 아니죠. 바로 이럴 때 필요한 것이 TF-IDF(Term Frequency-Inverse Document Frequency)입니다.

이것은 단어가 얼마나 자주 나오는지뿐만 아니라, 얼마나 희귀한지도 함께 고려하여 진짜 중요한 단어를 찾아냅니다.

개요

간단히 말해서, TF-IDF는 "이 단어가 이 문서에서는 많이 나오지만, 다른 문서에서는 잘 안 나오네? 그럼 이 문서의 특징을 나타내는 중요한 단어구나!"라고 판단하는 똑똑한 방법입니다.

왜 이 개념이 필요할까요? 모든 문서에 공통으로 나오는 단어는 문서를 구별하는 데 도움이 안 되기 때문입니다.

검색 엔진에서 "날씨"를 검색했을 때, "의", "를", "이"가 많이 나오는 문서가 아니라 "날씨", "기온", "강수량" 같은 특정 단어가 많은 문서를 보여줘야 하죠. 예를 들어, 이메일 분류 시스템을 만들 때 TF-IDF를 사용하면 스팸 메일에만 특징적으로 나타나는 단어들을 자동으로 찾아낼 수 있습니다.

기존 CountVectorizer가 단순히 단어 출현 횟수만 셌다면, TF-IDF는 여기에 "이 단어가 얼마나 희귀한가"라는 가중치를 곱해줍니다. 그래서 흔한 단어는 점수가 낮아지고, 특정 문서에만 나타나는 단어는 점수가 높아집니다.

TF-IDF의 핵심 특징은 첫째, TF(Term Frequency)로 문서 내 빈도를 측정합니다. 단어가 많이 나올수록 점수가 올라가요.

둘째, IDF(Inverse Document Frequency)로 희귀도를 측정합니다. 전체 문서 중 적은 문서에만 나타날수록 점수가 올라갑니다.

셋째, 이 둘을 곱해서 최종 점수를 만듭니다. 이렇게 하면 "자주 나오면서도 희귀한" 단어, 즉 문서의 핵심 단어를 정확히 찾아낼 수 있습니다.

코드 예제

# TF-IDF를 사용한 중요 단어 찾기
from sklearn.feature_extraction.text import TfidfVectorizer
import pandas as pd

# 분석할 문서들 (IT 관련 기사 예시)
documents = [
    "파이썬은 데이터 과학에서 가장 인기있는 언어입니다",
    "자바스크립트는 웹 개발의 필수 언어입니다",
    "파이썬과 자바스크립트 모두 훌륭한 언어입니다"
]

# TF-IDF 벡터화 객체 생성
tfidf = TfidfVectorizer()

# 벡터화 수행
tfidf_matrix = tfidf.fit_transform(documents)

# 결과를 보기 쉽게 DataFrame으로 변환
df = pd.DataFrame(
    tfidf_matrix.toarray(),
    columns=tfidf.get_feature_names_out()
)

print("TF-IDF 점수:")
print(df)
# 각 문서에서 어떤 단어가 중요한지 점수로 나타납니다
# 점수가 높을수록 해당 문서의 핵심 단어입니다

설명

이것이 하는 일: 이 코드는 여러 문서를 분석해서 각 문서를 가장 잘 나타내는 핵심 단어가 무엇인지 점수로 알려줍니다. 단순히 많이 나온다고 중요한 게 아니라, 다른 문서에는 없고 이 문서에만 특징적으로 나타나는 단어를 찾아냅니다.

첫 번째로, TfidfVectorizer 객체를 생성하는 부분은 CountVectorizer와 비슷해 보이지만, 내부적으로는 훨씬 복잡한 계산을 준비합니다. 이 객체는 각 단어가 몇 개의 문서에 나타나는지 기록하고, 이를 바탕으로 IDF 값을 계산할 준비를 합니다.

왜 이렇게 하냐면, "전체 문서 중 몇 퍼센트에 이 단어가 나오나?"를 알아야 희귀도를 계산할 수 있기 때문입니다. 그 다음으로, fit_transform()이 실행되면 두 단계의 계산이 일어납니다.

먼저 각 단어의 TF(문서 내 빈도)를 계산합니다. 예를 들어 "파이썬"이 첫 번째 문서에 1번 나왔다면 TF는 1/8(전체 단어 수)입니다.

그 다음 IDF를 계산합니다. "언어"는 3개 문서 모두에 나타나므로 IDF가 낮지만, "데이터"는 1개 문서에만 나타나므로 IDF가 높습니다.

최종 TF-IDF는 이 둘을 곱한 값입니다. 마지막으로, DataFrame으로 변환하면 각 문서의 각 단어에 대한 TF-IDF 점수를 한눈에 볼 수 있습니다.

첫 번째 문서에서는 "데이터", "과학", "파이썬" 같은 단어의 점수가 높게 나타날 것입니다. 왜냐하면 이 단어들은 첫 번째 문서에만 나타나거나, 다른 문서보다 더 자주 나타나기 때문입니다.

반면 "언어"는 모든 문서에 나타나므로 점수가 상대적으로 낮습니다. 여러분이 이 코드를 사용하면 문서 검색 엔진을 만들 수 있고, 자동 태그 생성 시스템을 구축할 수 있으며, 문서 분류의 정확도를 크게 높일 수 있습니다.

특히 비슷해 보이는 문서들을 구별해야 할 때, TF-IDF는 각 문서의 고유한 특성을 숫자로 명확하게 표현해줍니다. 실제로 많은 검색 엔진과 추천 시스템이 이 기술을 기반으로 동작합니다.

실전 팁

💡 TF-IDF를 사용할 때는 반드시 stop_words 매개변수를 설정하세요. "은", "는", "이", "가" 같은 불용어를 제거하면 훨씬 의미 있는 결과를 얻을 수 있습니다. 한글은 stop_words='english'가 아닌 별도의 한글 불용어 리스트가 필요합니다.

💡 문서의 길이가 서로 많이 다르다면 norm='l2' 옵션을 사용하세요(기본값). 이것은 벡터를 정규화해서 긴 문서와 짧은 문서를 공정하게 비교할 수 있게 해줍니다.

💡 min_df와 max_df 매개변수를 활용하세요. min_df=2는 "최소 2개 문서에 나타난 단어만 사용", max_df=0.8은 "80% 이상의 문서에 나타난 단어는 제외"를 의미합니다. 이렇게 하면 너무 희귀하거나 너무 흔한 단어를 자동으로 걸러낼 수 있습니다.

💡 실시간 검색 시스템을 만들 때는 fit()과 transform()을 분리하세요. 전체 문서 집합으로 한 번 fit()한 후, 새로운 쿼리나 문서는 transform()만 사용하면 일관된 벡터 공간에서 비교할 수 있습니다.

💡 TF-IDF 점수가 높은 상위 5-10개 단어를 뽑아내려면 각 행(문서)을 정렬하세요. 이것들이 바로 해당 문서의 자동 생성 태그나 키워드가 됩니다.


3. Word2Vec 임베딩 - 단어의 의미를 벡터로 담다

시작하며

여러분이 "왕 - 남자 + 여자 = ?"라는 수식을 단어로 계산할 수 있다면 믿으시겠어요? 그리고 그 답이 "여왕"이라고 나온다면요?

이것은 단순한 마법이 아닙니다. Word2Vec이라는 기술이 가능하게 만든 현실입니다.

기존의 CountVectorizer나 TF-IDF는 단어를 숫자로 바꿔주긴 하지만, 단어 간의 의미 관계는 전혀 고려하지 못했습니다. "강아지"와 "개"가 거의 같은 의미인지, "강아지"와 "비행기"가 전혀 다른 의미인지 컴퓨터는 알 수 없었죠.

바로 이럴 때 필요한 것이 Word2Vec(Word to Vector) 임베딩입니다. 이것은 단어를 벡터로 바꾸되, 비슷한 의미의 단어는 비슷한 벡터를, 다른 의미의 단어는 먼 벡터를 갖도록 학습합니다.

마치 3차원 공간에 단어들을 배치하는데, 의미가 비슷한 단어는 가까이, 다른 단어는 멀리 놓는 것처럼요.

개요

간단히 말해서, Word2Vec은 대량의 텍스트를 읽어서 "어떤 단어가 어떤 단어 옆에 자주 나오는지"를 학습합니다. 그리고 이 정보를 바탕으로 각 단어를 수백 차원의 벡터로 표현합니다.

왜 이 개념이 필요할까요? 의미가 비슷한 단어를 찾거나, 단어 간의 관계를 이해하거나, 문장의 뉘앙스를 파악하려면 단순한 빈도수만으로는 부족하기 때문입니다.

챗봇을 만들 때 사용자가 "개"라고 말했든 "강아지"라고 말했든 같은 의도로 이해해야 하고, 추천 시스템에서 "좋아요"와 "최고예요"를 비슷한 긍정적 반응으로 인식해야 합니다. 예를 들어, 상품 리뷰 분석 시스템에서 "훌륭함", "최고", "완벽" 같은 단어들이 유사한 벡터를 가지면, 새로운 긍정 단어를 만나도 자동으로 긍정으로 분류할 수 있습니다.

기존 방법들이 단어를 독립적인 심볼로 취급했다면, Word2Vec은 단어를 의미 공간상의 점으로 표현합니다. "커피"라는 단어는 "음료", "카페", "아침" 같은 단어들과 가까운 벡터를 갖게 되고, "컴퓨터"와는 먼 벡터를 갖게 됩니다.

Word2Vec의 핵심 특징은 첫째, 문맥을 학습한다는 것입니다. "같은 문맥에서 나타나는 단어는 비슷한 의미"라는 분포 가설을 이용합니다.

둘째, 벡터 연산이 의미 있습니다. "한국 - 서울 + 도쿄 = 일본" 같은 비유 추론이 가능해집니다.

셋째, 차원 축소가 자연스럽습니다. 수만 개의 단어를 보통 100-300차원 벡터로 압축하면서도 의미 정보를 잘 보존합니다.

이러한 특징들이 현대 자연어 처리의 혁명을 일으켰습니다.

코드 예제

# Word2Vec으로 단어 임베딩 학습하기
from gensim.models import Word2Vec

# 학습할 문장들 (토큰화된 형태로 준비)
sentences = [
    ['나는', '고양이를', '좋아합니다'],
    ['나는', '강아지를', '좋아합니다'],
    ['고양이는', '귀여운', '동물입니다'],
    ['강아지는', '귀여운', '동물입니다'],
    ['나는', '동물을', '사랑합니다']
]

# Word2Vec 모델 학습
# vector_size: 벡터 차원 수, window: 고려할 주변 단어 범위
# min_count: 최소 등장 횟수, sg: 0=CBOW, 1=Skip-gram
model = Word2Vec(sentences, vector_size=100, window=2,
                 min_count=1, sg=0, epochs=100)

# 특정 단어의 벡터 얻기
cat_vector = model.wv['고양이를']
print("'고양이를'의 벡터 (일부):", cat_vector[:5])

# 비슷한 단어 찾기
similar = model.wv.most_similar('고양이를', topn=3)
print("'고양이를'와 비슷한 단어:", similar)

설명

이것이 하는 일: 이 코드는 주어진 문장들을 읽고 분석해서, 각 단어가 어떤 맥락에서 사용되는지 학습합니다. 그리고 그 학습 결과를 바탕으로 각 단어를 100차원의 벡터로 표현합니다.

첫 번째로, 문장들을 토큰(단어) 리스트의 리스트로 준비합니다. Word2Vec은 "어떤 단어 옆에 어떤 단어가 자주 오는가"를 학습하기 때문에, 문장을 단어별로 쪼개놓아야 합니다.

예를 들어 "고양이를" 옆에는 "좋아합니다"가 자주 나오고, "귀여운" 옆에는 "동물입니다"가 자주 나온다는 패턴을 찾습니다. 왜 이렇게 하냐면, 같은 위치에 나타나는 단어들은 비슷한 역할을 한다고 가정하기 때문입니다.

그 다음으로, Word2Vec 모델이 학습을 시작합니다. vector_size=100은 각 단어를 100개의 숫자로 표현하겠다는 의미이고, window=2는 현재 단어의 앞뒤 2개 단어까지 문맥으로 고려한다는 뜻입니다.

sg=0은 CBOW(Continuous Bag of Words) 방식을 사용한다는 의미로, "주변 단어들로 중심 단어를 예측"하는 방식입니다. 모델은 여러 번(epochs=100) 문장들을 반복해서 읽으면서 점점 더 정확한 벡터를 학습합니다.

마지막으로, 학습이 끝나면 wv(word vectors) 객체를 통해 각 단어의 벡터를 가져올 수 있고, most_similar() 메서드로 비슷한 단어를 찾을 수 있습니다. 예를 들어 "고양이를"과 가장 비슷한 단어로 "강아지를"이 나올 것입니다.

왜냐하면 두 단어 모두 "좋아합니다"라는 단어 앞에 자주 나타나고, 비슷한 문맥에서 사용되기 때문입니다. 여러분이 이 코드를 사용하면 동의어 찾기, 단어 유사도 측정, 문장 의미 분석 등 다양한 작업을 할 수 있습니다.

검색 쿼리를 확장하거나("강아지" 검색 시 "개" 관련 문서도 표시), 추천 시스템을 개선하거나("액션" 영화 좋아하면 "스릴러"도 추천), 감정 분석의 정확도를 높이는 데 활용할 수 있습니다. 실제로 Google 검색, Netflix 추천, 챗봇 등 수많은 AI 서비스가 Word2Vec이나 이와 유사한 임베딩 기술을 사용합니다.

실전 팁

💡 충분한 데이터로 학습하세요. Word2Vec은 최소 수백만 개의 단어로 학습해야 의미 있는 결과를 얻을 수 있습니다. 작은 데이터셋에서는 사전 학습된 모델(GoogleNews-vectors, FastText 등)을 사용하는 것이 좋습니다.

💡 한글 텍스트는 반드시 형태소 분석을 하세요. "좋아합니다"를 그대로 사용하면 "좋아해요", "좋아한다"를 다른 단어로 인식합니다. KoNLPy로 "좋아하다"로 원형화하면 훨씬 좋은 결과를 얻을 수 있습니다.

💡 vector_size는 보통 100-300이 적절합니다. 너무 작으면 의미 정보가 손실되고, 너무 크면 과적합과 연산 비용 증가가 발생합니다. 데이터가 많을수록 큰 차원을 사용할 수 있습니다.

💡 CBOW(sg=0)는 빠르고 자주 나오는 단어에 강하며, Skip-gram(sg=1)은 느리지만 희귀 단어와 작은 데이터셋에 강합니다. 데이터가 충분하면 CBOW, 적으면 Skip-gram을 선택하세요.

💡 학습된 모델은 반드시 저장하세요. model.save('word2vec.model')로 저장하고 Word2Vec.load()로 불러오면 매번 학습할 필요가 없습니다. 학습에는 시간이 오래 걸리지만, 저장된 모델 로딩은 몇 초면 됩니다.


4. 문장 임베딩 - 문장 전체를 하나의 벡터로

시작하며

여러분이 고객 상담 챗봇을 만들 때 이런 문제를 겪어본 적 있나요? 사용자가 "환불하고 싶어요"라고 말했을 때와 "돈 돌려받을 수 있나요?"라고 말했을 때, 완전히 다른 단어를 사용했지만 의도는 같다는 걸 어떻게 알 수 있을까요?

단어 하나하나의 벡터를 구하는 것도 중요하지만, 실제로는 문장 전체의 의미를 이해해야 하는 경우가 훨씬 많습니다. 단어 벡터를 그냥 평균 내면 될까요?

아닙니다. "영화가 재미없지 않았다"와 "영화가 재미있었다"는 단어는 비슷하지만 의미는 정반대입니다.

단어 순서, 문법, 부정 표현 등 문장의 구조까지 고려해야 진짜 의미를 파악할 수 있습니다. 바로 이럴 때 필요한 것이 문장 임베딩입니다.

문장 전체를 하나의 벡터로 압축하되, 문장의 의미를 최대한 보존하는 기술이죠. 이것을 사용하면 두 문장이 얼마나 비슷한 의미인지, 문장의 감정이 긍정인지 부정인지, 어떤 카테고리에 속하는지 등을 판단할 수 있습니다.

개요

간단히 말해서, 문장 임베딩은 단어 몇 개부터 문단 전체까지, 길이에 상관없이 텍스트를 고정된 크기의 벡터로 변환하는 기술입니다. "안녕하세요"도, "오늘 날씨가 정말 좋네요"도, 모두 똑같이 512차원 벡터로 표현됩니다.

왜 이 개념이 필요할까요? 실제 애플리케이션에서는 단어가 아니라 문장이나 문서 단위로 작업하는 경우가 대부분이기 때문입니다.

FAQ 검색 시스템에서 사용자 질문과 가장 비슷한 FAQ를 찾거나, 뉴스 기사를 주제별로 자동 분류하거나, 채팅 메시지의 감정을 분석하려면 문장 전체를 하나의 단위로 처리해야 합니다. 예를 들어, "배송이 언제 되나요?"와 "주문한 제품 언제 받을 수 있어요?"는 완전히 다른 단어로 구성되어 있지만, 문장 임베딩으로 벡터화하면 매우 비슷한 벡터를 갖게 되어 같은 의도로 인식할 수 있습니다.

기존 Word2Vec이 단어를 벡터로 만들었다면, 문장 임베딩은 문장을 벡터로 만듭니다. 단순히 단어 벡터의 평균을 구하는 것이 아니라, BERT, Sentence-BERT 같은 딥러닝 모델이 문장의 구조와 문맥을 고려하여 훨씬 정교한 벡터를 생성합니다.

문장 임베딩의 핵심 특징은 첫째, 길이 독립성입니다. 3단어짜리 문장이든 100단어짜리 문장이든 같은 크기의 벡터로 변환됩니다.

둘째, 의미 보존성입니다. 비슷한 의미의 문장은 코사인 유사도가 높은 벡터를 갖게 됩니다.

셋째, 전이 학습 가능성입니다. 대규모 말뭉치로 사전 학습된 모델을 내 데이터에 바로 적용할 수 있어 적은 데이터로도 좋은 성능을 낼 수 있습니다.

이러한 특징들이 최신 AI 서비스의 핵심 기술이 되었습니다.

코드 예제

# Sentence Transformers로 문장 임베딩 생성
from sentence_transformers import SentenceTransformer, util

# 사전 학습된 한국어 문장 임베딩 모델 로드
model = SentenceTransformer('sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')

# 임베딩할 문장들
sentences = [
    "환불하고 싶어요",
    "돈 돌려받을 수 있나요?",
    "오늘 날씨가 좋네요"
]

# 문장들을 벡터로 변환 (각 문장 -> 384차원 벡터)
embeddings = model.encode(sentences)

print(f"문장 벡터 shape: {embeddings.shape}")  # (3, 384)

# 첫 번째와 두 번째 문장의 유사도 계산
similarity_1_2 = util.cos_sim(embeddings[0], embeddings[1])
print(f"'환불'과 '돈 돌려받기' 유사도: {similarity_1_2.item():.4f}")

# 첫 번째와 세 번째 문장의 유사도 계산
similarity_1_3 = util.cos_sim(embeddings[0], embeddings[2])
print(f"'환불'과 '날씨' 유사도: {similarity_1_3.item():.4f}")

설명

이것이 하는 일: 이 코드는 사전 학습된 딥러닝 모델을 사용해서 한국어 문장을 384개의 숫자로 이루어진 벡터로 변환합니다. 그리고 이 벡터들 간의 코사인 유사도를 계산하여 문장들이 얼마나 비슷한 의미인지 수치로 보여줍니다.

첫 번째로, SentenceTransformer 모델을 로드합니다. 이 모델은 이미 수억 개의 문장으로 학습된 상태이므로, 우리가 별도로 학습시킬 필요가 없습니다.

'paraphrase-multilingual-MiniLM-L12-v2'는 한국어를 포함한 50개 이상의 언어를 지원하며, 특히 같은 의미를 다르게 표현한 문장들(paraphrase)을 잘 찾아내도록 학습되었습니다. 왜 사전 학습된 모델을 사용하냐면, 우리가 직접 이런 모델을 학습시키려면 막대한 데이터와 컴퓨팅 자원이 필요하기 때문입니다.

그 다음으로, encode() 메서드가 실행되면 각 문장이 모델의 여러 레이어를 거쳐 처리됩니다. 모델은 문장의 각 단어를 읽고, 단어 간의 관계를 파악하고, 전체 문장의 의미를 이해한 후, 그 모든 정보를 384개의 숫자로 압축합니다.

"환불하고 싶어요"와 "돈 돌려받을 수 있나요?"는 다른 단어를 사용하지만, 모델은 둘 다 "환불"이라는 의도를 담고 있다는 것을 이해하고 비슷한 벡터를 만들어냅니다. 마지막으로, util.cos_sim()으로 코사인 유사도를 계산합니다.

이 값은 -1에서 1 사이인데, 1에 가까울수록 두 문장이 비슷한 의미라는 뜻입니다. "환불"과 "돈 돌려받기"는 0.8 이상의 높은 유사도를 보일 것이고, "환불"과 "날씨"는 0.2 이하의 낮은 유사도를 보일 것입니다.

이 숫자를 기준으로 FAQ 시스템에서 가장 적절한 답변을 찾거나, 중복 질문을 자동으로 병합하는 등의 작업을 할 수 있습니다. 여러분이 이 코드를 사용하면 문장 유사도 검색, 의미 기반 FAQ 시스템, 중복 콘텐츠 감지, 문서 클러스터링 등 다양한 애플리케이션을 만들 수 있습니다.

특히 사용자가 정확한 키워드를 입력하지 않아도 의미만 비슷하면 원하는 정보를 찾아주는 "의미 검색(Semantic Search)" 시스템을 구축할 수 있습니다. 실제로 Notion, Google Docs 등 많은 서비스가 이런 기술로 검색 기능을 개선하고 있습니다.

실전 팁

💡 처음 사용할 때는 'paraphrase-multilingual-MiniLM-L12-v2' 모델을 추천합니다. 크기도 작고(420MB) 속도도 빠르며 한국어 성능도 준수합니다. 더 높은 성능이 필요하면 'paraphrase-multilingual-mpnet-base-v2'를 사용하세요.

💡 유사도 임계값을 실험으로 찾으세요. 일반적으로 0.7 이상이면 비슷한 의미, 0.5-0.7은 관련 있음, 0.5 이하는 다른 의미로 볼 수 있지만, 도메인에 따라 다르므로 직접 테스트해보는 것이 중요합니다.

💡 대량의 문장을 처리할 때는 배치 처리를 사용하세요. model.encode([문장1, 문장2, ...], batch_size=32)처럼 하면 한 번에 여러 문장을 처리하여 GPU를 효율적으로 활용할 수 있습니다.

💡 벡터를 데이터베이스에 저장할 때는 FAISS나 Pinecone 같은 벡터 DB를 사용하세요. 수백만 개의 문장 벡터 중에서 가장 유사한 것을 찾는 작업을 밀리초 단위로 수행할 수 있습니다.

💡 한국어 성능을 더 높이려면 KR-SBERT 같은 한국어 전용 모델을 사용하거나, 여러분의 도메인 데이터로 파인튜닝하세요. 일반 모델은 법률, 의료 등 전문 분야에서는 성능이 떨어질 수 있습니다.


5. 원-핫 인코딩 - 가장 단순한 벡터화 방법

시작하며

여러분이 처음으로 텍스트를 숫자로 바꿔야 하는데, 복잡한 수학이나 딥러닝은 모르겠고, 가장 간단하고 직관적인 방법이 필요하다면 어떻게 하시겠어요? 원-핫 인코딩(One-Hot Encoding)은 텍스트 벡터화의 가장 기초적인 방법입니다.

"사과", "바나나", "오렌지" 세 단어가 있다면, 사과는 [1, 0, 0], 바나나는 [0, 1, 0], 오렌지는 [0, 0, 1]로 표현하는 방식이죠. 마치 스위치처럼 해당 단어 위치만 1로 켜고 나머지는 0으로 끄는 것입니다.

바로 이것이 원-핫 인코딩입니다. 가장 단순하지만, 텍스트 벡터화의 원리를 이해하기에 최적입니다.

많은 고급 기법들도 결국 이 원리에서 시작되었습니다.

개요

간단히 말해서, 원-핫 인코딩은 전체 어휘 사전의 크기만큼 긴 벡터를 만들고, 해당 단어의 위치만 1, 나머지는 모두 0으로 채우는 방식입니다. 만약 어휘가 10,000개라면 각 단어는 10,000차원 벡터가 됩니다.

왜 이 개념이 필요할까요? 머신러닝 모델은 카테고리 데이터를 직접 처리할 수 없기 때문입니다.

"사과"를 1번, "바나나"를 2번으로 숫자를 매기면 안 될까요? 안 됩니다.

그러면 모델이 "바나나(2)가 사과(1)보다 크다"는 잘못된 순서 관계를 학습하게 됩니다. 예를 들어, 과일 분류 모델을 만들 때 "사과", "바나나", "오렌지"를 각각 독립적인 카테고리로 표현해야 하는데, 원-핫 인코딩을 사용하면 이들 사이에 순서나 크기 관계가 없다는 것을 명확히 표현할 수 있습니다.

기존에 단어를 1, 2, 3... 같은 정수로 인코딩했다면, 원-핫 인코딩은 각 단어를 독립적인 차원으로 표현합니다.

이렇게 하면 어떤 단어도 다른 단어보다 "크거나" "작지" 않고, 모두 동등하게 취급됩니다. 원-핫 인코딩의 핵심 특징은 첫째, 완벽한 독립성입니다.

각 단어가 자기만의 차원을 갖기 때문에 서로 영향을 주지 않습니다. 둘째, 희소성(sparsity)입니다.

10,000개 중 1개만 1이고 나머지는 모두 0이므로, 메모리는 많이 차지하지만 대부분이 0입니다. 셋째, 의미 정보 부재입니다.

"강아지"와 "개"가 비슷한 의미라는 정보는 전혀 담기지 않습니다. 이러한 특징들 때문에 원-핫 인코딩은 작은 어휘 집합이나 카테고리 데이터에 주로 사용됩니다.

코드 예제

# 원-핫 인코딩 직접 구현하기
import numpy as np

# 어휘 사전 구축
vocab = ["사과", "바나나", "오렌지"]
vocab_size = len(vocab)

# 단어를 인덱스로 매핑
word_to_idx = {word: idx for idx, word in enumerate(vocab)}
print("단어-인덱스 매핑:", word_to_idx)

# 원-핫 인코딩 함수
def one_hot_encode(word, vocab_size, word_to_idx):
    # 모두 0으로 초기화된 벡터 생성
    vector = np.zeros(vocab_size)
    # 해당 단어의 위치만 1로 설정
    if word in word_to_idx:
        vector[word_to_idx[word]] = 1
    return vector

# 각 단어를 원-핫 인코딩
for word in vocab:
    encoded = one_hot_encode(word, vocab_size, word_to_idx)
    print(f"{word}: {encoded}")

# 문장 인코딩 (각 단어를 원-핫으로 변환 후 평균)
sentence = ["사과", "바나나"]
sentence_vectors = [one_hot_encode(w, vocab_size, word_to_idx) for w in sentence]
sentence_vector = np.mean(sentence_vectors, axis=0)
print(f"문장 '사과 바나나' 벡터: {sentence_vector}")

설명

이것이 하는 일: 이 코드는 각 단어를 벡터로 변환하되, 어휘 사전의 크기만큼 긴 벡터에서 해당 단어의 위치만 1로 표시하고 나머지는 모두 0으로 채웁니다. 마치 전구 여러 개 중 하나만 켜는 것과 같습니다.

첫 번째로, 어휘 사전(vocabulary)을 만들고 각 단어에 고유 번호를 부여합니다. "사과"는 0번, "바나나"는 1번, "오렌지"는 2번이 됩니다.

이 매핑은 나중에 단어를 벡터로 바꿀 때 "몇 번째 위치를 1로 만들지" 결정하는 데 사용됩니다. 왜 이런 매핑이 필요하냐면, 컴퓨터는 "사과"라는 텍스트를 직접 처리할 수 없고, 숫자 인덱스로 작업해야 하기 때문입니다.

그 다음으로, one_hot_encode() 함수가 실제 변환을 수행합니다. 먼저 모든 원소가 0인 벡터를 만듭니다(np.zeros).

그리고 해당 단어의 인덱스 위치만 1로 바꿉니다. 예를 들어 "바나나"는 인덱스 1이므로, [0, 0, 0] 벡터의 1번 위치를 1로 만들어 [0, 1, 0]이 됩니다.

이 과정은 마치 3개의 전구 중 2번째 전구만 켜는 것과 같습니다. 마지막으로, 문장을 인코딩할 때는 각 단어를 원-핫 벡터로 만든 후 평균을 냅니다.

"사과 바나나"는 [1, 0, 0]과 [0, 1, 0]의 평균인 [0.5, 0.5, 0]이 됩니다. 이렇게 하면 문장에 어떤 단어들이 포함되어 있는지 표현할 수 있습니다.

물론 이 방법은 단어 순서 정보를 잃어버리지만, 간단한 문서 분류나 감정 분석에는 충분히 사용할 수 있습니다. 여러분이 이 코드를 사용하면 작은 규모의 텍스트 분류 문제를 빠르게 해결할 수 있습니다.

특히 카테고리가 명확하고 어휘 크기가 작을 때(수백 개 이하) 유용합니다. 예를 들어 감정 분류(긍정/부정/중립), 스팸 필터(스팸/정상), 언어 감지(한국어/영어/일본어) 같은 간단한 작업에 적합합니다.

다만 어휘가 수만 개 이상으로 커지면 메모리 문제가 발생하므로, 그때는 TF-IDF나 임베딩 방식을 사용해야 합니다.

실전 팁

💡 어휘 크기가 1,000개를 넘어가면 원-핫 인코딩보다 TF-IDF나 임베딩을 고려하세요. 원-핫은 희소 벡터를 만들어 메모리와 연산량이 급격히 증가합니다.

💡 Keras나 scikit-learn의 원-핫 인코더를 사용하면 위 코드를 직접 작성할 필요가 없습니다. keras.utils.to_categorical()이나 sklearn.preprocessing.OneHotEncoder()를 활용하세요.

💡 순서가 중요하지 않은 카테고리 데이터(성별, 지역, 상품 종류 등)에는 원-핫 인코딩이 최적입니다. 순서가 있는 데이터(학년, 등급 등)는 ordinal encoding을 사용하세요.

💡 문장을 인코딩할 때 평균 대신 합(sum)을 사용할 수도 있습니다. 평균은 문장 길이를 정규화하고, 합은 단어 빈도를 보존합니다. 작업에 따라 선택하세요.

💡 미등록 단어(OOV, Out-Of-Vocabulary)를 처리하려면 "<UNK>" 같은 특수 토큰을 어휘에 추가하세요. 학습 때 보지 못한 새 단어는 모두 이 토큰으로 처리하면 됩니다.


6. 임베딩 레이어 - 신경망이 벡터를 학습하다

시작하며

여러분이 딥러닝 모델을 만들 때 이런 고민을 해본 적 있나요? "Word2Vec으로 미리 만든 벡터를 사용할까, 아니면 모델이 직접 학습하게 할까?" 사전 학습된 임베딩(Word2Vec, GloVe 등)은 범용적이지만, 여러분의 특정 작업에 최적화되어 있지 않을 수 있습니다.

예를 들어 의료 문서 분류 모델을 만드는데, 일반 뉴스로 학습된 Word2Vec을 사용하면 "처방", "진단" 같은 의료 용어의 의미를 제대로 표현하지 못할 수 있습니다. 바로 이럴 때 필요한 것이 임베딩 레이어(Embedding Layer)입니다.

이것은 신경망의 첫 번째 레이어로, 모델이 학습하는 동안 입력 데이터와 작업에 맞춰서 최적의 단어 벡터를 자동으로 학습합니다. 마치 모델이 자기만의 단어 사전을 만들어가는 것처럼요.

개요

간단히 말해서, 임베딩 레이어는 정수로 인코딩된 단어를 입력받아 고정 크기의 밀집 벡터(dense vector)로 변환하는 신경망 레이어입니다. 이 벡터는 학습 과정에서 역전파를 통해 계속 업데이트됩니다.

왜 이 개념이 필요할까요? 원-핫 인코딩은 벡터가 너무 희소하고 크며, 사전 학습된 임베딩은 내 데이터에 맞지 않을 수 있기 때문입니다.

임베딩 레이어를 사용하면 모델이 학습 데이터의 패턴에 맞춰 단어 벡터를 최적화합니다. 예를 들어, 영화 리뷰 감정 분석 모델을 만들 때 임베딩 레이어를 사용하면, "재미없다", "지루하다", "아쉽다" 같은 부정 단어들이 자동으로 비슷한 벡터를 갖게 되어 모델의 성능이 향상됩니다.

기존 방법들이 미리 정해진 벡터를 사용했다면, 임베딩 레이어는 학습 가능한 벡터를 사용합니다. 마치 신경망의 가중치처럼, 단어 벡터도 학습 데이터에 맞춰 조정됩니다.

임베딩 레이어의 핵심 특징은 첫째, 학습 가능성입니다. 벡터가 고정되어 있지 않고 학습 과정에서 업데이트됩니다.

둘째, 차원 축소입니다. 어휘 크기가 10,000이어도 보통 100-300차원으로 압축하여 효율적입니다.

셋째, 종단간 학습(end-to-end)입니다. 임베딩과 분류기가 함께 학습되어 전체 시스템이 최적화됩니다.

이러한 특징들이 현대 NLP 딥러닝의 표준이 되었습니다.

코드 예제

# Keras로 임베딩 레이어 사용하기
from tensorflow import keras
import numpy as np

# 가상의 문장 데이터 (정수 인코딩 완료)
# 예: [[1, 2, 3], [4, 5]] = [["나는", "영화가", "좋다"], ["이", "영화"]]
X_train = np.array([
    [1, 2, 3, 0],  # 문장 1 (패딩 포함)
    [4, 5, 2, 0],  # 문장 2
    [1, 5, 3, 0]   # 문장 3
])
y_train = np.array([1, 0, 1])  # 레이블 (긍정=1, 부정=0)

# 모델 구축
vocab_size = 100  # 전체 어휘 크기
embedding_dim = 16  # 임베딩 벡터 차원

model = keras.Sequential([
    # 임베딩 레이어: 각 단어를 16차원 벡터로 변환
    keras.layers.Embedding(input_dim=vocab_size,
                          output_dim=embedding_dim,
                          input_length=4),
    # 평균 풀링: 문장 벡터 생성
    keras.layers.GlobalAveragePooling1D(),
    # 출력 레이어: 이진 분류
    keras.layers.Dense(1, activation='sigmoid')
])

model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
model.fit(X_train, y_train, epochs=10, verbose=0)

# 학습된 임베딩 추출
embeddings = model.layers[0].get_weights()[0]
print(f"임베딩 shape: {embeddings.shape}")  # (100, 16)
print(f"단어 1번의 벡터: {embeddings[1][:5]}...")  # 학습된 벡터

설명

이것이 하는 일: 이 코드는 감정 분류 작업을 수행하면서 동시에 각 단어의 최적 벡터 표현을 학습합니다. 모델은 "어떤 단어 벡터를 사용해야 분류가 잘 되는가?"를 스스로 찾아냅니다.

첫 번째로, 임베딩 레이어를 정의합니다. input_dim=100은 어휘가 100개라는 의미이고, output_dim=16은 각 단어를 16개의 숫자로 표현하겠다는 의미입니다.

input_length=4는 모든 문장을 4단어로 맞췄다는 뜻입니다(짧은 문장은 0으로 패딩). 이 레이어 내부에는 100x16 크기의 행렬이 있고, 각 행이 한 단어의 벡터입니다.

왜 이런 구조를 사용하냐면, 정수 인덱스를 받아서 해당 행을 찾아오는 것이 매우 빠르고 효율적이기 때문입니다. 그 다음으로, 모델이 학습하는 동안 임베딩 벡터가 업데이트됩니다.

예를 들어 "좋다"(단어 3번)가 나오는 문장이 긍정으로 분류되어야 하는데 부정으로 예측됐다면, 역전파 과정에서 "좋다"의 벡터가 긍정 방향으로 조금씩 이동합니다. 반대로 "싫다"는 부정 방향으로 이동하겠죠.

이 과정이 수천 번 반복되면서, 비슷한 의미의 단어들이 자연스럽게 비슷한 벡터를 갖게 됩니다. 마지막으로, 학습이 끝난 후 get_weights()로 학습된 임베딩 행렬을 추출할 수 있습니다.

이것은 100x16 크기의 행렬인데, 각 행이 학습된 단어 벡터입니다. 이 벡터들은 여러분의 감정 분류 작업에 최적화되어 있습니다.

일반적인 Word2Vec보다 이 특정 작업에서는 더 좋은 성능을 낼 가능성이 높습니다. 여러분이 이 코드를 사용하면 텍스트 분류, 감정 분석, 스팸 필터, 챗봇 의도 파악 등 다양한 NLP 작업을 딥러닝으로 해결할 수 있습니다.

특히 도메인 특화된 데이터(의료, 법률, 금융 등)나 사전 학습된 임베딩이 없는 저자원 언어에서 매우 유용합니다. 임베딩 레이어는 LSTM, GRU, Transformer 같은 모든 시퀀스 모델의 첫 레이어로 사용됩니다.

실전 팁

💡 임베딩 차원은 보통 어휘 크기의 4제곱근 정도가 적당합니다. 어휘가 10,000개면 sqrt(sqrt(10000)) ≈ 10, 즉 128차원 정도가 좋습니다. 너무 작으면 정보 손실, 너무 크면 과적합이 발생합니다.

💡 데이터가 적다면 사전 학습된 임베딩(GloVe, FastText)을 로드한 후 trainable=False로 고정하거나, 처음엔 고정했다가 나중에 미세 조정(fine-tuning)하는 전략을 사용하세요.

💡 임베딩 레이어 다음에는 Dropout을 추가하세요. keras.layers.SpatialDropout1D(0.2)를 사용하면 과적합을 방지하면서도 단어 벡터의 구조를 유지할 수 있습니다.

💡 학습된 임베딩을 시각화하려면 t-SNE나 PCA로 2차원으로 축소한 후 플롯하세요. 비슷한 의미의 단어들이 가까이 모여 있는지 확인하면 임베딩 품질을 평가할 수 있습니다.

💡 pad_sequences()로 문장 길이를 맞출 때 padding='post'를 사용하세요. 문장 뒤에 패딩을 추가하는 것이 앞에 추가하는 것보다 RNN 계열 모델에서 성능이 좋습니다.


7. BERT 임베딩 - 문맥을 이해하는 혁신적 벡터화

시작하며

여러분이 "그는 은행에 갔다"라는 문장을 읽을 때, "은행"이 금융 기관인지 강가인지 어떻게 아시나요? 바로 앞뒤 문맥을 보고 판단하시죠.

기존의 Word2Vec이나 임베딩 레이어는 "은행"이라는 단어를 항상 같은 벡터로 표현했습니다. 금융 은행이든 강변 은행이든 똑같은 벡터를 사용했죠.

이것은 동음이의어, 다의어가 많은 언어에서 큰 문제입니다. "배가 고프다"의 "배"와 "배를 타다"의 "배"는 완전히 다른 의미인데 같은 벡터를 쓰면 혼란스럽겠죠?

바로 이럴 때 필요한 것이 BERT(Bidirectional Encoder Representations from Transformers)입니다. 이것은 같은 단어라도 문맥에 따라 다른 벡터를 생성합니다.

"그는 은행에 갔다"에서는 금융 은행 벡터를, "강 은행에 앉았다"에서는 강변 은행 벡터를 만들어냅니다.

개요

간단히 말해서, BERT는 문장 전체를 읽고 각 단어의 문맥을 파악한 후, 그 문맥이 반영된 벡터를 생성하는 딥러닝 모델입니다. 같은 단어도 문장에 따라 다른 벡터를 갖게 됩니다.

왜 이 개념이 필요할까요? 자연어는 문맥 의존적이기 때문입니다.

"사과"가 과일인지 사과하는 행위인지는 "맛있는 사과"인지 "진심으로 사과"인지에 따라 달라집니다. 검색 엔진에서 "애플 먹었어"와 "애플 샀어"를 구별하려면, "애플"이 과일인지 회사 제품인지 문맥으로 판단해야 합니다.

예를 들어, Q&A 시스템에서 "이 제품 배터리 오래 가나요?"라는 질문의 "배터리"는 기술 용어이지만, "내 배터리가 방전됐어"의 "배터리"는 은유적 표현일 수 있습니다. BERT는 이런 차이를 문맥으로 구분합니다.

기존 임베딩이 단어마다 하나의 고정 벡터를 가졌다면, BERT는 문장마다 다른 벡터를 생성합니다. 양방향으로 문장을 읽어서 앞뒤 문맥을 모두 고려합니다.

BERT의 핵심 특징은 첫째, 양방향 문맥 이해입니다. 왼쪽에서 오른쪽만 보는 것이 아니라 양쪽 모두를 봅니다.

둘째, 사전 학습 + 파인튜닝 구조입니다. 대규모 텍스트로 미리 학습한 후 특정 작업에 맞춰 미세 조정합니다.

셋째, Transformer 아키텍처 기반입니다. Self-Attention 메커니즘으로 단어 간 관계를 효과적으로 모델링합니다.

이러한 특징들이 2018년 NLP 분야에 혁명을 일으켰고, GPT, RoBERTa 등 수많은 후속 모델의 기초가 되었습니다.

코드 예제

# transformers 라이브러리로 BERT 임베딩 생성
from transformers import BertTokenizer, BertModel
import torch

# 한국어 BERT 모델과 토크나이저 로드
model_name = 'bert-base-multilingual-cased'
tokenizer = BertTokenizer.from_pretrained(model_name)
model = BertModel.from_pretrained(model_name)

# 서로 다른 문맥의 문장들
sentences = [
    "그는 은행에 돈을 입금했다",      # 금융 은행
    "우리는 강 은행에 앉아 있었다"    # 강변
]

# 각 문장을 BERT로 인코딩
for sentence in sentences:
    # 토큰화 및 인코딩
    inputs = tokenizer(sentence, return_tensors='pt',
                      padding=True, truncation=True)

    # BERT 모델 실행 (문맥 벡터 생성)
    with torch.no_grad():
        outputs = model(**inputs)

    # 마지막 은닉층의 벡터 추출
    last_hidden_states = outputs.last_hidden_state  # (1, seq_len, 768)

    # "은행" 토큰의 벡터 찾기
    tokens = tokenizer.convert_ids_to_tokens(inputs['input_ids'][0])
    print(f"\n문장: {sentence}")
    print(f"토큰들: {tokens}")
    print(f"벡터 shape: {last_hidden_states.shape}")

설명

이것이 하는 일: 이 코드는 사전 학습된 BERT 모델을 사용해서 문장을 입력받아, 각 단어의 문맥을 고려한 768차원 벡터를 생성합니다. 동일한 "은행"이라는 단어도 문장에 따라 서로 다른 벡터를 갖게 됩니다.

첫 번째로, BERT 모델과 토크나이저를 로드합니다. 'bert-base-multilingual-cased'는 104개 언어로 학습된 다국어 모델이므로 한국어도 처리할 수 있습니다.

토크나이저는 문장을 BERT가 이해할 수 있는 토큰으로 쪼개는 역할을 하고, 모델은 실제 벡터를 생성하는 역할을 합니다. 왜 사전 학습된 모델을 사용하냐면, BERT를 처음부터 학습시키려면 수백 GB의 텍스트와 수백만 원의 GPU 비용이 필요하기 때문입니다.

그 다음으로, 토크나이저가 문장을 처리합니다. "그는 은행에 돈을 입금했다"를 ['[CLS]', '그는', '은', '##행', '##에', '돈', '##을', '입', '##금', '##했', '##다', '[SEP]'] 같은 토큰으로 나누고, 각 토큰을 정수 ID로 변환합니다.

[CLS]는 문장 시작, [SEP]는 문장 끝을 나타내는 특수 토큰입니다. ##는 하위 단어(subword)를 나타냅니다.

마지막으로, BERT 모델이 실행되면서 각 토큰의 문맥 벡터를 생성합니다. 모델은 Self-Attention 메커니즘을 통해 "은행" 토큰이 "돈", "입금"과 함께 나타났다는 것을 파악하고, 금융 은행에 가까운 벡터를 만듭니다.

반대로 두 번째 문장에서는 "강", "앉아"와 함께 나타났으므로 강변 은행에 가까운 벡터를 만듭니다. last_hidden_state는 (배치 크기, 시퀀스 길이, 768) 형태의 텐서로, 각 토큰의 768차원 벡터를 담고 있습니다.

여러분이 이 코드를 사용하면 문맥을 고려한 텍스트 검색, 정교한 감정 분석, 질문-답변 시스템, 문서 요약, 번역 등 거의 모든 고급 NLP 작업을 수행할 수 있습니다. 특히 동음이의어나 다의어가 많거나, 문맥이 중요한 도메인(법률, 의료, 금융)에서 기존 방법보다 훨씬 높은 정확도를 보입니다.

실제로 Google 검색, 네이버 스마트렌즈 등 많은 서비스가 BERT 기반 기술을 사용합니다.

실전 팁

💡 처음 시작할 때는 'bert-base-multilingual-cased' 대신 'klue/bert-base' 같은 한국어 전용 모델을 사용하세요. 한국어 데이터로만 학습되어 한국어 성능이 훨씬 뛰어납니다.

💡 BERT는 메모리와 연산량이 많으므로, 실시간 서비스에서는 DistilBERT나 ALBERT 같은 경량 모델을 고려하세요. 성능은 약간 낮지만 속도는 2-3배 빠릅니다.

💡 문장 벡터를 얻으려면 [CLS] 토큰의 벡터를 사용하거나, 모든 토큰 벡터의 평균을 구하세요. [CLS]는 문장 전체를 대표하도록 학습되었지만, 평균이 더 나은 경우도 있으므로 실험해보세요.

💡 특정 도메인 데이터로 파인튜닝하면 성능이 크게 향상됩니다. HuggingFace의 Trainer API를 사용하면 몇 줄의 코드로 쉽게 파인튜닝할 수 있습니다.

💡 GPU가 없다면 transformers의 pipeline API를 사용하세요. 'feature-extraction' 파이프라인은 BERT 임베딩을 간단하게 추출할 수 있고, CPU에서도 비교적 빠르게 동작합니다.


8. 해시 벡터화 - 메모리 효율적인 변환 기법

시작하며

여러분이 실시간 스트리밍 데이터를 처리하는 시스템을 만들 때 이런 문제를 겪어본 적 있나요? 새로운 단어가 계속 들어오는데, 어휘 사전을 매번 업데이트하고 다시 학습시킬 수도 없고...

일반적인 벡터화 방법들은 모두 어휘 사전을 먼저 만들어야 합니다. CountVectorizer든 TF-IDF든, 전체 데이터를 한 번 읽어서 어떤 단어들이 있는지 파악(fit)한 후에야 변환(transform)할 수 있죠.

하지만 트위터 실시간 분석, 뉴스 크롤링, 사용자 리뷰 스트림 같은 경우, 새로운 단어가 끊임없이 생겨나고, 미리 어휘를 알 수 없습니다. 바로 이럴 때 필요한 것이 해시 벡터화(Hashing Vectorizer)입니다.

이것은 어휘 사전 없이, 해시 함수만으로 텍스트를 즉시 벡터로 변환합니다. 마치 단어를 해시테이블에 넣듯이, 단어를 받으면 바로 벡터의 특정 위치에 매핑합니다.

개요

간단히 말해서, 해시 벡터화는 단어를 해시 함수에 넣어서 나온 해시값을 벡터 인덱스로 사용하는 방법입니다. "사과"라는 단어가 들어오면 hash("사과") % 벡터크기 = 1234 같은 식으로 즉시 위치를 결정합니다.

왜 이 개념이 필요할까요? 첫째, 메모리 효율성입니다.

수백만 개의 단어를 저장할 어휘 사전이 필요 없습니다. 둘째, 확장성입니다.

학습 때 못 본 새로운 단어도 즉시 처리할 수 있습니다. 셋째, 속도입니다.

fit 단계 없이 바로 transform할 수 있어 실시간 처리에 유리합니다. 예를 들어, 실시간 스팸 필터에서 해커가 새로운 스팸 단어를 만들어도, 해시 벡터화는 모델 재학습 없이 바로 처리할 수 있습니다.

기존 방법들이 "단어 -> 사전에서 찾기 -> 인덱스 -> 벡터"였다면, 해시 벡터화는 "단어 -> 해시 -> 벡터"로 직행합니다. 사전 찾기 단계를 건너뛰는 거죠.

해시 벡터화의 핵심 특징은 첫째, 고정 크기 벡터입니다. 어휘가 10개든 100만 개든 미리 정한 크기(예: 2^18 = 262,144)의 벡터를 사용합니다.

둘째, 해시 충돌 가능성입니다. 서로 다른 단어가 같은 해시값을 가질 수 있지만, 벡터 크기가 충분하면 성능 저하가 크지 않습니다.

셋째, 비가역성입니다. 벡터에서 원래 단어를 복원할 수 없습니다(get_feature_names() 불가).

이러한 특징들이 대규모 온라인 학습 시스템에서 매우 유용합니다.

코드 예제

# HashingVectorizer로 메모리 효율적인 벡터화
from sklearn.feature_extraction.text import HashingVectorizer
import numpy as np

# 해싱 벡터화 객체 생성
# n_features: 벡터 크기 (2의 거듭제곱 권장)
# alternate_sign: 해시 충돌 영향을 줄이는 옵션
vectorizer = HashingVectorizer(n_features=2**10,  # 1024 차원
                               alternate_sign=False)

# 첫 번째 배치 데이터
batch1 = [
    "파이썬은 훌륭한 언어입니다",
    "자바스크립트도 좋은 언어입니다"
]

# fit 없이 바로 transform (어휘 사전 불필요!)
X1 = vectorizer.transform(batch1)
print(f"첫 번째 배치 shape: {X1.shape}")  # (2, 1024)

# 새로운 배치 (처음 보는 단어 포함)
batch2 = [
    "러스트는 안전한 언어입니다",  # 새 단어: 러스트, 안전한
    "파이썬은 여전히 인기있습니다"  # 새 단어: 여전히, 인기있습니다
]

# 같은 vectorizer로 즉시 변환 (재학습 불필요!)
X2 = vectorizer.transform(batch2)
print(f"두 번째 배치 shape: {X2.shape}")  # (2, 1024)

print("\n장점: fit 단계 없이 즉시 변환, 메모리 효율적")
print("단점: 어떤 단어가 어느 위치에 매핑되었는지 알 수 없음")

설명

이것이 하는 일: 이 코드는 어휘 사전을 만들지 않고, 단어를 해시 함수에 넣어서 나온 숫자를 벡터 인덱스로 사용합니다. 새로운 단어가 나타나도 즉시 처리할 수 있어 실시간 스트리밍 데이터에 적합합니다.

첫 번째로, HashingVectorizer를 생성할 때 n_features로 벡터 크기를 정합니다. 1024차원 벡터를 사용한다는 의미입니다.

이 크기는 어휘 크기와 무관하게 미리 고정됩니다. alternate_sign=False는 단순 카운트를 사용한다는 의미입니다(True면 +1/-1을 번갈아 사용해 충돌 영향을 줄임).

왜 2의 거듭제곱을 사용하냐면, 해시값을 벡터 인덱스로 변환할 때 나머지 연산(%) 대신 비트 연산(&)을 사용할 수 있어 더 빠르기 때문입니다. 그 다음으로, transform()이 실행되면 각 단어가 해시 함수를 거쳐 0부터 1023 사이의 숫자로 변환됩니다.

예를 들어 "파이썬"은 hash("파이썬") % 1024 = 482번 위치에, "언어"는 715번 위치에 매핑될 수 있습니다. 이 과정은 fit 단계 없이 즉시 일어나므로, 첫 번째 배치든 백만 번째 배치든 똑같은 속도로 처리됩니다.

기존 방법처럼 "이 단어가 사전에 있나?"를 검색할 필요가 없습니다. 마지막으로, 새로운 배치에 처음 보는 단어("러스트", "안전한")가 나타나도 문제없이 처리됩니다.

해시 함수는 어떤 입력이든 받아서 0-1023 범위의 숫자로 바꿔주기 때문입니다. 다만 해시 충돌이 발생할 수 있습니다.

예를 들어 "파이썬"과 "러스트"가 우연히 같은 482번 위치에 매핑될 수 있습니다. 하지만 1024개 위치 중 2개만 충돌하므로, 전체적인 벡터 표현에는 큰 영향을 주지 않습니다.

벡터 크기를 더 크게(2^18 = 262,144 등) 하면 충돌 확률이 더 줄어듭니다. 여러분이 이 코드를 사용하면 온라인 학습 시스템, 실시간 텍스트 분류, 대규모 웹 크롤링 분석, 스트리밍 데이터 처리 등에서 메모리와 속도 면에서 큰 이점을 얻을 수 있습니다.

특히 어휘가 계속 변하거나 크기가 예측 불가능한 경우(소셜 미디어, 신조어, 다국어 혼용 등) 매우 유용합니다. 단, 해시 충돌과 비가역성 때문에 해석 가능성이 중요한 작업(특징 중요도 분석, 키워드 추출)에는 적합하지 않습니다.

실전 팁

💡 n_features는 예상 어휘 크기의 2-10배 정도로 설정하세요. 어휘가 10,000개 예상이면 2^14 (16,384)나 2^16 (65,536) 정도가 적절합니다. 충돌을 줄이면서도 메모리를 아낄 수 있습니다.

💡 alternate_sign=True를 사용하면 해시 충돌의 영향을 줄일 수 있습니다. 충돌된 단어들의 값이 +1/-1로 상쇄되어 노이즈가 감소합니다. 일반적으로 True 사용을 권장합니다.

💡 온라인 학습(SGDClassifier, PassiveAggressiveClassifier 등)과 함께 사용하면 시너지가 큽니다. 두 기법 모두 fit 없이 즉시 처리 가능하므로, 무한 스트림 데이터를 처리할 수 있습니다.

💡 성능을 TF-IDF와 비교 실험하세요. 데이터가 정적이고 메모리가 충분하면 TF-IDF가 더 나을 수 있지만, 동적이고 대규모면 해시가 유리합니다. A/B 테스트로 결정하세요.

💡 해시 벡터화는 특징 이름을 알 수 없으므로, 모델 해석이 필요한 작업(LIME, SHAP 등)에는 사용하지 마세요. 규제 준수나 설명 가능한 AI가 요구되면 CountVectorizer나 TF-IDF를 사용하세요.


#AI#텍스트벡터화#임베딩#자연어처리#머신러닝

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.