アラカン"BOKU"のITな日常

文系システムエンジニアの”BOKU”が勉強したこと、経験したこと、日々思うことを書いてます。

日本語文章の形態素解析と単語数のカウントで学習・評価で使えるデータに変換-ファーストチャレンジ/使い方26

ニューラルネットワークコンソール(Neural Network Console 以後 NNCと略して書きます)では、文章テキストデータは、そのままでは学習データとしては使えません。 

オリジナルデータとして使えるのは「おおむね-1.0~1.0の間に正規化した数値データ」だけだからです。 

公式ドキュメントに明記されています。

データセットの準備 – Docs - Neural Network Console」

データCSVファイルはヘッダを持たず、数値セルのみで構成されます。Neural Network ConsoleはCSVファイルを要素数が (行数,列数)である配列として扱います。実数値はおおむね-1.0~1.0の間に収まるように事前に加工しておく必要があります。

 

 じゃあテキストデータを利用して学習・推論するのは100%無理なのか? 

いやいや、そういう訳ではないです。 

ただ、データ・セットを作るときに一手間が必要だというだけです。 

つまり、文章テキストデータを数値化(符号化)してやる必要があるのです。 

ということで、今回からしばらく、「NNCで自然言語の文章を分類する」というテーマで、試行錯誤してみようかな。

 

文章テキストデータを数値化するってどういうことか?

 

正直いろんなやり方はあります。 

一番シンプルなやり方は単語の出現回数をもとにする方法です。 

まず、これからやってみます。 

といっても、わかりづらいと思うので、例を書いてみます。 

例えば、”あなたが噛んだ小指が痛い。"・・という文章があったとします。(※我ながら古い・・) 

これをまず「あなた」「が」「噛んだ」「小指」「が」「痛い」みたいに単語に分解します。 

このうちで「が」みたいに名詞とか動詞以外の単語は、文章の意味にとって対して重要でないので取り去ります。 

そして{あなた:0、噛んだ:1、小指:2、痛い:4}みたいな辞書を作るわけです。 

実際の辞書はもっとたくさんの文章で作るんですけどね。 

その辞書をもとにして、”あなたが叩いた親指が痛い”みたいな文章に対して、辞書の各単語の出現回数をカウントすると[ 1 0 0 1]みたいなデータになります。 

さっきの{あなた:0、噛んだ:1、小指:2、痛い:4}はリストの中の引数(位置)を表しているので、[ 1 0 0 1]というのは、[あなたが1回、噛んだは0回、小指が0回、痛いが1回」でてきたよということです。 

そして、今度はこれらの単語に重みをつけます。 

ここでの重みというのは、それが文章を特徴づける単語として重要視すべき度合い・・という様なことを表します。 

ざっくり言えば、100の文章に横断的に頻繁に登場する単語は、その中の文章のひとつを特徴づける単語としては重みが軽く、その文章だけに登場する単語は、ひとつの文章を特徴できる単語として重要(重い)みたいな判断の仕方です。 

それを計算で求めるのが、TF-IDF(Term Frequency-Inverse Document Frequency)と呼ばれる方法です。(計算式は省略しときます) 

これをすると、上記が[3.39  0   0   1.12] みたいに同じ回数でも重みが異なるデータになります。 

 最後は、これを、「概ね -1.0 ~ 1.0」の範囲に収まるように「正規化」ということをやります。 

かんたんに言えば、各値を合計で割るわけですけどね。 

ただ、マイナスの値とかもあるので、そのまま足さず、二乗して(つまりマイナスを消して)合計して、平方根をとる(二乗してるから元にもどす)という手順で求めるわけです。 

ここまでやると、NNCで使える数値データにテキストが変換できたことになります。 

この一連のやり方は数学用語を使うとコンパクトに「テキストを形態素解析して分かち書きに変換後、ベクター化して、TF-IDF処理をしたあとにL2正規化する」といえるらしいのですが、文系の自分には、なんかピンとこないのですよね。

 

pythonのソースで試すために必要なパッケージをインストール

 

準備として、必要なパッケージをインストールします。 

すでにインストールされている場合は必要ありませんけど。 

ちなみに、動作確認した環境は、Windows10 + anaconda(python3.6)です。 

まず、scikit-learn。

conda install scikit-learn

 

続いて、janome

pip install janome

 

学習用・評価用のテキストデータを準備する

 

今回は簡単にするために、ネガ・ポジ判断にしてみます。 

文章があって、それがネガティブなのかポジティブなのかを判断してみます。 

スパムフィルターなんかと同じですね。 

あれは、スパムか否かという2択のネガポジ判断をしているわけなので。 

元になる言葉は、インターネットの名言集とか一言集からひろってきます。 

それらに対して、ポジティブ(0)か、ネガティブ(1)かの正解を割り当てます。 

作成したデータの一部はこんな感じ。

ポジティブな言葉の例

いまの僕には勢いがある。新しいことを始めてもうまくいきそうな気がする。,0
希望のために扉はいつも開けておきましょう,0
楽しいから笑うのではない。笑うから楽しいのだ,0
幸せとは、健康で記憶力が悪いということだ,0
現在から、未来は生まれ落ちる,0
食べ物に対する愛より誠実な愛はない,0
笑われて笑われてつよくなる,0
元気が一番、元気があれば何でもできる,0

 

ネガティブな言葉の例

僕には勢いがない。新しいことをやってもうまくいかない気がする。,1
希望を持っても仕方ない,1
楽しくもないのに笑えないよね,1
辛い記憶ばっかり思い出す,1
未来よりも過去を振り返って悔やむことが多い,1
愛なんて信じないよ,1
笑われると落ち込む,1
 

 

こういうのを200個くらい集めて、学習用と評価用にふりわけてCSVファイルとして保存します。

 

CSVファイルから、NNC用のDATASETを作成する

 準備した元ネタCSVファイルを読み込んで、NNC用のデータ・セットを作成するのはpythonのプログラムでやります。 

NNCで処理するデータ・セットは2種類のCSVを作る必要があります。 

データ・セットCSVは、実データの場所と正解ラベルを示すものです。 

例えば、こんな感じ。

x:image,y:label
.\0\0.csv,0
.\1\1.csv,1
.\0\2.csv,0
.\1\3.csv,1

 

1行目を例にすると、 カレントフォルダの0 または 1という名前のフォルダの下の0.csvは0(ポジティブ)ですよ・・みたいな意味です。 

それでデータの実態は、.\0\0.csv に作成するわけです。 

もとデータを読み込んで、NNC用のデータCSVとデータセットCSVを生成するpythonプログラムの全文は以下です。

 

import os
import csv
import re
import numpy as np
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
from janome.tokenizer import Tokenizer


# 元になるCSVファイルを読み込んでリストに変換する
def get_input_csv(csvfile):
    with open(csvfile,'r',encoding='utf8') as csvf:
       csvreader = csv.reader(csvf)
       return list(csvreader)

# NNC用のデータ・セットCSVを生成する
def make_dataset_csv(l_in,s_outputfile):
    with open(s_outputfile,'w',newline='',encoding='utf8') as outf:
        csvwriter = csv.writer(outf)
        # ヘダーを出力する
        csvwriter.writerow(['x:image','y:label'])
        for i in range(len(l_in)):
            # データCSVのパス
            s_path = '.\\' + l_in[i][1] + '\\' + str(i) + '.csv'
            # 正解ラベル
            csvwriter.writerow([s_path,str(l_in[i][1])])
    outf.close()
    
# NNC用に数値データに変換したデータを1行ずつ別ファイルにして保存する
def make_data_csv(l_in):
    s_pat = r'名詞|動詞|形容詞'
    o_reg = re.compile(s_pat)
    na_ar = np.array([])
    l_dir = []
    # 形態素解析・・単語に分割する
    o_t = Tokenizer()
    # 単語辞書を生成し、各単語の出現数をカウント
    o_cv = CountVectorizer()
    
# 単語の出現頻度から重みつけをする
    tfidf = TfidfTransformer(use_idf=True, norm='l2', smooth_idf=True)
    for i in range(len(l_in)):
        # 1行分の単語を分割
        o_malist = o_t.tokenize(l_in[i][0])
        l_line = []
        # あとで保存するフォルダは正解データによって変える
        l_dir.append(str(l_in[i][1]))
        for n in o_malist:
            
# 名詞・動詞・形容詞のみを対象にする
            if(o_reg.match(n.part_of_speech)):
                # 分割した単語を1行分リストに加えていく
                l_line.append(n.surface)
        # 空白で分かち書きして単語を1行に組み立て直す
        na_ar = np.append(na_ar,np.array(' '.join(l_line)))
    # 辞書を生成し、単語ごとにカウントし、重み付けをしてL2正規化する
    t_bag = tfidf.fit_transform(o_cv.fit_transform(na_ar))
    a_tred = t_bag.toarray()
    # 1行分ずつCSVファイルに保存する。フォルダは正解ラベル毎に念のためわけておく
    for t in range(len(a_tred)):
        os.makedirs(l_dir[t],exist_ok=True)
        with open('.\\' + l_dir[t] + '\\' + str(t) + '.csv','w',newline='',encoding='utf8') as df:
            cw = csv.writer(df)
            cw.writerow(a_tred[t])
        df.close()

l_csvdata = get_input_csv('negapo.csv')
make_dataset_csv(l_csvdata,'n.csv')
make_data_csv(l_csvdata)

 

例によって、自分はIDLEでソース表示させて「F5」キーで実行・・というやり方が好きなので、CSVファイル名とかパラメータにせずに、直書きしてます。 

また、ちょっとでもエラーがあれば、例外で落ちてくれる方が良いので、あえて例外の処理とかしてません。 

なので、そのへんが気になる方は手をいれてくださいね。

 

ソースのポイントになるところを補足説明します

 

まず、前の説明で以下のように書いた部分です。 

「これをまず「あなた」「が」「噛んだ」「小指」「が」「痛い」みたいに単語に分解します。このうちで「が」みたいに名詞とか動詞以外の単語は、文章の意味にとって対して重要でないので取り去ります。」 

これをやっているソースは以下です。

 
        o_t = Tokenizer()
        # 1行分の単語を分割
        o_malist = o_t.tokenize(l_in[i][0])
        l_line = []
        for n in o_malist:
      # 名詞・動詞・形容詞のみを対象にする
            if(o_reg.match(n.part_of_speech)):
                # 分割した単語を1行分リストに加えていく
                l_line.append(n.surface)
        # 空白で分かち書きして単語を1行に組み立て直す
        na_ar = np.append(na_ar,np.array(' '.join(l_line)))

 

単語分割(形態素解析)して、必要な品詞だけ抜く部分については、詳しく以下の記事にも書いてますので、興味があれば参照ください。

arakan-pgm-ai.hatenablog.com

 

そして、前の説明で以下のように書いた部分です。(一部抜粋) 

「そして{あなた:0、噛んだ:1、小指:2、痛い:4}みたいな辞書を作るわけです。その辞書をもとにして、”あなたが叩いた親指が痛い”みたいな文章に対して、辞書の各単語の出現回数をカウントすると[ 1 0 0 1]みたいなデータになります。そして、今度はこれらの単語に重みをつけます。それを計算で求めるのが、TF-IDF(Term Frequency-Inverse Document Frequency)と呼ばれる方法です。 最後は、これを、「概ね -1.0 ~ 1.0」の範囲に収まるように「正規化」ということをやります。」 

これをソースで書くと・・・。 

    # 単語辞書を生成し、各単語の出現数をカウント
    o_cv = CountVectorizer()
    # 単語の出現頻度から重みつけをする
    tfidf = TfidfTransformer(use_idf=True, norm='l2', smooth_idf=True)
    # 辞書を生成し、単語ごとにカウントし、重み付けをしてL2正規化する
    t_bag = tfidf.fit_transform(o_cv.fit_transform(na_ar))

 

 なんと、3行で終わりです。

 

scikit-learnというのは、ほんとに有り難いライブラリですねえ。 

上記の動きを実際に見るとどうなるか。 

もとの文章がこうだった場合。

いまの僕には勢いがある。新しいことを始めてもうまくいきそうな気がする。

 

これを形態素解析して、名詞・動詞・形容詞だけにして、分かち書きにした結果はこんな感じになります。

いま 僕 勢い ある 新しい こと 始め うまく いき そう 気 する

 

そして、 「tfidf.fit_transform(o_cv.fit_transform(na_ar))」を通した結果です。

[0. 0.3060892 0. 0. 0. 0.35725883
0. 0.35725883 0. 0.32732652 0. 0.
0. 0. 0. 0. 0. 0.
0. 0.21907835 0. 0. 0. 0.
0. 0.26477716 0.32732652 0. 0. 0.]

 

この最後のアウトプットを、「cw.writerow(a_tred[t])」で1行のCSVデータにして、ファイルに保存すれば、「いまの僕には勢いがある。新しいことを始めてもうまくいきそうな気がする。」という文章をNNCで使える数値データに変換したもの・・になるわけですね。

 

ブログが長くなったので続きにします。

 

さて、この後はこれで生成したデータをNNCにセットして学習・評価を行うわけですが、ちょっと長くなりすぎるので、続きは次回にします。 

ではでは。

 

2018/02/04追記

念のために、補足します。

この記事で紹介している方法は汎用性はあまりありません。

学習データに出現する単語数によって、列数が増減しますから・・。

あくまで、「自然言語をNNCで学習可能なデータに落とし込むひとつの方法論を勉強を兼ねてやってみる」という感じで捉えてもらえると良いかなと思います。

 

2018/02/15追記

上記の欠点を改善した汎用性のある方法論で、再度データ・セット作りをやってみました。こちらです。

arakan-pgm-ai.hatenablog.com

 

f:id:arakan_no_boku:20171115215731j:plain