まっそのせかい

やった事のメモとか

これまで作ったものを振り返る -いわゆるポエム-

この記事はCCS †裏† Advent Calendar 2018の2日目の記事です。
昨日の飯テロ(実行犯:kakira)

CCS4年のまっそうめんです。
気付けばもう4年生で、もうすぐ卒業して消えゆく存在です。
卒業論文が提出できればの話
これまで自分は何をしてきたのか、何を学び、何を得る事ができたのだろうか。
年を取ったせいか、ふとそんな風に考える事があります。

そんな訳で、今回は「これまで作ったもの」をなんとなく振り返って懐かしさに浸ろうかなと思います。
少し前に某先輩が過去に作ったゲームを紹介してるのを見てやりたくなっただけ
CCSの作品集に出したり大学祭の展示に出したものを中心に、思い出せる範囲で書き連ねます。

以下、基本的に作品紹介という名のポエムで有用な情報の類は無い気がするので、興味の無い人はそっ閉じ。
むしろ興味ある人がいるのか…?

nightmare

  • 制作時期:1年夏
  • 使用言語:C

f:id:massoumen:20181114023234p:plain

大学に入ってプログラミングを始めて最初に作った記念すべき作品。
コンソール上で動くプログラムで、C言語講座発表会に出しました。
このゲーム実は必勝法があって、逆にそれ以外では勝てない(COMが絶対勝つ)ようになっています。
(暇な人は考えてみて下さい)

FullFrontal

  • 制作時期:1年秋
  • 使用言語:C(DXライブラリ)

f:id:massoumen:20181117233431p:plain

全てはここから始まった…(?)
ゲーム制作に挑戦するにあたり、まずは小規模のものを作る事が大事らしいです。
それを遵守した結果、友達がこちらを見ている間スペースキーを押して服を着るゲームが生まれました。(?)
見られてない間は全裸です。これ以降サークルでは全裸ゲーの人という扱いになった気がします。
大学祭で展示したら遊びに来たょぅι゛ょがこのゲームをプレイしてる光景を見てしまい、死にたくなりました。
(大学祭に展示するゲームの内容はよく吟味した方がいいと思いました)

エンシェントジャガイモ

  • 制作時期:1年冬
  • 使用言語:C++(DXライブラリ)

f:id:massoumen:20181117233452p:plain

C++の練習がてらシューティングゲームの制作にチャレンジしたもの。
std::vectorがやたら便利だったり、画像の読み込みミスってメモリがやばいことになったり、追尾弾の処理って面倒なんだなぁ…って思ったりいろいろあったけど、その辺も含めプログラム作るの楽しいなぁとか思いながらやっていた気がする。
ゲームを作るにあたり、素材の確保に困っていた時にエンシェントジャガイモっていう謎のスレを見つけてしまい、「そうだ、じゃがいもの画像を使えばいいんだ」って謎の思考の結果ビジュアルがこうなった。

あおてんじょう

  • 制作時期:2年春
  • 使用言語:C++(DXライブラリ)

f:id:massoumen:20181114222003p:plain

前回のシューティングのビジュアルが完全にネタなので、今度はまともなビジュアルにしようと思い引き続きシューティングゲームを制作。
プログラムは前作のものを流用し気になる箇所を直ししたりシステムを少し変えただけなので、全体像がどことなく前作と同じ。
1枚画像を用意して表示するだけだと敵がスィーと平行移動する(伝われ)何とも言えない感じになるので、2枚用意してちょっと動きを足すなどしてそれっぽさを出そうとしていた。
敵1号(名前はまだ無い)が地味にお気に入り。

f:id:massoumen:20181114220928g:plain

新歓ランチャー

  • 制作時期:2年春
  • 使用言語:C#(DXライブラリ)

f:id:massoumen:20181115011821p:plain

新歓の展示用に作った。
最初C++で作ろうとしたんだけどなんか上手くいかなくて途方に暮れていた。
先輩がC#で作れるって教えてくれて、それでなんとか新歓までに間に合ったもの。
唐突に作ってって仕事丸投げされて、作り方とかの情報とか一切無くて結構頭を抱えていたのは今となっては良い思い出(?)である。
ランチャーの作り方とかデータとか、情報の類は頑張って後輩に託そうと思った瞬間だった。
(この後C++で作った奴は以前のアドカレで一部記事にしたりした)
制作そのものは結構楽しいので、なんか作りたいけど題材がない人とかおすすめかもしれない。

FullFrontalRevolution -全裸再臨-

  • 制作時期:2年夏
  • 使用言語:C++(Siv3D)

f:id:massoumen:20181117152340p:plain

全裸ゲー第2弾です。(爆)
今回は友達に見つからないように全裸で踊ります。
コンボが繋がるとフィーバーして友達も脱ぎます。(爆)

f:id:massoumen:20181117152400p:plain

綺麗なシーン遷移や図形の当たり判定取得などライブラリの力を存分に活用していたり、キャラ絵を描いてもらったおかげかやたらハイクオリティ(当社比)なバカゲーになってしまった。
あとゲーム内には組み込まなかったけど送られてきたキャラ絵の中にKBTITって名前のファイルが混ざってて爆笑していた。

なぜかこれまで作ったものの中でトップクラスに気に入っている。
使っていたデスクトップPCが逝ったので開発データが消滅してて、ちょっと悲しんでいる。
(完成品は別の所にもたまたま保存していて生存)

Sara Washer

  • 制作時期:2年夏
  • 使用言語:C++(Siv3D)

f:id:massoumen:20181117154533p:plain

コミケ作品集の締め切り直前で「もう1本なんか作ろうぜ!」っていう例の勢いでプログラムを担当し、1日だか2日くらいで極力バグを出さないように勢いでコードを書きまくった気がする謎ゲー。
基本的には流れてくる皿を洗い拭き取るという労働の基本を学ぶゲームなのだが、処理が追い付かないと判断するや皿を粉砕して証拠を隠滅する狂気のムーブを行えるのが特徴。
ポエム書いてて思ったけどこいつろくなもん作ってねぇな

大学祭ランチャー

  • 制作時期:2年秋
  • 使用言語:C++(Siv3D)

f:id:massoumen:20181117175513p:plain

大学祭が秋にあるので、安直だけど読書の秋という事で本を題材にしたデザインで作ったランチャー。
ページをめくるアニメーションを作ろうと思ったけど、手書きでそれを表現する画力はないのであれこれ考えた結果、何故かBlenderという3DCGソフトを使って本とページめくりのアニメーションを作ろうという結論に至った。(?)

f:id:massoumen:20181117174959p:plain

制作期間は1ヶ月無いくらいで、その内2,3週間はBlenderでの本とアニメーション制作に費やされている。(プログラムは約3日)
Blenderを使った事がなかったので手探り感あって楽しかったけど結構疲れた。(操作とか慣れなかったな…)

旅するナイト

  • 制作時期:2年秋
  • 使用言語:C++(Siv3D)

f:id:massoumen:20181117180306p:plain

夏休みは競技プログラミングに時間を注ぎ、後期が始まったらランチャーを作っていたのでゲームを作る時間が無い中、何か軽く作りたくなったので短時間でサクッと作ったミニゲーム
元ネタはKnight's Tour(騎士の巡歴)と呼ばれるパズルです。
昔に比べサクッと作れる度合いの高まりを感じ、プログラミング始めた頃に比べだいぶ手馴れてきたんじゃないかとか思ったり思わなかったり。

朧火

  • 制作時期:2年冬
  • 使用言語:C++(Siv3D)

f:id:massoumen:20181126215415p:plain

複数人で組んでローグライクゲームを作ろうと思ったが、残念ながら制作が止まってしまったもの。
チーム制作ってめっちゃ難しいんだなって思いました。(小並感)
幽霊とか妖怪とか、その手の類が出るタイプのゲームになる予定でした。
僕はプログラム担当だったんですが、ターン制でプレイヤーの動作に合わせて敵の移動方向や攻撃を決定したり、ローグライク特有の視界設定やマップ描画など、これまで作ってきたものとは段違いにプログラムの錬成が難しくてつらぽよだった(白目
敵の移動処理のために幅優先探索を用いたりと、競技プログラミングで学んだ知識が役に立って感動するなどしていた。
ローグライク系のゲーム、機会があれば再度挑戦したいと思ったり思わなかったり。

Fantasia

  • 制作時期:3年夏
  • 使用言語:C#(Unity)

f:id:massoumen:20181118001635p:plain

3Dのゲーム制作に挑戦したくてUnityの本読んでいろいろググりながら作ったアクションゲーム。
一部のエフェクトはEffekseerというエフェクト制作ツールを用いて作成してみた。
DXライブラリやSiv3Dでのゲーム制作とは結構勝手が違って慣れるまで時間がかかったけど、結構新鮮で楽しかった。
あとユニティちゃんがかわいい

この世の終わりみたいなゲーム

  • 制作時期:3年夏
  • 使用言語:C++(Siv3D)

f:id:massoumen:20181118004713j:plain

危なさがMAXなバカゲー
ゲームシステム自体は全裸ゲーに近く、ウェイにバレないように酒を捨てるゲームという内容となっている。
オタクたちが自宅で録音した「ウェーイwwwww」や某インスタの音声に似た何か(加工済み)や当時Twitterでよく見かけたバジリスクタイム風のアニメーションなど、改めて見るといろいろ酷い。

卯月識別器

  • 制作時期:3年冬
  • 使用言語:Python(Keras)

どうも流行っているらしい(?)DeepLearningをやってみたくて、画像を入力したらそれが卯月かどうかを判定するプログラムの作成に挑戦した。
データを集めるためにアニメを再履修し、卯月を中心とする各キャラクターの顔が映ったタイミングでスクショを撮る苦痛極まりない作業が必要で、推しにかける情熱が試されているなと。
年末、弟が大量に友達を連れ込んでリビングで盛り上がってる中、僕は画面に向かって黙々と改良をしていた。
黒魔術的パラメータ調整(手動でのごり押し)、精度向上の祈りを捧げるなど数々の行いによりそれっぽい(?)結果になってとりあえず満足した。

声質変換(toみくにゃんボイス)

  • 制作時期:3年冬
  • 使用言語:Python(Keras)

f:id:massoumen:20181122175611p:plain

卯月識別器で画像を扱ったので、次は音声かなと軽い気持ちで手を出して絶望した奴。
今回用いた手法では同じ内容を喋っているデータが必要なんですが、これが何を意味するかというとみくにゃんと同じセリフを喋って録音する必要があるんですよね。
自分の事をみくにゃんだと勘違いしている精神異常者

音声処理自体もよく分からんし、かなりキツいチャレンジだなと始めて少し経って気付いてしまった。
結局クオリティも微妙で、DeepLearningが全てを解決してくれる訳じゃないんだなと思ったり思わなかったり。
(いろいろなモデルが次から次へと提案されているので、その辺試したらもう少しマシな結果になるのかもしれないが…)

卯月識別器と声質変換以外にも、画像のスタイル変換だったりGANを用いたキャラの生成や変換を試したりもした。
個人的にはスタイル変換が一番面白かった気がする。

f:id:massoumen:20181122153633p:plain

Secret Code

  • 制作時期:4年夏
  • 使用言語:C++(Siv3D)

f:id:massoumen:20181122183121p:plain

最後はまともなゲームで締めようと思い、コツコツ作っていたタワーディフェンスゲーム。
本当は夏コミの作品集に出す予定だったのだが、研究とシージに明け暮れていたせいで間に合わなかったので大学祭の展示で出す事に。
大学祭では割と好評だったらしく、作ったかいがあったなぁと。(ウレチイ・・・ウレチイ・・・)
ただ、ゲームの性質上1プレイの時間が長くなってしまい混雑に貢献していたらしく、現場の人たちへの申し訳なさで心の中で土下座してた。
少し手直しして冬コミの作品集にも出す予定。

ポエム書くために過去作品引っ張り出したりいろいろ思い出したりしてたら懐かしさやら何やらで予想以上に死んでしまった…
明日の記事の担当は…あれ、誰もいない…
とっちーさんがシージの記事を書いたようです。

yoooomaruuuu.hatenablog.com

CycleGANで推しキャラを相互に変換してみた

はじめに

カードキャプターさくらクリアカード編が終わってしまい絶望したので、GANを使ってさくらちゃんの生成を試みようと思いました。
GANにはいろいろなものが提案されていますが、調査をしているとCycleGAN(論文)という面白そうなGANがある事を知りました。CycleGANでは2種類の画像A,Bを互いに変換する事ができるそうです。そこで今回はCycleGANを使って推しキャラであるさくらちゃん(カードキャプターさくら)と島村卯月(アイドルマスターシンデレラガールズ)の2名を相互に変換する事を試みます。
単にさくらちゃんを生成するだけなら他のGANでも良いと思われますが、単純にCycleGANを試してみたいというのもあり、何より推し2名を相互に変換するという罪深さ(?)に魅入られました。
(両陣営からボコボコにされそう)

今回試した結果、以下のような画像が得られました。

  • 卯月→さくら

f:id:massoumen:20180701191305p:plainf:id:massoumen:20180701191319p:plain

  • さくら→卯月

f:id:massoumen:20180701191645p:plainf:id:massoumen:20180701191648p:plain

良さげなものを載せましたが、全部ここまで綺麗という訳ではないですね…
実際は細部が崩れてたり、見るのがつらいものも多々…(

CycleGANとは

基本的なGANではGenerator(生成器)とDiscriminator(判別器)を1つずつ用意しますが、CycleGANでは2種類のデータA,Bに対しGeneratorとDiscriminatorを2つずつ用意します。 (ここではAがさくらちゃんの顔画像、Bが卯月の顔画像となります)

各GeneratorとDiscriminatorは以下の処理を行います。

  • G_{A→B}:Aを入力しBを生成
  • G_{B→A}:Bを入力しAを生成
  • D_A:本物のAとG_{B→A}で生成されたA(偽物)を分類
  • D_B:本物のBとG_{A→B}で生成されたB(偽物)を分類

CycleGANでは各{G,D}に対し基本的なGANで用いるAdversarial Loss({L_{GAN}})だけでなく、Cycle Consistency Loss({L_{cyc}})という損失を用いているのが大きな特徴になります。
L_{cyc}ではG_{A→B}にAを入力して得られた出力BをG_{B→A}に入れ、最終的に得られた出力A'が元の入力Aとどのくらい近いか(画素値の差)を求めます。Bについても同様です。
L_{cyc}を用いる事で各Generatorに元の入力に戻しやすいような変換をGeneratorに行わせる事ができます。ここではキャラの顔画像を用いるので、顔の向きなどを保った変換を期待できます。

従来提案されているような画像の変換(pix2pixなど)では変換前と変換後のペアを用意して学習する必要があったのが、L_{cyc}を導入する事によりペアである必要がなくなるというのがCycleGANの大きな利点です。今回行うようなキャラクターの変換をpix2pixなどで行う場合、さくらちゃんと卯月の画像のペアが同じ位置や向きをしている必要がありデータを用意する事が難しいです。一方CycleGANでは画像のペアを用意する必要が無く、単にさくらちゃんと卯月の画像がある程度確保できれば学習する事ができます。

L_{cyc}ではA→B→Aと変換した時に元に戻る事ができれば良いので、必ずしも同じ顔の向きで変換が行われる保証は無いと思われます。(直感的には同じ向きや表情で変換する方が戻しやすいとは思いますが…)
この辺りが少し怪しくはありますが、少なくとも論文の画像や今回試した結果を見る感じでは割と上手くいってるので良いという事にします。

今回、コードはここを参考にしました。
Generatorではストライド2の畳み込みでサイズを小さくした後Residual Block(入力を最後に足す事で残差を学習する奴)を用い、最後に転置畳み込みでサイズを元に戻し出力します。
また、Discriminatorでは通常のCNNを利用します。
(論文だとPatchGANを利用していると書いてあった気がしますが、どうなんでしょうね…)

顔画像に関しては以前卯月識別器を作った時のように、アニメをキャプチャしたものから顔部分を切り取って利用しました。
枚数は卯月、さくらちゃん共に約700枚くらいです。

おわりに

変換結果をざっと見た感じ、ある程度は変換できていますがどれも高品質とは言い難かったです。
モデルの改良とかパラメータ調整とかしたらいい感じになるんですかね…
(学習に時間がかかるのであまりパラメータ変化による出力の変化などを見れてません)
とは言うものの、機械学習での変換には普通1対1対応の画像を用意する必要があるので、その必要が無いというのはすごいなと思いました。
CycleGANを多対多にしたStarGANという変換モデルも提案されているそうなので、そのあたりも試してみたいですね。

CycleGANで推しの変換テスト

CycleGANで推し2名(さくらちゃん、島村卯月)の変換を試したのでメモ。
とりあえずサイズは64*64でテスト。
(サイズが小さいので参考コードのDとGの一部層を削除)
うまくいくと以下のような結果になった。

  • 卯月→さくら

f:id:massoumen:20180624025755p:plainf:id:massoumen:20180624025820p:plain

  • さくら→卯月

f:id:massoumen:20180624030021j:plainf:id:massoumen:20180624030024j:plain

微妙な結果のものもあるが、それでも1対1対応でないものからそれらしいものを作れるのはすごいなぁと。
パラメータの調整とかモデルの改良とかしたらさらに綺麗なものができるのだろうか。

Webアプリケーション制作

サークルの先輩がWebアプリケーション制作の資料をアップしていたので、試しにやってみました。
利用する言語はPythonで、最近機械学習とかで遊ぶのに使っていたのでちょうど良いですね。

qiita.com

一通り読んで、試しになんか作ってみようという事で、せっかくなので以前作った卯月識別器をブラウザ上で試せるようなものを作ってみました。

\デデーン/

f:id:massoumen:20180329215512p:plain

はい。見た目の調節とかほぼ何もしていないので雑さが目立ちますね。
いくつかサンプルとして画像を見る事ができるようなってます。
(無駄に)データベース使って画像のパスを管理してみたり。

f:id:massoumen:20180329215724p:plain

手持ちの画像を与えて識別させる事もできます。こっちは識別のために裏でCNNによる計算処理が行われています。

f:id:massoumen:20180329215934p:plain

GPUのせいなのか何なのか良く分かりませんがたまにPythonが死にます。
問題だらけのWebアプリですね。

f:id:massoumen:20180329220150j:plain

ブラウザ上であれこれポチポチするとちゃんと反応するの、なかなか面白いですね。
せっかくなので試しに公開とかしてみたいんですが、どうすれば公開できるんでしょう。
何も分かっていません。あとできればお金かけたくない

声質変換に挑戦しました

はじめに

機械学習を用いて以前キャラクターの識別について遊んだので、次は画像以外で何か遊んでみたいなーって思い、「そうだ、声質変換に挑戦しよう」と思い立ったので挑戦しました。
地獄を見る事になるとは思いもよらなかったのであった…

声質変換とは、ある人の声を他の人の声に変換する事を言います。
これが上手くいった場合自分の声を好きな人の声にする事ができます。 夢が広がりまくりですね。 現実は非情であった

ちょっと調べてみた感じ声質変換にはいくつか種類があり、特定の人の声から特定の人へ変換する一対一変換、任意の声から特定の人の声へ変換する多対一変換、その逆の一対多、任意の声から任意の声へ変換する多対多があるようです。今回は一番シンプルそうな一対一変換に挑戦します。

今回の目標

さて、変換する声は当然自分の声になる訳ですが、誰の声に変換するかをまずは決めます。
突然ですが私、みくにゃんが大好きなんですよ。

f:id:massoumen:20180319205921p:plain

みくにゃんというのはアイドルマスターシンデレラガールズに登場する猫系アイドルで、アニメにも登場しています。
猫系アイドルなのに魚が嫌いというすさまじい性質の持ち主だったりします。あとおっぱい

今回は機械学習を用いて自分の声をみくにゃんの声に変換するプログラムを作成するのを目標にしようと思います。

結果

先に結果を載せようと思います。まずは「アイドルマスターシンデレラガールズ」と喋ったものの変換結果です。

目標(みくにゃんの声)

変換前(僕の声)

変換結果

みくにゃんに近づいてるとは思いますが、何とも言えない微妙感ですね。機械感も強いです。
激甘評価をするならまぁまぁみくにゃんです。(自分に甘い)
とは言うものの、このデータの組は機械学習の際に用いたものなので、イイ感じに変換できて当然と言えば当然です。(イイ感じ…?)
本当に大事なのは学習データに含まれていないものがどう変換されるかという所だと思うので、その結果も載せようと思います。

1つ目は「みくはお魚が大好きだにゃ」と喋ったものの変換結果です。単語単位では学習データに存在すると思いますが、1文としては存在していません。
みくにゃんは魚が苦手です。

変換結果

すごい力の抜けた感じや機械感がしますが、一応音声としては聞き取れる…?それぞれの単語は学習データに含まれているせいか、そこまで酷い結果には感じません。
(学習データすら結果が微妙じゃないかと言えばその通りなのですが…)

次は「島村卯月、頑張ります」と喋ったものの変換結果です。先ほどのものとは異なり単語単位ですら殆ど存在しないと思います。
(みくにゃんが「島村卯月」と喋っているデータを用いた記憶は無いです)

変換結果

思ったより酷くはないし音声としては(多分…?)認識できますが、それでもやはり先ほど同様すごい力の抜けた感じや機械感はします。

さて、いくつか結果を載せたところで微妙感が漂いまくりな訳ですが、私も微妙だと思います。
いろいろ試行錯誤した結果、出力が音声だと認識できるくらいにはなりましたが、どうしても微妙感が拭えません。
正直これでも結構マシな結果で、音声とは言えないような謎の出力を多数観測したりしています。
最近ニコニコ動画に投稿された結月ゆかりの声になってみた人のものの方が遥かにクオリティが高いと思います。

www.nicovideo.jp

僕ももっとクオリティアップを図りたいところですが、だいぶ力尽きてきたので一段落と言った感じでこの記事を書いています。

手順の概要

今回は以下の手順で声質変換を行いました。

  1. 同じ内容を喋っているデータを作成
  2. メルケプストラムの抽出
  3. 機械学習を用いてメルケプストラム変換用のパラメータを推定
  4. 推定結果を用いてメルケプストラムを変換し音声を得る

残念ながらそれぞれの細かい処理について、完璧に把握してるとは言えず雰囲気でやってたりします。
Pythonと各種ライブラリに任せまくっています。

データの作成

今回の手法ではまず自分の声とみくにゃんの声データについて、同じ内容を喋っているもの(パラレルデータ)が必要になります。
みくにゃんのデータはセリフ集とかから持ってくるので大した問題ではありません。
問題は自分の声データです。みくにゃんと同じセリフを喋って録音する必要があります。

なので録音します。(迫真

これが…ほんとですね…気が狂いそうになるといいますかね、もうあれですよ。二度とやりたくないです。
オタクがひたすらみくにゃんのセリフを喋ってる光景、相当キツいです。
自分の事をみくにゃんだと勘違いしている精神異常者

声優ってすごいなって改めて思いました。

メルケプストラムの抽出

なんとか録音を終えたら音声データから特徴量としてメルケプストラムを抽出します。
メルケプストラム以外にも特徴量がいろいろあったりしますが、どれが良いんでしょうね。(正直良く分かりません
メルケプストラムを抽出するにあたり以下の記事の内容を参考にしました。

aidiary.hatenablog.com

wavファイルはあらかじめ16kHz・モノラルにしておきます。
メルケプストラムの抽出は上のサイトのものをそのまま利用させていただきました。

自分の声とみくにゃんの声データから得られたメルケプストラムの内、1次元目は以下のようになっています。

f:id:massoumen:20180320031829p:plain

同じ内容(アイドルマスターシンデレラガールズ)を喋っているのですが、かなり値が異なっています。
また、今後の処理のために時間方向のデータ数を揃えておきます。以下の記事の内容を参考にしました。

aidiary.hatenablog.com

時間方向に揃えると以下のようになりました。
割と合ってる箇所とそうでない箇所が見られます。

f:id:massoumen:20180320031840p:plain

機械学習

データを用意したらモデルを構築し学習を行います。ライブラリはKerasを利用しました。そこまで複雑でないモデルならシンプルに書けて好きです。
画像を扱った時はCNNを用いましたが、今回は時系列データという事でRNN(Bidirectional LSTM)を用いてみたいと思います。
自分のメルケプストラムの時刻tからt+aの値を入力として与え、みくにゃんのメルケプストラムの時刻tの値を出力するように学習します。
また、メルケプストラムは最初の次元にパワーの値が入り26次元のベクトルになっているそうですが、それを無視した方が良さげな結果になったので、今回の場合は入力が(a, 25)の行列、出力が25次元のベクトルになります。
また、時刻0からa、1からa+1、2からa+2、…とスライドさせながら入出力の組を作成します。入力データについて、スライドさせていくと終わりの部分が足りなくなるので、足りない部分は0で埋めるようにしています。
また、各次元ごとに平均0、分散1になるように正規化を行いました。平均を引いて標準偏差で割るアレです。
学習用のソースコードは以下のようになりました。

from keras.models import Sequential
from keras.layers import CuDNNLSTM, Dense, Bidirectional
from sklearn.model_selection import train_test_split
import numpy as np
import glob
import gc
import tensorflow as tf
from keras.backend import tensorflow_backend

config = tf.ConfigProto(gpu_options=tf.GPUOptions(allow_growth=True))
session = tf.Session(config=config)
tensorflow_backend.set_session(session)

max_t = 40
input_dim = 25 # 入力の次元
hidden_dim = 100 # 隠れ層の次元
output_dim = 25 # 出力の次元

# 1フレームずつずらしながら(data_num, max_t, input_dim)のデータを生成
def read_input(mcep_dir, mean, std):
    data = []
    mcep_list = glob.glob(mcep_dir + "/*")

    for mcep_path in mcep_list:
        print(mcep_path)

        mcep = np.loadtxt(mcep_path)[:, 1:] # パワーを無視

        mcep = (mcep - mean) / std

        mcep = np.r_[mcep, np.zeros((max_t - 1, input_dim))]

        # ずらしながらデータを追加(max_tに満たない場合は0を付け足して追加)
        if mcep.shape[0] >= max_t:
            for i in range(mcep.shape[0] - max_t + 1):
                data.append(mcep[i:i + max_t, :])
        else:
            zero = np.zeros((max_t - mcep.shape[0], mcep.shape[1]))
            mcep = np.r_[mcep, zero]

            data.append(mcep)

    return np.array(data).astype("float32")

# 時刻t,t+max_tのデータから時刻tの変換結果を推定する用のデータを生成
def read_teacher(mcep_dir, mean, std):
    data = []
    mcep_list = glob.glob(mcep_dir + "/*")

    for mcep_path in mcep_list:
        print(mcep_path)

        mcep = np.loadtxt(mcep_path)[:, 1:] # パワーを無視

        mcep = (mcep - mean) / std

        data.extend(mcep)

    return np.array(data).astype("float32")

def read_mcep(mcep_dir):
    data = []
    mcep_list = glob.glob(mcep_dir + "/*")

    for mcep_path in mcep_list:
        print(mcep_path)

        mcep = np.loadtxt(mcep_path)[:, 1:] # パワーを無視
        data.extend(mcep)

    return np.array(data)

# LSTMの構築(入力:(data_num, max_t, input_dim), 出力:(data_num, output_dim))
model = Sequential()
model.add(Bidirectional(CuDNNLSTM(hidden_dim, return_sequences=True), input_shape=(max_t, input_dim)))
model.add(Bidirectional(CuDNNLSTM(hidden_dim, return_sequences=True)))
model.add(Bidirectional(CuDNNLSTM(hidden_dim, return_sequences=True)))
model.add(Bidirectional(CuDNNLSTM(hidden_dim)))
model.add(Dense(output_dim))

model.compile(optimizer="adam", loss="mae")

# 全部のデータをまとめ、各次元ごとに平均と標準偏差を求める
x_temp = read_mcep("./aligned_mcep/me")
t_temp = read_mcep("./aligned_mcep/miku")
y = []
y.extend(x_temp)
y.extend(t_temp)
y = np.array(y)
mean = np.mean(y, axis=0)
std = np.std(y, axis=0)
del x_temp
del t_temp
gc.collect()

# 平均と標準偏差を保存
np.savetxt("lstm_mean.txt", mean, fmt="%0.6f")
np.savetxt("lstm_std.txt", std, fmt="%0.6f")

# 入力データと教師データの読み込み
x = read_input("./aligned_mcep/me", mean, std)
t = read_teacher("./aligned_mcep/miku", mean, std)

# 学習用データとテストデータに分離
x_train, x_test, t_train, t_test = train_test_split(x, t, test_size=0.3)

# 学習
history = model.fit(x_train, t_train, epochs=50, batch_size=256, validation_data=(x_test, t_test))

# モデルを保存
model_json_str = model.to_json()
open("lstm_model.json", "w").write(model_json_str)

# 重みを保存
model.save_weights("lstm.h5")

あとは実際に実行し、学習が終わるのを待ちます。

実際に変換

学習が終わったらそれを用いて実際に変換を行います。
流れとしては以下のようになります。

  1. 変換したい音声からメルケプストラムを抽出
  2. 学習したRNNを利用してメルケプストラムを変換
  3. 変換したメルケプストラムから音声合成

新たに書いたソースコードは以下の2つです。
RNNを利用してメルケプストラムを変換する部分とメルケプストラムから音声合成を行う部分です。

import os
import sys
import numpy as np
from keras.layers import Input, Dense
from keras.optimizers import Adam
from keras.models import Model, model_from_json
import tensorflow as tf
from keras.backend import tensorflow_backend

config = tf.ConfigProto(gpu_options=tf.GPUOptions(allow_growth=True))
session = tf.Session(config=config)
tensorflow_backend.set_session(session)

if len(sys.argv) != 3:
    print("usage: python convert_mcep.py <mcep_path> <converted_mcep_path>")
    exit()

mcep_path = sys.argv[1]
converted_mcep_path = sys.argv[2]

input_dim = 25 # 入力の次元
hidden_dim = 100 # 隠れ層の次元
output_dim = 25 # 出力の次元

max_t = 40
mcep = np.loadtxt(mcep_path)
power = mcep[:, 0]
mc = mcep[:, 1:]
mean = np.loadtxt("lstm_mean.txt")
std = np.loadtxt("lstm_std.txt")

mc = (mc - mean) / std # 正規化
mc = np.r_[mc, np.zeros((max_t - 1, input_dim))]

# 変換用のLSTMを読み込む
lstm = model_from_json(open("lstm_model.json").read())
lstm.load_weights("lstm.h5")

# LSTM入力用に整形
data = []

for i in range(mc.shape[0] - max_t + 1):
    data.append(mc[i:i + max_t, :])

data = np.array(data).astype("float32")

converted_mcep = lstm.predict(data) # LSTMで変換

converted_mcep = converted_mcep * std + mean # 正規化したものを戻す
converted_mcep = np.c_[power, converted_mcep]

np.savetxt(converted_mcep_path, converted_mcep, fmt="%0.6f")
import os
import sys
import subprocess
import struct
import numpy as np
from scipy.stats import multivariate_normal
from sklearn.externals import joblib

# メルケプストラム次数
# 実際はパワー項を追加して26次元ベクトルになる
m = 25

def extract_mcep(wav_file, mcep_file, ascii=False):
    cmd = "bcut +s -s 22 %s | x2x +sf | frame -l 400 -p 80 | window -l 400 -L 512 | mcep -l 512 -m %d -a 0.42 | x2x +fa%d > %s" % (wav_file, m, m + 1, mcep_file)
    subprocess.call(cmd, shell=True)

def extract_pitch(wav_file, pitch_file):
    cmd = "bcut +s -s 22 %s | x2x +sf | pitch -a 1 -s 16 -p 80 > %s" % (wav_file, pitch_file)
    subprocess.call(cmd, shell=True)

def synthesis(pitch_file, mcep_file, wav_file):
    cmd = "excite -p 80 %s | mlsadf -m %d -a 0.42 -p 80 %s | clip -y -32000 32000 | x2x +fs > temp.raw" % (pitch_file, m, mcep_file)
    print(cmd)
    subprocess.call(cmd, shell=True)

    cmd = "sox -e signed-integer -c 1 -b 16 -r 16000 temp.raw %s" % (wav_file)
    print(cmd)
    subprocess.call(cmd, shell=True)

    os.remove("temp.raw")

if len(sys.argv) != 4:
    print("usage: python convert_voice.py <wav_path> <converted_mcep_path> <converted_wav_path>")
    exit()

source_wav_file = sys.argv[1]
converted_wav_file = sys.argv[3]

print("extract pitch ...")
source_pitch_file = "source.pitch"
extract_pitch(source_wav_file, source_pitch_file)

source_mcep = np.loadtxt(sys.argv[2]) # 変換後のmcepを読み込む
source_mcep_file = "temp.mcep"
fp = open(source_mcep_file, "wb")

for t in range(len(source_mcep)):
    x_t = source_mcep[t]
    fp.write(struct.pack('f' * (m + 1), * x_t))

fp.close()

# 変換元のピッチと変換したメルケプストラムから再合成
print("synthesis ...")
synthesis(source_pitch_file, source_mcep_file, converted_wav_file)

# 一時ファイルを削除
os.remove(source_mcep_file)
os.remove(source_pitch_file)

変換前と変換後、みくにゃんのメルケプストラムの1次元目は以下のようになっています。
青色が変換前、オレンジが変換後、緑がみくにゃんのメルケプストラムです。
ある程度みくにゃんのものに近づいている感じはあります。(時間は揃えています)

f:id:massoumen:20180320035830p:plain

おわりに

以上が今回の声質変換の結果と大まかな流れです。
音声処理、超難しいですね…(決して画像処理が簡単という訳ではないと思いますが…)
クオリティを上げるためには音声処理や機械学習についてもっといろいろ学ぶ必要がありそうです。
今回、GANを用いたものにも挑戦してみたかったんですが、理解が及ばず今のところ試していません…
(ソースコードの複雑さが段違いである…無念)

今後も勉強しつつ何かネタを見つけてチャレンジしたり、これまで作ったものの改良をしたりしていきたいですね。

卯月識別器の作成に挑戦した話

はじめに

機械学習とかDeepLearningとか、そういったワードを最近よく聞いてる気がするんですよ。 気になるのでそういうの触ってみたいなーとか思う訳ですよ。
僕は一応情報系の学科に所属する大学生で、パターン認識とかも少しくらいは勉強してる訳ですが、あまりプログラムとかは書いたことないなーって。
そんな中、今更ながら(?)ゼロから作るDeepLearningを読んでみたり、アニメキャラの識別をしている記事をいくつか読んだりして、「僕もなんかやってみたい!!!」ってなりました。

さて、プログラムを書く前に「まず何を識別しようか?」と考える訳ですが、やはりここは「推しキャラの識別に挑戦する」しかないでしょう。ちなみの僕の推しキャラはデレステ島村卯月です。卯月の笑顔はとても眩しくて、尊いです。

f:id:massoumen:20180102151718p:plain

なので、顔画像に対してそれが島村卯月かそうでないかを判定するプログラムの作成にチャレンジしました。
勉強も兼ねて、Pythonで作りました。作成の流れは以下のようになりました。

  1. 卯月と卯月以外の顔画像を集める
  2. サイズの統一や明るさの調節など顔画像に前処理を行う
  3. CNNに顔画像をぶち込んで学習させる

また、学習結果を利用し卯月なら赤い枠を、そうでないなら青い枠を付けるプログラムを作成しました。完成したプログラムにいくつか画像を入れて試してみた結果、以下のようになりました。

f:id:massoumen:20180102160002j:plain

f:id:massoumen:20180102160009j:plain

f:id:massoumen:20180102160016j:plain

f:id:massoumen:20180102160027j:plain

訓練用のデータは全てアニメのスクショなので、アニメの卯月の情報からゲームの卯月を識別できていると考えれば、悪くないのかなという気はします。(もちろん、うまく識別できなかったケースもありますが…)
以下、作業の内容とかを少し書いていこうかなと思います。

顔画像の収集

機械学習で良い結果を出すにはたくさんのデータが必要らしいので、まずはアニメのスクショをたくさん撮り、そこから顔を切り取り卯月とそれ以外に分けます。ここで、大量のスクショから顔を切り取るという作業が地獄だという事に気付いたので、先人たちを参考にし、OpenCVを用いたアニメ顔識別器を利用してスクショから顔画像を切り取ってもらいました。

卯月かどうかの判定よりアニメ顔を見つける事の方が難しいのではないか。

f:id:massoumen:20180102154910p:plain f:id:massoumen:20180102154951p:plain

こんな感じに切り取られた顔画像に対し、卯月と卯月以外に分ける作業を手作業で行いました。顔だと認識されていなかったり、顔以外のものが顔だと誤認識してるものもあるので、そういうのは自分で切り取ったり削除します。

プロデューサーと美城常務、顔認識されなさすぎる問題。(悲しいね)

最初は卯月とそれ以外で200枚と500枚くらい用意したのですが、これで学習させたらあまり上手くいかなかったので、最終的に550枚と1350枚くらいまで増やしました。ラブライブ!の人は6000枚以上、ごちうさの人は12000枚以上用意したらしいので、それに比べるとかなり少ないですね…けど、手作業で分類するの、結構疲れるんですよ…

プログラムに識別させるためにまず自分で識別してるの、なんだかなぁ。

顔画像の前処理

入力画像のサイズは統一する必要があるので、得られた顔画像を100*100の大きさに調節します。また、明るさの類は統一した方が良いのかなと思い、画素値の平均や標準偏差を揃えました。(この辺の処理は何が適切なのか正直良く分かっていない…いろんなパターンを試して比較するべきだった…)

f:id:massoumen:20180102170249p:plain f:id:massoumen:20180102170334p:plain

学習

集めた顔画像を訓練用とテスト用に分け、実際に学習させます。Kerasというライブラリを用いてCNNを構築し学習させました。また、ちゃんと卯月を見分ける事ができるかどうか、ゲームの画像を何枚か用意し自分で与え、判定させて試したりします。上手くいかなかったら訓練用の顔画像を増やすべくアニメのスクショを撮りに行ったり、Kerasに用意されているデータの水増し機能を試してみたりしました。
以下、いろいろ試した過程です。

卯月識別器1号

記念すべき第1号です。データ数は卯月、卯月以外でそれぞれ200枚、500枚となっています。ゲームの画像を与えたところ、ラブレター卯月は卯月ではないと判定されました。道のりは険しそうです。

f:id:massoumen:20180102171649j:plain

卯月識別器2号

データ数をそれぞれ350枚、950枚に増やしました。ところが、全て卯月じゃないと判定されてしまいました。
訓練データの正答率が100%となってるあたり、いわゆる過学習という奴でしょうか。やはりデータが少ないようです。

f:id:massoumen:20180102171951j:plain

卯月識別器3号

OpenCVを使って顔画像を回転させ、データ数を3倍に増やしました。恒常卯月のみ卯月だと判定されました。
やはり、まだ足りないのでしょうか。頑張ってスクショ撮りましょう。(疲れるんだよなぁあの作業…)

f:id:massoumen:20180102172320j:plain

卯月識別器4号

データ数をそれぞれ550枚、1350枚に増やしました。1号に比べるとだいぶ増えましたね。(まだまだ少ない感が拭えませんが…)
回転させてデータ数を3倍にした場合もそうでない場合も、恒常卯月しか卯月と判定してくれませんでした。
訓練用に含まれていないアニメopの卯月も1枚用意してみたんですが、それも卯月じゃないと言い出す始末。

お前は何を見て育ったんだ。

f:id:massoumen:20180102172727j:plain

ループを回しすぎなのかなぁと思いループ数を減らしてみたら、天海春香島村卯月だと判定されてしまいました。

f:id:massoumen:20180102172848j:plain

ここで、最後の確認用のデータは手動で切り取っていたのですが、訓練データと同様にアニメ顔識別器で切り取った方が良いのではないか?と思いアニメ顔識別器を使って切り取って用いてみました。その結果、なんか精度が上がりました。訓練データの顔画像にはあまり髪の毛の部分が含まれていないので、そのせいで髪の毛を多く含んだラブレター卯月の顔画像は識別できていなかったのかなぁ…?(恒常佐久間まゆがうまく切り取られていなくて少し悲しかった。イーブイもダメだった。)

f:id:massoumen:20180102173223j:plain

卯月識別器5号

データ数はそのままですが、Kerasにデータを水増しする機能が含まれているらしいので使ってみる事に。(良く分かっていない顔)
学習にかかる時間が増えているっぽいので、たぶんちゃんと水増しされています。最後の確認用のデータも増やしてみましたが、今までで一番いい感じに識別できています。そろそろ疲れてきたのでこの辺で一旦完成という事にして学習結果を保存しました。

f:id:massoumen:20180102173713j:plain

卯月識別器の完成

得られた学習結果を利用し、画像が与えられたらアニメ顔認識器で顔を認識し、さらにそれが卯月であるかどうかを卯月識別器で判定するプログラムを作りました。記事の初めの方に載せた奴以外にもこんな感じの結果が得られました。顔認識がそもそもされていなかったり、されても卯月かどうかの判定に失敗していたりと、認識の難しさを感じる…

f:id:massoumen:20180102185154p:plain

f:id:massoumen:20180102185750j:plain

f:id:massoumen:20180102185800p:plain

f:id:massoumen:20180102185820p:plain

また、ゲームのイラストに関してはそこそこの精度でちゃんと卯月かどうか判定してくれるんですが、いろいろな絵師が描いた卯月については思ってた以上に精度が落ちますね…絵柄の違いが響いているのでしょうか。僕は絵柄とかによっては卯月かどうか判別しにくい場合があったりしますが、卯月識別器もそうなんですかね。いったい、何が「これは卯月である」と言わせているんですかね。

「卯月」とはいったい…(哲学)

感想

2,3日くらい卯月識別器と格闘していましたが、初めて挑戦したという事もあり、中々楽しかったですね。なんやかんやそれっぽく識別できてる感があるのもあり、いろいろな画像で試してみては「お~」って感じになってます。DeepLearningの類はデータがたくさん必要らしいですが、実際に動かしてみてそれを強く感じましたね…何はともあれ、使えるデータがたくさん必要だなと。今回はCNNの層の構成を変化させたり、画像の前処理などについてちゃんと調査したりしていないので、その辺についても調べていきたいですね。
また、今回いろいろ試してみて「まだまだ知識不足だな~」というのも強く感じたので、勉強も必要かなと。

気が向いたらソースコードどっかに上げたり改良版作ったりするかも…?

VisualStudio2017Communityの導入

VisualStudio 2017 Communityを入手してC言語(C++)でHelloWorld!!を表示するまでのメモ。

ダウンロードとインストー

まずは公式サイトからVisualStudio 2017 Communityのインストーラを入手。
すごい分かりにくいのは気のせいだろうか

f:id:massoumen:20170505113600p:plain

ダウンロードしたらインストールすべく実行します。実行するとこんな感じの画面が出てくる。

f:id:massoumen:20170505113614p:plain

今回はC言語(C++)を使いたいので、とりあえず「C++によるデスクトップ開発」にチェックを入れる。

f:id:massoumen:20170505113622p:plain

右下のインストールを実行するとインストールが始まる。

f:id:massoumen:20170505113628p:plain

完了すると再起動を要求されるので再起動。

f:id:massoumen:20170505113633p:plain

再起動後、VisualStudio 2017が入っている事を確認。

f:id:massoumen:20170505113645p:plain

実行するとサインインを要求される。
マイクロソフトのアカウントを持っているならサインイン、そうでなければアカウントを作りサインインする。

f:id:massoumen:20170505113651p:plain

サインインに成功するとVisualStudio 2017が起動しこんな感じの画面が出る。

f:id:massoumen:20170505113658p:plain

プロジェクトの作成からHelloWorld!!まで

試しにC言語(C++)を使ってコンソール上でHelloWorld!!を表示させる。
まずは「ファイル」→「新規作成」→「プロジェクト」を選択。

f:id:massoumen:20170505113711p:plain

Visual C++の欄からWin32コンソールアプリケーションを選択。
名前とファイルの保存場所はご自由に。(今回名前はTest、保存場所はデスクトップ上にした。)

f:id:massoumen:20170505113719p:plain

決定するとこんな感じの画面が出てくる。

f:id:massoumen:20170505113729p:plain

次にソースコードを記述するためのソースファイルを追加する。
ソースファイルの箇所を右クリックし「追加」→「新しい項目」を選択。

f:id:massoumen:20170505113732p:plain

C++ファイル(.cpp)を選択。ファイル名はご自由に。(今回はSource.cppのまま。)

f:id:massoumen:20170505113735p:plain

すると、ソースコードが書けるようになるので次のコードを記述。

#include <stdio.h>

int main()
{
    printf("Hello World!!\n");

    return 0;
}

f:id:massoumen:20170505113739p:plain

CtrlとF5を同時押しして実行すると「Hello World!!」が表示される(と思う)。

f:id:massoumen:20170505113742p:plain

表示されたら成功。やったぜ。

コンソールが表示されない場合

CtrlとF5を同時押ししても黒い画面が出てこない場合は以下の操作を試してみて下さい。
まず、「プロジェクト」→「(プロジェクト名)のプロパティ」を選択。

f:id:massoumen:20170505114140p:plain

左上の「構成」を「すべての構成」にした後、「構成プロパティ」→「リンカー」→「システム」→「サブシステム」の設定を「コンソール(/SUBSYSTEM:CONSOLE)」に変更。

f:id:massoumen:20170505114204p:plain

設定を終了し再びCtrlとF5を同時押しして「Hello World!!」が表示されるか確認。
表示されたら成功。ダメだったら…僕にはよく分からないわ、ごめん。

後から他の機能を追加

後からC++以外(C#など)を利用したくなった場合の機能の追加方法のメモ。

今回はC#の機能を追加してみる。まず、VisualStudio Installerを起動。

f:id:massoumen:20170505114244p:plain

「変更」を選択。

f:id:massoumen:20170505114251p:plain

最初にインストールする時と同じ画面が出てくるので、追加したいものを選んで右下の「変更」を押す。
今回はC#の機能を追加したいので「.NETデスクトップ開発」を追加。
(追加インストール後に画面をキャプチャしたのでここに載っている画像は「閉じる」になっているが…)

f:id:massoumen:20170505114257p:plain

すると追加のインストールが始まるので見守る。

f:id:massoumen:20170505114303p:plain

終わったら「起動」でVisualStudio 2017を起動する。

f:id:massoumen:20170505114311p:plain

C言語(C++)の時と同様にプロジェクトの新規作成を行うと、項目が増えている。
今回はVisual C#の中の「コンソールアプリ(.NetFramework)」を選択。

f:id:massoumen:20170505114316p:plain

C#の場合最初からソースコードが少し書かれているので、適切な位置に以下の記述を追加。

Console.WriteLine("Hello World!!");

f:id:massoumen:20170505114322p:plain

C++の時と同様にCtrlとF5を同時押しして実行。

f:id:massoumen:20170505114329p:plain

表示されたら成功。

こんな感じで後から他の機能をいろいろ追加できる。