深度学习入门

从梯度下降到神经网络学习

本次学习通过对《深度学习入门:基于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

组装网络

复制代码
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

4.数据加载及主训练循环

数据加载

复制代码
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

主训练循环

复制代码
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() 

注:关于MNIST数据集在代码中使用:

复制代码
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)

运行:1.安装依赖

复制代码
pip install -r requirements.txt

2.运行训练

复制代码
python main.py

3. 简单实验记录(loss 曲线或日志)

以上是该次学习的基本内容。