推荐系统详解(十)常见模块
巧妇难为无米之炊:数据采集关键要素
推荐系统离不开数据,数据就是推荐系统的粮食,要有数据就得收集数据。在自己产品中收集数据,主要还是来自日志。
日志和数据
数据驱动这个概念也是最近几年才开始流行起来的,在古典互联网时代,设计和开发产品完全侧重于功能易用和设计精巧上,并且整体驱动力受限于产品负责人的个人眼光,这属于是一种感性的把握,也因此对积累数据这件事就不是很重视。在我经手的产品中,就有产品上线很久,需要搭建推荐系统时,却发现并没有收集相应的数据,或者收集得非常杂乱无章。关于数据采集,按照用途分类又有三种:报表统计;数据分析;机器学习。当然,这三种的用途并不冲突,而且反而有层层递进的关系。最基本的数据收集,是为了统计一些核心的产品指标,例如次日留存,七日留存等,一方面是为了监控产品的健康状况,另一方面是为了对外秀肌肉,这一类数据使用非常浅层,对数据的采集要求也不高。第二种就是比较常见的数据采集需求所在了。在前面第一种用途基础上,不但需要知道产品是否健康,还需要知道为什么健康、为什么不健康,做对了什么事、做错了什么事,要从数据中去找到根本的原因。
这种数据采集的用途,驱动了很多多维分析软件应运而生,也驱动了多家大数据创业公司应运而生。数据分析工作,最后要产出的是比较简明清晰直观的结论,这是数据分析师综合自己的智慧加工出来的,是有人产出的。它主要用于指导产品设计、指导商业推广、指导开发方式。走到这一步的数据采集,已经是实打实的数据驱动产品了。第三种,就是收集数据为了机器学习应用,或者更广泛地说人工智能应用。那么机器学习应用,主要在消化数据的角色是算法、是计算机,而不是人。这个观点是我在专栏写作之初,讲解用户画像相关内容时就提到的,再强调一遍就是,所有的数据,不论原始数据还是加工后的数据都是给机器“看”的,而不是给人“看”的。所以在数据采集上,可以说多多益善,样本是多多益善,数据采集的维度,也就是字段数多多益善,但另一方面,数据是否适合分析,数据是否易于可视化地操作并不是核心的内容。当然,实际上在任何一款需要有推荐系统的产品中,数据采集的需求很可能要同时满足上述三种要求。本文为了讨论方便,不会重点讨论多维数据分析的用途,而是专门看看为了满足推荐系统,你需要怎么收集日志、采集数据。因为推荐系统就是一个典型的人工智能应用,数据是要喂给机器“吃”的。下面我就开始给你详细剖析一下为推荐系统收集日志这件事。
数据采集
给推荐系统收集日志这件事,依次要讨论的是:日志的数据模型,收集哪些日志,用什么工具收集,收集的日志怎么存储。
1. 数据模型
数据模型是什么?所谓数据模型,其实就是把数据归类。产品越复杂,业务线越多,产生的日志就越复杂。如果看山是山,一个数据来源一个数据来源地去对待的话,那将效率非常低下,因此需要首先把要收集的日志数据归入几个模型。不同的数据应用,数据模型略有不同。就推荐系统而言,推荐系统要做的事情就是预测那些最终会建立的人和物之间的连接,依赖的是已有的连接,以及人和物的属性,而且,其中最主要的是已有的连接,人和物的属性只不过是更加详细描述这些连接而已。数据模型帮助梳理日志、归类存储,以方便在使用时获取。你可以回顾一下在前面讲过的推荐算法,这些推荐算法形形色色,但是他们所需要的数据可以概括为两个字:矩阵。再细分一下,这些矩阵就分成了四种。
基于这个分析,可以给要收集的数据归纳成下面几种。
有了数据模型,就可以很好地去梳理现有的日志,看看每一种日志属于哪一种。并且,在一个新产品上线之初,该如何为将来的推荐系统记录日志也比较清楚了。这个数据模型当然不能概括全部数据,但是用来构建一个推荐系统就绰绰有余了。接下来就是去收集数据了。收集数据,就是把散布在各个地方的数据聚拢,也包括那些还根本没有记录的数据的地方要开始记录。
2. 数据在哪?
按照前面的数据建模,我们一起来看一下要收集的数据会怎么产生。主要来自两种,一种是业务运转必须要存储的记录,例如用户注册资料,如果不在数据库中记录,产品就无法正常运转。另一种就是在用户使用产品时顺便记录下来的,这叫做埋点。第一种数据源来自业务数据库,通常都是结构化存储,MySQL。第二种数据需要埋点,埋点又有几种不同方法。第一种,SDK 埋点。这个是最经典古老的埋点方法,就是在开发自己的 App 或者网站时,嵌入第三方统计的 SDK,App 如友盟等,网站如 Google Analytics 等。SDK 在要收集的数据发生点被调用,将数据发送到第三方统计,第三方统计得到数据后再进一步分析展示。这种数据收集方式对推荐系统的意义不大,因为得不到原始的数据而只是得到统计结果,我们可以将其做一些改动,或者自己仿造做一些开发内部数据采集 SDK,从而能够收集到鲜活的数据。第二种,可视化埋点。可视化埋点在 SDK 埋点基础上做了进一步工作,埋点工作通过可视化配置的方式完成,一般是在 App 端或者网站端嵌入可视化埋点套件的 SDK,然后再管理端接收前端传回的应用控件树,通过点选和配置,指令前端收集那些事件数据。业界有开源方案实现可参考,如 Mixpanel。第三种,无埋点。所谓无埋点不是不埋点收集数据,而是尽可能多自动收集所有数据,但是使用方按照自己的需求去使用部分数据。
SDK 埋点就是复杂度高,一旦埋点有错,需要更新客户端版本,可视化埋点的不足就是:收集数据不能收集到非界面数据,例如收集了点击事件,也仅仅能收集一个点击事件,却不能把更详细的数据一并返回。上面是按照技术手段分,如果按照收集数据的位置分,又分为前端埋点和后端埋点。这两个区别是这样的,举个例子,要收集用户的点击事件,前端埋点就是在用户点击时,除了响应他的点击请求,还同时发送一条数据给数据采集方。后端埋点就不一样了,由于用户的点击需要和后端交互,后端收到这个点击请求时就会在服务端打印一条业务日志,所以数据采集就采集这条业务日志即可。埋点是一项非常复杂繁琐的事情,需要前端工程师或者客户端工程师细心处理,不在本文讨论范围内。但是幸好,国内如神策数据等公司,将这些工作已经做得很傻瓜化了,大大减轻了埋点数据采集的困扰。对于推荐系统来说,所需要的数据基本上都可以从后端收集,采集成本较低,但是有两个要求:要求所有的事件都需要和后端交互,要求所有业务响应都要有日志记录。这样才能做到在后端收集日志。后端收集业务日志好处很多,比如下面几种。
实时性。由于业务响应是实时的,所以日志打印也是实时的,因此可以做到实时收集。可及时更新。由于日志记录都发生在后端,所以需要更新时可以及时更新,而不用重新发布客户端版本。开发简单。不需要单独维护一套 SDK。
归纳一下,Event 类别的数据从后端各个业务服务器产生的日志来,Item 和 User 类型数据,从业务数据库来,还有一类特殊的数据就是 Relation 类别,也从业务数据库来。
3. 元素有哪些?
后端收集事件数据需要业务服务器打印日志。需要打印哪些信息才算是一条完整的时间数据呢?大致需要包含下面的几类元素。用户 ID,唯一标识用户身份。物品 ID,唯一标识物品。这个粒度在某些场景中需要注意,例如电商,物品的 ID 就不是真正要去区别物和物之间的不同,而是指同一类试题,例如一本书《岛上书店》,库存有很多本,并不需要区别库存很多本之间的不同,而是区别《岛上书店》和《白夜行》之间的不同。事件名称,每一个行为一个名字。事件发生时间,时间非常重要。以上是基本的内容,下面再来说说加分项。事件发生时的设备信息和地理位置信息等等;从什么事件而来;从什么页面而来;事件发生时用户的相关属性;事件发生时物品的相关属性。把日志记录想象成一个 Live 快照,内容越丰富就越能还原当时的场景。
4. 怎么收集?
一个典型的数据采集架构如下图所示。
下面描述一下这个图。最左边就是数据源,有两部分,一个是来自非常稳定的网络服务器日志,nginx 或者 Apache 产生的日志。这类日志对推荐系统的作用是什么呢?因为有一类埋点,在 PC 互联网时代,有一种事件数据收集方式是,放一个一像素的图片在某个要采集数据的位置。这个图片被点击时,向服务端发送一个不做什么事情的请求,只是为了在服务端的网络服务器那里产生一条系统日志。 这类日志用 logstash 收集。左边另外的数据源就是业务服务器,这类服务器会处理具体场景的具体业务,甚至推荐系统本身也是一个业务服务器。这类服务器有各自不同的日志记录方式,例如 Java 是 Log4j,Python 是 Logging 等等,还有 RPC 服务。这些业务服务器通常会分布在多台机器上,产生的日志需要用 Flume 汇总。Kafka 是一个分布式消息队列,按照 Topic 组织队列,订阅消费模式,可以横向水平扩展,非常适合作为日志清洗计算层和日志收集之间的缓冲区。所以一般日志收集后,不论是 Logstash 还是 Flume,都会发送到 Kafka 中指定的 Topic 中。在 Kafka 后端一般是一个流计算框架,上面有不同的计算任务去消费 Kafka 的数据 Topic,流计算框架实时地处理完采集到的数据,会送往分布式的文件系统中永久存储,一般是 HDFS。日志的时间属性非常重要。因为在 HDFS 中存储日志时,为了后续抽取方便快速,一般要把日志按照日期分区。当然,在存储时,按照前面介绍的数据模型分不同的库表存储也能够方便在后续构建推荐模型时准备数据。
5. 质量检验
数据采集,日志收集还需要对采集到的数据质量做监控。数据质量通常需要数据中心的同学重点关注。推荐系统作为数据的使用方,虽然不用重点关注如何保证数据质量,但是需要能够发现数据质量问题,不然在错误的数据上无法训练出聪明的推荐模型的。关注数据质量,大致需要关注下面几个内容。是否完整?事件数据至少要有用户 ID、物品 ID、事件名称三元素才算完整,才有意义。是否一致?一致是一个广泛的概念。数据就是事实,同一个事实的不同方面会表现成不同数据,这些数据需要互相佐证,逻辑自洽。是否正确?该记录的数据一定是取自对应的数据源,这个标准不能满足则应该属于 Bug 级别,记录了错误的数据。是否及时?虽然一些客户端埋点数据,为了降低网络消耗,会积攒一定时间打包上传数据,但是数据的及时性直接关系到数据质量。由于推荐系统所需的数据通常会都来自后端埋点,所以及时性还可以保证。
总结
今天和你聊了数据采集的若干要点。数据是推荐系统做饭的米,没有数据就没有任何推荐策略的落地,因此采集数据是一个非常重要的工作。采集数据需要首先梳理好自己的数据有哪些,本文不是帮你梳理你的自己的产品中有哪些数据,而是告诉你看推荐系统需要哪些数据。另外有一点,数据采集的需求方有很多,推荐系统只是其中一个,通常数据分析对数据的需求,集中在多维数据分析,当然推荐系统也需要多维数据,只是推荐系统更关注事件。我把这些数据全都看成矩阵,有了矩阵,无论是内容推荐还是协同过滤,矩阵分解,还是机器学习深度学习,就都有了输入。我总结了推荐系统需要四种矩阵,对应四种数据,列表如下:
为了构建推荐系统,上面四类数据足够了,其中除了 Relation 数据之外,另外三种是必须的。所以,你照着这个药方子去抓药就好了。另外,我还介绍了日志收集系统的架构图,以及一些埋点技术的简要介绍,可以帮助理解埋点收集数据这件事。最后,数据质量要过硬,好质量的数据胜似黄金,低质量的数据价值也就不高,收集到错误的数据除了带来存储和传输成本,还无法创造价值,所以检测数据质量也很重要。今天就讲到这里,最后留一个思考问题,一个信息流产品,需要采集的数据具体有哪些?
让你的推荐系统反应更快:实时推荐
更快,更高,更强,不只是奥林匹克运动所追求的,也是推荐系统从业者所追求的三个要素:捕捉兴趣要更快,指标要更高,系统要更健壮。我今天就要说的就是这个“更快”。推荐系统是为了在用户和物品之间建立连接,手段是利用已有的用户物品之间的连接,然而任何事物都是有生命周期的,包括这里说的这个虚无的“连接”也是有的。
为什么要实时
一个连接从建立开始,其连接的强度就开始衰减,直到最后,可能用户不记得自己和那个物品曾经交汇过眼神。因此,推荐系统既然使用已有的连接去预测未来的连接,那么追求“更快”就成了理所当然的事情。用户和物品之间产生的连接,不论轻如点击,还是重如购买,都有推荐的黄金时间。在这个黄金时间,捕捉到用户的兴趣并且给与响应,可能就更容易留住用户。在业界,大家为了高大上,不会说“更快”的推荐系统,而是会说“实时”推荐系统。实际上,绝对的实时是不存在的,哪怕延迟级别在微秒的推荐,也是会有延迟的。但是为了顺应时代潮流,我还是会在后面的内容中说这是实时推荐,你就那么一听,知道就好。关于到底什么是实时推荐,实际上有三个层次。第一层,“给得及时”,也就是服务的实时响应。这个是最基本的要求,一旦一个推荐系统上线后,在互联网的场景下,没有让用户等个一天一夜的情况,基本上最慢的服务接口整个下来响应时间也超过秒级。达到第一层不能成为实时推荐,但是没达到就是不合格。第二层,“用得及时”,就是特征的实时更新。例如用户刚刚购买了一个新的商品,这个行为事件,立即更新到用户历史行为中,参与到下一次协同过滤推荐结果的召回中。做到这个层次,已经有实时推荐的意思了,常见的效果就是在经过几轮交互之后,用户的首页推荐会有所变化。这一层次的操作影响范围只是当前用户。第三层,“改得及时”,就是模型的实时更新。还是刚才这个例子,用户刚刚购买了一个新的商品,那需要实时地去更新这个商品和所有该用户购买的其他商品之间的相似度,因为这些商品对应的共同购买用户数增加了,商品相似度就是一种推荐模型,所以它的改变影响的是全局推荐。
实时推荐
好,下面就讲一下如何构建一个处在第三层次的实时推荐系统。
1. 架构概览
按照前面的分析,一个处在第三层次的实时推荐,需要满足三个条件:数据实时进来数据实时计算结果实时更新为此,下面给出一个基本的实时推荐框图。
整体介绍一下这个图,前端服务负责和用户之间直接交互,不论是采集用户行为数据,还是给出推荐服务返回结果。用户行为数据经过实时的消息队列发布,然后由一个流计算平台消费这些实时数据,一方面清洗后直接入库,另一方面就是参与到实时推荐中,并将实时计算的结果更新到推荐数据库,供推荐服务实时使用。
2. 实时数据
实时流数据的接入,在上一篇专栏中已经讲到过,需要一个实时的消息队列,开源解决方案 Kafka 已经是非常成熟的选项。
Kafka 以生产者消费者的模式吞吐数据,这些数据以主题的方式组织在一起,每一个主题的数据会被分为多块,消费者各自去消费,互不影响,Kafka 也不会因为某个消费者消费了而删除数据。每一个消费者各自保存状态信息:所消费数据在 Kafka 某个主题某个分块下的偏移位置。也因此任意时刻、任意消费者,只要自己愿意,可以从 Kafka 任意位置开始消费数据,一遍消费,对应的偏移量顺序往前移动。示意图如下。
一个生产者可以看做一个数据源,生产者决定数据源放进哪个主题中,甚至通过一些算法决定数据如何落进哪个分块里。示意图如下:
因此,Kafka 的生产者和消费者在自己的项目中实现时都非常简单,就是往某个主题写数据,以及从某个主题读数据。
3. 流计算
整个实时推荐建立在流计算平台上。常见的流计算平台有 Twitter 开源的 Storm,“Yahoo!”开源的 S4,还有 Spark 中的 Streaming。不过随着 Storm 使用者越来越多,社区越来越繁荣,并且相比 Streaming 的 MiniBatch 模式,Storm 才是真正的流计算。因此,在你构建自己的实时推荐时,流计算平台不妨就选用 Storm,不过最新的流计算框架 FLink 表现强劲,高吞吐低延迟,如果你所在团队有人愿意尝试一下也很不错。Storm 是一个流计算框架,它有以下几个元素。Spout,意思是喷嘴,水龙头,接入一个数据流,然后以喷嘴的形式把数据喷洒出去。Bolt,意思是螺栓,像是两段水管的连接处,两端可以接入喷嘴,也可以接入另一个螺栓,数据流就进入了下一个处理环节。Tuple,意思是元组,就是流在水管中的水。Topology,意思是拓扑结构,螺栓和喷嘴,以及之间的数据水管,一起组成了一个有向无环图,这就是一个拓扑结构。注意,Storm 规定了这些基本的元素,也是你在 Storm 平台上编程时需要实现的,但不用关心水管在哪,水管由 Storm 提供,你只用实现自己需要的水龙头和水管连接的螺栓即可。因此,其编程模型也非常简单。举一个简单的例子,看看如何用 Storm 实现流计算?假如有一个字符串构成的数据流,这个数据流恰好也是 Kafka 中的一个主题,正在源源不断地在接入。要用 Storm 实现一个流计算统计每一个字符的频率。你首先需要实现一个 Spout,也就是给数据流加装一个水龙头,这个水龙头那一端就是一个 Kafka 的消费者,从 Kafka 中不断取出字符串数据,这头就喷出来,然后再实现 Bolt,也就是螺栓。
当有字符串数据流进来时,把他们拆成不同的字符,并以(字符,1)这样的方式变成新的数据流发射出去,最后就是去把相同字符的数据流聚合起来,相加就得到了字符的频率。实际上,如果你知道 MapReduce 过程的话,你会发现虽然 Storm 重新取了名字,仍然可以按照 MapReduce 来理解。Storm 的模型示意如下:
Storm 中要运行实时推荐系统的所有计算和统计任务,比如有下面几种:清洗数据;合并用户的历史行为;重新更新物品相似度;在线更新机器学习模型;更新推荐结果。
4. 算法实时化
我在前面的文章里面,已经介绍过基于物品的协同过滤原理。下面我以基于物品的协同过滤算法为主线,来讲解一下如何实现实时推荐,其他算法你可以举一反三改造。主要是两个计算,第一个是计算物品之间的相似度。
计算了物品和物品之间的相似度之后,用另一个公式来计算推荐分数:
要做到前面说的第三层次实时推荐,首先就是要做到增量更新物品之间的相似度。相似度计算分成三部分:分子上的“物品对”,共同评分用户数;分母上左边是物品 i 的评价用户数;分母上右边是物品 j 的评价用户数。所以更新计算相似度就要更新三部分,实际上一种相似度增量更新策略是在收到一条用户评分事件数据时,然后取出这个用户的历史评分物品列表,因为所有的历史评分物品现在和这个新评分物品之间,就要增加一个共同评分了。并且,这个新物品本身,也要给自己一个评分用户数。更新完三个后,就实时更新所有这些“物品对”的相似度了。转换成 Storm 的编程模型,你需要实现:
- Spout:消费实时消息队列中的用户评分事件数据,并发射成(UserID , ItemID_i)这样的 Tuple
- Bolt1:接的是源头 Spout,输入了 UserID 和 ItemID_i,读出用户历史评分 Item 列表,遍历这些 ItemID_j,逐一发射成 ((Item_i, Item_j), 1) 和 ((Item_j, Item_i), 1),并将 Item_i 加进历史评分列表中;
- Bolt2:接的是源头 Spout,输入了 UserID 和 ItemID_i,发射成 (ItemID_i, 1);
- Bolt3:接 Bolt1,更新相似度所需的分子
- Bolt4:接 Bolt2,更新物品自己的评分用户数
把这个过程表示成公式就是:
另外,还有实时更新推荐结果,也是作为 Storm 的一个 Bolt 存在,接到用户行为数据,重新更新推荐结果,写回推荐结果数据中。
5. 效率提升
上面展示了一个基于物品的协同过滤算法在实时推荐中的计算过程,那么随之而来的一些问题也需要解决。比如当用户历史行为数据有很多时,或者物品对是热门物品时,相似度实时更新就有些挑战了。对此可以有如下应对办法:剪枝,加窗,采样,缓存。所谓剪枝就是,并不是需要对每一个“物品对”都做增量计算,为什么呢?你想一想,两个物品之间的相似度,每更新一次得到的新相似度,可以看成一个随机变量,那么这个随机变量就有一个期望值,一旦物品之间的相似度可以以较高的置信度确认,它已经在期望值附近小幅度波动了,也就没必要再去更新了。甚至如果进一步确定是一个比较小的相似度,或者可以直接干掉这个物品对,不被更新,也不参与计算。那么问题就来了,怎么确定什么时候可以不再更新这个物品对的相似度了呢?这时候要用到一个不等式:Hoeffding 不等式。Hoeffding 不等式适用于有界的随机变量。相似度明显是有界的,最大值是 1,最小值是 0。所以可以用这个不等式,Hoeffding 不等式是这样一个统计法则:随机变量的真实期望值不会超过 x^+ϵ 的概率是概率 1- δ,其中 ϵ 的值是这样算的:
公式中:x^ 是历次更新得到的相似度平均值,n 是更新过的次数。这样一来,你选定 δ 和 ϵ 之后就知道更新多少次之后就可以放心大胆地使用了。例如,下面这个表格是举的几个例子,这里设置 δ=0.05。
也就是在前面讲到的更新相似度的 Bolt 中,如果发现一个物品对的更新次数已经达到最少更新次数,则可以不再更新,并且,如果此时相似度小于设定阈值,就可以斩钉截铁地说:这两个物品不相似,以后不用再参与推荐计算了。这就是一项基于统计的剪枝方法,除此之外还有加窗、采样、合并三种常规办法。首先,关于加窗。当我说,用户的兴趣会衰减,请你不要怀疑这一点,因为这是这篇文章的基本假设和出发点。用户兴趣衰减,那么一个直接的推论就是,比较久远的用户历史行为数据所起的作用应该小一些。所以,另一个剪枝技术就是:滑窗。设定一个时间窗口,时间窗口内的历史行为数据参与实时计算,窗口外的不再参与实时计算。这个窗口有两种办法:最近 K 次会话。用户如果反复来访问产品,每次访问是一次会话,那么实时计算时只保留最近 K 次会话信息。最近 K 条行为记录。不管访问多少次,只保留最近 K 条历史行为事件,参与到实时推荐中。两种滑窗方法都可以有效保证实时计算的效率,同时不会明显降低推荐效果。关于采样。当你的推荐系统遇到热门的物品或者异常活跃的用户,或者有时候就只是突然一个热点爆发了。它们会在短时间产生大量的数据,除了前面的剪枝方法,还可以对这种短时间大量出现的数据采样,采样手段有很多,可以均匀采样,也可以加权采样,这在前面的专栏里已经详细介绍过方法。
关于合并计算。在前面介绍的增量计算中,是假设收到每一个用户行为事件时都要去更新相似度和推荐结果,如果在突然大量涌入行为数据时,可以不必每一条来了都去更新,而是可以在数据流的上游做一定的合并。相似度计算公式的分子分母两部分都可以这样做,等合并若干事件数据之后,再送入下游去更新相似度和推荐结果。最后,提高实时推荐的效率,甚至不只是推荐系统,在任何互联网应用的后端,缓存都是提高效率必不可少的部分。可以根据实际情况,对于高频访问的物品或者用户增加缓存,这可能包括:活跃用户的历史行为数据;热门物品的特征数据;热门物品的相似物品列表。缓存系统一般采用 Memcached 或者 Redis 集群。缓存有个问题就是,数据的一致性可能比较难保证,毕竟它和真正的业务数据库之间要保持时时刻刻同步也是一项挑战。好了,上面讲到的这些实时推荐有关的优化技巧,其实都是为了满足第三层次的实时推荐要求。
实时更新的推荐结果同步到推荐服务所依赖的线上数据库,这个线上数据库还要定期被线下离线批量的推荐结果所替代。这样一来,实时推荐和离线批量之间就形成了互为补充的作用,这个模式也就是大数据架构最常见的 Lambda 架构。
总结
今天以协同过滤为例讲到了如何构建一个实时推荐,实际上,并不是每一种推荐算法都适合做实时推荐的,或者没必要。幸运的是,很多机器学习算法,都可以使用一些在线学习方法更新模型,这些在线学习算法非常适合作为流计算的任务运行,属于我说的第三层次实时推荐。另外还有一种算法天然就适合在线实时进行,那就是前面讲到的 Bandit 算法,通过和用户之间反复互动更新推荐。实时推荐有三个层次,很多非工程师的朋友们常常脑海里想象的实时推荐实际上只是第二层次,也就是实时更新特征,并没实时更新模型,虽然两者的结果看上去都是推荐结果实时更新了,但是意义不同,难度不同,效果也不同。
让数据驱动落地,你需要一个实验平台
数据驱动这个口号喊了很多年了,这个口号也几乎成为了行业共识,但是数据驱动又像鬼一样,人人都在说,但几乎没人见过它长什么样子。
数据驱动和实验平台
要做到数据驱动,就要做到两点:第一点是数据,第二点是驱动。这听上去似乎像是废话,实际上不是。这第一点的意思是,要采集数据,全方位,数据像是石油一样,没有它就谈不上驱动;第二点的意思是要让大家看数据,光采集了没有用,还需要让所有人盯着数据看。而要做到驱动,需要一个 AB 实验平台。数据驱动的重点是做对比实验,通过对比,让模型、策略、设计等不同创意和智慧结晶新陈代谢,不断迭代更新。对比实验也常常被大家叫做 ABTest,这个意思就是一个 A 实验,一个 B 实验,这样说可能有些模糊,所以我需要先和你说说什么叫做对比实验,然后再说说一个对比实验平台应该长什么样子。你都可以把任何一家个性化推荐产品想象成一个函数,这个函数有很多参数影响它工作,函数的输出就是推荐物品列表。这些函数参数可以有各种组合,通过其中一种参数组合去面对一小股用户的考验,这就是一个实验。要做实验,要做很多实验,要很快做很多实验,要很多人同时很快做很多实验,就需要实验平台。要讨论实验平台,先要认识实验本身。互联网实验,需要三个要素。流量:流量就是用户的访问,也是实验的样本来源。参数:参数就是各种组合,也是用户访问后,从触发互联网产品这个大函数,到最后返回结果给用户,中间所走的路径。结果:实验的全过程都有日志记录,通过这些日志才能分析出实验结果,是否成功,是否显著。
把互联网产品想象一个有向无环图,每个节点是一个参数,不同的分支是参数的不同取值,直到走到终点,这一条路径上所有经过的参数取值,构成了服务的调用路径。具体在推荐系统中,可能这些参数就是不同的模型与策略名称。每当一个用户经过这一系列的调用路径后,就为每一个分支产生了一条实验样本。于是问题来了,每一个用户到来时,如何为他们决定要走哪条路径呢?这就要先经过实验对照来看。实验要观察的结果就是一个随机变量,这个变量有一个期望值,要积累很多样本才能说观察到的实验结果比较接近期望值了,或者要观察一定时期才能说对照实验之间有区别或者没区别。因为只有明显有区别并且区别项好,才能被进一步推上全线。在设计一个实验之初,实验设计人员总是需要考虑下面这些问题。
实验的起止时间。这涉及到样本的数量,关系到统计效果的显著性,也涉及能否取出时间因素的影响。实验的流量大小。这也涉及了样本的数量,关系到统计效果的显著性。流量的分配方式。每一个流量在为其选择参数分支时,希望是不带任何偏见的,也就是均匀采样,通常按照 UUID 或者 Cookie 随机取样。流量的分配条件。还有一些实验需要针对某个流量的子集,例如只对重庆地区的用户测试,推荐时要不要把火锅做额外的提升加权。流量如何无偏置。这是流量分配最大的问题,也是最难的问题。同时只做一个实验时,这个问题不明显,但是要同时做多个实验,那么如何避免前面的实验给后面的实验带来影响,这个影响就是流量偏置,意思是在前面实验的流量分配中,有一种潜在的因素在影响流量分配,这个潜在的因素不易被人察觉,潜在的因素如果会影响实验结果,那么处在这个实验后面获得流量的实验,就很难得到客观的结论。这个无偏置要求,也叫做“正交”。
这些问题也是实验平台在设计之初要考虑的。试想一下,推荐系统中,算法工程师总是在尝试很多模型,或者在线下给出很多的模型调参,线下评测时,各种指标都是一片锣鼓喧天、红旗招展,恨不得立即上线去验验真实效果。每一个算法工程师都这么想,但是线上流量有限,因此需要重叠实验,废水循环,最好能够做到洗脸的水冲马桶,这样灵活的实验平台长什么样子。Google 公司的实验平台已经成为行业争相学习的对象,所以今天我会以 Google 的实验平台为主要对象,深入浅出地介绍一个重叠实验平台的方方面面。
重叠实验架构
所谓重叠实验,就是一个流量从进入产品服务,到最后返回结果呈现给用户,中间设置了好几个检查站,每个检查站都在测试某些东西,这样同时做多组实验就是重叠实验。前面说了,重叠实验最大的问题是怎么避免流量偏置。为此,需要引入三个概念。域:是流量的一个大的划分,最上层的流量进来时首先是划分域。层:是系统参数的一个子集,一层实验是对一个参数子集的测试。桶:实验组和对照组就在这些桶中。层和域可以互相嵌套。意思是对流量划分,例如划分出 50%,这 50% 的流量是一个域,这个域里面有多个实验层,每一个实验层里面还可以继续嵌套域,也就是可以进步划分这 50% 的流量。下面这两个图示意了有域划分和没有域划分的两种情况。
图中左边是一个三层实验,但是并没有没有划分域。第一层实验要测试 UI 相关,第二层要测试推荐结果,第三层要测试在推荐结果插入广告的结果。三层互不影响。图中的右边则添加了域划分,也就是不再是全部流量都参与实验中,而是被分走了一部分到左边域中。剩下的流量和左边的实验一样。这里要理解一点,为什么多层实验能做到重叠而不带来流量偏置呢?这就需要说桶的概念。还是上面示意图中的左图,假如这个实验平台每一层都是均匀随机分成 5 个桶,在实际的实验平台上,可能是上千个桶,这里只是为了举例。示意图如下:
这是一个划分域的三层实验。每一层分成 5 个桶,一个流量来了,在第一层,有统一的随机分流算法,将 Cookie 或者 UUID 加上第一层 ID,均匀散列成一个整数,再把这个整数对 5 取模,于是一个流量就随机地进入了 5 个桶之一。每一个桶均匀得到 20% 的流量。每一个桶里面已经决定好了为你展示什么样的 UI,流量继续往下走。每一个桶的流量接着依然面对随机进入下一层实验的 5 个桶之一,原来每个桶的 20% 流量都被均分成 5 份,每个桶都有 4% 的流量进入到第二层的每个桶。这样一来,第二层每个桶实际上得到的依然是总流量的 20%,而且上一层实验带来的影响被均匀地分散在了这一层的每一个桶中,也就是可以认为上一层实验对这一层没有影响。同样的,第三层实验也是这样。这就是分层实验最最基本的原理。在这个基础上,增加了域的概念,只是为了更加灵活地配置更多实验。关于分层实验,有几点需要注意:每一层分桶时,不是只对 Cookie 或者 UUID 散列取模,而是加上了层 ID,是为了让层和层之间分桶相互独立;Cookie 或者 UUID 散列成整数时,考虑用均匀的散列算法,如 MD5。取模要一致,为了用户体验,虽然是分桶实验,但是同一个用户在同一个位置每次感受不一致,会有损用户体验。Google 的重叠实验架构还有一个特殊的实验层,叫做发布层,优先于所有其他的实验层,它拥有全部流量。这个层中的实验,通常是已经通过了 ABtest 准备全量发布了。示意图如下:
前面举例所说的对用户身份 ID 做散列的流量分配方式,只是其中一种,还有三种流量分配方式,一共四种:Cookie+ 层 ID 取模;完全随机;用户 ID+ 层 ID 取模;Cookie+ 日期取模。在实验中,得到流量后还可以增加流量条件,比如按照流量地域,决定要不要对其实验,如果不符合条件,则这个流量不会再参与后面的实验,这样避免引入偏置,那么这个流量会被回收,也就是使用默认参数返回结果。在 Google 的架构中,由于层和域还可以嵌套,所以在进入某个层时,可能会遇到一个嵌套域,这时候需要按照域划分方式继续下沉,直到遇到实验或者被作为回收流量返回。整个实验平台,工作的示意图如下所示:
说明如下:图中涉及了判断的地方,虚线表示判断为假,实线表示判断为真。从最顶端开始,不断遍历域、层、桶,最终输出一个队列 Re,其中记录对每一个系统参数子集如何处理,取实验配置的参数还是使用默认参数,其中无偏流量表示使用默认参数,也就是在那一层不参与实验,流量被回收。拿到 Re 就得到了全部的实验,在去调用对应的服务。
统计效果
除了分层实验平台之外,还存在另一个问题,每一个实验需要累计获得多少流量才能得到实验结论呢?这涉及了一点统计学知识。实验得到的流量不够,可以说实验的结论没有统计意义,也就浪费了这些流量,而实验在已经具有统计意义之后,如果还占用流量做测试,则也是在浪费流量。如何确定实验规模呢?Google 给出了如下公式
公式中:s 是实验指标的标准差。θ 是希望检测的敏感度,比如想检测到 2% 的 CTR 变化。上面这个公式计算出来的实验规模,表示以 90% 的概率相信结果的显著性,也就是有 90% 的统计功效。
对比实验的弊端
AB 测试实验平台,是产品要做到数据驱动必不可少的东西,但是这种流量划分的实验方式也有自己的弊端,列举如下:落入实验组的流量,在实验期间,可能要冒着一定的风险得到不好的用户体验,在实验结束之前,这部分流量以 100% 的概率面对这不确定性;要得得到较高统计功效的话,就需要较长时间的测试,如果急于看到结果全面上线来说有点不能接收;下线的实验组如果不被人想起,就不再有机会得到测试。诸如此类弊端,也可以考虑在实验平台中用 Bandit 算法替代流量划分的方式,通过 Bandit 算法选择不同的参数组合、策略,动态实时地根据用户表现给出选择策略,一定程度上可以避免上面列举的弊端。
总结
实验平台是推荐系统要做到数据驱动必不可少的东西,但是如何做到科学高效快速地做实验呢?常见的做实验,只是简单地选择一个尾号的用户 ID 作为实验组,再选择另一个尾号作为对照组,甚至选择剩下所有的用户 ID 作为对照组。这样做出来的实验,显然是有问题,因为并不知道通过用户尾号来分组是不是能做到无偏?另一个问题是,这样就只能在一个时期只能做一个实验,非常低效。本文以 Google 开放的实验平台架构作为原型,对其核心技术做了详细介绍。这个实验平台做到了同时无偏地做多组对照实验。因为它巧妙地引入了三个概念的嵌套结合:
域;层;桶。三个概念层层相扣,流量划分得到了一个可行的方案。这个实验平台方案已经应用在很多公司中,你不妨在自己的公司尝试做一下。最后留给你一个问题,关于分层实验的原理,你是否已经理解了为什么层和层之间可以做到毫不影响,欢迎给我留言讨论。
推荐系统服务化、存储选型及API设计
在过往的文章中,我讲到了推荐系统方方面面的相关概念。那么说,对于认识一个推荐系统来说,还差最后一个问题需要解决,那就是:万事俱备,如何给用户提供一个真正的在线推荐服务呢?
服务化是最后一步
其实一个推荐系统的在线服务,和任何别的在线服务相比,也没有什么本质区别,只是仍然还有一些特殊性。提供一个在线服务,需要两个关键元素:数据库和 API。今天我就来专门说一说推荐系统中大家常常用到的数据库,并会谈谈推荐系统的 API 应该如何设计。
存储
这里注意一下,今天这里讲到的存储,专指近线或者在线部分所用的数据库,并不包括离线分析时所涉及的业务数据库或者日志数据库。近线和在线的概念我在前面已经讲到过。推荐系统在离线阶段会得到一些关键结果,这些关键结果需要存进数据库,供近线阶段做实时和准实时的更新,最终会在在线阶段直接使用。首先来看一下,离线阶段会产生哪些数据。按照用途来分,归纳起来一共就有三类。特征。特征数据会是最多的,所谓用户画像,物品画像,这些都是特征数据,更新并不频繁。模型。尤其是机器学习模型,这类数据的特点是它们大都是键值对,更新比较频繁。结果。就是一些推荐方法在离线阶段批量计算出推荐结果后,供最后融合时召回使用。任何一个数据都可以直接做推荐结果,如协同过滤结果。如果把整个推荐系统笼统地看成一个大模型的话,它依赖的特征是由各种特征工程得到的,这些线下的特征工程和样本数据共同得到模型数据,这些模型在线上使用时,需要让线上的特征和线下的特征一致,因此需要把线下挖掘的特征放到线上去。特征数据有两种,一种是稀疏的,一种是稠密的,稀疏的特征常见就是文本类特征,用户标签之类的,稠密的特征则是各种隐因子模型的产出参数。特征数据又常常要以两种形态存在:一种是正排,一种是倒排。正排就是以用户 ID 或者物品 ID 作为主键查询,倒排则是以特征作为主键查询。两种形态的用途在哪些地方呢?在需要拼凑出样本的特征向量时,如线下从日志中得到曝光和点击样本后,还需要把对应的用户 ID 和物品 ID 展开成各自的特征向量,再送入学习算法中得到最终模型,这个时候就需要正排了。
另一种是在需要召回候选集时,如已知用户的个人标签,要用个人标签召回新闻,那么久就需要提前准备好标签对新闻的倒排索引。这两种形态的特征数据,需要用不同的数据库存储。正排需要用列式数据库存储,倒排索引需要用 KV 数据库存储。前者最典型的就是 HBase 和 Cassandra,后者就是 Redis 或 Memcached。稍后再介绍这几个数据库。另外,对于稠密特征向量,例如各种隐因子向量,Embedding 向量,可以考虑文件存储,采用内存映射的方式,会更加高效地读取和使用。模型数据也是一类重要的数据,模型数据又分为机器学习模型和非机器学习模型。机器学习模型与预测函数紧密相关。模型训练阶段,如果是超大规模的参数数量,业界一般采用分布式参数服务器,对于达到超大规模参数的场景在中小公司不常见,可以不用牛刀。而是采用更加灵活的 PMML 文件作为模型的存储方式,PMML 是一种模型文件协议,其中定义模型的参数和预测函数,稍后也会介绍。非机器学习模型,则不太好定义,有一个非常典型的是相似度矩阵,物品相似度,用户相似度,在离线阶段通过用户行为协同矩阵计算得到的。相似度矩阵之所算作模型,因为,它是用来对用户或者物品历史评分加权的,这些历史评分就是特征,所以相似度应该看做模型数据。最后,是预先计算出来的推荐结果,或者叫候选集,这类数据通常是 ID 类,召回方式是用户 ID 和策略算法名称。这种列表类的数据一般也是采用高效的 KV 数据库存储,如 Redis。
另外,还要介绍一个特殊的数据存储工具:ElasticSearch。这原本是一个构建在开源搜索引擎 Lucene 基础上的分布式搜索引擎,也常用于日志存储和分析,但由于它良好的接口设计,扩展性和尚可的性能,也常常被采用来做推荐系统的简单第一版,直接承担了存储和计算的任务。下面我逐一介绍刚才提到的这些存储工具。
1. 列式数据库
所谓列式数据库,是和行式数据库相对应的,这里不讨论数据库的原理,但是可以有一个简单的比喻来理解这两种数据库。你把数据都想象成为矩阵,行是一条一条的记录,例如一个物品是一行,列是记录的各个字段,例如 ID 是一列,名称是一列,类似等等。当我们在说行和列的时候,其实是在大脑中有一个抽象的想象,把数据想象成了二维矩阵,但是实际上,数据在计算机中,管你是行式还是列式,都要以一个一维序列的方式存在内存里或者磁盘上。那么有意思的就来了,是按照列的方式把数据变成一维呢,还是按照行的方式把数据变成一维呢,这就是列式数据库和行式数据库的区别。当然实际上数据库比这复杂多了,这只是一个简单形象的说明,有助于你去理解数据的存储方式。列式数据库有个列族的概念,可以对应于关系型数据库中的表,还有一个键空间的概念,对应于关系型数据库中的数据库。众所周知,列式数据库适合批量写入和批量查询,因此常常在推荐系统中有广泛应用。列式数据库当推 Cassandra 和 HBase,两者都受 Google 的 BigTable 影响,但区别是:Cassandra 是一个去中心化的分布式数据库,而 HBase 则是一个有 Master 节点的分布式存储。Cassandra 在数据库的 CAP 理论中可以平滑权衡,而 HBase 则是强一致性,并且 Cassandra 读写性能优于 HBase,因此 Cassandra 更适合推荐系统,毕竟推荐系统不是业务逻辑导向的,对强一致性要求不那么强烈,这和我在一开始建议“你要建立起不确定思维”是一脉相承的。
Cassandra 的数据模型组织形式如下图所示:
从这个图可以看出来,可以通过行主键及列名就可以访问到数据矩阵的单元格值。前面也说过,用户和物品的画像数据适合存储在 Cassandra 中。也适合存储模型数据,如相似度矩阵,还可以存储离线计算的推荐结果。
2. 键值数据库
除了列式数据库外,还有一种存储模式,就是键值对内存数据库,这当然首推 Redis。Redis 你可以简单理解成是一个网络版的 HashMap,但是它存储的值类型比较丰富,有字符串、列表、有序列表、集合、二进制位。并且,Redis 的数据放在了内存中,所以都是闪电般的速度来读取。在推荐系统的以下场景中常常见到 Redis 的身影:消息队列,List 类型的存储可以满足这一需求;优先队列,比如兴趣排序后的信息流,或者相关物品,对此 sorted set 类型的存储可以满足这一需求;模型参数,这是典型的键值对来满足。另外,Redis 被人诟病的就是不太高可用,对此已经有一些集群方案,有官方的和非官方的,可以试着加强下 Redis 的高可用。
3. 非数据库
除了数据库外,在推荐系统中还会用到一些非主流但常用的存储方式。第一个就是虚拟内存映射,称为 MMAP,这可以看成是一个简陋版的数据库,其原理就是把磁盘上的文件映射到内存中,以解决数据太大不能读入内存,但又想随机读取的矛盾需求。哪些地方可以用到呢?比如你训练的词嵌入向量,或者隐因子模型,当特别大时,可以二进制存在文件中,然后采用虚拟内存映射方式读取。另外一个就是 PMML 文件,专门用于保存数据挖掘和部分机器学习模型参数及决策函数的。当模型参数还不足以称之为海量时,PMML 是一个很好的部署方法,可以让线上服务在做预测时并不依赖离线时的编程语言,以 PMML 协议保存离线训练结果就好。
API
除了存储,推荐系统作为一个服务,应该以良好的接口和上有服务之间交互,因此要设计良好的 API。API 有两大类,一类数据录入,另一类是推荐服务。数据录入 API,可以用于数据采集的埋点,或者其他数据录入。
推荐服务的 API 按照推荐场景来设计,则是一种比较常见的方式,下面分别简单说一下 API 的样子。
1. 猜你喜欢
接口:/Recommend
输入:
* UserID – 个性化推荐的前提 * PageID – 推荐的页面 ID,关系到一些业务策略 * FromPage – 从什么页面来 * PositionID – 页面中的推荐位 ID * Size – 请求的推荐数量 * Offset – 偏移量,这是用于翻页的
输出:
* Items – 推荐列表,通常是数组形式,每一个物品除了有 ID,还有展示所需的各类元素 * Recommend_id – 唯一 ID 标识每一次调用,也叫做曝光 ID,标识每一次曝光,用于推荐后追踪推荐效果的,很重要 * Size – 本次推荐数量 * Page —— 用于翻页的
2. 相关推荐
接口:/Relative
输入:
* UserID – 个性化推荐的前提 * PageID – 推荐的页面 ID,关系到一些业务策略 * FromPage – 从什么页面来 * PositionID – 页面中的推荐位 ID * ItemID – 需要知道正在浏览哪个物品导致推荐相关物品 * Size – 请求的推荐数量 * Offset – 偏移量,这是用于翻页的
输出:
* Items – 推荐列表,通常是数组形式,每一个物品除了有 ID,还有展示所需的各类元素 * Recommend_ID – 唯一 ID 标识每一次调用,也叫做曝光 ID,标识每一次曝光,用于推荐后追踪推荐效果的,很重要 * Size – 本次推荐数量 * Page —— 用于翻页的
3. 热门排行榜
接口:/Relative
输入:
* UserID – 个性化推荐的前提 * PageID – 推荐的页面 ID,关系到一些业务策略 * FromPage – 从什么页面来 * PositionID – 页面中的推荐位 ID * Size – 请求的推荐数量 * Offset – 偏移量,这是用于翻页的
输出:
* Items – 推荐列表,通常是数组形式,每一个物品除了有 ID,还有展示所需的各类元素 * Recommend_id – 唯一 ID 标识每一次调用,也叫做曝光 ID,标识每一次曝光,用于推荐后追踪推荐效果的,很重要 * Size – 本次推荐的数量 * Page —— 用于翻页的相信你看到了吧,实际上这些接口都很类似。
总结
今天我主要讲解了推荐系统上线的两大问题,一个是线上数据存储,另一个是推荐系统的 API 有哪些。虽然实际情况肯定不是只有这点问题,但是这些也足以构建出一个简单的推荐系统线上版了。你还记得在前几篇专栏中,我提到统一考虑搜索和推荐的问题吗?那么说,如果要把推荐和搜索统一考虑的话,API 该如何设计呢?
推荐系统详解(十)常见模块相关推荐
- 文件系统的详解与常见文件系统模式比较
目录 1. 文件系统的详解 2. 常见的文件系统比较 2.1 Windows下的文件系统 2.1.1 FAT16文件系统 2.1.2 FAT32文件系统 2.1.3 FAT64文件系统(exFAT文件 ...
- android WebView详解,常见漏洞详解和安全源码(下)
上篇博客主要分析了 WebView 的详细使用,这篇来分析 WebView 的常见漏洞和使用的坑. 上篇:android WebView详解,常见漏洞详解和安全源码(上) 转载请注明出处:http ...
- android WebView详解,常见漏洞详解和安全源码(上)
这篇博客主要来介绍 WebView 的相关使用方法,常见的几个漏洞,开发中可能遇到的坑和最后解决相应漏洞的源码,以及针对该源码的解析. 由于博客内容长度,这次将分为上下两篇,上篇详解 WebView ...
- SpringMVC接受JSON参数详解及常见错误总结我改
SpringMVC接受JSON参数详解及常见错误总结 最近一段时间不想使用Session了,想感受一下Token这样比较安全,稳健的方式,顺便写一个统一的接口给浏览器还有APP.所以把一个练手项目的前 ...
- python中yaml模块的使用_详解Python yaml模块
一.yaml文件介绍 yaml是一个专门用来写配置文件的语言. 1. yaml文件规则 区分大小写: 使用缩进表示层级关系: 使用空格键缩进,而非Tab键缩进 缩进的空格数目不固定,只需要相同层级的元 ...
- c#listbox使用详解和常见问题解决
c#listbox使用详解和常见问题解决 参考文章: (1)c#listbox使用详解和常见问题解决 (2)https://www.cnblogs.com/whuanle/p/8622830.html ...
- python实现日历功能_详解Python日历模块的使用
calendar模块的函数都是日历相关的,提供了对日期的一些操作方法,和生成日历的方法. calendar模块中提供了三大类: 一.calendar.Calendar(firstweekday=0) ...
- Linux内核Thermal框架详解十二、Thermal Governor(2)
本文部分内容参考 万字长文 | Thermal框架源码剖析, Linux Thermal机制源码分析之框架概述_不捡风筝的玖伍贰柒的博客-CSDN博客, "热散由心静,凉生为室空" ...
- 元宇宙技术普及读本重磅问世 详解十大技术 把脉数字经济 前瞻产业布局
转自 元宇宙共识圈 王恩东.倪光南.沈昌祥.郑纬民--四位中国工程院院士联袂力荐 倪健中.姚前.李正茂.朱嘉明.肖风.敖然等权威专家一致推荐 汇聚元宇宙技术专家及产业一线佼佼者倾力撰写 元宇宙技术普及 ...
- 攻防世界杂项(misc)--新手练习区(详解十二道题完结,附件做题过程中使用到的各种工具和网站)
攻防世界杂项(misc)–新手练习区(详解) 第一题:this_is_flag 题目描述:Most flags are in the form flag{xxx}, for example:flag{ ...
最新文章
- php fopen 中文,php fopen用法是什么
- python教程:文件读写
- 【推荐系统】手写ItemCF/UserCF代码,你会吗?
- k8s ready 不调度_从零开始学K8s: 10.在K8s上运行应用
- 使用MyBatis集成阿里巴巴druid连接池(不使用spring)
- Jetty 服务器架构分析(中)
- SprinBoot 集成 Flowable/Activiti工作流引擎
- 案例分享,从0到1了解一个完整项目
- Android -- 重置Bitmap大小Bitmap转角度
- Codeforces Round #456 (Div. 2)
- SAP ABAP开发视频学习(视频教程)
- CentOS7.4 更改SSH端口号
- Matlab画堆叠柱状图(颜色设置,x轴外部标注,y轴标注,颜色设置)
- 计算机网络 如何算 子网号,已知Ip地址子网掩码如何计算子网号、主机号.doc
- 意外的计算机音乐,富有灵魂的音乐 Realwav SVEN 意外发烧
- 大数据平台的SQL查询引擎有哪些
- w7系统事件日志服务器,win7事件查看器里说事件日志服务不可用怎么回事
- team viewer如何解绑设备
- 无线蓝牙手表FCC ID认证测试项目有哪些?
- js 实现在当前页面打开新窗口
热门文章
- 高可用架构设计——深入剖析分布式高可用架构的关键技术和实战经验
- [1]云计算的收益与风险
- 怎么用python算单价和总价_使用Python语言编写某个交易所合约手续费计算程序
- 大数据分析平台洱源县_洱源:引入“发财树” 群众奔富路
- 2017.2.11【初中部 GDKOI】模拟赛B组 T4:摧毁巴士站
- 浅谈软件测试的核心价值
- 微信小程序小练习——豆瓣电影小程序练习
- codeforces 719E (线段树 + 矩阵快速幂)
- Yocto系列讲解[技巧篇]82 - 静态库编译问题
- 二级MySQL数据库程序设计(五)