├── LICENSE ├── README.md ├── Requirements.txt ├── image ├── alexnet2.png ├── char2.PNG ├── rnn.jpg ├── selfAA.png ├── skipgram.png ├── tfidf.png └── ykim14.png └── kor_char.npy /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 warnikchow 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DLK2NLP: Day-by-day Line-by-line Keras-based Korean NLP 2 | ## Sentence classification: From data construction to self-attentive BiLSTM 3 | ### The tutorial is provided in both English (for *Korean* NLP learners) and Korean (for Korean *NLP* learners) - but the meanings may differ. 4 | * 문장 분류 task를 중심으로 본 한국어 NLP 튜토리얼입니다. 본 튜토리얼에 사용된 컨텐츠 일부는 [페이스북 페이지](https://www.facebook.com/nobodybelongs/notes/)에 기고했던 글에서 발췌하였음을 명시합니다. 한국어 NLP를 하고 싶은 외국인들과 NLP를 배워보고 싶은 한국인들을 모두 대상으로 하여 영/한 설명을 모두 제공하나, 번역이 아닌 의역이며 내용도 다를 수 있습니다. 5 | 6 | ## Requirements 7 | #### The recommended versions are in *Requirements.txt*, but can be replaced depending on the environment 8 | fasttext (Gensim if inavailable), Keras, konlpy (refer to the [documentation](http://konlpy.org/en/v0.4.4/)), hgtk, nltk, numpy, scikit-learn, tensorflow-gpu 9 | #### Download [100-dimension fastText vector dictionary which was trained with 2M drama scripts](https://drive.google.com/open?id=1jHbjOcnaLourFzNuP47yGQVhBTq6Wgor) and place in the folder named *vectors*. For the utilization of the dictionary, cite the following: 10 | ``` 11 | @article{cho2018real, 12 | title={Real-time Automatic Word Segmentation for User-generated Text}, 13 | author={Cho, Won Ik and Cheon, Sung Jun and Kang, Woo Hyun and Kim, Ji Won and Kim, Nam Soo}, 14 | journal={arXiv preprint arXiv:1810.13113}, 15 | year={2018} 16 | } 17 | ``` 18 | #### Download the [dataset](https://github.com/warnikchow/3i4k/blob/master/data/fci.txt) and place in the folder named *data*. For the utilization of the dataset, cite the following: 19 | ``` 20 | @article{cho2018speech, 21 | title={Speech Intention Understanding in a Head-final Language: A Disambiguation Utilizing Intonation-dependency}, 22 | author={Cho, Won Ik and Lee, Hyeon Seung and Yoon, Ji Won and Kim, Seok Min and Kim, Nam Soo}, 23 | journal={arXiv preprint arXiv:1811.04231}, 24 | year={2018} 25 | } 26 | ``` 27 | 28 | ## Contents (to be updated) 29 | [0. Corpus labelling](https://github.com/warnikchow/dlk2nlp/blob/master/README.md#0-corpus-labeling)
30 | [1. Data preprocessing](https://github.com/warnikchow/dlk2nlp#1-data-preprocessing)
31 | [2. One-hot encoding and sentence vector](https://github.com/warnikchow/dlk2nlp#2-one-hot-encoding-and-sentence-vector)
32 | [3. TF-IDF and basic classifiers](https://github.com/warnikchow/dlk2nlp#3-tf-idf-and-basic-classifiers)
33 | [4. Dense word embeddings](https://github.com/warnikchow/dlk2nlp#4-dense-word-embeddings)
34 | [5. Document vectors and NN classifier](https://github.com/warnikchow/dlk2nlp#5-document-vectors-and-nn-classifier)
35 | [6. CNN-based sentence classification](https://github.com/warnikchow/dlk2nlp#6-cnn-based-sentence-classification)
36 | [7. RNN (BiLSTM)-based sentence classification](https://github.com/warnikchow/dlk2nlp#7-rnn-bilstm-based-sentence-classification)
37 | [8. Character embedding](https://github.com/warnikchow/dlk2nlp#8-character-embedding)
38 | [9. Concatenation of CNN and RNN layers](https://github.com/warnikchow/dlk2nlp#9-concatenation-of-cnn-and-rnn-layers)
39 | [10. Self-attentive BiLSTM](https://github.com/warnikchow/dlk2nlp#10-self-attentive-bilstm)
40 | [11. Transformer, BERT, and after](https://github.com/warnikchow/dlk2nlp#11-transformer-bert-and-after) 41 | 42 | ## 0. Corpus labeling 43 | **The most annoying and confusing process. Annotation guideline should be provided to annotators, and more than two natives are to be engaged in to make the labeling reliable and also for the computation of inter-annotator agreement (IAA). In this project, a multi-class (7) annotation of short Korean utterances is utilized.** 44 | 45 | * 데이터를 만드는, 가장 귀찮은 과정입니다. 언어학적 직관은 1인분이기 때문에, 레이블링이 설득력을 얻기 위해서는 적어도 3명 이상의 1화자를 통한 레이블링으로 그 타당성을 검증해야 합니다 (아카데믹하게는...) 46 | * 본 프로젝트에서는 7-class의 한국어 문장 분류 방법이 사용됩니다. 47 | 48 | --- 49 | 50 | **The task is on classification; especially about extracting intention from a single utterance with the punctuation removed, which is suggested in [3i4k](https://github.com/warnikchow/3i4k). As the description displays, the corpus was partially hand-annotated and incorporates the utterances which are generated or semi-automatically collected. The total number of the utterances reaches 61K, with each label denoting**

51 | **0: Fragments**
52 | **1: Statement**
53 | **2: Question**
54 | **3: Command**
55 | **4: Rhetorical question**
56 | **5: Rhetorical command**
57 | **6: Intonation-dependent utterance**

58 | **where the [inter-annotator (IAA)](https://en.wikipedia.org/wiki/Cohen%27s_kappa) was computed 0.85 (quite high!) for the manually annotated 2K utterance set (corpus 1 in the table below). The [annotation guideline](https://drive.google.com/open?id=1AvxzEHr7wccMw7LYh0J3Xbx5GLFfcvMW) is in Korean.**
59 |

60 | 61 | 62 | * 태스크는 의도 분류로써, [3i4k](https://github.com/warnikchow/3i4k) 프로젝트를 위해 제작된 DB를 사용합니다. 사실 국책과제에 쓰려고 만든건데 어차피 논문으로도 submit했으니 공개는 상관 없지 않을까 싶어요. 6만 1천 문장쯤으로 아주 규모가 크지는 않지만, 일단 수작업으로 2만 문장 정도에서 0.85의 IAA를 얻었으며 (꽤 높은 agreement!), 4만 문장 가량이 더 수집/생성되어 그래도 어느정도 쓸만한 데이터셋이 만들어졌습니다. 63 | 64 | * 레이블 7개는 위에 써 둔 것처럼, Statement~Rhetorical command까지의 clear한 의도 5가지와 (논문에선 clear-cut cases라고 칭했습니다), 의도가 불분명한 Fragment (명사, 명사구, 혹은 불완전한 문장), 마지막으로 Intonation-dependent utterances *억양에 따라 의도가 달라지는 문형* 입니다. 마지막 레이블은 저 논문에서 하나의 문장 유형으로 분류하기로 제안한 것이지만, 한국어 화자라면 어떤 문장들이 그런 성질을 가지는지 감이 올 것입니다. "뭐 먹고 싶어" "천천히 가고 있어" 같은 문장들이 그러한 유형이죠. 주로 puncutation이 제거된 상태로 등장하는 spoken language의 understanding에 아주 골머리를 썩이는 녀석들이기 때문에 따로 분류하기로 하였습니다. 65 | 66 | * Annotation guideline이 어떤 형식인지 궁금하신 분들은 [이곳](https://drive.google.com/open?id=1AvxzEHr7wccMw7LYh0J3Xbx5GLFfcvMW)을 참고하시면 됩니다. 67 | 68 | ## 1. Data preprocessing 69 | **For the next step, we should pay attention to how we can manage HANGUL, the letters of Korean writing system (WS). Korean WS and its alphabet *Jamo* which was (maybe solely) invented by The Great King Sejong, incorporate special morpho-syllabic blocks which each acts as a syllable and consists of CV(C), making Korean as a representative language with featural WS. The blocks are agglutinated to make up a word *eojeol*, which should be decomposed into morphemes for semantically meaningful language processing. Spaces go between the *eojeol*s, to enhance the readability of a sentence. Many morphological analyzers give an additional spacing between the morphemes; some analyzers such as [Twitter](https://github.com/twitter/twitter-korean-text) simply give spaces, and there are the ones which conduct an elaborate tokenizing process of block decomposition, such as [Kkma](http://kkma.snu.ac.kr/). Total five analyzers are wrapped in a famous Korean natural language processing toolkit, [KoNLPy](http://konlpy.org/en/v0.4.4/). In this tutorial, we proceed with the Twitter analyzer, for its speed and to prevent the characters being decomposed.** 70 | 71 | * 한국어와 한글, 문자 체계, 형태소, 띄어쓰기 및 형태소 분석기에 대해 주절주절 설명해 봤는데요, 아마 한국어 원어민 화자라면 대부분 익숙한 내용이실 테니 이 부분의 설명은 생략하도록 하겠습니다 ㅎㅎ 72 | 73 | * 아직 국제 무대에서는 크게 주목받지는 못하지만, 그래도 한국어는 화자 수로만 봐도 세계 15위권 안에 들고 진짜로 몇 안되는 featural writing system (갓종갓왕님 1인 프로젝트 덕분에 sign writing이나 소설을 위해 만들어진 언어 등과 같은 문자형식 지위를 획득 ...) 이며 promissive와 같은 독특한 particle 덕분에 언어학 서적에서도 왕왕 나오는 언어 및 문자체계를 갖고 있습니다. 또한 BTS의 떡상으로 전세계 사람들이 한국과 한국어와 한글을 알게 되어 Korean NLP가 국제무대에서도 Chinese나 Arabic만큼 중요하게 다뤄지는 때가 오기를... 혹은 통일을 기원하며 떡상을 기다리는... 74 | 75 | * 여튼 여기서 한 얘기는 한국어 NLP에서 semantic하게 의미있는 작업을 위해서는 agglutinate된 block들을 morpheme 단위로 적절히 쪼개는 과정이 필수적이라는 얘기를 하고 있습니다. 저는 KoNLPy에 들어 있는 Twitter가, 가볍기도 하고 자소분해를 하지 않아 많이 사용하는데, 여기서도 해당 모듈을 사용하도록 하겠습니다. 76 | 77 | --- 78 | 79 | **The most important part of Korean NLP lies in using Python 3.x. For the lower version, the encoding issue will bother you. That is, in Python 3.x, you don't have to perform an additional encoding for HANGUL to be read and written. [THIS](https://github.com/warnikchow/3i4k/blob/master/data/fci.txt) is the dataset URL, which is a single *.txt* file that has the first column made up of the labels and the second column of the sentences. The dataset is split into train and test set of ratio 9:1 and the test set denotes the last 10% of the corpus. The following code reads the file and makes it into sentences and labels, where the dataset is placed in the path 'data/fci.txt' for the directory you're running the console.** 80 | 81 | ```python 82 | def read_data(filename): 83 | with open(filename, 'r') as f: 84 | # if in Window environment, use: 85 | # with open(filename, 'r', encoding='utf-8') as f: 86 | data = [line.split('\t') for line in f.read().splitlines()] 87 | return data 88 | 89 | fci = read_data('data/fci.txt') 90 | fci_data = [t[1] for t in fci] 91 | fci_label= [int(t[0]) for t in fci] 92 | ``` 93 | 94 | * 한국어엔 역시 Python 3.x 죠.... 2.x로 한국어 인코딩은 지옥입니다 ㅠㅠ 처음에 아주 고생했네요. 어쨌든 제일 첫 삽은 [data](https://github.com/warnikchow/3i4k/blob/master/data/fci.txt)를 로딩하고 sentence와 label들로 나누는 과정입니다. 저는 [Lucy Park님의 유명한 슬라이드](https://www.lucypark.kr/docs/2015-pyconkr.pdf)에서 처음 봤지만, 외국 NLP 튜토리얼들에도 비슷하게 나와 있습니다. 일단 data라는 폴더를 만들어서 *fci.txt* 파일을 넣어주셔야 하며, read_data로 파일을 읽어들이고 탭(\t)으로 split한 후, data와 label array로 나누게 됩니다. 95 | 96 | * Daewon Yoon님이 제기해주신 issue를 반영하여, 윈도우 환경에서 생길 수 있는 인코딩 에러를 보완하기 위해 Window environment용 코드를 병기해 두었습니다. 혹시 우분투나 리눅스가 아닌 환경에서 작업하실 경우 해당 코드를 반영해 주시면 될 것 같습니다. 97 | 98 | --- 99 | 100 | **The last part of data preprocessing is tokenizing the sentences into morphemes, as emphasized previously. Although many character-level (morpho-syllabic blocks) or sub-character-level (consonants and vowels, or *Jamo*) approaches are utilized these days, the morpheme-level approach is still meaningful due to the nature of Korean as an agglutinative language. For the sparse vector classification such as one-hot encoding and TF-IDF which will be displayed in the following chapter, we will adopt the morpheme sequence which can be obtained by the Twitter tokenizer. For a modified usage of Twitter() class since Konlpy > 0.5, refer to [this documentation](http://konlpy.org/en/latest/api/konlpy.tag/#okt-class).** 101 | 102 | ```python 103 | import numpy as np 104 | import nltk 105 | from konlpy.tag import Twitter 106 | pos_tagger = Twitter() 107 | 108 | def twit_token(doc): 109 | x = [t[0] for t in pos_tagger.pos(doc)] 110 | return ' '.join(x) 111 | 112 | len_train = int(np.floor(len(fci_data)*0.9)) 113 | fci_data_train = fci_data[:len_train] 114 | fci_data_test = fci_data[len_train:] 115 | fci_label_train = fci_label[:len_train] 116 | fci_label_test = fci_label[len_train:] 117 | 118 | fci_token_train = [twit_token(row) for row in fci_data_train] 119 | fci_token_test = [twit_token(row) for row in fci_data_test] 120 | 121 | fci_sp_token_train = [nltk.word_tokenize(row) for row in fci_token_train] 122 | fci_sp_token_test = [nltk.word_tokenize(row) for row in fci_token_test] 123 | ``` 124 | 125 | * 데이터를 읽어들였으니, 이제 형태소 분리를 통해 어절이라는 큰 뭉텅이들을 좀 더 세밀한 단위로 나눠 보도록 하겠습니다. 음절 기반 방법, 혹은 자소 분해 방법들도 요즘 많이 사용되지만, 형태소 기반의 문장 표현이 아무래도 교착어인 한국어에서 가장 기본적인 접근 방법이 아닌가 싶습니다. 126 | 127 | * 아까 말씀드렸듯 트위터 형태소 분석기를 사용하여 어절들을 분리할 계획이며, 이는 one-hot encoding이나 TF-IDF를 이용한 sparse vector classification에 활용하기 위함입니다. 위의 코드에서 *twit_token* 함수는 각 문장을 형태소 레벨로 나누어, 형태소 사이에 space를 준 morpheme sequece를 출력합니다. 아래의 코드는 *fci_data*를 train:test ratio 9:1로 split합니다. 128 | 129 | * 위 코드는 Konlpy 0.4.4를 이용한 코드입니다. Konlpy > 0.5 부터 변형된 Twitter() 클래스의 사용을 위해서는 [이 문서](http://konlpy.org/en/latest/api/konlpy.tag/#okt-class)를 참고하세요. 130 | 131 | ## 2. One-hot encoding and sentence vector 132 | **The fundamentals of computational linguistics lies in making machines understand human utterances (or, natural language). Since it is difficult for even human beings to understand the *real meaning*, the first step is to represent the words and sentences into the numerics that are computable for the machine learning systems. Given the dictionary of vocabulary size N, the most famous and intuitive approach is *one-hot encoding* that assigns 1 for only one entry if a specific word is given. Here, the words denote morphemes which are yielded by a morphological analyzer, not the *eojeol* that is the unit of spacing.** 133 | 134 | * 전산 언어학은 기본적으로 컴퓨터한테 사람이 말하는 것, 즉 자연어를 이해시키는 과정이라고 할 수 있겠습니다. 그래서 우리는 컴퓨터로 하여금, '아 진짜 우리 말을 이해하진 못해도, 대충 어떤 내용인지 판단은 할 수 있게 해 보자' 생각을 하게 됩니다. 그렇게 해서 나오게 된 표현법 (representation) 중 가장 기본적으로 사용되고, 매우 강력한 편이라 아직까지도 많은 곳에서 사용하고 있는 방법론은 바로 단어의 one-hot encoding입니다. 135 | 136 | * 이 방법을 쓰기 위해선 '우리가 생각할 단어 전체 set = *Dict*'을 생각해야 합니다. 가장 먼저 생각해볼 수 있는 건 우리가 사용할 코퍼스에 있는 모든 단어들의 모임이죠. 코퍼스의 사이즈가 커질수록 Dict도 커질 것이고, 물론 단어는 countably infinite하게 만들어낼 수 있지만 여기선 '그나마 자주 쓰이는 녀석들'로 생각합시다. 137 | 138 | * 이 *Dict*가 생각보다 커서 실제로는 보통 많이 쓰이는 n만 단어 같이 제한을 둬서 잡습니다. 목적에 따라 functional한 녀석들은 아예 카운트하지 않기도 해요. 이 때 size(*Dict*) = V 이라 하면, 우리는 (고려하기로 한) 모든 단어들을 V-dim의 one-hot vector로 표현할 수 있게 되는 겁니다. 물론 이전 chapter에서의 결과를 생각한다면, 여기서 word는 morpheme이 되겠죠. 물론 한국어에서 띄어쓰기의 단위인 어절이 영어에서의 word와 조금 더 잘 대응이 되고, 실제로 어절 기반의 processing이 더 정확하게 반영할 수 있는 정보들도 있겠지만, 몇만 개나 되는 morpheme들의 조합이 훨씬 많은 어절 variation을 내포하고 있을 것을 고려하면, 어절을 사용한 one-hot encoding이 얼마나 많은 computation을 요구하게 될지 짐작할 수 있습니다. 139 | 140 | --- 141 | 142 | **What should we do with these large-dimensional vectors? The first thing we can think of is a feature called *Bag-of-Words* (BoW). The literal meaning is a bag which contains words, and for Korean, it might be either words (*eojeol*), morphemes, characters, or sub-characters (*Jamo*). Though we've decided only to use the morphemes for sparse representation, these can be replaced with whatever feature you want to adopt. BoW approach is straightforward; we just assign 1 to the corresponding entry if a word in the sentence, and 0 for the others. This can provide the computer with a numerical value which represents the sentence!** 143 | 144 | * 어쨌든 우리는 가장 간단한, sparse한 word vector을 만들었습니다. 이걸 갖고 뭘 하느냐? 가장 먼저 생각해볼 수 있는 것은 bag-of-words란 녀석입니다. 말 그대로 '단어가 든 가방' 이에요. 이 때 가방 = 문장 입니다. 문장 안에 어떤 단어들이 들었냐를 V-dim one-hot vector들의 OR-sum operation (하나만 있어도 1됨) 으로 표현하는 거죠. 145 | 146 | * 예컨대 "신이 그댈 사랑해"라는 문장이 있고 '신' = \[1 0 0 0 0 0 0 0\], '이' = \[0 0 1 0 0 0 0 0\], '그댈' = \[0 0 0 1 0 0 0 0\], '사랑' = \[0 0 0 0 1 0 0 0\], '해' = \[0 0 0 0 0 0 1 0\] 로 표현된다고 합시다. 여기서 코퍼스는 여덟 개의 토큰 (단어구성단위 라고 합시다 일단)으로 구성된 아주 작은 *Dict*를 yield했겠지요. 그렇다면 상기 문장은 \[1 0 1 1 1 0 1 0\]의 size(Dict)-dim binary vector로 표현되는 겁니다. 이렇게 해서 뭘 할 수 있냐구요? 이제 컴퓨터도 알아먹는 수치적 정보가 되었으니, 각종 분류기에 넣어 재미를 볼수 있죠! 147 | 148 | * 물론 태클이 들어올 수 있습니다. 저 문장을 사실 '신 이 그대 -ㄹ 사랑 ㅎ -ㅐ' 로 나눠야 합당하지 않느냐, 한 문장에서 여러 번 카운트되는 단어들이 있으면 one-hot은 부당한 representation이 아니냐 뭐 그런... 첫 번째의 경우 우리가 문장을 자소로 분리하지 않는 Twitter analyzer을 썼기 때문에 어쩔 수 없는 부분입니다. 두 번째의 경우 다음 chapter에서 더 다뤄 보도록 하겠습니다. 149 | 150 | ## 3. TF-IDF and basic classifiers 151 | **Previously, we've introduced one-hot encoding of the words and the sparse sentence representation based on the BoW model. However, despite its transparency and conciseness, one-hot encoding does not convey the word frequency regarding the document. This is where the concept of *term frequency* (TF) came out; the word frequency is taken into account to convey the relative importance of each word. For instance, the word 'I' and 'you' in the sentence *I love you, I want you, I need you* may be assigned the word frequency of 3 instead of 1 which is assigned to the verbs.** 152 | 153 |

154 |
155 | (Image from https://skymind.ai/wiki/bagofwords-tf-idf) 156 | 157 | * 앞서 bag-of-words 모델을 통해 문장을 1-hot vector들의 합(여기서 합은 Boolean의 or에 해당)으로 나타내어 컴퓨터가 알아먹을 만한 어떤 수치로 나타내는 과정을 설명했습니다. 이는 전통적이고 직관적이며 상당히 강력한 방법이기도 합니다. 158 | 159 | * 하지만 이 방법의 문제는 문장 내에 등장하는 모든 단어들을, 등장 횟수에 상관없이 공평하게 대한다는 것입니다. 예컨대 "나는 아브라함의 하나님이요 이삭의 하나님이요 야곱의 하나님이로라 하신 것을 읽어 보지 못하였느냐 (전 종교가 없습니다! 그냥 예문을 찾고싶었을뿐)" 라는 문장이 하고 싶은 말은 누가 봐도 내가 하나님이란 것 같은데 아브라함이니 이삭이니 하는 것들과 동급으로 1로 카운트되면 얼마나 억울하겠습니까. 160 | 161 | * 이러한 문제를 해결하기 위해 우린 일종의 normalization으로 볼 수 있는 term frequency, 즉 문장의 전체 word중 해당 word의 빈도를 고려해 문장을 벡터화해 볼 수 있습니다. 위의 문장에선 '하나님'이 3/X (형태소분석한 결과를 모두 카운트하기 귀찮으니 X로 퉁칩시다)의 값을 부여받고 나머지 녀석들은 1/X의 값을 부여받게 되겠죠. 162 | 163 | --- 164 | 165 | **However, the problem is that the high frequency does not indicate the importance of the word. Thus here, the concept of *inverse-document frequency* (IDF) is introduced, as a way of multiplying the inverse fraction of the frequency of the word among the whole documents. This prevents the overestimation of the functional words in many cases; to be honest, 'I' and 'you' are not as important as 'love', 'want' and 'need' in most cases (unless if the task deals with directivity). For instance, in the morpheme-level analysis, many particles in Korean are used repetitively in the sentences; those will be assigned a low IDF so that the lexical words are emphasized alternatively. The following code utilizing Scikit-learn library demonstrates how the *term frequency-inverse document frequency* (TF-IDF) is computed for our corpus.** 166 | 167 | ```python 168 | # Referred to the followings for the code: 169 | # https://gist.github.com/jason-riddle/1a854af26562c0cdb1e6ff550d1bf32d#file-complex-tf-idf-example-py-L40 170 | # http://blog.christianperone.com/2011/10/machine-learning-text-feature-extraction-tf-idf-part-ii/ 171 | 172 | from sklearn.feature_extraction.text import CountVectorizer 173 | from sklearn.feature_extraction.text import TfidfTransformer 174 | from sklearn.feature_extraction.text import TfidfVectorizer 175 | 176 | def tfidf_featurizer(corpus_train,corpus_test): 177 | count_vectorizer = CountVectorizer() 178 | count_vectorizer.fit_transform(corpus_train) 179 | freq_term_matrix = count_vectorizer.transform(corpus_train) 180 | tfidf = TfidfTransformer(norm="l2") 181 | tfidf.fit(freq_term_matrix) 182 | # unigram features 183 | tfidf_token = TfidfVectorizer(ngram_range=(1,1),max_features=3000) 184 | token_tfidf_total = tfidf_token.fit_transform(corpus_train+corpus_test) 185 | token_tfidf_total_mat = token_tfidf_total.toarray() 186 | # bigram features 187 | tfidf_bi_token = TfidfVectorizer(ngram_range=(1,2),max_features=3000) 188 | token_tfidf_bi_total = tfidf_bi_token.fit_transform(corpus_train+corpus_test) 189 | token_tfidf_bi_total_mat = token_tfidf_bi_total.toarray() 190 | return token_tfidf_total_mat,token_tfidf_bi_total_mat 191 | 192 | fci_tfidf,fci_tfidf_bi = tfidf_featurizer(fci_token_train,fci_token_test) 193 | fci_tfidf_train = fci_tfidf[:len_train] 194 | fci_tfidf_test = fci_tfidf[len_train:] 195 | fci_tfidf_bi_train = fci_tfidf_bi[:len_train] 196 | fci_tfidf_bi_test = fci_tfidf_bi[len_train:] 197 | ``` 198 | 199 | * 그런데 또 문제가 있습니다. 바로 하나님이 소유를 나타내는 '의'와 동급이 돼버리는 겁니다. 아아, 이렇게 원통할 데가... 딱 봐도 '의'라는 녀석은 문장의 의미를 판단하는 데에 큰 도움을 주지 않을 것으로 보입니다. 다른 문장들에도 많이 나올 게 분명하거든요. 200 | 201 | * 그래서 우리가 생각해볼 수 있는 건 '다른 문장들에도 많이 나오는 녀석엔 가중치를 조금 주면 어떨까?'하는 것입니다. 예컨대 전체 문장 중 해당 문장이 나오는 비율의 역수 같은 걸 곱해준다면? 이런 생각으로 나온 녀석이 바로 inverse document frequency입니다. 약간의 smoothing factor을 추가하자면, test corpus에 해당 term이 없는 경우를 대비해 분모에 1을 더해주고, corpus size가 방대해질 때를 고려해 log를 입히는 정도? 202 | 203 | * 상기한 두 개의 요소를 곱해 BoW를 개량한 모델이 바로 TF-IDF (term frequency-inverse document frequency) 입니다. 문서 분류에 아직도 활발히 사용되는 모델이지요. 위의 코드에서 *tfidf_vectorizer* 함수는 train corpus에서 tf-idf statistics를 뽑아내고 이를 이용해 train corpus+test corpus를 수치화하는 과정을 담고 있습니다. statistics를 뽑아낼 때 train corpus만 사용하는 것은, test corpus까지 되면 prediction의 본래 의미에 맞지 않게 되기 때문이죠. 수치화하는 과정은 이 statistics를 이용하여 *fit_transform*의 함수를 통해 일괄적으로 진행됩니다. 204 | 205 | --- 206 | **The aforementioned sentence representation can be directly utilized with basic classifiers such as naive Bayes (NB), decision tree (DT), support vector machine (SVM) and logistic regression (LR). The evaluation is done with accuracy and F1-score; accuracy refers to the fraction of correct instances out of the whole data. Understanding the meaning of the value is intuitive, but the flaw is that it does not convey how incorrect the model predicts for the classes of data with a small portion. For example, if the data consists of the binary label and the portion is 9:1 yielding an imbalance, then the accuracy may reach 90% just by a simple guess of a class with the larger volume. A better solution can be obtained by using F1-score, which considers the true negatives and the false positives. The following code displays how the sparse vectors we previously obtained are used in training and prediction, with both an accuracy and an F1-score.** 207 | 208 | ```python 209 | from sklearn.linear_model import LogisticRegression 210 | from sklearn import metrics 211 | from sklearn.metrics import accuracy_score, precision_recall_fscore_support 212 | 213 | classifier_uni = LogisticRegression(random_state=1234) 214 | classifier_uni.fit(fci_tfidf_train,fci_label_train) 215 | uni_pred = classifier_uni.predict(fci_tfidf_test) 216 | accuracy_score(uni_pred,fci_label_test) 217 | metrics.f1_score(uni_pred,fci_label_test,average="macro") 218 | metrics.f1_score(uni_pred,fci_label_test,average="weighted") 219 | precision_recall_fscore_support(uni_pred,fci_label_test) 220 | 221 | classifier_bi = LogisticRegression(random_state=1234) 222 | classifier_bi.fit(fci_tfidf_bi_train,fci_label_train) 223 | bi_pred = classifier_bi.predict(fci_tfidf_test) 224 | accuracy_score(bi_pred,fci_label_test) 225 | metrics.f1_score(bi_pred,fci_label_test,average="macro") 226 | metrics.f1_score(bi_pred,fci_label_test,average="weighted") 227 | precision_recall_fscore_support(bi_pred,fci_label_test) 228 | ``` 229 | 230 | * 지금까지 tf-idf를 이용한 sentence 의 sparse representation에 대해 얘기했지요. 이제 구체적으로 이 녀석들을 문장 분류에 이용해먹을 만한 방법들에 대해 생각해봐야 하는데요, 그 중 하나가 이 글의 task로 제시된 분류 (classification)입니다. 231 | 232 | * 사실 데이터 집합(문장들)과 레이블 집합(긍정문/부정문, 의문문별 분류, 토픽별 분류 등)으로 된 트레이닝 데이터를 input으로 넣어, 별도의 테스트 데이터로 얻어진 prediction와 정답의 비교를 통해 그 네트워크의 accuracy, F-measure 등을 evaluate하여 성능을 평가한다는 점은 많은 분야에서 사용되는 분류기 evaluation의 구조입니다. 233 | 234 | * 이 때 accuracy는 전체 테스트 인풋 중 분류에 성공한 input의 비율이 될 겁니다. 그런데 함정이 있다면, 데이터 편중을 배제하기 어렵단 것이죠. 예컨대 데이터셋의 90퍼센트가 긍정문이고 10퍼센트만 부정문이라면, 공정한 트레이닝 및 테스트가 되었다고 하기 어렵겠죠. 그래서 binary classification에서 많이 사용되는 F-measure의 경우는 precision과 recall이라는, 각각 '1이라고 했을 때 정말 맞을 확률'과 '전체 맞은 것들 중 1이라고 했을 확률'을 생각하여 이들의 조화평균을 구해주는 방향으로 accuracy의 함정을 보정합니다. 물론 단순 조화평균이 아닌 별도 parameter을 주기도 하고, multiclass에 대해선 따로 class별 weight를 정의하기도 하지만요. 235 | 236 | --- 237 | 238 | **The result is as below; it is weird that the bigram features yielded quite a lower accuracy. It is assumed that the property of the corpus, where the sentence label does not depend solely on the polarity items or emotion words, may have affected the performance. Also, the dimension of the feature (3,000) which was fixed to guarantee the fair comparison between the two models, could have been a harsh obstacle for the bigram model.** 239 | 240 | ```properties 241 | # CONSOLE RESULT 242 | >>> accuracy_score(uni_pred,fci_label_test) 243 | 0.7742409402546523 244 | >>> metrics.f1_score(uni_pred,fci_label_test,average="macro") 245 | 0.6000194409152938 246 | >>> metrics.f1_score(uni_pred,fci_label_test,average="weighted") 247 | 0.7880295802371478 248 | >>> precision_recall_fscore_support(uni_pred,fci_label_test) 249 | (array([0.74875208, 0.87206124, 0.88099174, 0.73828125, 0.14534884, 250 | 0.23148148, 0.32398754]), array([0.70754717, 0.71046771, 0.84513742, 0.82749562, 0.5952381 , 251 | 0.80645161, 0.75362319]), array([0.72756669, 0.78301424, 0.8626922 , 0.78034682, 0.23364486, 252 | 0.35971223, 0.45315904]), array([ 636, 2245, 1892, 1142, 42, 31, 138])) 253 | 254 | >>> accuracy_score(bi_pred,fci_label_test) 255 | 0.321743388834476 256 | >>> metrics.f1_score(bi_pred,fci_label_test,average="macro") 257 | 0.18947002986641723 258 | >>> metrics.f1_score(bi_pred,fci_label_test,average="weighted") 259 | 0.35601345547363006 260 | >>> precision_recall_fscore_support(bi_pred,fci_label_test) 261 | (array([0.82529118, 0.46473483, 0.18787879, 0.22109375, 0. , 262 | 0. , 0.00311526]), array([0.28101983, 0.36340316, 0.34236948, 0.28936605, 0. , 263 | 0. , 0.05 ]), array([0.41927303, 0.40786948, 0.24261829, 0.2506643 , 0. , 264 | 0. , 0.0058651 ]), array([1765, 2339, 996, 978, 12, 16, 20])) 265 | ``` 266 | 267 | * 결과는 뜻밖이었는데요, bigram으로 구한 결과가 그다지 신통치 않은 것 같습니다. (코드를 잘못 짰나...?) 어쨌든 궁색하게 분석을 해보자면, 본 task가 sentiment analysis 처럼 단어 한두개로 문장의 의미가 크게 달라지는 task들과 다르게 전체적인 맥락 및 어순을 고려해야 하기도 하고, fair comparison을 위해 3,000으로 제한한 feature dimension이 오히려 bigram에는 해가 되어 정작 중요한 형태소들을 놓친 게 아닌가...하는 생각이 듭니다. 268 | 269 | * 앞서 구한 one-hot encoding으로도 이러한 evaluation을 손쉽게 할 수 있습니다. 방금 TF-IDF에서 그랬듯, evaluation을 위한 prediction은 10%의 test data로 하게 되지요. Bigram TF-IDF는 좀 불만족스럽지만, 어쨌든 sparse representation으로도 어느 정도는 높은 정확도를 가진 모델을 얻을 수 있음을 알 수 있습니다. 물론 일부 class들에 대해선 random guess만도 못한 결과를 내고 있지만요. 다만 아직도 찝찝한 점은, 컴퓨터가 단어를 세고만 있지 그 단어들이 문장 내에서, 작게는 컨텍스트에서 어떤 역할을 하는지 아무것도 모르는 것 같다는 점입니다. 270 | 271 | ## 4. Dense word embeddings 272 | 273 | **Three-line summary:**
274 | **1. Computational linguistics aims making machines understand human language.**
275 | **2. As a fundamental approach for the representation of words and sentences, one-hot encoding and TF-IDF are introduced.**
276 | **3. For the Korean language, due to the property of the agglutinative language, morpheme-level analysis can be more effective than the word (*eojeol*)-level one.**
277 | 278 | **However, considering the computation issue which has been crucial up to this date, the sparse representations may not be the optimal solution for the contemporary neural network-based systems. This is the point where the dense representation such as [*Word2Vec*](https://papers.nips.cc/paper/5021-distributed-representations-of-words-and-phrases-and-their-compositionality.pdf) is successfully adopted.** 279 | 280 | * 여태까지 한 내용을 세줄요약해 보면 다음과 같습니다.
281 | 1. 전산언어학은 기계로 하여금 사람 말을 잘 알아먹도록 하는 학문
282 | 2. 단어 및 문장을 모델링하는 기본적인 방법으로 one-hot encoding (BoW) 및 TF-IDF가 있음
283 | 3. 한국어는 교착어적 특징 때문에 어절 단위 분석보다 형태소 단위 분석이 효과적일 수 있음 284 | 285 | * 이외에도 수치화된 문장을 분류하는 알고리즘에는 SVM 이나 LR 등이 있다, 그 성능을 평가하는 measure에는 accuracy나 F1 score 등이 있다 ... 라는 것도 간략히 말하고 지나왔죠. 286 | 287 | * 그런데 이러한 방법론들은 분석해야 할 데이터가 많아지고 사전의 크기가 커질수록 computation의 문제에 당면하게 됩니다. 특히나 데이터를 몰빵해넣고 병렬연산으로 승부하는 딥러닝의 경우 더욱 그렇지요. 예컨대 30 개의 형태소로 된 문장을 벡터 sequence로 나타내어 recurrent neural network 같은 시스템으로 요약하고자 할 때, 딕셔너리 사이즈가 10만이라면, 10만 x 30이라는 무시무시한 사이즈의 행렬이 문장 하나를 표현하여 인풋으로 들어가게 되는거죠. 물론 아까 보았듯 사용하는 벡터의 크기는 조절할 수 있고, 각 벡터가 각 단어를 표현하는 것이 아주 명확하긴 하지만요. 288 | 289 | * 이럴 때 유용하게 사용되는 개념이 2013년 태동한 word2vec, 혹은 dense low-dimension embedding 입니다. 등장 목적이 위와 같다고는 할 수 없지만, [2006년부터 촉발된 딥러닝 아키텍쳐](http://www.cs.toronto.edu/~fritz/absps/ncfast.pdf)와 맞물려 사용되며 문장 분석의 패러다임 자체를 바꾸고 있죠. Word2vec, GloVe, fastText에 대한 설명을 간략하게만이라도 해보려 합니다. 290 | 291 | --- 292 | 293 | The term word2Vec is very intuitive, and it converts the words, which are discrete (and were sparsely represented so far), into the numerics that are close to continuousness. Since there are a [bunch of](https://skymind.ai/wiki/word2vec) [terrific](https://towardsdatascience.com/introduction-to-word-embedding-and-word2vec-652d0c2060fa) [articles](https://www.tensorflow.org/tutorials/representation/word2vec) on the topic outside, in English, a review on word2vec and its related models are discussed mainly in Korean. 294 | 295 |

296 |
297 | (Image from [the tutorial site](https://sheffieldnlp.github.io/com4513-6513/lectures_reveal_js/lecture12_word_embeddings.html). The matrix W x D is obtained by minimizing the cost function regarding the performance of context prediction given an input vector.) 298 | 299 | * word2vec이라는 말은 상당히 직관적입니다. 말 그대로 이산적 개념인 단어를 수치적 개념인 벡터로 바꿔 준다는 의미이죠. 사실 one-hot encoding 역시 고차원의 벡터를 만들어준다는 점을 생각하면 어폐가 있긴 합니다. 그래서 두 개념의 차이를 벡터가 sparse한지 (드문드문하게 nonzero인 성분이 있는지), 아니면 dense한지 (유클리드 공간에서처럼 빽빽이 들어차 있는지)를 차이점으로 봅니다. 물론 word2vec은 후자를 의미하죠. 300 | 301 | * 당연한 얘기겠지만, 어떤 방식의 워드 임베딩이든, 임베딩 벡터 셋을 트레이닝하는 과정에서 어떤 원칙 (혹은 제약조건)을 주냐에 따라 결과물로 나오는 벡터들 간의 관계가 달라지게 됩니다. 예컨대 아무 제약 조건도 주지 않는 one-hot encoding의 경우, 모든 단어가 평등합니다. 아무리 비슷해 보이는 단어들이라도 1이 위치하는 엔트리가 다르다면 아무 상관없는 단어인 것이나 다름없게 되는 것이죠. distributional semantics에 대한 고려는 one-hot vector에 들어 있지 않은 겁니다. 302 | 303 | * 모든 sparse vector가 one-hot vector같이 엔트리 간 equivalence를 지니는 건 아닙니다. 오히려 희소한 케이스에 가깝죠. 가중치를 곱해준 TF-IDF만 해도, 개별 단어를 모델링하지는 않지만 어떤 분포적인 특성이 반영되게 됩니다. binary이지만 multi-hot encoding을 차용하는 [boolean distributional semantics](https://transacl.org/ojs/index.php/tacl/article/view/616)의 경우, 의미론에서 얘기하는 feature의 개념이 등장하여, taxonomy 상 상위 위계에 해당하는 단어의 embedding이 하위 위계의 단어의 embedding에 포함되게 하는 방식으로 트레이닝이 됩니다. 아마도 벡터 사이즈는 one-hot보다는 줄어들겠죠? 벡터들 간의 distance measure가 euclidean이 아닌 어떤 이산적인 개념이 된다는 건 challenging하긴 합니다만, 언어가 기본적으로 이산적인 속성을 버릴 수 없다는 생각을 갖고 있는 저로썬 매우 매력적이라는 생각을 했습니다. 304 | 305 | * 그렇지만 이상은 이상이고, 우리는 많은 순간 현실과 타협해야 합니다. 작은 벡터에 정보들을 우겨 넣어야 하죠. 또한 back propagation과 같은 연산을 통해 이루어지는 최신 최적화 기법들의 수혜를 받으려면, 벡터 간의 거리가 어떤 미분가능한, 이산적이지 않은 개념으로 정의가 되어야 함도 사실입니다 (물론 이산적인 목적 함수들을 위해 별도의 신경망을 연결해 주는 알고리즘도 있는 것으로 압니다만 일단 그건 나중에 생각하도록 하지요). 그러기 위해서, 고차원의 벡터를 저차원에 임베딩해 넣으면서도, 단어들 간의 관계가 좀 더 유기적으로 연결될 수 있는 방법엔 뭐가 있을까요? 306 | 307 | * 이러한 관점에서 볼 때, word2vec은 상당히 매력적인 시도였다 볼 수 있겠습니다. 혹자는 '비슷한 context에 등장하는 녀석들은 실제로도 비슷한/관련 있는 녀석들일 가능성이 높다는 distributional semantics의 원칙을 수치적 제약으로 잘 지키면서 one-hot vector을 저차원에 pca한 결과물'이라고 하더군요. 예컨대 '너는 나쁜 아이야'와 '너는 착한 아이야'라는 문장들을 볼 때, '나쁜'과 '착한'이 실제로 저런 류의 context를 많이 공유한다 생각해 보면, 둘이 아주 관계가 없는 단어들은 아니다, 어느정도 유기적이다, 그런 판단을 할 수 있겠죠? word2vec의 큰 철학은 이렇습니다. 컨텍스트를 주고 center word를 추론하는 CBOW와 그 반대인 skip-gram (SG) 모두 word2vec의 최적화와 관련된 알고리듬인데요, SG가 여러 태스크에서 더 성능이 좋음이 보여진 바 있고 실제로도 더 자주 활용됩니다. 308 | 309 | --- 310 | 311 | **Up to date, many advanced models of word vectors which base on word2vec have been proposed. For instance, [*GloVe*](https://nlp.stanford.edu/pubs/glove.pdf) represents the word vectors considering the co-occurrence in the window, and [*fastText*](https://arxiv.org/abs/1607.04606) utilizes subword n-gram so that the embedding can be efficient for morphologically rich languages. In the following approaches that use dense word vectors, we adopt [100-dimension fastText vector dictionary which was trained with 2M drama scripts](https://drive.google.com/open?id=1jHbjOcnaLourFzNuP47yGQVhBTq6Wgor), for the project [RAWS](github.com/warnikchow/raws). Use Gensim if fastText is not installed.** 312 | 313 | ```python 314 | import fasttext 315 | model_ft = fasttext.load_model('vectors/model_drama.bin') 316 | 317 | # In case fasttext is not installed 318 | # use FastText wrapper in Gensim as: 319 | # from gensim.models.wrappers import FastText 320 | # model_ft = FastText.load_fasttext_format('vectors/model_drama.bin') 321 | ``` 322 | 323 | * Word2vec은 이런 저차원 임베딩의 포문을 열었을 뿐이고 (for문 아닙니다) 이후로 수많은 베리에이션이 등장했습니다. 이 때 사용하는 알고리듬이 skip-gram이란 점이 크게 변하지는 않았지만, 다양한 objective를 설정하며 특정 task에 효율적으로 적용할 수 있는 vector들이 등장했죠. 324 | 325 | * 가장 많이 알려진 두 베리에이션은 GloVe와 fastText입니다. 전자는 word2vec 이전에 사용되던 sparse representation 중 하나인 co-occurrence matrix의 개념을 training objective에 활용하여 보다 global하고 syntactic한 특징도 word vector에 반영할 수 있게 한 것이고, 후자는 word2vec과 사실상 같은 알고리즘이지만 그 트레이닝 효율을 거의 몇백배 수준으로 끌어올려 빠르게 트레이닝하면서도 task 성능은 엇비슷하게 유지할 수 있게 하는 알고리듬입니다. 또한 fastText는 subword n-gram model을 차용하여, word 단위로 나뉘어지지 않은, 단어 내의 character n-gram들도 일종의 word로 보고 그 분포를 전체 트레이닝에 고려한다는 특징을 가지고 있지요. 이를 통해 morphologically rich한 언어들에서도 효율적으로 활용 가능하다는 점을 논문에서 어필하고 있구요. 326 | 327 | * GloVe의 경우 stanford에서 제공하는 [wiki/twitter기반 pre-trained vector](https://nlp.stanford.edu/projects/glove/)가 있으며, fastText의 경우는 꽤 많은 언어로 pre-trained vector을 제공하지만 학습 속도가 굉장히 빨라 저 같은 경우는 갖고 있는 코퍼스로 새로 training하기도 합니다. Subword n-gram이 교착어인 한국어에도 굉장히 유용하여, 저 같은 경우는 fastText로 트레이닝된 word vector set을 character embedding에 사용하고 있구요. 약 200만 문장의 드라마 스크립트를 통해 학습한 word vector dictionary는 [다음의 주소](https://drive.google.com/open?id=1jHbjOcnaLourFzNuP47yGQVhBTq6Wgor)에서 제공됩니다. [fasttext 라이브러리](https://pypi.org/project/fasttext/0.8.3/)를 이용해 bin 파일을 바로 load할 수 있으며, 설치가 잘 되지 않을 경우 Gensim의 FastText wrapper을 이용해 비슷한 방식으로 load가 가능합니다. 328 | 329 | * 앞서도 말했지만, 26개의 letter로 된 알파벳만 있으면 되는 영어와 달리, 한국어는 2500개 상당의 자모 조합이 있어 one-hot encoding을 직접적으로 character-level embedding에 사용하기 쉽지 않죠. 이럴 때 유용하게 사용할 수 있는 것이 저차원으로 임베딩된 character vector입니다. 어느 정도 분포적인 특징을 반영할 수 있으면서도 computational하게 부담을 덜 줄 수 있는 그런 feature로 사용할 수 있는 것입니다. 물론 형태소, 어절 모두 임베딩의 대상이 될 수 있습니다. 어떤 것을 선택할지는 형태소 분석기의 유무, 구동 및 개발 환경 등에 따라 자유롭게 선택하면 될 것입니다. 330 | 331 | ## 5. Document vectors and NN classifier 332 | 333 | **In the first few chapters, we've demonstrated how the corpus we adopted is preprocessed, featurized, trained and used in prediction with the sparse sentence encodings. However, since we've obtained the dense word embeddings for the morphemes (as we've obtained for one-hot encoded words), it's plausible to extend it to the sentence vector, for instance by summation. ** 334 | 335 | ```python 336 | from numpy import linalg as la 337 | 338 | def featurize_nn(corpus,wdim): 339 | nn_total = np.zeros((len(corpus),wdim)) 340 | for i in range(len(corpus)): 341 | if i%1000 ==0: 342 | print(i) 343 | s = corpus[i] 344 | count_word=0 345 | for j in range(len(s)): 346 | if s[j] in model_ft: 347 | nn_total[i,:] = nn_total[i,:] + model_ft[s[j]] 348 | count_word = count_word + 1 349 | if count_word > 0: 350 | nn_total[i] = nn_total[i]/la.norm(nn_total[i]) 351 | return nn_total 352 | 353 | fci_nn = featurize_nn(fci_sp_token_train+fci_sp_token_test,100) 354 | ``` 355 | 356 | * 앞서 one-hot encoding과 tf-idf로 문장을 수치화하는 방법, 그리고 그것이 sparse vector이라는 것까지 말씀드린 바 있습니다. 정보가 discrete하다는 것은 기계에게나 사람에게나 매우 직관적으로 다가오기에, 문장의 분류에 있어 굉장히 유용하게 사용된다는 것두요. 하지만 지난 몇 시간 동안 dense word vector들을 얘기하는 동안, 그 친구들을 문장 수치화에 이용할 수 있지 않을까 생각하는 분도 분명 계실 겁니다. word2vec을 제안한 사람들의 머릿속을 들여다볼 수는 없지만, 결과적으로 그 등장이 문장 임베딩에 있어 하나의 패러다임 전환이 되었죠. 357 | 358 | * “I really love you” 이라는 영어 문장을 한번 생각해 봅시다. 일단 가장 먼저 생각해볼 수 있는 것은, \[0 0 0 ... 1 ... 1 ... 1 ... 1 ... 0 0 0\] 이런 식으로 나타내어진 multi-hot vector (one-hot vectors의 or summation)이겠죠. 그 다음은 \[0 0 0 ... 0.3 ... 0.7 ... 0.9 ... 0.4 ... 0 0 0\] 이런 식으로 표현된 tf-idf일 겁니다. 이제 word2vec이 무엇인지 배웠으므로, 이런 방법도 생각해 볼 수 있을 겁니다. ‘문장 내 모든 word와 word embedding function f에 대해, f(w)를 모두 더하기’ 즉, 워드 임베딩이 100차원의 real vector이라면, 전체 sentence vector s=sum(f(w))도 100차원의 real vector가 되는 방법이죠. 여기에 normalization을 위해 l2_norm(s)나 word 개수 (여기서는 4)로 s를 나눠 주면 좀 더 reliable한 표현이 될 것입니다. 앞서 말한 과정이 *featurize_nn*에 구현되어 있으며, 해당 함수는 corpus 전체를 input으로 받아 각 문장마다 100차원 (word vector dim)의 real vector을 출력합니다. 359 | 360 | --- 361 | 362 | **And here comes Keras, which is a widely used high-level wrapper for TensorFlow (and other libraries i.e. Theano and CNTK; though not used in general). Since we only use *Sequential()* for the codes not incorporating the concatenated layers or attention models, only *layer* will be imported. For the computation of average/weighted F1 score per epoch, an additional module is defined here. Also, considering the imbalance of the class volumes, we obtain the class weight set that is utilized in the training session.** 363 | 364 | ```python 365 | import tensorflow as tf 366 | from keras.backend.tensorflow_backend import set_session 367 | config = tf.ConfigProto() 368 | config.gpu_options.per_process_gpu_memory_fraction = 0.1 369 | set_session(tf.Session(config=config)) 370 | from keras.models import Sequential 371 | import keras.layers as layers 372 | from keras import optimizers 373 | adam_half = optimizers.Adam(lr=0.0005) 374 | from keras.callbacks import ModelCheckpoint 375 | 376 | from keras.callbacks import Callback 377 | from sklearn import metrics 378 | class Metricsf1macro(Callback): 379 | def on_train_begin(self, logs={}): 380 | self.val_f1s = [] 381 | self.val_recalls = [] 382 | self.val_precisions = [] 383 | self.val_f1s_w = [] 384 | self.val_recalls_w = [] 385 | self.val_precisions_w = [] 386 | def on_epoch_end(self, epoch, logs={}): 387 | val_predict = np.asarray(self.model.predict(self.validation_data[0])) 388 | val_predict = np.argmax(val_predict,axis=1) 389 | val_targ = self.validation_data[1] 390 | _val_f1 = metrics.f1_score(val_targ, val_predict, average="macro") 391 | _val_f1_w = metrics.f1_score(val_targ, val_predict, average="weighted") 392 | _val_recall = metrics.recall_score(val_targ, val_predict, average="macro") 393 | _val_recall_w = metrics.recall_score(val_targ, val_predict, average="weighted") 394 | _val_precision = metrics.precision_score(val_targ, val_predict, average="macro") 395 | _val_precision_w = metrics.precision_score(val_targ, val_predict, average="weighted") 396 | self.val_f1s.append(_val_f1) 397 | self.val_recalls.append(_val_recall) 398 | self.val_precisions.append(_val_precision) 399 | self.val_f1s_w.append(_val_f1_w) 400 | self.val_recalls_w.append(_val_recall_w) 401 | self.val_precisions_w.append(_val_precision_w) 402 | print("— val_f1: %f — val_precision: %f — val_recall: %f"%(_val_f1, _val_precision, _val_recall)) 403 | print("— val_f1_w: %f — val_precision_w: %f — val_recall_w: %f"%(_val_f1_w, _val_precision_w, _val_recall_w)) 404 | 405 | metricsf1macro = Metricsf1macro() 406 | 407 | from sklearn.utils import class_weight 408 | class_weights_fci = class_weight.compute_class_weight('balanced', np.unique(fci_label), fci_label) 409 | ``` 410 | 411 | * 본격적으로 Keras를 써볼 때가 왔습니다. 일단 제가 TensorFlow를 제대로 써본 적은 없지만, conctenated layer나 attention model같은 복잡한 시스템을 포함하지 않는 대부분의 모델들에 대해서는 *Sequential()* 모듈을 import하는 것으로 대부분 구현 가능합니다 ㅎㅎ TF-IDF의 evaluation에서 활용한 average/weighted F1 score을 epoch마다 계산하기 위해 별도의 함수를 정의했으며, class imbalance를 고려하여 weight set을 구했습니다. 다음의 코드에서 활용되는 것을 볼 수 있습니다. 412 | 413 | --- 414 | 415 | **And the Below is the model construction and evaluation phase. Note that the folder *tutorial* was created in the same directory to save checkpoint models, recording F1 scores and the accuracy. It is quite surprising that a simple summation boosts the accuracy and F1 score by a large factor, even considering that the concept of making up the sentence vector is fundamentally identical to that of one-hot encoding and TF-IDF!** 416 | 417 | ```python 418 | def validate_nn(result,y,hidden_dim,cw,filename): 419 | model = Sequential() 420 | model.add(layers.Dense(hidden_dim, activation = 'relu', input_dim=len(result[0]))) 421 | model.add(layers.Dense(int(max(y))+1, activation='softmax')) 422 | model.summary() 423 | model.compile(optimizer=adam_half, loss="sparse_categorical_crossentropy", metrics=['accuracy']) 424 | filepath=filename+"-{epoch:02d}-{val_acc:.4f}.hdf5" 425 | checkpoint = ModelCheckpoint(filepath, monitor='val_acc', verbose=1, mode='max') 426 | callbacks_list = [metricsf1macro,checkpoint] 427 | model.fit(result,y,validation_split=0.1,epochs=30,batch_size=16,callbacks=callbacks_list,class_weight=cw) 428 | 429 | validate_nn(fci_nn,fci_label,128,class_weights_fci,'model/tutorial/nn') 430 | ``` 431 | 432 | ```properties 433 | # CONSOLE RESULT 434 | >>> validate_nn(fci_nn,fci_label,128,class_weights_fci,'model/tutorial/nn') 435 | _________________________________________________________________ 436 | Layer (type) Output Shape Param # 437 | ================================================================= 438 | dense_3 (Dense) (None, 128) 12928 439 | _________________________________________________________________ 440 | dense_4 (Dense) (None, 7) 903 441 | ================================================================= 442 | Total params: 13,831 443 | Trainable params: 13,831 444 | Non-trainable params: 0 445 | _________________________________________________________________ 446 | Train on 55129 samples, validate on 6126 samples 447 | Epoch 1/30 448 | 54960/55129 [============================>.] - ETA: 0s - loss: 0.8674 - acc: 0.7068— val_f1: 0.591953 — val_precision: 0.721290 — val_recall: 0.549977 449 | — val_f1_w: 0.731234 — val_precision_w: 0.747052 — val_recall_w: 0.742899 450 | 55129/55129 [==============================] - 6s 116us/step - loss: 0.8672 - acc: 0.7069 - val_loss: 0.7665 - val_acc: 0.7429 451 | Epoch 2/30 452 | 55056/55129 [============================>.] - ETA: 0s - loss: 0.7255 - acc: 0.7537— val_f1: 0.642565 — val_precision: 0.721759 — val_recall: 0.602373 453 | — val_f1_w: 0.754185 — val_precision_w: 0.761563 — val_recall_w: 0.760366 454 | 55129/55129 [==============================] - 7s 119us/step - loss: 0.7253 - acc: 0.7537 - val_loss: 0.7111 - val_acc: 0.7604 455 | Epoch 3/30 456 | 54992/55129 [============================>.] - ETA: 0s - loss: 0.6856 - acc: 0.7660— val_f1: 0.650076 — val_precision: 0.731066 — val_recall: 0.608824 457 | — val_f1_w: 0.760444 — val_precision_w: 0.769572 — val_recall_w: 0.766569 458 | 55129/55129 [==============================] - 7s 119us/step - loss: 0.6856 - acc: 0.7660 - val_loss: 0.6827 - val_acc: 0.7666 459 | Epoch 4/30 460 | 54704/55129 [============================>.] - ETA: 0s - loss: 0.6587 - acc: 0.7760— val_f1: 0.665685 — val_precision: 0.750019 — val_recall: 0.625693 461 | — val_f1_w: 0.768111 — val_precision_w: 0.777307 — val_recall_w: 0.773914 462 | 55129/55129 [==============================] - 7s 119us/step - loss: 0.6585 - acc: 0.7761 - val_loss: 0.6611 - val_acc: 0.7739 463 | Epoch 5/30 464 | 54912/55129 [============================>.] - ETA: 0s - loss: 0.6361 - acc: 0.7839— val_f1: 0.675093 — val_precision: 0.756560 — val_recall: 0.633105 465 | — val_f1_w: 0.772584 — val_precision_w: 0.779329 — val_recall_w: 0.779465 466 | 55129/55129 [==============================] - 6s 117us/step - loss: 0.6363 - acc: 0.7838 - val_loss: 0.6531 - val_acc: 0.7795 467 | Epoch 6/30 468 | 54752/55129 [============================>.] - ETA: 0s - loss: 0.6175 - acc: 0.7909— val_f1: 0.677678 — val_precision: 0.760474 — val_recall: 0.638087 469 | — val_f1_w: 0.782750 — val_precision_w: 0.789371 — val_recall_w: 0.788116 470 | 55129/55129 [==============================] - 6s 116us/step - loss: 0.6178 - acc: 0.7908 - val_loss: 0.6286 - val_acc: 0.7881 471 | Epoch 7/30 472 | 55056/55129 [============================>.] - ETA: 0s - loss: 0.6018 - acc: 0.7958— val_f1: 0.698941 — val_precision: 0.753141 — val_recall: 0.667789 473 | — val_f1_w: 0.790828 — val_precision_w: 0.793462 — val_recall_w: 0.795462 474 | 55129/55129 [==============================] - 6s 117us/step - loss: 0.6021 - acc: 0.7957 - val_loss: 0.6192 - val_acc: 0.7955 475 | Epoch 8/30 476 | 54736/55129 [============================>.] - ETA: 0s - loss: 0.5884 - acc: 0.8009— val_f1: 0.699604 — val_precision: 0.766330 — val_recall: 0.662288 477 | — val_f1_w: 0.794905 — val_precision_w: 0.797012 — val_recall_w: 0.800033 478 | 55129/55129 [==============================] - 7s 120us/step - loss: 0.5887 - acc: 0.8008 - val_loss: 0.6067 - val_acc: 0.8000 479 | Epoch 9/30 480 | 54880/55129 [============================>.] - ETA: 0s - loss: 0.5753 - acc: 0.8059— val_f1: 0.703015 — val_precision: 0.768262 — val_recall: 0.665053 481 | — val_f1_w: 0.800541 — val_precision_w: 0.807472 — val_recall_w: 0.804114 482 | 55129/55129 [==============================] - 7s 118us/step - loss: 0.5758 - acc: 0.8057 - val_loss: 0.5961 - val_acc: 0.8041 483 | Epoch 10/30 484 | 55040/55129 [============================>.] - ETA: 0s - loss: 0.5649 - acc: 0.8090— val_f1: 0.707821 — val_precision: 0.784078 — val_recall: 0.666537 485 | — val_f1_w: 0.804109 — val_precision_w: 0.811529 — val_recall_w: 0.809011 486 | 55129/55129 [==============================] - 6s 116us/step - loss: 0.5646 - acc: 0.8091 - val_loss: 0.5840 - val_acc: 0.8090 487 | Epoch 11/30 488 | 55056/55129 [============================>.] - ETA: 0s - loss: 0.5552 - acc: 0.8127— val_f1: 0.711974 — val_precision: 0.784070 — val_recall: 0.671254 489 | — val_f1_w: 0.809180 — val_precision_w: 0.819130 — val_recall_w: 0.813092 490 | 55129/55129 [==============================] - 7s 118us/step - loss: 0.5550 - acc: 0.8128 - val_loss: 0.5850 - val_acc: 0.8131 491 | Epoch 12/30 492 | 55008/55129 [============================>.] - ETA: 0s - loss: 0.5454 - acc: 0.8167— val_f1: 0.714970 — val_precision: 0.773000 — val_recall: 0.679629 493 | — val_f1_w: 0.807968 — val_precision_w: 0.811567 — val_recall_w: 0.810970 494 | 55129/55129 [==============================] - 7s 119us/step - loss: 0.5459 - acc: 0.8165 - val_loss: 0.5757 - val_acc: 0.8110 495 | Epoch 13/30 496 | 54880/55129 [============================>.] - ETA: 0s - loss: 0.5371 - acc: 0.8196— val_f1: 0.708290 — val_precision: 0.802882 — val_recall: 0.662145 497 | — val_f1_w: 0.808888 — val_precision_w: 0.817245 — val_recall_w: 0.814887 498 | 55129/55129 [==============================] - 7s 118us/step - loss: 0.5369 - acc: 0.8196 - val_loss: 0.5711 - val_acc: 0.8149 499 | Epoch 14/30 500 | 55008/55129 [============================>.] - ETA: 0s - loss: 0.5295 - acc: 0.8216— val_f1: 0.720639 — val_precision: 0.784796 — val_recall: 0.684186 501 | — val_f1_w: 0.813487 — val_precision_w: 0.820760 — val_recall_w: 0.816520 502 | 55129/55129 [==============================] - 7s 119us/step - loss: 0.5294 - acc: 0.8216 - val_loss: 0.5735 - val_acc: 0.8165 503 | Epoch 15/30 504 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.5218 - acc: 0.8245— val_f1: 0.723226 — val_precision: 0.793214 — val_recall: 0.682388 505 | — val_f1_w: 0.813296 — val_precision_w: 0.821299 — val_recall_w: 0.817009 506 | 55129/55129 [==============================] - 7s 118us/step - loss: 0.5217 - acc: 0.8246 - val_loss: 0.5625 - val_acc: 0.8170 507 | Epoch 16/30 508 | 54784/55129 [============================>.] - ETA: 0s - loss: 0.5147 - acc: 0.8259— val_f1: 0.718378 — val_precision: 0.805836 — val_recall: 0.671866 509 | — val_f1_w: 0.812241 — val_precision_w: 0.821222 — val_recall_w: 0.817009 510 | 55129/55129 [==============================] - 7s 118us/step - loss: 0.5150 - acc: 0.8259 - val_loss: 0.5591 - val_acc: 0.8170 511 | Epoch 17/30 512 | 55056/55129 [============================>.] - ETA: 0s - loss: 0.5085 - acc: 0.8286— val_f1: 0.710259 — val_precision: 0.800253 — val_recall: 0.667554 513 | — val_f1_w: 0.811823 — val_precision_w: 0.817403 — val_recall_w: 0.818642 514 | 55129/55129 [==============================] - 7s 118us/step - loss: 0.5087 - acc: 0.8285 - val_loss: 0.5605 - val_acc: 0.8186 515 | Epoch 18/30 516 | 54704/55129 [============================>.] - ETA: 0s - loss: 0.5026 - acc: 0.8304— val_f1: 0.719479 — val_precision: 0.797672 — val_recall: 0.676889 517 | — val_f1_w: 0.816203 — val_precision_w: 0.823536 — val_recall_w: 0.820764 518 | 55129/55129 [==============================] - 7s 119us/step - loss: 0.5029 - acc: 0.8303 - val_loss: 0.5533 - val_acc: 0.8208 519 | Epoch 19/30 520 | 54784/55129 [============================>.] - ETA: 0s - loss: 0.4975 - acc: 0.8326— val_f1: 0.718767 — val_precision: 0.790945 — val_recall: 0.677815 521 | — val_f1_w: 0.815264 — val_precision_w: 0.819433 — val_recall_w: 0.820601 522 | 55129/55129 [==============================] - 6s 118us/step - loss: 0.4972 - acc: 0.8328 - val_loss: 0.5533 - val_acc: 0.8206 523 | Epoch 20/30 524 | 54704/55129 [============================>.] - ETA: 0s - loss: 0.4914 - acc: 0.8341— val_f1: 0.732335 — val_precision: 0.777586 — val_recall: 0.702288 525 | — val_f1_w: 0.817729 — val_precision_w: 0.819907 — val_recall_w: 0.820927 526 | 55129/55129 [==============================] - 6s 118us/step - loss: 0.4915 - acc: 0.8340 - val_loss: 0.5515 - val_acc: 0.8209 527 | Epoch 21/30 528 | 54832/55129 [============================>.] - ETA: 0s - loss: 0.4868 - acc: 0.8365— val_f1: 0.730587 — val_precision: 0.787088 — val_recall: 0.694430 529 | — val_f1_w: 0.816936 — val_precision_w: 0.820749 — val_recall_w: 0.819948 530 | 55129/55129 [==============================] - 7s 118us/step - loss: 0.4865 - acc: 0.8365 - val_loss: 0.5466 - val_acc: 0.8199 531 | Epoch 22/30 532 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.4820 - acc: 0.8377— val_f1: 0.729099 — val_precision: 0.795526 — val_recall: 0.689722 533 | — val_f1_w: 0.820917 — val_precision_w: 0.825831 — val_recall_w: 0.825171 534 | 55129/55129 [==============================] - 6s 117us/step - loss: 0.4821 - acc: 0.8377 - val_loss: 0.5439 - val_acc: 0.8252 535 | Epoch 23/30 536 | 54960/55129 [============================>.] - ETA: 0s - loss: 0.4769 - acc: 0.8401— val_f1: 0.728253 — val_precision: 0.777373 — val_recall: 0.697222 537 | — val_f1_w: 0.816949 — val_precision_w: 0.818159 — val_recall_w: 0.820764 538 | 55129/55129 [==============================] - 7s 120us/step - loss: 0.4770 - acc: 0.8399 - val_loss: 0.5483 - val_acc: 0.8208 539 | Epoch 24/30 540 | 54880/55129 [============================>.] - ETA: 0s - loss: 0.4726 - acc: 0.8416— val_f1: 0.737025 — val_precision: 0.785367 — val_recall: 0.705383 541 | — val_f1_w: 0.822261 — val_precision_w: 0.824854 — val_recall_w: 0.825661 542 | 55129/55129 [==============================] - 7s 122us/step - loss: 0.4725 - acc: 0.8416 - val_loss: 0.5429 - val_acc: 0.8257 543 | Epoch 25/30 544 | 54736/55129 [============================>.] - ETA: 0s - loss: 0.4682 - acc: 0.8432— val_f1: 0.729383 — val_precision: 0.774703 — val_recall: 0.698946 545 | — val_f1_w: 0.819586 — val_precision_w: 0.823633 — val_recall_w: 0.821580 546 | 55129/55129 [==============================] - 6s 117us/step - loss: 0.4684 - acc: 0.8430 - val_loss: 0.5506 - val_acc: 0.8216 547 | Epoch 26/30 548 | 54928/55129 [============================>.] - ETA: 0s - loss: 0.4644 - acc: 0.8430— val_f1: 0.719513 — val_precision: 0.784795 — val_recall: 0.681772 549 | — val_f1_w: 0.816008 — val_precision_w: 0.819116 — val_recall_w: 0.820601 550 | 55129/55129 [==============================] - 6s 117us/step - loss: 0.4645 - acc: 0.8429 - val_loss: 0.5515 - val_acc: 0.8206 551 | Epoch 27/30 552 | 54864/55129 [============================>.] - ETA: 0s - loss: 0.4604 - acc: 0.8444— val_f1: 0.739462 — val_precision: 0.782457 — val_recall: 0.710235 553 | — val_f1_w: 0.823328 — val_precision_w: 0.825483 — val_recall_w: 0.826641 554 | 55129/55129 [==============================] - 7s 119us/step - loss: 0.4607 - acc: 0.8443 - val_loss: 0.5390 - val_acc: 0.8266 555 | Epoch 28/30 556 | 54912/55129 [============================>.] - ETA: 0s - loss: 0.4567 - acc: 0.8456— val_f1: 0.726324 — val_precision: 0.789678 — val_recall: 0.689405 557 | — val_f1_w: 0.821804 — val_precision_w: 0.826340 — val_recall_w: 0.825171 558 | 55129/55129 [==============================] - 6s 116us/step - loss: 0.4569 - acc: 0.8455 - val_loss: 0.5420 - val_acc: 0.8252 559 | Epoch 29/30 560 | 54752/55129 [============================>.] - ETA: 0s - loss: 0.4533 - acc: 0.8479— val_f1: 0.735079 — val_precision: 0.787528 — val_recall: 0.700854 561 | — val_f1_w: 0.824626 — val_precision_w: 0.828260 — val_recall_w: 0.828273 562 | 55129/55129 [==============================] - 6s 115us/step - loss: 0.4529 - acc: 0.8479 - val_loss: 0.5414 - val_acc: 0.8283 563 | ``` 564 | 565 | * 모델 construction과 (최고 performance를 보이는 지점까지의) training-evaluation 입니다. 매 checkpoint에서 모델들이 tutorial이라는 폴더에 저장되어야 하니, 미리 만들어 두어야겠죠 ㅎㅎ TF-IDF의 결과들이 그렇게 만족스럽지는 못했다는 걸 생각하면, 괄목할 만한 성장입니다. accuracy도 올랐고, F1의 평균값 (val_f1)도 상당한 수준으로 상승했네요. one-hot vector을 만들 때 그랬던 것처럼 그냥 구성요소들을 더했을 뿐인데 ...? 566 | 567 | * 겨우 100차원인 벡터들을 더해서 뭘 표현할 수 있을까? 싶은 분들도 분명 계실 겁니다. 하지만, 두 가지를 상기할 필요가 있습니다. (1) word vector들은 one-hot vector들처럼 equivalent하지 않고, 특정 기준에 의해 training되었다 - 즉 그 자체로 어떤 의미를 지니고 있다. (2) 벡터들의 합으로 얻는 벡터 역시 100dim 공간에 표현될 수 있으며, 100dim은 그 방향만 해도 2^100 개 이상을 나타낼 수 있을 정도로 꽤나 많은 것을 표현할 수 있다! 568 | 569 | --- 570 | 571 | **This kind of sentence encoding gives us quite a rich representation of the sentences in the sense that the 100-dimensional vector itself yields a variety of values. This might be advantageous for tasks such as sentiment analysis, in which the inference largely relies on some polarity items or emotion words. However, we should notice that a simple summation does not say anything about the distributional or sequential information the sentence possesses; for instance, “You haven’t done it at all” and “Haven’t you done it at all” share the same word composition but their intention clearly differs. The same kind of problem is more critical in Korean, due to the language being scrambling.** 572 | 573 | * 이런 방식의 sentence vector 만들기는 sentiment classification 같은 task에서는 꽤나 좋은 성능을 냅니다. sentiment는 주로 단어 내의 어떤 polarity 및 subjectivity가 있는 단어에 의해 형성될 가능성이 높은데, 더하는 것만으로도 어떤 word가 있다는 것을 classifier가 알게 하기엔 충분하기 때문이죠. 하지만, summation의 단점은 분포나 순서를 고려할 수 없다는 겁니다. 예컨대, “You haven’t done it at all”과 “Haven’t you done it at all”은 그 단어 구성은 같아도 (capital 여부는 무시합시다) 전달하는 의미는 전혀 다르죠. word vector의 summation이 아니라 concatenation으로 한다면 이런 일을 예방할 수 있겠습니다만, 뭔가 임시방편적인 처방이고 결국 다시 저차원 임베딩이 아니게 되어버리겠죠. 이런 문제를 어떻게 하면 해결할 수 있을까요? 574 | 575 | ## 6. CNN-based sentence classification 576 | 577 | [**Convolutional neural network**](https://ieeexplore.ieee.org/abstract/document/726791) (CNN), originally developed by LeCun, is a widely used neural network system which was primarily suggested for image processing (handwriting recognition). It roughly resembles the perception process of a visual system, summarizing a given image into an abstract values via repititive application of convolution and pooling. 578 | 579 |

580 |
581 | (image from https://jeremykarnowski.wordpress.com/2015/07/15/alexnet-visualization/) 582 | 583 | * AI를 공부하시는 분이라면 convolutional neural network, 즉 CNN을 한번쯤 들어보셨을 겁니다. 초기에 르쿤 등등에 의해 연구되고, 6-7년 전쯤부터 폭발적으로 성장하여 지금은 이미지 관련 태스크라면 베이스라인 혹은 그 베리에이션으로 인용된다는 것두요. 그러한 이력 덕분인지, NLP에 CNN을 사용한다고 하면 의아해하는 경우가 있더군요. 저도 사실 익숙해져서 그렇지, 다시 첨부터 써보라고 하면 이게 무슨 소리야 싶을지도 모르겠네요ㅎㅎ 584 | 585 | * 제가 이미지에 CNN을 사용해본 적은 거의 없지만, 쉽게 말해 raw data를 부분부분 보고 그 정보를 추상화하는 과정을 여러번 거친다, 라고 생각하고 있습니다. 아주 러프하게요. 그것이 이미지에 처음 적용된 것이죠. 하지만 사실 정보의 추상화란 이미지에만 적용될 이유는 없습니다. 그래서 저는 cnn을 distributional information의 summarizer로 표현합니다. 어디에 무엇이 있는지 아주 간단하게 요약해 주는. 586 | 587 | --- 588 | 589 | **However, since the very classic breakthrough of [Kim 2014](https://arxiv.org/abs/1408.5882), CNN has been widely used in the text processing, understanding the word vector sequence as a single channel image. Unlike the previous approach where all the information of the words in the sentence are aggregated into a single vector, the featurization for CNN has its limitation in the volume. Thus, here we restrict the maximum length of the morpheme sequence to 30, with zero-padding for the short utterances. Taking into account the head-finality of Korean, we decided to place the word vectors on the right side of the matrix. That is, for the long utterances, only the last 30 morphemes are utilized.** 590 | 591 | ```python 592 | def featurize_cnn(corpus,wdim,maxlen): 593 | conv_total = np.zeros((len(corpus),maxlen,wdim,1)) 594 | for i in range(len(corpus)): 595 | if i%1000 ==0: 596 | print(i) 597 | s = corpus[i] 598 | for j in range(len(s)): 599 | if s[-j-1] in model_ft and j < maxlen: 600 | conv_total[i][-j-1,:,0] = model_ft[s[-j-1]] 601 | return conv_total 602 | 603 | fci_conv = featurize_cnn(fci_sp_token_train+fci_sp_token_test,100,30) 604 | ``` 605 | 606 |

607 |
608 | (image from Kim 2014) 609 | 610 | * 이것이 문장 분류에 어떻게 사용되느냐? 가장 먼저 거치는 과정은 쉽게 말해 문장을 그림처럼 바꾸는 겁니다. 즉, 단일 채널 matrix를 만드는 거죠 (그림은 보통 rgb의 3 channel). 우린 sentence matrix란 걸 논한 적 없으니 word vector들로 어떻게 해 봐야 될 텐데, word vector나 TF-IDF를 가지고는 듬성듬성하게 nonzero가 박혀 있는 것들밖에 만들지 못할 테죠. 애초에 값에 대한 위치 bias가 없는 녀석들이니 순서(order)적인 것 외에 아무 정보도 CNN에 주지는 못할 겁니다. 611 | 612 | * 이 때 다시 등장하는 것이 앞서 언급한 word2vec입니다. 문장을 수치화해 넣을 수 있는 일종의 고정된 사이즈의 도화지가 있다고 생각해 봅시다. 예컨대 100 x 30정도의? 거기에 100-dim word vector 30개를 padding해 넣는 겁니다. 물론 문장 길이가 30이 되지 않을 수도 있지요. 그러면 빈 부분은 0으로 채웁니다. 진짜 없으니까요. 문장이 더 길다면? 자릅니다. 물론 이 부분은 '문장 최대길이'를 조사해서 적절히 설정하면 될 일입니다 (물론 이렇게 하지 않고 모두 보존하는 방법도 있겠습니다만, 일단 여기선 다루지 않겠습니다). 이 과정에서 한국어의 head-finality를 고려하여 문장은 오른쪽에 치우치게 배열하도록 결정하였습니다. 한국어는 역시 끝까지 들어봐야 하니까, 자르더라도 끝은 남겨야죠! 613 | 614 | --- 615 | 616 | **There are so many types of convolutional networks out there (LeNet, AlexNet, VGG, YOLO ...), however such deep and wide networks might not be required for the sentence classification. The model architecture used for the implementation is quite simple; two convolutional layers with the window width 3 and a max pooling layer between with the size (2,1). For the first conv-layer, the window size is (3,100) and for the second (3,1), since the information was abstracted and max-pooled to make up a single vector.** 617 | 618 | ```python 619 | def validate_cnn(result,y,filters,hidden_dim,cw,filename): 620 | model = Sequential() 621 | model.add(layers.Conv2D(filters,(3,len(result[0][0])),activation= 'relu',input_shape = (len(result[0]),len(result[0][0]),1))) 622 | model.add(layers.MaxPooling2D((2,1))) 623 | model.add(layers.Conv2D(filters,(3,1),activation='relu')) 624 | model.add(layers.Flatten()) 625 | model.add(layers.Dense(hidden_dim,activation='relu')) 626 | model.add(layers.Dense(int(max(y)+1), activation='softmax')) 627 | model.summary() 628 | model.compile(optimizer=adam_half, loss="sparse_categorical_crossentropy", metrics=["accuracy"]) 629 | filepath=filename+"-{epoch:02d}-{val_acc:.4f}.hdf5" 630 | checkpoint = ModelCheckpoint(filepath, monitor='val_acc', verbose=1, mode='max') 631 | callbacks_list = [metricsf1macro,checkpoint] 632 | model.fit(result,y,validation_split=0.1,epochs=30,batch_size=16,callbacks=callbacks_list,class_weight=cw) 633 | 634 | validate_cnn(fci_conv,fci_label,32,128,class_weights_fci,'model/tutorial/conv') 635 | ``` 636 | 637 | * 일단 image를 cnn에 적용하는 과정을 패러미터화하면, 채널, 필터, 컨벌루션레이어, 윈도우, 풀링 정도로 요약할 수 있습니다. 채널은 앞서 말했듯 rgb 같이 몇 개의 요소로 나타내냐이며 필터는 얼마나 병렬로 처리할거냐, 컨벌루션레이어 수는 추상화 과정을 몇번 거칠거냐, 윈도우는 어떤 식으로 각 컨벌루션 레이어를 훑을거냐, 풀링은 컨벌루션 레이어를 훑은 값들에서 중요한 요소들을 어떻게 취사선택할거냐? 이정도로 나타낼 수 있겠네요. 638 | 639 | * 이미지의 cnn은 그래서 일반적으로 3채널, 많은 필터 (>64?), 다층 컨벌루션 레이어, 상하좌우로 stride되는 3 by 3 혹은 5 by 5 window 등으로 요약될 수 있습니다. 물론 alexnet, vgg, yolo 등 다양한 아키텍쳐들이 있고, 모두 특색이 있겠지만, 기본적으론 저렇습니다. 하지만 word vector sequence에서 상하좌우로 움직이는 window가 어떤 의미가 있을까요? 우리는 100dim의 벡터 각 엔트리에 어떤 성질의 성분들이 자리잡고 있는지 알지 못하며, 굳이 그런 성질을 지정해줄 필요도 느끼지 못하였습니다. 이미지는 두차원 모두가 semantic을 포함하지만, sentence에서 semantic이 의미가 있는 방향은 word vector가 pad되는 방향이니까요. 640 | 641 | * 그래서 저는 sentence의 cnn에선 3 by 100 혹은 5 by 100 window를 사용합니다. 결론적으론 2D convolution이 1D처럼 돼버리긴 합니다만, 역설적으로 문장을 그림이 아니라 문장처럼 볼 수 있는 방법이 되는 것 같아요. Word2vec의 결과물로 나온 그 단어의 vector의 특색을 결정짓는 entry를 가장 왜곡 없이 전달해줄 수 있는 방법이라고 저는 보고 있습니다. 그 이후의 max-pooling과 추가적인 convolution 아키텍쳐는 개인의 선택에 달렸지만요. Boolean distributional semantics처럼 word vector의 각 entry가 어떤 의미를 갖는다면 모르겠지만, 그렇지 않다면 문장은 문장처럼 읽는 것이 cnn에 있어서도 효과적이지 않나? 라는 것이 저의 생각입니다. (물론 반례 및 피드백은 언제나 환영입니다!) 642 | 643 | --- 644 | 645 | **The result is encouraging! Although there was a little improvement in F1 score, we had about 20% RRER for the accuracy (regarding the model with the best performance). The CNN-based featurization and classification of the sentence shows quite satisfactory result with very fast training. However, the architecture does not seem to still convey the correlation between the non-consecutive components. The recurrent neural network covers such characteristics, with a sequence-based approach.** 646 | 647 | ```properties 648 | # CONSOLE RESULT 649 | >>> validate_cnn(fci_conv,fci_label,32,128,class_weights_fci,'model/tutorial/conv') 650 | _________________________________________________________________ 651 | Layer (type) Output Shape Param # 652 | ================================================================= 653 | conv2d_1 (Conv2D) (None, 28, 1, 32) 9632 654 | _________________________________________________________________ 655 | max_pooling2d_1 (MaxPooling2 (None, 14, 1, 32) 0 656 | _________________________________________________________________ 657 | conv2d_2 (Conv2D) (None, 12, 1, 32) 3104 658 | _________________________________________________________________ 659 | flatten_1 (Flatten) (None, 384) 0 660 | _________________________________________________________________ 661 | dense_5 (Dense) (None, 128) 49280 662 | _________________________________________________________________ 663 | dense_6 (Dense) (None, 7) 903 664 | ================================================================= 665 | Total params: 62,919 666 | Trainable params: 62,919 667 | Non-trainable params: 0 668 | _________________________________________________________________ 669 | Train on 55129 samples, validate on 6126 samples 670 | Epoch 1/30 671 | 54992/55129 [============================>.] - ETA: 0s - loss: 0.5817 - acc: 0.8026— val_f1: 0.715119 — val_precision: 0.766993 — val_recall: 0.687496 672 | — val_f1_w: 0.836404 — val_precision_w: 0.840650 — val_recall_w: 0.842148 673 | 55129/55129 [==============================] - 21s 375us/step - loss: 0.5816 - acc: 0.8027 - val_loss: 0.4907 - val_acc: 0.8421 674 | Epoch 2/30 675 | 54992/55129 [============================>.] - ETA: 0s - loss: 0.4391 - acc: 0.8505— val_f1: 0.748518 — val_precision: 0.785899 — val_recall: 0.724397 676 | — val_f1_w: 0.850521 — val_precision_w: 0.852202 — val_recall_w: 0.853901 677 | 55129/55129 [==============================] - 21s 376us/step - loss: 0.4389 - acc: 0.8506 - val_loss: 0.4531 - val_acc: 0.8539 678 | Epoch 3/30 679 | 54992/55129 [============================>.] - ETA: 0s - loss: 0.3934 - acc: 0.8667— val_f1: 0.755061 — val_precision: 0.791092 — val_recall: 0.731512 680 | — val_f1_w: 0.855977 — val_precision_w: 0.857298 — val_recall_w: 0.859288 681 | 55129/55129 [==============================] - 20s 371us/step - loss: 0.3936 - acc: 0.8667 - val_loss: 0.4413 - val_acc: 0.8593 682 | Epoch 4/30 683 | 54992/55129 [============================>.] - ETA: 0s - loss: 0.3640 - acc: 0.8753— val_f1: 0.762634 — val_precision: 0.783417 — val_recall: 0.755429 684 | — val_f1_w: 0.853230 — val_precision_w: 0.858127 — val_recall_w: 0.856024 685 | 55129/55129 [==============================] - 21s 374us/step - loss: 0.3640 - acc: 0.8753 - val_loss: 0.4558 - val_acc: 0.8560 686 | Epoch 5/30 687 | 54992/55129 [============================>.] - ETA: 0s - loss: 0.3402 - acc: 0.8824— val_f1: 0.756524 — val_precision: 0.798401 — val_recall: 0.728596 688 | — val_f1_w: 0.857324 — val_precision_w: 0.857582 — val_recall_w: 0.860431 689 | 55129/55129 [==============================] - 20s 370us/step - loss: 0.3403 - acc: 0.8824 - val_loss: 0.4348 - val_acc: 0.8604 690 | Epoch 6/30 691 | 55024/55129 [============================>.] - ETA: 0s - loss: 0.3210 - acc: 0.8910— val_f1: 0.761926 — val_precision: 0.782188 — val_recall: 0.748026 692 | — val_f1_w: 0.856406 — val_precision_w: 0.857747 — val_recall_w: 0.858146 693 | 55129/55129 [==============================] - 21s 374us/step - loss: 0.3209 - acc: 0.8910 - val_loss: 0.4406 - val_acc: 0.8581 694 | Epoch 7/30 695 | 55040/55129 [============================>.] - ETA: 0s - loss: 0.3051 - acc: 0.8947— val_f1: 0.758700 — val_precision: 0.784020 — val_recall: 0.740566 696 | — val_f1_w: 0.850813 — val_precision_w: 0.850469 — val_recall_w: 0.853575 697 | 55129/55129 [==============================] - 21s 373us/step - loss: 0.3051 - acc: 0.8947 - val_loss: 0.4540 - val_acc: 0.8536 698 | Epoch 8/30 699 | 55088/55129 [============================>.] - ETA: 0s - loss: 0.2900 - acc: 0.9003— val_f1: 0.764175 — val_precision: 0.814364 — val_recall: 0.732869 700 | — val_f1_w: 0.861861 — val_precision_w: 0.865253 — val_recall_w: 0.865491 701 | 55129/55129 [==============================] - 21s 373us/step - loss: 0.2899 - acc: 0.9004 - val_loss: 0.4441 - val_acc: 0.8655 702 | ``` 703 | 704 | * 결과입니다. F1 score에서는 그렇게 큰 향상을 얻지는 못했으나, accuracy는 0.8283에서 0.8655로, error rate가 약 17%에서 14%로 감소하며 약 20%의 에러율 감소 (RRER)을 보였습니다. 아무래도 distributional한 information을 반영하는 것이, 단순히 existence를 체크하는 것보다는 더 정확하겠죠. 705 | 706 | * 이상으로 distributional information의 가장 효과적인 summarizer 중 하나에 대하여 살펴봤습니다. 하지만 그 단점은, non-consecutive한 성분들 간의 상관관계를 설명하기 쉽지 않다는 점이죠. 다음 편부터 설명되는 rnn은 그러한 점들을 보완합니다. 707 | 708 | ## 7. RNN (BiLSTM)-based sentence classification 709 | 710 | ***Recurrent neural network (RNN)*, which was suggested originally in the late 20th century, is a representative network that reflects the sequential information in the numerical summarization. Due to high-computation issue, its materialization has recently been possible with the help of modern computing systems (e.g. GPU boost-up). The problem of vanishing gradient has been partially solved by [*long short-term memory (LSTM)*](https://www.mitpressjournals.org/doi/abs/10.1162/neco.1997.9.8.1735), whose direction-bias was improved along with the bidirectional sequencing (BiLSTM).** 711 | 712 |

713 |
714 | (image from https://aikorea.org/blog/rnn-tutorial-1/) 715 | 716 | * RNN, 즉 recurrent neural network란 time-series의 input을 받아, 그에 대한 latent information을 담고 있는 hidden layer sequence를 생성하되, 특정 시점에서의 hidden layer가 그 시점에서의 input data와 이전 시점의 hidden layer로부터 연산되는 알고리듬입니다. hidden Markov model (HMM)과 그 컨셉은 유사하지만 바로 이전 시점에만 영향받지 않고, 앞서 있는 데이터 모두의 정보를 뒷부분의 hidden layer에 반영한다는 장점이 있죠. 쉽게 말해 sequential data의 summarization이라고 보면 될 것 같네요. 717 | 718 | * rnn을 트레이닝하는 과정 역시 일반적인 mlp나 cnn에서와 마찬가지로 back-propagation을 이용하게 되는데요, 이 과정에서 vanishing gradient의 문제가 발생하게 됩니다. 너무 많은 정보들이 encoding되다 보니 패러미터가 폭발해 버리는 겁니다. 사람은 이와 다르게, 문장이나 글이 길어지게 되면 너무 멀리 떨어져 있는 정보는 잊어버리죠 :D 뭐 그게 꼭 좋은 건 아니겠지만서두, 정보 과잉을 방지해주거나 뭐 그렇지 않겠습니까? 그런 컨셉으로 나온 것이 적당히 forget gate를 추가한, 1997년의 long short-term memory (LSTM) 입니다. lstm이 앞부분의 정보를 반영하지 못한다는 단점을 보완하기 위해 제시된 것이, lstm을 양방향으로 (처음에서 시작해서 끝으로, 끝에서 시작해서 처음으로) 하여 얻은 hidden layer sequences를 augment한 Bidirectional lstm도 같은 해에 제시되었구요. 생각보다 옛날인데 왜 이제 와서 유행하게 됐냐구요? 계산량이 엄청나기 때문이죠... 갓비디아 짱짱컴퍼니 719 | 720 | --- 721 | 722 | **In this tutorial, we utilize the BiLSTM structure. The featurization is almost identical to the case of CNN, except that the channel information is omitted. To say short, for the same data (ignoring the channel number), CNN extracts locally notable features and RNN extracts the relations reflected in the sequential arrangement.** 723 | 724 | ```python 725 | def featurize_rnn(corpus,wdim,maxlen): 726 | rnn_total = np.zeros((len(corpus),maxlen,wdim)) 727 | for i in range(len(corpus)): 728 | if i%1000 ==0: 729 | print(i) 730 | s = corpus[i] 731 | for j in range(len(s)): 732 | if s[-j-1] in model_ft and j < maxlen: 733 | rnn_total[i][-j-1,:] = model_ft[s[-j-1]] 734 | return rnn_total 735 | 736 | fci_rec = featurize_rnn(fci_sp_token_train+fci_sp_token_test,100,30) 737 | 738 | from keras.layers import LSTM 739 | from keras.layers import Bidirectional 740 | 741 | def validate_bilstm(result,y,hidden_lstm,hidden_dim,cw,filename): 742 | model = Sequential() 743 | model.add(Bidirectional(LSTM(hidden_lstm), input_shape=(len(result[0]), len(result[0][0])))) 744 | model.add(layers.Dense(hidden_dim, activation='relu')) 745 | model.add(layers.Dense(int(max(y)+1), activation='softmax')) 746 | model.summary() 747 | model.compile(optimizer=adam_half, loss="sparse_categorical_crossentropy", metrics=["accuracy"]) 748 | filepath=filename+"-{epoch:02d}-{val_acc:.4f}.hdf5" 749 | checkpoint = ModelCheckpoint(filepath, monitor='val_acc', verbose=1, mode='max') 750 | callbacks_list = [metricsf1macro,checkpoint] 751 | model.fit(result,y,validation_split=0.1,epochs=30,batch_size=16,callbacks=callbacks_list,class_weight=cw) 752 | 753 | validate_bilstm(fci_rec,fci_label,32,128,class_weights_fci,'model/tutorial/rec') 754 | ``` 755 | 756 | * 본 튜토리얼에서는 가장 무난히 적용할 수 있는 BiLSTM을 사용합니다. LSTM의 문제는 문장이 길어질수록 앞부분의 내용을 반영하지 못할 수 있다는 것인데, BiLSTM은 LSTM을 양방향으로 돌린 두 sequence를 hidden layer sequence 하나로 concatenate하면서 어느 정도 그런 점을 보완할 수 있게 합니다. CNN과 간단히 차이를 말해 보자면, CNN은 global하게 본 후 local하게 중요한 피쳐들을 뽑아내어 추상화시키는 것이고, RNN (BiLSTM)은 sequential한 배열에 내재한 관계를 추상화한다고 보면 될 것 같습니다. 757 | 758 | --- 759 | 760 | **We obtained another boost in performance, especially very high in F1 score. This implies that our task which deals with the intention of Korean sentences is largely influenced by the word order. This may have been an important factor in identifying the rhetoricalness, for instance, '뭐 해 지금 (what / do / now, *what (on earth) are you doing now*)' sounds much more rhetorical than '지금 뭐 해 (now / what / do, *what are you doing now*)', in view of native. However, so far we've conducted the experiments given the morpheme-level decomposition. Will the real semantics NOT be embedded in the characters? Well, before that, how should *character* be defined in Korean, which has distinguished morpho-syllabic blocks (which are different from alphabets) as a unit of character? ** 761 | 762 | ```properties 763 | # CONSOLE RESULT 764 | >>> validate_bilstm(fci_rec,fci_label,32,128,class_weights_fci,0.1,16,'model/tutorial/rec') 765 | _________________________________________________________________ 766 | Layer (type) Output Shape Param # 767 | ================================================================= 768 | bidirectional_1 (Bidirection (None, 64) 34048 769 | _________________________________________________________________ 770 | dense_7 (Dense) (None, 128) 8320 771 | _________________________________________________________________ 772 | dense_8 (Dense) (None, 7) 903 773 | ================================================================= 774 | Total params: 43,271 775 | Trainable params: 43,271 776 | Non-trainable params: 0 777 | _________________________________________________________________ 778 | Train on 55129 samples, validate on 6126 samples 779 | Epoch 1/50 780 | 55088/55129 [============================>.] - ETA: 0s - loss: 0.5545 - acc: 0.8145— val_f1: 0.732477 — val_precision: 0.764964 — val_recall: 0.716559 781 | — val_f1_w: 0.838581 — val_precision_w: 0.844676 — val_recall_w: 0.841169 782 | 55129/55129 [==============================] - 86s 2ms/step - loss: 0.5544 - acc: 0.8145 - val_loss: 0.4835 - val_acc: 0.8412 783 | Epoch 2/50 784 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.4302 - acc: 0.8532— val_f1: 0.755420 — val_precision: 0.818620 — val_recall: 0.719585 785 | — val_f1_w: 0.856714 — val_precision_w: 0.860478 — val_recall_w: 0.860757 786 | 55129/55129 [==============================] - 83s 2ms/step - loss: 0.4302 - acc: 0.8531 - val_loss: 0.4248 - val_acc: 0.8608 787 | Epoch 3/50 788 | 55088/55129 [============================>.] - ETA: 0s - loss: 0.3909 - acc: 0.8658— val_f1: 0.769535 — val_precision: 0.820192 — val_recall: 0.738047 789 | — val_f1_w: 0.860804 — val_precision_w: 0.862253 — val_recall_w: 0.864675 790 | 55129/55129 [==============================] - 82s 1ms/step - loss: 0.3908 - acc: 0.8658 - val_loss: 0.4071 - val_acc: 0.8647 791 | Epoch 4/50 792 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.3626 - acc: 0.8757— val_f1: 0.780424 — val_precision: 0.814412 — val_recall: 0.758550 793 | — val_f1_w: 0.868592 — val_precision_w: 0.869740 — val_recall_w: 0.870389 794 | 55129/55129 [==============================] - 82s 1ms/step - loss: 0.3626 - acc: 0.8758 - val_loss: 0.3861 - val_acc: 0.8704 795 | Epoch 5/50 796 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.3419 - acc: 0.8823— val_f1: 0.783106 — val_precision: 0.806826 — val_recall: 0.767035 797 | — val_f1_w: 0.871497 — val_precision_w: 0.871639 — val_recall_w: 0.872837 798 | 55129/55129 [==============================] - 83s 2ms/step - loss: 0.3418 - acc: 0.8823 - val_loss: 0.3835 - val_acc: 0.8728 799 | Epoch 6/50 800 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.3238 - acc: 0.8889— val_f1: 0.787453 — val_precision: 0.815045 — val_recall: 0.766895 801 | — val_f1_w: 0.872195 — val_precision_w: 0.874534 — val_recall_w: 0.874633 802 | 55129/55129 [==============================] - 82s 1ms/step - loss: 0.3239 - acc: 0.8889 - val_loss: 0.3798 - val_acc: 0.8746 803 | Epoch 7/50 804 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.3093 - acc: 0.8935— val_f1: 0.782065 — val_precision: 0.797697 — val_recall: 0.772691 805 | — val_f1_w: 0.873711 — val_precision_w: 0.875246 — val_recall_w: 0.873327 806 | 55129/55129 [==============================] - 82s 1ms/step - loss: 0.3093 - acc: 0.8935 - val_loss: 0.3732 - val_acc: 0.8733 807 | Epoch 8/50 808 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.2953 - acc: 0.8978— val_f1: 0.777524 — val_precision: 0.800048 — val_recall: 0.762805 809 | — val_f1_w: 0.867708 — val_precision_w: 0.868467 — val_recall_w: 0.869572 810 | 55129/55129 [==============================] - 83s 1ms/step - loss: 0.2952 - acc: 0.8978 - val_loss: 0.3942 - val_acc: 0.8696 811 | Epoch 9/50 812 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.2820 - acc: 0.9028— val_f1: 0.783454 — val_precision: 0.822934 — val_recall: 0.758621 813 | — val_f1_w: 0.875902 — val_precision_w: 0.877293 — val_recall_w: 0.879367 814 | 55129/55129 [==============================] - 82s 1ms/step - loss: 0.2820 - acc: 0.9028 - val_loss: 0.3732 - val_acc: 0.8794 815 | Epoch 10/50 816 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.2708 - acc: 0.9065— val_f1: 0.790558 — val_precision: 0.811155 — val_recall: 0.773668 817 | — val_f1_w: 0.877717 — val_precision_w: 0.876924 — val_recall_w: 0.879530 818 | 55129/55129 [==============================] - 82s 1ms/step - loss: 0.2708 - acc: 0.9065 - val_loss: 0.3804 - val_acc: 0.8795 819 | Epoch 11/50 820 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.2572 - acc: 0.9108— val_f1: 0.778422 — val_precision: 0.789590 — val_recall: 0.770837 821 | — val_f1_w: 0.870727 — val_precision_w: 0.872009 — val_recall_w: 0.872674 822 | 55129/55129 [==============================] - 82s 1ms/step - loss: 0.2571 - acc: 0.9108 - val_loss: 0.3960 - val_acc: 0.8727 823 | Epoch 12/50 824 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.2460 - acc: 0.9154— val_f1: 0.788357 — val_precision: 0.805196 — val_recall: 0.775248 825 | — val_f1_w: 0.876743 — val_precision_w: 0.876289 — val_recall_w: 0.878387 826 | 55129/55129 [==============================] - 82s 1ms/step - loss: 0.2460 - acc: 0.9154 - val_loss: 0.3865 - val_acc: 0.8784 827 | Epoch 13/50 828 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.2356 - acc: 0.9185— val_f1: 0.768287 — val_precision: 0.766577 — val_recall: 0.772194 829 | — val_f1_w: 0.864052 — val_precision_w: 0.865701 — val_recall_w: 0.864022 830 | 55129/55129 [==============================] - 82s 1ms/step - loss: 0.2355 - acc: 0.9186 - val_loss: 0.4198 - val_acc: 0.8640 831 | Epoch 14/50 832 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.2253 - acc: 0.9225— val_f1: 0.768811 — val_precision: 0.775967 — val_recall: 0.768394 833 | — val_f1_w: 0.864381 — val_precision_w: 0.867127 — val_recall_w: 0.864185 834 | 55129/55129 [==============================] - 82s 1ms/step - loss: 0.2254 - acc: 0.9225 - val_loss: 0.4194 - val_acc: 0.8642 835 | Epoch 15/50 836 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.2147 - acc: 0.9258— val_f1: 0.782913 — val_precision: 0.790473 — val_recall: 0.778720 837 | — val_f1_w: 0.875200 — val_precision_w: 0.876058 — val_recall_w: 0.875775 838 | 55129/55129 [==============================] - 82s 1ms/step - loss: 0.2147 - acc: 0.9258 - val_loss: 0.4084 - val_acc: 0.8758 839 | Epoch 16/50 840 | 55088/55129 [============================>.] - ETA: 0s - loss: 0.2045 - acc: 0.9285— val_f1: 0.790696 — val_precision: 0.819661 — val_recall: 0.768516 841 | — val_f1_w: 0.878328 — val_precision_w: 0.878134 — val_recall_w: 0.880673 842 | 55129/55129 [==============================] - 82s 1ms/step - loss: 0.2044 - acc: 0.9285 - val_loss: 0.4099 - val_acc: 0.8807 843 | ``` 844 | 845 | * 놀랍게도 정확도가 다시 한번 올랐고, F1 score가 상당한 수준으로 올랐습니다. 평균치가 저 정도 올랐다는 것은, 이제 아무리 F1 score가 안 좋아도 0.5 정도는 된다는 걸 의미한다고 봐도 무방할 것 같습니다. 처음에 TF-IDF로 코드를 돌릴 때 intonation-dependent utterances (마지막 case) 에 대해 상당히 낮은 F1 score가 나왔던 것 같은데, 해당 태스크에서 상대적으로 Recall (재현률)을 높게 하여 false alarm을 울리는 것이 false positive보다는 낫다는 점을 고려하면 나쁘지 않은 결과입니다. 그런데 한 가지 간과한 것이 있습니다. 지금까지 우리는 열심히 morpheme들을 가지고 놀았는데, 과연 이것을 character-level로 끊어 보면 어떻게 될까요? 아니, 그 전에 우선, 한국어에서 character을 어떻게 정의하는 것이 바람직할까요? 846 | 847 | ## 8. Character embedding 848 | 849 | **Due to the distinguished writing style, the embedding of Korean letters *Hangul* is difficult even from its definition. For many Romanian or German langauges, the letters of latin alphabet are utilized as character and thereby character-level embeddding has been widely used in the region of text analysis since [Zhang, 2015](http://papers.nips.cc/paper/5782-character-level-convolutional-networks-for-text-classifica). However, for Korean, the morpho-syllabic blocks that represent the syllables, can be decomposed into *jamo*, the alphabet of the Korean writing system.** 850 | 851 | **To be specific, The letters of alphabet make up the morpho-syllabic blocks (characters) that are equal to the phonetic unit of the syllable, in the conjunct form of Syllable: CV(C). This notation implies that there should be at least one consonant (namely *cho-seng*, the first sound) and one vowel (namely *cwung-seng*, the second sound). An additional consonant (namely *cong-seng*, the third sound) is auxiliary. However, usually in the character decomposition, three slots are fully held for each component; an empty cell comes for the third entry if there is no auxiliary consonant. The number of the possible letters, or (composite) consonants/vowels, that can come for each slot is 19, 21, and 27. For instance, in a character '각 (*kak*)', three clock-wisely arranged alphabets ㄱ, ㅏ, and ㄱ, each sounds *k*, *a*, and *k* respectively.** 852 | 853 | * 앞서 말했듯, 한국어의 writing system은 자연발생된 것이 아닌 단체 (거진 개인)에 의해 창조되었다는 점에서 매우 특별하며, 어떤 음절에 해당하는 morpho-syllabic block을 decompose하여 그 음절을 이루는 소리 성분들에 해당하는 sub-character, 즉 자모를 알아낼 수 있다는 점에서도 독특합니다. 854 | 855 | * 좀 더 자세히 얘기하자면 한글은 CV(C)의 conjunct form으로 되어 있으며, 이는 한 음절을 구성하기 위해 최소한 자모 한 개씩은 있어야 함을 의미합니다. 이는 초성과 중성이라고 불리며, CV를 지칭하죠. 종성, 즉 third sound (C)의 경우는, 있어도 되고 없어도 상관없습니다. 초성으로 가능한 자음은 19개, 중성으로 가능한 모음은 21개이며, 종성으로 가능한 자음은 composite consonants를 포함하여 27개입니다 (empty일 경우 포함하면 28개). 856 | 857 | --- 858 | 859 | **For a clearness, let's denote the morpho-syllabic blocks as *characters* and the consonants/vowels as *Jamo*. The description above yields total 11,172 possible characters. However, one-hot encoding those combinations into 11,172-dim vector seems very redundant at a glance. Therefore, there have been various character encoding schemes suggested. The schemes include two approaches; (1) decomposing the blocks into sub-characters and (2) preserving them.** 860 | 861 | * 보다 논의를 명확히 하기 위해, 음절들을 character라고 하고 자모를 Jamo라고 둡시다. 위의 설명에 따르면 우리는 총 11,172개의 가능한 자모 조합을 얻는데, 이를 one-hot encoding하는 것은 매우 costly해 보이기도 하거니와 그에 따른 merit을 딱히 찾기도 어렵습니다. 따라서 이 문제에 대하여 한국어 character을 encoding하는 다양한 방법들이 소개되었는데, 이 방법들은 크게 1) 자소 분리 2) 음절 유지의 두 가지로 나뉠 수 있습니다. 862 | 863 | --- 864 | 865 | **For the first approach, the most simple way is to spread a character into three alphabet sequence. There are some toolkits (e.g., [hgtk](https://github.com/bluedisk/hangul-toolkit)) that decompose Hangul characters into alphabets; by utilizing them, we can obtain the tuple ('ㄱ', 'ㅏ', 'ㄱ') from a single character '각'. This blurs the property of full characters but allows a sparse representation with low dimensional vectors (of size 67). However note that the total length of numericalization increases since the sole alphabets ('ㄱ' and 'ㅏ') and the characters ('각') are assigned equally two bytes. Due to this, degrade in computation efficiency is inavoidable. To deal with this, [romanization has been suggested and showed a good performance](https://arxiv.org/abs/1708.02657), but here we don't utilize it since the processing may induce an ambiguity. ** 866 | 867 | **In this tutorial, we present two conventional methodologies in one-hot encoding, namely [Shin](https://www.dbpia.co.kr/Journal/ArticleDetail/NODE07207314#) and [Cho](http://www.dbpia.co.kr/Journal/ArticleDetail/NODE07503227). The former is a simple 67-dim version, and the latter considers the cases where the alphabets are used solely (e.g., 'ㅠㅠ', 'ㅋㅋ'), not in a form of block. Each corresponds with *shin_onehot* and *cho_onehot* in the code below. A description on *char2onehot* will be provided afterwards.** 868 | 869 | ```python 870 | import hgtk 871 | from hgtk.letter import decompose as decom 872 | 873 | choseng = ['ㄱ','ㄴ','ㄷ','ㄹ','ㅁ','ㅂ','ㅅ','ㅇ','ㅈ','ㅊ','ㅋ','ㅌ','ㅍ','ㅎ','ㄲ','ㄸ','ㅃ','ㅆ','ㅉ'] 874 | cwungseng = ['ㅏ','ㅑ','ㅓ','ㅕ','ㅗ','ㅛ','ㅜ','ㅠ','ㅡ','ㅣ','ㅐ','ㅒ','ㅔ','ㅖ','ㅘ','ㅙ','ㅚ','ㅝ','ㅞ','ㅟ','ㅢ'] 875 | congseng = ['ㄱ','ㄴ','ㄷ','ㄹ','ㅁ','ㅂ','ㅅ','ㅇ','ㅈ','ㅊ','ㅋ','ㅌ','ㅍ','ㅎ','ㄲ','ㅆ','ㄳ','ㄵ','ㄶ','ㄺ','ㄻ','ㄼ','ㄽ','ㄾ','ㄿ','ㅀ','ㅄ 876 | '] 877 | alp = choseng+cwungseng+congseng 878 | uniquealp = list(set(choseng+cwungseng+congseng)) 879 | 880 | def cho2onehot(s): 881 | res = np.zeros(len(choseng)) 882 | if s in choseng: 883 | res[choseng.index(s)]=1 884 | return res 885 | 886 | def cwu2onehot(s): 887 | res = np.zeros(len(cwungseng)) 888 | if s in cwungseng: 889 | res[cwungseng.index(s)]=1 890 | return res 891 | 892 | def con2onehot(s): 893 | res = np.zeros(len(congseng)) 894 | if s in congseng: 895 | res[congseng.index(s)]=1 896 | return res 897 | 898 | def uni2onehot(s): 899 | res = np.zeros(len(uniquealp)) 900 | if s in uniquealp: 901 | res[uniquealp.index(s)]=1 902 | return res 903 | 904 | def shin_onehot(s): 905 | z = decom(s) 906 | res = np.zeros((len(alp),3)) 907 | res[:len(choseng),0] = cho2onehot(z[0]) 908 | res[len(choseng):len(choseng)+len(cwungseng),1] = cwu2onehot(z[1]) 909 | res[len(choseng)+len(cwungseng):len(alp),2] = con2onehot(z[2]) 910 | return res 911 | 912 | def cho_onehot(s): 913 | z = decom(s) 914 | res = np.zeros((len(alp)+len(uniquealp),3)) 915 | if len(z[0]+z[1]+z[2]) > 1: 916 | res[:len(alp),:] = shin_onehot(s) 917 | elif len(z[0])>0: 918 | res[len(alp):,0] = uni2onehot(s) 919 | elif len(z[1])>0: 920 | res[len(alp):,1] = uni2onehot(s) 921 | else: 922 | res[len(alp):,2] = uni2onehot(s) 923 | return res 924 | 925 | def char2onehot(s): 926 | z = decom(s) 927 | res = np.concatenate([cho2onehot(z[0]),cwu2onehot(z[1]),con2onehot(z[2])]) 928 | return res 929 | ``` 930 | 931 | * 자소 분리의 방법에 있어 가장 먼저 생각해볼 수 있는 것은 한 글자 (character)을 세 개의 자모 sequence (e.g., '각' > 'ㄱ', 'ㅏ', 'ㄱ')로 나타내는 것입니다. 이를 손쉽게 수행할 수 있는 [hgtk](https://github.com/bluedisk/hangul-toolkit)와 같은 툴킷들도 제공되고 있구요. 이러한 변환이 full character (syllabic block)의 property를 뚜렷하게 하지 못할 수 있으나, low dimension(67?)의 sparse representation을 가능하게 한다는 점에서 유용할 수 있습니다. 다만 이 과정에서 정보량이 세 배로 증가해서 computation efficiency를 떨어뜨릴 수 있다는 점은 단점이 될 수 있겠네요. 이를 해결할 수 있는 방법으로 romanized Hangul을 생각해볼 수 있겠지만, 직관적으로 어색하고 중의성을 유발할 수 있기에 여기서는 따로 다루지 않겠습니다. 932 | 933 | * 본 튜토리얼에서 다루는 one-hot encoding방법은 [Shin](https://www.dbpia.co.kr/Journal/ArticleDetail/NODE07207314#) 과 [Cho](http://www.dbpia.co.kr/Journal/ArticleDetail/NODE07503227)의 두 가지입니다. 전자는 앞서 말한 67-dim의 단순 임베딩이며, 후자는 자모가 단독으로 사용되는 경우들을 고려하여 unique하게 사용되는 것을 지칭하는 별도의 벡터를 augment해 줍니다. 물론 해당 논문들에서는 특수 기호들에 대한 placeholder들도 고려하지만, 3i4k데이터셋에는 punctuation이 지워져 있기 때문에 각각 67/118 dim을 쓰는 것으로 충분하다고 보도록 하겠습니다. *char2onehot*에 대한 설명은 다음 단락에서 이어가도록 하겠습니다. 934 | 935 | --- 936 | 937 | For the second type of approachs, namely block-preserving ones, a significant advantage is that we can treat the characters as subword, which can be detered in the previous approaches. Theoretically we can utilize 11,172 characters as a syllable, but in real life it is sufficient with only around 2,500 ones. This allows us to utilize the one-hot encoding of the syllables. A simple integer-indexed dictionary that contains 2,534 syllables from [100-dimension fastText vector dictionary which was trained with 2M drama scripts](https://drive.google.com/open?id=1jHbjOcnaLourFzNuP47yGQVhBTq6Wgor) is uploaded here. However, another practical approach that can be adopted is using the dense character vectors from the fastText dictionary we've embedded. It compresses a sparse 2,534-dim vector into a dense 100-dim vector, possibly reflecting the distributive semantics of syllables as subwords of morphemes. 938 | 939 | Besides, another sparse embedding scheme that aggregates all sounds in a block into a form of multi-hot encoding, was suggested recently, and we call it [Song](https://www.researchgate.net/publication/331987503_Sequence-to-Sequence_Autoencoder_based_Korean_Text_Error_Correction_using_Syllable-level_Multi-hot_Vector_Representation/stats). The code below imports the dictionary for sparse syllable encoding, and the multi-hot encoded vectors can be obtained by *char2onehot* defined above. 940 | 941 | ```python 942 | kor_char = np.load('kor_char.npy').item() 943 | ``` 944 | 945 | * 그 다음으로 자소 분리를 하지 않고 음절을 임베딩하는 것을 생각해볼 수 있는데요, 가장 큰 장점은 일종의 subword로써의 음절의 성질을 유지할 수 있다는 점입니다. 물론 형태소가 '-ㄴ', '-ㄹ'처럼 decompose되어 나타나는 경우도 있지만, 앞서 말했듯 우리는 non-invasive한 형태소 분석기를 쓸 거니까요. 위에 언급했듯 가능한 음절 조합은 11,172개이지만 실생활에서 사용되는 가짓수는 대략 2,500개 정도이며 이는 NN classification들에서 사용했던 fastText dictionary에 존재하는 음절의 갯수가 2,534개임을 고려해 볼 때 어느 정도 타당한 주장임을 알 수 있습니다. 그래서 가장 먼저 생각해볼 수 있는 one-hot encoding 방법은 해당 dictionary에 있는 모든 음절에 번호를 매겨 사용하는 것이죠. 물론 2,534도 작은 수는 아닙니다만, 모두 사용되는지조차 알 수 없는 11,172개의 음절을 모두 가정하는 것보단 훨씬 효율적이리라 예측할 수 있지요. 해당 음절 모음은 위의 code를 통해 import할 수 있습니다. 946 | 947 | * 또다른 음절 임베딩 방법으로는, 언급했던 fastText dictionary에 있는 length 1의 단어들 (음절들)의 dense embedding만 활용하는 것을 생각해볼 수 있겠습니다. 이는 2,534의 크기를 100으로 줄이면서, 음절들 간의 distributional semantics도 고려해줄 수 있기에 상당한 성능 개선이 있을 것으로 예상해볼 수 있는 방법입니다. 실제로 제가 [딥러닝 기반 띄어쓰기](https://github.com/warnikchow/ttuyssubot)에서 활용하여 어느 정도의 효과를 보았지요. 948 | 949 | * 그리고 또 하나의 임베딩 방법이 최근 제시되었는데, 바로 하나의 character의 초/중/종성을 multi-hot vector로 만들어 주는 방법입니다. 저는 그런 생각을 한 게 제가 처음인 줄 알고 일단 특허 쓰고 논문을 작성 중이었는데, 구글 스칼라에서는 검색되지 않지만 이미 2018년 HCLT에서 [같은 방법](https://www.researchgate.net/publication/331987503_Sequence-to-Sequence_Autoencoder_based_Korean_Text_Error_Correction_using_Syllable-level_Multi-hot_Vector_Representation/stats)이 제시가 되었더라구요 ㅎㅎ역시 이 세계는 생각나는 대로 바로바로 하지 않으면 누가 하는 곳... 어쨌든 그래서 그 방식도 함께 여기서 비교해 보도록 하겠습니다. 해당 위에 있는 *char2onehot*을 통해 정의됩니다. 950 | 951 | --- 952 | 953 | **The aforementioned methodologies are implemented in the following code as a featurization for RNN. Since the number of morphemes are generally smaller than the number of 솓 characters, we fixed the max character length to 80. Consequently, the sequence length for the *Jamo*-level embeddings (Shin & Cho) reaches 240. Again, to reflect the head-finality of Korean, the Jamos/characters are padded from the sentence-final.** 954 | 955 | ```python 956 | def featurize_rnnchar(corpus,wdim,chardict,maxlen): 957 | rnn_shin = np.zeros((len(corpus),maxlen*3,len(alp))) 958 | rnn_cho = np.zeros((len(corpus),maxlen*3,len(alp)+len(uniquealp))) 959 | rnn_char = np.zeros((len(corpus),maxlen,len(alp))) 960 | rnn_onehot= np.zeros((len(corpus),maxlen,len(chardict))) 961 | rnn_total = np.zeros((len(corpus),maxlen,wdim)) 962 | for i in range(len(corpus)): 963 | if i%1000 ==0: 964 | print(i) 965 | s = corpus[i] 966 | for j in range(len(s)): 967 | if j < maxlen and hgtk.checker.is_hangul(s[-j-1])==True: 968 | if j>0: 969 | rnn_shin[i][-3*j-3:-3*j,:] = np.transpose(shin_onehot(s[-j-1])) 970 | rnn_cho[i][-3*j-3:-3*j,:] = np.transpose(cho_onehot(s[-j-1])) 971 | else: 972 | rnn_shin[i][-3*j-3:,:] = np.transpose(shin_onehot(s[-j-1])) 973 | rnn_cho[i][-3*j-3:,:] = np.transpose(cho_onehot(s[-j-1])) 974 | rnn_char[i][-j-1,:] = char2onehot(s[-j-1]) 975 | if s[-j-1] in model_ft: 976 | rnn_total[i][-j-1,:] = model_ft[s[-j-1]] 977 | if s[-j-1] in chardict: 978 | rnn_onehot[i][-j-1,chardict[s[-j-1]]]=1 979 | return rnn_shin, rnn_cho, rnn_char, rnn_onehot, rnn_total 980 | 981 | fci_rec_shin, fci_rec_cho, fci_rec_char, fci_rec_onehot, fci_rec = featurize_rnnchar(fci_data,100,kor_char,80) 982 | ``` 983 | 984 | * 앞서 얘기한 character embedding 방법론들이 RNN featurization의 형태로 정리된 코드입니다. 각각 Shin, Cho, Song, one-hot encoded char embedding, 그리고 dense char embedding 을 output으로 합니다. 보통 형태소의 개수보다 음절 개수가 훨씬 많기 때문에 길이는 80으로 하였고, 이 과정에서 space도 하나의 character로 카운트됩니다. 자소분리 임베딩의 경우 3배의 길이를 가지는 것으로 간주하여 size 240을 최대로 하였습니다. 그리고 앞에서 그랬듯, 한국어의 head-finality를 고려하여 문장 뒷쪽에서부터 padding하였습니다. 985 | 986 | --- 987 | 988 | The below is the evaluation phase utilizing the case of dense character embedding, which is the best case among the five feature engineerings above. Since the feature size is much bigger than the morpheme-based models, training epoch was extended to 50 for a fair comparison. The convergence was significantly slow compared with the morpheme-based cases, but we've obtained compatible accuracy (0.8802 > 0.8823) and F1 score (0.7906 > 0.7934) with respect to the performance with the morpheme-bilstm model. We assume that this result originates in the property of the syllables in Korean as subwords and the nature of the intention understanding task as a syntax-semantic task. 989 | 990 | ```python 991 | validate_bilstm(fci_rec,fci_label,32,128,class_weights_fci,0.1,16,'model/tutorial/rec_char_dense') 992 | ``` 993 | 994 | ```properties 995 | # CONSOLE RESULT 996 | >>> validate_bilstm(fci_rec,fci_label,32,128,class_weights_fci,0.1,16,'model/tutorial/rec_char_dense') 997 | _________________________________________________________________ 998 | Layer (type) Output Shape Param # 999 | ================================================================= 1000 | bidirectional_12 (Bidirectio (None, 64) 34048 1001 | _________________________________________________________________ 1002 | dense_43 (Dense) (None, 128) 8320 1003 | _________________________________________________________________ 1004 | dense_44 (Dense) (None, 7) 903 1005 | ================================================================= 1006 | Total params: 43,271 1007 | Trainable params: 43,271 1008 | Non-trainable params: 0 1009 | _________________________________________________________________ 1010 | Train on 55129 samples, validate on 6126 samples 1011 | Epoch 1/50 1012 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.6033 - acc: 0.7970— val_f1: 0.611853 — val_precision: 0.759945 — val_reca 1013 | — val_f1_w: 0.805877 — val_precision_w: 0.822164 — val_recall_w: 0.821907 1014 | 55129/55129 [==============================] - 169s 3ms/step - loss: 0.6034 - acc: 0.7970 - val_loss: 0.5275 - val_acc: 0.8219 1015 | Epoch 2/50 1016 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.4744 - acc: 0.8399— val_f1: 0.694396 — val_precision: 0.770346 — val_reca 1017 | — val_f1_w: 0.834075 — val_precision_w: 0.840706 — val_recall_w: 0.840679 1018 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.4743 - acc: 0.8399 - val_loss: 0.4752 - val_acc: 0.8407 1019 | Epoch 3/50 1020 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.4298 - acc: 0.8538— val_f1: 0.735305 — val_precision: 0.779485 — val_reca 1021 | — val_f1_w: 0.851305 — val_precision_w: 0.851509 — val_recall_w: 0.855534 1022 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.4298 - acc: 0.8538 - val_loss: 0.4277 - val_acc: 0.8555 1023 | Epoch 4/50 1024 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.4013 - acc: 0.8632— val_f1: 0.743459 — val_precision: 0.793688 — val_reca 1025 | — val_f1_w: 0.850187 — val_precision_w: 0.859941 — val_recall_w: 0.854228 1026 | 55129/55129 [==============================] - 164s 3ms/step - loss: 0.4012 - acc: 0.8632 - val_loss: 0.4334 - val_acc: 0.8542 1027 | Epoch 5/50 1028 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.3808 - acc: 0.8699— val_f1: 0.759329 — val_precision: 0.791125 — val_reca 1029 | — val_f1_w: 0.862013 — val_precision_w: 0.863887 — val_recall_w: 0.864838 1030 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.3809 - acc: 0.8699 - val_loss: 0.4008 - val_acc: 0.8648 1031 | Epoch 6/50 1032 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.3639 - acc: 0.8744— val_f1: 0.772891 — val_precision: 0.818381 — val_reca 1033 | — val_f1_w: 0.867532 — val_precision_w: 0.868421 — val_recall_w: 0.870715 1034 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.3638 - acc: 0.8744 - val_loss: 0.3888 - val_acc: 0.8707 1035 | Epoch 7/50 1036 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.3497 - acc: 0.8791— val_f1: 0.769384 — val_precision: 0.800740 — val_reca 1037 | — val_f1_w: 0.865818 — val_precision_w: 0.866474 — val_recall_w: 0.869083 1038 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.3496 - acc: 0.8791 - val_loss: 0.3859 - val_acc: 0.8691 1039 | Epoch 8/50 1040 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.3363 - acc: 0.8844— val_f1: 0.776859 — val_precision: 0.809094 — val_reca 1041 | — val_f1_w: 0.871162 — val_precision_w: 0.870220 — val_recall_w: 0.874306 1042 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.3363 - acc: 0.8844 - val_loss: 0.3775 - val_acc: 0.8743 1043 | Epoch 9/50 1044 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.3238 - acc: 0.8888— val_f1: 0.769087 — val_precision: 0.823962 — val_reca 1045 | — val_f1_w: 0.871311 — val_precision_w: 0.872008 — val_recall_w: 0.874633 1046 | 55129/55129 [==============================] - 164s 3ms/step - loss: 0.3238 - acc: 0.8888 - val_loss: 0.3690 - val_acc: 0.8746 1047 | Epoch 10/50 1048 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.3147 - acc: 0.8912— val_f1: 0.778358 — val_precision: 0.815374 — val_reca 1049 | — val_f1_w: 0.871938 — val_precision_w: 0.872361 — val_recall_w: 0.873980 1050 | 55129/55129 [==============================] - 164s 3ms/step - loss: 0.3148 - acc: 0.8912 - val_loss: 0.3691 - val_acc: 0.8740 1051 | Epoch 11/50 1052 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.3051 - acc: 0.8944— val_f1: 0.775244 — val_precision: 0.801135 — val_reca 1053 | — val_f1_w: 0.872543 — val_precision_w: 0.872058 — val_recall_w: 0.875449 1054 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.3051 - acc: 0.8944 - val_loss: 0.3665 - val_acc: 0.8754 1055 | Epoch 12/50 1056 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.2970 - acc: 0.8981— val_f1: 0.781538 — val_precision: 0.809303 — val_reca 1057 | — val_f1_w: 0.875536 — val_precision_w: 0.875564 — val_recall_w: 0.878387 1058 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.2970 - acc: 0.8981 - val_loss: 0.3629 - val_acc: 0.8784 1059 | Epoch 13/50 1060 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.2891 - acc: 0.9001— val_f1: 0.781662 — val_precision: 0.795075 — val_reca 1061 | — val_f1_w: 0.872933 — val_precision_w: 0.873100 — val_recall_w: 0.873817 1062 | 55129/55129 [==============================] - 164s 3ms/step - loss: 0.2891 - acc: 0.9001 - val_loss: 0.3684 - val_acc: 0.8738 1063 | Epoch 14/50 1064 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.2811 - acc: 0.9032— val_f1: 0.778117 — val_precision: 0.799786 — val_reca 1065 | — val_f1_w: 0.873586 — val_precision_w: 0.874717 — val_recall_w: 0.875775 1066 | 55129/55129 [==============================] - 164s 3ms/step - loss: 0.2810 - acc: 0.9032 - val_loss: 0.3671 - val_acc: 0.8758 1067 | Epoch 15/50 1068 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.2744 - acc: 0.9035— val_f1: 0.784468 — val_precision: 0.810065 — val_reca 1069 | — val_f1_w: 0.874109 — val_precision_w: 0.873767 — val_recall_w: 0.876265 1070 | 55129/55129 [==============================] - 164s 3ms/step - loss: 0.2744 - acc: 0.9036 - val_loss: 0.3618 - val_acc: 0.8763 1071 | Epoch 16/50 1072 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.2686 - acc: 0.9063— val_f1: 0.780115 — val_precision: 0.807169 — val_reca 1073 | — val_f1_w: 0.874687 — val_precision_w: 0.874115 — val_recall_w: 0.876755 1074 | 55129/55129 [==============================] - 164s 3ms/step - loss: 0.2687 - acc: 0.9063 - val_loss: 0.3641 - val_acc: 0.8768 1075 | Epoch 17/50 1076 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.2615 - acc: 0.9094— val_f1: 0.782015 — val_precision: 0.796383 — val_reca 1077 | — val_f1_w: 0.874941 — val_precision_w: 0.876734 — val_recall_w: 0.875939 1078 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.2616 - acc: 0.9094 - val_loss: 0.3640 - val_acc: 0.8759 1079 | Epoch 18/50 1080 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.2548 - acc: 0.9099— val_f1: 0.790453 — val_precision: 0.812157 — val_reca 1081 | — val_f1_w: 0.880186 — val_precision_w: 0.880995 — val_recall_w: 0.881815 1082 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.2548 - acc: 0.9098 - val_loss: 0.3696 - val_acc: 0.8818 1083 | Epoch 19/50 1084 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.2484 - acc: 0.9136— val_f1: 0.787465 — val_precision: 0.791837 — val_reca 1085 | — val_f1_w: 0.877686 — val_precision_w: 0.880073 — val_recall_w: 0.877245 1086 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.2484 - acc: 0.9136 - val_loss: 0.3612 - val_acc: 0.8772 1087 | Epoch 20/50 1088 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.2432 - acc: 0.9150— val_f1: 0.764492 — val_precision: 0.800527 — val_reca 1089 | — val_f1_w: 0.863816 — val_precision_w: 0.865599 — val_recall_w: 0.867124 1090 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.2431 - acc: 0.9150 - val_loss: 0.4086 - val_acc: 0.8671 1091 | Epoch 21/50 1092 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.2364 - acc: 0.9179— val_f1: 0.786431 — val_precision: 0.825723 — val_reca 1093 | — val_f1_w: 0.877461 — val_precision_w: 0.877627 — val_recall_w: 0.880346 1094 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.2364 - acc: 0.9179 - val_loss: 0.3791 - val_acc: 0.8803 1095 | Epoch 22/50 1096 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.2314 - acc: 0.9198— val_f1: 0.784653 — val_precision: 0.813028 — val_reca 1097 | — val_f1_w: 0.879205 — val_precision_w: 0.880109 — val_recall_w: 0.880999 1098 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.2314 - acc: 0.9198 - val_loss: 0.3741 - val_acc: 0.8810 1099 | Epoch 23/50 1100 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.2251 - acc: 0.9218— val_f1: 0.780445 — val_precision: 0.804015 — val_reca 1101 | — val_f1_w: 0.875199 — val_precision_w: 0.874711 — val_recall_w: 0.877897 1102 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.2251 - acc: 0.9218 - val_loss: 0.3859 - val_acc: 0.8779 1103 | Epoch 24/50 1104 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.2195 - acc: 0.9240— val_f1: 0.784534 — val_precision: 0.789635 — val_reca 1105 | — val_f1_w: 0.876094 — val_precision_w: 0.875345 — val_recall_w: 0.877081 1106 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.2195 - acc: 0.9240 - val_loss: 0.3862 - val_acc: 0.8771 1107 | Epoch 25/50 1108 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.2145 - acc: 0.9252— val_f1: 0.789590 — val_precision: 0.800697 — val_reca 1109 | — val_f1_w: 0.879318 — val_precision_w: 0.878898 — val_recall_w: 0.880346 1110 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.2145 - acc: 0.9252 - val_loss: 0.3892 - val_acc: 0.8803 1111 | Epoch 26/50 1112 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.2087 - acc: 0.9281— val_f1: 0.786858 — val_precision: 0.804829 — val_reca 1113 | — val_f1_w: 0.878004 — val_precision_w: 0.877461 — val_recall_w: 0.879693 1114 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.2087 - acc: 0.9281 - val_loss: 0.3853 - val_acc: 0.8797 1115 | Epoch 27/50 1116 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.2042 - acc: 0.9290— val_f1: 0.779832 — val_precision: 0.808263 — val_reca 1117 | — val_f1_w: 0.873661 — val_precision_w: 0.872953 — val_recall_w: 0.876755 1118 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.2042 - acc: 0.9290 - val_loss: 0.4095 - val_acc: 0.8768 1119 | Epoch 28/50 1120 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.1993 - acc: 0.9306— val_f1: 0.792366 — val_precision: 0.799918 — val_reca 1121 | — val_f1_w: 0.881174 — val_precision_w: 0.881553 — val_recall_w: 0.881489 1122 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.1994 - acc: 0.9306 - val_loss: 0.3898 - val_acc: 0.8815 1123 | Epoch 29/50 1124 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.1939 - acc: 0.9330— val_f1: 0.787657 — val_precision: 0.799441 — val_reca 1125 | — val_f1_w: 0.878101 — val_precision_w: 0.877934 — val_recall_w: 0.879367 1126 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.1939 - acc: 0.9330 - val_loss: 0.4101 - val_acc: 0.8794 1127 | Epoch 30/50 1128 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.1895 - acc: 0.9333— val_f1: 0.767905 — val_precision: 0.779847 — val_reca 1129 | — val_f1_w: 0.863264 — val_precision_w: 0.863594 — val_recall_w: 0.865491 1130 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.1896 - acc: 0.9332 - val_loss: 0.4535 - val_acc: 0.8655 1131 | Epoch 31/50 1132 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.1856 - acc: 0.9350— val_f1: 0.787478 — val_precision: 0.806525 — val_reca 1133 | — val_f1_w: 0.875649 — val_precision_w: 0.874640 — val_recall_w: 0.877571 1134 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.1856 - acc: 0.9350 - val_loss: 0.4093 - val_acc: 0.8776 1135 | Epoch 32/50 1136 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.1798 - acc: 0.9380— val_f1: 0.796609 — val_precision: 0.802828 — val_reca 1137 | — val_f1_w: 0.877572 — val_precision_w: 0.877537 — val_recall_w: 0.878224 1138 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.1799 - acc: 0.9380 - val_loss: 0.4327 - val_acc: 0.8782 1139 | Epoch 33/50 1140 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.1760 - acc: 0.9389— val_f1: 0.794457 — val_precision: 0.807102 — val_reca 1141 | — val_f1_w: 0.878977 — val_precision_w: 0.879220 — val_recall_w: 0.879693 1142 | 55129/55129 [==============================] - 164s 3ms/step - loss: 0.1760 - acc: 0.9389 - val_loss: 0.4271 - val_acc: 0.8797 1143 | Epoch 34/50 1144 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.1714 - acc: 0.9403— val_f1: 0.794570 — val_precision: 0.800183 — val_reca 1145 | — val_f1_w: 0.874815 — val_precision_w: 0.875788 — val_recall_w: 0.874959 1146 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.1713 - acc: 0.9404 - val_loss: 0.4511 - val_acc: 0.8750 1147 | Epoch 35/50 1148 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.1675 - acc: 0.9414— val_f1: 0.793498 — val_precision: 0.804919 — val_reca 1149 | — val_f1_w: 0.881061 — val_precision_w: 0.881226 — val_recall_w: 0.882305 1150 | 55129/55129 [==============================] - 165s 3ms/step - loss: 0.1675 - acc: 0.9414 - val_loss: 0.4274 - val_acc: 0.8823 1151 | ``` 1152 | 1153 | * 이제 evaluation part입니다. 그런데 다섯 개나 되는 모델의 train 결과를 모두 올리는 건 좀 무리수인 것 같네요. 그래서 결론만 말씀드리면, one-hot encoded char < Cho < Shin < Song < dense char 의 순서로 성능이 좋습니다 ㅎㅎ 아무래도 궁금하실 dense char embedding의 트레이닝 phase만을 여기엔 업로드하도록 하겠습니다. 다른 알고리즘들은 validate_bilstm 모듈에 한번씩 돌려보면서 성능을 살펴보시면 좋을 것 같아요! 1154 | 1155 | * 비록 feature size의 차이로 training epoch가 좀 늘어나긴 했지만 이는 convergence를 유도하기 위함이고, converge했다는 가정 하에서 morpheme based model보다 근소하게 더 성능이 좋음을 확인할 수 있었습니다. 이는 좀 의외의 결과였는데요, morpheme-based model과 다르게 여기서는 의미에 관련된 어떤 preprocessing도 거치지 않고 raw data만을 dictionary embedding한 것이기 때문입니다. 여기서, 한국어에서는 각 음절이 어느 정도 subword 역할을 해 주며 그 sequential한 배열로부터 문장의 의도를 파악하면 형태소 단위의 분석보다 때로는 더 정확할 수 있다는 것을 짐작할 수 있습니다. 좋은 성능의 배경에는 intention understanding 이라는 task의 특징이, 그리고 한국어의 agglutinative language로써의 특징이 있겠지만요. 1156 | 1157 | --- 1158 | 1159 | **Advanced from the vanilla models we've utilized so far, we may customize some Keras layers in the following chapter, to boost the performance.** 1160 | 1161 | * 지금까지 우리가 사용한 알고리즘들은 모두 vanilla cnn/bilstm이었고, 별도로 레이어를 쪼개고 합하고 곱하고 하지는 않았습니다. 하지만 back propagation을 이용한 트레이닝들의 장점은 연산의 커스터마이징이 가능하다는 점이며, 케라스에서도 그러한 연산들은 대부분 가능합니다. 다음 꼭지부터는 cnn과 bilstm을 엮어서 모델을 짜는 방법을 한번 알아보도록 하겠습니다. 1162 | 1163 | ## 9. Concatenation of CNN and RNN layers 1164 | 1165 | **Now, we get back to the morpheme-based approaches. Here, for the first time we customize the layers for a new implementation! The procedure is the most simple one, a concatenation of two separate networks. We've done with CNN and RNN(BiLSTM)-based approaches so far, thus, a concatenation of the two systems will be executed. Beyond simply putting *model.xxx*, we need some unseen module at this point, *Model*. This helps make in-out of the customized layers.** 1166 | 1167 | ```python 1168 | from keras.models import Model 1169 | import keras.layers as layers 1170 | from keras.layers import Input 1171 | from keras.layers.core import Dense, Dropout 1172 | ``` 1173 | 1174 | * 이제 다시 형태소 기반의 접근으로 돌아가 보도록 하겠습니다. 이제 처음으로, 단순히 model.머시기로 쌓는 게 아닌, 커스터마이즈드 레이어를 만들어보려고 해요! 물론 가장 기본적인, simple concatenation부터 시작해보도록 하겠습니다. 대상은 앞서 다루었던 CNN과 RNN(BiLSTM)입니다. 이를 위해서, 앞과는 다른 모듈들을 import할 필요가 있겠죠. 바로 **Model** 입니다. 커스터마이즈드 레이어의 입출력을 다룰 수 있게 해주죠! 1175 | 1176 | --- 1177 | 1178 | **The following is a simple code for a concatenation of CNN and RNN networks. The input dimensions were filled with the property regarding RNN input. There are some points you should look at: the dense layers after CNN and RNN summarization, the line with *layer.concatenate*, and the model declaration stage *model = Model(inputs = [cnn,rnn], outputs = [main_output])*. Dropouts were added since the model became so bigger than the previous ones. Wait ... there is another unseen function ... in callbacks?** 1179 | 1180 | ```python 1181 | def validate_cnnrnn(conv,rnn,train_y,filters,hidden_lstm,hidden_dim,cw,filename): 1182 | cnn_input = Input(shape=(len(rnn[0]),len(rnn[0][0]),1), dtype='float32') 1183 | cnn_layer = layers.Conv2D(filters,(3,len(rnn[0][0])),activation='relu')(cnn_input) 1184 | cnn_layer = layers.MaxPooling2D((2,1))(cnn_layer) 1185 | cnn_layer = layers.Conv2D(filters,(3,1),activation='relu')(cnn_layer) 1186 | cnn_layer = layers.Flatten()(cnn_layer) 1187 | cnn_output= Dense(hidden_dim, activation='relu')(cnn_layer) 1188 | rnn_input = Input(shape=(len(rnn[0]),len(rnn[0][0])), dtype='float32') 1189 | rnn_layer = Bidirectional(LSTM(hidden_lstm))(rnn_input) 1190 | rnn_output= Dense(hidden_dim, activation='relu')(rnn_layer) 1191 | output = layers.concatenate([cnn_output,rnn_output]) 1192 | output = Dense(hidden_dim, activation='relu')(output) 1193 | output = Dropout(0.3)(output) 1194 | output = Dense(hidden_dim, activation='relu')(output) 1195 | output = Dropout(0.3)(output) 1196 | main_output = Dense(7,activation='softmax')(output) 1197 | model = Sequential() 1198 | model = Model(inputs=[cnn_input,rnn_input], outputs=[main_output]) 1199 | model.compile(optimizer=adam_half, loss="sparse_categorical_crossentropy", metrics=["accuracy"]) 1200 | filepath=filename+"-{epoch:02d}-{val_acc:.4f}.hdf5" 1201 | checkpoint = ModelCheckpoint(filepath, monitor='val_acc', verbose=1, mode='max') 1202 | callbacks_list = [metricsf1macro_2input,checkpoint] 1203 | model.summary() 1204 | model.fit([conv,rnn],train_y,validation_split=0.1,epochs=30,batch_size=16,callbacks=callbacks_list,class_weight=cw) 1205 | ``` 1206 | 1207 | 상기 코드는 간단하게 CNN과 RNN 구조를 concatenate해 줍니다! 앞에서부터 쭉 보면 CNN_input과 CNN_output을 정의하고, RNN_input과 RNN_output을 정의하고, 두 레이어를 layer.concatenate를 이용해 합친 뒤, 앞서 했던 output에 대한 방법론과 비슷하게 진행하는 것을 파악할 수 있지요. 여기서 중요하게 볼 point들은 1. CNN과 RNN을 Dense layer로 summarize하여 concatenate하기 쉽게 만드는 부분, 2. *layer.concatenate*로 concatenate하는 부분, 3. *Model* 펑션으로 inputs와 outputs를 이은 하나의 큰 덩어리를 만드는 부분입니다. Dropout은 그냥 모델이 너무 커져서 넣어 보았습니다 ㅎㅎ 어... 그런데 callback부분에 못 보던 내용이 있군요? 1208 | 1209 | --- 1210 | 1211 | **As in Keras, we need to define another callback function for F1 score if we adopt more than one input. Actually, a different format of function is required for every number of more-than-one-input! Very annoying, but anyway, we've adopted two inputs, thus let's make up another function for the evaluation.** 1212 | 1213 | ```python 1214 | class Metricsf1macro_2input(Callback): 1215 | def on_train_begin(self, logs={}): 1216 | self.val_f1s = [] 1217 | self.val_recalls = [] 1218 | self.val_precisions = [] 1219 | self.val_f1s_w = [] 1220 | self.val_recalls_w = [] 1221 | self.val_precisions_w = [] 1222 | def on_epoch_end(self, epoch, logs={}): 1223 | if len(self.validation_data)>2: 1224 | val_predict = np.asarray(self.model.predict([self.validation_data[0],self.validation_data[1]])) 1225 | val_predict = np.argmax(val_predict,axis=1) 1226 | val_targ = self.validation_data[2] 1227 | else: 1228 | val_predict = np.asarray(self.model.predict(self.validation_data[0])) 1229 | val_predict = np.argmax(val_predict,axis=1) 1230 | val_targ = self.validation_data[1] 1231 | _val_f1 = metrics.f1_score(val_targ, val_predict, average="macro") 1232 | _val_f1_w = metrics.f1_score(val_targ, val_predict, average="weighted") 1233 | _val_recall = metrics.recall_score(val_targ, val_predict, average="macro") 1234 | _val_recall_w = metrics.recall_score(val_targ, val_predict, average="weighted") 1235 | _val_precision = metrics.precision_score(val_targ, val_predict, average="macro") 1236 | _val_precision_w = metrics.precision_score(val_targ, val_predict, average="weighted") 1237 | self.val_f1s.append(_val_f1) 1238 | self.val_recalls.append(_val_recall) 1239 | self.val_precisions.append(_val_precision) 1240 | self.val_f1s_w.append(_val_f1_w) 1241 | self.val_recalls_w.append(_val_recall_w) 1242 | self.val_precisions_w.append(_val_precision_w) 1243 | print("— val_f1: %f — val_precision: %f — val_recall: %f"%(_val_f1, _val_precision, _val_recall)) 1244 | print("— val_f1_w: %f — val_precision_w: %f — val_recall_w: %f"%(_val_f1_w, _val_precision_w, _val_recall_w)) 1245 | 1246 | metricsf1macro_2input = Metricsf1macro_2input() 1247 | ``` 1248 | 1249 | 매-우 귀찮지만, 케라스에서 하나 이상의 input을 가진 모델에 대해 F1 score을 얻고 싶다면, 앞서 정의한 것과 다른, 별도의 function을 만들어야 합니다... 왜 이랬을까요? 아마 그쪽에서도 귀찮아서가 아닐까 싶습니다. 어쨌든 evaluation을 해야 하니, 정의를 해 보도록 하겠습니다. 1250 | 1251 | --- 1252 | 1253 | Now, let's validate with the morpheme-based feaftures! 1254 | 1255 | ```python 1256 | validate_cnnrnn(fci_conv,fci_rec,fci_label,32,32,128,class_weights_fci,'model/modelfci/charcnn+bilstm') 1257 | ``` 1258 | 1259 | **We can see that the performance has degenerated even compared with the vanilla BiLSTM approach. Well, not always the bigger model results in the better performance. We infer that this originates in the distortion that comes from the difference in the way that CNN and RNN solve the problem. Next, we try another module, *attention network*, by introducing the backend of Keras.** 1260 | 1261 | ```properties 1262 | # CONSOLE RESULT 1263 | __________________________________________________________________________________________________ 1264 | Layer (type) Output Shape Param # Connected to 1265 | ================================================================================================== 1266 | input_7 (InputLayer) (None, 30, 100, 1) 0 1267 | __________________________________________________________________________________________________ 1268 | conv2d_7 (Conv2D) (None, 28, 1, 32) 9632 input_7[0][0] 1269 | __________________________________________________________________________________________________ 1270 | max_pooling2d_4 (MaxPooling2D) (None, 14, 1, 32) 0 conv2d_7[0][0] 1271 | __________________________________________________________________________________________________ 1272 | conv2d_8 (Conv2D) (None, 12, 1, 32) 3104 max_pooling2d_4[0][0] 1273 | __________________________________________________________________________________________________ 1274 | input_8 (InputLayer) (None, 30, 100) 0 1275 | __________________________________________________________________________________________________ 1276 | flatten_4 (Flatten) (None, 384) 0 conv2d_8[0][0] 1277 | __________________________________________________________________________________________________ 1278 | bidirectional_3 (Bidirectional) (None, 64) 34048 input_8[0][0] 1279 | __________________________________________________________________________________________________ 1280 | dense_11 (Dense) (None, 128) 49280 flatten_4[0][0] 1281 | __________________________________________________________________________________________________ 1282 | dense_12 (Dense) (None, 128) 8320 bidirectional_3[0][0] 1283 | __________________________________________________________________________________________________ 1284 | concatenate_3 (Concatenate) (None, 256) 0 dense_11[0][0] 1285 | dense_12[0][0] 1286 | __________________________________________________________________________________________________ 1287 | dense_13 (Dense) (None, 128) 32896 concatenate_3[0][0] 1288 | __________________________________________________________________________________________________ 1289 | dropout_5 (Dropout) (None, 128) 0 dense_13[0][0] 1290 | __________________________________________________________________________________________________ 1291 | dense_14 (Dense) (None, 128) 16512 dropout_5[0][0] 1292 | __________________________________________________________________________________________________ 1293 | dropout_6 (Dropout) (None, 128) 0 dense_14[0][0] 1294 | __________________________________________________________________________________________________ 1295 | dense_15 (Dense) (None, 7) 903 dropout_6[0][0] 1296 | ================================================================================================== 1297 | Total params: 154,695 1298 | Trainable params: 154,695 1299 | Non-trainable params: 0 1300 | __________________________________________________________________________________________________ 1301 | Train on 55129 samples, validate on 6126 samples 1302 | Epoch 1/30 1303 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.6263 - acc: 0.7916— val_f1: 0.730286 — val_precision: 0.768703 — val_recall: 0.705825 1304 | — val_f1_w: 0.843637 — val_precision_w: 0.842989 — val_recall_w: 0.847698 1305 | 55129/55129 [==============================] - 90s 2ms/step - loss: 0.6261 - acc: 0.7916 - val_loss: 0.4664 - val_acc: 0.8477 1306 | Epoch 2/30 1307 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.4405 - acc: 0.8548— val_f1: 0.758616 — val_precision: 0.786346 — val_recall: 0.740990 1308 | — val_f1_w: 0.856354 — val_precision_w: 0.860634 — val_recall_w: 0.857982 1309 | 55129/55129 [==============================] - 89s 2ms/step - loss: 0.4405 - acc: 0.8548 - val_loss: 0.4282 - val_acc: 0.8580 1310 | Epoch 3/30 1311 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.3859 - acc: 0.8731— val_f1: 0.759942 — val_precision: 0.804874 — val_recall: 0.729082 1312 | — val_f1_w: 0.861656 — val_precision_w: 0.863499 — val_recall_w: 0.865002 1313 | 55129/55129 [==============================] - 89s 2ms/step - loss: 0.3859 - acc: 0.8731 - val_loss: 0.3992 - val_acc: 0.8650 1314 | Epoch 4/30 1315 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.3492 - acc: 0.8831— val_f1: 0.781057 — val_precision: 0.795622 — val_recall: 0.769233 1316 | — val_f1_w: 0.868542 — val_precision_w: 0.869711 — val_recall_w: 0.869899 1317 | 55129/55129 [==============================] - 89s 2ms/step - loss: 0.3493 - acc: 0.8831 - val_loss: 0.3879 - val_acc: 0.8699 1318 | Epoch 5/30 1319 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.3177 - acc: 0.8927— val_f1: 0.779071 — val_precision: 0.829605 — val_recall: 0.746311 1320 | — val_f1_w: 0.865831 — val_precision_w: 0.870711 — val_recall_w: 0.868919 1321 | 55129/55129 [==============================] - 90s 2ms/step - loss: 0.3177 - acc: 0.8927 - val_loss: 0.4028 - val_acc: 0.8689 1322 | Epoch 6/30 1323 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.2919 - acc: 0.9011— val_f1: 0.783441 — val_precision: 0.804650 — val_recall: 0.768078 1324 | — val_f1_w: 0.872711 — val_precision_w: 0.873444 — val_recall_w: 0.873980 1325 | 55129/55129 [==============================] - 89s 2ms/step - loss: 0.2919 - acc: 0.9011 - val_loss: 0.3968 - val_acc: 0.8740 1326 | Epoch 7/30 1327 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.2667 - acc: 0.9091— val_f1: 0.784232 — val_precision: 0.819105 — val_recall: 0.761072 1328 | — val_f1_w: 0.870747 — val_precision_w: 0.870663 — val_recall_w: 0.873164 1329 | 55129/55129 [==============================] - 90s 2ms/step - loss: 0.2667 - acc: 0.9091 - val_loss: 0.4189 - val_acc: 0.8732 1330 | Epoch 8/30 1331 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.2447 - acc: 0.9174— val_f1: 0.781662 — val_precision: 0.811667 — val_recall: 0.758755 1332 | — val_f1_w: 0.868505 — val_precision_w: 0.869532 — val_recall_w: 0.871205 1333 | 55129/55129 [==============================] - 89s 2ms/step - loss: 0.2447 - acc: 0.9174 - val_loss: 0.4401 - val_acc: 0.8712 1334 | Epoch 9/30 1335 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.2226 - acc: 0.9235— val_f1: 0.777220 — val_precision: 0.781946 — val_recall: 0.782025 1336 | — val_f1_w: 0.873209 — val_precision_w: 0.874389 — val_recall_w: 0.873980 1337 | 55129/55129 [==============================] - 90s 2ms/step - loss: 0.2227 - acc: 0.9235 - val_loss: 0.4419 - val_acc: 0.8740 1338 | Epoch 10/30 1339 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.2055 - acc: 0.9280— val_f1: 0.776107 — val_precision: 0.795161 — val_recall: 0.763229 1340 | — val_f1_w: 0.867720 — val_precision_w: 0.867430 — val_recall_w: 0.869246 1341 | 55129/55129 [==============================] - 90s 2ms/step - loss: 0.2055 - acc: 0.9280 - val_loss: 0.4681 - val_acc: 0.8692 1342 | Epoch 11/30 1343 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.1893 - acc: 0.9344— val_f1: 0.788884 — val_precision: 0.796599 — val_recall: 0.782946 1344 | — val_f1_w: 0.875588 — val_precision_w: 0.876054 — val_recall_w: 0.875612 1345 | 55129/55129 [==============================] - 89s 2ms/step - loss: 0.1892 - acc: 0.9345 - val_loss: 0.4614 - val_acc: 0.8756 1346 | Epoch 12/30 1347 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.1749 - acc: 0.9389— val_f1: 0.784650 — val_precision: 0.809236 — val_recall: 0.767474 1348 | — val_f1_w: 0.871932 — val_precision_w: 0.871065 — val_recall_w: 0.874306 1349 | 55129/55129 [==============================] - 89s 2ms/step - loss: 0.1750 - acc: 0.9389 - val_loss: 0.4874 - val_acc: 0.8743 1350 | Epoch 13/30 1351 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.1613 - acc: 0.9439— val_f1: 0.790170 — val_precision: 0.813151 — val_recall: 0.771651 1352 | — val_f1_w: 0.874813 — val_precision_w: 0.874452 — val_recall_w: 0.876755 1353 | 55129/55129 [==============================] - 90s 2ms/step - loss: 0.1613 - acc: 0.9439 - val_loss: 0.5269 - val_acc: 0.8768 1354 | ``` 1355 | 1356 | 어... vanilla BiLSTM보다 성능이 떨어졌네요 ㅎㅎ 뭐 그럴수도 있죠... 아무래도 CNN과 BiLSTM이 infer하는 과정에서 양상이 달라, jointly training에서는 혼선이 생긴 것이 아닌가 싶습니다. model을 merge하는 것이 꼭 좋은 결과를 가져오지는 않는 것 같아요. 다음 장에서는 backend인 tensorflow를 끌어 와서 self-attentive BiLSTM을 구현해 보도록 하겠습니다. 1357 | 1358 | ## 10. Self-attentive BiLSTM 1359 | 1360 | **The second to the final step is applying 'the' attention mechanism, which has shifted paradigm of deep learning architectures, and still shifting... Many will be familiar with the [attention model](https://arxiv.org/abs/1409.0473) which came out along with [RNN encoder-decoder](https://arxiv.org/abs/1406.1078), or the self-attention which was suggested in [Transformer](http://papers.nips.cc/paper/7181-attention-is-all-you-need), which deals with seq2seq-style problems such as machine translation. Though the inherit philosophy may be consistent, here, we introduce a [structured self attentive embedding](https://arxiv.org/abs/1703.03130) which was suggested for an effective sentence classification.** 1361 | 1362 | **The key idea in this paper is to train an additional attention layer that assigns weight to each hidden layer of the BiLSTM structure. For this, a context vector of the size same as the hidden layer width, i.e. 64 as will be implemented, is separately defined and jointly trained, in the manner that *it is column-wisely multiplied with the MLP from each hidden layer to yield the attention vector*. The attention vector is recursively multiplied to the hidden layers so that the weighted sum becomes the final summarization for the fine-tuning. The code is implemented below; with the new function *lambda* which enables us to customize the layers in somewhat sophiscated ways. We also need to import the backend of Keras, here TensorFlow, for the layer-level and specific operations.** 1363 | 1364 | ```python 1365 | from keras.layers import Lambda 1366 | import keras.backend as K 1367 | 1368 | def validate_rnn_self_drop(x_rnn,x_y,hidden_lstm,hidden_con,hidden_dim,cw,val_sp,bat_size,filename): 1369 | char_r_input = Input(shape=(len(x_rnn[0]),len(x_rnn[0][0])),dtype='float32') 1370 | r_seq = Bidirectional(LSTM(hidden_lstm,return_sequences=True))(char_r_input) 1371 | r_att = Dense(hidden_con, activation='tanh')(r_seq) 1372 | att_source = np.zeros((len(x_rnn),hidden_con)) 1373 | att_test = np.zeros((len(x_rnn),hidden_con)) 1374 | att_input = Input(shape=(hidden_con,), dtype='float32') 1375 | att_vec = Dense(hidden_con,activation='relu')(att_input) 1376 | att_vec = Dropout(0.3)(att_vec) 1377 | att_vec = Dense(hidden_con,activation='relu')(att_vec) 1378 | att_vec = Lambda(lambda x: K.batch_dot(*x, axes=(1,2)))([att_vec,r_att]) 1379 | att_vec = Dense(len(x_rnn[0]),activation='softmax')(att_vec) 1380 | att_vec = layers.Reshape((len(x_rnn[0]),1))(att_vec) 1381 | r_seq = layers.multiply([att_vec,r_seq]) 1382 | r_seq = Lambda(lambda x: K.sum(x, axis=1))(r_seq) 1383 | r_seq = Dense(hidden_dim, activation='relu')(r_seq) 1384 | r_seq = Dropout(0.3)(r_seq) 1385 | r_seq = Dense(hidden_dim, activation='relu')(r_seq) 1386 | r_seq = Dropout(0.3)(r_seq) 1387 | main_output = Dense(int(max(x_y)+1),activation='softmax')(r_seq) 1388 | model = Sequential() 1389 | model = Model(inputs=[char_r_input,att_input],outputs=[main_output]) 1390 | model.summary() 1391 | model.compile(optimizer=adam_half,loss="sparse_categorical_crossentropy",metrics=["accuracy"]) 1392 | filepath=filename+"-{epoch:02d}-{val_acc:.4f}.hdf5" 1393 | checkpoint = ModelCheckpoint(filepath, monitor='val_acc', verbose=1, mode='max') 1394 | callbacks_list = [metricsf1macro_2input,checkpoint] 1395 | model.fit([x_rnn,att_source],x_y,validation_split=val_sp,epochs=50,batch_size= bat_size ,callbacks=callbacks_list,class_weight=cw) 1396 | ``` 1397 | 1398 | **At the very beginning of the code, BiLSTM module is defined so that each hidden layer can be fed as an input of MLP (here a single layer was utilized though) whose final size is the same as *the context vector*, 64 (*hidden_con*), to make up *r_att*. Next, from an attention source *zeros*, an attention vector *att_vec* is yielded by MLP, with the size of *hidden_con*, and is multiplied column-wisely to *r_att*! This finally makes up an attention vector that is recursively multiplied to the hidden layer sequence to yield the weighted sum. The rest are the same, but due to the model being large, we raised the number of epochs to 50. The same callback functions were utilized as in the last chapter since we utilized two inputs, RNN dataset and attention source (zeros).** 1399 | 1400 |

1401 |
1402 | (image from Lin 2017) 1403 | 1404 | 요즘, 아니 꽤 오랫동안 NLP와 ML에서 혁신을 가져왔던 attention mechanism을 이제서야 만나볼 수 있게 되었습니다. 많은 분들이 익숙하신 내용은 주로 machine translation과 함께 사용되었던 [attention model](https://arxiv.org/abs/1409.0473)이나 Transformer의 [self attention](http://papers.nips.cc/paper/7181-attention-is-all-you-need)일 텐데요, 여기서는 비슷한 시기에 sentence classification task를 위해 등장한 structured self-attentive embedding을 살펴보도록 하겠습니다. self attention이 나온다기는 좀 뭐하지만, attention vector을 활용하여 sentence classification에 사용되는 latent variable들에 정보를 주는데, 그 source가 자기 자신이라는 점이 self-attention과 유사한 측면이 있다고 생각됩니다. 1405 | 1406 | 아주 러프하게 말하면, 전체 procedure는 BiLSTM의 hidden layer sequence에 column-wise하게 곱해지는 attention vector를 얻는 데에 있어, context vector라는 개념을 도입하는 데에 있습니다. Context vector의 dimension을 *hidden_con*이라고 한다면, 우선 BiLSTM에서 MLP를 통해 *hidden_con*의 크기를 가진 dense layer을 각 hidden layer에 대해 뽑아낸 후, context vector와 column-wise한 inner product를 통해 attention vector을 얻는 것이죠. 어떻게 말해도 복잡하네요... 위에 있는 그림을 보는 것이 좀 더 이해가 빠를 것입니다 ㅎㅎ 여튼 여기서 중요한 점은, 케라스를 이용해서도 이 과정을 구현할 수 있다, 그런데 구현하려면 backend와 lambda라는 녀석들이 필요하다! 이 정도인 것 같네요. 1407 | 1408 | --- 1409 | 1410 | **The implementation might not be optimum, but well, we followed the description in the paper! The result shows that the self-attentive BiLSTM outperforms the vanilla one and the CNN-RNN concatenation, and matches with the character-based approach (which incorporates pretrained word embedding)! But it's not sure if we can push through the wall of 90% ...** 1411 | 1412 | ```properties 1413 | >>> validate_rnn_self_drop(fci_rec,fci_label,32,64,256,class_weights_fci,0.1,16,'model/tutorial/rec_self_drop') 1414 | 1415 | __________________________________________________________________________________________________ 1416 | Layer (type) Output Shape Param # Connected to 1417 | ================================================================================================== 1418 | input_12 (InputLayer) (None, 64) 0 1419 | __________________________________________________________________________________________________ 1420 | dense_20 (Dense) (None, 64) 4160 input_12[0][0] 1421 | __________________________________________________________________________________________________ 1422 | input_11 (InputLayer) (None, 30, 100) 0 1423 | __________________________________________________________________________________________________ 1424 | dropout_8 (Dropout) (None, 64) 0 dense_20[0][0] 1425 | __________________________________________________________________________________________________ 1426 | bidirectional_5 (Bidirectional) (None, 30, 64) 34048 input_11[0][0] 1427 | __________________________________________________________________________________________________ 1428 | dense_21 (Dense) (None, 64) 4160 dropout_8[0][0] 1429 | __________________________________________________________________________________________________ 1430 | dense_19 (Dense) (None, 30, 64) 4160 bidirectional_5[0][0] 1431 | __________________________________________________________________________________________________ 1432 | lambda_1 (Lambda) (None, 30) 0 dense_21[0][0] 1433 | dense_19[0][0] 1434 | __________________________________________________________________________________________________ 1435 | dense_22 (Dense) (None, 30) 930 lambda_1[0][0] 1436 | __________________________________________________________________________________________________ 1437 | reshape_1 (Reshape) (None, 30, 1) 0 dense_22[0][0] 1438 | __________________________________________________________________________________________________ 1439 | multiply_1 (Multiply) (None, 30, 64) 0 reshape_1[0][0] 1440 | bidirectional_5[0][0] 1441 | __________________________________________________________________________________________________ 1442 | lambda_2 (Lambda) (None, 64) 0 multiply_1[0][0] 1443 | __________________________________________________________________________________________________ 1444 | dense_23 (Dense) (None, 256) 16640 lambda_2[0][0] 1445 | __________________________________________________________________________________________________ 1446 | dropout_9 (Dropout) (None, 256) 0 dense_23[0][0] 1447 | __________________________________________________________________________________________________ 1448 | dense_24 (Dense) (None, 256) 65792 dropout_9[0][0] 1449 | __________________________________________________________________________________________________ 1450 | dropout_10 (Dropout) (None, 256) 0 dense_24[0][0] 1451 | __________________________________________________________________________________________________ 1452 | dense_25 (Dense) (None, 7) 1799 dropout_10[0][0] 1453 | ================================================================================================== 1454 | Total params: 131,689 1455 | Trainable params: 131,689 1456 | Non-trainable params: 0 1457 | __________________________________________________________________________________________________ 1458 | Train on 55129 samples, validate on 6126 samples 1459 | Epoch 1/50 1460 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.6279 - acc: 0.7886— val_f1: 0.711039 — val_precision: 0.807089 — val_recall: 0.670628 1461 | — val_f1_w: 0.831240 — val_precision_w: 0.842004 — val_recall_w: 0.839700 1462 | 55129/55129 [==============================] - 86s 2ms/step - loss: 0.6279 - acc: 0.7886 - val_loss: 0.4957 - val_acc: 0.8397 1463 | Epoch 2/50 1464 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.4680 - acc: 0.8442— val_f1: 0.754434 — val_precision: 0.791817 — val_recall: 0.731321 1465 | — val_f1_w: 0.848612 — val_precision_w: 0.848852 — val_recall_w: 0.851126 1466 | 55129/55129 [==============================] - 88s 2ms/step - loss: 0.4680 - acc: 0.8442 - val_loss: 0.4490 - val_acc: 0.8511 1467 | Epoch 3/50 1468 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.4244 - acc: 0.8578— val_f1: 0.771808 — val_precision: 0.812655 — val_recall: 0.743364 1469 | — val_f1_w: 0.858047 — val_precision_w: 0.858865 — val_recall_w: 0.861410 1470 | 55129/55129 [==============================] - 86s 2ms/step - loss: 0.4243 - acc: 0.8578 - val_loss: 0.4134 - val_acc: 0.8614 1471 | Epoch 4/50 1472 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.3948 - acc: 0.8673— val_f1: 0.751910 — val_precision: 0.806796 — val_recall: 0.719961 1473 | — val_f1_w: 0.856063 — val_precision_w: 0.856820 — val_recall_w: 0.860431 1474 | 55129/55129 [==============================] - 87s 2ms/step - loss: 0.3948 - acc: 0.8673 - val_loss: 0.4073 - val_acc: 0.8604 1475 | Epoch 5/50 1476 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.3688 - acc: 0.8747— val_f1: 0.772794 — val_precision: 0.823605 — val_recall: 0.740154 1477 | — val_f1_w: 0.860423 — val_precision_w: 0.866368 — val_recall_w: 0.863206 1478 | 55129/55129 [==============================] - 91s 2ms/step - loss: 0.3688 - acc: 0.8747 - val_loss: 0.4034 - val_acc: 0.8632 1479 | Epoch 6/50 1480 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.3482 - acc: 0.8801— val_f1: 0.782679 — val_precision: 0.834986 — val_recall: 0.751268 1481 | — val_f1_w: 0.867117 — val_precision_w: 0.871998 — val_recall_w: 0.871041 1482 | 55129/55129 [==============================] - 89s 2ms/step - loss: 0.3482 - acc: 0.8801 - val_loss: 0.3943 - val_acc: 0.8710 1483 | Epoch 7/50 1484 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.3285 - acc: 0.8869— val_f1: 0.775507 — val_precision: 0.834404 — val_recall: 0.740948 1485 | — val_f1_w: 0.866852 — val_precision_w: 0.870674 — val_recall_w: 0.871205 1486 | 55129/55129 [==============================] - 86s 2ms/step - loss: 0.3284 - acc: 0.8869 - val_loss: 0.4053 - val_acc: 0.8712 1487 | Epoch 8/50 1488 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.3166 - acc: 0.8914— val_f1: 0.789958 — val_precision: 0.832346 — val_recall: 0.761638 1489 | — val_f1_w: 0.874285 — val_precision_w: 0.877032 — val_recall_w: 0.877408 1490 | 55129/55129 [==============================] - 90s 2ms/step - loss: 0.3166 - acc: 0.8914 - val_loss: 0.3843 - val_acc: 0.8774 1491 | Epoch 9/50 1492 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.3011 - acc: 0.8956— val_f1: 0.796802 — val_precision: 0.811898 — val_recall: 0.786651 1493 | — val_f1_w: 0.872073 — val_precision_w: 0.872473 — val_recall_w: 0.873653 1494 | 55129/55129 [==============================] - 91s 2ms/step - loss: 0.3011 - acc: 0.8956 - val_loss: 0.3862 - val_acc: 0.8737 1495 | Epoch 10/50 1496 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.2873 - acc: 0.9008— val_f1: 0.786752 — val_precision: 0.821892 — val_recall: 0.765839 1497 | — val_f1_w: 0.874281 — val_precision_w: 0.877892 — val_recall_w: 0.877408 1498 | 55129/55129 [==============================] - 93s 2ms/step - loss: 0.2873 - acc: 0.9008 - val_loss: 0.3845 - val_acc: 0.8774 1499 | Epoch 11/50 1500 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.2755 - acc: 0.9048— val_f1: 0.796917 — val_precision: 0.856046 — val_recall: 0.760149 1501 | — val_f1_w: 0.877631 — val_precision_w: 0.881019 — val_recall_w: 0.881815 1502 | 55129/55129 [==============================] - 92s 2ms/step - loss: 0.2755 - acc: 0.9048 - val_loss: 0.3812 - val_acc: 0.8818 1503 | Epoch 12/50 1504 | 55088/55129 [============================>.] - ETA: 0s - loss: 0.2633 - acc: 0.9076— val_f1: 0.793011 — val_precision: 0.825051 — val_recall: 0.769426 1505 | — val_f1_w: 0.878768 — val_precision_w: 0.878530 — val_recall_w: 0.881325 1506 | 55129/55129 [==============================] - 90s 2ms/step - loss: 0.2633 - acc: 0.9076 - val_loss: 0.3828 - val_acc: 0.8813 1507 | Epoch 13/50 1508 | 55104/55129 [============================>.] - ETA: 0s - loss: 0.2515 - acc: 0.9121— val_f1: 0.791692 — val_precision: 0.792037 — val_recall: 0.792989 1509 | — val_f1_w: 0.873094 — val_precision_w: 0.872973 — val_recall_w: 0.873980 1510 | 55129/55129 [==============================] - 91s 2ms/step - loss: 0.2515 - acc: 0.9122 - val_loss: 0.3975 - val_acc: 0.8740 1511 | Epoch 14/50 1512 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.2413 - acc: 0.9150— val_f1: 0.799131 — val_precision: 0.802153 — val_recall: 0.797500 1513 | — val_f1_w: 0.878161 — val_precision_w: 0.877972 — val_recall_w: 0.878877 1514 | 55129/55129 [==============================] - 94s 2ms/step - loss: 0.2413 - acc: 0.9150 - val_loss: 0.3853 - val_acc: 0.8789 1515 | Epoch 15/50 1516 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.2295 - acc: 0.9190— val_f1: 0.800154 — val_precision: 0.813464 — val_recall: 0.789562 1517 | — val_f1_w: 0.876815 — val_precision_w: 0.876335 — val_recall_w: 0.877897 1518 | 55129/55129 [==============================] - 96s 2ms/step - loss: 0.2294 - acc: 0.9190 - val_loss: 0.3951 - val_acc: 0.8779 1519 | Epoch 16/50 1520 | 55120/55129 [============================>.] - ETA: 0s - loss: 0.2200 - acc: 0.9227— val_f1: 0.791979 — val_precision: 0.816055 — val_recall: 0.775612 1521 | — val_f1_w: 0.879622 — val_precision_w: 0.879747 — val_recall_w: 0.881978 1522 | 55129/55129 [==============================] - 97s 2ms/step - loss: 0.2200 - acc: 0.9227 - val_loss: 0.4043 - val_acc: 0.8820 1523 | ``` 1524 | 1525 | 어찌어찌하여 character-level embedding과 비슷한 결과가 나오긴 했네요 ㅎㅎ 힘들었습니다...만 여기서 그나마 얻은 것은 케라스에서도 이런 layer 단위의 빡센 아키텍쳐 수립이 가능하다는 점이었네요 ㅎㅎ 이게 베스트 구현인지는 모르겠습니다만 어쨌든 논문에서 하라는 대로 다 구현도 했고 성능도 아까에 비해서는 올랐습니다! ㅠㅠ 그런데 과연 우린 90%의 벽을 넘을 수 있을까요? 그 전에 89% 벽을 넘을 수 있을까요..? 지금까지의 feature-based approach들과 차별화된 뭔가 다른 접근방법이 필요한 것은 아닐까요....? 1526 | 1527 | ## 11. Transformer, BERT, and after 1528 | -------------------------------------------------------------------------------- /Requirements.txt: -------------------------------------------------------------------------------- 1 | fasttext==0.8.3 2 | Keras==2.1.2 3 | konlpy==0.4.4 4 | nltk==3.3 5 | numpy==1.14.3 6 | scikit-learn==0.19.1 7 | tensorflow-gpu==1.4.1 8 | -------------------------------------------------------------------------------- /image/alexnet2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/warnikchow/dlk2nlp/ea650390f750cce35acec9768857b7a71d6a4aac/image/alexnet2.png -------------------------------------------------------------------------------- /image/char2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/warnikchow/dlk2nlp/ea650390f750cce35acec9768857b7a71d6a4aac/image/char2.PNG -------------------------------------------------------------------------------- /image/rnn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/warnikchow/dlk2nlp/ea650390f750cce35acec9768857b7a71d6a4aac/image/rnn.jpg -------------------------------------------------------------------------------- /image/selfAA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/warnikchow/dlk2nlp/ea650390f750cce35acec9768857b7a71d6a4aac/image/selfAA.png -------------------------------------------------------------------------------- /image/skipgram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/warnikchow/dlk2nlp/ea650390f750cce35acec9768857b7a71d6a4aac/image/skipgram.png -------------------------------------------------------------------------------- /image/tfidf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/warnikchow/dlk2nlp/ea650390f750cce35acec9768857b7a71d6a4aac/image/tfidf.png -------------------------------------------------------------------------------- /image/ykim14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/warnikchow/dlk2nlp/ea650390f750cce35acec9768857b7a71d6a4aac/image/ykim14.png -------------------------------------------------------------------------------- /kor_char.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/warnikchow/dlk2nlp/ea650390f750cce35acec9768857b7a71d6a4aac/kor_char.npy --------------------------------------------------------------------------------