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 ...
グラフを定義
骨格の情報から、どの関節同士が接続しているかを表す行列に変換
イメージ:
- 自分自身は自分に接続している.
0と1は接続している2と3は接続している- 無指向のため反対も接続している
$$
\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 に参加した
Matsuriba vol.4 スタートしました!!🔥
— MatsuribaTech🏮 (@MatsuribaTech) May 17, 2024
70名以上のご参加を頂いております!
#祭り場 pic.twitter.com/md6SSZYunp
Ateam Officeのトイレからの眺めが良かった
