想盡快入門點(diǎn)云,因此就從這個(gè)經(jīng)典的點(diǎn)云處理神經(jīng)網(wǎng)絡(luò)開始。源碼已經(jīng)有了中文注釋,但在一些對(duì)于自己不理解的地方添加了一些注釋。歡迎大家一起討論。
代碼是來自github:GitHub - yanx27/Pointnet_Pointnet2_pytorch: PointNet and PointNet++ implemented by pytorch (pure python) and on ModelNet, ShapeNet and S3DIS.
PointNet系列代碼復(fù)現(xiàn)詳解(2)—PointNet++part_seg_葭月甘九的博客-CSDN博客
先學(xué)習(xí)的是分類部分代碼
train_classification.py
下面代碼就是獲取當(dāng)前文件所在的路徑,賦值給BASE_DIR
。ROOT_DIR
被賦值為BASE_DIR
,表示當(dāng)前文件所在的目錄為根目錄。將models
目錄添加到根目錄下,并使用sys.path.append()
將該路徑添加到Python解釋器的搜索路徑中,以便于在程序中導(dǎo)入models
目錄下的模塊和類。
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
ROOT_DIR = BASE_DIR
sys.path.append(os.path.join(ROOT_DIR, 'models'))
下面就是一些命令行參數(shù),設(shè)置一些訓(xùn)練網(wǎng)絡(luò)的基本參數(shù)
比如是否使用GPU,訓(xùn)練批次大小,模型,訓(xùn)練總輪數(shù),以及優(yōu)化器,訓(xùn)練日志保存路徑等等。具體看代碼后。
def parse_args():
'''PARAMETERS'''
parser = argparse.ArgumentParser('training')
parser.add_argument('--use_cpu', action='store_true', default=False, help='use cpu mode')
parser.add_argument('--gpu', type=str, default='0', help='specify gpu device')
parser.add_argument('--batch_size', type=int, default=24, help='batch size in training')
parser.add_argument('--model', default='pointnet_cls', help='model name [default: pointnet_cls]')
parser.add_argument('--num_category', default=40, type=int, choices=[10, 40], help='training on ModelNet10/40')
parser.add_argument('--epoch', default=200, type=int, help='number of epoch in training')
parser.add_argument('--learning_rate', default=0.001, type=float, help='learning rate in training')
parser.add_argument('--num_point', type=int, default=1024, help='Point Number')
parser.add_argument('--optimizer', type=str, default='Adam', help='optimizer for training')
parser.add_argument('--log_dir', type=str, default=None, help='experiment root')
parser.add_argument('--decay_rate', type=float, default=1e-4, help='decay rate')
parser.add_argument('--use_normals', action='store_true', default=False, help='use normals')
parser.add_argument('--process_data', action='store_true', default=False, help='save data offline')
parser.add_argument('--use_uniform_sample', action='store_true', default=False, help='use uniform sampiling')
return parser.parse_args()
--use_cpu
:是否使用CPU模式。--gpu
:指定GPU設(shè)備的編號(hào)。--batch_size
:訓(xùn)練時(shí)的批大小。--model
:指定使用的模型名稱。--num_category
:指定數(shù)據(jù)集的類別數(shù),可選值為10和40。--epoch
:訓(xùn)練的輪數(shù)。--learning_rate
:學(xué)習(xí)率。--num_point
:點(diǎn)云中的點(diǎn)數(shù)。--optimizer
:優(yōu)化器類型,默認(rèn)為Adam。--log_dir
:實(shí)驗(yàn)的根目錄。--decay_rate
:衰減率。--use_normals
:是否使用法向量。--process_data
:是否將數(shù)據(jù)離線保存。--use_uniform_sample
:是否使用均勻采樣策略
下面就是主函數(shù)里網(wǎng)絡(luò)訓(xùn)練的設(shè)置
1.log_string(str)用于記錄訓(xùn)練數(shù)據(jù),然后是讀取命令行參數(shù),調(diào)用gpu
def log_string(str):
logger.info(str)
print(str)
'''調(diào)用顯卡 gpu'''
os.environ["CUDA_VISIBLE_DEVICES"] = args.gpu
下面就是創(chuàng)建訓(xùn)練記錄文件夾,記錄訓(xùn)練過程的信息?
'''CREATE DIR'''
# 創(chuàng)建文件夾 記錄信息
timestr = str(datetime.datetime.now().strftime('%Y-%m-%d_%H-%M')) # 獲取當(dāng)前時(shí)間并轉(zhuǎn)換為標(biāo)準(zhǔn)字符串(年-月-日-時(shí)-分)
exp_dir = Path('./log/') # 使用 Path 類創(chuàng)建一個(gè)路徑對(duì)象 exp_dir,指定日志文件存儲(chǔ)的根目錄為 './log/'
exp_dir.mkdir(exist_ok=True) # 目錄存在正常返回,不存在創(chuàng)建
exp_dir = exp_dir.joinpath('classification') # 在 exp_dir 變量所代表的目錄路徑下創(chuàng)建一個(gè)名為 'classification' 的子目錄
exp_dir.mkdir(exist_ok=True)
if args.log_dir is None:
exp_dir = exp_dir.joinpath(timestr)
else:
exp_dir = exp_dir.joinpath(args.log_dir)
exp_dir.mkdir(exist_ok=True)
checkpoints_dir = exp_dir.joinpath('checkpoints/')
checkpoints_dir.mkdir(exist_ok=True)
log_dir = exp_dir.joinpath('logs/')
log_dir.mkdir(exist_ok=True)
'''LOG 日志記錄'''
args = parse_args()
logger = logging.getLogger("Model") # 創(chuàng)建了一個(gè)名為 "Model" 的日志記錄器 logger
logger.setLevel(logging.INFO) # 設(shè)置了日志記錄器 logger 的日志級(jí)別為 INFO,即只記錄 INFO 級(jí)別及以上的日志信息。
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s') # 日志格式化器 設(shè)置日志記錄的格式。 時(shí)間-記錄器名稱-日志級(jí)別-內(nèi)容
file_handler = logging.FileHandler('%s/%s.txt' % (log_dir, args.model)) # 文件處理器,用于將日志信息寫入到文件中
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
log_string('PARAMETER ...')
log_string(args)
數(shù)據(jù)讀取
'''DATA LOADING'''
log_string('Load dataset ...')
data_path = 'data/modelnet40_normal_resampled/'
train_dataset = ModelNetDataLoader(root=data_path, args=args, split='train', process_data=args.process_data)
test_dataset = ModelNetDataLoader(root=data_path, args=args, split='test', process_data=args.process_data)
# 分批訓(xùn)練數(shù)據(jù) 打亂輸入的數(shù)據(jù) 開4線程 可丟棄一些數(shù)據(jù)
trainDataLoader = torch.utils.data.DataLoader(train_dataset, batch_size=args.batch_size, shuffle=True,
num_workers=4, drop_last=True)
# 分批測(cè)試數(shù)據(jù) 不打亂輸入的數(shù)據(jù) 開4線程
testDataLoader = torch.utils.data.DataLoader(test_dataset, batch_size=args.batch_size, shuffle=False,
num_workers=4)
?下面代碼就是把訓(xùn)練的模型復(fù)制到對(duì)應(yīng)的目錄下,以便以后查看和對(duì)比,然后就獲取對(duì)應(yīng)的分類模型以及損失函數(shù),激活函數(shù)。
'''MODEL LOADING '''
num_class = args.num_category
model = importlib.import_module(args.model)
shutil.copy('./models/%s.py' % args.model, str(exp_dir))
shutil.copy('models/pointnet2_utils.py', str(exp_dir))
shutil.copy('./train_classification.py', str(exp_dir))
# 定義了模型、損失函數(shù)和激活函數(shù)。
classifier = model.get_model(num_class, normal_channel=args.use_normals)
criterion = model.get_loss()
classifier.apply(inplace_relu)
?使用gpu訓(xùn)練,并且查看是否有預(yù)訓(xùn)練模型。
# gpu訓(xùn)練
if not args.use_cpu:
classifier = classifier.cuda()
criterion = criterion.cuda()
try:
checkpoint = torch.load(str(exp_dir) + '/checkpoints/best_model.pth')
start_epoch = checkpoint['epoch']
classifier.load_state_dict(checkpoint['model_state_dict']) # 將模型的參數(shù)設(shè)置為加載的狀態(tài)字典
log_string('Use pretrain model')
except:
log_string('No existing model, starting training from scratch...') # 無預(yù)訓(xùn)模型
start_epoch = 0
這里就是優(yōu)化器選擇,以及一些優(yōu)化器參數(shù)的設(shè)置
# 優(yōu)化器
if args.optimizer == 'Adam':
optimizer = torch.optim.Adam(
classifier.parameters(),
lr=args.learning_rate,
betas=(0.9, 0.999),
eps=1e-08,
weight_decay=args.decay_rate
)
else:
optimizer = torch.optim.SGD(classifier.parameters(), lr=0.01, momentum=0.9)
# 調(diào)度器 防止陷入訓(xùn)練循環(huán)
# 將 optimizer 設(shè)置為之前定義的 Adam 優(yōu)化器,step_size 設(shè)置為 20,gamma 設(shè)置為 0.7,表示每隔 20 個(gè) epoch,將學(xué)習(xí)率乘以 0.7 進(jìn)行調(diào)整。
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=20, gamma=0.7)
global_epoch = 0
global_step = 0
best_instance_acc = 0.0
best_class_acc = 0.0
下面就是主要訓(xùn)練網(wǎng)絡(luò)片段?,可以查看注釋。上面一些基本工作準(zhǔn)備完成后,開始訓(xùn)練。主要就是先記錄一下訓(xùn)練輪次等基本信息,開啟訓(xùn)練模式,更新學(xué)習(xí)率,然后開始一輪訓(xùn)練,優(yōu)化器清零,然后數(shù)據(jù)增強(qiáng),然后進(jìn)行訓(xùn)練,一輪訓(xùn)練結(jié)束后會(huì)進(jìn)行一次檢測(cè),最后檢測(cè)結(jié)果與以前訓(xùn)練數(shù)據(jù)進(jìn)行比較,保存最好的那個(gè)。
'''TRANING'''
logger.info('Start training...')
for epoch in range(start_epoch, args.epoch):
log_string('Epoch %d (%d/%s):' % (global_epoch + 1, epoch + 1, args.epoch))
mean_correct = [] # 存儲(chǔ)每個(gè) batch 中預(yù)測(cè)正確的樣本數(shù)
classifier = classifier.train() # 訓(xùn)練模式
# 更新當(dāng)前的學(xué)習(xí)率。在每個(gè) epoch 結(jié)束時(shí),調(diào)用 scheduler.step() 方法,將當(dāng)前 epoch 的信息傳遞給學(xué)習(xí)率調(diào)度器,從而更新當(dāng)前的學(xué)習(xí)率。
scheduler.step()
# tqdm進(jìn)度條
for batch_id, (points, target) in tqdm(enumerate(trainDataLoader, 0), total=len(trainDataLoader),
smoothing=0.9):
# 優(yōu)化器清零
optimizer.zero_grad()
# 數(shù)據(jù)增強(qiáng)
points = points.data.numpy()
points = provider.random_point_dropout(points) # 隨機(jī)點(diǎn)丟失
points[:, :, 0:3] = provider.random_scale_point_cloud(points[:, :, 0:3]) # 隨機(jī)縮放
points[:, :, 0:3] = provider.shift_point_cloud(points[:, :, 0:3]) # 隨機(jī)偏移
points = torch.Tensor(points) # 將 points 轉(zhuǎn)換為 Tensor
points = points.transpose(2, 1) # (24,1024,4)->(24,3,1024) 轉(zhuǎn)置
# 用于檢查是否使用CPU模式,如果沒有指定使用CPU模式,則將點(diǎn)云數(shù)據(jù)和目標(biāo)值加載到GPU上進(jìn)行訓(xùn)練。
if not args.use_cpu:
points, target = points.cuda(), target.cuda()
pred, trans_feat = classifier(points)
loss = criterion(pred, target.long(), trans_feat)
pred_choice = pred.data.max(1)[1]
# 準(zhǔn)確率 可以使用sklearn
correct = pred_choice.eq(target.long().data).cpu().sum()
mean_correct.append(correct.item() / float(points.size()[0]))
loss.backward()
optimizer.step()
global_step += 1
train_instance_acc = np.mean(mean_correct)
log_string('Train Instance Accuracy: %f' % train_instance_acc)
# 模式測(cè)試 classifier.eval()用于將模型設(shè)置為評(píng)估模式
with torch.no_grad():
instance_acc, class_acc = test(classifier.eval(), testDataLoader, num_class=num_class)
# 保存訓(xùn)練參數(shù) 通用寫法
if (instance_acc >= best_instance_acc):
best_instance_acc = instance_acc
best_epoch = epoch + 1
if (class_acc >= best_class_acc):
best_class_acc = class_acc
log_string('Test Instance Accuracy: %f, Class Accuracy: %f' % (instance_acc, class_acc))
log_string('Best Instance Accuracy: %f, Class Accuracy: %f' % (best_instance_acc, best_class_acc))
if (instance_acc >= best_instance_acc):
logger.info('Save model...')
savepath = str(checkpoints_dir) + '/best_model.pth'
log_string('Saving at %s' % savepath)
state = {
'epoch': best_epoch,
'instance_acc': instance_acc,
'class_acc': class_acc,
'model_state_dict': classifier.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
}
torch.save(state, savepath)
global_epoch += 1
?然后這是測(cè)試部分,部分代碼解釋見后面:
def test(model, loader, num_class=40):
mean_correct = []
class_acc = np.zeros((num_class, 3))
classifier = model.eval() # 模型設(shè)置為評(píng)估模式
for j, (points, target) in tqdm(enumerate(loader), total=len(loader)):
if not args.use_cpu:
points, target = points.cuda(), target.cuda()
points = points.transpose(2, 1) # 將點(diǎn)云數(shù)據(jù)的坐標(biāo)軸從(x,y,z)轉(zhuǎn)換為(x,z,y)的順序,這是因?yàn)樵邳c(diǎn)云數(shù)據(jù)處理中,通常將y軸作為垂直方向
pred, _ = classifier(points)
pred_choice = pred.data.max(1)[1]
for cat in np.unique(target.cpu()):
classacc = pred_choice[target == cat].eq(target[target == cat].long().data).cpu().sum()
class_acc[cat, 0] += classacc.item() / float(points[target == cat].size()[0])
class_acc[cat, 1] += 1
correct = pred_choice.eq(target.long().data).cpu().sum()
mean_correct.append(correct.item() / float(points.size()[0]))
class_acc[:, 2] = class_acc[:, 0] / class_acc[:, 1]
class_acc = np.mean(class_acc[:, 2])
instance_acc = np.mean(mean_correct)
return instance_acc, class_acc
代碼首先使用np.unique()
方法獲取目標(biāo)值中的不同類別。代碼先通過target == cat
選出該類別對(duì)應(yīng)的樣本,然后使用pred_choice[target == cat]
獲取分類器在該類別上的預(yù)測(cè)結(jié)果,target[target == cat].long().data
獲取該類別中所有樣本的目標(biāo)值,并使用eq()
方法比較分類器的預(yù)測(cè)結(jié)果和目標(biāo)值是否相等。接著,使用cpu().sum()
方法計(jì)算分類正確的樣本數(shù),再除以該類別中的總樣本數(shù),即可得到分類器在該類別上的準(zhǔn)確率。最后,將該類別的準(zhǔn)確率和樣本數(shù)量保存到class_acc
數(shù)組中。其中,class_acc
是一個(gè)二維數(shù)組,其形狀為(num_class, 2),表示每個(gè)類別的準(zhǔn)確率和樣本數(shù)量。第一列表示每個(gè)類別的準(zhǔn)確率,第二列表示每個(gè)類別中的總樣本數(shù)。
for cat in np.unique(target.cpu()):
classacc = pred_choice[target == cat].eq(target[target == cat].long().data).cpu().sum()
class_acc[cat, 0] += classacc.item() / float(points[target == cat].size()[0])
class_acc[cat, 1] += 1
unqiue()示例:
arr = np.array([1, 2, 3, 2, 4, 5, 4, 6])
unique_arr = np.unique(arr)
print(unique_arr)
結(jié)果:[1 2 3 4 5 6]
?pointnet_cls.py
下面就是分類的整個(gè)網(wǎng)絡(luò),第一個(gè)if判斷用于根據(jù)是否包含法向量信息來確定輸入數(shù)據(jù)的通道數(shù)。具體而言,如果normal_channel
為True
,則輸入數(shù)據(jù)包含法向量信息,通道數(shù)為6;否則,輸入數(shù)據(jù)不包含法向量信息,通道數(shù)為3。之后就是一些基本網(wǎng)絡(luò)組成塊。在前向傳播中,首先先進(jìn)行從輸入點(diǎn)云數(shù)據(jù)中提取特征,其中通過global_feat=True
指定輸出全局特征,即對(duì)輸入點(diǎn)云數(shù)據(jù)進(jìn)行全局特征池化;通過feature_transform=True
指定使用特征變換模塊,即對(duì)提取出的特征進(jìn)行空間變換,增強(qiáng)模型的魯棒性;通過channel=channel
指定輸入數(shù)據(jù)的通道數(shù),即根據(jù)輸入數(shù)據(jù)是否包含法向量信息來確定通道數(shù)。獲取到特征后就是全連接層,最后輸出的是類別。
class get_model(nn.Module):
def __init__(self, k=40, normal_channel=True):
super(get_model, self).__init__()
if normal_channel:
channel = 6
else:
channel = 3
self.feat = PointNetEncoder(global_feat=True, feature_transform=True, channel=channel)
self.fc1 = nn.Linear(1024, 512)
self.fc2 = nn.Linear(512, 256)
self.fc3 = nn.Linear(256, k)
self.dropout = nn.Dropout(p=0.4)
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)
x = F.log_softmax(x, dim=1)
return x, trans_feat
損失函數(shù)? 交叉熵?fù)p失+正交化規(guī)范處理的損失
class get_loss(torch.nn.Module):
def __init__(self, mat_diff_loss_scale=0.001):
super(get_loss, self).__init__()
self.mat_diff_loss_scale = mat_diff_loss_scale
def forward(self, pred, target, trans_feat):
loss = F.nll_loss(pred, target)
mat_diff_loss = feature_transform_reguliarzer(trans_feat)
total_loss = loss + mat_diff_loss * self.mat_diff_loss_scale
return total_loss
?pointnet_utils.py
?下面就是特征提取的代碼
class PointNetEncoder(nn.Module):
def __init__(self, global_feat=True, feature_transform=False, channel=3):
super(PointNetEncoder, self).__init__()
self.stn = STN3d(channel)
self.conv1 = torch.nn.Conv1d(channel, 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):
B, D, N = x.size()
trans = self.stn(x)
x = x.transpose(2, 1) # 交換2,3維
# 判斷D的大小是因?yàn)樵谑褂每臻g變換網(wǎng)絡(luò)(STN)對(duì)輸入圖像進(jìn)行變換時(shí),只需要對(duì)圖像的空間維度進(jìn)行變換,而不需要對(duì)通道維度進(jìn)行變換。
# 因此,如果輸入圖像的通道數(shù)大于3,則需要將通道數(shù)超過3的部分分離出來,并在變換后再次拼接回去,以保持通道數(shù)不變。
# 因此,如果輸入圖像的通道數(shù)小于等于3,則不需要進(jìn)行通道數(shù)的分離和拼接操作,否則需要進(jìn)行相應(yīng)的操作,以保證空間變換網(wǎng)絡(luò)的正確性。
if D > 3:
feature = x[:, :, 3:]
x = x[:, :, :3]
x = torch.bmm(x, trans)
if D > 3:
x = torch.cat([x, feature], dim=2)
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)
return torch.cat([x, pointfeat], 1), trans, trans_feat
?下面就是特征提取當(dāng)中第一個(gè)T-Net網(wǎng)絡(luò),第二天T-Net網(wǎng)絡(luò)大同小異,只是改變了輸入和輸出。
class STN3d(nn.Module):
def __init__(self, channel):
super(STN3d, self).__init__()
self.conv1 = torch.nn.Conv1d(channel, 64, 1)
self.conv2 = torch.nn.Conv1d(64, 128, 1)
self.conv3 = torch.nn.Conv1d(128, 1024, 1)
self.fc1 = nn.Linear(1024, 512)
self.fc2 = nn.Linear(512, 256)
self.fc3 = nn.Linear(256, 9)
self.relu = nn.ReLU()
self.bn1 = nn.BatchNorm1d(64)
self.bn2 = nn.BatchNorm1d(128)
self.bn3 = nn.BatchNorm1d(1024)
self.bn4 = nn.BatchNorm1d(512)
self.bn5 = nn.BatchNorm1d(256)
def forward(self, x):
batchsize = x.size()[0]
x = F.relu(self.bn1(self.conv1(x)))
x = F.relu(self.bn2(self.conv2(x)))
x = F.relu(self.bn3(self.conv3(x)))
x = torch.max(x, 2, keepdim=True)[0]
x = x.view(-1, 1024)
x = F.relu(self.bn4(self.fc1(x)))
x = F.relu(self.bn5(self.fc2(x)))
x = self.fc3(x)
iden = Variable(torch.from_numpy(np.array([1, 0, 0, 0, 1, 0, 0, 0, 1]).astype(np.float32))).view(1, 9).repeat(
batchsize, 1)
if x.is_cuda:
iden = iden.cuda()
x = x + iden
x = x.view(-1, 3, 3)
return x
test_classification.py 這個(gè)測(cè)試文件里的就是加載剛剛訓(xùn)練的最好模型,與訓(xùn)練的代碼大同小異,就沒有看,如果有時(shí)間看了再更新吧。?
下面是分類網(wǎng)絡(luò)鑒于個(gè)人理解畫的圖,如有錯(cuò)誤,歡迎指正。?文章來源:http://www.zghlxwxcb.cn/news/detail-479685.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-479685.html
到了這里,關(guān)于PointNet系列代碼復(fù)現(xiàn)詳解(1)—PointNet分類部分的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!