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

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

日本語文章をNeural Network Console用データに変換する(文字コード出現頻度編)-OK版/使い方28

前々回から、自然言語の文章をNeural Network Console(ニューラルネットワークコンソール 以後NNCと書きます)で分類可能なデータにする方法を試行錯誤してます。 

前回までで、暫定版として単語の出現回数カウントで数値化(符号化)する方法でやってみました。

arakan-pgm-ai.hatenablog.com

 

わりと、本とかでも紹介されたりしてる一般的なやり方ではあります。 

そこそこ、良い感じで分類もできてます。 

でも、暫定版なんです。 

NNC用のデータセットにする方法としては問題があります。 

それは、学習データに出現する単語の数によって、列数(次元)が変動してしまうリスクがあるからです。 

出現する単語の数によって、列数(次元)が増減する。 

これはNNCで扱うにはちょっと厳しいです。 

都度inputの値を変更するなどの手間がかかるので面倒くさいですからね。 

ということで、今回は、文章に登場する単語の数や文章の長短に影響されず、安定したNNCのデータ・セットに変換する方法を検討してみます。 

まだ、思い付きレベルの試行ですが、何事も、まず、やってみないと・・ですね。

 

言葉を単位にしないアプローチを考えてみる

 

とにかく。 

文章に登場する単語の増減に関係なく列数(次元)を固定にする。 

文章の長短(全体の文字数)が異なるを同一基準で比較することを可能にする。 

この2つが何よりも重要なことです。 

しかし、後者はなんとかなっても、前者(列数を固定)は普通に考えると無理です。 

言葉(単語)は無限にありえますからね。 

だから、列数を固定にする・・となると、言葉(単語)を単位にはできない。 

ということになります。 

有限個であらゆる文章をカバーできるのは、なんだろう? 

それは「文字コード=16進数」になるんだろうな・・・ 

ということで。 

今回は汎用版として、「00」~「FF」の256パターンが文章中に出現する比率を求めて、1行256列のデータに変換してみます。

 

コードを単位にする方法を整理してみる

 

さてコード(16進数)を単位して、列数(次元)を、'00'~'FF'までの256列(次元)でやってみると決めました。 

それ以外の決め事は、以下のような感じでいきます。

  • 文字コードUTF-8固定にする
  • 空白(全角・半角)とタブ文字はカウントしない。
  • 全角・半角の記号もカウントしない。
  • それ以外の文字は品詞に関わりなくカウントの対象とする。

基本のコードは、前回言葉を単位に処理したものをベースにします。

arakan-pgm-ai.hatenablog.com

 

コードを単位にNNCデータに変換するpythonソース 

メインのコード部分です。

import os
import csv
import re
import codecs
import numpy as np
from janome.tokenizer import Tokenizer

# CSVにテキストファイル(フルパス)とラベルを書いている前提
def make_data(textfile,s_outputfile):
    l_in = get_input_csv(textfile)
    outf = open(s_outputfile,'w',newline='',encoding='utf8')
    csvwriter = csv.writer(outf)
    csvwriter.writerow(['x:image','y:label'])
    for i,l_p in enumerate(l_in):
        l_text = get_input_txt(l_p[0])
        s_path = '.\\' + l_p[1] + '\\' + str(i) + '.csv'
        csvwriter.writerow([s_path,l_p[1]])
        make_data_csv(l_text,l_p[1],i)
    outf.close()

この関数の使い方はこうです。

 make_data('file_list.csv','0_result.csv')

 

file_result.csv は、学習対象のテキストファイルのパスと、正解ラベルを書いたCSVファイルです。

内容例

C:/data/sports/sports-watch-4597641.txt,0
C:/data/sports/sports-watch-4601248.txt,0
C:/data/sports/sports-watch-4604621.txt,0
C:/data/sports/sports-watch-4609913.txt,0
C:/data/sports/sports-watch-4612295.txt,0
C:/data/sports/sports-watch-4623406.txt,0

 

学習するデータは、「livedoor ニュースコーパス」の一部を使います。

arakan-pgm-ai.hatenablog.com

 

様々なジャンルのニュースがありますが、今回は「sports-watch」901ファイルと、「peachy」(女性向けの記事みたい)843ファイルを学習と推論に使います。 

「sports-watch」を分類0、「peachy」を分類1にして、140ファイルくらいを推論用、残りを学習用にします。 

ファイル単位にわかれているので、それらは適当なフォルダにおいて、そのパスと正解ラベルを書いたCSVをインプットにします。 

こういう風にテキストファイル単位で学習させるやり方は、色々応用が考えられて面白いです。 

たとえば、メールとか日報とかで、要注意な内容のものを学習させておいて、それで予測させることで、従業員フォローのきっかけにする・・とかですね。 

後ろの引数、「0_result.csv」の方は、アウトプットの一つであるNNCのデータ・セットCSVの名前です。 

処理をして、学習した結果をラベル(0 か 1)のフォルダに保存して、そのデータCSVへのパスと、正解ラベルを書いたファイルです。

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

 

このソースのポイントを補足します。 

まず、各ニュースファイルを読み込む部分です。

def get_input_txt(textfile):
    with codecs.open(textfile,'r','utf8') as f:
        data = f.read()
        data2 = re.sub(r'[\s \t]',"",data)
        data3 = re.sub(r'^http.+0900',"",data2)
        return re.split(r'[。??]',data3)

 

ちょっと、妙な部分があるので解説しておきます。 

ニュースはこんな感じの構成です。

http://news.livedoor.com/article/detail/4304647/
2009-08-19T11:30:00+0900
“バレバレ”では男性も幻滅 つけまつ毛専用マスカラ「つけまマスカラ」誕生 
 女性がメイクをする上で最も気合が入るのが、アイメイク。部分使いに、上下フルに、2枚重ねと、パッチリ印象的な目元作りの為につけまつ毛を利用している人が増え続けている。

 簡単に長く、黒々としたまつ毛が手に入るつけまつ毛だが、“つけているのがバレバレ”では男性からしても幻滅ポイントに。そんな悩みを解消すべく生まれたのが、つけまつ毛用マスカラ「つけまマツカラ」だ。自分のまつ毛とつけまつ毛の隙間をしっかり埋めて、美しく目元をボリュームアップしてくれる優れ物。

 

最初にURLと日付時刻で2行あって、3行目から記事がはじまります。 

ただ、内容には余分な空白や改行・タブ文字などが混在してます。 

学習させるのに、前の2行は邪魔なので消して、改行だけだとへんなところで切れるので、行の切れ目を「。??」で識別し、余計な空白とタブ文字は消してる・・という処理がはいってます。 

これで読み込んで、行ごとに分割したリストを返します。

 

さてソースのメイン部分です。

 

そのリストを受けて、文字コードごとに分解してカウントして、構成比を計算してファイルを書く部分のコードです。

# NNC用に数値データに変換したデータを1行ずつ別ファイルにして保存する
def make_data_csv(l_in,label,n_out):
    s_pat = r'記号'
    o_reg = re.compile(s_pat)
    na_ar = np.array([])
    l_line = ['00','01','02','03','04','05','06','07','08','09','0A','0B','0C','0D','0E','0F']
    #初期化 00からFFをひとつずつおいて、位置を決定する
    for fo in range(256):
        if(fo > 15):
            s = str(hex(fo)).upper()
            l_line.append(s[2:4])
    # 形態素解析・・単語に分割するインスタンス
    o_t = Tokenizer()
    for i,l_p in enumerate(l_in):
        # 1行分の単語を分割
        o_malist = o_t.tokenize(l_p)
        for n in o_malist:            
            # 記号以外を対象にする
            if not (o_reg.match(n.part_of_speech)):
                # 分割した単語を1行分リストに加えていく                    
                for i in range(len(n.surface)):                    
                    h = str(hex(ord(n.surface[i]))).upper()
                    if(len(h) >=4):
                        l01 = h[2:4]
                        l_line.append(l01)
                    if(len(h) >=6):
                        l02 = h[4:6]
                        l_line.append(l02)
    # コードごとにカウントする
    c_dic = {}
    t_cnt = 0
    for tok in l_line:
        c_dic.setdefault(tok,0)
        c_dic[tok] += 1
        t_cnt += 1

    # ファイルの中の出現割合に変換する
    l_output = []
    for k, v in sorted(c_dic.items()):
        rt = v / t_cnt
        l_output.append(rt)
    # 1行分ずつCSVファイルに保存する。フォルダは正解ラベル毎にわけておく
    os.makedirs(label,exist_ok=True)
    with open('.\\' + label + '\\' + str(n_out) + '.csv','w',newline='',encoding='utf8') as df:
         cw = csv.writer(df)
         cw.writerow(l_output)
    df.close()

 ポイントは、この部分です。

 for n in o_malist:            
            # 記号以外を対象にする
            if not (o_reg.match(n.part_of_speech)):
                # 分割した単語を1行分リストに加えていく                    
                for i in range(len(n.surface)):                    
                    h = str(hex(ord(n.surface[i]))).upper()
                    if(len(h) >=4):
                        l01 = h[2:4]
                        l_line.append(l01)
                    if(len(h) >=6):
                        l02 = h[4:6]
                        l_line.append(l02)

 

単語を16進数のコードに変換してから、文字列(大文字)にしてます。 

その時、全角なら「0xA0FF」、半角なら「0x31」のような文字列になります。 

前の0xはいらないので、コード部分だけ2桁ずつスライスで取り出して、リストに追加しているというわけですね。 

それを辞書でカウントして、ファイルの中の総数で割ることで、0~1.0の間の数値に正規化して、1行ずつ、CSVデータファイルに書き出してます。 

これでとりあえず、ニュースのテキストデータをNNCで処理可能なデータに変換できます。 

次回は、これを使って、NNCで学習・推論をやってみます。 

最後にソースコードの全文です。

import os
import csv
import re
import codecs
import numpy as np
from janome.tokenizer import Tokenizer


# CSVにテキストファイル(フルパス)とラベルを書いている前提
def make_data(textfile,s_outputfile):
    l_in = get_input_csv(textfile)
    outf = open(s_outputfile,'w',newline='',encoding='utf8')
    csvwriter = csv.writer(outf)
    csvwriter.writerow(['x:image','y:label'])
    for i,l_p in enumerate(l_in):
        l_text = get_input_txt(l_p[0])
        s_path = '.\\' + l_p[1] + '\\' + str(i) + '.csv'
        csvwriter.writerow([s_path,l_p[1]])
        make_data_csv(l_text,l_p[1],i)
    outf.close()
    
# 元になるCSVファイルを読み込んでリストに変換する
def get_input_csv(csvfile):
    with open(csvfile,'r',encoding='utf8') as csvf:
       csvreader = csv.reader(csvf)
       return list(csvreader)

def get_input_txt(textfile):
    with codecs.open(textfile,'r','utf8') as f:
        data = f.read()
        data2 = re.sub(r'[\s \t]',"",data)
        data3 = re.sub(r'^http.+0900',"",data2)
        return re.split(r'[。??]',data3)
    
# NNC用に数値データに変換したデータを1行ずつ別ファイルにして保存する
def make_data_csv(l_in,label,n_out):
    s_pat = r'記号'
    o_reg = re.compile(s_pat)
    na_ar = np.array([])
    l_line = ['00','01','02','03','04','05','06','07','08','09','0A','0B','0C','0D','0E','0F']
    #初期化 00からFFをひとつずつおいて、位置を決定する
    for fo in range(256):
        if(fo > 15):
            s = str(hex(fo)).upper()
            l_line.append(s[2:4])
    # 形態素解析・・単語に分割するインスタンス
    o_t = Tokenizer()
    for i,l_p in enumerate(l_in):
        # 1行分の単語を分割
        o_malist = o_t.tokenize(l_p)
        for n in o_malist:            
            # 記号以外を対象にする
            if not (o_reg.match(n.part_of_speech)):
                # 分割した単語を1行分リストに加えていく                    
                for i in range(len(n.surface)):                    
                    h = str(hex(ord(n.surface[i]))).upper()
                    if(len(h) >=4):
                        l01 = h[2:4]
                        l_line.append(l01)
                    if(len(h) >=6):
                        l02 = h[4:6]
                        l_line.append(l02)
    # コードごとにカウントする
    c_dic = {}
    t_cnt = 0
    for tok in l_line:
        c_dic.setdefault(tok,0)
        c_dic[tok] += 1
        t_cnt += 1

    # ファイルの中の出現割合に変換する
    l_output = []
    for k, v in sorted(c_dic.items()):
        rt = v / t_cnt
        l_output.append(rt)
    # 1行分ずつCSVファイルに保存する。フォルダは正解ラベル毎にわけておく
    os.makedirs(label,exist_ok=True)
    with open('.\\' + label + '\\' + str(n_out) + '.csv','w',newline='',encoding='utf8') as df:
         cw = csv.writer(df)
         cw.writerow(l_output)
    df.close()

 

今回はこのへんで・・。

f:id:arakan_no_boku:20171115215731j:plain