从梯度下降到神经网络学习
本次学习通过对《深度学习入门:基于Python的理论与实践》该书前四章进行理论研究,及在ai大模型协助下进行可训练神经网络框架的书写,深刻理解深度学习。以下是相关的学习成果:
1.理论问题回答
一、学习与模型(第 1 章)
Q1. 神经网络训练过程中,哪些量是已知的,哪些量是未知的?
已知量:
1.训练数据和测试数据(或监督数据和测试数据);
2.网络结构:神经元的层数、各层神经元数量;人为设定的结构参数。
未知量:
权重参数W和偏置参数b。
学习的目标到底是什么?
通过调整未知的权重参数和偏置参数,最小化损失函数的值,让模型具备对测试数据的泛化识别能力,实现模型对未知数据的稳定预测。
二、线性模型与非线性(第 2 章)
Q2. 为什么单层感知机只能解决线性可分问题?
因为单层感知机只能表示由直线分割的线性空间,对于异或门这类非线性可分问题无法用一条直线划分两类样本。
**Q3. 为什么必须引入非线性激活函数? **
因为线性激活函数有局限性,只能等价于单层线性模型,无法学习复杂的非线性关系。为了发挥叠加层所带来的优势,激活函数必须使用非线性函数。
如果把神经网络中所有激活函数都去掉,会发生什么?
无法学习非线性模式,仅能处理线性可分问题。
三、神经网络的前向计算(第 3 章)
Q4. 神经网络的前向传播,本质上在做什么数学运算?
本质上是矩阵乘法和激活函数运算的交替执行。
先通过矩阵乘法计算输入信号与权重的加权和,并叠加偏置,再通过激活函数对结果进行非线性转换,逐层传递至输出层。
Q5. 为什么分类问题中,输出层通常使用 Softmax? Softmax 在概率意义上做了什么?
核心原因:将神经网络输出的“得分”(未归一化值)转换为概率分布,使输出值总和为1,便于直观解读类别概率,且能与交叉熵误差配合,使反向传播时梯度计算更简洁(直接得到输出与标签的差分)。
在概率上,它对于每个类别i,都计算其相对概率,既保留了各得分的相对大小关系,又将输出归一化到[0,1]区间,满足概率的基本性质(非负性、总和为1)。
四、损失函数与梯度(第 4 章 · 核心)
**Q6. 为什么“准确率”不能作为训练时的优化目标? **
因为准确率是离散指标,多数情况下梯度为0,无法引导参数更新。例如,微小调整权重可能不会改变分类结果,导致准确率不变,参数更新停滞;且准确率的变化不连续,无法反映参数变化对模型性能的连续影响。
为什么必须引入损失函数?
因为它是连续可微的函数,能量化模型预测与真实标签的差异,其梯度可指导参数沿“减小误差”的方向更新;同时,损失函数的连续变化能反映参数调整的效果,确保学习过程持续推进。
五、梯度下降的本质(第 4 章 · 灵魂)
Q7. 梯度在几何意义上代表什么?
在几何意义上,梯度是损失函数在当前参数点处的方向导数最大值方向,即函数值增长最快的方向,其模长表示增长的速率。
为什么沿着负梯度方向更新参数?
因为神经网络学习的目标是最小化损失函数,负梯度方向是损失函数值减小最快的方向,沿该方向更新参数能高效逼近损失函数的最小值(或局部最小值)。
**Q8. 学习率在梯度下降中起什么作用? **
作用:控制参数更新的步长,决定每次迭代中参数沿负梯度方向调整的幅度,是平衡学习速度与收敛效果的关键超参数。
学习率过大会怎样?过小又会怎样?
学习率过大:参数更新步长过大,可能导致损失函数值震荡不收敛,甚至发散(如参数值超出最优范围,损失函数值持续增大)。
学习率过小:参数更新步长过小,学习速度极慢,需要大量迭代才能逼近最优解;且可能陷入局部最小值或鞍点,无法抵达全局最优。
2. 完整可运行代码
1.激活函数 & 损失函数:
使用 numpy 实现:Sigmoid,ReLU,Softmax
实现:交叉熵损失(支持 batch 输入)- import numpy as np
- def sigmoid(x):
- """
- Sigmoid 激活函数:
- 公式: h(x) = 1 / (1 + exp(-x))
- 参数:
- x: 输入数据
- 返回:
- Sigmoid 输出
- """
- return 1 / (1 + np.exp(-x))
- def relu(x):
- """
- ReLU 激活函数:
- 公式: h(x) = max(0, x)
- 参数:
- x: 输入数据 (numpy array)
- 返回:
- ReLU 输出
- """
- return np.maximum(0, x)
- def softmax(x):
- """
- Softmax 激活函数:
- 公式: y_k = exp(a_k) / sum(exp(a_i))
- 参数:
- x: 输入数据 (numpy array)
- 如果是 1D 数组,视为单个样本。
- 如果是 2D 数组,视为 batch 样本。
- 返回:
- Softmax 输出
- """
- if x.ndim == 2:
- # Batch 处理
- x = x.T
- x = x - np.max(x, axis=0) # 稳定性优化(减去最大值,防止 exp 溢出)
- y = np.exp(x) / np.sum(np.exp(x), axis=0)
- return y.T
- # 单个样本处理
- x = x - np.max(x)
- return np.exp(x) / np.sum(np.exp(x))
- def cross_entropy_error(y, t):
- """
- 交叉熵损失函数:
- 公式: E = -sum(t_k * log(y_k))
- 参数:
- y: 神经网络的输出 (概率分布),经过 Softmax
- t: 监督数据 (标签)
- 可以是 one-hot 向量 (例如 [0, 1, 0, 0, ...])
- 也可以是标签索引 (例如 1)
- 返回:
- 损失值 (标量)
- """
- if y.ndim == 1:
- t = t.reshape(1, t.size)
- y = y.reshape(1, y.size)
-
- # 如果 t 是 one-hot 向量,转换为标签索引
- if t.size == y.size:
- t = t.argmax(axis=1)
-
- batch_size = y.shape[0]
- # 添加一个微小值 delta 防止 log(0)
- delta = 1e-7
- return -np.sum(np.log(y[np.arange(batch_size), t] + delta)) / batch_size
复制代码 2.数值梯度
- import numpy as np
- def numerical_gradient(f, x):
- """
- 数值梯度计算函数
- 使用中心差分法近似计算梯度: (f(x+h) - f(x-h)) / 2h
- 参数:
- f: 目标函数
- x: 输入变量 (numpy array)
- 返回:
- 梯度 (与 x 形状相同)
- """
- h = 1e-4 #设置一个很小的数
- grad = np.zeros_like(x) # 生成和 x 形状相同的数组,用于存放梯度
- #遍历x的每一个元素
- it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
- while not it.finished:
- idx = it.multi_index
- tmp_val = x[idx]
-
- # 计算 f(x+h)
- x[idx] = float(tmp_val) + h
- fxh1 = f(x)
-
- # 计算 f(x-h)
- x[idx] = float(tmp_val) - h
- fxh2 = f(x)
-
- # 计算梯度
- grad[idx] = (fxh1 - fxh2) / (2*h)
-
- # 还原值
- x[idx] = tmp_val
- it.iternext()
-
- return grad
复制代码 3.搭建网络
层与反向传播- import numpy as np
- from src.functions import softmax,
- cross_entropy_error
- class Relu:
- """
- ReLU 层
- 前向传播: out = x (x > 0), 0 (x <= 0)
- 反向传播: dx = dout (x > 0), 0 (x <= 0)
- """
- def __init__(self):
- self.mask = None # 用于记录 x 中小于等于 0 的位置
- def forward(self, x):
- """
- 前向传播
- """
- self.mask = (x <= 0)
- out = x.copy()
- out[self.mask] = 0
- return out
- def backward(self, dout):
- """
- 反向传播
- dout: 上一层传下来的梯度
- """
- dout[self.mask] = 0
- dx = dout
- return dx
- class Affine:
- """
- Affine 层 (全连接层)
- 前向传播: out = np.dot(x, W) + b
- """
- def __init__(self, W, b):
- self.W = W # 权重
- self.b = b # 偏置
- self.x = None # 保存输入,用于反向传播
- self.dW = None # 权重的梯度
- self.db = None # 偏置的梯度
- def forward(self, x):
- # 如果输入是张量 (N, C, H, W),需要展平为 (N, D)
- self.original_x_shape = x.shape
- x = x.reshape(x.shape[0], -1)
- self.x = x
-
- out = np.dot(self.x, self.W) + self.b
- return out
- def backward(self, dout):
- dx = np.dot(dout, self.W.T)
- self.dW = np.dot(self.x.T, dout)
- self.db = np.sum(dout, axis=0)
-
- dx = dx.reshape(*self.original_x_shape) # 还原输入形状
- return dx
- class SoftmaxWithLoss:
- """
- SoftmaxWithLoss 层
- 结合了 Softmax 激活函数和 Cross Entropy Loss
- """
- def __init__(self):
- self.loss = None
- self.y = None # Softmax 的输出
- self.t = None # 监督数据 (One-hot 或 标签索引)
- def forward(self, x, t):
- self.t = t
- self.y = softmax(x)
- self.loss = cross_entropy_error(self.y, self.t)
- return self.loss
- def backward(self, dout=1):
- batch_size = self.t.shape[0]
-
- # 处理 one-hot 编码和标签索引两种情况
- if self.t.size == self.y.size: # one-hot
- dx = (self.y - self.t) / batch_size
- else:
- dx = self.y.copy()
- dx[np.arange(batch_size), self.t] -= 1
- dx = dx / batch_size
-
- return dx
复制代码 4.数据加载及主训练循环
数据加载- import numpy as np
- from collections import OrderedDict
- from src.layers import *
- from src.gradient import numerical_gradient
- class TwoLayerNet:
- """
- 两层神经网络
- 结构: Input -> Affine -> ReLU -> Affine -> SoftmaxWithLoss
- """
- def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
- """
- 初始化网络权重
- 参数:
- input_size: 输入层神经元数量
- hidden_size: 隐藏层神经元数量
- output_size: 输出层神经元数量
- weight_init_std: 权重初始化标准差
- """
- # 初始化权重
- self.params = {}
- self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
- self.params['b1'] = np.zeros(hidden_size)
- self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
- self.params['b2'] = np.zeros(output_size)
- # 生成层
- self.layers = OrderedDict()
- self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])
- self.layers['Relu1'] = Relu()
- self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2'])
- self.lastLayer = SoftmaxWithLoss()
-
- def predict(self, x):
- """
- 前向传播 (预测)
- """
- for layer in self.layers.values():
- x = layer.forward(x)
- return x
-
- def loss(self, x, t):
- """
- 计算损失函数值
-
- 参数:
- x: 输入数据
- t: 监督数据 (标签)
- """
- y = self.predict(x)
- return self.lastLayer.forward(y, t)
- def accuracy(self, x, t):
- """
- 计算精度
- """
- y = self.predict(x)
- y = np.argmax(y, axis=1)
- if t.ndim != 1 : t = np.argmax(t, axis=1)
-
- accuracy = np.sum(y == t) / float(x.shape[0])
- return accuracy
-
- def numerical_gradient(self, x, t):
- """
- 使用数值微分计算梯度 (速度较慢,用于验证)
- """
- loss_W = lambda W: self.loss(x, t)
-
- grads = {}
- grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
- grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
- grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
- grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
-
- return grads
-
- def gradient(self, x, t):
- """
- 使用误差反向传播法计算梯度 (速度快)
- """
- # 1. 前向传播
- self.loss(x, t)
- # 2. 反向传播
- dout = 1
- dout = self.lastLayer.backward(dout)
-
- layers = list(self.layers.values())
- layers.reverse()
- for layer in layers:
- dout = layer.backward(dout)
- # 3. 收集梯度
- grads = {}
- grads['W1'] = self.layers['Affine1'].dW
- grads['b1'] = self.layers['Affine1'].db
- grads['W2'] = self.layers['Affine2'].dW
- grads['b2'] = self.layers['Affine2'].db
- return grads
复制代码 }- import os
- import gzip
- import numpy as np
- import urllib.request
- # MNIST 数据集下载链接
- url_base = 'https://ossci- datasets.s3.amazonaws.com/mnist/'
- key_file = {
- 'train_img': 'train-images-idx3-ubyte.gz',
- 'train_label': 'train-labels-idx1-ubyte.gz',
- 'test_img': 't10k-images-idx3-ubyte.gz',
- 'test_label': 't10k-labels-idx1-ubyte.gz'
复制代码 主训练循环- dataset_dir = os.path.dirname(os.path.abspath(__file__))
- save_file = dataset_dir + "/mnist.pkl"
- train_num = 60000
- test_num = 10000
- img_dim = (1, 28, 28)
- img_size = 784
- def _download(file_name):
- file_path = dataset_dir + "/" + file_name
- if os.path.exists(file_path):
- return
- print("Downloading " + file_name + " ... ")
- # 使用 header 模拟浏览器,防止某些服务器拒绝
- headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'}
- req = urllib.request.Request(url_base + file_name, headers=headers)
- try:
- with urllib.request.urlopen(req) as response, open(file_path, 'wb') as out_file:
- data = response.read()
- out_file.write(data)
- print("Done")
- except Exception as e:
- print(f"Failed to download {file_name}: {e}")
- # 如果下载失败,尝试备用镜像或提示用户
- print("Please try to download manually and place in: ", dataset_dir)
- def download_mnist():
- for v in key_file.values():
- _download(v)
- def _load_label(file_name):
- file_path = dataset_dir + "/" + file_name
- print("Converting " + file_name + " to NumPy Array ...")
- with gzip.open(file_path, 'rb') as f:
- labels = np.frombuffer(f.read(), np.uint8, offset=8)
- print("Done")
- return labels
- def _load_img(file_name):
- file_path = dataset_dir + "/" + file_name
- print("Converting " + file_name + " to NumPy Array ...")
- with gzip.open(file_path, 'rb') as f:
- data = np.frombuffer(f.read(), np.uint8, offset=16)
- data = data.reshape(-1, img_size)
- print("Done")
- return data
- def _convert_numpy():
- dataset = {}
- dataset['train_img'] = _load_img(key_file['train_img'])
- dataset['train_label'] = _load_label(key_file['train_label'])
- dataset['test_img'] = _load_img(key_file['test_img'])
- dataset['test_label'] = _load_label(key_file['test_label'])
- return dataset
- def init_mnist():
- download_mnist()
- dataset = _convert_numpy()
- print("Creating pickle file ...")
- import pickle
- with open(save_file, 'wb') as f:
- pickle.dump(dataset, f, -1)
- print("Done!")
- def load_mnist(normalize=True, flatten=True, one_hot_label=False):
- """
- 读入 MNIST 数据集
- Parameters
- normalize : 将图像的像素值正规化为 0.0~1.0
- one_hot_label :
- False -> 7, 2, ...
- True -> [0,0,0,0,0,0,0,1,0,0], [0,0,1,0,0,0,0,0,0,0], ...
- flatten : 是否将图像展开为一维数组
- Returns
- (训练图像, 训练标签), (测试图像, 测试标签)
- """
- if not os.path.exists(save_file):
- init_mnist()
-
- import pickle
- with open(save_file, 'rb') as f:
- dataset = pickle.load(f)
- if normalize:
- for key in ('train_img', 'test_img'):
- dataset[key] = dataset[key].astype(np.float32)
- dataset[key] /= 255.0
-
- if one_hot_label:
- dataset['train_label'] = _change_one_hot_label(dataset['train_label'])
- dataset['test_label'] = _change_one_hot_label(dataset['test_label'])
- if not flatten:
- for key in ('train_img', 'test_img'):
- dataset[key] = dataset[key].reshape(-1, 1, 28, 28)
- return (dataset['train_img'], dataset['train_label']), (dataset['test_img'], dataset['test_label'])
- def _change_one_hot_label(X):
- T = np.zeros((X.size, 10))
- for idx, row in enumerate(T):
- row[X[idx]] = 1
-
- return T
复制代码 注:关于MNIST数据集在代码中使用:- import numpy as np
- import matplotlib.pyplot as plt
- from src.dataset import load_mnist
- from src.network import TwoLayerNet
- # 1. 读入数据
- print("正在加载 MNIST 数据集...")
- (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
- print(f"训练数据形状: {x_train.shape}")
- print(f"测试数据形状: {x_test.shape}")
- # 2. 超参数设置
- iters_num = 10000 # 适当设定循环的次数
- train_size = x_train.shape[0]
- batch_size = 100
- learning_rate = 0.1
- train_loss_list = []
- train_acc_list = []
- test_acc_list = []
- # 平均每个 epoch 的重复次数
- iter_per_epoch = max(train_size / batch_size, 1)
- # 3. 初始化网络
- # 输入层 784 (28x28), 隐藏层 50, 输出层 10 (0-9)
- network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
- print("开始训练...")
- for i in range(iters_num):
- # 获取 mini-batch
- batch_mask = np.random.choice(train_size, batch_size)
- x_batch = x_train[batch_mask]
- t_batch = t_train[batch_mask]
-
- # 计算梯度
- # 推荐: 使用反向传播 (Task 4 实现)
- grad = network.gradient(x_batch, t_batch)
- # 也可以使用数值梯度 (Task 3 实现),但速度非常慢,不建议在实际训练中使用
- # grad = network.numerical_gradient(x_batch, t_batch)
- # 更新参数 (SGD - Task 5)
- for key in ('W1', 'b1', 'W2', 'b2'):
- network.params[key] -= learning_rate * grad[key]
- # 记录学习过程
- loss = network.loss(x_batch, t_batch)
- train_loss_list.append(loss)
- # 计算每个 epoch 的识别精度
- if i % iter_per_epoch == 0:
- train_acc = network.accuracy(x_train, t_train)
- test_acc = network.accuracy(x_test, t_test)
- train_acc_list.append(train_acc)
- test_acc_list.append(test_acc)
- print(f"epoch: {int(i/iter_per_epoch)}, loss: {loss:.4f}, train acc: {train_acc:.4f}, test acc: {test_acc:.4f}")
- print("训练结束!")
- # 4. 绘图 (Task 6)
- # 绘制损失函数变化
- plt.figure(figsize=(12, 5))
- plt.subplot(1, 2, 1)
- plt.plot(train_loss_list)
- plt.title("Loss Function History")
- plt.xlabel("Iterations")
- plt.ylabel("Loss")
- # 绘制精度变化
- plt.subplot(1, 2, 2)
- markers = {'train': 'o', 'test': 's'}
- x = np.arange(len(train_acc_list))
- plt.plot(x, train_acc_list, label='train acc')
- plt.plot(x, test_acc_list, label='test acc', linestyle='--')
- plt.xlabel("Epochs")
- plt.ylabel("Accuracy")
- plt.ylim(0, 1.0)
- plt.legend(loc='lower right')
- plt.title("Accuracy History")
- plt.tight_layout()
- plt.savefig("training_result.png")
- print("结果已保存至 training_result.png")
- # plt.show()
复制代码 运行:1.安装依赖- from src.dataset import load_mnist
- # 第一次运行时会自动下载并生成 mnist.pkl 缓存文件
- (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, flatten=True, one_hot_label=True)
- print(x_train.shape) # (60000, 784)
- print(t_train.shape) # (60000, 10)
复制代码 2.运行训练- pip install -r requirements.txt
复制代码 3. 简单实验记录(loss 曲线或日志)
以上是该次学习的基本内容。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |