エンジニアMのAI学習記録(2017年11月分)

2017.11.10
こちらは、Webに関連するエンジニア向けの記事です。
当社のWeb関連技術の公開と採用活動のために掲載しています。

(2017年11月10日更新)

みなさんこんにちは、エンジニアのMです。
これは当社の技術やAIに興味のある方に向けた日誌になります。
今月も毎週更新していきます、よろしくお願いします。

11月6日

■ JavaとPythonの連携をする

KuromojiをPythonから扱えるようにしたい、ということでJavaとPythonの連携について覚える。

  • 参考:(Qiita)PythonからJavaを呼び出す簡単な方法[リンク]

こちらで解説している「py4j」を使ってKuromojiをサーバーとして起動し、Pythonから呼び出して使えるようにする。

■ 手順

Windows環境で作業する。

Anacondaをインストール済みなので、まずはpipでpy4jをインストールする。
Pythonインストール先のshareフォルダ(例:C:\Python36\share)にpy4j0.xx.x.jarができていることを確認。

Eclipseでプロジェクトを作成した後、メニューの プロジェクト>プロパティ>Javaのビルドパス>ライブラリー と辿って「外部JARの追加」から先程の.jarファイルを指定する。
これでサンプルのAdditionApplication.javaを動かして、Pythonからクラスを利用できることができた。

■ 実装

とりあえず簡単にKuromojiを使えれば良いということで、

  • 文を投げると分かち書きして返してくれる
  • 正規表現で記号、数字は勝手に除去する
  • 品詞の組み合わせは「名詞のみ」、「全部」の2通りだけ

と簡素な仕様とする。

package kuromoji4py.io;

import org.atilika.kuromoji.*;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import py4j.GatewayServer;

public class KuromojiServerIO {

    public String kuromojiIO(String strIn, Boolean onlyNoun) {
        boolean debug = false;
        String strOut = "";
        String word;
        Tokenizer tokenizer = Tokenizer.builder().build();
        Matcher m;
        Pattern p = Pattern.compile("^[0-9]+$"
                + "|^[ -/:-@\\[-\\`\\{-\\~]+$"
                + "|^(+$|^)+$|^%+$|^◯+$|^♪+$"
                + "|^「+$|^」+$|^。+$|^、+$|^.+$"
                + "|^,+$|^■+$|^□+$");

        if(debug) {
            System.out.println("Input:"+strIn);
        }

        try {
            for (Token token : tokenizer.tokenize(strIn)) {
                if(token.getPartOfSpeech().substring(0,2).contentEquals("名詞") || !onlyNoun) {
                    word = token.getSurfaceForm();
                    m = p.matcher(word);
                    if (!m.find()) {
                        strOut = strOut.concat(word+" ");
                    }
                }
            }
        } catch (Exception e) {
            System.out.println("Error!");
        }

        if(debug) {
            System.out.println("Output:"+strOut);
        }
        return strOut;
    }
    public static void main(String[] args) {
        KuromojiServerIO app = new KuromojiServerIO();
        GatewayServer server = new GatewayServer(app);
        server.start();
        System.out.println("Kuromoji Server Started");
    }

}

# テスト用Pythonコード(kuromojiIOtest.py)
from py4j.java_gateway import JavaGateway

gateway = JavaGateway()
kuromoji = gateway.entry_point
text = "これはKuromojiのテストです。形態素解析します。記号「」は除去します。"
split = kuromoji.kuromojiIO(text, False)
print(split)
python kuromojiIOtest.py
これ は Kuromoji の テスト です 形態素 解析 し ます 記号 は 除去 し ます

これでPythonから簡単にKuromojiを扱えるようになった。
サーバーとして起動している状態なので、Pythonから呼び出して繰り返し利用することができる。
あとはPython側でファイル入出力のコードを用意して、目的のパスやファイルを処理できるようにすれば良い。

Eclipse上でしか動かせないのは不便なので、仮想CentOSマシンで動かそうとしてみたがなかなかうまくいかず。クラスパスについてまだまだ勉強が足りないようだ。

11月7日

■ Yahoo知恵袋のデータでdoc2vecを試す

昨日はKuromojiを扱いやすくしたので、今日はそれを使って実験を進めてみようと思う。
とりあえず検索ワード「プリンタ」、「Android」について30件ずつ手動で拾ってきて形態素解析した後、doc2vecの結果を見てみる。
一つの質問についてだけ、元の文章を少し変えたものを加えて試してみた。
元々ある程度関連のある文章を並べていて、Kuromojiが名詞と判定した単語を抽出しているので、頻度の高い単語を切る必要はほとんどない。

結果として、ほぼ同じ内容のはずの文章は類似度で上のほうに来るようにはなったものの、突出しているわけではなかった。
今回のような少数の文章に対してはdoc2vec単体ではあまり効果を発揮できないのかもしれない。
他のアイデアと組み合わせることでの可能性を探る必要がありそうだ。
情報が得られそうなワードで検索し、論文や記事から自分の目的に合った手法を見つけることを目標とする。

11月8日

■ Word Mover’s Distance(WMD)を試してみる

  • Word Mover’s Distance: word2vecの文書間距離への応用 [リンク]
  • Earth Mover’s Distance (EMD) [リンク]
  • Word Mover’s Distance を使って文の距離を計算する [リンク]
  • 日本語 Wikipedia エンティティベクトル [リンク]

以上を参考にWMDの実力を試してみた。

今回はJuman++のほうを使った。
wmdistanceを呼び出すときは単語リストを渡す必要があるので、入力された文をその場で分かち書きするのに使っている。

文を入力してください(1つ目)
>> プリンタを選ぶ時に気をつけるべき点はありますか?
文を入力してください(2つ目)
>> プリンタはどこに気をつけて選ぶべきでしょうか?
距離:12.422376563388186

文を入力してください(1つ目)
>> プリンタを選ぶ時に気をつけるべき点はありますか?
文を入力してください(2つ目)
>> Androidの良さについて教えてください
距離:24.573640321058548

文を入力してください(1つ目)
>> プリンタを選ぶ時に気をつけるべき点はありますか?
文を入力してください(2つ目)
>> プリンタを選ぶ時の注意点はなんですか?
距離:12.663622264187333

文を入力してください(1つ目)
>> プリンタを選ぶ時に気をつけるべき点はありますか?
文を入力してください(2つ目)
>> このプリンタでレーベル印刷はできますか?
距離:21.350387439460583

知恵袋のテキストで試した流れで、プリンタとAndroidに関する文で試した結果こうなった。
短い一文で試したが、そこそこ妥当な結果が得られた。

文を入力してください(1つ目)
>> プリンタを選ぶ時に気をつけるべき点はありますか?
文を入力してください(2つ目)
>> 携帯電話を選ぶ時に気をつけるべき点はありますか?
距離:3.1761431100963127

物だけ変えた文の距離がこれくらいだと、あまり信用はできない気もしてくる。
しかし、面倒なパラメータ調整が不要なことは大きなメリットに感じる。
ターミナルでWMDを試すことができるコードを公開する。

# -*- coding: utf-8 -*-

import sys
import re
from gensim import models
from pyknp import Jumanpp

# 学習済みモデルの場所
vectors = "./japanese_entity_vector/entity_vector.model.bin"

# モデル読み込み
print("Wikipedia日本語エンティティベクトルを読み込んでいます")
print("しばらくお待ち下さい…")
model = models.KeyedVectors.load_word2vec_format(vectors, binary=True)
print("{} :読み込み完了".format(vectors))


# 関連語テスト
def similar_words():
    while(True):
        print('')
        word = input("検索したい単語を入力してください\n>> ")
        if word == '':
            break
        try:
            sim = model.most_similar(positive=word, topn=10)
            for x in sim:
                print("{}\t{}".format(x[1], x[0]))
        except KeyError as err:
            print("エラー:単語 {} は辞書にありません".format(word))


# Word Mover's Distanceテスト
def wmd():
     while(True):
         print('')
         temp = input("文を入力してください(1つ目)\n>> ")
         if temp == '':
             break
         replaced=re.sub('\u3000| ', '', temp)
         text1 = split_words(replaced)
         print_words(text1)
         temp = input("文を入力してください(2つ目)\n>> ")
         if temp == '':
             break
         replaced=re.sub('\u3000| ', '', temp)
         text2 = split_words(replaced)
         print_words(text2)
         distance = model.wmdistance(text1, text2)
         print("距離:{}".format(distance))


# Jumanppによる分かち書き
def split_words(text):
    result = Jumanpp().analysis(text)    
    return [mrph.midasi for mrph in result.mrph_list() if check_hinsi(mrph.hinsi)]


# 分かち書き出力条件
def check_hinsi(hinsi):
     if hinsi == "名詞" or\
        hinsi == "未定義語" or\
        hinsi == "動詞" or\
        hinsi == "形容詞":
        return True
     else:
        return False


# 解析結果出力(テスト用)
def print_words(words):
     print("形態素:", end='')
     for word in words:
         print(word+" ", end='')
     print("")


# メイン
print("※何も入力せずにEnterで終了します")
while(True):
    print('')
    word = input("どちらをテストしますか?:similar or wmd\n>> ")
    if word == '':
        break
    elif word == 'similar':
        similar_words()
    elif word == 'wmd':
        wmd()

同じ文を入力してテストしてみたが、特に結果が良くなった感じはしなかった。
何か一つの手法で全部解決すれば良いのだが、そういうわけでもないのが自然言語処理の難しいところか。
文字ベースのCNNによる文書分類の論文があるらしいので、読んでみる。今のマシンパワーでは日本語を扱いきれないと思うが、興味はある。

  • 文字レベルの畳込みニューラルネットワークによる文書分類 [リンク]
  • Character-level Convolutional Networks for Text
    Classification [リンク]

結果を見ると、ユーザーの投稿内容のような、精選されていない文に対する精度が高いことが分かる。
単語ベースのアルゴリズムが、誤字脱字が多く見込まれる文への対応が弱いことはイメージしやすい。
もちろん、あらゆる文字の組み合わせに対応する以上、モデル作成の計算量は膨大なものとなる。
日本語なら文字数は少なくなるが、文字の種類が多いので結果的に英語以上に計算は困難だろう。

日本語に対してもCNNで分類している研究はあるようだ。

  • 文字画像による Character-level Embeddingと文書分類 [リンク]

文字を画像として扱うという方法はなるほどと思った。
漢字は図形であり、似た意味の字は同じ部首を持っていたりするので、次元削減のついでに特徴を抜き出すことができれば確かに都合がいい。

11月9日

■ 文書間距離の計算を別モデルについてもしてみる

Wikipediaエンティティベクトルだけでなく、対象のテキストデータ群から学習したベクトル値も使って文書間距離を確認する。
類似度を計りたいテキストを対象として学習させたデータなので、そのデータ群に対しては汎用的なものよりはフィットしたモデルのはず。
ただし、gensimの学習値は正規化済みだと分かったので、併用するにはそれぞれの距離に対する重みを考えなければならない。

■ 各文書間の距離をリストアップして眺めてみる

各文書間の距離(知恵袋からの30+30文書)を全通りの組合せについて計算して、その数字を眺めて情報を得たいと思う。

  • 文書:Kuromojiで名詞抽出した知恵袋の 2カテゴリ60文書
  • モデル:Wikipediaエンティティベクトルと対象文書から計算したベクトル

この計算の結果、

  • 平均(And+Priは別カテゴリ間の距離の意)
  • Wikiモデル:Android 20.9 / Printer 20.6 / And+Pri 22.4
  • 対象間モデル(x1000):Android 18.2 / Printer 17.2 / And+Pri 21.0

  • 最大

  • Wikiモデル:Android 26.0 / Printer 26.3 / And+Pri 28.0
  • 対象間モデル(x1000):Android 23.2 / Printer 22.2 / And+Pri 23.2

となった。見やすいようにレンジを揃えてある。同一カテゴリ間の距離より、別カテゴリ間の距離のほうが大きいことが分かる。サンプル数が少なくても、確かにある程度分離することができている。
ミックスすることで精度を上げられそうではあるが、2通りだと計算にかかる時間が気にかかった。
ある文書に対して最も距離的に近い文書を見つけ出すような計算には、あまり向かないかもしれない。モデルが大きくなるほど、メモリの消費量も相応に大きくなる。

■ WMD以外の方法についてまた考えてみる

WMDが比較的手軽に文書分類できることが分かったが、運用時にリソースの消費がそこそこ大きそうなので他の方法も検討してみる。

11月10日

■ 類似文を探すということについて

類似文を検索するという行為について、自分の認識にズレがあるのではということに気づいた。
例えば、

  • Androidについて教えてください
  • iOSについて教えてください

という2つの文があったとする。
「何についての話か?」という見方をすれば、AndroidとiOSなので、ジャンル・カテゴリ的には近いが違う物について聞いている。
「これは類似の文か?」という見方をすれば、AndroidとiOSの部分しか違いがないので、これは明らかに類似の文である。
長い文であれば関連するワードの差異によって、類似ではないという判定になるかもしれない。しかしこのような短文だけで見れば、これは間違いなく類似の文である。

認識のズレというのはまさにこのことである。何についての文か、というところと、文法的に類似の文か、というところで違うのに、全部まとめて「類似文」として処理させようとしていた。
比較的短い文に対して何についての文か知りたいのであれば、単純に類似文検索の手法を持ち込むのは適切ではなかった。

勉強としては良かったが、もう少し目的と手法について検討しながら進めるべきだったと思う。

■ 改めて手法について考える

何を持って類似性があるとみなすか、というところが重要なポイントとなる。
例えば、「家電Aは値段の割に高性能でオススメ。例を挙げると(以下略)」のような文があれば、

  • カテゴリ:家電・電化製品
  • 感情:好意的
  • 文体:カジュアル
  • 文の形式:商品レビュー
  • テーマ:家電Aについて

のようにタグ付けできるとする。
家電Aについてのレビューが欲しければ、感情・文体は関係ないのでカテゴリ・文の形式・テーマについて分類して、完全一致したものを抽出すれば良い。
テーマについては未知語が該当するケースも多いと思われるので、方法について検討する必要があるかもしれない。

例えば教師データとしてタグ付けを済ませた文章から、Doc2Vecで文章のベクトル値を得る。
そのベクトル値についてディープラーニングによる教師あり学習によって、適切にそれぞれ分類できるよう学習させる。
分類精度は十分に上げられそうだが、この方法は人力で教師データを用意しなければならないのが悩ましい。さほど多くないデータ量で精度を上げるには悪くない方法だとは思うのだが。

■ この方法について

実際に運用するとしたら、新規文書のベクトル値を求めた後に行列演算でタグ付けして保存するだけなので、負荷は小さく済みそうだ。大きな辞書データを保持する必要があるので、メモリ消費量はそれなりか。
人手で枠組み(タグの数、種類)を決める部分も検討を重ねる必要がある。例えば感情をプラス・マイナスの2通りから喜怒哀楽の4通りにしたい、となった場合、一から学習させなければならないからだ。


この記事が気に入ったら
いいね ! しよう

フォームズでは無料でメールフォームを作ることができます
メールフォームとは、フォームに入力された情報をメールで送るサービスです。問い合わせの受付や申し込み、アンケートなどに幅広く利用できます。