no image
[NLP] 임베딩과 Word2Vec 실습해보기
1. 임베딩원핫인코딩(One-Hot-Encoding)은 머신러닝, 그중에서도 범주형 데이터(categorical data)에 많이 사용하는 기법입니다. 예를 들어, 음식에 대한 데이터가 있을 때 종류를 구분한다면 '한식', '중식', '일식', '이탈리아' 등과 같이 구분할 수 있습니다. 원핫인코딩은 이런 상황에서 아래 표처럼 표기하게 됩니다.종류_한식종류_중식종류_일식종류_이탈리아100000100001 하지만 NLP에서 이러한 방법은 효과적이지 않은데, 굉장히 다양한 단어들을 표현해야하는 NLP에서 원핫인코딩은 데이터의 표현이 희소(Sparse)하여 특정 행/열에서 무의미한 0 만 존재하고 이는 데이터가 너무 많아져 비효율적인 연산을 가져올 수 있습니다. 또한, 1과 0으로 구분하는 원핫인코딩으로는 유..
2022.09.07
no image
Selenium 을 활용한 Element 찾기 (find_element, By) | Python, Web Scraping, Web Crawling, 자동화
1. 소개 Selenium을 통해 페이지에 있는 요소를 찾는 것은 여러가지 방법이 있습니다. Selenium 에서는 By 클래스를 통해 다양한 속성으로 이용이 가능합니다.(By Strategy) 먼저 현재 사용하고 있는 Selenium 라이브러리의 버전은 다음과 같습니다. Version Selenium = 4.3.0. (향후 업데이트가 될 경우 아래 소개되는 내용은 이용이 불가능할 수 있으니 버전을 꼭 확인하시기 바랍니다) 전반적인 내용은 Selenium의 문서를 가져왔으며, 구성이나 번역은 제가 이해하기 쉽도록 바꾸었습니다. 원본에 대한 내용을 확인하고 싶으신 경우 아래 링크를 확인하시기 바랍니다. Selenium Python Docs 적용은 아래 예시처럼 find_element 메서드를 활용해 가능합..
2022.08.09
no image
[NLP] Char-RNN 을 활용하여 언어 모델링 실습 | 텍스트 생성
Char-RNN 개념앞서 살펴봤던 통계기반 언어모델은 단어들의 빈도를 기반으로 학습을 합니다. 하지만 그렇기 때문에 발생하는 문제들이 있는데, N-gram을 다루면서 (링크) 모델의 한계를 살펴봤습니다.그렇다면 대안은 무엇일까요? 인공신경망에 대한 연구와 컴퓨팅 능력이 향상되면서 뉴럴 네트워크로 학습하는 언어모델도 있습니다. 여기서는 글자(Character) 단위 언어 모델인 Char-RNN에 대해 살펴보고자 합니다. 우선, RNN에 대해 생각해보겠습니다. RNN은 (t+1)에 대한 예측을 하기 위해 t까지 결과를 이용하여 순환적으로 예측에 사용합니다. 이는 다시 다음인 (t+2)에 영향을 미치는 과정을 끊임없이 반복하는 구조입니다.그럼 이를 확장해서 문자단위 RNN 모델(Char-RNN)을 생각하게 보..
2022.06.06
no image
[NLP] N-gram 모델 구현하기 기초 | 임베딩, NLTK
1. N-gram 모델N-gram 모델은 총 N개의 단어의 그룹 내에서 N번째 단어 이전의 (N-1)개의 단어에 기반해 다음에 올 단어를 예측하는 모델입니다. 단어들의 집합에 확률을 부여하는 방식으로 다음에 올 단어를 맞추게 되는데, 기본적으로 조건부확률을 기반으로 추정하게 됩니다. 이에 대한 수학식은 다음과 같습니다.N-gram 모델은 몇 가지 문제점을 가지고 있습니다.데이터셋에 존재하지 않은 단어의 조합은 추정이 어렵다N 개수를 증가시키는 것은 모델의 성능을 높일 수 있지만, 모델 사이즈 증가에 따른 비용 문제가 된다그래서 최근에는 이런 문제들을 해결하기 위해 딥러닝에 기반한 언어모델들이 주로 사용되고 있습니다. 이는 추후 미래의 글에서 다루도록 하겠습니다.2. N-gram 모델 구현N-gram 모델..
2022.05.30
no image
TensorFlow로 딥러닝 모델 구현하기 | 시퀀셜, 함수형, 서브클래싱 API
딥러닝의 간단한 개념딥러닝 모델을 구현하기에 앞서서 딥러닝이란 무엇인가에 대해 살펴볼 필요가 있습니다. 딥러닝은 여러 연산의 조합을 깊게(길게) 연결된 모델을 통해 문제를 풀고자 하는 머신러닝 방법 중 하나입니다. 여기서 여러 연산의 조합은 1957년에 제안된 퍼셉트론("TLU"라고도 불림)이라는 개념에서 유래하는데, 입력(x)과 가중치의 곱셈의 합(선형결합)을 계산해 비선형함수에 대입하면서 문제를 풉니다. 이에 대한 식은 아래와 같습니다.이런 TLU들이 층을 쌓아올리게 되면 일부 제약을 줄일 수 있다는 사실이 밝혀졌는데, 이를 다층퍼셉트론(MLP)라고 합니다. 그리고 이들이 모두 연결되어 있을 때, 즉 누군가의 결과값이 다음 층의 입력값인 구조에서 우리는 밀집층이라고 부릅니다.각각의 TLU는 뉴런 층의..
2022.05.17
no image
[알고리즘] 파이썬으로 에라토스테네스의 체를 이용해 소수 판별하기 | Python, Algorithm, Prime Number
에라토스테네스의 체는 고대 그리스의 수학자 에라토스테네스가 발견한 소수(prime number)를 찾는 방법입니다. 수학시간은 아니므로 소수냐 합성수냐에 대한 정의 설명은 생략하고, 어떤 방식으로 동작하는지 살펴보겠습니다. 1. 소수를 찾고자 하는 구간의 모든 수를 나열합니다.2. 어느 한 소수의 배수를 모두 지웁니다.3. 남아있는 수 가운데 다음 소수를 찾습니다. 4. 2-3 과정을 반복합니다. 여기서 2가지 포인트가 있습니다. 먼저, 위 과정에서 2단계의 최초 시작은 2부터 합니다. (왜냐하면 1은 소수도 합성수도 아닌 수이기 때문입니다) 그다음은 3, 5, 7.... 순으로 커지게 됩니다.  두번째는 최대 약수는 어느 숫자의 제곱근을 넘을 수 없다는 사실입니다. 정수론 시간이 아니므로 엄밀한 증명은..
2022.03.17
no image
[알고리즘] 파이썬으로 다이나믹 프로그래밍 구현하기 | Python, Dynamic Programming, Algorithm
위키피디아를 참고하면 다이나믹 프로그래밍, 다른 말로 동적계획법은 복잡한 문제를 간단한 여러 개의 문제로 나누어 푸는 것을 의미합니다. 한편, 알고리즘 스터디를 위해 참고하는 블로그(안경잡이 개발자)에서는 '하나의 문제를 단 한 번만 풀도록 하는 알고리즘'이라고 정의하고 있습니다. 일반적으로 분할 정복 기법은 동일한 문제를 다시 풀기 때문에 비효율적입니다. 그러나 다이나믹 프로그래밍은 하나의 문제를 한 번만 풀도록 하기 때문에 비효율적인 알고리즘을 개선시킬 수 있습니다. 구현에 대한 사례로 피보나치 수열을 활용하겠습니다. 중/고등학교 수학시간에서 배운 것처럼, 피보나치 수열은 어느 숫자를 구하기 위해서 그 앞과 앞앞 자리의 숫자를 더하면서 구해가는 수열입니다. 이를 식으로 나타내보면 다음과 같습니다. a..
2022.03.14
no image
[알고리즘] 파이썬으로 순회(Traversal) 구현하기 | Python, Algorithm, Traversal, Binary Tree
이전 글에서 이진트리에 대해서 알아봤습니다. 이진트리 자료구조에 대한 사항은 아래 글을 참고하시기 바랍니다. https://seanpark11.tistory.com/82 이러한 트리 구조에서 각가의 노드를 정확히 한번만 방문하는 것을 체계화한 것이 트리순회(Tree Traveral, 줄여서 순회)입니다. 여기서 구현해볼 순회의 방법은 3가지 입니다 : 전위순회(Preorder Traversal), 중위순회(Inorder Traversal), 후위순회(Postorder Traversal). 사실은 더 많은 순회에 대한 내용들이 있지만, 편의상 이 세가지 내용만 다루고자 합니다. 먼저 전위순회는 다음과 같은 순서입니다. 노드를 방문 왼쪽 자식을 전위순회 오른쪽 자식을 전위순회 앞서 언급한 트리 예시를 사용하..
2022.02.17
no image
[자료구조] 파이썬으로 이진트리 구현하기
이진트리(Binary Tree)는 각가의 노드가 최대 두개의 자식 노드를 가지는 트리 자료구조 입니다. 왼쪽에 있는 자식노드를 '왼쪽 자식 노드', 오른쪽에 있는 자식노드를 '오른쪽 자식 노드'로 구분합니다. (약간 말장난같지만, 이 둘은 분명히 다른 노드이기 때문에 다른 용어가 필요합니다) 엄밀한 의미의 정의는 아니지만 이런 형태의 정의도 이해하기 충분하니, 다음은 몇 가지 용어로 넘어가겠습니다. 노드(Node) : 데이터를 저장하는 기본 요소 부모 노드(Parent Node) : 어느 노드를 자식으로 갖는 노드 자식 노드(Child Node) : 어느 노드를 부모로 갖는 노드 형제 노드(Sibling Node) : 부모가 같은 노드 차수(Degree) : 자식의 수 루트 노드(Root Node) : ..
2022.02.14
반응형

1. 임베딩

원핫인코딩(One-Hot-Encoding)은 머신러닝, 그중에서도 범주형 데이터(categorical data)에 많이 사용하는 기법입니다. 예를 들어, 음식에 대한 데이터가 있을 때 종류를 구분한다면 '한식', '중식', '일식', '이탈리아' 등과 같이 구분할 수 있습니다. 원핫인코딩은 이런 상황에서 아래 표처럼 표기하게 됩니다.

종류_한식 종류_중식 종류_일식 종류_이탈리아
1 0 0 0
0 0 1 0
0 0 0 1

 

하지만 NLP에서 이러한 방법은 효과적이지 않은데, 굉장히 다양한 단어들을 표현해야하는 NLP에서 원핫인코딩은 데이터의 표현이 희소(Sparse)하여 특정 행/열에서 무의미한 0 만 존재하고 이는 데이터가 너무 많아져 비효율적인 연산을 가져올 수 있습니다. 또한, 1과 0으로 구분하는 원핫인코딩으로는 유사한 의미에 대해서도 표현이 불가합니다. 그래서 NLP에서는 임베딩이라는 다른 방식을 사용합니다.

 

임베딩은 쉽게 이야기하면 컴퓨터가 자연어를 이해할 수 있도록 숫자의 나열인 벡터로 바꾸는 과정을 이야기 합니다. 수학적으로 이야기하자면, 원본 데이터에 임베딩 행렬을 곱해서 표현합니다. 이렇게 표현한 임베딩은 다양한 역할을 수행할 수 있습니다.

  • 차원축소
    원핫인코딩에서 지적했던 희소한 데이터 포인트들을 좀 더 밀집(Dense)되게 표현하고 그 결과로 차원을 축소할 수 있습니다. 이는 연산의 효율성을 도모할 수 있습니다.
  • 의미함축
    또한, 원핫인코딩은 의미에 대해서 표현이 불가능하다고 했지만, 임베딩은 의미를 담을 수 있습니다. 단어의 의미, 문법 정보를 함축하여 단어 혹은 문장 사이의 관련도를 담을 수 있다는 의미입니다. 단어 벡터의 의미를 담고 있기 때문에 '한국-서울+도쿄' 연산을 수행하면 '일본'이라는 결과를 도출할 수 있습니다. 이에 대한 재밌는 실험은 아래 링크를 통해 시도해 볼 수 있습니다.

https://word2vec.kr/search/

  • 전이학습
    전이학습이란 임베딩을 다른 딥러닝 모델의 입력값으로 쓰는 것을 일컫습니다. 이미 대규모 말뭉치를 활용해 만들어둔 검증된 임베딩을 활용할 수 있다면, 훨씬 효율적으로 사용할 수 있을텐데 임베딩은 이러한 전이학습이 가능하기 때문에 효율적입니다.

 

2. Word2Vec

자연어 처리에서 분포는 특정 범위(또는 '윈도우')에서 동시에 등장하는 단어들의 집합(문맥)을 의미합니다. 이 집합에서 자주 등장하는 경우 의미가 유사할 것이라는 가정을 분포 가정이라 합니다.

 

임베딩에 대한 기법 중 Word2Vec는 위에서 언급한 분포 가정의 대표적인 모델입니다. 구글 연구 팀에서 2013년에 발표(링크)한 임베딩 기법으로, 방식은 CBOW와 Skip-Gram 두가지로 구분됩니다. CBOW는 Continuous Bag-Of-Words의 준말로 문맥 단어들을 가지고 타깃을 예측하는 모델입니다. Skip-Gram은 타깃 단어로부터 문맥을 예측하는 모델입니다. 예를 들면, CBOW는 'she is a beautiful'라는 문맥에서 'girl'이라는 타깃을 예측합니다. 반면, Skip-Gram에서는 'girl'이라는 단어에서 'she is a beautiful' 이라는 문맥을 예측합니다.

 

여기서는 더 큰 규모의 데이터셋에 적합한 Skip-Gram에 대해 살펴보고자 합니다. Skip-Gram 모델은 다음과 같은 과정을 거칩니다.

 

(1) 먼저 문맥을 정의합니다.

 

문맥은 윈도우에 의해 정의됩니다. 윈도우는 타깃단어의 양 쪽으로 확장되는 것으로 볼 수 있습니다. Google에서 제시하고 있는 예시를 통해 이해해보겠습니다.

밑줄 친 단어는 타깃단어로 윈도우를 2개로 지정하면서 양 옆 두개 단어까지 확장하고 있는 것을 확인할 수 있습니다. 타깃단어와 문맥단어를 쌍으로 연결하는 Skip-gram 열에서 볼 수 있습니다.

 

(2) Negtive Sample을 형성합니다.

 

Negative Sample이란 타깃 단어와 그 주변에 없는 단어(noise; 텍스트에서 임의 추출)의 쌍을 말합니다. 반대로, Positive Sample은 타깃 단어와 그 주변에 있는 단어의 쌍을 말합니다. 위의 예시에서 임의의 네거티브 샘플을 추출하면 다음과 같습니다.

 

(hot, shimmered)

(wide, hot)

(wide, sun)

 

Skip-Gram에서는 이렇게 같은 텍스트에서 많은 학습 데이터를 만들 수 있습니다.

 

(3) 실제 단어에는 높은 확률을 부여하고, 그렇지 않은 단어들에는 낮은 확률을 부여하도록 합니다. (Negative Sampling)

 

3. Word2Vec 구현

그렇다면 이를 실제로 코드로 구현해보고자 합니다. 전반적인 설명을 하다보니 내용이 꽤 길어서 실제 예제에 적용하는 것만 보고 싶으신 분은 우측 상단에서 3.3 후반 ~ 6까지로 이동하셔서 확인하시기 바랍니다. 모든 예제 소스들은 Google Tenflow 팀의 홈페이지의 Word2Vec에 예시를 참고하였습니다. (링크)

3.1. 기본 준비 및 벡터화

import io
import re
import string
import tqdm
import numpy as np
import tensorflow as tf
from tensorflow.keras import layers

# Vectorize
sentence = "The wide road shimmered in the hot sun"
tokens = list(sentence.lower().split())

vocab, index = {}, 1  # start indexing from 1
vocab['<pad>'] = 0  # add a padding token
for token in tokens:
  if token not in vocab:
    vocab[token] = index
    index += 1
vocab_size = len(vocab)
print(vocab)

example_sequence = [vocab[word] for word in tokens]
print(example_sequence)

우선 Word2Vec 구현을 위해 필요한 라이브러리를 호출하였습니다. 예시로 사용하는 텍스트를 정의하고 어휘집합을 딕셔너리 형태로 만듭니다. 그리고 문장을 인덱스로 이용해 벡터화 합니다. 그에 대한 출력 결과는 아래와 같습니다.

3.2. Skip-gram 및 Negative Sampling

window_size = 2
positive_skip_grams, _ = tf.keras.preprocessing.sequence.skipgrams(
      example_sequence,
      vocabulary_size=vocab_size,
      window_size=window_size,
      negative_samples=0)

위에서 설명했던 것처럼 Skip-gram은 네거티브 샘플을 형성할 필요가 있습니다. 텐서플로우에서는 이 귀찮은 과정을 해결하기 위해 tf.keras.preprocessing.sequence.skipgrams로 지원합니다. skipgrams 함수는 모든 Skip-gram 쌍을 주어진 윈도우 크기만큼 슬라이딩하면서 반환합니다.  

 

그 다음으로는 네거티브 샘플링을 위한 코드입니다. 훈련을 위해서 더 많은 쌍을 만들기 위해서는 어휘집합으로부터 임의의 단어들을 뽑아낼 필요가 있습니다. tf.random.log_uniform_candidate_sampler 함수는 num_ns로 표기되는 네거티브 샘플 개수만큼 샘플링 합니다.

# Get target and context words for one positive skip-gram.
target_word, context_word = positive_skip_grams[0]

# Set the number of negative samples per positive context.
num_ns = 4

context_class = tf.reshape(tf.constant(context_word, dtype="int64"), (1, 1))
negative_sampling_candidates, _, _ = tf.random.log_uniform_candidate_sampler(
    true_classes=context_class,  # class that should be sampled as 'positive'
    num_true=1,  # each positive skip-gram has 1 positive context class
    num_sampled=num_ns,  # number of negative context words to sample
    unique=True,  # all the negative samples should be unique
    range_max=vocab_size,  # pick index of the samples from [0, vocab_size]
    seed=SEED,  # seed for reproducibility
    name="negative_sampling"  # name of this operation
)

참고로 작은 데이터 셋에는 num_ns = [ 5, 20 ] 사이의 값이 적당할 수 있으며, 큰 데이터셋에는 num_ns = [ 2, 5 ] 사이의 값이 충분하다고 알려져있습니다.

 

3.3. 훈련 데이터셋 구성 및 요약

# Add a dimension so you can use concatenation (in the next step).
negative_sampling_candidates = tf.expand_dims(negative_sampling_candidates, 1)

# Concatenate a positive context word with negative sampled words.
context = tf.concat([context_class, negative_sampling_candidates], 0)

# Label the first context word as `1` (positive) followed by `num_ns` `0`s (negative).
label = tf.constant([1] + [0]*num_ns, dtype="int64")

# Reshape the target to shape `(1,)` and context and label to `(num_ns+1,)`.
target = tf.squeeze(target_word)
context = tf.squeeze(context)
label = tf.squeeze(label)

학습용 데이터셋을 만드는 과정을 요약한 다이어그램은 아래와 같습니다.

텍스트에서 데이터 셋을 형성하는 과정 요약 (출처 : Google TensorFlow 'Word2Vec')

'the', 'is', 'on'과 같은 단어들은 흔히 발견할 수 있지만, 의미를 해석하는데 유용한 정보를 제공하지는 않습니다. 따라서 임베딩의 품질을 높이기 위해 단어별 가중치를 주는 작업이 필요합니다.

sampling_table = tf.keras.preprocessing.sequence.make_sampling_table(size=10)

make_sampling_table 함수는 확률론적 샘플링 테이블을 기반으로 단어의 빈도 순위를 생성합니다. sampling_table[i]는 i번째의 가장 빈번하게 등장하는 단어의 확률을 나타냅니다.

 

다음은 훈련데이터의 생성입니다. 이 함수는 뒤에서 사용될 예정입니다.

# Generates skip-gram pairs with negative sampling for a list of sequences
# (int-encoded sentences) based on window size, number of negative samples
# and vocabulary size.
def generate_training_data(sequences, window_size, num_ns, vocab_size, seed):
  # Elements of each training example are appended to these lists.
  targets, contexts, labels = [], [], []

  # Build the sampling table for `vocab_size` tokens.
  sampling_table = tf.keras.preprocessing.sequence.make_sampling_table(vocab_size)

  # Iterate over all sequences (sentences) in the dataset.
  for sequence in tqdm.tqdm(sequences):

    # Generate positive skip-gram pairs for a sequence (sentence).
    positive_skip_grams, _ = tf.keras.preprocessing.sequence.skipgrams(
          sequence,
          vocabulary_size=vocab_size,
          sampling_table=sampling_table,
          window_size=window_size,
          negative_samples=0)

    # Iterate over each positive skip-gram pair to produce training examples
    # with a positive context word and negative samples.
    for target_word, context_word in positive_skip_grams:
      context_class = tf.expand_dims(
          tf.constant([context_word], dtype="int64"), 1)
      negative_sampling_candidates, _, _ = tf.random.log_uniform_candidate_sampler(
          true_classes=context_class,
          num_true=1,
          num_sampled=num_ns,
          unique=True,
          range_max=vocab_size,
          seed=seed,
          name="negative_sampling")

      # Build context and label vectors (for one target word)
      negative_sampling_candidates = tf.expand_dims(
          negative_sampling_candidates, 1)

      context = tf.concat([context_class, negative_sampling_candidates], 0)
      label = tf.constant([1] + [0]*num_ns, dtype="int64")

      # Append each element from the training example to global lists.
      targets.append(target_word)
      contexts.append(context)
      labels.append(label)

  return targets, contexts, labels

 

3.4. 학습 데이터 임포트

그러면 실전으로 들어가겠습니다. Word2Vec 모델을 활용해 문장 모음들을 훈련 모델을 생성해보겠습니다. 활용할 데이터는 TensorFlow에서 제공하는 셰익스피어 텍스트 입니다. 앞서 벡터화했던 방법과는 다르게, TextVectorization 레이어를 통해 텍스트를 벡터화를 했습니다.

path_to_file = tf.keras.utils.get_file('shakespeare.txt', 'https://storage.googleapis.com/download.tensorflow.org/data/shakespeare.txt')
# 비어있는 줄 제거
text_ds = tf.data.TextLineDataset(path_to_file).filter(lambda x: tf.cast(tf.strings.length(x), bool))

# 소문자 활용 및 구두점 제거
def custom_standardization(input_data):
  lowercase = tf.strings.lower(input_data)
  return tf.strings.regex_replace(lowercase,
                                  '[%s]' % re.escape(string.punctuation), '')

vocab_size = 4096
sequence_length = 10

# TextVectorization
vectorize_layer = layers.TextVectorization(
    standardize=custom_standardization,
    max_tokens=vocab_size,
    output_mode='int',
    output_sequence_length=sequence_length)

TextVectorization.adapt를 통해 어휘 집합을 생성하고, TextVectorization.get_variable 을 통해 레이어가 말뭉치를 나태내도록 할 수 있습니다. get_variable 함수는 어휘집합의 토큰을 많이 등장하는 내림차순에 따라 리스트를 반환합니다. 그리고 이 과정을 거치게 되면, 벡터를 형성할 수 있습니다.

vectorize_layer.adapt(text_ds.batch(1024))
invers_vocab = vectorize_layer.get_vocabulary()

# Vectorize the data in text_ds.
text_vector_ds = text_ds.batch(1024).prefetch(AUTOTUNE).map(vectorize_layer).unbatch()

Word2Vec 모델에 훈련하기 위해서 벡터 시퀀스 리스트로 만듭니다. 이는 positive & negative samples를 만들기 위해 필요합니다. 앞서 만들었던 generate_training_data 함수를 활용해 Word2Vec 모델에 들어갈 값들을 계산합니다.

sequences = list(text_vector_ds.as_numpy_iterator())

targets, contexts, labels = generate_training_data(
    sequences=sequences,
    window_size=2,
    num_ns=4,
    vocab_size=vocab_size,
    seed=SEED)

targets = np.array(targets)
contexts = np.array(contexts)[:,:,0]
labels = np.array(labels)

BATCH_SIZE = 1024
BUFFER_SIZE = 10000
dataset = tf.data.Dataset.from_tensor_slices(((targets, contexts), labels))
dataset = dataset.shuffle(BUFFER_SIZE).batch(BATCH_SIZE, drop_remainder=True)

 

3.5. 모델링

Word2Vec 모델을 서브클래싱 API를 통해 작성할 수 있습니다. 각 레이어별로 설명은 다음과 같습니다.

 

- target_embedding : 타깃단어로 나타나는 단어의 임베딩을 찾는 레이어 입니다. 파라미터의 개수는 vocab_size * embedding_dim 으로 계산할 수 있습니다.

 

- context_embedding : 문맥단어로 나타나는 단어의 임베딩을 찾는 레이어 입니다. 파라미터의 개수는 vocab_size * embedding_dim 으로 계산할 수 있습니다.

 

class Word2Vec(tf.keras.Model):
  def __init__(self, vocab_size, embedding_dim):
    super(Word2Vec, self).__init__()
    self.target_embedding = layers.Embedding(vocab_size,
                                      embedding_dim,
                                      input_length=1,
                                      name="w2v_embedding")
    self.context_embedding = layers.Embedding(vocab_size,
                                       embedding_dim,
                                       input_length=num_ns+1)

  def call(self, pair):
    target, context = pair
    if len(target.shape) == 2:
      target = tf.squeeze(target, axis=1)
    word_emb = self.target_embedding(target)
    context_emb = self.context_embedding(context)
    dots = tf.einsum('be,bce->bc', word_emb, context_emb)
    return dots

# Loss function 
def custom_loss(x_logit, y_true):
      return tf.nn.sigmoid_cross_entropy_with_logits(logits=x_logit, labels=y_true)

여기서는 카테고리 크로스엔트로피를 네거티브 샘플링의 손실함수로 정의했습니다. 그럼 이제 모든 준비가 끝났습니다. 갖고있는 데이터를 Word2Vec 모델에 적용하도록 하겠습니다.

embedding_dim = 128
word2vec = Word2Vec(vocab_size, embedding_dim)
word2vec.compile(optimizer='adam',
                 loss=tf.keras.losses.CategoricalCrossentropy(from_logits=True),
                 metrics=['accuracy'])
word2vec.fit(dataset, epochs=20, callbacks=[tensorboard_callback])

 

3.6. 임베딩 분석

모델의 가중치는 Model.get_layer와 Layer.get_weights를 통해 확인할 수 있습니다. 그리고 위 과정을 통해 만든 벡터와 메타데이터 파일도 저장할 수 있습니다.

weights = word2vec.get_layer('w2v_embedding').get_weights()[0]

# Save vectors & metadata files
out_v = io.open('vectors.tsv', 'w', encoding='utf-8')
out_m = io.open('metadata.tsv', 'w', encoding='utf-8')

for index, word in enumerate(vocab):
  if index == 0:
    continue  # skip 0, it's padding.
  vec = weights[index]
  out_v.write('\t'.join([str(x) for x in vec]) + "\n")
  out_m.write(word + "\n")
out_v.close()
out_m.close()

# Download vector
try:
  from google.colab import files
  files.download('vectors.tsv')
  files.download('metadata.tsv')
except Exception:
  pass
반응형
반응형

1. 소개

 

Selenium을 통해 페이지에 있는 요소를 찾는 것은 여러가지 방법이 있습니다. Selenium 에서는 By 클래스를 통해 다양한 속성으로 이용이 가능합니다.(By Strategy) 먼저 현재 사용하고 있는 Selenium 라이브러리의 버전은 다음과 같습니다.

 

Version
Selenium = 4.3.0.
(향후 업데이트가 될 경우 아래 소개되는 내용은 이용이 불가능할 수 있으니 버전을 꼭 확인하시기 바랍니다)

 

전반적인 내용은 Selenium의 문서를 가져왔으며, 구성이나 번역은 제가 이해하기 쉽도록 바꾸었습니다. 원본에 대한 내용을 확인하고 싶으신 경우 아래 링크를 확인하시기 바랍니다.

 

Selenium Python Docs

 

적용은 아래 예시처럼 find_element 메서드를 활용해 가능합니다. 만약에 찾고자 하는 요소가 없다면, NoSuchElementException 예외가 발생합니다. 여러 요소를 찾을 경우에는 find_elements 메서드를 활용할 수 있습니다.

 

from selenium import webdriver
from selenium.webdriver.common.by import By
# 기본 작업 : driver 작동 및 웹페이지 접근
driver = webdriver.Chrome()
driver.get("address")

# 페이지 탐색 방법
driver.find_element(By.ID, "id")
driver.find_element(By.NAME, "name")
driver.find_element(By.XPATH, "xpath")
driver.find_element(By.LINK_TEXT, "link text")
driver.find_element(By.PARTIAL_LINK_TEXT, "partial link text")
driver.find_element(By.TAG_NAME, "tag name")
driver.find_element(By.CLASS_NAME, "class name")
driver.find_element(By.CSS_SELECTOR, "css selector")

 

2. Selenium에서 요소(Element) 찾기

2.1. Id 로 찾기

id 속성을 알고 있는 경우, 사용할 수 있는 방법입니다. 아래는 페이지 소스 예시입니다. (이후 케이스에서도 활용)

<html>
 <body>
  <form id="loginForm">
   <input name="username" type="text" />
   <input name="password" type="password" />
   <input name="continue" type="submit" value="Login" />
  </form>
 </body>
</html>

여기서 id인 loginForm을 찾기 위해서는 아래와 같이 코드를 사용합니다.

login_form = driver.find_element(By.ID, 'loginForm')

 

2.2. 이름으로 찾기

이름 속성을 아는 경우 사용할 수 있는 방법입니다. 2.1에서 사용한 페이지 소스 예시에서 username과 password란 name의 요소를 찾고자 하는 경우 아래와 같이 구현이 가능합니다.

username = driver.find_element(By.NAME, 'username')
password = driver.find_element(By.NAME, 'password')

 

2.3. XPath로 찾기

XPath는 W3C 표준으로 XML 문서 구조를 통해 노드를 찾도록 사용되는 언어입니다. XPath는 절대항으로 찾거나 상대적인 속성을 통해 찾을 수 있습니다. 2.1.에서 사용한 페이지 소스 예시에서 loginForm을 XPath를 활용하여 아래 코드와 같이 찾을 수 있습니다.

login_form = driver.find_element(By.XPATH, "/html/body/form[1]")       # Absoulte Path
login_form = driver.find_element(By.XPATH, "//form[1]")                # 1st form element 
login_form = driver.find_element(By.XPATH, "//form[@id='loginForm']")  # attribute id

XPath에 대한 더 세부적인 사용법을 공부할 수 있는 컨텐츠를 W3C나 W3School에서 제공하고 있습니다. XPath를 더욱 잘 활용하기 위해 공부를 하고 싶다면 아래 링크를 통해 접근할 수 있습니다.

W3C XPath

W3School (Xpath_Syntax)

 

2.4. 하이퍼링크 찾기

링크가 달려있는 텍스트를 알고 있다면 사용가능한 방법입니다. 새로운 페이지 소스 예시입니다. 안에는 앵커태그()가 있고 안에는 href 속성을 통해 보여주고자 하는 html을 정의하고 있습니다.

<html>
 <body>
  <p>Are you sure you want to do this?</p>
  <a href="continue.html">Continue</a>
  <a href="cancel.html">Cancel</a>
</body>
</html>

continue.html 로 가는 링크를 찾기 위해서는 아래와 같이 찾을 수 있습니다.

continue_link = driver.find_element(By.LINK_TEXT, 'Continue')
continue_link = driver.find_element(By.PARTIAL_LINK_TEXT, 'Conti')

 

2.5. 태그 이름으로 찾기

BeautifulSoup에서도 많이 활용되는 방법인데, 태그 이름을 가지고도 찾을 수 있습니다. 아래와 같은 페이지 소스가 있다고 가정합니다.

<html>
 <body>
  <h1>Welcome</h1>
  <p>Site content goes here.</p>
</body>
</html>

여기서 "Welcome"이라는 문구가 적힌 h1 태그이름을 알고 있다면 아래와 같이 찾을 수 있습니다.

heading1 = driver.find_element(By.TAG_NAME, 'h1'))

 

2.6. 클래스 이름으로 찾기

태그와 동일하게 클래스 이름으로도 찾을 수 있습니다. (BeautifulSoup을 많이 사용해보신 분들은 익숙하실 것 같습니다) 2.5.와 동일한 페이지 소스 예시에서 "p" 요소를 찾으려면 아래와 같이 구현할 수 있습니다.

content = driver.find_element(By.CLASS_NAME, 'content')

 

2.7. CSS 선택자로 찾기

XPath처럼 여러가지 정보를 담을 수 있는 CSS 선택자(Selector)도 크롤링에서 자주 등장하는 구문해석 방법입니다. 2.5.의 동일한 예시에서 같은 내용을 찾으려면 아래와 같이 구현이 가능합니다.

content = driver.find_element(By.CSS_SELECTOR, 'p.content')

XPath와 마찬가지로, 다양한 기관에서 CSS 선택자에 대한 자료를 제공하고 있으니 참고하시기 바랍니다. (Selenium 프로젝트 웹페이지에서는 아래 링크를 추천하고 있습니다.)

Sauce Labs (CSS documentation)

 

3. 활용

이렇게 구현한 요소를 찾는 것은 Web Element를 반환합니다. 한 예시로 제가 최근에 크롤링 작업을 했던 web element에 담겨있는 정보들입니다.

페이지를 구성하는 여러가지 요소들이 보이는데, 필요한 정보에 접근하기 위해서는 '변수명.정보'와 같이 해서 가져올 수 있습니다.  

반응형
반응형

Char-RNN 개념

앞서 살펴봤던 통계기반 언어모델은 단어들의 빈도를 기반으로 학습을 합니다. 하지만 그렇기 때문에 발생하는 문제들이 있는데, N-gram을 다루면서 (링크) 모델의 한계를 살펴봤습니다.그렇다면 대안은 무엇일까요? 인공신경망에 대한 연구와 컴퓨팅 능력이 향상되면서 뉴럴 네트워크로 학습하는 언어모델도 있습니다. 여기서는 글자(Character) 단위 언어 모델인 Char-RNN에 대해 살펴보고자 합니다.

 

우선, RNN에 대해 생각해보겠습니다. RNN은 (t+1)에 대한 예측을 하기 위해 t까지 결과를 이용하여 순환적으로 예측에 사용합니다. 이는 다시 다음인 (t+2)에 영향을 미치는 과정을 끊임없이 반복하는 구조입니다.

그럼 이를 확장해서 문자단위 RNN 모델(Char-RNN)을 생각하게 보겠습니다. Char-RNN은 이전 시점의 예측문자를 다음 시점의 입력으로 사용하게 됩니다. 그렇기 때문에 학습을 위해서는 타겟을 입력값을 한 글자씩 뒤로 밀린 형태로 구성해주면 됩니다. 가령, "HELLO"를 학습시키고 싶은 경우 아래 표와 같이 구성이 될 것입니다.

입력 타겟
H E
E L
L L
L O

Char-RNN의 출력전체 문자 집합에 대한 softmax 출력값이 됩니다. 즉, 영문을 기준으로 할 때 26개의 알파벳에 대하여 확신의 정도를 나타내는 26 x 1의 행렬이 출력값이 되고, 가장 높은 확률의 문자를 다음에 올 글자로 예측하는 과정을 거칩니다.

 

다음은 구글에서 제공하고 있는 셰익스피어 데이터셋을 이용해 텍스트를 생성하는 간단한 프로젝트를 통해 Char-RNN을 구현하는 것을 실습해보고자 합니다. 내용이 꽤 길수 있으니 원하는 것만 찾고 싶으신 경우 옆의 TOC 스크롤에서 선택해 이동하시기 바랍니다. 순서는 아래와 같이 구현합니다.

 

(1) 준비 및 데이터 적재

(2) 데이터 전처리

(3) 모델링

(4) 훈련

(5) 텍스트 생성


(1) 준비 및 데이터 적재

기본적으로 필요한 라이브러리 및 데이터를 가져옵니다. 데이터는 Google에서 제공하고 있는 텍스트를 활용합니다. 여기서 사용하는 코드의 원본은 아래 링크를 기반으로 구성하였습니다.

Text Generation_Shakespeare

import tensorflow as tf
import numpy as np
import os
import time

data_dir = tf.keras.utils.get_file('shakespeare.txt', 'https://storage.googleapis.com/download.tensorflow.org/data/shakespeare.txt')
text = open(data_dir, 'rb').read()
vocab = sorted(set(text)) # unique characters

 

(2) 데이터 전처리

다음은 데이터 전처리 과정입니다. 우선 텍스트에서 문자 집합을 만든 후 인덱스를 부여합니다. 이렇게 부여된 인덱스를 기반으로 정수 인코딩을 실시합니다.

 

다음은 훈련 시퀀스를 생성합니다. 앞서 설명한 것처럼 훈련을 위한 입력값은 한칸씩 밀린 상태로 타겟데이터를 생성합니다. (split_input_target 함수)

# 문자열로 문자 집합 만들기 (토크나이징을 위한 딕셔너리)
char2idx = {char: ind for ind, char in enumerate(vocab)}
idx2char = np.array(vocab)

# 정수 인코딩
text_as_int = np.array([char2idx[c] for c in text])
char_dataset = tf.data.Dataset.from_tensor_slices(text_as_int) # train dataset 생성
sequences = char_dataset.batch(seq_length+1, drop_remainder=True) # 새로운 데이터셋 생성

# split_input_target 함수를 이용해서 input 데이터와 input 데이터를 한글자씩 뒤로 민 target 데이터를 생성합니다.
def split_input_target(chunk):
  input_text = chunk[:-1]
  target_text = chunk[1:]
  return input_text, target_text

dataset = sequences.map(split_input_target)

마지막으로 훈련데이터를 배치 형태로 가져옵니다.

# tf.data API를 이용해서 데이터를 섞고 batch 형태로 가져옵니다.
batch_size = 64
buffer_size = 10000
dataset = dataset.shuffle(10000).batch(batch_size, drop_remainder=True)

(3) 모델링

구현하고자 하는 Char-RNN은 아래와 같은 구조로 되어 있습니다. 이를 Subclassing을 활용해 구현합니다. (TensorFlow API 구현에 대해 살펴보기 위해서는 다음 링크를 통해 확인해주세요)

RNN 구조

여기에 손실함수 및 최적화를 위한 옵티마이저(Adam)까지 추가해 줍니다. 코드는 아래와 같이 구현하면 됩니다.

seq_length = 100     
batch_size = 64
embedding_dim = 256  
hidden_size = 1024   
num_epochs = 10
vocab_size = len(vocab)

# Char-RNN Model - Subclassing API
class RNN(tf.keras.Model):
 def __init__(self, batch_size):
   super(RNN, self).__init__()
   self.embedding_layer = tf.keras.layers.Embedding(vocab_size, embedding_dim,
                                                    batch_input_shape=[batch_size, None])
   self.hidden_layer_1 = tf.keras.layers.LSTM(hidden_size,
                                             return_sequences=True,
                                             stateful=True,
                                             recurrent_initializer='glorot_uniform')
   self.output_layer = tf.keras.layers.Dense(vocab_size)

 def call(self, x):
   embedded_input = self.embedding_layer(x)
   features = self.hidden_layer_1(embedded_input)
   logits = self.output_layer(features)

   return logits

# sparse cross-entropy loss function
def sparse_cross_entropy_loss(labels, logits):
  return tf.reduce_mean(tf.keras.losses.sparse_categorical_crossentropy(labels, logits, from_logits=True))

# 최적화를 위한 Adam 옵티마이저를 정의합니다.
optimizer = tf.keras.optimizers.Adam()

# 최적화를 위한 function을 정의합니다.
@tf.function
def train_step(model, input, target):
  with tf.GradientTape() as tape:
    logits = model(input)
    loss = sparse_cross_entropy_loss(target, logits)
  grads = tape.gradient(loss, model.trainable_variables)
  optimizer.apply_gradients(zip(grads, model.trainable_variables))

  return loss

 

(4) 훈련

훈련은 위에서 작성한 클래스를 활용해 컴파일(optimizer, loss function 설정) 후 fit 메서드를 통해 훈련해주는 것이 전부입니다. (물론 더 세부적으로 접근도 가능하지만, 여기서는 최대한 간단한 방법을 사용했습니다.) 

# 훈련
RNN_model = RNN(batch_size=batch_size)
RNN_model.compile(
    optimizer=optimizer,
    loss=sparse_cross_entropy_loss
)
history1 = RNN_model.fit(x=dataset, epochs= 40)

훈련이 잘 이뤄졌나를 확인하기 위해 손실값이 어떻게 변화하는지 살펴봤습니다. 에포크가 진행됨에 따라 잘 내려가고 있는 것을 확인할 수 있네요.

import matplotlib.pyplot as plt

def render_training_history(training_history):
    loss = training_history.history['loss']
    plt.title('Loss')
    plt.xlabel('Epoch')
    plt.ylabel('Loss')
    plt.plot(loss, label='Training set')
    plt.legend()
    plt.grid(linestyle='--', linewidth=1, alpha=0.5)
    plt.show()

render_training_history(history1)

(5) 텍스트 생성

이렇게 훈련한 모델을 바탕으로 텍스트를 생성하고자 합니다. 먼저 텍스트를 생성하나는 함수(generate_text)를 작성합니다. 그리고 앞서 훈련한 모델의 가중치를 가져와 적용해 샘플링을 해줍니다.

def generate_text(model, start_string):
  num_sampling = 1000  # 생성할 글자 수
  temperature = 1.0 # temperature 값에 따라 낮으면 정확한, 높으면 다양한 텍스트 생성

  # start_sting을 integer 형태로 변환(Vectorizing)
  input_indices = [char2idx[s] for s in start_string]
  input_indices = tf.expand_dims(input_indices, 0)

  text_generated = []
  model.reset_states()
  for i in range(num_sampling):
    predictions = model(input_indices)
    # 불필요한 batch dimension 삭제
    predictions = tf.squeeze(predictions, 0) 

    # 모델의 예측결과에 기반해서 랜덤 샘플링을 하기위해 categorical distribution을 사용
    predictions = predictions / temperature
    predicted_id = tf.random.categorical(predictions, num_samples=1)[-1,0].numpy()

    # 예측된 character를 다음 input으로 사용
    input_indices = tf.expand_dims([predicted_id], 0)

    text_generated.append(idx2char[predicted_id])

  return (start_string + ''.join(text_generated))

sampling_RNN_model = RNN(batch_size = 1)
sampling_RNN_model.load_weights(tf.train.latest_checkpoint(checkpoint_dir))
sampling_RNN_model.build(tf.TensorShape([1, None]))

print(generate_text(sampling_RNN_model, start_string=u' ')

https://colab.research.google.com/github/trekhleb/machine-learning-experiments/blob/master/experiments/text_generation_shakespeare_rnn/text_generation_shakespeare_rnn.ipynb

반응형
반응형

1. N-gram 모델

N-gram 모델은 총 N개의 단어의 그룹 내에서 N번째 단어 이전의 (N-1)개의 단어에 기반해 다음에 올 단어를 예측하는 모델입니다. 단어들의 집합에 확률을 부여하는 방식으로 다음에 올 단어를 맞추게 되는데, 기본적으로 조건부확률을 기반으로 추정하게 됩니다. 이에 대한 수학식은 다음과 같습니다.

N-gram 모델은 몇 가지 문제점을 가지고 있습니다.

  • 데이터셋에 존재하지 않은 단어의 조합은 추정이 어렵다
  • N 개수를 증가시키는 것은 모델의 성능을 높일 수 있지만, 모델 사이즈 증가에 따른 비용 문제가 된다

그래서 최근에는 이런 문제들을 해결하기 위해 딥러닝에 기반한 언어모델들이 주로 사용되고 있습니다. 이는 추후 미래의 글에서 다루도록 하겠습니다.

2. N-gram 모델 구현

N-gram 모델은 NLTK(Natural Language Toolkit) 라이브러리를 대표로 하여 구현할 수 있습니다다. (아래 링크 참조)
NLTK Libarary

다음은 바이그램(2글자)을 구현한 코드입니다. bigrams 함수는 입력은 시퀀스(문자열, 리스트, 튜플)을 받아 바이그램을 리스트를 반환합니다. 바이(bi)그램은 2개의 글자를 뜻하므로, 아래 출력값처럼 2개의 글자씩 묶여있는 것을 확인할 수 있습니다.

from nltk.util import bigrams
text = 'abc'
list(bigrams(text))

같은 방식으로 nltk에서 제공하는 ngrams 함수를 이용해 구현할 수 있습니다.

from nltk.util import ngrams
list(ngrams(text, n=2))

문장의 끝을 표시하는 것은 문장단위 해석을 위해서 필요할텐데, 이를 위한 기능이 nltk에서 제공됩니다. 문장의 시작과 끝을 나타내는 것을 추가해주는 것입니다. 사용하기 위한 예제코드는 다음과 같습니다.

from nltk.util import pad_sequence
list(pad_sequence(text,
                  pad_left=True, left_pad_symbol="<s>",
                  pad_right=True, right_pad_symbol="</s>",
                  n=2))

또는 pad_both_ends를 통해 같은 기능을 수행할 수도 있습니다.

from nltk.lm.preprocessing import pad_both_ends
list(pad_both_ends(text, n=2))

 

 

우리가 원하는 모델은 이상치에도 영향을 덜 받기 원합니다. 그래서 단일 단어를 기준으로도 훈련을 할 수 있는데, nltk에서는 이를 everygrams라는 함수를 통해서 가능합니다.

from nltk.util import everygrams
padded_bigrams = list(pad_both_ends(text, n=2))
list(everygrams(padded_bigrams, max_len=2))

여기서 문자를 하나의 흐름으로 펼치기 위해 사용하는 것은 flatten 함수입니다. 사용방법은 다음과 같습니다.

from nltk.lm.preprocessing import flatten
list(flatten(pad_both_ends(sent, n=2) for sent in text))

위 작업들을 한꺼번에 수행할 수 있습니다.

from nltk.lm.preprocessing import padded_everygram_pipeline
train, vocab = padded_everygram_pipeline(2, text)

3. 훈련

MLE(Maximum Likelihood Estimation)는 한글로 최대우도추정이라고 하는데, 이는 원하는 값이 나올 가능성(우도)을 최대로 만드는 모수를 추정하는 기법입니다. 위에서 준비된 데이터를 바탕으로 MLE를 통해 훈련을 진행합니다.

from nltk.lm import MLE
lm = MLE(2) # bigram
lm.fit(train, vocab)

또한, 아래는 nltk 언어모델의 몇 가지 메서드를 정리한 것 입니다.

# 'a' 개수 세기
lm.counts['a']
# 'a'가 들어간 것 중 'b' 개수 세기. 즉, Count('b'| 'a')
lm.counts[['a']]['b']
# 'a'가 있을 확률
lm.score('a')
# 'b'가 'a'보다 앞에 올 확률
lm.score('b', ['a'])

4. 생성

이렇게 만들어진 n-gram 모델을 통해서 새로운 텍스트를 생성 가능합니다.

print(lm.generate(20, random_seed=123))
반응형
반응형

딥러닝의 간단한 개념

딥러닝 모델을 구현하기에 앞서서 딥러닝이란 무엇인가에 대해 살펴볼 필요가 있습니다. 딥러닝은 여러 연산의 조합을 깊게(길게) 연결된 모델을 통해 문제를 풀고자 하는 머신러닝 방법 중 하나입니다. 여기서 여러 연산의 조합은 1957년에 제안된 퍼셉트론("TLU"라고도 불림)이라는 개념에서 유래하는데, 입력(x)과 가중치의 곱셈의 합(선형결합)을 계산해 비선형함수에 대입하면서 문제를 풉니다. 이에 대한 식은 아래와 같습니다.

이런 TLU들이 층을 쌓아올리게 되면 일부 제약을 줄일 수 있다는 사실이 밝혀졌는데, 이를 다층퍼셉트론(MLP)라고 합니다. 그리고 이들이 모두 연결되어 있을 때, 즉 누군가의 결과값이 다음 층의 입력값인 구조에서 우리는 밀집층이라고 부릅니다.

각각의 TLU는 뉴런 층의 한 형태로 작용할 수 있습니다. 인공신경망(Artificial Neural Network; ANN)은 생물학적 신경망을 모사하여 형상화한 네트워크로 여기서는 TLU(더 포괄적인 개념으로는 뉴런층)을 서로 연결하여 구성하게 됩니다.

MLP는 입력층, 출력층, 그리고 나머지 사이의 은닉층(hidden layer)로 구분됩니다. 여기서 은닉층을 여러개 쌓아올린 인공신경망을 심층신경망(Deep Neural Network; DNN)이라고 하는데, 여기서 우리가 흔히 사용하는 딥러닝의 개념도 등장합니다. 즉, 쌓아올린 DNN에 훈련을 통해 학습시키고 연구하는 것을 딥러닝이라고 합니다.

딥러닝 모델을 구현하기에 앞서 간단한 개념은 위에서 살펴봤고, 파이썬 코드를 작성할 준비는 아래와 같이 시작할 수 있습니다.

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

딥러닝을 파이썬을 이용해 구현하기 위해서는 대표적인 라이브러리가 여럿(Keras, TensorFlow, Pytorch 등)이 있는데 그중에서도 여기서는 TensorFlow2(이하 "TF")를 활용할 예정입니다. TF는 케라스를 자체적으로 구현하여 백엔드로 지원합니다. 이에 대해 더 궁금한 사항은 아래 링크에서 살펴보시기 바랍니다.

텐서플로우 블로그 : 케라스 소개

케라스를 통해 딥러닝을 구현하는 방법을 크게 세가지(Sequential API, Functional API, Subclassing API)로 구분하고 각각에 대해 살펴보고자 합니다.

시퀀셜 API (Sequential API)

시퀀셜 API는 이름에서 알 수 있듯이 여러 층을 단순히 일렬로 연결하여 이루어진 모델입니다. 즉, 하나의 입력(input)과 하나의 출력(output) 텐서만을 갖는 경우에 구현이 가능합니다. 비교적 단순한 구조인만큼 구현하기 쉽지만 아래의 경우에는 사용하기에 적절하지 않습니다.

  • 입력층 / 출력층에 여러개의 입력 / 출력이 필요한 경우
  • 은닉층 중 일부라도 여러개의 입력 / 출력이 필요한 경우
  • 선형적 토폴로지를 갖지 않는 경우 (즉, cycle과 같은 복잡한 형태가 포함된 모델인 경우)

시퀀셜 API에 대한 구현은 다음과 같습니다.

model = keras.Sequential(
    [
        layers.Dense(2, activation="relu", name="layer1"),
        layers.Dense(3, activation="relu", name="layer2"),
        layers.Dense(4, name="layer3"),
    ]
)

또 다른 방식으로는 add 메서드를 통해 모델을 구현할 수 있습니다.

model = keras.Sequential()
model.add(layers.Dense(2, activation="relu"))
model.add(layers.Dense(3, activation="relu"))
model.add(layers.Dense(4))

더 세부적인 내용들은 아래 링크를 통해 확인이 가능합니다.
TensorFlow : The Sequential Model

함수형 API (Functional API)

함수형 API는 앞선 시퀀셜보다는 좀 더 유연한 적용이 가능합니다. 이름 그대로 어느 층을 함수의 입력값으로하여 결과값을 출력하는 방식으로 구현합니다. 기본적인 방법은 다음 예제 코드를 통해 확인할 수 있습니다.

# inputs은 사전에 정의됨
dense = layers.Dense(64, activation="relu")
x = dense(inputs)
x = layers.Dense(64, activation="relu")(x)
outputs = layers.Dense(10)(x)
model = keras.Model(inputs=inputs, outputs=outputs, name="mnist_model")

위에서 확인할 수 있듯이 함수형으로 구현하는 것은 y = f(x)처럼 작성해주면 됩니다. 더 많은 내용과 모델들을 통해 연습하고 싶다면, 아래 링크를 참조해주시기 바랍니다.
TensorFlow : The Functional API

서브클래싱 API (Subclassing API)

두 구현방법은 모두 사용할 층과 연결방식을 먼저 정의하는 것이 필요합니다. 모델을 분석 / 디버깅이 쉬우며, 다른 곳에서 사용하기도 복사나 공유를 통해서 쉽게 가능할 것 입니다. 하지만, 어떤 모델에서는 반복 / 조건문이 필요하거나 다양한 크기를 다뤄야 할 수도 있습니다. 이런 상황에서는 가장 유연하게 할 수 있는 동적인 구조가 필요합니다. 이처럼 명령형(imperative) 프로그래밍이 필요하다면 이용하는 것이 서브클래싱 API 입니다. 아래의 한 예시를 통해 확인하시기 바랍니다.

class Linear(keras.layers.Layer):
    def __init__(self, units=32, input_dim=32):
        super(Linear, self).__init__()
        w_init = tf.random_normal_initializer()
        self.w = tf.Variable(
            initial_value=w_init(shape=(input_dim, units), dtype="float32"),
            trainable=True,
        )
        b_init = tf.zeros_initializer()
        self.b = tf.Variable(
            initial_value=b_init(shape=(units,), dtype="float32"), trainable=True
        )

    def call(self, inputs):
        return tf.matmul(inputs, self.w) + self.b
반응형
반응형

에라토스테네스의 체는 고대 그리스의 수학자 에라토스테네스가 발견한 소수(prime number)를 찾는 방법입니다. 수학시간은 아니므로 소수냐 합성수냐에 대한 정의 설명은 생략하고, 어떤 방식으로 동작하는지 살펴보겠습니다.

 

1. 소수를 찾고자 하는 구간의 모든 수를 나열합니다.

2. 어느 한 소수의 배수를 모두 지웁니다.

3. 남아있는 수 가운데 다음 소수를 찾습니다. 

4. 2-3 과정을 반복합니다.

 

여기서 2가지 포인트가 있습니다. 먼저, 위 과정에서 2단계의 최초 시작은 2부터 합니다. (왜냐하면 1은 소수도 합성수도 아닌 수이기 때문입니다) 그다음은 3, 5, 7.... 순으로 커지게 됩니다. 

 

두번째는 최대 약수는 어느 숫자의 제곱근을 넘을 수 없다는 사실입니다. 정수론 시간이 아니므로 엄밀한 증명은 넘어가더라도, 제곱근 값을 넘어가게 된다면 나머지 다른 약수는 제곱근보다 작은 숫자일 것입니다. 작은 숫자는 이미 카운트가 됐을 것이므로, 따라서 제곱근을 넘을 수 없는 것으로 셀 수 있을 것입니다.

 

이를 코드로 구현하면 다음과 같습니다.

 

# 에라토스테네스의 체
def eratos(n):
    sieve = [True] * n
    gcd = int(n ** 0.5)

    for i in range(2,  gcd+1):
        if sieve[i] == True:
            for j in range(i+i, n, i):
                sieve[j] = False   
    return [i for i in range(2, n) if sieve[i] == True]

print(eratos(20))

 

아래 사진을 보면, 잘 동작하는 것을 확인할 수 있습니다.


참고:

 

1. Wikipedia, "에라토스테네스의 체" 

https://ko.wikipedia.org/wiki/%EC%97%90%EB%9D%BC%ED%86%A0%EC%8A%A4%ED%85%8C%EB%84%A4%EC%8A%A4%EC%9D%98_%EC%B2%B4

 

에라토스테네스의 체 - 위키백과, 우리 모두의 백과사전

수학에서 에라토스테네스의 체는 소수를 찾는 방법이다. 고대 그리스 수학자 에라토스테네스가 발견하였다. 알고리즘[편집] 2부터 소수를 구하고자 하는 구간의 모든 수를 나열한다. 그림에서

ko.wikipedia.org

 

반응형
반응형

위키피디아를 참고하면 다이나믹 프로그래밍, 다른 말로 동적계획법은 복잡한 문제를 간단한 여러 개의 문제로 나누어 푸는 것을 의미합니다. 한편, 알고리즘 스터디를 위해 참고하는 블로그(안경잡이 개발자)에서는 '하나의 문제를 단 한 번만 풀도록 하는 알고리즘'이라고 정의하고 있습니다.

 

일반적으로 분할 정복 기법은 동일한 문제를 다시 풀기 때문에 비효율적입니다. 그러나 다이나믹 프로그래밍은 하나의 문제를 한 번만 풀도록 하기 때문에 비효율적인 알고리즘을 개선시킬 수 있습니다. 구현에 대한 사례로 피보나치 수열을 활용하겠습니다. 중/고등학교 수학시간에서 배운 것처럼, 피보나치 수열은 어느 숫자를 구하기 위해서 그 앞과 앞앞 자리의 숫자를 더하면서 구해가는 수열입니다. 이를 식으로 나타내보면 다음과 같습니다. 

 

a(n)  = a(n-1) + a(n-2)

 

여기서 a(n)을 계산하기 위해서는 a(n-1)과 a(n-2) 값이 필요한데, 이를 해결하기 위해서는 해당 값들을 특정 메모리에 저장해서 나중에 필요할 때 꺼내쓰는 방식이 필요합니다. 이를 메모이제이션(memoization)이라고 하며, 동일한 연산을 반복할 때 새로 연산이 필요없이 꺼내쓰기만 해서 불필요한 행위를 줄이는 것입니다.

 

(쉽게 말해, 옆에 메모해두고 필요할때마다 보고 사용하는 것입니다)

Photo by Kaleidico on Unsplash

이를 구현하는 것은 그렇게 어렵지 않습니다. 

# Fibonacci

d = [0] * 100

def fibo(x):
    if (x == 1):
        return 1
    if (x == 2):
        return 1
    if (d[x] != 0):
        return d[x]
    d[x] = fibo(x-1) + fibo(x-2)
    return d[x]

print(fibo(30))

피보나치수열로 30번이면 꽤 많은 연산을 수행했는데도, 제법 빠르게 나오는 것을 확인할 수 있습니다. 


참고:

1. Wikipedia, 동적계획법(Dynamic Programming)

https://ko.wikipedia.org/wiki/%EB%8F%99%EC%A0%81_%EA%B3%84%ED%9A%8D%EB%B2%95

 

동적 계획법 - 위키백과, 우리 모두의 백과사전

수학과 컴퓨터 과학, 그리고 경제학에서 동적 계획법(動的計劃法, dynamic programming)이란 복잡한 문제를 간단한 여러 개의 문제로 나누어 푸는 방법을 말한다. 이것은 부분 문제 반복과 최적 부분

ko.wikipedia.org

2. 안경잡이 개발자, "20. 다이나믹 프로그래밍 (Dynamic Programming)"

https://blog.naver.com/ndb796/221233570962

 

20. 다이나믹 프로그래밍(Dynamic Programming)

다이나믹 프로그래밍은 프로그래밍 대회를 준비하시는 분에게는 절대 피할 수 없는 숙명입니다. 다이나믹 ...

blog.naver.com

 

반응형
반응형

이전 글에서 이진트리에 대해서 알아봤습니다. 이진트리 자료구조에 대한 사항은 아래 글을 참고하시기 바랍니다. 

 

https://seanpark11.tistory.com/82

 

이러한 트리 구조에서 각가의 노드를 정확히 한번만 방문하는 것을 체계화한 것이 트리순회(Tree Traveral, 줄여서 순회)입니다. 여기서 구현해볼 순회의 방법은 3가지 입니다 : 전위순회(Preorder Traversal), 중위순회(Inorder Traversal), 후위순회(Postorder Traversal). 사실은 더 많은 순회에 대한 내용들이 있지만, 편의상 이 세가지 내용만 다루고자 합니다.

 

먼저 전위순회는 다음과 같은 순서입니다.

  1. 노드를 방문
  2. 왼쪽 자식을 전위순회
  3. 오른쪽 자식을 전위순회

앞서 언급한 트리 예시를 사용하면, 방문순서는 1-2-4-5-3-6-7 입니다. 이를 코드로 구현하면 다음과 같습니다.

 

class BinaryTree():
    def __init__(self):
        self.root = None
        
    # 전위 순회
    def preorder(self, n):
        if n != None:
            print(n.item, ' ', end='')
            if n.left:
                self.preorder(n.left)
            if n.right:
                self.preorder(n.right)


# 전위 순회 출력
print("전위 순회 : ")
bt.preorder(bt.root)
print("\n")

 

 

다음은 중위순회 입니다.

  1. 왼쪽 자식을 중위 순회
  2. 노드를 방문
  3. 오른쪽 자식을 중위 순회

동일한 트리 예시를 사용하면, 중위순회의 방문순서는 2-4-5-1-3-6-7 입니다. 이를 코드로 구현하면 다음과 같습니다.

class BinaryTree():
    def __init__(self):
        self.root = None

    # 중위 순회
    def inorder(self, n):
        if n != None:
            if n.left:
                self.preorder(n.left)
            print(n.item, ' ', end='')
            if n.right:
                self.preorder(n.right)
                
# 중위 순회 출력
print("중위 순회 : ")
bt.inorder(bt.root)
print("\n")

 

마지막은 후위순회는 다음과 같은 순서로 방문합니다.

  1. 왼쪽 자식을 후위순회
  2. 오른쪽 자식을 후위순회
  3. 노드를 방문

마찬가지로 예시를 살펴보면, 후위순회의 방문순서는 2-4-5-3-6-7-1 입니다. 이를 코드로 구현하면 아래와 같습니다. 

class BinaryTree():
    def __init__(self):
        self.root = None

    # 후위 순회
    def postorder(self, n):
        if n != None:
            if n.left:
                self.preorder(n.left)
            if n.right:
                self.preorder(n.right)
            print(n.item, ' ', end='')

# 후위 순회 출력
print("후위 순회 : ")
bt.postorder(bt.root)
print("\n")

 


참고 내용

 

1. 안경잡이 개발자, "19. 이진 트리의 구현과 순회(Traversal)",

https://m.blog.naver.com/PostView.naver?blogId=ndb796&logNo=221233560789&navType=by 

 

19. 이진 트리의 구현과 순회(Traversal) 방식

기본적으로 가장 많이 사용되는 비선형 자료구조는 이진 트리(Binary Tree)입니다. 이진 트리는 트리 자...

blog.naver.com

2. Wikipedia, "트리 순회",

https://ko.wikipedia.org/wiki/%ED%8A%B8%EB%A6%AC_%EC%88%9C%ED%9A%8C

 

트리 순회 - 위키백과, 우리 모두의 백과사전

전산학에서 트리 순회(Tree traversal)는 트리 구조에서 각각의 노드를 정확히 한 번만, 체계적인 방법으로 방문하는 과정을 말한다. 이는 노드를 방문하는 순서에 따라 분류된다. 여기서 설명하는

ko.wikipedia.org

 

 

 

 

 

 

 

반응형
반응형

이진트리(Binary Tree)는 각가의 노드가 최대 두개의 자식 노드를 가지는 트리 자료구조 입니다. 왼쪽에 있는 자식노드를 '왼쪽 자식 노드', 오른쪽에 있는 자식노드를 '오른쪽 자식 노드'로 구분합니다. (약간 말장난같지만, 이 둘은 분명히 다른 노드이기 때문에 다른 용어가 필요합니다) 엄밀한 의미의 정의는 아니지만 이런 형태의 정의도 이해하기 충분하니, 다음은 몇 가지 용어로 넘어가겠습니다.


노드(Node) : 데이터를 저장하는 기본 요소

부모 노드(Parent Node) : 어느 노드를 자식으로 갖는 노드

자식 노드(Child Node) : 어느 노드를 부모로 갖는 노드

형제 노드(Sibling Node) : 부모가 같은 노드

차수(Degree) : 자식의 수

루트 노드(Root Node) : 트리 맨 위에 있는 노드

깊이(Depth) : 루트 노드에서 어느 노드까지 가는 경로의 길이 

레벨(Level) : 루트 노드에서 어느 노드까지 가는 경로의 길이 +1

 

다음은 실제 사례를 보면서 파이썬으로 구현해보겠습니다. 아래 그림과 같은 이진트리를 생각해보겠습니다. 

 

1은 루트 노드이고, 왼쪽 자식 노드는 2 오른쪽 자식 노드는 3을 가진 것을 알 수 있습니다. 그 아래로 차례로 2는 4, 5를 3은 6, 7을 각각 왼쪽 자식 노드, 오른쪽 자식 노드로 갖습니다. 이를 파이썬으로 구현하면 다음과 같습니다.

class Node:
    def __init__(self, item):
        self.item = item
        self.left = None
        self.right = None

class BinaryTree():
    def __init__(self):
        self.root = None
    
n1 = Node(1)
n2 = Node(2)
n3 = Node(3)
n4 = Node(4)
n5 = Node(5)
n6 = Node(6)
n7 = Node(7)

bt = BinaryTree()
bt.root = n1
n1.left = n2
n1.right = n3
n2.left = n4
n2.right = n5
n3.left = n6
n3.right = n7

 

참고자료

 

1. Wikipedia, 이진트리(Binary Tree) 

https://ko.wikipedia.org/wiki/%EC%9D%B4%EC%A7%84_%ED%8A%B8%EB%A6%AC

 

이진 트리 - 위키백과, 우리 모두의 백과사전

크기가 9이고, 높이가 3인 이진 트리 컴퓨터 과학에서, 이진 트리(二進-, 영어: binary tree)는 각각의 노드가 최대 두 개의 자식 노드를 가지는 트리 자료 구조로, 자식 노드를 각각 왼쪽 자식 노드와

ko.wikipedia.org

2. 

반응형