はじめに
頑張れば、何かがあるって、信じてる。nikkieです。
2019年12月末から自然言語処理のネタで毎週1本ブログを書いています。
先週、総括としてライブドアニュース(日本語テキスト)の分類に取り組んだところ、tf.py_function
が何をやっているのか分からないという課題が見えました。
そこで、今回はドキュメントを参考にtf.py_function
の理解を深めました。
先週から参照している記事「tf.data.Dataset apiでテキスト (自然言語処理) の前処理をする方法をまとめる - Qiita 」のコードの書き換えにも挑戦しています。
先週の週1ブログ駆動開発!
目次
- はじめに
- 目次
- 動作環境
- tf.py_functionとは何かの前に:tf.data.Dataset.mapとは何か
- 本題:tf.py_functionとは何か
- 自然言語処理でtf.py_functionを使ったコードの書き換えに挑む
- 感想
動作環境
先週と同じ環境です。
$ sw_vers ProductName: Mac OS X ProductVersion: 10.14.6 BuildVersion: 18G103 $ # 注:コードを動かすのにGPUは使っていません $ python -V Python 3.7.3 $ pip list # grepを使って抜粋して表示 tensorflow 2.1.0 tensorflow-datasets 2.0.0 matplotlib 3.1.2 numpy 1.18.1 Janome 0.3.10
tf.py_function
とは何かの前に:tf.data.Dataset.map
とは何か
ドキュメントはこちら。
必須引数map_func
に焦点を当てます1。
引数map_func
は関数(オブジェクト)を受け取り、
Maps
map_func
across the elements of this dataset.
(データセットの要素全体にmap_func
を適用する)
map
メソッドがやることは以下の2つです:
- returns a new dataset((
map_func
が適用された) 新しいtf.data.Dataset
(以下では「データセット」と表記)オブジェクトを返す) - same order as they appeared in the input(要素は入力に現れたのと同じ順番)
map_func
の入力
The input signature of
map_func
is determined by the structure of each element in this dataset.
(map_func
の入力シグネチャは現データセットオブジェクトの各要素の構造によって決定される)
シグネチャ(signature)は馴染みがない言葉なのですが、ここでは「パラメーターとその型」と理解しました。2
ドキュメントにはmap_func
の入力を決める例が3つ続きます:
tf.Tensor
1つから構成されたデータセットオブジェクト3- 2つの
tf.Tensor
からなるタプル(tf.Tensor, tf.Tensor)
から構成されたデータセットオブジェクト - 辞書から構成されたデータセットオブジェクト
1の例(dataset.map(lambda x: x + 1)
)ではx
がtf.Tensor
1つに対応します。
In [2]: dataset = tf.data.Dataset.range(1, 6) In [7]: list(dataset.as_numpy_iterator()) Out[7]: [1, 2, 3, 4, 5] In [8]: dataset2 = dataset.map(lambda x: x + 1) In [9]: list(dataset2.as_numpy_iterator()) Out[9]: [2, 3, 4, 5, 6]
今回知ったデータセットオブジェクトの便利なメソッド
(1) 上の例でもlist()
と合わせて使いましたが、as_numpy_iterator
メソッドが便利です。
for in
でデータセットオブジェクトを扱わなくても値を見ることができます。
(2)from_generator
メソッドでデータセットオブジェクトを作る方法を学びました。
In [22]: elements = [(1, "foo"), (2, "bar"), (3, "baz")] In [23]: dataset = tf.data.Dataset.from_generator(lambda: elements, (tf.int32, tf.string))
from_generator
メソッドの
generator
引数にlambda(引数なし)を渡し、output_types
引数で型を定義
しています。
今回使った引数なしのlambda(無名関数)の確認
In [32]: f = lambda: elements In [33]: f() Out[33]: [(1, 'foo'), (2, 'bar'), (3, 'baz')] # elememntsが返る
なお、「dataset2 = tf.data.Dataset.from_tensor_slices(elements)
でもいいのでは」と試したところ、
ValueError: Can't convert Python sequence with mixed types to Tensor.
(型が混在したシーケンスはtf.Tensor
に変換できない)とのことです。
だからfrom_generator
メソッドがあるのですね。納得!
map_func
の出力
入力を見たので続いて出力を見ます。
The value or values returned by
map_func
determine the structure of each element in the returned dataset.
(map_func
の返り値が、map
で返されるデータセットオブジェクトの各要素の型を決める)
ドキュメントには、タプルを返すいくつかの例が紹介されていました。
- 関数
g
の例:要素が全て定数で変換されたdatasetが返される - 関数
h
の例:リストとnumpy arrayはともにtf.Tensor
に変換される - 関数
i
はネストされたタプルの例
g
の例です。
In [36]: def g(x): ...: return tf.constant(37.0), tf.constant(["Foo", "Bar", "Baz"]) ...: In [37]: g(1) # 実引数の値によらず返り値は同じ Out[37]: (<tf.Tensor: shape=(), dtype=float32, numpy=37.0>, <tf.Tensor: shape=(3,), dtype=string, numpy=array([b'Foo', b'Bar', b'Baz'], dtype=object)>) In [38]: type(g(1)) Out[38]: tuple In [39]: result = dataset.map(g) In [40]: for d in result: # 値だけの確認であれば list(result.as_numpy_iterator()) が使える ...: print(d) ...: (<tf.Tensor: shape=(), dtype=float32, numpy=37.0>, <tf.Tensor: shape=(3,), dtype=string, numpy=array([b'Foo', b'Bar', b'Baz'], dtype=object)>) (<tf.Tensor: shape=(), dtype=float32, numpy=37.0>, <tf.Tensor: shape=(3,), dtype=string, numpy=array([b'Foo', b'Bar', b'Baz'], dtype=object)>) (<tf.Tensor: shape=(), dtype=float32, numpy=37.0>, <tf.Tensor: shape=(3,), dtype=string, numpy=array([b'Foo', b'Bar', b'Baz'], dtype=object)>) In [41]: result.element_spec Out[41]: (TensorSpec(shape=(), dtype=tf.float32, name=None), TensorSpec(shape=(3,), dtype=tf.string, name=None))
tf.Tensor
2つからなるタプルがデータセットオブジェクトの1つ1つとなっていることが確認できます(40のセル参照)。
セル41で使っているelement_spec
属性は便利そうです。
本題:tf.py_function
とは何か
map
のドキュメントを読んでいくと、map_func
にPythonのコードを渡す方法について言及されています4
To use Python code inside of the function you have two options
- Rely on AutoGraph to convert Python code into an equivalent graph computation(AutoGraphを用いてPythonコードを等価なグラフ計算に変換する)
- AutoGraphですべてのPythonコードを変換できるとは限らない
- Use
tf.py_function
- 任意のPythonコードを書ける
- 1に比べて一般にパフォーマンスは劣る
map
のドキュメントからリンクされているtf.py_function
を見ていきます。
引数について
func
:Pythonの関数inp
:tf.Tensor
のリストfunc
はinp
と型が対応するtf.Tensor
のリストを受け付ける
Tout
:TensorFlowのデータ型のリストorタプル。func
の返り値を示す(Noneが返るときは空のリストとする)func
はTout
の値に対応する型のtf.Tensor
のリストを返す
返り値は
A list of
Tensor
or a singleTensor
whichfunc
computes
(func
が計算するtf.Tensor
のリストまたはtf.Tensor
単体)
tf.py_function
の使い方を試す
map
のドキュメントにあるupper_case_fn
の例で確認します(書き換えたいQiita記事との読み替えが減るように、関数名の先頭をアンダースコアで始めています)。
In [75]: d = tf.data.Dataset.from_tensor_slices(['hello', 'world']) In [76]: def _upper_case_fn(t): ...: return t.numpy().decode().upper() ...: In [77]: d = d.map(lambda x: tf.py_function(func=_upper_case_fn, inp=[x], Tout=tf.string)) In [78]: list(d.as_numpy_iterator()) Out[78]: [b'HELLO', b'WORLD']
map
のmap_func
に渡す関数オブジェクトをlambda以外で作ってみます。
lambdaを使っていないだけで、関数の処理内容は変わりません(tf.py_function
の適用結果を返す)。
In [79]: def upper_case_func(x): ...: return tf.py_function(func=_upper_case_fn, inp=[x], Tout=tf.string) ...: In [80]: d = tf.data.Dataset.from_tensor_slices(['hello', 'world']) In [81]: list(d.map(upper_case_func).as_numpy_iterator()) Out[81]: [b'HELLO', b'WORLD']
というわけで、map
メソッドのmap_func
引数には
tf.py_function
を返すlambda- または、
tf.py_function
を返す関数
のどちらかを渡せばよさそうです。
自然言語処理でtf.py_function
を使ったコードの書き換えに挑む
tf.data.Dataset apiでテキスト (自然言語処理) の前処理をする方法をまとめる - Qiita 中のコードを書き換えてみます。
例えば、janome
を使って原形を取り出す部分は、関数内関数を多用しているために書き手以外にわかりにくいコードという印象です。
もっとわかりやすく書けないか、自分の理解を深めるために書き直します。
書き換え前のコード
def janome_tokenizer(): token_filters = [ CompoundNounFilter(), POSStopFilter(["記号", "助詞", "助動詞", "名詞,非自立", "名詞,代名詞"]), LowerCaseFilter(), ExtractAttributeFilter("surface"), ] tokenizer = Tokenizer() analyzer = Analyzer(tokenizer=tokenizer, token_filters=token_filters) analyze_function = analyzer.analyze def _tokenizer(text_tensor, label): text_str = text_tensor.numpy().decode("utf-8") tokenized_text = " ".join(list(analyze_function(text_str))) return tokenized_text, label return _tokenizer def tokenize_map_fn(tokenizer): def _tokenize_map_fn(text_tensor, label): return tf.py_function( tokenizer, inp=[text_tensor, label], Tout=(tf.string, tf.int64) ) return _tokenize_map_fn datasets = datasets.map(tokenize_map_fn(janome_tokenizer()))
tokenize_map_fn(janome_tokenizer())
(map
の実引数)で_tokenize_map_fn
関数が返るmap
で適用する_tokenize_map_fn
の引数text_tensor, label
は、データセットの各要素と一致map
で渡す関数はデータセットの構成要素に対応する数の引数を受け取れる- 1つの関数でまとめようとすると、引数は
text_tensor, label
とする必要がある(tf.py_function
のinp
引数への指定で使う)
1つの関数にまとめてみた
def tokenize_map_fn(): token_filters = [ CompoundNounFilter(), POSStopFilter(["記号", "助詞", "助動詞", "名詞,非自立", "名詞,代名詞"]), LowerCaseFilter(), ExtractAttributeFilter("surface"), ] tokenizer = Tokenizer() analyzer = Analyzer(tokenizer=tokenizer, token_filters=token_filters) analyze_function = analyzer.analyze def _tokenizer(text_tensor, label): text_str = text_tensor.numpy().decode("utf-8") tokenized_text = " ".join(list(analyze_function(text_str))) return tokenized_text, label def _tokenize_map_fn(text_tensor, label): return tf.py_function( _tokenizer, inp=[text_tensor, label], Tout=(tf.string, tf.int64), ) return _tokenize_map_fn all_tokenized_data = all_labeled_data.map(tokenize_map_fn())
map
に渡るのはtokenize_map_fn()
、すなわち_tokenize_map_fn
という関数オブジェクト_tokenize_map_fn
はデータセットの要素を引数に受け取れる(def _tokenize_map_fn(text_tensor, label):
)_tokenize_map_fn
はtf.py_function
を適用したオブジェクトを返す
- 関数
tokenize_map_fn
はjanome
のAnalyzerを作った後、それを使った関数_tokenizer
を用意し、_tokenize_map_fn
関数の中に設定している
動作するがWarningが出る例
def tokenize_map_fn(text_tensor, label): token_filters = [ CompoundNounFilter(), POSStopFilter(["記号", "助詞", "助動詞", "名詞,非自立", "名詞,代名詞"]), LowerCaseFilter(), ExtractAttributeFilter("surface"), ] tokenizer = Tokenizer() analyzer = Analyzer(tokenizer=tokenizer, token_filters=token_filters) analyze_function = analyzer.analyze def _tokenizer(text_tensor, label): text_str = text_tensor.numpy().decode("utf-8") tokenized_text = " ".join(list(analyze_function(text_str))) return tokenized_text, label return tf.py_function( _tokenizer, inp=[text_tensor, label], Tout=(tf.string, tf.int64), ) all_tokenized_data = all_labeled_data.map(tokenize_map_fn)
map
に直接渡せるようにtokenize_map_fn
にtext_tensor, label
を引数で渡せるようにしました。
ですが、以下のようなWarningが大量に出ます。
WARNING:tensorflow:AutoGraph could not transform <bound method Tokenizer.tokenize of <janome.tokenizer.Tokenizer object at 0x13e673860>> and will run it as-is.
WARNING:tensorflow:Entity <bound method CompoundNounFilter.apply of <janome.tokenfilter.CompoundNounFilter object at 0x13e6732b0>> appears to be a generator function. It will not be converted by AutoGraph.
おそらくPythonのコードが全て、tf.py_function
で包まれていないのが原因ではないかと見ています。
感想
map
とtf.py_function
のドキュメントからだいぶ理解は深まりました。
ですが、実務レベルのコードでの適用方法は、書き換えでwarningに出会ったこともあり、まだ自信がありません(時間切れ感もあります)。
先週感じた課題の中からTensorFlowを今後使っていく上で影響しそうなtf.py_function
を選択しました。
まだまだ課題は残っていますし、新しい調査事項も出てきました。
Qiita記事のencodeの部分、私のコードではset_shape
が見られ、これを削除するとエラーになります。
これが何をするものなのか気になっています。
課題は山積していますが、いったんそれらは脇に置いて(時間が余った時にやるように優先度を下げて)、次週からは自然言語処理の基礎を固めるために、「禁書にすべき」という声もあるあの本に取り組みます。
-
今回は
tf.py_function
がよく分からない問題に取り組んでおり、tf.py_function
が関わってくるのがmap_func
のためです。↩ -
Signature (functions) (シグネチャ (関数)) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN↩
-
ドキュメントの上部「Source Datasets」の記載から、データセットオブジェクトを
for in
で取り出したときの1つ1つがtf.Tensor
と理解しています↩ -
ここまでの例で単純なlambdaは渡せることが確認できたので、グラフとして扱う(tf.data traces the function and executes it as a graph)中でPythonのコードを使う方法のようです↩