やってみた!

やってみた!

試したことを中心に、書評や興味のあること、思ったこととか

Open AI Gym Box2D BipedalWalkerをColaboratoryで動かしてみる(2)

 前回の続きです。ColaboratoryでBipedicalWalkerを動かすセッティング等は前回の記事を参照ください。

 まずは、ニューラルネットワークを使わずにとにかく動かして、BipedicalWalkerがどのようなものなのか試してみます。歩くとこまで行くかどうか(現在も試行錯誤中)。

7.とにかく動かしてみる

(1)角度制御を追加してとにかく動かす

 ソースリストを示します。計算部分と表示部分の2つにわけてありますので、Colaboratoryで動かす際は順番に動かしてください。

・計算部分

import torch
import numpy as np
import gym
from pyvirtualdisplay import Display
from scipy.interpolate import interp1d

display = Display(visible=0, size=(1024, 768))
display.start()
import os
#pyvirtualdisplayの仕様変更で次の文はエラーになるのでコメントアウトします(2021/6/5修正)
#os.environ["DISPLAY"] = ":" + str(display.display) + "." + str(display.screen)

#Bipedalwalkerのv2は無くなったのでv3に変更(2021/6/5)
env = gym.make('BipedalWalker-v3')

#角度制御ゲイン
 #P(比例)項
pgain = np.array([4,4,4,4], dtype = 'float')
 #D(微分)項
dgain = np.array([0.2,0.2,0.2,0.2], dtype = 'float')

#脚の指令角スケジュール
xsch = np.array([0,1,2,3,4,5,6,7,8,9,10],dtype = 'float')
yhip1  = np.array([    0, -0.3, -0.6, -0.6, -0.3,    0, 0.85,  0.7,  0.5,  0.3,    0],dtype = 'float')
yknee1 = np.array([  0.2,  0.9,  0.8,  0.7, -0.4,    0,  0.0,  0.0,  0.0,  0.0,  0.0],dtype = 'float')
yhip2  = np.array([ 0.85,  0.7,  0.5,  0.3,    0, -0.3, -0.6, -0.6, -0.3,    0, 0.85],dtype = 'float')
yknee2 = np.array([ -0.4,  0.0,  0.0,  0.0,  0.2,  0.9,  0.8,  0.7, -0.4,    0,    0],dtype = 'float')
numx = xsch.shape[0]

hip1demmand = interp1d(xsch, yhip1)
knee1demmand = interp1d(xsch, yknee1)
hip2demmand = interp1d(xsch, yhip2)
knee2demmand = interp1d(xsch, yknee2)

graphschys =[]
graphschys.append([hip1demmand(0)])
graphschys.append([knee1demmand(0)])
graphschys.append([hip2demmand(0)])
graphschys.append([knee2demmand(0)])
graphschxs =[0.0]

for graphschx in np.arange(0.1,10.0,0.1):
  graphschxs.append(graphschx)
  graphschys[0].append(hip1demmand(graphschx))
  graphschys[1].append(knee1demmand(graphschx))
  graphschys[2].append(hip2demmand(graphschx))
  graphschys[3].append(knee2demmand(graphschx))

#モデルタイムステップ(1秒間に50フレーム = 0.02s)
FPS = 50.0

for i in range(1):
    obs = env.reset()
    done = False
    frames = []
    observations = []
    actions = []
    ts = []
    legangle = np.array([0,0,0,0], dtype = 'float')
    leganglespeed = np.array([0,0,0,0], dtype = 'float')
    legangledemand = np.array([0,0,0,0], dtype = 'float')
    speed = 5
    x = 0.0
    t = 0
    leg1gcount = 0
    leg2gcount = 0
    leg1touchground =0
    leg2touchground =0

    while not done and t < 400:
        #動画用画面保存-------------------------------
        frames.append(env.render(mode = 'rgb_array'))

        #指令値生成-----------------------------------
        x += speed/FPS
        if x>10.0 :x-=10

        #接地でスケジュール強制的に進める
#        if x>0.5 and x<2.5 and leg2touchground == 1:x = 4.5 - x
#        if x>5.5 and x<7.5 and leg1touchground == 1:x = 9.5 - (x - 5)

        legangledemand[0] = hip1demmand(x)
        legangledemand[1] = knee1demmand(x)
        legangledemand[2] = hip2demmand(x)
        legangledemand[3] = knee2demmand(x)

        #角度指令値上書き。スケジュールで動かす時はコメントアウトすること
        legangledemand = np.array([1,1,1,1], dtype = 'float')

        #脚の角度 PD制御
        action = pgain * (legangledemand - legangle) - dgain * leganglespeed

        #2足歩行物理モデル1ステップ(0.02s)-------------
        observation, reward, done, info = env.step(action)

        #脚の角度,角速度,胴体角取り出し----------------
        legangle[0] = observation[4] #HIP1
        leganglespeed[0] = observation[5]
        legangle[1] = observation[6] #KNEE1
        leganglespeed[1] = observation[7]
        legangle[2] = observation[9] #HIP2
        leganglespeed[2] = observation[10]
        legangle[3] = observation[11] #KNEE2
        leganglespeed[3] = observation[11]

        #leg1 接地判定 チャタリング防止
        if observation[8] == 1:
          if leg1gcount <3 :leg1gcount += 1
        else:
          leg1gcount = 0
        
        if leg1gcount == 3:
          leg1touchground = 1
        else:
          leg1touchground = 0

        #leg2 接地判定 チャタリング防止
        if observation[13] == 1:
          if leg2gcount <3 :leg2gcount += 1
        else:
          leg2gcount = 0;

        if leg2gcount == 3:
          leg2touchground = 1
        else:
          leg2touchground = 0

        #グラフ用データ保存----------------------------
        if t == 0 :
          for i in range(24):
            observations.append([observation[i]])
          for i in range(4):
            actions.append([action[i]])
        else :
          for i in range(24):
            observations[i].append(observation[i])
          for i in range(4):
            actions[i].append(action[i])
        ts.append(t/FPS)

        #時間を進める----------------------------------
        t += 1

・表示部分

import matplotlib.pyplot as plt
import matplotlib.animation
import numpy as np
from IPython.display import HTML

fig1 = plt.figure(figsize=(12, 16))
sub1 = fig1.add_subplot(621)
sub1.plot(ts,observations[0],label="hull angle")
sub1.plot(ts,observations[1],label="hull angle Velocity")
sub1.legend()

sub2 = fig1.add_subplot(622)
sub2.plot(ts,observations[2],label="Velocity x")
sub2.plot(ts,observations[3],label="Velocity y")
sub2.legend()

sub3 = fig1.add_subplot(623)
sub3.plot(ts,observations[4],label="Hip1 Angle")
sub3.plot(ts,observations[9],label="Hip2 Angle")
sub3.legend()

sub4 = fig1.add_subplot(624)
sub4.plot(ts,observations[5],label="Hip1 Angle Velocity")
sub4.plot(ts,observations[10],label="Hip2 Angle Velocity")
sub4.legend()

sub5 = fig1.add_subplot(625)
sub5.plot(ts,observations[6],label="Knee1 Angle")
sub5.plot(ts,observations[11],label="Knee2 Angle")
sub5.legend()

sub6 = fig1.add_subplot(626)
sub6.plot(ts,observations[7],label="Knee1 Angle Velocity")
sub6.plot(ts,observations[12],label="Knee2 Angle Velocity")
sub6.legend()

sub7 = fig1.add_subplot(627)
sub7.plot(ts,observations[8],label="Leg1 Touch Ground")
sub7.plot(ts,observations[13],label="Leg2 Touch Ground")
sub7.legend()

sub8 = fig1.add_subplot(326)
sub8.plot(ts,actions[0],label="HIP1 Angle Velocity Demand")
sub8.plot(ts,actions[1],label="KNEE1 Angle Velocity Demand")
sub8.plot(ts,actions[2],label="HIP2 Angle Velocity Demand")
sub8.plot(ts,actions[3],label="KNEE2 Angle Velocity Demand")
sub8.set_ylim(-2.0,2.0)
sub8.legend()

sub9 = fig1.add_subplot(325)
sub9.plot(graphschxs,graphschys[0],label="HIP1 Angle Sch")
sub9.plot(graphschxs,graphschys[1],label="KNEE1 Angle Sch")
sub9.plot(graphschxs,graphschys[2],label="HIP2 Angle Sch")
sub9.plot(graphschxs,graphschys[3],label="KNEE2 Angle Sch")
sub8.set_ylim(-1.0,1.0)
sub9.legend()

plt.show()

fig2 = plt.figure(figsize=(6, 6))
plt.axis('off')

images = []
for f in frames:
  image = plt.imshow(f)
  images.append([image])
ani = matplotlib.animation.ArtistAnimation(fig2, images, interval=30, repeat_delay=1)

HTML(ani.to_jshtml())

 

 モデルの入力は脚の関節の角速度です。これでは歩行させるためのスケジュールを決めずらいので、まずは決まった角度に制御するよう制御回路を追加します。制御回路の構造を図に示します。

f:id:akifukka:20191208113348j:plain

 上記じゃなきゃダメというわけでは無く、他にも様々な構造を考えることができます。制御構造は何を重視するか(目標角度と実際の角度の差、偏差を小さくするのを重視したいのか、差はちょっとくらいはあっても、即応性を重視したいのかなど)によって変わってくるのですが、今回はバランスの良い(というか、だいたい何でもそこそこ対応できる万能型)PID制御から、即応性を重視してP(比例)、D(微分)を使うPD制御としました(I(積分)項は使わないことにしました)。

 図を見るとわかるように、P項、D項は次のように動きます。

  • P項
    目標と今の位置が大きくずれていたら、指令値もそれに応じて大きくし早く動かす。
  • D項
    目標へ向かう速度が早すぎ、最後に止まれず行き過ぎてしまう(オーバーシュート)。オーバシュート量を減らすため、速度が出たら指令値を少し小さくし、速度を加減するよう動く。

 P項、D項をどのくらい効かせるかはPゲイン、Dゲインの値で決めますが、今回は何回か動かして適当(何となくうまく動く程度)に次の値に決めました(ソースリストの上の方)

 

#角度制御ゲイン
#P(比例)項
 pgain = np.array([4,4,4,4], dtype = 'float')
#D(微分)項
 dgain = np.array([0.2,0.2,0.2,0.2], dtype = 'float')

 それぞれ、脚1の腿、脚1の膝、脚2の腿、脚2の膝のゲインです。

 

 PD制御の計算部分はソースリストのforループの中です。それぞれの値はnumpyのテンソル(np.array)にして1行で書いてます。

 #脚の角度 PD制御
 action = pgain * (legangledemand - legangle) - dgain * leganglespeed

 現在の脚の角度(legangle)、角速度(leganglespeed)はソースリストのforループの中で次のようにobservationから取り出します。

#脚の角度,角速度,胴体角取り出し----------------
 legangle[0] = observation[4] #HIP1
 leganglespeed[0] = observation[5]
 legangle[1] = observation[6] #KNEE1
 leganglespeed[1] = observation[7]
 legangle[2] = observation[9] #HIP2
 leganglespeed[2] = observation[10]
 legangle[3] = observation[11] #KNEE2
 leganglespeed[3] = observation[11]

では、ソースリストの次の文の角度指令値を色々変更して動かしてみます。

#角度指令値上書き。スケジュールで動かす時はコメントアウトすること
 legangledemand = np.array([1,1,1,1], dtype = 'float')

結果

f:id:akifukka:20191208123737j:plain

 指令値(legangledemand)は脚1の腿の角度、脚1の膝の角度、脚2の腿の角度、脚2の膝の角度です。

 この結果から、次のことがわかります。

  1. 脚1は薄い紫、脚2は濃い紫
  2. 腿の角度は1 ~ -1の範囲で動く。前が正、後ろが負の値。
  3. 膝の角度は1 ~ -0.6の範囲で動く。1が伸ばした状態、-0.6がもっとも曲げた状態。

さてさて、ソースリストの補足です

 ソースリスト中で指令値を与えている行をコメントアウトすると、ソースリストの上の方で"#脚の指令角スケジュール”として与えているスケジュールに従い、脚を動かして歩こうとします(何歩か歩けよォ。心の中の叫び)。

 が、結構難しくてうまく歩いてくれません・・・・。スケジュールの値、speed値(1秒間にスケジュールをどのくらい進めるか)を変えて試しているんですが・・。

 スケジュールは0~10のステップを1巡(脚1で1歩、脚2で1歩、計2歩で元に戻る)として、繰り返します。speed=5だと、1秒でスケジュール半分相当なので、1秒1歩です。

 このあたりは、うまく歩けてないので大幅に修正するかも知れませんが、現状でも色々試せると思います。

つづく
 どうやったらもう少し歩けるか思案して、人力でのチューニングはあきらめ乱数を使って半自動でチューニングしました。なんとか2,3歩ですが、歩けるようになりました。歩き方が若干変ですが・・・(何かぎこちないスキップ?)。

 強化学習 カテゴリーの記事一覧 - やってみた!

Open AI Gym Box2D BipedalWalkerをColaboratoryで動かしてみる(1)

 Open AI Gymをご存知の方も多いと思いますが、強化学習の開発用にOpen AIがフリーで提供しているテスト環境です。pythonで書かれています。

https://gym.openai.com/

 この中にはブロック崩し、インベーダゲームを始めとするAtariの様々なゲームや、2次元の月着陸、2足歩行(この記事)、3次元の人型ロボット等の様々な課題があります。当初、3次元はMujocoという有料の物理エンジンが使われていましたが、現在はPybulletというフリーのものが使えるようです。

 この記事ではOpen AI Gymの中でBox2D(2次元の物理エンジン)を使った2足歩行のBipedalWalker-v2を試してみたいと思います。 

1.Google Colaboratoryの準備

 GoogleアカウントでログインしてColaboratoryを起動、ファイルーPython3の新しいノートブックを開きます。

 

2.Box2Dのインストール

 次の2行でBox2D自身と、Box2D対応gymをインストールします。

!pip3 install box2d-py
!pip3 install 'gym[Box2D]'
 上記を実行したら必ず「ランタイムを再起動」を実行しましょう。実行しないとインストールしてもBox2Dを認識せずに、importしようとするとエラー(BipedalWalker-v2が無いと怒られます)が起きます。毎度ながらランタイムを再起動していないのが原因だと気づくまで1時間近くかかってしまいました。

3.画像表示の準備

 表示に必要なライブラリ類をインストールします。

!apt-get update
!apt-get -qq -y install xvfb freeglut3-dev ffmpeg
!pip3 -q install pyglet
!pip3 -q install pyopengl
!pip3 -q install pyvirtualdisplay

4.動かしてみます

  次のコードで動かします。この時点では絵は出ません。2足歩行BipedalWalker-v2を動かして、後で可視化するために次のデータを保存します。

・画像をリストframesに保存します。

・状態(observation)をリストobservationsに保存します。

・指令は action=[1,-1,1,-1]を与えます。
 両足のももを速度+1で動かし、両足の膝を速度ー1で動かします。#でコメントアウトしているのはサンプルで乱数で指令を与える関数です。

 BipedalWalker-v2はソースを見ると50FPS(0.02秒ステップで計算)しているので、グラフ描画用のリストts(横軸に使う)を1/50秒ステップで作っています。

import torch
import numpy as np
import gym
from pyvirtualdisplay import Display
display = Display(visible=0, size=(1024, 768))
display.start()
import os
#pyvirtualdisplayの仕様変更で次の文はエラーになるのでコメントアウトします(2021/6/5修正)
#os.environ["DISPLAY"] = ":" + str(display.display) + "." + str(display.screen)

#Bipedalwalkerのv2は無くなったのでv3に変更(2021/6/5)
env = gym.make('BipedalWalker-v3')

for i in range(1):
    obs = env.reset()
    done = False
    frames = []
    observations = []
    ts = []
    t = 0
    while not done and t < 20:
        frames.append(env.render(mode = 'rgb_array'))
#        action = env.action_space.sample()
        action = np.array([1,-1,1,-1])
        observation, reward, done, info = env.step(action)
        if t == 0 :
          for i in range(24):
            observations.append([observation[i]])
        else :
          for i in range(24):
            observations[i].append(observation[i])
        ts.append(t/50)
        t += 1

5.表示します

 次で4.項で計算した結果を表示します。

(1)observationsを使ってグラフを書きます。

 手直しできるよう簡単に使っている命令を解説します。
 fig1=plt.figure(figsize=(10,8)) 縦10、横8の図を生成
 sub1=fig1.add_subplot(421) サブプロット縦4、横2分割の場所1に追加。
 sub1.plot(ts,observations[0],label="hull angle") 横ts、縦observations[0]の折れ線追加
                     ラベル名はhull angle

 sub1.legend()       ラベルを表示

 後は上記の繰り返しです。

(2)frames内のimageを使って動画を作成します。

  fig2 = plt.figure(figsize=(6, 6))以降で図を生成して動画を作っています。

import matplotlib.pyplot as plt
import matplotlib.animation
import numpy as np
from IPython.display import HTML

fig1 = plt.figure(figsize=(10, 8))
sub1 = fig1.add_subplot(421)
sub1.plot(ts,observations[0],label="hull angle")
sub1.plot(ts,observations[1],label="hull angle Velocity")
sub1.legend()

sub2 = fig1.add_subplot(422)
sub2.plot(ts,observations[2],label="Velocity x")
sub2.plot(ts,observations[3],label="Velocity y")
sub2.legend()

sub3 = fig1.add_subplot(423)
sub3.plot(ts,observations[4],label="Hip1 Angle")
sub3.plot(ts,observations[9],label="Hip2 Angle")
sub3.legend()

sub4 = fig1.add_subplot(424)
sub4.plot(ts,observations[5],label="Hip1 Angle Velocity")
sub4.plot(ts,observations[10],label="Hip2 Angle Velocity")
sub4.legend()

sub5 = fig1.add_subplot(425)
sub5.plot(ts,observations[6],label="Knee1 Angle")
sub5.plot(ts,observations[11],label="Knee2 Angle")
sub5.legend()

sub6 = fig1.add_subplot(426)
sub6.plot(ts,observations[7],label="Knee1 Angle Velocity")
sub6.plot(ts,observations[12],label="Knee2 Angle Velocity")
sub6.legend()

sub7 = fig1.add_subplot(427)
sub7.plot(ts,observations[8],label="Leg1 Touch Ground")
sub7.plot(ts,observations[13],label="Leg2 Touch Ground")
sub7.legend()

plt.show()

fig2 = plt.figure(figsize=(6, 6))
plt.axis('off')

images = []
for f in frames:
  image = plt.imshow(f)
  images.append([image])
ani = matplotlib.animation.ArtistAnimation(fig2, images, interval=30, repeat_delay=1)

HTML(ani.to_jshtml())

 こんな感じでしゃがみます。

f:id:akifukka:20191206232803j:plain

f:id:akifukka:20191207125142j:plain

 

 足の指令値として同じ値を与えていても、地面からの反力等の影響で必ずしも同じにはなりません。動かしてみて、足の指令(角度の速度指令)は次の方向だとわかりました。

腿(HIP)   前に出す方が +

膝(KNEE) しゃがむ方向が -

6.observation

githubでgym/envs/box2d/bipedal_walker.pyを見てみます。

    def step(self, action):
        #self.hull.ApplyForceToCenter((0, 20), True) -- Uncomment this to receive a bit of stability help
        control_speed = False  # Should be easier as well
        if control_speed:
            self.joints[0].motorSpeed = float(SPEED_HIP  * np.clip(action[0], -1, 1))
            self.joints[1].motorSpeed = float(SPEED_KNEE * np.clip(action[1], -1, 1))
            self.joints[2].motorSpeed = float(SPEED_HIP  * np.clip(action[2], -1, 1))
            self.joints[3].motorSpeed = float(SPEED_KNEE * np.clip(action[3], -1, 1))
        else:
            self.joints[0].motorSpeed     = float(SPEED_HIP     * np.sign(action[0]))
            self.joints[0].maxMotorTorque = float(MOTORS_TORQUE * np.clip(np.abs(action[0]), 0, 1))
            self.joints[1].motorSpeed     = float(SPEED_KNEE    * np.sign(action[1]))
            self.joints[1].maxMotorTorque = float(MOTORS_TORQUE * np.clip(np.abs(action[1]), 0, 1))
            self.joints[2].motorSpeed     = float(SPEED_HIP     * np.sign(action[2]))
            self.joints[2].maxMotorTorque = float(MOTORS_TORQUE * np.clip(np.abs(action[2]), 0, 1))
            self.joints[3].motorSpeed     = float(SPEED_KNEE    * np.sign(action[3]))
            self.joints[3].maxMotorTorque = float(MOTORS_TORQUE * np.clip(np.abs(action[3]), 0, 1))

        self.world.Step(1.0/FPS, 6*30, 2*30)

        pos = self.hull.position
        vel = self.hull.linearVelocity

        for i in range(10):
            self.lidar[i].fraction = 1.0
            self.lidar[i].p1 = pos
            self.lidar[i].p2 = (
                pos[0] + math.sin(1.5*i/10.0)*LIDAR_RANGE,
                pos[1] - math.cos(1.5*i/10.0)*LIDAR_RANGE)
            self.world.RayCast(self.lidar[i], self.lidar[i].p1, self.lidar[i].p2)

        state = [
            self.hull.angle,        # Normal angles up to 0.5 here, but sure more is possible.
            2.0*self.hull.angularVelocity/FPS,
            0.3*vel.x*(VIEWPORT_W/SCALE)/FPS,  # Normalized to get -1..1 range
            0.3*vel.y*(VIEWPORT_H/SCALE)/FPS,
            self.joints[0].angle,   # This will give 1.1 on high up, but it's still OK (and there should be spikes on hiting the ground, that's normal too)
            self.joints[0].speed / SPEED_HIP,
            self.joints[1].angle + 1.0,
            self.joints[1].speed / SPEED_KNEE,
            1.0 if self.legs[1].ground_contact else 0.0,
            self.joints[2].angle,
            self.joints[2].speed / SPEED_HIP,
            self.joints[3].angle + 1.0,
            self.joints[3].speed / SPEED_KNEE,
            1.0 if self.legs[3].ground_contact else 0.0
            ]
        state += [l.fraction for l in self.lidar]
        assert len(state)==24
        .
        .
        .return np.array(state), reward, done, {}

 最後の方にdef stepという記述があり、この関数が実際のモデルの中枢です。上の方で指令値actionを実際のモデルに適用して、最後の方でstate(observationに相当)を作っています。

 上記ソースコードからobservationの中身は次の通りです。

  0:胴体角度
 1:胴体角速度
 2:x方向移動速度
 3:y方向移動速度
 4:足1のHIP(腿)角度
 5:足1のHIP(腿)角速度
 6:足1のKNEE(膝)角度
 7:足1のKNEE(膝)角速度
 8:足1接地で1
 9:足2のHIP(腿)角度
10:足2のHIP(腿)角速度
11:足2のKNEE(膝)角度
12:足2のKNEE(膝)角速度
13:足2接地で1
14~23:Lidar(レーザレーダ、障害物検知用)、ここでは省略します。

actionの値を変更する等、試してみるといいと思います。

つづく

  強化学習 カテゴリーの記事一覧 - やってみた!

 

Jetson NanoでTacotron2(音声合成)を動かす

 Jetson nanoでTacotron2を動かしてみます。Tacotron2の詳細は記事をご覧ください。

 なお本記事は2019年11月末時点のものです。ツールのバージョンアップ等により、この手順で動かなくなることもありうるので、あらかじめご容赦ください。

github.com

先に結果を報告します。

  • Jetson NanoでTacotron2+WaveGlowが動きました。
  • 実行にかかる時間は約3分ちょいでした(そのうちモデル読み込み時間が約2分)。nvpmodeはmax(10W 4コア)、クロックも最大設定です(gpu 921.6MHz)。
  • NVIDIA実装の英語のままです。
  • メモリはぎりぎりで、Tacotron2、WaveGlowの片方でほぼ一杯。Nanoは4GBのメモリがありますが、常時約1.6GBはシステムが占有しており、残りだと精一杯のようです。今回、Docker Containerで動かしましたが、ホストOS側でSwap設定が必須です。
  • インターネットブラウザ等、他のアプリは全て落として動かさないと、メモリSwapでえらく時間がかかります(下手すると数10分以上)。
  • 高速化の余地はだいぶありそうですが、専門的知識、経験が無いと難しいかも。

 興味がある方は以下の手順で試してみてください。環境構築するのに1,2時間かかります。docker containerを使うのでSDカードは容量に余裕があるもの(64GB以上)を推奨します。だいたいcontainerで5GB~6GBくらい消費します。

 あと、Jetson NanoはHDMI経由で音を出します。HDMIに接続されているディスプレイにスピーカがついているか事前に確認が必要です。

 なお、Jetsonを持ってないけど試してみたい方はこちら。Google Colaboratoryで試しています。
akifukka.hatenablog.com

 1.JETPACK4.2.2の準備

 本記事ではDocker Containerを使います。Containerを使うにはJETPACK4.2.2以降が必要です。JETPACK4.2.2のセットアップは次を参考にしてください。

 メモリSwapもJETCARDをインストールすると自動的に設定してくれます。

akifukka.hatenablog.com

2.DeepStream-l4t Containerの作成、起動

 Tacotron2をContainerで動かします。まずはl4t-base Container(r32.2.1)を作成、起動します。

sudo xhost +si:localuser:root
sudo docker run --runtime nvidia --network host -it -e DISPLAY=$DISPLAY -v /tmp/.X11-unix/:/tmp/.X11-unix --device /dev/snd:/dev/snd nvcr.io/nvidia/l4t-base:r32.2.1

4.環境構築

 Containerの端末で次を実行します。

  • update
    apt-get update
  • PIP3
    wget https://bootstrap.pypa.io/get-pip.py
    python3 get-pip.py
  • git
    apt-get install git
  • apex pip3でlibblas.so.3 errorが出るのを回避
    apt-get install libatlas3-base
  • コンパイラ
    apt-get install -y --no-install-recommends make g++
  • python3の開発ツール
    apt-get install python3.6-dev
  • pytorch1.0 少し時間がかかります。
    wget https://nvidia.box.com/shared/static/2ls48wc6h0kp1e58fjk21zast96lpt70.whl -O torch-1.0.0a0+bb15580-cp36-cp36m-linux_aarch64.whl

pip3 install numpy torch-1.0.0a0+bb15580-cp36-cp36m-linux_aarch64.whl

  • scipy 
    apt-get install python3-scipy

 なお、python2,3自体は最初からcontainerにインストールされています。

5.Tacotron2関係のインストール

(1)tacotron2

git clone https://github.com/NVIDIA/tacotron2.git

(2)submodule(waveglowなど)導入
waveglowを最新版にしないと後でdenoiserが無いというエラーが出るので、--remote --mergeで最新版に更新しています。

 以降はtacotron2フォルダで作業します。

cd tacotron2
git submodule init
git submodule update --remote --merge

(3)Apexインストール

git clone https://github.com/NVIDIA/apex
cd apex
pip3 install -v --no-cache-dir ./

(4)matplotlib

apt-get install python3-matplotlib=2.1.1-2ubuntu3

 地域を聞かれるのでAsia-Tokyoを選択します。

(5)tensorflowインストール

apt-get install libhdf5-serial-dev hdf5-tools libhdf5-dev zlib1g-dev zip libjpeg8-dev python3-h5py
pip3 install -U future==0.17.1 mock==3.0.5 keras_preprocessing==1.0.5 keras_applications==1.0.6 enum34 futures testresources setuptools protobuf
pip3 install --extra-index-url https://developer.download.nvidia.com/compute/redist/jp/v42 tensorflow-gpu==1.13.1+nv19.3
pip3 install tensorboardX==1.1tensorboardX==1.1 unidecode==1.0.22 pillow inflect==0.2.5 

(6)librosaインストール

 librosaのインストールは次のホームページに助けられました。Thanks!

https://learninone209186366.wordpress.com/2019/07/24/how-to-install-the-librosa-library-in-jetson-nano-or-aarch64-module/

ア. llvmliteのインストール

apt-get install llvm-7
cd /usr/bin
ln -s llvm-config-7 llvm-config
cd /tacotron2
pip3 install llvmlite
apt-get install libblas-dev liblapack-dev libatlas-base-dev gfortran

イ.cython

pip3 install cython

ウ.pip3アップグレード

pip3 install --upgrade pip

エ.librosaのインストール 時間がかかります

pip3 install librosa==0.6.0

6.学習済みモデルのダウンロード

(1)WaveGlow

wget https://api.ngc.nvidia.com/v2/models/nvidia/waveglow_ljs_256channels/versions/3/files/waveglow_256channels_ljs_v3.pt

(2)Tacotron2

wget --load-cookies /tmp/cookies.txt "https://docs.google.com/uc?id=1c5ZTuT7J08wLUoVZ2KkUs_VdZuJ86ZqA&export=download&confirm=$(wget --quiet --save-cookies /tmp/cookies.txt --keep-session-cookies --no-check-certificate 'https://docs.google.com/uc?id=1c5ZTuT7J08wLUoVZ2KkUs_VdZuJ86ZqA&export=download&id=FILEID' -O- | sed -rn 's/.*confirm=([0-9A-Za-z_]+).*/\1\n/p')&id=FILEID" -O tacotron2_statedict.pt && rm -rf /tmp/cookies.tx

7.Pythonからサウンド出力するためのライブラリ

apt-get install libasound-dev
apt-get install portaudio19-dev python-all-dev
pip3 install pyaudio

 8.動かしてみる

①inference.pyとして以下をtactron2フォルダに置く。viエディタで 'i’ でインサートモードにしてコピペすれば複製できます。

②他のソフト、Chrome等を全て終了させます。終了させないとメモリが足りなくなって時間がものすごくかかったりします。

③実行します。

python3 inference.py

④warningがたくさん出ますが気にしないで、高速化設定のJetson Nanoで3分半ほど待つとmel spectrogramの画面が表示されます。画面をx印をクリックして終了させると音声が出ます。日本語風はあまり聞き取れませんが・・・。

・waveglow is really awesome!

・こんにちわ、今日もいい天気ですね!

 exitを入力するとcontainerを終了させることができます。

import matplotlib
import matplotlib.pylab as plt
import sys
sys.path.append('waveglow/')
import numpy as np
import torch
import pyaudio
import gc
import time

from hparams import create_hparams
from model import Tacotron2
from layers import TacotronSTFT, STFT
from audio_processing import griffin_lim
from train import load_model
from text import text_to_sequence
from denoiser import Denoiser

def plot_data(data, figsize=(16, 4)):
    fig, axes = plt.subplots(1, len(data), figsize=figsize)
    for i in range(len(data)):
        axes[i].imshow(data[i], aspect='auto', origin='bottom',
                       interpolation='none')

torch.cuda.synchronize()
time0 = time.time()

hparams = create_hparams()
hparams.sampling_rate = 22050

checkpoint_path = "tacotron2_statedict.pt"
model = load_model(hparams)
model.load_state_dict(torch.load(checkpoint_path)['state_dict'])
_ = model.cuda().eval().half()

torch.cuda.synchronize()
time1 = time.time()
print('Tacotron2 load',time1 - time0)

text = "Waveglow is really awesome!"
sequence = np.array(text_to_sequence(text, ['english_cleaners']))[None, :]
sequence = torch.autograd.Variable(
    torch.from_numpy(sequence)).cuda().long()

mel_outputs, mel_outputs_postnet, _, alignments = model.inference(sequence)

text2 = "kon nichiwa!,kyou moiitenkidesue ne!"
sequence2 = np.array(text_to_sequence(text2, ['english_cleaners']))[None, :]
sequence2 = torch.autograd.Variable(
    torch.from_numpy(sequence2)).cuda().long()

mel_outputs2, _, _, _ = model.inference(sequence2)

torch.cuda.synchronize()
time2 = time.time()
print('Tacotron2 infer ',time2 - time1)

del model
gc.collect()

torch.cuda.synchronize()
time3 = time.time()

waveglow_path = 'waveglow_256channels_ljs_v3.pt'
waveglow = torch.load(waveglow_path)['model']
waveglow.cuda().eval().half()
for k in waveglow.convinv:
    k.float()
denoiser = Denoiser(waveglow)

torch.cuda.synchronize()
time4 = time.time()
print('waveglow init ',time4 - time3)

with torch.no_grad():
    audio = waveglow.infer(mel_outputs_postnet, sigma=0.666)
    audio2 = waveglow.infer(mel_outputs2, sigma=0.666)

torch.cuda.synchronize()
time5 = time.time()
print('waveglow infer ',time5 - time4)

np.save('audio',audio[0].data.cpu().numpy())
np.save('audio2',audio2[0].data.cpu().numpy())

list_audio = []
for i in audio[0].data.cpu().numpy():
    list_audio.append(i)
    list_audio.append(i)

audio1_2 = np.array(list_audio)

list_audio2 = []
for i in audio2[0].data.cpu().numpy():
    list_audio2.append(i)
    list_audio2.append(i)

audio2_2 = np.array(list_audio2)

p = pyaudio.PyAudio()
stream = p.open(format = pyaudio.paFloat32,
                channels = 1,
                rate = 44100,
                output = True,
                frames_per_buffer = 1024)

plot_data((mel_outputs.float().data.cpu().numpy()[0],
           mel_outputs_postnet.float().data.cpu().numpy()[0],
           alignments.float().data.cpu().numpy()[0].T))
plt.show()

print("play")
stream.write(audio1_2.astype(np.float32).tostring())
stream.write(audio2_2.astype(np.float32).tostring())

stream.close()

f:id:akifukka:20191130204515p:plain

9.containerの再実行

・CONTAINER_IDを取得します。
 sudo docker ps -a

・実行させます。-iを忘れずに。
 sudo docker start -i CONTAINER_ID

・なお、CONTAINER_IDをわかりやすい名前に変えることもできます。
 sudo docker rename CONTAINER_ID 変更後のCONTAINER_ID

10.まとめ

  Jetson NanoでTacotron2音声合成を試してみました。かろうじて動きましたが、リアルタイムとは行きませんでした。

一応、2行分で、time関数で処理時間を計測した結果は次の通り。

 tacotron2 6.5s、waveglow 20.6s

 実際はモデル読み込みに2分38秒、その他ソフト初期化等で30秒弱といったところです。高速化チューニングで処理時間(モデル読み込み、ソフト初期化を除く)を数分の一程度に短縮できる可能性はあると思いますが、それでもまだまだですね。

 といっても、Jetsonザビエルあたりだとリアルタイムに動くかも知れません。ハードウェアの高速化を待った方がよさそうですね。

 日本語化も課題ですが、ちょっと簡単にはいかなさそうなので、次どうするかは未定です。

Jetson Nano カテゴリーの記事一覧へ

音声合成 カテゴリーの記事一覧へ

Tacotron2を調べてみた3

  前回はDecoderの概要部分でしたので、次は中身を見ていきたいと思います。

 2.6 decode

 クラスDecoderのinference中の次の文の中身を見てみます。

 mel_output, gate_output, alignment = self.decode(decoder_input)

 この文は、クラスDecoderのメソッドdecodeを呼び出しています。メソッドdecodeを次に示します。decoder_inputは前回推定したmel spectrogramをprenetに通したものでで大きさ(1,256)のテンソルです。

    def decode(self, decoder_input):
        """ Decoder step using stored states, attention and memory
        PARAMS
        ------
        decoder_input: previous mel output

        RETURNS
        -------
        mel_output:
        gate_output: gate output energies
        attention_weights:
        """
        cell_input = torch.cat((decoder_input, self.attention_context), -1)
        self.attention_hidden, self.attention_cell = self.attention_rnn(
            cell_input, (self.attention_hidden, self.attention_cell))
        self.attention_hidden = F.dropout(
            self.attention_hidden, self.p_attention_dropout, self.training)

        attention_weights_cat = torch.cat(
            (self.attention_weights.unsqueeze(1),
             self.attention_weights_cum.unsqueeze(1)), dim=1)
        self.attention_context, self.attention_weights = self.attention_layer(
            self.attention_hidden, self.memory, self.processed_memory,
            attention_weights_cat, self.mask)

        self.attention_weights_cum += self.attention_weights
        decoder_input = torch.cat(
            (self.attention_hidden, self.attention_context), -1)
        self.decoder_hidden, self.decoder_cell = self.decoder_rnn(
            decoder_input, (self.decoder_hidden, self.decoder_cell))
        self.decoder_hidden = F.dropout(
            self.decoder_hidden, self.p_decoder_dropout, self.training)

        decoder_hidden_attention_context = torch.cat(
            (self.decoder_hidden, self.attention_context), dim=1)
        decoder_output = self.linear_projection(
            decoder_hidden_attention_context)

        gate_prediction = self.gate_layer(decoder_hidden_attention_context)
        return decoder_output, gate_prediction, self.attention_weights

ソースコードだと全体像がわかりずらいので図にしました。

f:id:akifukka:20191127222347j:plain


decodeの入力
・decoder_input    前回のmel spectrogramをprenetに通したもの
・encoder_output  encoderの出力
decoderの出力
・attention_weights 位置検出結果、出力は状態確認(表示)で使用しているのみ
・gate_prediction  終了判断に使用
・decoder_output  mel spectrogram

 構造を見てわかるように、attention_hidden、processed_memory、attention_weights_catの3信号をQueryとして、encoder出力をKey及びValueとしてAttentionを行っています。

 ここから先は推測まじりですが、

 終了を最後のgate_layerで判定できているので、attention_layerの出力attention_contextにどこまで進んだかを示す信号が含まれていると思われます。
 encoderの出力は文字単位なので、attention_contextも基本的には文字単位のデータなのですが、このデータの中に1文字の間に変化する音の情報も含まれていると思われます。この後の処理のどこかで、このデータから今の瞬間の音に関する情報を抽出してmel spectrogramを生成しているのでしょう。ただし、attentionの後ろに複雑なネットワーク構造は無いので、1文字中ではあまり複雑な変化に対応できないかも知れません。

2.7 Location Sensitive Attenation

 こちらもわかりやすよう図にしました。

f:id:akifukka:20191128211043j:plain

 get_alignment_energiesで現在が各文字の位置にある可能性を推測、softmaxで正規化しています。その後、現在の位置に相当する文字に相当する情報をattention_contextとしてエンコーダの出力から抽出します。

 2.6 decodeの項に戻り、さらにいくつか処理をして現在のmel_spectrogramと、終了を示すフラグgate_predictionを推測します。

2.8 Post Net

 Post Netは5層の1次元Convolution Network(畳み込みニューラルネットワーク)です。

mel_outputs_postnet = self.postnet(mel_outputs) mel_outputs_postnet mel_outputs_postnet = mel_outputs + mel_outputs_postnet

Postnetの仕様は次の通りです。

・入力 80
・フィルタ数 512 フィルタ長 5
・出力80

 mel_outputsの大きさは(1,80,n)、nは時間方向のspectrum数です。

 時間方向に畳み込みをしてmel spectrogramの時間方向のなめらかさ?改善のための補正量を推定し、Post Netを通る前のmel_outputsに加算して補正をかけます。

 個人的には補正前後で大きな違いは感じられませんでした。

 2.9 Tactron2.inferenceの出力

 クラスTactoron2のinferenceからparse_output経由で出力されます。

outputs = self.parse_output(
[mel_outputs, mel_outputs_postnet, gate_outputs, alignments])

return outputs

 ・mel_outputs、mel_outputs_postnet
 mel spectrogramです。テンソルのサイズは(1,80,n)でnは時間軸方向のデータ数です。

・gate_outputs
 終了フラグです。サイズは(1,n,1)です。その時々のgateの値を保管したものです。基本的に、音声合成では使いません。

・alignments
 位置あわせを示すテンソルです。サイズは(1,n,文字数)でnは時間軸方向のデータ数です。

3. 補足 Tacotron2のinference実行

 Tacotron2のプロジェクトフォルダにあるJupitorノートブックでは次の流れで呼び出しています。一応補足まで。

①from train import load_model
 load_modelをインポート

②model = load_model(hparams)
 load_model呼び出して、modelを生成。

③train.pyのload_model
 def load_model(hparams):
 model = Tacotron2(hparams).cuda()
 ・・・
 return model

 ここで、クラスTacotron2を生成してリターンしているので、②のmodelはTacotron2になる。

④inference呼び出し
 mel_outputs, mel_outputs_postnet, _, alignments = model.inference(sequence)
 model=Tacotron2なので、Tacotron2のメソッドinferenceを呼び出します。ここから今まで見てきた処理が始まります。

4.まとめ

 NVIDIA実装をもとにTacotron2の構造を見てみました。次回はJetson Nanoで動かしてみます。

音声合成 カテゴリーの記事一覧へ

Tacotron2を調べてみた2

 前回の続きです。誤記、わかりやすくするため、たびたび加筆、修正するかも知れませんが、ご容赦頂きたく。
akifukka.hatenablog.com

 2.3 Decoderの概要

 ここからはmodel.pyのクラスTacotron2のinferenceの次の文の中身になります。Decoderはちょっと入り組んでいるので、最初に概要を解説した後に構成要素を詳しく見ていくことにします。

 次の文はクラスDecoderのinferenceを呼び出します。

mel_outputs, gate_outputs, alignments = self.decoder.inference(
   encoder_outputs)

 クラスDecoderのinferenceを次に示します。

    def inference(self, memory):
        """ Decoder inference
        PARAMS
        ------
        memory: Encoder outputs

        RETURNS
        -------
        mel_outputs: mel outputs from the decoder
        gate_outputs: gate outputs from the decoder
        alignments: sequence of attention weights from the decoder
        """
        decoder_input = self.get_go_frame(memory)

        self.initialize_decoder_states(memory, mask=None)

        mel_outputs, gate_outputs, alignments = [], [], []
        while True:
            decoder_input = self.prenet(decoder_input)
            mel_output, gate_output, alignment = self.decode(decoder_input)

            mel_outputs += [mel_output.squeeze(1)]
            gate_outputs += [gate_output]
            alignments += [alignment]

            if torch.sigmoid(gate_output.data) > self.gate_threshold:
                break
            elif len(mel_outputs) == self.max_decoder_steps:
                print("Warning! Reached max decoder steps")
                break

            decoder_input = mel_output

        mel_outputs, gate_outputs, alignments = self.parse_decoder_outputs(
            mel_outputs, gate_outputs, alignments)

        return mel_outputs, gate_outputs, alignments

 論文のブロック図を再度掲載します。

f:id:akifukka:20191118220212j:plain
 (1)Decoderの初期化

decoder_input = self.get_go_frame(memory)
 decoder_inputは前回推定したmel spctrogramをいれるテンソルで、2layer Pre-Netに入力するものです。最初は全て0に初期化します。get_go_frameはクラスDecoder内で定義されているメソッドです。memoryのサイズは(1,文字数,512)、ここでのdecoder_inputのサイズは(1,80)です。

self.initialize_decoder_states(memory, mask=None)
 decoderの内部状態の初期化をします。また、memory(Encoder出力)を内部変数のself.memoryに保管して、これ以降関数呼び出しでmemoryを引数に設定する必要を無くしています。

(2)whileループ

 発声の時間軸を追って1タイムステップ(256/22050=11.6ms?)ずつ、mel spectrogramを推定していきます。

decoder_input = self.prenet(decoder_input)
 decoder_inputには前回推定したmel spectrogramが入っています。これを256要素、全結合2層ニューラルネットワークのprenetに通します。
 GoogleのTacotron2の論文によると、このprenetによる情報の制限はattntionの学習の本質にかかわるとのことです。
 Encoderから入力される信号は基本的に文字数と同じですが、12.5msずつ推定されるmel spectrogramの時間の長さと1対1の関係になりません。そのため何らかの方法で、ある文字の発声の中のどのあたりを推定しているのかを知る必要があります。前回のmel spectrogramの情報を使うことで、どこまで進んだかを知ることができます。
 prenetを通した後のdecoder_inputのサイズは(1,256)です。

mel_output, gate_output, alignment = self.decode(decoder_input)
mel spectrogram、gate_outpupt、を推定します。詳細は別途。
・mel spectrogram
 1ステップ分のmel spectrogramで、テンソルの大きさは(1,80)です。
・gate_output
 終了判定用信号で大きさは(1,1)です。
・alignment
 今どの文字部分の推定をしているかを示すテンソルで大きさは(1,文字数)です。

③1ステップ分の推定値をリストに追加
mel_outputs += [mel_output.squeeze(1)]
gate_outputs += [gate_output]
alignments += [alignment]

④終了判定
if torch.sigmoid(gate_output.data) > self.gate_threshold:
     break 
gate_outputの値をsigmoid関数を通した後の値で終了判定をしています。gate_thresholdは0.5です。

⑤mel_outputをdecoder_input に代入して、①に戻ります。

(3)出力準備

 次の関数内でテンソルのリストを結合し、軸を増やしてテンソルに変換します(toarch.stack関数を使用)。

mel_outputs, gate_outputs, alignments = self.parse_decoder_outputs(
       mel_outputs, gate_outputs, alignments)

 2.4 クラスPrenet

 prenetは前回のステップで推定したmel sptectrogram(これから推定するものに対して11.6ms前のmel sptectrogram)からLocation Sensitive Attentionに入力する特徴を抽出するものです。主に今どこの時間の処理をしているかをAttentionに伝える働きをしていると思われます。
 prenetは次の文で呼び出していますが、prenetはクラスDecodeのメソッドでは無く、独立したクラスPrenetのインスタンスです。

decoder_input = self.prenet(decoder_input)

 self.prenetはクラスDecoderの__init__(コンストラクタ)の次の文でクラスPrenetのインスタンスとして生成されています。

self.prenet = Prenet(
  hparams.n_mel_channels * hparams.n_frames_per_step,
  [hparams.prenet_dim, hparams.prenet_dim])

 パラメータは次の通りで、入力80、大きさ256の隠れ層を2層もった出力256のネットワークを生成します。
hparams.n_mel_channels = 80
hparams.n_frames_per_step = 1
hparams.prenet_dim = 256

 クラスPrenetを次に示します。

class Prenet(nn.Module):
    def __init__(self, in_dim, sizes):
        super(Prenet, self).__init__()
        in_sizes = [in_dim] + sizes[:-1]
        self.layers = nn.ModuleList(
            [LinearNorm(in_size, out_size, bias=False)
             for (in_size, out_size) in zip(in_sizes, sizes)])

    def forward(self, x):
        for linear in self.layers:
            x = F.dropout(F.relu(linear(x)), p=0.5, training=True)
        return x

 クラスPrenetの__init__でin_sizes=[in_dim]+size[:-1]のsize[:-1]で層の入力数を、sizesで出力数を設定しています。-1はsizeの後ろから数えて1つ目を意味し、in_sizesは最初はin_dim~最後から1層前の層の要素数のリストになります。LinearNormはTacotron2プロジェクトのlayers.pyで定義されており、nn.Linearを使った線形ニューラルネットワークです。

 さて、prenet(decoder_input)はメソッド(関数)として使っていますが、実体はどこでしょうか?。自分が実体を探すのに手間取ったので、ちょっとpythonの文法について触れておきたいと思います。

 メソッド名が省略されると、pythonでは__call__メソッドが呼ばれます。クラスPrenetには__call__の記載がありませんが、Prenetはpytorchのnn.Moduleを継承しているので、nn.Moduleの__call__が呼ばれます。

  pytorchのソースはGithubにあり、torch/nn/modules/module.pyにクラスModuleのの中身があります。

 GitHub - pytorch/pytorch: Tensors and Dynamic neural networks in Python with strong GPU acceleration

 クラスnn.Moduleの__call__はニューラルネットワークの推定処理forwardに加えて、あらかじめユ-ザがHookとして登録したメソッドを一緒に実行するよう作られています。Hookの登録方法等はpytorchのnnのドキュメントに書かれています。ご参考まで。

torch.nn — PyTorch master documentation

2.5 Attention(注意機構)の概要

  ここではイメージをつかむためAttentionの基本について簡単に紹介します。

 Attentionはある情報に着目(注意)して出力を作る仕組みのことです。Tactron2では、”1ステップ前は、どの文字のどこのmel spectrogramを作った”という情報に着目して、”じゃあ次のspectrumはこれ”といった感じで動きます。

f:id:akifukka:20191123182335j:plain

 機械翻訳を例にすると、

①"Ihave a pen."をencoderで分析します。言葉の場合、順番が重要なのでLSTM等を使って語句の順番も踏まえて分析します。

②Attentionを使ってdecodeします。Attentionの代表的な作動は次の通りです。

・入力はquery、key、value。keyとvalueはencoderの出力情報、queryはdecoderの直前の出力とすることが多い。

・queryにattention(注意)して、最も親和性の高い(関連する、一致する)keyを抽出。

・keyに相当するvalueを抽出して出力とする。

③例えば、"私は"が前回のdecoder出力だとすると、queryとして”私は"をattentionに入力、次に来るのが動詞、目的語どちらが良いかを計算(テンソルの掛け算)すると、より好ましい方(例えば目的語)の評価値が大きくなる。

④続いてvalueテンソルと掛け算すると、評価値の高いkeyである目的語に相当するvalue”ペンを”の評価値が高くなるので、次の出力として”ペンを”を選ぶ。

 実際はこんなに簡単にはならない(主語の次は目的語が良いという情報はどこに含ませる?等)ので、ニューラルネットワークを挿入して情報を補完する必要があります。

 Tactron2では、Attentionを音声合成がどこまで進んだかを検出する(alignment)のに使っています。前回のmel spectrogramをQueryとする他、内部状態も使って位置あわせをしています。前回のmel spectrogramだけでは文章中の同じようなspqctrum=発音と混同するなど、間違う可能性があるということなのでしょう(だからLocation Sensitive Attentionという名称なのかも知れません)。

 Tacotron2の論文からAttention部分について引用されている論文へのリンクを2つ貼り付けておきます。

 Tacotron2は音声合成ですが学習時は音声とうまくスペルの位置を合わせられないと、まともに学習できないだろうと想像されます。学習側のソースは読んでないので想像ですが・・・。Attentionの規模が大きいのにも納得です・・。

arxiv.org

まだつづきます

音声合成 カテゴリーの記事一覧へ

Tacotron2を調べてみた1

 以前Colaboratoryで試して、英語の音声合成ができることはわかったので、日本語を目標にまずはtacotron2の中身を解説してみます。Googleの論文とNVIDIA実装を中心に見ていきます。

過去にColaboratoryで試した時の記事
akifukka.hatenablog.com

 GoogleのTacotron2論文

arxiv.org

NVIDIAによるTacotron2実装

github.com

 1.概要

 Tacotron2はGoogleで開発されたTTS(Text To Speech)アルゴリズムです。テキストをmel spectrogramに変換、mel spectrogramを音声波形に変換するという大きく2段の処理でTTSを実現しています。本家はmel spectrogramを音声波形に変換する箇所はWavenetからの流用で、Tacotron2の本体はテキストからmel spectrogramへの変換部分と言えます。なお、NVIDIA実装では処理軽量化のため音声波形への変換部分をWaveGlowに変更しています。

 次に示すのは論文に掲載されていた全体構造の図(ちょっと追記しました)です。Tacotron2の処理は大きく次の4つに分けられます。
①Character Enbedding
②Encoder
③Decoder
・Location Sensitive Attenation
④Post Net

 また、図にはありませんがTacotron2に入力する前に省略表記(Mr、Missや、1st、2ndとか)を直す等の前処理が必要です。

f:id:akifukka:20191116212142j:plain

 Tactron2はエンコーダ・デコーダモデルです。エンコーダで入力データを解析、入力データを中間データに変換し、デコーダで中間データの情報を出力データに変換します。出力データ長を自在に変えられるのが特徴で、自動翻訳などで使われます。

2.NVIDIA実装

 NVIDIA実装を使ってTacotron2の仕組みを見ていきます。
 NVIDIA実装におけるTacotron2の主体は、model.pyのclass Tacotron2です。クラスTacotron2のinferenceを次に示します。

    def inference(self, inputs):
        embedded_inputs = self.embedding(inputs).transpose(1, 2)
        encoder_outputs = self.encoder.inference(embedded_inputs)
        mel_outputs, gate_outputs, alignments = self.decoder.inference(
            encoder_outputs)

        mel_outputs_postnet = self.postnet(mel_outputs)
        mel_outputs_postnet = mel_outputs + mel_outputs_postnet

        outputs = self.parse_output(
            [mel_outputs, mel_outputs_postnet, gate_outputs, alignments])

        return outputs

2.1 Character Embedding

 Embedding(日本語で組み込みの意味)はニューラルネットワークに入力できるよう、入力それぞれにテンソルを割り当てる処理のことを言います。今回の場合は65種類の入力(文字)にそれぞれ512次元のテンソルを割り当てます。テンソルの各項目の値を学習で変化させることで値に特徴の意味を持たせて分類することができます。例えば大文字のAと小文字のaでは実質同じとかです。

 Character EmbeddingはクラスTacotron2のinferenceの次の文で設定しています。

embedded_inputs = self.embedding(inputs).transpose(1, 2)

 この文はtorch.nn.embedding()を呼び出してembeddingの設定をします。transposeはテンソルの軸の入れ替えで、後で実行するConvolutional networkにあわせて軸(文字の順番とテンソルの次元)を入れ替えています。(1,文字数,512)→(1,512,文字数)。

 self.embeddingはclass Tacotron2 __init__で次のように定義されています。

self.embedding = nn.Embedding(
 hparams.n_symbols, hparams.symbols_embedding_dim)
std = sqrt(2.0 / (hparams.n_symbols + hparams.symbols_embedding_dim))
val = sqrt(3.0) * std # uniform bounds for std
self.embedding.weight.data.uniform_(-val, val)

 パラメータは次の通りです。
hparams.n_symbols = アルファベット大文字+小文字+記号類でたぶん65
hparams.symbols_embedding_dim = 512

参考

Word Embeddings: Encoding Lexical Semantics — PyTorch Tutorials 1.3.0 documentation

2.2 Encoder

 EncoderはクラスTacotron2のinferenceの次の文で処理しています。__init__でself.encoder = Encoder(hparams)を宣言しているので、次の文でクラスEncoderのinferrenceが呼び出されます。

encoder_outputs = self.encoder.inference(embedded_inputs)

 class Encoderのinferenceを次に示します。

    def inference(self, x):
        for conv in self.convolutions:
            x = F.dropout(F.relu(conv(x)), 0.5, self.training)

        x = x.transpose(1, 2)

        self.lstm.flatten_parameters()
        outputs, _ = self.lstm(x)

        return outputs

①3層Convolutionalネットワーク

 上記inference中のconvolutionsはclass Encoderの__init__で次のように定義されています。

for _ in range(hparams.encoder_n_convolutions):
            conv_layer = nn.Sequential(
                ConvNorm(hparams.encoder_embedding_dim,
                         hparams.encoder_embedding_dim,
                         kernel_size=hparams.encoder_kernel_size, stride=1,
                         padding=int((hparams.encoder_kernel_size - 1) / 2),
                         dilation=1, w_init_gain='relu'),
                nn.BatchNorm1d(hparams.encoder_embedding_dim))
            convolutions.append(conv_layer)

ここで、hparams.encoder_n_convolutions = 3です。
さらにConvNormはlayers.pyでクラスConvNormとして次のように定義されています。

self.conv = torch.nn.Conv1d(in_channels, out_channels,
  kernel_size=kernel_size, stride=stride,
  padding=padding, dilation=dilation,
  bias=bias)

 パラメータは次の通りです。
in_channels = 512, out_channels = 512, kernel_size = 5, stride = 1, padding = 2, dilation = 1, bias = True

 5 × 512のfilterを512種使ったConvolutional(折り畳みネットワーク)が3層定義されています。

 この3層Convolutionalネットワークで5文字の組み合わせ(filter)を使い発音の特徴を抽出しています。各層ごとにfilterは512種類あり、複雑な組み合わせにも対応できるようになっています。

 ここまでの処理を図にまとめると次のようになります。

f:id:akifukka:20191116130357j:plain

②Bydirectional LSTM(Long Short Term Memory)

 Bydirectional LSTMはクラスEncoderのinferenceの次の文で実行されます。

self.lstm.flatten_parameters()
outputs, _ = self.lstm(x)

 flatten_parametersはRNNのparameter datapointerをリセットして処理を高速化するための処理とのこと。

 self.lstmは__init__で次のように定義されています。赤字は解説用に追記しました。

self.lstm = nn.LSTM(hparams.encoder_embedding_dim = 512,
    int(hparams.encoder_embedding_dim / 2) = 256, 1,
    batch_first=True, bidirectional=True)

 pytoachのライブラリnn.LSTMを使っています。
 入力512次元、隠れ要素数256、層数1、双方向です。LSTMの出力テンソルの次元 = 隠れ要素数ですが、双方向なので×2して512次元のテンソルになります。

 LSTMの処理の概要の図を次に示します。

f:id:akifukka:20191116182920j:plain

 LSTMはニューロンのFF(フリップフロップ)のようなもので、過去の入力状態を保持することができ、それを使って状態分析を行うことができます。入力を保持する(INPUT GATE)、出力する(OUTPUT GATE)、リセット(FORGET GATE)の3つの制御信号をニューラルネットワークで制御して記憶を操作します。

 Tacotron2ではLSTMで離れた位置の文字と文字との関係を分析して発声に反映させることを可能にしているのではないかと思われます。

 なお、双方向LSTMを使っているので、前の文字だけではなく、後にくる文字の影響も汲みあげることができるようになっています。

  LSTMはRNN(Recurrent  Neural Network、再帰ニューラルネットワーク)を長期記憶できるよう改良したものです。自然言語分析、時系列データの分析といった用途で利用される強力な手法です。

次回、Decoderにつづきます。

 

音声合成 カテゴリーの記事一覧へ

ランニングことはじめ2

 この記事は2019年秋現在です。

  今回は、今実際に使っているものを中心にランニングに便利なグッヅを紹介します。ちなみに、毎週土曜は21km、日曜は10kmと、週2回のペースでランニングしています。

f:id:akifukka:20191117134710j:plain


1.シューズ

 何はともあれこれでしょう。前回も書いた通り、シューズは最初から全力投資すべきです。自分の経験から、安物買いは膝を痛めて結局短期間で買い替えになると思います。

 シューズにはいろいろありますが、最初はクッション性重視が全てです。今履いているうちのひとつ、ASICSのGEL NIMBUS20(20は2018年モデル)はその点では十分すぎるくらいです。

 膝を痛めた時に購入して以降、ひざが痛むことは無くなり今では週末に21kmを1時間50分くらいで走ることができます。今では、ASICSのLYTE RACER、NIKEのエピックリアクトフライニット2と他のシューズも使っていますが、いまでもちょっと脚に負担がかかったなと感じた日の次はGEL NIMBUSを使っています。

 GEL NIMBUS20

f:id:akifukka:20191117101127j:plain

 なお、シューズ購入の際は次にご注意を。

  • サイズは普段履きより1cm大き目を推奨します。自分は0.5cm大き目ですが、それでも指先が詰まる感じがします。次回は1cm大きいものにするつもりです。
  • 色々流行りもありますが、最初はクッション性が高く、癖のないものがいいと思います。NIKEの厚底シューズはちょっと癖があって、購入から半年たつけど、いまだにしっくりきません・・・。

 週2回程度のランニングだとシューズは1年くらいは十分持ちます。

2.ランニングポーチ

 スマホ、小銭入れ、ボトル入れにランニングポーチがあると便利です。今使っているのはamazonで買った2代目です。最初は探しまわってドンキホーテで購入したんですが、店ではなかなかいいものが見つからなくて。

AiRun Techランニングポーチ 

  コンパクトで、洗濯もでき、腰に巻く長さはマジックテープで容易に調整できます。マジックテープは十分強力で、かつゴムで伸び縮みするので体にフィットし、重宝しています。ただ次の2つだけ注意が必要です。

  • ボトルの揺れを防止するゴムバンドがボトルの袋のさらに上の方についていたのですが、タイガーのステンレスの魔法瓶(冷たいスポーツドリンク入り)を入れて走ったら15分くらいで切れました。最初見た時から切れそうな気がしてましたが・・・。今は近くのケーヨデーツーで伸縮マジックテープバンドを購入、ゴムバンドの代わりにしてからは全く問題なく使えてます。普通のペットボトル等であれば大丈夫だと思いますが・・・。
  • 洗濯する際はマジックテープ部分はくっついた状態にして洗濯する必要があります。さもないと他の洗濯物にくっつきます。

 この手の商品は入れ替えも早く、いいデザインのものが出ると次々同じような商品が出てきて改良されていくので、上記の欠点もすぐに解消されていくと思います。

 毎回洗濯しているので、少しづつほつれてきました。価格的には半年から1年くらいの消耗品かな。

3.スポーツサングラス

f:id:akifukka:20191117110059j:plain

 10kmとか走れるようになり、時間も1時間から2時間と増えてくると、太陽光の紫外線による目のダメージも無視できなくなってきます。実際夏は朝でも光が眩しい。太陽を見ないようにしても、地面から反射した紫外線が目に入り、気づかないうちにダメージが蓄積していきます。

 日中、ある程度の時間走る人は紫外線をカットするサングラスは必要だと思います。

4.ワイヤレスイヤホン

 音楽聞きながらランニング、また、無料のランニングアプリを使うと時間、距離、ペースを音声で教えてくれます。今使っているのはです。再生時間は最大10時間。最大時間は確かめてませんが7時間くらいは問題ありませんでした。ケースがバッテリー内蔵充電器になっていて、イヤホンをケースに収納すると充電が開始されます。ケース自体を充電するのにUSB-CタイプのACアダプタが別途必要です。

f:id:akifukka:20191117115802j:plain

 購入前は運動時に落ちないかとい不安もありましたが、結局ランニングくらいでは全く問題ありませんでした。音質も上々で、これ以上の音質を求めるなら、こんな手ごろな価格のものではなく高級オーディオ品から探すべきという感じです。

 ランニング時、通勤時にSpotifyを聞いてます。

音楽発見サービス - Spotify

 周囲の音が聞きずらくなるので、そこだけは気を付ける必要があります。

 5.ランニングアプリ

 ランニングアプリとしては以下の2つが有名です。機能的には無料版で十分だと思います。スマホにインストールして使います。GPSと連動して走ったコース、タイム、スプリットタイム、速さを記録してくれます。ランニング中に距離、時間、ペースを音声で教えてくれます。

 ASICS RUN KEEPER

www.asics.com

 NIKE  Run Club

www.nike.com

 RUN KEEPERから使い始め → RUN CLUBを試して→ 今は再びRUN KEEPERを使っています。RUN CLUBは半年使って2、3回、途中からあさってのコースを走ったことになっていました。(往復したのに、まっすぐいったことになっているとか)

 ただしスマホGPSの性能によるものかもしれません。

 RUN KEEPERは今までそこまでおかしな計測にはなったことがありませんが、180度ターンのようなコーナがあると少し短めに計測されている気がします。

 また、RUN KEEPERケイデンス(1分あたりの歩数)も計測してくれます。

6. 吸汗速乾シャツ

 ミズノの長袖トレーニングウェア 32JA6130 を着ています。すぐに乾くので汗で気持ち悪くなることがありません。同じような効果のものが色々あるようなのでお好みで。

7.ランニングソックス

 厚手でクッション性があり、丈夫で長持ちです。お勧め。 

 8.防寒対策

 普段は半袖(中に長袖トレーニングウェア)、ハーフパンツですが冬は防寒対策が必要です。ペースによりますが、走り始めると暑くなります。

(1)スポーツタイツ

 ハーフパンツの下に着用します。素足は寒いです。

(2)ウインドブレーカ

 薄手のジャケットだけでいいと思います。走り始めて暑くなったら脱いで腰に巻きます。

(3)手袋

 コンビニの手袋で・・・。走り始めたら脱いでます。手も汗をかくので容易に洗濯できるものがいいと思います。

9.暑さ対策

 夏は朝でも走るのは困難です。朝7時25~6℃くらいのところで走りはじめますが、普段の距離は走り切れず5~6kmすぎからトボトボ走りになってしまいます・・・。が、走らないと体重増えるし、走れなくなっていくし・・。

 休憩時、走り終えた後に合計1リッターくらいスポーツドリンク等をガブ飲みしますが、それでも走る前と比べて2kgくらい体重が減ります。水分なので、2,3日で戻りますが・・。

 夏は脱水、熱射病で倒れないように十分気を付けて。

(1)帽子

 無いと熱射病で倒れます。あっても死にそうです。ランニングのたびに洗濯機で洗ってます。洗わないと塩吹きます。

(2)日焼け止め

 日焼けに弱いのでこれも必須です。SPF50+を足、顔、手にぬってますが、それでも焼けます。

10.ボトル

 タイガーのステンレスマグボトル600mlを使っています。夏は氷をいれて冷えたスポーツドリンクを飲んでいますが、足りないので走り終わった直後、自販機で追加購入。

 ステンレスのマグボトルの代わりに500mlのペットボトルでも全く問題ありませんが、夏はやっぱり冷たいのがメチャ美味い。

 スポーツドリンクは近所のドラッグストアで購入してます。

11.ビタミンC

 ビタミンCは運動で発生する活性化酸素に対する抗酸化作用があります。走る前に飲んでますが、井藤漢方製薬のC1200は60袋600円とコストパフォーマンス高いです。以前は近所のドラッグストアにもあったのですが、取り扱われなくなってしまいました・・・。 

井藤漢方製薬 ビタミンC 1200 (60袋)

井藤漢方製薬 ビタミンC 1200 (60袋)

 

12.プロテイン

 ランニングは脂肪をエネルギーに変えますが、足りなくなると筋肉もエネルギーに変えてしまいます。ランニング直後にプロテインを飲むと、筋肉の回復を助けてくれるとのこと。確かにランニング直後に飲むと筋肉痛になりずらいような・・・・気がします。

 最初のうちはいらないと思います。ある程度走れるようになると効果があるかも。

13.まとめ

 以上、ランニングに便利なグッヅの紹介でした。ランニングウェアは紹介していませんが、これはお好みで! 

ランニングの帰りにいた鴨のつがい

f:id:akifukka:20191117134427j:plain

では。

ランニング カテゴリーの記事一覧へ