学海无涯

WSL 下安装 docker 踩坑

起因

项目改微服务化了,于是开始研究 rabbitMQ 这个消息队列框架。然后官方推荐使用docker启动,索性在WSL下装 docker 了(太懒了,不想双系统or虚拟机)

安装

-*-FBI WARNING-*- 安装前请注意查阅自己的WSL是否为2.0版,不然请直接看踩坑 & 解决问题三

因为我的WSL下安装的是Ubuntu,参见docker官方安装文档

# 添加源
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

# 测试指纹是否存在
$ sudo apt-key fingerprint 0EBFCD88

pub   rsa4096 2017-02-22 [SCEA]
      9DC8 5822 9FC7 DD38 854A  E2D8 8D81 803C 0EBF CD88
uid           [ unknown] Docker Release (CE deb) <docker@docker.com>
sub   rsa4096 2017-02-22 [S]

# 设置使用稳定版仓库
$ sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) \
  stable"

# 更新源
$ sudo apt-get update

# 安装
$ sudo apt-get install docker-ce docker-ce-cli containerd.io

安装好后先别着急跑sudo docker run hello-world, WSL下有很多很多坑。

GRU门控结构

变种 LSTM —— GRU 原理

GRU 原理

门控循环单元(GRU)与 长短期记忆(LSTM)原理非常相似,同为使用门控机制控制输入、记忆等信息而在当前时间步做出预测。但比起 LSTM,GRU的门控逻辑有些许不同。

GRU 门控逻辑

因为与 LSTM 非常相似,这里就不赘述相同点,仅谈谈他们之间的不同点,想要详细了解,请移步LSTM原理及Keras中实现了解

与 LSTM 的三中门(输入门、遗忘门和输出门)和细胞状态不同,GRU 摆脱了细胞状态仅隐藏状态来传输信息,他只有两个门,一个复位门(reset gate)和一个更新门(update gate)

GRU门控结构

注:GRU 同样也有激活函数tanh(蓝)和Sigmoid(红)

更新门

更新门的作用类似于LSTM的遗忘门和输入门。它决定了要丢弃哪些信息以及要添加的新信息。

重置门

重置门是另一个门,它决定忘记过去的信息量。

GRU优势

因为 GRU 的一个细胞单元门结构少于 LSTM,所以计算量要小于 LSTM,使得他比 LSTM 更快。

GRU 在 Keras 中的实现

代码几乎与同 LSTM 相同,仅需导入 GRU 模型,即可建立与 LSTM 类似的模型结构,参数说明也几乎一致,不再赘述。

from keras.models import Sequential
from keras.layers.core import Dense, Activation, Dropout
from keras.layers.recurrent import GRU
model = Sequential()
model.add(GRU(units=100, input_shape=(30, 40), return_sequences=True))
model.add(Dropout(0.2))

model.add(GRU(units=50, return_sequences=False))
model.add(Dropout(0.2))

model.add(Dense(units=1))
model.add(Activation("linear"))
model.compile(loss="mse", optimizer="adam", metrics=['mae', 'mape'])
输入演示

LSTM原理及Keras中实现

LSTM 原理

LSTM(Long Short-Term Memory) 即长短期记忆,适合于处理和预测时间序列中间隔和延迟非常长的重要事件。其中的内部机制就是通过四个门调节信息流,了解序列中哪些数据需要保留或丢弃。

Long Short-Term Memory

通俗的原理

假设你在网上查看淘宝评论,以确定你是否想购买生活物品。你将首先阅读评论,然后确定是否有人认为它是好的或是否是坏的。

某麦片评论

当你阅读评论时,你的大脑下意识地只会记住重要的关键词。你会选择“惊人”和“完美均衡的早餐”这样的词汇。你不太关心“this”,“give”,“all”,“should”等。如果你的朋友第二天问你评论说什么,你不可能一字不漏地记住它。但你可能还记得主要观点,比如“肯定会再次购买”。其他的话就会从记忆中逐渐消失。

这基本上就是LSTM或GRU的作用。它可以学习只保留相关信息来进行预测,并忘记不相关的数据。在这种情况下,你记住的词让你判断它是好的。

核心概念

LSTM原理

LSTM 的核心概念是细胞状态,三个门和两个激活函数。细胞状态充当高速公路,在序列链中传递相关信息。门是不同的神经网络,决定在细胞状态上允许那些信息。有些门可以了解在训练期间保持或忘记那些信息。

激活函数 Tanh

Tanh squishes值介于-1和1之间

用于调节流经神经网络的值,限制在-1和1之间,防止梯度爆炸

没有tanh函数的变换

经过tanh函数的变换

激活函数 Sigmoid

Sigmoid squishes值介于0和1之间

与激活函数 Tanh不同,他是在0和1之间取值。这有助于更新或忘记数据,因为任何数字乘以0都是0,这会导致值小时或被"遗忘"。而任何数字乘1都是相同的值。网络可以通过这种方法了解那些数据不重要或那些数据重要。

遗忘门

遗忘门决定应该丢弃或保留那些信息。来自先前隐藏状态的信息和来自当前输入的信息通过sigmoid函数传递。值接近0和1之间,越接近0意味着忘记,越接近1意味着要保持。

遗忘门操作

输入门

输入门可以更新细胞状态,将先前的隐藏状态和当前输入分别传递sigmoid函数和tanh函数。然后将两个函数的输出相乘。

输入门

细胞状态

细胞状态逐点乘以遗忘向量(遗忘门操作得到),然后与输入门获得的输出进行逐点相加,将神经网络发现的新值更新为细胞状态。

细胞状态

输出门

输出门可以决定下一个隐藏状态应该是什么,并且可用于预测。首先将先前的隐藏状态和当前的输入传给sigmoid函数,然后将新修改的细胞状态传递给tanh函数,最后就结果相乘。输出的是隐藏状态,然后将新的细胞状态和新的隐藏状态移动到下一个时间序列中。

输出门

数学描述

从上述图解操作,我们可以轻松的理解LSTM的数学描述。

数学描述

  1. $c^t = z^f \odot c^{t-1} + z^i \odot z$

    其中$c^t$为当前细胞状态,$z^f$为遗忘门,$z^i$和$z$为输入门中两个操作。表示LSTM的遗忘阶段,对上一节点传进来的输入进行选择性忘记。

  2. $h^t = z^o \odot tanh (c^t)$

    其中$h^t$表示当前隐藏状态,$z^o$表示输出门中前一操作。表示LSTM的选择记忆阶段,对输入的$x^t$进行选择记忆。哪些重要则着重记录下来,哪些不重要,则少记一些。

  3. $y^t = \sigma (W^ \prime h^t)$

    表示LSTM的输出阶段,通过当前隐藏状态$h^t$一些变化得到。

西瓜书笔记-模型评估与选择

评估方法

将数据拆分为训练数据和验证数据,可以减小过拟合的可能性。但这样就必须拆分出和训练集数据分布几乎一致的验证数据。

留出法

通过分层采样对数据集D划分出样本集S和测试集T,$D=S \cup T,S \cap T=\varnothing$。例如,对D进行分层采样而获得70%样本的训练集S和含30%样本的训练集T,若D包含500个正例、500个反例,则分层抽样得到的S应该包含350个正例和350个反例,T包含150个正例和150个反例。

若有多种区分正例反例的划分方法,应当重复上述操作,进行多次划分、训练,最终实验评估结果取多次划分训练结果的平均。通常训练集和验证集的比例是2/3~4/5

分层抽样的具体程序是:把总体各单位分成两个或两个以上的相互独立的完全的组(如男性和女性),从两个或两个以上的组中进行简单随机抽样,样本相互独立。总体各单位按主要标志加以分组,分组的标志与关心的总体特征相关。例如,正在进行有关啤酒品牌知名度方面的调查,初步判别,在啤酒方面男性的知识与和女性的不同,那么性别应是划分层次的适当标准。

交叉验证法

现将数据集D划分为k个大小相似的互斥子集,即$D=D_1 \cup D_2 \cup D_3 \ldots \cup D_k, D_i \cap D_j= \varnothing (i \neq j)$每个子集都尽可能保持数据分布一致,同上即可对每个子集$D_i$进行分层抽样。看后用$k-1$个子集做训练集,余下的那一个做测试集。

从而进行k次训练、验证,最终返回测试结果的平均值。而k值取值很影响最终的结果。

自助法

网络延迟测试

Redis优化之内存碎片小踩坑

起因

近来项目里引用了Redis,本来用这高端玩意就是为了加速项目跑的速度。然而,用着用着越来越慢。而之前就做着性能优化的活,也顺手接下了优化Redis的活

内存碎片率mem_fragmentation_ratio

查阅相关资料得知,速度过慢很有可能是因为内存不足使用了swap导致。而mem_fragmentation_ratio是一个很明显的是否使用了swap的指标。

mem_fragmentation_ratio的计算公式为

$$MemFragmentationRatio = \frac {UsedMemoryRss} {UsedMemory}$$

而我Redis执行info命令结果为:

# Memory
used_memory:3803742104
used_memory_human:3.54G
used_memory_rss:3531386880
used_memory_rss_human:3.29G
used_memory_peak:3940788176
used_memory_peak_human:3.67G
total_system_memory:33567162368
total_system_memory_human:31.26G
used_memory_lua:37888
used_memory_lua_human:37.00K
maxmemory:21474836480
maxmemory_human:20G
maxmemory_policy:noeviction
mem_fragmentation_ratio:0.93
mem_allocator:jemalloc-3.6.0

而相关资料显示,ratio值正常范围在1~1.5左右。

  • 大于1.5表示,系统分配的内存大于Redis实际使用的内存,Redis没有把这部分内存返还给系统,产生了很多内存碎片。在Redis 4.0版以前,只能通过安全重启解决这个问题。
  • 小于1表示,系统分配的内存小于Redis实际使用的内存,而Redis很有可能在使用Swap了!使用swap是相当影响性能的。

而我这个ratio小于1,那么说明很有可能使用Swap了。

初步尝试

$ free -h
total        used        free      shared  buff/cache   available
Mem:            31G        5.6G        326M         49M         25G         25G
Swap:            0B          0B          0B

woc,没有使用Swap?那是什么鬼,那为啥我的ratio还小于1?

死马当活马医

然后尝试ratio大于1的方法,重启大法尝试。然后发现日志

(error) MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk. Commands that may modify the data set are disabled. Please check Redis logs for details about the error

噫?竟然没有写持久化文件RDB的权限,根据Redis配置文件查询到RDB位置,后访问发现文件夹权限为

层次树

聚类算法之层次聚类

层次聚类

层次聚类(Hierarchical Clustering)是聚类算法的一种,通过计算不同类别的相似度类创建一个有层次的嵌套的树。

层次聚类怎么算

层次聚类分为自底向上自顶向下两种,这里仅采用scikit-learn中自底向上层次聚类法。

  1. 将数据集中每一个样本都标记为不同类
  2. 计算找出其中距离最近的2个类别,合并为一类
  3. 依次合并直到最后仅剩下一个列表,即建立起一颗完整的层次树

以下为看图说话~ 感谢 Laugh’s blog借用下说明图

第一步

把所有数据全部分为不同组

第二步

将相邻最近的两组归为同一组

重复上述操作

重复第二步,直到合并成为一个组,聚类结束

层次树

聚类过程的散点图变化一下,就是我们要的层次图

层次聚类 Python 实现

import numpy as np
from sklearn.cluster import AgglomerativeClustering
data = np.random.rand(100, 3) #生成一个随机数据,样本大小为100, 特征数为3

#假如我要构造一个聚类数为3的聚类器
estimator = AgglomerativeClustering(n_clusters=3)#构造聚类器
estimator.fit(data)

print(estimator.labels_)#获取聚类标签

主函数 AgglomerativeClustering 参数解释

AgglomerativeClustering(affinity='euclidean', compute_full_tree='auto',
                        connectivity=None, linkage='ward', 
                        memory=None, n_clusters=2,
                        pooling_func='deprecated')
  • affinity: 亲和力度量,有 euclidean(欧式距离), l1(L1 范数), l2(L2 范数)
  • compute_full_tree: 通常当训练了n_clusters后,训练过程就会停止,但是如果compute_full_tree=True,则会继续训练从而生成一颗完整的树
  • connectivity: 一个数组或者可调用对象或者None,用于指定连接矩阵
  • linkage: 连接方法:ward(单连接), complete(全连接), average(平均连接)可选
  • memory: 用于缓存输出的结果,默认为不缓存
  • n_clusters: 表示最终要查找类别的数量,例如上面的 2 类
  • pooling_func: 一个可调用对象,它的输入是一组特征的值,输出是一个数

返回值

result

聚类算法之DBSCAN聚类

DBSCAN (Density-Based Spatial Clustering of Applications with Noise) 是一种基于密度的聚类算法,基于密度的聚类寻找被低密度区域分离的高密度区域。常用于异常值或者离群点检测。

DBSCAN 怎么算

当某个点的密度达到算法设定的阈值,则这个点称为核心对象。(即r领域内点的数量小于minPts),其中领域的距离阈值为用户设定值。

若某点p在q的r领域内,且q是核心点,则p-q直接密度可达。若有一个点的序列q0、q1、q2…qK,对任意的qi-qi+1是直接密度可达的,则称q0到qK密度可达。称为密度的传播。

当一个非核心点不能发展下线,则称该点为边界点。若某一点,从任一核心地点出发都是密度不可达的,则称该点为噪声点

DBSCAN 聚类算法实现如下图: DBSCAN算法

当出现奇葩数据时,K-Means 无法正常聚类,而 DBSCAN 完全无问题

DBSCAN

优点:

  1. 与K-Means相比,不需要手动确定簇的个数K,但需要确定邻域r和密度阈值minPts

  2. 能发现任意形状的簇

  3. 能有效处理噪声点(邻域r和密度阈值minPts参数的设置可以影响噪声点)

缺点:

  1. 当数据量大时,处理速度慢,消耗大

  2. 当空间聚类的密度不均匀、聚类间距差相差很大时参数密度阈值minPts和邻域r参数选取困难

  3. 对于高维数据,容易产生“维数灾难”(聚类算法基于欧式距离的通病)

DBSCAN 聚类 Python 实现

# coding=utf-8
"""
Created on 2019/10/12 11:42

@author: EwdAger
"""

import numpy as np
from sklearn.cluster import DBSCAN
from sklearn import metrics
from sklearn.datasets.samples_generator import make_blobs
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt


centers = [[1, 1], [-1, -1], [1, -1]]  # 生成聚类中心点
X, labels_true = make_blobs(n_samples=750, centers=centers, cluster_std=0.4,random_state=0)
# 生成样本数据集

X = StandardScaler().fit_transform(X)
# StandardScaler 标准化处理。且是针对每一个特征维度来做的,而不是针对样本。

# 调用密度聚类  DBSCAN
db = DBSCAN(eps=0.3, min_samples=10).fit(X)
# print(db.labels_)  # db.labels_为所有样本的聚类索引,没有聚类索引为-1
# print(db.core_sample_indices_) # 所有核心样本的索引

core_samples_mask = np.zeros_like(db.labels_, dtype=bool)  # 设置一个样本个数长度的全false向量
core_samples_mask[db.core_sample_indices_] = True #将核心样本部分设置为true
labels = db.labels_

n_clusters_ = len(set(labels)) - (1 if -1 in labels else 0)
# 获取聚类个数。(聚类结果中-1表示没有聚类为离散点)

# 模型评估
print('估计的聚类个数为: %d' % n_clusters_)
print("同质性: %0.3f" % metrics.homogeneity_score(labels_true, labels))  # 每个群集只包含单个类的成员。
print("完整性: %0.3f" % metrics.completeness_score(labels_true, labels))  # 给定类的所有成员都分配给同一个群集。
print("V-measure: %0.3f" % metrics.v_measure_score(labels_true, labels))  # 同质性和完整性的调和平均
print("调整兰德指数: %0.3f" % metrics.adjusted_rand_score(labels_true, labels))
print("调整互信息: %0.3f" % metrics.adjusted_mutual_info_score(labels_true, labels))
print("轮廓系数: %0.3f" % metrics.silhouette_score(X, labels))

# Plot result

unique_labels = set(labels)
colors = [plt.cm.Spectral(each) for each in np.linspace(0, 1, len(unique_labels))]

plt.figure(figsize=(10,6))
for k, col in zip(unique_labels, colors):
    if k == -1:  # 聚类结果为-1的样本为离散点
        # 使用黑色绘制离散点
        col = [0, 0, 0, 1]

    class_member_mask = (labels == k)  # 将所有属于该聚类的样本位置置为true

    xy = X[class_member_mask & core_samples_mask]  # 将所有属于该类的核心样本取出,使用大图标绘制
    plt.plot(xy[:, 0], xy[:, 1], 'o', markerfacecolor=tuple(col), markeredgecolor='k', markersize=14)

    xy = X[class_member_mask & ~core_samples_mask]  # 将所有属于该类的非核心样本取出,使用小图标绘制
    plt.plot(xy[:, 0], xy[:, 1], 'o', markerfacecolor=tuple(col), markeredgecolor='k', markersize=6)

plt.title('Estimated number of clusters: %d' % n_clusters_)
plt.show()

输出

Python异常处理traceback和exc_info

开发过程中一般都会使用traceback将捕获到的异常打印出来。

import traceback

def fake_exception():
    1 / 0

def catch_exception():
    try:
        fake_exception()
    except:
        traceback.print_exc()

catch_exception()

结果

Traceback (most recent call last):
  File ".\test.py", line 9, in catch_exception
    fake_exception()
  File ".\test.py", line 5, in fake_exception
    1 / 0
ZeroDivisionError: integer division or modulo by zero

事实上,traceback里的所有信息都是从exc_info里面获取的。

traceback.print_exc([limit[, file]])
In fact, it uses sys.exc_info() to retrieve the same information in a thread-safe way instead of using the deprecated variables.

那么我们再来看一下exc_info()这个方法。 https://docs.python.org/2/library/sys.html?highlight=sys#module-sys 该方法返回三个值:type, value, traceback.

  • type (异常类别) get the exception type of the exception being handled (a class object)
  • value (异常说明,可带参数) get the exception parameter (a class instance)
  • traceback (traceback对象,包含更丰富的信息) get a traceback object which encapsulates the call stack at the point where the exception originally occurred (a traceback object)

其中traceback中还包含了更为丰富的信息,比如文件名,行号等等。如果觉得系统默认的traceback打印格式不好看的话,可以利用exc_info的返回值自定义格式。

Python读取Excel文件sheet名性能优化

原始版本

直接使用pandas读取整个Excel文件,再从中取列名。这种场景对于小的Excel文件还适用,但数据量上升到10M+时,取个sheet name26s之久。几乎无法忍受。

data = pandas.ExcelFile(file_url)
names = data.sheet_names

优化

查阅资料可知.xlsx文件是一个压缩格式的文件,可以直接通过zipfile读到sheet name等相关信息。所以写如下函数直接取sheet name

def get_sheet_details(file_path):
    sheets = []
    file_name = os.path.splitext(os.path.split(file_path)[-1])[0]
    # 用文件名创建一个临时目录
    directory_to_extract_to = file_name
    os.mkdir(directory_to_extract_to)

    # 提取xlsx文件,因为它只是一个zip文件
    zip_ref = zipfile.ZipFile(file_path, 'r')
    zip_ref.extractall(directory_to_extract_to)
    zip_ref.close()

    # 建立一个临时的workbook文件
    path_to_workbook = os.path.join(directory_to_extract_to, 'xl', 'workbook.xml')
    with open(path_to_workbook, 'r') as f:
        xml = f.read()
        dictionary = xmltodict.parse(xml)
        # 多个sheet
        if isinstance(dictionary['workbook']['sheets']['sheet'], list):
            for sheet in dictionary['workbook']['sheets']['sheet']:
                # 有些版本的sheet是@name有些是@sheetId
                if sheet["@name"]:
                    meta_sheet = sheet["@name"]
                else:
                    meta_sheet = sheet["@sheetId"]
                sheets.append(meta_sheet)
        # 单个sheet
        else:
            sheet_dict = dictionary['workbook']['sheets']['sheet']
            if sheet_dict["@name"]:
                sheets.append(sheet_dict["@name"])
            else:
                sheets.append(sheet_dict["@sheetId"])

    shutil.rmtree(directory_to_extract_to)
    f.close()
    return sheets

使用该种方法,读14M的文件仅需0.04s。(数据都没加载,当然和文件大小无关啦)

k_clusters4

聚类算法之K-Means(K均值)聚类

聚类与分类的区别

分类: 类别是已知的,通过对已知分类的数据进行训练和学习,找到不同类的特征再对未分类的数据进行分类,属于监督学习。

聚类: 事先不知道数据会分为几类,通过聚类分析将数据聚合成几个群体。聚类不需要对数据进行训练和学习。属于无监督学习。

数据输入有标签则为监督学习,否则为无监督学习

k-means 聚类怎么算

  1. 首先输入 k 的值,即希望通过聚类得到 k 个分组;
  2. 从数据集中随机选取 k 个数据点作为初始质心
  3. 对数据中每一个点计算与每一个质心的距离,离哪个质心近,就分为哪一类
  4. 基于当前分好的类,重新计算类中的所有向量的平均值,确定新的质心
  5. 重复3、4步
  6. 直到新的质心与老的质心的距离小于某一个设定的阈值,可以认为达到预期,算法终止。

具体步骤如下图: k-means

k-means 聚类 Python 实现

import numpy as np
from sklearn.cluster import KMeans
data = np.random.rand(100, 3) #生成一个随机数据,样本大小为100, 特征数为3

#假如我要构造一个聚类数为3的聚类器
estimator = KMeans(n_clusters=3)#构造聚类器
estimator.fit(data)#聚类
label_pred = estimator.labels_ #获取聚类标签
centroids = estimator.cluster_centers_ #获取聚类中心
inertia = estimator.inertia_ # 获取聚类准则的总和

主函数 KMeans 参数解释

sklearn.cluster.KMeans(n_clusters=8,
	init='k-means++', 
	n_init=10, 
	max_iter=300, 
	tol=0.0001, 
	precompute_distances='auto', 
	verbose=0, 
	random_state=None, 
	copy_x=True, 
	n_jobs=1, 
	algorithm='auto'
	)
  • n_clusters:簇的个数,即你想聚成几类
  • init: 初始簇中心的获取方法
  • n_init: 获取初始簇中心的更迭次数,为了弥补初始质心的影响,算法默认会初始10次质心,实现算法,然后返回最好的结果。
  • max_iter: 最大迭代次数(因为kmeans算法的实现需要迭代)
  • tol: 容忍度,即kmeans运行准则收敛的条件
  • precompute_distances:是否需要提前计算距离,这个参数会在空间和时间之间做权衡,如果是True 会把整个距离矩阵都放到内存中,auto 会默认在数据样本大于featurs*samples 的数量大于12e6 的时候False,False 时核心实现的方法是利用Cpython 来实现的
  • verbose: 冗长模式(不太懂是啥意思,反正一般不去改默认值)
  • random_state: 随机生成簇中心的状态条件。
  • copy_x: 对是否修改数据的一个标记,如果True,即复制了就不会修改数据。bool 在scikit-learn 很多接口中都会有这个参数的,就是是否对输入数据继续copy 操作,以便不修改用户的输入数据。这个要理解Python 的内存机制才会比较清楚。
  • n_jobs: 并行设置
  • algorithm: kmeans的实现算法,有:auto, full, elkan, 其中 full表示用EM方式实现

具体 Python 实现

# coding=utf-8
"""
Created on 2019/10/9 16:31

@author: EwdAger
"""
import numpy as np
import pandas as pd
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt

data = pd.read_csv(r"F:\MachineLearning\Kmeans\data\testSet.txt", sep='\t',
                   header=0, dtype=str, na_filter=False).astype(np.float)

k = 4
clt = KMeans(n_clusters=k)
clt.fit(data)
cents = clt.cluster_centers_  # 获取聚类中心
labels = clt.labels_  # 获取聚类标签
colors = ['b', 'g', 'r', 'k', 'c', 'm', 'y', '#e24fff', '#524C90', '#845868']

# 画图
for i in range(k):
    # 获取i组数据的index
    index = np.nonzero(labels == i)[0]
    x0 = data.iloc[data.index.isin(index), 0].tolist()
    x1 = data.iloc[data.index.isin(index), 1].tolist()
    y_i = i
    # 给第i组数据涂上颜色,进行分组
    for j in range(len(x0)):
        plt.text(x0[j], x1[j], str(y_i), color=colors[i], fontdict={'weight': 'bold', 'size': 6})
    # 绘制该组质心
    plt.scatter(cents[i, 0], cents[i, 1], marker='x', color=colors[i], linewidths=7)

plt.axis([-7, 7, -7, 7])
plt.show()

结果如下: k_clusters4