Ahogrammer

Deep Dive Into NLP, ML and Cloud

【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

参考資料

Universal Sentence Encoderをチューニングして多言語のテキスト分類

Googleが開発した多言語の埋め込みモデル「LaBSE」を使って多言語のテキスト分類」と題した記事を書いたところ、「Universal Sentence Encoder(以下、USE)と比べてどうなのか?」というコメントを見かけました。そこで、本記事では、多言語の埋め込み表現を作ることのできる「Multilingual USE(m-USE)」を使って、テキスト分類をしてみます。設定としては前回と同様、学習には英語、評価には日本語とフランス語のデータセットを使います。

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

文類似度

LaBSEの場合と同様に、TensorFlow Hubで公開されているモデルを使って、多言語の文類似度を計算してみます。m-USEには、TransformerベースのモデルとCNNベースのモデルがあります。Transformerベースのモデルは性能が高く、CNNベースのモデルは速度が速いという特徴があります。今回は、性能重視でTransformerベースのモデルを使用します。

以下の英語、イタリア語、日本語文の類似度を計算してみます。ここで、各言語の同一インデックスの文は同じ意味の文になっています。

english_sentences = [
  'dog',
  'Puppies are nice.',
  'I enjoy taking long walks along the beach with my dog.'
]
italian_sentences = [
  'cane',
  'I cuccioli sono carini.',
  'Mi piace fare lunghe passeggiate lungo la spiaggia con il mio cane.'
]
japanese_sentences = [
  '犬',
  '子犬はいいです',
  '私は犬と一緒にビーチを散歩するのが好きです'
]

類似度を計算した結果は以下のようになりました。左から英語と日本語、イタリア語と日本語、英語とイタリア語のペアとなっています。結果を見ると、LaBSEと同様、行列の対角成分の値が他より大きくなっていることがわかります。つまり、言語が異なっていても、同じ意味の文は類似度が高くなるように埋め込めているということを示唆しています。

f:id:Hironsan:20200918091807p:plain
各言語間の文類似度

テキスト分類

文を埋め込んで類似度を計算できることは確認したので、次はテキスト分類をしてみましょう。今回もAmazon Customer Reviews Datasetを使ってモデルの学習と評価を行います。Amazonの商品レビューを入力し、1/0の2分類で予測するモデルを作成します。前回のLaBSEでは、レビュー冒頭の128トークンを使って予測するモデルを作成しました。m-USEの場合、可変長の入力に対応しているので、理屈的には何トークンでも入力できるのですが、あまり長くても計算時間がかかるだけなので、今回は1024文字目までのトークンだけを用いています。

モデルのアーキテクチャは以下のようになっています。LaBSEの場合と同様、m-USEの上にドロップアウトと全結合層を足しただけです。

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_4 (InputLayer)         [(None,)]                 0         
_________________________________________________________________
keras_layer_4 (KerasLayer)   (None, 512)               85213184  
_________________________________________________________________
dropout_3 (Dropout)          (None, 512)               0         
_________________________________________________________________
dense_3 (Dense)              (None, 2)                 1026      
=================================================================
Total params: 85,214,210
Trainable params: 85,214,210
Non-trainable params: 0
_________________________________________________________________

学習データと評価データの数は以下の通りです。冒頭で述べたように、学習データには英語、テストデータには日本語とフランス語を使って評価します。

en fr ja
train 32000
test 8000 8000 8000

実験結果(acc)は以下のようになりました。性能の傾向はLaBSEと同様に英語>フランス語>日本語の順でしたが、LaBSEと比べると3〜4%程度、性能が低くなるという結果になりました。この結果がすべてのデータセットに当てはまるとは思いませんが、今回の設定ではLaBSEの方がm-USEよりも良い結果となりました。

en fr ja
LaBSE 0.9300 0.8944 0.8473
m-USE 0.9056 0.8589 0.8055

参考資料