2年ぐらい前に必要があって生TensorFlowとTensorFlow-Hubによる様々なモデルやフレームワーク並びに事前学習済みモデルの実装を試していたのですが、TF2の浸透に伴いそれらの多くの仕様が変更になっており、中には回らなくなっていたコードもあったので、それらを調べるついでに最近のTF-Hubのアップデートも覗いてきました。ということで、自分向けの備忘録として簡単にまとめておきます。
TensorFlow-Hubの事前学習モデル
まず試したのがUniversal Sentence Encoderの多言語版。リンク先を見れば分かるように、16言語(アラビア語・簡体字中国語・繁体字中国語・英語・フランス語・ドイツ語・イタリア語・日本語・韓国語・オランダ語・ポーランド語・ポルトガル語・スペイン語・タイ語・トルコ語・ロシア語)に対応しています。色々な使い方ができますが、一番簡単なのは異言語間でのセンテンスの「意味」の類似度を出すというものです。デモのコードをなぞって実行した結果を以下に示します。
import tensorflow_hub as hub import numpy as np import tensorflow_text # Some texts of different lengths. english_sentences = ["You should send a dataset and a data scientific task to be solved in advance to a candidate. The candidate should solve it by the interview.", "At the interview, the candidate should show a process of building their solution and the performance of it for the dataset."] japanese_sentences = ["候補者に事前に解くべきデータセットとデータサイエンス的な課題を送り、面接当日までに1–2週間かけてそれを解いてもらう", "実際の面接の際は、候補者に自身のソリューションを構築した過程と、そのソリューションのデータセットに対するパフォーマンス(予測性能など)や得られた知見などを示してもらう"] embed = hub.load("https://tfhub.dev/google/universal-sentence-encoder-multilingual/3") # Compute embeddings. en_result = embed(english_sentences) ja_result = embed(japanese_sentences) # Compute similarity matrix. Higher score indicates greater similarity. similarity_matrix_ja = np.inner(en_result, ja_result) print(similarity_matrix_ja)
>>> [[0.6826382 0.4218406] [0.5020407 0.7091768]]
これは上記の英語版・日本語版双方で僕自身が書いたQuoraアンサーから、対訳となる2つのセンテンスを抜き出してきたものです。類似度はそれぞれ0.68, 0.71ぐらいと、まぁまぁというところですかね……というかUniversal Sentence Encoderの判定厳しいです(笑)。
もう一つの事前学習モデルは日本語Wikipediaコーパスを学習したTransformer-XLモデルで、シードとなるセンテンスを元にして新たなセンテンスを生成するデモが例示されています。
import numpy as np import tensorflow.compat.v1 as tf import tensorflow_hub as hub import tensorflow_text as tf_text tf.disable_eager_execution() n_layer = 12 d_model = 768 max_gen_len = 128 def generate(module, inputs, mems): inputs = tf.dtypes.cast(inputs, tf.int64) generation_input_dict = dict(input_tokens=inputs) mems_dict = {} for i in range(n_layer): mems_dict["mem_{}".format(i)] = mems[i] generation_input_dict.update(mems_dict) generation_outputs = module(generation_input_dict, signature="prediction", as_dict=True) probs = generation_outputs["probs"] new_mems = [] for i in range(n_layer): new_mems.append(generation_outputs["new_mem_{}".format(i)]) return probs, new_mems g = tf.Graph() with g.as_default(): module = hub.Module("https://tfhub.dev/google/wiki40b-lm-ja/1") text = ["候補者に事前に解くべきデータセットとデータサイエンス的な課題を送り、面接当日までに1–2週間かけてそれを解いてもらう"] # Word embeddings. embeddings = module(dict(text=text), signature="word_embeddings", as_dict=True) embeddings = embeddings["word_embeddings"] # Activations at each layer. activations = module(dict(text=text),signature="activations", as_dict=True) activations = activations["activations"] # Negative log likelihood of the text, and perplexity. neg_log_likelihood = module(dict(text=text), signature="neg_log_likelihood", as_dict=True) neg_log_likelihood = neg_log_likelihood["neg_log_likelihood"] ppl = tf.exp(tf.reduce_mean(neg_log_likelihood, axis=1)) # Tokenization and detokenization with the sentencepiece model. token_ids = module(dict(text=text), signature="tokenization", as_dict=True) token_ids = token_ids["token_ids"] detoken_text = module(dict(token_ids=token_ids), signature="detokenization", as_dict=True) detoken_text = detoken_text["text"] # Generation mems_np = [np.zeros([1, 0, d_model], dtype=np.float32) for _ in range(n_layer)] inputs_np = token_ids sampled_ids = [] for step in range(max_gen_len): probs, mems_np = generate(module, inputs_np, mems_np) sampled_id = tf.random.categorical(tf.math.log(probs[0]), num_samples=1, dtype=tf.int32) sampled_id = tf.squeeze(sampled_id) sampled_ids.append(sampled_id) inputs_np = tf.reshape(sampled_id, [1, 1]) sampled_ids = tf.expand_dims(sampled_ids, axis=0) generated_text = module(dict(token_ids=sampled_ids), signature="detokenization", as_dict=True) generated_text = generated_text["text"] init_op = tf.group([tf.global_variables_initializer(), tf.tables_initializer()]) # Initialize session. with tf.Session(graph=g) as session: session.run(init_op) embeddings, neg_log_likelihood, ppl, activations, token_ids, detoken_text, generated_text = session.run([ embeddings, neg_log_likelihood, ppl, activations, token_ids, detoken_text, generated_text]) for e in generated_text: print(e.decode('utf-8'))
同じQuoraアンサーの一文をシードにして生成させた結果がこちらです。まともな文章っぽく見えてちょっと変ですね(笑)。実は全く同じことを昔の職場で同僚だった「教授」氏が「バベルの図書館」と称して全く同じことを村上春樹の作品を学習データにしたRNNをベースにしてやっていたんですが、それよりはもうちょっと綺麗な文章っぽくなっている気がします。
>>> ことができるように」といったようにようにようにして決定すれば即座に即座に勝利を与えるようにとてきていながらばかりなので 実際の計算論のように多くの経路のようにして判断しても簡単に決めることは難しいようになっている デミル・デミルのように「自由あるいは権力でしかその予め雛られることが遅れていないようにして下さい... これらの方法で強制的に決定することはできない」だけでなく 「事前にその手順をしている段階でその予め待つ必要がある情報を」あるいは「この段階でその予予め用意されていなければできない情報を(少なくとも次のときに)」にしか定めていなければ決して収入のとれるような善策ばかりやっても良いのではなく それら予め予測
ちなみにシードを「俺はジャイアン、ガキ大将」というセンテンスにして生成させた結果がこちらです。
>>> 、俺は俺だっだ、俺や俺は誰だっ俺だっ俺俺が指だっ俺俺だっ俺ホイだってっていう奴を引きだって俺だって俺だもんって俺だってビルだって俺だ」といったような表現ってっていう奴はいやだっていういう奴がいるからバッじゃないんだってそういう奴気分って言葉ってっていう奴もいるっていう。(traditional) as well as only as anytime, but it had not held a most poetry
意味が分かりません(笑)。
Estimatorクラス
以前書いた、Estimatorクラスを使った日本語NNLMによるテキスト分類のコードですが、TF1の多くの関数がdeprecateしていてtf.compat.v1に移されているので、以下のように書き換える必要があることに気づきました。
import tensorflow.compat.v1 as tf # 特に1, 2が混在していなければインポートの時点でこうしておいた方が早い import tensorflow_hub as hub import numpy as np import pandas as pd from sklearn.utils import shuffle if __name__ == "__main__": df_train = pd.read_csv("kk_cut_traindev_w_token_header.csv", encoding="utf-8", sep=',') df_train = shuffle(df_train) train_input_fn = tf.estimator.inputs.pandas_input_fn( df_train, df_train["label"], num_epochs=None, shuffle=True) df_test = pd.read_csv("kk_cut_test_w_token_header.csv", encoding="utf-8", sep=',') df_test = shuffle(df_test) predict_test_input_fn = tf.estimator.inputs.pandas_input_fn( df_test, df_test["label"], shuffle=False) embedded_text_feature_column = hub.text_embedding_column( key="text", module_spec="https://tfhub.dev/google/nnlm-ja-dim128/1") estimator = tf.estimator.DNNClassifier( hidden_units=[512, 128], feature_columns=[embedded_text_feature_column], n_classes=14, optimizer=tf.train.AdamOptimizer(learning_rate=0.003)) estimator.train(input_fn=train_input_fn, steps=1000); test_eval_result = estimator.evaluate(input_fn=predict_test_input_fn) print("Test set accuracy: {accuracy}".format(**test_eval_result))
>>> Test set accuracy: 0.7618844509124756
import seaborn as sns import matplotlib.pyplot as plt def get_predictions(estimator, input_fn): return [x["class_ids"][0] for x in estimator.predict(input_fn=input_fn)] LABELS = range(14) # Create a confusion matrix on training data. with tf.Graph().as_default(): cm = tf.math.confusion_matrix(df_test["label"], get_predictions(estimator, predict_test_input_fn)) with tf.Session() as session: cm_out = session.run(cm) # Normalize the confusion matrix so that each row sums to 1. cm_out = cm_out.astype(float) / cm_out.sum(axis=1)[:, np.newaxis] plt.figure(figsize=(16, 12)) sns.heatmap(cm_out, annot=True, xticklabels=LABELS, yticklabels=LABELS); plt.xlabel("Predicted"); plt.ylabel("True");
ちなみに上記のコードは最初にimport Estimatorクラスはザッと適当なfine tuning系モデルを組むには便利なんですが、ちゃんとTF2ベースで書けるように勉強しておこうと思いました。。。
余談
TF-Hubの事前学習モデルは一度ダウンロードすると適当にOSが見繕ったディレクトリに一時保存されるのですが、OS側の都合でモデルファイルがいつの間にか削除されていたりするようで、しばらく時間が経ってからコード全体を再実行すると"OSError: SavedModel file does not exist at XXX"みたいなエラーが返ってくることがあります。
その場合はエラーメッセージに表示されている中で一番階層の深いディレクトリをOS上で削除した上で、再度実行すれば事前学習モデルが再ダウンロードされて動くようになります。ご注意ください。
また、モデルによっては(今回はwiki40b-lm-jaが該当した)実行すると延々と"INFO:tensorflow:Saver not created because there are no variables in the graph to restore"という警告メッセージが出続けることがあり、ひどいと何千行ぐらいになることもあるようですが、これ自体はエラーではないので無視して大丈夫です(放っておいても最後まで回り切る)。ただし今回のように事前学習済みモデルを使う場合は問題ありませんが、自前で用意するなどした再学習可能なモデルを使う場合は対処が必要になることがあるので、GitHubのissuesやStackOverFlowを参照して対処することをお勧めします。