数模笔记模型篇二

灰色预测 GM(1,1)

灰色系统理论旨在解决小样本贫信息下的不确定性问题。它不追求数据的统计规律,而是通过特定方法处理原始数据,寻找其内在的发展趋势

GM(1,1)是该理论中最基础的模型,其名称含义为:Grey Model of First-order equation in One variable(一阶单变量灰色模型)

其核心操作是累加生成(AGO),即将原始的、可能看似杂乱的数据序列进行累加,生成一个更平滑、趋势性更强的序列,然后对这个新序列建模

为了直观理解,可以将数据预测过程比作观察并预测一棵树的生长:

  • 原始数据 X(0)X^{(0)}: 你每天测量的树的生长量(例如:2cm, 3cm, 2.5cm…)。数据波动较大
  • 累加数据 X(1)X^{(1)}: 树的累计总高度(例如:2cm, 5cm, 7.5cm…)。这条曲线比每日生长量平滑得多
  • GM(1,1)的目标: 通过对相对规律的“总高度”曲线进行建模,来反向推断并预测未来不规律的“每日生长量”

核心步骤

假设原始非负数据序列为 X(0)=(x(0)(1),x(0)(2),,x(0)(n))X^{(0)} = (x^{(0)}(1), x^{(0)}(2), \dots, x^{(0)}(n))

1. 累加生成 (AGO)

目的:削弱原始数据的随机性,使其呈现单调趋势
X(0)X^{(0)} 进行一次累加,得到一阶累加生成序列 X(1)X^{(1)}

x(1)(k)=i=1kx(0)(i)x^{(1)}(k) = \sum_{i=1}^{k} x^{(0)}(i)

  • x(1)(1)=x(0)(1)x^{(1)}(1) = x^{(0)}(1)
  • x(1)(2)=x(0)(1)+x(0)(2)x^{(1)}(2) = x^{(0)}(1) + x^{(0)}(2)

2. 构造背景值 (紧邻均值生成)

目的:为建立微分方程模型做准备,用X(1)X^{(1)}的均值来近似其积分
构造背景值序列 Z(1)Z^{(1)}

z(1)(k)=0.5(x(1)(k)+x(1)(k1)),k=2,3,,nz^{(1)}(k) = 0.5 (x^{(1)}(k) + x^{(1)}(k-1)), \quad k = 2, 3, \dots, n

3. 建立灰色微分方程并求解参数

目的:从离散的数据点中,提炼出能代表整体趋势的核心参数 aabb
灰色微分方程:

x(0)(k)+az(1)(k)=bx^{(0)}(k) + a z^{(1)}(k) = b

参数含义:

  • aa: 发展系数 (Development Coefficient)。决定了趋势发展的强度
  • bb: 灰色作用量 (Grey Action Quantity)。反映了数据的内生驱动力

求解: 使用最小二乘法求解参数列向量 u^=[a,b]T\hat{u} = [a, b]^T

Y=(x(0)(2)x(0)(3)x(0)(n)),B=(z(1)(2)1z(1)(3)1z(1)(n)1)Y = \begin{pmatrix} x^{(0)}(2) \\ x^{(0)}(3) \\ \vdots \\ x^{(0)}(n) \end{pmatrix}, \quad B = \begin{pmatrix} -z^{(1)}(2) & 1 \\ -z^{(1)}(3) & 1 \\ \vdots & \vdots \\ -z^{(1)}(n) & 1 \end{pmatrix}

u^=(BTB)1BTY\hat{u} = (B^T B)^{-1} B^T Y

4. 建立预测模型 (白化方程)

目的:将离散的灰色关系转化为连续的、可求解的微分方程,从而得到一个可以预测未来的“生长曲线”公式

白化方程 (时间的连续函数):

dx(1)dt+ax(1)=b\frac{dx^{(1)}}{dt} + a x^{(1)} = b

求解方程得到预测公式:

x^(1)(k+1)=(x(0)(1)ba)eak+ba\hat{x}^{(1)}(k+1) = \left(x^{(0)}(1) - \frac{b}{a}\right) e^{-ak} + \frac{b}{a}

该公式用于预测累加序列在未来任意时刻的值

5. 累减还原 (IAGO)

目的:将预测出的“累计总高度”还原为我们真正关心的“每日生长量”
对预测出的 X^(1)\hat{X}^{(1)} 序列进行逆向操作

  • x^(0)(1)=x(0)(1)\hat{x}^{(0)}(1) = x^{(0)}(1)
  • x^(0)(k+1)=x^(1)(k+1)x^(1)(k)\hat{x}^{(0)}(k+1) = \hat{x}^{(1)}(k+1) - \hat{x}^{(1)}(k)

这样就得到了原始序列的拟合值与预测值

模型检验

检验类型 检验对象 检验目的 检验性质
级比偏差检验 模型参数 a 决定的理论趋势 与 数据自身的演化趋势 评估模型的内部结构合理性 内部结构检验
残差/后验差检验 最终预测值 x^(0)\hat{x}^{(0)}与 原始真实值 x(0)x^{(0)} 评估模型的最终预测精度 外部精度检验

1. 级比偏差检验

  1. 计算单个级比偏差 ρ(k)\rho(k)

ρ(k)=1(10.5a1+0.5a)1σ(k)\rho(k) = \left| 1 - \left(\frac{1-0.5a}{1+0.5a}\right) \frac{1}{\sigma(k)} \right|

  • aa为模型已求出的发展系数
  • σ(k)=x(0)(k1)x(0)(k)\sigma(k) = \frac{x^{(0)}(k-1)}{x^{(0)}(k)}为原始数据的级比
  1. 计算平均级比偏差 ρˉ\bar{\rho}

ρˉ=1n1k=2nρ(k)\bar{\rho} = \frac{1}{n-1} \sum_{k=2}^{n} \rho(k)

评判标准:

平均级比偏差 ρˉ\bar{\rho} 级别
<0.1< 0.1
<0.2< 0.2 合格
0.2\ge 0.2 不合格

2. 残差检验

目的:直观判断模型对历史数据的拟合程度

  • 相对误差:

Δk=x(0)(k)x^(0)(k)x(0)(k)\Delta_k = \left| \frac{x^{(0)}(k) - \hat{x}^{(0)}(k)}{x^{(0)}(k)} \right|

  • 平均相对精度:

P精度=(11nk=1nΔk)×100%P_{\text{精度}} = \left(1 - \frac{1}{n}\sum_{k=1}^{n} \Delta_k\right) \times 100\%

平均相对精度 级别
> 90%
> 80%
> 70% 合格
< 70% 不合格

3. 后验差检验

目的:更严格地从统计角度评估模型的泛化能力

  1. 计算原始序列方差 S12S_1^2 和残差序列方差 S22S_2^2
  2. 计算后验差比值 C:

C=S2S1C = \frac{S_2}{S_1}

  1. 计算小误差概率 P:

P=Prob(残差(k)残差均值<0.6745S1)P = \text{Prob} (|\text{残差}(k) - \text{残差均值}| < 0.6745 S_1)

根据C值和P值,将模型精度分为四个等级:

精度等级 C 值 P 值
优 (一级) C0.35C \le 0.35 P0.95P \ge 0.95
合格 (二级) C0.50C \le 0.50 P0.80P \ge 0.80
勉强合格 (三级) C0.65C \le 0.65 P0.70P \ge 0.70
不合格 (四级) C>0.65C > 0.65 P<0.70P < 0.70

只有通过后验差检验(至少达到勉强合格)的模型,才能用于外推预测

优缺点与适用场景

优点

  • 样本需求少: 理论上4个数据点即可建模,非常适合数据稀疏的场景
  • 分布要求宽: 不需要数据服从特定的统计分布(如正态分布)
  • 计算简单: 相较于复杂的计量或机器学习模型,计算过程清晰、快捷
  • 短期预测准: 对于具有明显单调(类指数)趋势的序列,短期预测效果好

缺点

  • 适用范围窄: 对波动性强、周期性或无明显趋势的序列效果很差
  • 中长期预测差: 本质是指数拟合,预测期越长,误差累积越严重
  • 模型较敏感: 原始数据的微小变动或新数据的加入可能导致模型参数变化较大

适用场景
适用于数据量有限短期单调趋势预测

  • 社会经济: 能源消耗、粮食产量、人口数量、特定商品价格的短期趋势
  • 管理决策: 新产品销量、网站访问量、库存变化的短期预测
  • 地质水文: 河流径流量、自然灾害发生次数的短期预测

示例代码

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
import numpy as np
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False


class GreyForecast:
"""
GM(1,1)灰色预测模型
该类实现了灰色预测的完整流程,包括级比检验、建模、预测和多维度检验。
"""

def __init__(self, data, n_preds):
"""
初始化模型
:param data: 原始数据序列 (list or numpy array)
:param n_preds: 需要向后预测的期数 (int)
"""
if not isinstance(data, (list, np.ndarray)) or len(data) < 4:
raise ValueError("输入数据必须是列表或Numpy数组,且至少包含4个数据点。")
self.original_data = np.array(data)
self.n_preds = n_preds
self.n = len(data)
self.prediction_results = None

def _level_ratio_test(self, data_series):
"""
执行级比检验
:param data_series: 需要检验的数据序列
:return: (bool, list) 是否通过检验,级比序列
"""
level_ratios = data_series[1:] / data_series[:-1]
lower_bound = np.exp(-2 / (self.n + 1))
upper_bound = np.exp(2 / (self.n + 1))

passed = np.all((level_ratios > lower_bound) & (level_ratios < upper_bound))
return passed, level_ratios

def fit(self):
"""
执行建模与预测全流程
"""
x0 = self.original_data
translation_c = 0

# 1. 建模前级比检验
test_passed, _ = self._level_ratio_test(x0)

if not test_passed:
# 尝试平移变换
translation_c = -x0.min() + 1 # 保证所有数据为正
x0 = x0 + translation_c
test_passed_after_translation, _ = self._level_ratio_test(x0)
if not test_passed_after_translation:
print("警告:原始数据未通过级比检验,且平移变换后仍未通过。模型可能不适用。")

# 2. 累加生成 (AGO)
x1 = np.cumsum(x0)

# 3. 构造背景值 (紧邻均值生成)
z1 = 0.5 * (x1[:-1] + x1[1:])

# 4. 最小二乘法求解参数 a, b
Y = x0[1:].reshape(-1, 1)
B = np.vstack((-z1, np.ones_like(z1))).T

# u = [a, b]^T
u = np.linalg.inv(B.T @ B) @ B.T @ Y
a, b = u.flatten()

# 5. 建立预测模型
total_len = self.n + self.n_preds
x1_fit_pred = np.zeros(total_len)
x1_fit_pred[0] = x0[0]

for k in range(1, total_len):
x1_fit_pred[k] = (x0[0] - b / a) * np.exp(-a * k) + b / a

# 6. 累减还原 (IAGO)
x0_fit_pred = np.zeros(total_len)
x0_fit_pred[0] = x0[0]
x0_fit_pred[1:] = x1_fit_pred[1:] - x1_fit_pred[:-1]

# 7. 如果进行了平移,进行逆变换
if translation_c > 0:
x0_fit_pred -= translation_c

# 8. 分离拟合值和预测值
fitted_values = x0_fit_pred[:self.n]
predicted_values = x0_fit_pred[self.n:]

# 9. 模型检验
tests = self._run_post_tests(self.original_data, fitted_values, a)

self.prediction_results = {
"parameters": {"a": a, "b": b},
"translation_c": translation_c,
"fitted_values": fitted_values,
"predicted_values": predicted_values,
"tests": tests
}
return self.prediction_results

def _run_post_tests(self, original, fitted, a):
"""
执行所有建模后的检验
"""
# --- 残差检验 ---
residuals = original - fitted
relative_errors = np.abs(residuals / original)
mean_relative_error = np.mean(relative_errors)
accuracy = 1 - mean_relative_error

# --- 后验差检验 ---
s1_sq = np.var(original, ddof=1)
s2_sq = np.var(residuals, ddof=1)
C = np.sqrt(s2_sq / s1_sq)

# 小误差概率P
s1 = np.std(original, ddof=1)
p_count = np.sum(np.abs(residuals - np.mean(residuals)) < 0.6745 * s1)
P = p_count / self.n

# --- 级比偏差检验 ---
_, level_ratios = self._level_ratio_test(self.original_data) # 使用原始数据级比
theory_ratio = (1 - 0.5 * a) / (1 + 0.5 * a)
ratio_deviations = np.abs(1 - theory_ratio / level_ratios[1:]) # sigma(k) from k=2
mean_ratio_deviation = np.mean(ratio_deviations)

return {
"residual_test": {
"residuals": residuals,
"relative_errors": relative_errors,
"mean_relative_error": mean_relative_error,
"accuracy": accuracy
},
"posterior_error_test": {
"C_ratio": C,
"P_small_error_prob": P
},
"level_ratio_deviation_test": {
"mean_deviation": mean_ratio_deviation
}
}

def plot(self):
"""
绘制结果图
"""
if self.prediction_results is None:
print("请先调用 .fit() 方法进行建模。")
return

original_time_axis = np.arange(1, self.n + 1)
fitted_time_axis = np.arange(1, self.n + 1)
predicted_time_axis = np.arange(self.n + 1, self.n + self.n_preds + 1)

plt.figure(figsize=(12, 7))
plt.plot(original_time_axis, self.original_data, 'o-', label='原始数据 (Original Data)', color='blue')
plt.plot(fitted_time_axis, self.prediction_results['fitted_values'], 's--', label='拟合数据 (Fitted Data)',
color='green')
plt.plot(predicted_time_axis, self.prediction_results['predicted_values'], '^-',
label='预测数据 (Predicted Data)', color='red')

plt.title('GM(1,1) 灰色预测模型结果')
plt.xlabel('时间序列 (Time Series)')
plt.ylabel('数值 (Value)')
plt.legend()
plt.grid(True, linestyle='--', alpha=0.6)
plt.show()

def print_results(self):
"""
格式化打印所有结果
"""
if self.prediction_results is None:
print("请先调用 .fit() 方法进行建模。")
return

res = self.prediction_results
print("\n" + "=" * 50)
print("GM(1,1) 灰色预测模型结果报告")
print("=" * 50)

# 添加级比检验结果打印
print("\n[1] 级比检验结果")
original_passed, original_ratios = self._level_ratio_test(self.original_data)
lower_bound = np.exp(-2 / (self.n + 1))
upper_bound = np.exp(2 / (self.n + 1))

print(f" - 级比检验区间: ({lower_bound:.4f}, {upper_bound:.4f})")
print(f" - 原始数据级比检验: {'通过' if original_passed else '不通过'}")
print(f" - 级比序列: {[f'{ratio:.4f}' for ratio in original_ratios]}")

if res['translation_c'] > 0:
transformed_data = self.original_data + res['translation_c']
transformed_passed, transformed_ratios = self._level_ratio_test(transformed_data)
print(f" - 平移变换后级比检验: {'通过' if transformed_passed else '不通过'}")
print(f" - 平移后级比序列: {[f'{ratio:.4f}' for ratio in transformed_ratios]}")

print("\n[2] 模型参数")
print(f" - 发展系数 a = {res['parameters']['a']:.4f}")
print(f" - 灰色作用量 b = {res['parameters']['b']:.4f}")
if res['translation_c'] > 0:
print(f" - 平移变换常数 c = {res['translation_c']:.4f}")

print("\n[3] 拟合与预测值")
for i, val in enumerate(res['fitted_values']):
print(f" - 第 {i + 1} 期 (拟合值): {val:.4f}")
for i, val in enumerate(res['predicted_values']):
print(f" - 第 {self.n + i + 1} 期 (预测值): {val:.4f}")

print("\n[4] 模型检验结果")
# 残差检验
acc = res['tests']['residual_test']['accuracy']
print(f"\n--- 4.1 残差检验 ---")
print(f" - 平均相对误差: {1 - acc:.4%}")
print(f" - 模型精度: {acc:.4%}")

# 后验差检验
C = res['tests']['posterior_error_test']['C_ratio']
P = res['tests']['posterior_error_test']['P_small_error_prob']
c_level = "优" if C <= 0.35 else "合格" if C <= 0.50 else "勉强合格" if C <= 0.65 else "不合格"
p_level = "优" if P >= 0.95 else "合格" if P >= 0.80 else "勉强合格" if P >= 0.70 else "不合格"
print(f"\n--- 4.2 后验差检验 ---")
print(f" - 后验差比值 C = {C:.4f} (等级: {c_level})")
print(f" - 小误差概率 P = {P:.4f} (等级: {p_level})")

# 级比偏差检验
rho = res['tests']['level_ratio_deviation_test']['mean_deviation']
rho_level = "优" if rho < 0.1 else "合格" if rho < 0.2 else "不合格"
print(f"\n--- 4.3 级比偏差检验 ---")
print(f" - 平均级比偏差 = {rho:.4f} (等级: {rho_level})")

print("\n" + "=" * 50)


# ==============================================================================
# 示例用法
# ==============================================================================
if __name__ == '__main__':
# 示例1:一组典型的增长数据
print("--- 示例 1:典型增长数据 ---")
# 数据来源:某地区财政收入,单位:亿元
raw_data_1 = [71.8, 80.6, 96.5, 108.3, 118.9, 130.1]
# 建立模型,向后预测3期
gm_model_1 = GreyForecast(raw_data_1, n_preds=3)
# 执行建模
gm_model_1.fit()
# 打印结果报告
gm_model_1.print_results()
# 绘制图表
gm_model_1.plot()

# 示例2:一组波动较大,可能需要平移的数据
print("\n\n--- 示例 2:波动较大,不一定适用的示例 ---")
raw_data_2 = [22, 20, 25, 28, 26, 30, 34, 32]
# 建立模型,向后预测4期
gm_model_2 = GreyForecast(raw_data_2, n_preds=4)
gm_model_2.fit()
gm_model_2.print_results()
gm_model_2.plot()

ARIMA 时间序列模型

ARIMA模型,全称为差分整合移动平均自回归模型 (Autoregressive Integrated Moving Average Model),是应用统计学中进行时间序列分析和预测的基石模型。它的核心思想是,一个时间序列中的数值变化规律可以通过其自身过去的值、过去的预测误差以及这两者的线性组合来捕捉和描述

该模型由三个核心参数(p, d, q)定义,每个字母都代表了模型的一个关键组成部分:

  • p (Autoregressive - AR): 自回归阶数,指当前值与过去多少个观测值直接相关
  • d (Integrated - I): 差分阶数,指为使序列变得平稳所需要进行的差分运算次数
  • q (Moving Average - MA): 移动平均阶数,指当前值与过去多少个预测误差直接相关

平稳性 (Stationarity)与差分

平稳性是ARIMA模型应用的先决条件。一个平稳的时间序列,其统计特性不随时间的推移而改变。具体来说,它应满足:

  1. 均值恒定: 序列的平均水平不随时间改变
  2. 方差恒定: 序列的波动幅度不随时间改变(无异方差)
  3. 自协方差不依赖于时间: 序列在任意两个时间点tt-k的关联性只取决于时间间隔k,而与t的位置无关

只有当序列的统计规律不随时间变化时,我们才能相信从历史数据中学到的模式(如均值、方差、自相关性)在未来同样适用,从而使预测成为可能

差分是处理非平稳性的主要手段,其操作由参数d差分次数控制,具体为计算相邻观测值之差,一阶差分Yt=YtYt1Y'_t = Y_t - Y_{t-1}通常能消除线性趋势

判断是否平稳的方法:

  1. 视觉判断: 观察时序图,如果存在明显趋势或季节性,则序列非平稳。
  2. 统计检验: 使用增强迪基-福勒检验 (Augmented Dickey-Fuller Test, ADF检验)
    • 原假设 (H₀): 序列存在单位根,即非平稳
    • 检验目标: 我们希望检验结果的p值小于0.05,这样就可以拒绝原假设,认为序列是平稳的

即对原始序列进行ADF检验。若不平稳,则进行差分,再对差分后的序列进行检验。重复此过程,直到检验通过。总共的差分次数就是d的值,d通常不超过2

模型构成

在序列通过差分变得平稳后,我们用AR和MA部分来对其中的动态结构进行建模

AR(pp) - 自回归部分

描述当前值与过去值之间的线性关系,即“系统的记忆”

数学形式:

Yt=c+ϕ1Yt1+ϕ2Yt2++ϕpYtp+ϵtY_t = c + \phi_1 Y_{t-1} + \phi_2 Y_{t-2} + \dots + \phi_p Y_{t-p} + \epsilon_t

其中 ϕ\phi 是自回归系数,pp 是阶数

偏自相关函数图 (PACF):衡量了在剔除中间所有滞后项的传递影响后,YtY_tYtkY_{t-k}之间纯粹的、直接的相关性
对于一个纯AR(pp)过程,其PACF图会在滞后第pp阶后出现清晰的截尾(相关系数突然跌落至置信区间内并保持在其中)

MA(qq) - 移动平均部分

描述当前值与过去预测误差(随机冲击)之间的关系,即“冲击的记忆”。它不是指滚动的平均值

数学形式:

Yt=μ+ϵt+θ1ϵt1+θ2ϵt2++θqϵtqY_t = \mu + \epsilon_t + \theta_1 \epsilon_{t-1} + \theta_2 \epsilon_{t-2} + \dots + \theta_q \epsilon_{t-q}

其中 θ\theta 是移动平均系数,qq 是阶数

自相关函数图 (ACF):衡量了YtY_tYtkY_{t-k}之间所有的(直接和间接的)相关性
对于一个纯MA(qq)过程,其ACF图会在滞后第qq阶后出现清晰的截尾

核心步骤

Box-Jenkins方法论是一个由四个步骤构成的、科学且迭代的建模框架

1. 模型识别 (Identification)

确定最合适的模型阶数(p,d,q)(p, d, q)

  1. dd: 通过ADF检验确定差分次数
  2. pp, qq: 对差分后的平稳序列,绘制ACF和PACF图
    • PACF截尾,ACF拖尾 -> 考虑AR(pp)模型,即ARIMA(p,d,0)(p, d, 0)
    • ACF截尾,PACF拖尾 -> 考虑MA(qq)模型,即ARIMA(0,d,q)(0, d, q)
    • ACF和PACF均拖尾 -> 考虑ARMA(p,qp,q)模型,即ARIMA(p,d,q)(p, d, q)

信息准则辅助: 当图形特征不明显时,使用AIC(赤池信息量准则)BIC(贝叶斯信息量准则),但是需要在模型的拟合优度与复杂度之间做权衡,我们倾向于选择AIC或BIC值最小的模型,BIC对模型复杂度的惩罚更重,因此更倾向于推荐简约模型

一般BIC效果更好,但是不能盲目根据AIC或BIC的结果之间确定模型,还应结合PACF和ACF图形进行判断

2. 参数估计 (Estimation)

在确定了模型结构(p,d,q)(p,d,q)后,计算出所有未知参数(ϕ\phi, θ\theta, cc等)的具体数值

最大似然估计 (Maximum Likelihood Estimation, MLE)

假定模型的误差项ϵt\epsilon_t服从正态分布,MLE会寻找一组能使我们观测到的这组真实数据出现的概率(似然)达到最大的参数值,这是一个由计算机执行的复杂数值优化过程

3. 模型诊断 (Diagnostic Checking)

对已估计好的模型进行“质检”,确保其充分捕捉了数据信息,通过检验模型的残差 (Residuals =真实值 - 预测值) 是否满足白噪声的假定

诊断方法:

  1. 残差时序图: 检查残差是否围绕0随机波动,且方差恒定
  2. 残差ACF图: 检查残差是否存在自相关。理想情况下,所有滞后阶数(lag>0)的柱子都应在置信区间内
  3. Ljung-Box Q检验: 对残差的自相关性进行正式的统计检验
    • 原假设(H0H_0): 残差序列不存在自相关性
    • 判决: 我们希望检验的p值 > 0.05。一个大的p值意味着模型通过检验,残差是随机的
  4. 残差直方图/Q-Q图: 检查残差是否服从正态分布,这对于预测区间的可靠性至关重要

迭代循环: 若诊断检验失败(如Ljung-Box检验p值很小),必须返回第一步,重新识别模型(例如调整ppqq的值),然后再次进行估计和诊断,直至模型通过检验

4. 模型应用与预测 (Application & Forecasting)

使用通过所有检验的最终模型来预测未来的数值,预测结果通常包含一个点预测(最可能的值)和一个预测区间(一个值范围)

随着预测时间向未来延伸,不确定性会累积,导致预测区间越来越宽。

ARIMA的局限与扩展模型

模型局限性

  • 线性关系: 只能捕捉线性规律,对于复杂的非线性模式无能为力
  • 单变量: 传统ARIMA无法整合外部信息(如天气、政策、其他经济指标等)
  • 常数方差: 假设误差的波动性是恒定的,不适用于金融市场等波动性聚集的场景

扩展模型

  1. ARIMAX (ARIMA with eXogenous variables): 在ARIMA模型中加入了外生解释变量,使得模型可以利用外部信息进行预测
  2. GARCH族模型: 用于处理时变的波动性(异方差),常与ARIMA模型结合使用,先用ARIMA对收益率建模,再用GARCH对残差的波动性建模
  3. SARIMA (Seasonal ARIMA): ARIMA(p,d,q)(P,D,Q)m(p,d,q)(P,D,Q)_m,在ARIMA基础上增加了季节性参数,专门用于处理具有固定周期(如年度、季度)的数据

SARIMA:

基础ARIMA模型难以处理具有明显**季节性(Seasonality)**规律的时间序列。季节性指的是数据中以固定频率(如每年、每季度、每周)重复出现的模式。例如,零售业销售额通常在每年年底出现高峰

SARIMA模型的核心思想可以理解为两个ARIMA模型的巧妙结合:一个用于描述短期非季节性动态,另一个用于描述长期季节性动态

它引入了四个新的季节性参数

  • mm (Seasonal Period): 季节性周期长度。这是最重要的参数,它定义了“季节”的长度。对于月度数据,m=12m=12;对于季度数据,m=4m=4
  • PP (Seasonal AR Order): 季节性自回归阶数。表示当前值与过去季节的对应值(如YtmY_{t-m}, Yt2mY_{t-2m})之间的关系
  • DD (Seasonal Differencing Order): 季节性差分阶数。用于消除季节性趋势。其操作为:Yt=YtYtmY'_t = Y_t - Y_{t-m}。例如,用今年1月的数据减去去年1月的数据
  • QQ (Seasonal MA Order): 季节性移动平均阶数。表示当前值与过去季节的预测误差(如ϵtm\epsilon_{t-m}, ϵt2m\epsilon_{t-2m})之间的关系

一个完整的SARIMA模型表示为:

SARIMA(p,d,q)(P,D,Q)m\text{SARIMA}(p,d,q)(P,D,Q)_m

建模过程与Box-Jenkins方法论类似,但在识别阶段需要特别关注:

  1. 观察原始序列的ACF图,如果在大间隔的滞后(如m,2m,3m,m, 2m, 3m, \dots)上有显著的峰值,则强烈暗示存在季节性
  2. 先进行季节性差分(定DD),再进行普通差分(定dd),直至序列平稳
  3. 在平稳序列的ACF/PACF图上,观察滞后m,2m,m, 2m, \dots处的模式来确定PPQQ,同时观察短期滞后(1, 2, 3, …)处的模式来确定ppqq

代码示例

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
# -*- coding: utf-8 -*-
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import statsmodels.api as sm
from statsmodels.tsa.stattools import adfuller
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
from statsmodels.tsa.statespace.sarimax import SARIMAX
import pmdarima as pm

import warnings

# This will specifically ignore the FutureWarning related to 'force_all_finite'
warnings.filterwarnings('ignore', category=FutureWarning)

# 全局设置,用于解决Matplotlib中文显示问题
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False


# --- 步骤 1: 数据加载与准备 ---

# 为了代码的可运行性,我们首先创建一个模拟数据集。
def create_sample_data():
"""
创建一个带线性趋势和标准年度(12个月)季节性的模拟时间序列数据。
"""
# 设置随机种子以保证结果可复现
np.random.seed(42)

# 10年的月度数据,共120个样本
n_samples = 120

# 创建日期范围 (2010-01-01 到 2019-12-01)
dates = pd.date_range(start='2010-01-01', periods=n_samples, freq='MS')

# 1. 创建线性趋势项
trend = np.linspace(0, 50, n_samples)

# 2. 创建季节性项 (核心修正)
# 为了在10年(120个月)内有10个完整的周期,角度需要从0变化到 10 * 2 * pi
n_years = 10
seasonal = 15 * np.sin(np.linspace(0, n_years * 2 * np.pi, n_samples))

# 3. 创建随机噪声项
noise = np.random.normal(0, 5, n_samples)

# 4. 合成数据 (趋势 + 季节性 + 噪声 + 基础值)
data = 50 + trend + seasonal + noise

# 创建一个带日期索引的Pandas Series,这更符合时间序列分析的习惯
ts_data = pd.Series(data, index=dates, name='value')

return ts_data

# 使用模拟数据
ts_data = create_sample_data()

# 【竞赛中请替换为您的数据加载代码】
# 例如:
# df = pd.read_csv('您的数据.csv')
# df['date_column'] = pd.to_datetime(df['date_column']) # 转换日期列
# df = df.set_index('date_column') # 将日期列设为索引
# ts_data = df['your_value_column']

print("--- 数据预览 ---")
print(ts_data.head())
print("\n")

# 可视化原始数据
plt.figure(figsize=(12, 6))
plt.plot(ts_data, label='原始数据')
plt.title('原始时间序列图')
plt.xlabel('日期')
plt.ylabel('数值')
plt.legend()
plt.grid(True)
plt.show()


# --- 步骤 2: 平稳性检验 (确定差分阶数d) ---

def adf_test(timeseries):
"""
执行ADF检验并打印结果。
ADF检验的原假设(H0)是:序列存在单位根,即非平稳。
如果p-value < 0.05,我们拒绝原假设,认为序列是平稳的。
"""
print('--- ADF检验结果 ---')
# adfuller函数返回多个值,我们主要关心第一个(ADF统计量)和第二个(p-value)
result = adfuller(timeseries, autolag='AIC')
print(f'ADF 统计量: {result[0]}')
print(f'p-value: {result[1]}')
print('临界值:')
for key, value in result[4].items():
print(f'\t{key}: {value}')

if result[1] <= 0.05:
print("结论: 序列是平稳的 (拒绝原假设)")
else:
print("结论: 序列是非平稳的 (无法拒绝原假设)")


print("--- 对原始序列进行平稳性检验 ---")
adf_test(ts_data)
print("\n")

# --- 步骤 2.5: ACF和PACF图 (用于模型识别) ---

# ACF和PACF图是识别ARIMA模型参数的重要工具
# ACF(自相关函数)用于识别MA(q)参数
# PACF(偏自相关函数)用于识别AR(p)参数

print("--- 绘制ACF和PACF图进行模型识别 ---")

# 创建子图 - 改为3行2列布局
fig, axes = plt.subplots(3, 2, figsize=(15, 15))

# 原始序列的ACF和PACF
plot_acf(ts_data, ax=axes[0, 0], lags=40, title='原始序列 - 自相关函数(ACF)')
plot_pacf(ts_data, ax=axes[0, 1], lags=40, title='原始序列 - 偏自相关函数(PACF)')

# 一阶差分序列的ACF和PACF
ts_diff = ts_data.diff().dropna()
plot_acf(ts_diff, ax=axes[1, 0], lags=40, title='一阶差分序列 - 自相关函数(ACF)')
plot_pacf(ts_diff, ax=axes[1, 1], lags=40, title='一阶差分序列 - 偏自相关函数(PACF)')

# 二阶差分序列的ACF和PACF
ts_diff2 = ts_data.diff().diff().dropna()
plot_acf(ts_diff2, ax=axes[2, 0], lags=40, title='二阶差分序列 - 自相关函数(ACF)')
plot_pacf(ts_diff2, ax=axes[2, 1], lags=40, title='二阶差分序列 - 偏自相关函数(PACF)')

plt.tight_layout()
plt.show()

# 对一阶差分序列进行平稳性检验
print("--- 对一阶差分序列进行平稳性检验 ---")
adf_test(ts_diff)
print("\n")

# 对二阶差分序列进行平稳性检验
print("--- 对二阶差分序列进行平稳性检验 ---")
adf_test(ts_diff2)
print("\n")

# --- 步骤 3: 模型识别与拟合 (自动调参) ---

# 使用auto_arima自动模型选择
print("--- 使用auto_arima自动寻找最优模型 ---")
model_fit = pm.auto_arima(ts_data,
start_p=1, start_q=1,
test='adf', # 使用adf检验来确定d
max_p=8, max_q=8, # p和q的最大值
m=12, # 季节性周期
d=1, # 差分阶数,None表示自动选择
seasonal=True, # 开启季节性
start_P=0,
D=None, # 季节性差分阶数,None表示自动选择
trace=True, # 打印尝试的每个模型的信息
error_action='ignore',
suppress_warnings=True,
stepwise=True, # 逐步搜索,速度更快,但会陷入局部最优解
information_criterion='bic')

# 注释掉手动指定的SARIMAX模型
# print("--- 使用手动指定的SARIMAX模型 ---")
#
# # 创建SARIMAX模型
# model = SARIMAX(ts_data,
# order=(0, 1, 1),
# seasonal_order=(0, 1, 1, 30),
# enforce_stationarity=False,
# enforce_invertibility=False)

# auto_arima返回的是已经拟合好的模型,不需要再调用fit()
# model_fit = model.fit(disp=False)

print("\n--- 自动选择的最优模型摘要 ---")
print(model_fit.summary())
print("\n")

# --- 步骤 4: 模型诊断 (检验残差是否为白噪声) ---

# 模型诊断是至关重要的一步,用于检验模型的充分性。
# 一个好的模型,其残差应该像白噪声,即没有自相关性。
print("--- 模型诊断 ---")
# plot_diagnostics会生成四张图,全方位诊断模型残差
# 1. 标准化残差时序图: 残差应围绕0随机波动,无明显模式。
# 2. 直方图+核密度估计: 残差分布应接近正态分布(钟形)。
# 3. Q-Q图: 点应基本落在红线上,表示残差是正态分布的。
# 4. 相关图(ACF): 残差的自相关系数不应有任何一个显著地超出置信区间(蓝色区域),这说明残差中没有剩余的自相关信息。Ljung-Box检验的p值(Prob(Q))应大于0.05。
model_fit.plot_diagnostics(figsize=(15, 12))
plt.suptitle('模型诊断图', fontsize=16, y=0.92)
plt.show()

# --- 步骤 5: 预测与可视化 ---

# 预测未来N个时间点
n_periods = 24 # 预测未来24个月
forecast, forecast_ci = model_fit.predict(n_periods=n_periods, return_conf_int=True)

# 创建未来的日期索引
future_index = pd.date_range(start=ts_data.index[-1] + pd.DateOffset(months=1), periods=n_periods, freq='MS')

# 将预测结果和置信区间转换为DataFrame
forecast_df = pd.DataFrame(forecast, index=future_index, columns=['预测值'])
conf_int_df = pd.DataFrame(forecast_ci, index=future_index, columns=['置信下限', '置信上限'])

print("--- 未来24期预测结果 ---")
print(forecast_df)
print("\n--- 置信区间 ---")
print(conf_int_df)
print("\n")

# 可视化预测结果
plt.figure(figsize=(15, 7))
# 绘制历史数据
plt.plot(ts_data, label='历史数据')
# 绘制预测数据
plt.plot(forecast_df, label='预测数据', color='red')
# 绘制置信区间
plt.fill_between(conf_int_df.index,
conf_int_df.iloc[:, 0],
conf_int_df.iloc[:, 1], color='pink', alpha=0.5, label='95%置信区间')

plt.title('ARIMA模型预测结果', fontsize=16)
plt.xlabel('日期')
plt.ylabel('数值')
plt.legend(loc='upper left')
plt.grid(True)
plt.show()

输出结果:

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
--- 数据预览 ---
2010-01-01 52.483571
2010-02-01 57.285931
2010-03-01 67.134656
2010-04-01 73.874347
2010-05-01 63.366282
Freq: MS, Name: value, dtype: float64


--- 对原始序列进行平稳性检验 ---
--- ADF检验结果 ---
ADF 统计量: 0.7525087015362423
p-value: 0.9908295754603504
临界值:
1%: -3.4936021509366793
5%: -2.8892174239808703
10%: -2.58153320754717
结论: 序列是非平稳的 (无法拒绝原假设)


--- 绘制ACF和PACF图进行模型识别 ---
--- 对一阶差分序列进行平稳性检验 ---
--- ADF检验结果 ---
ADF 统计量: -7.690443279835323
p-value: 1.4239687239875478e-11
临界值:
1%: -3.4936021509366793
5%: -2.8892174239808703
10%: -2.58153320754717
结论: 序列是平稳的 (拒绝原假设)


--- 对二阶差分序列进行平稳性检验 ---
--- ADF检验结果 ---
ADF 统计量: -9.512657488015064
p-value: 3.2183455076903586e-16
临界值:
1%: -3.4948504603223145
5%: -2.889758398668639
10%: -2.5818220155325444
结论: 序列是平稳的 (拒绝原假设)


--- 使用auto_arima自动寻找最优模型 ---
Performing stepwise search to minimize bic
ARIMA(1,1,1)(0,0,1)[12] intercept : BIC=853.299, Time=0.12 sec
ARIMA(0,1,0)(0,0,0)[12] intercept : BIC=853.166, Time=0.01 sec
ARIMA(1,1,0)(1,0,0)[12] intercept : BIC=837.517, Time=0.06 sec
ARIMA(0,1,1)(0,0,1)[12] intercept : BIC=849.036, Time=0.07 sec
ARIMA(0,1,0)(0,0,0)[12] : BIC=848.701, Time=0.01 sec
ARIMA(1,1,0)(0,0,0)[12] intercept : BIC=857.678, Time=0.02 sec
ARIMA(1,1,0)(2,0,0)[12] intercept : BIC=830.314, Time=0.18 sec
ARIMA(1,1,0)(2,0,1)[12] intercept : BIC=818.295, Time=0.33 sec
ARIMA(1,1,0)(1,0,1)[12] intercept : BIC=813.706, Time=0.14 sec
ARIMA(1,1,0)(0,0,1)[12] intercept : BIC=848.818, Time=0.06 sec
ARIMA(1,1,0)(1,0,2)[12] intercept : BIC=818.221, Time=0.33 sec
ARIMA(1,1,0)(0,0,2)[12] intercept : BIC=848.207, Time=0.17 sec
ARIMA(1,1,0)(2,0,2)[12] intercept : BIC=inf, Time=0.63 sec
ARIMA(0,1,0)(1,0,1)[12] intercept : BIC=inf, Time=0.22 sec
ARIMA(2,1,0)(1,0,1)[12] intercept : BIC=814.285, Time=0.14 sec
ARIMA(1,1,1)(1,0,1)[12] intercept : BIC=inf, Time=0.54 sec
ARIMA(0,1,1)(1,0,1)[12] intercept : BIC=inf, Time=0.25 sec
ARIMA(2,1,1)(1,0,1)[12] intercept : BIC=inf, Time=0.37 sec
ARIMA(1,1,0)(1,0,1)[12] : BIC=809.029, Time=0.11 sec
ARIMA(1,1,0)(0,0,1)[12] : BIC=844.285, Time=0.04 sec
ARIMA(1,1,0)(1,0,0)[12] : BIC=832.892, Time=0.03 sec
ARIMA(1,1,0)(2,0,1)[12] : BIC=813.618, Time=0.27 sec
ARIMA(1,1,0)(1,0,2)[12] : BIC=813.544, Time=0.24 sec
ARIMA(1,1,0)(0,0,0)[12] : BIC=853.190, Time=0.01 sec
ARIMA(1,1,0)(0,0,2)[12] : BIC=843.650, Time=0.11 sec
ARIMA(1,1,0)(2,0,0)[12] : BIC=825.668, Time=0.11 sec
ARIMA(1,1,0)(2,0,2)[12] : BIC=inf, Time=0.51 sec
ARIMA(0,1,0)(1,0,1)[12] : BIC=inf, Time=0.22 sec
ARIMA(2,1,0)(1,0,1)[12] : BIC=809.609, Time=0.12 sec
ARIMA(1,1,1)(1,0,1)[12] : BIC=790.425, Time=0.15 sec
ARIMA(1,1,1)(0,0,1)[12] : BIC=848.767, Time=0.07 sec
ARIMA(1,1,1)(1,0,0)[12] : BIC=817.402, Time=0.08 sec
ARIMA(1,1,1)(2,0,1)[12] : BIC=795.052, Time=0.26 sec
ARIMA(1,1,1)(1,0,2)[12] : BIC=794.988, Time=0.29 sec
ARIMA(1,1,1)(0,0,0)[12] : BIC=857.412, Time=0.02 sec
ARIMA(1,1,1)(0,0,2)[12] : BIC=848.417, Time=0.22 sec
ARIMA(1,1,1)(2,0,0)[12] : BIC=804.015, Time=0.22 sec
ARIMA(1,1,1)(2,0,2)[12] : BIC=799.983, Time=0.40 sec
ARIMA(0,1,1)(1,0,1)[12] : BIC=788.774, Time=0.09 sec
ARIMA(0,1,1)(0,0,1)[12] : BIC=844.499, Time=0.04 sec
ARIMA(0,1,1)(1,0,0)[12] : BIC=816.103, Time=0.06 sec
ARIMA(0,1,1)(2,0,1)[12] : BIC=793.108, Time=0.20 sec
ARIMA(0,1,1)(1,0,2)[12] : BIC=792.958, Time=0.25 sec
ARIMA(0,1,1)(0,0,0)[12] : BIC=853.266, Time=0.01 sec
ARIMA(0,1,1)(0,0,2)[12] : BIC=843.923, Time=0.14 sec
ARIMA(0,1,1)(2,0,0)[12] : BIC=799.897, Time=0.15 sec
ARIMA(0,1,1)(2,0,2)[12] : BIC=inf, Time=0.68 sec
ARIMA(0,1,2)(1,0,1)[12] : BIC=790.859, Time=0.15 sec
ARIMA(1,1,2)(1,0,1)[12] : BIC=795.032, Time=0.20 sec

Best model: ARIMA(0,1,1)(1,0,1)[12]
Total fit time: 9.120 seconds

--- 自动选择的最优模型摘要 ---
SARIMAX Results
==========================================================================================
Dep. Variable: y No. Observations: 120
Model: SARIMAX(0, 1, 1)x(1, 0, 1, 12) Log Likelihood -384.829
Date: Fri, 18 Jul 2025 AIC 777.657
Time: 15:45:24 BIC 788.774
Sample: 01-01-2010 HQIC 782.171
- 12-01-2019
Covariance Type: opg
==============================================================================
coef std err z P>|z| [0.025 0.975]
------------------------------------------------------------------------------
ma.L1 -0.9053 0.052 -17.516 0.000 -1.007 -0.804
ar.S.L12 0.9892 0.010 98.168 0.000 0.969 1.009
ma.S.L12 -0.6980 0.110 -6.363 0.000 -0.913 -0.483
sigma2 29.9352 4.124 7.258 0.000 21.852 38.019
===================================================================================
Ljung-Box (L1) (Q): 1.43 Jarque-Bera (JB): 0.22
Prob(Q): 0.23 Prob(JB): 0.89
Heteroskedasticity (H): 0.89 Skew: 0.11
Prob(H) (two-sided): 0.72 Kurtosis: 2.98
===================================================================================

Warnings:
[1] Covariance matrix calculated using the outer product of gradients (complex-step).


--- 模型诊断 ---
--- 未来24期预测结果 ---
预测值
2020-01-01 106.265838
...
2021-12-01 109.573949

--- 置信区间 ---
置信下限 置信上限
2020-01-01 95.539342 116.992334
...
2021-12-01 96.825016 122.322882

结果解读:

  1. 数据特性与预处理

原始序列:经ADF检验 (p-value=0.99),原始序列为非平稳时间序列,含有趋势或周期性。
差分处理:进行一阶差分 (d=1) 后,序列变得平稳 (p-value ≈ 1.42e-11),满足了ARIMA建模的前提

事实是在这个示例中差分可以被AR代替

  1. 最优模型选择

通过 auto_arima 自动寻优(基于BIC最小化原则),最终确定的最优模型为 SARIMAX(0, 1, 1)x(1, 0, 1, 12)

这是一个包含一阶差分 (d=1)、一个移动平均项 (q=1) 的模型,还包含了强大的年度季节性结构 (m=12),由一个季节性自回归项 (P=1) 和一个季节性移动平均项 (Q=1) 共同驱动,ar.S.L12 系数 (0.9892) 极接近1,表明数据存在极强的年度周期性

  1. 模型质量评估 (诊断检验)
    • 残差独立性 (Ljung-Box): Prob(Q) (0.23) > 0.05,表明残差为白噪声,模型已充分提取信息
    • 残差正态性 (Jarque-Bera): Prob(JB) (0.89) > 0.05,表明残差服从正态分布
    • 残差方差齐性 (Heteroskedasticity): Prob(H) (0.72) > 0.05,表明残差方差恒定
    • 所有模型系数的p-value均为0.000,统计上极其显著

朴素贝叶斯分类方法

朴素贝叶斯(Naive Bayes)是一种基于贝叶斯定理(Bayes’ Theorem)与特征条件独立性假设(Feature Conditional Independence Assumption)的概率分类算法。它以其简单、高效和在特定领域(尤其是文本分类)表现出奇的准确性而闻名

贝叶斯定理

贝叶斯定理是朴素贝叶斯算法的数学基石,它描述了在给定证据(特征)的情况下,一个假设(类别)的后验概率

其公式为:

P(CX)=P(XC)P(C)P(X)P(C|X) = \frac{P(X|C) \cdot P(C)}{P(X)}

  • P(CX)P(C|X) (后验概率):我们最终的目标,表示在看到特征 XX 后,样本属于类别 CC 的概率
  • P(XC)P(X|C) (似然概率):在类别 CC 的条件下,观察到特征 XX 的概率。这是模型需要从训练数据中学习的部分
  • P(C)P(C) (先验概率):在不考虑任何特征的情况下,类别 CC 发生的概率。可直接从训练数据中统计得出
  • P(X)P(X) (证据因子):特征 XX 发生的概率。在分类决策中,对于所有类别 CCP(X)P(X) 的值都是相同的,因此在比较不同类别的后验概率时,可以忽略此项

分类决策的核心是,计算样本 XX 属于每个类别 CkC_k 的后验概率,并选择概率最大的类别作为预测结果

Predicted Class=argmaxCkP(CkX)argmaxCkP(XCk)P(Ck)\text{Predicted Class} = \arg\max_{C_k} P(C_k|X) \propto \arg\max_{C_k} P(X|C_k) \cdot P(C_k)

“朴素”的特征条件独立性假设

这是该算法“朴素”(Naive)一词的来源。它假设:对于一个给定的类别,所有特征之间是相互独立的,互不影响

在独立性假设下,似然概率 P(XC)P(X|C) 的计算被简化为:

P(XC)=P(x1,x2,...,xnC)=i=1nP(xiC)P(X|C) = P(x_1, x_2, ..., x_n|C) = \prod_{i=1}^{n} P(x_i|C)

其中 X={x1,x2,...,xn}X=\{x_1, x_2, ..., x_n\} 是样本的 nn 个特征

核心步骤

  1. 准备阶段:确定特征属性,并对训练样本进行分类
  2. 训练阶段
    • 计算每个类别的先验概率 P(Ck)P(C_k)
    • 计算每个类别下,每个特征属性的条件概率 P(xiCk)P(x_i|C_k)
  3. 分类阶段
    • 对于一个新样本 XX,带入公式计算它属于每个类别 CkC_k 的后验概率(的分子部分):P(Ck)i=1nP(xiCk)P(C_k) \prod_{i=1}^{n} P(x_i|C_k)
    • 比较计算出的概率值,将样本归类到概率最大的那个类别

零概率问题与拉普拉斯平滑

问题:如果在训练集中,某个特征值在某个类别下从未出现过,会导致其条件概率 P(xiCk)P(x_i|C_k) 为 0。这会使得整个后验概率的乘积变为 0,从而“一票否决”该类别,这是不合理的

拉普拉斯平滑 (Laplacian Smoothing),也称加一平滑。它对概率计算公式进行修正,确保任何概率值都不会为零

P(xiCk)=类别 Ck 中特征 xi 的出现次数+α类别 Ck 的总样本数+αKP(x_i|C_k) = \frac{\text{类别 } C_k \text{ 中特征 } x_i \text{ 的出现次数} + \alpha}{\text{类别 } C_k \text{ 的总样本数} + \alpha \cdot K}

  • α\alpha 是平滑系数,通常取值为 1
  • KK 是特征 xix_i 的可能取值总数

主要模型类型

根据特征数据的分布类型,朴素贝叶斯主要分为三种模型:

  1. 高斯朴素贝叶斯 (Gaussian Naive Bayes)

    • 适用场景:特征是连续型数据(如身高、体重、温度),并假设这些特征在每个类别下都服从高斯(正态)分布
    • 方法:对每个类别下的每个特征,计算其均值和方差。在预测时,使用高斯概率密度函数来计算条件概率 P(xiCk)P(x_i|C_k)
  2. 多项式朴素贝叶斯 (Multinomial Naive Bayes)

    • 适用场景:特征是离散型数据,通常表示“出现次数”或“频率”
    • 典型应用:文本分类。特征是词汇表中的单词,值是该单词在文档中出现的次数(词频)
  3. 伯努利朴素贝叶斯 (Bernoulli Naive Bayes)

    • 适用场景:特征是二元(布尔)数据,即只关心特征“是否出现”
    • 典型应用:文本分类。特征是词汇表中的单词,值是 1 或 0,表示该单词是否在文档中出现过

    对立的特征可以直接转换成二元数据,如宽->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
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
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import GaussianNB, MultinomialNB, BernoulliNB
from sklearn.metrics import accuracy_score, classification_report
from sklearn.datasets import load_iris
from sklearn.feature_extraction.text import CountVectorizer

# ==============================================================================
# 示例 1: GaussianNB (高斯朴素贝叶斯) - 用于连续特征
# ==============================================================================
print("="*60)
print("示例 1: GaussianNB (高斯朴素贝叶斯) on Iris Dataset")
print("="*60)

# 1. 加载数据集
# scikit-learn 自带了鸢尾花数据集
iris = load_iris()
X = iris.data # 特征数据
y = iris.target # 标签(分类结果)

# 将数据转换成 DataFrame 格式,方便查看
df = pd.DataFrame(X, columns=iris.feature_names)
df['target'] = y
print("--- 数据集预览 (前5行) ---")
print(df.head())
print("\n标签含义: 0='setosa', 1='versicolor', 2='virginica'")

# 2. 划分训练集和测试集
# 将80%的数据用作训练,20%用作测试
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

print(f"\n训练集大小: {X_train.shape}")
print(f"测试集大小: {X_test.shape}")

# 3. 初始化并训练朴素贝叶斯模型
# 因为特征是连续的数值,我们选择高斯朴素贝叶斯 (GaussianNB)
model = GaussianNB()

# 使用训练数据来训练模型
# .fit() 方法会计算每个类别的先验概率 P(C) 和每个特征在每个类别下的均值和方差
model.fit(X_train, y_train)
print("\n--- 模型训练完成 ---")

# 4. 使用模型进行预测
# 用训练好的模型对测试集进行预测
y_pred = model.predict(X_test)

print("\n--- 预测结果 vs 真实结果 ---")
print("预测结果:", y_pred)
print("真实结果:", y_test)


# 5. 评估模型性能
accuracy = accuracy_score(y_test, y_pred)
print(f"\n模型准确率 (Accuracy): {accuracy:.4f}")

# 查看更详细的评估报告,包括精确率、召回率、F1分数
print("\n--- 分类报告 (Classification Report) ---")
print(classification_report(y_test, y_pred, target_names=iris.target_names))

# 6. 预测一个新样本
# 假设我们有一朵新的鸢尾花,其特征为 [花萼长度, 花萼宽度, 花瓣长度, 花瓣宽度]
new_flower = [[5.1, 3.5, 1.4, 0.2]] # 这组数据很像 setosa
prediction = model.predict(new_flower)
predicted_class_name = iris.target_names[prediction[0]]

print(f"\n--- 预测新样本 ---")
print(f"新样本 {new_flower} 的预测类别是: {predicted_class_name} ({prediction[0]})")


# ==============================================================================
# 示例 2: MultinomialNB (多项式朴素贝叶斯) - 用于文本分类 (词频)
# ==============================================================================
print("="*60)
print("示例 2: MultinomialNB (多项式朴素贝叶斯) for Text Classification")
print("="*60)

# 1. 创建文本数据
# 假设我们有一些关于体育和非体育的文档
corpus = [
'A great game of football',
'The election was close',
'A very clean sweep in the game',
'The president is speaking',
'The player scored a goal'
]
labels = ['sports', 'not sports', 'sports', 'not sports', 'sports']

# 2. 文本特征提取 (词频向量)
# CountVectorizer 会将文本转换为单词计数的向量
vectorizer_multi = CountVectorizer()
X_text_multi = vectorizer_multi.fit_transform(corpus)

# 将稀疏矩阵转换为DataFrame以便查看
df_multi = pd.DataFrame(X_text_multi.toarray(), columns=vectorizer_multi.get_feature_names_out())
print("文本数据的词频特征 (供 MultinomialNB 使用):")
print(df_multi)
print("-" * 30)

# 3. 训练模型 (这里为了演示,使用全部数据训练和预测)
mnb = MultinomialNB()
mnb.fit(X_text_multi, labels)

# 4. 预测与评估
y_pred_mnb = mnb.predict(X_text_multi)
accuracy_mnb = accuracy_score(labels, y_pred_mnb)

print(f"\nMultinomialNB 模型准确率: {accuracy_mnb:.4f}\n")
print("MultinomialNB 分类报告:")
print(classification_report(labels, y_pred_mnb))

# 预测新句子
new_sentence_multi = ['The team won the game']
new_vec_multi = vectorizer_multi.transform(new_sentence_multi)
prediction_mnb = mnb.predict(new_vec_multi)
print(f"预测新句子 '{new_sentence_multi[0]}' 的类别是: {prediction_mnb[0]}")
print("\n\n")


# ==============================================================================
# 示例 3: BernoulliNB (伯努利朴素贝叶斯) - 用于文本分类 (二元特征)
# ==============================================================================
print("="*60)
print("示例 3: BernoulliNB (伯努利朴素贝叶斯) for Text Classification")
print("="*60)

# 1. 创建文本数据 (与上面相同)
# 2. 文本特征提取 (二元特征)
# 使用 CountVectorizer 并设置 binary=True,它只关心单词是否出现,不关心次数
vectorizer_bern = CountVectorizer(binary=True)
X_text_bern = vectorizer_bern.fit_transform(corpus)

# 将稀疏矩阵转换为DataFrame以便查看
df_bern = pd.DataFrame(X_text_bern.toarray(), columns=vectorizer_bern.get_feature_names_out())
print("文本数据的二元特征 (供 BernoulliNB 使用):")
print(df_bern)
print("-" * 30)


# 3. 训练模型
bnb = BernoulliNB()
bnb.fit(X_text_bern, labels)

# 4. 预测与评估
y_pred_bnb = bnb.predict(X_text_bern)
accuracy_bnb = accuracy_score(labels, y_pred_bnb)

print(f"\nBernoulliNB 模型准确率: {accuracy_bnb:.4f}\n")
print("BernoulliNB 分类报告:")
print(classification_report(labels, y_pred_bnb))

# 预测新句子
new_sentence_bern = ['The president won the election']
new_vec_bern = vectorizer_bern.transform(new_sentence_bern)
prediction_bnb = bnb.predict(new_vec_bern)
print(f"预测新句子 '{new_sentence_bern[0]}' 的类别是: {prediction_bnb[0]}")
print("\n\n")

决策树

决策树是一种模拟人类决策过程的监督学习算法,可用于分类回归任务。它通过学习一系列简单的是/否决策规则,从数据特征中推断出目标值,并将这个决策过程呈现为一棵树的形状

  • 核心思想:“分而治之 (Divide and Conquer)”。从根节点开始,通过对特征的提问,不断将数据集切分成更小的、特征更同质化的子集
  • 结构
    • 根节点 (Root Node):包含所有训练样本的起始点
    • 内部节点 (Internal Node):代表一个特征的“问题”或“测试点”
    • 分支 (Branch):代表对这个问题的不同“答案”
    • 叶节点 (Leaf Node):代表最终的决策输出(一个类别或一个连续值)

一个生活中的例子:
想象一下决定是否出门打篮球

  • 根节点:我要不要去打球?
  • 内部节点1:天气怎么样?
    • 分支:“晴天”、“阴天”、“雨天”
  • 如果是“雨天”,直接到达叶节点:“不去”
  • 如果是“晴天”,再进入内部节点2:朋友去吗?
    • 分支:“去”、“不去”
    • 如果朋友“去”,到达叶节点:“去!”
    • 如果朋友“不去”,到达叶节点:“不去”
      这整个思考链条就构成了一棵决策树

决策树分类 (Classification Tree)

分类树的目标是预测一个离散的类别标签(如“是/否”)。CART (Classification and Regression Tree) 是最主流的实现算法,以下内容以其为例

除使用基尼不纯度的CART,还有主要使用信息增益的ID3的算法,和其改进版 - 使用信息增益率作为划分标准的 C4.5算法,这里不多赘述

核心指标:基尼不纯度 (Gini Impurity)

CART使用基尼不纯度来衡量一个节点的“混乱程度”,定义为从一个数据集中随机抽取两个样本,其类别标签不一致的概率,这个概率值越小,说明数据集越“纯净”,即数据集中大部分样本都属于同一个类别

Gini(D)=1i=1kpi2Gini(D) = 1 - \sum_{i=1}^{k} p_i^2

(其中 pip_i 是第i类样本的比例)

计算示例:
假设一个节点有10个样本,其中6个批准贷款(“是”),4个不批准(“否”)。

  • pyes=6/10=0.6p_{yes} = 6/10 = 0.6
  • pno=4/10=0.4p_{no} = 4/10 = 0.4
  • Gini = 1(0.62+0.42)=1(0.36+0.16)=10.52=0.481 - (0.6^2 + 0.4^2) = 1 - (0.36 + 0.16) = 1 - 0.52 = 0.48

这个0.48就代表了当前节点的混乱程度。如果10个样本全是“是”,Gini值将为 1(12+02)=01 - (1^2 + 0^2) = 0,即完全纯净

划分标准:基尼增益 (Gini Gain)

CART的目标是找到一个根据某个特征的某个切分点的划分,使得基尼不纯度下降得最多。这个下降量被称为基尼增益 (Gini Gain)

Gini_Gain=Gini(Dfather)[DleftDfatherGini(Dleft)+DrightDfatherGini(Dright)]Gini\_Gain = Gini(D_{father}) - \left[ \frac{|D_{left}|}{|D_{father}|} Gini(D_{left}) + \frac{|D_{right}|}{|D_{father}|} Gini(D_{right}) \right]

计算示例:
继续上面的例子(10个样本:6是,4否;Gini=0.48Gini=0.48)。我们尝试按特征“是否有房”进行划分。

  1. 划分数据:

    • 有房 (左节点):5个样本,其中5个“是”,0个“否”。
    • 无房 (右节点):5个样本,其中1个“是”,4个“否”。
  2. 计算子节点的Gini:

    • Gini(left)=1(12+02)=0Gini(left) = 1 - (1^2 + 0^2) = 0 (完全纯净)
    • Gini(right)=1((1/5)2+(4/5)2)=1(0.04+0.64)=0.32Gini(right) = 1 - ( (1/5)^2 + (4/5)^2 ) = 1 - (0.04 + 0.64) = 0.32
  3. 计算加权Gini:

    • 加权Gini = (510×0)+(510×0.32)=0+0.16=0.16(\frac{5}{10} \times 0) + (\frac{5}{10} \times 0.32) = 0 + 0.16 = 0.16
  4. 计算基尼增益:

    • Gini Gain (是否有房) = 0.480.16=0.320.48 - 0.16 = 0.32

算法会为所有特征(如“是否有工作”、“年龄”等)都计算一遍基尼增益,然后选择增益最高的那个特征(如此处的“是否有房”)作为当前节点的划分标准

创建子节点与递归运行

在选定好特征来创建子节点时,CART通过二元切分来处理所有类型的特征:

对于连续型特征:

处理方式与C4.5类似。将所有样本在该特征上的取值进行排序,然后遍历所有可能的切分点(通常是相邻两个值的中点),对每个切分点计算基尼增益,选择增益最大的那个点作为该特征的最佳二分点。例如,年龄 <= 30年龄 > 30

对于离散型特征:

这是最能体现CART二分思想的地方。如果一个离散特征有N个取值(N > 2),CART会遍历该特征所有可能的取值组合,将其划分为两个子集,然后从中找到最佳的划分方式

举例说明:
假设特征“城市”有三个取值:{北京, 上海, 广州}。CART在考虑如何用“城市”这个特征来分裂时,会评估以下所有可能的二分组合:

划分1: {北京} vs{上海, 广州}

划分2: {上海} vs {北京, 广州}

划分3: {广州}vs {北京, 上海}

CART会分别计算这三种划分方式的基尼增益,然后选择增益最大的那一种作为该节点的分裂方式。例如,如果划分1的基尼增益最大,那么该节点就会分裂成两条分支:“城市是北京”和“城市不是北京(即上海或广州)”

独热编码 (One-Hot Encoding):

用一个只包含0和1的稀疏向量来表示一个类别

示例:

原始数据:

ID 城市
1 北京
2 上海
3 广州

经过独热编码后,会变成:

ID 城市_北京 城市_上海 城市_广州
1 1 0 0
2 0 1 0
3 0 0 1

现在,每个城市都被表示成一个向量,它们之间的距离是相等的,没有了顺序关系

实践中为了API的统一性、计算效率和工程上的便利,scikit-learn中的DecisionTreeClassifier被设计为只接受数值型输入。它内部并没有实现上述理论中那种复杂的、针对类别组合的搜索功能。它期望用户在将数据喂给它之前,就已经将所有非数值特征(如文本类别)转换完毕,因此使用热独编码

递归执行:

让每一个新的子节点,带着它所分配到的数据子集,返回第1步,重复整个过程,直到所有分支都达到停止条件

  • 条件一(完全纯净):如果当前节点 D 中所有样本都属于同一类别,则无需再划分。将该节点标记为叶节点,类别即为该类别

  • 条件二(特征用尽):如果所有可用的特征都已经被用于划分,但节点内的样本仍不属于同一类别,则也停止划分。将该节点标记为叶节点,其类别通常定为该节点中样本数最多的那个类别(少数服从多数)

  • 条件三(样本集为空):如果划分到一个分支的样本集为空,则无法继续划分。将该节点标记为叶节点,其类别定为其父节点中样本数最多的类别

也可以人为设定条件进行剪枝,防止树过于庞大导致过拟合

预测

一个新的数据样本进入训练好的树,从根节点开始,根据每个节点的规则向下走,直到到达一个叶节点

决策树回归 (Regression Tree)

回归树的目标是预测一个连续的数值(如房价、气温)

核心指标:均方误差 (Mean Squared Error, MSE)

在分类树中,我们使用“信息熵”或“基尼不纯度”来衡量节点的“纯净度”。而回归树使用MSE来衡量一个节点内样本值的“离散程度”。MSE本质上就是节点内样本目标值的方差 (Variance)MSE越小,节点内的数值越相似

MSE(D)=1Ni=1N(yiyˉ)2MSE(D) = \frac{1}{N} \sum_{i=1}^{N} (y_i - \bar{y})^2

(其中 yˉ\bar{y} 是节点内所有样本目标值的平均值)

划分标准:MSE缩减 (MSE Reduction)

回归树选择能够最大化MSE缩减量的划分

MSE_Reduction=MSE(Dfather)[NleftNfatherMSE(Dleft)+NrightNfatherMSE(Dright)]MSE\_Reduction = MSE(D_{father}) - \left[ \frac{N_{left}}{N_{father}} MSE(D_{left}) + \frac{N_{right}}{N_{father}} MSE(D_{right}) \right]

计算示例:
我们要根据“房屋面积”预测“房价”。有5个样本数据(面积, 房价(万)):{(70, 80), (80, 90), (120, 150), (140, 180), (160, 200)}

  1. 计算根节点的MSE:

    • 平均房价 yˉ=(80+90+150+180+200)/5=140\bar{y} = (80+90+150+180+200)/5 = 140
    • MSE(root)=15[(80140)2+...+(200140)2]=15[3600+2500+100+1600+3600]=2280MSE(root) = \frac{1}{5}[(80-140)^2 + ... + (200-140)^2] = \frac{1}{5}[3600+2500+100+1600+3600] = 2280
  2. 尝试以“面积 <= 100”为切分点:

    • 左节点 (面积<=100): 样本{(70, 80), (80, 90)}
    • 右节点 (面积>100): 样本{(120, 150), (140, 180), (160, 200)}
  3. 计算子节点的MSE:

    • 左节点均值 yˉleft=(80+90)/2=85\bar{y}_{left} = (80+90)/2 = 85MSE(left)=12[(8085)2+(9085)2]=25MSE(left) = \frac{1}{2}[(80-85)^2 + (90-85)^2] = 25
    • 右节点均值 yˉright=(150+180+200)/3176.7\bar{y}_{right} = (150+180+200)/3 \approx 176.7MSE(right)=13[(150176.7)2+...]422.2MSE(right) = \frac{1}{3}[(150-176.7)^2 + ...] \approx 422.2
  4. 计算MSE缩减量:

    • 加权MSE = (25×25)+(35×422.2)=10+253.32=263.32(\frac{2}{5} \times 25) + (\frac{3}{5} \times 422.2) = 10 + 253.32 = 263.32
    • MSE缩减 = 2280263.32=2016.682280 - 263.32 = 2016.68

算法会测试所有可能的切分点,选择使MSE缩减最大的那个点

创建子节点与递归运行

与决策树分类同理,当一个节点停止分裂时,它就成为一个叶节点。该叶节点的最终预测值,就是该节点内所有训练样本目标值的平均数

预测

一个新的数据样本进入树,最终会落在一个叶节点上。该样本的预测值就是这个叶节点中所有训练样本目标值的平均数,因此所有落到同一个叶节点上的样品的预测值都相同

预测示例:
假设最终建成的回归树一条路径是:
[面积 <= 100?] -> 是 -> 叶节点 (预测值=85万)

现在来了一个新数据:房屋面积为90平米

  1. 在根节点,判断“面积<=100”,答案是“是”
  2. 到达叶节点,该叶节点的预测值为85万。因此模型预测该房屋价格为85万

这也揭示了回归树预测的特点:预测值是分段常数 (Piecewise Constant)

剪枝:防止过拟合 (Pruning)

为了防止决策树学得“太细”(过拟合),需要进行剪枝

  • 预剪枝 (Pre-pruning):在树的生长中提前结束

    例子:设定一个规则“如果一个节点样本数少于10个,则不准再分裂”。当算法遇到一个只有9个样本的节点时,就会强制停止,使其成为叶节点

  • 后剪枝 (Post-pruning):先让树“野蛮生长”,再“事后修剪”

    例子:树建好后,算法发现底部有一个节点分裂成了两个叶子,但这个分裂在验证集上的表现反而变差了。于是算法会“剪掉”这个分支,把这个节点变成一个叶节点

优缺点分析

优点

  1. 可解释性强:模型逻辑清晰,可以被可视化和解释
  2. 数据预处理要求低:无需归一化或标准化
  3. 能处理混合数据类型:可以同时处理数值型和类别型数据
  4. 自动进行特征选择:建树过程天然地选出了重要性高的特征

缺点

  1. 容易过拟合:单个决策树极易学习到训练数据中的噪声
  2. 不稳定性:数据的微小变动可能导致树结构发生巨大变化
  3. 预测能力有限(回归):回归树的预测不平滑,且无法进行外插(预测超出训练数据范围的值)
  4. 局部最优问题:采用贪心策略,不能保证得到全局最优树

多个决策树:集成学习 (Ensemble Learning)

为了克服单个决策树的缺点(尤其是不稳定性),现代机器学习通常将多个决策树组合起来,形成更强大的集成模型

  • 随机森林 (Random Forest):同时构建多棵树,每棵树使用随机的样本和特征子集。通过“集体投票”的方式进行预测,大大提高了模型的稳定性和准确性
  • 梯度提升决策树 (Gradient Boosted Decision Trees, GBDT):按顺序构建多棵树,每一棵新树都努力去纠正前面所有树犯下的错误。像XGBoost、LightGBM等是其高效实现,是结构化数据上更先进强大的算法

示例代码

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
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor, plot_tree
from sklearn.datasets import load_iris, fetch_california_housing
from sklearn.metrics import accuracy_score, classification_report, mean_squared_error, r2_score

# --- Matplotlib 全局设置 (解决中文显示问题) ---
plt.rcParams['font.sans-serif'] = ['SimHei', 'Heiti TC', 'Microsoft JhengHei']
plt.rcParams['axes.unicode_minus'] = False # 解决保存图像是负号'-'显示为方块的问题


def run_classification_example():
"""
执行并展示决策树分类的示例。
任务:使用鸢尾花数据集 (Iris dataset) 进行品种分类。
"""
print("=" * 50)
print(" 示例1: 决策树分类 (鸢尾花数据集)")
print("=" * 50)

# 1. 加载并准备数据
iris = load_iris()
X = iris.data
y = iris.target
feature_names = iris.feature_names
class_names = iris.target_names

print("任务描述: 根据花的4个特征,预测其属于哪个品种。")
print("特征名称:", feature_names)
print("目标品种:", class_names, "\n")

# 2. 划分数据
# 将数据分为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42, stratify=y)

# 3. 初始化并训练模型
# max_depth=3 是一个预剪枝策略,防止树长得太深导致过拟合
# 如果不设置,决策树会一直生长直到完美划分训练集,很容易过拟合
clf = DecisionTreeClassifier(criterion='gini', max_depth=3, random_state=42)

print("开始训练决策树分类模型...")
clf.fit(X_train, y_train)
print("训练完成!\n")

# 4. 进行预测与评估
y_pred = clf.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print(f"决策树在测试集上的准确率: {accuracy:.4f}\n")
print("分类报告:")
print(classification_report(y_test, y_pred, target_names=class_names))

# 5. 可视化决策树
plt.figure(figsize=(20, 12))
plot_tree(clf,
feature_names=feature_names,
class_names=class_names,
filled=True, # 填充颜色以表示类别
rounded=True, # 节点框使用圆角
fontsize=12)
plt.title("决策树分类可视化 (鸢尾花数据集, max_depth=3)", fontsize=16)
plt.show()


def run_regression_example():
"""
执行并展示决策树回归的示例。
任务:使用加州房价数据集 (California Housing dataset) 预测房价。
"""
print("\n" * 3)
print("=" * 50)
print(" 示例2: 决策树回归 (加州房价数据集)")
print("=" * 50)

# 1. 加载并准备数据
housing = fetch_california_housing()
X = housing.data
y = housing.target
feature_names = housing.feature_names

print("任务描述: 根据地区的8个特征,预测房价中位数。")
print("特征名称:", feature_names, "\n")

# 2. 划分数据
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

# 3. 初始化并训练模型
# 同样设置最大深度以防过拟合
regr = DecisionTreeRegressor(max_depth=4, random_state=42)

print("开始训练决策树回归模型...")
regr.fit(X_train, y_train)
print("训练完成!\n")

# 4. 进行预测与评估
y_pred = regr.predict(X_test)
mse = mean_squared_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)

print(f"决策树在测试集上的均方误差(MSE): {mse:.4f}")
print(f"决策树在测试集上的R^2决定系数: {r2:.4f}\n")

# 5. 可视化
# 可视化1:绘制真实值与预测值的散点图,对比模型效果
plt.figure(figsize=(10, 6))
plt.scatter(y_test, y_pred, alpha=0.5, edgecolor='k')
# 绘制一条 y=x 的参考线,点越靠近这条线说明预测越准
plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 'r--', lw=2)
plt.xlabel("真实房价")
plt.ylabel("预测房价")
plt.title("决策树回归预测结果对比", fontsize=16)
plt.grid(True)
plt.show()

# 可视化2:绘制回归树的结构
plt.figure(figsize=(22, 14))
plot_tree(regr,
feature_names=feature_names,
filled=True,
rounded=True,
fontsize=10)
plt.title("决策树回归结构可视化 (加州房价, max_depth=4)", fontsize=16)
plt.show()


if __name__ == "__main__":
# 依次执行分类和回归的示例
run_classification_example()
run_regression_example()

随机森林

一种基于 决策树 (CART)集成学习 (Ensemble Learning) 算法,通过构建了大量的决策树(即“森林”),通过集体决策(投票或平均)来提升模型的准确性和稳定性,有效克服单棵决策树容易过拟合的问题

两大随机性来源

随机森林的强大之处和名称来源在于其双重的随机性,这保证了“森林”中树木的多样性低相关性

  1. 样本随机 (行抽样 - Bagging)

    • 做法: 从包含N个样本的原始数据集中,有放回地随机抽取N个样本,形成一个训练子集。这个过程重复M次,创建M个不同的训练集,用于训练M棵树
    • 目的: 制造训练数据的多样性,使得每棵树都略有不同,从而降低整个模型的方差 (Variance)
    • 副产品: 每个训练过程中约有36.8%的数据未被抽中,称为袋外数据 (Out-of-Bag, OOB),可直接用于模型的验证,评估泛化能力
  2. 特征随机 (列抽样)

    • 做法: 在构建每棵决策树的每个节点进行分裂时,不是从全部P个特征中选择最优分裂点,而是先随机选取p个特征(通常p远小于P,例如 pPp \approx \sqrt{P}),再从这p个特征中寻找最优分裂点
    • 目的: 避免少数强特征主导所有决策树的构建,进一步降低树与树之间的相关性 (Correlation),增强模型的稳健性

核心步骤

  1. 训练阶段:
    对于M棵树里面的每一棵树:

    • a. 对原始数据集进行样本随机抽样,得到训练集 D_i
    • b. 使用 D_i 训练一棵决策树 T_i。在每个节点分裂时,都执行特征随机选择
    • c. 通常让树完全生长,不进行剪枝
  2. 预测阶段:

    • 将新数据点输入到森林中所有的 n_estimators 棵树
    • 得到 n_estimators 个预测结果
    • 分类问题: 采用投票 (Voting) 方式,得票最多的类别为最终结果
    • 回归问题: 采用平均 (Averaging) 方式,所有树预测值的平均数为最终结果

关键超参数:

  • n_estimators: 森林中决策树的数量。数量越多,模型越稳定,但计算成本也越高,且性能提升有边际效应
  • max_features: 在节点分裂时随机选择的特征数量。这是控制模型偏差和方差的关键参数,直接影响树之间的相关性
  • max_depth: 树的最大深度。用于控制单棵树的复杂度,防止过拟合
  • min_samples_split / min_samples_leaf: 节点分裂或叶节点所需的最小样本数,也用于控制树的生长

优缺点分析

优点

  • 性能强大,准确率高: 在绝大多数场景下,都是非常有效且强大的基线模型
  • 强抗过拟合能力: 双重随机性使其比单棵决策树稳健得多
  • 能评估特征重要性: 可以通过模型计算出每个特征对预测的贡献度
  • 易于并行化: 每棵树的训练相互独立,计算效率高
  • 对缺失值不敏感,能处理高维度数据

缺点

  • 模型解释性差: 相对于单棵决策树清晰的规则,随机森林是一个“黑盒子”,难以解释具体预测的内在逻辑
  • 资源消耗大: 当树的数量非常多时,训练和预测需要较大的内存和计算时间
  • 不适合某些类型数据: 在处理非常稀疏的数据(如文本)时,可能不如专门的模型(如线性模型或梯度提升树)
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
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns # 引入seaborn库以增强可视化
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
# 导入真实世界的数据集加载器
from sklearn.datasets import load_iris, fetch_california_housing
from sklearn.metrics import (accuracy_score, classification_report, confusion_matrix,
mean_squared_error, r2_score)
from matplotlib.colors import ListedColormap

# --- Matplotlib 和 Seaborn 全局设置 ---
# 确保中文字体可以正常显示
plt.rcParams['font.sans-serif'] = ['SimHei', 'Heiti TC', 'Microsoft JhengHei']
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
sns.set_theme(style="whitegrid", font='SimHei') # 设置seaborn的主题和字体


def plot_confusion_matrix(y_true, y_pred, class_names):
"""
绘制混淆矩阵热力图。
"""
cm = confusion_matrix(y_true, y_pred)
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
xticklabels=class_names, yticklabels=class_names)
plt.title('混淆矩阵热力图', fontsize=16)
plt.ylabel('真实类别', fontsize=12)
plt.xlabel('预测类别', fontsize=12)
plt.show()


def plot_decision_boundary(X, y, model, feature_names, class_names):
"""
绘制二维决策边界图。
注意:此函数仅为了可视化,只使用了两个最重要的特征。
"""
# 仅选择两个最重要的特征进行可视化
# 在鸢尾花数据集中,通常是花瓣长度和宽度
feature_indices = [2, 3]
X_vis = X[:, feature_indices]

# 使用这两个特征重新训练一个模型
model_vis = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1)
model_vis.fit(X_vis, y)

# 创建网格来绘制决策边界
h = .02 # 网格步长
x_min, x_max = X_vis[:, 0].min() - 0.5, X_vis[:, 0].max() + 0.5
y_min, y_max = X_vis[:, 1].min() - 0.5, X_vis[:, 1].max() + 0.5
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))

# 在网格上进行预测
Z = model_vis.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)

# 绘制背景和数据点
plt.figure(figsize=(10, 7))
custom_cmap = ListedColormap(['#FFAAAA', '#AAFFAA', '#AAAAFF'])
plt.contourf(xx, yy, Z, cmap=custom_cmap, alpha=0.8)

# 绘制数据点
scatter = plt.scatter(X_vis[:, 0], X_vis[:, 1], c=y, cmap=ListedColormap(['#FF0000', '#00FF00', '#0000FF']),
edgecolor='k', s=40)

plt.title('随机森林决策边界 (基于最重要的两个特征)', fontsize=16)
plt.xlabel(feature_names[feature_indices[0]], fontsize=12)
plt.ylabel(feature_names[feature_indices[1]], fontsize=12)
plt.legend(handles=scatter.legend_elements()[0], labels=list(class_names), title="品种")
plt.show()


def run_iris_classifier_example():
"""
运行随机森林分类器的示例。
"""
print("=" * 50)
print(" 示例1: 鸢尾花分类 (Random Forest Classifier)")
print("=" * 50)

# --- 1. 准备数据 ---
iris = load_iris()
X, y = iris.data, iris.target
feature_names, class_names = iris.feature_names, iris.target_names

print("任务描述: 根据花的4个特征,预测其属于哪个品种。")
print("特征名称:", feature_names)
print("目标品种:", class_names, "\n")

# --- 2. 划分数据集 ---
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
print(f"分类任务 - 训练集大小: {X_train.shape}")
print(f"分类任务 - 测试集大小: {X_test.shape}\n")

# --- 3. 创建并训练模型 ---
rf_classifier = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1)
print("开始训练鸢尾花分类模型...")
rf_classifier.fit(X_train, y_train)
print("训练完成!\n")

# --- 4. 进行预测与评估 ---
y_pred = rf_classifier.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print(f"模型准确率 (Accuracy): {accuracy:.4f}\n")
print("分类报告 (Classification Report):\n", classification_report(y_test, y_pred, target_names=class_names))

# --- 5. 增强的可视化 ---
# 5.1 特征重要性图 (保留)
importances = rf_classifier.feature_importances_
feature_importance_series = pd.Series(importances, index=feature_names).sort_values(ascending=False)
plt.figure(figsize=(12, 7))
sns.barplot(x=feature_importance_series, y=feature_importance_series.index)
plt.title("鸢尾花分类 - 特征重要性", fontsize=16)
plt.xlabel("重要性分数", fontsize=12)
plt.ylabel("特征", fontsize=12)
plt.show()

# 5.2 (新增) 混淆矩阵热力图
plot_confusion_matrix(y_test, y_pred, class_names)

# 5.3 (新增) 决策边界图
plot_decision_boundary(X, y, rf_classifier, feature_names, class_names)


def plot_residuals(y_true, y_pred):
"""
绘制残差图。
"""
residuals = y_true - y_pred
plt.figure(figsize=(10, 6))
sns.scatterplot(x=y_pred, y=residuals, alpha=0.5)
plt.axhline(y=0, color='r', linestyle='--')
plt.title('残差图 (Residuals vs. Predicted Values)', fontsize=16)
plt.xlabel('预测值', fontsize=12)
plt.ylabel('残差 (真实值 - 预测值)', fontsize=12)
plt.show()


def run_housing_regressor_example():
"""
运行随机森林回归器的示例。
"""
print("\n" * 3)
print("=" * 50)
print(" 示例2: 加州房价预测 (Random Forest Regressor)")
print("=" * 50)

# --- 1. 准备数据 ---
housing = fetch_california_housing()
X, y = housing.data, housing.target
feature_names = housing.feature_names

print("任务描述: 根据地区的8个特征,预测房价中位数。")
print("特征名称:", feature_names, "\n")

# --- 2. 划分数据集 ---
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
print(f"回归任务 - 训练集大小: {X_train.shape}")
print(f"回归任务 - 测试集大小: {X_test.shape}\n")

# --- 3. 创建并训练模型 ---
rf_regressor = RandomForestRegressor(n_estimators=100, random_state=42, n_jobs=-1)
print("开始训练加州房价预测模型...")
rf_regressor.fit(X_train, y_train)
print("训练完成!\n")

# --- 4. 进行预测与评估 ---
y_pred = rf_regressor.predict(X_test)
mse = mean_squared_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
print(f"均方误差 (MSE): {mse:.4f}")
print(f"R^2 决定系数: {r2:.4f}\n")

# --- 5. 增强的可视化 ---
# 5.1 特征重要性图 (保留并美化)
importances = rf_regressor.feature_importances_
feature_importance_series = pd.Series(importances, index=feature_names).sort_values(ascending=False)
plt.figure(figsize=(12, 7))
sns.barplot(x=feature_importance_series, y=feature_importance_series.index)
plt.title("加州房价预测 - 特征重要性", fontsize=16)
plt.xlabel("重要性分数", fontsize=12)
plt.ylabel("特征", fontsize=12)
plt.show()

# 5.2 (美化) 真实值 vs 预测值
plt.figure(figsize=(10, 6))
plt.scatter(y_test, y_pred, alpha=0.3, edgecolor='k')
plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 'r--', lw=2, label='完美预测线')
plt.title('真实值 vs. 预测值', fontsize=16)
plt.xlabel('真实房价', fontsize=12)
plt.ylabel('预测房价', fontsize=12)
plt.legend()
plt.grid(True)
plt.show()

# 5.3 (新增) 残差图
plot_residuals(y_test, y_pred)


if __name__ == "__main__":
# 依次运行分类和回归的示例
run_iris_classifier_example()
run_housing_regressor_example()

XGBoost

XGBoost 极致梯度提升 (eXtreme Gradient Boosting) 是梯度提升决策树 (GBDT) 的一种极致、高效、可扩展的工程实现。如果说随机森林是“民主投票制”(所有树独立并行工作),那么XGBoost就是“精英导师制”(每个新模型都在弥补前面模型的不足)

  • 核心定位:处理结构化/表格数据的非常有效的模型,在数据科学竞赛和工业界应用中被广泛使用
  • 与随机森林的根本区别:
    • 随机森林 (并行):构建多棵独立的树,通过投票或平均来降低模型的方差,防止过拟合
    • XGBoost (串行):依次构建树,每一棵新树都旨在修正前面所有树的预测残差(误差),目标是不断降低模型的偏差,提升精度

XGBoost的原理建立在GBDT之上,并通过一系列优化达到了极致

  1. 基础思想:加法模型 (Additive Model)
    最终的预测结果是所有弱学习器(决策树)预测结果的累加

    y^i=k=1Kfk(xi)\hat{y}_i = \sum_{k=1}^{K} f_k(x_i)

    其中,KK 是树的总数,fkf_k 是第 kk 棵树的模型

  2. 学习目标:修正残差 (Residual Fitting)
    模型是迭代构建的。假设我们已经构建了 t1t-1 棵树,当前的预测结果与真实值之间存在一个误差,即残差 error = y_true - y_pred
    tt 棵树 f_t 的学习目标不再是原始的 y_true,而是这个 error。通过不断学习和累加修正残差的树,模型最终能很好地逼近真实值

  3. “梯度”的体现:损失函数的梯度下降
    “学习残差”是均方误差(MSE)损失函数下的特例。更一般地,XGBoost在每一步都是在拟合损失函数关于当前预测值的负梯度。这使得XGBoost可以支持任意可微的自定义损失函数,应用场景更广

  4. XGBoost的“极致(eXtreme)”优化

    • 正则化 (Regularization):这是XGBoost防止过拟合的关键。它的目标函数包含了损失函数正则化项两部分,在追求高精度的同时惩罚模型的复杂度,使其泛化能力更强
    • 二阶泰勒展开:在优化目标函数时,XGBoost使用了二阶泰勒展开,同时利用了一阶导数(梯度)和二阶导数信息,使得优化过程更快、更准
    • 内置缺失值处理:能自动学习和处理数据中的缺失值
    • 高效的工程实现:支持大规模并行计算,优化了内存和缓存使用,训练速度远超传统GBDT

核心步骤

XGBoost (Extreme Gradient Boosting) 的核心在于其高度优化的目标函数和高效的树构建算法。整个推导过程可以分为四个主要部分:

  1. 定义模型与目标函数:确定我们要优化的目标。
  2. 泰勒展开近似:将复杂的目标函数简化为二次函数。
  3. 重构目标函数:将对样本的优化,转为对树结构的优化。
  4. 求解最优树:推导如何计算叶子权重和如何寻找最佳分裂点。

1. 定义模型与目标函数

XGBoost是一个加法模型(Additive Model),由 K 棵树组成。对于第 i 个样本,其最终预测值 y^i\hat{y}_i 是所有树预测值的总和:

y^i=k=1Kfk(xi)\hat{y}_i = \sum_{k=1}^{K} f_k(x_i)

其中 fkf_k 代表第 k 棵决策树

这是一个逐步迭代的过程。在第 t 步,我们添加第 t 棵树 ftf_t 来优化模型。此时,前 t-1 棵树已经确定,所以第 t 步的预测值为:

y^i(t)=y^i(t1)+ft(xi)\hat{y}_i^{(t)} = \hat{y}_i^{(t-1)} + f_t(x_i)

其中 y^i(t1)\hat{y}_i^{(t-1)} 是前 t-1 棵树的预测总和,在第 t 步是已知常数

我们的总目标函数 (Objective Function) 由两部分组成:损失函数正则化项

Obj(t)=i=1nL(yi,y^i(t))+k=1tΩ(fk)\text{Obj}^{(t)} = \sum_{i=1}^{n} L(y_i, \hat{y}_i^{(t)}) + \sum_{k=1}^{t} \Omega(f_k)

  • L(yi,y^i(t))L(y_i, \hat{y}_i^{(t)}):是损失函数,用于衡量预测值 y^i(t)\hat{y}_i^{(t)} 与真实值 yiy_i 的差距。例如,均方误差 (yiy^i(t))2(y_i - \hat{y}_i^{(t)})^2
  • Ω(fk)\Omega(f_k):是第 k 棵树的正则化项,用于惩罚树的复杂度,防止过拟合
  • nn:是训练样本的总数

y^i(t)\hat{y}_i^{(t)} 代入,并把常数项(前 t-1 棵树的正则化)分离出来:

Obj(t)=i=1nL(yi,y^i(t1)+ft(xi))+Ω(ft)\text{Obj}^{(t)} = \sum_{i=1}^{n} L(y_i, \hat{y}_i^{(t-1)} + f_t(x_i)) + \Omega(f_t)

我们的目标是在第 t 步找到一棵树 ftf_t,来最小化这个 Obj(t)\text{Obj}^{(t)}

2. 泰勒展开近似

目标函数中的 ft(xi)f_t(x_i) 被包含在一个通用的损失函数 LL 内部,直接优化非常困难。为了解决这个问题,我们使用二阶泰勒展开y^i(t1)\hat{y}_i^{(t-1)} 点对损失函数进行近似

一个通用的二阶泰勒展开公式为:

f(x+Δx)f(x)+f(x)Δx+12f(x)(Δx)2f(x + \Delta x) \approx f(x) + f'(x)\Delta x + \frac{1}{2} f''(x)(\Delta x)^2

我们将损失函数 LL 对应到这个公式:

  • xy^i(t1)x \rightarrow \hat{y}_i^{(t-1)}
  • Δxft(xi)\Delta x \rightarrow f_t(x_i)

应用展开,我们得到:

L(yi,y^i(t1)+ft(xi)) L(yi,y^i(t1))+L(yi,y^i(t1))y^i(t1)ft(xi)+122L(yi,y^i(t1))(y^i(t1))2ft2(xi)\begin{aligned} L\big(y_i, \hat{y}_i^{(t-1)} + f_t(x_i)\big) \approx\ &L\big(y_i, \hat{y}_i^{(t-1)}\big) + \frac{\partial L(y_i, \hat{y}_i^{(t-1)})}{\partial \hat{y}_i^{(t-1)}} \cdot f_t(x_i) + \frac{1}{2} \cdot \frac{\partial^2 L(y_i, \hat{y}_i^{(t-1)})}{\partial (\hat{y}_i^{(t-1)})^2} \cdot f_t^2(x_i) \end{aligned}

为了简化,我们定义一阶导数(gradient)二阶导数(Hessian)

  • gi=y^(t1)L(yi,y^i(t1))g_i = \partial_{\hat{y}^{(t-1)}} L(y_i, \hat{y}_i^{(t-1)})
  • hi=y^(t1)2L(yi,y^i(t1))h_i = \partial^2_{\hat{y}^{(t-1)}} L(y_i, \hat{y}_i^{(t-1)})

在第 t 步,y^i(t1)\hat{y}_i^{(t-1)} 是已知的,因此对于每个样本 iigig_ihih_i 都是可以计算出的常数

gig_ihih_i 代入目标函数,并移除所有对于优化 ftf_t 而言是常数的部分(如 L(yi,y^i(t1))L(y_i, \hat{y}_i^{(t-1)}) 和之前所有树的正则化项),我们得到在第 t 步需要最小化的简化目标函数

Obj(t)i=1n[gift(xi)+12hift2(xi)]+Ω(ft)\text{Obj}^{(t)} \approx \sum_{i=1}^{n} \left[ g_i f_t(x_i) + \frac{1}{2} h_i f_t^2(x_i) \right] + \Omega(f_t)

3. 重构目标函数(从样本到叶子)

现在,我们需要用数学语言来定义一棵树 ftf_t 和它的复杂度 Ω(ft)\Omega(f_t)

树的定义:一棵树由其结构 qq 和叶子权重向量 ww 定义。q(xi)q(x_i) 将样本 xix_i 映射到某个叶子的索引号 jj,即wjw_j 是第 jj 个叶子的分数值

ft(xi)=wq(xi)f_t(x_i) = w_{q(x_i)}

正则化项定义:复杂度由叶子的数量 TT 和叶子权重 ww 的L2范数共同定义:

Ω(ft)=γT+12λj=1Twj2\Omega(f_t) = \gamma T + \frac{1}{2} \lambda \sum_{j=1}^{T} w_j^2

其中 γ\gammaλ\lambda 是控制复杂度的超参数

将这两个定义代入简化目标函数:

Obj(t)=i=1n[giwq(xi)+12hiwq(xi)2]+γT+12λj=1Twj2\text{Obj}^{(t)} = \sum_{i=1}^{n} \left[ g_i w_{q(x_i)} + \frac{1}{2} h_i w_{q(x_i)}^2 \right] + \gamma T + \frac{1}{2} \lambda \sum_{j=1}^{T} w_j^2

这个公式仍然是对所有样本 ii 进行求和,不便于优化树的结构。我们通过改变求和的顺序,将其重构为对叶子的求和

  • 定义 Ij={iq(xi)=j}I_j = \{ i \mid q(x_i)=j \} 为所有落入第 jj 个叶子的样本索引集合

那么,对所有样本求和就等价于,先对所有叶子求和,再对每个叶子内的样本求和:

Obj(t)=j=1T[iIjgiwj+iIj12hiwj2]+γT+12λj=1Twj2\text{Obj}^{(t)} = \sum_{j=1}^{T} \left[ \sum_{i \in I_j} g_i w_j + \sum_{i \in I_j} \frac{1}{2} h_i w_j^2 \right] + \gamma T + \frac{1}{2} \lambda \sum_{j=1}^{T} w_j^2

由于对于同一个叶子 jj 内的所有样本,wjw_j 是常数,可以将其提取出来。同时合并正则项:

Obj(t)=j=1T[(iIjgi)wj+12(iIjhi+λ)wj2]+γT\text{Obj}^{(t)} = \sum_{j=1}^{T} \left[ \left(\sum_{i \in I_j} g_i\right) w_j + \frac{1}{2} \left(\sum_{i \in I_j} h_i + \lambda \right) w_j^2 \right] + \gamma T

再次为了简化,我们定义每个叶子 jj 上的一阶与二阶导数之和:

  • Gj=iIjgiG_j = \sum_{i \in I_j} g_i
  • Hj=iIjhiH_j = \sum_{i \in I_j} h_i

最终,我们得到了关于树结构的最终目标函数

Obj(t)=j=1T[Gjwj+12(Hj+λ)wj2]+γT\text{Obj}^{(t)} = \sum_{j=1}^{T} \left[ G_j w_j + \frac{1}{2} (H_j + \lambda) w_j^2 \right] + \gamma T

4. 求解最优树

这个最终的目标函数非常优美,它将问题分解成了两部分:

  1. 如何找到最优的树结构(即如何分裂节点)?

  2. 对于一个固定的树结构,如何计算最优的叶子权重 wjw_j

求解最优叶子权重 wjw_j^*

对于一个固定的树结构,每个叶子 jj 的目标函数部分 Gjwj+12(Hj+λ)wj2G_j w_j + \frac{1}{2} (H_j + \lambda) w_j^2 是一个简单的一元二次函数,在抛物线最低点解得最优叶子权重

wj=GjHj+λw_j^* = - \frac{G_j}{H_j + \lambda}

将最优权重 wjw_j^* 代回到目标函数中,我们可以得到在最优权重下,该树结构对应的目标函数最小值。这可以作为评价一个树结构好坏的评分(Score)

Obj=j=1T[Gj(GjHj+λ)+12(Hj+λ)(GjHj+λ)2]+γT\text{Obj}^* = \sum_{j=1}^{T} \left[ G_j \left(- \frac{G_j}{H_j + \lambda}\right) + \frac{1}{2} (H_j + \lambda) \left(- \frac{G_j}{H_j + \lambda}\right)^2 \right] + \gamma T

Obj=j=1T[Gj2Hj+λ+12Gj2Hj+λ]+γT\text{Obj}^* = \sum_{j=1}^{T} \left[ -\frac{G_j^2}{H_j + \lambda} + \frac{1}{2}\frac{G_j^2}{H_j + \lambda} \right] + \gamma T

Obj=12j=1TGj2Hj+λ+γT\text{Obj}^* = - \frac{1}{2} \sum_{j=1}^{T} \frac{G_j^2}{H_j + \lambda} + \gamma T

寻找最佳分裂点:分裂增益 (Gain)

XGBoost采用贪心算法来构建树。它从根节点开始,尝试所有可能的分裂,选择**增益(Gain)**最大的那个,增益的定义是:分裂后的收益减去分裂前的“收益”,再减去新增叶子带来的复杂度代价

增益 = (左子节点分数 + 右子节点分数) - 父节点分数 - γ\gamma

注意:这里的“分数”是指目标函数中与 wwG,HG, H 相关的那部分,即 12G2H+λ-\frac{1}{2}\frac{G^2}{H+\lambda}γ\gamma 是因为分裂行为本身使得叶子总数 TT 增加了1,带来的固定惩罚

设一个父节点为 PP,分裂为左子节点 LL 和右子节点 RR

Gain=(12GL2HL+λ)+(12GR2HR+λ)(12GP2HP+λ)γ\text{Gain} = \left(-\frac{1}{2}\frac{G_L^2}{H_L+\lambda}\right) + \left(-\frac{1}{2}\frac{G_R^2}{H_R+\lambda}\right) - \left(-\frac{1}{2}\frac{G_P^2}{H_P+\lambda}\right) - \gamma

整理后得到最终的分裂增益公式

Gain=12[GL2HL+λ+GR2HR+λGP2HP+λ]γ\text{Gain} = \frac{1}{2} \left[ \frac{G_L^2}{H_L + \lambda} + \frac{G_R^2}{H_R + \lambda} - \frac{G_P^2}{H_P + \lambda} \right] - \gamma

在构建树的每一步,算法都会遍历所有特征和所有可能的分裂点,计算其Gain值,并选择Gain最大的分裂方案。如果最大的Gain小于等于0,则停止分裂

总结:

  1. 初始化:从深度为 0 的根节点开始,根节点包含所有训练样本。计算根节点的Groot=i=1ngiG_{\text{root}} = \sum_{i=1}^n g_iHroot=i=1nhiH_{\text{root}} = \sum_{i=1}^n h_i

  2. 迭代生长:对于树中每一个尚未分裂的子节点,执行以下操作:

    a. 设当前节点为 CC,计算其父节点分数项GC2HC+λ\frac{G_C^2}{H_C + \lambda}

    b. 初始化 max_gain = 0best_split = null

    c. 遍历所有特征:对数据中的每一个特征 kk

    i. 遍历所有分裂点:对特征 kk 的所有可能取值(或对于连续特征,是排序后的唯一值之间的点),将其作为候选分裂点 vv

    ii. 尝试分裂:根据规则 $ \text{feature}_k < v $ 将当前节点 CC 的样本划分为左子集 ILI_L 和右子集 IRI_R

    iii. 计算增益

    • 计算 GL,HL,GR,HRG_L, H_L, G_R, H_R
    • 使用上面的分裂增益公式计算本次分裂的 Gain
    • 如果 $ \text{Gain} > \text{max_gain} $,则更新 max_gain = Gain,并记录下当前的分裂特征和分裂点 vv 作为 best_split

    d. 决策:在遍历完所有特征和所有分裂点后,如果 $ \text{max_gain} > 0 $,说明找到了一个可以带来收益的分裂点。就将当前节点 CC 按照 best_split 的方案进行分裂,生成两个新的子节点

  3. 停止生长:重复第 2 步,直到满足以下任一停止条件:

    • 计算出的最大增益 max_gain 小于等于 0(或者小于一个用户设定的阈值 min_split_gain)。这意味着任何分裂都无法带来收益
    • 树的深度达到了用户设定的最大深度 max_depth
    • 叶子节点包含的样本权重之和(即 HH 值)小于用户设定的 min_child_weight。这可防止叶子节点样本太少,避免过拟合

总体流程

  1. 初始化模型,例如一个返回常数值的基模型
  2. For t = 1 to K (迭代K棵树):
    a. 计算每个样本 ii 的一阶导数 gig_i 和二阶导数 hih_i
    b. 构建第 t 棵树 ftf_t
    c. 将构建好的树 ftf_t 的所有叶子节点权重 wjw_j 按公式 wj=GjHj+λw_j^* = - \frac{G_j}{H_j + \lambda} 计算出来
    d. 更新总模型:将新生成的树加入模型。通常会引入一个学习率 η\eta 来防止过拟合:

y^i(t)=y^i(t1)+ηft(xi)\hat{y}_i^{(t)} = \hat{y}_i^{(t-1)} + \eta \cdot f_t(x_i)

  1. 结束迭代,得到最终的模型 y^=k=1Kηfk(xi)\hat{y} = \sum_{k=1}^K \eta \cdot f_k(x_i)

模型评估

交叉检验 (Cross-Validation)

这是一种更稳健、更可靠的评估方法。最常用的是K-折交叉验证 (K-Fold Cross-Validation)

  1. 将训练数据(不包括测试集)平均分成 K 份(例如 K=5 或 10)
  2. 进行 K 轮迭代。在每一轮中,选择其中 1 份作为验证集,其余 K-1 份作为训练集
  3. 训练模型并在验证集上计算评估指标
  4. 最终的评估结果是这 K 轮指标的平均值

分类任务核心评估指标

分类模型的评估指标可以从不同角度衡量其性能,通常需要综合考量

指标 核心含义 适用场景与解读
准确率 (Accuracy) 所有预测正确的样本占总样本的比例。 类别均衡的数据集上直观易懂。但在类别不均衡时具有极强的误导性,不推荐作为主要指标。
精确率 (Precision) “查准率”:在所有被预测为正例的样本中,真正是正例的比例。 误报(False Positive)的代价很高时是关键指标。例如:垃圾邮件过滤(不想错杀好邮件)。
召回率 (Recall) “查全率”:在所有实际为正例的样本中,被成功预测出来的比例。 漏报(False Negative)的代价很高时是关键指标。例如:疾病诊断(不想漏掉真病人)。
F1分数 (F1-Score) 精确率和召回率的调和平均数 当精确率和召回率同等重要,需要一个综合性指标来平衡两者时使用。
AUC-ROC ROC曲线下的面积,衡量模型将正样本排在负样本前面的综合能力。 最常用、最重要的分类评估指标之一。它不受类别不平衡和分类阈值选择的影响,能全面评估模型的排序能力。AUC越接近1越好,0.5代表随机猜测。

回归任务核心评估指标

回归模型的评估指标主要衡量预测值与真实值之间的数值差距

指标 核心含义 解读
MAE (平均绝对误差) 预测误差绝对值的平均数。 直观,单位与原数据相同,对异常值不敏感。
MSE (均方误差) 预测误差平方的平均数。 对大误差的惩罚比MAE更重。
RMSE (均方根误差) MSE的平方根。 最常用的回归指标。单位与原数据相同,且对大误差敏感,易于解释。
R² (决定系数) 模型能解释的数据方差的比例。 值越接近1,说明模型对数据的拟合程度越好。

超参数与调参

无论使用何种工具,一个清晰的策略都至关重要。直接将所有参数扔给自动化工具不仅效率低下,而且结果可能不理想。推荐遵循以下结构化步骤,它能帮助你理解参数间的相互作用,并逐步优化模型

  1. 第0步:建立基准模型

    • 目标:确定一个合理的n_estimators(树的数量),为后续调优提供一个稳定的基准。
    • 做法
      1. 设置一个相对较高的学习率 learning_rate(如 0.1)。
      2. 使用一个较大的n_estimators(如 1000)。
      3. 利用**早停(Early Stopping)**机制,在验证集上监控性能。当验证集上的评估指标(如AUC, logloss等)在一定轮数内不再提升时,训练会自动停止。此时的迭代次数就是当前学习率下较优的n_estimators
  2. 第1步:优化树的结构

    • 目标:调整对模型性能影响最大的参数,即树的复杂度和结构。
    • 参数max_depth, min_child_weight
  3. 第2步:优化分裂阈值

    • 目标:通过调整分裂的“门槛”来进一步控制树的生长。
    • 参数gamma
  4. 第3步:优化随机性

    • 目标:通过采样增加模型的随机性,降低过拟合风险。
    • 参数subsample, colsample_bytree
  5. 第4步:优化正则化

    • 目标:对模型进行微调,进一步控制复杂度。
    • 参数reg_alpha, reg_lambda
  6. 第5步:最终确定与降速

    • 目标:使用已找到的最优参数组合,通过降低学习率来训练一个更稳健、更精确的模型。
    • 做法:将learning_rate降低(如从0.1降到0.01),同时按比例增加n_estimators(如增加10倍),再次使用早停确定最终的迭代次数。

网格搜索是最简单、最暴力的调参方法。它会尝试你指定参数的所有可能组合,通过为每个待调优的参数提供一个离散值的列表,网格搜索会遍历这些列表的所有组合,对每一种组合都训练一个模型并使用交叉验证进行评估,最终返回性能最好的参数组合

贝叶斯优化 (Bayesian Optimization)

贝叶斯优化是一种更智能、更高效的调参方法。它借鉴了先验知识来指导后续的搜索方向

  • 工作原理

    1. 代理模型 (Surrogate Model):它首先使用已有的(参数-性能)点,建立一个关于目标函数(如交叉验证得分)的概率模型。这个模型能够预测哪些参数组合可能会带来更高的分数
    2. 采集函数 (Acquisition Function):基于代理模型,采集函数会决定下一个应该尝试的参数点。它巧妙地平衡了探索(Exploration)利用(Exploitation)
      • 利用:在当前已知最优解的附近进行搜索,期望找到更好的解
      • 探索:在模型不确定性高的区域进行搜索,可能会发现全新的、未曾探索过的最优区域
    3. 迭代:用新评估的点来更新代理模型,然后重复第2步,直到达到预设的迭代次数
  • 优点

    • 效率最高:通常用比随机搜索少得多的迭代次数就能找到最优或接近最优的解
    • 智能搜索:不是盲目尝试,而是根据历史信息进行有根据的猜测
  • 缺点

    • 理论和实现上比前两者更复杂
    • 对于低维或简单的搜索空间,其优势可能不明显
  • 适用场景:几乎所有场景,特别是当模型训练成本高昂(如大数据集、复杂模型)时,其高效性优势尽显

即使使用自动化工具,也最好遵循前面提到的结构化策略。例如,使用Optuna分阶段调整参数组,而不是一次性把所有参数都放进去优化,这样通常效果更好,也更易于分析

示例代码

建议在Jupyter环境下运行

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
import pandas as pd
import numpy as np
import xgboost as xgb
import optuna

# 导入可视化库
import matplotlib.pyplot as plt
import seaborn as sns

# --- 修复/新增:增加SHAP可用性检查 ---
try:
import shap # 用于模型解释

is_shap_available = True
# 初始化JS环境,用于在Jupyter等环境中显示更美观的交互式图表
shap.initjs()
except ImportError:
is_shap_available = False
print(
"SHAP library is not available. Please install it with 'pip install shap'. Model explanation part will be skipped.")


# 检查Optuna可视化功能是否可用
try:
from optuna.visualization import plot_optimization_history, plot_param_importances

is_optuna_viz_available = True
except ImportError:
is_optuna_viz_available = False
print("Optuna visualization is not available. Please install it with 'pip install optuna-dashboard'.")

# 导入数据集
from sklearn.datasets import load_iris, fetch_california_housing

# 导入建模和评估工具
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report, mean_squared_error, \
mean_absolute_error, r2_score


# ==============================================================================
# Part 1: Multi-Class Classification with the Iris Dataset
# ==============================================================================
def run_iris_classification():
"""
执行完整的鸢尾花分类XGBoost工作流,
包括使用Optuna进行自动化超参数调优和结果可视化。
"""
print("==========================================================")
print("= Running XGBoost for Iris Multi-Class Classification =")
print("==========================================================")

# 1. 加载数据
iris = load_iris()
X = iris.data
y = iris.target
feature_names = iris.feature_names
print(f"Iris dataset loaded. Shape: {X.shape}, Number of classes: {len(np.unique(y))}\n")

# 2. 划分数据
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)

# 3. 使用Optuna进行自动化超参数调优
def objective_iris(trial):
"""定义Optuna要优化的目标函数。"""
params = {
'objective': 'multi:softprob',
'num_class': 3,
'eval_metric': 'mlogloss',
'verbosity': 0,
'booster': 'gbtree',
'tree_method': 'hist', # 确保使用CPU
'lambda': trial.suggest_float('lambda', 1e-8, 1.0, log=True),
'alpha': trial.suggest_float('alpha', 1e-8, 1.0, log=True),
'max_depth': trial.suggest_int('max_depth', 2, 7),
'eta': trial.suggest_float('eta', 0.01, 0.3, log=True),
'subsample': trial.suggest_float('subsample', 0.5, 1.0),
'colsample_bytree': trial.suggest_float('colsample_bytree', 0.5, 1.0),
}

model = xgb.XGBClassifier(**params)
score = cross_val_score(model, X_train, y_train, cv=5, scoring='accuracy', n_jobs=-1).mean()
return score

print("--- Starting Hyperparameter Tuning with Optuna ---")
study_iris = optuna.create_study(direction='maximize')
study_iris.optimize(objective_iris, n_trials=50)

print("\nIris Tuning Finished!")
print(f"Best CV Accuracy: {study_iris.best_value:.4f}")
best_params_iris = study_iris.best_params
print("Best Hyperparameters:", best_params_iris)

if is_optuna_viz_available:
print("\n--- Visualizing Tuning Process ---")
plot_optimization_history(study_iris).update_layout(title="Iris: Optimization History").show()
plot_param_importances(study_iris).update_layout(title="Iris: Hyperparameter Importances").show()

# 4. 使用最佳参数训练最终模型
print("\n--- Training Final Model on Full Training Data ---")
final_model_iris = xgb.XGBClassifier(
objective='multi:softprob',
num_class=3,
eval_metric='mlogloss',
tree_method='hist',
**best_params_iris
)
X_train_df = pd.DataFrame(X_train, columns=feature_names)
final_model_iris.fit(X_train_df, y_train)

# 5. 在测试集上评估
print("\n--- Evaluating Final Model on Test Set ---")
X_test_df = pd.DataFrame(X_test, columns=feature_names)
y_pred_iris = final_model_iris.predict(X_test_df)
accuracy_iris = accuracy_score(y_test, y_pred_iris)
print(f"Test Set Accuracy: {accuracy_iris:.4f}")

# --- 新增:可视化评估结果和特征重要性 ---
# 1. 可视化特征重要性
plt.figure(figsize=(10, 6))
xgb.plot_importance(final_model_iris, ax=plt.gca())
plt.title("Iris: Feature Importance")
plt.tight_layout()
plt.show()

# 2. 可视化混淆矩阵
cm = confusion_matrix(y_test, y_pred_iris)
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=iris.target_names, yticklabels=iris.target_names)
plt.xlabel('Predicted Label')
plt.ylabel('True Label')
plt.title('Iris: Confusion Matrix')
plt.show()

print("\nClassification Report:")
print(classification_report(y_test, y_pred_iris, target_names=iris.target_names))

# --- 新增:SHAP模型解释分析 ---
if not is_shap_available:
print("\n❌ SHAP库不可用,跳过模型解释分析")
return

print("\n--- SHAP Model Explanation Analysis ---")

# 为了避免CUDA/CPU设备不匹配的问题,创建一个CPU版本的模型用于SHAP分析
print("正在准备SHAP分析模型...")
shap_model_iris = xgb.XGBClassifier(
objective='multi:softprob', num_class=3, eval_metric='mlogloss',
tree_method='hist', **best_params_iris
)
shap_model_iris.fit(X_train_df, y_train)
explainer = shap.TreeExplainer(shap_model_iris)

print("正在计算SHAP值...")
# 使用训练集的一个子集来计算SHAP值,以提高效率
shap_sample_data = X_train_df.sample(n=100, random_state=42)
shap_values = explainer(shap_sample_data)

# 1. SHAP Summary Plot (Beeswarm) - 显示每个特征对每个类别的重要性
print("\n正在生成 SHAP 多分类汇总图 (Beeswarm)...")
shap.summary_plot(shap_values, shap_sample_data,
class_names=iris.target_names,
feature_names=feature_names)

# 2. SHAP Bar Plot - 显示平均特征重要性
print("\n正在生成 SHAP 特征重要性条形图...")
shap.summary_plot(shap_values, shap_sample_data,
plot_type="bar",
feature_names=feature_names,
class_names=iris.target_names,
show=False)
plt.title("Iris: SHAP Feature Importance (Mean Absolute SHAP Values)")
plt.tight_layout()
plt.show()

# 3. 选择几个样本进行详细的SHAP解释
print("\n--- Individual Sample SHAP Analysis ---")
sample_indices = [0, 5, 10]
X_test_sample = X_test_df.iloc[sample_indices]
y_test_sample = y_test[sample_indices]

# 批量计算样本的SHAP值
y_pred_sample_cpu = shap_model_iris.predict(X_test_sample)
explainer_sample = shap.TreeExplainer(shap_model_iris)
shap_values_sample = explainer_sample(X_test_sample)

for i, idx in enumerate(sample_indices):
print(f"\n分析测试样本 {idx}:")
print(f" 真实标签: {iris.target_names[y_test_sample[i]]}")
print(f" 预测标签: {iris.target_names[y_pred_sample_cpu[i]]}")

# 显示Force Plot (适用于单个预测)
pred_class = y_pred_sample_cpu[i]
print(f" 为预测类别 '{iris.target_names[pred_class]}' 生成力图...")
shap.force_plot(explainer_sample.expected_value[pred_class],
shap_values_sample.values[i, :, pred_class],
X_test_sample.iloc[i],
feature_names=feature_names,
matplotlib=True, show=True,
text_rotation=10)


# ==============================================================================
# Part 2: Regression with the California Housing Dataset
# ==============================================================================
def run_california_housing_regression():
"""
执行完整的加州房价预测XGBoost工作流,
包括使用Optuna进行自动化超参数调优和结果可视化。
"""
print("\n\n==========================================================")
print("= Running XGBoost for California Housing Price Prediction =")
print("==========================================================")

# 1. 加载数据
housing = fetch_california_housing()
X = housing.data
y = housing.target
feature_names = housing.feature_names
print(f"California Housing dataset loaded. Shape: {X.shape}\n")

# 2. 划分数据
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 3. 使用Optuna进行自动化超参数调优
def objective_housing(trial):
"""
定义Optuna要优化的目标函数。
"""
params = {
'objective': 'reg:squarederror',
'eval_metric': 'rmse',
'verbosity': 0,
'booster': 'gbtree',
'tree_method': 'hist', # 确保使用CPU
'lambda': trial.suggest_float('lambda', 1e-8, 10.0, log=True),
'alpha': trial.suggest_float('alpha', 1e-8, 10.0, log=True),
'max_depth': trial.suggest_int('max_depth', 3, 10),
'eta': trial.suggest_float('eta', 0.01, 0.3, log=True),
'subsample': trial.suggest_float('subsample', 0.6, 1.0),
'colsample_bytree': trial.suggest_float('colsample_bytree', 0.6, 1.0),
}

model = xgb.XGBRegressor(**params)
score = cross_val_score(model, X_train, y_train, cv=5, scoring='neg_root_mean_squared_error', n_jobs=-1).mean()
return score

print("--- Starting Hyperparameter Tuning with Optuna ---")
study_housing = optuna.create_study(direction='maximize')
study_housing.optimize(objective_housing, n_trials=50)

print("\nHousing Tuning Finished!")
print(f"Best CV RMSE: {-study_housing.best_value:.4f}")
best_params_housing = study_housing.best_params
print("Best Hyperparameters:", best_params_housing)

if is_optuna_viz_available:
print("\n--- Visualizing Tuning Process ---")
plot_optimization_history(study_housing).update_layout(title="Housing: Optimization History").show()
plot_param_importances(study_housing).update_layout(title="Housing: Hyperparameter Importances").show()

# 4. 使用最佳参数训练最终模型
print("\n--- Training Final Model on Full Training Data ---")
final_model_housing = xgb.XGBRegressor(
objective='reg:squarederror',
eval_metric='rmse',
tree_method='hist',
**best_params_housing
)
X_train_df = pd.DataFrame(X_train, columns=feature_names)
final_model_housing.fit(X_train_df, y_train)

# 5. 在测试集上评估
print("\n--- Evaluating Final Model on Test Set ---")
X_test_df = pd.DataFrame(X_test, columns=feature_names)
y_pred_housing = final_model_housing.predict(X_test_df)

rmse = np.sqrt(mean_squared_error(y_test, y_pred_housing))
mae = mean_absolute_error(y_test, y_pred_housing)
r2 = r2_score(y_test, y_pred_housing)

print(f"Root Mean Squared Error (RMSE): {rmse:.4f}")
print(f"Mean Absolute Error (MAE): {mae:.4f}")
print(f"R-squared (R²): {r2:.4f}")

# --- 新增:可视化评估结果和特征重要性 ---
# 1. 可视化特征重要性
plt.figure(figsize=(10, 6))
xgb.plot_importance(final_model_housing, ax=plt.gca())
plt.title("Housing: Feature Importance")
plt.tight_layout()
plt.show()

# 2. 可视化真实值 vs 预测值
plt.figure(figsize=(10, 10))
sns.scatterplot(x=y_test, y=y_pred_housing, alpha=0.5)
plt.plot([min(y_test), max(y_test)], [min(y_test), max(y_test)], '--', color='red', linewidth=2)
plt.xlabel('Actual Prices ($100,000s)')
plt.ylabel('Predicted Prices ($100,000s)')
plt.title('Housing: Actual vs. Predicted Prices')
plt.axis('equal')
plt.axis('square')
plt.tight_layout()
plt.show()

# --- 新增:SHAP模型解释分析 ---
if not is_shap_available:
print("\n❌ SHAP库不可用,跳过模型解释分析")
return

print("\n--- SHAP Model Explanation Analysis ---")

# --- 修复:为SHAP分析创建专用的CPU模型,保证稳定性 ---
print("正在准备SHAP分析模型...")
shap_model_housing = xgb.XGBRegressor(
objective='reg:squarederror', eval_metric='rmse',
tree_method='hist', **best_params_housing
)
shap_model_housing.fit(X_train_df, y_train)
explainer = shap.TreeExplainer(shap_model_housing)

print("正在计算SHAP值...")
shap_sample_data = X_train_df.sample(n=200, random_state=42)
shap_values = explainer(shap_sample_data)

# 1. SHAP Summary Plot (Beeswarm) - 显示特征重要性和影响方向
print("\n正在生成 SHAP 汇总图 (Beeswarm)...")
shap.summary_plot(shap_values, shap_sample_data, show=False)
plt.title("Housing: SHAP Summary Plot - Feature Impact on Predictions")
plt.tight_layout()
plt.show()

# 2. SHAP Bar Plot - 显示平均绝对SHAP值
print("\n正在生成 SHAP 特征重要性条形图...")
shap.summary_plot(shap_values, plot_type="bar", show=False)
plt.title("Housing: SHAP Feature Importance (Mean Absolute SHAP Values)")
plt.tight_layout()
plt.show()

# 3. 选择几个样本进行详细的SHAP解释
print("\n--- Individual Sample SHAP Analysis ---")
sample_indices = [0, 1, 2]
X_test_sample = X_test_df.iloc[sample_indices]
y_test_sample = y_test[sample_indices]
y_pred_sample = shap_model_housing.predict(X_test_sample)

# 批量计算单个样本的SHAP值
explainer_sample = shap.TreeExplainer(shap_model_housing)
shap_values_sample = explainer_sample(X_test_sample)

for i, idx in enumerate(sample_indices):
print(f"\n分析测试样本 {idx}:")
print(f" 真实房价: ${y_test_sample[i]:.2f} (万美元)")
print(f" 预测房价: ${y_pred_sample[i]:.2f} (万美元)")

# 显示 Waterfall Plot
print(f" 为样本 {idx} 生成瀑布图...")
shap.plots.waterfall(shap_values_sample[i], show=True)

# 显示 Force Plot
print(f" 为样本 {idx} 生成力图...")
shap.force_plot(explainer_sample.expected_value, shap_values_sample.values[i, :],
X_test_sample.iloc[i], matplotlib=True, show=True)

# 4. 特征交互分析
try:
print("\n--- Feature Interaction Analysis ---")
print("正在生成 MedInc 的依赖图,并根据 HouseAge 着色...")
shap.plots.scatter(shap_values[:, "MedInc"],
color=shap_values[:, "HouseAge"],
show=True)
except Exception as e:
print(f"特征交互分析失败: {e}")


# --- 主执行块 ---
if __name__ == '__main__':
run_iris_classification()
run_california_housing_regression()

k-近邻算法(KNN)

k-近邻(kNN)算法是监督学习中最简单、最直观的算法之一。它是一种“懒惰学习”(Lazy Learning)或基于实例的学习(Instance-based Learning)方法,本身没有显式的训练过程。

其核心思想为对于一个未知样本的分类,可以观察其在特征空间中最近的k个邻居,并由这些邻居的类别通过多数表决的方式来决定该样本的类别

这个思想可以应用于两个主要的机器学习任务:

  • 分类(Classification):新样本的类别由其k个最近邻居中出现次数最多的类别(即众数)决定
  • 回归(Regression):新样本的预测值是其k个最近邻居的数值的平均值或中位数

核心步骤

kNN算法的实现过程非常清晰,可以分为以下几个步骤:

先要对特征归一化标准化

  1. 确定关键参数
    • k值:选择一个正整数k,代表要观察的最近邻居的数量
    • 距离度量:选择一种计算样本之间距离的方法(如欧几里得距离)

k 太小容易过拟合,k 太大容易欠拟合,一般 kNk \le \sqrt{N},可以通过交叉验证等方法确定最优值

  • 欧几里得距离(Euclidean Distance):
    最常用的距离度量,表示两点之间的直线距离。对于两个n维向量 A=(x1,x2,...,xn)A=(x_1, x_2, ..., x_n)B=(y1,y2,...,yn)B=(y_1, y_2, ..., y_n),其欧几里得距离为:
$$
D(A, B) = \sqrt{\sum_{i=1}^{n} (x_i - y_i)^2}
$$
  • 曼哈顿距离(Manhattan Distance):
    也称为“城市街区距离”,表示在标准坐标系下两点之间沿着轴线走的路径总长

    D(A,B)=i=1nxiyiD(A, B) = \sum_{i=1}^{n} |x_i - y_i|

  • 闵可夫斯基距离(Minkowski Distance):
    欧几里得距离和曼哈顿距离的推广形式

    D(A,B)=(i=1nxiyip)1pD(A, B) = \left( \sum_{i=1}^{n} |x_i - y_i|^p \right)^{\frac{1}{p}}

    • p=1p=1 时,为曼哈顿距离
    • p=2p=2 时,为欧几里得距离
  1. 计算距离

    • 对于一个待预测的新样本,计算它与训练数据集中每一个样本之间的距离
  2. 找到k个最近邻

    • 将计算出的所有距离进行排序,找出距离最小的前k个样本,这些样本就是新样本的“k个最近邻”
  3. 做出决策

    • 对于分类任务:统计这k个邻居的类别。选择出现频率最高的类别作为新样本的预测类别。为避免平票,k通常取奇数
    • 对于回归任务:计算这k个邻居的目标值的平均值(或其他聚合值,如中位数),将该平均值作为新样本的预测值

优缺点分析

优点

  1. 简单直观:算法原理简单,易于理解和实现
  2. 无需训练:kNN是一种懒惰学习算法,没有显式的训练阶段,数据可以直接用于预测。这使得它可以轻松适应新数据
  3. 非参数模型:对数据分布没有假设,适用于各种复杂和非线性的决策边界
  4. 对异常值不敏感(在k值较大时)

缺点

  1. 计算成本高:在预测阶段,需要计算待测样本与所有训练样本的距离,当训练集非常大时,计算量巨大,效率低下
  2. 内存消耗大:需要存储全部训练数据集
  3. 对不相关特征和数据维度敏感:在高维空间中,点之间的距离变得难以区分(所谓的**“维度灾难”**),不相关的特征会严重干扰距离计算,导致算法性能下降
  4. 样本不均衡问题:如果某些类别的样本数量远多于其他类别,那么新样本很容易被归类到这些多数类中

示例代码

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
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
from collections import Counter
from matplotlib.colors import ListedColormap
from sklearn.neighbors import KNeighborsClassifier


def visualize_knn_process():
"""
一个完整的函数,用于分步可视化kNN算法的决策过程。
"""
# --- 0. 初始设置 ---
# 设置一个好看的绘图风格
sns.set_style("whitegrid")
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号

# --- 1. 已知的电影数据 ---
# 特征: [爆炸数量, 搞笑桥段数量]
action_movies = np.array([[10, 2], [12, 1], [15, 0], [9, 3], [11, 2]])
comedy_movies = np.array([[1, 10], [3, 12], [0, 9], [2, 8], [4, 11]])

# 将所有已知点合并,方便计算
all_movies = np.vstack((action_movies, comedy_movies))
all_labels = ['动作片'] * len(action_movies) + ['喜剧片'] * len(comedy_movies)

# 未知的新电影
new_movie = np.array([7, 6])

# --- 绘图 1: 显示已知数据 ---
plt.figure(figsize=(10, 7))
plt.scatter(action_movies[:, 0], action_movies[:, 1], c='red', s=100, label='动作片 (Action)')
plt.scatter(comedy_movies[:, 0], comedy_movies[:, 1], c='blue', s=100, label='喜剧片 (Comedy)')
plt.title('步骤 1: 已知的电影数据', fontsize=16)
plt.xlabel('爆炸场景数量', fontsize=12)
plt.ylabel('搞笑桥段数量', fontsize=12)
plt.legend()
plt.grid(True)
plt.show()

# --- 绘图 2: 加入未知电影 ---
plt.figure(figsize=(10, 7))
plt.scatter(action_movies[:, 0], action_movies[:, 1], c='red', s=100, label='动作片 (Action)')
plt.scatter(comedy_movies[:, 0], comedy_movies[:, 1], c='blue', s=100, label='喜剧片 (Comedy)')
plt.scatter(new_movie[0], new_movie[1], c='grey', s=250, marker='*', label='未知电影', edgecolors='black')
plt.title('步骤 2: 出现一个未知的新电影', fontsize=16)
plt.xlabel('爆炸场景数量', fontsize=12)
plt.ylabel('搞笑桥段数量', fontsize=12)
plt.legend()
plt.grid(True)
plt.show()

# --- 3. 计算距离并寻找邻居 (K=3) ---
# 计算新电影到所有已知电影的欧几里得距离
distances = np.sqrt(np.sum((all_movies - new_movie) ** 2, axis=1))

# 找到最近的 k=3 个邻居的索引
k = 3
nearest_indices = np.argsort(distances)[:k]
nearest_neighbors = all_movies[nearest_indices]

# --- 绘图 3: 显示最近的邻居 ---
plt.figure(figsize=(10, 7))
plt.scatter(action_movies[:, 0], action_movies[:, 1], c='red', s=100, alpha=0.3)
plt.scatter(comedy_movies[:, 0], comedy_movies[:, 1], c='blue', s=100, alpha=0.3)
plt.scatter(new_movie[0], new_movie[1], c='grey', s=250, marker='*', label='未知电影', edgecolors='black')
plt.scatter(nearest_neighbors[:, 0], nearest_neighbors[:, 1], s=150,
facecolors='none', edgecolors='green', linewidth=3, label=f'最近的 {k} 个邻居')
for neighbor in nearest_neighbors:
plt.plot([new_movie[0], neighbor[0]], [new_movie[1], neighbor[1]], 'g--')
plt.title(f'步骤 3: K={k}, 寻找最近的{k}个邻居', fontsize=16)
plt.xlabel('爆炸场景数量', fontsize=12)
plt.ylabel('搞笑桥段数量', fontsize=12)
plt.legend()
plt.grid(True)
plt.show()

# --- 4. 邻居投票并决定分类 ---
neighbor_labels = [all_labels[i] for i in nearest_indices]
vote_result = Counter(neighbor_labels)
final_decision = vote_result.most_common(1)[0][0]
final_color = 'blue' if final_decision == '喜剧片' else 'red'

# --- 绘图 4: 显示最终分类结果 ---
plt.figure(figsize=(10, 7))
plt.scatter(action_movies[:, 0], action_movies[:, 1], c='red', s=100, alpha=0.3)
plt.scatter(comedy_movies[:, 0], comedy_movies[:, 1], c='blue', s=100, alpha=0.3)
plt.scatter(new_movie[0], new_movie[1], c=final_color, s=250, marker='*',
label=f'最终分类: {final_decision}', edgecolors='black')
plt.scatter(nearest_neighbors[:, 0], nearest_neighbors[:, 1], s=150,
facecolors='none', edgecolors='green', linewidth=3)
vote_text = "投票结果:\n"
for label, count in vote_result.items():
vote_text += f"{label}: {count}票\n"
plt.text(0.5, 6, vote_text, fontsize=14, bbox=dict(facecolor='yellow', alpha=0.5))
plt.title(f'步骤 4: 邻居投票决定 (K={k}),结果为 {final_decision}', fontsize=16)
plt.xlabel('爆炸场景数量', fontsize=12)
plt.ylabel('搞笑桥段数量', fontsize=12)
plt.legend()
plt.grid(True)
plt.show()

# --- 5. (进阶) 绘制决策边界 ---
# 使用scikit-learn来简化决策边界的绘制
X = all_movies
# 将标签转换为数字 (0: 喜剧片, 1: 动作片)
y = np.array([1 if label == '动作片' else 0 for label in all_labels])

# 训练一个k=3的kNN模型
knn_classifier = KNeighborsClassifier(n_neighbors=k)
knn_classifier.fit(X, y)

# 创建网格来绘制决策边界
x_min, x_max = X[:, 0].min() - 2, X[:, 0].max() + 2
y_min, y_max = X[:, 1].min() - 2, X[:, 1].max() + 2
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.1),
np.arange(y_min, y_max, 0.1))

# 预测网格中每个点的类别
Z = knn_classifier.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)

# 自定义颜色 (0: 蓝色, 1: 红色)
cmap_light = ListedColormap(['#A0C4FF', '#FFB3B3'])
cmap_bold = ['blue', 'red']

# 绘制决策边界和数据点
plt.figure(figsize=(12, 8))
plt.contourf(xx, yy, Z, cmap=cmap_light)

# 绘制原始数据点
# 注意:这里的hue需要是数字标签y
sns.scatterplot(x=X[:, 0], y=X[:, 1], hue=y, palette=cmap_bold, s=150, edgecolor="k")

# 绘制我们的新电影点
plt.scatter(new_movie[0], new_movie[1], c=final_color, s=300, marker='*',
label=f'未知电影 (被分类为{final_decision})', edgecolors='black')

plt.title(f'kNN的决策边界 (K={k})', fontsize=18)
plt.xlabel('爆炸场景数量', fontsize=14)
plt.ylabel('搞笑桥段数量', fontsize=14)
# 手动创建图例
handles, labels = plt.gca().get_legend_handles_labels()
# 自定义图例标签
new_labels = ['喜剧片', '动作片', f'未知电影 (被分类为{final_decision})']
plt.legend(handles=handles, labels=new_labels, fontsize=12)
plt.show()


if __name__ == '__main__':
# 运行可视化函数
visualize_knn_process()

SVM 支持向量机

支持向量机是一种功能强大且用途广泛的监督学习算法,核心为不仅要找到一条能分开两类点的线(或更高维度的超平面),还要让这条线到两边数据点的**间隔(Margin)**尽可能宽,使得SVM具有很强的泛化能力,即对新数据的预测能力更强

超平面(Hyperplane): 在二维空间里是一条直线,在三维空间里是一个平面,在更高维度的空间中,这个分界被称为超平面。它是决策的边界

间隔(Margin):决策边界与离它最近的样本点之间的距离。SVM的目标就是最大化这个间隔

支持向量(Support Vectors):那些离决策边界最近的样本点(即图中分割线上的点),它们像“支撑”着整个决策边界一样,决定了间隔的宽度和超平面的最终位置。如果移动支持向量,超平面就会改变;如果移动其他非支持向量,超平面则不会受到影响。这是SVM高效的关键之一

硬间隔与软间隔:

  • 硬间隔完美分类所有点,同时最大化间隔,要求数据必须是完全线性可分
  • 软间隔在最大化间隔和最小化分类错误之间找到平衡,可以处理非线性可分有噪声的数据,鲁棒性更强

核心内容推导

模型定义

优化目标:最大化间隔

硬间隔的约束

其中

yi(wxi+b)10,i=1,,Ny_i(w \cdot x_i + b) - 1 \geq 0,\quad i = 1,\dots,N

即为原始可行性条件(Primal Feasibility)

然后就得到了以下的模型定义,即优化问题,再构建拉格朗日函数

最后得到了硬间隔KKT 条件:

  1. 原始可行性条件(Primal Feasibility)

yi(wxi+b)10,i=1,,Ny_i\,(w \cdot x_i + b) - 1 \ge 0,\quad i = 1,\dots,N

来源:SVM 原始优化问题的约束条件本身
含义:定义了 SVM 的“间隔”。要求每个样本点 ii 的函数间隔 yi(wxi+b)y_i(w \cdot x_i + b) 必须 1\ge 1,即所有样本点都被正确分类,且位于间隔边界上或之外

  1. 平稳性条件(Stationarity)
  • ww 的平稳性

wi=1Nλyixi=0w=i=1Nλiyixiw - \sum_{i=1}^{N}\lambda y_i x_i = 0 \quad\Longrightarrow\quad w = \sum_{i=1}^{N}\lambda_i y_i x_i

来源:拉格朗日函数 LL 对变量 ww 求偏导并令其为零 Lw=0\frac{\partial L}{\partial w}=0
含义:决策边界的法向量 ww 可表示为所有训练样本向量 xix_i 的加权线性组合;权重 αi\alpha_i 为拉格朗日乘子。只有支持向量的 αi>0\alpha_i>0,其余为零,因此 ww 完全由支持向量决定

  • bb 的平稳性

i=1Nλiyi=0i=1Nλiyi=0-\sum_{i=1}^{N}\lambda_i y_i = 0 \quad\Longrightarrow\quad \sum_{i=1}^{N}\lambda_i y_i = 0

来源:拉格朗日函数 LL 对变量 bb 求偏导并令其为零 Lb=0\frac{\partial L}{\partial b}=0
含义:对偶问题的约束条件,要求所有拉格朗日乘子与对应标签乘积之和为零

  1. 互补松弛条件(Complementary Slackness)

λi[yi(wxi+b)1]=0,i=1,,N\lambda_i\,\bigl[\,y_i(w \cdot x_i + b) - 1\,\bigr] = 0,\quad i = 1,\dots,N

来源:KKT 核心条件
含义:对任意样本点,必有下列两种情形之一:

  • 情形 A: λi=0\lambda_i = 0 ,该点 不是 支持向量,对决策边界无贡献;通常远离间隔边界且已正确分类
  • 情形 B: yi(wxi+b)1=0y_i(w \cdot x_i + b) - 1 = 0 ,该点恰位于间隔边界上,此时 λi>0\lambda_i > 0,即为 支持向量(Support Vectors)
  1. 对偶可行性条件(Dual Feasibility)

λi0,i=1,,N\lambda_i \ge 0,\quad i = 1,\dots,N

来源:拉格朗日乘子法对不等式约束的标准要求
含义:所有拉格朗日乘子必须非负

对偶函数

构造下界函数q(λ),令该函数一定小于目标函数的最小值,即 q(λ)f(w)q(\lambda) \leq f(w^*),通过求q(λ)最大值得到目标函数f(w)f(w^*)的最小值,从而将求最小值的原问题转换成求最大值的对偶问题

其中所有SVM问题(包括软&硬间隔)都是强对偶性,属于凸优化且满足斯莱特条件,因此max(q) = min(f)

将上述KKT条件全部代入后得到最终的对偶问题为:

  • Maximize:

    q(λ)=i=1sλi12i=1sj=1sλiλjyiyj(xixj)q(\lambda) = \sum_{i=1}^{s} \lambda_i - \frac{1}{2} \sum_{i=1}^{s} \sum_{j=1}^{s} \lambda_i \lambda_j y_i y_j (x_i \cdot x_j)

  • Subject to:

    1. i=1sλiyi=0\sum_{i=1}^{s} \lambda_i y_i = 0
    2. λi0,i=1,2,,s\lambda_i \geq 0, \quad i = 1, 2, \ldots, s

核方法

当原数据难以被超平面划分时,可以通过升维来提高区分度

**核技巧:**通过核函数直接计算出高维空间中的点积结果,从而避免对原始数据点进行升维计算

多项式核

高斯核

K(xi,xj)=eγxixj2K(\vec{x}_i, \vec{x}_j) = e^{-\gamma \lVert \vec{x}_i - \vec{x}_j \rVert^2}

其中 γ\gamma 的值影响着不同距离下数据点的区分度,效果类似于KNN中的k值

对于SVM非线性分类,可以直接尝试使用高斯核

构建并求解核化的对偶问题

  • 目标函数

maxλq(λ)=i=1sλi12i=1sj=1sλiλjyiyjK(xi,xj)\max_{\boldsymbol{\lambda}} \quad q(\boldsymbol{\lambda}) = \sum_{i=1}^{s} \lambda_i - \frac{1}{2}\sum_{i=1}^{s}\sum_{j=1}^{s} \lambda_i \lambda_j y_i y_j K(\vec{x_i}, \vec{x_j})

  • 约束条件

    1. i=1sλiyi=0\sum_{i=1}^{s} \lambda_i y_i = 0

    2. 0λiC(对于软间隔SVM)0 \le \lambda_i \le C \quad (\text{对于软间隔SVM})

获得最优解 λi\lambda_i^*

​ 将上述二次规划(QP)问题输入优化求解器,得到最优的拉格朗日乘子向量 λ=(λ1,λ2,...,λs)\boldsymbol{\lambda}^* = (\lambda_1^*, \lambda_2^*, ..., \lambda_s^*)

计算偏置项 bb

  1. 识别支持向量 (Support Vectors, SV)

    • 根据求解出的 λ\boldsymbol{\lambda}^*,所有满足 λi>0\lambda_i^* > 0 的样本点 xi\vec{x_i} 都是支持向量
  2. 选择一个边界上的支持向量

    • 选择一个满足 0<λs<C0 < \lambda_s^* < C 的支持向量 (xs,ys)(\vec{x_s}, y_s)。(这保证了该点严格在间隔边界上)
  3. 计算 bb

    • 利用KKT条件 ys(wϕ(xs)+b)=1y_s (\vec{w} \cdot \phi(\vec{x_s}) + b) = 1 和关系式 w=i=1sλiyiϕ(xi)\vec{w} = \sum_{i=1}^{s} \lambda_i^* y_i \phi(\vec{x_i}),推导出 bb 的计算公式:

      b=ysiSVλiyiK(xi,xs)b = y_s - \sum_{i \in \text{SV}} \lambda_i^* y_i K(\vec{x_i}, \vec{x_s})

为提高稳健性,实践中通常会计算所有边界支持向量的 bb 值,然后取其平均值。

决策函数

  1. 写出决策函数的形式

    • 分类新样本 xnew\vec{x}_{new} 的决策函数为:

      f(xnew)=wϕ(xnew)+bf(\vec{x}_{new}) = \vec{w} \cdot \phi(\vec{x}_{new}) + b

  2. 代入并使用核函数替换

    • w\vec{w} 的表达式代入,并应用核技巧:

f(xnew)=(iSVλiyiϕ(xi))ϕ(xnew)+bf(\vec{x}_{new}) = \left( \sum_{i \in \text{SV}} \lambda_i^* y_i \phi(\vec{x_i}) \right) \cdot \phi(\vec{x}_{new}) + b

\Downarrow

f(xnew)=iSVλiyiK(xi,xnew)+bf(\vec{x}_{new}) = \sum_{i \in \text{SV}} \lambda_i^* y_i K(\vec{x_i}, \vec{x}_{new}) + b

  1. 分类预测
  • 最终的分类规则是判断决策函数的符号:

Predicted Class=sign(f(xnew))\text{Predicted Class} = \text{sign}(f(\vec{x}_{new}))

  • 如果 f(xnew)>0f(\vec{x}_{new}) > 0,预测为正类 (+1)
  • 如果 f(xnew)<0f(\vec{x}_{new}) < 0,预测为负类 (-1)

软间隔

  • 松弛变量 ξi\xi_i:为每个样本点 xi\vec{x_i} 引入一个非负的松弛变量(ξi0\xi_i \ge 0),用以衡量该样本点违反间隔约束的程度

    • ξi=0\xi_i = 0 表示样本点遵守规则,位于间隔边界上或之外
    • 0<ξi10 < \xi_i \le 1 表示样本点进入了间隔区域,但仍被正确分类
    • ξi>1\xi_i > 1 表示样本点被错误分类

    对于于一个已经训练好的SVM模型(即我们已经找到了最优的 ww^*bb^*),ξi\xi_i 的值为:

    ξi=max(0,1yi(wxi+b))\xi_i = \max(0, 1 - y_i(w^* \cdot x_i + b^*))

  • 惩罚系数 CC:一个需要手动设置的超参数(C>0C > 0),用于控制对松弛量的惩罚力度,它在“最大化间隔”和“最小化分类错误”两个目标之间进行权衡

    • CC 值意味着对错误的惩罚很重,模型会更倾向于将所有点正确分类,容易过拟合
    • CC 值意味着对错误的惩罚较轻,模型会更注重获得一个宽的间隔,容忍更多错误,泛化能力可能更强

软间隔的优化目标

  • 目标函数:

minw,b,ξ12w2+Ci=1sξi\min_{\vec{w}, b, \boldsymbol{\xi}} \quad \frac{1}{2}||\vec{w}||^2 + C \sum_{i=1}^{s} \xi_i

  • 约束条件:

yi(wxi+b)1ξiy_i(\vec{w} \cdot \vec{x_i} + b) \ge 1 - \xi_i

ξi0,i=1,2,...,s\xi_i \ge 0, \quad i=1, 2, ..., s

  1. 对偶问题与支持向量的变化

    对偶问题的主要变化在于拉格朗日乘子 λi\lambda_i 的约束从 λi0\lambda_i \ge 0 变为了有上界的约束:

0λiC0 \le \lambda_i \le C

  • 这个变化也丰富了支持向量(SV)的类型:
    • λi=0\lambda_i = 0:非支持向量
    • 0<λi<C0 < \lambda_i < C:边界上的支持向量,这些点严格位于间隔边界上(ξi=0\xi_i = 0
    • λi=C\lambda_i = C:被约束的支持向量,这些点是违反了间隔约束的点(ξi>0\xi_i > 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
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
import numpy as np
import matplotlib.pyplot as plt
from sklearn import svm, datasets
from sklearn.preprocessing import StandardScaler

plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号


# 创建一个函数来绘制决策边界,这样可以复用代码
def plot_svm_boundary(clf, X, y, title):
"""
绘制SVM的决策边界、间隔和支持向量
"""
# 创建一个网格来绘制决策区域
h = .02 # 网格的步长
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
np.arange(y_min, y_max, h))

# 获得决策函数的值
Z = clf.decision_function(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)

# 绘制决策边界和间隔
plt.contourf(xx, yy, Z, cmap=plt.cm.coolwarm, alpha=0.3) # 填充决策区域
plt.contour(xx, yy, Z, colors='k', levels=[-1, 0, 1], alpha=0.5,
linestyles=['--', '-', '--']) # 绘制间隔和边界

# 绘制数据点
plt.scatter(X[:, 0], X[:, 1], c=y, cmap=plt.cm.coolwarm, s=50, edgecolors='k')

# 突出显示支持向量
plt.scatter(clf.support_vectors_[:, 0], clf.support_vectors_[:, 1],
s=100, facecolors='none', edgecolors='k', linewidth=2,
label='Support Vectors')

plt.title(title)
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.legend()
plt.axis('tight')


# --- 1. 线性SVM示例 ---
print("正在生成线性SVM图...")
# 创建一个基本线性可分的数据集
X_linear, y_linear = datasets.make_blobs(n_samples=100, centers=2, random_state=6)

# 创建一个线性核的SVC(Support Vector Classifier)实例
# C值设得很大,使其行为趋向于硬间隔
clf_linear = svm.SVC(kernel='linear', C=1000)
clf_linear.fit(X_linear, y_linear)

# 绘制结果
plt.figure(figsize=(8, 6))
plot_svm_boundary(clf_linear, X_linear, y_linear, '1. 线性SVM (趋向硬间隔)')
plt.show()

# --- 2. 软间隔的效果 (对比不同的C值) ---
print("正在生成软间隔SVM对比图...")
# 创建一个有一些重叠的数据集
X_overlap, y_overlap = datasets.make_blobs(n_samples=100, centers=2, random_state=0, cluster_std=1.2)

# C值很大,对错误惩罚很重,间隔较窄
clf_high_c = svm.SVC(kernel='linear', C=100)
clf_high_c.fit(X_overlap, y_overlap)

# C值很小,对错误惩罚很轻,间隔较宽
clf_low_c = svm.SVC(kernel='linear', C=0.1)
clf_low_c.fit(X_overlap, y_overlap)

# 绘制对比图
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plot_svm_boundary(clf_high_c, X_overlap, y_overlap, '2a. 软间隔 (高C值)')

plt.subplot(1, 2, 2)
plot_svm_boundary(clf_low_c, X_overlap, y_overlap, '2b. 软间隔 (低C值)')
plt.tight_layout()
plt.show()


# --- 3. 核函数SVM示例 (RBF核) ---
print("正在生成核函数SVM图...")
# 创建一个非线性可分的数据集(圆环形)
X_nonlinear, y_nonlinear = datasets.make_circles(n_samples=100, factor=.5, noise=.1)

# 重要:对于非线性核,特别是RBF核,数据标准化非常重要
scaler = StandardScaler()
X_nonlinear_scaled = scaler.fit_transform(X_nonlinear)

# 创建一个RBF核的SVC实例
clf_rbf = svm.SVC(kernel='rbf', C=1, gamma='auto')
clf_rbf.fit(X_nonlinear_scaled, y_nonlinear)

# 绘制结果
plt.figure(figsize=(8, 6))
plot_svm_boundary(clf_rbf, X_nonlinear_scaled, y_nonlinear, '3. 核函数SVM (RBF核)')
plt.show()

蒙特卡洛方法

蒙特卡洛方法,又称随机抽样法或统计试验法,是一种依赖于重复随机抽样来获得数值结果的计算方法。其核心思想是将一个难以通过确定性算法求解的问题,转化为一个等价的概率模型,然后通过进行大量随机试验,利用统计学原理(如大数定律)来估算其解

例如在一个边长为2的正方形内部,画一个半径为1的内切圆,随机地向这个正方形内投掷大量的点,统计落在圆内的点的数量(N_circle)和落在正方形内的总点数(N_square),根据几何概率,圆的面积与正方形面积之比为 (π * 1²) / (2²) = π / 4。同时,这个比值约等于落在圆内的点数与总点数之比,即 N_circle / N_square。因此,可以推导出 π ≈ 4 * (N_circle / N_square)

核心原理

该方法的理论基础是概率论中的大数定律:当样本数量足够大时,一个随机事件出现的频率会无限接近其真实概率。通过构造一个与问题解相关的随机试验,并进行海量重复,最终得到的统计平均值会收敛到问题的真实解

主要特点

  • 依赖随机性:使用伪随机数序列 (Pseudo-random Numbers) 来模拟随机过程
  • 概率收敛:结果是统计意义上的近似解,伴随着一定的统计误差
  • 收敛速度:误差收敛速度通常为 O(1/N)O(1/\sqrt{N}),其中 NN 是试验次数。这意味着为将误差减半,需要将计算量增加四倍
  • 通用性强:概念简单,易于实现,并且能广泛应用于各种领域,尤其是那些本身就包含不确定性或维度极高的问题

优缺点与应用

  • 优势:实现简单,能够直观地模拟复杂的物理或金融过程,且其收敛速度与问题维度无关,能有效规避“维度灾难”。
  • 局限:收敛速度相对较慢,获得高精度结果通常需要巨大的计算量
  • 典型应用:粒子物理学(模拟粒子输运)、金融风险分析(VaR计算)、计算机图形学、项目管理风险评估、人工智能等

拟蒙特卡洛方法 (Quasi-Monte Carlo Method)

拟蒙特卡洛方法是蒙特卡洛方法的一种改进,旨在通过更优化的抽样策略来提高计算效率和精度。它并非采用纯粹的随机抽样,而是使用经过精心设计的、确定性的序列来更均匀地探索整个样本空间

核心原理

该方法用低差异序列 (Low-discrepancy Sequences) 取代了伪随机数。所谓“低差异”,指的是点集在空间中分布得非常均匀,系统性地避免了随机抽样中可能出现的点“聚集”和“空白”区域。这种均匀性使得用更少的样本点就能达到对整个样本空间的有效覆盖

主要特点

  • 确定性序列:使用哈尔顿 (Halton)、索博尔 (Sobol’) 等确定性的拟随机数序列
  • 均匀覆盖:其设计目标是让样本点尽可能均匀地填充空间,新生成的点总会落在现有最“空旷”的位置
  • 收敛速度:在理想条件下,其误差收敛速度可接近 O(1/N)O(1/N),远快于标准蒙特卡洛方法
  • 效率驱动:专为提升数值积分和模拟的效率而设计

优缺点与应用

  • 优势:计算效率高,收敛速度快。在同样的计算成本下,通常能获得比标准蒙特卡洛方法精确得多的结果
  • 局限:在处理极高维度问题时,低差异序列的均匀性优势可能会减弱。同时,由于其确定性,误差估计比标准蒙特卡洛方法更为复杂
  • 典型应用:金融工程(尤其是期权定价)、高维数值积分、计算机图形学(全局光照渲染)等对计算精度和速度要求都非常高的领域

马尔科夫链

从本质上讲,马尔科夫链是一个数学模型,它通过一系列概率来描述一个系统如何从一个状态演变到另一个状态,下一步的状态只与当前状态有关

马尔科夫性质 (Markov Property)

这是马尔科夫链得以简化复杂现实问题的关键,即**“无记忆性” (Memorylessness)**

定义: 系统在 t+1t+1 时刻的状态,只取决于它在 tt 时刻的状态,而与 tt 时刻之前的所有状态都无关

P(Xn+1=jXn=i,Xn1=in1,...,X0=i0)=P(Xn+1=jXn=i)P(X_{n+1} = j | X_n = i, X_{n-1} = i_{n-1}, ..., X_0 = i_0) = P(X_{n+1} = j | X_n = i)

如明天的天气怎么样,只和今天的天气状况有关,而和昨天、前天的天气没有直接关系。虽然这在现实中是过度简化,但它为我们提供了一个强大且可计算的分析框架

定义

状态 (State) 与状态空间 (State Space)

  • 状态: 系统在某个瞬间的快照。它可以是任何离散的情况
  • 状态空间 (SS): 系统所有可能状态的集合
  • 示例:
    • 天气模型: 状态空间 S={晴天, 阴天, 雨天}S = \{\text{晴天, 阴天, 雨天}\}

转移概率 (Transition Probability) 与转移矩阵 (PP)

系统从一个状态到另一个状态的可能性。这些概率被组织在一个称为转移矩阵 PP 的方阵中

矩阵元素: pijp_{ij} 表示系统从状态 ii 经过一步转移到状态 jj 的概率

示例:三状态天气模型
假设状态空间为 {1:晴天, 2:阴天, 3:雨天},我们观察到如下规律:

  • 如果今天晴天,明天有80%概率继续晴天,20%概率变阴天,0%概率下雨
  • 如果今天阴天,明天40%概率转晴,40%概率继续阴天,20%概率下雨
  • 如果今天雨天,明天60%概率转晴,30%概率变阴天,10%概率继续下雨

对应的转移矩阵 PP 为:

P=(0.80.20.00.40.40.20.60.30.1)P = \begin{pmatrix} 0.8 & 0.2 & 0.0 \\ 0.4 & 0.4 & 0.2 \\ 0.6 & 0.3 & 0.1 \end{pmatrix}

矩阵性质:

  1. 非负性: pij0p_{ij} \ge 0 (概率不能为负)
  2. 行和为1: jpij=1\sum_{j} p_{ij} = 1

1. n阶转移矩阵 (P(n)P^{(n)})

描述系统经过 n 步转移的概率矩阵,即P(n)=PnP^{(n)} = P^n (1阶转移矩阵的 n 次幂

  • 示例 (续): 想知道如果周一晴天,周三(2天后)下雨的概率是多少?我们需要计算 P(2)=P2P^{(2)} = P^2

    P(2)=P×P=(0.80.20.00.40.40.20.60.30.1)(0.80.20.00.40.40.20.60.30.1)=(0.720.240.040.600.300.100.660.250.07)P^{(2)} = P \times P = \begin{pmatrix} 0.8 & 0.2 & 0.0 \\ 0.4 & 0.4 & 0.2 \\ 0.6 & 0.3 & 0.1 \end{pmatrix} \begin{pmatrix} 0.8 & 0.2 & 0.0 \\ 0.4 & 0.4 & 0.2 \\ 0.6 & 0.3 & 0.1 \end{pmatrix} = \begin{pmatrix} 0.72 & 0.24 & 0.04 \\ 0.60 & 0.30 & 0.10 \\ 0.66 & 0.25 & 0.07 \end{pmatrix}

    矩阵第1行第3列的元素 p13(2)=0.04p_{13}^{(2)} = 0.04,意味着周一晴天,周三下雨的概率是4%

2. 切普曼-柯尔莫哥洛夫方程 (Chapman-Kolmogorov Equation)

  • 公式: P(m+n)=P(m)P(n)P^{(m+n)} = P^{(m)} P^{(n)}
  • 解释: 从状态 iim+nm+n 步到状态 jj 的过程,可以分解为:先用 mm 步从 ii 走到某个中间状态 kk,再用 nn 步从 kk 走到 jj。这个方程将所有可能的中间路径 kk 的概率加和起来,其结果恰好是矩阵乘法

链的分类与状态的性质

1. 可约性 (Reducibility)

  • 不可约 (Irreducible): 链中任意两个状态都是互通的(可以相互到达)。系统像一个设计良好的城市,所有地点之间都有路径
    • 示例: 上述天气模型就是不可约的,你可以从任何天气类型最终转到任何其他天气类型
  • 可约 (Reducible): 链中存在不互通的状态。系统内部存在“孤岛”或“陷阱”
    • 示例:带吸收态的游戏
      状态 {1:游戏中, 2:胜利, 3:失败}。转移矩阵可能像这样:

      P=(0.50.20.30.01.00.00.00.01.0)P = \begin{pmatrix} 0.5 & 0.2 & 0.3 \\ 0.0 & 1.0 & 0.0 \\ 0.0 & 0.0 & 1.0 \end{pmatrix}

      一旦进入状态2“胜利”或状态3“失败”,就再也出不来 (p22=1,p33=1p_{22}=1, p_{33}=1)。状态1可以到2和3,但反过来不行。因此这个链是可约

2. 周期性 (Periodicity)

  • 非周期 (Aperiodic): 状态的返回没有固定的节奏

    • 示例: 在天气模型中,从“晴天”出发,可能1步后(持续晴天)、2步后、3步后…都能返回“晴天”,这些返回步数(1,2,3,…)的最大公约数是1,所以是非周期
  • 周期 (Periodic): 状态的返回只能在特定时间间隔的倍数时发生

    • 示例:简单的钟摆
      状态 {1:左, 2:右}:系统只能从左到右,再从右到左

      P=(0.01.01.00.0)P = \begin{pmatrix} 0.0 & 1.0 \\ 1.0 & 0.0 \end{pmatrix}

      从状态1出发,只能在第2, 4, 6,…步后返回状态1。返回步数的最大公约数是2,所以这个链的周期是2

3. 状态类型

  • 常返态 (Recurrent State): 只要离开,最终总能100%回来
  • 瞬时态 (Transient State): 离开后,有一定的概率再也回不来了
  • 在上面可约的游戏示例中,状态2和3是常返态(甚至是吸收态)。状态1是瞬时态,因为只要离开状态1,就再也回不来了。

4. 平稳分布 (Stationary Distribution)

当一个马尔科夫链运行足够长时间后,系统处于各个状态的概率将不再随时间变化,达到一种统计上的平衡。这个平衡时的概率分布,就是平稳分布 π\pi

πP=π\pi P = \pi

​ 约束:jπj=1\sum_j \pi_j = 1

平稳分布向量π\pi即特征值为1的特征向量

当前的概率分布 π\pi 经过一次转移(乘以P)后,得到的下一时刻的分布依然是 π\pi

对于一个有限状态、不可约、非周期的马尔科夫链,存在唯一的平稳分布 π\pi

与极限矩阵的关系:

  • 对于满足上述黄金定理的链,当 nn \to \infty 时, P(n)P^{(n)} 会收敛到一个所有行都完全相同的矩阵 WW
  • 这个矩阵的每一行,就是那个唯一的平稳分布向量 π\pi
  • 深刻含义: 经过足够长的时间,系统遗忘了它的初始状态。无论一开始是晴天、阴天还是雨天,在遥远的未来,是晴天的概率都会趋近于同一个值 π1\pi_1
  • 示例(续):对于我们的天气模型,可以解出其唯一平稳分布约为 π(0.655,0.259,0.086)\pi \approx (0.655, 0.259, 0.086)。这意味着,长期来看,这个地区大约65.5%的日子是晴天,25.9%是阴天,8.6%是雨天

HMM 隐马尔可夫模型

隐马尔可夫模型 (Hidden Markov Model, HMM) 是标准马尔科夫链的扩展,用于解决状态不可观测的问题。它描述了这样一个过程:系统内部存在一个不可见的、隐藏的马尔科夫状态链,而我们只能观测到由这些隐藏状态以一定概率生成的输出序列。HMM的目标就是通过可观测的序列,来推断隐藏状态的性质和序列

示例:密室天气

  • 隐藏状态 (Hidden State): 你无法直接看到的真实天气(如:晴天、雨天)。它的变化遵循一个马尔科夫过程
  • 观测值 (Observation): 你能看到的表象(如:进屋的人的心情)。某个观测值的出现概率与
  • 当时的隐藏状态有关

HMM 的五大组成要素

一个完整的HMM模型由一个五元组 λ=(S,O,A,B,π)\lambda = (S, O, A, B, \pi) 定义:

  1. 隐藏状态集合 SS:

    • 所有可能的隐藏状态的集合
    • S={s1,s2,...,sN}S = \{s_1, s_2, ..., s_N\},其中 NN 是隐藏状态的数量
  2. 观测集合 OO:

    • 所有可能的观测值的集合。
    • O={o1,o2,...,oM}O = \{o_1, o_2, ..., o_M\},其中 MM 是观测值的数量
  3. 状态转移概率矩阵 AA:

    • 描述隐藏状态之间的转移概率,与标准马尔科夫链的转移矩阵相同
    • A=[aij]A = [a_{ij}],其中 aij=P(下一时刻为状态 sj当前为状态 si)a_{ij} = P(\text{下一时刻为状态 } s_j | \text{当前为状态 } s_i)
  4. 发射概率矩阵 BB (观测概率矩阵):

    • 描述在某个隐藏状态下,生成某个观测值的概率。这是HMM特有的核心
    • B=[bj(k)]B = [b_j(k)],其中 bj(k)=P(观测到 ok当前为状态 sj)b_j(k) = P(\text{观测到 } o_k | \text{当前为状态 } s_j)
  5. 初始状态概率分布 π\pi:

    • 描述系统在初始时刻处于各个隐藏状态的概率
    • π=[πi]\pi = [\pi_i],其中 πi=P(初始时刻为状态 si)\pi_i = P(\text{初始时刻为状态 } s_i)

HMM 的三大应用

HMM的强大之处在于它能系统性地解决以下三个核心问题:

问题类型 问题描述 解决方法
评估问题 (Evaluation) 给定模型 λ\lambda 和观测序列 OO,计算该序列出现的总概率 $P(O \lambda)$
解码问题 (Decoding) 给定模型 λ\lambda 和观测序列 OO,找到最有可能产生此观测的隐藏状态序列 QQ 维特比算法 (Viterbi Algorithm)
学习问题 (Learning) 仅给定观测序列 OO,如何调整模型参数 λ=(A,B,π)\lambda=(A, B, \pi),使其最能解释观测数据 鲍姆-韦尔奇算法 (Baum-Welch Algorithm)

数模笔记模型篇二
https://blog.giraffish.me/post/c8b9524f/
作者
卖柠檬雪糕的鱼
发布于
2025年7月18日
更新于
2025年7月29日
许可协议