别再死记硬背了!用Python+Scikit-learn实战复现机器学习期末考点(附代码)
用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)
实现要点解析 :
- 初始化权重和偏置为零
- 对每个误分类样本,按规则更新参数:
w = w + η * y_i * x_ib = b + η * y_i
- 预测时使用符号函数
注意:感知机只能解决线性可分问题。尝试用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调整学习率
扩展实验:
- 尝试增加/减少隐藏层神经元数量,观察模型容量对性能的影响
- 添加Dropout层防止过拟合
- 比较不同优化器(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}")
随机森林的两个随机性 :
- 数据随机采样(bootstrap)
- 特征随机选择
特征重要性解读 :
- 数值越大表示该特征对预测贡献越大
- 可用于特征选择,去除不重要特征简化模型
对比实验:尝试用不同树数量(n_estimators)观察特征重要性的稳定性
更多推荐

所有评论(0)