一、为什么要有乾净的专案架构?

随着 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.其他....


三、范例程式

  • data_processor.py此档案用于封装所有资料前处理与切分的逻辑。范例中使用 StandardScaler 进行特徵缩放,并在训练集上应用不平衡资料处理(如 SMOTE)来平衡类别数量。(碎碎念: 虽然有些人不在乎,但我建议先进行缩放调整后,再进行不平衡处理,原因是先用不平衡括处理并扩增或减少样本后,此时的平均、标準差就不会是原始资料的统计量,这时候进行标準化会失真! 但若是minmax,则因为不太会改变,我认为先后就没差。但上述两种情境,都会因为执行先后而有差异,我将在另一篇文章示范)
  • 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

  • trainers.py此档案主要存放模型训练与评估逻辑。以下范例展示了如何建立抽象基底类别 BaseTrainer,并在其上扩充两种不同的模型训练器(AdaBoostTrainer 与 RandomForestTrainer)。此外,也示范了如何进行超参数搜寻 (GridSearchCV) 和评估 (accuracy_score, classification_report, confusion_matrix)。
  • 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)

  • main.py此档案为整个专案的「进入点」。读取资料集后,会透过 DataProcessor 完成前处理,再透过 AdaBoostTrainer 或 RandomForestTrainer 进行模型训练、超参数调整与评估。此档案主要用于串接整个专案流程。
  • 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)