简介:本资源是《机器学习》(周志华著,俗称“西瓜书”)配套课程作业的完整代码实现合集,面向高校人工智能、计算机科学及相关专业学生,以及自学机器学习的开发者,旨在辅助理解核心算法原理与动手实践。压缩包共90个文件,包含22个Python脚本(覆盖KMeans、KNN、PCA、AdaBoost、SVM、决策树等关键算法实现)、10个Markdown习题解析文档、34张图表类图片(含算法可视化与数据示例)、6个PNG图示及3个CSV/文本格式西瓜数据集,另有MATLAB实验数据(ex7faces.mat)与Jupyter Notebook等辅助文件,整体大小为11.74MB。已有934人学习下载,资源按章节结构化组织(ch2至ch10),每章含习题说明、代码、图像与数据,目录清晰、即开即用,特别适合对照教材逐章演练、调试算法、验证理论结果,并积累可复用的机器学习工程模板。
1. 西瓜书机器学习课程作业代码实现:不是抄答案,是把周志华《机器学习》第2–16章的“黑匣子”亲手拧开
你手头有一本翻得卷边的《机器学习》(俗称“西瓜书”),书页上密密麻麻记着公式推导、算法流程和“此处略去证明”的叹息;你刚交完第三次作业——用sklearn调LogisticRegression()跑通了鸢尾花分类,但老师问“逻辑回归的决策边界为什么是线性的?梯度下降更新时偏置项b要不要正则化?”,你卡住了。这不是个例。“西瓜书机器学习课程作业代码实现”的真实诉求,从来不是找一份能直接提交的GitHub仓库,而是:在不跳过数学本质的前提下,用最小可行代码复现每一章核心算法的底层逻辑,让公式从纸面落到可调试、可打断点、可改参数的Python里。它面向两类人:一是高校学生正在啃西电/山大/国科大等校机器学习课的期末硬仗,需要把“对数几率回归”“支持向量机”“EM算法”这些名词变成自己写的def predict(self, X):;二是转行者想绕过吴恩达视频里封装好的fit()接口,真正看清SVM的拉格朗日乘子怎么一步步求解、K-means的质心更新为何会陷入局部最优。本文不提供“一键运行”的打包脚本,只给你一套可验证、可拆解、可debug的代码骨架——每行代码都对应西瓜书某一页的公式编号,每个函数名都直译自教材小节标题。
2. 从线性模型开始:用纯NumPy手写对数几率回归,拒绝sklearn黑盒
西瓜书第3章“线性模型”是整本书的基石,而对数几率回归(Logistic Regression)更是所有分类任务的起点。很多作业要求“不使用sklearn,仅用NumPy实现”,但学生常陷入两个误区:一是直接套用scipy.optimize.minimize求解,绕过了梯度下降的迭代过程;二是混淆了“对数几率函数”和“交叉熵损失”,导致梯度推导错误。真正的落地,必须从西瓜书式推导出发——以式(3.18)的对数几率函数为入口,以式(3.27)的梯度更新为终点。
2.1 梯度推导与代码映射:让每一行代码都有公式出处
西瓜书第3章明确给出对数几率回归的损失函数(式3.27):
$$J(\mathbf{w}, b) = -\frac{1}{m}\sum_{i=1}^{m}\left[y^{(i)}\log h_\theta(x^{(i)}) + (1-y^{(i)})\log(1-h_\theta(x^{(i)}))\right]$$
其中 $h_\theta(x) = \sigma(\mathbf{w}^T\mathbf{x} + b)$,$\sigma(z) = \frac{1}{1+e^{-z}}$ 是Sigmoid函数。
关键在于梯度计算。西瓜书虽未显式写出梯度公式,但根据链式法则可得:
$$\frac{\partial J}{\partial \mathbf{w}} = \frac{1}{m}\sum_{i=1}^{m}(h_\theta(x^{(i)}) - y^{(i)})x^{(i)}$$
$$\frac{\partial J}{\partial b} = \frac{1}{m}\sum_{i=1}^{m}(h_\theta(x^{(i)}) - y^{(i)})$$
这段推导必须转化为代码,且不能依赖自动微分。以下是核心训练循环:
import numpy as np class LogisticRegression: def __init__(self, learning_rate=0.01, max_iter=1000, tol=1e-5): self.lr = learning_rate self.max_iter = max_iter self.tol = tol self.w = None self.b = None def sigmoid(self, z): # 防止溢出:当z很大时,exp(-z)≈0;z很小时,exp(-z)很大,直接计算会nan return np.where(z >= 0, 1 / (1 + np.exp(-z)), np.exp(z) / (1 + np.exp(z))) def fit(self, X, y): m, n = X.shape self.w = np.random.normal(0, 0.01, n) # 初始化权重 self.b = 0.0 for i in range(self.max_iter): # 步骤1:计算线性输出 z = X @ w + b (对应西瓜书式3.18中 w^T x + b) z = X @ self.w + self.b # 步骤2:计算预测概率 h = sigmoid(z) (式3.18) h = self.sigmoid(z) # 步骤3:计算梯度(式3.27的导数,即西瓜书隐含的梯度公式) dw = (1/m) * X.T @ (h - y) # ∂J/∂w = (1/m) * X^T @ (h - y) db = (1/m) * np.sum(h - y) # ∂J/∂b = (1/m) * sum(h - y) # 步骤4:更新参数(梯度下降) w_old, b_old = self.w.copy(), self.b self.w -= self.lr * dw self.b -= self.lr * db # 步骤5:收敛判断(避免无限循环) if np.linalg.norm(self.w - w_old) < self.tol and abs(self.b - b_old) < self.tol: print(f"Converged at iteration {i}") break def predict_proba(self, X): return self.sigmoid(X @ self.w + self.b) def predict(self, X): return (self.predict_proba(X) >= 0.5).astype(int)提示:
sigmoid函数的防溢出写法是血泪经验。直接写1/(1+np.exp(-z))在z=-100时会因np.exp(100)溢出为inf,导致结果为nan。np.where分支处理是工业级写法,也是西瓜书习题3.3“讨论sigmoid函数数值稳定性”的实践答案。
2.2 用UCI乳腺癌数据集验证:不只是跑通,要验证数学一致性
作业常要求在真实数据上测试。我们选用UCI乳腺癌威斯康星(诊断)数据集(sklearn.datasets.load_breast_cancer),但不直接用其target,而是手动构造二分类标签并标准化,确保过程透明:
from sklearn.datasets import load_breast_cancer from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler # 加载并预处理数据 data = load_breast_cancer() X, y = data.data, data.target X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) # 标准化:西瓜书强调特征缩放对梯度下降收敛速度的影响(第3章脚注) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) # 训练自定义模型 lr = LogisticRegression(learning_rate=0.1, max_iter=5000) lr.fit(X_train_scaled, y_train) # 验证:对比sklearn结果(仅用于验证,非作业要求) from sklearn.linear_model import LogisticRegression as SklearnLR sklearn_lr = SklearnLR() sklearn_lr.fit(X_train_scaled, y_train) print("Custom LR accuracy:", (lr.predict(X_test_scaled) == y_test).mean()) print("Sklearn LR accuracy:", (sklearn_lr.predict(X_test_scaled) == y_test).mean())这段代码的价值不在准确率数字,而在可验证性:
X_train_scaled的均值应接近0、方差接近1,验证标准化是否生效;lr.w的L2范数应在训练后期稳定,说明梯度下降收敛;- 若将
learning_rate设为0.001,迭代次数需增至10000+才收敛,印证西瓜书第3章“学习率过大易震荡,过小收敛慢”的论述; - 手动计算
X_test_scaled @ lr.w + lr.b,再套sigmoid,结果应与lr.predict_proba(X_test_scaled)完全一致——这是公式到代码的闭环验证。
3. 支持向量机:从拉格朗日对偶到SMO算法,手撕西瓜书第6章核心
西瓜书第6章“支持向量机”是学生公认的难点。作业常要求“实现软间隔SVM”,但多数人直接调用sklearn.svm.SVC,或用cvxopt求解QP问题,却从未理解:为什么原始问题要转成对偶问题?α_i的KKT条件如何约束支持向量?SMO算法为何每次只优化两个α?这一章的代码实现,必须回到西瓜书式(6.9)的对偶问题,并用最朴素的坐标上升法(Coordinate Ascent)模拟SMO逻辑。
3.1 对偶问题构建:从西瓜书式(6.9)到可编程目标函数
西瓜书式(6.9)给出软间隔SVM的对偶问题:
$$\max_{\alpha} \sum_{i=1}^{m}\alpha_i - \frac{1}{2}\sum_{i=1}^{m}\sum_{j=1}^{m}\alpha_i\alpha_jy_iy_j\mathbf{x}_i^T\mathbf{x}j$$
$$\text{s.t. } 0 \leq \alpha_i \leq C,\ \sum{i=1}^{m}\alpha_i y_i = 0$$
这个目标函数可直接翻译为NumPy函数。注意两点:
- 核函数暂用线性核(
K[i,j] = X[i] @ X[j]),后续可替换为RBF; - 约束条件
∑α_i y_i = 0在SMO中通过“选一对α_i, α_j,一个增一个减”来满足。
def svm_objective(alpha, y, K): """计算对偶问题目标函数值(西瓜书式6.9)""" term1 = np.sum(alpha) term2 = 0.5 * np.sum(alpha[:, None] * alpha[None, :] * y[:, None] * y[None, :] * K) return term1 - term2 def compute_kernel(X, kernel='linear'): """计算核矩阵K,支持线性核(默认)和RBF核""" if kernel == 'linear': return X @ X.T elif kernel == 'rbf': # RBF核:K[i,j] = exp(-gamma * ||x_i - x_j||^2) from scipy.spatial.distance import pdist, squareform pairwise_dists = squareform(pdist(X, 'euclidean')) gamma = 1.0 / X.shape[1] # 经验值 return np.exp(-gamma * (pairwise_dists ** 2))3.2 SMO算法骨架:每次只更新两个α,严格遵循KKT条件
SMO(Sequential Minimal Optimization)是西瓜书第6章算法6.1的工程实现。其精髓在于:每次只选择两个拉格朗日乘子α_i, α_j进行优化,在满足约束的前提下解析求解。这避免了QP求解器的开销,也让我们看清支持向量的诞生逻辑。
class SVM: def __init__(self, C=1.0, kernel='linear', max_iter=1000): self.C = C self.kernel = kernel self.max_iter = max_iter self.alpha = None self.b = 0.0 self.X = None self.y = None self.K = None def fit(self, X, y): self.X = X self.y = y.astype(float) # 确保y为+1/-1 self.y = np.where(y == 0, -1, 1) # 二分类标签转为{-1,+1} self.K = compute_kernel(X, self.kernel) m = len(y) self.alpha = np.zeros(m) # 初始化α_i=0 # SMO主循环 for iter_num in range(self.max_iter): alpha_changed = 0 # 遍历所有样本,寻找违反KKT条件的α_i for i in range(m): E_i = self._decision_function(X[i:i+1]) - y[i] # KKT条件:若0<α_i<C,则y_i*f(x_i)=1;若α_i=0,则y_i*f(x_i)≥1;若α_i=C,则y_i*f(x_i)≤1 # 违反条件时,选另一个j进行优化 if (self.alpha[i] < self.C and self.alpha[i] > 0 and abs(y[i] * (self._decision_function(X[i:i+1]) - self.b) - 1) > 1e-3): j = self._select_j(i, m) E_j = self._decision_function(X[j:j+1]) - y[j] # 保存旧值 alpha_i_old, alpha_j_old = self.alpha[i], self.alpha[j] # 计算L,H(西瓜书式6.13) if y[i] != y[j]: L = max(0, self.alpha[j] - self.alpha[i]) H = min(self.C, self.C + self.alpha[j] - self.alpha[i]) else: L = max(0, self.alpha[i] + self.alpha[j] - self.C) H = min(self.C, self.alpha[i] + self.alpha[j]) if L == H: continue # 计算η(西瓜书式6.15) eta = 2 * self.K[i, j] - self.K[i, i] - self.K[j, j] if eta >= 0: continue # 更新α_j(西瓜书式6.14) alpha_j_new = self.alpha[j] - y[j] * (E_i - E_j) / eta alpha_j_new = np.clip(alpha_j_new, L, H) # 更新α_i(西瓜书式6.16) alpha_i_new = self.alpha[i] + y[i] * y[j] * (self.alpha[j] - alpha_j_new) # 更新α self.alpha[j] = alpha_j_new self.alpha[i] = alpha_i_new # 更新b(西瓜书式6.18, 6.19) b1 = self.b - E_i - y[i]*(self.alpha[i]-alpha_i_old)*self.K[i,i] - \ y[j]*(self.alpha[j]-alpha_j_old)*self.K[i,j] b2 = self.b - E_j - y[i]*(self.alpha[i]-alpha_i_old)*self.K[i,j] - \ y[j]*(self.alpha[j]-alpha_j_old)*self.K[j,j] if 0 < self.alpha[i] < self.C: self.b = b1 elif 0 < self.alpha[j] < self.C: self.b = b2 else: self.b = (b1 + b2) / 2 if abs(self.alpha[j] - alpha_j_old) > 1e-5: alpha_changed += 1 if alpha_changed == 0: break # 计算最终b(西瓜书式6.20) sv_indices = np.where((self.alpha > 1e-5) & (self.alpha < self.C - 1e-5))[0] if len(sv_indices) > 0: self.b = np.mean([ y[i] - np.sum(self.alpha * y * self.K[i, :]) for i in sv_indices ]) def _select_j(self, i, m): """随机选择j≠i""" j = i while j == i: j = np.random.randint(0, m) return j def _decision_function(self, X_test): """计算决策函数值 f(x) = Σα_i y_i K(x_i, x) + b""" K_test = self.K[:len(self.X), :len(self.X)] @ X_test.T # 简化版,实际需重算核 # 更严谨做法:对每个x_test重新计算K(x_i, x_test) preds = [] for x in X_test: k_vec = np.array([self._kernel_func(self.X[i], x) for i in range(len(self.X))]) pred = np.sum(self.alpha * self.y * k_vec) + self.b preds.append(pred) return np.array(preds) def _kernel_func(self, x1, x2): if self.kernel == 'linear': return x1 @ x2 elif self.kernel == 'rbf': gamma = 1.0 / len(x1) return np.exp(-gamma * np.sum((x1 - x2) ** 2)) def predict(self, X): return np.sign(self._decision_function(X)).astype(int)这段代码刻意保留了西瓜书的术语:α_i,C,KKT条件,支持向量。运行时你会看到:
- 初始
alpha全为0,随着迭代,部分alpha[i]逐渐增大至C或稳定在(0,C)之间; sv_indices选出的索引,正是西瓜书图6.2中“离超平面最近的几个点”;self.b的计算逻辑,严格对应式(6.20)——用所有支持向量的平均值修正偏置。
4. 决策树与集成方法:ID3、CART与AdaBoost,还原西瓜书第4、7、8章的递归灵魂
西瓜书第4章“决策树”、第7章“集成学习”、第8章“聚类”构成另一条主线。作业常要求“用信息增益实现ID3”“用基尼指数实现CART”“用AdaBoost提升弱分类器”。但学生常把树当成sklearn.tree.DecisionTreeClassifier的参数开关,忘了决策树的本质是递归划分空间,而集成方法的本质是加权组合弱学习器。这一章,我们用纯Python递归实现ID3,并用NumPy向量化实现AdaBoost,让西瓜书的伪代码真正活起来。
4.1 ID3算法:从信息增益到递归建树,拒绝sklearn黑盒
西瓜书第4章算法4.1是ID3的完整描述。核心是信息增益(式4.2):
$$Gain(D,a) = Ent(D) - \sum_{v=1}^{V}\frac{|D^v|}{|D|}Ent(D^v)$$
其中Ent(D)是信息熵(式4.1)。实现的关键在于:递归终止条件必须严格对应西瓜书4.2节的三条规则(纯度达标、属性用尽、样本为空)。
from collections import Counter import numpy as np def entropy(y): """计算信息熵 Ent(D)(西瓜书式4.1)""" if len(y) == 0: return 0 counts = np.bincount(y) probs = counts / len(y) return -np.sum([p * np.log2(p) for p in probs if p > 0]) def information_gain(X, y, feature_idx): """计算特征feature_idx的信息增益 Gain(D,a)(西瓜书式4.2)""" ent_D = entropy(y) unique_vals = np.unique(X[:, feature_idx]) weighted_ent = 0 for val in unique_vals: mask = X[:, feature_idx] == val if np.sum(mask) == 0: continue ent_Dv = entropy(y[mask]) weighted_ent += (np.sum(mask) / len(y)) * ent_Dv return ent_D - weighted_ent class ID3Tree: def __init__(self, max_depth=None, min_samples_split=2): self.max_depth = max_depth self.min_samples_split = min_samples_split self.tree = None def _build_tree(self, X, y, depth=0, used_features=None): if used_features is None: used_features = set() # 终止条件1:所有样本属于同一类(西瓜书4.2节第一条) if len(np.unique(y)) == 1: return {'type': 'leaf', 'class': y[0]} # 终止条件2:没有属性可划分(第二条) if len(used_features) == X.shape[1]: return {'type': 'leaf', 'class': Counter(y).most_common(1)[0][0]} # 终止条件3:样本数少于阈值(第三条) if len(y) < self.min_samples_split or (self.max_depth and depth >= self.max_depth): return {'type': 'leaf', 'class': Counter(y).most_common(1)[0][0]} # 选择最优划分属性(信息增益最大) gains = [information_gain(X, y, i) if i not in used_features else -1 for i in range(X.shape[1])] best_feature = np.argmax(gains) used_features.add(best_feature) # 构建当前节点 node = { 'type': 'split', 'feature': best_feature, 'children': {} } # 递归构建子树 unique_vals = np.unique(X[:, best_feature]) for val in unique_vals: mask = X[:, best_feature] == val if np.sum(mask) == 0: continue child_tree = self._build_tree( X[mask], y[mask], depth + 1, used_features.copy() ) node['children'][val] = child_tree return node def fit(self, X, y): self.tree = self._build_tree(X, y) def _predict_sample(self, x, tree): if tree['type'] == 'leaf': return tree['class'] feature_val = x[tree['feature']] if feature_val not in tree['children']: # 处理测试集中出现训练集未见的值 return Counter([self._predict_sample(x, child) for child in tree['children'].values()]).most_common(1)[0][0] return self._predict_sample(x, tree['children'][feature_val]) def predict(self, X): return np.array([self._predict_sample(x, self.tree) for x in X])注意:
_predict_sample中的缺失值处理,对应西瓜书4.4节“未出现属性值”的讨论。这里采用“投票法”,而非简单报错,更贴近实际作业场景。
4.2 AdaBoost:从加权误差到指数损失,手写西瓜书第8章算法8.1
西瓜书第8章算法8.1是AdaBoost.M1的完整流程。其精妙在于:每一轮调整样本权重,使前一轮分错的样本在下一轮获得更高关注;最终分类器是各轮弱分类器的加权和。我们用决策树桩(单层树)作为基学习器,严格按算法步骤实现:
class AdaBoost: def __init__(self, n_estimators=10): self.n_estimators = n_estimators self.estimators = [] self.alphas = [] def fit(self, X, y): m = len(y) # 初始化样本权重 D = np.full(m, 1 / m) for t in range(self.n_estimators): # 步骤1:基于权重D_t训练弱学习器(这里用决策树桩) stump = ID3Tree(max_depth=1) stump.fit(X, y) self.estimators.append(stump) # 步骤2:计算加权误差 e_t(西瓜书式8.1) pred = stump.predict(X) err = np.sum(D * (pred != y)) # 步骤3:计算学习器权重 α_t(西瓜书式8.2) if err == 0: alpha = float('inf') else: alpha = 0.5 * np.log((1 - err) / err) self.alphas.append(alpha) # 步骤4:更新样本权重 D_{t+1}(西瓜书式8.3) D = D * np.exp(-alpha * y * pred) D = D / np.sum(D) # 归一化 # 最终分类器:sign(Σα_t * h_t(x)) def predict(self, X): weak_preds = np.array([est.predict(X) for est in self.estimators]) # 将预测值转为{-1,+1} weak_preds = np.where(weak_preds == 0, -1, 1) weighted_sum = np.sum(np.array(self.alphas)[:, None] * weak_preds, axis=0) return np.where(weighted_sum >= 0, 1, 0)这段代码的魔力在于:
D数组随轮次变化,可视化后能看到错分样本的权重像涟漪一样扩散;alpha值在早期较大(弱学习器效果好),后期变小(弱学习器趋近随机),印证西瓜书“AdaBoost对噪声敏感”的结论;weak_preds的维度是(n_estimators, n_samples),weighted_sum是向量化加权,避免Python循环——这是工程与理论的平衡点。
5. 避坑指南:西瓜书作业代码实现的5个致命陷阱与血泪解法
写西瓜书作业代码,最大的风险不是写不出来,而是写出来却无法验证、无法调试、无法复现。以下是我带过三届机器学习课程、批改过2000+份作业后总结的5个高频翻车点,每一条都对应真实作业场景和调试日志。
5.1 现象:逻辑回归训练时loss不下降,甚至发散
原因:学习率过大,或特征未标准化。西瓜书第3章明确指出:“若各特征尺度差异很大,梯度下降会沿锯齿路径缓慢收敛,甚至发散。” 但学生常忽略StandardScaler,或误用MinMaxScaler(后者不中心化,仍会导致偏置项b震荡)。
解决:
- 强制在
fit前添加X = (X - X.mean(axis=0)) / X.std(axis=0); - 学习率从0.001起步,观察loss曲线:若前10步loss突降后平稳,说明合适;若第一步loss就
nan,立即降为0.0001; - 打印
np.linalg.norm(X),确认特征均值接近0、标准差接近1。
5.2 现象:SVM支持向量数量远超预期(如100个样本选出80个SV)
原因:C值设置过大(如C=1000),导致软间隔退化为硬间隔,所有靠近边界的点都被强制成为支持向量。西瓜书第6章强调:“C控制对误分类的惩罚,C越大容错越小。” 但作业常给定C=1,学生却自行调高。
解决:
- 用网格搜索
C:[0.01, 0.1, 1, 10, 100],选验证集准确率最高且SV数最少的C; - 监控
np.sum((alpha > 1e-5) & (alpha < C - 1e-5)),理想SV数应占总样本10%~30%; - 若SV数=样本数,必是
C过大或数据线性不可分,需换核函数。
5.3 现象:ID3树深度只有1层,或生成空树
原因:信息增益计算错误。常见有三:①entropy函数未处理p=0导致log(0);②information_gain中weighted_ent未加权求和;③ 特征离散化错误(如将连续特征直接==比较)。西瓜书第4章习题4.3专门讨论离散化问题。
解决:
entropy中加if p > 0保护;information_gain中weighted_ent必须是Σ(|D^v|/|D|)*Ent(D^v),不能漏掉权重;- 对连续特征,先排序、取中位数切分,再计算增益——这是西瓜书4.2节“连续属性离散化”的标准做法。
5.4 现象:AdaBoost训练几轮后alpha变为inf,后续权重爆炸
原因:某轮弱学习器在加权数据上完全正确(err=0),导致log(1/0)。西瓜书算法8.1的“若e_t=0则退出”是理论假设,实际中因浮点精度或数据巧合会发生。
解决:
- 在
err计算后加err = max(err, 1e-10),避免除零; - 或改用
alpha = 0.5 * np.log((1 - err + 1e-10) / (err + 1e-10)); - 更鲁棒的做法:当
err < 0.01时,提前终止训练,因后续提升已无意义。
5.5 现象:K-means聚类结果每次运行都不一样
原因:初始质心随机,而西瓜书第9章算法9.1未指定初始化策略。学生常直接np.random.rand(k, n_features),导致结果不可复现。
解决:
- 固定随机种子:
np.random.seed(42); - 采用西瓜书推荐的
k-means++初始化:先随机选1个点,后续每轮按距离平方概率选新质心; - 作业中若要求“多次运行取最优”,需记录每次SSE(式9.11),选SSE最小的一次——这才是西瓜书“避免局部最优”的实践。
6. 验证与进阶:用西瓜书原题数据集做端到端测试,建立你的作业可信度锚点
写完代码只是开始,验证才是西瓜书作业的灵魂。我坚持一个原则:任何算法实现,必须用西瓜书原文提到的数据集或习题数据,跑通书中给出的数值结果。这不是为了凑数,而是建立“代码-公式-结果”的三角验证闭环。下面以西瓜书第5章“神经网络”的习题5.5为例,展示如何用“异或(XOR)数据集”验证你手写的BP网络。
6.1 西瓜书习题5.5:用BP网络解决XOR问题,复现书中表格数值
西瓜书习题5.5给出XOR数据集:
| x1 | x2 | y |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
并要求:“试编程实现标准BP算法,设置学习率为0.1,隐层节点数为3,训练1000轮,记录每轮的均方误差(MSE)。” 书中表5.1给出了第1、10、100、1000轮的MSE值(0.2500, 0.1502, 0.0521, 0.0042)。你的代码必须复现这些数字,误差不超过±0.0001。
# XOR数据集(西瓜书习题5.5) X_xor = np.array([[0,0], [0,1], [1,0], [1,1]]) y_xor = np.array([0, 1, 1, 0]) # BP网络实现(简化版,仅1隐层) class BPNet: def __init__(self, input_size, hidden_size, output_size, lr=0.1): self.W1 = np.random.normal(0, 0.1, (input_size, hidden_size)) self.b1 = np.zeros(hidden_size) self.W2 = np.random.normal(0, 0.1, (hidden_size, output_size)) self.b2 = np.zeros(output_size) self.lr = lr def sigmoid(self, z): return 1 / (1 + np.exp(-np.clip(z, -500, 500))) # 防溢出 def forward(self, X): self.z1 = X @ self.W1 + self.b1 self.a1 = self.sigmoid(self.z1) self.z2 = self.a1 @ self.W2 + self.b2 self.a2 = self.sigmoid(self.z2) return self.a2 def backward(self, X, y): m = len(y) # 输出层梯度 dz2 = self.a2 - y.reshape(-1,1) dW2 = (1/m) * self.a1.T @ dz2 db2 = (1/m) * np.sum(dz2, axis=0) # 隐层梯度 da1 = dz2 @ self.W2.T dz1 = da1 * self.a1 * (1 - self.a1) # sigmoid导数 dW1 = (1/m) * X.T @ dz1 db1 = (1/m) * np.sum(dz1, axis=0) # 更新 self.W2 -= self.lr * dW2 self.b2 -= self.lr * db2 self.W1 -= self.lr * dW1 self.b1 -= self.lr * db1 def train(self, X, y, epochs=1000): mse_history = [] for epoch in range(epochs): pred = self.forward(X) mse = np.mean((pred.flatten() - y) ** 2) mse_history.append(mse) self.backward(X, y) if epoch in [0, 9, 99, 99 <p> <a href="https://download.csdn.net/download/qq_47888212/74791917" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>