在機器學習專案中,程式碼品質往往決定了專案的成敗。良好的程式碼結構不僅能提升可讀性,也讓後續維護和擴充更加容易。本文將以 Python 和 scikit-learn 為例,逐步示範如何重構機器學習程式碼,並融入測試、設計原則和最佳實踐。首先,我們會將一個典型的機器學習 Jupyter Notebook 轉換為 Python 指令碼,接著運用特性測試和單元測試確保重構過程的安全性。過程中,我們將識別並消除程式碼異味,例如過長的函式、命名不佳的變數以及重複的程式碼片段。同時,我們也會探討變數命名、註解使用和死碼移除等最佳實踐,並示範如何將資料準備、模型訓練和評估等步驟模組化,最終建立一個更具結構性、可讀性和可維護性的機器學習專案。

抽象的重要性

抽象是軟體開發中的一種重要的設計原則。透過使用抽象,我們可以將複雜的系統簡化為一個簡單的表示,從而更容易地理解和使用它們。抽象還可以幫助我們將系統的不同部分解耦合,從而更容易地維護和修改它們。

在機器學習中,抽象是一種強大的工具,能夠簡化複雜的系統,讓我們更容易地理解和使用它們。透過使用抽象,我們可以將複雜的實作細節封裝起來,然後用一個簡單的表示來替代它,從而更容易地理解和使用系統。

內容解密:

抽象是一種強大的工具,能夠簡化複雜系統,讓我們更容易地理解和使用它們。抽象的基本思想是將複雜的實作細節封裝起來,然後用一個簡單的表示來替代它,例如一個函式的名稱或一個類別及其方法。透過使用抽象,我們可以將複雜的系統簡化為一個簡單的表示,從而更容易地理解和使用它們。

# 抽象的例子
class DataFrame:
    def __init__(self, data):
        self.data = data

    def head(self):
        return self.data[:5]

    def sort_values(self, by):
        return self.data.sort_values(by)

# 使用抽象的例子
df = DataFrame([1, 2, 3, 4, 5])
print(df.head())  # [1, 2, 3, 4, 5]
print(df.sort_values(by='value'))  # [1, 2, 3, 4, 5]

圖表翻譯:

以下是抽象的流程圖:

  flowchart TD
    A[複雜系統] --> B[抽象]
    B --> C[簡單表示]
    C --> D[使用]
    D --> E[維護和修改]

這個流程圖描述了抽象的過程,從複雜系統到簡單表示,然後到使用和維護和修改。

介面設計:可讀性和可維護性的解決方案

當我們想要建立酷炫的東西時,我們不希望花太多時間維護程式碼。我們想要寫程式碼,而不是讀程式碼!你可能比年輕的 David 更明智,無需說明可讀程式碼的重要性。但是一位 StackExchange 使用者曾經這樣說:「寫程式碼是一個迭代的過程,每次迭代都根據前一次產生的程式碼,新增功能,修復 bug 等。為了做到這一點,你需要能夠讀懂現有的程式碼,以知道如何和在哪裡修改它,如何寫出使用現有程式碼的程式碼。」

可讀性和可維護性至關重要。如果沒有它們,我們會因為複雜性和認知負擔而被拖慢腳步,無法執行我們的想法和實驗。

現在,我們要把凌亂的虛擬「廚房」(程式碼函式庫)整理好,並使用玄貓的測試和軟體設計原則。讓我們看看第三個也是最後一個工具箱(重構),然後我們就可以開始清理了。

重構 101

在這個部分,我們將涵蓋指導我們在重構時做出小決策的原則。

在我們開始之前,讓我們重新審視一下重構的定義。重構是指在不改變可觀察行為的情況下重新構建現有程式碼。 Martin Fowler 對此有很好的描述:「重構就是應用小的行為保留步驟,並透過這些步驟實作大改變。每個個別的重構要麼是很小的,要麼是小步驟的組合。因此,在重構時,我的程式碼不會花太多時間處於破碎的狀態,這樣我就可以在任何時候停止,即使我尚未完成。 […] 如果有人說他們的程式碼因為重構而壞了幾天,你可以肯定他們不是在重構。」

如果我們把程式碼函式庫或解決方案想象成一個物理工作室,重構就能幫助我們整理、系統化和標準化工作室,使其免受雜亂的影響,讓我們能夠高效地工作,而不是一直被東西絆倒。

有很多關於重構的演講、書籍和文章(我們將在本章參照一些很好的資源),我們從中提煉出四個可以幫助指導你在重構時做出決策的啟發式:

兩頂帽子

在重構(一頂帽子)時,不要新增功能(另一頂帽子),反之亦然。在程式設計時,你可能會頻繁地在兩頂帽子之間切換,但同時戴兩頂帽子會導致過度的認知負擔和破碎的程式碼函式庫。

探路規則

把程式碼函式庫留得比你找到它時更乾淨。如果你在路上看到一點「垃圾」,請在不花太多時間的情況下把它撿起來。如果它是一個需要更多時間並可能超出任務範圍的坑,請在你的團隊的技術債務雷達上使其可見,以便團隊記得回來修復它(更多關於技術債務雷達的內容將在最後一節中)。直到它被修復之前,坑會繼續絆倒人們,甚至可能損壞車輛。

目標不是一條鍍金的道路。目標是維護一個功能性和合理的程式碼函式庫,使每個人都能夠在不必要的意外或耗時繞道的情況下編寫程式碼。

  flowchart TD
    A[開始重構] --> B[識別需要重構的程式碼]
    B --> C[應用重構原則]
    C --> D[測試和驗證]
    D --> E[重構完成]

圖表翻譯:

此圖表示重構的流程。從開始重構到識別需要重構的程式碼,然後應用重構原則,接著進行測試和驗證,最後完成重構。這個流程可以幫助我們在重構時做出正確的決策,並確保程式碼函式庫的可讀性和可維護性。

重構:從混亂到井然有序

在前面的章節中,我們討論瞭如何使用 IDE 進行重構,以提高開發效率。現在,我們將探討如何避免過早的抽象化,並使用重構迴圈來指導我們的重構旅程。

避免過早的抽象化

過早的抽象化是指在不完全瞭解需求或問題的情況下,就開始進行抽象化的設計。這種做法可能會導致錯誤的抽象化,從而使得程式碼更加複雜和難以維護。Sandi Metz 在她的演講中提到,現有的程式碼會對我們的設計產生很大的影響,尤其是當程式碼複雜和難以理解時,我們會感到壓力去保留它。

在這種情況下,簡單的做法往往是最好的選擇。即使這意味著沒有抽象化,也比創造一個錯誤的抽象化要好。當我們看到合適的抽象化時,創造正確的抽象化比拆除現有的錯誤設計要容易得多。

重構迴圈

重構迴圈是一個有用的過程,幫助我們安全地重構問題的程式碼函式庫。它包括兩個部分:準備步驟和迭代重構步驟。

準備步驟

  1. 執行程式碼或筆記本,確保它按照預期工作。
  2. 測試程式碼或筆記本,確保它是正確的。

迭代重構步驟

  1. 找到需要重構的程式碼或模組。
  2. 測試程式碼或模組,確保它是正確的。
  3. 重構程式碼或模組,使用重構技術和設計原則。
  4. 測試重構後的程式碼或模組,確保它是正確的。
  5. 重複步驟 1-4,直到程式碼或筆記本完全重構。

如何重構一個筆記本或程式碼函式庫

如果我們把程式碼函式庫想象成一個物理工作坊,我們知道我們有問題當我們難以找到正確的工具或材料,或甚至在程式碼中迷失方向。同樣,在程式碼中,我們可能需要拆解一個 200 行的函式,或者在亂七八糟的程式碼中找到正確的邏輯或行為。

在這一節中,我們將使用三個工具箱(測試、設計原則和重構技術)來重構一個問題的程式碼函式庫,將其轉化為可讀、可維護和可演化的解決方案。

練習

開始練習之前,請確保您已經安裝了 Python 3.10 或 3.11。如果您使用的是更新的 Python 版本(例如 Python 3.12),並且在執行 poetry install 時遇到問題,請使用 Python 3.10 或 3.11。

圖表翻譯

  graph LR
    A[開始] --> B[執行程式碼或筆記本]
    B --> C[測試程式碼或筆記本]
    C --> D[找到需要重構的程式碼或模組]
    D --> E[測試程式碼或模組]
    E --> F[重構程式碼或模組]
    F --> G[測試重構後的程式碼或模組]
    G --> H[重複步驟]

圖表翻譯

此圖表展示了重構迴圈的過程。從開始到執行程式碼或筆記本,然後測試程式碼或筆記本,找到需要重構的程式碼或模組,測試程式碼或模組,重構程式碼或模組,測試重構後的程式碼或模組,最後重複步驟直到程式碼或筆記本完全重構。

重構前的準備工作

在進行重構之前,需要進行一些準備工作,以確保重構的順暢進行。首先,需要重啟notebook的kernel,以避免隱藏的狀態影響重構的結果。然後,需要移除print陳述式,以減少噪音和視覺雜亂,使得重構的過程更加容易。

移除print陳述式不僅可以減少噪音和視覺雜亂,也可以幫助保護敏感的資料。因為print陳述式可能會將敏感的資料輸出到日誌中,從而增加了資料洩露的風險。因此,需要審查print陳述式,確保它們不會將敏感的資料輸出到日誌中。

列出程式碼異味

程式碼異味是指程式碼中存在的問題或缺陷,它們可能會導致程式碼的維護和擴充套件更加困難。列出程式碼異味可以幫助我們識別出程式碼中存在的問題,從而進行重構和改進。例如,一個函式中有五行的解釋性註解,可能表明該函式過於複雜和難以理解。

將notebook轉換為Python檔案

將notebook轉換為Python檔案可以幫助我們將程式碼分解為可匯入的Python模組。這樣可以使得我們更容易地進行重構和維護程式碼。另外,使用Python檔案也可以使得我們更容易地使用IDE進行重構和除錯。

新增特徵測試

新增特徵測試是重構的關鍵步驟。特徵測試是一種自動化測試,描述了現有軟體的行為,從而保護了現有的行為不會被意外地改變。這種測試可以幫助我們確保重構的過程中不會導致程式碼的行為發生改變。

以下是重構前的準備工作的步驟:

  1. 重啟notebook的kernel
  2. 移除print陳述式
  3. 列出程式碼異味
  4. 將notebook轉換為Python檔案
  5. 新增特徵測試

這些步驟可以幫助我們進行重構前的準備工作,確保重構的順暢進行和程式碼的品質。

程式碼重構與測試

程式碼重構是一個關鍵的步驟,讓我們可以將複雜的程式碼分解成模組化、合理的元件。這個過程中,我們會使用到不同的測試方法,包括特性測試(characterization test)和單元測試(unit test)。

特性測試

特性測試是一種將程式視為黑盒的測試方法,目的是要描述程式的行為。例如,我們可以建立一個模型,並使用特性測試來確保其準確度達到90%。這種測試方法可以讓我們在修改程式碼時,確保其行為仍然符合預期。

單元測試

單元測試則是用來測試程式碼中的個別元件。這種測試方法可以讓我們快速地發現程式碼中的問題,並確保修改後的程式碼仍然能正常運作。

重構迴圈

重構迴圈是一個迭代的過程,包括以下步驟:

  1. 識別要抽取的程式碼:找到需要重構的程式碼區塊。
  2. 撰寫單元測試:為抽取的程式碼撰寫單元測試,並觀察測試失敗(紅色)。
  3. 讓測試透過:修改程式碼,使測試透過(綠色)。
  4. 匯入新函式:將新函式匯入程式碼中。
  5. 確保特性測試透過:確保特性測試仍然能透過。
  6. 提交修改:提交修改後的程式碼。

程式碼氣味

程式碼氣味(code smell)是指程式碼中存在的問題或壞味道。常見的程式碼氣味包括:

  • 命名不良:變數名稱不明確,導致閱讀程式碼時需要額外的精神努力。
  • 資料框架命名不良:資料框架(dataframe)命名不明確,例如使用 df 作為資料框架的名稱。

資料框架命名最佳實踐

資料框架的命名應該反映其內容。例如,如果每一行代表一個貸款,則可以將資料框架命名為 loans。這樣可以使程式碼更容易閱讀和理解。

範例

以下是兩個命名變數的例子:

壞例子

df = pd.read_csv('loans.csv')

好例子

loans = pd.read_csv('loans.csv')

在好例子中,變數 loans 的名稱明確地反映了其內容,讓程式碼更容易閱讀和理解。

程式碼品質與可維護性

在撰寫程式碼時,變數名稱的選擇和註解的使用對於程式碼的可讀性和維護性有著重要的影響。良好的變數名稱可以清晰地表達變數的意義和用途,減少了對註解的需求。另一方面,過度的註解可能會使程式碼變得混亂和難以理解。

變數名稱的重要性

選擇適當的變數名稱可以大大提高程式碼的可讀性。例如,在處理貸款資料時,使用 monthly_loansmonthly_loans_in_december 作為變數名稱可以清晰地表達出這些變數所代表的意義。

loans = pd.read_csv('loans.csv')
monthly_loans = loans.groupby(['month']).sum()
monthly_loans_in_december = filter_loans(monthly_loans, month=12)

註解的適當使用

註解可以用來解釋複雜的程式碼或是提供額外的資訊,但是過度的註解可能會使程式碼變得混亂。良好的程式碼應該盡量減少註解的使用,取而代之的是使用清晰的變數名稱和函式名稱。

# Bad example:
# Check to see if employee is eligible for full benefits
if (employee.flags and HOURLY_FLAG) and (employee.age > 65):
    ... do something

# Good example:
if employee.is_eligible_for_benefits():
    ... do something

死碼的移除

死碼是指那些不會影響程式執行結果的程式碼。這些程式碼可能會增加維護的困難度和認知負擔。因此,應該盡量移除死碼,以保持程式碼的清晰和簡潔。

# Bad example:
df = get_data()
print(df)
# do_other_stuff()
df.head()

# Good example:
df = get_data()
print(df)

資料準備和模型訓練的重構之旅

在這個章節中,我們將引導您完成重構一個問題多端的notebook的過程。這個notebook執行特徵工程和簡單的分類模型訓練,以預測泰坦尼克號乘客的生存機率。讓我們一步一步地重構這個notebook,從原本長且凌亂的程式碼轉變成模組化、可讀性高且經過測試的解決方案。

首先,讓我們來看看原始的程式碼結構:

def prepare_data_and_train_model():
    # 讀取乘客資料
    passengers = pd.read_csv("./input/train.csv")
    
    # 處理缺失值
    passengers = impute_nans(passengers, 
                              categorical_columns=["Embarked"], 
                              continuous_columns=["Fare", "Age"])
    
    # 新增衍生欄位
    passengers = add_derived_title(passengers)
    passengers = add_is_alone_column(passengers)
    passengers = add_categorical_columns(passengers)
    
    # 刪除不必要的欄位
    passengers = passengers.drop(["Parch", "SibSp", "Name", "Passengerid", 
                                 "Ticket", "Cabin"], axis=1)

這個原始程式碼中,所有的資料準備和模型訓練邏輯都混在一起,缺乏模組化和可讀性。讓我們開始重構這個程式碼,首先將資料準備和模型訓練分開成不同的函式。

資料準備

def prepare_data(passengers_path):
    # 讀取乘客資料
    passengers = pd.read_csv(passengers_path)
    
    # 處理缺失值
    passengers = impute_nans(passengers, 
                              categorical_columns=["Embarked"], 
                              continuous_columns=["Fare", "Age"])
    
    # 新增衍生欄位
    passengers = add_derived_title(passengers)
    passengers = add_is_alone_column(passengers)
    passengers = add_categorical_columns(passengers)
    
    # 刪除不必要的欄位
    passengers = passengers.drop(["Parch", "SibSp", "Name", "Passengerid", 
                                 "Ticket", "Cabin"], axis=1)
    
    return passengers

模型訓練

def train_model(passengers):
    # 進行模型訓練
    model = train_model(passengers)
    return model

現在,讓我們使用這些函式來重構原始的程式碼:

def main():
    # 資料準備
    passengers_path = "./input/train.csv"
    passengers = prepare_data(passengers_path)
    
    # 模型訓練
    model = train_model(passengers)
    
    # 進行預測和評估
    # ...

if __name__ == "__main__":
    main()

這個重構過的程式碼中,資料準備和模型訓練被分開成不同的函式,提高了程式碼的模組化和可讀性。同時,也方便了未來的維護和擴充套件。

將 Jupyter Notebook 轉換為 Python 指令碼並進行重構

步驟 1:執行 Jupyter Notebook

執行 Jupyter Notebook 並確保它能夠正常運作。首先,克隆儲存函式庫並啟動 Jupyter 伺服器:

# 克隆儲存函式庫
git clone https://github.com/username/repository.git

# 執行 Jupyter 伺服器
jupyter notebook

開啟 titanic-notebook-0.ipynb 檔案並執行整個 Notebook。這將訓練多個模型並列印每個模型的指標。

步驟 2:移除 print 陳述式

移除 print 陳述式和圖表,以減少視覺雜亂。這將使 Notebook 從 37 頁減少到 10 頁。比較圖 8-4 和圖 8-5,可以看到移除 print 陳述式後,資料轉換的邏輯變得更加明顯。

步驟 3:列出程式碼異味

在這個步驟中,瀏覽 Notebook 並為每個程式碼異味新增註解。例如,第一個程式碼異味是暴露內部實作。可以將複雜的實作隱藏在一個名稱清晰的函式中(例如,derive_title_from_name())。

步驟 4:將 Notebook 轉換為 Python 檔案

使用以下命令將 Notebook 轉換為 Python 檔案:

jupyter nbconvert --output-dir=./src \
--to=script ./notebooks/titanic-notebook-refactoring-starter.ipynb

步驟 5:執行 Python 指令碼

執行轉換後的 Python 指令碼,以確保它能夠正常運作:

python src/titanic-notebook-refactoring-starter.py

這將幫助您找出任何錯誤或問題,並使您能夠進行重構和最佳化。

內容解密:

上述步驟將幫助您將 Jupyter Notebook 轉換為 Python 指令碼,並進行重構以提高可讀性和可維護性。透過移除 print 陳述式和圖表,您可以減少視覺雜亂並使程式碼更容易理解。列出程式碼異味可以幫助您找出需要重構的區域,並將 Notebook 轉換為 Python 檔案可以使您更容易地執行和測試程式碼。

圖表翻譯:

  flowchart TD
    A[執行 Jupyter Notebook] --> B[移除 print 陳述式]
    B --> C[列出程式碼異味]
    C --> D[將 Notebook 轉換為 Python 檔案]
    D --> E[執行 Python 指令碼]

這個流程圖顯示了將 Jupyter Notebook 轉換為 Python 指令碼並進行重構的步驟。每個步驟都與下一個步驟相連,展示了整個過程的邏輯流程。

使用 Python 和 scikit-learn 進行機器學習模型開發

在進行機器學習模型開發時,瞭解模型的效能和準確度是非常重要的。以下是使用 Python 和 scikit-learn 進行機器學習模型開發的步驟。

步驟 1:準備資料

首先,我們需要準備好資料。這包括載入資料、預處理資料和分割資料等步驟。

# 載入必要的函式庫
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

# 載入 iris 資料集
iris = load_iris()

# 分割資料
X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, test_size=0.2, random_state=42)

步驟 2:訓練模型

接下來,我們需要訓練模型。這包括選擇模型、設定模型引數和訓練模型等步驟。

# 載入必要的函式庫
from sklearn.ensemble import RandomForestClassifier

# 訓練模型
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)

步驟 3:評估模型

評估模型的效能是非常重要的。這包括計算模型的準確度、精確度、召回率和 F1 分數等指標。

# 載入必要的函式庫
from sklearn.metrics import accuracy_score, classification_report

# 預測測試資料
y_pred = model.predict(X_test)

# 評估模型
accuracy = accuracy_score(y_test, y_pred)
print("Accuracy:", accuracy)
print("Classification Report:")
print(classification_report(y_test, y_pred))

步驟 4:最佳化模型

最佳化模型的效能是非常重要的。這包括調整模型引數、使用交叉驗證和早停等技術。

# 載入必要的函式庫
from sklearn.model_selection import GridSearchCV

# 定義模型引數
param_grid = {
    "n_estimators": [10, 50, 100, 200],
    "max_depth": [None, 5, 10, 15]
}

# 最佳化模型
grid_search = GridSearchCV(model, param_grid, cv=5, scoring="accuracy")
grid_search.fit(X_train, y_train)

# 預測測試資料
y_pred = grid_search.predict(X_test)

# 評估模型
accuracy = accuracy_score(y_test, y_pred)
print("Accuracy:", accuracy)

步驟 5:佈署模型

佈署模型是最後一步。這包括儲存模型、載入模型和使用模型進行預測等步驟。

# 載入必要的函式庫
import pickle

# 儲存模型
with open("model.pkl", "wb") as f:
    pickle.dump(model, f)

# 載入模型
with open("model.pkl", "rb") as f:
    loaded_model = pickle.load(f)

# 預測測試資料
y_pred = loaded_model.predict(X_test)

# 評估模型
accuracy = accuracy_score(y_test, y_pred)
print("Accuracy:", accuracy)

內容解密:

以上步驟介紹了使用 Python 和 scikit-learn 進行機器學習模型開發的基本步驟。這包括準備資料、訓練模型、評估模型、最佳化模型和佈署模型等步驟。每一步都非常重要,需要仔細進行。

圖表翻譯:

以下是使用 Mermaid 語法繪製的流程圖:

  flowchart TD
    A[準備資料] --> B[訓練模型]
    B --> C[評估模型]
    C --> D[最佳化模型]
    D --> E[佈署模型]

這個流程圖顯示了機器學習模型開發的基本步驟。每一步都非常重要,需要仔細進行。

重構迭代:一步一步地改善程式碼

在前面的步驟中,我們已經建立了一個安全的測試環境,現在是時候開始重構迭代了。這個過程涉及到反覆地改善程式碼,直到它達到我們想要的標準。

從程式碼重構到機器學習模型開發的全面檢視顯示,提升程式碼品質和可維護性是軟體開發的關鍵目標。本文深入探討了抽象的重要性、介面設計的最佳實務、重構的原則和步驟,以及如何將Jupyter Notebook轉換為更易於維護的Python指令碼。透過特性測試和單元測試的運用,我們確保重構過程不會改變程式碼的行為,同時提升了程式碼的可讀性和可測試性。此外,文章也強調了避免過早抽象化以及程式碼異味的重要性,並提供實務上的重構和範例。對於資料科學家而言,理解如何有效地準備資料、訓練、評估和最佳化模型至關重要,本文也提供了使用Python和scikit-learn進行機器學習模型開發的逐步。展望未來,隨著軟體專案日益複雜,持續整合和自動化測試將扮演更重要的角色,以確保程式碼的品質和可維護性。玄貓認為,掌握這些重構技巧和機器學習開發流程,將有助於開發者建構更穩健、高效且易於維護的軟體系統。