テキスト分類 (PyTorch)

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

自然言語は今までのデータとは異なり、固定の長さのデータではなく、データごとに長さが異なるため、学習の際には固定の長さの数値に落とし込む方法を理解する必要があります。文章を品詞ごとに分割する形態解析、単語を数値化する手法の一つである 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 を確認することができます。

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

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, 12, 13 です。こちらをエンコーディング前の単語に当てはめると下記の単語を取得することができます。

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

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

文書分類の実装

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

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

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

!unzip -d text texts.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
      
# ディレクトリの取得
directories = glob('/content/text/*')
directories
      
['/content/text/peachy', '/content/text/topic-news', '/content/text/livedoor-homme', '/content/text/it-life-hack', '/content/text/movie-enter', '/content/text/kaden-channel', '/content/text/smax', '/content/text/sports-watch']

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

filepaths = glob('{}/*.txt'.format(directories[0]))
filepaths[:3]
      
['/content/text/peachy/peachy-5054325.txt', '/content/text/peachy/peachy-5971014.txt', '/content/text/peachy/peachy-4289213.txt']

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

with open(filepaths[0], encoding='utf-8') as f:
  text = ''.join(f.readlines()[2:])
print(text)
      
ジーンズは、ヘビーローテーション! 亀梨さんが『ベストジーニスト2010』受賞  “最もジーンズが似合う有名人”に贈られる「ベストジーニスト賞」。第27回目となる「ベストジーニスト2010」の発表会が5日、都内で行われました。一般投票では、「KAT-TUN」の亀梨和也さんと歌手の倖田來未さんが5年連続の受賞で、史上初めて男女同時に殿堂入りを果たしました。  また、主催者の日本ジーンズメーカー協議会が推薦・選出する協議会選出部門では、個性派俳優として人気の桐谷健太さん、「AKB48」の板野友美さん、タレントのはるな愛さんが選ばれました。  今日は普段着だという亀梨さんは、今年も受賞できるかプレッシャーはなかったのかと質問されると、「この時期になるとそわそわしました」と胸の内を明かしてくれました。そして、今日履いているジーンズについては「結構お気に入りで、ヘビーローテーションです」と、「AKB48」の歌のタイトルで受け答えするも、伝わらずすべってしまっていました。それでも、クルッと一周しお尻を会場に向けてジーンズをアピールすると、思わぬパフォーマンスに会場からは、キャーという声援があがっていました。  それに対して、かつてトロフィーや盾を一度ももらったことがないという桐谷さんは、シンプルな着こなしで、シャツを羽織ジーンズを格好良く着こなしていましたが、クルッと一周しても亀梨さんと対照的に全く歓声があがらず仕舞…。ガックリと肩を落としていました。  小学生の頃にはケミカルウォッシュの上下を愛着していたというほど、ジーンズが大好きな桐谷さんは、大好きな旅に出かける時にも、基本はボロボロになっても平気なジーンズで行き、旅から戻ってくるとジーンズにその国の匂いがついていたりするのが好きだと話してくれました。  受賞したみなさんは、ジーンズをファッションアイテムとしてだけではなく、人生や個性を主張するアイテムとしても自然と取り入れているのがわかりました。亀梨さんや桐谷さんのように、ナチュラルに着こなしたり、今年流行りのファーアイテムを上手にポイント使いしていた、倖田さんと板野さんのように、自分らしく着崩してみたり、あなた次第でジーンズとの付き合い方の幅が広がるかもしれませんね! 【男性】 1 亀梨和也(KAT-TUN) 29,118票 2 相葉雅紀(嵐) 10,696票 3 ジェジュン(東方神起) 3,932票 4 山下智久(NEWS) 1,872票 5 岩沢厚治(ゆず) 1,696票 6 氷川きよし 1,458票 7 松本潤(嵐) 1,186票 8 大野智(嵐) 973票 9 赤西仁 908票 10 内博貴 672票 【女性】 1 倖田來未 12,961票 2 aiko 5,456票 3 安室奈美恵 5,219票 4 黒木メイサ 4,935票 5 ベッキー 3,580票 6 北川景子 3,056票 7 香里奈 2,066票 8 木村カエラ 1,905票 9 仲間由紀恵 1,796票 10 土屋アンナ 1,321票 投票総数:72,153票 ■関連リンク Best-Jeans.com - 公式サイト

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

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

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

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

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]
      
'ジーンズは、ヘビーローテーション!\u3000亀梨さんが『ベストジーニスト2010』受賞\n\u3000“最もジーンズが似合う有名人”に贈られる「ベストジーニスト賞」。第27回目となる「ベストジーニスト2010」の発表会が5日、都内で行われました。一般投票では、「KAT-TUN」の亀梨和也さんと歌手の倖田來未さんが5年連続の受賞で、史上初めて男女同時に殿堂入りを果たしました。\n\n\u3000また、主催者の日本ジーンズメーカー協議会が推薦・選出する協議会選出部門では、個性派俳優として人気の桐谷健太さん、「AKB48」の板野友美さん、タレントのはるな愛さんが選ばれました。\n\n\u3000今日は普段着だという亀梨さんは、今年も受賞できるかプレッシャーはなかったのかと質問されると、「この時期になるとそわそわしました」と胸の内を明かしてくれました。そして、今日履いているジーンズについては「結構お気に入りで、ヘビーローテーションです」と、「AKB48」の歌のタイトルで受け答えするも、伝わらずすべってしまっていました。それでも、クルッと一周しお尻を会場に向けてジーンズをアピールすると、思わぬパフォーマンスに会場からは、キャーという声援があがっていました。\n\n\u3000それに対して、かつてトロフィーや盾を一度ももらったことがないという桐谷さんは、シンプルな着こなしで、シャツを羽織ジーンズを格好良く着こなしていましたが、クルッと一周しても亀梨さんと対照的に全く歓声があがらず仕舞…。ガックリと肩を落としていました。\n\n\u3000小学生の頃にはケミカルウォッシュの上下を愛着していたというほど、ジーンズが大好きな桐谷さんは、大好きな旅に出かける時にも、基本はボロボロになっても平気なジーンズで行き、旅から戻ってくるとジーンズにその国の匂いがついていたりするのが好きだと話してくれました。\n\n\u3000受賞したみなさんは、ジーンズをファッションアイテムとしてだけではなく、人生や個性を主張するアイテムとしても自然と取り入れているのがわかりました。亀梨さんや桐谷さんのように、ナチュラルに着こなしたり、今年流行りのファーアイテムを上手にポイント使いしていた、倖田さんと板野さんのように、自分らしく着崩してみたり、あなた次第でジーンズとの付き合い方の幅が広がるかもしれませんね!\n\n【男性】\n1\u3000亀梨和也(KAT-TUN)\u300029,118票\n2\u3000相葉雅紀(嵐)\u300010,696票\n3\u3000ジェジュン(東方神起)\u30003,932票\n4\u3000山下智久(NEWS)\u30001,872票\n5\u3000岩沢厚治(ゆず)\u30001,696票\n6\u3000氷川きよし\u30001,458票\n7\u3000松本潤(嵐)\u30001,186票\n8\u3000大野智(嵐)\u3000973票\n9\u3000赤西仁\u3000908票\n10\u3000内博貴\u3000672票\n\n【女性】\n1\u3000倖田來未\u300012,961票\n2\u3000aiko\u30005,456票\n3\u3000安室奈美恵\u30005,219票\n4\u3000黒木メイサ\u30004,935票\n5\u3000ベッキー\u30003,580票\n6\u3000北川景子\u30003,056票\n7\u3000香里奈\u30002,066票\n8\u3000木村カエラ\u30001,905票\n9\u3000仲間由紀恵\u30001,796票\n10\u3000土屋アンナ\u30001,321票\n\n投票総数:72,153票\n\n■関連リンク\n Best-Jeans.com - 公式サイト\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]
      
'ジーンズ ヘビー ローテーション 亀梨 さん ベストジーニスト 2010 受賞 ジーンズ 有名人 ベストジーニスト 賞 27 回 目 ベストジーニスト 2010 発表 会 5 日 都内 一般 投票 KAT - TUN 亀梨 和也 さん 歌手 倖田 來未 さん 5 年 連続 受賞 史上 男女 殿堂 入り 主催 者 日本 ジーンズ メーカー 協議 会 推薦 選出 協議 会 選出 部門 個性 派 俳優 人気 桐谷 健太 さん AKB 48 板野 友美 さん タレント 愛 さん 今日 普段着 亀梨 さん 今年 受賞 プレッシャー の 質問 時期 胸 内 今日 ジーンズ お気に入り ヘビー ローテーション AKB 4 8 歌 タイトル 受け答え クルッ 一周 お 尻 会場 ジーンズ アピール パフォーマンス 会場 声援 それ トロフィー 盾 一 度 こと 桐谷 さん シンプル 着こなし シャツ 羽織 ジーンズ 格好 クルッ 一周 亀梨 さん 対照 的 歓声 仕舞 肩 小学生 頃 ケミカルウォッシュ 上下 愛着 ジーンズ 大好き 桐谷 さん 大好き 旅 時 基本 ボロボロ 平気 ジーンズ 旅 ジーンズ 国 匂い の 好き 受賞 みなさん ジーンズ ファッション アイテム 人生 個性 主張 アイテム 自然 の 亀梨 さん 桐谷 さん よう ナチュラル 今年 ファー アイテム 上手 ポイント 使い 倖田 さん 板野 さん よう 自分 着 あなた 次第 ジーンズ 付き合い 方 幅 男性 1 亀梨 和也 KAT - TUN 29 , 118 票 2 葉 雅紀 嵐 10 , 696 票 3 ジェジュン 東方 神 起 3 , 932 票 4 山下 智久 NEWS 1 , 872 票 5 岩沢 厚 治 ゆず 1 , 696 票 6 氷川 1 , 458 票 7 松本 潤 嵐 1 , 186 票 8 大野 智 嵐 973 票 9 赤 西 仁 908 票 10 内 博 672 票 女性 1 倖田 來未 12 , 961 票 2 aiko 5 , 456 票 3 安室 奈美恵 5 , 219 票 4 黒木 メイサ 4 , 935 票 5 ベッキー 3 , 580 票 6 北川 景子 3 , 056 票 7 香里奈 2 , 066 票 8 木村 カエラ 1 , 905 票 9 仲間 由紀恵 1 , 796 票 10 土屋 アンナ 1 , 321 票 投票 総数 72 , 153 票 関連 リンク Best - Jeans . com - 公式 サイト'

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

PyTorchで取り扱えるデータ形式に変換

機械学習で扱える形式でデータセットを準備できたので、PyTorch で取り扱えるデータセットの形式への変換まで行っておきましょう。

import torch
      

PyTorch用に torch.tensor形式へ変換を行います。

# tensor形式へ変換
x = torch.tensor(x, dtype=torch.float32)
t = torch.tensor(labels, dtype=torch.int64)
      
x.shape,t.shape
      
(torch.Size([6505, 5795]), torch.Size([6505]))
type(x), x.dtype
      
(torch.Tensor, torch.float32)
type(t), t.dtype
      
(torch.Tensor, torch.int64)
import torch.utils.data
      

次に、データセットの作成を行いましょう。

dataset = torch.utils.data.TensorDataset(x, t)
dataset
      
<torch.utils.data.dataset.TensorDataset at 0x7f7ced8e61d0>
# 全サンプル数
len(dataset)
      
6505

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

n_train = int(len(dataset) * 0.6)
n_val = int(len(dataset) * 0.2)
n_test = len(dataset) - n_train - n_val
      
# データの分割
torch.manual_seed(0)

train, val, test = torch.utils.data.random_split(dataset, [n_train, n_val, n_test])
      
len(train), len(val), len(test)
      
(3903, 1301, 1301)

モデルの定義と学習

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

モデルの定義から学習までの一連の流れはこれまでと同じです。

!pip install pytorch_lightning
      
import torch.nn as nn
import torch.nn.functional as F

import pytorch_lightning as pl
from pytorch_lightning import Trainer
      
class TrainNet(pl.LightningModule):

    @pl.data_loader
    def train_dataloader(self):
        return torch.utils.data.DataLoader(train, self.batch_size, shuffle=True, num_workers=self.num_workers)

    def training_step(self, batch, batch_nb):
        x, t = batch
        y = self.forward(x)
        loss = self.lossfun(y, t)
        results = {'loss': loss}
        return results
      
class ValidationNet(pl.LightningModule):

    @pl.data_loader
    def val_dataloader(self):
        return torch.utils.data.DataLoader(val, self.batch_size, num_workers=self.num_workers)

    def validation_step(self, batch, batch_nb):
        x, t = batch
        y = self.forward(x)
        loss = self.lossfun(y, t)
        y_label = torch.argmax(y, dim=1)
        acc = torch.sum(t == y_label) * 1.0 / len(t)
        results = {'val_loss': loss, 'val_acc': acc}
        return results

    def validation_end(self, outputs):
        avg_loss = torch.stack([x['val_loss'] for x in outputs]).mean()
        avg_acc = torch.stack([x['val_acc'] for x in outputs]).mean()
        results = {'val_loss': avg_loss, 'val_acc': avg_acc}
        return results
      
class TestNet(pl.LightningModule):

    @pl.data_loader
    def test_dataloader(self):
        return torch.utils.data.DataLoader(test, self.batch_size, num_workers=self.num_workers)

    def test_step(self, batch, batch_nb):
        x, t = batch
        y = self.forward(x)
        loss = self.lossfun(y, t)
        y_label = torch.argmax(y, dim=1)
        acc = torch.sum(t == y_label) * 1.0 / len(t)
        results = {'test_loss': loss, 'test_acc': acc}
        return results

    def test_end(self, outputs):
        avg_loss = torch.stack([x['test_loss'] for x in outputs]).mean()
        avg_acc = torch.stack([x['test_acc'] for x in outputs]).mean()
        results = {'test_loss': avg_loss, 'test_acc': avg_acc}
        return results
      
class Net(TrainNet, ValidationNet, TestNet):

    def __init__(self, batch_size=128, num_workers=8):
        super(Net, self).__init__()
        self.batch_size = batch_size
        self.num_workers = num_workers
        self.fc1 = nn.Linear(5795, 200)
        self.fc2 = nn.Linear(200, 9)

    def lossfun(self, y, t):
        return F.cross_entropy(y, t)

    def configure_optimizers(self):
        return torch.optim.Adam(self.parameters(), lr=0.01)

    def forward(self, x):
        x = self.fc1(x)
        x = F.relu(x)
        x = self.fc2(x)
        return x
      

モデルの学習

# cuDNN に対する再現性の確保
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
      
# 乱数のシードを固定
torch.manual_seed(0)

net = Net(batch_size=128)
trainer = Trainer(gpus=1, max_nb_epochs=20)

# モデルの学習
trainer.fit(net)
      
# 最終的なエポックの検証データに対する結果
trainer.callback_metrics
      
{'epoch': 19, 'loss': 0.0003264896513428539, 'val_acc': 0.9309727549552917, 'val_loss': 0.32949724793434143}
# テストデータに対する検証
trainer.test()

# テストデータに対する結果
trainer.callback_metrics
      
HBox(children=(IntProgress(value=0, description='Testing', layout=Layout(flex='2'), max=11, style=ProgressStyl…
---------------------------------------------------------------------------------------------------- TEST RESULTS {} ----------------------------------------------------------------------------------------------------
{'epoch': 19, 'loss': 0.0003264896513428539, 'test_acc': 0.932325541973114, 'test_loss': 0.36444222927093506, 'val_acc': 0.9309727549552917, 'val_loss': 0.32949724793434143}
shareアイコン