什么是人工智能(Artificial Intelligence, AI)?

这一名词并没有一个全世界通用的定义,在大多数的文件与标准中,分别从学科技术系统两个角度进行定义:

  • 中国国家标准:利用数字计算机或由其控制的机器,模拟、延伸和扩展人的智能,感知环境、获取知识并使用知识获得最佳结果的理论、方法、技术及应用系统。

  • 国务院《新一代人工智能发展规划》:人工智能是研究、开发用于模拟、延伸和扩展人的智能的理论、方法、技术及应用系统的一门新的技术科学。

  • 法律操作性定义(OECD/欧盟《人工智能法案》):是一种基于机器的系统,能够在不同程度的自主性下运行,部署后可展现出适应性,并针对明示或隐含的目标,从接收的输入中推断出如何生成可影响物理或虚拟环境的输出,如预测、内容、建议或决策。

从上述这些官方定义中可以看到一些共同处:AI是一个系统,并且能感知外部环境并输出结果。 进一步抽象AI的定义,我们可以简化为一个函数:y=ax+b,其中x就是感知到的外部环境信息,y就是输出的结果,而a与b就是训练出来的AI参数。

初中方程配上高中导数=简单的小AI

举个例子

房价计算

房价=a面积+b房价 = a * 面积 + b

  • (x):面积;
  • (a):面积对房价的影响程度;
  • (b):即使面积为 0 时,模型给出的基础偏移;
  • (y):预测房价。

目前这个AI只能考虑面积这一个因素,现实世界要考虑的东西非常复杂,比如地段、楼层、交通情况、装修费用等,于是现在的AI模型变成了:

房价=a1面积+a2楼层+a3房龄+a4公交距离+b房价 = a1 * 面积 + a2 * 楼层 + a3 * 房龄 + a4 * 公交距离 + b

现在这个AI可以帮我快速算出一个房子合理的价格了。

现在出现了一个新的问题,这个公式里面的 a1, a2, a3, a4, b都是从哪里来的?总不能直接拍脑门定下下这些参数。此时我们就要“从已有的数据中进行学习”,我们需要教会AI,给定面积、楼层、房龄、公交距离时,他的房价是多少,因此我们会获得这样一个表格:

编号 面积 x₁/㎡ 楼层 x₂ 房龄 x₃/年 公交距离 x₄/米 房价 y/万元
1 55 3 18 950 92
2 60 5 15 800 105
3 65 6 12 700 119
4 70 8 10 650 135
5 75 10 8 600 151
6 80 12 6 500 170
7 85 15 5 450 185
8 90 16 4 400 202
9 95 18 3 350 220
10 100 20 2 300 238
11 72 4 20 1100 123
12 78 7 16 900 142
13 88 9 12 750 175
14 92 11 10 600 196
15 105 14 8 500 232
16 110 17 6 450 254
17 115 19 5 350 273
18 120 22 3 250 298

理论上,对于表中的任意一行,将x1-x4带入y的表达式中,我们会得到一个预测的价格:y^\hat{y},以及他的真实值yy,这两个值之间的差距应该越小越好(真实世界几乎无法做到百分百线性,只要误差在一定范围内即可)。

此时出现一个问题,我们并不知道a1-a4以及b具体是多少,如果直接赋予一个随机的初始值,那么整个表达式计算出来的结果y^\hat{y}与真实值yy会有非常大的偏差,具体可以写(第一行为例):

y^92=55a1+3a2+18a3+950a4+b92\hat{y} - 92 = 55a_1 + 3a_2 + 18a_3 +950a_4 + b - 92

那么,我们是不是可以找一组参数,让这个误差最小?不过我们不能只顾第一行数据,我们要让已有的这18条都能最小的一组参数,即:

i=118yi^yi=i=118(a1xi面积+a2xi楼层+a3xi房龄+a4xi公交距离+byi)\sum_{i=1}^{18} \hat{y_i} - y_i = \sum_{i=1}^{18} (a_1x_{i面积} + a_2x_{i楼层} + a_3x_{i房龄} + a_4x_{i公交距离} + b - y_i)

但是这个方程还有些问题,如果一个房价预测比真实值高10w,另一个低了10w,那么求和之后就是0,总的误差成0了!无法起到监督的效果,所以我们对原先的y^y\hat{y}-y加上一个平方,保证每一项的误差都是一个正值,不会出现抵消,而且二次项会把误差放大,更利于收敛。

Loss=i=118(yi^yi)2Loss = \sum_{i=1}^{18} (\hat{y_i} - y_i)^2

当然这是一个总误差,数据量如果比较大,那这个误差会非常大,一个好的方法它应该不挑数据量大小,应该在每一条数据上反应他的误差,所以在此基础上我们还要再加一个平均,同时,考虑到求最小化一般都要求导,二次项求导会产生一个2倍系数,我们再凑一个1/2用于后续抵消求导,最终的损失函数为:

Loss=1218i=118(yi^yi)2Loss = \frac{1}{2*18} \sum_{i=1}^{18} (\hat{y_i} - y_i)^2

而这个形式就是日后常见的损失:MSE均方误差。

怎么求解?

对于一元二次方程通过求导并令导函数为0,可以求出极值点,例如:f(x)=(x3)2f(x)=(x-3)^2,导函数为f(x)=2(x3)f^{'}(x)=2(x-3),令导函数为0可以得到x=3x=3,此时函数获得极小值。

为什么可以这么做?直观的说,导数表示的是函数在某点的变化趋势,大于0说明上升,小于0说明下降,等于0说明为极值点。

回到我们的模型,如果是最开始单变量问题,y^=ax\hat{y}=ax,是一个关于a的函数,损失函数为,一般也会写成:JJ

Loss(a)=1ni=1n(axiyi)2Loss(a)=\frac{1}{n}\sum_{i=1}^{n}(ax_i-y_i)^2

此时求导并令导函数为0,可以求出令损失函数最小的a。

但是,现在的模型并不是一个单变量的模型,而是多个参数[a1,a2,a3,a4,b][a_1, a_2, a_3, a_4, b],损失函数也是一个多变量的函数:

1218i=118(yi^yi)2=1218i=118(a1xi面积+a2xi楼层+a3xi房龄+a4xi公交距离+byi)2\frac{1}{2*18}\sum_{i=1}^{18} (\hat{y_i} - y_i)^2 = \frac{1}{2*18}\sum_{i=1}^{18} (a_1x_{i面积} + a_2x_{i楼层} + a_3x_{i房龄} + a_4x_{i公交距离} + b - y_i)^2

此时需要分别计算偏导数,计算各个分量对损失的影响:

Ja1=118i=118(a1xi面积+a2xi楼层+a3xi房龄+a4xi公交距离+byi)xi面积Ja2=118i=118(a1xi面积+a2xi楼层+a3xi房龄+a4xi公交距离+byi)xi楼层Ja3=118i=118(a1xi面积+a2xi楼层+a3xi房龄+a4xi公交距离+byi)xi房龄Ja4=118i=118(a1xi面积+a2xi楼层+a3xi房龄+a4xi公交距离+byi)xi公交距离Jb=118i=118(a1xi面积+a2xi楼层+a3xi房龄+a4xi公交距离+byi)1\begin{aligned} \frac{\partial J}{\partial a_1} &= \frac{1}{18}\sum_{i=1}^{18}(a_1x_{i\text{面积}} + a_2x_{i\text{楼层}} + a_3x_{i\text{房龄}} + a_4x_{i\text{公交距离}} + b - y_i) \cdot x_{i\text{面积}} \\ \frac{\partial J}{\partial a_2} &= \frac{1}{18}\sum_{i=1}^{18}(a_1x_{i\text{面积}} + a_2x_{i\text{楼层}} + a_3x_{i\text{房龄}} + a_4x_{i\text{公交距离}} + b - y_i) \cdot x_{i\text{楼层}} \\ \frac{\partial J}{\partial a_3} &= \frac{1}{18}\sum_{i=1}^{18}(a_1x_{i\text{面积}} + a_2x_{i\text{楼层}} + a_3x_{i\text{房龄}} + a_4x_{i\text{公交距离}} + b - y_i) \cdot x_{i\text{房龄}} \\ \frac{\partial J}{\partial a_4} &= \frac{1}{18}\sum_{i=1}^{18}(a_1x_{i\text{面积}} + a_2x_{i\text{楼层}} + a_3x_{i\text{房龄}} + a_4x_{i\text{公交距离}} + b - y_i) \cdot x_{i\text{公交距离}} \\ \frac{\partial J}{\partial b} &= \frac{1}{18}\sum_{i=1}^{18}(a_1x_{i\text{面积}} + a_2x_{i\text{楼层}} + a_3x_{i\text{房龄}} + a_4x_{i\text{公交距离}} + b - y_i) \cdot 1 \end{aligned}

将所有偏导数放置在一起,形成一个向量:

J=[Ja1Ja2Ja3Ja4Jb]\nabla J= \begin{bmatrix} \frac{\partial J}{\partial a_1} \frac{\partial J}{\partial a_2} \frac{\partial J}{\partial a_3} \frac{\partial J}{\partial a_4} \frac{\partial J}{\partial b} \end{bmatrix}

该向量称为梯度,梯度可理解为:在当前参数位置,损失函数上升最快的方向。既然梯度指示了损失函数上升的方向,那么如果想要损失最小,就要往负方向走,也即:梯度下降。我们随机给参数赋予初始值,并不断计算梯度,不断往下移动,更新参数,最终实现损失函数的最小化,更新方式如下:

参数新值 = 参数旧值 - 学习率 x 梯度

对于参数a1a_1,可写成:a1:=a1ηJa1a_1 := a_1 - \eta \frac{\partial J}{\partial a_1}。其中 η\eta 表示学习率,用来控制每次更新参数时走多大一步。

同理,所有参数都可以按照类似方式更新:

a1:=a1ηJa1a2:=a2ηJa2a3:=a3ηJa3a4:=a4ηJa4b:=bηJb\begin{aligned} a_1 &:= a_1-\eta\frac{\partial J}{\partial a_1}\\ a_2 &:= a_2-\eta\frac{\partial J}{\partial a_2}\\ a_3 &:= a_3-\eta\frac{\partial J}{\partial a_3}\\ a_4 &:= a_4-\eta\frac{\partial J}{\partial a_4}\\ b &:= b-\eta\frac{\partial J}{\partial b} \end{aligned}

不过,上面的写法仍然比较繁琐。既然我们已经知道这些参数本质上是一组数,那么就可以把它们写成向量形式。

令参数向量为:

w=[a1a2a3a4]\mathbf{w}= \begin{bmatrix} a_1 & a_2 & a_3 & a_4 \end{bmatrix}

第 (i) 条样本的特征向量为:

xi=[xi面积xi楼层xi房龄xi公交距离]\mathbf{x}i= \begin{bmatrix} x_{i面积} & x_{i楼层} & x_{i房龄} & x_{i公交距离} \end{bmatrix}

那么第 (i) 条样本的预测结果可以写成:

y^i=wTxi+b\hat{y}_i=\mathbf{w}^T\mathbf{x}_i+b

如果把 18 条样本全部放在一起,可以得到特征矩阵:

X=[x1面积x1楼层x1房龄x1公交距离x2面积x2楼层x2房龄x2公交距离x18面积x18楼层x18房龄x18公交距离]X= \begin{bmatrix} x_{1面积} & x_{1楼层} & x_{1房龄} & x_{1公交距离}\\ x_{2面积} & x_{2楼层} & x_{2房龄} & x_{2公交距离}\\ \vdots & \vdots & \vdots & \vdots\\ x_{18面积} & x_{18楼层} & x_{18房龄} & x_{18公交距离} \end{bmatrix}

真实房价向量为:

y=[y1y2y18]\mathbf{y}= \begin{bmatrix} y_1\\ y_2\\ \vdots\\ y_{18} \end{bmatrix}

预测房价向量为:

y^=[y^1y^2y^18]\hat{\mathbf{y}}= \begin{bmatrix} \hat{y}_1\\ \hat{y}2\\ \vdots\\ \hat{y}{18} \end{bmatrix}

于是,损失函数也可以写成更简洁的向量形式:

12×18Xw+b1y2\frac{1}{2\times18} \left|X\mathbf{w}+b\mathbf{1}-\mathbf{y}\right|^2

因此,参数更新可以写成:

w:=wηJwb:=bηJb\begin{aligned} \mathbf{w}&:=\mathbf{w}-\eta \frac{\partial J}{\partial \mathbf{w}}\\ b&:=b-\eta\frac{\partial J}{\partial b} \end{aligned}

代码实现

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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import font_manager


# =====================================================
# 0. 设置 Matplotlib 中文字体
# =====================================================

def set_chinese_font():
"""
尝试自动设置中文字体。
如果本机没有这些字体,中文可能仍然显示为方块。
"""
candidate_fonts = [
"SimHei", # Windows 常见黑体
"Microsoft YaHei", # Windows 微软雅黑
"PingFang SC", # macOS 苹方
"Heiti SC", # macOS 黑体
"Noto Sans CJK SC", # Linux 常见中文字体
"WenQuanYi Micro Hei", # Linux 文泉驿
"Arial Unicode MS"
]

installed_fonts = {f.name for f in font_manager.fontManager.ttflist}

for font in candidate_fonts:
if font in installed_fonts:
plt.rcParams["font.sans-serif"] = [font]
print(f"已设置中文字体:{font}")
break
else:
print("警告:未找到常见中文字体,图中的中文可能显示为方块。")

# 解决负号显示为方块的问题
plt.rcParams["axes.unicode_minus"] = False


set_chinese_font()


# =====================================================
# 1. 准备数据
# =====================================================

# X 是输入特征矩阵
# 每一行表示一套房子
# 每一列表示一个特征:
# 第 1 列:面积
# 第 2 列:楼层
# 第 3 列:房龄
# 第 4 列:公交距离

X = np.array([
[55, 3, 18, 950],
[60, 5, 15, 800],
[65, 6, 12, 700],
[70, 8, 10, 650],
[75, 10, 8, 600],
[80, 12, 6, 500],
[85, 15, 5, 450],
[90, 16, 4, 400],
[95, 18, 3, 350],
[100, 20, 2, 300],
[72, 4, 20, 1100],
[78, 7, 16, 900],
[88, 9, 12, 750],
[92, 11, 10, 600],
[105, 14, 8, 500],
[110, 17, 6, 450],
[115, 19, 5, 350],
[120, 22, 3, 250]
], dtype=float)

# y 是真实房价,单位:万元
y = np.array([
92, 105, 119, 135, 151, 170, 185, 202, 220,
238, 123, 142, 175, 196, 232, 254, 273, 298
], dtype=float)

# 样本数量 n = 18
n = X.shape[0]

# 特征数量 = 4
num_features = X.shape[1]


# =====================================================
# 2. 特征标准化
# =====================================================
# 为什么要标准化?
#
# 因为不同特征的数值范围差别很大:
# 面积大约是几十到一百多;
# 楼层大约是几到二十几;
# 公交距离大约是几百到一千多。
#
# 如果不标准化,公交距离这一列数值太大,
# 会导致梯度下降过程不稳定,可能震荡或者收敛很慢。
#
# 标准化公式:
#
# x_scaled = (x - mean) / std

X_mean = X.mean(axis=0)
X_std = X.std(axis=0)

X_scaled = (X - X_mean) / X_std


# =====================================================
# 3. 初始化参数
# =====================================================
# 模型形式:
#
# y_hat = a1*x1 + a2*x2 + a3*x3 + a4*x4 + b
#
# 向量形式:
#
# y_hat = Xw + b
#
# 其中:
# w = [a1, a2, a3, a4]

np.random.seed(42)

# 权重向量 w,对应 a1, a2, a3, a4
w = np.random.randn(num_features) * 0.01

# 偏置 b
b = 0.0


# =====================================================
# 4. 设置训练参数
# =====================================================

# 学习率,也就是每次沿着负梯度方向走多大一步
learning_rate = 0.03

# 训练轮数
epochs = 200

# 用来记录每一轮的损失值,方便后面画损失下降曲线
loss_history = []


# =====================================================
# 5. 使用梯度下降训练模型
# =====================================================

for epoch in range(epochs):

# -------------------------------------------------
# 第一步:计算预测值
# -------------------------------------------------
# 对所有样本同时预测:
#
# y_hat = Xw + b
#
# X_scaled 的形状是 18 行 4 列
# w 的形状是 4 行 1 列
# 所以 X_scaled @ w 的结果是 18 个预测值

y_hat = X_scaled @ w + b


# -------------------------------------------------
# 第二步:计算误差
# -------------------------------------------------
# error_i = y_hat_i - y_i
#
# 如果 error_i > 0,说明预测高了;
# 如果 error_i < 0,说明预测低了。

error = y_hat - y


# -------------------------------------------------
# 第三步:计算损失函数
# -------------------------------------------------
# 损失函数:
#
# J = 1 / (2n) * sum((y_hat_i - y_i)^2)
#
# 这里使用 1 / (2n),而不是 1 / n,
# 是为了求导时可以抵消平方项产生的 2。

loss = (1 / (2 * n)) * np.sum(error ** 2)

loss_history.append(loss)


# -------------------------------------------------
# 第四步:计算偏导数,也就是梯度
# -------------------------------------------------
# 对权重 w 的偏导数:
#
# dJ/dw = 1/n * X^T * error
#
# 展开来看就是:
#
# dJ/da1 = 1/n * sum(error_i * x_i面积)
# dJ/da2 = 1/n * sum(error_i * x_i楼层)
# dJ/da3 = 1/n * sum(error_i * x_i房龄)
# dJ/da4 = 1/n * sum(error_i * x_i公交距离)

dw = (1 / n) * (X_scaled.T @ error)

# 对偏置 b 的偏导数:
#
# dJ/db = 1/n * sum(error_i)

db = (1 / n) * np.sum(error)


# -------------------------------------------------
# 第五步:更新参数
# -------------------------------------------------
# 参数新值 = 参数旧值 - 学习率 * 梯度
#
# w := w - learning_rate * dw
# b := b - learning_rate * db

w = w - learning_rate * dw
b = b - learning_rate * db


# 每隔 5000 轮打印一次损失
if epoch % 5000 == 0:
print(f"Epoch: {epoch:5d}, Loss: {loss:.6f}")


# =====================================================
# 6. 训练完成后,查看结果
# =====================================================

print("\n训练完成!")
print("标准化特征下的权重 w:")
print(w)
print(f"标准化特征下的偏置 b:{b:.6f}")

# 使用训练好的参数重新预测
y_pred = X_scaled @ w + b

print("\n真实房价 vs 预测房价:")
for i in range(n):
print(
f"第 {i + 1:2d} 套房子:"
f"真实值 = {y[i]:7.2f} 万元,"
f"预测值 = {y_pred[i]:7.2f} 万元,"
f"误差 = {y_pred[i] - y[i]:7.2f} 万元"
)


# =====================================================
# 7. 将参数还原到原始特征尺度
# =====================================================
# 训练时使用的是标准化后的特征:
#
# X_scaled = (X - X_mean) / X_std
#
# 模型为:
#
# y_hat = X_scaled @ w + b
#
# 展开:
#
# y_hat = ((X - X_mean) / X_std) @ w + b
#
# 可以整理成原始尺度下的形式:
#
# y_hat = X @ w_original + b_original

w_original = w / X_std
b_original = b - np.sum((X_mean / X_std) * w)

feature_names = ["面积", "楼层", "房龄", "公交距离"]

print("\n还原到原始特征尺度后的参数:")
for name, coef in zip(feature_names, w_original):
print(f"{name} 对应的权重:{coef:.6f}")

print(f"原始尺度下的偏置 b:{b_original:.6f}")

print("\n原始尺度下的模型可以写成:")
print(
f"房价 = {w_original[0]:.6f} * 面积 "
f"+ {w_original[1]:.6f} * 楼层 "
f"+ {w_original[2]:.6f} * 房龄 "
f"+ {w_original[3]:.6f} * 公交距离 "
f"+ {b_original:.6f}"
)


# =====================================================
# 8. 计算最终评价指标
# =====================================================

mse = np.mean((y_pred - y) ** 2)
rmse = np.sqrt(mse)
mae = np.mean(np.abs(y_pred - y))

print("\n模型评价指标:")
print(f"MSE = {mse:.6f}")
print(f"RMSE = {rmse:.6f} 万元")
print(f"MAE = {mae:.6f} 万元")


# =====================================================
# 9. 图 1:损失函数下降曲线
# =====================================================
# 这张图用于说明:
# 梯度下降的过程,就是损失函数不断变小的过程。

plt.figure(figsize=(8, 5))
plt.plot(loss_history)
plt.xlabel("训练轮数 Epoch")
plt.ylabel("损失 Loss")
plt.title("梯度下降过程中损失函数的变化")
plt.grid(True)
plt.show()


# =====================================================
# 10. 图 2:真实房价与预测房价散点对比
# =====================================================
# 这张图最适合展示预测效果。
#
# 横坐标:真实房价
# 纵坐标:预测房价
#
# 如果模型预测得很好,那么点应该接近 y = x 这条虚线。

plt.figure(figsize=(6, 6))

plt.scatter(y, y_pred)

min_value = min(y.min(), y_pred.min())
max_value = max(y.max(), y_pred.max())

# y = x 参考线
plt.plot(
[min_value, max_value],
[min_value, max_value],
linestyle="--",
label="理想预测线 y = x"
)

plt.xlabel("真实房价 / 万元")
plt.ylabel("预测房价 / 万元")
plt.title("真实房价与预测房价散点对比")
plt.legend()
plt.grid(True)
plt.show()


# =====================================================
# 11. 图 3:每个样本的预测误差
# =====================================================
# 这张图用于说明:
#
# error = y_hat - y
#
# 误差越接近 0,说明预测越准确。
# 误差大于 0,说明预测偏高。
# 误差小于 0,说明预测偏低。

sample_id = np.arange(1, n + 1)
prediction_error = y_pred - y

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

plt.bar(sample_id, prediction_error)
plt.axhline(0, linestyle="--")

plt.xlabel("样本编号")
plt.ylabel("预测误差 / 万元")
plt.title("每个样本的预测误差")
plt.xticks(sample_id)
plt.grid(axis="y")
plt.show()


# =====================================================
# 12. 图 4:真实房价与预测房价柱状对比
# =====================================================

bar_width = 0.4

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

plt.bar(
sample_id - bar_width / 2,
y,
width=bar_width,
label="真实房价"
)

plt.bar(
sample_id + bar_width / 2,
y_pred,
width=bar_width,
label="预测房价"
)

plt.xlabel("样本编号")
plt.ylabel("房价 / 万元")
plt.title("真实房价与预测房价对比")
plt.xticks(sample_id)
plt.legend()
plt.grid(axis="y")
plt.show()