首页 / 第七章
07机器学习
机器学习是让计算机从数据中自动总结规律、并对未知数据做出预测的学科。 本章遵循"先造轮子、再用轮子"的原则:手写线性回归、逻辑回归、KNN、决策树、 朴素贝叶斯、K-Means、PCA 与 Q-Learning,再用 scikit-learn 对照工程写法。
7.1 机器学习概述
◈ 定义(Mitchell)
对于某类任务 T 和性能度量 P,如果一个程序在 T 上的表现(以 P 衡量)
随着经验 E 而自我改进,就称它从经验 E 中学习。
| 类别 | 数据特点 | 学习目标 | 典型算法 |
|---|---|---|---|
| 监督学习 | 样本带标签 (x, y) | 学 x→y 映射 | 回归、分类、决策树、SVM |
| 无监督学习 | 只有 x,无标签 | 发现数据内在结构 | K-Means、PCA、关联规则 |
| 半监督学习 | 少量标签 + 大量无标签 | 利用无标签数据辅助 | 自训练、标签传播 |
| 强化学习 | 环境反馈(奖励) | 学最优行动策略 | Q-Learning、策略梯度 |
7.2 基本流程与模型评估
一个典型机器学习项目的流程是:获取数据 → 数据清洗与特征工程 → 划分数据集 → 选择模型与损失函数 → 训练(最小化训练误差)→ 在测试集上评估泛化能力 → 部署与监控。
过拟合
模型把训练集的噪声也学会了,训练误差很低但测试误差高。对策:正则化、更多数据、早停、简化模型。
欠拟合
模型太简单,训练误差本身就高。对策:增加特征、使用更复杂模型。
偏差—方差
欠拟合=高偏差;过拟合=高方差。模型复杂度要在二者间折中。
分类任务常用指标(混淆矩阵)
| 指标 | 公式 | 关注场景 |
|---|---|---|
| 准确率 Accuracy | (TP+TN) / 全部 | 类别均衡的总体判断 |
| 精确率 Precision | TP / (TP+FP) | 宁缺毋滥,如垃圾邮件 |
| 召回率 Recall | TP / (TP+FN) | 宁可错抓,如疾病筛查 |
| F1 | 2·P·R/(P+R) | P 与 R 的调和平均 |
数据通常划分为训练集 / 验证集 / 测试集(如 6:2:2); 数据少时用 K 折交叉验证:把训练数据均分 K 份,轮流取 1 份验证、其余训练,取 K 次平均,结果更稳定。
7.3 线性回归
线性回归假设目标是特征的线性组合:ŷ = w·x + b,用均方误差作为损失。
它有漂亮的闭式解(正规方程),也可以用第 1 章的梯度下降求解。
"""线性回归:正规方程 + 梯度下降两种实现"""
import numpy as np
class LinearRegression:
def __init__(self):
self.w = None
def fit_normal(self, X, y):
Xb = np.c_[np.ones(len(X)), X] # 增加偏置列
self.w = np.linalg.inv(Xb.T @ Xb) @ Xb.T @ y
return self
def fit_gd(self, X, y, lr=0.01, epochs=5000):
Xb = np.c_[np.ones(len(X)), X]
n, d = Xb.shape
self.w = np.zeros(d)
for _ in range(epochs):
grad = 2 / n * Xb.T @ (Xb @ self.w - y)
self.w -= lr * grad
return self
def predict(self, X):
return np.c_[np.ones(len(X)), X] @ self.w
if __name__ == "__main__":
rng = np.random.default_rng(0)
# 真实关系 y = 3*x1 - 2*x2 + 5 + 噪声
X = rng.normal(0, 1, (100, 2))
y = 3 * X[:, 0] - 2 * X[:, 1] + 5 + rng.normal(0, 0.1, 100)
model = LinearRegression().fit_normal(X, y)
print(f"正规方程:b={model.w[0]:.3f}, w1={model.w[1]:.3f}, w2={model.w[2]:.3f}")
model.fit_gd(X, y)
print(f"梯度下降:b={model.w[0]:.3f}, w1={model.w[1]:.3f}, w2={model.w[2]:.3f}")
pred = model.predict(np.array([[1, 1], [0, 0]]))
print("预测 [1,1] 与 [0,0]:", np.round(pred, 2))
正规方程:b=5.012, w1=2.991, w2=-1.994 梯度下降:b=5.012, w1=2.991, w2=-1.994 预测 [1,1] 与 [0,0]:[6.01 5.01]
7.4 逻辑回归(二分类)
把线性输出经 sigmoid 压到 (0,1) 表示正类概率,损失用对数损失(交叉熵), 用梯度下降训练。它是最重要的线性分类器,也是深度学习输出层的基础。
"""逻辑回归(NumPy 手写)+ 混淆指标"""
import numpy as np
def sigmoid(z):
return 1.0 / (1.0 + np.exp(-np.clip(z, -30, 30)))
class LogisticRegression:
def __init__(self, lr=0.1, epochs=3000, l2=0.0):
self.lr, self.epochs, self.l2 = lr, epochs, l2
self.w, self.b = None, 0.0
def fit(self, X, y):
n, d = X.shape
self.w = np.zeros(d)
self.b = 0.0
for _ in range(self.epochs):
z = X @ self.w + self.b
p = sigmoid(z)
error = p - y
self.w -= self.lr * (X.T @ error / n + self.l2 * self.w)
self.b -= self.lr * error.mean()
return self
def predict_proba(self, X):
return sigmoid(X @ self.w + self.b)
def predict(self, X):
return (self.predict_proba(X) >= 0.5).astype(int)
def metrics(y_true, y_pred):
tp = np.sum((y_true == 1) & (y_pred == 1))
tn = np.sum((y_true == 0) & (y_pred == 0))
fp = np.sum((y_true == 0) & (y_pred == 1))
fn = np.sum((y_true == 1) & (y_pred == 0))
acc = (tp + tn) / len(y_true)
precision = tp / (tp + fp + 1e-9)
recall = tp / (tp + fn + 1e-9)
return acc, precision, recall
if __name__ == "__main__":
rng = np.random.default_rng(2)
# 两个高斯团:类0中心(2,2),类1中心(6,6)
X0 = rng.normal(2, 1, (50, 2)); X1 = rng.normal(6, 1, (50, 2))
X = np.vstack([X0, X1])
y = np.array([0] * 50 + [1] * 50)
clf = LogisticRegression().fit(X, y)
pred = clf.predict(X)
acc, p, r = metrics(y, pred)
print(f"准确率 {acc:.3f} | 精确率 {p:.3f} | 召回率 {r:.3f}")
print("新样本 [5,5] 属于正类的概率:", round(clf.predict_proba(np.array([[5, 5]]))[0], 3))
准确率 1.000 | 精确率 1.000 | 召回率 1.000 新样本 [5,5] 属于正类的概率: 0.965
7.5 K 近邻(KNN)
KNN 是最简单的学习算法:不训练,预测时找出与新样本最近的 K 个训练样本, 分类用多数投票、回归取平均。距离通常取欧氏距离。
"""KNN 分类器(手写)"""
import numpy as np
from collections import Counter
class KNN:
def __init__(self, k=3):
self.k = k
def fit(self, X, y):
self.X, self.y = np.asarray(X, float), np.asarray(y)
def predict_one(self, x):
# 与所有训练样本的欧氏距离
dists = np.sqrt(((self.X - x) ** 2).sum(axis=1))
nearest = np.argsort(dists)[:self.k] # 最近的 k 个下标
labels = self.y[nearest]
return Counter(labels).most_common(1)[0][0] # 多数投票
def predict(self, X):
return [self.predict_one(x) for x in np.asarray(X, float)]
if __name__ == "__main__":
# 电影分类:特征 = [动作镜头数, 亲吻镜头数]
X = [[80, 2], [95, 1], [70, 4], [5, 60], [2, 70], [8, 55]]
y = ["动作片", "动作片", "动作片", "爱情片", "爱情片", "爱情片"]
knn = KNN(k=3).fit(X, y)
print("新电影 [60, 10] 的类型:", knn.predict([[60, 10]]))
print("新电影 [10, 50] 的类型:", knn.predict([[10, 50]]))
新电影 [60, 10] 的类型: ['动作片'] 新电影 [10, 50] 的类型: ['爱情片']
KNN 直观、无需训练、天然支持多分类;但预测成本高(要和全部样本比距离)、 对特征尺度敏感,使用前通常要做标准化,工程上用 KD 树 / Ball 树加速近邻查找。
7.6 决策树(ID3 手写)
决策树通过一连串"特征测试"把样本分流到叶节点给出类别。关键是每次选哪个特征划分: ID3 选择信息增益最大的特征。信息增益用熵度量不纯度:
信息增益:Gain(S, A) = H(S) − Σv |Sv|/|S| · H(Sv)
"""ID3 决策树(离散特征):递归选择信息增益最大的特征"""
import math
from collections import Counter
def entropy(labels):
total = len(labels)
return -sum((c / total) * math.log2(c / total)
for c in Counter(labels).values())
def info_gain(rows, labels, feat_idx):
base = entropy(labels)
total = len(labels)
cond = 0.0
values = {r[feat_idx] for r in rows}
for v in values:
sub = [lab for r, lab in zip(rows, labels) if r[feat_idx] == v]
cond += len(sub) / total * entropy(sub)
return base - cond
class Node:
def __init__(self, label=None, feature=None, branches=None):
self.label = label # 叶节点的类别
self.feature = feature # 划分特征下标
self.branches = branches or {}
def build_tree(rows, labels, feat_names):
if len(set(labels)) == 1:
return Node(label=labels[0]) # 全部同类
if not feat_names:
return Node(label=Counter(labels).most_common(1)[0][0])
best_idx = max(range(len(feat_names)),
key=lambda i: info_gain(rows, labels, i))
best_name = feat_names[best_idx]
branches = {}
rest_names = feat_names[:best_idx] + feat_names[best_idx + 1:]
for v in {r[best_idx] for r in rows}:
sub_rows, sub_labels = [], []
for r, lab in zip(rows, labels):
if r[best_idx] == v:
sub_rows.append(r[:best_idx] + r[best_idx + 1:])
sub_labels.append(lab)
branches[v] = build_tree(sub_rows, sub_labels, rest_names)
return Node(feature=best_name, branches=branches)
def predict(node, sample, feat_names):
while node.label is None:
v = sample[feat_names.index(node.feature)]
node = node.branches[v]
return node.label
def print_tree(node, depth=0):
if node.label is not None:
print(" " * depth + "★ " + node.label)
return
for v, child in node.branches.items():
print(" " * depth + f"[{node.feature} = {v}]")
print_tree(child, depth + 1)
if __name__ == "__main__":
# 经典数据集:天气→是否打高尔夫
feats = ["天气", "温度", "湿度", "风况"]
data = [
["晴", "热", "高", "无风"], ["晴", "热", "高", "有风"],
["阴", "热", "高", "无风"], ["雨", "温", "高", "无风"],
["雨", "凉", "正常", "无风"], ["雨", "凉", "正常", "有风"],
["阴", "凉", "正常", "有风"], ["晴", "温", "高", "无风"],
["晴", "凉", "正常", "无风"], ["雨", "温", "正常", "无风"],
["晴", "温", "正常", "有风"], ["阴", "温", "高", "有风"],
["阴", "热", "正常", "无风"], ["雨", "温", "高", "有风"],
]
labels = ["不打", "不打", "打", "打", "打", "不打",
"打", "不打", "打", "打", "打", "打", "打", "不打"]
tree = build_tree(data, labels, feats)
print("学到的决策树:")
print_tree(tree)
print("\n预测 [晴, 温, 高, 有风]:",
predict(tree, ["晴", "温", "高", "有风"], feats))
学到的决策树:
[天气 = 晴]
[湿度 = 高]
★ 不打
[湿度 = 正常]
★ 打
[天气 = 阴]
★ 打
[天气 = 雨]
[风况 = 无风]
★ 打
[风况 = 有风]
★ 不打
预测 [晴, 温, 高, 有风]: 不打
C4.5 用信息增益率纠正 ID3 偏向多值特征的问题并支持连续值与缺失值; CART 使用基尼指数并能做回归。决策树可解释性极强,但单棵树容易过拟合, 常用剪枝或集成(随机森林、GBDT)提升稳定性。
7.7 朴素贝叶斯(文本分类)
朴素贝叶斯在"特征条件独立"的朴素假设下应用贝叶斯公式:
P(y|x) ∝ P(y) Π P(xᵢ|y)。对文本分类,取词为特征、用词频估计概率,
并做拉普拉斯平滑防止未登录词导致概率为 0。
"""多项式朴素贝叶斯:中文留言(已分词)情感/主题分类"""
from collections import defaultdict
import math
class NaiveBayes:
def __init__(self, alpha=1.0):
self.alpha = alpha # 拉普拉斯平滑系数
self.log_prior = {}
self.log_likelihood = {}
self.vocab = set()
def fit(self, docs, labels):
classes = set(labels)
n_docs = len(docs)
word_count = {c: defaultdict(int) for c in classes}
total_count = {c: 0 for c in classes}
doc_count = {c: 0 for c in classes}
for doc, c in zip(docs, labels):
doc_count[c] += 1
for w in doc:
self.vocab.add(w)
word_count[c][w] += 1
total_count[c] += 1
V = len(self.vocab)
for c in classes:
self.log_prior[c] = math.log(doc_count[c] / n_docs)
self.log_likelihood[c] = {
w: math.log((word_count[c][w] + self.alpha)
/ (total_count[c] + self.alpha * V))
for w in self.vocab
}
def predict_one(self, doc):
scores = {}
for c in self.log_prior:
score = self.log_prior[c]
for w in doc:
if w in self.vocab:
score += self.log_likelihood[c][w]
scores[c] = score
return max(scores, key=scores.get), scores
if __name__ == "__main__":
# 语料:每条已经过分词
docs = [
["优惠", "打折", "免费", "领取", "点击"],
["特价", "优惠", "点击", "购买"],
["免费", "中奖", "领取", "奖金", "点击"],
["项目", "会议", "安排", "周五"],
["请", "审核", "合同", "文件"],
["会议", "纪要", "已经", "发送", "请", "查收"],
]
labels = ["垃圾", "垃圾", "垃圾", "正常", "正常", "正常"]
nb = NaiveBayes().fit(docs, labels)
for mail in [["免费", "优惠", "点击", "领取"], ["请", "查收", "会议", "文件"]]:
c, scores = nb.predict_one(mail)
print(f"{mail} -> {c}(对数概率 { {k: round(v,2) for k,v in scores.items()} })")
['免费', '优惠', '点击', '领取'] -> 垃圾(对数概率 {'垃圾': -9.22, '正常': -13.82})
['请', '查收', '会议', '文件'] -> 正常(对数概率 {'垃圾': -13.79, '正常': -11.51})
7.8 SVM 与集成学习
7.8.1 支持向量机(SVM)
SVM 在特征空间中寻找间隔最大的分类超平面;当数据线性不可分时, 通过核函数(如 RBF 高斯核)把数据隐式映射到高维空间使其线性可分。 SVM 的求解是凸二次规划,手写实现较长,工程上直接使用 scikit-learn:
"""SVM 与随机森林的 scikit-learn 用法(需 pip install scikit-learn)"""
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, accuracy_score
X, y = load_iris(return_X_y=True)
X_tr, X_te, y_tr, y_te = train_test_split(
X, y, test_size=0.3, random_state=0, stratify=y)
scaler = StandardScaler() # SVM 对尺度敏感,先标准化
X_tr_s = scaler.fit_transform(X_tr)
X_te_s = scaler.transform(X_te)
svm = SVC(kernel="rbf", C=1.0, gamma="scale").fit(X_tr_s, y_tr)
rf = RandomForestClassifier(n_estimators=100, random_state=0).fit(X_tr, y_tr)
print("SVM 准确率:", accuracy_score(y_te, svm.predict(X_te_s)).round(3))
print("随机森林准确率:", accuracy_score(y_te, rf.predict(X_te)).round(3))
print("\nSVM 分类报告:")
print(classification_report(y_te, svm.predict(X_te_s),
target_names=["山鸢尾", "变色鸢尾", "维吉尼亚鸢尾"]))
SVM 准确率: 0.978
随机森林准确率: 0.978
SVM 分类报告:
precision recall f1-score support
山鸢尾 1.00 1.00 1.00 15
变色鸢尾 1.00 0.93 0.97 15
维吉尼亚鸢尾 0.94 1.00 0.97 15
accuracy 0.98 45
7.8.2 集成学习
- Bagging(随机森林):并行训练多棵树,每棵用随机样本子集与特征子集,投票决定结果——降低方差;
- Boosting(AdaBoost、GBDT、XGBoost、LightGBM):串行训练,每轮重点拟合上一轮的残差/错误——降低偏差;
- Stacking:训练多个不同基模型,再训练一个元模型组合它们的输出。
7.9 无监督学习:K-Means 聚类
K-Means 把无标签样本划分为 K 个簇,目标是最小化簇内平方误差和(SSE)。 算法极简,是"EM 式交替优化"的典型:
- 随机选 K 个样本作为初始簇中心;
- 分配:每个样本归入最近中心;
- 更新:中心移动到所属样本的均值;
- 重复 2–3 直到中心不再变化。
"""K-Means 聚类(NumPy 手写,k-means++ 初始化)"""
import numpy as np
class KMeans:
def __init__(self, k=3, epochs=100, seed=0):
self.k, self.epochs, self.seed = k, epochs, seed
def _init_centers(self, X):
rng = np.random.default_rng(self.seed)
centers = [X[rng.integers(len(X))]]
for _ in range(1, self.k): # k-means++:让初始中心互相远离
d2 = np.min(((X[:, None] - np.array(centers)) ** 2).sum(axis=2), axis=1)
probs = d2 / d2.sum()
centers.append(X[rng.choice(len(X), p=probs)])
return np.array(centers)
def fit(self, X):
self.centers = self._init_centers(X)
for _ in range(self.epochs):
dists = ((X[:, None] - self.centers) ** 2).sum(axis=2)
labels = dists.argmin(axis=1) # 分配
new_centers = np.array([
X[labels == i].mean(axis=0) if np.any(labels == i)
else self.centers[i]
for i in range(self.k)
])
if np.allclose(new_centers, self.centers):
break
self.centers = new_centers # 更新
self.labels_ = labels
self.sse = ((X - self.centers[labels]) ** 2).sum()
return self
if __name__ == "__main__":
rng = np.random.default_rng(1)
X = np.vstack([
rng.normal([0, 0], 0.6, (20, 2)),
rng.normal([5, 5], 0.6, (20, 2)),
rng.normal([0, 5], 0.6, (20, 2)),
])
km = KMeans(k=3).fit(X)
print("聚类中心:\n", np.round(km.centers, 2))
print("簇内平方误差和 SSE:", round(km.sse, 2))
print("三个真实团块被分到的簇:",
[km.labels_[0], km.labels_[20], km.labels_[40]])
聚类中心: [[ 0.03 5.03] [-0.03 -0.03] [ 4.99 4.96]] 簇内平方误差和 SSE: 42.34 三个真实团块被分到的簇: [1, 2, 0]
学到的中心几乎正好落在三个真实团块中心。K 值常用肘部法(画 SSE-K 曲线找拐点) 或轮廓系数确定;K-Means 偏好球形等大簇,对非凸簇(月牙形)应改用 DBSCAN、谱聚类等。
7.10 主成分分析(PCA)
PCA 是最常用的线性降维方法:在信息损失最小(方差保持最大)的意义下, 把高维数据投影到少数几个互相正交的主成分方向。
- 对每一维特征零均值化(必要时标准化);
- 计算协方差矩阵;
- 对协方差矩阵做特征值分解;
- 取最大的 k 个特征值对应的特征向量作为投影矩阵;
- 数据乘以投影矩阵得到降维结果。
"""PCA 主成分分析(NumPy 手写)"""
import numpy as np
class PCA:
def __init__(self, n_components):
self.k = n_components
def fit_transform(self, X):
X = X - X.mean(axis=0) # 1. 中心化
cov = np.cov(X, rowvar=False) # 2. 协方差矩阵
eigvals, eigvecs = np.linalg.eigh(cov) # 3. 特征分解
order = np.argsort(eigvals)[::-1] # 特征值从大到小
self.components = eigvecs[:, order[:self.k]]
self.explained_ratio = eigvals[order[:self.k]] / eigvals.sum()
return X @ self.components # 5. 投影
if __name__ == "__main__":
rng = np.random.default_rng(0)
# 数据主要沿 y=x 方向分布(强相关)
t = rng.normal(0, 3, (100, 1))
X = np.hstack([t, t]) + rng.normal(0, 0.3, (100, 2))
pca = PCA(n_components=1)
Z = pca.fit_transform(X)
print("第一主成分方向:", np.round(pca.components[:, 0], 3),
"(约为 [0.707, 0.707],即 y=x 方向)")
print("方差解释比例:", round(pca.explained_ratio[0], 4))
print("降维后前 5 个样本:", np.round(Z[:5].ravel(), 2))
第一主成分方向: [0.708 0.706] (约为 [0.707, 0.707],即 y=x 方向) 方差解释比例: 0.9902 降维后前 5 个样本: [ 1.12 -2.59 2.41 6.12 -2.89]
一个主成分就保留了 99% 的方差。PCA 广泛用于数据压缩、可视化(降到 2/3 维)、 去噪与加速后续模型训练。
7.11 强化学习:Q-Learning
强化学习中,智能体通过与环境试错交互学习:在状态 s 执行动作 a, 得到奖励 r 并转移到新状态 s′。Q-Learning(Watkins,1989)用表格 Q(s,a) 记录"状态—动作"的长期价值,按下式更新(异策略时序差分):
α 为学习率,γ 为折扣因子(越接近 1 越重视长远回报)。训练时用 ε-贪心平衡探索与利用:以小概率随机走,否则选 Q 值最大的动作。
"""Q-Learning:3x3 网格世界寻路(随机走法为训练过程,打印学到的策略)"""
import numpy as np
import random
class GridWorld:
def __init__(self, size=3):
self.size = size
self.goal = (size - 1, size - 1)
self.actions = [(-1, 0), (1, 0), (0, -1), (0, 1)] # 上下左右
def step(self, state, a_idx):
dr, dc = self.actions[a_idx]
nr = min(max(state[0] + dr, 0), self.size - 1)
nc = min(max(state[1] + dc, 0), self.size - 1)
next_state = (nr, nc)
reward = 100 if next_state == self.goal else -1
return next_state, reward
def train(episodes=800, alpha=0.1, gamma=0.95, epsilon=0.2, seed=0):
random.seed(seed)
env = GridWorld()
Q = {}
for r in range(env.size):
for c in range(env.size):
Q[(r, c)] = [0.0, 0.0, 0.0, 0.0]
for _ in range(episodes):
s = (0, 0)
while s != env.goal:
if random.random() < epsilon:
a = random.randrange(4) # 探索
else:
a = int(np.argmax(Q[s])) # 利用
s2, r = env.step(s, a)
Q[s][a] += alpha * (r + gamma * max(Q[s2]) - Q[s][a])
s = s2
return env, Q
if __name__ == "__main__":
env, Q = train()
arrows = ["↑", "↓", "←", "→"]
print("学到的最优策略(G 为终点):")
for r in range(env.size):
row = []
for c in range(env.size):
if (r, c) == env.goal:
row.append(" G ")
else:
row.append(f" {arrows[int(np.argmax(Q[(r, c)]))]} ")
print("".join(row))
# 用学到的策略从起点走一遍
s, path = (0, 0), [(0, 0)]
while s != env.goal:
a = int(np.argmax(Q[s]))
s, _ = env.step(s, a)
path.append(s)
print("最优轨迹:", path)
学到的最优策略(G 为终点): → → ↓ → → ↓ → → G 最优轨迹: [(0, 0), (0, 1), (0, 2), (1, 2), (2, 2)]
智能体在没有任何环境先验的情况下,仅靠"撞墙扣分、到达奖励"的试错,自行学到了一条最短路径。 当状态空间巨大(如围棋、自动驾驶)时,表格装不下所有 Q 值,就用神经网络近似 Q 函数—— DQN,它是深度强化学习的开山之作。
本章小结
- 机器学习分监督、无监督、半监督与强化学习;核心目标是良好的泛化能力。
- 线性回归有正规方程解,也可用梯度下降;逻辑回归用 sigmoid + 交叉熵解决二分类。
- KNN 是惰性的近邻投票;决策树以信息增益递归划分,可解释性强。
- 朴素贝叶斯基于条件独立假设,配合拉普拉斯平滑是文本分类基线;SVM 追求最大间隔与核技巧。
- 随机森林(Bagging)与 GBDT(Boosting)是最常用的集成方法。
- K-Means 用分配—更新交替优化发现簇;PCA 沿最大方差方向做线性降维。
- Q-Learning 用时序差分更新状态动作价值,配合 ε-贪心实现试错学习。