在学习人工智能之前,首先,我们需要系统性的了解一下人工智能的分类以及各个部分的组成

人工智能主要由以下几个部分组成:

{

​ EC(嵌入与压缩)[GA(遗传算法)、NSGA Ⅱ(多目标优化算法)]

​ ML(机器学习)[K-means(K均值聚类)、regression(回归)、拟合]

​ DL(深度学习)[MLP(多层感知机)、CNN(卷积神经网络)、Transformer]

​ RL(强化学习)[DQN(结合深度学习与强化学习的DQN算法)、MAPPO(多智能体强化学习)]

}

在了解了分类之后我们就可以先从简单的开始学习,先从K-means开始学习,K-means就是K均值聚类算法,基于相似性度量将相近的样本归为同一个子集,使得相同子集中各元素间差异性最小,而不同子集间的元素差异性最大[1],这就是(空间)聚类算法的本质。而K-Means正是这样一种算法的代表。

v2-43657c99d27a6fd0344ad9a958bd601b_1440w

如图所示,我们利用mnist数据集进行举例,mnist数据集是一个非常常用的数据集,也是非常适合新手入门开始学习的工具,这一数据集中包含了7w张手写的0~9数字图片,这些图片都是2828像素的,并且是黑白单通道的,怎么来理解黑白单通道以及28 * 28像素呢,我们知道,三原色可以调和出任意一种颜色,所以彩色图片一般是有RGB三个通道,而黑白图片则是只有一个通道,在计算机的视角里,图片都是由一些矩阵组成的,矩阵中包含着每个点的颜色(用数字0~255表示),彩色图片由三个这样的矩阵组成,黑白图片则是由一个矩阵组成,所以我们在计算机中读取图片,实际上是读取了包含这个图片信息的矩阵,这样以来我们就能更加方便的理解什么是像素了,像我们常说的2k就是25601440,在计算机的视角中2k的图片也就是是一个2560 * 1440的矩阵,上文中说到的mnist数据集中包含的图片就是28 * 28像素的,所以比较适合新手来进行,因为需要使用到的运算资源比较小

知道了以上的前情提要之后我们就可以考虑如何使用K-means算法来讲Mnist数据集进行聚合分类了,我们知道这个数据集中是0~9的图片,那我们应当如何让计算机也认识他们并将他们正确分类呢,我们接下来讲一下K-means的基本步骤。

第一步:从需要分类的样本中选取K个对象作为我们初始的聚类中心。

K是我们需要提前设定好的,基本上是我们需要分成几类我们就将这个数字设定为几,再讲一下聚类中心,聚类中心可以理解为我们在二维坐标系中点的一个点,我们当前认为这个点就是这一类中最标准的点,接下来的分类就需要参照这个点来进行。

第二步:分别计算每个样本点到各个聚类中心的距离,并逐个分配到距离其最近的簇中;

是否可以和剧烈中心分为一簇就是看当前对象的点和聚类中心的距离是否足够近,当距离小于一定值时,我们就认为当前对象可以分为一簇。

第三步:所有对象分配完成之后,更新k个类中心位置,类中心定义为簇内所有对象在各个维度的均值;

到这一步就初步确定下来这一类的中心点了,接下来就是要确保这一步确定下来的是否需要调整。

第四步:与前一次计算得到的K个聚类中心比较,如果聚类中心发生变化,转至步骤二,否则转至步骤五;

这一步就是决定之前的分类是否出现了问题并进行调整。

第五步:当类中心不再发生变化,停止并输出聚类结果,然后整理我们所需要的信息,如各个样本所属的类等等,进行后续的统计和分析。v2-4c51640e85ee3cbd2bf121cd88c48417_1440w

容易看出,聚类结束之前,类中心会不断移动而随着类中心的移动样本的划分情况也会持续发生改变。对此,有这样一个牧师-村民模型可以帮助大家理解这个聚类的过程:

四位牧师**(即K=4)去郊区布道,一开始牧师们随意选了几个布道点(即初始聚类中心),并且把这几个布道点的情况公告给了郊区所有的居民,于是每个居民到离自己家最近的布道点去听课(样本分配)
听课之后,大家觉得距离太远了,于是每个牧师统计了一下自己的课上所有的居民的地址,搬到了所有地址的中心地带,并且在海报上更新了自己的布道点的位置
(聚类中心的迁移和更新)
牧师每一次移动不可能离所有人都更近,有的人发现A牧师移动以后自己还不如去B牧师处听课更近,于是每个居民又去了离自己最近的布道点……
就这样,牧师每个礼拜更新自己的位置
(聚类迭代),居民根据自己的情况选择布道点,最终稳定了下来(聚类完成)**。

很自然地,最终的聚类结果应该是:在同一位牧师布道处听课的村民们,他们的住址应该最相近,同时他们离在其他牧师处听课的村民们的家都相对较远。这与我们“簇内差异小,簇间差异大”的聚类目标不谋而合。

知道了原理之后我们不妨来考虑一下这个算法存在的问题:

算法所需要预设的参数至少有2个:聚类个数K初始聚类中心点。其中,最为人“诟病”的当属合适K值的选取。现实中,K值的选定通常是难以估计的,很多时候,我们并不知道给定的数据集应该分成多少个类别才最合适,这也在一定程度上影响和限制了K-Means的应用合理性

聚类的目标是使得每个样本点到距离其最近的聚类中心的总误差平方和(也即聚类的代价函数,后文记作SSE)尽可能小v2-3c4349b45be1fc90e18e6151cf24ccb1_1440w

理论上随着K的增加,SSE会单调递减,因为类数的增加意味着总有一部分样本点会因为归属到新的类簇而节约下一段距离,直到K=N时,情况将演变为每个样本自成一类,此时SSE值就会降为0。可是这显然不是我们希望的。

根据学者们的长期实践经验,K值最大不应超过样本量的开平方根,即Kmax≤√N。而确定了范围后,最优K值又应该怎么判断?一种简单的思路是:试图找到某一个K值,要求当K大于该值时,SSE的下降变化幅度(或速度)明显变小。换句话说,当K超过某一个数后,每个类簇的聚合程度不再获得显著提升,此时我们就可以认为已找到最佳K的取值。这也是**手肘法:通过画出不同K值与SSE值的折线图,若SSE值下降过程中存在“肘点”(下降速度骤减的拐点处),该点所对应的K值即合适的聚类数**。不过遗憾的是,若SSE的下降是均匀的,传统的肘部图法也就失灵了。v2-21a006110167e3ced240c636630ef80d_1440w

虽然目前不少学者对传统的手肘法和其他检验聚类有效性的函数提出了改进,但对于最优K值确定,仍未有最佳的方案,对于我们学习者,更多的时候还是需要凭借常识经验,或者从实际应用场景出发,与算法互相“磨合”

另一个需要预设的参数,是初始聚类中心点

常用分析软件中的功能模块或函数包,基本上都已经代替使用者们自动预设了随机初始点,只需填入目标K值,就可以跑动算法。但实际上,K-Means对初始聚类中心的位置十分敏感,每次迭代,初始点的不同往往会导致不同的聚类结果。此外过于临近的初始中心点,有时还会导致模型的收敛时间变长(即Step4中迭代时间变长)。一种简单粗暴的解决方式是,选择不同的初始聚类中心,多次运行算法,挑出聚类效果更佳(SSE更小)、解释性更强的一组结果。

当然了,我们或许更想知道算法上的改进手段。一种常见的优化方法是采用最大距离法,如:首先选取数据集中距离最大的两个点作为初始聚类中心,将剩余数据对象依据到聚类中心点距离的远近分配到相应的簇中,并更新聚类中心,然后继续寻找与聚类中心距离最远的点作为下一个中心点……

v2-aa3f886ef79782ff287eb0b45bf75a2d_1440w

在知道了K-means算法的核心思想之后我们就可以尝试通过手写算法来真正的了解并且掌握其核心思想,不通过调库的编写更能让你明白这一算法的核心,同时也更能够让你掌握到知识。

下面简单讲述一下代码的编写过程以及遇到的问题,由于Python本人在本科期间从未使用和了解过,所以在编写的过程中有很多的问题与疑问,下文中本人的理解也可能存在一定的问题,请批判性的查看

在算法执行完毕后需要绘制图表,用到matplotlib,但是由于字体的调用与编码问题,不做设定的话会导致绘制出来的图表中,中文字体被方框替代,无法正常显示,而为了解决这一问题,我们需要进行设定

from pylab import mpl
# 设置matplotlib支持中文显示
mpl.rcParams['font.sans-serif'] = ['SimHei']
mpl.rcParams['axes.unicode_minus'] = False

之后我们就需要读取数据集中的信息了,mnist是一个很出名的数据集,我们可以直接在网上下载到,mnist数据集中包含四个文件,分别为

train-images-idx3-ubyte.gz:训练集图片(9912422字节),55000张训练集,5000张验证集

train-labels-idx1-ubyte.gz:训练集图片对应的标签(28881字节),

t10k-images-idx3-ubyte .gz:测试集图片(1648877字节),10000张图片

t10k-labels-idx1-ubyte.gz:测试集图片对应的标签(4542字节)

我们使用如下代码从本地加载mnist数据集

def load_local_mnist(path, kind='train'):
    images_path = os.path.join(path, f'{kind}-images-idx3-ubyte.gz')
    labels_path = os.path.join(path, f'{kind}-labels-idx1-ubyte.gz')

    with gzip.open(images_path, 'rb') as img_file:
        img_file.read(16)  # 跳过文件头
        buffer = img_file.read()
        images = np.frombuffer(buffer, dtype=np.uint8)
        images = images.reshape(-1, 28 * 28)  # 展平为784维向量

    with gzip.open(labels_path, 'rb') as lbl_file:
        lbl_file.read(8)  # 跳过文件头
        buffer = lbl_file.read()
        labels = np.frombuffer(buffer, dtype=np.uint8)

    return images / 255.0, labels  # 归一化处理
# 请修改为你的本地MNIST数据集所在的目录
mnist_path = 'D:\desk\MNIST'
# 加载并准备数据
x_train, y_train = load_local_mnist(mnist_path, kind='train')
x_test, y_test = load_local_mnist(mnist_path, kind='t10k')

MNIST 数据集的图像文件(如 train-images-idx3-ubyte)是二进制格式,前 16 字节是文件头(包含魔术数、图像数量、行数、列数等信息),后面的字节全部是像素数据(每个像素用 1 个字节表示)。这样读取完成之后,就成功将mnist数据集读取为了NumPy数组,每个像素点都是0~255的像素值。

之后就是相关功能的定义,其中包括:

计算准确率的函数:

# 辅助函数:计算准确率
def calculate_accuracy(true_labels, pred_labels):
    correct = 0
    for t, p in zip(true_labels, pred_labels):
        if t == p:
            correct += 1
    return correct / len(true_labels)

映射聚类标签到实际数字标签的函数:

# 辅助函数:映射聚类标签到实际数字标签
def map_cluster_labels(cluster_labels, true_labels):
    mapping = {}
    for i in range(len(np.unique(cluster_labels))):
        # 找到属于当前簇的所有样本的真实标签
        cluster_samples = []
        for j in range(len(cluster_labels)):
            if cluster_labels[j] == i:
                cluster_samples.append(true_labels[j])

        # 找到出现次数最多的真实标签作为映射
        label_counts = {}
        for label in cluster_samples:
            if label in label_counts:
                label_counts[label] += 1
            else:
                label_counts[label] = 1

        # 确定最大计数的标签
        max_count = -1
        best_label = 0
        for label, count in label_counts.items():
            if count > max_count:
                max_count = count
                best_label = label

        mapping[i] = best_label

    return mapping

然后是主体部分,这部分实现了计算两向量之间的距离、初始化聚类中心、显示初始中心的几个随机特征值、分配样本到簇中、计算并迭代聚类中心、计算准确率、使用训练好的模型预测图片所属的簇,并计算准确率等等

# 2. 带准确率跟踪的K-means算法实现
class KMeansWithAccuracyTracking:
    def __init__(self, n_clusters, max_iter, tol):
        self.n_clusters = n_clusters  # 聚类数量
        self.max_iter = max_iter  # 最大迭代次数
        self.tol = tol  # 收敛阈值
        self.centroids = None  # 聚类中心
        self.labels = None  # 样本所属簇标签
        self.cluster_sizes = None  # 每个簇的样本数量
        self.accuracy_history = []  # 记录每次迭代的准确率
        self.iteration_count = 0  # 迭代次数计数

    # 计算两个向量之间的欧氏距离
    def _euclidean_distance(self, a, b):
        distance = 0.0
        for i in range(len(a)):
            distance += (a[i] - b[i]) ** 2
        return np.sqrt(distance)

    # 初始化聚类中心并打印信息
    def _initialize_centroids(self, X):
        n_samples = X.shape[0]
        n_features = X.shape[1]  # 获取特征总数(784)
        print(f"\n初始化{self.n_clusters}个聚类中心...")

        # 随机选择不重复的索引
        indices = []
        while len(indices) < self.n_clusters:
            idx = np.random.randint(0, n_samples)
            if idx not in indices:
                indices.append(idx)

        # 根据索引选择初始中心(修改前的版本)
        # centroids = []
        # for i, idx in enumerate(indices):
        #     centroids.append(X[idx])
        #     if i < 10 or i == self.n_clusters - 1:  # 显示10个中心的部分信息
        #         # 做如下处理方可显示归一化后的特征值的小数部分,而不是显示一串0
        #         # print(f"  初始中心 {i}: 样本索引={idx}, 前5个特征值={X[idx][:5]}...")
        #         features_str = [f"{val:.10f}" for val in X[idx][:5]]
        #         print(f"  初始中心 {i}: 样本索引={idx}, 前5个特征值=[{', '.join(features_str)}]...")

        #修改之后的版本,修改之后修复了每次显示特征值都是0的问题,能更真实地展示特征值的分布情况,避免因固定选择边缘特征而产生的误解。
        centroids = []
        for i, idx in enumerate(indices):
            centroids.append(X[idx])

            # 随机选择5个特征索引(0-783之间)
            random_feature_indices = np.random.choice(n_features, 5, replace=False)
            # 提取并格式化这5个特征值
            features = [f"{X[idx][j]:.6f}" for j in random_feature_indices]
            # 打印时同时显示特征索引,方便对照
            print(f"  初始中心 {i}: 样本索引={idx}, 随机5个特征值(索引{random_feature_indices})={features}...")

        return np.array(centroids)


    # 将样本分配到最近的簇并打印进度
    def _assign_clusters(self, X):
        # 初始化样本数量相同的列表数值为0,方便后续添加标签
        n_samples = X.shape[0]
        labels = [0] * n_samples
        print(f"\n分配{n_samples}个样本到{self.n_clusters}个簇中...")

        # 记录每个簇的样本数量
        self.cluster_sizes = [0] * self.n_clusters

        # 逐个样本计算距离并分配
        for i in range(n_samples):
            # 设定一个参照,将初始设为最大,之后可以逐步缩小,并且确定了最佳答案
            min_dist = float('inf')
            best_cluster = 0
            # 计算训练集中每个向量与初始中心的距离
            for j in range(self.n_clusters):
                dist = self._euclidean_distance(X[i], self.centroids[j])
                if dist < min_dist:
                    min_dist = dist
                    best_cluster = j

            labels[i] = best_cluster
            self.cluster_sizes[best_cluster] += 1

            # 打印进度
            if (i + 1) % 1000 == 0 or i + 1 == n_samples:
                print(f"  已处理 {i + 1}/{n_samples} 个样本 ({(i + 1) / n_samples * 100:.1f}%)")

        # 打印每个簇的样本数量
        print("  簇样本数量分布:")
        for i in range(self.n_clusters):
            print(f"    簇 {i}: {self.cluster_sizes[i]} 个样本 ({self.cluster_sizes[i] / n_samples * 100:.1f}%)")

        return np.array(labels)

    # 计算新的聚类中心并打印变化
    def _update_centroids(self, X, iter_num):
        n_features = X.shape[1]
        new_centroids = np.zeros((self.n_clusters, n_features))
        counts = [0] * self.n_clusters

        # 累加每个簇的所有样本
        for i in range(X.shape[0]):
            cluster = self.labels[i]
            for j in range(n_features):
                new_centroids[cluster, j] += X[i, j]
            counts[cluster] += 1

        # 计算平均值作为新的中心
        print("\n更新聚类中心:")
        for i in range(self.n_clusters):
            if counts[i] > 0:
                for j in range(n_features):
                    new_centroids[i, j] /= counts[i]

                # 计算与旧中心的距离
                movement = self._euclidean_distance(self.centroids[i], new_centroids[i])

                # 打印部分信息(修改前的版本)
                # if i < 5 or i == self.n_clusters - 1:
                #     # 由于进行了归一化处理,特征值呈现小数形式,所以为了能够显示出特征值的小数部分,做如下处理
                #     # print(f"  中心 {i}: 移动距离={movement:.6f}, 前5个特征值={new_centroids[i][:5]}...")
                #     features_str = [f"{val:.6f}" for val in new_centroids[i][:5]]
                #     print(f"  中心 {i}: 移动距离={movement:.6f}, 前5个特征值=[{', '.join(features_str)}]...")

                #修改之后的版本,能更真实地展示特征值的分布情况,避免因固定选择边缘特征而产生的误解。
                if i < 5 or i == self.n_clusters - 1:
                    # 随机选择5个特征索引
                    random_feature_indices = np.random.choice(n_features, 5, replace=False)
                    # 提取并格式化这5个特征值
                    features = [f"{new_centroids[i][j]:.6f}" for j in random_feature_indices]
                    print(
                        f"  中心 {i}: 移动距离={movement:.6f}, 随机5个特征值(索引{random_feature_indices})={features}...")


        return new_centroids

    # 检查是否收敛
    def _is_converged(self, old_centroids, new_centroids):
        total_movement = 0.0
        for i in range(self.n_clusters):
            total_movement += self._euclidean_distance(old_centroids[i], new_centroids[i])

        avg_movement = total_movement / self.n_clusters
        print(f"\n本轮迭代总移动距离: {total_movement:.6f}, 平均移动距离: {avg_movement:.6f}")
        return avg_movement < self.tol

    # 计算当前迭代的准确率
    def _calculate_current_accuracy(self, true_labels):
        # 建立当前聚类结果到真实标签的映射
        mapping = map_cluster_labels(self.labels, true_labels)
        # 应用映射
        predicted_labels = [mapping[label] for label in self.labels]
        # 计算准确率
        return calculate_accuracy(true_labels, predicted_labels)

    # 训练模型并打印过程
    def fit(self, X, true_labels):
        print("\n===== 开始K-means聚类训练 =====")
        print(f"聚类数量: {self.n_clusters}, 最大迭代次数: {self.max_iter}, 收敛阈值: {self.tol}")

        # 初始化聚类中心
        self.centroids = self._initialize_centroids(X)

        # 迭代更新
        for iter_num in range(self.max_iter):
            self.iteration_count = iter_num + 1
            print(f"\n===== 第 {self.iteration_count}/{self.max_iter} 次迭代 =====")

            # 分配样本到簇
            self.labels = self._assign_clusters(X)

            # 计算当前准确率并记录
            current_accuracy = self._calculate_current_accuracy(true_labels)
            self.accuracy_history.append(current_accuracy)
            print(f"\n本轮迭代准确率: {current_accuracy:.4f}")

            # 计算新的中心
            new_centroids = self._update_centroids(X, iter_num)

            # 检查是否收敛
            if self._is_converged(self.centroids, new_centroids):
                print(f"\n===== 训练完成 =====")
                print(f"在第 {self.iteration_count} 次迭代达到收敛条件")
                self.centroids = new_centroids
                break

            # 更新中心
            self.centroids = new_centroids

            # 如果是最后一次迭代
            if iter_num + 1 == self.max_iter:
                print(f"\n===== 训练完成 =====")
                print(f"达到最大迭代次数 {self.max_iter},停止训练")

        return self

    # 预测新样本的簇
    def predict(self, X):
        print(f"\n预测{len(X)}个样本的聚类标签...")
        predictions = []
        for i, sample in enumerate(X):
            min_dist = float('inf')
            best_cluster = 0
            for j in range(self.n_clusters):
                dist = self._euclidean_distance(sample, self.centroids[j])
                if dist < min_dist:
                    min_dist = dist
                    best_cluster = j
            predictions.append(best_cluster)

            # 打印进度
            if (i + 1) % 100 == 0 or i + 1 == len(X):
                print(f"  已预测 {i + 1}/{len(X)} 个样本")

        return np.array(predictions)

    # 绘制准确率变化折线图
    def plot_accuracy_history(self):
        plt.figure(figsize=(10, 6))
        plt.plot(range(1, len(self.accuracy_history) + 1), self.accuracy_history, 'o-', color='b')
        plt.title('K-means聚类准确率随迭代次数的变化')
        plt.xlabel('迭代次数')
        plt.ylabel('准确率')
        plt.grid(True, linestyle='--', alpha=0.7)
        plt.xticks(range(1, len(self.accuracy_history) + 1))
        plt.ylim(0, 1)  # 准确率范围在0到1之间

        # 在每个点上标注准确率值
        for i, acc in enumerate(self.accuracy_history):
            plt.text(i + 1, acc, f'{acc:.4f}', ha='center', va='bottom')

        plt.tight_layout()
        plt.show()

完成了这部分就基本将K-means算法完成了实现,接下来是将我们训练成果保存下来,K-means需要保存的核心是三个:

聚类中心(centroids:形状为(10, 784)的数组,是模型判断新样本所属簇的关键依据。

簇标签映射(label_mapping:字典{簇编号: 数字标签},用于将无意义的簇编号转换为实际数字。

聚类数量(n_clusters:固定为 10(对应 10 个数字),确保预测时使用正确的簇数量。

以下是保存模型的代码:

def save_kmeans_model(model, mapping, save_path):
    # 创建保存目录(如果不存在)
    os.makedirs(os.path.dirname(save_path), exist_ok=True)

    # 需要保存的核心参数
    model_data = {
        'centroids': model.centroids,  # 聚类中心
        'n_clusters': model.n_clusters,  # 聚类数量
        'label_mapping': mapping,  # 簇到数字的映射关系
        'tol': model.tol  # 收敛阈值(可选)
    }

    # 序列化保存到文件
    with open(save_path, 'wb') as f:
        pickle.dump(model_data, f)
    print(f"模型已保存到 {save_path}")


# 保存模型(例如保存到models目录下)
save_path = 'D:\desk\MNIST\model'
save_kmeans_model(kmeans, label_mapping, save_path)

再往后就是把需要用到的图表用已经有的数据绘制出来即可

# 7. 绘制混淆矩阵
def plot_confusion_matrix(true_labels, pred_labels):
    # 初始化混淆矩阵
    cm = np.zeros((10, 10), dtype=int)
    for t, p in zip(true_labels, pred_labels):
        cm[t][p] += 1

    # 绘制热力图
    plt.figure(figsize=(10, 8))
    plt.imshow(cm, interpolation='nearest', cmap=plt.cm.Blues)
    plt.title('K-means聚类混淆矩阵')
    plt.colorbar()

    # 添加坐标轴标签
    tick_marks = np.arange(10)
    plt.xticks(tick_marks, range(10))
    plt.yticks(tick_marks, range(10))
    plt.xlabel('预测标签')
    plt.ylabel('真实标签')

    # 在矩阵中显示数值
    thresh = cm.max() / 2.
    for i in range(cm.shape[0]):
        for j in range(cm.shape[1]):
            plt.text(j, i, format(cm[i, j], 'd'),
                     horizontalalignment="center",
                     color="white" if cm[i, j] > thresh else "black")

    plt.tight_layout()
    plt.show()

我们在了解了K-means算法之后,不难发现这个方法处理mnist数据集的效果可以说是非常的不好了,往往处理1W张图片迭代50次之后的准确率只有60%左右,那么我们有什么办法去提高我们的准确率呢,首先我们想到了K-means++。它是传统K-Means的改良版,同样是基于最大距离,这里结合加权概率的思想优化了对K个初始中心的选取,使得在选取第n+1(n+1<k)个聚类中心时,距离当前n个聚类中心越远的点会有更高的概率被选为第n+1个聚类中心

K-means++ 的核心思想是:让初始聚类中心尽可能远离彼此,具体初始化步骤如下:

  1. 从样本集中随机选择 1 个样本作为第一个初始中心;
  2. 对剩余每个样本,计算它与已选中心的最短距离 d(x)(即到最近一个中心的距离);
  3. 选择下一个中心时,样本被选中的概率与 d(x)2 成正比(距离越远的样本被选中概率越高);
  4. 重复步骤 2-3,直到选够 k 个初始中心;
  5. 后续迭代过程与传统 K-means 一致(分配样本 → 更新中心 → 检查收敛)。

并且不需要修改太多的地方就能够达到升级算法的目的,与原算法相比较,仅修改了初始化聚类中心的部分,因为与原算法相比也仅仅是这一块有差异,初始化代码修改如下:

# 核心修改:K-means++初始化方法
    def _initialize_centroids(self, X):
        n_samples, n_features = X.shape
        centroids = np.zeros((self.n_clusters, n_features))
        
        # 步骤1:随机选择第一个中心
        first_idx = np.random.randint(n_samples)
        centroids[0] = X[first_idx]
        print(f"K-means++ 初始化:选择样本 {first_idx} 作为第一个中心")
        
        # 步骤2:按概率选择剩余中心
        for i in range(1, self.n_clusters):
            # 计算所有样本到最近已选中心的距离
            distances = []
            for x in X:
                # 找最近的中心
                min_dist = min([np.linalg.norm(x - c) for c in centroids[:i]])
                distances.append(min_dist)
            distances = np.array(distances)
            
            # 按距离平方的概率分布选择下一个中心
            # 避免距离为0导致概率为0(处理重复样本)
            distances = np.maximum(distances, 1e-10)
            # 概率与距离平方成正比
            probabilities = distances **2 / np.sum(distances** 2)
            
            # 按概率选择一个样本作为新中心
            next_idx = np.random.choice(n_samples, p=probabilities)
            centroids[i] = X[next_idx]
            print(f"K-means++ 初始化:选择样本 {next_idx} 作为第 {i+1} 个中心")
        
        return centroids

在尝试完K-means++之后我们可以尝试另一个方面去优化,那就是使用PCA对mnist数据集的数据进行降维,在使用这个方法之前,我们先了解一下什么是PCA算法。

PCA(主成分分析)是一种常用的降维方法,核心思想是在保留数据主要信息的前提下,将高维数据映射到低维空间。它通过寻找数据中方差最大的方向(主成分),用这些方向组成的新坐标系来表示数据,从而减少特征维度。

  • 高维数据往往存在冗余(不同特征间有相关性),PCA 通过去除冗余信息实现降维
  • 降维后的数据能加快后续算法(如 K-means)的计算速度,同时可能提升模型效果(减少噪声影响)

用数学的原理去理解,会更加方便我们理解这一算法

# 方法步骤

  1. 数据标准化:将所有特征归一化到相同尺度(MNIST 已归一化,可省略)
  2. 计算协方差矩阵:描述特征间的相关性
  3. 求解协方差矩阵的特征值和特征向量:特征值表示对应特征向量的重要性
  4. 选择主成分:选取前 k 个特征值最大的特征向量,组成投影矩阵
  5. 数据映射:将原始数据与投影矩阵相乘,得到降维后的低维数据

上面这五条是比较官方的话术,用通俗一点的去理解或者说用数学语言去了解这个就是如下的几步:

  1. 将原始数据按列组成 n 行 m 列矩阵 X;
  2. 将 X 的每一行进行零均值化,即减去这一行的均值;
  3. 求出协方差矩阵C=(1/m)*XXᵀ;
  4. 求出协方差矩阵的特征值及对应的特征向量;
  5. 将特征向量按对应特征值大小从上到下按行排列成矩阵,取前 k 行组成矩阵 P;
  6. Y=PX即为降维到 k 维后的数据。

之后我们再使用降维之后的数据来进行之前的运算和聚类即可。

下面是使用MLP来对MNIST数据集进行分类,首先讲一下遇到的问题,就是在环境安装上面,因为实验室在2l有服务器,CPU是9950x显卡是一张4090和一张4090D,那么在完成了用户分配之后就需要完成远程环境安装以及代码部署,那么首先我们使用ssh连接远程服务器,可以用到VScode或者PyCharm都可以,先讲VScode,我们需要在左侧边栏上面点击插件按钮安装以下拓展插件屏幕截图 2025-10-22 191656

安装完成之后我们点击左侧边栏的Remote Exployer然后点击SSH右边的齿轮按钮屏幕截图 2025-10-22 192128

选择第一项进入到配置页面,

然后按照以下格式配置给你的服务器信息

屏幕截图 2025-10-22 192440

host和hostname就是给你的服务器地址,port就是端口,User就是用户名,配置完成之后保存屏幕截图 2025-10-22 192602

双击这里就可以连接了,之后在上方输入给你的密码就完成了服务器的连接,这是VScode的SSH连接方法。

下面讲PyCharm的方法,打开PyCharm之后在上方的选项栏中选择工具,然后找到部署,在部署中选择配置,打开配置页面,然后点击左上角的加号新建连接,连接类型选择SFTP,名称可以随意输入,屏幕截图 2025-10-22 193253

然后点击SSH右边的三个点新建配置

屏幕截图 2025-10-23 151257

按照给你的信息去填服务器ip地址、端口、用户名、密码之后可以测试一下连接就好了,记得不要勾选使用rsync下载/上传/同步那个选项,然后点确定就好了。

然后在使用远程命令行对你的虚拟环境进行安装,首先我们需要安装conda环境,使用wget命令

在官网安装conda

wget https://repo.anaconda.com/archive/Anaconda3-2023.09-0-Linux-x86_64.sh

可以自己去官网找到最新的版本替换

然后使用命令bash Anaconda3-2023.09-0-Linux-x86_64.sh 来进行安装,之后使用命令 vim ~./bashrc

在最后一行添加

export PATH="/home/username/anaconda3/bin:$PATH"将username替换为你的用户名即可

然后输入

conda config --set auto_activate_base true 在终端启动时默认使用base环境,之后输入nvidia-smi来查看显卡支持的cuda版本,之后我们创建一个常用的虚拟环境

conda create -n name python=3.10

这里我们创建的是一个叫做name的python版本为3.10的环境,name可以自行修改,然后我们进入这个环境

conda activate name

然后在这里安装我们所需要的东西我们首先安装一下pip的国内镜像源

pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/

这个是阿里的镜像源

然后我们安装一下pytorch,我们打开pytorch的官网PyTorch

点击get start之后再选择界面选择我们系统的信息和之前看到的cuda的版本

屏幕截图 2025-10-23 153420

复制下面的那一行命令,然后在远程终端中输入即可,等待安装结束之后这样我们就完成了pytorch的安装,然后我们安装CUDA,这里我们因为是没有su权限的所以我们安装起来要麻烦一点,我们使用

cat /proc/version来查看服务器的信息,然后进入CUDA Toolkit Archive | NVIDIA Developer

CUDA官网根据之前使用nvidia-smi看到的最高支持的CUDA版本来进行选择,屏幕截图 2025-10-23 153906

选择好之后我们点击那个版本然后在接下来的界面选择我们服务器的信息

屏幕截图 2025-10-23 155133

然后我们在远程终端中输入下面的wegt指令,但是可能存在问题,我们可能会遇到无法建立SSL连接从而无法下载,那么我们就先在我们本地下载这个文件,然后给服务器传输过去,我使用的是Xftp软件进行的传输,可以自行搜索一下,软件使用还是很简单的,传输过去之后我们看到下面的是需要SU权限的但是我们并没有,那么我们就在我们的用户名之下自行建立一个文件夹叫做CUDA12.4.1,我的路径是home/XXX/CUDA12.4.1然后将SUDO去掉即可,直接运行sh cuda_12.4.1_550.54.15_linux.run,然后按照安装的过程进行,首先输入accept,之后在选项中取消掉Driver的选项,然后进入最下方的Options屏幕截图 2025-10-23 155731

然后选择第二个Toolkit Options屏幕截图 2025-10-23 155806

然后选择第一个修改Toolkit路径,修改为你刚刚新建的那个文件夹路径即可,然后Library的路径也修改为你新建的那个文件夹,之后选择install安装即可。

安装完成之后我们接着修改bashrc 输入vim ~/.bashrc

最后插入这三行

export CUDA_HOME=$CUDA_HOME:/home/XXX/CUDA12.4.1

export PATH="/home/XXX/CUDA12.4.1/bin:$PATH"

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/XXX/CUDA12.4.1/lib64

之后要去官网下载CUDNNcuDNN Archive | NVIDIA Developer

注册好账号之后就下载需要的版本,屏幕截图 2025-10-23 160248先下载到本地,然后在本地解压,解压两次,直到出现子文件夹屏幕截图 2025-10-23 160308

之后在服务器端新建一个文件夹叫Cudnn使用Xftp将解压后的所有文件都上传到那里,然后是复制文件,在终端中运行

cp /home/XXX/Cudnn/include/cudnn.h /home/XXX/CUDA12.4.1/include/

cp /home/XXX/Cudnn/lib/libcudnn* /home/XXX/CUDA12.4.1**/lib64/

记得修改为自己的名称

然后是权限的修改

chmod a+r /home/XXX/CUDA12.4.1/include/cudnn*.h

chmod a+r home/XXX/CUDA12.4.1/lib64/libcudnn*

第二行报错时修改为这条命令执行

chmod a+r /home/XXX/CUDA12.4.1/targets/x86_64-linux/lib/libcudnn*

之后就安装完成了,可以执行nvcc -V来验证是否安装成功

接着我们安装一些深度学习常用的工具包

pip install torch_geometric

pip install pyg_lib torch-scatter torch-sparse torch-cluster torch-spline-conv -f https://data.pyg.org/whl/torch-2.7.0+cu128.html --force-reinstall

pip install pandas scikit-learn xgboost jupyterlab tqdm transformers sentence-transformers shap

conda install matplotlib seaborn

完成之后就算是将环境彻底安装好了,之后再需要什么再安装一下就好了

接着我们配置PyCharm来使得我们可以在服务器上跑我们的项目,我们点击第一栏文件,然后选择设置,在设置里找到Python下面的解释器,然后点击添加解释器,然后点击基于ssh

aaa

屏幕截图 2025-10-23 161030

然后选择现有,在ssh服务器里找到我们之前配置过的配置,然下一步屏幕截图 2025-10-23 161129

然后选择现有在解释器那里选择右边的三个点在远程服务器那里选择路径,选择我们刚刚配置好的虚拟环境即可,我的路径是/home/XXX/anaconda3/envs/010/bin/python3,你修改为自己的即可

然后再下面的同步文件夹那里屏幕截图 2025-10-23 161445

本地路径就是你在本地存放这个Python项目的路径,右边的是你要同步在远程服务器存放项目的路径,建议在自己的文件夹下创建一个文件夹存放自己的项目我的远程路径是/home/XXX/001你可以自己创建一个文件夹然后修改为那个路径即可,然后点击确定,如果配置成功我们查看右下角就可以发现屏幕截图 2025-10-23 161717

是这样显示的,那么到这里就完成了我们所有的环境配置,你可以在远程服务器上运行你的项目了,可喜可贺