From 175bf7b7da010f2d4e900135bfce85fef91dc323 Mon Sep 17 00:00:00 2001 From: kejingfan Date: Tue, 10 Oct 2023 09:42:31 +0800 Subject: [PATCH] first commit --- .gitignore | 1 + .../Pytorch基本操作实验报告-checkpoint.ipynb | 1262 +++++++++++++++++ Lab1/.vscode/settings.json | 6 + Lab1/Pytorch基本操作实验报告.ipynb | 1262 +++++++++++++++++ Lab1/code/1.1.py | 39 + Lab1/code/1.2.py | 23 + Lab1/code/1.3.py | 12 + Lab1/code/2.1.py | 143 ++ Lab1/code/2.2.py | 89 ++ Lab1/code/3.1.py | 169 +++ Lab1/code/3.2.py | 96 ++ images/school_logo.png | Bin 0 -> 116916 bytes 12 files changed, 3102 insertions(+) create mode 100644 .gitignore create mode 100644 Lab1/.ipynb_checkpoints/Pytorch基本操作实验报告-checkpoint.ipynb create mode 100644 Lab1/.vscode/settings.json create mode 100644 Lab1/Pytorch基本操作实验报告.ipynb create mode 100644 Lab1/code/1.1.py create mode 100644 Lab1/code/1.2.py create mode 100644 Lab1/code/1.3.py create mode 100644 Lab1/code/2.1.py create mode 100644 Lab1/code/2.2.py create mode 100644 Lab1/code/3.1.py create mode 100644 Lab1/code/3.2.py create mode 100644 images/school_logo.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93db21b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dataset/ diff --git a/Lab1/.ipynb_checkpoints/Pytorch基本操作实验报告-checkpoint.ipynb b/Lab1/.ipynb_checkpoints/Pytorch基本操作实验报告-checkpoint.ipynb new file mode 100644 index 0000000..c0645c6 --- /dev/null +++ b/Lab1/.ipynb_checkpoints/Pytorch基本操作实验报告-checkpoint.ipynb @@ -0,0 +1,1262 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "3b57686b-7ac8-4897-bf76-3d982b1ff8da", + "metadata": {}, + "source": [ + "

本科生《深度学习》课程
实验报告

\n", + "
\n", + "
课程名称:深度学习
\n", + "
实验题目:Pytorch基本操作
\n", + "
学号:21281280
\n", + "
姓名:柯劲帆
\n", + "
班级:物联网2101班
\n", + "
指导老师:张淳杰
\n", + "
报告日期:2023年10月9日
\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "e24aa17e-faf9-4d69-9eae-43159116b56f", + "metadata": {}, + "source": [ + "实验环境:\n", + "- OS:Ubuntu 22.04 内核版本 6.2.0-34-generic\n", + "- CPU:12th Gen Intel(R) Core(TM) i7-12700H\n", + "- GPU:NVIDIA GeForce RTX 3070 Ti Laptop\n", + "- conda: miniconda 23.9.0\n", + "- python:3.10.13\n", + "- pytorch:2.1.0" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "a4e12268-bad4-44c4-92d5-883624d93e25", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import torch\n", + "from torch.autograd import Variable\n", + "from torch.utils.data import Dataset, DataLoader\n", + "from torch import nn\n", + "from torchvision import datasets, transforms" + ] + }, + { + "cell_type": "markdown", + "id": "cc7f0ce5-d613-425b-807c-78115632cd80", + "metadata": {}, + "source": [ + "引用相关库。" + ] + }, + { + "cell_type": "markdown", + "id": "59a43d35-56ac-4ade-995d-1c6fcbcd1262", + "metadata": {}, + "source": [ + "# 一、Pytorch基本操作考察\n", + "## 题目2\n", + "**使用 𝐓𝐞𝐧𝐬𝐨𝐫 初始化一个 𝟏×𝟑 的矩阵 𝑴 和一个 𝟐×𝟏 的矩阵 𝑵,对两矩阵进行减法操作(要求实现三种不同的形式),给出结果并分析三种方式的不同(如果出现报错,分析报错的原因),同时需要指出在计算过程中发生了什么。**" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "79ea46db-cf49-436c-9b5b-c6562d0da9e2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "方法1的结果:\n", + "tensor([[-3, -2, -1],\n", + " [-4, -3, -2]])\n", + "方法2的结果:\n", + "tensor([[-3, -2, -1],\n", + " [-4, -3, -2]])\n", + "方法3的结果:\n", + "tensor([[-3, -2, -1],\n", + " [-4, -3, -2]])\n" + ] + } + ], + "source": [ + "A = torch.tensor([[1, 2, 3]])\n", + "\n", + "B = torch.tensor([[4],\n", + " [5]])\n", + "\n", + "# 方法1: 使用PyTorch的减法操作符\n", + "result1 = A - B\n", + "\n", + "# 方法2: 使用PyTorch的sub函数\n", + "result2 = torch.sub(A, B)\n", + "\n", + "# 方法3: 手动实现广播机制并作差\n", + "def mysub(a:torch.Tensor, b:torch.Tensor):\n", + " if not (\n", + " (a.size(0) == 1 and b.size(1) == 1) \n", + " or \n", + " (a.size(1) == 1 and b.size(0) == 1)\n", + " ):\n", + " raise ValueError(\"输入的张量大小无法满足广播机制的条件。\")\n", + " else:\n", + " target_shape = torch.Size([max(A.size(0), B.size(0)), max(A.size(1), B.size(1))])\n", + " A_broadcasted = A.expand(target_shape)\n", + " B_broadcasted = B.expand(target_shape)\n", + " result = torch.zeros(target_shape, dtype=torch.int64).to(device=A_broadcasted.device)\n", + " for i in range(target_shape[0]):\n", + " for j in range(target_shape[1]):\n", + " result[i, j] = A_broadcasted[i, j] - B_broadcasted[i, j]\n", + " return result\n", + "\n", + "result3 = mysub(A, B)\n", + "\n", + "print(\"方法1的结果:\")\n", + "print(result1)\n", + "print(\"方法2的结果:\")\n", + "print(result2)\n", + "print(\"方法3的结果:\")\n", + "print(result3)" + ] + }, + { + "cell_type": "markdown", + "id": "2489a3ad-f6ff-4561-bb26-e02654090b98", + "metadata": {}, + "source": [ + "## 题目2\n", + "1. **利用Tensor创建两个大小分别3*2和4*2的随机数矩阵P和Q,要求服从均值为0,标准差0.01为的正态分布;**\n", + "2. **对第二步得到的矩阵Q进行形状变换得到Q的转置Q^T;**\n", + "3. **对上述得到的矩阵P和矩阵Q^T求矩阵相乘。**" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "41e4ee02-1d05-4101-b3f0-477bac0277fb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "矩阵 P:\n", + "tensor([[ 0.0053, 0.0013],\n", + " [-0.0086, 0.0136],\n", + " [-0.0013, 0.0176]])\n", + "矩阵 Q:\n", + "tensor([[ 0.0044, 0.0014],\n", + " [ 0.0147, 0.0078],\n", + " [-0.0002, -0.0023],\n", + " [ 0.0001, -0.0011]])\n", + "矩阵 QT:\n", + "tensor([[ 0.0044, 0.0147, -0.0002, 0.0001],\n", + " [ 0.0014, 0.0078, -0.0023, -0.0011]])\n", + "矩阵相乘的结果:\n", + "tensor([[ 2.4953e-05, 8.7463e-05, -3.8665e-06, -8.9576e-07],\n", + " [-1.9514e-05, -2.0557e-05, -2.9649e-05, -1.5913e-05],\n", + " [ 1.8189e-05, 1.1834e-04, -4.0097e-05, -1.9608e-05]])\n" + ] + } + ], + "source": [ + "mean = 0\n", + "stddev = 0.01\n", + "\n", + "P = torch.normal(mean=mean, std=stddev, size=(3, 2))\n", + "Q = torch.normal(mean=mean, std=stddev, size=(4, 2))\n", + "\n", + "print(\"矩阵 P:\")\n", + "print(P)\n", + "print(\"矩阵 Q:\")\n", + "print(Q)\n", + "\n", + "# 对矩阵Q进行转置操作,得到矩阵Q的转置Q^T\n", + "QT = Q.T\n", + "print(\"矩阵 QT:\")\n", + "print(QT)\n", + "\n", + "# 计算矩阵P和矩阵Q^T的矩阵相乘\n", + "result = torch.matmul(P, QT)\n", + "print(\"矩阵相乘的结果:\")\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "id": "cea9cb6d-adde-4e08-b9f2-8c417abf4231", + "metadata": {}, + "source": [ + "## 题目2\n", + "**给定公式$ y_3=y_1+y_2=𝑥^2+𝑥^3$,且$x=1$。利用学习所得到的Tensor的相关知识,求$y_3$对$x$的梯度,即$\\frac{dy_3}{dx}$。**" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "951512cd-d915-4d04-959f-eb99d1971e2d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "梯度(dy_3/dx): 2.0\n" + ] + } + ], + "source": [ + "x = torch.tensor(1.0, requires_grad=True)\n", + "y_1 = x ** 2\n", + "with torch.no_grad():\n", + " y_2 = x**3\n", + "\n", + "y3 = y_1 + y_2\n", + "\n", + "y3.backward()\n", + "\n", + "print(\"梯度(dy_3/dx): \", x.grad.item())" + ] + }, + { + "cell_type": "markdown", + "id": "3269dbf6-889a-49eb-8094-1e588e1a6c30", + "metadata": {}, + "source": [ + "# 二、动手实现logistic回归\n", + "## 题目1\n", + "**要求动手从0实现 logistic 回归(只借助Tensor和Numpy相关的库)在人工构造的数据集上进行训练和测试,并从loss以及训练集上的准确率等多个角度对结果进行分析(可借助nn.BCELoss或nn.BCEWithLogitsLoss作为损失函数,从零实现二元交叉熵为选作)**" + ] + }, + { + "cell_type": "markdown", + "id": "bcd12aa9-f187-4d88-8c59-af6d16107edb", + "metadata": {}, + "source": [ + "给定预测概率$ \\left( \\hat{y} \\right) $和目标标签$ \\left( y \\right)$(通常是0或1),BCELoss的计算公式如下:\n", + "$$\n", + " \\text{BCELoss}(\\hat{y}, y) = -\\frac{1}{N} \\sum_{i=1}^{N} \\left(y_i \\cdot \\log(\\hat{y}_i) + (1 - y_i) \\cdot \\log(1 - \\hat{y}_i)\\right) \n", + "$$\n", + "其中,$\\left( N \\right) $是样本数量,$\\left( \\hat{y}_i \\right) $表示模型的预测概率向量中的第$ \\left( i \\right) $个元素,$\\left( y_i \\right) $表示实际的目标标签中的第$ \\left( i \\right) $个元素。在二分类问题中,$\\left( y_i \\right) $通常是0或1。这个公式表示对所有样本的二分类交叉熵损失进行了求和并取平均。\n", + "\n", + "因此BCELoss的手动实现如下。" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "e31b86ec-4114-48dd-8d73-fe4e0686419a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "输入:\n", + "tensor([0.6900])\n", + "标签:\n", + "tensor([1.])\n", + "My_BCELoss损失值: 0.37110066413879395\n", + "nn.BCELoss损失值: 0.37110066413879395\n" + ] + } + ], + "source": [ + "class My_BCELoss:\n", + " def __call__(self, prediction: torch.Tensor, target: torch.Tensor):\n", + " loss = -torch.mean(target * torch.log(prediction) + (1 - target) * torch.log(1 - prediction))\n", + " return loss\n", + "\n", + "\n", + "# 测试\n", + "prediction = torch.sigmoid(torch.tensor([0.8]))\n", + "target = torch.tensor([1.0])\n", + "print(f\"输入:\\n{prediction}\")\n", + "print(f\"标签:\\n{target}\")\n", + "\n", + "my_bce_loss = My_BCELoss()\n", + "my_loss = my_bce_loss(prediction, target)\n", + "print(\"My_BCELoss损失值:\", my_loss.item())\n", + "\n", + "nn_bce_loss = nn.BCELoss()\n", + "nn_loss = nn_bce_loss(prediction, target)\n", + "print(\"nn.BCELoss损失值:\", nn_loss.item())" + ] + }, + { + "cell_type": "markdown", + "id": "345b0300-8808-4c43-9bf9-05a7e6e1f5af", + "metadata": {}, + "source": [ + "Optimizer的实现较为简单。\n", + "\n", + "主要实现:\n", + "- 传入参数:`__init__()`\n", + "- 对传入的参数进行更新:`step()`\n", + "- 清空传入参数存储的梯度:`zero_grad()`" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "0297066c-9fc1-448d-bdcb-29a6f1519117", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "y.backward()之后,x的梯度: 2.0\n", + "optimizer_test.step()之后,x的值: 0.800000011920929\n", + "optimizer_test.zero_grad()之后,x的梯度: 0.0\n" + ] + } + ], + "source": [ + "class My_optimizer:\n", + " def __init__(self, params: list[torch.Tensor], lr: float):\n", + " self.params = params\n", + " self.lr = lr\n", + "\n", + " def step(self):\n", + " for param in self.params:\n", + " param.data = param.data - self.lr * param.grad.data\n", + "\n", + " def zero_grad(self):\n", + " for param in self.params:\n", + " if param.grad is not None:\n", + " param.grad.data.zero_()\n", + "\n", + "\n", + "# 测试\n", + "x = torch.tensor(1.0, requires_grad=True)\n", + "y = x ** 2\n", + "optimizer_test = My_optimizer([x], lr=0.1)\n", + "\n", + "y.backward()\n", + "print(\"y.backward()之后,x的梯度: \", x.grad.item())\n", + "\n", + "optimizer_test.step()\n", + "print(\"optimizer_test.step()之后,x的值: \", x.item())\n", + "\n", + "optimizer_test.zero_grad()\n", + "print(\"optimizer_test.zero_grad()之后,x的梯度: \", x.grad.item())" + ] + }, + { + "cell_type": "markdown", + "id": "6ab83528-a88b-4d66-b0c9-b1315cf75c22", + "metadata": {}, + "source": [ + "线性层主要有一个权重(weight)和一个偏置(bias)。\n", + "线性层的数学公式如下:\n", + "$$\n", + "x:=x \\times weight^T+bias\n", + "$$\n", + "因此代码实现如下:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "8e18695a-d8c5-4f77-8b5c-de40d9240fb9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "输入:\n", + "tensor([[1.],\n", + " [2.]], requires_grad=True)\n", + "权重:\n", + "tensor([[-1.0980],\n", + " [-0.5413],\n", + " [ 1.5884]], requires_grad=True)\n", + "偏置:\n", + "tensor([-1.1733], requires_grad=True)\n", + "输出:\n", + "tensor([[-2.2713, -1.7146, 0.4151],\n", + " [-3.3692, -2.2559, 2.0036]], grad_fn=)\n" + ] + } + ], + "source": [ + "class My_Linear:\n", + " def __init__(self, input_feature: int, output_feature: int):\n", + " self.weight = torch.randn((output_feature, input_feature), requires_grad=True, dtype=torch.float32)\n", + " self.bias = torch.randn(1, requires_grad=True, dtype=torch.float32)\n", + " self.params = [self.weight, self.bias]\n", + "\n", + " def __call__(self, x: torch.Tensor):\n", + " return self.forward(x)\n", + "\n", + " def forward(self, x: torch.Tensor):\n", + " x = torch.matmul(x, self.weight.T) + self.bias\n", + " return x\n", + "\n", + " def to(self, device: str):\n", + " for param in self.params:\n", + " param.data = param.data.to(device=device)\n", + " return self\n", + "\n", + " def parameters(self):\n", + " return self.params\n", + "\n", + " \n", + "# 测试\n", + "linear_test = My_Linear(1, 3)\n", + "x = torch.tensor([[1.], [2.]], requires_grad=True)\n", + "print(f\"输入:\\n{x}\")\n", + "print(f\"权重:\\n{linear_test.weight}\")\n", + "print(f\"偏置:\\n{linear_test.bias}\")\n", + "y = linear_test(x)\n", + "print(f\"输出:\\n{y}\")" + ] + }, + { + "cell_type": "markdown", + "id": "5ff813cc-c1f0-4c73-a3e8-d6796ef5d366", + "metadata": {}, + "source": [ + "手动实现logistic回归模型。\n", + "\n", + "模型很简单,主要由一个线性层和一个sigmoid层组成。\n", + "\n", + "Sigmoid函数(又称为 Logistic函数)是一种常用的激活函数,通常用于神经网络的输出层或隐藏层,其作用是将输入的实数值压缩到一个范围在0和1之间的数值。" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "e7de7e4b-a084-4793-812e-46e8550ecd8d", + "metadata": {}, + "outputs": [], + "source": [ + "class Model_2_1():\n", + " def __init__(self):\n", + " self.linear = My_Linear(1, 1)\n", + " self.params = self.linear.params\n", + "\n", + " def __call__(self, x):\n", + " return self.forward(x)\n", + "\n", + " def forward(self, x):\n", + " x = self.linear(x)\n", + " x = torch.sigmoid(x)\n", + " return x\n", + "\n", + " def to(self, device: str):\n", + " for param in self.params:\n", + " param.data = param.data.to(device=device)\n", + " return self\n", + "\n", + " def parameters(self):\n", + " return self.params" + ] + }, + { + "cell_type": "markdown", + "id": "e14acea9-e5ef-4c24-aea9-329647224ce1", + "metadata": {}, + "source": [ + "人工随机构造数据集。\n", + "\n", + "这里我遇到了比较大的问题。因为数据构建不合适,会导致后面的训练出现梯度爆炸。\n", + "\n", + "我采用随机产生数据后归一化的方法,即\n", + "$$\n", + "\\hat{x} = \\frac{x - \\text{min}_x}{\\text{max}_x - \\text{min}_x} \n", + "$$\n", + "将数据控制在合适的区间。\n", + "\n", + "我的y设置为$4-3\\times x + noise$,noise为随机噪声。\n", + "\n", + "生成完x和y后进行归一化处理,并写好DataLoader访问数据集的接口`__getitem__()`。" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "c39fbafb-62e4-4b8c-9d65-6718d25f2970", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "测试数据集大小:100\n", + "测试数据集第0对数据:\n", + "x_0 = 0.5531462811708403\n", + "y_0 = 0.42036701080526284\n" + ] + } + ], + "source": [ + "class My_Dataset(Dataset):\n", + " def __init__(self, data_size=1000000):\n", + " np.random.seed(0)\n", + " x = 2 * np.random.rand(data_size, 1)\n", + " noise = 0.2 * np.random.randn(data_size, 1)\n", + " y = 4 - 3 * x + noise\n", + " self.min_x, self.max_x = np.min(x), np.max(x)\n", + " min_y, max_y = np.min(y), np.max(y)\n", + " x = (x - self.min_x) / (self.max_x - self.min_x)\n", + " y = (y - min_y) / (max_y - min_y)\n", + " self.data = [[x[i][0], y[i][0]] for i in range(x.shape[0])]\n", + "\n", + " def __len__(self):\n", + " return len(self.data)\n", + "\n", + " def __getitem__(self, index):\n", + " x, y = self.data[index]\n", + " return x, y\n", + "\n", + "\n", + "# 测试\n", + "dataset_test = My_Dataset(data_size=100)\n", + "dataset_size = len(dataset_test)\n", + "print(f\"测试数据集大小:{dataset_size}\")\n", + "x0, y0 = dataset_test[0]\n", + "print(f\"测试数据集第0对数据:\")\n", + "print(f\"x_0 = {x0}\")\n", + "print(f\"y_0 = {y0}\")" + ] + }, + { + "cell_type": "markdown", + "id": "957a76a2-b306-47a8-912e-8fbf00cdfd42", + "metadata": {}, + "source": [ + "训练Logistic回归模型。\n", + "进行如下步骤:\n", + "1. 初始化超参数\n", + "2. 获取数据集\n", + "3. 初始化模型\n", + "4. 定义损失函数和优化器\n", + "5. 训练\n", + " 1. 从训练dataloader中获取批量数据\n", + " 2. 传入模型\n", + " 3. 使用损失函数计算与ground_truth的损失\n", + " 4. 使用优化器进行反向传播\n", + " 5. 循环以上步骤\n", + "6. 测试\n", + " 1. 设置测试数据\n", + " 2. 传入模型\n", + " 3. 得到预测值" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "5612661e-2809-4d46-96c2-33ee9f44116d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 1/10, Loss: 680.9198314547539, Acc: 0.9677169744703272\n", + "Epoch 2/10, Loss: 677.2582936882973, Acc: 0.9985965700887113\n", + "Epoch 3/10, Loss: 677.1911396384239, Acc: 0.9993738265049104\n", + "Epoch 4/10, Loss: 677.1777537465096, Acc: 0.9995470920810262\n", + "Epoch 5/10, Loss: 677.1745615005493, Acc: 0.9998228389835642\n", + "Epoch 6/10, Loss: 677.1743944883347, Acc: 0.9999690339979311\n", + "Epoch 7/10, Loss: 677.1735371947289, Acc: 0.9998205132243208\n", + "Epoch 8/10, Loss: 677.1737813353539, Acc: 0.999798559017381\n", + "Epoch 9/10, Loss: 677.1740361452103, Acc: 0.9998672931901137\n", + "Epoch 10/10, Loss: 677.1736125349998, Acc: 0.9997257713704987\n", + "Model weights: -0.0006128809181973338, bias: 0.023128816857933998\n", + "Prediction for test data: 0.505628764629364\n" + ] + } + ], + "source": [ + "learning_rate = 5e-2\n", + "num_epochs = 10\n", + "batch_size = 1024\n", + "device = \"cuda:0\" if torch.cuda.is_available() else \"cpu\"\n", + "\n", + "dataset = My_Dataset()\n", + "dataloader = DataLoader(\n", + " dataset=dataset, batch_size=batch_size, shuffle=True, num_workers=5, pin_memory=True\n", + ")\n", + "\n", + "model = Model_2_1().to(device)\n", + "criterion = My_BCELoss()\n", + "optimizer = My_optimizer(model.parameters(), lr=learning_rate)\n", + "\n", + "for epoch in range(num_epochs):\n", + " total_epoch_loss = 0\n", + " total_epoch_pred = 0\n", + " total_epoch_target = 0\n", + " for x, targets in dataloader:\n", + " optimizer.zero_grad()\n", + " \n", + " x = x.to(device).to(dtype=torch.float32)\n", + " targets = targets.to(device).to(dtype=torch.float32)\n", + " \n", + " x = x.unsqueeze(1)\n", + " y_pred = model(x)\n", + " loss = criterion(y_pred, targets)\n", + " total_epoch_loss += loss.item()\n", + " total_epoch_target += targets.sum().item()\n", + " total_epoch_pred += y_pred.sum().item()\n", + "\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " print(\n", + " f\"Epoch {epoch + 1}/{num_epochs}, Loss: {total_epoch_loss}, Acc: {1 - abs(total_epoch_pred - total_epoch_target) / total_epoch_target}\"\n", + " )\n", + "\n", + "with torch.no_grad():\n", + " test_data = (np.array([[2]]) - dataset.min_x) / (dataset.max_x - dataset.min_x)\n", + " test_data = Variable(\n", + " torch.tensor(test_data, dtype=torch.float32), requires_grad=False\n", + " ).to(device)\n", + " predicted = model(test_data).to(\"cpu\")\n", + " print(\n", + " f\"Model weights: {model.linear.weight.item()}, bias: {model.linear.bias.item()}\"\n", + " )\n", + " print(f\"Prediction for test data: {predicted.item()}\")" + ] + }, + { + "cell_type": "markdown", + "id": "9e416582-a30d-4084-acc6-6e05f80a6aff", + "metadata": {}, + "source": [ + "## 题目2\n", + "**利用 torch.nn 实现 logistic 回归在人工构造的数据集上进行训练和测试,并对结果进行分析,并从loss以及训练集上的准确率等多个角度对结果进行分析**" + ] + }, + { + "cell_type": "markdown", + "id": "0460d125-7d03-44fe-845c-c4d13792e241", + "metadata": {}, + "source": [ + "使用torch.nn实现模型。\n", + "\n", + "将之前的Model_2_1中的手动实现函数改为torch.nn内置函数即可,再加上继承nn.Module以使用torch.nn内置模型模板特性。" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "fa121afd-a1af-4193-9b54-68041e0ed068", + "metadata": {}, + "outputs": [], + "source": [ + "class Model_2_2(nn.Module):\n", + " def __init__(self):\n", + " super(Model_2_2, self).__init__()\n", + " self.linear = nn.Linear(1, 1, dtype=torch.float64)\n", + "\n", + " def forward(self, x):\n", + " x = self.linear(x)\n", + " x = torch.sigmoid(x)\n", + " return x" + ] + }, + { + "cell_type": "markdown", + "id": "176eee7e-4e3d-470e-8af2-8761bca039f8", + "metadata": {}, + "source": [ + "训练与测试过程与之前手动实现的几乎一致。仅有少量涉及数据类型(dtype)的代码需要更改以适应torch.nn的内置函数要求。" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "93b0fdb6-be8b-4663-b59e-05ed19a9ea09", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 1/10, Loss: 600.8090852049173, Acc: 0.9945839732715815\n", + "Epoch 2/10, Loss: 565.9542879898308, Acc: 0.9999073566261442\n", + "Epoch 3/10, Loss: 565.9275637627202, Acc: 0.9999969933728429\n", + "Epoch 4/10, Loss: 565.927609191542, Acc: 0.9999961959888584\n", + "Epoch 5/10, Loss: 565.928202885308, Acc: 0.9999953721249991\n", + "Epoch 6/10, Loss: 565.9323843971484, Acc: 0.9999969051674709\n", + "Epoch 7/10, Loss: 565.9298919086365, Acc: 0.9999935973983517\n", + "Epoch 8/10, Loss: 565.9299267993255, Acc: 0.9999985970973472\n", + "Epoch 9/10, Loss: 565.9306044380719, Acc: 0.9999947955797296\n", + "Epoch 10/10, Loss: 565.9329843268798, Acc: 0.9999973784035556\n", + "Model weights: -3.7066140776793373, bias: 1.8709382558479912\n", + "Prediction for test data: 0.13756338580653613\n" + ] + } + ], + "source": [ + "learning_rate = 1e-2\n", + "num_epochs = 10\n", + "batch_size = 1024\n", + "device = \"cuda:0\" if torch.cuda.is_available() else \"cpu\"\n", + "\n", + "dataset = My_Dataset()\n", + "dataloader = DataLoader(\n", + " dataset=dataset, batch_size=batch_size, shuffle=True, num_workers=5, pin_memory=True\n", + ")\n", + "\n", + "model = Model_2_2().to(device)\n", + "criterion = nn.BCELoss()\n", + "optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)\n", + "\n", + "for epoch in range(num_epochs):\n", + " total_epoch_loss = 0\n", + " total_epoch_pred = 0\n", + " total_epoch_target = 0\n", + " for x, targets in dataloader:\n", + " optimizer.zero_grad()\n", + "\n", + " x = x.to(device)\n", + " targets = targets.to(device)\n", + "\n", + " x = x.unsqueeze(1)\n", + " targets = targets.unsqueeze(1)\n", + " y_pred = model(x)\n", + " loss = criterion(y_pred, targets)\n", + " total_epoch_loss += loss.item()\n", + " total_epoch_target += targets.sum().item()\n", + " total_epoch_pred += y_pred.sum().item()\n", + "\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " print(\n", + " f\"Epoch {epoch + 1}/{num_epochs}, Loss: {total_epoch_loss}, Acc: {1 - abs(total_epoch_pred - total_epoch_target) / total_epoch_target}\"\n", + " )\n", + "\n", + "with torch.no_grad():\n", + " test_data = (np.array([[2]]) - dataset.min_x) / (dataset.max_x - dataset.min_x)\n", + " test_data = Variable(\n", + " torch.tensor(test_data, dtype=torch.float64), requires_grad=False\n", + " ).to(device)\n", + " predicted = model(test_data).to(\"cpu\")\n", + " print(\n", + " f\"Model weights: {model.linear.weight.item()}, bias: {model.linear.bias.item()}\"\n", + " )\n", + " print(f\"Prediction for test data: {predicted.item()}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "e6bff679-f8d2-46cc-bdcb-82af7dab38b3", + "metadata": {}, + "source": [ + "对比发现,使用torch.nn的内置损失函数和优化器,正确率提升更快。\n", + "\n", + "但是为什么相同分布的数据集训练出的权重和偏置,以及预测结果存在较大差别,这个问题的原因还有待我探究。" + ] + }, + { + "cell_type": "markdown", + "id": "ef41d7fa-c2bf-4024-833b-60af0a87043a", + "metadata": {}, + "source": [ + "# 三、动手实现softmax回归\n", + "\n", + "## 问题1\n", + "\n", + "**要求动手从0实现softmax回归(只借助Tensor和Numpy相关的库)在Fashion-MNIST数据集上进行训练和测试,并从loss、训练集以及测试集上的准确率等多个角度对结果进行分析(要求从零实现交叉熵损失函数)**" + ] + }, + { + "cell_type": "markdown", + "id": "3c356760-75a8-4814-ba69-73b270396a4e", + "metadata": {}, + "source": [ + "手动实现nn.one_hot()。\n", + "\n", + "one-hot向量用于消除线性标签值所映射的类别的非线性。\n", + "\n", + "one-hot向量是使用一个长度为分类数量的数组表示标签值,其中有且仅有1个值为为1,该值的下标为标签值;其余为0。\n", + "\n", + "原理很简单,步骤如下:\n", + "1. 初始化全零的张量,大小为(标签数量,分类数量);\n", + "2. 将标签值映射到全零张量的\\[下标,标签值\\]中,将该位置为1;\n", + "3. 返回修改后的张量,即是ont-hot向量。" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "e605f1b0-1d32-410f-bddf-402a85ccc9ff", + "metadata": {}, + "outputs": [], + "source": [ + "def my_one_hot(indices: torch.Tensor, num_classes: int):\n", + " one_hot_tensor = torch.zeros(len(indices), num_classes).to(indices.device)\n", + " one_hot_tensor.scatter_(1, indices.view(-1, 1), 1)\n", + " return one_hot_tensor" + ] + }, + { + "cell_type": "markdown", + "id": "902603a6-bfb9-4ce3-bd0d-b00cebb1d3cb", + "metadata": {}, + "source": [ + "手动实现CrossEntropyLoss。\n", + "\n", + "CrossEntropyLoss由一个log_softmax和一个nll_loss组成。\n", + "\n", + "softmax的数学表达式如下:\n", + "$$\n", + "\\text{softmax}(y_i) = \\frac{e^{y_i - \\text{max}(y)}}{\\sum_{j=1}^{N} e^{y_j - \\text{max}(y)}} \n", + "$$\n", + "log_softmax即为$\\log\\left(softmax\\left(y\\right)\\right)$。\n", + "\n", + "CrossEntropyLoss的数学表达式如下:\n", + "$$\n", + "\\text{CrossEntropyLoss}(y, \\hat{y}) = -\\frac{1}{N} \\sum_{i=1}^{N} \\hat{y}_i \\cdot \\log(\\text{softmax}(y_i)) \n", + "$$\n", + "\n", + "故代码如下:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "759a3bb2-b5f4-4ea5-a2d7-15f0c4cdd14b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "输入:\n", + "tensor([[-1.2914, 0.4715, -0.0432, 1.7427, -1.9236],\n", + " [ 0.5361, -0.7551, -0.6810, 1.0945, 0.6135],\n", + " [-1.3398, -0.0026, -1.6066, -0.4659, -1.6076]], requires_grad=True)\n", + "标签:\n", + "tensor([[1., 0., 0., 0., 0.],\n", + " [0., 0., 0., 0., 1.],\n", + " [0., 0., 0., 0., 1.]])\n", + "My_CrossEntropyLoss损失值: 2.4310648441314697\n", + "nn.CrossEntropyLoss损失值: 2.4310646057128906\n" + ] + } + ], + "source": [ + "class My_CrossEntropyLoss:\n", + " def __call__(self, predictions: torch.Tensor, targets: torch.Tensor):\n", + " max_values = torch.max(predictions, dim=1, keepdim=True).values\n", + " exp_values = torch.exp(predictions - max_values)\n", + " softmax_output = exp_values / torch.sum(exp_values, dim=1, keepdim=True)\n", + " log_probs = torch.log(softmax_output)\n", + " \n", + " nll_loss = -torch.sum(targets * log_probs, dim=1)\n", + " average_loss = torch.mean(nll_loss)\n", + " return average_loss\n", + "\n", + " \n", + "# 测试\n", + "input = torch.randn(3, 5, requires_grad=True)\n", + "target = torch.randn(3, 5).softmax(dim=1).argmax(1)\n", + "target = torch.nn.functional.one_hot(target, num_classes=5).to(dtype=torch.float32)\n", + "print(f\"输入:\\n{input}\")\n", + "print(f\"标签:\\n{target}\")\n", + "\n", + "my_crossentropyloss = My_CrossEntropyLoss()\n", + "my_loss = my_crossentropyloss(input, target)\n", + "print(\"My_CrossEntropyLoss损失值:\", my_loss.item())\n", + "\n", + "nn_crossentropyloss = nn.CrossEntropyLoss()\n", + "nn_loss = nn_crossentropyloss(input, target)\n", + "print(\"nn.CrossEntropyLoss损失值:\", nn_loss.item())" + ] + }, + { + "cell_type": "markdown", + "id": "dbf78501-f5be-4008-986c-d331d531491f", + "metadata": {}, + "source": [ + "手动实现Flatten。\n", + "\n", + "原理很简单,就是把多维的张量拉直成一个向量。" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "74322629-8325-4823-b80f-f28182d577c1", + "metadata": {}, + "outputs": [], + "source": [ + "class My_Flatten:\n", + " def __call__(self, x: torch.Tensor):\n", + " return self.forward(x)\n", + "\n", + " def forward(self, x: torch.Tensor):\n", + " x = x.view(x.shape[0], -1)\n", + " return x" + ] + }, + { + "cell_type": "markdown", + "id": "35aee905-ae37-4faa-a7f1-a04cd8579f78", + "metadata": {}, + "source": [ + "手动实现softmax回归模型。\n", + "\n", + "模型很简单,主要由一个Flatten层和一个线性层组成。\n", + "\n", + "Flatten层主要用于将2维的图像展开,直接作为1维的特征量输入网络。" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "bb31a75e-464c-4b94-b927-b219a765e35d", + "metadata": {}, + "outputs": [], + "source": [ + "class Model_3_1:\n", + " def __init__(self, num_classes):\n", + " self.flatten = My_Flatten()\n", + " self.linear = My_Linear(28 * 28, num_classes)\n", + " self.params = self.linear.params\n", + "\n", + " def __call__(self, x: torch.Tensor):\n", + " return self.forward(x)\n", + "\n", + " def forward(self, x: torch.Tensor):\n", + " x = self.flatten(x)\n", + " x = self.linear(x)\n", + " return x\n", + "\n", + " def to(self, device: str):\n", + " for param in self.params:\n", + " param.data = param.data.to(device=device)\n", + " return self\n", + "\n", + " def parameters(self):\n", + " return self.params" + ] + }, + { + "cell_type": "markdown", + "id": "17e686d1-9c9a-4727-8fdc-9990d348c523", + "metadata": {}, + "source": [ + "训练与测试过程与之前手动实现的几乎一致。由于数据集的变化,对应超参数也进行了调整。\n", + "\n", + "数据集也使用了现成的FashionMNIST数据集,且划分了训练集和测试集。\n", + "\n", + "FashionMNIST数据集直接调用API获取。数据集的image为28*28的单通道灰白图片,label为单个数值标签。\n", + "\n", + "训练softmax回归模型。\n", + "进行如下步骤:\n", + "1. 初始化超参数\n", + "2. 获取数据集\n", + "3. 初始化模型\n", + "4. 定义损失函数和优化器\n", + "5. 训练\n", + " 1. 从训练dataloader中获取批量数据\n", + " 2. 传入模型\n", + " 3. 使用损失函数计算与ground_truth的损失\n", + " 4. 使用优化器进行反向传播\n", + " 5. 循环以上步骤\n", + "6. 测试\n", + " 1. 从测试dataloader中获取批量数据\n", + " 2. 传入模型\n", + " 3. 将预测值与ground_truth进行比较,得出正确率\n", + " 4. 对整个训练集统计正确率,从而分析训练效果" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "d816dae1-5fbe-4c29-9597-19d66b5eb6b4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 1/10, Loss: nan, Acc: 0.09999999403953552\n", + "Epoch 2/10, Loss: nan, Acc: 0.09999999403953552\n", + "Epoch 3/10, Loss: nan, Acc: 0.09999999403953552\n", + "Epoch 4/10, Loss: nan, Acc: 0.09999999403953552\n", + "Epoch 5/10, Loss: nan, Acc: 0.09999999403953552\n", + "Epoch 6/10, Loss: nan, Acc: 0.09999999403953552\n", + "Epoch 7/10, Loss: nan, Acc: 0.09999999403953552\n", + "Epoch 8/10, Loss: nan, Acc: 0.09999999403953552\n", + "Epoch 9/10, Loss: nan, Acc: 0.09999999403953552\n", + "Epoch 10/10, Loss: nan, Acc: 0.09999999403953552\n" + ] + } + ], + "source": [ + "learning_rate = 5e-3\n", + "num_epochs = 10\n", + "batch_size = 4096\n", + "num_classes = 10\n", + "device = \"cuda:0\" if torch.cuda.is_available() else \"cpu\"\n", + "\n", + "transform = transforms.Compose(\n", + " [\n", + " transforms.ToTensor(),\n", + " transforms.Normalize((0.5,), (0.5,)),\n", + " ]\n", + ")\n", + "train_dataset = datasets.FashionMNIST(root=\"./dataset\", train=True, transform=transform, download=True)\n", + "test_dataset = datasets.FashionMNIST(root=\"./dataset\", train=False, transform=transform, download=True)\n", + "train_loader = DataLoader(\n", + " dataset=train_dataset, batch_size=batch_size,\n", + " shuffle=True, num_workers=4, pin_memory=True,\n", + ")\n", + "test_loader = DataLoader(\n", + " dataset=test_dataset, batch_size=batch_size,\n", + " shuffle=True, num_workers=4, pin_memory=True,\n", + ")\n", + "\n", + "model = Model_3_1(num_classes).to(device)\n", + "criterion = My_CrossEntropyLoss()\n", + "optimizer = My_optimizer(model.parameters(), lr=learning_rate)\n", + "\n", + "for epoch in range(num_epochs):\n", + " total_epoch_loss = 0\n", + " for images, targets in train_loader:\n", + " optimizer.zero_grad()\n", + "\n", + " images = images.to(device)\n", + " targets = targets.to(device).to(dtype=torch.long)\n", + "\n", + " one_hot_targets = (\n", + " my_one_hot(targets, num_classes=num_classes)\n", + " .to(device)\n", + " .to(dtype=torch.long)\n", + " )\n", + "\n", + " outputs = model(images)\n", + " loss = criterion(outputs, one_hot_targets)\n", + " total_epoch_loss += loss\n", + "\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " total_acc = 0\n", + " with torch.no_grad():\n", + " for image, targets in test_loader:\n", + " image = image.to(device)\n", + " targets = targets.to(device)\n", + " outputs = model(image)\n", + " total_acc += (outputs.argmax(1) == targets).sum()\n", + " print(\n", + " f\"Epoch {epoch + 1}/{num_epochs}, Loss: {total_epoch_loss}, Acc: {total_acc / len(test_dataset)}\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "a49d0165-aeb7-48c0-9b67-956bb08cb356", + "metadata": {}, + "source": [ + "这里发现梯度爆炸。暂时无法解决。" + ] + }, + { + "cell_type": "markdown", + "id": "3ef5240f-8a11-4678-bfce-f1cbc7e71b77", + "metadata": {}, + "source": [ + "## 问题2\n", + "\n", + "**利用torch.nn实现softmax回归在Fashion-MNIST数据集上进行训练和测试,并从loss,训练集以及测试集上的准确率等多个角度对结果进行分析**" + ] + }, + { + "cell_type": "markdown", + "id": "5c4a88c6-637e-4af5-bed5-f644685dcabc", + "metadata": {}, + "source": [ + "使用torch.nn实现模型。\n", + "\n", + "将之前的Model_3_1中的手动实现函数改为torch.nn内置函数即可,再加上继承nn.Module以使用torch.nn内置模型模板特性。" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "0163b9f7-1019-429c-8c29-06436d0a4c98", + "metadata": {}, + "outputs": [], + "source": [ + "class Model_3_2(nn.Module):\n", + " def __init__(self, num_classes):\n", + " super(Model_3_2, self).__init__()\n", + " self.flatten = nn.Flatten()\n", + " self.linear = nn.Linear(28 * 28, num_classes)\n", + "\n", + " def forward(self, x: torch.Tensor):\n", + " x = self.flatten(x)\n", + " x = self.linear(x)\n", + " return x" + ] + }, + { + "cell_type": "markdown", + "id": "6e765ad7-c1c6-4166-bd7f-361666bd4016", + "metadata": {}, + "source": [ + "训练与测试过程与之前手动实现的几乎一致。" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "a58a23e1-368c-430a-ad62-0e256dff564d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 1/10, Loss: 15.148970603942871, Acc: 0.7520999908447266\n", + "Epoch 2/10, Loss: 9.012335777282715, Acc: 0.7996999621391296\n", + "Epoch 3/10, Loss: 7.9114227294921875, Acc: 0.8095999956130981\n", + "Epoch 4/10, Loss: 7.427404403686523, Acc: 0.8215999603271484\n", + "Epoch 5/10, Loss: 7.084254264831543, Acc: 0.8277999758720398\n", + "Epoch 6/10, Loss: 6.885956287384033, Acc: 0.8274999856948853\n", + "Epoch 7/10, Loss: 6.808426380157471, Acc: 0.8327999711036682\n", + "Epoch 8/10, Loss: 6.647855758666992, Acc: 0.8323000073432922\n", + "Epoch 9/10, Loss: 6.560361862182617, Acc: 0.8317999839782715\n", + "Epoch 10/10, Loss: 6.5211310386657715, Acc: 0.8349999785423279\n" + ] + } + ], + "source": [ + "learning_rate = 5e-3\n", + "num_epochs = 10\n", + "batch_size = 4096\n", + "num_classes = 10\n", + "device = \"cuda:0\" if torch.cuda.is_available() else \"cpu\"\n", + "\n", + "transform = transforms.Compose(\n", + " [\n", + " transforms.ToTensor(),\n", + " transforms.Normalize((0.5,), (0.5,)),\n", + " ]\n", + ")\n", + "train_dataset = datasets.FashionMNIST(\n", + " root=\"./dataset\", train=True, transform=transform, download=True\n", + ")\n", + "test_dataset = datasets.FashionMNIST(\n", + " root=\"./dataset\", train=False, transform=transform, download=True\n", + ")\n", + "train_loader = DataLoader(\n", + " dataset=train_dataset,\n", + " batch_size=batch_size,\n", + " shuffle=True,\n", + " num_workers=4,\n", + " pin_memory=True,\n", + ")\n", + "test_loader = DataLoader(\n", + " dataset=test_dataset,\n", + " batch_size=batch_size,\n", + " shuffle=True,\n", + " num_workers=4,\n", + " pin_memory=True,\n", + ")\n", + "\n", + "model = Model_3_2(num_classes).to(device)\n", + "criterion = nn.CrossEntropyLoss()\n", + "optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)\n", + "\n", + "for epoch in range(num_epochs):\n", + " total_epoch_loss = 0\n", + " model.train()\n", + " for images, targets in train_loader:\n", + " optimizer.zero_grad()\n", + "\n", + " images = images.to(device)\n", + " targets = targets.to(device)\n", + "\n", + " one_hot_targets = (\n", + " torch.nn.functional.one_hot(targets, num_classes=num_classes)\n", + " .to(device)\n", + " .to(dtype=torch.float32)\n", + " )\n", + "\n", + " outputs = model(images)\n", + " loss = criterion(outputs, one_hot_targets)\n", + " total_epoch_loss += loss\n", + "\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " model.eval()\n", + " total_acc = 0\n", + " with torch.no_grad():\n", + " for image, targets in test_loader:\n", + " image = image.to(device)\n", + " targets = targets.to(device)\n", + " outputs = model(image)\n", + " total_acc += (outputs.argmax(1) == targets).sum()\n", + " print(\n", + " f\"Epoch {epoch + 1}/{num_epochs}, Loss: {total_epoch_loss}, Acc: {total_acc / len(test_dataset)}\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "59555b67-1650-4e1a-a98e-7906878bf3d0", + "metadata": {}, + "source": [ + "与手动实现的softmax回归相比较,nn.CrossEntropyLoss比手动实现的My_CrossEntropyLoss更加稳定,没有出现梯度爆炸的情况。" + ] + }, + { + "cell_type": "markdown", + "id": "f40431f2-e77b-4ead-81a3-ff6451a8e452", + "metadata": {}, + "source": [ + "**实验心得体会**\n", + "\n", + "通过完成本次Pytorch基本操作实验,让我对Pytorch框架有了更加深入的理解。我接触深度学习主要是在大语言模型领域,比较熟悉微调大模型,但是涉及到底层的深度学习知识,我还有很多短板和不足。这次实验对我这方面的锻炼让我收获良多。\n", + "\n", + "首先是数据集的设置。如果数据没有进行归一化,很容易出现梯度爆炸。这是在我以前直接使用图片数据集的经历中没有遇到过的问题。\n", + "\n", + "在实现logistic回归模型时,通过手动实现各个组件如优化器、线性层等,让我对这些模块的工作原理有了更清晰的认识。尤其是在实现广播机制时,需要充分理解张量操作的维度变换规律。而使用Pytorch内置模块进行实现时,通过继承nn.Module可以自动获得許多功能,使代码更加简洁。\n", + "\n", + "在实现softmax回归时,则遇到了更大的困难。手动实现的模型很容易出现梯度爆炸的问题,而使用Pytorch内置的损失函数和优化器则可以稳定训练。这让我意识到了选择合适的优化方法的重要性。另外,Pytorch强大的自动微分机制也是构建深度神经网络的重要基础。\n", + "\n", + "通过这个实验,让我对Pytorch框架有了更加直观的感受,也让我看到了仅靠基础模块搭建复杂模型的难点所在。这些经验对我后续使用Pytorch构建数据集模型会很有帮助。" + ] + } + ], + "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.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Lab1/.vscode/settings.json b/Lab1/.vscode/settings.json new file mode 100644 index 0000000..d99f2f3 --- /dev/null +++ b/Lab1/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" + }, + "python.formatting.provider": "none" +} \ No newline at end of file diff --git a/Lab1/Pytorch基本操作实验报告.ipynb b/Lab1/Pytorch基本操作实验报告.ipynb new file mode 100644 index 0000000..c0645c6 --- /dev/null +++ b/Lab1/Pytorch基本操作实验报告.ipynb @@ -0,0 +1,1262 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "3b57686b-7ac8-4897-bf76-3d982b1ff8da", + "metadata": {}, + "source": [ + "

本科生《深度学习》课程
实验报告

\n", + "
\n", + "
课程名称:深度学习
\n", + "
实验题目:Pytorch基本操作
\n", + "
学号:21281280
\n", + "
姓名:柯劲帆
\n", + "
班级:物联网2101班
\n", + "
指导老师:张淳杰
\n", + "
报告日期:2023年10月9日
\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "e24aa17e-faf9-4d69-9eae-43159116b56f", + "metadata": {}, + "source": [ + "实验环境:\n", + "- OS:Ubuntu 22.04 内核版本 6.2.0-34-generic\n", + "- CPU:12th Gen Intel(R) Core(TM) i7-12700H\n", + "- GPU:NVIDIA GeForce RTX 3070 Ti Laptop\n", + "- conda: miniconda 23.9.0\n", + "- python:3.10.13\n", + "- pytorch:2.1.0" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "a4e12268-bad4-44c4-92d5-883624d93e25", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import torch\n", + "from torch.autograd import Variable\n", + "from torch.utils.data import Dataset, DataLoader\n", + "from torch import nn\n", + "from torchvision import datasets, transforms" + ] + }, + { + "cell_type": "markdown", + "id": "cc7f0ce5-d613-425b-807c-78115632cd80", + "metadata": {}, + "source": [ + "引用相关库。" + ] + }, + { + "cell_type": "markdown", + "id": "59a43d35-56ac-4ade-995d-1c6fcbcd1262", + "metadata": {}, + "source": [ + "# 一、Pytorch基本操作考察\n", + "## 题目2\n", + "**使用 𝐓𝐞𝐧𝐬𝐨𝐫 初始化一个 𝟏×𝟑 的矩阵 𝑴 和一个 𝟐×𝟏 的矩阵 𝑵,对两矩阵进行减法操作(要求实现三种不同的形式),给出结果并分析三种方式的不同(如果出现报错,分析报错的原因),同时需要指出在计算过程中发生了什么。**" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "79ea46db-cf49-436c-9b5b-c6562d0da9e2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "方法1的结果:\n", + "tensor([[-3, -2, -1],\n", + " [-4, -3, -2]])\n", + "方法2的结果:\n", + "tensor([[-3, -2, -1],\n", + " [-4, -3, -2]])\n", + "方法3的结果:\n", + "tensor([[-3, -2, -1],\n", + " [-4, -3, -2]])\n" + ] + } + ], + "source": [ + "A = torch.tensor([[1, 2, 3]])\n", + "\n", + "B = torch.tensor([[4],\n", + " [5]])\n", + "\n", + "# 方法1: 使用PyTorch的减法操作符\n", + "result1 = A - B\n", + "\n", + "# 方法2: 使用PyTorch的sub函数\n", + "result2 = torch.sub(A, B)\n", + "\n", + "# 方法3: 手动实现广播机制并作差\n", + "def mysub(a:torch.Tensor, b:torch.Tensor):\n", + " if not (\n", + " (a.size(0) == 1 and b.size(1) == 1) \n", + " or \n", + " (a.size(1) == 1 and b.size(0) == 1)\n", + " ):\n", + " raise ValueError(\"输入的张量大小无法满足广播机制的条件。\")\n", + " else:\n", + " target_shape = torch.Size([max(A.size(0), B.size(0)), max(A.size(1), B.size(1))])\n", + " A_broadcasted = A.expand(target_shape)\n", + " B_broadcasted = B.expand(target_shape)\n", + " result = torch.zeros(target_shape, dtype=torch.int64).to(device=A_broadcasted.device)\n", + " for i in range(target_shape[0]):\n", + " for j in range(target_shape[1]):\n", + " result[i, j] = A_broadcasted[i, j] - B_broadcasted[i, j]\n", + " return result\n", + "\n", + "result3 = mysub(A, B)\n", + "\n", + "print(\"方法1的结果:\")\n", + "print(result1)\n", + "print(\"方法2的结果:\")\n", + "print(result2)\n", + "print(\"方法3的结果:\")\n", + "print(result3)" + ] + }, + { + "cell_type": "markdown", + "id": "2489a3ad-f6ff-4561-bb26-e02654090b98", + "metadata": {}, + "source": [ + "## 题目2\n", + "1. **利用Tensor创建两个大小分别3*2和4*2的随机数矩阵P和Q,要求服从均值为0,标准差0.01为的正态分布;**\n", + "2. **对第二步得到的矩阵Q进行形状变换得到Q的转置Q^T;**\n", + "3. **对上述得到的矩阵P和矩阵Q^T求矩阵相乘。**" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "41e4ee02-1d05-4101-b3f0-477bac0277fb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "矩阵 P:\n", + "tensor([[ 0.0053, 0.0013],\n", + " [-0.0086, 0.0136],\n", + " [-0.0013, 0.0176]])\n", + "矩阵 Q:\n", + "tensor([[ 0.0044, 0.0014],\n", + " [ 0.0147, 0.0078],\n", + " [-0.0002, -0.0023],\n", + " [ 0.0001, -0.0011]])\n", + "矩阵 QT:\n", + "tensor([[ 0.0044, 0.0147, -0.0002, 0.0001],\n", + " [ 0.0014, 0.0078, -0.0023, -0.0011]])\n", + "矩阵相乘的结果:\n", + "tensor([[ 2.4953e-05, 8.7463e-05, -3.8665e-06, -8.9576e-07],\n", + " [-1.9514e-05, -2.0557e-05, -2.9649e-05, -1.5913e-05],\n", + " [ 1.8189e-05, 1.1834e-04, -4.0097e-05, -1.9608e-05]])\n" + ] + } + ], + "source": [ + "mean = 0\n", + "stddev = 0.01\n", + "\n", + "P = torch.normal(mean=mean, std=stddev, size=(3, 2))\n", + "Q = torch.normal(mean=mean, std=stddev, size=(4, 2))\n", + "\n", + "print(\"矩阵 P:\")\n", + "print(P)\n", + "print(\"矩阵 Q:\")\n", + "print(Q)\n", + "\n", + "# 对矩阵Q进行转置操作,得到矩阵Q的转置Q^T\n", + "QT = Q.T\n", + "print(\"矩阵 QT:\")\n", + "print(QT)\n", + "\n", + "# 计算矩阵P和矩阵Q^T的矩阵相乘\n", + "result = torch.matmul(P, QT)\n", + "print(\"矩阵相乘的结果:\")\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "id": "cea9cb6d-adde-4e08-b9f2-8c417abf4231", + "metadata": {}, + "source": [ + "## 题目2\n", + "**给定公式$ y_3=y_1+y_2=𝑥^2+𝑥^3$,且$x=1$。利用学习所得到的Tensor的相关知识,求$y_3$对$x$的梯度,即$\\frac{dy_3}{dx}$。**" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "951512cd-d915-4d04-959f-eb99d1971e2d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "梯度(dy_3/dx): 2.0\n" + ] + } + ], + "source": [ + "x = torch.tensor(1.0, requires_grad=True)\n", + "y_1 = x ** 2\n", + "with torch.no_grad():\n", + " y_2 = x**3\n", + "\n", + "y3 = y_1 + y_2\n", + "\n", + "y3.backward()\n", + "\n", + "print(\"梯度(dy_3/dx): \", x.grad.item())" + ] + }, + { + "cell_type": "markdown", + "id": "3269dbf6-889a-49eb-8094-1e588e1a6c30", + "metadata": {}, + "source": [ + "# 二、动手实现logistic回归\n", + "## 题目1\n", + "**要求动手从0实现 logistic 回归(只借助Tensor和Numpy相关的库)在人工构造的数据集上进行训练和测试,并从loss以及训练集上的准确率等多个角度对结果进行分析(可借助nn.BCELoss或nn.BCEWithLogitsLoss作为损失函数,从零实现二元交叉熵为选作)**" + ] + }, + { + "cell_type": "markdown", + "id": "bcd12aa9-f187-4d88-8c59-af6d16107edb", + "metadata": {}, + "source": [ + "给定预测概率$ \\left( \\hat{y} \\right) $和目标标签$ \\left( y \\right)$(通常是0或1),BCELoss的计算公式如下:\n", + "$$\n", + " \\text{BCELoss}(\\hat{y}, y) = -\\frac{1}{N} \\sum_{i=1}^{N} \\left(y_i \\cdot \\log(\\hat{y}_i) + (1 - y_i) \\cdot \\log(1 - \\hat{y}_i)\\right) \n", + "$$\n", + "其中,$\\left( N \\right) $是样本数量,$\\left( \\hat{y}_i \\right) $表示模型的预测概率向量中的第$ \\left( i \\right) $个元素,$\\left( y_i \\right) $表示实际的目标标签中的第$ \\left( i \\right) $个元素。在二分类问题中,$\\left( y_i \\right) $通常是0或1。这个公式表示对所有样本的二分类交叉熵损失进行了求和并取平均。\n", + "\n", + "因此BCELoss的手动实现如下。" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "e31b86ec-4114-48dd-8d73-fe4e0686419a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "输入:\n", + "tensor([0.6900])\n", + "标签:\n", + "tensor([1.])\n", + "My_BCELoss损失值: 0.37110066413879395\n", + "nn.BCELoss损失值: 0.37110066413879395\n" + ] + } + ], + "source": [ + "class My_BCELoss:\n", + " def __call__(self, prediction: torch.Tensor, target: torch.Tensor):\n", + " loss = -torch.mean(target * torch.log(prediction) + (1 - target) * torch.log(1 - prediction))\n", + " return loss\n", + "\n", + "\n", + "# 测试\n", + "prediction = torch.sigmoid(torch.tensor([0.8]))\n", + "target = torch.tensor([1.0])\n", + "print(f\"输入:\\n{prediction}\")\n", + "print(f\"标签:\\n{target}\")\n", + "\n", + "my_bce_loss = My_BCELoss()\n", + "my_loss = my_bce_loss(prediction, target)\n", + "print(\"My_BCELoss损失值:\", my_loss.item())\n", + "\n", + "nn_bce_loss = nn.BCELoss()\n", + "nn_loss = nn_bce_loss(prediction, target)\n", + "print(\"nn.BCELoss损失值:\", nn_loss.item())" + ] + }, + { + "cell_type": "markdown", + "id": "345b0300-8808-4c43-9bf9-05a7e6e1f5af", + "metadata": {}, + "source": [ + "Optimizer的实现较为简单。\n", + "\n", + "主要实现:\n", + "- 传入参数:`__init__()`\n", + "- 对传入的参数进行更新:`step()`\n", + "- 清空传入参数存储的梯度:`zero_grad()`" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "0297066c-9fc1-448d-bdcb-29a6f1519117", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "y.backward()之后,x的梯度: 2.0\n", + "optimizer_test.step()之后,x的值: 0.800000011920929\n", + "optimizer_test.zero_grad()之后,x的梯度: 0.0\n" + ] + } + ], + "source": [ + "class My_optimizer:\n", + " def __init__(self, params: list[torch.Tensor], lr: float):\n", + " self.params = params\n", + " self.lr = lr\n", + "\n", + " def step(self):\n", + " for param in self.params:\n", + " param.data = param.data - self.lr * param.grad.data\n", + "\n", + " def zero_grad(self):\n", + " for param in self.params:\n", + " if param.grad is not None:\n", + " param.grad.data.zero_()\n", + "\n", + "\n", + "# 测试\n", + "x = torch.tensor(1.0, requires_grad=True)\n", + "y = x ** 2\n", + "optimizer_test = My_optimizer([x], lr=0.1)\n", + "\n", + "y.backward()\n", + "print(\"y.backward()之后,x的梯度: \", x.grad.item())\n", + "\n", + "optimizer_test.step()\n", + "print(\"optimizer_test.step()之后,x的值: \", x.item())\n", + "\n", + "optimizer_test.zero_grad()\n", + "print(\"optimizer_test.zero_grad()之后,x的梯度: \", x.grad.item())" + ] + }, + { + "cell_type": "markdown", + "id": "6ab83528-a88b-4d66-b0c9-b1315cf75c22", + "metadata": {}, + "source": [ + "线性层主要有一个权重(weight)和一个偏置(bias)。\n", + "线性层的数学公式如下:\n", + "$$\n", + "x:=x \\times weight^T+bias\n", + "$$\n", + "因此代码实现如下:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "8e18695a-d8c5-4f77-8b5c-de40d9240fb9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "输入:\n", + "tensor([[1.],\n", + " [2.]], requires_grad=True)\n", + "权重:\n", + "tensor([[-1.0980],\n", + " [-0.5413],\n", + " [ 1.5884]], requires_grad=True)\n", + "偏置:\n", + "tensor([-1.1733], requires_grad=True)\n", + "输出:\n", + "tensor([[-2.2713, -1.7146, 0.4151],\n", + " [-3.3692, -2.2559, 2.0036]], grad_fn=)\n" + ] + } + ], + "source": [ + "class My_Linear:\n", + " def __init__(self, input_feature: int, output_feature: int):\n", + " self.weight = torch.randn((output_feature, input_feature), requires_grad=True, dtype=torch.float32)\n", + " self.bias = torch.randn(1, requires_grad=True, dtype=torch.float32)\n", + " self.params = [self.weight, self.bias]\n", + "\n", + " def __call__(self, x: torch.Tensor):\n", + " return self.forward(x)\n", + "\n", + " def forward(self, x: torch.Tensor):\n", + " x = torch.matmul(x, self.weight.T) + self.bias\n", + " return x\n", + "\n", + " def to(self, device: str):\n", + " for param in self.params:\n", + " param.data = param.data.to(device=device)\n", + " return self\n", + "\n", + " def parameters(self):\n", + " return self.params\n", + "\n", + " \n", + "# 测试\n", + "linear_test = My_Linear(1, 3)\n", + "x = torch.tensor([[1.], [2.]], requires_grad=True)\n", + "print(f\"输入:\\n{x}\")\n", + "print(f\"权重:\\n{linear_test.weight}\")\n", + "print(f\"偏置:\\n{linear_test.bias}\")\n", + "y = linear_test(x)\n", + "print(f\"输出:\\n{y}\")" + ] + }, + { + "cell_type": "markdown", + "id": "5ff813cc-c1f0-4c73-a3e8-d6796ef5d366", + "metadata": {}, + "source": [ + "手动实现logistic回归模型。\n", + "\n", + "模型很简单,主要由一个线性层和一个sigmoid层组成。\n", + "\n", + "Sigmoid函数(又称为 Logistic函数)是一种常用的激活函数,通常用于神经网络的输出层或隐藏层,其作用是将输入的实数值压缩到一个范围在0和1之间的数值。" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "e7de7e4b-a084-4793-812e-46e8550ecd8d", + "metadata": {}, + "outputs": [], + "source": [ + "class Model_2_1():\n", + " def __init__(self):\n", + " self.linear = My_Linear(1, 1)\n", + " self.params = self.linear.params\n", + "\n", + " def __call__(self, x):\n", + " return self.forward(x)\n", + "\n", + " def forward(self, x):\n", + " x = self.linear(x)\n", + " x = torch.sigmoid(x)\n", + " return x\n", + "\n", + " def to(self, device: str):\n", + " for param in self.params:\n", + " param.data = param.data.to(device=device)\n", + " return self\n", + "\n", + " def parameters(self):\n", + " return self.params" + ] + }, + { + "cell_type": "markdown", + "id": "e14acea9-e5ef-4c24-aea9-329647224ce1", + "metadata": {}, + "source": [ + "人工随机构造数据集。\n", + "\n", + "这里我遇到了比较大的问题。因为数据构建不合适,会导致后面的训练出现梯度爆炸。\n", + "\n", + "我采用随机产生数据后归一化的方法,即\n", + "$$\n", + "\\hat{x} = \\frac{x - \\text{min}_x}{\\text{max}_x - \\text{min}_x} \n", + "$$\n", + "将数据控制在合适的区间。\n", + "\n", + "我的y设置为$4-3\\times x + noise$,noise为随机噪声。\n", + "\n", + "生成完x和y后进行归一化处理,并写好DataLoader访问数据集的接口`__getitem__()`。" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "c39fbafb-62e4-4b8c-9d65-6718d25f2970", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "测试数据集大小:100\n", + "测试数据集第0对数据:\n", + "x_0 = 0.5531462811708403\n", + "y_0 = 0.42036701080526284\n" + ] + } + ], + "source": [ + "class My_Dataset(Dataset):\n", + " def __init__(self, data_size=1000000):\n", + " np.random.seed(0)\n", + " x = 2 * np.random.rand(data_size, 1)\n", + " noise = 0.2 * np.random.randn(data_size, 1)\n", + " y = 4 - 3 * x + noise\n", + " self.min_x, self.max_x = np.min(x), np.max(x)\n", + " min_y, max_y = np.min(y), np.max(y)\n", + " x = (x - self.min_x) / (self.max_x - self.min_x)\n", + " y = (y - min_y) / (max_y - min_y)\n", + " self.data = [[x[i][0], y[i][0]] for i in range(x.shape[0])]\n", + "\n", + " def __len__(self):\n", + " return len(self.data)\n", + "\n", + " def __getitem__(self, index):\n", + " x, y = self.data[index]\n", + " return x, y\n", + "\n", + "\n", + "# 测试\n", + "dataset_test = My_Dataset(data_size=100)\n", + "dataset_size = len(dataset_test)\n", + "print(f\"测试数据集大小:{dataset_size}\")\n", + "x0, y0 = dataset_test[0]\n", + "print(f\"测试数据集第0对数据:\")\n", + "print(f\"x_0 = {x0}\")\n", + "print(f\"y_0 = {y0}\")" + ] + }, + { + "cell_type": "markdown", + "id": "957a76a2-b306-47a8-912e-8fbf00cdfd42", + "metadata": {}, + "source": [ + "训练Logistic回归模型。\n", + "进行如下步骤:\n", + "1. 初始化超参数\n", + "2. 获取数据集\n", + "3. 初始化模型\n", + "4. 定义损失函数和优化器\n", + "5. 训练\n", + " 1. 从训练dataloader中获取批量数据\n", + " 2. 传入模型\n", + " 3. 使用损失函数计算与ground_truth的损失\n", + " 4. 使用优化器进行反向传播\n", + " 5. 循环以上步骤\n", + "6. 测试\n", + " 1. 设置测试数据\n", + " 2. 传入模型\n", + " 3. 得到预测值" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "5612661e-2809-4d46-96c2-33ee9f44116d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 1/10, Loss: 680.9198314547539, Acc: 0.9677169744703272\n", + "Epoch 2/10, Loss: 677.2582936882973, Acc: 0.9985965700887113\n", + "Epoch 3/10, Loss: 677.1911396384239, Acc: 0.9993738265049104\n", + "Epoch 4/10, Loss: 677.1777537465096, Acc: 0.9995470920810262\n", + "Epoch 5/10, Loss: 677.1745615005493, Acc: 0.9998228389835642\n", + "Epoch 6/10, Loss: 677.1743944883347, Acc: 0.9999690339979311\n", + "Epoch 7/10, Loss: 677.1735371947289, Acc: 0.9998205132243208\n", + "Epoch 8/10, Loss: 677.1737813353539, Acc: 0.999798559017381\n", + "Epoch 9/10, Loss: 677.1740361452103, Acc: 0.9998672931901137\n", + "Epoch 10/10, Loss: 677.1736125349998, Acc: 0.9997257713704987\n", + "Model weights: -0.0006128809181973338, bias: 0.023128816857933998\n", + "Prediction for test data: 0.505628764629364\n" + ] + } + ], + "source": [ + "learning_rate = 5e-2\n", + "num_epochs = 10\n", + "batch_size = 1024\n", + "device = \"cuda:0\" if torch.cuda.is_available() else \"cpu\"\n", + "\n", + "dataset = My_Dataset()\n", + "dataloader = DataLoader(\n", + " dataset=dataset, batch_size=batch_size, shuffle=True, num_workers=5, pin_memory=True\n", + ")\n", + "\n", + "model = Model_2_1().to(device)\n", + "criterion = My_BCELoss()\n", + "optimizer = My_optimizer(model.parameters(), lr=learning_rate)\n", + "\n", + "for epoch in range(num_epochs):\n", + " total_epoch_loss = 0\n", + " total_epoch_pred = 0\n", + " total_epoch_target = 0\n", + " for x, targets in dataloader:\n", + " optimizer.zero_grad()\n", + " \n", + " x = x.to(device).to(dtype=torch.float32)\n", + " targets = targets.to(device).to(dtype=torch.float32)\n", + " \n", + " x = x.unsqueeze(1)\n", + " y_pred = model(x)\n", + " loss = criterion(y_pred, targets)\n", + " total_epoch_loss += loss.item()\n", + " total_epoch_target += targets.sum().item()\n", + " total_epoch_pred += y_pred.sum().item()\n", + "\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " print(\n", + " f\"Epoch {epoch + 1}/{num_epochs}, Loss: {total_epoch_loss}, Acc: {1 - abs(total_epoch_pred - total_epoch_target) / total_epoch_target}\"\n", + " )\n", + "\n", + "with torch.no_grad():\n", + " test_data = (np.array([[2]]) - dataset.min_x) / (dataset.max_x - dataset.min_x)\n", + " test_data = Variable(\n", + " torch.tensor(test_data, dtype=torch.float32), requires_grad=False\n", + " ).to(device)\n", + " predicted = model(test_data).to(\"cpu\")\n", + " print(\n", + " f\"Model weights: {model.linear.weight.item()}, bias: {model.linear.bias.item()}\"\n", + " )\n", + " print(f\"Prediction for test data: {predicted.item()}\")" + ] + }, + { + "cell_type": "markdown", + "id": "9e416582-a30d-4084-acc6-6e05f80a6aff", + "metadata": {}, + "source": [ + "## 题目2\n", + "**利用 torch.nn 实现 logistic 回归在人工构造的数据集上进行训练和测试,并对结果进行分析,并从loss以及训练集上的准确率等多个角度对结果进行分析**" + ] + }, + { + "cell_type": "markdown", + "id": "0460d125-7d03-44fe-845c-c4d13792e241", + "metadata": {}, + "source": [ + "使用torch.nn实现模型。\n", + "\n", + "将之前的Model_2_1中的手动实现函数改为torch.nn内置函数即可,再加上继承nn.Module以使用torch.nn内置模型模板特性。" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "fa121afd-a1af-4193-9b54-68041e0ed068", + "metadata": {}, + "outputs": [], + "source": [ + "class Model_2_2(nn.Module):\n", + " def __init__(self):\n", + " super(Model_2_2, self).__init__()\n", + " self.linear = nn.Linear(1, 1, dtype=torch.float64)\n", + "\n", + " def forward(self, x):\n", + " x = self.linear(x)\n", + " x = torch.sigmoid(x)\n", + " return x" + ] + }, + { + "cell_type": "markdown", + "id": "176eee7e-4e3d-470e-8af2-8761bca039f8", + "metadata": {}, + "source": [ + "训练与测试过程与之前手动实现的几乎一致。仅有少量涉及数据类型(dtype)的代码需要更改以适应torch.nn的内置函数要求。" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "93b0fdb6-be8b-4663-b59e-05ed19a9ea09", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 1/10, Loss: 600.8090852049173, Acc: 0.9945839732715815\n", + "Epoch 2/10, Loss: 565.9542879898308, Acc: 0.9999073566261442\n", + "Epoch 3/10, Loss: 565.9275637627202, Acc: 0.9999969933728429\n", + "Epoch 4/10, Loss: 565.927609191542, Acc: 0.9999961959888584\n", + "Epoch 5/10, Loss: 565.928202885308, Acc: 0.9999953721249991\n", + "Epoch 6/10, Loss: 565.9323843971484, Acc: 0.9999969051674709\n", + "Epoch 7/10, Loss: 565.9298919086365, Acc: 0.9999935973983517\n", + "Epoch 8/10, Loss: 565.9299267993255, Acc: 0.9999985970973472\n", + "Epoch 9/10, Loss: 565.9306044380719, Acc: 0.9999947955797296\n", + "Epoch 10/10, Loss: 565.9329843268798, Acc: 0.9999973784035556\n", + "Model weights: -3.7066140776793373, bias: 1.8709382558479912\n", + "Prediction for test data: 0.13756338580653613\n" + ] + } + ], + "source": [ + "learning_rate = 1e-2\n", + "num_epochs = 10\n", + "batch_size = 1024\n", + "device = \"cuda:0\" if torch.cuda.is_available() else \"cpu\"\n", + "\n", + "dataset = My_Dataset()\n", + "dataloader = DataLoader(\n", + " dataset=dataset, batch_size=batch_size, shuffle=True, num_workers=5, pin_memory=True\n", + ")\n", + "\n", + "model = Model_2_2().to(device)\n", + "criterion = nn.BCELoss()\n", + "optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)\n", + "\n", + "for epoch in range(num_epochs):\n", + " total_epoch_loss = 0\n", + " total_epoch_pred = 0\n", + " total_epoch_target = 0\n", + " for x, targets in dataloader:\n", + " optimizer.zero_grad()\n", + "\n", + " x = x.to(device)\n", + " targets = targets.to(device)\n", + "\n", + " x = x.unsqueeze(1)\n", + " targets = targets.unsqueeze(1)\n", + " y_pred = model(x)\n", + " loss = criterion(y_pred, targets)\n", + " total_epoch_loss += loss.item()\n", + " total_epoch_target += targets.sum().item()\n", + " total_epoch_pred += y_pred.sum().item()\n", + "\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " print(\n", + " f\"Epoch {epoch + 1}/{num_epochs}, Loss: {total_epoch_loss}, Acc: {1 - abs(total_epoch_pred - total_epoch_target) / total_epoch_target}\"\n", + " )\n", + "\n", + "with torch.no_grad():\n", + " test_data = (np.array([[2]]) - dataset.min_x) / (dataset.max_x - dataset.min_x)\n", + " test_data = Variable(\n", + " torch.tensor(test_data, dtype=torch.float64), requires_grad=False\n", + " ).to(device)\n", + " predicted = model(test_data).to(\"cpu\")\n", + " print(\n", + " f\"Model weights: {model.linear.weight.item()}, bias: {model.linear.bias.item()}\"\n", + " )\n", + " print(f\"Prediction for test data: {predicted.item()}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "e6bff679-f8d2-46cc-bdcb-82af7dab38b3", + "metadata": {}, + "source": [ + "对比发现,使用torch.nn的内置损失函数和优化器,正确率提升更快。\n", + "\n", + "但是为什么相同分布的数据集训练出的权重和偏置,以及预测结果存在较大差别,这个问题的原因还有待我探究。" + ] + }, + { + "cell_type": "markdown", + "id": "ef41d7fa-c2bf-4024-833b-60af0a87043a", + "metadata": {}, + "source": [ + "# 三、动手实现softmax回归\n", + "\n", + "## 问题1\n", + "\n", + "**要求动手从0实现softmax回归(只借助Tensor和Numpy相关的库)在Fashion-MNIST数据集上进行训练和测试,并从loss、训练集以及测试集上的准确率等多个角度对结果进行分析(要求从零实现交叉熵损失函数)**" + ] + }, + { + "cell_type": "markdown", + "id": "3c356760-75a8-4814-ba69-73b270396a4e", + "metadata": {}, + "source": [ + "手动实现nn.one_hot()。\n", + "\n", + "one-hot向量用于消除线性标签值所映射的类别的非线性。\n", + "\n", + "one-hot向量是使用一个长度为分类数量的数组表示标签值,其中有且仅有1个值为为1,该值的下标为标签值;其余为0。\n", + "\n", + "原理很简单,步骤如下:\n", + "1. 初始化全零的张量,大小为(标签数量,分类数量);\n", + "2. 将标签值映射到全零张量的\\[下标,标签值\\]中,将该位置为1;\n", + "3. 返回修改后的张量,即是ont-hot向量。" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "e605f1b0-1d32-410f-bddf-402a85ccc9ff", + "metadata": {}, + "outputs": [], + "source": [ + "def my_one_hot(indices: torch.Tensor, num_classes: int):\n", + " one_hot_tensor = torch.zeros(len(indices), num_classes).to(indices.device)\n", + " one_hot_tensor.scatter_(1, indices.view(-1, 1), 1)\n", + " return one_hot_tensor" + ] + }, + { + "cell_type": "markdown", + "id": "902603a6-bfb9-4ce3-bd0d-b00cebb1d3cb", + "metadata": {}, + "source": [ + "手动实现CrossEntropyLoss。\n", + "\n", + "CrossEntropyLoss由一个log_softmax和一个nll_loss组成。\n", + "\n", + "softmax的数学表达式如下:\n", + "$$\n", + "\\text{softmax}(y_i) = \\frac{e^{y_i - \\text{max}(y)}}{\\sum_{j=1}^{N} e^{y_j - \\text{max}(y)}} \n", + "$$\n", + "log_softmax即为$\\log\\left(softmax\\left(y\\right)\\right)$。\n", + "\n", + "CrossEntropyLoss的数学表达式如下:\n", + "$$\n", + "\\text{CrossEntropyLoss}(y, \\hat{y}) = -\\frac{1}{N} \\sum_{i=1}^{N} \\hat{y}_i \\cdot \\log(\\text{softmax}(y_i)) \n", + "$$\n", + "\n", + "故代码如下:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "759a3bb2-b5f4-4ea5-a2d7-15f0c4cdd14b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "输入:\n", + "tensor([[-1.2914, 0.4715, -0.0432, 1.7427, -1.9236],\n", + " [ 0.5361, -0.7551, -0.6810, 1.0945, 0.6135],\n", + " [-1.3398, -0.0026, -1.6066, -0.4659, -1.6076]], requires_grad=True)\n", + "标签:\n", + "tensor([[1., 0., 0., 0., 0.],\n", + " [0., 0., 0., 0., 1.],\n", + " [0., 0., 0., 0., 1.]])\n", + "My_CrossEntropyLoss损失值: 2.4310648441314697\n", + "nn.CrossEntropyLoss损失值: 2.4310646057128906\n" + ] + } + ], + "source": [ + "class My_CrossEntropyLoss:\n", + " def __call__(self, predictions: torch.Tensor, targets: torch.Tensor):\n", + " max_values = torch.max(predictions, dim=1, keepdim=True).values\n", + " exp_values = torch.exp(predictions - max_values)\n", + " softmax_output = exp_values / torch.sum(exp_values, dim=1, keepdim=True)\n", + " log_probs = torch.log(softmax_output)\n", + " \n", + " nll_loss = -torch.sum(targets * log_probs, dim=1)\n", + " average_loss = torch.mean(nll_loss)\n", + " return average_loss\n", + "\n", + " \n", + "# 测试\n", + "input = torch.randn(3, 5, requires_grad=True)\n", + "target = torch.randn(3, 5).softmax(dim=1).argmax(1)\n", + "target = torch.nn.functional.one_hot(target, num_classes=5).to(dtype=torch.float32)\n", + "print(f\"输入:\\n{input}\")\n", + "print(f\"标签:\\n{target}\")\n", + "\n", + "my_crossentropyloss = My_CrossEntropyLoss()\n", + "my_loss = my_crossentropyloss(input, target)\n", + "print(\"My_CrossEntropyLoss损失值:\", my_loss.item())\n", + "\n", + "nn_crossentropyloss = nn.CrossEntropyLoss()\n", + "nn_loss = nn_crossentropyloss(input, target)\n", + "print(\"nn.CrossEntropyLoss损失值:\", nn_loss.item())" + ] + }, + { + "cell_type": "markdown", + "id": "dbf78501-f5be-4008-986c-d331d531491f", + "metadata": {}, + "source": [ + "手动实现Flatten。\n", + "\n", + "原理很简单,就是把多维的张量拉直成一个向量。" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "74322629-8325-4823-b80f-f28182d577c1", + "metadata": {}, + "outputs": [], + "source": [ + "class My_Flatten:\n", + " def __call__(self, x: torch.Tensor):\n", + " return self.forward(x)\n", + "\n", + " def forward(self, x: torch.Tensor):\n", + " x = x.view(x.shape[0], -1)\n", + " return x" + ] + }, + { + "cell_type": "markdown", + "id": "35aee905-ae37-4faa-a7f1-a04cd8579f78", + "metadata": {}, + "source": [ + "手动实现softmax回归模型。\n", + "\n", + "模型很简单,主要由一个Flatten层和一个线性层组成。\n", + "\n", + "Flatten层主要用于将2维的图像展开,直接作为1维的特征量输入网络。" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "bb31a75e-464c-4b94-b927-b219a765e35d", + "metadata": {}, + "outputs": [], + "source": [ + "class Model_3_1:\n", + " def __init__(self, num_classes):\n", + " self.flatten = My_Flatten()\n", + " self.linear = My_Linear(28 * 28, num_classes)\n", + " self.params = self.linear.params\n", + "\n", + " def __call__(self, x: torch.Tensor):\n", + " return self.forward(x)\n", + "\n", + " def forward(self, x: torch.Tensor):\n", + " x = self.flatten(x)\n", + " x = self.linear(x)\n", + " return x\n", + "\n", + " def to(self, device: str):\n", + " for param in self.params:\n", + " param.data = param.data.to(device=device)\n", + " return self\n", + "\n", + " def parameters(self):\n", + " return self.params" + ] + }, + { + "cell_type": "markdown", + "id": "17e686d1-9c9a-4727-8fdc-9990d348c523", + "metadata": {}, + "source": [ + "训练与测试过程与之前手动实现的几乎一致。由于数据集的变化,对应超参数也进行了调整。\n", + "\n", + "数据集也使用了现成的FashionMNIST数据集,且划分了训练集和测试集。\n", + "\n", + "FashionMNIST数据集直接调用API获取。数据集的image为28*28的单通道灰白图片,label为单个数值标签。\n", + "\n", + "训练softmax回归模型。\n", + "进行如下步骤:\n", + "1. 初始化超参数\n", + "2. 获取数据集\n", + "3. 初始化模型\n", + "4. 定义损失函数和优化器\n", + "5. 训练\n", + " 1. 从训练dataloader中获取批量数据\n", + " 2. 传入模型\n", + " 3. 使用损失函数计算与ground_truth的损失\n", + " 4. 使用优化器进行反向传播\n", + " 5. 循环以上步骤\n", + "6. 测试\n", + " 1. 从测试dataloader中获取批量数据\n", + " 2. 传入模型\n", + " 3. 将预测值与ground_truth进行比较,得出正确率\n", + " 4. 对整个训练集统计正确率,从而分析训练效果" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "d816dae1-5fbe-4c29-9597-19d66b5eb6b4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 1/10, Loss: nan, Acc: 0.09999999403953552\n", + "Epoch 2/10, Loss: nan, Acc: 0.09999999403953552\n", + "Epoch 3/10, Loss: nan, Acc: 0.09999999403953552\n", + "Epoch 4/10, Loss: nan, Acc: 0.09999999403953552\n", + "Epoch 5/10, Loss: nan, Acc: 0.09999999403953552\n", + "Epoch 6/10, Loss: nan, Acc: 0.09999999403953552\n", + "Epoch 7/10, Loss: nan, Acc: 0.09999999403953552\n", + "Epoch 8/10, Loss: nan, Acc: 0.09999999403953552\n", + "Epoch 9/10, Loss: nan, Acc: 0.09999999403953552\n", + "Epoch 10/10, Loss: nan, Acc: 0.09999999403953552\n" + ] + } + ], + "source": [ + "learning_rate = 5e-3\n", + "num_epochs = 10\n", + "batch_size = 4096\n", + "num_classes = 10\n", + "device = \"cuda:0\" if torch.cuda.is_available() else \"cpu\"\n", + "\n", + "transform = transforms.Compose(\n", + " [\n", + " transforms.ToTensor(),\n", + " transforms.Normalize((0.5,), (0.5,)),\n", + " ]\n", + ")\n", + "train_dataset = datasets.FashionMNIST(root=\"./dataset\", train=True, transform=transform, download=True)\n", + "test_dataset = datasets.FashionMNIST(root=\"./dataset\", train=False, transform=transform, download=True)\n", + "train_loader = DataLoader(\n", + " dataset=train_dataset, batch_size=batch_size,\n", + " shuffle=True, num_workers=4, pin_memory=True,\n", + ")\n", + "test_loader = DataLoader(\n", + " dataset=test_dataset, batch_size=batch_size,\n", + " shuffle=True, num_workers=4, pin_memory=True,\n", + ")\n", + "\n", + "model = Model_3_1(num_classes).to(device)\n", + "criterion = My_CrossEntropyLoss()\n", + "optimizer = My_optimizer(model.parameters(), lr=learning_rate)\n", + "\n", + "for epoch in range(num_epochs):\n", + " total_epoch_loss = 0\n", + " for images, targets in train_loader:\n", + " optimizer.zero_grad()\n", + "\n", + " images = images.to(device)\n", + " targets = targets.to(device).to(dtype=torch.long)\n", + "\n", + " one_hot_targets = (\n", + " my_one_hot(targets, num_classes=num_classes)\n", + " .to(device)\n", + " .to(dtype=torch.long)\n", + " )\n", + "\n", + " outputs = model(images)\n", + " loss = criterion(outputs, one_hot_targets)\n", + " total_epoch_loss += loss\n", + "\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " total_acc = 0\n", + " with torch.no_grad():\n", + " for image, targets in test_loader:\n", + " image = image.to(device)\n", + " targets = targets.to(device)\n", + " outputs = model(image)\n", + " total_acc += (outputs.argmax(1) == targets).sum()\n", + " print(\n", + " f\"Epoch {epoch + 1}/{num_epochs}, Loss: {total_epoch_loss}, Acc: {total_acc / len(test_dataset)}\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "a49d0165-aeb7-48c0-9b67-956bb08cb356", + "metadata": {}, + "source": [ + "这里发现梯度爆炸。暂时无法解决。" + ] + }, + { + "cell_type": "markdown", + "id": "3ef5240f-8a11-4678-bfce-f1cbc7e71b77", + "metadata": {}, + "source": [ + "## 问题2\n", + "\n", + "**利用torch.nn实现softmax回归在Fashion-MNIST数据集上进行训练和测试,并从loss,训练集以及测试集上的准确率等多个角度对结果进行分析**" + ] + }, + { + "cell_type": "markdown", + "id": "5c4a88c6-637e-4af5-bed5-f644685dcabc", + "metadata": {}, + "source": [ + "使用torch.nn实现模型。\n", + "\n", + "将之前的Model_3_1中的手动实现函数改为torch.nn内置函数即可,再加上继承nn.Module以使用torch.nn内置模型模板特性。" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "0163b9f7-1019-429c-8c29-06436d0a4c98", + "metadata": {}, + "outputs": [], + "source": [ + "class Model_3_2(nn.Module):\n", + " def __init__(self, num_classes):\n", + " super(Model_3_2, self).__init__()\n", + " self.flatten = nn.Flatten()\n", + " self.linear = nn.Linear(28 * 28, num_classes)\n", + "\n", + " def forward(self, x: torch.Tensor):\n", + " x = self.flatten(x)\n", + " x = self.linear(x)\n", + " return x" + ] + }, + { + "cell_type": "markdown", + "id": "6e765ad7-c1c6-4166-bd7f-361666bd4016", + "metadata": {}, + "source": [ + "训练与测试过程与之前手动实现的几乎一致。" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "a58a23e1-368c-430a-ad62-0e256dff564d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 1/10, Loss: 15.148970603942871, Acc: 0.7520999908447266\n", + "Epoch 2/10, Loss: 9.012335777282715, Acc: 0.7996999621391296\n", + "Epoch 3/10, Loss: 7.9114227294921875, Acc: 0.8095999956130981\n", + "Epoch 4/10, Loss: 7.427404403686523, Acc: 0.8215999603271484\n", + "Epoch 5/10, Loss: 7.084254264831543, Acc: 0.8277999758720398\n", + "Epoch 6/10, Loss: 6.885956287384033, Acc: 0.8274999856948853\n", + "Epoch 7/10, Loss: 6.808426380157471, Acc: 0.8327999711036682\n", + "Epoch 8/10, Loss: 6.647855758666992, Acc: 0.8323000073432922\n", + "Epoch 9/10, Loss: 6.560361862182617, Acc: 0.8317999839782715\n", + "Epoch 10/10, Loss: 6.5211310386657715, Acc: 0.8349999785423279\n" + ] + } + ], + "source": [ + "learning_rate = 5e-3\n", + "num_epochs = 10\n", + "batch_size = 4096\n", + "num_classes = 10\n", + "device = \"cuda:0\" if torch.cuda.is_available() else \"cpu\"\n", + "\n", + "transform = transforms.Compose(\n", + " [\n", + " transforms.ToTensor(),\n", + " transforms.Normalize((0.5,), (0.5,)),\n", + " ]\n", + ")\n", + "train_dataset = datasets.FashionMNIST(\n", + " root=\"./dataset\", train=True, transform=transform, download=True\n", + ")\n", + "test_dataset = datasets.FashionMNIST(\n", + " root=\"./dataset\", train=False, transform=transform, download=True\n", + ")\n", + "train_loader = DataLoader(\n", + " dataset=train_dataset,\n", + " batch_size=batch_size,\n", + " shuffle=True,\n", + " num_workers=4,\n", + " pin_memory=True,\n", + ")\n", + "test_loader = DataLoader(\n", + " dataset=test_dataset,\n", + " batch_size=batch_size,\n", + " shuffle=True,\n", + " num_workers=4,\n", + " pin_memory=True,\n", + ")\n", + "\n", + "model = Model_3_2(num_classes).to(device)\n", + "criterion = nn.CrossEntropyLoss()\n", + "optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)\n", + "\n", + "for epoch in range(num_epochs):\n", + " total_epoch_loss = 0\n", + " model.train()\n", + " for images, targets in train_loader:\n", + " optimizer.zero_grad()\n", + "\n", + " images = images.to(device)\n", + " targets = targets.to(device)\n", + "\n", + " one_hot_targets = (\n", + " torch.nn.functional.one_hot(targets, num_classes=num_classes)\n", + " .to(device)\n", + " .to(dtype=torch.float32)\n", + " )\n", + "\n", + " outputs = model(images)\n", + " loss = criterion(outputs, one_hot_targets)\n", + " total_epoch_loss += loss\n", + "\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " model.eval()\n", + " total_acc = 0\n", + " with torch.no_grad():\n", + " for image, targets in test_loader:\n", + " image = image.to(device)\n", + " targets = targets.to(device)\n", + " outputs = model(image)\n", + " total_acc += (outputs.argmax(1) == targets).sum()\n", + " print(\n", + " f\"Epoch {epoch + 1}/{num_epochs}, Loss: {total_epoch_loss}, Acc: {total_acc / len(test_dataset)}\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "59555b67-1650-4e1a-a98e-7906878bf3d0", + "metadata": {}, + "source": [ + "与手动实现的softmax回归相比较,nn.CrossEntropyLoss比手动实现的My_CrossEntropyLoss更加稳定,没有出现梯度爆炸的情况。" + ] + }, + { + "cell_type": "markdown", + "id": "f40431f2-e77b-4ead-81a3-ff6451a8e452", + "metadata": {}, + "source": [ + "**实验心得体会**\n", + "\n", + "通过完成本次Pytorch基本操作实验,让我对Pytorch框架有了更加深入的理解。我接触深度学习主要是在大语言模型领域,比较熟悉微调大模型,但是涉及到底层的深度学习知识,我还有很多短板和不足。这次实验对我这方面的锻炼让我收获良多。\n", + "\n", + "首先是数据集的设置。如果数据没有进行归一化,很容易出现梯度爆炸。这是在我以前直接使用图片数据集的经历中没有遇到过的问题。\n", + "\n", + "在实现logistic回归模型时,通过手动实现各个组件如优化器、线性层等,让我对这些模块的工作原理有了更清晰的认识。尤其是在实现广播机制时,需要充分理解张量操作的维度变换规律。而使用Pytorch内置模块进行实现时,通过继承nn.Module可以自动获得許多功能,使代码更加简洁。\n", + "\n", + "在实现softmax回归时,则遇到了更大的困难。手动实现的模型很容易出现梯度爆炸的问题,而使用Pytorch内置的损失函数和优化器则可以稳定训练。这让我意识到了选择合适的优化方法的重要性。另外,Pytorch强大的自动微分机制也是构建深度神经网络的重要基础。\n", + "\n", + "通过这个实验,让我对Pytorch框架有了更加直观的感受,也让我看到了仅靠基础模块搭建复杂模型的难点所在。这些经验对我后续使用Pytorch构建数据集模型会很有帮助。" + ] + } + ], + "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.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Lab1/code/1.1.py b/Lab1/code/1.1.py new file mode 100644 index 0000000..5f23159 --- /dev/null +++ b/Lab1/code/1.1.py @@ -0,0 +1,39 @@ +import torch + +A = torch.tensor([[1, 2, 3]]) + +B = torch.tensor([[4], + [5]]) + +# 方法1: 使用PyTorch的减法操作符 +result1 = A - B + +# 方法2: 使用PyTorch的sub函数 +result2 = torch.sub(A, B) + +# 方法3: 手动实现广播机制并作差 +def mysub(a:torch.Tensor, b:torch.Tensor): + if not ( + (a.size(0) == 1 and b.size(1) == 1) + or + (a.size(1) == 1 and b.size(0) == 1) + ): + raise ValueError("输入的张量大小无法满足广播机制的条件。") + else: + target_shape = torch.Size([max(A.size(0), B.size(0)), max(A.size(1), B.size(1))]) + A_broadcasted = A.expand(target_shape) + B_broadcasted = B.expand(target_shape) + result = torch.zeros(target_shape, dtype=torch.int64).to(device=A_broadcasted.device) + for i in range(target_shape[0]): + for j in range(target_shape[1]): + result[i, j] = A_broadcasted[i, j] - B_broadcasted[i, j] + return result + +result3 = mysub(A, B) + +print("方法1的结果:") +print(result1) +print("方法2的结果:") +print(result2) +print("方法3的结果:") +print(result3) diff --git a/Lab1/code/1.2.py b/Lab1/code/1.2.py new file mode 100644 index 0000000..66986c2 --- /dev/null +++ b/Lab1/code/1.2.py @@ -0,0 +1,23 @@ +import torch + +mean = 0 +stddev = 0.01 + +P = torch.normal(mean=mean, std=stddev, size=(3, 2)) +Q = torch.normal(mean=mean, std=stddev, size=(4, 2)) + +print("矩阵 P:") +print(P) +print("矩阵 Q:") +print(Q) + +# 对矩阵Q进行转置操作,得到矩阵Q的转置Q^T +QT = Q.T +print("矩阵 QT:") +print(QT) + +# 计算矩阵P和矩阵Q^T的矩阵相乘 +result = torch.matmul(P, QT) +print("矩阵相乘的结果:") +print(result) + diff --git a/Lab1/code/1.3.py b/Lab1/code/1.3.py new file mode 100644 index 0000000..ef057e1 --- /dev/null +++ b/Lab1/code/1.3.py @@ -0,0 +1,12 @@ +import torch + +x = torch.tensor(1.0, requires_grad=True) +y_1 = x**2 +with torch.no_grad(): + y_2 = x**3 + +y3 = y_1 + y_2 + +y3.backward() + +print("梯度(dy_3/dx): ", x.grad.item()) diff --git a/Lab1/code/2.1.py b/Lab1/code/2.1.py new file mode 100644 index 0000000..7b10711 --- /dev/null +++ b/Lab1/code/2.1.py @@ -0,0 +1,143 @@ +import numpy as np +import torch +from torch.autograd import Variable +from torch.utils.data import Dataset, DataLoader +from tqdm import tqdm +import ipdb + + +class My_BCELoss: + def __call__(self, prediction: torch.Tensor, target: torch.Tensor): + loss = -torch.mean( + target * torch.log(prediction) + (1 - target) * torch.log(1 - prediction) + ) + return loss + + +class My_optimizer: + def __init__(self, params: list[torch.Tensor], lr: float): + self.params = params + self.lr = lr + + def step(self): + for param in self.params: + param.data = param.data - self.lr * param.grad.data + + def zero_grad(self): + for param in self.params: + if param.grad is not None: + param.grad.data.zero_() + + +class My_Linear: + def __init__(self, input_feature: int, output_feature: int): + self.weight = torch.randn( + (output_feature, input_feature), requires_grad=True, dtype=torch.float32 + ) + self.bias = torch.randn(1, requires_grad=True, dtype=torch.float32) + self.params = [self.weight, self.bias] + + def __call__(self, x): + return self.forward(x) + + def forward(self, x): + x = torch.matmul(x, self.weight.T) + self.bias + return x + + def to(self, device: str): + for param in self.params: + param.data = param.data.to(device=device) + return self + + def parameters(self): + return self.params + + +class Model: + def __init__(self): + self.linear = My_Linear(1, 1) + self.params = self.linear.params + + def __call__(self, x): + return self.forward(x) + + def forward(self, x): + x = self.linear(x) + x = torch.sigmoid(x) + return x + + def to(self, device: str): + for param in self.params: + param.data = param.data.to(device=device) + return self + + def parameters(self): + return self.params + + +class My_Dataset(Dataset): + def __init__(self, data_size=1000000): + np.random.seed(0) + x = 2 * np.random.rand(data_size, 1) + noise = 0.2 * np.random.randn(data_size, 1) + y = 4 - 3 * x + noise + self.min_x, self.max_x = np.min(x), np.max(x) + min_y, max_y = np.min(y), np.max(y) + x = (x - self.min_x) / (self.max_x - self.min_x) + y = (y - min_y) / (max_y - min_y) + self.data = [[x[i][0], y[i][0]] for i in range(x.shape[0])] + + def __len__(self): + return len(self.data) + + def __getitem__(self, index): + x, y = self.data[index] + return x, y + + +learning_rate = 5e-2 +num_epochs = 10 +batch_size = 1024 +device = "cuda:0" if torch.cuda.is_available() else "cpu" + +dataset = My_Dataset() +dataloader = DataLoader( + dataset=dataset, batch_size=batch_size, shuffle=True, num_workers=5, pin_memory=True +) + +model = Model().to(device) +criterion = My_BCELoss() +optimizer = My_optimizer(model.parameters(), lr=learning_rate) + +for epoch in range(num_epochs): + total_epoch_loss = 0 + total_epoch_pred = 0 + total_epoch_target = 0 + for index, (x, targets) in tqdm(enumerate(dataloader), total=len(dataloader)): + optimizer.zero_grad() + x = x.to(device).to(dtype=torch.float32) + targets = targets.to(device).to(dtype=torch.float32) + x = x.unsqueeze(1) + y_pred = model(x) + loss = criterion(y_pred, targets) + total_epoch_loss += loss.item() + total_epoch_target += targets.sum().item() + total_epoch_pred += y_pred.sum().item() + + loss.backward() + optimizer.step() + + print( + f"Epoch {epoch + 1}/{num_epochs}, Loss: {total_epoch_loss}, Acc: {1 - abs(total_epoch_pred - total_epoch_target) / total_epoch_target}" + ) + +with torch.no_grad(): + test_data = (np.array([[2]]) - dataset.min_x) / (dataset.max_x - dataset.min_x) + test_data = Variable( + torch.tensor(test_data, dtype=torch.float64), requires_grad=False + ).to(device) + predicted = model(test_data).to("cpu") + print( + f"Model weights: {model.linear.weight.item()}, bias: {model.linear.bias.item()}" + ) + print(f"Prediction for test data: {predicted.item()}") diff --git a/Lab1/code/2.2.py b/Lab1/code/2.2.py new file mode 100644 index 0000000..1015a09 --- /dev/null +++ b/Lab1/code/2.2.py @@ -0,0 +1,89 @@ +import numpy as np +import torch +from torch.autograd import Variable +from torch.utils.data import Dataset, DataLoader +from torch import nn +from tqdm import tqdm +import ipdb + + +class Model(nn.Module): + def __init__(self): + super(Model, self).__init__() + self.linear = nn.Linear(1, 1, dtype=torch.float64) + + def forward(self, x): + x = self.linear(x) + x = torch.sigmoid(x) + return x + + +class My_Dataset(Dataset): + def __init__(self, data_size=1000000): + np.random.seed(0) + x = 2 * np.random.rand(data_size, 1) + noise = 0.2 * np.random.randn(data_size, 1) + y = 4 - 3 * x + noise + self.min_x, self.max_x = np.min(x), np.max(x) + min_y, max_y = np.min(y), np.max(y) + x = (x - self.min_x) / (self.max_x - self.min_x) + y = (y - min_y) / (max_y - min_y) + self.data = [[x[i][0], y[i][0]] for i in range(x.shape[0])] + + def __len__(self): + return len(self.data) + + def __getitem__(self, index): + x, y = self.data[index] + return x, y + + +learning_rate = 1e-2 +num_epochs = 10 +batch_size = 1024 +device = "cuda:0" if torch.cuda.is_available() else "cpu" + +dataset = My_Dataset() +dataloader = DataLoader( + dataset=dataset, batch_size=batch_size, shuffle=True, num_workers=5, pin_memory=True +) + +model = Model().to(device) +criterion = nn.BCELoss() +optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate) + +for epoch in range(num_epochs): + total_epoch_loss = 0 + total_epoch_pred = 0 + total_epoch_target = 0 + for index, (x, targets) in tqdm(enumerate(dataloader), total=len(dataloader)): + optimizer.zero_grad() + + x = x.to(device) + targets = targets.to(device) + + x = x.unsqueeze(1) + targets = targets.unsqueeze(1) + y_pred = model(x) + loss = criterion(y_pred, targets) + total_epoch_loss += loss.item() + total_epoch_target += targets.sum().item() + total_epoch_pred += y_pred.sum().item() + + loss.backward() + optimizer.step() + + print( + f"Epoch {epoch + 1}/{num_epochs}, Loss: {total_epoch_loss}, Acc: {1 - abs(total_epoch_pred - total_epoch_target) / total_epoch_target}" + ) + +with torch.no_grad(): + test_data = (np.array([[2]]) - dataset.min_x) / (dataset.max_x - dataset.min_x) + test_data = Variable( + torch.tensor(test_data, dtype=torch.float64), requires_grad=False + ).to(device) + predicted = model(test_data).to("cpu") + print( + f"Model weights: {model.linear.weight.item()}, bias: {model.linear.bias.item()}" + ) + print(f"Prediction for test data: {predicted.item()}") diff --git a/Lab1/code/3.1.py b/Lab1/code/3.1.py new file mode 100644 index 0000000..6dd8b47 --- /dev/null +++ b/Lab1/code/3.1.py @@ -0,0 +1,169 @@ +import numpy as np +import torch +from torch.utils.data import Dataset, DataLoader +from torch import nn +from tqdm import tqdm +from torchvision import datasets, transforms +from torch.utils.data import DataLoader + + +def my_one_hot(indices: torch.Tensor, num_classes: int): + one_hot_tensor = torch.zeros(len(indices), num_classes).to(indices.device) + one_hot_tensor.scatter_(1, indices.view(-1, 1), 1) + return one_hot_tensor + + +class My_CrossEntropyLoss: + def __call__(self, predictions: torch.Tensor, targets: torch.Tensor): + max_values = torch.max(predictions, dim=1, keepdim=True).values + exp_values = torch.exp(predictions - max_values) + softmax_output = exp_values / torch.sum(exp_values, dim=1, keepdim=True) + + log_probs = torch.log(softmax_output) + nll_loss = -torch.sum(targets * log_probs, dim=1) + average_loss = torch.mean(nll_loss) + return average_loss + + +class My_optimizer: + def __init__(self, params: list[torch.Tensor], lr: float): + self.params = params + self.lr = lr + + def step(self): + for param in self.params: + param.data = param.data - self.lr * param.grad.data + + def zero_grad(self): + for param in self.params: + if param.grad is not None: + param.grad.data.zero_() + + +class My_Linear: + def __init__(self, input_feature: int, output_feature: int): + self.weight = torch.randn( + (output_feature, input_feature), requires_grad=True, dtype=torch.float32 + ) + self.bias = torch.randn(1, requires_grad=True, dtype=torch.float32) + self.params = [self.weight, self.bias] + + def __call__(self, x: torch.Tensor): + return self.forward(x) + + def forward(self, x: torch.Tensor): + x = torch.matmul(x, self.weight.T) + self.bias + return x + + def to(self, device: str): + for param in self.params: + param.data = param.data.to(device=device) + return self + + def parameters(self): + return self.params + + +class My_Flatten: + def __call__(self, x: torch.Tensor): + return self.forward(x) + + def forward(self, x: torch.Tensor): + x = x.view(x.shape[0], -1) + return x + + +class Model_3_1: + def __init__(self, num_classes): + self.flatten = My_Flatten() + self.linear = My_Linear(28 * 28, num_classes) + self.params = self.linear.params + + def __call__(self, x: torch.Tensor): + return self.forward(x) + + def forward(self, x: torch.Tensor): + x = self.flatten(x) + x = self.linear(x) + return x + + def to(self, device: str): + for param in self.params: + param.data = param.data.to(device=device) + return self + + def parameters(self): + return self.params + + +learning_rate = 5e-3 +num_epochs = 10 +batch_size = 4096 +num_classes = 10 +device = "cuda:0" if torch.cuda.is_available() else "cpu" + +transform = transforms.Compose( + [ + transforms.ToTensor(), + transforms.Normalize((0.5,), (0.5,)), + ] +) +train_dataset = datasets.FashionMNIST( + root="./dataset", train=True, transform=transform, download=True +) +test_dataset = datasets.FashionMNIST( + root="./dataset", train=False, transform=transform, download=True +) +train_loader = DataLoader( + dataset=train_dataset, + batch_size=batch_size, + shuffle=True, + num_workers=4, + pin_memory=True, +) +test_loader = DataLoader( + dataset=test_dataset, + batch_size=batch_size, + shuffle=True, + num_workers=4, + pin_memory=True, +) + +model = Model_3_1(num_classes).to(device) +criterion = My_CrossEntropyLoss() +optimizer = My_optimizer(model.parameters(), lr=learning_rate) + +for epoch in range(num_epochs): + total_epoch_loss = 0 + for index, (images, targets) in tqdm( + enumerate(train_loader), total=len(train_loader) + ): + optimizer.zero_grad() + + images = images.to(device) + targets = targets.to(device).to(dtype=torch.long) + + one_hot_targets = ( + my_one_hot(targets, num_classes=num_classes).to(device).to(dtype=torch.long) + ) + + outputs = model(images) + # ipdb.set_trace() + loss = criterion(outputs, one_hot_targets) + total_epoch_loss += loss + + loss.backward() + optimizer.step() + + total_acc = 0 + with torch.no_grad(): + for index, (image, targets) in tqdm( + enumerate(test_loader), total=len(test_loader) + ): + image = image.to(device) + targets = targets.to(device) + outputs = model(image) + total_acc += (outputs.argmax(1) == targets).sum() + print( + f"Epoch {epoch + 1}/{num_epochs} Train, Loss: {total_epoch_loss}, Acc: {total_acc / len(test_dataset)}" + ) diff --git a/Lab1/code/3.2.py b/Lab1/code/3.2.py new file mode 100644 index 0000000..8624929 --- /dev/null +++ b/Lab1/code/3.2.py @@ -0,0 +1,96 @@ +import numpy as np +import torch +from torch.utils.data import DataLoader +from torch import nn +from tqdm import tqdm +from torchvision import datasets, transforms +from torch.utils.data import DataLoader +import ipdb + + +class Model(nn.Module): + def __init__(self, num_classes): + super(Model, self).__init__() + self.flatten = nn.Flatten() + self.linear = nn.Linear(28 * 28, num_classes) + + def forward(self, x: torch.Tensor): + x = self.flatten(x) + x = self.linear(x) + return x + + +learning_rate = 5e-3 +num_epochs = 10 +batch_size = 4096 +num_classes = 10 +device = "cuda:0" if torch.cuda.is_available() else "cpu" + +transform = transforms.Compose( + [ + transforms.ToTensor(), + transforms.Normalize((0.5,), (0.5,)), + ] +) +train_dataset = datasets.FashionMNIST( + root="./dataset", train=True, transform=transform, download=True +) +test_dataset = datasets.FashionMNIST( + root="./dataset", train=False, transform=transform, download=True +) +train_loader = DataLoader( + dataset=train_dataset, + batch_size=batch_size, + shuffle=True, + num_workers=4, + pin_memory=True, +) +test_loader = DataLoader( + dataset=test_dataset, + batch_size=batch_size, + shuffle=True, + num_workers=4, + pin_memory=True, +) + +model = Model(num_classes).to(device) +criterion = nn.CrossEntropyLoss() +optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate) + +for epoch in range(num_epochs): + total_epoch_loss = 0 + model.train() + for index, (images, targets) in tqdm( + enumerate(train_loader), total=len(train_loader) + ): + optimizer.zero_grad() + + images = images.to(device) + targets = targets.to(device) + + one_hot_targets = ( + torch.nn.functional.one_hot(targets, num_classes=num_classes) + .to(device) + .to(dtype=torch.float32) + ) + + outputs = model(images) + loss = criterion(outputs, one_hot_targets) + total_epoch_loss += loss + + loss.backward() + optimizer.step() + + model.eval() + total_acc = 0 + with torch.no_grad(): + for index, (image, targets) in tqdm( + enumerate(test_loader), total=len(test_loader) + ): + image = image.to(device) + targets = targets.to(device) + outputs = model(image) + total_acc += (outputs.argmax(1) == targets).sum() + print( + f"Epoch {epoch + 1}/{num_epochs} Train, Loss: {total_epoch_loss}, Acc: {total_acc / len(test_dataset)}" + ) diff --git a/images/school_logo.png b/images/school_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..777f05e43c5f50e0339071890c449ae5bde88416 GIT binary patch literal 116916 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ⅅU%x%dPe-XFI|HEKw%x;j@?T?H>j4;nOh5TJfE-ZFy*XR0pv-r4OuPT-Sv zVg-|3^GOW{Ylh<{0eb7Xfq24;wYvS$|37zs9VW%ut&QUUpYQz6`M$m1y#6RUo?#%&z~tKb-P6-u zUDhRc-D|B!)>GmtC^Ew_kyBR5T_nx;0f79LyZU&>pUKu{()6@rTT=d1_0z7b4azA6 z)+4t?YuD}B`f_PL%QU~9b0kanYba?M&aYPhM=`Yy3TeFMWx;VQ?5{O&I3^GN?1znG zC!Vul+08L|A^%_IJuyt#N;1<)&Mls%*&4aG*^uSPaxtH&4dC>ESZj`GuXq) zn@!U&fiEhP^&h>WqCIJa65`mRa7ZPew7eib@>S0#1z>ou<*oavK&0%HdaISV%K20E zzEpJ7OCrA$8{8ep*@*`BvFER90^MWSV5A`x9T&^~>n2Gx1#}}|-vit|DGhZT;kA}{ z_gOQ{Ll`;z#YpOYKZhikg)5VYg&L3exL6QK4xIr5(>)qO$Afe>$~H|N0Ui*?(ro<9 z%_=IWca0C^E+WO@1&-p=7`S^Eoz3D$V1c&`-n@0fb5=XuE%VF8PXt&_ zTFzinZUjY?l=QnNKWW>N?EZFapr`!9gZE>cKh1>VlU{DM=SF79B0*OnZplFfTH0(BWw%%nutS{;9YkPAZh}DLy9+3QM8A% z4W&{~ran~_Nm9v?nG%b*a4;FJXng|TBXcWT=67ofQBo`?T{5$ml18V@$M&S;C*jvL zRnz!~qoomZ`U0Ma?6_C>Vj-uJR;6UC!GLrMmAg#+=ab^`hEYj4I(YcF+AQ;?Tp*cs z$ORk_y!?AYVa+AU8n5F8l63C(t~3}Q_m44jB-LMQsP#%cm=r`%XRLKBAn70g>T7F? z@y_T(9oGONN-jw@mi*qqR?y�z(hT9%AX0#uxIc^BP5^OS;D;VszH`&uFSlqHZNE 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%0D_D-m+s=2Hq&M0a-tn6I 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

&pP?YnUf0;yv2Ne zq#vTHoa6;Ojbk%zZTu(6EMW`B-}=_-UI^$_UB>=XmlP=dGvLob(rWOHEHWXCA#a(* zZ6Dk|Z$h4mQow~6MEsa>3KRiu5a0BB0B;e@KYp5TEgX2*DHoi1=WB1iyKeiIZcY_? zB^)BB85QGSo6((e8H{{)Pj~m0Z$5bH`ZG^GWA4PMZ8(G#9Z=Zf54;Xy?3r9Dl1<}o z<;JC{LMrMFPC5GGr@j@c2HwFz_kilJ`V-(!V(x1&s zCPyPX8LW9$)3zUia0;A+fG?JrG&YkNf5_p-pMKieSKRjKD{sI4#oASyH*MOyv8%JQ zYxA1bYnFYn_=_(-eEHrRuetQxW9A>U|D*!zr{nQZL!ipeAO8yS)oiH6}S&bjx0wB4xaJh=*fVJ)6N4*pyub*qI*aaPHf2S=Oy z3>YThj|V(76^W+7j|_QzbT*p-iES;wZStf^lO|5g7xKA$A{u6+;W#Lw&rhWjp~RSY zDi!?+cJ2a(%Cxm6vEhl$_#@L#ePkJDqW+^1AYSgD41X??!a2qs{c8UV$!i9la!YPI zZwliHg5T-OjLZ4|JxgPmEF1LZ3;sZ)F%Y9ti7*w7MkCRv*NgX2iab)$c#e&F{eb{T zGwC#nhg({H&;a_cfS}DEN`&ZOfXA)a*p~R!!|~Ie_;v``T8+^WY;2H{ z@JX%h03!9B3;Z}pgqF{@&A;@)FE}jc1X^#ZrpB2#ZuC!yKNU%L(ID)D0vSa$wN(uV z-npi{=9PypIkdGfCg8;wsm7)sAr*X>9-B^QYg}eSL`j)!7U-4B)!xO>2)i-aK#(xB zwizZjxEkX78z((Zv3T3WxtHJd?9wir&BWAj$djyLr`Ax3-9MX_{;5cc%&Y;=GpZ>( z7&wVDsVq^kwJo2#`p_kFCrrx4s6+-8mB|iOINufVaA4=K_o_-(88=8fHV_I1;sH=w z@+mx2-{0jKtXOTSM*uz1Z*7}A@6x*;e{WkIcx2>Rch^5R_gu1HJ?S0O zq0n&cP{}t|k52i=?f<@J@$zMpZit$^aIW+J#9*5A&%{GJxKy(uEP?H@;k1h7Tm3gI zmD|_4NJ9R|KX$p=lg;aEHbre{dsKes@xi_Kc7C`2XZ>%-kIO%o*fTn>-8NUrBvk&! zmygli>+F)gO)+~Suxq9L&DLMW5<;O1p85qnHs0nH+IDKmBH+p0e&=RXUO9Wd{!eUr z=i#D7r8WGelbANV6@0Sg8`oSW;cU}V{mS3HyZ!Ae>$FeAt}t}i)sf}$J5@qO%_i(= z@dw$qL%EG!zm*pj+Y5Jg#V)Vidx&Xg-?BuX zrxtg9h0eFDe;ny9R~(@x$NwWBRPm3R?UD}p&Q)biyetR)s~nQ#nQ#EOR{BtO8>h*H zmgU!&`z$|xX+5uZcSCZ6fbbswUSpXZN&j-#{yq;>*JSc^oaH09>ygwPwv8fYUz00k za}$owTGzjB=d(G`N z_LH=8*59s7*8BCldbN4pi$^_Pp;n&8D}ohM%U(t8;(nzf+vD7JgnOO^-^@1`uZB)u zZ}vMPCO*Er{P*?qcPkzqoj#}dNe`!`O2P_5pBr7We*e2tn!dOO@;v5NYu43$n6jtf zK;iPaRi`4&b$_L~`ePN~oZYzQ`MjDZipJ{OT9pI)zsEn6Hoat@wZwc@M()}3*8ksZ z*4y`Emh}7IIqfToVpC44EerPnq8Pi>E@{?lkJc5lZA_rjl3x$`3`9`}~peY-SCHdW?t%9|k5Ex&k{ z7Qc|JOZ^(%zHb+w!`vnRvp3}UTsHUlA}# ze@!O1J1d(mYG^#+&FiE$b;^^KM$<$t9zg`})_Lf-G z@qCxLOZLm|50UDebZy@CkJ16^nX-rFN+(@2d=ogz95mtZ@&CHXz|*Ptp8sC~q#yE5 d|6l+2KcnPY=Y)B?x%?S`z|+;wWt~$(69D