【机器学习及其应用】南航NUAA大作业-机器学习及其应用作业题目完全解析,保姆级指导!!(附源代码)

南航NUAA机器学习及其应用解题指南

本文档详细讲解七个机器学习任务的解题思路、关键代码实现和注意事项,帮助您理解从数据处理到模型训练的完整流程。


目录

  1. 任务一:Iris 数据集线性回归
  2. 任务二:Breast Cancer Wisconsin 逻辑回归
  3. 任务三:Image Segmentation SVM 分类
  4. 任务四:Boston Housing 神经网络回归
  5. 任务五:MNIST 卷积神经网络
  6. 任务六:Wine 数据集 PCA 降维
  7. 任务七: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,
    }

⚠️ 注意事项

  1. 特征标准化:不同特征的量纲差异大,必须标准化,否则梯度下降收敛慢或不收敛
  2. 学习率选择:太大会震荡,太小会收敛慢。建议从 0.01-0.1 之间尝试
  3. 偏置项处理:通过在特征矩阵前添加全 1 列,将偏置项和权重统一处理
  4. 独热编码:类别特征需要转换为数值,drop_first=False 保留所有类别信息
  5. 测试集不泄漏: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)

⚠️ 注意事项

  1. 数值稳定性:sigmoid 函数可能溢出,使用 np.clip 限制概率范围
  2. 正则化:偏置项不参与 L2 正则,否则会引入不必要的惩罚
  3. 分层抽样:使用 stratify=y 保证训练/测试集类别比例一致
  4. 学习率调整:逻辑回归对学习率较敏感,建议 0.01-0.1
  5. 噪声强度:标准高斯噪声 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)

⚠️ 注意事项

  1. 合页损失的次梯度:在 margin < 1 时才有梯度,注意条件判断
  2. OvR 策略:每个类别训练一个二分类器,最终取得分最高的类
  3. 核参数选择:γ 控制 RBF 核的宽度,太大导致过拟合,太小欠拟合。建议先用验证集粗调(0.01-0.1)
  4. RFF 维度:维度越高近似越准确,但计算成本增加。600-1000 通常足够
  5. 正则化系数:λ 控制模型复杂度,建议 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)

⚠️ 注意事项

  1. 激活函数选择

    • ReLU 在隐藏层中表现好,训练快
    • 输出层不加激活(线性),因为房价是连续值
  2. 权重衰减:weight_decay 参数实现 L2 正则,防止过拟合,建议 1e-4 到 1e-3

  3. 批量大小

    • 太小:训练不稳定,但泛化更好
    • 太大:收敛快,但容易过拟合
    • 建议 32-128
  4. 学习率:Adam 默认 0.001 通常有效,可尝试 0.0001-0.01

  5. 评估指标

    • RMSE:sqrt(MSE),与目标同量纲
    • R²:解释的方差比例,越接近 1 越好
  6. 早停策略:监控验证损失,连续 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

⚠️ 注意事项

  1. BatchNorm 的作用

    • 加速收敛,减少对初始化的敏感
    • 内部归一化,降低梯度消失/爆炸风险
  2. Dropout

    • 仅在全连接层使用,不在卷积层使用
    • 训练时随机丢弃神经元,测试时关闭(model.eval() 自动处理)
  3. 填充(padding)

    • padding=1 配合 kernel_size=3 保持特征图尺寸不变
    • 便于设计更深的网络
  4. GPU 加速

    • 使用 model.to(device)data.to(device)
    • CNN 计算密集,GPU 加速效果显著(10-50 倍)
  5. 数据增强

    • 可选添加 RandomRotationRandomAffine
    • 提升泛化能力,但增加训练时间
  6. 模型保存

    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()

⚠️ 注意事项

  1. 标准化必要性

    • PCA 对特征尺度敏感,必须先标准化
    • 使用 StandardScaler 使每个特征均值为 0,方差为 1
  2. 正交约束

    • 通过 L2 归一化 + 正交损失实现
    • 正交损失:||WW^T - I||²,惩罚非正交情况
  3. 主成分数量选择

    • 根据累计贡献率:通常保留 85-95% 方差
    • 可视化需求:2-3 个主成分
  4. 解释方差计算

    • 投影后数据的协方差矩阵对角线元素
    • 归一化得到方差贡献比
  5. 与 sklearn 对比

    • sklearn PCA 基于 SVD,确定性结果
    • 本实现基于梯度下降,结果可能略有不同但等价
  6. 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)
  • 算法流程:
    1. 随机初始化 K 个簇中心
    2. 分配:将每个样本分配到最近的簇中心
    3. 更新:重新计算每个簇的中心(均值)
    4. 重复 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()

⚠️ 注意事项

  1. K 值选择方法

    • 肘部法则:Inertia 随 K 增大而减小,找到拐点
    • Silhouette 系数:范围 [-1, 1],越接近 1 越好
    • 业务需求:根据实际场景确定簇数量
  2. 标准化重要性

    • K-means 基于欧氏距离,对特征尺度敏感
    • 必须使用 StandardScaler 标准化
  3. 初始化敏感性

    • K-means 对初始中心敏感,可能陷入局部最优
    • 建议多次运行(不同随机种子),选择 Inertia 最小的
  4. 空簇处理

    • 若某个簇没有样本分配,随机重新初始化该中心
    • 或采用 K-means++ 初始化策略
  5. 收敛判断

    • 两种方式:中心不再变化 或 Inertia 变化小于阈值
    • 设置 max_iters 防止无限循环
  6. Silhouette 系数解释

    • s > 0.5:簇结构合理
    • s < 0.25:簇结构较弱,可能需要调整 K
  7. 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}")

🎓 学习资源推荐

  1. 在线课程

    • 吴恩达《机器学习》(Coursera)
    • 李宏毅《机器学习》(B 站)
  2. 书籍

    • 《机器学习》周志华
    • 《深度学习》Ian Goodfellow
  3. 文档

    • scikit-learn 官方文档
    • PyTorch 官方教程
  4. 论坛

    • 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、肘部法则⭐⭐⭐

核心要点

  1. 数据预处理(标准化、编码)至关重要
  2. 超参数调优需要结合验证集和可视化
  3. GPU 加速适用于大规模数据和深度模型
  4. 可视化有助于理解模型行为和调试
  5. 代码规范性和可复现性同样重要

🎓祝你天天开心!!

最后更新:2025年11月
作者:Echo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值