![]() |
![]() |
![]() |
![]() |
本指南示範如何使用 TensorFlow Core 低階 API 搭配 logistic 迴歸 執行 二元分類。它使用 威斯康辛乳癌資料集 進行腫瘤分類。
Logistic 迴歸 是最熱門的二元分類演算法之一。給定一組具備特徵的範例,logistic 迴歸的目標是輸出介於 0 和 1 之間的值,這些值可以解讀為每個範例屬於特定類別的機率。
設定
本教學課程使用 pandas 將 CSV 檔案讀取至 DataFrame、seaborn 繪製資料集中成對關係圖、Scikit-learn 計算混淆矩陣,以及 matplotlib 建立視覺化效果。
pip install -q seaborn
import tensorflow as tf
import pandas as pd
import matplotlib
from matplotlib import pyplot as plt
import seaborn as sns
import sklearn.metrics as sk_metrics
import tempfile
import os
# Preset matplotlib figure sizes.
matplotlib.rcParams['figure.figsize'] = [9, 6]
print(tf.__version__)
# To make the results reproducible, set the random seed value.
tf.random.set_seed(22)
載入資料
接下來,從 UCI 機器學習儲存庫 載入 威斯康辛乳癌資料集。此資料集包含各種特徵,例如腫瘤的半徑、紋理和凹面。
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/wdbc.data'
features = ['radius', 'texture', 'perimeter', 'area', 'smoothness', 'compactness',
'concavity', 'concave_poinits', 'symmetry', 'fractal_dimension']
column_names = ['id', 'diagnosis']
for attr in ['mean', 'ste', 'largest']:
for feature in features:
column_names.append(feature + "_" + attr)
使用 pandas.read_csv
將資料集讀取至 pandas DataFrame
dataset = pd.read_csv(url, names=column_names)
dataset.info()
顯示前五列
dataset.head()
使用 pandas.DataFrame.sample
、pandas.DataFrame.drop
和 pandas.DataFrame.iloc
將資料集分割成訓練集和測試集。請務必從目標標籤分割特徵。測試集用於評估模型對未見資料的泛化能力。
train_dataset = dataset.sample(frac=0.75, random_state=1)
len(train_dataset)
test_dataset = dataset.drop(train_dataset.index)
len(test_dataset)
# The `id` column can be dropped since each row is unique
x_train, y_train = train_dataset.iloc[:, 2:], train_dataset.iloc[:, 1]
x_test, y_test = test_dataset.iloc[:, 2:], test_dataset.iloc[:, 1]
預先處理資料
此資料集包含每個範例收集的 10 項腫瘤測量值的平均值、標準誤和最大值。"diagnosis"
目標欄是一個類別變數,其中 'M'
表示惡性腫瘤,'B'
表示良性腫瘤診斷。此欄需要轉換為數值二元格式才能進行模型訓練。
pandas.Series.map
函式適用於將二元值對應至類別。
資料集也應在使用 tf.convert_to_tensor
函式完成預先處理後轉換為張量。
y_train, y_test = y_train.map({'B': 0, 'M': 1}), y_test.map({'B': 0, 'M': 1})
x_train, y_train = tf.convert_to_tensor(x_train, dtype=tf.float32), tf.convert_to_tensor(y_train, dtype=tf.float32)
x_test, y_test = tf.convert_to_tensor(x_test, dtype=tf.float32), tf.convert_to_tensor(y_test, dtype=tf.float32)
使用 seaborn.pairplot
檢閱訓練集中少數幾對基於平均值的特徵的聯合分佈,並觀察它們與目標的關係
sns.pairplot(train_dataset.iloc[:, 1:6], hue = 'diagnosis', diag_kind='kde');
此成對關係圖示範半徑、周長和面積等特定特徵高度相關。這是預期的,因為腫瘤半徑直接參與周長和面積的計算。此外,請注意,對於許多特徵而言,惡性診斷似乎更為右偏。
務必也檢查整體統計資料。請注意每個特徵涵蓋的數值範圍差異很大。
train_dataset.describe().transpose()[:10]
考慮到範圍不一致,最好將資料標準化,使每個特徵都具有零平均值和單位變異數。此程序稱為 標準化。
class Normalize(tf.Module):
def __init__(self, x):
# Initialize the mean and standard deviation for normalization
self.mean = tf.Variable(tf.math.reduce_mean(x, axis=0))
self.std = tf.Variable(tf.math.reduce_std(x, axis=0))
def norm(self, x):
# Normalize the input
return (x - self.mean)/self.std
def unnorm(self, x):
# Unnormalize the input
return (x * self.std) + self.mean
norm_x = Normalize(x_train)
x_train_norm, x_test_norm = norm_x.norm(x_train), norm_x.norm(x_test)
Logistic 迴歸
在建構 logistic 迴歸模型之前,務必瞭解此方法與傳統線性迴歸的差異。
Logistic 迴歸基礎知識
線性迴歸會傳回其輸入的線性組合;此輸出是無界的。Logistic 迴歸 的輸出範圍在 (0, 1)
之間。對於每個範例,它表示範例屬於正類別的機率。
Logistic 迴歸將傳統線性迴歸的連續輸出 (-∞, ∞)
對應至機率 (0, 1)
。此轉換也是對稱的,因此翻轉線性輸出的符號會產生原始機率的反向值。
讓 \(Y\) 表示屬於類別 1
(腫瘤為惡性) 的機率。所需的對應可以透過將線性迴歸輸出解讀為屬於類別 1
與類別 0
的 對數優勢比 來達成
\[\ln(\frac{Y}{1-Y}) = wX + b\]
透過設定 \(wX + b = z\),即可求解 \(Y\) 的此方程式
\[Y = \frac{e^{z} }{1 + e^{z} } = \frac{1}{1 + e^{-z} }\]
運算式 \(\frac{1}{1 + e^{-z} }\) 稱為 Sigmoid 函數 \(\sigma(z)\)。因此,logistic 迴歸的方程式可以寫成 \(Y = \sigma(wX + b)\)。
本教學課程中的資料集處理的是高維度特徵矩陣。因此,上述方程式必須以矩陣向量形式重寫如下
\[{\mathrm{Y} } = \sigma({\mathrm{X} }w + b)\]
其中
- \(\underset{m\times 1}{\mathrm{Y} }\):目標向量
- \(\underset{m\times n}{\mathrm{X} }\):特徵矩陣
- \(\underset{n\times 1}w\):權重向量
- \(b\):偏差
- \(\sigma\):套用至輸出向量每個元素的 Sigmoid 函數
首先視覺化 Sigmoid 函數,此函數會轉換線性輸出 (-∞, ∞)
,使其落在 0
和 1
之間。Sigmoid 函數可在 tf.math.sigmoid
中取得。
x = tf.linspace(-10, 10, 500)
x = tf.cast(x, tf.float32)
f = lambda x : (1/20)*x + 0.6
plt.plot(x, tf.math.sigmoid(x))
plt.ylim((-0.1,1.1))
plt.title("Sigmoid function");
對數損失函數
對數損失 或二元交叉熵損失是具有 logistic 迴歸的二元分類問題的理想損失函數。對於每個範例,對數損失會量化預測機率與範例真實值之間的相似性。它由以下方程式決定
\[L = -\frac{1}{m}\sum_{i=1}^{m}y_i\cdot\log(\hat{y}_i) + (1- y_i)\cdot\log(1 - \hat{y}_i)\]
其中
- \(\hat{y}\):預測機率向量
- \(y\):真實目標向量
您可以使用 tf.nn.sigmoid_cross_entropy_with_logits
函式計算對數損失。此函式會自動將 Sigmoid 啟用套用至迴歸輸出
def log_loss(y_pred, y):
# Compute the log loss function
ce = tf.nn.sigmoid_cross_entropy_with_logits(labels=y, logits=y_pred)
return tf.reduce_mean(ce)
梯度下降更新規則
TensorFlow Core API 透過 tf.GradientTape
支援自動微分。如果您對 logistic 迴歸 梯度更新 背後的數學原理感到好奇,以下是簡短說明
在上述對數損失方程式中,回想一下每個 \(\hat{y}_i\) 都可以根據輸入重寫為 \(\sigma({\mathrm{X_i} }w + b)\)。
目標是找到能最小化對數損失的 \(w^*\) 和 \(b^*\)
\[L = -\frac{1}{m}\sum_{i=1}^{m}y_i\cdot\log(\sigma({\mathrm{X_i} }w + b)) + (1- y_i)\cdot\log(1 - \sigma({\mathrm{X_i} }w + b))\]
透過取得 \(L\) 相對於 \(w\) 的梯度,您會得到以下結果
\[\frac{\partial L}{\partial w} = \frac{1}{m}(\sigma({\mathrm{X} }w + b) - y)X\]
透過取得 \(L\) 相對於 \(b\) 的梯度,您會得到以下結果
\[\frac{\partial L}{\partial b} = \frac{1}{m}\sum_{i=1}^{m}\sigma({\mathrm{X_i} }w + b) - y_i\]
現在,建構 logistic 迴歸模型。
class LogisticRegression(tf.Module):
def __init__(self):
self.built = False
def __call__(self, x, train=True):
# Initialize the model parameters on the first call
if not self.built:
# Randomly generate the weights and the bias term
rand_w = tf.random.uniform(shape=[x.shape[-1], 1], seed=22)
rand_b = tf.random.uniform(shape=[], seed=22)
self.w = tf.Variable(rand_w)
self.b = tf.Variable(rand_b)
self.built = True
# Compute the model output
z = tf.add(tf.matmul(x, self.w), self.b)
z = tf.squeeze(z, axis=1)
if train:
return z
return tf.sigmoid(z)
為了驗證,請確保未訓練模型針對訓練資料的小子集輸出 (0, 1)
範圍內的值。
log_reg = LogisticRegression()
y_pred = log_reg(x_train_norm[:5], train=False)
y_pred.numpy()
接下來,編寫準確度函式,以計算訓練期間正確分類的比例。為了從預測機率中擷取分類,請設定一個閾值,高於該閾值的所有機率都屬於類別 1
。這是一個可設定的超參數,可以預設設為 0.5
。
def predict_class(y_pred, thresh=0.5):
# Return a tensor with `1` if `y_pred` > `0.5`, and `0` otherwise
return tf.cast(y_pred > thresh, tf.float32)
def accuracy(y_pred, y):
# Return the proportion of matches between `y_pred` and `y`
y_pred = tf.math.sigmoid(y_pred)
y_pred_class = predict_class(y_pred)
check_equal = tf.cast(y_pred_class == y,tf.float32)
acc_val = tf.reduce_mean(check_equal)
return acc_val
訓練模型
使用迷你批次進行訓練可提供記憶體效率和更快的收斂速度。tf.data.Dataset
API 具有用於批次處理和隨機排序的實用函式。API 可讓您從簡單、可重複使用的部分建構複雜的輸入管線。
batch_size = 64
train_dataset = tf.data.Dataset.from_tensor_slices((x_train_norm, y_train))
train_dataset = train_dataset.shuffle(buffer_size=x_train.shape[0]).batch(batch_size)
test_dataset = tf.data.Dataset.from_tensor_slices((x_test_norm, y_test))
test_dataset = test_dataset.shuffle(buffer_size=x_test.shape[0]).batch(batch_size)
現在為 logistic 迴歸模型編寫訓練迴圈。迴圈利用對數損失函數及其相對於輸入的梯度,以便迭代更新模型的參數。
# Set training parameters
epochs = 200
learning_rate = 0.01
train_losses, test_losses = [], []
train_accs, test_accs = [], []
# Set up the training loop and begin training
for epoch in range(epochs):
batch_losses_train, batch_accs_train = [], []
batch_losses_test, batch_accs_test = [], []
# Iterate over the training data
for x_batch, y_batch in train_dataset:
with tf.GradientTape() as tape:
y_pred_batch = log_reg(x_batch)
batch_loss = log_loss(y_pred_batch, y_batch)
batch_acc = accuracy(y_pred_batch, y_batch)
# Update the parameters with respect to the gradient calculations
grads = tape.gradient(batch_loss, log_reg.variables)
for g,v in zip(grads, log_reg.variables):
v.assign_sub(learning_rate * g)
# Keep track of batch-level training performance
batch_losses_train.append(batch_loss)
batch_accs_train.append(batch_acc)
# Iterate over the testing data
for x_batch, y_batch in test_dataset:
y_pred_batch = log_reg(x_batch)
batch_loss = log_loss(y_pred_batch, y_batch)
batch_acc = accuracy(y_pred_batch, y_batch)
# Keep track of batch-level testing performance
batch_losses_test.append(batch_loss)
batch_accs_test.append(batch_acc)
# Keep track of epoch-level model performance
train_loss, train_acc = tf.reduce_mean(batch_losses_train), tf.reduce_mean(batch_accs_train)
test_loss, test_acc = tf.reduce_mean(batch_losses_test), tf.reduce_mean(batch_accs_test)
train_losses.append(train_loss)
train_accs.append(train_acc)
test_losses.append(test_loss)
test_accs.append(test_acc)
if epoch % 20 == 0:
print(f"Epoch: {epoch}, Training log loss: {train_loss:.3f}")
效能評估
觀察模型損失和準確度隨時間的變化。
plt.plot(range(epochs), train_losses, label = "Training loss")
plt.plot(range(epochs), test_losses, label = "Testing loss")
plt.xlabel("Epoch")
plt.ylabel("Log loss")
plt.legend()
plt.title("Log loss vs training iterations");
plt.plot(range(epochs), train_accs, label = "Training accuracy")
plt.plot(range(epochs), test_accs, label = "Testing accuracy")
plt.xlabel("Epoch")
plt.ylabel("Accuracy (%)")
plt.legend()
plt.title("Accuracy vs training iterations");
print(f"Final training log loss: {train_losses[-1]:.3f}")
print(f"Final testing log Loss: {test_losses[-1]:.3f}")
print(f"Final training accuracy: {train_accs[-1]:.3f}")
print(f"Final testing accuracy: {test_accs[-1]:.3f}")
模型在訓練資料集中分類腫瘤時展現出高準確度和低損失,並且也能很好地泛化到未見的測試資料。若要更進一步,您可以探索錯誤率,以提供超出整體準確度分數的更多深入分析。二元分類問題最熱門的兩種錯誤率為偽陽率 (FPR) 和偽陰率 (FNR)。
針對此問題,FPR 是實際為良性腫瘤的腫瘤中,惡性腫瘤預測的比例。相反地,FNR 是實際為惡性腫瘤的腫瘤中,良性腫瘤預測的比例。
使用 sklearn.metrics.confusion_matrix
計算混淆矩陣,以評估分類的準確度,並使用 matplotlib 顯示矩陣
def show_confusion_matrix(y, y_classes, typ):
# Compute the confusion matrix and normalize it
plt.figure(figsize=(10,10))
confusion = sk_metrics.confusion_matrix(y.numpy(), y_classes.numpy())
confusion_normalized = confusion / confusion.sum(axis=1, keepdims=True)
axis_labels = range(2)
ax = sns.heatmap(
confusion_normalized, xticklabels=axis_labels, yticklabels=axis_labels,
cmap='Blues', annot=True, fmt='.4f', square=True)
plt.title(f"Confusion matrix: {typ}")
plt.ylabel("True label")
plt.xlabel("Predicted label")
y_pred_train, y_pred_test = log_reg(x_train_norm, train=False), log_reg(x_test_norm, train=False)
train_classes, test_classes = predict_class(y_pred_train), predict_class(y_pred_test)
show_confusion_matrix(y_train, train_classes, 'Training')
show_confusion_matrix(y_test, test_classes, 'Testing')
觀察錯誤率測量值,並解讀其在此範例背景中的意義。在許多醫療檢測研究 (例如癌症檢測) 中,具有高偽陽率以確保低偽陰率是完全可以接受的,而且實際上是鼓勵的,因為錯過惡性腫瘤診斷 (偽陰性) 的風險遠比將良性腫瘤誤分類為惡性腫瘤 (偽陽性) 來得嚴重。
為了控制 FPR 和 FNR,請嘗試在分類機率預測之前變更閾值超參數。較低的閾值會增加模型做出惡性腫瘤分類的整體機率。這不可避免地會增加偽陽性和 FPR 的數量,但也有助於減少偽陰性和 FNR 的數量。
儲存模型
首先建立一個匯出模組,以接收原始資料並執行下列作業
- 標準化
- 機率預測
- 類別預測
class ExportModule(tf.Module):
def __init__(self, model, norm_x, class_pred):
# Initialize pre- and post-processing functions
self.model = model
self.norm_x = norm_x
self.class_pred = class_pred
@tf.function(input_signature=[tf.TensorSpec(shape=[None, None], dtype=tf.float32)])
def __call__(self, x):
# Run the `ExportModule` for new data points
x = self.norm_x.norm(x)
y = self.model(x, train=False)
y = self.class_pred(y)
return y
log_reg_export = ExportModule(model=log_reg,
norm_x=norm_x,
class_pred=predict_class)
如果您想儲存目前狀態的模型,可以使用 tf.saved_model.save
函式。若要載入已儲存的模型並進行預測,請使用 tf.saved_model.load
函式。
models = tempfile.mkdtemp()
save_path = os.path.join(models, 'log_reg_export')
tf.saved_model.save(log_reg_export, save_path)
log_reg_loaded = tf.saved_model.load(save_path)
test_preds = log_reg_loaded(x_test)
test_preds[:10].numpy()
結論
本筆記本介紹了一些處理 logistic 迴歸問題的技巧。以下是一些可能有幫助的訣竅
- 可以使用 TensorFlow Core API 建構具有高度可設定性的機器學習工作流程
- 分析錯誤率是深入瞭解分類模型效能 (超出其整體準確度分數) 的絕佳方式。
- 過度擬合是 logistic 迴歸模型的另一個常見問題,儘管它在本教學課程中並非問題。請造訪過度擬合和欠擬合教學課程,以取得更多相關協助。
如需使用 TensorFlow Core API 的更多範例,請查看指南。如果您想進一步瞭解載入和準備資料,請參閱關於圖片資料載入或CSV 資料載入的教學課程。