# CoachMe 開發紀錄
> contributed by <[`weihsinyeh`](https://github.com/weihsinyeh)>
[原始程式碼](https://github.com/MotionXperts/MotionExpert)
[Concept Difference](https://hackmd.io/I_Egug6sStCAArt4122Mmw)
1. 擁有教練看細節的能力
Understanding detailed body part interrelationships, beyond overall action recognition. (第一句話)
2. 擁有教練分辨對錯的能力
Typical video captioning fails to distinguish skill levels (e.g., beginner vs. expert).
## 模型的問題

1.圈數不足->因為骨架被固定在原地->不知道哪時候落地->圈數看得不準
2.缺點被放大<-目前的模型被設計來挑毛病,訓練模型時教練的指導也全是負面的指導語,因此就算運動員跳的不錯也沒有一個機制去評價是否已經達到目標,而是一昧的吹毛求疵
3. 471706304466387043_0 正確的指導語講在錯誤的時間點 ex "471706263479386249_0 & _1
4. 講的話是要懂一些溜冰才講得出來的。
5. 換句話說
6. -> 錯誤區間錯 第一跳講第二跳的問題。 -> 動作分類對
7. 換句話說的比例 我先分類文字 。
文字 A 類 -> 影片 B C D 類
文字 B 類 -> 影片 B 類
文字 C 類 -> 影片 D 類
video llava. LLaVA consists of a CLIP image encoder, a multimodal projector and a Vicuna text decoder.
文字跟文字的關係。entity relationship. 量化分析。教練習慣指導腳的問題,指導的大部分都是某部位可以回報給做資料的人。 關節的 relationship 可以給做模型的人。教練用語的 distribution 與生成出來的 distribution 的差別。
## 建立資料集
> github repository : `/MotionXperts/HybrIK`
[HybrIK 論文連結](https://arxiv.org/abs/2011.14672) 以 30 frame per second 去預測影片中的骨架。此骨架 format 為 SMPL 格式。且為local coordinate system 以胸腔為起點。因此從中可以得到以 fps : 30 預測的影片長度 $sequence\ length$ 以及每一個 frame 有的 $22 \times 3$ 的關節 (joints) 區域座標 : $x$、$y$、$z$。
### 修改
原先 HybriK 為輸出 $24 \times 3$ 的 24 個關節的$x$、$y$、$z$ 座標,但因為 pre-trained 資料集 HumanML3D 使用的是 $22 \times 3$ 的 22 個關節的$x$、$y$、$z$ 座標。因此我也調整為 22 個並且跟 HumanML3D 一樣的關節對應,少掉的 2 個關節為雙手的末端。
:::info
> [name= weihsinyeh ]
檢查縮放比例。之前有寫,但不確定新版本有沒有納入這個縮放比例的考量。
:::
## 模型架構
> github repository : `/MotionXperts/MotionExpert`
首先輸入資料為 : $3 \times\ sequence\ length \times 22$ 為 joints 的 $x$、$y$、$z$ 其中 $sequence\ length$ 為影片的長度。

bonelink = $[(0, 1), (0, 2), (0, 3), (1, 4), (2,5),(3,6), (4, 7), (5, 8), (6, 9), (7, 10), (8, 11),$$(9, 12), (9, 13), (9, 14), (12, 15), (13, 16), (14, 17), (16, 18), (17, 19), (18, 20), (19, 21)]$
而 bone 的 $x$、$y$、$z$ 則透過將這 joints 相減得到向量。此向量的方向均為從 pelvis outward。
因此從dataset 可以做出 $6 \times\ sequence\ length \times 22$ 為 joints 與 bone 的 $x$、$y$、$z$ 此稱作 channel 。

而 `input_ids` 為 $batch\ size \times 6 (channel) \times\ sequence\ length \times 22$。而下方的 get encoder feature 。
下方為整個模型的 `forward` 函式
```python=1
def forward(self, input_ids, attention_mask, decoder_input_ids=None, labels=None, use_embeds=True,output_attentions=True):
if use_embeds:
batch_size, channel,seq_length, feature_dim = input_ids.shape
input_embeds, attention_node, attention_matrix = self._get_encoder_feature(input_ids)
```
上方的程式碼經過 `_get_encoder_feature` 函式: 而之後給 T5 作為輸入的就是這裡的embedding。
```python
def _get_encoder_feature(self, src):
embedding, attention_node, attention_matrix = self.STAGCN(src)
return embedding, attention_node, attention_matrix
```
而給 T5 作為輸入的就是這裡的 embedding。所以看到第 7 行 `inputs_embeds=input_embeds`
```python=
def forward(self, input_ids, attention_mask, decoder_input_ids=None, labels=None, use_embeds=True,output_attentions=True):
if use_embeds:
batch_size, channel,seq_length, feature_dim = input_ids.shape
input_embeds, attention_node, attention_matrix = self._get_encoder_feature(input_ids)
new_attentention_mask = attention_mask[:,:,::4].clone()
attention_mask = new_attentention_mask[:,0,:]
output = self.t5(inputs_embeds=input_embeds, attention_mask=attention_mask, decoder_input_ids=decoder_input_ids, labels=labels,output_attentions=True)
```
因此整體的模型分為前半部分的 STAGCN 模型與後部分的 T5 模型。接下來細看前半部分的 STAGCN 模型
---
### [Spatial Temporal Attention Graph Convolution Network](https://github.com/machine-perception-robotics-group/SpatialTemporalAttentionGCN)
其中分為三個 branch : (1) Feature Extractor 、(2) Attention Branch 、(3) Perception branch

#### 0. [Spatial Temporal Graph Convolutional Networks](https://github.com/yysijie/st-gcn)
這個模型的架構是做圖卷積其中包含 空間(spatial dimension) 與 時間(temporal dimension)的維度。而卷積的部分我改成 SMPL ,但目前也支援其他模式此為 optional 。但因為 pre-trained 的資料集為 humanML3D 其為sequence 維度下的 feature 維度為 $22 \times 3$。
`/MotionExpert/net/Utils_attention/make_graph.py` `layout == 'SMPL'`。透過這個做出最初始的 Adjacency Matrix $A_0$。在骨骼中相鄰的關節彼此的關係則設定為 $1$。
##### 空間 (Spatial Convolution)
###### **Make Graph**
> 參考 /MotionExpert/net/Utils_attention/make_graph.py
接著我設定做出 Graph 的方式為 `strategy == 'spatial':` 。概念如下。

${\color{Green}{Root\ node}}$ :
${\color{DodgerBlue}{Centripetal\ group}}$ : the neighboring nodes that are closer to the gravity center of the skeleton than the root node
${\color{Gold}{Centrifugal\ group}}$ : otherwise the centrifugal group
這裡我設定 `hop = 3`,意思為在空間中最多會拜訪一個關節點與它距離為 3 的關節點。接下來會有 7 個 Adjacency Matrix $M_{0-6}$ 。
透過 Adjacency matrix 計算相鄰的節點 `hop = 1` 儲存在$A_1$ ,與較近的節點 `hop = 2` 儲存在 $A_1 \times A_1 = A_2$ ,與較遠的節點 `hop = 3` 儲存在 $A_1 \times A_1 \times A_1= A_3$。計算關節間彼此的距離,下段演算法用來形成 group 。如果`hop <= 3` 則這些兩個關節點為 group。
```python
def get_hop_distance(num_node, edge, hop_size=1):
A = np.zeros((num_node, num_node))
for i, j in edge:
A[j, i] = 1
A[i, j] = 1
hop_dis = np.zeros((num_node, num_node)) + np.inf
transfer_mat = [np.linalg.matrix_power(A, d) for d in range(hop_size + 1)]
arrive_mat = (np.stack(transfer_mat) > 0)
for d in range(hop_size, -1, -1):
hop_dis[arrive_mat[d]] = d
return hop_dis
```
接下來為 spatial 策略的演算法: 我設定SMPL 的 `self.center = 0` 也就是 pevis 的關節點,此符合[STGCN 論文](https://arxiv.org/pdf/1801.07455)中稱這個關節點為重心 : "gravity center of the skeleton"。
接下來來尋找每個 group,中有兩個關節點分別為 $joint = i$ 與 $joint = j$,兩者在 hop 的範圍內,則兩者為 group。
* case 1 :
如果距離 pevis 的關節點 $joint = 0$ 的距離相等。那彼此在這個 group 都為 ${\color{Green}{Root\ node}}$,因為他們距離重心相同距離。因此放到 $M_{root}[j,i]$。
* case 2 :
如果 $joint = i$ 較 $joint = j$ 距離 pevis 的關節點 $joint = 0$ 的距離更近。則 $joint = i$ 相對於 $joint = j$ 為 ${\color{DodgerBlue}{Centripetal\ group}}$ 。因此放到 $M_{close}[j,i]$。
* case 3 :
如果 $joint = i$ 較 $joint = j$ 距離 pevis 的關節點 $joint = 0$ 的距離更遠。則 $joint = i$ 相對於 $joint = j$ 為 ${\color{Gold}{Centrifugal\ group}}$ 。因此放到 $M_{far}[j,i]$。
因此可以觀察發現三個關係(1)、$M_{root}[j,i] = 1$ 則 $M_{root}[i,j]$ (2)、$M_{close}[j,i] = 1$ 則 $M_{far}[j,i]$ (3)、$M_{close}[i,j] = 1$ 則 $M_{far}[i,j]$
```python
A = []
for hop in valid_hop:
a_root = np.zeros((self.num_node, self.num_node))
a_close = np.zeros((self.num_node, self.num_node))
a_further = np.zeros((self.num_node, self.num_node))
for i in range(self.num_node):
for j in range(self.num_node):
if self.hop_dis[j, i] == hop:
if self.hop_dis[j, self.center] == self.hop_dis[i, self.center]:
a_root[j, i] = normalize_adjacency[j, i]
elif self.hop_dis[j, self.center] > self.hop_dis[i, self.center]:
a_close[j, i] = normalize_adjacency[j, i]
else:
a_further[j, i] = normalize_adjacency[j, i]
if hop == 0:
A.append(a_root)
else:
A.append(a_root + a_close)
A.append(a_further)
A = np.stack(A)
self.A = A
```
其中 $M_0$ 為 root node 也就是在製作最原先的 Adjacency Matrix layout的矩陣 $A_0$,相鄰的兩節點距離重心距離都相等。接著 $M_{1-6}$ 分別如下 :
$M_{1}$ 為 `hop = 1` 範圍內時,${\color{Green}{Root\ node}}$ + ${\color{DodgerBlue}{Centripetal\ group}}$ (較近關節點)
$M_{2}$ 為 `hop = 1` 範圍內時,${\color{Gold}{Centrifugal\ group}}$ (較遠關節點)
$M_{3}$ 為 `hop = 2` 範圍內時,${\color{Green}{Root\ node}}$ + ${\color{DodgerBlue}{Centripetal\ group}}$ (較近關節點)
$M_{4}$ 為 `hop = 2` 範圍內時,${\color{Gold}{Centrifugal\ group}}$ (較遠關節點)
$M_{5}$ 為 `hop = 3` 範圍內時,${\color{Green}{Root\ node}}$ + ${\color{DodgerBlue}{Centripetal\ group}}$ (近關節點)
$M_{6}$ 為 `hop = 3` 範圍內時,${\color{Gold}{Centrifugal\ group}}$ (較遠關節點)
透過這些對輸入 $x$、$y$、$z$ 此 3 個 channel 。卷積後會進而產生 7 個 channel。
###### **Convolution**
> 參考 /MotionExpert/net/Utils_attention/graph_convolution.py
```python
def __init__(self, in_channels, out_channels, s_kernel_size, bias):
super().__init__()
self.s_kernel_size = s_kernel_size
self.conv = nn.Conv2d(in_channels=in_channels,
out_channels=out_channels * s_kernel_size,
kernel_size=(1, 1),
padding=(0, 0),
stride=(1, 1),
dilation=(1, 1),
bias=bias)
def forward(self, x, A, att_A):
x = self.conv(x)
n, kc, t, v = x.size()
x = x.view(n, self.s_kernel_size, kc//self.s_kernel_size, t, v)
x = torch.einsum('nkctv,kvw->nctw', (x, A))
return x.contiguous()
```
Randomly-Initialized Neural Network $(7\times 32) \times 3$ convolution 裡的權重。再透過back propagation 更新權重的值。
> Training from scratch: randomly initialize your neural network and train it (in a supervised manner) on your target task.
> Transfer learning: pre-train the network on a separate dataset, then fine-tune it (i.e., train it more) on the target task.
$X$ : $batch\ size \times 3(channel) \times sequence\ length \times 22$

`self.conv(x)` : $batch\ size \times (32\times 7)(channel) \times sequence\ length \times 22$
`x = x.view(n, self.s_kernel_size, kc//self.s_kernel_size, t, v)` :
$batch\ size \times 7 \times 32 \times sequence\ length \times 22$

`torch.einsum('nkctv,kvw->nctw',(a,b))`
`a: tensor(n,k,c,t,v)` 也就是 ($batch\ size$,${ \color{red}7}$,$32$,$sequence\ length$,${\color{red}{22}}$)
`b: tensor(k,v,w)` 也就是 ($7$,$22$,$22$)
輸出 : `tensor(n,c,t,w)` 也就是 ($batch\ size$,$32$,$sequence\ length$,$22$)


:::info
> [name= weihsinyeh ]
TODO : Adjacency Matrix 讓他可以被learned
:::
所以經過圖卷積(用 [Einstein summation](https://rogerspy.github.io/2021/09/12/einsum-mhsa/) 達到卷積中結合乘法與加法運算的效果)
```
for n in range(N): # batch size
for c in range(C): # channel
for t in range(T): # sequence length
for w in range(W): # 22 個關節點
# Perform the einsum operation for each index
for k in range(K): # 7 個空間Matrix
for v in range(V): # 22 個關節點
result[n, c, t, w] += x[n, k, c, t, v] * A[k, v, w]
```
輸入 : $batch\ size \times 3(channel) \times sequence\ length \times 22$
輸出 : $batch\ size\times 32(channel) \times sequence\ length \times 22$
GCN 的用意是在改變 channel 的維度,且卷積將 graph layout 考慮進去。
##### 時間 (Temporal Convolution)
```python
self.tgc = nn.Sequential(
nn.BatchNorm2d(out_channels),
nn.ReLU(),
nn.Conv2d(out_channels,
out_channels,
(t_kernel_size, 1), #(temporal kernel size , spatial kernel size)
(stride, 1), #(temporal stride = 1, spatial stride)
((t_kernel_size - 1) // 2, 0), #(temporal padding kernal,spatial padding)
bias=bias),
nn.BatchNorm2d(out_channels),
nn.Dropout(dropout),
nn.ReLU())
```
> 參數設定 : dropout=0.5, t_kernel_size=9 。且用上面 空間(Spatial Convolution) 的情況, temporal stride = 1 ,out_channels = 32 (因為 stride = 1)。
因為 `temporal kernel size = 9` ,所以
`padding kernel` $=(temporal\ kernel\ size - 1) // 2$

但 Feature Extractor 中第一個 `block dropout=0` , `residual=False` 。
當做完空間卷積後做了時間卷積,由於 `residual=True` ,所以會做 ResNet 的工作。
```python
sgc_out = self.sgc(x, A * self.M, att_A)
x = self.tgc(sgc_out) + self.residual(x)
return x
```

---
#### 1. Feature Extractor

首先 Feature extractor 會處理原先的 $6 \times\ sequence\ length \times 22$ 。接下來做卷積就是用來自 **0. STGCN**。以下為 joint 與 bone channel 的改變。
$3 \to 32$ `(stride = 1)`
$32 \to 32$ `(stride = 1)`
$32 \to 32$ `(stride = 1)`
$32 \to 64$ `(stride = 2)`
$64 \to 64$ `(stride = 1)`
接下來將 joint 與 bone 的 channel 連結起來。`feature = torch.cat([x1, x2], dim=1)`
因為有一次 `temportal stride = 2`, 所以維度變為$(sequence\ length / 2)$
再來就得到黃色 embedding  : $batch\ size\times 128(channel) \times (sequence\ length / 2) \times 22$。
---
#### 2. Attention Branch

從 Feature Extractor 得到 $batch\ size\times 128(channel) \times (sequence\ length / 2) \times 22$ 。接下來做卷積就是用來自 **0. STGCN** 。以下為 channel 的改變。
$128 \to 128$ `(stride = 1)`
$128 \to 128$ `(stride = 1)`
$128 \to 256$ `(stride = 2)`
$256 \to 256$ `(stride = 1)`
$256 \to 256$ `(stride = 1)`
因為有一次 `temportal stride = 2` 所以$(sequence\ length / 2)$
再來就得到橘色 embedding  : $batch\ size\times 256(channel) \times (sequence\ length / 4) \times 22$。
### Attention
```python
self.att_bn0 = nn.BatchNorm2d(config[-1][1]) # 256
self.att_conv = nn.Conv2d(config[-1][1], # 256
num_class,
kernel_size=1,
padding=0,
stride=1,
bias=False)
...
x_att = self.att_bn0(x_last)
x_att = self.att_conv(x_att) # inp
```
> att_conv 的參數 : input channel = 256, output channel = 1200 (我自己定義的)
因此得到 embedding : $batch\ size\times 1200(channel) \times (sequence\ length / 4) \times 22$
##### Attention Node
```python=
self.att_node_conv = nn.Conv2d(num_class,
1,
kernel_size=1, padding=0,
stride=1,
bias=False)
self.att_node_bn = nn.BatchNorm2d(1)
self.sigmoid = nn.Sigmoid()
# Attention node
x_node = self.att_node_conv(x_att)
x_node = self.att_node_bn(x_node)
x_node = F.interpolate(x_node, size=(T, V))
att_node = self.sigmoid(x_node)
```
> att_conv 的參數 : input channel = 1200 (我自己定義的), output channel = 1
>
因此原先輸入的 embedding : $batch\ size\times 1200(channel) \times (sequence\ length / 4) \times 22$
第 10 行的時候 embedding 卷積為 : $batch\ size\times 1(channel) \times (sequence\ length / 4) \times 22$
第 11 行 : 經 [Pytorch 預設 nearest 線性差值](https://pytorch.org/docs/stable/_modules/torch/nn/functional.html#interpolate),將$(sequence\ length / 4)$ 兩相鄰點線性插值,則相當於放大二倍 : $(sequence\ length / 2)$ 。此為 `attention node`。
$batch\ size\times 1(channel) \times (sequence\ length / 2 )\times 22$ 得到 : 
##### Attention Matrix
```python=
self.att_A_conv = nn.Conv2d(num_class, num_att_A * A_size[2], kernel_size=1, padding=0, stride=1, bias=False)
self.att_A_bn = nn.BatchNorm2d(num_att_A * A_size[2])
self.tanh = nn.Tanh()
self.relu = nn.ReLU()
x_A = F.avg_pool2d(x_att, (x_att.size()[2], 1))
x_A = self.att_A_conv(x_A)
x_A = self.att_A_bn(x_A)
x_A = x_A.view(N, self.num_att_A, V, V)
x_A = self.tanh(x_A)
att_A = self.relu(x_A)
```
> att_conv 的參數 : input channel = 1200 (我自己定義的), output channel = 4(# of attention head) x 22
因此原先輸入的 embedding : $batch\ size\times 1200(channel) \times (sequence\ length / 4 ) \times 22$
> **參考 [torch.nn.functional.avg_pool2d](torch.nn.functional.avg_pool2d)**
> input – input tensor (minibatch, in_channels, 𝑖𝐻, 𝑖𝑊)
kernel_size – size of the pooling region. Can be a single number or a tuple (kH, kW)
這裡用的 `kernel size` 為 $((sequence\ length / 4 ),1)$
在第5行的時候 embedding : $batch\ size\times 1200(channel) \times 1 \times 22$
第 6 行的時候 embedding 卷積,此為 `attention matrix`。
第 8 行做 reshape 為 : [參考 STAGCN attention_branch.py](https://github.com/machine-perception-robotics-group/SpatialTemporalAttentionGCN/blob/master/Tools/Model/Utils/attention_branch.py#L46C48-L46C58)
$batch\ size\times 88(channel) \times 1 \times 22 \to batch\ size\times 4(channel) \times 22 \times 22$ 得到 `Attention Matrix` : 
:::info
> [name= weihsinyeh ]
研究別人的 GCN 的操作是如何的。
:::
---
#### 3. Perception Branch

從 Feature Extractor 得到的黃色 embedding : $batch\ size\times 128(channel) \times (sequence\ length /2) \times 22$ 。
從 Attention Branch 得到 Attention Matrix 與 Attention Node。
然而要輸入到 ST${\color{red}{A}}$GCN 的黃色 embedding `feature` $batch\ size\times 128(channel) \times (sequence\ length / 2 ) \times 22$,會先將 Attention Branch 產生的 `Attention Node(att_node)` : 
$batch\ size\times 1(channel) \times (sequence\ length /2)\times 22$ 做運算得到 `att_x` : $batch\ size\times 128(channel) \times (sequence\ length /2)\times 22$
```python
# Attention Mechanism
att_x = feature * att_node
```
接下來就是用新的 `att_x` 作為新的輸入到 Perception branch 的 embedding : $batch\ size\times 128(channel) \times (sequence\ length /2)\times 22$
而之所以叫 ST${\color{red}{A}}$GCN 是因為原先在 **0. STGCN** 只用了 7 個 channel,而現在這 7 個 Adjacency Matrix $A_{0-6}$ 會加上新的 4 個 `Attention Matrix` 。成為有 11 個 Matrix 。
這裡再補充說明 11 個 Matrix 的意義。前 7 個 Adjacency Matrix $M_{0-6}$ 為在 `hop = 3` 之內**基於骨架**所形成的**空間關係(hop <=3)**,而他們的關係權重 (weight) 也就是 Adjacency Matrix 中的數值都為 1 。因此在 STGCN 中關節點的關係都一樣重要。所有影片每次 STGCN 卷積 (先 $*$ 再 $+$ )的時候,$M_{0-6}$ 都為一樣的,因為都是基於 SMPL 的骨頭架構建立的。
而後 4 個 Adjacency Matrix $M_{7-10}$ 是透過 Feature Extractor 與 Attention Branch 分別都為 ($5 \times STGCN\ block$) 產生得來。
與 $M_{0-6}$ 不同的意義有三個 :
1. $M_{7-10}$ 關節的關係並不基於**骨架的空間關係(hop <= 3)**。他能夠找到原先不在基於骨架的空間佈局中,某些節點是**有潛在的關係(可能大於 hop > 3)**。
2. 此外也 $M_{7-10}$ 的值是介於 $0 \sim 1$ ,代表有些關係重要的程度。
3. 且 $M_{7-10}$ 會隨影片不同而有不同。
```python
def forward(self, x, A, att_A):
x = self.conv(x)
n, kc, t, v = x.size() # kc = (11*128)
x = x.view(n, self.s_kernel_size, kc//self.s_kernel_size, t, v) # (self.s_kernel_size = 7+4), (128)
x1 = x[:, :self.s_kernel_size-self.num_att_A, :, :, :] # 7
x2 = x[:, -self.num_att_A:, :, :, :] # 4
x1 = torch.einsum('nkctv,kvw->nctw', (x1, A))
x2 = torch.einsum('nkctv,nkvw->nctw', (x2, att_A))
x_sum = x1 + x2
```
從這裡可以知道 attention matrix 最後做 einsum (乘完再等權重相加,所以 einsum 並沒有表示每個 attention matrix 到底最後貢獻多少給最後的 embedding。因此能夠分配貢獻多少的要從原先的embedding 來計算得出。
:::info
> [name= weihsinyeh ]
>
跟前面attention matrix 結合的想法:
* 將前面哪個身體部位的 attention matrix 也就是上面 4 個 attention matrix 產生出來的`PA_embedding` 用 gradient back propagation 回去。看是哪個 attention matrix 的貢獻較大。
* 此作法的缺點是無法跟文字 token 建立關係,只能跟一整句話建立關係。
:::
接下來做卷積就是用來自 **0. STGCN** 。以下為 channel 的改變跟 Attention Branch 一樣。
$128 \to 128$ `(stride = 1)`
$128 \to 128$ `(stride = 1)`
$128 \to 256$ `(stride = 2)`
$256 \to 256$ `(stride = 1)`
$256 \to 256$ `(stride = 1)`
因為有一次 `temportal stride = 2` 所以$(sequence\ length / 2)$
因此得到紫色 embedding  : $batch\ size\times 256(channel) \times (sequence\ length / 4) \times 22$。
---
最後將
`attention_last` : $batch\ size\times 256(channel) \times (sequence\ length /4)\times 22$ 
`perception_last` $batch\ size\times 256(channel) \times (sequence\ length /4)\times 22$ 
先交換順序
`attention_last` : $batch\ size\times (sequence\ length /4) \times 22 \times 256(channel)$
`perception_last` $batch\ size\times (sequence\ length /4) \times 22 \times 256(channel)$
合併後變為 `PA_embedding` : $batch\ size\times (sequence\ length /4) \times 22 \times 512(channel)$ 得到 : 
```python!
perception_last = perception_last.permute(0,2,3,1)
attention_last = attention_last.permute(0,2,3,1)
PA_embedding = torch.cat([perception_last, attention_last], dim=-1)
```
### [T5](https://huggingface.co/google-t5/t5-base)
輸入是 `PA_embedding` : $batch\ size\times (sequence\ length /4) \times 22 \times 512(channel)$ 給 T5 前又將feature dimension 降為到 T5-base 的 input embedding 長度。
sequence length 用影片每個 frame ,可以讓原先有先後關係的序列在 T5 可以做 positional encoding。像在 ViT 模型中也是將影片的 frame 用在 sequence length 的維度上。此方式可應用在讓 T5 的 attention 視覺化時間區段有問題。像是說明左手有問題會顯示運動員伸左手的時間區段。
然而由於 sequence length 太大難以視覺化,因此將 sequence length影片長度 跟 22關節點 交換。 而 22 關節點間也有關係 在 T5 positional encoding。
:::info
> [name= weihsinyeh ]
在輸入 embedding 給 T5 前,先將 $22$ 跟 $sequence\ lenght$ 交換。這樣 T5 做 token 的時候不是以時間為單位。而是以關節點作為單位,缺點是導致 : T5 的輸入 (22個token) 不含前後關係,這樣沒有善用語言模型 positional encoding 的特性。
改成22 x times的架構
:::
[commit 066710](https://github.com/MotionXperts/MotionExpert/commit/066710bbd94212c1d37892cec212497d1c9d25aa) attention matrix 可以比以前的方式多跑出一個,也就是可以一個 attention matrix 可以 mapping 到 Openpose 成功。

```python
self.embedding = nn.Sequential(
# (22 * 512 = 11264) -> (22 * 256 = 5632)
nn.Linear(11264,5632),
nn.ReLU(),
# (22 * 256 = 5632) -> (768)
nn.Linear(5632,768)).to(PA_embedding.get_device())
# batch_size, (sequence length /4), feature_dim (11264 -> 768)
output_embedding = self.embedding(PA_embedding.view(-1,11264)).view(N, -1, 768)
return output_embedding, att_node, att_A
```
將 `output_embedding` 輸入給 T5 (下方程式碼第七行)。並用 `att_node` 與 `att_A` 做後面的 Evaluation。(下面為整個模型的 `forward` 函式)。
```python=1
def forward(self, input_ids, attention_mask, decoder_input_ids=None, labels=None, use_embeds=True,output_attentions=True):
if use_embeds:
batch_size, channel,seq_length, feature_dim = input_ids.shape
input_embeds, attention_node, attention_matrix = self._get_encoder_feature(input_ids)
new_attentention_mask = attention_mask[:,:,::4].clone()
attention_mask = new_attentention_mask[:,0,:]
output = self.t5(inputs_embeds=input_embeds, attention_mask=attention_mask, decoder_input_ids=decoder_input_ids, labels=labels,output_attentions=True)
```
#### T5 的模型架構

[圖片來源](https://medium.com/analytics-vidhya/t5-a-detailed-explanation-a0ac9bc53e51)
#### T5 visualize attention
參考 (1)[bertviz github](https://github.com/jessevig/bertviz) (2) [bertviz 論文](https://aclanthology.org/P19-3007.pdf) (3) [bertviz 網頁](https://towardsdatascience.com/deconstructing-bert-part-2-visualizing-the-inner-workings-of-attention-60a16d86b5c1)
```python=
odict_keys(['loss', 'logits', 'past_key_values', 'decoder_attentions', 'cross_attentions', 'encoder_last_hidden_state', 'encoder_attentions'])
```
`model_view()` 需要 decoder_attention 與 encoder_attention 與 cross_attention。查詢文件 [Hugging face 's Document : text_generation](https://huggingface.co/docs/transformers/main_classes/text_generation
) 後發現 `generate` 的函式不只要 `output_attentions=True`也要 `return_dict_in_generate=return_dict_in_generate` 。然而當我可以拿到視覺化需要有的 encoder , decoder, cross attention,卻無法直接用到 `model_view()` 或是 `head_view()` 的函式中,會有 tuple 與 tensor 的型態轉換錯誤。這裡參考 [Error when trying to visualize attention in T5 model 的討論](https://discuss.huggingface.co/t/error-when-trying-to-visualize-attention-in-t5-model/35350/2?fbclid=IwZXh0bgNhZW0CMTAAAR2yhEnldQ9f-2bNl7HEnwiiKN2iTuekNKgwqi05s6VmlOCYZbD_WUC4Ca0_aem_AQkFeGkQTeBS7y6oQ37F18-ympxFr3HOg4k_CQKeP5v-6U-W64N3COVsHqP2eYiBpRI3cdkdVEBeEKbRDEW3alEb) 後解決了。[commit 066710](https://github.com/MotionXperts/MotionExpert/commit/066710bbd94212c1d37892cec212497d1c9d25aa)
視覺化後的 attention 意義可以參考 [blog : illustrated-transformer](https://jalammar.github.io/illustrated-transformer/)。但實驗結果發現 cross attention 全連接有點怪,可能跟我直接在 sequence length 與 22 joints 交換過程中,直接做 maxpool 有關係。
:::info
> [name= weihsinyeh ]
1. 研究將 max pool 的操作換成其他的。
2. 改成Args 的方式去讓train的人可以自己選要哪一種 finetune 方式要用 prompt 的方式還是放 token (Chain of Thought)的方式
:::
## Evaluation
> [github : Evaluation](https://github.com/MotionXperts/Evaluation)
參考 [卷積計算的倒傳遞推導](https://chih-sheng-huang821.medium.com/卷積神經網路-convolutional-neural-network-cnn-卷積計算的倒傳遞推導與稀疏矩陣觀點來看卷積計算-e82ac16e510f) 直接用 back propagation 算attention 的 weigth。
---
### Reference
> [卷積參考](https://zhuanlan.zhihu.com/p/77471866)
> [Keeping Your Eye on the Ball: Trajectory Attention in Video Transformers](https://proceedings.neurips.cc/paper_files/paper/2021/file/67f7fb873eaf29526a11a9b7ac33bfac-Paper.pdf)
>[ SwinBERT: End-to-End Transformers with Sparse Attention for Video Captioning.](https://arxiv.org/abs/2111.13196)
#### finetune 設定
| Column 1 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| --------------- | -------- | -------- | --------- | -------- | ----- | ---| --|
| pretrained model| cindy's | cindy's | tommy's | cindy's |tommy's | tommy's| tommy's |
| pooling type |temporal |temporal |temporal |temporal |skeleton| skeleton| skeleton |
| Encoder | STAGCN | STAGCN | STAGCN | STAGCN | STAGCN | STAGCN + RGB | STAGCN + RGB(TCC) |
| alignment | ❌ | ❌ | ✅ | ✅ |❌ | ✅ | ✅|
| decorder random | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌|
| 指導語結果 | 不正常(重複)| 正常 | 正常 | 正常 | 正常 | 正常|正常|
| T5 attention 結果|注意在特定關節|注意在特定關節|看不出來全連接|注意在特定關節| ? |?|?|
#### pretrain 設定
| Column 1 | A (cindy's) | B | C (cindy's) |
| --------------- | -------- | -------- | ----------- |
| pooling type | skeleton | temporal | skeleton |
| Encoder | STAGCN | STAGCN | STAGCN |
| alignment | ✅ | ❌ | ❌ |
| decorder random | ❌ | ✅ | ✅ |
| 描述語結果 | 正常 | 正常 | 正常 |
| T5 attention 結果| 注意在特定關節 | ? | 注意在特定關節 |
#### 實驗一 : 2 vs 4 vs 6: (1) Is alignment better? (2) Is RGB better? (3) Is skeleton pooling better?
2: baseline using temporal pooling
4: alignment with temporal pooling
5: RGB aligned with skeleton pooling
Thinking: What does "alignment" better mean? (1) More fluent? (2) More accurate?
#### 實驗二 : alignment 對 description 的效果? (automatic metric)
A 跟 C 需不需要做 alignment 也可以 description 比較好
#### 實驗三 : pretrain 跟 finetune 是同個 task? (human eval)
3 跟 4 的差別一 : 有沒有做 alignment ,差別二 有使用pretrained's transform module
4 : pretrain 跟 finetune 是同個 task,只是 dataset 不一樣
3 : pretrain 跟 finetune 是不同個 task,因為模型就不一樣(差在有無做alignment)
#### 實驗四 : pooling type 的實驗 (automatic metric)
B 跟 C
#### 實驗五 : 如果只靠generator 設定就可以好有什麼不好嗎?現在的+generator 可以更好嗎?類似這樣 (human eval)
1 跟 4 可以用來說明 alignment 有用 -> theres no reason to eval 1
2 跟 4 教練 human evaluation 來做看是否能辨別 2 的指導語是靠 decoder 生出。 -> should revised to exp1.
#### 實驗六 : (human eval)
6 vs 7: Does the performance of aligning module affect generation? (first do autometic eval)
---
## 7/7 討論
1. intruduction
2. 從兩個 challenge 出發。
challenge 1
challenge 2
challenge 的敘述描述不對。
從頭就要講是要做指導語生成就是要做這兩件事情。
4. introduction 這個問題是要如何做
5. introduction
啥問題,
問題困難 general,
困難別人如何做,但無法解決。
contribution 。
6. how we solve
教練有些講的是對的,但講的區段錯誤。
Related work
Presentation :
一開始就先 flows 。
video 進來 black box 與出去 black box 。
part 功能
module 方法
Human Pose Percetion
22 x vector 。
bones 相減的 vector 。
輸入跟輸出 。 GCN 。
要說抽象 spatial matrix 骨頭間的關係 骨骼間的關係。
input 黃色 feature, output 是橘色 feature 。
spatial matrix 進到 attention branch 它可以從上面的東西黃色有呈現啥。
但我們還想要啥。
是在做什麼,如何做,可以做,可以 highlight 哪個 feature 啥。可以抽出啥。
單張的照片,影片的差異。
Concept 是啥 : 兩個 frame 特徵
Concept Difference : 兩個 frame 之間動作的差異。
影片的差異如何連接到 Concept Difference Understanding :
理解啥 :
1. alignment
2. 量化之間的差距 。 Difference 大做的不好離 standard 越遠。
其中幾個分支。
Results 要 show 啥
ref 對 description 沒有幫助。
description 是啥,左邊的 module 可以表現的比較好。
instruction 為啥。
線 :
手的線不要如此多顏色。
ref 對 insstruction 有幫助。
1. 動作分類錯誤,
2. 不知道實際 input 的視覺內容。
## 模型架構參考
[LLaVA: Large Language and Vision Assistant](https://llava-vl.github.io/)

[Visual Instruction Tuning](https://arxiv.org/pdf/2304.08485)
[ImprovedBaselineswithVisualInstructionTuning](https://arxiv.org/pdf/2310.03744)
[LORA](https://arxiv.org/pdf/2106.09685)
* [Lora Github](https://github.com/microsoft/LoRA)
[Introducing Parameter-Efficient Fine-Tuning (PEFT)](https://medium.com/@jyotikhetan2/introducing-parameter-efficient-fine-tuning-peft-e1188943d7fc)
* [huggingface/peft](https://github.com/huggingface/peft)
```python
interpolated_tensor = F.interpolate(
stagcn_embedding.permute(0, 2, 3, 1), # Change to (Batch, Time, 22, 512)
size=(standard_embedding.shape[3], standard_embedding.shape[1]), # Spatial dimensions only
mode="bilinear",
align_corners=True
).permute(0, 3, 1, 2) # Restore to (Batch, Time, 22, 512)
stagcn_embedding = interpolated_tensor
print(stagcn_embedding.shape)
print(standard_embedding.shape)
if (video_name[0] == '471703066060784233_1'):
print("Visualize")
print("stagcn_embedding",stagcn_embedding.shape)
print("standard_embedding",standard_embedding.shape)
embs = [stagcn_embedding[0].clone().cpu(),standard_embedding[0].clone().cpu()]
viz_tSNE(embs,"/home/weihsin/projects/MotionExpert/results/finetune_joint_training/471703066060784233_1.jpg")
```
### Rebuttal 實驗
Dataset 比較表格
| Sport | 標註者 | 運動員 | 拍攝影片視角 | 動作類型
| - | - | - | - | - |
| Skating | 同個標註者 | 同個運動員 | 隨手拍的影片 | 不同動作
| Boxing | 不同標註者 | 不同運動員 | 不同視角 | 不同動作
不同人,指導語的變化 -> 分析每個人錯誤率的變化 V
同個人,不同角度的變化 -> 目標 : 證明視角改變變不影響 V
找出哪個 epoch 花很久。
---
1. 同個人,來自不同標註者的指導語都 concatenate
2. 同個人,來自不同標註者的指導語都分開
(2) 看生出來的指導語是否描述錯誤的問題會更豐富 (diversity)。
(2) 反而變成看起來過於 general (G-eval) 。
(2) 生出的指導語有衝突的問題。
(2) 有傾向哪個標註者。
6 個動作
基本 record teach 再做一次。
prompt 不一樣。
一個影片 -> 一句指導語 -> GPT -> 四句標註者標註的最像。
算最像的那個
一個影片 x 4 -> 對應
一個影片 x 1 -> predict <-> 去找四個最像(cosine similarity)的一個去收斂。
## Boxing dataset :
原本的 alignment model 的程式碼放在 : `/home/c1l1mo/SkateAppModels/SkatingApp/engine/BoxingAlignment/app/model.py`
```python=81
def process_video(self, video_tensor: torch.Tensor) -> np.ndarray:
"""Process a single video tensor and return embeddings"""
with torch.no_grad():
if self._cfg.MODEL.EMBEDDER_TYPE != 'conv':
with torch.cuda.amp.autocast():
emb_feats = self._model(video_tensor.unsqueeze(0).cuda(), video_masks=None)
else:
seq_len = video_tensor.size(0)
steps = torch.arange(0, seq_len, self._cfg.DATA.SAMPLE_ALL_STRIDE)
context_stride = self._cfg.DATA.CONTEXT_STRIDE
steps = steps.view(-1,1) + context_stride*torch.arange(
-(self._cfg.DATA.NUM_CONTEXTS-1), 1
).view(1,-1)
steps = torch.clamp(steps.view(-1), 0, seq_len - 1)
input_video = video_tensor[steps.long()].unsqueeze(0).cuda()
with torch.cuda.amp.autocast():
emb_feats = self._model(input_video, video_masks=None)
return emb_feats[0].cpu().numpy()
def align_videos(self, video1_path: str, video2_path: str,output_dir: str) -> str:
"""Align two videos and return the path to the output video"""
# Create transform for video preprocessing
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
# Read and preprocess videos
def load_video(path):
video, _, _ = read_video(path)
video = video.float() / 255.0
video = video.permute(0, 3, 1, 2) # (T, H, W, C) -> (T, C, H, W)
video = transform(video)
return video
video1 = load_video(video1_path)
video2 = load_video(video2_path)
# Get embeddings
timer.start('process both videos')
embs1 = self.process_video(video1)
embs2 = self.process_video(video2)
process_time = timer.end('process both videos')
# print(f"Processed both videos in {process_time:.2f} seconds")
# Prepare output path
os.makedirs(os.path.join(output_dir,'result'), exist_ok=True)
video_path = os.path.join(output_dir,'result',f'output.mp4')
# Create alignment video
timer.start('create video')
aligned_query, aligned_key, query_indices, key_indices = create_video( # originally, there is no return value
embs1,
video1.permute(0, 2, 3, 1), # (T, H, W, C)
embs2,
video2.permute(0, 2, 3, 1),
video_path,
use_dtw=True,
interval=200
)
create_time = timer.end('create video')
# print(f"Created alignment video in {create_time:.2f} seconds")
return video_path, aligned_query, aligned_key, query_indices, key_indices
```
alignment 的影片轉成很多個幀的程式碼 : https://github.com/pytorch/vision/blob/main/torchvision/io/video.py#L274 。其中第 112 行是用第 13 行 `from torchvision.io import read_video` 是使用原始影片的 fps 。
其中 `https://github.com/MotionXperts/HybrIK/blob/main/scripts/demo_video.py#L39` 是用
```python=33
def get_video_info(in_file):
stream = cv2.VideoCapture(in_file)
assert stream.isOpened(), 'Cannot capture source'
# self.path = input_source
datalen = int(stream.get(cv2.CAP_PROP_FRAME_COUNT))
fourcc = int(stream.get(cv2.CAP_PROP_FOURCC))
fps = int(stream.get(cv2.CAP_PROP_FPS))
frameSize = (int(stream.get(cv2.CAP_PROP_FRAME_WIDTH)),
int(stream.get(cv2.CAP_PROP_FRAME_HEIGHT)))
# bitrate = int(stream.get(cv2.CAP_PROP_BITRATE))
videoinfo = {'fourcc': fourcc, 'fps': fps, 'frameSize': frameSize}
stream.release()
return stream, videoinfo, datalen
```
---
## Loss Calculation
sample A -> augment to 4 ground truth
### Method A : PerGT Loss
訓練 : 算四次 loss
```
sample A -> prediction A-1 <-> GT A-1 (算 loss)
sample A -> prediction A-2 <-> GT A-2 (算 loss)
sample A -> prediction A-3 <-> GT A-3 (算 loss)
sample A -> prediction A-4 <-> GT A-4 (算 loss)
```
> Skating
> skating_t5_6 epoch 135
```bash
$ ./MotionExpert_tmp/MotionExpert/results/finetune_skeleton_t5_6/jsons/results_epoch135.json
```
> Boxing : better
```bash
$ ./MotionExpert_tmp/MotionExpert/results/finetune_boxing_0304/jsons
```
去看 GT 怎麼變化。
---
method A 還沒辦法修好 Lora 無法 reproduce 的問題。method B 跟 method C 都修好 Lora 沒辦法 reproduce 的問題
### Method B : ClosestSimGT Loss
訓練 : 每一個 sample 算一次 loss
```
sample A -> prediction <-> 找最相似的 GT 從 (A-1,A-2,A-3,A-4) (算 loss)
```
#### Skating
```bash
$ ./MotionExpert_tmp/skating_gt_tmp/jsons/results_epoch175.json
```
| Skating ClosestSimGT : Loss | Skating ClosestSimGT : (Trainging) Metric |
| - | - |
|  |  |
| Skating ClosestSimGT : GT |  |
#### Boxing
bad
> 去看 GT 怎麼變化 : 視覺化後 ground truth 尋找時跳來跳去的。
```bash
$ ./boxing_gt2/jsons
```
| Boxing ClosestSimGT : Loss | Boxing ClosestSimGT : (Trainging) Metric |
| - | - |
|  |  |
|Boxing ClosestSimGT : GT | 
|
### Method C : PerGT Loss
Change 1 : 修好 Lora 沒辦法 reporduce 的問題
| Skating PerGT : Loss | Skating PerGT : (Trainging) Metric |
| - | - |
|  |  |
| Skating PerGT : GT |  |
Boxing GT 我只跑到 135 epoch 因為趨勢沒有進步的空間。
| Boxing PerGT: Loss | Boxing PerGT : (Trainging) Metric |
| - | - |
| |  |
| Boxing PerGT: GT |  |
| Method | bleu_1 | bleu_4 | rouge | bertscore | G-eval | epoch
| - | - | - | - | - | - | - |
| A (bad lora)| 24.7 | 2.3 | 16.9 | 26.5 | 1.73 | 135 |
| B ClosestSimGT Loss | 21.3 | 2.8 | 17.7 | 12.2 | 1.78 | 175 |
| C PerGT Loss| 24.3 | 6.8 | 19.6 | 12.8 | 1.75 | 90 |
## 0416
### 測試 :
1. 四個影片接在一起變成一個長影片 對應到 4 個 GT
2. 新的 boxing dataset
3. 一個影片 對應到 4 個
#### Lora Config
```python
if cfg.TASK.SPORT == "Skating" :
stagcn_lora_config = {"bias" : "none",
"r" : 32,
"lora_alpha" : 64,
"lora_dropout" : 0.1}
transformation_lora_config = {"bias" : "none",
"r" : 32,
"lora_alpha" : 64,
"lora_dropout" : 0.1}
elif cfg.TASK.SPORT == "Boxing" :
stagcn_lora_config = {"bias" : "none",
"r" : 64,
"lora_alpha" : 128,
"lora_dropout" : 0.5}
transformation_lora_config = {"bias" : "none",
"r" : 64,
"lora_alpha" : 128,
"lora_dropout" : 0.5}
stagcn_lora_config = {"bias" : "none",
"r" : 32,
"lora_alpha" : 64,
"lora_dropout" : 0.1}
transformation_lora_config = {"bias" : "none",
"r" : 32,
"lora_alpha" : 64,
"lora_dropout" : 0.1}
```
```python
import pickle, os
input_path = "/home/c1l1mo/datasets/scripts/skating_pipeline/Skating_GT_test/aggregate.pkl"
output_path = "./skating_test_aggregate_reformat.pkl"
with open(input_path, "rb") as f:
data = pickle.load(f)
for item in data:
trimmed_start = item.get('trimmed_start', 0)
if not item['standard_longer']:
start_frame = item['start_frame'] + trimmed_start
end_frame = item['end_frame'] + trimmed_start
item['start_frame'] = start_frame
item['end_frame'] = end_frame
item['std_start_frame'] = 0
item['std_end_frame'] = end_frame - start_frame
else:
std_start = item['start_frame']
std_end = item['end_frame']
item['std_start_frame'] = std_start
item['std_end_frame'] = std_end
item['start_frame'] = trimmed_start
item['end_frame'] = trimmed_start + (std_end - std_start)
with open(output_path, "wb") as f:
pickle.dump(data, f)
```
---
```python
if cfg.TASK.SPORT == 'Skating' :
if item['standard_longer'] :
std_start = start_frame
usr_start = trimmed_start
else :
usr_start = start_frame + trimmed_start
std_start = 0
length = min(min(length,
len(std_features[0]) - std_start),
len(features[0]) - usr_start)
if cfg.TASK.SPORT == 'Boxing' :
std_start = item["std_start_frame"]
usr_start = item["start_frame"]
length = min(min(item["aligned_seq_len"],
len(std_features[0]) - item["std_start_frame"]),
len(features[0]) - item["start_frame"])
# std_start = item["std_start_frame"]
# usr_start = item["start_frame"]
# length = item["aligned_seq_len"]
```
---
remove 2 coach.
boxing 1 2 3 4 -> (1->1, 3->3, 4->4)
video : 10 * 2
clip 10 : A B C D
clip E B C D
new boxing 1 -> 5
skating
video : 292 1 -> 5
clip : 4 A B C D
---
## 有錯的資料
> /home/weihsin/datasets/Axel_com/471706401488502884
> /home/weihsin/datasets/Lutz/485958788181131265
> /home/weihsin/datasets/Lutz/485958785647771873
> /home/weihsin/datasets/Lutz/485958798918549798
> /home/weihsin/datasets/Axel_com/471706229102870738
> /home/weihsin/datasets/Axel_com/471706248866431172
> /home/weihsin/datasets/Axel_com/471706360988565506
> /home/weihsin/datasets/Axel_com/471706339664986566
**coach1**
2_back_10
2_back_8
2_back_9
3_back_1
**coach2**
5_front_10
5_front_9
```bash
$ source /home/c1l1mo/miniconda3/etc/profile.d/conda.sh
$ conda activate yolo
$ python crop_video.py --dataset new_BX --alphacrop 1
```
我先在 `/home/c1l1mo/testings/yolov7` 在這個路徑裡面,新增了 `/home/c1l1mo/testings/yolov7/bbox_queue_gen.py` 因為當用 bash 檔案 run.sh 生出每個影片的 bounding box 還要再切影片,切影片的程式碼是 crop_video.py
`PYTHONPATH=. python crop_video.py --dataset newBX --alphacrop 1` 需要 bboxes_queue.json 檔案
## New boxing
| video | type | action |
| - | - | - |
| 20250318101244 | 1-1 | deleted
| 20250318101839 | 1-2 | 前手直拳
| 20250318102107 | 1-3 |
| 20250318102438 | 1-4 |
| 20250318102651 | 1-5 |
| 20250318102834 | 1-6 |
| 20250318103431 | 1-7 |
| 20250318103922 | 1-8 |
| 20250318104331 | 1-9 |
| 20250318105057 | 1-10 |
| 20250318105602 | 2-1 | 後手直拳
| 20250318110730 | 2-2 |
| 20250318111226 | 2-3 |
| 20250318111705 | 2-4 |
| 20250318112203 | 2-5 |
| 20250318112356 | 2-6 |
| 20250318112602 | 2-7 |
| 20250318113208 | 3-1 | 前勾手 |
| 20250318113629 | 3-2 |
| 20250318114103 | 3-3 |
| 20250318114659 | 3-4 |
| 20250318114848 | 3-5 |
| 20250318115344 | 4-1 | 後勾手
| 20250318115912 | 4-2 |
| 20250318120441 | 4-3 |
| 20250318120757 | 4-4 |
| 20250318121025 | 4-5 |
| 20250318121600 | 5-1 | upper前手
| 20250318121952 | 5-2 |
| 20250318122427 | 5-3 |
| 20250318122737 | 5-4 |
| 20250318123118 | 5-5 |
| 20250318123506 | 6-1 |
| 20250318123706 | 6-2 |
| 20250318124132 | 6-3 |
| 20250318124403 | 6-4 |
| 20250318124603 | 6-5 |