단순한 미션인 줄 알았는데 생각보다 난이도가 높다...
학습시간 09:00~03:00(당일18H/누적1200H)
어제 1~3번에 이어서 오늘 4번 부터 진행!
어제 EDA를 끝냈고 이제 전처리를 할 차례다.
전처리는 크게 토큰화와 벡터화로 나눌 수 있다. 토큰화에서는 특수문자, 토크나이즈, 대소문자, 어간, 표제어, 불용어 등을 다룬다. 이렇게 나온 결과물을 모델에 넣기 좋게 수치화하는 작업이 벡터화다.
from nltk.corpus import stopwords
from nltk.stem import WordNetLemmatizer
class TextPreprocessor:
def __init__(self):
self.lemmatizer = WordNetLemmatizer()
self.stop_words = set(stopwords.words('english'))
일단 전처리 클래스를 만들어 주고, nltk에서 표제어와 불용어 모듈을 가져와서 적용해 준다.
def preprocess_text(self, text):
text = text.lower()
text = re.sub(r'\S*@\S*\s?', '', text)
text = re.sub(r'http\S+', '', text)
text = re.sub(r'\d+', '', text)
text = re.sub(r'[^\w\s]', '', text)
text = re.sub(r'\s+', ' ', text).strip()
if not text:
return []
이어서 정제를 위한 함수를 만든다. 여기에는 정규식이 들어가서 조금 까다롭다 ㅠㅠ
순서대로 소문자 변환, 이메일 주소 제거, URL 제거, 숫자 제거, 특수문자 제거, 공백 제거로 진행했다.
마지막엔 다 지우고나서 데이터가 아무것도 없을 때 빈 리스트를 리턴하는 안전장치를 추가했다.
tokens = text.split()
processed_tokens = [
self.lemmatizer.lemmatize(word) for word in tokens
if word not in self.stop_words
]
return processed_tokens
split() 모듈로 단어를 토큰 단위로 쪼개고 표제어를 찾아서 processed_tokens에 다시 저장한다.
def preprocess_documents(self, documents):
return [self.preprocess_text(doc) for doc in documents]
방금 만든 토큰화 함수를 모든 문서를 돌면서 다 적용하는 함수다.
pp = TextPreprocessor()
processed_documents = pp.preprocess_documents(documents)
타이핑 편하게 pp로 초기화 해준다! 토큰화 끝!
벡터화를 위해 임베딩 모델을 만들어야 하는데, 이번 미션에서 Word2Vec, FastText, GloVe 모델을 사용하라고 명시했다.
근데 임베딩 모델이 3개고 자연어 처리 모델도 3개다. 이걸 각각 나누어 학습하면 1에폭에 9번을 돌려야한다는 소린데,,,, 이게 가능한가??
일단 함 해보자.
from gensim.models import Word2Vec, FastText
from gensim.scripts.glove2word2vec import glove2word2vec
def set_embedding_models():
임베딩 모델 3개를 한방 만들기 위해 함수를 만들었다.
word2vec_model = Word2Vec(
sentences=processed_documents,
vector_size=100,
window=5,
min_count=5,
workers=4,
sg=1,
epochs=10
)
먼저 Word2Vec 모델이다. gensim으로 불러오는 거라서 딱히 어려운 부분은 없다. 벡터 사이즈나 윈도우 수에서 성능 차이가 많이 갈릴 것 같긴 한데,,, 일단 통일해서 사용해 보자.
fasttext_model = FastText(
sentences=processed_documents,
vector_size=100,
window=5,
min_count=5,
workers=4,
sg=1,
epochs=10
)
다음은 FastText 모델이다. 이것도 특이사항은 없다.
download_url = 'http://nlp.stanford.edu/data/glove.6B.zip'
glove_dir = 'news-topic-classifier/glove'
zip_path = os.path.join(glove_dir, 'glove.6B.zip')
glove_txt = os.path.join(glove_dir, 'glove.6B.100d.txt')
word2vec_txt = os.path.join(glove_dir, 'glove.6B.100d.word2vec.txt')
os.makedirs(glove_dir, exist_ok=True)
# GloVe 다운로드
if not os.path.exists(glove_txt):
!wget --no-check-certificate -O news-topic-classifier/glove/glove.6B.zip http://nlp.stanford.edu/data/glove.6B.zip
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(glove_dir)
GloVe 모델이 가장 문제다. 이녀석은 사전학습된 가중치를 가져와야 하는데,, 이게 자꾸 에러가 나서 힘들었다.

어쨌든 다운로드 받고 나면 압축파일이 하나 생긴다. 압축파일 안에는 4개 txt 파일(50d~300d)이 있는데, 이게 임베딩 차원이라고 한다.
# 포맷 변환
if not os.path.exists(word2vec_txt):
glove2word2vec(glove_txt, word2vec_txt)
# GloVe 로드
glove_model = KeyedVectors.load_word2vec_format(word2vec_txt, binary=False)
return word2vec_model, fasttext_model, glove_model
다운받은 임베딩 가중치를 glove2word2vec() 모듈로 변환을 해준다. GloVe 모델인데 왜 Word2Vec으로 바꾸는지 궁금했는데, 이렇게 포맷을 바꿔줘야 모델에 넣을 수 있다고 한다.
word2vec_model, fasttext_model, glove_model = set_embedding_models()
이렇게 만든 함수로 모델 3개를 뽑아준다. 이걸 위한 큰 그림이었다!!
이렇게 벡터화를 위한 모델까지 생성 완료했다.
실질적인 벡터화는 학습루프 실행시킬 때 이루어지면 될 것 같다.
X_train, X_test, y_train, y_test = train_test_split(processed_documents, labels, test_size=0.2, random_state=42, stratify=labels)
print(f"Train data: {len(X_train)}")
print(f"Test data: {len(X_test)}")
머신러닝 시절이 떠오르는 스플릿 함수다.
이 간단한 코드가 머신러닝 배울 땐 왜 그리도 이해가 안 되었는지....
Train data: 15076
Test data: 3770
8:2 비율로 잘 나누어졌다.
class NewsDataset(Dataset):
def __init__(self, documents, labels, word_to_index, max_len):
self.documents = documents
self.labels = labels
self.word_to_index = word_to_index
self.max_len = max_len
def __len__(self):
return len(self.documents)
다음은 데이터셋 클래스다. 필요한 파라미터를 쭉 넣어준다.
이 코드가 있어야 전처리된 문서 리스트를 받아와서 라벨을 붙이고 내보낼 수 있다.
def __getitem__(self, idx):
tokens = self.documents[idx]
seq = [self.word_to_index.get(word, self.word_to_index['<UNK>']) for word in tokens]
if len(seq) < self.max_len:
seq = seq + [self.word_to_index['<PAD>']] * (self.max_len - len(seq))
else:
seq = seq[:self.max_len]
return torch.LongTensor(seq), torch.LongTensor([self.labels[idx]])
각 문서에 숫자 라벨을 달아준다. 처음 보는 단어가 들어오면 UNK 토큰을 넣고, 문서 길이가 최대 길이보다 작으면 PAD 토큰으로 채워준다.
def create_vocabulary(train_documents):
word_counts = Counter(word for doc in train_documents for word in doc)
vocab = sorted(word_counts, key=word_counts.get, reverse=True)
word_to_index = {word: i+2 for i, word in enumerate(vocab)}
word_to_index['<PAD>'] = 0
word_to_index['<UNK>'] = 1
vocab_size = len(word_to_index)
print(f"Vocabulary size: {vocab_size}")
return word_to_index, vocab_size
word_to_index, vocab_size = create_vocabulary(X_train)
이 함수가 많이 생소하다.
일단 Counter() 모듈로 문서 내 모든 단어의 출현 빈도를 계산한다.
sorted()를 통해 내림차순 정렬을 하고 가자 첫번 째 단어에 2번을 매핑한다. 2번부터 하는 이유는 0번은 PAD 1번은 UNK 토큰이기 때문이다.
Counter + enumerate 조합이 이렇게 들어가는구만
def create_embedding_matrix(embedding_model, word_to_index, embedding_dim=100):
이 함수도 약간 생소하다. 확실히 텍스트 모델은 생소한 개념이 많이 등장한다.
이건 쉽게 말해 각 임베딩 모델의 벡터공간에 단어를 맵핑하는 거다! 보물지도라고 하면 적당할듯
embedding_matrix = np.zeros((len(word_to_index), embedding_dim))
일단 zeros()로 0으로 채워진 행렬을 만든다. 여기에다가 위에서 만든 함수를 적용한다.
if isinstance(embedding_model, KeyedVectors):
vectors = embedding_model
else:
vectors = embedding_model.wv
그리고 GloVe는 이미 학습된 가중치를 가져오는 것이기 때문에 Word Vectors라는 속성이 이미 들어가 있다. 그래서 .wv를 붙여 학습된 임베딩 가중치를 가져오는 것!
for word, i in word_to_index.items():
if word in vectors:
embedding_matrix[i] = vectors[word]
마지막으로 create_vocabulary 함수의 결과 값인 word_to_index를 돌면서 zeros 공간에 쭉 맵핑을 한다.
return torch.FloatTensor(embedding_matrix)
텐서로 말아서 리턴하면 보물지도 만들기 완성~
word2vec_embedding_matrix = create_embedding_matrix(word2vec_model, word_to_index)
fasttext_embedding_matrix = create_embedding_matrix(fasttext_model, word_to_index)
glove_embedding_matrix = create_embedding_matrix(glove_model, word_to_index)
방금 만든 함수를 호출해서 각 임베딩 모델에 적용한다.
train_dataset = NewsDataset(X_train, y_train, word_to_index, max_len=150)
test_dataset = NewsDataset(X_test, y_test, word_to_index, max_len=150)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)
마지막으로 학습 전에 늘 하던 datset과 dataloader를 만들어주면 끝이다.
max_len은 토큰이라고 하는데, 어제 EDA할 때 텍스트 수가 2000~12000자로 격차가 심했다. 토큰 150개면 1000자 간신히 커버하려나... 일단 학습 한번 돌려보고 다시 조정해야겠다.
배치도 몇으로 해야할지 모르겠네. 일단 64 해보고 생각하자..!
텍스트 학습은 처음이라 하파를 몇으로 줘야할지 잘 모르겠네...
이제 모델 만들 차례인데,, 오늘도 해커톤을 위해 여기까지 해야겠다 ㅠㅠ
미션 기간이 길어서 참 다행이다.