用於二元分類的 Logistic 迴歸搭配核心 API

在 TensorFlow.org 上檢視 在 Google Colab 中執行 在 GitHub 上檢視原始碼 下載筆記本

本指南示範如何使用 TensorFlow Core 低階 API 搭配 logistic 迴歸 執行 二元分類。它使用 威斯康辛乳癌資料集 進行腫瘤分類。

Logistic 迴歸 是最熱門的二元分類演算法之一。給定一組具備特徵的範例,logistic 迴歸的目標是輸出介於 0 和 1 之間的值,這些值可以解讀為每個範例屬於特定類別的機率。

設定

本教學課程使用 pandas 將 CSV 檔案讀取至 DataFrameseaborn 繪製資料集中成對關係圖、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.samplepandas.DataFrame.droppandas.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 函數,此函數會轉換線性輸出 (-∞, ∞),使其落在 01 之間。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 資料載入的教學課程。