阿里巴巴批發(fā)網(wǎng)站上面怎么做微商單頁(yè)網(wǎng)站制作
第二部分:從現(xiàn)實(shí)世界的圖像中學(xué)習(xí):肺癌的早期檢測(cè)
第 2 部分的結(jié)構(gòu)與第 1 部分不同;它幾乎是一本書(shū)中的一本書(shū)。我們將以幾章的篇幅深入探討一個(gè)單一用例,從第 1 部分學(xué)到的基本構(gòu)建模塊開(kāi)始,構(gòu)建一個(gè)比我們迄今為止看到的更完整的項(xiàng)目。我們的第一次嘗試將是不完整和不準(zhǔn)確的,我們將探討如何診斷這些問(wèn)題,然后修復(fù)它們。我們還將確定我們解決方案的各種其他改進(jìn)措施,實(shí)施它們,并衡量它們的影響。為了訓(xùn)練第 2 部分中將開(kāi)發(fā)的模型,您將需要訪問(wèn)至少 8 GB RAM 的 GPU,以及數(shù)百 GB 的可用磁盤(pán)空間來(lái)存儲(chǔ)訓(xùn)練數(shù)據(jù)。
第九章介紹了我們將要消耗的項(xiàng)目、環(huán)境和數(shù)據(jù),以及我們將要實(shí)施的項(xiàng)目結(jié)構(gòu)。第十章展示了我們?nèi)绾螌?shù)據(jù)轉(zhuǎn)換為 PyTorch 數(shù)據(jù)集,第十一章和第十二章介紹了我們的分類模型:我們需要衡量數(shù)據(jù)集訓(xùn)練效果的指標(biāo),并實(shí)施解決阻止模型良好訓(xùn)練的問(wèn)題的解決方案。在第十三章,我們將轉(zhuǎn)向端到端項(xiàng)目的開(kāi)始,通過(guò)創(chuàng)建一個(gè)生成熱圖而不是單一分類的分割模型。該熱圖將用于生成位置進(jìn)行分類。最后,在第十四章,我們將結(jié)合我們的分割和分類模型進(jìn)行最終診斷。
九、使用 PyTorch 來(lái)對(duì)抗癌癥
本章涵蓋內(nèi)容
-
將一個(gè)大問(wèn)題分解為更小、更容易的問(wèn)題
-
探索復(fù)雜深度學(xué)習(xí)問(wèn)題的約束,并決定結(jié)構(gòu)和方法
-
下載訓(xùn)練數(shù)據(jù)
本章有兩個(gè)主要目標(biāo)。我們將首先介紹本書(shū)第二部分的整體計(jì)劃,以便我們對(duì)接下來(lái)的各個(gè)章節(jié)將要構(gòu)建的更大范圍有一個(gè)堅(jiān)實(shí)的概念。在第十章中,我們將開(kāi)始構(gòu)建數(shù)據(jù)解析和數(shù)據(jù)操作例程,這些例程將在第十一章中訓(xùn)練我們的第一個(gè)模型時(shí)產(chǎn)生要消耗的數(shù)據(jù)。為了很好地完成即將到來(lái)的章節(jié)所需的工作,我們還將利用本章來(lái)介紹我們的項(xiàng)目將運(yùn)行的一些背景:我們將討論數(shù)據(jù)格式、數(shù)據(jù)來(lái)源,并探索我們問(wèn)題領(lǐng)域?qū)ξ覀兪┘拥募s束。習(xí)慣執(zhí)行這些任務(wù),因?yàn)槟銓⒉坏貌粸槿魏螄?yán)肅的深度學(xué)習(xí)項(xiàng)目做這些任務(wù)!
9.1 用例簡(jiǎn)介
本書(shū)這一部分的目標(biāo)是為您提供處理事情不順利的工具,這種情況比第 1 部分可能讓你相信的更常見(jiàn)。我們無(wú)法預(yù)測(cè)每種失敗情況或涵蓋每種調(diào)試技術(shù),但希望我們能給你足夠的東西,讓你在遇到新的障礙時(shí)不感到困惑。同樣,我們希望幫助您避免您自己的項(xiàng)目出現(xiàn)情況,即當(dāng)您的項(xiàng)目表現(xiàn)不佳時(shí),您不知道接下來(lái)該做什么。相反,我們希望您的想法列表會(huì)很長(zhǎng),挑戰(zhàn)將是如何優(yōu)先考慮!
為了呈現(xiàn)這些想法和技術(shù),我們需要一個(gè)具有一些細(xì)微差別和相當(dāng)重要性的背景。我們選擇使用僅通過(guò)患者胸部的 CT 掃描作為輸入來(lái)自動(dòng)檢測(cè)肺部惡性腫瘤。我們將專注于技術(shù)挑戰(zhàn)而不是人類影響,但不要誤解–即使從工程角度來(lái)看,第 2 部分也需要比第 1 部分更嚴(yán)肅、更有結(jié)構(gòu)的方法才能使項(xiàng)目成功。
注意 CT 掃描本質(zhì)上是 3D X 射線,表示為單通道數(shù)據(jù)的 3D 數(shù)組。我們很快會(huì)更詳細(xì)地討論它們。
正如你可能已經(jīng)猜到的,本章的標(biāo)題更多是引人注目的、暗示夸張,而不是嚴(yán)肅的聲明意圖。讓我們準(zhǔn)確一點(diǎn):本書(shū)的這一部分的項(xiàng)目將以人體軀干的三維 CT 掃描作為輸入,并輸出懷疑的惡性腫瘤的位置,如果有的話。
早期檢測(cè)肺癌對(duì)生存率有巨大影響,但手動(dòng)進(jìn)行這項(xiàng)工作很困難,特別是在任何全面、整體人口意義上。目前,審查數(shù)據(jù)的工作必須由經(jīng)過(guò)高度訓(xùn)練的專家執(zhí)行,需要極其細(xì)致的注意,而且主要是由不存在癌癥的情況主導(dǎo)。
做好這項(xiàng)工作就像被放在 100 堆草垛前,并被告知:“確定這些中哪些,如果有的話,包含一根針?!边@樣搜索會(huì)導(dǎo)致潛在的警告信號(hào)被忽略,特別是在早期階段,提示更加微妙。人類大腦并不適合做那種單調(diào)的工作。當(dāng)然,這就是深度學(xué)習(xí)發(fā)揮作用的地方。
自動(dòng)化這個(gè)過(guò)程將使我們?cè)谝粋€(gè)不合作的環(huán)境中獲得經(jīng)驗(yàn),在那里我們必須從頭開(kāi)始做更多的工作,而且可能會(huì)遇到更少的問(wèn)題容易解決。不過(guò),我們一起努力,最終會(huì)成功的!一旦你讀完第二部分,我們相信你就準(zhǔn)備好開(kāi)始解決你自己選擇的一個(gè)真實(shí)且未解決的問(wèn)題了。
我們選擇了肺部腫瘤檢測(cè)這個(gè)問(wèn)題,有幾個(gè)原因。主要原因是這個(gè)問(wèn)題本身尚未解決!這很重要,因?yàn)槲覀兿胍鞔_表明你可以使用 PyTorch 有效地解決尖端項(xiàng)目。我們希望這能增加你對(duì) PyTorch 作為框架以及作為開(kāi)發(fā)者的信心。這個(gè)問(wèn)題空間的另一個(gè)好處是,雖然它尚未解決,但最近許多團(tuán)隊(duì)一直在關(guān)注它,并且已經(jīng)看到了有希望的結(jié)果。這意味著這個(gè)挑戰(zhàn)可能正好處于我們集體解決能力的邊緣;我們不會(huì)浪費(fèi)時(shí)間在一個(gè)實(shí)際上離合理解決方案還有幾十年的問(wèn)題上。對(duì)這個(gè)問(wèn)題的關(guān)注也導(dǎo)致了許多高質(zhì)量的論文和開(kāi)源項(xiàng)目,這些是靈感和想法的重要來(lái)源。如果你有興趣繼續(xù)改進(jìn)我們創(chuàng)建的解決方案,這將在我們完成書(shū)的第二部分后非常有幫助。我們將在第十四章提供一些額外信息的鏈接。
本書(shū)的這一部分將繼續(xù)專注于檢測(cè)肺部腫瘤的問(wèn)題,但我們將教授的技能是通用的。學(xué)習(xí)如何調(diào)查、預(yù)處理和呈現(xiàn)數(shù)據(jù)以進(jìn)行訓(xùn)練對(duì)于你正在進(jìn)行的任何項(xiàng)目都很重要。雖然我們將在肺部腫瘤的具體背景下涵蓋預(yù)處理,但總體思路是這是你應(yīng)該為你的項(xiàng)目做好準(zhǔn)備的。同樣,建立訓(xùn)練循環(huán)、獲得正確的性能指標(biāo)以及將項(xiàng)目的模型整合到最終應(yīng)用程序中都是我們將在第 9 至 14 章中使用的通用技能。
注意 盡管第 2 部分的最終結(jié)果將有效,但輸出不夠準(zhǔn)確以用于臨床。我們專注于將其作為教授 PyTorch的激勵(lì)示例,而不是利用每一個(gè)技巧來(lái)解決問(wèn)題。
9.2 準(zhǔn)備一個(gè)大型項(xiàng)目
這個(gè)項(xiàng)目將建立在第 1 部分學(xué)到的基礎(chǔ)技能之上。特別是,從第八章開(kāi)始的模型構(gòu)建內(nèi)容將直接相關(guān)。重復(fù)的卷積層后跟著一個(gè)分辨率降低的下采樣層仍將構(gòu)成我們模型的大部分。然而,我們將使用 3D 數(shù)據(jù)作為我們模型的輸入。這在概念上類似于第 1 部分最后幾章中使用的 2D 圖像數(shù)據(jù),但我們將無(wú)法依賴 PyTorch 生態(tài)系統(tǒng)中所有 2D 特定工具。
我們?cè)诘诎苏率褂镁矸e模型的工作與第 2 部分中將要做的工作之間的主要區(qū)別與我們投入到模型之外的事情有關(guān)。在第八章,我們使用一個(gè)提供的現(xiàn)成數(shù)據(jù)集,并且在將數(shù)據(jù)饋送到模型進(jìn)行分類之前幾乎沒(méi)有進(jìn)行數(shù)據(jù)操作。我們幾乎所有的時(shí)間和注意力都花在構(gòu)建模型本身上,而現(xiàn)在我們甚至不會(huì)在第十一章開(kāi)始設(shè)計(jì)我們的兩個(gè)模型架構(gòu)之一。這是由于有非標(biāo)準(zhǔn)數(shù)據(jù),沒(méi)有預(yù)先構(gòu)建的庫(kù)可以隨時(shí)提供適合插入模型的訓(xùn)練樣本。我們將不得不了解我們的數(shù)據(jù)并自己實(shí)現(xiàn)相當(dāng)多的內(nèi)容。
即使完成了這些工作,這也不會(huì)成為將 CT 轉(zhuǎn)換為張量,將其饋送到神經(jīng)網(wǎng)絡(luò)中,并在另一側(cè)得到答案的情況。對(duì)于這樣的真實(shí)用例,一個(gè)可行的方法將更加復(fù)雜,以考慮到限制數(shù)據(jù)可用性、有限的計(jì)算資源以及我們?cè)O(shè)計(jì)有效模型的能力的限制因素。請(qǐng)記住這一點(diǎn),因?yàn)槲覀儗⒅鸩浇忉屛覀冺?xiàng)目架構(gòu)的高級(jí)概述。
談到有限的計(jì)算資源,第 2 部分將需要訪問(wèn) GPU 才能實(shí)現(xiàn)合理的訓(xùn)練速度,最好是至少具有 8 GB 的 RAM。嘗試在 CPU 上訓(xùn)練我們將構(gòu)建的模型可能需要幾周時(shí)間!1 如果你手頭沒(méi)有 GPU,我們?cè)诘谑恼绿峁┝祟A(yù)訓(xùn)練模型;那里的結(jié)節(jié)分析腳本可能可以在一夜之間運(yùn)行。雖然我們不想將本書(shū)與專有服務(wù)綁定,但值得注意的是,目前,Colaboratory(colab.research.google.com
)提供免費(fèi)的 GPU 實(shí)例,可能會(huì)有用。PyTorch 甚至已經(jīng)預(yù)安裝!你還需要至少 220 GB 的可用磁盤(pán)空間來(lái)存儲(chǔ)原始訓(xùn)練數(shù)據(jù)、緩存數(shù)據(jù)和訓(xùn)練模型。
注意 第 2 部分中呈現(xiàn)的許多代碼示例省略了復(fù)雜的細(xì)節(jié)。與其用日志記錄、錯(cuò)誤處理和邊緣情況來(lái)混淆示例,本書(shū)的文本只包含表達(dá)討論中核心思想的代碼。完整的可運(yùn)行代碼示例可以在本書(shū)的網(wǎng)站(www.manning.com/books/deep-learning-with-pytorch)和 GitHub(github.com/deep-learning-with-pytorch/dlwpt-code
)上找到。
好的,我們已經(jīng)確定了這是一個(gè)困難、多方面的問(wèn)題,但我們要怎么解決呢?我們不是要查看整個(gè) CT 掃描以尋找腫瘤或其潛在惡性,而是要解決一系列更簡(jiǎn)單的問(wèn)題,這些問(wèn)題將組合在一起提供我們感興趣的端到端結(jié)果。就像工廠的裝配線一樣,每個(gè)步驟都會(huì)接收原材料(數(shù)據(jù))和/或前一步驟的輸出,進(jìn)行一些處理,并將結(jié)果交給下一個(gè)站點(diǎn)。并不是每個(gè)問(wèn)題都需要這樣解決,但將問(wèn)題分解成獨(dú)立解決的部分通常是一個(gè)很好的開(kāi)始。即使最終發(fā)現(xiàn)這種方法對(duì)于特定項(xiàng)目來(lái)說(shuō)是錯(cuò)誤的,但在處理各個(gè)部分時(shí),我們可能已經(jīng)學(xué)到足夠多的知識(shí),以便知道如何重新構(gòu)建我們的方法以取得成功。
在我們深入了解如何分解問(wèn)題的細(xì)節(jié)之前,我們需要了解一些關(guān)于醫(yī)學(xué)領(lǐng)域的細(xì)節(jié)。雖然代碼清單會(huì)告訴你我們?cè)谧鍪裁?#xff0c;但了解放射腫瘤學(xué)將解釋為什么我們這樣做。無(wú)論是哪個(gè)領(lǐng)域,了解問(wèn)題空間都是至關(guān)重要的。深度學(xué)習(xí)很強(qiáng)大,但它不是魔法,盲目地將其應(yīng)用于非平凡問(wèn)題可能會(huì)失敗。相反,我們必須將對(duì)空間的洞察力與對(duì)神經(jīng)網(wǎng)絡(luò)行為的直覺(jué)相結(jié)合。從那里,有紀(jì)律的實(shí)驗(yàn)和改進(jìn)應(yīng)該為我們提供足夠的信息,以便找到可行的解決方案。
9.3 什么是 CT 掃描,確切地說(shuō)?
在我們深入項(xiàng)目之前,我們需要花點(diǎn)時(shí)間解釋一下什么是 CT 掃描。我們將廣泛使用 CT 掃描數(shù)據(jù)作為我們項(xiàng)目的主要數(shù)據(jù)格式,因此對(duì)數(shù)據(jù)格式的優(yōu)勢(shì)、劣勢(shì)和基本特性有一個(gè)工作理解將對(duì)其有效利用至關(guān)重要。我們之前指出的關(guān)鍵點(diǎn)是:CT 掃描本質(zhì)上是 3D X 射線,表示為單通道數(shù)據(jù)的 3D 數(shù)組。正如我們可能從第四章中記得的那樣,這就像一組堆疊的灰度 PNG 圖像。
體素
一個(gè)體素是熟悉的二維像素的三維等價(jià)物。它包圍著空間的一個(gè)體積(因此,“體積像素”),而不是一個(gè)區(qū)域,并且通常排列在一個(gè)三維網(wǎng)格中以表示數(shù)據(jù)場(chǎng)。每個(gè)維度都將與之關(guān)聯(lián)一個(gè)可測(cè)量的距離。通常,體素是立方體的,但在本章中,我們將處理的是長(zhǎng)方體體素。
除了醫(yī)學(xué)數(shù)據(jù),我們還可以在流體模擬、從 2D 圖像重建的 3D 場(chǎng)景、用于自動(dòng)駕駛汽車的光探測(cè)與測(cè)距(LIDAR)數(shù)據(jù)等問(wèn)題領(lǐng)域看到類似的體素?cái)?shù)據(jù)。這些領(lǐng)域都有各自的特點(diǎn)和微妙之處,雖然我們將在這里介紹的 API 通常適用,但如果我們想要有效地使用這些 API,我們也必須了解我們使用的數(shù)據(jù)的性質(zhì)。
每個(gè) CT 掃描的體素都有一個(gè)數(shù)值,大致對(duì)應(yīng)于內(nèi)部物質(zhì)的平均質(zhì)量密度。大多數(shù)數(shù)據(jù)的可視化顯示高密度材料如骨骼和金屬植入物為白色,低密度的空氣和肺組織為黑色,脂肪和組織為各種灰色。再次,這看起來(lái)與 X 射線有些相似,但也有一些關(guān)鍵區(qū)別。
CT 掃描和 X 射線之間的主要區(qū)別在于,X 射線是將 3D 強(qiáng)度(在本例中為組織和骨密度)投影到 2D 平面上,而 CT 掃描保留了數(shù)據(jù)的第三維。這使我們能夠以各種方式呈現(xiàn)數(shù)據(jù):例如,作為一個(gè)灰度實(shí)體,我們可以在圖 9.1 中看到。
圖 9.1 人體軀干的 CT 掃描,從上到下依次顯示皮膚、器官、脊柱和患者支撐床。來(lái)源:mng.bz/04r6
; Mindways CT Software / CC BY-SA 3.0 (creativecommons.org/licenses/by-sa/3.0/deed.en
)。
注意 CT 掃描實(shí)際上測(cè)量的是輻射密度,這是受檢材料的質(zhì)量密度和原子序數(shù)的函數(shù)。在這里,區(qū)分并不相關(guān),因?yàn)闊o(wú)論輸入的確切單位是什么,模型都會(huì)處理和學(xué)習(xí) CT 數(shù)據(jù)。
這種 3D 表示還允許我們通過(guò)隱藏我們不感興趣的組織類型來(lái)“看到”主體內(nèi)部。例如,我們可以將數(shù)據(jù)呈現(xiàn)為 3D,并將可見(jiàn)性限制在骨骼和肺組織,如圖 9.2 所示。
圖 9.2 顯示了肋骨、脊柱和肺結(jié)構(gòu)的 CT 掃描
與 X 射線相比,CT 掃描要難得多,因?yàn)檫@需要像圖 9.3 中所示的那種機(jī)器,通常新機(jī)器的成本高達(dá)一百萬(wàn)美元,并且需要受過(guò)培訓(xùn)的工作人員來(lái)操作。大多數(shù)醫(yī)院和一些設(shè)備齊全的診所都有 CT 掃描儀,但它們遠(yuǎn)不及 X 射線機(jī)器普及。這與患者隱私規(guī)定結(jié)合在一起,可能會(huì)使得獲取 CT 掃描有些困難,除非已經(jīng)有人做好了收集和整理這些數(shù)據(jù)的工作。
圖 9.3 還顯示了 CT 掃描中包含區(qū)域的示例邊界框。患者躺在的床來(lái)回移動(dòng),使掃描儀能夠成像患者的多個(gè)切片,從而填充邊界框。掃描儀較暗的中心環(huán)是實(shí)際成像設(shè)備的位置。
圖 9.3 一個(gè)患者在 CT 掃描儀內(nèi),CT 掃描的邊界框疊加顯示。除了庫(kù)存照片外,患者在機(jī)器內(nèi)通常不穿著便裝。
CT 掃描與 X 射線之間的最后一個(gè)區(qū)別是數(shù)據(jù)僅以數(shù)字格式存在。CT代表計(jì)算機(jī)斷層掃描(en.wikipedia.org/wiki/CT_scan#Process
)。掃描過(guò)程的原始輸出對(duì)人眼來(lái)說(shuō)并不特別有意義,必須由計(jì)算機(jī)正確重新解釋為我們可以理解的內(nèi)容。掃描時(shí) CT 掃描儀的設(shè)置會(huì)對(duì)結(jié)果數(shù)據(jù)產(chǎn)生很大影響。
盡管這些信息可能看起來(lái)并不特別相關(guān),但實(shí)際上我們學(xué)到了一些東西:從圖 9.3 中,我們可以看到 CT 掃描儀測(cè)量頭到腳軸向距離的方式與其他兩個(gè)軸不同。患者實(shí)際上沿著這個(gè)軸移動(dòng)!這解釋了(或至少是一個(gè)強(qiáng)烈的暗示)為什么我們的體素可能不是立方體,并且也與我們?cè)诘谑轮腥绾翁幚頂?shù)據(jù)有關(guān)。這是一個(gè)很好的例子,說(shuō)明我們需要了解我們的問(wèn)題領(lǐng)域,如果要有效地選擇如何解決問(wèn)題。在開(kāi)始處理自己的項(xiàng)目時(shí),確保您對(duì)數(shù)據(jù)的細(xì)節(jié)進(jìn)行相同的調(diào)查。
9.4 項(xiàng)目:肺癌端到端檢測(cè)器
現(xiàn)在我們已經(jīng)掌握了 CT 掃描的基礎(chǔ)知識(shí),讓我們討論一下我們項(xiàng)目的結(jié)構(gòu)。大部分磁盤(pán)上的字節(jié)將用于存儲(chǔ)包含密度信息的 CT 掃描的 3D 數(shù)組,我們的模型將主要消耗這些 3D 數(shù)組的各種子切片。我們將使用五個(gè)主要步驟,從檢查整個(gè)胸部 CT 掃描到給患者做出肺癌診斷。
我們?cè)趫D 9.4 中展示的完整端到端解決方案將加載 CT 數(shù)據(jù)文件以生成包含完整 3D 掃描的Ct
實(shí)例,將其與執(zhí)行分割(標(biāo)記感興趣的體素)的模塊結(jié)合,然后將有趣的體素分組成小塊,以尋找候選結(jié)節(jié)。
結(jié)節(jié)
肺部由增殖細(xì)胞組成的組織塊稱為腫瘤。腫瘤可以是良性的,也可以是惡性的,此時(shí)也被稱為癌癥。肺部的小腫瘤(僅幾毫米寬)稱為結(jié)節(jié)。大約 40%的肺結(jié)節(jié)最終被證實(shí)是惡性的–小癌癥。盡早發(fā)現(xiàn)這些對(duì)于醫(yī)學(xué)影像非常重要,這取決于我們正在研究的這種類型的醫(yī)學(xué)影像。
圖 9.4 完整胸部 CT 掃描并確定患者是否患有惡性腫瘤的端到端過(guò)程
結(jié)節(jié)位置與 CT 體素?cái)?shù)據(jù)結(jié)合,產(chǎn)生結(jié)節(jié)候選,然后可以由我們的結(jié)節(jié)分類模型檢查它們是否實(shí)際上是結(jié)節(jié),最終是否是惡性的。后一項(xiàng)任務(wù)特別困難,因?yàn)閻盒钥赡軆H從 CT 成像中無(wú)法明顯看出,但我們將看看我們能走多遠(yuǎn)。最后,每個(gè)單獨(dú)的結(jié)節(jié)分類可以組合成整體患者診斷。
更詳細(xì)地說(shuō),我們將執(zhí)行以下操作:
-
將我們的原始 CT 掃描數(shù)據(jù)加載到一個(gè)可以與 PyTorch 一起使用的形式中。將原始數(shù)據(jù)放入 PyTorch 可用的形式將是您面臨的任何項(xiàng)目的第一步。對(duì)于 2D 圖像數(shù)據(jù),這個(gè)過(guò)程稍微復(fù)雜一些,對(duì)于非圖像數(shù)據(jù)則更簡(jiǎn)單。
-
使用 PyTorch 識(shí)別肺部潛在腫瘤的體素,實(shí)現(xiàn)一種稱為分割的技術(shù)。這大致相當(dāng)于生成應(yīng)該輸入到我們第 3 步分類器中的區(qū)域的熱圖。這將使我們能夠?qū)W⒂诜尾績(jī)?nèi)部的潛在腫瘤,并忽略大片無(wú)趣的解剖結(jié)構(gòu)(例如,一個(gè)人不能在胃部患肺癌)。
通常,在學(xué)習(xí)時(shí)能夠?qū)W⒂趩我恍∪蝿?wù)是最好的。隨著經(jīng)驗(yàn)的積累,有些情況下更復(fù)雜的模型結(jié)構(gòu)可以產(chǎn)生最優(yōu)結(jié)果(例如,我們?cè)诘诙驴吹降?GAN 游戲),但是從頭開(kāi)始設(shè)計(jì)這些模型需要對(duì)基本構(gòu)建模塊有廣泛的掌握。先學(xué)會(huì)走路,再跑步,等等。
-
將有趣的體素分組成塊:也就是候選結(jié)節(jié)(有關(guān)結(jié)節(jié)的更多信息,請(qǐng)參見(jiàn)圖 9.5)。在這里,我們將找到熱圖上每個(gè)熱點(diǎn)的粗略中心。
每個(gè)結(jié)節(jié)可以通過(guò)其中心點(diǎn)的索引、行和列來(lái)定位。我們這樣做是為了向最終分類器提供一個(gè)簡(jiǎn)單、受限的問(wèn)題。將體素分組不會(huì)直接涉及 PyTorch,這就是為什么我們將其拆分為一個(gè)單獨(dú)的步驟。通常,在處理多步解決方案時(shí),會(huì)在項(xiàng)目的較大、由深度學(xué)習(xí)驅(qū)動(dòng)的部分之間添加非深度學(xué)習(xí)的連接步驟。
-
使用 3D 卷積將候選結(jié)節(jié)分類為實(shí)際結(jié)節(jié)或非結(jié)節(jié)。
這將類似于我們?cè)诘诎苏轮薪榻B的 2D 卷積。確定候選結(jié)構(gòu)中腫瘤性質(zhì)的特征是與問(wèn)題中的腫瘤局部相關(guān)的,因此這種方法應(yīng)該在限制輸入數(shù)據(jù)大小和排除相關(guān)信息之間提供良好的平衡。做出這種限制范圍的決定可以使每個(gè)單獨(dú)的任務(wù)受限,這有助于在故障排除時(shí)限制要檢查的事物數(shù)量。
-
使用組合的每個(gè)結(jié)節(jié)分類來(lái)診斷患者。
與上一步中的結(jié)節(jié)分類器類似,我們將嘗試僅基于成像數(shù)據(jù)確定結(jié)節(jié)是良性還是惡性。我們將簡(jiǎn)單地取每個(gè)腫瘤惡性預(yù)測(cè)的最大值,因?yàn)橹恍枰粋€(gè)腫瘤是惡性,患者就會(huì)患癌癥。其他項(xiàng)目可能希望使用不同的方式將每個(gè)實(shí)例的預(yù)測(cè)聚合成一個(gè)文件分?jǐn)?shù)。在這里,我們問(wèn)的是,“有什么可疑的嗎?”所以最大值是一個(gè)很好的聚合方式。如果我們正在尋找定量信息,比如“A 型組織與 B 型組織的比例”,我們可能會(huì)選擇適當(dāng)?shù)钠骄怠?/p>
肩上的巨人
當(dāng)我們決定采用這種五步方法時(shí),我們站在巨人的肩膀上。我們將在第十四章更詳細(xì)地討論這些巨人及其工作。我們事先并沒(méi)有特別的理由認(rèn)為這種項(xiàng)目結(jié)構(gòu)對(duì)這個(gè)問(wèn)題會(huì)很有效;相反,我們依賴于那些實(shí)際實(shí)施過(guò)類似事物并報(bào)告成功的人。在轉(zhuǎn)向不同領(lǐng)域時(shí),預(yù)計(jì)需要進(jìn)行實(shí)驗(yàn)以找到可行的方法,但始終嘗試從該領(lǐng)域的早期努力和那些在類似領(lǐng)域工作并發(fā)現(xiàn)可能轉(zhuǎn)移的事物的人那里學(xué)習(xí)。走出去,尋找他人所做的事情,并將其作為一個(gè)基準(zhǔn)。同時(shí),避免盲目獲取代碼并運(yùn)行,因?yàn)槟枰耆斫饽谶\(yùn)行的代碼,以便利用結(jié)果為自己取得進(jìn)展。
圖 9.4 僅描述了在構(gòu)建和訓(xùn)練所有必要模型后通過(guò)系統(tǒng)的最終路徑。訓(xùn)練相關(guān)模型所需的實(shí)際工作將在我們接近實(shí)施每個(gè)步驟時(shí)詳細(xì)說(shuō)明。
我們將用于訓(xùn)練的數(shù)據(jù)為步驟 3 和 4 提供了人工注釋的輸出。這使我們可以將步驟 2 和 3(識(shí)別體素并將其分組為結(jié)節(jié)候選)幾乎視為與步驟 4(結(jié)節(jié)候選分類)分開(kāi)的項(xiàng)目。人類專家已經(jīng)用結(jié)節(jié)位置注釋了數(shù)據(jù),因此我們可以按照自己喜歡的順序處理步驟 2 和 3 或步驟 4。
我們將首先處理步驟 1(數(shù)據(jù)加載),然后跳到步驟 4,然后再回來(lái)實(shí)現(xiàn)步驟 2 和 3,因?yàn)椴襟E 4(分類)需要一種類似于我們?cè)诘诎苏轮惺褂玫姆椒?#xff0c;即使用多個(gè)卷積和池化層來(lái)聚合空間信息,然后將其饋送到線性分類器中。一旦我們掌握了分類模型,我們就可以開(kāi)始處理步驟 2(分割)。由于分割是更復(fù)雜的主題,我們希望在不必同時(shí)學(xué)習(xí)分割和 CT 掃描以及惡性腫瘤的基礎(chǔ)知識(shí)的情況下解決它。相反,我們將在處理一個(gè)更熟悉的分類問(wèn)題的同時(shí)探索癌癥檢測(cè)領(lǐng)域。
從問(wèn)題中間開(kāi)始并逐步解決問(wèn)題的方法可能看起來(lái)很奇怪。從第 1 步開(kāi)始逐步向前推進(jìn)會(huì)更直觀。然而,能夠?qū)?wèn)題分解并獨(dú)立解決各個(gè)步驟是有用的,因?yàn)檫@樣可以鼓勵(lì)更模塊化的解決方案;此外,將工作負(fù)載在小團(tuán)隊(duì)成員之間劃分會(huì)更容易。此外,實(shí)際的臨床用戶可能更喜歡一個(gè)系統(tǒng),可以標(biāo)記可疑的結(jié)節(jié)供審查,而不是提供單一的二進(jìn)制診斷。將我們的模塊化解決方案適應(yīng)不同的用例可能會(huì)比如果我們采用了單一的、自上而下的系統(tǒng)更容易。
當(dāng)我們逐步實(shí)施每一步時(shí),我們將詳細(xì)介紹肺部腫瘤,以及展示大量關(guān)于 CT 掃描的細(xì)節(jié)。雖然這可能看起來(lái)與專注于 PyTorch 的書(shū)籍無(wú)關(guān),但我們這樣做是為了讓你開(kāi)始對(duì)問(wèn)題空間產(chǎn)生直覺(jué)。這是至關(guān)重要的,因?yàn)樗锌赡艿慕鉀Q方案和方法的空間太大,無(wú)法有效地編碼、訓(xùn)練和評(píng)估。
如果我們?cè)谧鲆粋€(gè)不同的項(xiàng)目(比如你在完成這本書(shū)后要處理的項(xiàng)目),我們?nèi)匀恍枰M(jìn)行調(diào)查來(lái)了解數(shù)據(jù)和問(wèn)題空間。也許你對(duì)衛(wèi)星地圖制作感興趣,你的下一個(gè)項(xiàng)目需要處理從軌道拍攝的地球圖片。你需要詢問(wèn)關(guān)于收集的波長(zhǎng)的問(wèn)題–你只得到正常的 RGB 嗎,還是更奇特的東西?紅外線或紫外線呢?此外,根據(jù)白天時(shí)間或者成像位置不直接在衛(wèi)星正上方,可能會(huì)使圖像傾斜。圖像是否需要校正?
即使你假設(shè)的第三個(gè)項(xiàng)目的數(shù)據(jù)類型保持不變,你將要處理的領(lǐng)域可能會(huì)改變事情,可能會(huì)發(fā)生顯著變化。處理自動(dòng)駕駛汽車的相機(jī)輸出仍然涉及 2D 圖像,但復(fù)雜性和注意事項(xiàng)卻大不相同。例如,映射衛(wèi)星不太可能需要擔(dān)心太陽(yáng)照射到相機(jī)中,或者鏡頭上沾上泥巴!
我們必須能夠運(yùn)用直覺(jué)來(lái)引導(dǎo)我們對(duì)潛在優(yōu)化和改進(jìn)的調(diào)查。這對(duì)于深度學(xué)習(xí)項(xiàng)目來(lái)說(shuō)是真實(shí)的,我們將在第 2 部分中練習(xí)使用我們的直覺(jué)。所以,讓我們這樣做??焖偻撕笠徊?#xff0c;做一個(gè)直覺(jué)檢查。你的直覺(jué)對(duì)這種方法有什么看法?對(duì)你來(lái)說(shuō)是否過(guò)于復(fù)雜?
9.4.1 為什么我們不能簡(jiǎn)單地將數(shù)據(jù)輸入神經(jīng)網(wǎng)絡(luò)直到它工作?
在閱讀最后一節(jié)之后,如果你認(rèn)為,“這和第八章完全不同!”我們并不會(huì)責(zé)怪你。你可能會(huì)想知道為什么我們有兩種不同的模型架構(gòu),或者為什么整體數(shù)據(jù)流如此復(fù)雜。嗯,我們之所以采取這種方法與第八章不同是有原因的。自動(dòng)化這個(gè)任務(wù)很困難,人們還沒(méi)有完全弄清楚。這種困難轉(zhuǎn)化為復(fù)雜性;一旦我們作為一個(gè)社會(huì)徹底解決了這個(gè)問(wèn)題,可能會(huì)有一個(gè)現(xiàn)成的庫(kù)包,我們可以直接使用,但我們還沒(méi)有達(dá)到那一步。
為什么會(huì)這么困難呢?
首先,大部分 CT 掃描基本上與回答“這個(gè)患者是否患有惡性腫瘤?”這個(gè)問(wèn)題無(wú)關(guān)。這是很直觀的,因?yàn)榛颊呱眢w的絕大部分組織都是健康的細(xì)胞。在有惡性腫瘤的情況下,CT 中高達(dá) 99.9999%的體素仍然不是癌癥。這個(gè)比例相當(dāng)于高清電視上某處兩個(gè)像素的顏色錯(cuò)誤或者一本小說(shuō)書(shū)架上一個(gè)拼錯(cuò)的單詞。
你能夠在圖 9.5 的三個(gè)視圖中識(shí)別被標(biāo)記為結(jié)節(jié)的白點(diǎn)嗎?2
如果你需要提示,索引、行和列值可以幫助找到相關(guān)的密集組織塊。你認(rèn)為只有這些圖像(這意味著只有圖像–沒(méi)有索引、行和列信息!)你能找出腫瘤的相關(guān)特性嗎?如果你被給予整個(gè) 3D 掃描,而不僅僅是與掃描的有趣部分相交的三個(gè)切片呢?
注意 如果你找不到腫瘤,不要擔(dān)心!我們?cè)噲D說(shuō)明這些數(shù)據(jù)有多微妙–難以在視覺(jué)上識(shí)別是這個(gè)例子的全部意義。
圖 9.5 一張 CT 掃描,大約有 1,000 個(gè)對(duì)于未經(jīng)訓(xùn)練的眼睛看起來(lái)像腫瘤的結(jié)構(gòu)。當(dāng)由人類專家審查時(shí),只有一個(gè)被確定為結(jié)節(jié)。其余的是正常的解剖結(jié)構(gòu),如血管、病變和其他無(wú)問(wèn)題的腫塊。
你可能在其他地方看到端到端方法在對(duì)象檢測(cè)和分類中非常成功。TorchVision 包括像 Fast R-CNN/Mask R-CNN 這樣的端到端模型,但這些模型通常在數(shù)十萬(wàn)張圖像上進(jìn)行訓(xùn)練,而這些數(shù)據(jù)集并不受稀有類別樣本數(shù)量的限制。我們將使用的項(xiàng)目架構(gòu)有利于在更適量的數(shù)據(jù)上表現(xiàn)良好。因此,雖然從理論上講,可以向神經(jīng)網(wǎng)絡(luò)投入任意大量的數(shù)據(jù),直到它學(xué)會(huì)尋找傳說(shuō)中的丟失的針,以及如何忽略干草,但實(shí)際上收集足夠的數(shù)據(jù)并等待足夠長(zhǎng)的時(shí)間來(lái)正確訓(xùn)練網(wǎng)絡(luò)是不現(xiàn)實(shí)的。這不會(huì)是最佳方法,因?yàn)榻Y(jié)果很差,大多數(shù)讀者根本無(wú)法獲得計(jì)算資源來(lái)實(shí)現(xiàn)它。
要想得出最佳解決方案,我們可以研究已被證明能夠更好地端到端集成數(shù)據(jù)的模型設(shè)計(jì)。這些復(fù)雜的設(shè)計(jì)能夠產(chǎn)生高質(zhì)量的結(jié)果,但它們并不是最佳,因?yàn)橐斫馑鼈儽澈蟮脑O(shè)計(jì)決策需要先掌握基本概念。這使得這些先進(jìn)模型在教授這些基本概念時(shí)不是很好的選擇!
這并不是說(shuō)我們的多步設(shè)計(jì)是最佳方法,因?yàn)椤白罴选敝皇窍鄬?duì)于我們選擇用來(lái)評(píng)估方法的標(biāo)準(zhǔn)而言。有許多“最佳”方法,就像我們?cè)陧?xiàng)目中工作時(shí)可能有許多目標(biāo)一樣。我們的自包含、多步方法也有一些缺點(diǎn)。
回想一下第二章的 GAN 游戲。在那里,我們有兩個(gè)網(wǎng)絡(luò)合作,制作出老大師藝術(shù)家的逼真贗品。藝術(shù)家會(huì)制作一個(gè)候選作品,學(xué)者會(huì)對(duì)其進(jìn)行評(píng)論,給予藝術(shù)家如何改進(jìn)的反饋。用技術(shù)術(shù)語(yǔ)來(lái)說(shuō),模型的結(jié)構(gòu)允許梯度從最終分類器(假或真)傳播到項(xiàng)目的最早部分(藝術(shù)家)。
我們解決問(wèn)題的方法不會(huì)使用端到端梯度反向傳播直接優(yōu)化我們的最終目標(biāo)。相反,我們將分別優(yōu)化問(wèn)題的離散塊,因?yàn)槲覀兊姆指钅P秃头诸惸P筒粫?huì)同時(shí)訓(xùn)練。這可能會(huì)限制我們解決方案的最高效果,但我們認(rèn)為這將帶來(lái)更好的學(xué)習(xí)體驗(yàn)。
我們認(rèn)為,能夠一次專注于一個(gè)步驟使我們能夠放大并集中精力學(xué)習(xí)的新技能數(shù)量更少。我們的兩個(gè)模型將專注于執(zhí)行一個(gè)任務(wù)。就像人類放射科醫(yī)生在逐層查看 CT 切片時(shí)一樣,如果范圍被很好地限定,訓(xùn)練工作就會(huì)變得更容易。我們還希望提供能夠?qū)?shù)據(jù)進(jìn)行豐富操作的工具。能夠放大并專注于特定位置的細(xì)節(jié)將對(duì)訓(xùn)練模型的整體生產(chǎn)率產(chǎn)生巨大影響,而不是一次查看整個(gè)圖像。我們的分割模型被迫消耗整個(gè)圖像,但我們將構(gòu)建結(jié)構(gòu),使我們的分類模型獲得感興趣區(qū)域的放大視圖。
第 3 步(分組)將生成數(shù)據(jù),第 4 步(分類)將消耗類似于圖 9.6 中包含腫瘤順序橫截面的圖像。這幅圖像是(潛在惡性,或至少不確定)腫瘤的近距離視圖,我們將訓(xùn)練第 4 步模型識(shí)別,并訓(xùn)練第 5 步模型將其分類為良性或惡性。對(duì)于未經(jīng)訓(xùn)練的眼睛(或未經(jīng)訓(xùn)練的卷積網(wǎng)絡(luò))來(lái)說(shuō),這個(gè)腫塊可能看起來(lái)毫無(wú)特征,但在這個(gè)樣本中識(shí)別惡性的預(yù)警信號(hào)至少比消耗我們之前看到的整個(gè) CT 要容易得多。我們下一章的代碼將提供生成類似圖 9.6 的放大結(jié)節(jié)圖像的例程。
圖 9.6 CT 掃描中腫瘤的近距離、多層切片裁剪
我們將在第十章中進(jìn)行第 1 步數(shù)據(jù)加載工作,第十一章和第十二章將專注于解決分類這些結(jié)節(jié)的問(wèn)題。之后,我們將回到第十三章工作于第 2 步(使用分割找到候選腫瘤),然后我們將在第十四章中結(jié)束本書(shū)的第 2 部分,通過(guò)實(shí)現(xiàn)第 3 步(分組)和第 5 步(結(jié)節(jié)分析和診斷)的端到端項(xiàng)目。
注意 CT 的標(biāo)準(zhǔn)呈現(xiàn)將上部放在圖像的頂部(基本上,頭向上),但 CT 按順序排列其切片,使第一切片是下部(向腳)。因此,Matplotlib 會(huì)顛倒圖像,除非我們注意翻轉(zhuǎn)它們。由于這種翻轉(zhuǎn)對(duì)我們的模型并不重要,我們不會(huì)在原始數(shù)據(jù)和模型之間增加代碼路徑的復(fù)雜性,但我們會(huì)在渲染代碼中添加翻轉(zhuǎn)以使圖像正面朝上。有關(guān) CT 坐標(biāo)系統(tǒng)的更多信息,請(qǐng)參見(jiàn)第 10.4 節(jié)。
讓我們?cè)趫D 9.7 中重復(fù)我們的高層概述。
圖 9.7 完成全胸 CT 掃描并確定患者是否患有惡性腫瘤的端到端過(guò)程
9.4.2 什么是結(jié)節(jié)?
正如我們所說(shuō)的,為了充分了解我們的數(shù)據(jù)以有效使用它,我們需要學(xué)習(xí)一些關(guān)于癌癥和放射腫瘤學(xué)的具體知識(shí)。我們需要了解的最后一件重要事情是什么是結(jié)節(jié)。簡(jiǎn)單來(lái)說(shuō),結(jié)節(jié)是可能出現(xiàn)在某人肺部?jī)?nèi)部的無(wú)數(shù)腫塊和隆起之一。有些從患者健康角度來(lái)看是有問(wèn)題的;有些則不是。精確的定義將結(jié)節(jié)的大小限制在 3 厘米以下,更大的腫塊被稱為肺塊;但我們將使用結(jié)節(jié)來(lái)交替使用所有這樣的解剖結(jié)構(gòu),因?yàn)檫@是一個(gè)相對(duì)任意的分界線,我們將使用相同的代碼路徑處理 3 厘米兩側(cè)的腫塊。肺部的小腫塊–結(jié)節(jié)–可能是良性或惡性腫瘤(也稱為癌癥)。從放射學(xué)的角度來(lái)看,結(jié)節(jié)與其他有各種原因的腫塊非常相似:感染、炎癥、血液供應(yīng)問(wèn)題、畸形血管以及除腫瘤外的其他疾病。
關(guān)鍵部分在于:我們?cè)噲D檢測(cè)的癌癥將始終是結(jié)節(jié),要么懸浮在肺部非密集組織中,要么附著在肺壁上。這意味著我們可以將我們的分類器限制在僅檢查結(jié)節(jié),而不是讓它檢查所有組織。能夠限制預(yù)期輸入范圍將有助于我們的分類器學(xué)習(xí)手頭的任務(wù)。
這是另一個(gè)例子,說(shuō)明我們將使用的基礎(chǔ)深度學(xué)習(xí)技術(shù)是通用的,但不能盲目應(yīng)用。我們需要了解我們所從事的領(lǐng)域,以做出對(duì)我們有利的選擇。
在圖 9.8 中,我們可以看到一個(gè)惡性結(jié)節(jié)的典型例子。我們關(guān)注的最小結(jié)節(jié)直徑僅幾毫米,盡管圖 9.8 中的結(jié)節(jié)較大。正如我們?cè)诒菊虑懊嬗懻摰哪菢?#xff0c;這使得最小結(jié)節(jié)大約比整個(gè) CT 掃描小一百萬(wàn)倍?;颊邫z測(cè)到的結(jié)節(jié)中超過(guò)一半不是惡性的。
圖 9.8 一張顯示惡性結(jié)節(jié)與其他結(jié)節(jié)視覺(jué)差異的 CT 掃描
9.4.3 我們的數(shù)據(jù)來(lái)源:LUNA 大挑戰(zhàn)
我們剛剛查看的 CT 掃描來(lái)自 LUNA(LUng Nodule Analysis)大挑戰(zhàn)。LUNA 大挑戰(zhàn)是一個(gè)開(kāi)放數(shù)據(jù)集與患者 CT 掃描(許多帶有肺結(jié)節(jié)的)高質(zhì)量標(biāo)簽的結(jié)合,以及對(duì)數(shù)據(jù)的分類器的公開(kāi)排名。有一種公開(kāi)分享醫(yī)學(xué)數(shù)據(jù)集用于研究和分析的文化;對(duì)這些數(shù)據(jù)的開(kāi)放訪問(wèn)使研究人員能夠在不必在機(jī)構(gòu)之間簽訂正式研究協(xié)議的情況下使用、結(jié)合和對(duì)這些數(shù)據(jù)進(jìn)行新穎的工作(顯然,某些數(shù)據(jù)也是保密的)。LUNA 大挑戰(zhàn)的目標(biāo)是通過(guò)讓團(tuán)隊(duì)輕松競(jìng)爭(zhēng)排名榜上的高位來(lái)鼓勵(lì)結(jié)節(jié)檢測(cè)的改進(jìn)。項(xiàng)目團(tuán)隊(duì)可以根據(jù)標(biāo)準(zhǔn)化標(biāo)準(zhǔn)(提供的數(shù)據(jù)集)測(cè)試其檢測(cè)方法的有效性。要包含在公開(kāi)排名中,團(tuán)隊(duì)必須提供描述項(xiàng)目架構(gòu)、訓(xùn)練方法等的科學(xué)論文。這為提供進(jìn)一步的想法和啟發(fā)項(xiàng)目改進(jìn)提供了很好的資源。
注意 許多 CT 掃描“在野外”非常混亂,因?yàn)楦鞣N掃描儀和處理程序之間存在獨(dú)特性。例如,一些掃描儀通過(guò)將那些超出掃描儀視野范圍的 CT 掃描區(qū)域的密度設(shè)置為負(fù)值來(lái)指示這些體素。CT 掃描也可以使用各種設(shè)置在 CT 掃描儀上獲取,這可能會(huì)以微妙或截然不同的方式改變結(jié)果圖像。盡管 LUNA 數(shù)據(jù)通常很干凈,但如果您整合其他數(shù)據(jù)源,請(qǐng)務(wù)必檢查您的假設(shè)。
我們將使用 LUNA 2016 數(shù)據(jù)集。LUNA 網(wǎng)站(luna16.grand-challenge.org/Description
)描述了挑戰(zhàn)的兩個(gè)軌道:第一軌道“結(jié)節(jié)檢測(cè)(NDET)”大致對(duì)應(yīng)于我們的第 1 步(分割);第二軌道“假陽(yáng)性減少(FPRED)”類似于我們的第 3 步(分類)。當(dāng)該網(wǎng)站討論“可能結(jié)節(jié)的位置”時(shí),它正在討論一個(gè)類似于我們將在第十三章中介紹的過(guò)程。
9.4.4 下載 LUNA 數(shù)據(jù)
在我們進(jìn)一步探討項(xiàng)目的細(xì)節(jié)之前,我們將介紹如何獲取我們將使用的數(shù)據(jù)。壓縮后的數(shù)據(jù)約為 60 GB,因此根據(jù)您的互聯(lián)網(wǎng)連接速度,可能需要一段時(shí)間才能下載。解壓后,它占用約 120 GB 的空間;我們還需要另外約 100 GB 的緩存空間來(lái)存儲(chǔ)較小的數(shù)據(jù)塊,以便我們可以比讀取整個(gè) CT 更快地訪問(wèn)它。
導(dǎo)航至 luna16.grand-challenge.org/download
并注冊(cè)使用電子郵件或使用 Google OAuth 登錄。登錄后,您應(yīng)該看到兩個(gè)指向 Zenodo 數(shù)據(jù)的下載鏈接,以及指向 Academic Torrents 的鏈接。無(wú)論哪個(gè)鏈接,數(shù)據(jù)應(yīng)該是相同的。
提示 截至目前,luna.grand-challenge.org 域名沒(méi)有鏈接到數(shù)據(jù)下載頁(yè)面。如果您在查找下載頁(yè)面時(shí)遇到問(wèn)題,請(qǐng)仔細(xì)檢查 luna16. 的域名,而不是 luna.
,如果需要,請(qǐng)重新輸入網(wǎng)址。
我們將使用的數(shù)據(jù)分為 10 個(gè)子集,分別命名為 subset0
到 subset9
。解壓縮每個(gè)子集,以便您有單獨(dú)的子目錄,如 code/data-unversioned/ part2/luna/subset0,依此類推。在 Linux 上,您將需要 7z
解壓縮實(shí)用程序(Ubuntu 通過(guò) p7zip-full
軟件包提供此功能)。Windows 用戶可以從 7-Zip 網(wǎng)站(www.7-zip.org)獲取提取器。某些解壓縮實(shí)用程序可能無(wú)法打開(kāi)存檔;如果出現(xiàn)錯(cuò)誤,請(qǐng)確保您使用的是提取器的完整版本。
另外,您需要 candidates.csv 和 annotations.csv 文件。為了方便起見(jiàn),我們已經(jīng)在書(shū)的網(wǎng)站和 GitHub 倉(cāng)庫(kù)中包含了這些文件,因此它們應(yīng)該已經(jīng)存在于 code/data/part2/luna/*.csv 中。也可以從與數(shù)據(jù)子集相同的位置下載它們。
注意 如果您沒(méi)有輕松獲得約 220 GB 的免費(fèi)磁盤(pán)空間,可以僅使用 1 或 2 個(gè)數(shù)據(jù)子集來(lái)運(yùn)行示例。較小的訓(xùn)練集將導(dǎo)致模型表現(xiàn)得更差,但這總比完全無(wú)法運(yùn)行示例要好。
一旦您擁有候選文件和至少一個(gè)已下載、解壓縮并放置在正確位置的子集,您應(yīng)該能夠開(kāi)始運(yùn)行本章的示例。如果您想提前開(kāi)始,可以使用 code/p2ch09_explore_data .ipynb Jupyter Notebook 來(lái)開(kāi)始。否則,我們將在本章后面更深入地討論筆記本。希望您的下載能在您開(kāi)始閱讀下一章之前完成!
9.5 結(jié)論
我們已經(jīng)取得了完成項(xiàng)目的重大進(jìn)展!您可能會(huì)覺(jué)得我們沒(méi)有取得多少成就;畢竟,我們還沒(méi)有實(shí)現(xiàn)一行代碼。但請(qǐng)記住,當(dāng)您獨(dú)自處理項(xiàng)目時(shí),您需要像我們?cè)谶@里做的研究和準(zhǔn)備一樣。
在本章中,我們著手完成了兩件事:
-
了解我們的肺癌檢測(cè)項(xiàng)目周圍的更大背景
-
勾勒出我們第二部分項(xiàng)目的方向和結(jié)構(gòu)
如果您仍然覺(jué)得我們沒(méi)有取得實(shí)質(zhì)性進(jìn)展,請(qǐng)意識(shí)到這種心態(tài)是一個(gè)陷阱–理解項(xiàng)目所處的領(lǐng)域至關(guān)重要,我們所做的設(shè)計(jì)工作將在我們繼續(xù)前進(jìn)時(shí)大大獲益。一旦我們?cè)诘谑麻_(kāi)始實(shí)現(xiàn)數(shù)據(jù)加載例程,我們將很快看到這些回報(bào)。
由于本章僅提供信息,沒(méi)有任何代碼,我們將暫時(shí)跳過(guò)練習(xí)。
9.6 總結(jié)
-
我們檢測(cè)癌性結(jié)節(jié)的方法將包括五個(gè)大致步驟:數(shù)據(jù)加載、分割、分組、分類以及結(jié)節(jié)分析和診斷。
-
將我們的項(xiàng)目分解為更小、半獨(dú)立的子項(xiàng)目,使得教授每個(gè)子項(xiàng)目變得更容易。對(duì)于未來(lái)具有不同目標(biāo)的項(xiàng)目,可能會(huì)采用其他方法,而不同于本書(shū)的目標(biāo)。
-
CT 掃描是一個(gè)包含大約 3200 萬(wàn)體素的強(qiáng)度數(shù)據(jù)的 3D 數(shù)組,大約比我們想要識(shí)別的結(jié)節(jié)大一百萬(wàn)倍。將模型集中在與手頭任務(wù)相關(guān)的 CT 掃描裁剪部分上,將使訓(xùn)練得到合理結(jié)果變得更容易。
-
理解我們的數(shù)據(jù)將使編寫(xiě)處理數(shù)據(jù)的程序更容易,這些程序不會(huì)扭曲或破壞數(shù)據(jù)的重要方面。CT 掃描數(shù)據(jù)的數(shù)組通常不會(huì)具有立方體像素;將現(xiàn)實(shí)世界單位的位置信息映射到數(shù)組索引需要進(jìn)行轉(zhuǎn)換。CT 掃描的強(qiáng)度大致對(duì)應(yīng)于質(zhì)量密度,但使用獨(dú)特的單位。
-
識(shí)別項(xiàng)目的關(guān)鍵概念,并確保它們?cè)谖覀兊脑O(shè)計(jì)中得到很好的體現(xiàn)是至關(guān)重要的。我們項(xiàng)目的大部分方面將圍繞著結(jié)節(jié)展開(kāi),這些結(jié)節(jié)是肺部的小腫塊,在 CT 上可以被發(fā)現(xiàn),與許多其他具有類似外觀的結(jié)構(gòu)一起。
-
我們正在使用 LUNA Grand Challenge 數(shù)據(jù)來(lái)訓(xùn)練我們的模型。LUNA 數(shù)據(jù)包含 CT 掃描,以及用于分類和分組的人工注釋輸出。擁有高質(zhì)量的數(shù)據(jù)對(duì)項(xiàng)目的成功有重大影響。
1 我們假設(shè)–我們還沒(méi)有嘗試過(guò),更不用說(shuō)計(jì)時(shí)了。
2 這個(gè)樣本的series_uid
是1.3.6.1.4.1.14519.5.2.1.6279.6001.12626457893177825889037 1755354
,如果您以后想要詳細(xì)查看它,這可能會(huì)很有用。
3 例如,Retina U-Net (arxiv.org/pdf/1811.08661.pdf
) 和 FishNet (mng.bz/K240
)。
?Eric J. Olson,“肺結(jié)節(jié):它們可能是癌癥嗎?”梅奧診所,mng.bz/yyge
。
? 至少如果我們想要得到像樣的結(jié)果的話,是不行的。
? 根據(jù)國(guó)家癌癥研究所癌癥術(shù)語(yǔ)詞典:mng.bz/jgBP
。
? 所需的緩存空間是按章節(jié)計(jì)算的,但一旦完成了一個(gè)章節(jié),你可以刪除緩存以釋放空間。
十、將數(shù)據(jù)源合并為統(tǒng)一數(shù)據(jù)集
本章涵蓋
-
加載和處理原始數(shù)據(jù)文件
-
實(shí)現(xiàn)一個(gè)表示我們數(shù)據(jù)的 Python 類
-
將我們的數(shù)據(jù)轉(zhuǎn)換為 PyTorch 可用的格式
-
可視化訓(xùn)練和驗(yàn)證數(shù)據(jù)
現(xiàn)在我們已經(jīng)討論了第二部分的高層目標(biāo),以及概述了數(shù)據(jù)將如何在我們的系統(tǒng)中流動(dòng),讓我們具體了解一下這一章我們將要做什么?,F(xiàn)在是時(shí)候?yàn)槲覀兊脑紨?shù)據(jù)實(shí)現(xiàn)基本的數(shù)據(jù)加載和數(shù)據(jù)處理例程了?;旧?#xff0c;你在工作中涉及的每個(gè)重要項(xiàng)目都需要類似于我們?cè)谶@里介紹的內(nèi)容。1 圖 10.1 展示了我們項(xiàng)目的高層地圖,來(lái)自第九章。我們將在本章的其余部分專注于第 1 步,數(shù)據(jù)加載。
圖 10.1 我們端到端的肺癌檢測(cè)項(xiàng)目,重點(diǎn)關(guān)注本章的主題:第 1 步,數(shù)據(jù)加載
我們的目標(biāo)是能夠根據(jù)我們的原始 CT 掃描數(shù)據(jù)和這些 CT 的注釋列表生成一個(gè)訓(xùn)練樣本。這聽(tīng)起來(lái)可能很簡(jiǎn)單,但在我們加載、處理和提取我們感興趣的數(shù)據(jù)之前,需要發(fā)生很多事情。圖 10.2 展示了我們需要做的工作,將我們的原始數(shù)據(jù)轉(zhuǎn)換為訓(xùn)練樣本。幸運(yùn)的是,在上一章中,我們已經(jīng)對(duì)我們的數(shù)據(jù)有了一些理解,但在這方面我們還有更多工作要做。
圖 10.2 制作樣本元組所需的數(shù)據(jù)轉(zhuǎn)換。這些樣本元組將作為我們模型訓(xùn)練例程的輸入。
這是一個(gè)關(guān)鍵時(shí)刻,當(dāng)我們開(kāi)始將沉重的原始數(shù)據(jù)轉(zhuǎn)變,如果不是成為黃金,至少也是我們的神經(jīng)網(wǎng)絡(luò)將會(huì)將其轉(zhuǎn)變?yōu)辄S金的材料。我們?cè)诘谒恼轮惺状斡懻摿诉@種轉(zhuǎn)變的機(jī)制。
10.1 原始 CT 數(shù)據(jù)文件
我們的 CT 數(shù)據(jù)分為兩個(gè)文件:一個(gè)包含元數(shù)據(jù)頭信息的.mhd 文件,以及一個(gè)包含組成 3D 數(shù)組的原始字節(jié)的.raw 文件。每個(gè)文件的名稱都以稱為系列 UID(名稱來(lái)自數(shù)字影像和通信醫(yī)學(xué)[DICOM]命名法)的唯一標(biāo)識(shí)符開(kāi)頭,用于討論的 CT 掃描。例如,對(duì)于系列 UID 1.2.3,將有兩個(gè)文件:1.2.3.mhd 和 1.2.3.raw。
我們的Ct
類將消耗這兩個(gè)文件并生成 3D 數(shù)組,以及轉(zhuǎn)換矩陣,將患者坐標(biāo)系(我們將在第 10.6 節(jié)中更詳細(xì)地討論)轉(zhuǎn)換為數(shù)組所需的索引、行、列坐標(biāo)(這些坐標(biāo)在圖中顯示為(I,R,C),在代碼中用_irc
變量后綴表示)。現(xiàn)在不要為所有這些細(xì)節(jié)擔(dān)心;只需記住,在我們應(yīng)用這些坐標(biāo)到我們的 CT 數(shù)據(jù)之前,我們需要進(jìn)行一些坐標(biāo)系轉(zhuǎn)換。我們將根據(jù)需要探討細(xì)節(jié)。
我們還將加載 LUNA 提供的注釋數(shù)據(jù),這將為我們提供一個(gè)結(jié)節(jié)坐標(biāo)列表,每個(gè)坐標(biāo)都有一個(gè)惡性標(biāo)志,以及相關(guān) CT 掃描的系列 UID。通過(guò)將結(jié)節(jié)坐標(biāo)與坐標(biāo)系轉(zhuǎn)換信息結(jié)合起來(lái),我們得到了我們結(jié)節(jié)中心的體素的索引、行和列。
使用(I,R,C)坐標(biāo),我們可以裁剪我們的 CT 數(shù)據(jù)的一個(gè)小的 3D 切片作為我們模型的輸入。除了這個(gè) 3D 樣本數(shù)組,我們必須構(gòu)建我們的訓(xùn)練樣本元組的其余部分,其中將包括樣本數(shù)組、結(jié)節(jié)狀態(tài)標(biāo)志、系列 UID 以及該樣本在結(jié)節(jié)候選 CT 列表中的索引。這個(gè)樣本元組正是 PyTorch 從我們的Dataset
子類中期望的,并代表了我們從原始原始數(shù)據(jù)到 PyTorch 張量的標(biāo)準(zhǔn)結(jié)構(gòu)的橋梁的最后部分。
限制或裁剪我們的數(shù)據(jù)以避免讓模型淹沒(méi)在噪音中是重要的,同樣重要的是確保我們不要過(guò)于激進(jìn),以至于我們的信號(hào)被裁剪掉。我們希望確保我們的數(shù)據(jù)范圍行為良好,尤其是在歸一化之后。裁剪數(shù)據(jù)以去除異常值可能很有用,特別是如果我們的數(shù)據(jù)容易出現(xiàn)極端異常值。我們還可以創(chuàng)建手工制作的、算法轉(zhuǎn)換的輸入;這被稱為特征工程;我們?cè)诘谝徽轮泻?jiǎn)要討論過(guò)。通常我們會(huì)讓模型大部分工作;特征工程有其用處,但在第 2 部分中我們不會(huì)使用它。
10.2 解析 LUNA 的注釋數(shù)據(jù)
我們需要做的第一件事是開(kāi)始加載我們的數(shù)據(jù)。在著手新項(xiàng)目時(shí),這通常是一個(gè)很好的起點(diǎn)。確保我們知道如何處理原始輸入是必需的,無(wú)論如何,知道我們的數(shù)據(jù)加載后會(huì)是什么樣子可以幫助我們制定早期實(shí)驗(yàn)的結(jié)構(gòu)。我們可以嘗試加載單個(gè) CT 掃描,但我們認(rèn)為解析 LUNA 提供的包含每個(gè) CT 掃描中感興趣點(diǎn)信息的 CSV 文件是有意義的。正如我們?cè)趫D 10.3 中看到的,我們期望獲得一些坐標(biāo)信息、一個(gè)指示坐標(biāo)是否為結(jié)節(jié)的標(biāo)志以及 CT 掃描的唯一標(biāo)識(shí)符。由于 CSV 文件中的信息類型較少,而且更容易解析,我們希望它們能給我們一些線索,告訴我們一旦開(kāi)始加載 CT 掃描后要尋找什么。
圖 10.3 candidates.csv 中的 LUNA 注釋包含 CT 系列、結(jié)節(jié)候選位置以及指示候選是否實(shí)際為結(jié)節(jié)的標(biāo)志。
candidates.csv 文件包含有關(guān)所有潛在看起來(lái)像結(jié)節(jié)的腫塊的信息,無(wú)論這些腫塊是惡性的、良性腫瘤還是完全不同的東西。我們將以此為基礎(chǔ)構(gòu)建一個(gè)完整的候選人列表,然后將其分成訓(xùn)練和驗(yàn)證數(shù)據(jù)集。以下是 Bash shell 會(huì)話顯示文件包含的內(nèi)容:
$ wc -l candidates.csv # ?
551066 candidates.csv$ head data/part2/luna/candidates.csv # ?
seriesuid,coordX,coordY,coordZ,class # ?
1.3...6860,-56.08,-67.85,-311.92,0
1.3...6860,53.21,-244.41,-245.17,0
1.3...6860,103.66,-121.8,-286.62,0
1.3...6860,-33.66,-72.75,-308.41,0
...$ grep ',1$' candidates.csv | wc -l # ?
1351
? 統(tǒng)計(jì)文件中的行數(shù)
? 打印文件的前幾行
? .csv 文件的第一行定義了列標(biāo)題。
? 統(tǒng)計(jì)以 1 結(jié)尾的行數(shù),表示惡性
注意 seriesuid
列中的值已被省略以更好地適應(yīng)打印頁(yè)面。
因此,我們有 551,000 行,每行都有一個(gè)seriesuid
(我們?cè)诖a中將其稱為series_uid
)、一些(X,Y,Z)坐標(biāo)和一個(gè)class
列,對(duì)應(yīng)于結(jié)節(jié)狀態(tài)(這是一個(gè)布爾值:0 表示不是實(shí)際結(jié)節(jié)的候選人,1 表示是結(jié)節(jié)的候選人,無(wú)論是惡性還是良性)。我們有 1,351 個(gè)標(biāo)記為實(shí)際結(jié)節(jié)的候選人。
annotations.csv 文件包含有關(guān)被標(biāo)記為結(jié)節(jié)的一些候選人的信息。我們特別關(guān)注diameter_mm
信息:
$ wc -l annotations.csv
1187 annotations.csv # ?$ head data/part2/luna/annotations.csv
seriesuid,coordX,coordY,coordZ,diameter_mm # ?
1.3.6...6860,-128.6994211,-175.3192718,-298.3875064,5.651470635
1.3.6...6860,103.7836509,-211.9251487,-227.12125,4.224708481
1.3.6...5208,69.63901724,-140.9445859,876.3744957,5.786347814
1.3.6...0405,-24.0138242,192.1024053,-391.0812764,8.143261683
...
? 這是與 candidates.csv 文件中不同的數(shù)字。
? 最后一列也不同。
我們有大約 1,200 個(gè)結(jié)節(jié)的大小信息。這很有用,因?yàn)槲覀兛梢允褂盟鼇?lái)確保我們的訓(xùn)練和驗(yàn)證數(shù)據(jù)包含了結(jié)節(jié)大小的代表性分布。如果沒(méi)有這個(gè),我們的驗(yàn)證集可能只包含極端值,使得看起來(lái)我們的模型表現(xiàn)不佳。
10.2.1 訓(xùn)練和驗(yàn)證集
對(duì)于任何標(biāo)準(zhǔn)的監(jiān)督學(xué)習(xí)任務(wù)(分類是典型示例),我們將把數(shù)據(jù)分成訓(xùn)練集和驗(yàn)證集。我們希望確保兩個(gè)集合都代表我們預(yù)期看到和正常處理的真實(shí)世界輸入數(shù)據(jù)范圍。如果任一集合與我們的真實(shí)用例有實(shí)質(zhì)性不同,那么我們的模型行為很可能與我們的預(yù)期不同–我們收集的所有訓(xùn)練和統(tǒng)計(jì)數(shù)據(jù)在轉(zhuǎn)移到生產(chǎn)使用時(shí)將不具有預(yù)測(cè)性!我們并不試圖使這成為一門(mén)精確的科學(xué),但您應(yīng)該在未來(lái)的項(xiàng)目中留意,以確保您正在對(duì)不適合您操作環(huán)境的數(shù)據(jù)進(jìn)行訓(xùn)練和測(cè)試。
讓我們回到我們的結(jié)節(jié)。我們將按大小對(duì)它們進(jìn)行排序,并取每第N個(gè)用于我們的驗(yàn)證集。這應(yīng)該給我們所期望的代表性分布。不幸的是,annotations.csv 中提供的位置信息并不總是與 candidates.csv 中的坐標(biāo)精確對(duì)齊:
$ grep 100225287222365663678666836860 annotations.csv
1.3.6...6860,-128.6994211,-175.3192718,-298.3875064,5.651470635 # ?
1.3.6...6860,103.7836509,-211.9251487,-227.12125,4.224708481$ grep '100225287222365663678666836860.*,1$' candidates.csv
1.3.6...6860,104.16480444,-211.685591018,-227.011363746,1
1.3.6...6860,-128.94,-175.04,-297.87,1 # ?
? 這兩個(gè)坐標(biāo)非常接近。
如果我們從每個(gè)文件中截取相應(yīng)的坐標(biāo),我們得到的是(-128.70, -175.32,-298.39)與(-128.94,-175.04,-297.87)。由于問(wèn)題中的結(jié)節(jié)直徑為 5 毫米,這兩個(gè)點(diǎn)顯然都是結(jié)節(jié)的“中心”,但它們并不完全對(duì)齊。決定處理這種數(shù)據(jù)不匹配是否值得并忽略該文件是完全合理的反應(yīng)。然而,我們將努力使事情對(duì)齊,因?yàn)楝F(xiàn)實(shí)世界的數(shù)據(jù)集通常以這種方式不完美,并且這是您需要做的工作的一個(gè)很好的例子,以從不同的數(shù)據(jù)源中組裝數(shù)據(jù)。
10.2.2 統(tǒng)一我們的注釋和候選數(shù)據(jù)
現(xiàn)在我們知道我們的原始數(shù)據(jù)文件是什么樣子的,讓我們構(gòu)建一個(gè)getCandidateInfoList
函數(shù),將所有內(nèi)容串聯(lián)起來(lái)。我們將使用文件頂部定義的命名元組來(lái)保存每個(gè)結(jié)節(jié)的信息。
列表 10.1 dsets.py:7
from collections import namedtuple
# ... line 27
CandidateInfoTuple = namedtuple('CandidateInfoTuple','isNodule_bool, diameter_mm, series_uid, center_xyz',
)
這些元組不是我們的訓(xùn)練樣本,因?yàn)樗鼈內(nèi)鄙傥覀冃枰?CT 數(shù)據(jù)塊。相反,這些代表了我們正在使用的人工注釋數(shù)據(jù)的經(jīng)過(guò)消毒、清潔、統(tǒng)一的接口。將必須處理混亂數(shù)據(jù)與模型訓(xùn)練隔離開(kāi)非常重要。否則,你的訓(xùn)練循環(huán)會(huì)很快變得混亂,因?yàn)槟惚仨氃诒緫?yīng)專注于訓(xùn)練的代碼中不斷處理特殊情況和其他干擾。
提示 明確地將負(fù)責(zé)數(shù)據(jù)消毒的代碼與項(xiàng)目的其余部分分開(kāi)。如果需要,不要害怕重寫(xiě)數(shù)據(jù)一次并將其保存到磁盤(pán)。
我們的候選信息列表將包括結(jié)節(jié)狀態(tài)(我們將訓(xùn)練模型對(duì)其進(jìn)行分類)、直徑(有助于在訓(xùn)練中獲得良好的分布,因?yàn)榇蠛托〗Y(jié)節(jié)不會(huì)具有相同的特征)、系列(用于定位正確的 CT 掃描)、候選中心(用于在較大的 CT 中找到候選)。構(gòu)建這些NoduleInfoTuple
實(shí)例列表的函數(shù)首先使用內(nèi)存緩存裝飾器,然后獲取磁盤(pán)上存在的文件列表。
列表 10.2 dsets.py:32
@functools.lru_cache(1) # ?
def getCandidateInfoList(requireOnDisk_bool=True): # ?mhd_list = glob.glob('data-unversioned/part2/luna/subset*/*.mhd')presentOnDisk_set = {os.path.split(p)[-1][:-4] for p in mhd_list}
? 標(biāo)準(zhǔn)庫(kù)內(nèi)存緩存
? requireOnDisk_bool 默認(rèn)篩選掉尚未就位的數(shù)據(jù)子集中的系列。
由于解析某些數(shù)據(jù)文件可能很慢,我們將在內(nèi)存中緩存此函數(shù)調(diào)用的結(jié)果。這將在以后很有用,因?yàn)槲覀儗⒃谖磥?lái)的章節(jié)中更頻繁地調(diào)用此函數(shù)。通過(guò)仔細(xì)應(yīng)用內(nèi)存或磁盤(pán)緩存來(lái)加速我們的數(shù)據(jù)流水線,可以在訓(xùn)練速度上取得一些令人印象深刻的收益。在您的項(xiàng)目中工作時(shí),請(qǐng)留意這些機(jī)會(huì)。
之前我們說(shuō)過(guò),我們將支持使用不完整的訓(xùn)練數(shù)據(jù)集運(yùn)行我們的訓(xùn)練程序,因?yàn)橄螺d時(shí)間長(zhǎng)且磁盤(pán)空間要求高。requireOnDisk_bool
參數(shù)是實(shí)現(xiàn)這一承諾的關(guān)鍵;我們正在檢測(cè)哪些 LUNA 系列 UID 實(shí)際上存在并準(zhǔn)備從磁盤(pán)加載,并且我們將使用該信息來(lái)限制我們從即將解析的 CSV 文件中使用的條目。能夠通過(guò)訓(xùn)練循環(huán)運(yùn)行我們數(shù)據(jù)的子集對(duì)于驗(yàn)證代碼是否按預(yù)期工作很有用。通常情況下,當(dāng)這樣做時(shí),模型的訓(xùn)練結(jié)果很差,幾乎無(wú)用,但是進(jìn)行日志記錄、指標(biāo)、模型檢查點(diǎn)等功能的練習(xí)是有益的。
在獲取候選人信息后,我們希望合并注釋.csv 中的直徑信息。首先,我們需要按 series_uid
對(duì)我們的注釋進(jìn)行分組,因?yàn)檫@是我們將用來(lái)交叉參考兩個(gè)文件中每一行的第一個(gè)關(guān)鍵字。
代碼清單 10.3 dsets.py:40,def
getCandidateInfoList
diameter_dict = {}
with open('data/part2/luna/annotations.csv', "r") as f:for row in list(csv.reader(f))[1:]:series_uid = row[0]annotationCenter_xyz = tuple([float(x) for x in row[1:4]])annotationDiameter_mm = float(row[4])diameter_dict.setdefault(series_uid, []).append((annotationCenter_xyz, annotationDiameter_mm))
現(xiàn)在我們將使用 candidates.csv 文件中的信息構(gòu)建候選人的完整列表。
代碼清單 10.4 dsets.py:51,def
getCandidateInfoList
candidateInfo_list = []
with open('data/part2/luna/candidates.csv', "r") as f:for row in list(csv.reader(f))[1:]:series_uid = row[0]if series_uid not in presentOnDisk_set and requireOnDisk_bool: # ?continueisNodule_bool = bool(int(row[4]))candidateCenter_xyz = tuple([float(x) for x in row[1:4]])candidateDiameter_mm = 0.0for annotation_tup in diameter_dict.get(series_uid, []):annotationCenter_xyz, annotationDiameter_mm = annotation_tupfor i in range(3):delta_mm = abs(candidateCenter_xyz[i] - annotationCenter_xyz[i])if delta_mm > annotationDiameter_mm / 4: # ?breakelse:candidateDiameter_mm = annotationDiameter_mmbreakcandidateInfo_list.append(CandidateInfoTuple(isNodule_bool,candidateDiameter_mm,series_uid,candidateCenter_xyz,))
? 如果系列 UID 不存在,則它在我們沒(méi)有在磁盤(pán)上的子集中,因此我們應(yīng)該跳過(guò)它。
? 將直徑除以 2 得到半徑,并將半徑除以 2 要求兩個(gè)結(jié)節(jié)中心點(diǎn)相對(duì)于結(jié)節(jié)大小不要相距太遠(yuǎn)。(這導(dǎo)致一個(gè)邊界框檢查,而不是真正的距離檢查。)
對(duì)于給定 series_uid
的每個(gè)候選人條目,我們循環(huán)遍歷我們之前收集的相同 series_uid
的注釋,看看這兩個(gè)坐標(biāo)是否足夠接近以將它們視為同一個(gè)結(jié)節(jié)。如果是,太好了!現(xiàn)在我們有了該結(jié)節(jié)的直徑信息。如果我們找不到匹配項(xiàng),那沒(méi)關(guān)系;我們將只將該結(jié)節(jié)視為直徑為 0.0。由于我們只是使用這些信息來(lái)在我們的訓(xùn)練和驗(yàn)證集中獲得結(jié)節(jié)尺寸的良好分布,對(duì)于一些結(jié)節(jié)的直徑尺寸不正確不應(yīng)該是問(wèn)題,但我們應(yīng)該記住我們這樣做是為了防止我們這里的假設(shè)是錯(cuò)誤的情況。
這是為了合并我們的結(jié)節(jié)直徑而進(jìn)行的許多有些繁瑣的代碼。不幸的是,根據(jù)您的原始數(shù)據(jù),必須進(jìn)行這種操作和模糊匹配可能是相當(dāng)常見(jiàn)的。然而,一旦我們到達(dá)這一點(diǎn),我們只需要對(duì)數(shù)據(jù)進(jìn)行排序并返回即可。
代碼清單 10.5 dsets.py:80,def
getCandidateInfoList
candidateInfo_list.sort(reverse=True) # ?
return candidateInfo_list
? 這意味著我們所有實(shí)際結(jié)節(jié)樣本都是從最大的開(kāi)始,然后是所有非結(jié)節(jié)樣本(這些樣本沒(méi)有結(jié)節(jié)大小信息)。
元組成員在 noduleInfo_list
中的排序是由此排序驅(qū)動(dòng)的。我們使用這種排序方法來(lái)幫助確保當(dāng)我們?nèi)?shù)據(jù)的一個(gè)切片時(shí),該切片獲得一組具有良好結(jié)節(jié)直徑分布的實(shí)際結(jié)節(jié)。我們將在第 10.5.3 節(jié)中進(jìn)一步討論這一點(diǎn)。
10.3 加載單個(gè) CT 掃描
接下來(lái),我們需要能夠?qū)⑽覀兊?CT 數(shù)據(jù)從磁盤(pán)上的一堆位轉(zhuǎn)換為一個(gè) Python 對(duì)象,從中我們可以提取 3D 結(jié)節(jié)密度數(shù)據(jù)。我們可以從圖 10.4 中看到這條路徑,從 .mhd 和 .raw 文件到 Ct
對(duì)象。我們的結(jié)節(jié)注釋信息就像是我們?cè)紨?shù)據(jù)中有趣部分的地圖。在我們可以按照這張地圖找到我們感興趣的數(shù)據(jù)之前,我們需要將數(shù)據(jù)轉(zhuǎn)換為可尋址的形式。
圖 10.4 加載 CT 掃描產(chǎn)生一個(gè)體素?cái)?shù)組和一個(gè)從患者坐標(biāo)到數(shù)組索引的轉(zhuǎn)換。
提示 擁有大量原始數(shù)據(jù),其中大部分是無(wú)趣的,是一種常見(jiàn)情況;在處理自己的項(xiàng)目時(shí),尋找方法限制范圍僅限于相關(guān)數(shù)據(jù)是很重要的。
CT 掃描的本機(jī)文件格式是 DICOM(www.dicomstandard.org)。DICOM 標(biāo)準(zhǔn)的第一個(gè)版本是在 1984 年編寫(xiě)的,正如我們可能期望的那樣,來(lái)自那個(gè)時(shí)期的任何與計(jì)算有關(guān)的東西都有點(diǎn)混亂(例如,現(xiàn)在已經(jīng)廢棄的整個(gè)部分專門(mén)用于選擇要使用的數(shù)據(jù)鏈路層協(xié)議,因?yàn)楫?dāng)時(shí)以太網(wǎng)還沒(méi)有勝出)。
注意 我們已經(jīng)找到了正確的庫(kù)來(lái)解析這些原始數(shù)據(jù)文件,但對(duì)于你從未聽(tīng)說(shuō)過(guò)的其他格式,你將不得不自己找到一個(gè)解析器。我們建議花時(shí)間去做這件事!Python 生態(tài)系統(tǒng)幾乎為太陽(yáng)下的每種文件格式都提供了解析器,你的時(shí)間幾乎肯定比寫(xiě)解析器來(lái)處理奇特?cái)?shù)據(jù)格式的工作更值得花費(fèi)在項(xiàng)目的新穎部分上。
令人高興的是,LUNA 已經(jīng)將我們將在本章中使用的數(shù)據(jù)轉(zhuǎn)換為 MetaIO 格式,這樣使用起來(lái)要容易得多(itk.org/Wiki/MetaIO/Documentation#Quick_Start
)。如果你以前從未聽(tīng)說(shuō)過(guò)這種格式,不用擔(dān)心!我們可以將數(shù)據(jù)文件的格式視為黑匣子,并使用SimpleITK
將其加載到更熟悉的 NumPy 數(shù)組中。
代碼清單 10.6 dsets.py:9
import SimpleITK as sitk
# ... line 83
class Ct:def __init__(self, series_uid):mhd_path = glob.glob('data-unversioned/part2/luna/subset*/{}.mhd'.format(series_uid) # ?)[0]ct_mhd = sitk.ReadImage(mhd_path) # ?ct_a = np.array(sitk.GetArrayFromImage(ct_mhd), dtype=np.float32) # ?
? 我們不關(guān)心給定 series_uid 屬于哪個(gè)子集,因此我們使用通配符來(lái)匹配子集。
? sitk.ReadImage
隱式消耗了傳入的.mhd
文件以及.raw
文件。
? 重新創(chuàng)建一個(gè) np.array,因?yàn)槲覀兿雽⒅殿愋娃D(zhuǎn)換為 np.float3。
對(duì)于真實(shí)項(xiàng)目,你會(huì)想要了解原始數(shù)據(jù)中包含哪些類型的信息,但依賴像SimpleITK
這樣的第三方代碼來(lái)解析磁盤(pán)上的位是完全可以的。找到了關(guān)于你的輸入的一切與盲目接受你的數(shù)據(jù)加載庫(kù)提供的一切之間的正確平衡可能需要一些經(jīng)驗(yàn)。只需記住,我們主要關(guān)心的是數(shù)據(jù),而不是位。重要的是信息,而不是它的表示方式。
能夠唯一標(biāo)識(shí)我們數(shù)據(jù)中的特定樣本是很有用的。例如,清楚地傳達(dá)哪個(gè)樣本導(dǎo)致問(wèn)題或得到較差的分類結(jié)果可以極大地提高我們隔離和調(diào)試問(wèn)題的能力。根據(jù)我們樣本的性質(zhì),有時(shí)這個(gè)唯一標(biāo)識(shí)符是一個(gè)原子,比如一個(gè)數(shù)字或一個(gè)字符串,有時(shí)它更復(fù)雜,比如一個(gè)元組。
我們使用系列實(shí)例 UID(series_uid
)來(lái)唯一標(biāo)識(shí)特定的 CT 掃描,該 UID 是在創(chuàng)建 CT 掃描時(shí)分配的。DICOM 在個(gè)別 DICOM 文件、文件組、治療過(guò)程等方面大量使用唯一標(biāo)識(shí)符(UID),這些標(biāo)識(shí)符在概念上類似于 UUIDs(docs.python.org/3.6/library/uuid.html
),但它們具有不同的創(chuàng)建過(guò)程和不同的格式。對(duì)于我們的目的,我們可以將它們視為不透明的 ASCII 字符串,用作引用各種 CT 掃描的唯一鍵。官方上,DICOM UID 中只有字符 0 到 9 和句點(diǎn)(.)是有效字符,但一些野外的 DICOM 文件已經(jīng)通過(guò)替換 UID 為十六進(jìn)制(0-9 和 a-f)或其他技術(shù)上不符合規(guī)范的值進(jìn)行了匿名化(這些不符合規(guī)范的值通常不會(huì)被 DICOM 解析器標(biāo)記或清理;正如我們之前所說(shuō),這有點(diǎn)混亂)。
我們之前討論的 10 個(gè)子集中,每個(gè)子集大約有 90 個(gè) CT 掃描(總共 888 個(gè)),每個(gè) CT 掃描表示為兩個(gè)文件:一個(gè)帶有.mhd
擴(kuò)展名的文件和一個(gè)帶有.raw
擴(kuò)展名的文件。數(shù)據(jù)被分割到多個(gè)文件中是由sitk
例程隱藏的,因此我們不需要直接關(guān)注這一點(diǎn)。
此時(shí),ct_a
是一個(gè)三維數(shù)組。所有三個(gè)維度都是空間維度,單一的強(qiáng)度通道是隱含的。正如我們?cè)诘谒恼轮锌吹降?#xff0c;在 PyTorch 張量中,通道信息被表示為一個(gè)大小為 1 的第四維。
10.3.1 豪斯菲爾德單位
回想一下,我們之前說(shuō)過(guò)我們需要了解我們的數(shù)據(jù),而不是存儲(chǔ)數(shù)據(jù)的位。在這里,我們有一個(gè)完美的實(shí)例。如果不了解數(shù)據(jù)值和范圍的微妙之處,我們將向模型輸入值,這將妨礙其學(xué)習(xí)我們想要的內(nèi)容。
繼續(xù)__init__
方法,我們需要對(duì)ct_a
值進(jìn)行一些清理。CT 掃描體素以豪斯菲爾德單位(HU;en.wikipedia.org/ wiki/Hounsfield_scale
)表示,這是奇怪的單位;空氣為-1,000 HU(對(duì)于我們的目的足夠接近 0 克/立方厘米),水為 0 HU(1 克/立方厘米),骨骼至少為+1,000 HU(2-3 克/立方厘米)。
注意 HU 值通常以有符號(hào)的 12 位整數(shù)(塞入 16 位整數(shù))的形式存儲(chǔ)在磁盤(pán)上,這與 CT 掃描儀提供的精度水平相匹配。雖然這可能很有趣,但與項(xiàng)目無(wú)關(guān)。
一些 CT 掃描儀使用與負(fù)密度對(duì)應(yīng)的 HU 值來(lái)指示那些體素位于 CT 掃描儀視野之外。對(duì)于我們的目的,患者之外的一切都應(yīng)該是空氣,因此我們通過(guò)將值的下限設(shè)置為-1,000 HU 來(lái)丟棄該視野信息。同樣,骨骼、金屬植入物等的確切密度與我們的用例無(wú)關(guān),因此我們將密度限制在大約 2 克/立方厘米(1,000 HU),即使在大多數(shù)情況下這在生物學(xué)上并不準(zhǔn)確。
列表 10.7 dsets.py:96,Ct.__init__
ct_a.clip(-1000, 1000, ct_a)
高于 0 HU 的值與密度并不完全匹配,但我們感興趣的腫瘤通常在 1 克/立方厘米(0 HU)左右,因此我們將忽略 HU 與克/立方厘米等常見(jiàn)單位并不完全對(duì)應(yīng)的事實(shí)。這沒(méi)關(guān)系,因?yàn)槲覀兊哪P蛯⒈挥?xùn)練直接使用 HU。
我們希望從我們的數(shù)據(jù)中刪除所有這些異常值:它們與我們的目標(biāo)沒(méi)有直接關(guān)聯(lián),而且這些異常值可能會(huì)使模型的工作變得更加困難。這種情況可能以多種方式發(fā)生,但一個(gè)常見(jiàn)的例子是當(dāng)批量歸一化被這些異常值輸入時(shí),關(guān)于如何最佳歸一化數(shù)據(jù)的統(tǒng)計(jì)數(shù)據(jù)會(huì)被扭曲。始終注意清理數(shù)據(jù)的方法。
我們現(xiàn)在已經(jīng)將所有構(gòu)建的值分配給self
。
列表 10.8 dsets.py:98,Ct.__init__
self.series_uid = series_uid
self.hu_a = ct_a
重要的是要知道我們的數(shù)據(jù)使用-1,000 到+1,000 的范圍,因?yàn)樵诘谑轮?#xff0c;我們最終會(huì)向我們的樣本添加信息通道。如果我們不考慮 HU 和我們額外數(shù)據(jù)之間的差異,那么這些新通道很容易被原始 HU 值所掩蓋。對(duì)于我們項(xiàng)目的分類步驟,我們不會(huì)添加更多的數(shù)據(jù)通道,因此我們現(xiàn)在不需要實(shí)施特殊處理。
10.4 使用患者坐標(biāo)系定位結(jié)節(jié)
深度學(xué)習(xí)模型通常需要固定大小的輸入,2因?yàn)橛泄潭〝?shù)量的輸入神經(jīng)元。我們需要能夠生成一個(gè)包含候選者的固定大小數(shù)組,以便我們可以將其用作分類器的輸入。我們希望訓(xùn)練我們的模型時(shí)使用一個(gè)裁剪的 CT 掃描,其中候選者被很好地居中,因?yàn)檫@樣我們的模型就不必學(xué)習(xí)如何注意藏在輸入角落的結(jié)節(jié)。通過(guò)減少預(yù)期輸入的變化,我們使模型的工作變得更容易。
10.4.1 患者坐標(biāo)系
不幸的是,我們?cè)诘?10.2 節(jié)加載的所有候選中心數(shù)據(jù)都是以毫米為單位表示的,而不是體素!我們不能簡(jiǎn)單地將毫米位置插入數(shù)組索引中,然后期望一切按我們想要的方式進(jìn)行。正如我們?cè)趫D 10.5 中所看到的,我們需要將我們的坐標(biāo)從以毫米表示的坐標(biāo)系(X,Y,Z)轉(zhuǎn)換為用于從 CT 掃描數(shù)據(jù)中獲取數(shù)組切片的基于體素地址的坐標(biāo)系(I,R,C)。這是一個(gè)重要的例子,說(shuō)明了一致處理單位的重要性!
圖 10.5 使用轉(zhuǎn)換信息將病人坐標(biāo)中的結(jié)節(jié)中心坐標(biāo)(X,Y,Z)轉(zhuǎn)換為數(shù)組索引(索引,行,列)。
正如我們之前提到的,處理 CT 掃描時(shí),我們將數(shù)組維度稱為索引、行和列,因?yàn)?X、Y 和 Z 有不同的含義,如圖 10.6 所示。病人坐標(biāo)系定義正 X 為病人左側(cè)(左),正 Y 為病人后方(后方),正 Z 為朝向病人頭部(上部)。左后上有時(shí)會(huì)縮寫(xiě)為LPS。
圖 10.6 我們穿著不當(dāng)?shù)牟∪苏故玖瞬∪俗鴺?biāo)系的軸線
病人坐標(biāo)系以毫米為單位測(cè)量,并且具有任意位置的原點(diǎn),不與 CT 體素?cái)?shù)組的原點(diǎn)對(duì)應(yīng),如圖 10.7 所示。
圖 10.7 數(shù)組坐標(biāo)和病人坐標(biāo)具有不同的原點(diǎn)和比例。
病人坐標(biāo)系通常用于指定有趣解剖的位置,這種方式與任何特定掃描無(wú)關(guān)。定義 CT 數(shù)組與病人坐標(biāo)系之間關(guān)系的元數(shù)據(jù)存儲(chǔ)在 DICOM 文件的頭部中,而該元圖像格式也保留了頭部中的數(shù)據(jù)。這些元數(shù)據(jù)允許我們構(gòu)建從(X,Y,Z)到(I,R,C)的轉(zhuǎn)換,如圖 10.5 所示。原始數(shù)據(jù)包含許多其他類似的元數(shù)據(jù)字段,但由于我們現(xiàn)在不需要使用它們,這些不需要的字段將被忽略。
10.4.2 CT 掃描形狀和體素大小
CT 掃描之間最常見(jiàn)的變化之一是體素的大小;通常它們不是立方體。相反,它們可以是 1.125 毫米×1.125 毫米×2.5 毫米或類似的。通常行和列維度的體素大小相同,而索引維度具有較大的值,但也可以存在其他比例。
當(dāng)使用方形像素繪制時(shí),非立方體體素可能看起來(lái)有些扭曲,類似于使用墨卡托投影地圖時(shí)在北極和南極附近的扭曲。這是一個(gè)不完美的類比,因?yàn)樵谶@種情況下,扭曲是均勻和線性的–在圖 10.8 中,病人看起來(lái)比實(shí)際上更矮胖或胸部更寬。如果我們希望圖像反映真實(shí)比例,我們將需要應(yīng)用一個(gè)縮放因子。
圖 10.8 沿索引軸具有非立方體體素的 CT 掃描。請(qǐng)注意從上到下肺部的壓縮程度。
知道這些細(xì)節(jié)在試圖通過(guò)視覺(jué)解釋我們的結(jié)果時(shí)會(huì)有所幫助。沒(méi)有這些信息,很容易會(huì)認(rèn)為我們的數(shù)據(jù)加載出了問(wèn)題:我們可能會(huì)認(rèn)為數(shù)據(jù)看起來(lái)很矮胖是因?yàn)槲覀儾恍⌒奶^(guò)了一半的切片,或者類似的情況。很容易會(huì)浪費(fèi)很多時(shí)間來(lái)調(diào)試一直正常運(yùn)行的東西,熟悉你的數(shù)據(jù)可以幫助避免這種情況。
CT 通常是 512 行×512 列,索引維度從大約 100 個(gè)切片到可能達(dá)到 250 個(gè)切片(250 個(gè)切片乘以 2.5 毫米通常足以包含感興趣的解剖區(qū)域)。這導(dǎo)致下限約為 225 個(gè)體素,或約 3200 萬(wàn)數(shù)據(jù)點(diǎn)。每個(gè) CT 都會(huì)在文件元數(shù)據(jù)中指定體素大小;例如,在列表 10.10 中我們會(huì)調(diào)用ct_mhd .GetSpacing()
。
10.4.3 毫米和體素地址之間的轉(zhuǎn)換
我們將定義一些實(shí)用代碼來(lái)幫助在病人坐標(biāo)中的毫米和(I,R,C)數(shù)組坐標(biāo)之間進(jìn)行轉(zhuǎn)換(我們將在代碼中用變量和類似的后綴_xyz
表示病人坐標(biāo)中的變量,用_irc
后綴表示(I,R,C)數(shù)組坐標(biāo))。
您可能想知道 SimpleITK
庫(kù)是否帶有實(shí)用函數(shù)來(lái)進(jìn)行轉(zhuǎn)換。確實(shí),Image
實(shí)例具有兩種方法–TransformIndexToPhysicalPoint
和 TransformPhysicalPointToIndex
–可以做到這一點(diǎn)(除了從 CRI [列,行,索引] IRC 進(jìn)行洗牌)。但是,我們希望能夠在不保留 Image
對(duì)象的情況下進(jìn)行此計(jì)算,因此我們將在這里手動(dòng)執(zhí)行數(shù)學(xué)運(yùn)算。
軸翻轉(zhuǎn)(以及可能的旋轉(zhuǎn)或其他變換)被編碼在從ct_mhd.GetDirections()
返回的 3 × 3 矩陣中,以元組形式返回。為了從體素索引轉(zhuǎn)換為坐標(biāo),我們需要按順序執(zhí)行以下四個(gè)步驟:
-
將坐標(biāo)從 IRC 翻轉(zhuǎn)到 CRI,以與 XYZ 對(duì)齊。
-
用體素大小來(lái)縮放指數(shù)。
-
使用 Python 中的
@
矩陣乘以方向矩陣。 -
添加原點(diǎn)的偏移量。
要從 XYZ 轉(zhuǎn)換為 IRC,我們需要按相反順序執(zhí)行每個(gè)步驟的逆操作。
我們將體素大小保留在命名元組中,因此我們將其轉(zhuǎn)換為數(shù)組。
列表 10.9 util.py:16
IrcTuple = collections.namedtuple('IrcTuple', ['index', 'row', 'col'])
XyzTuple = collections.namedtuple('XyzTuple', ['x', 'y', 'z'])def irc2xyz(coord_irc, origin_xyz, vxSize_xyz, direction_a):cri_a = np.array(coord_irc)[::-1] # ?origin_a = np.array(origin_xyz)vxSize_a = np.array(vxSize_xyz)coords_xyz = (direction_a @ (cri_a * vxSize_a)) + origin_a # ?return XyzTuple(*coords_xyz)def xyz2irc(coord_xyz, origin_xyz, vxSize_xyz, direction_a):origin_a = np.array(origin_xyz)vxSize_a = np.array(vxSize_xyz)coord_a = np.array(coord_xyz)cri_a = ((coord_a - origin_a) @ np.linalg.inv(direction_a)) / vxSize_a # ?cri_a = np.round(cri_a) # ?return IrcTuple(int(cri_a[2]), int(cri_a[1]), int(cri_a[0])) # ?
? 在轉(zhuǎn)換為 NumPy 數(shù)組時(shí)交換順序
? 我們計(jì)劃的最后三個(gè)步驟,一行搞定
? 最后三個(gè)步驟的逆操作
? 在轉(zhuǎn)換為整數(shù)之前進(jìn)行適當(dāng)?shù)乃纳嵛迦?/p>
? 洗牌并轉(zhuǎn)換為整數(shù)
哦。如果這有點(diǎn)沉重,不要擔(dān)心。只需記住我們需要將函數(shù)轉(zhuǎn)換并使用為黑匣子。我們需要從患者坐標(biāo)(_xyz
)轉(zhuǎn)換為數(shù)組坐標(biāo)(_irc
)的元數(shù)據(jù)包含在 MetaIO 文件中,與 CT 數(shù)據(jù)本身一起。我們從 .mhd 文件中提取體素大小和定位元數(shù)據(jù)的同時(shí)獲取 ct_a
。
列表 10.10 dsets.py:72, class
Ct
class Ct:def __init__(self, series_uid):mhd_path = glob.glob('data-unversioned/part2/luna/subset*/{}.mhd'.format(series_uid))[0]ct_mhd = sitk.ReadImage(mhd_path)# ... line 91self.origin_xyz = XyzTuple(*ct_mhd.GetOrigin())self.vxSize_xyz = XyzTuple(*ct_mhd.GetSpacing())self.direction_a = np.array(ct_mhd.GetDirection()).reshape(3, 3) # ?
? 將方向轉(zhuǎn)換為數(shù)組,并將九元素?cái)?shù)組重塑為其正確的 3 × 3 矩陣形狀
這些是我們需要傳遞給我們的 xyz2irc
轉(zhuǎn)換函數(shù)的輸入,除了要轉(zhuǎn)換的單個(gè)點(diǎn)。有了這些屬性,我們的 CT 對(duì)象實(shí)現(xiàn)現(xiàn)在具有將候選中心從患者坐標(biāo)轉(zhuǎn)換為數(shù)組坐標(biāo)所需的所有數(shù)據(jù)。
10.4.4 從 CT 掃描中提取結(jié)節(jié)
正如我們?cè)诘诰耪轮刑岬降?#xff0c;對(duì)于肺結(jié)節(jié)患者的 CT 掃描,高達(dá) 99.9999% 的體素不會(huì)是實(shí)際結(jié)節(jié)的一部分(或者癌癥)。再次強(qiáng)調(diào),這個(gè)比例相當(dāng)于高清電視上某處不正確著色的兩個(gè)像素斑點(diǎn),或者一本小說(shuō)書(shū)架上一個(gè)拼寫(xiě)錯(cuò)誤的單詞。強(qiáng)迫我們的模型檢查如此龐大的數(shù)據(jù)范圍,尋找我們希望其關(guān)注的結(jié)節(jié)的線索,將會(huì)像要求您從一堆用您不懂的語(yǔ)言寫(xiě)成的小說(shuō)中找到一個(gè)拼寫(xiě)錯(cuò)誤的單詞一樣有效!3
相反,正如我們?cè)趫D 10.9 中所看到的,我們將提取每個(gè)候選者周圍的區(qū)域,并讓模型一次關(guān)注一個(gè)候選者。這類似于讓您閱讀外語(yǔ)中的單個(gè)段落:仍然不是一項(xiàng)容易的任務(wù),但要少得多!尋找方法來(lái)減少我們模型的問(wèn)題范圍可以幫助,特別是在項(xiàng)目的早期階段,當(dāng)我們?cè)噲D讓我們的第一個(gè)工作實(shí)現(xiàn)運(yùn)行起來(lái)時(shí)。
圖 10.9 通過(guò)使用候選者中心的數(shù)組坐標(biāo)信息(索引,行,列)從較大的 CT 體素?cái)?shù)組中裁剪候選樣本
getRawNodule
函數(shù)接受以患者坐標(biāo)系(X,Y,Z)表示的中心(正如在 LUNA CSV 數(shù)據(jù)中指定的那樣),以及以體素為單位的寬度。它返回一個(gè) CT 的立方塊,以及將候選者中心轉(zhuǎn)換為數(shù)組坐標(biāo)的中心。
列表 10.11 dsets.py:105, Ct.getRawCandidate
def getRawCandidate(self, center_xyz, width_irc):center_irc = xyz2irc(center_xyz,self.origin_xyz,self.vxSize_xyz,self.direction_a,)slice_list = []for axis, center_val in enumerate(center_irc):start_ndx = int(round(center_val - width_irc[axis]/2))end_ndx = int(start_ndx + width_irc[axis])slice_list.append(slice(start_ndx, end_ndx))ct_chunk = self.hu_a[tuple(slice_list)]return ct_chunk, center_irc
實(shí)際實(shí)現(xiàn)將需要處理中心和寬度的組合將裁剪區(qū)域的邊緣放在數(shù)組外部的情況。但正如前面所述,我們將跳過(guò)使函數(shù)的更大意圖變得模糊的復(fù)雜情況。完整的實(shí)現(xiàn)可以在書(shū)的網(wǎng)站上找到(www.manning.com/books/deep-learning-with-pytorch?query=pytorch)以及 GitHub 倉(cāng)庫(kù)中(github.com/deep-learning-with-pytorch/dlwpt-code
)。
10.5 一個(gè)直接的數(shù)據(jù)集實(shí)現(xiàn)
我們?cè)诘谄哒率状慰吹搅?PyTorch 的Dataset
實(shí)例,但這將是我們第一次自己實(shí)現(xiàn)一個(gè)。通過(guò)子類化Dataset
,我們將把我們的任意數(shù)據(jù)插入到 PyTorch 生態(tài)系統(tǒng)的其余部分中。每個(gè)Ct
實(shí)例代表了數(shù)百個(gè)不同的樣本,我們可以用它們來(lái)訓(xùn)練我們的模型或驗(yàn)證其有效性。我們的LunaDataset
類將規(guī)范化這些樣本,將每個(gè) CT 的結(jié)節(jié)壓縮成一個(gè)單一集合,可以從中檢索樣本,而不必考慮樣本來(lái)自哪個(gè)Ct
實(shí)例。這種壓縮通常是我們處理數(shù)據(jù)的方式,盡管正如我們將在第十二章中看到的,有些情況下簡(jiǎn)單的數(shù)據(jù)壓縮不足以很好地訓(xùn)練模型。
在實(shí)現(xiàn)方面,我們將從子類化Dataset
所施加的要求開(kāi)始,并向后工作。這與我們之前使用的數(shù)據(jù)集不同;在那里,我們使用的是外部庫(kù)提供的類,而在這里,我們需要自己實(shí)現(xiàn)和實(shí)例化類。一旦我們這樣做了,我們就可以像之前的例子那樣使用它。幸運(yùn)的是,我們自定義子類的實(shí)現(xiàn)不會(huì)太困難,因?yàn)?PyTorch API 只要求我們想要實(shí)現(xiàn)的任何Dataset
子類必須提供這兩個(gè)函數(shù):
一個(gè)__len__
的實(shí)現(xiàn),在初始化后必須返回一個(gè)單一的常量值(在某些情況下該值會(huì)被緩存)
__getitem__
方法接受一個(gè)索引并返回一個(gè)元組,其中包含用于訓(xùn)練(或驗(yàn)證,視情況而定)的樣本數(shù)據(jù)
首先,讓我們看看這些函數(shù)的函數(shù)簽名和返回值是什么樣的。
列表 10.12 dsets.py:176, LunaDataset.__len__
def __len__(self):return len(self.candidateInfo_list)def __getitem__(self, ndx):# ... line 200return (candidate_t, 1((CO10-1))pos_t, 1((CO10-2))candidateInfo_tup.series_uid, # ?torch.tensor(center_irc), # ?)
這是我們的訓(xùn)練樣本。
我們的__len__
實(shí)現(xiàn)很簡(jiǎn)單:我們有一個(gè)候選列表,每個(gè)候選是一個(gè)樣本,我們的數(shù)據(jù)集大小與我們擁有的樣本數(shù)量一樣大。我們不必使實(shí)現(xiàn)像這里這樣簡(jiǎn)單;在后面的章節(jié)中,我們會(huì)看到這種變化!?唯一的規(guī)則是,如果__len__
返回值為N,那么__getitem__
需要對(duì)所有輸入 0 到 N - 1 返回有效值。
對(duì)于__getitem__
,我們?nèi)?code>ndx(通常是一個(gè)整數(shù),根據(jù)支持輸入 0 到 N - 1 的規(guī)則)并返回如圖 10.2 所示的四項(xiàng)樣本元組。構(gòu)建這個(gè)元組比獲取數(shù)據(jù)集長(zhǎng)度要復(fù)雜一些,因此讓我們來(lái)看看。
這個(gè)方法的第一部分意味著我們需要構(gòu)建self.candidateInfo _list
以及提供getCtRawNodule
函數(shù)。
列表 10.13 dsets.py:179, LunaDataset.__getitem__
def __getitem__(self, ndx):candidateInfo_tup = self.candidateInfo_list[ndx]width_irc = (32, 48, 48)candidate_a, center_irc = getCtRawCandidate( # ?candidateInfo_tup.series_uid,candidateInfo_tup.center_xyz,width_irc,)
返回值 candidate_a 的形狀為 (32,48,48);軸是深度、高度和寬度。
我們將在 10.5.1 和 10.5.2 節(jié)中馬上看到這些。
在__getitem__
方法中,我們需要將數(shù)據(jù)轉(zhuǎn)換為下游代碼所期望的正確數(shù)據(jù)類型和所需的數(shù)組維度。
列表 10.14 dsets.py:189, LunaDataset.__getitem__
candidate_t = torch.from_numpy(candidate_a)
candidate_t = candidate_t.to(torch.float32)
candidate_t = candidate_t.unsqueeze(0) # ?
.unsqueeze(0) 添加了‘Channel’維度。
目前不要太擔(dān)心我們?yōu)槭裁匆倏v維度;下一章將包含最終使用此輸出并施加我們?cè)诖酥鲃?dòng)滿足的約束的代碼。這將是你應(yīng)該期望為每個(gè)自定義Dataset
實(shí)現(xiàn)的內(nèi)容。這些轉(zhuǎn)換是將您的“荒野數(shù)據(jù)”轉(zhuǎn)換為整潔有序張量的關(guān)鍵部分。
最后,我們需要構(gòu)建我們的分類張量。
列表 10.15 dsets.py:193,LunaDataset.__getitem__
pos_t = torch.tensor([not candidateInfo_tup.isNodule_bool,candidateInfo_tup.isNodule_bool],dtype=torch.long,
)
這有兩個(gè)元素,分別用于我們可能的候選類別(結(jié)節(jié)或非結(jié)節(jié);或正面或負(fù)面)。我們可以為結(jié)節(jié)狀態(tài)設(shè)置單個(gè)輸出,但nn.CrossEntropyLoss
期望每個(gè)類別有一個(gè)輸出值,這就是我們?cè)谶@里提供的內(nèi)容。您構(gòu)建的張量的確切細(xì)節(jié)將根據(jù)您正在處理的項(xiàng)目類型而變化。
讓我們看看我們最終的樣本元組(較大的nodule_t
輸出并不特別可讀,所以我們?cè)诹斜碇惺÷粤舜蟛糠謨?nèi)容)。
列表 10.16 p2ch10_explore_data.ipynb
# In[10]:
LunaDataset()[0]# Out[10]:
(tensor([[[[-899., -903., -825., ..., -901., -898., -893.], # ?..., # ?[ -92., -63., 4., ..., 63., 70., 52.]]]]), # ?tensor([0, 1]), # ?'1.3.6...287966244644280690737019247886', # ?tensor([ 91, 360, 341])) # ?
? candidate_t
? cls_t
? candidate_tup.series_uid(省略)
? center_irc
這里我們看到了我們__getitem__
返回語(yǔ)句的四個(gè)項(xiàng)目。
10.5.1 使用getCtRawCandidate
函數(shù)緩存候選數(shù)組
為了使LunaDataset
獲得良好的性能,我們需要投資一些磁盤(pán)緩存。這將使我們避免為每個(gè)樣本從磁盤(pán)中讀取整個(gè) CT 掃描。這樣做將速度非常慢!確保您注意項(xiàng)目中的瓶頸,并在開(kāi)始減慢速度時(shí)盡力優(yōu)化它們。我們有點(diǎn)過(guò)早地進(jìn)行了這一步,因?yàn)槲覀冞€沒(méi)有證明我們?cè)谶@里需要緩存。沒(méi)有緩存,LunaDataset
的速度會(huì)慢 50 倍!我們將在本章的練習(xí)中重新討論這個(gè)問(wèn)題。
函數(shù)本身很簡(jiǎn)單。它是我們之前看到的Ct.getRawCandidate
方法的文件緩存包裝器(pypi.python.org/pypi/ diskcache
)。
列表 10.17 dsets.py:139
@functools.lru_cache(1, typed=True)
def getCt(series_uid):return Ct(series_uid)@raw_cache.memoize(typed=True)
def getCtRawCandidate(series_uid, center_xyz, width_irc):ct = getCt(series_uid)ct_chunk, center_irc = ct.getRawCandidate(center_xyz, width_irc)return ct_chunk, center_irc
我們?cè)谶@里使用了幾種不同的緩存方法。首先,我們將getCt
返回值緩存在內(nèi)存中,這樣我們就可以重復(fù)請(qǐng)求相同的Ct
實(shí)例而不必重新從磁盤(pán)加載所有數(shù)據(jù)。在重復(fù)請(qǐng)求的情況下,這將極大地提高速度,但我們只保留一個(gè) CT 在內(nèi)存中,所以如果我們不注意訪問(wèn)順序,緩存未命中會(huì)頻繁發(fā)生。
調(diào)用getCt
的getCtRawCandidate
函數(shù)也具有其輸出被緩存,因此在我們的緩存被填充后,getCt
將不會(huì)被調(diào)用。這些值使用 Python 庫(kù)diskcache
緩存在磁盤(pán)上。我們將在第十一章討論為什么有這種特定的緩存設(shè)置。目前,知道從磁盤(pán)中讀取 215 個(gè)float32
值要比讀取 225 個(gè)int16
值,轉(zhuǎn)換為float32
,然后選擇 215 個(gè)子集要快得多。從第二次通過(guò)數(shù)據(jù)開(kāi)始,輸入的 I/O 時(shí)間應(yīng)該降至可以忽略的程度。
注意 如果這些函數(shù)的定義發(fā)生實(shí)質(zhì)性變化,我們將需要從磁盤(pán)中刪除緩存的數(shù)值。如果不這樣做,即使現(xiàn)在函數(shù)不再將給定的輸入映射到舊的輸出,緩存仍將繼續(xù)返回它們。數(shù)據(jù)存儲(chǔ)在 data-unversioned/cache 目錄中。
10.5.2 在 LunaDataset.init 中構(gòu)建我們的數(shù)據(jù)集
幾乎每個(gè)項(xiàng)目都需要將樣本分為訓(xùn)練集和驗(yàn)證集。我們將通過(guò)指定的val_stride
參數(shù)將每個(gè)第十個(gè)樣本指定為驗(yàn)證集的成員來(lái)實(shí)現(xiàn)這一點(diǎn)。我們還將接受一個(gè)isValSet_bool
參數(shù),并使用它來(lái)確定我們應(yīng)該保留僅訓(xùn)練數(shù)據(jù)、驗(yàn)證數(shù)據(jù)還是所有數(shù)據(jù)。
列表 10.18 dsets.py:149,class
LunaDataset
class LunaDataset(Dataset):def __init__(self,val_stride=0,isValSet_bool=None,series_uid=None,):self.candidateInfo_list = copy.copy(getCandidateInfoList()) # ?if series_uid:self.candidateInfo_list = [x for x in self.candidateInfo_list if x.series_uid == series_uid]
? 復(fù)制返回值,以便通過(guò)更改 self.candidateInfo_list 不會(huì)影響緩存副本
如果我們傳入一個(gè)真值series_uid
,那么實(shí)例將只包含該系列的結(jié)節(jié)。這對(duì)于可視化或調(diào)試非常有用,因?yàn)檫@樣可以更容易地查看單個(gè)有問(wèn)題的 CT 掃描。
10.5.3 訓(xùn)練/驗(yàn)證分割
我們?cè)试SDataset
將數(shù)據(jù)的 1/N部分分割成一個(gè)用于驗(yàn)證模型的子集。我們將如何處理該子集取決于isValSet _bool
參數(shù)的值。
列表 10.19 dsets.py:162, LunaDataset.__init__
if isValSet_bool:assert val_stride > 0, val_strideself.candidateInfo_list = self.candidateInfo_list[::val_stride]assert self.candidateInfo_list
elif val_stride > 0:del self.candidateInfo_list[::val_stride] # ?assert self.candidateInfo_list
? 從self.candidateInfo_list
中刪除驗(yàn)證圖像(列表中每個(gè)val_stride
個(gè)項(xiàng)目)。我們之前復(fù)制了一份,以便不改變?cè)剂斜怼?/p>
這意味著我們可以創(chuàng)建兩個(gè)Dataset
實(shí)例,并確信我們的訓(xùn)練數(shù)據(jù)和驗(yàn)證數(shù)據(jù)之間有嚴(yán)格的分離。當(dāng)然,這取決于self.candidateInfo_list
具有一致的排序順序,我們通過(guò)確保候選信息元組有一個(gè)穩(wěn)定的排序順序,并且getCandidateInfoList
函數(shù)在返回列表之前對(duì)列表進(jìn)行排序來(lái)實(shí)現(xiàn)這一點(diǎn)。
關(guān)于訓(xùn)練和驗(yàn)證數(shù)據(jù)的另一個(gè)注意事項(xiàng)是,根據(jù)手頭的任務(wù),我們可能需要確保來(lái)自單個(gè)患者的數(shù)據(jù)只出現(xiàn)在訓(xùn)練或測(cè)試中,而不是同時(shí)出現(xiàn)在兩者中。在這里這不是問(wèn)題;否則,我們需要在到達(dá)結(jié)節(jié)級(jí)別之前拆分患者和 CT 掃描列表。
讓我們使用p2ch10_explore_data.ipynb
來(lái)查看數(shù)據(jù):
# In[2]:
from p2ch10.dsets import getCandidateInfoList, getCt, LunaDataset
candidateInfo_list = getCandidateInfoList(requireOnDisk_bool=False)
positiveInfo_list = [x for x in candidateInfo_list if x[0]]
diameter_list = [x[1] for x in positiveInfo_list]# In[4]:
for i in range(0, len(diameter_list), 100):print('{:4} {:4.1f} mm'.format(i, diameter_list[i]))# Out[4]:0 32.3 mm100 17.7 mm200 13.0 mm300 10.0 mm400 8.2 mm500 7.0 mm600 6.3 mm700 5.7 mm800 5.1 mm900 4.7 mm
1000 4.0 mm
1100 0.0 mm
1200 0.0 mm
1300 0.0 mm
我們有一些非常大的候選項(xiàng),從 32 毫米開(kāi)始,但它們迅速減半。大部分候選項(xiàng)在 4 到 10 毫米的范圍內(nèi),而且有幾百個(gè)根本沒(méi)有尺寸信息。這看起來(lái)正常;您可能還記得我們實(shí)際結(jié)節(jié)比直徑注釋多的情況。對(duì)數(shù)據(jù)進(jìn)行快速的健全性檢查非常有幫助;及早發(fā)現(xiàn)問(wèn)題或錯(cuò)誤的假設(shè)可能節(jié)省數(shù)小時(shí)的工作!
更重要的是,我們的訓(xùn)練和驗(yàn)證集應(yīng)該具有一些屬性,以便良好地工作:
兩個(gè)集合都該包含所有預(yù)期輸入變化的示例。
任何一個(gè)集合都不應(yīng)該包含不代表預(yù)期輸入的樣本,除非它們有一個(gè)特定的目的,比如訓(xùn)練模型以對(duì)異常值具有魯棒性。
訓(xùn)練集不應(yīng)該提供關(guān)于驗(yàn)證集的不真實(shí)的提示,這些提示在真實(shí)世界的數(shù)據(jù)中不成立(例如,在兩個(gè)集合中包含相同的樣本;這被稱為訓(xùn)練集中的泄漏)。
10.5.4 渲染數(shù)據(jù)
再次,要么直接使用p2ch10_explore_data.ipynb
,要么啟動(dòng) Jupyter Notebook 并輸入
# In[7]:
%matplotlib inline # ?
from p2ch10.vis import findNoduleSamples, showNodule
noduleSample_list = findNoduleSamples()
? 這個(gè)神奇的行設(shè)置了通過(guò)筆記本內(nèi)聯(lián)顯示圖像的能力。
提示 有關(guān) Jupyter 的 matplotlib 內(nèi)聯(lián)魔術(shù)的更多信息,請(qǐng)參閱mng.bz/rrmD
。
# In[8]:
series_uid = positiveSample_list[11][2]
showCandidate(series_uid)
這產(chǎn)生了類似于本章前面顯示的 CT 和結(jié)節(jié)切片的圖像。
如果您感興趣,我們邀請(qǐng)您編輯p2ch10/vis.py
中渲染代碼的實(shí)現(xiàn),以滿足您的需求和口味。渲染代碼大量使用 Matplotlib (matplotlib.org
),這是一個(gè)對(duì)我們來(lái)說(shuō)太復(fù)雜的庫(kù),我們無(wú)法在這里覆蓋。
記住,渲染數(shù)據(jù)不僅僅是為了獲得漂亮的圖片。重點(diǎn)是直觀地了解您的輸入是什么樣子的。一眼就能看出“這個(gè)有問(wèn)題的樣本與我的其他數(shù)據(jù)相比非常嘈雜”或“奇怪的是,這看起來(lái)非常正常”可能在調(diào)查問(wèn)題時(shí)很有用。有效的渲染還有助于培養(yǎng)洞察力,比如“也許如果我修改這樣的東西,我就能解決我遇到的問(wèn)題?!彪S著您開(kāi)始處理越來(lái)越困難的項(xiàng)目,這種熟悉程度將是必不可少的。
注意由于每個(gè)子集的劃分方式,以及在構(gòu)建LunaDataset.candidateInfo_list
時(shí)使用的排序方式,noduleSample_list
中條目的排序高度依賴于代碼執(zhí)行時(shí)存在的子集。請(qǐng)記住這一點(diǎn),尤其是在解壓更多子集后嘗試第二次找到特定樣本時(shí)。
10.6 結(jié)論
在第九章中,我們已經(jīng)對(duì)我們的數(shù)據(jù)有了深入的了解。在這一章中,我們讓PyTorch對(duì)我們的數(shù)據(jù)有了深入的了解!通過(guò)將我們的 DICOM-via-meta-image 原始數(shù)據(jù)轉(zhuǎn)換為張量,我們已經(jīng)為開(kāi)始實(shí)現(xiàn)模型和訓(xùn)練循環(huán)做好了準(zhǔn)備,這將在下一章中看到。
不要低估我們已經(jīng)做出的設(shè)計(jì)決策的影響:我們的輸入大小、緩存結(jié)構(gòu)以及如何劃分訓(xùn)練和驗(yàn)證集都會(huì)對(duì)整個(gè)項(xiàng)目的成功或失敗產(chǎn)生影響。不要猶豫在以后重新審視這些決策,特別是當(dāng)你在自己的項(xiàng)目上工作時(shí)。
10.7 練習(xí)
-
實(shí)現(xiàn)一個(gè)程序,遍歷
LunaDataset
實(shí)例,并計(jì)算完成此操作所需的時(shí)間。為了節(jié)省時(shí)間,可能有意義的是有一個(gè)選項(xiàng)將迭代限制在前N=1000
個(gè)樣本。-
第一次運(yùn)行需要多長(zhǎng)時(shí)間?
-
第二次運(yùn)行需要多長(zhǎng)時(shí)間?
-
清除緩存對(duì)運(yùn)行時(shí)間有什么影響?
-
使用最后的
N=1000
個(gè)樣本對(duì)第一/第二次運(yùn)行有什么影響?
-
-
將
LunaDataset
的實(shí)現(xiàn)更改為在__init__
期間對(duì)樣本列表進(jìn)行隨機(jī)化。清除緩存,并運(yùn)行修改后的版本。這對(duì)第一次和第二次運(yùn)行的運(yùn)行時(shí)間有什么影響? -
恢復(fù)隨機(jī)化,并將
@functools.lru_cache(1, typed=True)
裝飾器注釋掉getCt
。清除緩存,并運(yùn)行修改后的版本?,F(xiàn)在運(yùn)行時(shí)間如何變化?
摘要
-
通常,解析和加載原始數(shù)據(jù)所需的代碼并不簡(jiǎn)單。對(duì)于這個(gè)項(xiàng)目,我們實(shí)現(xiàn)了一個(gè)
Ct
類,它從磁盤(pán)加載數(shù)據(jù)并提供對(duì)感興趣點(diǎn)周圍裁剪區(qū)域的訪問(wèn)。 -
如果解析和加載例程很昂貴,緩存可能會(huì)很有用。請(qǐng)記住,一些緩存可以在內(nèi)存中完成,而一些最好在磁盤(pán)上執(zhí)行。每種緩存方式都有其在數(shù)據(jù)加載管道中的位置。
-
PyTorch 的
Dataset
子類用于將數(shù)據(jù)從其原生形式轉(zhuǎn)換為適合傳遞給模型的張量。我們可以使用這個(gè)功能將我們的真實(shí)世界數(shù)據(jù)與 PyTorch API 集成。 -
Dataset
的子類需要為兩個(gè)方法提供實(shí)現(xiàn):__len__
和__getitem__
。其他輔助方法是允許的,但不是必需的。 -
將我們的數(shù)據(jù)分成合理的訓(xùn)練集和驗(yàn)證集需要確保沒(méi)有樣本同時(shí)出現(xiàn)在兩個(gè)集合中。我們通過(guò)使用一致的排序順序,并為驗(yàn)證集取每第十個(gè)樣本來(lái)實(shí)現(xiàn)這一點(diǎn)。
-
數(shù)據(jù)可視化很重要;能夠通過(guò)視覺(jué)調(diào)查數(shù)據(jù)可以提供有關(guān)錯(cuò)誤或問(wèn)題的重要線索。我們正在使用 Jupyter Notebooks 和 Matplotlib 來(lái)呈現(xiàn)我們的數(shù)據(jù)。
1 對(duì)于那些事先準(zhǔn)備好所有數(shù)據(jù)的稀有研究人員:你真幸運(yùn)!我們其他人將忙于編寫(xiě)加載和解析代碼。
2 有例外情況,但現(xiàn)在并不相關(guān)。
3 你在這本書(shū)中找到拼寫(xiě)錯(cuò)誤了嗎? 😉
? 實(shí)際上更簡(jiǎn)單一些;但重點(diǎn)是,我們有選擇。
? 他們的術(shù)語(yǔ),不是我們的!
十一、訓(xùn)練一個(gè)分類模型以檢測(cè)可疑腫瘤
本章涵蓋
-
使用 PyTorch 的
DataLoader
加載數(shù)據(jù) -
實(shí)現(xiàn)一個(gè)在我們的 CT 數(shù)據(jù)上執(zhí)行分類的模型
-
設(shè)置我們應(yīng)用程序的基本框架
-
記錄和顯示指標(biāo)
在前幾章中,我們?yōu)槲覀兊陌┌Y檢測(cè)項(xiàng)目做好了準(zhǔn)備。我們涵蓋了肺癌的醫(yī)學(xué)細(xì)節(jié),查看了我們項(xiàng)目將使用的主要數(shù)據(jù)來(lái)源,并將原始 CT 掃描轉(zhuǎn)換為 PyTorch Dataset
實(shí)例。現(xiàn)在我們有了數(shù)據(jù)集,我們可以輕松地使用我們的訓(xùn)練數(shù)據(jù)。所以讓我們開(kāi)始吧!
11.1 一個(gè)基礎(chǔ)模型和訓(xùn)練循環(huán)
在本章中,我們將做兩件主要的事情。我們將首先構(gòu)建結(jié)節(jié)分類模型和訓(xùn)練循環(huán),這將是第 2 部分探索更大項(xiàng)目的基礎(chǔ)。為此,我們將使用我們?cè)诘谑聦?shí)現(xiàn)的Ct
和LunaDataset
類來(lái)提供DataLoader
實(shí)例。這些實(shí)例將通過(guò)訓(xùn)練和驗(yàn)證循環(huán)向我們的分類模型提供數(shù)據(jù)。
我們將通過(guò)運(yùn)行訓(xùn)練循環(huán)的結(jié)果來(lái)結(jié)束本章,引入本書(shū)這一部分中最困難的挑戰(zhàn)之一:如何從混亂、有限的數(shù)據(jù)中獲得高質(zhì)量的結(jié)果。在后續(xù)章節(jié)中,我們將探討我們的數(shù)據(jù)受限的具體方式,并減輕這些限制。
讓我們回顧一下第九章的高層路線圖,如圖 11.1 所示?,F(xiàn)在,我們將致力于生成一個(gè)能夠執(zhí)行第 4 步分類的模型。作為提醒,我們將候選者分類為結(jié)節(jié)或非結(jié)節(jié)(我們將在第十四章構(gòu)建另一個(gè)分類器,試圖區(qū)分惡性結(jié)節(jié)和良性結(jié)節(jié))。這意味著我們將為呈現(xiàn)給模型的每個(gè)樣本分配一個(gè)單一特定的標(biāo)簽。在這種情況下,這些標(biāo)簽是“結(jié)節(jié)”和“非結(jié)節(jié)”,因?yàn)槊總€(gè)樣本代表一個(gè)候選者。
圖 11.1 我們的端到端項(xiàng)目,用于檢測(cè)肺癌,重點(diǎn)是本章的主題:第 4 步,分類
獲得項(xiàng)目中一個(gè)有意義部分的早期端到端版本是一個(gè)重要的里程碑。擁有一個(gè)足夠好使得結(jié)果可以進(jìn)行分析評(píng)估的東西,讓你可以有信心進(jìn)行未來(lái)的改變,確信你正在通過(guò)每一次改變來(lái)改進(jìn)你的結(jié)果,或者至少你能夠擱置任何不起作用的改變和實(shí)驗(yàn)!在自己的項(xiàng)目中進(jìn)行大量的實(shí)驗(yàn)是必須的。獲得最佳結(jié)果通常需要進(jìn)行大量的調(diào)試和微調(diào)。
但在我們進(jìn)入實(shí)驗(yàn)階段之前,我們必須打下基礎(chǔ)。讓我們看看我們第 2 部分訓(xùn)練循環(huán)的樣子,如圖 11.2 所示:鑒于我們?cè)诘谖逭驴吹搅艘唤M類似的核心步驟,這應(yīng)該會(huì)讓人感到熟悉。在這里,我們還將使用驗(yàn)證集來(lái)評(píng)估我們的訓(xùn)練進(jìn)展,如第 5.5.3 節(jié)所討論的那樣。
圖 11.2 我們將在本章實(shí)現(xiàn)的訓(xùn)練和驗(yàn)證腳本
我們將要實(shí)現(xiàn)的基本結(jié)構(gòu)如下:
-
初始化我們的模型和數(shù)據(jù)加載。
-
循環(huán)遍歷一個(gè)半隨機(jī)選擇的 epoch 數(shù)。
-
循環(huán)遍歷
LunaDataset
返回的每個(gè)訓(xùn)練數(shù)據(jù)批次。 -
數(shù)據(jù)加載器工作進(jìn)程在后臺(tái)加載相關(guān)批次的數(shù)據(jù)。
-
將批次傳入我們的分類模型以獲得結(jié)果。
-
根據(jù)我們預(yù)測(cè)結(jié)果與地面真實(shí)數(shù)據(jù)之間的差異來(lái)計(jì)算我們的損失。
-
記錄關(guān)于我們模型性能的指標(biāo)到一個(gè)臨時(shí)數(shù)據(jù)結(jié)構(gòu)中。
-
通過(guò)誤差的反向傳播更新模型權(quán)重。
-
循環(huán)遍歷每個(gè)驗(yàn)證數(shù)據(jù)批次(與訓(xùn)練循環(huán)非常相似的方式)。
-
加載相關(guān)的驗(yàn)證數(shù)據(jù)批次(同樣,在后臺(tái)工作進(jìn)程中)。
-
對(duì)批次進(jìn)行分類,并計(jì)算損失。
-
記錄模型在驗(yàn)證數(shù)據(jù)上的表現(xiàn)信息。
-
打印出本輪的進(jìn)展和性能信息。
-
當(dāng)我們閱讀本章的代碼時(shí),請(qǐng)注意我們正在生成的代碼與第一部分中用于訓(xùn)練循環(huán)的代碼之間的兩個(gè)主要區(qū)別。首先,我們將在程序周圍放置更多結(jié)構(gòu),因?yàn)檎麄€(gè)項(xiàng)目比我們?cè)谠缙谡鹿?jié)中做的要復(fù)雜得多。沒(méi)有額外的結(jié)構(gòu),代碼很快就會(huì)變得混亂。對(duì)于這個(gè)項(xiàng)目,我們將使我們的主要訓(xùn)練應(yīng)用程序使用許多良好封裝的函數(shù),并進(jìn)一步將像數(shù)據(jù)集這樣的代碼分離為獨(dú)立的 Python 模塊。
確保對(duì)于您自己的項(xiàng)目,您將結(jié)構(gòu)和設(shè)計(jì)水平與項(xiàng)目的復(fù)雜性水平匹配。結(jié)構(gòu)太少,將難以進(jìn)行實(shí)驗(yàn)、排除問(wèn)題,甚至描述您正在做的事情!相反,結(jié)構(gòu)太多意味著您正在浪費(fèi)時(shí)間編寫(xiě)您不需要的基礎(chǔ)設(shè)施,并且在所有管道都就位后,您可能會(huì)因?yàn)椴坏貌蛔袷厮鴾p慢自己的速度。此外,花時(shí)間在基礎(chǔ)設(shè)施上很容易成為一種拖延策略,而不是投入艱苦工作來(lái)實(shí)際推進(jìn)項(xiàng)目。不要陷入這種陷阱!
本章代碼與第一部分的另一個(gè)重大區(qū)別將是專注于收集有關(guān)訓(xùn)練進(jìn)展的各種指標(biāo)。如果沒(méi)有良好的指標(biāo)記錄,準(zhǔn)確確定變化對(duì)訓(xùn)練的影響是不可能的。在不透露下一章內(nèi)容的情況下,我們還將看到收集不僅僅是指標(biāo),而是適合工作的正確指標(biāo)是多么重要。我們將在本章中建立跟蹤這些指標(biāo)的基礎(chǔ)設(shè)施,并通過(guò)收集和顯示損失和正確分類的樣本百分比來(lái)運(yùn)用該基礎(chǔ)設(shè)施,無(wú)論是總體還是每個(gè)類別。這足以讓我們開(kāi)始,但我們將在第十二章中涵蓋一組更現(xiàn)實(shí)的指標(biāo)。
11.2 我們應(yīng)用程序的主要入口點(diǎn)
本書(shū)中與之前訓(xùn)練工作的一個(gè)重大結(jié)構(gòu)性差異是,第二部分將我們的工作封裝在一個(gè)完整的命令行應(yīng)用程序中。它將解析命令行參數(shù),具有完整功能的 --help
命令,并且可以在各種環(huán)境中輕松運(yùn)行。所有這些都將使我們能夠輕松地從 Jupyter 和 Bash shell 中調(diào)用訓(xùn)練例程。1
我們的應(yīng)用功能將通過(guò)一個(gè)類來(lái)實(shí)現(xiàn),以便我們可以實(shí)例化應(yīng)用程序并在需要時(shí)傳遞它。這可以使測(cè)試、調(diào)試或從其他 Python 程序調(diào)用更容易。我們可以調(diào)用應(yīng)用程序而無(wú)需啟動(dòng)第二個(gè) OS 級(jí)別的進(jìn)程(在本書(shū)中我們不會(huì)進(jìn)行顯式單元測(cè)試,但我們創(chuàng)建的結(jié)構(gòu)對(duì)于需要進(jìn)行這種測(cè)試的真實(shí)項(xiàng)目可能會(huì)有所幫助)。
利用能夠通過(guò)函數(shù)調(diào)用或 OS 級(jí)別進(jìn)程調(diào)用我們的訓(xùn)練的方式之一是將函數(shù)調(diào)用封裝到 Jupyter Notebook 中,以便代碼可以輕松地從本機(jī) CLI 或?yàn)g覽器中調(diào)用。
代碼清單 11.1 code/p2_run_everything.ipynb
# In[2]:w
def run(app, *argv):argv = list(argv)argv.insert(0, '--num-workers=4') # ?log.info("Running: {}({!r}).main()".format(app, argv))app_cls = importstr(*app.rsplit('.', 1)) # ?app_cls(argv).main()log.info("Finished: {}.{!r}).main()".format(app, argv))# In[6]:
run('p2ch11.training.LunaTrainingApp', '--epochs=1')
? 我們假設(shè)您有一臺(tái)四核八線程 CPU。如有需要,請(qǐng)更改 4。
? 這是一個(gè)稍微更干凈的 import 調(diào)用。
注意 這里的訓(xùn)練假設(shè)您使用的是一臺(tái)四核八線程 CPU、16 GB RAM 和一塊具有 8 GB RAM 的 GPU 的工作站。如果您的 GPU RAM 較少,請(qǐng)減小 --batch-size
,如果 CPU 核心較少或 CPU RAM 較少,請(qǐng)減小 --num-workers
。
讓我們先把一些半標(biāo)準(zhǔn)的樣板代碼搞定。我們將從文件末尾開(kāi)始,使用一個(gè)相當(dāng)標(biāo)準(zhǔn)的 if main
語(yǔ)句塊,實(shí)例化應(yīng)用對(duì)象并調(diào)用 main
方法。
代碼清單 11.2 training.py:386
if __name__ == '__main__':LunaTrainingApp().main()
從那里,我們可以跳回文件頂部,查看應(yīng)用程序類和我們剛剛調(diào)用的兩個(gè)函數(shù),__init__
和main
。我們希望能夠接受命令行參數(shù),因此我們將在應(yīng)用程序的__init__
函數(shù)中使用標(biāo)準(zhǔn)的argparse
庫(kù)(docs.python.org/3/library/argparse.html
)。請(qǐng)注意,如果需要,我們可以向初始化程序傳遞自定義參數(shù)。main
方法將是應(yīng)用程序核心邏輯的主要入口點(diǎn)。
列表 11.3 training.py:31,class
LunaTrainingApp
class LunaTrainingApp:def __init__(self, sys_argv=None):if sys_argv is None: # ?sys_argv = sys.argv[1:]parser = argparse.ArgumentParser()parser.add_argument('--num-workers',help='Number of worker processes for background data loading',default=8,type=int,)# ... line 63self.cli_args = parser.parse_args(sys_argv)self.time_str = datetime.datetime.now().strftime('%Y-%m-%d_%H.%M.%S') # ?# ... line 137def main(self):log.info("Starting {}, {}".format(type(self).__name__, self.cli_args))
? 如果調(diào)用者沒(méi)有提供參數(shù),我們會(huì)從命令行獲取參數(shù)。
? 我們將使用時(shí)間戳來(lái)幫助識(shí)別訓(xùn)練運(yùn)行。
這種結(jié)構(gòu)非常通用,可以在未來(lái)的項(xiàng)目中重復(fù)使用。特別是在__init__
中解析參數(shù)允許我們將應(yīng)用程序的配置與調(diào)用分開(kāi)。
如果您在本書(shū)網(wǎng)站或 GitHub 上檢查本章的代碼,您可能會(huì)注意到一些額外的提到TensorBoard
的行?,F(xiàn)在請(qǐng)忽略這些;我們將在本章后面的第 11.9 節(jié)中詳細(xì)討論它們。
11.3 預(yù)訓(xùn)練設(shè)置和初始化
在我們開(kāi)始迭代每個(gè) epoch 中的每個(gè)批次之前,需要進(jìn)行一些初始化工作。畢竟,如果我們還沒(méi)有實(shí)例化模型,我們就無(wú)法訓(xùn)練模型!正如我們?cè)趫D 11.3 中所看到的,我們需要做兩件主要的事情。第一,正如我們剛才提到的,是初始化我們的模型和優(yōu)化器;第二是初始化我們的Dataset
和DataLoader
實(shí)例。LunaDataset
將定義組成我們訓(xùn)練 epoch 的隨機(jī)樣本集,而我們的DataLoader
實(shí)例將負(fù)責(zé)從我們的數(shù)據(jù)集中加載數(shù)據(jù)并將其提供給我們的應(yīng)用程序。
圖 11.3 我們將在本章實(shí)現(xiàn)的訓(xùn)練和驗(yàn)證腳本,重點(diǎn)放在預(yù)循環(huán)變量初始化上
11.3.1 初始化模型和優(yōu)化器
對(duì)于這一部分,我們將LunaModel
的細(xì)節(jié)視為黑匣子。在第 11.4 節(jié)中,我們將詳細(xì)介紹內(nèi)部工作原理。您可以探索對(duì)實(shí)現(xiàn)進(jìn)行更改,以更好地滿足我們對(duì)模型的目標(biāo),盡管最好是在至少完成第十二章之后再進(jìn)行。
讓我們看看我們的起點(diǎn)是什么樣的。
列表 11.4 training.py:31,class
LunaTrainingApp
class LunaTrainingApp:def __init__(self, sys_argv=None):# ... line 70self.use_cuda = torch.cuda.is_available()self.device = torch.device("cuda" if self.use_cuda else "cpu")self.model = self.initModel()self.optimizer = self.initOptimizer()def initModel(self):model = LunaModel()if self.use_cuda:log.info("Using CUDA; {} devices.".format(torch.cuda.device_count()))if torch.cuda.device_count() > 1: # ?model = nn.DataParallel(model) # ?model = model.to(self.device) # ?return modeldef initOptimizer(self):return SGD(self.model.parameters(), lr=0.001, momentum=0.99)
? 檢測(cè)多個(gè) GPU
? 包裝模型
? 將模型參數(shù)發(fā)送到 GPU。
如果用于訓(xùn)練的系統(tǒng)有多個(gè) GPU,我們將使用nn.DataParallel
類在系統(tǒng)中的所有 GPU 之間分發(fā)工作,然后收集和重新同步參數(shù)更新等。就模型實(shí)現(xiàn)和使用該模型的代碼而言,這幾乎是完全透明的。
DataParallel vs. DistributedDataParallel
在本書(shū)中,我們使用DataParallel
來(lái)處理利用多個(gè) GPU。我們選擇DataParallel
,因?yàn)樗俏覀儸F(xiàn)有模型的簡(jiǎn)單插入包裝器。然而,它并不是使用多個(gè) GPU 的性能最佳解決方案,并且它僅限于與單臺(tái)機(jī)器上可用的硬件一起使用。
PyTorch 還提供DistributedDataParallel
,這是在需要在多個(gè) GPU 或機(jī)器之間分配工作時(shí)推薦使用的包裝類。由于正確的設(shè)置和配置并不簡(jiǎn)單,而且我們懷疑絕大多數(shù)讀者不會(huì)從復(fù)雜性中獲益,因此我們不會(huì)在本書(shū)中涵蓋DistributedDataParallel
。如果您希望了解更多,請(qǐng)閱讀官方文檔:pytorch.org/tutorials/intermediate/ddp_tutorial.html
。
假設(shè)self.use_cuda
為真,則調(diào)用self.model.to(device)
將模型參數(shù)移至 GPU,設(shè)置各種卷積和其他計(jì)算以使用 GPU 進(jìn)行繁重的數(shù)值計(jì)算。在構(gòu)建優(yōu)化器之前這樣做很重要,否則優(yōu)化器將只查看基于 CPU 的參數(shù)對(duì)象,而不是復(fù)制到 GPU 的參數(shù)對(duì)象。
對(duì)于我們的優(yōu)化器,我們將使用基本的隨機(jī)梯度下降(SGD;pytorch.org/docs/stable/optim.html#torch.optim.SGD
)與動(dòng)量。我們?cè)诘谖逭轮惺状慰吹搅诉@個(gè)優(yōu)化器?;叵氲?1 部分,PyTorch 中提供了許多不同的優(yōu)化器;雖然我們不會(huì)詳細(xì)介紹大部分優(yōu)化器,但官方文檔(pytorch.org/docs/stable/optim.html#algorithms
)很好地鏈接到相關(guān)論文。
當(dāng)選擇優(yōu)化器時(shí),使用 SGD 通常被認(rèn)為是一個(gè)安全的起點(diǎn);有一些問(wèn)題可能不適合 SGD,但它們相對(duì)較少。同樣,學(xué)習(xí)率為 0.001,動(dòng)量為 0.9 是相當(dāng)安全的選擇。從經(jīng)驗(yàn)上看,SGD 與這些值一起在各種項(xiàng)目中表現(xiàn)得相當(dāng)不錯(cuò),如果一開(kāi)始效果不佳,可以嘗試學(xué)習(xí)率為 0.01 或 0.0001。
這并不意味著這些值中的任何一個(gè)對(duì)我們的用例是最佳的,但試圖找到更好的值是在超前。系統(tǒng)地嘗試不同的學(xué)習(xí)率、動(dòng)量、網(wǎng)絡(luò)大小和其他類似配置設(shè)置的值被稱為超參數(shù)搜索。在接下來(lái)的章節(jié)中,我們需要先解決其他更為突出的問(wèn)題。一旦我們解決了這些問(wèn)題,我們就可以開(kāi)始微調(diào)這些值。正如我們?cè)诘谖逭碌摹皽y(cè)試其他優(yōu)化器”部分中提到的,我們還可以選擇其他更為奇特的優(yōu)化器;但除了可能將torch.optim.SGD
替換為torch.optim.Adam
之外,理解所涉及的權(quán)衡是本書(shū)所討論的范圍之外的一個(gè)過(guò)于高級(jí)的主題。
11.3.2 數(shù)據(jù)加載器的照料和喂養(yǎng)
我們?cè)谏弦徽轮袠?gòu)建的LunaDataset
類充當(dāng)著我們擁有的任何“荒野數(shù)據(jù)”與 PyTorch 構(gòu)建模塊期望的更加結(jié)構(gòu)化的張量世界之間的橋梁。例如,torch.nn.Conv3d
( pytorch.org/docs/stable/nn.html#conv3d
) 期望五維輸入:(N, C, D, H, W):樣本數(shù)量,每個(gè)樣本的通道數(shù),深度,高度和寬度。這與我們的 CT 提供的本機(jī) 3D 非常不同!
您可能還記得上一章中LunaDataset.__getitem__
中的ct_t.unsqueeze(0)
調(diào)用;它提供了第四維,即我們數(shù)據(jù)的“通道”。回想一下第四章,RGB 圖像有三個(gè)通道,分別用于紅色、綠色和藍(lán)色。天文數(shù)據(jù)可能有幾十個(gè)通道,每個(gè)通道代表電磁波譜的各個(gè)切片–伽馬射線、X 射線、紫外線、可見(jiàn)光、紅外線、微波和/或無(wú)線電波。由于 CT 掃描是單一強(qiáng)度的,我們的通道維度只有大小 1。
還要回顧第 1 部分,一次訓(xùn)練單個(gè)樣本通常是對(duì)計(jì)算資源的低效利用,因?yàn)榇蠖鄶?shù)處理平臺(tái)能夠進(jìn)行更多的并行計(jì)算,而模型處理單個(gè)訓(xùn)練或驗(yàn)證樣本所需的計(jì)算量要少。解決方案是將樣本元組組合成批元組,如圖 11.4 所示,允許同時(shí)處理多個(gè)樣本。第五維度(N)區(qū)分了同一批中的多個(gè)樣本。
圖 11.4 將樣本元組整合到數(shù)據(jù)加載器中的單個(gè)批元組中
方便的是,我們不必實(shí)現(xiàn)任何批處理:PyTorch 的DataLoader
類將處理所有的整理工作。我們已經(jīng)通過(guò)LunaDataset
類將 CT 掃描轉(zhuǎn)換為 PyTorch 張量,所以唯一剩下的就是將我們的數(shù)據(jù)集插入數(shù)據(jù)加載器中。
列表 11.5 training.py:89,LunaTrainingApp.initTrainDl
def initTrainDl(self):train_ds = LunaDataset( # ?val_stride=10,isValSet_bool=False,)batch_size = self.cli_args.batch_sizeif self.use_cuda:batch_size *= torch.cuda.device_count()train_dl = DataLoader( # ?train_ds,batch_size=batch_size, # ?num_workers=self.cli_args.num_workers,pin_memory=self.use_cuda, # ?)return train_dl# ... line 137
def main(self):train_dl = self.initTrainDl()val_dl = self.initValDl() # ?
? 我們的自定義數(shù)據(jù)集
? 一個(gè)現(xiàn)成的類
? 批處理是自動(dòng)完成的。
? 固定內(nèi)存?zhèn)鬏數(shù)?GPU 快速。
? 驗(yàn)證數(shù)據(jù)加載器與訓(xùn)練非常相似。
除了對(duì)單個(gè)樣本進(jìn)行分批處理外,數(shù)據(jù)加載器還可以通過(guò)使用單獨(dú)的進(jìn)程和共享內(nèi)存提供數(shù)據(jù)的并行加載。我們只需在實(shí)例化數(shù)據(jù)加載器時(shí)指定num_workers=...
,其余工作都在幕后處理。每個(gè)工作進(jìn)程生成完整的批次,如圖 11.4 所示。這有助于確保饑餓的 GPU 得到充分的數(shù)據(jù)供應(yīng)。我們的validation_ds
和validation_dl
實(shí)例看起來(lái)很相似,除了明顯的isValSet_bool=True
。
當(dāng)我們迭代時(shí),比如for batch_tup in self.train_dl:
,我們不必等待每個(gè)Ct
被加載、樣本被取出和分批處理等。相反,我們將立即獲得已加載的batch_tup
,并且后臺(tái)的工作進(jìn)程將被釋放以開(kāi)始加載另一個(gè)批次,以便在以后的迭代中使用。使用 PyTorch 的數(shù)據(jù)加載功能可以加快大多數(shù)項(xiàng)目的速度,因?yàn)槲覀兛梢詫?shù)據(jù)加載和處理與 GPU 計(jì)算重疊。
11.4 我們的第一次神經(jīng)網(wǎng)絡(luò)設(shè)計(jì)
能夠檢測(cè)腫瘤的卷積神經(jīng)網(wǎng)絡(luò)的設(shè)計(jì)空間實(shí)際上是無(wú)限的。幸運(yùn)的是,在過(guò)去的十年左右,已經(jīng)付出了相當(dāng)大的努力來(lái)研究有效的圖像識(shí)別模型。雖然這些模型主要集中在 2D 圖像上,但一般的架構(gòu)思想也很適用于 3D,因此有許多經(jīng)過(guò)測(cè)試的設(shè)計(jì)可以作為起點(diǎn)。這有助于我們,因?yàn)楸M管我們的第一個(gè)網(wǎng)絡(luò)架構(gòu)不太可能是最佳選擇,但現(xiàn)在我們只是追求“足夠好以讓我們開(kāi)始”。
我們將基于第八章中使用的內(nèi)容設(shè)計(jì)網(wǎng)絡(luò)。我們將不得不稍微更新模型,因?yàn)槲覀兊妮斎霐?shù)據(jù)是 3D 的,并且我們將添加一些復(fù)雜的細(xì)節(jié),但圖 11.5 中顯示的整體結(jié)構(gòu)應(yīng)該感覺(jué)熟悉。同樣,我們?yōu)檫@個(gè)項(xiàng)目所做的工作將是您未來(lái)項(xiàng)目的良好基礎(chǔ),盡管您離開(kāi)分類或分割項(xiàng)目越遠(yuǎn),就越需要調(diào)整這個(gè)基礎(chǔ)以適應(yīng)。讓我們從組成網(wǎng)絡(luò)大部分的四個(gè)重復(fù)塊開(kāi)始剖析這個(gè)架構(gòu)。
圖 11.5 LunaModel
類的架構(gòu)由批量歸一化尾部、四個(gè)塊的主干和由線性層后跟 softmax 組成的頭部。
11.4.1 核心卷積
分類模型通常由尾部、主干(或身體)和頭部組成。尾部是處理網(wǎng)絡(luò)輸入的前幾層。這些早期層通常具有與網(wǎng)絡(luò)其余部分不同的結(jié)構(gòu)或組織,因?yàn)樗鼈儽仨殞⑤斎胝{(diào)整為主干所期望的形式。在這里,我們使用簡(jiǎn)單的批量歸一化層,盡管通常尾部也包含卷積層。這些卷積層通常用于大幅度降低圖像的大小;由于我們的圖像尺寸已經(jīng)很小,所以這里不需要這樣做。
接下來(lái),網(wǎng)絡(luò)的骨干通常包含大部分層,這些層通常按塊的系列排列。每個(gè)塊具有相同(或至少類似)的層集,盡管通常從一個(gè)塊到另一個(gè)塊,預(yù)期輸入的大小和濾波器數(shù)量會(huì)發(fā)生變化。我們將使用一個(gè)由兩個(gè) 3 × 3 卷積組成的塊,每個(gè)卷積后跟一個(gè)激活函數(shù),并在塊末尾進(jìn)行最大池化操作。我們可以在圖 11.5 的擴(kuò)展視圖中看到標(biāo)記為Block[block1]
的塊的實(shí)現(xiàn)。以下是代碼中塊的實(shí)現(xiàn)。
代碼清單 11.6 model.py:67,class
LunaBlock
class LunaBlock(nn.Module):def __init__(self, in_channels, conv_channels):super().__init__()self.conv1 = nn.Conv3d(in_channels, conv_channels, kernel_size=3, padding=1, bias=True,)self.relu1 = nn.ReLU(inplace=True) 1((CO5-1))self.conv2 = nn.Conv3d(conv_channels, conv_channels, kernel_size=3, padding=1, bias=True,)self.relu2 = nn.ReLU(inplace=True) # ?self.maxpool = nn.MaxPool3d(2, 2)def forward(self, input_batch):block_out = self.conv1(input_batch)block_out = self.relu1(block_out) # ?block_out = self.conv2(block_out)block_out = self.relu2(block_out) # ?return self.maxpool(block_out)
? 這些可以作為對(duì)功能 API 的調(diào)用來(lái)實(shí)現(xiàn)。
最后,網(wǎng)絡(luò)的頭部接收來(lái)自骨干的輸出,并將其轉(zhuǎn)換為所需的輸出形式。對(duì)于卷積網(wǎng)絡(luò),這通常涉及將中間輸出展平并傳遞給全連接層。對(duì)于一些網(wǎng)絡(luò),也可以考慮包括第二個(gè)全連接層,盡管這通常更適用于具有更多結(jié)構(gòu)的分類問(wèn)題(比如想想汽車與卡車有輪子、燈、格柵、門(mén)等)和具有大量類別的項(xiàng)目。由于我們只進(jìn)行二元分類,并且似乎不需要額外的復(fù)雜性,我們只有一個(gè)展平層。
使用這樣的結(jié)構(gòu)可以作為卷積網(wǎng)絡(luò)的良好第一構(gòu)建塊。雖然存在更復(fù)雜的設(shè)計(jì),但對(duì)于許多項(xiàng)目來(lái)說(shuō),它們?cè)趯?shí)現(xiàn)復(fù)雜性和計(jì)算需求方面都過(guò)于復(fù)雜。最好從簡(jiǎn)單開(kāi)始,只有在確實(shí)需要時(shí)才增加復(fù)雜性。
我們可以在圖 11.6 中看到我們塊的卷積在 2D 中表示。由于這是較大圖像的一小部分,我們?cè)谶@里忽略填充。(請(qǐng)注意,未顯示 ReLU 激活函數(shù),因?yàn)閼?yīng)用它不會(huì)改變圖像大小。)
讓我們?cè)敿?xì)了解輸入體素和單個(gè)輸出體素之間的信息流。當(dāng)輸入發(fā)生變化時(shí),我們希望對(duì)輸出如何響應(yīng)有一個(gè)清晰的認(rèn)識(shí)。最好回顧第八章,特別是第 8.1 至 8.3 節(jié),以確保您對(duì)卷積的基本機(jī)制完全掌握。
圖 11.6 LunaModel
塊的卷積架構(gòu)由兩個(gè) 3 × 3 卷積和一個(gè)最大池組成。最終像素具有 6 × 6 的感受野。
我們?cè)谖覀兊膲K中使用 3 × 3 × 3 卷積。單個(gè) 3 × 3 × 3 卷積具有 3 × 3 × 3 的感受野,這幾乎是顯而易見(jiàn)的。輸入了 27 個(gè)體素,輸出一個(gè)體素。
當(dāng)我們使用兩個(gè)連續(xù)的 3 × 3 × 3 卷積時(shí),情況變得有趣。堆疊卷積層允許最終輸出的體素(或像素)受到比卷積核大小所示的更遠(yuǎn)的輸入的影響。如果將該輸出體素作為邊緣體素之一輸入到另一個(gè) 3 × 3 × 3 卷積核中,則第一層的一些輸入將位于第二層的輸入 3 × 3 × 3 區(qū)域之外。這兩個(gè)堆疊層的最終輸出具有 5 × 5 × 5 的有效感受野。這意味著當(dāng)兩者一起考慮時(shí),堆疊層的作用類似于具有更大尺寸的單個(gè)卷積層。
換句話說(shuō),每個(gè) 3 × 3 × 3 卷積層為感受野添加了額外的一像素邊界。如果我們?cè)趫D 11.6 中向后跟蹤箭頭,我們可以看到這一點(diǎn);我們的 2 × 2 輸出具有 4 × 4 的感受野,進(jìn)而具有 6 × 6 的感受野。兩個(gè)堆疊的 3 × 3 × 3 層比完整的 5 × 5 × 5 卷積使用更少的參數(shù)(因此計(jì)算速度更快)。
我們兩個(gè)堆疊的卷積的輸出被送入一個(gè) 2×2×2 的最大池,這意味著我們正在取一個(gè) 6×6×6 的有效區(qū)域,丟棄了七分之八的數(shù)據(jù),并選擇了產(chǎn)生最大值的一個(gè) 5×5×5 區(qū)域?,F(xiàn)在,那些“被丟棄”的輸入體素仍然有機(jī)會(huì)貢獻(xiàn),因?yàn)榫嚯x一個(gè)輸出體素的最大池還有一個(gè)重疊的輸入?yún)^(qū)域,所以它們可能以這種方式影響最終輸出。
請(qǐng)注意,雖然我們展示了每個(gè)卷積層的感受野隨著每個(gè)卷積層的縮小而縮小,但我們使用了填充卷積,它在圖像周圍添加了一個(gè)虛擬的一像素邊框。這樣做可以保持輸入和輸出圖像的大小不變。
nn.ReLU
層與我們?cè)诘诹轮锌吹降膶酉嗤4笥?0.0 的輸出將保持不變,小于 0.0 的輸出將被截?cái)酁榱恪?/p>
這個(gè)塊將被多次重復(fù)以形成我們模型的主干。
11.4.2 完整模型
讓我們看一下完整模型的實(shí)現(xiàn)。我們將跳過(guò)塊的定義,因?yàn)槲覀儎倓傇诖a清單 11.6 中看到了。
代碼清單 11.7 model.py:13,class
LunaModel
class LunaModel(nn.Module):def __init__(self, in_channels=1, conv_channels=8):super().__init__()self.tail_batchnorm = nn.BatchNorm3d(1) # ?self.block1 = LunaBlock(in_channels, conv_channels) # ?self.block2 = LunaBlock(conv_channels, conv_channels * 2) # ?self.block3 = LunaBlock(conv_channels * 2, conv_channels * 4) # ?self.block4 = LunaBlock(conv_channels * 4, conv_channels * 8) # ?self.head_linear = nn.Linear(1152, 2) # ?self.head_softmax = nn.Softmax(dim=1) # ?
? 尾部
? 主干
? 頭部
在這里,我們的尾部相對(duì)簡(jiǎn)單。我們將使用nn.BatchNorm3d
對(duì)輸入進(jìn)行歸一化,正如我們?cè)诘诎苏轮锌吹降哪菢?#xff0c;它將移動(dòng)和縮放我們的輸入,使其具有均值為 0 和標(biāo)準(zhǔn)差為 1。因此,我們的輸入單位處于的有點(diǎn)奇怪的漢斯菲爾德單位(HU)尺度對(duì)網(wǎng)絡(luò)的其余部分來(lái)說(shuō)并不明顯。這是一個(gè)有點(diǎn)武斷的選擇;我們知道我們的輸入單位是什么,我們知道相關(guān)組織的預(yù)期值,所以我們可能很容易地實(shí)現(xiàn)一個(gè)固定的歸一化方案。目前尚不清楚哪種方法更好。
我們的主干是四個(gè)重復(fù)的塊,塊的實(shí)現(xiàn)被提取到我們之前在代碼清單 11.6 中看到的單獨(dú)的nn.Module
子類中。由于每個(gè)塊以 2×2×2 的最大池操作結(jié)束,經(jīng)過(guò) 4 層后,我們將在每個(gè)維度上將圖像的分辨率降低 16 倍。回想一下第十章,我們的數(shù)據(jù)以 32×48×48 的塊返回,最終將變?yōu)?2×3×3。
最后,我們的尾部只是一個(gè)全連接層,然后調(diào)用nn.Softmax
。Softmax 是用于單標(biāo)簽分類任務(wù)的有用函數(shù),并具有一些不錯(cuò)的特性:它將輸出限制在 0 到 1 之間,對(duì)輸入的絕對(duì)范圍相對(duì)不敏感(只有輸入的相對(duì)值重要),并且允許我們的模型表達(dá)對(duì)答案的確定程度。
函數(shù)本身相對(duì)簡(jiǎn)單。輸入的每個(gè)值都用于求冪e
,然后得到的一系列值除以所有求冪結(jié)果的總和。以下是一個(gè)簡(jiǎn)單的非優(yōu)化 softmax 實(shí)現(xiàn)的 Python 代碼示例:
>>> logits = [1, -2, 3]
>>> exp = [e ** x for x in logits]
>>> exp
[2.718, 0.135, 20.086]>>> softmax = [x / sum(exp) for x in exp]
>>> softmax
[0.118, 0.006, 0.876]
當(dāng)然,我們?cè)谀P椭惺褂?PyTorch 版本的nn.Softmax
,因?yàn)樗旧砭湍芾斫馀幚砗蛷埩?#xff0c;并且會(huì)快速且如預(yù)期地執(zhí)行自動(dòng)梯度。
復(fù)雜性:從卷積轉(zhuǎn)換為線性
繼續(xù)我們的模型定義,我們遇到了一個(gè)復(fù)雜性。我們不能簡(jiǎn)單地將self.block4
的輸出饋送到全連接層,因?yàn)樵撦敵鍪敲總€(gè)樣本的 64 通道的 2×3×3 圖像,而全連接層期望一個(gè) 1D 向量作為輸入(技術(shù)上說(shuō),它們期望一個(gè)批量的 1D 向量,這是一個(gè) 2D 數(shù)組,但無(wú)論如何不匹配)。讓我們看一下forward
方法。
代碼清單 11.8 model.py:50,LunaModel.forward
def forward(self, input_batch):bn_output = self.tail_batchnorm(input_batch)block_out = self.block1(bn_output)block_out = self.block2(block_out)block_out = self.block3(block_out)block_out = self.block4(block_out)conv_flat = block_out.view(block_out.size(0), # ?-1,)linear_output = self.head_linear(conv_flat)return linear_output, self.head_softmax(linear_output)
? 批處理大小
請(qǐng)注意,在將數(shù)據(jù)傳遞到全連接層之前,我們必須使用view
函數(shù)對(duì)其進(jìn)行展平。由于該操作是無(wú)狀態(tài)的(沒(méi)有控制其行為的參數(shù)),我們可以簡(jiǎn)單地在forward
函數(shù)中執(zhí)行該操作。這在某種程度上類似于我們?cè)诘诎苏掠懻摰墓δ芙涌?。幾乎每個(gè)使用卷積并產(chǎn)生分類、回歸或其他非圖像輸出的模型都會(huì)在網(wǎng)絡(luò)頭部具有類似的組件。
對(duì)于forward
方法的返回值,我們同時(shí)返回原始logits和 softmax 生成的概率。我們?cè)诘?7.2.6 節(jié)中首次提到了 logits:它們是網(wǎng)絡(luò)在被 softmax 層歸一化之前產(chǎn)生的數(shù)值。這可能聽(tīng)起來(lái)有點(diǎn)復(fù)雜,但 logits 實(shí)際上只是 softmax 層的原始輸入。它們可以有任何實(shí)值輸入,softmax 會(huì)將它們壓縮到 0-1 的范圍內(nèi)。
在訓(xùn)練時(shí),我們將使用 logits 來(lái)計(jì)算nn.CrossEntropyLoss
,?而在實(shí)際對(duì)樣本進(jìn)行分類時(shí),我們將使用概率。在訓(xùn)練和生產(chǎn)中使用的輸出之間存在這種輕微差異是相當(dāng)常見(jiàn)的,特別是當(dāng)兩個(gè)輸出之間的差異是像 softmax 這樣簡(jiǎn)單、無(wú)狀態(tài)的函數(shù)時(shí)。
初始化
最后,讓我們談?wù)劤跏蓟W(wǎng)絡(luò)參數(shù)。為了使我們的模型表現(xiàn)良好,網(wǎng)絡(luò)的權(quán)重、偏置和其他參數(shù)需要表現(xiàn)出一定的特性。讓我們想象一個(gè)退化的情況,即網(wǎng)絡(luò)的所有權(quán)重都大于 1(且沒(méi)有殘差連接)。在這種情況下,重復(fù)乘以這些權(quán)重會(huì)導(dǎo)致數(shù)據(jù)通過(guò)網(wǎng)絡(luò)層時(shí)層輸出變得非常大。類似地,小于 1 的權(quán)重會(huì)導(dǎo)致所有層輸出變得更小并消失。類似的考慮也適用于反向傳播中的梯度。
許多規(guī)范化技術(shù)可以用來(lái)保持層輸出的良好行為,但其中最簡(jiǎn)單的一種是確保網(wǎng)絡(luò)的權(quán)重初始化得當(dāng),使得中間值和梯度既不過(guò)小也不過(guò)大。正如我們?cè)诘诎苏掠懻摰哪菢?#xff0c;PyTorch 在這里沒(méi)有給予我們足夠的幫助,因此我們需要自己進(jìn)行一些初始化。我們可以將以下_init_weights
函數(shù)視為樣板,因?yàn)榇_切的細(xì)節(jié)并不特別重要。
列表 11.9 model.py:30,LunaModel._init_weights
def _init_weights(self):for m in self.modules():if type(m) in {nn.Linear,nn.Conv3d,}:nn.init.kaiming_normal_(m.weight.data, a=0, mode='fan_out', nonlinearity='relu',)if m.bias is not None:fan_in, fan_out = \nn.init._calculate_fan_in_and_fan_out(m.weight.data)bound = 1 / math.sqrt(fan_out)nn.init.normal_(m.bias, -bound, bound)
11.5 訓(xùn)練和驗(yàn)證模型
現(xiàn)在是時(shí)候?qū)⑽覀円恢痹谔幚淼母鞣N部分組裝起來(lái),以便我們實(shí)際執(zhí)行。這個(gè)訓(xùn)練循環(huán)應(yīng)該很熟悉–我們?cè)诘谖逭驴吹搅祟愃茍D 11.7 的循環(huán)。
圖 11.7 我們將在本章實(shí)現(xiàn)的訓(xùn)練和驗(yàn)證腳本,重點(diǎn)是在每個(gè)時(shí)期和時(shí)期中的批次上進(jìn)行嵌套循環(huán)
代碼相對(duì)緊湊(doTraining
函數(shù)僅有 12 個(gè)語(yǔ)句;由于行長(zhǎng)限制,這里較長(zhǎng))。
列表 11.10 training.py:137,LunaTrainingApp.main
def main(self):# ... line 143for epoch_ndx in range(1, self.cli_args.epochs + 1):trnMetrics_t = self.doTraining(epoch_ndx, train_dl)self.logMetrics(epoch_ndx, 'trn', trnMetrics_t)# ... line 165
def doTraining(self, epoch_ndx, train_dl):self.model.train()trnMetrics_g = torch.zeros( # ?METRICS_SIZE,len(train_dl.dataset),device=self.device,)batch_iter = enumerateWithEstimate( # ?train_dl,"E{} Training".format(epoch_ndx),start_ndx=train_dl.num_workers,)for batch_ndx, batch_tup in batch_iter:self.optimizer.zero_grad() # ?loss_var = self.computeBatchLoss( # ?batch_ndx,batch_tup,train_dl.batch_size,trnMetrics_g)loss_var.backward() # ?self.optimizer.step() # ?self.totalTrainingSamples_count += len(train_dl.dataset)return trnMetrics_g.to('cpu')
? 初始化一個(gè)空的指標(biāo)數(shù)組
? 設(shè)置我們的批次循環(huán)和時(shí)間估計(jì)
? 釋放任何剩余的梯度張量
? 我們將在下一節(jié)詳細(xì)討論這種方法。
? 實(shí)際更新模型權(quán)重
我們從前幾章的訓(xùn)練循環(huán)中看到的主要區(qū)別如下:
-
trnMetrics_g
張量在訓(xùn)練過(guò)程中收集了詳細(xì)的每類指標(biāo)。對(duì)于像我們這樣的大型項(xiàng)目,這種洞察力可能非常有用。 -
我們不直接遍歷
train_dl
數(shù)據(jù)加載器。我們使用enumerateWithEstimate
來(lái)提供預(yù)計(jì)完成時(shí)間。這并不是必要的;這只是一種風(fēng)格上的選擇。 -
實(shí)際的損失計(jì)算被推入
computeBatchLoss
方法中。再次強(qiáng)調(diào),這并不是絕對(duì)必要的,但代碼重用通常是一個(gè)優(yōu)點(diǎn)。
我們將在第 11.7.2 節(jié)討論為什么我們?cè)?code>enumerate周圍包裝了額外的功能;目前,假設(shè)它與enumerate(train_dl)
相同。
trnMetrics_g
張量的目的是將有關(guān)模型在每個(gè)樣本基礎(chǔ)上的行為信息從computeBatchLoss
函數(shù)傳輸?shù)?code>logMetrics函數(shù)。讓我們接下來(lái)看一下computeBatchLoss
。在完成主要訓(xùn)練循環(huán)的其余部分后,我們將討論logMetrics
。
11.5.1 computeBatchLoss
函數(shù)
computeBatchLoss
函數(shù)被訓(xùn)練和驗(yàn)證循環(huán)調(diào)用。顧名思義,它計(jì)算一批樣本的損失。此外,該函數(shù)還計(jì)算并記錄模型產(chǎn)生的每個(gè)樣本信息。這使我們能夠計(jì)算每個(gè)類別的正確答案百分比,從而讓我們專注于模型遇到困難的領(lǐng)域。
當(dāng)然,函數(shù)的核心功能是將批次輸入模型并計(jì)算每個(gè)批次的損失。我們使用CrossEntropyLoss
( pytorch.org/docs/stable/nn.html#torch.nn.CrossEntropyLoss
),就像在第七章中一樣。解包批次元組,將張量移動(dòng)到 GPU,并調(diào)用模型應(yīng)該在之前的訓(xùn)練工作后都感到熟悉。
列表 11.11 training.py:225,.computeBatchLoss
def computeBatchLoss(self, batch_ndx, batch_tup, batch_size, metrics_g):input_t, label_t, _series_list, _center_list = batch_tupinput_g = input_t.to(self.device, non_blocking=True)label_g = label_t.to(self.device, non_blocking=True)logits_g, probability_g = self.model(input_g)loss_func = nn.CrossEntropyLoss(reduction='none') # ?loss_g = loss_func(logits_g,label_g[:,1], # ?)# ... line 238return loss_g.mean() # ?
? reduction=‘none’
給出每個(gè)樣本的損失。
? one-hot 編碼類別的索引
? 將每個(gè)樣本的損失重新組合為單個(gè)值
在這里,我們不使用默認(rèn)行為來(lái)獲得平均批次的損失值。相反,我們得到一個(gè)損失值的張量,每個(gè)樣本一個(gè)。這使我們能夠跟蹤各個(gè)損失,這意味著我們可以按照自己的意愿進(jìn)行聚合(例如,按類別)。我們馬上就會(huì)看到這一點(diǎn)。目前,我們將返回這些每個(gè)樣本損失的均值,這等同于批次損失。在不想保留每個(gè)樣本統(tǒng)計(jì)信息的情況下,使用批次平均損失是完全可以的。是否這樣取決于您的項(xiàng)目和目標(biāo)。
一旦完成了這些,我們就完成了對(duì)調(diào)用函數(shù)的義務(wù),就 backpropagation 和權(quán)重更新而言,需要做的事情。然而,在這之前,我們還想要記錄我們每個(gè)樣本的統(tǒng)計(jì)數(shù)據(jù)以供后人(和后續(xù)分析)使用。我們將使用傳入的metrics_g
參數(shù)來(lái)實(shí)現(xiàn)這一點(diǎn)。
列表 11.12 training.py:26
METRICS_LABEL_NDX=0 # ?
METRICS_PRED_NDX=1
METRICS_LOSS_NDX=2
METRICS_SIZE = 3# ... line 225def computeBatchLoss(self, batch_ndx, batch_tup, batch_size, metrics_g):# ... line 238start_ndx = batch_ndx * batch_sizeend_ndx = start_ndx + label_t.size(0)metrics_g[METRICS_LABEL_NDX, start_ndx:end_ndx] = \ # ?label_g[:,1].detach() # ?metrics_g[METRICS_PRED_NDX, start_ndx:end_ndx] = \ # ?probability_g[:,1].detach() # ?metrics_g[METRICS_LOSS_NDX, start_ndx:end_ndx] = \ # ?loss_g.detach() # ?return loss_g.mean() # ?
? 這些命名的數(shù)組索引在模塊級(jí)別范圍內(nèi)聲明
? 我們使用detach
,因?yàn)槲覀兊闹笜?biāo)都不需要保留梯度。
? 再次,這是整個(gè)批次的損失。
通過(guò)記錄每個(gè)訓(xùn)練(以及后續(xù)的驗(yàn)證)樣本的標(biāo)簽、預(yù)測(cè)和損失,我們擁有大量詳細(xì)信息,可以用來(lái)研究我們模型的行為。目前,我們將專注于編譯每個(gè)類別的統(tǒng)計(jì)數(shù)據(jù),但我們也可以輕松地使用這些信息找到被錯(cuò)誤分類最多的樣本,并開(kāi)始調(diào)查原因。同樣,對(duì)于一些項(xiàng)目,這種信息可能不那么有趣,但記住你有這些選項(xiàng)是很好的。
11.5.2 驗(yàn)證循環(huán)類似
圖 11.8 中的驗(yàn)證循環(huán)看起來(lái)與訓(xùn)練很相似,但有些簡(jiǎn)化。關(guān)鍵區(qū)別在于驗(yàn)證是只讀的。具體來(lái)說(shuō),返回的損失值不會(huì)被使用,權(quán)重也不會(huì)被更新。
圖 11.8 我們將在本章實(shí)現(xiàn)的訓(xùn)練和驗(yàn)證腳本,重點(diǎn)放在每個(gè) epoch 的驗(yàn)證循環(huán)上
在函數(shù)調(diào)用的開(kāi)始和結(jié)束之間,模型的任何內(nèi)容都不應(yīng)該發(fā)生變化。此外,由于with torch.no_grad()
上下文管理器明確告知 PyTorch 不需要計(jì)算梯度,因此速度要快得多。
在 LunaTrainingApp.main
中的 training.py:137
,代碼清單 11.13
def main(self):for epoch_ndx in range(1, self.cli_args.epochs + 1):# ... line 157valMetrics_t = self.doValidation(epoch_ndx, val_dl)self.logMetrics(epoch_ndx, 'val', valMetrics_t)# ... line 203
def doValidation(self, epoch_ndx, val_dl):with torch.no_grad():self.model.eval() # ?valMetrics_g = torch.zeros(METRICS_SIZE,len(val_dl.dataset),device=self.device,)batch_iter = enumerateWithEstimate(val_dl,"E{} Validation ".format(epoch_ndx),start_ndx=val_dl.num_workers,)for batch_ndx, batch_tup in batch_iter:self.computeBatchLoss(batch_ndx, batch_tup, val_dl.batch_size, valMetrics_g)return valMetrics_g.to('cpu')
? 關(guān)閉訓(xùn)練時(shí)的行為
在不需要更新網(wǎng)絡(luò)權(quán)重的情況下(回想一下,這樣做會(huì)違反驗(yàn)證集的整個(gè)前提;我們絕不希望這樣做!),我們不需要使用computeBatchLoss
返回的損失,也不需要引用優(yōu)化器。 在循環(huán)內(nèi)部剩下的只有對(duì)computeBatchLoss
的調(diào)用。請(qǐng)注意,盡管我們不使用computeBatchLoss
返回的每批損失來(lái)做任何事情,但我們?nèi)匀辉?code>valMetrics_g中收集指標(biāo)作為調(diào)用的副作用。
11.6 輸出性能指標(biāo)
每個(gè)時(shí)期我們做的最后一件事是記錄本時(shí)期的性能指標(biāo)。如圖 11.9 所示,一旦我們記錄了指標(biāo),我們就會(huì)返回到下一個(gè)訓(xùn)練時(shí)期的訓(xùn)練循環(huán)中。在訓(xùn)練過(guò)程中隨著進(jìn)展記錄結(jié)果是很重要的,因?yàn)槿绻?xùn)練出現(xiàn)問(wèn)題(在深度學(xué)習(xí)術(shù)語(yǔ)中稱為“不收斂”),我們希望能夠注意到這一點(diǎn),并停止花費(fèi)時(shí)間訓(xùn)練一個(gè)不起作用的模型。在較小的情況下,能夠監(jiān)視模型行為是很有幫助的。
圖 11.9 我們將在本章實(shí)現(xiàn)的訓(xùn)練和驗(yàn)證腳本,重點(diǎn)放在每個(gè)時(shí)期結(jié)束時(shí)的指標(biāo)記錄上
之前,我們?cè)?code>trnMetrics_g和valMetrics_g
中收集結(jié)果以記錄每個(gè)時(shí)期的進(jìn)展。這兩個(gè)張量現(xiàn)在包含了我們計(jì)算每個(gè)訓(xùn)練和驗(yàn)證運(yùn)行的每類百分比正確和平均損失所需的一切。每個(gè)時(shí)期執(zhí)行此操作是一個(gè)常見(jiàn)選擇,盡管有些是任意的。在未來(lái)的章節(jié)中,我們將看到如何調(diào)整我們的時(shí)期大小,以便以合理的速率獲得有關(guān)訓(xùn)練進(jìn)度的反饋。
11.6.1 logMetrics 函數(shù)
讓我們談?wù)?code>logMetrics函數(shù)的高級(jí)結(jié)構(gòu)。簽名看起來(lái)像這樣。
在 LunaTrainingApp.logMetrics
中的 training.py:251
,代碼清單 11.14
def logMetrics(self,epoch_ndx,mode_str,metrics_t,classificationThreshold=0.5,
):
我們僅使用epoch_ndx
來(lái)在記錄結(jié)果時(shí)顯示。mode_str
參數(shù)告訴我們指標(biāo)是用于訓(xùn)練還是驗(yàn)證。
我們要么使用傳入的metrics_t
參數(shù)中的trnMetrics_t
或valMetrics_t
?;叵胍幌?#xff0c;這兩個(gè)輸入都是浮點(diǎn)值的張量,在computeBatchLoss
期間我們填充了數(shù)據(jù),然后在我們從doTraining
和doValidation
返回它們之前將它們轉(zhuǎn)移到 CPU。這兩個(gè)張量都有三行,以及我們有樣本數(shù)(訓(xùn)練樣本或驗(yàn)證樣本,取決于)的列數(shù)。作為提醒,這三行對(duì)應(yīng)以下常量。
在 training.py:26
,代碼清單 11.15
METRICS_LABEL_NDX=0 # ?
METRICS_PRED_NDX=1
METRICS_LOSS_NDX=2
METRICS_SIZE = 3
? 這些在模塊級(jí)別范圍內(nèi)聲明。
張量掩碼和布爾索引
掩碼張量是一種常見(jiàn)的使用模式,如果您以前沒(méi)有遇到過(guò),可能會(huì)感到不透明。您可能熟悉 NumPy 概念稱為掩碼數(shù)組;張量和數(shù)組掩碼的行為方式相同。
如果您對(duì)掩碼數(shù)組不熟悉,NumPy 文檔中的一個(gè)優(yōu)秀頁(yè)面(mng.bz/XPra
)很好地描述了其行為。 PyTorch 故意使用與 NumPy 相同的語(yǔ)法和語(yǔ)義。
構(gòu)建掩碼
接下來(lái),我們將構(gòu)建掩碼,以便僅將指標(biāo)限制為結(jié)節(jié)或非結(jié)節(jié)(也稱為陽(yáng)性或陰性)樣本。我們還將計(jì)算每個(gè)類別的總樣本數(shù),以及我們正確分類的樣本數(shù)。
在 LunaTrainingApp.logMetrics
中的 training.py:264
,代碼清單 11.16
negLabel_mask = metrics_t[METRICS_LABEL_NDX] <= classificationThreshold
negPred_mask = metrics_t[METRICS_PRED_NDX] <= classificationThresholdposLabel_mask = ~negLabel_mask
posPred_mask = ~negPred_mask
雖然我們?cè)谶@里沒(méi)有assert
,但我們知道存儲(chǔ)在metrics _t[METRICS_LABEL_NDX]
中的所有值屬于集合{0.0, 1.0}
,因?yàn)槲覀冎牢覀兊慕Y(jié)節(jié)狀態(tài)標(biāo)簽只是True
或False
。通過(guò)與默認(rèn)值為 0.5 的classificationThreshold
進(jìn)行比較,我們得到一個(gè)二進(jìn)制值數(shù)組,其中True
值對(duì)應(yīng)于所討論樣本的非結(jié)節(jié)(也稱為負(fù))標(biāo)簽。
我們進(jìn)行類似的比較以創(chuàng)建negPred_mask
,但我們必須記住METRICS_PRED_NDX
值是我們模型產(chǎn)生的正預(yù)測(cè),可以是介于 0.0 和 1.0 之間的任意浮點(diǎn)值。這并不改變我們的比較,但這意味著實(shí)際值可能接近 0.5。正掩模只是負(fù)掩模的反向。
注意 雖然其他項(xiàng)目可以利用類似的方法,但重要的是要意識(shí)到,我們正在采取一些捷徑,這是因?yàn)檫@是一個(gè)二元分類問(wèn)題。如果您的下一個(gè)項(xiàng)目有超過(guò)兩個(gè)類別或樣本同時(shí)屬于多個(gè)類別,您將需要使用更復(fù)雜的邏輯來(lái)構(gòu)建類似的掩模。
接下來(lái),我們使用這些掩模計(jì)算一些每個(gè)標(biāo)簽的統(tǒng)計(jì)數(shù)據(jù),并將其存儲(chǔ)在字典metrics_dict
中。
代碼清單 11.17 training.py:270,LunaTrainingApp.logMetrics
neg_count = int(negLabel_mask.sum()) # ?
pos_count = int(posLabel_mask.sum())neg_correct = int((negLabel_mask & negPred_mask).sum())
pos_correct = int((posLabel_mask & posPred_mask).sum())metrics_dict = {}
metrics_dict['loss/all'] = \metrics_t[METRICS_LOSS_NDX].mean()
metrics_dict['loss/neg'] = \metrics_t[METRICS_LOSS_NDX, negLabel_mask].mean()
metrics_dict['loss/pos'] = \metrics_t[METRICS_LOSS_NDX, posLabel_mask].mean()metrics_dict['correct/all'] = (pos_correct + neg_correct) \/ np.float32(metrics_t.shape[1]) * 100 # ?
metrics_dict['correct/neg'] = neg_correct / np.float32(neg_count) * 100
metrics_dict['correct/pos'] = pos_correct / np.float32(pos_count) * 100
? 轉(zhuǎn)換為普通的 Python 整數(shù)
? 避免整數(shù)除法,轉(zhuǎn)換為 np.float32
首先,我們計(jì)算整個(gè)時(shí)期的平均損失。由于損失是訓(xùn)練過(guò)程中要最小化的單一指標(biāo),我們始終希望能夠跟蹤它。然后,我們將損失平均限制為僅使用我們剛剛制作的negLabel_mask
的那些帶有負(fù)標(biāo)簽的樣本。我們對(duì)正損失也是一樣的。像這樣計(jì)算每類損失在某種情況下是有用的,如果一個(gè)類別比另一個(gè)類別更難分類,那么這種知識(shí)可以幫助推動(dòng)調(diào)查和改進(jìn)。
我們將通過(guò)確定我們正確分類的樣本比例以及每個(gè)標(biāo)簽的正確比例來(lái)結(jié)束計(jì)算,因?yàn)槲覀儗⒃谏院髮⑦@些數(shù)字顯示為百分比,所以我們還將這些值乘以 100。與損失類似,我們可以使用這些數(shù)字來(lái)幫助指導(dǎo)我們?cè)谶M(jìn)行改進(jìn)時(shí)的努力。計(jì)算完成后,我們通過(guò)三次調(diào)用log.info
記錄我們的結(jié)果。
代碼清單 11.18 training.py:289,LunaTrainingApp.logMetrics
log.info(("E{} {:8} {loss/all:.4f} loss, "+ "{correct/all:-5.1f}% correct, ").format(epoch_ndx,mode_str,**metrics_dict,)
)
log.info(("E{} {:8} {loss/neg:.4f} loss, "+ "{correct/neg:-5.1f}% correct ({neg_correct:} of {neg_count:})").format(epoch_ndx,mode_str + '_neg',neg_correct=neg_correct,neg_count=neg_count,**metrics_dict,)
)
log.info( # ?# ... line 319
)
? “pos”日志與之前的“neg”日志類似。
第一個(gè)日志包含從所有樣本計(jì)算得出的值,并標(biāo)記為/all
,而負(fù)(非結(jié)節(jié))和正(結(jié)節(jié))值分別標(biāo)記為/neg
和/pos
。我們這里不顯示正值的第三個(gè)日志聲明;它與第二個(gè)相同,只是在所有情況下將neg替換為pos。
11.7 運(yùn)行訓(xùn)練腳本
現(xiàn)在我們已經(jīng)完成了 training.py 腳本的核心部分,我們將開(kāi)始實(shí)際運(yùn)行它。這將初始化和訓(xùn)練我們的模型,并打印關(guān)于訓(xùn)練進(jìn)展情況的統(tǒng)計(jì)信息。我們的想法是在我們?cè)敿?xì)介紹模型實(shí)現(xiàn)的同時(shí),將其啟動(dòng)在后臺(tái)運(yùn)行。希望我們完成后能夠查看結(jié)果。
我們從主代碼目錄運(yùn)行此腳本;它應(yīng)該有名為 p2ch11、util 等的子目錄。所使用的python
環(huán)境應(yīng)該安裝了 requirements.txt 中列出的所有庫(kù)。一旦這些庫(kù)準(zhǔn)備就緒,我們就可以運(yùn)行:
$ python -m p2ch11.training # ?
Starting LunaTrainingApp,Namespace(batch_size=256, channels=8, epochs=20, layers=3, num_workers=8)
<p2ch11.dsets.LunaDataset object at 0x7fa53a128710>: 495958 training samples
<p2ch11.dsets.LunaDataset object at 0x7fa537325198>: 55107 validation samples
Epoch 1 of 20, 1938/216 batches of size 256
E1 Training ----/1938, starting
E1 Training 16/1938, done at 2018-02-28 20:52:54, 0:02:57
...
? 這是 Linux/Bash 的命令行。Windows 用戶可能需要根據(jù)所使用的安裝方法以不同方式調(diào)用 Python。
作為提醒,我們還提供了一個(gè)包含訓(xùn)練應(yīng)用程序調(diào)用的 Jupyter 筆記本。
代碼清單 11.19 code/p2_run_everything.ipynb
# In[5]:
run('p2ch11.prepcache.LunaPrepCacheApp')# In[6]:
run('p2ch11.training.LunaTrainingApp', '--epochs=1')
如果第一個(gè)時(shí)代似乎需要很長(zhǎng)時(shí)間(超過(guò) 10 或 20 分鐘),這可能與需要準(zhǔn)備 LunaDataset
需要的緩存數(shù)據(jù)有關(guān)。有關(guān)緩存的詳細(xì)信息,請(qǐng)參閱第 10.5.1 節(jié)。第十章的練習(xí)包括編寫(xiě)一個(gè)腳本以有效地預(yù)先填充緩存。我們還提供了 prepcache.py 文件來(lái)執(zhí)行相同的操作;可以使用 python -m p2ch11 .prepcache
調(diào)用它。由于我們每章都重復(fù)我們的 dsets.py 文件,因此緩存需要為每一章重復(fù)。這在一定程度上是空間和時(shí)間上的低效,但這意味著我們可以更好地保持每一章的代碼更加完整。對(duì)于您未來(lái)的項(xiàng)目,我們建議更多地重用您的緩存。
一旦訓(xùn)練開(kāi)始,我們要確保我們正在按照預(yù)期使用手頭的計(jì)算資源。判斷瓶頸是數(shù)據(jù)加載還是計(jì)算的一個(gè)簡(jiǎn)單方法是在腳本開(kāi)始訓(xùn)練后等待幾分鐘(查看類似 E1 Training 16/7750, done at...
的輸出),然后同時(shí)檢查 top
和 nvidia-smi
:
如果八個(gè) Python 工作進(jìn)程消耗了 >80% 的 CPU,那么緩存可能需要準(zhǔn)備(我們知道這一點(diǎn)是因?yàn)樽髡咭呀?jīng)確保在這個(gè)項(xiàng)目的實(shí)現(xiàn)中沒(méi)有 CPU 瓶頸;這不會(huì)是普遍的情況)。
如果 nvidia-smi
報(bào)告 GPU-Util
>80%,那么你的 GPU 已經(jīng)飽和了。我們將在第 11.7.2 節(jié)討論一些有效等待的策略。
我們的意圖是 GPU 飽和;我們希望盡可能多地利用計(jì)算能力來(lái)快速完成時(shí)代。一塊 NVIDIA GTX 1080 Ti 應(yīng)該在 15 分鐘內(nèi)完成一個(gè)時(shí)代。由于我們的模型相對(duì)簡(jiǎn)單,CPU 不需要太多的預(yù)處理才能成為瓶頸。當(dāng)處理更深的模型(或者總體需要更多計(jì)算的模型)時(shí),處理每個(gè)批次將需要更長(zhǎng)的時(shí)間,這將增加 CPU 處理的數(shù)量,以便在 GPU 在下一批輸入準(zhǔn)備好之前耗盡工作之前。
11.7.1 訓(xùn)練所需的數(shù)據(jù)
如果訓(xùn)練樣本數(shù)量少于 495,958 個(gè),驗(yàn)證樣本數(shù)量少于 55,107 個(gè),可能有必要進(jìn)行一些合理性檢查,以確保完整的數(shù)據(jù)已經(jīng)準(zhǔn)備就緒。對(duì)于您未來(lái)的項(xiàng)目,請(qǐng)確保您的數(shù)據(jù)集返回您期望的樣本數(shù)量。
首先,讓我們看一下我們的 data-unversioned/ part2/luna 目錄的基本目錄結(jié)構(gòu):
$ ls -1p data-unversioned/part2/luna/
subset0/
subset1/
...
subset9/
接下來(lái),讓我們確保每個(gè)系列 UID 都有一個(gè) .mhd 文件和一個(gè) .raw 文件
$ ls -1p data-unversioned/part2/luna/subset0/
1.3.6.1.4.1.14519.5.2.1.6279.6001.105756658031515062000744821260.mhd
1.3.6.1.4.1.14519.5.2.1.6279.6001.105756658031515062000744821260.raw
1.3.6.1.4.1.14519.5.2.1.6279.6001.108197895896446896160048741492.mhd
1.3.6.1.4.1.14519.5.2.1.6279.6001.108197895896446896160048741492.raw
...
以及我們是否有正確數(shù)量的文件:
$ ls -1 data-unversioned/part2/luna/subset?/* | wc -l
1776
$ ls -1 data-unversioned/part2/luna/subset0/* | wc -l
178
...
$ ls -1 data-unversioned/part2/luna/subset9/* | wc -l
176
如果所有這些看起來(lái)都正確,但事情仍然不順利,請(qǐng)?jiān)?Manning LiveBook 上提問(wèn)(livebook.manning.com/book/deep-learning-with-pytorch/chapter-11
),希望有人可以幫助解決問(wèn)題。
11.7.2 插曲:enumerateWithEstimate 函數(shù)
使用深度學(xué)習(xí)涉及大量的等待。我們談?wù)摰氖乾F(xiàn)實(shí)世界中坐在那里,看著墻上的時(shí)鐘,一個(gè)看著的壺永遠(yuǎn)不會(huì)煮開(kāi)(但你可以在 GPU 上煎蛋),純粹的 無(wú)聊。
唯一比坐在那里盯著一個(gè)一個(gè)小時(shí)都沒(méi)有移動(dòng)的閃爍光標(biāo)更糟糕的是,讓您的屏幕充斥著這些:
2020-01-01 10:00:00,056 INFO training batch 1234
2020-01-01 10:00:00,067 INFO training batch 1235
2020-01-01 10:00:00,077 INFO training batch 1236
2020-01-01 10:00:00,087 INFO training batch 1237
...etc...
至少安靜閃爍的光標(biāo)不會(huì)讓你的滾動(dòng)緩沖區(qū)溢出!
從根本上說(shuō),在所有這些等待的過(guò)程中,我們想要回答“我有時(shí)間去倒?jié)M水杯嗎?”這個(gè)問(wèn)題,以及關(guān)于是否有時(shí)間的后續(xù)問(wèn)題
沖一杯咖啡
準(zhǔn)備晚餐
在巴黎吃晚餐?
要回答這些緊迫的問(wèn)題,我們將使用我們的 enumerateWithEstimate
函數(shù)。使用方法如下:
>>> for i, _ in enumerateWithEstimate(list(range(234)), "sleeping"):
... time.sleep(random.random())
...
11:12:41,892 WARNING sleeping ----/234, starting
11:12:44,542 WARNING sleeping 4/234, done at 2020-01-01 11:15:16, 0:02:35
11:12:46,599 WARNING sleeping 8/234, done at 2020-01-01 11:14:59, 0:02:17
11:12:49,534 WARNING sleeping 16/234, done at 2020-01-01 11:14:33, 0:01:51
11:12:58,219 WARNING sleeping 32/234, done at 2020-01-01 11:14:41, 0:01:59
11:13:15,216 WARNING sleeping 64/234, done at 2020-01-01 11:14:43, 0:02:01
11:13:44,233 WARNING sleeping 128/234, done at 2020-01-01 11:14:35, 0:01:53
11:14:40,083 WARNING sleeping ----/234, done at 2020-01-01 11:14:40
>>>
這是超過(guò) 200 次迭代的 8 行輸出。即使考慮到random.random()
的廣泛變化,該函數(shù)在 16 次迭代后(不到 10 秒)就有了相當(dāng)不錯(cuò)的估計(jì)。對(duì)于具有更穩(wěn)定時(shí)間的循環(huán)體,估計(jì)會(huì)更快地穩(wěn)定下來(lái)。
就行為而言,enumerateWithEstimate
與標(biāo)準(zhǔn)的enumerate
幾乎完全相同(差異在于我們的函數(shù)返回一個(gè)生成器,而enumerate
返回一個(gè)專門(mén)的<enumerate object at 0x...>
)。
列表 11.20 util.py:143,def
enumerateWithEstimate
def enumerateWithEstimate(iter,desc_str,start_ndx=0,print_ndx=4,backoff=None,iter_len=None,
):for (current_ndx, item) in enumerate(iter):yield (current_ndx, item)
然而,副作用(特別是日志記錄)才是使函數(shù)變得有趣的地方。與其陷入細(xì)節(jié)中試圖覆蓋實(shí)現(xiàn)的每個(gè)細(xì)節(jié),如果您感興趣,可以查閱函數(shù)文檔字符串(github .com/deep-learning-with-pytorch/dlwpt-code/blob/master/util/util.py#L143
)以獲取有關(guān)函數(shù)參數(shù)的信息并對(duì)實(shí)現(xiàn)進(jìn)行桌面檢查。
深度學(xué)習(xí)項(xiàng)目可能非常耗時(shí)。知道何時(shí)預(yù)計(jì)完成意味著您可以明智地利用這段時(shí)間,它還可以提示您某些地方出了問(wèn)題(或者某種方法行不通),如果預(yù)計(jì)完成時(shí)間遠(yuǎn)遠(yuǎn)超出預(yù)期。
11.8 評(píng)估模型:達(dá)到 99.7%的正確率意味著我們完成了,對(duì)吧?
讓我們來(lái)看一下我們訓(xùn)練腳本的一些(縮減的)輸出。作為提醒,我們使用命令行python -m p2ch11.training
運(yùn)行了這個(gè)腳本:
E1 Training ----/969, starting
...
E1 LunaTrainingApp
E1 trn 2.4576 loss, 99.7% correct
...
E1 val 0.0172 loss, 99.8% correct
...
經(jīng)過(guò)一輪訓(xùn)練,訓(xùn)練集和驗(yàn)證集都顯示至少 99.7%的正確結(jié)果。這是 A+!是時(shí)候來(lái)一輪高五,或者至少滿意地點(diǎn)點(diǎn)頭微笑了。我們剛剛解決了癌癥!…對(duì)吧?
嗯,不是。
讓我們更仔細(xì)地(不那么縮減地)看一下第 1 個(gè)時(shí)代的輸出:
E1 LunaTrainingApp
E1 trn 2.4576 loss, 99.7% correct,
E1 trn_neg 0.1936 loss, 99.9% correct (494289 of 494743)
E1 trn_pos 924.34 loss, 0.2% correct (3 of 1215)
...
E1 val 0.0172 loss, 99.8% correct,
E1 val_neg 0.0025 loss, 100.0% correct (494743 of 494743)
E1 val_pos 5.9768 loss, 0.0% correct (0 of 1215)
在驗(yàn)證集上,我們對(duì)非結(jié)節(jié)的分類 100%正確,但實(shí)際結(jié)節(jié)卻 100%錯(cuò)誤。網(wǎng)絡(luò)只是將所有東西都分類為非結(jié)節(jié)!數(shù)值 99.7%只意味著大約 0.3%的樣本是結(jié)節(jié)。
經(jīng)過(guò) 10 個(gè)時(shí)代,情況只是稍微好轉(zhuǎn):
E10 LunaTrainingApp
E10 trn 0.0024 loss, 99.8% correct
E10 trn_neg 0.0000 loss, 100.0% correct
E10 trn_pos 0.9915 loss, 0.0% correct
E10 val 0.0025 loss, 99.7% correct
E10 val_neg 0.0000 loss, 100.0% correct
E10 val_pos 0.9929 loss, 0.0% correct
分類輸出保持不變–沒(méi)有一個(gè)結(jié)節(jié)(也稱為陽(yáng)性)樣本被正確識(shí)別。有趣的是,我們開(kāi)始看到val_pos
損失有所減少,然而,val_neg
損失并沒(méi)有相應(yīng)增加。這意味著網(wǎng)絡(luò)正在學(xué)習(xí)。不幸的是,它學(xué)習(xí)得非常,非常慢。
更糟糕的是,這種特定的失敗模式在現(xiàn)實(shí)世界中是最危險(xiǎn)的!我們希望避免將腫瘤誤分類為無(wú)害的結(jié)構(gòu),因?yàn)檫@不會(huì)促使患者接受可能需要的評(píng)估和最終治療。了解所有項(xiàng)目的誤分類后果很重要,因?yàn)檫@可能會(huì)對(duì)您設(shè)計(jì)、訓(xùn)練和評(píng)估模型的方式產(chǎn)生很大影響。我們將在下一章中更詳細(xì)地討論這個(gè)問(wèn)題。
然而,在此之前,我們需要升級(jí)我們的工具,使結(jié)果更易于理解。我們相信您和任何人一樣喜歡盯著數(shù)字列,但圖片價(jià)值千言。讓我們繪制一些這些指標(biāo)的圖表。
11.9 使用 TensorBoard 繪制訓(xùn)練指標(biāo)圖表
我們將使用一個(gè)名為 TensorBoard 的工具,作為一種快速簡(jiǎn)便的方式,將我們的訓(xùn)練指標(biāo)從訓(xùn)練循環(huán)中提取出來(lái),并呈現(xiàn)為一些漂亮的圖表。這將使我們能夠跟蹤這些指標(biāo)的趨勢(shì),而不僅僅查看每個(gè)時(shí)代的瞬時(shí)值。當(dāng)您查看可視化表示時(shí),要知道一個(gè)值是異常值還是趨勢(shì)的最新值就容易得多。
“嘿,等等”,您可能會(huì)想,“TensorBoard 不是 TensorFlow 項(xiàng)目的一部分嗎?它在我的 PyTorch 書(shū)中做什么?”
嗯,是的,它是另一個(gè)深度學(xué)習(xí)框架的一部分,但我們的理念是“使用有效的工具”。沒(méi)有理由限制自己不使用一個(gè)工具,只因?yàn)樗壴谖覀儾皇褂玫牧硪粋€(gè)項(xiàng)目中。PyTorch 和 TensorBoard 的開(kāi)發(fā)人員都同意,因?yàn)樗麄兒献鲗?TensorBoard 的官方支持添加到 PyTorch 中。TensorBoard 很棒,它有一些易于使用的 PyTorch API,讓我們可以將數(shù)據(jù)從幾乎任何地方連接到其中進(jìn)行快速簡(jiǎn)單的顯示。如果您堅(jiān)持深度學(xué)習(xí),您可能會(huì)看到(并使用)很多 TensorBoard。
實(shí)際上,如果您一直在運(yùn)行本章的示例,您應(yīng)該已經(jīng)有一些準(zhǔn)備好并等待顯示的數(shù)據(jù)在磁盤(pán)上。讓我們看看如何運(yùn)行 TensorBoard,并查看它可以向我們展示什么。
11.9.1 運(yùn)行 TensorBoard
默認(rèn)情況下,我們的訓(xùn)練腳本將指標(biāo)數(shù)據(jù)寫(xiě)入 runs/ 子目錄。如果在 Bash shell 會(huì)話期間列出目錄內(nèi)容,您可能會(huì)看到類似于以下內(nèi)容:
$ ls -lA runs/p2ch11/
total 24
drwxrwxr-x 2 elis elis 4096 Sep 15 13:22 2020-01-01_12.55.27-trn-dlwpt/ # ?
drwxrwxr-x 2 elis elis 4096 Sep 15 13:22 2020-01-01_12.55.27-val-dlwpt/ # ?
drwxrwxr-x 2 elis elis 4096 Sep 15 15:14 2020-01-01_13.31.23-trn-dwlpt/ # ?
drwxrwxr-x 2 elis elis 4096 Sep 15 15:14 2020-01-01_13.31.23-val-dwlpt/ # ?
? 之前的單次運(yùn)行
? 最近的 10 次訓(xùn)練運(yùn)行
要獲取 tensorboard
程序,請(qǐng)安裝 tensorflow
(pypi.org/project/tensorflow
) Python 包。由于我們實(shí)際上不會(huì)使用 TensorFlow 本身,所以如果您安裝默認(rèn)的僅 CPU 包也是可以的。如果您已經(jīng)安裝了另一個(gè)版本的 TensorBoard,那也沒(méi)問(wèn)題。確保適當(dāng)?shù)哪夸浽谀穆窂缴?#xff0c;或者使用 ../path/to/tensorboard --logdir runs/
來(lái)調(diào)用它。從哪里調(diào)用它并不重要,只要您使用 --logdir
參數(shù)將其指向存儲(chǔ)數(shù)據(jù)的位置即可。最好將數(shù)據(jù)分隔到單獨(dú)的文件夾中,因?yàn)橐坏┻M(jìn)行了 10 或 20 次實(shí)驗(yàn),TensorBoard 可能會(huì)變得有些難以管理。您將不得不在每個(gè)項(xiàng)目中決定最佳的做法。如果需要,隨時(shí)移動(dòng)數(shù)據(jù)也是個(gè)好主意。
現(xiàn)在讓我們開(kāi)始 TensorBoard 吧:
$ tensorboard --logdir runs/
2020-01-01 12:13:16.163044: I tensorflow/core/platform/cpu_feature_guard.cc:140]# ?Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 FMA 1((CO17-2))
TensorBoard 1.14.0 at http://localhost:6006/ (Press CTRL+C to quit)
? 這些消息可能對(duì)您來(lái)說(shuō)是不同的或不存在的;這沒(méi)關(guān)系。
完成后,您應(yīng)該能夠?qū)g覽器指向 http://localhost:6006 并查看主儀表板。圖 11.10 展示了這是什么樣子。
圖 11.10 主要的 TensorBoard 用戶界面,顯示了一對(duì)訓(xùn)練和驗(yàn)證運(yùn)行
在瀏覽器窗口的頂部,您應(yīng)該看到橙色的標(biāo)題。標(biāo)題的右側(cè)有用于設(shè)置的典型小部件,一個(gè)指向 GitHub 存儲(chǔ)庫(kù)的鏈接等。我們現(xiàn)在可以忽略這些。標(biāo)題的左側(cè)有我們提供的數(shù)據(jù)類型的項(xiàng)目。您至少應(yīng)該有以下內(nèi)容:
-
標(biāo)量(默認(rèn)選項(xiàng)卡)
-
直方圖
-
精確-召回曲線(顯示為 PR 曲線)
您可能還會(huì)看到分布,以及圖 11.10 中標(biāo)量右側(cè)的第二個(gè) UI 選項(xiàng)卡。我們這里不會(huì)使用或討論這些。確保您已經(jīng)通過(guò)單擊選擇了標(biāo)量。
左側(cè)是一組用于顯示選項(xiàng)的控件,以及當(dāng)前存在的運(yùn)行列表。如果您的數(shù)據(jù)特別嘈雜,平滑選項(xiàng)可能會(huì)很有用;它會(huì)使事情變得平靜,這樣您就可以找出整體趨勢(shì)。原始的非平滑數(shù)據(jù)仍然以相同顏色的淡線的形式顯示在背景中。圖 11.11 展示了這一點(diǎn),盡管在黑白打印時(shí)可能難以辨認(rèn)。
圖 11.11 帶有平滑設(shè)置為 0.6 和選擇了兩個(gè)運(yùn)行以顯示的 TensorBoard 側(cè)邊欄
根據(jù)您運(yùn)行訓(xùn)練腳本的次數(shù),您可能有多個(gè)運(yùn)行可供選擇。如果呈現(xiàn)的運(yùn)行太多,圖表可能會(huì)變得過(guò)于嘈雜,所以不要猶豫在目前不感興趣的情況下取消選擇運(yùn)行。
如果你想永久刪除一個(gè)運(yùn)行,可以在 TensorBoard 運(yùn)行時(shí)從磁盤(pán)中刪除數(shù)據(jù)。您可以這樣做來(lái)擺脫崩潰、有錯(cuò)誤、不收斂或太舊不再有趣的實(shí)驗(yàn)。運(yùn)行的數(shù)量可能會(huì)增長(zhǎng)得相當(dāng)快,因此經(jīng)常修剪并重命名運(yùn)行或?qū)⑻貏e有趣的運(yùn)行移動(dòng)到更永久的目錄以避免意外刪除是有幫助的。要?jiǎng)h除train
和validation
運(yùn)行,執(zhí)行以下操作(在更改章節(jié)、日期和時(shí)間以匹配要?jiǎng)h除的運(yùn)行之后):
$ rm -rf runs/p2ch11/2020-01-01_12.02.15_*
請(qǐng)記住,刪除運(yùn)行將導(dǎo)致列表中后面的運(yùn)行向上移動(dòng),這將導(dǎo)致它們被分配新的顏色。
好的,讓我們來(lái)談?wù)?TensorBoard 的要點(diǎn):漂亮的圖表!屏幕的主要部分應(yīng)該填滿了從收集訓(xùn)練和驗(yàn)證指標(biāo)中得到的數(shù)據(jù),如圖 11.12 所示。
圖 11.12 主要的 TensorBoard 數(shù)據(jù)顯示區(qū)域向我們展示了我們?cè)趯?shí)際結(jié)節(jié)上的結(jié)果非常糟糕
比起E1 trn_pos 924.34 loss, 0.2% correct (3 of 1215)
,這樣解析和吸收起來(lái)要容易得多!雖然我們將把討論這些圖表告訴我們的內(nèi)容保存到第 11.10 節(jié),現(xiàn)在是一個(gè)好時(shí)機(jī)確保清楚這些數(shù)字對(duì)應(yīng)我們的訓(xùn)練程序中的內(nèi)容?;c(diǎn)時(shí)間交叉參考你通過(guò)鼠標(biāo)懸停在線條上得到的數(shù)字和訓(xùn)練.py 在同一訓(xùn)練運(yùn)行期間輸出的數(shù)字。你應(yīng)該看到工具提示的值列和訓(xùn)練期間打印的值之間有直接對(duì)應(yīng)關(guān)系。一旦你對(duì) TensorBoard 顯示的內(nèi)容感到舒適和自信,讓我們繼續(xù)討論如何讓這些數(shù)字首次出現(xiàn)。
11.9.2 將 TensorBoard 支持添加到度量記錄函數(shù)
我們將使用torch.utils.tensorboard
模塊以 TensorBoard 可消費(fèi)的格式編寫(xiě)數(shù)據(jù)。這將使我們能夠快速輕松地為此項(xiàng)目和任何其他項(xiàng)目編寫(xiě)指標(biāo)。TensorBoard 支持 NumPy 數(shù)組和 PyTorch 張量的混合使用,但由于我們沒(méi)有將數(shù)據(jù)放入 NumPy 數(shù)組的理由,我們將專門(mén)使用 PyTorch 張量。
我們需要做的第一件事是創(chuàng)建我們的SummaryWriter
對(duì)象(我們從torch.utils.tensorboard
導(dǎo)入)。我們將傳入的唯一參數(shù)初始化為類似runs/p2ch11/2020-01-01_12 .55.27-trn-dlwpt
的內(nèi)容。我們可以在我們的訓(xùn)練腳本中添加一個(gè)注釋參數(shù),將dlwpt
更改為更具信息性的內(nèi)容;使用python -m p2ch11.training --help
獲取更多信息。
我們創(chuàng)建兩個(gè)寫(xiě)入器,一個(gè)用于訓(xùn)練運(yùn)行,一個(gè)用于驗(yàn)證運(yùn)行。這些寫(xiě)入器將在每個(gè)時(shí)代重復(fù)使用。當(dāng)SummaryWriter
類被初始化時(shí),它還會(huì)作為副作用創(chuàng)建log_dir
目錄。如果訓(xùn)練腳本在寫(xiě)入任何數(shù)據(jù)之前崩潰,這些目錄將顯示在 TensorBoard 中,并且可能會(huì)用空運(yùn)行雜亂 UI,這在你嘗試某些東西時(shí)很常見(jiàn)。為了避免寫(xiě)入太多空的垃圾運(yùn)行,我們等到準(zhǔn)備好第一次寫(xiě)入數(shù)據(jù)時(shí)才實(shí)例化SummaryWriter
對(duì)象。這個(gè)函數(shù)從logMetrics()
中調(diào)用。
列表 11.21 training.py:127,.initTensorboardWriters
def initTensorboardWriters(self):if self.trn_writer is None:log_dir = os.path.join('runs', self.cli_args.tb_prefix, self.time_str)self.trn_writer = SummaryWriter(log_dir=log_dir + '-trn_cls-' + self.cli_args.comment)self.val_writer = SummaryWriter(log_dir=log_dir + '-val_cls-' + self.cli_args.comment)
如果你回憶起來(lái),第一個(gè)時(shí)代有點(diǎn)混亂,訓(xùn)練循環(huán)中的早期輸出基本上是隨機(jī)的。當(dāng)我們保存來(lái)自第一批次的指標(biāo)時(shí),這些隨機(jī)結(jié)果最終會(huì)使事情有點(diǎn)偏斜。從圖 11.11 中可以看出,TensorBoard 具有平滑功能,可以消除趨勢(shì)線上的噪音,這在一定程度上有所幫助。
另一種方法可能是在第一個(gè) epoch 的訓(xùn)練數(shù)據(jù)中完全跳過(guò)指標(biāo),盡管我們的模型訓(xùn)練速度足夠快,仍然有必要查看第一個(gè) epoch 的結(jié)果。隨意根據(jù)需要更改此行為;第 2 部分的其余部分將繼續(xù)采用包括第一個(gè)嘈雜訓(xùn)練 epoch 的模式。
提示 如果你最終進(jìn)行了許多實(shí)驗(yàn),導(dǎo)致異?;蛳鄬?duì)快速終止訓(xùn)練腳本,你可能會(huì)留下許多垃圾運(yùn)行,混亂了你的 runs/目錄。不要害怕清理它們!
向 TensorBoard 寫(xiě)入標(biāo)量
寫(xiě)入標(biāo)量很簡(jiǎn)單。我們可以取出已經(jīng)構(gòu)建的metrics_dict
,并將每個(gè)鍵值對(duì)傳遞給writer.add_scalar
方法。torch.utils.tensorboard.SummaryWriter
類具有add_scalar
方法( mng.bz/RAqj
),具有以下簽名。
代碼清單 11.22 PyTorch torch/utils/tensorboard/writer.py:267
def add_scalar(self, tag, scalar_value, global_step=None, walltime=None):# ...
tag
參數(shù)告訴 TensorBoard 我們要向哪個(gè)圖形添加值,scalar_value
參數(shù)是我們數(shù)據(jù)點(diǎn)的 Y 軸值。global_step
參數(shù)充當(dāng) X 軸值。
請(qǐng)記住,我們?cè)?code>doTraining函數(shù)內(nèi)更新了totalTrainingSamples_count
變量。我們將通過(guò)將其作為global_step
參數(shù)傳入來(lái)將totalTrainingSamples_count
用作我們 TensorBoard 圖表的 X 軸。以下是我們代碼中的示例。
代碼清單 11.23 training.py:323,LunaTrainingApp.logMetrics
for key, value in metrics_dict.items():writer.add_scalar(key, value, self.totalTrainingSamples_count)
請(qǐng)注意,我們鍵名中的斜杠(例如'loss/all'
)導(dǎo)致 TensorBoard 通過(guò)斜杠前的子字符串對(duì)圖表進(jìn)行分組。
文檔建議我們應(yīng)該將 epoch 數(shù)作為global_step
參數(shù)傳入,但這會(huì)導(dǎo)致一些復(fù)雜性。通過(guò)使用向網(wǎng)絡(luò)呈現(xiàn)的訓(xùn)練樣本數(shù),我們可以做一些事情,比如改變每個(gè) epoch 的樣本數(shù),仍然能夠?qū)⑽磥?lái)的圖形與我們現(xiàn)在創(chuàng)建的圖形進(jìn)行比較。如果每個(gè) epoch 花費(fèi)的時(shí)間是四倍長(zhǎng),那么說(shuō)一個(gè)模型在一半的 epoch 中訓(xùn)練是沒(méi)有意義的!請(qǐng)記住,這可能不是標(biāo)準(zhǔn)做法;然而,預(yù)計(jì)會(huì)看到各種值用于全局步驟。
11.10 為什么模型無(wú)法學(xué)習(xí)檢測(cè)結(jié)節(jié)?
我們的模型顯然在學(xué)習(xí)某些東西–隨著 epoch 增加,損失趨勢(shì)線是一致的,結(jié)果是可重復(fù)的。然而,模型正在學(xué)習(xí)的內(nèi)容與我們希望它學(xué)習(xí)的內(nèi)容之間存在分歧。發(fā)生了什么?讓我們用一個(gè)簡(jiǎn)單的比喻來(lái)說(shuō)明問(wèn)題。
想象一下,一位教授給學(xué)生一份包含 100 個(gè)真/假問(wèn)題的期末考試。學(xué)生可以查閱這位教授過(guò)去 30 年的考試版本,每次只有一個(gè)或兩個(gè)問(wèn)題的答案是 True。其他 98 或 99 個(gè)問(wèn)題每次都是 False。
假設(shè)分?jǐn)?shù)不是按曲線劃分的,而是有一個(gè)典型的 90%正確或更高為 A 的等級(jí)刻度,便很容易獲得 A+:只需將每個(gè)問(wèn)題標(biāo)記為 False!讓我們想象今年只有一個(gè) True 答案。像圖 11.13 中左側(cè)的學(xué)生那樣毫無(wú)頭緒地將每個(gè)答案標(biāo)記為 False 的學(xué)生會(huì)在期末考試中得到 99%的分?jǐn)?shù),但實(shí)際上并沒(méi)有證明他們學(xué)到了什么(除了如何從舊測(cè)試中臨時(shí)抱佛腳)。這基本上就是我們的模型目前正在做的事情。
圖 11.13 一位教授給予兩名學(xué)生相同的分?jǐn)?shù),盡管知識(shí)水平不同。問(wèn)題 9 是唯一一個(gè)答案為 True 的問(wèn)題。
將其與右側(cè)學(xué)生進(jìn)行對(duì)比,右側(cè)學(xué)生也回答了 99%的問(wèn)題,但是通過(guò)回答兩個(gè)問(wèn)題為 True 來(lái)實(shí)現(xiàn)。直覺(jué)告訴我們,圖 11.13 中右側(cè)的學(xué)生可能比所有回答為 False 的學(xué)生更好地掌握了材料。在只有一個(gè)錯(cuò)誤答案的情況下找到一個(gè)正確答案是相當(dāng)困難的!不幸的是,我們的學(xué)生分?jǐn)?shù)和我們模型的評(píng)分方案都沒(méi)有反映這種直覺(jué)。
我們有一個(gè)類似的情況,99.7%的“這個(gè)候選人是結(jié)節(jié)嗎?”的答案是“不是”。我們的模型正在采取簡(jiǎn)單的方式,對(duì)每個(gè)問(wèn)題都回答 False。
然而,如果我們更仔細(xì)地查看模型的數(shù)字,訓(xùn)練集和驗(yàn)證集上的損失是在減少!我們?cè)诎┌Y檢測(cè)問(wèn)題上取得任何進(jìn)展都應(yīng)該給我們帶來(lái)希望。下一章的工作將是實(shí)現(xiàn)這一潛力。我們將在第十二章開(kāi)始時(shí)介紹一些新的相關(guān)術(shù)語(yǔ),然后我們將提出一個(gè)更好的評(píng)分方案,不像我們迄今為止所做的那樣容易被操縱。
11.11 結(jié)論
本章我們走了很長(zhǎng)的路–我們現(xiàn)在有了一個(gè)模型和一個(gè)訓(xùn)練循環(huán),并且能夠使用我們?cè)谏弦徽轮猩傻臄?shù)據(jù)。我們的指標(biāo)不僅被記錄在控制臺(tái)上,還以圖形方式呈現(xiàn)。
雖然我們的結(jié)果還不能使用,但實(shí)際上我們比看起來(lái)更接近。在第十二章中,我們將改進(jìn)用于跟蹤進(jìn)度的指標(biāo),并利用它們來(lái)指導(dǎo)我們需要做出的改變,以使我們的模型產(chǎn)生合理的結(jié)果。
11.12 練習(xí)
-
實(shí)現(xiàn)一個(gè)程序,通過(guò)將
LunaDataset
實(shí)例包裝在DataLoader
實(shí)例中來(lái)迭代,同時(shí)計(jì)時(shí)完成此操作所需的時(shí)間。將這些時(shí)間與第十章練習(xí)中的時(shí)間進(jìn)行比較。在運(yùn)行腳本時(shí)要注意緩存的狀態(tài)。-
將
num_workers=...
設(shè)置為 0、1 和 2 會(huì)產(chǎn)生什么影響? -
在給定
batch_size=...
和num_workers=...
組合下,您的機(jī)器支持的最高值是多少,而不會(huì)耗盡內(nèi)存?
-
-
顛倒
noduleInfo_list
的排序順序。在訓(xùn)練一個(gè)周期后,模型的行為會(huì)如何改變? -
將
logMetrics
更改為修改在 TensorBoard 中使用的運(yùn)行和鍵的命名方案。-
嘗試不同的斜杠放置方式,將鍵傳遞給
writer.add_scalar
。 -
讓訓(xùn)練和驗(yàn)證運(yùn)行使用相同的寫(xiě)入器,并在鍵的名稱中添加
trn
或val
字符串。 -
自定義日志目錄和鍵的命名以適應(yīng)您的口味。
-
11.13 總結(jié)
-
數(shù)據(jù)加載器可以在多個(gè)進(jìn)程中從任意數(shù)據(jù)集加載數(shù)據(jù)。這使得否則空閑的 CPU 資源可以用于準(zhǔn)備數(shù)據(jù)以供 GPU 使用。
-
數(shù)據(jù)加載器從數(shù)據(jù)集中加載多個(gè)樣本并將它們整理成一個(gè)批次。PyTorch 模型期望處理數(shù)據(jù)批次,而不是單個(gè)樣本。
-
數(shù)據(jù)加載器可以通過(guò)改變個(gè)別樣本的相對(duì)頻率來(lái)操作任意數(shù)據(jù)集。這允許對(duì)數(shù)據(jù)集進(jìn)行“售后”調(diào)整,盡管直接更改數(shù)據(jù)集實(shí)現(xiàn)可能更合理。
-
我們將在第二部分中使用 PyTorch 的
torch.optim.SGD
(隨機(jī)梯度下降)優(yōu)化器,學(xué)習(xí)率為 0.001,動(dòng)量為 0.99。這些值也是許多深度學(xué)習(xí)項(xiàng)目的合理默認(rèn)值。 -
我們用于分類的初始模型將與第八章中使用的模型非常相似。這讓我們可以開(kāi)始使用一個(gè)我們有理由相信會(huì)有效的模型。如果我們認(rèn)為模型設(shè)計(jì)是阻止項(xiàng)目表現(xiàn)更好的原因,我們可以重新審視模型設(shè)計(jì)。
-
訓(xùn)練過(guò)程中監(jiān)控的指標(biāo)選擇很重要。很容易不小心選擇那些對(duì)模型表現(xiàn)誤導(dǎo)性的指標(biāo)。使用樣本分類正確的整體百分比對(duì)我們的數(shù)據(jù)沒(méi)有用處。第十二章將詳細(xì)介紹如何評(píng)估和選擇更好的指標(biāo)。
-
TensorBoard 可以用來(lái)直觀顯示各種指標(biāo)。這使得消化某些形式的信息(特別是趨勢(shì)數(shù)據(jù))在每個(gè)訓(xùn)練周期中發(fā)生變化時(shí)更容易。
1 任何 shell 都可以,但如果你使用的是非 Bash shell,你已經(jīng)知道這一點(diǎn)。
2 請(qǐng)記住,盡管是 2D 圖,但我們實(shí)際上是在 3D 中工作。
3 這就是為什么下一章有一個(gè)練習(xí)來(lái)嘗試兩者的原因!
? 這樣做有數(shù)值穩(wěn)定性的好處。通過(guò)使用 32 位浮點(diǎn)數(shù)計(jì)算的指數(shù)來(lái)準(zhǔn)確傳播梯度可能會(huì)有問(wèn)題。
? 如果在法國(guó)吃晚餐不涉及機(jī)場(chǎng),可以隨意用“Texas 的巴黎”來(lái)制造笑話;en.wikipedia.org/wiki/Paris_(disambiguation)
。
? 如果你在不同的計(jì)算機(jī)上運(yùn)行訓(xùn)練,你需要用適當(dāng)?shù)闹鳈C(jī)名或 IP 地址替換localhost。