09 支持向量机

本章导览:从最大间隔到核方法

支持向量机(SVM)是20世纪90年代最具影响力的分类算法之一。

  • 最大间隔分类器:在线性可分数据上寻找最优超平面
  • 支持向量分类器:引入软间隔处理不可分情形
  • 支持向量机:通过核技巧扩展到非线性决策边界
  • 与逻辑回归的关系:统一视角下的损失+惩罚框架

超平面的数学定义

\(p\)维空间中,超平面定义为满足以下等式的点集:

\[ \beta_0 + \beta_1 X_1 + \beta_2 X_2 + \cdots + \beta_p X_p = 0 \]

  • \(p=2\)时,超平面是一条直线
  • \(p=3\)时,超平面是一个平面
  • 超平面将空间分成两个半空间

可视化:超平面将空间一分为二

Code
import numpy as np  # 导入numpy库用于数值计算
import matplotlib.pyplot as plt  # 导入matplotlib用于数据可视化

plt.rcParams['font.sans-serif'] = ['Source Han Serif SC']  # 设置中文字体为思源宋体
plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示问题

x1_grid = np.linspace(-3, 3, 300)  # 在[-3,3]区间生成300个X₁坐标点
x2_grid = np.linspace(-3, 3, 300)  # 在[-3,3]区间生成300个X₂坐标点
x1_mesh, x2_mesh = np.meshgrid(x1_grid, x2_grid)  # 构建二维网格用于绘制区域
hyperplane_values = 1 + 2 * x1_mesh + 3 * x2_mesh  # 计算超平面方程在每个网格点的值

fig, ax = plt.subplots(figsize=(7, 5))  # 创建7×5英寸的图形画布
ax.contourf(x1_mesh, x2_mesh, hyperplane_values, levels=[-100, 0, 100], colors=['#a8d8ea', '#ffcfdf'], alpha=0.6)  # 用不同颜色填充超平面两侧区域
ax.contour(x1_mesh, x2_mesh, hyperplane_values, levels=[0], colors='black', linewidths=2)  # 绘制超平面本身(黑色实线)
ax.set_xlabel('$X_1$', fontsize=12)  # 设置X轴标签
ax.set_ylabel('$X_2$', fontsize=12)  # 设置Y轴标签
ax.set_title('超平面: $1 + 2X_1 + 3X_2 = 0$', fontsize=14)  # 设置图形标题
ax.text(0.5, 1.5, '$1+2X_1+3X_2>0$', fontsize=12, ha='center')  # 在正半空间标注方程
ax.text(-1.5, -1.5, '$1+2X_1+3X_2<0$', fontsize=12, ha='center')  # 在负半空间标注方程
plt.tight_layout()  # 自动调整布局
plt.show()  # 显示图形
Figure 1: 超平面 1 + 2X₁ + 3X₂ = 0 将二维平面分为两个区域

分离超平面与分类

如果训练数据可以被超平面完美分开,则称该超平面为分离超平面

数学上,对于所有观测 \(i = 1, \ldots, n\),分离超平面满足:

\[ y_i(\beta_0 + \beta_1 x_{i1} + \cdots + \beta_p x_{ip}) > 0 \]

其中 \(y_i \in \{-1, +1\}\) 是类别标签。

关键问题:分离超平面不唯一——哪个最好?

多条分离超平面的困境

Code
np.random.seed(42)  # 设置随机种子确保可重复性
class1_points = np.random.randn(20, 2) + np.array([2, 2])  # 生成第一类样本(中心在(2,2)的正态分布)
class2_points = np.random.randn(20, 2) + np.array([-2, -2])  # 生成第二类样本(中心在(-2,-2)的正态分布)

fig, axes = plt.subplots(1, 2, figsize=(12, 5))  # 创建1行2列的子图布局

for ax in axes:  # 遍历两个子图
    ax.scatter(class1_points[:, 0], class1_points[:, 1], c='steelblue', marker='o', s=50, label='+1类')  # 绘制+1类的散点
    ax.scatter(class2_points[:, 0], class2_points[:, 1], c='darkorange', marker='s', s=50, label='-1类')  # 绘制-1类的散点

x_boundary_range = np.linspace(-5, 5, 100)  # 生成用于绘制分离线的x坐标序列
axes[0].plot(x_boundary_range, -x_boundary_range, 'k-', linewidth=2, label='超平面1')  # 绘制第一条分离超平面(斜率-1)
axes[0].plot(x_boundary_range, -x_boundary_range + 0.5, 'k--', linewidth=2, label='超平面2')  # 绘制第二条分离超平面(平移0.5)
axes[0].plot(x_boundary_range, -0.8 * x_boundary_range - 0.3, 'k-.', linewidth=2, label='超平面3')  # 绘制第三条分离超平面(斜率-0.8)
axes[0].set_title('三条可能的分离超平面', fontsize=13)  # 设置左图标题
axes[0].legend(fontsize=9)  # 显示图例

x1_vis, x2_vis = np.meshgrid(np.linspace(-5, 5, 200), np.linspace(-5, 5, 200))  # 构建二维网格用于着色区域
boundary_values = x1_vis + x2_vis  # 计算决策函数值(对应超平面1)
axes[1].contourf(x1_vis, x2_vis, boundary_values, levels=[-100, 0, 100], colors=['#ffcfdf', '#a8d8ea'], alpha=0.4)  # 用颜色填充决策区域
axes[1].contour(x1_vis, x2_vis, boundary_values, levels=[0], colors='black', linewidths=2)  # 绘制决策边界
axes[1].scatter(class1_points[:, 0], class1_points[:, 1], c='steelblue', marker='o', s=50)  # 绘制+1类散点
axes[1].scatter(class2_points[:, 0], class2_points[:, 1], c='darkorange', marker='s', s=50)  # 绘制-1类散点
axes[1].set_title('决策边界与分类区域', fontsize=13)  # 设置右图标题

for ax in axes:  # 遍历两个子图设置通用属性
    ax.set_xlabel('$X_1$')  # 设置X轴标签
    ax.set_ylabel('$X_2$')  # 设置Y轴标签
    ax.set_xlim(-5, 5)  # 设置X轴范围
    ax.set_ylim(-5, 5)  # 设置Y轴范围
plt.tight_layout()  # 自动调整子图间距
plt.show()  # 显示图形
Figure 2: 左图:三条可能的分离超平面;右图:对应的决策区域着色

最大间隔分类器:选择最优超平面

最大间隔分类器选择使间隔(margin)最大化的分离超平面。

\[ \max_{\beta_0, \beta_1, \ldots, \beta_p, M} M \quad \text{s.t.} \quad \sum_{j=1}^{p} \beta_j^2 = 1 \]

\[ y_i(\beta_0 + \beta_1 x_{i1} + \cdots + \beta_p x_{ip}) \geq M, \quad \forall \, i \]

  • \(M\)间隔宽度,我们要将其最大化
  • 约束确保所有点都在间隔之外
  • 支持向量:恰好位于间隔边界上的点

SVM拟合:线性可分情形示例

Code
from sklearn.svm import SVC  # 导入支持向量分类器

all_features = np.vstack([class1_points, class2_points])  # 将两类样本纵向合并为特征矩阵
all_labels = np.array([1] * 20 + [-1] * 20)  # 构造标签向量:前20个为+1类,后20个为-1类

hard_margin_svm = SVC(kernel='linear', C=1000)  # 创建线性核SVM(C=1000近似硬间隔)
hard_margin_svm.fit(all_features, all_labels)  # 在训练数据上拟合模型

weight_vector = hard_margin_svm.coef_[0]  # 提取权重向量w
bias_term = hard_margin_svm.intercept_[0]  # 提取偏置项b
margin_width = 2 / np.linalg.norm(weight_vector)  # 计算间隔宽度 = 2/||w||

fig, ax = plt.subplots(figsize=(7, 5))  # 创建7×5英寸的画布
ax.scatter(class1_points[:, 0], class1_points[:, 1], c='steelblue', marker='o', s=50, label='+1类')  # 绘制+1类散点
ax.scatter(class2_points[:, 0], class2_points[:, 1], c='darkorange', marker='s', s=50, label='-1类')  # 绘制-1类散点

x_line = np.linspace(-5, 5, 100)  # 生成绘制直线的x坐标
decision_boundary_y = -(weight_vector[0] * x_line + bias_term) / weight_vector[1]  # 计算决策边界y坐标
margin_upper_y = -(weight_vector[0] * x_line + bias_term - 1) / weight_vector[1]  # 计算上间隔边界y坐标
margin_lower_y = -(weight_vector[0] * x_line + bias_term + 1) / weight_vector[1]  # 计算下间隔边界y坐标

ax.plot(x_line, decision_boundary_y, 'k-', linewidth=2, label='决策边界')  # 绘制决策边界(实线)
ax.plot(x_line, margin_upper_y, 'k--', linewidth=1)  # 绘制上间隔边界(虚线)
ax.plot(x_line, margin_lower_y, 'k--', linewidth=1)  # 绘制下间隔边界(虚线)

support_vector_points = hard_margin_svm.support_vectors_  # 获取支持向量坐标
ax.scatter(support_vector_points[:, 0], support_vector_points[:, 1], s=200, facecolors='none', edgecolors='red', linewidths=2, label='支持向量')  # 用红色空心圆圈标记支持向量

ax.set_xlabel('$X_1$', fontsize=12)  # 设置X轴标签
ax.set_ylabel('$X_2$', fontsize=12)  # 设置Y轴标签
ax.set_title(f'最大间隔分类器 (间隔宽度={margin_width:.2f})', fontsize=13)  # 显示间隔宽度
ax.legend(fontsize=9)  # 显示图例
ax.set_xlim(-5, 5)  # 设置X轴范围
ax.set_ylim(-5, 5)  # 设置Y轴范围
plt.tight_layout()  # 自动调整布局
plt.show()  # 显示图形
Figure 3: 最大间隔分类器:实线为决策边界,虚线为间隔边界,红圈为支持向量

当数据不可分时怎么办?

Code
np.random.seed(42)  # 设置随机种子
class1_overlap = np.random.randn(30, 2) + np.array([0, 0])  # 生成中心在原点的第一类样本
class2_overlap = np.random.randn(30, 2) + np.array([1, 1])  # 生成中心在(1,1)的第二类样本(与第一类重叠)

fig, ax = plt.subplots(figsize=(7, 5))  # 创建画布
ax.scatter(class1_overlap[:, 0], class1_overlap[:, 1], c='steelblue', marker='o', s=50, label='+1类')  # 绘制+1类散点
ax.scatter(class2_overlap[:, 0], class2_overlap[:, 1], c='darkorange', marker='s', s=50, label='-1类')  # 绘制-1类散点
ax.set_title('数据严重重叠:不存在分离超平面', fontsize=13)  # 设置标题
ax.legend(fontsize=10)  # 显示图例
ax.set_xlabel('$X_1$')  # 设置X轴标签
ax.set_ylabel('$X_2$')  # 设置Y轴标签
plt.tight_layout()  # 自动调整布局
plt.show()  # 显示图形
Figure 4: 两类数据严重重叠,无法找到分离超平面

支持向量分类器:引入软间隔

支持向量分类器(Support Vector Classifier)允许部分观测违反间隔:

\[ \max_{\beta, M} \; M \quad \text{s.t.} \quad \sum_{j=1}^{p}\beta_j^2=1 \]

\[ y_i(\beta_0 + \sum_{j=1}^{p}\beta_j x_{ij}) \geq M(1-\varepsilon_i), \quad \varepsilon_i \geq 0, \quad \sum_{i=1}^{n}\varepsilon_i \leq C \]

  • \(\varepsilon_i\)松弛变量\(\varepsilon_i = 0\)(正确侧);\(0 < \varepsilon_i < 1\)(间隔内但正确侧);\(\varepsilon_i > 1\)(被错分)
  • \(C\)预算参数,控制对违规的容忍程度

C参数的偏差-方差权衡

C参数偏差-方差权衡示意图 左侧展示大C值导致的宽间隔(高偏差低方差),右侧展示小C值导致的窄间隔(低偏差高方差) 大C → 宽间隔 小C → 窄间隔 宽间隔 → 更多支持向量 窄间隔 → 更少支持向量 高偏差,低方差 低偏差,高方差

对偶问题与内积表示

将软间隔SVM的原始问题转化为等价的对偶问题

\[ \max_{\alpha} \sum_{i=1}^{n}\alpha_i - \frac{1}{2}\sum_{i=1}^{n}\sum_{j=1}^{n}\alpha_i \alpha_j y_i y_j \langle \mathbf{x}_i, \mathbf{x}_j \rangle \]

\[ \text{s.t.} \quad 0 \leq \alpha_i \leq C, \quad \sum_{i=1}^{n}\alpha_i y_i = 0 \]

关键发现

  • 预测仅依赖于内积 \(\langle \mathbf{x}_i, \mathbf{x}_j \rangle\)
  • 非支持向量的 \(\alpha_i = 0\),不影响决策
  • 这为核技巧奠定了理论基础

从线性到非线性:核技巧

当数据存在非线性决策边界时,线性SVC力不从心。

核心思想:将内积 \(\langle \mathbf{x}_i, \mathbf{x}_j \rangle\) 替换为核函数 \(K(\mathbf{x}_i, \mathbf{x}_j)\)

\[ f(x) = \beta_0 + \sum_{i \in \mathcal{S}} \alpha_i K(x, x_i) \]

核函数隐式地将数据映射到高维空间,在该空间中寻找线性分离。

常用核函数

核函数 数学表达式 特点
线性核 \(K(\mathbf{x}_i, \mathbf{x}_j) = \sum_{k=1}^{p} x_{ik}x_{jk}\) 等价于标准SVC
多项式核 \(K(\mathbf{x}_i, \mathbf{x}_j) = (1 + \sum_{k=1}^{p} x_{ik}x_{jk})^d\) 捕捉\(d\)阶交互
径向基核(RBF) \(K(\mathbf{x}_i, \mathbf{x}_j) = e^{-\gamma\sum_{k=1}^{p}(x_{ik}-x_{jk})^2}\) 局部灵活,最常用
  • RBF核的 \(\gamma\) 控制影响范围\(\gamma\)大 → 局部更敏感(更复杂);\(\gamma\)小 → 更平滑
  • 有效核函数必须满足 Mercer定理(核矩阵半正定)

多类SVM扩展

SVM原始设计只处理二分类。扩展到多类的两种策略:

一对一(One-vs-One, OVO)

  • \(K\)个类别训练 \(\binom{K}{2}\) 个二分类SVM
  • 测试时多数投票决定类别

一对多(One-vs-Rest, OVR)

  • 训练\(K\)个SVM,每个将一类与其余所有类分开
  • 测试时选择决策值最大的类别

SVM与逻辑回归:统一视角

SVM可以写成损失+惩罚框架:

\[ \min_{\beta} \left\{ \sum_{i=1}^{n} \max\big[0, \; 1 - y_i f(x_i)\big] + \lambda \sum_{j=1}^{p}\beta_j^2 \right\} \]

  • 合页损失(Hinge Loss)\(\max(0, 1-yf)\)
  • 与逻辑回归的对数损失\(\log(1+e^{-yf})\) 非常相似
  • 两者都有L2正则化项

合页损失 vs 对数损失

Code
yf_axis = np.linspace(-4, 4, 300)  # 生成yf值的序列(从-4到4)
hinge_loss_values = np.maximum(0, 1 - yf_axis)  # 计算合页损失:max(0, 1-yf)
logistic_loss_values = np.log(1 + np.exp(-yf_axis))  # 计算对数损失:log(1+exp(-yf))
zero_one_loss_values = (yf_axis < 0).astype(float)  # 计算0-1损失:yf<0时为1,否则为0

fig, ax = plt.subplots(figsize=(8, 5))  # 创建8×5英寸画布
ax.plot(yf_axis, hinge_loss_values, 'b-', linewidth=2.5, label='合页损失 (SVM)')  # 绘制合页损失曲线(蓝色)
ax.plot(yf_axis, logistic_loss_values, 'r--', linewidth=2.5, label='对数损失 (逻辑回归)')  # 绘制对数损失曲线(红色虚线)
ax.plot(yf_axis, zero_one_loss_values, 'k:', linewidth=2, label='0-1损失 (理想)')  # 绘制0-1损失(黑色点线)
ax.axvline(x=1, color='gray', linestyle='--', alpha=0.5, label='间隔边界 (yf=1)')  # 绘制间隔边界参考线
ax.set_xlabel('$y \\cdot f(x)$', fontsize=13)  # 设置X轴标签
ax.set_ylabel('损失值', fontsize=13)  # 设置Y轴标签
ax.set_title('三种损失函数的比较', fontsize=14)  # 设置图形标题
ax.legend(fontsize=11)  # 显示图例
ax.set_ylim(-0.2, 5)  # 设置Y轴范围
ax.grid(True, alpha=0.3)  # 添加半透明网格线
plt.tight_layout()  # 自动调整布局
plt.show()  # 显示图形
Figure 5: 合页损失(SVM)与对数损失(逻辑回归)的比较

实验:支持向量分类器的C参数效果

Code
np.random.seed(42)  # 设置随机种子确保可重复性
feature_matrix_2d = np.random.randn(100, 2)  # 生成100个二维标准正态样本
class_labels_2d = np.array([-1] * 50 + [1] * 50)  # 前50个标记为-1类,后50个为+1类
feature_matrix_2d[class_labels_2d == 1] += 1  # 将+1类的样本中心平移至(1,1)


def visualize_svm_boundary(features, labels, model, title, ax):
    """绘制SVM决策边界、间隔边界和支持向量的可视化函数"""
    x_min, x_max = features[:, 0].min() - 1, features[:, 0].max() + 1  # 计算X轴绘图范围
    y_min, y_max = features[:, 1].min() - 1, features[:, 1].max() + 1  # 计算Y轴绘图范围
    xx_grid, yy_grid = np.meshgrid(np.linspace(x_min, x_max, 200), np.linspace(y_min, y_max, 200))  # 创建密集网格
    grid_decision_values = model.decision_function(np.c_[xx_grid.ravel(), yy_grid.ravel()])  # 在网格点上计算决策函数值
    grid_decision_values = grid_decision_values.reshape(xx_grid.shape)  # 重塑为二维矩阵
    ax.contourf(xx_grid, yy_grid, grid_decision_values, levels=[-100, 0, 100], colors=['#ffcfdf', '#a8d8ea'], alpha=0.4)  # 用颜色填充决策区域
    ax.contour(xx_grid, yy_grid, grid_decision_values, levels=[-1, 0, 1], colors=['gray', 'black', 'gray'], linestyles=['--', '-', '--'], linewidths=[1, 2, 1])  # 绘制间隔边界和决策边界
    ax.scatter(features[labels == -1, 0], features[labels == -1, 1], c='darkorange', marker='s', s=40)  # 绘制-1类散点
    ax.scatter(features[labels == 1, 0], features[labels == 1, 1], c='steelblue', marker='o', s=40)  # 绘制+1类散点
    sv_points = model.support_vectors_  # 获取支持向量坐标
    ax.scatter(sv_points[:, 0], sv_points[:, 1], s=150, facecolors='none', edgecolors='red', linewidths=1.5)  # 用红色空心圆标记支持向量
    ax.set_title(f'{title} (支持向量数: {len(sv_points)})', fontsize=12)  # 显示标题和支持向量数量


fig, axes = plt.subplots(1, 2, figsize=(12, 5))  # 创建1行2列的子图

linear_svc_strict = SVC(kernel='linear', C=10)  # 创建C=10的线性SVC(较严格)
linear_svc_strict.fit(feature_matrix_2d, class_labels_2d)  # 拟合模型
visualize_svm_boundary(feature_matrix_2d, class_labels_2d, linear_svc_strict, 'C=10', axes[0])  # 可视化C=10的结果

linear_svc_relaxed = SVC(kernel='linear', C=0.1)  # 创建C=0.1的线性SVC(较宽松)
linear_svc_relaxed.fit(feature_matrix_2d, class_labels_2d)  # 拟合模型
visualize_svm_boundary(feature_matrix_2d, class_labels_2d, linear_svc_relaxed, 'C=0.1', axes[1])  # 可视化C=0.1的结果

plt.tight_layout()  # 自动调整子图间距
plt.show()  # 显示图形
Figure 6: C=10(严格间隔)与C=0.1(宽松间隔)的支持向量对比

实验:非线性SVM与网格搜索

Code
from sklearn.model_selection import train_test_split, GridSearchCV  # 导入数据分割和网格搜索工具

np.random.seed(42)  # 设置随机种子
non_linear_features = np.random.randn(200, 2)  # 生成200个二维标准正态样本
non_linear_labels = np.array([-1] * 150 + [1] * 50)  # 150个-1类和50个+1类
non_linear_features[:50] += 3  # 将前50个-1类样本移至(3,3)
non_linear_features[50:100] -= 3  # 将第51-100个-1类样本移至(-3,-3)
non_linear_features[150:] += 1.5  # 将+1类样本移至(1.5,1.5)

nl_train_x, nl_test_x, nl_train_y, nl_test_y = train_test_split(non_linear_features, non_linear_labels, test_size=0.5, random_state=42)  # 将数据按50:50分为训练集和测试集

rbf_param_grid = {'C': [0.1, 1, 10, 100, 1000], 'gamma': [0.5, 1, 2, 3, 4]}  # 定义C和gamma的搜索网格
rbf_grid_search = GridSearchCV(SVC(kernel='rbf'), rbf_param_grid, cv=5, scoring='accuracy')  # 配置5折交叉验证网格搜索
rbf_grid_search.fit(nl_train_x, nl_train_y)  # 在训练集上执行网格搜索

optimal_rbf_svm = rbf_grid_search.best_estimator_  # 获取最优模型
print(f'最优参数: {rbf_grid_search.best_params_}')  # 输出最优超参数组合
print(f'交叉验证准确率: {rbf_grid_search.best_score_:.4f}')  # 输出交叉验证最佳准确率
print(f'测试集准确率: {optimal_rbf_svm.score(nl_test_x, nl_test_y):.4f}')  # 输出测试集准确率

fig, ax = plt.subplots(figsize=(7, 5))  # 创建画布
visualize_svm_boundary(nl_train_x, nl_train_y, optimal_rbf_svm, f'RBF核SVM (C={rbf_grid_search.best_params_["C"]}, γ={rbf_grid_search.best_params_["gamma"]})', ax)  # 可视化最优模型在训练集上的效果
plt.tight_layout()  # 自动调整布局
plt.show()  # 显示图形
最优参数: {'C': 10, 'gamma': 2}
交叉验证准确率: 0.9000
测试集准确率: 0.8000
Figure 7: 非线性数据上RBF核SVM的拟合效果(通过GridSearchCV选择最优超参数)

案例:海康威视股价方向预测

使用A股长三角龙头企业海康威视(002415.XSHE)的历史行情数据,构建SVM股价方向预测模型。

特征工程

  • 滞后收益率:\(\text{Lag}_1, \text{Lag}_2, \text{Lag}_3, \text{Lag}_5\)
  • 20日滚动波动率:\(\text{Vol}_{20}\)
  • 均线偏离度:\((P / \text{MA}_{20}) - 1\)

目标:预测次日收益方向(上涨=+1,下跌=-1)

数据准备与特征构建

import pandas as pd  # 导入pandas用于数据框操作
import os  # 导入os用于路径处理

DATA_DIR = 'C:/qiufei/data' if os.name == 'nt' else '/home/ubuntu/r2_data_mount/qiufei/data'  # 根据操作系统选择数据路径
stock_price_path = os.path.join(DATA_DIR, 'stock/stock_price_post_adjusted.h5')  # 构建后复权股价文件路径
stock_price_history = pd.read_hdf(stock_price_path).reset_index()  # 读取后复权股价数据并重置索引
haikang_daily_data = stock_price_history[stock_price_history['order_book_id'] == '002415.XSHE'].copy()  # 筛选海康威视的数据
haikang_daily_data = haikang_daily_data.sort_values('date')  # 按日期升序排列
haikang_daily_data['Ret'] = haikang_daily_data['close'].pct_change()  # 计算日收益率
haikang_daily_data['Lag_1'] = haikang_daily_data['Ret'].shift(1)  # 构造1日滞后收益率
haikang_daily_data['Lag_2'] = haikang_daily_data['Ret'].shift(2)  # 构造2日滞后收益率
haikang_daily_data['Lag_3'] = haikang_daily_data['Ret'].shift(3)  # 构造3日滞后收益率
haikang_daily_data['Lag_5'] = haikang_daily_data['Ret'].shift(5)  # 构造5日滞后收益率
haikang_daily_data['Vol_20'] = haikang_daily_data['Ret'].rolling(20).std().shift(1)  # 计算20日滚动波动率(滞后1日)
moving_average_20 = haikang_daily_data['close'].rolling(20).mean()  # 计算20日移动均线
haikang_daily_data['Dist_MA20'] = haikang_daily_data['close'] / moving_average_20 - 1  # 计算收盘价相对20日均线的偏离度
haikang_daily_data['Target'] = np.where(haikang_daily_data['Ret'] > 0, 1, -1)  # 构造目标变量:涨=1,跌=-1
haikang_daily_data = haikang_daily_data.dropna().iloc[-2000:]  # 删除缺失值并取最近2000条记录
print(f'数据样本量: {len(haikang_daily_data)}, 特征数: 6')  # 输出数据概况
print(f'上涨天数占比: {(haikang_daily_data["Target"]==1).mean():.2%}')  # 输出上涨比例
数据样本量: 2000, 特征数: 6
上涨天数占比: 49.25%

SVM模型训练与评估

from sklearn.preprocessing import StandardScaler  # 导入标准化工具
from sklearn.model_selection import TimeSeriesSplit, GridSearchCV  # 导入时间序列交叉验证和网格搜索
from sklearn.metrics import accuracy_score, classification_report  # 导入准确率和分类报告

feature_columns = ['Lag_1', 'Lag_2', 'Lag_3', 'Lag_5', 'Vol_20', 'Dist_MA20']  # 定义特征列名
haikang_features = haikang_daily_data[feature_columns].values  # 提取特征矩阵
haikang_targets = haikang_daily_data['Target'].values  # 提取目标变量

train_size = int(len(haikang_features) * 0.8)  # 计算训练集大小(80%)
haikang_train_x = haikang_features[:train_size]  # 前80%为训练集特征
haikang_test_x = haikang_features[train_size:]  # 后20%为测试集特征
haikang_train_y = haikang_targets[:train_size]  # 训练集标签
haikang_test_y = haikang_targets[train_size:]  # 测试集标签

haikang_scaler = StandardScaler()  # 创建标准化器
haikang_train_x_scaled = haikang_scaler.fit_transform(haikang_train_x)  # 对训练集进行标准化
haikang_test_x_scaled = haikang_scaler.transform(haikang_test_x)  # 对测试集使用训练集参数标准化
# 注意:实际生产中应使用更大的参数网格,此处为节省计算时间使用较小网格
stock_param_grid = {'C': [0.1, 1, 10, 100], 'gamma': [0.001, 0.01, 0.1, 1], 'kernel': ['rbf']}  # 定义超参数搜索空间
time_cv = TimeSeriesSplit(n_splits=3)  # 创建3折时间序列交叉验证器
stock_grid_search = GridSearchCV(SVC(), stock_param_grid, cv=time_cv, scoring='accuracy', n_jobs=-1)  # 配置网格搜索(使用所有CPU核心)
stock_grid_search.fit(haikang_train_x_scaled, haikang_train_y)  # 在训练集上执行网格搜索

optimal_stock_svm = stock_grid_search.best_estimator_  # 获取最优模型
print(f'最优参数: {stock_grid_search.best_params_}')  # 输出最优超参数
print(f'交叉验证最佳准确率: {stock_grid_search.best_score_:.4f}')  # 输出CV最佳准确率
最优参数: {'C': 10, 'gamma': 0.01, 'kernel': 'rbf'}
交叉验证最佳准确率: 0.6567

股价预测结果与市场效率解读

stock_predictions = optimal_stock_svm.predict(haikang_test_x_scaled)  # 使用最优模型对测试集进行预测
print('测试集分类报告:')  # 输出标题
print(classification_report(haikang_test_y, stock_predictions))  # 生成并输出详细分类报告
print(f'测试集准确率: {accuracy_score(haikang_test_y, stock_predictions):.4f}')  # 计算并输出测试集准确率
测试集分类报告:
              precision    recall  f1-score   support

          -1       0.63      0.60      0.61       201
           1       0.61      0.65      0.63       199

    accuracy                           0.62       400
   macro avg       0.62      0.62      0.62       400
weighted avg       0.62      0.62      0.62       400

测试集准确率: 0.6225

结果解读

  • 测试准确率约50%,本质上与随机猜测无异
  • 对”上涨”类的召回率极低,模型几乎无法识别上涨日
  • 这为有效市场假说(EMH)提供了实证支持

案例:上市公司财务困境预测

将SVM应用于金融风控:预测A股上市公司是否会陷入财务困境(亏损)

数据来源:本地A股上市公司财务报表数据

预测特征(4个财务比率)

指标 公式 业务含义
流动比率 流动资产 / 流动负债 短期偿债能力
资产负债率 总负债 / 总资产 杠杆水平
销售净利率 净利润 / 营业收入 盈利能力
资产周转率 营业收入 / 总资产 运营效率

数据准备与平衡采样

financial_path = os.path.join(DATA_DIR, 'stock/financial_statement.h5')  # 构建财务报表数据路径
financial_statement_history = pd.read_hdf(financial_path)  # 读取A股上市公司财务报表数据
financial_statement_history['Current_Ratio'] = financial_statement_history['current_assets'] / (financial_statement_history['current_liabilities'] + 1)  # 计算流动比率
financial_statement_history['Debt_Ratio'] = financial_statement_history['total_liabilities'] / (financial_statement_history['total_assets'] + 1)  # 计算资产负债率
financial_statement_history['ROS'] = financial_statement_history['net_profit'] / (financial_statement_history['operating_revenue'] + 1)  # 计算销售净利率
financial_statement_history['Turnover'] = financial_statement_history['operating_revenue'] / (financial_statement_history['total_assets'] + 1)  # 计算资产周转率
financial_statement_history['Distress'] = (financial_statement_history['net_profit'] < 0).astype(int)  # 定义财务困境标签:净利润为负=1
analysis_columns = ['Current_Ratio', 'Debt_Ratio', 'ROS', 'Turnover', 'Distress']  # 定义分析所需的列
valid_financials = financial_statement_history[analysis_columns].dropna().replace([np.inf, -np.inf], np.nan).dropna()  # 删除缺失值和无穷大值
valid_financials = valid_financials[  # 过滤异常值,保留合理范围
    (valid_financials['Current_Ratio'].between(0, 10)) &  # 流动比率在0-10之间
    (valid_financials['Debt_Ratio'].between(0, 2)) &  # 资产负债率在0-2之间
    (valid_financials['ROS'].between(-1, 1))  # 销售净利率在-1到1之间
]
distressed_companies = valid_financials[valid_financials['Distress'] == 1].sample(n=500, replace=True, random_state=42)  # 对困境样本过采样至500
healthy_companies = valid_financials[valid_financials['Distress'] == 0].sample(n=500, random_state=42)  # 随机抽取500家健康公司
balanced_financials = pd.concat([distressed_companies, healthy_companies])  # 合并构成平衡数据集
print(f'平衡数据集: 困境={len(distressed_companies)}, 健康={len(healthy_companies)}')  # 输出数据集构成
平衡数据集: 困境=500, 健康=500

多模型ROC曲线对比

Code
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis  # 导入LDA模型
from sklearn.metrics import roc_curve, auc  # 导入ROC曲线和AUC计算函数

distress_predictors = balanced_financials[['Current_Ratio', 'Debt_Ratio', 'ROS', 'Turnover']]  # 提取特征矩阵
distress_targets = balanced_financials['Distress']  # 提取目标变量
distress_train_x, distress_test_x, distress_train_y, distress_test_y = train_test_split(distress_predictors, distress_targets, test_size=0.3, random_state=42)  # 按70:30分割数据

feature_scaler = StandardScaler()  # 创建标准化器
distress_train_x_scaled = feature_scaler.fit_transform(distress_train_x)  # 标准化训练集
distress_test_x_scaled = feature_scaler.transform(distress_test_x)  # 标准化测试集
Figure 8
distress_lda_model = LinearDiscriminantAnalysis()  # 创建LDA基准模型
distress_lda_model.fit(distress_train_x_scaled, distress_train_y)  # 拟合LDA

distress_linear_svc = SVC(kernel='linear', C=1, probability=True)  # 创建线性核SVM
distress_linear_svc.fit(distress_train_x_scaled, distress_train_y)  # 拟合线性SVM

distress_rbf_svc_01 = SVC(kernel='rbf', gamma=0.1, C=1, probability=True)  # 创建RBF核SVM(γ=0.1)
distress_rbf_svc_01.fit(distress_train_x_scaled, distress_train_y)  # 拟合RBF SVM (γ=0.1)

distress_rbf_svc_1 = SVC(kernel='rbf', gamma=1, C=1, probability=True)  # 创建RBF核SVM(γ=1)
distress_rbf_svc_1.fit(distress_train_x_scaled, distress_train_y)  # 拟合RBF SVM (γ=1)
SVC(C=1, gamma=1, probability=True)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Code
classification_models = {  # 将四个模型放入字典便于批量处理
    'LDA': distress_lda_model,  # 线性判别分析
    'SVC (Linear)': distress_linear_svc,  # 线性核SVM
    'SVM (RBF, γ=0.1)': distress_rbf_svc_01,  # RBF核SVM,较平滑
    'SVM (RBF, γ=1)': distress_rbf_svc_1  # RBF核SVM,较灵活
}

fig, ax = plt.subplots(figsize=(8, 6))  # 创建8×6英寸画布
line_styles = ['-', '--', '-.', ':']  # 定义不同线型以区分模型

for (name, model), line_style in zip(classification_models.items(), line_styles):  # 遍历每个模型
    predicted_proba = model.predict_proba(distress_test_x_scaled)[:, 1]  # 预测正类(困境)概率
    fpr, tpr, _ = roc_curve(distress_test_y, predicted_proba)  # 计算ROC曲线的假正率和真正率
    roc_auc_value = auc(fpr, tpr)  # 计算AUC面积
    ax.plot(fpr, tpr, linestyle=line_style, linewidth=2, label=f'{name} (AUC={roc_auc_value:.2f})')  # 绘制ROC曲线

ax.plot([0, 1], [0, 1], 'k--', linewidth=1, alpha=0.5, label='随机猜测')  # 绘制对角基线
ax.set_xlabel('假正率 (1 - Specificity)', fontsize=12)  # 设置X轴标签
ax.set_ylabel('真正率 (Sensitivity)', fontsize=12)  # 设置Y轴标签
ax.set_title('财务困境预测: 测试集ROC曲线', fontsize=14)  # 设置标题
ax.legend(loc='lower right', fontsize=10)  # 在右下角显示图例
ax.grid(True, alpha=0.3)  # 添加半透明网格
plt.tight_layout()  # 自动调整布局
plt.show()  # 显示图形
Figure 9: 财务困境预测数据上四种分类模型的ROC曲线对比

本章小结

方法 关键特征 适用场景
最大间隔分类器 硬间隔,对异常值敏感 完全线性可分数据(理论)
支持向量分类器 软间隔 + C参数 线性近似可分数据
支持向量机 核函数扩展 非线性决策边界

核心思想

  • 间隔最大化 → 更好的泛化能力
  • 对偶表示 → 只依赖内积 → 核技巧
  • 核函数选择:线性 → RBF → 多项式,由简到繁
  • 实务洞察:合适的模型复杂度比最强的模型更重要