TadaoYamaokaの開発日記

個人開発しているスマホアプリや将棋AIの開発ネタを中心に書いていきます。

fastTextでwikipediaを学習する

先日の日記でfastTextでWikipediaの要約を学習させたが、期待した結果にはならなかったので、全記事を使って学習し直した。
Wikipediaの学習済みモデルは、
fastTextの学習済みモデルを公開しました - Qiita
こちらの方が配布されていますが、MeCabの辞書のバージョンが異なるためか登録されていない単語があったため自分で学習し直した。

コーパスの前処理は、doc2vecの学習で行った、10以下の頻度の単語を含む文を除外したものを使用した。

Bash on WindowsでビルドしたfastTextを使い、Core i7-6700K(4.00GHz)で35分ほどで学習できた。

学習したモデルはgensimを使ってPythonから使用できる。

実行例
from gensim.models.wrappers.fasttext import FastText
model = FastText.load_fasttext_format("wiki_model")
model.most_similar("リンゴ")
実行結果
[('青リンゴ', 0.7415881156921387),
 ('サクランボ', 0.6609212160110474),
 ('リンゴジュース', 0.6432982087135315),
 ('ヘーゼルナッツ', 0.6267402768135071),
 ('柑橘系', 0.6092754602432251),
 ('キウイフルーツ', 0.6087037324905396),
 ('ピスタチオ', 0.6070229411125183),
 ('グレープフルーツ', 0.6048493981361389),
 ('サトウダイコン', 0.6035013794898987),
 ('トロピカルフルーツ', 0.6010643243789673)]

前回Wikipediaの要約ではうまくいかなかった「王様」+「女性」-「男性」の結果は、

model.most_similar(positive = ["王様","女性"], negative = ["男性"])
Out: 
[('女王様', 0.5537782907485962),
 ('妖精の女王', 0.5271559357643127),
 ('雪の女王', 0.5174967646598816),
 ('先々代の女王様', 0.5147340893745422),
 ('黄金の林檎', 0.5090798735618591),
 ('妖精達', 0.5088496804237366),
 ('チャーミング', 0.5082257986068726),
 ('ハートの女王', 0.5066112279891968),
 ('黄金の鳥', 0.5021530985832214),
 ('王女', 0.5012854337692261)]

それなりに期待した結果になった。

model.most_similar(positive = ["ザク","赤い"])
Out: 
[('ザクザク', 0.6754602193832397),
 ('ザクI', 0.6736077070236206),
 ('ザクレロ', 0.6639564037322998),
 ('ザクII', 0.6604661345481873),
 ('赤い輪', 0.6454007625579834),
 ('シャア専用', 0.6375926733016968),
 ('ザクキャノン', 0.635945200920105),
 ('ザクIII', 0.6316030621528625),
 ('ゲルググキャノン', 0.6254370808601379),
 ('Vガンダム', 0.6244575381278992)]

これも期待通りです。

doc2vecでWikipediaを学習する

先日の日記でTF-IDFでFAQに回答することを試したが、TF-IDFでは質問文の類似度を単語の頻度に重み付けをして測っている。
そのため、単語が完全に一致している必要があり、同じ意味の単語でも異なる単語として認識してしまう。

word2vecを使用すると単語をベクトル化することができ、意味が近ければ近いベクトルを出力することができる。
word2vecを文に適用する場合、単語ベクトルの平均をとる方法もあるが、語順が失われるという欠点がある。

doc2vecを使用すると、文の語順を考慮して、文自体をベクトル化することができる。

doc2vecには、PV-DMとPV-DBOWの2種類があり、PV-DMとPV-DBOWのベクトルを組み合わせて使用することで精度を上げることができる。

PV-DMは、文と単語にユニークな固定次元のベクトルを割り当て、文の単語列をウィンドウサイズ幅ごとに抽出し、文のベクトルを追加して、それらの平均(または連結)をとったベクトルから、次に現れる単語を多クラス分類により予測するように学習する。
その際、単語ベクトルも同時に学習される。

PV-DBOWは、文のベクトルから、文中からランダムに抽出した単語を予測するように学習する。

PV-DMで、学習したモデルを使用して予測する時には、単語ベクトルモデルは固定して、文のベクトルのみを学習することで、文のベクトルを出力する。
予測のたびに、ランダムに初期化したベクトルから学習するため、毎回結果が異なることに注意する必要がある。

ここでは、gensimのdoc2vecの実装を使用して、日本語のWikipediaの全記事を使って、PV-DMのモデルを学習することを試みる。

環境

Wikipediaの全記事の取得

https://dumps.wikimedia.org/jawiki/latest/
ここから、jawiki-latest-pages-articles.xml.bz2をダウンロードする。
記事はXML形式になっている。

XMLから文章のみを抽出する

WikiExtractorを使用してXMLから文章のみを抽出する。

git clone https://github.com/attardi/wikiextractor.git
python wikiextractor/WikiExtractor.py -b 500M -o path/to/corpus jawiki-latest-pages-articles.xml.bz2

path/to/corpusディレクトリに、500Mごとにwiki_00 wiki_01 wiki_02 wiki_03 wiki_04が作成されるので、1つのファイルに連結する。

MinGWなどのcatコマンドを使用して、

cat wiki_00  wiki_01  wiki_02  wiki_03  wiki_04 >wiki

分かち書きする

MeCabを使用して分かち書きする。

mecab -O wakati -d <辞書> wiki -o wiki_wakati

辞書にはmecab-ipadic-NEologdを使用した。Windowsでの使用方法はこちらの日記を参照。

頻度の少ない単語を除外する

そのまま学習するとメモリが24GBあっても不足して学習できなかった。
文の数が多いと読み込みの際メモリ不足するのと、前処理で文を減らして読み込めた後でも単語数が多いとメモリが不足する。
そこで、前処理として、頻度の少ない単語を含む文自体を削除することにした。
単語の頻度は5以下でもメモリが不足したので、頻度が10以下の単語を含む文を除外した。

以下のようなスクリプトで、頻度の低い単語を抽出する。入力は分かち書き済みのコーパス

import argparse
from collections import defaultdict

parser = argparse.ArgumentParser()
parser.add_argument('input')
parser.add_argument('output')
args = parser.parse_args()

words = defaultdict(lambda: 0)
for line in open(args.input, "r", encoding="utf-8"):
    line = line.strip()
    if line == "" or line[0] == "<":
        continue
    for word in line.split(" "):
        words[word] += 1

print("word num : ", len(words))

few_word_num = 0
with open(args.output, "w", encoding="utf-8") as f:
    for word in words:
        if words[word] <= 10:
            few_word_num += 1
            f.write(word)
            f.write("\n")

print("few word num : ", few_word_num)

単語数は以下の通りとなった。

総単語数 2,822,644
頻度10以下 2,311,196
残り単語数 511,448

頻度の少ない単語の一覧を使って、文を削除する。入力は分かち書き済みのコーパス

import argparse
import re

parser = argparse.ArgumentParser()
parser.add_argument('input')
parser.add_argument('few_words')
parser.add_argument('output')
args = parser.parse_args()

few_words = set()
for line in open(args.few_words, "r", encoding="utf-8"):
    dst = re.sub(r'([\[\](){}\\*+.?^$\-|])', r'\\\1', line.strip())
    if dst in ("", "「", "」", "、", "。", "(", ")"):
        continue
    few_words.add(dst)

f = open(args.output, "w", encoding="utf-8")
for line in open(args.input, "r", encoding="utf-8"):
    if line.strip() == "" or line[0] == "<":
        continue
    found = False
    words = line.strip().split(" ")
    if len(words) <= 2:
        continue
    for word in words:
        if word in few_words:
            found = True
            break
    if found:
        continue
    f.write(line)
f.close()

doc2vecで学習する

前処理を行ったコーパスを使用してdoc2vecでPV-DMのモデルを学習する。
学習する単位は、行の単位とした。
gensimのdoc2vecのデフォルトでは長さが1の単語は除外されるが、日本語では長さが1の単語があるため対象とした。

以下のようなスクリプトで学習できる。

import gensim
import smart_open
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('input', type=str)
parser.add_argument('--save_model', '-s', default='model', type=str)
args = parser.parse_args()

def read_corpus(fname):
    with smart_open.smart_open(fname, encoding="utf-8") as f:
        for i, line in enumerate(f):
            # For training data, add tags
            yield gensim.models.doc2vec.TaggedDocument(gensim.utils.simple_preprocess(line, min_len=1), [i])


train_corpus = list(read_corpus(args.input))

model = gensim.models.doc2vec.Doc2Vec(size=300, min_count=10, iter=55)

model.build_vocab(train_corpus)
model.train(train_corpus, total_examples=model.corpus_count, epochs=model.iter)

model.save(args.save_model)

ベクトルの次数は300、イテレーション回数は55とした。
元の論文では次数に400が使われているので、400とした方が良かったかもしれない。

学習には、2時間41分かかった。
保存したモデルのサイズは、約5GBとなった。

学習済みモデルを使用して類似単語を調べる

doc2vecにより単語ベクトルも学習されるため、学習済みモデルを使用して類似単語を調べることができる。

類似単語は以下のようにして調べる。

import gensim

model = gensim.models.Doc2Vec.load("model")
model.most_similar("リンゴ")
実行例
model.most_similar("リンゴ")
Out: 
[('ワイン', 0.6092509627342224),
 ('コーヒー', 0.6062097549438477),
 ('ケーキ', 0.595314621925354),
 ('トマト', 0.592261552810669),
 ('花', 0.5917296409606934),
 ('果物', 0.5882657766342163),
 ('バナナ', 0.5871806144714355),
 ('酒', 0.5837405920028687),
 ('ビール', 0.5820529460906982),
 ('牛乳', 0.5812638401985168)]

足し算もできる。

model.most_similar(positive=["ザク", "ガンダム"])
Out: 
[('ms', 0.7314956188201904),
 ('モビルスーツ', 0.6884805560112),
 ('x', 0.664232075214386),
 ('マシン', 0.6640591621398926),
 ('モンスター', 0.662639856338501),
 ('機体', 0.651045024394989),
 ('ロボット', 0.6460052132606506),
 ('戦車', 0.6428899168968201),
 ('戦闘機', 0.6382737159729004),
 ('cpu', 0.6371015906333923)]

引き算もできる。

model.most_similar(positive=["猫"], negative=["フレンズ"])
Out: 
[('人間', 0.6333577036857605),
 ('子供', 0.6314771175384521),
 ('動物', 0.6307740807533264),
 ('親', 0.6073141098022461),
 ('家族', 0.5995410084724426),
 ('が', 0.5993083715438843),
 ('犬', 0.5975848436355591),
 ('も', 0.5944583415985107),
 ('何', 0.5930898785591125),
 ('を', 0.5907615423202515)]

学習済みモデルを使用して文の類似度を測る

以前の日記でTF-IDFで試した気象庁のFAQを使用して、入力した文に意味が近い質問文を予想する。

import gensim
import MeCab
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('faq', type=str)
parser.add_argument('model', type=str)
parser.add_argument("--dictionary", "-d", type=str, help="mecab dictionary")
args = parser.parse_args()

mecab = MeCab.Tagger("-Owakati" + ("" if not args.dictionary else " -d " + args.dictionary))

model = gensim.models.Doc2Vec.load(args.model)

questions = []
answers = []
for line in open(args.faq, "r", encoding="utf-8"):
    cols = line.strip().split('\t')
    questions.append(gensim.utils.simple_preprocess(mecab.parse(cols[0]).strip(), min_len=1))
    answers.append(cols[1])

doc_vecs = []
for question in questions:
    doc_vecs.append(model.infer_vector(question))

while True:
    line = input("> ")
    if not line:
        break

    vec = model.infer_vector(gensim.utils.simple_preprocess(mecab.parse(line), min_len=1))
    sims = cosine_similarity([vec], doc_vecs)
    index = np.argsort(sims[0])

    print(questions[index[-1]])
    print()
    print(answers[index[-1]])
    print()

    print(questions[index[-2]])
    print(questions[index[-3]])
    print(questions[index[-4]])
    print()

以下のような文を入力すると、それぞれ予測した質問文(上位4つ)は以下のようになった。

> みぞれとは何ですか?
['フェーン現象', 'と', 'は', '何', 'です', 'か']
['大雨', 'の', '回数', 'は', '増え', 'て', 'いる', 'の', 'です', 'か']
['プレート', 'と', 'は', '何', 'です', 'か']
['エルニーニョ現象', 'ラニーニャ現象', 'が', '発生', 'する', 'と', '日本', '近海', 'の', '海面', '水温', 'は', 'どの', 'よう', 'に', 'なる', 'の', 'です', 'か']

> 天気予報が外れる理由は?
['週間', '天気予報', 'が', '外れる', 'こと', 'が', 'あり', 'ます', 'が', 'なぜ', 'です', 'か']
['週間', '天気予報', 'は', 'よく', '外れる', 'ので', '日', '先', 'くらい', 'の', '予報', 'だけ', 'で', '良い', 'の', 'で', 'は', 'ない', 'です', 'か']
['噴火警報', '火口', '周辺', '警報', '噴火', '予報', 'について', '教え', 'て', 'ください']
['海上', 'の', '台風', 'の', '中心', '気圧', 'は', 'どの', 'よう', 'に', '測っ', 'て', 'い', 'ます', 'か']

> 地震は予知できますか?
['花粉情報', 'は', '気象庁', 'で', '発表', 'し', 'て', 'い', 'ます', 'か']
['空', 'は', 'どうして', '青い', 'の', 'です', 'か', '夕焼け', 'は', 'どうして', '赤い', 'の', 'です', 'か']
['東海', '地域', 'に', 'は', 'どの', 'よう', 'な', '監視', '体制', 'が', 'とら', 'れ', 'て', 'い', 'ます', 'か']
['地震', 'の', '予知', 'は', 'でき', 'ます', 'か']

> 津波の規模によってどんな被害が起きるのですか?
['津波', 'の', '高さ', 'によって', 'どの', 'よう', 'な', '被害', 'が', '発生', 'する', 'の', 'です', 'か']
['特別警報', 'と', '既存', 'の', '記録的短時間大雨情報', 'の', '違い', 'は', '何', 'です', 'か', '廃止', 'さ', 'れ', 'たり', 'は', 'し', 'ない', 'の', 'です', 'か']
['テレビ局', 'によって', '天気予報', 'の', '内容', 'が', '違う', 'こと', 'が', 'ある', 'の', 'は', 'なぜ', 'です', 'か']
['検定', 'が', '必要', 'と', 'なる', '気象', '測', '器', 'に', 'は', 'どんな', 'もの', 'が', 'あり', 'ます', 'か']

> 高潮の発生の仕組みは?
['副振動', 'と', 'は', '何', 'です', 'か']
['花粉症記念日', '月', '日', 'と', 'は', 'なん', 'です', 'か']
['雪', 'は', 'どうして', 'できる', 'の', 'です', 'か']
['竜巻', 'は', 'どうして', '起きる', 'の', 'です', 'か']

> 高潮の発生の仕組みは?
['高潮', 'の', '発生', 'の', '仕組み', 'は']
['雲', 'が', '七', '色', 'に', '見える', '彩雲', 'の', '仕組み', 'は', '何', 'です', 'か']
['津波', 'は', 'どの', 'よう', 'な', '仕組み', 'で', '発生', 'する', 'の', 'です', 'か']
['竜巻', 'は', 'どうして', '起きる', 'の', 'です', 'か']

みぞれについて聞いているのに、「みぞれ」が現れない文を予測している。
「天気予報」、「外れる」と2つ単語を含む場合は、近い文を予測している。
地震」、「予知」では単語が2つでも4番目になっている。
津波の規模によってどんな被害が起きるのですか?」と長い文を入力すると、それなりの精度になるようだ。

また、「高潮の発生の仕組みは?」を2回予測すると、異なる結果が返っている。予測のたびにランダムな初期化から学習するため起きる。

以上のように、短い文については、あまり精度が高くないという結果になった。
ウィンドウサイズはデフォルトの5を使用しているので、キーとなる単語は少なくとも5個はあった方が良いかもしれない。

ベクトルの次元やウィンドウサイズやイテレーション回数を変えてみてどうなるかさらに検証が必要そうだ。

短い文での類似を測るには、word2vecのベクトルの平均を使用するなどdoc2vecとは別の方法が有効かもしれない。

英語の論文を翻訳する際のTips

機械学習を勉強している際に必ずぶち当たるのが英語の壁である。
有用な論文のほとんどは英語で書かれているのだ。

無料で読める論文は
arXiv.org e-Print archive
で公開されていることが多い。

例えば、以下の論文などを無料で読むことができる。
<物体検出>

<word2vec、doc2vec>

日本語の情報は、Qiitaなどで個人レベルで解説しているサイトはあるが、解説が中途半端だったりして、結局直接論文を読んだ方が理解できる場合もある。

英語のレベルが低いと読むのに苦労するが、Google翻訳ディープラーニングにより精度が向上したことで、ほぼ読解に困らないレベルで翻訳できるようになった。

新しいGoogle翻訳(GNMT)は、全てのGoogleのサービスで対応しているわけではなく、例えばGoogleドキュメントの翻訳機能では古いエンジンが使用されているようである。

最近Cloud Translation APIがGNMTに対応したしたので、これを使えばある程度翻訳作業が自動化できそうであるが、ここでは手作業で、ブラウザのGoogle翻訳を使用して翻訳する際に、作業効率を上げる方法について紹介する。

翻訳手順

arXiv.orgなど入手できる論文はPDF形式であることが多い。
PDFから英文をコピーして、ブラウザのGoogle翻訳に張り付けると以下の問題が発生する。

  • 行末で改行されてしまう
  • 行末で単語がハイフネーションされていると誤訳される
  • et al.(論文の筆者のその他の意味)がピリオドと解釈される
  • ページをまたがる文の間に注釈やフッターが含まれる

これらは手作業で修正が必要である。
コピーしたテキストをテキストエディタに張り付けて加工してもよいが、GoogleドキュメントでPDFをGoogleドキュメントに変換すると、行末で改行されずに段落が一つにつながる。
また、章節が太字になるので、テキストエディタで一括で変換した場合より構造が把握しやすい。

PDFをGoogleドキュメントに変換するには、GoogleドライブにPDFファイルをドロップして、PDFファイルを右クリックして、アプリで開く→Googleドキュメントで変換できる。

Googleドキュメントで開いた後、置換機能を使って、上記のハイフネーションなどを取り除く加工をしてから、Google翻訳に段落単位でコピー&ペーストし、翻訳文を段落の下に張り付けていくと、後から英文と翻訳文がを対比して見やすい。
ただし、ブラウザのGoogle翻訳で直接見た方が、単語を選択して意味を確認したり、発音を確認できたりするので、併用しながら解釈していくのがおすすめである。

英文中に引用文献を()で記述されていると、誤訳する場合が多いので貼り付ける際に取り除いた方がよい。

また、数式・表は崩れて変換されるので、Acrobatのスナップショット機能で図としてコピーし、貼り付けていくとよい。
図も削除されるので、PDFからコピーして図として貼り付けるとよい。

TF-IDFを使ってFAQに回答する

とある理由からBotについて調べています。

最近はAIを使ったBotもありますが、古典的な方法として、あらかじめ質問と回答を用意しておき、ユーザが入力した質問と類似度の高い質問を選んで回答する方法があります。

TF-IDFがその代表的なアルゴリズムになります。

ここでは、TF-IDFをPythonで実装する方法を示します。

scikit-learnにあるTfidfVectorizerクラスを利用することで、簡単に実装できます。
日本語を扱う場合は、あらかじめ形態素解析を行う必要があります。
形態素解析にはMeCab(mecab-python3)を使用します。

fit_transformで、形態素解析した質問文のセットを使用してモデルを学習します。
モデルを使用して質問文をベクトル化した結果が出力されます。

transformで学習したモデルを使用してユーザの入力文をベクトル化し、scikit-learnのcosine_similarity関数で入力文のベクトルと用意した質問文のベクトルのコサイン類似度を計算します。
最もコサイン類似度が高い質問を選択し、その回答を表示します。

コード例

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
import MeCab

import argparse

parser = argparse.ArgumentParser(description="convert csv")
parser.add_argument("input", type=str, help="faq tsv file")
parser.add_argument("--dictionary", "-d", type=str, help="mecab dictionary")
parser.add_argument("--stop_words", "-s", type=str, help="stop words list")
args = parser.parse_args()

mecab = MeCab.Tagger("-Owakati" + ("" if not args.dictionary else " -d " + args.dictionary))

questions = []
answers = []
for line in open(args.input, "r", encoding="utf-8"):
    cols = line.strip().split('\t')
    questions.append(mecab.parse(cols[0]).strip())
    answers.append(cols[1])

stop_words = []
if args.stop_words:
    for line in open(args.stop_words, "r", encoding="utf-8"):
        stop_words.append(line.strip())

vectorizer = TfidfVectorizer(token_pattern="(?u)\\b\\w+\\b", stop_words=stop_words)
vecs = vectorizer.fit_transform(questions)

#for k,v in vectorizer.vocabulary_.items():
#    print(k, v)

while True:
    line = input("> ")
    if not line:
        break

    index = np.argmax(cosine_similarity(vectorizer.transform([mecab.parse(line)]), vecs))
    print(questions[index])
    print()
    print(answers[index])
    print()

TfidfVectorizerはデフォルトでは一文字の単語はトークンとして認識しませんが、日本語では一文字の単語(「雪」など)があるので、オプションで1文字もトークンとして認識するようにしています。

実行方法

>python tf_idf.py ..\data\faq.tsv -d H:\src\mecab-ipadic-neologd\build\mecab-ipadic-2.7.0-20070801-neologd-20170420 -s ..\data\stop_words.txt

引数にタブ区切りの質問と回答のテキストデータを指定し、オプションでMeCabの辞書とストップワードを指定しています。
ストップワードには、助詞や助動詞、連体詞、記号などを登録しておきます。

実行例

気象庁のよくある質問集で試した結果です。

> みぞれとは何ですか?
「 みぞれ 」 と は 何 です か ?

みぞれとは、雨と雪が混じったものをいいます。上空から雪が降ってくる途中、地上近くの気温が高いと、雪がとけて雨になります。一部分とけずに雪のまま落ちてくると、みぞれになります。みぞれは、観測分類上は雪に含めます。

> 台風とハリケーンの違いは?
台風 と ハリケーン と サイクロン の 違い は 何 です か ?

台風は、東経180度より西の北西太平洋および南シナ海に存在する熱帯低気圧のうち、最大風速が約17m/s以上になったものを指します。ハ リケーンは、北大西洋、カリブ海、メキシコ湾および西経180度より東の北東太平洋に存在する熱帯低気圧のうち、最大風速が約33m/s以上 になったものを指します。サイクロンは、ベンガル湾やアラビア海などの北インド洋に存在する熱帯低気圧のうち、最大風速が約17m/s以上に なったものを指します。このように、それぞれの名称を付している最大風速の基準には違いはありますが、台風もハリケーンもサイクロンもそれぞれの地域に存在する熱帯低気圧を強さによって分類している用語の1つということになります。なお、サイクロンは熱帯低気圧と温帯低気圧の区別をせず、広く低気圧一般を指す用語としても用いられることがあります。

> 異常気象について
「 異常気象 」 の 定義 は ある の です か ?

一般には、過去に経験した現象から大きく外れた現象のことを言います。大雨や暴風等の激しい数時間の気象から、数か月も続く干ばつ、極端な冷夏・暖冬まで含みます。また、気象災害も異常気象に含む場合があります。気象庁では、気温や降水量などの異常を判断する場合、原則として 「ある場所(地域)・ある時期(週、月、季節)において30年に1回以下で発生する現象」を異常気象としています。


文章における単語の重要度を見ているので、全文検索よりは精度が高くなりますが、古典的な方法であり類似語や質問の意味は解釈しません。
FAQに回答する場合では、これでも十分実用的です。

64bitのWindowsにMeCabをインストールする

MeCabの公式のサイトではWindows用は32bitのインストーラしか提供されていないため、64bitのWindowsで64bitのPythonから使おうとすると使用できない。

64bit向けには、32bitのインストーラでインストールした後、個別にビルドしたファイルで実行ファイルとライブラリを置き換える必要がある。
http://qiita.com/ksomemo/items/02e98bf1dbd4107a8d13qiita.com
こちらのサイトに書かれていた方法で、ビルドして、Python3から使用することができた。

ただし、環境変数MECABRCがないと、pythonコマンドからMeCabを使用したスクリプトを実行すると、

Traceback (most recent call last):
  File "a.py", line 3, in <module>
    mecab = MeCab.Tagger("-Owakati")
  File "h:\src\mecab-python3\MeCab.py", line 307, in __init__
    this = _MeCab.new_Tagger(*args)
RuntimeError

というエラーが出力される。

その場合、環境変数MECABRCに、

<インストールパス>\etc\mecabrc

を設定すればよい。

また、環境変数PATHに<インストールパス>\binを追加する。

mecab-ipadic-NEologdを使う

mecab-ipadic-NEologdを使うには、まずBash on Windowsmecab-ipadic-NEologdをインストールする。
github.com
公式に書かれている手順通りでインストールできる。
その際、git cloneするディレクトリは、Windowsから見えるパスにする。(/mnt/ドライブ名/~)

Bash on Windowsにインストールした際に、git cloneしたディレクトリ配下に
「build\mecab-ipadic-2.7.0-20070801-neologd-20170420」
というディレクトリができる。
このディレクトリを辞書として指定すれば、Windows側のMeCabでも使用できる。
ただし、辞書の文字コードは「UTF-8」となる。
Python3から使う分には問題ない。

import MeCab

mecab = MeCab.Tagger(r"-Owakati -d H:\src\mecab-ipadic-neologd\build\mecab-ipadic-2.7.0-20070801-neologd-20170420")

print(mecab.parse("私はペンを持っています。"))

このように「-d」オプションに辞書のパスを指定する。

AIで質問の回答を選ぶ

ディープラーニングを使って自然言語の質問に、自然言語の選択肢から回答することを試します。

例えば、
Which of the following is the primary advantage of sexual reproduction when compared to asexual reproduction?
という質問文に、
(A) There is a greater number of offspring.
(B) There is more food available to offspring.
(C) There is greater genetic variety in offspring.
(D) There is a longer development time for offspring.
という選択肢から回答します。(答えは、C)


試すのは以下の論文の方法です。
[1511.04108] LSTM-based Deep Learning Models for Non-factoid Answer Selection

質問と回答をそれぞれLSTMに入力して出力ベクトルのコサイン値のヒンジ損失を最小化するように学習します。

質問と回答は自然言語のため、単語単位にword2vecでベクトル化(Word Embedding)を行い、LSTMに入力します。

LSTMの出力を平均もしくは最大プーリングした出力ベクトルのコサイン値について、正解と不正解のマージンを最大化(ヒンジ損失を最小化)します。


これを一から実装するのは大変そうなので、公開されている実装がないか調べたところ、Kerasでの実装が見つかりました。
github.com

ただし、この実装は質問と回答のLSTMの出力ベクトルをマージして正解か不正解かを2値で出力し、損失関数をcategorical_crossentropyとしています。

どちらの方がよいかは実験してみないとわかりませんが、とりえずこのKerasの実装を試してみました。

準備

適当な作業ディレクトリに移動して、gitでリポジトリをcloneします。

git clone https://github.com/sujitpal/dl-models-for-qa.git

README.mdに書かれているとおり、データセットとモデル格納用のディレクトリを作成します。

cd dl-models-for-qa
mkdir data
cd data
mkdir comp_data
mkdir models

データセットダウンロード

README.mdに書かれている「AI2 8th Grade Science Questions (No Diagrams)」データセットをダウンロードします。
リンク先のURLでは見つからなかったので、以下の場所から入手しました。
dataset-sts/data/hypev/ai2-8grade at master · brmson/dataset-sts · GitHub
に書かれている
http://aristo-public-data.s3.amazonaws.com/AI2-8thGr-NDMC-Feb2016.zip
をダウンロードします。

ダウンロードしたzipに含まれる8thGr-NDMC-Train.csvを使用します。

データの加工

ダウンロードしたファイルはそのままではファイル形式が異なるため使用できません。

No. <tab> 質問文 <tab> 正解 <tab> 回答文A <tab> 回答文B <tab> 回答文C <tab> 回答文D

という形式に加工します。

以下のようなスクリプトでカンマ区切りのCSVをタブ区切りに変換した後、テキストエディタ正規表現置換で回答の選択肢を分割して、Excelで列を並べ替えました。

import argparse
import pandas as pd
import re

parser = argparse.ArgumentParser(description='convert csv')
parser.add_argument('input', type=str, help='input')
parser.add_argument('output', type=str, help='output')
args = parser.parse_args()

df = pd.read_csv(args.input)
df.to_csv(args.output, sep='\t', columns=('AnswerKey', 'question'), header=False, index=False)

文字化けする行があったので削除しました。
保存するときは改行コードをLFにする必要があります。

加工したファイルを、上記で作成したcomp_dataディレクトリに格納します。

Word2Vecの学習済みモデルのダウンロード

単語のベクトル化(Word Embedding)には、word2vecの学習済みモデルを使用します。
README.mdに書かれているGoogleNews Word2Vec modelから、GoogleNews-vectors-negative300.bin.gzをダウンロードして、上記で作成したcomp_dataディレクトリに格納します。

Python3用にスクリプト修正

Python3を使用している場合は、Python2用のコードになっているため修正が必要です。
フォークして修正したレポジトリを公開しました。
GitHub - TadaoYamaoka/dl-models-for-qa: Keras DL models to answer 8th grade science multiple choice questions (Kaggle AllenAI competition).

学習実行

srcディレクトリに移動して学習用スクリプトを実行します。
複数アルゴリズムが実装されていますが、今回は「QA-LSTM with Attention」を試しました。

python qa-lstm-attn.py

以下のように結果が表示されます。

C:\Anaconda3\lib\site-packages\gensim\utils.py:860: UserWarning: detected Windows; aliasing chunkize to chunkize_serial
  warnings.warn("detected Windows; aliasing chunkize to chunkize_serial")
Using TensorFlow backend.
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:135] successfully opened CUDA library cublas64_80.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:135] successfully opened CUDA library cudnn64_5.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:135] successfully opened CUDA library cufft64_80.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:135] successfully opened CUDA library nvcuda.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:135] successfully opened CUDA library curand64_80.dll locally
Loading and formatting data...
(805, 393) (345, 393) (805, 393) (345, 393) (805, 2) (345, 2)
Loading Word2Vec model and generating embedding matrix...
Building model...
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "BestSplits" device_type: "CPU"') for unknown op: BestSplits
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "CountExtremelyRandomStats" device_type: "CPU"') for unknown op: CountExtremelyRandomStats
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "FinishedNodes" device_type: "CPU"') for unknown op: FinishedNodes
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "GrowTree" device_type: "CPU"') for unknown op: GrowTree
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "ReinterpretStringToFloat" device_type: "CPU"') for unknown op: ReinterpretStringToFloat
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "SampleInputs" device_type: "CPU"') for unknown op: SampleInputs
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "ScatterAddNdim" device_type: "CPU"') for unknown op: ScatterAddNdim
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "TopNInsert" device_type: "CPU"') for unknown op: TopNInsert
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "TopNRemove" device_type: "CPU"') for unknown op: TopNRemove
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "TreePredictions" device_type: "CPU"') for unknown op: TreePredictions
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "UpdateFertileSlots" device_type: "CPU"') for unknown op: UpdateFertileSlots
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:885] Found device 0 with properties:
name: GeForce GTX 1080
major: 6 minor: 1 memoryClockRate (GHz) 1.8225
pciBusID 0000:01:00.0
Total memory: 8.00GiB
Free memory: 6.63GiB
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:906] DMA: 0
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:916] 0:   Y
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:975] Creating TensorFlow device (/gpu:0) -> (device: 0, name: GeForce GTX 1080, pci bus id: 0000:01:00.0)
qa-lstm-attn.py:73: UserWarning: The `Merge` layer is deprecated and will be removed after 08/2017. Use instead layers from `keras.layers.merge`, e.g. `add`, `concatenate`, etc.
  attn.add(Merge([qenc, aenc], mode="dot", dot_axes=[1, 1]))
qa-lstm-attn.py:79: UserWarning: The `Merge` layer is deprecated and will be removed after 08/2017. Use instead layers from `keras.layers.merge`, e.g. `add`, `concatenate`, etc.
  model.add(Merge([qenc, attn], mode="sum"))
Training...
C:\Anaconda3\lib\site-packages\keras\models.py:834: UserWarning: The `nb_epoch` argument in `fit` has been renamed `epochs`.
  warnings.warn('The `nb_epoch` argument in `fit` '
Train on 724 samples, validate on 81 samples
Epoch 1/20
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\pool_allocator.cc:247] PoolAllocator: After 37467 get requests, put_count=9098 evicted_count=1000 eviction_rate=0.109914 and unsatisfied allocation rate=0.786532
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\pool_allocator.cc:259] Raising pool_size_limit_ from 100 to 110
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\pool_allocator.cc:247] PoolAllocator: After 7998 get requests, put_count=16010 evicted_count=8000 eviction_rate=0.499688 and unsatisfied allocation rate=0
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\pool_allocator.cc:247] PoolAllocator: After 17994 get requests, put_count=36006 evicted_count=18000 eviction_rate=0.499917 and unsatisfied allocation rate=0
(略)
Epoch 20/20
704/724 [============================>.] - ETA: 0s - loss: 0.0199 - acc: 0.9986Epoch 00019: val_loss did not improve
724/724 [==============================] - 21s - loss: 0.0195 - acc: 0.9986 - val_loss: 2.9519 - val_acc: 0.6543
Evaluation...
345/345 [==============================] - 1s
Test loss/accuracy final model = 2.7262, 0.6638
345/345 [==============================] - 1s
Test loss/accuracy best model = 0.6961, 0.7275

最終的なTest accuracyが、0.6638になっています。
README.mdより良い結果ですが、データセットの差と思われます。

README.mdの「QA-LSTM with Attention + Custom Embedding」ではさらに精度が上がっているので、word2vecのモデルを最適化すればさらに精度があがるようです。


この方法を応用することで、質問に回答するBotなどが作れると思います。
この実装で使用できるのは英語のみなので、日本語処理はこれとは別に対応が必要になります。

WindowsでKerasを使う

とある理由でKerasを使い始めました。
備忘録を兼ねてWindowsでバックエンドにTensorFlowを使用してKerasを使う方法について書きます。

環境

  • Windows 10 Home 64bit
  • Python 3.5.2(Anaconda 4.2.0 (64-bit))
  • Tensorflow-gpu (1.0.1)
  • Keras (2.0.3)

TensorFlowのインストール

まず、WindowsにTensorFlowをインストールします。
インストール手順は以前書いた日記を参考にしてください。
tadaoyamaoka.hatenablog.com

Kerasのインストール

コマンドプロンプトまたは、PowerShellから、

pip install keras

pythonコマンドでインタープリタを起動して、

import keras
keras.__version__

で、

'2.0.3'

と表示されればインストール成功です。

MNISTサンプルの実行

まず初めに行うことと言ったらMNISTサンプルの実行です。

コマンドプロンプトまたはPowerShellで適当な作業ディレクトリに移動して、リポジトリをcloneします。

git clone https://github.com/fchollet/keras.git

MNISTサンプルを実行します。

cd keras
cd examples
python mnist_mlp.py

実行結果は、以下のようになります。

Using TensorFlow backend.
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:135] successfully opened CUDA library cublas64_80.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:135] successfully opened CUDA library cudnn64_5.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:135] successfully opened CUDA library cufft64_80.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:135] successfully opened CUDA library nvcuda.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:135] successfully opened CUDA library curand64_80.dll locally
Downloading data from https://s3.amazonaws.com/img-datasets/mnist.npz
60000 train samples
10000 test samples
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "BestSplits" device_type: "CPU"') for unknown op: BestSplits
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "CountExtremelyRandomStats" device_type: "CPU"') for unknown op: CountExtremelyRandomStats
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "FinishedNodes" device_type: "CPU"') for unknown op: FinishedNodes
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "GrowTree" device_type: "CPU"') for unknown op: GrowTree
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "ReinterpretStringToFloat" device_type: "CPU"') for unknown op: ReinterpretStringToFloat
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "SampleInputs" device_type: "CPU"') for unknown op: SampleInputs
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "ScatterAddNdim" device_type: "CPU"') for unknown op: ScatterAddNdim
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "TopNInsert" device_type: "CPU"') for unknown op: TopNInsert
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "TopNRemove" device_type: "CPU"') for unknown op: TopNRemove
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "TreePredictions" device_type: "CPU"') for unknown op: TreePredictions
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "UpdateFertileSlots" device_type: "CPU"') for unknown op: UpdateFertileSlots
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
dense_1 (Dense)              (None, 512)               401920
_________________________________________________________________
dropout_1 (Dropout)          (None, 512)               0
_________________________________________________________________
dense_2 (Dense)              (None, 512)               262656
_________________________________________________________________
dropout_2 (Dropout)          (None, 512)               0
_________________________________________________________________
dense_3 (Dense)              (None, 10)                5130
=================================================================
Total params: 669,706
Trainable params: 669,706
Non-trainable params: 0
_________________________________________________________________
Train on 60000 samples, validate on 10000 samples
Epoch 1/20
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:885] Found device 0 with properties:
name: GeForce GTX 1080
major: 6 minor: 1 memoryClockRate (GHz) 1.8225
pciBusID 0000:01:00.0
Total memory: 8.00GiB
Free memory: 6.63GiB
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:906] DMA: 0
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:916] 0:   Y
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:975] Creating TensorFlow device (/gpu:0) -> (device: 0, name: GeForce GTX 1080, pci bus id: 0000:01:00.0)
60000/60000 [==============================] - 3s - loss: 0.2445 - acc: 0.9252 - val_loss: 0.1483 - val_acc: 0.9543
(略)
Epoch 20/20
60000/60000 [==============================] - 1s - loss: 0.0177 - acc: 0.9953 - val_loss: 0.1100 - val_acc: 0.9841
Test loss: 0.110033185067
Test accuracy: 0.9841
CNN版

上記で学習したのは隠れ層が2層のパーセプトロン(MLP)なので、次は畳み込みニューラルネットワーク(CNN)を試します。

python mnist_cnn.py

以下のように結果が表示されます。

Using TensorFlow backend.
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:135] successfully opened CUDA library cublas64_80.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:135] successfully opened CUDA library cudnn64_5.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:135] successfully opened CUDA library cufft64_80.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:135] successfully opened CUDA library nvcuda.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:135] successfully opened CUDA library curand64_80.dll locally
x_train shape: (60000, 28, 28, 1)
60000 train samples
10000 test samples
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "BestSplits" device_type: "CPU"') for unknown op: BestSplits
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "CountExtremelyRandomStats" device_type: "CPU"') for unknown op: CountExtremelyRandomStats
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "FinishedNodes" device_type: "CPU"') for unknown op: FinishedNodes
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "GrowTree" device_type: "CPU"') for unknown op: GrowTree
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "ReinterpretStringToFloat" device_type: "CPU"') for unknown op: ReinterpretStringToFloat
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "SampleInputs" device_type: "CPU"') for unknown op: SampleInputs
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "ScatterAddNdim" device_type: "CPU"') for unknown op: ScatterAddNdim
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "TopNInsert" device_type: "CPU"') for unknown op: TopNInsert
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "TopNRemove" device_type: "CPU"') for unknown op: TopNRemove
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "TreePredictions" device_type: "CPU"') for unknown op: TreePredictions
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_kernel.cc:943] OpKernel ('op: "UpdateFertileSlots" device_type: "CPU"') for unknown op: UpdateFertileSlots
Train on 60000 samples, validate on 10000 samples
Epoch 1/12
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:885] Found device 0 with properties:
name: GeForce GTX 1080
major: 6 minor: 1 memoryClockRate (GHz) 1.8225
pciBusID 0000:01:00.0
Total memory: 8.00GiB
Free memory: 6.63GiB
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:906] DMA: 0
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:916] 0:   Y
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:975] Creating TensorFlow device (/gpu:0) -> (device: 0, name: GeForce GTX 1080, pci bus id: 0000:01:00.0)
60000/60000 [==============================] - 6s - loss: 0.3410 - acc: 0.8981 - val_loss: 0.0830 - val_acc: 0.9747
(略)
Epoch 12/12
60000/60000 [==============================] - 5s - loss: 0.0380 - acc: 0.9886 - val_loss: 0.0282 - val_acc: 0.9909
Test loss: 0.0282454267572
Test accuracy: 0.9909

Test accuracy: 0.9909と、MLPの0.9841より精度が上がっています。

サンプルを続けて実行すると、最後に

Exception ignored in: <bound method BaseSession.__del__ of <tensorflow.python.client.session.Session object at 0x00000235B0A0EEB8>>
Traceback (most recent call last):
  File "C:\Anaconda3\lib\site-packages\tensorflow\python\client\session.py", line 582, in __del__
AttributeError: 'NoneType' object has no attribute 'TF_DeleteStatus'

というエラーが出力される場合があります。

実行が終わった後なので特に問題ありませんが、コードの末尾に

import gc; gc.collect()

と記述するとエラーが出なくなるようです。
python - Tensorflow AttributeError: 'NoneType' object has no attribute 'TF_DeleteStatus' - Stack Overflow