从零开始用Python构建你的第一个机器学习模型(附Jupyter Notebook实战)
从零开始用Python构建你的第一个机器学习模型(附Jupyter Notebook实战)
如果你对“机器学习”这个词既感到兴奋又有些望而生畏,觉得它似乎是数据科学家和算法工程师的专属领域,那么这篇文章就是为你准备的。我的目标很简单:让你在接下来的几十分钟里,亲手完成一个完整的机器学习项目,从一堆原始数据开始,最终得到一个能做出预测的模型。整个过程就像搭积木,我们一块一块来,你不需要深厚的数学背景,只需要一点Python基础和对新事物的好奇心。
我们将全程在Jupyter Notebook中进行,这是一个交互式的编程环境,特别适合数据探索和实验。你可以实时看到每一行代码的输出,即时调整,即时反馈。我们会聚焦于最核心的流程:数据清洗、特征工程、模型训练和结果可视化。更重要的是,我会穿插那些新手最容易踩的“坑”,比如库版本冲突、环境配置的“玄学”问题,以及一些看似微小却能决定成败的数据处理细节。这篇文章适合那些希望快速看到成果、通过实践建立信心的入门者。让我们暂时忘掉复杂的理论,先动手做出点东西来。
1. 环境准备与第一行代码
在开始任何机器学习项目之前,一个稳定、一致的工作环境是成功的基石。很多初学者在第一步就卡住了,不是因为代码难,而是因为环境“有毒”——库版本不兼容、路径错误、依赖缺失。为了避免这些,我强烈建议使用conda或venv来创建独立的Python虚拟环境。
1.1 创建并激活虚拟环境
打开你的终端(Windows上是CMD或PowerShell,macOS/Linux上是Terminal),我们一步步来。
# 使用conda创建环境(如果你安装了Anaconda或Miniconda)
conda create -n ml_starter python=3.9
conda activate ml_starter
# 或者使用Python自带的venv模块
python -m venv ml_starter_env
# 激活环境
# Windows:
ml_starter_env\Scripts\activate
# macOS/Linux:
source ml_starter_env/bin/activate
环境激活后,你的命令行提示符前通常会显示环境名称,如(ml_starter)。这意味着你之后安装的所有包都只在这个“沙箱”里,不会影响系统其他Python项目。
1.2 安装核心库
接下来,安装我们项目所需的库。我们将使用scikit-learn作为机器学习工具箱,pandas处理数据,numpy进行数值计算,matplotlib和seaborn进行可视化。使用pip安装时,强烈建议指定版本号,这是避免依赖冲突最有效的方法。
pip install pandas==1.5.3 numpy==1.24.3 matplotlib==3.7.1 seaborn==0.12.2 scikit-learn==1.3.0 jupyter==1.0.0
安装完成后,可以通过pip list命令检查版本。如果遇到网络问题导致下载缓慢或失败,可以尝试使用国内的镜像源,例如在命令后添加 -i https://pypi.tuna.tsinghua.edu.cn/simple。
1.3 启动Jupyter Notebook并创建第一个单元格
在终端中,确保你处于项目目录下,然后输入:
jupyter notebook
浏览器会自动打开Jupyter界面。点击右上角“New” -> “Python 3”,创建一个新的Notebook。你可以将其重命名为my_first_ml_model.ipynb。
在第一个单元格中,让我们导入所有必要的库,并做一个简单的版本检查。这是一个好习惯,能确保教程中的代码在你的环境中可以复现。
# 导入核心库
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn import datasets, preprocessing, model_selection, metrics
from sklearn.linear_model import LinearRegression
# 设置可视化风格,让图表更好看
sns.set(style="whitegrid")
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
# 打印版本信息
print(f"pandas version: {pd.__version__}")
print(f"scikit-learn version: {sklearn.__version__}")
运行这个单元格(Shift+Enter),如果没有报错,并且输出了版本号,恭喜你,环境搭建成功!你已经跨过了机器学习实践中最令人头疼的一道坎。
2. 理解数据:加载、探索与清洗
没有数据,机器学习就是无米之炊。我们将使用一个经典的数据集:波士顿房价数据集。这个数据集规模适中,特征含义清晰,非常适合教学。在scikit-learn中,它被内置为一个方便加载的数据集。
2.1 加载数据集并转换为DataFrame
scikit-learn加载的数据通常是numpy数组格式。为了更直观地查看和操作,我们将其转换为pandas的DataFrame,这是数据分析的“瑞士军刀”。
# 加载波士顿房价数据集
boston = datasets.load_boston()
# 注意:在较新版本的scikit-learn中,load_boston()已被移除,因为它包含的伦理问题。
# 如果报错,我们可以用另一个数据集替代,比如糖尿病数据集,但为了教程连贯性,我们假设使用旧版本。
# 替代方案:使用加利福尼亚住房数据集
# from sklearn.datasets import fetch_california_housing
# california = fetch_california_housing()
# 将特征数据转换为DataFrame
df_features = pd.DataFrame(boston.data, columns=boston.feature_names)
# 将目标变量(房价)也加入DataFrame
df_target = pd.Series(boston.target, name='MEDV') # MEDV是房价中位数(单位:千美元)
df = pd.concat([df_features, df_target], axis=1)
# 查看数据的前5行
print("数据集前5行:")
display(df.head())
# 查看数据的基本信息:行数、列数、数据类型、非空值数量
print("\n数据集信息:")
df.info()
# 查看数据的统计摘要
print("\n数据统计描述:")
display(df.describe())
运行后,你会看到一个表格,包含诸如CRIM(城镇人均犯罪率)、ZN(住宅用地比例)、INDUS(非零售商业用地比例)、CHAS(查尔斯河虚拟变量)、NOX(氮氧化物浓度)、RM(住宅平均房间数)、AGE(1940年以前建成的自住单位比例)、DIS(到波士顿五个就业中心的加权距离)、RAD(放射状公路可达性指数)、TAX(每万美元财产税率)、PTRATIO(城镇师生比例)、B(黑人比例指数)、LSTAT(低收入人群百分比)和我们的目标MEDV。
注意:在实际项目中,理解每个特征的实际含义至关重要。这决定了你后续的特征工程策略。如果某个特征的含义模糊,预测结果将难以解释。
2.2 数据探索与可视化
在清洗数据之前,我们需要“感受”数据。可视化是发现数据模式、异常值和特征间关系的强大工具。
# 1. 查看目标变量的分布
plt.figure(figsize=(10, 6))
sns.histplot(df['MEDV'], bins=30, kde=True)
plt.title('房价中位数(MEDV)分布')
plt.xlabel('房价(千美元)')
plt.ylabel('频数')
plt.show()
这张直方图能告诉你房价的大致范围、中心趋势以及是否存在多峰分布。接下来,我们看看特征与目标的关系。以RM(房间数)为例,直觉上房间越多,房价应该越高。
# 2. 特征与目标变量的关系散点图
plt.figure(figsize=(10, 6))
sns.scatterplot(x=df['RM'], y=df['MEDV'], alpha=0.6)
plt.title('房间数(RM)与房价(MEDV)的关系')
plt.xlabel('平均房间数')
plt.ylabel('房价(千美元)')
plt.show()
你应该能看到一个明显的正相关趋势。但也要注意,在房间数很多的地方,房价的波动也很大,这可能暗示还有其他因素在起作用。
2.3 数据清洗:处理缺失值与异常值
真实数据很少是完美的。波士顿数据集虽然是“干净”的经典数据集,但我们仍要练习处理数据问题的流程。
检查缺失值:
# 检查每列的缺失值数量
missing_values = df.isnull().sum()
print("缺失值统计:")
print(missing_values[missing_values > 0])
如果存在缺失值,常见的处理策略有:
- 删除: 如果缺失行很少,可以直接删除(
df.dropna())。 - 填充: 用均值、中位数或众数填充(
df.fillna(df.mean())),或者用前后值填充(df.fillna(method='ffill'))。 - 预测: 使用其他特征来预测缺失值(更高级的方法)。
识别和处理异常值: 异常值(Outliers)可能会严重扭曲模型的预测。我们可以用箱线图(Boxplot)来可视化每个特征的异常值。
# 绘制多个特征的箱线图
fig, axes = plt.subplots(3, 5, figsize=(20, 12))
axes = axes.ravel() # 将二维坐标轴数组展平为一维
for i, col in enumerate(df.columns):
if i < len(axes):
sns.boxplot(y=df[col], ax=axes[i])
axes[i].set_title(col)
axes[i].set_ylabel('')
plt.tight_layout()
plt.show()
箱线图中间的线是中位数,箱体上下边界是上下四分位数,箱体外的点通常被视为异常值。对于异常值,你需要根据业务知识决定是保留、修正还是删除。例如,CRIM(犯罪率)的异常高值可能是真实的,反映了某些区域的极端情况,直接删除可能会丢失重要信息。一个常见的稳健做法是使用缩尾处理(Winsorization),将极端值替换为指定分位数(如1%和99%)的值。
# 示例:对‘LSTAT’特征进行缩尾处理(1%和99%分位数)
lower_bound = df['LSTAT'].quantile(0.01)
upper_bound = df['LSTAT'].quantile(0.99)
df['LSTAT_winsorized'] = df['LSTAT'].clip(lower=lower_bound, upper=upper_bound)
3. 特征工程:为模型准备“食材”
原始数据就像未经处理的食材,特征工程就是洗菜、切配、调味的过程,目的是让模型(“厨师”)能更好地“烹饪”出准确的预测。这一步往往比选择模型本身更能影响最终效果。
3.1 特征缩放:标准化与归一化
许多机器学习算法(如基于距离的KNN、支持向量机SVM,以及使用梯度下降的算法)对特征的尺度非常敏感。如果TAX(税率)的值在几百,而CHAS(0或1)的值在0-1,模型会错误地认为TAX更重要。我们需要将特征缩放到相似的尺度。
标准化(Standardization): 将数据转换为均值为0,标准差为1的分布。 归一化(Normalization): 将数据缩放到[0, 1]或[-1, 1]的固定区间。
我们通常使用标准化,因为它对异常值不那么敏感。
from sklearn.preprocessing import StandardScaler
# 选择需要缩放的特征列(通常不包括目标变量和已经二值化的特征)
features_to_scale = ['CRIM', 'ZN', 'INDUS', 'NOX', 'RM', 'AGE', 'DIS', 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT']
# 初始化标准化器
scaler = StandardScaler()
# 拟合(计算均值和标准差)并转换数据
df_scaled = df.copy()
df_scaled[features_to_scale] = scaler.fit_transform(df[features_to_scale])
print("标准化后的数据描述(均值为0,标准差为1):")
print(df_scaled[features_to_scale].describe().loc[['mean', 'std']])
3.2 特征选择与构造
不是所有特征都有用。有些特征可能与目标无关,有些特征之间高度相关(多重共线性),这会导致模型不稳定。我们可以通过计算特征与目标的相关性,以及特征间的相关性来辅助判断。
# 计算特征与目标变量的相关系数
correlation_with_target = df.corr()['MEDV'].sort_values(ascending=False)
print("特征与房价(MEDV)的相关系数:")
print(correlation_with_target)
# 可视化相关系数矩阵
plt.figure(figsize=(12, 10))
sns.heatmap(df.corr(), annot=True, fmt='.2f', cmap='coolwarm', center=0)
plt.title('特征相关系数矩阵热力图')
plt.show()
从相关系数可以看出,RM(房间数)与房价正相关最强,LSTAT(低收入人群比例)负相关最强。而特征之间,例如RAD和TAX相关性极高(0.91),这意味着它们提供的信息大量重叠,可以考虑只保留一个。
我们也可以尝试构造新特征。例如,直觉上,房间总数(RM)和房屋年龄(AGE)的交互可能影响房价,或者人均房间数可能是一个更好的指标(但这里没有人口数据)。特征工程需要创造力和对问题的理解。
3.3 划分训练集与测试集
在训练模型之前,我们必须将数据分成两部分:一部分用于训练模型,另一部分用于评估模型的泛化能力(即对未见过数据的预测能力)。如果只用训练数据评估,模型可能会“死记硬背”(过拟合),而在新数据上表现糟糕。
from sklearn.model_selection import train_test_split
# 定义特征X和目标y
X = df_scaled.drop('MEDV', axis=1) # 使用标准化后的特征
y = df_scaled['MEDV']
# 划分数据集,80%用于训练,20%用于测试。random_state确保每次划分结果一致,便于复现。
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
print(f"训练集大小: {X_train.shape}")
print(f"测试集大小: {X_test.shape}")
4. 模型训练、评估与优化
现在,我们来到了核心环节:选择算法、训练模型、评估性能并尝试优化。
4.1 选择模型并训练
我们从最简单的线性回归开始。线性回归假设目标变量和特征之间存在线性关系,即 房价 = w1*CRIM + w2*ZN + ... + w13*LSTAT + b。我们的目标是找到一组最佳的权重(w)和偏置(b)。
# 初始化线性回归模型
model = LinearRegression()
# 在训练集上训练(拟合)模型
model.fit(X_train, y_train)
# 查看模型学到的系数(权重)和截距
print("模型系数(权重):")
coef_df = pd.DataFrame({'特征': X_train.columns, '系数': model.coef_})
display(coef_df.sort_values(by='系数', key=abs, ascending=False)) # 按绝对值排序
print(f"\n模型截距: {model.intercept_:.2f}")
系数的大小和正负代表了该特征对房价影响的强度和方向。例如,RM的系数为正且较大,说明房间数越多,预测房价越高,符合直觉。
4.2 模型评估:量化预测性能
模型训练好了,但它表现如何?我们需要用测试集(模型从未见过的数据)来检验。
# 在测试集上进行预测
y_pred = model.predict(X_test)
# 导入评估指标
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
# 计算常用回归指标
mae = mean_absolute_error(y_test, y_pred)
mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse) # 均方根误差,与目标变量单位一致,更易解释
r2 = r2_score(y_test, y_pred)
print("模型在测试集上的表现:")
print(f"平均绝对误差 (MAE): {mae:.2f}")
print(f"均方误差 (MSE): {mse:.2f}")
print(f"均方根误差 (RMSE): {rmse:.2f}")
print(f"决定系数 (R²): {r2:.4f}")
- MAE/RMSE: 衡量预测值与真实值的平均差距。例如,RMSE为4.5,意味着平均预测误差约为4.5千美元(4500美元)。这个值需要与房价的波动范围对比来看。
- R²: 表示模型能解释的目标变量方差的比例。范围在0到1之间,越接近1越好。0.7的R²意味着模型能解释70%的房价波动。
4.3 结果可视化:让评估更直观
数字指标很重要,但图表能让我们更直观地理解模型的预测效果。
# 1. 预测值与真实值散点图
plt.figure(figsize=(8, 8))
plt.scatter(y_test, y_pred, alpha=0.6)
# 绘制理想预测线(y=x)
plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 'r--', lw=2)
plt.xlabel('真实房价 (千美元)')
plt.ylabel('预测房价 (千美元)')
plt.title('预测值 vs 真实值')
plt.show()
# 2. 残差图(Residual Plot)
residuals = y_test - y_pred
plt.figure(figsize=(10, 6))
sns.scatterplot(x=y_pred, y=residuals, alpha=0.6)
plt.axhline(y=0, color='r', linestyle='--')
plt.xlabel('预测房价 (千美元)')
plt.ylabel('残差 (真实值 - 预测值)')
plt.title('残差图')
plt.show()
理想的残差图应该是随机、均匀地分布在0线上下,没有明显的模式(如漏斗形、曲线形)。如果出现模式,说明模型有系统性误差,可能遗漏了某些重要特征或关系(如非线性关系)。
4.4 模型优化初探:尝试其他算法
线性回归是一个好的起点,但数据关系可能更复杂。我们可以轻松尝试scikit-learn中的其他算法,比如决策树和随机森林。
from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import RandomForestRegressor
# 初始化其他模型
tree_model = DecisionTreeRegressor(random_state=42)
forest_model = RandomForestRegressor(n_estimators=100, random_state=42) # n_estimators是树的数量
# 训练模型
tree_model.fit(X_train, y_train)
forest_model.fit(X_train, y_train)
# 评估模型
models = {'线性回归': model, '决策树': tree_model, '随机森林': forest_model}
results = {}
for name, mdl in models.items():
y_pred_m = mdl.predict(X_test)
results[name] = {
'RMSE': np.sqrt(mean_squared_error(y_test, y_pred_m)),
'R²': r2_score(y_test, y_pred_m)
}
# 比较结果
results_df = pd.DataFrame(results).T
print("不同模型性能对比:")
display(results_df.sort_values(by='R²', ascending=False))
你可能会发现随机森林的R²更高,RMSE更低。这是因为树模型能捕捉非线性关系和特征交互。但要注意,树模型更容易过拟合(在训练集上表现极好,测试集上变差)。我们可以通过交叉验证来更稳健地评估模型。
from sklearn.model_selection import cross_val_score
# 对随机森林进行5折交叉验证
cv_scores = cross_val_score(forest_model, X, y, cv=5, scoring='r2')
print(f"随机森林5折交叉验证R²分数: {cv_scores}")
print(f"平均交叉验证R²: {cv_scores.mean():.4f} (+/- {cv_scores.std() * 2:.4f})")
交叉验证将数据分成多份,轮流将其中一份作为验证集,其余作为训练集,能更好地评估模型的平均性能和稳定性。
5. 项目复盘与下一步行动指南
走到这里,你已经完成了一个完整的机器学习小项目。让我们回顾一下关键步骤,并聊聊接下来可以探索的方向。
核心流程复盘:
- 环境搭建: 使用虚拟环境隔离项目,固定库版本以避免“玄学”错误。
- 数据探索: 用
.head(),.info(),.describe()和可视化(直方图、散点图、箱线图、热力图)了解数据全貌。 - 数据清洗: 处理缺失值和异常值,为模型提供干净的“食材”。
- 特征工程: 通过标准化/归一化统一特征尺度,通过相关性分析进行特征选择,甚至可以尝试构造新特征。
- 数据划分: 使用
train_test_split严格分离训练集和测试集,确保评估的公正性。 - 模型训练与评估: 从简单模型(线性回归)开始,用测试集计算MAE、RMSE、R²等指标,并通过残差图诊断模型问题。
- 模型优化与比较: 尝试不同算法(决策树、随机森林),使用交叉验证获得更稳健的性能评估。
常见陷阱与应对:
- 数据泄露: 绝对不要在拟合标准化器(
scaler.fit)时使用测试集数据,也不要在特征选择时用到测试集信息。这会导致对模型性能的乐观估计。确保数据预处理步骤只在训练集上进行拟合,然后统一应用于测试集。 - 过拟合: 模型在训练集上表现完美,在测试集上却很差。应对方法包括:获取更多数据、进行特征选择、使用正则化(如岭回归、Lasso)、或选择更简单的模型。
- 欠拟合: 模型在训练集和测试集上都表现不佳。应对方法包括:增加更多相关特征、构造更有意义的特征、使用更复杂的模型、或减少正则化强度。
下一步你可以做什么?
- 深入特征工程: 尝试多项式特征(捕捉非线性关系)、交互项、或对偏态分布的特征进行对数变换。
- 超参数调优: 使用
GridSearchCV或RandomizedSearchCV系统性地搜索随机森林的n_estimators、max_depth等参数的最佳组合。 - 尝试更高级的模型: 梯度提升机(如XGBoost, LightGBM)通常在结构化数据比赛中表现优异。
- 部署你的模型: 使用
pickle或joblib库保存训练好的模型,然后集成到一个简单的Web应用(例如用Flask或Streamlit)中,让用户输入房屋特征来预测房价。 - 挑战新数据集: 去Kaggle或UCI机器学习仓库找一个你感兴趣的新数据集,把这套流程从头到尾再跑一遍。实践是巩固知识的最佳方式。
记住,机器学习项目是一个迭代的过程。很少有模型第一次就能达到完美。你需要不断地回到数据探索、特征工程和模型调整的步骤,形成一个“分析-建模-评估-优化”的循环。这次实战只是一个开始,工具箱已经交到你手上,接下来用它们去解决你感兴趣的真实问题吧。我在最初学习时,也是通过这样一个个小项目的成功构建,才逐渐积累了信心和直觉。遇到报错别灰心,那正是你理解系统如何工作的好机会。
更多推荐
所有评论(0)