跑通第一個(gè)神經(jīng)網(wǎng)絡(luò)訓(xùn)練循環(huán))
簡(jiǎn)介這份神經(jīng)網(wǎng)絡(luò)導(dǎo)論課程實(shí)驗(yàn)一代碼面向正在學(xué)習(xí)人工神經(jīng)網(wǎng)絡(luò)基礎(chǔ)的高校學(xué)生與自學(xué)者圍繞Adaline自適應(yīng)線性神經(jīng)元及LMS最小均方誤差學(xué)習(xí)規(guī)則展開幫助讀者通過Matlab實(shí)踐理解權(quán)重更新、誤差收斂與線性分類的基本流程。壓縮包共5個(gè)文件約10KB主要由3個(gè)Matlab腳本和2個(gè)數(shù)據(jù)文件組成腳本承擔(dān)算法實(shí)現(xiàn)、隨機(jī)權(quán)重初始化與樣本隨機(jī)劃分?jǐn)?shù)據(jù)文件保存訓(xùn)練集和測(cè)試集便于直接加載運(yùn)行并觀察訓(xùn)練與評(píng)估結(jié)果。資源已有300人學(xué)習(xí)適合作為課程實(shí)驗(yàn)參考或自學(xué)練手材料。內(nèi)容覆蓋Adaline結(jié)構(gòu)、LMS權(quán)重更新公式、學(xué)習(xí)率選擇、隨機(jī)初始化對(duì)收斂的影響以及訓(xùn)練與測(cè)試集劃分等知識(shí)點(diǎn)讀者可據(jù)此復(fù)現(xiàn)實(shí)驗(yàn)、對(duì)比不同參數(shù)下的誤差變化并加深對(duì)早期神經(jīng)網(wǎng)絡(luò)模型訓(xùn)練邏輯與泛化評(píng)估的理解。1. 從實(shí)驗(yàn) 1 代碼出發(fā)Adaline 的 LMS 是神經(jīng)網(wǎng)絡(luò)最樸素的一次“學(xué)習(xí)”打開神經(jīng)網(wǎng)絡(luò)導(dǎo)論課程的第一份實(shí)驗(yàn)代碼大多數(shù)人的第一反應(yīng)是懵滿屏的循環(huán)、向量和權(quán)重跟課本上那些畫著連線的示意圖根本對(duì)不上號(hào)。這份實(shí)驗(yàn) 1 的代碼要做的事其實(shí)很樸素——讓你親手實(shí)現(xiàn)一個(gè) Adaline 神經(jīng)元用 Adaline 的 LMS 學(xué)習(xí)規(guī)則讓它在幾十輪迭代里自己把權(quán)重調(diào)對(duì)。它解決的正是新手最大的卡點(diǎn)知道神經(jīng)網(wǎng)絡(luò)會(huì)“學(xué)習(xí)”卻不知道“學(xué)習(xí)”這一步到底改了哪個(gè)數(shù)字。適合剛學(xué)完一點(diǎn)線性代數(shù)、會(huì)寫幾行 Python、想真正跑通第一個(gè)神經(jīng)網(wǎng)絡(luò)的零基礎(chǔ)同學(xué)。學(xué)完你拿到的不只是一份能跑的代碼而是一條之后可以反復(fù)復(fù)用的訓(xùn)練循環(huán)骨架。2. 先弄懂一個(gè)概念A(yù)daline 的 LMS 到底在算什么零基礎(chǔ)學(xué)神經(jīng)網(wǎng)絡(luò)最容易掉的坑是一上來就去啃“反向傳播”“梯度消失”這些詞結(jié)果連一個(gè)神經(jīng)元里發(fā)生了什么都沒搞清楚。這一章不碰多層網(wǎng)絡(luò)只把 Adaline 這一個(gè)模型扒開看看完你會(huì)發(fā)現(xiàn)它簡(jiǎn)單到有點(diǎn)可愛——本質(zhì)就是一次加權(quán)求和加一條用來修正權(quán)重的規(guī)則。2.1 一個(gè)神經(jīng)元本質(zhì)是一次加權(quán)投票想象你要判斷一家餐廳好不好吃。你問了三個(gè)朋友甲說好吃乙說一般丙說難吃。但你對(duì)這三個(gè)人的信任程度不一樣甲的口味跟你最像權(quán)重給 0.6乙只給 0.3丙給 0.1。最后你算了個(gè)綜合分0.6×甲 0.3×乙 0.1×丙。這個(gè)綜合分超過某個(gè)門檻你就去否則不去。Adaline 干的正是這件事。它有 n 個(gè)輸入 x? 到 x?n 個(gè)對(duì)應(yīng)的權(quán)重 w? 到 w?把輸入和權(quán)重一一相乘再全部加起來z w?x? w?x? ... w?x? b這里的 b 叫偏置你可以理解成“這個(gè)神經(jīng)元天生樂觀還是悲觀”——它不隨輸入變化是一個(gè)固定的加分項(xiàng)或減分項(xiàng)。在代碼里這個(gè)式子就是一行點(diǎn)積np.dot(w, x)沒有任何神秘的地方。提示神經(jīng)網(wǎng)絡(luò)導(dǎo)論課上畫的那些圓圈和箭頭箭頭代表權(quán)重圓圈代表求和一個(gè)圓圈就是一個(gè)這樣的加權(quán)求和單元。你以后看到再?gòu)?fù)雜的網(wǎng)絡(luò)圖單個(gè)節(jié)點(diǎn)做的還是這一件事。2.2 和感知機(jī)的差別一條線拉直了很多人第一次學(xué)到這里會(huì)問這不就是感知機(jī)嗎差別就在輸出那一步。感知機(jī)算完加權(quán)和后立刻套一個(gè)階躍函數(shù)大于 0 輸出 1小于 0 輸出 ?1。然后拿這個(gè) ±1 去跟正確答案比錯(cuò)了才更新權(quán)重對(duì)了就完全不動(dòng)。Adaline 不一樣它算完加權(quán)和之后不套函數(shù)直接把這個(gè)連續(xù)值 z 拿去和真實(shí)目標(biāo) y 比較用兩者的差去更新權(quán)重分類只是訓(xùn)練完之后順手做的一件事看 z 的正負(fù)號(hào)。這個(gè)差別帶來的后果很實(shí)際感知機(jī)只要分對(duì)了就收工哪怕這個(gè)樣本只是勉強(qiáng)壓線過關(guān)Adaline 會(huì)繼續(xù)把 z 往目標(biāo)值上拉讓“分對(duì)”變成“分得又對(duì)又穩(wěn)”。所以同樣的數(shù)據(jù)、同樣的學(xué)習(xí)率Adaline 的收斂過程通常更平滑遇到噪聲也不至于因?yàn)橐粋€(gè)樣本就把權(quán)重拽飛。用一張表對(duì)比會(huì)更清楚對(duì)比項(xiàng)感知機(jī)Adaline輸出階躍后的 ±1未激活的連續(xù)值 z更新依據(jù)分類對(duì)錯(cuò)連續(xù)誤差 y ? z分對(duì)之后停止更新繼續(xù)縮小誤差學(xué)習(xí)規(guī)則感知機(jī)規(guī)則LMS最小均方規(guī)則記住這張表的第三行它是新手理解兩種模型時(shí)最容易忽略的一點(diǎn)。2.3 LMS 在最小化什么誤差平方和加上一句梯度下降LMS 的全稱是 Least Mean Squares最小均方。名字里的“均方”指的就是它想壓小的東西——預(yù)測(cè)值和真實(shí)值之差的平方。把所有樣本的誤差平方加起來再除以 2這個(gè) 1/2 純粹是為了求導(dǎo)時(shí)把平方的 2 約掉不影響結(jié)果就得到J(w) 1/2 × Σ (y? ? z?)2訓(xùn)練的目標(biāo)就是找到一組 w讓 J(w) 盡量小。怎么找沿著讓 J 下降最快的方向挪一小步這就是梯度下降。對(duì) J 求關(guān)于 w 的偏導(dǎo)會(huì)得到一個(gè)非常干凈的式子?J/?w ?Σ (y? ? z?) x?負(fù)號(hào)表示我們要往梯度的反方向走所以權(quán)重更新寫成w ← w η × (y ? z) × x四個(gè)符號(hào)挨個(gè)解釋?duì)?是學(xué)習(xí)率決定每次挪多大一步y(tǒng) 是真實(shí)目標(biāo)z 是當(dāng)前預(yù)測(cè)x 是這次的輸入向量。整個(gè)式子的意思就是——預(yù)測(cè)低了就順著輸入的方向把權(quán)重調(diào)大一點(diǎn)預(yù)測(cè)高了就往反方向調(diào)一點(diǎn)調(diào)多少由誤差大小和學(xué)習(xí)率共同決定。我第一次推這個(gè)式子的時(shí)候卡了很久后來發(fā)現(xiàn)卡點(diǎn)根本不在求導(dǎo)而在于沒想明白“為什么更新量要乘 x”。你換個(gè)角度想如果某個(gè)輸入 x? 這次是 0那它對(duì)結(jié)果沒貢獻(xiàn)自然也不該背鍋乘上 x? 之后它的權(quán)重就紋絲不動(dòng)輸入越大責(zé)任越大改得越多。這么一看就順了。2.4 入門第一課選它不是因?yàn)樗?jiǎn)單有人會(huì)覺得都學(xué)神經(jīng)網(wǎng)絡(luò)了還搞這么個(gè)線性模型是不是太小兒科。恰恰相反Adaline 是唯一一個(gè)你能把梯度自己手推一遍、并且每一步都能對(duì)上代碼的模型。它身上有三個(gè)特點(diǎn)讓它成為最好的起點(diǎn)。第一沒有隱藏層沒有鏈?zhǔn)椒▌t誤差從輸出到權(quán)重的路徑只有一步你不需要把反向傳播當(dāng)成黑匣子先背下來第二權(quán)重可解釋訓(xùn)練完的 w 里每個(gè)數(shù)字對(duì)應(yīng)一個(gè)特征的重要程度正負(fù)號(hào)代表方向你能拿它跟業(yè)務(wù)常識(shí)對(duì)一對(duì)對(duì)不對(duì)一目了然第三它的訓(xùn)練循環(huán)骨架——算預(yù)測(cè)、算誤差、算梯度、更新權(quán)重——和后面幾十層的網(wǎng)絡(luò)是同一個(gè)骨架只是梯度那一步換成了更復(fù)雜的算法。說句血淚經(jīng)驗(yàn)直接去啃多層網(wǎng)絡(luò)的反向傳播絕大多數(shù)零基礎(chǔ)的人會(huì)在第三天放棄因?yàn)槟悴恢滥切┢珜?dǎo)符號(hào)在算什么。先在 Adaline 上把“誤差驅(qū)動(dòng)權(quán)重”這件事跑通十遍再去學(xué)多層你會(huì)發(fā)現(xiàn)新東西只是量變。3. 跑通第一個(gè)例子30 行代碼讓 Adaline 學(xué)會(huì)與運(yùn)算概念講完就該動(dòng)手了。這一章的目標(biāo)很具體不裝任何深度學(xué)習(xí)框架只用 Python 和 numpy寫出一個(gè)能自己學(xué)會(huì)“與運(yùn)算”的 Adaline并且能看懂它每一步在干什么。整個(gè)過程 30 行左右跑完你會(huì)有一種“原來就這么點(diǎn)東西”的踏實(shí)感。3.1 開跑之前環(huán)境、目錄和一份最小數(shù)據(jù)需要的東西少得可憐Python 3.8 以上加一個(gè) numpy。打開終端執(zhí)行下面這行看到版本號(hào)打印出來就算齊活。# 只需要 numpy不需要 torch / tensorflow pip install numpy python -c import numpy; print(numpy.__version__)如果你不想折騰環(huán)境用任何一個(gè)自帶 numpy 的在線筆記本環(huán)境也一樣能跑代碼完全一致。目錄建議這樣組織圖個(gè)清爽exp1_adaline/ ├── adaline.py # 模型主體 ├── data.py # 數(shù)據(jù)構(gòu)造 └── run.py # 跑起來看結(jié)果數(shù)據(jù)我們手工造四個(gè)樣本兩個(gè)特征。這里用±1 編碼而不是常見的 0/1原因稍后 3.3 節(jié)會(huì)說這個(gè)細(xì)節(jié)是新手最容易忽略、又最容易導(dǎo)致“代碼看著沒錯(cuò)但結(jié)果不對(duì)”的地方。目標(biāo)值是“與運(yùn)算”只有兩個(gè)輸入都是 1 時(shí)輸出 1其余三個(gè)樣本都是 ?1。3.2 第一版完整代碼從初始化到收斂把下面這段存成run.py直接運(yùn)行。import numpy as np # 雙極編碼的四個(gè)樣本第 0 列固定為 1充當(dāng)偏置項(xiàng)的乘數(shù) X np.array([[1, 1, 1], [1, 1, -1], [1, -1, 1], [1, -1, -1]], dtypefloat) # 與運(yùn)算目標(biāo)只有 (1, 1) 輸出 1其余三個(gè)都是 -1 y np.array([1, -1, -1, -1], dtypefloat) eta 0.1 # 學(xué)習(xí)率 w np.zeros(3) # 權(quán)重從 0 開始 max_epochs 30 # 最多掃 30 輪防止死循環(huán) for epoch in range(max_epochs): wrong 0 # 這一輪分錯(cuò)幾個(gè) sse 0.0 # 這一輪的誤差平方和 for xi, yi in zip(X, y): z w xi # 加權(quán)求和注意這里沒有套激活函數(shù) err yi - z # 真實(shí)值減預(yù)測(cè)值負(fù)數(shù)說明預(yù)測(cè)高了 w w eta * err * xi # LMS 更新一行就是全部的核心 sse err ** 2 if (z 0) ! (yi 0): # 用符號(hào)判斷分類是否出錯(cuò) wrong 1 print(f第 {epoch:2d} 輪 誤差平方和 {sse:7.4f} 分錯(cuò) {wrong} 個(gè)) if wrong 0: # 全分對(duì)就提前收工 print(f第 {epoch} 輪起已經(jīng)全部分對(duì)停止訓(xùn)練) break print(學(xué)到的權(quán)重:, np.round(w, 3)) for xi, yi in zip(X, y): z w xi print(f輸入 {xi[1:]} 線性輸出 {z:.3f} 分類 {1 if z 0 else -1} 目標(biāo) {int(yi)})跑起來你會(huì)看到類似這樣的輸出數(shù)值會(huì)隨環(huán)境有些微差異趨勢(shì)一致第 0 輪 誤差平方和 1.7081 分錯(cuò) 2 個(gè) 第 1 輪 誤差平方和 0.5205 分錯(cuò) 0 個(gè) 第 1 輪起已經(jīng)全部分對(duì)停止訓(xùn)練 學(xué)到的權(quán)重: [-0.198 0.178 0.156]第 0 輪結(jié)束后還有兩個(gè)樣本分錯(cuò)第 1 輪就全對(duì)了。這不是巧合也不是代碼寫錯(cuò)了——四個(gè)樣本在二維平面上是線性可分的Adaline 只需要把那條分界線推到正確位置就夠。停下來的時(shí)候權(quán)重還沒收斂到最終解只是“剛好夠用”這個(gè)問題 5.4 節(jié)會(huì)專門講。3.3 逐行拆開訓(xùn)練循環(huán)里其實(shí)只有四個(gè)動(dòng)作把上面那段循環(huán)扒掉所有裝飾剩下的就是四件事這四件事構(gòu)成了所有神經(jīng)網(wǎng)絡(luò)訓(xùn)練的核心算預(yù)測(cè)z w xi一行點(diǎn)積。算誤差err yi - z方向很重要反了就變成梯度上升了。改權(quán)重w w eta * err * xi誤差乘以輸入再乘以學(xué)習(xí)率??辞闆r停全分對(duì)就 break否則繼續(xù)下一輪。第 3 步是整份實(shí)驗(yàn)代碼里唯一真正屬于“學(xué)習(xí)”的部分其余都是準(zhǔn)備工作。你可以試著把這一行注釋掉程序會(huì)原地打轉(zhuǎn) 30 輪權(quán)重永遠(yuǎn)是全 0——這就是“不學(xué)習(xí)”的網(wǎng)絡(luò)長(zhǎng)什么樣?,F(xiàn)在說回編碼的問題。為什么用 ±1 而不是 0/1敲下面幾行試試就明白了import numpy as np # 假設(shè)某個(gè)樣本是 (0, 0)用 0/1 編碼偏置列固定為 1 xi np.array([1.0, 0.0, 0.0]) err 1.0 # 假設(shè)當(dāng)前預(yù)測(cè)是 0目標(biāo) 1 print(0.1 * err * xi) # 輸出 [0.1 0. 0. ]結(jié)果是只有偏置那一維被推動(dòng)了兩個(gè)特征的權(quán)重一動(dòng)不動(dòng)。因?yàn)樗鼈兊妮斎胧?0乘出來就是 0。在 0/1 編碼下一個(gè)樣本“沒有某個(gè)特征”和“某個(gè)特征的權(quán)重不該改”是無法區(qū)分的訓(xùn)練會(huì)變得非常拖沓。換成 ±1 編碼后?1 也能貢獻(xiàn)更新量每個(gè)維度每一輪都在參與調(diào)整收斂快得多。這是入門實(shí)驗(yàn)里最常見的一個(gè)隱性坑很多人抄完代碼跑不收斂問題就出在編碼上。3.4 結(jié)果怎么看三個(gè)必須打印的東西跑通不算完能讀懂輸出才算真的懂了。每次跑完至少要看這三樣。第一樣是收斂輪數(shù)。上面第 1 輪就停了說明數(shù)據(jù)太簡(jiǎn)單。如果你的實(shí)驗(yàn)數(shù)據(jù)收斂得要幾十輪也正常跟學(xué)習(xí)率、初始化和數(shù)據(jù)尺度都有關(guān)系。第二樣是最終權(quán)重。你會(huì)看到第一個(gè)數(shù)偏置項(xiàng)是負(fù)的后兩個(gè)是正的。這完全符合邏輯偏置為負(fù)意味著這個(gè)神經(jīng)元默認(rèn)傾向輸出 ?1要推翻它必須兩個(gè)特征同時(shí)給出正貢獻(xiàn)把 z 頂?shù)?0 以上。這正好就是“與運(yùn)算”的語(yǔ)義。這就是線性模型的可解釋性——權(quán)重不是隨機(jī)數(shù)它講了一個(gè)故事。第三樣是每個(gè)樣本的線性輸出 z。注意不只是看符號(hào)還要看絕對(duì)值。z 0.5 和 z 5.0 符號(hào)相同、分類相同但后者離分界線遠(yuǎn)得多遇到新樣本時(shí)更不容易翻車。這種“離邊界有多遠(yuǎn)”的量后面會(huì)變成支持向量機(jī)里的間隔概念現(xiàn)在先有個(gè)直覺就行。注意如果某個(gè)樣本分對(duì)了但 z 只有 0.001別高興太早這說明它正好踩在分界線上稍微換個(gè)輸入就會(huì)錯(cuò)。真正的收斂應(yīng)該讓每個(gè)樣本都有一定的余量。3.5 動(dòng)手改一改三個(gè)小實(shí)驗(yàn)驗(yàn)證你真的懂了看懂和會(huì)用之間隔著一段距離最快的跨法是動(dòng)手改參數(shù)然后預(yù)測(cè)結(jié)果再看實(shí)際是不是這樣。實(shí)驗(yàn)一改學(xué)習(xí)率。把eta改成 0.001 跑一遍再改成 5.0 跑一遍。前者你會(huì)看到收斂變慢、需要更多輪后者很可能直接震蕩誤差平方和越跑越大。這條經(jīng)驗(yàn)以后調(diào)任何模型都用得上。實(shí)驗(yàn)二改初始化。把w np.zeros(3)換成w np.array([0.5, -0.3, 0.8])重跑。你會(huì)發(fā)現(xiàn)收斂輪數(shù)變了但最終仍然能分對(duì)。這就是線性可分?jǐn)?shù)據(jù)的好處從這個(gè)角度看初始化的影響沒那么玄學(xué)。實(shí)驗(yàn)三改成或運(yùn)算。把 y 改成[1, 1, 1, -1]只要有一個(gè)輸入是 1 就輸出 1重跑看看權(quán)重變成什么樣。你會(huì)發(fā)現(xiàn)偏置變成了正數(shù)兩個(gè)特征權(quán)重仍然是正的——語(yǔ)義又對(duì)上了。三個(gè)實(shí)驗(yàn)加起來不到十分鐘但對(duì)“權(quán)重在表達(dá)什么”這件事的理解比看十頁(yè)教材都實(shí)在。4. 從玩具數(shù)據(jù)到像樣的任務(wù)加上標(biāo)準(zhǔn)化和批量更新四個(gè)樣本的與運(yùn)算跑通只說明你會(huì)寫循環(huán)了。真實(shí)一點(diǎn)的數(shù)據(jù)馬上會(huì)給你上課特征尺度不一樣、目標(biāo)值是連續(xù)的、樣本幾百上千個(gè)逐個(gè)更新的循環(huán)慢得讓人想砸鍵盤。這一章解決這三個(gè)問題改完之后你手上這套代碼就能處理正經(jīng)的回歸任務(wù)了。4.1 為什么輸入尺度一變就翻車先看一個(gè)現(xiàn)象。假設(shè)你要預(yù)測(cè)房?jī)r(jià)兩個(gè)特征房間數(shù)1 到 5和面積30 到 300 平方米。量級(jí)差了 60 倍。這時(shí)候?qū)W習(xí)率設(shè)成 0.1房間數(shù)那維的更新量小得可以忽略面積那維一更新就把權(quán)重甩飛訓(xùn)練過程要么慢如蝸牛要么直接發(fā)散成 NaN。這不是代碼寫錯(cuò)了是梯度的形狀被尺度帶偏了。你可以把損失函數(shù)想象成一口鍋尺度一致的時(shí)候鍋是圓的梯度直直地指向鍋底尺度差太多的時(shí)候鍋被拉成了細(xì)長(zhǎng)的橢圓梯度方向斜得厲害走一步就沖到對(duì)面山壁上來回震蕩。血淚經(jīng)驗(yàn)在這里我見過太多人包括我自己在實(shí)驗(yàn)報(bào)告里寫“模型不收斂”回頭一查數(shù)據(jù)壓根沒做任何預(yù)處理。神經(jīng)網(wǎng)絡(luò)導(dǎo)論的實(shí)驗(yàn)里標(biāo)準(zhǔn)化這一步經(jīng)常被一句話帶過但它是性價(jià)比最高的一個(gè)改動(dòng)。4.2 加一步標(biāo)準(zhǔn)化三行代碼效果立竿見影標(biāo)準(zhǔn)化的做法很樸素對(duì)每個(gè)特征減去均值、除以標(biāo)準(zhǔn)差把它壓成均值 0、方差 1 的分布。import numpy as np rng np.random.default_rng(42) # 固定隨機(jī)種子保證每次跑結(jié)果一樣 n 200 X_raw rng.uniform(0, 1, size(n, 2)) X_raw[:, 1] X_raw[:, 1] * 500 # 故意讓第二個(gè)特征量級(jí)大 500 倍 # 真實(shí)關(guān)系y 主要受第一個(gè)特征影響第二個(gè)特征影響很小 true_w np.array([3.0, -0.02]) y X_raw true_w 1.0 rng.normal(0, 0.1, sizen) # 關(guān)鍵的三行按列求均值、標(biāo)準(zhǔn)差然后整體縮放 mu X_raw.mean(axis0) sigma X_raw.std(axis0) X_std (X_raw - mu) / sigma # 在最左邊補(bǔ)一列 1作為偏置項(xiàng)的乘數(shù)這樣后面不用單獨(dú)處理 b X np.hstack([np.ones((n, 1)), X_std]) print(標(biāo)準(zhǔn)化后每列的均值:, np.round(X[:, 1:].mean(axis0), 6)) print(標(biāo)準(zhǔn)化后每列的標(biāo)準(zhǔn)差:, np.round(X[:, 1:].std(axis0), 6))axis0表示沿著樣本方向聚合也就是“對(duì)每一列單獨(dú)算”。這個(gè)參數(shù)新手經(jīng)常寫錯(cuò)成axis1結(jié)果算出來的是每個(gè)樣本自己的均值標(biāo)準(zhǔn)化完全失效——這也是個(gè)高頻翻車點(diǎn)。np.hstack那一行值得多說一句。它把一列全 1 拼到特征矩陣最左邊這樣偏置 b 就變成了權(quán)重向量的第 0 個(gè)分量更新公式里不用再單獨(dú)寫一項(xiàng)。這是工程上的小技巧幾乎所有入門代碼都這么干看到的時(shí)候不要懵。提示如果以后要?jiǎng)澐钟?xùn)練集和測(cè)試集標(biāo)準(zhǔn)化必須用訓(xùn)練集算出來的 mu 和 sigma 去處理測(cè)試集不能各算各的。否則測(cè)試集的分布信息就提前泄露了評(píng)估結(jié)果會(huì)虛高。4.3 換成連續(xù)目標(biāo)把 Adaline 當(dāng)線性回歸用剛才那段數(shù)據(jù)的目標(biāo) y 是連續(xù)值不再是 ±1。這正是 Adaline 擅長(zhǎng)的場(chǎng)景——它本來輸出的就是連續(xù)值做分類只是順手。把訓(xùn)練循環(huán)改一改損失換成均方誤差w np.zeros(X.shape[1]) # 權(quán)重個(gè)數(shù)等于特征數(shù)含偏置列 eta 0.1 for epoch in range(200): z X w # 一次算出全部 200 個(gè)樣本的預(yù)測(cè)形狀 (200,) err y - z # 殘差向量 w w eta * (X.T err) / n # 批量 LMS把所有樣本的更新量取平均 if epoch % 50 0: mse np.mean(err ** 2) print(f第 {epoch:3d} 輪 均方誤差 {mse:.6f}) print(學(xué)到的權(quán)重:, np.round(w, 3))跑完你會(huì)看到均方誤差快速下降然后趨于平緩。學(xué)到的權(quán)重里第一個(gè)是偏置后兩個(gè)對(duì)應(yīng)兩個(gè)特征——因?yàn)樽隽藰?biāo)準(zhǔn)化它們不再直接對(duì)應(yīng)原始量綱但可以橫向比較重要性第一個(gè)特征的權(quán)重絕對(duì)值明顯大于第二個(gè)跟造數(shù)據(jù)時(shí)設(shè)定的 3.0 和 ?0.02 是對(duì)得上的。這三行代碼里有兩處值得停下來想。第一處是X w把原來逐個(gè)樣本的循環(huán)換成了矩陣乘法200 個(gè)樣本一次算完速度快幾十倍這叫向量化是 numpy 最值得早點(diǎn)養(yǎng)成的習(xí)慣。第二處是X.T err / n它其實(shí)就是把每個(gè)樣本的err * xi加起來再取平均——和 3.2 節(jié)那個(gè)一行更新在數(shù)學(xué)上是同一件事只是從“逐個(gè)別”變成了“一起算”??炊@個(gè)等價(jià)關(guān)系你就跨過了從教學(xué)代碼到工程代碼的那道坎。4.4 調(diào)參的先后順序先學(xué)習(xí)率再初始化最后輪數(shù)新手調(diào)參最常見的做法是三個(gè)參數(shù)一起瞎試試到能跑就收工。這樣即便跑通了你也不知道是誰(shuí)的功勞。有個(gè)更靠譜的順序第一步只動(dòng)學(xué)習(xí)率。固定初始化和輪數(shù)讓 eta 從 0.001、0.01、0.1、1.0 各跑一次記錄每輪的損失。判斷標(biāo)準(zhǔn)很簡(jiǎn)單損失穩(wěn)步下降且沒有劇烈抖動(dòng)就是合適損失上下橫跳甚至變大說明太大損失降得像蝸牛說明太小。標(biāo)準(zhǔn)化之后0.01 到 0.1 通常是個(gè)安全區(qū)間。第二步再動(dòng)初始化。學(xué)習(xí)率定下來之后換幾組初始權(quán)重看收斂輪數(shù)的波動(dòng)。如果波動(dòng)極大說明數(shù)據(jù)可能接近不可分或者學(xué)習(xí)率還是偏大。第三步才考慮輪數(shù)。輪數(shù)不是越多越好Adaline 是線性模型訓(xùn)練太久只會(huì)在最優(yōu)解附近來回微調(diào)甚至因?yàn)閷W(xué)習(xí)率偏大而越過最優(yōu)點(diǎn)。所以別一上來就設(shè) 10000 輪圖安心。5. 新手避坑Adaline 實(shí)驗(yàn)里最容易翻車的 5 個(gè)地方這一章的五條基本都是我和身邊人真真切切踩過的。每條按“現(xiàn)象、原因、解決”寫對(duì)照著看你自己卡在哪一步。5.1 坑一代碼跑通了但換個(gè)輸入就全錯(cuò)現(xiàn)象四個(gè)訓(xùn)練樣本分類 100% 正確你興沖沖拿個(gè)新樣本 (1, 1) 去測(cè)結(jié)果輸出 ?1和預(yù)期相反。原因訓(xùn)練停得太早權(quán)重還停在“勉強(qiáng)分對(duì)”的位置分界線正好貼著某個(gè)樣本。3.2 節(jié)那個(gè)例子第 1 輪就停了權(quán)重只有 ±0.2 的量級(jí)余量非常小。解決把停止條件從“全分對(duì)”改成“連續(xù)若干輪全分對(duì)”或者“誤差平方和小于某個(gè)閾值”比如要求連續(xù) 5 輪wrong 0才停。另一個(gè)辦法是訓(xùn)練時(shí)多跑幾輪不停早事后再看每個(gè)樣本的 z 絕對(duì)值是否都大于 0.5。留余量這件事在真實(shí)項(xiàng)目里比在實(shí)驗(yàn)里重要得多。5.2 坑二損失不降反升幾輪之后變成 nan現(xiàn)象打印出來的誤差平方和一輪比一輪大跑到后面出現(xiàn)nan或者inf。原因九成是學(xué)習(xí)率太大配合沒做標(biāo)準(zhǔn)化的數(shù)據(jù)一次更新就把權(quán)重推到很遠(yuǎn)的地方。剩下的一成是數(shù)據(jù)里有極端值或者標(biāo)簽寫反了。解決先把 eta 除以 10 再跑一遍如果還是發(fā)散檢查數(shù)據(jù)尺度用 4.2 節(jié)的標(biāo)準(zhǔn)化處理。另外把每輪的損失打印出來而不是只看最后一輪這樣你能看到它從第幾輪開始變壞的定位快得多。5.3 坑三權(quán)重一直是 0訓(xùn)練像沒發(fā)生現(xiàn)象跑了 30 輪最后打印出來的權(quán)重還是[0. 0. 0.]或者每輪損失完全一樣。原因幾種可能——eta被寫成了整數(shù)除法比如1/100在某些寫法下直接是 0、更新寫成了w eta * err * xi而不是w w eta * err * xi少了累加每次都被覆蓋、或者xi傳進(jìn)去的是空數(shù)組。解決在循環(huán)里加一句print(w)打印第一輪前后的權(quán)重一眼就能看出是沒更新還是被覆蓋。更新語(yǔ)句記住老規(guī)矩帶加號(hào)。5.4 坑四循環(huán)看著收斂了但每次跑結(jié)果都不一樣現(xiàn)象代碼沒改連跑三次權(quán)重三個(gè)樣有時(shí)候收斂有時(shí)候不收斂。原因初始化用了np.random.randn卻沒設(shè)種子或者數(shù)據(jù)生成部分每次都在隨機(jī)。解決在程序開頭固定rng np.random.default_rng(42)之后所有隨機(jī)操作都走這個(gè) rng別用全局的np.random。實(shí)驗(yàn)代碼最忌諱“結(jié)果不可復(fù)現(xiàn)”寫報(bào)告的時(shí)候你會(huì)感謝自己。5.5 坑五把回歸任務(wù)當(dāng)分類任務(wù)判斷收斂現(xiàn)象做一個(gè)連續(xù)值預(yù)測(cè)的實(shí)驗(yàn)怎么跑都“不收斂”打印出來分錯(cuò)的樣本數(shù)一直是 0但均方誤差很大。原因用if (z 0) ! (yi 0)這種符號(hào)判斷去衡量連續(xù)目標(biāo)的訓(xùn)練效果。連續(xù)目標(biāo)下幾乎所有樣本的符號(hào)都相同這個(gè)指標(biāo)永遠(yuǎn)是 0 分錯(cuò)完全沒有參考價(jià)值。解決分類看分錯(cuò)個(gè)數(shù)和間隔回歸看均方誤差或平均絕對(duì)誤差兩套指標(biāo)別混用。判斷標(biāo)準(zhǔn)跟著任務(wù)類型走這條規(guī)律在以后所有模型上都成立。6. 下一步學(xué)什么從 Adaline 走到多層網(wǎng)絡(luò)的路線圖這份實(shí)驗(yàn) 1 的代碼跑通之后最怕的就是停在原地反復(fù)改學(xué)習(xí)率玩一星期然后不知道下一步干嘛。下面這條路線是我自己走過、也推薦給別人的順序每一階段都給了明確的驗(yàn)收標(biāo)準(zhǔn)你照著往下一格一格跳就行。階段要學(xué)的東西大致投入驗(yàn)收標(biāo)準(zhǔn)一把本次代碼封裝成Adaline類半天能初始化、fit、predict 三個(gè)方法跑通二感知機(jī)學(xué)習(xí)規(guī)則與 Adaline 的對(duì)比半天能說清兩者更新時(shí)機(jī)差在哪三異或問題為什么線性模型做不了一天能畫出兩類點(diǎn)、說明不存在一條直線分開四多層感知機(jī)與反向傳播三到五天能手推兩層網(wǎng)絡(luò)的梯度五用框架復(fù)現(xiàn)同一個(gè)實(shí)驗(yàn)兩天同一份數(shù)據(jù)框架和手寫結(jié)果對(duì)得上第一階段的具體技巧值得展開講。把你現(xiàn)在這堆散在腳本里的代碼收成一個(gè)類class Adaline: def __init__(self, eta0.01, epochs50, seed42): self.eta eta # 學(xué)習(xí)率 self.epochs epochs # 最大輪數(shù) self.rng np.random.default_rng(seed) def fit(self, X, y): self.w self.rng.normal(0, 0.01, X.shape[1]) # 小隨機(jī)數(shù)初始化 self.losses_ [] for _ in range(self.epochs): err y - X self.w self.w self.eta * (X.T err) / len(X) self.losses_.append(float(np.mean(err ** 2))) return self def predict(self, X): return X self.w # 回歸直接返回連續(xù)值別小看這一步。fit里把損失記到losses_里、參數(shù)全部放進(jìn)__init__、fit返回self以便鏈?zhǔn)秸{(diào)用這些習(xí)慣和主流框架的接口是一致的。等你后面換到框架時(shí)會(huì)發(fā)現(xiàn)調(diào)用方式幾乎沒有區(qū)別只是把self.w換成了框架里的參數(shù)對(duì)象。第三階段千萬別跳過。異或問題看著像課后習(xí)題其實(shí)是整個(gè)神經(jīng)網(wǎng)絡(luò)歷史的轉(zhuǎn)折點(diǎn)正是因?yàn)榫€性模型做不了它才有了隱藏層和反向傳播。你自己動(dòng)手把這個(gè)失敗復(fù)現(xiàn)一遍再看多層網(wǎng)絡(luò)就知道那些額外的東西是為了解決什么問題而存在的不是憑空冒出來的。第五階段有個(gè)容易忽略的細(xì)節(jié)用框架復(fù)現(xiàn)時(shí)務(wù)必把隨機(jī)種子、數(shù)據(jù)劃分、標(biāo)準(zhǔn)化參數(shù)全部對(duì)齊否則兩邊結(jié)果對(duì)不上你會(huì)懷疑人生。對(duì)不上是常態(tài)對(duì)上一次之后你對(duì)整個(gè)流程的理解會(huì)上一個(gè)臺(tái)階。從那以后我每次跑新實(shí)驗(yàn)都強(qiáng)制自己先把隨機(jī)種子、數(shù)據(jù)標(biāo)準(zhǔn)化、損失打印這三件事寫死在腳本開頭再開始調(diào)模型——省下來的時(shí)間比多試十個(gè)超參數(shù)加起來都多。這份實(shí)驗(yàn) 1 的代碼就是你練習(xí)這個(gè)習(xí)慣最安全的沙盒跑壞了重來一遍只要十分鐘希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取