女子高生になりたい

はるみちゃんのブログだよ。えへへ。

Scalaコストメモ

自分のコードのボトルネックを探すための検証。個人的にメモった結果を貼ってるだけです><><>

10万回繰り返す時間を100回計測し集計しています。

とりあえず時間測定のために、簡単な計測関数を作っておきます。

def printExecutionTime(proc: => Unit) = {
    val start = System.currentTimeMillis
    proc
    System.currentTimeMillis - start
}

(参照: Scalaで処理時間を計測 - a-sanの日記)

traitをnewする

簡単なcase classとtraitを用意

case class User(name: String)
trait A {
    val users: Seq[User]
}

以下計測コード

for{ _ <- 1 to 100}yield {
  val t = printExecutionTime {
    for {_ <- 1 to 100000} yield {
      new A {
        override val users: Seq[User] = Seq.empty
      }
    }
  }
  times += t
}
val sum = times.sum
val mean = sum / 100.0
val variance = times.map(v => Math.pow(v - mean, 2)).sum / 100.0
println(s"合計:$sum ms")
println(s"平均:$mean ms")
println(s"分散:$variance")

(結果)

合計:480 ms

平均:4.8 ms

分散:10.920000000000023

因みにSeq.empty[User]と型を明示しても差はないです。

事前にemptySeqを作っておくと、Seqインスタンス生成コストが発生しないので当然はやくなります。

val emptySeq = Seq.empty[User]

for {....} yield {
    new A {
    override val users: Seq[User] = emptySeq
    }
}

合計:243 ms

平均:2.43 ms

分散:11.745100000000015

参照を渡すだけなので、Seqインスタンスのサイズによって速度に影響がでることはありません。

val longUsers = Seq.fill(1000)(User("sample"))

for {....} yield {
    new A {
    override val users: Seq[User] = longUsers
    }
}

合計:255 ms

平均:2.55 ms

分散:10.407500000000006

classをnewする

class B(users: Seq[User])
new B(users = Seq.empty)

合計:457 ms

平均:4.57 ms

分散:32.42509999999994

new B(users = longUsers)

合計:203 ms

平均:2.03 ms

分散:10.70909999999997

traitのインスタンス化と大差なさそうです

SeqのlengthとIndexedSeqのlength

longUsers.length

Seqのとき

val longUsers = Seq.fill(1000)(User("sample"))

合計:24732 ms

平均:247.32 ms

分散:171.07760000000005

IndexedSeqのとき

val longUsers = IndexedSeq.fill(1000)(User("sample"))

合計:194 ms

平均:1.94 ms

分散:7.196400000000003

IndexedSeqのlengthは計算量O(1)ですが、Seq(List)はO(n)です。

SeqのlengthとIndexedSeqのランダムアクセス

longUsers(10)
longUsers(20)
longUsers(30)

Seqのとき 合計:992 ms 平均:9.92 ms 分散:12.473599999999994

IndexedSeqのとき

合計:232 ms

平均:2.32 ms

分散:10.397599999999994

IndexedSeqのランダムアクセスは計算量O(1)ですが、Seq(List)はO(n)です。 逆にheadとかtailはSeqの方がはやいです。

Seqインスタンス生成

Seq.fill(1000)(User("sample"))

10万回はきつかったので、1000回にしてます。

合計:2681 ms

平均:26.81 ms

分散:132.4138999999999

大きなオブジェクトをインスタンス化するのはしんどいですね。

Seqインスタンス生成ー>シャッフル

10万回はきつかったので、1000回にしてます。

シャフルのコストはO(n)です。

random.shuffle(Seq.fill(1000)(User("sample")))

合計:6584 ms

平均:65.84 ms

分散:589.0543999999986

IndexedSeq to List

10万回はきつかったので、1000回にしてます。

longUsers.toList

合計:3329 ms

平均:33.29 ms

分散:80.40589999999993

List to IndexedSeq

longUsersSeq.toIndexedSeq

合計:580 ms

平均:5.8 ms

分散:16.120000000000033

toListの方がコストが高い

SeqのSlice

10万回に戻してます。

longUsers.slice(500,520)

Seq

合計:14837 ms

平均:148.37 ms

分散:472.6131

IndexedSeq

合計:2904 ms

平均:29.04 ms

分散:494.5183999999998

sliceもindexedSeqの方が速い。500にランダムアクセスしてそこからO(n)の計算量

LogstashのMultiple-Pipelinesを使う

確か5系まではmainのpipelineしかありませんでした。

なので、こんな感じでtagなどでoutput先を切り替えたりしてました。。

output {
  if "hoge_feed" in [tags] {
    elasticsearch {
      hosts => ["http://127.0.0.1:9200"]
      index => "hoge"
      document_id => "%{item_id}"
      template => "/usr/share/logstash/templates/hoge.json"
      template_name => "hoge_index"
      manage_template => "true"
      template_overwrite => "true"
    }
  }
}

(hoge.conf)

6系からはMultiple-Piplineが使用できるようになったので、これを使っていきます。

使い方は簡単で、piplines.ymlをpath.setting配下に設置するだけです。

- pipeline.id: APipline
  path.config: "/usr/share/logstash/pipeline/hoge.conf"
  queue.type: persisted
- pipeline.id: BPipline
  path.config: "/usr/share/logstash/pipeline/hogehoge.conf"
  queue.type: persisted

(piplines.yml)

ユニークなpipline.idを決めて、confファイルのパスを指定するだけです。 あとはいろいろなオプション。 設定できるオプションはドキュメント参照。

www.elastic.co

これをpath.settingsにおいて、Logstashを-fオプションなど指定せずに起動するとpiplines.ymlの設定が適用されます。

動かない時に確認する事とか注意する事とか。

パイプラインのリソース

piplineのデフォルト値は、単一のpipline前提で設定されています。

pipeline.workersのデフォルト値は「Number of the host’s CPU cores」です。。

この辺りは調整した方が良さそうです。

piplines.ymlの置き場所

piplines.ymlはpath.settings配下に置きます。

path.settingsの場所は、デフォルトでは以下のような設定なっています。

(1).zip .tar.gzから解答した場合 → {extract.path}/config

(2)Debian package, RPM packageからインストールした場合→/etc/logstash

(3)公式のDocker Imageを使用している場合→/usr/share/logstash/config

ディレクトリレイアウトは↓に詳しく載っています。

www.elastic.co

起動時のオプション

Logstashの起動時に
-e, --config.stringや-f, --path.config オプションを指定するとpipelines.ymlを無視します。(警告がログにでます)

これでif文使ったり、プロセスわけたりする必要がなくなりました・・!ありがたい。

DCGANで遊んでみたメモ(2)

DCGANによる顔画像の生成

データセットはみんな使ってるこれ。

mmlab.ie.cuhk.edu.hk

画像サイズを28x28x3(RGB)に整形して入力とする。

ポイントは、batch_normalizationを導入すること、らしい。 これは、ミニバッチ毎にデータを標準化する手法。

各レイヤーのLeakyReluへの入力タイミングでbatch_normalizationを適用する。

以下のようなイメージ。

Generator

with tf.variable_scope('generator', reuse=not is_train):
    x1 = tf.layers.dense(z, 4*4*512)
    x1 = tf.reshape(x1, (-1, 4, 4, 512))
    x1 = tf.layers.batch_normalization(x1, training=is_train)
    x1 = tf.maximum(0.2 * x1, x1)
    
    x2 = tf.layers.conv2d_transpose(x1, 256, 5, strides=3, padding='same')
    x2 = tf.layers.batch_normalization(x2, training=is_train)
    x2 = tf.maximum(0.2 * x2, x2)
    
    x3 = tf.layers.conv2d_transpose(x2, 128, 5, strides=2, padding='same')
    x3 = tf.layers.batch_normalization(x3, training=is_train)
    x3 = tf.maximum(0.2 * x3, x3)
    
    logits = tf.layers.conv2d_transpose(x3, out_channel_dim, 5, strides=1, padding='valid')
    
    out = tf.multiply(tf.tanh(logits), 0.5)

    return out

Discriminator

with tf.variable_scope('discriminator', reuse=reuse):
    x1 = tf.layers.conv2d(images, 64, 5, strides=2, padding='same')
    relu1 = tf.maximum(0.2 * x1, x1)
    
    x2 = tf.layers.conv2d(relu1, 128, 5, strides=2, padding='same')
    bn2 = tf.layers.batch_normalization(x2, training=True)
    relu2 = tf.maximum(0.2 * bn2, bn2)
    
    x3 = tf.layers.conv2d(relu2, 256, 5, strides=2, padding='same')
    bn3 = tf.layers.batch_normalization(x3, training=True)
    relu3 = tf.maximum(0.2 * bn3, bn3)

    flat = tf.reshape(relu3, (-1, 4*4*256))
    logits = tf.layers.dense(flat, 1)
    out = tf.sigmoid(logits)
    
    return out, logits

あとハマりどころがコレ↓


Note: when training, the moving_mean and moving_variance need to be updated. By default the update ops are placed in tf.GraphKeys.UPDATE_OPS, so they need to be added as a dependency to the train_op. Also, be sure to add any batch_normalization ops before getting the update_ops collection. Otherwise, update_ops will be empty, and training/inference will not work properly. For example:


(引用)tf.layers.batch_normalization  |  TensorFlow

ということでOptimizerではこんな感じで更新を明示します

with tf.control_dependencies(tf.get_collection(tf.GraphKeys.UPDATE_OPS)):
    discriminator_optimizer = tf.train.AdamOptimizer(learning_rate, beta1=beta1).minimize(discriminator_loss, var_list=discriminator_vars)
    generator_optimizer = tf.train.AdamOptimizer(learning_rate, beta1=beta1).minimize(generator_loss, var_list=generator_vars)

結果

最初はランダムノイズです。

f:id:sakata_harumi:20180506224346p:plain

学習途中
(ミニバッチのLoss)
Discriminator Loss: 0.0795...
Generator Loss: 3.1901...

ふええ・・

f:id:sakata_harumi:20180506224416p:plain

学習途中

徐々に色があって顔っぽくなってきた。まだホラー。

f:id:sakata_harumi:20180506224812p:plain f:id:sakata_harumi:20180506224912p:plain

学習途中

Discriminator Loss > Generator Lossになってきた辺りで、 顔っぽくなってきた。

f:id:sakata_harumi:20180506225030p:plainf:id:sakata_harumi:20180506225027p:plain

学習停止直前

人の顔っぽくなってきたので、学習を打ち止めました。

ホラー感がなくなりましたね。

Discriminator Loss: 2.1258...
Generator Loss: 0.2235...

f:id:sakata_harumi:20180506225253p:plain

Discriminator Loss: 2.2205...
Generator Loss: 0.2133....

f:id:sakata_harumi:20180506225347p:plain

次は、Conditional-GANで遊んでみたいなあ。(小並感)

DCGANで遊んだメモ(1)

GANってなんや

GAN => 敵対的生成ネットワーク

敵対的、とあるように2つの異[なった役割のネットワークが競い合う。

よく説明で使われるのは警察と偽札を作ろうとする偽造者の例。

警察は特定の紙幣を調べ、本物か偽札かを判別する。 偽造者は本物とそっくりな偽札を作ろうとする。

警察の偽札を見分ける能力があがるー>偽造者はより本物に近い偽札を作らないといけない

偽造者がより本物に近い偽札を作るー>警察は偽札を見分ける能力を高めなければいけない

というわけで、2つのネットワークがどんどん強くなっていく。 最終的には、ほぼ本物と見分けが付かない偽札を生成しないといけなくなる。。

先の例で言う警察の役割をするネットワークがDiscriminator、偽造者の役割をするネットワークがGeneratorと呼ばれる。 Generatorが訓練データと同じようなデータを生成しようとし、Discriminatorが渡されたデータが訓練データから来たものか、偽造されたものかを判別する。

図で表すとこんな感じ。

Generatorへの入力はRandom Noise。 RandomNoiseからのマッピングを学習して、偽画像を精巧にしていく。

f:id:sakata_harumi:20180506221651p:plain

原論文は 1406.2661 Generative Adversarial Networks

原論文には以下の図で学習の過程が示されている。

f:id:sakata_harumi:20180506221649p:plain1406.2661 Generative Adversarial Networks 3P 中段の図)

青線がDiscriminatorの判別関数 黒い点が訓練画像の分布。緑の画像が生成画像の分布。

(a) 最初は、訓練画像の分布と生成画像の分布は大きく異なる。 そして、Discriminatorもはっきりとした判別を出来ていない状態。

(b) Discriminatorを学習させる。 そうすると、Discriminatorは生成画像と訓練画像をはっきりと判別できるようになる

(c) Generatorがマッピングを学習していくと、生成される画像は訓練画像に近づく。

(d) 分布が等しくなると、Discriminatorは区別ができなくなる(訓練画像、生成画像どちらを渡しても出力値が0.5になる)

という過程を経て学習が進んでいく、という図。

DCGANってなんや

GeneratorとDiscriminatorの実装を畳み込みニューラルネットワークにしたもの。 複雑な画像でも学習しやすくなった。

Conditional GANってなんや

GANでは入力にランダムノイズを入れるため、生成される画像を任意のものにすることはできなかった。 そこでラベルと一緒に学習させる事で、ラベルによって生成される画像のパターンを制御する。

VTuberになりたい!(成果物)

VTuberになりたい!のでやってみました。

ざっくりした作り方とか、制作過程のメモとかは↓で。

sakataharumi.hatenablog.jp

1. とりあえず動いた!

感動の瞬間です。

なんか鏡が暗いし、内股で、手の角度もおかしい、表情もない等欠点ばかりですが、とりあえず動きました。

2. ハレ晴れユカイ踊って歌ってみた!

内股とか色々調節して、歌いながら踊ってみました。

固定視点カメラじゃないのでブレブレです。

友人からは、「よくこんな醜態晒せるな」とか「餌を乞う豚」とか散々言われました><ふええ・・

3. 演技力じゃかりこ面接!(女性にアクターをやってもらいました)

某方にアクターをやって頂きました!なんか声もキズナアイに似てるしいい感じ!! リアルタイムのキーボード操作で表情をつけています。

撮影風景はこんな感じ。 マイク持ち係の豚が自分で、見切れている人が動画編集&表情操作をやってくれている友人です。

今後やりたい事

1. オリジナルモデルを動かす

やっぱりオリジナルキャラクターを動かしてVTuberやりたいです。

鋭意製作・・してもらってます。楽しみ。

2. Vive Trackerを導入する

Viveと2本のコントローラだけでは頭、左右の腕しかトラッキングできてません。。

頭の位置から足の曲がり具合はわかりますが、移動したり、キックしたりなどが出来ません。。そこを何とかしたい・・!

何個かViveトラッカーを購入してトラッキング出来る箇所を増やしていきたいです。

VTuberになりたい!(Tips編)

女子高生になりたい。 せや、バーチャルユーチューバーになればええんや!

ということで、VTuber環境を構築したメモです。

成果物はこの記事に置いてます。

sakataharumi.hatenablog.jp

使用したもの

[VR Device]

  • HTC Vive

[ソフトウェア]

  • Unity(開発)
  • OBS Studio(動画撮影)

[Unity Plugin]

  • MMD4Mecanim
  • Finak IK
  • OVRLipSync
  • SteamVR Plugin
  • VIVE Input Utility for Unity

[モデル]

制作

だいたい作り方は Unity+Vive+MMD+VRIKで、キズナアイちゃんになりきりVR - Qiita などに載っています。

流れとしては、

  1. MMDモデルをUnityで使用可能な形式に変換
  2. SteamVR PluginのCameraRig, Status, StreamVRをヒエラルキ上に置く
  3. Final IKの設定をする
  4. OVRLipSyncの設定をする
  5. HMDから自分が見えるように鏡を置く
  6. 腕の角度や足の角度を調節する
  7. 表情をキーボードやViveコントローラで変えられるようにする
  8. 撮影

って感じです。

以下はちょっと迷ったところとか、個人的にメモっときたい事を書いときます。

Final IK

IK(= Inverse Kinematics)日本語では逆運動学になります

順運動学は、例えば腕が○度曲がったから手の位置はココ!みたいな計算をします。

それに対し、逆運動学では、手の位置がココだから腕は○度曲がってる!というような計算をします。

Viveにより、頭の位置、手の位置がトラッキングできるので、これで手の角度や足の曲がり具合などをIKにより計算するわけです。

FinalIKの設定で、VRIK Scriptをアタッチした際のReferencesですが、デフォルトでは全てNoneになっていました(自動で参照してくれない)

VRIKのコンポーネント上で右クリックするとAuto-detect Referencesという項目があったので、これで自動選択してくれました。。

f:id:sakata_harumi:20180504173048p:plain

ただし、一部自動で設定されない項目や誤っている項目がありました(多分)。最終的に、自分は以下のように設定しました。

f:id:sakata_harumi:20180504173742p:plain

OVRLipSync

うまく口が動いてくれない事がありました。 口の動きが小さいからといってGainを極端に大きくしても変わらなかったように見えます。 (しかもキツイノイズが入るようになった)

最終的には、Gain 10くらいでハッキリと口が動くようになりました。

マイク指してGameシーンをPreviewした直後は認識しないのかな?ちょっと待ったら動くようになった気がする・・?

この辺りはよくわかってないです。

表情の変更

表情はキズナアイモデルの、U_Char_1にアタッチされてるSkinned Mesh Rendererの値を操作(1~100)する事で変更可能です。

f:id:sakata_harumi:20180504174449p:plain

ScriptではSkinnedMeshRendererコンポーネントのSetBlendShapeWeightで変更できます。

public SkinnedMeshRenderer skinnedMeshRenderer;

this.skinnedMeshRenderer.SetBlendShapeWeight (0, 100);

今回、Viewのトリガーボタン、丸いトラックパッドを押した時に表情を変更するようにしました。 ボタン押下中は特定の表情のweightを増加させ、表情を徐々に変化させます。

ViveのボタンはSteamVR_Controller.Inputから取得できます。

var device = SteamVR_Controller.Input ((int)vrTrack.index);
device.GetPress(SteamVR_Controller.ButtonMask.Trigger);

(トリガー押下)

トラックパッドの指の位置はGetAxis()で取れますが、ボタンを押した時のみに反応するには.GetPress(SteamVR_Controller.ButtonMask.Touchpad)と組み合わせる必要があります。

if (device.GetPress(SteamVR_Controller.ButtonMask.Touchpad)){
  if(device.GetAxis().y < 0 && device.GetAxis().x > 0){
    // something
  }
}

成果物

次の記事で公開します

sakataharumi.hatenablog.jp

(イメージ)

f:id:sakata_harumi:20180504180249p:plain

ElasticSearch x Kubernetesメモ

「Elastic Search Kubernetes」でググると、この方のリポジトリが上位に出てきます。

github.com

ここのstateful/にあるStatefulSetの設定をコピペすれば大体OKなんだけど、使用してるDockerImageが公式のものじゃないことに注意。

公式のものに差し替えて適用してみると、そのままではAccessDeniedみたいなパーミッション系のエラーがでて、PersistentVolume に書き込むことができない><><

spec:
  securityContext:
    fsGroup: 1000
    runAsUser: 0

fsGroupの設定をすればおk

公式イメージでは、1000がelastic search User。

後、微妙に違う点といえば、上記リポジトリでは/dataにVolumeをマウントしてるけど、公式イメージでは確か「/usr/share/elasticsearch/data」がデフォルトのdataディレクトリになってる。

なので、イメージの環境変数にpath.data = /dataを追加するのを忘れないようにしてください><

- name: path.data
    value: "/data"