ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • (4) 자연어처리_워드임베딩
    자연어처리 2023. 7. 30. 00:10
    728x90

    1. 워드 임베딩(Word Embedding)

    • 단어를 컴퓨터가 이해하고, 효율적으로 처리할 수 있도록 단어를 벡터화하는 기술
    • 단어를 밀집 벡터의 형태로 표현하는 방법
    • 워드 임베딩 과정을 통해 나온 결과를 임베딩 벡터
    • 워드 임베딩을 거쳐 잘 표현된 단어 벡터들은 계산이 가능하며, 모델에 입력으로 사용할 수 있음

    1-1. 인코딩(Encoding)

    • 기계는 자연어를 이해할 수 없기 때문에 데이터를 기계가 이해할 수 있도록 숫자 등으로 변환해주는 작업
    • 자연어를 수치화된 벡터로 변환하는 작업

    1-2. 희소 표현(Sparse Representation)

    • 원-핫 인코딩을 통해서 나온 원-핫 벡터들은 표현하고자 하는 단어의 인덱스의 값만 1이고, 나머지 인덱스에는 전부 0으로 표현되는 벡터 표현 방법
    • 벡터 또는 행렬의 값이 대부분이 0으로 표현되는 방법을 희소 표현이라고 함
    • 원-핫 인코딩에 의해 만들어지는 벡터를 희소 벡터라고 함

    1-3. 희소 벡터의 문제점

    • 희소 벡터의 특징은 단어의 개수가 늘어나면 벡터의 차원이 한없이 커진다는 것
    • 원-핫 벡터는 벡터 표현 방식이 매우 단순하여, 단순히 단어의 출현 여부만을 벡터에 표시할 수 있음
    • 희소 벡터를 이용하여 문장 혹은 텍스트간 유사도를 계산해보면 원하는 유사도를 얻기 힘듬

    1-4. 밀집 표현(Dense Representation)

    • 벡터의 차원이 조밀해졌다는 의미
    • 사용자가 설정한 값으로 모든 단어의 벡터 표현의 차원을 맞추는 표현 방식
    • 자연어를 밀집 표현으로 변환하는 인코딩 과정에서 0과 1의 binary 값이 아니라 연속적인 실수 값을 가질 수 있음

    1-5. 밀집 표현의 장점

    • 적은 차원으로 대상을 표현할 수 있음
    • 더 큰 일반화 능력을 가지고 있음

    1-6. 웟-핫 벡터와 워드 임베딩의 차이

                                                                                 원-핫 벡터                                                             워드 임베딩

    차원 고차원(단어 집합의 크기) 저차원으로 표현
    종류 희소 벡터 밀집 벡터
    표현방법 수동 커퍼스에서 학습
    값의 유형 0, 1 실수

    1-7. 차원 축소(Dimensionality Reduction)

    • 희소 벡터를 밀집 벡터의 형태로 변환하는 방법
    • 머신러닝에서 많은 피처들로 구성된 고차원의 데이터에서 중요한 피처들만 뽑아 저차원의 데이터(행렬)로 변환하기 위해 사용
      • PCA(Principal Component Analysis)
      • 잠재 의미 분석(Latent Semantic Analysis)
      • 잠재 디리클레 할당(Latent Dirichelet Allocation, LDA)
      • SVD(Singulat Value Decomposition, SVD)

    2. Word2vec

    2-1. 분산 표현(Distributed Representation)

    • 분포 가설이라는 가정 하에 만들어진 표현 방법
    • 분포 가설: "비슷한 문맥에서 등장하는 단어들은 비슷한 의미를 가진다"
    • 분포 가설의 목표는 단어 주변의 단어들, Window 크기에 따라 정의되는 문맥의 의미를 이용해 단어를 벡터로 표현(분산 표현) 하는 것
    • 분산 표현으로 표현된 벡터들은 원-핫 벡터처럼 차원이 단어 집합의 크기일 필요가 없으므로, 벡터의 차원이 상대적으로 저차원으로 줄어듬
    • 밀집 표현을 분산 표현이라 부르기도 함
    • 희소 표현에서는 각각의 차원이 각각의 독립적인 정보를 갖고 있지만, 밀집에서는 하나의 차원이 여러 속성들이 버무려진 정보를 갖고 있음
    • 밀집 표현을 이용한 대표적인 학습 방법이 Word2Vec 임

    2-2. Word2Vec 이란?

    • 분포 가설 하에 표현한 분산 표현을 따르는 원드 임베딩 모델
    • Google이 2013년도 처음 공개
    • 중심 단어와 주변의 단어들을 사용하여 단어를 예측하는 방식으로 임베딩을 만듬
    • Word2Vec의 학습 방식에는 두가지 방식
      • CBOW(Continuous Bag of Words)
      • Skip-Gram

    2-3. CBOW(Continuous Bag of Words)

    • Word2Vec
    • 주변에 있는 단어들을 보고 중간에 있는 단어를 예측하는 방법
    • 주변 단어(Context)는 타겟 단어(target word)의 직전 n개 단어와 직후 n개 단어를 의미하며, 이 범위를 window라 부르고, n을 window size라고 함
    • 문장 하나에 대해 한 번만 학습을 진행하는 것은 아깝기 때문에 sliding window 방식을 사용하여 하나의 문장을 가지고 여러 개의 학습 데이터셋을 만듬
    • Word2Vec은 최초 입력으로 one-hot-vector를 받는데, 1*V 크기의 one-hot-vector의 각 요소와 hidden layer N개의 각 노드는 1대1 대응이 이뤄져야 하므로 가중치 행렬 W의 크기는 V x N이 됨
    • 학습 코퍼스에 단어가 10,000개 있고 hidden layer의 노드를 300개로 지정하면, 가중치 행렬 W는 10,000 * 300 행렬 형태가 됨
    • 각각의 가중치 행렬은 랜덤한 값으로 초기화 되어 있고, 학습 시 target word를 맞추는 과정에서 w가 계속해서 조정됨
    • 예를 들어 4개의 단어들이 target word 예측에 사용될 때 각각의 단어들에 해당하는 W의 임베딩 벡터들 4개의 평균을 사용함
    • 평균 벡터는 두 번째 가중치 행렬 W' 와 곱해지며 곱해진 결과로는 타겟 word의 원 핫 벡터와 크기가 동일한 벡터를 얻을 수 있음
    • 최종 출력 값 벡터는 다중 클래스 분류 문제를 위한 일종의 스코어 벡터이며 0과 1 사이의 값을 가지는데 이는 중심 단어일 확률을 나타냄
    • 스코어 벡터 값은 정답 레이블에 해당하는 target word의 원-핫 벡터 내 1의 값에 가까워 져야 함
    • 스코어 벡터와 원-핫 벡터의 오차를 줄이기 위해 손실 함수(cross-entropy)함수를 사용함

    2-4. Skip-gram

    • 중심 단어에서 주변 단어를 예측
    • 중심 단어를 sliding window 하면서 학습 데이터를 증강
    • 중심 단어를 가지고 주변 단어를 예측하는 방법이기 때문에 Projection layer에서 벡터들 간의 평균을 구하는 과정이 없으며 대신 output layer를 통해 벡터가 2n개 만큼 나옴

    2-5. CBOW vs Skip-gram

    • Skip-gram이 CBOW에 비해 여러 문맥을 고려하기 때문에 Skip-gram의 성능이 일반적으로 더 좋음
    • Skip-gram이 단어 당 학습 횟수가 더 많고, 임베딩의 조정 기회가 많으므로 더 정교한 임베딩 학습이 가능

    2-6. Word2Vec의 한계점

    • 단어의 형태학적 특성을 반영하지 못함
    • 예) teach, teacher, tearchers 와 같이 세 단어는 의미적으로 유사한 단어지만 각 단어를 개별 단어로 처리하여 세 단어 모두 벡터 값이 다르게 구성됨
    • 단어 빈도 수의 영향을 많이 받아 희소한 단어를 임베딩하기 어려움
    • OOV(Out of Vocabulary)의 처리가 어려움
    • 새로운 단어가 등장하면 데이터 전체를 다시 학습시켜야 함
    • 단어 사전의 크기가 클수록 학습하는데 오래거릶
    • 단어 사전의 크기가 수 많개 이상인 경우, Word2Vec은 학습하기에 무거운 모델이 됨

    2-7. Word2Vec의 학습 트릭

    • Subsampling Frequent Words
      • 자연어 코퍼스에서 자주 등장하는 단어의 학습량을 확률적인 방법으로 줄이는 것
      • 등장 빈도가 높을수록 단어가 업데이트 될 기회가 많기 때문에 빈도수를 조절하게 됨
      • f(Wi)는 해당 단어가 말뭉치에 등장할 비율(해당 단어 빈도/전체 단어수)
      • t는 사용자가 지정해주는 값으로, 연구팀에서는 0.00001을 권장
      • 학습량을 효과적으로 줄여 계산량을 감소시키는 전략

    • Negative Sampling
      • 학습하는 중심 단어와 주변 단어들외에, 이 단어들과 별 연관 없는 수많은 단어의 임베딩까지 업데이트 하는 것은 비효율적임
      • 무작위로 네거티브 샘플들을 선택하여, 하나의 중심 단어에 대해 [주변단어+네거티브 샘플들]로만 구성된 작은 단어 집합을 만들어서 학습하면 훨씬 효율적으로 학습할 수 있을 것
      • positive sample은 1로, negative sample은 0으로 간주하는 binary classification 형식의 학습 방법을 사용
      • 모델을 학습할 때 기존의 Skip-gram과 다르게, target words에 대해서 단어 사전의 subset에 대해서만 학습을 진행할 수 있으므로 효율적으로 학습

    3. FastText

    • 분포 가설하에 표현한 분산 표현을 따르는 또 다른 임베딩 모델
    • 2017년 페이스북에서 공개한 워드 임베딩 기법

    3-1. FastText 동작 원리

    • < , >는 단어의 경계를 나타내기 위한 특수 기호
    • 단어를 먼저 <, >로 감싼 후, 설정한 n-gram의 값에 따라 앞에서부터 단어를 쪼갬
    • 마지막에 본 단어를 설명하기 위해 <,>으로 감싸진 전체 단어를 하나 추가함
    • n-gram 설정은 최소값과 최대값을 설정 할 수 있음

    3-2. FastText의 장점

    • 오타나 모르는 단어에 대한 대응이 좋음
    • 단어 집합 내의 빈도수가 적었던 단어에 대한 대응이 좋음
    • 자연어 코퍼스 노이즈 내에 대응이 좋음

    4. 워드 임베딩 구축하기

    import pandas as pd
    import numpy as np
    from sklearn.datasets import fetch_20newsgroups
     
    #예제 데이터셋
    #헤더,풋터,인용구 지우고 가져오겠다.
    dataset = fetch_20newsgroups(shuffle=True, random_state=10,remove=('headers','footers','quotes'))
    dataset= dataset.data
    # document 필드를 가진 데이터 프레임으로 변환
    news_df = pd.DataFrame({'document':dataset})
    news_df

    # 데이터셋에 결측값이 있는지 확인하기
    news_df.replace('', float('NaN'), inplace=True)
    print(news_df.isnull().values.any())

    # 데이터셋의 결측값을 제거 후 데이터셋 총 개수
    news_df = news_df.dropna().reset_index(drop=True)
    print(f'필터링된 데이터셋 총 개수: {len(news_df)}')

    # 열을 기준으로 중복된 데이터를 제거
    processed_news_df = news_df.drop_duplicates(['document']).reset_index(drop=True)
    processed_news_df

    len(processed_news_df.iloc[0][0])

    # 데이터셋에 특수 문자를 제거
    processed_news_df['document'] = processed_news_df['document'].str.replace(pat=r'[^\w]', repl=r' ', regex=True)
    processed_news_df['document']

    # 데이터셋에 길이가 너무 짧은 단어를 제거(단어의 길이가 2이하)
    processed_news_df['document'] = processed_news_df['document'].apply(lambda x: ' '.join([token for token in x.split() if len(token) > 2]))
    processed_news_df

    # 전체 길이가 200 이하이거나 전체 단어 개수가 5개 이하인 데이터를 필터링
    processed_news_df = processed_news_df[processed_news_df.document.apply(lambda x: len(str(x)) > 200 and len(str(x).split()) > 5)].reset_index(drop=True)
    processed_news_df

    # 전체 단어에 대한 소문자 변환
    processed_news_df['document'] = processed_news_df['document'].apply(lambda x: x.lower())
    processed_news_df

    import nltk
    from nltk.corpus import stopwords
    nltk.download('stopwords')
    stop_words = stopwords.words('english')
    print(len(stop_words))
    print(stop_words[:10])

    # 데이터 셋에 불용어를 제외하여 띄어쓰기 단위로 문장을 분리
    tokenized_doc = processed_news_df['document'].apply(lambda x: x.split())
    tokenized_doc = tokenized_doc.apply(lambda x: [s_word for s_word in x if s_word not in stop_words])
    tokenized_doc

    tokenized_doc = tokenized_doc.to_list()
    print(len(tokenized_doc))

    from tensorflow.keras.preprocessing.text import Tokenizer
    tokenizer = Tokenizer()
    tokenizer.fit_on_texts(tokenized_doc)
    word2idx = tokenizer.word_index
    idx2word = {value: key for key, value in word2idx.items()}
    encoded = tokenizer.texts_to_sequences(tokenized_doc)
    vocab_size = len(word2idx) + 1
    print(f'단어 사전의 크기:  {vocab_size}')

     

    print(encoded[0])

    # 네거티브 샘플링을 위해 keras에서 제공하는 전처리 도구 skipgrams 사용
    from tensorflow.keras.preprocessing.sequence import skipgrams

     

    skip_grams = [skipgrams(sample,vocabulary_size=vocab_size, window_size=10) for sample in encoded [:5]]
    print(f'전체 샘플 수 : {len(skip_grams)}')

    # skip_grams[0]에 skipgrams로 형성된 데이터셋 확인
    pairs, labels = skip_grams[0][0], skip_grams[0][1]
    print(f'3 pairs: {pairs[:3]}')
    print(f'3 labels: {labels[:3]}')

    # 첫번째 뉴스그룹 샘플에 대해 생긴 pairs와 labels의 개수
    print(len(pairs))
    print(len(labels))

    for i in range(5):
        print('({:s} ({:d}), {:s} ({:d})) -> {:d}'.format(
            idx2word[pairs[i][0]], pairs[i][0],
            idx2word[pairs[i][1]], pairs[i][1],
            labels[i]
        ))

    training_dataset = [skipgrams(sample, vocabulary_size=vocab_size, window_size=10) for sample in encoded[:5000]]
    len(training_dataset)

    from tensorflow.keras.models import Sequential, Model
    from tensorflow.keras.layers import Embedding, Reshape,Activation,Input,Dot
    from tensorflow.keras.utils import plot_model
    embedding_dim = 100
    # 중심 단어를 위한 임베딩 테이블
    w_inputs = Input(shape=(1,), dtype='int32')
    word_embedding = Embedding(vocab_size, embedding_dim)(w_inputs)
    c_inputs = Input(shape=(1,), dtype='int32')
    context_embedding = Embedding(vocab_size, embedding_dim)(c_inputs)
    dot_product = Dot(axes = 2)([word_embedding, context_embedding])
    dot_product = Reshape((1, ), input_shape = (1, 1))(dot_product)
    output = Activation('sigmoid')(dot_product)
    model = Model(inputs = [w_inputs, c_inputs], outputs = output)
    model.summary()

    model.compile(loss = 'binary_crossentropy', optimizer='adam')

     

    plot_model(model, to_file='model.png', show_shapes = True, show_layer_names = True)

    for epoch in range(100):
      loss=0
      for _,elem in enumerate(skip_grams):
        first_elem = np.array(list(zip(*elem[0]))[0], dtype='int32')
        second_elem = np.array(list(zip(*elem[0]))[1], dtype='int32')
        labels = np.array(elem[1],dtype = 'int32')
        X = [first_elem, second_elem]
        Y = labels
        loss += model.train_on_batch(X,Y)
      print('Epoch:',epoch+1,'Loss:',loss)

    import gensim
     
    for _,elem in enumerate(skip_grams):
      print(first_elem)
      print(second_elem)


    f = open('vectors.txt', 'w')
    f.write('{} {}\n'.format(vocab_size -1, embedding_dim))
    vectors = model.get_weights()[0]
    # print(vectors)
    for word, i in tokenizer.word_index.items():
        f.write('{} {}\n'.format(word, ' '.join(map(str, list(vectors[i, :])))))
    f.close()
    w2v = gensim.models.KeyedVectors.load_word2vec_format('./vectors.txt', binary=False)
    w2v.most_similar(positive=['doctor'])

    w2v.most_similar(positive=['engine'])

     

    '자연어처리' 카테고리의 다른 글

    (6) 자연어처리_Seq2Seq  (0) 2023.07.30
    (5) 자연어처리_ 워드 임베딩 시각화  (0) 2023.07.30
    (3) 자연어처리_임베딩  (0) 2023.07.11
    (2) 자연어처리_데이터전처리  (1) 2023.07.11
    (1) 자연어 처리 개요  (2) 2023.07.10

    댓글

Designed by Tistory.