一、基本簡介
PointNet,PointNet++
- PointNet 論文翻譯,中英文對照以及關鍵點詳細解讀,請參考 。
- PointNet++ 論文翻譯,。
- PointNet,PointNet++ 論文作者的公開課鏈接為:(大佬的課必須去感受下啊~~)
二、PointNet
- 解決了什麼問題?
a. 由於卷積網絡在2D圖像上的興起,很多研究者開始將神經網絡應用於3D點雲數據。但是,大部分工作都是將3D點雲體素化或者轉換為多個視角的2D圖像,然後應用常規的卷積神經網絡。因此,PointNet 主要解決了如何將2D卷積神經網絡【直接】處理3D點雲本身,即使點雲出現波動,噪聲或者缺失的情況,也能穩定的提取點集特徵。
- 本篇論文的亮點?
a. 如何解決點雲無序性問題? 3D點雲一個重要的特徵是無序性,對於
個點的點集,它有
種輸入順序。那麼,針對每一種順序,如何保證網絡的學習結果保持不變?為此,本文提出對稱函數(最大池化層,Max-Pooling)來解決無序性的問題。同時,最大池化層將獨立學習的點特徵聚合為全局點集特徵,進而進行後續的3D識別任務。
b. 輸入點雲和特徵對齊模塊的具體網絡結構是什麼,有什麼作用?網絡的預測結果應該對特定的變換具有不變性,比如剛性變換。為此,本文提出 T-Net 變換矩陣,將輸入以及不同點的特徵進行對齊,使得網絡學習得到的表達也具有這種特性。下面的圖片是 T-Net 的具體結構,包括每一層的輸入和輸出。【注意,輸入點集對齊模塊和特徵對齊模塊的差別在於紅色標識部分的尺寸的差別。】 輸入點集對齊模塊如下圖所示,
錯誤修正:上圖的第三個卷積核修改為 1x1(1x3是錯誤的)特徵對齊模塊如下圖所示,
兩個變換模塊的訓練表現如下圖所示,
單獨使用任何一個模塊,網絡的精度提升非常有限。由於特徵對齊模塊的維度較大,需要添加正則化,才能保證訓練效果有所提升。同時使用連個模塊以及正則化,精度提升相對較大。
c. 網絡的穩定性 網絡對輸入點雲的波動,添加一定的噪聲點,以及刪除點集部分點等干擾操作,網絡能夠保證學習能力的魯棒性,也可以達到良好的預測效果。那麼,為何網絡具有一定的【穩定性】?參考下面的解釋。
給定一個無序的點集,可以定義一個函數集合
,將點集映射為一個向量:
![]()
這裏,通常是多層感知機網絡(MLP).
集合函數對輸入點集的順序具有不變性,並且可以擬合任何連續函數集(論文 PointNet 論述)。對於連續函數而言,微小的變動不會影響函數值。
被認為是單獨一個點(a point)的空間編碼,學習每一個點的特徵。
- 詳細的網絡結構
論文中大致的網絡的結構如下,分類和分割任務的主幹網絡基本一樣,差別在於後面網絡的處理,
PointNet 分類網絡如下圖所示,
PointNet Part分割網絡如下圖所示,應注意以下幾點:(a)特徵變換矩陣後的兩層卷積的核輸出分別為512,2048,論文是128,512;(b)concate 模塊中的16,指示着數據集目標總共的類別,有助於提升效果;(c)倒數第三、第四層網絡添加了 Dropout層。(d)特徵轉換矩陣的大小為128,大於分類的轉換矩陣64.
- 損失函數
(A) 分類任務
損失函數包括兩個部分,分別是常規的交叉熵(cross_entropy)分類損失,以及特徵變換矩陣的損失函數。因為特徵變換矩陣的維度較大,為了能夠保證網絡的訓練效果,需要添加正則項(上文有相關介紹)。損失函數的代碼實現如下代碼片段所示,
def get_loss(pred, label, end_points, reg_weight=0.001):
""" pred: B*NUM_CLASSES, label: B, """
print('classify loss compute================>\n')
loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=pred, labels=label)
classify_loss = tf.reduce_mean(loss)
tf.summary.scalar('classify loss', classify_loss)
# Enforce the transformation as orthogonal matrix
transform = end_points['transform'] # BxKxK
K = transform.get_shape()[1].value
mat_diff = tf.matmul(transform, tf.transpose(transform, perm=[0,2,1]))
mat_diff -= tf.constant(np.eye(K), dtype=tf.float32)
mat_diff_loss = tf.nn.l2_loss(mat_diff)
tf.summary.scalar('mat loss', mat_diff_loss)
return classify_loss + mat_diff_loss * reg_weight
按照論文的陳述,約束生成的特徵矩陣為正交矩陣,相應的損失函數設計如下,
正交矩陣的性質1:如果
,或者
,
是單位矩陣,那麼是矩陣
是正交矩陣。
正交矩陣的性質2:設為
階實矩陣,則
為正交矩陣的充分必要條件是其列(行)向量組是標準正交向量組。
對於性質2可以推斷,如果特徵變換矩陣是正交矩陣,它的行或列為正交向量組,那麼不同的輸入點集經過特徵轉換後,更能表達學習的特徵空間,也就是表達性更強。
(B) 分割任務
Part分割的損失函數同樣包含兩個部分,第一部分是分類損失,這一部分損失被置零,不參與訓練。第二部分是Part類別損失,其實計算的仍是輸入點集中的每一個點的語義分類。第三部分是關於特徵變換矩陣的損失(分類任務已陳述,兩個特徵矩陣僅僅是尺寸的差別)。
def get_loss(l_pred, seg_pred, label, seg, weight, end_points):
per_instance_label_loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=l_pred, labels=label)
label_loss = tf.reduce_mean(per_instance_label_loss)
# size of seg_pred is batch_size x point_num x part_cat_num(50)
# size of seg is batch_size x point_num
per_instance_seg_loss = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(logits=seg_pred, labels=seg), axis=1)
print('per_instance_seg_loss: ', per_instance_seg_loss)
seg_loss = tf.reduce_mean(per_instance_seg_loss)
per_instance_seg_pred_res = tf.argmax(seg_pred, 2)
# Enforce the transformation as orthogonal matrix
transform = end_points['transform'] # BxKxK
K = transform.get_shape()[1].value
mat_diff = tf.matmul(transform, tf.transpose(transform, perm=[0,2,1])) - tf.constant(np.eye(K), dtype=tf.float32)
mat_diff_loss = tf.nn.l2_loss(mat_diff)
total_loss = weight * seg_loss + (1 - weight) * label_loss + mat_diff_loss * 1e-3
return total_loss, label_loss, per_instance_label_loss, seg_loss, per_instance_seg_loss, per_instance_seg_pred_res
- 數據集
ModelNet40:用於分類的CAD模型,總共40類。12311個CAD模型,9843個用於訓練,2468個用於測試。下載官方的開源代碼,如下圖所示,運行【train.py】腳本,會自動下載數據(~416M,可能需要才能下載)。
ShapeNet Part:該數據集用於Part分割,總共有16881個分塊形狀,總共16類,50個不同的分塊,每一個對象標註了大概2-5個分塊。如下圖所示,所有的資源均在文件夾【./part_seg]中,
Part分割任務的訓練流程:按照下圖中官網的步驟就可以訓練了,大概要下載2個數據。這個數據可能也得才能下載。
- 訓練過程,參數設置
具體的訓練設置,可以參考開源代碼,都是常規的設置,這裏不贅述了。
- 論文不足
論文最大的不足是不能很好的提取局部點雲特徵。從分割網絡的設計而言,特徵合併模塊(直接 concate)也過於簡單。
三、PointNet++
- 論文主要為了解決什麼問題?
a. 由於 PointNet 不能很好地提取局部精細的特徵,那麼無法應用於需要識別精細特徵的識別任務,比如語義分割。為此,本文主要是為了解決直接提取3D點雲特徵的過程中,如何能夠更好的提取【不同尺度下局部】的精細特徵。
b. 相比於之前的 PointNet 網絡,本文的 PointNet++
- 論文的關鍵點有哪些?
下圖是 PointNet++ 網絡的一部分結構,也是最為核心的結構——分層次的點集抽象層(Hierarchical Set Abstraction)。所謂的分層,也就是使用多個 SA (Set Abstraction)模塊進行特徵提取,差別在於每一個 SA 模塊的採樣點數量(K)和採樣半徑(R)不一樣。隨着層次加深,K和R會隨之增大。
SA模塊是該論文最為核心的部分,它包含三個關鍵層:
Sampling Layer,
Grouping Layer,
PointNet Layer。每一個模塊的作用,請參考下面【a,b,c】的分析。【d】介紹瞭如何解決密度不均勻的特徵學習問題。
a. 如何劃分相互重疊的局部鄰域?
Sampling Layer:首先,通過最遠點採樣算法(Furthest Point Sample,FPS),在輸入點集中均勻採樣固定數量的點,記為C。
Grouping Layer:然後,以點集C中的點作為中心點,在一定半徑R內,取出K個點,此時完成了對輸入點集的單尺度局部鄰域劃分。那麼輸入點集就被劃為為空間上很多個相互重疊的球鄰域,每一個球鄰域就是一個局部點集。輸出的形狀為,其中 B 是輸入的 batch,N 為中心點的數量,K 是每一個鄰域球中點的數量,3表示x,y,z通道的座標。
下面的代碼片段給出了一個SA模塊的相關參數設置,包括中心點個數,局部鄰域的半徑大小,局部鄰域內採樣點數量,以及 PointNet 層的參數等等。
# 單尺度 SA 模塊
l1_xyz, l1_points, l1_indices =pointnet_sa_module(l0_xyz, l0_points, # 輸入點集以及特徵
npoint=512, # 中心點的採樣數量
radius=0.2, nsample=32, # 鄰域劃分參數
mlp=[64,64,128], # pointnet 輸出通道數量
mlp2=None,
group_all=False,
is_training=is_training,
bn_decay=bn_decay,
scope='layer1',
use_nchw=True)
b. 如何學習局部鄰域特徵?
PointNet Layer:輸入點集經過前面兩層的劃分,得到N個空間球鄰域,然後將其輸入到 PointNet Layer,學習局部鄰域的點集特徵。以【a】中的代碼片段的SA的參數為例(第一個SA模塊),經過MLP,Max-Pooling的處理,得到相應的點集特徵。點集特徵提取的代碼片段如下所示,
# new_points of group layer: Tensor("layer1/sub:0", shape=(16, 512, 32, 3),
# dtype=float32, device=/device:GPU:0)
for i, num_out_channel in enumerate(mlp):
new_points = tf_util.conv2d(new_points, num_out_channel, [1,1],
padding='VALID', stride=[1,1],
bn=bn, is_training=is_training,
scope='conv%d'%(i), bn_decay=bn_decay,
data_format=data_format)
# output:
# new_points after mlp: Tensor("layer1/transpose_1:0", shape=(16, 512, 32, 128),
# dtype=float32, device=/device:GPU:0)
if pooling=='max':
new_points = tf.reduce_max(new_points, axis=[2], keep_dims=True, name='maxpool')
print('new_points after of max-pooling: ', new_points)
# output
# new_points after of max-pooling: Tensor("layer1/maxpool:0", shape=(16, 512, 1, 128),
# dtype=float32, device=/device:GPU:0)
還需注意的是,在 Grouping Layer 通常會將上一層的特徵與當前的局部鄰域劃分點集進行連接(concate),組成新的特徵(也即是將xyz通道融合進特徵向量),進而作為 PointNet Layer 的輸入,參考下面的代碼片段中的tf.concat(),
if points is not None:
print('kkkkkkk')
grouped_points = group_point(points, idx) # (batch_size, npoint, nsample, channel)
if use_xyz:
# (batch_size, npoint, nample, 3+channel)
new_points = tf.concat([grouped_xyz, grouped_points], axis=-1)
else:
new_points = grouped_points
else:
print('xxxxxxx')
new_points = grouped_xyz
c. PointNet 用於提取局部鄰域的點集特徵,沒有特別複雜的結構,具體結構可以參考下面【3. 詳細的網絡結構】部分的結構圖。在這一層,輸入為
個局部鄰域點集,大小為
。每一個局部鄰域的輸出是被其中心和局部特徵(編碼了中心的局部鄰域特徵)抽象,輸出大小為
.
局部鄰域的點座標首先被轉換為相對於中心點的局部結構:
![]()
這裏,是中心點的座標。我們使用 PointNet 作為學習局部特徵的基礎模塊。通過將點的相對座標以及點特徵連接而形成新的特徵,並作為 PointNet 的輸入,我們可以提取局部區域內點與點之間的相互關係。
d. 如何解決密度變化對特徵學習的影響? 在3D點雲中,不同區域的密度變化時非常普遍的現象,為了保證特徵學習的有效性。本文提出兩種密度自適應層來解決該問題,分別為 MSG 和 MRG 方法。
MSG(Multi-Scale Grouping) 該方法思想很簡單,就是在一個局部鄰域中心點添加多個尺度,然後將不同尺度下的局部鄰域特徵連接為一個特徵,作為當前SA模塊提取的點集特徵。
# 單個SA模塊的多尺度參數
l1_xyz, l1_points = pointnet_sa_module_msg(l0_xyz, l0_points, # 輸入點集和特徵
512, # 中心點數量
[0.1,0.2,0.4], # 鄰域的半徑的大小(多尺度)
[16,32,128], # 對應三個尺度(半徑)下的局部採樣點數量
[[32,32,64], [64,64,128], [64,96,128]], # 輸出通道數量
is_training, bn_decay, scope='layer1', use_nchw=True)
MRG(Multi-Resolution Grouping) 上面的MSG方法的計算代價會非常高。為此,提出MRG方法,
層的特徵由兩個向量組成:第一個向量為
- 詳細的網絡結構
a. 分類網絡結構
ModelNet-40分類的網絡結構如圖所示,網絡結構圖為單尺度版本(每個SA只設置了一個半徑值)。各種顏色的的框表示模塊的類別,前後箭頭之上分別都標示了輸入和輸出的形狀。由於繪圖空間有限,輸入和輸出只標出了SA輸出的特徵向量形狀。實際上,每個SA模塊的輸入和輸出有2個向量,分別是中心點的座標(BxNx3)以及相應的點特徵向量。要注意的是,這裏的點特徵向量不再是單獨一箇中心點的特徵,而是以該點為中心的鄰域特徵。b. 分割網絡的一些細節 相比於 PointNet 簡單將淺層網絡和深層網絡特徵進行連接,PointNet++ 提出新的融合特徵的方法(或者説更為有效的方法),逆距離權重法(IDW,Inverse Distance Weight)。 IDW:該方法是一種插值算法,簡言之,就是將已知點的特徵通過插值的形式傳遞到目標點。具體在本文中,就是將個點特徵傳遞給
個點,由於 SA模塊降採樣,所有
. 該插值算法的具體公式如下圖所示,
上圖的公式中,
![]()
是計算點權重,它與距離成反比,距離越近,影響越大。通常,
![]()
稱為未知點的值,
![]()
是已知點的值,k 表示在已知點集中取 k 個點進行插值計算,p 表示距離對權重的影響程度。
具體到本文的算法中,取 p=2,k=3,也就是從已知點集中取出最近的3(
參數 k)個點進行插值計算。比如,
![]()
層有1024個點,
![]()
採樣後剩下 512個點,那麼如何將512個點的特徵傳遞給1024個點呢?簡單説,就是1024中的每一個點,從512個點中選取最近的三個點,然後根據最近的3個點進行插值,得到新的特徵。計算方式是依照上面的公式,作者的開源代碼中也實現了該算法,具體細節可以參考代碼。
網絡結構參考如下:
與分類網絡的差別在於FP之後的結果,其它均是一樣的結構,更為詳細的結構,可以參考代碼,這裏不在贅述。
上圖分割網絡的3個FP模塊用於上採樣,那麼具體實現細節是什麼樣的呢?下面,我們將單獨解析該模塊:
作用:由於分割需要確定每個點的分類,所以就需要得的特徵,
- 損失函數
兩個任務都是分類問題,所以用的是交叉熵損失函數,這裏不在贅述,我之前的博文也詳細介紹了此損失函數的理論和應用細節。博文參考鏈接:交叉熵損失函數理論以及tensorflow應用。
- 數據集介紹
數據集與 PointNet 使用的是一樣的數據,並且
ModelNet40和ShapeNet並不是實際場景掃描數據,不在具體敍述。
- 訓練流程
a. 訓練環境配置由於 PointNet++ 的開源代碼中添加了使用C++、Cuda編寫的採樣層,分組層,插值層,因此,需要單獨編譯這幾個接口。編譯步驟如下:首先,如果是用conda安裝開發環境,那麼需要激活深度學習環境(tensorflow)。其次,如下圖所示,進入相應的【3d_interpolation,grouping,sampling】文件夾,更改sh文件的配置,見下面的代碼片段,主要是更改tenssorflow的相關路徑。最後,運行sh **_compile.sh腳本,如果沒有問題則會生成相應的【.so】文件。
編譯過程中,修改tensorflow路徑如下代碼所示,如果找不到tensorflow的【include,lib】路徑,可以執行相關代碼,打印路徑即可,代碼如下【
import tensorflow as tf; print(tf.sysconfig.get_include()),import tensorflow as tf; print(tf.sysconfig.get_include())】,
#/bin/bash/usr/local/cuda-10.2/bin/nvcc tf_grouping_g.cu -o tf_grouping_g.cu.o -c -O2 -DGOOGLE_CUDA=1 -x cu -Xcompiler -fPIC
# TF1.2
# g++ -std=c++11 tf_grouping.cpp tf_grouping_g.cu.o -o tf_grouping_so.so -shared -fPIC -I /usr/local/lib/python2.7/dist- packages/tensorflow/include -I /usr/local/cuda-8.0/include -lcudart -L /usr/local/cuda-8.0/lib64/ -O2 -D_GLIBCXX_USE_CXX11_ABI=0
# TF1.4
g++ -std=c++11 tf_grouping.cpp tf_grouping_g.cu.o -o tf_grouping_so.so -shared -fPIC -I /home/slam/anaconda3/envs/TF1.4/lib/python3.6/site-packages/tensorflow/include -I /usr/local/cuda-10.2/include -I /home/slam/anaconda3/envs/TF1.4/lib/python3.6/site-packages/tensorflow/include/external/nsync/public -lcudart -L /usr/local/cuda-10.2/lib64/ -L/home/slam/anaconda3/envs/TF1.4/lib/python3.6/site-packages/tensorflow -ltensorflow_framework -O2 -D_GLIBCXX_USE_CXX11_ABI=0
我的編譯環境,tensorflow-1.4,python3.6,Ubuntu-18,cuda-10.2,編譯過程中,遇到如下圖所示的問題。從錯誤提示,明顯可以知道是系統的【gcc,g++】版本太高,因此需要安裝更低版本的編譯器(
如何安裝,自行百度,比較簡單),
根據上述錯誤提示,建立如下的軟鏈接,
sudo ln -s gcc-5 gcc(將系統的gcc版本改為gcc-5)sudo ln -s g++-5 g++
按照上述更改,依然會報同樣的錯誤,為此按照下面的代碼片段的操作,繼續更改軟鏈接。之所以進行下面的更改,應該是當初系統安裝cuda的時候,gcc、g++默認使用的系統的gcc、g++版本太高導致。
sudo ln -s /usr/bin/gcc-5 /usr/local/cuda/bin/gcc(更改cuda中gcc的版本)sudo ln -s /usr/bin/g++-5 /usr/local/cuda/bin/g++
b. 訓練流程
- 論文不足
暫時略