隨著 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)