{ "cells": [ { "cell_type": "markdown", "metadata": { "id": "FG8-PmuCMpNZ" }, "source": [ "# 머신 러닝 교과서 3판" ] }, { "cell_type": "markdown", "metadata": { "id": "3T2FfnhGeT4O" }, "source": [ "# 네이버 영화 리뷰 감성 분류" ] }, { "cell_type": "markdown", "source": [ "**아래 링크를 통해 이 노트북을 주피터 노트북 뷰어(nbviewer.jupyter.org)로 보거나 구글 코랩(colab.research.google.com)에서 실행할 수 있습니다.**\n", "\n", "\n", " \n", " \n", "
\n", " 주피터 노트북 뷰어로 보기\n", " \n", " 구글 코랩(Colab)에서 실행하기\n", "
" ], "metadata": { "id": "zDTBRtqVElXm" } }, { "cell_type": "markdown", "metadata": { "id": "FsYp7FqjeT4T" }, "source": [ "IMDb 영화 리뷰 데이터셋과 비슷한 네이버 영화 리뷰 데이터셋(https://github.com/e9t/nsmc)을 사용해 한글 문장의 감성 분류 예제를 다루어 보겠습니다. 이 데이터는 네이버 영화 사이트에 있는 리뷰 20만 개를 모은 것입니다. 네이버 영화 리뷰 데이터셋 깃허브에서 직접 데이터를 다운로드 받아도 되지만 편의를 위해 이 책의 깃허브의 `ch09` 폴더에 데이터셋을 넣어 놓았습니다.\n", "\n", "20만개 데이터 중 15만개는 훈련 데이터셋으로 `ratings_train.txt` 파일에 저장되어 있고 5만개는 테스트 데이터셋으로 `ratings_test.txt` 파일에 저장되어 있습니다. 리뷰의 길이는 140을 넘지 않습니다. 부정 리뷰는 1\\~4까지 점수를 매긴 리뷰이고 긍정 리뷰는 6\\~10까지 점수를 매긴 리뷰입니다. 훈련 데이터셋과 테스트 데이터셋의 부정과 긍정 리뷰는 약 50%씩 구성되어 있습니다.\n", "\n", "한글은 영어와 달리 조사와 어미가 발달해 있기 때문에 BoW나 어간 추출보다 표제어 추출 방식이 적합합니다. 이런 작업을 형태소 분석이라 부릅니다. 파이썬에서 한글 형태소 분석을 하기 위한 대표적인 패키지는 `konlpy`와 `soynlp`입니다. 두 패키지를 모두 사용해 네이버 영화 리뷰를 긍정과 부정으로 분류해 보겠습니다.\n", "\n", "먼저 이 예제를 실행하려면 `konlpy`와 `soynlp`가 필요합니다. 다음 명령을 실행해 두 패키지를 설치해 주세요." ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "ZWy3R1S_eT4T", "outputId": "1fabc286-ecbe-4caf-f3ee-3c73c09a600f" }, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Collecting konlpy\n", " Downloading konlpy-0.6.0-py2.py3-none-any.whl (19.4 MB)\n", "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m19.4/19.4 MB\u001b[0m \u001b[31m73.6 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", "\u001b[?25hCollecting soynlp\n", " Downloading soynlp-0.0.493-py3-none-any.whl (416 kB)\n", "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m416.8/416.8 kB\u001b[0m \u001b[31m39.6 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", "\u001b[?25hCollecting JPype1>=0.7.0 (from konlpy)\n", " Downloading JPype1-1.4.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (465 kB)\n", "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m465.3/465.3 kB\u001b[0m \u001b[31m43.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", "\u001b[?25hRequirement already satisfied: lxml>=4.1.0 in /usr/local/lib/python3.10/dist-packages (from konlpy) (4.9.3)\n", "Requirement already satisfied: numpy>=1.6 in /usr/local/lib/python3.10/dist-packages (from konlpy) (1.23.5)\n", "Requirement already satisfied: psutil>=5.0.1 in /usr/local/lib/python3.10/dist-packages (from soynlp) (5.9.5)\n", "Requirement already satisfied: scipy>=1.1.0 in /usr/local/lib/python3.10/dist-packages (from soynlp) (1.11.3)\n", "Requirement already satisfied: scikit-learn>=0.20.0 in /usr/local/lib/python3.10/dist-packages (from soynlp) (1.2.2)\n", "Requirement already satisfied: packaging in /usr/local/lib/python3.10/dist-packages (from JPype1>=0.7.0->konlpy) (23.2)\n", "Requirement already satisfied: joblib>=1.1.1 in /usr/local/lib/python3.10/dist-packages (from scikit-learn>=0.20.0->soynlp) (1.3.2)\n", "Requirement already satisfied: threadpoolctl>=2.0.0 in /usr/local/lib/python3.10/dist-packages (from scikit-learn>=0.20.0->soynlp) (3.2.0)\n", "Installing collected packages: JPype1, konlpy, soynlp\n", "Successfully installed JPype1-1.4.1 konlpy-0.6.0 soynlp-0.0.493\n" ] } ], "source": [ "!pip install konlpy soynlp" ] }, { "cell_type": "markdown", "metadata": { "id": "-YQTzDuEMpNe" }, "source": [ "최신 tweepy를 설치할 경우 StreamListener가 없다는 에러가 발생(https://github.com/tweepy/tweepy/issues/1531) 하므로 3.10버전을 설치해 주세요." ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "id": "v_yX0jSnMpNe", "outputId": "25efde3d-fcd4-469b-f738-fd1db2beb79b", "colab": { "base_uri": "https://localhost:8080/" } }, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Collecting tweepy==3.10\n", " Downloading tweepy-3.10.0-py2.py3-none-any.whl (30 kB)\n", "Requirement already satisfied: requests[socks]>=2.11.1 in /usr/local/lib/python3.10/dist-packages (from tweepy==3.10) (2.31.0)\n", "Requirement already satisfied: requests-oauthlib>=0.7.0 in /usr/local/lib/python3.10/dist-packages (from tweepy==3.10) (1.3.1)\n", "Requirement already satisfied: six>=1.10.0 in /usr/local/lib/python3.10/dist-packages (from tweepy==3.10) (1.16.0)\n", "Requirement already satisfied: oauthlib>=3.0.0 in /usr/local/lib/python3.10/dist-packages (from requests-oauthlib>=0.7.0->tweepy==3.10) (3.2.2)\n", "Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.10/dist-packages (from requests[socks]>=2.11.1->tweepy==3.10) (3.3.2)\n", "Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.10/dist-packages (from requests[socks]>=2.11.1->tweepy==3.10) (3.4)\n", "Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.10/dist-packages (from requests[socks]>=2.11.1->tweepy==3.10) (2.0.7)\n", "Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.10/dist-packages (from requests[socks]>=2.11.1->tweepy==3.10) (2023.7.22)\n", "Requirement already satisfied: PySocks!=1.5.7,>=1.5.6 in /usr/local/lib/python3.10/dist-packages (from requests[socks]>=2.11.1->tweepy==3.10) (1.7.1)\n", "Installing collected packages: tweepy\n", " Attempting uninstall: tweepy\n", " Found existing installation: tweepy 4.14.0\n", " Uninstalling tweepy-4.14.0:\n", " Successfully uninstalled tweepy-4.14.0\n", "Successfully installed tweepy-3.10.0\n" ] } ], "source": [ "!pip install tweepy==3.10" ] }, { "cell_type": "markdown", "metadata": { "id": "7mxbeWrgeT4T" }, "source": [ "그다음 `konlpy`, `pandas`, `numpy`를 임포트합니다." ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "execution": { "iopub.execute_input": "2020-11-29T15:01:41.121048Z", "iopub.status.busy": "2020-11-29T15:01:41.119848Z", "iopub.status.idle": "2020-11-29T15:01:41.730013Z", "shell.execute_reply": "2020-11-29T15:01:41.731225Z" }, "id": "OyaGZYqneT4U" }, "outputs": [], "source": [ "import konlpy\n", "import pandas as pd\n", "import numpy as np" ] }, { "cell_type": "markdown", "metadata": { "id": "TGU6opMbeT4U" }, "source": [ "코랩을 사용하는 경우 다음 코드 셀을 실행하세요." ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "efnMXOnteT4U", "outputId": "93f82e56-0b68-4926-ded7-b5cb70ca3ecc" }, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "--2023-11-10 07:37:00-- https://github.com/rickiepark/python-machine-learning-book-3rd-edition/raw/master/ch08/ratings_train.txt\n", "Resolving github.com (github.com)... 140.82.114.3\n", "Connecting to github.com (github.com)|140.82.114.3|:443... connected.\n", "HTTP request sent, awaiting response... 302 Found\n", "Location: https://raw.githubusercontent.com/rickiepark/python-machine-learning-book-3rd-edition/master/ch08/ratings_train.txt [following]\n", "--2023-11-10 07:37:00-- https://raw.githubusercontent.com/rickiepark/python-machine-learning-book-3rd-edition/master/ch08/ratings_train.txt\n", "Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.110.133, 185.199.109.133, 185.199.108.133, ...\n", "Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.110.133|:443... connected.\n", "HTTP request sent, awaiting response... 200 OK\n", "Length: 14628807 (14M) [text/plain]\n", "Saving to: ‘ratings_train.txt’\n", "\n", "ratings_train.txt 100%[===================>] 13.95M --.-KB/s in 0.1s \n", "\n", "2023-11-10 07:37:00 (109 MB/s) - ‘ratings_train.txt’ saved [14628807/14628807]\n", "\n" ] } ], "source": [ "!wget https://github.com/rickiepark/python-machine-learning-book-3rd-edition/raw/master/ch08/ratings_train.txt -O ratings_train.txt" ] }, { "cell_type": "markdown", "metadata": { "id": "fO5TLM6ReT4U" }, "source": [ "감성 분류를 시작하기 전에 훈련 데이터셋과 테스트 데이터셋을 각각 판다스 데이터프레임으로 읽은 후 넘파이 배열로 준비하겠습니다. 먼저 훈련 데이터셋부터 읽어 보죠. `ratings_train.txt` 파일은 하나의 리뷰가 한 행을 구성하며 각 필드는 탭으로 구분되어 있기 때문에 판다스의 `read_csv()` 함수로 간편하게 읽어 들일 수 있습니다. `read_csv()`는 기본적으로 콤마를 기준으로 필드를 구분하므로 `delimiter='\\t'`으로 지정하여 탭으로 변경합니다. 기본적으로 판다스는 빈 문자열을 NaN으로 인식합니다. 빈 문자열을 그대로 유지하기 위해 `keep_default_na` 매개변수를 `False`로 지정합니다." ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "execution": { "iopub.execute_input": "2020-11-29T15:01:41.740407Z", "iopub.status.busy": "2020-11-29T15:01:41.738928Z", "iopub.status.idle": "2020-11-29T15:01:42.170874Z", "shell.execute_reply": "2020-11-29T15:01:42.172342Z" }, "id": "JwJMox3veT4U" }, "outputs": [], "source": [ "df_train = pd.read_csv('ratings_train.txt',\n", " delimiter='\\t', keep_default_na=False)" ] }, { "cell_type": "markdown", "metadata": { "id": "8jSgN3H4eT4V" }, "source": [ "데이터프레임의 `head()` 메서드를 호출하면 처음 5개의 행을 출력해 줍니다. 이 예제에서 사용할 데이터는 document 열과 label 열입니다. label은 리뷰가 긍정(1)인지 부정(0)인지를 나타내는 값입니다." ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 206 }, "execution": { "iopub.execute_input": "2020-11-29T15:01:42.193842Z", "iopub.status.busy": "2020-11-29T15:01:42.192161Z", "iopub.status.idle": "2020-11-29T15:01:42.202855Z", "shell.execute_reply": "2020-11-29T15:01:42.204059Z" }, "id": "cDsM3ikBeT4V", "outputId": "cb04a7aa-a06d-4fe5-d804-25ee10386344" }, "outputs": [ { "output_type": "execute_result", "data": { "text/plain": [ " id document label\n", "0 9976970 아 더빙.. 진짜 짜증나네요 목소리 0\n", "1 3819312 흠...포스터보고 초딩영화줄....오버연기조차 가볍지 않구나 1\n", "2 10265843 너무재밓었다그래서보는것을추천한다 0\n", "3 9045019 교도소 이야기구먼 ..솔직히 재미는 없다..평점 조정 0\n", "4 6483659 사이몬페그의 익살스런 연기가 돋보였던 영화!스파이더맨에서 늙어보이기만 했던 커스틴 ... 1" ], "text/html": [ "\n", "
\n", "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
iddocumentlabel
09976970아 더빙.. 진짜 짜증나네요 목소리0
13819312흠...포스터보고 초딩영화줄....오버연기조차 가볍지 않구나1
210265843너무재밓었다그래서보는것을추천한다0
39045019교도소 이야기구먼 ..솔직히 재미는 없다..평점 조정0
46483659사이몬페그의 익살스런 연기가 돋보였던 영화!스파이더맨에서 늙어보이기만 했던 커스틴 ...1
\n", "
\n", "
\n", "\n", "
\n", " \n", "\n", " \n", "\n", " \n", "
\n", "\n", "\n", "
\n", " \n", "\n", "\n", "\n", " \n", "
\n", "
\n", "
\n" ] }, "metadata": {}, "execution_count": 6 } ], "source": [ "df_train.head()" ] }, { "cell_type": "markdown", "metadata": { "id": "Jue5OUxAeT4W" }, "source": [ "데이터프레임의 열을 선택하여 `Series` 객체의 `values` 속성을 사용하면 document 열과 label 열을 넘파이 배열로 저장할 수 있습니다. 각각 훈련 데이터셋의 특성과 타깃 값으로 저장합니다." ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "execution": { "iopub.execute_input": "2020-11-29T15:01:42.212705Z", "iopub.status.busy": "2020-11-29T15:01:42.211590Z", "iopub.status.idle": "2020-11-29T15:01:42.215138Z", "shell.execute_reply": "2020-11-29T15:01:42.216006Z" }, "id": "7HIPcCfPeT4W" }, "outputs": [], "source": [ "X_train = df_train['document'].values\n", "y_train = df_train['label'].values" ] }, { "cell_type": "markdown", "metadata": { "id": "PqxXBSMBeT4W" }, "source": [ "코랩을 사용하는 경우 다음 코드 셀을 실행하세요." ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "yUrZByb5eT4W", "outputId": "4dfda8aa-25b9-4cb0-cf33-ef0d5ed60165" }, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "--2023-11-10 07:37:01-- https://github.com/rickiepark/python-machine-learning-book-3rd-edition/raw/master/ch08/ratings_test.txt\n", "Resolving github.com (github.com)... 140.82.114.4\n", "Connecting to github.com (github.com)|140.82.114.4|:443... connected.\n", "HTTP request sent, awaiting response... 302 Found\n", "Location: https://raw.githubusercontent.com/rickiepark/python-machine-learning-book-3rd-edition/master/ch08/ratings_test.txt [following]\n", "--2023-11-10 07:37:02-- https://raw.githubusercontent.com/rickiepark/python-machine-learning-book-3rd-edition/master/ch08/ratings_test.txt\n", "Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.108.133, 185.199.109.133, 185.199.110.133, ...\n", "Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.108.133|:443... connected.\n", "HTTP request sent, awaiting response... 200 OK\n", "Length: 4893335 (4.7M) [text/plain]\n", "Saving to: ‘ratings_test.txt’\n", "\n", "ratings_test.txt 100%[===================>] 4.67M 12.9MB/s in 0.4s \n", "\n", "2023-11-10 07:37:02 (12.9 MB/s) - ‘ratings_test.txt’ saved [4893335/4893335]\n", "\n" ] } ], "source": [ "!wget https://github.com/rickiepark/python-machine-learning-book-3rd-edition/raw/master/ch08/ratings_test.txt -O ratings_test.txt" ] }, { "cell_type": "markdown", "metadata": { "id": "DzmwdGhQeT4W" }, "source": [ "`ratings_test.txt` 파일에 대해서도 동일한 작업을 수행합니다." ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "execution": { "iopub.execute_input": "2020-11-29T15:01:42.225841Z", "iopub.status.busy": "2020-11-29T15:01:42.224625Z", "iopub.status.idle": "2020-11-29T15:01:42.355393Z", "shell.execute_reply": "2020-11-29T15:01:42.356362Z" }, "id": "8LuQzIO_eT4W" }, "outputs": [], "source": [ "df_test = pd.read_csv('ratings_test.txt',\n", " delimiter='\\t', keep_default_na=False)\n", "\n", "X_test = df_test['document'].values\n", "y_test = df_test['label'].values" ] }, { "cell_type": "markdown", "metadata": { "id": "CIg6nOV0eT4X" }, "source": [ "훈련 데이터셋과 테스트 데이터셋의 크기를 확인해 보죠. 각각 150,000개와 50,000개의 샘플을 가지고 있고 양성 클래스와 음성 클래스의 비율은 거의 50%에 가깝습니다." ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "execution": { "iopub.execute_input": "2020-11-29T15:01:42.363876Z", "iopub.status.busy": "2020-11-29T15:01:42.362699Z", "iopub.status.idle": "2020-11-29T15:01:42.369415Z", "shell.execute_reply": "2020-11-29T15:01:42.370340Z" }, "id": "aCJj4gz2eT4X", "outputId": "7ac70561-91fe-454d-90a2-4498e0daec69" }, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "150000 [75173 74827]\n" ] } ], "source": [ "print(len(X_train), np.bincount(y_train))" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "execution": { "iopub.execute_input": "2020-11-29T15:01:42.379051Z", "iopub.status.busy": "2020-11-29T15:01:42.377892Z", "iopub.status.idle": "2020-11-29T15:01:42.383488Z", "shell.execute_reply": "2020-11-29T15:01:42.384353Z" }, "id": "j7_pV67ieT4X", "outputId": "83c1fa97-b0dc-4773-e8a0-7719b7534f64" }, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "50000 [24827 25173]\n" ] } ], "source": [ "print(len(X_test), np.bincount(y_test))" ] }, { "cell_type": "markdown", "metadata": { "id": "SvH5IqbSeT4X" }, "source": [ "훈련 데이터셋과 테스트 데이터셋을 준비했으므로 형태소 분석기를 사용해 본격적인 감성 분류 작업을 시작해 보겠습니다.\n", "\n", "`konlpy`는 5개의 한국어 형태소 분석기를 파이썬 클래스로 감싸서 제공하는 래퍼 패키지입니다. `konlpy`가 제공하는 형태소 분석기에 대한 자세한 내용은 온라인 문서(https://konlpy.org/ko/latest/)를 참고하세요. 이 예에서는 스칼라로 개발된 open-korean-text 한국어 처리기(https://github.com/open-korean-text/open-korean-text)를 제공하는 `Okt` 클래스를 사용해 보겠습니다. open-korean-text는 비교적 성능이 높고 별다른 설치 없이 구글 코랩에서도 바로 사용할 수 있습니다.\n", "\n", "`konlpy.tag` 패키지에서 `Okt` 클래스를 임포트하고 객체를 만든 다음 훈련 데이터셋에 있는 문장 하나를 `morphs()` 메서드로 형태소로 나누어 보겠습니다." ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "execution": { "iopub.execute_input": "2020-11-29T15:01:42.393575Z", "iopub.status.busy": "2020-11-29T15:01:42.392373Z", "iopub.status.idle": "2020-11-29T15:01:46.646365Z", "shell.execute_reply": "2020-11-29T15:01:46.647813Z" }, "id": "TuyMi1QseT4X", "outputId": "664ab205-6cc6-4d7f-da7d-ee2d76f9e69b", "scrolled": true }, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "사이몬페그의 익살스런 연기가 돋보였던 영화!스파이더맨에서 늙어보이기만 했던 커스틴 던스트가 너무나도 이뻐보였다\n", "['사이', '몬페', '그', '의', '익살스런', '연기', '가', '돋보였던', '영화', '!', '스파이더맨', '에서', '늙어', '보이기만', '했던', '커스틴', '던스트', '가', '너무나도', '이뻐', '보였다']\n" ] } ], "source": [ "from konlpy.tag import Okt\n", "\n", "okt = Okt()\n", "print(X_train[4])\n", "print(okt.morphs(X_train[4]))" ] }, { "cell_type": "markdown", "metadata": { "id": "C0bYdHCLeT4Y" }, "source": [ "한글 문장에서 조사와 어미가 잘 구분되어 출력된 것을 볼 수 있습니다. '사이몬페그'와 같은 고유 명사는 처리하는데 어려움을 겪고 있네요. 완벽하지는 않지만 이 클래스를 사용해 분류 문제를 풀어 보겠습니다.\n", "\n", "`TfidfVectorzier`는 기본적으로 공백을 기준으로 토큰을 구분하지만 `tokenizer` 매개변수에 토큰화를 위한 사용자 정의 함수를 전달할 수 있습니다. 따라서 앞서 테스트했던 `okt.morphs` 메서드를 전달하면 형태소 분석을 통해 토큰화를 수행할 수 있습니다. `tokenizer` 매개변수를 사용할 때 패턴`token_pattern=None`으로 지정하여 `token_pattern` 매개변수가 사용되지 않는다는 경고 메시지가 나오지 않게 합니다.\n", "\n", "`TfidfVectorzier`을 `ngram_range=(1, 2)`로 설정하여 유니그램과 바이그램을 사용하고 `min_df=3`으로 지정하여 3회 미만으로 등장하는 토큰은 무시합니다. 또한 `max_df=0.9`로 두어 가장 많이 등장하는 상위 10%의 토큰도 무시하겠습니다. 이런 작업이 불용어로 생각할 수 있는 토큰을 제거할 것입니다.\n", "\n", "컴퓨팅 파워가 충분하다면 하이퍼파라미터 탐색 단계에서 `TfidfVectorzier`의 매개변수도 탐색해 보는 것이 좋습니다. 여기에서는 임의의 매개변수 값을 지정하여 데이터를 미리 변환하고 하이퍼파라미터 탐색에서는 분류기의 매개변수만 탐색하겠습니다." ] }, { "cell_type": "markdown", "metadata": { "id": "OzXPfYhNeT4Y" }, "source": [ "노트: 토큰 데이터를 생성하는 시간이 많이 걸리므로 주피터 노트북에서는 다음 번 실행 때 이 과정을 건너 뛸 수 있도록 이 데이터를 한 번 생성하여 npz 파일로 저장합니다." ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "execution": { "iopub.execute_input": "2020-11-29T15:01:46.674650Z", "iopub.status.busy": "2020-11-29T15:01:46.673542Z", "iopub.status.idle": "2020-11-29T15:11:36.679650Z", "shell.execute_reply": "2020-11-29T15:11:36.681173Z" }, "id": "hq8ipu4meT4Y" }, "outputs": [], "source": [ "import os\n", "from scipy.sparse import save_npz, load_npz\n", "from sklearn.feature_extraction.text import TfidfVectorizer\n", "\n", "if not os.path.isfile('okt_train.npz'):\n", " tfidf = TfidfVectorizer(ngram_range=(1, 2),\n", " min_df=3,\n", " max_df=0.9,\n", " tokenizer=okt.morphs,\n", " token_pattern=None)\n", " tfidf.fit(X_train)\n", " X_train_okt = tfidf.transform(X_train)\n", " X_test_okt = tfidf.transform(X_test)\n", " save_npz('okt_train.npz', X_train_okt)\n", " save_npz('okt_test.npz', X_test_okt)\n", "else:\n", " X_train_okt = load_npz('okt_train.npz')\n", " X_test_okt = load_npz('okt_test.npz')" ] }, { "cell_type": "markdown", "metadata": { "id": "fGreYI2QeT4Y" }, "source": [ "`X_train_okt`와 `X_test_okt`가 준비되었으므로 `SGDClassifier` 클래스를 사용해 감성 분류 문제를 풀어 보겠습니다. 탐색할 `SGDClassifier`의 매개변수는 규제를 위한 `alpha` 매개변수입니다. `RandomizedSearchCV` 클래스를 사용하기 위해 `loguniform` 함수로 탐색 범위를 지정하겠습니다. 여기에서는 `SGDClassifier`의 손실 함수로 로지스틱 손실(`'log_loss'`)을 사용하지만 다른 손실 함수를 매개변수 탐색에 포함할 수 있습니다. 총 반복 회수(`n_iter`)는 50회로 지정합니다. 만약 CPU 코어가 여러개라면 `n_jobs` 매개변수를 1 이상으로 설정하여 수행 속도를 높일 수 있습니다." ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 136 }, "execution": { "iopub.execute_input": "2020-11-29T15:11:36.694918Z", "iopub.status.busy": "2020-11-29T15:11:36.693663Z", "iopub.status.idle": "2020-11-29T15:12:42.390611Z", "shell.execute_reply": "2020-11-29T15:12:42.392041Z" }, "id": "_wU0Ya2HeT4Y", "outputId": "1eddd78c-6876-4ee9-9b49-893854f0ebc4" }, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Fitting 5 folds for each of 50 candidates, totalling 250 fits\n" ] }, { "output_type": "execute_result", "data": { "text/plain": [ "RandomizedSearchCV(estimator=SGDClassifier(loss='log_loss', random_state=1),\n", " n_iter=50,\n", " param_distributions={'alpha': },\n", " random_state=1, verbose=1)" ], "text/html": [ "
RandomizedSearchCV(estimator=SGDClassifier(loss='log_loss', random_state=1),\n",
              "                   n_iter=50,\n",
              "                   param_distributions={'alpha': <scipy.stats._distn_infrastructure.rv_continuous_frozen object at 0x78894ae89cf0>},\n",
              "                   random_state=1, verbose=1)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
" ] }, "metadata": {}, "execution_count": 14 } ], "source": [ "from sklearn.model_selection import RandomizedSearchCV\n", "from sklearn.linear_model import SGDClassifier\n", "from scipy.stats import loguniform\n", "\n", "sgd = SGDClassifier(loss='log_loss', random_state=1)\n", "param_dist = {'alpha': loguniform(0.0001, 100.0)}\n", "\n", "rsv_okt = RandomizedSearchCV(estimator=sgd,\n", " param_distributions=param_dist,\n", " n_iter=50,\n", " random_state=1,\n", " verbose=1)\n", "rsv_okt.fit(X_train_okt, y_train)" ] }, { "cell_type": "markdown", "metadata": { "id": "T747MdCCeT4Z" }, "source": [ "하이퍼파라미터 탐색으로 찾은 최상의 점수와 매개변수 값을 확인해 보죠." ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "execution": { "iopub.execute_input": "2020-11-29T15:12:42.400217Z", "iopub.status.busy": "2020-11-29T15:12:42.399676Z", "iopub.status.idle": "2020-11-29T15:12:42.402066Z", "shell.execute_reply": "2020-11-29T15:12:42.402499Z" }, "id": "65mXqAPLeT4Z", "outputId": "a037d4a2-611f-4acf-992e-47f3193ffd59" }, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "0.8251533333333334\n", "{'alpha': 0.00010015813955858975}\n" ] } ], "source": [ "print(rsv_okt.best_score_)\n", "print(rsv_okt.best_params_)" ] }, { "cell_type": "markdown", "metadata": { "id": "yGrhn-WfeT4Z" }, "source": [ "테스트 데이터셋 `X_test_okt`에서 점수도 확인해 보겠습니다." ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "execution": { "iopub.execute_input": "2020-11-29T15:12:42.408737Z", "iopub.status.busy": "2020-11-29T15:12:42.407752Z", "iopub.status.idle": "2020-11-29T15:12:42.423387Z", "shell.execute_reply": "2020-11-29T15:12:42.422942Z" }, "id": "7tp7zV6ueT4Z", "outputId": "0d68d30c-616c-4af5-84f8-81a2597a7716" }, "outputs": [ { "output_type": "execute_result", "data": { "text/plain": [ "0.8189" ] }, "metadata": {}, "execution_count": 16 } ], "source": [ "rsv_okt.score(X_test_okt, y_test)" ] }, { "cell_type": "markdown", "metadata": { "id": "iW662y6geT4Z" }, "source": [ "약 82%의 정확도를 냈습니다. 간단한 작업으로 꽤 좋은 성능을 냈습니다. `konlpy`의 다른 형태소 분석 클래스를 사용하거나 `SGDClassifier` 외에 다른 분류기를 시도하지 않을 이유는 없습니다. 충분한 컴퓨팅 파워가 없다면 사이킷런 0.24버전에서 추가되는 `HalvingRandomSearchCV` 클래스를 사용해 볼 수도 있습니다.\n", "\n", "이번에는 또 다른 파이썬 형태소 분석기인 `soynlp`를 사용해 보겠습니다. `soynlp`는 순수하게 파이썬으로 구현된 형태소 분석 패키지입니다. 깃허브(https://github.com/lovit/soynlp)에는 소스 코드 뿐만 아니라 다양한 튜토리얼도 함께 제공합니다. `soynlp`는 3개의 토큰화 클래스를 제공합니다. 기본적으로 띄어쓰기가 잘 되어 있다면 `LTokenizer`가 잘 맞습니다. 이외에는 `MaxScoreTokenizer`와 `RegexTokenizer`가 있습니다. 이 예에서는 `LTokenizer`를 사용해 보겠습니다. 먼저 `soynlp.tokenizer`에서 `LTokenizer`를 임포트합니다." ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "execution": { "iopub.execute_input": "2020-11-29T15:12:42.426570Z", "iopub.status.busy": "2020-11-29T15:12:42.426035Z", "iopub.status.idle": "2020-11-29T15:12:42.464962Z", "shell.execute_reply": "2020-11-29T15:12:42.465486Z" }, "id": "TtdRt0BaeT4Z" }, "outputs": [], "source": [ "from soynlp.tokenizer import LTokenizer" ] }, { "cell_type": "markdown", "metadata": { "id": "189ZB-HceT4Z" }, "source": [ "`LTokenizer` 클래스의 객체를 만든다음 앞에서와 같이 훈련 데이터셋에 있는 샘플(`X_train[4]`) 하나의 형태소를 분석해 보겠습니다." ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "execution": { "iopub.execute_input": "2020-11-29T15:12:42.468398Z", "iopub.status.busy": "2020-11-29T15:12:42.467780Z", "iopub.status.idle": "2020-11-29T15:12:42.473531Z", "shell.execute_reply": "2020-11-29T15:12:42.474052Z" }, "id": "NIXkrCZ3eT4a", "outputId": "c89c13ad-1465-4f68-cbb9-a18afbec708c" }, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "['사이몬페그의', '익살스런', '연기가', '돋보였던', '영화!스파이더맨에서', '늙어보이기만', '했던', '커스틴', '던스트가', '너무나도', '이뻐보였다']\n" ] } ], "source": [ "lto = LTokenizer()\n", "\n", "print(lto.tokenize(X_train[4]))" ] }, { "cell_type": "markdown", "metadata": { "id": "HUGVFJqfeT4a" }, "source": [ "`soynlp`는 말뭉치의 통계 데이터를 기반으로 동작하기 때문에 기본 `LTokenizer` 객체로는 공백으로만 토큰화를 수행합니다. `LTokenizer`에 필요한 통계 데이터를 생성하기 위해서 `WordExtractor`를 사용해 보겠습니다." ] }, { "cell_type": "code", "execution_count": 19, "metadata": { "execution": { "iopub.execute_input": "2020-11-29T15:12:42.476948Z", "iopub.status.busy": "2020-11-29T15:12:42.476313Z", "iopub.status.idle": "2020-11-29T15:12:42.480668Z", "shell.execute_reply": "2020-11-29T15:12:42.481192Z" }, "id": "plxTDWh1eT4a" }, "outputs": [], "source": [ "from soynlp.word import WordExtractor" ] }, { "cell_type": "markdown", "metadata": { "id": "lCjrr9I4eT4a" }, "source": [ "`WordExtractor` 객체를 만든 후 `train()` 메서드에 `X_train`을 전달하여 훈련합니다. 훈련이 끝나면 `word_scores()` 메서드에서 단어의 점수를 얻을 수 있습니다. 반환된 `scores` 객체는 단어마다 결합 점수(cohesion score)와 브랜칭 엔트로피(branching entropy)를 가진 딕셔너리입니다." ] }, { "cell_type": "code", "execution_count": 20, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "execution": { "iopub.execute_input": "2020-11-29T15:12:42.484178Z", "iopub.status.busy": "2020-11-29T15:12:42.483525Z", "iopub.status.idle": "2020-11-29T15:13:02.265607Z", "shell.execute_reply": "2020-11-29T15:13:02.266059Z" }, "id": "UfDQAyMBeT4a", "outputId": "8813268f-9a78-48e7-ca3a-f1c606438f03" }, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "training was done. used memory 1.312 Gb\n", "all cohesion probabilities was computed. # words = 85683\n", "all branching entropies was computed # words = 101540\n", "all accessor variety was computed # words = 101540\n" ] } ], "source": [ "word_ext = WordExtractor()\n", "word_ext.train(X_train)\n", "scores = word_ext.word_scores()" ] }, { "cell_type": "markdown", "metadata": { "id": "rV-r5PgDeT4a" }, "source": [ "`soynlp` 깃허브의 튜토리얼(https://github.com/lovit/soynlp/blob/master/tutorials/wordextractor_lecture.ipynb)을 따라 결합 점수(`cohesion_forward`)와 브랜칭 엔트로피(`right_branching_entropy`)에 지수를 취한 값에 곱하여 최종 점수를 만들겠습니다." ] }, { "cell_type": "code", "execution_count": 21, "metadata": { "execution": { "iopub.execute_input": "2020-11-29T15:13:02.336859Z", "iopub.status.busy": "2020-11-29T15:13:02.300535Z", "iopub.status.idle": "2020-11-29T15:13:02.362949Z", "shell.execute_reply": "2020-11-29T15:13:02.363379Z" }, "id": "zIumyVEZeT4a" }, "outputs": [], "source": [ "import math\n", "\n", "score_dict = {key: scores[key].cohesion_forward *\n", " math.exp(scores[key].right_branching_entropy)\n", " for key in scores}" ] }, { "cell_type": "markdown", "metadata": { "id": "pv1jrxNeeT4b" }, "source": [ "이제 이 점수를 `LTokenizer`의 `scores` 매개변수로 전달하여 객체를 만들고 앞에서 테스트한 샘플에 다시 적용해 보겠습니다." ] }, { "cell_type": "code", "execution_count": 22, "metadata": { "execution": { "iopub.execute_input": "2020-11-29T15:13:02.370019Z", "iopub.status.busy": "2020-11-29T15:13:02.367826Z", "iopub.status.idle": "2020-11-29T15:13:02.372862Z", "shell.execute_reply": "2020-11-29T15:13:02.371490Z" }, "id": "GJfd5Yd6eT4b" }, "outputs": [], "source": [ "lto = LTokenizer(scores=score_dict)" ] }, { "cell_type": "code", "execution_count": 23, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "execution": { "iopub.execute_input": "2020-11-29T15:13:02.378318Z", "iopub.status.busy": "2020-11-29T15:13:02.377355Z", "iopub.status.idle": "2020-11-29T15:13:02.382387Z", "shell.execute_reply": "2020-11-29T15:13:02.383156Z" }, "id": "v5LzdkboeT4b", "outputId": "538fb1d4-b504-4dd8-8e1e-27f222d54853" }, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "['사이', '몬페그의', '익살스', '런', '연기', '가', '돋보', '였던', '영화', '!스파이더맨에서', '늙어', '보이기만', '했던', '커스틴', '던스트가', '너무', '나도', '이뻐', '보였다']\n" ] } ], "source": [ "print(lto.tokenize(X_train[4]))" ] }, { "cell_type": "markdown", "metadata": { "id": "0rhMAkiCeT4b" }, "source": [ "단어 점수를 활용했기 때문에 토큰 추출이 훨씬 잘 된 것을 볼 수 있습니다. `lto.tokenizer` 메서드를 `TfidVectorizer` 클래스에 전달하여 `konlpy`를 사용했을 때와 같은 조건으로 훈련 데이터셋과 테스트 데이터셋을 변환해 보겠습니다." ] }, { "cell_type": "code", "execution_count": 24, "metadata": { "execution": { "iopub.execute_input": "2020-11-29T15:13:02.406183Z", "iopub.status.busy": "2020-11-29T15:13:02.404835Z", "iopub.status.idle": "2020-11-29T15:13:30.205753Z", "shell.execute_reply": "2020-11-29T15:13:30.207164Z" }, "id": "7cIFrliheT4c" }, "outputs": [], "source": [ "if not os.path.isfile('soy_train.npz'):\n", " tfidf = TfidfVectorizer(ngram_range=(1, 2),\n", " min_df=3,\n", " max_df=0.9,\n", " tokenizer=lto.tokenize,\n", " token_pattern=None)\n", " tfidf.fit(X_train)\n", " X_train_soy = tfidf.transform(X_train)\n", " X_test_soy = tfidf.transform(X_test)\n", " save_npz('soy_train.npz', X_train_soy)\n", " save_npz('soy_test.npz', X_test_soy)\n", "else:\n", " X_train_soy = load_npz('soy_train.npz')\n", " X_test_soy = load_npz('soy_test.npz')" ] }, { "cell_type": "markdown", "metadata": { "id": "F8Lqpo4veT4c" }, "source": [ "동일한 `SGDClassifier` 객체와 매개변수 분포를 지정하고 하이퍼파라미터 탐색을 수행해 보겠습니다." ] }, { "cell_type": "code", "execution_count": 25, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 136 }, "execution": { "iopub.execute_input": "2020-11-29T15:13:30.217890Z", "iopub.status.busy": "2020-11-29T15:13:30.216509Z", "iopub.status.idle": "2020-11-29T15:14:25.175608Z", "shell.execute_reply": "2020-11-29T15:14:25.176810Z" }, "id": "-IJgUB_OeT4c", "outputId": "5a457054-79b0-4242-9a51-10be71f7e174" }, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Fitting 5 folds for each of 50 candidates, totalling 250 fits\n" ] }, { "output_type": "execute_result", "data": { "text/plain": [ "RandomizedSearchCV(estimator=SGDClassifier(loss='log_loss', random_state=1),\n", " n_iter=50,\n", " param_distributions={'alpha': },\n", " random_state=1, verbose=1)" ], "text/html": [ "
RandomizedSearchCV(estimator=SGDClassifier(loss='log_loss', random_state=1),\n",
              "                   n_iter=50,\n",
              "                   param_distributions={'alpha': <scipy.stats._distn_infrastructure.rv_continuous_frozen object at 0x78894ae89cf0>},\n",
              "                   random_state=1, verbose=1)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
" ] }, "metadata": {}, "execution_count": 25 } ], "source": [ "rsv_soy = RandomizedSearchCV(estimator=sgd,\n", " param_distributions=param_dist,\n", " n_iter=50,\n", " random_state=1,\n", " verbose=1)\n", "rsv_soy.fit(X_train_soy, y_train)" ] }, { "cell_type": "markdown", "metadata": { "id": "yriXOgmOeT4c" }, "source": [ "`soynlp`를 사용했을 때 최상의 점수와 매개변수는 다음과 같습니다." ] }, { "cell_type": "code", "execution_count": 26, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "execution": { "iopub.execute_input": "2020-11-29T15:14:25.181559Z", "iopub.status.busy": "2020-11-29T15:14:25.180493Z", "iopub.status.idle": "2020-11-29T15:14:25.184547Z", "shell.execute_reply": "2020-11-29T15:14:25.185022Z" }, "id": "2Rbtw_HfeT4c", "outputId": "489de69d-746a-45a3-8b8a-a70868a4dcd1" }, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "0.8141066666666665\n", "{'alpha': 0.00010015813955858975}\n" ] } ], "source": [ "print(rsv_soy.best_score_)\n", "print(rsv_soy.best_params_)" ] }, { "cell_type": "markdown", "metadata": { "id": "DrMF_ur-eT4c" }, "source": [ "마지막으로 테스트 데이터셋에 대한 점수를 확인해 보겠습니다." ] }, { "cell_type": "code", "execution_count": 27, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "execution": { "iopub.execute_input": "2020-11-29T15:14:25.189932Z", "iopub.status.busy": "2020-11-29T15:14:25.189391Z", "iopub.status.idle": "2020-11-29T15:14:25.203260Z", "shell.execute_reply": "2020-11-29T15:14:25.204144Z" }, "id": "ilETGKlFeT4c", "outputId": "ddf3fe31-53d5-41d7-f93e-d939902a8174" }, "outputs": [ { "output_type": "execute_result", "data": { "text/plain": [ "0.8085" ] }, "metadata": {}, "execution_count": 27 } ], "source": [ "rsv_soy.score(X_test_soy, y_test)" ] }, { "cell_type": "markdown", "metadata": { "id": "R-y2lf04eT4d" }, "source": [ "`Okt`를 사용했을 때 보다는 조금 더 낮지만 약 81% 이상의 정확도를 얻었습니다." ] } ], "metadata": { "colab": { "name": "naver_movie_review.ipynb", "provenance": [] }, "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.7.3" } }, "nbformat": 4, "nbformat_minor": 0 }