Goolge Text-to-Speech "gTTS" を試す

※ この記事は、 クリエイティブ・コモンズ・表示・継承ライセンス3.0 のもとで公表されたウィキペディアの項目 「ディープラーニング - Wikipedia」 を素材として二次利用しています。

シンプルにテキストを音声にしたい、というモチベーションからgTTS(Google Text-to-Speech)を試してみた。 Web環境を用意して、gTTSをインストールすればOK。

$ pip install gTTS

ディープラーニング - Wikipedia のテキストを音声変換するテストコードを書いてみた。

from gtts import gTTS

def main():
    txt_01 = """
  ディープラーニングまたは深層学習とは、対象の全体像から細部までの各々の粒度の概念を階層構造として関連させて学習する手法のことである。
  深層学習として最も普及した手法は、多層の人工ニューラルネットワークによる機械学習手法である。
  多層ニューラルネットワークについては、ジェフリー・ヒントンの研究チームが2006年に考案したスタックドオートエンコーダが直接の起源となった。
  """

    txt_02 = """
  要素技術としてはバックプロパゲーションなど、20世紀のうちに開発されていたものの、4層以上の深層ニューラルネットについて、
  局所最適解や勾配消失などの技術的な問題によって十分学習させられず、性能も芳しくなかった。
  しかし、21世紀に入って、スタックドオートエンコーダを始めとするヒントンらによる多層ニューラルネットワークの学習の研究や、
  学習に必要な計算機の能力向上、および、インターネットの発展による学習データの流通により、十分に学習させられるようになった。
  その結果、音声・画像・自然言語を対象とする諸問題に対し、他の手法を圧倒する高い性能を示し、2010年代に普及した。
  学界では更に抽象化された数学的概念によるディープラーニングが研究されている。
  """

    txt_03 = """
  ディープラーニングは、学習に用いる具体的な数学的概念はどうであれ、対象の全体像から細部までの各々の粒度の概念を階層構造として関連させて学習する手法を指す。
  21世紀に入って、オートエンコーダを始めとするジェフリー・ヒントンらによる多層ニューラルネットワークによる学習の研究や、学習に必要な計算機の能力向上、
  および、インターネットの発展による学習データの流通により、多層ニューラルネットによる手法が最初に確立された。
  その結果、音声・画像・自然言語を対象とする諸問題に対し、他の手法を圧倒する高い性能を示し、2010年代に普及した。
  結果として多層の人工ニューラルネットワークによる機械学習手法が広く知られるようになったが、ニューラルネットワーク以外でも深層学習は構成可能であり、
  現在はニューラルネットワークよりも抽象的な深層学習の数学的概念が模索されている最中にある。
  ビジネスの現場では多層ニューラルネットワークの応用が盛んであり、「ディープラーニング=ニューラルネットワーク」などと解釈される事が多いが、
  学界ではニューラルネットワーク以外の手法も含めた抽象的な概念として説明される。
  """

    t2s(txt_01, "audio_001.mp3")
    t2s(txt_01+txt_02, "audio_002.mp3")
    t2s(txt_01+txt_02+txt_03, "audio_003.mp3")


def t2s(txt, audio_file_name):
    print('len(text):', len(txt))
    tts = gTTS(text=txt, lang='ja')
    tts.save(audio_file_name)
    print(f"Saved {audio_file_name}")
    print('')


if __name__ == "__main__":
    main()

コア部分はt2s()。 テキストと音声ファイル名を渡すだけ。

音声は女性の声で多少の癖はあるが聞きやすい方だと思う。 ただし、話す速度はもっと速くてもいいかな、というレベル。 音声はこちら にあるのでダウンロードして聞いてみて下さい。

gTTSのドキュメント を見たけど、slowパラメーターはあるが、fastというパラメーターはなかった。 なので、速くしたいなら自分で調整する処理を実装しないといけない。

あと制限については Google Cloud Text-to-Speechの「割り当てと上限」 に書いてあった。 ただし、Cloudの仕様なので、gTTSと同じとは限らないが、無料ならまあ十分と言える仕様である。 1分あたりのリクエスト数は1,000回なので、リクエストを数回叩けば、数万文字を音声にできるので、 この辺も自分で音声を繋げる処理を書けばなんとかなりそう。

音声合成の上限
1リクエストあたりの合計文字数 5,000
上限のタイプ 使用量上限
1 分あたりのリクエスト数 1,000
1 分あたりの文字数 500,000

上記のコードでは3種類の文字数のリクエストを投げているが、 いずれの文字数も問題なく10秒ほどで処理できていた。

$ python main.py 
len(text): 190
Saved audio_001.mp3

len(text): 526
Saved audio_002.mp3

len(text): 1033
Saved audio_003.mp3

参考文献

とにかくDjangoでAjaxのPOSTを機能させる近道

f:id:Shoto:20211003202222p:plain

DjangoAjaxのPOSTを機能させる近道が分かったのでメモ。 今回はDjango-3.0.9を利用。 参考文献の1と2でアプリの画面を表示できるようにして、3でajaxに関連するコードをコピペ&アプリ名修正したら、簡単に実現できた。 もうアプリ画面が表示できてる場合は3だけ見ればOK。

昨日半日ぐらいかけて調べまくったのにできなかったけど、今日はすんなりできてしまった。。色々、おまじない的なコードが重要っぽい。 まだよく理解してないけど、実現できたのでよかった。

参考文献

  1. Python Django入門 (3)
  2. Python Django入門 (4)
  3. 【django】Ajaxによる非同期通信:動的にページ更新する方法

【Blender 2.8 x Python API】ボールで壁を破壊する物理演算

f:id:Shoto:20200610233742g:plain

ボールで壁を破壊する物理演算をPython APIだけで実現する。 前準備として、Blenderを起動して、 LayoutタブからScriptingタブに変えて、新規をクリックしておく。

Python API 実装解説

BlenderPython API bpyをimport。 Blender 2.8からかなり仕様が変わっている。 bpyを操作に調べると分かるが、 ブログや記事に書いてあるコードが動かないことが多い。

# import libraries
import bpy

操作は何度か繰り替えすことを想定している。 なので、繰り返す度に環境をリフレッシュさせるため、全選択&削除を実行。

# Reset objects.
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(True)

light_add()でライトをセット。 以下のパラメーターは、配置するボールと壁に当たるように調整した結果。 と言いつつ、上図はソリッドモードなので関係ないが。 ライトの当たり具合を確認するには、レンダープレビューにする必要がある。

# Add light
bpy.ops.object.light_add(type='AREA', radius=10, location=(0, -20, 20))
bpy.context.object.data.energy = 10000
bpy.context.object.rotation_euler[0] = 0.52

primitive_plane_add()で平面を設置。 object_add(type='PASSIVE')で崩壊する壁を受け止めるようになる。

# Add plane.
bpy.ops.mesh.primitive_plane_add(enter_editmode=False, location=(0, 0, 0))
bpy.ops.transform.resize(value=(10, 40, 1))
bpy.ops.rigidbody.object_add(type='PASSIVE')

primitive_cube_add()でキューブを積み重ねて壁を作る。 object_add(type='ACTIVE')とする事で、平面とは異なり、ボールの衝突によってキューブが物理演算に従って移動する。

# Add wall.
idx = 0
for x in range(-5, 5):
    for y in range(-1, 0):
        for z in range(0, 5):
            bpy.ops.mesh.primitive_cube_add(size=1, enter_editmode=False, location=(x+0.5,y+0.5,z+0.5))
            bpy.ops.rigidbody.object_add(type='ACTIVE')

primitive_uv_sphere_add()で球をセット。 キューブと同様、object_add(type='ACTIVE')とし、ボールを壁へ衝突させる物理演算を実現する。

# Add ball.
bpy.ops.mesh.primitive_uv_sphere_add(radius=1, enter_editmode=False, location=(0, -15, 2))
bpy.ops.rigidbody.object_add(type='ACTIVE')

ボールが動き出すための時間をセット。 30フレームで1秒。 これが0フレーム(0秒)だと、開始時から壁が破壊され始めて、何が起きたかよく分からなくなる。

# Set animation and physical calculation
n_stay = 30

n_stay後のボールの動きをセットする。 まずは加速をアニメーションで作成。
(1)frame_set()でフレームを移動
(2) ボールのIDであるSphereを選択、このIDは球のデフォルトの名前
(3) keyframe_insert()でボールをセットした位置をキーフレームに登録する位置としてセット
(4) アニメーションをONに
(5) keyframe_insert(data_path='kinematic')で上記の内容をキーフレームに登録

bpy.context.scene.frame_set(n_stay+1)  # (1)
obj = bpy.context.scene.objects.get('Sphere') # (2)
obj.keyframe_insert(data_path='location') # (3)
bpy.context.object.rigid_body.kinematic = True  # (4) Animation ON
obj.rigid_body.keyframe_insert(data_path='kinematic')  # (5)

ボールの加速の終わりをセット。 設定方法は上記とほぼ同じ。 frame_set()でスタート位置より+4のフレームに移動し、 Sphereを選択して、 Y軸だけ-15mから12mに移動して、 keyframe_insert(data_path='location')でキーフレームに登録する。 4フレームで27mを進んだことになるので、 この時点で秒速202.5m(= 27m / (4/30)s)出ていることになる。

bpy.context.scene.frame_set(n_stay+5)
obj = bpy.context.scene.objects.get('Sphere')
obj.location = (0, 12, 2)
obj.keyframe_insert(data_path='location')

最後に物理演算の設定。 202.5mになった次のフレームで、アニメーションを解除する。 キーフレームを移動して、ボールを選択するまでは一緒。 その後、kinematicをFalseにした状態で、 keyframe_insert(data_path='kinematic')でキーフレームを登録することで、 アニメーションが解除になり、202.5m/sで放たれたボールが物理演算に従ってシミュレートされる。

bpy.context.scene.frame_set(n_stay+6)
obj = bpy.context.scene.objects.get('Sphere')
bpy.context.object.rigid_body.kinematic = False  # (1)
obj.rigid_body.keyframe_insert(data_path='kinematic')  # Animation OFF  (2)

フレームを1に戻す。 Layoutタブに戻して、タイムラインウィンドウで再生を押すだけでシミュレートできる。

bpy.context.scene.frame_set(1)

全コード

# import libraries
import bpy

# Reset objects.
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(True)

# Add light
bpy.ops.object.light_add(type='AREA', radius=10, location=(0, -20, 20))
bpy.context.object.data.energy = 10000
bpy.context.object.rotation_euler[0] = 0.52

# Add plane.
bpy.ops.mesh.primitive_plane_add(enter_editmode=False, location=(0, 0, 0))
bpy.ops.transform.resize(value=(10, 40, 1))
bpy.ops.rigidbody.object_add(type='PASSIVE')

# Add wall.
idx = 0
for x in range(-5, 5):
    for y in range(-1, 0):
        for z in range(0, 5):
            bpy.ops.mesh.primitive_cube_add(size=1, enter_editmode=False, location=(x+0.5,y+0.5,z+0.5))
            bpy.ops.rigidbody.object_add(type='ACTIVE')

# Add ball.
bpy.ops.mesh.primitive_uv_sphere_add(radius=1, enter_editmode=False, location=(0, -15, 2))
bpy.ops.rigidbody.object_add(type='ACTIVE')

# Set animation and physical calculation
n_stay = 30

bpy.context.scene.frame_set(n_stay+1)
obj = bpy.context.scene.objects.get('Sphere')
obj.keyframe_insert(data_path='location')
bpy.context.object.rigid_body.kinematic = True  # Animation ON
obj.rigid_body.keyframe_insert(data_path='kinematic')

bpy.context.scene.frame_set(n_stay+5)
obj = bpy.context.scene.objects.get('Sphere')
obj.location = (0, 12, 2)
obj.keyframe_insert(data_path='location')

bpy.context.scene.frame_set(n_stay+6)
obj = bpy.context.scene.objects.get('Sphere')
bpy.context.object.rigid_body.kinematic = False  # Animation OFF
obj.rigid_body.keyframe_insert(data_path='kinematic')

bpy.context.scene.frame_set(1)

参考文献

Ubuntu18.04をインストールしてPyTorchでGPUが使えるかを確認するまでの設定

Nvidia GeForce RTX 2080 Tiを搭載したOSなしのPCを買ってライブUSBを指すも、バグった画面が出てインストール画面にすら行かないという状況だったが、 なんとかNvidia Driver/CUDA/cuDNNを入れて、PyTorchでGPUを認識するに至った過程を残しておく。

1. インストールメディアの作成

ライブUSBでもライブDVDでもいいが、Ubuntu18.04のISOファイルを書き込んだメディアを作成する。 簡単だがそれぞれ作り方が異なるので この辺とか ネットで検索してから実施した。 自分は最初、手持ちの古いUSBでライブUSBを作成したがバグ画面が出たので、古いからダメなのかなと思い、 DVDを買ってきてライブDVDを作成して再度インストールを試みたが同じ画面が出た感じ。 結局どっちでもいいと分かったので、その後はDVDで進めている。 今回の件とは無関係かもしれないが、今回インストールしたUbuntu18.04は日本語翻訳版で、 それが悪さしているという噂 も散見されるので、英語版でやった方がすんなり行くかも。

2. インストール画面に行けない問題の解決

メディアを読み込むとGNU GRUB画面が出る。 デフォルトのTry Ubuntu without installingか、その下のInstall Ubuntuに進むと、 Ubuntu18.04のインストール画面に行く。 自分はWi-Fiが使えるのかを確認してからインストールしたかったので前者を選択したが、 どちらもその先はバグった画面にメッセージが書かれたまま進まない状況となる。 解決策としては、 ここ にも書かれている通り、以下の手順を踏むと上手く行く。

  1. GNU GRUB画面でTry Ubuntu without installingInstall Ubuntuを選択した状態にする
  2. e キーを押して起動コマンドを編集する画面を開く
  3. 起動コマンドのquiet splashの箇所をnomodesetに書き換える
  4. F10で起動

3の手順は最後に追加すればOKとかいうサイトもあるけど、quiet splashnomodesetに置き換える必要がある。 あとnomodesetのスペルミスに気を付けて入力すること。 上手く行けばインストール画面に進める。

3. ログイン画面に行かない問題の解決

無事インストールできたと思っても、再起動後にログイン画面が表示されず、またバグった画面が出るという状態になった。 問題はWaylandという未完成のディスプレイサーバーを利用しているかららしい。 なので、これをオフにすると上手く行った。 本記事には詳しく書かないが、 ここ を参考に進めたら上手く行った。1度だけ。。 ちなみに、Advanced option for Ubuntuを選択する画面だが、 自分の環境ではShiftではなくEscを連打したら行くことができた。 あと、自分はviではなくemacs派だったのでviを使えないのだが、 手順の例に挙げられているnanoは普通のエディタなので、vi使えない人はnanoがいいと思う。

4. ログイン画面に行かない問題の解決 その2

再起動したもの再びログイン画面は表示されなかった。 上記と同じ手順に従うと行けるはずと思ったが、もうconfファイルの編集は必要ないので、 [Advanced option for Ubuntu] > [Ubuntu, with Linux ... (recovery mode)] > [resume] > [Ok] の手順でログイン画面の表示に成功した。

5. Nvidia Driverを入れて問題解決

様々な参考文献を読んでると、どうやら諸悪の根源はNvidia Driverのような感じがする。 と言うわけで、Nvidia Driverを入れれば全部解決するっぽい。 ただし、自分はNvidia Driverだけでなく、 CUDA/cuDNNまで入れてPyTorchが動くことまで確認したら、 ログイン画面が表示されるようになった。 ただし、Ubuntu18.04とPyTorch1.4では、この辺はほぼ全自動で簡単にできる。

5.1. Secure BootをDisableにしてNvidia Driverを入れる

まずPCを再起動して、F11を連打して、UEFI画面を開いて、セキュアブートを無効にする。 次にログイン画面して、以下のコマンドを入力して各種ドライバーをインストールする。 詳しくは ここここを参照。 なお、これだけでもログイン画面問題は解決するかも。

$ ubuntu-drivers devices
$ sudo ubuntu-drivers autoinstall

5.2 Anacondaを入れて仮想環境にPyTrochを入れる

ここを参考にAnacondaを入れたあと、 仮想環境を作って、そのにPyTorchを入れる。 PyTorchをインストールする際は、以下のようにcudatoolkitも入るので、 これでCUDAとcuDNNもバージョンを気にせずインストールできる。

conda install pytorch torchvision cudatoolkit=10.1 -c pytorch

すべて上手くいくと以下のように環境構築できたことが確認できる。

$ nvidia-smi
Sun Apr  5 12:31:15 2020       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 435.21       Driver Version: 435.21       CUDA Version: 10.1     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  GeForce RTX 208...  Off  | 00000000:01:00.0  On |                  N/A |
| 35%   30C    P8    12W / 260W |    395MiB / 11011MiB |      8%      Default |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|    0      1169      G   /usr/lib/xorg/Xorg                            18MiB |
|    0      1263      G   /usr/bin/gnome-shell                          58MiB |
|    0      1572      G   /usr/lib/xorg/Xorg                           157MiB |
|    0      1705      G   /usr/bin/gnome-shell                         113MiB |
|    0      2938      G   ...quest-channel-token=6894681319300380423    46MiB |
+-----------------------------------------------------------------------------+

$ python
Python 3.6.10 |Anaconda, Inc.| (default, Mar 25 2020, 23:51:54) 
[GCC 7.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import torch
>>> print(torch.cuda.is_available())
True

参考文献

【論文解説】Real-world Anomaly Detection in Surveillance Videos

監視カメラ映像の異常検知に関する論文。 内容が弱ラベル訓練データをmultiple instance learning (MIL) で訓練するという、 個人的にノータッチの手法だったので読んでみたが、中身は非常に分かりやすかった。

1. 概要

監視カメラ映像における実世界異常検知のデータと手法に関する研究。 1900本128時間の映像からなる大規模データセットYouTube等から選別して異常・正常映像を同数用意。 multiple instance learning(MIL)により、異常を含む映像か否かのみを付与した弱ラベル訓練データでの学習を可能にしている。 映像から異常セグメントを見つけるだけなく、そのセグメントにおける異常行動認識も行う。

2. 先行研究との差異

異常検知は、映像を複数分割したセグメントの異常度をランキングするタスクとして考えられる。 近年のランキング手法は、Deep Learningがコンピュータービジョンの様々な分野でSOTAを達成しているが、 正常と異常のアノテーションデータが大量に必要となる。

提案手法ではアノテーション作業を緩和するために、弱ラベル訓練データを利用している。 これにより、セグメントレベルではなく映像レベルで異常をラベリングすることができる。 つまり、ある映像に異常なセグメントがあれば、その映像は異常とラベリングされる。 弱ラベル訓練データの学習は、教師あり学習の一種であるmultiple instance learningにより可能になる。 ある映像の複数セグメントを1つのバッグと考え、異常ラベルの映像はどのセグメントが異常かを学習して、 異常度に応じたランキングを行う。

また異常検知手法として、近年、正常な振舞いを学習したオートエンコーダーの再構築ロスを利用した研究がある。 提案手法でもこの手法を利用するが、正常だけでなく異常な振舞いも考慮している。

3. 手法

3.1 アルゴリズム

手法の全体像を図1に示す。異常映像と正常映像を入力として与え、時間軸を保持したまま32個のセグメントに分割し、それぞれ陽性バッグと陰性バッグに入れる。各セグメントは動画分類器の一種であるC3Dによって特徴抽出されたのち全結合層を通り、陽性バッグと陰性バッグ各々の最大スコアを利用してランキングロスを計算する。

図1 手法の全体像
f:id:Shoto:20200308190221p:plain

ランキングロスは式(6)のように3つのロスの和となる。\beta _ {a}が陽性バッグ、\beta _ {n}が陰性バッグ、l(\beta _ {a}, \beta _ {n})が両者のロス、f(\nu _ {a} ^ {i})が異常映像の各セグメントの異常スコア、f(\nu _ {n} ^ {i})が正常映像の各セグメントの異常スコア、\lambda _ {1}が2つ目のロスの重み、\lambda _ {2}が3つ目のロスの重みとなる。

1つ目のロスは、図1に示す通り陽性バッグと陰性バッグの最大ロスを利用したもので、両者の差が大きくなるように学習する。陰性バッグの最大スコアとなるセグメントはいわゆるHard NegativeであるためFalse Positiveとならないよう、このロスによってスコアが低くなるよう学習が行われる。

2つ目のロスは、映像はセグメントの連続であるため、隣合うセグメントのスコアの変化はスムーズである必要があるという制約となる。3つ目のロスは、異常は突発的に起こるためスコアがまばらに(小数のセグメントのスコアが高く)なるようにという制約となる。

f:id:Shoto:20200308190241p:plain

3.2 データセット

既存の異常検知向けの映像データセットは、全体的に映像の数が少なく長さも短い。また異常のバリエーションが限られており、そのうち幾つかは異常とは呼べないものもある。

提案手法で作成したデータセットは、トリミングしていない比較的長い監視カメラ映像となる。公共の安全において重要な13個の実世界の異常な振舞い(虐待、逮捕、放火、突撃、事故、強盗、爆発、喧嘩、強盗、射撃、窃盗、万引き、破壊行為)が含まれている。10人のアノテーターを訓練して、YouTubeとLiveLeakから"car crash"や"load accident"など多言語でテキストサーチして収集。編集動画、いたずら動画、手持ちカメラ、不明瞭な異常などは含めない。正常・異常各々950本の計1900本の映像からなる。データセットここからダウンロードできる。

なお訓練だけなら映像レベルのアノテーションで問題ないが、テストには異常なフレームのアノテーションが必要になるため、複数人のアノテーターが付けた平均を異常なフレームとしてアノテーションしている。訓練・テストに利用した正常・異常映像の数を表1に示す。

表1 訓練・テストに利用した正常・異常映像の数

normal anomaly
train 800 810
test 150 140

3.3 実装

映像のフレームレートは30FPSに固定して、各セグメントを240x320x16にして、正常・異常共に30バッチずつ入力。 フレームベースで描いたROCのAUCで評価を行う。

4. 評価

4.1 異常検知

表2に既存手法と提案手法のAUCによる評価結果を示す。 先に述べたようにほとんどの異常な振舞いは短時間で発生する。Binary classifier(バイナリ分類器)は、提案手法のように映像をセグメント分割しないため、多くの正常セグメントを含む異常とラベリングされた映像の異常検知には利用できない。Hasan et al.[18]は、正常映像で訓練したオートエンコーダーである。正常セグメントはよく分類できるが、未知の正常セグメントで高い異常スコアが出る傾向にある。Lu[28] et al.は辞書学習で、[18]に比べて大幅に精度は高くなるが、ロバストでない。提案手法は、既存手法のすべての課題を解決できており、[28]に比べて精度が10ポイント程上昇している。2つ目と3つ目のロスで制約を加えた効果も見られる。

表2 既存手法と提案手法のAUC
f:id:Shoto:20200308190301p:plain

図2にテストデータの定性評価を示す。赤い窓が異常範囲のGround Truth、縦軸が異常スコア、横軸が時間を示す。適切に異常検知できたケースでは、Ground Truthと予測した異常スコアの高い部分が重なっていることが分かる。一方、暗闇の侵入を正常と判断したり、集団の出現を異常と判断するなどの誤判定も見られる。

図2 テストデータの定性評価
f:id:Shoto:20200308190315p:plain

表3に正常映像のみで評価したアラーム間違い率を示す。正常と異常が両方含まれる訓練データで学習しているため、アラーム間違い率が1.9と低い。

表3 正常映像のアラーム間違い率
f:id:Shoto:20200308190442p:plain

4.2 異常行動認識

異常行動認識のために、TCNN(Tube Convolutional Neural Network)を利用している。TCNNでは、C3Dの一部をToI(tube of interest)プーリング層に置き換えている。ToIプーリング層は、全セグメントの特徴を統合して1つの映像の特徴とする。これによりend-to-endな異常行動認識が可能になる。表4に異常行動認識の精度比較を示す。C3Dの一部をToIプーリング層に置き換えることにより、5ポイント程精度が向上していることが分かる。

表4 異常行動認識の精度比較
f:id:Shoto:20200308190503p:plain

まとめ

正常映像と異常映像を同数含む1900本128時間という膨大な映像データセットを作成し、映像の異常フレームのラベリングという非常にコストが高いアノテーション作業を、弱ラベル訓練データでも学習可能なMILを利用することで映像単位まで簡略化し、映像の異常検知において既存手法を10ポイントも上回り、さらにTCNNによって異常行動認識まで可能にした。

所感

まとめに書いたようにすごく良くできた論文だった。ただし少しできすぎている感がある。 例えば、セグメントごとに異常スコアを算出している認識だったが、評価指標のAUCを算出するために描いたROCが何故かフレームベースだった。あとTCNN(ToIプーリング層)の部分もあっさりしていて、もう少し詳細な説明が欲しい。あとロス関数の2つの制約のうち、どちらの効果が大きいのかablation testの結果が見たいところ。 また自分の理解力の問題でもあるが、32個のセグメント分割と30FPSのフレームレートと16フレームの入力の関係が結び付かなかった。これはコードを読めば分かるかも知れないが、MATLABで実装っていう。。。 ただし、非常に納得できる内容だったし、今後の映像異常検知のベースとなる研究であると思う(というかなってきている)。 特に、データは正常映像だけでは限界があるので異常映像も同時に学習させるべきというのと、異常部分にフォーカスするために映像はセグメント分割すべきというのがポイントだと思った。

Torchreid入門

Person ReIDライブラリーのTorchreid がいい感じだったので簡単まとめておく。 チュートリアル に色々な使い方が記載されているが、ここでは以下の3つについてまとめている。

  1. 訓練
    README.mdGet started: 30 seconds to Torchreid。 Market1501で距離学習モデルを訓練する。
  2. テスト
    チュートリアルTest a trained model。 1で訓練でしたモデルの精度を算出する。
  3. ランキング結果の可視化
    チュートリアルVisualize ranking results。 1で訓練したモデルでqueryに対するgalleryの検索結果を可視化する。

コードはこちら

1. 訓練

データ、モデル定義、最適化関数は、訓練だけでなくテストとランキング結果の可視化でも必要になるので、 これらを取得する共通メソッドget_items()を定義しておく。 また訓練ではschedulerも必要なので、それもリターンする。 これらをtrain()に渡してモデルを訓練する。

# Training
datamanager, model, optimizer, scheduler = get_items()
train(datamanager, model, optimizer, scheduler)

データはdatamanager、モデル定義はmodel、最適化関数はoptimizer、 スケジューラーはschedulerに格納する。

  • データ
    今回は訓練データとテストデータ共にmarket1501を利用する。 各々sourcesとtargetsに指定するが、他のデータ(との組合せ)を記載することも可能。

  • モデル定義
    ベースモデルはResNet50を利用。 メトリックの指定方法など、具体的な設定方法はまだ勉強中。

  • 最適化関数
    無難にAdamを利用。 pytorch.optimに実装されているのなら利用できる予感。

  • スケジューラー
    シングルステップを指定。 pytorch.optim.lr_schedulerに実装されているのなら利用できる予感。

def get_items():
    # Step 2: construct data manager
    datamanager = torchreid.data.ImageDataManager(
        root='D:/workspace/datasets',
        sources='market1501',
        targets='market1501',
        height=256,
        width=128,
        batch_size_train=32,
        batch_size_test=100,
        transforms=['random_flip', 'random_crop']
    )

    # Step 3: construct CNN model
    model = torchreid.models.build_model(
        name='resnet50',
        num_classes=datamanager.num_train_pids,
        loss='softmax',
        pretrained=True
    )
    model = model.cuda()
    
    # Step 4: initialise optimiser and learning rate scheduler
    optimizer = torchreid.optim.build_optimizer(
        model,
        optim='adam',
        lr=0.0003
    )
    
    scheduler = torchreid.optim.build_lr_scheduler(
        optimizer,
        lr_scheduler='single_step',
        stepsize=20
    )

    return datamanager, model, optimizer, scheduler

上で定義した4つ変数を引数として訓練を実施。 engineを作ってrunを実行する。 チェックポイントをsave_dirに保存してくれる。 また訓練時はtest_onlyFalseにする。

def train(datamanager, model, optimizer, scheduler):
    # Step 5: construct engine
    engine = torchreid.engine.ImageSoftmaxEngine(
        datamanager,
        model,
        optimizer=optimizer,
        scheduler=scheduler,
        label_smooth=True
    )

    # Step 6: run model training and test
    engine.run(
        save_dir='../experiments/models/checkpoints',
        max_epoch=60,
        eval_freq=10,
        print_freq=10,
        test_only=False
    )

訓練が開始されると次のように標準出力される。 GeForce GTX 1080だと、60エポックは1時間46分かかり、モデル精度はRank-1で85.2%となった。

> python .\main.py
Building train transforms ...
+ resize to 256x128
+ random flip
+ random crop (enlarge to 288x144 and crop 256x128)
+ to torch tensor of range [0, 1]
+ normalization (mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
Building test transforms ...
+ resize to 256x128
+ to torch tensor of range [0, 1]
+ normalization (mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
=> Loading train (source) dataset
=> Loaded Market1501
  ----------------------------------------
  subset   | # ids | # images | # cameras
  ----------------------------------------
  train    |   751 |    12936 |         6
  query    |   750 |     3368 |         6
  gallery  |   751 |    15913 |         6
  ----------------------------------------
=> Loading test (target) dataset
=> Loaded Market1501
  ----------------------------------------
  subset   | # ids | # images | # cameras
  ----------------------------------------
  train    |   751 |    12936 |         6
  query    |   750 |     3368 |         6
  gallery  |   751 |    15913 |         6
  ----------------------------------------


  **************** Summary ****************
  source            : ['market1501']
  # source datasets : 1
  # source ids      : 751
  # source images   : 12936
  # source cameras  : 6
  target            : ['market1501']
  *****************************************


=> Start training
Epoch: [1/1][10/404]    Time 0.188 (0.833)      Data 0.000 (0.505)      Loss 6.7096 (6.7458)    Acc 0.00 (0.62) Lr 0.000300     eta 0:05:28
Epoch: [1/1][20/404]    Time 0.190 (0.511)      Data 0.001 (0.253)      Loss 6.7111 (6.8223)    Acc 0.00 (0.94) Lr 0.000300     eta 0:03:16
Epoch: [1/1][30/404]    Time 0.187 (0.404)      Data 0.001 (0.169)      Loss 6.6187 (6.7905)    Acc 0.00 (1.15) Lr 0.000300     eta 0:02:31
...
Epoch: [60/60][380/404] Time 0.188 (0.202)      Data 0.000 (0.013)      Loss 1.0709 (1.0722)    Acc 100.00 (99.99)      Lr 0.000003     eta 0:00:04
Epoch: [60/60][390/404] Time 0.196 (0.202)      Data 0.001 (0.013)      Loss 1.0650 (1.0722)    Acc 100.00 (99.98)      Lr 0.000003     eta 0:00:02
Epoch: [60/60][400/404] Time 0.194 (0.202)      Data 0.000 (0.012)      Loss 1.0691 (1.0722)    Acc 100.00 (99.98)      Lr 0.000003     eta 0:00:00
=> Final test
##### Evaluating market1501 (source) #####
Extracting features from query set ...
Done, obtained 3368-by-2048 matrix
Extracting features from gallery set ...
Done, obtained 15913-by-2048 matrix
Speed: 0.0236 sec/batch
Computing distance matrix with metric=euclidean ...
Computing CMC and mAP ...
** Results **
mAP: 68.4%
CMC curve
Rank-1  : 85.2%
Rank-5  : 93.6%
Rank-10 : 95.9%
Rank-20 : 97.3%
Checkpoint saved to "../experiments/models/checkpoints/model.pth.tar-60"

Elapsed 1:45:48

2. テスト

テストとランキング結果の可視化のコードはほどんど同じ。 ロードした学習済みモデルの重みについて、精度評価するか距離計算するかの違い。 先に述べたようにデータ、モデル定義、最適化関数を引数として渡す。

# Test
weight_path = '../experiments/models/model_market1501_resnet50.pth.tar-60'
torchreid.utils.load_pretrained_weights(model, weight_path)
test(datamanager, model, optimizer)
vis_rank(datamanager, model, optimizer)

engine.run()test_onlyTrueにするとテスト(精度評価)になる。

def test(datamanager, model, optimizer):
    # Step 5: construct engine
    engine = torchreid.engine.ImageSoftmaxEngine(
        datamanager,
        model,
        optimizer=optimizer
    )

    # Step 6: run model training and test
    engine.run(
        test_only=True

訓練時、最後に保存したチェックポイントをロードしているので精度が同じになる。

Successfully loaded pretrained weights from "../experiments/models/model_market1501_resnet50.pth.tar-60"
##### Evaluating market1501 (source) #####
Extracting features from query set ...
Done, obtained 3368-by-2048 matrix
Extracting features from gallery set ...
Done, obtained 15913-by-2048 matrix
Speed: 0.0317 sec/batch
Computing distance matrix with metric=euclidean ...
Computing CMC and mAP ...
** Results **
mAP: 68.4%
CMC curve
Rank-1  : 85.2%
Rank-5  : 93.6%
Rank-10 : 95.9%
Rank-20 : 97.3%

3. ランキング結果の可視化

ランキング結果の可視化でも2と同様に精度算出してくれる。 さらに画像検索の結果も保存してくれる。 engine.run()save_dirに画像検索結果の保存先を指定して、 visrankTrueにする。

def vis_rank(datamanager, model, optimizer):
    #torchreid.utils.load_pretrained_weights(model, weight_path)

    # Step 5: construct engine
    engine = torchreid.engine.ImageSoftmaxEngine(
        datamanager,
        model,
        optimizer=optimizer
    )

    # Step 6: run model training and test
    engine.run(
        save_dir='../experiments/',
        test_only=True,
        visrank=True
    )

標準出力は次のようになる。

Successfully loaded pretrained weights from "../experiments/models/model_market1501_resnet50.pth.tar-60"
##### Evaluating market1501 (source) #####
Extracting features from query set ...
Done, obtained 3368-by-2048 matrix
Extracting features from gallery set ...
Done, obtained 15913-by-2048 matrix
Speed: 0.0242 sec/batch
Computing distance matrix with metric=euclidean ...
Computing CMC and mAP ...
** Results **
mAP: 68.4%
CMC curve
Rank-1  : 85.2%
Rank-5  : 93.6%
Rank-10 : 95.9%
Rank-20 : 97.3%
# query: 3368
# gallery 15913
Visualizing top-10 ranks ...
- done 100/3368
- done 200/3368
- done 300/3368
- done 400/3368
- done 500/3368
...

検索結果は次の通り。 1列目のように得意なqueryは全正解だが、 2列目のように画像の一部がqueryになると著しく正解率が下がる。 また3列目のようにリュックと服の色の区別ができている訳でもないみたい。

f:id:Shoto:20200215170905j:plain f:id:Shoto:20200215170909j:plain f:id:Shoto:20200215170913j:plain

まとめ

様々なデータやモデルが気軽に非常に便利。 ただし用意されたものだけでは痒い所に手は届かないので、 チュートリアルUse your own datasetDesign your own Engine を参考に改良する必要がある。

PyTorch物体検出チュートリアルを魔改造

PyTorchの物体検出チュートリアルが、 個人的にいじりたい場所だらけだったので、色々と魔改造してみた。 コードはこちら

概要

チュートリアルではTrainingだけだが、今回はTestに関するコードも実装している。 それを含めて以下が今回魔改造した点。 TrainingとTestで各々3つずつポイントがある。

1. Training

  • 1.1. データのCSV
    チュートリアルではデータセットをPyTorchの手続きに従ってクラス化していたが、 自分はほとんどのデータセットを一旦CSVファイルにまとめてからクラス化している。 CSV化することでPandasのDataFrameにできるので、色々な実験をしたい時に便利。

  • 1.2. モデルをFaster RCNNに変更
    ホントは様々なモデルが扱えるMMDetection が良かったが、自分にはちょっとハードルが高かった。 魔改造第2弾があれば使ってみたい。今回はTorchVision。 チュートリアルではMask RCNNを使っていたが、特にマスクは不要なのでFaster RCNNに変更した。 ただし、これはチュートリアルにも例として呼び出し方が載っている。 あとMask RCNNは、今回は関係ないけど、人のマスクでは頭が真っ平になるのが気に食わない っていうのもあって敬遠している。

  • 1.3. チェックポイントを保存
    チュートリアルではモデル自体保存していないが、テストしたいのでチェックポイントを保存できるようにした。

2. Test

  • 2.1. 物体検出
    保存したモデルをロードして物体検出を実行。 検出結果は例によってCSV化している。

  • 2.2. スコアをVOC方式で算出
    cocoapiにより、訓練中にCOCO方式で算出されたmAPが出力されるが、 出力結果を変数として簡単には取得できなかったので、Cartucho/mAPを利用した。 これにより、mAPを変数として取得できるようになり、またTP/FP/Recall/Precisionなども取得できるようになっている。

  • 2.3. Ground Truthと検出結果の描画
    タイトルの通り。 スコアを見るだけは分からない具体的な誤検出や未検出の原因を画像で確認する。

1. Training

まずはTrainingから。 train.pyに実装。 訓練自体についてはほとんどタッチしていないが、その前後をいじっている。

1.1 データのCSV

コードは外部ファイル化している。 他のデータセットも増やせるよう、データセット用のディレクトリーを用意して、./datasets/penn_fudan_ped.pyに実装している。 train.pymain()では以下のように呼び出している。 最終的には、trainとtestのDataLoaderを返しているが、それらを取得するためのget_dataset()の引数はCSVファイルのパスになる。 CSVファイルはmake_csv()にデータセットパスとCSV出力パスを渡すことで作成できる。

from datasets import penn_fudan_ped

def main():
    # Get data.
    if not os.path.exists(args.anno_path):
        penn_fudan_ped.make_csv(args.data_dir, args.anno_path)
    train_data_loader, test_data_loader = penn_fudan_ped.get_dataset(args.anno_path)

./datasets/penn_fudan_ped.pyの中身については詳解しないが、 get_dataset()はどのデータにも横展開できるコードになっているが、 make_csv()チュートリアルと同様、データセットごとに対応したコードを実装してCSVファイルに落とし込む必要がある。 なおCSVファイルにすることにより、Train時は学習データのフィルタリング、 Test時はスコア算出やバウンディングボックス描画などで利便性が出てくる。

1.2. モデルをFaster RCNNに変更

データとモデルは別ファイルにした方がよいというのが経験から得られているので、 ./src/models.pyに実装している。 ただし、ほとんどチュートリアルに載ってるのをそのまま。 クラス数はTrainingとTestで共通なので、引数として渡すようにしている。

def get_fasterrcnn_resnet50(num_classes, pretrained=False):
    model = torchvision.models.detection.fasterrcnn_resnet50_fpn(pretrained=pretrained)
    in_features = model.roi_heads.box_predictor.cls_score.in_features
    model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)

    return model

1.3. チェックポイントを保存

モデルの重みを保存しないと、テストだけしたいという時に困るので。 ./src/train.pymain()に以下のように実装している。

# Save a model checkpoint.
model_ckpt_path = args.model_ckpt_path_temp.format(args.dataset_name, args.model_name, epoch+1)
torch.save(model.state_dict(), model_ckpt_path)
print('Saved a model checkpoint at {}'.format(model_ckpt_path))

訓練中は以下のように標準出力され、 最後にcocoapiのスコアとチェックポイントを保存した旨が表示される。

$ python .\train.py
Loading a model...
Epoch: [0]  [ 0/60]  eta: 0:05:40  lr: 0.000090  loss: 0.9656 (0.9656)  loss_classifier: 0.7472 (0.7472)  loss_box_reg: 0.1980 (0.1980)  loss_objectness: 0.0093 (0.0093)  loss_rpn_box_reg: 0.0111 (0.0111)  time: 5.6684  data: 3.8234  max mem: 3548
Epoch: [0]  [10/60]  eta: 0:00:47  lr: 0.000936  loss: 0.7725 (0.6863)  loss_classifier: 0.4681 (0.4597)  loss_box_reg: 0.1726 (0.2008)  loss_objectness: 0.0127 (0.0163)  loss_rpn_box_reg: 0.0070 (0.0095)  time: 0.9561  data: 0.3484  max mem: 4261
Epoch: [0]  [20/60]  eta: 0:00:28  lr: 0.001783  loss: 0.4149 (0.5500)  loss_classifier: 0.2203 (0.3413)  loss_box_reg: 0.1567 (0.1806)  loss_objectness: 0.0127 (0.0188)  loss_rpn_box_reg: 0.0060 (0.0093)  time: 0.4775  data: 0.0011  max mem: 4261
Epoch: [0]  [30/60]  eta: 0:00:19  lr: 0.002629  loss: 0.3370 (0.4652)  loss_classifier: 0.1338 (0.2607)  loss_box_reg: 0.1504 (0.1787)  loss_objectness: 0.0094 (0.0152)  loss_rpn_box_reg: 0.0084 (0.0106)  time: 0.4745  data: 0.0011  max mem: 4261
Epoch: [0]  [40/60]  eta: 0:00:12  lr: 0.003476  loss: 0.2195 (0.3939)  loss_classifier: 0.0534 (0.2088)  loss_box_reg: 0.1345 (0.1620)  loss_objectness: 0.0063 (0.0129)  loss_rpn_box_reg: 0.0094 (0.0102)  time: 0.4814  data: 0.0011  max mem: 4458
Epoch: [0]  [50/60]  eta: 0:00:05  lr: 0.004323  loss: 0.1478 (0.3427)  loss_classifier: 0.0442 (0.1765)  loss_box_reg: 0.0842 (0.1432)  loss_objectness: 0.0021 (0.0118)  loss_rpn_box_reg: 0.0091 (0.0112)  time: 0.5083  data: 0.0014  max mem: 4623
Epoch: [0]  [59/60]  eta: 0:00:00  lr: 0.005000  loss: 0.1176 (0.3073)  loss_classifier: 0.0433 (0.1562)  loss_box_reg: 0.0561 (0.1293)  loss_objectness: 0.0009 (0.0102)  loss_rpn_box_reg: 0.0117 (0.0117)  time: 0.5165  data: 0.0013  max mem: 4623
Epoch: [0] Total time: 0:00:34 (0.5814 s / it)
creating index...
index created!
Test:  [ 0/50]  eta: 0:03:14  model_time: 0.1120 (0.1120)  evaluator_time: 0.0010 (0.0010)  time: 3.8935  data: 3.7785  max mem: 4623
Test:  [49/50]  eta: 0:00:00  model_time: 0.0900 (0.0962)  evaluator_time: 0.0010 (0.0012)  time: 0.0994  data: 0.0006  max mem: 4623
Test: Total time: 0:00:08 (0.1791 s / it)
Averaged stats: model_time: 0.0900 (0.0962)  evaluator_time: 0.0010 (0.0012)
Accumulating evaluation results...
DONE (t=0.01s).
IoU metric: bbox
 Average Precision  (AP) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = 0.641
 Average Precision  (AP) @[ IoU=0.50      | area=   all | maxDets=100 ] = 0.987
 Average Precision  (AP) @[ IoU=0.75      | area=   all | maxDets=100 ] = 0.859
 Average Precision  (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = -1.000
 Average Precision  (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.325
 Average Precision  (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.644
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets=  1 ] = 0.286
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets= 10 ] = 0.706
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = 0.706
 Average Recall     (AR) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = -1.000
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.700
 Average Recall     (AR) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.706
Saved a model checkpoint at ../experiments/models/checkpoints/PennFudanPed_FasterRCNN-ResNet50_epoch=1.pth

なお、cocoapiをWindows10で利用する場合、インストールに失敗する場合がある。 その際はClone of COCO APIを利用すると上手くいった。

2. Test

Trainingで保存したモデルの重みをロードして、 物体検出とそのスコアの算出および検出結果の画像描画をTestとして行う。

2.1. 物体検出

まずはシンプルに物体検出を実施。 コードは./src/test.pydetect_objects()を参照。 以下のように検出結果が標準出力される。 これは例によってCSVファイルに保存される。

$ python .\test.py
Loading a model from ../experiments/models/PennFudanPed_FasterRCNN-ResNet50_epoch=10.pth
Detecting objects... 100%
========================== DETECTION RESULTS ==========================
   label  score  xmin  ymin  xmax  ymax                                         image_path
0      1  0.999   294   129   447   419  D:/workspace/datasets/PennFudanPed/PNGImages/F...
1      1  0.999   361   135   456   399  D:/workspace/datasets/PennFudanPed/PNGImages/F...
2      1  0.999   207   100   350   382  D:/workspace/datasets/PennFudanPed/PNGImages/F...
3      1  0.999     0   111    88   383  D:/workspace/datasets/PennFudanPed/PNGImages/F...
4      1  0.999    37   100    97   362  D:/workspace/datasets/PennFudanPed/PNGImages/F...
5      1  0.999    40   106    87   268  D:/workspace/datasets/PennFudanPed/PNGImages/F...
6      1  0.998   268    92   397   374  D:/workspace/datasets/PennFudanPed/PNGImages/F...
7      1  0.998   260   191   294   345  D:/workspace/datasets/PennFudanPed/PNGImages/F...
8      1  0.998   262    97   338   357  D:/workspace/datasets/PennFudanPed/PNGImages/F...
9      1  0.999   384   192   551   482  D:/workspace/datasets/PennFudanPed/PNGImages/F...
Detection results saved to ../experiments/results/tables/dets.csv

2.2. スコアをVOC方式で算出

1.1 データのCSV化で保存したGround TruthのCSVファイルと 2.1. 物体検出で保存した検出結果のCSVファイルを照合して、VOC方式のmAPを算出する。 Cartucho/mAPを利用しているが、 以下のようなmAP以外のスコアを得るためにmain.pyを改修したmain_ex.py./src/test.pydetect_objects()から呼び出している。 以下が実行結果。

Making gt text files: 100%|██████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 769.32it/s]
Making det text files: 100%|█████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 735.01it/s]
65.53% = 1 AP
mAP = 65.53%
  class_name        ap  recall  precision   gt  n_det   tp  fp
0          1  0.655343     1.0   0.581395  125    215  125  90
Score saved to ../experiments/results/tables/score.csv

結果を見るとrecall(=tp/gt)は1.0だが、precision(=tp/n_det)は0.58と低くなっている。 つまり誤検出が多いということだが、具体的には何を誤検出しているのか? それを2.3. Ground Truthと検出結果の描画で確認する。

2.3. Ground Truthと検出結果の描画

./src/test.pydraw_gt_n_det()を実行すると、 テスト画像の上にGround Truthと検出結果が描画される。 以下のように描画結果(青がGround Truth、黄緑が検出結果)を見てみると、 自転車に乗っている人、小さく写る人、オクルージョンのある人が アノテーションされておらず、それらを検出しているため誤検出となり、 Precisionの低下を導いていることが分かる。 あと、non-maximum suppressionが上手くできてないというのも原因の1つ。

f:id:Shoto:20200202002031p:plainf:id:Shoto:20200202002026p:plain
f:id:Shoto:20200202002038p:plainf:id:Shoto:20200202002034p:plain

まとめ

チュートリアルだと、 テストがほとんどなかったが、説明したコードを利用すれば基礎な分析はできるようになった。 あとは、訓練時にTorchVisionに依存しているので、 MMDetectionを利用して多数のモデルが使えるようにしたり、 Albumentationsを用いたData Augmentationや、 Schedulerの充実などを行いたい。

参考文献