のどあめ

ググってもなかなか出てこなかった情報を垂れ流すブログ。

TransformerとSentencepieceでなんJスレタイ生成器を構築する(3回目)

なんJスレタイを生成するシリーズ3回目です。

おさらい

なんJスレタイ生成とは

ニューラルネットの文書生成を利用して、なんJのスレッドタイトルのような文書を生成します。

過去のシリーズ

前回(2回目)のおさらい

  • 文書生成を行う生成器Generatorと、その文書の評価器Evaluatorの2つのモデルを利用する。
  • Generatorは過去のなんJスレタイを利用して学習し、Evaluatorは真のスレタイかGeneratorが生成したスレタイかを分類するよう学習する。
  • Generatorでたくさんのスレタイを生成し、Evaluatorが高スコアを出したものだけを出力する

後で似たアイデアの論文*1を見つけましたので、本稿ではその表記に合わせて生成器をGenerator・評価器をFilterと呼びます。

3回目のなんJスレタイ生成

github.com

今回と前回(2回目)との差分

大きく3つの点を変更しています。

  1. 文書生成にTransformer(のEncoder)を利用する(前回:多層GRU)
  2. 文書のトークナイズにsentencepieceを利用する(前回:MeCab + NEologd)
  3. 評価器(Filter)にCNNを利用する(前回:多層Bidirectional GRU)

Transformerによる文書生成

前回は文書生成に多層GRUを利用していましたが、流行りのTransformerのほうが文書生成も強いのでは?と思って導入しました。

TransformerはもともとSeq2Seqのモデルなのでその前半のEncoder部分だけ利用します。 Masked Multi-Head Attentionを適用したEncoderを用いて、1文字前までのトークンシーケンスから次のトークンを予測します。

※詳しくはgithubのソースコードを参照してください

sentencepieceによるトークナイズ

前回はMeCab+NEologdを利用していましたが、Sentencepiece : ニューラル言語処理向けトークナイザ - Qiita によると機械翻訳においてはMeCab+NEologdよりsentencepieceを使うほうが良いそうです。

機械翻訳も文書生成タスクの一種であることを考えると、単純な文書生成においてもsentencepieceの方が良い気がしたので採用しました。

sentencepieceを使うことで2点良いことがありました。

  • 辞書を別でダウンロード必要がなく実験が早く進められた
  • 語彙サイズに合わせてトークンを決めてくれるので、最低限の正規化でも違和感の少ないスレタイが生成できた

特に後者が強力で、今回数字も特殊トークン化しなくても違和感のない文書を生成できました(後の生成例参照)。 昔は何でも形態素解析をしていた印象ですが、今の主流はsentencepieceなのでしょうか。

評価器(Filter)による選別

前回は多層Bidirectional GRUを利用していましたが、今回はより精度が良かったCNN3層による文書分類器で実装しました。 他にもいくつかモデルを試しましたが、イマイチ精度が良くなりませんでした。ここは改良の余地がありそうです。

実験

※ 前回同様、一部ブログに書くにはアレな文書も生成されていたので選抜しています。 選抜していないものを見たい方はこちら

※ 文書の開始・終了を表すトークンは表示していません、<kusa>は「w」が2つ以上連打されることを意味する特殊トークン、スレタイの後ろの数値はFilterの評価値です。

制限なしで生成したスレタイ

「はん」を「さ」に変えて予測変換が一致する唯一の球団 0.866355836391449
ワイの彼女「ふにゃふにゃふにゃふにゃゅ」ワイ「ふゅうごゅ?」 0.8488953113555908
コミケのオリジナルグッズガチでクオリティが高そうな件について<kusa> 0.814984917640686
コンビニでいつも悩んでいるオヤジにブチ切れて 0.8129239082336426
dmmでゲームやってるとやってるニキおる? 0.8119136095046997
ワイ「今日はリリースさてそろそろでしょ」上司「ちょっと待って!それ面白いやつくわ!」 0.8110376596450806
【悲報】古参オタクに意見を「お前のスレ」と書き込まれたこともなく何故か開く 0.8091040849685669
ワイ草野球で143キロ計測 0.8077213764190674
禁書の七人に一人無能がおるよな 0.8069646954536438
【画像】「私はロボットではありません」と主張する男がsnsに広がり始める 0.801096498966217

Prefix「三大」で生成したスレタイ

三大購入特典で必ず発送された商品「発送」「6時」「前売り」 0.8469894528388977
三大モテない奴は難しい言葉「努力以上の努力が必要」「努力は必ず責め」「代表」 0.8417271375656128
三大怖さがよくわからない単語 「マクナル」「マクド」 0.8301395177841187
三大気になっててくる車『山登る』『砂降り』『雪の降る街』『雪』 0.8113009333610535
三大深夜12時にありがちな事「伸びてるレス」「実は内容が急に伸びていた」 0.8103548884391785
三大理解不能な「若干滑ったな」「それでも否定コメしたらええんや」あとひとつは? 0.8086734414100647
三大アフィ大喜利本 「 頭 く い な っ て の い」 0.8043513894081116
三大森野メジャーで獲らない球団「日ハム」「ヤクルト」あと1つは? 0.8020216822624207
三大彼女連れてきた車「砂漠」「まや」「ゲド戦」 0.7955830097198486
三大なんかあるミステリー小説『ダディ』『ゲームの面別』 0.7863622903823853

Prefix「【なぞなぞ】」で生成したスレタイ

【なぞなぞ】平常時川で橋の上に座る? 0.897125244140625
【なぞなぞ】鉄板志の鉄が1つだけ買えるポジションってなーんだ? 0.8656874895095825
【なぞなぞ】屁がこえることが出来ない奴と良い法律で同居できる問題 0.8614312410354614
【なぞなぞ】2chではすぐngにするとその程度の書き込みができる人ってなーんだ? 0.8599361181259155
【なぞなぞ】『校長部類』と「先生」しか居ない説 0.8505473136901855
【なぞなぞ】トロースでかい野菜ってなーんだ? 0.8492956757545471
【なぞなぞ】野菜がパンしか食えない食べ物ってなーんだ? 0.8488330841064453
【なぞなぞ】女の子が大好きな男をデートに誘うために行くものってなーんだ? 0.8411668539047241
【なぞなぞ】痒くもなく終える前に食べるものはなんでしょう? 0.8402188420295715
【なぞなぞ】「悪の趣味」を使って文章を作りなさい 0.8386388421058655

Prefix「ゲーミング」で生成したスレタイ

ゲーミングpc買うのに利用者数分金払ってるやつ<kusa> 0.8862704038619995
ゲーミングpcとwindowsとドラえもんってどっち選ぶんや? 0.8519608974456787
ゲーミングpc買いたいからオススメのオススメ教えてクレメンス 0.8464795351028442
ゲーミングpcのチャクチャチャのiphone買えばええ? 0.8367217183113098
ゲーミングpcでpubg配信した結果 0.8358103632926941
ゲーミングpcをsfcとやりたいんだけど何か良いゲーム教えて 0.828700602054596
ゲーミングpcの探しが遅いニキおる? 0.8259073495864868
ゲーミングスマホ買おうと思うんやけどオススメのゲームない? 0.8258553743362427
ゲーミングpc使いにいったら気づいた事<kusa> 0.8251885771751404
ゲーミングpcのパーツ1tb使ってるやつおる? 0.8218405842781067

雑感

  • 全体的に前回より精度が良い文書生成ができたと思います。
    • 本当はちゃんと比較をしたかったのですが、色々変えて直接比較ができそうになかったため諦めました。
    • パラメータの問題かもしれませんが、前回よりややユーモアのある文書が生成できているようにも見えます
  • 数が入る文書(ワイ草野球で143キロ計測禁書の七人に一人無能がおるよな)も違和感ない数値を使った文書を生成できています(※両者とも学習データに同じ文書は存在せず、似たものは存在する)
  • sentencepieceの影響?
    • 前回と比べてUNKトークンの生成率が大幅に下がりました。
    • 若干文法がおかしい文書が生成されやすくなった気がします。
    • 鉄板志のような(おそらく)造語を使った文書が生成されるようになりました。

参考にしたページ

(最終版)Karabiner-Elementsで「EISUU to Escape(入力ソースが日本語以外の場合のみ)」を実現する

以前に紹介したKarabinerの設定「EISUU to Escape(入力ソースが日本語以外の場合のみ)」をKarabiner-Elementsを使って実現する方法について、最終版と思える設定を実現できたため紹介します。

f:id:ykicisk:20170916162357p:plain
KarabinerのEISUU to Escape

以前の記事ではComplex Modificationの変数を使ったゴリ押し設定で実現する方法を紹介しましたが、Karabiner-Elements 11.2.0でinput_source_ifinput_source_ifが追加されたため完全再現ができるようになりました。 *1

設定方法

今回もComplex Modificationの設定で実現をします。

.config/karabiner/assets/complex_modifications/EisuuToEscapeInTerminal.json に以下のようなjsonファイルを配置します。

frontmost_application_ifのconditionは特定のアプリケーションを利用しているときだけに発動させたいときに設定します。自分の環境に合わせて適宜変更してください。

{
  "title": "Terminal等で入力ソースが日本語以外の場合に英数をEscapeに変更する",
  "rules": [
    {
      "description": "Terminal等で入力ソースが日本語以外の場合に英数をEscapeに変更する",
      "manipulators": [
        {
          "type": "basic",
          "from": { "key_code": "japanese_eisuu", "modifiers": { "optional": [ "any" ] } },
          "to": [ { "key_code": "escape"} ],
          "conditions": [
            {
              "type": "input_source_unless",
              "input_sources": [ { "language": "^ja$" } ]
            },
            {
              "type": "frontmost_application_if",
              "bundle_identifiers": [
                "^com\\.apple\\.Terminal$",
                "^org\\.vim\\.",
                "^com\\.googlecode\\.iterm2$",
                "^com\\.microsoft\\.VSCode$"
              ]
            }
          ]
        }
      ]
    }
  ]
}

次に、Karabiner-ElementsのPreferenceから「Complex Modifications」→「Add rule」→「Terminal等で入力ソースが日本語以外の場合に英数をEscapeに変更する」を有効にします。

以上で設定は完了です。

まとめ

非常にシンプルな設定ファイルになって満足しています。

自分以外にJSキーボード+英数をEscにする派は見たことがないですが、お役に立つ方がいたら嬉しいです。

*1:実は以前の記事を公開した時点でKarabiner-Elements 11.2.0がリリースされていたのですが、気づいていませんでした、、

【Swift】WKWebViewを使ってパワーポイントをPDFに変換する

以前開発したPDF Margin Adjusterについて、パワーポイントのファイルを直接読み込めるようにしてほしいという要望を頂きました。

なんとかそれらしいものを実装できましたが、意外と手こずったのでハマった部分などをまとめました。

前回の記事:PDFの余白を調整するだけのアプリを作りました - のどあめ

PDF Margin Adjuster

PDF Margin Adjuster

  • Yohei Iseki
  • Productivity
  • Free
apps.apple.com

ソースコード

今回はパワーポイントからPDFに変換する部分だけを切り出したコードを公開しています。

github.com

f:id:ykicisk:20200419205910p:plainf:id:ykicisk:20200419205914p:plain
(左)変換前(WKWebView)(右)変換後(PDFDocument)

ハマり1:パワーポイントファイルのUniform Type Identifier(UTI)がわからない

iOSアプリはファイルの読み込み・他アプリから連携できるファイルをUTIで指定します。

今回はパワーポイントファイルの読み込みを対応させるので、対応するUTIをInfo.plistに記載する必要がありました。

UTIの公式は System-Declared Uniform Type Identifiers のようなのですが、サードパーティで定義されたUTIは網羅されていないようです。

調べると com.microsoft.powerpoint.​ppt org.openxmlformats.presentationml.presentation あたりはすぐ見つかるのですが、 これだけでは開けないファイルも少なからずあります。

GitHubでUTI名で検索して周辺の類似したものを取得するという方法で、 以下のものを追加しました。
これで手元にあったパワーポイントファイルは開けるようになりましたが、すべてを網羅できているかは不明です。。

com.microsoft.powerpoint.ppt
com.microsoft.powerpoint.pot
com.microsoft.powerpoint.pps
com.microsoft.powerpoint.openxml.presentation
com.microsoft.powerpoint.openxml.slideshow
com.microsoft.powerpoint.openxml.template
org.openxmlformats.presentationml.presentation
org.openxmlformats.presentationml.slideshow
org.openxmlformats.presentationml.template

ハマり2:WKWebViewからPDF変換のあれこれ

PDF変換方法

UIViewからPDFに変換する方法はいくつかあるようですが、 一部の方法ではページの後半が描画されなかったりして苦戦しました。

結論としては UIPrintPageRendererを使ってPDFデータを取得する方法でうまくいきました。

実装は このあたり です

WKWebViewで開いているパワーポイントのスライドサイズを取得する

UIPrintPageRendererは印刷用のレンダラーなので用紙サイズ(=1スライドのサイズ)を指定する必要があります。

以下のブログを参考に、描画後の innerHTMLからスライドサイズが入っている要素をevaluateJavascriptで取得します。

実装は このあたり です

WKWebViewのマージン削除

WKWebViewでパワーポイントファイルを開くと、top・left・ページ間にマージンが発生します(下図)

UIPrintPageRendererを使ってPDFファイルに変換すると、このマージンも正確に描画されてしまうため、実際のスライドサイズとずれてしまいます。

そこでWKWebViewConfiguration を使ってファイル読み込み後にCSSを適用し、このマージンを削除します。

f:id:ykicisk:20200419214357p:plainf:id:ykicisk:20200419214351p:plain
(左)デフォルト、マージンあり(右)マージン削除した結果

実装は このあたり です

WKWebViewのロード後1秒待つ

WKWebViewがパワーポイントファイルを描画するときに、先に一部が描画されて少し時間が経ってから全体が描画されることがあります

全体が描画されたかどうかを判定する術はなさそう(例えばisLoadingは一部が描画された時点で false になる)ですが、一部だけ描画されたタイミングでPDF変換をすると、その一部だけが描画されたPDFになってしまいます。

今回はファイルをロードしてから1秒待つという残念ロジックでこれを回避しました。

GitHubに載せたコードでは省略しています)

用紙サイズをスライドサイズの0.8倍にする(おまじない)

先程取得したスライドサイズを UIPrintPageRendererpaperRect printableRectに指定するのですが、これをそのまま指定すると何故か用紙サイズがちょっと大きくなります。

f:id:ykicisk:20200419221857p:plain
スライドサイズをそのまま指定した場合

色々試しましたが、paperRectprintableRectに用紙サイズ * 0.8を入れることで安定してPDFファイルに変換することができました。

理由は不明で完全におまじない状態です。。

実装は このあたり です

まとめと感想

WKWebViewからPDFに変換する方法と、そのハマりポイントをまとめました。
PDF変換周りは挙動が怪しいところがあるので、もう二度とやりたくないです。。

謎のおまじないを残してしまったのは残念ですが、これ以上時間をかけても意味がないので放置しました。
どなたかわかる方がいらっしゃいましたらご指摘いただけると助かります。

SwiftUIを初めて触ってみましたが、結構良さげな雰囲気がありますね。
iOSだけだったらReactNative+Expoなどを使わずとも簡単にアプリ作れそうです。

AlphaZeroで最強のターン制ぷよぷよAIを作る!

最近こちらの本を読みました。
丁寧にAlphaZeroの解説が載っているので、AlphaZeroに興味ある方にはおすすめです。

www.borndigital.co.jp

この本を読んでゲームAI作りたい欲が湧いてきたので、
今回はAlphaZeroでターン制ぷよぷよ(細かいルールは後述)の最強AIの学習にチャレンジしました。

※今回はかなり本を参考に実装したためソースコード公開はしません

結果(CNN モデル VS ResNetモデル)

先に結果だけ見せるとこんな感じです。
自己対戦だけで初代ぷよぷよの定石(早めに中連鎖する)を学習することができました!

f:id:ykicisk:20200405170810g:plain
CNNモデル(左) VS ResNetモデル(右)

AlphaZeroとは

AlphaZero自体については色々な方の解説記事が沢山あるので割愛します。
例えばブレインパッドさんの記事がおすすめです。

強化学習入門 Part3 - AlphaGoZeroでも重要な技術要素! モンテカルロ木探索の入門 - - Platinum Data Blog by BrainPad

AlphaZeroでターン制ぷよぷよのゲームAIを学習する

AlphaZeroは二人零和有限確定完全情報ゲームのためのアルゴリズムです。
今回はぷよぷよをターン制にした二人零和有限確定ゲーム(完全情報ではない)を考え、これを対象としました。

ぷよぷよを参考にしたターン制落ち物ゲームは過去いくつか考えられているようです。
今回はpuyoloftさんの連棋(rengi)というゲームを参考にルールを決めました
「連棋(Rengi)」ターン制落ち物対戦パズルゲーム

今回扱うターン制ぷよぷよのルール

実装の都合などで本家から色々改変していますが、 基本的にはフィールドが小さい初代ぷよぷよだと思って貰えればOKです。

  • 基本ルール
    • プレイヤー2人
    • ターン制で同時着手方式(※この要素で完全情報ゲームでなくなっている)
    • ぷよは4色+お邪魔ぷよ
    • お邪魔ぷよ以外の同色ぷよが4つ以上つながると消える
    • お邪魔ぷよは隣接するぷよが消えたとき一緒に消える
    • フィールドは横6列×縦9段(※計算時間短縮のため本家より4段小さい)
    • 左から3列目、下から8段目が埋まると負け
    • N連鎖するとN+1ターンの待状態になる
  • ツモ(組ぷよ)
    • 2個1組のツモ
    • ツモがあるときはどこかに置かなければならない
    • ツモ固定
    • 未来のツモが既知 (※AlphaZeroのアルゴリズム上そうなる)
  • 点数計算・お邪魔ぷよ
    • 相殺なし(初代ぷよぷよルール)
    • 点数計算、落ちてくるお邪魔ぷよの数は本家と同じ
    • お邪魔ぷよがおちる場所は(ターン数シードの)ランダム

モデル(デュアルネットワーク)

AlphaZeroは「自己対戦→棋譜を作成→モデルを学習」を繰り返してゲームAIの学習を行います。

AlphaZeroのモデルには、現在のゲーム状態を入力・ (policy, value)の2値を出力とするデュアルネットワークを使用します。 ここでpolicyは次の手の確率(行動数次元の出力)、valueはゲーム状態の評価値(1次元)みたいなイメージです。

(詳しくはAlphaZeroを解説している記事を参照してください)

行動数

行動数はツモを落とすパターン+パスの23通りです。
そのため、モデルのpolicy出力は23次元です。

ツモを落とすパターン 行動数
○▲ 左端〜右端までの5通り
▲○ 左端〜右端までの5通り

左端〜右端までの6通り

左端〜右端までの6通り
ツモなし 1通り ※自分・相手の連鎖待ちなどでツモがない場合はこの行動を取る

ゲーム状態のテンソル表現

モデルの入力にするためにターン制ぷよぷよのゲーム状態をテンソルで表現します。

今回はゲーム状態を(9 × 6 × 49)次元で表現します。
ここで(9 × 6)はフィールドのサイズで、49のchannelに以下の要素が含まれます。

1.フィールドの状態

  • ぷよが存在する位置は1, それ以外は0のテンソル
  • {自分, 相手} × {ぷよ 5種 ※お邪魔ぷよ含む}の10 channel

2.ツモ色を(9 × 6 × channel数)にone-hotエンコーディングっぽくしたもの

  • {自分, 相手} × {現在, Next} × {ツモ個数 2個} × {ぷよ 4種} の32 channel
  • 例えば、現在ツモが「」のときは、以下のような(9 × 6 × 8)次元のテンソルになる。

f:id:ykicisk:20200405153540p:plain
ツモ(組ぷよ)のテンソル表現

3.上と同様にその他の情報を(9 × 6 × channel数)にエンコーディングしたもの

  • 以下の7channel
    • 経過ターン数 1 channel
    • 次ターンに落ちるおじゃまぷよの数 {自分, 相手} の 2channel
    • 連鎖が終わったら落ちるおじゃまぷよの数 {自分, 相手} の 2channel
    • 連鎖待ち時間 {自分, 相手}の 2 channel

モデル

今回はTensorflow 2.1のtensorflow.kerasで2つのモデルを試しました。
本家AlphaZeroに比べるとかなり軽量なモデルです。

モデル名 ネットワーク詳細
CNNモデル Conv2D→Conv2D→MaxPooling2D→DropOut→Flatten→Dense
ResNetモデル Conv2D→BatchNormalization→ResidualBlock※*4→GlobalAveragePooling2D→Dense

※ResidualBlockは Conv2D→BatchNormalization→Conv2D→BatchNormalizationで1ブロックになっています。
※ResidualBlockの出力は、入力と最後のBatchNormalization出力の和になる

実装の工夫

自己対戦では現在のモデルのPredict値を使ってモンテカルロ木探索します。
そのため、普通に実装すると1手ごとにモンテカルロ木探索数だけ直列にPredictが走ってしまいます。

そこで計算時間短縮のために以下の工夫を行いました。

  • multiprocessingで自己対戦の並列化
    • 自己対戦は独立なので並列に行っても問題ありません
    • Tensorflowが絡む部分(モデルのPredictionなど)はメインプロセスで行います。
    • 自己対戦プロセスからPredictionの入力(ゲーム状態)を Queue でメインプロセスに送り、メインプロセスはPrediction結果を後述のキャッシュに格納します。
  • (ゲーム状態, モデルのPredict値)、(ゲーム状態, モンテカルロ木探索結果)のキャッシュ

正確に測っていませんが、体感で10倍以上の高速化になっていると思います。
(もうちょっと賢い方法はありそう)

実験

パラメータ・学習時間

パラメータ 備考
エポック数 100 自己対戦〜モデル学習のサイクル数
1エポック数あたりの自己対戦数 150 AlphaZero本家は25,000回。学習時間の削減のためかなり少なめ。
1エポック数あたりの時間 約60分 高速化した割には遅い気がする。。
うちモデル学習時間 約1分 学習のほとんど自己対戦している時間。GPUなんていらなかった。

結果(再掲)

左がCNNモデル、右がResNetモデルで対戦した結果です。
CNNモデルが3連鎖ダブル、ResNetモデルが4連鎖でResNetモデルの勝ちです。

ターン制だけあってちょっとアグレッシブな積み方をしていますが、 いらないぷよを端に避けたりしいて面白いですね。

f:id:ykicisk:20200405170810g:plain
CNNモデル(左) VS ResNetモデル(右)

考察

  • 自己対戦の時間
    • 自己対戦ではPredictionのキャッシュがあるので同じような展開が多いと学習時間が短くなります。
    • 学習時間はエポックが進むに連れて単調に落ちると思ったのですが、新手を見つけるとキャッシュに当たらなくなり学習が遅くなるようです。(下図)
    • 学習時間を見ることでブレイクスルー的なものが確認できるかもしれません。
    • 学習時間が増えている「エポック数40-50」、「エポック数80-100」の差を見てみました。

    f:id:ykicisk:20200405162623p:plain
    ResNetモデルにおけるエポック数(横軸)と自己対戦にかかった時間(縦軸)

エポック数40と50の差

f:id:ykicisk:20200405162708p:plain
ResNetモデルにおけるエポック数40(左)とエポック数50(右)の差

エポック数40と50では、3連鎖 vs 3連鎖(2連鎖目ダブル)という違いが出ています。
中盤で置くぷよの位置が変わったようです。

エポック数80と100の差

f:id:ykicisk:20200405162753p:plain
ResNetモデルにおけるエポック数80(左)とエポック数100(右)の差、(上)連鎖前、(下)連鎖後

エポック数80と100では、3連目での消えるぷよが1個増えています。
その結果、お邪魔ぷよが落ちたときにギリギリ1手差で勝つようになりました。

まとめと感想

  • AlphaZeroを使うことでターン制ぷよぷよのゲームAIを作ることができました
  • ツモが固定問題
    • ツモ固定のため、対戦というより中連鎖の最適解を探すモデルになっていたようです。
    • 実際相手の盤面をほとんど見ないモデルになっていそうでした。
    • ツモを固定にせず学習すれば汎用的なぷよぷよAIになるかもしれません。
  • 自己対戦時間がネック
    • AlphaZeroにおいて自己対戦をいかに早くするかが勝負です。
    • モデルの学習よりモンテカルロ木探索やゲーム自体の最適化が重要だと思います。
    • もちろんお金があればサーバを横に並べて力で殴っても良いと思います。
  • 過学習問題
    • 早々に過学習してしまう場合があり、AlphaZeroを安定して学習するのは難しそうです。
    • 例えば1〜5エポックでカエル積みを覚えてしまうと、何エポック経ってもそこから抜け出せないことがありました。
    • 1エポック数あたりの自己対戦数を増やすと解決するかもしれません。
  • matplotlibでアニメーション作るのが想像以上に便利でした。

Tensorflow 2.0でなんJスレタイ生成器を構築する(2回目)

前回なんJスレタイ生成器を作る記事を書きましたが、 これには以下の問題がありました。

  • 当時は train_on_batch の存在を知らずにゴリ押しでモデル学習をしていた
  • 生成されるサンプルに多様性がなかった

今回Tensorflow 2.0を勉強するついでになんJスレタイ生成器を作り直してみました。

github.com

注意

NLPニューラルネットもよくわかってない人が書いているので間違っている部分があると思います。

生成例

※ 一部ブログに書くにはアレな文書も生成されていたので選抜しています。 選抜していないものを見たい方はこちら
SOSはスタートトークン、EOSはエンドトークン、PADはパディングトークン、UNKは未知語トークンを表します

制限なし

かなりバリエーションのある文書を生成できるようになりました。
また、今回は割と長めの文書も生成できるようになっています。

スレタイの後ろにあるスコアはEvaluator(後述)の出力値です。

SOS 名前 に 「 デス 」 が 付く 曲 NUM つ しか ない EOS PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD score: 4.6163287
SOS 敵 「 ワイ の こと 好き な ん ? 」 トッモ 「 は ? 僕 は ? 」 EOS PAD PAD PAD PAD score: 3.3199003
SOS なんj 民 が 知っ て いる 一番 の UNK は ? EOS PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD score: 3.029164
SOS ワイ NUM 歳 大学生 結婚 し たい のに 仕事 が ない EOS PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD score: 2.7726371
SOS (*^◯^*) ← 今年 ? 行か なけれ ば なら ない EOS PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD score: 2.641209

prefix「三大」

前回の例でも出した「三大」から始まるスレタイです。

今回も「三大〜「A」「B」あと一つは?」のような構造を学習できています。 (一部怪しいのもありますが)

SOS 三大 やらかし た UNK の 根本 の 中 で の 会話 「 ドア 机 」 「 杉浦 」 EOS PAD PAD PAD PAD score: 3.601511
SOS 三大 う けど 勢い 揃っ てる 気 が つい た 欠陥 UNK 馬 EOS PAD PAD PAD PAD PAD PAD PAD PAD PAD score: 3.2168586
SOS 三大 UNK が UNK に 必要 な もの 「 早稲田 」 「 ポテチ 」 あと 一 人 は ? EOS PAD PAD PAD score: 3.192483
SOS 三大 値段 高い と 思う な カッコイイ モビルスーツ ランキング ( NUM 番 最初 数字 ) EOS PAD PAD PAD PAD PAD PAD PAD score: 3.0752976
SOS 三大 当たり 出し が 激しい チーム 助っ人 采配 イチロー EOS PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD score: 3.0569434

prefix「【なぞなぞ】」

【なぞなぞ】から始まるスレタイはたいてい最後が問いかけになるのですが、 その構造がうまく取れています。ちゃんとなぞなぞっぽくなっていますね。

SOS 【 なぞなぞ 】 誰 でも 作る べき UNK の 曲 なー ん だ ? EOS PAD PAD PAD PAD PAD PAD PAD PAD score: 4.264069
SOS 【 なぞなぞ 】 ゴム つけ て そのまま ケーキ 誘う もの ってな ー ん だ ? EOS PAD PAD PAD PAD PAD PAD PAD score: 3.9145274
SOS 【 なぞなぞ 】 口 に 入れる と 美味しい 食べ物 ってな ~ ん だ ? EOS PAD PAD PAD PAD PAD PAD PAD PAD score: 3.8251681
SOS 【 なぞなぞ 】 UNK で 海 ワン が かける 言葉 ってな ー ん だ ? EOS PAD PAD PAD PAD PAD PAD PAD score: 3.6090522
SOS 【 なぞなぞ 】 バイク が 食わ れ て の sa お トラブル みたい な 名前 の メンバー は なん でしょ う ? EOS score: 3.5852108

モデルの詳細

今回のなんJスレタイ生成器は、スレタイっぽい文章を生成するGeneratorと、 Generatorの生成した文書を評価するEvaluaterの2つで構成されます。

Generatorでたくさんスレタイを生成し、Evaluaterで高スコアのスレタイのみを出力します。

Generator

Generatorは4層GRUによるテキスト生成器です。モデル構造はnotebookで見たほうが早いと思います。

Evaluater

EvaluatorはBidirectional GRUで構成された「Generatorの生成したスレタイ」と「本物のスレタイ」を区別する識別器です。 こちらもモデル構造はnotebookを見てください。

工夫した点・苦労した点

Generator / Evaluator構成

最初はVAEでスレタイを潜在ベクトルzで表現し、zをGANで生成する、というアイデアで文書生成を試みました。

ところが、このアイデアはVAEまではうまくいったのですがGANの学習がうまく行かず断念し、 最終的にGenerator・Evaluator構成に落ち着きました。

ちなみに、Evaluatorがないと以下のように構造を捉えられていないスレタイが生成されてしまうので、Evaluatorには一定の効果はありそうです。

SOS 【 なぞなぞ 】 女児 に 笑わ れ た 男 EOS PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD
SOS 【 なぞなぞ 】 bbq に いる 女の子 と は なん だ ? EOS PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD
SOS 【 なぞなぞ 】 なんj を UNK て も 訪れ なかっ た もの EOS PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD
SOS 【 なぞなぞ 】 ミサイル が 嫌 に なら なかっ た 球場 に ビビっ てる スレ で は ない チーム は ? 日本 だけ
SOS 【 なぞなぞ 】 精神的 に しか いる ん だ う なー ンゴ ? EOS PAD PAD PAD PAD PAD PAD PAD PAD PAD

Functional APIの利用

Tensorflow 2.0のチュートリアルの一部では tf.keras.Modelを継承したクラスを作ってモデルを作っている例が出ていますが、 この場合、そのままだとsave_weightsなどでモデルを保存することができません。

KerasのFunctional APIを使えば回避できるので、 よっぽどでなければFunctional APIを使うのが良いと思います。

tf.functionを使うときはなるべくtf.functionで完結させる

Better performance with tf.function  |  TensorFlow Core によると、tf.function を使うと色々と性能UPするようです。

今回の検証でもtf.functionを使うと学習時間が早くなり、大きいバッチサイズでもOOMしないなどメモリ効率も良くなりました。 一方で、副作用が出ないように注意したのに@tf.functionだとエラーが出る、という挙動になることがままありました。

いろいろ試した結論としては「tf.functionを使うときはなるべくtf.function内ですべてを完結させる」ということです。

例えば一部の前処理を tf.function外で行ってその結果をtf.functionの引数で渡すのではなく、 前処理も含めてtf.function内で動かすと良いです。 tf.functionの引数はdatasetから取得したデータだけにする、ぐらいの心持ちで実装するのが良いと思います。

まとめ

Tensorflow 2.0を触るついでに、なんJスレタイ生成器を作り直しました。

はじめてTensorflow 2.0を使ってハマることも多かったですが、 最終的にまあまあの精度の生成器ができたので満足です。

参考にしたサイト

PDFの余白を調整するだけのアプリを作りました

最近論文を読むときはiPad + Apple Pencil + GoodNotes 4を使ってPDFにメモを書き込みながら読んでいます。 以前は紙に印刷して直接書き込んでいたのですが、この方法だと論文とメモをデジタルで残しておけるので非常に便利です。

しかし、1点だけ不満を思っていることがあります。 それは論文のPDFは余白が小さくメモをするスペースが狭いことです。 *1

PDF Margin Adjuster

そこで、この不満を解決すべくPDFのファイルの余白の大きさを調整するアプリを開発しました。

PDF Margin Adjuster

PDF Margin Adjuster

  • Yohei Iseki
  • Productivity
  • Free

開いたPDFファイルを[+][-]ボタンでインタラクティブに余白の大きさを調整できます。

f:id:ykicisk:20190209214807p:plain
スクリーンショット

これで、余白を大きくして広々とメモを取ることができ、快適に論文を読めるようになりました。

GoodNotes 4で開いたときのイメージ

f:id:ykicisk:20190209173535p:plainf:id:ykicisk:20190209173540p:plain
余白調整前後での比較(左: オリジナル, 右: 調整後)

それでは、皆様も良い論文ライフをお過ごしください!

*1:拡大してメモすればいいじゃんといえばそうなのですが、個人的には英文全体を見ながらメモしたい気持ちがあります

ReactNative + Expo.io で素早く画像認識アプリを作る

最近、顔の美しさをアノテーションしたデータセットSCUT-FBP5500-Databaseを知り、これでイケメン度・美女度判定アプリをつくったらウケるのでは?と考えたりしています。

これに限らず、ポケモン図鑑的な(写真をとったら何か結果を返してくれる)アプリはいろいろと応用が効くため、前々から作ってみたいと思っていました。

そこで、今回比較的時間をかけずにAndroid, iOS共通のアプリが作れると噂のReactNative + Expo.ioを使った画像認識アプリの作成に挑戦しました。

github.com

作りたいアプリのイメージ

  1. アプリで写真を撮影する
  2. アプリで撮った写真を何らかの方法で画像認識する
  3. 結果をアプリに表示する

事前調査

Expoの使い方

こちらの方の記事がよくまとまっています。
http://ykubot.com/2017/07/30/react-native-expo/

画面遷移の方法

今はreact-nativigationを使うのが主流だそうです。
こちらの記事がよくまとまっています。
https://bagelee.com/programming/react-native/react-navigation-react-native/

画像認識の方法

ReactNative+ExpoでkerasやTensorflowのモデルを動かせるか?

結論: APIを利用してfetchで取ってくる

ReactNative+Expo単体では画像認識が難しいので、base64エンコードした画像をAPIにPOSTして結果を受け取るようにします。
GoogleVision APIなどを使ってもよいですし、自力でAPIを作っても良いです。

最終的に作ったもの

github.com

Overview

以下のようなclient(アプリ)とserver(Webサーバ)を作ります。

f:id:ykicisk:20180531223550p:plain

server(Webサーバ)

  • ソース
  • flaskで実装、PCFでデプロイすることを想定しています。
  • /recognizebase64エンコードした画像が入ったjsonをPOSTすると、numpy.ndarrayに変換してshapeを返す仕様です。
    • 本当はここでkerasなりtensorflowなりでpredictした結果を返すことを想定しています。
@app.route('/recognize', methods=['POST'])
def recognize():
    if request.headers['Content-Type'] != 'application/json':
        print(request.headers['Content-Type'])
        return jsonify(res='error'), 400

    # decode base64 to image
    enc_data = request.json["base64"]
    dec_data = base64.b64decode(enc_data)
    img = Image.open(BytesIO(dec_data))
    x = np.asarray(img, dtype='uint8')

    # run something with 'x'

    return jsonify(result=str(x.shape))

client (アプリ)

  • ソース
  • 大抵はexpoで初期で作成されるファイルなので、.jsファイルだけ見ていただければと思います。
  • ReactNativeで実装、Expoで実機にデプロイすることを想定しています。

App.js

  • react-navigationを使って、画面遷移をする設定をします。
  • 今回はOverviewに書いてあるMainScreen(初期)とResultScreenです。 stackNavigatiorを使うと自動的に戻るボタンが作成されるので便利です(※後のスクリーンショットを参照)

src/componentes/Main.js

  • MainScreenの実装が書いてあります。
  • 「Camera」や「CameraRoll」ボタンで画像を取得します。
  • 「Recognize!」ボタンでResultScreenに遷移します。このとき、stateのimageをResultScreenに渡します。

f:id:ykicisk:20180531224419p:plain:w150
MainScreen
f:id:ykicisk:20180531224650p:plain:w150
ImagePicker利用時

src/componentes/Result.js

  • ResultScreenの実装が書いてあります。
  • 読み込み時にMainScreenから受け取った画像をserverにPOSTします(_getResultAsync関数)
  • serverから結果が返ってきたら、結果をstateに保存してRenderします。

f:id:ykicisk:20180531225322p:plain:w150
ResultScreen

所管

ReactNative(Expo)は決められたプリセットを組み合わせるだけのアプリなら簡単に作れますが、 枠から外れた途端何もできなくなる(難しくなる)印象です。

以下のものは実装を検討しましたがExpoで簡単に実装するのは無理だと判断し、諦めました。

  • KerasやTensorflowのモデルを動かす
  • 撮影した画像の任意の場所でCroppingする
  • Camera撮影画面をいじる

余談

SCUT-FBP5500-Databaseを使ったイケメン度・美女度判定アプリの大枠は無事完成しました。 が、このデータセットは研究目的でしか使えないことに気づいたので公開はしません。残念。