全连接神经网络

全连接:每一层的神经元都与上一层的所有神经元相连接

整体结构

分为输入层,隐藏层,输出层

隐藏层层数视任务而定:可以是1层,也可以是很多层

输出层可以有一个输出也可以是 多个输出

image-20250714162914765

单元结构

image-20250714163504336

数学表达式:
a=h(wTX+b)=h(w1x1+⋯+wnxn+b)h()就是激活函数,是一个非线性函数wTX+b就是前面的线性回归 a = h(w^TX+b)=h(w_1x_1+\dots+w_nx_n+b) \\ h()就是激活函数,是一个非线性函数 \\ w^TX+b就是前面的线性回归 a=h(wTX+b)=h(w1x1++wnxn+b)h()就是激活函数,是一个非线性函数wTX+b就是前面的线性回归
思考:全连接神经网络的单元结构就是

如果不经过激活函数,那就是线性回归
a=wTX+b a = w^TX+b a=wTX+b
如果激活函数是sigmoid函数
a=σ(wTX+b) a = \sigma(w^TX+b) a=σ(wTX+b)
那就变成逻辑回归了

激活函数

作用

引入了激活函数,网络才具有学习更加复杂关系能力的原因,由前面的思考得到,如果去掉了激活函数,那么单元结构就会变成一个线性回归模型,这样网络学习能力就会受限

为什么激活函数不是线性函数?

以一个小例子:数学表达式为a=h(x)=cx,单元结构如下

X->a11->a21->a31->y

有两个隐藏层
a11=cxa21=ca11=c2xa31=ca21=c3x a_{11}=cx \\ a_{21} = ca_{11}=c^2x \\ a_{31}=ca_{21}=c^3x a11=cxa21=ca11=c2xa31=ca21=c3x
我们发现,最终结果还是一个线性的,我们可以直接把单元结构改成

X->a31->y

数学表达式为
a=h(x)=kx=c3x a=h(x)=kx=c^3x a=h(x)=kx=c3x
那么两个隐藏层就被抵消掉了;隐藏层的作用是为了执行更复杂的计算任务,但使用线性函数作为激活函数,不仅浪费了计算资源还没起什么作用,等价于没有隐藏层的单元结构

分类

Linear

线性函数,相当于没有使用激活函数
y=g(z)=z=w⃗x⃗+b y = g(z) = z=\vec{w}\vec{x}+b y=g(z)=z=w x +b
适用于:回归模型,比如预测房价

python实现单隐藏层的前向传播

使用numpy手动实现

def g(w, x, b):
        return np.dot(w, x) + b


def dense(a_in, W, B, g):
        a_out = np.zeros(W.shape[0])
        for i in range(a_in.shape[0]):
                w = W[i, :]
                a_out[i] = g(w, a_in, B[i])
        return a_out

使用tensorflow

用到的库

import numpy as np
import tensorflow as tf
# 层模型:units:神经元数 activation:激活函数类别
from tensorflow.keras.layers import Dense
# 顺序模型
from tensorflow.keras.models import Sequential
# 特征 shape=(1,2)
X = np.array([[2, 1]])
# 标签 shape=(1,)
Y = np.array([7])

# 隐藏层,3个神经元,使用线性激活函数
layer1 = Dense(units=3, activation="linear")
layer2 = Dense(units=1, activation="linear")
model = Sequential(
        [
                # 显式声明,不推荐
                layer1, layer2
        ]
)

# 编译模型 loss使用均方误差MSE
model.compile(
        loss=tf.keras.metrics.mean_squared_error
)

# print(X.reshape(-1, 1))
# print(Y.reshape(-1, 1).shape)

# fit实现前向传播;其实fit的功能包括前向传播,反向传播,参数更新
model.fit(X, Y,epochs=1000)
#输出模型结构
model.summary()
# 输出预测值
print(model.predict(X))

image-20250718013244033

sigmoid函数

y=11+e−zy′=y(1−y) y = \frac{1}{1+e^{-z}} \\ y' = y(1-y) y=1+ez1y=y(1y)

绘制图像:

import numpy as np
import matplotlib.pyplot as plt

x_list = []
sig_list = []
d_sig_list = []


def sigmoid(x):
        return 1 / (1 + np.exp(-x))


def sigmoid_de(x):
        sig = sigmoid(x)
        return sig*(1 - sig)


for x in np.arange(-10, 11, 0.1):
        x_list.append(x)
        sigm = sigmoid(x)
        desig = sigmoid_de(x)
        sig_list.append(sigm)
        d_sig_list.append(desig)

plt.plot(x_list, sig_list,label="sigmoid")
plt.plot(x_list, d_sig_list,label="sigmoid'")
plt.xlabel("x")
plt.ylabel("y")
plt.legend()
plt.grid(True)
plt.show()

image-20250714173058770

分析:

sigmoid导数图像两侧都趋近于0,就导致偏离对称轴一定距离后,梯度值会变得很小,这就会导致梯度下降时参数更新的慢

优点:

1.简单,非常适用于二分类分类任务

缺点:

1.反向传播训练时有梯度消失的问题

(什么是反向传播?使用链式法则将损失函数对参数的梯度逐层回传,用于更新参数和偏置)

比如这样
w=w−α∂J∂wsigmoid导数的最大值是0.25,我们假设梯度就是0.25对于很多隐藏层的网络结构,反向传播时就会这样w=w−α(0.25∗0.25∗⋯∗0.25)梯度就变得很小很小,也就是梯度消失问题 w = w - \alpha\frac{\partial J}{\partial w} \\ sigmoid导数的最大值是0.25,我们假设梯度就是0.25 \\ 对于很多隐藏层的网络结构,反向传播时就会这样 \\ w = w-\alpha(0.25*0.25*\dots*0.25) \\ 梯度就变得很小很小,也就是梯度消失问题 w=wαwJsigmoid导数的最大值是0.25,我们假设梯度就是0.25对于很多隐藏层的网络结构,反向传播时就会这样w=wα(0.250.250.25)梯度就变得很小很小,也就是梯度消失问题
2.输出值区间为(0,1),关于原点不对称,会使参数更新的比较慢

(所以我们希望的激活函数是关于原点对称的,即奇函数f(-x) = -f(x))

3.梯度更新在不同方向走的太远(导数图像对称轴两侧),使优化难度增大,训练耗时

Tanh函数

双曲正切激活函数
y=ez−e−zez+e−zy′=1−y2 y = \frac{e^z-e^{-z}}{e^z+e^{-z}} \\ y' = 1-y^2 y=ez+ezezezy=1y2

import numpy as np
import matplotlib.pyplot as plt

w = 0.5
b = 0.1

x_list = []
y_list = []
de_y_list = []


def get_z(x, w, b):
        return w * x + b


def tanh(x, w, b):
        z = get_z(x, w, b)
        frac1 = np.exp(z) - np.exp(-z)
        frac2 = np.exp(z) + np.exp(-z)
        return frac1 / frac2


def de_tanh(y):
        return 1 - y ** 2


for x in np.arange(-10.0, 10.1, 0.1):
        x_list.append(x)
        y = tanh(x, w, b)
        y_list.append(y)
        de_y = de_tanh(y)
        de_y_list.append(de_y)

plt.plot(x_list,y_list,label="tanh")
plt.plot(x_list,de_y_list,label="de_tanh")

plt.legend()
plt.grid(True)
plt.show()

图像:

image-20250714180838803

分析:原函数关于原点对称了,原函数取值(-1,1);导数图像与sigmoid的导数类似,x趋近±∞时导数趋于0

优点:

1.解决了sigmoid不关于原点对称的问题,参数更新得更快

2.导数(梯度)最大值为1,因此训练的速度高于sigmoid

缺点:

1.仍存在梯度消失的问题

(尽管梯度最大值发生变化,但图像的形状仍于sigmoid类似)

2.还是和sigmoid很类似

ReLU函数

y={z,ifz>00,ifz≤0y′={1,ifz>00,ifz≤0 y=\begin{cases} z,& if& z >0 \\ 0,&if&z\leq0 \end{cases}\\ y'=\begin{cases} 1,&if&z>0\\ 0,&if&z\leq0 \end{cases} y={z,0,ififz>0z0y={1,0,ififz>0z0

图像

image-20250714231356613

优点:

1.解决了梯度消失的问题

2.没有指数运算,计算更为简单

缺点:

1.训练时可能出现神经元死亡的情况

(当z<0时,y=0,J(w)=1/mX^T(h(z)-y),那么J对w的梯度就是0了,此时参数更新就失效了)

2.y不关于零点对称,参数更新的比较慢

适用于:输出值y只能取非负值

Leaky ReLU函数

y={z,ifz>0az,ifz≤0y′={1,ifz>0a,ifz≤0其中0<α≪1(通常设为0.01) y=\begin{cases} z,& if& z >0 \\ az,&if&z\leq0 \end{cases}\\ y'=\begin{cases} 1,&if&z>0\\ a,&if&z\leq0 \\ \end{cases} 其中 0<α≪1(通常设为 0.01)\\ y={z,az,ififz>0z0y={1,a,ififz>0z0其中0<α1(通常设为0.01

图像

image-20250714231710622

绘图代码注意事项,使用子图对象设置y轴刻度

# 获得子图对象
fig, ax = plt.subplots()
ax.plot(z_list, y_list, label="y (Leaky ReLU)")
ax.plot(z_list, de_y_list, label="y' (Derivative)")

# 设置 y 轴刻度间隔为 0.5
ax.yaxis.set_ticks(np.arange(-2, 10.1, 0.5))

ax.set_xlabel("z")
ax.set_ylabel("Activation / Derivative")
ax.set_title("Leaky ReLU and its Derivative")
ax.legend()
ax.grid(True)
plt.show()

优点:

1.解决了ReLU神经元死亡问题

(输出值以及导数都不会变为0)

缺点:

1.无法为正负输入值提供一致的关系预测(不同区间函数不同)

  • 对于正输入,神经元“活跃”,直接将输入传递下去;
  • 对于负输入,神经元“较弱地活跃”,只传递一小部分信号(乘以 α);
SoftMax激活函数

用于多分类问题的输出层的激活函数

image-20250714235738492

image-20250718103450257
给定一个输入向量z=[z1,z2,…,zn],y=1,2,…,nzi=wi⃗x⃗+biai=SoftMax(zi)=ezi∑j=1nezj 给定一个输入向量z=[z_1,z_2,\dots,z_n] ,y=1,2,\dots,n\\ z_i = \vec{w_i}\vec{x}+b_i\\ a_i=SoftMax(z_i) = \frac{e^{z_i}}{\sum_{j=1}^{n}e^{z_j}} 给定一个输入向量z=[z1,z2,,zn],y=1,2,,nzi=wi x +biai=SoftMax(zi)=j=1nezjezi

如何选择

输出层

image-20250717160340236

  • 对于先前的预测房价之类的回归模型,激活函数用线性函数比较合适
  • 对于二分类任务,比如肿瘤诊断,Minist手写数字0/1,适合使用sigmoid
  • 对于输出值y非负的情况,使用ReLU
隐藏层

常用ReLU,当输出层是二分类任务时常用sigmoid

原因

  • 前面介绍激活函数时提到的,计算开销
  • ReLU梯度下降只会在一个方向收敛(y<0),sigmoid会在两个方向收敛(x -> -∞,x -> +∞);sigmoid这个特性会使损失函数J有许多梯度接近0的位置,不利于模型的梯度下降和参数更新

image-20250717161255810

前向传播

对于线性回归和逻辑回归来说,前向传播就是计算得到回归结果的过程

对于神经网络

image-20250715092215171

前向传播是指从输入层开始,依次经过隐藏层,最终到达输出层,逐层计算神经元输出值的过程,前向传播其实就是模型进行推理的过程

在这个过程中

  • 神经元会对输入进行带权求和加上偏置(wa+b)
  • 使用激活函数进行非线性变换

神经网络训练的步骤就分为:前向传播->计算误差->计算梯度->反向传播进行梯度更新

计算过程

eg:

image-20250715094743109

import numpy as np

# 权重
W = np.array([0.5, 1])
# 偏置
B = np.array([0.5, 1])
# 初始输入
x1 = 1
# 对应的标签
label = 1

# 带权求和 
def init_z(x, w, b):
        return w * x + b

# 隐藏层激活函数sigmoid
def sigmoid(a):
        return 1 / (1 + np.exp(-a))

# 输出层激活函数ReLU
def ReLU(a):
        if (a > 0):
                return a
        else:
                return 0


if __name__ == '__main__':
        a = x1
        w = W.copy()
        b = B.copy()
        for i in range(len(W)):
                z = init_z(a, w[i], b[i])
                if i < len(W) - 1:
                        a = sigmoid(z)
                elif i == len(W) - 1:
                        a = ReLU(z)
        output = a
        print(output)

损失函数

回顾:

线性回归模型中:均方误差损失函数:
J(w)=12m∑i=1m(wxi−yi)2 J(w) = \frac{1}{2m}\sum_{i=1}^{m}(wx_i-y_i)^2 J(w)=2m1i=1m(wxiyi)2
逻辑回归中:交叉熵损失函数:
J(w)=−1m∑i=1m(yilog(σ(wTxi+b))+(1−yi)log(1−σ(wTxi+b))) J(w)= -\frac{1}{m}\sum_{i=1}^m(y_ilog(\sigma(w^Tx_i+b))+(1-y_i)log(1-\sigma(w^Tx_i+b))) J(w)=m1i=1m(yilog(σ(wTxi+b))+(1yi)log(1σ(wTxi+b)))

链式法则

因为神经网络模型中可能存在着多层的隐藏层,当我们需要反向传播求梯度时,就涉及到对复合函数的求导,此时需要使用链式法则

单变量

y=f(u)u=g(v)v=h(x)dydx=dydududvdvdx y=f(u) \\ u = g(v) \\ v = h(x) \\ \frac{dy}{dx}=\frac{dy}{du}\frac{du}{dv}\frac{dv}{dx} y=f(u)u=g(v)v=h(x)dxdy=dudydvdudxdv

多变量

z=f(u,v)u=g(y)v=h(y)y=j(x) z = f(u,v)\\ u = g(y)\\ v = h(y) \\ y = j(x) z=f(u,v)u=g(y)v=h(y)y=j(x)

∂z∂x=∂z∂u∂u∂y∂y∂x+∂z∂v∂v∂y∂y∂x \frac{\partial z}{\partial x} = \frac{\partial z}{\partial u}\frac{\partial u}{\partial y}\frac{\partial y}{\partial x}+\frac{\partial z}{\partial v}\frac{\partial v}{\partial y}\frac{\partial y}{\partial x} xz=uzyuxy+vzyvxy

image-20250715103707051

反向传播

还是前面的例子,隐藏层激活函数为sigmoid,输出层激活函数为ReLU

image-20250715105630558

求J对w_21的梯度
∂J(w21,b)∂w21=∂J∂y∂y∂a21∂a21∂w21=∂12(y−label)2∂y∂a21∂a21∂ReLU(w21a11+b2)∂w21=(y−label)∂ReLU(w21a11+b2)∂w21a11+b2∂w21a11+b2∂w21=(y−label)×a11×∂ReLU(w21a11+b2)∂w21a11+b2 \frac{\partial J(w_{21},b)}{\partial w_{21}} = \frac{\partial J}{\partial y}\frac{\partial y}{\partial a_{21}}\frac{\partial a_{21}}{\partial w_{21}} = \frac{\partial \frac{1}{2}(y-label)^2}{\partial y}\frac{\partial a_{21}}{\partial a_{21}}\frac{\partial ReLU(w_{21}a_{11}+b_2)}{\partial w_{21}}=(y-label)\frac{\partial ReLU(w_{21}a_{11}+b_2)}{\partial w_{21}a_{11}+b_2}\frac{\partial w_{21}a_{11}+b_2}{\partial w_{21}} = (y-label)\times a_{11}\times \frac{\partial ReLU(w_{21}a_{11}+b_2)}{\partial w_{21}a_{11}+b_2} w21J(w21,b)=yJa21yw21a21=y21(ylabel)2a21a21w21ReLU(w21a11+b2)=(ylabel)w21a11+b2ReLU(w21a11+b2)w21w21a11+b2=(ylabel)×a11×w21a11+b2ReLU(w21a11+b2)
求J对b_2的梯度
∂J(w21,b2)∂b2=∂J(w21,b2)∂y∂y∂a21∂a21∂b2=∂12(y−label)2∂y∂y∂a21∂ReLU(w21a11+b2)∂w21a11+b2∂w21a11+b2∂b2 \frac{\partial J(w_{21},b_2)}{\partial b_2} = \frac{\partial J(w_{21},b_2)}{\partial y}\frac{\partial y}{\partial a_{21}}\frac{\partial a_{21}}{\partial b_2} = \frac{\partial \frac{1}{2}(y-label)^2}{\partial y}\frac{\partial y}{\partial a_{21}}\frac{\partial ReLU(w_{21}a_{11}+b_2)}{\partial w_{21}a_{11}+b_2}\frac{\partial w_{21}a_{11}+b_2}{\partial b_2} b2J(w21,b2)=yJ(w21,b2)a21yb2a21=y21(ylabel)2a21yw21a11+b2ReLU(w21a11+b2)b2w21a11+b2
求J对w_11的梯度
∂J(w11,b)∂w11=∂J∂y∂y∂a21∂a21∂a11∂a11∂w11=(y−label)×1×∂ReLU(w21a11+b2)∂w21a11+b2×∂w21a11+b2∂a11×∂σ(w11x1+b1)∂w11x1+b1×∂w11x1+b1∂w11 \frac{\partial J(w_{11},b)}{\partial w_{11}} = \frac{\partial J}{\partial y}\frac{\partial y}{\partial a_{21}}\frac{\partial a_{21}}{\partial a_{11}}\frac{\partial a_{11}}{\partial w_{11}} = (y-label)\times1\times\frac{\partial ReLU(w_{21}a_{11}+b_2)}{\partial w_{21}a_{11}+b_2}\times\frac{\partial w_{21}a_{11}+b_2}{\partial a_{11}}\times\frac{\partial \sigma(w_{11}x_1+b_1)}{\partial w_{11}x_1+b_1}\times\frac{\partial w_{11}x_1+b_1}{\partial w_{11}} w11J(w11,b)=yJa21ya11a21w11a11=(ylabel)×1×w21a11+b2ReLU(w21a11+b2)×a11w21a11+b2×w11x1+b1σ(w11x1+b1)×w11w11x1+b1
image-20250715113122746

经典的梯度更新
wj=wj=α∂J∂wbj=bj=α∂J∂b w_j = w_j = \alpha\frac{\partial J}{\partial w} \\ b_j = b_j = \alpha\frac{\partial J}{\partial b} wj=wj=αwJbj=bj=αbJ
代码实现

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(0)
# 3组2特征输入
X = np.array([
        [1, 2],
        [2, 3],
        [4, 6],
])
# 标签 
Y = np.array([
        [10],
        [11],
        [15]
])

# 输入层大小
input_size = X.shape[1]
# 神经元个数
hidden_size = 2
# 输出层大小
output_size = Y.shape[1]

# 输入层到隐藏层的权重矩阵,input_size*hidden_size是为了让输入的每个特征都与隐藏层中每个神经元有权重连接
W1 = np.random.randn(input_size, hidden_size)
# 隐藏层到输出层的权重矩阵,hidden_size*output_size是为了让隐藏层中每个神经元都与输出层的输出有权重连接
W2 = np.random.randn(hidden_size, output_size)
# W1*X.shape=(X.shape[0],hidden_size),B1.shape=(1,hidden_size)有利于广播对齐
B1 = np.zeros((1, hidden_size))
# 输出的格式是Y.shape,B2.shape=(1,output_size)有利于广播对齐以及运算
B2 = np.zeros((1, output_size))

learning_rate = 0.1
nums_epochs = 1000
test_epochs = 1000

error_list = []


def ReLU(x):
        # 将所有负值变为 0,正值保持不变
        return np.maximum(0, x)


def ReLU_de(x):
        # (x > 0):将列表转换为布尔列表;如果列表中元素大于0,该位置就是True;.astype(float) True->1.0 False—>0.0
        return (x > 0).astype(float)


def sigmoid(x):
        return 1 / (1 + np.exp(-x))


def sigmoid_de(x):
        y = sigmoid(x)
        return y * (1 - y)


def mse(Y_pred, Y):
        m = len(Y_pred)
        return np.sum((Y_pred - Y) ** 2) / (2 * m)


m = X.shape[0]
for i in range(nums_epochs):
        # 前向传播
        z = np.dot(X, W1) + B1
        a1 = sigmoid(z)
        # print(a1.shape)
        z2 = np.dot(a1, W2) + B2
        a2 = ReLU(z2)
        # print(a2.shape)
        # 反向传播
        # mse
        MSE = mse(a2, Y)
        error_list.append(MSE)

        # J对Y的偏导
        pJpY = (a2 - Y) / m
        # Y对a2的偏导
        pYpa2 = 1
        # a2对w2a1+b2的偏导
        dR = ReLU_de(z2)
        # w2a1+b2对w2的偏导
        pa2pw2 = a1

        # a1.shape=(3,2) (a2-Y).shape = dR.shape=(3,1),而dw2的shape要和W2相同,即(2,1)

        delta2 = pJpY * pYpa2 * dR
        dw2 = np.dot(pa2pw2.T, delta2)

        # a2对a1的偏导
        pa2pa1 = W2
        # a1对w1X+b1的偏导
        dS = sigmoid_de(z)
        # delta*X
        # (3,1) (2,1) (3,2) (3,2)
        delta1 = np.dot(delta2, W2.T) * dS
        dw1 = np.dot(X.T, delta1)

        W1 -= learning_rate * dw1
        W2 -= learning_rate * dw2

plt.plot(error_list, label="Learning curve")
plt.xlabel("Iteration")
plt.ylabel("Loss")
plt.legend()
plt.grid(True)
plt.show()

z1_test = np.dot(X, W1) + B1
a1_test = sigmoid(z1_test)
z2_test = np.dot(a1_test, W2) + B2
a2_test = ReLU(z2_test)
print("预测结果\n", a2_test)
print("实际结果\n", Y)

学习曲线

image-20250715153801884

输出结果

预测结果
 [[ 9.22571926]
 [11.27514153]
 [13.36276318]]
实际结果
 [[10]
 [11]
 [15]]

多类

目标标签超过两个的分类任务,输出标签可以是两个中的一个,也可以是多个类别中的任意一个类别

与二分类的图像对比

SoftMax的损失函数

逻辑回归中:交叉熵损失函数:
J(w)=−1m∑i=1m(yilog(σ(wTxi+b))+(1−yi)log(1−σ(wTxi+b))) J(w)= -\frac{1}{m}\sum_{i=1}^m(y_ilog(\sigma(w^Tx_i+b))+(1-y_i)log(1-\sigma(w^Tx_i+b))) J(w)=m1i=1m(yilog(σ(wTxi+b))+(1yi)log(1σ(wTxi+b)))
将逻辑回归中推出的交叉熵损失函数进行推广以应用到SoftMax上
P(y∣X)=σ(wTxi+b)y[1−σ(wTxi+b)](1−y)a1=P(1∣X)=σ(wTxi+b)a2=1−a1=P(0∣X)=1−σ(wTxi+b)loss=−ylog(a1)−(1−y)log(1−a1)1是真实类别时:loss=−log(a1)0是真实类别时:loss=−log(a2)我们使用一种叫做one−hot编码的方式,将y,(1−y)统一成0或1,只有真实的标签才是1,此时我们可以将公式变为:loss=−∑i=1myilog(ai)J(w,b)=−1m∑i=1myilog(ai) P(y|X) = \sigma(w^Tx_i+b)^y[1-\sigma(w^Tx_i+b)]^{(1-y)}\\ a_1=P(1|X)=\sigma(w^Tx_i+b) \\ a_2 = 1-a_1= P(0|X) = 1-\sigma(w^Tx_i+b) \\ loss = -ylog(a_1)-(1-y)log(1-a_1) \\ 1是真实类别时:loss =-log(a1)\\ 0是真实类别时:loss =-log(a2)\\ 我们使用一种叫做one-hot编码的方式,将y,(1-y)统一成0或1,只有真实的标签才是1,此时我们可以将公式变为: \\ loss = -\sum_{i=1}^{m}y_ilog(a_i) \\ J(w,b) = -\frac{1}{m}\sum_{i=1}^{m}y_ilog(a_i) P(yX)=σ(wTxi+b)y[1σ(wTxi+b)](1y)a1=P(1∣X)=σ(wTxi+b)a2=1a1=P(0∣X)=1σ(wTxi+b)loss=ylog(a1)(1y)log(1a1)1是真实类别时:loss=log(a1)0是真实类别时:loss=log(a2)我们使用一种叫做onehot编码的方式,y,(1y)统一成01,只有真实的标签才是1,此时我们可以将公式变为:loss=i=1myilog(ai)J(w,b)=m1i=1myilog(ai)

one-hot编码

one-hot编码是用于将**类别型数据(类别标签等)**转换为二进制向量的编码方法;对于有N个类别的分类任务,每个类别会被表示为一个长度为N的二进制向量,只有一个位置为1,其他位置都为0
在分类任务中,真实类别是第i类,则yj={1ifj=i,0其他 在分类任务中,真实类别是第i类,则 \\ y_j = \begin{cases} 1&if&j=i,\\ 0&其他 \end{cases} 在分类任务中,真实类别是第i类,则yj={10if其他j=i,
比如2分类中,我们假设y=1是正例,y=2是反例
二进制向量=(1,0)loss=−∑i=1Nyilog(ai)=−log(a1)+0×(−log(a2)) 二进制向量=(1,0)\\ loss = -\sum_{i=1}^{N}y_ilog(a_i) = -log(a_1)+0\times(-log(a_2)) 二进制向量=(1,0)loss=i=1Nyilog(ai)=log(a1)+0×(log(a2))

SoftMax的损失函数

1.使用one-hot编码+交叉熵简化形式
loss=−∑i=1myilog(ai)loss=−log(aj),当y=j其中yi是one−hot编码后二进制向量第i个元素的值;aj是SoftMax计算得到的概率 loss = -\sum_{i=1}^{m}y_ilog(a_i)\\ loss = -log(a_j),当y=j\\ 其中y_i是one-hot编码后二进制向量第i个元素的值;a_j是SoftMax计算得到的概率 loss=i=1myilog(ai)loss=log(aj),当y=j其中yionehot编码后二进制向量第i个元素的值;ajSoftMax计算得到的概率
分析:对于损失loss,每个训练样例的y都只能取一个值;当a_j越小,-log(a_j)的值会越大,因此模型会激励a_j变大,尽可能接近1

SoftMax实现输出层

image-20250718121106530
zj[l]=wj[l]⃗a⃗+bj[i]aj[l]=ezj[l]∑i=1unitseei[l]units:神经元数 z_j^{[l]} = \vec{w_j^{[l]}}\vec{a}+b_j^{[i]} \\ a_j^{[l]} = \frac{e^{z_j^{[l]}}}{\sum_{i=1}^{units}e^{e_i^{[l]}}}\\ units:神经元数 zj[l]=wj[l] a +bj[i]aj[l]=i=1unitseei[l]ezj[l]units:神经元数

减少数字舍入误差

在计算机中,存储浮点数的精度是有限的

比如:

image-20250718141811604

image-20250718141832602

发现在数学上计算相等的两个算式,在代码中却出现了运行结果不相等

tensorflow可以对这种情况做出优化

逻辑回归例子

以逻辑回归的loss值为例,我们看看数学上等价的两个公式

a=g(w⃗x⃗+b)=g(z)=11+e−zloss=−∑i=1m(ylog(a)+(1−y)log(1−a)) a = g(\vec{w}\vec{x}+b) = g(z) = \frac{1}{1+e^{-z}} \\ loss = - \sum_{i=1}^{m}(ylog(a)+(1-y)log(1-a))\\ a=g(w x +b)=g(z)=1+ez1loss=i=1m(ylog(a)+(1y)log(1a))
对应的代码

model1 = Sequential([
        Dense(units=10, activation=relu),
        Dense(units=5, activation=relu),
        Dense(units=1, activation=sigmoid),
])

# 将模型损失值用激活函数计算后,再传入损失函数中计算
model1.compile(loss=BinaryCrossentropy())

loss=−∑i=1m(ylog(11+e−z)+(1−y)log(1−11+e−z)) loss = -\sum_{i=1}^{m}(ylog(\frac{1}{1+e^{-z}})+(1-y)log(1-\frac{1}{1+e^{-z}})) loss=i=1m(ylog(1+ez1)+(1y)log(11+ez1))

model2 = Sequential([
        Dense(units=10, activation=relu),
        Dense(units=5, activation=relu),
        Dense(units=1, activation=linear),  # 直接输出线性拟合的结果,在计算损失函数处再进行激活
])
# 将BinaryCrossentropy当作方法调用(使用了python中的__call__,类似于PHP的__invoke)
# logits是模型输出的原始输出值(未经过激活函数),from_logits=True会将原始输出直接扔进损失函数中再应用激活函数,最终计算出损失
model2.compile(loss=BinaryCrossentropy(from_logits=True))
model2.fit(X, Y, epochs=500)

# 前向传播值
logits = model2(X)
predict = tf.nn.sigmoid(logits)

整体对比代码

import numpy as np
from keras.activations import relu, sigmoid, linear
from keras.losses import BinaryCrossentropy
from tensorflow.keras.layers import Dense
from tensorflow.keras import Sequential

X = np.array([
        [1., 2.],
        [3., 4.],
        [5., 6.],
])

# 逻辑回归2分类任务
Y = np.array([0, 1, 1])

model1 = Sequential([
        Dense(units=10, activation=relu),
        Dense(units=5, activation=relu),
        Dense(units=1, activation=sigmoid),
])

# 将模型损失值用激活函数计算后,再传入损失函数中计算
model1.compile(loss=BinaryCrossentropy())

model1.fit(X, Y, epochs=500)

model2 = Sequential([
        Dense(units=10, activation=relu),
        Dense(units=5, activation=relu),
        Dense(units=1, activation=linear),  # 直接输出线性拟合的结果,在计算损失函数处再进行激活
])
# 将BinaryCrossentropy当作方法调用(使用了python中的__call__,类似于PHP的__invoke)
# logits是模型输出的原始输出值(未经过激活函数),from_logits=True会将原始输出直接扔进损失函数中再应用激活函数,最终计算出损失
model2.compile(loss=BinaryCrossentropy(from_logits=True))
model2.fit(X, Y, epochs=500)

# 使用evaluate获得编译时设置的loss模型的计算值
loss1 = model1.evaluate(X, Y)
loss2 = model2.evaluate(X, Y)

print("loss1:", loss1)
print("loss2:", loss2)

运行结果

image-20250718150444667

python知识:

  • __call__:将对象实例作为方法调用,与PHP的__invoke很类似

    class test:
            def __init__(self):
                    print("__init")
    
            def __call__(self, X):
                    print(f"call:{X}")
    
    t = test()
    t(1)
    

    image-20250718145950712

<?php
class test{
        public $a;

        public function __invoke()
        {
                echo "__invoke";
        }
}

$t = new test();
$t();

image-20250718150152508

对SoftMax进行优化

原本的公式
a⃗=(a1,a2,…,a10)=g(z1,z2,…,z10)Loss=L(a⃗,y)={−log(a1)ify=1,⋮−log(a10)ify=10 \vec{a} =(a_1,a_2,\dots,a_{10}) = g(z_1,z_2,\dots,z_{10}) \\ Loss = L(\vec{a},y) = \begin{cases} -log(a_1)&if&y=1,\\ \vdots\\ -log(a_{10})&if&y=10\\ \end{cases} a =(a1,a2,,a10)=g(z1,z2,,z10)Loss=L(a ,y)= log(a1)log(a10)ifify=1,y=10

model1 = Sequential([
        Dense(units=10, activation=relu),
        Dense(units=5, activation=relu),
        Dense(units=10, activation=softmax),
])

# 将模型损失值用激活函数计算后,再传入损失函数中计算
# 使用稀疏类别交叉熵SparseCategoricalCrossentropy
model1.compile(loss=SparseCategoricalCrossentropy())
model1.fit(X, Y, epochs=500)

# 获得前向结果值
predict1 = model1(X)
print(predict1)

image-20250718154427469

将激活函数这一步直接放在计算损失中
Loss=L(a⃗,y)={−log(ez1ez1+ez2+⋯+ez10)ify=1,⋮−log(ez10ez1+ez2+⋯+ez10)ify=10 Loss = L(\vec{a},y) = \begin{cases} -log(\frac{e^{z_1}}{e^{z_1}+e^{z_2}+\dots+e^{z_{10}}})&if&y=1,\\ \vdots\\ -log(\frac{e^{z_{10}}}{e^{z_1}+e^{z_2}+\dots+e^{z_{10}}})&if&y=10\\ \end{cases} Loss=L(a ,y)= log(ez1+ez2++ez10ez1)log(ez1+ez2++ez10ez10)ifify=1,y=10

model2 = Sequential([
        Dense(units=10, activation=relu),
        Dense(units=5, activation=relu),
        Dense(units=10, activation=linear),  # 直接输出线性拟合的结果,在计算损失函数处再进行激活
])
# 将SparseCategoricalCrossentropy当作方法调用
# logits是模型输出的原始输出值(未经过激活函数),from_logits=True会将原始输出直接扔进损失函数中再应用激活函数,最终计算出损失
model2.compile(loss=SparseCategoricalCrossentropy(from_logits=True))
model2.fit(X, Y, epochs=500)

#预测
# 获取最后一层输出的向量,输出z1->z10而不是a1->a10
# 将model当作方法获得前向传播结果与.predict()有什么区别?:model(X):X:tf.Tensor return:tf.Tensor .predict(X): X:numpy,tensor,dataset return:numpy.ndarray
logits = model2(X)
# 获取最终的概率分布
predict = tf.nn.softmax(logits)

什么是稀疏交叉熵损失函数(SparseCategoricalCrossentropy)?

  • 适用于这样的多分类任务:标签是整数(表示编号),模型输出是概率分布(像softmax这样的输出)

损失函数
L(Py)=−log(Py)Py:真实标签对应的案例比如,标签向量是[0,1,2],模型输出的概率分布是[0.6,0.2,0.2],真实标签是0,那损失函数就是−log(0.6) L(P_y) = -log(P_y) \\ P_y:真实标签对应的案例 \\ 比如,标签向量是[0,1,2],模型输出的概率分布是[0.6,0.2,0.2],真实标签是0,那损失函数就是-log(0.6) L(Py)=log(Py)Py:真实标签对应的案例比如,标签向量是[0,1,2],模型输出的概率分布是[0.6,0.2,0.2],真实标签是0,那损失函数就是log(0.6)
SoftMax的一个特点就是使输出两极化,体现为

  • 正样本趋近1,负样本趋近0
  • 样本绝对值越大,两极化越明显
import numpy as np
import matplotlib.pyplot as plt


def softmax(x):
        exp_x = np.exp(x)
        sum = np.sum(exp_x)
        return exp_x / sum


X = np.array([-3, -1, 0, 3, 5])
Y = softmax(X)

# [ ... for val in Y ]这个操作表示对Y中元素进行...操作后放入新列表
print([f"{val:.4f}" for val in Y])

fig, ax = plt.subplots()
ax.plot(X, Y)
# 设置坐标轴精度
ax.yaxis.set_ticks(np.arange(0, 1, 0.05))

plt.xlabel("x")
plt.ylabel("SoftMax")
plt.legend()
plt.grid(True)
plt.show()

输出结果

['0.0003', '0.0022', '0.0059', '0.1182', '0.8734']

图像

image-20250720102939142

存在的问题

  • 对于很大的输入(大到->+∞),分子的会变得非常非常大,大到变成inf,而分母同样也会变成inf,softmax计算出的值就不确定了(inf / inf 最终结果是nan),这就是上溢
  • 对于很小的输入(小到->-∞),分子->0,导致最终的结果被四舍五入为0,这就是下溢
优化1

思路:控制输入向量中x_i的大小;在进行幂指数运算时先减去向量中的最大值,这样一来,输入x_i-max(x)的大小范围就在(-∞,0]
SoftMax(xi)=exi−xamx∑j=1mexj SoftMax(x_i) = \frac{e^{x_i-x_{amx}}}{\sum_{j=1}^{m}e^{x_j}} SoftMax(xi)=j=1mexjexixamx
输入特征X

X = np.array([-3, -1, 0, 3, 1000])

使用原始的softmax(上文的softmax),输出结果

exp_x [ 0.04978707  0.36787944  1.         20.08553692         inf]
sum inf
['0.0000', '0.0000', '0.0000', '0.0000', 'nan']

使用优化后的softmax:

def softmax(x):
        # np.max用于获取列表中最大值
        # 与np.maximum区别:np.maximum用于比较两个列表X,Y,返回列表是X,Y中较大的那个元素,形状不同会通过numpy的广播机制进行对齐,无法对齐就会报错
        exp_x = np.exp(x - np.max(x))
        print("exp_x", exp_x)
        sum = np.sum(exp_x)
        print("sum", sum)
        return exp_x / sum

运行结果

exp_x [0. 0. 0. 0. 1.]
sum 1.0
['0.0000', '0.0000', '0.0000', '0.0000', '1.0000']

这个优化后的softmax解决了上溢问题,但是并没有解决下溢的问题,如果最大值太大,会导致有很多结果丢失精度变为0

优化2

这个算法被称为log_softmax,将上面优化1的公式取对数
SoftMax(xi)=logexi−xmax∑j=1mexj=log(exi−xmax)−log(∑j=1mexj)=xi−xmax−log(∑i=1mexj) SoftMax(x_i) = log\frac{e^{x_i-x_{max}}}{\sum_{j=1}^{m}e^{x_j}} = log(e^{x_i-x_{max}})-log(\sum_{j=1}^{m}e^{x_j})=x_i-x_{max}-log(\sum_{i=1}^{m}e^{x_j}) SoftMax(xi)=logj=1mexjexixmax=log(exixmax)log(j=1mexj)=xixmaxlog(i=1mexj)

多标签分类问题

是一种与每个图像相关联的,有多个输出标签的分类问题

image-20250720103137385

区别:输出标签向量y中不只有一个真实类别;多分类问题,即使有多个输入特征,最终输出的真实类别也只有一个

也就是说,输出向量中元素是不互斥的,可以不只有一个1

如何构建神经网络

方法1:把三个标签分类分别当作三个神经网络(不推荐)

image-20250720103901581

方法2:训练一个神经网络同时检测三种情况

image-20250720103959544

可以视作三个二分类问题,因此激活函数可以使用sigmoid

高级优化方法

Adam算法(自适应向量估计)

自动调整学习率以实现更高效地梯度下降;adam算法为模型的每个参数使用不同的学习率

image-20250720105236473

直观理解:如果参数wj和b似乎一直在大致相同的方向上移动,那么学习率太小,adam算法会增大该参数的学习率;如果一个参数来回震荡,那么学习率太大,adam算法会减小该参数的学习率

在代码中使用

编译模型时指定优化项

# 定义密集层
model = Sequential([
        Dense(units=25, activation=sigmoid),
        Dense(units=15, activation=sigmoid),
        Dense(units=5, activation=linear)
])

# 编译
model.compile(
        loss=SparseCategoricalCrossentropy(from_logits=True),
        # 指定优化器为adam,初始学习率为0.001
        optimizer=keras.optimizers.Adam(learning_rate=1e-3)
)

优点:可以自动调整学习率,让算法整体更具有稳健性(鲁棒性)

Logo

GitCode 天启AI是一款由 GitCode 团队打造的智能助手,基于先进的LLM(大语言模型)与多智能体 Agent 技术构建,致力于为用户提供高效、智能、多模态的创作与开发支持。它不仅支持自然语言对话,还具备处理文件、生成 PPT、撰写分析报告、开发 Web 应用等多项能力,真正做到“一句话,让 Al帮你完成复杂任务”。

更多推荐