梶研 [ST-GCN序章]
2024年05月21日
ST-GCN序章
出席率
- 3年セミナー:??%
スケジュール
短期的な予定
- [ ] mocopi と お料理センシング
- [x] シーンとランドマークを決める(~2月上旬)
- [x] SVM で動作判別する
- [x] 機械学習を深める
- [ ] お料理センシング
- [x] お料理でどんな動作があるかを知る
- [x] レシピを決める
- [x] 関節を3次元座標に変換する
- [ ] 関節を2次元座標に変換する
- [x] ST-GCN を大体理解
- [ ] ST-GCN で動作推定
- [ ] 伊達巻きで動作推定する
- [ ] レシピ(手順書)を元に動作推定を補う
- [ ] 論文書く
- [ ] 発表
- [ ] BookWorm
- [x] Pasori と デスクトップアプリを接続する(技術検証)
- [x] nfc読み込み機能 & 画面を作る
- [ ] API と連携させる
- [x] 管理者画面を作る
長期的な予定
- 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用に置き換えて行く
ランダム値を固定する
何度実行しても同じ結果になるらしい
ただし、処理が少し遅くなるかも
seed = 123
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.backends.cudnn.deterministic = True
torch.use_deterministic_algorithms = True エポックサイズとバッチサイズ
エポックサイズ: 訓練データを学習させる回数
バッチサイズ: 1回の学習に使うデータ数
NUM_EPOCH = 100
BATCH_SIZE = 64 モデルを作成
何者?
model = ST_GCN(
num_classes=10,
in_channels=3,
t_kernel_size=9,
hop_size=2,
) モデルを定義
# モデルを定義
class ST_GCN(nn.Module):
def __init__(self, num_classes, in_channels, t_kernel_size, hop_size):
super().__init__()
# 誰?
graph = Graph(hop_size)
...
def forward(self, x):
... グラフを定義
骨格の情報から、どの関節同士が接続しているかを表す行列に変換
イメージ:
- 自分自身は自分に接続している.
0と1は接続している2と3は接続している- 無指向のため反対も接続している
$$
\begin{bmatrix}
1, 1, 0, 0 \\\\
1, 1, 0, 1 \\\\
0, 0, 1, 0 \\\\
0, 1, 0, 1
\end{bmatrix}
$$
# 隣接行列を作成
class Graph:
def __init__(self, hop_size, bvhp):
skeleton = bvhp.get_skeleton()
node_num = len(skeleton)
self.edge = self.__get_edge(bvhp)
# hop数分離れた関節を取得
hop_dis = self.__get_hop_distance(self.node_num, self.edge, hop_size)
# 隣接行列を作成
self.A = self.__get_adjacency_mat(hop_dis, hop_size)
... STGC_block
空間と時間で畳み込んでくれるやつ
class STGC_block(nn.Module):
def __init__(
self, in_channels, out_channels, stride, t_kernel_size, A_size, dropout=0.5
):
super().__init__()
# 空間グラフの畳み込み
self.sgc = SpatialGraphConvolution(
in_channels=in_channels, out_channels=out_channels, s_kernel_size=A_size[0]
)
# 重要なエッジを学習してくれるパラメータ
self.M = nn.Parameter(torch.ones(A_size))
# 時間畳み込み
self.tgc = nn.Sequential(
nn.BatchNorm2d(out_channels),
nn.ReLU(),
nn.Dropout(dropout),
nn.Conv2d(
out_channels,
out_channels,
(t_kernel_size, 1),
(stride, 1),
((t_kernel_size - 1) // 2, 0),
),
nn.BatchNorm2d(out_channels),
nn.ReLU(),
)
def forward(self, x, A):
# 実際に畳み込む
x = self.tgc(self.sgc(x, A * self.M))
return x モデルを定義(再々)
平均値プーリング: ウィンドウサイズを指定し、集約することで特徴量を出す
多次元版移動平均フィルター(意訳)
https://cvml-expertguide.net/terms/dl/layers/pooling-layer/global-average-pooling/
全結合層: NNで全てのノードを結合する層
最終的にどのラベルに属するかを示す確率を表す
https://zero2one.jp/ai-word/fully-connected-layer
# モデルを定義
class ST_GCN(nn.Module):
def __init__(self, num_classes, in_channels, t_kernel_size, hop_size):
...
# 空間・時間で畳み込むやつ. 何度もやる理由はわからない
self.stgc1 = STGC_block(in_channels, 32, 1, t_kernel_size, A_size)
self.stgc2 = STGC_block(32, 32, 1, t_kernel_size, A_size)
self.stgc3 = STGC_block(32, 32, 1, t_kernel_size, A_size)
self.stgc4 = STGC_block(32, 64, 2, t_kernel_size, A_size)
self.stgc5 = STGC_block(64, 64, 1, t_kernel_size, A_size)
self.stgc6 = STGC_block(64, 64, 1, t_kernel_size, A_size)
...
def forward(self, x):
# データの次元をリシェイプしつつ, バッチ正規化を行い, 元の次元に戻す
N, C, T, V = x.size() # batch, channel, frame, node
x = x.permute(0, 3, 1, 2).contiguous().view(N, V * C, T)
x = self.bn(x)
x = x.view(N, V, C, T).permute(0, 2, 3, 1).contiguous()
# 特徴を深くまで学習している
x = self.stgc1(x, self.A)
x = self.stgc2(x, self.A)
x = self.stgc3(x, self.A)
x = self.stgc4(x, self.A)
x = self.stgc5(x, self.A)
x = self.stgc6(x, self.A)
# 予測
## 平均プーリング
x = F.avg_pool2d(x, x.size()[2:])
x = x.view(N, -1, 1, 1)
## 全結合層の適用
x = self.fc(x)
x = x.view(x.size(0), -1)
return x モデルを作成(再)
model = ST_GCN(
num_classes=10,
in_channels=3,
t_kernel_size=9,
hop_size=2,
bvhp=bvhp, # Graph をBVHファイルから生成するように変更した
) オプティマイザ
別名: 最適化アルゴリズム
ゴール(損失0)になっれるように効率的にたどり着けるようにするやつ
https://qiita.com/omiita/items/1735c1d048fe5f611f80
いろんな手法があるが、特に SGD を使用している
# 確率的勾配降下法を使う
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9) 誤差関数
モデルの精度を測るやつ
どれだけの損失があるかを計算
criterion = torch.nn.CrossEntropyLoss() データセットの用意
訓練用とテスト用に分けて読み込んでいる
data_loader = dict()
data_loader["train"] = torch.utils.data.DataLoader(
# 誰?
dataset=Feeder(data_path="data/train_data.npy", label_path="data/train_label.npy"),
batch_size=BATCH_SIZE,
shuffle=True,
)
data_loader["test"] = torch.utils.data.DataLoader(
dataset=Feeder(data_path="data/test_data.npy", label_path="data/test_label.npy"),
batch_size=BATCH_SIZE,
shuffle=False,
) データセットの定義
データセット用のクラスがある torch.utils.data
※Pytorch はちゃんと公式ドキュメント読んだほうが良さそう
データセットには2種類ある
これは map-style datasets
class Feeder(torch.utils.data.Dataset):
def __init__(self, data_path, label_path):
super().__init__()
# 良い感じにデータをロードする
self.motion_df = self.__load_bvh(bvh_path)
self.label_df = self.__load_label(label_path)
# データの数を返す
def __len__(self):
return len(self.label)
# index番目のデータを返す
def __getitem__(self, index):
data = np.array(self.data[index])
label = self.label[index]
return data, label モデルのモードを設定
モデルを学習モードにする
model.train() eval を使うと訓練モードになる
model.eval() 学習させる
逆伝播: 出力層から入力層に向かって誤差を伝播させてパラメータの勾配を計算する
# エポックをループ
for epoch in range(1, NUM_EPOCH + 1):
correct = 0 # 正しく分類されたサンプル数をカウント
sum_loss = 0 # 累積損失を計算
# ミニバッチ(データとラベルの組み合わせ) でループ
for batch_idx, (data, label) in enumerate(data_loader["train"]):
# GPU に転送する ※MacBookは無理
data = data.cuda()
label = label.cuda()
# 予測を行う
output = model(data)
# 損失を計算
loss = criterion(output, label)
# オプティマイザ の勾配を初期化
optimizer.zero_grad()
# 逆伝播で損失を計算
loss.backward()
# モデルのパラメータを更新
optimizer.step()
# 累積損失に加算
sum_loss += loss.item()
# 予測結果から正解数を出す
_, predict = torch.max(output.data, 1)
correct += (predict == label).sum().item()
print(
"# Epoch: {} | Loss: {:.4f} | Accuracy: {:.4f}".format(
epoch,
sum_loss / len(data_loader["train"].dataset),
(100.0 * correct / len(data_loader["train"].dataset)),
)
) メモ
もしかして二次元に落とし込まなくてもできる?
次回TODO
- 簡単な動作のデータを取る
- (お料理はデータ取るの大変)
- ST-GCNする
- このままではエラーが頻発するはずなので、修正し動くようにする
- 精度は二の次
進路関係
気が向いたので、ゆめみパスポートに応募してみた
ゆめみのコーディング試験を受けれるやつ. 腕試しとしてやってみる
https://hrmos.co/pages/yumemi/jobs/101000000010
余談
Matsuriba vol.4 に参加した
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Matsuriba vol.4 スタートしました!!🔥<br>70名以上のご参加を頂いております!<br> <a href="https://twitter.com/hashtag/%E7%A5%AD%E3%82%8A%E5%A0%B4?src=hash&refsrc=twsrc%5Etfw">#祭り場</a> <a href="https://t.co/md6SSZYunp">pic.twitter.com/md6SSZYunp</a></p>— MatsuribaTech🏮 (@MatsuribaTech) <a href="https://twitter.com/MatsuribaTech/status/1791416048228659673?refsrc=twsrc%5Etfw">May 17, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
Ateam Officeのトイレからの眺めが良かった