テキスト分類 (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)
print()
関数を使用しているため、見た目上はきれいに表示されていますが、形態素解析した結果は下記のように、エスケープシーケンス(改行などを表す特殊な文字列)を多く含みます。
res
このように分割されたテキストデータはエスケープシーケンスを取り除き、単語ごとに抽出を行う処理を施す必要があります。
形態素解析を行い、単語ごとに文章を分割ができました。次のステップは分割された単語を数値に変換していく事になります。
名詞の抽出
本章で取り組む文書分類の問題設定をここで確認しておきましょう。分類を行う文書は下記の 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)
形態素解析を行ったテキストデータは下記のようにエスケープシーケンスが含まれていました。
それぞれを改行 (\n) で分割を行います。特定の文字列でテキストデータを分けるときは、split()
メソッドを使用します。
res
# 改行ごとに分割し、リストに格納
res.split('\n')
最後 2 行に EOS (End Of Sentence) と空白が入ってしまっているため、最後 2 つの要素までをスライスして使用します。
# 最後の 2 つの要素までをスライス
res.split('\n')[:-2]
次に、一番最初の要素に対して、タブ (\t
) で分割を行います。
# 1 つ目の単語をスライス
res.split('\n')[0]
こちらの結果から名詞であるかどうかを判定するためには、左から 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
自然言語の特徴量変換
自然言語の特徴量変換には様々な手法が存在します。その方法は多種多様であり、どの手法を用いるかは問題設定に応じて変更する必要があります。下記は基礎的なテキストデータのエンコーディング方法(ベクトル化)になります。
- Bag of Words (Count encoding)
- tf-idf
- One-hot encoding
- Word2Vec
本章ではエンコーディング手法の中で最もシンプルなものの 1 つである Bag of Words を用いてのエンコーディング方法を確認します。
Bag of Words の概要
Bag of Words (以下 BoW)とは、単語の出現回数によって単語を数値に変換する方法です。
次の 3 つの文章を BoW を用いてエンコーディングを行った場合の結果を確認しましょう。
- 私は電車が好きです。
- 電車より車をよく使います。
- 好きな果物はりんごです。
3 つの文に出現する単語をすべて羅列します。この出現する単語から重複を取り除いたものを辞書 (dictionary) と呼びます。
[ 私 は 電車 が 好き です より 車 を よく 使い ます な 果物 りんご ]
各文章に対し、羅列した単語を出現数に変換します。
- [ 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 ]
- [ 0 0 1 0 0 0 1 1 1 1 1 1 0 0 0 ]
- [ 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_
エンコーディング後の数値は toarray()
メソッドを使用して取得します。
x = x.toarray()
print(x)
エンコーディング後の数値を確認して、エンコーディングが想定通りなされているか確認しましょう。
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ライフハック | 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
入力変数と目的変数の作成を行う方法を順を追って確認を行います。取得したディレクトリ名の中から 1 つ目の要素を取り出し中身の確認を行います。
filepaths = glob('{}/*.txt'.format(directories[0]))
filepaths[:3]
1 つ目のディレクトリ内のテキストファイル名の取得を行うことができました。ファイルの展開には with
構文を用います。読み込んだ際に [2:]
で空白と日付が含まれる最初の 2 つの要素を除外しています。
with open(filepaths[0], encoding='utf-8') as f:
text = ''.join(f.readlines()[2:])
print(text)
取り出したこの 1 テキストファイルが入力変数になります。このテキストデータからこの記事がどのカテゴリに属するのかの分類を行います。
各ディレクトリに対応する要素番号を分類に使用するラベルとして使用します。enumerate()
を使うことで、for 文を用いて繰り返しを実行する際に要素番号も併せて取得することができます。
for (i, directory) in enumerate(directories):
print(i, directory)
print('- - -')
全てのディレクトリ・テキストファイルを読み込み、同時にラベル付けも行います。
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)
texts[0]
labels[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]
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_)
len(x)
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
type(x), x.dtype
type(t), t.dtype
import torch.utils.data
次に、データセットの作成を行いましょう。
dataset = torch.utils.data.TensorDataset(x, t)
dataset
# 全サンプル数
len(dataset)
学習用データセットとテスト用データセットの分割
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)
モデルの定義と学習
本章で学んだ内容をもとに、文書分類を行いましょう。作成したデータセットを使用して、ニューラルネットワークの実装を行います。モデルの定義・学習・評価を行い、文書分類がどの程度の精度で行うことができているのか確認していきます。
モデルの定義から学習までの一連の流れはこれまでと同じです。
!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
# テストデータに対する検証
trainer.test()
# テストデータに対する結果
trainer.callback_metrics