Closed40

PointNet追実装

さくたまさくたま

ボリュームをマウントして作業が消えないようにしたい
ボリューム作る

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

参考
https://docs.docker.jp/engine/userguide/dockervolumes.html

さくたまさくたま

んー,でもボリュームだと中身を外から書き換えたりできないのか

ディレクトリをマウントすることもできるのでそうする

これでwsl内の/mnt/c/より下のディレクトリをマウントすれば
docker ⇄ wsl ⇄ Windows でファイル共有可能になった?

さくたまさくたま
Powershell
(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
wsl
(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 ...
docker
root@af89cff70751:/workspace# ls
pointnet  test

いけてる

さくたまさくたま

wslでエイリアス追加しておく

~/.bashrc (wsl内)
alias pointnet=' docker run --gpus all -v /mnt/c/Users/sakutama/wsl_pytorch:/workspace -it --rm nvcr.io/nvidia/pytorch:22.01-py3'
さくたまさくたま

https://dreamer-uma.com/pytorch-network/
ネットワークには書き方がありますよみたいなやつね,何回か見たことある

nn.Sequentialの方を使ってるパターンなのか
ぱっと見の好き度としては
x = x.Relu()みたいな方が好きだけど記法で詰まるのやだからSequentialでそのままうつす

さくたまさくたま

MaxPoolの方はx=x.hoge()の方の書き方してるな...
レイヤーの外で次元を語ににょごにょしてるからか

MaxPool前の次元が1024xnで,カーネルをポイント数でMaxPoolするから,出力は1024x1??
あってそう

ポイント数をMaxPoolして点群全体の特徴にする

さくたまさくたま

viewで次元を減らしている

model.py
 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++で一回やったけどな...)

さくたまさくたま

mainを理解していく

このdictの入り方はなんだという話

ネットワークをクラスで定義するとき,
https://qiita.com/mathlive/items/d9f31f8538e20a102e14
self.xxxx = nn.Sequential()
みたいにすると,そのネットワーク中のハイパーパラメータが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)
さくたまさくたま

学習〜
https://zenn.dev/kutabarenn/articles/b18b60178c1f15

# 各バッチ
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クラス分類用の損失関数らしい

さくたまさくたま

https://zenn.dev/hirayuki/articles/bbc0eec8cd816c183408
ありがたい先人のまとめ

学習は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を更新してるかとか,依存の方向性が分かりにくくて,独立して動いているように見えて理解しにくい...

さくたまさくたま

点群のサンプリング方法をこだわるとすれば,点群からoctree構築→深さを決めて近傍から均等にサンプリング,とかすると一部に偏ってる点群入力にも対応できるようになったりとかするんだろうなぁ

でも,パーツセグメンテーションとかは回転・並進移動させたら部分的に重なっちゃうパーツもあるだろうし一部じゃ無理かしら

敢えて,均等にサンプリングするんじゃなくてインデックスでランダムにサンプリングする,くらいの乱雑さがちょうどいいのかな

さくたまさくたま

一旦整理,間違えてたわ.
今回書いてあるモデルは,Classificationまで.つまり,点群:ラベルのデータセット
今やろうとしてしまってたのは,セグメンテーション.それをするには画像下のsegmentation networkを実装しないといけない

二つのパーツの点を混ぜて,各点にラベルをつけて 点:ラベルのデータセットを作ったらSegmentation
どちらかのパーツの点群をとってきて,点群にラベルをつけて 点群:ラベルのデータセット作ったらClassification

記事に倣って今書いたモデルは,Classificationのモデルなので,それぞれの点群をサブサンプリングした点群に対してラベルをつけないといけないねーーーー

さくたまさくたま

ハサミだと,パーツごとが回転+並進でほぼ重なるので,別の形状にする

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)
さくたまさくたま

動いたやつ

sampler.py

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のイメージそのまま使ってるしめんどくさいので,初回だけ待つことにしてキャッシュしてみる

https://zenn.dev/hpp/articles/da5caa8dc000ae
色々やってるけどsaveとかだけでいいな

さくたまさくたま

mainに組み込み

main.py
    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

みたいなのが出たので
https://discuss.pytorch.org/t/runtimeerror-expected-object-of-scalar-type-double-but-got-scalar-type-float-for-argument-2-weight/38961/3
この通りにfloatに合わせた

さくたまさくたま

そもそもデータのノイズも多いし形状もちょっと似てるし色ないし,むずそうではある

さくたまさくたま

テストを一通り書いた

test.py
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)

このスクラップは2023/03/15にクローズされました