Ahogrammer

Deep Dive Into NLP, ML and Cloud

Wikipediaの前処理はもうやめて「Wiki-40B」を使う

最近の自然言語処理では、大規模なテキストから単語の分散表現や言語モデルを学習させて使っています。学習する際のテキストとしては、分量や利用しやすさの都合からWikipediaが選ばれることが多いですが、その前処理は意外と面倒で時間のかかる作業です。そこで、本記事では比較的最近リリースされた前処理済みのデータセットWiki-40B」とその使い方を紹介します。

Wiki-40Bとは?

Wiki-40Bは、40言語以上のWikipediaを前処理して作られたデータセットです。このデータセットは言語ごとに学習/検証/テスト用に分かれているので、単語分散表現や言語モデルの学習・評価に使えます。言語ごとの対応状況については、以下のページを参照するとよいでしょう。

前処理としては、大きくは以下の2つに分けられます。

  • ページのフィルタリング
  • ページ内の処理

ページのフィルタリングでは、不要なページそのものをフィルタリングして取り除いています。Wikipediaにはコンテンツを表すページだけでなく、以下のようなページが含まれています。これらのページは十分な量のテキストが含まれていなかったり、含まれていてもリストで細切れだったりするので、言語モデルや分散表現の学習には不向きです。そのため、最初に取り除きます。

  • 曖昧さ回避ページ
  • リダイレクトページ
  • 削除済みページ
  • 非エンティティページ(リスト、インフォボックス、画像などのページ)

ページ内の処理では、マークアップやページ内の非コンテンツ部分を除去します。マークアップの除去はみなさんやられていると思いますが、それだけでは十分ではありません。たとえば、参考文献や外部リンク、脚注などの節や、画像やキャプション、リスト、テーブルといった構造化された情報を取り除かなければ質の高いデータセットになりません。以下は非コンテンツ部分の例です。

f:id:Hironsan:20200926115716p:plain
不要な節の例: 参考文献

以下は、従来のWikipediaのデータセットとの比較です。不要な節が除去され、学習に必要な部分は残っていることを確認できます。また、従来の前処理では誤って除去されていたフレーズが残っていることも確認できます。

f:id:Hironsan:20200926121252p:plain
従来のデータセットとの比較。画像は「Wiki-40B: Multilingual Language Model Dataset」より。

Wiki-40Bの使い方

Wiki-40Bは、TensorFlow Datasetsを利用することで、簡単に使えます。まずは、TensorFlow Datasetsをインストールしましょう。ここで注意なのですが、インストールするTensorFlow Datasetsのバージョンはv3.2.0以上にします。Wiki-40B自体はv3.0.0以降で利用可能ですが、v3.2.0からデータセット名が変更されているので、v3.2.0以降を推奨します。

pip install -U tensorflow-datasets

次に、データセットを読み込みます。データセット名はwiki40b/{lang}の形式になっており、{lang}の部分に言語を指定します。今回は日本語を読み込むので、wiki40b/jaを指定します。また、データセットは学習/検証/テスト用に分かれており、それぞれ全記事の90/5/5%を占めています。今回はsplit='test'と指定して、テスト用データセットを読み込むことにしましょう。

import tensorflow_datasets as tfds

ds = tfds.load('wiki40b/ja', split='test')

データセットのレコードは以下のような形式の辞書です。textには前処理済みのテキスト、wikidata_idは対応するWikidataのIDです。version_idが何のバージョンを表すかはよくわかりませんでした。わかる方がいたらコメントで教えて頂けると助かります。

FeaturesDict({
    'text': Text(shape=(), dtype=tf.string),
    'version_id': Text(shape=(), dtype=tf.string),
    'wikidata_id': Text(shape=(), dtype=tf.string),
})

実際のデータは以下のようになっています。

>>> list(ds.as_numpy_iterator())[0]
{'text': b'\n_START_ARTICLE_\n\xe3\x83\x93\xe3\x83\xbc\xe3\x83\x88\xe3\x81'
         b'\x9f\xe3\x81\x91\xe3\x81\x97\xe3\x81\xae\xe6\x95\x99\xe7\xa7\x91'
         b'\xe6\x9b\xb8\xe3\x81\xab\xe8\xbc\x89\xe3\x82\x89\xe3\x81\xaa\xe3'
         b'\x81\x84\xe6\x97\xa5\xe6\x9c\xac\xe4\xba\xba\xe3\x81\xae\xe8\xac'
         b'\x8e\n_START_SECTION_\n\xe6\xa6\x82\xe8\xa6\x81\n_START_PARAGRAP'
         b'H_\n\xe3\x80\x8c\xe6\x95\x99\xe7\xa7\x91\xe6\x9b\xb8\xe3'
         b'\x81\xab\xe3\x81\xaf\xe6\xb1\xba\xe3\x81\x97\xe3\x81\xa6\xe8\xbc'
         b'\x89\xe3\x82\x89\xe3\x81\xaa\xe3\x81\x84\xe3\x80\x8d\xe6\x97\xa5'
         b'\xe6\x9c\xac\xe4\xba\xba\xe3\x81\xae\xe8\xac\x8e\xe3\x82\x84\xe3'
         b'\x81\x97\xe3\x81\x8d\xe3\x81\x9f\xe3\x82\x8a\xe3\x82\x92\xe5\xa4'
         b'\x9a\xe8\xa7\x92\xe7\x9a\x84\xe3\x81\xab\xe6\xa4\x9c\xe8\xa8\xbc'
         b'\xe3\x81\x97\xe3\x80\x81\xe6\x97\xa5\xe6\x9c\xac\xe4\xba\xba\xe3'
         b'\x81\xaeDNA\xe3\x82\x92\xe8\xa7\xa3\xe6\x98\x8e\xe3\x81'
         b'\x99\xe3\x82\x8b\xe3\x80\x82_NEWLINE_\xe6\x96\xb0\xe6'
         b'\x98\xa5\xe7\x95\xaa\xe7\xb5\x84\xe3\x81\xa8\xe3\x81\x97\xe3\x81'
         b'\xa6\xe5\xae\x9a\xe6\x9c\x9f\xe7\x9a\x84\xe3\x81\xab\xe6\x94\xbe'
         b'\xe9\x80\x81\xe3\x81\x95\xe3\x82\x8c\xe3\x81\xa6\xe3\x81\x8a\xe3'
         b'\x82\x8a\xe3\x80\x81\xe5\xb9\xb4\xe6\x9c\xab\xe3\x81\xae\xe5\x8d'
         b'\x88\xe5\x89\x8d\xe4\xb8\xad\xe3\x81\xab\xe5\x86\x8d\xe6\x94\xbe'
         b'\xe9\x80\x81\xe3\x81\x95\xe3\x82\x8c\xe3\x82\x8b\xe3\x81\xae\xe3'
         b'\x81\x8c\xe6\x81\x92\xe4\xbe\x8b\xe3\x81\xa8\xe3\x81\xaa\xe3\x81'
         b'\xa3\xe3\x81\xa6\xe3\x81\x84\xe3\x82\x8b\xe3\x80\x82',
 'version_id': b'1848243370795951995',
 'wikidata_id': b'Q11331136'}

以下はテキストをデコードした結果です。なんらかの構造があることがわかります。

_START_ARTICLE_
ビートたけしの教科書に載らない日本人の謎
_START_SECTION_
概要
_START_PARAGRAPH_
「教科書には決して載らない」日本人の謎やしきたりを多角的に検証し、日本人のDNAを解明する。_NEWLINE_新春番組として定期的に放送されており、年末の午前中に再放送されるのが恒例となっている。

文書の構造は以下の4つのマークアップで表されます。

  • _START_ARTICLE_
  • _START_SECTION_
  • _START_PARAGRAPH_
  • _NEWLINE_

_START_ARTICLE_は記事の始まりを表しており、その後にページタイトルが続きます。_START_SECTION_は節の始まりを表しており、その後には節のタイトルが続きます。_START_PARAGRAPH_は節のタイトルと節内のパラグラフを分割する役割があります。_NEWLINE_は1パラグラフの終わりを表しています。

以上で、Wiki-40Bの使い方の説明は終わりです。今回はTensorFlow Datasetsからの使い方を説明しましたが、Huggingface Datasetsを使って読み込むこともできます。いずれにせよ、最新版のWikipediaを使いたいとか、前処理で落とした情報の中に使いたい情報があるという状況でないなら、Wiki-40Bを使うことで生産性を向上させられるでしょう。

huggingface.co

参考資料

【TensorFlow】StringLookupの使い方

以下のTweetで紹介したTensorFlow Recommendersのコードを見ていたら、StringLookupクラスという見慣れぬクラスを使っていました。あまり紹介している記事を見たことがないので、Tipsとしてどのようなものか紹介します。

StringLookupとは?

StringLookupは、文字列を整数値に変換するためのクラスです。一般に、機械学習のモデルはその入力として、整数値や実数値などの数値をとります。たとえば自然言語処理のような分野では、機械学習のモデルに単語を入力する際、単語の文字列ではなく整数値のIDに変換して入力します。StringLookupはそのようなときに使うクラスであり、内部に持っている変換のためのルックアップテーブルを使って実現しています。

あらかじめ用意しておいたボキャブラリをvocab引数に渡してルックアップテーブルを作成後、変換したいデータを渡して呼び出すことで、文字列を数値に変換することができます。

>>> vocab = ["a", "b", "c", "d"]
>>> data = tf.constant([["a", "c", "d"], ["d", "z", "b"]])
>>> layer = StringLookup(vocabulary=vocab)
>>> layer(data)
<tf.Tensor: shape=(2, 3), dtype=int64, numpy=
array([[2, 4, 5],
       [5, 1, 3]])>

上の例では、StringLookupのvocab引数に変換用のボキャブラリを渡していましたが、adaptメソッドを使ってデータから構築することもできます。読み込んだデータからルックアップテーブルを作成する場合には便利な方法でしょう。

>>> data = tf.constant([["a", "c", "d"], ["d", "z", "b"]])
>>> layer = StringLookup()
>>> layer.adapt(data)
>>> layer.get_vocabulary()
['', '[UNK]', 'd', 'z', 'c', 'b', 'a']

ちなみに、以下のように長さが異なる入力をadaptに与えるとValueErrorが発生します。

>>> data = [["a", "c", "d"], ["d", "z"]]
>>> layer = StringLookup()
>>> layer.adapt(data)
ValueError: Can't convert non-rectangular Python sequence to Tensor.

このようなときは、可変長の入力を扱えるRaggedTensorにすると、ValueErrorが発生しなくなります。

>>> data = tf.ragged.constant([["a", "c", "d"], ["d", "z"]])
>>> layer = StringLookup()
>>> layer.adapt(data)
>>> layer.get_vocabulary()
['', '[UNK]', 'd', 'z', 'c', 'a']

この他、最大トークン数やエンコーディング、OOV、IDから文字列への逆変換などを扱うこともできます。Kerasのレイヤーとして組み込むこともできるようですが、以下の資料によると、tf.data.Datasetmapメソッドなどで前処理として使う方法を推奨しています。

参考資料

GiNZAで解析した依存構造を文節単位で可視化する

自然言語処理において、依存構造解析は多くのアプリケーションに役立つ重要な技術の1つでしょう。たとえば、テキストからさまざまな情報を抽出するシステムやアスペクトベースの評判解析、含意関係認識といった幅広いタスクで役立ちます。日本語で依存構造を解析するためのツールとしては、CaboChaやKNP、GiNZAがあります。本記事では、オープンソースの日本語NLPライブラリであるGiNZA v4.0で追加された文節単位の解析APIを使って、文節単位での依存構造を可視化する方法を紹介します。

記事では要点だけを紹介するので、コードについては以下のノートブックを参照してください。

GiNZA v4の文節単位の解析API

GiNZAでは、Universal Dependenciesの枠組みで依存構造解析をしています。GiNZA v4以前は、以下の図の左側のように、単語単位の依存関係を解析することができました。これはこれでいいのですが、日本語においては単語単位より以下の図の右側のように、文節単位で依存構造を捉えられた方が応用するのに便利です。GiNZA v4では、文節単位で解析をするためのAPIが追加されたので、より日本語の解析で使いやすくなりました。

f:id:Hironsan:20200920190030p:plain
文節を考慮した解析。画像はGiNZA - Japanese NLP Libraryより

新しく追加された文節解析APIの一覧については、以下を参照してください。

依存構造の可視化

依存構造解析の結果を使って情報抽出やアスペクトベースの評判解析を行う場合、解析した依存構造に対してルールを定義して行うことがあるかと思います。そのようなルールを書く場合、依存構造がひと目でわかるように可視化されていると、仕事がしやすくなります。spaCyには組み込みで依存構造の可視化機能があるのですが、普通に使うと、以下のように単語単位での可視化になってしまいます。

f:id:Hironsan:20200920190139p:plain
単語単位の可視化

文節単位で解析した結果を、spaCyの可視化機能に合わせた形式へ変換することで、以下のように文節単位での可視化を行うことができます。単語単位での可視化と比べて、よりスッキリとしてわかりやすくなりました。このように可視化したほうが、OpenIEやアスペクトベース評判解析のために、どういったルールを書くかの分析をしやすくなるのではないでしょうか。もちろん、単語単位の可視化と併用することもできます。

f:id:Hironsan:20200920190200p:plain
文節単位の可視化

もう少し長い文も可視化してみましょう。「デザインがスタイリッシュからどんどん可愛らしくなっています。」を単語単位で可視化すると以下のようになります。

f:id:Hironsan:20200920190236p:plain
単語単位の可視化2

一方で、同じ文を文節単位で可視化すると以下のようになります。

f:id:Hironsan:20200920190305p:plain
文節単位の可視化2

StreamlitによるWebアプリ化

こうして可視化した依存構造は、見出し語や品詞と合わせてインタラクティブに表示できると議論しやすいです。このような機能は、Streamlitを使うことで、Webアプリとして簡単に実現できます(下図)。この仕組みは、後輩の@yasufumyくんがKNPの出力を見やすくするために使ったのがきっかけで知ったのですが、今回はGiNZAで実現するために、そのアイデアを借用させてもらいました。ありがとう!

f:id:Hironsan:20200920190327g:plain
StreamlitによるWebアプリ化

spaCyで可視化する場合は、以下のリポジトリが参考になると思います。

github.com

参考資料