今日的示例代码包含2个部分
昨天我们已经介绍了如何在不同的文件中,导入其他目录的文件,核心在于了解导入方式和python解释器检索目录的方式。
搞清楚了这些,那我们就可以来看看,如何把一个文件,拆分成多个具有着独立功能的文件,然后通过import的方式,来调用这些文件。这样具有几个好处:
一个典型的机器学习项目通常包含以下阶段:
load_data.py
、data_loader.py
eda.py
、visualization_utils.py
preprocess.py
、data_cleaning.py
、data_transformation.py
feature_engineering.py
model.py
、train.py
evaluate.py
predict.py
、inference.py
src/data/load_data.py
:负责从各类数据源(如文件系统、数据库、API 等)读取原始数据。src/data/preprocess.py
:进行数据清洗(处理缺失值、异常值)、数据转换(标准化、归一化、编码等)操作。src/data/feature_engineering.py
:根据业务和数据特点,创建新特征或对现有特征进行选择、优化。src/models/model.py
:定义模型架构,比如神经网络结构、机器学习算法模型设定等。src/models/train.py
:设置模型超参数,并执行训练过程,保存训练好的模型。src/models/evaluate.py
:使用合适的评估指标(如准确率、召回率、均方误差等),在测试集上评估模型性能,生成评估报告。src/models/predict.py
或 src/models/inference.py
:利用训练好的模型对新数据进行预测。src/utils/io_utils.py
:包含文件读写相关帮助函数,比如读取特定格式文件、保存数据到文件等。src/utils/logging_utils.py
:实现日志记录功能,方便记录项目运行过程中的信息,便于调试和监控。src/utils/math_utils.py
:特定的数值计算函数,像自定义的矩阵运算、统计计算等。src/utils/plotting_utils.py
:绘图工具函数,用于生成数据可视化图表(如绘制损失函数变化曲线、特征分布直方图等 )。config/config.py
或 config/settings.py
:以 Python 代码形式定义配置参数。config/config.yaml
或 config/config.json
:采用 YAML 或 JSON 格式,清晰列出文件路径、模型超参数、随机种子、API 密钥等可配置参数。.env
文件:通常放在项目根目录,用于存储敏感信息(如数据库密码、API 密钥等),在代码中通过环境变量的方式读取,一般会被 .gitignore
忽略,防止敏感信息泄露。notebooks/ 或 experiments/ 目录:用于初期的数据探索、快速实验、模型原型验证。
notebooks/initial_eda.ipynb
:在项目初期,使用 Jupyter Notebook 进行数据探索与可视化,了解数据特性,分析数据分布、相关性等。experiments/model_experimentation.py
:编写脚本对不同模型架构、超参数组合进行快速实验,对比实验结果,寻找最优模型设置。这部分往往是最开始的探索阶段,后面跑通了后拆分成了完整的项目,留作纪念用。
data/raw/
:放置从外部获取的未经处理的原始数据,保持数据原始状态。data/processed/
:存放经过预处理(清洗、转换、特征工程等操作)后的数据,供模型训练和评估使用。data/interim/
:(可选)保存中间处理结果,比如数据清洗过程中生成的临时文件、特征工程中间步骤产生的数据等。.pkl
(Python pickle 格式,常用于保存 sklearn 模型 )、.h5
(常用于保存 Keras 模型 )、.joblib
等。reports/evaluation_report.txt
:记录模型评估的详细结果,包括各项评估指标数值、模型性能分析等。reports/visualizations/
:存放数据可视化图片,如损失函数收敛图、预测结果对比图等。output/logs/
:保存项目运行日志文件,记录项目从开始到结束过程中的关键信息,如训练开始时间、训练过程中的损失值变化、预测时间等。总结一下通用的拆分起步思路:
.py
文件中。 这是最基本也是最有价值的一步。utils.py
来存放通用的辅助函数。config.py
文件中。data/
和 models/
,将它们与你的源代码(通常放在 src/
目录)分开。当遵循这些通用的拆分思路和原则时,项目结构自然会变得清晰。
常常会看到if name == "main"这个写法,实际上,每个文件都是一个对象,对象就会有属性和方法。
如果直接运行这个文件,则__name__等于__main__,若这个文件被其他模块导入,则__name__不等于__main__。
这个写法有如下好处:
明确程序起点:一个 Python 项目往往由多个模块组成。if name == "main" 可清晰界定程序执行的起始位置。比如一个包含数据处理模块 data_processing.py、模型训练模块 model_training.py 的机器学习项目,在 model_training.py 中用 if name == "main" 包裹训练相关的主逻辑代码,运行该文件时就知道需要从这里开始执行(其他文件都是附属文件),让项目结构和执行流程更清晰。(大多时候如此)
避免执行:python遵从模块导入即执行机制,当你使用 import xxx 导入一个模块时,Python 会执行该模块中的所有顶层代码(即不在任何函数或类内部的代码)。如果顶层代码中定义了全局变量或执行了某些操作(如读取文件、初始化数据库连接),这些操作会在导入时立即生效,并可能影响整个程序的状态。为了避免执行不必要的代码,我们可以使用 if name == "main" 来避免在导入时执行不必要的代码。这样,只有当模块被直接运行时(即被执行 python xxx.py),才会执行顶层代码,而导入时则不会执行。这样,我们就可以确保在导入模块时,不会执行不必要的代码,从而提高程序的性能和可维护性。
合理的资源管理:if name == "main" 与定义 main 函数结合使用,函数内变量在函数执行完这些变量被释放,能及时回收内存资源,避免内存泄漏,保证程序高效运行。
规范的py文件,首行会有:# -- coding: utf-8 --
主要目的是 显式声明文件的编码格式,确保 Python 解释器能正确读取和解析文件中的非 ASCII 字符(如中文、日文、特殊符号等)。也就是说这个是写给解释器看的。
因为,在 Python 2.x 时代,默认编码是 ASCII,不支持直接在代码中写入非 ASCII 字符(如中文注释、字符串中的中文),否则会报错(SyntaxError: Non-UTF-8 code starting with...)。但是Python 3.x 默认为 UTF-8 编码,理论上可以省略编码声明。但实际开发中,为了兼容旧代码、明确文件编码规则,或在团队协作中避免因编辑器 / 环境配置不同导致的乱码问题,许多开发者仍会保留这一行声明。
ps:
非 ASCII 字符的代码如下所示:
# -*- coding: utf-8 -*-
msg = "你好,世界!" # 中文字符串
print(msg)
你好,世界!
很多时候,项目中会包含gitattribute文件,来确保在不同操作系统和编辑器中,文件的编码格式一致。这里我们后面说到git工具在介绍
Python 的类型注解是在 Python 3.5+ 引入的特性,用于为变量、函数参数、返回值和类属性等添加类型信息。虽然 Python 仍是动态类型语言,但类型注解可以提高代码可读性、可维护性,并支持静态类型检查工具(如 mypy)。
其次你在安装python插件的时候,附带安装了2个插件
变量类型注解语法为 变量名: 类型
# 变量的类型注解
name: str = "Alice"
age: int = 30
height: float = 1.75
is_student: bool = False
函数类型注解为函数参数和返回值指定类型,语法为 def 函数名(参数: 类型) -> 返回类型。
def add(a: int, b: int) -> int:
return a + b
def greet(name: str) -> None:
print(f"Hello, {name}")
类属性与方法的类型注解:为类的属性和方法添加类型信息。
# 定义一个矩形类
class Rectangle:
width: float # 矩形宽度(浮点数),类属性的类型注解(不初始化值)
height: float # 矩形高度(浮点数)
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
# 计算面积(宽度 × 高度)
return self.width * self.height
上述的width: float # 矩形宽度(浮点数)
这个写法由于没有对变量赋值,所以是一种类型注解写法
尝试针对之前的心脏病项目ipynb,将他按照今天的示例项目整理成规范的形式,思考下哪些部分可以未来复用。
补充介绍:
pyc文件的介绍
mkdir -p heart-disease-project/data/{raw,processed}
mkdir -p heart-disease-project/models
mkdir -p heart-disease-project/src
touch heart-disease-project/config.py
touch heart-disease-project/README.md
touch heart-disease-project/requirements.txt
# config.py
import os
# 数据路径
RAW_DATA_PATH = os.path.join("data", "raw", "heart_disease.csv")
PROCESSED_DATA_PATH = os.path.join("data", "processed", "heart_data_processed.csv")
# 模型路径
MODEL_SAVE_PATH = os.path.join("models", "heart_model.pkl")
# 超参数
TEST_SIZE = 0.2
RANDOM_STATE = 42
# src/data_processing.py
import pandas as pd
from sklearn.model_selection import train_test_split
from config import RAW_DATA_PATH, PROCESSED_DATA_PATH
def load_and_preprocess_data():
# 加载原始数据
df = pd.read_csv(RAW_DATA_PATH)
# 处理缺失值(示例:填充均值)
df.fillna(df.mean(), inplace=True)
# 特征与标签分离
X = df.drop("target", axis=1)
y = df["target"]
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# 保存预处理后的数据
processed_data = {
"X_train": X_train,
"X_test": X_test,
"y_train": y_train,
"y_test": y_test,
}
pd.to_pickle(processed_data, PROCESSED_DATA_PATH)
return processed_data
# src/model_training.py
import joblib
from sklearn.ensemble import RandomForestClassifier
from config import MODEL_SAVE_PATH
from src.data_processing import load_and_preprocess_data
def train_model():
# 加载预处理后的数据
data = pd.read_pickle(config.PROCESSED_DATA_PATH)
X_train, y_train = data["X_train"], data["y_train"]
# 初始化模型
model = RandomForestClassifier(n_estimators=100, random_state=42)
# 训练模型
model.fit(X_train, y_train)
# 保存模型
joblib.dump(model, MODEL_SAVE_PATH)
print(f"Model saved to {MODEL_SAVE_PATH}")
# src/evaluation.py
import joblib
from sklearn.metrics import classification_report, roc_auc_score
from config import MODEL_SAVE_PATH, PROCESSED_DATA_PATH
def evaluate_model():
# 加载模型和数据
model = joblib.load(MODEL_SAVE_PATH)
data = pd.read_pickle(PROCESSED_DATA_PATH)
X_test, y_test = data["X_test"], data["y_test"]
# 预测结果
y_pred = model.predict(X_test)
y_proba = model.predict_proba(X_test)[:, 1]
# 输出评估指标
print("Classification Report:")
print(classification_report(y_test, y_pred))
print("ROC AUC Score:")
print(roc_auc_score(y_test, y_proba))
# src/utils.py
import os
import logging
def create_directory(path):
"""确保目录存在"""
if not os.path.exists(path):
os.makedirs(path)
logging.info(f"Created directory: {path}")
# requirements.txt
pandas
scikit-learn
joblib
* 准备数据
将原始数据文件 heart_disease.csv 放入 data/raw/ 目录。
* 运行数据处理
python src/data_processing.py
* 训练模型
python src/model_training.py
* 评估模型
python src/evaluation.py
@浙大疏锦行