云自編碼與潛空間GAN生成實(shí)戰(zhàn):從PointNet到ShapeNet)
簡介這份資源面向計(jì)算機(jī)視覺與深度學(xué)習(xí)方向的學(xué)習(xí)者和研究者聚焦用自動(dòng)編碼器實(shí)現(xiàn)3D點(diǎn)云的壓縮、重建與生成。內(nèi)容基于latent_3d_points項(xiàng)目涵蓋自編碼器、變分自編碼器與生成對抗網(wǎng)絡(luò)等模型涉及點(diǎn)云預(yù)處理、潛空間特征提取、損失函數(shù)設(shè)計(jì)與評估指標(biāo)計(jì)算適合具備Python與TensorFlow基礎(chǔ)、希望深入點(diǎn)云生成的中高級(jí)讀者。壓縮包共44個(gè)文件約2.1MB以24個(gè)py源碼和4個(gè)ipynb交互式筆記為核心輔以sh數(shù)據(jù)下載腳本、cpp與cu底層實(shí)現(xiàn)、md說明文檔及少量配置與圖片文件結(jié)構(gòu)清晰便于按模塊研讀。已有107人學(xué)習(xí)下載。通過源碼與筆記讀者可掌握編碼器-解碼器架構(gòu)搭建、潛變量操作、GAN與VAE訓(xùn)練流程并借助評估腳本驗(yàn)證生成點(diǎn)云質(zhì)量是理解點(diǎn)云內(nèi)在結(jié)構(gòu)、降低計(jì)算復(fù)雜度并復(fù)現(xiàn)生成實(shí)驗(yàn)的實(shí)用參考。1. 從一份能跑通的 3D 點(diǎn)云自編碼工程說起如果你正在找一份能直接跑起來的 3D 點(diǎn)云生成代碼而不是又一篇只講公式的論文復(fù)現(xiàn)筆記那這份latent_3d_points-master值得花一個(gè)下午拆一遍。它用 Python 和 Jupyter Notebook 把「點(diǎn)云自編碼 潛空間 GAN 生成」這條鏈路完整串了起來point_net_ae.py負(fù)責(zé)把無序點(diǎn)云壓進(jìn)低維潛向量latent_gan.py在潛空間里做對抗生成train_single_class_ae.ipynb和train_latent_gan.ipynb則是可以直接點(diǎn)開就執(zhí)行的訓(xùn)練入口。整套代碼基于 TensorFlow 1.x 的靜態(tài)圖風(fēng)格寫成配合download_data.sh拉取 ShapeNet 子集覆蓋了從數(shù)據(jù)預(yù)處理、模型搭建、訓(xùn)練到compute_evaluation_metrics.ipynb評估的完整閉環(huán)。適合已經(jīng)寫過基礎(chǔ) Python、想切入 3D 深度學(xué)習(xí)但被點(diǎn)云無序性和生成模型調(diào)參卡住的人。下面我按「它是什么 → 怎么跑 → 坑在哪 → 怎么改」的順序把這份資源拆開講。2. 點(diǎn)云自編碼器的結(jié)構(gòu)拆解從 PointNet 編碼器到潛空間約束2.1 為什么點(diǎn)云不能直接塞進(jìn)普通自編碼器3D 點(diǎn)云本質(zhì)上是一個(gè) (N \times 3) 的矩陣N 個(gè)點(diǎn)每個(gè)點(diǎn)三個(gè)坐標(biāo)。問題在于這個(gè)矩陣的行順序是任意的——你把同一朵點(diǎn)云的點(diǎn)打亂它表示的還是同一個(gè)物體。普通全連接自編碼器把輸入當(dāng)成固定維度的向量一旦點(diǎn)序變化重構(gòu)目標(biāo)就變了學(xué)出來的編碼器對旋轉(zhuǎn)、平移、點(diǎn)序都不魯棒。這份工程的做法是引入 PointNet 風(fēng)格的編碼器。核心思路是用共享權(quán)重的多層感知機(jī)MLP逐點(diǎn)提特征再用一個(gè)對稱函數(shù)max pooling把 N 個(gè)點(diǎn)的特征聚合成一個(gè)全局向量。對稱函數(shù)保證了輸出與輸入點(diǎn)序無關(guān)這是點(diǎn)云編碼器能成立的前提。encoders_decoders.py里的Encoder和Decoder類就是干這個(gè)的編碼器輸出一個(gè)bottleneck_size維的潛向量解碼器再從潛向量回歸出 N 個(gè)點(diǎn)的坐標(biāo)。我一般會(huì)把bottleneck_size設(shè)成 128 或 256。太小比如 32會(huì)丟失細(xì)節(jié)重構(gòu)出來的椅子缺腿太大比如 1024潛空間冗余后面做 GAN 時(shí)判別器很難區(qū)分真假訓(xùn)練容易崩。這個(gè)參數(shù)在ae_templates.py里以配置字典的形式給出改起來不用動(dòng)模型代碼。2.2 編碼器與解碼器的關(guān)鍵實(shí)現(xiàn)先看編碼器的骨架這是理解整份代碼的入口# encoders_decoders.py 中 Encoder 的核心邏輯簡化示意 def encoder_mlp(point_cloud, is_training, bn_decay, scope, bnTrue): # point_cloud: [B, N, 3] net tf_util.conv2d(point_cloud, 128, [1, 3], paddingVALID, stride[1, 1], bnbn, is_trainingis_training, scopeconv1, bn_decaybn_decay) net tf_util.conv2d(net, 256, [1, 1], ...) # 逐點(diǎn)提特征 net tf_util.conv2d(net, 512, [1, 1], ...) net tf.reduce_max(net, axis1, keep_dimsTrue) # 對稱聚合消除點(diǎn)序影響 return net邏輯說明conv2d的卷積核是[1, 3]和[1, 1]等價(jià)于對每個(gè)點(diǎn)獨(dú)立做全連接權(quán)重在所有點(diǎn)上共享。reduce_max沿點(diǎn)數(shù)維度取最大值這一步是點(diǎn)云編碼的靈魂——無論點(diǎn)怎么排列max 的結(jié)果不變。參數(shù)上三層通道數(shù) 128/256/512 是 PointNet 原文的經(jīng)典配置顯存吃緊可以整體砍半。解碼器反過來從潛向量z出發(fā)先全連接擴(kuò)維再 reshape 成點(diǎn)特征最后回歸坐標(biāo)。generators_discriminators.py里的生成器復(fù)用了同樣的解碼結(jié)構(gòu)這也是為什么潛空間 GAN 能直接接在自編碼器后面——生成器和解碼器共享同一套「潛向量到點(diǎn)云」的映射假設(shè)。2.3 訓(xùn)練入口與數(shù)據(jù)準(zhǔn)備數(shù)據(jù)靠download_data.sh拉取腳本里用的是 ShapeNet 的單類子集比如 chair、car。執(zhí)行前先確認(rèn)external/structural_losses和external/python_plyfile兩個(gè)子模塊已經(jīng)初始化否則in_out.py讀.ply文件時(shí)會(huì)直接報(bào)ImportError。# 初始化子模塊并下載數(shù)據(jù) git submodule update --init --recursive bash download_data.shdownload_data.sh內(nèi)部會(huì)調(diào)用wget或curl拉取壓縮包并解壓到data/目錄。如果你的網(wǎng)絡(luò)環(huán)境訪問原始地址慢可以手動(dòng)下載后放到對應(yīng)路徑腳本里判斷文件是否存在的邏輯會(huì)跳過重復(fù)下載。數(shù)據(jù)就位后打開train_single_class_ae.ipynb按順序執(zhí)行單元格即可。Notebook 里把超參、路徑、訓(xùn)練輪數(shù)都寫成了變量改完直接重跑比改.py腳本再命令行啟動(dòng)直觀得多。3. 潛空間 GAN 的生成鏈路從 ae 權(quán)重到新點(diǎn)云采樣3.1 為什么在潛空間做 GAN 而不是直接生成點(diǎn)云直接在點(diǎn)云空間做 GAN 是可行的但判別器要處理 (N \times 3) 的高維輸出訓(xùn)練極不穩(wěn)定模式坍塌幾乎是必然。這份工程選了一條更聰明的路先用自編碼器把點(diǎn)云壓到低維潛空間再在這個(gè)低維空間里訓(xùn)練 GAN。潛空間維度低128 或 256數(shù)據(jù)分布簡單GAN 容易收斂生成的新潛向量再喂給解碼器就能得到新點(diǎn)云。代價(jià)是生成質(zhì)量受限于自編碼器的重構(gòu)能力。如果自編碼器本身重構(gòu)就糊潛空間 GAN 生成的點(diǎn)云只會(huì)更糊。所以正確順序是先單獨(dú)把train_single_class_ae.ipynb訓(xùn)到重構(gòu)損失收斂再拿它的編碼器權(quán)重去初始化train_latent_gan.ipynb。latent_gan.py里加載 ae 權(quán)重的邏輯在main函數(shù)開頭路徑寫死在配置里換數(shù)據(jù)集時(shí)記得同步改。3.2 訓(xùn)練潛空間 GAN 的步驟# latent_gan.py 訓(xùn)練主循環(huán)的關(guān)鍵片段簡化示意 for epoch in range(num_epochs): for batch in dataset: z_real encoder(batch) # 真實(shí)點(diǎn)云編碼到潛空間 z_fake generator.sample(batch_size) # 生成器采樣假潛向量 d_loss discriminator_loss(z_real, z_fake) g_loss generator_loss(z_fake) train_op_d.run(d_loss) train_op_g.run(g_loss)邏輯說明encoder在這里是凍結(jié)的只用來把真實(shí)點(diǎn)云轉(zhuǎn)成潛向量不參與梯度更新。判別器discriminator在潛空間里區(qū)分真?zhèn)紊善鱣enerator學(xué)的是潛空間的數(shù)據(jù)分布。參數(shù)上w_gan_gp.py實(shí)現(xiàn)了 WGAN-GP 的梯度懲罰比原始 GAN 的 JS 散度穩(wěn)定得多l(xiāng)ambda_gp一般設(shè) 10。raw_gan.py和vanilla_gan.py是消融對比用的原始版本實(shí)際訓(xùn)練建議直接用w_gan_gp.py。訓(xùn)練輪數(shù)上潛空間 GAN 收斂比自編碼器快通常幾百個(gè) epoch 就能看到像樣的生成結(jié)果。判別器和生成器的學(xué)習(xí)率不要設(shè)成一樣常見做法是判別器 1e-4、生成器 1e-4但更新頻率上判別器每步更新、生成器隔步更新能緩解判別器過強(qiáng)導(dǎo)致的梯度消失。3.3 評估指標(biāo)怎么讀compute_evaluation_metrics.ipynb里實(shí)現(xiàn)了 Chamfer DistanceCD和 Earth Movers DistanceEMD兩個(gè)點(diǎn)云相似度指標(biāo)。CD 計(jì)算兩組點(diǎn)云互相最近鄰距離的均值對點(diǎn)序不敏感計(jì)算快EMD 找的是最優(yōu)傳輸匹配更準(zhǔn)但慢得多N 大時(shí)基本跑不動(dòng)。evaluation_metrics.py里兩個(gè)都有實(shí)現(xiàn)日常調(diào)參看 CD 就夠了最終報(bào)告再補(bǔ) EMD。讀指標(biāo)時(shí)注意CD 的絕對值沒有意義只有同一數(shù)據(jù)集、同一歸一化方式下的相對比較才有意義。我見過有人拿不同尺度點(diǎn)云算出的 CD 直接對比結(jié)論完全反了。歸一化方式在general_utils.py的normalize_point_cloud里默認(rèn)把點(diǎn)云縮放到單位球內(nèi)換數(shù)據(jù)集時(shí)確認(rèn)這一步?jīng)]被跳過。4. 避坑與排查這份代碼在真實(shí)環(huán)境里會(huì)翻車的幾個(gè)地方4.1 現(xiàn)象Notebook 一執(zhí)行就報(bào)ModuleNotFoundError: No module named tf_util原因src/目錄沒有加入 Python 路徑。這份工程用的是相對導(dǎo)入Notebook 的工作目錄如果不是src/的父目錄導(dǎo)入鏈就斷了。解決在 Notebook 第一個(gè)單元格里顯式加路徑或者把工作目錄切到工程根目錄再啟動(dòng) Jupyter。import sys, os sys.path.append(os.path.abspath(./src)) sys.path.append(os.path.abspath(./external/structural_losses))4.2 現(xiàn)象訓(xùn)練損失一直是nan原因TensorFlow 1.x 的靜態(tài)圖里學(xué)習(xí)率設(shè)太大或者梯度沒做裁剪WGAN-GP 的梯度懲罰項(xiàng)容易爆。另外點(diǎn)云坐標(biāo)沒歸一化也會(huì)導(dǎo)致輸入量級(jí)過大。解決確認(rèn)normalize_point_cloud被調(diào)用把學(xué)習(xí)率降到 1e-4 以下在優(yōu)化器里加tf.clip_by_norm梯度裁剪。tf_utils.py里有現(xiàn)成的optimizer封裝檢查beta1是不是 0.5 而不是默認(rèn)的 0.9GAN 訓(xùn)練用 0.5 更穩(wěn)。4.3 現(xiàn)象download_data.sh執(zhí)行完data/目錄還是空的原因腳本里的下載地址失效或者wget沒裝。腳本沒有對下載失敗做顯式報(bào)錯(cuò)靜默失敗后解壓步驟也跳過。解決手動(dòng)確認(rèn)壓縮包是否下載成功檢查文件大小是否為 0。沒有wget就裝一個(gè)或者改用curl -O。數(shù)據(jù)解壓后的目錄結(jié)構(gòu)要和in_out.py里讀取路徑的假設(shè)一致通常是data/category/split/*.ply。4.4 現(xiàn)象生成的點(diǎn)云全是噪點(diǎn)看不出形狀原因潛空間 GAN 訓(xùn)練時(shí)判別器過強(qiáng)生成器梯度消失或者自編碼器本身沒訓(xùn)好解碼器輸出的點(diǎn)云就是散的。解決先單獨(dú)驗(yàn)證自編碼器的重構(gòu)效果把train_single_class_ae.ipynb訓(xùn)到重構(gòu)點(diǎn)云能看出物體輪廓再往下走。潛空間 GAN 階段降低判別器更新頻率或者給生成器加更小的學(xué)習(xí)率。WGAN-GP 的lambda_gp調(diào)大到 10 以上也能壓制判別器。4.5 現(xiàn)象換自己的.ply數(shù)據(jù)后讀取報(bào)錯(cuò)原因python_plyfile子模塊沒初始化或者自己的.ply文件用了非標(biāo)準(zhǔn)的屬性字段。這份代碼假設(shè)點(diǎn)云只有x, y, z三個(gè)屬性帶法線或顏色的文件會(huì)解析失敗。解決先git submodule update --init拉全子模塊。自己的數(shù)據(jù)先用python_plyfile讀一遍確認(rèn)屬性列表只有坐標(biāo)多余的字段在預(yù)處理階段刪掉。in_out.py里的read_ply函數(shù)可以加一層字段過濾。5. 進(jìn)階改造把單類自編碼器擴(kuò)到多類與條件生成跑通單類之后最自然的下一步是讓它支持多類點(diǎn)云。ae_templates.py里的配置是按類別寫死的擴(kuò)多類需要改兩處數(shù)據(jù)加載部分按類別打標(biāo)簽?zāi)P筒糠衷诰幋a器輸出后拼接一個(gè)類別 one-hot 向量。這樣潛空間就變成了「類別條件潛空間」生成時(shí)指定類別就能采樣出對應(yīng)形狀。# 條件編碼器示意潛向量拼接類別標(biāo)簽 z encoder(point_cloud) # [B, bottleneck] z_cond tf.concat([z, one_hot_label], 1) # [B, bottleneck num_classes] # 解碼器和判別器同步接收 z_cond參數(shù)上類別數(shù)多的時(shí)候 one-hot 維度會(huì)膨脹可以換成可學(xué)習(xí)的 embedding維度設(shè) 16 或 32 就夠。判別器也要相應(yīng)改成條件判別器輸入潛向量和類別標(biāo)簽判斷「這個(gè)類別的這個(gè)潛向量是否真實(shí)」。驗(yàn)證改造是否成功別只看損失曲線。我習(xí)慣固定一組隨機(jī)種子每訓(xùn)練 50 個(gè) epoch 就采樣一批潛向量解碼出來用compute_evaluation_metrics.ipynb算 CD同時(shí)肉眼看形狀。CD 下降但形狀變糊的情況我遇到過不止一次指標(biāo)和肉眼必須一起看。從那以后我每次改完模型結(jié)構(gòu)都強(qiáng)制走一遍「固定種子采樣 CD 肉眼」三件套再?zèng)Q定要不要繼續(xù)訓(xùn)。希望這份拆解幫到你少走幾個(gè)我踩過的彎路。本文還有配套的精品資源點(diǎn)擊獲取