机器学习之集体智慧编程笔记4-优化
本章主要介绍通过随机优化来解决协作类问题,主要内容:
- 随机搜索
- 爬山法
- 退火算法
- 遗传算法
优化算法是尝试许多不同的题解,并通过给这些题解打分来找到最优题解的一种解决方案。优化算法的应用场景是存在大量题解而我们无法一一尝试的情况。最简单也是效率最低的解决方法是随机猜测大量题解并从中找出最优解。而更有效的方法是通过有改进的方式对随机数据进行不断修正,得到最优解。
组团旅游问题
我们通过给处在全国各地的一家人安排旅游来学习一些优化算法
#家庭成员以及所处位置
people = [
('王帅', '北京'),
('赵美丽', '杭州'),
('王大', '上海'),
('王小二', '长沙'),
('王三', '西安'),
('王四', '郑州'),
]
#目的地
dest = '大理'
读取模拟的航班信息
# 把当天时间转化为分钟数,便于计算
def get_minute(time):
time_detail = strptime(time, '%H:%M')
return time_detail[3] * 60 + time_detail[4]
def read_flights():
# 存储始发地-目的地 的信息
flights = {}
with open('schedule.txt', 'r') as f:
for line in f:
# 获取始发地,目的地,出发时间,到达时间,价格
origin, dest, depart_time, arrive_time, price = line.replace('\n', '').split(',')
flights.setdefault((origin, dest), [])
flights[(origin, dest)].append((get_minute(depart_time), get_minute(arrive_time), int(price)))
return flights
现在我们知道了所有家庭成员在的位置,以及到目的地的航班信息,怎么让他们在旅游中花费最小就是我们的目的
首先我们要分析一下对于旅行者来说花费主要体现在那些方面:
- 来回的路费
- 到了目的地之后等待他人的时间
- 返回所在地是等待飞机时间
- 飞机飞行过程耗时
- 超时多支出一天车费
- …
再分析了消耗之后我们可以得到总的花费函数:
# 飞行:0.1元/分钟
# 等待:0.5元/分钟
# 8点前出发:0.3元/分钟
# 超时支付多余车费80元
def schedule_cost(schedule):
total_cost = 0.0
# 记录最后一个到的和第一个走的
# 这两个决定了其他人等待的时间和是否需要多付一天车钱
to_last_arrive, back_first_leave = 0, 0
# 计算每个人的车费和飞机消耗费
for i in range(int(len(schedule) / 2)):
origin = people[i][1]
try:
to = flights[(origin, dest)][schedule[i]]
back = flights[(dest, origin)][schedule[2 * i + 1]]
except BaseException as e:
print(schedule)
to_time_cost = to[1] - to[0]
back_time_cost = back[1] - back[0]
# 计算所有人的路费和飞机上乘车费用
total_cost += to[2] + back[2] + (to_time_cost + back_time_cost) * 0.1
# 计算最晚到的和最早走的时间
if to[1] > to_last_arrive:
to_last_arrive = to[1]
if back[0] > back_first_leave:
back_first_leave = back[0]
m_eight = get_minute('08:00')
# 计算每个人的等待费和早起费
for i in range(int(len(schedule) / 2)):
# 出发时间
depart_time = flights[(origin, dest)][schedule[i]][0]
# 到达时间
arrive_time = flights[(origin, dest)][schedule[i]][1]
# 离开时间
leave_time = flights[(dest, origin)][schedule[2 * i + 1]][0]
# 最晚到的减其他人的时间得到其他人浪费的时间
# 其他人走的时间减去最早走的得到其他人浪费的时间
total_cost += (to_last_arrive - arrive_time + leave_time - back_first_leave) * 0.5
# 如果出发时间早于8点,增加早起费用
if depart_time < m_eight:
total_cost += (m_eight - depart_time) * 0.3
# 第一个早于8点走,其他人都要早起
if back_first_leave < m_eight:
total_cost += (m_eight - back_first_leave) * 0.3
# 如果最后到的时间比走的时间还早,说明要多租一天车
if to_last_arrive > back_first_leave:
total_cost += 80
#取小数点后两位
return round(total_cost, 2)
schedule参数是每个成员对应的航班班次组成的数组,例如[0,0,0,0,0,0,0,0,0,0,0,0]
有了消耗函数之后我们可以模拟一些数据,然后通过计算找到最小的消耗最为可行方案提供给这家人,下面就介绍一些可行的算法来找寻最优方案:
随机搜索
随机搜索并不是一个很好的优化算法,但是通过这个算法我们可以先了解一下优化算法的真正目的,并且这个算法也代表了优化算法的最低要求
我们通过随机出大量的解决方案,然后用花费函数计算消耗,找到最小的消耗即可
下面的函数就是实现方法,其中domain参数代表了每个人航班的取值范围
# time:随机次数,domain:各位置取值范围,fn_cost:消耗计算方法
def random_optimization(time, domain, fn_cost=schedule_cost):
best_cost = 99999
best_r = None
for i in range(time):
# 随机一个解并分析是否更优秀
r = [random.randint(item[0], item[1]) for item in domain]
cost = fn_cost(r)
# 消耗更小则替换
if cost < best_cost:
best_cost = cost
best_r = r
return best_cost, best_r
爬山法
随机搜索的方法是非常低效的,因为存在大量题解,我们能模拟出来的只是其中非常小的一部分。我们可以通过先模拟一个题解,然后在这个题解周围找到更好的题解的方法来代替随机搜索。这就像是爬山,我们站在山腰,四处找更低的点然后走,直到向四处走都没有更低点就找到我们要的结果
# 爬山法
def hillclimb(domain, fn_cost=schedule_cost):
# 随机一个解
sol = [random.randint(item[0], item[1]) for item in domain]
best_cost = 99999
while True:
# 记录周围解
neigbors = []
# 对于所有人的解,都进行修改
for i in range(len(domain)):
if sol[i] > domain[i][0]:
neigbors.append(sol[0:i] + [sol[i] - 1] + sol[i + 1:])
if sol[i] < domain[i][1]:
neigbors.append(sol[0:i] + [sol[i] + 1] + sol[i + 1:])
for item in neigbors:
cost = fn_cost(item)
if cost < best_cost:
best_cost = cost
sol = item
# 最优解是上次的解,说明已经到了最低点
if sol not in neigbors:
return best_cost, sol
虽然爬山法在随机搜索的基础上做了一定的优化,但是很容易陷入局部最优解,我们可以通过多个随机值求最优解再在最优解中找到最优解的方法来避免局部最优解的问题,另外一种解决方法就是模拟退火算法
退火算法
退火算法以一个随机解开始,每一次迭代时会选取随机解中的某个位置进行改变,再比较变化前后的消耗。和爬山法不同,退火算法会有一个代表当前温度的值,这个值在不断变小,当温度较高时,算法扔有可能会接受较差的解,但是随着算法的进行,越来越倾向于选择更优解而不是更差解
# 退火法
# T代表温度,cool代表退火率,step代表改变范围
def backfire(domain, fn_cost=schedule_cost, T=10000, cool=0.95, step=3):
# 随机解
sol = [random.randint(item[0], item[1]) for item in domain]
while T > 0.1:
# 随机出来一个要改变的位置
i = random.randint(0, len(domain) - 1)
# 随机一个要改变的数
change = random.randint(-step, step)
sol_copy = sol[:]
sol_copy[i] += change
# 保证数据没有越界
if sol_copy[i] < domain[i][0]:
sol_copy[i] = domain[i][0]
if sol_copy[i] > domain[i][1]:
sol_copy[i] = domain[i][1]
c = fn_cost(sol)
cc = fn_cost(sol_copy)
# 如果是更优解或者现在的随机概率在范围内
# 随着计算的进行,此范围会越来越小,越来越倾向于接收更优解
if cc < c or random.random() < pow(math.e, -(cc - c) / T):
sol = sol_copy[:]
T *= cool
return fn_cost(sol), sol
遗传算法
遗传算法的运行过程是先随机生成一组解所谓初始族群,族群中每次迭代都会选取更优秀的一部分,然后这部分优秀种进行变异或者交叉来补充族群到最大,并重复这一过程
变异示例:
[7,5,3,2,5,3,0,1,1,5,3,6] ->[7,5,3,2,5,3,5,1,1,5,3,6]
交叉示例:
[7,5,3,2,5,3,**交叉位置**0,1,1,5,3,6]
[3,8,2,6,5,4,**交叉位置**1,2,3,5,4,6]
->[7,5,3,2,5,3,1,2,3,5,4,6]
# domain:取值范围
# fn_cost:求解方法
# popsize:数据量
# variation:变异概率
# elite:遗传率
# maxiter:遗传多少代
def genetic(domain, fn_cost=schedule_cost, popsize=50, variation=0.2, elite=0.3, maxiter=100):
# 变异
def mutate(r):
i = random.randint(0, len(domain) - 1)
# 两个方向的变异概率都是50%
if random.random() < 0.5:
res = r[0:i] + [r[i] - 1] + r[i + 1:]
else:
res = r[0:i] + [r[i] + 1] + r[i + 1:]
if res[i] < domain[i][0]:
res[i] = domain[i][0]
if res[i] > domain[i][1]:
res[i] = domain[i][1]
return res
# 交叉
def crossover(r1, r2, pops):
if r1 is None or r2 is None:
for line in pops:
print(str(line))
# 交叉必然发生
i = random.randint(1, len(domain) - 2)
return r1[0:i] + r2[i:]
# 计算能存活下来的数量
elite_count = int(popsize * elite)
# 随机出第一代
pops = [[random.randint(item[0], item[1]) for item in domain] for i in range(popsize)]
for i in range(maxiter):
# 计算消耗
pops_cost = [(fn_cost(item), item) for item in pops]
pops_cost.sort()
# 只取最优解遗传
pops = [item[1] for item in pops_cost[0:elite_count]]
# 补充族群至最大
while len(pops) < popsize:
# 随机进行变异和交叉
# 变异
if random.random() < variation:
pops.append(mutate(pops[random.randint(0, len(pops) - 1)]))
else:
# 随机选出两个解进行交叉
r1 = pops[random.randint(0, len(pops) - 1)]
r2 = pops[random.randint(0, len(pops) - 1)]
pops.append(crossover(r1, r2, pops))
pops_cost = [(fn_cost(item), item) for item in pops]
pops_cost.sort()
return pops_cost[0]
偏好优化-分配宿舍
假设现在有5间宿舍,每个宿舍可以睡两个学生,每个学生都有自己的首选和次选宿舍,我们怎么给学生安排宿舍才能最大可能满足学生的预期呢
# 模拟学生和首选次选宿舍
droms = [101, 102, 103, 104, 105]
prefs = [
('赵刚', (102, 103)),
('刘帅', (101, 105)),
('王网', (102, 103)),
('钱多', (102, 103)),
('周助', (103, 101)),
('李牧', (104, 105)),
('刘刚', (105, 102)),
('王帅', (102, 101)),
('胡呼', (102, 104)),
('赵行', (101, 104)),
]
模拟学生选择情况下的分数
def drom_cost(res):
cost = 0
slots = []
for i in droms:
slots += [i, i]
for i in range(len(res)):
# 首选+0,次选+1,非选择+5
if slots[res[i]] == prefs[i][1][0]:
cost += 0
elif slots[res[i]] == prefs[i][1][1]:
cost += 1
else:
cost += 5
del slots[res[i]]
return cost
打印结果
def print_solution(res):
slots = []
for i in droms:
slots += [i, i]
for i in range(len(res)):
print('%s=>%s' % (prefs[i][0], slots[res[i]]))
del slots[res[i]]
关联图
# 绘制关系网图
people = ['赵刚', '刘帅', '王网', '钱多', '周助', '李牧', '刘刚', '王帅', '胡呼', '赵行']
links = [
('赵刚', '王网'),
('王网', '王帅'),
('赵刚', '赵行'),
('赵刚', '王网'),
('刘帅', '刘刚'),
('李牧', '胡呼'),
('赵行', '钱多'),
('刘帅', '周助'),
('刘帅', '李牧'),
('王帅', '赵行'),
('钱多', '胡呼'),
]
# 计算交点个数
def link_node(res):
node = 0
points = {}
# 把名字和坐标用k-v形式对应起来
for i in range(len(people)):
points[people[i]] = (res[2 * i], res[2 * i + 1])
# 取连接两两对比,看有没有交点
for i in range(len(links)):
for j in range(i, len(links)):
(x1, y1), (x2, y2) = points[links[i][0]], points[links[i][1]]
(x3, y3), (x4, y4) = points[links[j][0]], points[links[j][1]]
# 判断是否平行
den = (x1 - x2) * (y3 - y4) - (x3 - x4) * (y1 - y2)
# 斜率相同,平行线
if den == 0:
continue
ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / den
ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / den
if ua > 0 and ua < 1 and ub > 0 and ub < 1:
node += 1
# 如果两个人距离太近,也会增加评分
for i in range(len(people)):
for j in range(i, len(people)):
(x1, y1), (x2, y2) = points[people[i]], points[people[j]]
# 计算距离
dist = math.sqrt(pow((x1 - x2), 2) + pow((y1 - y2), 2))
if dist < 50:
node += 1 - dist / 50
return node
def drawnet(res, filename='net.png'):
# 以k-v形式存储名字和坐标
poionts = dict([(people[i], (res[2 * i], res[2 * i + 1])) for i in range(len(people))])
# 把链接中的名字转化为坐标
point_links = [(poionts[link[0]], poionts[link[1]]) for link in links]
# 拿到画布
img = Image.new('RGB', (400, 400), (255, 255, 255))
# 拿到画笔
draw = ImageDraw.Draw(img)
# 划线
for link in point_links:
draw.line((link[0], link[1]), fill=(255, 0, 0))
for name in poionts:
# 为什么这里会报错???
draw.text(poionts[name], name, fill=(0, 0, 0))
img.save(filename)