1. 从感知机到多层感知机:为什么它依然是深度学习的基石
如果你刚开始接触机器学习,可能觉得“多层感知机”这个名字听起来有点复古,甚至不如“卷积神经网络”或“Transformer”那么酷炫。但我想告诉你,无论你现在研究的是哪个前沿方向,从图像识别到自然语言处理,其底层最核心的“全连接前馈网络”结构,本质上就是一个多层感知机。它就像乐高积木里的基础砖块,看似简单,却是构建一切复杂模型不可或缺的单元。我从业十多年,从最早用MATLAB的Statistics and Machine Learning Toolbox手搓神经网络,到后来在PyTorch、TensorFlow里构建复杂的模型,发现很多问题的根源,其实都能追溯到对MLP这个基础结构理解不够透彻。
今天,我们不谈那些花哨的变体,就深入聊聊这个“老家伙”——多层感知机。我会结合最新的实践,比如如何利用现代框架高效实现,以及像《Physics-Informed Machine Learning》这类前沿交叉领域如何与MLP结合,帮你彻底搞懂它的原理、实现细节和那些“坑”。无论你是想夯实基础,还是希望优化现有模型,这篇文章都能给你带来直接的参考价值。
2. 多层感知机的核心设计哲学与架构拆解
2.1 从线性到非线性:单层感知机的根本局限
要理解多层感知机,必须从它的前身——单层感知机说起。1958年Frank Rosenblatt提出的感知机模型,本质上是一个线性二分类器。它的数学形式很简单:y = sign(w·x + b)。这里,w是权重向量,x是输入特征,b是偏置,sign是符号函数。它的决策边界是一个超平面。
这个模型的局限性非常明显:它无法解决线性不可分问题,最经典的例子就是“异或”问题。想象一个二维平面上的四个点,(0,0)和(1,1)属于A类,(0,1)和(1,0)属于B类。你无法用一条直线把A类和B类完美分开。这就是单层感知机的“死穴”。当年这个缺陷直接导致了神经网络研究的第一次寒冬。
那么,如何突破这个局限?答案就是引入“多层”结构和“非线性”激活函数。多层结构让网络具备了组合特征的能力,而非线性激活函数则让这种组合不再是简单的线性叠加,从而能够拟合任意复杂的函数。这就是多层感知机设计的核心哲学:通过堆叠简单的线性变换层,并在每层之后施加非线性变换,来逼近任何复杂的映射关系。
2.2 架构深度解析:层、神经元与连接
一个标准的多层感知机通常包含三部分:
- 输入层:不对数据做任何处理,只是将特征向量送入网络。神经元数量等于特征维度。
- 隐藏层:这是MLP的“大脑”,可以有一层或多层。每一层隐藏层都执行两个操作:
- 线性变换:
z = W * a_prev + b。其中,W是权重矩阵,a_prev是上一层的输出(或输入层的输入),b是偏置向量。 - 非线性激活:
a = σ(z)。其中,σ是非线性激活函数,如ReLU、Sigmoid或Tanh。
- 线性变换:
- 输出层:最后一层,其结构和激活函数取决于任务类型。
- 二分类:一个神经元,使用Sigmoid激活函数,输出介于0和1之间的概率值。
- 多分类:神经元数量等于类别数,使用Softmax激活函数,输出每个类别的概率分布。
- 回归:一个或多个神经元,通常不使用激活函数(或使用线性激活),直接输出预测值。
这里有一个关键细节:层与层之间是全连接的,这意味着当前层的每一个神经元,都与前一层的所有神经元有连接。正是这种密集的连接方式,使得MLP具备强大的表示能力,但同时也是其参数爆炸、计算开销大的根源。
注意:在讨论网络深度时,我们通常只计算具有可学习参数(权重和偏置)的层。因此,输入层不计入层数。一个“单隐藏层MLP”指的是:输入层 -> 隐藏层 -> 输出层。
2.3 为什么选择MLP?优势与适用场景再思考
在卷积神经网络和注意力机制大行其道的今天,MLP的价值在哪里?我认为主要体现在以下几个方面:
- 通用近似器:理论上,只要有一个足够大的隐藏层,MLP就可以以任意精度逼近任何连续函数。这为其解决复杂问题提供了理论保障。
- 处理结构化数据的利器:对于表格数据,即特征明确排列成向量的数据(比如金融风控、用户画像、科学实验数据),MLP往往是首选。卷积神经网络擅长捕捉空间局部相关性(如图像),而MLP对特征的位置不敏感,更适合处理这种“扁平化”的数据。
- 作为更大模型的组件:在几乎所有现代深度学习架构中,MLP都扮演着关键角色。Transformer中的前馈网络层、CNN分类器末端的全连接层,本质上都是MLP。理解MLP是理解这些复杂模型的基石。
- 物理信息机器学习的基础:这正是当前的热点之一。在《Physics-Informed Machine Learning》中,研究人员常将物理定律(如偏微分方程)作为约束条件融入损失函数。MLP因其灵活性和强大的函数拟合能力,常被用作求解器的参数化函数。例如,用MLP来直接近似物理场的解,通过训练让网络同时满足观测数据和物理方程。
3. 核心细节:激活函数、权重初始化与损失函数
3.1 激活函数:网络非线性的灵魂
激活函数的选择直接决定了网络的表达能力、训练速度和梯度流动的健康状况。下面我对比几个最常用的激活函数,并分享我的选择经验。
| 激活函数 | 公式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| Sigmoid | σ(x) = 1 / (1 + e^{-x}) | 输出平滑,值域(0,1),易于解释为概率。 | 1.梯度消失:在 | x |
| Tanh | tanh(x) = (e^x - e^{-x}) / (e^x + e^{-x}) | 输出零均值(值域(-1,1)),收敛速度通常比Sigmoid快。 | 同样存在梯度消失问题(但比Sigmoid稍好)。 | 在RNN中仍有应用,但在MLP的隐藏层中已被ReLU系列取代。 |
| ReLU | f(x) = max(0, x) | 1.计算极其高效(只需比较和赋值)。 2. 在正区间梯度恒为1,有效缓解梯度消失。 3. 具有稀疏激活性(约50%的神经元被激活)。 | Dying ReLU问题:输入为负时,梯度为0,神经元可能“死亡”且无法恢复。 | 默认的隐藏层激活函数,适用于大多数情况。 |
| Leaky ReLU | f(x) = max(αx, x)(α为小的正数,如0.01) | 解决了Dying ReLU问题,负区间也有微小梯度。 | 引入了超参数α,需要调优(但通常设为0.01即可)。 | 当担心神经元死亡时使用,效果通常略优于或等同于ReLU。 |
| ELU | x if x>0; α(e^x -1) if x<=0 | 输出接近零均值,可能加快收敛;负饱和区缓解噪声。 | 涉及指数运算,计算量比ReLU大。 | 对噪声鲁棒性要求较高的任务。 |
实操心得:对于绝大多数MLP隐藏层,我的第一选择永远是ReLU。它简单、快速、有效。只有在训练过程中发现大量神经元输出恒为0(即“死亡”)时,我才会考虑切换到Leaky ReLU或ELU。对于输出层,根据任务选择:回归用线性,二分类用Sigmoid,多分类用Softmax。
3.2 权重初始化:训练成功的第一步
糟糕的权重初始化会导致梯度消失或爆炸,使得训练根本无法开始。这里的原则是:保持每一层输出的方差尽可能稳定。
- Xavier/Glorot初始化:这是Sigmoid和Tanh时代的经典方法。它根据输入和输出的神经元数量来缩放初始权重的方差。对于均匀分布,权重从
[-sqrt(6/(fan_in+fan_out)), sqrt(6/(fan_in+fan_out))]中采样。其中fan_in是输入神经元数,fan_out是输出神经元数。 - He初始化:这是为ReLU及其变体设计的,现在更常用。它只考虑
fan_in(输入神经元数)。对于均匀分布,权重从[-sqrt(6/fan_in), sqrt(6/fan_in)]中采样。其核心思想是补偿ReLU将一半神经元的输出置零所带来的方差减半效应。
在现代深度学习框架中,这通常已经默认设置好了。例如,在PyTorch的nn.Linear层中,默认使用Kaiming(He)均匀初始化。但了解其原理至关重要,当你自定义网络层时,必须手动正确初始化。
一个常见的坑:将所有权重初始化为0或相同的常数。这会导致网络中的所有神经元在每一层都学到完全相同的特征,对称性无法被打破,网络能力退化成单神经元。
3.3 损失函数:定义你要优化的目标
损失函数是连接模型输出和真实世界的桥梁,它量化了预测的“错误”程度。
- 均方误差:最常用的回归损失。
MSE = (1/n) * Σ(y_true - y_pred)^2。它对异常值敏感,因为误差被平方了。 - 平均绝对误差:
MAE = (1/n) * Σ|y_true - y_pred|。对异常值比MSE更鲁棒。 - 交叉熵损失:分类任务的绝对主力。对于二分类,是二元交叉熵;对于多分类,是多元交叉熵。它衡量的是模型预测的概率分布与真实标签的分布之间的差异。在PyTorch中,通常将Softmax激活和交叉熵损失合并为
nn.CrossEntropyLoss,这样数值上更稳定。
选择经验:回归任务我通常先尝试MSE,如果数据中有明显异常值且不想让模型过于关注它们,则换用MAE或Huber损失(结合了MSE和MAE)。分类任务无脑用交叉熵。
4. 手把手实现:从零构建与训练一个MLP
理论说再多,不如动手做一遍。这里我将用PyTorch完整演示一个用于Fashion-MNIST图像分类的多层感知机的实现、训练和评估流程。Fashion-MNIST是一个10分类数据集,每张图是28x28的灰度图,非常适合MLP入门。
4.1 数据准备与预处理
首先,我们需要将二维图像“压平”成一维向量,因为MLP处理的是向量输入。
import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader # 定义数据预处理管道 transform = transforms.Compose([ transforms.ToTensor(), # 将PIL图像或numpy数组转换为Tensor,并缩放到[0,1] transforms.Normalize((0.5,), (0.5,)) # 将[0,1]归一化到[-1,1],有助于训练稳定性 ]) # 下载并加载训练集和测试集 train_dataset = datasets.FashionMNIST(root='./data', train=True, download=True, transform=transform) test_dataset = datasets.FashionMNIST(root='./data', train=False, download=True, transform=transform) # 创建数据加载器 batch_size = 64 train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False) # 查看数据形状 for images, labels in train_loader: print(f"Batch image shape: {images.shape}") # torch.Size([64, 1, 28, 28]) print(f"Batch label shape: {labels.shape}") # torch.Size([64]) break预处理的关键一步是Normalize。归一化可以加速模型收敛,并提高训练稳定性。这里我们使用均值为0.5、标准差为0.5进行归一化,使得输入数据大致分布在[-1, 1]区间。
4.2 网络模型定义
现在我们来定义MLP模型。我们将构建一个具有两个隐藏层的网络。
class MLP(nn.Module): def __init__(self, input_size=784, hidden_size1=256, hidden_size2=128, num_classes=10): super(MLP, self).__init__() # 将网络层定义为类属性 self.flatten = nn.Flatten() # 将28x28的图像压平成784的向量 self.fc1 = nn.Linear(input_size, hidden_size1) # 第一隐藏层 self.relu1 = nn.ReLU() self.dropout1 = nn.Dropout(p=0.25) # 添加Dropout防止过拟合 self.fc2 = nn.Linear(hidden_size1, hidden_size2) # 第二隐藏层 self.relu2 = nn.ReLU() self.dropout2 = nn.Dropout(p=0.25) self.fc3 = nn.Linear(hidden_size2, num_classes) # 输出层 # 注意:CrossEntropyLoss内部包含了Softmax,所以这里输出层不需要激活函数 def forward(self, x): x = self.flatten(x) # [batch, 1, 28, 28] -> [batch, 784] x = self.fc1(x) # [batch, 784] -> [batch, 256] x = self.relu1(x) x = self.dropout1(x) x = self.fc2(x) # [batch, 256] -> [batch, 128] x = self.relu2(x) x = self.dropout2(x) x = self.fc3(x) # [batch, 128] -> [batch, 10] return x # 输出的是logits(未归一化的分数) # 实例化模型、损失函数和优化器 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = MLP().to(device) criterion = nn.CrossEntropyLoss() # 内部整合了Softmax和交叉熵 optimizer = optim.Adam(model.parameters(), lr=0.001) # Adam通常比SGD表现更好关键点解析:
nn.Flatten:这是关键一步,将二维图像数据转换为一维向量。对于[batch, 1, 28, 28]的输入,输出形状为[batch, 784]。nn.Dropout:这是一种正则化技术,在训练时随机“丢弃”一部分神经元(将其输出置零),可以防止网络对某些特定的神经元产生过度的依赖,从而减轻过拟合。参数p表示丢弃的概率。注意:Dropout只在训练时启用,在模型评估(验证/测试)时会自动关闭。- 输出层无激活:因为使用了
nn.CrossEntropyLoss,它期望输入的是“logits”(原始分数),内部会进行Softmax和交叉熵计算。这样做比先手动Softmax再算交叉熵数值上更稳定。 - 优化器选择Adam:对于大多数任务,Adam优化器因其自适应学习率特性,通常比传统的随机梯度下降收敛更快、效果更好,是目前的默认选择。
4.3 训练循环与验证
接下来是核心的训练循环。我们会在每个epoch后评估模型在测试集上的准确率。
num_epochs = 15 train_loss_history = [] test_acc_history = [] for epoch in range(num_epochs): # 训练阶段 model.train() # 设置为训练模式(启用Dropout等) running_loss = 0.0 for batch_idx, (images, labels) in enumerate(train_loader): images, labels = images.to(device), labels.to(device) # 前向传播 outputs = model(images) loss = criterion(outputs, labels) # 反向传播与优化 optimizer.zero_grad() # 清空过往梯度,至关重要! loss.backward() # 反向传播计算梯度 optimizer.step() # 根据梯度更新参数 running_loss += loss.item() avg_train_loss = running_loss / len(train_loader) train_loss_history.append(avg_train_loss) # 评估阶段 model.eval() # 设置为评估模式(关闭Dropout等) correct = 0 total = 0 with torch.no_grad(): # 关闭梯度计算,节省内存和计算 for images, labels in test_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, predicted = torch.max(outputs.data, 1) # 获取预测类别 total += labels.size(0) correct += (predicted == labels).sum().item() test_acc = 100 * correct / total test_acc_history.append(test_acc) print(f'Epoch [{epoch+1}/{num_epochs}], Train Loss: {avg_train_loss:.4f}, Test Accuracy: {test_acc:.2f}%') print('Finished Training')训练中的关键操作:
model.train()和model.eval():这两个方法会切换模型的状态,主要影响Dropout和BatchNorm等层的行为。训练时必须用.train(),评估时必须用.eval()。optimizer.zero_grad():PyTorch的梯度是累加的。如果不在每次反向传播前清空梯度,梯度会从上一个批次累积下来,导致更新错误。这是新手最常见的错误之一。with torch.no_grad():在评估时,我们不需要计算梯度(因为不更新参数)。这个上下文管理器可以显著减少内存消耗并加速计算。
运行这个训练脚本,一个简单的两层MLP在Fashion-MNIST上通常能达到88%-90%的测试准确率。这已经是一个不错的基础结果。
4.4 超参数调优实战
模型结构(层数、神经元数)和训练参数(学习率、批大小等)都是超参数。下面是一个简单的超参数搜索思路:
# 示例:尝试不同的学习率和隐藏层大小组合 hidden_sizes = [(128, 64), (256, 128), (512, 256)] learning_rates = [0.1, 0.01, 0.001, 0.0001] best_acc = 0 best_params = {} for hidden in hidden_sizes: for lr in learning_rates: print(f"\nTesting hidden={hidden}, lr={lr}") model = MLP(hidden_size1=hidden[0], hidden_size2=hidden[1]).to(device) optimizer = optim.Adam(model.parameters(), lr=lr) # ... 简化的训练循环(例如只跑3个epoch快速评估) # 记录测试准确率 # if current_acc > best_acc: # best_acc = current_acc # best_params = {'hidden': hidden, 'lr': lr} # print(f"Best params: {best_params}, Best Acc: {best_acc:.2f}%")在实际项目中,更系统的方法是使用网格搜索、随机搜索或贝叶斯优化工具(如Optuna)。但手动尝试几组关键参数,对于理解参数影响非常有益。
我的经验法则:
- 学习率:这是最重要的超参数。从0.001(Adam的常用值)或0.01(SGD的常用值)开始尝试。太大可能导致震荡不收敛,太小则收敛过慢。可以使用学习率调度器(如
StepLR或ReduceLROnPlateau)动态调整。 - 批大小:常用的有32、64、128、256。更大的批大小使梯度估计更准确,但需要更多内存,且可能收敛到尖锐的极小值。更小的批大小有正则化效果,可能泛化更好,但训练更慢。我通常从64或128开始。
- 网络深度与宽度:对于Fashion-MNIST这类问题,1-3个隐藏层通常足够。宽度(神经元数)比深度更重要。可以先从一个较宽的浅层网络开始(如单层512个神经元),如果欠拟合再增加深度。
5. 高级话题:MLP的优化、正则化与物理信息融合
5.1 应对过拟合:超越Dropout的策略
Dropout是有效的,但并非唯一手段。当模型在训练集上表现很好,但在测试集上表现糟糕时,说明出现了过拟合。除了Dropout,还有以下武器:
- L1/L2权重衰减:在优化器中直接加入。L2衰减(也叫权重衰减)是最常见的,它在损失函数中添加了所有权重平方和的一项,惩罚大的权重值,迫使网络学习更简单、更平滑的函数。在PyTorch的优化器中,通过
weight_decay参数设置。optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-5) # L2衰减 - 批量归一化:虽然最初是为CNN设计的,但在MLP的隐藏层后加入
nn.BatchNorm1d也能带来奇效。它通过对每一批数据进行归一化(减均值、除以标准差),使得中间层的输入分布更稳定,从而允许使用更大的学习率,加速训练,并有一定的正则化效果。self.fc1 = nn.Linear(input_size, hidden_size1) self.bn1 = nn.BatchNorm1d(hidden_size1) self.relu1 = nn.ReLU() - 早停:监控验证集上的性能,当性能在连续多个epoch不再提升时,就停止训练。这是最简单也最有效的正则化方法之一,防止模型在训练集上“钻牛角尖”。
- 数据增强:对于图像数据,可以通过旋转、裁剪、翻转等方式人工增加训练数据。对于表格数据,虽然手段有限,但可以通过添加轻微噪声或使用SMOTE等过采样技术来增加多样性。
5.2 梯度问题深度排查:消失与爆炸
尽管ReLU和良好的初始化缓解了梯度问题,但在非常深的MLP中,问题依然可能存在。
- 梯度爆炸:表现为损失值变成NaN,或者权重值变得极大。解决方法:
- 使用梯度裁剪:
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm)。 - 检查并降低学习率。
- 使用更小的权重初始化尺度。
- 使用梯度裁剪:
- 梯度消失:表现为深层网络的权重更新非常缓慢,底层网络的梯度几乎为零。解决方法:
- 使用ReLU及其变体代替Sigmoid/Tanh。
- 使用残差连接(ResNet的思想),让梯度有捷径可走。
- 使用批量归一化层。
一个简单的诊断方法是打印出每一层权重的梯度范数:
for name, param in model.named_parameters(): if param.grad is not None: print(f'{name} gradient norm: {param.grad.norm().item()}')如果发现某一层的梯度范数远小于其他层(如1e-7对比1e-2),就可能存在梯度消失;如果远大于其他层(如100对比1),则可能存在梯度爆炸。
5.3 物理信息机器学习:当MLP遇见科学定律
这是当前非常前沿和火热的方向。传统的MLP是纯粹的数据驱动,而物理信息机器学习旨在将已知的物理定律(通常以偏微分方程PDE的形式)融入到机器学习模型中,从而在数据稀缺的情况下也能得到物理上一致的解。
其核心思想是:将物理方程作为软约束加入到损失函数中。
假设我们想用一个MLPu(x, t; θ)来近似描述某个物理场(如温度场、流体速度场)。我们有两部分数据:
- 观测数据:在少数点
(x_i, t_i)上观测到的场值u_i。 - 物理定律:控制该场的PDE,例如热传导方程:
∂u/∂t - α ∇²u = 0。
我们可以构建一个复合损失函数:
Loss = Loss_data + λ * Loss_pde其中:
Loss_data = MSE(u(x_i, t_i; θ), u_i),衡量模型输出与观测数据的拟合程度。Loss_pde = MSE( ∂u/∂t - α ∇²u, 0 ),衡量模型输出违反物理方程的程度。这里的偏导数可以通过自动微分(如PyTorch的autograd)精确计算。λ是一个超参数,用于平衡两项损失。
实现要点:
- MLP在这里充当了一个“万能函数近似器”,其输入是坐标
(x, t),输出是物理场值u。 - 计算
Loss_pde需要在计算域内采样大量的“残差点”。这些点不需要有观测数据,用于强制网络满足PDE。 - 通过自动微分计算网络输出
u对输入(x, t)的偏导数,以构建PDE残差。
这种方法的好处是,即使观测数据非常稀疏,模型也能依靠物理定律的约束,在整个域内给出合理的预测。这对于许多科学和工程问题(如流体力学、材料科学、地球物理)具有巨大价值。
6. 常见问题、调试技巧与性能优化
6.1 训练过程问题诊断速查表
| 现象 | 可能原因 | 排查与解决方法 |
|---|---|---|
| 损失不下降 | 1. 学习率太小。 2. 网络结构太浅或太窄,欠拟合。 3. 数据预处理错误(如标签错误)。 4. 梯度消失(使用Sigmoid/Tanh的深层网络)。 | 1. 增大学习率,尝试1e-3, 1e-2。 2. 增加网络层数或神经元数量。 3. 检查数据加载和标签映射。 4. 换用ReLU,检查梯度流。 |
| 损失为NaN | 1. 学习率太大,导致梯度爆炸。 2. 数据包含NaN或inf值。 3. 损失函数或网络计算中出现除零或log(0)。 | 1. 大幅降低学习率,使用梯度裁剪。 2. 检查输入数据,进行清洗。 3. 在Softmax或log计算前添加微小epsilon。 |
| 训练集准确率高,测试集准确率低(过拟合) | 1. 模型过于复杂。 2. 训练数据不足。 3. 训练时间太长。 | 1. 增加Dropout率,增强L2权重衰减。 2. 尝试数据增强。 3. 使用早停策略。 |
| 训练波动大,不收敛 | 1. 学习率太大。 2. 批大小太小,梯度估计噪声大。 3. 数据没有打乱或存在异常批次。 | 1. 降低学习率,使用学习率热身或调度。 2. 适当增大批大小。 3. 确保DataLoader的 shuffle=True。 |
| GPU内存溢出 | 1. 批大小太大。 2. 模型参数量太大。 3. 在训练循环中累积了中间张量。 | 1. 减小批大小,使用梯度累积模拟大批次。 2. 简化模型,减少层宽/深度。 3. 使用 with torch.no_grad(),及时释放不需要的张量。 |
6.2 模型性能与推理速度优化
当模型部署到生产环境时,效率和速度至关重要。
- 模型剪枝:移除网络中不重要的权重或神经元。例如,可以将绝对值小的权重置零。PyTorch提供了
torch.nn.utils.prune工具包。 - 量化:将模型的权重和激活从32位浮点数转换为8位整数。这可以大幅减少模型大小和内存占用,并加速推理,但可能会带来轻微的精度损失。使用
torch.quantization。 - 使用更高效的层:例如,用
nn.Linear+nn.ReLU的组合是标准的,但也可以探索nn.BatchNorm1d与nn.ReLU的融合等优化。 - ONNX导出:将PyTorch模型导出为ONNX格式,然后使用专门的推理引擎(如ONNX Runtime, TensorRT)进行部署,这些引擎通常有更深的图优化。
6.3 一个容易被忽略的细节:随机种子
深度学习训练具有随机性(权重初始化、数据打乱、Dropout)。为了结果可复现,固定随机种子是很好的实践。
import torch import numpy as np import random def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed(seed) torch.cuda.manual_seed_all(seed) # if you are using multi-GPU. torch.backends.cudnn.deterministic = True # 可能会降低一些性能 torch.backends.cudnn.benchmark = False set_seed(42)在报告实验结果或调试模型时,固定种子可以确保每次运行得到相同的结果,便于比较不同超参数或模型结构带来的真实影响。
多层感知机作为深度学习最基础、最核心的模型,其重要性怎么强调都不为过。我见过很多工程师和研究者,在追逐最新最热的模型架构时,却因为对基础的全连接层、激活函数、梯度传播理解不深,而在调参和debug时浪费大量时间。把MLP的原理吃透,训练过程中每一个环节的细节都搞清楚,就像是练好了内功,之后再学习任何复杂的模型都会事半功倍。无论是处理传统的表格数据,还是将其作为组件嵌入Transformer,亦或是探索物理信息机器学习这样的前沿交叉领域,扎实的MLP功底都是你不可或缺的武器。