ハロー!!きんいろ deep learning モザイク

MikuHatsune2015-05-05

注意:この記事は大好評放送中のハロー!!きんいろモザイクと最近話題のDeep learning をかぶせて話題沸騰!!にしたかったけれども、きんいろモザイクに出ている声優のサンプルボイス(東山奈央)が入手できず、DNNについても結局実装が間に合わずにrandom forestとか多項ロジスティック回帰でごまかしてるじゃんェ…と思ったらなんとかDNNできたので半分タイトル詐欺です。
 
感動した。
ご注文はDeep Learningですか? - kivantium活動日記
ここではOpenCV を用いて顔認識をして、そのデータをDNNに流して主要キャラ+その他判定をしている。
ならば、声優統計を修める者としては、音声解析の技術を用いて
誰が今歌っているのかを識別したい。
これをDNNの技術を用いてやってみる。
 
やり方としては、
サンプルボイスの収集→統計量の作成→学習→学習器の性能評価→推定→動画作成
という感じでやる。
 
サンプルボイスの収集
きんいろモザイクのキャストは西明日香田中真奈美種田梨沙内山夕実東山奈央の5人だが、東山奈央のサンプルボイスがなかったので、代替案としてご注文はうさぎですか?の水瀬いのり佐倉綾音種田梨沙佐藤聡美内田真礼のサンプルボイスを集めた。
BGMがないものを採用し、無音領域は適当にカットした。
 
統計量の作成
とりあえずメルケプストラムを採用した。12次元とってきて、時間変化の動的特徴量と合わせて1フレーム 10 msec あたり24次元のデータが入力となる。
 
学習
とりあえずRでも使えるh2oを使ってみる。パラメータの調整はi5, 8GB RAM のレッツノートでは全くうまく行かなかったよ…
入力データは5人のメインキャラ+その他の6ラベルが50000ずつのフレームとした。
その他というのはBGMや他の声優の声ということで、前回の解析で使ったボイス集からサンプリングした。
計算機資源のしょぼさを反省しており、現在はGPUPCを使えるように申請中で、TheanoCaffeを使ってさらに高速な感じでできればいいなと思う。

# Deep learning 
library(h2o)
# deep learning のための接続
localH2O <- h2o.init(ip = "localhost", port = 54321, startH2O = TRUE, nthreads=-1)
# dat_tr は1列目がラベル、2列目以降が入力ベクトルのひらすらでかいデータ
# 読み込み
h2o_tr <- h2o.importFile(localH2O, path = "dl_train_mel.csv")
h2o_ts <- h2o.importFile(localH2O, path = "dl_test_mel.csv")
df_tr <- as.data.frame(h2o_tr)
df_ts <- as.data.frame(h2o_ts)
# これくらいが計算時間の限界
dl <- h2o.deeplearning(x = 2:ncol(df_tr), y = 1, data = h2o_tr, activation = "Tanh", hidden=rep(1000, 3), epochs = 5, rate=0.01)

pred.dl <- h2o.predict(object=dl, newdata=h2o_ts)
p <- as.data.frame(pred.dl)$predict
table(p)
v <- as.data.frame(h2o_ts$V1)$V1
mat <- table(v, p)
sum(diag(mat))/length(v)

 
学習器の性能評価
テストデータは6ラベル10000ずつのフレームを用意した。とりあえず同じように分類できたかを割合で出すということで。
結果は70%くらいの分類能だった。

	Minase_Inori	Sakura_Ayane	Taneda_Risa	Sato_Satomi	Uchida_Maaya	Other
Minase_Inori	9655	72	29	33	11	59
Sakura_Ayane	64	9478	97	5	168	169
Taneda_Risa	194	323	8999	42	99	297
Sato_Satomi	292	59	24	9425	8	72
Uchida_Maaya	91	814	191	21	8532	305
Other	6829	14666	9350	1451	4508	50331

 
推定
predict するだけ。10 msec ごとに推定されて、最も確率の高いキャラが推定される。混声の場合はそれぞれの確率で混合されていると考えると
初っ端から圧倒的種田率()

predict	Minase_Inori	Sakura_Ayane	Taneda_Risa	Uchida_Maaya	Sato_Satomi	Other
Taneda_Risa	0.0188608989	0.1630974859	0.8161082864	0.000003831	0.000003944	0.0019255573
Taneda_Risa	0.0069014449	0.0141900461	0.9757663012	4.78813080917462E-006	0.000431119	0.0027063172
Taneda_Risa	0.0010154428	0.2572121322	0.7388253808	2.58052859862801E-005	0.0002405713	0.0026806884
Sakura_Ayane	0.0272961222	0.9442297816	0.0271465927	4.11001019529067E-005	0.0002363344	0.0010500529
Taneda_Risa	0.1239169091	0.0305609833	0.8443024158	3.30391935676744E-006	3.27450834447518E-006	0.0012130789
Taneda_Risa	0.0202784631	0.3523899317	0.6195970774	0.000002033	3.84907465900142E-008	0.007732505
Taneda_Risa	0.0186102167	0.0161491111	0.9485545754	1.07119058156968E-006	2.20463178379759E-007	0.0166848917
Taneda_Risa	0.0865577981	0.1521711946	0.6512304544	1.5910183719825E-005	9.73642386270512E-007	0.1100237072
Taneda_Risa	0.0029677181	0.0482471623	0.9420395494	9.03342788660666E-006	5.52017036170582E-006	0.0067310599

 
動画作成
Rではanimation パッケージの saveVideo でできて、1フレームあたりの秒数と拡張子指定で時間がかかるけれども完成する。
これ自体には音楽がついていないので、適当な動画編集ソフトで音楽をつける。
ちなみにキャラ画像は公式HPのツイッターからパクってくる。その他は不純物シノにした。

library(png)
library(jpeg)
library(animation)
pngs <- list.files("/cv/pic/", pattern="jpg") # パクってきたツイッター画像を適当にラベル付けしていれておく
pics <- mapply(function(x) readJPEG(x, native=TRUE), pngs)
ra <- 1 #原点に近いところが潰れるので拡大したかったけど、等倍でやった。
xy0 <- sapply(pics, dim)[1:2, ] #pixel
xy0[] <- min(xy0)
xy0[2,] <- 1000 # なんか横が潰れるのでテコ入れ
rownames(xy0) <- c("height", "width")
s0 <- 0.001 #拡大縮小率
cols <- c("pink", "lightblue", "violet", "yellow", "lightgreen", "black") # キャラの色

saveVideo({
	ani.options(interval = 0.01, nmax = nrow(dat))
	for(j in seq(nrow(dat))){
	b0 <- barplot(unlist(dat[j,-1]), ylim=c(0, 1), col=cols, axisname=FALSE, las=1)
	pa <- par()$usr
	text(pa[2], pa[4]+0.02, paste(round(j*0.01), "sec"), pos=2, xpd=TRUE, cex=2)
	lay0 <- cbind(b0, pa[3]-0.09)
		for(i in seq(pics)){
			xleft=lay0[i, 1]*ra - xy0[2, i]/2*s0
			ybottom=lay0[i, 2]*ra - xy0[1, i]/2*s0
			xright=lay0[i, 1]*ra + xy0[2, i]/2*s0
			ytop=lay0[i, 2]*ra + xy0[1, i]/2*s0
			rasterImage(image=pics[[i]], xleft=xleft, ybottom=ybottom, xright=xright, ytop=ytop, xpd=TRUE)
		}
	}
}, video.name = "BM.mp4", other.opts = "-b 300k")  # higher bitrate, better quality

動画をアップロードするのは非常にめんどくさかったので時系列プロットにする。

dat <- read.csv("predicted.csv")
cvnames <- c("佐倉綾音", "水瀬いのり", "種田梨沙", "内田真礼", "佐藤聡美", "その他")
cols <- c("pink", "lightblue", "violet", "yellow", "lightgreen", "black")
par(mfrow=c(6, 1), mar=c(2, 4, 2, 2), cex.lab=1.2)
for(i in 2:7){
	plot(dat[, i], type="l", col=cols[i-1], xaxt="n", xlab="", ylab="Probability", ylim=c(0, 1), las=1)
	title(cvnames[i-1])
	axis(1, at=seq(0, 9000, length=10), labels=seq(0, 90, length=10))
}

 
完成品がこれ。
h2oの実験の段階から、推定がすべて種田梨沙だったり水瀬いのりになったりでものすごい汎化性能が悪く、入力データでそこそこ精度が高くなってもやっぱりテストデータで汎化性能が悪かった。歌のほとんどが内田真礼かその他で占められているし、佐藤聡美にいたっては1秒以下しかない。
その他のラベルを抜いて同様にやってみたが、まったく改善しなかった。

 
改善策としては、DNNは特徴量の抽出も行えるというのが売りのひとつになっているから、特徴量として入力したメルケプストラムはたぶんよろしくなかった。というのも、メルケプストラムは人間の耳の特性に非常によく似た、母音の周波数を表すとかなんとかがよくある説明らしく、既に完成(?)された特徴量を入力にしたのはあまりよろしくなかったかもしれない。MFCC features are not suitable. とはこちらでも言われている。というわけで、改善策その1としてはメルケプストラムになる前の、FFTしたくらいのデータでやるのはどうか。FFTしたくらいであれば、周波数はある程度特徴的なピークがとれて、なおかつ2000次元くらい取れるから適当にDNNに放り込んだらなんとかしてくれるっしょ(適当。メルフィルタバンクくらいの処理はかませてもいいか。
 
もうひとつ、OPのBGMは分離すべきだったかもしれない。独立成分分析はやったけれども、これも前回同様うまく分離できなかった。と思っていたらdeep karaoke(論文)という、いわゆるカクテルパーティ効果をDNNでやりましょう、という話があったのでこれを試して、BGMによるノイズを減らすのも手だろう。
MEDLEYDBという楽曲学習器もあるので、これを使えばたぶん早い。
 

library(tuneR)
library(seewave)
library(sound)
library(dtt)
library(fastICA)
library(phonTools)
library(e1071)
wd1 <- "/cv/cv_deep/"  # ごちうさ声優のサンプルボイス
wd2 <- "/cv/original/" # その他声優のサンプルボイス
f1 <- list.files(wd1, pattern="wav")
f2 <- list.files(wd2, pattern="wav")
cv_gochiusa <- unique(mapply(function(x) x[1], strsplit(f1, "_silence_")))

wav1 <- mapply(readWave, paste(wd1, f1, sep=""))
wav2 <- mapply(readWave, paste(wd2, f2, sep=""))

fs <- 44100
msec <- 0.05 # サンプリングの長さ
niter <- 10000 # 作成するデータ数
n_frm <- 8 # フォルマント
# ごちうさ声優のデータを集める
res_mel <- res_frm <- NULL
for(cv in seq(cv_gochiusa)){
	pb <- txtProgressBar(max=niter, style=3)
	for(n in seq(niter)){
	setTxtProgressBar(pb, n)
	tmp_mel <- tmp_frm <- NULL
	i <- sample(grep(cv_gochiusa[cv], f1), size=1)
	r <- rle(wav1[[i]]@left > 100) # 無音っぽいところは省きたい
	lidx <- cumsum(r$lengths)[r$lengths < msec*fs] # 
	if(length(lidx) > 1){
		cutpoint <- sample(head(lidx, -1), size=1)
		tmp_w <- extractWave(wav1[[i]], from=cutpoint, to=cutpoint+msec*fs)
		m0 <- try(melfcc(tmp_w, wintime=0.01, spec_out=TRUE), silent=TRUE)
			if(class(m0) != "try-error"){
				dc <- delta_cepstrum(m0$cepstra, dd=5)
				tmp_mel <- rbind(tmp_mel, cbind(m0$cepstra, dc))
				tmp_mel <- as.data.frame(cbind(cv_gochiusa[cv], tmp_mel))
				res_mel <- rbind(res_mel, tmp_mel)
			}
			frm <- findformants(tmp_w@left, fs=fs, verify=FALSE) # フォルマント抽出
			if(length(frm$formant) >= n_frm){
				tmp_frm <- rbind(tmp_frm, head(frm$formant, n_frm))
				tmp_frm <- as.data.frame(cbind(cv_gochiusa[cv], tmp_frm))
				res_frm <- rbind(res_frm, tmp_frm)
			}
		}
		#print(n)
	}
}
write.csv(res_frm, "gochiusa_frm.csv")
write.csv(res_mel, "gochiusa_mel.csv")


# 別声優データを作る
# ごちうさ声優のデータを集める
res_mel <- res_frm <- NULL
pb <- txtProgressBar(max=niter, style=3)
for(n in seq(niter)){
	setTxtProgressBar(pb, n)
	tmp_mel <- tmp_frm <- NULL
	i <- sample(seq(wav2), size=1)
	r <- rle(wav2[[i]]@left > 100) # 無音っぽいところは省きたい
	lidx <- cumsum(r$lengths)[r$lengths < msec*fs] # 
	if(length(lidx) > 1){
		cutpoint <- sample(head(lidx, -1), size=1)
		tmp_w <- extractWave(wav2[[i]], from=cutpoint, to=cutpoint+msec*fs)
		m0 <- try(melfcc(tmp_w, wintime=0.01, spec_out=TRUE), silent=TRUE)
			if(class(m0) != "try-error"){
				dc <- delta_cepstrum(m0$cepstra, dd=5)
				tmp_mel <- rbind(tmp_mel, cbind(m0$cepstra, dc))
				tmp_mel <- as.data.frame(cbind("Other", tmp_mel))
				res_mel <- rbind(res_mel, tmp_mel)
			}
			frm <- findformants(tmp_w@left, fs=fs, verify=FALSE) # フォルマント抽出
			if(length(frm$formant) >= n_frm){
				tmp_frm <- rbind(tmp_frm, head(frm$formant, n_frm))
				tmp_frm <- as.data.frame(cbind("Other", tmp_frm))
				res_frm <- rbind(res_frm, tmp_frm)
			}
		}
	}
write.csv(res_mel, "other_mel.csv")
write.csv(res_frm, "other_frm.csv")

# melfcc のcepstra 行列をぶち込む形式
# dd は前後いくつ取るか
delta_cepstrum <- function(mat, dd=2){
	res <- mat
	dat1 <- mat[c(rep(1, dd) ,seq(nrow(mat)), rep(nrow(mat), dd)), ] 
	x <- seq(2*dd + 1)
	for(j in seq(ncol(dat1))){
		for(i in (dd+1):(nrow(dat1)-dd)){
			y <- dat1[(i-dd):(i+dd), j]
			lm1 <- lm(y ~ x)
			res[i-dd, j] <- lm1$coefficients[2]
		}
	}
	return(res)
}