理论知识

Q1. 神经网络训练过程中,哪些量是已知的,哪些量是未知的?学习的目标到底是什么?

A1.已知:训练数据(输入和标签),测试数据(对模型来说x已知y不已知),损失函数;未知:神经网路的权重和偏置。
学习的目标是让机器从训练数据中总结规律,找到一组权重和偏置,让损失函数的值更小,模型的预测结果更符合真实值。

Q2. 为什么单层感知机只能解决线性可分问题?

A2.因为单层感知机的公式为y=h(b+w1x1+w2y2),训练时不断调整w和b,所以b+w1x1+w2y2部分为一条直线,只能解决线性可分问题。

Q3. 为什么必须引入非线性激活函数? 如果把神经网络中所有激活函数都去掉,会发生什么?

A3.如果不引用非线性激活函数,那么神经网络不管层数有多少,多层线性都会有与之等价的一层线性。

Q4. 神经网络的前向传播,本质上在做什么数学运算?

A4.是在做激活函数和线性函数的组合运算。

Q5. 为什么分类问题中,输出层通常使用 Softmax? Softmax 在概率意义上做了什么?

A5.因为分类问题如果没有softmax,得到的结果将是一堆实数,不方便人们观察。而softmax将这些有正有负有大有小的实数都转化为对应的概率,且这些概率相加为1,更直观。

Q6. 为什么“准确率”不能作为训练时的优化目标? 为什么必须引入损失函数?

A6.因为如果使用准确率作为指标,在权重改动较小时大多数地方的导数都会变为0,无法知道更新方向。而引入损失函数后权重的改动都会带来损失函数导数的变化,从而能改动使其值变小,进而间接提高准确率。

Q7. 梯度在几何意义上代表什么? 为什么沿着负梯度方向更新参数?

A7.梯度是一个向量,指向函数在该点上升最快的方向,而其大小是此时的变化率。所以负梯度方向就是函数下降最快的方向。沿着负梯度方向更新参数是为了损失函数下降,从而提升准确率。

Q8. 学习率在梯度下降中起什么作用? 学习率过大会怎样? 过小又会怎样?

A8.学习率决定了在一次学习中,应该学习多少,以及在多大程度上更新参数,过小会使训练低效,更新缓慢,过大会使训练太发散,可能更新过头,不够稳定。

手写神经网络与手写数字实践

激活函数

import numpy as np

#常用于二分类问题输出层,压缩输出值为0到1
def sigmoid(x):
    return 1/(1+np.exp(-x))

#常用于神经网路的隐藏层,环节梯度消失问题
def relu(x):
    return np.maxinum(0,x)

#常用于多分类且类别相互排斥问题的隐藏层,压缩输出值为0到1
def softmax(x):
    c=np.max(a)
    exp_a=np.exp(a-c)
    sum_exp_p=np.sum(exp_a)
    y=exp_a/sum_exp_a
    return y

损失函数

#交叉熵误差
#t为独热编码数组
#单组数据
def cross_entropy_error(y,t):
    delta = 1e-7
    return -np.sum(t*np.log(y + delta))

#支持batch输入
def cross_entropy_error(y,t):
    if y.ndim == 1:
        t = t.reshape(1,t.size)
        y = y.reshape(1,y.size)
    batch_size = y.shape[0]
    return -np.sum(t*np.log(y + 1e-7))/batch_size

#t为标签形式
def cross_entropy_error(y,t):
    if y.ndim == 1
        t = t.reshape(1,t.size)
        y = y.reshape(1,y.size)
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arrange(batch_size),t]+1e-7))/batch_size

数值梯度

def numerical_gradient(f, x):#x为Numpy数组,f为函数
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x) # 生成和x形状相同的所有元素为0的数组
    for idx in range(x.size):
        tmp_val = x[idx]
        # f(x+h)的计算
        x[idx] = tmp_val + h
        fxh1 = f(x)
        
        # f(x-h)的计算
        x[idx] = tmp_val - h
        fxh2 = f(x)
        
        grad[idx] = (fxh1 - fxh2) / (2*h)
        x[idx] = tmp_val # 还原值
	return grad
#这样生成的就是所有x变量的偏导数的数组了

实现神经网络

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import fetch_openml

#下载数据
mnist = fetch_openml('mnist_784',version=1,as_frame=False)
#切分数据
x, y = mnist.data, mnist.target.astype(np.uint8)
x_train, x_test = x[:5000], x[2000:]
y_train, y_test = y[:5000], y[2000:]
#数据归一化
x_train = x_train.astype(np.float32) / 255.0
x_test = x_test.astype(np.float32) / 255.0


print(f'训练集图像形状: {x_train.shape}, 标签数量: {len(y_train)}')
print(f'测试集图像形状: {x_test.shape}, 标签数量: {len(y_test)}')

#生成梯度
def numerical_gradient(f,x):
    h = 1e-4
    x_flat = x.flatten()
    grad_flat = np.zeros_like(x_flat)
    for idx in range(x_flat.size):
        tmp_val = x_flat[idx]
        x_flat[idx] = tmp_val + h
        x_new = x_flat.reshape(x.shape)  # 还原为原形状
        fxh1 = f(x_new)
        x_flat[idx] = tmp_val - h
        x_new = x_flat.reshape(x.shape)
        fxh2 = f(x_new)
        grad_flat[idx] = (fxh1 - fxh2)/(2*h)
        x_flat[idx] = tmp_val

        grad = grad_flat.reshape(x.shape)
    return grad

#梯度下降法
def gradient_descent(f,init_x,lr=0.01,step_num=100):
    x = init_x
    for i in range(step_num):
        grad = numerical_gradient(f,x)
        x -= lr*grad
    return x

#激活函数
def relu(x):
    return np.maximum(0,x)

def softmax(a):
    c = np.max(a,axis=-1, keepdims=True)
    exp_a = np.exp(a-c)
    sum_exp_a = np.sum(exp_a,axis=-1, keepdims=True)
    y = exp_a / sum_exp_a
    return y

#损失函数
def cross_entropy_error(y,t):
    if y.ndim == 1:
        t = t.reshape(1,t.size)
        y = y.reshape(1,y.size)
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size),t] + 1e-7))/batch_size

class netWork:
    #初始化权重和偏置参数
    def __init__(self,input_size,hidden_size,output_size,weight_init_std=0.01):
        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)
    
    #前向传播,返回预测概率
    def predict(self,x):
        w1,w2 = self.params['w1'],self.params['w2']
        b1,b2 = self.params['b1'],self.params['b2']
        
        self.a1 = np.dot(x,w1) + b1
        self.z1 = relu(self.a1)

        self.a2 = np.dot(self.z1,w2) + b2

        y = softmax(self.a2)
        return y
    
    #计算损失
    def loss(self,x,t):
        y = self.predict(x)
        return cross_entropy_error(y,t)
    
    #计算精度
    def accuracy(self,x,t):
        y = self.predict(x)
        y = np.argmax(y, 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 train(self,x_train,t_train,x_test,t_test,lr=0.01,epochs=10,batch_size=100):
        train_loss_list = []
        train_acc_list = []
        test_acc_list = []

        iter_per_epoch = max(len(x_train)/batch_size,1)#记录每个epoch迭代次数
        print(f"\n开始训练,共 {epochs} 个epoch,每个epoch {iter_per_epoch} 个batch\n")
        for epoch in range(epochs):
            #打乱数据
            indices = np.random.permutation(len(x_train))
            x_train_shuffled = x_train[indices]
            t_train_shuffled = t_train[indices]

            for i in range(0,len(x_train),batch_size):
                x_batch = x_train_shuffled[i:i+batch_size]
                t_batch = t_train_shuffled[i:i+batch_size]

                grad = self.numerical_gradient(x_batch,t_batch)

                for j in ('w1','b1','w2','b2'):
                    self.params[j] -= lr*grad[j]

                loss = self.loss(x_batch,t_batch)
                train_loss_list.append(loss)

            #计算精度
            train_acc = self.accuracy(x_train, t_train)
            test_acc = self.accuracy(x_test, t_test)
            train_acc_list.append(train_acc)
            test_acc_list.append(test_acc)
            print(f"Epoch [{epoch + 1}/{train_epochs}] | "
                f"损失: {loss:.4f} | "
                f"训练准确率: {train_acc:.4f} | "
                f"测试准确率: {test_acc:.4f}")
        return {
            'loss': train_loss_list,
            'train_acc': train_acc_list,
            'test_acc': test_acc_list
        }


#实例化神经网络    
net = netWork(input_size=784,hidden_size=50, output_size=10)

#训练网络
history = net.train(x_train, y_train, x_test, y_test, lr=0.1, epochs=10, batch_size=100)

# 绘图
fig, axes = plt.subplots(1, 2, figsize=(12, 4))

# 损失曲线
axes[0].plot(history['loss'])
axes[0].set_xlabel('Iterations')
axes[0].set_ylabel('Loss')
axes[0].set_title('Training Loss')

# 精度曲线
epochs = range(1, len(history['train_acc']) + 1)
axes[1].plot(epochs, history['train_acc'], label='Train Accuracy')
axes[1].plot(epochs, history['test_acc'], label='Test Accuracy')
axes[1].set_xlabel('Epochs')
axes[1].set_ylabel('Accuracy')
axes[1].set_title('Accuracy over Epochs')
axes[1].legend()

plt.tight_layout()
plt.show()

print(f"最终测试集精度: {history['test_acc'][-1]:.4f}")

运行时间过长

修改使用反向传播版本

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import fetch_openml

# 下载数据
mnist = fetch_openml('mnist_784', version=1, as_frame=False)
# 切分数据
x, y = mnist.data, mnist.target.astype(np.uint8)
x_train, x_test = x[:5000], x[5000:6000] 
y_train, y_test = y[:5000], y[5000:6000]
# 数据归一化
x_train = x_train.astype(np.float32) / 255.0
x_test = x_test.astype(np.float32) / 255.0

print(f'训练集图像形状: {x_train.shape}, 标签数量: {len(y_train)}')
print(f'测试集图像形状: {x_test.shape}, 标签数量: {len(y_test)}')

# 激活函数
def relu(x):
    return np.maximum(0, x)

# ReLU的导数(反向传播用)
def relu_grad(x):
    grad = np.zeros_like(x)
    grad[x > 0] = 1  # x>0时导数为1,x<=0时为0
    return grad

def softmax(a):
    c = np.max(a, axis=-1, keepdims=True)  # 防止指数爆炸
    exp_a = np.exp(a - c)
    sum_exp_a = np.sum(exp_a, axis=-1, keepdims=True)
    y = exp_a / sum_exp_a
    return y

# 损失函数
def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

class NetWork:
    # 初始化权重和偏置参数
    def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
        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)
    
    # 前向传播(保存中间值,供反向传播使用)
    def predict(self, x):
        w1, w2 = self.params['w1'], self.params['w2']
        b1, b2 = self.params['b1'], self.params['b2']
        
        # 前向传播计算
        a1 = np.dot(x, w1) + b1
        z1 = relu(a1)
        a2 = np.dot(z1, w2) + b2
        y = softmax(a2)
        
        # 保存中间值
        self.x = x
        self.a1 = a1
        self.z1 = z1
        self.a2 = a2
        
        return y
    
    # 计算损失
    def loss(self, x, t):
        y = self.predict(x)
        return cross_entropy_error(y, t)
    
    # 计算精度
    def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)  # 取概率最大的类别
        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy
    
    # 反向传播计算梯度
    def gradient(self, x, t):
        # 先执行前向传播,确保中间值已保存
        self.loss(x, t)
        batch_size = x.shape[0]
        
        # 反向传播:输出层
        dy = (self.predict(x) - np.eye(10)[t]) / batch_size  # 损失对a2的梯度
        dw2 = np.dot(self.z1.T, dy)                         # w2的梯度
        db2 = np.sum(dy, axis=0)                            # b2的梯度
        
        # 反向传播:隐藏层
        da1 = np.dot(dy, self.params['w2'].T)               # 损失对a1的梯度
        dz1 = da1 * relu_grad(self.a1)                      # 损失对z1的梯度(ReLU导数)
        dw1 = np.dot(self.x.T, dz1)                         # w1的梯度
        db1 = np.sum(dz1, axis=0)                            # b1的梯度
        
        # 整理梯度
        grads = {
            'w1': dw1,
            'b1': db1,
            'w2': dw2,
            'b2': db2
        }
        return grads
    
    # 训练函数
    def train(self, x_train, t_train, x_test, t_test, lr=0.01, epochs=10, batch_size=100):
        train_loss_list = []
        train_acc_list = []
        test_acc_list = []

        iter_per_epoch = max(len(x_train) // batch_size, 1)  # 修正:整除避免浮点数
        print(f"\n开始训练,共 {epochs} 个epoch,每个epoch {iter_per_epoch} 个batch\n")
        
        for epoch in range(epochs):
            # 打乱训练数据
            indices = np.random.permutation(len(x_train))
            x_train_shuffled = x_train[indices]
            t_train_shuffled = t_train[indices]

            for i in range(0, len(x_train), batch_size):
                # 取小批量数据
                x_batch = x_train_shuffled[i:i+batch_size]
                t_batch = t_train_shuffled[i:i+batch_size]

                grad = self.gradient(x_batch, t_batch)

                for j in ('w1', 'b1', 'w2', 'b2'):
                    self.params[j] -= lr * grad[j]

                # 记录当前batch的损失
                loss = self.loss(x_batch, t_batch)
                train_loss_list.append(loss)

            # 计算当前epoch的精度并打印(修正变量名错误)
            train_acc = self.accuracy(x_train, t_train)
            test_acc = self.accuracy(x_test, t_test)
            train_acc_list.append(train_acc)
            test_acc_list.append(test_acc)
            print(f"Epoch [{epoch + 1}/{epochs}] | "
                  f"损失: {loss:.4f} | "
                  f"训练准确率: {train_acc:.4f} | "
                  f"测试准确率: {test_acc:.4f}")
        
        return {
            'loss': train_loss_list,
            'train_acc': train_acc_list,
            'test_acc': test_acc_list
        }

# 实例化神经网络    
net = NetWork(input_size=784, hidden_size=50, output_size=10)

# 训练网络(反向传播版本,速度提升1000倍)
history = net.train(x_train, y_train, x_test, y_test, lr=0.1, epochs=10, batch_size=100)

# 绘图
fig, axes = plt.subplots(1, 2, figsize=(12, 4))

# 损失曲线
axes[0].plot(history['loss'])
axes[0].set_xlabel('Iterations')
axes[0].set_ylabel('Loss')
axes[0].set_title('Training Loss')

# 精度曲线
epochs_range = range(1, len(history['train_acc']) + 1)
axes[1].plot(epochs_range, history['train_acc'], label='Train Accuracy')
axes[1].plot(epochs_range, history['test_acc'], label='Test Accuracy')
axes[1].set_xlabel('Epochs')
axes[1].set_ylabel('Accuracy')
axes[1].set_title('Accuracy over Epochs')
axes[1].legend()

plt.tight_layout()
plt.show()

print(f"最终测试集精度: {history['test_acc'][-1]:.4f}")

实验记录

更多推荐