你能把美元价格和 “优雅,细腻的单宁”,“成熟的醋栗香气” 或 “醇厚的烤面包香气” 联系在一起吗? 事实证明机器学习模型可以。 在这篇文章中,我将解释如何使用 Keras(tf.keras)建立一个 Wide & Deep 网络来预测其描述中的葡萄酒价格。对于那些刚接触 Keras 的人来说,它是用于构建 ML 模型的更高级别的 TensorFlow API。 如果您想直接跳到代码,可以在 GitHub 上找到它。 你也可以直接在浏览器中使用 Colab 进行零设置运行模型。
向 Francois,Josh 和 Yufeng 致敬,感谢他们对这篇文章的帮助和意见。
模型:Wide & Deep 与 Keras 结合
我最近一直在使用 Sequential Model API 构建了许多 Keras 模型(这里有一些例子),但我想尝试使用 Functional API。Sequential API 是开始使用 Keras 的最佳方式,它可以让你轻松地将模型定义为图层堆栈。Functional API 则更加灵活,它最适合具有多个输入或组合模型的模型。Functional API 的一个很好的实例是在 Keras 中执行 wide and deep 网络。关于学习 wide and deep 有很多很好的资源,因此我不会着重于细节,但如果你有兴趣了解更多,我推荐这篇文章。
在通过 wide & deep 网络解决ML问题之前,你最好确保它非常适合您尝试预测的内容。如果你有一个预测任务,输入和输出之间存在相对直接的关系,那么 wide 模型可能就足够了。 Wide 模型是具有稀疏特征向量的模型,或具有大多数零值的向量。另一方面,已知多层深度网络在诸如图像或语音识别之类的任务上表现良好,其中输入和输出之间可能存在意外关系。如果你有一个可以从这两个模型中受益的预测任务(推荐模型或带有文本输入的模型都是很好的例子),那么 wide & deep 可能是一个很好的选择。在这种情况下,我分别尝试了 Wide 模型和 Deep 模型,然后将它们组合在一起,结果发现 Wide & Deep 在一起的情况下精确度表现最好。让我们深入了解一下吧。
数据集:预测葡萄酒的价格
我们将使用 Kaggle 葡萄酒数据集 this wine dataset 来查看:
我们可以从描述和品种中预测出一瓶葡萄酒的价格吗?
这个问题非常适合 Wide & Deep 的学习,因为它涉及到文本输入,并且葡萄酒的描述与其价格之间并没有明显的相关性。我们无法斩钉截铁地说,描述中带有 “果味” 一词的葡萄酒更贵,或者有 “柔和的单宁” 描述的葡萄酒更便宜。此外,当我们将文本提供给模型时,有很多种方式来表示文本,两者都可以导致不同类型的见解。有 Wide 表示(词袋)和 Deep 表达(嵌入)两种方式,将两者结合起来可以让我们从文本中提取更多的含义。这个数据集有很多不同的功能可能性,但我们只使用描述以及品种来进行相对简化。以下是此数据集的示例输入和预测:
输入
描述: 馥郁的香草气息从杯中升起,尽管是处于这个葡萄生长艰难的年份,果味也立即出现。 它的酸味和尖锐,带着浓烈的草药香,葡萄酒迅速成熟,果味,酸味,单宁,草本植物和香草味道的比例相当。 这款葡萄酒醇厚而紧实,它还很年轻,需要醒酒和/或更长时间的醒酒瓶才能展现出最佳效果。
品种: 黑皮诺
预测
价格 — 45美元
首先,以下是我们构建此模型所需的所有导入:
1 import os
2 import numpy as np
3 import pandas as pd
4 import tensorflow as tf
5
6 from sklearn.preprocessing import LabelEncoder
7
8 from tensorflow import keras
9 layers = keras.layers
10
11 # This code was tested with TensorFlow v1.7
12 print("You have TensorFlow version", tf.__version__)
由于我们的模型输出(预测)是数字的价格,我们将价格值直接提供给我们的模型进行训练和评估。 GitHub 上提供了此模型的完整代码。 在这里,我将重点介绍关键点。
首先,让我们下载数据并将其转换为 Pandas 数据结构:
1 ! wget -q https://storage.googleapis.com/sara-cloud-ml/wine_data.csv
2 data = pd.read_csv("wine_data.csv")
接下来,我们将其拆分为训练和测试集并提取功能和标签:
1 train_size = int(len(data) * .8)
2
3 # Train features
4 description_train = data['description'][:train_size]
5 variety_train = data['variety'][:train_size]
6
7 # Train labels
8 labels_train = data['price'][:train_size]
9
10 # Test features
11 description_test = data['description'][train_size:]
12 variety_test = data['variety'][train_size:]
13
14 # Test labels
15 labels_test = data['price'][train_size:]
Part 1:Wide 模型
特征1:葡萄酒描述
为了创建我们的文本描述的 Wide 表示,我们将使用一个词袋模型。 Here 有更多相关内容,但在这里我们快速回顾一下:一个词袋模型会在模型的每个输入中查找单词的存在。你可以将每个输入视为一袋 Scrabble 图块,其中每个图块包含一个单词而不是一个字母。该模型没有考虑描述中单词的顺序,仅考虑单词的存在与否。
注:Here 链接
https://en.wikipedia.org/wiki/Bag-of-words_model
将一袋单词模型的输入想象为 Scrabble tiles,其中每个 tile 包含来自输入的单词(而不是字母)
我们不会去逐一查看数据集中每个描述里的每一个单词,而是将我们的单词数量限制在数据集中的前 12,000 个单词中(不用担心,还有一个用于创建此词汇表的内置 Keras 实用程序)。这就被认为是 “Wide”,因为我们的模型对每个描述的输入将是 12k 元素 Wide 的向量,其中 1 和 0 表示在特定描述中存在来自词汇表的单词。
Keras 有一些方便的文本预处理实用程序,我们将用它们将文本描述转换成一个词袋。使用词袋模型,我们通常只希望在词汇表中包含我们数据集中找到的总单词的子集。 在这个例子中,我使用了 12,000 个单词,但这是一个可以调整的超参数(尝试一些值并查看数据集的最佳效果)。 我们可以使用 Keras Tokenizer 类创建我们的词袋词汇:
1 vocab_size = 12000
2 tokenize = keras.preprocessing.text.Tokenizer(num_words=vocab_size, char_level=False)
3 tokenize.fit_on_texts(description_train) # only fit on train
然后我们将使用 texts_to_matrix 函数将每个描述转换为词袋向量:
1 description_bow_train = tokenize.texts_to_matrix(description_train)
2 description_bow_test = tokenize.texts_to_matrix(description_test)
特征2: 葡萄酒品种
在原始的 Kaggle 数据集中,共有 632 种葡萄酒品种。 为了让我们的模型更容易提取模式,我做了一些预处理,只保留了前 40 个品种(约占原始数据集的 65%,总共 96k 大小的范例)。 我们将使用 Keras 实用程序将这些变量中的每一个转换为整数表示,然后我们将为每个输入创建 40 个元素宽度的的单热矢量以指示变化:
1 # Use sklearn utility to convert label strings to numbered index
2 encoder = LabelEncoder()
3 encoder.fit(variety_train)
4 variety_train = encoder.transform(variety_train)
5 variety_test = encoder.transform(variety_test)
6 num_classes = np.max(variety_train) + 1
7
8 # Convert labels to one hot
9 variety_train = keras.utils.to_categorical(variety_train, num_classes)
10 variety_test = keras.utils.to_categorical(variety_test, num_classes)
现在我们准备搭建 Wide 模型。
用 Keras functional API 搭建 Wide 模型
Keras 有两个用于构建模型的 API:Sequential API 和 Functional API。 Functional API 为我们定义图层的方式提供了更多的灵活性,并允许我们将多个特征输入组合到一个图层中。 当一切就绪,它还可以轻松地将我们的 Wide 模型和 Deep 模型组合二为一。 使用 Functional API,我们只使用几行代码就能轻松定义 Wide 模型。首先,我们将输入层定义为 12k 元素向量(对于词汇表中的每个单词)。然后我们将它连接到我们的 Dense 输出层以生成价格预测:
1 bow_inputs = layers.Input(shape=(vocab_size,))
2 variety_inputs = layers.Input(shape=(num_classes,))
3 merged_layer = layers.concatenate([bow_inputs, variety_inputs])
4 merged_layer = layers.Dense(256, activation='relu')(merged_layer)
5 predictions = layers.Dense(1)(merged_layer)
6 wide_model = Model(inputs=[bow_inputs, variety_inputs], outputs=predictions)
然后我们编译模型,这样就可以使用:
1 wide_model.compile(loss='mse', optimizer='adam', metrics=['accuracy'])
如果我们自己使用 Wide 模型,那么我们将使用 fit() 和 evaluate() 进行评估。由于我们稍后会将它与我们的 Deep 模型相结合,我们可以暂停训练,直到两个模型结合起来。现在是时候建立我们的 Deep 模型了!
Part 2:Deep 模型
为了创建葡萄酒描述的 Deep 表示,我们将其表示为嵌入。关于单词嵌入有很多资源,简而言之是它们提供了一种将单词映射到向量的方法,以便相似的单词在向量空间中更加靠近。
将描述表示为单词嵌入
要将我们的文本描述转换为嵌入层,我们首先需要将每个描述转换为与词汇表中每个单词对应的整数向量。我们可以用方便的 Keras text_to_sequencesmethod 来做到这一点:
1 train_embed = tokenize.texts_to_sequences(description_train)
2 test_embed = tokenize.texts_to_sequences(description_test)
现在我们已经有了整数描述向量,我们需要确保它们的长度都相同,以便将它们输入到我们的模型中。 Keras 也有一个方便的方法。我们将使用 pad_sequ 为了创建葡萄酒描述的深层表示,我们将其表示为嵌入。关于单词嵌入有很多资源,但简短的版本是它们提供了一种将单词映射到向量的方法,以便相似的单词在向量空间中更加接近。为每个描述向量添加零以使它们的长度相同(我使用 170 作为最大长度,这样就没有任何描述被缩短:
1 max_seq_length = 170
2 train_embed = keras.preprocessing.sequence.pad_sequences(train_embed, maxlen=max_seq_length)
3 test_embed = keras.preprocessing.sequence.pad_sequences(test_embed, maxlen=max_seq_length)
将我们的描述转换为长度相同的矢量,我们已准备好创建嵌入层并将其输入 Deep 模型。
创建 Deep 模型
要创建嵌入层有两种方法 - 我们可以使用预训练嵌入的权重(有许多开源词嵌入),或者我们可以从词汇表中学习嵌入。最好先试验这两种方法,看看哪一个在数据集上表现更好。在这里,我们将使用学习嵌入。
首先,我们将定义 Deep 模型输入的形状。然后我们将它提供给嵌入层。 这里我使用的是 8 维的嵌入图层(您可以尝试调整嵌入图层的维度)。嵌入层的输出将是具有形状的三维矢量:[批量大小,序列长度(在该示例中为 170),嵌入维度(在该示例中为 8)]。为了将我们的嵌入层连接到密集,完全连接的输出层,我们需要先将其展平:
1 deep_inputs = layers.Input(shape=(max_seq_length,))
2 embedding = layers.Embedding(vocab_size, 8, 3 input_length=max_seq_length)(deep_inputs)
embedding = layers.Flatten()(embedding)
一旦嵌入层变平,就可以将其输入模型并进行编译:
1 embed_out = layers.Dense(1, activation='linear')(embedding)
2 deep_model = Model(inputs=deep_inputs, outputs=embed_out)
3 deep_model.compile(loss='mse', optimizer='adam', metrics=['accuracy'])
Part 3:Wide 和 Deep
一旦我们定义了两个模型,将它们组合起来就很容易。 我们只需要创建一个连接每个模型的输出图层,然后将它们合并到一个完全连接的 Dense 图层中,最后定义一个组合模型,它将每个模型的输入和输出结合起来。显然,由于每个模型都预测相同的事情(价格),因此每个模型的输出或标签都将是相同的。另请注意,由于我们的模型输出是一个数值,我们不需要进行任何预处理 - 它已经是正确的格式:
1 merged_out = layers.concatenate([wide_model.output, deep_model.output])
2 merged_out = layers.Dense(1)(merged_out)
3 combined_model = Model(wide_model.input + [deep_model.input], merged_out)
4 combined_model.compile(loss='mse',optimizer='adam', metrics=['accuracy'])
有了这个,就该进行培训和评估了。你可以尝试最适合您的数据集的训练时期和批量大小的训练次数:
1 # Training
2 combined_model.fit([description_bow_train, variety_train] + [train_embed], labels_train, epochs=10, batch_size=128)
3
4 # Evaluation
5 combined_model.evaluate([description_bow_test, variety_test] + [test_embed], labels_test, batch_size=128)
在我们训练模型中生成预测
到了最关键部分的时间了。了解我们的模型对之前从未见过的数据如何表现。 为此,我们可以在我们训练的模型上调用 predict(),并将测试数据集传递给它(在以后的文章中我将介绍如何从纯文本输入中获取预测):
1 predictions = combined_model.predict([description_bow_test, variety_test] + [test_embed])
然后我们将预测与我们测试数据集中前 15 种葡萄酒的实际价格进行比较:
1 for i in range(15):
2 val = predictions[i]
3 print(description_test[i])
4 print(val[0], 'Actual: ', labels_test.iloc[i], ' ')
模型是怎么做的?我们来看看测试集中的三个例子:
馥郁的香草气息从杯中升起,尽管是处于葡萄生长艰难的年份,果味也立即出现。它的酸味和尖锐,带着浓烈的草药香,葡萄酒迅速成熟,果味,酸味,单宁,草本植物和香草味道的比例相当。这款葡萄酒醇厚而紧实,它还很年轻,需要醒酒和/或更长时间的醒酒瓶才能展现出最佳效果。
预测价格: 46.233624 实际价格: 45.0
一款美味的日常酒。它是干型的,浓郁,足够的浆果樱桃香,包裹成光滑的质地。
预测价格: 9.694958 实际价格: 10.0
这是一款现代,圆润,天鹅绒般的巴罗罗(来自 Monforte d'Alba),适合那些喜欢醇厚多汁的葡萄酒的人。香气包含薰衣草,五香粉,肉桂,白巧克力和香草。 酸味浆果口味附带着酸甜的口感和结实的单宁赋予了肯定和坚韧的口感。
预测价格: 41.028854 实际价格: 49.0
很不错! 事实证明,葡萄酒的描述与其价格之间存在某种关系。我们可能无法本能地看到它,但我们的 ML 模型可以。
全部0条评论
快来发表一下你的评论吧 !