跳轉到內容

在體資料、點雲和影片上訓練模型

在繪製的 CT 掩碼上擬合 3D U-Net,在雷射雷達長方體上執行自己的 OpenPCDet 檢測器,並訓練影片跟蹤器,每種模型都在留出條目上評分。

v2.14.0 新增

本教程根據 Potato 研究自己的標註,在 3D 資料和運動資料上訓練三個模型。volume-unet 訓練分割 CT 和 MR 體資料的 3D U-Net,external-3d 執行由你自己構建和執行的雷射雷達檢測器,video-track 訓練影片目標跟蹤器。本頁面向已經在 Potato 中標註體資料、點雲或影片,並希望用模型為其餘條目預標註的團隊。對每個訓練器,本頁介紹產生訓練資料的方案、能通過 potato validate 的配置、一次執行做了什麼、留出得分,以及預測如何送到標註者面前。訓練你的第一個模型教程用一個不需要 GPU 的文本分類器演示了訓練頁面,本教程以該教程為前提。

訓練器概覽

三個訓練器每次執行都只擬合一個方案,在該次執行留出的 val 和 test 條目上給模型評分,並把預測作為供標註者編輯的預標籤回寫。volume-unet 和 external-3d 背後的檢測器在實踐中需要 GPU,所以這兩個訓練器的配置通過 ssh 把執行傳送到 GPU 伺服器,設定方法見遠端訓練教程。

訓練器方案條目在訓練機器上安裝主得分
volume-unetvolume_annotation(繪製的掩碼)NIfTI 體資料、DICOM 序列pip install "potato-annotation[train-vision,volumes]";實踐中需要 GPUmean_dice
external-3dspatial_annotation(長方體)點雲你自己的檢測器(OpenPCDet、MMDetection3D)map_3d_50
video-tracktracking 或 combined 模式的 video_annotation影片pip install "potato-annotation[train-vision]" opencv-pythonidf1

每個訓練器都從磁碟上的檔案讀取媒體。媒體路徑必須指向任務目錄或 media_directory 之內,媒體為 http(s) URL 的條目不參與訓練和預測。

用 volume-unet 分割 CT 和 MR 體資料

volume-unet 學習標註者在 volume_annotation 方案中繪製的掩碼,並在還沒有人分割的體資料上繪製掩碼。該方案在三個聯動的檢視(軸狀面、冠狀面和矢狀面)中顯示 NIfTI 檔案或 DICOM 序列,標註者用畫筆繪製每個標籤,或者閉合一個多邊形。source_field 指定的條目欄位儲存一個 .nii 或 .nii.gz 檔案,或者儲存包含一個 DICOM 序列的目錄,路徑相對於 media_directory。在切片上畫的框沒有掩碼,所以只含框的答案不是訓練目標。Potato 是研究軟體,沒有臨床或監管許可,在這裡訓練的模型提出的掩碼需要由標註者修正。

下面的配置分割脾臟和肝臟,在 GPU 伺服器上訓練,併為 train 劃分之外的每個體資料預標註:

yaml
annotation_task_name: "Spleen segmentation"
task_dir: .
media_directory: media
data_files:
  - data/scans.jsonl
item_properties:
  id_key: id
  text_key: volume
output_annotation_dir: annotation_output
user_config:
  allow_all_users: true
 
annotation_schemes:
  - annotation_type: volume_annotation
    name: organs
    description: "Paint the spleen and the liver on every slice where each organ appears."
    source_field: volume
    labels:
      - {name: Spleen, color: "#e6553a", key_value: "1"}
      - {name: Liver, color: "#3a8ee6", key_value: "2"}
    tools: [brush, eraser, polygon]
    default_window: Soft tissue
 
model_training:
  enabled: true
  backend: gpu-server
  backends:
    gpu-server:
      type: ssh
      host: gpu.example.edu
      python: /opt/conda/bin/python3
      gpus: auto
      min_free_gpu_mb: 8000
  trainer_params:
    volume-unet:
      patch: 96
  writeback:
    schemas: [organs]
    min_confidence: 0.6
    include_unlabeled: true

每行資料對應一個體資料:

json
{"id": "case_001", "volume": "case_001.nii.gz"}
{"id": "case_002", "volume": "case_002_ct"}

patch 的值設定網路訓練所用立方體塊的邊長,在預設的三個層級下必須是 4 的倍數(2 的 levels 減 1 次方)。96 體素的塊讓網路在每個體素周圍看到比預設 64 更多的上下文,也需要更多的 GPU 記憶體。設定 min_free_gpu_mb: 8000 後,Potato 只在空閒記憶體至少 8,000 MB 的 GPU 上啟動執行。Potato 取模型在每個已繪製標籤的掩碼內的平均機率,並把這些平均值中最低的一個作為該體資料的置信度。設定 min_confidence: 0.6 後,只要有任何一個器官的繪製平均機率低於 0.6,該體資料就不會得到預標籤。

volume-unet 執行中的訓練步驟

一次執行按控制元件繪製時的體素順序讀取每個已標註的體資料,並把掩碼變成每個體素一個標籤。兩個標籤重疊的地方,以包中標籤順序靠後的標籤為準,與 NIfTI 匯出一致。強度被裁剪到每個體資料的 0.5 和 99.5 百分位並做標準化。網路是純 torch 實現的 3D U-Net(三個層級,instance norm),用交叉熵加 soft Dice 訓練,每個 epoch 取 100 個隨機塊。其中一半的塊以已繪製的體素為中心,所以小結構會出現在訓練中。每個 epoch 之後,執行在 val 體資料上計算平均 Dice 並保留最佳 epoch,連續 10 個 epoch 沒有更好的得分時執行停止。預測時讓塊以半塊重疊的方式滑過整個體資料,並對機率取平均。

train 劃分至少需要三個已標註的體資料(min_instances)。已繪製形狀與檔案形狀不同的體資料會被排除,並在執行日誌中給出警告,檔案缺失的體資料也一樣。同一個體資料有多位讀片者時,執行用 aggregation 解析出的那一位讀片者的掩碼訓練,因為聚合器比較的是整個答案。Segment CT Volumes With Several Readers and STAPLE 介紹讀片者之間的一致性,以及在 Potato 之外構建的共識掩碼。

體資料的留出得分

Potato 對每個留出體資料上的每個標籤計算 Dice 係數(dice_<label>),並把所有體資料和標籤上的平均值報告為 mean_dice。模型在含有某標籤的體資料中漏掉該標籤時,得分為 0。訓練器在 val 上選出最佳 epoch,所以 Runs 表的主得分取自 test(test_mean_dice)。各標籤得分的完整列表在執行頁面上。

切片檢視上的掩碼預標籤

預測的掩碼以控制元件自己的形狀(每個標籤、每個軸狀面切片)返回,標記為 prefilled_from: model,標註者開啟該體資料時會顯示在切片檢視上。標註者用畫筆和橡皮擦修正掩碼,修正後的掩碼儲存為該標註者的答案。屬於該次執行 train 劃分的條目永遠不會得到預標籤。沒有 include_unlabeled 時,一次執行只在該次執行的 val 和 test 體資料上預測。

用 external-3d 檢測 3D 長方體

external-3d 把已標註的掃描交給你安裝在訓練主機上的 3D 檢測器,執行你的訓練和預測命令,再把預測的框作為長方體讀回。Potato 不捆綁點雲檢測器。該訓練器針對的兩個檢測器 OpenPCDet 和 MMDetection3D 需要在 GPU 主機上編譯 CUDA 運算元。在 Potato 一側,該訓練器只需要 numpy。

訓練資料來自帶 cuboid_3d 工具的 spatial_annotation 方案。標註者在 PCD、PLY、LAS 或 KITTI .bin 掃描上放置有朝向的框,以米為單位儲存在掃描自己的座標系中(見3D 點雲和長方體)。長方體是唯一的目標,所以同一方案中的點、折線和點段會被忽略。每個條目是一次掃描,帶軌跡 id 的幀序列不是訓練目標。

為檢測器寫出的資料集資料夾

訓練命令執行之前,Potato 把掃描寫入 {dataset},這是執行資料夾內名為 dataset 的資料夾,採用 OpenPCDet 的自定義資料集佈局:

  • points/<id>.npy 把一次掃描儲存為 x y z intensity 的 float32 行,檔案沒有強度時強度為 0。
  • labels/<id>.txt 為 train 和 val 掃描的每個長方體寫一行,格式為 x y z dx dy dz heading label。heading 是繞 +z 的旋轉,單位為弧度,俯仰和橫滾不保留。標籤名中的空格變成下劃線。
  • ImageSets/train.txt、val.txt 和 predict.txt 列出掃描 id。predict.txt 列出 val 和 test 掃描,在 writeback.include_unlabeled 開啟時還包括未標註的掃描。

每個 <id> 是條目 id 的 SHA-1 的前 16 個十六進位制字元,因為條目 id 可能含有檔名不能使用的字元。test 掃描的標籤永遠不會寫入該資料夾。Potato 以掃描的原點為基準寫出帶地理參考的掃描,因為地理參考座標對 float32 來說太大,並把預測的框按同樣的偏移量移回。

從檢測器讀回的預測檔案

預測命令為每次掃描向 {predictions}/<id>.txt 寫一個檔案,每行一個框,格式為 x y z dx dy dz heading label score。score 是可選的。Potato 會排除無法解析、含有 nan 或 inf、使用了方案中沒有的標籤,或者描述的框沒有大小的行,執行日誌會統計被排除的行數。其餘的框作為標記為 source: model 的 cuboid_3d 物件返回,每個框的 score 成為該框的置信度。Potato 把一次掃描的置信度設為該掃描中最低的框得分,min_confidence 作用於這個值。

訓練和預測命令

兩個命令都設為訓練器參數。Potato 按 shell 的方式把每個命令拆成詞,把 {dataset}、{workdir}(執行的資料夾)和 {predictions} 替換為對應路徑,並在執行的資料夾中不經 shell 執行該命令。命令中的其他花括號保持原樣。管道、&& 和 $VARIABLES 會作為字面的詞傳給程式,所以多步工作應放進指令碼。把下面的塊新增到點雲研究的配置中:

yaml
model_training:
  enabled: true
  backend: gpu-server
  backends:
    gpu-server:
      type: ssh
      host: gpu.example.edu
      python: /opt/conda/envs/pcdet/bin/python
      gpus: auto
  trainer_params:
    external-3d:
      train_command: "/opt/pcdet-jobs/train.sh {dataset} {workdir}"
      predict_command: "/opt/conda/envs/pcdet/bin/python /opt/pcdet-jobs/predict.py {dataset} {workdir} {predictions}"
      timeout_s: 43200
  writeback:
    schemas: [objects]
    min_confidence: 0.3
    include_unlabeled: true

這些路徑位於訓練主機上,執行的資料夾也在那裡。timeout_s 限制每個命令的時長,這裡的 12 小時取代了預設的 24 小時。訓練命令以非零狀態退出會使執行失敗,執行的錯誤資訊會顯示該命令輸出的最後幾行。

train.sh 是你自己的指令碼。對於 OpenPCDet,指令碼把 OpenPCDet 的自定義資料集配置指向 {dataset},用 python -m pcdet.datasets.custom.custom_dataset create_custom_infos <dataset config> 構建資訊檔案,執行 OpenPCDet 的訓練指令碼,並把檢查點儲存在 {workdir} 下。檢測器配置中的類別名必須與 labels/ 中寫出的標籤名一致。MMDetection3D 的自定義資料集指南只讀取 .bin 檔案形式的點雲,所以對 MMDetection3D,指令碼要先把每個 points/<id>.npy 寫成 .bin 檔案。train_command 留空則跳過訓練,此時 predict_command 執行你在別處訓練的模型。

predict.py 讀取 predict.txt 中的 id,對每次掃描執行檢測器,並寫出框。load_model 和 detect 函式是你圍繞所訓練的檢測器寫的程式碼:

python
"""predict.py DATASET WORKDIR PREDICTIONS: one line per box, one file per scan."""
import os
import sys
 
import numpy as np
 
from my_detector import load_model, detect  # your code around OpenPCDet or MMDetection3D
 
dataset, workdir, out = sys.argv[1:4]
model = load_model(os.path.join(workdir, "output"))  # where train.sh saved the checkpoint
os.makedirs(out, exist_ok=True)
 
with open(os.path.join(dataset, "ImageSets", "predict.txt")) as fh:
    scan_ids = fh.read().split()
 
for scan_id in scan_ids:
    points = np.load(os.path.join(dataset, "points", scan_id + ".npy"))  # float32, (N, 4)
    with open(os.path.join(out, scan_id + ".txt"), "w") as fh:
        for x, y, z, dx, dy, dz, heading, label, score in detect(model, points):
            fh.write(f"{x} {y} {z} {dx} {dy} {dz} {heading} {label} {score}\n")

沒有 train_command 時,預測命令就是整個執行,預測命令以非零狀態退出會使執行失敗。預測命令即使以 0 退出,但沒有為任何掃描寫出 <id>.txt,或只寫出 Potato 會捨棄的行,也算失敗,因為按這種輸出評分的執行會像是模型什麼也沒找到。空檔案表示該掃描沒有物體。在訓練命令之後,失敗的預測命令不會使執行失敗。執行會帶著警告“Prediction pass failed after 0 prediction(s)”和該命令的最後幾行結束,此時執行沒有預測,也沒有留出得分。已結束的執行顯示“not scored”時,請在執行日誌中查詢這條警告。

長方體的留出得分

Potato 在體積 IoU 為 0.5 時為每個標籤計算 3D 平均精度(ap_3d_50_<label>)。對任意旋轉的有朝向長方體,該 IoU 是精確的。Potato 還報告各標籤在 IoU 0.25 和 0.5 時的平均值,以及 0.5 時的精確率、召回率和 F1。主得分是 map_3d_50。有 train_command 時,Runs 表的主得分優先取自 test,因為訓練命令在 {dataset} 中拿到了 val 標籤,可能據此選擇檢查點。沒有 train_command 時,模型從未見過研究的 val 條目,主得分取自 val。

點雲檢視器中的長方體預標籤

預測的長方體在點雲檢視器中顯示為可編輯的框,編輯後的框儲存為標註者的答案。Potato 在檢視器繪製每個預測物件之前先檢查它,並排除幾何無法解析的物件、沒有大小的長方體,以及方案中沒有的標籤。How to Annotate 3D Point Clouds 介紹標註一側,以及 3D 框上的一致性。

用 video-track 跟蹤影片中的目標

video-track 從標註者在 tracking 或 combined 模式的 video_annotation 方案中畫的軌跡學習,並在還沒有人跟蹤的影片上預測軌跡。一條軌跡由一個標籤和每個關鍵幀上的一個框組成,控制元件在關鍵幀之間對框做插值。其他模式的 video_annotation 方案儲存的是片段而不是軌跡,該方案不提供 video-track。

yaml
annotation_task_name: "Pedestrian tracking"
task_dir: .
media_directory: media
data_files:
  - data/clips.jsonl
item_properties:
  id_key: id
  text_key: video
output_annotation_dir: annotation_output
user_config:
  allow_all_users: true
 
annotation_schemes:
  - annotation_type: video_annotation
    name: tracks
    description: "Track every pedestrian and cyclist with a box at each keyframe."
    source_field: video
    mode: tracking
    video_fps: 25
    labels:
      - {name: pedestrian, color: "#FF6B6B"}
      - {name: cyclist, color: "#45B7D1"}
    tracking_options:
      interpolation: linear
      auto_advance_frames: 5
    frame_stepping: true
 
model_training:
  enabled: true
  trainer_params:
    video-track:
      frame_fps: 2
      predict_fps: 5
      max_age: 4
  writeback:
    schemas: [tracks]
    include_unlabeled: true

每行資料對應 media_directory 下的一個影片片段:

json
{"id": "clip_001", "video": "/media/clip_001.mp4"}
{"id": "clip_002", "video": "/media/clip_002.mp4"}

請把 video_fps 設為片段的真實幀率。關鍵幀按幀編號,訓練器按方案的 video_fps(未設定時為 30)為預測的關鍵幀編號。訓練器按每條軌跡自己的 interpolation 設定(線性、三次 Catmull-Rom 或常數)讀取取樣幀上的框,所以模型學習的框與標註者看到的路徑一致。與控制元件中一樣,只有一個關鍵幀的軌跡在每一幀都保持不動,控制元件為其指定了 startFrame 和 endFrame 的軌跡則在這兩幀之間執行。位於這兩幀之外的關鍵幀仍會顯示在它自己的那一幀上。predict_fps: 5 表示每秒在五幀上檢測,而不是兩幀。每秒五幀讓快速運動的目標得到更多關鍵幀,代價是更多的檢測器遍數。一條軌跡在連續超過 max_age 個取樣幀沒有匹配的檢測之後結束,所以在每秒 5 幀下 max_age: 4 讓軌跡能挺過 0.8 秒的漏檢。行人從杆子後面經過時,只要檢測器在 0.8 秒內重新發現該行人,就仍是同一條軌跡。該配置在執行 Potato 的機器上訓練(local 後端),體資料配置中的 gpu-server 後端同樣適用於 video-track。

video-track 執行中的訓練和軌跡連線

一次執行以每秒 frame_fps 幀的速率對每段已標註的影片取樣,從開頭到最後一個關鍵幀,並用 OpenCV 讀取每個取樣幀。畫布畫素取決於標註者的螢幕,所以訓練器把每條軌跡在該幀上的框讀取為畫面的一個比例。取樣幀上沒有軌跡時,該幀仍留在訓練中,作為空場景的示例。執行在這些幀上微調 torchvision 的 Faster R-CNN,參數和預設值與 torchvision-detect 相同。檢測器從 torchvision 的 COCO 權重開始。每個 epoch 之後,執行在 val 幀上計算 IoU 為 0.5 時的框 mAP(frame_map_50)並保留最佳 epoch。

預測以 predict_fps 對每段影片取樣,在每個取樣幀上檢測,並把檢測連線成軌跡。每一幀的檢測與同一標籤的活動軌跡一對一匹配,依據是與每條軌跡最後一個框的重疊,匹配要求 IoU 至少為 match_iou(0.3)。得分至少為 new_track_score(0.5)的未匹配檢測開始一條新軌跡。訓練器在寫回和評分之前,會丟棄檢測數少於 min_track_keyframes(預設 2)的預測軌跡。否則控制元件會讓只有一個檢測的軌跡在每一幀都保持不動。每個檢測都成為一個關鍵幀,所以控制元件在模型的關鍵幀之間做插值,與在標註者的關鍵幀之間一樣。

train 劃分至少需要兩段已標註的影片(min_instances)。OpenCV 無法解碼的影片會被排除,並給出點名該條目的警告,執行用其餘的繼續。

軌跡的留出得分

Potato 在每段留出影片上每秒取樣 2 幀,再加上每個已標註的關鍵幀,把預測的軌跡與已標註的軌跡比較。得分包括 MOTA、IDF1、ID 切換、漏檢和誤檢,IoU 達到 0.5 的框算作找到。MOTA 把漏檢、誤檢和身份切換按已標註框的數量計入代價。IDF1 是在整段影片中以正確身份匹配的框的 F1 得分,IDF1 是主得分(idf1)。訓練器在 val 上選出最佳 epoch,所以 Runs 表的主得分取自 test。

影片控制元件中的軌跡預標籤

預測的軌跡以軌跡的形式填入影片控制元件,關鍵幀標記為 AI 輸出並帶有檢測器的得分。控制元件在繪製軌跡時把框轉換為控制元件自己的畫布畫素,標註者像修正其他軌跡一樣修正這些軌跡。一段影片的置信度是模型關鍵幀得分的平均值。在 combined 模式下,預標籤只含軌跡,片段由標註者新增。How to Annotate Video Object Tracking 介紹標註一側、遮擋和時間上的一致性。

開始一次執行並檢查結果

新增 model_training 之後重啟伺服器,開啟 /admin/training,選擇方案和訓練器,然後點選 Train now。該頁面只提供適合每個方案的訓練器,並把未安裝的訓練器列為“not installed”,附上安裝命令。要在第一次執行之前檢查 GPU 伺服器,請執行:

bash
potato training check gpu-server config.yaml

該檢查會報告遠端 Python 版本、每塊 GPU 及其空閒記憶體、一次執行將分到的 GPU,以及遠端直譯器缺少的訓練包。也可以通過 API 啟動執行,每次執行的參數會覆蓋 trainer_params:

bash
curl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  http://localhost:8000/admin/training/api/runs \
  -d '{"trainer": "video-track", "schemas": ["tracks"], "params": {"predict_fps": 10}}'

執行結束後,Runs 表列出留出得分,劃分和指標顯示在提示中。預標籤會錨定標註者,所以請等留出得分足以支援展示模型的答案之後,再在新研究中開啟 include_unlabeled。模型在環教程介紹優先提供置信度最低的預標籤,以及在修正上重新訓練,模型訓練配置和路由列出每個鍵、後端和回寫檢查。

延伸閱讀

參考文獻

  • Bernardin, K., and Stiefelhagen, R. (2008). Evaluating Multiple Object Tracking Performance: The CLEAR MOT Metrics. EURASIP Journal on Image and Video Processing. https://doi.org/10.1155/2008/246309
  • Çiçek, Ö., Abdulkadir, A., Lienkamp, S. S., Brox, T., and Ronneberger, O. (2016). 3D U-Net: Learning Dense Volumetric Segmentation from Sparse Annotation. MICCAI 2016. https://arxiv.org/abs/1606.06650
  • Ristani, E., Solera, F., Zou, R. S., Cucchiara, R., and Tomasi, C. (2016). Performance Measures and a Data Set for Multi-Target, Multi-Camera Tracking. ECCV 2016 Workshop on Benchmarking Multi-Target Tracking. https://arxiv.org/abs/1609.01775