触覚提示のために音響を振動に変換する方式

触覚提示のための振動を用意するには、
(1)手作業で作る方法と(2)音響から変換して作る方法(と他にも色々あるが割愛)がある.

(1)手作業で作る方法は,immersionとかが出してるエディタで行う(のかな).
一方,(2)音響から変換する方法にも色々な方式があるようで、
今後需要もありそうだしまとめてみた.
CHI13のこの文献を出発点として整理.

方式をざっと分類

これだけではないと思うが、ざっと分類

# 方式 文献
1 そのまま変換 -
2 バイス特性を考慮して変換 [2-1]
3 人の音響や振動に対する知覚特性を考慮して変換 [3-1], [3-2]

1. そのまま変換する方式

特に信号処理せずそのまま変換する方式.
何も考えない.
自分が昔作ったシステムでも雪を踏んだ音を触覚提示としても使いまわしている.

2. デバイス特性を考慮して変換する方式

この論文では, デバイスとして使っているMotorola e380, e398, e680の特性として,

  • 100-300Hzの信号を出力すると振動として現れる
  • 300Hz以上だと音響として現れる

ことを考慮して,
音響の低周波を強調して変換することで振動刺激として使おうとしている.

こういったデバイス特性に考慮して変換する必要はあるはず.

3. 人の音響や振動に対する知覚特性を考慮して変換する方式

波形を人の振動知覚範囲に収めるような変換を施す方式

人の聴覚周波数の知覚範囲が20Hz~20kHzであるのに対し,
振動知覚の知覚範囲が0~1000Hzであるため,
単純に音響波形を変換すると大部分を振動として知覚できずロストしてしまうことになる.
そこで波形をN分周(周波数を1/N倍)して変換する方式が提案されている.

音響の強度・粗さ知覚と振動の強度・粗さ知覚をマッピングする方式

人の音響と振動に対する強度の知覚と粗さの知覚をマッピングするモデルを構築し,
このモデルで変換してやろうという研究
結構面白かった.

まとめ

さて,いざどの変換方式を使おうと思うと, これらの方式を俯瞰した評価がないので、どれが優れているかを一概に言えない. ベンチマークテストが欲しい.

関係ないが、google scholarで検索していくと、
この辺の特許はほんとにimmersionが強いな...という感想を抱く.

IEEE Haptics Symposium 2018に行ってきた

趣味でやっている研究活動の一環で
触覚に関する国際会議の1つの IEEE Haptics Symposium 2018 に行ってきた.
会社でまるまる有給をとり,共著の方の援助を受けサンフランシスコへ.

f:id:yusuke_ujitoko:20180405210024j:plain

普段は深層学習まわりしか記事にしてきていなかったが、
今回は触覚にまつわる研究について少しだけ紹介。

形状ディスプレイ

ピンアレイ型形状ディスプレイのデモ。

バーチャル空間の物体に触ったときの物体形状をピンを制御して提示する。
実際触ったのは初めてだったので感激した。

錯覚を使ったテクスチャ提示法

Anatole Lécuyerのところの錯覚を使った触覚提示も興味深かった.
視覚によって触覚が引っ張られることを利用したpseudo-hapticsの一種の展示.

タッチスクリーンでは,指と視覚提示部が同時に視認できてしまうので,
この種の錯覚は提示するのが難しいと言われていたが, それにチャレンジしようという研究.

ちなみに我々の研究グループが発表したのもタッチスクリーンでのpseudo-hapticsについてで、 重さや抵抗感の提示を試みる新たなアプローチに関するものだった。

その他

篠田研のテニス鑑賞映像への振動付与システムの完成度がとても高く,
梶本研の指先への触覚を手首に提示するシステムも、
奇抜な見た目にも関わらず直感的でよかった.

終わりに

これまでまともな触覚系学会には行ったことなかったが、
今回機会を頂けて行くことができてよかった。

「会社休んで趣味で来てます」 というと結構驚かれてインパクトがあるようなので、
今後もこの方針で活動を続けて、キャラを育てていきたい。 f:id:yusuke_ujitoko:20180404215339p:plain

6月のEuroHapticsでも別のテーマで発表します。
コンテンツに応じた触振動刺激の自動生成をGANを使って行う研究です。
立て続けに会社を休むわけにもいかないので、こちらは共著者の方に発表はお任せしますが..。

TensorFlowのRNNを追ってみる

TensorFlowのRNN実装はサンプルが少なく、
かつそういったサンプルコードでは、
限定された一部のAPIしか使っていないなど全体を網羅しづらい感じがあるので、
なるべく全体感を思い出しやすいように、自分用にメモ。
(と言う割に基本的なAPIしか使ってないが...)

TensorFlowのRNNのAPIを使わないRNNの実装

まずは2(タイム)ステップだけ動作するRNNをつくる。
各ステップで入れるデータをX0, X1としてplaceholderをつくる。
各ステップでは、入力ユニット数(=4)にあたるデータを用意。

今回は隠れ層は1つとして、
唯一の隠れ層のユニット数は6。

n_inputs = 4
n_neurons = 6

X0 = tf.placeholder(tf.float32, [None, n_inputs])
X1 = tf.placeholder(tf.float32, [None, n_inputs])

Wx = tf.Variable(tf.random_normal(shape=[n_inputs, n_neurons],dtype=tf.float32))
Wy = tf.Variable(tf.random_normal(shape=[n_neurons,n_neurons],dtype=tf.float32))
b = tf.Variable(tf.zeros([1, n_neurons], dtype=tf.float32))

Y0 = tf.tanh(tf.matmul(X0, Wx) + b)
Y1 = tf.tanh(tf.matmul(Y0, Wy) + tf.matmul(X1, Wx) + b)

init = tf.global_variables_initializer()

ミニバッチは4つデータを含む。

# t = 0
X0_batch = np.array([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11], [12, 13, 14, 15]])
# t = 1
X1_batch = np.array([[15, 14, 13, 12], [0, 0, 0, 0], [8, 7, 6, 5], [4, 3, 2, 1]])

with tf.Session() as sess:
    init.run()
    Y0_eval, Y1_eval = sess.run([Y0, Y1], feed_dict={X0: X0_batch, X1: X1_batch})

print(Y0_eval)
print(Y1_eval)

tf.contrib.rnn.static_rnn()を使う実装

上記実装はシンプルだが、
ステップ数を増やそうとするとネットワークが複雑になっていく。
そこでTensorFlowのstatic_rnnというAPIを使う。

static_rnnを使うと、TensorFlowで用意されたRNNのcellをrollingせずに記述できる。
cell(下ではBasicRNNCellを使用)というのは、各ステップに対応するRNNユニットと考えるとよい。

static_rnnで、それらのcellと各ステップの入力をAPI内で繋げてくれる。
具体的なstatic_rnnの内部の処理としては、
入力があるごとにcellをコピーして、上の実装のように繋げているようだ。

n_inputs = 4
n_neurons = 6

X0 = tf.placeholder(tf.float32, [None, n_inputs])
X1 = tf.placeholder(tf.float32, [None, n_inputs])

basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
output_seqs, states = tf.contrib.rnn.static_rnn(basic_cell, [X0, X1],
                                                dtype=tf.float32)
Y0, Y1 = output_seqs
init = tf.global_variables_initializer()
# t = 0
X0_batch = np.array([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11], [12, 13, 14, 15]])
# t = 1
X1_batch = np.array([[15, 14, 13, 12], [0, 0, 0, 0], [8, 7, 6, 5], [4, 3, 2, 1]])

with tf.Session() as sess:
    init.run()
    Y0_eval, Y1_eval = sess.run([Y0, Y1], feed_dict={X0: X0_batch, X1: X1_batch})

print(Y0_eval)
print(Y1_eval)

各ステップの入力をまとめて1つとして扱う

上の実装では、2ステップ分の入力のplaceholderをそれぞれX0, X1と定義していた。
これもステップ数が多くなるとX0, X1, X2... と増やさないといけなくなるので、
入力をまとめて扱うようにする。

具体的には、
[バッチサイズ、ステップ数、入力ユニット数]としたplaceholderを用意してまとめて扱い、
それらを各ステップの入力ごとにバラす
(tf.unstackで1次元目に沿って、別々のリストに分ける)。

出力のところでは逆にtf.stackを使って、
各ステップでの出力結果を結合して、
[バッチサイズ,ステップ数,出力ユニット数]を得ている。

n_steps = 2
n_inputs = 4
n_neurons = 6

X = tf.placeholder(tf.float32, [None, n_steps, n_inputs])
X_seqs = tf.unstack(tf.transpose(X, perm=[1, 0, 2]))

basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
output_seqs, states = tf.contrib.rnn.static_rnn(basic_cell, X_seqs,
                                                dtype=tf.float32)
outputs = tf.transpose(tf.stack(output_seqs), perm=[1, 0, 2])

init = tf.global_variables_initializer()
X_batch = np.array([
        # t = 0      t = 1 
        [[0, 1, 2, 3], [15, 14, 13, 12]], 
        [[4, 5, 6, 7], [0, 0, 0, 0]], 
        [[8, 9, 10, 11], [8, 7, 6, 5]], 
        [[12, 13, 14, 15], [4, 3, 2, 1]], 
    ])

with tf.Session() as sess:
    init.run()
    outputs_val = outputs.eval(feed_dict={X: X_batch})

dynamic_rnnを使う

上の実装にはまだ問題がある。
逆伝搬時の勾配計算のために、
順伝搬時の各ステップの計算結果を保持して置く必要があるため、 メモリを圧迫してしまう。
これを避けるためにstatic_rnnの代わりにdynamic_rnnを使うとよい。

dynamic_rnnに渡す引数としてswap_memoryをTrueにしておけば、
GPUからCPUにメモリ退避してくれる。

また、別の利点として各ステップの入出力に、
[バッチサイズ,ステップ数,ユニット数]をそのまま使えるので、
上の実装にあるようにunstackやstackをしなくてよくなる。

X = tf.placeholder(tf.float32, [None, n_steps, n_inputs])

basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
outputs, states = tf.nn.dynamic_rnn(basic_cell, X, dtype=tf.float32)

ステップごとの入力を可変長とする

ここまでは各ステップでの入力長は固定だった。
でも文字列みたいに可変長のステップを入れないといけない場合もある。

そういう場合には、dynamic_rnnやstatic_rnnを呼ぶときに、sequence_lengthを設定する。

seq_length = tf.placeholder(tf.int32, [None])
outputs, states = tf.nn.dynamic_rnn(basic_cell, X, dtype=tf.float32,
                                    sequence_length=seq_length)

設定したsquence_lengthに満たない入力は0でpaddingして入力しなければならない

X_batch = np.array([
        # step 0     step 1
        [[0, 1, 2], [9, 8, 7]], # 
        [[3, 4, 5], [0, 0, 0]], # 0でパディング
        [[6, 7, 8], [6, 5, 4]], # 
        [[9, 0, 1], [3, 2, 1]], # 
    ])
seq_length_batch = np.array([2, 1, 2, 2])

その他

いまBasicRNNCellをつかっているが、
cellはLSTMやGRUに入れ替えることも可能。

PyTorchでGPUメモリが解放されないときの対処法

PyTorchのDataLoaderのバグでGPUメモリが解放されないことがある.
nvidia-smiで見ても該当プロセスidは表示されない.
下のコマンドで無理やり解放できる.

ps aux|grep <username>|grep python|awk '{print $2}'|xargs kill

Neural Style Transferを音に応用した研究たち

結論としては今のところ上手くいっていないように見える.
今後の進展にとても期待.

  • Audio style transfer
    https://arxiv.org/abs/1710.11385
    • Gatysらの手法というよりも高速化されたJohnsonらのstyle transferの手法に近く,
      コンテンツ画像を初期値としてスタイル変換する.
    • audioではcontentとstyleが定義されていないんですという話がイントロに載っている
      • In audio, the notions of style and content are even harder to define and would depend more on the context. For speech for instance, content may refer to the linguistic information like phonemes and words while style may relate to the particularities of the speaker such as speaker’s identity, intonation, accent, and/or emotion.
      • For music, on the other hand, content could be some global musical structure (including, e.g., the score played and rhythm) while style may refer to the timbres of musical instruments and musical genre
    • 微妙

  • Time Domain Neural Audio Style Transfer
    https://arxiv.org/abs/1711.11160
    • 上の二つの研究はスペクトログラムを画像として扱って,もとのneural style transferの手法を適用していたが, それだと変換後のスペクトログラムをGriffin-Limアルゴリズムで位相復元する必要があった.
      • Griffin-Limを使うと次のような欠点が生まれる
        • 結局,位相情報のtransferができていない
        • 位相復元が収束するまで反復する必要があるので実時間性を確保できない
    • そこでこの研究では,生のaudioに対してneural style transferの手法を適用した
    • 学習済みwavenetのdecoderとNSynth encoderを使って,Gatysらの手法を適用.
      wavenetとNSynthは次のようなもの f:id:yusuke_ujitoko:20180127003558p:plain