【大模型微调系列-04】 神经网络基础与小项目实战

💡 本章目标:通过构建一个能识别手写数字的AI模型,让你真正理解神经网络是如何"学习"的。2-3小时后,你将拥有第一个自己训练的AI模型!

4.1 理论讲解:神经网络的核心机制

4.1.1 激活函数:让神经网络"活"起来

想象一下,如果神经网络只是简单地把输入乘以权重再相加,那会发生什么?答案是:无论叠加多少层,最终都等价于一个简单的线性变换。这就像无论你用多少个直尺去画图,最终也只能画出直线,永远画不出曲线。

激活函数就是神经网络的"魔法开关",它把线性运算的结果进行非线性变换,让网络能够学习复杂的模式。

输入 x
线性变换
z = Wx + b
激活函数
a = f z
非线性输出
为什么必须要激活函数?

让我们用一个简单的数学例子来说明。假设有一个两层网络,没有激活函数:

第一层:y₁ = W₁x + b₁
第二层:y₂ = W₂y₁ + b₂
展开后:y₂ = W₂(W₁x + b₁) + b₂ = (W₂W₁)x + (W₂b₁ + b₂)

看到了吗?两层网络最终可以简化为 y = Wx + b 的形式,和单层网络没有区别!这就是为什么我们需要激活函数来打破这种线性叠加。

三种常用激活函数

让我们通过代码直观地看看这些激活函数长什么样:

import numpy as np
import matplotlib.pyplot as plt# 创建输入数据
x = np.linspace(-5, 5, 100)# 定义三种激活函数
def relu(x):"""ReLU: 过滤负值,保留正值"""return np.maximum(0, x)def sigmoid(x):"""Sigmoid: 压缩到0-1之间,像概率"""return 1 / (1 + np.exp(-x))def tanh(x):"""Tanh: 压缩到-1到1之间,中心化的sigmoid"""return np.tanh(x)# 绘制激活函数图像
plt.figure(figsize=(12, 4))plt.subplot(1, 3, 1)
plt.plot(x, relu(x), 'b-', linewidth=2)
plt.title('ReLU: 过滤器')
plt.xlabel('输入')
plt.ylabel('输出')
plt.grid(True, alpha=0.3)
plt.axhline(y=0, color='k', linewidth=0.5)
plt.axvline(x=0, color='k', linewidth=0.5)plt.subplot(1, 3, 2)
plt.plot(x, sigmoid(x), 'r-', linewidth=2)
plt.title('Sigmoid: 概率转换器')
plt.xlabel('输入')
plt.ylabel('输出')
plt.grid(True, alpha=0.3)
plt.axhline(y=0.5, color='k', linewidth=0.5, linestyle='--')plt.subplot(1, 3, 3)
plt.plot(x, tanh(x), 'g-', linewidth=2)
plt.title('Tanh: 中心化压缩器')
plt.xlabel('输入')
plt.ylabel('输出')
plt.grid(True, alpha=0.3)
plt.axhline(y=0, color='k', linewidth=0.5)
plt.axvline(x=0, color='k', linewidth=0.5)plt.tight_layout()
plt.show()

💭 理解检查

  • ReLU为什么叫"整流"?因为它像电子元件中的整流器,只让正信号通过
  • Sigmoid为什么常用于二分类?因为它的输出恰好在0-1之间,可以解释为概率
  • Tanh相比Sigmoid的优势?输出以0为中心,有助于下一层的学习
XOR问题:激活函数的威力展示

XOR(异或)问题是一个经典例子,它无法用直线分割,但加入激活函数后就能轻松解决:

# XOR问题的数据
X = np.array([[0,0], [0,1], [1,0], [1,1]])
y = np.array([0, 1, 1, 0])  # XOR的输出# 没有激活函数:永远无法正确分类
# 有激活函数:能够学习非线性边界

📝 一句话总结:激活函数是神经网络的"非线性变换器",让网络能够学习曲线、圆形等复杂边界,而不仅仅是直线。

4.1.2 前向传播与反向传播:神经网络的学习机制

前向传播:数据的流水线

想象神经网络是一条生产流水线,原材料(输入数据)经过每个工作站(网络层)的加工,最终产出成品(预测结果)。

前向传播流程
第一层
W1*x+b1
输入
28*28图像
ReLU
激活
第二层
W2*a1+b2
ReLU
激活
输出层
W3*a2+b3
预测
数字0-9

每一层的计算都很简单:

  1. 线性变换z = W×x + b (权重矩阵乘以输入,加上偏置)
  2. 激活函数a = f(z) (引入非线性)
  3. 传递输出:这一层的输出成为下一层的输入
反向传播:智能的纠错机制

如果前向传播是"考试答题",那反向传播就是"批改试卷"。它从最终的错误开始,逐层往回找每个参数应该承担多少"责任"。

反向传播流程
输出层梯度
dL/dW3
计算误差
loss
第二层梯度
dL/dW2
第一层梯度
dL/dW1
更新所有参数
W = W - lr*梯度

核心概念解释

  • 梯度(Gradient):就是"改变的方向"。想象你在山上,梯度告诉你哪个方向下山最快
  • 链式法则(Chain Rule):错误的传递就像接力赛,每一层都要把"接力棒"(梯度)传给前一层
  • 参数更新W_new = W_old - 学习率 × 梯度

让我们用代码演示一个简化版的反向传播:

# 简化的反向传播示例
import torch# 创建一个简单的计算图
x = torch.tensor([1.0], requires_grad=True)
w = torch.tensor([2.0], requires_grad=True)
b = torch.tensor([1.0], requires_grad=True)# 前向传播
y = w * x + b  # y = 2*1 + 1 = 3
loss = (y - 5) ** 2  # 假设目标是5,loss = (3-5)² = 4# 反向传播(PyTorch自动完成)
loss.backward()print(f"x的梯度: {x.grad}")  # 告诉我们x对loss的影响
print(f"w的梯度: {w.grad}")  # 告诉我们w对loss的影响
print(f"b的梯度: {b.grad}")  # 告诉我们b对loss的影响# 参数更新(梯度下降)
learning_rate = 0.1
w_new = w - learning_rate * w.grad
print(f"更新后的w: {w_new}")

🎯 动手试试:修改上面代码中的目标值(从5改为其他数字),观察梯度如何变化。

📝 一句话总结:前向传播计算预测结果,反向传播计算如何调整参数,两者配合让网络不断学习进步。

4.1.3 训练与验证:如何让模型真正"学会"

数据集划分:考试系统的智慧

训练神经网络就像准备考试,我们需要合理安排学习和测试:

完整数据集
70,000张图片
训练集 60%
42,000张图片
用来学习
验证集 20%
14,000张图片
检验效果
测试集 20%
14,000张图片
最终考试
  • 训练集:相当于课本习题,模型通过它学习规律
  • 验证集:相当于模拟考试,用来调整学习策略(超参数)
  • 测试集:相当于期末考试,只在最后用一次,评估真实能力
过拟合:死记硬背的陷阱

过拟合就像学生死记硬背答案,考试题目稍微变化就不会做了。让我们看看过拟合的表现:

# 绘制训练过程中的loss曲线
epochs = np.arange(1, 21)# 模拟三种情况
# 正常情况
train_loss_normal = 1.0 / np.sqrt(epochs) + 0.1
val_loss_normal = 1.0 / np.sqrt(epochs) + 0.15# 欠拟合
train_loss_under = 0.8 - 0.01 * epochs + 0.5
val_loss_under = 0.8 - 0.01 * epochs + 0.52# 过拟合
train_loss_over = 1.0 / epochs
val_loss_over = 1.0 / epochs[:10].tolist() + (0.1 + 0.02 * epochs[10:]).tolist()plt.figure(figsize=(12, 4))# 绘制三种情况
for i, (train, val, title) in enumerate([(train_loss_under, val_loss_under, '欠拟合:还没学会'),(train_loss_normal, val_loss_normal, '正常:恰到好处'),(train_loss_over, val_loss_over, '过拟合:死记硬背')
], 1):plt.subplot(1, 3, i)plt.plot(epochs, train, 'b-', label='训练loss', linewidth=2)plt.plot(epochs, val, 'r--', label='验证loss', linewidth=2)plt.xlabel('训练轮数')plt.ylabel('Loss')plt.title(title)plt.legend()plt.grid(True, alpha=0.3)plt.tight_layout()
plt.show()

防止过拟合的技巧

  1. Early Stopping(早停):验证loss不再下降就停止训练
  2. Dropout(随机失活):训练时随机"关闭"一些神经元,防止过度依赖
  3. 数据增强:对图片旋转、缩放,制造更多训练样本

📝 一句话总结:合理划分数据集并监控验证指标,是让模型真正"理解"而非"死记"的关键。

4.1.4 模型保存与加载:AI的"存档系统"

训练好的模型就像游戏存档,需要妥善保存以便后续使用:

# 保存模型的两种方式# 方式1:保存整个模型(结构+参数)
torch.save(model, 'my_model.pth')
# 优点:加载简单
# 缺点:文件较大,依赖代码版本# 方式2:只保存参数(推荐)
torch.save(model.state_dict(), 'model_params.pth')
# 优点:文件小,跨版本兼容性好
# 缺点:加载时需要先定义模型结构# 保存训练状态(用于中断后继续训练)
checkpoint = {'epoch': epoch,'model_state_dict': model.state_dict(),'optimizer_state_dict': optimizer.state_dict(),'loss': loss,
}
torch.save(checkpoint, 'checkpoint.pth')

模型文件包含什么?

  • 网络每一层的权重矩阵和偏置
  • 批归一化层的统计信息(如果有)
  • 优化器的动量信息(如果保存checkpoint)

4.2 实操案例:构建你的第一个AI - MNIST手写数字识别

现在让我们动手实现一个完整的神经网络项目!MNIST手写数字识别是深度学习的"Hello World"。

4.2.1 项目概览与环境准备

# 导入必要的库
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm  # 显示进度条# 检查是否有GPU可用
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f'使用设备: {device}')# 设置随机种子,保证结果可复现
torch.manual_seed(42)
np.random.seed(42)

4.2.2 数据加载与探索

# 定义数据预处理
transform = transforms.Compose([transforms.ToTensor(),  # 将图片转为Tensortransforms.Normalize((0.1307,), (0.3081,))  # 标准化(MNIST的均值和标准差)
])# 下载并加载MNIST数据集
print("正在下载MNIST数据集...")
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform
)
test_dataset = datasets.MNIST(root='./data', train=False, 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)print(f"训练集大小: {len(train_dataset)} 张图片")
print(f"测试集大小: {len(test_dataset)} 张图片")
print(f"每批次大小: {batch_size}")
print(f"训练批次数: {len(train_loader)}")# 可视化一些样本
def show_samples():"""展示一些MNIST样本"""fig, axes = plt.subplots(2, 5, figsize=(12, 6))axes = axes.ravel()# 获取一批数据images, labels = next(iter(train_loader))for i in range(10):# 反标准化用于显示img = images[i].squeeze()img = img * 0.3081 + 0.1307axes[i].imshow(img, cmap='gray')axes[i].set_title(f'标签: {labels[i].item()}')axes[i].axis('off')plt.suptitle('MNIST手写数字样本')plt.tight_layout()plt.show()show_samples()# 打印数据形状,帮助理解
sample_image, sample_label = train_dataset[0]
print(f"\n单张图片形状: {sample_image.shape}")  # torch.Size([1, 28, 28])
print(f"展平后的维度: {sample_image.flatten().shape}")  # torch.Size([784])

💡 理解要点

  • MNIST图片是28×28像素的灰度图
  • 展平后变成784维的向量,作为网络输入
  • 标签是0-9的数字,需要预测10个类别

4.2.3 定义神经网络模型

class MLP(nn.Module):"""多层感知器(MLP)网络结构:784输入 → 128隐藏 → 64隐藏 → 10输出"""def __init__(self):super(MLP, self).__init__()# 定义网络层self.fc1 = nn.Linear(784, 128)  # 输入层到第一隐藏层self.fc2 = nn.Linear(128, 64)   # 第一隐藏层到第二隐藏层self.fc3 = nn.Linear(64, 10)    # 第二隐藏层到输出层# 定义激活函数self.relu = nn.ReLU()# Dropout层,防止过拟合self.dropout = nn.Dropout(0.2)  # 20%的神经元会被随机关闭def forward(self, x):"""前向传播过程"""# 将28×28的图片展平成784维向量x = x.view(-1, 784)  # -1表示自动计算批次大小# 第一层:线性变换 + 激活 + Dropoutx = self.fc1(x)       # [batch_size, 784] → [batch_size, 128]x = self.relu(x)      # ReLU激活x = self.dropout(x)   # Dropout# 第二层:线性变换 + 激活 + Dropoutx = self.fc2(x)       # [batch_size, 128] → [batch_size, 64]x = self.relu(x)      # ReLU激活x = self.dropout(x)   # Dropout# 输出层:线性变换(不需要激活函数)x = self.fc3(x)       # [batch_size, 64] → [batch_size, 10]return x# 创建模型实例
model = MLP().to(device)# 打印模型结构
print("模型结构:")
print(model)# 计算模型参数量
total_params = sum(p.numel() for p in model.parameters())
print(f"\n总参数量: {total_params:,}")
MLP网络结构
隐藏层1
128个神经元
ReLU激活
输入层
784个神经元
28*28图片
Dropout
20%
隐藏层2
64个神经元
ReLU激活
Dropout
20%
输出层
10个神经元
数字0-9

4.2.4 训练循环实现

# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()  # 交叉熵损失,适合多分类
optimizer = optim.Adam(model.parameters(), lr=0.001)  # Adam优化器# 记录训练过程
train_losses = []
train_accuracies = []
val_losses = []
val_accuracies = []def train_epoch(model, loader, criterion, optimizer):"""训练一个epoch"""model.train()  # 设置为训练模式(启用Dropout)total_loss = 0correct = 0total = 0# 使用tqdm显示进度条progress_bar = tqdm(loader, desc='训练中')for batch_idx, (images, labels) in enumerate(progress_bar):# 将数据移到GPU(如果有)images, labels = images.to(device), labels.to(device)# ========= 5步训练流程 =========# 步骤1:清零梯度(必须的,否则梯度会累积)optimizer.zero_grad()# 步骤2:前向传播outputs = model(images)# 步骤3:计算损失loss = criterion(outputs, labels)# 步骤4:反向传播loss.backward()# 步骤5:更新参数optimizer.step()# ==============================# 统计指标total_loss += loss.item()_, predicted = torch.max(outputs.data, 1)total += labels.size(0)correct += (predicted == labels).sum().item()# 更新进度条显示if batch_idx % 10 == 0:progress_bar.set_postfix({'Loss': f'{loss.item():.4f}','Acc': f'{100.*correct/total:.2f}%'})avg_loss = total_loss / len(loader)accuracy = 100. * correct / totalreturn avg_loss, accuracydef validate(model, loader, criterion):"""验证模型性能"""model.eval()  # 设置为评估模式(关闭Dropout)total_loss = 0correct = 0total = 0# 不需要计算梯度,节省内存with torch.no_grad():for images, labels in tqdm(loader, desc='验证中'):images, labels = images.to(device), labels.to(device)outputs = model(images)loss = criterion(outputs, labels)total_loss += loss.item()_, predicted = torch.max(outputs.data, 1)total += labels.size(0)correct += (predicted == labels).sum().item()avg_loss = total_loss / len(loader)accuracy = 100. * correct / totalreturn avg_loss, accuracy# 开始训练
num_epochs = 10
best_val_acc = 0print("="*50)
print("开始训练...")
print("="*50)for epoch in range(num_epochs):print(f'\nEpoch [{epoch+1}/{num_epochs}]')# 训练train_loss, train_acc = train_epoch(model, train_loader, criterion, optimizer)train_losses.append(train_loss)train_accuracies.append(train_acc)# 验证val_loss, val_acc = validate(model, test_loader, criterion)val_losses.append(val_loss)val_accuracies.append(val_acc)print(f'训练 - Loss: {train_loss:.4f}, Accuracy: {train_acc:.2f}%')print(f'验证 - Loss: {val_loss:.4f}, Accuracy: {val_acc:.2f}%')# 保存最佳模型if val_acc > best_val_acc:best_val_acc = val_acctorch.save(model.state_dict(), 'best_model.pth')print(f'✅ 保存最佳模型 (验证准确率: {val_acc:.2f}%)')print(f'\n训练完成!最佳验证准确率: {best_val_acc:.2f}%')

🔍 常见错误提示

  • 如果出现"CUDA out of memory",减小batch_size
  • 如果loss变成NaN,检查学习率是否过大
  • 如果准确率一直不提升,检查数据预处理是否正确

4.2.5 训练曲线可视化

def plot_training_history():"""绘制训练历史曲线"""fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))epochs_range = range(1, len(train_losses) + 1)# 绘制Loss曲线ax1.plot(epochs_range, train_losses, 'b-', label='训练Loss', linewidth=2)ax1.plot(epochs_range, val_losses, 'r-', label='验证Loss', linewidth=2)ax1.set_xlabel('Epoch')ax1.set_ylabel('Loss')ax1.set_title('Loss曲线')ax1.legend()ax1.grid(True, alpha=0.3)# 标注最低验证Lossmin_val_loss_epoch = np.argmin(val_losses) + 1ax1.plot(min_val_loss_epoch, val_losses[min_val_loss_epoch-1], 'ro', markersize=10)ax1.annotate(f'最低验证Loss\nEpoch {min_val_loss_epoch}', xy=(min_val_loss_epoch, val_losses[min_val_loss_epoch-1]),xytext=(min_val_loss_epoch+1, val_losses[min_val_loss_epoch-1]+0.05),arrowprops=dict(arrowstyle='->', color='red'))# 绘制准确率曲线ax2.plot(epochs_range, train_accuracies, 'b-', label='训练准确率', linewidth=2)ax2.plot(epochs_range, val_accuracies, 'r-', label='验证准确率', linewidth=2)ax2.set_xlabel('Epoch')ax2.set_ylabel('准确率 (%)')ax2.set_title('准确率曲线')ax2.legend()ax2.grid(True, alpha=0.3)# 标注最高验证准确率max_val_acc_epoch = np.argmax(val_accuracies) + 1ax2.plot(max_val_acc_epoch, val_accuracies[max_val_acc_epoch-1], 'go', markersize=10)ax2.annotate(f'最高准确率\n{val_accuracies[max_val_acc_epoch-1]:.2f}%', xy=(max_val_acc_epoch, val_accuracies[max_val_acc_epoch-1]),xytext=(max_val_acc_epoch-2, val_accuracies[max_val_acc_epoch-1]-3),arrowprops=dict(arrowstyle='->', color='green'))plt.suptitle('训练过程监控', fontsize=14)plt.tight_layout()plt.show()plot_training_history()

4.2.6 模型评估与错误分析

def evaluate_model():"""详细评估模型性能"""model.eval()# 收集所有预测结果all_predictions = []all_labels = []all_probs = []with torch.no_grad():for images, labels in test_loader:images, labels = images.to(device), labels.to(device)outputs = model(images)# 获取预测概率和类别probs = torch.softmax(outputs, dim=1)_, predicted = torch.max(outputs, 1)all_predictions.extend(predicted.cpu().numpy())all_labels.extend(labels.cpu().numpy())all_probs.extend(probs.cpu().numpy())# 计算混淆矩阵from sklearn.metrics import confusion_matriximport seaborn as snscm = confusion_matrix(all_labels, all_predictions)plt.figure(figsize=(10, 8))sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=range(10), yticklabels=range(10))plt.title('混淆矩阵')plt.ylabel('真实标签')plt.xlabel('预测标签')plt.show()# 找出预测错误的样本errors = []for i in range(len(all_predictions)):if all_predictions[i] != all_labels[i]:errors.append({'index': i,'true': all_labels[i],'pred': all_predictions[i],'confidence': max(all_probs[i]) * 100})print(f"错误样本数: {len(errors)} / {len(all_predictions)}")print(f"错误率: {len(errors)/len(all_predictions)*100:.2f}%")# 展示一些错误案例if errors:print("\n部分错误案例:")for error in errors[:5]:print(f"样本{error['index']}: 真实={error['true']}, "f"预测={error['pred']}, 置信度={error['confidence']:.1f}%")return errorserrors = evaluate_model()# 可视化错误案例
def show_error_cases(num_cases=6):"""展示预测错误的案例"""if not errors:print("没有错误案例!")returnfig, axes = plt.subplots(2, 3, figsize=(12, 8))axes = axes.ravel()# 随机选择一些错误案例error_indices = np.random.choice(len(errors), min(num_cases, len(errors)), replace=False)for idx, ax in enumerate(axes):if idx >= len(error_indices):ax.axis('off')continueerror = errors[error_indices[idx]]# 获取对应的图片test_data = test_dataset[error['index']][0]img = test_data.squeeze()img = img * 0.3081 + 0.1307  # 反标准化ax.imshow(img, cmap='gray')ax.set_title(f"真实: {error['true']}, 预测: {error['pred']}\n"f"置信度: {error['confidence']:.1f}%",color='red')ax.axis('off')plt.suptitle('预测错误案例分析', fontsize=14)plt.tight_layout()plt.show()show_error_cases()

4.2.7 模型保存与加载

# 保存完整的训练状态
def save_checkpoint(model, optimizer, epoch, loss, accuracy, filename='checkpoint.pth'):"""保存训练检查点"""checkpoint = {'epoch': epoch,'model_state_dict': model.state_dict(),'optimizer_state_dict': optimizer.state_dict(),'loss': loss,'accuracy': accuracy,'model_architecture': str(model),}torch.save(checkpoint, filename)print(f"✅ 检查点已保存到 {filename}")# 保存当前模型
save_checkpoint(model, optimizer, num_epochs, val_losses[-1], val_accuracies[-1])# 加载模型进行推理
def load_model_for_inference():"""加载训练好的模型"""# 创建新的模型实例loaded_model = MLP().to(device)# 加载参数loaded_model.load_state_dict(torch.load('best_model.pth'))loaded_model.eval()print("✅ 模型加载成功!")return loaded_model# 测试加载的模型
loaded_model = load_model_for_inference()# 用加载的模型预测单张图片
def predict_single_image(model, image_tensor):"""预测单张图片"""model.eval()with torch.no_grad():image_tensor = image_tensor.unsqueeze(0).to(device)  # 添加batch维度output = model(image_tensor)probabilities = torch.softmax(output, dim=1)predicted_class = torch.argmax(output, dim=1)confidence = torch.max(probabilities) * 100return predicted_class.item(), confidence.item()# 测试预测功能
test_image, test_label = test_dataset[0]
pred_class, confidence = predict_single_image(loaded_model, test_image)
print(f"预测结果: {pred_class}, 置信度: {confidence:.2f}%, 真实标签: {test_label}")

4.2.8 进阶:CNN网络实现(选做)

class SimpleCNN(nn.Module):"""简单的卷积神经网络相比MLP的优势:参数共享、局部感知"""def __init__(self):super(SimpleCNN, self).__init__()# 卷积层self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1)self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)# 池化层self.pool = nn.MaxPool2d(2, 2)# 全连接层self.fc1 = nn.Linear(64 * 7 * 7, 128)self.fc2 = nn.Linear(128, 10)# 激活和Dropoutself.relu = nn.ReLU()self.dropout = nn.Dropout(0.25)def forward(self, x):# 第一个卷积块x = self.conv1(x)           # [batch, 1, 28, 28] → [batch, 32, 28, 28]x = self.relu(x)x = self.pool(x)            # [batch, 32, 28, 28] → [batch, 32, 14, 14]# 第二个卷积块x = self.conv2(x)           # [batch, 32, 14, 14] → [batch, 64, 14, 14]x = self.relu(x)x = self.pool(x)            # [batch, 64, 14, 14] → [batch, 64, 7, 7]# 展平并通过全连接层x = x.view(-1, 64 * 7 * 7)  # 展平x = self.fc1(x)x = self.relu(x)x = self.dropout(x)x = self.fc2(x)return x# 创建CNN模型
cnn_model = SimpleCNN().to(device)# 计算参数量对比
mlp_params = sum(p.numel() for p in model.parameters())
cnn_params = sum(p.numel() for p in cnn_model.parameters())print("模型参数量对比:")
print(f"MLP: {mlp_params:,} 参数")
print(f"CNN: {cnn_params:,} 参数")
print(f"CNN参数量是MLP的 {cnn_params/mlp_params*100:.1f}%")# 性能对比表格
comparison_data = {'模型': ['MLP', 'CNN'],'参数量': [mlp_params, cnn_params],'准确率': ['~98%', '~99%'],'训练时间/epoch': ['~30秒', '~45秒'],'优势': ['简单易懂', '更高准确率']
}import pandas as pd
comparison_df = pd.DataFrame(comparison_data)
print("\n性能对比:")
print(comparison_df.to_string(index=False))

4.3 本章小结与练习

🎯 学习成果检查清单

完成本章学习后,你应该能够:

  • 解释为什么神经网络需要激活函数
  • 描述前向传播和反向传播的基本流程
  • 独立完成MNIST手写数字识别项目
  • 绘制并解读训练曲线
  • 保存和加载训练好的模型
  • 识别并处理过拟合问题

💡 关键概念回顾

  1. 激活函数:引入非线性,让网络能学习复杂模式
  2. 前向传播:数据从输入层流向输出层的计算过程
  3. 反向传播:根据误差调整网络参数的过程
  4. 梯度下降:沿着梯度的反方向更新参数
  5. 过拟合:模型在训练集上表现好但泛化能力差

🚀 进阶练习

  1. 调参实验

    • 改变隐藏层大小(如256、512),观察效果
    • 调整学习率(0.01、0.001、0.0001),找出最优值
    • 修改Dropout率,观察对过拟合的影响
  2. 数据增强

    • 对MNIST图片进行随机旋转(-15°到15°)
    • 添加随机噪声,提高模型鲁棒性
  3. 新数据集挑战

    • 尝试Fashion-MNIST(服装分类)
    • 挑战CIFAR-10(彩色图片分类)

📚 推荐资源

  • PyTorch官方教程:https://pytorch.org/tutorials/
  • 可视化神经网络:http://playground.tensorflow.org/
  • MNIST数据集详情:http://yann.lecun.com/exdb/mnist/

🎉 恭喜你!

你已经成功训练了第一个神经网络!这是深度学习之旅的重要里程碑。下一章,我们将深入了解Qwen大模型的架构,看看这些基础知识如何应用到数十亿参数的大模型中。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/919161.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/919161.shtml
英文地址,请注明出处:http://en.pswp.cn/news/919161.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JavaWeb前端(HTML,CSS具体案例)

前言 一直在学习B站黑马程序员苍穹外卖。现在已经学的差不多了,但是我学习一直是针对后端开发的,前端也没太注重去学(他大部分都给课程资料嘻嘻🤪),但我还是比较感兴趣,准备先把之前学JavaWeb&…

核心数据结构:DataFrame

3.3.1 创建与访问什么是 DataFrame?DataFrame 是 Pandas 中的核心数据结构之一,多行多列表格数据,类似于 Excel 表格 或 SQL 查询结果。它是一个 二维表格结构,具有行索引(index)和列标签(colu…

深入探索Go语言标准库 net 包中的 IP 处理

深入探索Go语言标准库 net 包中的 IP 处理 文章目录深入探索Go语言标准库 net 包中的 IP 处理引言核心知识type IP常用函数常用方法代码示例常见问题1. DNS 查询失败怎么办?2. 如何区分 IPv4 和 IPv6 地址?使用场景1. 服务器端编程2. 网络监控和调试3. 防…

2.4 双向链表

目录 引入 结构定义 结构操作 初始化 插入 删除 打印 查找 随机位置插入 随机位置删除 销毁 总结 数据结构专栏https://blog.csdn.net/xyl6716/category_13002640.html 精益求精 追求卓越 【代码仓库】:Code Is Here 【合作】 :apollomona…

开发指南132-DOM的宽度、高度属性

宽度、高度类似。这里以高度为例来说明DOM中有关高度的概念:1、height取法:element.style.height说明:元素内容区域的高度,不含padding、border、margin该属性可写2、clientHeight取法:element..clientHeight&#xff…

魔改chromium源码——解除 iframe 的同源策略

在进行以下操作之前,请确保已完成之前文章中提到的 源码拉取及编译 部分。 如果已顺利完成相关配置,即可继续执行后续操作。 同源策略限制了不同源(协议、域名、端口)的网页脚本访问彼此的资源。iframe 的跨域限制由 Blink 渲染引擎和 Chromium 的安全层共同实现。 咱们直…

在鸿蒙中实现深色/浅色模式切换:从原理到可运行 Demo

摘要 现在几乎所有主流应用都支持“深色模式”和“浅色模式”切换,这已经成了用户习惯。鸿蒙(HarmonyOS)同样提供了两种模式(dark / light),并且支持应用根据系统主题切换,或者应用内手动切换。…

Redux搭档Next.js的简明使用教程

Redux 是一个用于 JavaScript 应用的状态管理库,主要解决组件间共享状态和复杂状态逻辑的问题。当应用规模较大、组件层级较深或多个组件需要共享/修改同一状态时,Redux 可以提供可预测、可追踪的状态管理方式,避免状态在组件间混乱传递。Red…

SCAI采用公平发射机制成功登陆LetsBonk,60%代币供应量已锁仓

去中心化科学(DeSci)平台SCAI宣布,其代币已于今日以Fair Launch形式在LetsBonk.fun平台成功发射。为保障资金安全与透明,开发团队已将代币总量的60%进行锁仓,进一步提升社区信任与项目合规性。SCAI是一个专注于高质量科…

【Kubernetes系列】Kubernetes中的resources

博客目录1. limits(资源上限)2. requests(资源请求)关键区别其他注意事项示例场景在 Kubernetes (k8s) 中,resources 用于定义容器的资源请求(requests)和限制(limits)&a…

hadoop 前端yarn 8088端口查看任务执行情况

图中资源相关参数含义及简单分析思路&#xff1a; 基础资源抢占参数 Total Resource Preempted: <memory:62112, vCores:6> 含义&#xff1a;应用总共被抢占的资源量&#xff0c; memory:62112 表示累计被收回的内存&#xff08;单位通常是MB &#xff0c;结合Hadoop生态…

基于SpringBoot的个性化教育学习平台的设计与实现(源码+lw+部署文档+讲解等)

课题介绍在教育数字化转型与学习者需求差异化的背景下&#xff0c;传统学习平台 “统一内容、统一进度” 的模式已显局限。当前&#xff0c;平台多提供标准化课程资源&#xff0c;无法根据学习者年龄、基础、目标&#xff08;如升学、技能提升&#xff09;定制学习路径&#xf…

UE5多人MOBA+GAS 48、制作闪现技能

文章目录添加标签添加GA_Blink添加标签 CRUNCH_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Ability_Blink_Teleport)CRUNCH_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Ability_Blink_Cooldown)UE_DEFINE_GAMEPLAY_TAG_COMMENT(Ability_Blink_Teleport, "Ability.Blink.Teleport"…

Swift 实战:实现一个简化版的 Twitter(LeetCode 355)

文章目录摘要描述示例解决答案设计思路题解代码分析测试示例和结果时间复杂度空间复杂度总结摘要 在社交媒体平台里&#xff0c;推送机制是核心功能之一。比如你关注了某人&#xff0c;就希望在自己的时间线上能看到他们的最新消息&#xff0c;同时自己的消息也要能出现在别人…

在浏览器端使用 xml2js 遇到的报错及解决方法

在浏览器端使用 xml2js 遇到的报错及解决方法 一、引言 在前端开发过程中&#xff0c;我们常常需要处理 XML 数据。xml2js 是一个非常流行的用于将 XML 转换为 JavaScript 对象的库。然而&#xff0c;当我们在浏览器端使用它时&#xff0c;可能会遇到一些问题。本文将介绍在浏览…

eChart饼环pie中间显示总数_2个以上0值不挤掉

<!DOCTYPE html> <html> <head><meta charset"utf-8"><title>环饼图显示总数</title><script src"https://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js"></script><style>#main { widt…

Ansible 核心功能进阶:自动化任务的灵活控制与管理

一、管理 FACTS&#xff1a;获取远程主机的 “身份信息”FACTS 是 Ansible 自动收集的远程主机详细信息&#xff08;类似 “主机身份证”&#xff09;&#xff0c;包括主机名、IP、系统版本、硬件配置等。通过 FACTS 可以动态获取主机信息&#xff0c;让 Playbook 更灵活1. 查看…

gRPC网络模型详解

gRPC协议框架 TCP层&#xff1a;底层通信协议&#xff0c;基于TCP连接。 TLS层&#xff1a;该层是可选的&#xff0c;基于TLS加密通道。 HTTP2层&#xff1a;gRPC承载在HTTP2协议上&#xff0c;利用了HTTP2的双向流、流控、头部压缩、单连接上的多 路复用请求等特性。 gRPC层…

[优选算法专题二滑动窗口——将x减到0的最小操作数]

题目链接 将x减到0的最小操作数 题目描述 题目解析 问题重述 给定一个整数数组 nums 和一个整数 x&#xff0c;每次只能从数组的左端或右端移除一个元素&#xff0c;并将该元素的值从 x 中减去。我们需要找到将 x 恰好减为 0 的最少操作次数&#xff0c;如果不可能则返回 -…

AOP配置类自动注入

本文主要探究AopAutoConfiguration配置类里面的bean怎么被自动装配的。代码如下&#xff1a;package com.example.springdemo.demos.a05;import com.example.springdemo.demos.a04.Bean1; import com.example.springdemo.demos.a04.Bean2; import com.example.springdemo.demos…