本次的赛题比较适应当前的机器学习的潮流。初赛题目分成两个部分——虚拟机用量预测+虚拟机部署。预测部分是一个时序预测问题,需要根据历史数据训练得到模型,来预测将来某个时间段的虚拟机使用量。虚拟机部署则需要将预测的虚拟机用量部署到固定大小的物理机上,以达到最高的利用率。比赛最终的成绩由两部分的共同决定:
代码仓库地址
预测部分需要预测的不是每天虚拟机的数量,而是一段时间内(一周至两周)的虚拟机用量。
最初的想法,就是按照时间长短比例,来预测未来的值。就是按照训练数据时间长度(一个月)和待预测时间段(一周)的时间比例,和训练数据中各种虚拟机用量的统计数据,来预测待预测时间段的各种虚拟机用量。如果虚拟机用量是相对稳定的,除却噪声的影响,这种方法效果应该不会太差。但是根据线下线上的测试结果来看,效果不是太好,应该是偏低了,说明虚拟机用量呈现了一个增长趋势。
直接平均的方法丢失了较多的信息,预测的结果也不好,于是就考虑用滑动平均的方法。即根据待预测目标的时间长(一周),对训练时间内各种虚拟机的用量序列进行滑动求和,就是统计连续一周内的虚拟机用量。这里会有一个步长选择的问题,滑动步长为1,即滑动一天求和一次;还可以有其他的滑动步长,比如滑动一周求和一次。我只尝试了1天的滑动步长。滑动求和会产生一个序列,长度依据滑动步长而定。得到滑动求和序列后,最朴素的想法就是对这个序列求平均,但这个直接求平均差别不大,所以我没有尝试。另一个思路是得到滑动求和序列后,可以在此基础上做线性回归。但是选定步长为一天的线上效果并不好,赛后交流同学说步长为一周的效果可能会好一点。┐(゚~゚)┌
另一个队友的奇思妙想,同样是以滑动求和序列为基础但是不做线性回归,而是做加权平均。时序预测最直接的感觉就是与待预测时间越近的历史数据越重要,所以应该分配一个较高的权重。而这些权重的求和最好能为1。所以最终表达式就是:
除了上面的方法,我们还尝试了对滑动求和序列取最大值,这个方法在线下的效果还不错,但是线上效果比较差。
所以为了利用各种模型各自的优势,我们各种模型的结果进行了一定的集成。我们朴素的对“取最大值”、“加权平均”、“线性回归”三种方法的结果进行了平均,但是很遗憾,效果依然不如人意。到此为止,我们已经用完了初赛的最后一次尝试机会了。GG
其实在做比赛的时候,我们先做的分配。分配问题是需要将一些虚拟机部署到有固定容量的物理机上,乍一看是个背包问题:以虚拟机的CPU和MEM作为重量,以待优化的目标作为价值,比如需要优化CPU利用率最大,则将CPU的利用率作为物品的价值。但是背包问题的解法只能求解一个背包情况下,放置物品能够得到的最大价值,现在遇到的问题是需要将这些物品都放置完毕,单个背包的已经不能解决问题,需要进行多次背包——多次背包已经不能保证解最优了,所以本质上是一个贪心的算法,每次背包就是从剩余虚拟机中选择使得物理机利用率最高的虚拟机。
注意:分配问题存在一个下限,即max{v_CPU/CPU,v_MEM/MEM},下限表明了该分配问题至少需要这么多台物理机。这个下限可以用来验证某种分配方案的性能。
限制虚拟机放置的因素有两维:CPU和MEM。所以dp数组是三维,dp[i][j][k],表示当物理机CPU总量为i,MEM总量为j时,考虑物品k所能达到的最大价值。在判定是否放置物品k时,与普通背包问题是一样的。下面展示一下关键部分的示意代码,这里优化目标以CPU利用率为例
for i in range(1,CPU+1):
for j in range(1,MEM+1):
for k in range(1,K+1):
if i>=cpu_k and j>= mem_k:
dp[i][j][k]=max(dp[i-cpu_k][j-mem_k][k-1]+cpu_k,dp[i][j][k-1])
else:
dp[i][j][k]=dp[i][j][k-1]
直接将CPU或者MEM的使用量作为价值,贪心出来的效果不是很好,因为在优化CPU的情况下,有的方案可以使CPU的使用率达到100%,但是内存的使用率却比较低,但在之前的算法看来,这些方案是等价的。所以改进就是同时考虑CPU和MEM的使用率,定义价值v为cpu和mem两者使用率求和。还是展示核心部分的代码:
for i in range(1,CPU+1):
for j in range(1,MEM+1):
for k in range(1,K+1):
v=cpu_k/CPU+mem_k/MEM
if i >= cpu_k and j>= mem_k
dp[i][j][k]=max(dp[i-cpu_k][j-mem_k][k-1]+v,dp[i][j][k-1])
else:
dp[i][j][k]=dp[i][j][k-1]
这种分配方案就能比较好的接近分配的下限,但是代码运行速度不够快。
因为我用的编程语言是python,运行效率较慢。三层for循环对于较大的规模的放置问题需要的时间就很长,所以在同学的建议下,将三层for循环改进到了两层for循环,可以大幅度减少时间,但是不能像层循环一样保证某一步背包的最优。同样只考虑优化CPU的情况,dp[i][k][2],i表示当前物理机的内存总量,k表示当前考虑的虚拟机,之前的不同,每个dp[i][k]对应了两个值,[总利用率,当前的CPU用量],在考虑是否放置第k个虚拟机时,要求考虑当前CPU存量,但是这个方法不能保证最优。
下面的代码还引入了提早退出的机制:如果总的使用率已经达到了1.0+1.0=2.0,则这一轮的分配就可以提前返回了,不用再继续,再次提高了分配的速度。
for i in range(1,MEM+1):
for k in range(1,K+1):
if i > mem_k and dp[i][k-1][1] + cpu_k <= CPU and dp[i - mem_k][k - 1][0]+cpu_k / CPU + mem_k / MEM > dp[i][k - 1][0]:
dp[i][k][0] = dp[i - mem_k][k - 1][0] + cpu_k / CPU + mem_k / MEM
dp[i][k][1] = dp[i - mem_k][k - 1][1] + cpu_k
else:
dp[i][k][0] = dp[i][k - 1][0]
dp[i][k][1] = dp[i][k - 1][1]
if dp[i][k][0] == 2:
return
python语言有很多便捷之处,但是有些语法上的便捷底层的效率比较低。比如在做背包问题的时候,某样物品被选定放入背包的时候,我们需要将其从原物品集(假设是list)中删除。比较方便的语法是直接将改下标的元素pop,这就导致了变长list的产生,而且每次pop一个元素,都会变化一次,这是非常低效的行为。后来我们在做代码优化的时候,尽量避免了这些写法,速度有了较大的提高。
最终成绩由预测+分配两部分相乘得到,所以可以牺牲预测的准确性,来提高物理机的分配效率。比如根据预测的用量,分配完毕后,存在某台物理机的用量非常低,则可以牺牲这些虚拟机,修改预测的值,从而消除这台利用率较低的物理机,提高分配效率。毕竟预测总是不太准。可惜我做初赛的时候不剩几天了,而且面试腾讯刚跪,心情那个糟啊(╯︵╰),这些想法都无法尝试了。根据赛后同学讲述,这个方法还是可以提高成绩的。
后来交流后才知道还有更骚的操作:不光删除最后一台没有满的物理机,还可以用较小的虚拟机去填补之前没有放满的物理机。
当然这些操作的存在都是基于预测不太准确的前提。
第一:官方给的线下训练集和线上的差距太大了,对线下的模型选择屁用没有,基本要靠初赛前期每天100次的尝试机会到线上去过拟合!
第二:初、中、高三个等级的测试用例没有明显的区分,靠着前期对初级和中级用例的过拟合,高级用例也可以得到一个相同水平的分数,个人感觉检测不了模型的泛化性能。
好了,总结就到这里了。这几天过的比较惨,找实习各种悲剧,到现在一个offer都没拿到,比赛也进不了复赛,真是一件事情都没做好。祝各位复赛玩得开心!