如何构建商品定价模型?Mercari Price Suggestion Challenge 最佳方案出炉
雷锋网 AI 研习社按:为了自动为商品定价,日本著名的社区电子商务服务提供商 Mercari 举办了「Mercari Price Suggestion Challenge」大赛,旨在利用商品的名称、类型、描述等文本信息为卖家提供定价建议。
上面的两件毛衣,一件价值 335 美元,另一件价值 9.99 美元。对于我们人类来说,想要通过上面的描述为毛衣定价简直可以说是「不可能完成的任务」!
有了自然语言处理这个利器,它是否能帮助我们解决这个问题呢?该比赛围绕这个问题展开,雷锋网将比赛具体细节以及冠军方案编译整理如下:
比赛简介
本比赛分为两个阶段,在这个比赛的第一阶段中,选手被要求提交代码解决这个问题,并且代码需要在 Kaggle 比赛的服务器上用一个小时以内的时间生成答案。而在下一阶段中,选手被要求不能修改第一阶段使用过的代码。更为严苛的是,在这个阶段,数据规模增长到了第一阶段的 5 倍!这对项目代码的鲁棒性提出了很高的要求。本比赛采用均方根误差(RMSLE)作为评价选手提交的模型性能的标准,具体来说,均方根误差可以由如下图所示的方法计算得到:
其中,ϵ 是 RMSLE 的值,n 是在公开或者私密的数据集中观察的样本点的个数,p i 是我们预测的商品价格,a i 是商品实际的价格,log(x) 是 x 的自然对数。
数据集
本比赛使用的数据由 train.tsv 和 test.tsv 两个数据文件组成,数据文件中的内容由 tab 符分隔开来,每个文件包含以下字段:
-
train_id 和 test_id:条目的编号
-
name:商品名称
-
item_condition_id:卖家提供的商品的状态
-
category_name:商品的类别
-
brand-name:品牌名称
-
price:商品曾经的售价,这也是我们的预测目标
-
shipping:运费
-
item_description :对商品的文字描述
本次比赛奖池巨大,主办方一共提供了 10 万美金的奖金,吸引到 2384 支参赛队伍。Pawel 和 Konstantin 两位选手在分别提出了性能十分优秀的模型后,决定双剑合璧,取长补短。他们合并而成的队伍获得了本次比赛的大奖。下面让我们看看他们拔得头筹的方案吧!
Pawel 在组队之前的解决方案(RMSLE 大约为 0.3950)
在与 Konstantin 组队之前,Pawel 曾经设计了一个十分复杂的模型,该模型由三部分组成:
1. 针对每个类别的商品建立一个模型:Pawel 首先针对每个类别建立了 3 级的岭回归模型。这个 3 级模型可以看作基数 2 加上如「项目_条件_编号」这样形式的偏移。这个模型训练起来十分快,在 20 分钟内取得了 0.4050 的预测准确率。
2. 残差模型 MLP:在 1 中提到的模型的基础上,Pawel 接着在稀疏的输入数据上训练了一个神经网络模型。这个网络设计损失函数的总体目标是让预测值和真实数据之间的差别尽量小。这个模型的设计方法可以被视作强分类器的集成学习。
3. 残差模型 LGBM 模型:想法和 2 一样。
尽管为每个类别建立一个模型的想法似乎很棒,但是实际上并非如此!训练这样一个模型看起来很 cool,但是它会使每个模型的性能不能被充分发挥出来。我们可以这样想,神经网络是不能被训练去理解类别、描述、标签之间的交互信息的,这是一个错误的假设。我们的实验说明了,一个调试好的神经网络能够学习到它想要学到的知识。
Konstantin 在组队之前的解决方案 (RMSLE 大约为0.3920)
在合并之前他建立了两个模型:
1. 利用 TensorFlow 实现了一个稀疏的 MLP 模型。这个模型并没有非常复杂的数据特征,与公开的方案相比,仅仅改变了特征的数量,并且使用岭回归的 eli5 解释机制做了单词化切片的处理。这样依次训练了 3 个模型。
2. 在 Keras 框架下实现了一维卷积组成的卷积神经网络,这与在其它的很多方案中的情况相类似。模型本身的性能并不是非常优秀,但是它和 MLP 模型差别十分大,所以这个模型在集成学习框架中起到很好的效果。
融合
在组队的时候,Konstantin 排名第一,Pawel 排名第二。为了能够很好地继承各自模型的优点,并不能盲目地将两个解决方案拼凑起来,需要融合两个人的思路,创造出新的解决方案。
要让这个新的模型在满足各种约束的条件下正常工作是一件富有挑战性的事。对他们来说,训练时间并不是一个大麻烦,他们将更多的精力放在了筹集资金上,来购买硬件设备以便同时训练 4 个神经网络。
他们花了 2 个星期做出了一个两人都赞成的解决方案。结果表明,两个不同的预处理方案产生了许多解决方案需要的变量,这使得很容易地将准确率提升了百分之 1,这是一个很大的突破。
最后,他们一共使用了 3 个数据集,并且在每个数据集上建立了 4 个模型。他们尝试着通过以下方式建立更多样化的模型:
1. 不同的分词方式,带词干的和不带词干的
2. Countvectorizer 向量化技术/Tfidfvectorizer 向量化技术
构建系统
如果不将解决方案分解到各个模块中,想要管理整个项目十分困难。他们针对自己的代码创造构造系统。最终,他们使用这个系统写出了解决方案的 python 程序包。代码有些奇怪,伪代码看起来是这样的:
ENCODED_FILES = {some base64 encoded characters}
DECODE_FILES
INSTALL THE MERCARI PACKAGE
RUN MAIN FUNCTIONS
值得注意的是,按顺序执行这 3 个程序十分重要。这是因为 python 很有可能在执行了某些操作之后并不会清理内存,尤其是在数据预处理过程中。
数据预处理
有一些简单的数据处理技巧十分有效:
1. 基于名字字符的 N 元模型:尽管不能完全明白其背后的技术原理,但是使用基于项目名称字符串的 N 元模型(N-gram)能够有效地提升预测的 RMSLE。可能是因为这样得到了相对更稠密的数据特征。
2. 词根化算法:使用了标准的 PorterStemmer 分词算法。
3. 数值向量化:诸如「10 data 5 scientists」这样的数据描述是一个重要的错误来源,这样的数据描述往往被直观地向量化表示为(data=10, scientists=5)。他们仅仅在一个数据集中应用了这种向量化方法,并且使总体的 RMSLE 得分提升了 0.001。但他们没有更多的时间去验证这个想法。
4. 文本链接技术:为了减小直接将文本链接起来后的文本域的数据维度,他们测试了改变名字(name)、项目描述(item_description)、种类(category)、品牌(barnd)等参数之后的模型效果。通过这个步骤,RMSLE 得分提升了 0.37xx。
除此之外,一些自认为很棒的特征工程技术并不十分奏效。在这里,简单举几个例子:
1. 抽取诸如「for [Name]」这样的数据特征:他们注意到,很多数据项目仅仅出现在某些人的数据中。他们不能确定这究竟意味着什么,但它似乎很重要,可以创建一个特征类别。他们利用 nltk 工具创建了一个品名列表,并且通过 AhoCorasick 算法搜索相似的字符串。
2. 他们注意到,在商品的描述中有一些由于换行引起的问题。只要有人在描述中使用换行符,就会连接这样的单词。
3. 拼写检查
使用神经网络做特征提取就好比「好吧!我猜我能在这里用你做特征工程,你能提升 0.0003 的 RMSLE 得分」(意在说明神经网络是一个黑盒技术)
模型
Pawel 发现,当建立了一个经过精心调试之后的稀疏多层感知机(MLP)模型后,如果在不同的数据集上训练相同的模型会比在相同的数据集上训练不同的模型整体上得到更加多样化的结果。因此,他们决定坚持做出一个「精品」模型,这也确实简化了解决方案。
最基本的模型是一个有着稀疏输入的多层前馈神经网络,这个模型貌似平凡无奇,但令人意外的是,它确实很有效。他们认为,这个模型之所以性能优秀的一个原因可能是,它比其它的方法都要更高效。具体而言,数据集十分大的,所以模型必须能够处理大量的数据,并且能够捕获特征之间的相互作用。在相同时间内,一个一维卷积神经网络只能训练 4 个嵌入了 32 个节点的计算核心,他们的模型却能够训练 4 个有 256 个隐含节点的多层感知机模型。
细心学习进度调优是十分关键的:最重要的是,他们在每一轮迭代之后都将批处理数据的规模(batch size)扩大到之前的两倍。这使得他们的模型在每一轮迭代之后都能够被训练地更快,这也让模型最终获得了更好的性能。除了让批处理数据的规模加倍,他们还降低了学习率。
之前所做的这些工作让他们的模型在第二次迭代之后,在模型验证过程中获得了最高的 RMSLE 分数。之后,在第三次迭代之后,就出现了过拟合的情况。这样的情况让模型的整体性能更好。
和之前的方案( https://www.kaggle.com/lopuhin/mercari-golf-0-3875-cv-in-75-loc-1900-s )相比,他们在以下的几个方面进行了调整:
1. 使用了模型的变体:第一个是通过 Huber loss 作为损失函数训练的,这样做可以使得模型对于数据中的离群点不那么敏感;另外一个是把这个任务当作了一个分类问题而非回归问题。
对于这个回归问题,他们将所有的价格分配到 64 个区间内,并且为最终的分类预测制订了一个灵活的目标:他们首先计算了每个区间的中心之间的 L2 范数,之后对其应用 softmax 函数,这个 softmax 函数带有更高的 softmax 温度参数 T(温度参数 T 是一个超参数)。当 T 很大时,所有的激活值对应的激活概率趋近于相同(激活概率差异性较小),而当 T 很低时,不同的激活值对应的激活概率差异也就越大。由于过拟合得到了缓解,这个分类模型本身能够获得更高的 RMSLE 分数,也使得预测的多样性更强。
2. 在每个数据集上,对于 4 个模型中的 2 个,他们在训练和预测的过程中将所有的非零值设置为了 1,从而对于输入数据进行了二值化处理(离散化成 0 和 1)。这有点类似于得到一个通过二值的 CountVectorizer 技术产生的额外的数据集,而不是用 TFIDF 技术产生的数据大小没有约束的数据集。这个想法很赞!他们也试着通过其它非零的阈值进行二值化处理,但是这些做法都没有带来很大的性能提升。
3. 他们仅仅在第一层中使用了 L2 正则化项,这也将模型性能提升了一点点。他们发现,在 TensorFlow 环境下编写的模型使用 PRELU 作为激励函数时,会比使用 RELU 激励函数时取得更好的效果。
这个比赛除了对模型的理论构建有很高的要求,对于数据集的大小也有着非常严格的限制,让训练过程变得更加高效十分重要。扩大第一个隐藏层的节点数目很有可能提升预测的准确率得分。除了对数据集进行预处理,他们还训练了 12 个模型。最终,他们成功地在 200K 的数据上训练了 12 个有着 256 个隐藏节点的模型。以下是具体的实施方案:
1. TensorFlow 可以使用多个计算核心,但这并不是让模型性能线性增长,特别是对于稀疏的数据作为输入的模型。因此更好的解决方案是,每个模型使用一个计算核心,并行训练模型。具体而言,他们通过「OMP_NUM_THREADS=1」指令将 TensorFlow 环境下每个计算核心上的线程数设置为 1。接着,他们使用 TensorFlow 的配置变量,通过非正式的「use_per_session_treads=1」指令,允许 TensorFlow 使用多个核心线程。这就意味着不需要启动多个进程,还能使用更少的内存。
2. 结果表明 MXNet 框架能够在 CPU 上实现一个更高效的稀疏 MLP 模型。这是因为 MXNet 支持稀疏更新(当然,TensorFlow 也许支持,但是他们在这里没有使用 TensorFlow 的这个功能)。Pawel 写了一个 MXNet 版本,并且将速度提升了两倍。之后他们添加了 TensorFlow 模型提取出的所有特征,使它性能更好。一个值得注意的问题是,MXNet 的执行引擎不是线程安全的,如果你尝试使用线程来做并行计算,最终只能得到一个可用的计算核心,或者引发段错误(Segfault)。因此,他们不得不转而使用多进程计算。由于 MXNet 生成了大量的副本,并且使用了太多内存,他们也需要编写自己的数据生成器。我们还有一个版本,将数据放入共享内存中,但磁盘空间不足,所以也不得不放弃这个版本。
总的来说,MXNet 解决方案的速度更快,在不牺牲速度的条件下,允许使用较小的初始批处理数据规模。但使用了更大的内存,因此看起来不那么可靠。在最后,他们使用同样的数据集提交了两个版本。一个使用 MXNet(0.37758 private LB / 0.37665 public),另一个使用 TensorFlow(0.38006 private / 0.37920 public)。
最后,他们获得了 12 组预测结果,需要将其融合。平均之后效果会很好,调整混合权重更好。因此,他们使用了 1% 的数据集进行验证(其中 5% 来自本地),并且使用 Lasso 模型调整权重。Lasso 模型使用了 L1 正则化。
除此之外,他们将一些效果较差的模型列举如下:
1. 混合专家模型(MoE):这里有一篇很棒的论文(https://arxiv.org/abs/1701.06538),这篇文章描述在使用相同的计算资源时,如何训练一个模型得到更大的容量。但是很不幸,最终结果表明,TensorFlow 对一些操作缺乏稀疏支持。
2. 他们尝试加入一些外部的混合模型,例如:在同样的架构中将 FM 和 MLP 模型合并,并且在 MLP 的输出中添加一个线性回归的 skip 层。这些最终都会收敛到一个简单的 MLP 模型。
只有使用更大的 MLP 网络才能提高模型性能,但是由于比赛限制,显然不能这么做。只有添加更多的数据/特性时,模型才会得到改进。
比赛地址: https://www.kaggle.com/c/mercari-price-suggestion-challenge
方案地址: https://www.kaggle.com/c/mercari-price-suggestion-challenge/discussion/50256
雷锋网 (公众号:雷锋网) 编译整理。
。