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