广告已经成为Web收入主要模式。这是一项数十亿美元的业务,也谷歌大部分收入来源。此外,在互联网领域存在这样的可能性,像谷歌这样的公司以免费的形式提供它的主要服务,而通过广告赚取公司的利润。
让我们来探讨如何使用MapReduce实现一个简单的Adwords平衡算法。
Adwords允许客户竞投关键字。例如,广告主“A”可以用2美元价格竞购关键字“Hadoop支持”,提供100美元的最大预算,广告主的“B”会竞购关键词“Hadoop支持”为1.50美元,提供最高预算200美元。当用户为一个文件中搜索与给定的关键字时,系统将选择的出价这些关键字中的一个或多个广告。只有当用户点击广告时,广告客户才支付费用。
Adwords问题是如何显示广告,使得其将收益最大化。在设计此类解决方案时,有几个需要考虑的场景因素:
- 只有用户点击,而不是显示广告,才能够给我们带来收入。因此,我们要显示可能被经常点击的广告。这里使用一个广告点击次数与广告显示次数的比值,我们称此为关键字的“点击通过率”。
- 我们想展示那些拥有大笔预算的人们,因为这些都可能是那些有硬花钱的用户,而不是更小预算的用户。
在本章攻略中,我们将实现可以在这种情况下使用的AdWords帐户的余额算法的简化版本。为简单起见,我们假设广告主出价仅在单个关键字。此外,由于我们无法找到一个真正的出价数据集,我们将生成一个模拟出价数据集。
假设你去支持基于关键字的广告系统,使用亚马逊的数据集。攻略摘要如下:
- 第一道MapReduce的任务将使用亚马逊销售指数估算出关键字的点击通过率
。这里,我们假定存在于标题的关键字,
如果该产品具有较高的销售排名将有更好的点击率。 - 然后,我们将运行一个Java任务生成一个出价的数据集。
- 然后第二道MapReduce的任务将出价相同的产品分组在一起,创建一个输出是否适合于所使用的广告分配方案。
- 最后,我们将使用一个广告分配方案,以关键字分配给广告客户。我们将使用的Adword平衡算法,它使用下面的公式,根据各广告主拥有的未动用的预算,投标价值,并点击率的分数下列公式分配优先级。
Measure = bid value * click-through rate * (1-e^(-1*current budget/ initial budget))
准备工作
下面的步骤描述了如何准备运行Adwords示例:
- 这里假设你已经按照第1章《获取Hadoop并在集群中运行》,并且已经安装Hadoop的。我们将用HADOOP_HOME指代Hadoop的安装目录。
- 按照第1章中的说明,《获取Hadoop并在集群中运行》,启动Hadoop。
- 本章攻略假设读者知道如何使用Hadoop的处理作业。如果你没有这样做的话,应该遵循第1章《获取Hadoop并在集群中运行》中的《写WordCount MapReduce示例程序,打包并使用独立的Hadoop运行它》攻略。
操作步骤
以下步骤描述了如何运行Adwords示例:
- 从http://snap.stanford.edu/data/amazon-meta.html找到亚马逊的产品合作采购网元数据,下载的数据集并将其解压缩。我们称这个文件夹为
DATA_DIR
。 - 从
HADOOP_HOME
运行以下命令将数据上传到HDFS。如果在/data
目录已经存在,它清理干净。此数据集很大,如果你试图用一台计算机上运行它可能需要很长的时间。读者可能希望只上传了第一个50,000行的数据集左右,如果你需要的示例程序快速运行。
$ bin/hadoopdfs -mkdir /data
$ bin/hadoopdfs -mkdir /data/input1
$ bin/hadoopdfs -put /amazon-meta.txt /data/input1
- 解压缩适用于第8章的源代码(chapter8.zip)。我们将称该文件夹
CHAPTER_8_SRC
。 - 更改
CHAPTER_8_SRC/build.xml
文件的hadoop.home属性,使其指向Hadoop的安装目录。 - 从
CHAPTER_8_SRC
目录下运行ant build
命令编译源代码。 - 将
build/lib/hadoop-cookbook-chapter8.jar
复制到HADOOP_HOME
。 - 通过从
HADOOP_HOME
运行以下命令,提交MapReduce作业。
$ bin/hadoopjar hadoop-cookbook-chapter8.jar chapter8.adwords.ClickRateApproximator/data/input1 /data/output6
- 通过运行以下命令,将结果下载到计算机上:
$ bin/hadoopdfs -get /data/output6/part-r-00000clickrate.data
- 可以看到该文件包含结果如下。可以使用这些值与贝叶斯分类器的输入进行分类。
keyword:(Annals 74
keyword:(Audio 153
keyword:(BET 95
keyword:(Beat 98
keyword:(Beginners) 429
keyword:(Beginning 110
- 从
HADOOP_HOME
运行以下命令生成一个出价的数据集。你可以从biddata.data
文件找到最终结果。
$ java -cp build/lib/hadoop-cookbook-chapter8.jar chapter8.adwords.AdwordsBidGenerator clickrate.data
- 创建一个名为
/data/input2
目录并上传竞价数据集,并从早期的MapReduce任务结果上传到HDFS的/data/input2目录。
$ bin/hadoopdfs -put clickrate.data /data/input2
$ bin/hadoopdfs -put biddata.data /data/input2
- 通过运行第二道MapReduce作业,生成Adwords数据集要使用的数据。
$ bin/hadoopjar hadoop-cookbook-chapter8.jar chapter8.adwords.AdwordsBalanceAlgorithmDataGenerator/data/input2 /data/output7
- 通过运行以下命令将结果下载到计算机:
$ bin/hadoopdfs -get /data/output7/part-r-00000adwords.data
- 可以看到,将会如下打印结果:
(Animated client23,773.0,5.0,97.0|
(Animated) client33,310.0,8.0,90.0|
(Annals client76,443.0,13.0,74.0|
client51,1951.0,4.0,74.0|
(Beginners) client86,210.0,6.0,429.0|
client6,236.0,5.0,429.0|
(Beginning client31,23.0,10.0,110.0|
- 通过运行以下命令为一组随机的关键字进行匹配:
$ javajar hadoop-cookbook-chapter8.jar chapter8.adwords.AdwordsAssigneradwords.data
工作原理
正如我们在工作原理一节中讨论,攻略包含两道MapReduce作业。读者可以从src/chapter8/adwords/ClickRateApproximator.java
找到第一道MapReduce作业的源代码。
该mapper函数如下所示。它使用使用亚马逊的数据格式解析亚马逊提供的数据集,并为每个产品标题的每个关键字,输出关键字以及该产品的销售排名。
public void map(Object key, Text value, Context context)
{
ItemDataitemData = null;
ListcustomerList =
AmazonCustomer.parseAItemLine(value.toString());
if(customerList.size() == 0)
{
return;
}
for (AmazonCustomer customer : customerList)
{
itemData = customer.itemsBrought.iterator().next();
break;
}
String[] tokens = itemData.title.split("\\s");
for(String token: tokens)
{
if(token.length() > 3)
{
context.write(new Text(token),
new IntWritable(itemData.salesrank));
}
}
}
然后,由Hadoop的键进行排序所发出的键-值对,并为每个键通过针对该键发出的值列表调用reducer一次。如图所示,在下面的代码中,reducer的计算近似使用对按键发出的销售队列的点击通过率。
public void reduce(Text key, Iterable values,
Context context) throws IOException, InterruptedException
{
doubleclickrate = 0;
for(IntWritableval: values)
{
if(val.get() > 1)
{
clickrate = clickrate + 1000/Math.log(val.get());
}
else
{
clickrate = clickrate + 1000;
}
}
context.write(new Text("keyword:" +key.toString()),
newIntWritable((int)clickrate));
}
暂时没有公开可用的竞价数据集。因此,我们将生成一个随机的出价数据使用本章攻略配置的AdwordsBidGenerator
。它将读出由先前的配方所生成的关键字,并生成一个随机的出价数据集。
然后,我们将使用第二道MapReduce作业合并出价数据集的点击率,并生成具有投标“信息分类对关键字的数据集。可以从src/chapter8/adwords/AdwordsBalanceAlgorithmDataGenerator.java
找到第二道MapReduce作业的源代码。Mapper功能如下所示:
public void map(Object key, Text value, Context context)
throws IOException, InterruptedException
{
String[] keyVal = value.toString().split("\\s");
if (keyVal[0].startsWith("keyword:"))
{
context.write(
new Text(keyVal[0].replace("keyword:", "")),
new Text(keyVal[1]));
}
else if (keyVal[0].startsWith("client"))
{
List bids = new ArrayList();
double budget = 0;
String clientid = keyVal[0];
String[] tokens = keyVal[1].split(",");
for (String token : tokens)
{
String[] kp = token.split("=");
if (kp[0].equals("budget"))
{
budget = Double.parseDouble(kp[1]);
}
else if (kp[0].equals("bid"))
{
String[] bidData = kp[1].split("\\|");
bids.add(bidData);
}
}
for (String[] bid : bids)
{
String keyword = bid[0];
String bidValue = bid[1];
context.write(new Text(keyword),
new Text(new StringBuffer()
.append(clientid).append(",")
.append(budget).append(",")
.append(bidValue).toString()));
}
}
}
该map函数读取两个投标的数据集和点击通过率数据集,输出这两种类型作为关键字的数据。然后,每个reducer为每个关键字收到相应的所有投标和相关的点击数据。然后reducer合并的数据,并发出针对每个关键字出价的列表。
public void reduce(Text key, Iterable values,
Context context) throws IOException, InterruptedException
{
String clientid = null;
String budget = null;
String bid = null;
String clickRate = null;
Listbids = new ArrayList();
for (Text val : values)
{
if (val.toString().indexOf(",") > 0)
{
bids.add(val.toString());
}
else
{
clickRate = val.toString();
}
}
StringBufferbuf = new StringBuffer();
for (String bidData : bids)
{
String[] vals = bidData.split(",");
clientid = vals[0];
budget = vals[1];
bid = vals[2];
buf.append(clientid).append(",")
.append(budget).append(",")
.append(Double.valueOf(bid)).append(",")
.append(Math.max(1, Double.valueOf(clickRate)));
buf.append("|");
}
if (bids.size() > 0)
{
context.write(key, new Text(buf.toString()));
}
}
最后,Adwords分配器加载投标数据和存储他们对应内存中的关键字。给定一个关键字时,AdWords分配器发现具有下列公式最大值和选择所有的投标广告中的投标:
Measure = bid value * click-through rate * (1-e^(-1*current budget/ initial budget))
更多参考
上述配方假设Adwords分配器可以加载所有的数据进入内存中,并进行广告分配的决策。但是,如果数据集很大,我们可以通过关键字的数据集划分在多台计算机(例如,分配字母“A-D”开头的关键字到第一台计算机等)。
关于在线广告的更多信息,可以发现从Anand Rajaraman和Jeffrey D. Ullman编写的《Mining of Massive Datasets》一书找到。这本书可以从http://infolab.stanford.edu/~ullman/mmds.html找到。