Deep Learning for NLP with PyTorch

#Deep-Learning-for-NLP-with-PyTorch

저자: Robert Guthrie
원본: http://pytorch.org/tutorials/beginner/deep_learning_nlp_tutorial.html
역자: Don Kim

이 튜토리얼을 통해서 Pytorch를 이용한 딥러닝의 핵심을 전달하려 한다. 여기서 소개되는 많은 개념들 (computation graph abstraction, autograd 등)은 Pytorch에만 특별히 있는 것이 아니고, 여타 딥러닝 도구에서도 찾아 볼 수 있을 것이다.

특히 이 튜토리얼은 딥러닝 framework (Tensorflow, Theano, Keras, Dynet)을 전혀 사용하지 않은 사람들을 대상으로 NLP에 국한해서 진행한다. 또한 NLP의 기본 지식(part-of-speech tagging, language modeling 등)과 기초 AI 수업 (예를 들어 Russell과 Norvig의 교재)에서 배우는 정도의 neural networks 기본 지식은 알고 있다고 가정한다. 기초 AI 수업에서는 feed-forward neural network에 대한 기본적인 backpropagation을 소개하고, network는 linearity와 non-linearity의 조합의 연속으로 이루어 진다는 점을 알려줄 것이다. 이 튜토리얼은 이러한 기초 지식을 바탕으로 딥러닝 코드를 직접 짜보는 것을 목적으로 한다.

이 글은 모델 에 관한 것이지, 데이터에 대한 것이 아니다. 여기서 다루는 모델의 weight들이 training 과정에 따라 어떻게 변화하는지 잘 볼 수 있도록 낮은 차원에서의 test 예제를 소개할 뿐이다. 실제 데이터를 가지고 해보고 싶다면 이 튜토리얼의 어떤 모델이든지 가져다가 사용해보길 바란다.

Introduction to Torch's Tensor Library

#Introduction-to-Torch's-Tensor-Library

딥러닝 계산은 모두 tensor를 통해 이루어진다. Tensor는 matrix를 일반화한 개념으로, matrix가 2방향으로 index를 가질 수 있듯이 tensor는 2개 이상의 index를 가질 수 있다. 머지않아 tensor가 정확히 어떤 의미를 가지는 지 알아볼 것이다.

우선 우리가 tensor를 가지고 뭘 할 수 있는 지 한 번 보자.

Loading output library...

Creating Tensors

#Creating-Tensors

Tensor는 torch.Tensor() 함수가 Python list를 받아서 생성된다.

그래서 3D tensor가 뭔데? 이렇게 생각해 보자.

Vector의 원소 하나 하나는 숫자값(scalar)이다.
Matrix의 원소 하나 하나는 벡터다.
그렇다면 3D tensor의 원소 하나 하나로는 matrix가 나오는 것이다!

용어 정리: 앞으로 이 튜토리얼에서의 "tensor"는 torch.Tensor object를 의미하는 용어로 사용할 것이다. Vector와 matrix도 torch.Tensor의 차원이 1, 2인 간단한 경우로 속한다. 차원을 명시할 필요가 있을 경우에는 꼭 밝힐 것이다. 예를 들어 3차원 tensor를 가지고 설명할 경우에는 그냥 "tensor"가 아닌 "3D tensor"라고 쓸 것이다.

여러 자료 유형에 대해서 텐서를 만들 수 있다. 이미 위 코드에서 봤듯이, 기본 세팅은 Float이다. Integer 형태의 tensor를 만들 때는 torch.LongTensor()를 사용하자. 그 외 다른 경우를 위해서는 documentation을 체크해보길 바란다. 하지만 대부분의 경우에 FloatLong로 충분할 것이다.

torch.randn()을 통해서 원하는 차원의 난수 tensor를 생성할 수도 있다.

Operations with Tensors

#Operations-with-Tensors

Tensor간의 operation은 아마도 우리가 익숙한 방식대로 작동할 것이다.

Documentation을 보면 우리가 tensor를 통해 할 수 있는 수 많은 operation 리스트를 볼 수 있을 것이다. 그 중에는 수학적인 연산의 개념을 넘어서는 operation도 있다.

그 중에서 자주 사용하게 될 concatenation을 사용해 보자. torch.cat은 나중에 이 튜토리얼에서도 사용하게 될 것이다.

Reshaping Tensors

#Reshaping-Tensors

Tensor의 모양을 바꿔주는 .view() method는 대단히 많이 이용되는데, 그 이유로는 많은 neural network의 부품들이 자기의 input이 어떤 특정 모양이길 바라고 있기 때문이다. 당신의 데이터를 그 부품에게 주기 전에 모양을 바꿔줄 필요가 종종 있을 것이다.

Computational Graphs and Automatic Differentiation

#Computational-Graphs-and-Automatic-Differentiation

Computation graph는 효율적인 딥러닝 개발을 위해 필수적인 개념이다. 왜냐하면 computation graph가 우리 대신에 back propagation gradient를 계산해주기 때문이다. Computation graph를 간단하게 말하자면, 어떻게 데이터가 결합돼서 output으로 계산이 된 것인지를 담은 기록장이다. 어느 parameter가 어느 연산과 연계되었는지를 모두 기록하기 때문에 computation graph는 미분을 계산할 수 있을 정도로 충분한 정보를 갖게 된다. 지금까지 설명한 것이 확실하게 와닿지 않을 수 있으므로, Pytorch의 핵심 class 중 하나인 autograd.Variable가 어떻게 작동하는 지 직접 보려고 한다.

먼저 프로그래머의 관점에서 생각해보겠다. 위에서 우리가 만든 torch.Tensor object에는 무엇이 담겨져 있을까? 당연하게도 데이터가 있을 것이고, 그 모양(shape) 정보도 있을 것이고, 기타 등등이 있을 것이다. 근데 두 tensor가 더해질 때를 생각해 보자. 그 결과로 받는 tensor도 그 데이터와 모양 정도는 알고 있겠지만, 그 tensor 입장에서 본인이 다른 두 tensor의 합이라는 사실을 알고 있을 리가 없다 (마찬가지로 tensor는 자기가 파일에서 읽어서 생성된 tensor인지, 다른 operation으로 생성된 것인지 알 수 없다).

Variable class는 자기 자신이 어떤 작업이 결과로 만들어진 것인지를 계속 기록한다. 코드를 보자.

Variable은 무엇이 자신을 만들었는지 안다. z는 자기 자신이 파일에서 읽어들여서 만들어진 것도 아니고, 곱셈 연산이나 지수 연산 등의 결과로 만들어진 게 아님을 아는 것이다. z.grad_fn을 따라가보면 우리는 xy를 찾을 수 있게 된다.

하지만 그래서 어떻게 gradient를 계산할 수 있다는 걸까?

이제 z의 총합 sx의 첫 번째 변수에 대한 미분값이 뭘까? 수식으로 간단하게 쓰자면 우리가 구하고 싶은 것은 이거다.

@@0@@

s는 자기가 tensor z의 합으로 만들어졌다는 것을 안다. zx + y의 결과라는 것을 안다. 따라서

@@1@@

이고 s는 우리가 원하는 미분값이 1이라고 말해줄 수 있을만 한 충분한 정보를 다 갖고 있는 셈이다!

물론 지금까지 설명한 것으로는 진짜로 미분을 어떻게 계산하는 지를 완벽하게 설명할 순 없다. 요점은 s가 미분을 계산하기 위한 재료를 계속 가지고 다닌다는 것이다. 실제로 Pytorch 개발자들은 sum()+ 연산 자체가 스스로 gradient를 계산하는 방법을 알고, back propagation 알고리즘을 수행할 수 있도록 고안했다. 더 자세한 내용은 이 튜토리얼의 범위를 벗어나므로 여기까지만 설명하겠다.

이제 직접 Pytorch로 gradient를 계산하고 위에서 설명한 것과 맞는지 확인해보자. 참고로, 만약 아래 cell을 여러 번 실행한다면 gradient는 누적 합산된다. 이것은 Pytorch가 의도한 설계로, gradient를 .grad누적 시키는 것이 여러 모델에서 편리하기 때문이다.

좋은 딥러닝 프로그래머가 되려면 아래 cell에서 어떤 일이 일어나는 지를 꼭 잘 이해해야 한다.

autograd.Variable의 연산에 관해서 기본적이지만 몹시 중요한 규칙 하나를 얘기하겠다. 그리고 이것은 Pytorch에 국한된 것이 아니고 모든 주요 딥러닝 도구를 사용함에 있어서 적용되는 개념이다.

Loss function으로부터 network 구성 요소에까지 backpropagation을 통해 error를 계산하기 원한다면, 그 과정에서의 Variable chain을 절대! 끊으면 안된다. 만약 그 chain을 끊는다면 loss는 network의 구성 요소가 존재하는지 조차 모를 것이고, parameter들은 업데이트될 수가 없다.

볼드체로 진지하게 쓴 이유는, 이로 인한 문제가 당신의 network를 굉장히 미묘하게 괴롭힐 수 있기 때문이다 (나중에 직접 보여주겠다). 또한 이런 문제는 코드 레벨에서 에러가 나거나 불평하지 않으므로, 훨씬 조심스럽게 접근해야 한다.

Deep Learning Building Blocks: Affine Maps, Non-linearities and Objectives

#Deep-Learning-Building-Blocks:-Affine-Maps,-Non-linearities-and-Objectives

딥러닝은 linearity와 non-linearity를 똑똑하게 결합하는 방법으로 구성된다. Non-linearity가 개입함으로써 model은 강력해진다. 여기서는 Pytorch의 핵심 모듈들을 가지고 놀면서 objective function도 구성해보고, 모델이 어떻게 학습하는지 보겠다.

Affine Maps

#Affine-Maps

딥러닝을 이끄는 핵심 구성 요소 중 하나는 affine map으로, 다음 함수 @@0@@로 표시할 수 있다.

@@1@@

여기서 @@2@@는 matrix, @@3@@는 vector로, 우리가 구하고자 하는 parameter가 된다. @@4@@는 bias라는 이름으로 불리기도 한다.

Pytorch 및 여타 딥러닝 framework들 모두 전통적인 선형 대수의 방법과는 조금 다르게 행렬 연산을 하는데, input의 열(column) 대신 행(row)를 기준으로 연산한다. 이는 output의 @@5@@ 번째 행을 계산하기 위해 input의 @@6@@ 번째 행을 mapping @@7@@로 보내고, bias를 더한다는 말이다. 아래 예제를 보자.

Non-linearities

#Non-linearities

Non-linearity가 대접받아야 하는 이유를 알기 위해 우선적으로 짚고 넘어가야할 사실이 있다. 2개의 affine map이 있다고 해보자.

@@0@@

그러면 @@1@@는 무엇이 될까?

@@2@@

@@3@@는 matrix고 @@4@@는 vector다. 그러니까 결국 그 결과는 다시 affine map이 된다.

따라서, neural network가 그저 affine map들의 긴 chain이라면 그 network는 단 하나의 affine map과 같은 레벨의 모델이 될 뿐임을 알 수 있다.

여기서 affine layer 사이에 non-linearity를 끼워 넣는다면, 하나의 affine map과 다른 훨씬 강력한 모델을 구축할 수 있다.

핵심적인 non-linearity가 여럿 있는데, 그 중에서도 @@5@@가 널리 쓰인다. 누군가 궁금할 수 있다, "왜 하필 쟤네들이지? 저거 말고 다른 non-linear function은 너무 많잖아". 그 이유는 학습에 필수적인 gradient를 쉽게 계산할 수 있기 때문이다. 예를 들어 sigmoid의 경우를 들겠다.

@@6@@

잠깐 첨언하자면, AI 관련 수업에서 @@7@@를 기본 non-linearity로 채택해서 neural network를 설명했을 수도 있지만 실제로 sigmoid는 주로 사용되지 않는다. 그 이유는 argument @@8@@의 크기(absolute value)가 커질 수록 굉장히 빠르게 0으로 사라지기 때문이다. 작은 gradient는 곧 학습시키기 어려워짐을 의미한다. 그래서 대부분 @@9@@를 기본 non-linearity로 사용한다.

Softmax and Similarities

#Softmax-and-Similarities

@@0@@ 번째 값은 다음과 같다.

@@1@@

이 결과는 틀림없이 확률 분포가 된다: 모든 값들이 non-negative하고, 그 합이 1이 된다.

Softmax는 input의 값들을 각각 지수 함수로 태워서 non-negative하게 만든 뒤, 합이 1이 되도록 (normalizing constant로) 나눠준 거라고도 생각할 수 있다.

Objective Functions

#Objective-Functions

Objective function은 network가 학습하기 위한 목표로 최소화하고자 하는 함수를 말한다 (loss function 또는 cost function이라고도 불린다). Objective function은 우선 training 자료를 하나 골라서 neural network에 넣어서 돌린 후에 output의 loss를 계산한다. 모델의 parameter들은 loss function의 미분을 계산해서 업데이트하게 된다. 직관적으로 생각해봤을 때, 모델의 답이 맞다고 확신하고 있는데 그 답이 틀렸다면 loss는 높을 것이고, 그 답이 맞다면 loss는 낮을 것이다.

Training example에 대해서 loss function을 최소화하는 것에 깔려있는 생각은, network가 부디 일반적으로 잘 맞출 수 있는 모델로 발전하길 바라고, dev set/test set/production에서 나오는 새로운 데이터가 들어왔을 때 작은 loss가 나오길 바란다는 것이다.

Loss function의 예로 negative log likelihood loss를 들 수 있는데, 이것은 multi-class 분류 문제에서 굉장히 널리 쓰이는 objective이다. Supervised multi-class 분류 문제에서 negative log likelihood loss를 사용하면 올바른 output에 대한 negative log probability를 최소화하는 방향 (반대로 말하면 올바른 output일 경우의 log 확률을 최대화하는 방향)으로 network을 훈련시킬 수 있게 된다.

Optimization and Training

#Optimization-and-Training

그래서 loss function으로 우리는 뭘 계산할 수 있는가? 이미 우리는 autograd.Variable이 우리가 적용한 연산을 기억해서 gradient를 계산할 수 있는 정보를 기록해두고 있다고 알고 있다. 우리가 사용할 loss 역시 autograd.Variable이어서 loss를 계산하는데 필요한 모든 parameter에 대한 gradient를 계산할 수 있다! 그러고 나면 일반적인 gradient 업데이트를 할 수 있게 된다. @@0@@를 parameter라고 하고, @@1@@를 loss function, @@2@@를 양수의 learning rate라고 표시하면, 다음과 같이 parameter 업데이트를 할 수 있다.

@@3@@

위와 같은 "vanilla" gradient 업데이트보다 일을 잘 할 수 있는 수 많은 알고리즘들이 있고, 계속 연구가 진행되고 있다. 많은 사람들이 train 단계에서 learning rate를 변화시켜보곤 한다. 이런 이론에 관심이 없다면 딱히 어떤 알고리즘이 어떻게 작동하는지 신경쓰지 않아도 된다. Torch는 torch.optim 패키지를 통해 많은 방법을 완전 투명하게 개발해두었다. 세상에서 제일 간단한 gradient update를 사용하는 것이나 그것보다 복잡한 알고리즘을 사용하는 것이나 Pytorch에서는 똑같은 방법으로 사용할 수 있다. 여러 update 알고리즘과 그 알고리즘에 사용될 여러 parameter (예를 들어 여러 가지 초기 learning rate)들을 시험해보는 것은 network 성능을 올리는데 중요한 일이다. 종종 기본 SGD를 Adam이나 RMSProp으로만 바꿔줘도 성능이 크게 올라가는 것을 볼 때가 있다.

Creating Network Components in Pytorch

#Creating-Network-Components-in-Pytorch

NLP로 들어가기 전에, Pytorch에서 affine map과 non-linearity만을 이용해서 network를 구성해보는 연습을 해보자. 또한 Pytorch에 내장된 negative log likelihood를 통해서 loss function을 어떻게 계산하고 어떻게 backpropagation을 해서 parameter를 업데이트하는지도 보게 될 것이다.

모든 network 구성 요소들은 nn.Module을 상속해야하고 .forward() method를 덮어씌워 놓아야 한다. Boilerplate과 관련돼서는 이 두 가지만 지키면 된다.

nn.Module을 상속하는 구성 요소들은 많은 기능을 탑재하게 된다. 예를 들어, 학습시키고자 하는 parameter들을 추적하면서, CPU와 GPU 사이를 오가도록 .cpu().cuda()를 사용해서 바꿀 수도 있는 것이다.

Sparse bag-of-words 표현을 받아서 "English"인지 "Spanish"인지에 대한 확률을 내뱉는 network를 예제로 작성해보겠다. 이번 모델은 단순한 logistic regression이다.

Example: Logistic Regression Bag-of-Words Classifier

#Example:-Logistic-Regression-Bag-of-Words-Classifier

우리의 모델은 sparse한 BoW represenation을 label에 따른 log probability로 변환(map)해 줄 것이다. 우리 단어장에 모든 단어들을 index로 지정해두겠다. 예를 들어 단어장에 "hello"와 "world" 두 단어만이 index 0과 1로 등록되어 있다면, "hello hello hello hello"를 변환한 BoW vector는 @@0@@이 되고, "hello world hello world"는 @@1@@가 될 것이다. 일반적으로는

@@2@@

가 될 것이다.

BoW vector를 @@3@@라고 지칭하겠다. 이제 우리의 network가 내보내는 output은

@@4@@

으로, 다시 말하면 input을 affine map에 태운 뒤 log softmax를 하는 것이다.

여기서 어떤 값들이 ENGLISHSPANISH에 연결된 log probability인지 우리가 아직 지정한 적이 없다. 따라서 학습을 시키려면 output label을 지정해야 한다.

자, 이제 학습시켜 보자! 그 과정으로 우리는 training 데이터를 집어 넣어서 log probability를 받고, loss function을 계산한 뒤, loss function의 gradient를 구하고, gradient step으로 parameter를 업데이트하면 된다.

Loss function은 Torch에서 nn 패키지로 제공한다. nn.NLLLoss()가 우리가 원하는 negative log likelihood loss다. 또한 torch.optim 패키지에서 최적화를 위한 기능을 이용할 수 있는데, 여기는 간단한 SGD를 사용하겠다.

NLLLossinput 으로 log probability를 가진 vector와, target label을 필요로 한다는 것을 알아두자. 우리를 위해서 log probability까지 계산해주지는 않는 것이다. 이것이 우리가 위에서 log softmax를 마지막 layer로 추가한 이유이다. 반면에 nn.CrossEntropyLoss()는 log softmax를 대신 해주는 NLLLoss()와 같다.