mecab
機械学習
scikit-learn
Python3

ディープラーニングを用いて社説から新聞社を予測する

概要

機械学習の勉強も兼ねて
各新聞社の社説を形態素解析により分解しニューラルネットを用いて学習することでどこの新聞社っぽい文章か判別するモデルを作りました。

プログラム

1.text_make.py  :  社説一覧サイトから社説のテキストファイルを作成する
2.csv_make.py  :  1でできたテキストファイルからcsvファイルを作成する
3.nlp_tasks.py  :  MeCabを用いて文章の分かち書きを行う
4.main.py    :  ニューラルネットによる学習を行う
3.4.は以下のサイトを参考にしています。
https://spjai.com/category-classification/

テキストファイルの作成

text_make.pyの説明
このプログラムでは各社の社説がストックされているサイト
http://shasetsu.seesaa.net/
からテキストファイルの作成を行っていきますが、日経新聞はこのサイトでは乗っていなかったので
http://editorial.x-winz.net/
から抽出し作成しました。

text_make.py
import requests,bs4,os

#現在のdirを取得
current_dir=os.getcwd()
"""日経新聞用記事の中身抽出"""
def body(url):
    res= requests.get(url)

    soup=bs4.BeautifulSoup(res.text,"html.parser")
    elems=soup.select('.entry')
    for elem in elems:
        return elem.getText()
"""日経新聞用記事の書き込み"""

def write_nikkei(num,company,url):

    f = open(company+str(num)+'.txt', 'w') # 書き込みモードで開く
    f.write(body(url)) # 引数の文字列をファイルに書き込む
    f.close() 

"""日経新聞以外の記事の書き込み"""

def write(num,text,company):

    print(num)
    f = open(company+str(num)+'.txt', 'w') # 書き込みモードで開く
    f.write(text) # 引数の文字列をファイルに書き込む
    f.close() 


def text_make(company,company_num,page_end):
    #フォルダの作成
    os.mkdir(company)
    #作成したフォルダにディレクトリの移動
    os.chdir("./"+company)
    #日経新聞に関して
    if(company =='nikkei'):
        for page in range(1,page_end):
            res_list= requests.get('http://editorial.x-winz.net/ed-category/editorial/national/'+company+'/page/'+str(page))


            soup_list=bs4.BeautifulSoup(res_list.text,"html.parser")
            elems_list=soup_list.select('.post-box-title a')
            list_url=[]
            for elem in elems_list:

                list_url.append(elem.get('href'))

            print(list_url)

            for i in range(len(list_url)):

                #中国人の名前などは書き込みの際エンコードエラーが出るので例外処理でスキップする。

                try:
                    write_nikkei(i+(page-1)*10,company,list_url[i])#日経用
                    print(company+str(i+(page-1)*10))

                except:
                    print('error')



    #日経新聞以外に関して

    else:


        for page in range(1,page_end):
            res= requests.get('http://shasetsu.seesaa.net/category/'+company_num+'-'+str(page)+'.html')

            soup=bs4.BeautifulSoup(res.text,"html.parser")
            elems=soup.select('.text')
            list_text=[]
            for elem in elems:
                list_text.append(elem.getText())

            for i in range(0,len(list_text)):
                print('page'+str(page))
                #中国人の名前などは書き込みの際エンコードエラーが出るので例外処理でスキップする。
                try:

                    write(i+(page-1)*19,list_text[i],company)
                except:
                    print('error')

    os.chdir(current_dir)
page_end = 74

text_make('asahi','4848832',page_end)
text_make('mainichi','4848854',page_end)
text_make('yomiuri','4848855',page_end)
text_make('sankei','4848857',page_end)
text_make('nikkei',' ',140)

日経新聞のストックが1400記事ほどしかないので他の新聞社に関しても大まかに1400記事になるようにpage_endの値を74に設定しています。

CSVファイルの作成

得られたテキストファイルからCSVファイルの作成を行います。

asahi_make.py
import csv
import os

#現在のdirを取得
current_dir=os.getcwd()
#フォルダ内のファイルの数を取得する
def GetFileNum(sInFdr):
    if not os.path.isdir(sInFdr): return 0
    i=0
    for root, dirs, files in os.walk(sInFdr):
        i+=len(files)
    return i


datas=[]
f = open('data_test.csv', 'w')
csv_writer = csv.writer(f,quotechar="'")
def make_csv(list_company):
    for i in range(len(list_company)):#3398#2718

        os.chdir(current_dir)
        file_num=GetFileNum(list_company[i])

        os.chdir("./"+list_company[i]) # 各新聞社のディレクトリに移動

        for j in range(file_num):

            try:
                r = open(list_company[i]+str(j)+'.txt', 'r')
                line_a = r.readlines()
            except:
                print('Nofile')


            #日経新聞のみテキストファイルの形式が違うことに注意する
            if(list_company[i] =='nikkei'):

                text = ''
                for line in line_a[2:]:
                    text += line.strip()
            else:
                text = ''
                if(len(line_a)!=0):
                    for line in line_a[0]:
                        text += line.strip()



            datas.append([text,list_company[i]])

list_company=['asahi','mainichi', 'sankei', 'yomiuri','nikkei']
make_csv(list_company)
csv_writer.writerows(datas)
f.close()

日経新聞のみ別サイトからとってきているためテキストの形式が異なることに注意してください。

形態素解析

文章を文章のままで分析することは難しいのである程度の部分列で区切る必要がありますが文章を区切る上で英語と比べ日本語には決定的な弱点があります。それは単語の区切りがわかりにくいことです。
英語であれば単語同士が半角スペースで区切られているため文の構成要素がすぐにわかりますが日本語ではすぐに構成要素はわかりません。
よく上げられる例として「すもももももももものうち」の文の区切り方を見つけいるのは難解です。
そこで形態素解析という手法を用います。
「すもももももももものうち」という文であれば
→すもも(名詞,一般)/も(助詞,係助詞)/もも(名詞,一般)/も(助詞,係助詞)/
 もも(名詞,一般)/の(助詞,連体化)/うち(名詞,非自立)

のように分割することで文章を解析できるようにします。
分割の方法として、MeCabという形態素解析のライブラリを用いることで
形態素解析を行うことができます。

bag of words

bag of wordsとは上記のように形態素解析したものを文のつながりを考えずにすべてバッグに詰め込んでしまうイメージです。

形態素解析.png
このようにして出現する全ての単語を把握したあと、全ての単語に対してテキスト中に出現するかどうかというベクトルを作成します。
それを入力データとしてニューラルネットワークで学習します。

社説の学習と推定

modelsというフォルダを作成します。
下記のプログラムで文章の分かち書きを行い配列を作成します。

nlp_tasks.py
import MeCab
from sklearn.feature_extraction.text import CountVectorizer

def _split_to_words(text):
    tagger = MeCab.Tagger('-O wakati')
    try:
            res = tagger.parse(text.strip())
    except:
            return []
    return res



def get_vector_by_text_list(_items):
    count_vect = CountVectorizer(analyzer=_split_to_words)
    bow = count_vect.fit_transform(_items)
    X = bow.todense()
    return [X,count_vect]

main.pyにより学習と予測を行います。

main.py
import pandas as pd
import numpy as np
from sklearn.preprocessing import LabelEncoder
from sklearn.externals import joblib
import os.path
import nlp_tasks
from sklearn.model_selection import cross_val_score


from sklearn.neural_network import MLPClassifier  # アルゴリズムとしてmlpを使用

def train():
    classifier = MyMLPClassifier()
    classifier.train('data_syasetu.csv')

def predict():
    classifier = MyMLPClassifier()
    classifier.load_model()
    d=open('judge_text.txt',"r")
    reader=d.read()

    d.close

    result = classifier.predict(reader)
    print(result)
    """
    f=open('result.txt','w')
    f.write(result)
    """


class MyMLPClassifier():
    model = None
    model_name = "mlp"

    def load_model(self):
        if os.path.exists(self.get_model_path())==False:
            raise Exception('no model file found!')
        self.model = joblib.load(self.get_model_path())
        self.classes =  joblib.load(self.get_model_path('class')).tolist()
        self.vectorizer = joblib.load(self.get_model_path('vect'))
        self.le = joblib.load(self.get_model_path('le'))

    def get_model_path(self,type='model'):
        return 'models/'+self.model_name+"_"+type+'.pkl'

    def get_vector(self,text):
        return self.vectorizer.transform([text])



    def train(self, csvfile):
        df = pd.read_csv(csvfile,names=('text','category'),encoding='shift_jis')

        X, vectorizer = nlp_tasks.get_vector_by_text_list(df["text"])

        # loading labels
        le = LabelEncoder()
        le.fit(df['category'])
        Y = le.transform(df['category'])

        model = MLPClassifier(max_iter=150, hidden_layer_sizes=(150,),verbose=10,)
        model.fit(X, Y)

    # save models
        joblib.dump(model, self.get_model_path())
        joblib.dump(le.classes_, self.get_model_path("class"))
        joblib.dump(vectorizer, self.get_model_path("vect"))
        joblib.dump(le, self.get_model_path("le"))

        self.model = model
        self.classes = le.classes_.tolist()
        self.vectorizer = vectorizer

    def predict(self,query):
        X = self.vectorizer.transform([query])

        key = self.model.predict(X)

        return self.classes[key[0]]
    def cross_validation(self,csvfile):
        self.model = MLPClassifier(max_iter=150, hidden_layer_sizes=(150,),verbose=10,)
        df = pd.read_csv(csvfile,names=('text','category'),encoding='shift_jis')
        _items = df["text"]

        X, vectorizer = nlp_tasks.get_vector_by_text_list(_items)

       # loading labels
        le = LabelEncoder()
        le.fit(df['category'])
        Y = le.transform(df['category'])
        scores = cross_val_score(self.model, X, Y, cv=4)
        print(scores)
        print(np.average(scores))


if __name__ == '__main__':
    #train()
    predict()
    """
    classifier = MyMLPClassifier()
    classifier.cross_validation('data_syasetu.csv')
    """

日経新聞で中国人の名前が使われていると文字コードエラーが出てしまったのでエラーが出た場合その部分を削除する必要があります。
predictをコメントアウトしtrainで学習することができます。
judge_text.txtというテキストファイルを作成しそこに判定したい文を書き込み
trainをコメントアウトしpredictを実行することで社名を予測することができます。
結果はresult.txtにも出力されます。

まとめと今後の課題

交差検証の結果として平均して8割の正答率であった。
文字コードでエラーが出そうなページは前もって間引くようなプログラムを作る必要がある。
より精度を高めるためにパラメータチューニングを行っていくことや学習データをもっと増やす必要がある。

おまけ

しんぶん赤旗の直近10日の社説を予測したところ7日分は朝日,三日分は毎日であった。両社ともどちらかというと左寄りの新聞であり左寄りか右寄りかで言葉選びにも違いがあるのかもしれない。