煉丹師在轉換模型的時候,常常會發現給轉換先後的模型輸入一樣的圖片,模型結果有微小的差異。其中的緣由有數值算法的偏差、不一樣 jpeg 解碼庫產生的結果不一樣等等,也有不一樣框架內部對某些算子的實現差別。node
在給 ONNX 貢獻 Resize 算子的 spec 的時候,我發現 Resize 是一個突出體現了框架實現差別的算子——多種 Resize 類型、不統一的超參數、將錯就錯的歷史遺留 bug 和其它極易被忽略的問題集中在一塊兒,致使幾乎每一個框架的 Resize 操做的結果都有差別,而 ONNX 是一個神經網絡模型的中間格式,它應該儘可能保留原始框架的算子的語義。通過查看相關論文和各類框架的源代碼,我分析和總結了 Resize 操做衆多的實現方式。最終爲 ONNX 貢獻了一個較爲完善的、標準化的 Resize 算子的 spec,它包含多個(基本)正交的參數,TensorFlow 1.x、TensorFlow 2.x、PyTorch、OpenCV 的 resize/interpolation 方法均可以用這個算子 100% 無損的表達。本文將簡單介紹各類 resize 操做的共同流程,並分析是哪些因素引發了不一樣框架 resize 操做的不一樣。git
多維 tensor (例如二維圖像)的 resize 操做是用多個在一維 tensor 上進行的 resize 操做組合出來的,因此咱們只討論一維 tensor 上的 resize 操做,通過分析各個框架的源代碼,我發現它的流程能夠總結以下:github
先討論w和f,w(i)是第 i 個像素點的座標,乍一看, w(i)徹底能夠等於i自己,其實沒有這麼簡單。例如一個長度爲 3 的 tensor,若是第i個像素點的座標等於i自己,那麼三個像素點在tensor 中的位置就以下圖中最左邊的樣子,橫線的長度表明一維 tensor 的長度,圓圈表明像素點:算法
三個像素點沒有對稱地分佈在 tensor 上,而是往左偏了。出於直覺,咱們以爲這不是一件特別好的事情。在各類框架中,有兩種常見的方法來解決這個問題:網絡
一個是選取w(i)=i+0.5,以一個長度爲 3 的一維 tensor 爲例,它第 0 個像素點在 0.5 位置,第 1 個像素點在 1.5 位置,第 2 個像素點在 2.5 位置,這稱爲 half_pixel,也就是上圖中中間的方法。這種方法中,
(這很符合直覺)。另外一個是仍讓w(i)=i,但改變函數f,使
仍以長度爲 3 的一維 tensor 爲例,這種方法至關於在 resize 時砍掉了最右邊長度爲 1 的部分,使像素點的分佈「被」對稱了。這稱爲 align_corner,也就是上圖中最右邊的方法,在各類框架的 resize 方法的參數裏常見的 align_corner=True/False 就是它了,它的名字來源於它可讓 tensor 中第一個和最後一個像素(即 corner)在縮放後保持不變。框架
那若是咱們不採用這兩種方法,必定要使用「直覺很差」的 asymmetric 方法,究竟會發生什麼呢?TensorFlow 1.x 就給咱們提供了這樣一個反面典型,它在 align_corner=False 時的實現是錯的,緣由就是使用了上圖中錯誤的 asymmetric 方法,這會致使奇怪的縮放結果,這篇博客中🔗https://hackernoon.com/how-tensorflows-tf-image-resize-stole-60-days-of-my-life-aba5eb093f35,函數
做者用 TensorFlow 1.x 訓練的超分辨率神經網絡老是出現奇怪的問題,最終他發現問題根源是 TensorFlow 錯誤的 resize 實現,他還給了一個形象的例子:把 16x16 的下圖左側圖像縮小到 4x4,本應獲得以下圖右側所示的圖像,而 TensorFlow 1.x 卻給出了下圖中間的奇怪結果,圖像的對稱性被徹底破壞了,其中的緣由就如上文所述。TensorFlow 1.x 的 resize 結果和其它框架不一樣的一大緣由就是它錯誤的 resize 實現,好在 TensorFlow 2.x 已經修復了這個問題。spa
接下來討論另外兩個函數g和h,nearest, linear, cubic 這三種常見的 resize 的不一樣方式,是在g和h上有所不一樣。如上文所述,函數g(i')獲得離i'最近的像素點,nearest 只須要找最近的一個像素點,linear 要找最近的兩個(左右各一個),cubic 要找最近的四個(左右各兩個);函數h(a,r)是計算這一個/兩個/四個像素點的加權平均值,其中權值是由r肯定的(如上文所述,r是i'距左側像素點的距離)。對 nearest/linear/cubic 的每一種來講,如何從r獲得各個像素點的權值都有各自標準的實現,nearest resize 沒必要說,對於 linear resize,兩個像素點的權值是
對 cubic 來講,四個像素點的權值是
其中A是一個固定的參數,它的取值倒是每一個框架不一樣,兩個常見的選擇是 -0.5 (TensorFlow 部分版本的實現)和 -0.75(PyTorch)。由於A沒有統一的標準取值,因此各個框架的 cubic resize 結果不一樣是常見的事情。3d
補充一句題外話:cubic resize 的權值計算起來比 linear resize 複雜的多,因此它的耗時確定會長一些,但產生的圖像性質更好(這篇 paper 🔗https://arxiv.org/abs/1812.01187發現圖片預處理使用 cubic resize 能夠提高分類網絡準確率)。rest
還有一個會引發 cubic resize 結果差別的細節是,cubic resize 須要找到i'的左右各兩個最相鄰的像素點,但i'左右兩側不必定能保證各有兩個像素點(假設某種狀況下計算獲得i'=0.6,那麼它左邊只有一個像素點),此時也有兩種現存的不一樣方法,一種是對圖像作 edge padding,即認爲仍從左邊找到了兩個像素點,而且這兩個像素點的值都是第一個像素點的值;另外一種是認爲找到了三個而不是四個像素點,並對三個像素點的權值作歸一化。
總結一下,各個框架 Resize 操做的結果不一樣的緣由是多種多樣的,例如 TensorFlow 用了本身發明的錯誤實現、cubic resize 中參數 A 沒有固定的取值、非整數的
是否自動取整等等。
ONNX Resize 算子的 spec 就是基於上面的分析寫出來的,具體的描述在🔗https://github.com/onnx/onnx/blob/master/docs/Operators.md#Resize,
Python 版的參考實如今 🔗https://github.com/onnx/onnx/blob/master/onnx/backend/test/case/node/resize.py
其中比較核心的屬性 coordinate_transformation_mode 是把w、f和w^(-1)複合獲得的單個函數f',即
在這裏沒有用獨立的函數w和f的緣由除了看起來更簡單以外,也有解決現實問題的考慮——有一些框架的某些 resize 實現沒有使用
的形式,而是直接讓
雖然這顯然是不合理的(coordinate_transformation_mode=tf_half_pixel_for_nn 就描述了這樣一個不合理的實現),但也只能認可它們的存在。相比起來,上一個版本的 ONNX Resize 算子 spec 的制定者沒有意識到 Resize 算子的複雜性,徹底模仿了 TensorFlow 的實現,不只和其它框架的結果不一致,並且連 TensorFlow 的 bug 也一併模仿了。
如今 TensorFlow、PyTorch 都支持了導出這一版本的 Resize 算子,TensorRT 等部署框架也支持導入和運行這個 Resize 算子。本身創造的東西能被衆多知名的框架跟進,我感到很是大的成就感。
參考:https://ieeexplore.ieee.org/document/1163711
歡迎點擊「京東智聯雲」瞭解更多精彩內容!