はじめに
PointNetの勉強・実装をやってみます。
https://arxiv.org/abs/1612.00593
すでにほかの方々がしっかりしたソースで実装しているので
ここでは自分でソースを自作して理解を深めつつ、本格的な検証はやらない方針で進めます。(やれ)
(https://github.com/charlesq34/pointnet)
(https://github.com/charlesq34/pointnet2)
(https://github.com/nikitakaraevv/pointnet)
前提知識
筆者の機械学習の知識はこの時点では
https://qiita.com/akaiteto/items/9ac0a84377600ed337a6
https://qiita.com/akaiteto/items/0c25371eb86ca43402ce
に書いた内容しかありません。
補足的な知識自分用にまとめます。
多層パーセプトロン
たとえば、
テータAがきたら「犬」という特徴、
データBがきたら「サイズが小さい」という特徴を取得できる処理機構があったとして、
「小型犬」という併せ持った特徴を抽出するには、データA,データBを合わせて
処理を行わなければいけません。こういった状況でベースになるのが多層パーセプトロン。
線形返還で結合することで複数の特徴を1つに集約させます。
メリットの一つとして、教師データを与えて、
正解との誤差が最小になるように重み$w_i$を調整していくことが可能な点です。
活性化関数
活性化関数は、特徴を際立たせたり、最終的に出力されるデータの形状に整えるために定義するようです。
●ソフトマックス関数
ソフトマックスは多クラスの問題に適した活性化関数です。
多クラスごとに出力された数値の合計が1になるように出力され、
各クラスの数値がそのまま整合率になります。
PointNetでいえば、最終的には点群が犬、猫、サルなどの複
数の分類を行うので、
ソフトマックス関数という活性化関数を最終的には使用します。
●Relu関数
Pointnetの全レイヤーでは必ずReluを行います。
Reluを通すと、マイナスの値は0、それ以外はそのまま出力する仕組みのようです。
「マイナスの数値ほんとにきりすててええんか?」と素人考え的には思いたくなりますが、
データによって向き不向きがあるのかもしれません。
読む
https://arxiv.org/abs/1612.00593
読んでみます。
実装部分の3章~4章を読みます。
ここでは整理のために翻訳した内容をだらだらとメモ代わりに書いてるだけなので、
読まなくても良いです。
3
順不同にポイントが入っている点群を想定します。
点群データにはRGBとか法線情報(そのポイントが属する面に対する法線)が
入ってる場合もありますが、ここでは(x,y,z)だけを想定します。
入力として、N個の点群を与えて、
出力として、n個の点群とm個の分類があったときはN×Mの行列でスコアが出力されます。
4-1
点群のディープラーニングを行う場合、3つの考慮しないといけない性質があります
●Unordered(順不同)
画像なら隣り合うピクセルに関係性あったり、
音声なら時間の順番でデータが入っていたりと、登録されているデータの順序に規則があります。
しかし点群は異なります。点群データ(.plyなど)に入っているデータは規則なくばらばらに
ポイントの位置情報が入っています。
●Interaction(ポイントが互いに関連性を持っている)
Unordered(順不同)に記載した通り、データとして入ってるポイントの順番はバラバラです。
ですが、空間に配置したときに隣り合うポイントは互いに関連性を持っています。
たとえば、3つのポイントがあれば、
それらのポイント同士を結び付けることで一つの面をなし、
そしてその面同士が結びつくことで大きな構造を形成します。
点群のディープラーニングでは、
このようなポイント間の関連性の特徴も取得できないといけません。
●Invariance(不変性)
点群は回転したり移動して登録されている場合が常です。
全ての点群データが正面を向いて常に同じ位置で登録されていることはありません。
どのような変化が起きていようと起きていまいと、
実行した後は同じ結果が得られるようにしないといけません。
こいつらを達成できるネットワークでなければなりません。
PointNetはこの中でも特に、Unordered(順不同)を重視しています。
4-2
ネットワーク
ネットワークの全体像は以下の通りです。
流れ
1.n個のポイントを入力
2.特徴変換の適用
3.マックスプーリングで特徴を集約
4.セグメンテーションネットワーク(分類ネットの拡張)を適用し、
各分類m個のスコアを出力します。
n個のローカルな特徴に対して実施し、
統合されたグローバルな特徴に対しても実施します。
その他
mlp : 多層パーセプトロン
ドロップレイヤー:セグメンテーションネットワークの最後のmlpで実行
このネットワークにおける重要なポイントは以下の3つです。
●マックスプーリング層
●ローカル・グローバルな情報を組み合わせる構造
●入力ポイントとその特徴を並べて扱えるネットワーク
以降に詳しく見て行きます。
点群を表す関数の定義
まずは点群にたいして対称関数を適用します。
これにより、点群内のバラバラな三次元情報を関数の形式で表現し、
点群を一般的な関数で表します。($x_n$は点群内の各ポイント)
f(\{x_1,\cdots,x_n\})
\approx
g(h(x_1),\cdots,h(x_n))
対称関数なので、$f(x,y) = f(y,x)$となり、入力値の順番に依存しません。順不同な点群にはぴったりです。
h : \mathbb{R}^N ⇒ \mathbb{R}^K \\
g : \mathbb{R}^K × \mathbb{R}^K × \cdots × \mathbb{R}^K × \mathbb{R}^K\\
hを多層パーセプトロン、gを最大プーリング関数です。
ローカル・グローバルな情報を組み合わせる
点群のセグメンテーションには、ローカル・グローバルな情報を組み合わせることが必要です。
その組み合わせを行うネットワークが下記になります。
まず、N個の点群のベクトルである入力データ$[f_1, \cdots,f_n]$は、
全ての点群の情報を一緒くたに持っているグローバルなデータの塊です。
このネットワークでは、下記の計算を行います。
1.全ての点群全体から計算した特徴を、点群の各ポイントに対して結びつけます。
これにより、すべてのポイントに対して入力点群全体をもとに抽出したグローバルな特徴を付与させます。
2.各点群内におけるある1つの点群内における結びつきから計算されたローカルな特徴を取得し各ポイントに付与させます。
ローカル・グローバルな特徴を併せ持つことにより、
部分的に尖ってるだとかのローカルな幾何学的な構造と、
入力として与えたn個の点群全体と比べたときのグローバルな特徴、
これら2つを集約て処理を行うことができます。
点群が回転・移動している場合の考慮を行うネットワーク
点群のネットワークは、4-1でのべた「●Invariance(不変性)」が担保できていることが必要です。
T-netによって変換行列を予測することで、不変性を保ちます。
T-net内部では、特徴抽出、最大プーリングなどで実装されます。
実装
理論を最低限理解できる程度に実装してみます。
このソースがただしいかどうか、実際にセグメンテーションを行って検証すべきですが、
時間の都合でおこなっていないので、参考にする程度にしてください。
import torch.nn as nn
import torch.nn.functional as F
import torch
from torch import optim
import numpy as np
class T_net(nn.Module):
def __init__(self,pointNum=1024, mat_dim=3):
super().__init__()
self.pointNum = pointNum
self.mat_dim = mat_dim
#主要なレイヤー
self.conv1_1 = nn.Conv1d(mat_dim, 64, 1)
self.conv1_2 = nn.Conv1d(64, 128, 1)
self.conv1_3 = nn.Conv1d(128, 1024, 1)
self.MaxPool = nn.MaxPool1d(pointNum)
self.fc1_1 = nn.Linear(1024, 512)
self.fc1_2 = nn.Linear(512, 256)
self.fc1_3 = nn.Linear(256, mat_dim * mat_dim)
# すべてのレイヤーで共通で行うレイヤー
self.bn_conv1_1 = nn.BatchNorm1d(64)
self.bn_conv1_2 = nn.BatchNorm1d(128)
self.bn_conv1_3 = nn.BatchNorm1d(1024)
self.bn_fc1_1= nn.BatchNorm1d(512)
self.bn_fc1_2 = nn.BatchNorm1d(256)
def forward(self, input):
input_pcl = input
# 畳み込み層
input_pcl = self.conv1_1(input_pcl)
input_pcl = self.filter_common(self.bn_conv1_1,input_pcl)
input_pcl = self.conv1_2(input_pcl)
input_pcl = self.filter_common(self.bn_conv1_2,input_pcl)
input_pcl = self.conv1_3(input_pcl)
input_pcl = self.filter_common(self.bn_conv1_3,input_pcl)
# マックスプーリング
# 最重要なポイント
# 対称関数の役割として実行。「Unordered(順不同)」の確保
input_pcl = self.MaxPool(input_pcl)
input_pcl = nn.Flatten(1)(input_pcl)
# 結合層
input_pcl = self.fc1_1(input_pcl)
input_pcl = self.filter_common(self.bn_fc1_1,input_pcl)
input_pcl = self.fc1_2(input_pcl)
input_pcl = self.filter_common(self.bn_fc1_2,input_pcl)
# 変換行列のk**2個の要素取得
trans_mat = self.fc1_3(input_pcl)
# k x k の変換行列に整形
trans_mat = trans_mat.view(-1, self.mat_dim, self.mat_dim)
# T-netもネットワークなので最適化が必要
# weightにたいして最適化を行う。
weight = torch.eye(self.mat_dim, requires_grad=True).repeat(input_pcl.shape[0], 1, 1)
trans_mat = trans_mat + weight
return trans_mat
def filter_common(self,bn,inputdata):
return F.relu((bn(inputdata)))
class PointNet(nn.Module):
def __init__(self, pointNum=1024,classes=10):
super().__init__()
self.pointNum = pointNum
self.classes = classes
#主要なレイヤー(実行順に記載)
self.input_transform = self.transform
self.fc1 = nn.Linear(3, 64)
self.feature_transfor = self.transform
self.fc2_1 = nn.Linear(64, 64)
self.fc2_2 = nn.Linear(64, 128)
self.fc2_3 = nn.Linear(128, 1024)
self.MaxPool = nn.MaxPool1d(pointNum)
self.fc3_1 = nn.Linear(1024, 512)
self.fc3_2 = nn.Linear(512,256)
self.fc3_3 = nn.Linear(256,classes)
# すべてのレイヤーで共通で行うレイヤー
self.bn1 = nn.BatchNorm1d(64)
self.bn2_1 = nn.BatchNorm1d(64)
self.bn2_2 = nn.BatchNorm1d(128)
self.bn2_3 = nn.BatchNorm1d(1024)
self.bn3_1 = nn.BatchNorm1d(512)
self.bn3_2 = nn.BatchNorm1d(256)
self.DropOut = nn.Dropout(p=0.3)
self.logsoftmax = nn.LogSoftmax(dim=1)
self.SegmentationNW =nn.Sequential(
nn.Linear(1088, 512),
nn.Linear(512, 256),
nn.Linear(256, 128),
nn.Linear(128, classes),
)
def transform(self,input_pcl,k):
trans_kxK = T_net(self.pointNum, k)(input_pcl)
input_pcl = torch.bmm(torch.transpose(input_pcl, 1, 2), trans_kxK)
return input_pcl,trans_kxK
def forward(self, input):
input_pcl = input
# 1. 各点群に対する不変性(Invariance)の確保
input_pcl,trans3x3 = self.input_transform(input_pcl.transpose(1, 2),k=3)
# 2. 一回目のmlp
input_pcl = self.fc1(input_pcl)
input_pcl = self.filter_common(self.bn1,input_pcl)
# 3. 各点群内でのローカルな特徴の取得
input_pcl,trans64x64 = self.feature_transfor(input_pcl.transpose(1, 2),k=64)
# 4. 二回目のmlp
input_pcl = self.fc2_1(input_pcl)
input_pcl = self.filter_common(self.bn2_1,input_pcl)
input_pcl = self.fc2_2(input_pcl)
input_pcl = self.filter_common(self.bn2_2,input_pcl)
input_pcl = self.fc2_3(input_pcl)
input_pcl = self.filter_common(self.bn2_3,input_pcl)
# 5. マックスプーリング
input_pcl = self.MaxPool(input_pcl)
input_pcl = nn.Flatten(1)(input_pcl)
# 6. 三回目のmlp
input_pcl = self.fc3_1(input_pcl)
input_pcl = self.filter_common(self.bn3_1,input_pcl)
# 7. 最後のmlp
input_pcl = self.fc3_2(input_pcl)
input_pcl = self.DropOut(input_pcl)
input_pcl = self.filter_common(self.bn3_2,input_pcl)
# 7. 分類ごとに出力
input_pcl = self.fc3_3(input_pcl)
output_pcl = self.logsoftmax(input_pcl)
return output_pcl,trans3x3,trans64x64
def filter_common(self,bn,inputdata):
# BatchNormalをするために次元を一時的にかえる
if inputdata.ndim == 3:
inputdata = inputdata.transpose(1, 2)
inputdata = F.relu((bn(inputdata)))
inputdata = inputdata.transpose(1, 2)
if inputdata.ndim == 2:
inputdata = F.relu((bn(inputdata)))
return inputdata
# # #デフォルトで1024ポイント持ってる点群を入力する設定
# test_data = np.load("D:/worj/untitled/pointnet/train_pcl16_1024pts.npy")
# test_data = torch.from_numpy(test_data.astype(np.float32)).clone()
# # テストデータの次元 = [点群のデータ数,1つの点群あたりのポイント数, 3(XYZ)]
# train_data = test_data.transpose(1, 2)
# # 正解データの次元 = [点群のデータ数]
# answer_data = np.load("D:/worj/untitled/pointnet/answer_pcl16.npy")
# answer_data = torch.from_numpy(answer_data).clone()
# print(answer_data.shape)
# ランダムなデータを作成
# 検証は行わずに理論だけを追う予定なので適当なデータを用意
# 16個の位置情報(XYZ)のみをもつ点群を用意して、
# 1024のポイントをサンプリングした、という想定の適当なランダムデータ
train_data = np.random.rand(16,1024,3)
train_data = torch.from_numpy(train_data.astype(np.float32)).clone()
answer_data = np.arange(16)
answer_data = torch.from_numpy(answer_data).clone()
print("***学習フェーズ***")
PointNet = PointNet()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(PointNet.parameters(), lr=0.001, momentum=0.9)
epochs = 1
for i in range(epochs):
# 最適化処理
optimizer.zero_grad()
outputs,trans3x3,trans64x64 = PointNet(train_data)
# PointNet,input_transform,feature_transformに対して最適化を行う。
# 未実装
# optimizer.step()
exit()
ひとつずつみてみる
#⓪入力データ
# ランダムなデータを作成
# 検証は行わずに理論だけを追う予定なので適当なデータを用意
# 16個の位置情報(XYZ)のみをもつ点群を用意して、
# 1024のポイントをサンプリングした、という想定の適当なランダムデータ
train_data = np.random.rand(16,1024,3)
train_data = torch.from_numpy(train_data.astype(np.float32)).clone()
answer_data = np.arange(16)
answer_data = torch.from_numpy(answer_data).clone()
論文に倣い、n×3のデータを適当に用意します。
nはポイント数で、ここでは1024ポイント内包する点群です。
これを16個用意するので、データとしては[16,1024,3]の次元になります。
#①③:T-net
class T_net(nn.Module):
def __init__(self,pointNum=1024, mat_dim=3):
super().__init__()
self.pointNum = pointNum
self.mat_dim = mat_dim
#主要なレイヤー
self.conv1_1 = nn.Conv1d(mat_dim, 64, 1)
self.conv1_2 = nn.Conv1d(64, 128, 1)
self.conv1_3 = nn.Conv1d(128, 1024, 1)
self.MaxPool = nn.MaxPool1d(pointNum)
self.fc1_1 = nn.Linear(1024, 512)
self.fc1_2 = nn.Linear(512, 256)
self.fc1_3 = nn.Linear(256, mat_dim * mat_dim)
# すべてのレイヤーで共通で行うレイヤー
self.bn_conv1_1 = nn.BatchNorm1d(64)
self.bn_conv1_2 = nn.BatchNorm1d(128)
self.bn_conv1_3 = nn.BatchNorm1d(1024)
self.bn_fc1_1= nn.BatchNorm1d(512)
self.bn_fc1_2 = nn.BatchNorm1d(256)
def forward(self, input):
input_pcl = input
# 畳み込み層
input_pcl = self.conv1_1(input_pcl)
input_pcl = self.filter_common(self.bn_conv1_1,input_pcl)
input_pcl = self.conv1_2(input_pcl)
input_pcl = self.filter_common(self.bn_conv1_2,input_pcl)
input_pcl = self.conv1_3(input_pcl)
input_pcl = self.filter_common(self.bn_conv1_3,input_pcl)
# マックスプーリング
# 最重要なポイント
# 対称関数の役割として実行。「Unordered(順不同)」の確保
input_pcl = self.MaxPool(input_pcl)
input_pcl = nn.Flatten(1)(input_pcl)
# 結合層
input_pcl = self.fc1_1(input_pcl)
input_pcl = self.filter_common(self.bn_fc1_1,input_pcl)
input_pcl = self.fc1_2(input_pcl)
input_pcl = self.filter_common(self.bn_fc1_2,input_pcl)
# 変換行列のk**2個の要素取得
trans_mat = self.fc1_3(input_pcl)
# k x k の変換行列に整形
trans_mat = trans_mat.view(-1, self.mat_dim, self.mat_dim)
# T-netもネットワークなので最適化が必要
# weightにたいして最適化を行う。
weight = torch.eye(self.mat_dim, requires_grad=True).repeat(input_pcl.shape[0], 1, 1)
trans_mat = trans_mat + weight
return trans_mat
def filter_common(self,bn,inputdata):
return F.relu((bn(inputdata)))
T-netの実装です。
最初は「論文にT-netの詳細のってないじゃねーか!」と激怒して、
論文作成者の方?のコードを読みに行きましたが、
やっていることの大半は基本的な畳み込みネットワークです。
おおまかな流れとしては、
畳み込み層を何重にもかけることで特徴を抽出し、
結合層で1つに統合しています。
特筆すべき箇所は、2つ。
1つは、Unordered(順不同)の確保。
具体的にはプーリング層の箇所です。
input_pcl = self.MaxPool(input_pcl)
プーリングを実行することで、論文の4-2章にある対称関数を表現しています。
これにより、Unordered(順不同)(同じ対象物の点群でも、内部で登録されている位置情報がバラバラでの問題)
を解決しています。
2つ目は、Invariance(不変性)の確保。
畳み込み・プーリング・結合の処理により、
各点群は等しい条件で特徴を抽出できました。
しかし、点群は同じ物体を表すデータでも回転・移動している場合があるので
このままでは特徴を計算しても異なっているケースが考えられます。
weight = torch.eye(self.mat_dim, requires_grad=True).repeat(input_pcl.shape[0], 1, 1)
trans_mat = trans_mat + weight
やりたいこととしては、
実は同じものである点群Aと点群Bがあったとき、この2つが同じものであるように
特徴を抽出できるようにパラメータを調整してやること、
まさしくニュートラルネットワークが得意とするところです。
なので、ここではweightという単位行列を定義します。
単位行列の剛体変換を掛け合わせても点群に変化はありませんが、
最適化を行いパラメータを調整していくことで、weightの数値が変化し、
最終的には点群Aと点群Bの特徴が同じになるパラメータ、すなわち剛体変換が
取得できるわけです。
このT-netはほかのことにも使えそうですね。
個人的に若干疑問なのは、3x3しか想定していないので移動行列は想定しないのかな?
というのが若干不思議です。
#②④⑥:多層パーセプトロン
mlp:多層パーセプトロンを定義します。
self.fc2_1 = nn.Linear(64, 64)
self.fc2_2 = nn.Linear(64, 128)
self.fc2_3 = nn.Linear(128, 1024)
画像に書かれている通りに、64,128,1924と線形変換します。
input_pcl = self.fc2_1(input_pcl)
input_pcl = self.filter_common(self.bn2_1,input_pcl)
def filter_common(self,bn,inputdata):
# BatchNormalをするために次元を一時的にかえる
if inputdata.ndim == 3:
inputdata = inputdata.transpose(1, 2)
inputdata = F.relu((bn(inputdata)))
inputdata = inputdata.transpose(1, 2)
if inputdata.ndim == 2:
inputdata = F.relu((bn(inputdata)))
return inputdata
注意点としては、
論文の「Figure 2. PointNet Architecture」に、
BtchNormakとReluを実行せよとあるので、その通りに実行します。
#⑤ :マックスプーリング
あら・・・またマックスプーリングが・・・。
# 5. マックスプーリング
input_pcl = self.MaxPool(input_pcl)
input_pcl = nn.Flatten(1)(input_pcl)
T-netの中で行ったマックスプーリングで、Unordered(順不同)の確保は
十分に行えていると認識していましたが、違うようです。
私の認識を整理します。
T-netのなかでおこなったマックスプーリングは、
あくまで各点群に対して行っています。あちらは「ローカル」な点群の特徴に対する処理です。
この処理により、不変性と同時に順不同性も獲得てきたと認識しています。
一方、こちらは入力した各点群のすべつのポイントを加味して特徴を統合している「グローバル」な特徴に対する処理です。
ここの処理の目的は、
膨大なグローバルなデータを扱う前に、次元数を削減している・・・・のかな?
と勝手に思っていますが正直断定できていません。
#⑦ :最後のレイヤー
# 7. 最後のmlp
input_pcl = self.fc3_2(input_pcl)
input_pcl = self.DropOut(input_pcl)
input_pcl = self.filter_common(self.bn3_2,input_pcl)
ほかの多層パーセプトロンとほとんど変わりありませんが、
論文の「Figure 2. PointNet Architecture」の通り、、
DropOutを実行します。
#学習部分
for i in range(epochs):
# 最適化処理
optimizer.zero_grad()
outputs,trans3x3,trans64x64 = PointNet(train_data)
# PointNet,input_transform,feature_transformに対して最適化を行う。
# 未実装
# optimizer.step()
学習部分を実装するのが時間的に難しく断念しました。
ポイントとしては、PointNetだけでなく、T-netも学習させないと不変性のための剛体変換ができないので、
そこもしっかりと実装しないといけないのだろうなと思います。
そのうち実装したいですね。