思维导图
image-20250722193343693

神经网络

与传统机器学习算法(线性回归,逻辑回归等)在不同数据量下解决问题的表现区别:

image-20250714154236813

特点:弱化了特征工程,传统的机器学习算法(线性回归,逻辑回归,TODO:决策树…)严重依赖人工对特征的选取和构造(比如前面的特征工程),而神经网络通过多层的非线性变化和反向传播更新参数,能够自动学习到对任务有用的高阶抽象特征

神经元

通过一个预测商品是否畅销的案例来引入

image-20250714155529664

神经元将价格x作为输入,通过激活函数a将输入x转换为输出,即是否畅销的可能性

也可以将神经元理解成小型计算机,唯一的工作就是输入一个或几个数字,经过激活函数a后输出一个或几个数字

更复杂的案例

此时商品拥有4个特征:价格,运费,营销,材料

但是判断是否畅销取决于3个因素:实惠性,人们的意识,人们对质量的预测

image-20250714161430034

实惠性,人们的意识,人们对质量的预测,可能性都是神经元的激活值

工作流程

image-20250716094424421
对于隐藏层来说,为什么叫隐藏层呢?

神经网络的结构和工作流程对我们来说,输入的特征向量X是透明的,最终模型输出的y是透明的,虽然可以提取激活向量,但是我们并不能知道激活向量是经过什么变换得到的,也就是说隐藏层的工作模式其实对我们不透明

这也是为什么神经网络又称为黑箱模型(只知道输入和输出,但是难以解释从输入如何得到输出)

输入层(也称为第0层)到隐藏层

image-20250716100426096
$$
\vec{a^{[i]}}:第i层输出的向量
$$

隐藏层到输出层

image-20250716102529252

更复杂的结构

image-20250716103054151

第L层的第J个元素/单元
$$
a^{[L]}_{J} = h(w^{[L]}\vec{a^{[L-1]}}+b_J) \
也适用于输入层的X,X可以表示为\vec{a^{[0]}}
$$

前向传播

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

在这个过程中

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

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

eg:手写识别0/1,使用8*8的像素网格,255表示亮白

image-20250716104753202

整体结构

image-20250716104853845

过程:

输入层到第一层隐藏层
$$
\vec{a^{[1]}} =
\begin{pmatrix}
h(w^{[1]}1\vec{a^{[0]}}+b_1^{[1]}) \
h(w^{[1]}2\vec{a^{[0]}}+b_2^{[1]}) \
\vdots \
h(w^{[1]}
{25}\vec{a^{[0]}}+b
{25}^{[1]})
\end{pmatrix}
$$
第一层隐藏层到第二层隐藏层
$$
\vec{a^{[2]}} =
\begin{pmatrix}
h(w^{[2]}1\vec{a^{[1]}}+b_1^{[2]}) \
h(w^{[2]}2\vec{a^{[1]}}+b_2^{[2]}) \
\vdots \
h(w^{[2]}
{15}\vec{a^{[1]}}+b
{15}^{[2]})
\end{pmatrix}
$$

到输出层
$$
\vec{a^{[3]}} =
\begin{pmatrix}
h(w^{[3]}_1\vec{a^{[2]}}+b_1^{[3]})
\end{pmatrix} \
a^{[3]}变成标量了
$$
案例神经网络结构特点:越靠近输出层,隐藏层中神经元数量越少

代码实现:

数据集导入,使用sklearn.datasets中的load_digits

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import numpy as np
# 引入手写数字数据集
from sklearn.datasets import load_digits


def get_imgdata(num):
# 生成一个偏移列表[0,10,20,30];获取的数据集前30个都是按照0-9的顺序排序的
rdm_idx_arr = np.arange(0, 31, 10)
# 随机选择0 10 20 30
idx = np.random.randint(0, 4)

# 获取到的8*8数据
digit = load_digits()
# 对矩阵初步处理,符合要求中的手写数字外的部分的值是255
res = (digit.images[num+rdm_idx_arr[idx]] + 255) % 256
# 返回图像的8*8矩阵形式
return res

获取特征与标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def getXY(nums=10):
# 特征X,shape=(nums,8,8),nums个样本
X = np.zeros((nums, 8, 8))
# 标签Y
Y = np.zeros(nums)
for i in range(nums):
# 随机生成0/1
n = np.random.randint(0, 2)
# 获取图像矩阵
img = get_imgdata(n)
X[i] = img
Y[i] = n

return X, Y


X, Y = getXY()

顺序模型

1
2
3
4
5
6
model = Sequential([
Flatten(input_shape=(8, 8)), # 将 (8,8) 展平为 (64,),否则传入隐藏层时仍以(8,8)的形式,最终经过输出层时形状为(num,8,1),与Y的形状不匹配
Dense(units=25, activation="relu"),
Dense(units=15, activation="relu"),
Dense(units=1, activation="sigmoid")
])

进行模型编译、拟合和获取预测值

1
2
3
4
5
6
7
# 损失函数模型为交叉熵损失BinaryCrossentropy
model.compile(loss="BinaryCrossentropy")
# 拟合100次
model.fit(X, Y, epochs=100)

# Y的预测值
Y_pred = model.predict(X)

整体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import numpy as np
from keras.layers import Flatten
from tensorflow.keras.layers import Dense
from tensorflow.keras import Sequential
from digits import get_imgdata


def getXY(nums=10):
# 特征X,shape=(nums,8,8),nums个样本
X = np.zeros((nums, 8, 8))
# 标签Y
Y = np.zeros(nums)
for i in range(nums):
# 随机生成0/1
n = np.random.randint(0, 2)
# 获取图像矩阵
img = get_imgdata(n)
X[i] = img
Y[i] = n

return X, Y


X, Y = getXY()

model = Sequential([
Flatten(input_shape=(8, 8)), # 将 (8,8) 展平为 (64,),否则传入隐藏层时仍以(8,8)的形式,最终经过输出层时形状为(num,8,1),与Y的形状不匹配
Dense(units=25, activation="relu"),
Dense(units=15, activation="relu"),
Dense(units=1, activation="sigmoid")
])

# 损失函数模型为交叉熵损失BinaryCrossentropy
model.compile(loss="BinaryCrossentropy")
# 拟合100次
model.fit(X, Y, epochs=100)

# Y的预测值
Y_pred = model.predict(X)

print((Y_pred > 0.8).astype(int))
print(Y)

# 通过model.evaluate获取根据编译阶段设置的loss模型计算得到的loss值
loss = model.evaluate(X, Y)
print(f"Loss: {loss}")

模型运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
预测值 [[0]
[0]
[1]
[0]
[0]
[0]
[0]
[1]
[0]
[0]]
标签值 [0. 0. 1. 0. 0. 0. 0. 1. 0. 0.]
1/1 [==============================] - 0s 108ms/step - loss: 3.2461e-06
Loss: 3.246126652811654e-06

课上的案例

烘焙咖啡

image-20250716155333700

对输入矩阵进行硬编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import numpy as np


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


X = np.array([200.0, 17.0])

W1_1 = np.array([2.0, 3.6])
W1_2 = np.array([5.0, 4.6])
W1_3 = np.array([1.0, 5.6])

b1_1 = np.array([-4])
b1_2 = np.array([2])
b1_3 = np.array([3])

a1_1 = sigmoid(np.dot(W1_1, X) + b1_1)
a1_2 = sigmoid(np.dot(W1_2, X) + b1_2)
a1_3 = sigmoid(np.dot(W1_3, X) + b1_3)

a2_1 = np.array([a1_1, a1_2, a1_3])
W2_1 = np.array([1.14, 5.14, 1.9])
b2_1 = np.array([6])

a2_1 = sigmoid(np.dot(W2_1, a2_1) + b2_1)
print(a2_1)

更通用的做法:

层中计算激活向量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import numpy as np

X = np.array([200.0, 17.0])
# 3个神经元
W = np.array([
[2.0, 3.6],
[5.0, 4.6],
[1.0, 5.6]
])

B = np.array([-1.3, 2.6, 3.9])


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


def dense(a_in, W, b, g):
'''

:param a_in: 输入的激活向量
:param W: 每个神经元的权重压缩到一起
:param b: 偏置
:param g: 激活函数
:return: 输出该层的激活向量
'''

m = W.shape[0]
a_out = np.zeros(m)

for i in range(m):
# 对W进行分片,每次取出一行
w = W[i, :]
a_out[i] = g(np.dot(w, a_in) + b[i])

return a_out

实现顺序模型

1
2
3
4
5
6
7
8
9
10
11
def Sequential(X, g):
'''
:param X: 输入向量
:param g: 激活函数
:return:
'''

a1 = dense(X, 3 * np.random.rand(5, len(X)), 5 * np.random.rand(5), g)
a2 = dense(a1, 3 * np.random.rand(3, len(a1)), 5 * np.random.rand(3), g)
a3 = dense(a2, 3 * np.random.rand(1, len(a2)), 5 * np.random.rand(1), g)
return a3

使用tensorflow进行代码实现,这里要注意tf和numpy对矩阵的不同表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import numpy as np
import tensorflow as tf
# 用于定义全连接层
from tensorflow.keras.layers import Dense

# 1个含有2个样本的输入a^[0]
a0 = np.array([[200.0, 17.0]])
# 3个神经元的隐藏层
layer1 = Dense(units=3, activation="sigmoid")
# 一个神经元的输出层
layer2 = Dense(units=1, activation="sigmoid")

# 激活向量a1
a1 = layer1(a0)
# 输出向量a2
a2 = layer2(a1)

# 对a2进行变换,等价与numpy中的(a2>0.2).astype(int)
a3 = tf.cast(a2 > 0.5, dtype=tf.int32)
print(a3)

# 也可以转换成numpy格式再进行变换,tf提供了.numpy()来将tf中的张量tensor转换成numpy形式的矩阵
a2 = a2.numpy()
a3 = (a2 > 0.5).astype(int)
print(a3)

可以使用顺序模型进行优化,这样我们就不用手动将向量传递给下一层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import numpy as np
import tensorflow as ts
from tensorflow.keras.layers import Dense
from tensorflow.keras.models import Sequential

X = np.array([
[200.0, 17.0],
[120.0, 5.0],
[425.0, 20.0],
[212.0, 18.0]
])
Y = np.array([
[1],
[0],
[0],
[1]
])

'''通常不进行显式声明'''
# layer_1 = Dense(units=5, activation="sigmoid")
# layer_2 = Dense(units=1, activation="sigmoid")

# 构建顺序模型,模型中的层按顺序堆叠
model = Sequential([
Dense(units=5, activation="sigmoid"),
Dense(units=1, activation="sigmoid")
])
# 进行编译
model.compile(
loss='binary_crossentropy', # 二分类任务
optimizer='sgd', # 使用随机梯度下降
metrics=['accuracy'] # 监控准确率
)
# 进行拟合
model.fit(X, Y, epochs=100)
# 前向传播
y_pred = model.predict(X)

print(y_pred)

向量化

对于上一个例子的dense,X是1×2矩阵,W是3×2矩阵,B是1×3矩阵

那么可以用
$$
XW^T+B
$$
代替循环,实现向量化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 要将原本的向量变成列/行矩阵
X1 = np.array([[200.0, 17.0]])
# 3个神经元
W1 = np.array([
[2.0, 3.6],
[5.0, 4.6],
[1.0, 5.6]
])

B1 = np.array([[-1.3, 2.6, 3.9]])

def dense2(a_in, W, b, g):
# np.matmul()用来计算矩阵乘法,等价于@运算符
return g(np.matmul(a_in,W.T)+b)

image-20250717105611629

矩阵乘法

线性代数知识
$$
对于向量\vec{a} = \begin{pmatrix}
a_1,\
a_2,\
\vdots,\
a_n\
\end{pmatrix}{n\times1}\vec{b}=\begin{pmatrix}b_1,\b_2,\\dots,\b_n\\end{pmatrix}{n\times1}\
\vec{a}\cdot \vec{b}=a^T\cdot b=a_1b_1+a_2b_2+\dots+a_nb_n \
向量\vec{a} = \begin{pmatrix}
a_1,\
a_2,\
\vdots,\
a_n\
\end{pmatrix},矩阵W_{n\times m} = \begin{pmatrix}\vec{w_1},\vec{w_2},\dots,\vec{w_m}\end{pmatrix}\
Z= a^TW = \begin{pmatrix}a^T\vec{w_1},a^T\vec{w_2},\dots,a^T\vec{w_m}\end{pmatrix}\
矩阵A_{n\times m},B_{m \times a} \
A\cdot B = C_{n \times a}
$$

全连接神经网络

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

整体结构

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

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

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

image-20250714162914765

单元结构

image-20250714163504336

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

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

激活函数

作用

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

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

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

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

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

1
X->a31->y

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

分类

Linear

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

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

使用numpy手动实现

1
2
3
4
5
6
7
8
9
10
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

用到的库

1
2
3
4
5
6
import numpy as np
import tensorflow as tf
# 层模型:units:神经元数 activation:激活函数类别
from tensorflow.keras.layers import Dense
# 顺序模型
from tensorflow.keras.models import Sequential
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 特征 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 = \frac{1}{1+e^{-z}} \
y’ = y(1-y)
$$

绘制图像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 - \alpha\frac{\partial J}{\partial w} \
sigmoid导数的最大值是0.25,我们假设梯度就是0.25 \
对于很多隐藏层的网络结构,反向传播时就会这样 \
w = w-\alpha(0.250.25\dots*0.25) \
梯度就变得很小很小,也就是梯度消失问题
$$
2.输出值区间为(0,1),关于原点不对称,会使参数更新的比较慢

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

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

Tanh函数

双曲正切激活函数
$$
y = \frac{e^z-e^{-z}}{e^z+e^{-z}} \
y’ = 1-y^2
$$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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=\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}
$$

图像

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=\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)\
$$

图像

image-20250714231710622

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 获得子图对象
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=[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}}
$$

如何选择

输出层

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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) = \frac{1}{2m}\sum_{i=1}^{m}(wx_i-y_i)^2
$$
逻辑回归中:交叉熵损失函数:
$$
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)))
$$

链式法则

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

单变量

$$
y=f(u) \
u = g(v) \
v = h(x) \
\frac{dy}{dx}=\frac{dy}{du}\frac{du}{dv}\frac{dv}{dx}
$$

多变量

$$
z = f(u,v)\
u = g(y)\
v = h(y) \
y = j(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}
$$

image-20250715103707051

反向传播

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

image-20250715105630558

求J对w_21的梯度
$$
\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}
$$
求J对b_2的梯度
$$
\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}
$$
求J对w_11的梯度
$$
\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}}
$$
image-20250715113122746

经典的梯度更新
$$
w_j = w_j = \alpha\frac{\partial J}{\partial w} \
b_j = b_j = \alpha\frac{\partial J}{\partial b}
$$
代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
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

输出结果

1
2
3
4
5
6
7
8
预测结果
[[ 9.22571926]
[11.27514153]
[13.36276318]]
实际结果
[[10]
[11]
[15]]

多类

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

与二分类的图像对比

SoftMax的损失函数

逻辑回归中:交叉熵损失函数:
$$
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)))
$$
将逻辑回归中推出的交叉熵损失函数进行推广以应用到SoftMax上
$$
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)
$$

one-hot编码

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

SoftMax的损失函数

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

SoftMax实现输出层

image-20250718121106530
$$
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:神经元数
$$

减少数字舍入误差

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

比如:

image-20250718141811604

image-20250718141832602

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

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

逻辑回归例子

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

$$
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))\
$$
对应的代码

1
2
3
4
5
6
7
8
model1 = Sequential([
Dense(units=10, activation=relu),
Dense(units=5, activation=relu),
Dense(units=1, activation=sigmoid),
])

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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)

整体对比代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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很类似

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class test:
    def __init__(self):
    print("__init")

    def __call__(self, X):
    print(f"call:{X}")

    t = test()
    t(1)

    image-20250718145950712

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class test{
public $a;

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

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

image-20250718150152508

对SoftMax进行优化

原本的公式
$$
\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}
$$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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(\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}
$$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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(P_y) = -log(P_y) \
P_y:真实标签对应的案例 \
比如,标签向量是[0,1,2],模型输出的概率分布是[0.6,0.2,0.2],真实标签是0,那损失函数就是-log(0.6)
$$
SoftMax的一个特点就是使输出两极化,体现为

  • 正样本趋近1,负样本趋近0
  • 样本绝对值越大,两极化越明显
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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()

输出结果

1
['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(x_i) = \frac{e^{x_i-x_{amx}}}{\sum_{j=1}^{m}e^{x_j}}
$$
输入特征X

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

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

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

使用优化后的softmax:

1
2
3
4
5
6
7
8
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

运行结果

1
2
3
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(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})
$$

多标签分类问题

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

image-20250720103137385

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

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

如何构建神经网络

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

image-20250720103901581

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

image-20250720103959544

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

高级优化方法

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

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

image-20250720105236473

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

在代码中使用

编译模型时指定优化项

1
2
3
4
5
6
7
8
9
10
11
12
13
# 定义密集层
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)
)

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

卷积神经网络CNN

全连接神经网络存在的问题:

  • 输入的形式应该是列向量,但是卷积神经网络中的输入是图像(2D矩阵),那么就需要对图片进行展平处理,原本图像中蕴含的空间等信息就被打乱了
  • 输入的特征多了,那么神经元的参数就会很多,层的结构就很复杂,容易过拟合

通道数

通道数 是指图像或特征图在深度方向上的维度数量(可以类比全连接神经网络中的特征矩阵的x_i的特征数)

例如:

  • 灰度图只有一个通道(特征):亮度
  • RGB图像:3个通道(红,绿,蓝)
  • RGBA图像:4个通道(红,绿,蓝,透明度)

图像在计算机中的本质

一个大的数值矩阵,每个元素代表像素的亮度或颜色

对于灰度图(只有黑白两色):255表示亮白,0表示黑,只有一个通道

image-20250720152235550

对于彩色图像

三通道图

image-20250720153053484

整体结构

image-20250720154104648

卷积层

还是与手写数字为例;在密集层中,每个神经元都可以获取到整个的像素矩阵;而在卷积层中,我们让神经元只能获取到一部分像素而不是全部,这个特性被称为局部感知

局部感知的优点
  • 计算更快(输入的矩阵更小,那么对于神经元来说,参数的数量大大减小)
  • 需要更少的训练数据,更不容易过拟合(参数减少,模型复杂度降低,就不容易过拟合)

卷积层会改变通道的数目,在前一层的输入提取出不同的特征,同时会改变图片的长和宽

卷积核/滤波器

定义:一个在图像上滑动的小窗口,它与图像的局部区域进行点积运算,从而提取出该区域的特征。其实可以类比全连接神经网络的参数w,只不过它是共享的

卷积核是一个4D张量,形状为(C_out,C_in,K,K)

  • C_out:输出通道数/卷积核数量
  • C_in:输入通道数
  • K:卷积核大小(K×K)

案例:心电图信号分类

image-20250720120256155

池化层

池化层可以改变图像的大小,但不改变通道数

卷积运算

image-20250720162847525

对应位置相乘再相加

带偏置的计算过程

image-20250720163246832

权重共享

对于全连接神经网络,输入中的每个特征都对应着一个神经元中的一个参数

而对于卷积神经网络并非这样

卷积神经网络中,是同一个卷积核在整个输入图像上滑动,对所有局部区域使用相同权重参数

  • 卷积核的参数在整个图像上是共享的
  • 不同位置的局部区域都使用这个相同的卷积核做卷积操作
  • 每个位置的输出是该卷积核与对应局部区域的点积结果

填充

为了控制卷积后输出的特征图尺寸;在输入特征图周围填充0

image-20250721110521320

步幅

指卷积核一次移动几格;能够控制输出特征图的大小

eg:步幅为1

image-20250721111455305

步幅为2

image-20250721111709373

卷积运算后特征图大小

$$
输出特征图的高OH=\frac{H+2P-FH}{S}+1
$$

  • H:输入特征图的高
  • P:填充的宽度(多了几圈)
  • FH:卷积核的高
  • S:步幅

$$
OW = \frac{W+2P-FW}{S}+1
$$

  • W:输入特征图的宽
  • P:填充的宽度(多了几圈)
  • FW:卷积核的宽
  • S:步幅

多通道卷积运算

特征图有n通道,卷积核就有n通道

image-20250721152357448

将每个通道的卷积结构相加;有n个卷积核,输出特征图通道数就为n

image-20250721152823648

对于多通道卷积运算,我们可以用立体图表示

  • C:输入通道数
  • W:输入特征图的宽度
  • H:输入特征图的高度
  • FW:卷积核宽度
  • FH:卷积核高度
  • FN:输出通道数/输出的特征图数/卷积核数量

池化运算

池化运算是按通道独立运算的,即池化层运算不会对输入特征图的通道进行改变

池化核

类比卷积运算,我们也可以引入池化核这一概念

与卷积核相同和区别:

  • 相同点:都是在特征图上滑动的窗口,并提取信息
  • 不同:池化核仅仅是告诉算法要在特征图什么区域提取一块多大的特征,对这块特征取最大值或者平均,而不是和自身进行点积

最大池化运算

image-20250721155433414

每次在区域中找到最大值

平均池化运算

image-20250721155621383

区域内的值相加求平均

优点

  • 对微小的位置变化具有鲁棒性,使模型更加健壮

    image-20250721160001357

输出尺寸

$$
OH=\frac{H+2P-FH}{S}+1 \
OW = \frac{W+2P-FW}{S}+1
$$

  • H,W:输入特征图的高、宽
  • P:填充的宽度(多了几圈)
  • FH,FW:池化核的高、宽
  • S:步幅

代码实现

全部代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import numpy as np
import tensorflow as tf
from keras.optimizers import Adam
from tensorflow.keras import models, layers, activations

# 获取mnist手写数字数据集
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()

# 维度改成4维,适配卷积层的输入格式 (60000, 28, 28)->(60000, 28, 28, 1)
x_train = x_train[:, :, :, np.newaxis].astype(float)
x_test = x_test[:, :, :, np.newaxis].astype(float)

model = models.Sequential([
layers.Conv2D(
32, # 卷积核数量
(4, 4), # 卷积核尺寸
activation=activations.relu, # 经过该层后使用的激活函数,为了引入非线性特征
input_shape=(28, 28, 1) # 与mnist特征的shape相同
),
layers.MaxPool2D(
(3, 3) # 池化核大小
),
layers.Conv2D(
64, # 卷积核数量
(4, 4), # 卷积核尺寸
activation=activations.relu, # 经过该层后使用的激活函数,为了引入非线性特征
),
layers.MaxPool2D(
(3, 3) # 池化核大小
),
# 将2D展平为1D才能传入全连接层
layers.Flatten(),
# 全连接层
layers.Dense(
units=64,
activation=activations.relu
),
# 输出层
layers.Dense(
units=10,
activation=activations.linear
)
])

model.compile(
loss=tf.losses.SparseCategoricalCrossentropy(from_logits=True), # 稀疏交叉熵损失函数
optimizer=Adam() # adam优化
)

model.fit(
x_train,
y_train,
epochs=5,
validation_split=0.1 # 每次从训练集中划分10%作为验证集
)

# 前向传播值
logits = model(x_train)
y_train_pred = tf.nn.softmax(logits)
print(y_train_pred)

# 评估模型
loss = model.evaluate(x_test, y_test)
print(loss)

详解:

用到的库

1
2
3
4
import numpy as np
import tensorflow as tf
from keras.optimizers import Adam
from tensorflow.keras import models, layers, activations

获取数据集并进行初步处理

1
2
3
4
5
6
# 获取mnist手写数字数据集
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()

# 维度改成4维,适配卷积层的输入格式 (60000, 28, 28)->(60000, 28, 28, 1)
x_train = x_train[:, :, :, np.newaxis].astype(float)
x_test = x_test[:, :, :, np.newaxis].astype(float)

卷积层,使用tensorflow.keras.layers.Conv2D()

1
2
3
4
5
6
layers.Conv2D(
32, # 卷积核数量
(4, 4), # 卷积核尺寸
activation=activations.relu, # 经过该层后使用的激活函数,为了引入非线性特征
input_shape=(28, 28, 1) # 与mnist特征的shape相同
),

池化层,使用tensorflow.keras.layers.MaxPool2D() (最大池化运算)

1
2
3
layers.MaxPool2D(
(3, 3) # 池化核大小
),

整体结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
model = models.Sequential([
layers.Conv2D(
32, # 卷积核数量
(4, 4), # 卷积核尺寸
activation=activations.relu, # 经过该层后使用的激活函数,为了引入非线性特征
input_shape=(28, 28, 1) # 与mnist特征的shape相同
),
layers.MaxPool2D(
(3, 3) # 池化核大小
),
layers.Conv2D(
64, # 卷积核数量
(4, 4), # 卷积核尺寸
activation=activations.relu, # 经过该层后使用的激活函数,为了引入非线性特征
),
layers.MaxPool2D(
(3, 3) # 池化核大小
),
# 将2D展平为1D才能传入全连接层
layers.Flatten(),
# 全连接层
layers.Dense(
units=64,
activation=activations.relu
),
# 输出层
layers.Dense(
units=10,
activation=activations.linear
)
])

编译模型

1
2
3
4
model.compile(
loss=tf.losses.SparseCategoricalCrossentropy(from_logits=True), # 稀疏交叉熵损失函数
optimizer=Adam() # adam优化
)

训练模型

1
2
3
4
5
6
model.fit(
x_train,
y_train,
epochs=5,
validation_split=0.1 # 每次从训练集中划分10%作为验证集
)

获取最终损失函数值

1
2
3
# 评估模型
loss = model.evaluate(x_test, y_test)
print(loss)

输出结果

image-20250721211942667

模型评估

划分测试集与训练集

image-20250721170149583

计算损失函数

线性回归

image-20250721170305019

逻辑回归

image-20250721172635258

不适用逻辑损失计算训练误差和测试误差:

image-20250721172910100

也成为0-1损失
$$
\hat{y_i} = sign(f_{w,b}(\vec{x_i})) \
sign()是符号函数,输出-1/+1;\hat{y_i} 表示对第i个样本的预测类别,y_i表示第i个样本的真实标签 \
Error = \frac{1}{m}\sum_{i=1}^{m}1(\hat{y_i} != y_i) \
当\hat{y_i} != y_i,累加1,否则累加0 \
换句话说,\sum_{i=1}^{m}1(\hat{y_i} != y_i)就是预测类别与真实标签不符的数量
$$
测试误差能更客观地表现模型面对新数据时的情况

模型选择

image-20250721213305246

拟合一个多项式次数d=k的多项式,训练得到参数
$$
w^{},b^{}
$$
为什么课上提出
$$
J_{test}(w^{<5>},b^{<5>})
$$
太过于乐观呢?

个人理解:

因为选择多项式模型时,拟合的参数除了w,b,还有多项式指数d

但是我们选择超参数d时使用了测试集;即我们使用测试集对不同d的选择进行了评估;也就是说测试集参与了优化超参数d,那么最终测试时,选择的d必然是在测试集上表现最优的那个,那么就会造成测试误差小于实际泛化时的误差

就好比考前看到了卷子,考出的成绩虚高

结论:选择超参数(比如这个例子中的d)时,要使用验证集,测试集仅用于测试和最终评估

交叉验证集(验证集/开发集[dev set])

使用额外的数据集来交叉检查有效性或者不同模型的准确性

image-20250721220520549

对于上面选择合适的指数d,应该改为用验证集

找到最低的交叉验证误差对应的d

此时报告泛化误差就可以直接使用测试误差了,测试误差将不再小于泛化误差

整体过程

  • 训练集->拟合模型参数
  • 验证集->拟合超参数(模型以外的参数)
  • 测试集->评估模型

整体过程中测试集全程没有参与任何参数的拟合,这就是为什么测试误差是泛化误差公平估计

偏差与方差的平衡解决

诊断偏差和方差

在前面的欠拟合与过拟合学习中,我们是通过绘制标签以及预测曲线来进行判断是否具有高偏差/方差,可是当特征多了之后,绘图就变得困难,此时需要引进更系统的方法来进行判断

image-20250722002352770

对于欠拟合:

特点是训练损失$$J_{train}$$很高(在训练集上表现的就不好),验证损失$$J_{cv}$$也很高($$J_{cv} \approx J_{train}$$),所以当$$J_{train}$$很高的时候,我们就可以断定模型欠拟合

对于过拟合:

特点是训练损失$$J_{train}$$很低,但是对于验证损失$$J_{cv}$$来说,由于是新数据,所以拟合效果不佳,验证损失$$J_{cv}$$会很高(证损失$$J_{cv} >> J_{train}$$)

image-20250722002949364

如果两种情况同时存在(一般不出现在线性回归):

$$
J_{train}很高,同时J_{cv} >> J_{train}
$$

image-20250722002324366

正则化

与前面类似的内容

image-20250722003653034

新方法:使用交叉验证,与前面决定幂次d类似

image-20250722004127461

选择不同的超参数λ,分别进行模型训练得到一组参数w,b,找到验证损失最小的一组作为最终的λ

image-20250722004400444

表现基准

eg:语音识别搜索

image-20250722005119280

训练误差为10.8%,交叉验证误差为14.8%,乍一看其实很高,但是人类识别错误的概率也来到了10.6%,那么其实这个算法表现得相当可以了

对于这个例子来说,判断两个误差是否高,更有用的是看训练的误差是否远高于人类的表现水平

定义

基准:可以合理期望学习算法最终达到的误差水平是多少

比如

  • 通过人类在这件事上的表现来确定
  • 通过竞争算法的一些指标数据进行确定
  • 经验主义

测量的两个关键量

  • 训练误差与基准之间的差距(大:欠拟合)
  • 训练误差和交叉验证误差的差距(大:过拟合)image-20250722010450437

学习曲线

样本尺寸和误差(J_{train},J_{cv})之间的关系

绘制学习曲线

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import numpy as np
import matplotlib.pyplot as plt
# 线性回归模型
from sklearn.linear_model import LinearRegression
# 用于评估模型学习过程
from sklearn.model_selection import learning_curve
# 引入管道
from sklearn.pipeline import make_pipeline
# 多项式特征
from sklearn.preprocessing import PolynomialFeatures

X = 5 * np.random.rand(600, 3)
Y = (np.exp(X) + np.exp(-X) + np.sin(X)) * 0.1


def plot_learning_curve(X, Y, ax=None, degree=1, title=None):
# 通过管道创建模型,先创建多项式特征再线性拟合
model = make_pipeline(PolynomialFeatures(degree=degree), LinearRegression())
'''
train_size:训练集尺寸的集合(从10%~100%等距划分20份)
train_scores:每个训练集的负均方误差
test_scores:每个验证集的负均方误差
'''
train_size, train_scores, test_scores = learning_curve(
estimator=model, # 指定模型
cv=5, # 将数据集划分为5份,进行5轮训练;其中1份是验证集,4份是训练集;总共产生5份误差
X=X,
y=Y,
scoring="neg_mean_squared_error", # 负的均方误差
train_sizes=np.linspace(0.1, 1, 40), # 20 个从 10% 到 100% 的等间距数值,20个不同的训练集大小
shuffle=True, # 划分训练集前随机打乱
n_jobs=-1 # 全部CPU加速
)
# 对每个训练集5轮训练的误差求平均再取负
train_scores_mean = -train_scores.mean(axis=1)
test_scores_mean = -test_scores.mean(axis=1)

# 绘图 o-是指定点形状是圆点
ax.plot(train_size, train_scores_mean, 'o-', color="red", label="Training error")
ax.plot(train_size, test_scores_mean, 'o-', color="blue", label="cv error")

ax.set_title(title)
ax.set_xlabel('size of sample')
ax.set_ylabel('Error')
# 图例
ax.legend()
ax.grid(True)


# 1张有3个子图的画布
fig, axes = plt.subplots(1, # 1张画布
3, # 3个子图
figsize=(18, 6), # 画布尺寸
sharex=True # y轴坐标一致
)

plot_learning_curve(X, Y, ax=axes[0], degree=1, title="Underfitting")
plot_learning_curve(X, Y, ax=axes[1], degree=3, title="Nice")
plot_learning_curve(X, Y, ax=axes[2], degree=4, title="Overfitting")

plt.show()

展示了欠拟合,拟合良好,过拟合的情况

image-20250722122314150

随样本量增大,交叉验证误差降低,训练误差上升,为什么?

  • 训练误差上升:随样本量增加,曲线完美拟合样本点的难度会加大;只有个别样本点时,拟合难度就很低
  • 交叉验证误差降低:随样本量增加,模型泛化能力增强,对新数据预测得更好

欠拟合

image-20250722021841500

训练损失和验证损失都很大,但样本量达到一定水平时,训练损失和验证损失都将趋于稳定

image-20250722122524801

过拟合

image-20250722021943524

训练误差低的不切实际

image-20250722022411882

解决高偏差和高方差

解决高方差

  • 对于小样本过拟合,选择获取更多训练样本
  • 使用更有用的特征子集,简化模型
  • 增大正则化参数λ,减小权重

解决高偏差

  • 添加更多特征
  • 添加多项式特征
  • 减小正则化参数λ
  • 不能通过减小训练集规模来解决高偏差,会导致出现高方差

在神经网络中的偏差和方差

大型神经网络在中小型数据集上训练时,表现低偏差;也就是说神经网络足够大时,模型几乎能总是很好的拟合训练集

调整方差和偏差

image-20250722131119461

如果在训练集上表现不佳(J_train很大),可以通过使用更复杂的神经网络结构(更多的隐藏层以及层中有更多的神经元)

在验证集上表现不佳,可以选择增大训练数据规模

局限性:

  • 太大太复杂的神经网络会导致计算开销非常非常大
  • 数据规模到一定程度后就不容易再获取到新的数据了

神经网络太大会导致高方差吗

一个经过良好正则化的大型神经网络通常表现得和小网络一样好甚至更好

实现正则化

image-20250722132339790

tensorflow中需要在Dense中指定kernel_regularizer

1
2
3
4
5
6
7
8
import tensorflow as tf
from tensorflow.keras import models,layers

model = models.Sequential([
layers.Dense(units=25,activation=tf.keras.activations.relu,kernel_regularizer=tf.keras.regularizers.L2(0.02)), # 使用L2正则化,参数为0.02
layers.Dense(units=15,activation=tf.keras.activations.relu,kernel_regularizer=tf.keras.regularizers.L2(0.02)),
layers.Dense(units=1,activation=tf.keras.activations.sigmoid,kernel_regularizer=tf.keras.regularizers.L2(0.02))
])

代码验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import numpy as np
import tensorflow as tf
from tensorflow.keras import models, layers
from sklearn.model_selection import train_test_split

X = np.random.rand(600, 3)
Y = np.random.randint(0, 2, 600)

X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size=0.2)

# 无正则化的小模型
model1 = models.Sequential([
layers.Dense(units=25, activation=tf.keras.activations.relu),
layers.Dense(units=15, activation=tf.keras.activations.relu),
layers.Dense(units=1, activation=tf.keras.activations.sigmoid)
])
# 带正则化的大模型
model2 = models.Sequential([
layers.Dense(units=35, activation=tf.keras.activations.relu, kernel_regularizer=tf.keras.regularizers.L2(0.02)),
# 使用L2正则化,参数为0.02
layers.Dense(units=25, activation=tf.keras.activations.relu, kernel_regularizer=tf.keras.regularizers.L2(0.02)),
layers.Dense(units=15, activation=tf.keras.activations.relu, kernel_regularizer=tf.keras.regularizers.L2(0.02)),
layers.Dense(units=5, activation=tf.keras.activations.relu, kernel_regularizer=tf.keras.regularizers.L2(0.02)),
layers.Dense(units=1, activation=tf.keras.activations.sigmoid,
kernel_regularizer=tf.keras.regularizers.L2(0.02))
])

model1.compile(
loss=tf.keras.losses.BinaryCrossentropy(),
optimizer=tf.keras.optimizers.Adam()
)

model2.compile(
loss=tf.keras.losses.BinaryCrossentropy(),
optimizer=tf.keras.optimizers.Adam()
)

history1 = model1.fit(
X_train,
y_train,
validation_split=0.2, # 抽20%的测试集作为验证集
epochs=100
)

history2 = model2.fit(
X_train,
y_train,
validation_split=0.2, # 抽20%的测试集作为验证集
epochs=100
)

train_loss1 = history1.history['loss']
val_loss1 = history1.history['val_loss']

print(f"无正则化小模型训练最终损失{train_loss1[len(train_loss1) - 1]}")
print(f"无正则化小模型验证最终损失{val_loss1[len(val_loss1) - 1]}")

train_loss2 = history2.history['loss']
val_loss2 = history2.history['val_loss']

print(f"正则化大模型训练最终损失{train_loss2[len(train_loss2) - 1]}")
print(f"正则化大模型验证最终损失{val_loss2[len(val_loss2) - 1]}")

image-20250722135134309

训练100轮后,正则化大模型的表现优于无正则化的小模型

结论:大的神经网络特别适合拟合复杂函数;当数据集规模不是很大时,大的神经网络通常是低偏差的

机器学习系统

迭代发展

image-20250722141425961

选择架构(模型,数据集,特征工程,超参数选择…)->模型训练->模型评估(偏差,方差,误差分析)

eg:垃圾邮件分类器

image-20250722141818917

例子中,特征向量构建使用的是:构建一个字典,根据其中单词是否出现来把元素设置为0/1;也可以把元素设置成单词出现的次数

误差分析

手动检查算法错误的例子,看看是否存在系统性错误;可以帮助我们理解模型在哪些方面表现不佳,从而指导我们如何改进模型

添加数据

机器学习中,我们几乎总是希望能有更多的数据,因此有时我们会忍不住想去获取更多的所有类型的数据,但是获取所有类型的数据代价花销太高

数据增强

拿一个现有的训练样本取创建一个新的训练样本,在原来的训练样本上做出一些变化,大小变化,对比度变化,旋转等;能够提高模型泛化能力,防止过拟合

注意:对源数据进行修改或失真处理应该代表测试集中存在的噪音或失真类型,添加的噪音应该是我们预期中出现的而不是随机添加

image-20250722152300542

数据合成

从零开始编造全新的样本,数据合成是一种通过人工生成或模拟数据的方法,以弥补真实数据不足、不平衡或难以获取的问题

比如OCR(Optical Character Recognition,光学字符识别)任务中,使用自己电脑上的字体截图来创造新的样本

image-20250722153956034

迁移学习

将在一个任务或数据集上训练好的模型,迁移到另一个相关但不同的任务或数据集中

优点:迁移学习可以显著减少训练时间和数据需求,尤其适用于数据量较小的任务

image-20250722154319506

图片中的例子,保留了隐藏层的结构以及参数,但是将输出层换成了更小的输出层

此时有两种训练方法

1.只训练输出层,使用adam等优化算法更新输出层的参数(适用于小训练集)

2.训练所有的参数,但前四层用迁移前的模型训练的值进行初始化(适用于大训练集)

机器学习项目完整周期

image-20250722155149916

  1. Scope project(定义项目范围)
    • 在这个阶段,你需要明确项目的目标、需求和预期成果。这包括定义项目的范围,确定项目要解决的问题,以及项目成功的关键指标。
  2. Collect data(收集数据)
    • 一旦项目范围确定,下一步就是收集数据。这可能涉及到定义数据收集的方法和来源,以及实际的数据收集工作。数据的质量对机器学习模型的性能至关重要。
  3. Train model(训练模型)
    • 在收集到足够的数据后,接下来的任务是训练模型。这包括选择合适的算法,使用训练数据来调整模型参数,以及进行模型的训练、错误分析和迭代改进。这个阶段的目标是开发出一个性能良好的模型。
  4. Deploy in production(部署到生产环境)
    • 当模型训练完成并且经过验证后,最后一个阶段是将模型部署到生产环境中。这意味着模型将被用于实际的业务流程中,对真实世界的数据进行预测或决策。在这个阶段,还需要对系统进行部署、监控和维护,确保其稳定运行并根据需要进行更新。

倾斜数据集的误差指标

详见第0周的模型评价指标(真正,真负,假正,假负,精确率,召回率,F1 scope)