使用核心 API 进行二元分类的逻辑回归

在 TensorFlow.org 上查看 在 Google Colab 中运行 在 GitHub 上查看源代码 下载笔记本

本指南演示了如何使用 TensorFlow 核心低级 API 使用 二元分类 进行 逻辑回归。它使用 威斯康星乳腺癌数据集 进行肿瘤分类。

逻辑回归 是最流行的二元分类算法之一。给定一组具有特征的示例,逻辑回归的目标是输出 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.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)

逻辑回归

在构建逻辑回归模型之前,了解该方法与传统线性回归相比的差异至关重要。

逻辑回归基础

线性回归返回其输入的线性组合;此输出是无界的。 逻辑回归 的输出在 (0, 1) 范围内。对于每个样本,它表示该样本属于类的概率。

逻辑回归将传统线性回归的连续输出 (-∞, ∞) 映射到概率 (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} }\) 称为 S 型函数 \(\sigma(z)\)。因此,逻辑回归的方程可以写成 \(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\): 应用于输出向量每个元素的 S 型函数

首先可视化 S 型函数,它将线性输出 (-∞, ∞) 转换为介于 01 之间的值。S 型函数在 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");

对数损失函数

对数损失 或二元交叉熵损失是逻辑回归二元分类问题的理想损失函数。对于每个样本,对数损失量化了预测概率与样本真值之间的相似性。它由以下公式确定

\[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 函数来计算对数损失。此函数会自动将 S 型激活应用于回归输出。

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 进行自动微分。如果您对逻辑回归 梯度更新 背后的数学原理感到好奇,这里简要说明一下。

在对数损失的上述公式中,请记住每个 \(\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))\]

通过对 \(w\) 求 \(L\) 的梯度,得到以下结果

\[\frac{\partial L}{\partial w} = \frac{1}{m}(\sigma({\mathrm{X} }w + b) - y)X\]

通过对 \(b\) 求 \(L\) 的梯度,得到以下结果

\[\frac{\partial L}{\partial b} = \frac{1}{m}\sum_{i=1}^{m}\sigma({\mathrm{X_i} }w + b) - y_i\]

现在,构建逻辑回归模型。

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)

现在为逻辑回归模型编写一个训练循环。该循环利用对数损失函数及其相对于输入的梯度来迭代更新模型的参数。

# 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()

结论

本笔记本介绍了一些处理逻辑回归问题的技术。以下是一些可能有所帮助的其他提示

  • TensorFlow Core API 可用于构建具有高度可配置性的机器学习工作流。
  • 分析错误率是了解分类模型性能(超出其总体准确率分数)的绝佳方法。
  • 过拟合是逻辑回归模型的另一个常见问题,尽管在本教程中这不是问题。请访问 过拟合和欠拟合 教程以获取更多帮助。

有关使用 TensorFlow Core API 的更多示例,请查看 指南。如果您想了解有关加载和准备数据的更多信息,请查看有关 图像数据加载CSV 数据加载 的教程。