地平線軌跡預(yù)測 QCNet 參考算法 - V2.0
1.簡介
軌跡預(yù)測任務(wù)的目的是在給定歷史軌跡的情況下預(yù)測未來軌跡。這項任務(wù)在自動駕駛、智能監(jiān)控、運動分析等領(lǐng)域有著廣泛應(yīng)用。傳統(tǒng)方法通常直接利用歷史軌跡來預(yù)測未來,而忽略了預(yù)測目標(biāo)的上下文或查詢信息的影響。這種忽視可能導(dǎo)致預(yù)測精度的下降,特別是在復(fù)雜場景中。
QCNet(Query-Centric Network)引入了一種 query-centric 的預(yù)測機(jī)制,通過對 query 進(jìn)行顯式建模,增強了對未來軌跡的預(yù)測能力。首先,通過處理所有場景元素的局部時空參考框架和學(xué)習(xí)獨立于全局坐標(biāo)的表示,可以緩存和復(fù)用先前計算的編碼,另外不變的場景特征可以在所有目標(biāo) agent 之間共享,從而減少推理延遲。其次,使用無錨點查詢來周期性檢測場景上下文,并且在每次重復(fù)時解碼一小段未來的軌跡點。這種基于查詢的解碼管道將無錨方法的靈活性融入到基于錨點的解決方案中,促進(jìn)了多模態(tài)和長期時間預(yù)測的準(zhǔn)確性。
本文將介紹軌跡預(yù)測算法 QCNet 在地平線征程 6 平臺上的優(yōu)化部署。
模型參數(shù):
性能精度表現(xiàn):
3.公版模型介紹
由于軌跡預(yù)測的歸一化要求,現(xiàn)有方法采用以 agent 為中心的編碼范式來實現(xiàn)空間旋轉(zhuǎn)平移不變性,其中每個代理都在由其當(dāng)前時間步長位置和偏航角確定的局部坐標(biāo)系中編碼。但是觀測窗口每次移動時,場景元素的幾何屬性需要根據(jù) agent 最新狀態(tài)的位置重新歸一化,不斷變化的時空坐標(biāo)系統(tǒng)阻礙了先前計算編碼的重用,即使觀測窗口存在很大程度上的重疊。為了解決這個問題,QCNet 引入了以查詢?yōu)橹行牡木幋a范式,為查詢向量派生的每個場景元素建立一個局部時空坐標(biāo)系,并在其局部參考系中處理查詢元素的特征。然后,在進(jìn)行基于注意力的場景上下文融合時,將相對時空位置注入 Key 和 Value 元素中。下圖展示了場景元素的局部坐標(biāo)系示例:
QCNet 主要由編碼器和解碼器組成,其作用分別為:
編碼器:對輸入的場景元素進(jìn)行編碼,采用了目前流行的 factorized attention 實現(xiàn)了時間維度 attention、Agent-Map cross attention 和 Agent 與 Agent 間隔的 attention;
解碼器:借鑒 DETR 的解碼器,將編碼器的輸出解碼為每個目標(biāo) agent 的 K 個未來軌跡。
QCNet 首先進(jìn)行了場景元素編碼、相對位置編碼和地圖編碼,對于每個 agent 狀態(tài)和 map 上的每個采樣點,將傅里葉特征與語義屬性(例如:agent 的類別)連接起來,并通過 MLP 進(jìn)行編碼,為了進(jìn)一步生成車道和人行橫道的多邊形級表示,采用基于注意力的池化對每個地圖多邊形內(nèi)采樣點進(jìn)行。這些操作產(chǎn)生形狀為[A, T, D]的 agent 編碼和形狀為[M, D]的 map 編碼,其中 D 表示隱藏的特征維度。為了幫助 agent 編碼捕獲更多信息,編碼器還考慮了跨 agent 時間 step、agent 之間以及 agent 與 map 之間的注意力并重復(fù)多次。如下圖所示:
軌跡預(yù)測的第二步是利用編碼器輸出的場景編碼來解碼每個目標(biāo) agent 的 K 個未來軌跡。受目標(biāo)檢測任務(wù)的啟發(fā),采用類似 DETR 的解碼器來處理這種一對多問題。QCNet 使用可學(xué)習(xí)的、無錨點的 query 來提出初始軌跡。初始軌跡在 refine 模塊中充當(dāng)錨點。與 Multipath 和 DenseTNT 密集采樣的手動設(shè)置 anchor 相比,QCNet 在 propose 模塊用數(shù)據(jù)驅(qū)動的方式生成 k 個自適應(yīng) anchor。為了減輕 query 的上下文提取負(fù)擔(dān)并提高 anchor 的質(zhì)量,將類似 DETR 的解碼器推廣為循環(huán)方式。通過 TrecTrec 個循環(huán),具有上下文感知的模態(tài) query 僅通過每個循環(huán)末尾的 MLP 解碼 T’/TrecT’/Trec 未來的 waypoints。在隨后的循環(huán)中,這些 query 再次成為輸入,并提取與接下來幾個路徑點預(yù)測相關(guān)的場景上下文。相關(guān)流程如下所示:
4.地平線部署優(yōu)化
整體情況:QCNet 網(wǎng)絡(luò)主要由 MapEncoder, AgentEncoder, QCDecoder 構(gòu)成,其中 MapEncoder 計算地圖元素 embedding,AgentEncoder 計算 agent 元素 embedding,核心組件為 FourierEmbedding 和 AttentionLayer。
改動點:
相對于公版網(wǎng)絡(luò)結(jié)構(gòu),在不大幅影響精度的情況下,對網(wǎng)絡(luò)進(jìn)行了裁剪,實現(xiàn)了性能的提升,相關(guān)細(xì)節(jié)見 4.1.1 章節(jié);
優(yōu)化 FourierEmbedding 結(jié)構(gòu),去除其中的所有 edge_index,直接計算形狀為[B, lenq, lenk, D]的相對信息 r;
重構(gòu)代碼,將 AttentionLayer 中的 query 形狀設(shè)為[B, lenq, 1, D] , key 形狀為[B, 1, lenk, D], r 形狀為[B, lenq, lenk, D],利于性能提升;
適當(dāng)減少了相對位置編碼 RAttentionLayer 中的 Layermorm 操作,對精度影響不大;
decoder 復(fù)用 agent encoder 的 feature,并去除了 decoder propose 階段 a2m 的 RAttention;
適配流式推理:預(yù)測算法 QCNet 的兩種推理方式,一是對所有歷史幀數(shù)據(jù)并行 encode 后送入 decode 預(yù)測下一幀;二是流式推理按照時序,逐幀 encode 后,最后一幀將前面 encoder 的結(jié)果拼接后送入 decoder。流式推理符合實際部署的邏輯,但 hbminfer 速度會變慢。實際部署數(shù)據(jù)按時序逐幀給出,應(yīng)當(dāng)采用流式推理方案。
為了更優(yōu)異的性能表現(xiàn),參考算法相對于公版做了裁剪,主要為以下參數(shù):
FourierEmbedding 將每個場景元素的極坐標(biāo)轉(zhuǎn)換成傅里葉特征,以方便高頻信號的學(xué)習(xí)。 但是公版 QCNet 使用了大量 edge_index 索引操作, 使得模型中存在大量 BPU 暫不支持的 index_select、scatter 等操作。QCNet 參考算法重構(gòu)了代碼,去除了 FourierEmbedding 中的所有 edge_index,agent_encoder 編碼器注意力層的 query 形狀設(shè)為[B, lenq, 1, D] , key 形狀為[B, 1, lenk, D], r 形狀為[B, lenq, lenk, D],相關(guān)代碼如下所示:
def _attn_block(
self,
x_src,
x_dst,
r,
mask=None,
extra_1dim=False,
):
B = x_src.shape[0]
if extra_1dim:
...
else:
if x_src.dim() == 4 and x_dst.dim() == 3:
lenq, lenk = x_dst.shape[1], x_src.shape[2]
kdim1 = lenq
qdim = 1
elif x_src.dim() == 3:
kdim1 = qdim = 1
lenq = x_dst.shape[1]
lenk = x_src.shape[1]
#重構(gòu)q,k,v,rk,rv的shape
q = self.to_q(x_dst).view(
B, lenq, qdim, self.num_heads, self.head_dim
) # [B,pl, 1, h, d]
k = self.to_k(x_src).view(
B, kdim1, lenk, self.num_heads, self.head_dim
) # [B,pl, pt, h, d]
v = self.to_v(x_src).view(
B, kdim1, lenk, self.num_heads, self.head_dim
) # [B,pl, pt, h, d]
if self.has_pos_emb:
rk = self.to_k_r(r).view(
B, lenq, lenk, self.num_heads, self.head_dim
)
rv = self.to_v_r(r).view(
B, lenq, lenk, self.num_heads, self.head_dim
)
if self.has_pos_emb:
k = k + rk
v = v + rv
#計算相似性
sim = q * k
sim = sim.sum(dim=-1)
#self.scale = head_dim ** -0.5
sim = sim * self.scale # [B, pl, pt, h]
if mask is not None:
sim = torch.where(
mask.unsqueeze(-1),
sim,
self.quant(torch.tensor(-100.0).to(mask.device)),)
attn = torch.softmax(sim, dim=-2) # [B, pl, pt, h]
...
if extra_1dim:
inputs = out.view(B, ex_dim, -1, self.num_heads * self.head_dim)
else:
inputs = out.view(B, -1, self.num_heads * self.head_dim)
x = torch.cat([inputs, x_dst], dim=-1)
g = torch.sigmoid(self.to_g(x))
#重構(gòu)代碼后,edge_index也就不需要了,省去了僅能用CPU運行的索引類算子
#agg = self.propagate(edge_index=edge_index, x_dst=x_dst, q=q, k=k, v=v, r=r)
agg = inputs + g * (self.to_s(x_dst) - inputs)
return self.to_out(agg)
代碼路徑:
/usr/local/lib/python3.10/dist-packages/hat/models/task_modules/qcnet/rattention.py
4.1.3 FourierConvEmbedding為了提升性能,主要對 FourierEmbedding 做了以下改進(jìn):
Embedding 和 Linear 層全部替換為了對 BPU 更友好的 Conv1x1;
刪除 self.mlps 層中的 LayerNorm,對精度基本無影響;
將公版代碼中的 torch.stack(continuous_embs)。sum(dim=0)直接優(yōu)化為了 add 操作,獲得了比較大的性能收益。 對應(yīng)代碼如下所示
class FourierConvEmbedding(nn.Module):
def
__init__
(
self, input_dim: int, hidden_dim: int, num_freq_bands: int
) -> None:
super(FourierConvEmbedding, self).
__init__
()
self.input_dim = input_dim
self.hidden_dim = hidden_dim
#nn.Embedding替換為了Conv1x1
self.freqs = nn.ModuleList( [
nn.Conv2d(1, num_freq_bands, kernel_size=1, bias=False)
for _ in range(input_dim)])
#Linear層替換為了Conv1x1
self.mlps = nn.ModuleList(
[nn.Sequential(
nn.Conv2d(
num_freq_bands * 2 + 1, hidden_dim, kernel_size=1),
#刪除LayerNorm
#nn.LayerNorm(hidden_dim),
nn.ReLU(inplace=True),
nn.Conv2d(hidden_dim, hidden_dim, kernel_size=1),)
for _ in range(input_dim)
]
)
#Linear層替換為了Conv1x1
self.to_out = nn.Sequential(
LayerNorm((hidden_dim, 1, 1), dim=1),
nn.ReLU(inplace=True),
nn.Conv2d(hidden_dim, hidden_dim, 1),
)
...
def forward(
self,
continuous_inputs: Optional[torch.Tensor] = None,
categorical_embs: Optional[List[torch.Tensor]] = None,
) -> torch.Tensor:
if continuous_inputs is None:
...
else:
continuous_embs = 0
for i in range(self.input_dim):
...
if i == 0:
continuous_embs = self.mlps
[i](x)
else:
#將stack+sum的操作替換為add
continuous_embs = continuous_embs + self.mlps
[i](x)
# x = torch.stack(continuous_embs, dim=0).sum(dim=0)
x = continuous_embs
if categorical_embs is not None:
#將stack+sum的操作替換為add
# x = x + torch.stack(categorical_embs, dim=0).sum(dim=0)
x = x + categorical_embs
return self.to_out(x)
代碼路徑:/usr/local/lib/python3.10/dist-packages/hat/models/task_modules/qcnet/fourier_embedding.py
4.1.4 RAttentionLayer中的 RAttentionLayer 中存在 6 個 LayerNorm 操作,參考算法為了提升性能,將 LayerNorm 操作的數(shù)量減少至 3 個,如下圖所示:
從實驗結(jié)果來看,浮點精度反而略有提升。相關(guān)代碼如下:
class RAttentionLayer(nn.Module):
def
__init__
(
self,
...
def forward(self, x, r, mask=None, extra_dim=False):
if isinstance(x, torch.Tensor):
x_src = x_dst = self.attn_prenorm_x_src(x)
x_dst = x_dst
else:
x_src, x_dst = x
if self.bipartite:
x_src = self.attn_prenorm_x_src(x_src)
x_dst = self.attn_prenorm_x_dst(x_dst)
else:
x_src = self.attn_prenorm_x_src(x_src)
x_dst = self.attn_prenorm_x_src(x_dst)
x = x[1]
attn = self._attn_block(
x_src, x_dst, r, mask=mask, extra_1dim=extra_dim
) # [B, pl, h*d]
x = x + attn
x2 = self.ff_prenorm(x)
x = x + self.ff_mlp(x2)
return x
代碼路徑:/usr/local/lib/python3.10/dist-packages/hat/models/task_modules/qcnet/rattention.py
4.1.5 DecoderDecoder 采用類似 detr 的解碼器來處理一對多的 K 個軌跡預(yù)測的問題,首先利用了一個遞歸的、無錨點的 proposal 模塊來生成自適應(yīng)軌跡錨點,初步預(yù)測未來位置、方向,然后是進(jìn)一步 refine 初始 proposals。
為了優(yōu)化性能,去除了 decoder 中 proposal 階段 a2m 的 RAttenion 操作,相關(guān)代碼如下:
def forward(self, data: dict, scene_enc: dict):
B, A = data["decoder"]["mask_dst"].shape[:2]
M = self.num_modes
HT = self.HT
QT = self.num_t2m_steps
pt = HT - QT
...
for t in range(self.num_recurrent_steps):
for i in range(self.num_layers):
# [B, A, HT, D],[B, A, M, D], [B, A, M, HT, D]
m = self.t2m_propose_attn_layers_t[t][i](
(x_t, m), r_t2m6, extra_dim=True, mask=mask_t2m6
)
# [B, A, M, D]
m = m.transpose(1, 2)
m = m.reshape([B, M * A, -1])
# [B, pl, D] [B, M
*A, D], [B, M*
A, pl, D]
m = self.pl2m_propose_attn_layers_t[t][i](
(x_pl, m), r_pl2m6, mask=mask_pl2m6
)
#去除了proposal階段a2m的RAttenion操作
"""
# [B, A, D], [B, M
*A, D], [B, M*
A, A, D]
m = self.a2m_propose_attn_layers_t[t][i](
(x_a, m), r_a2m6, mask=mask_a2m6
)"""
m = m.reshape(B, M, A, D).transpose(1, 2)
# [B, A, M, D]
m = self.m2m_propose_attn_layer_t[t](m, None, extra_dim=True)
...
代碼路徑:/usr/local/lib/python3.10/dist-packages/hat/models/task_modules/qcnet/qc_decoder.p
4.2 量化精度優(yōu)化4.2.1 FourierConvEmbeddingQCNetMapEncoder 和 QCNetAgentEncode 的輸入中存在距離計算、torch.norm 等對量化不友好的操作,為了提升量化精度,將輸入全部置于預(yù)處理中,相關(guān)代碼如下所示:
class QCNetOEAgentEncoderStream(nn.Module):
def
__init__
(
self,
...
) -> None:
super().
__init__
()
def build_cur_r_inputs(self, data, cur):
pos_pl = data["map_polygon"]["position"] / 10.0
orient_pl = data["map_polygon"]["orientation"]
pos_a = data["agent"]["position"][:, :, :cur] / 10.0 # [B, A, HT, 2]
head_a = data["agent"]["heading"][:, :, :cur] # [B, A, HT]
vel = data["agent"]["velocity"][:, :, :cur, : self.input_dim] / 10.0
...
def build_cur_embs(self, data, cur, map_data, x_a_his, categorical_embs):
B, A = data["agent"]["valid_mask"].shape[:2]
D = self.hidden_dim
ST = self.time_span
pl_N = map_data["x_pl"].shape[1]
mask_a_cur = data["agent"]["valid_mask"][:, :, cur - 1]
....
另外, 由于 QCNet 模型 weight init 是分算子類型初始化的,embedding 改 conv 后 init weight 應(yīng)當(dāng)對齊 embedding 類型,具體為 embedding 的 weight 是 std=0.02;而且,相對速度,距離等會和角度量一起計算,保持相近的 scale 更加有利于量化。因此,在預(yù)處理時,將 position 等量輸入除以 10 后輸入到模型,相關(guān)代碼如下所示:
def build_map_r_inputs(self, data: dict):
#"position"信息除以10
pos_pt = data["map_point"]["position"] / 10.0
orient_pt = data["map_point"]["orientation"]
pos_pl = data["map_polygon"]["position"] / 10.0
orient_pl = data["map_polygon"]["orientation"]
...
return {"r_pl2pl": r_pl2pl, "r_pt2pl": r_pt2pl}
代碼路徑:/usr/local/lib/python3.10/dist-packages/hat/models/task_modules/qcnet/preprocess.py
4.2.2 訓(xùn)練模塊不量化模型中存在 scale 分量只用于計算 loss,建議相關(guān)過程不量化,即在其前面插入 Dequanstub,否則會影響 QAT 訓(xùn)練。相關(guān)代碼:
loc_refine_pos = self.to_loc_refine_pos(m).view(
B * A, self.num_modes, self.num_future_steps, self.output_dim
)
#loc_refine_pos和loc_propose_pos的加法計算放在后處理以支持高精度輸出
loc_refine_pos = self.dequant(loc_refine_pos) + self.dequant(
loc_propose_pos.detach()
)
pi = self.to_pi(m).squeeze(-1).reshape(B * A, M)
if not self.deploy or self.training:
scale_refine_pos = (
F.elu(
self.to_scale_refine_pos(self.dequant(m)).view(
B * A,
self.num_modes,
self.num_future_steps,
self.output_dim,
),
alpha=1.0,
)
+ 1.0
+ 0.1
)
return {
"loc_propose_pos": self.dequant(loc_propose_pos) * 10.0,
"loc_refine_pos": self.dequant(loc_refine_pos) * 10.0,
"pi": self.dequant(pi),
"scale_propose_pos": self.dequant(scale_propose_pos),
"scale_refine_pos": self.dequant(scale_refine_pos),
}
else:
return {
"loc_refine_pos": self.dequant(loc_refine_pos) * 10.0,
"pi": self.dequant(pi),
}
代碼路徑:/usr/local/lib/python3.10/dist-packages/hat/models/task_modules/qcnet/qc_decoder.py
4.2.3 量化配置首先使用 QAT 的精度 debug 工具獲取量化敏感節(jié)點,然后在 Calibration 和量化訓(xùn)練時,分別對兩個輸出的 top86 和 top42 的量化敏感節(jié)點配置為 int16 量化;并且在量化訓(xùn)練時固定了激活的 scale,對量化精度更友好。相關(guān)代碼如下:
sensitive_table1 = torch.load(sensitive_path1)4.3 不支持算子替換4.3.1 cumsum
sensitive_table2 = torch.load(sensitive_path2)
# calibration時使用的敏感度模版
cali_qconfig_setter = (
sensitive_op_calibration_8bit_weight_16bit_act_qconfig_setter(
sensitive_table1,
#將量化敏感度排序前 86 的算子配置為int16
topk=86,
ratio=None,
),
sensitive_op_calibration_8bit_weight_16bit_act_qconfig_setter(
sensitive_table2,
#將量化敏感度排序前 42的算子配置為int16
topk=42,
ratio=None,
),
#int8量化的模版
default_calibration_qconfig_setter,
)
# 量化訓(xùn)練時使用的敏感度模版
qat_qconfig_setter = (
sensitive_op_qat_8bit_weight_16bit_fixed_act_qconfig_setter(
sensitive_table1,
#將量化敏感度排序前 86 的算子配置為int16
topk=86,
ratio=None,
),
sensitive_op_qat_8bit_weight_16bit_fixed_act_qconfig_setter(
sensitive_table2,
#將量化敏感度排序前 42的算子配置為int16
topk=42,
ratio=None,
),
#固定激活scale
default_qat_fixed_act_qconfig_setter,
)
print("Load sensitive table!")
公版模型的 QCNetDecoder 中使用了 征程 6 暫不支持的 torch.cumsum 算子,參考算法中將其替換為了 Conv1x1,相關(guān)代碼如下:
self.loc_cumsum_conv = nn.Conv2d( self.num_future_steps, self.num_future_steps, kernel_size=1, bias=False, ) self.scale_cumsum_conv = nn.Conv2d( self.num_future_steps, self.num_future_steps, kernel_size=1, bias=False, )
代碼路徑:
/usr/local/python3.10/dist-packages/hat/models/models/task_moddules/qcnet/qc_decoder.py
4.3.2 取余操作公版的 AgentEncoder 使用了除余操作“%”用于 wrap_angle,參考算法將其替換為了 torch.where 操作,并放到預(yù)處理部分。wrap_angle 實現(xiàn)代碼對比如下所示:
公版代碼實現(xiàn):
def wrap_angle( angle: torch.Tensor, min_val: float = -math.pi, max_val: float = math.pi) -> torch.Tensor: return min_val + (angle + max_val) % (max_val - min_val)
參考算法實現(xiàn):
def wrap_angle( angle: torch.Tensor, min_val: float = -math.pi, max_val: float = math.pi ) -> torch.Tensor: angle = torch.where(angle < min_val, angle + 2 * math.pi, angle) angle = torch.where(angle > max_val, angle - 2 * math.pi, angle) return angle
代碼路徑:/usr/local/python3.10/dist-packages/hat/models/models/task_modules/qcnet/utils.py
4.4 其它優(yōu)化4.4.1 適配流式推理QCNet 存在兩種推理方式:
對所有歷史幀數(shù)據(jù)并行 encode 后送入 decode 預(yù)測下一幀;
按照時序,逐幀 encode 后,最后一幀將前面 encoder 的結(jié)果拼接后送入 decoder 的流式推理。 流式推理符合實際部署的邏輯,但推理速度會變慢。但是,實際部署數(shù)據(jù)按時序逐幀給出,應(yīng)當(dāng)采用流式推理方案進(jìn)行 bc 模型的推理。
在不大幅影響浮點精度的情況下對模型進(jìn)行適當(dāng)?shù)牟眉簦瑒h除若干 LayerNorm,以及其它算子替換,提升部署效率;
重構(gòu) AttentionLayer,將 query、key、相對信息 r 的形狀均改為四維,對部署更加友好;
QCNet 模型中存在索引類操作,建議在使用 hrt_model_exec 工具進(jìn)行板端性能評測時使用真實數(shù)據(jù)輸入。
附錄論文:
公版模型代碼:https://github.com/ZikangZhou/QCNet
參考算法使用指南:
*博客內(nèi)容為網(wǎng)友個人發(fā)布,僅代表博主個人觀點,如有侵權(quán)請聯(lián)系工作人員刪除。