
简介这份PDF教程面向机器学习初学者系统讲解用Python实现K近邻、朴素贝叶斯、逻辑回归、支持向量机、决策树、随机森林、感知机、多层前向网络与卷积神经网络等经典算法内容安排循序渐进。教程从评估问题入手依次涵盖数据预处理、特征工程含主成分分析降维、模型训练与交叉验证、结果检验和进一步优化形成完整的实践闭环。压缩包内共1个PDF文件大小仅35KB内容精炼适合快速导读与离线翻阅。已有2424人学习下载。教程不仅解释算法原理还给出Scikit-learn与Keras库的实际用法并结合PIL、Numpy展示图像数据与数值数据的处理思路通过案例帮助读者理解网格搜索、正则化等调优策略掌握从数据准备到模型部署的完整流程为机器学习入门者节省大量检索与踩坑时间。1. 为什么从零手写一个机器学习算法比直接调库更值接到一个临时需求线上环境不允许装scikit-learn要自己写一个线性回归做预测。很多人都能说出损失函数的样子但真到写梯度下降时矩阵维度、学习率、偏置项这些细节会一下子暴露出来。“手把手教你使用Python实现机器学习算法”这类以PDF手册形式组织的教程就是把公式到可运行代码的每一步拆开让人亲手把算法写出来而不是只当调包侠。我跟着这条思路把环境准备、线性回归、逻辑回归、决策树、聚类和模型验证整个闭环走了一遍这里把过程复述成一份可照着敲的笔记。适合刚学完Python基础、想往机器学习方向深入的人也适合写过业务代码但没系统动手实现过算法的工程师。2. 先把 Python 机器学习环境装到“可复现”手写算法最怕环境不一致。同一段代码换台机器结果对不上原因多半是numpy版本不同、BLAS后端不同或者随机种子没有固定。所以动手写第一个算法前先把工具链锁住。这里的目标不是把python安装教程再写一遍而是保证下面这几个关键库能import、能跑矩阵乘法。2.1 Python 版本、虚拟环境与依赖安装建议使用Python 3.10以上版本。如果你还没装Python先按官方安装包完成基础安装Windows下勾选Add python.exe to PATHLinux系统安装python后记得使用python3命令。然后创建一个干净的虚拟环境python -m venv ml_env # Windows ml_env\Scripts\activate # Linux/macOS source ml_env/bin/activate接下来用pip安装科学计算库python -m pip install --upgrade pip python -m pip install numpy scipy pandas matplotlib scikit-learn jupyter参数说明scipy提供信号处理与线性代数函数pandas负责读取音乐特征表这类结构化数据matplotlib画损失曲线和学习曲线scikit-learn在后续主要承担数据切分、评估、模型对比不承担手写算法的核心计算。如果你遇到下载超时这就是python下载numpy库的最直接方法用国内镜像。python -m pip install numpy -i https://pypi.tuna.tsinghua.edu.cn/simple-i指定Index URL把默认官方源换成清华镜像源。Windows用户在命令行看到“python was not found”时试一下py命令VSCode的python环境配置则是在命令面板里选择ml_env里的解释器再装好Python和Jupyter两个扩展。2.2 检查依赖版本与底层计算性能装完后不要着急写算法先跑一个环境自检脚本# check_env.py import sys import time import numpy as np import scipy import pandas as pd import sklearn import matplotlib print(Python:, sys.version.split()[0]) print(numpy:, np.__version__) print(scipy:, scipy.__version__) print(pandas:, pd.__version__) print(scikit-learn:, sklearn.__version__) print(matplotlib:, matplotlib.__version__) rng np.random.default_rng(42) a rng.normal(size(1024, 1024)) b rng.normal(size(1024, 1024)) t0 time.perf_counter() c a b print(matmul time: %.1f ms % ((time.perf_counter() - t0) * 1000))逻辑说明rng np.random.default_rng(42)创建固定随机源42是机器学习里常用的随机种子更重要的是它能保证每次生成的矩阵一致。矩阵乘法触发numpy底层的BLAS优化如果这台机器打印出的时间在百毫秒以内说明向量化链路是通的。如果出现几十秒甚至卡死需要检查numpy下载的是不是官方wheel以及CPU是否被限制。参数说明sys.version.split()[0]只取Python主版本号size(1024,1024)故意选一个不大不小的规模不会太久又能看出性能差异。后期手写算法时所有能改写成矩阵运算的地方都要用这种向量化写法不要在Python层写双层for循环。2.3 按章节组织“代码公式结果”的笔记形式Jupyter Notebook很适合呈现“手把手教你使用Python实现机器学习算法”的学习路径Markdown单元格放公式代码单元格放实现输出结果直接贴在下面。每个算法一章里面包含“数学目标—代码实现—运行验证”三段结构。下面是一个线性回归章节的Markdown骨架可以直接复制# 线性回归 ## 目标函数 J(theta) 1/(2m) * sum((X theta - y)^2) ## 梯度下降更新 theta : theta - lr/m * X.T (X theta - y)Jupyter官方提供的Notebook保存为.ipynb文件如果需要转成PDF报告用File - Download as - PDF导出即可。这样写代码时记录下来的公式和结果最后就是一份完整的学习笔记。用一个表格收住依赖库的用途。| 库 | 在算法实现中的用途 | | numpy | 矩阵运算、随机抽样、向量化实现梯度 | | pandas | 读取CSV特征、按类别分组统计 | | matplotlib | 绘制损失曲线、学习曲线、聚类散点图 | | scikit-learn | 数据切分、交叉验证、作为手写实现的对比基准 | | jupyter | 分章节保存公式、代码与运行结果 |3. 手写线性回归与逻辑回归把梯度下降落到 Python 代码回归和分类是机器学习算法里最常用的两类任务。线性回归用连续值做预测逻辑回归在sigmoid函数后输出概率两者共享同一个梯度下降框架。先用make_regression生成一份实验数据再手写完整训练过程最后和scikit-learn的现成实现做系数对比。3.1 线性回归矩阵形式、损失函数与手写梯度下降生成数据并切分from sklearn.datasets import make_regression from sklearn.model_selection import train_test_split X, y make_regression(n_samples200, n_features5, noise0.3, random_state42) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42)参数说明n_samples控制样本量n_features是特征维度noise是高斯噪声标准差噪声越大拟合难度越高。train_test_split里的test_size0.2表示留出20%做测试random_state固定切分保证多次运行看到相同结果。手写线性回归class LinearRegressionGD: 手写线性回归批量梯度下降版 def __init__(self, lr0.01, epochs1000): self.lr lr self.epochs epochs self.theta None self.loss_history [] def fit(self, X, y): # 左侧加一列1对应截距项 X np.hstack([np.ones((X.shape[0], 1)), X]) m X.shape[0] self.theta np.zeros(X.shape[1]) for _ in range(self.epochs): y_pred X self.theta error y_pred - y # 梯度为 (1/m) * X^T error grad (1 / m) * (X.T error) self.theta - self.lr * grad loss 0.5 * np.mean(error ** 2) self.loss_history.append(loss) return self def predict(self, X): X np.hstack([np.ones((X.shape[0], 1)), X]) return X self.theta逻辑说明训练循环里每次做一步批量梯度下降batch就是全部训练样本。grad等于(1/m)乘X转置再乘残差向量这一步来自损失函数对theta的偏导。theta更新是用学习率乘负梯度所以代码里写成theta - lr * grad。loss_history记录每次迭代后的均方误差方便后面画曲线判断收敛情况。参数说明lr决定每步走多远lr0.01是经验起步值loss变nan说明太大降到0.001再试loss平缓下降但很慢说明太小或epochs不够。epochs设置太大没有坏处只是训练变长实践中可以配合早停当loss连续多次变化小于1e-6就退出循环。手写版对特征量纲敏感from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_s scaler.fit_transform(X_train) X_test_s scaler.transform(X_test)fit_transform在训练集上计算均值和方差transform在测试集上只使用已经学到的参数防止信息泄漏。如果不做这一步age、income这类特征的取值范围相差几个数量级梯度下降会来回震荡。正规方程不用特征缩放但那个方法要解X^T X的逆矩阵数据量超过几万行时计算代价很大。两种方法的取舍用一张表说明。| 对比项 | 梯度下降 | 正规方程 | | 计算过程 | 迭代逼近 | 直接解线性方程组 | | 适合样本量 | 大样本也有方案 | 万行以内可用 | | 需要特征缩放 | 建议 | 不需要 | | 正则项扩展 | 容易 | 也容易但实现稍绕 | | 数值问题 | 学习率调不好会发散 | 特征共线时X^T X不可逆 |3.2 逻辑回归sigmoid、交叉熵与二分类实现逻辑回归把线性组合z X theta先经过sigmoid映射到(0,1)输出代表正样本概率。实现如下def sigmoid(z): z np.clip(z, -500, 500) return 1.0 / (1.0 np.exp(-z)) class LogisticRegressionGD: def __init__(self, lr0.05, epochs200): self.lr lr self.epochs epochs self.theta None def fit(self, X, y): X np.hstack([np.ones((X.shape[0], 1)), X]) m X.shape[0] self.theta np.zeros(X.shape[1]) for _ in range(self.epochs): p sigmoid(X self.theta) # 交叉熵损失对theta的梯度 grad (1 / m) * (X.T (p - y)) self.theta - self.lr * grad return self def predict_proba(self, X): X np.hstack([np.ones((X.shape[0], 1)), X]) return sigmoid(X self.theta) def predict(self, X, threshold0.5): return (self.predict_proba(X) threshold).astype(int)逻辑说明sigmoid里的np.clip把z限制在[-500,500]避免np.exp(-z)溢出为inf如果数据本身在正常范围clip不会改变输出。梯度表达式是(1/m)X^T(p-y)p是预测概率y是0/1标签这个形式比线性回归的梯度更简单但它是从对数似然推出来的。threshold参数值得多说一句。默认0.5适合类别均衡场景当正样本很少、业务要求尽可能找出正样本时把threshold调到0.3或0.4能提高召回率代价是误报变多。这是机器学习算法实现里常常被忽略的“决策阈值”自由度。3.3 用手写实现对比 scikit-learn 的数值结果现在用一份二分类数据验证手写逻辑回归的正确性from sklearn.datasets import make_classification from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score X, y make_classification(n_samples500, n_features10, n_informative6, random_state7) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state7) my_logit LogisticRegressionGD(lr0.1, epochs500).fit(X_train, y_train) sk_logit LogisticRegression(max_iter500, C1e6).fit(X_train, y_train) print(my acc:, accuracy_score(y_test, my_logit.predict(X_test))) print(sk acc:, accuracy_score(y_test, sk_logit.predict(X_test))) print(coef diff:, np.abs(my_logit.theta[1:] - sk_logit.coef_[0]).max())参数说明make_classification的n_informative控制与类别真正相关的特征数量其余特征即使存在信息量也不高。LogisticRegression里的C是正则强度的倒数C1e6把L2正则压到很低让sklearn接近“纯”梯度下降版本。coef diff取所有系数差的最大值如果小于1e-2说明手写推导和实现方向正确如果差距很大先检查学习率是否偏低、epochs是否够用。到这里线性回归和逻辑回归的手写路径已经走通。下一章进入决策树与随机森林它们的实现思路完全不同但同样遵循“先手写核心再用库来验证”的原则。4. 手写决策树与随机森林从信息熵到特征重要性树模型不需要特征缩放也能捕捉特征之间的非线性关系在表格数据上表现一直很强。这一章用numpy实现决策树的分裂逻辑再通过Bootstrap抽样实现一个迷你随机森林。后面做音乐风格分类时树模型可以用来筛选哪些音频特征更关键。4.1 信息熵与基尼指数怎么算分裂时需要一个度量来衡量节点纯度。信息熵和基尼指数是两种最常用的选择。def entropy(labels): _, counts np.unique(labels, return_countsTrue) p counts / counts.sum() return -np.sum(p * np.log2(p 1e-12)) def gini(labels): _, counts np.unique(labels, return_countsTrue) p counts / counts.sum() return 1 - np.sum(p ** 2)逻辑说明np.unique返回标签集合和出现次数p是每个类别的占比。熵在p接近0或1时趋近0所有类别均匀时最大。基尼指数等于“随机抽取两个样本类别不同的概率”值越小纯度越高。代码里的p1e-12是极小偏移量避免p0时log2(0)报错。选择哪个指标看场景。| 指标 | 特点 | 常见用途 | | 信息增益 | 偏好取值较多的特征 | ID3 | | 信息增益比 | 对多取值有修正 | C4.5 | | 基尼指数 | 计算快倾向二分裂 | CART |手写实现里换成另一个指标只需替换gini函数调用不需要改动树结构代码。一般分类实验用基尼指数速度更快结果差别很小。4.2 递归构建决策树的实现细节决策树从根节点开始找到让加权基尼指数下降最大的特征和阈值然后递归分裂。核心代码如下class DecisionTree: def __init__(self, max_depth3, min_samples_leaf1): self.max_depth max_depth self.min_samples_leaf min_samples_leaf self.tree None def fit(self, X, y): self.tree self._build(X, y, depth0) return self def _best_split(self, X, y): best_gain -1 best_feat, best_thr None, None parent_gini gini(y) n len(y) for f in range(X.shape[1]): values np.unique(X[:, f]) for i in range(len(values) - 1): thr (values[i] values[i 1]) / 2 left_mask X[:, f] thr if left_mask.sum() self.min_samples_leaf or (~left_mask).sum() self.min_samples_leaf: continue left_part y[left_mask] right_part y[~left_mask] weighted_gini (len(left_part) / n) * gini(left_part) (len(right_part) / n) * gini(right_part) gain parent_gini - weighted_gini if gain best_gain: best_gain gain best_feat f best_thr thr return best_feat, best_thr def _build(self, X, y, depth): # 停止条件达到深度、只剩一个类别、样本太少 if depth self.max_depth or len(np.unique(y)) 1 or len(y) 2 * self.min_samples_leaf: return {type: leaf, pred: np.bincount(y).argmax()} feat, thr self._best_split(X, y) if feat is None: return {type: leaf, pred: np.bincount(y).argmax()} left_mask X[:, feat] thr return { type: node, feature: feat, threshold: thr, left: self._build(X[left_mask], y[left_mask], depth 1), right: self._build(X[~left_mask], y[~left_mask], depth 1), } def predict_one(self, node, x): if node[type] leaf: return node[pred] if x[node[feature]] node[threshold]: return self.predict_one(node[left], x) return self.predict_one(node[right], x) def predict(self, X): return np.array([self.predict_one(self.tree, x) for x in X])逻辑说明_best_split对每个特征的每个相邻取值中点计算阈值这是一种简单但较慢的做法。min_samples_leaf限制左右子节点最少样本数可以避免叶子节点只落到一两个样本。max_depth是最大深度设太深时训练集准确率很高但验证集反而下降这是树模型过拟合的典型信号。predict_one顺着节点递归走走到叶子就返回该叶子的多数类别。注意np.unique(X[:, f])对连续特征会得到大量唯一值嵌套循环在特征多的时候很慢。实践里可以先对特征做分位数离散化或者随机挑一部分候选阈值。这段实现的重点在教学不是性能。一个常用的调参经验先用max_depth3跑通再慢慢加深同时把min_samples_leaf从5开始试。这个组合对多数表格数据都够用。4.3 随机森林用样本扰动与特征扰动提升泛化随机森林把决策树作为基学习器每棵树用Bootstrap抽样后的数据训练投票得到最终结果。对“基于机器学习的音乐风格分类算法设计与实现”这类任务树模型可以回答“哪个音频特征更重要”因此经常被用在特征筛选阶段。class RandomForestManual: def __init__(self, n_estimators50, max_depth5, min_samples_leaf2, random_state42): self.n_estimators n_estimators self.max_depth max_depth self.min_samples_leaf min_samples_leaf self.random_state random_state self.trees [] def fit(self, X, y): rng np.random.default_rng(self.random_state) n X.shape[0] for _ in range(self.n_estimators): idx rng.integers(0, n, n) tree DecisionTree(max_depthself.max_depth, min_samples_leafself.min_samples_leaf) tree.fit(X[idx], y[idx]) self.trees.append(tree) return self def predict(self, X): preds np.column_stack([tree.predict(X) for tree in self.trees]) from scipy import stats return stats.mode(preds, axis1).mode逻辑说明rng.integers(0, n, n)做有放回抽样每棵树使用的训练集平均包含63.2%的原始样本剩下约36.8%可以留作袋外验证。predict把所有树的预测结果放到一个二维数组里然后用stats.mode按行取众数这就是多数投票。特征扰动这里没有完整展开但一定要知道随机森林和决策树的关键区别除了数据抽样还有每次分裂时只随机选一部分特征参与比较。这样做的目的是降低树与树之间的相关性树越“独立”投票结果的方差越小模型就越可靠。sklearn的RandomForestClassifier里max_features参数默认是sqrt就是这个逻辑。树之间天然独立训练时可以用python多进程或joblib并行加速。要注意每个进程的随机源要独立否则多棵树可能拿到完全相同的抽样序列等于白训练。并行数量先设4到8数据量小的时候反而更慢。用sklearn的RandomForestClassifier对照验证一次特征重要性from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier(n_estimators100, max_depth5, random_state42) rf.fit(X_train, y_train) print(rf.feature_importances_)参数说明feature_importances_来自节点分裂带来的不纯度下降总和数值只表示相对重要性不代表某一特征与类别的相关系数。若特征维度差异很大建议先做分箱或编码因为高基数特征在树模型中更容易被选中。5. 用交叉验证、学习曲线和模型导出把算法“做实”手写算法跑通后下一步不是急着调参而是建立一套验证方法。只看训练集准确率很容易被过拟合骗过去模型泛化能力要用交叉验证来判断。5.1 用分层K折验证分类模型对分类任务推荐StratifiedKFold而不是普通KFold因为它的每一折都保持原始类别比例。下面的示例假设你已经通过StandardScaler得到了特征矩阵X_scaled和标签y。from sklearn.model_selection import StratifiedKFold, cross_val_score from sklearn.ensemble import RandomForestClassifier cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) scores cross_val_score( RandomForestClassifier(n_estimators50, random_state42), X_scaled, y, cvcv, scoringaccuracy ) print(mean%.4f std%.4f % (scores.mean(), scores.std()))n_splits5表示5折每一折用4/5数据训练、1/5验证。shuffleTrue打乱样本顺序防止原数据按类别排好队导致某些折没有某类样本。如果scores.std()很大说明模型在不同子集上的结果波动明显需要进一步处理特征或调参。5.2 用学习曲线区分欠拟合和过拟合学习曲线把训练集大小从10%逐步扩展到100%绘制训练分数和验证分数的变化。import matplotlib.pyplot as plt from sklearn.model_selection import learning_curve sizes, train_scores, val_scores learning_curve( RandomForestClassifier(n_estimators50, random_state42), X_scaled, y, cv5, train_sizesnp.linspace(0.1, 1.0, 5) )用matplotlib把train_scores.mean()和val_scores.mean()画成两条曲线。两条线离得远说明过拟合两条线一起偏低说明欠拟合。这个环节是Python数据分析与可视化的典型用法也是调参前的必要检查。5.3 保存模型再用Flet封装成内部小工具比较稳妥的保存方式是joblibimport joblib joblib.dump({model: rf, scaler: scaler}, music_model.pkl) loaded joblib.load(music_model.pkl)参数说明同时保存scaler和rf预测新样本时先transform再predict避免训练和预测两端特征处理不一致。检查模型封装时可以在预测前后分别打印特征列名确认顺序。如果想给同事一个能操作的小窗口Flet是一个轻量选择import flet as ft def main(page: ft.Page): page.title 音乐风格分类 page.add(ft.Text(选择特征文件点击预测)) # 读取新样本 - 标准化 - 模型预测 - 输出风格 ft.app(main)Flet的定位是让你用纯Python写桌面UI适合把算法演示做成交互工具。写完模型后建议把特征提取和预测封装成一个predict函数保证pkl之外不再夹杂其他逻辑。最后一个小技巧先记录好特征列名顺序再导出模型换环境预测结果才不会错位。本文还有配套的精品资源点击获取