一.理论部分

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

已知量:训练数据集,包括输入特征x和映射对应的“标签”y。

未知量:权重(W)和偏置(b)

学习目标:通过调整未知量,使模型预测的结果与真实标签尽可能一致,为此确定一个最理想的能够描述数据分布的函数关系。

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

       单层感知机只能处理线性可分问题,这是由它的数学本质决定的,我们可以通过或门的例子来清晰说明:

       以或门为例,当权重参数为(b,w1,w2)=(−0.5,1.0,1.0)时,其输出规则为:

       ​这里的−0.5+x1​+x2​=0是一个线性方程,在二维输入空间中对应一条直线,这条直线就是感知机的决策边界。它把输入空间分成了两个部分。在感知机的结果中,就是一部分0的空间和一部分1的空间。同时,从这个表达式看出来,它的数学本质就是一个线性函数,自然它能处理的就是线性问题。

       再者,如果两类样本可以被一个线性超平面完全分开,就称它们是线性可分的。从这一感知机结果的角度看,它也只能处理线性问题。

       如果想处理非线性问题,就需要叠加多层感知机(即神经网络)。通过叠加多层感知机,可以组合出复杂的非线性决策边界,从而解决更复杂的问题。

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

       在神经网络中使用非线性激活函数,其目的在于保证神经网络的普遍适用性,使其可以任意拟合各种函数。如果使用线性激活函数,那么就只能拟合线性函数情况,不适用复杂情况。为了更深刻理解,我们可以举个例子:

我们可以设置一个三层神经网络:  

我们经过数学计算可以推导出y的值:

       这还是关于x1和x2的一个线性函数,本质上我们可以通过改变权重和偏置来使隐藏层消失,无论我们堆叠多少层,它依旧是一个单层感知机。就像我们在上一道题目中得出来的结论,它还是只能处理线性问题,而现实世界中大多数系统则是非线性的,所以,为了普适性,我们使用的是非线性激活函数。

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

输入层到隐藏层:线性:矩阵乘法和加法

                非线性:非线性函数变换

隐藏层到隐藏层:  线性:矩阵乘法和加法

                非线性:非线性函数变换

隐藏层到输出层:线性:矩阵乘法和加法,如果是回归任务就是恒等函数变换

                非线性:如果是分类问题就是softmax

Q5. 为什么分类问题中,输出层通常使用 Softmax?

Softmax 在概率意义上做了什么?

       从上面这个过程看,softmax回归完全可以看为一个具有多个输出的单层神经网络,这使得它可以同时识别所有类别,因此它别广泛用在分类问题上,而且它并不会改变所有类别间的大小关系,它只会为每个类别分配相应的概率。

       神经网络最后一层算出来的数,它是无规律的,可能正可能负可能特别大可能特别小,通过softmax函数,它会将这些数变成一个在零到一闭区间的正数,为后续统计概率做铺垫。

       概率统计的核心规则是,所有可能的结果的概率和为1,而softmax函数具有归一化的特征,Softmax第二步会把第一步得到的所有正数加起来,再让每个正数除以这个总和,加起来总和就是1,符合概率的基本规则。

       最后,从数学公式上看,softmax的数学表达式中有指数,它会让结果间的差异更大,是统计后的结果更清晰。

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

首先,从数学的角度看,准确率可以用下列这个式子表示:

       这显然是一个阶跃函数,只有当参数调整变化到足以改变某个样本的预测类别时,准确率才会发生阶跃式的跳跃,而只对参数作微小调整时候,只要预测的类别没有改变,准确率的数值就完全不变,并且,最重要的一点,这种函数在绝大位置处的导数都是0.

       神经网络的训练依赖梯度下降法,其中梯度的计算需要目标函数是连续可导的。如果用准确率为目标,导数几乎处处为零,学习过程几乎无法进行。

       而损失函数(如均方误差、交叉熵误差)是连续可导的光滑函数:即使是参数的微小调整没有改变预测类别,也会改变预测概率,从而导致损失值发生微小变化,最后,因为这种变化是可导的,能提供非零的梯度,从而引导参数持续优化。

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

       由全部变量的偏导数汇总 而成的向量称为梯度(gradient)。

       梯度的几何意义:方向:梯度的方向是函数在该点函数值增长最快的方向。

       大小:梯度的模长(长度)代表了函数在该方向上的增长速率。

       神经网络训练的目标是最小化损失函数,即找到函数的最小值点。梯度是函数值上升最快的方向,那么它的反方向就是函数值下降最快的方向。所以沿着负梯度方向更新参数,能以最快的速度降低损失值,是寻找函数最小值的最高效方法。

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

       学习率决定在一次学习中,应该学习多少,以及在多大程度上更新参数。它平衡了 “优化速度” 和 “收敛精度”,是训练神经网络的关键超参数。

       学习率过大的话,参数更新会在最小值点附近剧烈震荡,甚至直接越过最小值,导致损失值越来越大,最终发散到无穷大。

       学习率过小的话,参数更新幅度微乎其微,即使迭代很多次,也几乎停留在初始位置左右,无法有效接近最小值。

二.手写神经网络与手写数字识别(实践)

任务 1:激活函数 使用 numpy 实现:Sigmoid,ReLU,Softmax

import numpy as np
import matplotlib.pyplot as plt
def sigmoid(x):    
    return 1/(1+np.exp(-x))
def relu(x):    
    return np.maximum(0, x)
def softmax(a):
    exp_a = np.exp(a)
    sum_exp_a = np.sum(exp_a)
    y = exp_a / sum_exp_a    
    return y

接下来我们看具体图像:

 # 绘制激活函数图像
x=np.linspace(-10, 10, 1000)
y_sigmoid=sigmoid(x)
y_relu=relu(x)

plt.figure(figsize=(12, 5))

# Sigmoid
plt.subplot(1, 2, 1)
plt.plot(x, y_sigmoid,linewidth=2.5,label='Sigmoid')
plt.title('Sigmoid激活函数')
plt.xlabel('x')
plt.ylabel('Sigmoid(x)')
plt.grid(True, alpha=0.3)
plt.legend()

# ReLU
plt.subplot(1, 2, 2)
plt.plot(x, y_relu,linewidth=2.5,label='ReLU')
plt.title('ReLU激活函数')
plt.xlabel('x')
plt.ylabel('ReLU(x)')
plt.grid(True,alpha=0.3)
plt.legend()

plt.tight_layout()
plt.show()

#softmax
z = np.array([[2.0, 1.0, 0.1, -1.0, 0.5, 3.0, 1.5, -0.5, 0.8, 1.2]])
# 计算Softmax概率
y = np.exp(z) / np.sum(np.exp(z), axis=1, keepdims=True)

# 绘制Softmax概率分布柱状图
plt.figure(figsize=(10, 5))
categories = [str(i) for i in range(10)]  # 数字0-9
plt.bar(categories, y[0],alpha=0.7)
plt.title('Softmax输出:手写数字0-9的概率分布', fontsize=12)
plt.xlabel('数字类别',fontsize=10)
plt.ylabel('Softmax概率(和为1)',fontsize=10)
plt.ylim(0, 1)
plt.grid(axis='y',alpha=0.3)
# 标注每个类别的概率值
for i, prob in enumerate(y[0]):
    plt.text(i, prob+0.02, f'{prob:.3f}',ha='center',fontsize=9)
plt.show()

任务 2:损失函数 实现:交叉熵损失(支持 batch 输入)

#任务2:损失函数实现
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

下面我们可以给一个具体的案例来说明:

# 模拟batch_size=3的预测结果
y = np.array([[0.1, 0.8, 0.1],[0.3, 0.1, 0.6],[0.9, 0.05, 0.05]])
# 对应的真实标签
t = np.array([[0, 1, 0],[0, 0, 1],[1, 0, 0]])
# 计算batch的交叉熵损失
loss1=cross_entropy_error(y,t)
print(f"预测概率:{y}")
print(f"真实One-hot标签:{t}")
print(f"Batch损失值:{loss1:.4f}\n")  

任务 3:数值梯度

#任务3:数值梯度
def numerical_gradient(f, x):
    h=1e-4 # 0.0001
    grad=np.zeros_like(x) 
    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
#以上适用于一维,下面给出多维
def numerical_gradient(f, x):
    h = 1e-4
    grad = np.zeros_like(x)
    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        tmp_val = x[idx]
        x[idx] = tmp_val + h
        fxh1 = f(x)
        x[idx] = tmp_val - h
        fxh2 = f(x)
        grad[idx] = (fxh1 - fxh2) / (2 * h)
        x[idx] = tmp_val
        it.iternext()
    return grad

任务 4:实现一个神经网络

#任务4:实现一个神经网络
class TwoLayerNet:
    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=sigmoid(a1)
        a2=np.dot(z1, W2) + b2
        y=softmax(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)
        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

任务 5:模型训练

import sys
sys.path.append("D:/桌面/deep-learning-from-scratch-master")
from dataset.mnist import load_mnist
 #任务5:模型训练
# 加载数据集
(x_train,t_train), (x_test,t_test)=load_mnist(normalize=True,one_hot_label=True)

network=TwoLayerNet(input_size=784,hidden_size=50,output_size=10)

iters_num=20
train_size=x_train.shape[0]
batch_size=10
learning_rate=0.1

train_loss_list=[]
train_acc_list=[]
test_acc_list=[]

iter_per_epoch=max(train_size/batch_size,1)

for i in range(iters_num):
    batch_mask=np.random.choice(train_size,batch_size)
    x_batch=x_train[batch_mask]
    t_batch=t_train[batch_mask]
    
    grad=network.numerical_gradient(x_batch,t_batch)
    
    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)
    if i % 2 == 0 and len(train_loss_list) > 0:
        plt.plot(train_loss_list)
        plt.show()
    
    if i % 5==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"train acc, test acc|{train_acc:.4f},{test_acc:.4f}")

# 绘制损失函数变化
plt.plot(train_loss_list)
plt.xlabel("Iteration")
plt.ylabel("Loss")
plt.show()

# 绘制准确率变化
plt.plot(train_acc_list,label='train acc')
plt.plot(test_acc_list,label='test acc')
plt.xlabel("Epoch")
plt.ylabel("Accuracy")
plt.legend()
plt.show()

为了看到即时结果,使用了较小数字。

更多推荐