程式碼範例 / 結構化資料 / FeatureSpace 進階使用案例

FeatureSpace 進階使用案例

作者: Dimitre Oliveira
建立日期 2023/07/01
上次修改日期 2025/01/03
描述: 如何將 FeatureSpace 用於進階預處理案例。

ⓘ 此範例使用 Keras 3

在 Colab 中檢視 GitHub 來源


簡介

此範例是 使用 FeatureSpace 進行結構化資料分類 程式碼範例的延伸,在這裡我們將擴展它以涵蓋 [keras.utils.FeatureSpace](/api/utils/feature_space#featurespace-class) 預處理實用程式更複雜的使用案例,例如特徵雜湊、特徵交叉、處理遺失值,以及將 Keras 預處理層 與 FeatureSpace 整合。

一般任務仍然是使用包含數值特徵、整數類別特徵和字串類別特徵的資料進行結構化資料分類(也稱為表格資料分類)。

資料集

我們的資料集 由一家葡萄牙銀行機構提供。它是一個 CSV 檔案,包含 4119 列。每一列包含有關基於電話的行銷活動的資訊,而每一欄描述客戶的一個屬性。我們使用這些特徵來預測客戶是否訂閱了該產品(銀行定期存款),「是」(yes)或「否」(no)。

以下是每個特徵的描述

欄位 描述 特徵類型
年齡 客戶的年齡 數值
工作 工作類型 類別
婚姻 婚姻狀況 類別
教育 客戶的教育程度 類別
違約 是否有信用違約? 類別
房屋 是否有房屋貸款? 類別
貸款 是否有個人貸款? 類別
聯絡 聯絡通訊類型 類別
月份 一年中最後聯絡的月份 類別
星期幾 最後聯絡的星期幾 類別
持續時間 最後聯絡的持續時間,以秒為單位 數值
活動 在此行銷活動中針對此客戶執行的聯絡次數 數值
過去的天數 自從上次行銷活動聯絡客戶以來經過的天數 數值
之前 在此行銷活動之前針對此客戶執行的聯絡次數 數值
結果 上次行銷活動的結果 類別
就業變動率 就業變動率 數值
消費者物價指數 消費者物價指數 數值
消費者信心指數 消費者信心指數 數值
歐元銀行同業拆款利率 3 個月 歐元銀行同業拆款利率 3 個月利率 數值
受僱人數 受僱人數 數值
Y 客戶是否已訂閱定期存款? 目標

關於特徵 duration 的重要注意事項:此屬性高度影響輸出目標(例如,如果 duration=0,則 y='no')。但是,在執行通話之前,持續時間是未知的。此外,在通話結束後,顯然知道 y。因此,此輸入僅應包含用於基準測試目的,如果目的是要有實際的預測模型,則應捨棄此輸入。基於此原因,我們將捨棄它。


設定

import os

os.environ["KERAS_BACKEND"] = "tensorflow"

import keras
from keras.utils import FeatureSpace
import pandas as pd
import tensorflow as tf
from pathlib import Path
from zipfile import ZipFile

載入資料

讓我們下載資料並將其載入到 Pandas 資料框中

data_url = "https://archive.ics.uci.edu/static/public/222/bank+marketing.zip"
data_zipped_path = keras.utils.get_file("bank_marketing.zip", data_url, extract=True)
keras_datasets_path = Path(data_zipped_path)
with ZipFile(f"{keras_datasets_path}/bank-additional.zip", "r") as zip:
    # Extract files
    zip.extractall(path=keras_datasets_path)

dataframe = pd.read_csv(
    f"{keras_datasets_path}/bank-additional/bank-additional.csv", sep=";"
)

我們將建立一個新的特徵 previously_contacted,以便能夠示範一些有用的預處理技術,此特徵是基於 pdays。根據資料集資訊,如果 pdays = 999,則表示之前沒有聯絡過客戶,因此讓我們建立一個特徵來捕捉這一點。

# Droping `duration` to avoid target leak
dataframe.drop("duration", axis=1, inplace=True)
# Creating the new feature `previously_contacted`
dataframe["previously_contacted"] = dataframe["pdays"].map(
    lambda x: 0 if x == 999 else 1
)

該資料集包含 4119 個樣本,每個樣本有 21 欄(20 個特徵,加上目標標籤),以下是一些樣本的預覽

print(f"Dataframe shape: {dataframe.shape}")
print(dataframe.head())
Dataframe shape: (4119, 21)
   age          job  marital          education default  housing     loan  \
0   30  blue-collar  married           basic.9y      no      yes       no   
1   39     services   single        high.school      no       no       no   
2   25     services  married        high.school      no      yes       no   
3   38     services  married           basic.9y      no  unknown  unknown   
4   47       admin.  married  university.degree      no      yes       no   
     contact month day_of_week  ...  pdays  previous     poutcome  \
0   cellular   may         fri  ...    999         0  nonexistent   
1  telephone   may         fri  ...    999         0  nonexistent   
2  telephone   jun         wed  ...    999         0  nonexistent   
3  telephone   jun         fri  ...    999         0  nonexistent   
4   cellular   nov         mon  ...    999         0  nonexistent   
  emp.var.rate  cons.price.idx  cons.conf.idx  euribor3m  nr.employed   y  \
0         -1.8          92.893          -46.2      1.313       5099.1  no   
1          1.1          93.994          -36.4      4.855       5191.0  no   
2          1.4          94.465          -41.8      4.962       5228.1  no   
3          1.4          94.465          -41.8      4.959       5228.1  no   
4         -0.1          93.200          -42.0      4.191       5195.8  no   
  previously_contacted  
0                    0  
1                    0  
2                    0  
3                    0  
4                    0  
[5 rows x 21 columns]

欄位「y」表示客戶是否已訂閱定期存款。


訓練/驗證分割

讓我們將資料分割為訓練集和驗證集

valid_dataframe = dataframe.sample(frac=0.2, random_state=0)
train_dataframe = dataframe.drop(valid_dataframe.index)

print(
    f"Using {len(train_dataframe)} samples for training and "
    f"{len(valid_dataframe)} for validation"
)
Using 3295 samples for training and 824 for validation

產生 TF 資料集

讓我們為每個資料框產生 [tf.data.Dataset](https://tensorflow.dev.org.tw/api_docs/python/tf/data/Dataset) 物件,由於我們的目標欄位 y 是字串,我們也需要將其編碼為整數,以便能夠使用它來訓練我們的模型。為了實現這一點,我們將建立一個 StringLookup 層,將字串「no」和「yes」分別對應到「0」和「1」。

label_lookup = keras.layers.StringLookup(
    # the order here is important since the first index will be encoded as 0
    vocabulary=["no", "yes"],
    num_oov_indices=0,
)


def encode_label(x, y):
    encoded_y = label_lookup(y)
    return x, encoded_y


def dataframe_to_dataset(dataframe):
    dataframe = dataframe.copy()
    labels = dataframe.pop("y")
    ds = tf.data.Dataset.from_tensor_slices((dict(dataframe), labels))
    ds = ds.map(encode_label, num_parallel_calls=tf.data.AUTOTUNE)
    ds = ds.shuffle(buffer_size=len(dataframe))
    return ds


train_ds = dataframe_to_dataset(train_dataframe)
valid_ds = dataframe_to_dataset(valid_dataframe)

每個 Dataset 會產生一個元組 (input, target),其中 input 是一個特徵字典,而 target 是值 01

for x, y in dataframe_to_dataset(train_dataframe).take(1):
    print(f"Input: {x}")
    print(f"Target: {y}")
Input: {'age': <tf.Tensor: shape=(), dtype=int64, numpy=56>, 'job': <tf.Tensor: shape=(), dtype=string, numpy=b'admin.'>, 'marital': <tf.Tensor: shape=(), dtype=string, numpy=b'married'>, 'education': <tf.Tensor: shape=(), dtype=string, numpy=b'university.degree'>, 'default': <tf.Tensor: shape=(), dtype=string, numpy=b'no'>, 'housing': <tf.Tensor: shape=(), dtype=string, numpy=b'yes'>, 'loan': <tf.Tensor: shape=(), dtype=string, numpy=b'no'>, 'contact': <tf.Tensor: shape=(), dtype=string, numpy=b'cellular'>, 'month': <tf.Tensor: shape=(), dtype=string, numpy=b'jul'>, 'day_of_week': <tf.Tensor: shape=(), dtype=string, numpy=b'fri'>, 'campaign': <tf.Tensor: shape=(), dtype=int64, numpy=5>, 'pdays': <tf.Tensor: shape=(), dtype=int64, numpy=999>, 'previous': <tf.Tensor: shape=(), dtype=int64, numpy=0>, 'poutcome': <tf.Tensor: shape=(), dtype=string, numpy=b'nonexistent'>, 'emp.var.rate': <tf.Tensor: shape=(), dtype=float64, numpy=1.4>, 'cons.price.idx': <tf.Tensor: shape=(), dtype=float64, numpy=93.918>, 'cons.conf.idx': <tf.Tensor: shape=(), dtype=float64, numpy=-42.7>, 'euribor3m': <tf.Tensor: shape=(), dtype=float64, numpy=4.957>, 'nr.employed': <tf.Tensor: shape=(), dtype=float64, numpy=5228.1>, 'previously_contacted': <tf.Tensor: shape=(), dtype=int64, numpy=0>}
Target: 0

預處理

通常我們的資料並非處於建模的正確或最佳格式,這就是為什麼我們大多數時候需要在特徵上進行某種預處理,以使它們與模型相容,或為任務從中提取最大值。我們需要為訓練執行此預處理步驟,但在推論時,我們也需要確保資料經過相同的處理,這就是 FeatureSpace 等實用工具的優勢,我們可以定義所有預處理一次,並在系統的不同階段重複使用。

在這裡,我們將了解如何使用 FeatureSpace 來執行更複雜的轉換及其彈性,然後將所有內容組合到一個元件中,以便為我們的模型預處理資料。

FeatureSpace 實用程式會使用 adapt() 函數從資料中學習如何處理資料,這需要一個僅包含特徵的資料集,因此讓我們建立它,並提供一個實用函數來實際展示預處理範例

train_ds_with_no_labels = train_ds.map(lambda x, _: x)


def example_feature_space(dataset, feature_space, feature_names):
    feature_space.adapt(dataset)
    for x in dataset.take(1):
        inputs = {feature_name: x[feature_name] for feature_name in feature_names}
        preprocessed_x = feature_space(inputs)
        print(f"Input: {[{k:v.numpy()} for k, v in inputs.items()]}")
        print(
            f"Preprocessed output: {[{k:v.numpy()} for k, v in preprocessed_x.items()]}"
        )

特徵雜湊

特徵雜湊表示將一組值雜湊或編碼為定義數量的區間,在這種情況下,我們有 campaign(在此行銷活動中針對客戶執行的聯絡次數),它是一個數值特徵,可以假設不同的值範圍,我們將其雜湊為 4 個區間,這表示原始特徵的任何可能值都將被放入其中 4 個可能的區間之一。此處的輸出可以是一次性編碼的向量或單個數字。

feature_space = FeatureSpace(
    features={
        "campaign": FeatureSpace.integer_hashed(num_bins=4, output_mode="one_hot")
    },
    output_mode="dict",
)
example_feature_space(train_ds_with_no_labels, feature_space, ["campaign"])
Input: [{'campaign': 1}]
Preprocessed output: [{'campaign': array([0., 1., 0., 0.], dtype=float32)}]

特徵雜湊也可用於字串特徵。

feature_space = FeatureSpace(
    features={
        "education": FeatureSpace.string_hashed(num_bins=3, output_mode="one_hot")
    },
    output_mode="dict",
)
example_feature_space(train_ds_with_no_labels, feature_space, ["education"])
Input: [{'education': b'university.degree'}]
Preprocessed output: [{'education': array([0., 0., 1.], dtype=float32)}]

對於數值特徵,我們可以透過使用 float_discretized 選項來獲得類似的行為,此選項和 integer_hashed 之間的主要差異在於,前者我們對值進行分箱時會保留一些數值關係(接近的值很可能被放入同一個分箱),而後者(雜湊)我們無法保證這些數字會被雜湊到同一個分箱中,這取決於雜湊函數。

feature_space = FeatureSpace(
    features={"age": FeatureSpace.float_discretized(num_bins=3, output_mode="one_hot")},
    output_mode="dict",
)
example_feature_space(train_ds_with_no_labels, feature_space, ["age"])
Input: [{'age': 56}]
Preprocessed output: [{'age': array([0., 0., 1.], dtype=float32)}]

特徵索引

索引字串特徵基本上表示為其建立離散的數值表示,這對於字串特徵尤其重要,因為大多數模型僅接受數值特徵。此轉換會將字串值放入不同的類別中。此處的輸出可以是一次性編碼的向量或單個數字。

請注意,透過指定 num_oov_indices=1,我們會在輸出向量中為 OOV(詞彙外)值保留一個位置,這是處理訓練後遺失或未見值(在 adapt() 步驟中未見過的值)的重要工具

feature_space = FeatureSpace(
    features={
        "default": FeatureSpace.string_categorical(
            num_oov_indices=1, output_mode="one_hot"
        )
    },
    output_mode="dict",
)
example_feature_space(train_ds_with_no_labels, feature_space, ["default"])
Input: [{'default': b'no'}]
Preprocessed output: [{'default': array([0., 1., 0., 0.], dtype=float32)}]

我們也可以對整數特徵執行特徵索引,這對於某些資料集來說非常重要,在這些資料集中,類別特徵會被數字取代,例如 sexgender 等特徵,其中(1 和 0)之類的值之間沒有數值關係,它們只是不同的類別,此行為可以透過此轉換完美捕捉。

在此資料集中,我們可以使用我們建立的特徵 previously_contacted。在此案例中,我們想要明確設定 num_oov_indices=0,原因是我們預期該特徵只有兩個可能的值,其他任何值都將是錯誤的輸入或資料建立的問題,基於此原因,我們可能只想讓程式碼擲回錯誤,以便我們可以了解該問題並加以修正。

feature_space = FeatureSpace(
    features={
        "previously_contacted": FeatureSpace.integer_categorical(
            num_oov_indices=0, output_mode="one_hot"
        )
    },
    output_mode="dict",
)
example_feature_space(train_ds_with_no_labels, feature_space, ["previously_contacted"])
Input: [{'previously_contacted': 0}]
Preprocessed output: [{'previously_contacted': array([1., 0.], dtype=float32)}]

特徵交叉(混合不同類型的特徵)

透過交叉,我們可以在任意數量的混合類型特徵之間執行特徵互動,只要它們是類別特徵,您可以想像,我們不是具有 {'age': 20} 和另一個 {'job': 'entrepreneur'} 的特徵,而是具有 {'age_X_job': 20_entrepreneur},但是透過 FeatureSpace交叉,我們可以將特定的預處理應用於每個單獨的特徵和特徵交叉本身。此選項對於特定使用案例可能非常強大,在這裡可能是一個不錯的選擇,因為年齡與工作結合對於銀行領域可能具有不同的含義。

我們會將 agejob 進行交叉組合,並將它們的組合輸出雜湊成一個大小為 8 的向量表示。這裡的輸出可以是一個 one-hot 編碼向量或是一個單一數字。

有時,多個特徵的組合可能會產生一個非常大的特徵空間,例如將某人的郵遞區號與其姓氏進行交叉組合,可能性將會達到數千種,這就是為什麼 crossing_dim 參數如此重要的原因,它可以限制交叉特徵的輸出維度。

請注意,age 的 6 個分箱值和 job 的 12 個值的可能組合將會是 72 種,因此選擇 crossing_dim = 8 代表我們選擇限制輸出向量。

feature_space = FeatureSpace(
    features={
        "age": FeatureSpace.integer_hashed(num_bins=6, output_mode="one_hot"),
        "job": FeatureSpace.string_categorical(
            num_oov_indices=0, output_mode="one_hot"
        ),
    },
    crosses=[
        FeatureSpace.cross(
            feature_names=("age", "job"),
            crossing_dim=8,
            output_mode="one_hot",
        )
    ],
    output_mode="dict",
)
example_feature_space(train_ds_with_no_labels, feature_space, ["age", "job"])
Input: [{'age': 33}, {'job': b'admin.'}]
Preprocessed output: [{'age': array([0., 0., 1., 0., 0., 0.], dtype=float32)}, {'job': array([1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], dtype=float32)}, {'age_X_job': array([0., 1., 0., 0., 0., 0., 0., 0.], dtype=float32)}]

使用 Keras 預處理層的 FeatureSpace

為了成為一個真正靈活且可擴展的特徵,我們不能只依賴那些預定義的轉換,我們必須能夠重複使用來自 Keras/TensorFlow 生態系統的其他轉換並客製化我們自己的轉換,這就是為什麼 FeatureSpace 也被設計為可以與 Keras 預處理層 一起工作的原因,這樣我們就可以使用框架提供的複雜資料轉換,您甚至可以創建自己的客製化 Keras 預處理層並以相同的方式使用它。

在這裡,我們將使用 [`keras.layers.TextVectorization`](/api/layers/preprocessing_layers/text/text_vectorization#textvectorization-class) 預處理層,從我們的資料中創建一個 TF-IDF 特徵。請注意,這個特徵並不是 TF-IDF 的一個很好的用例,這只是為了示範目的。

custom_layer = keras.layers.TextVectorization(output_mode="tf_idf")

feature_space = FeatureSpace(
    features={
        "education": FeatureSpace.feature(
            preprocessor=custom_layer, dtype="string", output_mode="float"
        )
    },
    output_mode="dict",
)
example_feature_space(train_ds_with_no_labels, feature_space, ["education"])
Input: [{'education': b'high.school'}]
Preprocessed output: [{'education': array([0.       , 0.       , 1.6840783, 0.       , 0.       , 0.       ,
       0.       , 0.       , 0.       ], dtype=float32)}]

配置最終的 FeatureSpace

現在我們知道如何將 FeatureSpace 用於更複雜的用例,讓我們選擇看起來對此任務更有用的那些,並創建最終的 FeatureSpace 元件。

要配置每個特徵應如何進行預處理,我們實例化一個 `keras.utils.FeatureSpace`,並將一個字典傳遞給它,該字典將我們的特徵名稱對應到特徵轉換函式。

feature_space = FeatureSpace(
    features={
        # Categorical features encoded as integers
        "previously_contacted": FeatureSpace.integer_categorical(num_oov_indices=0),
        # Categorical features encoded as string
        "marital": FeatureSpace.string_categorical(num_oov_indices=0),
        "education": FeatureSpace.string_categorical(num_oov_indices=0),
        "default": FeatureSpace.string_categorical(num_oov_indices=0),
        "housing": FeatureSpace.string_categorical(num_oov_indices=0),
        "loan": FeatureSpace.string_categorical(num_oov_indices=0),
        "contact": FeatureSpace.string_categorical(num_oov_indices=0),
        "month": FeatureSpace.string_categorical(num_oov_indices=0),
        "day_of_week": FeatureSpace.string_categorical(num_oov_indices=0),
        "poutcome": FeatureSpace.string_categorical(num_oov_indices=0),
        # Categorical features to hash and bin
        "job": FeatureSpace.string_hashed(num_bins=3),
        # Numerical features to hash and bin
        "pdays": FeatureSpace.integer_hashed(num_bins=4),
        # Numerical features to normalize and bin
        "age": FeatureSpace.float_discretized(num_bins=4),
        # Numerical features to normalize
        "campaign": FeatureSpace.float_normalized(),
        "previous": FeatureSpace.float_normalized(),
        "emp.var.rate": FeatureSpace.float_normalized(),
        "cons.price.idx": FeatureSpace.float_normalized(),
        "cons.conf.idx": FeatureSpace.float_normalized(),
        "euribor3m": FeatureSpace.float_normalized(),
        "nr.employed": FeatureSpace.float_normalized(),
    },
    # Specify feature cross with a custom crossing dim.
    crosses=[
        FeatureSpace.cross(feature_names=("age", "job"), crossing_dim=8),
        FeatureSpace.cross(feature_names=("housing", "loan"), crossing_dim=6),
        FeatureSpace.cross(
            feature_names=("poutcome", "previously_contacted"), crossing_dim=2
        ),
    ],
    output_mode="concat",
)

FeatureSpace 適應於訓練資料

在我們開始使用 FeatureSpace 來建立模型之前,我們必須將它適應於訓練資料。在 adapt() 期間,FeatureSpace 將會執行:

  • 為類別特徵建立可能的數值集合索引。
  • 計算數值特徵的均值和變異數以進行正規化。
  • 計算數值特徵不同分箱的數值邊界以進行離散化。
  • 自訂層所需的任何其他類型的預處理。

請注意,adapt() 應該在一個 `tf.data.Dataset` 上調用,該資料集產生特徵值的字典 – 沒有標籤。

但首先,讓我們對資料集進行批次處理

train_ds = train_ds.batch(32)
valid_ds = valid_ds.batch(32)

train_ds_with_no_labels = train_ds.map(lambda x, _: x)
feature_space.adapt(train_ds_with_no_labels)

此時,FeatureSpace 可以針對原始特徵值的字典進行調用,並且因為我們設定了 output_mode="concat",它將會為每個樣本返回一個單一的串聯向量,結合編碼的特徵和特徵交叉。

for x, _ in train_ds.take(1):
    preprocessed_x = feature_space(x)
    print(f"preprocessed_x shape: {preprocessed_x.shape}")
    print(f"preprocessed_x sample: \n{preprocessed_x[0]}")
preprocessed_x shape: (32, 77)
preprocessed_x sample: 
[ 0.          0.          1.          0.         -0.19560708  0.8937782
  0.7249699   0.          1.          0.          0.          0.
  1.          0.          0.          1.          0.          0.
  0.          0.          0.          1.          0.          0.
  0.          0.6566938   0.71815234  0.          0.          1.
  0.          1.          0.          0.          0.          1.
  1.          0.          0.          0.          1.          0.
  0.          0.          0.          0.          0.          0.
  0.          0.          0.33757654  0.          0.          1.
  0.          1.          0.          0.         -0.35691857  1.
  0.          0.          0.          0.          0.          1.
  0.          0.          0.          0.          0.          0.
  1.          0.          0.          1.          0.        ]

儲存 FeatureSpace

此時,我們可以選擇儲存我們的 FeatureSpace 元件,這有很多優點,例如在不同實驗中重複使用相同的模型、如果您需要重新執行預處理步驟可以節省時間,以及主要用於模型部署,其中透過載入它可以確保您將應用相同的預處理步驟,無論設備或環境如何,這是一個減少 訓練/服務偏差 的好方法。

feature_space.save("myfeaturespace.keras")

使用 FeatureSpace 作為 tf.data 管線的一部分進行預處理

我們將選擇使用我們的元件進行非同步處理,方法是將其作為 tf.data 管線的一部分,如 先前的指南 中所述。這可以在資料到達模型之前,對 CPU 上的資料進行非同步平行預處理。通常,這在訓練期間始終是正確的做法。

讓我們建立一個預處理批次的訓練和驗證資料集

preprocessed_train_ds = train_ds.map(
    lambda x, y: (feature_space(x), y), num_parallel_calls=tf.data.AUTOTUNE
).prefetch(tf.data.AUTOTUNE)

preprocessed_valid_ds = valid_ds.map(
    lambda x, y: (feature_space(x), y), num_parallel_calls=tf.data.AUTOTUNE
).prefetch(tf.data.AUTOTUNE)

模型

我們將利用我們的 FeatureSpace 元件來建立模型,因為我們希望模型與我們的預處理函式相容,所以讓我們使用 FeatureSpace 特徵映射作為我們模型的輸入。

encoded_features = feature_space.get_encoded_features()
print(encoded_features)
<KerasTensor shape=(None, 77), dtype=float32, sparse=False, name=keras_tensor_56>

這個模型非常簡單,僅用於示範目的,所以不要太關注其架構。

x = keras.layers.Dense(64, activation="relu")(encoded_features)
x = keras.layers.Dropout(0.5)(x)
output = keras.layers.Dense(1, activation="sigmoid")(x)

model = keras.Model(inputs=encoded_features, outputs=output)
model.compile(optimizer="adam", loss="binary_crossentropy", metrics=["accuracy"])

訓練

讓我們訓練我們的模型 20 個 epoch。請注意,特徵預處理是作為 tf.data 管線的一部分進行的,而不是作為模型的一部分進行的。

model.fit(
    preprocessed_train_ds, validation_data=preprocessed_valid_ds, epochs=10, verbose=2
)
Epoch 1/10

103/103 - 15s - 149ms/step - accuracy: 0.8753 - loss: 0.3639 - val_accuracy: 0.9102 - val_loss: 0.2747

Epoch 2/10

103/103 - 12s - 121ms/step - accuracy: 0.8965 - loss: 0.3058 - val_accuracy: 0.9078 - val_loss: 0.2716

Epoch 3/10

103/103 - 12s - 121ms/step - accuracy: 0.8947 - loss: 0.2972 - val_accuracy: 0.9053 - val_loss: 0.2712

Epoch 4/10

103/103 - 12s - 116ms/step - accuracy: 0.9002 - loss: 0.2877 - val_accuracy: 0.9102 - val_loss: 0.2677

Epoch 5/10

103/103 - 13s - 124ms/step - accuracy: 0.8974 - loss: 0.2815 - val_accuracy: 0.9041 - val_loss: 0.2688

Epoch 6/10

103/103 - 13s - 129ms/step - accuracy: 0.8986 - loss: 0.2917 - val_accuracy: 0.9066 - val_loss: 0.2658

Epoch 7/10

103/103 - 12s - 120ms/step - accuracy: 0.9029 - loss: 0.2779 - val_accuracy: 0.9053 - val_loss: 0.2670

Epoch 8/10

103/103 - 13s - 124ms/step - accuracy: 0.9011 - loss: 0.2809 - val_accuracy: 0.9090 - val_loss: 0.2660

Epoch 9/10

103/103 - 13s - 121ms/step - accuracy: 0.9008 - loss: 0.2748 - val_accuracy: 0.9041 - val_loss: 0.2689

Epoch 10/10

103/103 - 13s - 123ms/step - accuracy: 0.9038 - loss: 0.2768 - val_accuracy: 0.9053 - val_loss: 0.2674

<keras.src.callbacks.history.History at 0x723b293058d0>

使用端到端模型對新資料進行推論

現在,我們可以建立我們的推論模型(包括 FeatureSpace),根據原始特徵值的字典進行預測,如下所示

載入 FeatureSpace

首先,讓我們載入我們剛才儲存的 FeatureSpace,如果您訓練一個模型,但想在不同的時間進行推論,可能使用不同的設備或環境,這可能會非常方便。

loaded_feature_space = keras.saving.load_model("myfeaturespace.keras")

建立推論端到端模型

要建立推論模型,我們需要特徵輸入映射和預處理編碼的 Keras 張量。

dict_inputs = loaded_feature_space.get_inputs()
encoded_features = loaded_feature_space.get_encoded_features()
print(encoded_features)

print(dict_inputs)

outputs = model(encoded_features)
inference_model = keras.Model(inputs=dict_inputs, outputs=outputs)

sample = {
    "age": 30,
    "job": "blue-collar",
    "marital": "married",
    "education": "basic.9y",
    "default": "no",
    "housing": "yes",
    "loan": "no",
    "contact": "cellular",
    "month": "may",
    "day_of_week": "fri",
    "campaign": 2,
    "pdays": 999,
    "previous": 0,
    "poutcome": "nonexistent",
    "emp.var.rate": -1.8,
    "cons.price.idx": 92.893,
    "cons.conf.idx": -46.2,
    "euribor3m": 1.313,
    "nr.employed": 5099.1,
    "previously_contacted": 0,
}

input_dict = {
    name: keras.ops.convert_to_tensor([value]) for name, value in sample.items()
}
predictions = inference_model.predict(input_dict)

print(
    f"This particular client has a {100 * predictions[0][0]:.2f}% probability "
    "of subscribing a term deposit, as evaluated by our model."
)
<KerasTensor shape=(None, 77), dtype=float32, sparse=False, name=keras_tensor_99>
{'previously_contacted': <KerasTensor shape=(None, 1), dtype=int32, sparse=False, name=previously_contacted>, 'marital': <KerasTensor shape=(None, 1), dtype=string, sparse=False, name=marital>, 'education': <KerasTensor shape=(None, 1), dtype=string, sparse=False, name=education>, 'default': <KerasTensor shape=(None, 1), dtype=string, sparse=False, name=default>, 'housing': <KerasTensor shape=(None, 1), dtype=string, sparse=False, name=housing>, 'loan': <KerasTensor shape=(None, 1), dtype=string, sparse=False, name=loan>, 'contact': <KerasTensor shape=(None, 1), dtype=string, sparse=False, name=contact>, 'month': <KerasTensor shape=(None, 1), dtype=string, sparse=False, name=month>, 'day_of_week': <KerasTensor shape=(None, 1), dtype=string, sparse=False, name=day_of_week>, 'poutcome': <KerasTensor shape=(None, 1), dtype=string, sparse=False, name=poutcome>, 'job': <KerasTensor shape=(None, 1), dtype=string, sparse=False, name=job>, 'pdays': <KerasTensor shape=(None, 1), dtype=int32, sparse=False, name=pdays>, 'age': <KerasTensor shape=(None, 1), dtype=float32, sparse=False, name=age>, 'campaign': <KerasTensor shape=(None, 1), dtype=float32, sparse=False, name=campaign>, 'previous': <KerasTensor shape=(None, 1), dtype=float32, sparse=False, name=previous>, 'emp.var.rate': <KerasTensor shape=(None, 1), dtype=float32, sparse=False, name=emp.var.rate>, 'cons.price.idx': <KerasTensor shape=(None, 1), dtype=float32, sparse=False, name=cons.price.idx>, 'cons.conf.idx': <KerasTensor shape=(None, 1), dtype=float32, sparse=False, name=cons.conf.idx>, 'euribor3m': <KerasTensor shape=(None, 1), dtype=float32, sparse=False, name=euribor3m>, 'nr.employed': <KerasTensor shape=(None, 1), dtype=float32, sparse=False, name=nr.employed>}

/home/humbulani/tensorflow-env/env/lib/python3.11/site-packages/keras/src/models/functional.py:248: UserWarning: The structure of `inputs` doesn't match the expected structure.
Expected: {'age': 'age', 'campaign': 'campaign', 'cons.conf.idx': 'cons.conf.idx', 'cons.price.idx': 'cons.price.idx', 'contact': 'contact', 'day_of_week': 'day_of_week', 'default': 'default', 'education': 'education', 'emp.var.rate': 'emp.var.rate', 'euribor3m': 'euribor3m', 'housing': 'housing', 'job': 'job', 'loan': 'loan', 'marital': 'marital', 'month': 'month', 'nr.employed': 'nr.employed', 'pdays': 'pdays', 'poutcome': 'poutcome', 'previous': 'previous', 'previously_contacted': 'previously_contacted'}
Received: inputs={'age': 'Tensor(shape=(1,))', 'job': 'Tensor(shape=(1,))', 'marital': 'Tensor(shape=(1,))', 'education': 'Tensor(shape=(1,))', 'default': 'Tensor(shape=(1,))', 'housing': 'Tensor(shape=(1,))', 'loan': 'Tensor(shape=(1,))', 'contact': 'Tensor(shape=(1,))', 'month': 'Tensor(shape=(1,))', 'day_of_week': 'Tensor(shape=(1,))', 'campaign': 'Tensor(shape=(1,))', 'pdays': 'Tensor(shape=(1,))', 'previous': 'Tensor(shape=(1,))', 'poutcome': 'Tensor(shape=(1,))', 'emp.var.rate': 'Tensor(shape=(1,))', 'cons.price.idx': 'Tensor(shape=(1,))', 'cons.conf.idx': 'Tensor(shape=(1,))', 'euribor3m': 'Tensor(shape=(1,))', 'nr.employed': 'Tensor(shape=(1,))', 'previously_contacted': 'Tensor(shape=(1,))'}
  warnings.warn(msg)

1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 1s/step



1/1 ━━━━━━━━━━━━━━━━━━━━ 2s 2s/step

This particular client has a 10.85% probability of subscribing a term deposit, as evaluated by our model.