멀티캠퍼스 데이터분석 6월 9일 수업 내용 — FastText 이론 및 실습, L2 정규화, 상품 추천 시스템, Vectorizer 클래스 확장(Word2Vec+FastText), gensim_test 모듈화, GridSearchCV 연동
FastText
1. FastText 이론 — OOV 문제 해결
2. FastText vs Word2Vec 실습 비교
3. FastText 상품 추천 시스템
Vectorizer 클래스 확장
'강아지' 와 '강아지들' 을 완전히 다른 단어로 인식'강아지' → '강', '아', '지', '강아', '아지', '강아지' (n-gram 방식)| 매개변수 | 기본값 | 설명 |
|---|---|---|
min_n | 3 | 최소 subword 길이 |
max_n | 6 | 최대 subword 길이 |
bucket | 2e+6 | 단어 사전 최대 크기 제한 |
💡
min_n=0, max_n=0으로 설정하면 subword 사용 금지 → Word2Vec 방식과 동일
min_n은 반드시max_n보다 작거나 같아야 합니다.
from gensim.models import Word2Vec, FastText
sentences = [
['이커머스', '데이터', '분석', '진행'],
['상품', '리뷰', '감성', '분석', '합니다'],
['형태소', '단위', '임베딩', '가능']
]
model_wv = Word2Vec(
sentences=sentences, window=3, vector_size=50,
min_count=1, sg=1, epochs=10
)
model_ft = FastText(
sentences=sentences, window=3, vector_size=50,
min_count=1, sg=1, epochs=50,
min_n=2, max_n=4
)
# 단어 벡터 확인
model_wv.wv['이커머스']
model_ft.wv['이커머스']
# 유사 단어 검색
model_wv.wv.most_similar('데이터', topn=3)
model_ft.wv.most_similar('데이터', topn=3)
# Word2Vec — 사전에 없는 단어 → 에러 발생
# model_wv.wv['감정'] ← KeyError 발생
# FastText — 사전에 없는 단어도 subword로 벡터 생성 가능
model_ft.wv['감정']
model_ft.wv.most_similar('감정', topn=3)
sentences2 = [
['고양이', '고양이들', '귀엽다', '동물', '반려동물'],
['강아지', '강아지들', '귀엽다', '동물', '반려동물'],
['달리다', '달리는', '달림', '걷다', '걷는'],
['예쁘다', '예쁨', '예쁜', '매력적이다'],
['컴퓨터', '컴퓨팅', '컴퓨터들', '기계']
]
w2v = Word2Vec(sentences=sentences2, window=3, min_count=1, sg=1, seed=42)
ft = FastText(sentences=sentences2, window=3, min_count=1, sg=1, seed=42, min_n=2)
# 형태소 변형 단어 간 유사도 비교
test_text = [
['강아지', '강아지들'],
['고양이', '고양이들'],
['달리다', '달리는'],
['예쁘다', '예쁜'],
['컴퓨터', '컴퓨팅'],
['달리다', '걷다']
]
for word1, word2 in test_text:
w2v_sim = w2v.wv.similarity(word1, word2)
ft_sim = ft.wv.similarity(word1, word2)
print(f'{word1}과 {word2} 유사도 → Word2Vec({round(w2v_sim,4)}) / FastText({round(ft_sim,4)})')
💡 FastText는
'강아지'와'강아지들'을 더 높은 유사도로 계산합니다.
두 단어가 공통 subword('강아지')를 공유하기 때문입니다.
from konlpy.tag import Komoran
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
products = {
'P001': '무선 이어폰 블루투스 노이즈캔슬링 충전케이스',
'P002': '유선 이어폰 하이파이 금도금 플러그',
'P003': '게이밍 마우스 RGB 경량 디자인',
'P004': '무선 마우스 초경량 블루투스 듀얼모드',
'P005': '헤드폰 노이즈캔슬링 유선'
}
data = products.values()
komoran = Komoran()
tokens_split = [name.split() for name in data]
tokens_komoran = [komoran.morphs(name) for name in data]
# FastText 학습 (공백 기준 토큰화 사용)
ft2 = FastText(
sentences=tokens_split, window=3, min_count=1,
sg=1, min_n=2, max_n=4, seed=42
)
def sent_vector(token, type=None):
"""
단어 벡터 평균 계산 (FastText는 미등록 단어도 처리 가능)
type='l2' 시 L2 정규화 적용
"""
vectors = [ft2.wv[word] for word in token] # FastText는 모든 단어 처리 가능
if not vectors:
return np.zeros(ft2.vector_size)
v = np.mean(vectors, axis=0)
if type == 'l2':
# L2 정규화 — 벡터의 크기(norm)로 나눠 방향만 남김
v = v / (np.linalg.norm(v) + 1e-12)
return v
# 일반 벡터
items_vector = [sent_vector(token) for token in tokens_split]
# L2 정규화 벡터
items_vector_l2 = [sent_vector(token, 'l2') for token in tokens_split]
💡 L2 정규화(L2 Normalization)란?
벡터를 자신의 크기(norm) 로 나눠 길이가 1인 단위 벡터로 변환합니다.
코사인 유사도 계산 시 벡터 크기의 영향을 제거하여 방향만으로 유사도를 비교할 수 있습니다.
np.linalg.norm(v)로 L2 norm 계산,1e-12는 분모 0 방지용입니다.
def recommand_by_text(product_id):
"""특정 상품 ID와 유사한 상품 목록 반환"""
# list.index() — 특정 값의 위치(인덱스) 반환
idx = list(products.keys()).index(product_id)
sims = cosine_similarity([items_vector[idx]], items_vector).ravel()
order = sims.argsort()[::-1]
result = []
for i in order:
if sims[i] != 1.0: # 자기 자신 제외 (유사도=1.0)
result.append([list(products.values())[i], round(sims[i], 4)])
return result
recommand_by_text('P003')
def recommand_by_search(text):
"""검색어와 유사한 상품 상위 3개 반환 (L2 정규화 사용)"""
vector = ft2.wv[text]
v = vector / (np.linalg.norm(vector) + 1e-12) # 검색어도 L2 정규화
sims = cosine_similarity([v], items_vector_l2).ravel()
order = sims.argsort()[::-1]
result = []
for i in order[:3]:
result.append([list(products.values())[i], round(sims[i], 4)])
return result
print(recommand_by_search('노이즈캔슬링'))
기존 Word2VecVectorizer 에 FastText, L2 정규화, type 선택 기능을 추가합니다.
import numpy as np
from gensim.models import Word2Vec, FastText
from sklearn.base import BaseEstimator, TransformerMixin
class Vectorizer(BaseEstimator, TransformerMixin):
def __init__(self, tokenizer=None, vector_size=100, min_count=5,
window=5, sg=1, epochs=100, workers=1, seed=42,
min_n=2, max_n=4, bucket=2e+6,
type='w2v', # 'w2v': Word2Vec / 'ft': FastText
l2=False): # L2 정규화 적용 여부
self.tokenizer = tokenizer
self.vector_size = vector_size
self.min_count = min_count
self.window = window
self.sg = sg
self.epochs = epochs
self.workers = workers
self.seed = seed
self.min_n = min_n
self.max_n = max_n
self.bucket = bucket
self.type = type
self.l2 = l2
self.model = None
self.voca = None
def to_token(self, sentences):
if self.tokenizer is None:
return [sentence.split() for sentence in sentences]
return [self.tokenizer(sentence) for sentence in sentences]
def fit(self, X, y=None):
sentences = self.to_token(X)
if self.type == 'w2v':
self.model = Word2Vec(
sentences=sentences, vector_size=self.vector_size,
window=self.window, min_count=self.min_count, sg=self.sg,
epochs=self.epochs, workers=self.workers, seed=self.seed
)
elif self.type == 'ft':
self.model = FastText(
sentences=sentences, vector_size=self.vector_size,
window=self.window, min_count=self.min_count, sg=self.sg,
epochs=self.epochs, workers=self.workers, seed=self.seed,
min_n=self.min_n, max_n=self.max_n, bucket=self.bucket
)
self.voca = self.model.wv.key_to_index.keys()
return self
def doc_vec(self, token):
vectors = []
for word in token:
if self.type == 'w2v':
# Word2Vec은 사전에 없는 단어 제외
if word in self.model.wv.index_to_key:
vectors.append(self.model.wv[word])
elif self.type == 'ft':
# FastText는 미등록 단어도 subword로 처리
vectors.append(self.model.wv[word])
if vectors:
result = np.mean(vectors, axis=0)
if self.l2:
result = result / (np.linalg.norm(result) + 1e-12)
else:
result = np.zeros(self.model.vector_size)
return result
def transform(self, X):
sentences = self.to_token(X)
return np.array([self.doc_vec(token) for token in sentences])
| 항목 | Word2VecVectorizer | Vectorizer |
|---|---|---|
| 지원 모델 | Word2Vec만 | Word2Vec + FastText |
type 매개변수 | 없음 | 'w2v' / 'ft' 선택 가능 |
| L2 정규화 | 없음 | l2=True/False |
| FastText 전용 매개변수 | 없음 | min_n, max_n, bucket |
| OOV 처리 | 제외 | FastText 사용 시 처리 가능 |
위 Vectorizer 클래스를 gensim_test.py 로 저장하고 모듈로 불러옵니다.
from sklearn.pipeline import Pipeline
from sklearn.model_selection import GridSearchCV, StratifiedKFold
from sklearn.metrics import classification_report
from sklearn.svm import SVC
from konlpy.tag import Okt
import pandas as pd
from gensim_test import Vectorizer
df = pd.read_csv('../data/ratings_test.txt', sep='\t')
# 결측치 제거
df.dropna(inplace=True)
# 좌우 공백 제거
df['document'] = df['document'].str.strip()
# 빈 문자열 제거
df = df.loc[~(df['document'] == '')]
# 중복 document 제거
df.drop_duplicates('document', inplace=True)
💡 공백 처리 순서
1.str.strip()— 좌우 공백 제거
2.== ''조건으로 빈 문자열 확인 후 제거
공백만 있는 문자열은isna()로 감지되지 않으므로 반드시 위 두 단계를 거쳐야 합니다.
okt = Okt()
tokenizer = lambda x: [word for word in okt.morphs(x)]
pipe = Pipeline([
('vector', Vectorizer(tokenizer=tokenizer, min_count=1)),
('svc', SVC(random_state=42))
])
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
params = {
'vector__type' : ['w2v', 'ft'],
'vector__vector_size' : [80, 100],
'vector__l2' : [False, True],
'svc__C' : [0.8, 1.0]
}
grid = GridSearchCV(estimator=pipe, param_grid=params, cv=cv, verbose=1)
# 상위 100개로 학습
X_train = df.head(100)['document'].values
y_train = df.head(100)['label'].values
# 하위 100개로 평가
X_test = df.tail(100)['document'].values
y_test = df.tail(100)['label'].values
grid.fit(X_train, y_train)
print('최적 파라미터:', grid.best_params_)
print('최적 Score :', grid.best_score_)
pred = grid.predict(X_test)
print(classification_report(pred, y_test))
| 단계 | 매개변수 | 조합 |
|---|---|---|
vector | type | 'w2v' / 'ft' |
vector | vector_size | 80 / 100 |
vector | l2 | False / True |
svc | C | 0.8 / 1.0 |
총 2 × 2 × 2 × 2 = 16가지 조합, StratifiedKFold 5회 → 80번 학습
| 개념 | 설명 |
|---|---|
| FastText | subword 단위 벡터화 — OOV 문제 해결 |
| OOV | Out Of Vocabulary — 사전에 없는 미등록 단어 |
| subword | n-gram 방식으로 단어를 쪼갠 글자 단위 |
min_n / max_n | FastText subword 최소/최대 길이 |
min_n=0, max_n=0 | subword 비사용 → Word2Vec 방식과 동일 |
model_ft.wv['미등록단어'] | FastText는 미등록 단어도 벡터 생성 가능 |
| L2 정규화 | 벡터를 길이 1로 변환 → 방향만으로 유사도 비교 |
np.linalg.norm(v) | 벡터의 L2 norm(크기) 계산 |
v / (norm + 1e-12) | L2 정규화 수식 (분모 0 방지) |
list.index(value) | 리스트에서 특정 값의 위치 반환 |
sims.argsort()[::-1] | 유사도 내림차순 정렬 인덱스 |
type='w2v' / 'ft' | Vectorizer에서 모델 선택 매개변수 |
l2=True | Vectorizer에서 L2 정규화 적용 |
vector__type | Pipeline 내 Vectorizer 매개변수 접근 |
str.strip() | 좌우 공백 제거 |
~(df['col'] == '') | 빈 문자열 제외 필터 |
StratifiedKFold | 클래스 비율 유지하며 K겹 교차 검증 |