匈牙利算法原理与Python实现

匈牙利算法原理与Python实现

今天学习一个新的算法-匈牙利算法,用于聚类结果分析,先用图表示我当前遇到的问题:
请添加图片描述
这两列值是我用不同算法得到的聚类结果,从肉眼可以看出第一列聚类为0的结果在第二列中对应的值为1.

为了使得两聚类结果可比,我需要将第二列的聚类结果做一次数据映射。当然这只是一个非常简单的情况,如果出现3类及以上就会变得复杂。

那么匈牙利的算法原理是啥?

如何利用python实现,下面我将简略翻译这篇文章,如果有词不达意的地方,请批评指正~ Hungarian Algorithm Introduction & Python Implementation | by Eason | Python in Plain English

请添加图片描述

这位大佬真的写的很详细!谢谢 Eason 的分享!

正文

在这篇文章中,我将介绍如何使用匈牙利方法来解决线性赋值问题,并提供我个人的Python代码解决方案。

那么…什么是线性分配问题?

线性分配问题表示需要在有限的资源下使可用资源最大化(或支出最小化)。例如,下面是一个二维矩阵,每一行代表一个不同的供应商,每一列代表雇用他们生产一个特定产品的成本。每个供应商只能专门生产其中一种产品。换句话说,矩阵中的每一列和每一行只能选择一个元素,而且所选元素的总和必须是最小的(成本支出最小)。

请添加图片描述

这是一个简单的例子。通过尝试可能的组合,我们可以看到最小的总和是13,所以供应商A供应 Bubble tea,供应商B供应milk tea,而供应商C供应 fruit tea。然而,这样的尝试并不遵循明确的规则,在应用于大型任务时就会变得低效。

因此,下一节将逐步介绍匈牙利算法,该算法可应用于线性分配问题。

匈牙利算法和Python代码的步骤

在本节中,我们将展示如何使用匈牙利算法来解决线性赋值问题并找到矩阵中的最小组合。当然,匈牙利算法也可以用来寻找最大组合。

import numpy as np

cost_matrix = np.random.randint(10,size=(5, 5))
print(f"The cost matrix is:\n", cost_matrix)

如果我们想找到最大的总和,我们可以反过来做。

要解决的矩阵被看作是利润矩阵,矩阵中的最大值被设定为所有商品的共同价格。

成本矩阵是由利润矩阵减去最大值得到的。

最后,将成本矩阵代入匈牙利算法,得到最小化的组合。

然后重新映射回利润矩阵,得到最大的总和值和组合结果。

import numpy as np

# The matrix who you want to find the maximum sum
profit_matrix = np.random.randint(10,size=(5, 5))

# Using the maximum value of the profit_matrix to get the corresponding cost_matrix
max_value = np.max(profit_matrix)
#Using the cost matrix to find which positions are the answer
cost_matrix = max_value - profit_matrix

print(f"The profit matrix is:\n", profit_matrix, f"\nThe cost matrix is:\n", cost_matrix)

按照上面的步骤,你可以随机生成成本矩阵或利润矩阵。

接下来,我们将进入匈牙利算法的介绍,为了便于说明,下面的章节将使用下图所示的成本矩阵进行说明。我们将使用匈牙利算法来解决成本矩阵的线性分配问题,并找到相应的最小和。

请添加图片描述

步骤1:每一列和每一行减去其内部最小值

首先,每一列和每一行必须减去其内部最小值。减去最小值后,成本矩阵将看起来像这样。
请添加图片描述
当前实现的代码:

import numpy as np

def hungarian_algorithm(mat): 
    dim = mat.shape[0]
    cur_mat = mat

    #Step 1 - Every column and every row subtract its internal minimum
    for row_num in range(mat.shape[0]): 
        cur_mat[row_num] = cur_mat[row_num] - np.min(cur_mat[row_num])
    
    for col_num in range(mat.shape[1]): 
        cur_mat[:,col_num] = cur_mat[:,col_num] - np.min(cur_mat[:,col_num])


def main():

    #The matrix who you want to find the maximum sum
    cost_matrix = np.array([[7, 6, 2, 9, 2],
                        [6, 2, 1, 3, 9],
                        [5, 6, 8, 9, 5],
                        [6, 8, 5, 8, 6],
                        [9, 5, 6, 4, 7]])
    
    ans_pos = hungarian_algorithm(cost_matrix.copy())

if __name__ == '__main__':
    main()

步骤2-1: Min_zero_row函数的实现

首先,我们需要找到零元素最少的那一行。因此,我们可以将前面的矩阵转换成布尔矩阵(0→真,其他→假)。

import numpy as np

#Transform the matrix to boolean matrix(0 = True, others = False)
zero_bool_mat = (cur_mat == 0)

请添加图片描述

因此,我们可以使用 "min_zero_row "函数来找到相应的行。

# zero_mat = boolean, mark_zero = blank list
def min_zero_row(zero_mat, mark_zero):
    
    '''
    The function can be splitted into two steps:
    #1 The function is used to find the row which containing the fewest 0.
    #2 Select the zero number on the row, and then marked the element corresponding row and column as False
    '''

    #Find the row
    min_row = [99999, -1]

    for row_num in range(zero_mat.shape[0]): 
        if np.sum(zero_mat[row_num] == True) > 0 and min_row[0] > np.sum(zero_mat[row_num] == True):
            min_row = [np.sum(zero_mat[row_num] == True), row_num]

请添加图片描述

第三,标记相应行上的任何0元素,并整理其行和列(将布尔矩阵上的元素转换为False)。该元素的坐标被存储在mark_zero中。

# zero_mat = boolean matrix, mark_zero = blank list
def min_zero_row(zero_mat, mark_zero):
    
    '''
    The function can be splitted into two steps:
    #1 The function is used to find the row which containing the fewest 0.
    #2 Select the zero number on the row, and then marked the element corresponding row and column as False
    '''

    #Find the row
    min_row = [99999, -1]

    for row_num in range(zero_mat.shape[0]): 
        if np.sum(zero_mat[row_num] == True) > 0 and min_row[0] > np.sum(zero_mat[row_num] == True):
            min_row = [np.sum(zero_mat[row_num] == True), row_num]

    # Marked the specific row and column as False
    zero_index = np.where(zero_mat[min_row[1]] == True)[0][0]
    mark_zero.append((min_row[1], zero_index))
    zero_mat[min_row[1], :] = False
    zero_mat[:, zero_index] = False

因此,布尔矩阵将看起来像这样:

请添加图片描述

这个过程要重复几次,直到布尔矩阵中的元素都是假的。下图显示了它们被标记的顺序。

请添加图片描述

步骤2-2. Mark_matrix函数的实现

从步骤2-1得到Zero_mat后,我们可以根据一定的规则检查并标记矩阵。整个规则可以分解成几个步骤。

  1. 标记不包含被标记的0元素的行,并在non_marked_row中存储行索引;
  2. 搜索non_marked_row元素,并找出相应列中是否有未标记的0元素;
  3. 将列索引存储在marked_cols中;
  4. 比较存储在marked_zero和marked_cols中的列索引;
  5. 如果存在一个匹配的列索引,那么相应的行索引就会被保存到non_marked_rows中;
  6. 接下来,不在non_marked_row中的行索引被保存在marked_rows中;

最后,整个mark_matrx函数结束,然后返回mark_zero, marked_rows, marked_cols。这时,我们就可以根据返回的信息来决定结果。

def mark_matrix(mat):

    '''
    Finding the returning possible solutions for LAP problem.
    '''

    #Transform the matrix to boolean matrix(0 = True, others = False)
    cur_mat = mat
    zero_bool_mat = (cur_mat == 0)
    zero_bool_mat_copy = zero_bool_mat.copy()

    #Recording possible answer positions by marked_zero
    marked_zero = []
    while (True in zero_bool_mat_copy):
        min_zero_row(zero_bool_mat_copy, marked_zero)

    #Recording the row and column indexes seperately.
    marked_zero_row = []
    marked_zero_col = []
    for i in range(len(marked_zero)):
        marked_zero_row.append(marked_zero[i][0])
        marked_zero_col.append(marked_zero[i][1])
    #step 2-2-1
    non_marked_row = list(set(range(cur_mat.shape[0])) - set(marked_zero_row))
    
    marked_cols = []
    check_switch = True
    while check_switch:
        check_switch = False
        for i in range(len(non_marked_row)):
            row_array = zero_bool_mat[non_marked_row[i], :]
            for j in range(row_array.shape[0]):
                #step 2-2-2
                if row_array[j] == True and j not in marked_cols:
                    #step 2-2-3
                    marked_cols.append(j)
                    check_switch = True

        for row_num, col_num in marked_zero:
            #step 2-2-4
            if row_num not in non_marked_row and col_num in marked_cols:
                #step 2-2-5
                non_marked_row.append(row_num)
                check_switch = True
    #step 2-2-6
    marked_rows = list(set(range(mat.shape[0])) - set(non_marked_row))
    
    return(marked_zero, marked_rows, marked_cols)

如果我们使用示例成本矩阵,相应的marked_zero、marked_rows和marked_cols如下。

marked_zero: [(3, 2), (0, 4), (1, 1), (2, 0), (4, 3)]
marked_rows:[0, 1, 2, 3, 4]
marked_cols: []

第3步:识别结果

在这一步,如果mark_rows和mark_cols的长度之和等于成本矩阵的长度,就意味着线性赋值问题的解决方案已经被成功找到,mark_zero存储了解决方案的坐标。幸运的是,在示例矩阵中,我们第一次就找到了答案。因此,我们可以跳到第5步,计算出解。

然而,一切都不是一帆风顺的。大多数情况下,我们不会在第一次尝试时就找到答案,比如下面这个矩阵。
请添加图片描述

经过步骤1和2,相应的矩阵、标记的行和标记的列如下:

请添加图片描述

很明显,长度之和小于矩阵的长度。这时,我们需要进入第4步来调整矩阵。

def hungarian_algorithm(mat): 
    dim = mat.shape[0]
    cur_mat = mat

    #Step 1 - Every column and every row subtract its internal minimum
    for row_num in range(mat.shape[0]): 
        cur_mat[row_num] = cur_mat[row_num] - np.min(cur_mat[row_num])
    
    for col_num in range(mat.shape[1]): 
        cur_mat[:,col_num] = cur_mat[:,col_num] - np.min(cur_mat[:,col_num])

    zero_count = 0
    while zero_count < dim:
        #Step 2 & 3
        ans_pos, marked_rows, marked_cols = mark_matrix(cur_mat)
        zero_count = len(marked_rows) + len(marked_cols)

        if zero_count < dim:
            cur_mat = adjust_matrix(cur_mat, marked_rows, marked_cols)
    
	return ans_pos

第4步:调整矩阵

在步骤4中,我们要把步骤1之后的矩阵放入Adjust_Matrix函数中。以步骤3中的后一个矩阵为例,Adjust_Matrix中要修改的矩阵是:

整个函数可以分成三个步骤。

为不在标记的行和不在标记的列中的元素找到最小值。因此,我们可以发现最小值是1。

请添加图片描述

def adjust_matrix(mat, cover_rows, cover_cols):
    cur_mat = mat
    non_zero_element = []

    #Step 4-1 Find the minimum value
    for row in range(len(cur_mat)):
        if row not in cover_rows:
            for i in range(len(cur_mat[row])):
                if i not in cover_cols:
                    non_zero_element.append(cur_mat[row][i])
    
    min_num = min(non_zero_element)
  1. 从上一步得到的最小值中减去不在标记的行和标记的列中的元素。

请添加图片描述

def adjust_matrix(mat, cover_rows, cover_cols):
    cur_mat = mat
    non_zero_element = []

    #Step 4-1
    for row in range(len(cur_mat)):
        if row not in cover_rows:
            for i in range(len(cur_mat[row])):
                if i not in cover_cols:
                    non_zero_element.append(cur_mat[row][i])
    
    min_num = min(non_zero_element)

    #Step4-2
    for row in range(len(cur_mat)):
        if row not in cover_rows:
            for i in range(len(cur_mat[row])):
                if i not in cover_cols:
                    cur_mat[row, i] = cur_mat[row, i] - min_num
  1. 将marked_rows中的元素,也就是marked_cols中的元素,加到步骤4-1得到的最小值。

请添加图片描述

def adjust_matrix(mat, cover_rows, cover_cols):
    cur_mat = mat
    non_zero_element = []
    
    #Step 4-1
    for row in range(len(cur_mat)):
        if row not in cover_rows:
            for i in range(len(cur_mat[row])):
                if i not in cover_cols:
                    non_zero_element.append(cur_mat[row][i])
    
    min_num = min(non_zero_element)

    #Step 4-2
    for row in range(len(cur_mat)):
        if row not in cover_rows:
            for i in range(len(cur_mat[row])):
                if i not in cover_cols:
                    cur_mat[row, i] = cur_mat[row, i] - min_num
    #Step 4-3
    for row in range(len(cover_rows)):  
        for col in range(len(cover_cols)):
            cur_mat[cover_rows[row], cover_cols[col]] = cur_mat[cover_rows[row], cover_cols[col]] + min_num

    return cur_mat

返回调整后的矩阵,重复步骤2和步骤3,直到条件满足进入步骤5的要求。

第5步:计算答案

使用存储在mark_zero中的元素组成,可以计算出线性赋值问题的最小值和最大值。
请添加图片描述

Answer_Calculator函数的代码如下:

def ans_calculation(mat, pos):
    total = 0
    ans_mat = np.zeros((mat.shape[0], mat.shape[1]))
    for i in range(len(pos)):
        total += mat[pos[i][0], pos[i][1]]
        ans_mat[pos[i][0], pos[i][1]] = mat[pos[i][0], pos[i][1]]
    return total, ans_mat

结论

完整的代码如下,如果你有任何问题,请让我知道~

import numpy as np

def min_zero_row(zero_mat, mark_zero):
	
	'''
	The function can be splitted into two steps:
	#1 The function is used to find the row which containing the fewest 0.
	#2 Select the zero number on the row, and then marked the element corresponding row and column as False
	'''

	#Find the row
	min_row = [99999, -1]

	for row_num in range(zero_mat.shape[0]): 
		if np.sum(zero_mat[row_num] == True) > 0 and min_row[0] > np.sum(zero_mat[row_num] == True):
			min_row = [np.sum(zero_mat[row_num] == True), row_num]

	# Marked the specific row and column as False
	zero_index = np.where(zero_mat[min_row[1]] == True)[0][0]
	mark_zero.append((min_row[1], zero_index))
	zero_mat[min_row[1], :] = False
	zero_mat[:, zero_index] = False

def mark_matrix(mat):

	'''
	Finding the returning possible solutions for LAP problem.
	'''

	#Transform the matrix to boolean matrix(0 = True, others = False)
	cur_mat = mat
	zero_bool_mat = (cur_mat == 0)
	zero_bool_mat_copy = zero_bool_mat.copy()

	#Recording possible answer positions by marked_zero
	marked_zero = []
	while (True in zero_bool_mat_copy):
		min_zero_row(zero_bool_mat_copy, marked_zero)
	
	#Recording the row and column positions seperately.
	marked_zero_row = []
	marked_zero_col = []
	for i in range(len(marked_zero)):
		marked_zero_row.append(marked_zero[i][0])
		marked_zero_col.append(marked_zero[i][1])

	#Step 2-2-1
	non_marked_row = list(set(range(cur_mat.shape[0])) - set(marked_zero_row))
	
	marked_cols = []
	check_switch = True
	while check_switch:
		check_switch = False
		for i in range(len(non_marked_row)):
			row_array = zero_bool_mat[non_marked_row[i], :]
			for j in range(row_array.shape[0]):
				#Step 2-2-2
				if row_array[j] == True and j not in marked_cols:
					#Step 2-2-3
					marked_cols.append(j)
					check_switch = True

		for row_num, col_num in marked_zero:
			#Step 2-2-4
			if row_num not in non_marked_row and col_num in marked_cols:
				#Step 2-2-5
				non_marked_row.append(row_num)
				check_switch = True
	#Step 2-2-6
	marked_rows = list(set(range(mat.shape[0])) - set(non_marked_row))

	return(marked_zero, marked_rows, marked_cols)

def adjust_matrix(mat, cover_rows, cover_cols):
	cur_mat = mat
	non_zero_element = []

	#Step 4-1
	for row in range(len(cur_mat)):
		if row not in cover_rows:
			for i in range(len(cur_mat[row])):
				if i not in cover_cols:
					non_zero_element.append(cur_mat[row][i])
	min_num = min(non_zero_element)

	#Step 4-2
	for row in range(len(cur_mat)):
		if row not in cover_rows:
			for i in range(len(cur_mat[row])):
				if i not in cover_cols:
					cur_mat[row, i] = cur_mat[row, i] - min_num
	#Step 4-3
	for row in range(len(cover_rows)):  
		for col in range(len(cover_cols)):
			cur_mat[cover_rows[row], cover_cols[col]] = cur_mat[cover_rows[row], cover_cols[col]] + min_num
	return cur_mat

def hungarian_algorithm(mat): 
	dim = mat.shape[0]
	cur_mat = mat

	#Step 1 - Every column and every row subtract its internal minimum
	for row_num in range(mat.shape[0]): 
		cur_mat[row_num] = cur_mat[row_num] - np.min(cur_mat[row_num])
	
	for col_num in range(mat.shape[1]): 
		cur_mat[:,col_num] = cur_mat[:,col_num] - np.min(cur_mat[:,col_num])
	zero_count = 0
	while zero_count < dim:
		#Step 2 & 3
		ans_pos, marked_rows, marked_cols = mark_matrix(cur_mat)
		zero_count = len(marked_rows) + len(marked_cols)

		if zero_count < dim:
			cur_mat = adjust_matrix(cur_mat, marked_rows, marked_cols)

	return ans_pos

def ans_calculation(mat, pos):
	total = 0
	ans_mat = np.zeros((mat.shape[0], mat.shape[1]))
	for i in range(len(pos)):
		total += mat[pos[i][0], pos[i][1]]
		ans_mat[pos[i][0], pos[i][1]] = mat[pos[i][0], pos[i][1]]
	return total, ans_mat

def main():

	'''Hungarian Algorithm: 
	Finding the minimum value in linear assignment problem.
	Therefore, we can find the minimum value set in net matrix 
	by using Hungarian Algorithm. In other words, the maximum value
	and elements set in cost matrix are available.'''

	#The matrix who you want to find the minimum sum
	cost_matrix = np.array([[7, 6, 2, 9, 2],
				[6, 2, 1, 3, 9],
				[5, 6, 8, 9, 5],
				[6, 8, 5, 8, 6],
				[9, 5, 6, 4, 7]])
	ans_pos = hungarian_algorithm(cost_matrix.copy())#Get the element position.
	ans, ans_mat = ans_calculation(cost_matrix, ans_pos)#Get the minimum or maximum value and corresponding matrix.

	#Show the result
	print(f"Linear Assignment problem result: {ans:.0f}\n{ans_mat}")

	#If you want to find the maximum value, using the code as follows: 
	#Using maximum value in the cost_matrix and cost_matrix to get net_matrix
	profit_matrix = np.array([[7, 6, 2, 9, 2],
				[6, 2, 1, 3, 9],
				[5, 6, 8, 9, 5],
				[6, 8, 5, 8, 6],
				[9, 5, 6, 4, 7]])
	max_value = np.max(profit_matrix)
	cost_matrix = max_value - profit_matrix
	ans_pos = hungarian_algorithm(cost_matrix.copy())#Get the element position.
	ans, ans_mat = ans_calculation(profit_matrix, ans_pos)#Get the minimum or maximum value and corresponding matrix.
	#Show the result
	print(f"Linear Assignment problem result: {ans:.0f}\n{ans_mat}")

if __name__ == '__main__':
	main()

翻译原文:Hungarian Algorithm Introduction & Python Implementation | by Eason | Python in Plain English

再次感谢 Eason!

后续我将出一期在聚类评估中如何使用匈牙利函数对聚类结果进行精确度分析。

整理不易,欢迎点赞关注支持~

  • 7
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值