no image
[PyTorch] 파이토치 메서드로 텐서의 연산 수행하기
1. 텐서 안의 기본연산우선 코드를 실행하기 위한 PyTorch를 불러온다. 실행한 환경은 Google Colab에서 아래와 같은 버전의 Python과 PyTorch를 사용했다. import torchimport sysprint("Python version : ", sys.version)print("PyTorch version : ", torch.__version__) >>> Python version : 3.10.12>>> PyTorch version : 2.3.1.+cu1211.1.  요소들의 합텐서를 이루는 요소들의 합을 구하기 위해서는 torch.sum(Tensor, dim) 또는 Tensor.sum(dim)를 사용할 수 있다. dim에 특별한 값을 넣지 않은 경우 전체 요소들의 계산 값을 반환한..
2024.08.16
no image
[PyTorch] PyTorch를 활용한 텐서 모양 바꾸기
1. Version 우선 코드들을 실행하기 위한 PyTorch를 불러오고, 사용하고 있는 Python과 PyTorch 버전은 아래와 같다. import torchimport sysprint("Python version : ", sys.version)print("PyTorch version : ", torch.__version__)  2. 텐서 모양 바꾸기 2.1. view 와 reshape텐서 모양을 바꾸는 대표적인 메서드는 view()와 reshape()이 있다. 두 메서드 모두 Tensor.view(Size) / Tensor.reshape(Size) 형태로 메서드를 사용하며, 조작하고 싶은 텐서의 사이즈를 입력받아 텐서의 모양을 바꿔준다. 다만, view는 메모리가 인접하게 연속적(contiguous..
2024.08.09
no image
[PyTorch] PyTorch를 활용한 텐서 생성하기
1. 개요1.1. PyTorch 개요PyTorch는 대표적인 Python을 위한 딥러닝 프레임워크로 Facebook에서 개발을 진행했다. Google에서 개발한 또 다른 프레임워크인 TensorFlow도 있는데, 둘 다 많이 사용된다. 실제로 갖고 있는 책(Hands-On Machine Learning)의 경우 TensorFlow를 기반으로 설명하고 있고, 현재 참여하고 있는 교육과정이나 다른 강의들에서는 PyTorch를 기반으로 설명하고 있다.  PyTorch의 대표적인 특징은 동적 계산 그래프(Define-by-Run)의 형태를 띈다는 점이다. '계산 그래프'는 일련의 연산 과정을 그래프로 나타난 것[2]이고, '동적'의 의미는 계산 그래프의 생성 단계와 훈련 데이터를 입력하고 손실함수를 계산하는 단..
2024.08.09
no image
[NLP] 임베딩과 Word2Vec 실습해보기
1. 임베딩원핫인코딩(One-Hot-Encoding)은 머신러닝, 그중에서도 범주형 데이터(categorical data)에 많이 사용하는 기법입니다. 예를 들어, 음식에 대한 데이터가 있을 때 종류를 구분한다면 '한식', '중식', '일식', '이탈리아' 등과 같이 구분할 수 있습니다. 원핫인코딩은 이런 상황에서 아래 표처럼 표기하게 됩니다.종류_한식종류_중식종류_일식종류_이탈리아100000100001 하지만 NLP에서 이러한 방법은 효과적이지 않은데, 굉장히 다양한 단어들을 표현해야하는 NLP에서 원핫인코딩은 데이터의 표현이 희소(Sparse)하여 특정 행/열에서 무의미한 0 만 존재하고 이는 데이터가 너무 많아져 비효율적인 연산을 가져올 수 있습니다. 또한, 1과 0으로 구분하는 원핫인코딩으로는 유..
2022.09.07
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
[NLP] Python으로 영어 가독성 테스트하기 | Flesch, Gunning fog
최근에 Datacamp로 python을 공부하던 중 NLP 관련 재밌는 게 하나가 있어서 기록을 남기고자 한다. 한국어에 적용되는 건 아니지만, 영어로 된 문장에 대해서 가독성 테스트(Readability Test)를 하는 것이다. 가독성 테스트에 대해 굳이 설명할 필요는 없겠지만, 우리가 알아보고 싶은 문장이나 글이 얼마나 읽기 쉬운지를 알아보는 것이다. 기본적인 아이디어는 문장을 쪼개서 벡터화한 다음, 그것을 숫자로 해석하는 방향이다. 강의를 들으면서 알았던 케이스인데, 여기서 소개하고자 하는 것은 두 가지(Flesch, Gunning fog)이다. Flesch는 평균적인 문장과 음절이 길다면, 더 어렵다는 내용이다. 어려움에 대해서 측정하는 방법 자체에 대해서는 굉장히 직관적이기 때문에 그렇게 큰 ..
2020.09.28
no image
[Modeling] 파이썬으로 선형계획법 최적화하기 | SciPy
선형계획법. 영어로는 Linear Programming(LP) 라고 한다. 말은 그럴싸해보이고 뭔가 어려워보일 수도 있지만, 수학적인 기초 적용방법에 대해서는 이미 중고등학교 때 일차 부등식의 해를 구하는 단원에서 문제 해결방법에 대해 충분히 학습했다. 지금은 이렇게 중등 수학으로 분류되어 있지만, 2차 세계대전 군수 자원의 효율적 배분 등을 위해서 적극적으로 이용된 꽤나 유서가 깊은 분야이다. 현재도 경영 경제 분야에서 활발히 사용되고 있고, 선형계획법의 특수한 경우인 네트워크 흐름 같은 문제에 대한 알고리즘이 연구되고 있다고 한다. 최근에 하나의 최적화 문제를 시뮬레이션을 해야 했었는데, 그 기반이 되는 논문이 선형계획법을 사용했기 때문에 Python을 통해 시뮬레이션을 했던 내용 중 가장 핵심이 됐..
2020.07.20
반응형

1. 텐서 안의 기본연산

우선 코드를 실행하기 위한 PyTorch를 불러온다. 실행한 환경은 Google Colab에서 아래와 같은 버전의 Python과 PyTorch를 사용했다.

 

import torch
import sys
print("Python version : ", sys.version)
print("PyTorch version : ", torch.__version__)

 

>>> Python version : 3.10.12

>>> PyTorch version : 2.3.1.+cu121

1.1.  요소들의 합

텐서를 이루는 요소들의 합을 구하기 위해서는 torch.sum(Tensor, dim) 또는 Tensor.sum(dim)를 사용할 수 있다. dim에 특별한 값을 넣지 않은 경우 전체 요소들의 계산 값을 반환한다. 

 

a = torch.tensor([[1, 2], [3, 4]])
print(torch.sum(a))
print(torch.sum(a, dim=0))
print(torch.sum(a, dim=1))
print(a.sum())
print(a.sum(dim=0))
print(a.sum(dim=1))

 

1.2.  요소들의 곱

텐서를 이루는 요소들의 곱을 구하기 위해서는 torch.prod(Tensor, dim) 또는 Tensor.prod(dim)를 사용할 수 있다. dim에 특별한 값을 넣지 않은 경우 전체 요소들의 계산 값을 반환한다. 

 

a = torch.tensor([[1, 2], [3, 4]])
print(torch.prod(a))
print(torch.prod(a, dim=0))
print(torch.prod(a, dim=1))
print(a.prod())
print(a.prod(dim=0))
print(a.prod(dim=1))

 

1.3.  요소들의 평균 

텐서를 이루는 요소들의 평균을 구하기 위해서는 torch.mean(Tensor, dim) 또는 Tensor.mean(dim)를 사용할 수 있다. dim에 특별한 값을 넣지 않은 경우 전체 요소들의 계산 값을 반환한다. 

 

a = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32)
print(torch.mean(a))
print(torch.mean(a, dim=0))
print(torch.mean(a, dim=1))
print(a.mean())
print(a.mean(dim=0))
print(a.mean(dim=1))

.

 

1.4.  요소들의 표준편차, 분산 

텐서를 이루는 요소들의 표준편차와 분산을 구하기 위해서 앞에서와 동일하게 메서드(var, std)를 사용할 수 있다. dim에 특별한 값을 넣지 않은 경우 전체 요소들의 계산 값을 반환한다. 표준편차와 분산에서 알아둘 것은 표본(텐서)을 대상으로 계산한다. ([1] & [2])

 

a = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32)
print(torch.std(a))
print(a.std())
print(torch.var(a))
print(a.var())

 

 

1.5.  요소들의 최대, 최소 

텐서를 이루는 요소들의 최대값과 을 구하기 위해서는 앞선 연산들과 동일한 방식으로 메서드(max, min)를 사용하면 된다. dim에 특별한 값을 넣지 않은 경우 전체 요소들의 계산 값을 반환한다. 

 

a = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32)
print(torch.max(a))
print(a.max())
print(torch.min(a))
print(a.min())

 

 

2. 텐서 간 연산

텐서끼리 연산은 산술연산( +, -, x ), 요소 간 비교(<, >, ==), 행렬곱이 있다. 각각 연산에 대해 세부적으로 연산 결과가 어떻게 되는지 살펴보려고 한다. 

 

2.1. 산술연산

텐서 사이에서 덧셈, 뺄셈, 곱셈, 나눗셈, 거듭제곱 등을 지원한다. 파이썬의 기본연산(+, -, *,  /)도 가능하지만, torch 메서드를 통해서도 지원하고 있다. 차원이 다른 스칼라나 벡터들에 대해서도 Broadcasting을 통해 계산이 가능하다. 

 

a = torch.arange(1,5).reshape(2,2)
b = torch.arange(5,9).reshape(2,2)
c = 3
d = torch.tensor([1, 2])
print(a+b)
print(torch.add(a, b))
print(torch.add(a, c))
print(torch.add(a, d))

 

 

또한, 연산에서 알아둬야 할 것 중에 하나는 in-place 알고리즘(제자리 연산)이다. 명칭처럼 메모리를 추가로 할당하지 않고, 기존 메모리에서 제자리 연산을 수행한다는 점이다. in-place 연산은 메서드에 언더바( _ )를 추가하면서 수행할 수 있다. [3]

  

 

제자리 알고리즘 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 컴퓨터 과학에서 제자리(in-place) 알고리즘은 자료 구조를 추가로 사용하지 않고 입력을 변환하는 알고리즘이다. 그러나 보통 추가적인 변수를 위해 약간의 추

ko.wikipedia.org

in-place 연산 수행 방법은 Tensor.add(Tensor) 형태로 수행 가능하다. 여기서 알아둘 점은 in-place 연산은 추가 할당이 없어 메모리 효율적이지만, 후에 배울 autograd와 호환 문제가 발생할 수 있어서 실제 사용에서는 주의가 필요하다.

a = torch.arange(1,5).reshape(2,2)
b = torch.arange(5,9).reshape(2,2)
print(id(a))
print("address(a) : ", id(a))
print("address(in-place addition) : ", id(a.add_(b)))

 

 

더하기 메서드인 add 이외에도, 빼기(sub), 곱하기(mul), 나누기(div), 제곱(pow)를 통해 계산이 가능하며, 또한 in-place 연산 지원을 위해 sub_, mul_, div_, pow_ 역시 가능하다. 

 

2.2. 비교연산 

텐서는 각각 요소들을 비교하는 연산도 존재한다. torch의 메서드(eq, ne, gt, ge, lt, le)를 활용하며, 결과는 Boolean Tensor를 출력한다. 

 

# 비교연산
a = torch.tensor([1, 2, 3, 4])
b = torch.tensor([1, 3, 3, 5])
# == (equal)
print(torch.eq(a, b))
# != (not equal)
print(torch.ne(a, b))
# > (greater than)
print(torch.gt(a, b))
# >= (greater than or equal)
print(torch.ge(a, b))
# < (less than)
print(torch.lt(a, b))
# <= (less than or equal)
print(torch.le(a, b))

 

 

2.3. 논리연산 

텐서 요소들의 Boolean 값을 계산하는 논리연산도 지원한다. 논리연산의 기본은 and, or, not, xor, not을 지원하고 있으며, 이러한 논리연산을 잘 조합하면 NAND, NOR 같은 연산도 만들 수 있다. 참고로 논리연산의 결과값인 진리표를 참고하여 논리연산을 만들면 될 것이다. [4]

 

# 논리연산
x = torch.tensor([True, True, False, False])
y = torch.tensor([True, False, True, False])

print(torch.logical_and(x, y)) # AND
print(torch.logical_or(x,y)) # OR
print(torch.logical_xor(x,y)) # XOR
print(torch.logical_not(x)) # NOT
print(torch.logical_and(x,y).logical_not()) # NAND

 

 

2.4. 행렬 곱

앞에서 수행했던 스칼라 곱이나, element-wise 곱과 다르게 행렬의 일반적인 곱셈은 다르다. [5] 이러한 행렬 곱을 지원하는 방법은 tensor.matmul(Tensor1, Tensor2), Tensor1.matmul(Tensor2), @ 를 통해 가능하다. 

 

# Matrix Multiplication
X = torch.randn(2,3)
Y = torch.randn(3,4)
print(torch.matmul(X, Y))
print(X.matmul(Y))
print(X @ Y)

 

 

3. 텐서의 노름(norm)

수학에서 노름(Norm, 정확한 발음은 아니지만 통상적으로 많이 쓰이는 표현인 '노름' 사용)은 벡터들이 모인 공간에서의 거리를 말한다. 벡터들이 모인 공간에서 차원에 따라 거리의 정의는 다르게 표현되는데, 표현 방식은 아래 위키 링크를 통해 확인하는 것으로 충분할 것 같다.

 

 

Norm (mathematics) - Wikipedia

From Wikipedia, the free encyclopedia Length in a vector space In mathematics, a norm is a function from a real or complex vector space to the non-negative real numbers that behaves in certain ways like the distance from the origin: it commutes with scalin

en.wikipedia.org

 

PyTorch에서 노름은 매우 쉽게 계산할 수 있는데, torch.norm(Tensor, p)의 형태로 계산이 가능하다. 

a = torch.tensor([1, 2, 3, 4], dtype=torch.float32)
# L1 norm
print(torch.norm(a, p=1))
# L2 norm
print(torch.norm(a, p=2))
# L∞ norm
print(torch.norm(a, p=float('inf')))

 

 

4. 참고문헌

[1] https://pytorch.org/docs/stable/generated/torch.std.html

[2] https://pytorch.org/docs/stable/generated/torch.var.html

[3] https://en.wikipedia.org/wiki/In-place_algorithm

[4] https://en.wikipedia.org/wiki/Truth_table

[5] https://ko.wikipedia.org/wiki/%ED%96%89%EB%A0%AC_%EA%B3%B1%EC%85%88

[6] https://en.wikipedia.org/wiki/Norm_(mathematics)

 

 

 

 

 

반응형
반응형

1. Version 

우선 코드들을 실행하기 위한 PyTorch를 불러오고, 사용하고 있는 Python과 PyTorch 버전은 아래와 같다.

 

import torch
import sys
print("Python version : ", sys.version)
print("PyTorch version : ", torch.__version__)

 

 

2. 텐서 모양 바꾸기 

2.1. view 와 reshape

텐서 모양을 바꾸는 대표적인 메서드는 view()와 reshape()이 있다. 두 메서드 모두 Tensor.view(Size) / Tensor.reshape(Size) 형태로 메서드를 사용하며, 조작하고 싶은 텐서의 사이즈를 입력받아 텐서의 모양을 바꿔준다. 다만, view는 메모리가 인접하게 연속적(contiguous)이어야만 사용이 가능하다.

 

 Tip)

  1. 맨 끝 차원의 값에 -1을 넣을 경우 PyTorch에서 알아서 배열 사이즈를 지정해준다.
# view
tensor = torch.arange(12)
print(tensor)
print(tensor.is_contiguous())
tensor.view(4, -1)

 

 

reshape() 메서드는 연속적이지 않은 경우에도 사용이 가능하. 제한이 적어 유연하게 사용할 수 있지만, 텐서 사이즈가 커질수록 성능이 떨이질 수 있으니 주의가 필요하다. 

 

# reshape : non-contiguos에 대해서도 적용하지만, 성능은 떨어짐
tensor_sliced = tensor.view(4, -1)[:,:2]
print(tensor_sliced)
print(tensor_sliced.is_contiguous())
print(tensor_sliced.reshape(2,-1))
print(tensor_sliced.view(2,-1))

 

2.2. 평탄화

딥러닝을 수행하기 위해 입력 데이터의 차원을 맞춰줘야 하는 경우가 발생한다. 이럴 때 torch.flatten(Tensor, start, end) 메서드를 활용해 평탄화를 수행하면 view나 reshape 외에 모양변경을 수월하게 수행할 수 있다.

 

# flatten
tensor_3d = tensor.reshape(2,2,3)
print(tensor_3d)
# 1차원으로 평탄화
print("1차원으로 평탄화 : \n", torch.flatten(tensor_3d))
# n 차원부터 마지막 차원까지 평탄화
n = 1
print("n차원부터 마지막 차원까지 평탄화 : \n", torch.flatten(tensor_3d, n))
# m 차원부터 n차원까지 평탄화
m = 0
n = 1
print("m차원부터 n차원까지 평탄화 평탄화 : \n",torch.flatten(tensor_3d, m, n))

 

2.3. squeeze와 unsqueeze

텐서의 여러 차원 중 사인즈가 1인 차원들에 대해 squeeze() 메서드를 통해 차원을 축소 가능하다. 사용방법은 Tensor.squeeze(d), torch.squeeze(Tensor, d) 모두 가능하다. 기본 값은 사이즈가 1인 모든 차원에 대해 적용하며, 특정 차원(값 또는 튜플)을 지정하면 해당 차원을 축소한다. 만약, 사이즈가 1이 아니면 차원이 축소되지 않는다.

 

# squeeze : size 1을 제거 (만약 없다면, 그대로 유지)
tensor_sq = torch.randn(1,1,4)
print("Squeeze 전 : \n", tensor_sq)
print("Squeeze 후 : \n", torch.squeeze(tensor_sq))
# 특정 차원 d에 대해서 squeeze : d는 int 또는 tuple
d = 0
print("d차원에 대해서 squeeze : \n", torch.squeeze(tensor_sq, d))

 

 

 

squeeze의 반대로 차원을 확장하고자 한다면, unsqueeze 메서드를 사용하면 된다. 사용 방법은 squeeze와 동일하며, 한 차원이 확장되는 효과를 볼 수 있다. 

 

# unsqueeze
tensor = torch.arange(12)
tensor_sliced = tensor.view(4,-1)[:,:2]
print("Unsqueeze 전 : \n", tensor_sliced)
print("0차원 Unsqueeze : \n", torch.unsqueeze(tensor_sliced, 0))
print("1차원 Unsqueeze : \n", torch.unsqueeze(tensor_sliced, 1))

  

2.4. 전치 (transpose)

행과 열을 바꿔서 새로운 행렬을 만드는 전치 행렬(Transposed Matrix)이라는 개념이 있다.[1] 텐서에서도 특정 축끼리 바꿔서 만드는 텐서의 모양을 바꾸는 방법이 있다. Tensor.transpose(m, n)의 형태를 통해 사용이 가능하다.

 

# transpose
# m 차원과 n 차원 바꾸기
print("Transpose 전 : \n", tensor_3d)
m = 0
n = 1
print("Transpose 후 : \n",tensor_3d.transpose(m,n))

 

 

2.5. 텐서 연결 (stack, cat)

우선, 기존에 있던 차원은 그대로 유지하면서 텐서를 연결(붙이는) 방법은 torch.cat(Tensors, dim) 메서드를 활용하는 것이다. Tensors는 일련의 텐서들을 묶음(튜플, 리스트) 형태로 입력하고, dim에는 연결할 차원을 입력한다. cat을 사용하기 위해서는 크기가 같아야 한다. 

 

# cat : 기존 차원에 따라 연결
print(torch.cat((x, y)))
print("Size : ", torch.cat((x, y)).size())
print(torch.cat((x, y), 1))
# 만약 크기가 다르다면, 맞춰주는 과정 필요
print(torch.cat((x, y[:1, :].reshape(2, 1)), 1))

 

 

서로 다른 텐서를 연결하는 방법 중 새로운 차원을 생성하면서 연결하려면 stack을 활용하여 torch.stack(Tensors, dim=)를 통해 사용한다. Tensors는 일련의 텐서들을 묶음(튜플, 리스트) 형태로 입력하고, dim은 새롭게 추가될 차원의 위치를 입력한다. stack 역시 사용하기 위해서는 모든 텐서들이 같은 사이즈여야 한다. [2]

 

# stack
x = torch.arange(1,5).view(2,2)
y = torch.arange(5,9).view(2,2)
z = torch.arange(9,13).view(2,2)
print("dim 0 : \n", torch.stack([x,y,z], dim=0))
print(torch.stack([x,y,z], dim=0).shape)
print("dim 1 : \n", torch.stack([x,y,z], dim=1))
print(torch.stack([x,y,z], dim=1).shape)
print("dim 2 : \n", torch.stack([x,y,z], dim=2))
print(torch.stack([x,y,z], dim=2).shape)

 

2.6. 텐서 확장 (expand, repeat)

크기가 1인 차원이 있는 텐서를 여러개를 붙여 확장하는 방법은 Tensor.expand(size) 형태로 사용한다. 사용하는 방법은 아래와 같이 정리할 수 있다.

 

1) 사이즈가 1이 아닌 차원은 그대로 유지 (-1을 대입하면 유지할 수 있음)

2) 사이즈가 1인 차원은 확장할만큼 사이즈를 대입

3) 새로운 차원으로 확대할 경우 sizes의 가장 앞에 값을 대입 

 

# expand  : tensor 차원 중 일부 크기가 1인 경우 해당 차원의 크기를  확장
x = torch.tensor([[1], [2], [3]])
print(x.shape)
print(x.expand(3, 4))
print(x.expand(3, 4).shape)
print(x.expand(2, -1, 4))
print(x.expand(2, -1, 4).shape)

 

 

차원이 1이라는 제한 조건이 있는 expand 대신에 repeat 메서드를 사용 가능하다. 해당 메서드는 Tensor.repeat(size)로 사용 가능하며, size에 입력된 차원 방향으로 입력된 숫자만큼 복제한다. repeat 메서드의 동작 방식은 데이터를 복사해서 확장하기 때문에 추가 메모리 할당으로 인해 메모리 효율성이 떨어지게 된다.[4]

 

# repeat
x = torch.tensor([1, 2])
print(x.repeat(3,2))
print(x.repeat(3,2).shape)
print(x.repeat(3,2,1))
print(x.repeat(3,2,1).shape)

 

 

3. 참고자료

[1] https://ko.wikipedia.org/wiki/%EC%A0%84%EC%B9%98_%ED%96%89%EB%A0%AC

[2] https://pytorch.org/docs/stable/generated/torch.stack.html

[3] https://pytorch.org/docs/stable/generated/torch.Tensor.expand.html

[4] https://pytorch.org/docs/stable/generated/torch.Tensor.repeat.html

 

4. 수정사항

 

 

 

반응형
반응형

1. 개요

1.1. PyTorch 개요

PyTorch는 대표적인 Python을 위한 딥러닝 프레임워크로 Facebook에서 개발을 진행했다. Google에서 개발한 또 다른 프레임워크인 TensorFlow도 있는데, 둘 다 많이 사용된다. 실제로 갖고 있는 책(Hands-On Machine Learning)의 경우 TensorFlow를 기반으로 설명하고 있고, 현재 참여하고 있는 교육과정이나 다른 강의들에서는 PyTorch를 기반으로 설명하고 있다. 

 

PyTorch의 대표적인 특징은 동적 계산 그래프(Define-by-Run)의 형태를 띈다는 점이다. '계산 그래프'는 일련의 연산 과정을 그래프로 나타난 것[2]이고, '동적'의 의미는 계산 그래프의 생성 단계와 훈련 데이터를 입력하고 손실함수를 계산하는 단계를 같이 진행한다는 뜻이다. Define-by-Run이 동작하는 방식에 대한 개요는 아래 그림과 같다. [3]

Define-by-Run 개요 [3]

 

Define-by-Run의 장점은 계산이 진행되면서 계산 그래프가 새롭게 생성하게 되기 때문에 유연하게 적용이 가능하다.  [4]

1.2. 텐서의 정의 

딥러닝을 배우다보면 텐서(Tensor)라는 개념이 많이 나온다. 학술적으로 엄밀하지는 않지만 위키피디아에서 텐서의 개념에 대해 살펴보면, 텐서는 다중선형대수학에서 다뤄지는 대상이다. [5] 이공계열 대학 교육과정에서 많이 배우는 선형대수학에서 다루는 벡터와 행렬에서 더 나아간 것이라고 생각할 수 있다. 

  • 0차원 : 스칼라
  • 1차원 : 벡터
  • 2차원 : 행렬
  • 3+차원 : 텐서 

2. 텐서의 생성

2.1. Version 

우선 아래에서 나오는 코드는 PyTorch를 불러온 상태에서 실행 가능하므로 import를 통해 가져온다. Python과 PyTorch 버전은 아래와 같다.

 

import torch
import sys
print("Python version : ", sys.version)
print("PyTorch version : ", torch.__version__)

 

2.2. torch.tensor  

텐서는 torch.tensor(데이터) 를 통해 생성 가능하다. 여기서 입력받는 '데이터'는 리스트, 튜플, ndarray(NumPy), 스칼라 값 등이 가능하다. 

 

print(torch.tensor([1,2,3]))

 

 

또한, torch.tensor에서 입력할 수 있는 인자는 데이터 유형을 정할 수 있는 dtype, 기기를 할당하는 device 등이 있다. 

2.3.텐서 데이터 유형 지정

PyTorch에서 사용가능한 데이터 유형은 아래와 같다. [6]

 

Data Type dtype = Legacy
32-bit floating point torch.float32 or torch.float torch.*.FloatTensor
64-bit floating point torch.float64 or torch.double torch.*.DoubleTensor
64-bit complex torch.complex64 or torch.cfloat  
128-bit complex torch.complex128 or torch.cdouble  
16-bit floating point 1 torch.float16 or torch.half torch.*.HalfTensor
16-bit floating point 2 torch.bfloat16 torch.*.BFloat16Tensor
8-bit integer (unsigned) torch.uint8 torch.*.ByteTensor
8-bit integer (signed) torch.int8 torch.*.CharTensor
16-bit integer (signed) torch.int16 or torch.short torch.*.ShortTensor
32-bit integer (signed) torch.int32 or torch.int torch.*.IntTensor
64-bit integer (signed) torch.int64 or torch.long torch.*.LongTensor
Boolean torch.bool torch.*.BoolTensor

 

텐서에 담기는 데이터 유형을 지정하는 방법은 입력 인자에 'dtype ='에 위 표의 두번째 열에 있는 값을 넣어서 최초 데이터 생성에서 데이터 유형을 지정하는 방법이다. 

 

a = torch.tensor([1,2,3])
print(a.dtype)

b = torch.tensor([1,2,3], dtype = torch.float64)
print(b)
print(b.dtype)

 

 

두번째 방법은 '타입캐스팅'이라고 불리는 데이터 유형을 바꾸는 것이다. 텐서에 데이터 타입 메서드(float, double, int, short, long)를 직접 활용하는 방법과 to()메서드에 dtype을 지정하는 방법이 있다. 

 

print(a.dtype)
print(a.float().dtype)
print(a.double().dtype)
print(a.int().dtype)
print(a.short().dtype)
print(a.long().dtype)
print(a.to(dtype = torch.float64).dtype)

 

 

2.4. 0, 1 텐서 생성

모든 값이 0으로 이루어진 텐서는 torch.zeros(텐서 사이즈) 또는 torch.zeros_like(텐서)를 통해 만들 수 있다. 

  • 텐서 사이즈 : 생성하고 싶은 텐서의 사이즈 (예를 들어, 2 x 3 행렬을 원할 경우 2,3  입력)
  • 텐서 : 어느 텐서와 같은 사이즈의 0 텐서를 생성하고 싶을 경우 입력 

둘의 결과값은 거의 유사하지만, zeros_like는 입력받은 텐서의 dtype을 유지하고, zeros는 dtype 기본값 (보통 float32)을 따라가기 때문에 데이터 타입에 민감한 경우 주의해서 사용할 필요가 있다. 물론 메서드의 입력 인자로 dtype = 을 지정해주면 큰 차이 없이 사용할 수 있다. 

 

모든 값이 1로 이뤄진 텐서를 만드는 torch.ones와 torch.ones_like는 0과 사용방법은 동일하다. 

 

# tensor 생성
zero = torch.zeros(2,3)
one = torch.ones(2,3)

print(zero)
print(one)

# tensor를 입력받아 초기화된 tensor 생성
print(torch.ones_like(zero))
print(torch.zeros_like(one))

 

 

2.5. rand 텐서 생성

난수(random number) 생성은 0~1 사이 값으로 이뤄진 난수와 표준정규분포('n'ormal distributio)를 따르는 (크기는 매우 달라질 수 있음) 난수가 존재한다. 여기도 텐서 사이즈를 넣는 것과 텐서를 넣는 방법이 존재한다. 

  • 0~1 : torch.rand(사이즈), torch.rand_like(텐서)
  • 표준정규분포 : torch.randn(사이즈), torch.randn_like(텐서)
# 난수 생성
print(torch.rand(3))
print(torch.randn(3))
print(torch.rand(2,3))

# tensor를 입력받아 난수 생성
print(torch.rand_like(zero))
print(torch.randn_like(zero))

 

 

2.6. torch.arange

파이썬에서는 range() 함수를 통해 특정 범위의 배열을 생성할 수 있다. PyTorch에서도 이와 유사하게 특정 범위의 텐서를 생성하는 arange(start, end, step)으로 생성할 수 있다. 

print(torch.arange(10))
print(torch.arange(1,10,2))

2.7. Empty 텐서 생성과 값 입력 

empty 텐서는 torch.empty(사이즈)를 통해, empty_like(텐서)를 통해 초기화되지 않은(uninitialized) 텐서를 생성할 수 있다. empty 텐서의 효용은 0-텐서, 1-텐서처럼 초기화된 텐서를 생성하는데 사용하는 메모리를 절약할 수 있다는 점이다. 

empty = torch.empty(2,3)
print(empty)

 

이렇게 만들어진 empty 텐서에 특정한 값을 넣는 메서드는 fill_가 있다. 해당 메서드는 별도의 반환값이 존재하지 않고 해당 변수에 값이 입력되는 연산이라 동일한 메모리를 갖는 것을 확인할 수 있다.

# id(obj) : 파이썬 내장함수로 해당 객체(obj)의 메모리 주소를 반환
print("empty id : ", id(empty))
print(empty.fill_(3))
print("empty.fill_ id : ", id(empty.fill_(3)))

 

2.8. 텐서 복사하기 

때로는 똑같은 텐서를 새롭게 만들어서 사용해야 할 수 있다. 복사하는 방법은 clone()과 detach() 메서드 두가지가 있는데, 둘 사이의 중요한 차이점은 detach() 메서드는 기존에 사용하고 있던 계산 그래프에서 분리해서 저장한다는 점이다. 앞서 설명한 것처럼 PyTorch는 Define-by-Run 방식이기 때문에 계산 그래프에서 분리하는 것은 기존 계산 결과에서 분리할 수 있음을 의미합니다. 

 

# 복사하기
a = torch.tensor([1,2,3])
b = a.clone()
print(b)
c = a.detach()
print(c)

2.9. 텐서 정보 확인하기

텐서에 담겨있는 데이터 이외에 연산을 수행하기 위해 중요한 정보 중 하나는 텐서의 사이즈입니다. 텐서의 사이즈를 보는 방법은 텐서의 속성 값인 .shape를 쓰는 방법과 메서드인 .size()를 사용하는 방법이 있습니다. 유사하게, 데이터 타입을 보는 방법도 속성값을 통해 확인할 수 있습니다.

a = torch.tensor([1,2,3])
print(a.size())
print(a.shape)
print(a.dtype)

 

딥러닝 프레임워크의 장점 중 하나는 GPU 연산을 쉽게 지원한다는 것입니다. PyTorch의 주요 객체들은 어느 device에 올라가는지를 입력받기 때문에 속성에 접근함으로써 디바이스를 확인할 수 있습니다. 실행 환경에서는 GPU를 지원하고 있지 않아서 False로 나오는 것을 확인할 수 있습니다. 

print(a.device)
print(torch.cuda.is_available())

 

만약, GPU 지원을 할 경우 아래와 같은 코드를 통해 디바이스를 변경할 수 있습니다. 다만, to()와 cuda()의 미묘한 차이가 있기 때문에 주의는 필요합니다.[7]

# GPU 할당
a.to('cuda')
a.cuda()

# CPU 할당
a.to(device='cpu')
a.cpu()

 

3. 참고자료

[1] 위키백과:PyTorch

[2] https://compmath.korea.ac.kr/deeplearning/BackPropagation.html

[3] https://www.oreilly.com/content/complex-neural-networks-made-easy-by-chainer/

[4] https://wikidocs.net/209706

[5] 위키백과:텐서

[6] https://pytorch.org/docs/stable/tensor_attributes.html#torch.dtype

[7] https://velog.io/@hyungraelee/net.todevice-vs-net.cuda

4. 수정사항

 

 

 

 

 

 

 

 

반응형
반응형

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
반응형
반응형

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
반응형
반응형

최근에 Datacamp로 python을 공부하던 중 NLP 관련 재밌는 게 하나가 있어서 기록을 남기고자 한다. 한국어에 적용되는 건 아니지만, 영어로 된 문장에 대해서 가독성 테스트(Readability Test)를 하는 것이다. 가독성 테스트에 대해 굳이 설명할 필요는 없겠지만, 우리가 알아보고 싶은 문장이나 글이 얼마나 읽기 쉬운지를 알아보는 것이다.

 

기본적인 아이디어는 문장을 쪼개서 벡터화한 다음, 그것을 숫자로 해석하는 방향이다. 강의를 들으면서 알았던 케이스인데, 여기서 소개하고자 하는 것은 두 가지(Flesch, Gunning fog)이다. 

Photo by Patrick Tomasso on Unsplash

Flesch는 평균적인 문장과 음절이 길다면, 더 어렵다는 내용이다. 어려움에 대해서 측정하는 방법 자체에 대해서는 굉장히 직관적이기 때문에 그렇게 큰 어려움은 없을 것이다. 텍스트를 입력값으로 넣었을 때, 출력을 우리가 해석해야 하는데, Flesch는 점수가 높을 수록 가독성이 높다고 본다.

 

두 번째로 Gunning fog 역시 문장의 길이가 중요하지만, 여기에 더해서 어려운 단어들(음절이 긴 단어 등)이 포함된 비율에 관한 지표를 계산한다. 따라서, 이 비율이 높을수록 가독성이 낮다고 보는 것이다. 

 

실제로 적용은 다음 코드와 같이 적용이 가능하다. 텍스트를 Textatistic( )에 입력받고, scores 메서드를 적용해 가독성 점수를 만든다. 여기서 가독성 점수는 각각 유형별로 dictionary 형태로 저장되기 때문에 key만 입력해주면 출력이 간단하다. 

# Import Textatistic
from textatistic import Textatistic

# Compute the readability scores 
readability_scores = Textatistic(sisyphus_essay).scores

# Print the flesch reading ease score
print("Flesch :", readability_scores['flesch_score'])
print("Gunning fog :", readability_scores['gunningfog_score'])

 

생각보다 굉장히 간편하게 사용할 수 있는데, 다양한 분야에 응용도 가능해보인다. 우선, 원래 목적인 가독성 여부를 체크에 집중해 문서 발행 이전에 가독성 테스트에 활용할 수 있다. 또는 스팸메일 처럼 걸러내야 하는 메일을 찾을 때, 조건 중 하나로도 활용이 가능해 보인다.

 

 

 

참고: www.erinhengel.com/software/textatistic/

 

Erin Hengel

Python package to calculate the Flesch Reading Ease, Flesch-Kincaid, Gunning Fog, Simple Measure of Gobbledygook (SMOG) and Dale-Chall readability indices. Textatistic also contains functions to count the number of sentences, characters, syllables, words,

www.erinhengel.com

 

반응형
반응형

선형계획법. 영어로는 Linear Programming(LP) 라고 한다. 말은 그럴싸해보이고 뭔가 어려워보일 수도 있지만, 수학적인 기초 적용방법에 대해서는 이미 중고등학교 때 일차 부등식의 해를 구하는 단원에서 문제 해결방법에 대해 충분히 학습했다.

 

지금은 이렇게 중등 수학으로 분류되어 있지만, 2차 세계대전 군수 자원의 효율적 배분 등을 위해서 적극적으로 이용된 꽤나 유서가 깊은 분야이다. 현재도 경영 경제 분야에서 활발히 사용되고 있고, 선형계획법의 특수한 경우인 네트워크 흐름 같은 문제에 대한 알고리즘이 연구되고 있다고 한다.

 

최근에 하나의 최적화 문제를 시뮬레이션을 해야 했었는데, 그 기반이 되는 논문이 선형계획법을 사용했기 때문에 Python을 통해 시뮬레이션을 했던 내용 중 가장 핵심이 됐던 LP에 대해 간단히 정리하고자 한다.

 

나 같은 경우는 자원의 사용을 최대화가 아닌, 최소화하는 문제였어서 옆에서 확인할 수 있는 형태를 푸는 것었다. 옆의 사진은 행렬식이기 때문에 어느정도 감안해서 위의 모양대로 처리해주는 것이 필요하다. 

 

이 문제를 풀기 위해 Scipy.org에서 만들어준 scipy.optimize.linprog를 이용하면, 다음과 같이 접근하면 된다.

 

 

from scipy.optimize import linprog

c = [...]           				# ... 은 x의 변수 개수에 맞춰 작성
A = [[...], ... ,[...]]  			# A 처럼 2-D Array는 안에 있는 리스트 개수 = 조건의 개수
b = [...]
Aq = []								# equality 조건은 필요조건이 아님
bq = []
Bound = []

res = linprog(c, A_ub=A, b_ub=b, A_eq=Aq, b_eq=bq, bounds=Bound)

사실 여기서 언급한 거 외에도, 다양한 옵션이 있지만 그건 너무 세부적인 사항이라 제외하였다.

 

Scipy.org에서 제공하는 예제를 통해 살펴보면, 사용에 대해 금방 익힐 수 있다. 옆 그림과 같은 최소화 문제가 있다 고 하자. 우리가 배웠던 중고등학교 때 최대 최소 문제로 바꿔서 읽는다면, x0를 x로 x1을 y로 치환해서 -x+4y가 최소로 값을 갖는 문제로 생각할 수 있다.

 

그렇게 바꿔놓고 본다면, 직접 손으로 써서 풀 수 있는 분도 여럿 있으리라 생각하지만, python을 이용하면 좀 더 빠르게 풀 수 있다. 코드는 아래와 같이 작성가능하다.

 

from scipy.optimize import linprog

c = [-1, 4]
A = [[-3, 1], [1, 2]]
b = [6, 4]
x0_bounds = (None, None)
x1_bounds = (-3, None)

res = linprog(c, A_ub=A, b_ub=b, bounds=[x0_bounds, x1_bounds])
print(res)

결과 값은 아래와 같다. 다른 값들은 우리가 풀고싶은 문제의 본질과 조금은 거리가 있는 부가적인 설명들이고, 따라서 우리가 체크해야 할 것들은 message, status, success만 큰 문제없이 성공여부를 알려줬을 때 x가 우리가 원하는 해라는 것만 이해하면 충분하다.

 

     con: array([], dtype=float64)
     fun: -21.99999984082494
 message: 'Optimization terminated successfully.'
     nit: 6
   slack: array([3.89999997e+01, 8.46872439e-08]
  status: 0
 success: True
       x: array([ 9.99999989, -2.99999999]) 

 

 

 

출처: https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.linprog.html

반응형