您好,登錄后才能下訂單哦!
作者:京東AI研究院 張建浩
煉丹師在轉換模型的時候,經常會發現給轉換前后的模型輸入同樣的圖片,模型結果有微小的差別。其中的原因有數值算法的誤差、不同 jpeg 解碼庫產生的結果不同等等,也有不同框架內部對某些算子的實現差異。
在給 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 操作的不同。
多維 tensor (例如二維圖像)的 resize 操作是用多個在一維 tensor 上進行的 resize 操作組合出來的,所以我們只討論一維 tensor 上的 resize 操作,經過分析各個框架的源代碼,我發現它的流程可以總結如下:
先討論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 已經修復了這個問題。
接下來討論另外兩個函數g和h,nearest, linear, cubic 這三種常見的 resize 的不同方式,是在g和h上有所不同。如上文所述,函數
得到離
最近的像素點,nearest 只需要找最近的一個像素點,linear 要找最近的兩個(左右各一個),cubic 要找最近的四個(左右各兩個);函數h(a,r)是計算這一個/兩個/四個像素點的加權平均值,其中權值是由r確定的(如上文所述,r是
距左側像素點的距離)。對 nearest/linear/cubic 的每一種來說,如何從r得到各個像素點的權值都有各自標準的實現,nearest resize 不必說,對于 linear resize,兩個像素點的權值是
。對 cubic 來說,四個像素點的權值是
[1]其中A是一個固定的參數,它的取值卻是每個框架不同,兩個常見的選擇是 -0.5 (TensorFlow 部分版本的實現)和 -0.75(PyTorch)。因為A沒有統一的標準取值,所以各個框架的 cubic resize 結果不同是常見的事情。
補充一句題外話:cubic resize 的權值計算起來比 linear resize 復雜的多,所以它的耗時肯定會長一些,但產生的圖像性質更好(這篇 paper ???? https://arxiv.org/abs/1812.0118 7 發現圖片預處理使用 cubic resize 可以提升分類網絡準確率)。
還有一個會引起 cubic resize 結果差異的細節是,cubic resize 需要找到 的左右各兩個最相鄰的像素點,但 左右兩側不一定能保證各有兩個像素點(假設某種情況下計算得到 ,那么它左邊只有一個像素點),此時也有兩種現存的不同方法,一種是對圖像做 edge padding,即認為仍從左邊找到了兩個像素點,并且這兩個像素點的值都是第一個像素點的值;另一種是認為找到了三個而不是四個像素點,并對三個像素點的權值做歸一化。
總結一下,各個框架 Resize 操作的結果不同的原因是多種多樣的,例如 TensorFlow 用了自己發明的錯誤實現、cubic resize 中參數 A 沒有固定的取值、非整數的
是否自動取整等等。
ONNX Resize 算子的 spec 就是基于上面的分析寫出來的,具體的描述在???? https://github.com/onnx/onnx/bl ob/master/docs/Operators.md#Resize ,
Python 版的參考實現在 ???? https://github.com/onnx/onnx/bl ob/master/onnx/backend/test/case/node/resize.py
其中比較核心的屬性 coordinate_transformation_mode 是把w、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/doc ument/1163711
歡迎點擊“ 京東智聯云 ”了解更多精彩內容!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。