テキスト分類 (TensorFlow)

本章では、テキストデータの基本的な取り扱い方法からニューラルネットワークを用いて、文書の分類を行う方法までを理解、実装することができる事をゴールとします。

自然言語は今までのデータとは異なり、固定の長さのデータではなく、データごとに長さが異なるため、学習の際には固定の長さの数値に落とし込む方法を理解する必要があります。文章を品詞ごとに分割する形態解析、単語を数値化する手法の一つである Bag of Words について学び、自然言語の特徴量変換の方法について学んでいきましょう。

本章の構成

  • MeCab で形態素解析
  • Bag of Words で特徴量変換
  • 文書分類の実装

MeCab で形態素解析

テキストデータもこれまでのデータと同様に数値化を行う必要があります。まずはどのようにテキストデータをニューラルネットワークの入力変数として使用できるようなベクトルに変換できるかの方法について学んでいきます。

数値化を行う前に文章のようなテキストデータをどのように取り扱うのかについて確認します。
例えば、「私はキカガクです。」 といった文章を数値化する場合は、下記のように変換を加える事が一般的です。

私 / は / キカガク / です / 。

単語毎に文章を切り分けていることがわかります。このように文章を単語毎に区切り方法の事を 形態素解析 (morphological analysis) と呼びます。

形態素解析のプログラムを自身で組む事は非常に困難です。日本語の形態素解析に対してはよく MeCab と呼ばれるパッケージを用いる事が多いです。MeCab を使用しての形態素解析の実装方法を確認しましょう。

形態素解析の実装

使用方法は非常にシンプルです。実際に実装しながら確認していきます。Colab 上には MeCab がインストールされていないため、下記のコマンドを実行してインストールを行います。
ローカル環境にインストールする際はこちらを参照して下さい。

# !apt install aptitude
# !aptitude install mecab libmecab-dev mecab-ipadic-utf8 git make curl xz-utils file -y
# !pip install mecab-python3==0.7
      
import MeCab
mecab = MeCab.Tagger('-Ochasen')
      

上記のコードでは、Tagger クラスをインタンス化しています。引数にある -Ochasen とは形態素解析の出力方法を指定しています。詳細はこちらの Github を確認して下さい。

文章の分割には parse() メソッドを使用します。

res = mecab.parse('こんにちは、私はキカガクです。')
      
print(res)
      
こんにちは コンニチハ こんにちは 感動詞 、 、 、 記号-読点 私 ワタシ 私 名詞-代名詞-一般 は ハ は 助詞-係助詞 キカガク キカガク キカガク 名詞-一般 です デス です 助動詞 特殊・デス 基本形 。 。 。 記号-句点 EOS

print() 関数を使用しているため、見た目上はきれいに表示されていますが、形態素解析した結果は下記のように、エスケープシーケンス(改行などを表す特殊な文字列)を多く含みます。

res
      
'こんにちは\tコンニチハ\tこんにちは\t感動詞\t\t\n、\t、\t、\t記号-読点\t\t\n私\tワタシ\t私\t名詞-代名詞-一般\t\t\nは\tハ\tは\t助詞-係助詞\t\t\nキカガク\tキカガク\tキカガク\t名詞-一般\t\t\nです\tデス\tです\t助動詞\t特殊・デス\t基本形\n。\t。\t。\t記号-句点\t\t\nEOS\n'

このように分割されたテキストデータはエスケープシーケンスを取り除き、単語ごとに抽出を行う処理を施す必要があります。

形態素解析を行い、単語ごとに文章を分割ができました。次のステップは分割された単語を数値に変換していく事になります。

名詞の抽出

本章で取り組む文書分類の問題設定をここで確認しておきましょう。分類を行う文書は下記の 9 つのニュースサイトの記事となります。

名前 概要
ITライフハック IT関連情報をお届けするIT生活とビジネスのお役立ちサイト
家電チャンネル 家電情報をお届けするlifestyle支援サイト
livedoor HOMME 男性向けライフスタイルWebマガジン
トピックニュース 時事・芸能ニュースを取扱う
Sports Watch スポーツニュースを取扱う
MOVIE ENTER 映画関連のニュースを取扱う
独女通信 20歳以上の独身女性の本音を扱うコラム
エスマックス モバイル関連情報・ニュースを取扱う
Peachy 恋愛・グルメなどを取扱う女性のためのニュースサイト

入力変数は各記事(文書)をとり、目的変数はどのニュースサイトなのかのラベルになります。

文書の例も先に確認しておきましょう。下記は IT ライフハックの記事から一部文章を抜粋したものになります。

旧式Macで禁断のパワーアップ!最新PCやソフトを一挙にチェック【ITフラッシュバック】
テレビやTwitterと連携できるパソコンや、 ...

他の記事の文章も確認しておきましょう。下記の文章は Sports Watch から一部抜粋したものになります。

バルセロナ五輪柔道金メダリストとしての実績を引っさげ、2002年にプロ総合格闘家に転向。以後、数々の死闘を繰り広げてきた吉田。昨年大晦日のDynamite!!では、石井慧との金メダリスト対決

これらの文書を数値化し、分類を行う問題設定になります。数値化は全ての分割した単語の全ての品詞に適用してもいいですが今回は品詞を絞り数値化します。

上記 2 つの記事を分類する際には単語の中でもどの品詞が重要と考えられるでしょうか。IT ライフハックの記事では、Mac や PC 、ソフトといった名詞が確認でき、Sports Watch の記事では柔道、金メダル、プロのようなスポーツならではの名詞が使用されている事が確認できます。

このように今回の問題設定では名詞を用いて分類を行うことが可能な事が想定されます。文章の形態素解析から名詞の抽出方法について確認します。

練習として、下記の 3 つの文章から名詞の抽出を行います。

text1 = 'キカガクでは、ディープラーニングを含んだ機械学習や人工知能の教育を行っています。'
text2 = '代表の吉崎は大学院では機械学習・ロボットのシステム制御、画像処理の研究に携わっていました。'
text3 = '機械学習、システム制御、画像処理ではすべて線形代数とプログラミングが不可欠になります。'
      
# 形態素解析
res = mecab.parse(text1)
print(res)
      
キカガク キカガク キカガク 名詞-一般 で デ で 助詞-格助詞-一般 は ハ は 助詞-係助詞 、 、 、 記号-読点 ディープラーニング ディープラーニング ディープラーニング 名詞-一般 を ヲ を 助詞-格助詞-一般 含ん フクン 含む 動詞-自立 五段・マ行 連用タ接続 だ ダ だ 助動詞 特殊・タ 基本形 機械 キカイ 機械 名詞-一般 学習 ガクシュウ 学習 名詞-サ変接続 や ヤ や 助詞-並立助詞 人工 ジンコウ 人工 名詞-一般 知能 チノウ 知能 名詞-一般 の ノ の 助詞-連体化 教育 キョウイク 教育 名詞-サ変接続 を ヲ を 助詞-格助詞-一般 行っ オコナッ 行う 動詞-自立 五段・ワ行促音便 連用タ接続 て テ て 助詞-接続助詞 い イ いる 動詞-非自立 一段 連用形 ます マス ます 助動詞 特殊・マス 基本形 。 。 。 記号-句点 EOS

形態素解析を行ったテキストデータは下記のようにエスケープシーケンスが含まれていました。
それぞれを改行 (\n) で分割を行います。特定の文字列でテキストデータを分けるときは、split() メソッドを使用します。

res
      
'キカガク\tキカガク\tキカガク\t名詞-一般\t\t\nで\tデ\tで\t助詞-格助詞-一般\t\t\nは\tハ\tは\t助詞-係助詞\t\t\n、\t、\t、\t記号-読点\t\t\nディープラーニング\tディープラーニング\tディープラーニング\t名詞-一般\t\t\nを\tヲ\tを\t助詞-格助詞-一般\t\t\n含ん\tフクン\t含む\t動詞-自立\t五段・マ行\t連用タ接続\nだ\tダ\tだ\t助動詞\t特殊・タ\t基本形\n機械\tキカイ\t機械\t名詞-一般\t\t\n学習\tガクシュウ\t学習\t名詞-サ変接続\t\t\nや\tヤ\tや\t助詞-並立助詞\t\t\n人工\tジンコウ\t人工\t名詞-一般\t\t\n知能\tチノウ\t知能\t名詞-一般\t\t\nの\tノ\tの\t助詞-連体化\t\t\n教育\tキョウイク\t教育\t名詞-サ変接続\t\t\nを\tヲ\tを\t助詞-格助詞-一般\t\t\n行っ\tオコナッ\t行う\t動詞-自立\t五段・ワ行促音便\t連用タ接続\nて\tテ\tて\t助詞-接続助詞\t\t\nい\tイ\tいる\t動詞-非自立\t一段\t連用形\nます\tマス\tます\t助動詞\t特殊・マス\t基本形\n。\t。\t。\t記号-句点\t\t\nEOS\n'
# 改行ごとに分割し、リストに格納
res.split('\n') 
      
['キカガク\tキカガク\tキカガク\t名詞-一般\t\t', 'で\tデ\tで\t助詞-格助詞-一般\t\t', 'は\tハ\tは\t助詞-係助詞\t\t', '、\t、\t、\t記号-読点\t\t', 'ディープラーニング\tディープラーニング\tディープラーニング\t名詞-一般\t\t', 'を\tヲ\tを\t助詞-格助詞-一般\t\t', '含ん\tフクン\t含む\t動詞-自立\t五段・マ行\t連用タ接続', 'だ\tダ\tだ\t助動詞\t特殊・タ\t基本形', '機械\tキカイ\t機械\t名詞-一般\t\t', '学習\tガクシュウ\t学習\t名詞-サ変接続\t\t', 'や\tヤ\tや\t助詞-並立助詞\t\t', '人工\tジンコウ\t人工\t名詞-一般\t\t', '知能\tチノウ\t知能\t名詞-一般\t\t', 'の\tノ\tの\t助詞-連体化\t\t', '教育\tキョウイク\t教育\t名詞-サ変接続\t\t', 'を\tヲ\tを\t助詞-格助詞-一般\t\t', '行っ\tオコナッ\t行う\t動詞-自立\t五段・ワ行促音便\t連用タ接続', 'て\tテ\tて\t助詞-接続助詞\t\t', 'い\tイ\tいる\t動詞-非自立\t一段\t連用形', 'ます\tマス\tます\t助動詞\t特殊・マス\t基本形', '。\t。\t。\t記号-句点\t\t', 'EOS', '']

最後に 2 行に EOS (End Of Sentence) と空白が入ってしまっているため、最後 2 つの要素までをスライスして使用します。

# 最後の 2 つの要素までをスライス
res.split('\n')[:-2]
      
['キカガク\tキカガク\tキカガク\t名詞-一般\t\t', 'で\tデ\tで\t助詞-格助詞-一般\t\t', 'は\tハ\tは\t助詞-係助詞\t\t', '、\t、\t、\t記号-読点\t\t', 'ディープラーニング\tディープラーニング\tディープラーニング\t名詞-一般\t\t', 'を\tヲ\tを\t助詞-格助詞-一般\t\t', '含ん\tフクン\t含む\t動詞-自立\t五段・マ行\t連用タ接続', 'だ\tダ\tだ\t助動詞\t特殊・タ\t基本形', '機械\tキカイ\t機械\t名詞-一般\t\t', '学習\tガクシュウ\t学習\t名詞-サ変接続\t\t', 'や\tヤ\tや\t助詞-並立助詞\t\t', '人工\tジンコウ\t人工\t名詞-一般\t\t', '知能\tチノウ\t知能\t名詞-一般\t\t', 'の\tノ\tの\t助詞-連体化\t\t', '教育\tキョウイク\t教育\t名詞-サ変接続\t\t', 'を\tヲ\tを\t助詞-格助詞-一般\t\t', '行っ\tオコナッ\t行う\t動詞-自立\t五段・ワ行促音便\t連用タ接続', 'て\tテ\tて\t助詞-接続助詞\t\t', 'い\tイ\tいる\t動詞-非自立\t一段\t連用形', 'ます\tマス\tます\t助動詞\t特殊・マス\t基本形', '。\t。\t。\t記号-句点\t\t']

次に、一番最初の要素に対して、タブ (\t) で分割を行います。

# 1 つ目の単語をスライス
res.split('\n')[0]
      
'キカガク\tキカガク\tキカガク\t名詞-一般\t\t'

こちらの結果から名詞であるかどうかを判定するためには、左から 4 番目(要素番号 3)にアクセスすれば品詞を取得することができます。

# 品詞の取得
res .split('\n')[0].split('\t')[3]
      
'名詞-一般'

上記一連の流れを for 文を用いて文章全体に適用し、品詞が名詞の単語のみを抽出し、リストに格納します。

nouns = [] # 品詞が名詞 (noun) である単語を格納するリスト
res = mecab.parse(text1)
words = res.split('\n')[:-2]
for word in words:
    part = word.split('\t')
    if '名詞' in part[3]:
        nouns.append(part[0])
      
nouns
      
['キカガク', 'ディープラーニング', '機械', '学習', '人工', '知能', '教育']

上記のプログラムを関数化し、text1 ~ 3 の文章からも同様に名詞の抽出を行います。

def get_nouns(text):
    nouns = []
    res = mecab.parse(text)
    words = res.split('\n')[:-2]
    for word in words:
        part = word.split('\t')
        if '名詞' in part[3]:
            nouns.append(part[0])
    return nouns
      
nouns1 = get_nouns(text1)
nouns1
      
['キカガク', 'ディープラーニング', '機械', '学習', '人工', '知能', '教育']
nouns2 = get_nouns(text2)
nouns2
      
['代表', '吉崎', '大学院', '機械', '学習', 'ロボット', 'システム', '制御', '画像', '処理', '研究']
nouns3 = get_nouns(text3)
nouns3
      
['機械', '学習', 'システム', '制御', '画像', '処理', 'すべて', '線形', '代数', 'プログラミング', '不可欠']

自然言語の特徴量変換

自然言語の特徴量変換には様々な手法が存在します。その方法は多種多様であり、どの手法を用いるかは問題設定に応じて変更する必要があります。下記は基礎的なテキストデータのエンコーディング方法(ベクトル化)になります。

本章ではエンコーディング手法の中で最もシンプルなものの 1 つである Bag of Words を用いてのエンコーディング方法を確認します。

Bag of Words の概要

Bag of Words (以下 BoW)とは、単語の出現回数によって単語を数値に変換する方法です。

次の 3 つの文章を BoW を用いてエンコーディングを行った場合の結果を確認しましょう。

  1. 私は電車が好きです。
  2. 電車より車をよく使います。
  3. 好きな果物はりんごです。

3 つの文に出現する単語をすべて羅列します。この出現する単語から重複を取り除いたものを辞書 (dictionary) と呼びます。

[ 私 は 電車 が 好き です より 車 を よく 使い ます な 果物 りんご ]

各文章に対し、羅列した単語を出現数に変換します。

  1. [ 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 ]
  2. [ 0 0 1 0 0 0 1 1 1 1 1 1 0 0 0 ]
  3. [ 0 1 0 0 1 1 0 0 0 0 0 0 1 1 1 ]

BoW の実装

前節で取得した名詞のリストを BoW で、エンコーディングを行いましょう。BoW の実装は scikit-learn を用います。scikit-learn を用いる場合、テキストデータは単語ごとに半角スペース区切りになっている必要があります。join() メソッドを用いることでリストの要素を繋げる事が可能です。

nouns1
      
['キカガク', 'ディープラーニング', '機械', '学習', '人工', '知能', '教育']
# 要素を半角スペースで結合
' '.join(nouns1)
      
'キカガク ディープラーニング 機械 学習 人工 知能 教育'

上記の処理を text1 ~ 3 全てに適用し、1 つのリストに格納します。

nouns_list = [nouns1, nouns2, nouns3]
corpus = []
for nouns in nouns_list:
  corpus.append(' '.join(nouns))
      
corpus
      
['キカガク ディープラーニング 機械 学習 人工 知能 教育', '代表 吉崎 大学院 機械 学習 ロボット システム 制御 画像 処理 研究', '機械 学習 システム 制御 画像 処理 すべて 線形 代数 プログラミング 不可欠']

CountVectorizer クラスを使用します。インスタンス化後に fit_transform() メソッドを使用すると、前述の説明通り、単語毎に ID が割り振られ、ID ごとの出現回数を元にベクトル化が行われます。

from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer()
x = vectorizer.fit_transform(corpus)
      

vocabulary_ 属性にはエンコーディングされた単語とその ID を確認することができます。

テキストデータには 21 個の重複のない名詞があることが確認できます。

vectorizer.vocabulary_
      
{'すべて': 0, 'キカガク': 1, 'システム': 2, 'ディープラーニング': 3, 'プログラミング': 4, 'ロボット': 5, '不可欠': 6, '人工': 7, '代数': 8, '代表': 9, '処理': 10, '制御': 11, '吉崎': 12, '大学院': 13, '学習': 14, '教育': 15, '機械': 16, '画像': 17, '知能': 18, '研究': 19, '線形': 20}

エンコーディング後の数値は toarray() メソッドを使用して取得します。

x = x.toarray()
print(x)
      
[[0 1 0 1 0 0 0 1 0 0 0 0 0 0 1 1 1 0 1 0 0] [0 0 1 0 0 1 0 0 0 1 1 1 1 1 1 0 1 1 0 1 0] [1 0 1 0 1 0 1 0 1 0 1 1 0 0 1 0 1 1 0 0 1]]

エンコーディング後の数値を確認して、エンコーディングが想定通りなされているか確認しましょう。

1 つ目のリストの数値が 1 となっている要素番号は次になります。1, 3, 7, 14, 15, 16, 18 です。こちらをエンコーディング前の単語に当てはめると下記の単語を取得することができます。

 'キカガク': 1,
 'ディープラーニング': 3,
 '人工': 7,
 '学習': 14,
 '教育': 15,
 '機械': 16,
 '知能': 18,

1 つ目の名詞リストと同じものは入っていること確認できました。BoW を用いてのベクトル化の方法が理解できました。文書分類に取り組んで行きましょう。

文書分類の実装

実際のデータセットを用いて、文書分類の実装を行います。
データセットはこちらからダウンロードし、Colab 上にアップロードを行って下さい。

from google.colab import files
uploaded = files.upload()
      

データセットは zip 形式のファイルになります。次のコマンドを実行し、解凍します。

# 解凍コマンド
!unzip -d text text.zip
      
# 解凍したファイルの確認
!ls text/
      
it-life-hack livedoor-homme peachy sports-watch kaden-channel movie-enter smax topic-news

今回使用するデータセットの概要をもう一度確認します。

名前 概要
ITライフハック IT関連情報をお届けするIT生活とビジネスのお役立ちサイト
家電チャンネル 家電情報をお届けするlifestyle支援サイト
livedoor HOMME 男性向けライフスタイルWebマガジン
トピックニュース 時事・芸能ニュースを取扱う
Sports Watch スポーツニュースを取扱う
MOVIE ENTER 映画関連のニュースを取扱う
独女通信 20歳以上の独身女性の本音を扱うコラム
エスマックス モバイル関連情報・ニュースを取扱う
Peachy 恋愛・グルメなどを取扱う女性のためのニュースサイト

入力変数・目的変数の作成

テキストファイルごとに BoW を用いてエンコーディングを行います。また、それぞれのテキストファイルごとにどのカテゴリの文書なのかの目的変数の作成も行います。

Python でファイルやディレクトリを操作するときに便利なパッケージに glob があります。glob を用いてファイルの読み込みを行います。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
      
from glob import glob
      
# text ディレクトリ以下のディレクトリの取得
directories = glob('text/*')
directories
      
['text/sports-watch', 'text/it-life-hack', 'text/smax', 'text/peachy', 'text/livedoor-homme', 'text/movie-enter', 'text/topic-news', 'text/kaden-channel']

入力変数と目的変数の作成を行う方法を順を追って確認を行います。取得したディレクトリ名の中から 1 つ目の要素を取り出し中身の確認を行います。

filepaths = glob('{}/*.txt'.format(directories[0]))
filepaths[:3]
      
['text/sports-watch/sports-watch-5644902.txt', 'text/sports-watch/sports-watch-5137919.txt', 'text/sports-watch/sports-watch-6802377.txt']

1 つ目のディレクトリ内のテキストファイル名の取得を行うことができました。ファイルの展開には with 構文を用います。読み込んだ際に [2:] で空白と日付が含まれる最初の 2 つの要素を除外しています。

with open(filepaths[0], encoding='utf-8') as f:
  text = ''.join(f.readlines()[2:])
print(text)
      
【Sports Watch】俊足すぎるFW=U-22日本代表・永井の“スピード伝説”とは? ロンドン五輪出場を目指すサッカー・U-22日本代表。二次予選はクウェート一カ国と戦うホーム&アウェイ戦となっている。 注目を集めるのは、U-22代表戦11試合で8ゴールを誇る日本のエース・永井謙佑(名古屋グランパス)だ。18日深夜放送、TBS「S-1」は、50mを5.8秒で駆け抜ける期待の俊足FWに迫った。 「よーいドンは負けない」という永井は「自分ではよく分からないですけど、速いみたいですね。(足が速くなったのは)高校2年くらいです」と他人事のように語るも、“永井のスピード伝説”として、高校時代を知る友人は、番組のカメラに「車、40キロくらいを追い付く。あいつ走って」と話し、九州国際大付属高校時代の恩師・杉山公一監督は「自分で出したスルーパスに逆サイドの子がオフサイドだったので、そのまま自分で追いかけてドリブルになったりとか、そういうことはよくありました」と明かした。 また、俊足サッカー選手と言えば、“野人”岡野雅行があまりにも有名だが、岡野について訊かれた永井は「あんなに速くないですよ」と苦笑い。高校入学時は、小柄で足も速くなかったという永井は、「よく倒れたり、戻したりしてました」、「タイミングが合ったというか、彼の成長が、身体の成長とトレーニングがうまくあったのかも知れません」と杉山監督が振り返る同高校の名物=坂道&階段を使った地獄のトレーニングによって、その能力が開花したようだ。

取り出したこの 1 テキストファイルが入力変数になります。このテキストデータからこの記事がどのカテゴリに属するのかの分類を行います。

各ディレクトリに対応する要素番号を分類に使用するラベルとして使用します。enumerate() を使うことで、for 文を用いて繰り返しを実行する際に要素番号も併せて取得することができます。

for (i, directory) in enumerate(directories):
    print(i, directory)
    print('- - -')
      
0 text/sports-watch - - - 1 text/it-life-hack - - - 2 text/smax - - - 3 text/peachy - - - 4 text/livedoor-homme - - - 5 text/movie-enter - - - 6 text/topic-news - - - 7 text/kaden-channel - - -

全てのディレクトリ・テキストファイルを読み込み、同時にラベル付けも行います。

texts, labels = [], []
for (i, directory) in enumerate(directories):
    #各ディレクトリ内のtxtファイルのパスをすべて取得
    filepaths = glob('{}/*.txt'.format(directory))
    # テキストを読み込んで、内容をtextに格納、ラベルも併せて格納
    for filepath in filepaths:
        with open(filepath, encoding='utf-8') as f:
            text = ''.join(f.readlines()[2:])  # URL等の先頭2行を除いた各行の文章を連結(join)して格納
            texts.append(text)
            labels.append(i)
      

取り出したテキストデータとラベルを確認します。

len(texts), len(labels)
      
(6505, 6505)
texts[0]
      
'【Sports Watch】俊足すぎるFW=U-22日本代表・永井の“スピード伝説”とは?\nロンドン五輪出場を目指すサッカー・U-22日本代表。二次予選はクウェート一カ国と戦うホーム&アウェイ戦となっている。\n\n注目を集めるのは、U-22代表戦11試合で8ゴールを誇る日本のエース・永井謙佑(名古屋グランパス)だ。18日深夜放送、TBS「S-1」は、50mを5.8秒で駆け抜ける期待の俊足FWに迫った。\n\n「よーいドンは負けない」という永井は「自分ではよく分からないですけど、速いみたいですね。(足が速くなったのは)高校2年くらいです」と他人事のように語るも、“永井のスピード伝説”として、高校時代を知る友人は、番組のカメラに「車、40キロくらいを追い付く。あいつ走って」と話し、九州国際大付属高校時代の恩師・杉山公一監督は「自分で出したスルーパスに逆サイドの子がオフサイドだったので、そのまま自分で追いかけてドリブルになったりとか、そういうことはよくありました」と明かした。\n\nまた、俊足サッカー選手と言えば、“野人”岡野雅行があまりにも有名だが、岡野について訊かれた永井は「あんなに速くないですよ」と苦笑い。高校入学時は、小柄で足も速くなかったという永井は、「よく倒れたり、戻したりしてました」、「タイミングが合ったというか、彼の成長が、身体の成長とトレーニングがうまくあったのかも知れません」と杉山監督が振り返る同高校の名物=坂道&階段を使った地獄のトレーニングによって、その能力が開花したようだ。\n'
labels[0]
      
0

文章から名詞のみを抽出

前に作成した名詞抽出用の関数を使用して、文書全体で使用されている名詞を全て word_collect というリストに格納していきましょう。

import MeCab
mecab = MeCab.Tagger('-Ochasen')
      
def get_nouns(text):
    nouns = []
    res = mecab.parse(text)
    words = res.split('\n')[:-2]
    for word in words:
        part = word.split('\t')
        if '名詞' in part[3]:
            nouns.append(part[0])
    return nouns
      

それぞれのテキストデータに対し名詞抽出を行う関数を適用し、リストに追加する前に半角スペース区切りの文字列に変換を行います。

word_collect = []
for text in texts:
    nouns = get_nouns(text)
    word_collect.append(' '.join(nouns))
      
word_collect[0]
      
'Sports Watch 俊足 FW U - 22 日本 代表 永井 スピード 伝説 ロンドン 五輪 出場 サッカー U - 22 日本 代表 二 次 予選 クウェート 一 カ国 ホーム アウェイ 戦 注目 の U - 22 代表 戦 11 試合 8 ゴール 日本 エース 永井 謙 佑 名古屋 グランパス 18 日 深夜 放送 TBS S - 1 50 m 5 . 8 秒 期待 俊足 FW ドン 永井 自分 みたい 足 の 高校 2 年 他 人事 よう 永井 スピード 伝説 高校 時代 友人 番組 カメラ 車 40 キロ あいつ 九州国際大 付属 高校 時代 恩師 杉山 公一 監督 自分 スルーパス 逆 サイド 子 オフサイド 自分 ドリブル こと 俊足 サッカー 選手 野人 岡野 雅行 有名 岡野 永井 苦笑い 高校 入学 時 小柄 足 永井 タイミング 彼 成長 身体 成長 トレーニング の 杉山 監督 同 高校 名物 坂道 階段 地獄 トレーニング 能力 開花 よう'

BoW に変換

全ての名詞を使用して辞書を作成した場合、使用される単語量が膨大になることが想定されます。(約 5 万単語) そのため、今回はエンコーディング時に引数 min_df を指定し、出現頻度が指定した値以下のものは取り扱わない設定を行います。

詳細に関してはこちらの公式ドキュメントを確認して下さい。

from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer(min_df=20)
x = vectorizer.fit_transform(word_collect)
x = x.toarray()
      
len(vectorizer.vocabulary_)
      
5795
len(x)
      
6505
x = x.astype('float32')
t = np.array(labels, 'int32')
      

モデルの定義と学習

本章で学んだ内容をもとに、文書分類を行いましょう。作成したデータセットを使用して、ニューラルネットワークの実装を行います。モデルの定義・学習・評価を行い、文書分類がどの程度の精度で行うことができているのか確認していきます。以下の手順を追っていきます。

  • 学習用データセットとテスト用データセットに分割
  • モデルの定義
  • モデルの学習
  • モデルの評価

学習用データセットとテスト用データセットの分割

from sklearn.model_selection import train_test_split
x_train, x_test, t_train, t_test = train_test_split(x, t, train_size=0.7, random_state=0)
      

モデルの定義

import tensorflow as tf
      
import os
import random

def reset_seed(seed=0):
    
    os.environ['PYTHONHASHSEED'] = '0'
    random.seed(seed)
    np.random.seed(seed)
    tf.random.set_seed(seed)
      

入力・出力層のノードの数は事前に定義しておきます。

_, n_input = x_train.shape
n_output = len(np.unique(t_test))
      
from tensorflow.keras import models, layers
      
reset_seed(0)

model = models.Sequential([
    layers.Dense(200, input_shape=(n_input, ), activation='relu'),
    layers.Dense(n_output, activation='softmax')
])

optimizer = tf.keras.optimizers.SGD(lr=0.01)
model.compile(loss='sparse_categorical_crossentropy', 
              optimizer=optimizer, 
              metrics=['accuracy'])
      

モデルの学習

history = model.fit(x_train, t_train,
          batch_size=100,
          epochs=20,
          verbose=1,
          validation_data=(x_test, t_test))
      
Train on 4553 samples, validate on 1952 samples Epoch 1/50 4553/4553 [==============================] - 1s 233us/sample - loss: 1.7347 - accuracy: 0.4208 - val_loss: 1.4385 - val_accuracy: 0.6434 Epoch 2/50 4553/4553 [==============================] - 1s 177us/sample - loss: 1.2397 - accuracy: 0.7529 - val_loss: 1.1054 - val_accuracy: 0.7997 Epoch 3/50 4553/4553 [==============================] - 1s 182us/sample - loss: 0.9525 - accuracy: 0.8460 - val_loss: 0.8908 - val_accuracy: 0.8407 Epoch 4/50 4553/4553 [==============================] - 1s 179us/sample - loss: 0.7651 - accuracy: 0.8774 - val_loss: 0.7479 - val_accuracy: 0.8632 Epoch 5/50 4553/4553 [==============================] - 1s 178us/sample - loss: 0.6384 - accuracy: 0.8955 - val_loss: 0.6516 - val_accuracy: 0.8791 Epoch 6/50 4553/4553 [==============================] - 1s 179us/sample - loss: 0.5486 - accuracy: 0.9082 - val_loss: 0.5821 - val_accuracy: 0.8888 Epoch 7/50 4553/4553 [==============================] - 1s 179us/sample - loss: 0.4820 - accuracy: 0.9185 - val_loss: 0.5306 - val_accuracy: 0.8934 Epoch 8/50 4553/4553 [==============================] - 1s 178us/sample - loss: 0.4302 - accuracy: 0.9275 - val_loss: 0.4905 - val_accuracy: 0.9001 Epoch 9/50 4553/4553 [==============================] - 1s 179us/sample - loss: 0.3885 - accuracy: 0.9350 - val_loss: 0.4579 - val_accuracy: 0.9027 Epoch 10/50 4553/4553 [==============================] - 1s 180us/sample - loss: 0.3544 - accuracy: 0.9400 - val_loss: 0.4322 - val_accuracy: 0.9073 Epoch 11/50 4553/4553 [==============================] - 1s 181us/sample - loss: 0.3258 - accuracy: 0.9436 - val_loss: 0.4104 - val_accuracy: 0.9109 Epoch 12/50 4553/4553 [==============================] - 1s 176us/sample - loss: 0.3015 - accuracy: 0.9484 - val_loss: 0.3918 - val_accuracy: 0.9119 Epoch 13/50 4553/4553 [==============================] - 1s 177us/sample - loss: 0.2806 - accuracy: 0.9517 - val_loss: 0.3756 - val_accuracy: 0.9129 Epoch 14/50 4553/4553 [==============================] - 1s 182us/sample - loss: 0.2622 - accuracy: 0.9552 - val_loss: 0.3616 - val_accuracy: 0.9150 Epoch 15/50 4553/4553 [==============================] - 1s 183us/sample - loss: 0.2460 - accuracy: 0.9574 - val_loss: 0.3510 - val_accuracy: 0.9144 Epoch 16/50 4553/4553 [==============================] - 1s 178us/sample - loss: 0.2315 - accuracy: 0.9609 - val_loss: 0.3401 - val_accuracy: 0.9165 Epoch 17/50 4553/4553 [==============================] - 1s 179us/sample - loss: 0.2186 - accuracy: 0.9635 - val_loss: 0.3304 - val_accuracy: 0.9150 Epoch 18/50 4553/4553 [==============================] - 1s 180us/sample - loss: 0.2068 - accuracy: 0.9677 - val_loss: 0.3232 - val_accuracy: 0.9165 Epoch 19/50 4553/4553 [==============================] - 1s 178us/sample - loss: 0.1963 - accuracy: 0.9690 - val_loss: 0.3163 - val_accuracy: 0.9191 Epoch 20/50 4553/4553 [==============================] - 1s 177us/sample - loss: 0.1865 - accuracy: 0.9712 - val_loss: 0.3077 - val_accuracy: 0.9206 Epoch 21/50 4553/4553 [==============================] - 1s 180us/sample - loss: 0.1777 - accuracy: 0.9732 - val_loss: 0.3012 - val_accuracy: 0.9252 Epoch 22/50 4553/4553 [==============================] - 1s 177us/sample - loss: 0.1696 - accuracy: 0.9743 - val_loss: 0.2945 - val_accuracy: 0.9273 Epoch 23/50 4553/4553 [==============================] - 1s 180us/sample - loss: 0.1620 - accuracy: 0.9758 - val_loss: 0.2907 - val_accuracy: 0.9262 Epoch 24/50 4553/4553 [==============================] - 1s 177us/sample - loss: 0.1550 - accuracy: 0.9769 - val_loss: 0.2852 - val_accuracy: 0.9278 Epoch 25/50 4553/4553 [==============================] - 1s 179us/sample - loss: 0.1485 - accuracy: 0.9802 - val_loss: 0.2812 - val_accuracy: 0.9288 Epoch 26/50 4553/4553 [==============================] - 1s 176us/sample - loss: 0.1425 - accuracy: 0.9802 - val_loss: 0.2772 - val_accuracy: 0.9303 Epoch 27/50 4553/4553 [==============================] - 1s 180us/sample - loss: 0.1369 - accuracy: 0.9811 - val_loss: 0.2725 - val_accuracy: 0.9303 Epoch 28/50 4553/4553 [==============================] - 1s 184us/sample - loss: 0.1316 - accuracy: 0.9829 - val_loss: 0.2694 - val_accuracy: 0.9329 Epoch 29/50 4553/4553 [==============================] - 1s 180us/sample - loss: 0.1267 - accuracy: 0.9833 - val_loss: 0.2656 - val_accuracy: 0.9334 Epoch 30/50 4553/4553 [==============================] - 1s 182us/sample - loss: 0.1220 - accuracy: 0.9842 - val_loss: 0.2632 - val_accuracy: 0.9329 Epoch 31/50 4553/4553 [==============================] - 1s 181us/sample - loss: 0.1177 - accuracy: 0.9862 - val_loss: 0.2601 - val_accuracy: 0.9355 Epoch 32/50 4553/4553 [==============================] - 1s 178us/sample - loss: 0.1136 - accuracy: 0.9866 - val_loss: 0.2572 - val_accuracy: 0.9349 Epoch 33/50 4553/4553 [==============================] - 1s 181us/sample - loss: 0.1097 - accuracy: 0.9875 - val_loss: 0.2554 - val_accuracy: 0.9344 Epoch 34/50 4553/4553 [==============================] - 1s 179us/sample - loss: 0.1061 - accuracy: 0.9881 - val_loss: 0.2529 - val_accuracy: 0.9349 Epoch 35/50 4553/4553 [==============================] - 1s 180us/sample - loss: 0.1027 - accuracy: 0.9888 - val_loss: 0.2503 - val_accuracy: 0.9365 Epoch 36/50 4553/4553 [==============================] - 1s 179us/sample - loss: 0.0994 - accuracy: 0.9892 - val_loss: 0.2489 - val_accuracy: 0.9360 Epoch 37/50 4553/4553 [==============================] - 1s 178us/sample - loss: 0.0964 - accuracy: 0.9903 - val_loss: 0.2462 - val_accuracy: 0.9370 Epoch 38/50 4553/4553 [==============================] - 1s 179us/sample - loss: 0.0934 - accuracy: 0.9908 - val_loss: 0.2437 - val_accuracy: 0.9370 Epoch 39/50 4553/4553 [==============================] - 1s 178us/sample - loss: 0.0906 - accuracy: 0.9921 - val_loss: 0.2425 - val_accuracy: 0.9380 Epoch 40/50 4553/4553 [==============================] - 1s 180us/sample - loss: 0.0880 - accuracy: 0.9925 - val_loss: 0.2407 - val_accuracy: 0.9380 Epoch 41/50 4553/4553 [==============================] - 1s 182us/sample - loss: 0.0855 - accuracy: 0.9925 - val_loss: 0.2387 - val_accuracy: 0.9395 Epoch 42/50 4553/4553 [==============================] - 1s 177us/sample - loss: 0.0831 - accuracy: 0.9928 - val_loss: 0.2372 - val_accuracy: 0.9401 Epoch 43/50 4553/4553 [==============================] - 1s 180us/sample - loss: 0.0808 - accuracy: 0.9930 - val_loss: 0.2372 - val_accuracy: 0.9390 Epoch 44/50 4553/4553 [==============================] - 1s 181us/sample - loss: 0.0786 - accuracy: 0.9934 - val_loss: 0.2351 - val_accuracy: 0.9395 Epoch 45/50 4553/4553 [==============================] - 1s 176us/sample - loss: 0.0765 - accuracy: 0.9934 - val_loss: 0.2336 - val_accuracy: 0.9406 Epoch 46/50 4553/4553 [==============================] - 1s 177us/sample - loss: 0.0746 - accuracy: 0.9936 - val_loss: 0.2323 - val_accuracy: 0.9416 Epoch 47/50 4553/4553 [==============================] - 1s 183us/sample - loss: 0.0726 - accuracy: 0.9939 - val_loss: 0.2313 - val_accuracy: 0.9416 Epoch 48/50 4553/4553 [==============================] - 1s 179us/sample - loss: 0.0708 - accuracy: 0.9941 - val_loss: 0.2299 - val_accuracy: 0.9416 Epoch 49/50 4553/4553 [==============================] - 1s 178us/sample - loss: 0.0691 - accuracy: 0.9941 - val_loss: 0.2289 - val_accuracy: 0.9416 Epoch 50/50 4553/4553 [==============================] - 1s 177us/sample - loss: 0.0674 - accuracy: 0.9943 - val_loss: 0.2277 - val_accuracy: 0.9416

モデルの評価

# 学習結果を取得
results = pd.DataFrame(history.history)
results.tail(3)
      
loss accuracy val_loss val_accuracy
47 0.070822 0.994070 0.229944 0.941598
48 0.069069 0.994070 0.228853 0.941598
49 0.067414 0.994289 0.227700 0.941598
# 損失を可視化
results[['loss', 'val_loss']].plot(title='loss')
plt.xlabel('epochs')
      
Text(0.5, 0, 'epochs')
<Figure size 432x288 with 1 Axes>
# 正解率を可視化
results[['accuracy', 'val_accuracy']].plot(title='metric')
plt.xlabel('epochs')
      
Text(0.5, 0, 'epochs')
<Figure size 432x288 with 1 Axes>
shareアイコン