滝沢カレンを作る技術

滝沢カレン(以下、敬称略)をご存知でしょうか。ファッション雑誌『JJ』専属モデルであり、モデルやタレントとして活躍されています。*1

そんな彼女の書く文章は非常に豊かな表現で構成されており、一部では純文学とも評されています。 近年、彼女の文章を対象とした言語処理的アプローチが盛んであり*2、例えば構文解析を行った例などがあります。

今回は、滝沢カレン言語モデル (Takizawa Karen Language Model; TKLM) を作成し、その言語モデルを用いて自動で滝沢カレンっぽい文章を生成するタスクに挑戦します。実装はTensorFlow(における高レベルAPIに該当するKeras)で行います。

データセット

Instagramの滝沢カレンの投稿の中で、7月7日(日)22時までに投稿されていた全582件のテキストだけを手動で収集しました。これは利用規約でスクレイピングなど自動で情報を取得する行為が禁止されているためです。

滝沢カレン言語モデル

文字ベース言語モデル

まずは最も単純な、文字ベースの言語モデルの実装です。Kerasのexampleにも存在します。

keras/lstm_text_generation.py at master · keras-team/keras · GitHub

文字ベースの言語モデルでは、系列長 seq_len 分の文字を入力して、その次に出現する文字を予測するモデルを学習します。入力  x とターゲット  t の関係を図示すると以下のようになります。

f:id:pompom168:20190714151923p:plain

モデルのアーキテクチャはKerasのexampleを踏襲し、単一のLSTMレイヤのモデルとします。こちらも図示すると以下のような構成になります。モデルの出力 y は学習データセットにおける語彙数次元のベクトルになり、Softmax関数に通すことで各文字の出現確率となります。

f:id:pompom168:20190714154611p:plain

コードで表すとこんな感じです。

model = tf.keras.models.Sequential()
model.add(tf.keras.layers.LSTM(hidden_size, input_shape=(seq_len, vocab_size)))
model.add(tf.keras.layers.Dense(vocab_size, activation='softmax'))

それでは実験してみましょう。

今回のデータセットにおいては、語彙数(ユニークな文字数)は2457となりました。seq_len は8とし、サンプルサイズ346890となりました。LSTMのユニット数は128としました。損失関数はターゲットとのクロスエントロピーとし、KerasデフォルトのRMSpropで最適化を行いました。またバッチサイズを128として、100epochまで学習しました。今回は検証用データでのearly stoppingなどは行っていません。

学習終了後、シードとなる文字系列を与えてそこから予測された確率が最も高い文字を抽出する作業を300回繰り返し、文章を生成させます。滝沢カレンのインスタグラムで良くあるパターンの「みなさん、こんにちは」から文章を始めたいので、シードは「みなさん、こんに」とします。

出来上がった文章の例を見てみましょう!

みなさん、こんにみなさん、こんにちは😊💕 本日は、な場をシャルにまであのはず自分と言わせてください🙇🏼💋 そして、見ないかもしればせたのうちに、うってがとはついてきただけだから#コメントをまつの服にでしょうか😅クリスマッリタしで顔を見られるようでしょう所✨😍 本日はなんと明日がないけれでした😢 そして、はゃてスタジオは歩くえ。その、まさかんだん、きて居間に来てくれれるかというの笑いなっちゃわざうな明日の そんなことで、なんならからつまりはどうでサイト」に出場します🌙 明日1時間ス#すりと言ってもを会いましょう💕💗💗💗💦💞💞 今夜でみなさま、当たり前j目のひと数がか、きっと一緒に目ではいいでしょうか🙋🏼✨ そんな、知っ

。。。意味が分からない文章なだけで、こんなものは全く滝沢カレンではありません。

早々に文字ベース言語モデルは諦めて、単語ベース言語モデルに進みます。

単語ベース言語モデル

文字ベース言語モデルでは、系列長 seq_len 分の文字を入力してその次に出現する文字を予測する問題でしたが、単語ベース言語モデルでは、系列長 seq_len 分の単語を入力してその次に出現する単語を予測する問題となります。

先ほどと同様に、入力  x とターゲット  t の関係を図示すると以下のようになります。基本的には文字が分かち書き後の単語に変わるだけです。

f:id:pompom168:20190714165344p:plain

モデルのアーキテクチャには少し変更を加えます。LSTMの前にEmbeddingレイヤを置き単語の分散表現をLSTMに入力するようにします。こちらも図示すると以下のような構成になります。

f:id:pompom168:20190714165358p:plain

こちらもコードで表すとこんな感じです。

model = tf.keras.models.Sequential()
model.add(
    tf.keras.layers.Embedding(
        input_dim=vocab_size, output_dim=embedding_size, mask_zero=True))
model.add(tf.keras.layers.LSTM(hidden_size))
model.add(tf.keras.layers.Dense(vocab_size, activation='softmax'))

それでは実験してみましょう。

分かち書きはMeCab + mecab-ipadic-NEologdで行いました。

語彙数は11448になりました。一般的には出現頻度が n 以下の単語は未知語として扱うなどの処理を行いますが今回は行いませんでした。出現頻度が1回だけの単語でも、”う〜んこれは滝沢カレンっぽいな〜”という単語が多かったので、その情報を失わないためです。ただし、数字は全て N に変換しています。seq_lenは4とし、サンプルサイズは191512となりました。embeddingの次元数は50としました。それ以外のハイパーパラメータなどは文字ベースと同じです。

こちらも学習終了後、シードとなる単語系列を与えてそこから予測された確率が最も高い単語を抽出する作業を300回繰り返し、文章を生成させます(ただし終端記号<eos>が出現したらそこで終了します)。シードは「みなさん、こんにちは❣」とします。

出来上がった文章の例を見てみましょう!

みなさん、こんにちは❣️毎日本当に来てますので、グイグイしないで待っていてくれたので、いいときなんかの意識も入っている、嬉しいですまだ何にもこんだろうにしました🌻🎉汗なりお腹にも出場できるので、今日からのご登場です👏👏👏👏はい、みなさまがおっしゃるとおり、石ころもどきの私と人間の気持ちもあり👱🏻‍♀️🌹を緊張な#私は幸せをくれる#でもそんなより素敵な曜日もしているんでしょう😌🌟#カレンのタンス#秋への準備次第

これは割ともう滝沢カレンじゃないでしょうか。特に 毎日本当に来てますので の所なんて滝沢カレンな気がします。また文末においてハッシュタグで文章を続けている所は完全に滝沢カレンです。

しかし、更なる滝沢カレン言語モデルを獲得するために、改良を続けていきます。

多層化

モデルの表現能力向上のため、LSTMレイヤを重ねることにします。また、多層にすることでパラメータ数が増えるので、過学習を防ぐためDropoutも導入します。

コードで表すとこんな感じです。Denseレイヤに接続するLSTMレイヤ以外は、上のLSTMレイヤに途中の時刻の出力も渡さなければならないので、 return_sequences=True とします。

model = tf.keras.models.Sequential()
model.add(
    tf.keras.layers.Embedding(
        input_dim=vocab_size, output_dim=embedding_size, mask_zero=True))
model.add(tf.keras.layers.Dropout(dropout_rate))
model.add(
    tf.keras.layers.LSTM(
        hidden_size, return_sequences=True, dropout=dropout_rate))
model.add(tf.keras.layers.LSTM(hidden_size, dropout=dropout_rate))
model.add(tf.keras.layers.Dense(vocab_size, activation='softmax'))

それでは実験してみましょう。 dropout率を0.5に設定した以外は、先ほどの条件と全く同じです。

出来上がった文章の例を見てみましょう!

みなさん、こんにちは❣️✨毎日jjします😖💋💡みなさん観てくださいね✋🏻❣️令和のさんまさんが突っ込むちゃんの海外ウェディングさに深さしました🤗😅いい桜好きでのお留守番しますが、ここまで汗に遊すぎです🙇🏼‍♀️✋🏻💎頭の上着をたくさん広げていたでしょう🌙🌟それもはいつも過ぎた炊飯器な中に私もやっぱり言い、人間や本意って)が寝るのですか❓そうにもての期待をお楽しみください。☺️.5名全てに、上手いの仲良しになっていますが今夜もこんにちは👋🏻👁人は、未来でではいても感じない人だっていいですが、それはちゃんとが着ませんな余ら並みには入りそうですが、万が一テレビを見るよりもちろん覚えたいから怖い#滝沢カレンよかってと優しく端っこを左右して頂き恥ずかしよ#明日遠足に何日言いようの頑丈でみなさんインスタを見せたい背は、ほとんど火曜日よりなにで映りますが、その岩にの続けるのサポートな隙間を私と取りでしまう天気を心配としてしまっましたから

う〜ん、何とか文章にはなってはいるけど、滝沢カレンっぽさというよりはただただ文法規則に則っていない文章になっている気がします。これ以外の文章を見ても主観ですが、ただただ単語を並べただけの文章になってしまっていました。今回のデータセットのボリューム的には多層化は必要ないのかもしれません。

Weight tying

Embeddingレイヤはvocab_size次元からembedding_size次元へのマッピング、Denseレイヤはhidden_size次元からvocab_size次元へのマッピングであるので、embedding_size = hidden_sizeであればEmbeddingレイヤの重みを転置することでDenseレイヤの重みとして使い回せます。この重みの共有で予測精度が向上することが報告されています。

arxiv.org

これを、単一のLSTMレイヤの場合に適用してみます。コードで表すとこんな感じです。

model = tf.keras.models.Sequential()
model.add(
    tf.keras.layers.Embedding(
        input_dim=vocab_size,
        output_dim=embedding_size,
        mask_zero=True,
        name='embedding'))
model.add(tf.keras.layers.LSTM(hidden_size))
model.add(
    tf.keras.layers.Lambda(lambda x: tf.keras.backend.dot(
        x, tf.keras.backend.transpose(model.get_layer('embedding').embeddings))
                           ))
model.add(tf.keras.layers.Activation('softmax'))

それでは実験してみましょう。embedding_sizeとhidden_sizeの両方を128にした以外は、これまでと条件は同じです。

出来上がった文章の例を見てみましょう!

みなさん、こんにちは❣️本日、「カレンのタンス」ということで観ていたとある国を着てしまったので、改築がんばります😅いにくい方や最近立ち見で座布団すら手にできない方、見事場所は何も見つけないんだ#自分が揺らぎたきゃそれでいい#道筋の支えはしていたかなんて分かりませんがいい感情にはない#しっかり一歩踏み出したくないんだ#人間が動いてるんだから

これはもはや滝沢カレンではないでしょうか。表面的には意味がわかりませんが、何となく応援されている気分になるところが個人的には滝沢カレンです。

ここまでで、一旦定量的な評価もしておきたいと思います。学習データとして収集してから新規に4つの投稿があったので、それをテストデータとしてPerplexityを計算したいと思います。

テストデータの語彙数は11448、サンプルサイズは2117となりました。評価した結果は以下です。

そもそもあり得ないほどPerplexityが大きいですが、そもそもこの評価の方法が妥当かどうかもありますので、一旦絶対的な値には目をつむりモデル間の相対的な値を見てみます。Perplexityが小さい順に、single-LSTM with tying、single-LSTM、Multilayer-LSTMとなっており、weight tyingを用いたものが最良となっていました。これは生成された文章の質を見た定性的な評価とも一致しています。

モデル Perplexity
single-LSTM 1192374
Multilayer-LSTM 1939050
single-LSTM with tying 1092815

バッチ型からシーケンシャル型へ

これまでは、一定の系列長の単語を与えて次に出現する単語を予測する問題を考えていました。しかし、ある単語を与えたときに次の単語を予測する問題も考えることができます。以下のページの説明に従うと、前者はバッチ型で後者はシーケンシャル型と呼ばれます。

medium.com

また上記ページの実験によると、バッチ型の性能はシーケンシャル型に基本的には劣るそうです。よって、シーケンシャル型のモデルを試してみます。

シーケンシャル型では単語が与えられると逐次予測を行うので、データの与え方は以下の図のようになります。

f:id:pompom168:20190714225132p:plain

モデルのアーキテクチャは以下のようになります。全ての時刻においてLSTMは次の時刻へ出力を渡すのと共に、自身の時刻の上位レイヤにも出力を渡し予測を行います。

f:id:pompom168:20190714225143p:plain

コードで表すとこんな感じです。LSTMレイヤが return_sequences=True となっていることが注目点です。これにより、最終時刻以外もLSTMレイヤが上位層に出力を渡します。また、weight tyingを使用せずDenseレイヤを続ける場合は、TimeDistributedレイヤでラップすることで時刻ごとに単語の予測確率を出力します。

model = tf.keras.models.Sequential()
model.add(
    tf.keras.layers.Embedding(
        input_dim=vocab_size,
        output_dim=embedding_size,
        mask_zero=True,
        name='embedding'))
model.add(tf.keras.layers.LSTM(hidden_size, return_sequences=True))
model.add(
    tf.keras.layers.Lambda(lambda x: tf.keras.backend.dot(
        x, tf.keras.backend.transpose(model.get_layer('embedding').embeddings)))
)
# weight tyingを使用しない場合
# model.add(
#     tf.keras.layers.TimeDistributed(
#         tf.keras.layers.Dense(vocab_size, activation="softmax")))
model.add(tf.keras.layers.Activation('softmax'))

それでは実験してみましょう。実験の条件はweight tyingの場合と同様です。

出来上がった文章の例を見てみましょう!

みなさん、こんにちは❣️みなさまに是非お昼休みも近いことですから、あふれんばかりの一回根強さを身にまとう、長澤まさみさんの御殿にいるのではなく、素なわたしが出場してます😋👍🏻❣️これは、ラグビーの選手の方々が来てください😌💞💞#前略大徳さん#カレンのタンス#私服#irene

長澤まさみさんの御殿にいるのではなく、素なわたしが出場しているそうです。これも滝沢カレンっぽいといえばっぽいです。また、傾向としては終端記号<eos>がかなりの頻度で出力されるようになり、文章が割と短くなっています。以下の例などです。(数字を表すNが含まれるN:Nが出現すると9:00に変換しています。)

みなさん、こんにちは❣️本日もお願いします😖💕🙋🏼そんな素晴らしい生き物にサンキュー‼︎

みなさん、こんにちは❣️本日9:00〜より、日本テレビさんにて、「火曜サプライズ」と出場します🌟🙌🏻#カレンの髪技

確かに文章としては、正しい文章が生成されるようになりました。しかし滝沢カレンっぽさが欠如してしまった気がしますし、文章の多様性が失われた気がします。

またテストデータに対するPerplexityも 1210703 と、単一LSTMのweight tyingの場合の方が良い結果となりました。よってこれまでの結論としては、シーケンシャル型よりバッチ型のweight tyingが最良の滝沢カレン言語モデルであるということにします。

まとめ

滝沢カレン言語モデルを作成して、自動で滝沢カレンっぽい文章を生成するタスクに挑戦しました。

ほぼ主観的な評価では、単一のLSTMレイヤかつweight tyingを用いたモデルが最良のモデルとなりました。

例えば上に記載した例以外にも以下のような文章を生成できます。

みなさん、こんにちは❣️お仕事の日は扱いやすいお時間を通してするようで歩くのではなく質問風貌の大きさを取ってくれた、海老つけ麺食べたかったですが、歩く頃にとっちゃ関係ないよね

今後の課題

  • 滝沢カレン言語モデルの評価方法
  • 生成結果を何らかの形で利用できるwebページ作成
  • これまでの方法を踏襲するなら、正解データに対しても単語の確率分布を考え予測された分布と正解の分布との距離を損失に加えた方法を試す

arxiv.org

  • その他言語モデルの改良テクニックを試してみたい(以下の論文とか?これ見とけみたいな論文あったら教えて欲しい)

arxiv.org

  • GANベースの文章生成方法の検証

補足

実験に使用したコードは以下のリポジトリにあります。

しかし、実験段階で未整理かつ非常に雑なので取扱い注意です。

github.com