3D Deep Learning について学ぶ - pytorch3d

はじめに

3D オブジェクトを扱うDeep Learning技術について知りたいと
思ったので、pytorch3d1チュートリアルを行った。

概要

1次元的なデータを取り扱う自然言語処理や音声信号処理、
2次元的なデータを取り扱う画像処理だけでなく、
3Dオブジェクトを取り扱う Deep Learning アルゴリズム
近年多く提案されている。

今回は3Dオブジェクトを取り扱う処理がまとめられている、
pytorch3dライブラリのチュートリアルを行うことで3D Deep Learningに触れてみる。

pytorch3d 2

pytorch3dはpytorchをベースに3D Deep Learningタスクにおいて、
必要な処理が実装、最適化されているライブラリである。

  • メッシュ・テクスチャの入出力、汎用処理
  • 微分可能なrenderer
  • 損失関数

などを提供している。

カメラ位置最適化 (チュートリアル)

Camera position optimizationチュートリアルをやってみる。

このチュートリアルでは、ティーポットオブジェクトに対して、
カメラの撮影場所をrendererによって出力される画像から
最適化するというものである。

流れとしては

  1. 目標となるカメラ位置からティーポット画像を撮影する(目標画像)。
  2. 適当な位置にカメラを置く。
  3. 適当な位置からティーポット画像を撮影して目標画像との誤差を計算。
  4. 誤差を減らすようにbackpropagationでカメラ位置を調整

となる。

pytorch3dは微分可能なrendererが実装されているので
このようなこともできるよ、というチュートリアルだと思う。
(実用的には画像の美しさを定量化して、 それを最大化する
カメラ位置を求める、とかに利用できなくもない?)

環境設定

Google Colab上で検証した。

環境設定・モジュールimport

!pip install torch torchvision
!pip install 'git+https://github.com/facebookresearch/pytorch3d.git@stable'
:
import os
import torch
import numpy as np
from tqdm.notebook import tqdm
import imageio
import torch.nn as nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
from skimage import img_as_ubyte

# io utils
from pytorch3d.io import load_obj

# datastructures
from pytorch3d.structures import Meshes, Textures

# 3D transformations functions
from pytorch3d.transforms import Rotate, Translate

# rendering components
from pytorch3d.renderer import (
    OpenGLPerspectiveCameras, look_at_view_transform, look_at_rotation, 
    RasterizationSettings, MeshRenderer, MeshRasterizer, BlendParams,
    SoftSilhouetteShader, HardPhongShader, PointLights
)

データ読み込み

!mkdir -p data
!wget -P data https://dl.fbaipublicfiles.com/pytorch3d/data/teapot/teapot.obj

# Set the cuda device 
if torch.cuda.is_available():
    device = torch.device("cuda:0")
    torch.cuda.set_device(device)
else:
    device = torch.device("cpu")

# Load the obj and ignore the textures and materials.
verts, faces_idx, _ = load_obj("./data/teapot.obj")
faces = faces_idx.verts_idx

# Initialize each vertex to be white in color.
verts_rgb = torch.ones_like(verts)[None]  # (1, V, 3)
textures = Textures(verts_rgb=verts_rgb.to(device))

# Create a Meshes object for the teapot. Here we have only one mesh in the batch.
teapot_mesh = Meshes(
    verts=[verts.to(device)],   
    faces=[faces.to(device)], 
    textures=textures
)

obj形式のファイルを読み込み、頂点情報(verts)と面情報(faces_idx)を得る。
その後頂点に対応するテクスチャをそれぞれ1で初期化し、
3Dオブジェクトに対応するMeshesオブジェクトを構成している。
pytorch3dではこのMeshesオブジェクトを起点に様々な処理を行う。

rendererの設定

rendererの設定

# Initialize an OpenGL perspective camera.
cameras = OpenGLPerspectiveCameras(device=device)

# To blend the 100 faces we set a few parameters which control the opacity and the sharpness of 
# edges. Refer to blending.py for more details. 
blend_params = BlendParams(sigma=1e-4, gamma=1e-4)

# Define the settings for rasterization and shading. Here we set the output image to be of size
# 256x256. To form the blended image we use 100 faces for each pixel. We also set bin_size and max_faces_per_bin to None which ensure that 
# the faster coarse-to-fine rasterization method is used. Refer to rasterize_meshes.py for 
# explanations of these parameters. Refer to docs/notes/renderer.md for an explanation of 
# the difference between naive and coarse-to-fine rasterization. 
raster_settings = RasterizationSettings(
    image_size=256, 
    blur_radius=np.log(1. / 1e-4 - 1.) * blend_params.sigma, 
    faces_per_pixel=100, 
)

# Create a silhouette mesh renderer by composing a rasterizer and a shader. 
silhouette_renderer = MeshRenderer(
    rasterizer=MeshRasterizer(
        cameras=cameras, 
        raster_settings=raster_settings
    ),
    shader=SoftSilhouetteShader(blend_params=blend_params)
)


# We will also create a phong renderer. This is simpler and only needs to render one face per pixel.
raster_settings = RasterizationSettings(
    image_size=256, 
    blur_radius=0.0, 
    faces_per_pixel=1, 
)
# We can add a point light in front of the object. 
lights = PointLights(device=device, location=((2.0, 2.0, -2.0),))
phong_renderer = MeshRenderer(
    rasterizer=MeshRasterizer(
        cameras=cameras, 
        raster_settings=raster_settings
    ),
    shader=HardPhongShader(device=device, cameras=cameras, lights=lights)
)

カメラ、ライト、シェーダーなどからrendererを設定している。
ティーポットのシルエットのみを表示する silhouette_renderer と
陰影情報も表示するphong_rendererの2つのrendererを用意している。

描画

# Select the viewpoint using spherical angles  
distance = 3   # distance from camera to the object
elevation = 50.0   # angle of elevation in degrees
azimuth = 0.0  # No rotation so the camera is positioned on the +Z axis. 

# Get the position of the camera based on the spherical angles
R, T = look_at_view_transform(distance, elevation, azimuth, device=device)

# Render the teapot providing the values of R and T. 
silhouete = silhouette_renderer(meshes_world=teapot_mesh, R=R, T=T)
image_ref = phong_renderer(meshes_world=teapot_mesh, R=R, T=T)

silhouete = silhouete.cpu().numpy()
image_ref = image_ref.cpu().numpy()

plt.figure(figsize=(10, 10))
plt.subplot(1, 2, 1)
plt.imshow(silhouete.squeeze()[..., 3])  # only plot the alpha channel of the RGBA image
plt.grid(False)
plt.subplot(1, 2, 2)
plt.imshow(image_ref.squeeze())
plt.grid(False)

f:id:nakamrnk:20200721113842p:plain

左: silhouette_renderer(matplotlibにより着色) , 右: phong_rendererの描画結果

上の画像を目標としてカメラ位置を動かすモデルを構築する。

モデルと損失関数

class Model(nn.Module):
    def __init__(self, meshes, renderer, image_ref):
        super().__init__()
        self.meshes = meshes
        self.device = meshes.device
        self.renderer = renderer
        
        # Get the silhouette of the reference RGB image by finding all the non zero values. 
        image_ref = torch.from_numpy((image_ref[..., :3].max(-1) != 0).astype(np.float32))
        self.register_buffer('image_ref', image_ref)
        
        # Create an optimizable parameter for the x, y, z position of the camera. 
        self.camera_position = nn.Parameter(
            torch.from_numpy(np.array([3.0,  6.9, +2.5], dtype=np.float32)).to(meshes.device))

    def forward(self):
        
        # Render the image using the updated camera position. Based on the new position of the 
        # camer we calculate the rotation and translation matrices
        R = look_at_rotation(self.camera_position[None, :], device=self.device)  # (1, 3, 3)
        T = -torch.bmm(R.transpose(1, 2), self.camera_position[None, :, None])[:, :, 0]   # (1, 3)
        
        image = self.renderer(meshes_world=self.meshes.clone(), R=R, T=T)
        
        # Calculate the silhouette loss
        loss = torch.sum((image[..., 3] - self.image_ref) ** 2)
        return loss, image

# We will save images periodically and compose them into a GIF.
filename_output = "./teapot_optimization_demo.gif"
writer = imageio.get_writer(filename_output, mode='I', duration=0.3)

# Initialize a model using the renderer, mesh and reference image
model = Model(meshes=teapot_mesh, renderer=silhouette_renderer, image_ref=image_ref).to(device)

# Create an optimizer. Here we are using Adam and we pass in the parameters of the model
optimizer = torch.optim.Adam(model.parameters(), lr=0.05)

モデルは最適化するパラメータとしてカメラ位置(camera_position)を持っている。
損失関数は現在のカメラ位置でrenderingした画像と参考画像との二乗誤差であり、
これの最小化を目指す。
位置さえ分かればいいので、出力のうちアルファチャンネルのみを利用している。

学習

loop = tqdm(range(200))
for i in loop:
    optimizer.zero_grad()
    loss, _ = model()
    loss.backward()
    optimizer.step()
    
    loop.set_description('Optimizing (loss %.4f)' % loss.data)
    
    if loss.item() < 200:
        break
    
    # Save outputs to create a GIF. 
    if i % 10 == 0:
        R = look_at_rotation(model.camera_position[None, :], device=model.device)
        T = -torch.bmm(R.transpose(1, 2), model.camera_position[None, :, None])[:, :, 0]   # (1, 3)
        image = phong_renderer(meshes_world=model.meshes.clone(), R=R, T=T)
        image = image[0, ..., :3].detach().squeeze().cpu().numpy()
        image = img_as_ubyte(image)
        writer.append_data(image)
        
        plt.figure()
        plt.imshow(image[..., :3])
        plt.title("iter: %d, loss: %0.2f" % (i, loss.data))
        plt.grid("off")
        plt.axis("off")
    
writer.close()

結果

学習経過ムービー

f:id:nakamrnk:20200721123504g:plain

学習初期に遠くにあったティーポットに徐々にカメラが近づいている。
最終的には元の参考画像に近い位置に落ち着いている。

まとめ

今回はpytorch3dのチュートリアルに軽く触れてみた。
基本的な学習プロセス(前処理、モデルの構築、学習)は
pytorchと変わらないので、比較的触りやすかった。
rendererがbackpropagation可能なところはおもしろく、
目的関数を工夫すればいろいろできそうだと思った。

参考文献

JVSデータによるWaevGlowの再学習

はじめに

前回までJVSコーパス1、JSUTコーパス2を用いて、
Flowtron3を学習してきた。 今回はメルスペクトルグラムから
音声を復元するWaveGlow4部分の再学習を行った。

現状の課題

これまで検討してきたTTS (Text To Speech)システムは、
Flowtroonによるメルスペクトルグラムを生成し、 
WaveGlowによる音声復元を行うというものである。


これまでは主にFlowtron部分の検討を行い、
WaveGlow 側は公開されている5英語で
学習されたモデルをそのまま用い、
それなりに音声復元はできていたのだが、
復元音声と元音声の差異が多少気になってきたので、
再学習することとした。

学習条件

  • 学習データ
    • JVSデータ 95文章 (全話者)
  • 評価データ
    • JVSデータ 5文章 (全話者)

waveglownの学習パラメータはデフォルトのまま変更なし。
公開されている英語の学習済みモデルを初期値として、
fine-tuningした。

結果

NLL比較

スピーカー単位で文章平均のNLLを比較すると下図のようになる。 f:id:nakamrnk:20200717093902j:plain

青線が元の英語で学習したWaveGlowによるNLL。
橙線がfine-tuningしたWaveGlowによるNLL。

すべてのスピーカーにおいてfine-tuningによるNLLの減少が見られた。

話者ごとのNLLのヒストグラムを男女ごとに表示すると以下のようになる。
f:id:nakamrnk:20200717094328j:plain

また、話者間平均のNLLは以下の表のようになる。

NLL base jvs diff
F -5.32574 -7.4118 2.08607
M -5.77699 -7.49901 1.72202

男女問わずNLLが同程度まで減少しているが、女性側のほうが
減少量は大きい。元のWaveGlowは女性話者によるデータセット
であるLSSpeechデータセット6で学習されているはずなのに
女性側のほうがNLLがやや高いのには多少疑問が残る。
(日本語と英語の違いの問題?)

音声比較

特に違いが大きかったいくつかのスピーカーにおいて
fine-tuningによる変化を示す。

文章 : VOICEACTRESS100_098
乾ドックに入渠して、オーバーホールすべきかどうか、パフォーマンスがチェックされた。

JVS009

fine-tuning前

fine-tuning後

fine-tuning後のほうが男性らしい声となっており、
実際のデータにも近く感じた。

JVS015

fine-tuning前

fine-tuning後

fine-tuning前のほうがややかすれが多く、
後のほうが元データに近く感じた。

まとめ

WaveGlowをJVSデータで再学習することで日本語音声に関しては
復元性能が上がっているように感じる。
ただwaveglowはモデルサイズが重いのでもう少し軽くて同じくらい
の性能のモデルがあればいいのにと思った。

参考文献

Flowtron でJVS+JSUTデータを学習

はじめに

前回 Flowtorn1というText To Speechアルゴリズムにより
日本語音声コーパスであるJVSコーパス2の学習を行った。
今回は同じく日本語の音声コーパスである、JSUTコーパス3を追加して
学習を行い性能変化を検証した。

前回の問題点

前回 ある程度Flowtronの学習はできていたが、文章の一部を繰り返してしまう推論結果が多かった。

f:id:nakamrnk:20200715100703p:plain

上段は横軸時間、縦軸メル周波数のログメルスペクトルグラム。
下段は横軸時間、縦軸は入力文章の単語位置によるアテンションマップ。

アテンションマップは入力文章の各位置と各時間の出力の対応を表しており、正しく学習できている場合は、アテンションマップは左下から右上に単調に推移していくが、繰り返しが生じると上図のように折り返した構造を示す。

JSUTコーパス

JVSコーパスは100人の話者がそれぞれ、百数十の文章を発話しているコーパスだが、JSUTコーパスは1人の話者が5000以上の文章を発話しているコーパスである。 そのため、網羅している会話の種類がJVSコーパスよりもはるかに多い。前回の繰り返し問題が会話文章の少なさに起因する場合はこのデータを追加することで性能の向上が期待される。

評価指標について

今回は文章生成時に生じるアテンションマップに繰り返し構造があるかを評価基準とする。

f:id:nakamrnk:20200715102323p:plain

左上:メルスペクトルグラム、右上:アテンション値が0.6以上のプロット 、左下:1層目のアテンション、右下:2層目のアテンション


右上の図は左下のアテンションマップから値が0.6以上の場所を
抜き出したものであり、 これを繰り返しの有無判定に利用する。
左右の赤線はメルスペクトルグラムから推定した発話領域であり、
この領域内で赤丸内のような折り返しがあった場合に折り返しあり
と評価する。 この図では赤丸の箇所で8文字分の折り返しがあるので
スコアは8とする。 繰り返しが全くない場合はスコア0となる。
複数の繰り返しがある場合はそれぞれを加算する。

データ

学習データ

  • JVSデータ
    • parallel100のうち95
  • JSUTデータ
    • basic5000のうち4950

評価データ

  • JSUTデータ
    • basic5000のうち50

前処理は前回と同様。

結果

スコア平均比較

f:id:nakamrnk:20200715173632j:plain

左図は文章ごとのスピーカー平均の繰り返しスコア、右図はスピーカーごとの文章平均の繰り返しスコア

スピーカー平均で見ても、文章平均で見てもJSUTデータを追加することで繰り返しが減っている。

男女間で話者平均を比較すると下表のようになる。

Male_or_Female jvs jvs_jsut
F 3.17176 1.23765
M 2.54122 0.833469

追加したJSUTデータは女性のみの音声データだが、男性側も繰り返しは減っている。
学習する文章数を増やすことで、男女に関係ない文章のエンコーダー部分の性能が向上したためと考えられる。

 繰り返しが大きく改善された文章

以下の文章ではJSUTデータを追加で学習することで繰り返しが大きく減少した。 (7.99 → 1.29)
赤ん坊が浴槽の中で、ぼちゃぼちゃやっていた。

jvs27
JVSのみで学習

f:id:nakamrnk:20200715183433p:plain

JVS+JSUTで学習

f:id:nakamrnk:20200715183752p:plain

jvs49
JVSのみで学習

f:id:nakamrnk:20200715184213p:plain

JVS+JSUTで学習

f:id:nakamrnk:20200715184137p:plain

JVSのみの学習では"ぼちゃぼちゃ"の部分を"ぼちゃぼちゃぼちゃ"と
3回繰り返しているものが見られた。
これ以外にも"ますます"を"ますますます"と発音しているものも
見られたので繰り返し文に対してやや弱い可能性がある。
JSUTを加えると繰り返しは改善されているが、やや声質が変化している
ように感じる。

 性能が低かった文章

文章単位で見た場合にJVSのみで性能が最も低かった文章は
僕等の中国旅行が楽しいものになるといいな。
であった。 (スピーカー平均13.55)。
これはJSUTを加えても比較的性能は低いままであった (スピーカー平均6.87)

jvs01

僕等の中国旅行が楽しいものになるといいな。

JVSのみで学習

f:id:nakamrnk:20200715174907p:plain

JVS+JSUTで学習

f:id:nakamrnk:20200715175441p:plain

読み方が"ぼくとう"になっているのは読み方がMecabの辞書依存であるためである。
JSUTを加えることで繰り返しが緩和されているが、声質が多少変わっているような気がする。
(女性よりになっている?)

jvs72
JVSのみで学習

f:id:nakamrnk:20200715181037p:plain

JVS+JSUTで学習

f:id:nakamrnk:20200715181005p:plain

こちらはあまり声質が変化せずに繰り返しがなくなっている。

まとめ

JVSにJSUTデータを加えて学習した。
前回問題になっていた繰り返しに関しては改善が見られたが、
特に男性話者の声質がJSUTに引っ張られているような感じがする。
今回はJVSとJSUTを1:1の比率で学習させたので、
女性話者のほうが男性話者の約2倍程度となっていることが原因で
あると考えられる。学習時のサンプリング比率を調整することで
ある程度改善できると思われる。

参考文献

FlowtronでJVSデータを学習

はじめに

FlowtronはFlow-baseのText to Speech (TTS)アルゴリズムである1
今回は日本語の複数話者コーパスであるJVSコーパス2によってこの
Flowtronを学習してみた。

Flowtron

TTSは文章を入力として、音声を合成するタスクである。
Tacotron23などが有名である。

Flowtronは以下の特徴を持つ。

  • Flow-baseの自己回帰生成モデル
  • 文章からログメルスペクトルグラムを生成する
  • 非文章情報をある程度制御した生成が可能 

メルスペクトルグラムからの音声合成はwaveglowなど別の
アルゴリズムを用いる必要がある。

Flowtronの最大の特徴はFlowを用いることによる生成文章の制御である。 TTSは文章から音声を合成するが、本来音声とは話者の気分や体調にも依存するため、 同一話者においてもひとつの文章から複数の異なる音声が生成される可能性がある。 Flowtronにおいては、そういった非文章情報が異なる音声は特徴量空間の別の場所に射影されるように学習される。 生成時はFlowによって、その逆を行うことで非文章情報を制御した音声合成が可能となる。


以前検証した通り、Flowtronは生成時に異なるノイズを入力することで微妙にアクセントの異なる文章を生成できる。

JVSコーパス

JVSコーパスは 100の話者からなる日本語の音声コーパスである。

参考:
https://arxiv.org/abs/1908.06248
https://www.slideshare.net/ShinnosukeTakamichi/jvs-180982861

フリーでありながら、合計で30時間もの会話データが収録されている。
今回はこの中で話者間共通の100文章(parallel100)をFlowtronで学習した。

前処理

実装はNVIDIA公式実装を一部修正したものを利用。
また、モデルの初期値として学習済みFlowtronを利用した。4

文章入力の前処理は以下の流れとなっている。

  1. 日本語文章をMecab(neologd辞書)により形態素単位で分割
  2. jaconvでひらがなをカタカナに変換
  3. 句読点をピリオド、カンマに変換
  4. pykakasi でアルファベットに変換

例 :
こんにちは、今日はいい天気です。

konnichiwa , kyoo wa ii tenki desu .

前処理コード

import MeCab
from pykakasi import kakasi
import jaconv

kakasi = kakasi()
kakasi.setMode('K', 'a')
me = MeCab.Tagger ()
conv = kakasi.getConverter()

def convert_romaji(txt):
  arrays = [i[1] if len(i[1]) > 0 else i[0] for i in [t.split("\t") for t in me.parse(txt).split("\n")] if len(i) > 2]
  return " ".join([jaconv.hira2kata(a) for a in arrays if a is not None]).replace("、", ",").replace("。", ".")


def japanese_alphabet(jpn_text):
    kana = convert_romaji(jpn_text)
    kana = conv.do(kana)
    return kana

音声側の前処理はFlowtronのデフォルトをそのまま用いている。

結果

メルスペクトルグラムから音声の合成には公開されている学習済みのwaveglowを用いた。
以降sigmaはFlowtronのノイズ分布の分散を決めるパラメータとする。

話者比較

話者ごとに共通の文章を生成して比較する。
読み上げ文章「こんにちは、今日はいい天気です。」
sigma=0.2で推論。

jvs05

jvs10

jvs21

jvs26

4話者のメルスペクトルグラム f:id:nakamrnk:20200711055454j:plain

話者を判別できる程度には異なる音声が生成できている。
元の話者音声と比較するとある程度近いがやや印象が異なった。
(英語で学習されたwaveglowを復元に利用していることが原因の可能性もある。)

ノイズの分散と非言語情報

Flowtronはノイズと文章、話者情報を入力としてメルスペクトルグラムを生成する。
sigmaパラメータはFlowtronへ入力するノイズの分散を決めるパラメータであり、
この値が小さいほど安定したクオリティの高い文章が生成される。
(GANにおけるtruncationみたいなもの)
一方で、値が大きいとクオリティが低い代わりに多様な文章が生成される。

sigma = 0.8による生成結果

jvs26(seed 1)

jvs26(seed 3)

jvs26(seed 9)

10 seed 走らせて特徴的な3サンプルを選択した。
runごとにアクセントなどの非言語情報が微妙に異なっている。
(最後の音声はかんだような発音となっている。)

1 seedは完全に生成に失敗しており、sigmaを上げると不安定となることがわかる。
(sigma=0.2の場合、10 seed間でほぼばらつきはない。)

これをうまく制御できれば多様な会話ができるシステムが作れるので
おもしろそうだと思った。

話者補間

話者情報はspaker embeddingとして特徴量空間に埋め込まれる。
この特徴量空間を異なるspeakerの間で補間することにより、
中間の音声を求めることができる。

以下は「ありがとう。」という文章をspeaker_embedinngを補間しながら8回 繰り返したものである。(simga = 0.2)

jvs48 → jvs7

jvs54 → jvs67

補間中の音声もそれほど不自然でない音声が生成できている。
(2つのスピーカーの中間として適切かは不明。)

失敗例

現状学習している文章数は話者ごとに100であり、
任意の文章を生成するためには不十分である可能性がある。
ここではうまく生成できなかった文章の例を上げる。

不要な繰り返し

一部の文章で不要な繰り返しが生じる。
結果を確認した限りではこの失敗が最も多い。


痕跡残さず個人情報盗む 新たな手口のサイバー攻撃に注意を。

jvs24

特定単語の発音ミス

話者によらず特定の単語で発音に失敗している。

本気でやれば後悔することはない。

jvs54

これらの失敗が学習データがカバーできていない発音が原因なのか、 under-fittingが原因なのかは不明である。
(英語で学習したモデルをfine-tuningしているのでそれに起因しているかもしれない)

まとめ

今回はJVSデータを使ってFlowtronを学習した。
ある程度多様性を持った音声データ生成ができており、
Flowの特性をうまく使った制御方法ができれば
感情表現などのより汎用的なTTSが行えるのではないかと思った。

参考文献

Anchor-FreeなObject Detection アルゴリズム

はじめに

今回はAnchor-FreeなObject Detectionアルゴリズムについて
ソースコードを読みながら理解する。

Anchor-Free なObject Detection

SSDなどのObject Detectionアルゴリズムは画像にAnchor-Boxと呼ばれる
矩形領域を敷き詰めて、それぞれのAnchor-Boxに対してObjectのクラスや
Anchor-Boxに対するオフセットの予想を行う。このAnchor-Boxのアスペクト比や数などは
ハイパーパラメータとして指定する必要がある。
一方で最近はこのAnchor-Boxの制限を緩和した、Anchor-FreeなDeep Learning手法でも同等以上
の性能となるアルゴリズムが提案されてきている。
今回はその中で以下のアルゴリズムソースコードを読みながら理解する。

CenterNet (Objects as Points)

参考にした実装
https://github.com/xingyizhou/CenterNet

概要

Center Net (Object as Points)はObjectの中心点と
その他の情報をheatmapとして出力する。
heatmapのチャンネル数を変更することでobject detectionや
pose estimationなど様々なタスクに対応可能。

前処理

画像、クラスごとの矩形情報から以下の3つの情報を抽出する。

  1. クラスごとのオブジェクト中心を表すヒートマップ("クラス数"チャンネル)
  2. 各オブジェクトのオフセット
  3. クラス間の共通のオブジェクトサイズを表すヒートマップ(2チャンネル)

targetのヒートマップは元画像からR分の1倍に縮小している(論文ではR=4)。
元のオブジェクト矩形はクラスごとのヒートマップ上でガウシアンカーネルとして付与される。
ガウシアンカーネルのシグマは以下の
https://github.com/xingyizhou/CenterNet/blob/master/src/lib/utils/image.py
gaussian_radius関数のように求めている。

ヒートマップをリサイズしたことにより生じたズレを補正するために
各オブジェクト矩形の中心のオフセットを保持しておく。

オブジェクトのサイズ示す2次元のheat mapも用意する。
元論文ではクラス間でサイズheat mapは共通だが、
上記の実装ではクラスごとに独立なサイズheat mapを利用する
オプションもあるようである。

ネットワーク

ネットワークはHour-Glass, ResNet, DLAなどいくつかのBackboneを試している。
ResnetとDLAは deformable convolution による修正を加えている。
BackBoneを通した後はtransposed_convolutionなどで元画像の4分の1のfeature mapを得て、
3つの出力に対応するheat mapを得る。

損失関数

損失関数は以下の3つ

  1. 各クラスに対応するheat mapへのFocal Loss
  2. heat mapをリサイズしたことに対するズレ(縦横の2チャンネル)の補正(L1 - Loss)
  3. 各オブジェクトのサイズ(幅、高さの2チャンネル)に対するL1-Loss

元論文ではオブジェクトサイズのスケールは行っておらず、損失関数間の係数で
バランス調整を行っている。

推論処理

  1. 各クラスのheat mapからピーク点(8近傍)を求める(上位100点)。
  2. ピーク点のオフセットマップとサイズマップと合わせて矩形を得る。

その他

  • augmentation
    • random flip
    • random scale (0.7~1.3)
    • cropping
    • color jitter
  • optimizerはAdam
  • 特定エポックで学習率を10分の1にする学習率スケジュール

FCOS

参考にした実装
https://github.com/tianzhi0549/FCOS

概要

fully convolutional one-stage object detector (FCOS)はFully ConvolutionalでAnchor-Freeな
One-starge Object Detectorである。 Semantic SegmentationのようにConvolutionのみで
ピクセル単位のマップを出力してからBounding Boxを得る。

前処理

画像とBounding Boxのペアから各特徴量マップに対して同じ空間スケールの以下の3つのマップを用意する

  1. クラスを予想する Classification マップ (クラス数チャンネル)
  2. Bounding Boxを予測するRegressionマップ (4チャンネル)
    • ピクセルが対応するbounding boxの端までの距離(4方向)
    • あるピクセルが異なるBounding Boxで共有されている場合はサイズの小さい方を採用する
  3. center-nessマップ
    • 2のRegressionマップから求めたBounding Boxの中心ほど高くなるようなマップ(0~1)

ネットワーク

BackBoneは通常のObject Detectionで利用されるものと同様(Resnet, ResNeXtなど)。
複数のスケールの特徴マップに対して、共通のHeadモジュールにより
Classification, center-ness, Regressionの予測を行う。
異なるスケールのfeature mapで共通のheadを利用するため、
regression マップに対しては各feature mapの元画像に対応するストライドをかける。

損失関数

以下の3つの損失関数の重み付き和

  1. classification マップに対する Focal Loss
  2. 前景クラスのregressionマップに対する IOU Loss
  3. center-nessに対するBCE Loss

推論処理

  1. Classificationマップに対してcenter-nessマップをかける
  2. Classificationマップで一定確率以上のピクセルに対してregressionマップによりBounding Boxを求める
  3. 予測されたBounding Boxに対してNMSをかける

まとめ

今回Anchor-FreeなObject Detectionをいくつか調査した。
Anchor-baseの手法と比べるとハイパーパラメータが少ないのはいいと思ったが、
計算量的には既存手法とそれほど変わらないようなので、推論傾向にどのような
違いがあり、どのような問題で有利なのか今後理解していきたい。

学習済みFlowtronを試してみる

はじめに

前回Flowtron (Valle et al. 2020)の内容を理解しようとしたが、
今回は公開されている学習済みFlowtronを動かしてみる。

学習済みモデル

Flowtronの学習済みモデルFlowtron LibriTTS を用いた。
複数スピーカーによる音声データセットであるLibriTTSで学習されたモデルである。
このモデルにおいては123スピーカー分のembeddingが学習されていた。

メルスペクトルグラムから音声信号の推論にはwaveglowモデルを用いた。

結果

sigma = 0.4でサンプリングした
"Flowtron is awesome."
という文章に対するいくつかのスピーカーによる音声合成結果を以下に記述する。

speaker_id = 0

f:id:nakamrnk:20200519092523j:plain 左上は推論されたメルスペクトルグラム、右上はwaveglowによって再現された音声波形。
下段は2層あるflow layerのattentionである。
奇数番目と偶数番目で時間的に反転しているためattentionの方向も反転している。

speaker_id = 1

f:id:nakamrnk:20200519092958j:plain

speaker_id = 50

f:id:nakamrnk:20200519093205j:plain

speaker_id = 100

f:id:nakamrnk:20200519093301j:plain

ひとつのモデルでスピーカーごとに特徴のあるそれなりに自然な音声が生成できている ように感じる。

サンプリングの影響

Flowtronはガウス分布からサンプリングされたノイズによりメルスペクトルグラムを構成するため、
サンプリングされたノイズによって多少異なる音声信号が得られる。
ここではノイズ以外のパラメータを固定して3回runを回してどれくらい音声の多様性が
生まれるかを検証する。 ガウシアンノイズのsigma=1, 0.4の2つの値で検証した。

sigma=1

speaker_id=0 (run 1)

speaker_id=0 (run 2)

speaker_id=0 (run 3)

speaker_id=100 (run 1)

speaker_id=100 (run 2)

speaker_id=100 (run 3)

sigma=1でサンプリングした場合runごとのブレが大きすぎるように感じる。
特にspeaker_id=100の場合は同一スピーカーとは思えないほどブレている
(特にspeaker_id =100が顕著) 。
ガウス分布の分散が大きすぎると非言語情報だけでなく、スピーカーまで変わってしまうようだ。
また、やや発音が不自然に聞こえる音声も多い。

sigma=0.4

speaker_id=0 (run 1)

speaker_id=0 (run 2)

speaker_id=0 (run 3)

speaker_id=100 (run 1)

speaker_id=100 (run 2)

speaker_id=100 (run 3)

sigma=0.4の場合はsigma=1の場合と比較するとrunごとのブレは小さい。
同一speakerからの音声と言われても納得できる音声に対してアクセントなどに
微妙なブレが生じているためFlowtronに期待されている非文章情報のサンプリング
ができていると思われる。

感想

学習済みのFlowtronを動かしてみた。
少なくとも英語に関しては同一のネットワークで複数のスピーカーに対して
微妙に異なる自然な音声が生成できていてすごいと思った。
日本語音声でも同様なことができるかは今後可能ならば検証したい。

Flowtronを理解する

はじめに

文章から音声データを生成するタスク Text to Speech (TTS)におけるDeep Learning手法として
Tacotron2 (Shen et al. 2017)が有名である。そのTacotron2を拡張して生成する音声データを
ある程度調整可能にしたFlowtron (Valle et al. 2020)が最近提案されたのでその内容を理解したい。

概要

Tacotron2についての解説は以下のページが分かりやすい。
https://akifukka.hatenablog.com/entry/tacotrn2_1

音声データは本質的には1次元の信号データであるが、 人間の聴覚特性を考慮したメルスペクトルグラム (mel-spectrogram)という二次元データ(時間x周波数空間) を経由したほうが人間の感覚的には自然な音声データが作成できる。
Tacotron2や今回紹介するFlowtronは文章を入力としてこのメルスペクトルグラムを出力するアルゴリズムである。
(このメルスペクトルグラムから音声データを生成するのにはWaveglow (Prenger et al. 2018)などの他のアルゴリズムが必要)

Tacotron2は文章→メルスペクトルグラムを一意に推論していたが、
FlowtronではFlowによりガウシアンノイズ(prior)と文章(+スピーカー情報)からメルスペクトルグラムを
サンプリングすることで出力音声をある程度制御可能としている。

実装はNVIDIAが公開している。
https://github.com/NVIDIA/flowtron

前処理

Tacotron2と同様に音声データからメルスペクトルグラムを生成する。
メルスペクトルグラムのパラメータもTacotron2と同様。
入力文章は文字単位のsequenceとして取り扱われる。

モデル

Tacotron2にあったpostnetなどの構造はなくなっている。
Tacotron2のようなSeq2Seqなdecoderではなくflow-baseのdecoderとなっている

  1. 文章とスピーカー情報を独立にembeddingする
  2. 文章embeddingはCNNとLSTMを組み合わせたencoderを通す
  3. encodeされた文章とスピーカーembeddingをconcatしてdecoderに渡す
  4. decoderはflow-base
    • Affine Coupling Layer(ACL)という可逆(Invertible)でDeterminantが簡単に求められる層を重ねている
    • ACLのスケール、バイアスと終了条件(gate)推論にはAttentionとLSTMを組み合わせたネットワークを利用
    • 奇数層目は時間的に順方向、偶数層目は逆方向のメルスペクトルグラムに対して処理を行っている
    • decoder部分は学習時と推論時で挙動が異なる
      • 学習時はencoder出力とメルスペクトルグラムを入力としてガウス分布のpriorと各ACLのスケールを出力
      • 推論時はencoder出力とガウス分布からサンプリングしたpriorを入力としてメルスペクトルグラムを生成する

flowによる生成過程については以下のスライドが詳しく解説してくださっている。
https://www.slideshare.net/DeepLearningJP2016/dlflowbased-deep-generative-models

損失関数

最小化する損失関数は negative log-likelihood (NLL)である。
ACLによって実際に最適化しているのは各flow層のスケールの最大化と
最終出力(prior)の最小化である。

推論処理

ガウシアンサンプリングされたprior、文章、スピーカー情報を入力とする。
priorをいくつかのflow層に通すことでメルスペクトルグラムに変換していく。
(学習時とはスケールとバイアスのかけ方が逆となっている)
学習時にガウス分布sigmaを固定した場合、推論時のサンプリングでsigmaの値を下げることで
クオリティが上がるらしい。 (GANにおけるtruncation trickみたいなもの?)

特定のスピーカーの音声から求めたpriorを入力として別のsperakerのメルスペクトルグラムを出力 することも可能(voice style transfer)。

感想

TTSにおいて音声データには入力文章だけでは表現できない感情などの様々な情報が含まれている。
Flowtronではこれらの非文章情報(non-textual information)をうまく特徴量空間で吸収して
サンプリングにより再現しようとする。Flow-baseのアルゴリズムによって特定の話者の
特定のnon-textural informationに対応するpriorを求めて、それを別の話者に転移できるのはすごそうに感じた。
今後は実際にFlowtronを使ってどんなことができそうか確かめていきたい。