在利用K-Means对句子向量进行聚类的时候,发现了两种容易疏忽的错误。
关于代码的实现请参考 用K-Means对未标注的数据聚类 。
Error 1:一直循环无最优解
首先,在聚类的过程中,我发现整整迭代一个小时都没有结果。刚开始我以为是数据比较多,一共有 6000 多条数据,每个数据是 100 维的向量。后来,我减少数据量到10条,发现还是一直迭代,我渐渐开始慌了。
要是不按下暂停键,它就一直循环到地老天荒。我开始寻找原因。
刚开始我认为是判断循环的条件出了问题。在刚开始,需要创建一个标志变量clusterChanged
,若为真,则继续迭代。
# 数据聚类
def kMeans(dataSet, k):
m = np.shape(dataSet)[0] # 一共有m行数据
clusterAssment = np.mat(np.zeros((m, 2))) # 生成m*2的0矩阵,第一列存放该点所在簇的索引,第二列存放该点与簇质心的距离
centroids = randCent(dataSet, k) # 调用函数,构建质心矩阵
clusterChanged = True # 创建一个标志变量,若为真,则继续迭代
count = 0
while clusterChanged:
clusterChanged = False
# 寻找最近的质心
for i in range(m): # 对于每一行的数据
minIndex = -1 # 在未迭代之前,将簇的索引设为-1
maxDist = -np.inf # 在未迭代之前,将最大距离设为负无穷大
for j in range(k): # 对于当前每一个质心
distJI = distCal(centroids[j, :], dataSet[i, :]) # 计算第i个数据与第j个质心之间的距离
if distJI > maxDist:
minIndex = j # 若i与j之间距离小于目前的最小值,则更新当前点的索引为j
maxDist = distJI # 若i与j之间距离大于目前的最大值,则更新当前最大值
if clusterAssment[i, 0] != minIndex: # 如果第i行的第一个值(簇的索引)不为 minIndex,说明簇发生了改变
clusterChanged = True # 继续迭代
clusterAssment[i, :] = int(minIndex), maxDist**2
#print(centroids)
# 更新质心的位置
for cent in range(k):
category = np.nonzero(clusterAssment[:, 0].A == cent) # 得到簇索引为cent的值的位置
ptsInClust = dataSet[category[0]]
if len(ptsInClust) != 0:
centroids[cent, :] = np.mean(ptsInClust, axis=0) # axis=0表示沿着矩阵列的方向进行均值计算
#print(centroids)
#print(centroids)
return centroids, clusterAssment
但是反反复复看,逻辑应该是没有问题的。什么时候还需要继续迭代?如果第
行的第一个值(簇的索引)不为minIndex
,说明簇发生了改变,这个点已经被重新划分到了与它距离更近的一个簇。
换个思路。我把每一次迭代前后的簇和簇中心打印了出来。发现一个很有意思的现象。
先解释一下这个矩阵,一共有十行、两列,第一列存放距离它最近的簇的索引,第二列存放距离的平方。
|
|
|
|
我们发现,第三次迭代前后和第四次迭代前后的簇的索引,竟然是0-1
转换!之前索引为0
所有的点,经过转换变为1
;索引为1
所有的点,经过转换变为0
。这是什么???我将之命名为 薛定谔的最优解,又称 “ 李氏死循环 ” 。
我们再打印一下簇中心,发现不管是经过几百次、几千次的迭代,簇中心的值是一直没变的。只不过在来回颠倒,一会这个簇索引为 1 ,一会为 0 ,就像在震荡一样。
|
|
|
|
簇中心的位置没有变,索引却在来回交替变换,我左思右想,整整一天,搞不明白(留下了没有技术和头脑的泪水)。明明已经找到了最优解,一个点找到了最近的簇,下一次为什么又将它分到了另外的簇?
今天早上,看着迭代后第二列的值,我突然灵机一动。距离!应该是距离的问题!
我们理一下思路 :对于 正常 的点来说,两个点越近,距离越小(废话)。
因此,K-Means在确定簇的个数 之后,对于每一个点,要比较它于 个簇中心的距离,将其归到 距离最小 的那一个簇。
但是在 句子向量 计算的时候,情况发生了改变!!!
对于 句子向量,我们用 余弦 表示距离,它在[-1, 1]
之间,句子越相似,余弦值 越大!两个点越近,计算出的距离越远!
如果你选择距离最小的簇,意味着你将这个点归到了一个完全不相似的簇中,因此下一次,它又会反过来,就这样,子子孙孙无穷尽也。
破案了兄弟们!所以,给定
,每一个句子计算与
个簇中心的距离,应该将其归到 距离较大 的那个簇。因此,将初始的最大距离设为负无穷大, <
改为 >
即可。
for i in range(m): # 对于每一行的数据
minIndex = -1 # 在未迭代之前,将簇的索引设为-1
maxDist = -np.inf # 在未迭代之前,将最大距离设为负无穷大
for j in range(k): # 对于当前每一个质心
distJI = distCal(centroids[j, :], dataSet[i, :]) # 计算第i个数据与第j个质心之间的距离
if distJI > maxDist:
minIndex = j # 若i与j之间距离小于目前的最小值,则更新当前点的索引为j
maxDist = distJI # 若i与j之间距离大于目前的最大值,则更新当前最大值
同样,如果是用二分法进行计算,也要更改判断语句哦!应该选择使得SSE
值 最大 的簇进行二分,而不是最小了!
最后,经过两次迭代,求出了最终解。用此图,祭奠飘落的头发和后移的发际线。
Error 2:RuntimeWarning: Mean of empty slice
运行的时候,还发现了一个错误:RuntimeWarning: Mean of empty slice.
出现了空白的平均值。
我们看看这一段的代码。它是在每一个点找到最近的簇中心后,对于每一个簇进行平均,求出新的簇中心。当前给定了
个簇,对于每一个簇,找到对应的点,然后求平均。
# 更新质心的位置
for cent in range(k):
category = np.nonzero(clusterAssment[:, 0].A == cent) # 得到簇索引为cent的值的位置
ptsInClust = dataSet[category[0]]
centroids[cent, :] = np.mean(ptsInClust, axis=0) # axis=0表示沿着矩阵列的方向进行均值计算
出现问题的原因是某一个簇ptsInClust
可能为空,即没有点在其中。可能是k值选择的过大,因此可以适当减少k值,也可以加一个判断语句。修改后代码如下,就不会产生这样的错误。
# 更新质心的位置
for cent in range(k):
category = np.nonzero(clusterAssment[:, 0].A == cent) # 得到簇索引为cent的值的位置
ptsInClust = dataSet[category[0]]
if len(ptsInClust) != 0:
centroids[cent, :] = np.mean(ptsInClust, axis=0) # axis=0表示沿着矩阵列的方向进行均值计算
最后,我想说,下面这句话,胜过了世界上的千言万语。比起不痛不痒的“我爱你”、“我们在一起吧”,这句话才是照亮前行之路的灯塔。万般皆下品,惟有读书高,祝你万事胜意,遍地飘零。
转载:https://blog.csdn.net/qq_42491242/article/details/105527224