#import "labtemplate.typ": *
#show: nudtlabpaper.with(
  author: "程景愉", 
  id: "202302723005", 
  title: "机器学习",
  training_type: "普通本科生",
  grade: "2023级",
  major: "网络工程",
  department: "计算机学院",
  advisor: "胡罡",
  jobtitle: "教授",
  lab: "306-707",
  date: "2025.12.10",
  header_str: "机器学习实验报告",
  simple_cover: true,
)

#set page(header: [
    #set par(spacing: 6pt)
    #align(center)[#text(size: 11pt)[人工智能实验报告]]
    #v(-0.3em)
    #line(length: 100%, stroke: (thickness: 1pt))
],)

#show heading: it => if it.level == 1 [
    #v(1em)
    #set text(font: hei, size: 16pt)
    #counter(heading).display()
    #h(0.5em)
    #it.body
    #v(0.5em)
] else [
    #v(0.8em)
    #set text(font: "New Computer Modern", weight: "bold", style: "italic")
    #counter(heading).display(it.numbering)
    #h(0.5em)
    #it.body
    #v(0.5em)
]

#outline(title: "目录",depth: 2, indent: 1em)

#set enum(indent: 0.5em,body-indent: 0.5em,)
#pagebreak()

= 实验介绍
#para[
本项目是 CS188 课程的第五个项目，主题为机器学习（Machine Learning）。实验的主要目的是通过实践操作，熟悉机器学习的基本概念、神经网络的构建过程以及 PyTorch 深度学习框架的使用。在本项目中，我们将从最基础的感知机开始，逐步构建非线性回归模型、用于手写数字识别的多层感知机（MLP），以及卷积神经网络（CNN）。通过这些任务，我们将深入理解神经网络的初始化、前向传播、损失计算、反向传播以及参数更新等核心机制。
]

= 实验内容
#para[
本次实验主要包含以下四个核心编程任务，所有代码实现均在 `models.py` 文件中进行：
]
+ *Q1: Perceptron (感知机)*：实现一个简单的二元分类感知机，理解基本的线性分类器更新规则。
+ *Q2: Non-linear Regression (非线性回归)*：构建一个两层的神经网络，用于拟合正弦函数 $sin(x)$，理解如何利用神经网络的非线性能力进行函数逼近。
+ *Q3: Digit Classification (数字分类)*：设计并训练一个多层全连接神经网络，对 MNIST 手写数字数据集进行分类，掌握分类任务的损失函数和模型评估方法。
+ *Q5: Convolutional Neural Networks (卷积神经网络)*：手动实现二维卷积操作，并基于此构建卷积神经网络，理解 CNN 在处理图像数据时的优势。

= 实验要求
#para[
根据项目指导文档，实验的具体要求如下：
]
+ *文件修改*：仅允许修改 `models.py` 文件，其余文件（如 `autograder.py` 等）需保持原样，以确保自动评分器正常运行。
+ *框架使用*：需熟练使用 PyTorch 提供的 `Tensor` 操作、`nn.Parameter`、`optim` 优化器等工具。
+ *性能达标*：
  - Q1：感知机需在训练集上收敛（准确率 100%）。
  - Q2：回归模型的平均训练损失（MSE）需低于 0.02。
  - Q3：数字分类模型在测试集上的准确率需达到 97% 以上。
  - Q5：CNN 模型在简化版数据集上的准确率需达到 80% 以上。
+ *禁止事项*：Q3 的输出层不得使用 ReLU 激活函数；代码需通过自动评分器的技术正确性检查。

= 实验步骤与实现

== Q1: Perceptron (感知机)
*实现思路*：
感知机是一个最简单的线性二分类模型。我们的目标是找到一个权重向量 $w$，使得对于输入 $x$，如果 $w dot x >= 0$ 则预测为类别 1，否则为 -1。

1.  *初始化*：使用 `torch.nn.Parameter` 初始化权重 `self.w`，维度为 `(1, dimensions)`，初始化为全 1 向量。
2.  *计算得分*：计算输入 $x$ 与权重 $w$ 的点积。使用 `tensordot` 计算。
3.  *预测与训练*：`get_prediction` 根据得分符号返回 1 或 -1。`train` 方法中，我们遍历数据集。如果预测错误，则根据感知机规则更新权重：$w <- w + y dot x$。

*核心代码* (`models.py`):
```python
class PerceptronModel(Module):
    def __init__(self, dimensions):
        super(PerceptronModel, self).__init__()
        self.w = Parameter(ones(1, dimensions))

    def run(self, x):
        return tensordot(self.w, x, dims=2)

    def train(self, dataset):
        with no_grad():
            dataloader = DataLoader(dataset, batch_size=1, shuffle=True)
            while True:
                converged = True
                for batch in dataloader:
                    x = batch['x']
                    y = batch['label']
                    prediction = self.get_prediction(x)
                    if prediction != y.item():
                        self.w.data += y.item() * x
                        converged = False
                if converged:
                    break
```

*测试指令*:
```fish
python autograder.py -q q1
```

== Q2: Non-linear Regression (非线性回归)
*实现思路*：
为了拟合非线性的 $sin(x)$ 函数，我们需要引入非线性激活函数。我们构建了一个三层的神经网络：
- 第一层：线性层 `Linear(1, 256)` + ReLU。
- 第二层：线性层 `Linear(256, 256)` + ReLU。
- 第三层：线性层 `Linear(256, 1)`，输出预测值。

训练时使用 Adam 优化器和 MSE 损失函数。为了加速收敛，我们还引入了学习率调度器 (`StepLR`)，每 250 个 step 将学习率衰减为原来的 0.1。

*核心代码* (`models.py`):
```python
class RegressionModel(Module):
    def __init__(self):
        super().__init__()
        self.layer1 = Linear(1, 256)
        self.layer2 = Linear(256, 256)
        self.layer3 = Linear(256, 1)

    def forward(self, x):
        x = relu(self.layer1(x))
        x = relu(self.layer2(x))
        return self.layer3(x)

    def train(self, dataset):
        batch_size = 40
        dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
        optimizer = optim.Adam(self.parameters(), lr=0.001)
        scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=250, gamma=0.1)
        
        for epoch in range(1000): 
            total_loss = 0
            num_samples = 0
            for batch in dataloader:
                optimizer.zero_grad()
                loss = self.get_loss(batch['x'], batch['label'])
                loss.backward()
                optimizer.step()
                total_loss += loss.item() * len(batch['x'])
                num_samples += len(batch['x'])
            
            scheduler.step()
            if total_loss / num_samples < 0.02:
                break
```

*测试指令*:
```fish
python autograder.py -q q2
```

== Q3: Digit Classification (数字分类)
*实现思路*：
针对 MNIST 28x28 的图像分类任务，我们构建了一个三层全连接网络：
- 输入层：784 维（28*28 展平）。
- 隐藏层 1：`Linear(784, 256)` + ReLU。
- 隐藏层 2：`Linear(256, 128)` + ReLU。
- 输出层：`Linear(128, 10)`，直接输出 Logits。

使用 `cross_entropy` 损失函数和 Adam 优化器，批大小为 100，训练 5 个 Epoch 即可达到目标准确率。

*核心代码* (`models.py`):
```python
class DigitClassificationModel(Module):
    def __init__(self):
        super().__init__()
        input_size = 28 * 28
        output_size = 10
        self.layer1 = Linear(input_size, 256)
        self.layer2 = Linear(256, 128)
        self.layer3 = Linear(128, output_size)

    def run(self, x):
        x = relu(self.layer1(x))
        x = relu(self.layer2(x))
        return self.layer3(x)

    def train(self, dataset):
        batch_size = 100
        dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
        optimizer = optim.Adam(self.parameters(), lr=0.001)

        for epoch in range(5):
            for batch in dataloader:
                optimizer.zero_grad()
                loss = self.get_loss(batch['x'], batch['label'])
                loss.backward()
                optimizer.step()
```

*测试指令*:
```fish
python autograder.py -q q3
```

== Q5: Convolutional Neural Networks (卷积神经网络)
*实现思路*：
本任务的核心是手动实现二维卷积操作。`Convolve` 函数通过双重循环遍历输出矩阵的每个位置，计算输入子矩阵与卷积核的点积（使用 `tensordot`）。
基于此，我们构建了一个简单的 CNN：
1.  卷积层：使用 `Convolve` 处理输入，卷积核大小 3x3，输出特征图大小为 26x26。
2.  展平：将特征图展平为 676 维向量。
3.  全连接层 1：`Linear(676, 100)` + ReLU。
4.  全连接层 2：`Linear(100, 10)`。

*核心代码* (`models.py`):
```python
def Convolve(input: tensor, weight: tensor):
    input_h, input_w = input.shape
    weight_h, weight_w = weight.shape
    output_h = input_h - weight_h + 1
    output_w = input_w - weight_w + 1
    Output_Tensor = torch.zeros((output_h, output_w))
    
    for y in range(output_h):
        for x in range(output_w):
            sub_tensor = input[y:y+weight_h, x:x+weight_w]
            Output_Tensor[y, x] = tensordot(sub_tensor, weight, dims=2)
    return Output_Tensor

class DigitConvolutionalModel(Module):
    def __init__(self):
        super().__init__()
        self.convolution_weights = Parameter(ones((3, 3)))
        self.layer1 = Linear(26 * 26, 100)
        self.layer2 = Linear(100, 10)

    def forward(self, x):
        x = x.reshape(len(x), 28, 28)
        x = stack(list(map(lambda sample: Convolve(sample, self.convolution_weights), x)))
        x = x.flatten(start_dim=1)
        x = relu(self.layer1(x))
        return self.layer2(x)
```

*测试指令*:
```fish
python autograder.py -q q5
```

= 实验结果
#para[
本实验所有代码均通过 `autograder.py` 的测试，各项指标均达到或超过实验要求。
]

+ *Q1 感知机*：模型能够迅速收敛，在训练数据上实现了 100% 的分类准确率。
+ *Q2 非线性回归*：训练后的模型 Loss 稳定在 0.02 以下，能够很好地拟合 $sin(x)$ 曲线。
+ *Q3 数字分类*：构建的 MLP 模型在 MNIST 测试集上的准确率超过 97%（Staff 参考值为 98%），训练过程高效稳定。
+ *Q5 CNN*：手动实现的卷积层逻辑正确，通过了梯度检查。构建的 CNN 模型在测试数据集上准确率超过 80%，验证了卷积特征提取的有效性。

#figure(image("screenshot.png"))


#pagebreak()
= 实验总结
#para[
通过本次 Project 5 的实验，我收获颇丰：
]
#para[
首先，我熟练掌握了 *PyTorch* 框架的基础操作。从定义 `nn.Parameter` 到构建 `nn.Linear` 层，再到使用 `optim.Adam` 和各类 Loss 函数，我对深度学习模型的代码实现流程有了清晰的认识。
]
#para[
其次，我深入理解了*神经网络的内部机制*。通过 Q1，我复习了线性分类器的原理；通过 Q2，我直观体会到了激活函数（ReLU）引入非线性的重要性——没有它，多层网络也仅仅是线性变换的叠加；通过 Q3，我掌握了处理多分类问题的标准范式（CrossEntropy + Softmax Logits）。
]
#para[
最后，Q5 的手动实现卷积让我对 CNN 有了更底层的理解。以前只知道调用 API，现在通过自己编写滑动窗口和点积运算，深刻理解了权重共享和局部感知野的概念。这次实验不仅锻炼了编程能力，也为后续深入学习深度学习打下了坚实基础。
]
