PFRLを試してみる

はじめに

最近Preferred Networksが公開したpytorchによる強化学習ライブラリ
PFRLの内容を確かめて、openai-gymに実装されている
Pendulum問題を学習させてみた。

PFRL

PFRLはchainerによる強化学習ライブラリchainerrl1の後継ライブラリである。
強化学習を行うために必要な機能や、いくつかのモデルフリー強化学習アルゴリズム
が搭載されている。

強化学習

強化学習教師あり学習などと異なり、データを予め用意しない。
Agentが学習時に環境から状態を受け取って、行動を選択し、
報酬を元にデータを収集し、最適な方策を学習する。

f:id:nakamrnk:20200803080034j:plain

Agent

Agentはひとつの強化学習アルゴリズムに対応し、
環境との相互作用により方策を学ぶ。

PFRLは2020/8/3時点で以下のアルゴリズムが実装されている。

  • DQN
  • Categorical DQN
  • Rainbow
  • IQN
  • DDPG
  • A3C
  • ACER
  • PPO
  • TRPO
  • TD3
  • SAC

環境

強化学習が方策を学ぶため相互作用するための対象。
行動を受け取って、状態を変化させるシミュレータ。
PFRLは主に環境へのインターフェースを提供しており、
シミュレータはopenai-gym2やmujoco3などを利用する必要がある。

検証

今回はopenai-gymのpendulum問題に対してPFRLのSAC4を動かしてみる。

Pendulum問題

トルクを制御して一次元の振り子を立たせる問題。

  • 状態 : 3次元 連続値 (cos θ、 sinθ, 角速度)
  • 行動 : 1次元 連続値 (トルク)
  • 報酬 : 振り子の先端が頂点付近にあれば高くなる
  • 1エピソードは200ステップ

本来一次元の振り子なので、角度θと角速度だけでいいのだが、
角度θが0, 360間で不連続となるので、cosθ、sinθの2成分に分けている。

コード

ライブラリ読み込み

!pip install pfrl
import os
import random

import numpy as np
import pandas as pd

import matplotlib.pyplot as plt
import seaborn as sns
from tqdm.notebook import tqdm as tqdm

import pfrl
import torch
import torch.nn
import gym

環境設定

env = gym.make('Pendulum-v0')
obs_size = env.observation_space.low.size
n_actions = env.action_space.shape[0]

print('observation space:', env.observation_space)
print('action space:', env.action_space)

環境はopenai-gymのPendulum-v0をそのまま使う。

Q-Function, Policy-Function

from torch import distributions as dists


class QFunction(torch.nn.Module):

    def __init__(self, obs_size, n_actions):
        super().__init__()
        self.l1 = torch.nn.Linear(obs_size + n_actions, 50)
        self.l2 = torch.nn.Linear(50, 50)
        self.l3 = torch.nn.Linear(50, 1)

    def forward(self, x):
        state, action = x
        h = torch.cat([state, action], 1)
        h = torch.nn.functional.relu(self.l1(h))
        h = torch.nn.functional.relu(self.l2(h))
        h = self.l3(h)
        return h

    def __init__(self, obs_size, n_actions, log_std_max=3, log_std_min=-15):
        super().__init__()
        self.l1 = torch.nn.Linear(obs_size, 50)
        self.l2 = torch.nn.Linear(50, 50)
        self.mean = torch.nn.Linear(50, n_actions)
        self.log_std = torch.nn.Linear(50, n_actions)
        self.log_std_max = log_std_max
        self.log_std_min = log_std_min
    
    def forward(self, x):
        h = x
        h = torch.nn.functional.relu(self.l1(h))
        h = torch.nn.functional.relu(self.l2(h))
        mean = self.mean(h)
        log_std = self.log_std(h)
        log_std = torch.clamp(log_std, min=self.log_std_min, max=self.log_std_max)
        dist = dists.Normal(mean, log_std.exp())
        return dist



SACは状態と行動値を受け取って行動価値を返すQ関数と
状態を受け取って行動を返す方策関数からなる Actor-Criticである。
方策関数は平均と標準偏差をネットワークで予測し、ガウス分布を返す。
Q関数は状態と行動をconcatしてMLPに通して価値を返す。

SAC Agent

#https://github.com/pfnet/pfrl/blob/master/pfrl/agents/soft_actor_critic.py

from torch.nn import functional as F

class ModSAC(pfrl.agents.SoftActorCritic):
    def _batch_act_train(self, batch_obs):
        if self.burnin_action_func is not None and self.n_policy_updates == 0:
            batch_action = [self.burnin_action_func() for _ in range(len(batch_obs))]
        else:
            # deterministic フラグを追加
            batch_action = self.batch_select_greedy_action(batch_obs, deterministic=self.act_deterministically)
        self.batch_last_obs = list(batch_obs)
        self.batch_last_action = list(batch_action)

        return batch_action  

    def update_q_func(self, batch):
        """Compute loss for a given Q-function."""

        batch_next_state = batch["next_state"]
        batch_rewards = batch["reward"]
        batch_terminal = batch["is_state_terminal"]
        batch_state = batch["state"]
        batch_actions = batch["action"]
        batch_discount = batch["discount"]

        with torch.no_grad(), pfrl.utils.evaluating(self.policy), pfrl.utils.evaluating(
            self.target_q_func1
        ), pfrl.utils.evaluating(self.target_q_func2):
            next_action_distrib = self.policy(batch_next_state)
            next_actions = next_action_distrib.sample()
            next_log_prob = next_action_distrib.log_prob(next_actions)
            next_q1 = self.target_q_func1((batch_next_state, next_actions))
            next_q2 = self.target_q_func2((batch_next_state, next_actions))
            next_q = torch.min(next_q1, next_q2)
            # unsqueeze処理を無視(方策関数側で次元を落とすべき?)
#            entropy_term = self.temperature * next_log_prob[..., None]
            entropy_term = self.temperature * next_log_prob
            assert next_q.shape == entropy_term.shape

            target_q = batch_rewards + batch_discount * (
                1.0 - batch_terminal
            ) * torch.flatten(next_q - entropy_term)

        predict_q1 = torch.flatten(self.q_func1((batch_state, batch_actions)))
        predict_q2 = torch.flatten(self.q_func2((batch_state, batch_actions)))
        # print("===")
        # print(batch_rewards)
        # print(batch_terminal)
        # print(batch_discount)
        # print(next_q)
        # print(entropy_term)
        
        # print(target_q, predict_q1)
        loss1 = 0.5 * F.mse_loss(target_q, predict_q1)
        loss2 = 0.5 * F.mse_loss(target_q, predict_q2)

        # Update stats
        self.q1_record.extend(predict_q1.detach().cpu().numpy())
        self.q2_record.extend(predict_q2.detach().cpu().numpy())
        self.q_func1_loss_record.append(float(loss1))
        self.q_func2_loss_record.append(float(loss2))

        self.q_func1_optimizer.zero_grad()
        loss1.backward()
        if self.max_grad_norm is not None:
            clip_l2_grad_norm_(self.q_func1.parameters(), self.max_grad_norm)
        self.q_func1_optimizer.step()

        self.q_func2_optimizer.zero_grad()
        loss2.backward()
        if self.max_grad_norm is not None:
            clip_l2_grad_norm_(self.q_func2.parameters(), self.max_grad_norm)
        self.q_func2_optimizer.step()


    def update_policy_and_temperature(self, batch):
        """Compute loss for actor."""

        batch_state = batch["state"]

        action_distrib = self.policy(batch_state)
        actions = action_distrib.rsample()
        log_prob = action_distrib.log_prob(actions)
        q1 = self.q_func1((batch_state, actions))
        q2 = self.q_func2((batch_state, actions))
        q = torch.min(q1, q2)

        # unsqueeze処理を無視(方策関数側で次元を落とすべき?)
#        entropy_term = self.temperature * log_prob[..., None]
        entropy_term = self.temperature * log_prob
        assert q.shape == entropy_term.shape
        loss = torch.mean(entropy_term - q)

        self.policy_optimizer.zero_grad()
        loss.backward()
        if self.max_grad_norm is not None:
            clip_l2_grad_norm_(self.policy.parameters(), self.max_grad_norm)
        self.policy_optimizer.step()

        self.n_policy_updates += 1

        if self.entropy_target is not None:
            self.update_temperature(log_prob.detach())

        # Record entropy
        with torch.no_grad():
            try:
                self.entropy_record.extend(
                    action_distrib.entropy().detach().cpu().numpy()
                )
            except NotImplementedError:
                # Record - log p(x) instead
                self.entropy_record.extend(-log_prob.detach().cpu().numpy())

PFRL(pfrl==0.1.0)実装のSACがそのままでは動かなかったので、一部修正している。
(方策関数の出力形式がおかしい?)
(追記 : 公式のexampleに記載あり Independentを介する必要がある
https://github.com/pfnet/pfrl/blob/master/examples/mujoco/reproduction/soft_actor_critic/train_soft_actor_critic.py)

学習・評価


max_episode_len = 200


def evaluate(agent, num_eval):
  trajects = []
  prg = tqdm(range(num_eval))
  with agent.eval_mode():
    for i in prg:
        obs = env.reset()
        R = 0
        t = 0
        while True:
            # Uncomment to watch the behavior in a GUI window
            # env.render()
            action = agent.act(obs)
            obs, r, done, _ = env.step(action)
            R += r
            t += 1
            reset = t == max_episode_len
            trajects.append(list(obs) + [action[0], r, done, i])
#            agent.observe(obs, r, done, reset)
            if done or reset:
                break
  result_df = pd.DataFrame(trajects, columns=["cos", "sin", "dot", "action", "reward", "done", "run"]) 
  result_df["theta"] = np.arctan2(result_df["sin"], result_df["cos"]) * 180.0/np.pi
  mean_return = result_df.groupby(["run"])["reward"].sum().mean()
  result_df["mean_return"] = mean_return
  result_df["action"] = np.clip(result_df["action"], -2, 2)
  return mean_return, result_df


def train(agent, run, stat_period=50, show_period=10, eval_period=50, 
          num_eval=100, n_episodes=300):
  results = []
  Rs = []
  for i in range(1, n_episodes + 1):
      obs = env.reset()
      R = 0  # return (sum of rewards)
      t = 0  # time step
      while True:
          action = agent.act(obs)
          obs, reward, done, _ = env.step(action)
          R += reward
          t += 1
          reset = t == max_episode_len
          agent.observe(obs, reward, done, reset)
          if done or reset:
              break
      Rs.append(R)
      if i % show_period== 0:
          print('run', run, 'episode:', i, 'R:', np.mean(Rs[-50:]))
      if i % stat_period == 0:
          print('statistics:', agent.get_statistics())
      if i % eval_period == 0:
        mean, res_df = evaluate(agent, num_eval)
        print("evaluate :", mean)
        results.append([run, i, np.mean(Rs[-50:]), mean])
  return results, res_df


実行

%%time

n_episodes=500
gamma = 0.99
phi = lambda x: x.astype(numpy.float32, copy=False)
gpu = -1
num_runs = 3

def set_seed(seed):
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)
    torch.backends.cudnn.deterministic = True

res_dfs = []
results = []
for run in range(num_runs):
  env.seed(run)
  set_seed(run)
  policy_func = PolicyFunc(obs_size, n_actions)
  q_func1 = QFunction(obs_size, n_actions)
  q_func2 = QFunction(obs_size, n_actions)

  poly_optimizer = torch.optim.Adam(policy_func.parameters(), eps=1e-3)
  q1_optimizer = torch.optim.Adam(q_func1.parameters(), eps=1e-3)
  q2_optimizer = torch.optim.Adam(q_func2.parameters(), eps=1e-3)

  replay_buffer = pfrl.replay_buffers.ReplayBuffer(capacity=10 ** 6)
  explorer = pfrl.explorers.AdditiveGaussian(
      0.2, low=env.action_space.low, high=env.action_space.high)

  agent = ModSAC(
      policy_func, q_func1, q_func2,
      poly_optimizer, q1_optimizer, q2_optimizer,
      replay_buffer,
      gamma,
      replay_start_size=500,
      update_interval=1,
      phi=phi,
      gpu=gpu,
  )
  rows, res_df = train(agent, run, n_episodes=n_episodes)
  results.extend(rows)
  res_dfs.append(res_df)
log_df = pd.DataFrame(results, columns=["run", "episode", "train", "eval"])
print('Finished.')

SACはOff-polictyのActor-Criticであり、Q関数、方策関数、Experience-Replay
などを用意する必要がある。 また、SACは2つのQ関数を用いて安定性を高めているので、
初期に2つQ関数を用意する。

ハイパーパラメータは適当に設定している。
強化学習は初期値によって学習速度が大きくブレることがあるので、
今回は3run学習している(本来は10runほどやったほうがいいと思う)。
学習時の探索アルゴリズムはAdditiveGaussianとしている。
Pendulum問題は初期値によって1エピソードの合計報酬(収益)が大きく変わるので、
評価時は100エピソードに対する平均収益を求めている。

学習曲線

train_eval_df = log_df.set_index(["run", "episode"]).stack().reset_index().rename(columns={"level_2":"mode", 0:"return"})
sns.relplot(x="episode", y="return", data=train_eval_df, kind="line", hue="run", col="mode", palette="Set1")

f:id:nakamrnk:20200803143020p:plain

run1は100エピソード程度で収束しているのに対して、
run0 は500エピソードでも収束していない。
探索アルゴリズムやパラメータを調整すればもう少し、
runごとのバラつきは小さくなると思う。

軌跡

results_df = []
for r, r_df  in enumerate(res_dfs):
  r_df = r_df.rename(columns={"run":"episode"})
  r_df["run"] = "run {} mean return {:.2f}".format(r, r_df["mean_return"].values[0])
  results_df.append(r_df)
results_df = pd.concat(results_df)
sns.relplot(x="theta", y="dot", data=results_df[results_df["episode"]<8], col="run", hue="action", palette="RdBu", style="episode")

f:id:nakamrnk:20200803144239p:plain

学習終了時の評価100エピソード中8エピソード分の
状態空間軌跡を描画すると上図のようになる。

横軸は頂点からの振り子の選択の角度θ。 縦軸は角速度。 色はトルクの値。
原点(0, 0)がゴール(振り子の先端が上を向き、速度0)である。

基本的にどの軌跡もゴール付近には辿りつけている。
振り子が下のほう(-180, 180付近)にある場合は
トルクが大きく何回か左右にトルク振りながら加速し、
速度が十分に達すると、左上または右下から原点付近に向う。
ゴール近くになるとトルクの符号が変わり速度を落として
原点に静止させるように制御を行っている。

まとめ

今回はPFRLライブラリを使ってみた。
Pendulum問題に対してSACで学習ができることを
確認していくつかのrunについての結果を比較した。
今後は他のアルゴリズムや他の問題についても検証してみたい。

参考文献

stylegan2による猫画像生成 - 2

はじめに

前回 stylegan21 により Oxford-IIIT2の猫画像を学習した。
今回は前処理を少し変更して再学習した。
設定や用語等は前回参照。

前処理の変更

前回はannotation情報を利用して、猫が写っている部分以外は
白塗りになるように学習したが、輪郭部分が気になる結果となった。

今回の前処理では背景部分を白塗りにせずにそのまま利用することとした。
正方形に切り出せない部分は前景部分以外の平均色でpaddingした。

結果

学習経過

前回と同様FIDスコアによる学習曲線を表示すると以下のようになる。
f:id:nakamrnk:20200730060041j:plain

前回と同様20000 Iteration程度で収束しているが、
収束値は前回は90程度で、今回は110程度である。
今回は背景込みの画像なので背景部分を正しく再現することが難しいため
FIDスコアが悪くなっているものと思われる。
猫部分にだけ関していえ前回とそこまで変わらない印象がある。

40000 Iteration後のサンプリング結果

f:id:nakamrnk:20200730060312p:plain

noiseの影響

前回と同様できのいい画像を5枚サンプリングして、
style noiseでないnoiseの影響を見る。

f:id:nakamrnk:20200730060745p:plain

前回のように輪郭部分が大きく変化することはなく、
毛の細かい模様がnoiseでブレていることが分かる。

mixing

8層 (ネッワーク終盤)

f:id:nakamrnk:20200730061139p:plain
ネットワーク終盤でのmixingは前回と同様に全体的な色あいが置き換わり、 形状はほぼ変わらない。
前回は白塗り背景で学習していたため、背景は常に白だったが、
今回追加した背景についても色情報はネットワーク後半部が担っているようだ。

4層 (ネッワーク中盤)

f:id:nakamrnk:20200730061759p:plain

ネットワーク中盤付近によるmiximigにおいても前回と同様
中規模なstyle情報が後半部分のstyle noise依存となっている
傾向が見られた。 ただ、(3, 3)と(3, 2)の画像を比較すると
耳の形が変わりすぎているため、耳の形状等は前半部依存と
なっている。

2層 (ネットワーク序盤)

f:id:nakamrnk:20200730062612p:plain 前半部のmixingでは一部の画像(3列目など)が崩れて
猫と認識できなくなっている。 前半部と後半部が
完全に切り分けできているわけではなく、スタイルが違いすぎる
noiseを混ぜると正しく再構成できないようだ。

style制御ムービー

前回と同様後半部のstyle固定で前半部のstyleを特徴量空間で
補間したムービーは以下のようになる。

f:id:nakamrnk:20200730063202g:plain

目、鼻などの顔部分は姿勢が変わってもほとんど変化していないので、
それらの情報は固定した後半部styleが決定していると思われる。
一方で前回と同様、耳の形は姿勢と一緒に変化してしまっている。
耳の形は背景との境界を決めるので、姿勢などのグローバルなstyleに
結びつきやすいようだ。 同一の猫の異なる姿勢のムービーが作りたい
場合は、耳の形状が近い姿勢の異なる猫のstyleを利用する必要がある。

まとめ

前回 と同様stylegan2で猫画像を生成した。
FIDスコアは落ちているが、背景部分を残したほうが実際の画像
に近く感じるのでこちらの方が好みである。
次は犬も混ぜてみようかと思う。

参考文献

stylegan2による猫画像生成

はじめに

GANの一種であるstylegan21を使い、
Oxford-IIIT2の猫画像を学習して、結果を解析した。

stylegan2

Generative Adversarial Network (GAN)3は2つのネットワークが
競い合うように学習することでリアルな画像生成を行うアルゴリズムである。

オリジナルのGANは1つのノイズから画像を生成できるように生成器(Generator)
を学習するが、近年のstylegan4, stylegan25などはニューラルネットの各層に
ノイズを付与するように学習することで、画像生成時にグローバルな構造と ローカルな構造をある程度制御できる。

styleganに関する解説はすでに多くのweb記事があるので詳細は省く。

参考 : https://medium.com/@akichan_f/gan%E3%81%AE%E5%9F%BA%E7%A4%8E%E3%81%8B%E3%82%89stylegan2%E3%81%BE%E3%81%A7-dfd2608410b3
https://qiita.com/YasutomoNakajima/items/1e0153cfb598641f5c9b
https://www.slideshare.net/KentoDoi/stylegan-cvpr2019dena

重要な点は

  • ノイズをMLPに通して特徴量空間を綺麗にする(disentangle)
  • MLPを通したノイズ(style noise)を各層に入れる
  • 学習時に一定確率でネットワークの前半部と後半部のstyle noiseを入れ替える(mixing)
  • style noiseとは独立なノイズ(ここではnoiseと表記)を各層に足す

人の顔を学習した場合、noiseは髪の重なりや顔のシワ、目の開き方などの細かい要素を制御し、
前半層のstyle noise は顔の向きなどのグローバルな構造、
後半層のstyle noise は髪色などの情報を制御できるように学習が進む。

今回は以下のpytorch実装を利用させていただいた。
https://github.com/rosinality/stylegan2-pytorch

Oxford-IIIT

Oxford-IIITデータセットはペットの犬・猫の画像データセットである。
http://www.robots.ox.ac.uk/~vgg/data/pets/
猫画像2371枚、犬画像4978枚のデータセットである。

画像だけでなく、前景と背景、輪郭線情報も含んでいるため、 必要な部分だけを利用する前処理がしやすい。 今回は以下の前処理を行った。

  • annotation領域以外を白塗りにする
  • annotation領域の中心から正方形に切り出す
  • 128 x 128にリサイズ

f:id:nakamrnk:20200729060144j:plain

やや輪郭付近に周辺情報も含まれてしまうのは気になるが、
背景情報をほぼ含まない猫中心の画像となる。

検証結果

学習経過

FIDスコアはGANの性能評価に使われる指標の一つで、
生のデータセットとGANによって生成された画像群に対して、
学習済みニューラルネットによって抽出した特徴量の分布が
どれほど似ているかの指標である。
低いほど性能が高く、 完全に再現できている場合は0となる。
GANについての指標は色々あるようだが6
今回はとりあえずこの指標を用いる。

f:id:nakamrnk:20200729063714j:plain FIDスコアの学習曲線は20000 iteration程度である程度収束し、
その後も徐々に性能が向上している。

10000 Iteration f:id:nakamrnk:20200729063830p:plain

20000 Iteration f:id:nakamrnk:20200729063947p:plain

40000 Iteration f:id:nakamrnk:20200729064015p:plain

生成画像を見る限りはFIDスコアによる評価結果は感覚的には正しく感じる。
10000 Iterationでは猫の輪郭くらいしか再現できておらず、
20000 Iteration で猫らしくなってきているが、目や鼻の構造が歪んでいる。
40000 Iterationになるとかなり現実の猫画像に近くなっている。

noiseの影響

style noiseでないnoiseの影響を示したのが以下の図である。

f:id:nakamrnk:20200729065432p:plain

左5列はある特定のstyle noiseに対してnoiseだけを変化させた画像であり、
右から2番目の列はその平均画像、右端は標準偏差画像である。

平均画像でぼやけている部分や、標準偏差画像で値が高い部分は
noiseにより変化が大きい部分である。

この結果より、noiseが変化させているのは主に輪郭部分の構造(切り抜き背景)や 毛並みなどであり、ポーズや毛の色などにはほとんど影響していない
ことが分かる。

特徴量空間補間

styleganの特徴の一つにstyle noiseをMLPに通すことで特徴量空間がなめらかになる
ことがある。ここではその効果を検証する。

f:id:nakamrnk:20200729070143p:plain

この図の対角画像は元の画像であり、非対角な部分にある画像は
それぞれ対角画像のノイズ空間平均画像である。
例えば(2, 1)にある画像1と画像2のstyle noiseを平均した画像である。
いくつかの画像( (5, 1), (4, 2)など)は2つの画像の中間と言われれば
納得できるが、片方の影響が強く出過ぎている画像(3, 1)や
どちらにも似ていない画像(4, 3)など補間として微妙な画像もある。

次も同じような補間画像であるが、今回は補間を元のノイズ空間ではなく、
MLPを通した後の特徴量空間で行った画像である。

f:id:nakamrnk:20200729070431p:plain

この場合の平均画像は元のノイズ空間で平均したものと比べると
どちらかの画像に寄りすぎていることもなく、 2つの画像の中間としては適切に感じる。
MLP層を通すことにより特徴量空間が滑らかになっているためと思われる。

mixing

styleganのもうひとつの特徴は学習時に前半部と後半部でstyle noise
を入れ替えるmixingを行っていることである。
これにより推論時に複数の画像のstyle を混ぜることができる。

 mixing (8層 : ネットワーク後半)

f:id:nakamrnk:20200729072254p:plain

この図はgeneratorの前半部と後半部でstyle noiseを入れ替えた図である。
(2, 1)の画像は前半部は画像1, 後半部は画像2のstyle noiseを用いた画像であり、
(1, 4) は前半部4, 後半部1である。 対角成分は元画像のままである。

8層目はネットワークの後半であり、ここでmixingしても
全体の色が変わるくらいで、構図は変わらない。

 mixing (4層 : ネットワーク中盤)

f:id:nakamrnk:20200729073442p:plain

ネットワークの中盤である4層でmixingを行うと、8層でのmixing
よりもグローバルな構造が変化していることが分かる。
(1, 4)の画像を比較すると8層でのmixingのでは、単に全体の色が
画像1になった画像4だったが、4層でのmixingでは鼻の形や
目の構造などが画像1に近くなっている。
また, (4, 5)を比較すると分かるように顔付近だけ黒いといった、
中域的なスケールの色構造も後半部画像のスタイルに近くなっている。

このことからそういった情報は4~8層当たりのstyle noiseが担っている
ことが分かる。

 mixing (2層 : ネットワーク序盤)

f:id:nakamrnk:20200729075047p:plain

2層での入れ替えでは体の向きなどの全体構造も後半部styleに
影響を受けるようになっている。

style 制御ムービー

f:id:nakamrnk:20200729075747g:plain

styleの後半部を固定して、前半部だけを変化させることで
同じ猫に対して姿勢だけ変化させるムービーができる。
上図は左からそれぞれ2, 3, 4層を前半部とした補間ムービーである。
このデータの場合は2, 3層に姿勢情報が含まれているようなので、
4層当たりでmixingするのが良さそうに見える。

まとめ

今回はstylegan2による猫画像生成を検証してみた。
stylegan2の特性がある程度理解できたが、
画像を切り取ったが故に輪郭が強く出過ぎているように感じるため
切り取らずにやった場合にどうなるかも検証したい。

参考文献

Gradient Accumulation と Normalization

はじめに

batch sizeは学習の安定性やモデル性能に大きな影響を与えるパラメータである。
大きなbatch sizeは学習を安定化するが、GPUのメモリを使い果たしてしまう。
GPT31などの近年の大規模モデルは複数のGPUに分散して非常に大きな
batch sizeをとっており、計算リソースの乏しい人では再現することが難しい。
gradient accumulation2はこの問題をある程度解決してくれる可能性があり、
今回はこのgradient accumulationと各種normalizationの相性をMNISTデータに
対して検証した。

gradient accumulation

参考 : https://medium.com/huggingface/training-larger-batches-practical-tips-on-1-gpu-multi-gpu-distributed-setups-ec88c3e51255

gradient accumulationはbatch単位でパラメータの更新を行わずに、
複数個のbatchのパラメータ勾配を積算してから更新することで
実効的なbatch sizeを増やす手法である。
例えば、batch size 4で4回勾配の積算を行ってからパラメータ更新すると、
メモリ使用量はbatch size 4のままだが、batch size 16と同等の性能が期待される。

ただ、この手法ではbatch normalizationのように学習中のバッチ単位の統計量を
利用している手法ではうまく機能しない可能性がある。

検証

MNISTデータに対して各種normalizationに対する
gradient accumulationの効果を検証した。

データ読み込み

import torch
import torch.nn as nn

from torch.utils.data import Dataset, DataLoader

from torchvision.datasets import MNIST
from torchvision import transforms 

train_trans = transforms.Compose([transforms.ToTensor()])
valid_trans = transforms.ToTensor()


data_dir = "data"
train_dataset = MNIST(data_dir, train=True, download=True, transform=train_trans)
valid_dataset = MNIST(data_dir, train=False, download=True, transform=valid_trans)

特にデータ水増しはしていない。

ネットワーク

class SimpleModel(nn.Module):
  def __init__(self, norm_fn=None, channels=[32, 64, 128, 256], c0=1, 
               act_func=nn.ReLU, num_classes=10):
    super().__init__()
    prev_channel = c0
    features = []
    for c, channel in enumerate(channels):
      features.append(nn.Conv2d(prev_channel, channel, 3, padding=1))
      if norm_fn is not None:
        features.append(norm_fn(channel))
      features.append(act_func())
      if c < len(channels) -1:
        features.append(nn.MaxPool2d(2))
      prev_channel = channel
    self.features = nn.Sequential(*features)
    self.dense = nn.Linear(prev_channel, num_classes)

  def forward(self, x):
    x = self.features(x)
    x = nn.AdaptiveAvgPool2d(1)(x).squeeze(3).squeeze(2)
    x = self.dense(x)
    return x

skip connectionのない単純なVGG-likeなネットワークを利用。
Global Average Poolingから全結合1層で分類。

検証パラメータ

学習・評価コード

device = "cuda" if torch.cuda.is_available() else "cpu"
import numpy as np
import pandas as pd

from torch.optim import Adam
from tqdm.notebook import tqdm

def evaluate(model, valid_loader, show_progress=False):
  prg = tqdm(valid_loader) if show_progress else valid_loader
  prds = []
  lbls = []  
  for batch in prg:
    imgs, labels = [b.to(device) for b in batch]
    with torch.no_grad():
      o = model(imgs)
      pred = np.argmax(o.cpu().numpy(), axis=1)
      prds.append(pred)
      lbls.append(labels.cpu().numpy())

  prds = np.concatenate(prds)
  lbls = np.concatenate(lbls)
  acc = np.mean(prds==lbls)
  return acc

def get_iter(train_loader):
  while True:
    for batch in train_loader:
      yield batch


def train(model, num_iterations, batch_size, lr0=1e-3, eval_period=300, valid_batch_size=32, num_accum=1):
  model.train()
  train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
  valid_loader = DataLoader(valid_dataset, batch_size=valid_batch_size, shuffle=False)

  criteria = nn.CrossEntropyLoss()

  optimizer = Adam(model.parameters(), lr=lr0)
  optimizer.zero_grad()

  iters = get_iter(train_loader)

  iter = 0
  losses = []
  accs = []
  prg = tqdm(iters, total=num_iterations*num_accum)
  
  loss_factor = 1.0 / max(num_accum, 1)

  i = 0
  for batch in prg:
    i += 1
    imgs, labels = [b.to(device) for b in batch]
    o = model(imgs)
    loss = criteria(o, labels) * loss_factor
    loss.backward()
    if i % num_accum == 0:
      optimizer.step()
      optimizer.zero_grad()
      iter += 1
    else:
      continue

    losses.append(loss.item() / loss_factor)

    mean_loss = np.mean(losses[-100:])
    if iter % 10 == 0:
      prg.set_description("iter {} , mean loss {:.4f}".format(iter, mean_loss))
    if iter % eval_period == 0:
      model.eval()
      vacc = evaluate(model, valid_loader)
      model.train()
      accs.append((iter, mean_loss, vacc))
    if iter >= num_iterations:
      break

  accs_df = pd.DataFrame(accs, columns=["iter", "train_loss", "valid_acc"]).set_index("iter")
  return accs_df

num_iterations = 3600
batches = [1, 1, 4, 16]
num_accums = [1, 16, 4, 1]

iteration数は3600 (accumulationがある場合はaccumulate数倍される)。
batch size 1, 4, 16に対して、accumulation数を 16, 4, 1とすることで実行的な
batch sizeを合わせている。
比較のためbatch size 1, accumulation数1も計算。
また、初期値によるバラつきを考慮して、同じパラメータで3run実行している。

パラメータ検証 コード

from functools import partial
def get_group_norm_func(num_groups):
  def norm_func(num_channel):
    return nn.GroupNorm(num_groups, num_channel)
  return norm_func


norms = {
    "grp4_norm":get_group_norm_func(4),
    "grp16_norm":get_group_norm_func(16),
    "no_norm":None,
    "bn_norm": nn.BatchNorm2d,
    "ins_norm":nn.InstanceNorm2d,
}

results = []
for key, norm_fn in norms.items():
  accs_dfs = []
  for num_accum, batch_size in zip(num_accums, batches):
    for run in range(3):
      model = SimpleModel(norm_fn=norm_fn).to(device)
      accs_df = train(model, num_iterations, batch_size, num_accum=num_accum)
      accs_df["batch_size"] = batch_size
      accs_df["num_accum"] = num_accum
      accs_df["run"] = run + 1
      accs_dfs.append(accs_df)
  result_df = pd.concat(accs_dfs)
  result_df["norm"] = key
  results.append(result_df)

結果

import seaborn as sns
all_results_df = pd.concat(results).reset_index()
all_results_df["bs_accum"] = "bs" + all_results_df["batch_size"].map(str) + "_acum" + all_results_df["num_accum"].map(str)
g = sns.relplot(x="iter", y="valid_acc", data=all_results_df, kind="line", hue="bs_accum", col="norm", col_wrap=3)

f:id:nakamrnk:20200727134051p:plain

青線がgradient accumulationなし, batch size 1の結果である。
ほとんどの場合において、 batch sizeの増加やgradient accumulationにより
学習速度、最終的な性能ともに向上している。

唯一batch normalizaitonのbatch size 1, gradient accumulation=16の場合のみ
性能の改善が見られていない。 これはbatch normalizationが学習時にバッチ内の統計量
(の移動平均)を保持して推論時に利用するため、このgradient accumulationの実装では、
正しく学習できていないためと思われる。

一方で同じbatch normalizationでもbatch size 4
gradient accumulation=4の場合はbatch size 16と同程度の性能が出ている。
今回のMNISTデータの場合はbatch size=4程度あれば正しい統計量を学習できるため
と解釈できる。

gradient accumulationとバッチサイズ増加についてもう少し詳して見てみる

f:id:nakamrnk:20200727135456p:plain

batch normalization以外はgradient accumulationによりbatch size1, 4それぞれが
batch size 16でaccumulationなしの場合と同程度の性能を示している。
batch normalizationさえ使わなければgradient accumulationにより、
実効的なbatch sizeを増やせることが分かった。

2500 iteration以降の平均 valid accuracy

評価

all_results_df = pd.concat(results).reset_index()
all_results_df["bs_accum"] = "bs" + all_results_df["batch_size"].map(str) + "_acum" + all_results_df["num_accum"].map(str)
targ = ((all_results_df["batch_size"] > 1) | (all_results_df["num_accum"]>1)) & (all_results_df["iter"] > 250)
all_results_df = all_results_df[targ]
print(all_results_df.groupby(["norm", "bs_accum"])[ "valid_acc"].mean().unstack().to_markdown())

norm bs16_acum1 bs1_acum16 bs4_acum4
bn_norm 0.978367 0.908319 0.984044
grp16_norm 0.974481 0.971417 0.971078
grp4_norm 0.972542 0.973103 0.9706
ins_norm 0.984742 0.984817 0.984356
no_norm 0.969178 0.969169 0.967678

今回の問題においては、instance normalizationとbatch normalization
(gradient accumulationなし)がほぼ同程度の性能で、
group normalizationはやや劣る結果となった。
group normalizationについては、normalizationなしと比較すると性能が向上
しているため、全く効果がないわけではないと思われるが、  
instance normalizationには劣る結果となった。
(グループパラメータの値が適切でない?学習不足?)

まとめ

今回はgradient accumulationについて簡単な検証を行った。
結果batch normalization以外の場合はgradient accumulationで
メモリサイズを節約して学習できることが確認できた。
normalizationについては、問題依存の可能性はあるが、
instance normalizationあたりから始めればいいのではないかと思う。

参考文献

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倍程度となっていることが原因で
あると考えられる。学習時のサンプリング比率を調整することで
ある程度改善できると思われる。

参考文献