交互,破解多變量時序預(yù)測難題)
做多變量時間序列預(yù)測這幾年我最頭疼的不是把單條曲線的趨勢拉準而是變量和變量之間的關(guān)系。一開始我也迷信 Transformer 那套覺得讓注意力自己去“發(fā)現(xiàn)”變量間的相關(guān)性就完事了結(jié)果跑電力負荷、交通流這類數(shù)據(jù)時很快發(fā)現(xiàn)注意力給出來的關(guān)系在推理階段幾乎是固定的而真實場景里的變量交互根本不是這個樣子——凌晨的用電關(guān)系和晚高峰的用電關(guān)系能差出一倍。后來我把目光從“全局關(guān)系矩陣”挪到“局部交互模式”上逐步攢出了 FACT 這套思路也就是用細粒度跨變量卷積Fine-grained Cross-variable Convolution來建模動態(tài)變量交互。這篇文章就把這套方法從動機到實現(xiàn)、再到踩坑復(fù)盤完整拆開講。想直接抄代碼的想搞懂為什么這樣做更合理的應(yīng)該都能有收獲。1. 變量交互建模的隱痛靜態(tài)假設(shè)與全局注意力的失效場景1.1 大部分實際系統(tǒng)的交互是“時變”的先拋一個反常識的觀察許多人做多變量預(yù)測第一個動作就是求相關(guān)性矩陣或者讓模型學(xué)一個變量間相似度矩陣。這兩種做法本質(zhì)都在找一個“全局平均”的關(guān)系但真實系統(tǒng)的交互幾乎都是時變的。拿電力負荷舉例氣溫對用電量的影響夏季空調(diào)季是強正相關(guān)春秋季又接近無關(guān)到了采暖季還會變成另一種形態(tài)工作日的變量交互結(jié)構(gòu)和節(jié)假日完全不同。哪怕壓縮到一天里凌晨由工業(yè)負荷主導(dǎo)、晚間由居民負荷主導(dǎo)各變量之間的主導(dǎo)關(guān)系也在不斷切換。用一個靜態(tài)矩陣去擬合這種變化結(jié)果只能是折中——高峰段對不齊低谷段也不準。我在一次交通流預(yù)測的項目里第一次被這個問題擊中。給模型喂了 40 多個路口的流量數(shù)據(jù)整體相關(guān)性矩陣顯示 A 路口和 B 路口相關(guān)度只有 0.3但按小時切片算早高峰能到 0.8深夜只有 0.05。如果只在整體相關(guān)度上篩閾值這兩個路口會被認為“關(guān)系弱”而直接被忽略可早高峰那兩小時它們明顯在互相影響。這讓我徹底放棄了“一個矩陣打天下”的思路。1.2 注意力機制也在“偷懶”有人會說用 Transformer 總該能捕捉動態(tài)交互了吧每個時間步的注意力權(quán)重不是不一樣嗎理論上確實不一樣但實際訓(xùn)練之后你會發(fā)現(xiàn)幾個問題。第一注意力 softmax 在長序列上很容易趨于均勻化梯度信號也會把那些“穩(wěn)定出現(xiàn)”的模式強化最終學(xué)到的仍然是一種平均關(guān)系。第二在變量維度上做全局注意力復(fù)雜度是變量數(shù)的平方變量一多上百個傳感器節(jié)點訓(xùn)練非常痛苦顯存開銷直線上升。第三也是常被忽略的一點點積注意力建模的是對稱的相似度變量 A 對變量 B 的影響和 B 對 A 的影響在權(quán)重上天然不區(qū)分方向。但現(xiàn)實里這種方向性交互比比皆是電價上漲會帶動光伏出力調(diào)整反過來光伏出力卻幾乎不影響電價這種不對稱靠對稱注意力根本表達不出來。當然真要強行表達方向性可以在注意力里加方向編碼或者用非對稱的注意力變體但代價是更復(fù)雜的結(jié)構(gòu)、更多的調(diào)參成本。至少我在自己項目里試下來收益不夠填復(fù)雜度。1.3 “細粒度”到底指什么我理解的細粒度有三個層面缺一不可細粒度維度核心含義靜態(tài)建模為何不行時間維度交互模式不是整段序列共享一套參數(shù)而是隨小時間窗動態(tài)變化靜態(tài)矩陣只能給出一段時期的平均值變量維度不是所有變量共享一個相關(guān)性矩陣而是每個變量或相鄰變量簇有各自的局部交互模式全局矩陣把強相關(guān)的細節(jié)全部平均掉方向維度A 影響 B 與 B 影響 A 的強度可以不同相關(guān)性和對稱注意力都天然不區(qū)分方向FACT 的核心是想用盡可能簡單的操作把這三個層面的細粒度統(tǒng)一起來——在變量軸上做小核卷積同時讓卷積核參數(shù)隨輸入內(nèi)容動態(tài)生成。2. FACT 的核心機關(guān)從“學(xué)一個矩陣”到“動態(tài)卷積核”2.1 為什么選卷積先看數(shù)據(jù)形態(tài)多變量序列一般是 B×T×V 的張量B 是 batchT 是時間長度V 是變量數(shù)。傳統(tǒng)做法是構(gòu)造 V×V 的交互矩陣去乘 X參數(shù)和計算量都隨 V 平方增長或者讓所有變量在每個時間步上互相做注意力計算量依然逃不掉 V2。卷積的做法是把 V 當作“序列長度”用一個長度為 k 的卷積核沿著變量軸滑動每次只看相鄰的 k 個變量。為什么這種局部假設(shè)合理因為現(xiàn)實中變量交互大多發(fā)生在鄰近結(jié)構(gòu)電網(wǎng)里相鄰母線的相互影響遠強于遠端節(jié)點交通流里相鄰路段的相關(guān)性最強氣象站里隔幾百公里的觀測點幾乎談不上交互。局部性不等于放棄全局堆疊多層之后感受野層層擴大第一層窗口相鄰變量、第二層窗口次近鄰、第三層就能覆蓋十幾個變量甚至更遠的交互。這和 CNN 在圖像里從邊緣到紋理再到物體輪廓的路徑是同一個套路。卷積還有一個現(xiàn)實優(yōu)勢參數(shù)共享。同一個卷積核在所有變量位置復(fù)用不會因為變量數(shù)量增加就參數(shù)爆炸。原始的靜態(tài)卷積只有 k 個參數(shù)比 V×V 的矩陣小太多了。即便加上動態(tài)生成機制參數(shù)量的增長也是線性的整體仍然可控。2.2 動態(tài)核是怎么來的FACT 的關(guān)鍵在“動態(tài)”兩個字。最簡單的實現(xiàn)思路叫條件卷積先把時間維度的全局特征池化成一個小向量比如 mean-pool 或 max-pool把這個向量喂給一個小型 MLP輸出一組卷積核權(quán)重。這組權(quán)重就是針對當前輸入內(nèi)容“定制”的。為什么這個設(shè)計有效全局池化保留了這段序列的整體狀態(tài)現(xiàn)在整體是在高峰還是低谷、像工作日還是節(jié)假日、系統(tǒng)處于穩(wěn)態(tài)還是震蕩期。MLP 根據(jù)這個狀態(tài)決定變量交互的形態(tài)相當于“大局定框架局部填細節(jié)”。整體狀態(tài)編碼和局部交互組合正好對應(yīng)我們做人工判斷時的兩步邏輯先看當前處于什么場景再判斷在這個場景下誰和誰更相關(guān)。實際做的時候有個小細節(jié)動態(tài)核的初始化和訓(xùn)練穩(wěn)定性要特別小心。如果 MLP 隨機初始化剛開始產(chǎn)出的核可能讓輸出分布崩掉。我習(xí)慣把 MLP 最后一層的權(quán)重初始化為接近 0偏置初始化成單位脈沖的形狀這樣初始狀態(tài)下接近恒等映射訓(xùn)練會更穩(wěn)后面再讓梯度慢慢把核“推開”。2.3 時間分支與變量分支如何協(xié)作跨變量卷積只回答了“變量之間怎么交互”序列本身的時間依賴還得有人管。我在 FACT 里的做法是雙分支并行時間分支在時間軸做因果卷積或膨脹卷積負責(zé)捕捉局部趨勢、周期、突變跨變量分支在變量軸做細粒度卷積負責(zé)捕捉同一時刻變量間的橫向交互兩個分支的輸出逐元素相加再過 LayerNorm 和殘差堆疊多層。這樣設(shè)計的好處是兩類信息互不干擾不需要擠在同一個矩陣里互相妥協(xié)。有的數(shù)據(jù)集長程時間依賴特別強就把時間分支換成自注意力有的數(shù)據(jù)集變量交互特別復(fù)雜就加深跨變量分支、加大卷積核。組合式的結(jié)構(gòu)讓 FACT 在不同數(shù)據(jù)上遷移起來非常方便。整體流程可以這樣看輸入 X → 實例歸一化 → 可選 patch 切分與嵌入 → 多層并行塊時間分支 跨變量分支各帶殘差與歸一化→ 輸出投影 → 逆歸一化 → 預(yù)測 Y。實例歸一化處理分布偏移patch 切分能縮短序列、降低后續(xù)計算量這兩個都是被 PatchTST 等結(jié)構(gòu)驗證過的預(yù)處理技巧直接拿來配合使用就行。3. 把 FACT 寫進代碼維度變換、超網(wǎng)絡(luò)與堆疊細節(jié)3.1 先跑通最簡單的靜態(tài)版本不要一上來就上動態(tài)核。我建議先實現(xiàn)一個靜態(tài)跨變量卷積把維度變換搞清楚再說。最簡單的寫法是把變量軸當成卷積分組里的空間軸用 conv1d 處理import torch import torch.nn as nn class StaticCrossVariableConv(nn.Module): def __init__(self, kernel_size5): super().__init__() self.kernel_size kernel_size self.conv nn.Conv1d( in_channels1, out_channels1, kernel_sizekernel_size, paddingkernel_size // 2, biasFalse, ) def forward(self, x): # x: (B, T, V) - reshape 把時間步當成批次 b, t, v x.shape x x.reshape(b * t, 1, v) # (B*T, 1, V) y self.conv(x) # 沿變量維度卷積 return y.reshape(b, t, v)這里把 B×T 合并成一個批次維度對每個時間步單獨在變量軸上做卷積。padding 用 same 模式保證輸出長度不變。別小看這一步我見過不少朋友在維度上栽跟頭明明想卷變量軸結(jié)果把變量當通道卷時間軸了錯誤很隱蔽。這個靜態(tài)版本已經(jīng)能驗證“跨變量卷積”這個方向是否值得繼續(xù)。拿它在自己的數(shù)據(jù)上跑幾輪如果比線性基線有明顯提升再上動態(tài)核不遲如果連靜態(tài)版都沒有收益那說明數(shù)據(jù)里變量交互的局部性可能很弱動態(tài)版大概率也救不回來。3.2 動態(tài)核版本條件卷積的實現(xiàn)動態(tài)核的核心是用全局池化特征生成卷積權(quán)重。下面是一個教學(xué)級簡潔實現(xiàn)class DynamicCrossVariableConv(nn.Module): def __init__(self, num_vars, kernel_size5, hidden_dim64): super().__init__() self.num_vars num_vars self.k kernel_size self.hidden_dim hidden_dim # 超網(wǎng)絡(luò)從全局池化特征生成動態(tài)卷積核 self.condition_net nn.Sequential( nn.Linear(num_vars, hidden_dim), nn.GELU(), nn.Linear(hidden_dim, num_vars * kernel_size), ) # 動態(tài)核初始化 self._init_kernel_weights() def _init_kernel_weights(self): # 讓初始權(quán)重接近“只關(guān)注自身”中心位置為1其余為0 with torch.no_grad(): last self.condition_net[-1] last.weight.zero_() last.bias.zero_() mid self.k // 2 for i in range(self.num_vars): last.bias[i * self.k mid] 1.0 def forward(self, x): # x: (B, T, V) b, t, v x.shape cond x.mean(dim1) # (B, V) weights self.condition_net(cond) # (B, V*K) weights weights.view(b, v, self.k) # (B, V, K) # 變量維度滑窗 pad self.k // 2 x_padded torch.nn.functional.pad(x, (pad, pad), modereplicate) patches x_padded.unfold(2, self.k, 1) # (B, T, V, K) # 每個變量用對應(yīng)的動態(tài)核沿K求和 out (patches * weights.unsqueeze(1)).sum(dim-1) # (B, T, V) return out主要步驟拆開看cond 是每個樣本的時間池化特征喂給一個兩層 MLP 得到 B×V×K 的核權(quán)重torch padding 后用 unfold 提取每個變量位置長度為 K 的窗口最后用動態(tài)核與窗口逐位置相乘再求和。unfold 在這種小模塊里夠用如果變量數(shù)很大、追求速度可以用 im2col 思路替換但大部分場景不需要。再說一遍初始化為什么重要動態(tài)核是生成出來的如果 MLP 隨機初始化早期輸出分布會被奇怪的卷積核帶偏。上面我先把最后一個 Linear 的權(quán)重整個歸零、再把中心位置偏置設(shè)成 1初始時每個位置基本只看自己相當于恒等映射訓(xùn)練穩(wěn)定性立刻好很多。這個細節(jié)幫我省了不少調(diào)試時間。3.3 多層堆疊與感受野控制跨變量卷積不是一層就完事重點在堆疊。每層核寬 K堆疊 L 層后一個變量能影響的變量范圍是 1 L × (K?1)。比如 K5、堆 3 層感受野就是 13 個變量K7、堆 4 層感受野 25 個變量。堆疊的好處是淺層學(xué)局部小簇交互、深層學(xué)簇與簇之間的組合信息逐層抽象。堆疊時有三個經(jīng)驗值可以抄每層都用 same padding保持變量維度長度不變最后輸出層再做降維或投影每層輸出接 LayerNorm再套殘差。這一點在下章會展開說順序影響很大不要為了“加深”而加深先按感受野需求算層數(shù)。如果最強的交互隔著 20 個變量核寬 5 的模型堆 4 層就夠硬堆 10 層只會讓模型更笨重計算量也可以簡單算一下每層跨變量卷積的開銷是 O(T·V·K)堆 L 層就是 O(L·T·V·K)和變量注意力 O(T·V2) 相比在 V 較大時優(yōu)勢非常明顯。比如 V100、K5、L4跨變量卷積的乘加次數(shù)是 2000T而注意力光相關(guān)性矩陣就是 10000T還沒算 softmax 和其他開銷。3.4 和 Patch、實例歸一化怎么銜接Patch 處理在 PatchTST 等模型里被廣泛驗證把時間序列切成長度為 P、可能有重疊的小段每段嵌入成一個 token。FACT 完全可以和 patch 配合用。切完 patch 之后時間長度從 T 變成約 T/P跨變量卷積在變量軸的運算量不變但時間分支的后續(xù)計算會更省。我在實踐中用 patch 長度 16、步長 8 起步序列長度減半模型訓(xùn)練速度快很多預(yù)測精度基本不受損失。實例歸一化一定要做在主干網(wǎng)絡(luò)之前。它的作用是消除不同樣本間的均值/方差漂移尤其適合電力、流量這類分布會隨季節(jié)、事件變化的數(shù)據(jù)。做法很簡單對每個樣本的每個變量先減均值除以標準差模型輸出后再把預(yù)測結(jié)果按原來的統(tǒng)計量逆縮放。注意逆歸一化要在輸出層做完之后直接作用到最終預(yù)測值上不要夾在中間層否則統(tǒng)計信息會被模型內(nèi)部操作扭曲。4. 實驗復(fù)盤的三個深坑變量順序、核寬度、歸一化時機4.1 坑一變量順序決定生死這是我在項目里碰到的第一道坎。最初的 FACT 版本效果平平比線性模型沒強多少一度想放棄。在固定隨機種子、控制變量的前提下排查了很久懷疑學(xué)習(xí)率、懷疑動態(tài)核初始化、懷疑數(shù)據(jù)預(yù)處理把這些全部排除之后最后把輸入變量的列順序改了一下——按相關(guān)性做了一次層次聚類重排——模型效果立刻上了一個臺階MSE 大約下降了兩三成不同數(shù)據(jù)集幅度不同。這個現(xiàn)象背后的原理其實很直白卷積的局部性假設(shè)要求“順序即語義”相鄰位置的變量最好真的相關(guān)。如果變量列順序是隨意錄入的卷積窗口掃過去抓到的全是弱相關(guān)配對等于讓模型學(xué)一堆噪聲關(guān)系。解決方案有三檔最省事訓(xùn)練前用滑動窗口相關(guān)性矩陣做層次聚類把高相關(guān)變量排在一起固定這個順序訓(xùn)練更穩(wěn)訓(xùn)練時對變量順序做隨機 shuffle 增強。雖然卷積核是平移共享的但重排能讓模型不容易依賴某個絕對位置泛化更好更靈活把變量分組每組用獨立的卷積核類似分組卷積組內(nèi)排序、組間互不干擾如果你發(fā)現(xiàn)自己數(shù)據(jù)集里的變量順序無法改變可以先打開 shuffle 增強試一周。這個改動成本最低效果經(jīng)常超出預(yù)期。4.2 坑二卷積核寬度不是越大越好另一個折騰我兩天的問題是核寬度。直覺上覺得窗口越大看到的交互越多但把 K 從 3 一路調(diào)到 9、15 之后驗證指標先升后降K9 左右就開始出現(xiàn)退化。原因也好解釋窗口越大卷積的輸出越接近窗口內(nèi)變量的平均值細粒度交互被“糊”掉了。這和圖像里大卷積核容易丟細節(jié)是同一個原理——過平滑效應(yīng)。我后來用網(wǎng)格搜索在 K∈{3,5,7,9,15} 里試發(fā)現(xiàn)大部分數(shù)據(jù)集的最佳點在 5 到 7 之間。中小數(shù)據(jù)集建議直接定 5 起步別一上來就調(diào)大只有在變量數(shù)很多超過 200或者局部結(jié)構(gòu)特別復(fù)雜時才值得試更大的核。還有一個容易忽視的點核寬加大意味著可表達模式變多數(shù)據(jù)量不夠時學(xué)到的多半是噪聲所以要同步考慮過擬合風(fēng)險。4.3 坑三歸一化的位置影響訓(xùn)練穩(wěn)定性這個坑比較隱蔽但殺傷力很大。我在一套數(shù)據(jù)上訓(xùn)練時loss 曲線在 20 個 epoch 后開始劇烈震蕩試了調(diào)學(xué)習(xí)率、換優(yōu)化器都不管用。后來逐層打印激活值才發(fā)現(xiàn)跨變量卷積的輸出方差在深層里膨脹了——因為卷積本質(zhì)是窗口內(nèi) k 個值的加權(quán)和如果權(quán)重沒有約束方差會隨層數(shù)指數(shù)放大。解決辦法是把歸一化嵌入到每一層內(nèi)部在跨變量卷積之前先做一次輸入歸一化卷積輸出后再接一次 LayerNorm然后再進殘差。這個“前置 后置”的做法在深層 Transformer 里很常見放到 FACT 的跨變量模塊里同樣成立而且比只在 block 出口做一次歸一化要穩(wěn)得多。順帶一個小技巧動態(tài)核生成網(wǎng)絡(luò)里MLP 最后一層如果用了偏置初始時記得把卷積核的均值控制住。均值偏大等于給每個位置都加了一個常數(shù)偏移層級一深偏移會累積成系統(tǒng)性偏差各種指標都會異常。4.4 數(shù)據(jù)量少時動態(tài)核比靜態(tài)核更容易過擬合動態(tài)核的代價是參數(shù)增加——每層多了一個超網(wǎng)絡(luò)變量數(shù)超過 100 時這部分參數(shù)相當可觀。我一開始在只有幾十條序列的小數(shù)據(jù)集上直接上動態(tài)核驗證 loss 比訓(xùn)練 loss 高出一大截典型的過擬合信號。后來做了個簡單消融小數(shù)據(jù)集用靜態(tài)核 固定排序效果反而更好。我的建議是訓(xùn)練樣本量在幾千條以下時先用靜態(tài)核打底確認收益之后再切換動態(tài)核?;蛘呓o動態(tài)核生成網(wǎng)絡(luò)加一些正則比如把核權(quán)重的 L2 約束加上或者用低秩分解把 MLP 的輸出維度降下來。動核不是免費的午餐它的代價是參數(shù)變多這一點要心里有數(shù)。5. 哪些任務(wù)適合 FACT哪些任務(wù)別硬上5.1 優(yōu)勢場景中等變量數(shù)、強局部交互、交互時變FACT 最出彩的場景有三類特征變量數(shù)量在 20 到 200 之間變量間普遍存在相鄰或局部相關(guān)性交互結(jié)構(gòu)會隨狀態(tài)切換變化。我在電力負荷、城市交通流、氣象觀測這三類數(shù)據(jù)上都跑出了還不錯的效果電力相鄰饋線的負荷聯(lián)動、氣溫對用電量的時變影響FACT 的動態(tài)核在這個場景下能學(xué)出晝夜兩種明顯的交互模式交通路口間流量傳導(dǎo)主要發(fā)生在空間相鄰路段配合按相關(guān)性排序FACT 比全局注意力更早捕捉到早高峰的聯(lián)動變化氣象站點之間的相互影響隨天氣系統(tǒng)移動而變化跨變量卷積的局部性假設(shè)和真實物理過程天然對齊你也可以在部署前做一個快速自檢取同一天的數(shù)據(jù)按一小時切窗計算變量間相關(guān)性矩陣。如果這些矩陣在不同時段的差異明顯FACT 動態(tài)核的用武之地就很大如果矩陣非常穩(wěn)定靜態(tài)方法可能就夠用了。5.2 不適合的場景變量數(shù)目太少比如只有三五個時卷積的局部性假設(shè)沒有意義直接用線性層或者 Transformer 反而更簡單。變量數(shù)量太多比如上千個高維傳感器每層的動態(tài)核生成網(wǎng)絡(luò)會帶來不小的參數(shù)量這時先做變量篩選或者降維再接 FACT 會更合適。如果數(shù)據(jù)里變量相互之間獨立性極強幾乎沒有可建模的交互那 FACT 學(xué)到的只會是噪聲——這種情況下先考慮單變量模型別急著上多變量復(fù)雜結(jié)構(gòu)。還有一種情況要特別提醒最強的交互隔了非常遠比如跨了 50 個變量卷積要靠深層堆疊才能覆蓋。層數(shù)一多訓(xùn)練難度和收益都會變得不劃算這時候用圖神經(jīng)網(wǎng)絡(luò)或者全局注意力反而更直接。選擇模型之前先花半天把數(shù)據(jù)里的“交互距離”摸清楚能幫你省下一兩周的無效調(diào)參。5.3 還能怎么玩FACT 的動態(tài)核是個天然的可解釋性窗口。我訓(xùn)練好模型后在某段序列上推理把最后一層跨變量卷積生成的核畫成熱力圖能看到變量交互隨著狀態(tài)切換的變化——這比單純給出預(yù)測結(jié)果更有價值至少能回答業(yè)務(wù)方“為什么這兩個變量這段時間會一起動”這種問題。后續(xù)我還在考慮兩個擴展方向把核生成網(wǎng)絡(luò)換成低秩分解形式進一步壓參數(shù)以及給動態(tài)核加稀疏約束讓模型自動挑出最重要的變量交互路徑。這些改動都不影響整體框架屬于在 FACT 這個“殼子”里做局部替換感興趣的同學(xué)可以直接在自己的數(shù)據(jù)上動手試。