WaveNetメモ

WaveNetのArXiv原稿を読んだのでメモ

概要
生の音声波形の生成を行うDNN
自己回帰型モデル
前の全ての波形サンプルから次のサンプルを予測するための予測分布を持つ(自己回帰型なので)(途中の波形の履歴から次のデータを生成できる、くらいの意味)
10,000サンプル/secの音声波形を使った
TTSに適用すると、SOTAの性能。評価方法は人間による音声の自然性の主観評価、比べた相手はEnglish,Chinese両言語についての一番良いパラメトリックでいくつもの手法を組み合わせた手法
単体のWaveNetは多くの異なる話者の特徴を真に迫ってとらえている
Speakerのidentity(というパラメータ?)で条件づけることで対応する話者をスイッチ可能
音楽に適用すると、最新のすごくリアルな音楽の断片を生成できた
WaveNetは識別モデルにも適用可能であることを示し、音声認識でよい結果を出した

Introduction
PixelCNNdecoderやPixelRNNは良い生画像を生成できる。でも64x64=4096次元
特にPixelCNNに注目、これは音声(16,000 sample/sec)のような長い系列データでもうまく生成できるのか
この論文の貢献
・WaveNetを使って今までにない自然性を持つ音声の生成が可能なTTSを発表、人手評価による
・長期的な系列を扱うためにDilated causal convolutions(超大きな受容野を持つ)をもとにした新しいアーキテクチャを提案
・話者ごとに違った音声(声)を生成可能
・同じアーキテクチャで小さな音声認識データセットを使っても、強い結果を得た。また、音楽のような違う音声モダリティ(というかドメイン?)も生成できることを示した

アーキテクチャ
PixcelCNNから
条件確率分布はstack of convolutional layersでモデル化されている
プーリング層がない、出力は入力と同じ時間次元数(?)を持つ
出力はsoftmax層を使ったカテゴリ分布(カテゴリは次の値なのでx_tの数値のとりうる値だけある(量子化してるはずだがいくつか分からない))
最尤推定→扱いやすくてoverfitting, underfittingがみやすいから

Dilated causal convolutions
Stack of causal convolutional layers→図2
これを使うときちんと時系列を考慮して計算ができる(前の系列が後の系列に依存して生成されるというようなことがない)
Causal convolutionは[Oord 2016a]のmasked convolutionと等価→調べる
→あらかじめマスクテンソルとその乗算を要素ごとに、convoultion kernelについて行う
音声の場合次元が1次元になるのでいくらか実装が簡単

訓練中、条件付き分布はground truthを知っているので並列化可能(生成時はもちろん無理)

Causal convolutions は再帰結合をもたないのでRNNを訓練するより訓練時間が短くて済む
問題は多くの層を必要とすること、受容野を増やすため大きなフィルターが必要ということ(つまり、メモリをバカ食いする)
図2だと受容野は5つ(=5層+filter length 1 -1)filter lengthとは?
この論文では受容野のオーダーを大きくしつつ、計算コスト(時間もメモリも)を少なく抑えるためにdilated convolutionsを使った

Dilated convolution(図3)は要は結合の飛ばしを入れた形
以前にも信号処理や画像セグメンテーションで使われている手法
Dilationは1層上がるごとに2倍増えてくようにする(理由は不明)
1,2,4,8,…,512,1,2,4,8,…,512,1,2,…
↑2分木の形にすると層を深くできないから?

Softmax distributions
画像も256値のsoftmax層を用いた多クラス分類とするとうまくいくことが報告されたので
音声でもsoftmax予測をする。音声は16bitでサンプルされてるので、216=65,536値の多クラス分類をする
でももっと扱いやすいように、μ-law companding transformation(?)をデータに適用して256値にした
どういう経緯でこれが出てきたかは不明

Conditional wavenets
条件付きに拡張
話者ごとのスイッチ、TTSにおけるテキスト入力が該当
全ての時間について入力は固定
TTSのパラメータ(時系列)を使いたい場合→transposed convolutional networkでupsamplingしてから音声信号に時系列のスケールを合わせる
Transposed convolutionせずにむりやり利用してもいいが、若干劣る

Experiment
音声生成
English multi-speaker corpus (CSTR vice cloning toolkit (VCTK))を使用
WaveNetは話者の条件付けバージョン→speaker ID のone-hot vector
109人からの44時間分のデータセット
テキストで条件づけてないのでしゃべってる内容はわからないけど人間っぽい話声の単語の羅列がスムーズにでてくる
しかしCloser inspection(近い検査?元のデータ?)に対し明らかに不自然な部分がある
→これはモデルの受容野サイズが制限されているから(約300msec)つまり、前の2-3の音素しかちゃんと思い出せないということ
話者の違いはちゃんと学習できていた(評価は?)
声だけでなく、acoustics, recording quality, breathing, mouth movementsの特徴もとらえていた(だから評価は?)

TTS
同じ単一話者のデータベース(GogleのNorth American EnglishとMandarin Chinese) のTTSをつくった?
前者は24.6時間の音声データ、後者は34.8時間のデータ。どちらもプロの女性話者
WaveNetは局所的にテキストから抽出された言語特徴(詳細不明)とlogF_0で条件付けして訓練した
外部モデルが言語特徴から予測したlogF_0とphone durationsはそれぞれの言語で訓練に使った
受容野のサイズは240 msec

ベースライン
HMM-driven unist selection concatenative [Gonzalvo 2016]
LSTM-RNN-based statistical parametric [Zen 2016]
使うデータセット、言語特徴量は同じ。

評価
主観のペア比較(A/B test?)2つの音源から好きな方を選ぶ、好みがなければ自然な方を選ぶ
Mean opinion score(MOS) 自然性を5段階のリッカート尺度で評価

図5はペア比較の結果
一番上はベースライン2つ
真ん中はWaveNet2つ
一番下は上2つの決勝戦(ただし、bestベースラインはサンプルごとに上のもの逐一とってきてるものと思われる)
WaveNet:言語特徴のみの条件付けだと自然なクオリティはあるが、韻律がおかしいところがあった(強調しないはずの単語を強調していて若干不自然)
これはおそらくF_0の高さの長期的な依存をとらえられてないのが原因
受容野のサイズが240 msec分と短いのがこの原因
F_0も条件付けにいれた場合はこの問題が起きないので良くなっている。
外部のF_0予測モデルは200Hzという低周波数で動いているので、F_0のうち長期のスケールを持つ周波数帯も学習できたといえる

表1はMOSの結果(高いほど良い)
WaveNetが最良の結果

音楽
The MagnaTagAtune dataset
200時間の音楽、29秒ごとのクリップに188種類のうち該当するタグがついている(ジャンル、instrumentation, tempo, volume, mood of the music
YouTube pianoデータセット
60時間のピアノソロ動画、単一の楽器のみなのでモデル化は簡単だと思われる

定量的な評価は難しいが、主観評価は可能
受容野を大きくするのが超重要
数秒の受容野をもってしても、ジャンルの別や楽器の種類、音量、音声の質を学習したりできなかった
しかし、生成したサンプルはしばしば調和的で審美的に良かった。条件付けがなくても同様
MagnaTagATuneは比較的タグにノイズがあったり、省略が多かったり(タグデータの欠損?)したので、クリーニングして、似ているタグは一元化してやると比較的良い結果を得られたことを確認した

音声認識
TIMITデータセット
Dilated convolutionの後ろにmean-pooling層を入れた
荒いフレーム(10 msecのスパン)(160 x dounsampling)→スムージングみたいなイメージ
pooling層の後ろはnon-causal convolutions(普通のconvolution?後ろの時間にも依存して出力?)
2つのロス関数、一つは次のサンプルの予測(分類)、もう一つはフレームの音素のクラス分類
モデルは単一のロス関数にするよりも汎化性能がよかった
Testsetに対して18.8 PER(phone error rate)
これはTIMITコーパスにおいて波形から直接学習したモデルで著者らの知る限りベストスコア

PILで画像をロードできないときに確認すること

PILで画像をロードするとき、しばしば画像がロードできない現象が起きていたが、解決した。

stackoverflow.com

stackoverflow.com

自分のケースも

IOError: image file is truncated (0 bytes not processed)

というエラーだったので、おそらくこれで間違いない。

解決法としては、

from PIL import ImageFile
ImageFile.LOAD_TRUNCATED_IMAGES = True

を追加するだけで良いらしい。

どうやらPILは極端に大きな画像など高速にロードできない画像はロードしないで見過ごす仕様になっているらしい。
デフォルトでFalseになっているLOAD_TRUNCATED_IMAGESをTrueにすることできちんとロードするようになった。

いままで「え?なんで他の人は普通にロードできるの?俺だけ?」と思っていたものだが、これで一つ懸念が解決して良かった。
やっぱ先人のコードを読むのは大事

pythonのfilter関数

pythonのビルトイン関数にfilterというのがある。これはリストに対して条件に合致する要素をとってくることができる。よくネットで見るのは、

>>> lis=["apple","grape","banana","apple pie"]
>>> filter(lambda x:x=="apple",lis)
['apple']

ところが、pythonのバージョンによってはどうやらオブジェクトで返ってくる場合があるようである。

>>> lis=["apple","grape","banana","apple pie"]
>>> filter(lambda x:x=="apple",lis)
<filter object at 0x7fd0dd679860>

>>> list(filter(lambda x:x=="apple",lis))
['apple']

少し試してみた感じ、たぶん上がpython2で下がpython3?
思わぬところで注意が必要なようだ。

そもそも、filter関数を使わずに

>>> lis=["apple","grape","banana","apple pie"]
>>> [word for word in lis if word=="apple"]
['apple']

とした方が、文字列の部分一致だとかリストのindex返したりだとか色々応用が利きそうな気もする。

chainer.functionsの関数の色々

ChainerはVariableに変数を渡してforwardパス、backwardパスを計算するので、パス内に特殊な操作が入ると計算ができなくなる、と困るのでいくつかchainer.functionsに便利な関数が用意されていた。ipython起動してchainer.functionsをインポートしてからdir(chainer.functions)とかで一度関数の一覧を見てみると結構役に立ちそうな関数があったりする。

今回は主に配列や行列操作でお世話になったので、それらをメモとしてまとめておく。

以下、

from chainer import functions as F

とする。

  1. F.reshape(shape) numpy.reshapeのVariable版。

  2. F.broadcast_to(x,shape)
    numpy.broadcast_toのVariable版。配列や行列のxをshape(タプル)に拡張してくれる。

  3. F.swapaxes(x,a,b)
    numpy.swapaxesのVariable版。aの次元とbの次元の数が逆になるように行列を操作してくれる。

  4. F.transpose(x)
    numpy.transposeのVariable版。

  5. F.expand_dims(x,axis)
    axisで指定した次元を新たに挿入する。axis=0でVariable([x])と同じことをしてくれる(Variableを呼び出すと再定義になってbackwardできなくなってしまうのでこれを使う)

  6. F.split_axis(x,split_size,axis)
    axisで指定した次元についてsplit_sizeコの配列or行列になるように分割してくれる。分割してできたVariable達はタプルで返される。

  7. F.concat(x,axis)
    axisで指定した次元について配列もしくは行列を結合する。Mコの2次元配列を組み合わせて3次元配列をつくる場合、F.expand_dims(hoge,0)としてxを3次元配列にしてから使わないとエラーが起こる

  8. F.matmul(a,b,transa=False,transb=False) numpy.dot,numpy.tensordotのVariable版。何気に転置してくれるtransa,transbがついている。別にF.transposeしても同じだと思うけど、せっかくなので使ってる。

  9. F.batch_matmul(x,y) F.matmulのバッチ付き版。例えば、3次元配列X(MxAxB)とY(MxBxC)があるとすると、以下と同じ計算を効率的にやってくれている。

Z=[]
for i in range(M):
    temp=F.matmul(X[i],Y[i])
    Z.append(F.expand_dims(temp,0))
Z=F.concat(Z,0)

for文を使わなくて良くなるのが助かる。中身はnumpy.tensordotとか使ってForwardとBackwardを定義している。

ソースコードを読んだら自分でもchainerのfunctionを書いてみたくなった。

value値がidの場合のkeyのとり方

chainerなどで言語処理を行うときには単語をidに変換して使うため、単語→idの関係を格納する辞書をvocabularyとして用いる。そのため、単語や文章を生成したいという場合にはid→単語に直す作業が必要になる。

逆引きの辞書をつくる手もあるが、idはint型なのでkeyにするためにstrに変換するのはちょっとな・・・と思うので別の方法をとる。idは単純に単語の番号なのでdictのkeyをid(value)から呼び出すにはkeyをリスト化してやればよい。例として以下のような場合を考える。

id_dict = {"love":1,"i":2,"you":3} #vocabulary例
output = [2,1,3] #目標は「i love you」を出力すること

このとき、出力outputを単語に変換するには以下のようにリスト化を行えばよい。word_listがリスト化によってできたリストだ。

word_list = [0]*(len(id_dict)+1)      #リスト初期化 word_list[0]はout of vocabulary用にとっておく  
for key,value in id_dict.items(): #dictの各要素の呼び出し
    word_list[value] = key      #word_listの番号に注意

唯一注意点はword_listのインデックスをidと合わせることである。特に文頭文末記号を入れたり、id=0を未知語に割り当てたりするとずれることが往々にしてある。

words = []
for id_ in output:
    words.append(word_list[id_])

print(" ".join(words))
# 'i love you'

サイズの違う画像の扱い

reddit machine learningから、学習に使うときの画像の取り扱い方を独断と偏見によりまとめた。

www.reddit.com

方法(大体3通り)
1. 大きい画像に合わせて小さい画像の周りを0(黒)もしくは何らかの計算をした値で埋める
2. 小さい画像に合わせて大きい画像の周りを切り取る
3. 画像をリサイズして合わせる

表出する問題
1. 埋めたピクセル(黒とか)が画像全体の認識に影響を与える可能性がある
2. 切り取られた部分のアノテーションは考慮されなくなる
3. 引き伸ばし過ぎ、縮め過ぎると自然な画像では無くなってしまう(歪む、ぼやける)

どれが一番いいのかはぶっちゃけよく分かってない。
ただ、1.は元の画像に異物を混入しているわけで、あまり使いたくはないというイメージがある。

基本的に端に検出が必要なアノテーションなど無ければ画像の周りを適当な大きさに切り取って使えば良いだろう。

しかし、もしそうでなければリサイズかピクセルで埋め立てをする必要があるだろう。どちらも画像の縦横比が比較的1:1に近いかつ他の画像と極端に大きさが違わない場合は問題ないだろうが、ここらの使い分けってどうなってるのだろう?