アイコン

佐藤さとる

梶研 [ST-GCN序章]

2024年05月21日

thumbnail

ST-GCN序章

出席率

  • 3年セミナー:??%

スケジュール

短期的な予定

  • mocopi と お料理センシング
    • シーンとランドマークを決める(~2月上旬)
    • SVM で動作判別する
    • 機械学習を深める
    • お料理センシング
      • お料理でどんな動作があるかを知る
      • レシピを決める
      • 関節を3次元座標に変換する
      • 関節を2次元座標に変換する
      • ST-GCN を大体理解
      • ST-GCN で動作推定
      • 伊達巻きで動作推定する
      • レシピ(手順書)を元に動作推定を補う
    • 論文書く
    • 発表
  • BookWorm
    • Pasori と デスクトップアプリを接続する(技術検証)
    • nfc読み込み機能 & 画面を作る
    • API と連携させる
    • 管理者画面を作る

長期的な予定

  • 6月 精度は置いておき、動作認識を完成させる
  • 7月 リファクターと精度を向上させる
  • 8月 仕上げ
  • 9月 論文書き始め
  • 10月末 論文提出
  • 12月 WiNF当日

進捗報告

目的

料理中の動作を mocopi を使ってセンシングする。
このデータから最終的に位置推定を行う。

  • 一定の区間でどの動作をしているかを当てる (クラス分類)
  • 料理の手順を元にシーン検知を補正する
    • 例) 焼く動作 → 卵割る動作 はおかしい
  • 位置とシーンを相補的に補正する
    • 例) 冷蔵庫の前で焼く動作 はおかしい

目標

12月頃の WiNF に出たい
(10月末 論文完成)


ST-GCN する

ST-GCNとは
骨格から動作認識を行える機械学習の手法. すごいやつ

PyTorch実装で理解するST-GCN を参考にしようとしたが、エラー吐いてて試せなかった
ST-GCNによる動作認識 を参考にする

このコードの元になった研究: https://arxiv.org/abs/1801.07455

畳み込みとは
参考: https://zero2one.jp/ai-word/convolution/
https://www.youtube.com/watch?v=CHx6uHnWErY


理解しつつBVH用に置き換えて行く

ランダム値を固定する

何度実行しても同じ結果になるらしい
ただし、処理が少し遅くなるかも

1seed = 123 2 3np.random.seed(seed) 4torch.manual_seed(seed) 5torch.cuda.manual_seed(seed) 6torch.backends.cudnn.deterministic = True 7torch.use_deterministic_algorithms = True

エポックサイズとバッチサイズ

エポックサイズ: 訓練データを学習させる回数
バッチサイズ: 1回の学習に使うデータ数

1NUM_EPOCH = 100 2BATCH_SIZE = 64

モデルを作成

何者?

1model = ST_GCN( 2 num_classes=10, 3 in_channels=3, 4 t_kernel_size=9, 5 hop_size=2, 6)

モデルを定義

1# モデルを定義 2class ST_GCN(nn.Module): 3 def __init__(self, num_classes, in_channels, t_kernel_size, hop_size): 4 super().__init__() 5 6 # 誰? 7 graph = Graph(hop_size) 8 ... 9 10 def forward(self, x): 11 ...

グラフを定義

骨格の情報から、どの関節同士が接続しているかを表す行列に変換

イメージ:

  • 自分自身は自分に接続している.
  • 01 は接続している
  • 23 は接続している
  • 無指向のため反対も接続している
    $$
    \begin{bmatrix}
    1, 1, 0, 0 \\
    1, 1, 0, 1 \\
    0, 0, 1, 0 \\
    0, 1, 0, 1
    \end{bmatrix}
    $$
1# 隣接行列を作成 2class Graph: 3 def __init__(self, hop_size, bvhp): 4 skeleton = bvhp.get_skeleton() 5 node_num = len(skeleton) 6 self.edge = self.__get_edge(bvhp) 7 8 # hop数分離れた関節を取得 9 hop_dis = self.__get_hop_distance(self.node_num, self.edge, hop_size) 10 11 # 隣接行列を作成 12 self.A = self.__get_adjacency_mat(hop_dis, hop_size) 13 14 ...

STGC_block

空間と時間で畳み込んでくれるやつ

1class STGC_block(nn.Module): 2 def __init__( 3 self, in_channels, out_channels, stride, t_kernel_size, A_size, dropout=0.5 4 ): 5 super().__init__() 6 # 空間グラフの畳み込み 7 self.sgc = SpatialGraphConvolution( 8 in_channels=in_channels, out_channels=out_channels, s_kernel_size=A_size[0] 9 ) 10 11 # 重要なエッジを学習してくれるパラメータ 12 self.M = nn.Parameter(torch.ones(A_size)) 13 14 # 時間畳み込み 15 self.tgc = nn.Sequential( 16 nn.BatchNorm2d(out_channels), 17 nn.ReLU(), 18 nn.Dropout(dropout), 19 nn.Conv2d( 20 out_channels, 21 out_channels, 22 (t_kernel_size, 1), 23 (stride, 1), 24 ((t_kernel_size - 1) // 2, 0), 25 ), 26 nn.BatchNorm2d(out_channels), 27 nn.ReLU(), 28 ) 29 30 def forward(self, x, A): 31 # 実際に畳み込む 32 x = self.tgc(self.sgc(x, A * self.M)) 33 return x

モデルを定義(再々)

平均値プーリング: ウィンドウサイズを指定し、集約することで特徴量を出す
多次元版移動平均フィルター(意訳)
https://cvml-expertguide.net/terms/dl/layers/pooling-layer/global-average-pooling/

全結合層: NNで全てのノードを結合する層
最終的にどのラベルに属するかを示す確率を表す
https://zero2one.jp/ai-word/fully-connected-layer

1# モデルを定義 2class ST_GCN(nn.Module): 3 def __init__(self, num_classes, in_channels, t_kernel_size, hop_size): 4 ... 5 6 # 空間・時間で畳み込むやつ. 何度もやる理由はわからない 7 self.stgc1 = STGC_block(in_channels, 32, 1, t_kernel_size, A_size) 8 self.stgc2 = STGC_block(32, 32, 1, t_kernel_size, A_size) 9 self.stgc3 = STGC_block(32, 32, 1, t_kernel_size, A_size) 10 self.stgc4 = STGC_block(32, 64, 2, t_kernel_size, A_size) 11 self.stgc5 = STGC_block(64, 64, 1, t_kernel_size, A_size) 12 self.stgc6 = STGC_block(64, 64, 1, t_kernel_size, A_size) 13 ... 14 15 def forward(self, x): 16 # データの次元をリシェイプしつつ, バッチ正規化を行い, 元の次元に戻す 17 N, C, T, V = x.size() # batch, channel, frame, node 18 x = x.permute(0, 3, 1, 2).contiguous().view(N, V * C, T) 19 x = self.bn(x) 20 x = x.view(N, V, C, T).permute(0, 2, 3, 1).contiguous() 21 22 # 特徴を深くまで学習している 23 x = self.stgc1(x, self.A) 24 x = self.stgc2(x, self.A) 25 x = self.stgc3(x, self.A) 26 x = self.stgc4(x, self.A) 27 x = self.stgc5(x, self.A) 28 x = self.stgc6(x, self.A) 29 30 # 予測 31 ## 平均プーリング 32 x = F.avg_pool2d(x, x.size()[2:]) 33 x = x.view(N, -1, 1, 1) 34 35 ## 全結合層の適用 36 x = self.fc(x) 37 x = x.view(x.size(0), -1) 38 39 return x

モデルを作成(再)

1model = ST_GCN( 2 num_classes=10, 3 in_channels=3, 4 t_kernel_size=9, 5 hop_size=2, 6 bvhp=bvhp, # Graph をBVHファイルから生成するように変更した 7)

オプティマイザ

別名: 最適化アルゴリズム
ゴール(損失0)になっれるように効率的にたどり着けるようにするやつ
https://qiita.com/omiita/items/1735c1d048fe5f611f80

いろんな手法があるが、特に SGD を使用している

1# 確率的勾配降下法を使う 2optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)

誤差関数

モデルの精度を測るやつ
どれだけの損失があるかを計算

1criterion = torch.nn.CrossEntropyLoss()

データセットの用意

訓練用とテスト用に分けて読み込んでいる

1data_loader = dict() 2data_loader["train"] = torch.utils.data.DataLoader( 3 # 誰? 4 dataset=Feeder(data_path="data/train_data.npy", label_path="data/train_label.npy"), 5 batch_size=BATCH_SIZE, 6 shuffle=True, 7) 8data_loader["test"] = torch.utils.data.DataLoader( 9 dataset=Feeder(data_path="data/test_data.npy", label_path="data/test_label.npy"), 10 batch_size=BATCH_SIZE, 11 shuffle=False, 12)

データセットの定義

データセット用のクラスがある torch.utils.data
※Pytorch はちゃんと公式ドキュメント読んだほうが良さそう

データセットには2種類ある

これは map-style datasets

1class Feeder(torch.utils.data.Dataset): 2 def __init__(self, data_path, label_path): 3 super().__init__() 4 5 # 良い感じにデータをロードする 6 self.motion_df = self.__load_bvh(bvh_path) 7 self.label_df = self.__load_label(label_path) 8 9 # データの数を返す 10 def __len__(self): 11 return len(self.label) 12 13 # index番目のデータを返す 14 def __getitem__(self, index): 15 data = np.array(self.data[index]) 16 label = self.label[index] 17 18 return data, label

モデルのモードを設定

モデルを学習モードにする

1model.train()

eval を使うと訓練モードになる

1model.eval()

学習させる

逆伝播: 出力層から入力層に向かって誤差を伝播させてパラメータの勾配を計算する

1# エポックをループ 2for epoch in range(1, NUM_EPOCH + 1): 3 correct = 0 # 正しく分類されたサンプル数をカウント 4 sum_loss = 0 # 累積損失を計算 5 6 # ミニバッチ(データとラベルの組み合わせ) でループ 7 for batch_idx, (data, label) in enumerate(data_loader["train"]): 8 9 # GPU に転送する ※MacBookは無理 10 data = data.cuda() 11 label = label.cuda() 12 13 # 予測を行う 14 output = model(data) 15 16 # 損失を計算 17 loss = criterion(output, label) 18 19 # オプティマイザ の勾配を初期化 20 optimizer.zero_grad() 21 22 # 逆伝播で損失を計算 23 loss.backward() 24 25 # モデルのパラメータを更新 26 optimizer.step() 27 28 # 累積損失に加算 29 sum_loss += loss.item() 30 31 # 予測結果から正解数を出す 32 _, predict = torch.max(output.data, 1) 33 correct += (predict == label).sum().item() 34 35 print( 36 "# Epoch: {} | Loss: {:.4f} | Accuracy: {:.4f}".format( 37 epoch, 38 sum_loss / len(data_loader["train"].dataset), 39 (100.0 * correct / len(data_loader["train"].dataset)), 40 ) 41 )

メモ

もしかして二次元に落とし込まなくてもできる?

次回TODO

  • 簡単な動作のデータを取る
    • (お料理はデータ取るの大変)
  • ST-GCNする
    • このままではエラーが頻発するはずなので、修正し動くようにする
    • 精度は二の次

進路関係

気が向いたので、ゆめみパスポートに応募してみた
ゆめみのコーディング試験を受けれるやつ. 腕試しとしてやってみる
https://hrmos.co/pages/yumemi/jobs/101000000010

余談

Matsuriba vol.4 に参加した

Ateam Officeのトイレからの眺めが良かった

IMG_6804.JPG (1.1 MB)