Как да обучите Point Net за класификация на Point Cloud

Това е третата част от поредицата Point Net:

  1. Интуитивно въведение в Point Net
  2. Пойнт мрежа от нулата
  3. Мрежови точки за класификация
  4. Мрежа от точки за семантична сегментация

В този урок ще научим как да тренираме Point Net за Класификация. Ще се фокусираме основно върху данните и процеса на обучение; урокът, който показва как да кодирате Point Net от нулата, се намира тук. Кодът за този урок се намира в това хранилище. Бележникът, който ще използваме, се намира тук в това хранилище. Част от кода е вдъхновен от това „хранилище“.

Получаване на данните

Ние ще работим с по-малка версия на shapenet dataset, която има само 16 класа. Ако използвате Colab, можете да стартирате следния код, за да получите данните. ПРЕДУПРЕЖДЕНИЕ, това ще отнеме много време.

!wget -nv https://shapenet.cs.stanford.edu/ericyi/shapenetcore_partanno_segmentation_benchmark_v0.zip --no-check-certificate
!unzip shapenetcore_partanno_segmentation_benchmark_v0.zip
!rm shapenetcore_partanno_segmentation_benchmark_v0.zip

Ако искате да работите локално, посетете връзката на първия ред по-горе и данните автоматично ще бъдат изтеглени като zip файл.

Наборът от данни съдържа 16 папки с идентификатори на класове (наречени „synsetoffset“ в README). Структурата на папките е:

  • synsetoffset
    - точки: равномерно избрани точки от модели на ShapeNetCore
    - point_labels: етикети за сегментиране на точка
    - seg_img: визуализация на етикетиране
  • train_test_split: JSON файлове с разделяне на обучение/валидиране/тест

Персонализиран набор от данни на PyTorch се намира тук, като обяснението на кода е извън обхвата на този урок. Важното, което трябва да знаете е, че наборът от данни може да получи или (point_cloud, клас) или (point_cloud, seg_labels). По време на обучението и валидирането ние добавяме шум на Гаус към облаците от точки и произволно ги „въртим“ около вертикалната ос (в случая оста y). Ние също така извършваме мин-макс нормализация към облаците от точки, така че да имат диапазон от 0–1. Можем да създадем екземпляр на набора от данни shapenet така:

from shapenet_dataset import ShapenetDataset

# __getitem__ returns (point_cloud, class)
train_dataset = ShapenetDataset(ROOT, npoints=2500, split='train', classification=True)

Проучване на данните

Преди да започнем каквото и да е обучение, нека проучим някои от данните за обучението. За да направим това, ще използваме Open3d версия 0.16.0 (трябва 0.16.0 или по-нова).

!pip install open3d==0.16.0

Вече можем да видим примерен облак от точки със следния код. Трябва да забележите, че облакът от точки се показва в различна ориентация всеки път, когато изпълнявате кода.

import open3d as o3
from shapenet_dataset import ShapenetDataset

sample_dataset = train_dataset = ShapenetDataset(ROOT, npoints=20000, split='train', 
                                                 classification=False, normalize=False)

points, seg = sample_dataset[4000]

pcd = o3.geometry.PointCloud()
pcd.points = o3.utility.Vector3dVector(points)
pcd.colors = o3.utility.Vector3dVector(read_pointnet_colors(seg.numpy()))

o3.visualization.draw_plotly([pcd])

Вероятно няма да забележите голяма разлика в шума, тъй като добавяме толкова малко количество; ние добавяме малко количество, тъй като не искаме да нарушим значително структурите, но това малко количество е достатъчно, за да окаже влияние върху модела. Сега нека да разгледаме честотите на тренировъчните класове.

Можем да видим на фигура 2, че това определено не е балансиран комплект за обучение. Поради това може да поискаме да приложим класово претегляне или дори да използваме Фокална загуба, за да помогнем на нашия модел да се учи.

Точкова функция за нетни загуби

Когато обучаваме Point Net за класификация, можем да използваме стандарта „Кръстосана загуба на ентропия“ от PyTorch, но също така искаме да направим допълнение, за да включим термина за регулиране, споменат в статията [1]. Терминът за регулация принуждава матрицата за трансформация на характеристики да бъде ортогонална, но защо? Матрицата за преобразуване на функции е предназначена да върти (трансформира) високомерното представяне на Облака от точки. Как можем да сме сигурни, че това научено високоизмерно въртене всъщност върти облака от точки? За да отговорим на това, нека разгледаме някои желани свойства на въртенето. Искаме наученото въртене да бъде „афинно“, което означава, че запазва структурата. Искаме да сме сигурни, че не прави нещо странно, като картографиране обратно в пространство с по-ниско измерение или объркване на структурата. Не можем просто да начертаем облак от точки nx64, за да проверим това, но можем да накараме модела да научи валидна ротация, като насърчаваме ротацията да бъде „ортогонална“. Това е така, защото ортогоналните матрици запазват както дължината, така и ъгъла, а ротационните матрици са специален тип ортогонална матрица [2]. Можем да „насърчим“ модела да научи ортогонална ротационна матрица чрез регуляризация с термина:

Ние използваме фундаментално свойство на ортогоналните матрици, което е, че техните колони и редове са ортогонални вектори. Регулиращият член на фигура 3 ще бъде равен на нула за идеално ортогонална матрица. [2]

По време на тренировка ние просто добавяме този термин към нашата загуба. Ако сте преминали през предишния урок за това как да кодирате точкова мрежа, може би си спомняте, че матрицата за трансформация на признаци A се връща от главата на класификацията.

Сега нека кодираме функцията Point Net загуба. Продължихме напред и добавихме термини за претеглена (балансирана) загуба на кръстосана ентропия и фокална загуба, но обяснението им е извън обхвата на този урок. Кодът за това се намира тук. Този код е адаптиран от това хранилище.

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F


class PointNetLoss(nn.Module):
    def __init__(self, alpha=None, gamma=0, reg_weight=0, size_average=True):
        super(PointNetLoss, self).__init__()
        self.alpha = alpha
        self.gamma = gamma
        self.reg_weight = reg_weight
        self.size_average = size_average

        # sanitize inputs
        if isinstance(alpha,(float, int)): self.alpha = torch.Tensor([alpha,1-alpha])
        if isinstance(alpha,(list, np.ndarray)): self.alpha = torch.Tensor(alpha)

        # get Balanced Cross Entropy Loss
        self.cross_entropy_loss = nn.CrossEntropyLoss(weight=self.alpha)

    def forward(self, predictions, targets, A):

        # get batch size
        bs = predictions.size(0)

        # get Balanced Cross Entropy Loss
        ce_loss = self.cross_entropy_loss(predictions, targets)

        # reformat predictions and targets (segmentation only)
        if len(predictions.shape) > 2:
            predictions = predictions.transpose(1, 2) # (b, c, n) -> (b, n, c)
            predictions = predictions.contiguous() \
                                     .view(-1, predictions.size(2)) # (b, n, c) -> (b*n, c)

        # get predicted class probabilities for the true class
        pn = F.softmax(predictions)
        pn = pn.gather(1, targets.view(-1, 1)).view(-1)

        # get regularization term
        if self.reg_weight > 0:
            I = torch.eye(64).unsqueeze(0).repeat(A.shape[0], 1, 1) # .to(device)
            if A.is_cuda: I = I.cuda()
            reg = torch.linalg.norm(I - torch.bmm(A, A.transpose(2, 1)))
            reg = self.reg_weight*reg/bs
        else:
            reg = 0

        # compute loss (negative sign is included in ce_loss)
        loss = ((1 - pn)**self.gamma * ce_loss)
        if self.size_average: return loss.mean() + reg
        else: return loss.sum() + reg

Мрежа за точки за обучение за класификация

Сега, след като разбираме данните и функцията за загуба, можем да преминем към обучението. За нашето обучение ще искаме да определим количествено колко добре се представя нашият модел. Обикновено разглеждаме загубата и точността, но за този проблем с класификацията ще ни трябва метрика, която отчита както неправилната класификация, така и правилната класификация. Помислете за типичната матрица на объркването: истински положителни, фалшиви отрицателни, истински отрицателни и фалшиви положителни резултати; ние искаме класификатор, който се представя добре на всички тези. Корелационният коефициент на Матюс (MCC) определя количествено колко добре се представя нашият модел по всички тези показатели и се счита за по-надежден единичен показател за ефективност от точността или резултата F1 [3]. MCC варира от -1 до 1, където -1 е най-лошото представяне, 1 е най-доброто представяне и 0 е случайно предположение. Можем да използваме MCC с PyTorch чрез torchmetrics.

from torchmetrics.classification import MulticlassMatthewsCorrCoef

mcc_metric = MulticlassMatthewsCorrCoef(num_classes=NUM_CLASSES).to(DEVICE)

Процесът на обучение е основен цикъл на обучение на PyTorch, който редува обучение и валидиране. Използваме оптимизатора на Адам и нашата функция Point Net Loss плюс термина за регулиране, описан по-горе на фигура 3. За функцията Point Net loss избираме да зададем алфа, която претегля важността на всяка проба. Ние също задаваме гама, която модулира функцията на загубата и я принуждава да се фокусира върху трудните примери, където трудните примери са тези, които са класифицирани с по-ниска вероятност. Вижте бележките в бележника за повече подробности. Беше забелязано, че моделът се обучава по-добре при използване на „циклична скорост на обучение“, така че го внедрихме тук.

import torch.optim as optim
from point_net_loss import PointNetLoss

EPOCHS = 50
LR = 0.0001
REG_WEIGHT = 0.001 

# manually downweight the high frequency classes
alpha = np.ones(NUM_CLASSES)
alpha[0] = 0.5  # airplane
alpha[4] = 0.5  # chair
alpha[-1] = 0.5 # table

gamma = 1

optimizer = optim.Adam(classifier.parameters(), lr=LR)
scheduler = torch.optim.lr_scheduler.CyclicLR(optimizer, base_lr=0.0001, max_lr=0.01, 
                                              step_size_up=2000, cycle_momentum=False)
criterion = PointNetLoss(alpha=alpha, gamma=gamma, reg_weight=REG_WEIGHT).to(DEVICE)

classifier = classifier.to(DEVICE)

Моля, следвайте бележника за цикъла на обучение и се уверете, че имате GPU. Ако не, премахнете планировчика и задайте скоростта на обучение на 0,01, трябва да получите достатъчно прилични резултати след няколко епохи. Ако срещнете някакви потребителски предупреждения на PyTorch (поради бъдеща актуализация на nn.MaxPool1D), можете да ги потиснете с:

import warnings
warnings.filterwarnings("ignore")

Резултати от обучението

Можем да видим, че точността се повишава както за обучение, така и за валидиране, но MCC се повишава само за обучение, а не за валидиране. Това може да бъде причинено от класове с много малки размери на извадката за някои от класовете в разделянията за валидиране и тестване; така че в този случай MCC може да не е най-добрият единичен показател за валидиране и тестване. Това налага допълнително проучване кога MCC е добър показател; т.е. колко дисбаланс е твърде много за MCC? От колко проби се нуждае всеки клас, за да бъде ефективен MCC?

Нека да разгледаме резултатите от теста:

Виждаме, че точността на теста е прилична около 85%, но MCC е малко над 0. Тъй като имаме само 16 класа, нека прегледаме „матрицата на объркване“ в бележника, за да добием по-добра представа за резултатите от теста.

В по-голямата си част класификацията е наред, но има няколко по-рядко срещани класа като „ракета“ или „скейтборд“. Моделът има тенденция да има лоша прогнозна производителност за тези класове и производителността за тези по-рядко срещани класове е това, което води до спад на MCC. Друго нещо, което трябва да отбележите, е, че когато проверявате резултатите (както е показано в бележника), ще получите добра точност и уверено представяне на по-честите класове. Въпреки това в по-рядко срещаните класове ще забележите, че увереността е по-ниска и точността е по-лоша.

Проверка на критичните набори

Сега ще разгледаме най-интересната част от този урок, критичните набори. Критичните набори са съществените основни точки на набор от облак от точки. Тези точки определят основната му структура. Ето малко код, показващ как да ги визуализирате.

from open3d.web_visualizer import draw 


critical_points = points[crit_idxs.squeeze(), :]
critical_point_colors = read_pointnet_colors(seg.numpy())[crit_idxs.cpu().squeeze(), :]

pcd = o3.geometry.PointCloud()
pcd.points = o3.utility.Vector3dVector(critical_points)
pcd.colors = o3.utility.Vector3dVector(critical_point_colors)

# o3.visualization.draw_plotly([pcd])
draw(pcd, point_size=5) # does not work in Colab

И ето някои визуализации, забележете, че използвах „draw()“, за да получа по-големи размери на точки, но не работи в Colab.

Можем да видим, че критичните комплекти показват цялостната структура на съответните им облаци от точки, те по същество са облаци от точки с оскъдна извадка. Това показва, че обученият модел действително се е научил да разграничава различните структури и предполага, че той действително е в състояние да класифицира всяка категория облак от точки въз основа на техните отличителни структури.

Заключение

Ако сте стигнали дотук, поздравления! Вие научихте как да обучавате Point Net от нулата и ние дори се научихме как да визуализираме Point Sets. Бих ви насърчил да се върнете и да се уверите, че разбирате всичко, и ако наистина се интересувате, опитайте се да подобрите цялостното представяне на класификацията. Ето някои предложения, за да започнете:

  • Използване на различна функция за загуба
  • Опитайте различна настройка в планировчика на скоростта на циклично обучение
  • Експериментирайте с модификации на архитектурата на Point Net
  • Експериментирайте с различни увеличения на данни
  • Използвайте повече данни → Опитайте пълния shapenet datset

Препратки

[1] Charles, R. Q., Su, H., Kaichun, M., & Guibas, L. J. (2017). PointNet: Задълбочено обучение на набори от точки за 3D класификация и сегментиране. 2017 IEEE Конференция за компютърно зрение и разпознаване на образи (CVPR). https://doi.org/10.1109/cvpr.2017.16

[2] Knill, O. (n.d.). Раздел 8: Ортогоналната група — Харвардски университет. people.math.harvard.edu. Изтеглено на 10 декември 2022 г. от https://people.math.harvard.edu/~knill/teaching/math22b2019/handouts/lecture08.pdf

[3] Chicco, D., & Jurman, G. (2020). Предимствата на корелационния коефициент на Matthews (MCC) пред оценката F1 и точността при оценката на двоичната класификация. BMC Genomics, 21(1). https://doi.org/10.1186/s12864-019-6413-7