google提供的adb工具包_用 Google 运筹学工具包计算最优课堂分组

eb94ebdace763df84b149da07ca996e3.gif

同学们,新年都过了,来和老思一起 耍个花(误) 提高效率。

问题是这样的。我们有一个班,总共 20 名同学,要分成 5 组,每组 4 个人。因为选课的同学有不同专业,从大一到大四都有,还有交换生。出于 diversity 的考虑,我们要求同一专业的不能在一组(也包括交换生不能全在一组的限制)。为了均衡实力,大一的同学不能全部在一组。当满足以上条件后,允许同学提交组队倾向,我们尽可能照顾到。组队倾向是一个列表,依次写上自己希望组队的同学。为了鼓励大家相互了解,每个同学开了一个 GitHub repo,用 README 文件做自我介绍,包括感兴趣的议题、自己的专长、正在寻找的队友等。

怎么样?是不是很简单?

我想起了一张老图,快一年了。当时艺术圈的朋友发过来,问这个能不能用 AI 解决。我说能啊。不信我们往下看。

同学们先研究一下原问题:

72c8de0eece3d7795c5455bf9dda6e74.png

插播:能迅速解决以上问题的同学,欢迎将答案发到老思邮箱!老思帮你留意着,如果下次有朋友团队招人,第一时间通知。

这是一个典型的带约束多目标优化问题。优化的目标,一是广告费,二是持份者(stakeholder)的体验。至于约束,包括了老板的要求、腕的大小、鄙视链等等。鉴于这个问题还有诸多实际的细节需要考虑,就不在本篇文章展开探讨。感兴趣的同学,可以当成是思考题。

这个 “带约束多目标优化问题” ,还有一个响亮的名字,估计大多数人从小听过,但也没搞清楚是啥:

f50272f652b5ac7cc59dd0c23d180e3a.png 运筹学

我们举个更简单的例子。老思早上起来开会,洗漱需要15分钟,叫外卖送达时间是20分钟,吃早餐用10分钟,家到会场打车要30分钟,会前需要花10分钟处理紧急邮件,为免电梯排队最好提前5分钟到。问:如果开会时间是早上9点钟,老思应该什么时候起床?

7:30am 吗?

不。聪明的同学已经想到了,老思可以 8am 起床。先用美团叫一份麦当劳满分鸡腿堡,再用滴滴预约25分钟之后的车,接着花15分钟洗漱,刷一下微信朋友圈,收到麦当劳后下楼,上车,在车上用10分钟吃早餐,然后用10分钟处理紧急邮件,提前5分钟到。完美。

前提是不能赖床。这个简单,就不讨论了。

这就是运筹学要解决的问题。这个开早会的例子,应该可以在大多数教科书找到,是一个典型的 线性规划 问题。其目标和约束都是线性的,并且变量是在实数域,有成熟的解法,是该领域最简单的问题。而我们可怜的艺统,她所面对的问题就复杂很多,连目标都不止一个,解决起来就比较困难。至于开篇提到的分组问题,难度系数则是适中。它的目标只有一个,简单明了。与开早会不同的地方,在于变量是离散的。别小看这个简单的变化。虽然从实数到整数,似乎潜在的取值少了,但解决起来却更复杂。在运筹学的体系中,我们这个问题叫做 混合整数规划 (MIP)

好了,我们准备 起飞(误) 建模了。

先上神器,Google 运筹学工具包:

892e0ea2f4b2e49c225fce3dc1d6c150.png

https://developers.google.com/optimization/

运筹学的英文是 Operations Research(OR),所以这个工具叫 ortools。项目算是 Google AI 的一部分。所以大家知道,为什么老思告诉朋友,这个问题能用 AI 解了吧!AI 这个词的外延,大到几乎可以包括一切和“自动化”沾边的东西,而运筹学的理论,核心是优化模型和优化算法,属于根基中的根基。无论是搞图像的、文本的、声音的,最后多半会归结为一个优化问题。

我们今天来试试返璞归真。

撞击预警:前方略有硬度,如感不适,可以直接拉到底部点赞。

我们先看下数据集的基本特点。

e1129e12ce814a37144b2e3d05026730.png

专业的分布是比较散的,最多的专业,只有4个人。按照分5组的需求,可以完全打散。看年级,大一的5个,大三的6个,大四的9个。如果要把大一的都打散,刚好5个组。这个检验是有必要的。说明约束可以满足。如果约束根本无法满足,就要进行“松弛”操作,使得模型更复杂。

背景检查过关,可以开始建模了。先把 Google 的 ortools 搞进来再说。

334996dd0084ddbabb849b34b9936a1e.png

然后到了关键问题:什么是变量?

变量定义是建模过程的至关重要一步。变量定义得好,在约束和目标书写的时候,会更简单,而且也可能将问题规模缩小。对 MIP 来说,问题规模决定了在当前算力下是否可解。因为整数变量的存在,大多数算法是有一个“试错”的过程在里面的,就是对可行解进行“搜索”。这部分的成本非常高。

不过,也要考虑到建模本身的成本。如果物理问题本身规模不大,哪怕“暴力”地转化成优化问题,问题规模也可控。如果简单的模型就奏效,则不用过早地去优化它。就是杀鸡不用牛刀的意思。

很容易想到的一种变量设置方法,就是“对对碰”。设置整数变量 v_ij in {0, 1},为 0 表示 i 和 j 不在一个组里面,为 1 则表示 i 和 j 在一个组。我们班上有 20 个同学,所以总共有 200 个变量。因为 v_ij 和 v_ji 是对称的,这里省去了一半。变量用 ortools 里面的 IntVar 类来表示,用 Python 写出来就是这样:

1bb111223e75918258a29aa34533d0de.png

目前的模型中,只有变量,没有目标和约束,所以是 trivial 的。理论上可以解出来,而且是瞬间解出来。老思的习惯是,做一步,看一步,确认每步的结果都合理,再进行下一步。那我们就检验一下:

a37fb3812088da88d7c15e9b46e0071b.png

确认是得到了最优解。

还不放心的同学,可以检查一下当前变量的名字和值。通常的初始化是全 0 的。所以目前我们得到的结果是全 0,意味着没有同学和其他人分到一组。我们并没有指定一组的大小,所以这个结果是合理的。用以下方法打印出来:(记得 注释/反注释 相关的 print)

16d2fc4828f51f0fb92ac7ae6c56b512.png

接下来,我们可以添加约束了。首先限制每组的大小。我们的需求是每组有 4 个人,即每个人需要和另外 3 个人一组。用数学语言表达,就是对任意一个学生 i ,把所有的 v_ij 加在一起,值是 3 。注意 v_ij 的取值只能是 0 或者 1,这个约束就保证了每个 i 刚好和另外的 3 个同学是一组,不多不少。我们添加约束。并且老规矩,跑一下结果。

8b6b618643d2ca0b01a994cd2cda3759.png

看起来不难,很快出结果。Solve 函数返回 0就是最优的意思。

再进行下一步之前,我们想看看结果的物理意义,是否合理。考虑到 v 有 200 来个,全部打印出来,我们也看不懂是怎么分组的,需要换一种呈现方法。这就用到图论了。我们把每个同学当作节点, v_ij 当作边,0就是没,1就是有。先把这个图给建立好,再跑一个连通分量,就得到最终的结果了。同一个连通分量中的同学,就是一个组的。

我们把这个图给建立好。跑算法之前,最好先可视化一下,做做基本的肉眼检查,防止出现重大的么蛾子。

14fea2b0758129c2e002e3c3410975d4.png

果然有问题!按照这个图,全班都在一个组里面,因为只有一个联通分量。

然而,我们仔细检查一下。每个节点,确实连且只连了 3 条边出去,完全满足我们设置的约束。那问题再哪里?

我们在图中,找到相邻的 3 个点,A、B、C,就发现问题了:AB 相连,BC 相连,然而AC却不相连。这就是 transitivity 的问题。如果 A 和 B 在一个组,B 和 C 在一个组,同学们就可以推出, A 和 C 一定也在一个组。这时候代表 AC 边的变量,应该是 1 才对。这个约束没有考虑到。

对任意的 A、B、C,我们需要考虑一个值 v_AB + v_BC + v_CA。这个式子的取值范围是 0 到 3 。来做一个分类讨论。如果为 0,说明三个人互不在同一个组,合理。如果为1,说明有两个人在一个组。不失一般性,设为 A、B,则 v_AB 为 1。第三人 C 与该两人都不在一个组,即 v_BC 和 v_CA 为 0。合理。如果式子的值是 3,表示三个变量都为 1,三个人两两在同一个组,也就是三个人在同一个组。合理。所以出问题的,就是这个式子为 2 的时候。不失一般性,设 v_AB = 1, v_BC = 1,那么我们可以推出 CA 也在一个组, v_CA = 1。矛盾。所以要加的约束是:

v_AB + v_BC + v_CA != 2

原理是清楚了,不过要表达出不等于的约束可不容易。事后老思发现, ortools 提供了一个 AllDifferent 可以利用,解决起来也不难。可惜当时没看完手册,TLDR,就硬刚了。又印证那句古话:以思,无益,不如学也。在自己陷进去,不断思索和尝试前,如果能透彻地学习,就会少走很多弯路。不过路都是走出来的。选好了方向,一直走,就能走到。

现在我们遇到的问题是,要表达一个  x != c 的约束。x 是若干变量的线性组合,a 是一个常数(之后我们带入 2)。但是 ortools 提供的接口,是可以表达  a <= x <= b 这样的条件。怎么用两个 <= ,通过“且”连接,去表达出不等于呢?同学们先自己可以试试。我们马上要剧透了。

老思依稀记得是有方法的。但是已经还给老师了…… (如果能再回到十年前,老思一定好好学习……)

多亏了云存储,妈妈再也不用担心硬盘坏了:

b73a237a01198cb9f819ffbd134286f1.png

翻查了一下资料,方法倒是挺精彩的,不知道第一个人是如何想到的。Orz。

首先 x != c,就是 x > c 或 x < c。注意这个“或”字,不是“且”。难度都在这里了。因为标准的 MIP 模型,约束之间是“且”的关系。所以我们要做的事,是写两个“且”连接的约束,但通过处理,让其中任意一个满足即可。好像有一个开关,可以把两者中的一个给激活,但不影响另一个。而怎么制造开关呢。其实就是一个辅助变量  δ in {0, 1}。δ 为 0 的时候,激活一个约束,让另一个约束成为 trivial 的;δ 为 1 的时候,则反过来。

x > c - δ M

x < c + (1 - δ) M

整理一下就是:

c + 1 <= x + M δ <= c + M - 1

这个形式就和 Google ortools 的接口一致了。对任意的三个同学,组成的三个对对变量,我们都添加一组这个约束,就能保证 transitivity 了。总共有多少条这样的约束呢?

48032a8bd3dd4f0c61ea6192813ce233.png

多了1000多条约束,还可以。添加进我们的模型。

6e02e3d1eeb3b65a8b31f0435761a4bc.png

3 个同学之间的 transitivity 解决了。那现在又有一个问题,如果是 4 个同学,产生的 6 个变量,中间会出现矛盾吗?其实不会的,只要 3 个同学的 transitivity 满足了,可以用归纳法推及更多的同学组合。这次运行求解,大概花了几秒钟,有明显的延迟了。看来多了 1000 多条约束,计算开销确实大了。我们求解并画图:

920ba0567dbdf6b80f6f3517a2e47cf5.png

这个结果,肉眼看上去没有大问题。我们用一行求得连通分量,则可把 200 个优化问题的变量之解,转化为原问题的答案了。

872107159d8d49c7c8f4763be59e4bd3.png

好了,整了老半天,终于把一个分组问题的基本架子给搭好了。即,目前我们可以得到 20 个人分 5 组,每组 4 人,这个条件下的随机结果。到此为止的基础模型,是可以用在其他场景的,比如婚礼排桌号。接下来要做的事情,就比较针对特定问题了。我们这里还差一个最大化 preference 的目标,以及关于 diversity 的约束。

做事如同做人。先定目标。

f4ae08aab9de92848f1de4edd201626b.png

这里的目标设置相对简单,没有考虑 preference 的顺序。只要有某个同学 i ,她的 preference list 里面包含了 j ,则 v_ij 在目标中的系数,设置为 1 。最终的版本,会在收集齐所有的 preference list 之后,做一些微调,反应出顺序。比如,满足第一志愿的,配对成功的体验应该更显著。如何细化,同学们都可以发挥想象了。

在这之前,求出的结果其实都不算是优化的结果,只是约束满足问题(CSP)。既然设置了目标,并且最大化它,那么先确认一下,优化过程已经进行了。通过把最优值打印出来,可以看到,有 9 组 preference 被满足了,并且确认我们是得到了可行解的。

be5406949f25a3d56a9a0d90abb93ca5.png

在我们进一步添加约束之前,把分组的信息,和同学的背景放在一起,做个检验。如下图所示,第 0 组有两个同专业的。第 4 组出现了两个大一的。因为目前只设置了目标,尽可能满足同学提交的 preference,但是还没设置 diversity 约束,所以这个结果是合理的。而设置完 diversity 约束之后,我们再来看这个表,就出现变化后,就可以确认约束生效了。

一步一步检查,靠谱。

be3005fe3585287c10180263bea9db58.png

diversity 约束添加进去之后,问题应该变得更简单。说到底,diversity 约束是在声明一些 v_ij 为 0,当 i 和 j 不能在一组的时候。每多一组 diversity 约束,就相当于少了一个变量(变成了常数 0)。

终于到了最后一击。

9d873caff577adfd716dc275d2e681e1.png

添加完 diversity 约束后,有 23 个变量变为了 0。满足所有约束的条件下,目标值是 6,即有 6 对 preference 被满足。检查一下结果,已经没有同专业在一组,或者两个大一的在一组了。

1efaadc464ac370ed3d045494107c0cf.png

搞定。

当然,这不是最终的分组结果。目前还在搜集 preference 的阶段。搜集齐后,我们再跑一次程序就可以了。程序的好处,就是写一次、用 N 次。虽然时间成本不小,不过要收回来也快。下次再上这门课,就回本了。如果多几门课用,就赚翻了。

最后,我们总结一下知识点。

首先,同学们应该可以闲侃一下 “运筹学” 了。姿势大体上是这样的:把老思开早会的例子举一枚,然后抛出这个酷炫的名词,跟对方说,用 Google ortools 可以解,然后转身离去,把功与名藏好。如果被对方扭臊着问细节,则应该首先保持冷静,然后酷酷地说:我忙着呢,这种细节自己看文档去,不行找个程序员帮忙。

其次,Google ortools 好用,不及 Google Drive 好用。找回 10 年前的资料,还是很给力的。俗话说,书到用时方恨少,哪怕不少也不好找。真心希望 10 年后,还能找到这些资料。只是 Google Drive 最近突然涨价了 20% ,完全没没没没没防备。看来得迎接一下初五财神,来对冲它继续涨价的风险了!

完。

 P话 

写给100年后的考古学家

 服老思 

长期关注大数据和区块链,探索传媒应用与教学

9a519af7938975a042e85c666e000ad0.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值