Wikipediaのdumpからinfoboxの内容や文章を取ってくる方法

こんにちは!

Link-Uの町屋敷です。

今回はWikipediaの本文を収集する方法と特定のInfoboxを収集する方法を書いていきます。

Wikipediaから文章を取ってくる

Wikipediaの文章を取ってくる方法は主に以下の2つです。

  1. MediaWikiのAPIを使う
  2. 同じくMediaWikiが提供しているXML形式のダンプファイルを使う。

APIを使う方法のほうが簡単ですが、Wikipediaはクロールが禁止されているので、データセットの作成には方法2を使わざるを得ません。チャットボットとかなら1で良いでしょう。

Wikipedia Extractorを使う

今回はWikipediaExtracctorを使って本文を取得します。

まず、最新のMediaWikiが提供しているXML形式のダンプファイルの中から、

jawiki-latest-pages-articles.xml.bz2

をダウンロード

次に適当なフォルダに移動して

git clone git@github.com:attardi/wikiextractor.git
cd wikiextractor
python python setup.py install

でwikiextractorをインストール出来ます。

自分はminicondaの仮想環境に入ってからやりました。

早速本文を抽出してみましょう。

WikiExtractor.py --json --keep_tables -s --lists -o ../whole_data/ ~/Downloads/jawiki-latest-pages-articles.xml

コマンド説明
--json        : 出力をjsonにする
--keep_tables : 文章内の表を出力するようにする
-s            : セクション情報も出力するようにする
--list        : 文章内のリストを出力するようにする
-o            : 出力ファルダ名

最後のパスがさっきダウンロードしたxmlの入力ファイルです。

このフォイルはダウンロード時bz2という謎の形式で圧縮されています。

wikiextractorで使う分には圧縮されたままで良いのですが、後で中身を見る必要が出てくるので先に解凍しちゃいましょう。linuxなら

bzip2 -d jawiki-latest-pages-articles.xml.bz2 

で解凍できます。

Windowsの場合はLhaplusを使うと良いでしょう。(なぜか7zipだと失敗した)

少し話がそれましたが、先程のスクリプトを実行するとわらわらとファイルが生成されて、

中を見ると文章が取得できているのでめでたしめでたし。

と、思いきやよく見るInfoboxが取得できていない。

Infoboxとはなんぞやと言う事ですが、要はWikipedia見てると右側にわりといる情報が書かれた表です。

これ機械学習だと正解ラベルになるようなかなり重要なこと書かれてるのになんで無いの?

最初–keep_tablesのオプション使っていなくて絶対コレじゃんと思ってやってみたが増えたのは文章の途中に出てくる表だけだった。残念。

XMLから特定のInfoboxの情報を取得する

Infoboxの情報がどうしてもほしいのでネットで色々検索したが、出てくるのはAPIを使ったものばかり。

WikipediaのXMLをJSONに変換するツールなども試してみたがうまく動かず、

そんなこんなで1日くらいあがいても良いライブラリは見つからなかった。

こうなったら自分で作るしか無い

というわけで、XMLから特定のInfoboxの情報を取得してjsonで保存するスクリプトをPython3で作成した。

通常の文章はさっきほど抽出したものがあるので、必要なInfoboxが存在する項目だけタイトルとタイトルIDとセットで保存して、使うときに結合させるようにする。

どうせやるなら一緒に本文も保存したら良いじゃないかってなりそうだが、xml本文中にはいらないものが大量に含まれていて、消す作業が面倒だったので保留。

幸いあがいてる途中に参考になるサイトを見つけたので、大体はこれに沿ってやります。

プログラム全文はページの一番下。特に特別なパッケージは必要ない

大事なところだけ解説すると、

PythonでXMLからタグを取ってくる方法は、普通は

import xml.etree.ElementTree as ET
tree = ET.parse('country_data.xml')
root = tree.getroot()

こんな感じだが、今回はxmlファイルが馬鹿でかいのでこれを行うとメモリが死ぬ可能性がある。

なので、イテレータを用いて処理する。

for event, elem in etree.iterparse(pathWikiXML, events=('start', 'end')): 

処理は上のタグから一つずつ進行していく、例えば

<a>
    <b>0</b>
    <c>
      <d>70334050</d>
    </c>
</a>

このようなタグが来た時は、elem.tagにはa->b->b->c->d->d->c->aの順番でデータが入り、

開きタグなのか閉じタグなのかは,eventを見て判断する。

elem.textにそのタグに囲まれた部分の文章がひるようだ、

ここで、実際のxmlがどんな形式ななっているか確認しましょう。

less ~/Downloads/jawiki-latest-pages-articles.xml

巨大なファイルなので通常のテキストエディタでは開きません。

どうやら<page>~</page>でwikipediaの各ページが表現されていて、

<title>にタイトル

<id>にページ番号

<text>にメインの文章が書かれているようだ。

ただ、Infoboxは少々面倒で、特定のタグに囲われているわけでもなく、<text></text>の中に

{{東京都の特別区
|画像 = [[File:Sensoji 2012.JPG|200px]]
|画像の説明 = [[浅草寺]]境内
|区旗 = [[File:Flag of Taito, Tokyo.svg|100px]]
|区旗の説明 = 台東[[市町村旗|区旗]]&lt;div style=&quot;font-size:smaller&quot;&gt;[[1965年]][[6月4日]]制定
|区章 = [[File:東京都台東区区章.svg|75px]]
|区章の説明 = 台東[[市町村章|区章]]&lt;div style=&quot;font-size:smaller&quot;&gt;[[1951年]][[4月18日]]制定&lt;ref&gt;「東京都台東区紋章制定について」昭和26年4月18日台東区告示第47号&lt;/ref&gt;
|自治体名 = 台東区
|コード = 13106-7
|隣接自治体 = [[千代田区]]、[[中央区 (東京都)|中央区]]、[[文京区]]、[[墨田区]]、[[荒川区]]
|木 = [[サクラ]]
|花 = [[アサガオ]]
|郵便番号 = 110-8615
|所在地 = 台東区[[東上野]]四丁目5番6号&lt;br /&gt;&lt;small&gt;{{ウィキ座標度分秒|35|42|45.4|N|139|46|47.9|E|region:JP-13_type:adm3rd|display=inline,title}}&lt;/small&gt;&lt;br /&gt;[[File:Taito Ward Office.JPG|250px|台東区役所庁舎(東上野四丁目)]]
|外部リンク = [http://www.city.taito.lg.jp/ 台東区]
|位置画像 = {{基礎自治体位置図|13|106}}
|特記事項 =}}

突然現れるだけで文章からの抽出が必要。

どうやら<text>中で{{}}で表されているものは複数あるらしく、単純に{{}}に囲われた部分を抽出してもうまく行かない。

そこでInfoboxの名前を使う。名前は{{のすぐ後に書いているやつで、lessでそのInfoboxが使われている項目を検索して直接確認するか、Wikipediaの基本情報テンプレートに書いてあるTemplate:Infobox ~ の項目を確認する。

                    elem.text = re.sub('{{[F|f]lagicon.*?}}', '', elem.text)
                    infobox = re.findall('{{{0}n.*?|.*?}}'.format(INFOBOX_SEARCH_WORD), elem.text, re.MULTILINE | re.DOTALL)

ここがその抽出部分で、INFOBOX_SEARCH_WORDにInfoboxの名前を入れるとその名前に対応するInfoboxを抽出できる。ただし、{{}}が入れ子になっているとバグるので、re.subで事前にいらないものを消している。

reの代わりにregexを使うと入れ子でも処理できるようだが、今回選んだInfoboxではこれで大丈夫だったので保留。

試しに上のWikipedia画面のキャプチャで囲んでいた漫画のInfobox(Infobox名 animanga/Manga)で試してみた結果がこちら。

ちゃんと取得できていることが確認できた。

参考サイト

https://www.heatonresearch.com/2017/03/03/python-basic-wikipedia-parsing.html

プログラム全文

import xml.etree.ElementTree as etree
import time
import os
import json
import re
import collections as cl

PATH_WIKI_XML = '/home/machiyahiki/Downloads/'
FILENAME_WIKI = 'jawiki-latest-pages-articles.xml'
JSON_SAVE_DIR = '.'
INFOBOX_SEARCH_WORD = 'Infobox animanga/Manga'

ENCODING = "utf-8"

def strip_tag_name(t):
    idx = t.rfind("}")
    if idx != -1:
        t = t[idx + 1:]
    return t

pathWikiXML = os.path.join(PATH_WIKI_XML, FILENAME_WIKI)

totalCount = 0
articleCount = 0
redirectCount = 0
templateCount = 0
title = None
start_time = time.time()
dict_array = []

with open('{0}//wiki_infobox_{1}.json'.format(JSON_SAVE_DIR, re.sub('[ |//]', '_',  INFOBOX_SEARCH_WORD)),'w', encoding='utf_8') as jf:
    for event, elem in etree.iterparse(pathWikiXML, events=('start', 'end')):
        tname = strip_tag_name(elem.tag)
    
        if event == 'start':
            if tname == 'page':
                inrevision = False
                findinfobox = False
                data_dict = cl.OrderedDict()
            elif tname == 'revision':
                # Do not pick up on revision id's
                inrevision = True
            if tname == 'title':
                data_dict['title'] = elem.text
            elif tname == 'id' and not inrevision:
                data_dict['id'] = elem.text
        else:
            if tname == 'text':
                if elem.text:
                    elem.text = re.sub('{{[U|u]nicode.*?}}', '', elem.text)
                    elem.text = re.sub('{{[F|f]lagicon.*?}}', '', elem.text)
                    infobox = re.findall('{{{0}n.*?|.*?}}'.format(INFOBOX_SEARCH_WORD), elem.text, re.MULTILINE | re.DOTALL)
                    if infobox:
                        findinfobox = True
                        data_dict['infobox'] = infobox
            if tname == 'page':
                if findinfobox:
                    if data_dict['title']: #タイトル名に{{Unicode}}があるとnullが入る、後で修可
                        if 'プロジェクト:' in data_dict['title']: #プロジェクト:から始まる項目は無視
                            continue
                        if 'Template:Infobox' in data_dict['title']: #Template:Infoboxから始まる項目も無視
                            continue
                    dict_array.append(data_dict)
                    
                
        if len(dict_array) > 1 and (len(dict_array) % 10000) == 0:
            print("{:,}".format(len(dict_array)))

        
        elem.clear()
    json.dump(dict_array, jf, ensure_ascii=False)
    
    
elapsed_time = time.time() - start_time

print("Total pages: {:,}".format(len(dict_array)))
print("Elapsed Time: {:,}".format(elapsed_time))   
                
    

GPUを使って無線LANを(略) 番外編 GPUなしでCUDAは動かせるのか?

紅葉への切り替わり中です。秋って感じですね。

でも町を歩いていると、8月末くらいには紅葉のポスターが貼られだして、ハロウィン一色になったなぁ、と思ったら、今はもうクリスマス一色です。町から紅葉の絵はなくなり、雪の結晶やクリスマスツリーの絵ばかり。気が早えぇなぁ、人間。

わたしは最近こういう「イメージ」も「拡張現実」の一種なんじゃ無いかと感じています。ARゴーグルだけが「拡張現実」では、ないんじゃない?

みなさんはどう思います?

…と聞いてみるにもコメント欄ないんだったわ、このブログ。トラックバックもない。…時代は変わってしまったなぁ。

GPUサーバがない!

というわけで、前回山の上で瞑想して考えた「ぼくの考えた最強のベンチマーク・ルール」にしたがってガンガンPyritを書き換えていきたいところ…なのですが、なんと今回はオトナの事情により、GPUサーバは使えないらしいのです。かなしい。

しかし、案ずることなかれ。

こんな事もあろうかと、もう2枚ほどGeForce 1080を用意しておきました。

これをどこのご家庭にもある、PCI-Express用の電源ポートがなぜか10個あってPCI-Express x16のスロットがなぜか4つあるデスクトップパソコンに刺してPyritの改造をしていきましょう。

がっちゃん(GPUを突き刺す音)

カチッ(電源を押す音)

ブワー(電源がつきファンが回る音)

ピカピカピカ(1680万色に光るゲーミングマザーボードのLED)

…(しかしモノリスのように真っ黒なまま沈黙するモニタ)

…(その前でなす術もなく立ち尽して一緒に沈黙する筆者)

…(なす術もなく立ち尽くす筆者)

……(立ち尽くす筆者)

…………

……………はぁ〜〜〜〜〜〜〜

なんで画面が出ないんだよ〜〜〜〜〜〜〜〜

グラフィック・ボード」じゃなかったのかよ〜〜〜〜〜〜〜

…嘆いていてもしょうがないので、GPUなしでなんとかする方法を考えましょう。

遅くてよいので、CPUだけでCUDAのコードを動かせないものか…。

原理的には可能なはずですよね。CUDAはただ計算が早くなったり(ならなかったり)するだけの技術ですから。同じPCIeの拡張カードでも、真の乱数発生ボードと違って、原理的にはエミュレーションできる事は明らかです。原理的には、だけどね。

わたくし結構macでノマドもするので、できればLinuxだけじゃなくてmacでも動いて欲しいなぁ(欲を出しておく)。

あーそうそう、画面が出ない理由ですが、Radeonと一緒に刺すとダメみたいです。グラボのバグなのか、Linuxのバグなのか、その辺は、よくわからない。

代案を調べる

代案 その1:gpuocelot

gpuocelot公式サイトより引用

gpuocelotは、CUDAの実行ファイルであるPTXをnVidiaのGPUだけでなく、AMDのGPUやCPUで直接実行したり、エミュレーションしたり、ネットワーク経由で他のマシンのGPU(やCPU)で実行したりできるようにするぜ!というそのものズバリわたしが求めていたソフトウェアです。

が、公式サイトいわく「The GPU Ocelot project is no longer actively maintained.」ということで開発終了。まぁ大学製ソフトなので、資金が尽きたのか論文書き尽くしたのか、まぁ、そんな所でしょうか。しかも最後の論文が2014年なのであまり期待はできなさそうです。

あと個人的なプログラマ、あるいは大学院生しての直感が告げているのですが、こんな野心的なソフト、本当に安定して動くのかな…論文書くための機能が一応動いてるだけじゃないかな…という疑念が正直あります。

はい次。

代案 その2: MCUDA

The MCUDA translation framework is a linux-based tool designed to effectively compile the CUDA programming model to a CPU architecture.

IMPACT: MCUDA Toolset

こちらも大学製。何か書くにも、一切ドキュメントがない。

はい次。

代案 その3:CUDA Waste

GPUocelotはLinux専用らしく(gpuocelotのページにはそんな事書いてなかったけどな!)、そのためにWindows用に同じようなソフトウェアを開発したのがこちらだそうです。こちらもだいぶ前から更新がないので実用は難しそう。あとわたしWindows持ってない。

はい次。

代案その4: CU2CL

CUDAのソースコードをOpenCLのソースコードに変換するというソフトウェア。今までのソフトウェアに比べると大分「現実的」な落とし所なような気はします。

が、実際にこれを使おうとすると、CUDAを実行していた部分をOpenCLを実行するためのコードにモリモリ書き換えないといけませんし、そもそもPyritにはOpenCLのコードは最初から入っています。

悪くは無いんだけど、今回の場合は色々な意味で本末転倒。

はい次。

代案その5: NVEmulate

これはなんとNVIDIA公式。古いGPUのマシンでも、新しいGPUを要求するソフトウェアを動かせるようにするためのソフトウェアだそうです。しかし2014年から更新がなく、ついでに言えばWindows専用。よしんばWindowsを持っていたとして、CUDAが動くかどうかは怪しいです。ゲーム開発者のデバッグ用っぽい。

「非常に低速」であることには違いないが,例えば,GeForce FXシリーズのマシンでGeForce 6800のデモであるNaluやTimburyが動いてしまうという素晴らしいツールだ(普通の人用には,これ以外の用途を思いつかないのが残念だが)。
手持ちのGeForce FX 5900搭載マシンでは,Naluが0.1fps程度で動いている。

4Gamer.net ― 君のマシンでNaluが動く! GPUエミュレータNVemulate公開

はい次。

代案その6: nvcc -deviceemu

nvcc –deviceemu <filename>.cu

  • デバイスエミュレーションモードでビルド
  • デバッグシンボルなし、CPUですべてのコードを実行

NVIDIA – CUDA実践エクササイズ

こちらも公式。CUDAコンパイラであるnvccが、CUDAソースコードのうち、GPUで走るはずの部分も全部CPUで走るようにコンパイルしてくれるんだそうな。

これはかなり理想的かつ現実的なアプローチに思えます。PyritのCUDAモジュールをコンパイルする時にこのオプションをつけておくだけでいいのでラクチンですし、mac用のnvccもあるのでノマドワーカーにも優しい。そしてPTXを動的に変換するような大道芸はしない、安定したアプローチ。

しかし

 % cd /Developer/NVIDIA/CUDA-9.2/bin
 % ./nvcc --help | grep deviceemu
 %

とっくの昔にそんなものは無くなっていたのであった。

CUDA Emulation Modeというのもあった(GPUの代わりにCPUのエミュレータが出てくる)というのも昔はあったようなのですが、これも今はもうなさそう。

NVIDIAからの「GPU買ってね!」という熱いメッセージだと思っていいのかな…。でもそれ以前に動かないんだけど。

gpuocelotを試す

現実的に使えそうなものはgpuocelotぐらいなので、これちょっと試してみましょう。Instlationページによると、WindowsはExperimental(もう二度とExperimentalが外れることはなさそうだけど)で、macは最初から言及すらないので、ノマドは諦めてLinuxでやりましょう。

Ubuntu 18.04でやる事にします。

ビルド・インストラクションに沿って(かつGCC4.8では動かないなどの諸々の注意書きを見なかった事にして)やってみると:

ocelot/ir/implementation/Module.cpp:712:46: error: enum constant in boolean context [-Werror=int-in-bool-context]
     if (prototypeState == PS_ReturnParams || PS_Params) {
                                              ^~~~~~~~~
cc1plus: all warnings being treated as errors
scons: *** [.release_build/ocelot/ir/implementation/Module.os] Error 1
Build failed...
Build failed

「PS_Paramsはenumなのでboolの値としては使えないんだけど…!」という、もっともなエラー。しかしどう直せばいいのか。||の前を見る限り、”prototypeState == “を忘れたんですかね?

…これ…コンパイルエラーじゃなかったら、常に”true”になるif文としてコンパイルされてたんじゃないかな…。それでいいのかな…この世はコワイネ。

とりあえず追加してビルド再開。

In file included from ./ocelot/parser/interface/PTXLexer.h:11:0,
                 from ./ocelot/parser/interface/PTXParser.h:16,
                 from ocelot/ir/implementation/Module.cpp:10:
.release_build/ptxgrammar.hpp:354:22: error: 'PTXLexer' is not a member of 'parser'
 int yyparse (parser::PTXLexer& lexer, parser::PTXParser::State& state);
                      ^~~~~~~~
...(略)...
.release_build/ptxgrammar.hpp:354:47: error: 'parser::PTXParser' has not been declared
 int yyparse (parser::PTXLexer& lexer, parser::PTXParser::State& state);
                                               ^~~~~~~~~
...(略)...

scons: *** [.release_build/ocelot/ir/implementation/Module.os] Error 1
Build failed...
Build failed

parser::PTXLexerや、parser::PTXParser::Stateを宣言していないのに、yyparse関数のパラメータとして使ってるぞ!という、これまたもっともなコンパイルエラー。

ptxgrammar.hppはyaccから生成されるファイルで、NVIDIAのGPUの命令セット、PTXのパーサーのようです。で、このパーサーがparse::PTXParser::Stateを利用すると。

で、PTXParserを見ると次のようになっていて、プログラムの他の部分から使うための外面の実装のようです。そのため、ptxgrammar.hppにガッツリ依存しております。

namespace parser
{
	/*! brief An implementation of the Parser interface for PTX */
	class PTXParser : public Parser
	{
		public:
			class State
			{
				public:
					class OperandWrapper
...(略)...

必然的に、循環参照が発生しています。

PTXParserはyaccの生成したコードに依存していて、yaccの生成したコードはPTXParser::Stateに依存している。

で、コンパイラは、どちらを先に読み込んだとしても、知らないクラスや知らない定数がいきなり出てくるので、よくわかんなくなって、困ってしまう、と。

こういうシチュエーションはC++ではよくあることで、そのためにクラスの前方宣言という機能が用意されております:

namespace hoge{
class Fuga; // これが前方宣言。
}

// ポインタ、もしくは参照としてなら使える
int foo(hoge::Fuga& fuga);

namespace hoge {
// 実際の宣言
class Fuga {
...(略)...
}
}

こんな感じで最初に「hogeというネームスペースの中にFugaというクラスがあるぞ!」とコンパイラに教えておいてあげると、foo関数の定義を見たコンパイラは、「よくわかんないけど、そういうクラスがあるんやな」となんとなく察してコンパイルを続行することが出来ます(逆に、前方宣言をしないと「hoge::Fugaって何?」とエラーを吐いて止まってしまいます)。ただ、あくまで「なんとなく」なので、ポインタか参照としてでしか使えません(もっと詳しくはコンパイラの本かC++の本を読んでね)。

じゃあ今回もそうするか…と言いたいところなのですが、parser::PTXLexerはともかく、parser::PTXParser::Stateクラスはふつうのクラスではありません。上で見たとおり、Parserクラスの中に存在するクラス、「インナークラス」です。C++では、残念ながらインナークラスの前方宣言は出来ません。もしできれば万事解決しそうなのですが、C++の仕様上できないので仕方がない(どうしてそういう仕様なのかは、謎)。

まぁ、このインナークラスが前方宣言できなくて困るシチュエーションも、C++ではよくあることです。じゃあ、そういう時はどうするのかというと、普通に設計を見直してリファクターします

…はぁ…。まぁ、「動かない」ってことで、いいか…。でも、なんで今までこれでコンパイル通ってたんだろう?

今回のまとめと次回のPyrit

GPUなしでCUDAの実行をするのは、現実的にはどうやら難しいらしいことがわかりました。

CUDA用の純正コンパイラ「nvcc」に、CPUでエミュレートするためのコンパイルフラグが、過去には存在していたようですが、現在は跡形もなく無くなってしまいました。CUDAの使えるGPUが非常に限られていた時代ならいざしらず、NVIDIAのカードならどれでもCUDAが使える昨今ではあんまり需要は無くなったからなのか、…それともCUDAがGPGPUとしての覇権を取ったから、傲慢になったのか。その辺はわかりませんが。

ちなみにTensorFlowとかCaffeはCPU用とGPU用のコードが両方ともゴリゴリ書いてあったと記憶しております。大変だねぇ。資本を上げて人月で殴れ!

次回の月刊Pyritの頃にはGPUサーバが使えるようになるはずなので、素直にGPU使って開発しようと思います。