因为部分原因slam的学习估计是要推迟了。重新好好学习下pointnet.
pointnet是一篇2017年由斯坦福大学的Charles等人在CVPR上发表的文章,这个网络直接对点云进行处理,而不是像很多的网络那样,转换成图像,或者类似图像的结构后,再用处理图像的方法进行处理。学会pointnet在我看来有相当大的必要性。
pointnet论文网址
很简单的网络结构,输入n*3的点云数据->T-Net->mlp(64,64)->mlp(64,128,1024)->maxpool->mlp(512,256,k)->output.
这部分是重点学的,因为pytorch有挺多地方是不懂的,也准备通过pointnet的实现来学习pytorch。
gethub上有挺多大佬对pointnet进行复现大佬链接
系统,用的windows,虽然也可以用虚拟机的ubuntu,但是我windows上已经装好了pytorch等等的,不想花时间重新装pytorch等等的。
电脑上有80G的KITTI的数据集,但是那个太大了,训练时间太长,也不便于学习,用的就是shapenet的数据集,在大佬的github里面,是用download.sh写的一个脚本文件,运行脚本文件下载数据,但是我用windows,跟ubuntu有差异,就直接在网址下载数据,只有600+MB,挺小的。
在大佬的github中,首先就是克隆代码,然后进入目录后再安装。来看下setup.py文件
需要到torch库,tqdm库,plyfile库。先把这几个库安装好,在ubuntu下直接跟着大佬走就行,在windows下,新建一个虚拟环境,conda install … 就行了。
然后是下载数据跟建立可视化模块。下载数据,我们通过网址直接下载,可视化模块,我准备用opnen3d来弄,就先不管。
接下来就是utils文件夹中的train_classification.py和train_segmentation.py。先看分类,然后再看分割的。
在train_classification.py
from __future__ import print_function
import argparse
import os
import random
import torch
import torch.nn.parallel
import torch.optim as optim
import torch.utils.data
from pointnet.dataset import ShapeNetDataset, ModelNetDataset
from pointnet.model import PointNetCls, feature_transform_regularizer
import torch.nn.functional as F
from tqdm import tqdm
这部分就是导入库的模块,比较值得关注的是pointnet.dataset和pointnet.model这两个库,其他的库都是开源库,直接安装就好了,这两个库都是本地库,从命名上看,一个是对数据进行预处理的库,一个就是pointnet的模型结构库,先一个一个看,先看建立数据的库。从导入来看,是在pointnet这个文件夹下的dataset.py文件。
from __future__ import print_function
import torch.utils.data as data
import os
import os.path
import torch
import numpy as np
import sys
from tqdm import tqdm
import json
from plyfile import PlyData, PlyElement
导入模块,这部分导入模块就没有本地库这类了。
在train_classification.py的导入模块中是import ShapeNetDataset, ModelNetDataset,我们实验的数据集是shapenet的数据集,这里就只看shapenetdataset,在dataset.py中找到ShapeNetDataset部分。
class ShapeNetDataset(data.Dataset):
def __init__(self,
root,
npoints=2500,
classification=False,
class_choice=None,
split='train',
data_augmentation=True):
self.npoints = npoints
self.root = root
self.catfile = os.path.join(self.root, 'synsetoffset2category.txt')
self.cat = {}
self.data_augmentation = data_augmentation
self.classification = classification
self.seg_classes = {}
with open(self.catfile, 'r') as f:
for line in f:
ls = line.strip().split()
self.cat[ls[0]] = ls[1]
#print(self.cat)
if not class_choice is None:
self.cat = {k: v for k, v in self.cat.items() if k in class_choice}
self.id2cat = {v: k for k, v in self.cat.items()}
self.meta = {}
splitfile = os.path.join(self.root, 'train_test_split', 'shuffled_{}_file_list.json'.format(split))
#from IPython import embed; embed()
filelist = json.load(open(splitfile, 'r'))
for item in self.cat:
self.meta[item] = []
for file in filelist:
_, category, uuid = file.split('/')
if category in self.cat.values():
self.meta[self.id2cat[category]].append((os.path.join(self.root, category, 'points', uuid+'.pts'),
os.path.join(self.root, category, 'points_label', uuid+'.seg')))
self.datapath = []
for item in self.cat:
for fn in self.meta[item]:
self.datapath.append((item, fn[0], fn[1]))
self.classes = dict(zip(sorted(self.cat), range(len(self.cat))))
print(self.classes)
with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../misc/num_seg_classes.txt'), 'r') as f:
for line in f:
ls = line.strip().split()
self.seg_classes[ls[0]] = int(ls[1])
self.num_seg_classes = self.seg_classes[list(self.cat.keys())[0]]
print(self.seg_classes, self.num_seg_classes)
def __getitem__(self, index):
fn = self.datapath[index]
cls = self.classes[self.datapath[index][0]]
point_set = np.loadtxt(fn[1]).astype(np.float32)
seg = np.loadtxt(fn[2]).astype(np.int64)
#print(point_set.shape, seg.shape)
choice = np.random.choice(len(seg), self.npoints, replace=True)
#resample
point_set = point_set[choice, :]
point_set = point_set - np.expand_dims(np.mean(point_set, axis = 0), 0) # center
dist = np.max(np.sqrt(np.sum(point_set ** 2, axis = 1)),0)
point_set = point_set / dist #scale
if self.data_augmentation:
theta = np.random.uniform(0,np.pi*2)
rotation_matrix = np.array([[np.cos(theta), -np.sin(theta)],[np.sin(theta), np.cos(theta)]])
point_set[:,[0,2]] = point_set[:,[0,2]].dot(rotation_matrix) # random rotation
point_set += np.random.normal(0, 0.02, size=point_set.shape) # random jitter
seg = seg[choice]
point_set = torch.from_numpy(point_set)
seg = torch.from_numpy(seg)
cls = torch.from_numpy(np.array([cls]).astype(np.int64))
if self.classification:
return point_set, cls
else:
return point_set, seg
def __len__(self):
return len(self.datapath)
首先是__int__函数,这个函数中导入参数root,npoints,classification,class_choice,split, data_augmentation这几个变量,除了root之外,都是有默认参数的。在进行初始化时候,有一个参数值得注意下,catfile参数,这个参数是在root路径中synsetoffset2category.txt的路径。
然后读取synsetoffset2category.txt中的数据,放到self.cat中。
with open(self.catfile, 'r') as f:
for line in f:
ls = line.strip().split()
self.cat[ls[0]] = ls[1]
如果class_choice不为空的话,即为选取了种类,sel.cat只存储选取的种类。
if not class_choice is None:
self.cat = {k: v for k, v in self.cat.items() if k in class_choice}
1、self.id2cat中把字典的key跟value转换,变成id为key,种类为value。
2、定义路径splitfile,为root目录下的train_test_split文件夹下的shuffled_train_file_list.json文件或者shuffled_test_file_list.json文件,因为split取值就这两个,默认的split为train.
3、读取splitfile路径下的数据到filelist中
4、读取训练数据的路径到meta中
5、给每条训练数据都加上标签后再存储到datapath中
self.id2cat = {v: k for k, v in self.cat.items()}
self.meta = {}
splitfile = os.path.join(self.root, 'train_test_split', 'shuffled_{}_file_list.json'.format(split))
#from IPython import embed; embed()
filelist = json.load(open(splitfile, 'r'))
for item in self.cat:
self.meta[item] = []
for file in filelist:
_, category, uuid = file.split('/')
if category in self.cat.values():
self.meta[self.id2cat[category]].append((os.path.join(self.root, category, 'points', uuid+'.pts'),
os.path.join(self.root, category, 'points_label', uuid+'.seg')))
self.datapath = []
for item in self.cat:
for fn in self.meta[item]:
self.datapath.append((item, fn[0], fn[1]))
把num_seg_classes.txt中的数据存入seg_classes中,至于num_seg_classes就是在cat中第一个类别Airplane在seg_classes中的id,怎么感觉这个num_seg_classes没太大必要呢?
self.classes = dict(zip(sorted(self.cat), range(len(self.cat))))
print(self.classes)
with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../misc/num_seg_classes.txt'), 'r') as f:
for line in f:
ls = line.strip().split()
self.seg_classes[ls[0]] = int(ls[1])
self.num_seg_classes = self.seg_classes[list(self.cat.keys())[0]]
print(self.seg_classes, self.num_seg_classes)
接下来是__getitem__函数,这个函数是类成员函数,是一个带参函数,point_set中存储点的空间坐标,seg中存储每个点的类别。choice中随机选取npoints个数据的引导,把选好的数据更新在point_set中,再对point_set进行归一化。theta为从0-2pi中随机取的一个值,rotation_matrix为二维空间的旋转矩阵,给point_set中的点一个绕z轴的随机旋转。再给point_set中的每个点一个随机抖动,每个点的标签seg也是变成选择好后对应点的标签,接着把point_set,seg,cls都变成张量
def __getitem__(self, index):
fn = self.datapath[index]
cls = self.classes[self.datapath[index][0]]
point_set = np.loadtxt(fn[1]).astype(np.float32)
seg = np.loadtxt(fn[2]).astype(np.int64)
#print(point_set.shape, seg.shape)
choice = np.random.choice(len(seg), self.npoints, replace=True)
#resample
point_set = point_set[choice, :]
point_set = point_set - np.expand_dims(np.mean(point_set, axis = 0), 0) # center
dist = np.max(np.sqrt(np.sum(point_set ** 2, axis = 1)),0)
point_set = point_set / dist #scale
if self.data_augmentation:
theta = np.random.uniform(0,np.pi*2)
rotation_matrix = np.array([[np.cos(theta), -np.sin(theta)],[np.sin(theta), np.cos(theta)]])
point_set[:,[0,2]] = point_set[:,[0,2]].dot(rotation_matrix) # random rotation
point_set += np.random.normal(0, 0.02, size=point_set.shape) # random jitter
seg = seg[choice]
point_set = torch.from_numpy(point_set)
seg = torch.from_numpy(seg)
cls = torch.from_numpy(np.array([cls]).astype(np.int64))
if self.classification:
return point_set, cls
else:
return point_set, seg
最后这个类成员函数,只是为了得到数据的大小,datapath中存储着每条数据的路径。
def __len__(self):
return len(self.datapath)
看完dataset部分的代码,还有另一个本地库,就是model部分,导入了PointNetCls和feature_transform_regularizer,我们也是先只看这两部分。
头文件部分,这部分也没本地库这类,略过。
from __future__ import print_function
import torch
import torch.nn as nn
import torch.nn.parallel
import torch.utils.data
from torch.autograd import Variable
import numpy as np
import torch.nn.functional as F
这部分就是PointNetCls,这个类继承torch.nn.Model类,可以带两个参数输入,都有默认参数,至于torch.nn.Model类里面有哪些函数,有哪些参数,就去查文档吧。pytorch官网中文文档主页
这里面涉及到了一个新的类PointNetfeat,先去看下这个类。
class PointNetCls(nn.Module):
def __init__(self, k=2, feature_transform=False):
super(PointNetCls, self).__init__()
self.feature_transform = feature_transform
self.feat = PointNetfeat(global_feat=True, feature_transform=feature_transform)
self.fc1 = nn.Linear(1024, 512)
self.fc2 = nn.Linear(512, 256)
self.fc3 = nn.Linear(256, k)
self.dropout = nn.Dropout(p=0.3)
self.bn1 = nn.BatchNorm1d(512)
self.bn2 = nn.BatchNorm1d(256)
self.relu = nn.ReLU()
def forward(self, x):
x, trans, trans_feat = self.feat(x)
x = F.relu(self.bn1(self.fc1(x)))
x = F.relu(self.bn2(self.dropout(self.fc2(x))))
x = self.fc3(x)
return F.log_softmax(x, dim=1), trans, trans_feat
在PointNetfeat里又涉及一个STN3d类。。。吐了吐了,还是通过网络结构来看代码吧,这样一点一点回溯。挺麻烦的。
class PointNetfeat(nn.Module):
def __init__(self, global_feat = True, feature_transform = False):
super(PointNetfeat, self).__init__()
self.stn = STN3d()
self.conv1 = torch.nn.Conv1d(3, 64, 1)
self.conv2 = torch.nn.Conv1d(64, 128, 1)
self.conv3 = torch.nn.Conv1d(128, 1024, 1)
self.bn1 = nn.BatchNorm1d(64)
self.bn2 = nn.BatchNorm1d(128)
self.bn3 = nn.BatchNorm1d(1024)
self.global_feat = global_feat
self.feature_transform = feature_transform
if self.feature_transform:
self.fstn = STNkd(k=64)
def forward(self, x):
n_pts = x.size()[2]
trans = self.stn(x)
x = x.transpose(2, 1)
x = torch.bmm(x, trans)
x = x.transpose(2, 1)
x = F.relu(self.bn1(self.conv1(x)))
if self.feature_transform:
trans_feat = self.fstn(x)
x = x.transpose(2,1)
x = torch.bmm(x, trans_feat)
x = x.transpose(2,1)
else:
trans_feat = None
pointfeat = x
x = F.relu(self.bn2(self.conv2(x)))
x = self.bn3(self.conv3(x))
x = torch.max(x, 2, keepdim=True)[0]
x = x.view(-1, 1024)
if self.global_feat:
return x, trans, trans_feat
else:
x = x.view(-1, 1024, 1).repeat(1, 1, n_pts)
return torch.cat([x, pointfeat], 1), trans, trans_feat
下图是pointnet的网络结构,在模型搭建中,首先调用的是PointNetCls,PointNetCls先调用PointNetfeat进行网络搭建后,经过一个1024,512的全连接层,再经过一个512,256的全连接层,最后再经过一个256,k的全连接层,结束。对应的就是网络结构中最后的那个mlp(512,256,k),调用PointNetfeat搭建前面那一长串的神经网络,接着看PointNetfeat中,调用STN3d,一维卷积???为什么会是一维卷积呢?因为似乎在官方的tensorflow中是二维卷积,疑惑中,百度找到知乎上有个大佬的解读以及对一维卷积的理解大佬链接看完后有点不能理解计算时候怎么得到的结果,因为在我的理解里是1x3x2500,表示图像大小为1x3,然后有2500个输出通道,但是转置后就变成1x2500x3,就是图像大小为1x2500,有3个输出通道,然后是用输入通道为3,输出通道为64,卷积核大小为1x1的卷积进行卷积,原图像3个输出通道,卷积核输入通道为3,就是对应相乘再相加,但是算出来的结果跟大佬算出的结果不一样啊,0.1x0.3+0.3x1.3+0.5x0.2=0.52,不是1.11啊。头皮发麻,看了一天资料了,希望有大佬解答!!!!!
接下来T-NET的结果与原矩阵相乘,这个T-NET就是STN3d,然后又是一个卷积核大小为1,输入通道为3,输出通道为64的卷积层,虽然这个1x1的卷积也相当于一个全连接层。不过按道理说,mlp(64,64)是否还需要一个输入通道为64,输出通道为64,大小为1的卷积层?后面的也是,是否总共少了两个输入通道为64,输出通道为64,大小为1的卷积层?(PS:等会自己加两层跑跑看,对比下)这里有点懵,也希望有大佬能告诉下。
dataser部分,model部分后,还有feature_transform_regularizer部分,感觉这部分没什么好说的,对其中的两个维度分别求二范数,然后平均值作为正则化项。
def feature_transform_regularizer(trans):
d = trans.size()[1]
batchsize = trans.size()[0]
I = torch.eye(d)[None, :, :]
if trans.is_cuda:
I = I.cuda()
loss = torch.mean(torch.norm(torch.bmm(trans, trans.transpose(2,1)) - I, dim=(1,2)))
return loss
分类部分的导入库部分说过,接下来就是定义参数部分,我是在jupyter上运行的,这个参数部分需要做部分更改,不然会报错,第一,把opt = parser.parse_args()改成opt = parser.parse_args([]),然后在dataset参数中,把required=True这个删除,改成default=‘数据的路径’,就是修改默认路径。也可以改成空,运行的时候更改。
parser = argparse.ArgumentParser()
parser.add_argument(
'--batchSize', type=int, default=32, help='input batch size')
parser.add_argument(
'--num_points', type=int, default=2500, help='input batch size')
parser.add_argument(
'--workers', type=int, help='number of data loading workers', default=4)
parser.add_argument(
'--nepoch', type=int, default=250, help='number of epochs to train for')
parser.add_argument('--outf', type=str, default='cls', help='output folder')
parser.add_argument('--model', type=str, default='', help='model path')
parser.add_argument('--dataset', type=str, required=True, help="dataset path")
parser.add_argument('--dataset_type', type=str, default='shapenet', help="dataset type shapenet|modelnet40")
parser.add_argument('--feature_transform', action='store_true', help="use feature transform")
opt = parser.parse_args()
随机生成一个整数作为随机种子。如果dataset_type是shapenet就调用ShapeNetDataset建立数据,如果是modelnet40就调用ModelNetDataset,默认的是shapenet,我也只下了shapenet的数据做实验。ShapeNetDataset在dataset部分也讲过。如果两者都不是,则输入wrong dataset type。
opt.manualSeed = random.randint(1, 10000) # fix seed
print("Random Seed: ", opt.manualSeed)
random.seed(opt.manualSeed)
torch.manual_seed(opt.manualSeed)
if opt.dataset_type == 'shapenet':
dataset = ShapeNetDataset(
root=opt.dataset,
classification=True,
npoints=opt.num_points)
test_dataset = ShapeNetDataset(
root=opt.dataset,
classification=True,
split='test',
npoints=opt.num_points,
data_augmentation=False)
elif opt.dataset_type == 'modelnet40':
dataset = ModelNetDataset(
root=opt.dataset,
npoints=opt.num_points,
split='trainval')
test_dataset = ModelNetDataset(
root=opt.dataset,
split='test',
npoints=opt.num_points,
data_augmentation=False)
else:
exit('wrong dataset type')
把训练用的数据和实验用的数据载入pytorch框架中。
dataloader = torch.utils.data.DataLoader(
dataset,
batch_size=opt.batchSize,
shuffle=True,
num_workers=int(opt.workers))
testdataloader = torch.utils.data.DataLoader(
test_dataset,
batch_size=opt.batchSize,
shuffle=True,
num_workers=int(opt.workers))
在当前目录下新建一个名为cls的文件夹(PS:outf的默认参数就是cls),然后调用 PointNetCls运行网络,用adam算法代替随机梯度下降,然后动态更新学习率,每step_size个epoch做一次更新,更新因子为0.5。调用.cuda()放到GPU上。
try:
os.makedirs(opt.outf)
except OSError:
pass
classifier = PointNetCls(k=num_classes, feature_transform=opt.feature_transform)
if opt.model != '':
classifier.load_state_dict(torch.load(opt.model))
optimizer = optim.Adam(classifier.parameters(), lr=0.001, betas=(0.9, 0.999))
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=20, gamma=0.5)
classifier.cuda()
dataloader就是我们要训练的数据,然后循环每条数据,把数据放到gpu上就开始训练,每10个数据,就测试下,每训练完一个epoch,就保存下模型。
for epoch in range(opt.nepoch):
scheduler.step()
for i, data in enumerate(dataloader, 0):
points, target = data
target = target[:, 0]
points = points.transpose(2, 1)
points, target = points.cuda(), target.cuda()
optimizer.zero_grad()
classifier = classifier.train()
pred, trans, trans_feat = classifier(points)
loss = F.nll_loss(pred, target)
if opt.feature_transform:
loss += feature_transform_regularizer(trans_feat) * 0.001
loss.backward()
optimizer.step()
pred_choice = pred.data.max(1)[1]
correct = pred_choice.eq(target.data).cpu().sum()
print('[%d: %d/%d] train loss: %f accuracy: %f' % (epoch, i, num_batch, loss.item(), correct.item() / float(opt.batchSize)))
if i % 10 == 0:
j, data = next(enumerate(testdataloader, 0))
points, target = data
target = target[:, 0]
points = points.transpose(2, 1)
points, target = points.cuda(), target.cuda()
classifier = classifier.eval()
pred, _, _ = classifier(points)
loss = F.nll_loss(pred, target)
pred_choice = pred.data.max(1)[1]
correct = pred_choice.eq(target.data).cpu().sum()
print('[%d: %d/%d] %s loss: %f accuracy: %f' % (epoch, i, num_batch, blue('test'), loss.item(), correct.item()/float(opt.batchSize)))
torch.save(classifier.state_dict(), '%s/cls_model_%d.pth' % (opt.outf, epoch))
训练好模型后,最后测试,开始计算准确度。
total_correct = 0
total_testset = 0
for i,data in tqdm(enumerate(testdataloader, 0)):
points, target = data
target = target[:, 0]
points = points.transpose(2, 1)
points, target = points.cuda(), target.cuda()
classifier = classifier.eval()
pred, _, _ = classifier(points)
pred_choice = pred.data.max(1)[1]
correct = pred_choice.eq(target.data).cpu().sum()
total_correct += correct.item()
total_testset += points.size()[0]
print("final accuracy {}".format(total_correct / float(total_testset)))
代码部分结束,也就开始实验了,用的win10+jupyter,部分东西需要改下,比如路径参数这类的,我就直接弄成默认参数了,然后jupyter跟直接运行python可能代码方面有部分不同,比如参数那,用python,opt = parser.parse_args()不会报错,用jupyter,opt = parser.parse_args()会报错,至于为什么,表示没找到答案,只能解释为底层那些很复杂的东西结构不一样。
用新建一个jupyter文件,复制粘贴train_classification文件中的内容,改下dataset的路径,然后改下opt = parser.parse_args([])开始慢慢跑,等结果。
训练结束,效果还是挺好的,97.9%。
接下来来看看segmentation,训练开始,依然是把batchsize从32改成了16。。
椅子的分割结果,78.2%,感觉完全达到想象中的标准,不过这个epoch太小了,只有25,改成100再尝试下。
椅子分割结果:88.7%,感觉OK,看样子是因为25还是太小了,导致训练不充分,这次为了训练速度快一点,把batchsize改成了32。
汽车分割结果:76.4%,感觉也挺好的。
接下来实验下昨天想的,加两层感知机。依然用car分割来实验吧,因为数据量小,训练速比较快。
复制粘贴model.py文件,改名成model_test.py,然后在PointNetfeat类中新增以下画圈部分类内容:
把train_segmentation文件中的from pointnet.model import PointNetDenseCls, feature_transform_regularizer改成从model_test中导入:
继续跑看结果。。
结果:效果非常之差,是因为参数增多,数据量太少导致过拟合吗?
为了验证这个问题,输出训练时候的loss跟准确度。为了训练速度更快一点,epoch改为50。继续训练:
训练结果,训练的loss是0.2,但是测试的loss是0.8.。。。。。
用分类的跑了下,依然是这个问题,训练损失在慢慢下降,但是测试损失不下降。。。哎,加了两层网络就直接过拟合了。。。