很多想系统入门深度学习的朋友,最先撞上的就是“神经网络”这四个字。我见过太多人兴致勃勃翻开资料,结果被神经元、权重、偏置、激活函数、反向传播这一串术语劝退——每个词单看都懂,连在一起就懵。我当年真正开窍,不是靠背公式,而是把一个小网络从零用代码搭出来,亲眼看着损失一点一点往下掉。所以这一篇作为深度学习系列的第一篇,我打算把神经网络这座“地基”彻底讲明白:不跳过关键步骤,也不堆晦涩公式,尽量说人话。无论你是刚接触机器学习的新人,还是写过传统代码、想搞懂神经网络运行机制的开发者,这篇都适合你。读完你至少能回答三个问题:神经网络到底在算什么?它凭什么能学习?以及训练效果不好时,该从哪里排查。1. 从线性模型到神经元:神经网络到底在算什么1.1 为什么线性模型搞不定异或传统机器学习里最常见的就是线性模型:对输入特征做加权求和,再加一个偏置,输出一个数字。放到二分类问题上,它学到的实际是一条直线或一个超平面作为决策边界。数据能被一条线分开,线性模型就够用;一旦数据不能用一条线分开,线性模型再怎么调参也无能为力。异或(XOR)就是最经典的例子。输入是两个0/1取值,输出规则是:两个输入相同则0,不同则1。把四个样本画在二维平面上,(0,0)和(1,1)属于一类,(0,1)和(1,0)属于另一类,你会发现任何方向上都找不到一条直线能把两类点完全分开。正因如此,一个线性模型在异或任务上几乎永远学不会,损失会停在一个较高的位置不往下走。为什么单层感知机有局限,这大概是最直观的证明了。1.2 一个神经元内部发生什么:加权求和、偏置与激活神经网络的基本单元叫神经元,翻译成大白话就是一个小计算盒子。它接收若干个输入数字,给每个数字配一个权重,做加权求和,再加上一个偏置,最后过一个激活函数。公式写出来是:z = w1x1 + w2x2 + ... + wN*xN + b a = f(z)权重决定了每个输入对结果的影响程度,偏置是一个可调整的底线,激活函数负责把加权和转换成最终输出。这个结构并不神秘,用点菜来类比:你评估一家店,会看菜品、价格、环境,每个维度的权重就是你对它的重视程度;偏置是你的既有偏好——哪怕所有输入都是0,你也有一个默认态度;激活函数则是你把打分转成“要不要去”这个最终决定的那一步。单看一个神经元,它的决策能力本质上就是一条直线,这也是为什么单个神经元也解决不了异或。1.3 偏置不是摆设:它决定决策边界的位置很多人一开始只盯着 wx+b 里的 wx,觉得偏置可有可无。真实情况是:如果没有偏置,加权求和永远过原点。意思是,当所有特征都恰好是0时,无论权重怎么学习,神经元的输出永远不变,决策边界也被钉在原点附近。有偏置以后,决策边界就灵活了。拿sigmoid举例,σ(z)=0.5 发生在 z=0 的地方,也就是 wx+b=0 时,解得 x=-b/w。b 的数值直接决定这条“分界线”落在哪个位置。你想让神经元在 x 大于5 时才激活,就把 b 设在 -5w 附近;没有偏置,这个阈值根本调不出来。所以在网络里,偏置和权重一样都是可以训练的参数,不是装饰品。很多入门推导会把偏置简化掉,方便理解先行计算,但实际训练中必须留它一席之地。1.4 神经元组成层:从散装公式到矩阵运算实际任务里我们不会只用一个神经元。分类有好几个类别,特征也需要多个维度的组合,所以我们会把大量神经元并列排在一起,形成一层。当一层里有多个神经元时,写法就从散装公式变成矩阵运算:z = Wx + bW 的每一行对应一个神经元的一组权重,x 是输入向量,结果 z 是一个向量,每一位对应一个神经元的加权输出。矩阵写法不只是为了省事,它能让现代计算框架用并行的方式高效处理整层神经元。用一个小例子示意。假设输入 x 是三维向量,这层有2个神经元:import numpy as np W = np.array([[0.2, -0.4, 0.1], [0.3, 0.5, -0.2]]) b = np.array([0.1, -0.1]) x = np.array([0.8, 0.6, 0.4]) z = W @ x + b print(z)第一个神经元的加权输出是0.06,第二个是0.36。这两个数再分别过激活函数,才算真正的神经元输出。矩阵乘法一个@就完成了一层计算,这就是后面所有深度学习代码的地基。2. 让多层结构真正有用:激活函数、初始化与网络骨架2.1 激活函数的第一要务:引入非线性如果把神经元直接串起来,却不在出口加任何变换,会发生什么?两层线性变换拼在一起,y = W2(W1x+b1)+b2,展开后还是 (W2W1)x + 一个常数项,本质上依然是一个线性变换。也就是说,网络不管堆多深,能表达的依旧只是直线、平面这类线性关系,和单个神经元没有本质区别。激活函数就是专为打破这个局限而存在的。它在每个神经元的出口做一次非线性变换,让叠加起来的整个函数能拐弯、能拟合各种曲线。没有激活函数的网络是“纸糊的”;有了激活函数,网络才算真正获得表达能力。2.2 sigmoid、tanh、ReLU:该如何选入门阶段只需要记住三个:sigmoid、tanh、ReLU。直接说结论:隐藏层默认用ReLU;输出层做二分类用sigmoid;做多分类用softmax;做回归干脆不用激活函数。激活函数公式输出范围优点常见问题sigmoidσ(x)=1/(1+e^-x)(0,1)平滑,可解释为概率两端饱和、非零中心tanhtanh(x)(-1,1)输出零中心,训练更稳两端仍会饱和ReLUmax(0,x)[0,+∞)计算快,缓解梯度消失神经元可能死亡为什么说sigmoid“非零中心”不好?因为如果一层里所有神经元的输出恒为正,上游梯度乘上这一层的输出符号就容易被单一方向主导,优化路径会来回震荡。ReLU在正区间导数恒为1,不会把梯度越乘越小,所以成了当前隐藏层的默认选择。但ReLU也有自己的问题,后面单独说。2.3 参数初始化:全零初始化的坑与随机初始化全零初始化和相同常数初始化是新手最容易犯的错误。如果一层里所有神经元的权重都一样,前向传播时它们拿到相同输入、算出相同输出,反向传播的梯度也完全相同,于是它们永远保持一模一样地更新。这样的网络等于只有一个有效的神经元,再宽的层也没有意义。所以必须用随机初始化,打破这种对称性。随机到什么程度也有讲究。全都用特别小的数,前向传播虽然能跑,但梯度信号偏弱;用特别大的数,sigmoid或tanh直接进入饱和区,梯度立刻消失。标准做法是让初始权重标准差跟上一层神经元数量挂钩,框架里常见的he、xavier这类初始化就是解决这件事的。你现在不需要背公式,直接用框架默认设置就足够,但要明白“随机”不是随便,背后有梯度传播的考虑。2.4 深度和宽度:先别急着堆层数网络设计里经常听到“深”和“宽”。宽度指某一层放几个神经元,深度指从输入到输出一共多少层。宽的网络能在同一层并行提取很多特征,深的网络能让特征层层组合、逐步抽象。但初学者最容易犯的错是一上来就堆50层。层数越多,训练难度会指数级上升,调参的坑也越深。我的建议是从两到三层开始,先让网络学会任务,再慢慢加深。这个系列后续讲到卷积和残差连接时,会专门展开“深网络到底怎么训练”这个问题。现在你只需要理解:网络表达能力强不等于训练容易,能力只是基础,能不能训出来是另一回事。3. 学习发生的地方:损失函数与反向传播3.1 损失函数:用什么尺子量“错得有多远”网络输出了一串预测数字,怎么判断它好不好?损失函数就是这把尺子。回归问题最常用均方误差(MSE),把所有样本的预测值与真实值之差的平方求平均。平方的作用是让大误差受到更重的惩罚,训练时会更关注那些错得离谱的点。分类问题常用交叉熵,它衡量预测概率分布和真实标签之间的差异,在配合softmax输出时梯度更稳定。一个问题:为什么分类不用准确率直接当损失?准确率是阶梯函数,样本分对了梯度为0,分错了也没有连续的梯度信息可供调整。损失函数必须是可导的,而且梯度要能告诉网络“往哪个方向调、调多少”。准确率做不到这一点,所以只能当评估指标,不能当训练目标。3.2 反向传播的链条:链式法则怎么变成代码反向传播的核心就一句话:把损失对输出层的梯度,从输出层开始沿着计算图一层层往回传,每一步用链式法则乘上当前环节的局部导数。链式法则是这样接起来的:如果损失L受神经元的输出a影响,而a又受加权和z影响,z再受权重w影响,那么dL/dw = (dL/da) × (da/dz) × (dz/dw)看起来有点抽象,但用一个小例子算一遍就通了。下面的例子我们手算单步梯度更新,你会看到这个链条是怎么样一环扣一环的。3.3 一个数字例子:手算单步梯度更新设输入 x=2,权重 w=1,没有偏置,神经元用sigmoid激活,目标值 t=0。先做前向传播:z = w*x = 2 a = σ(2) ≈ 0.8808 L = (a - t)^2 = 0.8808^2 ≈ 0.7758反向传播要算 dL/dw,它就等于三个局部导数的乘积:dL/da = 2*(a-t) = 2×0.8808 = 1.7616 da/dz = a*(1-a) ≈ 0.8808×0.1192 ≈ 0.1050 dz/dw = x = 2 dL/dw = 1.7616 × 0.1050 × 2 ≈ 0.3699然后执行参数更新(学习率 lr=1):w_new = w - lr*(dL/dw) = 1 - 0.3699 = 0.6301。验证一下:新的 w=0.6301,前向传播 z=1.2602,a=σ(1.2602)≈0.7793,loss 变成了0.6073,确实比0.7758小。这个例子虽然只有一个神经元,但链条和真实网络一模一样。真实网络里的每个权重前面可能隔着十几层,梯度就是从损失出发,一层层乘回去的。你调用框架的.backward(),它做的就是这件事。3.4 梯度下降和学习率:下山该怎么迈步有了梯度,参数怎么更新也就定了。把损失函数想象成一个高低起伏的山谷,我们要找到谷底。对某个参数求梯度,得到的是当前点上函数上升最快的方向;要下降,就走反方向,再乘一个步长,这个步长就是学习率。w_new = w - lr * gradient学习率是训练里最敏感的超参数之一。太大,一步跨过谷底,loss来回震荡甚至变成NaN;太小,下山过程慢得让人抓狂。后续系列会讲动量、Adam这类优化器改进,但万变不离其宗,它们都在回答同一个问题:往哪走,走多快。4. 训练中的典型翻车现场:学习率、过拟合与梯度问题4.1 学习率太大太小:从loss曲线读出问题我把训练问题分成“能跑但跑不好”和“根本不能跑”两种,先看前者。最典型的场景是loss曲线像过山车,忽高忽低。每次看到一个阶梯状上升又下降的曲线,优先怀疑学习率太大;如果loss一上来就打出NaN,多半也是震荡太狠或者数据没归一化。反过来,如果loss下降像蜗牛爬,几百步才从0.5降到0.49,那学习率多半太小。标准做法是拿一组学习率做小规模实验:0.1、0.01、0.001、0.0001分别跑一小段,观察loss下降速度。后面会讲到学习率调度器,它会随训练过程自动调小步长,但底层思路和这个实验一致。建议:写代码时把loss打印在一个固定间隔上,比如每100步打印一次。没有loss曲线的调试,都是盲人摸象。4.2 过拟合与欠拟合:把“背题”和“没学会”分开另一个高频问题是过拟合与欠拟合。欠拟合很好认:训练集和验证集的loss都偏高,模型根本还没学会。过拟合比较狡猾:训练loss持续下降,验证loss却在一个节点开始回升,这时候模型已经把训练集里的细节背下来了,换没见过的新样本反而不行。直觉一点说:欠拟合是“没学会老师讲的内容”,过拟合是“把练习册答案都背下来,题改一个字就不会了”。遇到过拟合,常用手段是加数据、减小模型容量、加正则化、早停;遇到欠拟合,反过来加大模型容量或改善输入特征。初学者最容易混淆的一点是:模型在训练集上表现很好,不代表真的学得好,验证集才是更诚实的裁判。4.3 梯度消失与梯度爆炸:网络深了为什么难训网络一旦加深,训练难度急升,核心原因藏在反向传播的连乘过程中。梯度每过一层,就要乘一个激活函数局部导数。sigmoid导数的最大值只有0.25,两端饱和处接近0,连续乘上二三十次,梯度会小到几乎看不见。靠近输入层的权重基本收不到更新信号,这就是梯度消失。反过来,如果某些权重数值偏大,梯度一路被放大,更新一步整个参数就飞了,loss直接飘出天际,这是梯度爆炸。ReLU在正区间导数恒为1,缓解了“连乘越乘越小”的问题,所以成了默认选择。但它的代价是负区间梯度为0,这又引出下面要说的“死神经元”问题。深度网络的训练,本质上就是和这种连乘效应做斗争。4.4 数据归一化:让优化器少走弯路数据没归一化是新手排查时最容易漏掉的原因。假如两个特征的数值范围差几个数量级,一个是0到1,另一个是0到100000,损失函数的等高线会被拉成一个极扁的椭圆形。梯度下降在这种地形上会走之字形,看起来loss在降,其实每一步都很低效。归一化的常见做法是标准化:减去均值,除以标准差,让每个特征都落在0附近、量级接近1;图像数据通常直接除以255,压到0到1之间。代码很简单:mean = X_train.mean(axis=0) std = X_train.std(axis=0) X_train_norm = (X_train - mean) / (std + 1e-8)那个1e-8是防止某列标准差为0时除出无穷大。注意一个细节:mean和std必须用训练集算出来,测试集沿用同一组统计量,不能拿测试集重新算,否则就造成了信息泄漏。4.5 死神经元:隐藏层里的无声罢工ReLU在负数区间梯度恒为0,好处是稀疏、省计算,坏处是如果某次更新让某个神经元的输入长期落在负区间,它的输出就会一直为0,梯度也一直为0,从此这个神经元彻底“罢工”。学习率越大,越容易触发这种局面。症状是网络看起来层数不少,实际有效神经元却在减少,容量虚高、表达能力下降。解决办法很简单:学习率别开太大;如果问题反复出现,可以把隐藏层换成LeakyReLU这类变体,让负区间也保留一条小梯度通道。入门阶段不用纠结,但对训练异常保持警惕,会帮你省下大量排查时间。5. 用numpy从零实现一个小网络:异或任务实战5.1 为什么用异或做第一个实战前面一直拿异或当反面例子,这里就让它当实验对象。特征只有4个样本,答案只有0或1,计算量小到每一步都能手动核对,但它确实需要非线性才能解决。所以它是验证“隐藏层到底有没有用”的最小实验。如果一个小网络能把异或学会,就说明前面讲的层、激活函数、反向传播这套机制是真实有效的。反过来说,如果只用单个神经元,这个任务永远学不会,这正好能让你亲手体会到隐藏层的价值。5.2 前向传播与反向传播:numpy手写全程我们定的网络结构是2-4-1:输入2个特征,隐藏层4个神经元用tanh,输出层1个神经元用sigmoid。tanh输出范围是-1到1,对隐藏层来说通常比sigmoid好用;sigmoid用在最后,把结果压到0到1之间,正好对应标签的0/1。import numpy as np np.random.seed(42) X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float) y = np.array([[0], [1], [1], [0]], dtype=float) input_dim = 2 hidden_dim = 4 output_dim = 1 w1 = np.random.randn(input_dim, hidden_dim) b1 = np.zeros((1, hidden_dim)) w2 = np.random.randn(hidden_dim, output_dim) b2 = np.zeros((1, output_dim)) lr = 1.0 def sigmoid(x): return 1.0 / (1.0 + np.exp(-x)) for step in range(10000): # 前向传播 z1 = X @ w1 + b1 a1 = np.tanh(z1) z2 = a1 @ w2 + b2 a2 = sigmoid(z2) loss = np.mean((a2 - y) ** 2) # 反向传播 dL_da2 = 2.0 * (a2 - y) / len(X) dL_dz2 = dL_da2 * (a2 * (1.0 - a2)) dw2 = a1.T @ dL_dz2 db2 = np.sum(dL_dz2, axis=0, keepdims=True) dL_da1 = dL_dz2 @ w2.T dL_dz1 = dL_da1 * (1.0 - a1 ** 2) dw1 = X.T @ dL_dz1 db1 = np.sum(dL_dz1, axis=0, keepdims=True) w1 -= lr * dw1 b1 -= lr * db1 w2 -= lr * dw2 b2 -= lr * db2 if step % 1000 == 0: print(f"step {step:5d}, loss {loss:.6f}") print("预测结果(阈值0.5):") print((a2 > 0.5).astype(int).flatten()) print("真实标签:", y.astype(int).flatten())代码虽短,但每个环节都对应前面讲过的内容:前向的三行分别计算隐藏层加权和、隐藏层激活、输出层加权和与激活;反向的三段分别对应输出层梯度、向隐藏层回传的梯度、再向输入层回传的梯度。update那里就是前面反复说的 w = w - lr * gradient。如果你跑出来loss下降很慢,把hidden_dim调到8或者lr改成0.5再试,这是完全正常的调参过程。随机初始化不同,收敛速度本来就会不一样。5.3 跑完训练之后:看loss和输出预期你会看到损失从0.25左右一路往下掉,几千步后降到0.01以下,最后的预测结果和真实标签完全一致,也就是[0 1 1 0]。这个结果的含金量在于:它证明了区区几个参数组成的网络,已经能表达一个线性不可分的函数。你手动去调参数很难调出这个效果,但梯度下降自己找到了。这就是“学习”的本来面目——不是玄学,而是一步步沿着梯度方向修正权重。每次看到loss下降,你都可以在心里换算:梯度指了个方向,学习率迈了一小步,参数更新了一点,预测更接近真实了。这套循环,就是整个深度学习的核心循环。5.4 同样的事用PyTorch写,到底省了什么你可能会问:真实项目里没人这样手写反向传播。确实,现代框架把这套机制封装好了。用PyTorch写同样的事情:import torch import torch.nn as nn X = torch.tensor([[0., 0.], [0., 1.], [1., 0.], [1., 1.]]) y = torch.tensor([[0.], [1.], [1.], [0.]]) model = nn.Sequential( nn.Linear(2, 4), nn.Tanh(), nn.Linear(4, 1), nn.Sigmoid() ) loss_fn = nn.MSELoss() optimizer = torch.optim.SGD(model.parameters(), lr=1.0) for step in range(10000): pred = model(X) loss = loss_fn(pred, y) optimizer.zero_grad() loss.backward() optimizer.step() if step % 1000 == 0: print(f"{step:5d}, loss {loss.item():.6f}") print((model(X) > 0.5).int().flatten().tolist())框架版本里少了的那一大段numpy手写梯度,正是.backward()和optimizer.step()替我们做的。所以我一直不建议完全跳过手写阶段直接上框架——哪怕只手写一次,你对反向传播的理解都会扎实得多。以后遇到任何“为什么网络不收敛”的疑难杂症,你都会多一个思考方向,而不是只会换个学习率碰运气。6. 跑通代码之后的调试习惯,以及下一步怎么走6.1 我调试小网络时最常用的三个动作第一,永远先把loss曲线打出来。任何“模型不work”的问题,第一步永远是看loss。loss在下降,问题多半出在后续环节;loss卡住不动,才值得怀疑网络结构或参数设置。别急着改模型,先问数据流有没有问题。第二,固定随机种子。训练几乎全靠随机初始化,不固定种子,每次跑结果都不一样,你根本分不清这次改进是有效还是运气。加一行np.random.seed(...)或torch.manual_seed(...),成本极低,收益极高。第三,先在极小数据上跑通。拿十个样本,跑几十步,如果连这十个样本都过拟合不了,那基本是代码有bug;如果loss能顺利降到很低,说明网络本身没问题,再上全量数据。这个习惯能帮你把“算法问题”和“工程问题”快速分开。6.2 先怀疑数据,再怀疑网络还有一个经常被排到最后才排查的原因:数据本身。标签反了、训练集和验证集切分时有重叠、缺值没处理、特征量纲差太大……这些问题引发的loss异常,出现频率比网络结构问题高得多。我自己调试的顺序是:先看数据的shape和标签对不对,再检查归一化是否一致,然后看网络,最后才动学习率。顺序反过来,往往会浪费大量时间。6.3 下一步怎么走:数学、框架和后续系列这篇作为系列开篇,到这里已经给了你一个完整的基础框架:神经元与层、激活函数与初始化、损失与反向传播、常见训练问题、以及一个能亲手跑通的最小实验。接下来可以做的三件事:一是把线性代数里的矩阵乘法和导数链式法则再巩固一下,这是看懂后面一切材料的地基;二是把PyTorch自动求导机制配合官方入门Demo跑一遍,让框架和原理对上号;三是准备好迎接系列的下一篇——顺着这条线,我们会往卷积网络的方向走。我在反复带新人入门时最深的体会是:神经网络的学习不是神秘事件,它就是梯度信号驱动下的参数调整。把这篇里的每个公式亲手算一遍、每段代码亲手跑一遍,后面再花哨的结构,对你来说都会容易得多。
很多想系统入门深度学习的朋友,最先撞上的就是“神经网络”这四个字。我见过太多人兴致勃勃翻开资料,结果被神经元、权重、偏置、激活函数、反向传播这一串术语劝退——每个词单看都懂,连在一起就懵。我当年真正开窍,不是靠背公式,而是把一个小网络从零用代码搭出来,亲眼看着损失一点一点往下掉。所以这一篇作为深度学习系列的第一篇,我打算把神经网络这座“地基”彻底讲明白:不跳过关键步骤,也不堆晦涩公式,尽量说人话。无论你是刚接触机器学习的新人,还是写过传统代码、想搞懂神经网络运行机制的开发者,这篇都适合你。读完你至少能回答三个问题:神经网络到底在算什么?它凭什么能学习?以及训练效果不好时,该从哪里排查。1. 从线性模型到神经元:神经网络到底在算什么1.1 为什么线性模型搞不定异或传统机器学习里最常见的就是线性模型:对输入特征做加权求和,再加一个偏置,输出一个数字。放到二分类问题上,它学到的实际是一条直线或一个超平面作为决策边界。数据能被一条线分开,线性模型就够用;一旦数据不能用一条线分开,线性模型再怎么调参也无能为力。异或(XOR)就是最经典的例子。输入是两个0/1取值,输出规则是:两个输入相同则0,不同则1。把四个样本画在二维平面上,(0,0)和(1,1)属于一类,(0,1)和(1,0)属于另一类,你会发现任何方向上都找不到一条直线能把两类点完全分开。正因如此,一个线性模型在异或任务上几乎永远学不会,损失会停在一个较高的位置不往下走。为什么单层感知机有局限,这大概是最直观的证明了。1.2 一个神经元内部发生什么:加权求和、偏置与激活神经网络的基本单元叫神经元,翻译成大白话就是一个小计算盒子。它接收若干个输入数字,给每个数字配一个权重,做加权求和,再加上一个偏置,最后过一个激活函数。公式写出来是:z = w1x1 + w2x2 + ... + wN*xN + b a = f(z)权重决定了每个输入对结果的影响程度,偏置是一个可调整的底线,激活函数负责把加权和转换成最终输出。这个结构并不神秘,用点菜来类比:你评估一家店,会看菜品、价格、环境,每个维度的权重就是你对它的重视程度;偏置是你的既有偏好——哪怕所有输入都是0,你也有一个默认态度;激活函数则是你把打分转成“要不要去”这个最终决定的那一步。单看一个神经元,它的决策能力本质上就是一条直线,这也是为什么单个神经元也解决不了异或。1.3 偏置不是摆设:它决定决策边界的位置很多人一开始只盯着 wx+b 里的 wx,觉得偏置可有可无。真实情况是:如果没有偏置,加权求和永远过原点。意思是,当所有特征都恰好是0时,无论权重怎么学习,神经元的输出永远不变,决策边界也被钉在原点附近。有偏置以后,决策边界就灵活了。拿sigmoid举例,σ(z)=0.5 发生在 z=0 的地方,也就是 wx+b=0 时,解得 x=-b/w。b 的数值直接决定这条“分界线”落在哪个位置。你想让神经元在 x 大于5 时才激活,就把 b 设在 -5w 附近;没有偏置,这个阈值根本调不出来。所以在网络里,偏置和权重一样都是可以训练的参数,不是装饰品。很多入门推导会把偏置简化掉,方便理解先行计算,但实际训练中必须留它一席之地。1.4 神经元组成层:从散装公式到矩阵运算实际任务里我们不会只用一个神经元。分类有好几个类别,特征也需要多个维度的组合,所以我们会把大量神经元并列排在一起,形成一层。当一层里有多个神经元时,写法就从散装公式变成矩阵运算:z = Wx + bW 的每一行对应一个神经元的一组权重,x 是输入向量,结果 z 是一个向量,每一位对应一个神经元的加权输出。矩阵写法不只是为了省事,它能让现代计算框架用并行的方式高效处理整层神经元。用一个小例子示意。假设输入 x 是三维向量,这层有2个神经元:import numpy as np W = np.array([[0.2, -0.4, 0.1], [0.3, 0.5, -0.2]]) b = np.array([0.1, -0.1]) x = np.array([0.8, 0.6, 0.4]) z = W @ x + b print(z)第一个神经元的加权输出是0.06,第二个是0.36。这两个数再分别过激活函数,才算真正的神经元输出。矩阵乘法一个@就完成了一层计算,这就是后面所有深度学习代码的地基。2. 让多层结构真正有用:激活函数、初始化与网络骨架2.1 激活函数的第一要务:引入非线性如果把神经元直接串起来,却不在出口加任何变换,会发生什么?两层线性变换拼在一起,y = W2(W1x+b1)+b2,展开后还是 (W2W1)x + 一个常数项,本质上依然是一个线性变换。也就是说,网络不管堆多深,能表达的依旧只是直线、平面这类线性关系,和单个神经元没有本质区别。激活函数就是专为打破这个局限而存在的。它在每个神经元的出口做一次非线性变换,让叠加起来的整个函数能拐弯、能拟合各种曲线。没有激活函数的网络是“纸糊的”;有了激活函数,网络才算真正获得表达能力。2.2 sigmoid、tanh、ReLU:该如何选入门阶段只需要记住三个:sigmoid、tanh、ReLU。直接说结论:隐藏层默认用ReLU;输出层做二分类用sigmoid;做多分类用softmax;做回归干脆不用激活函数。激活函数公式输出范围优点常见问题sigmoidσ(x)=1/(1+e^-x)(0,1)平滑,可解释为概率两端饱和、非零中心tanhtanh(x)(-1,1)输出零中心,训练更稳两端仍会饱和ReLUmax(0,x)[0,+∞)计算快,缓解梯度消失神经元可能死亡为什么说sigmoid“非零中心”不好?因为如果一层里所有神经元的输出恒为正,上游梯度乘上这一层的输出符号就容易被单一方向主导,优化路径会来回震荡。ReLU在正区间导数恒为1,不会把梯度越乘越小,所以成了当前隐藏层的默认选择。但ReLU也有自己的问题,后面单独说。2.3 参数初始化:全零初始化的坑与随机初始化全零初始化和相同常数初始化是新手最容易犯的错误。如果一层里所有神经元的权重都一样,前向传播时它们拿到相同输入、算出相同输出,反向传播的梯度也完全相同,于是它们永远保持一模一样地更新。这样的网络等于只有一个有效的神经元,再宽的层也没有意义。所以必须用随机初始化,打破这种对称性。随机到什么程度也有讲究。全都用特别小的数,前向传播虽然能跑,但梯度信号偏弱;用特别大的数,sigmoid或tanh直接进入饱和区,梯度立刻消失。标准做法是让初始权重标准差跟上一层神经元数量挂钩,框架里常见的he、xavier这类初始化就是解决这件事的。你现在不需要背公式,直接用框架默认设置就足够,但要明白“随机”不是随便,背后有梯度传播的考虑。2.4 深度和宽度:先别急着堆层数网络设计里经常听到“深”和“宽”。宽度指某一层放几个神经元,深度指从输入到输出一共多少层。宽的网络能在同一层并行提取很多特征,深的网络能让特征层层组合、逐步抽象。但初学者最容易犯的错是一上来就堆50层。层数越多,训练难度会指数级上升,调参的坑也越深。我的建议是从两到三层开始,先让网络学会任务,再慢慢加深。这个系列后续讲到卷积和残差连接时,会专门展开“深网络到底怎么训练”这个问题。现在你只需要理解:网络表达能力强不等于训练容易,能力只是基础,能不能训出来是另一回事。3. 学习发生的地方:损失函数与反向传播3.1 损失函数:用什么尺子量“错得有多远”网络输出了一串预测数字,怎么判断它好不好?损失函数就是这把尺子。回归问题最常用均方误差(MSE),把所有样本的预测值与真实值之差的平方求平均。平方的作用是让大误差受到更重的惩罚,训练时会更关注那些错得离谱的点。分类问题常用交叉熵,它衡量预测概率分布和真实标签之间的差异,在配合softmax输出时梯度更稳定。一个问题:为什么分类不用准确率直接当损失?准确率是阶梯函数,样本分对了梯度为0,分错了也没有连续的梯度信息可供调整。损失函数必须是可导的,而且梯度要能告诉网络“往哪个方向调、调多少”。准确率做不到这一点,所以只能当评估指标,不能当训练目标。3.2 反向传播的链条:链式法则怎么变成代码反向传播的核心就一句话:把损失对输出层的梯度,从输出层开始沿着计算图一层层往回传,每一步用链式法则乘上当前环节的局部导数。链式法则是这样接起来的:如果损失L受神经元的输出a影响,而a又受加权和z影响,z再受权重w影响,那么dL/dw = (dL/da) × (da/dz) × (dz/dw)看起来有点抽象,但用一个小例子算一遍就通了。下面的例子我们手算单步梯度更新,你会看到这个链条是怎么样一环扣一环的。3.3 一个数字例子:手算单步梯度更新设输入 x=2,权重 w=1,没有偏置,神经元用sigmoid激活,目标值 t=0。先做前向传播:z = w*x = 2 a = σ(2) ≈ 0.8808 L = (a - t)^2 = 0.8808^2 ≈ 0.7758反向传播要算 dL/dw,它就等于三个局部导数的乘积:dL/da = 2*(a-t) = 2×0.8808 = 1.7616 da/dz = a*(1-a) ≈ 0.8808×0.1192 ≈ 0.1050 dz/dw = x = 2 dL/dw = 1.7616 × 0.1050 × 2 ≈ 0.3699然后执行参数更新(学习率 lr=1):w_new = w - lr*(dL/dw) = 1 - 0.3699 = 0.6301。验证一下:新的 w=0.6301,前向传播 z=1.2602,a=σ(1.2602)≈0.7793,loss 变成了0.6073,确实比0.7758小。这个例子虽然只有一个神经元,但链条和真实网络一模一样。真实网络里的每个权重前面可能隔着十几层,梯度就是从损失出发,一层层乘回去的。你调用框架的.backward(),它做的就是这件事。3.4 梯度下降和学习率:下山该怎么迈步有了梯度,参数怎么更新也就定了。把损失函数想象成一个高低起伏的山谷,我们要找到谷底。对某个参数求梯度,得到的是当前点上函数上升最快的方向;要下降,就走反方向,再乘一个步长,这个步长就是学习率。w_new = w - lr * gradient学习率是训练里最敏感的超参数之一。太大,一步跨过谷底,loss来回震荡甚至变成NaN;太小,下山过程慢得让人抓狂。后续系列会讲动量、Adam这类优化器改进,但万变不离其宗,它们都在回答同一个问题:往哪走,走多快。4. 训练中的典型翻车现场:学习率、过拟合与梯度问题4.1 学习率太大太小:从loss曲线读出问题我把训练问题分成“能跑但跑不好”和“根本不能跑”两种,先看前者。最典型的场景是loss曲线像过山车,忽高忽低。每次看到一个阶梯状上升又下降的曲线,优先怀疑学习率太大;如果loss一上来就打出NaN,多半也是震荡太狠或者数据没归一化。反过来,如果loss下降像蜗牛爬,几百步才从0.5降到0.49,那学习率多半太小。标准做法是拿一组学习率做小规模实验:0.1、0.01、0.001、0.0001分别跑一小段,观察loss下降速度。后面会讲到学习率调度器,它会随训练过程自动调小步长,但底层思路和这个实验一致。建议:写代码时把loss打印在一个固定间隔上,比如每100步打印一次。没有loss曲线的调试,都是盲人摸象。4.2 过拟合与欠拟合:把“背题”和“没学会”分开另一个高频问题是过拟合与欠拟合。欠拟合很好认:训练集和验证集的loss都偏高,模型根本还没学会。过拟合比较狡猾:训练loss持续下降,验证loss却在一个节点开始回升,这时候模型已经把训练集里的细节背下来了,换没见过的新样本反而不行。直觉一点说:欠拟合是“没学会老师讲的内容”,过拟合是“把练习册答案都背下来,题改一个字就不会了”。遇到过拟合,常用手段是加数据、减小模型容量、加正则化、早停;遇到欠拟合,反过来加大模型容量或改善输入特征。初学者最容易混淆的一点是:模型在训练集上表现很好,不代表真的学得好,验证集才是更诚实的裁判。4.3 梯度消失与梯度爆炸:网络深了为什么难训网络一旦加深,训练难度急升,核心原因藏在反向传播的连乘过程中。梯度每过一层,就要乘一个激活函数局部导数。sigmoid导数的最大值只有0.25,两端饱和处接近0,连续乘上二三十次,梯度会小到几乎看不见。靠近输入层的权重基本收不到更新信号,这就是梯度消失。反过来,如果某些权重数值偏大,梯度一路被放大,更新一步整个参数就飞了,loss直接飘出天际,这是梯度爆炸。ReLU在正区间导数恒为1,缓解了“连乘越乘越小”的问题,所以成了默认选择。但它的代价是负区间梯度为0,这又引出下面要说的“死神经元”问题。深度网络的训练,本质上就是和这种连乘效应做斗争。4.4 数据归一化:让优化器少走弯路数据没归一化是新手排查时最容易漏掉的原因。假如两个特征的数值范围差几个数量级,一个是0到1,另一个是0到100000,损失函数的等高线会被拉成一个极扁的椭圆形。梯度下降在这种地形上会走之字形,看起来loss在降,其实每一步都很低效。归一化的常见做法是标准化:减去均值,除以标准差,让每个特征都落在0附近、量级接近1;图像数据通常直接除以255,压到0到1之间。代码很简单:mean = X_train.mean(axis=0) std = X_train.std(axis=0) X_train_norm = (X_train - mean) / (std + 1e-8)那个1e-8是防止某列标准差为0时除出无穷大。注意一个细节:mean和std必须用训练集算出来,测试集沿用同一组统计量,不能拿测试集重新算,否则就造成了信息泄漏。4.5 死神经元:隐藏层里的无声罢工ReLU在负数区间梯度恒为0,好处是稀疏、省计算,坏处是如果某次更新让某个神经元的输入长期落在负区间,它的输出就会一直为0,梯度也一直为0,从此这个神经元彻底“罢工”。学习率越大,越容易触发这种局面。症状是网络看起来层数不少,实际有效神经元却在减少,容量虚高、表达能力下降。解决办法很简单:学习率别开太大;如果问题反复出现,可以把隐藏层换成LeakyReLU这类变体,让负区间也保留一条小梯度通道。入门阶段不用纠结,但对训练异常保持警惕,会帮你省下大量排查时间。5. 用numpy从零实现一个小网络:异或任务实战5.1 为什么用异或做第一个实战前面一直拿异或当反面例子,这里就让它当实验对象。特征只有4个样本,答案只有0或1,计算量小到每一步都能手动核对,但它确实需要非线性才能解决。所以它是验证“隐藏层到底有没有用”的最小实验。如果一个小网络能把异或学会,就说明前面讲的层、激活函数、反向传播这套机制是真实有效的。反过来说,如果只用单个神经元,这个任务永远学不会,这正好能让你亲手体会到隐藏层的价值。5.2 前向传播与反向传播:numpy手写全程我们定的网络结构是2-4-1:输入2个特征,隐藏层4个神经元用tanh,输出层1个神经元用sigmoid。tanh输出范围是-1到1,对隐藏层来说通常比sigmoid好用;sigmoid用在最后,把结果压到0到1之间,正好对应标签的0/1。import numpy as np np.random.seed(42) X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float) y = np.array([[0], [1], [1], [0]], dtype=float) input_dim = 2 hidden_dim = 4 output_dim = 1 w1 = np.random.randn(input_dim, hidden_dim) b1 = np.zeros((1, hidden_dim)) w2 = np.random.randn(hidden_dim, output_dim) b2 = np.zeros((1, output_dim)) lr = 1.0 def sigmoid(x): return 1.0 / (1.0 + np.exp(-x)) for step in range(10000): # 前向传播 z1 = X @ w1 + b1 a1 = np.tanh(z1) z2 = a1 @ w2 + b2 a2 = sigmoid(z2) loss = np.mean((a2 - y) ** 2) # 反向传播 dL_da2 = 2.0 * (a2 - y) / len(X) dL_dz2 = dL_da2 * (a2 * (1.0 - a2)) dw2 = a1.T @ dL_dz2 db2 = np.sum(dL_dz2, axis=0, keepdims=True) dL_da1 = dL_dz2 @ w2.T dL_dz1 = dL_da1 * (1.0 - a1 ** 2) dw1 = X.T @ dL_dz1 db1 = np.sum(dL_dz1, axis=0, keepdims=True) w1 -= lr * dw1 b1 -= lr * db1 w2 -= lr * dw2 b2 -= lr * db2 if step % 1000 == 0: print(f"step {step:5d}, loss {loss:.6f}") print("预测结果(阈值0.5):") print((a2 > 0.5).astype(int).flatten()) print("真实标签:", y.astype(int).flatten())代码虽短,但每个环节都对应前面讲过的内容:前向的三行分别计算隐藏层加权和、隐藏层激活、输出层加权和与激活;反向的三段分别对应输出层梯度、向隐藏层回传的梯度、再向输入层回传的梯度。update那里就是前面反复说的 w = w - lr * gradient。如果你跑出来loss下降很慢,把hidden_dim调到8或者lr改成0.5再试,这是完全正常的调参过程。随机初始化不同,收敛速度本来就会不一样。5.3 跑完训练之后:看loss和输出预期你会看到损失从0.25左右一路往下掉,几千步后降到0.01以下,最后的预测结果和真实标签完全一致,也就是[0 1 1 0]。这个结果的含金量在于:它证明了区区几个参数组成的网络,已经能表达一个线性不可分的函数。你手动去调参数很难调出这个效果,但梯度下降自己找到了。这就是“学习”的本来面目——不是玄学,而是一步步沿着梯度方向修正权重。每次看到loss下降,你都可以在心里换算:梯度指了个方向,学习率迈了一小步,参数更新了一点,预测更接近真实了。这套循环,就是整个深度学习的核心循环。5.4 同样的事用PyTorch写,到底省了什么你可能会问:真实项目里没人这样手写反向传播。确实,现代框架把这套机制封装好了。用PyTorch写同样的事情:import torch import torch.nn as nn X = torch.tensor([[0., 0.], [0., 1.], [1., 0.], [1., 1.]]) y = torch.tensor([[0.], [1.], [1.], [0.]]) model = nn.Sequential( nn.Linear(2, 4), nn.Tanh(), nn.Linear(4, 1), nn.Sigmoid() ) loss_fn = nn.MSELoss() optimizer = torch.optim.SGD(model.parameters(), lr=1.0) for step in range(10000): pred = model(X) loss = loss_fn(pred, y) optimizer.zero_grad() loss.backward() optimizer.step() if step % 1000 == 0: print(f"{step:5d}, loss {loss.item():.6f}") print((model(X) > 0.5).int().flatten().tolist())框架版本里少了的那一大段numpy手写梯度,正是.backward()和optimizer.step()替我们做的。所以我一直不建议完全跳过手写阶段直接上框架——哪怕只手写一次,你对反向传播的理解都会扎实得多。以后遇到任何“为什么网络不收敛”的疑难杂症,你都会多一个思考方向,而不是只会换个学习率碰运气。6. 跑通代码之后的调试习惯,以及下一步怎么走6.1 我调试小网络时最常用的三个动作第一,永远先把loss曲线打出来。任何“模型不work”的问题,第一步永远是看loss。loss在下降,问题多半出在后续环节;loss卡住不动,才值得怀疑网络结构或参数设置。别急着改模型,先问数据流有没有问题。第二,固定随机种子。训练几乎全靠随机初始化,不固定种子,每次跑结果都不一样,你根本分不清这次改进是有效还是运气。加一行np.random.seed(...)或torch.manual_seed(...),成本极低,收益极高。第三,先在极小数据上跑通。拿十个样本,跑几十步,如果连这十个样本都过拟合不了,那基本是代码有bug;如果loss能顺利降到很低,说明网络本身没问题,再上全量数据。这个习惯能帮你把“算法问题”和“工程问题”快速分开。6.2 先怀疑数据,再怀疑网络还有一个经常被排到最后才排查的原因:数据本身。标签反了、训练集和验证集切分时有重叠、缺值没处理、特征量纲差太大……这些问题引发的loss异常,出现频率比网络结构问题高得多。我自己调试的顺序是:先看数据的shape和标签对不对,再检查归一化是否一致,然后看网络,最后才动学习率。顺序反过来,往往会浪费大量时间。6.3 下一步怎么走:数学、框架和后续系列这篇作为系列开篇,到这里已经给了你一个完整的基础框架:神经元与层、激活函数与初始化、损失与反向传播、常见训练问题、以及一个能亲手跑通的最小实验。接下来可以做的三件事:一是把线性代数里的矩阵乘法和导数链式法则再巩固一下,这是看懂后面一切材料的地基;二是把PyTorch自动求导机制配合官方入门Demo跑一遍,让框架和原理对上号;三是准备好迎接系列的下一篇——顺着这条线,我们会往卷积网络的方向走。我在反复带新人入门时最深的体会是:神经网络的学习不是神秘事件,它就是梯度信号驱动下的参数调整。把这篇里的每个公式亲手算一遍、每段代码亲手跑一遍,后面再花哨的结构,对你来说都会容易得多。