for textmining

한국어 임베딩

|

임베딩(embedding)은 자연어를 숫자의 나열인 벡터로 바꾼 결과 혹은 그 일련의 과정 전체를 가리키는 용어입니다. 단어나 문장 각각을 벡터로 변환해 벡터 공간에 ‘끼워 넣는다(embed)’는 취지에서 임베딩이라는 이름이 붙었습니다. 컴퓨터가 자연어를 처리할 수 있게 하려면 자연어를 계산 가능한 형식인 임베딩으로 바꿔줘야 합니다.

임베딩은 컴퓨터가 자연어를 이해하도록 하는 첫 관문으로 매우 중요한 기능을 합니다. 자연어 처리 모델의 성능은 임베딩이 좌우한다고 해도 과언이 아닌데요. 제가 이번에 많은 시간과 노력을 들여서 한국어 임베딩이라는 책을 펴냈습니다. 이 책에서는 다양한 임베딩 기법을 일별하고 한국어 데이터 전처리, 임베딩 구축에 이르는 전 과정을 튜토리얼 방식으로 소개합니다.

다음 그림을 클릭하면 도서 안내 페이지로 이동합니다.

임베딩이 중요한 이유

임베딩에는 말뭉치(corpus)의 의미, 문법 정보가 응축돼 있습니다. 임베딩은 벡터이기 때문에 사칙연산이 가능하며, 단어/문서 관련도(relevance) 역시 계산할 수 있습니다.

최근 들어 임베딩이 중요해진 이유는 따로 있습니다. 바로 전이 학습(transfer learning) 때문입니다. 전이 학습이란 특정 문제를 풀기 위해 학습한 모델을 다른 문제를 푸는 데 재사용하는 기법을 의미합니다. 예컨대 대규모 말뭉치를 미리 학습(pretrain)한 임베딩을 문서 분류 모델의 입력값으로 쓰고, 해당 임베딩을 포함한 모델 전체를 문서 분류 과제를 잘할 수 있도록 업데이트(fine-tuning)하는 방식이 바로 그것입니다. 물론 전이 학습은 문서 분류 이외의 다양한 다른 과제에도 적용할 수 있습니다.

전이 학습 혹은 프리트레인-파인 튜닝 메커니즘은 사람의 학습과 비슷한 점이 있습니다. 사람은 무언가를 배울 때 제로 베이스에서 시작하지 않습니다. 사람이 새로운 사실을 빠르게 이해할 수 있는 이유는 그가 이해를 하는 데에 평생 쌓아 온 지식을 동원하기 때문입니다. 자연어 처리 모델 역시 제로에서 시작하지 않습니다. 우선 대규모 말뭉치를 학습시켜 임베딩을 미리 만들어 놓습니다(프리트레인). 이 임베딩에는 의미, 문법 정보가 녹아 있습니다. 이후 임베딩을 포함한 모델 전체를 문서 분류 과제에 맞게 업데이트합니다(파인 튜닝). 이로써 전이 학습 모델은 제로부터 학습한 모델보다 문서 분류 과제를 빠르게 잘 수행할 수 있습니다.

다음은 네이버 영화 리뷰 말뭉치(NSMC)를 가지고 영화 리뷰 문서의 극성(polarity)을 예측하는 모델의 정확도(accuracy)와 학습 손실(training loss)를 그래프로 나타낸 것입니다. FastText는 이 모델의 입력값을 단어 임베딩 기법의 일종인 FastText를 사용한 것이고, Random은 랜덤 임베딩을 썼다는 뜻입니다. 후자는 다시 말해 학습을 제로에서부터 시작했다는 뜻입니다. 그래프를 보면 아시겠지만 임베딩 품질이 좋으면 수행하려는 태스크(극성 분류)의 성능이 올라갑니다. 아울러 모델의 수렴(converge) 역시 빨라집니다.

품질 좋은 임베딩은 잘 담근 김치와 같습니다. 김치 맛이 좋으면 물만 부어 끓인 김치찌개 맛도 좋습니다. 임베딩 품질이 좋으면 단순한 모델로도 원하는 성능을 낼 수 있습니다. 모델 구조가 동일하다면 그 성능은 높고 수렴(converge)은 빠릅니다. 자연어 처리 모델을 만들고 서비스할 때 중요한 구성 요소 하나만 꼽으라고 한다면, 저는 주저하지 않고 ‘임베딩’을 꼽을 것입니다. ELMo(Embeddings from Language Models), BERT(Bidirectional Encoder Representations from Transformer), GPT(Generative Pre-Training) 등 자연어 처리 분야에서 당대 최고 성능을 내는 기법들이 모두 전이 학습 혹은 프리트레인-파인 튜닝 메커니즘을 사용하는 것은 우연의 일치가 아닙니다.

이 책이 다루는 범위

한국어 임베딩에서는 NPLM(Neural Probabilistic Language Model), Word2Vec, FastText, 잠재 의미 분석(LSA), GloVe, Swivel 등 6가지 단어 수준 임베딩 기법, LSA, Doc2Vec, 잠재 디리클레 할당(LDA), ELMo, BERT 등 5가지 문장 수준 임베딩 기법을 소개합니다. 이외에도 다양한 임베딩 기법이 있지만 두 가지 원칙에 입각해 일부만 골랐습니다. 우선 성능이 안정적이고 뛰어나 현업에 바로 적용해봄직한 기법을 선택했습니다. 또 임베딩 기법의 발전 양상을 이해하는 데 중요한 역할을 하는 모델을 포함했습니다. ‘정보의 홍수’ 속에서 살아가는 독자들에게 핵심에 해당하는 지식만을 전해주고 싶었기 때문입니다. 기타 임베딩 기법들은 대부분, 이 책에서 소개하는 11개 모델의 변형에 해당하기 때문에 독자 여러분이 추가로 공부하고 싶은 최신 기법이 있다면 이 책에서 가지를 쳐 나가는 식으로 학습하면 수월할 거라 생각합니다.

이와 관련해 XLNet이라는 기법을 짚고 넘어가야겠습니다. XLNet은 구글 연구팀이 2019년 상반기 발표한 모델로, 공개 당시 20개 자연어 처리 데이터셋에서 최고 성능을 기록해 주목받았습니다. 출간을 한 달 정도 늦춰 가며 목차와 내용을 전반적으로 손질할 수밖에 없었습니다. 그러나 직접 실험한 결과 XLNet의 파인 튜닝 성능(분류)이 BERT보다 뒤지는 것은 물론, 동일한 하이퍼파라미터(hyperparameter)로도 점수가 들쭉날쭉한 양상을 보였습니다. XLNet 저자 공식 리포지터리(https://github.com/zihangdai/xlnet)에도 비슷한 사례가 꾸준히 보고되고 있으나 출간 직전인 2019년 9월 현재까지 납득할 만한 해결책이 제시되지 않고 있습니다. 이에 아쉽기는 하지만 XLNet 관련 장을 1판에서는 제외하고 2판 이후를 기약하기로 했습니다. 그럼에도 XLNet을 공부하고 싶은 독자가 있다면 다음 링크를 확인하시면 됩니다.

튜토리얼

한국어 임베딩은 각 임베딩 기법의 이론적 배경을 설명함과 동시에 공개돼 있는 한국어 말뭉치로 실습하는 것을 목표로 합니다. 이 책의 모든 실습 코드는 다음 깃허브 리포지터리에 공개돼 있습니다. 코드는 예고 없이 수정될 수 있으니 다음 리포지터리의 최신 코드를 받아 실행하기를 권합니다.

● https://github.com/ratsgo/embedding

책에 소개된 코드(특히 bash 스크립트)를 그대로 실행하고 싶은데 일일이 타이핑하기엔 너무 길어 불편할 수 있습니다. 복사해서 붙여 넣기 쉽도록 다음 페이지에 기법별로 스크립트를 정리해 놓았습니다.

● https://ratsgo.github.io/embedding

도서 구매 안내

전국 주요 서점과 온라인으로 구매할 수 있습니다. 온라인 서점 링크는 다음과 같습니다.

Comment  Read more

XLNet

|

XLNet은 구글 연구팀(Yang et al., 2019)이 발표한 기법으로 공개 당시 20개 자연어 처리 데이터셋에서 최고 성능을 기록한 아키텍처입니다. 일부 데이터에 한해서는 기존 강자인 BERT를 크게 앞서 자연어 처리 연구자들의 주목을 받았습니다. XLNet은 트랜스포머 네트워크(Vaswani et al., 2017)를 개선한 ‘트랜스포머-XL(Dai et al., 2019)’의 확장판 성격의 모델입니다. 여기에서 XL이란 ‘eXtra-Long’으로, 기존 트랜스포머보다 좀 더 넓은 범위의 문맥(context)를 볼 수 있다는 점을 강조하는 의미로 쓰였습니다.

퍼뮤테이션 언어모델 (Permutaion Language Model)

Yang et al. (2019)는 임베딩 모델의 최근 흐름을 크게 두 가지로 나누어 정리했습니다. 하나는 오토리그레시브(AutoRegressive, AR) 모델이고 다른 하나는 오토인코딩(AutoEncoding, AE) 모델입니다. AR 모델은 데이터를 순차적으로 처리하는 기법의 총칭을 뜻한다. 이 관점에서 보면 ELMo나 GPT를 AR 범주로 분류할 수 있겠습니다. 두 모델 모두 이전 문맥을 바탕으로 다음 단어를 예측하는 과정에서 학습하기 때문입니다. 예컨대 ‘발 없는 말이 천리 간다’는 문장을 학습하는 경우 AE 모델은 그림 1처럼 단어를 순차적으로 읽어 나갑니다.

그림 1. 오토리그레시브(AR) 모델

AE 모델은 입력값을 복원하는 기법들을 두루 일컫습니다. 다시 말해 y=f(x)=x를 지향합니다. BERT가 대표적인 AE 모델입니다. BERT는 문장 일부에 노이즈(마스킹)를 주어서, 문장을 원래대로 복원하는 과정에서 학습합니다. 다시 말해 마스킹 처리가 된 단어가 실제로 어떤 단어일지 맞추는 데 주안점을 둔다는 이야기입니다. Yang et al. (2019)는 이런 맥락에서 BERT를 디노이징 오토인코더(Denoising Autoencoder)라고 표현하기도 했습니다. 디노이징 오토인코더란 노이즈가 포함된 입력을 받아 해당 노이즈를 제거한 원본 입력을 출력하는 모델입니다. AR 모델은 그림 2처럼 학습합니다.

그림 2. 오토인코딩(AE) 모델

Yang et al. (2019)는 기존 AE, AR 모델 모두 문제가 있다고 주장했습니다. AR의 경우 문맥을 양방향(bidirectional)으로 볼 수 없는 태생적 한계를 지닙니다. 이전 단어를 입력으로 하고 다음 단어를 출력으로 하는 언어모델을 학습할 때, 맞춰야 할 단어를 포함한 이후 문맥 정보를 미리 알려줄 수는 없기 때문입니다. 물론 ELMo의 경우 모델의 최종 출력값을 만들 때 마지막 레이어에서 순방향(forward), 역방향(backward) LSTM 레이어의 계산 결과를 모두 사용하기는 합니다. 그러나 프리트레인을 할 때 순방향, 역방향 레이어를 각각 독립적으로 학습하기 때문에 ELMo를 진정한 의미의 양방향 모델이라고 말하기는 어렵습니다.

BERT로 대표되는 AE는 양방향 모델입니다. 그림 2처럼 마스크 단어를 예측할 때 앞뒤 문맥을 모두 살피기 때문입니다. 이 덕분에 각종 다운스트림 태스크에서 BERT가 상당 기간 절대 강자로 군림할 수 있었습니다. 하지만 AE 역시 단점이 없지 않습니다. 가장 큰 문제는 마스킹 처리한 토큰들을 서로 독립(independent)이라고 가정한다는 점입니다. 이 경우 마스킹 토큰들 사이에 의존 관계(dependency)를 따질 수 없게 됩니다.

그림 3은 Yang et al. (2019)이 든 예시 문장(New York is a city)을 가지고 BERT의 학습 과정을 시각화한 것입니다. 영어 말뭉치에서 “New가 나온 다음에 York라는 단어가 나올 확률”과 “New가 나오지 않았을 경우에 York가 등장할 확률”은 분명히 다를 것입니다. 하지만 BERT 모델은 두 단어의 선후 관계나 등장 여부 등 정보를 전혀 따지지 않습니다. 그저 is a city라는 문맥만 보고 New, York 각각의 마스크 토큰을 독립적으로 예측합니다.

그림 3. BERT 모델의 학습

더구나 BERT가 프리트레인할 때 사용하는 마스크 토큰은 파인 튜닝 과정에서는 쓰지 않습니다. 파인튜닝과 다른 프리트레인 환경을 구성하면 모델의 일반화(generalization) 성능이 떨어질 수 있습니다. 마지막으로 Yang et al. (2019)는 BERT가 긴 문맥을 학습하기 어렵다는 점도 단점으로 꼽았습니다.

Yang et al. (2019)는 AR과 AE 모델의 한계를 극복하기 위해 퍼뮤테이션 언어모델(permutaion language model)을 제안했습다. 토큰을 랜덤으로 셔플한 뒤 그 뒤바뀐 순서가 마치 원래 그랬던 것인 양 언어모델을 학습하는 기법이다. 그림 4는 발 없는 말이 천리 간다를 퍼뮤테이션 언어모델로 학습하는 예시입니다. 그림 4를 보면 모델은 ‘없는, 이, 말, 발, 간다’를 입력받아 시퀀스의 마지막 단어인 ‘천리’를 예측한다.

그림 4. 퍼뮤테이션 언어모델 (1)

그림 4처럼 퍼뮤테이션을 수행하면 특정 토큰을 예측할 때 문장 전체 문맥을 살필 수 있게 됩니다. 해당 토큰을 제외한 문장의 부분집합 전부를 학습할 수 있다는 뜻입니다. 예컨대 발 없는 말이 천리 간다는 문장을 한번 더 퍼뮤테이션해서 이번엔 발, 없는, 천리, 이, 말, 간다가 나왔다고 가정해 봅시다. 그러면 ‘천리’라는 단어를 예측할 때의 입력 시퀀스는 발, 없는이 됩니다.

그림 5는 천리라는 단어를 맞추기 위해 상상해볼 수 있는 입력 시퀀스의 모음입니다. 그림 5에서 확인할 수 있는 것처럼 순방향 언어모델(발 없는 말 다음에 천리를 맞춤), 역방향 언어모델(간다 다음에 천리를 예측)은 퍼뮤테이션 언어모델의 부분 집합이 됩니다. 다시 말해 퍼뮤테이션 언어모델은 시퀀스를 순차적으로 학습하는 AR 모델이지만 퍼뮤테이션을 수행한 토큰 시퀀스 집합을 학습하는 과정에서 문장의 양방향 문맥을 모두 고려할 수 있게 된다는 이야기입니다.

그림 5. 퍼뮤테이션 언어모델 (2)

발, 없는, 말, 이
간다
없는, 간다
...

Yang et al. (2019)는 퍼뮤테이션 언어모델은 AR이기 때문에 BERT 같은 AE 모델의 단점 또한 극복할 수 있다고 설명합니다. 그림 6을 보면 퍼뮤테이션 언어모델은 셔플된 토큰 시퀀스를 순차적으로 읽어가면서 다음 단어를 예측합니다. 이전 문맥(New)을 이번 예측(York)에 활용합니다. 따라서 퍼뮤테이션 언어모델은 예측 단어(마스킹 토큰) 사이에 독립을 가정하는 BERT와 달리 단어 간 의존관계를 포착하기에 유리합니다. 뿐만 아니라 프리트레인 때 마스크를 하지 않기 때문에 프리트레인-파인튜닝 간 불일치 문제도 해결할 수 있습니다.

그림 6. 퍼뮤테이션 언어모델 (3)

퍼뮤테이션 언어모델의 학습 과정을 좀 더 구체적으로 살펴봅시다. 토큰 네 개짜리 문장을 랜덤으로 뒤섞은 결과가 그림 7처럼 $[3, 2, 4, 1]$이고 셔플된 시퀀스의 첫번째 단어(3번 토큰)를 맞춰야 하는 상황이라고 가정해 봅시다. 그러면 이 때 3번 토큰 정보를 넣어서는 안 됩니다. 3번 토큰을 맞춰야 하는데 모델에 3번 토큰 정보를 주면 문제가 너무 쉬워지기 때문입니다. 2번, 4번, 1번 토큰은 맞출 토큰(3번) 이후에 등장한 단어들이므로 이들 또한 입력에서 제외합니다. 결과적으로 이 상황에서 입력값은 이전 세그먼트(segment)의 메모리(memory) 정보뿐입니다. 메모리와 관련해서는 트랜스포머-XL(transformer-XL)에서 설명합니다.

그림 7. 퍼뮤테이션 언어모델 학습 (Yang et al., 2019)

  • [3, 2, 4, 1]

같은 문장을 또한번 셔플했더니 $[2, 4, 3, 1]$이고 이번 스텝 역시 3번 토큰을 예측해야 한다고 가정합시다. 그러면 3번 토큰 이전 문맥(메모리, 2번, 4번 단어)이 입력됩니다. 3번 토큰은 정답이므로 입력에서 제외합니다. 그림 8과 같습니다.

그림 8. 퍼뮤테이션 언어모델 학습 (Yang et al., 2019)

  • [2, 4, 3, 1]

셔플 시퀀스가 $[1, 4, 2, 3]$이고 3번 토큰을 맞춰야 한다면 입력벡터는 과거 문맥(메모리, 1번, 4번, 2번 단어)이 됩니다. 마찬가지로 $[4, 3, 1, 2]$이고 3번을 예측한다면 입력값은 메모리, 4번 단어가 됩니다. 각각 그림 9, 그림 10과 같습니다.

그림 9. 퍼뮤테이션 언어모델 학습 (Yang et al., 2019)

  • [1, 4, 2, 3]

그림 10. 퍼뮤테이션 언어모델 학습 (Yang et al., 2019)

  • [4, 3, 1, 2]

퍼뮤테이션 언어모델의 실제 구현은 토큰을 뒤섞는 게 아니라 어텐션 마스크(mask)로 실현됩니다. XLNet의 근간은 기존 트랜스포머 네트워크(Vaswani et al., 2017)이고, 그 핵심은 쿼리(query), 키(key) 벡터 간 셀프 어텐션(self-attention) 기법이기 때문입니다. 예컨대 토큰 네 개짜리 문장을 단어 등장 순서대로 예측해야 하는 상황을 가정해 봅시다. 이 경우 어텐션 마스크는 그림 11처럼 구축하면 됩니다.

그림 11 좌측 행렬은 셀프 어텐션을 수행할 때 소프트맥스 확률값에 적용하는 마스크 행렬입니다. 여기서 마스크란 소프트맥스 확률값을 0으로 무시하게끔 하는 역할을 한다는 뜻입니다. 소프트맥스 확률값이 0이 되면 해당 단어의 정보는 셀프 어텐션에 포함되지 않습니다. 그림 43에서 회색 동그라미는 확률값을 0으로 만드는 마스크라는 뜻이며, 붉은색 동그라미는 확률값을 살리는 의미를 지닙니다.

그림을 읽는 방법은 이렇습니다. 마스크 행렬의 행은 쿼리 단어, 열은 키 단어에 각각 대응합니다. 그림 11처럼 토큰 순서대로 예측해야 하는 경우 1번 단어를 예측할 때는 자기 자신(1번 단어)을 포함해 어떤 정보도 사용할 수 없습니다. 2번 단어를 맞춰야할 때는 이전 문맥인 1번 단어 정보를 활용합니다. 마찬가지로 3번 단어는 1, 2번 단어, 4번 단어는 1, 2, 3번 단어 정보를 쓰게끔 만듭니다. GPT가 그림 11과 같은 방식으로 학습합니다.

그림 11. 원래 시퀀스의 어텐션 마스크

그림 12는 퍼뮤테이션 언어모델이 사용하는 어텐션 마스크의 예시입니다. 셔플된 토큰 시퀀스가 $[3, 2, 4, 1]$이라고 가정해 봅시다. 그러면 3번 단어를 맞춰야할 때는 어떤 정보도 사용할 수 없습니다. 2번 단어를 예측할 때는 이전 문맥인 3번 단어 정보를 씁니다. 마찬가지로 4번 단어를 맞출 때는 3번, 2번 단어를, 1번 단어를 예측할 때는 3번, 2번, 4번 단어 정보를 입력합니다.

그림 12. 셔플된 시퀀스의 어텐션 마스크

퍼뮤테이션 언어모델 역시 단점이 있습니다. 예컨대 단어가 네 개인 문장을 랜덤 셔플한 결과가 아래와 같고 이번 스텝에서 셔플 시퀀스의 세번째를 예측해야 한다고 해봅시다. 이 경우 모델은 동일한 입력(3번, 2번 단어)을 받아 다른 출력을 내야 하는 모순에 직면합니다. Yang et al. (2019)는 이같은 문제를 해결하기 위해 투-스트림 어텐션(two-stream self attention) 기법을 제안했습니다. 다음 절에서 살펴봅시다.

  • [3, 2, 4, 1]
  • [3, 2, 1, 4]

투-스트림 셀프어텐션(Two-Stream Self Attention)

투-스트림 셀프어텐션은 쿼리 스트림(query stream)과 컨텐트 스트림(content stream) 두 가지를 혼합한 셀프 어텐션 기법입니다. 이 가운데 컨텐트 스트림은 기존 트랜스포머 네트워크(Vaswani et al., 2017)와 거의 유사합니다. 설명의 편의를 위해 컨텐트 스트림을 먼저 봅시다.

Yang et al. (2019)는 컨텐트 스트림 벡터를 $\mathbf{h}$라고 정의했습니다. 수식 1에서 $\mathbf{z}$는 원래 문장 순서를 랜덤 셔플한 인덱스 리스트입니다. 단어가 네 개인 문장이라면 이를 랜덤 셔플한 샘플 하나(예 : $[3, 2, 4, 1]$)가 바로 $\mathbf{z}$가 됩니다. $\mathbf{z}_t$는 $\mathbf{z}$의 $t$번째 요소를 가리킵니다. 수식 11의 계산 결과는 $m$번째 트랜스포머 블록의 $\mathbf{z}_t$에 해당하는 컨텐트 스트림 벡터입니다. $m$번째 블록의 컨텐트 스트림 벡터는 $m-1$번째 컨텐트 스트림 벡터에 기존 트랜스포머 블록(Vaswani et al., 2017)과 동일한 계산을 수행한 결과입니다.

수식 1. 컨텐트 스트림 (1)

[\mathbf{ h }{ { z }{ t } }^{ (m) }\leftarrow \textrm{Attention} \left( \mathbf{Q}=\mathbf{ h }{ { z }{ t } }^{ (m-1) },\mathbf{KV}=\mathbf{ h }{ { z }{ \le t } }^{ (m-1) };\theta \right)]

그림 13에서 $\mathbf{x}$는 토큰 임베딩을 뜻합니다. 단어가 네 개인 문장 나 어제 학교 갔어가 있다면 $\mathbf{x}_1$은 , $\mathbf{x}_2$는 어제, $\mathbf{x}_3$은 학교, $\mathbf{x}_4$는 갔어에 해당하는 토큰 임베딩입니다. 예컨대 이 문장을 셔플한 인덱스 리스트 $\mathbf{z}$가 $[3, 2, 4, 1]$이고 이번에 예측할 단어가 $\mathbf{z}$의 첫번째($\mathbf{z}_1=3$)라고 가정해 봅시다. 이 계산 과정은 그림 13의 상단 좌측과 같습니다.

수식 1과 그림 13을 자세히 보면 $\mathbf{z}$의 첫번째 컨텐트 스트림($\mathbf{g_{z_1}}=\mathbf{g_3}$)을 만들 때 자기 자신의 토큰 임베딩 정보($\mathbf{x_{z_1}}=\mathbf{x_{3}}=$ 학교) 역시 셀프 어텐션 계산에 포함되는 걸 확인할 수 있습니다. 이후 설명하겠지만 쿼리 스트림을 계산할 때는 $\mathbf{x_3}$을 빼고 계산합니다. 자기 자신의 토큰 정보를 빼는 방식이 컨텐트 스트림에도 동일하게 적용되고 이 방식이 트랜스포머 블록별로 누적되면 $\mathbf{x_3}$ 이후 등장하는 토큰들을 예측할 때 $\mathbf{x_3}$ 정보를 참고하기 어려워져 불리합니다. 이 때문에 Yang et al. (2019)는 컨텐트 스트림을 만들 때는 $\mathbf{x_3}$을 포함하도록 설계했습니다.

어쨌든 이번에 예측할 단어가 $\mathbf{z}$의 첫번째($\mathbf{z_1}=3$)인 상황이라면 이전 문맥(메모리)과 자기 자신의 토큰 임베딩 정보($\mathbf{x_3}=$학교)가 계산에 포함됩니다(그림 13 상단 좌측). $\mathbf{z}$의 두번째($\mathbf{z_2}=2$)를 계산한다면 이전 문맥(메모리, 학교)과 자기 자신($\mathbf{x_2}=$어제)을 넣습니다(그림 13 상단 우측). 이는 이후 단어들도 마찬가지입니다(그림 13 하단 좌측, 그림 13 하단 우측).

그림 13. 컨텐트 스트림 (Yang et al., 2019)

컨텐트 스트림 $\mathbf{h}$를 수식 1과 그림 13을 간소화해 표기하면 수식 2와 같습니다. $\mathbf{z}$의 $t$번째 요소에 해당하는 컨텐트 스트림을 만들 때는 이전 문맥과 자기 자신에 대응하는 토큰 정보($\mathbf{x}$)를 활용한다는 의미입니다. 첫번째 트랜스포머 블록에 입력되는 컨텐츠 스트림 벡터의 초기값은 해당 단어에 해당하는 임베딩 벡터($\mathbf{x}$)입니다.

수식 2. 컨텐트 스트림 (2)

[{ h }{ \theta }\left( \mathbf{ x }{ { z }_{ \le t } } \right)]

쿼리 스트림은 토큰과 위치(position) 정보를 활용한 셀프어텐션 기법입니다. 쿼리 스트림을 만들 때는 이전 토큰 정보뿐 아니라 이번에 맞춰야할 타겟 단어의 위치 정보를 활용합니다. 쿼리 스트림 $\mathbf{g}$를 만드는 식은 수식 3과 같습니다.

이번 레이어($m$)에서 특정 시점($\mathbf{z_t}$)에 해당하는 단어의 쿼리 스트림 벡터 $\mathbf{g_{z_t^{(m)}}}$를 계산할 때는 이전 레이어($m-1$) $t$번째 “미만”의 컨텐트 스트림($\mathbf{h_{z_{<t}^{(m-1)}}}$)을 키($\mathbf{K}$)와 값($\mathbf{V}$)으로 씁니다. $\mathbf{g_{z_t^{(m)}}}$의 쿼리($\mathbf{Q}$)는 직전 레이어의 $\mathbf{g_{z_t^{(m-1)}}}$이며 $\mathbf{g_{z_t^{(m-1)}}}$에는 해당 시점의 단어 정보($\mathbf{x_{z_t}}$)를 빼고 위치 정보($\mathbf{z_t}$)만 넣습니다. 결과적으로 $\mathbf{g_{z_t}^{(m)}}$에는 지금 맞춰야할 단어의 임베딩 정보($\mathbf{x_{z_t}}$)가 들어가지 않습니다.

수식 3. 쿼리 스트림 (1)

[\mathbf{ g }{ { z }{ t } }^{ (m) }\leftarrow \textrm{Attention} \left( \mathbf{Q}=\mathbf{ g }{ { z }{ t } }^{ (m-1) },\mathbf{KV}=\mathbf{ h }{ { z }{ <t } }^{ (m-1) };\theta \right)]

예시를 다시 떠올려 봅시다. 원래 문장의 첫번째 단어의 임베딩인 $\mathbf{x}_1$은 , 두번째 단어 임베딩($\mathbf{x}_2$)은 어제, 세번째 단어 임베딩($\mathbf{x}_3$)은 학교, 네번째 단어 임베딩($\mathbf{x}_4$)은 갔어에 대응하는 벡터입니다. 이 문장을 셔플한 인덱스 리스트 $\mathbf{z}$가 $[3, 2, 4, 1]$이고 이번에 예측할 단어가 $\mathbf{z}$의 세번째($\mathbf{z}_3=4$)라고 가정해 봅시다. 그림 14의 하단 좌측에 대응합니다.

$m$번째 레이어의 $\mathbf{z_t}$에 해당하는 단어의 쿼리 스트림을 구하려면 쿼리($\mathbf{Q}$), 키($\mathbf{K}$), 값($\mathbf{V}$)이 필요합니다. 1번째 레이어(그림 46 하단 좌측의 두번째 층)의 $\mathbf{z}$의 세번째($\mathbf{z_3}=4$)에 해당하는 단어의 쿼리 스트림($\mathbf{g_{z_3^{(1)}}}=\mathbf{g_{4^{(1)}}}$)을 계산할 때 키($\mathbf{K}$)와 값($\mathbf{V}$)은 $t$(=3)번째 미만의 컨텐트 스트림이 됩니다. 키($\mathbf{K}$), 값($\mathbf{V}$)에는 자기 자신의 임베딩($\mathbf{x_{z_3}}=\mathbf{x_4}=$갔어)을 제외한 이전 문맥(메모리, $\mathbf{x_3}=$학교, $\mathbf{x_2}=$어제)과 관련한 컨텐트 스트림만 반영됩니다(검은 실선으로 표시).

쿼리($\mathbf{Q}$)를 만들 땐 $\mathbf{z}$의 세 번째에 해당하는 단어 임베딩 정보($\mathbf{x_{z_3}}=\mathbf{x_4}=$학교)는 빼고 위치 정보($\mathbf{z_3}=4$)만 포함합니다(붉은색 점선으로 표시). 결과적으로 이번 레이어 쿼리 스트림 $\mathbf{g_{z_3}^{(1)}}=\mathbf{g_{4}^{(1)}}$을 계산할 때 쿼리($\mathbf{Q}$), 키($\mathbf{K}$), 값($\mathbf{V}$) 모두, 지금 맞춰야 할 단어 임베딩 정보($\mathbf{x_{z_3}}=\mathbf{x_4}=$갔어)가 빠지게 됩니다. 다음 레이어의 쿼리 스트림 $\mathbf{g_{z_3}^{(2)}}=\mathbf{g_{4}^{(2)}}$ 역시, 쿼리($\mathbf{Q}$)는 이전 레이어의 쿼리 스트림($\mathbf{g_{z_3}^{(1)}}=\mathbf{g_{4}^{(1)}}$)이기 때문에, 레이어가 계속 거듭되더라도 지금 맞춰야 할 단어의 임베딩 정보($\mathbf{x_{z_3}}=\mathbf{x_4}=$갔어)는 모델이 볼 수 없습니다.

그림 14. 쿼리 스트림 (Yang et al., 2019)

이번엔 그림 14의 우측 하단을 봅시다. 이번에 계산할 단어가 $\mathbf{z}$의 네번째($\mathbf{z_4}=1$)라면 이전 문맥(메모리, $\mathbf{x_3}=$학교, $\mathbf{x_2}=$어제, $\mathbf{x_4}=$갔어)만 입력하고, 자기 자신의 토큰 정보($\mathbf{x_1}=$)는 쿼리 스트림에 넣지 않습니다. 하지만 $\mathbf{z_4}=1$이라는 위치 정보는 쿼리 스트림 계산에 포함됩니다. 이는 모델에 다음 질문을 던지는 것과 같다고 생각합니다.

  • 여태까지 학교, 어제, 갔어라는 단어를 봤는데 말이야. 이번에 맞춰야 할 단어는 원래 문장에서 첫번째에 있었어. 이 단어는 뭘까?

Yang et al. (2019)이 쿼리 스트림에 자기 자신의 토큰 정보를 빼고, 이전 문맥 단어 정보와 자신의 위치 정보를 넣은 이유는 앞서 언급한 퍼뮤테이션 언어모델의 한계 때문입니다. 퍼뮤테이션 탓에 모델은 동일한 입력을 받아 다른 출력을 내야 하는 모순에 직면한다는 점을 이미 언급한 바 있습니다. 쿼리 스트림을 위와 같이 설계하게 되면 아래 두 퍼뮤테이션 시퀀스는 다음과 같은 의미를 지니게 돼 모순을 회피할 수 있습니다.

  • [3, 2, 4, 1] : 여태까지 학교, 어제라는 단어를 봤는데 말이야. 이번에 맞춰야 할 단어는 원래 문장에서 네번째에 있었어. 이 단어는 뭘까?
  • [3, 2, 1, 4] : 여태까지 학교, 어제라는 단어를 봤는데 말이야. 이번에 맞춰야 할 단어는 원래 문장에서 첫번째에 있었어. 이 단어는 뭘까?

쿼리 스트림 $\mathbf{g}$를 수식 13과 그림 46을 간소화해 표기하면 수식 4와 같습니다. $\mathbf{z}$의 $t$번째 요소에 해당하는 쿼리 스트림을 만들 때는 현 시점 “미만”의 이전 문맥에 대응하는 단어 정보($\mathbf{x_{<t}}$)와 자기 자신의 위치 정보($\mathbf{z_t}$)를 활용한다는 의미입니다. 첫번째 트랜스포머 블록의 쿼리 스트림을 계산할 때 사용되는 쿼리($\mathbf{Q}$)는 랜덤 초기화한 벡터이며 다른 모델 파라메터와 같이 학습합니다.

수식 4. 쿼리 스트림 (2)

[{ g }{ \theta }\left( \mathbf{ x }{ { z }{ <t } },{ z }{ t } \right)]

프리트레인 과정에서 트랜스포머-XL 레이어를 $m$개 사용했을 경우 $\mathbf{z_t}$번째 단어에 대응하는 XLNet 임베딩의 최종 출력 벡터(output)는 마지막 $m$번째 트랜스포머-XL 레이어의 쿼리 스트림 $\mathbf{g_{z_t^{(m)}}}$입니다. XLNet 모델은 이를 활용해 다음 단어 예측을 수행합니다.

트랜스포머-XL (Transformer-XL)

트랜스포머-XL(Dai et al., 2019)는 XLNet 이전에 발표된 모델입니다. Yang et al. (2019)는 트랜스포머-XL의 세그먼트 리커런스(segment recurrence)와 상대 위치 임베딩(relative position embedding) 기법을 그대로 차용했습니다.

먼저 세그먼트 리커런스를 봅시다. Dai et al. (2019)는 기존 트랜스포머 네트워크(Vaswani et al., 2017)의 단점으로 고정된 길이의 문맥 정보만 활용할 수 있다는 점을 꼽았습니다. 이에 좀 더 긴 컨텍스트를 보기 위해 세그먼트 리커런스라는 기법을 제안했습니다.

기존 트랜스포머 네트워크는 문서가 연구자가 정한 최대 시퀀스 길이를 넘을 경우 그 길이 이후에 나타난 토큰들은 학습에서 제외합니다. 하지만 Dai et al. (2019)의 방식은 다릅니다. 우선 문서를 작은 세그먼트 단위로 자른다. 첫번째 세그먼트를 기존 트랜스포머 네트워크처럼 학습합니다. 첫번째 세그먼트를 충분히 학습했다면 이를 저장(cache)해 두고, 두번째 세그먼트를 학습합니다. 두번째 세그먼트를 계산할 때는 첫번째 세그먼트 정보를 활용합니다.

Dai et al. (2019)는 현재 세그먼트를 학습할 때 고려 대상에 포함하는 직전 세그먼트 계산 결과를 메모리(memory)라고 이름 붙였습니다. 투-스트림 셀프 어텐션의 예시 그림에서 입력 토큰이 현재 세그먼트, memory라고 표현된 부분이 직전 세그먼트에 해당합니다. 단 현재 세그먼트를 계산할 때는 메모리를 학습하지 않습니다. 다시 말해 학습 손실(train loss)을 줄이기 위한 그래디언트를 메모리 쪽에 반영하지 않는다는 이야기입니다. 그림 15에서 확인할 수 있는 것처럼 이같은 세그먼트의 학습은 반복적(recurrence)으로 수행합니다.

그림 15. 세그먼트 리커런스의 학습 (Dai et al., 2019)

코드 1은 현재 세그먼트 계산 결과를 메모리에 저장하는 함수를 텐서플로로 구현한 것입니다. 이전 메모리 정보(prev_mem)와 현재 계산 결과(curr_out)를 합친(concat) 후 이들에 대해서는 그래디언트 업데이트를 하지 않도록(tf.stop_gradient) 처리합니다. XLNet의 modeling.py에 정의되어 있습니다.

코드 1. 현재 세그먼트를 메모리에 저장

def _cache_mem(curr_out, prev_mem, mem_len, reuse_len=None):
  """cache hidden states into memory."""
  ...
  new_mem = tf.concat([prev_mem, curr_out], 0)[-mem_len:]
  return tf.stop_gradient(new_mem)

세그먼트 리커런스 학습이 완료되면 모델이 고려할 수 있는 문맥 범위가 넓어집니다. 그림 16은 이를 시각화한 것입니다.

그림 16. 세그먼트 리커런스의 예측 (Dai et al., 2019)

Dai et al. (2019)는 단어 쌍 사이의 거리 정보인 상대 위치(relative position)를 활용했습니다. 원래 문장을 작은 세그먼트들로 쪼개고, 세그먼트별로 트랜스포머 네트워크를 학습하면 각 단어가 문장 내에서 차지하는 절대 위치(absolute position) 정보가 무의미해지기 때문입니다.

상대 위치와 절대 위치 개념을 예를 들어 설명하면 이렇습니다. 아래와 같은 문장이라면 천리의 절대 위치는 5입니다. 문장에서 다섯 번째로 등장한 단어라는 의미입니다. 천리를 기준으로 한 의 상대 위치는 4입니다. 천리에서 까지 이동하려면 왼쪽으로 네 칸을 이동해야 한다는 뜻입니다.

  • 발, 없는, 말, 이, 천리, 간다

Dai et al. (2019)가 세그먼트에 상대 위치를 적용한 걸 도식화하면 그림 17과 같다. 예컨대 토큰 수 기준 세그먼트 길이가 3이고 학습 대상 문장이 발, 없는, 말, 이, 천리, 간다이며 이번이 두번째 세그먼트를 학습할 차례라고 가정해 봅시다. 그러면 모델은 현재 세그먼트 학습의 첫 단계에서 발, 없는, 말이라는 메모리 정보를 입력받아 를 예측해야 합니다. 는 쿼리(query)가 되고, , 없는, 은 각각 키(key)가 됩니다. 이때 를 기준으로 한 의 상대 위치는 3입니다. 에서 까지 이동하려면 왼쪽으로 세 칸을 이동해야 한다는 의미입니다. 같은 방식으로 계산하면 이-없는, 이-말은 상대 위치가 각각 2, 1이 된다. 자기 자신의 상대 위치(이-이)는 0입니다.

현재 세그먼트 학습의 두번째 스텝에서는 메모리 정보(발, 없는, 말)와 직전 단어(이)를 바탕으로 천리를 예측해야 합니다. 천리가 새로운 쿼리가 되었기 때문에 이전 문맥 단어들과의 상대 거리를 다시 계산해야 합니다. 천리-발, 천리-없는, 천리-말, 천리-이, 천리-천리의 상대 위치는 각각 4, 3, 2, 1, 0이 됩니다. 세그먼트 학습의 세번째 단계(쿼리가 간다인 상황)도 같은 방식으로 수행하면 됩니다.

Dai et al. (2019)가 설계한 상대 위치는 음수값이 존재하지 않습니다. 트랜스포머-XL은 단어를 순차적으로 학습하는 AR 모델이기 때문입니다. 다시 말해 현재 쿼리 단어를 예측해야 하는 상황이라면, 쿼리 단어 이후의 단어에 어텐션이 걸리지 않도록 어텐션 마스크를 만듭니다. 예컨대 천리를 기준으로 한 간다의 상대 거리는 -1이겠지만, 간다는 천리 이후에 등장하는 단어이기 때문에 어텐션 계산에서 제외하게 됩니다. 따라서 천리-간다의 상대 위치 역시 구할 필요가 없습니다.

그림 17. 상대 위치 시각화

수식 5는 기존 트랜스포머 네트워크의 셀프 어텐션(Vaswani et al., 2017)을 나타낸 것입니다.

수식 5. Scaled Dot-Product Attention (1)

[\textrm{Attention} \left( \mathbf{Q,K,V} \right) = \textrm{softmax} \left( \frac { \mathbf{Q K }^{ T } }{ \sqrt { { d }_{ k } } } \right) \mathbf{V}]

수식 6은 Dai et al. (2019)가 수식 5 가운데 $\mathbf{QK^T}$만 떼어내어 다시 표현한 것입니다. 수식 6의 $\mathbf{A}$는 수식 5의 $\mathbf{Q}$와 $\mathbf{K}$를 내적한 결과인 정방행렬(square matrix)입니다. 여기에서 $i$와 $j$는 각각 쿼리와 키의 인덱스(index)를 뜻합니다. 따라서 $\mathbf{A_{ij}}$ 값은 $i$번째 쿼리 단어와 $j$번째 키 단어가 태스크 수행에 얼마나 관련을 맺고 있는지를 나타내는 점수(score)가 됩니다. $\mathbf{W_q} \mathbf{E_{x_i}}$는 수식 15의 행렬 $\mathbf{Q}$의 $i$번째 행, 즉 $i$번째 쿼리 벡터를 가리킵니다. $\mathbf{W_k} \mathbf{E_{x_j}}$는 수식 15의 행렬 $\mathbf{K}$의 $j$번째 행, 즉 $j$번째 키 벡터를 의미합니다.

기존 트랜스포머 네트워크는 단어의 절대 위치(absolute position) 정보를 사용합니다. $\mathbf{U}_i$와 $\mathbf{U}_j$는 각각 문장에서 $i$번째로 등장한 단어와 $j$번째 단어의 절대 위치 정보가 담긴 임베딩을 나타냅니다. 따라서 $\mathbf{W}_q \mathbf{U}_i$는 쿼리 단어의 위치 정보, $\mathbf{W}_k \mathbf{U}_j$엔 키 단어의 위치 정보가 녹아 있습니다.

수식 6. 절대 위치에 기반한 셀프 어텐션

Dai et al. (2019)는 상대 위치에 기반한 셀프 어텐션 기법을 제안했습니다. 수식 7과 같습니다. 수식 6에서 바뀐 부분은 칼라 표시가 되어 있습니다.

우선 절대 위치 임베딩 행렬 $\mathbf{U}$를 상대 위치 임베딩 행렬 $\mathbf{R}$로 대체한 점이 눈에 띕니다. $\mathbf{R}$은 최대 시퀀스 길이 $\times$ $d$차원 크기를 갖는 행렬입니다. 쿼리 단어를 기준으로 한 키 단어의 상대 위치가 $k$일 때 $\mathbf{R}$의 $k$번째 행 벡터를 참조(lookup)해서 씁니다. Dai et al. (2019)는 행렬 $\mathbf{R}$을 사인(sin), 코사인(cosine) 함수 등을 활용해, 학습하지 않는 파라메터(non-trainable parameter)로 두었습니다.

그림 17에서 확인했던 것처럼 쿼리 단어를 기준으로 한 쿼리 단어의 상대 위치는 늘 0이기 때문에 쿼리 단어의 위치 정보를 사용하는 게 큰 의미가 없습니다. 이에 Dai et al. (2019)는 $\mathbf{W_q} \mathbf{U_i}$를 벡터($\mathbf{u}, \mathbf{v}$) 형태로 단순화했습니다.

수식 7. 상대 위치에 기반한 셀프 어텐션

수식 7의 (a)와 (c)는 기존 트랜스포머 네트워크 계산방식과 크게 달리진 것이 없습니다. 문제는 $\mathbf{R}$이 포함된 (b)와 (d)입니다. Dai et al. (2019)는 상대 위치가 포함된 셀프 어텐션 행렬 계산을 효율적으로 하는 테크닉을 제시했습니다. 우선 (b)를 봅시다. 그림 17처럼 최대 시퀀스 길이가 6이라 가정하고, (b)에서 $\mathbf{W_{k,r}} \mathbf{R_{i-j}}$를 떼어 다시 표현하면 수식 8과 같습니다.

수식 8. 상대 위치 셀프 어텐션 계산 예시 (1)

[\mathbf{Q}=\begin{bmatrix} { \left[ \mathbf{ W }{ k,R }\mathbf{ R }{ 5 } \right] }^{ T } \ { \left[ \mathbf{ W }{ k,R }\mathbf{ R }{ 4 } \right] }^{ T } \ { \left[ \mathbf{ W }{ k,R }\mathbf{ R }{ 3 } \right] }^{ T } \ { \left[ \mathbf{ W }{ k,R }\mathbf{ R }{ 2 } \right] }^{ T } \ { \left[ \mathbf{ W }{ k,R }\mathbf{ R }{ 1 } \right] }^{ T } \ { \left[ \mathbf{ W }{ k,R }\mathbf{ R }{ 0 } \right] }^{ T } \end{bmatrix}=\begin{bmatrix} { \mathbf{ Q }{ 0 } }^{ T } \ { \mathbf{ Q }{ 1 } }^{ T } \ { \mathbf{ Q }{ 2 } }^{ T } \ { \mathbf{ Q }{ 3 } }^{ T } \ { \mathbf{ Q }{ 4 } }^{ T } \ { \mathbf{ Q }{ 5 } }^{ T } \end{bmatrix}]

$\mathbf{W_q}\mathbf{E_{x_i}}$를 $\mathbf{q_i}$라 둡시다. 그림 18의 위쪽 그림과 같은 상황에서 수식 8을 참고해 (b)를 계산한 결과는 그림 18의 아래쪽 그림과 같습니다.

그림 18. 상대 위치 셀프 어텐션 계산 예시 (2)

이와 별개로 각 쿼리 벡터들을 모아 만든 행렬을 $\mathbf{q}$라고 둡시다. 그러면 $\mathbf{q}$와 $\mathbf{Q}$를 내적한 $\mathbf{qQ}$를 시각화한 것은 그림 19와 같습니다. 그림 18의 아래쪽 그림과 비교해서 봅시다. $\mathbf{qQ}$(그림 19)의 각 행별로 한 칸씩 왼쪽으로 옮기면 우리가 원하는 결과(그림 18 아래쪽)를 얻을 수 있습니다.

요컨대 쿼리 벡터를 모아 만든 행렬 $\mathbf{q}$와 상대 위치 정보가 담긴 $\mathbf{Q}$를 내적하고 살짝 후처리만 해주는 것으로도 상대 위치 셀프 어텐션을 계산할 수 있습니다. 행렬 내적 연산은 텐서플로 등 다양한 라이브러리에서 최적화되어 있기 때문에 상대 위치 계산에 큰 비용이 들지 않습니다. 수식 7의 (d)도 이와 유사한 방식으로 효율적으로 계산할 수 있습니다.

그림 19. 상대 위치 셀프 어텐션 계산 예시 (3)

모델 구현

이 절에서는 XLNet의 실제 구현 방식을 살펴봅니다. 우선 학습데이터를 만드는 코드는 data_utils.py에 정의되어 있습니다. 이 코드를 실행하면 파일 단위로 분리되어 있는 말뭉치를 읽어들여 XLNet이 학습할 수 있는 tfrecord 파일로 변환합니다. 이 파일엔 모델의 입력인 토큰 ID 시퀀스(input), 출력인 토큰 ID 시퀀스(target), 세그먼트 ID 시퀀스(seg_id) 등 정보를 배치(batch)별로 기록해 둡니다.

input은 원래 문장을 구글 센텐스피스(sentencepiece) 패키지로 토크나이즈한 뒤 정수(integer) ID 시퀀스로 바꿔놓은 것입니다. targetinput을 한 칸씩 왼쪽으로 옮겨놓은 형태입니다. XLNet은 이전 단어 시퀀스로 다음 단어를 맞추는 언어모델이기 때문입니다. seg_id는 이전 세그먼트에 속하는 토큰들은 ID가 0, 현재 세그먼트에 속하는 토큰들은 ID가 1인 리스트입니다. 이밖에 기록되는 정보로 is_maskedlabel도 있는데 이후 설명하겠습니다.

그림 20은 원본 학습데이터의 예시다. 문장과 문장 사이는 줄바꿈 1개, 단락과 단락 사이는 <eop>, 문서와 문서 사이는 줄바꿈 2개로 구분합니다.

그림 20. XLNet 원본 학습데이터

이것은 첫번째 문장(sentence)입니다.
이것은 두번째 문장이며 단락(paragraph)의 마지막 문장입니다.\<eop>
이것은 두번째 단락입니다.

이것은 두번째 문서입니다.
(하략)

input은 배치 사이즈(bsz_per_host) x 최대 시퀀스 길이(seq_len)의 크기의 행렬입니다. input 행렬의 행 하나는 다음과 같이 구성돼 있습니다. 이전 세그먼트의 마지막 문장과 현재 세그먼트의 첫번째 문장은 말뭉치에서 실제 이어진 문장을 사용합니다.

  • 이전 세그먼트(길이 : reuse_len) + 현재 세그먼트

현재 세그먼트는 다음과 같이 구성돼 있습니다. 아래에서 SEP는 문장과 문장 사이를 구분하는 역할을 하는 스페셜 토큰이며 CLS는 문서의 종료를 알리는 스페셜 토큰입니다. Dai et al. (2019)도 BERT처럼 현재 세그먼트에 문장이 두 개(A, B)가 들어가도록 설계했습니다. 50%의 확률로 실제 이어진 문장 두 개, 나머지 50%의 확률로 이어지지 않는 문장 두 개를 A, B로 사용합니다.

이어진 문장을 썼을 경우 label 1, 랜덤 선택한 두 문장을 사용했다면 0을 부여합니다. Dai et al. (2019)에 따르면 BERT와 달리 다음 문장 예측(Next Sentence Prediction)은 XLNet 성능 향상엔 큰 도움이 되지 않는다고 합니다. 실제 XLNet 권장 파라메터에도 NSP가 빠져 있습니다. 어쨌든 문장 A와 B를 나누는 함수는 data_utils.py_split_a_and_b입니다.

  • 문장A + SEP + 문장B + SEP + CLS

Dai et al. (2019)는 프리트레인 계산량을 줄이기 위한 전략으로 부분 예측(partial prediction) 기법을 사용합니다. 입력값의 부분 집합만을 학습하는 것입니다. 이에 Dai et al. (2019)는 어텐션 마스크과 별개로 is_masked라는 리스트(list) 형태의 변수를 만들었습니다. is_masked의 요소가 True라면 해당 토큰을 학습에 포함하고, False이면 제외합니다. _sample_maskis_masked 변수를 만드는 함수로 랜덤으로 마스크를 생성하되 가급적 n-gram 단위로 마스킹합니다.

예컨대 네 개 단어로 구성된 문장의 토큰 인덱스를 랜덤 셔플한 결과가 $[3, 2, 4, 1]$이고 _sample_mask 함수가 뽑은 is_masked가 [False, False, True, True]라고 가정해 봅시다. 그러면 XLNet 모델은 수식 9와 같이 앞에서 한 칸씩 순차적으로 모두 학습하는 대신 앞의 두 개 단어를 학습에서 제외합니다.

수식 9. 부분 예측

[P\left( { x }{ 3 } \right) P\left( { x }{ 2 } { x }{ 3 } \right) P\left( { x }{ 4 } { x }{ 3,2 } \right) P\left( { x }{ 1 } { x }{ 3,2,4 } \right) \ \rightarrow P\left( { x }{ 4 } { x }{ 3,2 } \right) P\left( { x }{ 1 } { x }_{ 3,2,4 } \right)]

tfrecord를 구축했다면 XLNet 프리트레인을 시작할 준비를 마친 것입니다. data_utils.py_local_perm 함수는 5.7.1절 퍼뮤테이션 언어모델을 학습하기 위한 어텐션 마스크를 생성하는 역할을 합니다. 모델은 프리트레인을 시작하고 tfrecord를 읽은 다음에야 _local_perm를 호출합니다. 다시 말해 어텐션 마스크를 미리 만들어두는 것이 아니라 학습 과정에서 실시간으로 생성한다는 이야기입니다.

코드 2는 _local_perm 함수에서 토큰 인덱스 리스트를 퍼뮤테이션하는 부분만을 발췌한 것입니다. 퍼뮤테이션 가능한 모든 경우의 수(단어가 $n$개인 문장일 경우 $n!$가지)를 학습하는 게 아니라 토큰 인덱스를 랜덤으로 셔플하는 걸 1회 시행(sample)하고 해당 퍼뮤테이션 시퀀스로 어텐션 마스크를 만들고 있음을 확인할 수 있습니다.

코드 2. 퍼뮤테이션

def _local_perm(inputs, targets, is_masked, 
                perm_size, seq_len):
  """
  Sample a permutation of the factorization order, 
  and create an attention mask accordingly.
	"""
  ...
  index = tf.range(seq_len, dtype=tf.int64)
  index = tf.transpose(tf.reshape(index, [-1, perm_size]))
  index = tf.random_shuffle(index)
  index = tf.reshape(tf.transpose(index), [-1])
  ...

_local_perm 함수가 어텐션 마스크를 생성하는 과정을 간단한 예제와 함께 살펴봅시다. 예컨대 아래 같은 문장A 정보를 바탕으로 문장B를 예측하는 상황을 가정해 봅시다. 설명의 편의를 위해 이전 세그먼트(메모리)를 고려하지는 않고 현재 세그먼트만을 위한 어텐션 마스크를 만들어 봅니다.

  • 문장A : 지금 어디야?
  • 문장B : 나 학교

앞서 설명한 바와 같이 현재 세그먼트에 해당하는 입력은 문장A + SEP + 문장B + SEP + CLS입니다. input은 문장A와 B를 센텐스피스 패키지로 토크나이즈한 뒤 정수 ID로 바꾸고, 여기에 SEP ID(=4), CLS ID(=3)를 추가한 리스트입니다. 그 결과가 아래와 같다고 가정합시다.

  • 문장A 토크나이즈 결과 = [지금, 어디야, ?]
  • 문장A의 ID 시퀀스 = [10, 20, 30]
  • 문장B 토크나이즈 결과 = [나, 학교]
  • 문장B의 ID 시퀀스 = [40, 50]
  • input = [10, 20, 30, 4, 40, 50, 4, 3]

우리는 문장A를 바탕으로 문장B를 예측하는 상황을 가정했으므로 is_masked는 아래처럼 정하는 것이 자연스럽습니다. 다시 말해 문장B(나, 학교)에만 is_maskedTrue로 켜 놓는 것입니다. 코드 2에서 퍼뮤테이션 한 결과(index) 역시 우연히 원래 토큰 시퀀스 순서와 동일하다고 가정합시다.

물론 실제 프리트레인 과정에서는 _sample_mask, _local_perm 함수가 각각 is_maskedindex를 랜덤으로 정하기 때문에 이런 상황은 자주 발생하지 않습니다. 이해를 돕기 위한 예시이니 일단은 이렇다고 받아들입시다.

  • is_masked = [False, False, False, False, True, True, False, False]
  • index = [0,1,2,3,4,5,6,7]

코드 3은 _local_perm 함수를 거의 그대로 가져온 것입니다. 다만 우리가 가정한 상황에 맞는 어텐션 마스크(perm_mask)를 출력해보기 위해 코드를 약간 고쳤다. 코드 3을 파이썬 콘솔에서 실행하면 그림 20과 같은 결과를 볼 수 있다.

코드 3. 어텐션 마스크 생성 예시 (python)

import tensorflow as tf
CLS_ID, SEP_ID, seq_len = 3, 4, 8
inputs = [10,20,30,4,40,50,4,3]
is_masked = [False, False, False, False, True, True, False, False]
index = [0,1,2,3,4,5,6,7]
non_func_tokens = tf.logical_not(tf.logical_or(
      tf.equal(inputs, SEP_ID),
      tf.equal(inputs, CLS_ID)))
non_mask_tokens = tf.logical_and(tf.logical_not(is_masked), non_func_tokens)
masked_or_func_tokens = tf.logical_not(non_mask_tokens)
smallest_index = -tf.ones([seq_len], dtype=tf.int64)
rev_index = tf.where(non_mask_tokens, smallest_index, index)
target_tokens = tf.logical_and(masked_or_func_tokens, non_func_tokens)
target_mask = tf.cast(target_tokens, tf.float32)
self_rev_index = tf.where(target_tokens, rev_index, rev_index + 1)
perm_mask = tf.logical_and(
      self_rev_index[:, None] <= rev_index[None, :],
      masked_or_func_tokens)
perm_mask = tf.cast(perm_mask, tf.float32)
sess = tf.Session()
sess.run(perm_mask)

그림 20의 행렬은 seq_len x seq_len 크기의 정방행렬입니다. 각 행은 쿼리 단어, 열은 키 단어에 대응합니다. 행렬 요소값이 0이면 해당 쿼리 단어를 예측할 때 해당 키 단어 정보를 셀프 어텐션 계산에 활용하고 1이면 제외합니다.

예컨대 이번 스텝에서 학교를 맞춰야 한다고 해 봅시다(그림 20 녹색 박스). 다시 말해 쿼리 단어가 학교인 상황입니다. 그러면 모델은 지금, 어디야, ?, SEP, 를 바탕으로 학교라는 단어를 맞춰야 합니다. 이때 자기 자신(학교) 정보가 들어가는 건 반칙이므로 6행, 6열의 요소값은 1(파란색 표시)로 해둡니다. 물론 맞춰야할 단어 이후의 정보들도 이번 예측에 포함되면 안되므로 6행 7열, 6행 8열도 1로 합니다.

우리는 문장A를 바탕으로 문장B를 맞춰야 한다고 가정했습니다(부분 예측). 따라서 변수 문장A에 속한 단어와 관련된 정보는 처음부터 모델에 모두 줍니다(1~3행, 보라색 박스 표시). 단 모델이 문장B를 맞춰야 하는 순서가 오기 전까지는 문장B 정보를 가려놓습니다(5~6열, 노란색 박스 표시). 모델이 SEP, CLS 같은 스페셜 토큰 정보를 미리 보더라도 큰 의미가 없으므로 스페셜 토큰 정보 또한 제외합니다(4열, 7~8열, 붉은색 박스 표시).

한편 Dai et al. (2019)는 SEP가 쿼리 단어일 때는 모델 입력에 SEP 정보를 포함하고, 정답(target)으로 SEP 다음 단어(문장B의 첫 단어나 CLS)를 주도록 설계했습니다. 언어모델 학습 과정에서 SEP 토큰의 타겟으로 쓸 만한 것이 마땅치 않기 때문입니다.

예컨대 모델이 지금, 어디야, ?를 입력받았을 때 문장B의 첫 단어인 를 출력하도록 합니다. 결과적으로 이번 타겟이 SEP가 아니게 됐으니, 이번 스텝에는 SEP 정보를 줘도 관계 없습니다(4행 4열, 오렌지색 표시). 마지막으로 CLS가 쿼리 단어일 때는 이후 맞춰야할 타겟이 없으므로 모델 입력에 자기 자신(CLS)도 넣습니다(8행 8열, 오렌지색 표시).

그림 20. 어텐션 마스크 생성 예시

이제 트랜스포머-XL 메인 함수를 살펴봅시다. 코드 4는 트랜스포머-XL 블록의 전반부를 간략하게 정리한 것입니다. 메인 함수는 modeing.py에 정의돼 있습니다. inp_k는 토큰 ID 시퀀스를 가리킵니다. 이전 세그먼트와 현재 세그먼트(문장A + SEP + 문장B + SEP + CLS) 토큰 ID들입니다. 이 ID들에 해당하는 토큰 임베딩을 참조(embedding_lookup)해 만든 행렬이 바로 word_emb_k입니다. 컨텐트 스트림(output_h)의 초기값은 word_emb_k입니다. 한편 쿼리 스트림(output_g)의 초기값은 랜덤 초기화한 mask_emb_q입니다.

pos_emb는 상대 위치와 관련한 임베딩 행렬입니다. 이 행렬 구축에 관련된 함수를 따라가다 보면 tf.sin, tf.cos 등으로 만들어 그래디언트 전파로 학습하는 파라메터가 아님을 다시 한번 확인할 수 있습니다. 아울러 코드 4에는 표시되어 있지 않지만 세그먼트 정보에 해당하는 입력 행렬 seg_mat도 구축해 둡니다. 이 행렬은 word_emb_k와 같은 크기로 세그먼트를 구분해주는 역할을 합니다.

코드 4. 트랜스포머-XL (1)

def transformer_xl(inp_k, n_token, n_layer, d_model, 
                n_head, d_head, d_inner, dropout, dropatt, 
                attn_type, bi_data, initializer, 
                is_training, mem_len=None,
                inp_q=None, mems=None,
                same_length=False, clamp_len=-1, 
                untie_r=False, use_tpu=True, 
                input_mask=None, perm_mask=None, 
                seg_id=None, reuse_len=None,
                ff_activation='relu', target_mapping=None,
                use_bfloat16=False, scope='transformer', 
                **kwargs):
  ...
	with tf.variable_scope(scope):
    word_emb_k, lookup_table = embedding_lookup(x=inp_k, ...)
		...
    mask_emb = tf.get_variable('mask_emb', [1, 1, d_model], dtype=tf_float)
		...
    word_emb_q = tf.tile(mask_emb, 
                         [tf.shape(target_mapping)[0], bsz, 1])
		...
    output_h = tf.layers.dropout(word_emb_k, 
                                 dropout, 
                                 training=is_training)
		...
    output_g = tf.layers.dropout(word_emb_q, 
                                 dropout, 
                                 training=is_training)
		...
    pos_emb = relative_positional_encoding(
        qlen, klen, d_model, clamp_len, attn_type, bi_data,
        bsz=bsz, dtype=tf_float)
    pos_emb = tf.layers.dropout(pos_emb, dropout, 
                                training=is_training)

코드 5는 트랜스포머-XL을 본격적으로 계산합니다. inp_q라는 변수는 부분 예측(partial prediction)과 관련이 있습니다. 예컨대 네 개 단어로 구성돼 있고 두번째 단어만 부분 예측을 한다면 inp_q는 $[0, 1, 0, 0]$이 됩니다. 부분 예측은 프리트레인 과정에서만 수행하는 것으로 inp_q에 어떤 값이 있다(not None)면 프리트레인을 수행하고 있는 것으로 간주합니다. 반대로 값이 없다면 파인튜닝이라고 인식합니다.

현재 학습이 프리트레인(inp_q is not None)이라고 가정해 봅시다. 그러면 모델은 투-스트림 셀프어텐션 연산(two_stream_rel_attn 함수)을 수행해 컨텐트 스트림(output_h)과 쿼리 스트림(output_g)을 업데이트합니다. 코드를 자세히 보면 이전 레이어의 컨텐트/쿼리 스트림 출력값이 이번 레이어 컨텐트/쿼리 스트림의 입력값입니다.

이후 쿼리 스트림에 positionwise_ffn 함수를 적용합니다. 이 함수가 수행하는 역할은 BERT의 포인트 와이즈 피드포워드 뉴럴네트워크(Point-wise Feed-Forward Networks)와 같습니다. 모델의 최종 출력(output)은 마지막 레이어의 각 토큰별 쿼리 스트림(output_g)입니다.

attn_mask는 쿼리 스트림을 만들기 위한 어텐션 마스크입니다. XLNet 코드를 따라가다 보면 attn_mask는 자기 자신의 정보를 보지 못하도록 어텐션 마스크가 구성돼 있습니다. 반면 non_tgt_mask는 자기 자신 정보도 볼 수 있도록 구축한 어텐션 마스크이다. 컨텐트 스트림을 계산하는 데 쓰입니다.

파인튜닝 때는 컨텐트 스트림(output_h)만을 계산합니다. 기존 트랜스포머-XL 네트워크처럼 rel_multihead_attn 함수를 적용합니다. 파인튜닝 모델의 최종 출력은 마지막 레이어의 각 토큰별 컨텐트 스트림(output_h)입니다.

코드 5. 트랜스포머-XL (2)

    ...
    for i in range(n_layer):
      # cache new mems
      new_mems.append(_cache_mem(output_h, mems[i], mem_len, reuse_len))
			...
      with tf.variable_scope('layer_{}'.format(i)):
        if inp_q is not None:
          output_h, output_g = two_stream_rel_attn(
              h=output_h,
              g=output_g,
              r=pos_emb,
              ...
              attn_mask_h=non_tgt_mask,
              attn_mask_g=attn_mask,
              ...)
          reuse = True
        else:
          reuse = False
          output_h = rel_multihead_attn(
              h=output_h,
              r=pos_emb,
              ...
              attn_mask=non_tgt_mask,
              ...)
        if inp_q is not None:
          output_g = positionwise_ffn(inp=output_g, ...)
        output_h = positionwise_ffn(inp=output_h, ...)
    if inp_q is not None:
      output = tf.layers.dropout(output_g, dropout, 
                                 training=is_training)
    else:
      output = tf.layers.dropout(output_h, dropout, 
                                 training=is_training)
    return output, new_mems, lookup_table

XLNet은 이전 문맥으로 다음 단어를 맞추는 언어모델입니다. 언어모델 학습을 위한 마지막 레이어는 코드 6과 같이 정의합니다. 히든(hidden) 벡터 하나를 가지고 선형변환을 한 뒤 소프트맥스 함수를 적용하고 여기에 정답 단어와 비교해 크로스 엔트로피 손실(cross entropy loss)을 구합니다. 이후 학습 손실을 최소화하는 방향으로 모델 전체 파라메터를 업데이트하게 됩니다. 코드 6은 modeling.py에 정의돼 있습니다.

여기에서 손실 계산에 쓰이는 히든 벡터는 트랜스포머-XL 마지막 레이어의 출력 행렬(output) 가운데 하나의 벡터입니다. 언어모델 학습 손실을 구할 때 히든 벡터를 하나만 쓰는 이유는, 투-스트림 셀프어텐션 기법 덕분입니다. 해당 히든 벡터에 자기 자신을 제외한 이전 문맥 정보(메모리 정보 포함) 모두가 녹아 있습니다.

코드 6. 언어모델 레이어

def lm_loss(hidden, target, n_token, d_model, initializer, lookup_table=None,
            tie_weight=False, bi_data=True, use_tpu=False):
  ...
  with tf.variable_scope('lm_loss'):
    if tie_weight:
      assert lookup_table is not None, \
          'lookup_table cannot be None for tie_weight'
      softmax_w = lookup_table
    else:
      softmax_w = tf.get_variable('weight', 
                                  [n_token, d_model],
                                  dtype=hidden.dtype,
                                  initializer=initializer)
    softmax_b = tf.get_variable(
                  'bias', [n_token], dtype=hidden.dtype,
                  initializer=tf.zeros_initializer())
    logits = tf.einsum('ibd,nd->ibn', hidden, softmax_w) + softmax_b
		...
    loss = tf.nn.sparse_softmax_cross_entropy_with_logits(
                          labels=target, logits=logits)
    return loss

프리트레이닝 튜토리얼

Dai et al. (2019)는 벡터, 행렬 병렬처리에 특화된 TPU(Tensor Processing Unit) 환경에서 XLNet을 프리트레인하였습니다. 이 환경에서라면 트랜스포머-XL 블록을 24개 쌓은 XLNet-Base 모델도 배치 크기를 2048개 적용할 수 있어 프리트레인을 2.5일만에 끝낼 수 있다고 합니다. TPU 환경을 쓸 수 있는 독자는 아래의 XLNet 공식 레파지토리의 프리트레인 가이드를 참고하면 됩니다.

하지만 TPU 환경을 이용하려면 비싼 돈(2019년 하반기 기준 시간당 6.5달러)을 내야 합니다. 더구나 일반 GPU 환경에서는 이만한 크기의 모델을 메모리에 올리기조차 힘듭니다. 따라서 여기에서는 작은 말뭉치(네이버 영화 리뷰), 작은 레이어(3개)의 미니 XLNet을 GPU 환경에서 프레트레인하는 방법을 실습합니다.

우선 개발환경을 구축해야 합니다. 프리트레인 튜토리얼은 도커 이미지 위에서 동작하도록 만들었는데요. 다음 링크를 따라가면 도커 컨테이너를 띄우는 방법이 안내되어 있습니다.

  • https://ratsgo.github.io/embedding/environment.html

그다음 프리트레인 데이터를 준비합니다. 코드 7을 실행해 전처리가 완료된 네이버 영화 리뷰 데이터를 내려받습니다. 이후 코드 8을 실행해 그림 20과 같은 형태의 문서로 만듭니다. 단, 네이버 영화 리뷰 데이터엔 단락 구분이 없으므로 영화 댓글 하나를 문서 하나로 보고 데이터를 처리합니다. 원시 말뭉치가 너무 크면 전처리가 너무 느려질 수 있으므로 말뭉치를 30만 줄(line) 단위로 분리하였습니다. 다른 데이터로 프리트레인 하고 싶다면 코드 8의 input_path에 해당 파일 경로를 적어주면 됩니다.

코드 7. 전처리 완료된 말뭉치 다운로드 (bash)

git pull origin master
bash preprocess.sh dump-processed

코드 8. XLNet 데이터 전처리 (bash)

mkdir -p /notebooks/embedding/data/sentence-embeddings/pretrain-data
python preprocess/dump.py --preprocess_mode process-documents --input_path /notebooks/embedding/data/processed/corrected_ratings_corpus.txt --output_path /notebooks/embedding/data/processed/pretrain.txt
split -l 300000 /notebooks/embedding/data/processed/pretrain.txt /notebooks/embedding/data/sentence-embeddings/pretrain-data/data_

코드 9와 코드 10을 실행하면 XLNet이 쓰는 어휘 집합과 학습데이터(tfrecord 형태)를 구축할 수 있습니다.

코드 9. XLNet 어휘 집합 구축 (bash)

mkdir -p /notebooks/embedding/data/sentence-embeddings/xlnet/pretrain-ckpt
python preprocess/unsupervised_nlputils.py --preprocess_mode make_xlnet_vocab --input_path /notebooks/embedding/data/processed/corrected_ratings_corpus.txt --vocab_path /notebooks/embedding/data/sentence-embeddings/xlnet/pretrain-ckpt/sp10m.cased.v3

코드 10. XLNet tfrecord 학습데이터 구축 (bash)

cd models/xlnet
python data_utils.py --bsz_per_host=16 --num_core_per_host=1 --seq_len=256 --reuse_len=128 --input_glob=/notebooks/embedding/data/sentence-embeddings/pretrain-data/* --save_dir=/notebooks/embedding/data/sentence-embeddings/xlnet/pretrain-ckpt --num_passes=10 --bi_data=True --sp_path=/notebooks/embedding/data/sentence-embeddings/xlnet/pretrain-ckpt/sp10m.cased.v3.model --mask_alpha=6 --mask_beta=1 --num_predict=45

프리트레인을 시작하려면 코드 11을 실행하면 됩니다.

코드 11. XLNet 프리트레인 (bash)

python train_gpu.py --record_info_dir=/notebooks/embedding/data/sentence-embeddings/xlnet/pretrain-ckpt/tfrecords --model_dir=/notebooks/embedding/data/sentence-embeddings/xlnet/pretrain-ckpt --train_batch_size=16 --seq_len=256 --reuse_len=128 --mem_len=192 --perm_size=128 --n_layer=3 --d_model=512 --d_embed=512 --n_head=8 --d_head=32 --d_inner=2048 --uncased=True --untie_r=True --mask_alpha=6 --mask_beta=1 --num_predict=45 --save_steps=10000

코드 12는 XLNet 프리트레인을 자동화한 스크립트입니다. 코드 8~11을 한꺼번에 수행합니다.

코드 12. XLNet 프리트레인 자동화 스크립트 (bash)

git pull origin master
bash sentmodel.sh pretrain-xlnet

Comment  Read more

이탈리아 여행

|

지난 4일부터 1주일 간 로마를 시작으로 피렌체, 아시시, 베니스, 밀라노 5개 도시를 방문했다. 이 느낌을 오래 간직하기 위해 짧은 인상 위주로 정리해 둔다. (2018년 2월 11일 밀라노 말펜사 공항)

#01. 환승하기 위해 잠시 들렀던 터키 이스탄불 소재 아타튀르(Atatürk) 국제공항 서비스는 형편없었다. 환승 게이트 안내는 비행기 탑승 1시간 전에야 이뤄져서 환승객들이 공항 곳곳에서 갈팡질팡했다. 비행기 탑승 30분 전 게이트가 오픈된 것까지는 좋았다. 승객들을 게이트에서 스텝카(탑승용 계단차량)까지 실어나르는 버스에 가둬두고는 탑승시각이 넘어서까지 출발도 않고 대기하는 것 아닌가. 게이트를 관리하는 직원은 단 한 명. 항공편이 특별히 지연(delay)되어야 할 물리적 이유가 하나도 없었는데 이날 이스탄불발-로마행 TK1861편은 30여분 가까이 늑장 출발했다. 그에 반하면 인천공항은 정말이지 세계 최고 수준이다.

#02. TK1861편 창가에 비친 이탈리아의 첫 인상은 ‘따스한 햇살이 비치는 평온한 대평원’이었다. 날씨가 좋아서 로마 주변을 전체적으로 조망할 수 있었다. 드넓은 초지가 펼쳐져 있고 군데군데 조그마한 촌락들이 있으며 그 촌락들 사이를 거미줄처럼 잇는 길이 나 있었다. 자료를 찾아보니 테베레 강 유역, 아펜니노 산맥과 티레니아해 사이에 있는 넓은 평야지대를 ‘라티움(Latium)’이라고 한단다. 초기 고대 로마가 이곳을 중심으로 성장했다고 한다. 역시 그러면 그렇지. 라티움 같은 배후 생산지역이 없었다면 로마 같은 소비 도시는 탄생하기 어려웠을 것이다. 로마는 예나 지금이나 향락이 중심이다.

#03. 짐을 숙소에 내팽겨치다시피해서 처음 방문한 곳은 ‘콜로세움(Colosseum)’. 고대 로마 시대를 대표하는 원형 경기장이다. 테르미니역에 있는 숙소에서 멀지 않아서 우연히 Parco Del Colle Oppio 공원을 거쳐 가게 됐다. 그런데 공원에서 콜로세움을 후면에서 볼 수 있는 것 아닌가! 그것도 한적한 벤치까지 마련돼 있다. 지중해 따뜻한 겨울햇살을 내리쬐며 벤치에 앉아 한가로이 콜로세움을 바라보는 경험은 황홀함 그 자체였다. 이 글을 쓰는 지금도 그 때 그 감동을 잊을 수가 없다.

#04. 콜로세움을 기점으로 베네치아 광장(Piazza Venezia)에 이르는 거리 ‘Via dei Fori Imperiali’는 일요일이면 차없는 거리로 변신한다. 덕분에 온갖 행위예술인들이 세계 관광객들의 이목을 끈다. 특히 포룸 로마눔(Forum Romanum) 앞에서 공연하던 라틴 형제 3인방이 기억에 남는다. 기타 2, 콘트라베이스 1로 구성된 이들은 연주도 연주지만 한때 번성했지만 흔적만 남아있는 로마 중심 시가지를 배경으로 빼어난 실력을 뽐내고 있어 무척 아이러니하게 느껴졌다. 맥수지탄(麥秀之歎)이나 산 사람은 어찌됐든 살아야 한다. 3인방 중 한 명이 공짜로 주겠다며 CD 두 장을 내게 건넸다. 공짜인데 어찌 마다하겠는가. 그런데 노래 두 곡이 끝나고 나서 20유로를 달란다. 허허.. 주머니를 뒤집어 돈 없다는 시늉을 했다. 대신 노랫값만 내고 슬그머니 빠져나왔다. 역시 산 사람은 살아야 한다.

#05. 바티칸 시국 남동쪽에 있는 ‘성 베드로 대성당(Basilica di San Pietro)’과 바티칸 궁전 내 시스티나 성당(Aedicula Sixtina)은 이탈리아 여행 전체를 통틀어 최고라 할 만 하다. 규모도 웅장하고 장식과 그림 하나하나 정성이 깃들어 있다. 신자가 아니라도 절로 경외감이 들 정도로. 미켈란젤로가 시스티나 성당 벽에 그린 그림들이 압권이다. 1시간 넘게 ‘아담의 창조(천장)’, ‘최후의 심판(제단 쪽 벽면 전체)’을 구석구석 보느라 목 빠지는 줄 알았다. 당시 그들은 어떤 마음으로 프레스코화를 그렸을까. 그 신앙심이 존경스럽다.

#06. 로마에서 기차로 2시간여 거리인 소도시 아시시(Assisi). 평생 세속과는 멀리 하고 길거리에서 복음을 전파한 성 프란치스코(San Francesco, 1181-1226)가 이곳에 잠들어 있다. 프란치스코의 유해가 안치된 성 프란치스코 대성당 지하엔 성음악이 흐르고 있다. 거리는 적막할 정도로 조용하다. 그의 추종자들은 지금까지도 이 촌락에서 금욕적인 삶을 추구하며 수도 생활을 하고 있다. 그러나 상술 하나만큼은 철저히 세속적이다. 토마토와 치즈를 곁들인 피아디나(piadina)와 콜라 한 캔에 12유로(우리돈 1만7000원 가량)나 한다. 놀랍다.

#07. 피렌체에 있는 우피치 미술관(Galleria degli Uffizi)는 실로 대단하다. 그 빼어나고 방대한 작품들 모두 한 가문(메디치)이 기증한 것이라니. 미술에는 그다지 조예가 없어서 사람들이 많이들 감상하고 있는 작품들을 위주로 유심히 살펴보았다. 어떤 작품이든 자세히 들여다보면 그 디테일에 감탄하지 않을 수 없었다. 나중에 블로그 대문사진이나 휴대폰 잠금화면으로 쓰려고 그림 사진을 많이 찍어두었다.

#08. 비 갠 후 노을 진 피렌체 시내 겨울 풍광은 무척 아름답다. 피렌체 대성당의 쿠폴라(cupola)를 오르는 463개의 계단이 조금 버겁기는 하지만. 비가 주륵주륵 오는데도 기다린 보람이 있었다.

#09. 베니스엔 차가 없다. 바다와 운하를 오가는 곤돌라(gondola)들뿐이다. 경찰차, 구급차도 모두 곤돌라다. 버스도 물 위를 다닌다. 예전 베니스 귀족들은 곤돌라 치장에 꽤 많은 돈을 썼다 한다. 차 대신에 말이다. 어쨌든 저렴하고 탈 만한 지상 교통수단이 없어 베니스 시내 전체를 계속 걸어다닐 수밖에 없었다.

#10. 운좋게 베니스 2월 축제를 볼 수 있었다. 생각지도 못한 수확이었다. 춤과 노래는 언제나 흥겹다. 형형색색의 옷과 가면으로 치장한 사람들은 국적과 관계없이 거리에서 모두 친구가 됐다.

#11. 밀라노는 세계 패션 중심이다. 명품 상점들이 즐비하다. 행인들도 꽤 멋쟁이들인 것 같다. 하지만 밀라노는 화장실 인심이 박하다. 크디큰 쇼핑몰 안에 공용 화장실 하나 찾을 수가 없다. 가끔 찾는다 해도 0.5~1.5유로를 내야 한다. 그런데 저 수많은 사람들은 어디서 똥 누고 오줌을 싸는 걸까. 패션보다 중한 건 용변 해결일텐데. 용무가 급해서 공용 화장실을 찾느라 밀라노 중심가를 이리저리 뛰어다니다 든 생각이다.

Comment  Read more

밀라노의 상점들

|

지난 4일부터 1주일 간 이탈리아를 여행하고 있다. 마지막 행선지는 ‘세계 패션 중심’ 밀라노(Milano). 명성에 걸맞게 내로라할 만한 명품 브랜드 상점들이 거리에 즐비했다. 간판과 쇼윈도부터 행인들의 눈길을 확 끈다. 물건 그 자체보다는 감성과 경험을 내세운다. (내 입장에서)처음 보는 브랜드인데도 쇼윈도만 유심히 보면 이 가게가 뭘 팔고 뭘 내세울지 한 눈에 이해할 수 있었다. 책 한 권 팔더라도 고급지다(서점 Rizzoli). 브랜딩이니, 마케팅이니, 플랫폼이니 고민해야 하는 순간이 온다면 오늘 거닐었던 밀라노 거리 상점들을 잊지 말아야겠다. (2018년 2월 10일 밀라노)

Comment  Read more

Auto Regressive Models

|

이번 글에서는 Auto Regressive Model(AR)에 대해 살펴보도록 하겠습니다. 이 글은 전인수 서울대 박사과정이 2017년 12월에 진행한 패스트캠퍼스 강의와 위키피디아 등을 정리했음을 먼저 밝힙니다. 그럼 시작하겠습니다.

concept

자기 자신을 입력으로 하여 자기 자신을 예측하는 모형을 Auto Regressive Model(AR)이라고 합니다. 그 개념도와 likelihood 식은 다음과 같습니다.

[\begin{align} p\left( x \right) =&\coprod _{ i }^{ }{ p\left( { { x }_{ i } }|{ { x }_{ 1 },…,{ x }_{ i-1 } } \right) } \ =&p\left( { x }_{ 1 } \right) p\left( { x }_{ 2 }|{ x }_{ 1 } \right) …p\left( { { x }_{ i } }|{ { x }_{ 1 },…,{ x }_{ i-1 } } \right) \end{align}]

저해상도 이미지/영상을 고해상도로 변환하는 작업을 Super Resolution(SR)이라고 합니다. SR을 AR modeling 관점에서 이해할 수 있습니다. 아래 그림처럼 고해상도 이미지/영상을 픽셀 단위로 예측하는 경우, $x_i$를 예측할 때는 이전의 모든 픽셀 예측 결과를 활용하게 됩니다.

앞으로 설명해드릴 PixelRNN과 PixelCNN은 SR을 AR modeling으로 해결해 보려는 시도들입니다.

PixelRNN

Recurrent Neural Network(RNN)는 시퀀스 데이터 처리에 특화된 아키텍처이므로 SR을 AR modeling으로 풀 때 적용해 봄직한 시도입니다. 아래 그림의 빨간색 픽셀을 예측할 때 이전의 모든 시퀀스 정보를 RNN 아키텍처에 넣어서 예측이 가능합니다.

PixelCNN

Convolutional Neural Network(CNN)는 본래 시퀀스 데이터 처리와는 직접 관련이 없으나 아래 그림처럼 Masked Convolution Filter를 사용하면 AR modeling이 가능합니다. 예측해야 하는 시점의 픽셀(아래 행렬의 정중앙 픽셀)과 아직 예측하지 않은 시점의 픽셀에 해당하는 필터 값을 0으로 설정해 두고, 이를 일반적인 conv layer에 적용하면 CNN을 가지고 AR modeling을 할 수 있습니다.

WaveNet

WaveNet은 음성 생성에 Masked Convolution Filter를 활용한 사례입니다. conv filter를 아래처럼 설정해 두면 현 시점 예측 때 과거 다양한 시점의 데이터를 사용하게 됩니다.

Comment  Read more