ProgressiveGANでちょっときれいなアニメ画像生成

f:id:Hi_king:20171119141322g:plain f:id:Hi_king:20171119134734p:plain

github.com

2017年は様々なGANの改良手法が開発されましたが,先月,Progressive GANという,中でもわかりやすいアイディアで高解像度な画像を生成できる手法が発表されたので,実験してみました.

Progressive GAN

まず,普通のGANについておさらいですが,

f:id:Hi_king:20171119003639p:plain

この図のように, 1. Generatorがきれいな生成画像を作る 2. Discriminatorが生成画像とデータセット画像を見分ける という役割をそれぞれが果たし,Discriminatorを騙すGeneratorを鍛えることできれいな生成画像になっていくという過程です. この手法をベースに,より自然な,より多様な,画像生成が模索されているというのが現状です.

さて,ここはポエムですが,大雑把に言って,自然な画像というのは,"細部が整っている", "全体が矛盾していない"の2つの観点が考えられるかなと思います. 低解像度の画像においては,細部は,データセットでも既に失われているので,全体が矛盾せずに,(例えば)人間っぽく見えることを目指して生成ができます. 具体例を出すと,金髪であれば碧眼の確率が高いとか,両目は同じ色の確率が高いとか,口と目は平行であるとか,そういう構図レベルの話に集中できるってことです. ゆえに高解像度の画像を生成するってのは,そういう構図レベルの問題と,各パーツの詳細レベルの問題を両方騙さなきゃいけないという難しさがあったわけです.

それに対してProgressive GANの答えがこの図です.

f:id:Hi_king:20171119003501p:plain

高解像度の画像を生成する問題を,うまく設計を工夫して一気に解こうとするのではなく,低解像度の画像を生成するネットワークから徐々に拡大していこうというアイディアです. これにより,高解像度のステージでは,おそらく各パーツの詳細を生成することに集中して学習が進むという利点があるのでしょう.

通常のGAN Progressive GAN
f:id:Hi_king:20171119012920p:plain:w128 f:id:Hi_king:20171119013350p:plain:w128

ちなみに,この,低階層からパラメタを決めていくという話は,stacked RBMで教師なし学習でネットワークを初期化する話を思い出される方も多いと思います. また,強化学習としてGANをとらえると,強化学習で複雑な問題を解くときには簡単な問題からカリキュラム学習するのがわりとメジャーであることとの連想もできるかと思います. また,そもそもGANを複数のステージ(2,3くらい)重ねるという話自体は初出ではなく,StackGANなどが存在してます. それらの面からも,ProgressiveGANはとても理にかなってる解決策に感じます.

補足として,Progressive GANのこの結果は,単に階層化だけの工夫ではなく,サンプル間の分散を保つように,Discriminatorに統計量を渡したり,生成時にピクセル間で正規化したりといった多様な工夫も入ってます. また,高解像度だけが取り柄ではなく,たとえ32x32でも4x4から成長させることで,既存手法に対して性能向上が観られています.

失敗したこと

VRAMの限界

各解像度の中間表現サイズ
4x4x512
8x8x512
16x16x512
32x32x512
64x64x256
128x128x128
256x256x64
512x512x32
1024x1024x16

論文で使われてるパラメタをなぞると,表のように結構深いチャンネルを使っていて,バッチサイズを適当にするとすぐにOOMで落ちてしましまいます. 論文では,これに対処するために,後半はメモリに乗るギリギリのサイズまで,stageごとにバッチサイズを減らすという対応をしていました. 僕は面倒なので途中のチャンネル数を雑に減らす対応でごまかしてます.が,1024とかいうめちゃでかいところまではやってないので,そんなに影響は出てないと思います.

mode collapse / あるいは高解像度データセットの必要性

stage11: 128 x 128 stage13: 256 x 256
f:id:Hi_king:20171119004114p:plain f:id:Hi_king:20171119004203p:plain
f:id:Hi_king:20171119004406p:plain f:id:Hi_king:20171119004441p:plain

256x256の画像を生成することを目標としてたんですが,256x256になるstage13でmode collapse(どんな入力を入れても出力が幾つかの決まった画像になってしまう)が起こってしまいました. 異なるコンテンツのデータセットで同じstageで起こってることが不思議だな,と思ったんですが,おそらくはデータセットの解像度のせいかな,と考えています. 実は実写顔画像の方は CelebAデータセットで,もとも96x96の解像度です. アニメ顔の方は,より大きなデータから顔を256x256に切り出してるんですが,顔の位置だけ切り出している以上,これも結構な低解像度が多く含まれてると思われます. 生成画像は256で生成できて,データセットは,より低い解像度の画像を"引き伸ばした"エフェクトがかかってしまっていると,そのエフェクト自体をDiscriminatorが手がかりにできてしまってるのではないかと疑ってます.

いずれにせよ,画像生成はデータセットのサイズだけではなくデータセットの解像度も必要な時代になったということですね.((((;゚Д゚))))ガクガクブルブル

他の方の実験結果

すごい.ついに顔画像以外も生成できるようになってる.

t.co

顔に関して,圧倒的な美しさを誇るMakeGirlsMoeも改良を行ってる.

これらの結果を見るに,手法を追うことも大事だが,特に細部までクオリティを上げていくことを考えるなら,データセットや前処理を見直すのも大事だなぁと実感します. 失敗例観てると,結構背景要素に引っ張られてるのが多いので,まずは背景を整えたデータセットを作りたい..

参考文献

  • Progressive Growing of GANs for Improved Quality, Stability, and Variation
  • github.com
    • 多様なGANの改良手法のchainer実装を公開している.Progressive GANも真っ先に実装されていて,今回の実装も大幅にこちらのコードを利用させていただいてます.
  • Stacked Generative Adversarial Networks
    • Xun Huang, Yixuan Li, Omid Poursaeed, John Hopcroft, Serge Belongie
  • StackGAN: Text to Photo-realistic Image Synthesis with Stacked Generative Adversarial Networks
    • Han Zhang, Tao Xu, Hongsheng Li, Shaoting Zhang, Xiaogang Wang, Xiaolei Huang, Dimitris Metaxas
  • ultraist.hatenablog.com
    • 顔クロップに使わせていただきました

Twitter社が発表した超解像ネットワークをchainerで再実装

f:id:Hi_king:20161218093554p:plain

この記事は Chainer Advent Calendar 2016の18日目の記事です。昨日は@zacapa_23さんのPokemonGANでした。僕もDCGANを使って百合漫画の解析に活かそうとしたことがあるので、なんだか親近感がわきます。ちなみにこの記事もDCGANがらみです。

数年前にニューラルネット超解像を行う研究が発表されてから、近年のディープラーニングブームの中で、特に企業の研究として(学習型一枚絵)超解像が発表されています。国内でも、特にイラストへの応用として、waifu2xという実装が注目を浴びたりしました。GitHub - nagadomi/waifu2x: Image Super-Resolution for Anime-Style Art

さて、いくつかある論文の中で、今回はTwitter社が9月に公開したもの( Photo-Realistic Single Image Super-Resolution Using a Generative Adversarial Network )を実装してみました。

github.com

この論文を選んだ理由は、ここ1年くらいのトレンドを余すことなく押さえた、という印象の、素直に"強い"実装だからです。以下に、このネットワークの特徴を記します。

ネットワークの構造と4つの工夫
f:id:Hi_king:20161218054443p:plain
  • 工夫1. ResNetを用いて、微修正をたくさん行う非常に深いネットワークになっている
  • 工夫2. Content Lossを用いて、従来の最小二乗誤差を用いたものよりも、"主観的"な超解像を行う
  • 工夫3. 加えてAdversarial Lossを用いて、更に"主観的に"
  • 工夫4. Deconvolutionの代わりにPixel Shufflerを用いて高速かつ綺麗な画像を生成

以上4点が全て、この一年ほどで独立に現れた技術で、この論文ではそれを全て実装しています。いかにこの業界の技術の伝搬が早いかがわかりますね。

工夫1. ResNetによる深い微修正ネットワーク

convolutionを通る/通らないの2つのパスがあるResidual構造(論文Fig4)
f:id:Hi_king:20161218034159p:plain

原論文は "Deep Residual Learning for Image Recognition" 。 MSRAから発表され、2015/12の画像認識コンペティションで猛威を振るったネットワークの論文で、今年度は様々な問題に適用されました。ネットワークを深くするときに、必ずしもその追加分のレイヤーを使わず、バイパスするルートも設けることで、簡単なネットワークから深いネットワークに滑らかに変化するものです。

今回の論文の生成ネットワーク(論文Fig4)
f:id:Hi_king:20161218033922p:plain

超解像の問題は、元の画像とほぼ同じだが、ディテールが異なるものを出力する、という性質上、非常にResNetと相性がいいと考えられます。今回の論文では、原論文にならった、Layer2つごとにバイパスする経路のほか、ほぼ全てのLayerをskipする(!?)特別な経路も設けています。

工夫2. Content Lossによる主観的な学習

つぎの工夫は、"超解像が何を目標とするか"の問題設定の修正です。 ここで参考とする論文は、これもまた大きく話題となった、Neural Artの論文です。 この論文の中で、"元画像のざっくりした見た目を保存しながら別の操作を加える"ために使われていたのがContent Lossです。

超解像が抱えている決定不能性 最小二乗誤差基準の超解像でやりがちなこと
f:id:Hi_king:20161218041451p:plain f:id:Hi_king:20161218040601p:plain

さて、ここで、超解像に対して筆者らが抱いている問題意識を説明すると、上の図のように、本来、低解像度画像から超解像度画像を生成する際には、しばしば、どういう線が正しいか決定できない場合が存在します。このときに、ピクセル単位の二乗誤差を最小化するよくある手法では、ある種"平均的な画像"が作られてしまいます。平均的な画像は、基本的にぼやけてしまいがちで、本当の高解像度画像とは簡単に見分けられてしまいます。

赤のどれかを作りたいのに、誤差の最小化で青が作られてしまう(論文Fig3)
f:id:Hi_king:20161218051106p:plain

そこで、まず導入したのが、ピクセル単位で無理やり合わせようとすることをやめて、もっとざっくりした特徴を表してくれるContent Lossを利用するものです。なお、詳細はNeural Artの原論文を参照していただきたいのですが、ここでいうContent Lossというのは、画像認識のためにトレーニングされた別のネットワークに、超解像と高解像の2つの画像を通した後の、中間層同士の二乗誤差を指します。

ところで、Content Lossの最小化問題にしたことで得られたのは、"だいたいあってる画像"を作る能力であり、 それだけでは片手落ちで、やはりぼやけた画像になってしまいます。 次に説明するAdversarial Lossを加えることで、ぼやけた画像を許容しないようなロスの設計にします。

工夫3. Adversarial Lossによる"画像生成"の"超解像"への応用

ちょうど1年前、アドベントカレンダーで盛り上がった論文を皮切りに、今年は画像生成ネタが多いに流行りました。このDCGANの亜種がいくつも提案されたり、学習を安定させる方法が検討されたり、単に生成するだけでなく文章などを入力に与えらたりなど、とにかくGANまわりでは話題を事欠かない年でした。

qiita.com

qiita.com

筆者らが注目したのは、GANの"データセットと出力が見分けられないように学習する"というアイデアは、まさに、超解像の目的を、美しい画像を作り、"超解像と高解像を見分けられないように人間を騙す"というところにおけば、そのまま流用可能なところです。ただし、これが本当にすべての超解像の目的かは疑問の余地はあります。たとえば防犯カメラの超解像を犯罪捜査に使う、などの目的を考えると難しいところでしょう。

Adversarial Lossによる学習
f:id:Hi_king:20161218065617p:plain

この図に示すように、見分けがつかない画像を生成するネットワークは、素直にGANの仕組みをつかって実装できます。ただし、実は、これだと、低解像度画像に全く関係のない超解像度画像を生成してしまいます。なので、工夫2のContent Lossと適切に混ぜ合わせて利用します(論文では1:0.00001と書いてある)。

これによって、前の節で問題とした、平均っぽい画像を作るからぼやける問題が、解決する、というお話です。

工夫4. Pixel Shufflerによる高速かつ美しい画像生成

最後に、これも最近のトレンドなので紹介しておきます。 たとえば前節で紹介したDCGANの論文などでは、低次元の抽象的な特徴から、画像を生成するために、Deconvolutionを用いているのですが、最近幾つかのタスク(物体認識など)で、あまりこの仕組みは、精度も速度も上手くないと考えられつつあります。

この論文では、低解像度から画像サイズを大きくする過程では、同じように超解像に取り組んでいる論文で提案された、PixelShuffler Layerを利用しています。

チャンネルの深さをサイズに変換するPixel Shuffler
f:id:Hi_king:20161218043005p:plain

PixelShuffler Layerでは入力のチャンネルをそれぞれ別Pixelとして展開するため、処理がメモリコピーだけで高速、かつ、オーバーラップもないので学習が上手くいっていればぼやけにくいと考えられます。

実験結果

さて、ここまで長々と説明してきたネットワークが、いかほどの能力か、というのを紹介します。

トレーニングには100万枚ほどの画像を利用し、3エポックほど回しています。 そのうち2エポックはLossは従来の二乗誤差、最後の1エポックをAdversarial+Content Lossで回してます。

イラスト編

学習データは、ニコニコ静画から100万枚ほど

条件 結果
入力低解像度 f:id:Hi_king:20161218081357p:plain
二乗誤差最小化で超解像度(工夫1と2のみ) f:id:Hi_king:20161218081417p:plain
工夫1〜4を全部入れた超解像 f:id:Hi_king:20161218082232p:plain
正解高解像度 f:id:Hi_king:20161218081438p:plain

工夫1と2のみの結果でかなりいい絵が出来てるようにみえます。 一方でLossの改良は、たしかにぼやけるのを消そうと頑張ってるのはわかるんですが、今度は画面全体に幾何学的なノイズを載せる安易な出力に陥っているように見えます。 これがAdversarialとContentの割合調整が悪いのか、まだまだ学習が足りないだけなのか、などはしょうじきわからなです。。あるいはイラストではまだGANが高解像度に働かない問題のせいかもしれません(自然画像と違ってイラストはピクセルあたりの情報量が少ない)。

実写編

学習データは、Imagenetから200万枚ほど

条件 結果
論文で使われてるサンプル
入力低解像度 f:id:Hi_king:20161218075109p:plain
二乗誤差最小化で超解像度(工夫1と2のみ) f:id:Hi_king:20161218074846p:plain
工夫1〜4を全部入れた超解像 f:id:Hi_king:20161218074755p:plain
工夫1〜4を全部入れた超解像度(論文に乗ってる版) f:id:Hi_king:20161218081029p:plain
正解高解像度 f:id:Hi_king:20161218074039p:plain
新たに作ったサンプル 夢のなかで香る(バラ)|フリー写真素材・無料ダウンロード-ぱくたそ
入力低解像度 f:id:Hi_king:20161218085850p:plain
二乗誤差最小化で超解像度(工夫1と2のみ) f:id:Hi_king:20161218085904p:plain
工夫1〜4を全部入れた超解像 f:id:Hi_king:20161218090051p:plain
正解高解像度 f:id:Hi_king:20161218090104j:plain

こちらも、工夫1と2のみの結果でかなりいい絵が出来てるようにみえます。 また、こちらではLossの改良が、イラストよりは差が出ているように見えます。 ただ、大きな問題として、論文と同じデータセットで試してみたのに、大分論文より結果が悪く(というか論文の結果がとんでもない。。すごい)、実験環境なりパラメタなり実装なりをもう少し見直さなきゃなぁというところです。また、自分で低解像度画像を作ったものの方が、結果がいいようにちょっと感じるので、もしかしたら縮小時のアルゴリズムに強く依存するものを学習してる可能性もあり、検討が必要かもしれません。

まとめ

Twitter社から発表された超解像の論文を実装しました。この論文は今年のトレンドであるネットワーク構造を上手く取り入れており、超解像自体を利用しない人も参考にするところがあると思います。ResNetは当たり前のことになりつつありますが、Adversarial Lossを直接的な画像生成以外の問題に使うのは、他の問題でも検討する価値があると思います。

ちなみに発表時点では妥当な手法であることはまちがいないんですが、いまなら、pix2pix のネットワークとの比較が必要になるかと思います。僕も、機会を見つけてpix2pixでの超解像とも比べてみたいと思います。

OpenCVでアニメ制作ソフトOpenToonzのプラグインを書くよ

やりたいこと

先日公開されたばかりのオープンソースアニメ制作ソフトOpenToonzを利用して、以下のように映像にエフェクトをかけるプラグインを作ります。OpenToonzのプラグインOpenCVを利用することがサポートされているので、基本的に画像処理の知識だけで作れます。

完成品1: Kmeansによる減色プラグイン

どうせなら機械学習みが少しでもあるプラグインを作ろう、ということで、入力画像の全ピクセルをクラスタリングして、クラスタ中心の色に量子化することで減色して、実写背景をアニメになじませるものをプロトタイプ作ってみました。

減色(k=10) 入力画像
f:id:Hi_king:20160401225718p:plain f:id:Hi_king:20160401225739j:plain

github.com

完成品2: ネガポジ反転

こちらはプラグインの作り方の練習も兼ねてのネガポジ反転の簡単な実装です。映像でネガポジ反転ガチャガチャやったらちょっと目に悪い映像になりそうですねw

反転 入力画像
https://raw.githubusercontent.com/Hi-king/opentoonz_not/master/sample/hokusai_not.0001.png https://raw.githubusercontent.com/Hi-king/opentoonz_not/master/sample/hokusai.jpg

github.com

作り方

OpenToonzのプラグインの作り方の基本は、ネガポジ反転を例に以下の記事に書きました。

qiita.com

また、公式の開発ドキュメントも以下にあります。 opentoonz_plugin_utility/opentoonz_plugin_utility_ja.md at master · opentoonz/opentoonz_plugin_utility · GitHub

詳細はQiitaとgithubを観ていただくとして、ここではOpenCVによる処理の部分のみの話をします。 OpenToonzのプラグインは、opentoonz_plugin_utilityに定義されている、tnzu::Fx クラスを継承して作るのですが、パラメタの設定や入力画像数の設定などを飛ばすと、基本的には Fx::compute メソッドをオーバーライドし、そこに処理を書くことでエフェクトがつけられます

ネガポジ反転

CV::Matは ~ 演算子で反転が行えるので、以下の数行のコードだけで実現できます。

  int compute(Config const& config, Params const& params, Args const& args,
              cv::Mat& retimg) override try{
    DEBUG_PRINT(__FUNCTION__);

    // 画像の読み込み
    if (args.invalid(PORT_INPUT)) {
      return 0;
    }
    cv::Mat input_img;
    input_img = args.get(PORT_INPUT);

    // RGBAのうちRGBを反転
    std::vector<cv::Mat> planes;
    cv::split(input_img, planes);
    planes[0] = ~planes[0];
    planes[1] = ~planes[1];
    planes[2] = ~planes[2];

    // 反転したチャンネルをマージして結果画像に
    cv::merge(planes, retimg);
    return 0;
  } catch (cv::Exception const& e) {
    DEBUG_PRINT(e.what());
  }

k-meansでの減色

やりたかった、ピクセルごとのk-meansについて、そのものずばりのサンプルがあったので、大いに参考にさせていただきました k-meansクラスタリングによる画像分割,減色 | OpenCV.jp

  int compute(Config const& config, Params const& params, Args const& args,
              cv::Mat& retimg) override try {
    DEBUG_PRINT(__FUNCTION__);

    // 画像・パラメタの読み込み
    if (args.invalid(PORT_INPUT)) {
      return 0;
    }
    int const k = params.get<int>(PARAM_K);
    cv::Mat input_img, img_reshaped, img_reshaped_converted, labels, centroids;
    input_img = args.get(PORT_INPUT);
    cv::TermCriteria criteria(cv::TermCriteria::MAX_ITER+cv::TermCriteria::EPS, 50, FLT_EPSILON);

    // (width, height)の画像データを、(1, width x height)のベクトル系列に直す
    img_reshaped = input_img.reshape(0, input_img.rows * input_img.cols);
    img_reshaped.convertTo(img_reshaped_converted, CV_32FC4);

    // kmeans
    cv::kmeans(img_reshaped_converted, k, labels, criteria, 1, cv::KMEANS_PP_CENTERS, centroids);

    // 各ピクセルをクラスタ中心で置き換える
    for (int y = 0; y < img_reshaped.cols; ++y) {
        for (int x = 0; x < img_reshaped.rows; ++x) {
            uint assignment = labels.at<uint>(y, x);
            int channels = img_reshaped.channels();
            for (int c = 0; c < channels; ++c) {
                img_reshaped_converted.at<cv::Vec4f>(y, x)[c] = centroids.at<float>(assignment, c);
            }
        }
    }
    img_reshaped_converted.convertTo(img_reshaped, img_reshaped.type());

    // (width, height)の形に直して出力画像に
    retimg = img_reshaped.reshape(0, input_img.rows);

    return 0;
  } catch (cv::Exception const& e) {
    DEBUG_PRINT(e.what());
  }

ちなみに、速度に関しては特に気をつけてないので、kを大きくすると結構時間がかかってしまいます。また、減色アルゴリズムとして考えると、RGB色空間でやってるとか、空間的距離を考慮してないとかの問題もあります。まぁでも、結果画像はそこそこ面白いと思うので、軽く使う分には全然ありかな、と思います。

まとめ

OpenToonzのプラグイン作ってみた記事を書きました。OpenToonzのドメインの問題をあまり考えず、結構OpenCVドメインに集中して実装できるので、普段画像処理をやっている方なら手軽に実装できるかと思います。OpenToonzは先日公開されたばかりで、まだまだ色々整備されておらず、いいプラグインを作れば、皆が使うようなデファクトスタンダードとなることもありうると思うので、楽しいんじゃないかな、と思います。

DCGANでさくひまの子供を推定する ~ キャラクターのベクトル演算 ~

結果

ひまわり <-> さくらこモーフィング

f:id:Hi_king:20160116202106p:plain

コードは以下で公開

github.com

DCGAN

以下の2記事のように、昨年末はDCGANの効果のすごさを感じましたね。DCGANの仕組みについては両記事をみてもらうとして、この記事ではDCGANを用いて、既に存在する画像の平均画像を生成します。

qiita.com

qiita.com

画像からパラメタの算出

DCGANでは、"Generator: パラメタから画像を生成する"と、"Discriminator: 画像が機械的に生成されたものかどうかを判別する"の2つを学習します、詳しくは前述の記事を参照。このGeneratorの学習済みモデルは前述の記事で公開されているので、これで遊べるぜ、ってやつです。ところで、今回はひまわりさんとさくらこさんの子供の顔がみたいんじゃーという欲求を満たしたいんですが、そのためにパラメタチューニング職人になってひまわり画像を生成するパラメータを探したりはしたくないです。

ちょうどそういうパラメータ探しこそ機械学習が得意とするところゆえ、学習しておきましょう。つまり、"Vectorizer: 画像を入力として、Generatorによってその画像が再現されるパラメタを出力する"を学習します。コードは以下。

https://github.com/Hi-king/chainer-DCGAN/blob/master/bin/test_vectorizer.py

学習済みモデルは以下に公開します。

www.dropbox.com

モデルは、Discriminatorの出力をGeneratorの入力と同じ100次元にするだけです。学習用データセットは、ランダムなパラメータと、Generatorによって生成された画像をペアとして、画像からパラメータを出力するようにトレーニングします。ただし、DCGANの学習に使われたデータセットは、かなり綺麗に顔が切りだされているようで、ちょっとでも顔の位置がずれている画像だと、上手く再生成(後述)出来ませんでした。なので、Vectorizerで学習するデータセットでは、生成された画像を拡大縮小、上下左右移動をランダムに行います(Data Augmentation)。

本当にこのVectorizerでパラメタの学習が出来たかを確かめるために、画像A -> Vectorizer -> Generator -> 画像A'という再生成をやってみます。

ココアさん再生成

seiga.nicovideo.jp

f:id:Hi_king:20160116194957p:plain

チノさん再生成

seiga.nicovideo.jp

f:id:Hi_king:20160116195001p:plain

さすがに服装まで綺麗に生成することは出来ませんでしたが、無事にココアさん、チノさんだと言って納得してもらえる画像が生成出来たと思います。

モーフィング

Vectorizerでベクトルに落とし込めた、ということは、2人の距離の中間に存在する画像、というのも生成できるわけですね。やってみましょう。

ひまわり <-> さくらこ

f:id:Hi_king:20160116202106p:plain

左側がひまわりさん100%、右側がさくらこさん100%です。中間だと2人のベクトルを50%ずつ足し合わせた画像ということになります。ひまさくの子供はこういう顔になるんでしょうか(^_^)

ココア <-> チノ

f:id:Hi_king:20160116202054p:plain

さきほどの二枚のイラストからも中間画像を作ってみます。2人のベクトルの中間だと、紫髪の子になりそうですね。

ところで今回の仕組みとは違うんですが、nico-opendataでも、同様に二枚の画像の中間画像の検索機能があります。これによると、中間画像は漣ちゃんのようで、まぁピンク、紫あたりが中間画像になってて、DCGANと似たような特徴にはなってるかなぁ、と言った感想ですね。

複数人の平均画像

2人の画像の中間が作れるんだから、もっといろいろ混ぜてみたくなりますね。と、いうことで、ニコニコ静画で"ラブライブ"タグで引っかかった画像100枚の中間を作ってみました。生成に使ったパラメタは以下。100枚ないのは、顔認識に成功したもののみを使っているからです。

f:id:Hi_king:20160116201643p:plain

中間画像は

f:id:Hi_king:20160116201822p:plain

真姫さんだコレー

と、いうことで、皆の特徴をもっているというよりは、西木野真姫さんになってしまいました。ナニソレイミワカンナイ

chainerでニューラルネットを学んでみるよ(chainerでニューラルネット2)

前回の記事ではchainerのインストールからサンプルコードを使って画像識別問題を解くところまでやりました。 hi-king.hatenablog.com

今回の記事では回帰・分類問題用のシンプルなニューラルネットの作り方をやろうと思います。andとxorの論理式を学習させます。chainerでの実装の学習と、あとニューラルネットの教育目的に使いやすいなーと思ったので。2層のニューラルネットまで段階をふんで解説してるんですが、プログラム読むほうが得意、って方は一番最後のコードを先に読んだほうがわかりやすいかもしれません。

追記(7/13)型チェック

chainer1.1.0から型チェックが入ったので(https://github.com/pfnet/chainer/pull/95)、識別にはfloat32を入力してint32を出力、回帰にはfloat32を入力してfloat32を出力、という型でデータを入れるようになりました。元の、型を曖昧に扱っているコードが動かなくなったので修正しました。(train()の、x=....astype(numpy.int32)とかの部分)

ネットワークの書き方

下記のコードは、andを学習するために作ったネットワークです。最も小さいサンプルと言っていいと思います。andは2項演算なので、2入力のニューロン、そして2クラス分類問題として解くためにそれを2つ用意してます。図のNNをコード化したものです。

f:id:Hi_king:20150627194056p:plain

class SmallClassificationModel(chainer.FunctionSet):
    def __init__(self):
        super(SmallClassificationModel, self).__init__(
            fc1 = chainer.functions.Linear(2, 2)
            )

    def _forward(self, x):
        h = self.fc1(x)
        return h
        
    def train(self, x_data, y_data):
        x = chainer.Variable(x_data.reshape(1,2).astype(numpy.float32), volatile=False)
        y = chainer.Variable(y_data.astype(numpy.int32), volatile=False)
        h = self._forward(x)

        optimizer.zero_grads()
        error = chainer.functions.softmax_cross_entropy(h, y)
        error.backward()
        optimizer.update()

        print("x: {}".format(x.data))
        print("h: {}".format(h.data))
        print("h_class: {}".format(h.data.argmax()))

model = SmallClassificationModel()
optimizer = chainer.optimizers.MomentumSGD(lr=0.01, momentum=0.9)
optimizer.setup(model.collect_parameters())

chainerで、あるネットワークを記述するために必要な記述は主に3つ、層のリスト、層の結合、ロス関数です。

利用する層のリスト

initに記述している、fc1は一層目の(いまは一層しか作ってないけれど)ニューロン群の記述です。chainer.FunctionSetのinitに渡しているのは、こうすることでoptimizerでパラメータ学習する際によしなにやってくれるからです。 なお、chainerではネットワークは直列でなく、分岐や再帰してもよいので、層という言い方はおかしいかもしれません。関数とか、ニューロン群とか言えばいいでしょうか。いい名前有るのかしら。

層同士の結合

initでは使う層を定義しただけなので、それが相互にどう結合するか記述する必要があります。_forwardに記述しているのがそれです。今回は一層なので微妙ですが、例えば二層あればself.fc2(self.fc1(x))と言った感じです。具体的には後述する二層モデルのコードを見てください。

ロス関数

学習時に教師データとNNの出力の誤差を計算する関数です。error = chainer.functions.softmax_cross_entropy(h, y)の部分ですね。ここでは、分類問題としているので、softmax_cross_entropyを使ってます。回帰だったら例えばユークリッド距離を使うとか、分類でも、クラスごとに重みを変えたいとか、まぁいろいろ考えられると思います。

学習!

## 上のコードのあとに以下を追加。そして実行
 
data_and = [
    [numpy.array([0,0]), numpy.array([0])],
    [numpy.array([0,1]), numpy.array([0])],
    [numpy.array([1,0]), numpy.array([0])],
    [numpy.array([1,1]), numpy.array([1])],
]*1000

for invec, outvec in data_and:
    model.train(invec, outvec)

andのロジックを表すデータをとりあえず4*1000個入れてみます。なお、出力は2次元なんですが、正解データとして入れるのは、クラスのインデックス1次元なことに注意です。

...
x: [[0 0]]
h: [[ 5.30860519 -5.30860519]]
h_class: 0
x: [[0 1]]
h: [[ 2.06165457 -1.59285831]]
h_class: 0
x: [[1 0]]
h: [[ 0.9362464 -2.718467 ]]
h_class: 0
x: [[1 1]]
h: [[-2.31046915  0.9970448 ]]
h_class: 1

最終的にロジックが学習できたと思います。 ここで使ってるlinearについて説明してなかったですが、これはy1 = (w1, w2)*(x1, x2) + b1のw,bを学習するモデルなので、たとえばy_true = x1+x2、y_false = 1.5 とでもすればandの関数になりますね。

xorは学習できない

ここまで1層だけのNNだったので、つまらないなぁという感想だと思います。1層で学習できないロジックを例示してみましょう。先ほどと同じネットワークで以下のロジック、xorを学習してみます。

data_xor = [
    [numpy.array([0,0]), numpy.array([0])],
    [numpy.array([0,1]), numpy.array([1])],
    [numpy.array([1,0]), numpy.array([1])],
    [numpy.array([1,1]), numpy.array([0])],
]*1000
x: [[0 0]]
h: [[-0.00494588  0.00494588]]
h_class: 1
x: [[0 1]]
h: [[ 0.81032435  0.80043193]]
h_class: 0
x: [[1 0]]
h: [[ 0.34743322  0.33754217]]
h_class: 0
x: [[1 1]]
h: [[ 1.1429201   1.15281156]]
h_class: 1

まさかの、真逆のロジックを学習してますね。まぁこれは1層のlinearのロジックでは線形分離可能な問題しか解けないからです。という訳で2層に。

f:id:Hi_king:20150627194119p:plain

class ClassificationModel(chainer.FunctionSet):
    def __init__(self):
        super(ClassificationModel, self).__init__(
            fc1 = chainer.functions.Linear(2, 2),
            fc2 = chainer.functions.Linear(2, 2)
            )
    def _forward(self, x):
        h = self.fc2(chainer.functions.sigmoid(self.fc1(x)))
        return h
        
    def train(self, x_data, y_data):
        x = chainer.Variable(x_data.reshape(1,2).astype(numpy.float32), volatile=False)
        y = chainer.Variable(y_data.astype(numpy.int32), volatile=False)
        h = self._forward(x)

        optimizer.zero_grads()
        error = chainer.functions.softmax_cross_entropy(h, y)
        error.backward()

        print("x: {}".format(x.data))
        print("h: {}".format(h.data))
        print("h_class: {}".format(h.data.argmax()))

model = ClassificationModel()
optimizer = chainer.optimizers.MomentumSGD(lr=0.01, momentum=0.9)
optimizer.setup(model.collect_parameters())

fc2が追加されているのと、fc2に渡す前にシグモイド関数を通して二値化してます。

x: [[0 0]]
h: [[ 1.72677757 -1.22870416]]
h_class: 0
x: [[0 1]]
h: [[-0.93671401  1.36127727]]
h_class: 1
x: [[1 0]]
h: [[-0.9401792   1.36309223]]
h_class: 1
x: [[1 1]]
h: [[ 0.85140373 -0.75365336]]
h_class: 0

やったね。 ※稀に1000サンプルで学習が終わらないことがあるので、その場合はやり直すか、サンプル数増やしてください。

それ、回帰でできるよ

ここまで2クラス識別問題として解いてきましたが、xor関数やand関数は2入力1出力の関数と捉えるのが一般的かと思います。出力を1次元にしても、RMSEを誤差関数として用いた回帰問題にすることで解けます。

class RegressionModel(chainer.FunctionSet):
    def __init__(self):
        super(RegressionModel, self).__init__(
            fc1 = chainer.functions.Linear(2, 2),
            fc2 = chainer.functions.Linear(2, 1)
            )

    def _forward(self, x):
        h = self.fc2(chainer.functions.sigmoid(self.fc1(x)))
        return h
        
    def train(self, x_data, y_data):
        x = chainer.Variable(x_data.reshape(1,2).astype(numpy.float32), volatile=False)
        y = chainer.Variable(y_data.astype(numpy.float32), volatile=False)
        h = self._forward(x)
        optimizer.zero_grads()
        error = chainer.functions.mean_squared_error(h, y)
        error.backward()
        optimizer.update()
        print("x: {}".format(x.data))
        print("h: {}".format(h.data))

model = RegressionModel()
optimizer = chainer.optimizers.MomentumSGD(lr=0.01, momentum=0.9)
optimizer.setup(model.collect_parameters())

fc2の出力が1次元になってるのと、errorがmean_squared_errorになってます。

...
: [[0 0]]
h: [[ 0.0449773]]
x: [[0 1]]
h: [[ 0.93737204]]
x: [[1 0]]
h: [[ 0.93765563]]
x: [[1 1]]
h: [[ 0.09684665]]

出力が連続値になるので閾値入れて2値化する必要はありますけど、ちゃんと解けてますね。

感想

chainerで書くと、ニューラルネット書いてる、って感じになるので教育用にいいですね。CNN使っ他問題も解いてみたので、次回の記事に書きたいと思います。

ソースコード全体

gist.github.com

PFN発のディープラーニングフレームワークchainerで画像分類をするよ(chainerでニューラルネット1)

1日ちょっと前に、PFNから新しいディープラーニングフレームワーク"chainer"が公開されました[1]。触ってみた感じの特徴は、pythonのコードで完結するので、システムに組込みしやすそうで、処理の内容も読みやすい。同時に、処理の内容に興味を持たずに使うには難しいという思いでした。ベースにしてあたらしいツールを作るには最適に感じるので、これから、chainerをベースにした様々な用途のツールができるのが期待されます。

CPU用インストール ~ MNISTのトレーニング

ここでは、chainerのチュートリアル[1]に書いてあるとおりにインストールと初期タスクをおこなうだけです。

インストールは、githubからソースを落としてきてpython setup.py installでも、pipで入れてもいいと思います。とりあえずここではpip

pip install chainer

これでchainerが動くんですが(シンプル!)、とりあえず何かしらの学習をしたいので、チュートリアル通りにMNIST(手書き数字データベース)のトレーニングをします。これはCPUでも一瞬で行けます。

git clone https://github.com/pfnet/chainer.git
python chainer/examples/mnist/train_mnist.py

環境次第では、ダウンロード先ディレクトリの用意ができないことがあるので

スクリプト(chainer/examples/mnist/train_mnist.py)のこの行を
mnist = fetch_mldata('MNIST original')

こう変える(好きなディレクトリのパスでいいです)
mnist = fetch_mldata('MNIST original', data_home='/tmp')

あとはerrorが下がっていくのを眺めましょう(^^)

画像分類問題を解く

まぁMNISTが解けるのはいいです、でもcaffeとかをすでに触ってるみなさんは、画像分類問題とかを簡単に解きたいですよね。幸いchainerのexamplesにはimagenet[2]の分類問題を解くために先人たちが作ったネットワークがexampleとして付属してます(python ~/chainer/examples/imagenet/)。ただし、caffeの用にトレーニング済みのものがダウンロードして来られるわけではないので自分でデータを集めてトレーニングする必要があります。

データセット作る

本来なら、面白いデータ(アニメキャラの分類[3]とかAKBの分類[4]とか)で作りたいんですが、ファインチューニングならまだしも、1からトレーニングをするとなるとそれなりの量のデータが必要で正直面倒です。今回は、caffeにお手軽ダウンロードスクリプトがついてる[5]、flickr sytle datasetを使いましょう。

caffeの付属スクリプトを使って以下のように集めます。とりあえず80000サンプル使います

git clone https://github.com/BVLC/caffe.git
python caffe/examples/finetune_flickr_style/assemble_data.py --workers=-1 --images=80000 --seed 831486

これで

  • caffe/data/flickr_style/images
  • caffe/data/flickr_style/train.txt
  • caffe/data/flickr_style/test.txt

の3点セットが揃いました。なお、train.txtやtest.txtは画像ファイルパス、クラス名をスペースで区切ったファイルなので、これを人力で頑張って用意すれば、以下の手順で他の問題も解けます。

さて、chainerのexampleで用意されているスクリプトを動かすためには、まだ少し準備が必要です。まず、このデータはサイズが正規化されていないので、256x256の画像にする必要があります。どのようなやり方でやってもいいんですが、僕は、拡大縮小なしで、複製して連結+256切り取りでやりました(https://gist.github.com/Hi-king/1c7f86e0fa628c41eb3c)。

mv caffe/data/flickr_style/images caffe/data/flickr_style/images_raw
python crop.py caffe/data/flickr_style/images_raw caffe/data/flickr_style/images
# train.txtには、絶対パスが書かれているので、必ずcrop後の出力をimagesディレクトリに入れる必要がある

さらに、用意したデータセットの平均画像を作っておく必要があります。これはchainerのスクリプトが用意されてます。

cd caffe/data/flickr_style
python ${your_path}/chainer/examples/imagenet/compute_mean.py train.txt
# mean.npyというファイルが生成されます

GPUを有効化する

全ての用意が出来ました。これでトレーニングが始められます!が、規模の大きい学習をCPUだけでやるのは心もとないです。せっかくなのでGPUでやりましょう。検証してないですが、たぶん10~20倍の時間かければCPUでもできると思うので、お試しだけならそちらでもいいかもしれません。

依存するライブラリ(pycuda, scikit.cudaなど)がインストールされてればすぐに使い始められます。僕は用意してなかったので、ここでインストールします。また、setup.py install でchainerをインストールしていれば一緒に入るのかもしれません。

[追記] chainer-cuda-depsがリリースされたので、今後はそちらを使いましょう

pip install chainer-cuda-deps
# こちらは古い方法
pip install -r chainer/chainer/requirements/cuda-requirements.txt

これで、chainerが必要とするcuda関連pythonライブラリが入ります

学習!

cd caffe/data/flickr_style
python ${your_path}/chainer/examples/imagenet/train_imagenet.py -g 0 train.txt test.txt 2>&1 | tee log
# CPUでやる場合は-g -1
# ネットワーク構造も複数選べる(GoogLeNetとか)けれど、デフォルトのNINをつかってる

これで学習できるんですが、このスクリプト、デフォルトではかなり大規模な学習用に作られてるので、学習の進捗が全然出てこなくて寂しいです。もっとおしゃべりなスクリプトに修正しましょう。

# chainer/examples/imagenet/train_imagenet.py
            train_cur_loss += loss
            train_cur_accuracy += accuracy
            if train_count % 10 == 0: # ここ修正
                mean_loss  = train_cur_loss / 10 # ここ修正
                mean_error = 1 - train_cur_accuracy / 10 # ここ修正
                print >> sys.stderr, ''
                print json.dumps({'type': 'train', 'iteration': train_count,
                                  'error': mean_error, 'loss': mean_loss})

ログを吐く周期を上のよう短くすることで楽しく進捗が見られます。

さて、あとはログをグラフにして、進捗を見ましょう。とりあえずpythonスクリプト作りました(https://gist.github.com/Hi-king/1c7f86e0fa628c41eb3c)

less log|grep iteration > log_pretty
python plot.py log_pretty

結果は以下

f:id:Hi_king:20150611035412p:plain

この問題は、20クラス識別なので、チャンスレート(当てずっぽう)はだいたい95%のエラー。 "キュビズム"、"ロマンティック"、"バロック"という、人間でもよくわかんない抽象的なクラスを当てる問題なので[6]、これでもなかなか学習進んでる方かと思われます。論文中でも3割強の正解率(7割弱のerror)となっているので妥当かと思います。 まぁこれはクローズドテスト(学習に使ってるデータでのエラー)なので、ちゃんとした評価としては全然不足です。

ちなみに、5000イテレーションで、amazonEC2のg2.2xlargeで50分程度でした。CPUでも2,3日頑張れば行けそうですね。

chainerはcaffeよりも回帰とかRNNとかがやりやすいのが利点なので、次は識別じゃない問題を解いてみたいものです。

ニコニコの動画を識別するためのマルチモーダル特徴(ニコニコ動画の統計的機械学習1)

"ニコニコ動画の統計的機械学習"シリーズとして、ニコニコ動画のデータを使った解析を幾つか書いてみます。 結構前にやった、以下の発表の内容をブログ化するってのも含めて。

ニコニコ動画のコメント解析

このスライドのトピックは

  1. ごちうさ難民はどこにいったか
  2. コメントによる動画要約
  3. 動画を表現するマルチモーダル特徴量の精度検証

なんですが、まずこの記事では3番目の、特徴量の精度検証に触れます。

概要

ニコニコ動画の体験は、音声、画像、そしてなんといってもコメント、と マルチモーダルな体験ですが、それらの情報を使ってニコニコ動画を解析するにあたって、どういう入力特徴を使うことができるか、そしてどの特徴が有用か、を検証しました。

結果は、"コメント >>> 超えられない壁 >>> 画像 > 音声"でした。

また、特徴量を結合することで、ニコニコ動画のカテゴリ(12クラス)の識別問題を6割以上の正答率で識別できました。

実験の目的

f:id:Hi_king:20150430203646p:plain

ある動画の特徴を入れて、ランダムフォレスト識別器によってニコニコ動画上のカテゴリ(アニメ、スポーツ、歌ってみた etc...)を当てる問題を解きます。 様々な特徴量を入力として比較し、どの特徴が効くのかを調査します。

各特徴量のはなし

と、メディア分析でおそらく三大分野だと思われる特徴をそれぞれ考えて、全部を結合する

画像

まず、入力としてに使うのは、サムネイル画像です。 ここから取り出して使った特徴量は

  • GIST特徴量
  • CNNの中間層

の2種類。 カテゴリという抽象的な特徴を取るために、写真の分類によく使われるGIST特徴量[1]。 そして、ディープラーニングの流れにはとりあえず乗っておこうということでCNN。ただし、自前で学習するにはデータ集めるのも大変だったので、caffeのサンプル[2]に入っている、ILSVRC2012で優勝したモデルの、中間層(第6層)を入力特徴量として利用してランダムフォレストにかけています。中間層を利用しているのは、今回識別したい"ニコニコ動画のカテゴリ"というものが、もとの物体認識タスクと多少異なるからです。

f:id:Hi_king:20150430211905p:plain

混同行列(どのカテゴリをどのカテゴリと間違えてしまったか)は以下になります。 明るいところが、誤認識が多いカテゴリなんですが、(歌ってみた, ボカロ) = 全く同じサムネが多い、(アニメ, アイドルマスター) = アニメ絵、(車載, 旅行) = 風景画像、など、納得の間違いが多いです。そして、改めてみてみると、ニコニコのカテゴリって、似通ってるものも多いなぁと。

f:id:Hi_king:20150430222014p:plain

音声

動画から抽出した音声を入力として、2種類の特徴量を作りました。

  • パワーの統計量
  • 動画中で最大パワーの時刻の瞬時周波数特徴量

カテゴリを特徴付ける音声ってなんだろうと思って、動画毎のパワーをプロットしてみると、以下のように、綺麗に編集された動画と、無編集に近い動画ではパワーの動きに特徴があることがわかりました。 具体的には、編集によって無音部分が少なく、かつ、振れ幅も小さくなる傾向があります。そこで、毎秒のパワーを計算して、動画全体について、その(平均、最大、最小、標準偏差)をとって、4次元の入力としました。

f:id:Hi_king:20150430212154p:plain

また、音声の分析をするなら周波数特徴は必須だろうということで、動画中でパワーが最大となる瞬間の、周波数バンド毎のパワースペクトルを適度なバンド幅で離散化し、64次元にしたものも使いました。

それぞれの結果が以下。

f:id:Hi_king:20150430213404p:plain

混同行列は以下。画像のほうよりも得意不得意がはっきりしています。音楽系か人の声か、等はわけられているけど、歌ってみた、ボカロ、ニコニコインディーズはわけられないみたいです。

f:id:Hi_king:20150430221908p:plain

コメント

ニコニコらしい特徴量として、やはりコメントの文字情報には触れないわけにはいきませんね。

用いた特徴量は割とシンプルに

  • 繰り返しやユニコードの正規化をかけたあとのコメントそのものを次元とする、動画中のコメント出現頻度
  • 文字1gramの出現頻度
  • 文字1gram+文字2gramの出現頻度

の3種類です。 コメントはかなり短い文字列で、かつ、かなり通常の語彙と異なるため、形態素解析して語の出現頻度をはかるよりは、文字Ngramを扱ったほうがマシかとおもい、このようにしています。最近有用そうな辞書[3]がでてきたり、RNNを用いた文書解析が流行ってるので、そのへんを使って今度もうちょい掘り下げもやってみたいな、とは思います。

f:id:Hi_king:20150430215957p:plain

f:id:Hi_king:20150430215557p:plain

マルチモーダルに結合してみた

(画像+音声+コメント)を同時に使う話。最強の特徴量になるんじゃね?って思いでやったんですが、結局コメントの文字2gramの力が圧倒的でした。

f:id:Hi_king:20150430220225p:plain

f:id:Hi_king:20150430220235p:plain

混同行列は以下です。

f:id:Hi_king:20150430222856p:plain

コメントが少ない動画だったらいけるんじゃね?という実験でも、

  • コメント10以下の動画でもそれなりの精度がでる
  • コメント300も行けば精度はサチる

f:id:Hi_king:20150430220256p:plain

という結果がわかり、なかなかコメント特徴の精度を超えることは難しそうに感じました。 コメントの中には大した情報量のないものも多いことを考えると、コメント数10以下でも分類できてしまう、というのは結構面白い話な気がします。

参考文献

[1] Spatial envelope [2] http://nbviewer.ipython.org/github/BVLC/caffe/blob/master/examples/filter_visualization.ipynb [3] http://diary.overlasting.net/2015-03-13-1.html