南航NUAA机器学习及其应用解题指南
本文档详细讲解七个机器学习任务的解题思路、关键代码实现和注意事项,帮助您理解从数据处理到模型训练的完整流程。
目录
- 任务一:Iris 数据集线性回归
- 任务二:Breast Cancer Wisconsin 逻辑回归
- 任务三:Image Segmentation SVM 分类
- 任务四:Boston Housing 神经网络回归
- 任务五:MNIST 卷积神经网络
- 任务六:Wine 数据集 PCA 降维
- 任务七:Mall Customers K-means 聚类
任务一:Iris 数据集线性回归
📋 任务要求
利用 iris 数据集(https://archive.ics.uci.edu/dataset/53/iris),构建线性回归模型,对于鸢尾花数据集的花瓣长度做预测,并给出训练集及测试集上的均方根误差(Root Mean Squared Error,RMSE),并结合实验结果对模型预测效果进行分析。(15 分)
🎯 解题思路
1. 数据准备
- 下载 Iris 数据集,包含 150 条样本,4 个数值特征 + 1 个类别特征
- 选择花瓣长度作为目标变量,其余特征作为输入
- 对类别特征(species)进行独热编码
- 使用 StandardScaler 标准化数值特征
2. 模型设计
- 实现批量梯度下降的线性回归
- 假设函数:
h(x) = w^T x + b - 损失函数:均方误差 MSE
- 参数更新规则:
w := w - α * ∇J(w)
3. 训练流程
- 初始化权重为 0
- 迭代固定轮数(epochs),每轮计算梯度并更新参数
- 记录每轮的训练/测试 RMSE
💻 关键代码片段
1. 线性回归类实现
class LinearRegressionGD:
"""基于批量梯度下降的线性回归"""
def __init__(self, learning_rate: float = 0.05, epochs: int = 500):
self.learning_rate = learning_rate
self.epochs = epochs
self.weights_ = None
self.history_ = {
"train_rmse": [],
"test_rmse": [],
}
@staticmethod
def _add_bias(X: np.ndarray) -> np.ndarray:
"""添加偏置项(全 1 列)"""
ones = np.ones((X.shape[0], 1))
return np.hstack((ones, X))
def fit(self, X_train, y_train, X_test=None, y_test=None):
"""训练模型"""
X_train_b = self._add_bias(X_train)
self.weights_ = np.zeros(X_train_b.shape[1])
for _ in range(self.epochs):
# 前向传播
predictions = X_train_b @ self.weights_
errors = predictions - y_train
# 计算梯度
gradient = (X_train_b.T @ errors) / len(y_train)
# 更新权重
self.weights_ -= self.learning_rate * gradient
# 记录训练误差
train_rmse = float(np.sqrt(np.mean(errors**2)))
self.history_["train_rmse"].append(train_rmse)
# 记录测试误差
if X_test is not None and y_test is not None:
test_predictions = self.predict(X_test)
test_rmse = float(np.sqrt(np.mean((test_predictions - y_test) ** 2)))
else:
test_rmse = float("nan")
self.history_["test_rmse"].append(test_rmse)
return self
def predict(self, X: np.ndarray) -> np.ndarray:
"""预测"""
X_b = self._add_bias(X)
return X_b @ self.weights_
2. 数据准备与特征工程
def prepare_data(df: pd.DataFrame, test_size: float, random_state: int):
"""准备训练数据"""
# 分离特征和目标
X_raw = df[["sepal_length", "sepal_width", "petal_width", "species"]]
y = df["petal_length"].to_numpy()
# 对类别特征进行独热编码
X_encoded = pd.get_dummies(X_raw, columns=["species"], drop_first=False)
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
X_encoded.to_numpy(),
y,
test_size=test_size,
random_state=random_state,
shuffle=True,
)
# 标准化特征
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
return {
"X_train": X_train_scaled,
"X_test": X_test_scaled,
"y_train": y_train,
"y_test": y_test,
}
⚠️ 注意事项
- 特征标准化:不同特征的量纲差异大,必须标准化,否则梯度下降收敛慢或不收敛
- 学习率选择:太大会震荡,太小会收敛慢。建议从 0.01-0.1 之间尝试
- 偏置项处理:通过在特征矩阵前添加全 1 列,将偏置项和权重统一处理
- 独热编码:类别特征需要转换为数值,
drop_first=False保留所有类别信息 - 测试集不泄漏:StandardScaler 必须在训练集上 fit,测试集只能 transform
📊 预期结果
- 训练集 RMSE:约 0.35-0.40
- 测试集 RMSE:约 0.35-0.45
- 损失曲线应平滑下降,在 100-200 轮后趋于稳定
任务二:Breast Cancer Wisconsin 逻辑回归
📋 任务要求
利用 breast cancer wisconsin 数据集(https://archive.ics.uci.edu/dataset/17/breast+cancer+wisconsin+diagnostic)构建逻辑回归模型,同时对该数据加入高斯噪声(标准高斯分布𝒩(0,1))后重新构建逻辑回归模型,并对两次构建出的模型做对比。(15 分)
🎯 解题思路
1. 数据特点
- 569 个样本,30 个连续特征(细胞核几何和纹理特征)
- 二分类任务:恶性(M)vs 良性(B)
- 无缺失值,但特征量纲差异大
2. 模型设计
- 逻辑回归:
h(x) = σ(w^T x + b),其中 σ 是 sigmoid 函数 - 损失函数:对数损失(交叉熵)
- L2 正则化:防止过拟合
- 优化器:批量梯度下降
3. 噪声实验
- 在标准化后的特征上叠加标准正态分布噪声
- 对比噪声前后的准确率和损失
💻 关键代码片段
1. 逻辑回归类实现
class LogisticRegressionGD:
"""基于梯度下降的逻辑回归"""
@staticmethod
def _sigmoid(z: np.ndarray) -> np.ndarray:
"""Sigmoid 激活函数"""
return 1.0 / (1.0 + np.exp(-z))
@staticmethod
def _log_loss(y_true: np.ndarray, y_pred: np.ndarray) -> float:
"""对数损失(交叉熵)"""
eps = 1e-12 # 防止 log(0)
y_pred = np.clip(y_pred, eps, 1 - eps)
return float(-np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred)))
def fit(self, X_train, y_train, X_test=None, y_test=None):
"""训练模型"""
X_train_b = self._add_bias(X_train)
self.weights_ = np.zeros(X_train_b.shape[1])
for _ in range(self.epochs):
# 前向传播
logits = X_train_b @ self.weights_
probs = self._sigmoid(logits)
errors = probs - y_train
# 计算梯度
gradient = (X_train_b.T @ errors) / len(y_train)
# L2 正则化(不对偏置项正则)
if self.l2_lambda > 0:
l2_vector = np.concatenate(([0.0], self.weights_[1:]))
gradient += self.l2_lambda * l2_vector
# 更新权重
self.weights_ -= self.learning_rate * gradient
# 记录指标
train_probs = self._sigmoid(X_train_b @ self.weights_)
train_loss = self._log_loss(y_train, train_probs)
train_acc = self._accuracy(y_train, train_probs)
self.history_["train_loss"].append(train_loss)
self.history_["train_accuracy"].append(train_acc)
return self
2. 添加高斯噪声
def add_gaussian_noise(
X_train: np.ndarray,
X_test: np.ndarray,
random_state: int,
mean: float = 0.0,
std: float = 1.0,
) -> Dict[str, np.ndarray]:
"""在特征上叠加高斯噪声"""
rng = np.random.default_rng(random_state)
# 生成与输入相同形状的噪声
noise_train = rng.normal(loc=mean, scale=std, size=X_train.shape)
noise_test = rng.normal(loc=mean, scale=std, size=X_test.shape)
return {
"X_train_noisy": X_train + noise_train,
"X_test_noisy": X_test + noise_test,
}
3. 训练两个模型并对比
# 无噪声模型
clean_model = LogisticRegressionGD(learning_rate=0.05, epochs=400, l2_lambda=0.001)
clean_model.fit(X_train, y_train, X_test, y_test)
# 加噪声模型
noisy_data = add_gaussian_noise(X_train, X_test, random_state=42)
noisy_model = LogisticRegressionGD(learning_rate=0.05, epochs=400, l2_lambda=0.001)
noisy_model.fit(noisy_data["X_train_noisy"], y_train,
noisy_data["X_test_noisy"], y_test)
⚠️ 注意事项
- 数值稳定性:sigmoid 函数可能溢出,使用
np.clip限制概率范围 - 正则化:偏置项不参与 L2 正则,否则会引入不必要的惩罚
- 分层抽样:使用
stratify=y保证训练/测试集类别比例一致 - 学习率调整:逻辑回归对学习率较敏感,建议 0.01-0.1
- 噪声强度:标准高斯噪声 N(0,1) 与标准化后的特征量级相当,影响较大
📊 预期结果
- 无噪声:测试准确率约 96-98%,Log Loss 约 0.05-0.10
- 加噪声:测试准确率下降 2-5%,Log Loss 上升 0.1-0.2
- 说明噪声破坏了判别信息,导致性能下降
任务三:Image Segmentation SVM 分类
📋 任务要求
利用 image segmentation 数据集(https://archive.ics.uci.edu/dataset/50/image+segmentation),分别学习线性支持向量机与核支持向量机(利用高斯核,并对选取的高斯核进行介绍),对于学习出的两种模型及实验结果做对比分析。(15 分)
🎯 解题思路
1. 数据特点
- 2310 个样本,19 个连续特征(区域几何、纹理、颜色)
- 7 类图像区域:BRICKFACE、SKY、FOLIAGE、CEMENT、WINDOW、PATH、GRASS
- 特征包括位置、像素数、边缘密度、RGB 和 HSV 通道统计
2. 线性 SVM
- 采用一对多(OvR)策略,训练 7 个二分类器
- 损失函数:合页损失 + L2 正则
- 优化器:次梯度下降
3. 高斯核 SVM
- 使用随机傅里叶特征(RFF)近似 RBF 核
- 核函数:
K(x, z) = exp(-γ ||x - z||²) - 优势:将无限维映射转换为有限维线性问题
💻 关键代码片段
1. 线性 SVM 实现
class OneVsRestLinearSVM:
"""一对多线性 SVM"""
def __init__(self, n_classes, n_features, epochs, learning_rate, lambda_reg, class_labels):
self.n_classes = n_classes
self.W = np.zeros((n_classes, n_features), dtype=np.float64) # 权重矩阵
self.b = np.zeros(n_classes, dtype=np.float64) # 偏置向量
self.epochs = epochs
self.learning_rate = learning_rate
self.lambda_reg = lambda_reg
def fit(self, X_train, y_train, X_test, y_test):
"""训练模型"""
n_samples = X_train.shape[0]
for _ in range(self.epochs):
for class_idx in range(self.n_classes):
# 构造二分类标签:当前类为 +1,其余为 -1
y_binary = np.where(y_train == class_idx, 1.0, -1.0)
# 计算决策值和间隔
decision = X_train @ self.W[class_idx] + self.b[class_idx]
margins = y_binary * decision
# 找出违反间隔的样本(margin < 1)
mask = margins < 1
# 计算次梯度
grad_w = self.lambda_reg * self.W[class_idx]
grad_b = 0.0
if np.any(mask):
# 合页损失的次梯度
scaled = y_binary[mask][:, None] * X_train[mask]
grad_w -= (1.0 / n_samples) * scaled.sum(axis=0)
grad_b -= (1.0 / n_samples) * y_binary[mask].sum()
# 更新参数
self.W[class_idx] -= self.learning_rate * grad_w
self.b[class_idx] -= self.learning_rate * grad_b
# 记录损失和准确率
# ...
return self
def predict(self, X):
"""预测类别"""
scores = X @ self.W.T + self.b
return np.argmax(scores, axis=1) # 选择得分最高的类
2. 随机傅里叶特征(核近似)
class RandomFourierFeatures:
"""用 RFF 近似 RBF 核"""
def __init__(self, gamma: float, n_components: int, random_state: int):
self.gamma = gamma
self.n_components = n_components
self.random_state = np.random.default_rng(random_state)
self.W = None # 随机投影矩阵
self.b = None # 随机偏移
def fit(self, X: np.ndarray):
"""生成随机特征"""
n_features = X.shape[1]
# W ~ N(0, 2γI),依据 Bochner 定理
self.W = self.random_state.normal(
loc=0.0,
scale=np.sqrt(2 * self.gamma),
size=(n_features, self.n_components),
)
# b ~ U(0, 2π)
self.b = self.random_state.uniform(0, 2 * np.pi, size=self.n_components)
return self
def transform(self, X: np.ndarray) -> np.ndarray:
"""映射到高维空间"""
projection = X @ self.W + self.b
return np.sqrt(2.0 / self.n_components) * np.cos(projection)
# 使用流程
rff = RandomFourierFeatures(gamma=0.05, n_components=600, random_state=42)
rff.fit(X_train)
# 将数据映射到高维空间
X_train_rff = rff.transform(X_train)
X_test_rff = rff.transform(X_test)
# 在映射后的特征上训练线性 SVM
rbf_svm = OneVsRestLinearSVM(...)
rbf_svm.fit(X_train_rff, y_train, X_test_rff, y_test)
⚠️ 注意事项
- 合页损失的次梯度:在 margin < 1 时才有梯度,注意条件判断
- OvR 策略:每个类别训练一个二分类器,最终取得分最高的类
- 核参数选择:γ 控制 RBF 核的宽度,太大导致过拟合,太小欠拟合。建议先用验证集粗调(0.01-0.1)
- RFF 维度:维度越高近似越准确,但计算成本增加。600-1000 通常足够
- 正则化系数:λ 控制模型复杂度,建议 0.001-0.1
📊 预期结果
- 线性 SVM:测试准确率 95-97%
- RBF SVM:测试准确率 96-98%(略优于线性)
- 核 SVM 在非线性可分的类别(如 FOLIAGE vs GRASS)上表现更好
任务四:Boston Housing 神经网络回归
📋 任务要求
利用波士顿房价数据集(https://github.com/selva86/datasets/blob/master/BostonHousing.csv)学习一个用于回归任务的三层全连接前馈神经网络,该神经网络需要自行构建(报告中需要对每层神经元的个数与激活函数进行介绍)。(15 分)
🎯 解题思路
1. 数据特点
- 506 个样本,13 个特征(犯罪率、房间数、交通便捷度等)
- 回归任务:预测房价中位数(medv)
- 特征量纲差异大,需要标准化
2. 网络结构设计
- 输入层:13 维
- 隐藏层 1:128 个神经元 + ReLU
- 隐藏层 2:64 个神经元 + ReLU
- 隐藏层 3:32 个神经元 + ReLU
- 输出层:1 个神经元(线性)
3. 训练配置
- 损失函数:MSE(均方误差)
- 优化器:Adam(自适应学习率)
- 正则化:L2 权重衰减
- 批量大小:64
💻 关键代码片段
1. 三层神经网络定义(PyTorch)
import torch.nn as nn
class FeedForwardNN(nn.Module):
"""三层全连接前馈神经网络"""
def __init__(self, input_dim: int, hidden_dims: Tuple[int, int, int]):
super().__init__()
h1, h2, h3 = hidden_dims # 例如 (128, 64, 32)
self.model = nn.Sequential(
# 第一层:input_dim -> h1
nn.Linear(input_dim, h1),
nn.ReLU(),
# 第二层:h1 -> h2
nn.Linear(h1, h2),
nn.ReLU(),
# 第三层:h2 -> h3
nn.Linear(h2, h3),
nn.ReLU(),
# 输出层:h3 -> 1
nn.Linear(h3, 1),
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.model(x)
# 实例化模型
model = FeedForwardNN(input_dim=13, hidden_dims=(128, 64, 32))
2. 训练循环
def train_model(model, train_loader, test_loader, epochs, learning_rate, weight_decay):
"""训练神经网络"""
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
# 定义损失函数和优化器
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(
model.parameters(),
lr=learning_rate,
weight_decay=weight_decay # L2 正则
)
history = {"train_loss": [], "test_loss": []}
for epoch in range(1, epochs + 1):
# 训练阶段
model.train()
running_loss = 0.0
for features, targets in train_loader:
features, targets = features.to(device), targets.to(device)
# 前向传播
outputs = model(features)
loss = criterion(outputs, targets)
# 反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()
running_loss += loss.item()
avg_train_loss = running_loss / len(train_loader)
# 评估阶段
model.eval()
test_loss = 0.0
with torch.no_grad():
for features, targets in test_loader:
features, targets = features.to(device), targets.to(device)
outputs = model(features)
loss = criterion(outputs, targets)
test_loss += loss.item()
avg_test_loss = test_loss / len(test_loader)
# 记录历史
history["train_loss"].append(avg_train_loss)
history["test_loss"].append(avg_test_loss)
return history
3. 数据加载器
from torch.utils.data import DataLoader, TensorDataset
def to_dataloader(X: np.ndarray, y: np.ndarray, batch_size: int, shuffle: bool):
"""将 NumPy 数组转换为 PyTorch DataLoader"""
dataset = TensorDataset(
torch.from_numpy(X).float(),
torch.from_numpy(y).float(),
)
return DataLoader(dataset, batch_size=batch_size, shuffle=shuffle)
# 使用示例
train_loader = to_dataloader(X_train, y_train, batch_size=64, shuffle=True)
test_loader = to_dataloader(X_test, y_test, batch_size=len(y_test), shuffle=False)
⚠️ 注意事项
-
激活函数选择:
- ReLU 在隐藏层中表现好,训练快
- 输出层不加激活(线性),因为房价是连续值
-
权重衰减:weight_decay 参数实现 L2 正则,防止过拟合,建议 1e-4 到 1e-3
-
批量大小:
- 太小:训练不稳定,但泛化更好
- 太大:收敛快,但容易过拟合
- 建议 32-128
-
学习率:Adam 默认 0.001 通常有效,可尝试 0.0001-0.01
-
评估指标:
- RMSE:
sqrt(MSE),与目标同量纲 - R²:解释的方差比例,越接近 1 越好
- RMSE:
-
早停策略:监控验证损失,连续 N 轮不下降则停止训练
📊 预期结果
- 训练集 RMSE:约 2.0-2.5
- 测试集 RMSE:约 2.5-3.5
- R²:约 0.80-0.90
- 损失曲线应在 50-100 轮后趋于稳定
任务五:MNIST 卷积神经网络
📋 任务要求
编程实现一个三层卷积神经网络,并在手写字符识别数据 MNIST(http://yann.lecun.com/exdb/mnist/)上进行实验测试(15 分)
🎯 解题思路
1. 数据特点
- 70,000 张 28×28 灰度图像(60,000 训练 + 10,000 测试)
- 10 类手写数字(0-9)
- 像素值 0-255,需要归一化到 [0, 1] 并标准化
2. CNN 结构设计
- 卷积层 1:1 → 32 通道,3×3 卷积 + BN + ReLU + MaxPool(2×2)
- 卷积层 2:32 → 64 通道,3×3 卷积 + BN + ReLU + MaxPool(2×2)
- 卷积层 3:64 → 128 通道,3×3 卷积 + BN + ReLU
- 全连接层:展平后接 256 维 FC + ReLU + Dropout(0.3)
- 输出层:256 → 10(Softmax 分类)
3. 训练配置
- 损失函数:交叉熵
- 优化器:Adam
- 批量大小:128
- 数据增强:可选旋转、平移
💻 关键代码片段
1. 三层 CNN 定义
import torch.nn as nn
class ThreeLayerCNN(nn.Module):
"""三层卷积神经网络"""
def __init__(self, num_classes: int = 10):
super().__init__()
# 特征提取部分(3 层卷积)
self.features = nn.Sequential(
# 第 1 层卷积:1 -> 32
nn.Conv2d(1, 32, kernel_size=3, padding=1), # 28x28 -> 28x28
nn.BatchNorm2d(32),
nn.ReLU(inplace=True),
nn.MaxPool2d(2), # 28x28 -> 14x14
# 第 2 层卷积:32 -> 64
nn.Conv2d(32, 64, kernel_size=3, padding=1), # 14x14 -> 14x14
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(2), # 14x14 -> 7x7
# 第 3 层卷积:64 -> 128
nn.Conv2d(64, 128, kernel_size=3, padding=1), # 7x7 -> 7x7
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
)
# 分类部分(全连接)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(128 * 7 * 7, 256),
nn.ReLU(inplace=True),
nn.Dropout(0.3), # 防止过拟合
nn.Linear(256, num_classes),
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.features(x)
x = self.classifier(x)
return x
2. 数据加载与预处理
from torchvision import datasets, transforms
def load_mnist(random_seed: int, train_ratio: float, batch_size: int):
"""加载 MNIST 数据集"""
# 定义数据变换
transform = transforms.Compose([
transforms.ToTensor(), # 转换为 Tensor 并归一化到 [0, 1]
transforms.Normalize((0.1307,), (0.3081,)), # MNIST 的均值和标准差
])
# 加载训练集和测试集
full_train = datasets.MNIST(
root='./data',
train=True,
download=True,
transform=transform
)
test_dataset = datasets.MNIST(
root='./data',
train=False,
download=True,
transform=transform
)
# 将训练集拆分为训练/验证
train_len = int(len(full_train) * train_ratio)
val_len = len(full_train) - train_len
generator = torch.Generator().manual_seed(random_seed)
train_subset, val_subset = torch.utils.data.random_split(
full_train, [train_len, val_len], generator=generator
)
# 创建 DataLoader
train_loader = DataLoader(train_subset, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_subset, batch_size=batch_size, shuffle=False)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
return {
"train": train_loader,
"val": val_loader,
"test": test_loader,
}
3. 训练循环
def train_model(model, loaders, epochs, learning_rate, device):
"""训练 CNN"""
model.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
history = {"train_loss": [], "val_loss": [], "train_acc": [], "val_acc": []}
for epoch in range(1, epochs + 1):
# 训练阶段
model.train()
running_loss = 0.0
correct = 0
total = 0
for images, labels in loaders["train"]:
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() * images.size(0)
preds = outputs.argmax(dim=1)
correct += (preds == labels).sum().item()
total += labels.size(0)
train_loss = running_loss / total
train_acc = correct / total
# 验证阶段
model.eval()
val_loss = 0.0
val_correct = 0
val_total = 0
with torch.no_grad():
for images, labels in loaders["val"]:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
val_loss += loss.item() * images.size(0)
preds = outputs.argmax(dim=1)
val_correct += (preds == labels).sum().item()
val_total += labels.size(0)
val_loss /= val_total
val_acc = val_correct / val_total
# 记录历史
history["train_loss"].append(train_loss)
history["val_loss"].append(val_loss)
history["train_acc"].append(train_acc)
history["val_acc"].append(val_acc)
print(f"[Epoch {epoch}/{epochs}] "
f"Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.4f} | "
f"Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.4f}")
return history
⚠️ 注意事项
-
BatchNorm 的作用:
- 加速收敛,减少对初始化的敏感
- 内部归一化,降低梯度消失/爆炸风险
-
Dropout:
- 仅在全连接层使用,不在卷积层使用
- 训练时随机丢弃神经元,测试时关闭(
model.eval()自动处理)
-
填充(padding):
padding=1配合kernel_size=3保持特征图尺寸不变- 便于设计更深的网络
-
GPU 加速:
- 使用
model.to(device)和data.to(device) - CNN 计算密集,GPU 加速效果显著(10-50 倍)
- 使用
-
数据增强:
- 可选添加
RandomRotation、RandomAffine等 - 提升泛化能力,但增加训练时间
- 可选添加
-
模型保存:
torch.save(model.state_dict(), "mnist_cnn.pth") model.load_state_dict(torch.load("mnist_cnn.pth"))
📊 预期结果
- 训练集准确率:约 99.5%
- 验证集准确率:约 99.0%
- 测试集准确率:约 99.0-99.3%
- 12 个 epoch 后基本收敛
任务六:Wine 数据集 PCA 降维
📋 任务要求
编程实现一个主成分分析(PCA)算法,对 wine 数据集(https://archive.ics.uci.edu/dataset/109/wine)进行降维,并通过可视化手段展示降维效果(例如降维后的主成分样本空间分布等,也可采取其他手段)。(10 分)
🎯 解题思路
1. PCA 原理
- 目标:找到一组正交的投影方向,使得投影后的方差最大
- 等价于:最小化重构误差
||X - X̂||² - 数学表达:
min_W ||X - XW^T W||²,约束WW^T = I
2. 实现方式
- 传统方法:对协方差矩阵做特征分解
- 本实现:使用 PyTorch 在 GPU 上优化可学习的投影矩阵
- 优势:适用于大规模数据,训练过程可视化
3. 数据特点
- 178 个样本,13 个连续特征(酒精含量、酚类物质等)
- 3 类葡萄酒
- 标准化后执行 PCA
💻 关键代码片段
1. PCA 模型定义(PyTorch)
import torch
import torch.nn.functional as F
class TorchPCA(torch.nn.Module):
"""可学习的 PCA 投影矩阵"""
def __init__(self, n_features: int, n_components: int):
super().__init__()
# 初始化投影矩阵为随机值
self.components = torch.nn.Parameter(
torch.randn(n_components, n_features) * 0.1
)
def normalized_components(self) -> torch.Tensor:
"""对每个主成分进行 L2 归一化,保证正交性"""
return F.normalize(self.components, p=2, dim=1)
def forward(self, X: torch.Tensor):
"""前向传播:投影和重构"""
comps = self.normalized_components()
# 投影到低维空间:Z = X @ W^T
Z = X @ comps.T
# 重构到原始空间:X̂ = Z @ W
reconstruction = Z @ comps
return Z, reconstruction
2. 训练循环
def train_pca(data, config):
"""在 GPU 上训练 PCA"""
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 初始化模型
model = TorchPCA(
n_features=data["X_train"].shape[1],
n_components=config.n_components
)
model.to(device)
# 定义优化器
optimizer = torch.optim.Adam(
model.parameters(),
lr=config.learning_rate,
weight_decay=config.weight_decay,
)
# 将数据移到 GPU
train_tensor = torch.from_numpy(data["X_train"]).to(device)
test_tensor = torch.from_numpy(data["X_test"]).to(device)
history = {"train_loss": [], "test_loss": []}
for epoch in range(1, config.epochs + 1):
model.train()
# 前向传播
_, train_recon = model(train_tensor)
# 重构损失(MSE)
recon_loss = torch.mean((train_tensor - train_recon) ** 2)
# 正交损失(鼓励主成分相互正交)
comps = model.normalized_components()
identity = torch.eye(comps.shape[0], device=device)
orth_loss = torch.mean((comps @ comps.T - identity) ** 2)
# 总损失
loss = recon_loss + config.orthogonality_weight * orth_loss
# 反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 评估测试集
model.eval()
with torch.no_grad():
_, test_recon = model(test_tensor)
test_loss = torch.mean((test_tensor - test_recon) ** 2).item()
history["train_loss"].append(recon_loss.item())
history["test_loss"].append(test_loss)
print(f"[Epoch {epoch}/{config.epochs}] "
f"Train Loss: {recon_loss.item():.6f} | "
f"Test Loss: {test_loss:.6f}")
# 提取投影结果
model.eval()
with torch.no_grad():
train_proj, _ = model(train_tensor)
test_proj, _ = model(test_tensor)
# 转换为 NumPy
train_proj_np = train_proj.cpu().numpy()
test_proj_np = test_proj.cpu().numpy()
# 计算解释方差
cov_matrix = np.cov(train_proj_np, rowvar=False)
explained_variance = np.diag(cov_matrix)
explained_variance_ratio = explained_variance / explained_variance.sum()
return TrainingResult(
history=history,
explained_variance=explained_variance,
explained_variance_ratio=explained_variance_ratio,
projected_train=train_proj_np,
projected_test=test_proj_np,
train_targets=data["y_train"],
test_targets=data["y_test"],
)
3. 可视化降维结果
def plot_embedding_scatter(projections, targets, path, title_suffix):
"""绘制降维后的散点图(前两个主成分)"""
plt.figure(figsize=(6, 5))
# 定义颜色
palette = {1: "#1f77b4", 2: "#ff7f0e", 3: "#2ca02c"}
# 按类别绘制
for label in np.unique(targets):
mask = targets == label
plt.scatter(
projections[mask, 0],
projections[mask, 1],
alpha=0.7,
label=f"类别 {label}",
color=palette.get(label, None),
edgecolor="k",
linewidths=0.2,
)
plt.xlabel("主成分 1")
plt.ylabel("主成分 2")
plt.title(f"PCA 降维散点图({title_suffix})")
plt.legend()
plt.grid(alpha=0.3)
plt.tight_layout()
plt.savefig(path, dpi=300)
plt.close()
4. 绘制方差贡献
def plot_variance_ratio(explained_ratio, path):
"""绘制各主成分的方差贡献"""
cum_ratio = np.cumsum(explained_ratio)
plt.figure(figsize=(6, 4))
# 条形图:单个主成分贡献
plt.bar(range(1, len(explained_ratio) + 1), explained_ratio,
alpha=0.7, label="单个主成分贡献")
# 折线图:累计贡献率
plt.step(range(1, len(explained_ratio) + 1), cum_ratio,
where="mid", label="累计贡献率", color="red")
plt.xlabel("主成分序号")
plt.ylabel("方差贡献比")
plt.title("主成分方差贡献可视化")
plt.legend()
plt.tight_layout()
plt.savefig(path, dpi=300)
plt.close()
⚠️ 注意事项
-
标准化必要性:
- PCA 对特征尺度敏感,必须先标准化
- 使用
StandardScaler使每个特征均值为 0,方差为 1
-
正交约束:
- 通过 L2 归一化 + 正交损失实现
- 正交损失:
||WW^T - I||²,惩罚非正交情况
-
主成分数量选择:
- 根据累计贡献率:通常保留 85-95% 方差
- 可视化需求:2-3 个主成分
-
解释方差计算:
- 投影后数据的协方差矩阵对角线元素
- 归一化得到方差贡献比
-
与 sklearn 对比:
- sklearn PCA 基于 SVD,确定性结果
- 本实现基于梯度下降,结果可能略有不同但等价
-
GPU 加速:
- 小数据集(< 1000 样本)加速不明显
- 大数据集或高维特征时优势显著
📊 预期结果
- 前两个主成分:解释 50-70% 方差
- 前三个主成分:解释 70-85% 方差
- 散点图:不同类别形成相对分离的簇
- 训练损失:100 轮后收敛到 0.01-0.05
任务七:Mall Customers K-means 聚类
📋 任务要求
编程实现一个 K 均值聚类算法,并在 mall customers 数据集(https://github.com/Karansingh1221/Mall_Customer_dataset)上进行实验验证。
🎯 解题思路
1. K-means 原理
- 目标:将样本分为 K 个簇,最小化簇内平方距离和(Inertia)
- 算法流程:
- 随机初始化 K 个簇中心
- 分配:将每个样本分配到最近的簇中心
- 更新:重新计算每个簇的中心(均值)
- 重复 2-3 直到收敛
2. 数据特点
- 200 个样本,4 个特征:性别、年龄、年收入、消费得分
- 无监督学习,无真实标签
- 需要标准化特征
3. 实验设计
- 尝试不同 K 值(如 3, 5, 7)
- 对同一 K 值尝试不同初始化种子
- 使用 Silhouette 系数评估聚类质量
💻 关键代码片段
1. K-means 实现(PyTorch GPU 版本)
import torch
class TorchKMeans:
"""基于 PyTorch 的 K-means(GPU 加速)"""
def __init__(
self,
n_clusters: int,
max_iters: int = 100,
tol: float = 1e-4,
random_state: Optional[int] = None,
device: torch.device = torch.device("cpu"),
):
self.n_clusters = n_clusters
self.max_iters = max_iters
self.tol = tol
self.random_state = random_state
self.device = device
self.centers_ = None
self.inertia_history = []
def _init_centers(self, X: torch.Tensor) -> torch.Tensor:
"""初始化簇中心"""
if self.random_state is not None:
torch.manual_seed(self.random_state)
# 随机选择 K 个样本作为初始中心
indices = torch.randperm(X.shape[0], device=self.device)[:self.n_clusters]
return X[indices].clone()
def fit(self, X_np: np.ndarray):
"""训练 K-means"""
# 转换为 Tensor 并移到 GPU
X = torch.from_numpy(X_np).to(self.device)
centers = self._init_centers(X)
previous_inertia = None
for iter_idx in range(1, self.max_iters + 1):
# 步骤 1:分配 - 计算每个样本到各中心的距离
distances = torch.cdist(X, centers, p=2) # 欧氏距离
labels = torch.argmin(distances, dim=1) # 选择最近的中心
# 计算 Inertia(平均平方距离)
inertia = torch.mean(torch.min(distances, dim=1).values ** 2).item()
self.inertia_history.append(inertia)
print(f"[Iter {iter_idx}/{self.max_iters}] "
f"K={self.n_clusters} - Inertia: {inertia:.6f}")
# 步骤 2:更新 - 重新计算簇中心
new_centers = []
for k in range(self.n_clusters):
mask = labels == k
if mask.any():
# 簇中心 = 簇内样本的均值
new_centers.append(X[mask].mean(dim=0))
else:
# 空簇:随机重新初始化
rand_idx = torch.randint(0, X.shape[0], (1,), device=self.device)
new_centers.append(X[rand_idx].squeeze(0))
centers = torch.stack(new_centers, dim=0)
# 收敛判断
if previous_inertia is not None and abs(previous_inertia - inertia) < self.tol:
print(f"Converged at iteration {iter_idx}")
break
previous_inertia = inertia
# 保存结果
self.centers_ = centers
self.labels_ = labels.detach().cpu().numpy()
return self
def predict(self, X_np: np.ndarray) -> np.ndarray:
"""预测新样本的簇标签"""
if self.centers_ is None:
raise ValueError("Model not fitted")
X = torch.from_numpy(X_np).to(self.device)
distances = torch.cdist(X, self.centers_, p=2)
labels = torch.argmin(distances, dim=1)
return labels.detach().cpu().numpy()
2. 运行多个实验
def run_experiments(data, config):
"""运行不同 K 值和初始化的实验"""
results = []
X = data["X_scaled"]
device = config.device
# 实验 1:不同 K 值
for k in config.k_values: # 例如 [3, 5, 7]
model = TorchKMeans(
n_clusters=k,
max_iters=config.max_iters,
tol=config.tol,
random_state=42,
device=device,
)
model.fit(X)
# 计算 Silhouette 系数
silhouette = silhouette_score(X, model.labels_)
results.append(KMeansResult(
name=f"K={k}",
k=k,
inertia_history=model.inertia_history.copy(),
labels=model.labels_,
centers=model.centers_.cpu().numpy(),
silhouette=silhouette,
init_seed=42,
))
# 实验 2:同一 K 值,不同初始化
for seed in config.fixed_seeds: # 例如 [11, 99]
model = TorchKMeans(
n_clusters=config.fixed_k, # 例如 5
max_iters=config.max_iters,
tol=config.tol,
random_state=seed,
device=device,
)
model.fit(X)
silhouette = silhouette_score(X, model.labels_)
results.append(KMeansResult(
name=f"K={config.fixed_k}, seed={seed}",
k=config.fixed_k,
inertia_history=model.inertia_history.copy(),
labels=model.labels_,
centers=model.centers_.cpu().numpy(),
silhouette=silhouette,
init_seed=seed,
))
return results
3. 可视化聚类结果
def plot_scatter(data, labels, path, title):
"""绘制聚类散点图"""
plt.figure(figsize=(6, 5))
# 选择两个特征进行可视化
age = data["X_original"][:, 0] # Age
income = data["X_original"][:, 1] # Annual Income
# 按簇标签着色
scatter = plt.scatter(
age, income,
c=labels,
cmap="tab10",
alpha=0.75,
edgecolor="k",
linewidths=0.2
)
plt.xlabel("Age")
plt.ylabel("Annual Income (k$)")
plt.title(title)
plt.grid(alpha=0.3)
plt.colorbar(scatter, label="Cluster")
plt.tight_layout()
plt.savefig(path, dpi=300)
plt.close()
4. 肘部法则(Elbow Method)
def plot_k_vs_metrics(results, path):
"""绘制不同 K 值的指标对比"""
unique_k = sorted({res.k for res in results})
# 计算平均 Inertia 和 Silhouette
avg_inertia = []
avg_silhouette = []
for k in unique_k:
matching = [res for res in results if res.k == k]
avg_inertia.append(np.mean([res.inertia_history[-1] for res in matching]))
avg_silhouette.append(np.mean([res.silhouette for res in matching]))
# 双 Y 轴图
fig, ax1 = plt.subplots(figsize=(6.5, 4.5))
# 左 Y 轴:Inertia
ax1.set_xlabel("K")
ax1.set_ylabel("最终 Inertia", color="#1f77b4")
ax1.plot(unique_k, avg_inertia, marker="o", color="#1f77b4")
ax1.tick_params(axis="y", labelcolor="#1f77b4")
# 右 Y 轴:Silhouette
ax2 = ax1.twinx()
ax2.set_ylabel("平均 Silhouette", color="#ff7f0e")
ax2.plot(unique_k, avg_silhouette, marker="s", color="#ff7f0e")
ax2.tick_params(axis="y", labelcolor="#ff7f0e")
plt.title("不同 K 值的聚类指标对比")
fig.tight_layout()
plt.savefig(path, dpi=300)
plt.close()
⚠️ 注意事项
-
K 值选择方法:
- 肘部法则:Inertia 随 K 增大而减小,找到拐点
- Silhouette 系数:范围 [-1, 1],越接近 1 越好
- 业务需求:根据实际场景确定簇数量
-
标准化重要性:
- K-means 基于欧氏距离,对特征尺度敏感
- 必须使用
StandardScaler标准化
-
初始化敏感性:
- K-means 对初始中心敏感,可能陷入局部最优
- 建议多次运行(不同随机种子),选择 Inertia 最小的
-
空簇处理:
- 若某个簇没有样本分配,随机重新初始化该中心
- 或采用 K-means++ 初始化策略
-
收敛判断:
- 两种方式:中心不再变化 或 Inertia 变化小于阈值
- 设置
max_iters防止无限循环
-
Silhouette 系数解释:
s > 0.5:簇结构合理s < 0.25:簇结构较弱,可能需要调整 K
-
GPU 加速效果:
- 样本数 > 10000 时显著加速
- 小数据集(如 Mall Customers 200 样本)加速不明显
📊 预期结果
- K=3:Inertia 较高,Silhouette 约 0.4-0.5
- K=5:Inertia 适中,Silhouette 约 0.5-0.6(较优)
- K=7:Inertia 最低,但 Silhouette 下降(过拟合)
- 不同初始化:Inertia 差异 < 5%
📌 通用注意事项
1. 环境配置
# 安装依赖
pip install numpy pandas matplotlib seaborn scikit-learn
pip install torch torchvision # PyTorch(GPU 版本根据 CUDA 版本选择)
pip install python-docx requests
# 检查 GPU 是否可用
python -c "import torch; print(torch.cuda.is_available())"
2. 项目结构规范
TaskX_Dataset_Model/
├── data/ # 数据集目录
│ └── dataset.csv
├── outputs/ # 输出目录
│ ├── metrics.txt # 评估指标
│ ├── history.csv # 训练历史
│ └── *.png # 可视化图表
├── report/ # 报告目录
│ └── report.docx
└── main.py # 主程序
3. 代码规范
- 使用
dataclass组织配置和结果 - 函数功能单一,命名清晰
- 添加类型注解(Type Hints)
- 使用
Path对象处理路径(跨平台兼容) - 异常处理:网络请求、文件读写
4. 可复现性
# 固定所有随机种子
RANDOM_SEED = 42
# NumPy
np.random.seed(RANDOM_SEED)
# PyTorch
torch.manual_seed(RANDOM_SEED)
torch.cuda.manual_seed(RANDOM_SEED)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
# sklearn
random_state=RANDOM_SEED
5. 训练日志
# 实时输出训练进度
print(f"[Epoch {epoch:03d}/{epochs}] "
f"Train Loss: {train_loss:.4f}, "
f"Test Loss: {test_loss:.4f}")
# 保存历史到 CSV
pd.DataFrame(history).to_csv("history.csv", index=False)
6. 可视化技巧
# 中文字体支持(Windows)
plt.rcParams["font.family"] = "SimHei"
plt.rcParams["axes.unicode_minus"] = False
# 高分辨率保存
plt.savefig("figure.png", dpi=300, bbox_inches="tight")
# 多子图布局
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
7. 内存优化
- 使用
batch_size避免一次性加载所有数据 - 及时释放不用的变量:
del variable - PyTorch 显存管理:
with torch.no_grad(): # 推理阶段不计算梯度 predictions = model(X_test) # 清空显存 torch.cuda.empty_cache()
8. 调试技巧
# 打印张量形状
print(f"X.shape: {X.shape}, y.shape: {y.shape}")
# 检查数据范围
print(f"X: min={X.min():.4f}, max={X.max():.4f}, mean={X.mean():.4f}")
# 检查梯度
for name, param in model.named_parameters():
if param.grad is not None:
print(f"{name}: grad_norm={param.grad.norm():.4f}")
🎓 学习资源推荐
-
在线课程:
- 吴恩达《机器学习》(Coursera)
- 李宏毅《机器学习》(B 站)
-
书籍:
- 《机器学习》周志华
- 《深度学习》Ian Goodfellow
-
文档:
- scikit-learn 官方文档
- PyTorch 官方教程
-
论坛:
- Stack Overflow
- GitHub Issues
✅ 总结
本指南涵盖了七个经典机器学习任务的完整实现:
| 任务 | 算法 | 关键技术 | 难度 |
|---|---|---|---|
| Iris 线性回归 | 梯度下降 | 特征工程、标准化 | ⭐⭐ |
| Breast Cancer 逻辑回归 | 批量梯度下降 | 噪声对比、正则化 | ⭐⭐ |
| Image Segmentation SVM | 合页损失、RFF | 核方法、OvR 策略 | ⭐⭐⭐ |
| Boston Housing 神经网络 | 反向传播 | PyTorch、Adam 优化 | ⭐⭐⭐ |
| MNIST CNN | 卷积神经网络 | BatchNorm、Dropout | ⭐⭐⭐⭐ |
| Wine PCA | 降维 | GPU 加速、可视化 | ⭐⭐⭐ |
| Mall Customers K-means | 聚类 | Silhouette、肘部法则 | ⭐⭐⭐ |
核心要点:
- 数据预处理(标准化、编码)至关重要
- 超参数调优需要结合验证集和可视化
- GPU 加速适用于大规模数据和深度模型
- 可视化有助于理解模型行为和调试
- 代码规范性和可复现性同样重要
🎓祝你天天开心!!
最后更新:2025年11月
作者:Echo
1038

被折叠的 条评论
为什么被折叠?



