从0开始学习卷积神经网络(一)—— 1+1=2

本系列文章将分享我在学习卷积神经网络时的心得和体会,不会涉及复杂的数学公式,而是用简单易懂的语言来解释卷积神经网络。当然,如果对相关的数学公式有深入了解的话,对学习这部分知识会很有帮助。

卷积计算

首先,我们简单介绍一下卷积。卷积的相关公式如下:

$$
(f * g)(n) = \sum_{m=-\infty}^{\infty} f(m) g(n - m)
$$

在这里,我们不对卷积的具体含义做过多解释,有兴趣的读者可以自行搜索相关文章深入了解。下面我们主要聊聊卷积的计算过程和它的实际应用。

假设我们有一张5×5的RGB图片,这意味着图片由5×5个像素点组成,可以看作一个5×5的矩阵。每个像素点包含RGB三个元素,每个元素的值范围在0到255之间。这样一张RGB图片的矩阵可能是以下这个样子:

$$
RGB_image = \begin{bmatrix}
[10, 20, 30] & [40, 50, 60] & [70, 80, 90] & [100, 110, 120] & [130, 140, 150] \
[11, 21, 31] & [41, 51, 61] & [71, 81, 91] & [101, 111, 121] & [131, 141, 151] \
[12, 22, 32] & [42, 52, 62] & [72, 82, 92] & [102, 112, 122] & [132, 142, 152] \
[13, 23, 33] & [43, 53, 63] & [73, 83, 93] & [103, 113, 123] & [133, 143, 153] \
[14, 24, 34] & [44, 54, 64] & [74, 84, 94] & [104, 114, 124] & [134, 144, 154] \
\end{bmatrix}
$$

这个三维矩阵看起来还是有点复杂,所以我们可以把这张图片的RGB三个通道分别拆开来看,具体如下:

$$
R = \begin{bmatrix}
10 & 40 & 70 & 100 & 130 \
11 & 41 & 71 & 101 & 131 \
12 & 42 & 72 & 102 & 132 \
13 & 43 & 73 & 103 & 133 \
14 & 44 & 74 & 104 & 134 \
\end{bmatrix}
$$

$$
G = \begin{bmatrix}
20 & 50 & 80 & 110 & 140 \
21 & 51 & 81 & 111 & 141 \
22 & 52 & 82 & 112 & 142 \
23 & 53 & 83 & 113 & 143 \
24 & 54 & 84 & 114 & 144 \
\end{bmatrix}
$$

$$
B = \begin{bmatrix}
30 & 60 & 90 & 120 & 150 \
31 & 61 & 91 & 121 & 151 \
32 & 62 & 92 & 122 & 152 \
33 & 63 & 93 & 123 & 153 \
34 & 64 & 94 & 124 & 154 \
\end{bmatrix}
$$

或者把RGB图片转换为灰度图片,得到的也是一个二维矩阵。

这里我们以RGB图片为例进行说明。如果此时需要对图片进行卷积计算,就要应用相应的公式。可以将R通道的矩阵表示为函数f(m, j) ,而g(n-m, i-j)暂时先不考虑。于是,卷积公式可以转化为如下形式:

$$
(f * g)(n, i) = \sum_{m=n,j=i}^{n+2, i+2} f(m, j) g(n-m, i-j)
$$

可以先假设 g 是一个 3×3 的矩阵,因此将m/j的取值范围限定在 n, n+1, n+2/i, i+1, i+2之内。代入公式后可得:

1
2
3
fg(0, 0) = f(0, 0) * g(0, 0) + f(0, 1) * g(0, -1) + f(0, 2) * g(0, -2)
f(1, 0) * g(-1, 0) + f(1, 1) * g(-1, -1) + f(1, 2) * g(-1, -2)
f(2, 0) * g(-2, 0) + f(2, 1) * g(-2, -1) + f(2, 2) * g(-2, -2)

这个g函数好像带着符号看起来不太美观,这时候我们可以考虑定义一个3×3的g1矩阵,具体如下:

$$
g1 = \begin{bmatrix}
g(0, 0) & g(0, -1) & g(0, -2) \
g(-1, 0) & g(-1, -1) & g(-1, -2) \
g(-2, 0) & g(-2, -1) & g(-2, -2) \
\end{bmatrix} = \begin{bmatrix}
-1 & -1 & -1 \
-1 & 9 & -1 \
-1 & -1 & -1 \
\end{bmatrix}
$$

这个 g1 矩阵是 g 函数旋转 180 度后具体表现的形式。

现在,我们将图片的 R 通道与 g1 函数进行卷积计算,得到的结果是一个 3 × 3 的矩阵(这并不是因为 g 函数的定义域是 3 × 3,而是由于 f 函数的范围减去 2 所决定的)。通过这种计算,图片会产生一定的损失。为了减少这种损失,可以考虑对图片进行填充(padding),从而生成一个 7 × 7 的矩阵。这样,经过计算后,结果矩阵的尺寸就变为 5 × 5。填充的方式有多种选择,这里采用填充 0 的方法,由此得到一个新的 R 通道矩阵:

$$
Rpad = \begin{bmatrix}
0 & 0 & 0 & 0 & 0 & 0 & 0 \
0 & 10 & 40 & 70 & 100 & 130 & 0 \
0 & 11 & 41 & 71 & 101 & 131 & 0 \
0 & 12 & 42 & 72 & 102 & 132 & 0 \
0 & 13 & 43 & 73 & 103 & 133 & 0 \
0 & 14 & 44 & 74 & 104 & 134 & 0 \
0 & 0 & 0 & 0 & 0 & 0 & 0
\end{bmatrix}
$$

我把计算过程用代码的形式展示出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 首先,是最简单,通俗易懂的计算方案
g1 = [[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]]
Rpad = [
[0] * 7,
[0, 10, 40, 70, 100, 130, 0],
[0, 11, 41, 71, 101, 131, 0],
[0, 12, 42, 72, 102, 132, 0],
[0, 13, 43, 73, 103, 133, 0],
[0, 14, 44, 74, 104, 134, 0],
[0] * 7
]
result = [[0] * 5 for _ in range(5)]
for x in range(5):
for y in range(5):
for i in range(3):
for j in range(3):
result[x][y] += Rpad[x+i][y+j] * g1[i][j]
# result
# [[-2, 157, 277, 397, 838], [-46, 41, 71, 101, 614], [-42, 42, 72, 102, 618], [-38, 43, 73, 103, 622], [26, 179, 299, 419, 866]]

得到的结果为:

$$
NewR = \begin{bmatrix}
-2 & 157 & 277 & 397 & 838 \
-46 & 41 & 71 & 101 & 614 \
-42 & 42 & 72 & 102 & 618 \
-38 & 43 & 73 & 103 & 622 \
26 & 179 & 299 & 419 & 866 \
\end{bmatrix}
$$

或者,我们可以使用pytorch来进行卷积计算,相关代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import torch
import torch.nn.functional as F

g1 = [[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]]
g2 = torch.tensor(g1, dtype=torch.float32)

Rpad = [
[10, 40, 70, 100, 130],
[11, 41, 71, 101, 131],
[12, 42, 72, 102, 132],
[13, 43, 73, 103, 133],
[14, 44, 74, 104, 134],
]
Rpad2 = torch.tensor(Rpad, dtype=torch.float32)
result = F.conv2d(Rpad2.unsqueeze(0), g2.unsqueeze(0).unsqueeze(0), padding=1)

'''
>>> print(result)
tensor([[[ -2., 157., 277., 397., 838.],
[-46., 41., 71., 101., 614.],
[-42., 42., 72., 102., 618.],
[-38., 43., 73., 103., 622.],
[ 26., 179., 299., 419., 866.]]])
'''

自己动手复现一遍上面的过程,就能大致搞清楚卷积的计算是怎么回事了。那么,这种计算到底有什么用呢?在实际应用中,业界通过对图片进行卷积计算,可以提取出图片的特征,比如锐化、降噪等处理方式,都可以通过卷积来实现。

在整个计算过程中,只有g1这个矩阵是可控的变量,前面也没有细说,只是随便取了值生成一个3 × 3矩阵。

g1可以被称作卷积核滤波器等等。存在哪些滤波器,能达到什么效果,可以自行询问GPT和自行写代码测试。

如果处理的是灰度图片,也就是二维矩阵,那就可以直接按照上面说的过程计算。如果是 RGB 图片,也就是三维矩阵,就需要按照前面的步骤,把 RGB 三个通道拆开,分别进行计算,最后把结果组合成一张新的 RGB 图片。这种方式叫做“逐通道卷积”。

下面展示一个垂直边缘检测滤波器处理图片后的效果,相关代码如下:

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 torchvision.transforms as transforms
import torch.nn as nn
import numpy as np
import torch
from PIL import Image

# 加载图片
img_path = "./test2.png"
img_pil = Image.open(img_path)
# 转换为RGB格式
img_np = img_pil.convert("RGB")
img_np = np.array(img_np)
# 将 numpy 数组转换为 PyTorch 张量,shape: (H, W, C) -> (C, H, W)
transform = transforms.ToTensor()
img_tensor = transform(img_np).unsqueeze(0) # (1, 3, H, W)
# 垂直边缘检测滤波器
sobel_vertical = torch.tensor([[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]], dtype=torch.float32)
# 定义卷积层
mean_conv = nn.Conv2d(in_channels=3, out_channels=3, kernel_size=3, padding=1, bias=False, stride=1, groups=3)
# 设置滤波器
mean_conv.weight.data = sobel_vertical.expand(3, 1, 3, 3)

# 计算卷积
with torch.no_grad(): # 不需要计算梯度,可以加快速度
output_mean = mean_conv(img_tensor)

# 进行线性归一化,确保所有值都在 0-1 之间
# 本来RGB的值在0-255之间,但是transform把值转换为0-1之间的浮点数,也就是本身值除以255,要恢复可以再乘以255
min_val = output_mean.min()
max_val = output_mean.max()
# 进行线性归一化,确保所有值都在 0-1 之间
normalized_output = (output_mean - min_val) / (max_val - min_val)

# 把pytorch张量转化为图片
to_pil = transforms.ToPILImage()
output_mean_np = to_pil(normalized_output.squeeze(0))
output_mean_np.show()

原图:

cnn1

经过垂直滤波器处理后:

cnn2

以上就是卷积在实际应用中的一些例子。简单来说,卷积对图片的作用是通过分析像素点与其周围像素的关系来提取特征,而具体提取什么样的特征则取决于卷积核的设计。当然,卷积的应用远不止这些,将会在后文中遇到另外的应用案例。

神经网络中的1+1=2

学习编程语言时,我通常会先从搭建环境开始,然后输出一个Hello World。但是神经网络属于数学,属于算法,我认为学习这类知识应该首先了解该知识的1+1=2部分。接下来就分享一下我对神经网络1+1=2的理解。

首先,我们设定一个目标,比如:计算下雨的概率。

影响下雨的因素有哪些呢?比如有气压。那么可以有这么一个公式:气压 * 权重 = 降雨概率

假设气压值为9,权重值可以随便设置一个,比如0.1,那么降雨概率 = 9 × 0.1 = 0.9 = 90%。我们不用管这个值的对错,这就是一个最简单的神经网络。不过这不能算是完整的1+1=2,只能说是我们初步认识了“1”是什么。

实际上,影响下雨的因素肯定不只气压这一项,还可能包括湿度、温度等等。于是,我们可以扩展出这样一个公式:

$$
\begin{cases}
气压 * 权重1 = 概率1 \
湿度 * 权重2 = 概率2 \
温度 * 权重3 = 概率3
\end{cases}
$$

最终的输出结果为:概率1 + 概率2 + 概率3

上面这个过程就是一维卷积的计算过程。一维卷积公式如下所示:

$$
(fg)(n) = \sum_{m=0}^{2} f(m) g(n - m)
$$

其中f(0), f(1), f(2)就是气压、湿度、温度的值,g(n-0), g(n-1), g(n-2)就是权重1,2,3。结果fg(n)就是最终的降雨概率。

不过这不重要,我们先暂时忘掉卷积的内容。

接下来,我们继续分析所构建的降雨概率预测公式。由于目前尚不清楚权重的具体取值,我们可以暂时为其赋予随机值。

假设三个权重构成一个数组[q1, q2, q3],并将其初始值设为[0.02, 0.25, 0.42]

同时,我们假设输入数组[p, h, t]分别代表气压、湿度和温度,其初始值为:[11, 30, 25]

根据这些值,降雨概率的计算公式为:11 * 0.12 + 30 * 0.25 + 25 * 0.42 = 18.22

显然,由于权重是随机生成的,这一结果并不准确。然而,我们可以通过训练该公式,使其逐步学习并优化权重。以下是相关的代码示例:

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
weight = [0.02, 0.25, 0.42]
input = [11, 30, 25]
# 假定,在气压=11, 湿度等于30, 温度等于25的情况下,降雨概率为30%,也就是0.3
real_answer = 0.3

# 设置调整步数
step_amount = 0.01

# 卷积计算
def conv(f, g):
result = 0
for i in range(len(f)):
result += f[i] * g[i]
return result

# 让权重增加一定的值
def addWeight(w, num):
result = []
for x in w:
result.append(x + num)
return result

# 让权重减少一定的值
def subWeight(w, num):
result = []
for x in w:
result.append(x - num)
return result

step = 0
for i in range(1000):
step += 1
# 计算降雨概率
predict = conv(input, weight)
# 衡量误差
error = (predict - real_answer) ** 2
print(f"训练步数:{step}, 误差:{error}, 预测结果:{predict}")

# 测试需要怎么调整权重
# 计算调高权重后的预测结果
upWeight = addWeight(weight, step_amount)
upPredict = conv(input, upWeight)
# 计算误差
uperror = (upPredict - real_answer) ** 2

# 计算降低权重后的预测结果
downWeight = subWeight(weight, step_amount)
downPredict = conv(input, downWeight)
# 计算误差
downerror = (downPredict - real_answer) ** 2

if downerror < uperror:
weight = downWeight
else:
weight = upWeight

运行代码后,会得到下面的结果:

1
2
3
4
5
6
7
8
9
10
训练步数:26, 误差:2.016399999999973, 预测结果:1.7199999999999904
训练步数:27, 误差:0.5775999999999851, 预测结果:1.0599999999999903
训练步数:28, 误差:0.00999999999999812, 预测结果:0.3999999999999906
训练步数:29, 误差:0.31360000000001126, 预测结果:-0.26000000000001
训练步数:30, 误差:0.00999999999999812, 预测结果:0.3999999999999906
训练步数:31, 误差:0.31360000000001126, 预测结果:-0.26000000000001
训练步数:32, 误差:0.00999999999999812, 预测结果:0.3999999999999906
训练步数:33, 误差:0.31360000000001126, 预测结果:-0.26000000000001
训练步数:34, 误差:0.00999999999999812, 预测结果:0.3999999999999906
训练步数:35, 误差:0.31360000000001126, 预测结果:-0.26000000000001

根据代码输出的结果,我们可以发现,当训练到28步后,达到了该训练的极限,后面的预测结果会在0.399-0.26之间循环。

我们可以调整一下代码中的step_amount参数,设置为0.0001,再看看结果,如下所示:

1
2
3
4
5
6
7
8
训练步数:2711, 误差:0.0011560000000822351, 预测结果:0.33400000000120933
训练步数:2712, 误差:0.000750760000066336, 预测结果:0.3274000000012105
训练步数:2713, 误差:0.0004326400000503506, 预测结果:0.32080000000121034
训练步数:2714, 误差:0.00020164000003439458, 预测结果:0.31420000000121107
训练步数:2715, 误差:5.776000001841267e-05, 预测结果:0.30760000000121135
训练步数:2716, 误差:1.0000000024241737e-06, 预测结果:0.3010000000012121
训练步数:2717, 误差:3.135999998642643e-05, 预测结果:0.2944000000012119
训练步数:2718, 误差:1.0000000024241737e-06, 预测结果:0.3010000000012121

这次,我们发现直到训练到2716步的时候,才达到极限,并且预测的结果更加接近真实的答案。

下面,来增加一点难度,输入有多组数据,如下所示:

1
2
3
4
5
6
7
8
# 以下数据都是我随便输入的,不具任何现实参考价值
input = [
[11, 30, 25],
[1, 12, 15],
[4, 44, 20],
[3, 66, 30]
]
real_answer = [0.3, 0.45, 0.23, 0.66]

在只有一组输入数据的时候,权重的变化都是同步的,但是按照逻辑推测,真实情况下,不同权重的变化情况需要有所区别,所以我对训练代码做了如下修改:

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
import numpy as np

weight = np.array([0.02, 0.25, 0.42])
input = np.array([
[11, 30, 25],
[1, 12, 15],
[4, 44, 20],
[3, 66, 30]
])
real_answer = np.array([0.3, 0.1, 0.4, 0.5])
step = 0
for i in range(10000):
step += 1
for it in range(len(input)):
# 计算降雨概率
predict = sum(input[it] * weight)
# 衡量误差
error = (predict - real_answer[it]) ** 2
print(f"训练步数:{step}, 误差:{error}, 预测结果:{predict}")

# 使用一种方法计算权重变化值
delta = predict - real_answer[it]
weight_deltas = delta * input[it]
# print(f"weight_deltas = {weight_deltas}")
# 定义一个控制权重变化快慢的参数
alpha = 0.0001
for x in range(len(weight)):
weight[x] -= alpha * weight_deltas[x]
# print(f"weight = {weight}")

有兴趣的读者可以通过调整迭代次数和alpha参数来观察模型的收敛情况。

若您能够理解上述内容,说明您已经掌握了神经网络的1+1=2。接下来,就要如下图这样,在学会1+1=2后,我们要开始深入了解神经网络了,这部分内容将会出现在下篇文章。

1+1=2

通过1+1=2理解机器学习

PS:以下内容为个人理解记录,非标准答案

上述的样例中,为啥最终的概率是每种影响因素乘以权重然后求和呢?因为学习的是卷积神经网络,采用的算法当然是卷积计算。在深度学习中,一般叫xx神经网络的,表示使用xx算法构建的神经网络。

之前一直不知道机器学习是如何让计算机来学习的,神经网络中的网络到底是个什么东西。学到这,脑海中已经有个大致的概念了。在上面的例子中,网络就是可以通过不断训练来进行修改的权重,计算机学习的是如何调整权重来让预测结果接近训练过程中给定的答案。

在训练代码中,经常能看到需要损失函数计算损失值,这其实就是计算误差值,以便后续更新模型的权重值。

这种架构的人工智能我认为是永远无法具有思考的能力,这只能称作预测机。不过,换种思维,一个能预测到极致的预测机是不是可以被称为是预言机。如果计算机总能正确的预测出你预期的答案,那么这跟思考似乎没啥区别。

通过前文的示例可以看出,这种AI中包含的可控变量有:输入(训练数据集),算法,alpha值,计算损失值的算法。

其中,alpha值和计算损失值的算法,我认为只影响到训练的速度。

而训练数据集和算法则影响到最终的预测效果。比如,前文提到的卷积算法,该算法在处理图片,提取图片特征上比较有优势,但是用来处理自然语言文本生成效果可能就不太行。

所以我感觉现在的AI行业一方面需要会清洗数据的黑奴,另一方面需要研究算法的研究员。

从0开始学习卷积神经网络(一)—— 1+1=2

https://nobb.site/2025/02/14/0x8F/

Author

Hcamael

Posted on

2025-02-14

Updated on

2025-03-13

Licensed under