{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "fe30ef91-808e-4a96-8ae2-ccf4ee9ba6db",
   "metadata": {},
   "source": [
    "#### 训练一个卷积神经网络，用于分类MNIST\n",
    "#### Train a convolutional neural network to classify MNIST"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "16591229-4667-41ed-9bfd-2f13015ce85c",
   "metadata": {
    "ExecutionIndicator": {
     "show": true
    },
    "scrolled": true,
    "tags": []
   },
   "outputs": [],
   "source": [
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "import torch.optim as optim\n",
    "from torch.utils.data import DataLoader\n",
    "from torchvision import datasets, transforms\n",
    "from torch.utils.tensorboard import SummaryWriter\n",
    "\n",
    "# 超参数。 Hyperparameters\n",
    "batch_size = 64\n",
    "learning_rate = 0.01\n",
    "num_epochs = 5\n",
    "\n",
    "# 检查是否有可用的 GPU\n",
    "# Check if GPU is available\n",
    "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
    "\n",
    "# 数据预处理\n",
    "# data preprocessing\n",
    "transform = transforms.Compose([\n",
    "    transforms.ToTensor(),\n",
    "    transforms.Normalize((0.5,), (0.5,))\n",
    "])\n",
    "\n",
    "# 自动下载 MNIST 数据集，分为训练集和验证集。\n",
    "# Automatically download the MNIST dataset and split it into training and validation sets.\n",
    "train_dataset = datasets.MNIST(root='./dataSet', train=True, download=True, transform=transform)\n",
    "val_dataset = datasets.MNIST(root='./dataSet', train=False, download=True, transform=transform)\n",
    "\n",
    "train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)\n",
    "val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)\n",
    "\n",
    "\n",
    "# 定义简单的神经网络\n",
    "# define a simple neural network.\n",
    "class SimpleCNN(nn.Module):\n",
    "    def __init__(self):\n",
    "        super(SimpleCNN, self).__init__()\n",
    "        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)\n",
    "        self.conv2 = nn.Conv2d(10, 20, kernel_size=3)\n",
    "        self.fc1 = nn.Linear(20 * 5 * 5, 128)\n",
    "        self.fc2 = nn.Linear(128, 10)\n",
    "\n",
    "    def forward(self, x):\n",
    "        x = F.max_pool2d(F.relu(self.conv1(x)), 2)\n",
    "        x = F.max_pool2d(F.relu(self.conv2(x)), 2)\n",
    "        x = x.view(-1, 20 * 5 * 5)\n",
    "        x = F.relu(self.fc1(x))\n",
    "        x = self.fc2(x)\n",
    "        return x\n",
    "\n",
    "\n",
    "# 实例化模型，并将其移动到 GPU 上（如果可用）\n",
    "# Instantiate the model and move it to the GPU (if available).\n",
    "model = SimpleCNN().to(device)\n",
    "criterion = nn.CrossEntropyLoss()\n",
    "optimizer = optim.SGD(model.parameters(), lr=learning_rate)\n",
    "\n",
    "# 创建 TensorBoard 的 SummaryWriter，可用于可视化的查看模型训练过程\n",
    "# Create a TensorBoard SummaryWriter to visualize the training process\n",
    "writer = SummaryWriter('./output/runs/mnist_experiment')\n",
    "\n",
    "best_val_accuracy = 0.0\n",
    "\n",
    "# 训练模型并记录损失和准确率\n",
    "# Train the model and log loss and accuracy.\n",
    "for epoch in range(num_epochs):\n",
    "    model.train()\n",
    "    for batch_idx, (data, target) in enumerate(train_loader):\n",
    "        data, target = data.to(device), target.to(device)\n",
    "        optimizer.zero_grad()\n",
    "        output = model(data)\n",
    "        loss = criterion(output, target)\n",
    "        loss.backward()\n",
    "        optimizer.step()\n",
    "\n",
    "        # 记录训练损失到 TensorBoard\n",
    "        # log training loss to TensorBoard.\n",
    "        if batch_idx % 100 == 0:\n",
    "            writer.add_scalar('Loss/train', loss.item(), epoch * len(train_loader) + batch_idx)\n",
    "            print(f'Train Epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)} ({100. * batch_idx / len(train_loader):.0f}%)]\\tLoss: {loss.item():.6f}')\n",
    "\n",
    "    # 验证模型并记录验证损失和准确率\n",
    "    # validate the model and log validation loss and accuracy.\n",
    "    model.eval()\n",
    "    val_loss = 0\n",
    "    correct = 0\n",
    "    with torch.no_grad():\n",
    "        for data, target in val_loader:\n",
    "            data, target = data.to(device), target.to(device)\n",
    "            output = model(data)\n",
    "            val_loss += criterion(output, target).item() \n",
    "            pred = output.argmax(dim=1, keepdim=True) \n",
    "            correct += pred.eq(target.view_as(pred)).sum().item()\n",
    "\n",
    "    val_loss /= len(val_loader)\n",
    "    val_accuracy = 100. * correct / len(val_loader.dataset) \n",
    "    print(f'Validation Loss: {val_loss:.4f}, Accuracy: {correct}/{len(val_loader.dataset)} ({val_accuracy:.0f}%)')\n",
    "\n",
    "    # 记录验证损失和准确率到 TensorBoard\n",
    "    # log validation loss and accuracy to TensorBoard.\n",
    "    writer.add_scalar('Loss/validation', val_loss, epoch)\n",
    "    writer.add_scalar('Accuracy/validation', val_accuracy, epoch)\n",
    "\n",
    "    # 保存验证准确率最高的模型\n",
    "    # save the model with the highest validation accuracy.\n",
    "    if val_accuracy > best_val_accuracy:\n",
    "        best_val_accuracy = val_accuracy\n",
    "        torch.save(model.state_dict(), './output/best_model.pth')\n",
    "        print(f'Model saved with accuracy: {best_val_accuracy:.2f}%')\n",
    "\n",
    "# 关闭 SummaryWriter\n",
    "# close SummaryWriter\n",
    "writer.close()\n",
    "print('Training complete. writer.close()')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2e43bea6-7a73-4dcb-a64b-2ea037c9690a",
   "metadata": {},
   "source": [
    "#### 使用tensorboard查看loss曲线\n",
    "#### Visualize loss curves using TensorBoard."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5c6f8295-1643-48cb-9ff5-ba8d46eac1c5",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "!tensorboard --logdir=/mnt/workspace/output/runs"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "242aad19-3718-4ebc-b6c3-bcf4ad25d2e7",
   "metadata": {},
   "source": [
    "#### 调用训练后的模型，体验模型效果\n",
    "#### Try out the trained model to see its performance."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "63fdf063-9f7f-4b32-b897-47a75bf20577",
   "metadata": {
    "ExecutionIndicator": {
     "show": true
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "from torchvision import datasets, transforms\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "\n",
    "class SimpleCNN(nn.Module):\n",
    "    def __init__(self):\n",
    "        super(SimpleCNN, self).__init__()\n",
    "        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)\n",
    "        self.conv2 = nn.Conv2d(10, 20, kernel_size=3)\n",
    "        self.fc1 = nn.Linear(20 * 5 * 5, 128)\n",
    "        self.fc2 = nn.Linear(128, 10)\n",
    "\n",
    "    def forward(self, x):\n",
    "        x = F.max_pool2d(F.relu(self.conv1(x)), 2)\n",
    "        x = F.max_pool2d(F.relu(self.conv2(x)), 2)\n",
    "        x = x.view(-1, 20 * 5 * 5)\n",
    "        x = F.relu(self.fc1(x))\n",
    "        x = self.fc2(x)\n",
    "        return x\n",
    "\n",
    "\n",
    "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
    "\n",
    "\n",
    "model = SimpleCNN().to(device)\n",
    "model.load_state_dict(torch.load('output/best_model.pth'))\n",
    "model.eval()\n",
    "\n",
    "\n",
    "transform = transforms.Compose([\n",
    "    transforms.Grayscale(),\n",
    "    transforms.Resize((28, 28)),\n",
    "    transforms.ToTensor(),\n",
    "    transforms.Normalize((0.5,), (0.5,))\n",
    "])\n",
    "\n",
    "\n",
    "test_dataset = datasets.MNIST(root=\"./dataSet\", train=False, transform=transform, download=False)\n",
    "test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=20, shuffle=False)\n",
    "\n",
    "\n",
    "images, labels = next(iter(test_loader))\n",
    "\n",
    "\n",
    "with torch.no_grad():\n",
    "    images_device = images.to(device)\n",
    "    outputs = model(images_device)\n",
    "    _, predicted = torch.max(outputs, 1)\n",
    "    predicted = predicted.cpu()\n",
    "\n",
    "\n",
    "fig, axes = plt.subplots(2, 10, figsize=(12, 3.5))\n",
    "axes = axes.flatten()\n",
    "\n",
    "\n",
    "for i in range(20):\n",
    "    ax = axes[i]\n",
    "    ax.imshow(images[i].cpu().squeeze(), cmap=\"gray\")\n",
    "    ax.set_title(f\"label: {labels[i].item()}\\npredict: {predicted[i].item()}\", fontsize=9)\n",
    "    ax.axis('off') \n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e1ac9612-3499-4344-8edc-2659914130ef",
   "metadata": {},
   "source": [
    "#### 将模型文件拷贝至OSS中，持久化存储\n",
    "#### Upload the model file to OSS for persistent storage."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "f40d3dc6-3414-4fcb-9f31-154a8a37dad7",
   "metadata": {
    "ExecutionIndicator": {
     "show": true
    },
    "execution": {
     "iopub.execute_input": "2026-10-20T06:17:52.337901Z",
     "iopub.status.busy": "2026-10-20T06:17:52.337630Z",
     "iopub.status.idle": "2026-10-20T06:17:53.515355Z",
     "shell.execute_reply": "2026-10-20T06:17:53.514492Z",
     "shell.execute_reply.started": "2026-10-20T06:17:52.337885Z"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "!cp  -r /mnt/workspace/output/  /mnt/data/"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1b82c8d-211b-4905-95bd-f88972d479dd",
   "metadata": {},
   "source": [
    "#### 下载模型服务web接口代码和测试代码，并拷贝至OSS中\n",
    "#### Download the web API and test code for model serving, and copy them to OSS."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9fc01814-fc3a-4c37-aceb-31f2d266be96",
   "metadata": {
    "ExecutionIndicator": {
     "show": true
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "# 下载模型预测web服务接口代码web.py。 Download the model prediction web service API code web.py.\n",
    "!curl -f -o web.py \"http://aliyun-document-review.oss-cn-beijing.aliyuncs.com/dsw_files/web.py\"\n",
    "\n",
    "# 下载请求模型服务的测试代码。Download the test code for requesting the model service.\n",
    "!curl -f -o request_web.py \"http://aliyun-document-review.oss-cn-beijing.aliyuncs.com/dsw_files/request_web.py\"\n",
    "\n",
    "# 将web.py拷贝至OSS。Copy web.py to OSS.\n",
    "!cp /mnt/workspace/web.py  /mnt/data/web.py"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0c5ff811-c99e-4fe5-a26a-061d2117586d",
   "metadata": {},
   "source": [
    "#### 在DSW中运行web.py验证代码是否可以正常启动\n",
    "#### Run web.py in DSW to verify that it starts successfully."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "06e81049-36c9-4e94-a362-724af1232c78",
   "metadata": {
    "ExecutionIndicator": {
     "show": true
    },
    "scrolled": true,
    "tags": []
   },
   "outputs": [],
   "source": [
    "# 安装bottle和requests。 Install bottle and requests.\n",
    "!pip install bottle\n",
    "!pip install requests\n",
    "\n",
    "# 启动web.py服务。 Start the web.py service.\n",
    "!python web.py"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
