投稿/爆料
厂商入驻

机器学习开放课程:八、使用Vowpal Wabbit高速学习大规模数据集

论智|2018-07-16 13:08

【编者按】机器学习开放课程第八课,Mail.Ru数据科学家Yury Kashnitsky讲解了随机梯度下降、类别数据编码、Vowpal Wabbit机器学习库。

题图

这一课我们将从理论和实践的角度介绍Vowpal Wabbit训练速度非同寻常的原因,在线学习和哈希技巧。我们将在新闻、影评、StackOverflow问题上尝试Vowpal Wabbit。

概览

  1. 随机梯度下降和在线学习

    • SGD
    • 在线学习方法
  2. 类别数据处理

    • 标签编码
    • 独热编码
    • 哈希技巧
  3. Vowpal Wabbit

    • 新闻:二元分类
    • 新闻:多元分类
    • IMDB影评
    • 分类StackOverflow问题
  4. 相关资源

1. 随机梯度下降和在线学习

1.1 随机梯度下降

回顾一下,梯度下降的想法是通过在下降最快的方向上小步前进,以最小化某个函数。这一方法得名于以下微积分的事实:函数f(x) = f(x1, …, xn)的偏导数向量

梯度向量

指向函数增长最快的方向。这意味着,向相反方向移动(逆梯度),可能以最快的速度降低函数值。

谢列格什滑雪场

俄罗斯最受欢迎的冬季度假胜地——谢列格什滑雪场,踩着滑雪板的人为本文作者

除了宣传美丽的风光,上面的照片描绘了梯度下降的概念。如果你想滑得尽可能快,你需要选择最陡峭的下降路径。计算逆梯度可以看成评估不同点的坡度。

例子

我们将通过梯度下降求解一个成对回归问题(paired regression problem)。让我们根据一个变量预测另一个变量:根据体重预测身高。我们将假定这些变量是线性相关的。另外,我们将使用的是SOCR数据集。

首先我们导入数据,并绘制散布图:

import warnings
warnings.filterwarnings('ignore')
import os
import re
import numpy as np
import pandas as pd
from tqdm import tqdm_notebook
from sklearn.datasets import fetch_20newsgroups, load_files
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, accuracy_score, log_loss
from sklearn.metrics import roc_auc_score, roc_curve, confusion_matrix
from scipy.sparse import csr_matrix
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns

PATH_TO_ALL_DATA = '../../data/'
data_demo = pd.read_csv(os.path.join(PATH_TO_ALL_DATA,
                                     'weights_heights.csv'))
plt.scatter(data_demo['Weight'], data_demo['Height']);
plt.xlabel('Weight in lb')
plt.ylabel('Height in inches');

身高体重散布图

我们有一个l维向量x(每个人的体重,也就是训练样本)和向量y(包含数据集中每个人的身高)。

我们要完成的任务是:找到满足以下条件的权重w0和w1,使预测身高yi = w0 + w1xi最小化以下平方误差(等效于最小化均方误差,因为1/l并不会带来什么不同):

平方误差

我们将使用梯度下降,利用SE(w0, w1)在权重w0和w1上的偏导数。以下简单的更新公式定义了迭代训练过程:

梯度下降更新权重

展开偏导数后,我们得到:

展开偏导数

在数据量不大的情况下,上面的数学效果不错(我们这里不讨论局部极小值、鞍点、学习率选择、动量等问题,请参考《深度学习》一书的数值计算那一章)。批量梯度下降有一个问题——梯度演算需要累加训练集中所有对象的值。换句话说,该算法需要大量迭代,而每次迭代重新计算权重的过程中都包含累加整个训练集的运算。如果我们有数十亿训练样本,怎么办?

算不过来

这正是随机梯度下降的动机!简单来说,我们扔掉累加符号,仅仅根据单个训练样本或一小部分训练样本更新权重:

扔掉累加

这个方法无法保证我们在每次迭代中以最佳的方向移动。因此,我们可能需要更多的迭代,不过我们的权重更新会快很多。

吴恩达的机器学习课程很好地讲解了这一点。让我们来看一下。

SGD过程

这是某个函数的等值线图,我们想要找出该函数的全局最小值。红线展示了权重变动(图中的θ0和θ1相当于我们的w0和w1)。根据梯度的性质,每点的变动方向垂直于等值线。随机梯度下降时,权重以更难预测的方式变动(紫线),我们甚至可以看到,有些步骤是错误的,正远离最小值;然而,梯度下降和随机梯度下降这两个过程均收敛于同一解。

1.2 在线学习方法

在随机梯度下降的实践指导下,我们可以在多达数百GB的数据上训练分类器和回归器。

考虑成对回归的情形,我们可以将训练数据集(X, y)保存在硬盘上,而不是将整个训练数据集载入内存(内存放不下),然后逐个读取数据,更新模型的权重:

SGD过程

在处理完整个训练数据集后,我们的损失函数会下降,不过通常需要几十个epoch之后损失函数的值才足够小。

这一学习的方法称为在线学习,早在机器学习MOOC成为主流之前,这一术语就出现了。

在线学习术语

这里我们没有讨论SGD的很多细节。如果你想要深入这一理论,我强烈推荐Stephen Boyd写的《Convex Optimization》一书。现在,我们将介绍Vowpal Wabbit库,感谢随机优化和特征哈希,它非常擅长在大规模数据集上训练简单模型。

在scikit-learn中,基于SGD训练的分类器和回归器称为SGDClassifierSGDRegressor(见sklearn.linear_model)。这些是很好的SGD实现,不过我们将使用VW,因为在许多方面,它的性能比sklean的SGD模型要好。

2. 类别数据处理

2.1 标签编码

许多分类算法和回归算法基于欧几里得空间运作,这意味着数据表示为由实数组成的向量。然而,真实数据中我们常常碰到具有离散值的类别变量,比如是/否,一月/二月/…/十二月。下面我们将讨论如何处理这类数据,特别是配合线性模型使用的情况下。

让我们探索一下UCI bank marketing数据集,其中大部分特征是类别特征。

df = pd.read_csv(os.path.join(PATH_TO_ALL_DATA, 'bank_train.csv'))
labels = pd.read_csv(os.path.join(PATH_TO_ALL_DATA,
                                  'bank_train_target.csv'), header=None)

df.head()

UCI银行市场

你可以看到,大部分特征并不由数字表示。这就带来了一个问题,我们无法直接使用大多数机器学习方法(至少就那些scikit-learn实现的而言)。

让我们深入查看一下“教育”特征。

df['education'].value_counts().plot.barh();

教育特征

最直截了当的方案是将这一特征的每个值映射为唯一的数字。例如,我们可以将university.degree映射为0,basic.9y映射为1,等等。我们可以使用sklearn.preprocessing.LabelEncoder进行这一映射。

label_encoder = LabelEncoder()

mapped_education = pd.Series(label_encoder.fit_transform(
    df['education']))
mapped_education.value_counts().plot.barh()
print(dict(enumerate(label_encoder.classes_)))

输出:

{0: 'basic.4y', 1: 'basic.6y', 2: 'basic.9y', 3: 'high.school', 4: 'illiterate', 5: 'professional.course', 6: 'university.degree', 7: 'unknown'}
df['education'] = mapped_education
df.head()

标签编码后的教育特征

同样,我们转换其他列:

categorical_columns = df.columns[df.dtypes 
                                 == 'object'].union(['education'])
for column in categorical_columns:
    df[column] = label_encoder.fit_transform(df[column])
df.head()

标签编码其他列

这种方法的主要问题是我们现在引入了一些可能并不存在的相对顺序。

例如,我们隐式地引入了职业特征的代数,我们现在可以从客户一的职业中减去客户二的职业:

df.loc[1].job - df.loc[2].job  # -1.0

这样的操作有意义吗?没有。让我们尝试基于这一特征转换训练逻辑回归。

def logistic_regression_accuracy_on(dataframe, labels):
    features = dataframe.as_matrix()
    train_features, test_features, train_labels, test_labels = \
        train_test_split(features, labels)

    logit = LogisticRegression()
    logit.fit(train_features, train_labels)
    return classification_report(test_labels, 
                                 logit.predict(test_features))

print(logistic_regression_accuracy_on(df[categorical_columns], 
labels))

标签编码和逻辑回归

我们可以看到,逻辑回归从未预测分类1. 为了在类别特征上使用线性模型,我们需要使用一种不同的方法:独热编码(One-Hot Encoding)。

2.2 独热编码

假设某项特征可能有10个唯一值。独热编码为每个唯一值创建一个新特征,这10个特征中,除了一个特征以外,所有特征的值为零。

独热编码例子

sklearn.preprocessingOneHotEncoder类实现了独热编码。默认情况下,OneHotEncoder将数据转换为一个稀疏矩阵,以节约内存空间。不过,在这一特定问题中,我们没有碰到内存问题,所以我们将使用“密集”矩阵表示。

onehot_encoder = OneHotEncoder(sparse=False)
encoded_categorical_columns = \
pd.DataFrame(onehot_encoder.fit_transform(
    df[categorical_columns]))
encoded_categorical_columns.head()

独热编码矩阵表示

转换维独热编码之后,就可以使用线性模型了:

print(logistic_regression_accuracy_on(encoded_categorical_columns,  labels))

独热编码和逻辑回归

2.3 哈希技巧

真实数据可能是易变的,意味着我们无法保证类别特征不会出现新值。这一问题阻碍了训练好的模型在新数据上的应用。除此以外,LabelEncoder需要对整个数据集进行初步分析,并将构建的映射保存在内存中,这使得在大型数据集上运用标签编码变得困难。

有一个基于哈希的向量化类别数据的简单方法,毫不意外地,它被称为哈希技巧。

哈希函数可以帮助我们为不同的特征值找到唯一的编码,例如:

for s in ('university.degree', 'high.school', 'illiterate'):
print(s, '->', hash(s))

结果:

university.degree -> -6241459093488141593
high.school -> 7728198035707179500
illiterate -> -7360093633803373451

我们不打算使用负值,或者数量级很大的值,所以我们将限制哈希值的范围:

hash_space = 25
for s in ('university.degree', 'high.school', 'illiterate'):
print(s, '->', hash(s) % hash_space)
university.degree -> 7
high.school -> 0
illiterate -> 24

想象下我们的数据集包含一个单身学生,他在周一接到一个电话。他的特征向量会类似于通过独热编码创建的向量:

hashing_example = pd.DataFrame([{i: 0.0 for i in range(hash_space)}])
for s in ('job=student', 'marital=single', 'day_of_week=mon'):
    print(s, '->', hash(s) % hash_space)
    hashing_example.loc[0, hash(s) % hash_space] = 1
hashing_example
job=student -> 20
marital=single -> 23
day_of_week=mon -> 9

哈希学生向量

我们哈希的不是特征值,而是特征名 + 特征值对。这样我们就可以区分不同特征的相同值。

使用哈希编码可能会遇到碰撞吗?当然有可能,不过只要哈希空间足够大,碰撞很罕见。即使碰撞真的发生了,回归或分类表现也不会受多大影响。在这一情形下,哈希碰撞就像是一种正则化的形式。

WTF

你也许会说“尼玛这什么玩意?”;哈希看起来就违背直觉。然而,事实上,有时这是唯一可行的处理类别数据的方法。而且,这一技术已被证实就是好使。等你处理了足够多的数据之后,你可能自己意识到这一点。

3. Vowpal Wabbit

Vowpal Wabbit(VW)是业界使用最广泛的机器学习库之一。它的训练速度很快,支持许多训练模式,特别是在大数据和高维数据方面表现出色。同时,由于VM实现了哈希技巧,它是一个处理文本数据的完美选择。

VW可以作为命令行工具使用。输入以下命令访问VW的帮助页面:

vw --help

vw可以从文件或stdin读取数据,数据格式如下:

[Label] [Importance] [Tag]|Namespace Features |Namespace Features ... |Namespace Features

Namespace=String[:Value]

Features=(String[:Value] )*

其中,[]表示可选元素,(...)*表示接受多个输入。

  • Label(标签)是一个数字。在分类问题中,它通常是1或-1;在回归问题中,它是一个实数(浮点数)。

  • Importance (重要性)是一个数字。它指明了样本的权重。处理失衡数据时,设定Importance很有用。

  • Tag (标记)是不含空格的字符串。它是样本的“名称”。

  • Namespace (命名空间)用于创建不同的特征空间。

  • Features 是给定Namespace中的特征。特征默认权重为1.0,但可以调整,例如feature:0.1

例如,以下字符串匹配VW格式:

1 1.0 |Subject WHAT car is this |Organization University of Maryland:0.5 College Park

我们可以将其传给vw

echo '1 1.0 |Subject WHAT car is this |Organization University of Maryland:0.5 College Park' | vw

VW是一个非常棒的处理文本数据的工具。我们将通过20newsgroups数据集展示这一点,该数据集包含来自20种不同新闻组的信息。

3.1 新闻:二元分类

使用sklearn函数加载数据:

newsgroups = fetch_20newsgroups(PATH_TO_ALL_DATA)

newsgroups['target_names']

新闻组的20项主题为:

['alt.atheism',
 'comp.graphics',
 'comp.os.ms-windows.misc',
 'comp.sys.ibm.pc.hardware',
 'comp.sys.mac.hardware',
 'comp.windows.x',
 'misc.forsale',
 'rec.autos',
 'rec.motorcycles',
 'rec.sport.baseball',
 'rec.sport.hockey',
 'sci.crypt',
 'sci.electronics',
 'sci.med',
 'sci.space',
 'soc.religion.christian',
 'talk.politics.guns',
 'talk.politics.mideast',
 'talk.politics.misc',
 'talk.religion.misc']

让我们看下第一封消息:

text = newsgroups['data'][0]
target = newsgroups['target_names'][newsgroups['target'][0]]

print('-----')
print(target)
print('-----')
print(text.strip())
print('----')

输出:

-----
rec.autos
-----
From: lerxst@wam.umd.edu (where's my thing)
Subject: WHAT car is this!?
Nntp-Posting-Host: rac3.wam.umd.edu
Organization: University of Maryland, College Park
Lines: 15

 I was wondering if anyone out there could enlighten me on this car I saw
the other day. It was a 2-door sports car, looked to be from the late 60s/
early 70s. It was called a Bricklin. The doors were really small. In addition,
the front bumper was separate from the rest of the body. This is 
all I know. If anyone can tellme a model name, engine specs, years
of production, where this car is made, history, or whatever info you
have on this funky looking car, please e-mail.

Thanks,
- IL
   ---- brought to you by your neighborhood Lerxst ----
----

现在我们将把数据转换为Vowpal Wabbit可以理解的格式。我们将丢弃所有短于3个符号的单词。这里,我们跳过了一些重要的NLP步骤,像是词干提取和词形还原;不过,我们之后将看到,即使没有这些步骤,VW仍然解决了问题。

def to_vw_format(document, label=None):
    return str(label or '') + ' |text ' + ' '.join(re.findall('\w{3,}', 
                                               document.lower())) + '\n'

to_vw_format(text, 1 if target == 'rec.autos' else -1)

输出:

'1 |text from lerxst wam umd edu where thing subject what car this nntp posting host rac3 wam umd edu organization university maryland college park lines was wondering anyone out there could enlighten this car saw the other day was door sports car looked from the late 60s early 70s was called bricklin the doors were really small addition the front bumper was separate from the rest the body this all know anyone can tellme model name engine specs years production where this car made history whatever info you have this funky looking car please mail thanks brought you your neighborhood lerxst\n'

我们将数据集分为训练集和测试集,并将其分别写入不同的文件。如果一份文档和rec.autos相关,那么我们就将它视作正面样本。所以,我们正构建一个模型,区分出汽车有关的文章:

all_documents = newsgroups['data']
all_targets = [1 if newsgroups['target_names'][target] == 'rec.autos' 
               else -1 for target in newsgroups['target']]

train_documents, test_documents, train_labels, test_labels = \
    train_test_split(all_documents, all_targets, random_state=7)

with open(os.path.join(PATH_TO_ALL_DATA, '20news_train.vw'), 'w') as vw_train_data:
    for text, target in zip(train_documents, train_labels):
        vw_train_data.write(to_vw_format(text, target))
with open(os.path.join(PATH_TO_ALL_DATA, '20news_test.vw'), 'w') as vw_test_data:
    for text in test_documents:
        vw_test_data.write(to_vw_format(text))

现在,我们将创建的训练文件传给Vowpal Wabbit。我们通过铰链(hinge)损失函数(线性SVM)求解这一分类问题。训练好的模型将保存在20news_model.vw文件中:

vw -d $PATH_TO_ALL_DATA/20news_train.vw \
 --loss_function hinge -f $PATH_TO_ALL_DATA/20news_model.vw

输出:

final_regressor = ../../data//20news_model.vw
Num weight bits = 18
learning rate = 0.5
initial_t = 0
power_t = 0.5
using no cache
Reading datafile = ../../data//20news_train.vw
num sources = 1
average  since         example        example  current  current  current
loss     last          counter         weight    label  predict features
1.000000 1.000000            1            1.0  -1.0000   0.0000      157
0.911276 0.822551            2            2.0  -1.0000  -0.1774      159
0.605793 0.300311            4            4.0  -1.0000  -0.3994       92
0.419594 0.233394            8            8.0  -1.0000  -0.8167      129
0.313998 0.208402           16           16.0  -1.0000  -0.6509      108
0.196014 0.078029           32           32.0  -1.0000  -1.0000      115
0.183158 0.170302           64           64.0  -1.0000  -0.7072      114
0.261046 0.338935          128          128.0   1.0000  -0.7900      110
0.262910 0.264774          256          256.0  -1.0000  -0.6425       44
0.216663 0.170415          512          512.0  -1.0000  -1.0000      160
0.176710 0.136757         1024         1024.0  -1.0000  -1.0000      194
0.134541 0.092371         2048         2048.0  -1.0000  -1.0000      438
0.104403 0.074266         4096         4096.0  -1.0000  -1.0000      644
0.081329 0.058255         8192         8192.0  -1.0000  -1.0000      174

finished run
number of examples per pass = 8485
passes used = 1
weighted example sum = 8485.000000
weighted label sum = -7555.000000
average loss = 0.079837
best constant = -1.000000
best constant's loss = 0.109605
total feature number = 2048932

VW在训练时会打印很多信息(你可以通过--quiet参数让VW少输出信息)。关于VW输出信息的说明,可以参考GitHub上的文档。就目前而言,我们可以看到,随着训练的进行,平均损失下降了。VW使用之前未见的样本计算损失,所以VW的平均损失通常比较准确。现在,我们将训练好的模型应用于测试集,并将预测保存到由-p指定的文件:

vw -i $PATH_TO_ALL_DATA/20news_model.vw -t -d $PATH_TO_ALL_DATA/20news_test.vw \
-p $PATH_TO_ALL_DATA/20news_test_predictions.txt

现在我们加载预测,计算AUC,并绘制ROC曲线:

with open(os.path.join(PATH_TO_ALL_DATA, 
                       '20news_test_predictions.txt')) as pred_file:
    test_prediction = [float(label) 
                       for label in pred_file.readlines()]

auc = roc_auc_score(test_labels, test_prediction)
roc_curve = roc_curve(test_labels, test_prediction)

with plt.xkcd():
    plt.plot(roc_curve[0], roc_curve[1]);
    plt.plot([0,1], [0,1])
    plt.xlabel('FPR'); plt.ylabel('TPR'); 
    plt.title('test AUC = %f' % (auc)); 
plt.axis([-0.05,1.05,-0.05,1.05]);

新闻二元分类ROC曲线

可以看到,我们达到了很高的分类质量。

3.2 新闻:多元分类

我们仍将使用之前的新闻组数据集。不过,这次我们将解决一个多元分类问题。VW要求标签从1开始,而sklearn的LabelEncoder的标签则从0开始。因此,我们需要在LabelEncoder的编码上加1:

all_documents = newsgroups['data']
topic_encoder = LabelEncoder()
all_targets_mult = topic_encoder.fit_transform(newsgroups['target']) + 1

仍然像之前一样,我们切分训练集和测试集,并保存到不同文件。

train_documents, test_documents, train_labels_mult, test_labels_mult = \
    train_test_split(all_documents, all_targets_mult, random_state=7)

with open(os.path.join(PATH_TO_ALL_DATA, 
                       '20news_train_mult.vw'), 'w') as vw_train_data:
    for text, target in zip(train_documents, train_labels_mult):
        vw_train_data.write(to_vw_format(text, target))
with open(os.path.join(PATH_TO_ALL_DATA, 
                       '20news_test_mult.vw'), 'w') as vw_test_data:
    for text in test_documents:
vw_test_data.write(to_vw_format(text))

我们将在多元分类模式下训练Vowpal Wabbit,在oaa参数中传入分类的数目。同时,让我们看下模型的一些参数(更多信息可以在Vowpal Wabbit的官方教程中找到):

  • 学习率(-l,默认0.5)每步权重改变的比率
  • 学习率衰减(--power_t,默认0.5)实践表明,如果学习率随着随机梯度下降的推进而下降,我们能更好地逼近损失的最小值
  • 损失函数(--loss_function)整个训练算法取决于损失函数的选择。可以参考损失函数的文档
  • 正则化(-l1)注意VW为每个对象计算正则化。所以我们通常将正则值设为10-20左右。

此外,你也可以尝试使用Hyperopt自动调整Vowpal Wabbit参数。

vw — oaa 20 $PATH_TO_ALL_DATA/20news_train_mult.vw -f $PATH_TO_ALL_DATA/20news_model_mult.vw \
 — loss_function=hinge

vw -i $PATH_TO_ALL_DATA/20news_model_mult.vw -t -d $PATH_TO_ALL_DATA/20news_test_mult.vw \
-p $PATH_TO_ALL_DATA/20news_test_predictions_mult.txt

让我们看看结果如何:

with open(os.path.join(PATH_TO_ALL_DATA, 
                       '20news_test_predictions_mult.txt')) as pred_file:
    test_prediction_mult = [float(label) 
                            for label in pred_file.readlines()]

accuracy_score(test_labels_mult, test_prediction_mult)

输出:

0.8734535171438671

在测试集上的精确度超过87%,还不错。

3.3 IMDB影评

这一节中,我们将对IMDB影评进行二元分类。影评数据可从Google网盘下载:

https://drive.google.com/file/d/1xq4l5c0JrcxJdyBwJWvy0u9Ad_pvkJ1l/view

我们使用sklearn.datasetsload_files函数加载影评。数据集已经分为训练集、测试集两部分,各包含12500好评、12500差评。首先,我们将分割文本和标签:

import pickle

path_to_movies = os.path.expanduser('imdb_reviews')

reviews_train = load_files(os.path.join(path_to_movies, 'train'))
text_train, y_train = reviews_train.data, reviews_train.target

reviews_test = load_files(os.path.join(path_to_movies, 'test'))
text_test, y_test = reviews_test.data, reviews_train.target

查看一些影评的例子和相应的标签:

text_train[0]

输出:

b"Zero Day leads you to think, even re-think why two boys/young men would do what they did - commit mutual suicide via slaughtering their classmates. It captures what must be beyond a bizarre mode of being for two humans who have decided to withdraw from common civility in order to define their own/mutual world via coupled destruction.<br /><br />It is not a perfect movie but given what money/time the filmmaker and actors had - it is a remarkable product. In terms of explaining the motives and actions of the two young suicide/murderers it is better than 'Elephant' - in terms of being a film that gets under our 'rationalistic' skin it is a far, far better film than almost anything you are likely to see. <br /><br />Flawed but honest with a terrible honesty."

这是好评还是差评?

y_train[0]

输出:

1

看来是好评。

再看一条:

text_train[1]

输出:

b'Words can\'t describe how bad this movie is. I can\'t explain it by writing only. You have too see it for yourself to get at grip of how horrible a movie really can be. Not that I recommend you to do that. There are so many clich\xc3\xa9s, mistakes (and all other negative things you can imagine) here that will just make you cry. To start with the technical first, there are a LOT of mistakes regarding the airplane. I won\'t list them here, but just mention the coloring of the plane. They didn\'t even manage to show an airliner in the colors of a fictional airline, but instead used a 747 painted in the original Boeing livery. Very bad. The plot is stupid and has been done many times before, only much, much better. There are so many ridiculous moments here that i lost count of it really early. Also, I was on the bad guys\' side all the time in the movie, because the good guys were so stupid. "Executive Decision" should without a doubt be you\'re choice over this one, even the "Turbulence"-movies are better. In fact, every other movie in the world is better than this one.'

这条是好评还是差评?

y_train[1]

输出:

0

嗯,这条是差评。

如前所述,数据集已经分成训练集和测试集两部分。现在我们再从训练集中切分30%出来作为验证集。

train_share = int(0.7 * len(text_train))
train, valid = text_train[:train_share], text_train[train_share:]
train_labels, valid_labels = y_train[:train_share], y_train[train_share:]

同样,我们将它们保存到文件:

with open(os.path.join(PATH_TO_ALL_DATA, 'movie_reviews_train.vw'), 'w') as vw_train_data:
    for text, target in zip(train, train_labels):
        vw_train_data.write(to_vw_format(str(text), 1 if target == 1 else -1))
with open(os.path.join(PATH_TO_ALL_DATA, 'movie_reviews_valid.vw'), 'w') as vw_train_data:
    for text, target in zip(valid, valid_labels):
        vw_train_data.write(to_vw_format(str(text), 1 if target == 1 else -1))
with open(os.path.join(PATH_TO_ALL_DATA, 'movie_reviews_test.vw'), 'w') as vw_test_data:
    for text in text_test:
vw_test_data.write(to_vw_format(str(text)))

然后运行Vowpal Wabbit(我们仍然使用铰链损失,不过你可以试验其他算法):

vw -d $PATH_TO_ALL_DATA/movie_reviews_train.vw --loss_function hinge -f $PATH_TO_ALL_DATA/movie_reviews_model.vw --quiet

训练完成后,让我们在留置的验证集上测试一下表现:

vw -i $PATH_TO_ALL_DATA/movie_reviews_model.vw -t \ 
-d $PATH_TO_ALL_DATA/movie_reviews_valid.vw -p $PATH_TO_ALL_DATA/movie_valid_pred.txt --quiet

从文件读取预测,并估计精确度和AUC。

with open(os.path.join(PATH_TO_ALL_DATA, 'movie_valid_pred.txt')) as pred_file:
    valid_prediction = [float(label) 
                             for label in pred_file.readlines()]
print("Accuracy: {}".format(round(accuracy_score(valid_labels, 
               [int(pred_prob > 0) for pred_prob in valid_prediction]), 3)))
print("AUC: {}".format(round(roc_auc_score(valid_labels, valid_prediction), 3)))

输出:

Accuracy: 0.885
AUC: 0.942

在测试集上如法炮制:

vw -i $PATH_TO_ALL_DATA/movie_reviews_model.vw -t \ -d $PATH_TO_ALL_DATA/movie_reviews_test.vw \ -p $PATH_TO_ALL_DATA/movie_test_pred.txt --quiet
with open(os.path.join(PATH_TO_ALL_DATA, 'movie_test_pred.txt')) as pred_file:
    test_prediction = [float(label) 
                             for label in pred_file.readlines()]
print("Accuracy: {}".format(round(accuracy_score(y_test, 
               [int(pred_prob > 0) for pred_prob in test_prediction]), 3)))
print("AUC: {}".format(round(roc_auc_score(y_test, test_prediction), 3)))

和我们期望的一样,精确度和AUC几乎和验证集上一样:

Accuracy: 0.88
AUC: 0.94

让我们尝试下n元语法,看看能不能提高精确度:

vw -d $PATH_TO_ALL_DATA/movie_reviews_train.vw \ --loss_function hinge --ngram 2 -f $PATH_TO_ALL_DATA/movie_reviews_model2.vw --quiet

vw -i$PATH_TO_ALL_DATA/movie_reviews_model2.vw -t -d $PATH_TO_ALL_DATA/movie_reviews_valid.vw \ -p $PATH_TO_ALL_DATA/movie_valid_pred2.txt --quiet

vw -i $PATH_TO_ALL_DATA/movie_reviews_model2.vw -t -d $PATH_TO_ALL_DATA/movie_reviews_test.vw \ -p $PATH_TO_ALL_DATA/movie_test_pred2.txt --quiet

效果不错:

# 验证集
Accuracy: 0.894
AUC: 0.954

# 测试集
Accuracy: 0.888
AUC: 0.952

3.4 分类StackOverflow问题

现在,让我们看看Vowpal Wabbit在大型数据集上的表现。我们将使用一个10GB的StackOverflow问答数据集:

https://drive.google.com/file/d/1ZU4J3KhJDrHVMj48fROFcTsTZKorPGlG/view?usp=sharing

原始数据集由一千万问题组成,每个问题有多个标签。数据相当整洁,所以别叫它“大数据”,即使是在酒馆中。:)

再说一遍大数据

我们仅仅选取了10个标签:javascriptjavapythonrubyphpc++c#goscalaswift。让我们解决这一十元分类问题:我们想根据问题的文本预测这个问题的标签是10个流行的编程语言中的哪一个。

选取10个标签后,我们得到了一个4.7G的数据集,并将其切分为训练集和测试集。

我们将用Vowpal Wabbit处理训练集(3.1 GiB):

vw --oaa 10 -d $PATH_TO_STACKOVERFLOW_DATA/stackoverflow_train.vw \ -f vw_model1_10mln.vw -b 28 --random_seed 17 --quiet

其中,--oaa 10表示我们有10个分类,-b 28表示我们将使用28位哈希,也就是228特征空间,--random_seed 17固定随机数种子,以便复现。

训练完成之后,看看模型在测试集上的表现:

vw -t -i vw_model1_10mln.vw -d $PATH_TO_STACKOVERFLOW_DATA/stackoverflow_test.vw \ -p vw_test_pred.csv --random_seed 17 --quiet
vw_pred = np.loadtxt(os.path.join(PATH_TO_STACKOVERFLOW_DATA, 
                                  'vw_test_pred.csv'))
test_labels = np.loadtxt(os.path.join(PATH_TO_STACKOVERFLOW_DATA, 
                                      'stackoverflow_test_labels.txt'))
accuracy_score(test_labels, vw_pred)

结果:

0.91728604842865913

模型的训练和预测在不到1分钟内就完成了(我使用的是2015年中期的MacBook Pro,2.2 GHz Intel Core i7,16GB RAM)。精确度差不多达到了92%。我们没有使用什么Hadoop集群就做到了这一点。:) 令人印象深刻,不是吗?

4. 相关资源

原文 Open Machine Learning Course. Topic 8. Vowpal Wabbit: Fast Learning with Gigabytes of Data
感谢原作者[Yury Kashnitskiy]授权论智编译,未经授权禁止转载。详情见转载须知

本文来自机器人之家,如若转载,请注明出处:https://www.jqr.com/article/000348
爆料投稿,欢迎投递至邮箱:service@jqr.com
机器学习 Vowpal Wabbit 独热编码 标签编码
推荐阅读

最新评论(0

暂无回帖,快来抢沙发吧

评论

游客
robot
发布需求
联系客服联系客服
robot
联系客服