用Python代码解锁机器学习核心概念:从公式到实战

打开Jupyter Notebook,我们换个方式复习机器学习——不是死记硬背定义,而是用代码重现那些让你头疼的数学公式。当你能在屏幕上看到信息增益如何计算、SVM决策边界如何形成时,这些概念会突然变得清晰起来。

1. 决策树实战:亲手计算信息增益

理论课本告诉我们信息增益是划分属性的关键指标,但看着那一串熵的公式总让人云里雾雾。让我们用Python实现一个完整的计算过程:

import numpy as np
from math import log2

def entropy(probabilities):
    return -sum(p * log2(p) for p in probabilities if p > 0)

# 示例数据集:天气对是否打网球的影响
data = {
    'outlook': ['sunny', 'sunny', 'overcast', 'rain', 'rain', 'rain', 'overcast'],
    'play': ['no', 'no', 'yes', 'yes', 'yes', 'no', 'yes']
}

# 计算整体熵
total_play = data['play']
p_yes = total_play.count('yes')/len(total_play)
p_no = total_play.count('no')/len(total_play)
H_play = entropy([p_yes, p_no])
print(f"整体熵: {H_play:.4f}")

# 计算按outlook划分后的条件熵
def conditional_entropy(feature, target):
    categories = set(feature)
    cond_entropy = 0
    for cat in categories:
        indices = [i for i, x in enumerate(feature) if x == cat]
        subset = [target[i] for i in indices]
        p_subset = len(subset)/len(target)
        p_yes_sub = subset.count('yes')/len(subset)
        p_no_sub = subset.count('no')/len(subset)
        cond_entropy += p_subset * entropy([p_yes_sub, p_no_sub])
    return cond_entropy

H_play_outlook = conditional_entropy(data['outlook'], data['play'])
print(f"按outlook划分后的条件熵: {H_play_outlook:.4f}")
print(f"信息增益: {H_play - H_play_outlook:.4f}")

运行这段代码,你会看到:

整体熵: 0.9852
按outlook划分后的条件熵: 0.6793
信息增益: 0.3059

关键观察点

  • 信息增益计算的核心是熵的差值
  • 每个特征划分后,我们计算子集的熵并加权平均
  • 信息增益越大,说明该特征对分类的贡献越大

提示:尝试修改数据集,观察不同特征带来的信息增益变化。比如增加温度特征,比较它与outlook哪个信息增益更大。

2. 可视化理解过拟合与正则化

过拟合是机器学习中最让人头疼的问题之一。让我们用多项式回归的例子,直观展示正则化如何控制模型复杂度:

import numpy as np
import matplotlib.pyplot as plt
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression, Ridge
from sklearn.metrics import mean_squared_error

np.random.seed(42)
X = np.linspace(0, 1, 30)
y = np.sin(2 * np.pi * X) + np.random.normal(0, 0.1, X.shape[0])
X = X[:, np.newaxis]

degrees = [1, 4, 15]
plt.figure(figsize=(18, 5))

for i, degree in enumerate(degrees):
    # 无正则化
    polynomial_features = PolynomialFeatures(degree=degree, include_bias=False)
    linear_regression = LinearRegression()
    pipeline = Pipeline([("pf", polynomial_features), ("lr", linear_regression)])
    pipeline.fit(X, y)
    
    # 带L2正则化(Ridge回归)
    ridge = Ridge(alpha=0.1)
    ridge_pipeline = Pipeline([("pf", polynomial_features), ("ridge", ridge)])
    ridge_pipeline.fit(X, y)
    
    # 可视化
    plt.subplot(1, 3, i+1)
    X_test = np.linspace(0, 1, 100)[:, np.newaxis]
    plt.scatter(X, y, s=20, label="训练数据")
    plt.plot(X_test, pipeline.predict(X_test), label="无正则化")
    plt.plot(X_test, ridge_pipeline.predict(X_test), label="L2正则化")
    plt.plot(X_test, np.sin(2 * np.pi * X_test), '--', label="真实函数")
    plt.title(f"degree = {degree}")
    plt.legend()

plt.tight_layout()
plt.show()

这段代码生成三个子图,对比不同多项式阶数下,普通线性回归与Ridge回归的表现:

多项式阶数 无正则化表现 L2正则化表现
1 (欠拟合) 无法拟合曲线 类似直线
4 (适中) 较好拟合 更平滑拟合
15 (过拟合) 剧烈震荡 抑制了震荡

核心发现

  • 高阶多项式容易产生过拟合,在训练数据上表现完美但泛化能力差
  • L2正则化通过惩罚大系数,使模型更平滑
  • 正则化强度(alpha)需要调优:太大导致欠拟合,太小无法抑制过拟合

3. 从零实现感知机:理解线性分类基础

感知机是神经网络的基础,让我们抛开scikit-learn,用纯Python实现一个基础版本:

import numpy as np

class Perceptron:
    def __init__(self, learning_rate=0.01, n_iters=1000):
        self.lr = learning_rate
        self.n_iters = n_iters
        self.weights = None
        self.bias = None
    
    def fit(self, X, y):
        n_samples, n_features = X.shape
        self.weights = np.zeros(n_features)
        self.bias = 0
        
        # 确保标签为-1或1
        y_ = np.where(y <= 0, -1, 1)
        
        for _ in range(self.n_iters):
            for idx, x_i in enumerate(X):
                condition = y_[idx] * (np.dot(x_i, self.weights) + self.bias) <= 0
                if condition:
                    self.weights += self.lr * y_[idx] * x_i
                    self.bias += self.lr * y_[idx]
    
    def predict(self, X):
        linear_output = np.dot(X, self.weights) + self.bias
        return np.where(linear_output >= 0, 1, 0)

# 测试AND逻辑门
X = np.array([[0,0], [0,1], [1,0], [1,1]])
y = np.array([0, 0, 0, 1])

p = Perceptron()
p.fit(X, y)
print("预测结果:", p.predict(X))
print("权重:", p.weights)
print("偏置:", p.bias)

实现要点解析

  1. 初始化权重和偏置为零
  2. 对每个误分类样本,按规则更新参数:
    • w = w + η * y_i * x_i
    • b = b + η * y_i
  3. 预测时使用符号函数

注意:感知机只能解决线性可分问题。尝试用XOR数据测试,会发现它无法收敛——这正是引入多层神经网络的原因。

4. SVM实战:绘制决策边界与支持向量

支持向量机(SVM)的核心是找到最大间隔超平面。让我们用scikit-learn可视化这一过程:

import numpy as np
import matplotlib.pyplot as plt
from sklearn import svm
from sklearn.datasets import make_blobs

# 创建线性可分数据
X, y = make_blobs(n_samples=50, centers=2, random_state=6)
clf = svm.SVC(kernel='linear', C=1000)
clf.fit(X, y)

plt.scatter(X[:, 0], X[:, 1], c=y, s=30, cmap=plt.cm.Paired)

# 绘制决策边界
ax = plt.gca()
xlim = ax.get_xlim()
ylim = ax.get_ylim()

# 创建网格评估模型
xx = np.linspace(xlim[0], xlim[1], 30)
yy = np.linspace(ylim[0], ylim[1], 30)
YY, XX = np.meshgrid(yy, xx)
xy = np.vstack([XX.ravel(), YY.ravel()]).T
Z = clf.decision_function(xy).reshape(XX.shape)

# 绘制决策边界和间隔
ax.contour(XX, YY, Z, colors='k', levels=[-1, 0, 1], 
           alpha=0.5, linestyles=['--', '-', '--'])
# 标记支持向量
ax.scatter(clf.support_vectors_[:, 0], clf.support_vectors_[:, 1], 
           s=100, linewidth=1, facecolors='none', edgecolors='k')
plt.title("SVM决策边界与支持向量")
plt.show()

这段代码展示了几个关键概念:

  • 决策边界 :中间的实线
  • 间隔边界 :两侧的虚线
  • 支持向量 :落在间隔边界上的样本点

参数C的作用实验

for C in [0.1, 1, 1000]:
    clf = svm.SVC(kernel='linear', C=C).fit(X, y)
    # 可视化代码同上...
    plt.title(f"C={C}")

观察不同C值的影响:

  • C小:允许更多误分类,间隔更大
  • C大:严格要求分类正确,间隔更小

5. 模型评估:精确率-召回率曲线实战

分类模型的评估不只是看准确率。P-R曲线能更全面反映模型性能,特别是在类别不平衡时:

from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import precision_recall_curve, average_precision_score
import matplotlib.pyplot as plt

# 生成不平衡数据(90%负类,10%正类)
X, y = make_classification(n_samples=1000, n_classes=2, weights=[0.9, 0.1], random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

# 训练模型
clf = LogisticRegression().fit(X_train, y_train)
y_scores = clf.predict_proba(X_test)[:, 1]  # 正类的预测概率

# 计算P-R曲线
precision, recall, thresholds = precision_recall_curve(y_test, y_scores)
ap = average_precision_score(y_test, y_scores)

plt.plot(recall, precision, label=f'AP={ap:.2f}')
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.title('P-R曲线')
plt.legend()
plt.show()

# 对比不同阈值下的指标
thresholds = np.append(thresholds, 1)
for i in [100, 200, 300]:  # 查看几个关键点
    print(f"阈值={thresholds[i]:.2f}, 精确率={precision[i]:.2f}, 召回率={recall[i]:.2f}")

P-R曲线解读要点

  • 曲线越靠近右上角,模型性能越好
  • 平衡点(BEP)是P=R时的点
  • 平均精度(AP)是曲线下面积的近似

实际应用中,可以根据业务需求选择阈值:

  • 需要高精确率(如垃圾邮件分类):选择高阈值
  • 需要高召回率(如疾病筛查):选择低阈值

6. 神经网络基础:手写数字识别实战

用最简单的全连接网络实现MNIST分类,理解神经网络的基本运作:

import tensorflow as tf
from tensorflow.keras import layers, models
import matplotlib.pyplot as plt

# 加载数据
(train_images, train_labels), (test_images, test_labels) = tf.keras.datasets.mnist.load_data()
train_images = train_images.reshape((60000, 28*28)).astype('float32') / 255
test_images = test_images.reshape((10000, 28*28)).astype('float32') / 255

# 构建网络
model = models.Sequential([
    layers.Dense(512, activation='relu', input_shape=(28*28,)),
    layers.Dense(10, activation='softmax')
])

model.compile(optimizer='rmsprop',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

# 训练
history = model.fit(train_images, train_labels, epochs=5, batch_size=128, 
                    validation_split=0.2)

# 绘制训练曲线
plt.plot(history.history['accuracy'], label='训练准确率')
plt.plot(history.history['val_accuracy'], label='验证准确率')
plt.title('训练过程')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend()
plt.show()

# 评估
test_loss, test_acc = model.evaluate(test_images, test_labels)
print(f"测试准确率: {test_acc:.4f}")

关键组件解析

  • Dense层 :全连接层, relu 激活函数引入非线性
  • Softmax输出 :将输出转化为概率分布
  • 交叉熵损失 :适合分类问题的损失函数
  • 优化器 :RMSprop调整学习率

扩展实验:

  1. 尝试增加/减少隐藏层神经元数量,观察模型容量对性能的影响
  2. 添加Dropout层防止过拟合
  3. 比较不同优化器(SGD, Adam)的效果

7. 集成方法实战:随机森林特征重要性分析

随机森林不仅能提供高精度预测,还能评估特征重要性:

from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt

# 加载数据
iris = load_iris()
X, y = iris.data, iris.target
feature_names = iris.feature_names

# 训练随机森林
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X, y)

# 获取特征重要性
importances = rf.feature_importances_
indices = np.argsort(importances)[::-1]

# 可视化
plt.figure()
plt.title("特征重要性")
plt.bar(range(X.shape[1]), importances[indices], align="center")
plt.xticks(range(X.shape[1]), [feature_names[i] for i in indices])
plt.show()

# 输出具体数值
for f in range(X.shape[1]):
    print(f"{feature_names[indices[f]]}: {importances[indices[f]]:.4f}")

随机森林的两个随机性

  1. 数据随机采样(bootstrap)
  2. 特征随机选择

特征重要性解读

  • 数值越大表示该特征对预测贡献越大
  • 可用于特征选择,去除不重要特征简化模型

对比实验:尝试用不同树数量(n_estimators)观察特征重要性的稳定性

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐