TabTransformer 架构 — 作者提供的图片
想象一下,你正在尝试理解一个复杂的故事。你不会只孤立地阅读单个句子,对吧?你会关注句子之间的联系,事件的流程。这本质上就是 TabTransformer 对数据的作用。
TabTransformer 是一种专为表格数据设计的深度学习架构,由 Xin Huang、Ashish Khetan、Milan Cvitkovic 和 Zohar Karnin 在他们 2020 年的研究论文“ TabTransformer:使用上下文嵌入的表格数据建模”(Huang 等人,2020 年)中提出。
它通过引入一种理解数据的新方法彻底改变了表格数据建模:上下文嵌入。神经网络架构生成这些称为 Transformers 的上下文嵌入,其灵感来源于人类处理语言的方式。
这种模型架构超越了传统的准确性、稳健性和可解释性方法。它释放了表格数据的潜力,使我们能够做出更准确的预测,构建更具弹性的模型,并从我们收集的数据中获得更深入的见解。让我们深入研究这种革命性方法背后的数学原理。
1:TabTransformer:一种新方法
想象一下,Transformers是一群高度专业的侦探,每个人都有自己的专业领域。他们可以查看一条信息,例如客户的年龄,然后分析该年龄与其他因素(例如他们的收入、订阅计划和过去活动)的关系。这种相互关联的分析使他们能够考虑到所有相关因素,详细了解客户。
TabTransformer 使用这些Transformer为数据集中的每个分类特征创建“上下文嵌入”。这些嵌入不仅仅是特征的简单表示;它们还捕捉了该特征与数据中其他特征的细微关系。
例如,假设您的一项功能是“订阅计划”,其中包含“基本”、“高级”和“企业”等选项。传统方法可能只是为每个计划分配一个数值。然而,TabTransformer 会考虑“订阅计划”与其他功能(如“收入”、“年龄”和“过去活动”)的关系,从而更丰富地呈现该计划。这就像了解“高级”订阅者比“基本”订阅者更有可能年龄较大且收入较高。
这些上下文嵌入是 TabTransformer 成功的基础。它们提供了对数据的更全面理解,使模型能够做出更准确的预测并更深入地了解数据中的关系。
2:TabTransformer 背后的数学原理
让我们更深入地了解 TabTransformer 的技术核心。您可以将其视为深入了解该工具的工作原理。我们将在本节中介绍数学和 Python 示例。
假设我们的数据拼图由一张表表示,其中每行代表一位客户,每列代表年龄、收入或购买历史等特征。我们可以使用向量 x以数学方式表示此表。此向量包含有关单个客户的所有信息。
输入向量 — 作者提供的图片
此向量中的每个元素x_i代表客户的一个特征。我们可以在 Python 中使用以下代码来表示:
import numpy as np
# customer data
customer_age = 35
customer_income = 60000
customer_last_purchase = '2023-10-27'
# input vector x
x = np.array([customer_age, customer_income, customer_last_purchase])
乍一看,这似乎是客户详细信息的普通集合。但如果我告诉你,我们可以塑造这些数据,逐步转换它,每一层都会改变我们理解它的方式,你会怎么想?这正是TabTransformer所做的,动态转换特征。
在我们的案例中,我们特别感兴趣的是如何动态调整年龄、收入或上次购买日期等特征。这些特征不会保持不变——它们会随着我们将它们传递到不同的转换层而演变。
但在讨论之前,我们先来谈谈如何处理日期等非数字数据。由于机器学习模型不能直接理解日期,因此我们需要将其转换为数字。我们可以将上次购买日期转换为自参考日期(例如 2000 年 1 月 1 日)以来的天数来表示它:
from datetime import datetime
# function to convert date string to a numerical value (e.g., days since a reference date)
def encode_date(date_str):
reference_date = datetime(2000, 1, 1)
date_obj = datetime.strptime(date_str, '%Y-%m-%d')
return (date_obj - reference_date).days
现在,我们终于可以创建输入向量 x,这次是完全数字的。注意日期是如何转换成数字的:
customer_last_purchase = encode_date('2023-10-27') # Convert date to a numerical value
x = np.array([customer_age, customer_income, customer_last_purchase])
print(x)
# Output
# [35 60000 8700] # 8700 is our encoded date
现在,TabTransformer 使用一系列层处理这些数据,每个层将输入向量x转换为一个新向量。
让我们关注单个层,用数字q表示。在此层中,连接两个特征的每个边都有一个可学习的单变量函数。此函数表示应用于特定特征x_i的变换。这些函数被参数化为样条函数——分段多项式函数,允许根据数据进行动态调整。
在查看单变量函数的公式之前,让我们先澄清一下前面这段文字想要表达的意思。假设你正在建立一个模型来预测客户将在你的网站上花多少钱。你有他们的年龄、收入和过去的购买历史等信息。
- Layer:将 TabTransformer 中的每一层视为此预测过程中的不同阶段。我们将重点关注标记为“q”的单个阶段。
- Edges:在这个阶段,每条边连接两条信息,如年龄和收入。
- Univariate Function:每条边都有一个特殊规则,称为“单变量函数”。该规则采用特定的信息(如年龄),并根据其与其他相关部分(收入)的关系进行更改。
- Splines:这些函数很特殊,就像灵活的尺子。它们可以弯曲并适应数据中的独特模式。想象一下,你有一把尺子,它可以改变形状以完美贴合木头的曲线。样条函数也类似,它们可以调整形状以最好地捕捉特征之间的关系,从而使模型更加准确。
因此,在我们的示例中,连接年龄和收入的函数可能会根据数据以某种方式弯曲,从而了解到收入较高的年轻客户往往会花费更多。这种动态调整使 TabTransformer 如此强大,使其能够适应数据的细微差别并做出更准确的预测。
现在让我们看看这个函数是什么样的:
单变量函数 — 作者提供的图片
在哪里:
- x_i是放入框中的特定信息(例如,顾客的年龄)。
- q代表层数(可以将其视为配方中的步骤)。
- p表示该层内的特定边缘(框位于哪条边缘上)。
此函数获取客户的信息x_i,并根据该特定框定义的规则对其进行修改。在 Python 中,我们可以像这样实现这种转换:
def univariate_function(x, control_points, degree):
num_control_points = len(control_points)
if num_control_points < degree + 1:
raise ValueError(f"Need at least {degree + 1} control points for degree {degree}")
knot_vector = np.concatenate(([0] * degree, np.arange(num_control_points - degree + 1), [num_control_points - degree] * degree))
spline = BSpline(knot_vector, control_points, degree)
return spline(x)
让我们逐步分解一下:
num_control_points = len(control_points)
这条线只是计算提供的控制点的数量。控制点就像定义样条线形状的锚点。
if num_control_points < degree + 1
此检查可确保您拥有足够的控制点来达到样条线的指定次数。次数决定了样条线的平滑度。次数越高,曲线越平滑。要创建有效的样条线,您至少需要次数 + 1 个控制点。
knot_vector = np.concatenate(([0] * degree, np.arange(num_control_points - degree + 1), [num_control_points - degree] * degree))
这里,我们定义了结点向量,它对于 B 样条线至关重要。结点向量决定了样条线的多项式部分连接的位置。此结点向量的具体结构对于创建具有固定端点的钳制 B 样条线很常见。
spline = BSpline(knot_vector, control_points, degree)
这将创建 B 样条线对象。它将节点向量、控制点和度数作为输入。
return spline(x)
最后,我们评估给定输入 x 处的样条曲线。它本质上是计算 B 样条函数在 x 点的值。
因此,这些 B 样条函数是分段多项式函数,可以近似任何连续函数。想象一下在图形上绘制的一条线,但它不是直线,而是由几条连接在一起的平滑曲线组成。这类似于样条线。例如,让我们尝试在随机数据上使用上述函数并绘制它:
control_points = np.array([0, 1, 2, 3, 4])
degree = 3 # degree of the spline
# Generate a range of parameter values
x_values = np.linspace(0, len(control_points) - degree, 100)
# Evaluate the B-spline at these parameter values
y_values = univariate_function(x_values, control_points, degree)
# Plot the B-spline
plt.plot(x_values, y_values, label='B-spline')
plt.plot(np.arange(len(control_points)), control_points, 'o--', label='Control Points')
plt.legend()
plt.title('B-spline Curve')
plt.xlabel('x')
plt.ylabel('y')
plt.grid(True)
plt.show()
B 样条图 — 作者提供的图片
B 样条函数由一组控制点和一个度数定义。控制点决定样条的形状,度数决定曲线的平滑度。
现在,想象一下将这些框排成一排。此公式表示框的单层,它获取原始客户信息x并使用相应的函数修改其每一部分 ( x_i )。然后,将层q处的输入向量x的变换计算为这些单变量函数的组合:
图层转换 — 图片来自作者
- Φ_q(x)是将特定层q中的框应用于客户信息x的整个过程。
- 层边缘p上的每个框都有不同的函数?_{q,p}(x_i)
这意味着输入向量中的每个特征都通过层中其对应的函数进行转换。
到目前为止,我们已经探索了如何转换单个特征,但如何学习特征之间的关系呢?这就是多头注意力的作用所在。
class MultiHeadAttention:
def __init__(self, num_hiddens, num_heads, bias=False):
self.num_heads = num_heads
self.num_hiddens = num_hiddens
self.d_k = self.d_v = num_hiddens // num_heads
# Weights for query, key, value, and output projections
self.W_q = np.random.rand(num_hiddens, num_hiddens)
self.W_k = np.random.rand(num_hiddens, num_hiddens)
self.W_v = np.random.rand(num_hiddens, num_hiddens)
self.W_o = np.random.rand(num_hiddens, num_hiddens)
if bias:
self.b_q = np.random.rand(num_hiddens)
self.b_k = np.random.rand(num_hiddens)
self.b_v = np.random.rand(num_hiddens)
self.b_o = np.random.rand(num_hiddens)
else:
self.b_q = self.b_k = self.b_v = self.b_o = np.zeros(num_hiddens)
def __call__(self, queries, keys, values, valid_lens=None):
return self.forward(queries, keys, values, valid_lens)
def transpose_qkv(self, X):
"""
Transposition for batch processing.
Transpose the Q, K, V matrices for multi-head attention.
"""
X = X.reshape(X.shape[0], X.shape[1], self.num_heads, -1)
X = X.transpose(0, 2, 1, 3)
return X.reshape(-1, X.shape[2], X.shape[3])
def transpose_output(self, X):
"""
Transposition for output.
Combines the multiple heads back into the original format.
"""
X = X.reshape(-1, self.num_heads, X.shape[1], X.shape[2])
X = X.transpose(0, 2, 1, 3)
return X.reshape(X.shape[0], X.shape[1], -1)
def scaled_dot_product_attention(self, Q, K, V, valid_lens):
"""
Scaled dot product attention mechanism.
"""
d_k = Q.shape[-1]
# Calculate the attention scores
scores = np.matmul(Q, K.transpose(0, 2, 1)) / np.sqrt(d_k)
if valid_lens is not None:
mask = np.arange(scores.shape[-1]) < valid_lens[:, None]
scores = np.where(mask[:, None, :], scores, -np.inf)
# Softmax to get attention weights
attention_weights = np.exp(scores - np.max(scores, axis=-1, keepdims=True))
attention_weights /= attention_weights.sum(axis=-1, keepdims=True)
return np.matmul(attention_weights, V)
def forward(self, queries, keys, values, valid_lens):
# Transform the queries, keys, and values into multiple heads
queries = self.transpose_qkv(np.dot(queries, self.W_q) + self.b_q)
keys = self.transpose_qkv(np.dot(keys, self.W_k) + self.b_k)
values = self.transpose_qkv(np.dot(values, self.W_v) + self.b_v)
# If valid lengths are provided, repeat them across heads
if valid_lens is not None:
valid_lens = np.repeat(valid_lens, self.num_heads, axis=0)
# Apply scaled dot product attention
output = self.scaled_dot_product_attention(queries, keys, values, valid_lens)
# Concatenate the outputs from all heads
output_concat = self.transpose_output(output)
return np.dot(output_concat, self.W_o) + self.b_o
现在,让我们将多头注意力应用到我们的客户数据上:
# Example usage for a transformer layer (categorical features handled via embeddings)
num_features = len(x)
num_layers = 3 # Number of layers in the model
embedding_dim = 16 # Increased dimension for embedding
num_heads = 4 # Number of attention heads
categorical_features = [2] # Assume the third feature is categorical (encoded date)
# Layer transformation function
def layer_transform(x):
x_transformed = np.zeros_like(x)
for p in range(num_features):
if p in categorical_features:
# Create a tensor of the correct shape for the attention layer
cat_feature_tensor = np.random.randn(1, 1, embedding_dim) # Shape: (batch_size, seq_len, embedding_dim)
mha = MultiHeadAttention(embedding_dim, num_heads)
transformed = mha(cat_feature_tensor, cat_feature_tensor, cat_feature_tensor)
x_transformed[p] = transformed.mean() # Simplify by averaging the output
else:
# Ensure control points match required size
control_points = np.linspace(0, 1, degree + 2) # Generate control points if needed
x_transformed[p] = univariate_function(x[p], control_points, degree)
return x_transformed
# Example layer transformation
x_transformed = layer_transform(x)
# Print the transformed input
print(f"Transformed input: {x_transformed}")
这里:
- num_features = len(x):这只是计算 x 中有多少个特征(或数据列)。将 x 视为数据点列表,这会告诉我们有多少个数据点。
- num_layers = 3:将模型中的层数设置为 3。您可以将其视为按一系列步骤处理数据的次数。
- embedding_dim = 16:处理分类(非数字)特征时,这会增加数据的维度。如果分类特征由单个数字表示,它将扩展为 16 个数字,从而捕获更多信息。
- num_heads = 4 :设置处理分类特征时使用的“注意力头”的数量。每个注意力头将关注数据的不同部分。
- categorical_features = [2]:这假设第三个特征(记住,在编程中我们从 0 开始计数)是分类的。在此示例中,它将已编码为数字的日期视为分类特征。
函数layer_transform(x)接收输入 x,并根据特征是否为分类特征对其进行转换。如果特征在categorical_features列表中,它会创建一个随机张量(数字网格),其形状是使用注意力机制所必需的。然后,它将多头注意力机制应用于此张量。应用注意力机制后,结果被平均(简化)并用于替换原始特征值。
如果特征不是分类的,则会生成一组控制点。这些只是有助于重塑或处理特征的参考点。univariate_function应用于该特征,以更适合数据的方式对其进行转换。
可以将注意力视为模型决定哪些特征重要以及这些特征如何相互关联的一种方式。在我们的案例中,我们可以使用多头注意力来学习客户特征(如年龄、收入和购买历史)之间的复杂依赖关系。
TabTransformer 的最终输出是所有这些层转换的组合。它代表了对数据的最终理解,捕捉了特征之间的复杂关系。
输出 — 作者提供的图片
这里:
- x(Q)表示最后一层的输出,该输出经过了每一层的所有变换。
- g_ψ是最终函数,它使用修改后的客户信息x(Q)进行预测,例如客户是否可能流失。
TabTransformer 的核心功能在于它能够学习这些动态函数。每个,()(我们的单变量函数)在训练过程中都会根据数据进行调整,从而使模型能够适应数据集中存在的特定模式和关系。这种灵活性使 TabTransformer 有别于依赖固定激活函数的传统模型(如 MLP)。我们稍后将更详细地探讨这些优势。
3:半监督学习的预训练
假设您正在尝试建立一个模型来预测哪些客户可能会流失(停止使用您的服务)。您拥有大量历史数据,但其中只有一小部分数据具有实际的流失标签。您如何使用这些大量未标记的数据来帮助您的模型更好地学习?
这就是预训练的作用所在。它就像是给你的模型一个良好的开端,让它在看到任何标签之前学习数据的基本结构。
TabTransformer 采用两阶段方法:
- 预训练:该模型在大量未标记数据集上进行训练。可以将其想象为让您的模型探索一个巨大的图书馆,在开始寻找特定书名之前,了解其组织结构和不同书籍之间的关系。
- 微调:一旦模型很好地理解了数据的结构,您就可以向其输入标记数据并微调其预测。现在,这就像拥有一位了解图书馆布局并能快速找到您需要的特定书籍的图书管理员。
TabTransformer 主要使用两种预训练方法:
- 掩蔽语言模型 (MLM):想象一下有人遮盖了句子中的一些单词,并要求您根据周围上下文猜测它们是什么。MLM 对 TabTransformer 来说就像这样——它遮盖了未标记数据中的一些特征,并训练模型来预测这些缺失值。这有助于模型学习识别特征之间的模式和依赖关系。
- 替换标记检测 (RTD):在此方法中,TabTransformer 将特征的值替换为同一列中的随机值。然后,它会训练模型以检测特征是否已被替换。这进一步增强了模型对特征关系和上下文的理解。
这些预训练技术让 TabTransformer 即使在没有标记数据的情况下也能学习强大的上下文嵌入。这使其在半监督学习场景中具有显著优势,因为在半监督学习场景中,标记数据稀缺,但未标记数据丰富。
4:Python 中的 TabTransformer
现在,让我们看看TabTransformer 的实际应用!我们将使用一个著名的数据集 — KDD'99 数据集,该数据集广泛用于异常检测任务。在此示例中,我们将使用 pytorch-tabular 库构建 TabTransformer,这是一个简化表格模型实现的强大工具。
该数据集包含各种网络连接记录,我们的任务是分类连接是正常还是代表攻击。特征包括分类变量(例如protocol_type)和连续变量(例如duration和src_bytes)。您可以从 Kaggle 下载数据集:
1.数据集准备
我们将首先加载数据集、编码标签以及预处理分类和连续特征。
import pandas as pd
pd.set_option('display.max_columns', None)
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
# Load the dataset
df = pd.read_csv('data/kddcup.data_10_percent_corrected', header=None)
# Define column names based on the KDD'99 dataset
columns = [
"duration", "protocol_type", "service", "flag", "src_bytes", "dst_bytes",
"land", "wrong_fragment", "urgent", "hot", "num_failed_logins", "logged_in",
"num_compromised", "root_shell", "su_attempted", "num_root", "num_file_creations",
"num_shells", "num_access_files", "num_outbound_cmds", "is_host_login",
"is_guest_login", "count", "srv_count", "serror_rate", "srv_serror_rate",
"rerror_rate", "srv_rerror_rate", "same_srv_rate", "diff_srv_rate",
"srv_diff_host_rate", "dst_host_count", "dst_host_srv_count", "dst_host_same_srv_rate",
"dst_host_diff_srv_rate", "dst_host_same_src_port_rate", "dst_host_srv_diff_host_rate",
"dst_host_serror_rate", "dst_host_srv_serror_rate", "dst_host_rerror_rate",
"dst_host_srv_rerror_rate", "label"
]
df.columns = columns
# Encode the labels (Normal or Attack types)
df['label'] = LabelEncoder().fit_transform(df['label'])
# Encode categorical columns
categorical_columns = ['protocol_type', 'service', 'flag']
for col in categorical_columns:
df[col] = LabelEncoder().fit_transform(df[col])
num_cols = [col for col in df.columns if col not in categorical_columns + ['label']]
# Define the target and feature columns
target = 'label'
features = df.drop(columns=[target])
# Split the dataset into train and test
train_df, test_df = train_test_split(df, test_size=0.2, random_state=42)
# Convert to PyTorch Tabular compatible format
train_df.reset_index(drop=True, inplace=True)
test_df.reset_index(drop=True, inplace=True)
在此步骤中,我们:
- 加载KDD'99数据集。
- 使用LabelEncoder对目标标签(攻击与正常)进行编码。
- 预处理分类列(protocol_type,flag,service)以适合我们的模型。
- 将数据分成训练集和测试集,确保所有内容结构良好且可用于TabTransformer模型。
在任何 ML 任务中我们都可能会进行正常的数据预处理。
2.配置TabTransformer
现在我们的数据集已经准备好了,让我们配置我们的TabTransformer模型。我将尽可能使用此模型的默认设置来简化练习。
TabTransformer可以通过嵌入和注意力机制处理连续数据和分类数据。我们将为此任务配置数据、模型和训练器。
from pytorch_tabular import TabularModel
from pytorch_tabular.config import DataConfig, TrainerConfig, OptimizerConfig
from pytorch_tabular.models import TabTransformerConfig
# Define the configurations
data_config = DataConfig(
target=['label'],
continuous_cols=num_cols,
categorical_cols=categorical_columns
)
model_config = TabTransformerConfig(
task="classification",
metrics=["accuracy"]
)
trainer_config = TrainerConfig(
max_epochs=10
)
optimizer_config = OptimizerConfig()
以下是具体情况:
- DataConfig:我们定义连续列(num_cols)、分类列(categorical_columns)和目标(label)。
- TabTransformerConfig:我们将任务设置为"classification"并指定accuracy为评估指标。
- TrainerConfig:我们将训练限制为 10 个时期。
- OptimizerConfig:处理用于训练模型的优化器配置。
3.模型训练
接下来,我们初始化模型并在我们的数据集上进行训练。
# Initialize the model
tabular_model = TabularModel(
data_config=data_config,
model_config=model_config,
optimizer_config=optimizer_config,
trainer_config=trainer_config
)
# Fit the model
tabular_model.fit(train=train_df, validation=test_df)
该模型使用 train_df进行训练并在test_df 上进行验证。TabTransformer架构使用注意力层来捕获特征之间的关系,包括分类特征和连续特征。
4.模型评估
一旦模型训练完成,我们就可以在测试集上评估其性能。
# Evaluate the model
test_metrics = tabular_model.evaluate(test_df)
print(test_metrics)
模型对测试数据的准确率将被打印出来。以下是输出示例:
[{'test_loss': 2.305927276611328, 'test_accuracy': 0.9893730282783508}]
TabTransformer实现了98.94% 的准确率,对于一个在该数据集上训练且配置和调整最少的模型来说,这令人印象深刻。在这个例子中,我们使用了最小配置,训练了 10 多个 epoch。想象一下,如果我们让它训练更长时间并进一步优化超参数,这个模型能做什么!
5:TabTransformer 的优点
现在我们已经看到了 TabTransformer 的实际应用,让我们来谈谈为什么它会对表格数据产生如此大的改变。
还记得我们的侦探比喻吗?TabTransformer 就像那个超级聪明的侦探,他能看到别人错过的联系。这种理解上下文的能力带来了几个关键优势:
- 准确率: TabTransformer 通常比传统方法使用更少的参数实现更高的准确率。这就像我们的侦探使用更少的线索,但由于理解力更强,仍能得出更准确的结论。这使得 TabTransformer 更加高效和适应性强,尤其是在数据有限的场景中。
- 可解释性: TabTransformer 的上下文嵌入不仅仅是隐藏的计算,它们就像侦探的笔记,提供有关特征之间关系的宝贵见解。通过检查嵌入,我们可以了解哪些特征最重要以及它们如何相互作用。这有助于我们更深入地了解数据并做出更明智的决策。
- 稳健性:与传统方法相比,TabTransformer 对缺失和噪声数据具有更强的稳健性。可以将其视为我们的侦探,它不易受到误导性线索或缺失信息的影响。当数据不完整或损坏时,上下文嵌入可帮助模型“填补空白”,从而做出更可靠的预测。
- 可扩展性: TabTransformer 展现出比 MLP 更快的扩展规律,使其更适合处理更大、更复杂的数据集。这就像我们的侦探能够处理更多线索并有效地将它们联系起来而不会不知所措。
6:结论
TabTransformer 代表了我们在分析和理解表格数据方面的巨大飞跃。它是一种多功能工具,能够处理数据集内的复杂关系并适应现实世界的挑战。它学习上下文嵌入的能力将准确性、稳健性和可解释性提升到了一个新水平。
参考:
https://arxiv.org/abs/2012.06678
https://medium.com/@cristianleo120/the-math-behind-tabtransformer-78b78c12cfc1