

© 2026 async. fork自:GitHub
AI模型导论与推理基础原理。
智能的本质是对各种符号的处理,可以用明确的规则和逻辑表达式来表示。
例如定义 A 为今天是阴天,定义 B 为今天空气很潮湿,定义 C 为今天要下雨,定义 D 为出门要带伞,于是可以得到以下推论:
可以用以上逻辑表达式得出这四个定义之间的关系。从符号主义的视角来看,人脑也是用逻辑表达式来不断处理生活中的信息,无论有多复杂,也都可以使用逻辑表达式来表示。
根据这一思想,早期人工智能研究中诞生了专家系统。专家系统试图把特定领域专家的知识和判断过程转化为计算机可以处理的规则,主要由两部分组成:
MYCIN 是专家系统中最具代表性的案例之一。它由斯坦福大学研究人员在 20 世纪 70 年代开发,主要用于辅助医生判断严重细菌感染,例如菌血症和脑膜炎,并根据患者情况推荐抗生素治疗方案。MYCIN 的知识库中包含数百条由传染病专家总结的规则,系统通过问答方式收集患者的症状、化验结果、感染部位、过敏情况以及体重等信息,然后利用推理机进行分析。
例如,MYCIN 中的规则可以简化理解为:
MYCIN 在推理过程中会为结论附加“确定性因子”,表示系统对判断结果的可信程度,并综合多条规则推荐相应的抗生素和剂量。同时,它还能够解释结论所依据的规则,因此可以看作一个通过终端与医生交互的传染病辅助诊断系统。虽然 MYCIN 由于医疗责任、数据录入和系统部署等问题没有得到大规模应用,但它验证了利用知识库和逻辑规则模拟专家推理的可行性。
因此专家系统也存在以下缺陷:
与符号主义试图把知识写成明确的逻辑规则不同,联结主义认为智能来自大量简单计算单元之间的相互连接。它受到人脑神经系统的启发:单个神经元的功能相对简单,但大量神经元通过连接协同工作后,可以表现出识别、学习、记忆和决策等复杂能力。
在联结主义模型中,知识通常不是以“如果……那么……”规则直接保存,而是分布在神经元之间的连接权重中。系统通过观察训练数据,不断调整这些权重,使相同类型的输入逐渐产生相似的输出。联结主义强调的是让机器从数据中学习合适的连接方式。
感知器由弗兰克·罗森布拉特在 20 世纪 50 年代提出,是最早的人工神经网络模型之一,也是现代神经网络中人工神经元的雏形。早期的 Mark I 感知器已经能够通过训练识别简单的视觉模式。
一个感知器通常包含输入、权重、偏置和激活函数。
对概念的理解,常常是若干个基本特征的组合。感知器接收多个输入特征,并为每个特征分配一个权重。权重表示该特征对最终判断的影响程度:正权重会增强某种判断,负权重则会削弱这种判断。感知器首先将各个输入与对应权重相乘,再把结果相加,并加入偏置项:
随后,计算结果会经过激活函数。如果结果超过设定的阈值,感知器输出 1,表示满足目标类别;否则输出 0,表示不满足目标类别。在训练过程中,感知器会根据预测结果与正确答案之间的误差,不断调整权重和偏置,从而逐渐学会不同输入特征与输出结果之间的关系。
比起专家系统“非黑即白”的逻辑规则,通过引入数值增加的表示能力,可以通过不改变结构只改变参数的方式调整功能,提升了能力提升的潜力。但是也有其缺点:无法解决“异或”问题,异或是常见的逻辑运算,但是其线性不可分,因此无法由单个感知器来解决。
单个感知器无法处理异或问题,那可以换成多层感知器。
隐藏层将异或问题转变为:只要两个条件中有一个成立(但不同时成立),就能在隐藏层获得“1”的信号。在输出层再连接两个隐藏层的输出结果,全部为 1 时输出为 1。至此就解决了异或问题。 多层感知器当然不至于此,可以通过增加感知器的数量及层数,做出更为细致的分类。
每一层感知器输出的特征都可以作为下一层感知器的输入,形成神经网络,多层神经网络相连,也就能够形成深度神经网络。
经典的案例就是使用神经网络识别手写数字,每个图片都是一个 的矩阵,每个图片会输出长度为 10 的向量,代表每种数字可能的概率。隐藏层的层数以及每层神经元的个数为可调节的超参数。
在这里使用以下代码在 MNIST 数据集的基础上训练了一个手写数字识别模型:
智能的本质就是一个函数,让机器自动找到这个函数的过程即机器学习,神经网络是对智能的一种拟合函数。而要评估训练过程中神经网络的效果,需要使用损失函数,即评价拟合函数偏差的函数。神经网络越智能,其在真实数据上的损失就越小。
激活函数的作用是为神经网络引入非线性,使多层网络能够表示复杂关系。早期感知器常使用 sign 或阶跃函数,将输入直接变成 0 或 1,但这类函数在大部分位置的导数为 0,无法通过梯度下降有效更新参数,因此现代神经网络通常使用 ReLU、GELU、Sigmoid、Tanh 等可求导或近似可求导的激活函数。需要注意的是,网络最后一层通常不会直接输出 one-hot 向量,而是输出每个类别的连续分数,例如 [0.7, 0.1, 0.1, 0.1]。在分类任务中,可以使用 argmax 选择分数最高的类别;在语言模型等生成任务中,也可以根据概率分布进行采样。隐藏层激活函数负责提取和组合特征,而 argmax 或采样负责把最终输出转换成具体结果。
神经网络中的每个神经元都需要对输入进行加权求和,例如第 个神经元的计算可以写成 。为了同时处理一批样本和一层中的多个神经元,实际计算时不会逐个执行这些乘加操作,而是将输入、权重和输出分别组织成矩阵,通过 一次完成整批数据的线性变换,随后再经过激活函数。神经网络的前向传播、反向传播以及参数更新中都包含大量矩阵乘法,因此矩阵乘通常占据大模型训练和推理中的主要计算时间,也是 GPU、NPU 等加速硬件重点优化的核心算子。
不同类型的数据具有不同的规律,因此神经网络通常会采用专门的结构来提高学习效率。例如,CNN 利用局部连接和参数共享提取图像中的边缘、纹理和形状;RNN 通过保存前面时刻的信息处理文本、语音等序列数据;Transformer 则利用注意力机制学习序列中不同位置之间的关系。这些结构相当于提前利用了数据本身的特点,使模型不必依靠大量独立参数从头学习所有规律,从而能够用更少的参数捕捉更有效的特征。类似于七段数码管只需要控制七个灯段,就可以组合表示 0~9,不需要为每个数字单独设计一套完整电路。
随着神经网络层数不断增加,数据需要经过多次变换,前面提取的有效信息可能在传递过程中逐渐减弱,同时也会增加训练难度。残差连接通过增加一条“捷径”,将某一层的输入直接加到后续层的计算结果上,即 。这样,后续层既能学习新的特征,也能保留原始信息,并为反向传播提供更直接的梯度传递路径。以残差连接为核心的 ResNet 因此能够训练更深的网络,并缓解深层网络中的信息丢失、梯度消失和性能退化问题。
随着神经网络层数增加,中间特征的数值可能不断放大或缩小,导致训练不稳定,甚至出现梯度爆炸。LayerNorm 会对每个样本或每个 token 的特征向量进行归一化。对于向量 ,先计算均值和方差:
然后进行归一化和重新缩放:
其中, 用于防止分母为零, 和 是可以通过训练学习的参数。LayerNorm 的作用是让不同层中的特征保持相对稳定的中心和尺度,从而缓解数值溢出、梯度爆炸和训练震荡。
归一化还会减弱向量整体大小和整体偏移的影响,使模型更关注各个维度之间的相对关系。例如,向量 和 虽然绝对大小不同,但内部比例结构相似,经过 LayerNorm 后会得到近似相同的表示;而 的排列关系不同,归一化后的结果也不同。RMSNorm 是 LayerNorm 的简化形式:
它只根据均方根调整向量的整体尺度,因此计算更简单,在大模型中更加常见。
AI 模型接收的数据可以表示为不同形式,例如普通特征通常表示为向量,图像可以表示为像素矩阵,文本、语音和视频则通常表示为序列。模型对这些输入进行特征提取和计算后,可以完成多种任务。回归任务用于预测连续数值,例如气温、房价或某个事件发生的概率;分类任务用于从若干类别中选择结果,例如判断图像中的物体类别;生成任务则根据已有输入产生新的内容,例如文本翻译、文章续写和图片生成。三类任务的主要区别在于输出形式:回归输出数值,分类输出类别或类别概率,生成任务输出一段新的序列或结构化内容。
如果:
感染部位是血液,
细菌呈革兰氏阴性,
细菌形状为杆状,
那么:
该细菌可能属于某一类致病菌,
可信程度为0.8。from pathlib import Path
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
BATCH_SIZE = 128
LEARNING_RATE = 1e-3
EPOCHS = 5
DATA_DIR = Path("data")
MODEL_PATH = Path("mnist_mlp.pt")
def get_device() -> torch.device:
"""
Apple Silicon Mac 优先使用 MPS;
不支持 MPS 时自动使用 CPU。
"""
if torch.backends.mps.is_available():
return torch.device("mps")
return torch.device("cpu")
class MNISTMLP(nn.Module):
def __init__(self) -> None:
super().__init__()
self.network = nn.Sequential(
# 输入图片形状:
# [batch_size, 1, 28, 28]
#
# 展平后:
# [batch_size, 784]
nn.Flatten(),
# 输入层 -> 隐藏层
nn.Linear(28 * 28, 128),
# 非线性激活函数
nn.ReLU(),
# 隐藏层 -> 输出层
# 10 个输出分别对应数字 0~9
nn.Linear(128, 10),
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.network(x)
def create_dataloaders() -> tuple[DataLoader, DataLoader]:
# 将图片转换为 PyTorch 张量
transform = transforms.ToTensor()
training_dataset = datasets.MNIST(
root=DATA_DIR,
train=True,
download=True,
transform=transform,
)
test_dataset = datasets.MNIST(
root=DATA_DIR,
train=False,
download=True,
transform=transform,
)
training_loader = DataLoader(
training_dataset,
batch_size=BATCH_SIZE,
shuffle=True,
num_workers=0,
)
test_loader = DataLoader(
test_dataset,
batch_size=BATCH_SIZE,
shuffle=False,
num_workers=0,
)
return training_loader, test_loader
def train_one_epoch(
model: nn.Module,
dataloader: DataLoader,
loss_function: nn.Module,
optimizer: torch.optim.Optimizer,
device: torch.device,
) -> tuple[float, float]:
model.train()
total_loss = 0.0
correct_count = 0
sample_count = 0
for images, labels in dataloader:
# 把数据放到 MPS 或 CPU
images = images.to(device)
labels = labels.to(device)
# 前向传播:模型进行预测
logits = model(images)
# 计算预测结果与正确答案之间的误差
loss = loss_function(logits, labels)
# 清空上一次计算得到的梯度
optimizer.zero_grad(set_to_none=True)
# 反向传播:计算每个参数应该如何修改
loss.backward()
# 更新权重和偏置
optimizer.step()
batch_size = images.size(0)
total_loss += loss.item() * batch_size
correct_count += (
logits.argmax(dim=1) == labels
).sum().item()
sample_count += batch_size
average_loss = total_loss / sample_count
accuracy = correct_count / sample_count
return average_loss, accuracy
def evaluate(
model: nn.Module,
dataloader: DataLoader,
loss_function: nn.Module,
device: torch.device,
) -> tuple[float, float]:
model.eval()
total_loss = 0.0
correct_count = 0
sample_count = 0
# 测试阶段不需要计算梯度
with torch.inference_mode():
for images, labels in dataloader:
images = images.to(device)
labels = labels.to(device)
logits = model(images)
loss = loss_function(logits, labels)
batch_size = images.size(0)
total_loss += loss.item() * batch_size
correct_count += (
logits.argmax(dim=1) == labels
).sum().item()
sample_count += batch_size
average_loss = total_loss / sample_count
accuracy = correct_count / sample_count
return average_loss, accuracy
def main() -> None:
torch.manual_seed(42)
device = get_device()
print(f"使用设备:{device}")
training_loader, test_loader = create_dataloaders()
model = MNISTMLP().to(device)
# 多分类任务使用交叉熵损失
loss_function = nn.CrossEntropyLoss()
# Adam 根据梯度更新权重和偏置
optimizer = torch.optim.Adam(
model.parameters(),
lr=LEARNING_RATE,
)
for epoch in range(1, EPOCHS + 1):
training_loss, training_accuracy = train_one_epoch(
model=model,
dataloader=training_loader,
loss_function=loss_function,
optimizer=optimizer,
device=device,
)
test_loss, test_accuracy = evaluate(
model=model,
dataloader=test_loader,
loss_function=loss_function,
device=device,
)
print(
f"Epoch {epoch:02d}/{EPOCHS} | "
f"训练损失:{training_loss:.4f} | "
f"训练准确率:{training_accuracy:.2%} | "
f"测试损失:{test_loss:.4f} | "
f"测试准确率:{test_accuracy:.2%}"
)
# 只保存模型参数
torch.save(model.state_dict(), MODEL_PATH)
print(f"模型已保存到:{MODEL_PATH}")
if __name__ == "__main__":
main()