{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# **Ch11 문서 분석 시스템 만들기**\n", "- **처음 배우는 머신러닝** 11장 : [**(GitHub)**](https://github.com/your-first-ml-book/Examples)\n", "- **문서 분류 시스템 :** 간단한 **문서 분류 시스템**\n", "- **토픽 모델 시스템 :** 문서에서 **토픽을** 뽑아내는 시스템\n", "- **품사 분석 시스템 :** 문서에서 **각 단어의 품사를 분석하는** 시스템\n", "- **고유명사 태깅 시스템 :** 문서에서 시간, 장소등 **고유명사 태깅**\n", "- **단어 임베딩 학습 :** 문서에서 각 **단어의 임베딩을 수학적으로 모델링**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## **1 스펨메일 필터 만들기** (문서 분류 시스템)\n", "[**(스팸메일)**](http://archive.ics.uci.edu/ml/machine-learning-databases/00228/) 데이터로 **문서 분류 시스템** 만들기\n", "```\n", "ham\tGo until jurong point, crazy.. Available only in bugis n great world la e buffet... Cine there got amore wat...\n", "ham\tOk lar... Joking wif u oni...\n", "spam\tFree entry in 2 a wkly comp to win FA Cup final tkts 21st May 2005. Text FA to 87121 to receive entry question\n", "```\n", "### **01 스펨메일 데이터 분석**\n", "- 단어별 index를 생성합니다\n", "- 단어빈도 피처 만들기" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "전체 단어의 수 13627\n", "0 go\n", "1 until\n", "2 jurong\n", "3 point,\n", "4 crazy..\n" ] } ], "source": [ "vocabulary = {} # 단어집을 처리\n", "import numpy as np\n", "with open('data/SMSSpamCollection') as file_handle:\n", " for line in file_handle: # 파일을 한 줄씩 읽는다\n", " splits = line.split() # 한 줄을 빈 칸으로 쪼개서 리스트로 변환\n", " label = splits[0] # 맨 앞 단어는 레이블로 구분\n", " text = splits[1:]\n", " for word in text: # 단어 단위로 고유번호를 부여\n", " lower = word.lower()\n", " if not lower in vocabulary:\n", " vocabulary[lower] = len(vocabulary)\n", "\n", "# 단어집의 내용을 출력\n", "print(\"전체 단어의 수\", len(vocabulary))\n", "for no, word in enumerate(vocabulary):\n", " if no < 5: print(no, word)" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "단어빈도 피쳐의 수 5574\n" ] }, { "data": { "text/plain": [ "[array([0.05, 0.05, 0.05, ..., 0. , 0. , 0. ]),\n", " array([0., 0., 0., ..., 0., 0., 0.]),\n", " array([0., 0., 0., ..., 0., 0., 0.]),\n", " array([0., 0., 0., ..., 0., 0., 0.]),\n", " array([0., 0., 0., ..., 0., 0., 0.])]" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "features = [] # features 리스트 생성\n", "with open('data/SMSSpamCollection') as file_handle:\n", " for line in file_handle: \n", " splits = line.split()\n", " feature = np.zeros(len(vocabulary))\n", " text = splits[1:]\n", " for word in text: # Vocabulary 피처 갯수\n", " lower = word.lower()\n", " feature[vocabulary[lower]] += 1\n", "\n", " # 단어 빈도 피처를 문서 총 단어 갯수로 벡터를 나누어 피처를 생성\n", " feature = feature / sum(feature)\n", " features.append(feature)\n", "\n", "print(\"단어빈도 피쳐의 수\", len(features))\n", "features[:5]" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "스팸 레이블 피쳐의 수 5574\n" ] }, { "data": { "text/plain": [ "[0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0]" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "labels = [] # 레이블을 생성 합니다.\n", "with open('data/SMSSpamCollection') as file_handle:\n", " for line in file_handle: # 파일을 한 줄씩 읽습니다\n", " splits = line.split()\n", " label = splits[0]\n", " # 맨 앞 단어(label)이 spam 1, 아니면 0\n", " if label == 'spam': labels.append(1)\n", " else: labels.append(0)\n", "\n", "print(\"스팸 레이블 피쳐의 수\", len(labels))\n", "labels[:15]" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "# 위의 분석내용을 바탕으로 스펨메일 분석을 위한 데이터셋을 생성합니다\n", "spam_header = 'spam\\t'\n", "no_spam_header = 'ham\\t'\n", "documents, labels = [], []\n", "\n", "with open('data/SMSSpamCollection') as file_handle:\n", " for line in file_handle: # 각 줄에서 레이블만 뗀 documents\n", " if line.startswith(spam_header):\n", " labels.append(1)\n", " documents.append(line[len(spam_header):])\n", " elif line.startswith(no_spam_header):\n", " labels.append(0)\n", " documents.append(line[len(no_spam_header):])\n", "\n", "# 단어 횟수 피처에서 단어 빈도 피처 생성(idf 없으면 단어빈도(term frequency)가 생성)\n", "from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer\n", "vectorizer = CountVectorizer() # 단어 횟수 피처\n", "term_counts = vectorizer.fit_transform(documents)# 문서내 단어횟수\n", "vocabulary = vectorizer.get_feature_names()\n", "tf_transformer = TfidfTransformer(use_idf=False).fit(term_counts)\n", "features = tf_transformer.transform(term_counts)\n", "\n", "import pickle # 처리된 데이터를 파일로 저장\n", "with open('data/processed.pickle', 'wb') as file_handle:\n", " pickle.dump((vocabulary, features, labels), file_handle)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### **02 스펨메일 피처를 이용한 분류기**\n", "- 회귀모델을 활용한 분류기" ] }, { "cell_type": "code", "execution_count": 37, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "train accuracy: 0.97309\n", "test accuracy : 0.96125\n" ] } ], "source": [ "import pickle, warnings\n", "warnings.simplefilter(\"ignore\", category=FutureWarning)\n", "with open('data/processed.pickle', 'rb') as file_handle:\n", " vocabulary, features, labels = pickle.load(file_handle)\n", "\n", "total_number = len(labels)\n", "middle_index = total_number//2 # 50%를 Train, 나머지를 Test\n", "\n", "# 학습 데이터 분류 후 학습\n", "train_features = features[ :middle_index, :]\n", "train_labels = labels[ :middle_index]\n", "\n", "from sklearn.linear_model import LogisticRegression\n", "classifier = LogisticRegression()\n", "classifier.fit(train_features, train_labels)\n", "\n", "# 검증 데이터 분류허가\n", "test_features = features[middle_index: , :]\n", "test_labels = labels[middle_index: ]\n", "\n", "print(\"train accuracy: {:.5f}\\ntest accuracy : {:.5f}\".format(\n", " classifier.score(train_features, train_labels),\n", " classifier.score(test_features, test_labels)))" ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "score 4.3649 단어: txt\n", "score 4.0988 단어: call\n", "score 3.3730 단어: free\n", "score 2.6237 단어: text\n", "score 2.5961 단어: to\n", "score 2.4842 단어: uk\n", "score 2.4744 단어: www\n", "score 2.4241 단어: stop\n", "score 2.4017 단어: claim\n", "score 2.1648 단어: 150p\n" ] } ], "source": [ "# 어떤 항목이 판별에 영향을 많이 줬는지 찾아보기\n", "weights, pairs = classifier.coef_[0, :], []\n", "for index, value in enumerate(weights):\n", " pairs.append( (abs(value), vocabulary[index]) )\n", "\n", "# 생성된 pairs 자료를 점수 순서대로 정렬합니다\n", "pairs.sort(key=lambda x: x[0], reverse=True)\n", "for pair in pairs[:10]:\n", " print(\"score {:.4f} 단어: {}\".format(\n", " pair[0], pair[1]))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## **2 토픽 모델 시스템 만들기**\n", "LDA 모델을 구현하여 문자 Topic을 추출합니다" ] }, { "cell_type": "code", "execution_count": 43, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "LatentDirichletAllocation(batch_size=128, doc_topic_prior=None,\n", " evaluate_every=-1, learning_decay=0.7,\n", " learning_method='batch', learning_offset=10.0,\n", " max_doc_update_iter=100, max_iter=10, mean_change_tol=0.001,\n", " n_components=10, n_jobs=None, n_topics=None, perp_tol=0.1,\n", " random_state=None, topic_word_prior=None,\n", " total_samples=1000000.0, verbose=0)" ] }, "execution_count": 43, "metadata": {}, "output_type": "execute_result" } ], "source": [ "spam_header, no_spam_header = 'spam\\t', 'ham\\t'\n", "documents = []\n", "\n", "# 단순히 문서를 추출\n", "with open('data/SMSSpamCollection') as file_handle:\n", " for line in file_handle:\n", " if line.startswith(spam_header):\n", " documents.append(line[len(spam_header):])\n", " elif line.startswith(no_spam_header):\n", " documents.append(line[len(no_spam_header):])\n", "\n", "# 단어 feacture를 만듭니다\n", "from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer\n", "warnings.simplefilter(\"ignore\", category=PendingDeprecationWarning)\n", "vectorizer = CountVectorizer(stop_words='english', max_features=2000)\n", "term_counts = vectorizer.fit_transform(documents)\n", "vocabulary = vectorizer.get_feature_names()\n", " \n", "# LDA는 단어출현 갯수로 동작하므로 CountVectorizer를 활용\n", "from sklearn.decomposition import LatentDirichletAllocation\n", "topic_model = LatentDirichletAllocation(n_components=10)\n", "topic_model.fit(term_counts) # 토픽 모델을 학습" ] }, { "cell_type": "code", "execution_count": 46, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "topic 0: gt,lt,know,don,oh,let,money,like,make,sent,\n", "topic 1: day,good,dear,did,like,time,today,pls,morning,ur,\n", "topic 2: just,lol,tone,week,ur,like,txt,number,new,stop,\n", "topic 3: free,send,txt,ur,www,mobile,reply,stop,text,phone,\n", "topic 4: come,home,da,said,pick,wait,buy,happy,way,ask,\n", "topic 5: got,wat,time,say,come,dun,haha,need,soon,yeah,\n", "topic 6: ll,sorry,later,just,leave,talk,aight,text,ok,place,\n", "topic 7: love,hi,just,babe,hope,good,night,ur,miss,ya,\n", "topic 8: ok,just,work,feel,dont,im,want,getting,waiting,really,\n", "topic 9: lor,going,ok,urgent,prize,sleep,meet,trying,yup,dont,\n" ] } ], "source": [ "# 학습된 토픽들을 하나씩 출력합니다.\n", "topics = topic_model.components_\n", "for topic_id, weights in enumerate(topics):\n", " print('topic %d' % topic_id, end=': ')\n", " pairs = [(abs(value), vocabulary[term_id]) for term_id, value in enumerate(weights)]\n", " pairs.sort(key=lambda x: x[0], reverse=True)\n", " for pair in pairs[:10]:\n", " print(pair[1], end=',')\n", " print()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## **3 품사 분석 시스템 만들기**\n", "- `nltk.tag.corenlp.CoreNLPPOSTagger` 또는 `nltk.tag.corenlp.CoreNLPNERTagger` 대신 사용합니다\n", "- **스탠포드 품사 태깅소스를** 활용하여 품사 분석 머신러닝 모델을 만듭니다\n", "- 스탠포드 품사 태깅자료 다운받기 [**(Download)**](https://nlp.stanford.edu/software/tagger.shtml#Download)" ] }, { "cell_type": "code", "execution_count": 47, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/home/markbaum/Python/python/lib/python3.6/site-packages/nltk/tag/stanford.py:149: DeprecationWarning: \n", "The StanfordTokenizer will be deprecated in version 3.2.5.\n", "Please use \u001b[91mnltk.tag.corenlp.CoreNLPPOSTagger\u001b[0m or \u001b[91mnltk.tag.corenlp.CoreNLPNERTagger\u001b[0m instead.\n", " super(StanfordPOSTagger, self).__init__(*args, **kwargs)\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "['One', 'day', 'in', 'November', '2016', ',', 'the', 'two', 'authors', 'of', 'this', 'book', ',', 'Seungyeon', 'and', 'Youngjoo', ',', 'had', 'a', 'coffee', 'at', 'Red', 'Rock', 'cafe', ',', 'which', 'is', 'a', 'very', 'popular', 'place', 'in', 'Mountain', 'View', '.'] \n", "\n", " [('One', 'CD'), ('day', 'NN'), ('in', 'IN'), ('November', 'NNP'), ('2016', 'CD'), (',', ','), ('the', 'DT'), ('two', 'CD'), ('authors', 'NNS'), ('of', 'IN'), ('this', 'DT'), ('book', 'NN'), (',', ','), ('Seungyeon', 'NNP'), ('and', 'CC'), ('Youngjoo', 'NNP'), (',', ','), ('had', 'VBD'), ('a', 'DT'), ('coffee', 'NN'), ('at', 'IN'), ('Red', 'NNP'), ('Rock', 'NNP'), ('cafe', 'NN'), (',', ','), ('which', 'WDT'), ('is', 'VBZ'), ('a', 'DT'), ('very', 'RB'), ('popular', 'JJ'), ('place', 'NN'), ('in', 'IN'), ('Mountain', 'NNP'), ('View', 'NNP'), ('.', '.')]\n", "\n", "동사, 명사추출: day, November, authors, book, Seungyeon, Youngjoo, had, coffee, Red, Rock, cafe, is, place, Mountain, View\n" ] } ], "source": [ "# 분석할 Document\n", "text = \"\"\"One day in November 2016, the two authors of this book, \n", "Seungyeon and Youngjoo, had a coffee at Red Rock cafe, \n", "which is a very popular place in Mountain View.\"\"\"\n", "\n", "from nltk.tag import StanfordPOSTagger\n", "from nltk.tokenize import word_tokenize\n", "STANFORD_POS_MODEL_PATH = 'data/stanford-postagger-2018-10-16/models/english-bidirectional-distsim.tagger'\n", "STANFORD_POS_JAR_PATH = 'data/stanford-postagger-2018-10-16/stanford-postagger-3.9.2.jar'\n", "pos_tagger = StanfordPOSTagger(STANFORD_POS_MODEL_PATH, STANFORD_POS_JAR_PATH)\n", "tokens = word_tokenize(text)\n", "print(tokens, '\\n\\n', pos_tagger.tag(tokens))\n", "\n", "# 동사와 명사만 뽑아봅시다.\n", "noun_and_verbs = [token[0] for token in pos_tagger.tag(tokens)\n", " if token[1].startswith('V') or token[1].startswith('N')]\n", "print(\"\\n동사, 명사추출: \", ', '.join(noun_and_verbs))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## **4 고유명사 태깅 시스템 만들기**\n", "- `nltk.tag.corenlp.CoreNLPPOSTagger` 또는 `nltk.tag.corenlp.CoreNLPNERTagger` 를 활용합니다\n", "- **스탠포드 고유명사 추출기를** 활용하여 품사 분석 머신러닝 모델을 만듭니다\n", "- 스탠포드 고유명사 추출기 다운받기 [**(Download)**](https://nlp.stanford.edu/software/CRF-NER.shtml#Download)" ] }, { "cell_type": "code", "execution_count": 48, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/home/markbaum/Python/python/lib/python3.6/site-packages/nltk/tag/stanford.py:183: DeprecationWarning: \n", "The StanfordTokenizer will be deprecated in version 3.2.5.\n", "Please use \u001b[91mnltk.tag.corenlp.CoreNLPPOSTagger\u001b[0m or \u001b[91mnltk.tag.corenlp.CoreNLPNERTagger\u001b[0m instead.\n", " super(StanfordNERTagger, self).__init__(*args, **kwargs)\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "[('One', 'O'), ('day', 'O'), ('in', 'O'), ('November', 'DATE'), ('2016', 'DATE'), (',', 'O'), ('the', 'O'), ('two', 'O'), ('authors', 'O'), ('of', 'O'), ('this', 'O'), ('book', 'O'), (',', 'O'), ('Seungyeon', 'PERSON'), ('and', 'O'), ('Youngjoo', 'PERSON'), (',', 'O'), ('had', 'O'), ('a', 'O'), ('coffee', 'O'), ('at', 'O'), ('Red', 'ORGANIZATION'), ('Rock', 'ORGANIZATION'), ('cafe', 'O'), (',', 'O'), ('which', 'O'), ('is', 'O'), ('a', 'O'), ('very', 'O'), ('popular', 'O'), ('place', 'O'), ('in', 'O'), ('Mountain', 'O'), ('View', 'O'), ('.', 'O')]\n", "\n" ] } ], "source": [ "# 분석할 Document\n", "text = \"\"\"One day in November 2016, the two authors of this book, \n", "Seungyeon and Youngjoo, had a coffee at Red Rock cafe, \n", "which is a very popular place in Mountain View.\"\"\"\n", "\n", "STANFORD_NER_CLASSIFER_PATH = 'data/stanford-ner-2018-10-16/classifiers/english.muc.7class.distsim.crf.ser.gz'\n", "STANFORD_NER_JAR_PATH = 'data/stanford-ner-2018-10-16/stanford-ner-3.9.2.jar'\n", "\n", "from nltk.tag import StanfordNERTagger\n", "from nltk.tokenize import word_tokenize\n", "ner_tagger = StanfordNERTagger(STANFORD_NER_CLASSIFER_PATH, STANFORD_NER_JAR_PATH)\n", "tokens = word_tokenize(text)\n", "print(ner_tagger.tag(tokens))\n", "\n", "# 장소에 해당하는 단어만 출력합니다.\n", "all_locations = [token[0] for token in ner_tagger.tag(tokens) \n", " if token[1] == 'LOCATION']\n", "print(', '.join(all_locations))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## **5 한국어 위키백과를 이용한 Word2vec 만들기**\n" ] }, { "cell_type": "code", "execution_count": 50, "metadata": {}, "outputs": [], "source": [ "from konlpy.tag import Mecab\n", "from gensim.models import Word2Vec\n", "import sys, time, glob, unicodedata\n", "\n", "# 모델의 파라미터들입니다.\n", "WINDOW = 5\n", "EMBEDDING_SIZE = 200\n", "BATCH_SIZE = 10000\n", "ITER = 10" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "# 전처리된 위키백과 파일을 읽어 들입니다. (Mecab 경로 https://bitbucket.org/eunjeon/mecab-ko-dic)\n", "def read_text(fin):\n", " corpus_li = []\n", " mecab = Mecab(dicpath=' /usr/local/lib/mecab/dic/mecab-ko-dic')\n", " for line in open(fin):\n", " # 깨지는 글자를 처리하기 위해 unicodedata.normalize 함수를 이용해\n", " # NFKC로변환합니다.\n", " line = unicodedata.normalize('NFKC', line)\n", " try:\n", " # 첫 글자가 숫자로 시작하는 문장을 말뭉치에 추가합니다.\n", " _ = int(line[0])\n", " corpus_li.append(' '.join(mecab.nouns(line)) + '\\n')\n", "\n", " except ValueError:\n", " # 첫 글자가 한글로 시작하는 문장을 말뭉치에 추가합니다.\n", " if ord(line[0]) >= ord('가') and ord(line[0]) <= ord('힇'):\n", " corpus_li.append(' '.join(mecab.nouns(line))+'\\n')\n", " else:\n", " pass\n", " print('# of lines in corpus',len(corpus_li))\n", " return(corpus_li)\n", "\n", "def train_word2vec(corpus_li, fout_model):\n", " # read_text에서 생성한 말뭉치를 이용해 word2vec을 학습시킵니다.\n", " model = Word2Vec(corpus_li, sg=1, size=EMBEDDING_SIZE, window=WINDOW, min_count=5, workers=3, batch_words=BATCH_SIZE, iter=ITER)\n", " model.init_sims(replace=True) #clean up memory\n", " model.save(fout_model)\n", " return(model)\n", "\n", "# # 전처리된 파일을 한번에 읽어 들이기 위한 정규식\n", "# input_pattern = '파일위치/korean_wiki/kowiki-latest-pages-articles.xml-88.txt'\n", "# fin_li = glob.glob(input_pattern)\n", "\n", "# for fin in fin_li:\n", "# corpus_li = read_text(fin)\n", "\n", "# # 모델학습\n", "# model = train_word2vec(corpus_li, '파일위치/korean_wiki/test_model.txt')\n", "# print(model.most_similar('프랑스', topn=20))\n", "# print(model.most_similar(positive=['한국','파리'], negative=['서울']))" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.3" } }, "nbformat": 4, "nbformat_minor": 2 }