瀉藥,人在家中,剛下被窩。
2020真是多災多難的一年,又是蝗災,又是冠狀病毒,加上登革熱、埃博拉、禽流感,還有森林大火、火山噴發,搞得大家人心惶惶。
而我在2023年崩潰過的程式,有望趕超人生中前幾年之和。
上海是境外輸入非常嚴重的地區,這學期開學無望了,可以在家持續躺屍。
放假(霧)五個月,調了五個月的**,我已經快被整瘋了。
前幾天剛做完專業英語的神經網路大專案——對,專業英語是專業課,不是英語課。除錯的過**是崩潰啊,最終我發現了程式設計界的普遍規律:
bug存在性定理,程式設計師無論優劣,通常只會在最傻最簡單的函式中寫出bug
話不多說,先來說說我在debug卷積神經網路**的時候發現的一些**層面的問題。
更新預告:未來專欄將會介紹顯示卡計算框架cuda的使用。
由於卷積神經網路需要大量的並行運算,所以不得不借助gpu的大量廉價平行計算資源來加快執行速度。如何巧妙地設計演算法,使得cnn在主顯切換消耗盡可能少的時間,同時保證正確性,不引發訪問衝突,是cnn在**層面實現的難點。主要體現在:
1) 支援不同大小的輸入,且實現樣本並行
這在演算法層面需要應用空間金字塔池化層。而在實現細節上,每一層所保留的輸出和誤差對於每一batch內的不同樣本,可能只有通道數是相同的——這就限制了我們不能使用tensor4d。為了解決這一問題,我使用tensor陣列,並將它們開在視訊記憶體中,只在主存裡保留乙個指向它們的指標。為了減小主顯互動的開銷,主存中應該開乙個變數來單獨存放通道數。
樣本並行需要cuda的核函式呼叫核函式,那麼顯示卡必須要支援sm_50, compute_50及以上。
2) 巧妙利用cudadevicesynchronize()函式實現主顯同步,並盡量並行以減少開銷
由於神經網路的每一層都需要使用前一層的資料,因此要盡量少和晚地使用cudadevicesynchronize()函式——但debug時除外,開發時要盡量在每乙個核函式後面都加上cudadevicesynchronize()和cudagetlasterror()。
具體地,比如在卷積層反向傳播時,求前一層的誤差、卷積核權值的梯度、以及偏置量的梯度三者就可以平行計算,生產環境可以只使用乙個cudadevicesynchronize()和cudagetlasterror()。但debug時,如果cudagetlasterror()報錯,你只知道錯誤至少**於三個核函式中的乙個,而不知道具體是哪乙個;甚至三個同時出現了問題,這樣會增加debug的工作量,因此盡量分塊除錯。
而cuda除了核函式外所有i/o都是阻塞式的,包括cudamalloc和cudamemcpy等——因為它們的返回值並不是std::future型別,而是赤裸裸的cudaerror_t,以及curand裡的curandstatus_t、cufft裡的cufftresult_t等等列舉型別。因此只有核函式需要cudadevicesynchronize()實現同步。而同步的位置,可以是下一層前向傳播或者反向傳播呼叫之前,這將會充分利用主顯並行實現非同步計算。
由於主機往往會計算得快一些,最終出現host等待device的現象,我在第一輪debug時就出現了nan的問題,本來以為是梯度**,結果gradient clipping之後依然nan,最終發現了問題所在——cpu算得太快,顯示卡算得太慢,沒有通過cudadevicesynchronize()實現同步。
3) 可學習引數的儲存
卷積神經網路中,可學習的引數包括卷積層的卷積核和偏置向量、batchnorm層的scale和shift、全連線層的連線矩陣和偏置向量。其實概括起來就兩個詞:weight & bias。
為了減少主顯之間資料傳輸的開支,這些引數最好都開在視訊記憶體之中(batchnorm除外,因為它只有兩個浮點數,不過我也把它們開在了device中)。
但具體來看,卷積核確實可以用tensor陣列來儲存,但用tensor4d也比它好——因為同一層所有的卷積核長寬都是相等的,而且很多情況下卷積核都比較小,可能每個tensor的三個size_t的維度值就能占用卷積核10%以上的空間。
但實際操作中我使用了裸的double*,而且它的梯度、一階矩估計和二階矩估計都使用了double*,因為這樣更加靈活,也方便主存直接讀取資料——否則還要先讀取tensor4d的頭指標、然後再順次讀取權值這樣兩次i/o。
連線矩陣和偏置向量也是用了同樣的方法儲存,不但節省了空間,而且增強了靈活性。
一開始我的想法是,從每個樣本輸入網路開始到反向傳播回到輸入層結束這一過程,對網路實現樣本並行,以達到接近100%的device利用率,可惜寫到了batchnorm一層就不知道如何實現了——因為bn層要求每乙個樣本都參與計算。暫且放過這一問題,寫到反向傳播又不會寫了,因為在計算梯度時,引數的梯度變數是互斥量——難道要設定乙個mutex來控制訪問?
但後來仔細想了想發現並不用這樣,樣本並行只需要在每一層的前向傳播和反向傳播兩個過程分別實現就行了;而batchnorm是特例,這一層不需要樣本並行。反向傳播過程中,計算誤差時進行樣本並行,而計算梯度時同樣不需要樣本之間並行——不過可以引數並行。這樣做的缺點是降低了device的利用率,但執行時間和編寫難度都有所降低。
需要注意的是,樣本並行依然需要核函式巢狀核函式,準備好用最原始的printf debug法吧!
由於神經網路很難除錯,僅僅依靠單純的輸入輸出是無法除錯的——我們很難理解這些浮點數是做什麼的。所以可以落實到具體專案中去除錯。
比如我們專業英語的大專案:
乙個簡單的二分類問題。我經歷了三輪除錯,期間的艱難更是堪比高考前的三輪複習。
設計階段
網路結構
第一輪
正如前文中所說的,第一輪除錯,很快就出現了cudaillegaladdress問題,經過stackoverflow發現,需要用cudadevicesetlimit()手動設定顯示卡的堆記憶體大小;後來就出現了nan,經過檢查後發現了少量的權值未初始化和主顯未同步。
第二輪
接著,誤差穩定在了ln(2)處、正確率保持在50%左右降不下來,我嘗試了sgd、adadelta、甚至是更換了mse損失函式,都沒有解決。後來我發現,全連線層的反向傳播函式的傳參出現了問題,有兩個引數順序寫反了。這也算是我的低階錯誤之一了。
但繼續訓練,正確率公升高到了56%後也不能繼續上公升了。我依然預感到了我的低階錯誤不止一處。
第三輪
我把batch size調成了訓練集大小,發現誤差收斂得很順利;可就是batch size小於訓練集大小時,正確率就一直有問題,而且batch size越小,正確率越低。
最終我發現了問題所在——在乙個epoch中,我只設定了一次期望輸出,也就是說剩下的batch都用了第乙個batch的期望輸出來計算誤差!不過這次debug真是見了鬼了,訓練集正確率56%,測試集正確率高達99.71%!
修改後,整個程式都完全正確,成功執行出結果:
前端顯示結果
單純使用api的誤差下降資料
經過30個epoch後,誤差收斂,正確率達到100%,於是我停止訓練。
而測試集中1024個樣本的測試結果更是令我虎軀一震——正確率100%!無錯誤分類!無把握較小的**!
可能是玩cv太久的原因,一時難以接受,我還拿這個結果去問助教,只不過他這兩天還都沒回我……
其實debug神經網路並非難事,但過程相當麻煩,需要程式設計師擁有一顆勇敢的心。
一時感慨寫了這篇文章,可能有點語無倫次,不過以後我會提取一些乾貨作為除錯技巧。
卷積神經網路 卷積層
1 2 該部落格主要是對網上知識點的學習和整理,方便日後複習。侵刪。卷積神經網路 cnn 一般由輸入層 卷積層 啟用函式 池化層 全連線層組成,即input 輸入層 conv 卷積層 relu 啟用函式 pool 池化層 fc 全連線層 當我們給定乙個 x 的圖案,計算機怎麼識別這個圖案就是 x 呢...
卷積神經網路膨脹卷積
卷積核就是影象處理時,給定輸入影象,輸入影象中乙個小區域中畫素加權平均後成為輸出影象中的每個對應畫素,其中權值由乙個函式定義,這個函式稱為卷積核 又稱濾波器。卷積核的大小一般有1x1,3x3和5x5的尺寸 一般是奇數x奇數 同樣提取某個特徵,經過不同卷積核卷積後效果也不一樣 可以發現同樣是銳化,5x...
卷積神經網路 卷積操作
對於cnn,卷積操作的主要目的是從輸入影象中提取特徵。卷積通過使用輸入資料的小方塊學習影象特徵來保留畫素之間的空間關係。卷積操作就是卷積核 過濾器 filter 在原始中進行滑動得到特徵圖 feature map 的過程。假設我們現在有乙個單通道的原始和乙個卷積核,卷積的過程如圖2所示 卷積得到的特...