アプリとサービスのすすめ

アプリやIT系のサービスを中心に書いていきます。たまに副業やビジネス関係の情報なども気ままにつづります

PytorchのTransformerでテキストからの音声生成(TextToSpeech)をやってみた【機械学習】

今回はTransformerを改造して、文章から音声を生成してみた。

俗に言う、エンコーダ・デコーダ形式のモデル。
プロセスが長くなりそうなので、要点だけ備忘録もかねてまとめようと思う。


目次
1.Transformer-TextToSpeechとは?
2.テキスト前処理
3.TransformerとPostNetの学習
4.テキストから音声を作成してみる



1.transformer-TextToSpeechとは?

今回作ったtransformerでの音声生成は、Google が発表したTactron2を改造した。

tactron2のEncoderとDecoderをTransformerに置き換えて、waveGlowをpostnetに置き換えたモデル。

Tactron2はそもそもGoogleのこれまでの音声生成プロジェクトで作られた、WaveNetと初代Tacotronのネットワークを組み合わせたもので、詳しくはサイトを見て欲しい。

tacotron2での音声生成処理の流れ
f:id:trafalbad:20200630215005p:plain




今回作ったTransformerの音声生成処理の流れ
f:id:trafalbad:20200630215350j:plain


つまり、

・tacotron2 => Transformer
・waveGlow => PostNet
に置き換えた。

単純なAttentionモデルのseq2seqを使った場合より、4倍くらい速く、精度もbetter。

特にtransformerのattentionの部分が正確な音声の作成にかなり有効っぽい。




2.テキスト前処理

今回データセットは「The LJ Speech Dataset」を使った。

英語での音声を想定したデータセット


前処理では、日本語でもローマ字(英語のスペル)に変換する。
今回は英語対応なので、すべての文字を英語のスペルに変換。


試しにデータセットの一部分の、LJ050-0278のデータをのぞいてみる。

textデータ(csv)

LJ050-0278|the recommendations we have here suggested would greatly advance the security of the office without any impairment of our fundamental liberties.|the recommendations we have here suggested would greatly advance the security of the office without any impairment of our fundamental liberties.


音声データ

LJ050-0278.mag.npy
LJ050-0278.pt.npy
LJ050-0278.wav


まず訓練前にこのテキストデータを前処理した。







3.TransformerとPostNetの学習

まず、Transformerを学習させてから、最後にPostNetを学習して、音声合成する。

Transformerはスペクトログラム(人間の音高知覚に調整した特徴量:メル周波数)を出力。

PostNetは音声の波形データを出力する。

Pytorchなのでmodelをprint()してみた

Transformer

model = Transformer_model()
print(model)

>>>>>
Model(
(encoder): Encoder(
(pos_emb): Embedding(1024, 256)
(pos_dropout): Dropout(p=0.1, inplace=False)
(encoder_prenet): EncoderPrenet(
  (embed): Embedding(149, 512, padding_idx=0)
  (conv1): Conv(
    (conv): Conv1d(512, 256, kernel_size=(5,), stride=(1,), padding=(2,))

 ~略~

  (1): Attention(
    (key): Linear(
      (linear_layer): Linear(in_features=256, out_features=256, bias=False)
    )
    (value): Linear(
      (linear_layer): Linear(in_features=256, out_features=256, bias=False)
    )
    (query): Linear(
      (linear_layer): Linear(in_features=256, out_features=256, bias=False)

 ~略~

(decoder): MelDecoder(
(pos_emb): Embedding(1024, 256)
(pos_dropout): Dropout(p=0.1, inplace=False)
(decoder_prenet): Prenet(
  (layer): Sequential(
    (fc1): Linear(
      (linear_layer): Linear(in_features=80, out_features=512, bias=True)
    )

 ~略~

  (pre_batchnorm): BatchNorm1d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (dropout1): Dropout(p=0.1, inplace=False)
  (dropout_list): ModuleList(
    (0): Dropout(p=0.1, inplace=False)
    (1): Dropout(p=0.1, inplace=False)
    (2): Dropout(p=0.1, inplace=False)
  )
)
)
)

PostNet

model = PostNet_model() 
print(model)
>>>>>>

ModelPostNet(
(pre_projection): Conv(
(conv): Conv1d(80, 256, kernel_size=(1,), stride=(1,))
)
(cbhg): CBHG(
(convbank_list): ModuleList(
  (0): Conv1d(256, 256, kernel_size=(1,), stride=(1,))
  (1): Conv1d(256, 256, kernel_size=(2,), stride=(1,), padding=(1,))
  (2): Conv1d(256, 256, kernel_size=(3,), stride=(1,), padding=(1,))
  (3): Conv1d(256, 256, kernel_size=(4,), stride=(1,), padding=(2,))
  (4): Conv1d(256, 256, kernel_size=(5,), stride=(1,), padding=(2,))
  (5): Conv1d(256, 256, kernel_size=(6,), stride=(1,), padding=(3,))
  (6): Conv1d(256, 256, kernel_size=(7,), stride=(1,), padding=(3,))
  (7): Conv1d(256, 256, kernel_size=(8,), stride=(1,), padding=(4,))

  ~略~

  (linears): ModuleList(
    (0): Linear(
      (linear_layer): Linear(in_features=256, out_features=256, bias=True)
    )
    (1): Linear(
      (linear_layer): Linear(in_features=256, out_features=256, bias=True)
    )
    (2): Linear(
      (linear_layer): Linear(in_features=256, out_features=256, bias=True)
    )
    (3): Linear(
      (linear_layer): Linear(in_features=256, out_features=256, bias=True)
    )
  )
)
(gru): GRU(256, 128, num_layers=2, batch_first=True, bidirectional=True)
)
(post_projection): Conv(
(conv): Conv1d(256, 1025, kernel_size=(1,), stride=(1,))
)
)

コードだと分かりにくいのでモデルの全体像。
f:id:trafalbad:20200630215037p:plain



学習はfine-tuneで行った。
学習率のlearning rateは0.001がベストプラクティス。
あと学習が進むにつれてlearning rateも下がる調整もかなり重要っぽい

def adjust_learning_rate(optimizer, step_num, warmup_step=4000):
    lr = hp.lr * warmup_step**0.5 * min(step_num * warmup_step**-1.5, step_num**-0.5)
    for param_group in optimizer.param_groups:
        param_group['lr'] = lr


loss曲線はtctron2からの引用だけど、同等もさくはそれ以上に、かなりいい具合に減少。
f:id:trafalbad:20200630215049p:plain




でかいデータセットでやるとメモリエラーも起こるし、時間もとんでもなくかかるからfine-tuneがおすすめ。

最近はもうfine-tuneの方がかなり効率いいので、end-to-endの学習は余程のことがないとしないんじゃないかな?





4.テキストから音声を作成してみる

映画ホームアローンのハリーのセリフを作成してみる。

f:id:trafalbad:20200630215151j:plain



text1 = "I never made it to sixth grade"
text2 = "it dose not look like you are gonna"

周波数(fs)は低くするとドスのきいた声になり、高いと早口言葉みたいになる。

max_lenはtextの長さに比例するのでそれぞれちょうどいい具合に調整した。

def calculate_melsp(x, n_fft=1024, hop_length=128, n_mels=128):
    stft = np.abs(librosa.stft(x, n_fft=n_fft, hop_length=hop_length))**2
    log_stft = librosa.power_to_db(stft)
    melsp = librosa.feature.melspectrogram(S=log_stft, n_mels=n_mels)
    return melsp

# display wave in plots
def show_wave(x):
    plt.plot(x)
    plt.show()
    
    
# display wave in heatmap
def show_melsp(melsp, fs):
    librosa.display.specshow(melsp, sr=fs)
    plt.colorbar()
    plt.show()

text1 = "I never made it to sixth grade, kid."

max_len = 500
fs = 25000

text1 = "I never made it to sixth grade, kid."
wav = create_audio_wave(text1, max_len)

print(wav.shape)  # (137225,)
show_wave(wav)

melsp = calculate_melsp(wav, n_fft=fs, hop_length=max_len, n_mels=max_len)
print(melsp.shape) # (500, 275)

show_melsp(melsp, fs)

# 実際にjupyter上で音声が聞ける
ipd.Audio(wav, rate=fs)

この投稿をInstagramで見る

開発用 "I never made it to sixth grade"

Tatsuya Hagiwara(@gosei_creater)がシェアした投稿 -





text2 = "it dose not look like you are gonna"

text2 = "it dose not look like you are gonna"
wav = create_audio_wave(text2, max_len)

ipd.Audio(wav, rate=fs)

この投稿をInstagramで見る

開発用2 "it dose not look like you are gonna"

Tatsuya Hagiwara(@gosei_creater)がシェアした投稿 -




AttentionはNLPだけじゃなく、いろんな精度向上に役立つっぽい。

ガチの音声生成をしたのははじめてだった。分類系より、生成系は面白い。



参考サイト



GitHub - soobinseo/Transformer-TTS: A Pytorch Implementation of "Neural Speech Synthesis with Transformer Network"

強化学習で「手押し井戸ポンプ」で水をくむ動作をArduino Unoに学習させる【AI・Hardware】

「手押し井戸ポンプ」は、昔から田舎で使われてる「井戸から手動で水をくむポンプ」のこと(Amazonでも売ってる)。

「手押し井戸ポンプ」の水をくむまでの大まかな動作は

押す(down)=>水をくむ(push pump)=>あげる(up)

の行動パターンを、シーソーのごとく繰り返すことで、井戸から水をくめる。

手押し井戸ポンプの動作原理f:id:trafalbad:20200620111544g:plain



今回はArduinoを使って、実際に近い状況を再現(シミュレーション)して、強化学習で水をくむ動作を学習させた。

深層学習の強化学習アルゴリズムはCartPoleでお馴染みの「DQN」を使う。特にkeras-rlとか強化学習ライブラリを使わずに、普通にDQNの学習コード書いた。

その要点をまとめて行こうと思う。

目次
1.Arduinoで「手押し井戸ポンプ」の状況の再現
2.Arduinoの構成
3.DQNで学習
4.学習結果
5.最後に




1.Arduinoで「手押し井戸ポンプ」の状況の再現


まず、Arduinoで実際の「手押し井戸ポンプ」の状況を実際に再現してシミュレーションした。

はじめにwebカメラでデフォルトの画像を読み込ませる。

それをニューラルネットワーク(NN)に読み込ませて、DQNの学習システムに沿って水をくむ行動パターンを学習させていく。

1.webカメラで画像を読み込み、NNに入れる

2.PC側でNNから行動を出力して、Arduinoに送る

3.Arduinoで行動を読み取り動作させる。ポンプで水をくめたら、PCに「1」の信号送る

4.Arduinoから「1」を受け取りPC側で報酬(Reward)として受け取る


f:id:trafalbad:20200620111631j:plain

#include <Servo.h>

Servo mServo;

int state[2];
int pin_number = 10;

void setup() {
  mServo.attach(9);
  mServo.write(10);
  delay(500);
  Serial.begin(9600);
  pinMode(pin_number, OUTPUT);
  state[0] = 0;
  state[1] = 1;
  digitalWrite(pin_number, LOW);
}

void loop() {
  if (Serial.available() > 0) {
    char c = Serial.read();
    if (c == 'p') {
      if (state[0] == 0) {
        Serial.print("0");
        mServo.write(90);
        delay(500);
        state[0] = 1;
      }
      else {
        Serial.print("0");
        mServo.write(10);
        delay(500);
        state[0] = 0;
        state[1] = 1;
        digitalWrite(10, LOW);
      }
    }
    else if (c == 'i') {
      if (state[0] == 1 && state[1] == 1) {
        Serial.print("1");
        delay(500);
        digitalWrite(10, HIGH);
        state[1] = 0;
      }
      else {
        Serial.print("0");
      }
    }
    else if (c == 'c') {
      state[0] = 0;
      state[1] = 1;
      digitalWrite(10, LOW);
      mServo.write(10);
      delay(500);
    }
  }
}



Arduinoとリアルの「手押し井戸ポンプ」の行動の対応関係


「手押し井戸ポンプ」から水をくむのに必要な行動パターンは3つあって、順番に行動する(行動パターンを覚える)必要がある。



水をくむまで行動パターン

Action1(a1) : 押す(down)
Action2(a2) : 水をくむ(Push pump)
Action(a3) : あげる(up)



リアルの「手押し井戸ポンプ」の行動パターンf:id:trafalbad:20200620112256j:plain



Arduinoのシミュレーションの行動パターンf:id:trafalbad:20200620111714j:plain







2.Arduinoの構成

Arduino(Uno)で用意したのは付属パーツは

サーボモータ

・USBカメラ(Mac用)

・抵抗

・LED



Arduino回路図f:id:trafalbad:20200620111744j:plain




実際にUSBカメラで読み込むArduinoの映像

この投稿をInstagramで見る

#arduino for development

Tatsuya Hagiwara(@gosei_creater)がシェアした投稿 -








3.DQNで学習

アルゴリズムはDeep-QNetwork(DQN)。
DQNはモデルフリーなので報酬とか戦略の設定が大事だと思う。


このDQNのメカニズムを使ってNNに「手押し井戸ポンプ」から水を汲む行動パターンを覚えさせる。



DQNでの学習サイクル(Episode cycle)
f:id:trafalbad:20200620111911j:plain








Model



DQNではlossにHuber lossを使うのが一般的らしい。「Squeeze-and-Excitation Networks」を使って少し凝ったCNNを作った。

ef QFunction(inputs_):
    o = Conv2D(32, (3, 3), padding='same', kernel_initializer='random_uniform')(inputs_)
    o = channel_spatial_squeeze_excite(o)
    o = MaxPooling2D((2, 2))(o)
    o = Conv2D(64, (3, 3), padding='same', kernel_initializer='random_uniform')(o)
    o = channel_spatial_squeeze_excite(o)
    o = MaxPooling2D((2, 2))(o)
    o = Conv2D(64, (3, 3), padding='same', kernel_initializer='random_uniform')(o)
    o = channel_spatial_squeeze_excite(o)
    o = Flatten()(o)
    o = Dense(2, activation='linear')(o)
    model = Model(inputs=inputs_, outputs=o)
    model.compile(optimizer=Adam(lr=0.001), loss=huber_loss_mean, metrics=['acc'])
    return model

USBカメラから画像を読み込む部分。

cap = cv2.VideoCapture(0)

def capture(ndim=3):
    ret, frame = cap.read()
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    xp = int(frame.shape[1]/2)
    yp = int(frame.shape[0]/2)
    d = 400
    resize = 128
    cv2.rectangle(gray, (xp-d, yp-d), (xp+d, yp+d), color=0, thickness=10)
    cv2.imshow('gray', gray)
    gray = cv2.resize(gray[yp-d:yp + d, xp-d:xp + d],(resize, resize))
    env = np.asarray(gray, dtype=np.float32)
    if ndim == 3:
        return env[np.newaxis, :, :] 
    else:
        return env[np.newaxis, np.newaxis, :, :] 

camera_image = capture(ndim=3)
cap.release()



Environment


1回のepisodeでStepを30回回して、水汲みに8回成功したらsuccessに+1する。それを全episode中に5回(success=5)繰り返せたら学習終了。

NUM_EPISODES = 50 # エピソード数
GAMMA = 1.0 

# 探索パラメータ
E_START = 1.0 # εの初期値
E_STOP = 0.01 # εの最終値
E_DECAY_RATE = 0.001 # εの減衰率

SUCCESS_REWARD = 8

# メモリパラメータ
MEMORY_SIZE = 10000 # 経験メモリのサイズ
MAX_STEPS = 30 # 最大ステップ数
BATCH_SIZE = MAX_STEPS

H = 128
W = 128
C = 1
inputs = Input([H, W, C])

class Environment:
    def __init__(self, inputs):
        # main-networkの作成
        self.main_qn = QFunction(inputs)

        # target-networkの作成
        self.target_qn = QFunction(inputs)
        # 経験メモリの作成
        self.memory = Memory(MEMORY_SIZE)

 # 行動価値関数
    def action_value_function(self, action_b, step_reward_b):
        target = 0
        if action_b == 1:
            target = GAMMA + step_reward_b/MAX_STEPS
        else:
            target = GAMMA + step_reward_b/MAX_STEPS
        return target

    def default(self):
       # Memoryの初期化
        self.memory = Memory(MEMORY_SIZE)

    def run(self):
        # エピソード数分のエピソードを繰り返す
        total_step = 0 # 総ステップ数
        success_count = 0 # 成功数
        for episode in range(1, NUM_EPISODES+1):
            step = 0 # ステップ数
            R = 0
            batch_size = 0
            ser.write(b"c") # Arduino側を初期状態に戻す

            # target-networkの更新
            self.target_qn.set_weights(self.main_qn.get_weights())
            
            # 1エピソードのループ
            for _ in range(1, MAX_STEPS+1):
                step += 1
                total_step += 1
                camera_state = capture(ndim=3)
                camera_state = camera_state.reshape(1, H, W, C)
                # εを減らす
                epsilon = E_STOP + (E_START - E_STOP)*np.exp(-E_DECAY_RATE*total_step)
          
                # ランダムな行動を選択
                if epsilon > np.random.rand():
                    action = int(np.random.randint(0, 2, 1))
                # 行動価値関数で行動を選択
                else:
                    action = np.argmax(self.main_qn.predict(camera_state))
                    pred = self.main_qn.predict(camera_state)
   
                # 行動に応じて状態と報酬を得る
                reward = action_step(action)
                R += reward
                self.memory.add((camera_state, action, reward, R)) 
                print('step', step, 'action', action, "R", R, 'reward', reward)
            if R >=SUCCESS_REWARD:
                success_count += 1
                
            # ニューラルネットワークの入力と出力の準備
            inputs = np.zeros((BATCH_SIZE, H, W, C)) # 入力(状態)
            targets = np.zeros((BATCH_SIZE, 2)) # 出力(行動ごとの価値)
            # バッチサイズ分の経験を取得
            minibatch = self.memory.sample(BATCH_SIZE)
            
            # ニューラルネットワークの入力と出力の生成
            for i, (state_b, action_b, reward_b, step_reward_b) in enumerate(minibatch):
                
                # 入力に状態を指定
                inputs[i] = state_b
                
                # 採った行動の価値を計算
                target = self.action_value_function(action_b, step_reward_b)
                # 出力に行動ごとの価値を指定
                targets[i] = self.main_qn.predict(state_b)
                targets[i][action_b] = target # 行動の価値

            # 行動価値関数の更新
            print('training....')
            self.main_qn.fit(inputs, targets, epochs=30, verbose=0)
            
            # エピソード完了時のログ表示
            print('エピソード: {}, ステップ数: {}, epsilon: {:.4f}'.format(episode, step, epsilon))
            self.default()
            # 5回成功で学習終了
            if success_count >= 5:
                break

WEbカメラから画像をNNに入れる => 行動価値関数を出力 => 行動に変換 => 報酬get


の簡単な流れを図にしてみた。
f:id:trafalbad:20200620111700j:plain




4.学習結果


epochの1~50までのパターン行動の成功回数の推移
f:id:trafalbad:20200620112004p:plain

順調にパターンを学習して、水をくむ回数が増えていってるのがわかる。


行動パターンを覚えさせるために、行動価値関数を次のように設計した。

# 行動価値関数
 def action_value_function(action_b, step_reward_b):
        target = 0
        if action_b == 1:
            target = GAMMA + step_reward_b/MAX_STEPS
        else:
            target = GAMMA + step_reward_b/MAX_STEPS
        return target

# 行動を返す関数
def action_step(actions):
    r = 0
    if actions==0:
        ser.write(b"p")
    else:
        ser.write(b"i")
    time.sleep(1.0)
    r = ser.read() 
    return int(r)

この設定だとepisodeが進むにつれて、それぞれの行動(0と1)の行動価値が最後は均衡してくる。

NNからoutputされる行動価値の推移f:id:trafalbad:20200620112018j:plain

もっと上手い設定方法はあるけど、とりあえずうまく学習できてたので自分としては上出来。



5.最後に

50回目までにStep1回で水汲み16回以上を達成できるようになって、無事クリア。DQN

・報酬の設定

・行動価値をどう決めるか 

・戦略

・経験の学習



とかが一番学習結果に影響した。逆にネットワークは別にそんな複雑なものである必要はなかった。

現に今回は白黒画像でやってもこれだけの成果が出たし。強化学習は戦略とか「どう学ばせるか」が肝だと思う。


ハードウェアで強化学習ができるのはいろいろと厨二病的な愉悦。

f:id:trafalbad:20200620112502j:plain

webカメラ+Arduino Uno+CNN画像分類(AI)でカギの開錠・施錠システムを作ってみた【hardware,機械学習】

今回はArduino機械学習(AI)を埋め込んで、webカメラから読み込んだストリーミング画像の画像分類結果からカギを解錠・施錠できるシステムを作ってみた。

機械学習(AI)をハードウェアで実際に取り込んで作ったのはVitis AI以来、今回がはじめて。

Arduino系の記事はたくさんあるけど、機械学習を使った例はあんまなかったので、今回はその作成過程をまとめてく。


目次
1.CNN+Arduinoの鍵の開錠・施錠システムの構造
2.データセットの作成・CNNで学習
3.CNN+Arduinoで金庫の鍵の開錠・施錠


1.CNN+Arduinoの鍵の開錠・施錠システムの構造

用意したもの

Arduino Uno
・ジャックDIP化キット
サーボモータ
・ACアダプター(5V)
・USBカメラ
・スイッチ


Arduinoの回路図の概略f:id:trafalbad:20200601112800j:plain



作業手順

1.webカメラで学習用画像集めてデータセットを作成する

2.CNNで学習

3.key.txtに開錠番号を記載

4.Webカメラで取り込んだストリーミング画像を推論
=> 推論結果とkey.txtと同じ番号ならArduinoでカギの解錠

今回工夫したのは、
Webカメラからデータセットを作れるようにした

Webカメラのストリーミング画像を推論してkey.txtの番号と一致したら解錠する仕組み

の2点。


実物f:id:trafalbad:20200601113319j:plain



2.データセットの作成・CNNで学習

学習から推論までは下のフローで進めてく。

Webカメラからデータセットを作成=>訓練=>Webカメラのストリーミング画像を推論

解錠システムのデータセット作成から推論までの流れは図にするとこんな感じ
f:id:trafalbad:20200601112958j:plain





Webカメラから学習データ収集


今回はWebカメラから直接データセットを作れるようにした。

訓練画像にしたい部分をwebカメラバウンディングボックス内に写す。
そのとき、PCキーボードの番号(0~5)を押せばその番号のimgファルダに画像が保存されるので、大量の画像を短時間で作れる。

今回は訓練画像は6種類。何も写ってない画像を0として含めた。

訓練画像1~5f:id:trafalbad:20200601113108j:plain

Webカメラで写しながら、キーボードの数字を押せばimgファルダの該当番号のファルダに保存されてく仕組み。

import cv2

n0 = 0
n1 = 0
n2 = 0
n3 = 0
n4 = 0
n5 = 0
cap = cv2.VideoCapture(0)
while True:
    ret, frame = cap.read()
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    xp = int(frame.shape[1]/2)
    yp = int(frame.shape[0]/2)
    d = 400 # bbox size
    resize = 256
    cv2.rectangle(gray, (xp-d, yp-d), (xp+d, yp+d), color=0, thickness=10)
    cv2.imshow('gray', gray)
    gray = cv2.resize(gray[yp-d:yp + d, xp-d:xp + d],(resize, resize))
    c =cv2.waitKey(10) 
    if c == 48:#0
        cv2.imwrite('img/0/{0}.png'.format(n0), gray)
        n0 = n0 + 1
    elif c == 49:#1
        cv2.imwrite('img/1/{0}.png'.format(n1), gray)
        n1 = n1 + 1
    elif c == 50:#2
        cv2.imwrite('img/2/{0}.png'.format(n2), gray)
        n2 = n2 + 1
    elif c == 51:#3
        cv2.imwrite('img/3/{0}.png'.format(n3), gray)
        n3 = n3 + 1
    elif c == 52:#4
        cv2.imwrite('img/4/{0}.png'.format(n4), gray)
        n4 = n4 + 1
    elif c == 53:#5
        cv2.imwrite('img/5/{0}.png'.format(n5), gray)
        n5 = n5 + 1
    elif c == 27:#Esc
        break
cap.release()
$ tree img
img
├── 0
│   └── 0.png
├── 1
│   └── 1.png
├── 2
│   └── 2.png
├── 3
│   └── 3.png
├── 4
│   └── 4.png
└── 5
  └── 5.png




CNNで学習



MNIST並みに単純な画像なので、普通のCNNで訓練した。
Google colab上でGPUで訓練してから、学習済みモデルをローカルに落として使うことで、簡単に再訓練ができる構造。

def create_model(inputs_):
  o = Conv2D(32, (3, 3), padding='same', kernel_initializer='random_uniform')(inputs_)
  o = channel_spatial_squeeze_excite(o)
  o = MaxPooling2D((2, 2))(o)
  o = Conv2D(64, (3, 3), padding='same', kernel_initializer='random_uniform')(o)
  o = channel_spatial_squeeze_excite(o)
  o = MaxPooling2D((2, 2))(o)
  o = Conv2D(64, (3, 3), padding='same', kernel_initializer='random_uniform')(o)
  o = channel_spatial_squeeze_excite(o)
  #x = Dense(64, activation='relu')(x)
  o = Flatten()(o)
  o = Dense(6, activation='softmax')(o)
  model = Model(inputs=inputs_, outputs=o)
  model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['acc'])
  return model


def make_train_dataset(img_dir):
    x = []
    y =[]
    label = 0
    for c in os.listdir(img_dir):
        print('class: {}, class id: {}'.format(c, label))
        d = os.path.join(img_dir, c) 
        try:       
            imgs = os.listdir(d)
        except:
            continue
        for i in [f for f in imgs if ('png' in f)]:
            x.append(cv2.imread(os.path.join(d, i), 0))
            y.append(label)            
        label += 1
    return np.array(x)/255, to_categorical(y)


X, Y = make_train_dataset('img')
H = X.shape[1]
W = X.shape[2]
X = np.reshape(X, (len(X), H, W, 1))
model = create_model(inputs)
model.summary()

try:
    model.fit(X, Y, batch_size=batch_size, epochs=num_ep, callbacks=callback,
                          validation_data=(valid_X, valid_y), shuffle=True)
finally:
    model.save('CNN_model.h5')




3.CNN+Arduinoで金庫の鍵の開錠・施錠

Webカメラで深層学習の画像分類できるかテスト



バウンディングボックスに写った画像を推論。

推論結果がkey.txtの番号と一致していたら開錠の指令をシリアル通信でArduinoに送る仕組み。
f:id:trafalbad:20200601112840j:plain




Webカメラで写したPC画面はこんな感んじ。
f:id:trafalbad:20200601113042p:plain

Security.py

import numpy as np
import os
import cv2
import keras
from keras.models import Model
from keras.layers import *
from train_modules.scse import channel_spatial_squeeze_excite
from CNN_model import create_model
import serial
import time

H = 256
W = 256
inputs = Input((H, W, 1))
model = create_model(inputs)
model.load_weights('CNN_model.h5')
key_file = 'key.txt'
cap = cv2.VideoCapture(0)

with serial.Serial('/dev/cu.usbmodem14301', timeout=0.1) as ser:

    while True:
        ret, frame = cap.read()            
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        xp = int(frame.shape[1]/2)
        yp = int(frame.shape[0]/2)
        d = 400 # bbox size
        resize = 256
        cv2.rectangle(gray, (xp-d, yp-d), (xp+d, yp+d), color=0, thickness=10)
        cv2.imshow('gray', gray)
        if cv2.waitKey(10) == 27:
            break
        a = ser.read()
        print(a)
        if a == b'1':
            print('pass')
            gray = cv2.resize(gray[yp-d:yp + d, xp-d:xp + d],(resize, resize))
            img = np.asarray(gray,dtype=np.float32)  
            img = np.reshape(img, (1, 256, 256, 1))
            img = img/255
            y_pred = model.predict(img)
            c = int(np.argmax(y_pred, axis=1))
            print(c)
            with open('key.txt', 'r') as f:
                b = int(f.read())
            if b==0:
                if c!=0:
                    time.sleep(5.0) # wait 5 second
                    ser.write(b"o")
                    with open(key_file, 'w') as f:
                        f.write(str(c))
                    print('close')
            else:
                if b==c:
                    time.sleep(5.0) # wait 5 second
                    ser.write(b"c")
                    with open(key_file, 'w') as f:
                        f.write('0')
                    print('open')
cap.release()




Aruduinoで画像分類して、サーボモータで鍵を開錠・施錠


シリアル通信で開錠の指令がきたらサーボモータを180度回転させて解錠。

番号が間違ってたら施錠したまま。

Security.ino

#include <Servo.h>

Servo myservo; // Servoオブジェクト宣言

void setup() {
  Serial.begin(9600);
  myservo.attach(9); //9番ピンでサーボモータを動かす
  pinMode(2, INPUT_PULLUP); // Inputモードでプルアップ抵抗を有効
  pinMode(LED_BUILTIN, OUTPUT);
  myservo.write(120); // angle
  digitalWrite(LED_BUILTIN, HIGH);
}
 
void loop(){
  static int flag=0;
  if(digitalRead(2)==LOW){
    flag=1;
  }
  else{
    if(flag==1){
      flag=0;
      Serial.write('1');
      delay(500);
    }
  }
  if(Serial.available()>0){
    char a = Serial.read();
    if(a=='o'){
      myservo.write(120); // angle
      digitalWrite(LED_BUILTIN, HIGH);
    }
    else if(a=='c'){
      myservo.write(20);  // angle
      digitalWrite(LED_BUILTIN, LOW);
    }
  }
}

PythonコードとArduinoコードが用意できたら、ラストはPC側でPythonコードを実行。

Arduino回路のスイッチを押せば推論が行われるので、推論結果がkey.txtと合ってれば解錠。

# PC側で実行
$ python3 Security.py
>>>
Using TensorFlow backend.
StreamExecutor device (0): Host, Default Version
b''
b''
b’1' # 推論実行
pass
5
close # 不一致なので施錠

b''
b''
b’1' # 推論実行
pass
3
open # key.txtの番号と推論結果が一致したので解錠
b''


下の画像は解錠・施錠を交互に行った動画。

この投稿をInstagramで見る

保存用動画

Tatsuya Hagiwara(@gosei_creater)がシェアした投稿 -

金庫がないのでわかりにくいけど、USBカメラに画像を写せば「ピピっ」と漫画みたいに反応した後、キーの解錠・施錠ができる。

セキュリティのために金庫の鍵の解錠施錠とかいろんな用途に使えそう。

f:id:trafalbad:20200601113617j:plain



Vitis AIでチュートリアルをやった以来、はじめてハードウェアで機械学習(AI)を組み混んでモノを作ってみた。
Arduinoは速度は置いといて、仕組みさえわかれば、かなり簡単に機械学習システムを組み込めた。


参考サイト


Arduinoを用いてサーボモータを制御する

Arduino UnoとPCをシリアル通信させる方法と参考コード集まとめ【hardware】

ArduinoとPCとの情報のやりとりはシリアル通信でする。このサイトを参考にした。

深層学習(DNN)にはシリアル通信は必須なので、やり方とデモもかねて動かしたコードをまとめていく。

f:id:trafalbad:20200526161410j:plain

目次
1.Arduino Unoのシリアル通信の設定
2.Arduino UnoからPCへのデータ送信
3.PCからArduino Unoへのデータ送信



1.Arduinoのシリアル通信の設定

Arduino for Macを起動して、[ツール]-[シリアルボード~]で表示されてるパスを取得して確認。
f:id:trafalbad:20200526161555p:plain

# シリアル通信できるか確認
$ sudo pip install pyserial
$ python3
>>>import serial
>>>ser = serial.Serial("/dev/cu.usbmodem14301")
>>>print(ser)
Serial<id=0x106a8f710, open=True>(port='/dev/cu.usbmodem14301', baudrate=9600, bytesize=8, parity='N', stopbits=1, timeout=None, xonxoff=False, rtscts=False, dsrdtr=False)

>>>ser.close()
>>>exit()

シリアル通信ができてる。





2.Arduino UnoからPCへのデータ送信

f:id:trafalbad:20200526161623j:plain

0~9までの数字を一秒おきに送信


Arduinoから1秒毎にカウントされる値がPCに送られる。
通信中はArduinoのTXと書かれた通信用LEDが一秒毎に点滅する。

serial_send.ino

void setup() {
 int signal_speed = 9600;
 Serial.begin(signal_speed);
}

void loop() {
 static int count=0;
 Serial.print(count);
 count ++;
 if(count == 10)
   count = 0;
 delay(1000);
}


serial_receive1.py

# -*- coding: utf-8 -*-
import serial
import time
wait_second = 5.0
total_number = 10
ser = serial.Serial("/dev/cu.usbmodem14301", timeout=0.5)
time.sleep(wait_second)
for i in range(total_number):
   line = ser.read()
   print(line)
ser.close()
# 実行
$ python3 serial_receive1.py
>>>
b'0'
b'1'
b'2'
~
b'5'

f:id:trafalbad:20200526161707g:plain



1~10までを読みこんで表示


ser.read()関数で一文字ごとに読み込んで表示。

serial_receive2.py

# -*- coding: utf-8 -*-
import serial
import time

wait_second = 5.0
total_number = 10 
with serial.Serial("/dev/cu.usbmodem14301") as ser:
   time.sleep(wait_second)
   for i in range(total_number):
       line = ser.read()
       print(line)
# 実行
$ python3 serial_receive2.py
>>>
b'0'
b'1'
b'2'
~
b'8'
b'9'
# ファルダ構造
$ tree Serial_receive
>>>
Serial_receive
├── serial_send
│   └── serial_send.ino
├── serial_receive1.py
└── serial_receive2.py




データロガー



データロガーは「Arduinoで計測したデータをPCに送って、PCでそのデータを保存する」こと。

試しに一秒おきに

0, 0, 0.00
1, 1, 2.00

と0, 1, 2, 3と増えるデータをArduinoからPCに送信して、テキストファイル(data.txt)に保存してみる。

serial_datalogger.ino

void setup() {
 int signal_speed = 9600;
 Serial.begin(signal_speed);
}

void loop() {
 static int count=0;
 Serial.print(count);
 Serial.print(',');
 Serial.print(count);
 Serial.print(',');
 Serial.println(count*2.0);
 count ++;
 if(count == 10)
   count = 0;
 delay(1000);
}


serial_datalogger.py

# -*- coding: utf-8 -*-
import serial
import time
wait_second = 12.0

with serial.Serial('/dev/cu.usbmodem14301', timeout=0.5) as ser:
   time.sleep(wait_second)
   with open('data.txt', 'w') as f:
       for i in range(10):
           line = ser.readline()
           line = line.rstrip().decode('utf-8')
           print(line)
           f.write((line)+'\n')
# 実行
$ python3 serial_datalogger.py
>>>
0,0,0.00
1,1,2.00
2,2,4.008,8,16.00
9,9,18.00


データがうまく保存できないならtime.sleep()の時間を増やすのがコツ

# ファルダ構造
$ tree Serial_datalogger

Serial_datalogger
├── data.txt
├── serial_datalogger
│   └── serial_datalogger.ino
└── serial_datalogger.py




3.PCからArduino Unoへのデータ送信

今度は逆にPCからArduinoへ文字や数値を送信する。
これを使えばDNNで得た学習モデルを使って、Arduinoを含めたラズパイとかの電子工作を動かせる。

f:id:trafalbad:20200526161758j:plain

バイト文字の送信



void setup() {
 int signal_speed = 9600;
 Serial.begin(signal_speed);
 pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
 if(Serial.available()>0){
   char c = Serial.read();
   if(c=='a')
         digitalWrite(LED_BUILTIN,HIGH);
   else if(c=='b')
         digitalWrite(LED_BUILTIN,LOW);
 }
}
# -*- coding: utf-8 -*-
import serial
import time

with serial.Serial('/dev/cu.usbmodem14301') as ser:
   time.sleep(5.0)
   for i in range(5):
       ser.write(b'a')
       time.sleep(1.0)
       ser.write(b'b')
       time.sleep(1.0)


受信した変数が’a’なら確認用LEDを点灯、’b’なら確認用LED消灯。

‘a’を送信し、1秒待ち、’b’を送信して1秒待つを5回繰り返す。

Arduinoの確認用LEDが5回点滅する。
f:id:trafalbad:20200526161831g:plain





数値の送信



0~255の数値を送ってLEDの明るさを変化させる(もっと大きい数値(500とか1500)も送信可能)。

デジタル9番ピンを使ってLEDの明るさを変える。
serial_receive_value.ino

int pin_number = 9;

void setup() {
 int signal_speed = 9600;
 Serial.begin(signal_speed);
 pinMode(pin_number, OUTPUT);
}

void loop() {
 if(Serial.available()>0){
   long int v = Serial.parseInt();
   analogWrite(pin_number, v);
 }
}


Serial.available()関数は浮動小数点値を受け取る。

LEDがだんだん明るくなる=>消える

を5回繰り返す(0~255までの値を送る)。
serial_receive_value.py

# -*- coding: utf-8 -*-
import serial
import time

wait_second = 0.01
with serial.Serial('/dev/cu.usbmodem14301') as ser:
   time.sleep(5.0)
   for i in range(5):
       for j in range(255):
           ser.write((str(j)+'\n').encode('utf-8'))
           time.sleep(wait_second)  # 0.01秒待つ
# 実行
$ python3 serial_send_value.py

f:id:trafalbad:20200526161855g:plain

$ tree serial_receive_value
>>>
serial_receive_value
├── serial_receive_value.ino
└── serial_receive_value.py

これでArduinoを使って深層学習ができる。


速度とかとはultra96v2と比較して劣るものの、Arduino Unoは動かすのはかなり簡単だった。


f:id:trafalbad:20200526163409j:plain

Arduino Unoを動かす基本コードと環境設定方法【hardware】

ラズパイの一種、「Arduino UNO」の基本的な動作をさせる際のコード集(個人的メモ)。

Arduinoを動作させるまでの環境設定
MacにUSBを使ってUbuntuをインストールする方法

も簡単に書いた。


目次
1.LEDを点滅させる
2.LEDをだんだん明るくさせる
3.Arduino UNO環境設定
4.MacにUSBカードでubuntuをinstallする方法

1.LEDを点滅させる

LEDをarduinoに挿して、点滅させる。

f:id:trafalbad:20200525214049j:plain

ブレッドボードに挿して点滅させる。
f:id:trafalbad:20200525214641j:plain

int led = 13;

void setup() {//一度だけ実行される
  pinMode(led, OUTPUT);//Arduinoボードに付いているLEDを出力に
}

void loop() {//何度も繰り返し実行される
  digitalWrite(led, HIGH);//LEDを光らせる
  delay(1000);//1000ミリ秒待つ
  digitalWrite(led, LOW);//LEDを消す
  delay(1000);//1000ミリ秒待つ
}


2.LEDをだんだん明るくさせる

f:id:trafalbad:20200525220904g:plain

使ったもの

・抵抗(1/2W1kΩ)
・LED
・ブレッドボード
・ジャンパーワイヤー(オスーオス)

int led = 9;

void setup() {
  pinMode(led, OUTPUT);//デジタル9番ピンを出力に
}

void loop() {
  for(int i=0;i<256;i++){
    analogWrite(led, i);//iの値に従って明るさを設定
    delay(10);
  }
}


3.Arduino UNO環境設定

Arduinoのwebページから[Arduino for MAC]のMac版をここからインストールする。

ほぼ「Arduino IDEインストール方法 – Mac編」を参考にした。



ボードマネージャを使ってボード制御プログラムを追加


[Arduino]メニューの[Preferences]を選択。"ESP8266" のArduinoライブラリのgitサイトを開いて、
下の方にある

Boards manager link : https://arduino.esp8266.com/stable/package_esp8266com_index.json

のURLをコピーして、さっきにの環境設定画面の下の方にある「追加のボードマネージャのURL」という入力欄にペースト。[OK]ボタンをクリックして環境設定画面を閉じる。

メニューの[ツール]-[ボード “Arudino Uno”]-[ボードマネージャ]でボードマネージャ画面を開く。
インターネットからボード一覧を取得。ボード一覧の取得が終わるとプログレスバーが消える。

画面上部にある入力欄に「esp8266」を入力。最新版version=2.7.1をinstall。(2020/05月時点)

f:id:trafalbad:20200612230716p:plain

[閉じる]ボタンをクリックで閉じる。


ESPr Developer用のボード制御プログラムの選択とパラメータ設定


1.メニュー[ツール]-[ボード]で”Generic ESP8266 Module”を選ぶ。メニュー[ツール]を開いて、[書き込み装置]を”USBasp”に変更。
f:id:trafalbad:20200525223704p:plain


2.メニュー[ツール]-[シリアルボード]を”/dev/cu.usbmodem14301(Arduino Uno)”に変更。

f:id:trafalbad:20200612230255p:plain

3.メニュー[ツール]-[ボード]で”Generic ESP8266 Module”を”Arduino Uno”に変更
f:id:trafalbad:20200612230324p:plain

これでArduino Unoの設定完了。ボードに書き込める。



4.MacにUSBカードでubuntuをinstallする方法

1.USBカードを差し込む。

$ diskutil list
$ diskutil unMountDisk /dev/disk2 (USB= /dev/disk2)
$ sudo dd if=ubuntu-18.4.2.iso of=/dev/disk2 bs=1m

終わったら、「無視」をクリック。

2.macをoption押しながら起動してUSBの選択肢を選択する。

3.あとは[ubuntu〜 install]をクリックしてwifi設定も含めてubuntuをinstall
ほとんどvirtualBoxと同じ方法でinstall。

GKEのkubenetes上でflaskを動かし、curlでPOSTした画像の予測結果を受け取る【機械学習】

flaskで機械学習の学習済みモデルを入れてkubenetes上で動かしてみた。

curlで投げて、予測結果を受け取る形式だけど、envoyとかスケーラビリティも意識した構成になってる。

kubenetes上での flask動作環境
f:id:trafalbad:20200417224845j:plain


目次
1.flaskアプリケーションの作成
2.Docker上でflaskを動かす
3.GKEのkubenetesでflaskを動かす
4.GKEのflask動作環境
5.minikubeのメモ




1.flaskアプリケーションの作成

# 仮想環境作成
$ Python3.7 -m venv venv && source venv/bin/activate
$ pip install -r requirements.txt


projectフォルダ内のファイルの役割


・__init___.py
App = Flask(__name__)
でFlask本体を作る

Server.py単体でも動く。



・views.py
http://~でリクエストがあった時の処理

@app.route(‘/’)でhttpにリクエストがあったことをしめす
@app.route(‘/post’)とかは「https://~/post」とURLの後にその文字列をつける


・server.py
"python server.py" でHTTPにアクセスした時にする処理

rom flask import Flask, jsonify, request
import requests
import numpy as np
import cv2
import json
from flasks.predict import predict
app = Flask(__name__)

@app.route("/", methods=['GET'])
def hello():
    return "Hellos !"

@app.route('/reply', methods=['GET', 'POST'])
def reply():
    if request.method == 'POST':
        data = request.data.decode('utf-8')
        url = json.loads(data)['key']
        resp = requests.get(url, stream=True).raw
        img = np.asarray(bytearray(resp.read()), dtype="uint8")
        img = cv2.imdecode(img, cv2.IMREAD_COLOR)
        pred = predict(img)
        return int(pred)

if __name__ == "__main__":
    app.run(host='0.0.0.0',port=5000,debug=True)


・configファイル(config.py)
環境・設定情報を書く

DEBUG =True


ディレクトリ構成図

$ tree
├── Pipfile
├── Pipfile.lock
├── flasks
│   ├── __init__.py
│   ├── config.py
│    |└── views.py
     ________templates
└── server.py

html系の「templates」ファルダ
=>views.py返す処理(レンダリング)のhtmlやCSSその他を置く場所


Djangoとほぼ同じ仕組みだけど、かなり簡単な構成になってる。




2.Docker上でflaskを動かす

今回はtensorflow-gpuを入れた。

Dockerfile

FROM tensorflow/tensorflow:devel-gpu
RUN mkdir /home/app
WORKDIR /home/app/
COPY requirement.txt /home/app/
RUN pip install --upgrade pip
RUN pip install -r /home/app/requirement.txt
ADD server.py /home/app/
ADD flasks /home/app/flasks
ADD Pipfile /home/app/
ADD Pipfile.lock /home/app/
CMD ["python", "server.py"]


Makefileでコマンドをまとめて簡略化
Makefile

build:
	docker build -t flasks .
run:
	docker run -p 5000:5000 -it flasks
# dockerイメージ作成
$ make build
# コンテナ起動
$ make run


curl画像のURLをPOSTして、予測結果を受けってみる



# 試す
curl -X POST -H "Content-Type: application/json" -d{"key":"http://tore~e.jpg"}' http://localhost:5000/flask
>>>
16

返ってきた。


普通のpngとかの画像をcurlで投げるコマンドはこれ

$ curl -F "file=@test.png" http://localhost:5000/flask


3. GKEのkubenetesでflaskを動かす

全部GCPのターミナルで動かす。

# dockerイメージ作成
$ docker build -t gcr.io/[プロジェクト名]/flask .
# GCRにdockerイメージのpush
$ docker push gcr.io/[プロジェクト名]/flask

クラスタGUIで作成した後のコマンド。
yamlファイルとかflaskのアプリケーションをuoloadしておく。

# クラスタのアクセス権取得
$  gcloud container clusters get-credentials cluster-1 --zone us-central1-c

# yamlファイル実行
$ kubectl apply -f ~.yaml

# flaskが動いてるか確認
$ kubectl get service

f:id:trafalbad:20200417225002j:plain

# curlでPOSTしてみる
$ curl -X POST -H "Content-Type: application/json" -d{"key":"http://tore~e.jpg"}' http://[EXTERNAL-iP]/flask
>>>
16


正常に動いてる。





4.GKEのflask動作環境

外部からcurlとかでアクセスして、予測結果を受け取る形式になっている。

Podを多くしてもスケーラビリティが悪くならないようにenvoyを使った。


envoyを使うとPod数が多くても綺麗に分散される
f:id:trafalbad:20200417225020p:plain


再度動作環境の全体像
f:id:trafalbad:20200417224845j:plain



4.minikubeのメモ

minikubeはローカルでもkubectlコマンドも使えるkubenetesと同じ環境を簡単に作れて、動かすことができる。

GCPとかでいちいち動かすのが億劫なら、仮想環境に入れとくと便利。

# kubectl install
$ brew update && brew install kubectl 
# minikube install
$ brew install minikube

# バージョン確認
minikube version
#起動
minikube start
# minikube削除
$ minikube delete


バックエンドは門外漢に近いけど、flaskとかkubenetesに詳しくなったとか、いいスキル習得になった。



参考記事



kubernetesでgRPCするときにenvoy挟んでみたよ

Dockerfile-GCS

PIL を使って GCS にある画像を動的にリサイズして送信する

BytesIO(およびStringIO、cStringIO)の使い方【初心者向け】


追記


シンボリックリンクについて

シンボリックリンク
・ファイルやフォルダの代理人ファイル
・元のファイルとシンボリックリンクは別物
シンボリックリンクを削除しても元のファイルに影響はない
・どこにでも作れる
・フォルダに対しても作れる

■ハードリンク
・ファイルに付けたあだ名
・元のファイルとハードリンクは(ほぼ)同じ物
・状況によっては、ハードリンクが削除されると元のファイルも削除される
・同じパーティションにしか作れない
・フォルダに対しては作れない


参考サイト:シンボリックリンク(英:symbolic link)とは

PetaLinux上からultra96v2ボードでYolov3の物体検出【FPGA, hardware, AI】

前の記事「Vitis,Petalinuxのインストールから、Vivadoの起動まで【FPGA】-VitisPlatform作りpart2」でVivado, vitis, Petalinuxの環境構築をした。


PetaLinuxの環境構築をしておくことで、linux(PetaLinux)上から直接、ultra96ボードにアクセスできるらしい。

少なくともVivado, vitis, Petalinuxの環境構築さえしてしまえば、PetaLinuxを通して、直接ultra96を動かして性能を確かめられる。


今回は物体検出で有名なyolov3とtiny-yolov3をPetaLinux上で動かし、ultra96v2の性能を試してみた。



目次
1.Yolov3の準備
2.使う画像
3.ultra96v2でYolov3の性能検証
4.ultra96v2でTiny-Yolov3の性能検証
5.gprofでYolov3の処理性能可視化





1.Yolov3の準備

簡単にコマンド操作で完了。

# darknetをgit clone
$ git clone https://github.com/pjreddie/darknet
# Yolov3の動作環境構築
$ cd darknet; make

# 学習済みyolov3の重みをdownload
$ cd darknet
$ wget https://pjreddie.com/media/files/yolov3.weights


これでyolov3の動作環境ができた。PetaLinuxの環境構築をしておけば簡単。





2.使う画像

拾ってきたこの画像。


test1.jpegf:id:trafalbad:20200413114840j:plain


test2.jpegf:id:trafalbad:20200413114915j:plain

これでyolov3とtiny-yolov3の性能を比較してく。




3.ultra96v2でYolov3の性能検証

test1.jpegでトライ


# yolov3で物体検出
$ cd darknet
$ ./darknet detect cfg/yolov3.cfg yolov3.weights /home/[host-name]/test1..jpeg

>>>
../test1.jpeg: Predicted in 41.924274 seconds.
truck: 79%
car: 93%
car: 85%
car: 83%
car: 78%
car: 67%
person: 100%
person: 99%


推論にかかった処理時間(予測時間)は41.924274秒

予測結果画像f:id:trafalbad:20200413115150j:plain






test2.jpegでトライ



$ cd darknet
$ ./darknet detect cfg/yolov3.cfg yolov3.weights /home/[host-name]/test2..jpeg
>>>
../test2.jpeg: Predicted in 37.329435 seconds.
backpack: 80%
person: 100%
person: 100%
person: 99%
person: 98%
person: 93%
person: 87%

予測結果画像f:id:trafalbad:20200413115320j:plain

推論にかかった処理時間(予測時間)は37.329435秒


かなりいい具合に検出できてる。

ラズパイだとYolov3に213.6秒くらいかかるらしいから、ultra96v2がかなり高速で推論可能なことがわかる。




4.ultra96v2でTiny-Yolov3の性能検証

# 学習済みTiny-Yolov3の重みをdownload
$ wget https://pjreddie.com/media/files/yolov3-tiny.weights

# test1.jpegで物体検出
$ ./darknet detect cfg/yolov3-tiny.cfg yolov3-tiny.weights /home/[host-name]/test1.jpeg
>>>
Loading weights from yolov3-tiny.weights...Done!
../test1.jpeg: Predicted in 1.380456 seconds.
car: 65%
car: 55%
person: 94%


処理速度(推論速度)は1.380456 seconds(ほぼ1秒)
かなり高速

予測結果画像f:id:trafalbad:20200413115547j:plain

結果は本家Yolov3と比べてあまり良くないけど、簡単な画像なら十分な気がする。


ちなみにターミナル上での可視化は下コマンドで。

$ sudo apt install imagemagick
$ display predictions.jpg

f:id:trafalbad:20200413120009p:plain




5.gprofでYolov3の処理性能可視化

gprofでYolov3のtest1.jpeg, test2.jpeg処理性能を可視化してみる。

gprofでの可視化は「System-on-Chip (SoC) Design」を参考に。

'gprof2dot.py'はgprof2dot.pyの配布サイトから持ってきた。

gprofで可視化するまでの手順

#  gprof2dot.pyをダウンロード
$ git clone https://github.com/jrfonseca/gprof2dot
$ mv gprof2dot/gprof2dot.py gprof2dot.py; chmod 755 gprof2dot.py

# yolov3をダウンロード
$ git clone https://github.com/pjreddie/darknet
$ cd darknet; make

ここでMakeファイルを書き換える。CFLAGSライン全部に "-pg" オプションをつける。

$ sudo vi Makefile
>>>
# CFLAGSライン全てに -pgをつける
GPU=0
CUDNN=0
OPENCV=0LDFLAGS= -lm -pthread 
COMMON= -Iinclude/ -Isrc/
CFLAGS=-Wall -Wno-unused-result -Wno-unknown-pragmas -Wfatal-errors -fPIC -pg

ifeq ($(OPENMP), 1) 
CFLAGS+= -fopenmp -pg
endif

ifeq ($(DEBUG), 1) 
OPTS=-O0 -g
endif

CFLAGS+=$(OPTS) -pg

ifeq ($(OPENCV), 1) 
COMMON+= -DOPENCV
CFLAGS+= -DOPENCV -pg
LDFLAGS+= `pkg-config --libs opencv` -lstdc++
COMMON+= `pkg-config --cflags opencv` 
endif

ifeq ($(GPU), 1) 
COMMON+= -DGPU -I/usr/local/cuda/include/
CFLAGS+= -DGPU -pg
LDFLAGS+= -L/usr/local/cuda/lib64 -lcuda -lcudart -lcublas -lcurand
endif

ifeq ($(CUDNN), 1) 
COMMON+= -DCUDNN 
CFLAGS+= -DCUDNN -pg
LDFLAGS+= -lcudnn
endif
〜〜〜
# 再ビルド
$ make clean; make

# yolov3を動かして「gmon.out」を出力させる
$ ./darknet detect cfg/yolov3.cfg ../yolov3.weights ../street.jpeg
>>>>
../street.jpeg: Predicted in 35.500371 second

# 確認
$ ls 
gmon.out      predictions.jpg darknet  〜

# 可視化画像(output.png)を作成
$ cp ../gprof2dot.py gprof2dot.py
$ gprof ./darknet | ./gprof2dot.py | dot -Tpng -o output.png

Yolov3のtest1.jpegの処理性能の可視化f:id:trafalbad:20200413123603p:plain


Yolov3のtest2.jpegの処理性能の可視化f:id:trafalbad:20200413124300p:plain


Yolov3のNN以外のCPUの処理性能部分で高速化できる要素は、「gemm_nn」の部分。
ここがCPU部分の処理性能の90%以上を占めてるので、vivadoで高速化できる。




Tiny-Yolov3のtest1.jpegの処理性能の可視化f:id:trafalbad:20200413123746p:plain





今回はultra96v2でyolov3の物体検出をしてみた。
PetaLinuxの環境構築をしておけば、linux(PetaLinux)上から簡単にultra96v2を動かせるので便利。

今度は、USBカメラにつけてもっと実用的に動かしてみる。



参考サイト



Ultra96 PYNQ Darknet Google-Colabo

System-on-Chip (SoC) Design

gprof2dotのgithub