使用 Keras 功能式 API 和 TensorFlow 预测葡萄酒价格
2018 年 4 月 23 日
Sara Robinson 发布

你能给“优雅、细致的单宁”、“成熟的黑醋栗香气”或“浓郁而烤香”标上价格吗?事实证明,机器学习模型可以。在这篇文章中,我将解释我是如何使用 Keras (tf.keras) 构建一个宽而深的网络来根据葡萄酒的描述预测其价格。对于那些刚接触 Keras 的人来说,它是一个用于构建 ML 模型的更高级别的 TensorFlow API。如果你想直接跳到代码,它可以在 GitHub 上找到。你还可以使用 Colab 直接在浏览器中运行模型,无需任何设置
Wine model with TensorFlow
感谢 FrancoisJoshYufeng 对本文的帮助和意见。

模型:使用 Keras 构建宽而深的网络

我最近一直在构建很多 Keras 模型(这里有一些示例 示例)使用顺序模型 API,但我想要尝试一下功能式 API。顺序 API 是使用 Keras 入门的最佳方法 - 它允许你轻松地将模型定义为一层一层的堆叠。功能式 API 提供了更大的灵活性,最适合具有多个输入或组合模型的模型。功能式 API 的一个很好的用例是在 Keras 中实现一个宽而深的网络。关于宽而深的网络有很多很棒的资源,所以我不会关注细节,但如果你有兴趣了解更多信息,我建议你阅读这篇文章

在你使用宽而深的网络来解决 ML 问题之前,最好确保它适合你想要预测的内容。如果你有一个预测任务,其中输入和输出之间存在相对直接的关系,那么一个宽模型可能就足够了。宽模型是指具有稀疏特征向量的模型,或者说是向量中大部分值为零的模型。另一方面,多层深层网络在图像或语音识别等任务中表现出色,因为在这些任务中,输入和输出之间可能存在意想不到的关系。如果你有一个预测任务可以从这两个模型中获益(推荐模型或具有文本输入的模型是很好的例子),那么宽而深可能是一个不错的选择。在本例中,我尝试了分别使用宽模型和深模型,然后将它们结合起来,发现宽而深模型的准确率最高。让我们深入了解一下。

数据集:预测葡萄酒价格

我们将使用 Kaggle 上的这个葡萄酒数据集来查看

我们能否根据葡萄酒的描述和品种预测其价格?

这个问题非常适合宽而深学习,因为它涉及文本输入,而且葡萄酒的描述与其价格之间没有明显的相关性。我们不能肯定地说,描述中包含“果味”的葡萄酒更贵,或者说“单宁柔和”的葡萄酒更便宜。此外,在将文本输入我们的模型时,有多种表示方法,并且两种方法都可能导致不同类型的见解。既有宽的表示形式(词袋),也有深的表示形式(嵌入),将两者结合起来可以让我们从文本中提取更多意义。此数据集包含许多不同的特征可能性,但为了简单起见,我们只使用描述和品种。

输入

  • 描述:香草的浓郁香气从酒杯中升起,但果香,即使在这个艰难的年份,也立即显现出来。它酸涩而尖锐,带有浓郁的草本香气,酒液迅速变得清晰,果香、酸度、单宁、香草和草本香气比例相等。坚实而紧凑,仍然很年轻,这款酒需要醒酒或进一步瓶陈才能展现其最佳风味。
  • 品种:黑皮诺

预测

价格 - 45 美元

首先,以下是我们构建此模型所需的所有导入
import os
import numpy as np
import pandas as pd
import tensorflow as tf

from sklearn.preprocessing import LabelEncoder

from tensorflow import keras
layers = keras.layers

# This code was tested with TensorFlow v1.7
print("You have TensorFlow version", tf.__version__)
由于我们模型的输出(预测)是价格的数值,因此我们将直接将价格值输入到模型中进行训练和评估。该模型的完整代码可以在 GitHub 上找到。在这里,我将重点介绍关键点。首先,让我们下载数据并将其转换为 Pandas 数据帧
!wget -q https://storage.googleapis.com/sara-cloud-ml/wine_data.csv
data = pd.read_csv("wine_data.csv")
接下来,我们将将其拆分为训练集和测试集,并提取特征和标签
train_size = int(len(data) * .8)

# Train features
description_train = data['description'][:train_size]
variety_train = data['variety'][:train_size]

# Train labels
labels_train = data['price'][:train_size]

# Test features
description_test = data['description'][train_size:]
variety_test = data['variety'][train_size:]

# Test labels
labels_test = data['price'][train_size:]

第 1 部分:宽模型

特征 1:葡萄酒描述

为了创建文本描述的宽表示,我们将使用词袋模型。更多信息请点击这里,但简单回顾一下:词袋模型会查找每个输入中是否存在单词。你可以将每个输入看作是一个 Scrabble 棋盘,每个棋盘上都包含一个单词而不是一个字母。该模型不会考虑描述中单词的顺序,只考虑单词是否存在。
将词袋模型的输入想象成 Scrabble 棋盘,每个棋盘上都包含一个单词(而不是一个字母),这些单词来自我们的输入。
我们不会查看数据集中每个描述中发现的每个单词,而是将词袋限制为数据集中排名前 12,000 个单词(别担心,Keras 有一个内置的实用程序可以创建这个词汇表)。这被认为是“宽”的,因为我们模型中每个描述的输入将是一个包含 12k 个元素的宽向量,其中 1 和 0 表示词汇表中特定单词在特定描述中是否存在。

Keras 有一些方便的文本预处理实用程序,我们将使用它们将文本描述转换为词袋。使用词袋模型,我们通常希望仅将数据集中的部分单词包含在词汇表中。在本例中,我使用了 12,000 个单词,但这是一个可以调整的超参数(尝试几个值,看看哪个值对你的数据集最有效)。我们可以使用 Keras 的 Tokenizer 类来创建词袋词汇表
vocab_size = 12000
tokenize = keras.preprocessing.text.Tokenizer(num_words=vocab_size, char_level=False)
tokenize.fit_on_texts(description_train) # only fit on train
然后,我们将使用 texts_to_matrix 函数将每个描述转换为词袋向量
description_bow_train = tokenize.texts_to_matrix(description_train)
description_bow_test = tokenize.texts_to_matrix(description_test)

特征 2:葡萄酒品种

在最初的 Kaggle 数据集中,总共有 632 种葡萄酒品种。为了使我们的模型更容易提取模式,我进行了一些预处理,只保留前 40 种品种(约占原始数据集的 65%,或总共 96k 个示例)。我们将使用 Keras 实用程序将每个品种转换为整数表示形式,然后为每个输入创建 40 个元素的宽独热向量,以指示品种
# Use sklearn utility to convert label strings to numbered index
encoder = LabelEncoder()
encoder.fit(variety_train)
variety_train = encoder.transform(variety_train)
variety_test = encoder.transform(variety_test)
num_classes = np.max(variety_train) + 1

# Convert labels to one hot
variety_train = keras.utils.to_categorical(variety_train, num_classes)
variety_test = keras.utils.to_categorical(variety_test, num_classes)
现在,我们准备好构建宽模型了。

使用 Keras 功能式 API 构建宽模型

Keras 提供两种构建模型的 API:顺序 API 和功能式 API。功能式 API 在定义层方面给了我们更多灵活性,并允许我们将多个特征输入组合成一个层。它也让我们在准备好时轻松地将宽模型和深模型组合成一个。使用功能式 API,我们只需几行代码就可以定义宽模型。首先,我们将输入层定义为 12k 个元素的向量(对应于词汇表中的每个单词)。然后,我们将它连接到密集输出层以生成价格预测
bow_inputs = layers.Input(shape=(vocab_size,))
variety_inputs = layers.Input(shape=(num_classes,))
merged_layer = layers.concatenate([bow_inputs, variety_inputs])
merged_layer = layers.Dense(256, activation='relu')(merged_layer)
predictions = layers.Dense(1)(merged_layer)
wide_model = Model(inputs=[bow_inputs, variety_inputs], outputs=predictions)
然后,我们将编译模型,使其可以使用
wide_model.compile(loss='mse', optimizer='adam', metrics=['accuracy'])
如果我们单独使用宽模型,这里我们将使用 fit() 进行训练,使用 evaluate() 进行评估。由于我们稍后将把它与深模型结合起来,所以我们可以等到两个模型结合在一起后再进行训练。是时候构建我们的深模型了!

第 2 部分:深模型

为了创建葡萄酒描述的深层表示,我们将它表示为嵌入。关于词嵌入有很多资源,但简而言之,它们提供了一种将单词映射到向量的方式,以便相似的单词在向量空间中更接近。

将描述表示为词嵌入

为了将我们的文本描述转换为嵌入层,我们首先需要将每个描述转换为一个整数向量,对应于词汇表中的每个单词。我们可以使用方便的 Keras texts_to_sequences 方法做到这一点
train_embed = tokenize.texts_to_sequences(description_train)
test_embed = tokenize.texts_to_sequences(description_test)
现在我们已经获得了整数化的描述向量,我们需要确保它们都具有相同的长度,以便将它们输入我们的模型。Keras 也提供了一种方便的方法来实现这一点。我们将使用 pad_sequences 在每个描述向量中添加零,以便它们都具有相同的长度(我使用了 170 作为最大长度,这样就不会截断任何描述)
max_seq_length = 170
train_embed = keras.preprocessing.sequence.pad_sequences(train_embed, maxlen=max_seq_length)
test_embed = keras.preprocessing.sequence.pad_sequences(test_embed, maxlen=max_seq_length)
将我们的描述转换为具有相同长度的向量后,我们就可以创建嵌入层并将其输入到深层模型中。

构建深层模型

创建嵌入层有两种方法 - 我们可以使用来自预训练嵌入的权重(有很多开源词嵌入),或者我们可以从我们的词汇表中学习嵌入。最好尝试这两种方法,看看哪种方法对你的数据集效果更好。在这里,我们将使用学习到的嵌入。

首先,我们将定义深度模型输入的形状。然后将其输入到 Embedding 层。这里我使用了一个 8 维度的 Embedding 层(您可以尝试调整 Embedding 层的维度)。Embedding 层的输出将是一个三维向量,形状为:[批次大小,序列长度(本例中为 170),嵌入维度(本例中为 8)]。为了将我们的 Embedding 层连接到 Dense 全连接输出层,我们首先需要将其扁平化。
deep_inputs = layers.Input(shape=(max_seq_length,))
embedding = layers.Embedding(vocab_size, 8,   input_length=max_seq_length)(deep_inputs)
embedding = layers.Flatten()(embedding)
一旦 Embedding 层被扁平化,它就可以被输入到模型中并编译。
embed_out = layers.Dense(1, activation='linear')(embedding)
deep_model = Model(inputs=deep_inputs, outputs=embed_out)
deep_model.compile(loss='mse', optimizer='adam', metrics=['accuracy'])

第三部分:宽与深

一旦我们定义了两个模型,将它们组合起来就很简单。我们只需要创建一个层来连接每个模型的输出,然后将它们合并到一个全连接的 Dense 层,最后定义一个组合模型来合并每个模型的输入和输出。显然,由于每个模型都在预测相同的东西(价格),因此每个模型的输出或标签将是相同的。另外要注意,由于我们模型的输出是一个数值,因此我们不需要进行任何预处理——它已经处于正确的格式。
merged_out = layers.concatenate([wide_model.output, deep_model.output])
merged_out = layers.Dense(1)(merged_out)
combined_model = Model(wide_model.input + [deep_model.input], merged_out)
combined_model.compile(loss='mse',optimizer='adam', metrics=['accuracy'])
有了这些,就可以开始训练和评估了。您可以尝试不同的训练周期和批次大小,看看哪个最适合您的数据集。
# Training
combined_model.fit([description_bow_train, variety_train] + [train_embed], labels_train, epochs=10, batch_size=128)

# Evaluation
combined_model.evaluate([description_bow_test, variety_test] + [test_embed], labels_test, batch_size=128)

在训练好的模型上生成预测。

现在是时候进行最重要的部分了——看看我们的模型在以前没有见过的数据上的表现。为了做到这一点,我们可以对训练好的模型调用 predict() 方法,并将测试数据集(在以后的文章中,我将介绍如何从纯文本输入中获得预测)传递给它。
predictions = combined_model.predict([description_bow_test, variety_test] + [test_embed])
然后,我们将测试数据集前 15 种葡萄酒的预测值与实际值进行比较。
for i in range(15):
    val = predictions[i]
    print(description_test[i])
    print(val[0], 'Actual: ', labels_test.iloc[i], '\n')
模型表现如何?让我们看一下测试集中三个例子。
Powerful vanilla scents rise from the glass, but the fruit, even in this difficult vintage, comes out immediately. It's tart and sharp, with a strong herbal component, and the wine snaps into focus quickly with fruit, acid, tannin, herb and vanilla in equal proportion. Firm and tight, still quite young, this wine needs decanting and/or further bottle age to show its best.
Predicted:  46.233624 Actual:  45.0  

A good everyday wine. It's dry, full-bodied and has enough berry-cherry flavors to get by, wrapped into a smooth texture.
Predicted:  9.694958 Actual:  10.0  
Here's a modern, round and velvety Barolo (from Monforte d'Alba) that will appeal to those who love a thick and juicy style of wine. The aromas include lavender, allspice, cinnamon, white chocolate and vanilla. Tart berry flavors backed by crisp acidity and firm tannins give the mouthfeel determination and grit.
Predicted:  41.028854 Actual:  49.0
相当不错!事实证明,葡萄酒的描述与其价格之间存在某种关系。我们可能无法直观地看到它,但我们的机器学习模型可以。

下一步是什么?

我们在这里涵盖了很多内容,但总是有更多层 😉。在以后的文章中,我将介绍如何在云中训练这个模型。此外,训练好的模型并不是终点。如果您正在训练一个模型,那么您可能想要构建一个基于该模型进行预测的应用程序。在另一篇文章中,我将介绍如何在生产环境中使用该模型并构建一个应用程序来对其进行预测:输入葡萄酒描述,预测价格。

想要在 Keras 中构建自己的宽与深模型吗?查看该模型的完整代码 在 GitHub 上 并深入了解 Keras 函数式 API 文档。如果您有任何反馈,请在评论区或 Twitter @SRobTweets 上告诉我。干杯!🥂
下一篇文章
Predicting the price of wine with the Keras Functional API and TensorFlow

作者:Sara Robinson

你能给“优雅、细腻的单宁”、“成熟的黑醋栗香气”或“浓郁而烤香”标价吗?事实证明,机器学习模型可以做到。在这篇文章中,我将解释我如何使用 Keras(tf.keras)构建一个宽与深网络来预测葡萄酒的价格。对于那些不熟悉 Keras 的人来说,它是 TensorFlow 的高级 API,用于构建机器学习模型。一个…