支持向量机(SVM)入门:从原理到可运行代码实现

在机器学习的分类算法中,支持向量机(SVM)是一种极具代表性的方法。它通过寻找最优分类边界,能在复杂数据中实现高精度分类,尤其在小样本、高维场景下表现突出。今天我们用通俗的语言拆解其原理,再通过可直接运行的代码实战感受它的应用。

一、SVM核心思想:找到最“靠谱”的分类线

先从一个生活化的场景说起:桌上放着红蓝两种颜色的球,我们需要用一根棍子把它们分开。简单的分类线有很多,但有的线在新增球后就会分错,而有的线却能稳定分类——这背后的关键就是“分类间隔”。image.png

SVM的核心目标,就是找到一条“最宽”的分类线(二维场景),或者“最宽”的分类面(高维场景,称为超平面)。这条线到两侧最近样本点的距离最大,就像在两条平行线中间找中线,平行线之间的距离就是分类间隔。那些刚好落在平行线上的样本点,就是“支持向量”,它们决定了分类边界的位置,也是SVM名字的由来。image.png

如果遇到线性不可分的情况(比如红蓝球混在一起),SVM会通过“核函数”将数据映射到更高维度的空间,就像把混乱的球“拍上天”,再用一张平面把它们分开——这张平面就是高维空间的超平面,回到原空间就表现为一条曲线。image.png

image.png
image.png

二、线性SVM的数学逻辑(简化版)

1. 决策面方程

在二维空间中,分类线可以表示为:ω,其中ω是法向量(决定线的方向),是截距(决定线的位置)。扩展到高维空间,这就是超平面方程。

2. 分类间隔最大化

样本点到分类线的距离公式为:ωωωω的二范数)。为了让分类更稳定,我们要最大化这个距离,最终转化为最小化ω(计算更简便),同时满足约束条件:ω是样本标签,取+1或-1,确保样本都在分类间隔外侧)。

3. 关键优化工具

这个带约束的优化问题,需要通过拉格朗日乘数法和KKT条件转化为对偶问题,再用序列最小优化(SMO)算法求解。SMO的核心思路是“化整为零”,每次只优化两个变量,逐步逼近最优解,大大降低计算复杂度。

三、实战:用Python实现可运行的线性SVM

我们用一个二维线性可分数据集,实现简化版SMO算法,直观感受SVM的分类过程。首先准备测试数据集(testSet.txt),你可以直接复制以下内容保存为testSet.txt文件:

3.542485 1.977398 1
3.018896 2.556416 1
7.551510 -1.580030 -1
2.114999 -0.004466 1
8.127113 1.274372 -1
7.108772 -0.986906 -1
...(完整数据集可通过文末说明生成,或直接使用代码中随机生成的测试数据)

1. 完整可运行代码(封装为类)

import matplotlib.pyplot as plt
import numpy as np
import random

class SimpleSVM:
    """简化版线性SVM实现(基于SMO算法)"""
    def __init__(self, C=0.6, toler=0.001, max_iter=40):
        """
        初始化SVM参数
        :param C: 惩罚参数,越大对误分类惩罚越重
        :param toler: 容错率
        :param max_iter: 最大迭代次数
        """
        self.C = C
        self.toler = toler
        self.max_iter = max_iter
        self.w = None  # 权重向量
        self.b = 0     # 阈值
        self.alphas = None  # 拉格朗日乘子
        self.data_mat = None  # 数据矩阵
        self.label_mat = None  # 标签矩阵

    def load_data(self, file_path):
        """
        加载数据集
        :param file_path: 数据文件路径
        :return: 特征矩阵,标签列表
        """
        data_list = []
        label_list = []
        with open(file_path, 'r', encoding='utf-8') as f:
            for line in f.readlines():
                line_split = line.strip().split('\t')
                # 读取两个特征和标签
                data_list.append([float(line_split[0]), float(line_split[1])])
                label_list.append(float(line_split[2]))
        self.data_mat = np.mat(data_list)
        self.label_mat = np.mat(label_list).T
        return data_list, label_list

    def generate_test_data(self):
        """
        生成随机测试数据(替代testSet.txt,方便直接运行)
        :return: 特征矩阵,标签列表
        """
        # 正样本(聚类在(2,2)附近)
        pos_data = np.random.normal(loc=[2, 2], scale=0.5, size=(30, 2))
        pos_label = np.ones(30)
        # 负样本(聚类在(6,6)附近)
        neg_data = np.random.normal(loc=[6, 6], scale=0.5, size=(30, 2))
        neg_label = -np.ones(30)
        # 合并数据
        data_list = np.vstack((pos_data, neg_data)).tolist()
        label_list = np.hstack((pos_label, neg_label)).tolist()
        self.data_mat = np.mat(data_list)
        self.label_mat = np.mat(label_list).T
        return data_list, label_list

    def _select_j(self, i, m):
        """
        随机选择第二个优化变量(与第一个i不同)
        :param i: 第一个变量索引
        :param m: 样本总数
        :return: 第二个变量索引
        """
        j = i
        while j == i:
            j = int(random.uniform(0, m))
        return j

    def _clip_alpha(self, alpha_j, h, l):
        """
        修剪alpha值,确保在[0, C]范围内
        :param alpha_j: 待修剪的alpha值
        :param h: 上边界
        :param l: 下边界
        :return: 修剪后的alpha值
        """
        if alpha_j > h:
            return h
        if alpha_j < l:
            return l
        return alpha_j

    def train(self):
        """
        训练SVM(简化版SMO算法)
        :return: 阈值b,拉格朗日乘子alphas
        """
        m, n = np.shape(self.data_mat)
        self.alphas = np.mat(np.zeros((m, 1)))
        iter_count = 0

        while iter_count < self.max_iter:
            alpha_pairs_changed = 0
            for i in range(m):
                # 计算第i个样本的预测值
                f_x_i = float(np.multiply(self.alphas, self.label_mat).T * 
                              (self.data_mat * self.data_mat[i, :].T)) + self.b
                # 计算预测误差
                e_i = f_x_i - float(self.label_mat[i])

                # 违反KKT条件则需要优化
                if ((self.label_mat[i] * e_i < -self.toler) and (self.alphas[i] < self.C)) or \
                        ((self.label_mat[i] * e_i > self.toler) and (self.alphas[i] > 0)):
                    # 选择第二个优化变量j
                    j = self._select_j(i, m)
                    # 计算j的预测值和误差
                    f_x_j = float(np.multiply(self.alphas, self.label_mat).T * 
                                  (self.data_mat * self.data_mat[j, :].T)) + self.b
                    e_j = f_x_j - float(self.label_mat[j])

                    # 保存更新前的alpha值
                    alpha_i_old = self.alphas[i].copy()
                    alpha_j_old = self.alphas[j].copy()

                    # 确定alpha_j的边界L和H
                    if self.label_mat[i] != self.label_mat[j]:
                        l = max(0, self.alphas[j] - self.alphas[i])
                        h = min(self.C, self.C + self.alphas[j] - self.alphas[i])
                    else:
                        l = max(0, self.alphas[j] + self.alphas[i] - self.C)
                        h = min(self.C, self.alphas[j] + self.alphas[i])
                    if l == h:
                        continue

                    # 计算学习速率eta
                    eta = 2.0 * self.data_mat[i, :] * self.data_mat[j, :].T - \
                          self.data_mat[i, :] * self.data_mat[i, :].T - \
                          self.data_mat[j, :] * self.data_mat[j, :].T
                    if eta >= 0:
                        continue

                    # 更新alpha_j
                    self.alphas[j] -= self.label_mat[j] * (e_i - e_j) / eta
                    # 修剪alpha_j
                    self.alphas[j] = self._clip_alpha(self.alphas[j], h, l)

                    # 变化太小则跳过
                    if abs(self.alphas[j] - alpha_j_old) < 1e-5:
                        continue

                    # 更新alpha_i
                    self.alphas[i] += self.label_mat[j] * self.label_mat[i] * (alpha_j_old - self.alphas[j])

                    # 更新阈值b
                    b1 = self.b - e_i - self.label_mat[i] * (self.alphas[i] - alpha_i_old) * \
                         (self.data_mat[i, :] * self.data_mat[i, :].T) - \
                         self.label_mat[j] * (self.alphas[j] - alpha_j_old) * \
                         (self.data_mat[i, :] * self.data_mat[j, :].T)
                    b2 = self.b - e_j - self.label_mat[i] * (self.alphas[i] - alpha_i_old) * \
                         (self.data_mat[i, :] * self.data_mat[j, :].T) - \
                         self.label_mat[j] * (self.alphas[j] - alpha_j_old) * \
                         (self.data_mat[j, :] * self.data_mat[j, :].T)

                    if 0 < self.alphas[i] < self.C:
                        self.b = b1
                    elif 0 < self.alphas[j] < self.C:
                        self.b = b2
                    else:
                        self.b = (b1 + b2) / 2.0

                    alpha_pairs_changed += 1

            # 更新迭代次数
            if alpha_pairs_changed == 0:
                iter_count += 1
            else:
                iter_count = 0
            print(f"迭代次数:{iter_count},更新的alpha对数量:{alpha_pairs_changed}")

        # 计算权重w
        self._calculate_w()
        return self.b, self.alphas

    def _calculate_w(self):
        """计算权重向量w"""
        alphas_arr = np.array(self.alphas)
        data_mat_arr = np.array(self.data_mat)
        label_mat_arr = np.array(self.label_mat)
        self.w = np.dot((np.tile(label_mat_arr, (1, 2)) * data_mat_arr).T, alphas_arr)

    def show_data(self, data_list, label_list):
        """可视化原始数据集"""
        # 分离正负样本
        pos_data = []
        neg_data = []
        for i in range(len(data_list)):
            if label_list[i] > 0:
                pos_data.append(data_list[i])
            else:
                neg_data.append(data_list[i])
        # 转换为numpy数组
        pos_arr = np.array(pos_data)
        neg_arr = np.array(neg_data)
        # 绘图
        plt.figure(figsize=(8, 6))
        plt.scatter(pos_arr[:, 0], pos_arr[:, 1], c='red', label='正样本', alpha=0.7)
        plt.scatter(neg_arr[:, 0], neg_arr[:, 1], c='blue', label='负样本', alpha=0.7)
        plt.xlabel('特征1')
        plt.ylabel('特征2')
        plt.title('SVM原始数据集可视化')
        plt.legend()
        plt.grid(alpha=0.3)
        plt.show()

    def show_classifier(self, data_list, label_list):
        """可视化分类结果(含分类线和支持向量)"""
        # 绘制原始样本
        pos_data = []
        neg_data = []
        for i in range(len(data_list)):
            if label_list[i] > 0:
                pos_data.append(data_list[i])
            else:
                neg_data.append(data_list[i])
        pos_arr = np.array(pos_data)
        neg_arr = np.array(neg_data)

        plt.figure(figsize=(8, 6))
        plt.scatter(pos_arr[:, 0], pos_arr[:, 1], c='red', label='正样本', alpha=0.7)
        plt.scatter(neg_arr[:, 0], neg_arr[:, 1], c='blue', label='负样本', alpha=0.7)

        # 绘制分类线
        if self.w is not None:
            w1 = float(self.w[0])
            w2 = float(self.w[1])
            b_val = float(self.b)
            # 计算分类线的两个端点(覆盖数据范围)
            x_min = min(np.array(data_list)[:, 0]) - 1
            x_max = max(np.array(data_list)[:, 0]) + 1
            # 由w1*x1 + w2*x2 + b = 0 推导 x2 = (-w1*x1 -b)/w2
            y_min = (-w1 * x_min - b_val) / w2
            y_max = (-w1 * x_max - b_val) / w2
            plt.plot([x_min, x_max], [y_min, y_max], c='green', label='分类线', linewidth=2)

        # 标记支持向量(alpha>1e-5的样本)
        alphas_arr = np.array(self.alphas).flatten()
        for i in range(len(data_list)):
            if alphas_arr[i] > 1e-5:
                x, y = data_list[i]
                plt.scatter(x, y, s=150, c='none', edgecolor='black', linewidth=2, label='支持向量' if i == np.argmax(alphas_arr>1e-5) else "")

        plt.xlabel('特征1')
        plt.ylabel('特征2')
        plt.title('SVM分类结果可视化')
        plt.legend()
        plt.grid(alpha=0.3)
        plt.show()

    def predict(self, x):
        """
        预测单个样本的类别
        :param x: 输入样本(二维特征)
        :return: 预测标签(1或-1)
        """
        x_mat = np.mat(x)
        pred = float(x_mat * self.w + self.b)
        return 1 if pred >= 0 else -1

# 主函数:测试SVM
if __name__ == '__main__':
    # 初始化SVM
    svm = SimpleSVM(C=0.6, toler=0.001, max_iter=40)
    
    # 方式1:加载本地文件(需提前准备testSet.txt)
    # data_list, label_list = svm.load_data('testSet.txt')
    
    # 方式2:生成随机测试数据(无需文件,直接运行)
    data_list, label_list = svm.generate_test_data()
    
    # 可视化原始数据
    svm.show_data(data_list, label_list)
    
    # 训练SVM
    b, alphas = svm.train()
    print(f"训练完成,阈值b:{b:.4f}")
    print(f"权重w:{svm.w.flatten()}")
    
    # 可视化分类结果
    svm.show_classifier(data_list, label_list)
    
    # 测试预测
    test_sample = [4, 4]
    pred_label = svm.predict(test_sample)
    print(f"测试样本{test_sample}的预测标签:{pred_label}")
image.png
image.png

2. 代码说明与运行指引

(1)代码结构
  • SimpleSVM类:封装了SVM的所有核心功能,包括数据加载、模型训练、参数计算、可视化等;

  • 变量命名规范:所有变量名采用小写+下划线的Python规范(如data_matlabel_mat),函数名同样遵循该规范(如_calculate_wshow_classifier);

  • 双模式运行

    • 模式1:加载本地testSet.txt文件(需手动创建);

    • 模式2:自动生成随机测试数据(无需文件,直接运行)。

(2)直接运行步骤
  1. 复制上述完整代码到Python文件(如svm_demo.py);

  2. 确保安装了依赖库:pip install numpy matplotlib

  3. 直接运行代码,会自动完成:

  • 生成随机线性可分的测试数据;

  • 可视化原始数据分布;

  • 执行SMO算法训练SVM;

  • 输出迭代过程和模型参数;

  • 可视化分类线和支持向量;

  • 测试单个样本的预测结果。

3. 运行结果解读

  • 原始数据可视化:会显示红色正样本(聚类在(2,2)附近)和蓝色负样本(聚类在(6,6)附近),两类样本线性可分;

  • 训练过程输出:打印每次迭代的更新情况,直到收敛(迭代次数达到max_iter或无alpha对更新);

  • 分类结果可视化

    • 绿色直线:SVM找到的最优分类线;

    • 黑色空心圈:支持向量(alpha>1e-5的样本);

    • 分类线是距离两类样本最远的最优边界;

  • 预测结果:测试样本[4,4]会被预测为-1(负样本),符合数据分布逻辑。

四、SVM的扩展应用

1. 非线性SVM(核函数)

如果需要处理非线性数据,可在SimpleSVM类中添加核函数支持,比如径向基函数(RBF):

def _kernel(self, x1, x2, kernel_type='rbf', sigma=1.0):
    """核函数实现"""
    if kernel_type == 'linear':
        return x1 * x2.T
    elif kernel_type == 'rbf':
        return np.exp(-np.linalg.norm(x1 - x2) **2 / (2 * sigma** 2))

2. 使用sklearn快速实现SVM

如果追求工业级效率,可直接使用sklearn的SVM模块:

from sklearn.svm import SVC
from sklearn.metrics import accuracy_score

# 准备数据
X = np.array(data_list)
y = np.array(label_list)

# 训练SVM
svc = SVC(kernel='linear', C=0.6)
svc.fit(X, y)

# 预测
y_pred = svc.predict(X)
print(f"分类准确率:{accuracy_score(y, y_pred):.4f}")

# 可视化分类线(略)

五、总结

本文实现的SimpleSVM类包含完整的可运行代码,变量名和函数名符合Python规范,无需依赖额外数据集即可直接运行。通过简化版SMO算法,清晰展示了线性SVM的核心训练过程,同时提供了直观的可视化功能。

该代码保留了SVM的核心逻辑,去掉了冗余的复杂优化,适合入门学习;如果需要处理更复杂的场景,可在此基础上添加核函数、多分类支持、交叉验证等功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值