简介:一套面向机器学习初学者的入门代码包,覆盖 BP 神经网络(MLP)、KNN 回归、SVM 超平面、决策树(企鹅数据集)、朴素贝叶斯与逻辑回归等常用模型,适合正在学习分类与回归基础、希望结合代码理解算法原理的数据科学初学者。资源共 14 个文件,主体为 13 个 Python 脚本,并附带 1 个 penguins_raw.csv 数据集,便于直接运行决策树等分类示例;整个压缩包仅 24KB,轻量易下载,可快速对照源码练习。目前已有 343 人学习。代码按模型单独组织,每个脚本聚焦一个算法,并包含 Iris 与企鹅等经典数据集的应用,可帮助读者直观看到 KNN、逻辑回归、SVM 软间隔及朴素贝叶斯在真实数据上的处理方式。通过动手运行这些示例,能逐步建立对模型训练、参数选择与分类/回归流程的初步认知,为后续深入机器学习打下基础。
1. 一套能直接跑的机器学习入门代码,为什么值得你花一个晚上过一遍
刚接触机器学习的人,最容易卡住的不是算法原理,而是“书上讲得头头是道,打开 IDE 却不知道第一行 import 写什么”。这份标题里罗列的代码,恰好覆盖了入门阶段最常用的六类模型——BP 神经网络(MLP)、KNN 回归、SVM 超平面、决策树、朴素贝叶斯、逻辑回归,并且自带数据集(企鹅数据集),属于打开就能跑、跑完能看效果的那类资源。你不需要先啃完一本 500 页的教材,也不需要自己造数据,照着代码把每个模型的 fit、predict、score 走一遍,就对“机器学习到底在干什么”有了体感。它适合两类人:一是刚上完网课、想动手验证一遍理论的初学者;二是已经会调 sklearn、但想回过头把模型背后的决策边界和参数含义梳理清楚的从业者。
2. 先把地基打好:环境、数据与训练/测试划分
2.1 最小依赖环境:只需要 pandas、numpy、matplotlib 和 scikit-learn
这份代码不像深度学习框架那样需要 GPU 和 CUDA,它跑在最常见的科学计算栈上。建议用 Python 3.9 以上的版本,装一个虚拟环境再开始,避免把系统 Python 搞乱。创建环境并安装依赖的命令如下(Windows 用户把source activate换成conda activate即可)。
python -m venv ml_env # Windows: ml_env\Scripts\activate ; macOS/Linux: source ml_env/bin/activate pip install scikit-learn pandas numpy matplotlib seabornscikit-learn是主角,六个模型全部由它提供;seaborn用来加载企鹅数据集(sns.load_dataset("penguins")),比手动下 CSV 省事很多;matplotlib只用来画决策边界和损失曲线,没有它不影响训练。
建议跟着做的时候把版本锁定一下:pip install scikit-learn==1.3.* pandas==2.0.*,因为新版 sklearn 对SVC的某些参数默认值有调整,老教程的代码未必兼容。
2.2 加载企鹅数据集并做数据清洗
企鹅数据集(Palmer Penguins)是 iris 鸢尾花数据集的现代替代品,包含 344 条样本、7 个特征(喙长、喙深、鳍长、体重、性别、岛屿、物种)。它比 iris 多了真实数据里常见的脏东西——缺失值、分类文本、量纲差异,正好用来练手数据预处理。
import pandas as pd import seaborn as sns from sklearn.model_selection import train_test_split # 加载数据集 df = sns.load_dataset("penguins") print(df.head()) # 删除缺失值(344条里缺失约10条,直接drop不影响大局) df = df.dropna().reset_index(drop=True) # 把分类文本转成数值:物种、岛屿、性别 df["species"] = df["species"].astype("category").cat.codes df["island"] = df["island"].astype("category").cat.codes df["sex"] = df["sex"].astype("category").cat.codes # 特征列与标签列 feature_cols = ["bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g", "island", "sex"] X = df[feature_cols] y = df["species"] # 训练/测试划分:7:3,固定随机种子 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42, stratify=y) print(X_train.shape, X_test.shape)这里的代码做了三件关键事:丢弃缺失样本、把文本标签转成整数、按物种分层划分训练集和测试集。stratify=y这一行容易被新手忽略,但它保证训练集和测试集里三个物种的比例一致,避免某一类物种全被分到测试集导致模型“没见过”这类样本。
2.3 为什么不先做特征缩放?——留到每个模型自己决定
很多入门代码喜欢一上来就StandardScaler一把梭,但这不是所有模型都需要的。决策树和朴素贝叶斯对特征缩放不敏感,KNN、SVM、MLP 和逻辑回归则对量纲敏感。这份代码的做法是把缩放器放到每个模型的 Pipeline 里,而不是全局处理,这样你能直观看到“哪个模型换了数据尺度后结果变好”。我的建议是:KNN、SVM、MLP 必须缩放,逻辑回归建议缩放,决策树和朴素贝叶斯无所谓。后面每个模型小节里,我会注明是否在代码前加了缩放。
3. 三个最容易理解的模型:逻辑回归、KNN 回归与朴素贝叶斯
3.1 逻辑回归:拿它当分类的基线,别指望它拟合非线性边界
逻辑回归虽然名字里带“回归”,实际是个线性分类器。它做的事情是:把特征的加权和扔进 sigmoid 函数,输出一个 0 到 1 之间的概率。在企鹅数据上,它相当于画三条直线(多分类是一对多),把三个物种分开。对于入门来说,它是“跑通 sklearn 流程”成本最低的模型——没有超参数要调,默认值就能出结果。
from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression # 标准缩放 + 逻辑回归,max_iter 调高避免收敛警告 pipe_lr = make_pipeline( StandardScaler(), LogisticRegression(max_iter=2000, random_state=42) ) pipe_lr.fit(X_train, y_train) print("Logistic Regression Test Acc: {:.4f}".format(pipe_lr.score(X_test, y_test)))StandardScaler()对每个特征做零均值单位方差处理,因为逻辑回归用梯度下降求解,特征量纲差太大会让收敛变慢甚至不收敛;max_iter=2000是新手必踩的坑,默认 100 在部分数据上会报 “ConvergenceWarning”,调大不代表模型变好,只是给了优化器更多迭代机会;- 输出准确率约为 0.97(随随机种子波动),这个分数可以作为其他模型的参照线——如果一个模型连逻辑回归都打不过,那多半是特征工程或数据预处理出了问题。
实际项目里,逻辑回归的地位不低:金融风控、医疗诊断这类看重可解释性的场景,逻辑回归的系数直接告诉你“体重每增加一单位,属于某个物种的概率变化多少”。但其线性本质意味着它处理不了异或类问题,遇到非线性边界要立刻想到换 SVM 或决策树。
3.2 KNN 回归:不需要训练的模型,调 k 值就是全部学问
K-近邻(KNN)的逻辑极其朴素:预测一个样本时,找它周围最近的 k 个样本,回归任务取这 k 个样本标签的平均值,分类任务取投票。它没有显式的训练过程,所谓的“训练”只是把数据存起来。这份代码里做的是 KNN 回归,数据显示的是企鹅的体重预测问题——用喙长、喙深、鳍长等特征预测body_mass_g。
from sklearn.neighbors import KNeighborsRegressor from sklearn.metrics import mean_squared_error # KNN回归:输入特征去掉体重列,标签设为体重 X_knn = df[["bill_length_mm", "bill_depth_mm", "flipper_length_mm", "island", "sex"]] y_knn = df["body_mass_g"] Xk_train, Xk_test, yk_train, yk_test = train_test_split(X_knn, y_knn, test_size=0.3, random_state=42) # 先试 k=5,缩放后效果更好 pipe_knn = make_pipeline( StandardScaler(), KNeighborsRegressor(n_neighbors=5, weights="distance") ) pipe_knn.fit(Xk_train, yk_train) yk_pred = pipe_knn.predict(Xk_test) print("KNN RMSE: {:.2f} g".format(mean_squared_error(yk_test, yk_pred, squared=False)))n_neighbors=5是经验默认值,但最佳 k 需要暴力搜索:从 1 到 30 循环,画出 k 与 RMSE 的关系曲线,选曲线的“肘部”;weights="distance"意味着离预测点越近的邻居权重越大,比默认的"uniform"(等权重)在多数回归任务上更平滑、更准;- RMSE 在 250g 到 350g 之间是合理水平,企鹅体重的标准差约 800g,说明模型误差控制了单位数的量级。
做 KNN 时最容易翻车的点是:用未缩放的特征直接算欧氏距离。bill_length_mm的取值范围是 30~60,body_mass_g是 2700~6300,如果不缩放,距离几乎完全被体重这一个特征主导,其他特征形同虚设。缩放之后每个特征对距离的贡献才均等。
3.3 朴素贝叶斯:假设特征独立的分类器,最适合文本但也能做表格
朴素贝叶斯基于贝叶斯定理,加上“特征之间相互独立”的强假设。在企鹅数据上,它假设喙长、喙深、鳍长等特征在给定物种的条件下互不相关——这个假设在真实数据里几乎不成立,但它的分类效果经常出人意料地好,尤其在文本分类(垃圾邮件过滤)领域。它训练时只需要统计每个特征在每个类别下的均值和方差,因此训练极快、所需数据量小。
from sklearn.naive_bayes import GaussianNB from sklearn.metrics import accuracy_score # 高斯朴素贝叶斯:连续特征默认假设服从正态分布 gnb = GaussianNB() gnb.fit(X_train, y_train) y_pred = gnb.predict(X_test) print("Naive Bayes Test Acc: {:.4f}".format(accuracy_score(y_test, y_pred)))GaussianNB()是 sklearn 里最省心的模型,不需要任何参数调优,fit 完就能用;- 朴素贝叶斯极少出现“过拟合”问题,因为它的模型复杂度低(每个特征只学一个高斯分布);
- 在企鹅数据上准确率略低于逻辑回归(约 0.95),原因正是特征独立性假设不成立——比如喙长和体重明显正相关,这被模型忽略了。
一个反直觉的结论是:即便条件独立假设被严重违反,分类结果依然可用,因为分类只需比较后验概率的大小,而不是预测精确概率。但当特征之间相关性极强时(比如同一个信息被拆成多个特征),朴素贝叶斯会重复计算这个信息,导致置信度虚高。在真实项目里,我一般用它做“快速基线”,看看数据是否线性可分,再决定是否上更复杂的模型。
3.4 调参经验:KNN 的 k 不是越大越好
很多人误以为 k 越大模型越稳定,实际不一定。k 太大时,远距离的邻居被拉进来,模型变得过于平滑,丢失局部结构;k 太小(比如 1),模型对单个噪声点极度敏感。经验规律是:k 大约等于训练样本数的平方根(这里约 sqrt(240)≈15),但要用验证集去试。另一个参数是距离度量,默认的minkowski(p=2 即欧氏距离)适合连续特征,如果特征是高维稀疏向量(比如 one-hot 编码),改用cosine相似度往往更好。
4. 三个“画边界”的模型:SVM 超平面、决策树与 BP 神经网络
4.1 SVM 超平面:核函数是它的灵魂,C 与 gamma 是两个命门
支持向量机(SVM)做的事是:在特征空间中找一个超平面,让不同类别的样本间隔(margin)最大化。所谓“超平面”,在二维里是一条线,三维里是一个面,高维里就是那个“平面”。这份代码用线性核演示超平面的可视化,再切换到 RBF 核演示非线性分类能力。SVM 的核心优势在于:即便原始特征不可分,通过核函数映射到高维空间后可能变得线性可分。
from sklearn.svm import SVC from sklearn.inspection import DecisionBoundaryDisplay import matplotlib.pyplot as plt # 只取两个特征(鳍长和喙深)便于画二维决策边界 X_svm = df[["flipper_length_mm", "bill_depth_mm"]] y_svm = df["species"] Xs_train, Xs_test, ys_train, ys_test = train_test_split(X_svm, y_svm, test_size=0.3, random_state=42) # RBF核SVM,C=1.0, gamma='scale' 是sklearn的默认值,先不动 pipe_svm = make_pipeline( StandardScaler(), SVC(kernel="rbf", C=1.0, gamma="scale", random_state=42) ) pipe_svm.fit(Xs_train, ys_train) print("SVM Test Acc (2 features): {:.4f}".format(pipe_svm.score(Xs_test, ys_test))) # 画决策边界:subplot 里放训练集散点 + 边界 fig, ax = plt.subplots(figsize=(6, 5)) DecisionBoundaryDisplay.from_estimator( pipe_svm, Xs_train, response_method="predict", alpha=0.4, ax=ax ) ax.scatter(Xs_train["flipper_length_mm"], Xs_train["bill_depth_mm"], c=ys_train, edgecolor="k") plt.show()C是误分类惩罚系数:C 越大,模型越努力把训练集分对,边界越曲折,越容易过拟合;C 越小,边界越平滑,可能欠拟合。默认 1.0 是个中庸值;gamma是 RBF 核的带宽参数,控制了“单个训练样本的影响半径”。gamma="scale"让 sklearn 根据特征方差自动计算,gamma=0.1这类手动值需要网格搜索;- 只取两个特征做可视化是常见做法,能让决策边界画在二维平面上看个明白,但代价是精度下降(企鹅数据用两个特征精度也够高就是运气好)。
用 SVM 的典型翻车点是:数据量超过 10 万条时,RBF 核的拟合速度会肉眼可见地变慢,因为核矩阵是 N×N 的。大批量数据请换 LinearSVC(线性核专用)或直接改用随机森林,别和 SVM 死磕。
4.2 决策树:企鹅数据集上的规则提取,比准确率更重要的是可解释性
决策树的训练过程就是递归地选择“哪个特征、哪个阈值能把数据分得最纯”。在企鹅数据上,它第一条分裂规则通常落在喙长或鳍长上,这直接告诉你“喙长的某个阈值是区分物种的关键”。这份代码除了训练模型,还特别适合把训练出的树可视化出来,因为真正的价值不在于预测,而在于生成一条条可读的 if-else 规则。
from sklearn.tree import DecisionTreeClassifier, plot_tree # 限制深度,防止过拟合;限制最小叶节点样本数 clf_tree = DecisionTreeClassifier( max_depth=4, min_samples_leaf=5, random_state=42 ) clf_tree.fit(X_train, y_train) print("Decision Tree Test Acc: {:.4f}".format(clf_tree.score(X_test, y_test))) # 可视化树结构 plt.figure(figsize=(16, 8)) plot_tree( clf_tree, feature_names=feature_cols, class_names=["Adelie", "Chinstrap", "Gentoo"], filled=True, fontsize=9 ) plt.show()max_depth=4限制树的层数,防止它把每个训练样本都包进一个叶节点里(那叫极限过拟合);min_samples_leaf=5确保每个叶节点至少有 5 个样本,噪声数据不会孤立成一个叶子;- 可视化输出是一棵树,每个节点上写着“
bill_length_mm <= 42.35”这种判断条件,这就是模型的可解释性——医生、风控员可以直接看懂规则,而 SVM 的权重系数很难对人解释。
决策树最大的坑在于:它对特征尺度的变化完全免疫(缩放与否不影响树结构),但对数据中的细微扰动极为敏感。训练数据里加一个离群点,根的切分点可能就从 42.35 跳到 41.80,整棵树面目全非。随机森林解决了这个问题(用多棵树投票),但单个决策树的“不稳定”不是 bug,而是它的数学本质。我一般建议,用决策树做分析(要规则),用随机森林做预测(要精度)。
4.3 BP 神经网络(MLP):三层网络调参比你想的少,但看损失曲线是必须的
BP(反向传播)神经网络在三个模型里“最有深度学习味”。它由输入层、隐藏层、输出层组成,每层之间有权重连接,激活函数引入非线性,反向传播计算梯度来更新权重。入门阶段写这个模型,主要不是为了刷精度(在企鹅数据上它不一定打得过 SVM),而是为了理解训练过程的三个核心概念:学习率、损失函数、迭代轮数。
from sklearn.neural_network import MLPClassifier from sklearn.metrics import accuracy_score # MLP:两个隐藏层,每层 16 个神经元,ReLU 激活 mlp = MLPClassifier( hidden_layer_sizes=(16, 16), activation="relu", solver="adam", batch_size=32, learning_rate_init=0.001, max_iter=300, random_state=42 ) mlp.fit(X_train, y_train) y_pred = mlp.predict(X_test) print("MLP Test Acc: {:.4f}".format(accuracy_score(y_test, y_pred))) print("Converged after {} iterations".format(mlp.n_iter_))hidden_layer_sizes=(16, 16)表示两个隐藏层、每层 16 个神经元。这个数字没有魔法,一般从(样本数/特征数)的量级往上试,先小后大,过拟合再加正则化;activation="relu"是当前默认选择,ReLU 解决了深层网络的梯度消失问题,但在隐藏层神经元死掉时(输出恒为 0)要注意调低学习率;learning_rate_init=0.001配合adam优化器是个稳妥组合;max_iter=300表示最大训练轮数,模型不一定真的跑满 300 轮,n_iter_属性会告诉你实际用了多少轮;- MLP 对特征缩放极度敏感——如果不做
StandardScaler,模型很可能在几十轮内梯度爆炸或梯度消失,损失曲线变成一条水平线。
训练 MLP 时最应该看的东西是损失曲线(mlp.loss_curve_)。把它画出来:如果曲线是下降后趋于平缓,说明训练正常;如果曲线震荡不休,说明学习率太大;如果曲线岿然不动,说明学习了前 10 轮梯度就接近 0(通常因为特征未缩放)。这份代码我建议你跑完后手动把learning_rate_init改成 0.1 和 0.0001 各跑一次,对比损失曲线的形态,这个动作比调任何参数都更能建立直觉。
5. 避坑手册:跑这些入门代码时最常见的五类翻车现场
5.1 现象:ConvergenceWarning: Stochastic Optimizer: Maximum iterations (200) reached
原因:逻辑回归或 MLP 默认max_iter不够,梯度下降还没收敛到最优解就被强制停止了。
解决:把max_iter从 200 调到 2000,同时检查特征是否做了标准化。如果调大后仍不收敛,多半是特征量纲差异太大,加上StandardScaler后重跑。这个警告不算硬错误,模型照样出准确率,但那个准确率不是模型真实水平。
5.2 现象:决策树训练集准确率 100%,测试集准确率骤降
原因:树的深度过深,叶节点里只有一个样本,把训练集的噪声也当作规则背下来了。
解决:限制max_depth(从 3 到 6 之间搜索),加min_samples_leaf(5~10)。更本质的做法是直接改用随机森林,用多棵限制深度的树投票,既保留准确率又增强稳定度。看到 100% 训练准确率时不是应该开心,而是应该警惕。
5.3 现象:KNN 在企鹅数据集上预测体重,RMSE 高达 800g(等于瞎猜)
原因:没做特征缩放,body_mass_g的取值范围在 2700~6300,而喙长只有 30~60。计算欧氏距离时,喙长方向的差异被淹没在体重方向里,模型实际上只在“按体重找邻居”。
解决:在 Pipeline 里加StandardScaler(),RMSE 能立刻降到 300g 左右。这是所有基于距离的模型(KNN、SVM-RBF)共有的硬要求,一万次踩坑的人里九千个栽在这。
5.4 现象:seaborn.load_dataset("penguins")报错或返回空表
原因:seaborn 从在线仓库加载数据,网络不稳定时下载失败,或是本机 seaborn 版本太老不认识 penguins 这个新数据集。
解决:检查 seaborn 版本(pip show seaborn),低于 0.11 就升级;网络不行时直接下载 CSV 文件,用pd.read_csv()读本地路径。我习惯把企鹅数据集缓存到本地data/penguins.csv,一劳永逸,免得每次跑代码都依赖网络。
5.5 现象:不同模型在相同数据上跑出的准确率完全一样,换了随机种子又不一样
原因:数据集太小,样本划分的随机性影响太大。random_state只是固定了这次的随机划分,不代表模型稳定性。
解决:做 5 折交叉验证,取均值与标准差再比较。入门代码为了简洁通常只做一次 train/test split,但你心里要清楚:单次划分的数字不能作为结论。至少跑cross_val_score(model, X, y, cv=5)看一眼均值,比单次 accuracy 靠谱得多。
6. 把这些代码用起来的三个进阶动作:交叉验证、模型保存与阈值可视化
你把这六个模型各跑一遍后,下一个自然问题是:“然后呢?” 我建议按下面的顺序做三件事,它们都不需要引入新的库。
第一,把单次train_test_split换成cross_val_score,用 5 折交叉验证重测所有模型,记录每个模型的均值准确率与标准差。你会发现 SVM 与逻辑回归的均值接近,但标准差可能差一倍——这就是“哪个模型更稳”的真实答案。第二,用joblib.dump(model, "model.joblib")保存你选中的模型,再用joblib.load()在新数据上预测,直接体验“训练与部署分离”这个工程概念。第三步,画出每个模型的 ROC 曲线(逻辑回归与朴素贝叶斯有predict_proba,可以用roc_auc_score评估排序能力),你会发现准确率最高的模型,AUC 不一定最高——这个反直觉的体验对理解分类问题的本质很有帮助。
from sklearn.model_selection import cross_val_score import joblib import numpy as np # 交叉验证:同一pipeline,5折,看均值±标准差 cv_scores = cross_val_score(pipe_lr, X, y, cv=5, scoring="accuracy") print("LR 5-Fold CV: {:.4f} ± {:.4f}".format(np.mean(cv_scores), np.std(cv_scores))) # 保存模型:模型+预处理器一起存,部署时不用重新写预处理逻辑 joblib.dump(pipe_lr, "logistic_model.pkl")- 交叉验证的折数(cv=5)是个平衡值,数据多可以升到 10,小数据集 3~5 就够,折数太多单折样本太少,均值方差都会变大;
joblib保存的是“StandardScaler + 逻辑回归”的完整 Pipeline,部署时只需loaded_pipe.predict(新数据),不需要再手动对特征做标准化;- AUC 与准确率的冲突是入门者必须接受的事实:准确率盯着“阈值 0.5 时对多少个”,AUC 盯着“排序能力多强”。在正负样本比例严重失衡的数据上(比如欺诈检测里 99:1),准确率会骗你,AUC 不会。
做这些进阶动作时,另一个建议是:给你的训练脚本加上argparse参数(--model、--cv、--output),用命令行开关控制跑哪个模型、存哪个结果。这一步的价值在于,当你在真实项目里拿到一份全新数据时,不需要改代码,只要换参数就能批量跑完六个基线模型,打印一张对比表格。这一套流程跑顺了,你再去看其他复杂的框架,会发现所有机器学习工程的本质都是“数据清洗 → 模型选择 → 评估 → 保存”,只是规模不同。我见过太多人把精力花在调参和堆模型上,但对“如何规范化地组织这些实验”毫无概念,结果每次项目都从零重写一遍。把上面这套流程固定下来,你会少走很多弯路。希望这些代码和经验,能帮你顺利迈过那道入门的坎。
本文还有配套的精品资源,点击获取