PointNet追実装
これの通りにnvidia-docker環境構築
Dockerぽい
いけた
これの後半を真似していく
理解して書くまでは前もやったのだけど,データ入れて学習してみることに重きを置く
ボリュームをマウントして作業が消えないようにしたい
ボリューム作る
docker volume create pytorch_vol
ボリュームをマウントしてコンテナを立ち上げる
docker run --gpus all -v pytorch_vol:/workspace -it --rm nvcr.io/nvidia/pytorch:22.01-py3
または
docker run --gpus all --mount source=pytorch_vol,target=/workspace -it --rm nvcr.io/nvidia/pytorch:22.01-py3
参考
んー,でもボリュームだと中身を外から書き換えたりできないのか
ディレクトリをマウントすることもできるのでそうする
これでwsl内の/mnt/c/
より下のディレクトリをマウントすれば
docker ⇄ wsl ⇄ Windows でファイル共有可能になった?
(base) PS C:\Users\sakutama> cd .\wsl_pytorch\
(base) PS C:\Users\sakutama\wsl_pytorch> ls
ディレクトリ: C:\Users\sakutama\wsl_pytorch
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2023/03/08 7:26 pointnet
(base) PS C:\Users\sakutama\wsl_pytorch> mkdir test
ディレクトリ: C:\Users\sakutama\wsl_pytorch
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2023/03/08 7:32 test
(base) PS C:\Users\sakutama\wsl_pytorch> ls
ディレクトリ: C:\Users\sakutama\wsl_pytorch
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2023/03/08 7:26 pointnet
d----- 2023/03/08 7:32 test
(base) PS C:\Users\sakutama\wsl_pytorch> wsl.exe
(base) ubuntu@sakutamawin:/mnt/c/Users/sakutama/wsl_pytorch$ docker run --gpus all -v ./:/workspace -it --rm nvcr.io/nvidia/pytorch:22.01-py3
=============
== PyTorch ==
=============
NVIDIA Release 22.01 (build 31424411)
PyTorch Version 1.11.0a0+bfe5ad2
Container image Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
Copyright (c) 2014-2022 Facebook Inc.
Copyright (c) 2011-2014 Idiap Research Institute (Ronan Collobert)
Copyright (c) 2012-2014 Deepmind Technologies (Koray Kavukcuoglu)
Copyright (c) 2011-2012 NEC Laboratories America (Koray Kavukcuoglu)
Copyright (c) 2011-2013 NYU (Clement Farabet)
Copyright (c) 2006-2010 NEC Laboratories America (Ronan Collobert, Leon Bottou, Iain Melvin, Jason Weston)
Copyright (c) 2006 Idiap Research Institute (Samy Bengio)
Copyright (c) 2001-2004 Idiap Research Institute (Ronan Collobert, Samy Bengio, Johnny Mariethoz)
Copyright (c) 2015 Google Inc.
Copyright (c) 2015 Yangqing Jia
Copyright (c) 2013-2016 The Caffe contributors
All rights reserved.
Various files include modifications (c) NVIDIA CORPORATION & AFFILIATES. All rights reserved.
This container image and its contents are governed by the NVIDIA Deep Learning Container License.
By pulling and using the container, you accept the terms and conditions of this license:
https://developer.nvidia.com/ngc/nvidia-deep-learning-container-license
NOTE: MOFED driver for multi-node communication was not detected.
Multi-node communication performance may be reduced.
NOTE: The SHMEM allocation limit is set to the default of 64MB. This may be
insufficient for PyTorch. NVIDIA recommends the use of the following flags:
docker run --gpus all --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 ...
root@af89cff70751:/workspace# ls
pointnet test
いけてる
wslでエイリアス追加しておく
alias pointnet=' docker run --gpus all -v /mnt/c/Users/sakutama/wsl_pytorch:/workspace -it --rm nvcr.io/nvidia/pytorch:22.01-py3'
これ思い出しながらコピペ
ネットワークには書き方がありますよみたいなやつね,何回か見たことある
nn.Sequentialの方を使ってるパターンなのか
ぱっと見の好き度としては
x = x.Relu()みたいな方が好きだけど記法で詰まるのやだからSequentialでそのままうつす
MaxPoolの方はx=x.hoge()の方の書き方してるな...
レイヤーの外で次元を語ににょごにょしてるからか
MaxPool前の次元が1024xnで,カーネルをポイント数でMaxPoolするから,出力は1024x1??
あってそう
ポイント数をMaxPoolして点群全体の特徴にする
viewで次元を減らしている
def forward(self, input_data):
out = input_data.view(-1, self.num_channels, self.num_points)
out = self.main(out)
out = out.view(-1, self.num_channels)
return out
input_data.view(-1, self.num_channels, self.num_points)なんでいるんだ?
MaxPoolする向きが違うから転置してる感じなのかな
次元の順番混乱する...
モデルは理解した(思い出した)
ShapeNetでメッシュからサンプリングした点群をダウンサンプリングしてパーツセグメーテンションしてみる
blender で3Dモデル2パーツに割ってそれぞれの点群作るか
ここから本番
(これもPointNet++で一回やったけどな...)
見づらい... torchinfoがバージョン都合でスッと入らなかった
二つの点群ファイルを作るのは終わった
mainを理解していく
このdictの入り方はなんだという話
ネットワークをクラスで定義するとき,
みたいにすると,そのネットワーク中のハイパーパラメータがxxxxキーに保存されるぽい
試しに,Tnetのモジュール二つのインスタンス変数の名前を変えてみる
self.transT= nn.Sequential(...)
としてみたら
model.state_dict().keys()
の結果が
odict_keys(['main.0.transT.0.main.0.weight', 'main.0.transT.0.main.0.bias', 'main.0.transT.0.main.2.weight', 'main.0.transT.0.main.2.bias', 'main.0.transT.0.main.2.running_mean', 'main.0.transT.0.main.2.running_var', 'main.0.transT.0.main.2.num_batches_tracked', 'main.0.transT.1.main.0.weight', 'main.0.transT.1.main.0.bias', 'main.0.transT.1.main.2.weight', 'main.0.transT.1.main.2.bias', 'main.0.transT.1.main.2.running_mean', 'main.0.transT.1.main.2.running_var', 'main.0.transT.1.main.2.num_batches_tracked', 'main.0.transT.2.main.0.weight', 'main.0.transT.2.main.0.bias', 'main.0.transT.2.main.2.weight', 'main.0.transT.2.main.2.bias', 'main.0.transT.2.main.2.running_mean', 'main.0.transT.2.main.2.running_var', 'main.0.transT.2.main.2.num_batches_tracked', 'main.0.transT.4.main.0.weight', 'main.0.transT.4.main.0.bias', 'main.0.transT.4.main.2.weight', 'main.0.transT.4.main.2.bias', 'main.0.transT.4.main.2.running_mean', 'main.0.transT.4.main.2.running_var', 'main.0.transT.4.main.2.num_batches_tracked', 'main.0.transT.5.main.0.weight', 'main.0.transT.5.main.0.bias', 'main.0.transT.5.main.2.weight', 'main.0.transT.5.main.2.bias', 'main.0.transT.5.main.2.running_mean', 'main.0.transT.5.main.2.running_var', 'main.0.transT.5.main.2.num_batches_tracked', 'main.0.transT.6.weight', 'main.0.transT.6.bias', 'main.1.main.0.weight', 'main.1.main.0.bias', 'main.1.main.2.weight', ...])
みたいになったので,そうっぽい.
transTの後の数字は
self.transT = nn.Sequential(
NonLinear(3, 64),
NonLinear(64, 128),
NonLinear(128, 1024),
# max pooling by number of points
MaxPool(input_channels=1024, num_kernel=self.num_points),
NonLinear(1024, 512),
NonLinear(512, 256),
# make 9 dimension feature for transformation matrix
nn.Linear(256, 9)
)
ここの各レイヤ(0-6)に対応してる.
最後の,3x3のtransformation matrixに変換するところのバイアスを,単位行列に設定している.
main.0.main....みたいなのは訳わかんなくなりそうだから,各レイヤ?モジュール?の名前はこまめにつけるのが良さそう
書き方,nn.Sequential()とx = x.relu()みたいな書き方とどっちでもいいんだって言ったけど,このdictの表示はx=x.relu()みたいな方がわかりやすくなるんだと思った
↓0-6とかレイヤのインデックスじゃなくてconv1とかでアクセスできるようになる
損失関数と活性化関数を設定
criterion = nn.BCELoss()
optimizer = optim.Adam(pointnet.parameters(), lr=0.001)
学習〜
# 各バッチ
for iteration in range(100+1):
# 前の学習で算出された勾配をリセット
pointnet.zero_grad()
input_data, labels = data_sampler(batch_size, num_points)
output = pointnet(input_data)
output = nn.Sigmoid()(output)
error = criterion(output, labels)
# 誤差逆伝播法 (PointNetモデルの情報は持ってるの?Tensorを書き換えて勾配計算をしているはず?それはoptimizerの後か?)
error.backward()
optimizer.step()
# 10イテレーションごとにaccとlossを計算
if iteration % 10 == 0:
with torch.no_grad():
output[output > 0.5] = 1
output[output < 0.5] = 0
accuracy = (output==labels).sum().item()/batch_size
loss_list.append(error.item())
accuracy_list.append(accuracy)
print('Iteration : {} Loss : {}'.format(iteration, error.item()))
print('Iteration : {} Accuracy : {}'.format(iteration, accuracy))
ふああ,ちょっと損失関数の中身はわからんでし
とにかく2クラス分類用の損失関数らしい
ありがたい先人のまとめ
学習はAutogradという勾配を自動で計算する仕組みに支えられている。
そのAutogradを利用して、optimizer.zero_grad()で勾配を初期化し、loss.barkward()でそのエポックにおける損失に対する勾配を計算し、optimizer.step()で重みの更新を行う。この繰り返しで学習が進む。
output = pointnet(input_data)
output = nn.Sigmoid()(output)
# output Tensor printed
# print(output)
error = criterion(output, labels)
# 勾配を計算
error.backward()
# 重みを更新する
optimizer.step()
optimizer, errorがどうやってModelの値を参照してて,どうやってModelのTensorを更新してるかとか,依存の方向性が分かりにくくて,独立して動いているように見えて理解しにくい...
最後にモデルの重みを保存する行を追加
torch.save(pointnet.state_dict(), 'pointnet_weight.pth')
Dataloaderの使い方を思い出す
点群のサンプリング方法をこだわるとすれば,点群からoctree構築→深さを決めて近傍から均等にサンプリング,とかすると一部に偏ってる点群入力にも対応できるようになったりとかするんだろうなぁ
でも,パーツセグメンテーションとかは回転・並進移動させたら部分的に重なっちゃうパーツもあるだろうし一部じゃ無理かしら
敢えて,均等にサンプリングするんじゃなくてインデックスでランダムにサンプリングする,くらいの乱雑さがちょうどいいのかな
点群読み込み
サンプリング
ランダムに回転・並進
一旦整理,間違えてたわ.
今回書いてあるモデルは,Classificationまで.つまり,点群:ラベルのデータセット
今やろうとしてしまってたのは,セグメンテーション.それをするには画像下のsegmentation networkを実装しないといけない
二つのパーツの点を混ぜて,各点にラベルをつけて 点:ラベルのデータセットを作ったらSegmentation
どちらかのパーツの点群をとってきて,点群にラベルをつけて 点群:ラベルのデータセット作ったらClassification
記事に倣って今書いたモデルは,Classificationのモデルなので,それぞれの点群をサブサンプリングした点群に対してラベルをつけないといけないねーーーー
DatasetとDataLoaderの違い(transformは今回いらなそう)
ハサミだと,パーツごとが回転+並進でほぼ重なるので,別の形状にする
transform: データをnormalizeする
Dataset: 点群を点群数分サブサンプリングして部分点群: ラベルのデータセットを作る
256+256=512のデータセットにしてみる
indexサンプリング 128
furthest point sampling 128
DataLoader: Datasetからバッチサイズ分データを取り出す
バッチごとに各ラベル半分になってた方がいいのかしら?まぁいいや,普通にシャッフルで
こんな感じでテスト
def main():
data_set = PcdDataset(32)
dataloader = torch.utils.data.DataLoader(data_set, batch_size=2, shuffle=True)
for i in dataloader:
print(i)
動いたやつ
def farthest_point_sample(point, npoint):
"""
Input:
xyz: pointcloud data, [N, D]
npoint: number of samples
Return:
centroids: sampled pointcloud index, [npoint, D]
"""
N, D = point.shape
xyz = point[:,:3]
centroids = np.zeros((npoint,))
distance = np.ones((N,)) * 1e10
farthest = np.random.randint(0, N)
for i in range(npoint):
centroids[i] = farthest
centroid = xyz[farthest, :]
dist = np.sum((xyz - centroid) ** 2, -1)
mask = dist < distance
distance[mask] = dist[mask]
farthest = np.argmax(distance, -1)
point = point[centroids.astype(np.int32)]
return point
class PcdNormalize(object):
def __init__(self):
pass
def __call__(self, sample):
sample = sample[:, 0:3]
centroid = torch.mean(sample, axis=0)
sample = sample - centroid
m = torch.max(torch.sqrt(torch.sum(sample ** 2, axis=1)))
sample = sample / m
return sample
class PcdDataset(Dataset):
def __init__(self, data_num=512, npoints=1024, transform=PcdNormalize()):
self.npoints = npoints
self.data_num = data_num
self.transform = transform
class_0_pcd = np.loadtxt('./data/Moebius.txt').astype(np.float32)
class_1_pcd = np.loadtxt('./data/Zetton.txt').astype(np.float32)
print("============pcd files loaded")
print(class_0_pcd.shape)
pcds_0 = np.empty((0,npoints,class_0_pcd.shape[1]))
pcds_1 = np.empty((0,npoints,class_1_pcd.shape[1]))
print("============random sampling")
for i in range(data_num//4):
random_idx_0 = random.sample(range(len(class_0_pcd)), 1024)
pcds_0 = np.append(pcds_0, np.array([class_0_pcd[random_idx_0]]), axis=0)
random_idx_1 = random.sample(range(len(class_1_pcd)), 1024)
pcds_1 = np.append(pcds_1, np.array([class_1_pcd[random_idx_1]]), axis=0)
print("============farthest point sampling")
for i in range(data_num//4):
pcds_0 = np.append(pcds_0, np.array([farthest_point_sample(class_0_pcd, 1024)]), axis=0)
pcds_1 = np.append(pcds_1, np.array([farthest_point_sample(class_1_pcd, 1024)]), axis=0)
print("============create label")
labels_0 = torch.zeros(len(pcds_0))
labels_1 = torch.ones(len(pcds_1))
class_0_tensor = torch.from_numpy(pcds_0)
class_1_tensor = torch.from_numpy(pcds_1)
self.input_data = torch.cat((class_0_tensor[:, :,0:3], class_1_tensor[:, :,0:3]), dim=0)
self.labels = torch.cat((labels_0, labels_1), dim=0)
def __len__(self):
return self.data_num
def __getitem__(self, index):
data = self.input_data[index]
label = self.labels[index]
if self.transform:
data = self.transform(data)
return data, label
FPSが重いのでデータセットキャッシュしたい...
あとはopen3d使えば距離の探索にoctree使ってくれてもっと速いはず
今は環境のDockerfile作らずnvidiaのイメージそのまま使ってるしめんどくさいので,初回だけ待つことにしてキャッシュしてみる
色々やってるけどsaveとかだけでいいな
mainに組み込み
dataset = PcdDataset(512, num_points)
dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True)
for i in dataloader:
print(i)
for epoch in range(epochs):
for input_data, labels in dataloader:
pointnet.zero_grad()
# input_data, labels = data_sampler(batch_size, num_points)
# print(input_data.shape)
output = pointnet(input_data.float())
output = nn.Sigmoid()(output)
# output Tensor printed
# print(output)
error = criterion(output, labels)
# calculate grad
error.backward()
# update weights
optimizer.step()
with torch.no_grad():
output[output > 0.5] = 1
output[output < 0.5] = 0
accuracy = (output==labels).sum().item()/batch_size
loss_list.append(error.item())
accuracy_list.append(accuracy)
torch.save(pointnet.state_dict(), 'pointnet_weight.pth')
print('epoch : {} Loss : {}'.format(epoch, error.item()))
print('epoch : {} Accuracy : {}'.format(epoch, accuracy))
RuntimeError: expected scalar type Double but found Float
みたいなのが出たので
この通りにfloatに合わせたはー...これが分からんと何回言えば...
真似してこうすれば動くことはわかるんだけど理解できん
質は知らんがとりあえず学習してるぽい
そもそもデータのノイズも多いし形状もちょっと似てるし色ないし,むずそうではある
だそうです
1つのデータでテストしようとしたらbatch normで止まった
1つだとバッチ内に他のデータがないからダメらしい
そんなことある?よしなに無視してないんだ...
と思ったら model.eval()を使うべきらしい
テストを一通り書いた
def test():
pointnet = PointNet(num_points, num_labels)
pointnet.load_state_dict(torch.load('pointnet_weight.pth'))
pointnet = pointnet.float()
# target = 0
dataset = PcdDataset(4, num_points, for_test=True)
dataloader = torch.utils.data.DataLoader(dataset, batch_size=4, shuffle=False)
pointnet.eval()
for input_data, labels in dataloader:
input_data = input_data.view(-1, 3)
labels = labels.view(-1, 1)
with torch.no_grad():
pred = pointnet(input_data.float())
pred = nn.Sigmoid()(pred)
pred[pred > 0.5] = 1
pred[pred < 0.5] = 0
for predicted, actual in zip(pred, labels):
print(f'Predicted: "{predicted.item()}", Actual: "{actual.item()}"')
if predicted.item() == actual.item():
print("correct!")
else:
print("incorrect...")
if __name__ == '__main__':
test()
できたぽい
Predicted: "0.0", Actual: "0.0"
correct!
Predicted: "0.0", Actual: "0.0"
correct!
Predicted: "1.0", Actual: "1.0"
correct!
Predicted: "1.0", Actual: "1.0"
correct!
DataLoader使う必要なかったかもしれない
けど,testの場合キャッシュを使わずに新たにサンプリングをして,ファイルに保存する
(インデックスつけたり並び順管理して点群のファイル名とデータをマッチングしたりするの面倒だったから今はファイル名決めうち)
if for_test:
# 可視化のため一時的に
np.savetxt('./data/test/test_0_0_random.txt', class_0_pcd[random_idx_0])
np.savetxt('./data/test/test_2_1_random.txt', class_1_pcd[random_idx_1])
print("============farthest point sampling")
for i in range(self.data_num//4):
sampled_0 = farthest_point_sample(class_0_pcd, self.npoints)
sampled_1 = farthest_point_sample(class_1_pcd, self.npoints)
pcds_0 = np.append(pcds_0, np.array([sampled_0]), axis=0)
pcds_1 = np.append(pcds_1, np.array([sampled_1]), axis=0)
if for_test:
# 可視化のため一時的に
np.savetxt('./data/test/test_1_0.txt', sampled_0)
np.savetxt('./data/test/test_3_1.txt', sampled_1)
テストに使った点群(1024点)を見てみる
※色は加味されてない
0個目(indexをランダムサンプリング, ラベル0)
1個目(farthest point sampling, ラベル0)
2個目(indexをランダムサンプリング, ラベル1)
3個目(farthest point sampling, ラベル1)