一、为什么要有乾净的专案架构?
随着 AI 的快速发展,越来越多人开始写 AI 程式专案。然而,在学校或教科书上,许多 AI 专案或作业往往能够用一个 .ipynb 或 .py 档案就解决,这使得许多 AI 开发者习惯将所有流程塞进单一档案中,导致程式码难以维护、扩充困难,甚至连除错(debug)都变得极为痛苦。
通常一个完整的机器学习专案通常包含 资料读取、前处理、模型训练、最佳化、评估及部署 等环节。如果没有良好的架构规划习惯,随着专案变大,程式码会变得混乱不堪,影响可读性与可维护性,甚至降低开发效率。
乾净的专案架构能带来以下优势:
1.提升可读性:开发者可以轻鬆理解每个档案和模组的功能,不需要花大量时间寻找程式码逻辑。
2.提高维护性:当专案需要更新功能或套件时,可以精确修改相应的模组,而不会影响整体架构。
3.方便团队协作:多人开发时,各自负责不同模组,减少合併冲突,加速开发流程。
4.更容易除错:模组化的程式码让问题范围更明确,有助于快速定位错误,提升除错效率。
无论是个人专案还是公司专案,良好的专案架构都是确保专案成功的关键。也因此接下来,我将透过一个简单的示范,建立 乾净且可扩展 的机器学习专案架构,并且未来可以依不同需求举一反三!
核心精神: 不同的工作,就要模组化,分开放!
二、专案结构范例
以下是一个简化的专案架构示例,让你在初始阶段就能清楚规划。通常建议在同一层级放置 requirements.txt,并有一份 README.md 作为使用说明。
my_ml_project/
├── data_processor.py # 主要用于资料读取、前处理、切分、特徵缩放、抽样等
├── trainers.py # 主要放置模型训练、超参数调整、评估等相关程式
├── main.py # 专案进入点;整合前处理、模型训练与评估的流程
├── requirements.txt # 所需套件及其版本
├── README.md # 专案简介、安装及执行方式说明
└── ... # 其他可能需要的资料夹 (e.g. configs, docs, tests ...等)
1.data_processor.py:与资料处理相关的逻辑都封装在此,例如分割训练/测试集、资料前处理、特徵缩放、资料不平衡处理(如 SMOTE)等。2.trainers.py:与模型相关逻辑都封装在这里,例如不同的模型 Trainer 类别、超参数搜寻与调整、训练与评估函式等等。3.main.py:作为整个程式的进入点,负责串接所有流程,从读取与前处理资料开始,到最后的模型训练与评估。
因此,通常只需要执行 main.py 就能跑完整个专案,并且引用 dataprocessor.py、trainers.py 等相关模组。如果需要扩充新功能,只需新增一个新的 .py 档案,或者在现有的 .py 档案中扩展功能,而不会让主程式变得杂乱。这样的架构不仅能保持程式码乾净、有条理,也让除错变得更加直觉,开发者能快速定位并修正问题,提高维护性与可读性。
4.requirements.txt:使用 pip freeze > requirements.txt 可以将专案中使用的套件版本锁定,利于他人建立相同环境。5.README.md:说明专案架构、使用方式、环境需求等。6.其他....
三、范例程式
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from imblearn.over_sampling import SMOTE
class DataProcessor:
def __init__(self, test_size=0.2, random_state=42, resampler=None, sampling_strategy=\'auto\'):
self.test_size = test_size
self.random_state = random_state
self.scaler = StandardScaler()
# 若未指定 resampler,预设使用 SMOTE
self.resampler = resampler if resampler else SMOTE(
sampling_strategy=sampling_strategy,
random_state=random_state
)
def process(self, X, y):
# 切分资料
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=self.test_size,
stratify=y,
random_state=self.random_state
)
# 特徵缩放
X_train_scaled = self.scaler.fit_transform(X_train)
X_test_scaled = self.scaler.transform(X_test)
# 不平衡抽样
if self.resampler:
X_train_scaled, y_train = self.resampler.fit_resample(X_train_scaled, y_train)
return X_train_scaled, X_test_scaled, y_train, y_test
from abc import ABC, abstractmethod
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import AdaBoostClassifier, RandomForestClassifier
from sklearn.tree import DecisionTreeClassifier
class BaseTrainer(ABC):
def __init__(self):
self.model = None
@abstractmethod
def train(self, X_train, y_train):
pass
def tune_parameters(self, X_train, y_train, param_grid):
grid_search = GridSearchCV(self.model, param_grid, cv=5, scoring=\'f1\', n_jobs=-1)
grid_search.fit(X_train, y_train)
self.model = grid_search.best_estimator_
return grid_search.best_params_
def evaluate(self, X_test, y_test):
y_pred = self.model.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
conf_matrix = confusion_matrix(y_test, y_pred)
print("Accuracy:", accuracy)
print("Classification Report:\\n", classification_report(y_test, y_pred))
print("Confusion Matrix:\\n", conf_matrix)
return accuracy, conf_matrix
class AdaBoostTrainer(BaseTrainer):
def __init__(self):
super().__init__()
self.model = AdaBoostClassifier(
estimator=DecisionTreeClassifier(max_depth=1),
random_state=42
)
def train(self, X_train, y_train):
self.model.fit(X_train, y_train)
class RandomForestTrainer(BaseTrainer):
def __init__(self):
super().__init__()
self.model = RandomForestClassifier(random_state=42)
def train(self, X_train, y_train):
self.model.fit(X_train, y_train)
from sklearn.datasets import load_breast_cancer
from imblearn.over_sampling import SMOTE
from data_processor import DataProcessor # 引用我们建立的 DataProcessor.py
from trainers import AdaBoostTrainer, RandomForestTrainer # 引用我们建立的 trainers.py
# 载入资料 (以乳腺癌资料集为例)
data = load_breast_cancer()
X, y = data.data, data.target
# 建立 DataProcessor,并设定 SMOTE 参数
processor = DataProcessor(
test_size=0.2,
resampler=SMOTE(sampling_strategy=0.8, random_state=42)
)
X_train, X_test, y_train, y_test = processor.process(X, y)
print("\\n==== AdaBoost ====")
ada_trainer = AdaBoostTrainer()
ada_trainer.train(X_train, y_train)
# 超参数搜寻
param_grid = {\'n_estimators\': [50, 100], \'learning_rate\': [0.1, 1.0]}
best_params = ada_trainer.tune_parameters(X_train, y_train, param_grid)
print("Best Parameters:", best_params)
ada_trainer.evaluate(X_test, y_test)
print("\\n==== RandomForest ====")
rf_trainer = RandomForestTrainer()
rf_trainer.train(X_train, y_train)
param_grid = {\'n_estimators\': [50, 100]}
best_params = rf_trainer.tune_parameters(X_train, y_train, param_grid)
print("Best Parameters:", best_params)
rf_trainer.evaluate(X_test, y_test)
4、如何进行后续扩充与维护(1) 新增模型:若要新增其他模型(如 XGBoost、LightGBM、SVM 等),可以在 trainers.py 内新增一个类似 XGBoostTrainer 的类别,并继承 BaseTrainer。如此可保持整体结构一致。(2) 撰写测试:可以在专案根目录下新增 tests/,利用框架如 pytest 或 unittest,撰写单元测试及整合测试,确保前处理与模型训练流程正确。(3) 管理设定档:若你的专案需要更复杂的设定,可考虑在 config/ 内放置 YAML 或 JSON 等格式的设定档,集中管理超参数或路径资讯。(4) 对外文件与说明:README.md 中除了介绍专案背景外,也可以描述执行步骤 (如:pip install -r requirements.txt、python main.py),或者若专案复杂程度较高,建议另开 docs/ 目录,并有更详细的技术文件或范例使用说明。
这些步骤有助于让机器学习专案更容易扩充、新增功能、维护,以及让其他人更快上手与理解。
四、结语
透过上述的架构与程式码范例,可以打造一个乾净且清晰的机器学习专案。这样的设计能够让开发者或团队在实作各种功能时更具弹性,并能简单扩充至更多模型、不同的前处理方法、或进阶的参数搜寻策略。
在实务应用中,每个专案的需求皆不相同,但只要依循「让资料处理、模型训练、流程整合分开」的思路,就能有效降低维护成本、提高可读性。希望这份范例能对大家的专案开发带来帮助,并让你在打造机器学习系统的过程中更得心应手。(下次也就不要随便交出一个.jpynb 或.py给你的主管或同事了XD)